diff --git a/BOT_REVIEW_FIXES.md b/BOT_REVIEW_FIXES.md deleted file mode 100644 index ee18f4dbef..0000000000 --- a/BOT_REVIEW_FIXES.md +++ /dev/null @@ -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 { - 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 { - 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 { - 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` - -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index b32a097ded..326e154e04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md deleted file mode 100644 index 1d520379db..0000000000 --- a/PR_DESCRIPTION.md +++ /dev/null @@ -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!** 🚀 diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 78f0cb43f3..6a9ef135b6 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -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, diff --git a/docs/zed-oauth-import.md b/docs/zed-oauth-import.md deleted file mode 100644 index 1a60b68105..0000000000 --- a/docs/zed-oauth-import.md +++ /dev/null @@ -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) diff --git a/zws_docs/README.md b/zws_docs/README.md deleted file mode 100644 index 92b736b8da..0000000000 --- a/zws_docs/README.md +++ /dev/null @@ -1,103 +0,0 @@ -# zws_docs(ZWS 作者自用 + 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` 稀疏 PATCH;header 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) | 启动与 dev:HMR、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 描述或评论,减少维护者上下文切换。 diff --git a/zws_docs/ZWS_README_V4.md b/zws_docs/ZWS_README_V4.md deleted file mode 100644 index fac1ce81ee..0000000000 --- a/zws_docs/ZWS_README_V4.md +++ /dev/null @@ -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 秒。 - ---- - -## 二、根因分析 - -### 根因 1(P0):模块级单例在 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**。 - -### 根因 2(P0):强制使用 Webpack 而非 Turbopack - -`scripts/run-next.mjs` 中硬编码了 `--webpack` 标志: - -```javascript -if (mode === "dev") { - args.splice(2, 0, "--webpack"); -} -``` - -Next.js 16 默认使用 Turbopack(Rust 编写的增量打包器),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 | 不会触发 | - -### 根因 3(P1):`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` 中声明为服务端外部包。 - -### 根因 4(P1):Edge 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+ 条警告重复刷一遍**,严重污染终端输出,干扰开发调试。 - -### 根因 5(P2):启动 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 互不依赖,完全可以并行。 - ---- - -## 三、修复方案 - -### 修复 1:globalThis 单例守卫(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:支持通过环境变量切换 Turbopack(run-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 Instrumentation(instrumentation.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 { - 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()` 调用(警告会重新出现但不影响功能)。 -- **生产环境**:以上修复对生产构建无负面影响——生产环境不存在 HMR,globalThis 单例仅在首次调用时初始化一次。计算 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。 | - -### 修复 6:bootstrap-env 测试(tests/unit/bootstrap-env.test.mjs) - -在每个用例的 `withTempEnv` 回调开头增加 `process.env.DATA_DIR = dataDir`,使脚本在任意平台(含 Windows)都使用测试临时目录,而不是依赖 `HOME`/`APPDATA`。 - -### 修复 7:domain-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` 这类业务关键值,避免把真实累计错误放过去。 - -### 修复 8:fixes-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` 重试删除 | diff --git a/zws_docs/ZWS_README_V5.md b/zws_docs/ZWS_README_V5.md deleted file mode 100644 index 9e16b68757..0000000000 --- a/zws_docs/ZWS_README_V5.md +++ /dev/null @@ -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²) 的隐患。 - ---- - -## 二、根因分析 - -### 根因 1(P0):兼容选项无协议维度 - -V2 的 `normalizeToolCallId` / `preserveOpenAIDeveloperRole` 存储在模型级别的顶层字段,无法区分请求来源协议。`chatCore.ts` 中的 getter 函数只接收 `(providerId, modelId)` 两个参数,不感知当前请求的 `sourceFormat`。 - -**影响**:跨协议场景下用户只能设置一个全局值,无法精确控制。 - -### 根因 2(P1):客户端构建拉入 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 模块的路径。 - -### 根因 3(P2):前端查找性能 - -`effectiveNormalizeForProtocol`、`effectivePreserveForProtocol`、`anyNormalizeCompatBadge`、`anyNoPreserveCompatBadge` 四个函数每次调用都使用 `Array.find()` 在 `customModels` 和 `modelCompatOverrides` 数组中查找目标模型。在模型列表渲染时,每个模型行会调用多次这些函数,导致 O(n × m) 的查找开销(n = 模型数,m = 每行调用次数)。 - -### 根因 4(P2):类型定义与运行时不一致 - -```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>; - -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=false,preserveOpenAIDeveloperRole=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:请求管线传入 sourceFormat(chatCore.ts) - -```typescript -const normalizeToolCallId = getModelNormalizeToolCallId( - provider || "", - model || "", - sourceFormat // 新增第三参 -); -const preserveDeveloperRole = getModelPreserveOpenAIDeveloperRole( - provider || "", - model || "", - sourceFormat // 新增第三参 -); -``` - -`sourceFormat` 由已有的 `detectFormat(body)` 返回,无需新增检测逻辑。 - -**优点**: - -- 改动仅 2 行,精准传参。 -- 不影响其他 handler(embeddings、imageGeneration 等不涉及 developer 角色和 tool call ID)。 - -### 修复 3:API 路由支持 compatByProtocol(route.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 重构**: - -- 新增协议下拉选择器(`