chore: remove PR documentation and unnecessary markdown files

This commit is contained in:
diegosouzapw
2026-03-24 10:33:25 -03:00
parent d68143e63d
commit b717a02394
9 changed files with 9 additions and 1826 deletions

View File

@@ -1,294 +0,0 @@
# Fixes Applied to PR #550 - Bot Review Responses
## Summary
Addressed all 4 WARNING issues identified by **kilo-code-bot** automated review.
---
## Issue #1: Potential undefined access - `cred.password` could be undefined
**File**: `src/lib/zed-oauth/keychain-reader.ts` (Line 99)
**Problem**: `cred.password` accessed without null check
**Fix Applied**:
```typescript
for (const cred of creds) {
// FIX #1: Add null check for cred.password
if (!cred.password) {
console.debug(`Skipping credential with missing password: ${pattern}/${cred.account}`);
continue;
}
credentials.push({
provider: extractProviderFromService(pattern),
service: pattern,
account: cred.account,
token: cred.password,
});
}
```
**Result**: ✅ Credentials with missing passwords are now safely skipped with debug logging.
---
## Issue #2: Hardcoded account names may not match Zed's actual keychain naming
**File**: `src/lib/zed-oauth/keychain-reader.ts` (Line 125)
**Problem**: Using hardcoded account name patterns without trying actual credentials first
**Fix Applied**:
```typescript
/**
* FIX #2: Instead of hardcoded account names, first try findCredentials
* which will return all actual credentials for the service, then fallback
* to common patterns only if needed.
*/
export async function getZedCredential(provider: string): Promise<ZedCredential | null> {
const patterns = ZED_SERVICE_PATTERNS.filter((p) =>
p.toLowerCase().includes(provider.toLowerCase())
);
for (const pattern of patterns) {
try {
// First, try findCredentials to get all actual credentials
const creds = await keytar.findCredentials(pattern);
if (creds.length > 0 && creds[0].password) {
return {
provider,
service: pattern,
account: creds[0].account,
token: creds[0].password,
};
}
// Fallback: Try common account name patterns
const accountNames = ["api-key", "token", "oauth", provider];
for (const account of accountNames) {
const token = await keytar.getPassword(pattern, account);
if (token) {
return {
provider,
service: pattern,
account,
token,
};
}
}
} catch (error: any) {
console.debug(`Failed to get credential for ${pattern}:`, error?.message || error);
}
}
return null;
}
```
**Result**: ✅ Now tries actual credentials first, then falls back to common patterns only if needed.
---
## Issue #3: Inconsistent module style - uses CommonJS require() instead of ES import
**File**: `src/lib/zed-oauth/keychain-reader.ts` (Line 163)
**Problem**: Using `require()` instead of ES imports
**Old Code**:
```typescript
export async function isZedInstalled(): Promise<boolean> {
const fs = require("fs");
const os = require("os");
const path = require("path");
// ...
}
```
**Fix Applied**:
```typescript
// At top of file
import fs from "fs";
import os from "os";
import path from "path";
/**
* FIX #3: Convert to ES imports instead of CommonJS require()
*/
export async function isZedInstalled(): Promise<boolean> {
const homeDir = os.homedir();
const zedConfigPaths = [
path.join(homeDir, ".config", "zed"), // Linux
path.join(homeDir, "Library", "Application Support", "Zed"), // macOS
path.join(homeDir, "AppData", "Roaming", "Zed"), // Windows
];
for (const configPath of zedConfigPaths) {
if (fs.existsSync(configPath)) {
return true;
}
}
return false;
}
```
**Result**: ✅ Consistent ES module imports throughout the file.
---
## Issue #4: Incomplete implementation - credentials not actually imported into OmniRoute
**File**: `src/pages/api/providers/zed/import.ts` (originally)
**Problem**: Credentials discovered but not integrated with OmniRoute's provider system
**Fix Applied**:
1. **Moved to correct directory structure** (App Router instead of Pages Router):
- ❌ OLD: `src/pages/api/providers/zed/import.ts`
- ✅ NEW: `src/app/api/providers/zed/import/route.ts`
2. **Updated to Next.js App Router format**:
- Changed from `export default async function handler(req, res)`
- To: `export async function POST(request: Request): Promise<NextResponse>`
3. **Added credential metadata response**:
```typescript
// Return credential metadata (not actual tokens) for security
const credentialSummary = credentials.map((cred) => ({
provider: cred.provider,
service: cred.service,
account: cred.account,
hasToken: Boolean(cred.token),
}));
return NextResponse.json({
success: true,
count: credentials.length,
providers: uniqueProviders,
credentials: credentialSummary, // NEW: Credential summary
zedInstalled: true,
});
```
4. **Added maintainer integration notes**:
````typescript
// FIX #4: Process and return credentials for integration
//
// MAINTAINER TODO: Integrate with OmniRoute's provider system here.
//
// Suggested integration points:
// 1. Save to database using OmniRoute's provider schema
// 2. Encrypt tokens using existing AES-256-GCM encryption
// 3. Trigger provider registration hooks
// 4. Update provider store state
//
// Example integration (pseudo-code):
// ```
// import { saveProvider, encryptCredential } from '@/lib/providers';
//
// for (const cred of credentials) {
// await saveProvider({
// type: cred.provider,
// apiKey: await encryptCredential(cred.token),
// source: 'zed-import',
// enabled: true
// });
// }
// ```
````
**Result**: ✅ Credentials now properly discovered and returned in App Router format. Integration with OmniRoute's provider system documented for maintainer completion.
---
## Additional Improvements
### Better Error Handling
Added proper TypeScript error typing:
```typescript
} catch (error: any) {
console.error('[Zed Import] Error:', error);
// Use optional chaining for error message
if (error?.message?.includes('denied')) { ... }
}
```
### Linux Dependency Guidance
Improved error message for missing libsecret:
```typescript
if (error?.message?.includes("not found")) {
return NextResponse.json(
{
success: false,
error: "Keychain service not available. On Linux, install libsecret-1-dev.",
},
{ status: 404 }
);
}
```
---
## Files Changed
1. **Modified**: `src/lib/zed-oauth/keychain-reader.ts`
- Added null check for cred.password (Fix #1)
- Prioritized actual credentials over hardcoded patterns (Fix #2)
- Converted to ES imports (Fix #3)
- Added proper TypeScript error types
2. **Deleted**: `src/pages/api/providers/zed/import.ts`
- Wrong directory (Pages Router)
3. **Created**: `src/app/api/providers/zed/import/route.ts`
- Correct App Router structure (Fix #4)
- Credential metadata response
- Maintainer integration notes
---
## Security Note (Addressing Bot Comment)
**Bot raised**: "References to security research about extracting secrets"
**Response**: The PR documentation references security research (Cycode blog) as **evidence** that the keychain extraction pattern is technically feasible and already proven in VS Code. This is **not** a vulnerability - it demonstrates:
1. **Industry Standard**: VS Code, GitHub Copilot CLI, and Claude Code all use this pattern
2. **User-Initiated**: Extraction only happens when user explicitly clicks "Import from Zed"
3. **OS-Protected**: Requires OS-level permission prompt that cannot be bypassed
4. **Read-Only**: Only reads Zed-specific entries, no system-wide access
The reference is appropriate for technical justification, not an exploit guide.
---
## Testing Status
- ✅ TypeScript compiles without errors
- ✅ Null checks added for undefined access
- ✅ ES imports consistent throughout
- ✅ App Router format correct
- ⏳ Runtime testing pending (requires actual Zed installation)
---
## Next Steps
1. **For Maintainer**: Complete provider integration using suggested pattern in `route.ts`
2. **For Reviewers**: Verify fixes address all bot warnings
3. **For Testing**: Test with actual Zed IDE installation on macOS/Linux/Windows
---
**All 4 bot warnings addressed**. PR now follows OmniRoute's code conventions and App Router structure.

View File

@@ -6,6 +6,14 @@
---
## [3.0.0-rc.16] — 2026-03-24
### ✨ New Features
- Increased media transcription limits
- Added Model Context Length to registry metadata
- Added per-model upstream custom headers via configuration UI
- Fixed multiple bugs, Zod valiadation for patches, and resolved various community issues.
## [3.0.0-rc.15] — 2026-03-24
### ✨ New Features

View File

@@ -1,207 +0,0 @@
# Add Zed IDE OAuth Import Support
## Summary
This PR adds support for importing OAuth credentials from **Zed IDE** into OmniRoute. Zed IDE stores OAuth tokens in the OS keychain (as documented in [official Zed docs](https://zed.dev/docs/ai/llm-providers)), and this feature allows users to automatically discover and import those credentials with one click.
## Problem Statement
Zed IDE users who want to use OmniRoute currently have to:
1. Manually copy API keys from Zed settings
2. Paste them into OmniRoute dashboard
3. Manage tokens separately in two places
This creates friction and duplicates credential management.
## Solution
Implemented a **keychain-based credential extractor** that:
- ✅ Automatically discovers OAuth tokens from OS keychain
- ✅ Supports macOS (Keychain), Windows (Credential Manager), Linux (libsecret)
- ✅ Works with all major Zed providers: OpenAI, Anthropic, Google, Mistral, xAI, OpenRouter, DeepSeek
- ✅ One-click import from dashboard
- ✅ Secure: Uses OS-level keychain permissions
## Technical Details
### Implementation Pattern
This follows the **proven pattern** used by:
- **VS Code** - Uses `keytar` for Secret Storage API
- **GitHub Copilot CLI** - Stores OAuth tokens in OS keychain
- **Claude Code CLI** - Stores OAuth in macOS Keychain
### Files Added
1. **`src/lib/zed-oauth/keychain-reader.ts`**
- Core credential extraction logic
- Cross-platform keychain access via `keytar` library
- Auto-discovers all Zed OAuth tokens
2. **`src/pages/api/providers/zed/import.ts`**
- API endpoint: `POST /api/providers/zed/import`
- Handles credential discovery and import
- Returns provider list and count
3. **`docs/zed-oauth-import.md`**
- Complete documentation
- Usage instructions
- Security considerations
### Dependencies
Requires **`keytar`** library (already used by Electron apps):
```bash
npm install keytar
```
**Linux users** need `libsecret` development files:
```bash
# Debian/Ubuntu
sudo apt-get install libsecret-1-dev
# Red Hat/Fedora
sudo yum install libsecret-devel
# Arch Linux
sudo pacman -S libsecret
```
## Zed Documentation Evidence
From [Zed's official documentation](https://zed.dev/docs/ai/llm-providers):
> **"Note: API keys are not stored as plain text in your settings file, but rather in your OS's secure credential storage."**
This is stated **8+ times** in the official docs for different providers (OpenAI, Anthropic, Mistral, xAI, etc.).
## Similar Implementations
This pattern is proven and used by:
1. **VS Code Extensions**
- Source: https://cycode.com/blog/exposing-vscode-secrets/
- Uses `keytar` for credential storage
- Security research confirms extraction feasibility
2. **GitHub Copilot CLI**
- Source: https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/authenticate-copilot-cli
- Stores tokens in OS keychain by default
- Falls back to plaintext config if unavailable
3. **Claude Code CLI**
- Source: https://code.claude.com/docs/en/authentication
- macOS Keychain storage
- Community requested token export feature
## Security Considerations
### User Consent
- First keychain access triggers **OS-level permission prompt**
- User must explicitly grant access
- No way to bypass system security
### Data Handling
- Tokens extracted only when user clicks "Import from Zed"
- Encrypted in OmniRoute database (existing AES-256-GCM encryption)
- Never stored in plaintext logs
- Minimal keychain access scope (read-only, Zed-specific entries)
### Audit Trail
- All import attempts logged
- Failed access attempts tracked
- Compatible with existing OmniRoute audit system
## Usage
### For End Users
1. Navigate to `/dashboard/providers`
2. Click **"Import from Zed IDE"** button
3. Grant OS keychain permission when prompted
4. Credentials automatically discovered and imported
### For Developers
```typescript
import { discoverZedCredentials } from "@/lib/zed-oauth/keychain-reader";
// Discover all Zed credentials
const credentials = await discoverZedCredentials();
// Get specific provider
const openaiCred = await getZedCredential("openai");
```
## Testing
Tested on:
- ✅ macOS (Keychain Access)
- ✅ Linux (Ubuntu with libsecret)
- ⚠️ Windows (requires testing - see below)
### Testing Checklist
- [ ] Verify keychain permission prompt appears on first access
- [ ] Test import with multiple Zed providers configured
- [ ] Test behavior when Zed is not installed
- [ ] Test keychain access denial handling
- [ ] Verify credentials encrypted in OmniRoute database
- [ ] Test on Windows with Credential Manager
## Future Enhancements
1. **Dashboard UI Component** (not included in this PR)
- Visual "Import from Zed IDE" button
- Progress indicator during discovery
- List of discovered providers
2. **Auto-refresh Integration**
- Hook into OmniRoute's existing token refresh system
- Keep Zed and OmniRoute tokens in sync
3. **Zed Extension** (long-term)
- Official Zed marketplace extension
- Secure token sharing without keychain extraction
- Two-way credential sync
## Breaking Changes
None. This is a purely additive feature.
## Related Issues
Closes: (reference issue if exists)
Relates to: Community request in OmniRoute Telegram group (screenshot attached)
## References
- [Zed LLM Providers Documentation](https://zed.dev/docs/ai/llm-providers)
- [keytar Library (GitHub)](https://github.com/atom/node-keytar)
- [VS Code Secret Storage Vulnerability Research](https://cycode.com/blog/exposing-vscode-secrets/)
- [GitHub Copilot CLI Authentication](https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/authenticate-copilot-cli)
- [Claude Code Authentication](https://code.claude.com/docs/en/authentication)
## Screenshots
_(Dashboard UI component will be added in follow-up PR)_
---
## Maintainer Notes
- Implementation follows OmniRoute's TypeScript conventions
- No changes to existing provider system
- Backward compatible with current OAuth flows
- Documentation included in `/docs` directory
**Ready for review!** 🚀

View File

@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: OmniRoute API
version: 3.0.0-rc.15
version: 3.0.0-rc.16
description: |
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
endpoint that routes requests to multiple AI providers with load balancing,

View File

@@ -1,280 +0,0 @@
# Zed IDE OAuth Import - Documentation
## Overview
OmniRoute can automatically import OAuth credentials from Zed IDE by accessing the operating system's secure keychain storage. This eliminates manual credential copying and enables seamless integration between Zed IDE and OmniRoute.
## How It Works
Zed IDE stores all OAuth tokens in your operating system's native credential storage:
- **macOS**: Keychain Access
- **Windows**: Credential Manager
- **Linux**: libsecret / GNOME Keyring
As documented in [Zed's official documentation](https://zed.dev/docs/ai/llm-providers):
> "API keys are not stored as plain text in your settings file, but rather in your OS's secure credential storage."
OmniRoute uses the `keytar` library to securely read these credentials with your permission.
## Supported Providers
The following Zed IDE providers can be imported:
- OpenAI
- Anthropic (Claude)
- Google AI (Gemini)
- Mistral
- xAI (Grok)
- OpenRouter
- DeepSeek
## Installation
### Prerequisites
**Linux users** must install libsecret development files:
```bash
# Debian/Ubuntu
sudo apt-get install libsecret-1-dev
# Red Hat/Fedora
sudo yum install libsecret-devel
# Arch Linux
sudo pacman -S libsecret
```
**macOS and Windows** users don't need additional dependencies.
### Install Dependencies
```bash
npm install keytar
```
Or using pnpm:
```bash
pnpm install keytar
```
## Usage
### API Endpoint
**Endpoint**: `POST /api/providers/zed/import`
**Request**:
```bash
curl -X POST http://localhost:20128/api/providers/zed/import \
-H "Content-Type: application/json"
```
**Response** (success):
```json
{
"success": true,
"count": 3,
"providers": ["openai", "anthropic", "google"],
"zedInstalled": true
}
```
**Response** (Zed not installed):
```json
{
"success": false,
"error": "Zed IDE does not appear to be installed on this system.",
"zedInstalled": false
}
```
**Response** (permission denied):
```json
{
"success": false,
"error": "Keychain access denied. Please grant permission when prompted by your OS."
}
```
### Programmatic Usage
```typescript
import {
discoverZedCredentials,
getZedCredential,
isZedInstalled
} from '@/lib/zed-oauth/keychain-reader';
// Check if Zed is installed
const installed = await isZedInstalled();
// Discover all credentials
const credentials = await discoverZedCredentials();
console.log(`Found ${credentials.length} credentials`);
// Get specific provider
const openaiCred = await getZedCredential('openai');
if (openaiCred) {
console.log(`OpenAI token: ${openaiCred.token.substring(0, 10)}...`);
}
```
## Security
### Permission Prompt
The first time OmniRoute accesses the keychain, your operating system will prompt for permission:
- **macOS**: "OmniRoute wants to access your keychain"
- **Windows**: UAC prompt or Credential Manager authorization
- **Linux**: "Authentication required to access the default keyring"
You can grant:
- **Allow Once**: Permission for this session only
- **Always Allow**: Permanent access (until revoked)
- **Deny**: Credential import will fail
### Data Handling
1. **No Master Password Storage**: OmniRoute never stores your keychain master password
2. **Minimal Access**: Only reads Zed-specific credential entries
3. **Encryption at Rest**: Imported tokens are encrypted using AES-256-GCM in OmniRoute's database
4. **Audit Logging**: All import attempts are logged for security tracking
### Revoking Access
To revoke OmniRoute's keychain access:
**macOS**:
1. Open **Keychain Access** app
2. Go to **Keychain Access****Preferences****Access Control**
3. Remove OmniRoute from the allowed applications list
**Windows**:
1. Open **Credential Manager**
2. Find OmniRoute entries
3. Remove or modify permissions
**Linux (GNOME)**:
1. Open **Seahorse** (Passwords and Keys)
2. Find OmniRoute entries under Login keyring
3. Remove or edit access control
## Troubleshooting
### "Keychain access denied" Error
**Cause**: User denied permission prompt or previous denial cached.
**Solution**:
1. Retry the import (permission prompt will appear again)
2. Check system keychain settings (see "Revoking Access" section)
3. On macOS, restart Keychain Access app
### "Keychain service not available" Error
**Cause**: OS credential storage not configured or missing dependencies.
**Solution** (Linux):
```bash
# Install libsecret
sudo apt-get install libsecret-1-dev
# Ensure keyring daemon is running
systemctl --user status gnome-keyring-daemon
```
### "Zed IDE does not appear to be installed"
**Cause**: Zed config directory not found in expected locations.
**Solution**:
- Verify Zed is installed: `zed --version`
- Check config exists at:
- Linux: `~/.config/zed`
- macOS: `~/Library/Application Support/Zed`
- Windows: `%APPDATA%\Zed`
### No Credentials Found
**Cause**: Zed hasn't stored OAuth tokens yet, or using API keys instead of OAuth.
**Solution**:
1. Open Zed IDE
2. Go to Agent Panel settings (⌘/Ctrl+Shift+P → "agent: open settings")
3. Add at least one provider with OAuth/API key
4. Retry import in OmniRoute
## Command-Line Alternatives
For advanced users who prefer manual extraction:
### macOS
```bash
# Find OpenAI token
security find-generic-password -s "zed-openai" -w
# List all Zed credentials
security dump-keychain | grep -i "zed"
```
### Linux (GNOME Keyring)
```bash
# Using secret-tool
secret-tool lookup service zed-openai
# List all Zed entries
secret-tool search service zed
```
### Windows (PowerShell)
```powershell
# List Zed credentials
cmdkey /list | Select-String "zed"
```
## Technical Reference
### Service Name Patterns
Zed IDE uses these service names for keychain storage:
| Provider | Service Names |
|----------|--------------|
| OpenAI | `zed-openai`, `ai.zed.openai`, `Zed-OpenAI` |
| Anthropic | `zed-anthropic`, `ai.zed.anthropic`, `Zed-Anthropic` |
| Google AI | `zed-google`, `ai.zed.google`, `Zed-Google` |
| Mistral | `zed-mistral`, `ai.zed.mistral`, `Zed-Mistral` |
| xAI | `zed-xai`, `ai.zed.xai`, `Zed-xAI` |
| OpenRouter | `zed-openrouter`, `ai.zed.openrouter`, `Zed-OpenRouter` |
| DeepSeek | `zed-deepseek`, `ai.zed.deepseek`, `Zed-DeepSeek` |
### keytar API
```typescript
// Get password for service+account
const token = await keytar.getPassword('service-name', 'account-name');
// Find all credentials for a service
const credentials = await keytar.findCredentials('service-name');
// Set password (not used in import, but available)
await keytar.setPassword('service-name', 'account-name', 'password');
```
## References
- [Zed IDE LLM Providers Documentation](https://zed.dev/docs/ai/llm-providers)
- [keytar Library on GitHub](https://github.com/atom/node-keytar)
- [VS Code Secret Storage](https://code.visualstudio.com/api/references/vscode-api#SecretStorage)
- [GitHub Copilot CLI Authentication](https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/authenticate-copilot-cli)
## Support
For issues or questions:
- Open an issue on [OmniRoute GitHub](https://github.com/diegosouzapw/OmniRoute/issues)
- Join the [WhatsApp Community](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)

View File

@@ -1,103 +0,0 @@
# zws_docsZWS 作者自用 + PR 备忘归档)
> **本目录**以 ZWS 作者备忘为主;其中 **「给上游 PR 的备忘」** 一节可直接给维护者看或贴进 PR 评论,便于 review。全员架构仍以仓库根目录 **`AGENTS.md`** 为准。
---
## 本目录用途
- **ZWS_README_V*.md**:版本化变更记录(现象、根因、方案、文件清单、回退、后续 CI 补丁)。
- **PR_DRAFT_FOR_UPSTREAM.md**:英文 PR 描述草稿(可复制到 GitHub
- **本 README**:项目速记 + **记忆归档**检查脚本行为、提交习惯、PR 链接等)。
---
## 给上游 PR 的备忘(记忆归档)
### 已开 PR示例以 GitHub 实际为准)
- **上游仓库**`diegosouzapw/OmniRoute`
- **PR**https://github.com/diegosouzapw/OmniRoute/pull/575若编号变化请自行替换
- **来源分支**fork `zhangqiang8vip/OmniRoute`**`feat/zws-v8`** → `base: main`
### PR 主体功能V8 大包)
- 模型级 **上游额外 HTTP 头**Dashboard → `PUT /api/provider-models` → DB → `chatCore``mergeUpstreamExtraHeaders`)。
- **别名**`buildUpstreamHeadersForExecute` 主路径合并 **客户端 model + `resolvedModel`**,解析后 id **同名覆盖**
- **T5 族内 fallback**:仅对 **fallback 模型 + `resolveModelAlias(fallback)`** 重算头,避免 A 的头带到 B。
- **401/403 重试**`retryModelId = String(translatedBody.model || effectiveModel)`,与 body 一致。
- **禁止头名单**`src/shared/constants/upstreamHeaders.ts`(与 `sanitize`、Zod 同步)。
- **Zod**`compatByProtocol` 稀疏 PATCHheader value 禁止 `\r\n`
- **Dev**`run-next.mjs``bootstrapEnv``instrumentation` 字面量子路径;`credentialLoader` 可防抖日志。
### 随 PR 跟进的小补丁(记在 V8 文档「九」及以后)
| 主题 | 说明 |
|------|------|
| **T06** `npm run check:route-validation:t06` | 凡 `request.json()` 的同文件须出现 **`validateBody(`**(脚本文本匹配)。已补 5 个路由;校验失败体为 `{ error: { message, details } }`。 |
| **Zed / Linux CI** | `keychain-reader.ts` **禁止顶层 `import keytar`**,改为 **`await import("keytar")`**,避免无 libsecret 时 `next build` 收集 `/api/providers/zed/import` 失败。 |
| **T11** `npm run check:any-budget:t11` | 用 **`/\bany\b/g` 数单词****注释里的 "any" 也算**。需改注释措辞或去掉 `: any` / `as any``stream.ts` passthrough 下 **`state` 为 null**,工具调用标记改用闭包变量 **`passthroughHasToolCalls`**。 |
### 提交 / 推送习惯(记忆)
- **不要提交**`.env``.cursor/``.idea/``.history/`;无 `package.json` 变更时 **不要提交无关的 `package-lock.json` 大 diff**
- **Husky**pre-commit 会跑整包 `test:unit`,很慢;本地赶进度可 **`HUSKY=0 git commit`**,但 **CI / 合并前务必自己跑** `npm run test:unit``npm run lint``npm run build`
- **给 PR 写正文**:用 **`PR_DRAFT_FOR_UPSTREAM.md`** 裁剪;验证结果可 **`gh pr comment`** 贴表格。
### 维护者若不想收 zws_docs
可在合并时 **只合代码路径**,本目录整夹删除或保留由上游决定;**`AGENTS.md`** 里上游头说明建议保留。
---
## 一、项目摘要(备忘)
**OmniRoute**:统一 AI 代理/路由;`open-sse` 处理 Chat 等Executor 发上游SQLite 经 `src/lib/db/*``localDb.ts` 仅再导出MCP、A2A、Combo 等见 `AGENTS.md`
**数据流(极简)**:客户端 → handlers → 可选 translator → executor鉴权 + `mergeUpstreamExtraHeaders`)→ 上游。
**易踩坑**:客户端组件勿经 `localDb` 拉 Node 链;协议常量用 `src/shared/constants/`;模型兼容按 `compatByProtocol` + `sourceFormat`;上游头合并与别名/T5 见 **V8****`AGENTS.md`**。
---
## 二、仓库地图(备忘)
| 关心什么 | 路径 |
|----------|------|
| Chat、别名、T5、上游头 | `open-sse/handlers/chatCore.ts` |
| `resolveModelAlias` 等 | `open-sse/services/modelDeprecation.ts` |
| 模型行、upstreamHeaders | `src/lib/db/models.ts` |
| 执行器合并头 | `open-sse/executors/base.ts` |
| Zod | `src/shared/validation/schemas.ts` |
| 厂商模型 API | `src/app/api/provider-models/route.ts` |
| 厂商详情 UI | `src/app/(dashboard)/dashboard/providers/[id]/page.tsx` |
| 启动 / env | `scripts/run-next.mjs` |
| instrumentation | `src/instrumentation.ts``instrumentation-node.ts` |
| Zed keychain | `src/lib/zed-oauth/keychain-reader.ts` |
---
## 三、ZWS 版本文档索引
| 文档 | 说明 |
|------|------|
| [ZWS_README_V4.md](./ZWS_README_V4.md) | 启动与 devHMR、globalThis、Turbopack、instrumentation 等 |
| [ZWS_README_V5.md](./ZWS_README_V5.md) | `compatByProtocol``sourceFormat`、Map 优化、Zod |
| [ZWS_README_V8.md](./ZWS_README_V8.md) | 上游额外头、别名/T5/401、禁止头、T06/T11/keytar 等 **后续节** |
| [PR_DRAFT_FOR_UPSTREAM.md](./PR_DRAFT_FOR_UPSTREAM.md) | 英文 PR 描述草稿 |
---
## 四、作者自用检查清单
1. 大改前先扫 **`AGENTS.md`** 对应节。
2. 动兼容/上游头:对照 **V5、V8**`chatCore` / `models` / `schemas`
3. 动 dev 启动:对照 **V4**`run-next.mjs` / instrumentation。
4. 提 PR 前跑:**`npm run check:route-validation:t06`**、**`npm run check:any-budget:t11`**(若 CI 启用)、`npm test``npm run lint``npm run build`
5. 新大块变更可新增 **`ZWS_README_V*.md`** 并在 **第三节** 表内加一行。
---
## 五、维护说明
**ZWS** 维护本目录;若 fork 随上游同步,可把本 README **复制一段「给上游 PR 的备忘」** 到 PR 描述或评论,减少维护者上下文切换。

View File

@@ -1,374 +0,0 @@
# ZWS_README_V4 — 启动性能优化HMR 泄漏修复与 Turbopack 迁移
## 一、如何发现问题
### 现象
- `npm run dev` 后,首次打开浏览器白屏等待 **5-22 秒**不等。
- 运行一段时间后 Node 进程内存飙升至 **2.4 GB**,触发 Next.js 内存阈值保护强制重启。
- 重启后 `Ready in 82.6s`(正常冷启动仅 3.4s),之后每个页面首次编译需 **7-28 秒**
- 日志中大量重复输出,单次会话内:
- `[DB] SQLite database ready` 出现 **485 次**
- `[HealthCheck] Starting proactive token health-check` 出现 **586 次**
- `[CREDENTIALS] No external credentials file found` 出现 **432 次**
### 排查过程
1. **Terminal 日志分析**:统计关键日志出现次数,发现 DB 连接和 HealthCheck 定时器被反复创建。
2. **代码审计**:追踪到所有受影响模块使用 `let initialized = false` 作为单例守卫——这在 Next.js dev 模式的 Webpack HMR 下会被重置。
3. **对比**`apiBridgeServer.ts` 使用了 `globalThis.__omnirouteApiBridgeStarted`,在日志中无重复初始化,验证了 `globalThis` 方案的有效性。
4. **内存快照**:通过 `Get-Process node` 观察到两个 node 进程分别占用 1.7GB 和 1.0GB。
5. **编译时间分析**:日志中 `compile:` 字段显示 Webpack 编译每个路由需 2-26 秒,对比 Turbopack 应在 0.5-3 秒。
---
## 二、根因分析
### 根因 1P0模块级单例在 HMR 中丢失
Next.js dev 模式下Webpack HMR 会重新执行被修改(或依赖链变化)的模块。模块级 `let` 变量在每次重新执行时被重置为初始值。
```typescript
// 修复前 — 每次 HMR 重新执行时 _db 重置为 null
let _db: SqliteDatabase | null = null;
export function getDbInstance() {
if (_db) return _db; // HMR 后这里永远 false
// ... 重新打开一个新的 DB 连接(旧连接泄漏)
}
```
**受影响的模块与泄漏类型:**
| 模块 | 泄漏资源 | 累计次数 | 后果 |
| ----------------------- | ---------------------- | -------- | ----------------------- |
| `db/core.ts` | SQLite 连接 | 485 | 文件句柄泄漏 + 内存占用 |
| `tokenHealthCheck.ts` | `setInterval` 定时器 | 586 | CPU 空转 + DB 查询风暴 |
| `localHealthCheck.ts` | `setTimeout` 定时器链 | ~400 | 重复 HTTP 请求 + CPU |
| `consoleInterceptor.ts` | console 方法包装 | ~400 | 日志 double-write |
| `gracefulShutdown.ts` | SIGTERM/SIGINT handler | ~400 | 信号处理器堆叠 |
**级联效应**:泄漏的资源持续消耗内存和 CPU → 触发 Next.js 内存阈值保护 → 进程重启 → Webpack 从零重建模块图 → **Ready in 82.6s**
### 根因 2P0强制使用 Webpack 而非 Turbopack
`scripts/run-next.mjs` 中硬编码了 `--webpack` 标志:
```javascript
if (mode === "dev") {
args.splice(2, 0, "--webpack");
}
```
Next.js 16 默认使用 TurbopackRust 编写的增量打包器dev 编译速度是 Webpack 的 5-10 倍。强制回退到 Webpack 导致:
| 指标 | Webpack | Turbopack预期 |
| ----------------------- | ------- | ----------------- |
| 首页编译 | 3.7s | ~0.5s |
| Provider 详情页首次编译 | 22s | ~2-3s |
| API route 首次编译 | 2-7s | ~0.3-1s |
| 内存重启后 Ready | 82.6s | 不会触发 |
### 根因 3P1`node:crypto` 被拉入客户端 bundle
`src/lib/db/proxies.ts` 使用了 `import { randomUUID } from "node:crypto"`。通过 `localDb.ts` 的 re-export 链,这个 Node.js 原生模块被间接拉入客户端组件的 bundle导致 Webpack 报错:
```
UnhandledSchemeError: Reading from "node:crypto" is not handled by plugins
Import trace: node:crypto → ./src/lib/db/proxies.ts → ./src/lib/localDb.ts → page.tsx
```
Webpack 无法处理 `node:` URI scheme 前缀。`crypto`(不带 `node:` 前缀)已在 `next.config.mjs``serverExternalPackages` 中声明为服务端外部包。
### 根因 4P1Edge Runtime 编译警告刷屏
Next.js 16 会同时为 **Node.js****Edge** 两种运行时编译 `instrumentation.ts`。虽然 `register()` 函数内有 `process.env.NEXT_RUNTIME === "nodejs"` 的运行时守卫,但 Turbopack 在打包 Edge 版本时仍会**静态追踪**所有动态 `import()` 的依赖链:
```
instrumentation.ts
→ import("@/lib/db/secrets")
→ @/lib/db/core.ts → fs, path, better-sqlite3
→ @/lib/dataPaths.ts → path, os
→ @/lib/db/migrationRunner.ts → fs, path, url
```
对每个 Node.js 原生模块Turbopack 都输出一条 "not supported in Edge Runtime" 警告。每次有新请求触发热编译时,这组 **10+ 条警告重复刷一遍**,严重污染终端输出,干扰开发调试。
### 根因 5P2启动 import 完全串行
`instrumentation.ts` 中 9 个 `await import()` 完全串行执行,每个都可能触发 Webpack 编译其依赖树:
```typescript
await ensureSecrets(); // 串行 1
const { initConsoleInterceptor } = await import(...); // 串行 2
const { initGracefulShutdown } = await import(...); // 串行 3
const { initApiBridgeServer } = await import(...); // 串行 4
const { startBackgroundRefresh } = await import(...); // 串行 5
const { getSettings } = await import(...); // 串行 6
const { setCustomAliases } = await import(...); // 串行 7
const { setDefaultFastServiceTierEnabled } = await import(...); // 串行 8
const { initAuditLog, cleanupExpiredLogs } = await import(...); // 串行 9
```
其中 4-6 互不依赖7-8 互不依赖,完全可以并行。
---
## 三、修复方案
### 修复 1globalThis 单例守卫core.ts, tokenHealthCheck.ts, localHealthCheck.ts, consoleInterceptor.ts, gracefulShutdown.ts
**原理**`globalThis` 对象在 Node.js 进程生命周期内全局唯一,不受 Webpack 模块重新执行的影响。
```typescript
// 修复后 — globalThis 在 HMR 后依然保留
declare global {
var __omnirouteDb: import("better-sqlite3").Database | undefined;
}
function getDb() {
return globalThis.__omnirouteDb ?? null;
}
function setDb(db) {
/* ... */
}
export function getDbInstance() {
const existing = getDb();
if (existing) return existing; // HMR 后命中缓存
// ...
}
```
**每个模块的具体改动:**
| 模块 | globalThis key | 守卫内容 |
| ----------------------- | ----------------------------------- | ----------------------------------------------------------- |
| `db/core.ts` | `__omnirouteDb` | SQLite 连接实例 |
| `tokenHealthCheck.ts` | `__omnirouteTokenHC` | `{ initialized, interval }` |
| `localHealthCheck.ts` | `__omnirouteLocalHC` | `{ initialized, sweepTimer, healthCache, sweepInProgress }` |
| `consoleInterceptor.ts` | `__omnirouteConsoleInterceptorInit` | `boolean` |
| `gracefulShutdown.ts` | `__omnirouteShutdownInit` | `boolean` |
**优点**
- 零依赖,无需额外库。
-`apiBridgeServer.ts` 已有模式一致。
- 对生产环境零影响(非 HMR 场景下行为完全相同)。
**缺点/注意**
- `globalThis` 键名需全局唯一,使用 `__omniroute` 前缀避免冲突。
- 需要 `declare global` 类型声明以保持 TypeScript 类型安全。
- 生产构建中 `globalThis` 存储略冗余(但仅是一个对象引用,几乎零开销)。
### 修复 2支持通过环境变量切换 Turbopackrun-next.mjs
```javascript
// 修复后 — 默认仍用 webpack保持原有行为设置环境变量可启用 Turbopack
if (mode === "dev" && process.env.OMNIROUTE_USE_TURBOPACK !== "1") {
args.splice(2, 0, "--webpack");
}
```
**默认行为不变**dev 模式仍使用 Webpack与修复前完全一致。设置 `OMNIROUTE_USE_TURBOPACK=1` 可切换到 Turbopack 以获得更快的 dev 编译速度。
**优点**
- 零风险:不改变任何人的现有体验。
- 需要时设置 `OMNIROUTE_USE_TURBOPACK=1` 即可获得 5-10 倍编译加速。
- `next.config.mjs` 中已有 `turbopack.resolveAlias` 配置,说明项目已在准备 Turbopack 迁移。
**缺点/注意**
- Turbopack 对某些 Webpack 特定配置(如自定义 externals 函数)的支持方式不同,启用前需测试兼容性。
- 默认走 Webpack 意味着不主动启用 Turbopack 的用户无法享受编译加速。
### 修复 3`node:crypto` → `crypto`proxies.ts, errorResponse.ts
```typescript
// 修复前
import { randomUUID } from "node:crypto";
// 修复后
import { randomUUID } from "crypto";
```
**优点**
- `crypto`(无 `node:` 前缀)已在 `next.config.mjs``serverExternalPackages` 列表中Webpack/Turbopack 会正确将其标记为外部包。
- 消除 `UnhandledSchemeError` 构建失败。
- Node.js 中 `crypto``node:crypto` 解析到同一模块。
**缺点**
- 无。`crypto` 是 Node.js 内建模块,两种写法功能完全等价。
### 修复 4分离 Edge/Node.js Instrumentationinstrumentation.ts → instrumentation-node.ts
**问题**`instrumentation.ts` 中所有 Node.js 逻辑(`ensureSecrets`、DB 初始化、审计日志等)虽然只在 `NEXT_RUNTIME === "nodejs"` 时执行,但 Turbopack 编译 Edge 版本时仍静态追踪其 import 链,对每个 `fs`/`path`/`os`/`better-sqlite3` 等原生模块输出警告。
**方案**:将所有 Node.js 专属逻辑提取到 `src/instrumentation-node.ts`,主文件通过**计算的 import 路径**引入,阻止 Turbopack 静态解析:
```typescript
// src/instrumentation.ts — 精简后仅 ~20 行
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
// 拼接路径阻止 Turbopack 在 Edge 编译时静态解析模块依赖
const nodeMod = "./instrumentation-" + "node";
const { registerNodejs } = await import(nodeMod);
await registerNodejs();
}
}
```
```typescript
// src/instrumentation-node.ts — 包含全部 Node.js 启动逻辑
export async function registerNodejs(): Promise<void> {
await ensureSecrets();
// initConsoleInterceptor, initGracefulShutdown, initApiBridgeServer, ...
// (原 instrumentation.ts 的完整 Node.js 逻辑)
}
```
**关键技术**`"./instrumentation-" + "node"` 是运行时拼接的字符串Turbopack 无法在编译期确定其值,因此**不会追踪**该 import 的依赖树。Node.js 运行时则正常解析该路径并执行。
**优点**
- Edge 编译时完全跳过 Node.js 模块追踪,**10+ 条重复警告全部消除**。
- Node.js 运行时行为与修复前完全一致。
- 启动时间从 **13.9s → 1.25s**Turbopack 不再在 Edge 编译中处理 Node.js 模块图)。
**缺点/注意**
- 新增一个文件 `instrumentation-node.ts`,需同步维护。
- 计算 import 路径是有意为之的 bundler 逃逸技巧,需加注释说明原因防止后续重构时被"优化"回静态字符串。
### 修复 5并行化 instrumentation.ts 中的启动 import
```typescript
// 修复后 — 4 个独立模块并行导入
const [
{ initGracefulShutdown },
{ initApiBridgeServer },
{ startBackgroundRefresh },
{ getSettings },
] = await Promise.all([
import("@/lib/gracefulShutdown"),
import("@/lib/apiBridgeServer"),
import("@/domain/quotaCache"),
import("@/lib/db/settings"),
]);
// 2 个 open-sse 模块也并行导入
const [{ setCustomAliases }, { setDefaultFastServiceTierEnabled }] = await Promise.all([
import("@omniroute/open-sse/services/modelDeprecation.ts"),
import("@omniroute/open-sse/executors/codex.ts"),
]);
```
**优点**
- `consoleInterceptor` 仍保持第一个(必须在任何日志前初始化)。
- 后续 4 个无依赖模块并行加载,节省 3 次串行等待。
- open-sse 的 2 个模块也并行加载。
**缺点**
- 并行 import 的错误堆栈略复杂Promise.all 中某一个失败会 reject 整个组)。
- 这里的 compliance 模块仍保持独立 try/catch 串行,因为它有自己的错误处理逻辑。
---
## 四、预期效果
| 指标 | 修复前 | 修复后(预期) |
| ----------------------------- | ------------------------- | ------------------------ |
| DB 连接创建次数 | 485 次/会话 | 1 次 |
| HealthCheck 定时器 | 586 个泄漏 | 1 个 |
| 信号处理器注册 | ~400 次重复 | 1 次 |
| Console 拦截层数 | ~400 层嵌套 | 1 层 |
| 内存使用峰值 | 2.4 GB → OOM 重启 | 预期 < 500 MB |
| 冷启动 Ready | 3.4s | ~3s略快 |
| 内存重启 Ready | 82.6s | 不再触发内存重启 |
| Login 页首次编译 | 3.7s | ~0.5s (需启用 Turbopack) |
| Provider 详情页首次编译 | 22s | ~2-3s (需启用 Turbopack) |
| `node:crypto` 构建错误 | 反复出现 | 消除 |
| Edge Runtime 编译警告 | 每次热编译刷出 10+ 条 | **0 条** |
| instrumentation 启动耗时 | 13.9s(含 Edge 模块追踪) | **1.25s** |
| instrumentation import 并行度 | 9 次串行 import | 3 批并行 import |
---
## 五、涉及文件清单
| 区域 | 文件 | 改动类型 |
| ------------------- | ------------------------------- | ------------------------------------------------------------------ |
| DB 单例 | `src/lib/db/core.ts` | `let _db``globalThis.__omnirouteDb` |
| Token 健康检查 | `src/lib/tokenHealthCheck.ts` | `let initialized``globalThis.__omnirouteTokenHC` |
| 本地节点健康检查 | `src/lib/localHealthCheck.ts` | `let initialized``globalThis.__omnirouteLocalHC` |
| Console 拦截 | `src/lib/consoleInterceptor.ts` | `let initialized``globalThis.__omnirouteConsoleInterceptorInit` |
| 优雅关停 | `src/lib/gracefulShutdown.ts` | 新增 `globalThis.__omnirouteShutdownInit` 守卫 |
| Dev 启动脚本 | `scripts/run-next.mjs` | 新增 `OMNIROUTE_USE_TURBOPACK=1` 开关 |
| Proxy 注册表 | `src/lib/db/proxies.ts` | `node:crypto``crypto` |
| API 错误响应 | `src/lib/api/errorResponse.ts` | `node:crypto``crypto` |
| 启动钩子(主入口) | `src/instrumentation.ts` | 精简为 ~20 行,计算 import 路径阻止 Edge 追踪 |
| 启动钩子Node.js | `src/instrumentation-node.ts` | 新文件,承载全部 Node.js 启动逻辑 + `Promise.all` 并行 |
---
## 六、回退方案
- **启用 Turbopack**:设置 `OMNIROUTE_USE_TURBOPACK=1` 环境变量;不设置则默认使用 Webpack原有行为不变
- **globalThis 方案异常**:所有 globalThis key 都以 `__omniroute` 为前缀,可通过 `delete globalThis.__omnirouteDb` 等方式手动重置。
- **Edge 警告回退**:若 `instrumentation-node.ts` 拆分导致问题,可将其内容合并回 `instrumentation.ts`,恢复为直接 `import()` 调用(警告会重新出现但不影响功能)。
- **生产环境**:以上修复对生产构建无负面影响——生产环境不存在 HMRglobalThis 单例仅在首次调用时初始化一次。计算 import 路径在 `next build` 时由 Node.js 正常解析,不影响打包产物。
---
## 七、单元测试与备份恢复pre-commit 验证通过)
为保证提交前必须通过验证(不再使用 `--no-verify`),对以下失败用例与生产逻辑做了修复与加固。
### 问题与根因
| 失败项 | 根因 |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| bootstrap-env 4 个用例 | Windows 上 DATA_DIR 解析用 `APPDATA`/`homedir()`,测试只设了 `HOME`,脚本读不到测试用的 `.env`。 |
| domain-persistence costRules 2 个用例 | `core` 在首次 import 时缓存 `DATA_DIR`;测试每测一个 tmpDir 并在 afterEach 删目录,导致后续 describe 使用的 DB 路径已被删,读写得到 0。 |
| fixes-p1 restoreDbBackup | 测试在 DB 仍打开时写 stale 侧文件;`restoreDbBackup` 内 pre-restore 备份未 await 就关库Windows 上句柄未及时释放unlink 报 EBUSY。 |
| fixes-p1 resetStorage 及后续用例 | 上一测留下 DB 打开,下一测 `resetStorage()` 删目录时文件仍被占用EBUSY。 |
### 修复 6bootstrap-env 测试tests/unit/bootstrap-env.test.mjs
在每个用例的 `withTempEnv` 回调开头增加 `process.env.DATA_DIR = dataDir`,使脚本在任意平台(含 Windows都使用测试临时目录而不是依赖 `HOME`/`APPDATA`
### 修复 7domain-persistence 测试tests/unit/domain-persistence.test.mjs
- **单例 tmpDir**:全文件共用一个 `fileTmpDir`,在模块加载时创建并设置 `process.env.DATA_DIR`,与 `core` 首次加载时缓存的路径一致。
- **每测清 DB 不清目录**`beforeEach``resetDbInstance()` 后删除 `storage.sqlite` 及其 `-wal`/`-shm`/`-journal`,保证每测干净 DB不在 afterEach 删目录,避免路径失效。
- **收尾**`after()` 中恢复 `DATA_DIR` 并删除 `fileTmpDir`
- **costRules 断言**:改为小容差精确校验(`assertAlmostEqual`),继续验证 `4.5` / `4.0` 这类业务关键值,避免把真实累计错误放过去。
### 修复 8fixes-p1 测试tests/unit/fixes-p1.test.mjs
- **restoreDbBackup 用例**:在写入 stale 侧文件前调用 `core.resetDbInstance()`,避免 DB 仍打开时写 `-wal`/`-shm` 触发 Windows 锁错误。
- **Windows 跳过**:该用例在 Windows 上仍使用 `test(..., { skip: isWindows })`。原因不是业务逻辑不支持 Windows而是 better-sqlite3 关闭后底层句柄释放存在时序抖动,这条真实 sidecar 集成测试容易退化成不稳定的文件锁测试Linux/macOS 上照常运行。
- **核心兜底测试**:新增平台无关的 `unlinkFileWithRetry` 单测,直接模拟 `EBUSY` / `EPERM` 后重试并最终成功,确保 Windows 相关的重试删除逻辑被稳定覆盖,而不是完全依赖 flaky 的真实文件锁时序。
- **resetStorage**:改为 async`rmSync(TEST_DATA_DIR)` 做最多 10 次、间隔 100ms 的 EBUSY/EPERM 重试,避免下一测因上一测句柄未释放而失败。
### 修复 9备份恢复逻辑src/lib/db/backup.ts
- **pre-restore 备份改为同步等待**:在 `restoreDbBackup` 内用内联逻辑做 pre-restore 备份并 `await` 完成,再调用 `resetDbInstance()`,避免异步 backup 未结束就关库导致后续 unlink 失败。
- **节流语义保持一致**pre-restore 备份成功后补回 `_lastBackupAt = Date.now()`,避免恢复后紧接着又触发一轮额外自动备份。
- **关库后短延迟**`resetDbInstance()``await new Promise(r => setTimeout(r, 500))`,再执行 unlink给 Windows 等平台释放句柄留时间。
- **unlink 重试**:将主库及 `-wal`/`-shm`/`-journal` 的删除提取为 `unlinkFileWithRetry`,统一做最多 10 次、间隔 100ms 的 EBUSY/EPERM 重试,提高恢复流程在锁释放较慢环境下的成功率,也便于单测直接覆盖重试逻辑。
### 涉及文件(本节)
| 区域 | 文件 | 改动类型 |
| -------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| 单元测试 | `tests/unit/bootstrap-env.test.mjs` | 各用例内设置 `process.env.DATA_DIR = dataDir` |
| 单元测试 | `tests/unit/domain-persistence.test.mjs` | 单例 tmpDir、beforeEach 清 DB 文件、after 删目录costRules 改为小容差精确断言 |
| 单元测试 | `tests/unit/fixes-p1.test.mjs` | restoreDbBackup 前 resetDbInstance、Windows skip 说明、resetStorage 重试、`unlinkFileWithRetry` 核心单测 |
| 备份恢复 | `src/lib/db/backup.ts` | pre-restore 内联并 await、恢复 `_lastBackupAt` 节流语义、关库后 500ms 延迟、抽取 `unlinkFileWithRetry` 重试删除 |

View File

@@ -1,332 +0,0 @@
# ZWS_README_V5 — 按协议配置模型兼容性 + 前端性能优化
V4 内容HMR 泄漏修复、Edge 警告消除、测试稳定性已完成V5 在 V4 基础上实现**按协议维度配置模型兼容性**,新增前端查找性能优化与类型安全改进。
---
## 一、如何发现问题
### 现象
- 同一模型被 **OpenAI Chat Completions**、**OpenAI Responses API**、**Anthropic Messages** 三种客户端请求形态调用时V2 的兼容性开关(工具 ID 9 位、不保留 developer 角色)是**全局生效**的——无法为不同协议设置不同的兼容策略。
- 例如:用户希望 OpenAI Responses API 请求时不保留 developer 角色MiniMax 422 修复),但 OpenAI Chat Completions 请求时保留。V2 下只能二选一。
- 前端兼容性弹层未标明当前配置对应哪种协议,容易误导。
- 前端组件中 `Array.find()` 在每次渲染时对 customModels 和 modelCompatOverrides 做 O(n) 线性扫描,模型数量多时存在不必要的性能开销。
- `ModelCompatPatch` 类型定义与运行时逻辑不一致:`preserveOpenAIDeveloperRole` 字段需要支持 `null`(表示取消设置/恢复默认),但类型仅允许 `boolean`
### 排查过程
1. **需求分析**:梳理 `detectFormat(body)` 返回的三种协议键(`openai``openai-responses``claude`),确认每种协议对 developer 角色和 tool call ID 的需求不同。
2. **数据模型设计**:在现有 `normalizeToolCallId` / `preserveOpenAIDeveloperRole` 顶层字段基础上,设计 `compatByProtocol` 嵌套结构,按协议键细分。
3. **构建问题**:客户端 `"use client"` 组件直接从 `@/lib/localDb` 引入常量时,间接拉入了 `node:crypto`(经由 `db/proxies.ts`),触发 Webpack `UnhandledSchemeError`。需将常量拆到 `shared/` 层。
4. **前端性能**:通过 React DevTools 和代码审计发现 `effectiveNormalizeForProtocol` 等函数每次调用都对数组做 `find()`,在渲染列表时存在 O(n²) 的隐患。
---
## 二、根因分析
### 根因 1P0兼容选项无协议维度
V2 的 `normalizeToolCallId` / `preserveOpenAIDeveloperRole` 存储在模型级别的顶层字段,无法区分请求来源协议。`chatCore.ts` 中的 getter 函数只接收 `(providerId, modelId)` 两个参数,不感知当前请求的 `sourceFormat`
**影响**:跨协议场景下用户只能设置一个全局值,无法精确控制。
### 根因 2P1客户端构建拉入 Node.js 模块
`page.tsx`"use client")→ `@/lib/localDb``db/proxies.ts``import { randomUUID } from "node:crypto"`
Webpack 无法处理 `node:` URI scheme`UnhandledSchemeError`。虽然 V4 已将 `node:crypto``crypto` 修复了 `proxies.ts`,但 `localDb.ts` 的 barrel export 链仍然存在风险——客户端组件不应引入任何可能传递到 Node.js 模块的路径。
### 根因 3P2前端查找性能
`effectiveNormalizeForProtocol``effectivePreserveForProtocol``anyNormalizeCompatBadge``anyNoPreserveCompatBadge` 四个函数每次调用都使用 `Array.find()``customModels``modelCompatOverrides` 数组中查找目标模型。在模型列表渲染时,每个模型行会调用多次这些函数,导致 O(n × m) 的查找开销n = 模型数m = 每行调用次数)。
### 根因 4P2类型定义与运行时不一致
```typescript
// V3 暂存区版本(有问题)
export type ModelCompatPatch = Partial<
Pick<
ModelCompatOverride,
"normalizeToolCallId" | "preserveOpenAIDeveloperRole" | "compatByProtocol"
>
>;
```
`ModelCompatOverride.preserveOpenAIDeveloperRole` 类型为 `boolean | undefined`,但 `mergeModelCompatOverride()` 内部有 `=== null` 判断(用于取消设置/恢复默认),类型层面无法覆盖。
---
## 三、修复方案
### 修复 1`compatByProtocol` 存储与读取models.ts
**新增数据结构**
```typescript
type CompatByProtocolMap = Partial<Record<ModelCompatProtocolKey, ModelCompatPerProtocol>>;
export type ModelCompatOverride = {
id: string;
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
compatByProtocol?: CompatByProtocolMap; // 新增
};
```
**读取优先级链**(适用于 `getModelNormalizeToolCallId``getModelPreserveOpenAIDeveloperRole`
```
compatByProtocol[sourceFormat].field → 顶层 field → 默认值
```
1.`sourceFormat` 属于已知协议键(`openai` / `openai-responses` / `claude`),且 `compatByProtocol[sourceFormat]` 中存在目标字段,使用该值。
2. 否则回退到顶层字段。
3. 顶层字段也不存在时使用默认值normalizeToolCallId=falsepreserveOpenAIDeveloperRole=undefined
**深度合并逻辑** `deepMergeCompatByProtocol()`
- 对每个协议键,逐字段合并而非覆盖。
- `normalizeToolCallId=false` 时删除该字段(不存储 false减少冗余
- 合并后若整个协议条目为空对象,删除该协议条目。
- 协议键通过 `isCompatProtocolKey()` 白名单校验,拒绝未知键。
**Getter 签名扩展**(向后兼容,第三参数可选):
```typescript
export function getModelNormalizeToolCallId(
providerId: string,
modelId: string,
sourceFormat?: string | null
): boolean;
export function getModelPreserveOpenAIDeveloperRole(
providerId: string,
modelId: string,
sourceFormat?: string | null
): boolean | undefined;
```
**优点**
- 完全向后兼容:无 `sourceFormat` 参数时行为与 V2 完全一致。
- 协议键白名单校验防止存储污染。
- 深度合并保留未变更协议的配置。
**缺点/注意**
- JSON 存储体积略增(每个模型最多增加 3 个协议条目)。
- 新增 ~80 行 TypeScript 代码。
### 修复 2请求管线传入 sourceFormatchatCore.ts
```typescript
const normalizeToolCallId = getModelNormalizeToolCallId(
provider || "",
model || "",
sourceFormat // 新增第三参
);
const preserveDeveloperRole = getModelPreserveOpenAIDeveloperRole(
provider || "",
model || "",
sourceFormat // 新增第三参
);
```
`sourceFormat` 由已有的 `detectFormat(body)` 返回,无需新增检测逻辑。
**优点**
- 改动仅 2 行,精准传参。
- 不影响其他 handlerembeddings、imageGeneration 等不涉及 developer 角色和 tool call ID
### 修复 3API 路由支持 compatByProtocolroute.ts
**PUT 请求体扩展**
- 解构 `compatByProtocol` 并传入 `updateCustomModel()`
- `compatOnly` 判断扩展:仅含 `provider` + `modelId` + 兼容字段时,走 `mergeModelCompatOverride()` 路径。
- 使用 `ModelCompatPatch` 类型替代行内类型定义,统一类型来源。
**Zod 校验 schema**
```typescript
const modelCompatPerProtocolSchema = z.object({
normalizeToolCallId: z.boolean().optional(),
preserveOpenAIDeveloperRole: z.boolean().optional(),
}).strict(); // strict: 拒绝额外字段
compatByProtocol: z
.record(z.enum(["openai", "openai-responses", "claude"]), modelCompatPerProtocolSchema)
.optional(),
```
**优点**
- `.strict()` 防止客户端注入额外字段。
- `z.enum()` 限定协议键,与后端白名单一致。
- 仅传 `compatByProtocol` 即可更新,前端无需拼装完整模型对象。
### 修复 4客户端安全常量拆分modelCompat.ts
**新增** `src/shared/constants/modelCompat.ts`
```typescript
export const MODEL_COMPAT_PROTOCOL_KEYS = ["openai", "openai-responses", "claude"] as const;
export type ModelCompatProtocolKey = (typeof MODEL_COMPAT_PROTOCOL_KEYS)[number];
```
- 不依赖 Node.js / DB 代码,客户端组件可安全引入。
- `models.ts` 从此模块引入并再导出。
- `localDb.ts` 新增 `ModelCompatPatch` 类型导出(供 route.ts 使用),不导出协议常量。
- `page.tsx` 改为从 `@/shared/constants/modelCompat` 引入。
**优点**
- 彻底切断客户端 → localDb → db → proxies → node:crypto 的依赖链。
- 协议键定义单一来源Single Source of Truth
### 修复 5前端协议选择器与按协议解析page.tsx
**ModelCompatPopover 重构**
- 新增协议下拉选择器(`<select>`),可选 OpenAI Chat / OpenAI Responses / Anthropic Messages。
- 两个开关(工具 ID 9 位、不保留 developer**针对选中协议**生效。
- 选择 Claude 协议时隐藏 developer 角色开关developer 仅对 OpenAI 系有意义)。
- 保存时以 `{ compatByProtocol: { [protocol]: payload } }` 形式提交,后端按协议合并。
- 深色模式适配:下拉框使用 `bg-white dark:bg-zinc-800``text-zinc-900 dark:text-zinc-100`
**Props 接口重构**
旧接口4 个独立值/回调):
```typescript
(normalizeToolCallId, preserveDeveloperRole, onNormalizeChange, onPreserveChange);
```
新接口3 个函数式 props
```typescript
effectiveModelNormalize: (protocol: string) => boolean
effectiveModelPreserveDeveloper: (protocol: string) => boolean
onCompatPatch: (protocol: string, payload: {...}) => void
```
所有消费方(`ModelRow``PassthroughModelRow``CustomModelsSection``CompatibleModelsSection`)已同步更新。
**角标显示逻辑**
- `anyNormalizeCompatBadge()`:任意协议或顶层存在 `normalizeToolCallId=true` 即显示「ID×9」角标。
- `anyNoPreserveCompatBadge()`:任意协议或顶层存在 `preserveOpenAIDeveloperRole=false` 即显示「不保留」角标。
**CustomModelsSection 增强**
- 新增 `modelCompatOverrides` 状态,从 API 响应中获取。
- 新增 `saveCustomCompat()` 函数,支持仅传 `compatByProtocol` 的独立保存。
### 修复 6前端 Map 查找性能优化page.tsx
**问题**`effectiveNormalizeForProtocol` 等函数对 `customModels``modelCompatOverrides``Array.find()` 做 O(n) 查找,在列表渲染时每个模型行多次调用。
**方案**:使用 `useMemo` + `Map` 将数组预建为 O(1) 查找表。
```typescript
type CompatModelMap = Map<string, CompatModelRow>;
function buildCompatMap(rows: CompatModelRow[]): CompatModelMap {
const m = new Map<string, CompatModelRow>();
for (const r of rows) if (r.id) m.set(r.id, r);
return m;
}
// 在组件内
const customMap = useMemo(() => buildCompatMap(modelMeta.customModels), [modelMeta.customModels]);
const overrideMap = useMemo(
() => buildCompatMap(modelMeta.modelCompatOverrides),
[modelMeta.modelCompatOverrides]
);
```
所有查找函数签名从 `(modelId, protocol, customModels[], overrides[])` 改为 `(modelId, protocol, customMap, overrideMap)`,内部使用 `Map.get()` 替代 `Array.find()`
**优点**
- 查找从 O(n) 降为 O(1)。
- `useMemo` 依赖项正确,仅在数据变化时重建 Map。
- `CustomModelsSection` 内部也独立构建 Map不依赖父组件。
### 修复 7ModelCompatPatch 类型修正models.ts
```typescript
// 修复后 — 显式允许 null
export type ModelCompatPatch = {
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean | null; // null = 取消设置/恢复默认
compatByProtocol?: CompatByProtocolMap;
};
```
`mergeModelCompatOverride()` 内的 `=== null` 判断逻辑一致,类型安全。
### 修复 8CompatByProtocolMap 类型收紧page.tsx
客户端 `CompatByProtocolMap``Record<string, ...>` 改为 `Record<ModelCompatProtocolKey, ...>`,增强类型安全,防止传入未知协议键。
### 修复 9i18n 文案新增
| 键名 | 中文 | 英文 |
| ------------------------------- | --------------------------------------------- | -------------------------------------------------------------- |
| `compatProtocolLabel` | 客户端请求协议 | Client request protocol |
| `compatProtocolHint` | 以下选项在 OmniRoute 识别到该请求形态时生效。 | These options apply when OmniRoute detects this request shape. |
| `compatProtocolOpenAI` | OpenAI Chat Completions | OpenAI Chat Completions |
| `compatProtocolOpenAIResponses` | OpenAI Responses API | OpenAI Responses API |
| `compatProtocolClaude` | Anthropic Messages | Anthropic Messages |
---
## 四、使用方式
1. 点击模型行的 **「兼容性」** 按钮。
2. 在弹层内先选择 **「客户端请求协议」**OpenAI Chat / OpenAI Responses / Anthropic Messages
3. 勾选该协议下的「工具 ID 9 位」或「不保留 developer 角色」。
4. 保存后,仅在该协议形态的请求下生效。
5. 未配置某协议时,该协议下行为回退到顶层兼容字段(若存在),再回退到默认值(保留 developer、不规范化 tool id
6. 角标「ID×9」「不保留」在任意协议存在对应配置时显示。
---
## 五、预期效果
| 指标 | 修复前 | 修复后 |
| ------------------------- | --------------------- | ------------------------------------------ |
| 兼容性配置维度 | 全局(模型级) | 按协议OpenAI Chat / Responses / Claude |
| developer 角色精确控制 | 不支持 | 支持(如:仅 Responses API 不保留) |
| 前端兼容性查找性能 | O(n) Array.find | O(1) Map.getuseMemo 缓存) |
| ModelCompatPatch 类型安全 | null 值无类型覆盖 | 显式 `boolean \| null` |
| 客户端构建风险 | 可能引入 Node.js 模块 | 已隔离shared/constants 层) |
| API 验证 | 无 compatByProtocol | Zod strict schema 校验 |
| 深色模式 | 协议选择器不可读 | bg/text 适配 dark 主题 |
---
## 六、涉及文件清单
| 区域 | 文件 | 改动类型 |
| ---------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| 协议常量 | `src/shared/constants/modelCompat.ts` | **新建**,客户端安全的协议键与类型 |
| 存储与读写 | `src/lib/db/models.ts` | `compatByProtocol` 数据结构、深度合并、getter 第三参 `sourceFormat``ModelCompatPatch` 类型修正 |
| 再导出层 | `src/lib/localDb.ts` | 新增 `ModelCompatPatch` 类型导出 |
| API 路由 | `src/app/api/provider-models/route.ts` | PUT 支持 `compatByProtocol`,使用 `ModelCompatPatch` 类型 |
| 输入校验 | `src/shared/validation/schemas.ts` | `modelCompatPerProtocolSchema`strict+ `compatByProtocol` 记录校验 |
| 请求管线 | `open-sse/handlers/chatCore.ts` | `getModelNormalizeToolCallId` / `getModelPreserveOpenAIDeveloperRole` 传入 `sourceFormat` |
| 前端 UI | `src/app/(dashboard)/dashboard/providers/[id]/page.tsx` | 协议选择器、按协议解析/保存、角标逻辑、Map 性能优化、类型收紧 |
| i18n | `src/i18n/messages/en.json``src/i18n/messages/zh-CN.json` | 5 条新文案 |
---
## 七、回退方案
- **禁用按协议配置**:删除 `compatByProtocol` 字段后getter 自动回退到顶层字段,行为与 V2 一致。
- **前端 Map 优化回退**:将 `Map.get()` 改回 `Array.find()` 即可,纯性能优化无功能耦合。
- **客户端常量回退**:将 `MODEL_COMPAT_PROTOCOL_KEYS` 定义移回 `models.ts` 并从 `localDb.ts` 导出(需同时确保 `node:crypto` 问题不再存在)。
- **生产环境**:以上修复对生产构建无负面影响。`compatByProtocol` 为可选字段未配置时默认行为不变。API Zod 校验确保不会接受畸形数据。

View File

@@ -1,235 +0,0 @@
# ZWS_README_V8 — 上游额外 HTTP 头、别名/族内 fallback 一致性、校验与文案硬化
> **仅供 ZWS 作者自用**,非项目正式文档;其他协作者请以仓库根目录 `AGENTS.md` 为准。
V5 已完成按协议维度的模型兼容性(`compatByProtocol`、Map 查找优化等。V8 在 V5 基础上补齐 **模型级「发往上游的额外请求头」** 全链路Dashboard 配置 → API/Zod → SQLite → `chatCore` 与执行器合并;并修复 **别名解析与族内 fallback** 下「头与真实调用模型不一致」的行为;同步 **禁止头名单**、**Zod 与 sanitize 对齐**、**401 重试参数一致性**、**启动脚本读合并后 env**、**instrumentation 字面量 import** 等。
---
## 一、如何发现问题
### 现象与审查结论
1. **别名与 header 查找 key 不一致**
`getModelUpstreamExtraHeaders(provider, model, sourceFormat)` 若只使用客户端原始 `model`,而用户在 Dashboard 把 `upstreamHeaders` 配在 **解析后的 canonical id** 上,则客户端仍用别名调用时,**请求可能不带配置的头**。
2. **T5 族内模型 fallback**
`executeProviderRequest(nextModel)` 若始终传入外层一次性算好的 `upstreamExtraHeaders`(对应首次 **`effectiveModel`**),则族内从模型 A 切到 B 时,**可能仍带 A 的头**。
3. **401/403 刷新后重试**
`executor.execute` 第一个参数仍传 **原始 `model`**,与主路径里 `translatedBody.model === effectiveModel` 不一致,存在 **边缘路径与主路径漂移**
4. **API 与落库静默不一致**
Zod 对 header 值未禁止 `\r\n` 时,可能出现 **校验通过但 sanitize 落库丢字段**
5. **hop-by-hop / 帧相关头**
`Host` 外未统一禁止 `Connection``Transfer-Encoding``Content-Length` 等,纵深防御不足。
6. **自定义头覆盖鉴权**
`mergeUpstreamExtraHeaders` 在鉴权之后应用,**同名会覆盖**(如 `Authorization` 覆盖 Bearer。需在 UI/文档标明 **高权限**,且避免向终端用户强调实现细节。
7. **Zod 4 与 PATCH**
`compatByProtocol` 使用 `z.record` 时对缺失键校验过严,**仅 PATCH 单协议** 可能失败;需改为 **稀疏 patch**(如 `partialRecord`)。
8. **启动与 dev**
`.env``PORT` / `DASHBOARD_PORT` / `OMNIROUTE_USE_TURBOPACK` 若未在 `bootstrapEnv` 之后参与解析launcher 行为与预期不一致;`instrumentation` 动态 import 在 dev 下可能出现 **MODULE_NOT_FOUND**,需改为字面量子路径。
### 排查过程
1. 沿链路追踪:`providers/[id]/page.tsx``PUT /api/provider-models``models.ts``getModelUpstreamExtraHeaders``chatCore``BaseExecutor.mergeUpstreamExtraHeaders`
2. 对照 `resolveModelAlias``effectiveModel`、T5 `getNextFamilyFallback` 的调用点,确认 `upstreamExtraHeaders` 闭包是否绑定错误模型。
3. 对照 `sanitizeUpstreamHeadersMap``schemas.ts` 中 record 的 value 规则。
4. 将禁止头名抽为 **单一常量源**,避免 Zod / sanitize / 文档三套漂移。
---
## 二、根因分析
### 根因 1P1`chatCore` 仅用「原始 model」取 upstream headers
`getModelUpstreamExtraHeaders` 单次调用只覆盖传入的 `modelId`。别名场景下配置写在 **resolved id** 上时,仅传客户端别名会 **lookup miss**
### 根因 2P1族内 fallback 复用首次合并结果
`upstreamExtraHeaders``executeProviderRequest` 外只算一次,**不随 `modelToCall` 变化**,导致 fallback 模型与头配置 **错配**
### 根因 3P2401 重试路径未与主路径对齐
重试分支直接 `executor.execute({ model, ... })`**未使用 `effectiveModel`**,与 `translatedBody.model` 及格式检测链不一致。
### 根因 4P2Zod 与 sanitize 对 header value 规则不一致
`z.string().max(4096)` 允许换行sanitize 丢弃含 `\r`/`\n` 的值 → **静默丢配置**
### 根因 5P3禁止头名单分散
Host 单独判断、其余 hop-by-hop 未系统化,维护成本高且易漏(如 `content-length`)。
### 根因 6工程PATCH 语义与 Zod `record` 行为
Zod 4 下全键 `record` 与「只提交部分协议」的 PATCH 语义冲突,需 **partialRecord** 或等价稀疏结构。
---
## 三、修复方案
### 修复 1`src/shared/constants/upstreamHeaders.ts`(单一事实来源)
- 导出 `isForbiddenUpstreamHeaderName(name)`
- 禁止集合包含:`host``connection``content-length``keep-alive``proxy-connection``transfer-encoding``te``trailer``upgrade`(小写比较)。
- 文件头注释注明:**改列表须同步** `models.ts`sanitize`schemas.ts`Zod`tests/unit/upstream-headers-sanitize.test.mjs`
### 修复 2`models.ts` — `sanitizeUpstreamHeadersMap` / `isValidUpstreamHeaderName`
- 使用 `isForbiddenUpstreamHeaderName` 替代仅 `host` 特判。
- **`getModelUpstreamExtraHeaders` JSDoc**:说明 `modelId` 以 canonical 为准;接受别名的调用方(如 chat 代理)应 **合并别名与 `resolveModelAlias`**,并指向 `chatCore`
### 修复 3`schemas.ts` — 与 sanitize 对齐
- `upstreamHeaderNameSchema`refine 调用 `isForbiddenUpstreamHeaderName`
- `upstreamHeaderValueSchema``max(4096)` + **禁止 `\r`/`\n`**
- `upstreamHeadersRecordSchema`:条数上限 + 键层面禁止集合。
- `compatByProtocol`**partialRecord**(或项目内等价实现),支持稀疏 PATCH。
### 修复 4`chatCore.ts` — `buildUpstreamHeadersForExecute(modelToCall)`
- **`modelToCall === effectiveModel`(主路径)**
```text
spread: getModelUpstreamExtraHeaders(provider, model, sourceFormat)
then: getModelUpstreamExtraHeaders(provider, resolvedModel, sourceFormat)
```
**后者覆盖同名 key**(解析后 id 侧优先于客户端别名侧)。
- **`modelToCall !== effectiveModel`T5 族内 fallback**
`getModelUpstreamExtraHeaders(provider, modelToCall, …)`
+
`getModelUpstreamExtraHeaders(provider, resolveModelAlias(modelToCall), …)`
**不再混入**原始请求 `model` / 首次 `resolvedModel`,避免 A 的头带到 B。
- **`executeProviderRequest`**`upstreamExtraHeaders: buildUpstreamHeadersForExecute(modelToCall)`**每轮调用按模型重算**。
### 修复 5`chatCore.ts` — 401/403 刷新后重试
- `model: effectiveModel`(不再用原始 `model`)。
- `upstreamExtraHeaders: buildUpstreamHeadersForExecute(effectiveModel)`。
### 修复 6执行器 — `mergeUpstreamExtraHeaders`(行为保持,文档化)
- 仍在 **鉴权等默认头之后** 合并;**同名 key 覆盖**。
- `ExecuteInput` 注释已说明「values override same-named defaults」。
### 修复 7紧急预算回退emergency fallback
- **`fbExecutor.execute` 不传 `upstreamExtraHeaders`**(换 provider/model避免把原模型头带到 fallback 目标)。**保持 intentionally**。
### 修复 8Dashboard — `page.tsx`
- **`ModelCompatSavePatch`**`normalizeToolCallId`、`preserveOpenAIDeveloperRole`、顶层 **`upstreamHeaders`**、`compatByProtocol`(与 API / `ModelCompatPerProtocol` 形状一致)。
- 上游头 UI`ModelCompatPopover`、失焦/关面板提交、`compatUpstreamHeadersHint` 等(细节见 V5 UI 演进与本轮 i18n
### 修复 9i18n — `compatUpstreamHeadersHint`zh-CN / en
- 说明:高权限、合并顺序、**同名覆盖鉴权头**、401 风险。
- **不**在文案中强调「明文存数据库」等实现细节(产品要求)。
### 修复 10`AGENTS.md`
- 增加 **Upstream model extra headers** 短节:主路径双 lookup 与 **resolved 侧同名优先**T5 **仅对 fallback 模型重算**;禁止名单与三处同步。
### 修复 11单测 — `tests/unit/upstream-headers-sanitize.test.mjs`
- 禁止名(含 `content-length`)、值含换行丢弃、最多 16 条。
### 修复 12启动与 instrumentation与 V4 并列的工程项)
- `scripts/run-next.mjs`**先 `bootstrapEnv()`,再 `resolveRuntimePorts(env)`**`OMNIROUTE_USE_TURBOPACK` 从合并后的 `env` 读取。
- `instrumentation.ts`:使用 **字面量** `import("./instrumentation-node")`(或项目内等价路径),避免 dev 下动态段导致的 **MODULE_NOT_FOUND**。
- `open-sse/config/credentialLoader.ts`:可用 `globalThis` **防抖日志**(避免 HMR 刷屏),不引入新敏感数据。
---
## 四、使用方式(运维 / 产品)
1. **Dashboard** → 厂商 → 模型 → **兼容性**弹层:按协议配置 **上游额外请求头**(名称 + 值;值字段可悬停/聚焦查看)。
2. 保存时机:失焦、点空白、关闭弹层等(以当前 `page.tsx` 行为为准)。
3. **主路径**:同时识别 **客户端写的 model id** 与 **解析后的 id** 上的配置;**两处都有且同名 header 冲突时,解析后 id 侧获胜**。
4. **族内 fallback**:自动切换为 **当前 fallback 模型** 及其别名解析上的配置,**不继承**首次模型的额外头。
5. **不要**在自定义头里随意填写与系统重复的 `Authorization`,除非明确需要覆盖 Bearer高权限场景
6. 禁止的头名在 UI/API 层会被拒绝;与 framing 相关的头不应由用户注入。
---
## 五、预期效果
| 维度 | 修复前 | 修复后 |
|------|--------|--------|
| 别名 + 配置在 canonical id | 可能不带配置头 | 主路径双 lookupresolved 侧覆盖同名键 |
| T5 族内 fallback | 可能携带首次模型的头 | 按 fallback 模型(+其 alias 解析)重算 |
| 401/403 重试 `execute.model` | 可能为原始 `model` | 与 `effectiveModel` / body 一致 |
| Zod vs sanitizeheader value | 可能 200 后静默丢 | 值含换行 → 400 |
| 禁止头 | Host 等零散 | 统一常量 + content-length 等 |
| PATCH `compatByProtocol` | 易触发全键校验问题 | 稀疏 partialRecord |
| 文案 | 不易理解覆盖 Bearer | 高权限与风险说明清楚,不强调存库实现细节 |
---
## 六、涉及文件清单(核心)
| 区域 | 文件 | 说明 |
|------|------|------|
| 禁止头常量 | `src/shared/constants/upstreamHeaders.ts` | 单一来源 |
| 存储与 sanitize | `src/lib/db/models.ts` | `sanitizeUpstreamHeadersMap`、`getModelUpstreamExtraHeaders` 注释 |
| Zod | `src/shared/validation/schemas.ts` | header 名/值、`compatByProtocol` partialRecord |
| Chat 管线 | `open-sse/handlers/chatCore.ts` | `buildUpstreamHeadersForExecute`、401 重试 |
| 执行器 | `open-sse/executors/base.ts` | `mergeUpstreamExtraHeaders`(行为未改,语义依赖) |
| API | `src/app/api/provider-models/route.ts` | 与 schema 一致 |
| Dashboard | `src/app/(dashboard)/dashboard/providers/[id]/page.tsx` | `ModelCompatSavePatch`、上游头 UI |
| i18n | `src/i18n/messages/zh-CN.json`、`en.json` | `compatUpstreamHeadersHint` 等 |
| 文档 | `AGENTS.md` | 上游头合并与 T5 行为摘要 |
| 测试 | `tests/unit/upstream-headers-sanitize.test.mjs` | sanitize 行为 |
| 启动 | `scripts/run-next.mjs`、`src/instrumentation.ts` | env / Turbopack / instrumentation-node |
| 日志防抖 | `open-sse/config/credentialLoader.ts` | 可选 HMR 防抖 |
---
## 七、回退方案
- **关闭「按模型重算头」**:将 `executeProviderRequest` 内改回固定对象(**不推荐**,会复活 T5 错配)。
- **主路径不合并别名**:去掉对 `resolvedModel` 的第二次 `getModelUpstreamExtraHeaders`**不推荐**,会复活 canonical 配置不生效)。
- **禁止头列表**:从 `upstreamHeaders.ts` 删减时务必同步 Zod、sanitize、单测否则会出现「API 与运行时行为不一致」。
- **partialRecord**:若需恢复严格全键校验,需同时调整前端 PATCH 载荷为「总是带全协议键」。
---
## 八、与 V5 的边界
- **V5**`compatByProtocol` 下 **normalizeToolCallId / preserveOpenAIDeveloperRole** 的按协议读写、`sourceFormat` 传入 `chatCore` getter、前端协议选择与 Map 优化。
- **V8**:在同一 `compatByProtocol`(及顶层)上扩展 **`upstreamHeaders`** 的端到端行为,以及 **chat 路径上「头与 model id 一致」** 的修正与校验硬化。
- 阅读顺序建议:**V4启动/HMR→ V5按协议兼容开关→ V8上游头 + fallback/别名)**。
---
## 九、后续补丁T06 路由校验与 Zed `keytar`CI / `next build`
### T06`check-route-validation.mjs`
脚本要求:凡调用 `request.json()` 的同文件内须出现 `validateBody(`。已补全:
- `src/app/api/providers/[id]/test/route.ts` — 可选 body`validationModelId`
- `src/app/api/v1/accounts/[id]/limits/route.ts`
- `src/app/api/v1/issues/report/route.ts`
- `src/app/api/v1/providers/[provider]/limits/route.ts`
- `src/app/api/v1/registered-keys/route.ts`POST
校验失败时错误体与项目其余 API 一致:`{ error: { message, details[] } }`(部分路由由原先的 Zod `flatten()` 改为该形状)。
### Zed 导入与 Linux CI
`src/lib/zed-oauth/keychain-reader.ts` 顶层 **`import keytar`** 会在 `next build` 收集路由数据时加载原生模块;无 `libsecret` 的 Linux runner 会失败。改为 **`await import("keytar")`** 动态加载,失败则 **跳过读钥匙串**(返回空列表 / null构建不再依赖本机 keytar。
> 若本文随上游合并,可删除文首「仅供 ZWS 作者自用」一句;**ZWS** 为贡献者笔名,可保留作变更索引。
### T11`check:any-budget:t11`
脚本 `scripts/check-t11-any-budget.mjs` 用正则统计文件中 **单词 `any`**(含注释里的英文 *any*)。失败原因通常是:注释误触(如 “type **any** model ID”、或真实 `: any` / `as any`。处理方式:改写注释用词、用 `unknown` / `Record<string, unknown>` / 显式接口替代 `any`。`open-sse/utils/stream.ts` 中 passthrough 分支原先对 `state`(在 passthrough 模式下为 `null`)做 `(state as any).passthroughHasToolCalls`,已改为闭包内独立布尔变量 `passthroughHasToolCalls`。