diff --git a/.gitignore b/.gitignore index 188134e7..b9be9d1c 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ apps/site/node_modules/ # claude code session data .claude/ +app.log +app.pid +AGENTS.* \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 3557d25e..31bb731a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,86 +1,34 @@ -# AGENTS.md - -CodePilot — Codex 的桌面 GUI 客户端,基于 Electron + Next.js。 - -> 架构细节见 [ARCHITECTURE.md](./ARCHITECTURE.md),本文件只包含规则和流程。 - -## 开发规则 - -**提交前必须详尽测试:** -- 每次提交代码前,必须在开发环境中充分测试所有改动的功能,确认无回归 -- 涉及前端 UI 的改动需要实际启动应用验证(`npm run dev` 或 `npm run electron:dev`) -- 涉及构建/打包的改动需要完整执行一次打包流程验证产物可用 -- 涉及多平台的改动需要考虑各平台的差异性 - -**UI 改动必须用 CDP 验证(chrome-devtools MCP):** -- 修改组件、样式、布局后,必须通过 chrome-devtools MCP 实际验证效果 -- 验证流程:`npm run dev` 启动应用 → 用 CDP 打开 `http://localhost:3000` 对应页面 → 截图确认渲染正确 → 检查 console 无报错 -- 涉及交互的改动(按钮、表单、导航)需通过 CDP 模拟点击/输入并截图验证 -- 修改响应式布局时,用 CDP 的 device emulation 分别验证桌面和移动端视口 - -**新增功能前必须详尽调研:** -- 新增功能前必须充分调研相关技术方案、API 兼容性、社区最佳实践 -- 涉及 Electron API 需确认目标版本支持情况 -- 涉及第三方库需确认与现有依赖的兼容性 -- 涉及 Codex SDK 需确认 SDK 实际支持的功能和调用方式 -- 对不确定的技术点先做 POC 验证,不要直接在主代码中试错 - -**Worktree 隔离规则:** -- 如果任务设置了 Worktree,所有代码改动只能在该 Worktree 内进行 -- 严格禁止跨 Worktree 提交(不得在主目录提交 Worktree 的改动,反之亦然) -- 严格禁止 `git push`,除非用户主动提出 -- 启动测试服务(`npm run dev` 等)只从当前 Worktree 启动,不得在其他目录启动 -- 合并回主分支必须由用户主动发起,不得自动合并 -- **端口隔离**:Worktree 启动 dev server 时使用非默认端口(如 `PORT=3001`),避免与主目录冲突 -- **禁止跨目录编辑**:属于 Worktree 任务范围的文件,只在该 Worktree 内编辑,不得在主目录修改 -- **合并前检查 untracked 文件**:合并回主分支前先 `git status` 确认无调试残留、临时文件等 - -**Commit 信息规范:** -- 标题行使用 conventional commits 格式(feat/fix/refactor/chore 等) -- body 中按文件或功能分组,说明改了什么、为什么改、影响范围 -- 修复 bug 需说明根因;架构决策需简要说明理由 - -## 自检命令 - -**自检命令(pre-commit hook 会自动执行前三项):** -- `npm run test` — typecheck + 单元测试(~4s,无需 dev server) -- `npm run test:smoke` — 冒烟测试(~15s,需要 dev server) -- `npm run test:e2e` — 完整 E2E(~60s+,需要 dev server) - -修改代码后,commit 前至少确保 `npm run test` 通过。 -涉及 UI 改动时额外运行 `npm run test:smoke`。 - -## 改动自查 - -完成代码修改后,在提交前确认: -1. 改动是否涉及 i18n — 是否需要同步 `src/i18n/en.ts` 和 `zh.ts` -2. 改动是否涉及数据库 — 是否需要在 `src/lib/db.ts` 更新 schema 迁移 -3. 改动是否涉及类型 — 是否需要更新 `src/types/index.ts` -4. 改动是否涉及已有文档 — 是否需要更新 `docs/handover/` 中的交接文档 - -## 发版 - -**发版流程:** 更新 package.json version → `npm install` 同步 lock → 提交推送 → `git tag v{版本号} && git push origin v{版本号}` → CI 自动构建发布。不要手动创建 GitHub Release。 - -**发版纪律:** 禁止自动发版。`git push` + `git tag` 必须等用户明确指示后才执行。commit 可以正常进行。 - -**Release Notes 格式:** 标题 `CodePilot v{版本号}`,正文包含:更新内容、Downloads、Installation、Requirements、Changelog。 - -**构建:** macOS 产出 DMG(arm64 + x64),Windows 产出 NSIS 安装包。`scripts/after-pack.js` 重编译 better-sqlite3 为 Electron ABI。构建前清理 `rm -rf release/ .next/`。 - -## 执行计划 - -**中大型功能(跨 3+ 模块、涉及 schema 变更、需分阶段交付)必须先写执行计划再开工。** -- 活跃计划放 `docs/exec-plans/active/`,完成后移至 `completed/` -- 纯调研/可行性分析放 `docs/research/` -- 发现技术债务时记录到 `docs/exec-plans/tech-debt-tracker.md` -- 模板和规范见 `docs/exec-plans/README.md` - -## 文档 - -- [ARCHITECTURE.md](./ARCHITECTURE.md) — 项目架构、目录结构、数据流、新功能触及点 -- `docs/exec-plans/` — 执行计划(进度状态 + 决策日志 + 技术债务) -- `docs/handover/` — 交接文档(架构、数据流、设计决策) -- `docs/research/` — 调研文档(技术方案、可行性分析) - -**检索前先读对应目录的 README.md;增删文件后更新索引。** +## 核心原则 +- 行动前先读取相关代码、文档、配置和上下文;信息不足、边界不清或依赖未确认时先提问,禁止猜测后直接实现 +- 仅做完成当前目标所必需的最小修改;非必要不扩散重构、不顺手修复、不改变既有行为 +- 事实、推测、结论必须明确区分;所有判断都应尽量绑定到代码、文档、日志、测试或其他可验证证据 +- 用户当前指令优先级最高;若与历史约束、默认规范、局部实现或子代理建议冲突,以用户当前明确要求为准 + +## 沟通 +- 禁止客套、奉承、情绪化表述和无信息量废话 +- 默认直接输出代码、补丁、命令、计划或调度方案;仅在用户明确要求时提供摘要、背景说明或泛化建议 +- 只汇报对当前决策有价值的信息;不堆砌过程,不重复已确认结论 + +## 结构约束 +- 代码必须按树状语义组织:模块 → 功能组 → 动作点 +- 每一层只能承担单一职责;上层负责聚合、编排、路由和边界控制,下层负责具体实现,禁止职责交叉 +- 路径、目录名和文件名必须能表达语义;新增或修改代码时,必须优先落到语义最准确的最小节点 +- 单文件只解决单一目标;文件过大、职责混杂或上下文负担过重时,必须拆分到更小的语义节点 +- 禁止跨层乱放代码、禁止无意义转发、禁止把不相关逻辑塞进临近文件“顺手处理” + +## 代码质量 +- 禁止 `eslint-disable`、`@ts-ignore`、`any`,除非用户明确批准且说明理由 +- 优先复用项目内已有实现、类型、工具函数和约定;优先编辑现有代码,非必要不新建重复实现 +- 删除废弃、失效和未使用代码;不以注释形式保留旧实现 +- 保持与项目现有风格一致;若项目无统一约定,则使用 2 空格缩进、驼峰命名、函数名以动词开头 +- 修改必须保持局部与整体同时成立:类型、导入、接口、错误处理、边界条件、调用链和返回值必须一致,禁止只改表面不补全依赖 + +## 工作流程 +1. 非平凡任务必须先写 `PLAN.md`;任务拆分到 30 分钟至 2 小时粒度,每项必须有明确输入、输出、完成标准和依赖关系,并使用 `- [ ]` 跟踪状态 +2. 大目标必须拆分为可独立交付的 Phase;Phase 间尽量解耦;主代理只负责目标分解、边界约束、结果合并和最终验收 +3. 可并行且边界清晰的任务优先派给子代理;每个子代理只负责单一子目标,禁止同时承担规划、实现、验证三种以上职责 +4. 子代理输出必须可检查、可复现、可合并、可验证;未附带关键依据、改动说明或验证结果的输出不得直接并入主线 +5. 主代理不得盲信子代理结果;所有子代理产出在合并前必须经过主线复核,包括代码落点、依赖完整性、冲突风险和验证结果 +6. 每个功能都必须定义验证方式;未验证 = 未完成;固定流程为:实现 → 测试 → 修复 → 复测 +7. 测试失败、构建失败、类型检查失败、核心路径未验证,均不得标记为完成 +8. 发现阻塞时,先判断是需求阻塞、依赖阻塞、上下文阻塞还是实现阻塞;能局部推进的部分先推进,不能推进的部分再集中上报 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 8c89c2d0..31bb731a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,154 +1,34 @@ -# CLAUDE.md - -CodePilot — 多模型 AI Agent 桌面客户端,基于 Electron + Next.js。 - -> 架构细节见 [ARCHITECTURE.md](./ARCHITECTURE.md),本文件只包含规则和流程。 - -## 开发规则 - -**提交前必须详尽测试:** -- 每次提交代码前,必须在开发环境中充分测试所有改动的功能,确认无回归 -- 涉及前端 UI 的改动需要实际启动应用验证(`npm run dev` 或 `npm run electron:dev`) -- 涉及构建/打包的改动需要完整执行一次打包流程验证产物可用 -- 涉及多平台的改动需要考虑各平台的差异性 - -**UI 改动必须用 CDP 验证(chrome-devtools MCP):** -- 修改组件、样式、布局后,必须通过 chrome-devtools MCP 实际验证效果 -- 验证流程:`npm run dev` 启动应用 → 用 CDP 打开 `http://localhost:3000` 对应页面 → 截图确认渲染正确 → 检查 console 无报错 -- 涉及交互的改动(按钮、表单、导航)需通过 CDP 模拟点击/输入并截图验证 -- 修改响应式布局时,用 CDP 的 device emulation 分别验证桌面和移动端视口 - -**新增功能前必须详尽调研:** -- 新增功能前必须充分调研相关技术方案、API 兼容性、社区最佳实践 -- 涉及 Electron API 需确认目标版本支持情况 -- 涉及第三方库需确认与现有依赖的兼容性 -- 涉及 Claude Code SDK 需确认 SDK 实际支持的功能和调用方式 -- 对不确定的技术点先做 POC 验证,不要直接在主代码中试错 - -**Worktree 隔离规则:** -- 如果任务设置了 Worktree,所有代码改动只能在该 Worktree 内进行 -- 严格禁止跨 Worktree 提交(不得在主目录提交 Worktree 的改动,反之亦然) -- 严格禁止 `git push`,除非用户主动提出 -- 启动测试服务(`npm run dev` 等)只从当前 Worktree 启动,不得在其他目录启动 -- 合并回主分支必须由用户主动发起,不得自动合并 -- **端口隔离**:Worktree 启动 dev server 时使用非默认端口(如 `PORT=3001`),避免与主目录冲突 -- **禁止跨目录编辑**:属于 Worktree 任务范围的文件,只在该 Worktree 内编辑,不得在主目录修改 -- **合并前检查 untracked 文件**:合并回主分支前先 `git status` 确认无调试残留、临时文件等 - -**Commit 信息规范:** -- 标题行使用 conventional commits 格式(feat/fix/refactor/chore 等) -- body 中按文件或功能分组,说明改了什么、为什么改、影响范围 -- 修复 bug 需说明根因;架构决策需简要说明理由 - -## 自检命令 - -**自检命令(pre-commit hook 会自动执行前三项):** -- `npm run test` — typecheck + 单元测试(~4s,无需 dev server) -- `npm run test:smoke` — 冒烟测试(~15s,需要 dev server) -- `npm run test:e2e` — 完整 E2E(~60s+,需要 dev server) - -修改代码后,commit 前至少确保 `npm run test` 通过。 -涉及 UI 改动时额外运行 `npm run test:smoke`。 - -## 改动自查 - -完成代码修改后,在提交前确认: -1. 改动是否涉及 i18n — 是否需要同步 `src/i18n/en.ts` 和 `zh.ts` -2. 改动是否涉及数据库 — 是否需要在 `src/lib/db.ts` 更新 schema 迁移 -3. 改动是否涉及类型 — 是否需要更新 `src/types/index.ts` -4. 改动是否涉及已有文档 — 是否需要更新 `docs/handover/` 中的交接文档 -5. 改动是否构成新功能或大迭代 — 是否需要写文档(见下方"功能文档") - -## 功能文档 - -**新功能或大迭代完成后必须同时输出两份文档:** - -1. **技术交接文档** — 放 `docs/handover/` - - 目录结构、数据流、DB schema、API 路由、关键设计决策 - - 涉及 MCP 工具的需列出工具名、参数、自动批准策略 - - 目标读者:接手的开发者,需要能仅靠文档理解模块全貌 -2. **产品思考文档** — 放 `docs/insights/` - - 功能解决了什么用户问题、为什么这样设计而不是其他方案 - - 用户反馈驱动的决策、参考的外部文章/竞品/趋势 - - 未来可能的方向和已知的局限性 - - 目标读者:产品决策者,需要能理解设计背后的"为什么" - -**两份文档必须互相反向链接:** -- 交接文档开头:`> 产品思考见 [docs/insights/xxx.md](../insights/xxx.md)` -- 产品文档开头:`> 技术实现见 [docs/handover/xxx.md](../handover/xxx.md)` - -**文件命名保持一致**(如 `cli-tools.md`),方便对照查找。 - -## 发版 - -**发版流程:** 更新 `RELEASE_NOTES.md` → 更新 package.json version → `npm install` 同步 lock → 提交推送 → `git tag v{版本号} && git push origin v{版本号}` → CI 自动构建发布并使用 `RELEASE_NOTES.md` 作为 Release 正文。不要手动创建 GitHub Release(CI 会自动创建并上传构建产物)。 - -**发版纪律:** 禁止自动发版。`git push` + `git tag` 必须等用户明确指示后才执行。commit 可以正常进行。 - -**构建:** macOS 产出 DMG(arm64 + x64),Windows 产出 NSIS 安装包。`scripts/after-pack.js` 重编译 better-sqlite3 为 Electron ABI。构建前清理 `rm -rf release/ .next/`。 - -**Release Notes 格式(必须严格遵循):** - -标题:`CodePilot v{版本号}` - -正文结构: - -```markdown -## CodePilot v{版本号} - -> 一句话版本摘要,说明这个版本的核心主题或推荐升级理由。 - -### 新增功能 -- 功能描述(面向用户的语言,不要写 commit hash) - -### 修复问题 -- 修复了 xxx 的问题 - -### 优化改进 -- 优化了 xxx - -## 下载地址 - -### macOS -- [Apple Silicon (M1/M2/M3/M4)](https://github.com/op7418/CodePilot/releases/download/v{版本号}/CodePilot-{版本号}-arm64.dmg) -- [Intel](https://github.com/op7418/CodePilot/releases/download/v{版本号}/CodePilot-{版本号}-x64.dmg) - -### Windows -- [Windows 安装包](https://github.com/op7418/CodePilot/releases/download/v{版本号}/CodePilot.Setup.{版本号}.exe) - -## 安装说明 - -**macOS**: 下载 DMG → 拖入 Applications → 首次启动如遇安全提示,在系统设置 > 隐私与安全中点击"仍要打开" -**Windows**: 下载 exe 安装包 → 双击安装 - -## 系统要求 - -- macOS 12.0+ / Windows 10+ / Linux (glibc 2.31+) -- 需要配置 API 服务商(Anthropic / OpenRouter 等) -- 推荐安装 Claude Code CLI 以获得完整功能 -``` - -**Release Notes 写作规则:** -- 更新内容必须用用户能理解的语言,不要出现 commit hash、函数名、文件路径 -- 每个条目说清楚"用户能感知到什么变化" -- 下载链接必须是完整的 GitHub release download URL,用户点击即可下载 -- 如果某个分类没有内容(如没有修复),跳过该分类不要留空标题 -- `git log --oneline` 的输出只用于自己梳理,不要原样复制到 Release Notes - -## 执行计划 - -**中大型功能(跨 3+ 模块、涉及 schema 变更、需分阶段交付)必须先写执行计划再开工。** -- 活跃计划放 `docs/exec-plans/active/`,完成后移至 `completed/` -- 纯调研/可行性分析放 `docs/research/` -- 发现技术债务时记录到 `docs/exec-plans/tech-debt-tracker.md` -- 模板和规范见 `docs/exec-plans/README.md` - -## 文档 - -- [ARCHITECTURE.md](./ARCHITECTURE.md) — 项目架构、目录结构、数据流、新功能触及点 -- `docs/exec-plans/` — 执行计划(进度状态 + 决策日志 + 技术债务) -- `docs/handover/` — 技术交接文档(架构、数据流、设计决策) -- `docs/insights/` — 产品思考文档(用户问题、设计理由、趋势洞察) -- `docs/research/` — 调研文档(技术方案、可行性分析) - -**检索前先读对应目录的 README.md;增删文件后更新索引。** +## 核心原则 +- 行动前先读取相关代码、文档、配置和上下文;信息不足、边界不清或依赖未确认时先提问,禁止猜测后直接实现 +- 仅做完成当前目标所必需的最小修改;非必要不扩散重构、不顺手修复、不改变既有行为 +- 事实、推测、结论必须明确区分;所有判断都应尽量绑定到代码、文档、日志、测试或其他可验证证据 +- 用户当前指令优先级最高;若与历史约束、默认规范、局部实现或子代理建议冲突,以用户当前明确要求为准 + +## 沟通 +- 禁止客套、奉承、情绪化表述和无信息量废话 +- 默认直接输出代码、补丁、命令、计划或调度方案;仅在用户明确要求时提供摘要、背景说明或泛化建议 +- 只汇报对当前决策有价值的信息;不堆砌过程,不重复已确认结论 + +## 结构约束 +- 代码必须按树状语义组织:模块 → 功能组 → 动作点 +- 每一层只能承担单一职责;上层负责聚合、编排、路由和边界控制,下层负责具体实现,禁止职责交叉 +- 路径、目录名和文件名必须能表达语义;新增或修改代码时,必须优先落到语义最准确的最小节点 +- 单文件只解决单一目标;文件过大、职责混杂或上下文负担过重时,必须拆分到更小的语义节点 +- 禁止跨层乱放代码、禁止无意义转发、禁止把不相关逻辑塞进临近文件“顺手处理” + +## 代码质量 +- 禁止 `eslint-disable`、`@ts-ignore`、`any`,除非用户明确批准且说明理由 +- 优先复用项目内已有实现、类型、工具函数和约定;优先编辑现有代码,非必要不新建重复实现 +- 删除废弃、失效和未使用代码;不以注释形式保留旧实现 +- 保持与项目现有风格一致;若项目无统一约定,则使用 2 空格缩进、驼峰命名、函数名以动词开头 +- 修改必须保持局部与整体同时成立:类型、导入、接口、错误处理、边界条件、调用链和返回值必须一致,禁止只改表面不补全依赖 + +## 工作流程 +1. 非平凡任务必须先写 `PLAN.md`;任务拆分到 30 分钟至 2 小时粒度,每项必须有明确输入、输出、完成标准和依赖关系,并使用 `- [ ]` 跟踪状态 +2. 大目标必须拆分为可独立交付的 Phase;Phase 间尽量解耦;主代理只负责目标分解、边界约束、结果合并和最终验收 +3. 可并行且边界清晰的任务优先派给子代理;每个子代理只负责单一子目标,禁止同时承担规划、实现、验证三种以上职责 +4. 子代理输出必须可检查、可复现、可合并、可验证;未附带关键依据、改动说明或验证结果的输出不得直接并入主线 +5. 主代理不得盲信子代理结果;所有子代理产出在合并前必须经过主线复核,包括代码落点、依赖完整性、冲突风险和验证结果 +6. 每个功能都必须定义验证方式;未验证 = 未完成;固定流程为:实现 → 测试 → 修复 → 复测 +7. 测试失败、构建失败、类型检查失败、核心路径未验证,均不得标记为完成 +8. 发现阻塞时,先判断是需求阻塞、依赖阻塞、上下文阻塞还是实现阻塞;能局部推进的部分先推进,不能推进的部分再集中上报 \ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index cfdc06f5..5ef2c415 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,41 +1,32 @@ -## CodePilot v0.47.0 +## CodePilot v0.46.0 -> 服务商系统全面治理,新增连接测试和匿名错误上报,品牌重定位为多模型 AI Agent 桌面客户端。 +> 新增 Ollama 本地模型支持,优化工具调用展示和推理内容(Thinking)的显示与保留。 ### 新增功能 -- 服务商配置新增"测试连接"按钮:填完 API Key 后立即验证是否能连通,不用发消息才发现配置有误 -- 服务商配置新增引导面板:显示计费模式标签、API Key 获取链接、配置注意事项 -- 新增匿名错误上报(Sentry):帮助开发者定位高频问题,默认开启,可在设置中关闭 -- 新增服务商模型管理 API:支持为每个服务商自定义添加/删除模型 -- 新增小米 MiMo 服务商(按量付费 + Token Plan 两种模式) +- 新增 Ollama 服务商预设:一键接入本地模型,无需 API 密钥 +- 新增推理内容(Thinking)流式展示:支持实时查看模型的推理过程 +- 工具调用展示全面重构:分组折叠、状态指示、运行中工具实时输出 ### 修复问题 -- 修复智谱 GLM、Moonshot、OpenRouter、百炼等 6 个服务商的认证方式配置错误,大幅减少首次连接失败 -- 修复用户终端 Claude Code 的 settings.json 配置覆盖 CodePilot 服务商选择的问题 -- 修复运行时报错缺少恢复操作建议的问题,现在会显示"重新获取 Key"等可点击链接 -- 修复模型选择下拉框出现横向滚动条的问题 -- 修复"管理服务商"按钮跳转到通用设置而非服务商页面的问题 -- 修复 Kimi 使用了错误的认证头(Bearer 而非 X-Api-Key)的问题 +- 修复中断或出错时已展示的推理内容(Thinking)在完成态消失的问题 +- 修复远程桥接(Telegram/Discord/飞书)静默丢弃推理内容的问题 +- 修复未注册工具(MCP 工具、插件工具等)在操作列表中不显示名称的问题 +- 修复 auth_token 认证方式未显式清空 API Key 导致部分服务商连接失败的问题 ### 优化改进 -- 品牌重定位:从"Claude Code 桌面 GUI"更新为"多模型 AI Agent 桌面客户端" -- README 全面重构(中/英/日三语):新增下载量和 Stars badges,下载区前置,17+ 服务商表格 -- 服务商系统新增 Zod Schema 校验:防止无效配置上线,新增 61 个自动化测试 -- 服务商配置页去除 230 行重复代码,统一为单一数据源 -- 官网服务商文档更新:修正国内服务商表格,新增各服务商注意事项 -- GitHub About 描述和联系方式更新 +- 服务商文档新增 Ollama 配置指南(中英双语) ## 下载地址 ### macOS -- [Apple Silicon (M1/M2/M3/M4)](https://github.com/op7418/CodePilot/releases/download/v0.47.0/CodePilot-0.47.0-arm64.dmg) -- [Intel](https://github.com/op7418/CodePilot/releases/download/v0.47.0/CodePilot-0.47.0-x64.dmg) +- [Apple Silicon (M1/M2/M3/M4)](https://github.com/op7418/CodePilot/releases/download/v0.46.0/CodePilot-0.46.0-arm64.dmg) +- [Intel](https://github.com/op7418/CodePilot/releases/download/v0.46.0/CodePilot-0.46.0-x64.dmg) ### Windows -- [Windows 安装包](https://github.com/op7418/CodePilot/releases/download/v0.47.0/CodePilot.Setup.0.47.0.exe) +- [Windows 安装包](https://github.com/op7418/CodePilot/releases/download/v0.46.0/CodePilot.Setup.0.46.0.exe) ## 安装说明 @@ -45,5 +36,5 @@ ## 系统要求 - macOS 12.0+ / Windows 10+ / Linux (glibc 2.31+) -- 需要配置 API 服务商(Anthropic / OpenRouter / 智谱 / Kimi / Ollama 等) +- 需要配置 API 服务商(Anthropic / OpenRouter / Ollama 等) - 推荐安装 Claude Code CLI 以获得完整功能 diff --git a/apps/site/content/docs/en/providers.mdx b/apps/site/content/docs/en/providers.mdx index ea8ba47f..d945046e 100644 --- a/apps/site/content/docs/en/providers.mdx +++ b/apps/site/content/docs/en/providers.mdx @@ -60,24 +60,18 @@ Connect to third-party endpoints compatible with the Anthropic API format. CodePilot includes built-in configuration presets for major Chinese providers. After selecting one, the Base URL and default model are auto-filled: -| Provider | Description | Billing Model | -|----------|-------------|---------------| -| **智谱 GLM (Domestic/International)** | Zhipu AI GLM series | Coding Plan (credit-based) | -| **Kimi Coding Plan** | Moonshot Kimi coding edition | Pay-as-you-go | -| **Moonshot** | Moonshot API | Pay-as-you-go | -| **MiniMax (Domestic/International)** | MiniMax M2.7 | Token Plan | -| **火山引擎 Ark** | ByteDance Volcengine (Doubao, GLM, DeepSeek, Kimi) | Coding Plan | -| **小米 MiMo** | Xiaomi MiMo-V2-Pro (pay-as-you-go or Token Plan) | Pay-as-you-go / Token Plan | -| **阿里云百炼 Coding Plan** | Alibaba Cloud (Qwen, GLM, Kimi, MiniMax) | Coding Plan | - -When adding a Chinese provider in CodePilot, the system automatically handles the authentication method — you just need to enter the key provided by the respective platform. Each provider card shows a direct link to obtain your API key. - -> **Important notes for specific providers:** -> - **智谱 GLM**: Peak hours (14:00–18:00 UTC+8) consume 3x credits -> - **Kimi / Moonshot**: `tool_search` is automatically disabled to prevent 400 errors -> - **小米 MiMo**: Does not support Thinking mode -> - **阿里云百炼**: Must use Coding Plan key (starts with `sk-sp-`); standard DashScope keys will not work -> - **火山引擎 Ark**: Endpoint must be activated in the console before use +| Provider | Description | Auth Method | +|----------|-------------|-------------| +| **智谱 GLM (Domestic)** | 智谱 AI GLM series | API Key | +| **智谱 GLM (International)** | 智谱 AI international endpoint | API Key | +| **Kimi Coding Plan** | Moonshot Kimi coding edition | Auth Token | +| **Moonshot** | Moonshot API | API Key | +| **MiniMax (Domestic)** | MiniMax abab series | API Key | +| **MiniMax (International)** | MiniMax international endpoint | API Key | +| **火山引擎 Ark** | ByteDance Volcengine | Auth Token | +| **阿里云百炼 Coding Plan** | Alibaba Cloud Tongyi series | API Key | + +> **Auth Token type**: Kimi Coding Plan and 火山引擎 Ark use `ANTHROPIC_AUTH_TOKEN` rather than `ANTHROPIC_API_KEY` for authentication. When adding them in CodePilot, the system automatically handles the authentication method — you just need to enter the key provided by the respective platform. ### OpenRouter diff --git a/apps/site/content/docs/zh/providers.mdx b/apps/site/content/docs/zh/providers.mdx index 79c0638e..34da1501 100644 --- a/apps/site/content/docs/zh/providers.mdx +++ b/apps/site/content/docs/zh/providers.mdx @@ -60,24 +60,18 @@ export ANTHROPIC_API_KEY="sk-ant-..." CodePilot 内置了国内主流服务商的配置预设,选择后自动填充基础 URL 和默认模型: -| 服务商 | 说明 | 计费模式 | +| 服务商 | 说明 | 认证方式 | |--------|------|----------| -| **智谱 GLM(国内/国际)** | 智谱 AI GLM 系列 | Coding Plan(积分制) | -| **Kimi Coding Plan** | 月之暗面 Kimi 编程版 | 按量付费 | -| **Moonshot** | 月之暗面 Moonshot API | 按量付费 | -| **MiniMax(国内/国际)** | MiniMax M2.7 | Token Plan | -| **火山引擎 Ark** | 字节跳动火山引擎(豆包、GLM、DeepSeek、Kimi) | Coding Plan | -| **小米 MiMo** | 小米 MiMo-V2-Pro(按量付费或 Token Plan) | 按量 / Token Plan | -| **阿里云百炼 Coding Plan** | 阿里云(通义、GLM、Kimi、MiniMax) | Coding Plan | - -在 CodePilot 中添加国内服务商时,系统会自动处理认证方式,你只需填写对应平台提供的密钥。每个服务商卡片上都有直接获取 API Key 的链接。 - -> **各服务商注意事项:** -> - **智谱 GLM**:高峰时段(14:00–18:00 UTC+8)消耗 3 倍积分 -> - **Kimi / Moonshot**:`tool_search` 已自动关闭以避免 400 错误 -> - **小米 MiMo**:不支持 Thinking 模式 -> - **阿里云百炼**:必须使用 Coding Plan 专用 Key(以 `sk-sp-` 开头),普通 DashScope Key 无法使用 -> - **火山引擎 Ark**:需先在控制台激活 Endpoint 后才能使用 +| **智谱 GLM(国内版)** | 智谱 AI GLM 系列 | API 密钥 | +| **智谱 GLM(国际版)** | 智谱 AI 国际端点 | API 密钥 | +| **Kimi Coding Plan** | 月之暗面 Kimi 编程版 | Auth Token | +| **Moonshot** | 月之暗面 Moonshot API | API 密钥 | +| **MiniMax(国内版)** | MiniMax abab 系列 | API 密钥 | +| **MiniMax(国际版)** | MiniMax 国际端点 | API 密钥 | +| **火山引擎 Ark** | 字节跳动火山引擎 | Auth Token | +| **阿里云百炼 Coding Plan** | 阿里云通义系列 | API 密钥 | + +> **Auth Token 类型**:Kimi Coding Plan 和火山引擎 Ark 使用 `ANTHROPIC_AUTH_TOKEN` 而非 `ANTHROPIC_API_KEY` 进行认证。在 CodePilot 中添加时,系统会自动处理认证方式,你只需填写对应平台提供的密钥。 ### OpenRouter diff --git a/docs/exec-plans/README.md b/docs/exec-plans/README.md index 3b030245..43305b36 100644 --- a/docs/exec-plans/README.md +++ b/docs/exec-plans/README.md @@ -49,7 +49,6 @@ | active/site-and-docs.md | 官网 + 文档站(apps/site) | Phase 0-1 进行中 | | active/weixin-bridge-channel.md | 微信 Bridge 通道一次性交付方案 | One Shot 待开始 | | active/unified-context-layer.md | 统一上下文层 + 浮窗助理 + 产品架构演进 | Phase 1-3 已完成,Phase 4-5 待开始 | -| active/provider-governance.md | 服务商系统治理:Preset Schema 校验、宿主接管、连通性验证、引导 UX、错误治理 | Phase 0 完成,Phase 1-6 待开始 | ### Completed diff --git a/docs/exec-plans/active/provider-governance.md b/docs/exec-plans/active/provider-governance.md deleted file mode 100644 index 857999a1..00000000 --- a/docs/exec-plans/active/provider-governance.md +++ /dev/null @@ -1,379 +0,0 @@ -# 服务商系统治理方案 - -> 创建时间:2026-04-04 -> 最后更新:2026-04-04 -> 前置文档:[handover/provider-architecture.md](../../handover/provider-architecture.md) -> 参考项目:Claude Code CLI 源码、OpenCode、Craft Agents - -## 问题本质 - -当前服务商系统的脆弱性不是某个 bug,而是**架构上缺少防护层**: - -1. **Preset 是一堆手写的对象,没有任何机制验证它们是否正确。** 改一个 authStyle、加一个 envOverride,没有测试告诉你"这组配置跑不通"。 -2. **配置和逻辑互相打架。** Preset 的 `defaultEnvOverrides` 试图清空 `ANTHROPIC_API_KEY`,但 `toClaudeCodeEnv()` 的 AUTH_ENV_KEYS 跳过机制把这个意图静默吞掉了。两层各自有道理,合在一起就出 bug。 -3. **用户在配置时得不到任何反馈。** 填完 Key 没有验证,等发消息才发现不对,报错信息还是技术性的。 -4. **服务商信息散落在代码各处。** 名称在 catalog、URL 在 preset、获取 Key 的链接不存在、报错提示不知道是哪家的问题。 - -## 目标 - -改完之后应该是这样的: - -1. **加一个新服务商** = 写一个声明式配置文件 + 跑通自动化测试 → 就能上线 -2. **改一个模型名** = 改配置文件 → 测试自动告诉你有没有破坏别的服务商 -3. **用户配置服务商** = 选服务商 → 看到"去这里买/获取 Key" → 填 Key → 立即验证 → 成功/明确告知哪里错了 -4. **运行时出错** = 用户看到"智谱 GLM 返回了 401,可能是 API Key 过期,点这里重新获取" → 不是一个技术性的 stack trace - -## 状态 - -| Phase | 内容 | 状态 | 备注 | -|-------|------|------|------| -| Phase 0 | 架构设计 + 参考项目调研 | ✅ 已完成 | 本文档 | -| Phase 1 | Preset 声明式改造 + Schema 校验 | ✅ 已完成 | Zod PresetSchema + meta 字段 + 61 个新测试 | -| Phase 2 | 宿主接管 + authStyle 修正 | ✅ 已完成 | 6 个 authStyle 修正 + PROVIDER_MANAGED_BY_HOST 注入 | -| Phase 3 | 配置时连通性验证 | ✅ 已完成 | POST /api/providers/test + testProviderConnection() | -| Phase 4 | 用户引导 UX(服务商信息面板) | ✅ 已完成 | QUICK_PRESETS 去重(-181 行),meta 流通到前端 | -| Phase 5 | 运行时错误治理 | ✅ 已完成 | RecoveryAction + providerMeta → 分类错误码 + 恢复按钮 | -| Phase 6 | 模型目录动态化 | ✅ 已完成 | per-provider model CRUD API(GET/POST/DELETE) | - -## 决策日志 - -- 2026-04-04: 参考了三个项目的架构: - - **OpenCode** — Registry + Lazy Factory + Plugin,用 models.dev 做外部模型目录,Zod schema 校验所有配置,10+ 种错误模式匹配 - - **Craft Agents** — Driver 模式,连接模板,CredentialManager 多后端,ModelRefreshService 回退链,配置时连通性测试 + RecoveryAction - - **Claude Code CLI** — 扁平 if/else,`PROVIDER_MANAGED_BY_HOST` 宿主接管,per-provider 模型 ID 映射,529 自动切模型 -- 2026-04-04: 决策不做 Google Vertex,只走 AI Studio (Gemini API) 路线 -- 2026-04-04: 决策会话切换 provider 做软约束(提示开新会话),不硬锁 - ---- - -## Phase 1:Preset 声明式改造 + Schema 校验 - -### 设计思路 - -**学习 Craft Agents 的 Connection Template + OpenCode 的 Zod Schema。** - -现在 `provider-catalog.ts` 里的 preset 是一个巨大的数组,字段之间有隐式约束但没有显式校验。比如 `authStyle: 'auth_token'` 的 preset 不应该在 `defaultEnvOverrides` 里出现 `ANTHROPIC_API_KEY`,但没有任何东西阻止你这么写。 - -### 改动 - -#### 1.1 新增 Preset Schema(Zod) - -```typescript -// provider-catalog.ts 顶部新增 -const PresetSchema = z.object({ - key: z.string(), - name: z.string(), - protocol: ProtocolSchema, - authStyle: AuthStyleSchema, - baseUrl: z.string().url().optional(), - defaultEnvOverrides: z.record(z.string()).optional(), - defaultModels: z.array(CatalogModelSchema), - fields: z.array(z.string()), - sdkProxyOnly: z.boolean().optional(), - // --- 新增:服务商元信息 --- - meta: z.object({ - apiKeyUrl: z.string().url(), // 去哪获取 Key - docsUrl: z.string().url(), // 官方配置文档 - pricingUrl: z.string().url().optional(), // 定价页 - statusPageUrl: z.string().url().optional(), // 服务状态页 - billingModel: z.enum(['pay_as_you_go', 'coding_plan', 'token_plan', 'free', 'self_hosted']), - notes: z.array(z.string()).optional(), // 配置时显示的注意事项 - }), -}).refine(data => { - // 约束:auth_token 模式不应该在 envOverrides 里出现 ANTHROPIC_API_KEY - if (data.authStyle === 'auth_token' && data.defaultEnvOverrides?.ANTHROPIC_API_KEY !== undefined) { - return false; - } - // 约束:api_key 模式不应该在 envOverrides 里出现 ANTHROPIC_AUTH_TOKEN - if (data.authStyle === 'api_key' && data.defaultEnvOverrides?.ANTHROPIC_AUTH_TOKEN !== undefined) { - return false; - } - return true; -}, { message: 'authStyle 和 defaultEnvOverrides 中的 auth 变量冲突' }); -``` - -#### 1.2 编译期校验 - -在 `provider-catalog.ts` 末尾加一行: - -```typescript -// 编译期校验所有 preset -VENDOR_PRESETS.forEach(p => PresetSchema.parse(p)); -``` - -这样任何不合法的 preset 都会在 `npm run test`(typecheck)阶段直接报错,而不是等用户来报。 - -#### 1.3 Preset 单元测试 - -新增 `src/__tests__/unit/provider-preset.test.ts`: - -```typescript -for (const preset of VENDOR_PRESETS) { - test(`preset ${preset.key}: schema valid`, () => { - expect(() => PresetSchema.parse(preset)).not.toThrow(); - }); - - test(`preset ${preset.key}: meta has valid URLs`, () => { - // 验证 apiKeyUrl、docsUrl 格式正确 - }); - - test(`preset ${preset.key}: authStyle 和 envOverrides 不冲突`, () => { - // 具体校验逻辑 - }); - - test(`preset ${preset.key}: defaultModels 至少有一个`, () => { - expect(preset.defaultModels.length).toBeGreaterThan(0); - }); -} -``` - -### 每个 Preset 新增的 meta 字段内容 - -> 数据来源:用户提供的服务商配置文档 - -| 服务商 | apiKeyUrl | docsUrl | billingModel | -|--------|-----------|---------|-------------| -| Anthropic | platform.claude.com/settings/keys | platform.claude.com/docs/en/api/overview | pay_as_you_go | -| OpenRouter | openrouter.ai/workspaces/default/keys | openrouter.ai/docs/guides/coding-agents/claude-code-integration | pay_as_you_go | -| 智谱 GLM (CN) | bigmodel.cn/usercenter/proj-mgmt/apikeys | docs.bigmodel.cn/cn/coding-plan/tool/claude | coding_plan | -| 智谱 GLM (Global) | z.ai/manage-apikey/apikey-list | docs.z.ai/devpack/tool/claude | coding_plan | -| Kimi | kimi.com/code/console | kimi.com/code/docs/more/third-party-agents.html | pay_as_you_go | -| Moonshot | platform.moonshot.cn/console/api-keys | platform.moonshot.cn/docs/guide/agent-support | pay_as_you_go | -| MiniMax (CN) | platform.minimaxi.com/user-center/payment/token-plan | platform.minimaxi.com/docs/token-plan/claude-code | token_plan | -| MiniMax (Global) | platform.minimax.io/user-center/payment/token-plan | platform.minimax.io/docs/token-plan/opencode | token_plan | -| 火山引擎 | console.volcengine.com/ark (openManagement) | volcengine.com/docs/82379/1928262 | coding_plan | -| 小米 MiMo (按量) | platform.xiaomimimo.com/#/console/api-keys | platform.xiaomimimo.com/#/docs/integration/claudecode | pay_as_you_go | -| 小米 MiMo (套餐) | platform.xiaomimimo.com/#/console/plan-manage | platform.xiaomimimo.com/#/docs/integration/claudecode | token_plan | -| 阿里云百炼 | bailian.console.aliyun.com (Coding Plan) | help.aliyun.com/zh/model-studio/coding-plan | coding_plan | -| AWS Bedrock | console.aws.amazon.com (IAM) | aws.amazon.com/cn/bedrock/anthropic/ | pay_as_you_go | -| Google AI Studio | aistudio.google.com/api-keys | ai.google.dev/gemini-api/docs/gemini-3 | pay_as_you_go | -| Ollama | (无需) | docs.ollama.com/integrations/claude-code | free | -| LiteLLM | (无需) | docs.litellm.ai/docs/ | self_hosted | - -每个 preset 的 `meta.notes` 示例: - -```typescript -// 智谱 GLM -notes: ['高峰时段(14:00-18:00 UTC+8)消耗 3 倍积分', '需设置 API_TIMEOUT_MS=3000000'] - -// Kimi -notes: ['必须关闭 tool_search,否则会触发 400 错误'] - -// 小米 MiMo -notes: ['不支持 Thinking 模式,请在设置中关闭'] - -// 阿里云百炼 -notes: ['必须使用 Coding Plan 专用 Key(以 sk-sp- 开头)', '普通 DashScope Key 无法使用', '禁止用于自动化脚本'] - -// 火山引擎 -notes: ['需先在控制台激活 Endpoint', 'API Key 为临时凭证'] -``` - ---- - -## Phase 2:宿主接管 + authStyle 修正 - -### 2.1 注入 `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST` - -**一行改动**,在 `toClaudeCodeEnv()` 中: - -```typescript -// provider-resolver.ts, toClaudeCodeEnv() 开头 -env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST = '1'; -``` - -效果:用户终端 `~/.claude/settings.json` 中的 provider 路由变量不再覆盖 CodePilot 注入的配置。 - -### 2.2 修正 5 个 preset 的 authStyle - -| preset key | 改动 | -|-----------|------| -| `openrouter` | `authStyle: 'api_key'` → `'auth_token'`,移除 `defaultEnvOverrides.ANTHROPIC_API_KEY` | -| `glm-cn` | `authStyle: 'api_key'` → `'auth_token'`,移除 `defaultEnvOverrides.ANTHROPIC_API_KEY` | -| `glm-global` | 同上 | -| `moonshot` | `authStyle: 'api_key'` → `'auth_token'`,移除 `defaultEnvOverrides.ANTHROPIC_API_KEY`,新增 `ENABLE_TOOL_SEARCH: 'false'` | -| `kimi` | `authStyle: 'auth_token'` → `'api_key'`,移除 `defaultEnvOverrides.ANTHROPIC_AUTH_TOKEN`,新增 `ENABLE_TOOL_SEARCH: 'false'` | - -### 2.3 已有用户 DB 迁移 - -已保存的 provider 记录里可能带着旧的 authStyle 推断出的 env_overrides_json。需要在 DB migration 中: - -- **不删数据**(遵守 migration safety 规则) -- 如果 provider 的 base_url 匹配已知 preset,用新 preset 的 authStyle 更新记录 -- 对于用户手动创建的自定义 provider,保持不变 - ---- - -## Phase 3:配置时连通性验证 - -### 设计思路 - -**学习 Craft Agents 的 `validateAnthropicConnection()`。** 用户填完 Key → 点"测试" → 发一个最小请求 → 立即告诉用户结果。 - -### 改动 - -#### 3.1 新增 `POST /api/providers/test` 路由 - -```typescript -// 输入:provider 配置(不需要先保存到 DB) -// 流程: -// 1. 用 PresetSchema 校验配置合法性 -// 2. 用 toClaudeCodeEnv() 构建环境变量 -// 3. 发一个最小 Claude Agent SDK 请求(maxTurns: 1, systemPrompt: 'Reply OK') -// 4. 解析响应/错误 -// 输出:{ success: true } 或 { success: false, code: 'invalid_api_key', message: '...', suggestion: '...' } -``` - -#### 3.2 错误码分类 - -学习 OpenCode 的错误模式匹配 + Craft Agents 的 RecoveryAction: - -| 错误码 | 用户看到的提示 | 建议操作 | -|--------|-------------|---------| -| `invalid_api_key` | "API Key 无效,请检查是否复制完整" | 跳转到 apiKeyUrl | -| `endpoint_unreachable` | "无法连接到 {服务商名称},请检查网络" | 检查代理设置 | -| `wrong_key_type` | "Key 格式不匹配,{服务商} 需要 {格式} 开头的 Key" | 跳转到 apiKeyUrl | -| `model_not_found` | "模型 {name} 在此服务商不可用" | 查看可用模型列表 | -| `rate_limited` | "连接成功,但当前被限流" | 等待或升级套餐 | -| `billing_required` | "账户余额不足或未开通计费" | 跳转到 pricingUrl | -| `endpoint_not_activated` | "Endpoint 未激活(火山引擎)" | 跳转到控制台 | -| `tool_search_error` | "tool_search 调用失败(Kimi/Moonshot)" | 自动关闭或提示 | - -#### 3.3 UI 集成 - -配置表单底部加一个"测试连接"按钮: -- 点击后显示 loading -- 成功:绿色 ✓ "连接成功,检测到 {模型数} 个可用模型" -- 失败:红色 ✗ + 上述分类错误信息 + 建议操作按钮 - ---- - -## Phase 4:用户引导 UX(服务商信息面板) - -### 设计思路 - -**配置页面应该像一个向导,不是一个表单。** - -用户选择服务商后,应该看到: -1. 这家是谁(一句话描述 + 计费模式标签) -2. 去哪获取 Key(直接跳转按钮) -3. 注意事项(从 `meta.notes` 读取) -4. 填写区域(Key + 可选的模型选择) -5. 测试按钮 - -### 改动 - -#### 4.1 服务商选择卡片 - -每个服务商卡片显示: -- 图标 + 名称 -- 计费模式标签(`pay_as_you_go` / `coding_plan` / `token_plan` / `free`) -- 一句话描述 - -#### 4.2 配置面板(选中服务商后展开) - -``` -┌──────────────────────────────────────────┐ -│ [智谱 GLM 图标] 智谱 GLM(国内) │ -│ Coding Plan · 积分制 │ -│ │ -│ ⚠️ 高峰时段(14:00-18:00)消耗 3 倍积分 │ -│ │ -│ [去获取 API Key →] [查看配置文档 →] │ -│ │ -│ API Key: [________________________] │ -│ │ -│ [测试连接] │ -│ │ -│ ✓ 连接成功,检测到 3 个可用模型 │ -└──────────────────────────────────────────┘ -``` - -#### 4.3 数据来源 - -所有文案来自 preset 的 `meta` 字段——不需要额外维护,加新服务商时一起填。 - ---- - -## Phase 5:运行时错误治理 - -### 设计思路 - -**学习 Craft Agents 的 `AgentError` + `RecoveryAction` 模式。** - -现在用户看到的报错是 "Error: 401 Unauthorized" 或者更糟——一个技术性的 JSON。应该是 "智谱 GLM 返回了认证失败,可能是 API Key 已过期。[重新获取 Key →] [查看诊断 →]" - -### 改动 - -#### 5.1 错误信息携带服务商上下文 - -`error-classifier.ts` 的输出新增: - -```typescript -interface ClassifiedError { - code: ErrorCode; - message: string; // 用户看到的 - providerName: string; // "智谱 GLM" 不是 "glm-cn" - suggestion: string; // "请检查 API Key 是否过期" - actions: RecoveryAction[]; // [{ label: '重新获取 Key', url: apiKeyUrl }] -} -``` - -#### 5.2 学习 OpenCode 的错误模式匹配 - -```typescript -const OVERFLOW_PATTERNS = [ - /prompt is too long/i, // Anthropic - /input is too long/i, // Bedrock - /exceeds the context window/i, // OpenAI compatible - /maximum context length/i, // OpenRouter/DeepSeek -]; - -const AUTH_PATTERNS = [ - /invalid.*api.*key/i, - /unauthorized/i, - /authentication.*failed/i, -]; -``` - -不是按 HTTP status code 分类,而是按**错误消息模式**分类——因为不同服务商对同一种错误返回的 status code 不一样。 - ---- - -## Phase 6:模型目录动态化 - -### 设计思路 - -**不急于上独立的远程系统。** 先把现有的 `provider_models` + `role_models_json` 路径用好。 - -### 改动 - -#### 6.1 改善模型管理 UI - -- 用户可以在 Provider 设置中看到当前可用模型列表 -- 可以手动添加/删除模型(已有 `provider_models` 表支持) -- 手动添加的模型不会被 preset 更新覆盖 - -#### 6.2 模型发现(可选,学习 Craft Agents 的 ModelRefreshService) - -``` -回退链: -1. Provider API 发现(如果服务商提供 GET /models) -2. 用户自定义模型(provider_models 表) -3. Preset 默认模型列表 -``` - -保留用户的选择——如果用户手动选了某个模型且仍然可用,自动刷新不应该覆盖它。 - ---- - -## 对比:改造前 vs 改造后 - -| 场景 | 改造前 | 改造后 | -|------|-------|-------| -| 加新服务商 | 手写 preset 对象,没有校验,上线后用户报 bug 才发现错 | 写声明式配置 + meta,Schema 校验不过则 test 失败,不可能带错上线 | -| 改模型名 | 改 catalog,不确定有没有破坏别的,祈祷 | 改配置,preset 测试自动验证所有服务商不受影响 | -| 用户配置 GLM | 填 Key → 发消息 → 报错 401 → 截图发 Issue | 填 Key → 点测试 → 立即成功/立即看到"Key 无效,去这里重新获取" | -| 终端 Claude Code 配了 Bedrock,打开 CodePilot 选 OpenRouter | 请求莫名跑到 Bedrock,用户完全无法理解 | `PROVIDER_MANAGED_BY_HOST` 拦截,CodePilot 的选择不被干扰 | -| 运行时 API 报错 | "Error: 401 Unauthorized" | "智谱 GLM 认证失败,可能是 Key 过期。[重新获取 →]" | -| 百炼用户拿错 Key | 模糊报错,来回沟通 5 条 Issue 评论 | 配置时提示"请使用 sk-sp- 开头的 Coding Plan Key",测试时立即检出 | diff --git a/docs/handover/README.md b/docs/handover/README.md index 959206d7..450969ba 100644 --- a/docs/handover/README.md +++ b/docs/handover/README.md @@ -18,7 +18,6 @@ | onboarding-setup-center.md | 首次引导 Setup Center:三卡片引导流程、Claude Code 环境检测与冲突处理、Provider 三条凭据来源、目录校验回退链、Toast 系统、Windows 适配 | | generative-ui.md | 生成式 UI Widget 系统:代码围栏触发、receiver iframe 渲染、CSS 变量桥接、流式预览、高度缓存、安全模型、UX 优化清单 | | media-pipeline.md | 媒体管线:MCP image/audio 回显、Gallery 视频支持、文件树媒体预览、CLI 工具导入、MediaBlock 类型、入库机制、安全模型 | -| dashboard.md | 项目看板:MCP Server(5 工具)、数据源(file/mcp_tool/cli)、排序(CSS order)、导出(Electron 隔离窗口)、cross-widget 通信、CDN 脚本执行、fence-agnostic 解析器 | | provider-error-doctor.md | Provider/Auth/Error 全链路修复 + Doctor 诊断中心:16 类错误分类、Provider 生效修复、Auth Style 自动分流、5 探针诊断引擎、修复动作、runtime-log 脱敏、CI arm64 原生构建 | | memory-system-v3.md | 记忆系统 V3/V3.1:对话式 Onboarding、HEARTBEAT_OK 心跳协议、Memory Search MCP、时间衰减、Obsidian 感知、渐进式文件更新、Telegram 静默、transcript 裁剪 | | buddy-gamification.md | Buddy 游戏化系统:生成/进化/3D 视觉、心跳双模式(完整 tick + 软 hint)、定时任务调度器健壮性、通知队列/轮询/Electron IPC、symlink 安全、cron 4 年扫描 | @@ -26,6 +25,3 @@ | cli-upgrade-proxy.md | CLI 版本检测 + 一键升级 + 系统代理透传 + WinGet 支持 + Git for Windows 自动安装 | | tool-call-ux.md | 工具调用 UX 优化:thinking 展示全链路、工具注册表、上下文归组、状态动画、流式缓冲/节流 | | performance-memory.md | v0.45.0 内存优化:LRU 缓存、消息 300 条上限双向修剪、面板懒加载、流式文件读取、定时器追踪 | -| provider-architecture.md | 服务商架构全景:18 服务商配置对比、与 Claude Code 关系、认证/协议/模型矩阵、已知问题、优化建议 | -| provider-governance.md | 服务商治理系统:Zod Schema 防护、authStyle 修正 6 preset、宿主接管、连通性验证、引导 UX、错误恢复、模型 CRUD | -| sentry-error-reporting.md | Sentry 匿名错误上报:三层覆盖(browser/server/electron)、opt-out 机制、隐私保护、上报策略 | diff --git a/docs/handover/provider-architecture.md b/docs/handover/provider-architecture.md deleted file mode 100644 index 6daa1e68..00000000 --- a/docs/handover/provider-architecture.md +++ /dev/null @@ -1,518 +0,0 @@ -# 服务商架构全景 - -> 产品思考见 [docs/insights/user-audience-analysis.md](../insights/user-audience-analysis.md) -> 数据采集日期:2026-04-04 -> 数据来源:18 个服务商官方文档 + Claude Code 真实源码(/资料/src) + CodePilot 代码库 - ---- - -## 一、架构总览 - -CodePilot 通过 **Provider Catalog → Provider Resolver → Claude Code SDK subprocess** 三层架构连接 AI 服务商: - -``` -用户配置 Provider → DB (api_providers 表) - ↓ - resolveProvider() 统一解析 - ↓ - ┌───────────────────────┐ - │ toClaudeCodeEnv() │ → 构建环境变量 → Claude Code SDK 子进程 - │ toAiSdkConfig() │ → 构建 AI SDK 配置 → Vercel AI SDK 直接调用 - └───────────────────────┘ -``` - -### 关键文件 - -| 用途 | 文件 | -|------|------| -| Provider 预设定义(28+ 个) | `src/lib/provider-catalog.ts` | -| 统一解析 + 环境变量构建 | `src/lib/provider-resolver.ts` | -| Provider 快捷连接 UI | `src/components/settings/provider-presets.tsx` | -| SDK 流式调用 | `src/lib/claude-client.ts` | -| 能力缓存(模型/命令/MCP 状态) | `src/lib/agent-sdk-capabilities.ts` | -| 错误分类 | `src/lib/error-classifier.ts` | -| 诊断修复 | `src/lib/provider-doctor.ts` | -| DB schema(api_providers 表) | `src/lib/db.ts` | -| 模型列表 API | `src/app/api/providers/models/route.ts` | -| Provider CRUD API | `src/app/api/providers/route.ts` | -| Provider 类型定义 | `src/types/index.ts` | - ---- - -## 二、Claude Code 真实架构(源码分析) - -> 以下基于 Claude Code CLI 真实源码分析,非 DeepWiki 推断。 - -### 核心设计:扁平架构,无多态 - -Claude Code 的 Provider 架构**极其简单**——没有 Provider 抽象类、没有接口、没有注册表模式。就是一个返回字符串的函数 + if/else 分支。 - -**Provider 类型**(`utils/model/providers.ts`): -```typescript -type APIProvider = 'firstParty' | 'bedrock' | 'vertex' | 'foundry' -``` - -**Provider 选择**(`getAPIProvider()`): -``` -CLAUDE_CODE_USE_BEDROCK → 'bedrock' -CLAUDE_CODE_USE_VERTEX → 'vertex' -CLAUDE_CODE_USE_FOUNDRY → 'foundry' -否则 → 'firstParty' -``` - -### API 客户端创建(`services/api/client.ts`) - -`getAnthropicClient()` 是唯一入口,用 if/else 按 Provider 实例化不同 SDK: - -| Provider | SDK | 认证方式 | -|----------|-----|---------| -| firstParty | `new Anthropic()` | `ANTHROPIC_API_KEY` / keychain / OAuth Token | -| bedrock | `new AnthropicBedrock()` | AWS IAM / Bearer Token / 跳过认证 | -| vertex | `new AnthropicVertex()` | Google Auth / 跳过认证 | -| foundry | `new AnthropicFoundry()` | Azure API Key / Azure AD / 跳过认证 | - -**关键设计决策**:所有 Provider SDK 最终被 `as unknown as Anthropic` 强制转型。代码注释说 *"we have always been lying about the return type"*。这意味着下游代码统一通过 Anthropic SDK 接口调用,完全不感知具体 Provider。 - -### 模型 ID 的 Provider 映射(`utils/model/configs.ts`) - -每个模型有一个 per-provider 配置对象: - -```typescript -const CLAUDE_OPUS_4_6_CONFIG = { - firstParty: 'claude-opus-4-6', - bedrock: 'us.anthropic.claude-opus-4-6-v1', - vertex: 'claude-opus-4-6', - foundry: 'claude-opus-4-6', -} -``` - -Bedrock 模型 ID 有特殊处理:运行时通过 `getBedrockInferenceProfiles()` 异步查询推理配置文件,匹配自定义 ARN。 - -### 模型选择优先级(`utils/model/model.ts`) - -``` -/model 命令覆盖 → --model 启动参数 → ANTHROPIC_MODEL 环境变量 → 配置文件 → 内置默认值 -``` - -默认模型**按用户等级不同**: -- Anthropic 员工:Opus 4.6 (1M context) -- Max/Team Premium 订阅:Opus 4.6 -- 其他(PAYG、Enterprise、Pro):Sonnet 4.6 - -**3P Provider 滞后处理**:非 firstParty Provider 的默认模型可能是旧版(如 Sonnet 4.5 而非 4.6),因为第三方可用性滞后。 - -**模型别名**(`aliases.ts`):支持 `'sonnet'`、`'opus'`、`'haiku'`、`'best'`、`'opusplan'`,以及 `[1m]` 变体。 - -**旧模型重映射**:Opus 4.0/4.1 在 firstParty 上静默重映射到当前 Opus 默认值。 - -### 流式实现 - -**只有一个流式实现**,不是 per-provider 的(`services/api/claude.ts`): -1. `getAnthropicClient()` 创建客户端 -2. 调用 `anthropic.beta.messages.stream()` — 所有 Provider 走同一 API -3. 通过 `withStreamingVCR()` 包装(调试录制/回放) -4. 非流式回退:`executeNonStreamingRequest()` + `withRetry()` - -### 重试和错误处理(`services/api/withRetry.ts`) - -- 默认 10 次重试 + 指数退避 -- 529(过载):最多 3 次连续,然后切换到 `fallbackModel` -- 429(限流):提取 `retry-after` 头 -- 401:刷新 OAuth token 重新获取客户端 -- Bedrock 认证错误:清除 AWS 凭证缓存重试 -- Vertex 认证错误:清除 GCP 凭证缓存重试 -- ECONNRESET/EPIPE:禁用 HTTP keep-alive 重连 -- 无人值守模式(`CLAUDE_CODE_UNATTENDED_RETRY`):429/529 无限重试 - -错误分类(`services/api/errors.ts`):20+ 种分类,包括 `'invalid_api_key'`、`'bedrock_model_access'`、`'credit_balance_low'`、`'token_revoked'` 等。 - -### Beta 头和 Provider 特殊处理(`constants/betas.ts`) - -- Bedrock 只能通过 `extraBodyParams` 传递部分 beta(不是 header) -- Vertex 的 `countTokens` API 只允许特定 beta -- `isFirstPartyAnthropicBaseUrl()` 判断是否发送 firstParty-only 的 beta 头 -- 工具搜索 beta 不同:1P/Foundry 用 `advanced-tool-use-2025-11-20`,Vertex/Bedrock 用 `tool-search-tool-2025-10-19` - -### 完整环境变量表 - -| 变量 | 作用 | -|------|------| -| `ANTHROPIC_API_KEY` | 直连 API Key | -| `ANTHROPIC_AUTH_TOKEN` | Bearer Token(代理认证通用) | -| `ANTHROPIC_BASE_URL` | 自定义 API 端点 | -| `ANTHROPIC_CUSTOM_HEADERS` | 自定义 HTTP 头 | -| `CLAUDE_CODE_EXTRA_BODY` | 额外 JSON body 参数 | -| `ANTHROPIC_MODEL` | 覆盖默认模型 | -| `ANTHROPIC_SMALL_FAST_MODEL` | 覆盖 Haiku 模型 | -| `ANTHROPIC_DEFAULT_OPUS_MODEL` | 覆盖 Opus | -| `ANTHROPIC_DEFAULT_SONNET_MODEL` | 覆盖 Sonnet | -| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | 覆盖 Haiku | -| `ANTHROPIC_REASONING_MODEL` | 覆盖推理模型 | -| `ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES` | 3P 模型能力声明 | -| `CLAUDE_CODE_USE_BEDROCK` | 启用 Bedrock | -| `CLAUDE_CODE_USE_VERTEX` | 启用 Vertex | -| `CLAUDE_CODE_USE_FOUNDRY` | 启用 Foundry (Azure) | -| `CLAUDE_CODE_SKIP_BEDROCK_AUTH` | 跳过 Bedrock 认证 | -| `CLAUDE_CODE_SKIP_VERTEX_AUTH` | 跳过 Vertex 认证 | -| `CLAUDE_CODE_SKIP_FOUNDRY_AUTH` | 跳过 Foundry 认证 | -| `AWS_REGION` / `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | Bedrock 凭证 | -| `ANTHROPIC_BEDROCK_BASE_URL` | Bedrock 端点覆盖 | -| `CLOUD_ML_REGION` | Vertex 默认区域 | -| `ANTHROPIC_VERTEX_PROJECT_ID` | Vertex 项目 | -| `VERTEX_REGION_CLAUDE_*` | Vertex 按模型区域 | -| `ANTHROPIC_FOUNDRY_RESOURCE` | Azure 资源名 | -| `ANTHROPIC_FOUNDRY_BASE_URL` | Azure 端点 | -| `ANTHROPIC_FOUNDRY_API_KEY` | Azure API Key | -| `API_TIMEOUT_MS` | 请求超时 | -| `DISABLE_PROMPT_CACHING` | 禁用 prompt 缓存 | -| `ENABLE_TOOL_SEARCH` | 工具搜索开关 | -| `CLAUDE_CODE_UNATTENDED_RETRY` | 无人值守无限重试 | -| `CLAUDE_CODE_DISABLE_LEGACY_MODEL_REMAP` | 禁用旧模型重映射 | - ---- - -## 三、CodePilot 如何桥接 Claude Code - -### 桥接核心:`toClaudeCodeEnv()` - -`toClaudeCodeEnv()`(`provider-resolver.ts`)把 CodePilot DB 里的 Provider 配置翻译成 Claude Code 期望的环境变量,注入给 SDK 子进程: - -1. **凭证注入**: - - `authStyle === 'api_key'` → 同时设置 `ANTHROPIC_API_KEY` + `ANTHROPIC_AUTH_TOKEN` - - `authStyle === 'auth_token'` → 设置 `ANTHROPIC_AUTH_TOKEN` + 清空 `ANTHROPIC_API_KEY` - - `authStyle === 'env_only'` → 不注入 key(Bedrock/Vertex 走系统凭证) - -2. **环境清洗**:切换 Provider 时显式清除旧凭证(防止 Bedrock → Anthropic 时 AWS 凭证泄漏) - -3. **角色模型映射**:把 `roleModels.default/reasoning/small` 翻译成 `ANTHROPIC_MODEL/ANTHROPIC_REASONING_MODEL/ANTHROPIC_SMALL_FAST_MODEL` - -4. **上游模型 ID**:`upstreamModelId` 与 `modelId` 分离——UI 显示 "sonnet",实际发 "GLM-4.7" 到智谱 API - -### 与 Claude Code 的差异 - -| 方面 | Claude Code CLI | CodePilot | -|------|----------------|-----------| -| 架构风格 | 扁平 if/else,无多态 | Catalog + Resolver + DB,分层抽象 | -| Provider 数量 | 4 个(firstParty/bedrock/vertex/foundry) | 28+ 个预设 | -| Provider 选择 | 环境变量,单一 Provider | DB 存储,多 Provider 动态切换 | -| 协议支持 | anthropic / bedrock / vertex / foundry | + openai-compatible / openrouter / google / gemini-image(7 种) | -| 认证方式 | API Key / Bearer / IAM / Azure AD | + custom_header(4 种 authStyle) | -| 模型管理 | 静态 per-provider 配置 + 环境变量覆盖 | Catalog 预设 + DB 自定义 + SDK 运行时发现 | -| 模型 ID 映射 | 内置 `configs.ts` 精确映射(firstParty/bedrock/vertex/foundry) | `upstreamModelId` 别名系统 | -| 默认模型 | 按用户等级分层(员工/Max/PAYG) | 统一默认,用户可覆盖 | -| 3P 模型滞后 | 非 firstParty 默认旧版模型 | 无此区分 | -| 旧模型重映射 | Opus 4.0/4.1 → 当前默认 | 无 | -| 重试机制 | 10 次 + 指数退避 + 529 自动切模型 + 凭证刷新 | 依赖 SDK 内部重试 | -| 错误分类 | 20+ 种细分(含 bedrock_model_access 等) | 16 种分类 | -| Beta 头处理 | 精细的 per-provider beta 分流 | 依赖 SDK 处理 | -| Foundry (Azure) | ✅ 已支持 | ❌ **未支持** | -| 凭证轮换 | SDK 支持 ApiKeySetter + OAuth 刷新 | ❌ **未支持** | -| 模型能力声明 | `*_SUPPORTED_CAPABILITIES` 环境变量 | ❌ **未支持** | -| 无人值守重试 | `CLAUDE_CODE_UNATTENDED_RETRY` 无限重试 | ❌ **未支持** | - -### 我们应该从 Claude Code 学习的 - -1. **529 自动切模型**:Claude Code 连续 3 次 529 后自动切换到 `fallbackModel`,我们没有这个机制 -2. **凭证刷新**:Bedrock/Vertex 认证失败时自动清缓存重试,我们直接报错 -3. **3P 模型能力声明**:`ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES=effort,thinking` 让第三方模型声明自己支持哪些能力 -4. **旧模型重映射**:用户配置里的旧模型名自动映射到新版,减少用户困惑 -5. **Beta 头分流**:Bedrock 需要通过 `extraBodyParams` 传 beta 而非 header,Vertex 的 `countTokens` 有白名单——这些细节我们目前依赖 SDK 但可能不够 -6. **宿主接管机制**(`CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST`):见下方第四节 P0.1 - ---- - -## 三、服务商配置对比矩阵(官方要求 vs 当前实现) - -> 下表分两列:**官方文档推荐配置** vs **CodePilot 当前 preset 实际行为**。标 ⚠️ 的表示两者不一致。 - -### 高频服务商对齐状态 - -| 服务商 | 官方要求的认证方式 | CodePilot preset authStyle | 状态 | 差异说明 | -|--------|------------------|---------------------------|------|---------| -| Anthropic 官方 | `ANTHROPIC_API_KEY` | `api_key` | ✅ | 一致 | -| OpenRouter | `ANTHROPIC_AUTH_TOKEN` + `ANTHROPIC_API_KEY=""` | `api_key` | ⚠️ | preset 用 `api_key` 会同时设两个变量;`defaultEnvOverrides` 中的 `ANTHROPIC_API_KEY: ''` 被 AUTH_ENV_KEYS 跳过机制阻止生效 | -| 智谱 GLM(国内) | `ANTHROPIC_AUTH_TOKEN` | `api_key` | ⚠️ | 同上,`ANTHROPIC_API_KEY: ''` 不生效 | -| 智谱 GLM(海外) | `ANTHROPIC_AUTH_TOKEN` | `api_key` | ⚠️ | 同上 | -| Kimi | `ANTHROPIC_API_KEY` | `auth_token` | ⚠️ | 反了——官方用 API_KEY,preset 用 auth_token | -| Moonshot | `ANTHROPIC_AUTH_TOKEN` + `ENABLE_TOOL_SEARCH=false` | `api_key` | ⚠️ | authStyle 错 + 缺少 `ENABLE_TOOL_SEARCH=false` | -| MiniMax(国内) | `ANTHROPIC_AUTH_TOKEN` | `auth_token` | ✅ | 一致 | -| MiniMax(海外) | `ANTHROPIC_AUTH_TOKEN` | `auth_token` | ✅ | 一致 | -| 火山引擎 | `ANTHROPIC_AUTH_TOKEN` | `auth_token` | ✅ | 一致 | -| 小米 MiMo | `ANTHROPIC_AUTH_TOKEN` | `auth_token` | ✅ | 一致 | -| 阿里云百炼 | `ANTHROPIC_AUTH_TOKEN`(sk-sp-xxx) | `auth_token` | ✅ | 一致 | -| Ollama | `ANTHROPIC_AUTH_TOKEN=ollama` + `ANTHROPIC_API_KEY=""` | `auth_token` | ✅ | 一致 | -| AWS Bedrock | IAM / Bearer Token | `env_only` | ✅ | 一致 | -| Google AI Studio | `GEMINI_API_KEY` | `api_key`(media category) | ✅ | 一致;注:CodePilot 选择 AI Studio 路线,不跟随上游 Vertex 方案 | - -### Issue 归因:服务商配置问题不全是 CodePilot 的责任 - -GitHub 上 100+ 个服务商相关 Issue 的归因分三类: - -1. **~1/3 CodePilot preset 错误**(上表 ⚠️ 标注的项)— 修 preset authStyle 和注入 `PROVIDER_MANAGED_BY_HOST` 即可解决 -2. **~1/3 用户认知差**(以为终端 settings.json 配置能被 CodePilot 继承)— 需在配置引导中明确说明两套系统独立 -3. **~1/3 用户在服务商侧操作错误**(拿错 Key、未激活 endpoint 等)— 需配置向导放正确的 Key 获取链接 + 配置时立即验证 - -> 产品侧分析见 [docs/insights/user-audience-analysis.md](../insights/user-audience-analysis.md) 第八节 - -### 根因分析:为什么 `defaultEnvOverrides` 中的 `ANTHROPIC_API_KEY: ''` 不生效 - -`toClaudeCodeEnv()`(provider-resolver.ts:228-238)在注入 envOverrides 时,显式跳过 `AUTH_ENV_KEYS`(`ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`)。这是为了防止遗留 `extra_env` 中的占位符覆盖新注入的凭证。但副作用是:OpenRouter、GLM 等 preset 中想通过 `defaultEnvOverrides` 清空 `ANTHROPIC_API_KEY` 的意图**被静默忽略**。 - -### 按计费模式分组 - -| 模式 | 服务商 | -|------|--------| -| **Coding/Token Plan(订阅制)** | 火山引擎、阿里云百炼、小米 MiMo、智谱 GLM(积分制)、MiniMax | -| **按量付费** | Anthropic、OpenRouter、AWS Bedrock、Kimi/Moonshot、Google AI Studio | -| **免费 / 自托管** | Ollama、LiteLLM | -| **第三方代理(定价不一)** | Aiberm、PipeLLM | - -### Google 路线说明 - -CodePilot 的 Google 支持选择 **AI Studio (Gemini API)** 路线,不跟随上游 Claude Code 的 Vertex AI 方案。原因:Vertex 需要 GCP OAuth + 项目配置,门槛过高;AI Studio 只需一个 API Key。上游 Claude Code 原生支持 Vertex(`CLAUDE_CODE_USE_VERTEX`),这是上游能力,CodePilot 当前不实现。 - -### 按计费模式分组 - -| 模式 | 服务商 | -|------|--------| -| **Coding/Token Plan(订阅制)** | 火山引擎、阿里云百炼、小米 MiMo、智谱 GLM(积分制)、MiniMax | -| **按量付费** | Anthropic、OpenRouter、AWS Bedrock、Google Vertex、Kimi/Moonshot、Google Gemini | -| **免费 / 自托管** | Ollama、LiteLLM | -| **第三方代理(定价不一)** | Aiberm、PipeLLM | - ---- - -## 四、已发现的架构问题 - -### P0:直接导致用户配置失败 - -#### 1. 未设置 `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST` - -Claude Code 上游有一个关键的**宿主接管机制**(`managedEnv.ts`):当 host 在 spawn 环境中设置 `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1` 时,`withoutHostManagedProviderVars()` 会从 `~/.claude/settings.json` 中过滤掉 30+ 个 provider 路由相关的环境变量(包括 `ANTHROPIC_BASE_URL`、`ANTHROPIC_API_KEY`、`ANTHROPIC_MODEL` 等全部认证和模型变量)。 - -**CodePilot 没有设置这个变量。** 这意味着用户在终端直接用 Claude Code 时配置的 `~/.claude/settings.json`(比如指向 Bedrock 的配置)**会覆盖 CodePilot 注入的 provider 环境变量**,导致请求被路由到错误的 provider。 - -- **位置**:`provider-resolver.ts` — `toClaudeCodeEnv()` 需要注入 `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1` -- **上游参考**:`managedEnvConstants.ts:14` — `PROVIDER_MANAGED_ENV_VARS` 集合 -- **影响**:用户同时用终端 Claude Code 和 CodePilot 时,settings.json 配置互相干扰 - -#### 2. 多个高频 preset 的 authStyle 与官方文档不一致 - -见上方第三节对齐状态表。具体错配: - -| 服务商 | 官方要求 | 当前 preset | 错配后果 | -|--------|---------|------------|---------| -| OpenRouter | `auth_token` + 清空 `api_key` | `api_key`(同时注入两个变量) | `ANTHROPIC_API_KEY` 不为空,OpenRouter 可能用错认证头 | -| 智谱 GLM (CN/Global) | `auth_token` | `api_key`(同时注入两个变量) | 同上 | -| Moonshot | `auth_token` + `ENABLE_TOOL_SEARCH=false` | `api_key`(缺少 ENABLE_TOOL_SEARCH) | tool_search 调用导致 400 错误 | -| Kimi | `api_key`(`ANTHROPIC_API_KEY`) | `auth_token`(清空了 API_KEY) | 认证头方式反了 | - -**根因**:`toClaudeCodeEnv()` 的 AUTH_ENV_KEYS 跳过机制阻止了 `defaultEnvOverrides` 中清空 `ANTHROPIC_API_KEY` 的意图。 - -- **位置**:`provider-catalog.ts:162-244` — preset 定义,`provider-resolver.ts:228-238` — AUTH_ENV_KEYS 跳过 -- **修复方向**:把错配的 preset authStyle 改正,而非依赖 envOverrides 间接清空 - -#### 3. 无配置时验证 - -- 用户可以填入任意 base_url、任意格式的 api_key -- 验证只在实际 API 调用时才发生 -- **影响**:用户配置错误后看到模糊报错("Invalid API key"),不知道是 URL 错了还是 Key 错了还是协议选错了 - -### P1:影响维护性和可靠性 - -#### 4. 模型目录主要来自静态 preset,但已有多种补充路径 - -模型列表**不全是硬编码**——`providers/models/route.ts` 会合并以下来源: -1. `provider_models` 表(用户自定义) -2. `VendorPreset.defaultModels`(静态预设) -3. `role_models_json`(角色映射额外条目) -4. `env_overrides_json` 中的遗留 `ANTHROPIC_MODEL` - -但 preset 静态列表仍是大多数用户看到的来源,服务商出新模型后需要等 CodePilot 发版。 - -- **优化方向**:优先复用现有 `provider_models` / `role_models_json` 路径改善 UI,不要先起独立的远程模型系统 - -#### 5. 已删除 Provider 的会话恢复 - -- 用户在 Provider X 下创建会话,之后删除 Provider X -- 恢复会话时 `resolveProvider()` 找不到 Provider,回退到默认 -- 但 `sdk_session_id` 属于旧 Provider 的 conversation -- **已有缓解**:provider/model 变化时会清掉旧 `sdk_session_id`(chat session route:44),删除默认 provider 时 API 路径会做自愈(providers/[id]/route.ts:86) -- **建议**:做软约束——切换 provider 时提示"将开启新 SDK 会话",而不是硬锁禁止切换 - -#### 6. 默认 Provider 指向已删除记录 - -- `default_provider_id` 可能指向已删除的 Provider -- 只在 `/api/doctor/repair` 和少数路径自动修复 -- **影响**:其他路径解析到不存在的 Provider - -### P2:代码整洁度 - -#### 7. Chat 流程中重复 Provider 构建 - -- Chat route 调用 `resolveProviderUnified()`(route.ts:157),传给 `streamClaude()` -- `streamClaude()` 内部调用 `resolveForClaudeCode()`(claude-client.ts:445) -- **实际风险较低**:`resolveForClaudeCode()` 在拿到显式 provider 时直接返回(provider-resolver.ts:122),不会二次查 DB -- **本质**:重复构建 resolution 对象,代码整洁度问题,非竞态根因 - -#### 8. 角色模型环境变量注入时序 - -- 用户显式选择模型时,`roleModels.default` 被覆盖为 upstream ID -- 但 `roleModels.reasoning` / `roleModels.small` 仍指向旧值 -- **影响**:多角色场景下可能发错模型(概率低,大多数请求不使用多角色) - -#### 9. Base URL 标准化不一致 - -- `toAiSdkConfig()` 补全缺失的 `/v1` -- `toClaudeCodeEnv()` 不标准化——直接传原始 URL -- **已缓解**:SDK 内部有自己的标准化逻辑 - ---- - -## 五、服务商配置快速参考 - -### 每个服务商的 API Key 获取地址 - -| 服务商 | API Key 获取 | -|--------|-------------| -| Anthropic 官方 | https://platform.claude.com/settings/keys | -| OpenRouter | https://openrouter.ai/workspaces/default/keys | -| 智谱 GLM(国内) | https://bigmodel.cn/usercenter/proj-mgmt/apikeys | -| 智谱 GLM(海外) | https://z.ai/manage-apikey/apikey-list | -| Kimi | https://www.kimi.com/code/console | -| Moonshot | https://platform.moonshot.cn/console/api-keys | -| MiniMax(国内) | https://platform.minimaxi.com/user-center/payment/token-plan | -| MiniMax(海外) | https://platform.minimax.io/user-center/payment/token-plan | -| 火山引擎 | https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement | -| 小米 MiMo(按量) | https://platform.xiaomimimo.com/#/console/api-keys | -| 小米 MiMo(套餐) | https://platform.xiaomimimo.com/#/console/plan-manage | -| 阿里云百炼 | https://bailian.console.aliyun.com (Coding Plan 页) | -| AWS Bedrock | https://console.aws.amazon.com (IAM) | -| Google AI Studio | https://aistudio.google.com/api-keys | -| Ollama | 无需(本地) | -| LiteLLM | 无需(自托管) | -| Google Gemini | https://aistudio.google.com/api-keys | - -### 每个服务商的官方配置文档 - -| 服务商 | 文档链接 | -|--------|---------| -| Anthropic | https://platform.claude.com/docs/en/api/overview | -| OpenRouter | https://openrouter.ai/docs/guides/coding-agents/claude-code-integration | -| 智谱 GLM(国内) | https://docs.bigmodel.cn/cn/coding-plan/tool/claude | -| 智谱 GLM(海外) | https://docs.z.ai/devpack/tool/claude | -| Kimi | https://www.kimi.com/code/docs/more/third-party-agents.html | -| Moonshot | https://platform.moonshot.cn/docs/guide/agent-support | -| MiniMax(国内) | https://platform.minimaxi.com/docs/token-plan/claude-code | -| MiniMax(海外) | https://platform.minimax.io/docs/token-plan/opencode | -| 火山引擎 | https://www.volcengine.com/docs/82379/1928262 | -| 小米 MiMo | https://platform.xiaomimimo.com/#/docs/integration/claudecode | -| 阿里云百炼 | https://help.aliyun.com/zh/model-studio/coding-plan | -| AWS Bedrock | https://aws.amazon.com/cn/bedrock/anthropic/ | -| Google AI Studio | https://ai.google.dev/gemini-api/docs/gemini-3 | -| Ollama | https://docs.ollama.com/integrations/claude-code | -| LiteLLM | https://docs.litellm.ai/docs/ | -| Google Gemini(图片) | https://ai.google.dev/gemini-api/docs/image-generation | - -### 第三方 Anthropic 代理参考(不推荐,不透出到引导) - -| 服务商 | 参考文档 | -|--------|---------| -| Aiberm | https://aiberm.com/docs/zh/claude-code/ | -| PipeLLM | https://code.pipellm.ai/docs/cc-switch | - ---- - -## 六、架构优化建议 - -### P0:立即修复(直接影响用户配置成功率) - -#### 1. 接入 `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST` - -在 `toClaudeCodeEnv()` 中注入 `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1`,防止 `~/.claude/settings.json` 中的 provider 路由变量覆盖 CodePilot 注入的配置。 - -**改动范围**:`provider-resolver.ts` — 在构建环境变量时加一行。 -**风险**:低——这正是上游为 host 应用(如 Claude Code Desktop)设计的机制。 - -#### 2. 修正高频 preset 的 authStyle - -| 服务商 | 当前 authStyle | 应改为 | 额外改动 | -|--------|--------------|--------|---------| -| OpenRouter | `api_key` | `auth_token` | 无需 envOverrides 清空 API_KEY(auth_token 模式自动清空) | -| 智谱 GLM (CN) | `api_key` | `auth_token` | 移除 envOverrides 中的 `ANTHROPIC_API_KEY: ''` | -| 智谱 GLM (Global) | `api_key` | `auth_token` | 同上 | -| Moonshot | `api_key` | `auth_token` | 添加 `ENABLE_TOOL_SEARCH: 'false'` 到 envOverrides | -| Kimi | `auth_token` | `api_key` | 添加 `ENABLE_TOOL_SEARCH: 'false'` 到 envOverrides | - -**改动范围**:`provider-catalog.ts` — 5 个 preset 的 authStyle 字段。 -**风险**:中——需要确认已有用户的 DB 记录是否也需要迁移。 - -#### 3. 配置时连通性验证 - -在用户保存 Provider 配置时,立即发一个轻量请求验证: -- API Key 是否有效 -- Base URL 是否可达 -- 协议是否匹配 -- 模型是否存在 - -返回明确的分类错误:"Key 无效" / "URL 不通" / "协议不匹配" / "模型不存在" - -#### 4. 服务商特定注意事项前置 - -在配置 UI 中,根据所选服务商动态显示: -- 智谱:高峰期 3 倍计费提醒 -- Kimi:必须关闭 tool_search -- 小米 MiMo:不支持 Thinking 模式 -- 阿里云百炼:必须用 Coding Plan Key(sk-sp-xxx),不要用普通 DashScope Key -- 火山引擎:需先激活 endpoint -- Moonshot:建议设置每日消费上限 - -### P1:近期优化 - -#### 5. 模型目录改善 - -优先复用现有 `provider_models` / `role_models_json` 路径,改善 UI 让用户更容易手动添加模型。不要先起独立的远程模型系统。 - -#### 6. Provider 健康检查 - -定期对已配置的 base_url 做连通性检测,在设置页显示状态(绿/红)。删除正在使用的 Provider 时弹出警告。 - -#### 7. 会话 Provider 软约束 - -切换 Provider 时提示"将开启新 SDK 会话",而不是硬锁禁止切换。现有的 `sdk_session_id` 清除逻辑已经覆盖了大部分场景。 - -### P2:功能完整性 - -#### 8. Azure AI Foundry 支持 - -Claude Code 上游已支持 `@anthropic-ai/foundry-sdk`(`CLAUDE_CODE_USE_FOUNDRY`),CodePilot 缺少 `foundry` 协议。 - -#### 9. 3P 模型能力声明 - -上游支持通过 `ANTHROPIC_DEFAULT_*_MODEL_SUPPORTED_CAPABILITIES` 声明第三方模型能力。CodePilot 可以在 preset 中预设这些值,让 SDK 知道哪些第三方模型支持 thinking/effort 等功能。 - ---- - -## 七、配置模式统一性分析 - -### 核心发现:12/18 个服务商使用同一模式 - -``` -ANTHROPIC_BASE_URL = https:///anthropic -ANTHROPIC_AUTH_TOKEN = -ANTHROPIC_MODEL = -API_TIMEOUT_MS = 3000000 -``` - -这意味着**一个统一的配置表单就能覆盖 67% 的服务商**:选服务商 → 自动填 URL → 用户填 Key → 可选填模型 → 测试连通 → 完成。 - -### 需要特殊处理的服务商 - -| 服务商 | 特殊点 | -|--------|--------| -| OpenRouter | `auth_token` 模式 + 清空 `ANTHROPIC_API_KEY`,模型 ID 带前缀 `anthropic/` | -| Kimi | `api_key` 模式(非 auth_token),必须 `ENABLE_TOOL_SEARCH=false` | -| AWS Bedrock | 完全不同的认证流程(IAM),无 base_url | -| Ollama | 伪认证 `ollama`,本地 URL | -| Google AI Studio / Gemini(图片) | 独有 Gemini API 协议,不兼容 Anthropic/OpenAI | -| 阿里云百炼 | 必须用特殊格式 Key(sk-sp-xxx) | diff --git a/docs/handover/provider-governance.md b/docs/handover/provider-governance.md deleted file mode 100644 index e8331d50..00000000 --- a/docs/handover/provider-governance.md +++ /dev/null @@ -1,197 +0,0 @@ -# 服务商治理系统 - -> 产品思考见 [docs/insights/user-audience-analysis.md](../insights/user-audience-analysis.md) -> 架构全景见 [provider-architecture.md](./provider-architecture.md) -> 执行计划见 [docs/exec-plans/active/provider-governance.md](../exec-plans/active/provider-governance.md) - ---- - -## 一、改动概览 - -6 Phase 服务商治理方案的完整实施。目标:从根本上提升服务商配置的稳定性、灵活性和用户体验。 - ---- - -## 二、Preset 声明式防护(Phase 1) - -### Zod Schema 校验 - -**文件**:`src/lib/provider-catalog.ts` - -所有 `VENDOR_PRESETS` 在模块加载时经过 `PresetSchema.parse()` 校验。不合法的 preset 在 `npm run test` 阶段直接崩溃。 - -Schema refinement 规则: -- `auth_token` preset 禁止 `defaultEnvOverrides` 含 `ANTHROPIC_API_KEY` -- `api_key` preset 禁止 `defaultEnvOverrides` 含 `ANTHROPIC_AUTH_TOKEN` - -### Meta 元信息 - -每个 preset 新增 `meta` 字段: - -```typescript -meta?: { - apiKeyUrl?: string; // 用户获取 Key 的页面 - docsUrl?: string; // 官方配置文档 - pricingUrl?: string; // 定价页 - billingModel: 'pay_as_you_go' | 'coding_plan' | 'token_plan' | 'free' | 'self_hosted'; - notes?: string[]; // 配置时显示的注意事项 -} -``` - -18 个 chat/media preset 均已填充 meta。新增服务商时必须同时填写 meta。 - -### 测试覆盖 - -**文件**:`src/__tests__/unit/provider-preset.test.ts`(61 个测试) - -- 遍历所有 preset 逐个 Zod schema 校验 -- authStyle 与 envOverrides 冲突检测 -- 6 个 authStyle 回归测试 - ---- - -## 三、authStyle 修正(Phase 2) - -### 修正的 preset - -| preset | 改动 | 原因 | -|--------|------|------| -| openrouter | `api_key` → `auth_token` | 官方要求 Bearer + 清空 API_KEY | -| glm-cn | `api_key` → `auth_token` | 官方要求 ANTHROPIC_AUTH_TOKEN | -| glm-global | `api_key` → `auth_token` | 同上 | -| moonshot | `api_key` → `auth_token` + `ENABLE_TOOL_SEARCH: 'false'` | 官方要求 | -| kimi | `auth_token` → `api_key` + `ENABLE_TOOL_SEARCH: 'false'` | 官方用 ANTHROPIC_API_KEY | -| bailian | `api_key` → `auth_token` | 官方要求 ANTHROPIC_AUTH_TOKEN | - -### api_key 模式不再双注入 - -**文件**:`src/lib/provider-resolver.ts` - -`toClaudeCodeEnv()` 的 `api_key` 分支只设 `ANTHROPIC_API_KEY`,不再同时设 `ANTHROPIC_AUTH_TOKEN`。防止上游 Claude Code 添加 Bearer 头与 API-key-only 服务商冲突。 - -### 宿主接管 - -`toClaudeCodeEnv()` 末尾注入 `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1`,防止 `~/.claude/settings.json` 中的 provider 路由变量覆盖 CodePilot 注入的配置。 - -### authStyle 单一真相源 - -| 位置 | 之前 | 之后 | -|------|------|------| -| PresetConnectDialog 创建态 | 从 extra_env JSON 推断 | 读 `preset.authStyle` | -| PresetConnectDialog 编辑态 | 从 extra_env 推断 | 读 `preset.authStyle`(thirdparty 除外) | -| ProviderManager badge | 从 `extra_env` 检查 | 读 `findMatchingPreset().authStyle` | -| provider-doctor 修复建议 | 从 extra_env 推断 | 通过 `findPresetForLegacy()` 查 preset | - -Legacy fallback `inferAuthStyleFromLegacy()` 仅作用于无法匹配 preset 的自建 provider。 - ---- - -## 四、连通性验证(Phase 3) - -### API 端点 - -**文件**:`src/app/api/providers/test/route.ts` - -`POST /api/providers/test` — 接受 provider 配置(不需要先保存到 DB),直接发 HTTP 请求验证。 - -### 实现方式 - -**文件**:`src/lib/claude-client.ts` — `testProviderConnection()` - -直接发 HTTP POST 到 `{baseUrl}/v1/messages`,不走 Claude Code SDK 子进程(SDK 有自己的 keychain/OAuth 凭证解析,会导致假阳性)。 - -- `auth_token` → `Authorization: Bearer` 头 -- `api_key` → `x-api-key` 头 -- 2xx = 成功,4xx/5xx = 通过 error-classifier 分类 -- 网络错误(ECONNREFUSED/DNS/timeout)= 分类错误 -- bedrock/vertex/env_only = 跳过 HTTP 测试(返回 SKIPPED 中性状态) -- 15 秒超时 - -### 前端 - -**文件**:`src/components/settings/PresetConnectDialog.tsx` - -- "测试连接" 按钮,调用 POST /api/providers/test -- 三种结果状态:绿色成功 / 中性 SKIPPED / 红色失败 -- 失败时显示分类错误 + recoveryAction 链接 -- thirdparty preset 使用用户手选的 authStyle(不被 preset 固定值覆盖) -- 切换弹窗时自动清空上次测试结果 - ---- - -## 五、用户引导 UX(Phase 4) - -### QUICK_PRESETS 去重 - -**文件**:`src/components/settings/provider-presets.tsx` - -删除 295 行手写 `QUICK_PRESETS`,改为从 `VENDOR_PRESETS` 自动生成: - -- `resolveIcon(iconKey)` — 映射 iconKey → React 组件 -- `toQuickPreset(VendorPreset)` — 转换格式 -- `QUICK_PRESETS = VENDOR_PRESETS.map(toQuickPreset)` - -`QuickPreset` 接口保留(带 `icon: ReactNode` + `authStyle` + `meta`),下游组件无需改动。 - -新增服务商只需在 `provider-catalog.ts` 添加一个 preset 对象。 - -### Meta 引导面板 - -PresetConnectDialog 中显示: -- 计费标签(Pay-as-you-go / Coding Plan / Token Plan / Free / Self-hosted) -- "获取 API Key" 链接(指向 `meta.apiKeyUrl`) -- "配置指南" 链接(指向 CodePilot 官网文档) -- 注意事项警告(amber callout,来自 `meta.notes`) - ---- - -## 六、错误恢复动作(Phase 5) - -### 后端 - -**文件**:`src/lib/error-classifier.ts` - -- `RecoveryAction` 接口:`{ label, url?, action? }` -- `ErrorContext` 新增 `providerMeta`(apiKeyUrl/docsUrl/pricingUrl) -- `ClassifiedError` 新增 `recoveryActions` 字段 -- `buildRecoveryActions()` 按错误类别生成:AUTH → Get Key + Settings、RATE → Retry + Upgrade、SESSION → New Conversation -- 严重错误(PROCESS_CRASH/UNKNOWN/CLI_NOT_FOUND)自动上报 Sentry - -### 前端 - -**文件**:`src/hooks/useSSEStream.ts` - -SSE error 事件中渲染 recoveryActions: -- URL 动作 → markdown 外部链接 -- `open_settings` → `/settings#providers` -- `new_conversation` → `/chat` - ---- - -## 七、模型 CRUD API(Phase 6) - -**文件**:`src/app/api/providers/[id]/models/route.ts` - -- `GET` — 列出 provider 自定义模型 -- `POST` — 添加/更新(upsert by provider_id + model_id) -- `DELETE` — 删除 - -使用现有 `provider_models` 表和 DB 函数。 - ---- - -## 八、关键文件清单 - -| 文件 | 职责 | -|------|------| -| `src/lib/provider-catalog.ts` | Preset 定义 + Zod Schema + Meta | -| `src/lib/provider-resolver.ts` | 统一解析 + env 构建 + 宿主接管 | -| `src/lib/error-classifier.ts` | 错误分类 + RecoveryAction + Sentry 上报 | -| `src/lib/claude-client.ts` | testProviderConnection() 直接 HTTP 验证 | -| `src/app/api/providers/test/route.ts` | 连通性测试 API | -| `src/app/api/providers/[id]/models/route.ts` | 模型 CRUD API | -| `src/components/settings/provider-presets.tsx` | VENDOR_PRESETS → QUICK_PRESETS 映射 | -| `src/components/settings/PresetConnectDialog.tsx` | 配置弹窗 + 测试按钮 + Meta 面板 | -| `src/components/settings/ProviderManager.tsx` | authStyle badge 从 preset 读取 | -| `src/lib/provider-doctor.ts` | 修复建议从 preset catalog 读 authStyle | -| `src/__tests__/unit/provider-preset.test.ts` | 61 个 preset 校验测试 | diff --git a/docs/handover/sentry-error-reporting.md b/docs/handover/sentry-error-reporting.md deleted file mode 100644 index 1330ffab..00000000 --- a/docs/handover/sentry-error-reporting.md +++ /dev/null @@ -1,166 +0,0 @@ -# Sentry 匿名错误上报 - -> 产品思考见 [docs/insights/user-audience-analysis.md](../insights/user-audience-analysis.md) - ---- - -## 一、架构 - -三层覆盖,共用一个 DSN,共用一个 opt-out 机制。 - -``` -Browser (Renderer) Server (Node.js) Electron Main - │ │ │ - SentryInit.tsx instrumentation.ts electron/main.ts - @sentry/browser @sentry/node @sentry/electron - │ │ │ - beforeSend beforeSend Sentry.init() - (localStorage check) (strip auth headers) (marker file check) - │ │ │ - └─── DSN: next.config.ts env ──── DSN: hardcoded ┘ - │ - ~/.codepilot/sentry-disabled - (opt-out marker file) -``` - -### DSN - -- Renderer + Server:通过 `next.config.ts` 的 `NEXT_PUBLIC_SENTRY_DSN` 环境变量 -- Electron Main:硬编码(main process 不经过 Next.js env) -- DSN 是公开 ingest URL(Sentry 设计),安全提交到代码库 - -### 为什么不用 @sentry/nextjs - -`@sentry/nextjs@9.x` 的 peer dep 要求 `next@^13 || ^14 || ^15`,CodePilot 用 Next.js 16。改用: -- `@sentry/browser` — 客户端 -- `@sentry/node` — 服务端 -- `@sentry/electron` — 主进程 - ---- - -## 二、初始化点 - -### Browser(即时) - -**文件**:`src/components/layout/SentryInit.tsx` - -- 客户端组件,在 `AppShell` 中渲染 -- `useEffect` 中动态 `import('@sentry/browser')`,DSN 不存在时不加载 -- `beforeSend` 检查 `localStorage['codepilot:sentry-disabled']`,opt-out 即时生效 -- 去除 `ui.input` breadcrumb,删除 auth headers - -### Server(启动时) - -**文件**:`src/instrumentation.ts` - -- Next.js `register()` hook,服务启动时执行一次 -- 读取 `~/.codepilot/sentry-disabled` marker file -- 如果 marker 为 `true`,不初始化 `@sentry/node` -- opt-out 变更需**重启应用**才对 server 层生效 - -### Electron Main(启动时) - -**文件**:`electron/main.ts`(文件最顶部,所有其他 import 之前) - -- 读取 `~/.codepilot/sentry-disabled` marker file -- 如果 marker 为 `true`,不调用 `Sentry.init()` -- opt-out 变更需**重启应用**才对 main process 生效 - ---- - -## 三、Opt-out 机制 - -### 用户界面 - -**文件**:`src/components/settings/GeneralSection.tsx` — `SentryToggle` 组件 - -- 位置:Settings > General 卡片内,紧跟 Setup Center 下方 -- 使用 `useSyncExternalStore` 读取 localStorage(无 hydration mismatch) -- 默认开启,用户可关闭 - -### 持久化 - -切换开关时同时写两个位置: -1. `localStorage['codepilot:sentry-disabled']` — browser 层即时读取 -2. `~/.codepilot/sentry-disabled` 文件 — server + electron main 启动时读取 - -文件写入通过 `POST /api/settings/sentry`(`src/app/api/settings/sentry/route.ts`)。 - -### 生效时机 - -| 层 | opt-out 生效 | -|---|---| -| Browser | 即时(beforeSend 每次检查 localStorage) | -| Server | 重启后(instrumentation.ts 只在 register() 读一次) | -| Electron Main | 重启后(main.ts 顶部只读一次) | - -文案已明确提示用户"更改后需重启应用才能完全生效"。 - ---- - -## 四、上报策略 - -### 什么会被上报 - -**文件**:`src/lib/error-classifier.ts` — `reportToSentry()` - -仅上报严重错误类别: -- `PROCESS_CRASH` — Claude Code SDK 进程崩溃 -- `UNKNOWN` — 无法分类的错误 -- `CLI_NOT_FOUND` — CLI 找不到 -- `CLI_INSTALL_CONFLICT` — CLI 安装冲突 -- `MISSING_GIT_BASH` — Windows 缺 Git Bash -- `PROVIDER_NOT_APPLIED` — Provider 未生效 -- `SESSION_STATE_ERROR` — 会话状态损坏 - -### 什么不会被上报 - -- `RATE_LIMITED` — 预期内,限流 -- `CONTEXT_TOO_LONG` — 预期内,自动压缩处理 -- `AUTH_REJECTED` / `AUTH_FORBIDDEN` — 用户配置问题 -- `NETWORK_UNREACHABLE` — 网络问题 -- `RESUME_FAILED` — 会话恢复失败(自动处理) - -### React 渲染错误 - -**文件**:`src/components/layout/ErrorBoundary.tsx` - -`componentDidCatch` 中通过 `import('@sentry/browser').then(Sentry.captureException)` 上报所有未捕获的 React 渲染错误。 - -### 隐私保护 - -- 不采集 performance trace(`tracesSampleRate: 0`) -- 不采集 session replay -- 不采集用户输入 breadcrumb(`ui.input` 过滤) -- 删除 auth headers(`x-api-key`、`authorization`、`anthropic-api-key`) -- 不含对话内容 - ---- - -## 五、关键文件清单 - -| 文件 | 职责 | -|------|------| -| `next.config.ts` | DSN 环境变量(NEXT_PUBLIC_SENTRY_DSN) | -| `electron/main.ts` | Electron main Sentry.init() + opt-out check | -| `src/instrumentation.ts` | Server Sentry.init() + opt-out check | -| `src/components/layout/SentryInit.tsx` | Browser 动态初始化 | -| `src/components/layout/ErrorBoundary.tsx` | React 错误捕获 → Sentry | -| `src/components/layout/AppShell.tsx` | 渲染 SentryInit | -| `src/lib/error-classifier.ts` | reportToSentry() 严重错误上报 | -| `src/components/settings/GeneralSection.tsx` | SentryToggle opt-out 开关 | -| `src/app/api/settings/sentry/route.ts` | opt-out marker 文件读写 API | -| `src/i18n/en.ts` + `zh.ts` | 开关文案 + 重启提示 | - ---- - -## 六、免费额度管理 - -Sentry 免费版限额 5,000 错误/月。通过以下方式控制: - -1. 只上报 `SENTRY_REPORTABLE` 集合中的严重错误(7 个类别) -2. `tracesSampleRate: 0` — 不采性能数据 -3. `replaysSessionSampleRate: 0` — 不录回放 -4. 预期内错误(限流、上下文过长)不上报 - -如果接近额度,可以在 Sentry Dashboard 设置 rate limiting。 diff --git a/docs/insights/README.md b/docs/insights/README.md index 7d87a36a..299cae97 100644 --- a/docs/insights/README.md +++ b/docs/insights/README.md @@ -15,4 +15,4 @@ | [cli-upgrade-proxy.md](./cli-upgrade-proxy.md) | [handover/cli-upgrade-proxy.md](../handover/cli-upgrade-proxy.md) | CLI 升级 + 代理透传:P0 版本问题、分渠道升级策略、系统代理无感透传、Git 依赖引导 | | [tool-call-ux.md](./tool-call-ux.md) | [handover/tool-call-ux.md](../handover/tool-call-ux.md) | 工具调用 UX:thinking 展示设计决策、注册表 vs if/else、归组阈值、缓冲旁路、竞品对比 | | [performance-memory.md](./performance-memory.md) | [handover/performance-memory.md](../handover/performance-memory.md) | 内存优化:LRU vs 定期清理、300 条上限 + reconciliation、定时器泄漏、大文件流式读取 | -| [user-audience-analysis.md](./user-audience-analysis.md) | [handover/provider-architecture.md](../handover/provider-architecture.md) | 用户受众分析:画像、需求优先级、竞品格局、品牌定位路线取舍(2026-04-04 数据快照) | +| [user-audience-analysis.md](./user-audience-analysis.md) | 无(纯品牌/用户研究) | 用户受众分析:画像、需求优先级、竞品格局、品牌定位路线取舍(2026-04-04 数据快照) | diff --git a/docs/insights/user-audience-analysis.md b/docs/insights/user-audience-analysis.md index 0f41e578..c866e3cc 100644 --- a/docs/insights/user-audience-analysis.md +++ b/docs/insights/user-audience-analysis.md @@ -219,53 +219,17 @@ CodePilot 的自我定位是 **"A desktop GUI for Claude Code"**,但数据显 2. **多服务商** — 17+ provider 开箱即用 3. **独特能力** — 远程 Bridge、生成式 UI、助理工作区 -### 服务商适配问题归因(架构审查后修正) - -> 技术细节见 [docs/handover/provider-architecture.md](../handover/provider-architecture.md) - -原始数据显示 100+ 个 Issue 与服务商适配相关。经源码审查和官方文档对照,问题**并非全部是 CodePilot 的责任**,归因为三类: - -#### 1/3 — CodePilot preset 配置错误(我们的锅) - -- 5 个高频服务商(OpenRouter、智谱 GLM x2、Moonshot、Kimi)的 `authStyle` 与官方文档要求不一致,导致环境变量注入方式错误,表现为 401/400 -- 未设置 `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST`,导致用户终端 `~/.claude/settings.json` 中的配置覆盖 CodePilot 注入的 provider,请求被路由到错误服务商 -- **解法**:修代码——改 preset authStyle + 注入宿主接管标志 - -#### 1/3 — 用户对 CodePilot 与 Claude Code 关系的误解(认知问题) - -- 用户以为在终端 `~/.claude/settings.json` 里配了环境变量,CodePilot 就能直接用 -- 实际上 CodePilot 有独立的 Provider 系统,和终端 Claude Code 是两套配置 -- 用户不理解为什么终端好使但 CodePilot 不好使 -- **解法**:改引导——在配置页面明确告知"请在此重新配置服务商,与终端 Claude Code 配置无关" - -#### 1/3 — 用户在服务商侧操作错误(服务商的锅 + 用户操作) - -- Coding Plan 页面有多个 API Key 入口,用户拿了错的 Key(比如百炼的普通 DashScope Key 而非 sk-sp-xxx 的 Coding Plan Key) -- 买了按量付费但填到了 Coding Plan 入口,或反过来 -- 火山引擎没先激活 endpoint 就填 Key -- **解法**:加验证+跳转——配置向导里直接放"去这里获取 Key"的链接(指到正确页面),配完后立即测试连通性 - -### 执行项状态 +### 四个待解决的执行项 | 项目 | 状态 | 说明 | |------|------|------| | README 重构 | **已完成** | 定位、badges、下载前置、服务商表格、Agent 叙事 | -| 服务商架构文档 | **已完成** | 18 服务商对照、Claude Code 源码分析、preset 错配识别 → [handover/provider-architecture.md](../handover/provider-architecture.md) | -| preset authStyle 修正 | **已完成** | 6 个 preset 对齐官方文档 + Zod Schema 防护 + 61 个回归测试 | -| `PROVIDER_MANAGED_BY_HOST` 接入 | **已完成** | toClaudeCodeEnv() 末尾注入,防止 settings.json 覆盖 | -| api_key 模式不再双注入 | **已完成** | api_key 只设 ANTHROPIC_API_KEY,不再设 AUTH_TOKEN | -| 配置时连通性验证 | **已完成** | POST /api/providers/test + 前端测试按钮 + 带 preset 默认模型 | -| 配置引导优化 | **已完成** | meta 面板(API Key 链接 + 计费标签 + 注意事项 + CodePilot 文档链接) | -| 错误恢复动作 | **已完成** | RecoveryAction 后端 + SSE + 前端渲染(URL 链接 + 应用内导航) | -| QUICK_PRESETS 去重 | **已完成** | 从 VENDOR_PRESETS 自动生成,-181 行,单一数据源 | -| authStyle 单一真相源 | **已完成** | ProviderManager badge + Doctor + PresetConnectDialog 均从 preset catalog 读取 | -| 模型 CRUD API | **已完成** | GET/POST/DELETE /api/providers/[id]/models | -| 官网 providers 文档更新 | **已完成** | 国内服务商表格修正 + 注意事项 + 小米 MiMo | +| 服务商架构文档 | **待启动** | 需要创始人提供各服务商配置页面资料后,梳理完整架构和动态数据更新机制 | | 轻量级匿名报错 | **待启动** | 方案:接入 Sentry 免费版(5,000 错误/月),覆盖 Electron + Next.js 双层 | -| 细分错误模式 | **待启动** | billing_required、endpoint_not_activated、tool_search_error 等待补充 | +| 服务商配置向导化 | **待启动** | 配置预设模板 + 连通性测试 + 分步引导,降低小白用户配置失败率 | ### 项目背景补充 - 项目由单人创始人独立运营,全部开发通过 AI 协助完成(Vibe Coding),创始人不直接编写代码 - 功能层面已基本堆叠完毕,当前阶段重点是细节打磨、稳定性提升和品牌重塑 -- 服务商模块已完成系统级治理(6 Phase 全部完成)——后续改动应参照 [handover/provider-architecture.md](../handover/provider-architecture.md) 和 [exec-plans/active/provider-governance.md](../exec-plans/active/provider-governance.md) +- 服务商模块是改动风险最高的区域——架构文档缺失导致每次改动容易引发回归 diff --git a/electron/main.ts b/electron/main.ts index 1165a590..220b81e6 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,23 +1,3 @@ -// Sentry must be initialized before all other imports to catch early crashes -import * as Sentry from '@sentry/electron/main'; -import { existsSync, readFileSync } from 'fs'; -import { join } from 'path'; - -// Check opt-out before init — reads a marker file that the renderer writes -const sentryOptOutPath = join( - process.env.HOME || process.env.USERPROFILE || '', - '.codepilot', - 'sentry-disabled', -); -const sentryDisabled = existsSync(sentryOptOutPath) && - readFileSync(sentryOptOutPath, 'utf-8').trim() === 'true'; - -if (!sentryDisabled) { - Sentry.init({ - dsn: 'https://245dc3525425bcd8eb99dd4b9a2ca5cd@o4511161899548672.ingest.us.sentry.io/4511161904791552', - }); -} - import { app, BrowserWindow, Notification, nativeImage, dialog, session, utilityProcess, ipcMain, shell, Tray, Menu } from 'electron'; import path from 'path'; import { execFileSync, spawn, ChildProcess } from 'child_process'; @@ -1261,59 +1241,6 @@ app.whenReady().then(async () => { return { canceled: result.canceled, filePaths: result.filePaths }; }); - // --- Widget export IPC handler --- - // Uses an isolated BrowserWindow for secure, high-fidelity widget screenshot. - // The window is hidden, has its own session partition, no preload, no IPC access. - ipcMain.handle('widget:export-png', async (_event, { html, width }: { html: string; width: number }) => { - const exportWindow = new BrowserWindow({ - show: false, - width, - height: 2000, - webPreferences: { - contextIsolation: true, - nodeIntegration: false, - sandbox: true, - partition: `export-${Date.now()}`, // isolated session, destroyed with window - // No preload — no IPC access from this window - }, - }); - - // Block all navigation and window.open — prevents data exfiltration via top-level nav - exportWindow.webContents.on('will-navigate', (e) => e.preventDefault()); - exportWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' })); - - try { - // Load the widget HTML directly - await exportWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`); - - // Wait for widget scripts to finish (scriptsReady signal or timeout) - await new Promise((resolve) => { - let resolved = false; - const done = () => { if (!resolved) { resolved = true; resolve(); } }; - // Listen for console message from widget:scriptsReady - exportWindow.webContents.on('console-message', (_e, _level, message) => { - if (message === '__scriptsReady__') done(); - }); - // Fallback timeout for widgets without CDN/scripts - setTimeout(done, 6000); - }); - - // Extra delay for final paint - await new Promise(r => setTimeout(r, 300)); - - // Get actual content height and resize - const contentHeight = await exportWindow.webContents.executeJavaScript('document.body.scrollHeight'); - exportWindow.setSize(width, Math.min(contentHeight + 20, 4000)); - await new Promise(r => setTimeout(r, 100)); - - // Capture using Chromium's native screenshot - const image = await exportWindow.webContents.capturePage(); - return image.toPNG().toString('base64'); - } finally { - exportWindow.destroy(); - } - }); - // --- Terminal IPC handlers --- terminalManager.setOnData((id, data) => { mainWindow?.webContents.send('terminal:data', { id, data }); diff --git a/next.config.ts b/next.config.ts index 5fd7f624..6c0eeb39 100644 --- a/next.config.ts +++ b/next.config.ts @@ -9,7 +9,6 @@ const nextConfig: NextConfig = { serverExternalPackages: ['better-sqlite3', 'discord.js', '@discordjs/ws', 'zlib-sync'], env: { NEXT_PUBLIC_APP_VERSION: pkg.version, - NEXT_PUBLIC_SENTRY_DSN: 'https://245dc3525425bcd8eb99dd4b9a2ca5cd@o4511161899548672.ingest.us.sentry.io/4511161904791552', }, }; diff --git a/package-lock.json b/package-lock.json index c7f4bd5a..ed278e28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codepilot", - "version": "0.47.0", + "version": "0.46.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codepilot", - "version": "0.47.0", + "version": "0.46.0", "license": "BUSL-1.1", "workspaces": [ "apps/*", @@ -24,9 +24,6 @@ "@lobehub/icons": "^4.6.0", "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-use-controllable-state": "^1.2.2", - "@sentry/browser": "^9.27.0", - "@sentry/electron": "^6.3.0", - "@sentry/node": "^9.27.0", "@streamdown/cjk": "^1.0.1", "@streamdown/code": "^1.0.1", "@streamdown/math": "^1.0.1", @@ -5427,558 +5424,6 @@ "node": ">=8.0.0" } }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz", - "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/context-async-hooks": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", - "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/core": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", - "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.28.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", - "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", - "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.57.2", - "@types/shimmer": "^1.2.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", - "shimmer": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-amqplib": { - "version": "0.46.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.46.1.tgz", - "integrity": "sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-connect": { - "version": "0.43.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.1.tgz", - "integrity": "sha512-ht7YGWQuV5BopMcw5Q2hXn3I8eG8TH0J/kc/GMcW4CuNTgiP6wCu44BOnucJWL3CmFWaRHI//vWyAhaC8BwePw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/connect": "3.4.38" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-dataloader": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.16.1.tgz", - "integrity": "sha512-K/qU4CjnzOpNkkKO4DfCLSQshejRNAJtd4esgigo/50nxCB6XCyi1dhAblUHM9jG5dRm8eu0FB+t87nIo99LYQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.47.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.47.1.tgz", - "integrity": "sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-fs": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.19.1.tgz", - "integrity": "sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-generic-pool": { - "version": "0.43.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.43.1.tgz", - "integrity": "sha512-M6qGYsp1cURtvVLGDrPPZemMFEbuMmCXgQYTReC/IbimV5sGrLBjB+/hANUpRZjX67nGLdKSVLZuQQAiNz+sww==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-graphql": { - "version": "0.47.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.47.1.tgz", - "integrity": "sha512-EGQRWMGqwiuVma8ZLAZnExQ7sBvbOx0N/AE/nlafISPs8S+QtXX+Viy6dcQwVWwYHQPAcuY3bFt3xgoAwb4ZNQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-hapi": { - "version": "0.45.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.45.2.tgz", - "integrity": "sha512-7Ehow/7Wp3aoyCrZwQpU7a2CnoMq0XhIcioFuKjBb0PLYfBfmTsFTUyatlHu0fRxhwcRsSQRTvEhmZu8CppBpQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.57.2.tgz", - "integrity": "sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/instrumentation": "0.57.2", - "@opentelemetry/semantic-conventions": "1.28.0", - "forwarded-parse": "2.1.2", - "semver": "^7.5.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", - "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/instrumentation-http/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@opentelemetry/instrumentation-ioredis": { - "version": "0.47.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.47.1.tgz", - "integrity": "sha512-OtFGSN+kgk/aoKgdkKQnBsQFDiG8WdCxu+UrHr0bXScdAmtSzLSraLo7wFIb25RVHfRWvzI5kZomqJYEg/l1iA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/redis-common": "^0.36.2", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-kafkajs": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.7.1.tgz", - "integrity": "sha512-OtjaKs8H7oysfErajdYr1yuWSjMAectT7Dwr+axIoZqT9lmEOkD/H/3rgAs8h/NIuEi2imSXD+vL4MZtOuJfqQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-knex": { - "version": "0.44.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.44.1.tgz", - "integrity": "sha512-U4dQxkNhvPexffjEmGwCq68FuftFK15JgUF05y/HlK3M6W/G2iEaACIfXdSnwVNe9Qh0sPfw8LbOPxrWzGWGMQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-koa": { - "version": "0.47.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.47.1.tgz", - "integrity": "sha512-l/c+Z9F86cOiPJUllUCt09v+kICKvT+Vg1vOAJHtHPsJIzurGayucfCMq2acd/A/yxeNWunl9d9eqZ0G+XiI6A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-lru-memoizer": { - "version": "0.44.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.44.1.tgz", - "integrity": "sha512-5MPkYCvG2yw7WONEjYj5lr5JFehTobW7wX+ZUFy81oF2lr9IPfZk9qO+FTaM0bGEiymwfLwKe6jE15nHn1nmHg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongodb": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.52.0.tgz", - "integrity": "sha512-1xmAqOtRUQGR7QfJFfGV/M2kC7wmI2WgZdpru8hJl3S0r4hW0n3OQpEHlSGXJAaNFyvT+ilnwkT+g5L4ljHR6g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongoose": { - "version": "0.46.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.46.1.tgz", - "integrity": "sha512-3kINtW1LUTPkiXFRSSBmva1SXzS/72we/jL22N+BnF3DFcoewkdkHPYOIdAAk9gSicJ4d5Ojtt1/HeibEc5OQg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql": { - "version": "0.45.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.1.tgz", - "integrity": "sha512-TKp4hQ8iKQsY7vnp/j0yJJ4ZsP109Ht6l4RHTj0lNEG1TfgTrIH5vJMbgmoYXWzNHAqBH2e7fncN12p3BP8LFg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/mysql": "2.15.26" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql2": { - "version": "0.45.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.2.tgz", - "integrity": "sha512-h6Ad60FjCYdJZ5DTz1Lk2VmQsShiViKe0G7sYikb0GHI0NVvApp2XQNRHNjEMz87roFttGPLHOYVPlfy+yVIhQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@opentelemetry/sql-common": "^0.40.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-pg": { - "version": "0.51.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.51.1.tgz", - "integrity": "sha512-QxgjSrxyWZc7Vk+qGSfsejPVFL1AgAJdSBMYZdDUbwg730D09ub3PXScB9d04vIqPriZ+0dqzjmQx0yWKiCi2Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.26.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@opentelemetry/sql-common": "^0.40.1", - "@types/pg": "8.6.1", - "@types/pg-pool": "2.0.6" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-redis-4": { - "version": "0.46.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.46.1.tgz", - "integrity": "sha512-UMqleEoabYMsWoTkqyt9WAzXwZ4BlFZHO40wr3d5ZvtjKCHlD4YXLm+6OLCeIi/HkX7EXvQaz8gtAwkwwSEvcQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/redis-common": "^0.36.2", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-tedious": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.18.1.tgz", - "integrity": "sha512-5Cuy/nj0HBaH+ZJ4leuD7RjgvA844aY2WW+B5uLcWtxGjRZl3MNLuxnNg5DYWZNPO+NafSSnra0q49KWAHsKBg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/tedious": "^4.0.14" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.10.1.tgz", - "integrity": "sha512-rkOGikPEyRpMCmNu9AQuV5dtRlDmJp2dK5sw8roVshAGoB6hH/3QjDtRhdwd75SsJwgynWUNRUYe0wAkTo16tQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.7.0" - } - }, - "node_modules/@opentelemetry/instrumentation/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@opentelemetry/redis-common": { - "version": "0.36.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz", - "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/resources": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", - "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/semantic-conventions": "1.28.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", - "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", - "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/semantic-conventions": "1.28.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", - "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", - "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sql-common": { - "version": "0.40.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz", - "integrity": "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.1.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0" - } - }, "node_modules/@orama/orama": { "version": "3.1.18", "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-3.1.18.tgz", @@ -6057,18 +5502,6 @@ "object-assign": "^4.1.1" } }, - "node_modules/@prisma/instrumentation": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.11.1.tgz", - "integrity": "sha512-mrZOev24EDhnefmnZX7WVVT7v+r9LttPRqf54ONvj6re4XMF7wFTpK2tLJi4XHB7fFp/6xhYbgRel8YV7gQiyA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.8" - } - }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -8337,536 +7770,143 @@ "react-dom": ">=18.0.0" } }, - "node_modules/@rc-component/upload": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.0.tgz", - "integrity": "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.9.0.tgz", - "integrity": "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg==", - "license": "MIT", - "dependencies": { - "is-mobile": "^5.0.0", - "react-is": "^18.2.0" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@rc-component/util/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, - "node_modules/@rc-component/virtual-list": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.0.2.tgz", - "integrity": "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.20.0", - "@rc-component/resize-observer": "^1.0.1", - "@rc-component/util": "^1.4.0", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", - "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^11.0.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "11.1.4", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", - "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sapphire/async-queue": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", - "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/shapeshift": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", - "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=v16" - } - }, - "node_modules/@sapphire/snowflake": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", - "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "license": "MIT" - }, - "node_modules/@sentry-internal/browser-utils": { - "version": "9.47.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.47.1.tgz", - "integrity": "sha512-twv6YhrUlPkvKz4/iQDH4KHgcv9t4cMjmZPf4/dCSCXn4/GOjzjx2d74c1w+1KOdS7lcsQzI+MtbK6SeYLiGfQ==", - "license": "MIT", - "dependencies": { - "@sentry/core": "9.47.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry-internal/feedback": { - "version": "9.47.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.47.1.tgz", - "integrity": "sha512-xJ4vKvIpAT8e+Sz80YrsNinPU0XV7jPxPjdZ4ex8R2mMvx7pM0gq8JiR/sIVmNiOE0WiUDr6VwLDE8j2APSRMA==", - "license": "MIT", - "dependencies": { - "@sentry/core": "9.47.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry-internal/replay": { - "version": "9.47.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.47.1.tgz", - "integrity": "sha512-O9ZEfySpstGtX1f73m3NbdbS2utwPikaFt6sgp74RG4ZX4LlXe99VAjKR464xKECpYsLmj2bYpiK4opURF0pBA==", - "license": "MIT", - "dependencies": { - "@sentry-internal/browser-utils": "9.47.1", - "@sentry/core": "9.47.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry-internal/replay-canvas": { - "version": "9.47.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.47.1.tgz", - "integrity": "sha512-r9nve+l5+elGB9NXSN1+PUgJy790tXN1e8lZNH2ziveoU91jW4yYYt34mHZ30fU9tOz58OpaRMj3H3GJ/jYZVA==", - "license": "MIT", - "dependencies": { - "@sentry-internal/replay": "9.47.1", - "@sentry/core": "9.47.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/browser": { - "version": "9.47.1", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.47.1.tgz", - "integrity": "sha512-at5JOLziw5QpVYytxTDU6xijdV6lDQ/Rxp/qXJaHXud3gIK4suv2cXW+tupJfwoUoHFCnDNfccjCmPmP0yRqiA==", - "license": "MIT", - "dependencies": { - "@sentry-internal/browser-utils": "9.47.1", - "@sentry-internal/feedback": "9.47.1", - "@sentry-internal/replay": "9.47.1", - "@sentry-internal/replay-canvas": "9.47.1", - "@sentry/core": "9.47.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/core": { - "version": "9.47.1", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.47.1.tgz", - "integrity": "sha512-KX62+qIt4xgy8eHKHiikfhz2p5fOciXd0Cl+dNzhgPFq8klq4MGMNaf148GB3M/vBqP4nw/eFvRMAayFCgdRQw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/electron": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@sentry/electron/-/electron-6.11.0.tgz", - "integrity": "sha512-9wFa8Y2baoA70Vwv/CQSlCKYQs53RMr0tN3Bz4Pe2waWMB9oChu8m/+47+p0WaVpFLxWnBqd13S3SCeyABpCAA==", - "license": "MIT", - "dependencies": { - "@sentry/browser": "9.46.0", - "@sentry/core": "9.46.0", - "@sentry/node": "9.46.0" - }, - "peerDependencies": { - "@sentry/node-native": "9.46.0" - }, - "peerDependenciesMeta": { - "@sentry/node-native": { - "optional": true - } - } - }, - "node_modules/@sentry/electron/node_modules/@sentry-internal/browser-utils": { - "version": "9.46.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.46.0.tgz", - "integrity": "sha512-Q0CeHym9wysku8mYkORXmhtlBE0IrafAI+NiPSqxOBKXGOCWKVCvowHuAF56GwPFic2rSrRnub5fWYv7T1jfEQ==", - "license": "MIT", - "dependencies": { - "@sentry/core": "9.46.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/electron/node_modules/@sentry-internal/feedback": { - "version": "9.46.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.46.0.tgz", - "integrity": "sha512-KLRy3OolDkGdPItQ3obtBU2RqDt9+KE8z7r7Gsu7c6A6A89m8ZVlrxee3hPQt6qp0YY0P8WazpedU3DYTtaT8w==", - "license": "MIT", - "dependencies": { - "@sentry/core": "9.46.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/electron/node_modules/@sentry-internal/replay": { - "version": "9.46.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.46.0.tgz", - "integrity": "sha512-+8JUblxSSnN0FXcmOewbN+wIc1dt6/zaSeAvt2xshrfrLooVullcGsuLAiPhY0d/e++Fk06q1SAl9g4V0V13gg==", - "license": "MIT", - "dependencies": { - "@sentry-internal/browser-utils": "9.46.0", - "@sentry/core": "9.46.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/electron/node_modules/@sentry-internal/replay-canvas": { - "version": "9.46.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.46.0.tgz", - "integrity": "sha512-QcBjrdRWFJrrrjbmrr2bbrp2R9RYj1KMEbhHNT2Lm1XplIQw+tULEKOHxNtkUFSLR1RNje7JQbxhzM1j95FxVQ==", - "license": "MIT", - "dependencies": { - "@sentry-internal/replay": "9.46.0", - "@sentry/core": "9.46.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/electron/node_modules/@sentry/browser": { - "version": "9.46.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.46.0.tgz", - "integrity": "sha512-NOnCTQCM0NFuwbyt4DYWDNO2zOTj1mCf43hJqGDFb1XM9F++7zAmSNnCx4UrEoBTiFOy40McJwBBk9D1blSktA==", + "node_modules/@rc-component/upload": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.0.tgz", + "integrity": "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==", "license": "MIT", + "peer": true, "dependencies": { - "@sentry-internal/browser-utils": "9.46.0", - "@sentry-internal/feedback": "9.46.0", - "@sentry-internal/replay": "9.46.0", - "@sentry-internal/replay-canvas": "9.46.0", - "@sentry/core": "9.46.0" + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" }, - "engines": { - "node": ">=18" + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@sentry/electron/node_modules/@sentry/core": { - "version": "9.46.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.46.0.tgz", - "integrity": "sha512-it7JMFqxVproAgEtbLgCVBYtQ9fIb+Bu0JD+cEplTN/Ukpe6GaolyYib5geZqslVxhp2sQgT+58aGvfd/k0N8Q==", + "node_modules/@rc-component/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.9.0.tgz", + "integrity": "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg==", "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/electron/node_modules/@sentry/node": { - "version": "9.46.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-9.46.0.tgz", - "integrity": "sha512-pRLqAcd7GTGvN8gex5FtkQR5Mcol8gOy1WlyZZFq4rBbVtMbqKOQRhohwqnb+YrnmtFpj7IZ7KNDo077MvNeOQ==", - "license": "MIT", - "dependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1", - "@opentelemetry/core": "^1.30.1", - "@opentelemetry/instrumentation": "^0.57.2", - "@opentelemetry/instrumentation-amqplib": "^0.46.1", - "@opentelemetry/instrumentation-connect": "0.43.1", - "@opentelemetry/instrumentation-dataloader": "0.16.1", - "@opentelemetry/instrumentation-express": "0.47.1", - "@opentelemetry/instrumentation-fs": "0.19.1", - "@opentelemetry/instrumentation-generic-pool": "0.43.1", - "@opentelemetry/instrumentation-graphql": "0.47.1", - "@opentelemetry/instrumentation-hapi": "0.45.2", - "@opentelemetry/instrumentation-http": "0.57.2", - "@opentelemetry/instrumentation-ioredis": "0.47.1", - "@opentelemetry/instrumentation-kafkajs": "0.7.1", - "@opentelemetry/instrumentation-knex": "0.44.1", - "@opentelemetry/instrumentation-koa": "0.47.1", - "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", - "@opentelemetry/instrumentation-mongodb": "0.52.0", - "@opentelemetry/instrumentation-mongoose": "0.46.1", - "@opentelemetry/instrumentation-mysql": "0.45.1", - "@opentelemetry/instrumentation-mysql2": "0.45.2", - "@opentelemetry/instrumentation-pg": "0.51.1", - "@opentelemetry/instrumentation-redis-4": "0.46.1", - "@opentelemetry/instrumentation-tedious": "0.18.1", - "@opentelemetry/instrumentation-undici": "0.10.1", - "@opentelemetry/resources": "^1.30.1", - "@opentelemetry/sdk-trace-base": "^1.30.1", - "@opentelemetry/semantic-conventions": "^1.34.0", - "@prisma/instrumentation": "6.11.1", - "@sentry/core": "9.46.0", - "@sentry/node-core": "9.46.0", - "@sentry/opentelemetry": "9.46.0", - "import-in-the-middle": "^1.14.2", - "minimatch": "^9.0.0" + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" }, - "engines": { - "node": ">=18" + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" } }, - "node_modules/@sentry/electron/node_modules/@sentry/node-core": { - "version": "9.46.0", - "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-9.46.0.tgz", - "integrity": "sha512-XRVu5pqoklZeh4wqhxCLZkz/ipoKhitctgEFXX9Yh1e1BoHM2pIxT52wf+W6hHM676TFmFXW3uKBjsmRM3AjgA==", + "node_modules/@rc-component/util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/@rc-component/virtual-list": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.0.2.tgz", + "integrity": "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ==", "license": "MIT", + "peer": true, "dependencies": { - "@sentry/core": "9.46.0", - "@sentry/opentelemetry": "9.46.0", - "import-in-the-middle": "^1.14.2" + "@babel/runtime": "^7.20.0", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" }, "engines": { - "node": ">=18" + "node": ">=8.x" }, "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", - "@opentelemetry/core": "^1.30.1 || ^2.0.0", - "@opentelemetry/instrumentation": ">=0.57.1 <1", - "@opentelemetry/resources": "^1.30.1 || ^2.0.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", - "@opentelemetry/semantic-conventions": "^1.34.0" + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@sentry/electron/node_modules/@sentry/opentelemetry": { - "version": "9.46.0", - "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.46.0.tgz", - "integrity": "sha512-w2zTxqrdmwRok0cXBoh+ksXdGRUHUZhlpfL/H2kfTodOL+Mk8rW72qUmfqQceXoqgbz8UyK8YgJbyt+XS5H4Qg==", + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", "license": "MIT", "dependencies": { - "@sentry/core": "9.46.0" - }, - "engines": { - "node": ">=18" + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", - "@opentelemetry/core": "^1.30.1 || ^2.0.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", - "@opentelemetry/semantic-conventions": "^1.34.0" + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } } }, - "node_modules/@sentry/electron/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@sentry/electron/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/immer" } }, - "node_modules/@sentry/node": { - "version": "9.47.1", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-9.47.1.tgz", - "integrity": "sha512-CDbkasBz3fnWRKSFs6mmaRepM2pa+tbZkrqhPWifFfIkJDidtVW40p6OnquTvPXyPAszCnDZRnZT14xyvNmKPQ==", - "license": "MIT", - "dependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1", - "@opentelemetry/core": "^1.30.1", - "@opentelemetry/instrumentation": "^0.57.2", - "@opentelemetry/instrumentation-amqplib": "^0.46.1", - "@opentelemetry/instrumentation-connect": "0.43.1", - "@opentelemetry/instrumentation-dataloader": "0.16.1", - "@opentelemetry/instrumentation-express": "0.47.1", - "@opentelemetry/instrumentation-fs": "0.19.1", - "@opentelemetry/instrumentation-generic-pool": "0.43.1", - "@opentelemetry/instrumentation-graphql": "0.47.1", - "@opentelemetry/instrumentation-hapi": "0.45.2", - "@opentelemetry/instrumentation-http": "0.57.2", - "@opentelemetry/instrumentation-ioredis": "0.47.1", - "@opentelemetry/instrumentation-kafkajs": "0.7.1", - "@opentelemetry/instrumentation-knex": "0.44.1", - "@opentelemetry/instrumentation-koa": "0.47.1", - "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", - "@opentelemetry/instrumentation-mongodb": "0.52.0", - "@opentelemetry/instrumentation-mongoose": "0.46.1", - "@opentelemetry/instrumentation-mysql": "0.45.1", - "@opentelemetry/instrumentation-mysql2": "0.45.2", - "@opentelemetry/instrumentation-pg": "0.51.1", - "@opentelemetry/instrumentation-redis-4": "0.46.1", - "@opentelemetry/instrumentation-tedious": "0.18.1", - "@opentelemetry/instrumentation-undici": "0.10.1", - "@opentelemetry/resources": "^1.30.1", - "@opentelemetry/sdk-trace-base": "^1.30.1", - "@opentelemetry/semantic-conventions": "^1.34.0", - "@prisma/instrumentation": "6.11.1", - "@sentry/core": "9.47.1", - "@sentry/node-core": "9.47.1", - "@sentry/opentelemetry": "9.47.1", - "import-in-the-middle": "^1.14.2", - "minimatch": "^9.0.0" - }, - "engines": { - "node": ">=18" - } + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" }, - "node_modules/@sentry/node-core": { - "version": "9.47.1", - "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-9.47.1.tgz", - "integrity": "sha512-7TEOiCGkyShJ8CKtsri9lbgMCbB+qNts2Xq37itiMPN2m+lIukK3OX//L8DC5nfKYZlgikrefS63/vJtm669hQ==", + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", "license": "MIT", - "dependencies": { - "@sentry/core": "9.47.1", - "@sentry/opentelemetry": "9.47.1", - "import-in-the-middle": "^1.14.2" - }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", - "@opentelemetry/core": "^1.30.1 || ^2.0.0", - "@opentelemetry/instrumentation": ">=0.57.1 <1", - "@opentelemetry/resources": "^1.30.1 || ^2.0.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", - "@opentelemetry/semantic-conventions": "^1.34.0" + "node": ">=v14.0.0", + "npm": ">=7.0.0" } }, - "node_modules/@sentry/node/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@sentry/node/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=v16" } }, - "node_modules/@sentry/opentelemetry": { - "version": "9.47.1", - "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.47.1.tgz", - "integrity": "sha512-STtFpjF7lwzeoedDJV+5XA6P89BfmFwFftmHSGSe3UTI8z8IoiR5yB6X2vCjSPvXlfeOs13qCNNCEZyznxM8Xw==", + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", "license": "MIT", - "dependencies": { - "@sentry/core": "9.47.1" - }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", - "@opentelemetry/core": "^1.30.1 || ^2.0.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", - "@opentelemetry/semantic-conventions": "^1.34.0" + "node": ">=v14.0.0", + "npm": ">=7.0.0" } }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, "node_modules/@shikijs/core": { "version": "3.23.0", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", @@ -9606,15 +8646,6 @@ "@types/responselike": "^1.0.0" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -10024,15 +9055,6 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, - "node_modules/@types/mysql": { - "version": "2.15.26", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", - "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node": { "version": "20.19.32", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz", @@ -10048,26 +9070,6 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, - "node_modules/@types/pg": { - "version": "8.6.1", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", - "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@types/pg-pool": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", - "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", - "license": "MIT", - "dependencies": { - "@types/pg": "*" - } - }, "node_modules/@types/plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", @@ -10140,27 +9142,12 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, - "node_modules/@types/shimmer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", - "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", - "license": "MIT" - }, "node_modules/@types/statuses": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", "license": "MIT" }, - "node_modules/@types/tedious": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", - "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -10890,15 +9877,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -11800,6 +10778,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -12483,12 +11462,6 @@ "node": ">=8" } }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "license": "MIT" - }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -15956,12 +14929,6 @@ "node": ">= 0.6" } }, - "node_modules/forwarded-parse": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", - "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", - "license": "MIT" - }, "node_modules/framer-motion": { "version": "12.35.2", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.35.2.tgz", @@ -17254,18 +16221,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-in-the-middle": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz", - "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==", - "license": "Apache-2.0", - "dependencies": { - "acorn": "^8.14.0", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -21002,12 +19957,6 @@ "ufo": "^1.6.3" } }, - "node_modules/module-details-from-path": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", - "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", - "license": "MIT" - }, "node_modules/morphdom": { "version": "2.7.8", "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.8.tgz", @@ -22144,37 +21093,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", - "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -22366,45 +21284,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/powershell-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", @@ -24149,20 +23028,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-in-the-middle": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", - "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -25241,12 +24106,6 @@ } } }, - "node_modules/shimmer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", - "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", - "license": "BSD-2-Clause" - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -27603,15 +26462,6 @@ "node": ">=8.0" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 023f14c5..2211eb55 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ ], "author": { "name": "op7418", - "email": "guohao631@gmail.com", "url": "https://x.com/op7418" }, "description": "A multi-model AI agent desktop client", @@ -49,9 +48,6 @@ "@google/genai": "^1.43.0", "@larksuiteoapi/node-sdk": "^1.59.0", "@lobehub/icons": "^4.6.0", - "@sentry/browser": "^9.27.0", - "@sentry/electron": "^6.3.0", - "@sentry/node": "^9.27.0", "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-use-controllable-state": "^1.2.2", "@streamdown/cjk": "^1.0.1", @@ -69,8 +65,6 @@ "discord.js": "^14.25.1", "dompurify": "^3.3.3", "electron-updater": "^6.8.3", - "html-to-image": "^1.11.13", - "jdenticon": "^3.3.0", "markdown-it": "^14.1.1", "morphdom": "^2.7.8", "motion": "^12.33.0", diff --git a/src/__tests__/unit/claude-permissions.test.ts b/src/__tests__/unit/claude-permissions.test.ts new file mode 100644 index 00000000..40cdc325 --- /dev/null +++ b/src/__tests__/unit/claude-permissions.test.ts @@ -0,0 +1,60 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +import { + DANGEROUSLY_SKIP_PERMISSIONS_UNSUPPORTED_CODE, + getDangerouslySkipPermissionsSupport, + isDangerouslySkipPermissionsSupported, +} from '../../lib/claude-permissions'; + +describe('claude-permissions', () => { + it('allows auto-approve for non-root users on linux', () => { + const result = getDangerouslySkipPermissionsSupport({ + platform: 'linux', + uid: 1000, + env: {} as NodeJS.ProcessEnv, + }); + + assert.equal(result.supported, true); + assert.equal( + isDangerouslySkipPermissionsSupported({ + platform: 'linux', + uid: 1000, + env: {} as NodeJS.ProcessEnv, + }), + true, + ); + }); + + it('blocks auto-approve for unsandboxed root on linux', () => { + const result = getDangerouslySkipPermissionsSupport({ + platform: 'linux', + uid: 0, + env: {} as NodeJS.ProcessEnv, + }); + + assert.equal(result.supported, false); + assert.equal(result.reasonCode, DANGEROUSLY_SKIP_PERMISSIONS_UNSUPPORTED_CODE); + assert.match(result.reason || '', /root\/sudo/i); + }); + + it('allows auto-approve for sandboxed root on linux', () => { + const result = getDangerouslySkipPermissionsSupport({ + platform: 'linux', + uid: 0, + env: { IS_SANDBOX: '1' } as unknown as NodeJS.ProcessEnv, + }); + + assert.equal(result.supported, true); + }); + + it('allows auto-approve on windows', () => { + const result = getDangerouslySkipPermissionsSupport({ + platform: 'win32', + uid: 0, + env: {} as NodeJS.ProcessEnv, + }); + + assert.equal(result.supported, true); + }); +}); diff --git a/src/__tests__/unit/provider-preset.test.ts b/src/__tests__/unit/provider-preset.test.ts deleted file mode 100644 index 43c608bf..00000000 --- a/src/__tests__/unit/provider-preset.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; -import { VENDOR_PRESETS, PresetSchema } from '../../lib/provider-catalog'; - -describe('Preset Schema Validation', () => { - for (const preset of VENDOR_PRESETS) { - describe(`preset: ${preset.key}`, () => { - it('passes Zod schema validation', () => { - const result = PresetSchema.safeParse(preset); - if (!result.success) { - assert.fail(`Schema validation failed for ${preset.key}: ${result.error.message}`); - } - }); - - it('has at least one default model (or is volcengine/ollama)', () => { - if (preset.key === 'volcengine' || preset.key === 'ollama') return; - assert.ok(preset.defaultModels.length > 0, `Preset ${preset.key} expected at least one default model`); - }); - - it('authStyle and defaultEnvOverrides do not conflict', () => { - if (preset.authStyle === 'auth_token') { - assert.equal( - preset.defaultEnvOverrides.ANTHROPIC_API_KEY, - undefined, - `auth_token preset ${preset.key} should not have ANTHROPIC_API_KEY in envOverrides`, - ); - } - if (preset.authStyle === 'api_key') { - assert.equal( - preset.defaultEnvOverrides.ANTHROPIC_AUTH_TOKEN, - undefined, - `api_key preset ${preset.key} should not have ANTHROPIC_AUTH_TOKEN in envOverrides`, - ); - } - }); - }); - } - - // ── Regression tests for the authStyle fixes ── - - it('OpenRouter uses auth_token', () => { - const p = VENDOR_PRESETS.find(v => v.key === 'openrouter')!; - assert.equal(p.authStyle, 'auth_token'); - }); - - it('GLM CN uses auth_token', () => { - const p = VENDOR_PRESETS.find(v => v.key === 'glm-cn')!; - assert.equal(p.authStyle, 'auth_token'); - }); - - it('GLM Global uses auth_token', () => { - const p = VENDOR_PRESETS.find(v => v.key === 'glm-global')!; - assert.equal(p.authStyle, 'auth_token'); - }); - - it('Moonshot uses auth_token with ENABLE_TOOL_SEARCH disabled', () => { - const p = VENDOR_PRESETS.find(v => v.key === 'moonshot')!; - assert.equal(p.authStyle, 'auth_token'); - assert.equal(p.defaultEnvOverrides.ENABLE_TOOL_SEARCH, 'false'); - }); - - it('Kimi uses api_key with ENABLE_TOOL_SEARCH disabled', () => { - const p = VENDOR_PRESETS.find(v => v.key === 'kimi')!; - assert.equal(p.authStyle, 'api_key'); - assert.equal(p.defaultEnvOverrides.ENABLE_TOOL_SEARCH, 'false'); - }); - - it('Bailian uses auth_token', () => { - const p = VENDOR_PRESETS.find(v => v.key === 'bailian')!; - assert.equal(p.authStyle, 'auth_token'); - }); -}); - -describe('PROVIDER_MANAGED_BY_HOST', () => { - it('toClaudeCodeEnv always sets CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST', async () => { - const { toClaudeCodeEnv } = await import('../../lib/provider-resolver'); - const { VENDOR_PRESETS: presets } = await import('../../lib/provider-catalog'); - const anthropic = presets.find(p => p.key === 'anthropic-official')!; - - // With a provider - const resolvedWithProvider = { - provider: { - id: 'test', name: 'Test', provider_type: 'anthropic', protocol: 'anthropic', - base_url: 'https://api.anthropic.com', api_key: 'sk-test', - is_active: 1, sort_order: 0, extra_env: '{}', headers_json: '{}', - env_overrides_json: '', role_models_json: '{}', notes: '', options_json: '{}', - created_at: '', updated_at: '', - }, - protocol: 'anthropic' as const, - authStyle: 'api_key' as const, - model: 'sonnet', - modelDisplayName: 'Sonnet 4.6', - upstreamModel: 'sonnet', - headers: {}, - envOverrides: {}, - roleModels: {}, - hasCredentials: true, - availableModels: [], - settingSources: ['project', 'local'], - }; - const env = toClaudeCodeEnv({ PATH: '/usr/bin' }, resolvedWithProvider); - assert.equal(env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST, '1'); - - // Without a provider (env mode) - const resolvedWithoutProvider = { - provider: undefined, - protocol: 'anthropic' as const, - authStyle: 'api_key' as const, - model: undefined, - modelDisplayName: undefined, - upstreamModel: undefined, - headers: {}, - envOverrides: {}, - roleModels: {}, - hasCredentials: false, - availableModels: [], - settingSources: ['user', 'project', 'local'], - }; - const env2 = toClaudeCodeEnv({ PATH: '/usr/bin' }, resolvedWithoutProvider); - assert.equal(env2.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST, '1'); - }); -}); diff --git a/src/__tests__/unit/provider-resolver.test.ts b/src/__tests__/unit/provider-resolver.test.ts index 467a7494..43f7880a 100644 --- a/src/__tests__/unit/provider-resolver.test.ts +++ b/src/__tests__/unit/provider-resolver.test.ts @@ -39,11 +39,11 @@ describe('Provider Catalog', () => { } }); - it('Kimi preset uses anthropic protocol with api_key auth', () => { + it('Kimi preset uses anthropic protocol', () => { const kimi = VENDOR_PRESETS.find(p => p.key === 'kimi'); assert.ok(kimi, 'Kimi preset not found'); assert.equal(kimi.protocol, 'anthropic'); - assert.equal(kimi.authStyle, 'api_key'); + assert.equal(kimi.authStyle, 'auth_token'); }); it('MiniMax presets use anthropic protocol', () => { @@ -300,9 +300,7 @@ describe('Provider Resolver', () => { const env = toClaudeCodeEnv({ PATH: '/usr/bin' }, resolved); assert.equal(env.ANTHROPIC_API_KEY, 'sk-test-key'); - // api_key mode must NOT set ANTHROPIC_AUTH_TOKEN — upstream adds Bearer header - // when AUTH_TOKEN is present, which conflicts with API-key-only providers (Kimi) - assert.equal(env.ANTHROPIC_AUTH_TOKEN, undefined); + assert.equal(env.ANTHROPIC_AUTH_TOKEN, 'sk-test-key'); assert.equal(env.ANTHROPIC_BASE_URL, 'https://api.anthropic.com'); }); diff --git a/src/__tests__/unit/widget-system.test.ts b/src/__tests__/unit/widget-system.test.ts index b6ae3494..d74c1f97 100644 --- a/src/__tests__/unit/widget-system.test.ts +++ b/src/__tests__/unit/widget-system.test.ts @@ -23,6 +23,7 @@ import { parseAllShowWidgets, parseShowWidget, computePartialWidgetKey, + type WidgetSegment, } from '../../components/chat/MessageItem'; import { WIDGET_CSS_BRIDGE } from '../../lib/widget-css-bridge'; @@ -253,69 +254,6 @@ describe('buildReceiverSrcdoc', () => { }); }); -// ── CDN finalize script execution ──────────────────────────────────────── - -describe('finalizeHtml CDN script handling', () => { - const srcdoc = buildReceiverSrcdoc(':root{}', false); - - it('separates CDN and inline scripts in finalize', () => { - // The receiver script must filter scripts into cdn (has src) vs inline (has text) - assert.ok(srcdoc.includes('cdnScripts=scripts.filter'), 'should separate CDN scripts'); - assert.ok(srcdoc.includes('inlineScripts=scripts.filter'), 'should separate inline scripts'); - }); - - it('waits for all CDN scripts before executing inline', () => { - // When CDN scripts exist, inline must only run after all CDN onload/onerror fire - assert.ok(srcdoc.includes('_pending=cdnScripts.length'), 'should track pending CDN count'); - assert.ok(srcdoc.includes('_pending--'), 'should decrement on each CDN completion'); - assert.ok(srcdoc.includes('_pending<=0'), 'should run inline only when all CDN done'); - }); - - it('runs inline immediately when no CDN scripts', () => { - assert.ok(srcdoc.includes('cdnScripts.length===0'), 'should check for zero CDN scripts'); - // _appendInline is called directly in the no-CDN branch - assert.ok(srcdoc.includes('_appendInline()'), 'should call _appendInline'); - }); - - it('does NOT re-inject inline scripts on CDN load (no duplicate execution)', () => { - // _appendInline should only be called once — no _runInline on every onload - // The function is named _appendInline (not _runInline) and called via _onCdnDone counter - assert.ok(srcdoc.includes('function _onCdnDone'), 'should use counter-based callback'); - assert.ok(srcdoc.includes('n.onload=_onCdnDone'), 'onload should use counter, not direct _appendInline'); - assert.ok(srcdoc.includes('n.onerror=_onCdnDone'), 'onerror should use counter, not direct _appendInline'); - }); - - it('does NOT have a timeout fallback that could race with CDN load', () => { - // Previous bugs: setTimeout(3000) set _inlineRan=true before CDN arrived - // The finalizeHtml script section should not use setTimeout for inline execution - assert.ok(!srcdoc.includes('setTimeout(function(){_appendInline'), 'should not have timeout calling _appendInline'); - assert.ok(!srcdoc.includes('setTimeout(function(){_runInline'), 'should not have timeout calling _runInline'); - }); - - it('does NOT have a once-flag that could lock out late CDN arrivals', () => { - // Previous bug: _inlineRan flag prevented init after slow CDN load - assert.ok(!srcdoc.includes('_inlineRan'), 'should not have _inlineRan flag'); - }); - - it('strips model-provided onload to avoid double init', () => { - // CDN scripts with model-provided onload="init()" should have it stripped - // since our _onCdnDone callback handles execution timing - assert.ok(srcdoc.includes("!=='onload'"), 'should skip onload attribute when setting attrs'); - }); - - it('emits widget:scriptsReady after inline scripts execute', () => { - // Export relies on this signal to know when Chart.js etc. have finished drawing - assert.ok(srcdoc.includes("widget:scriptsReady"), 'should emit scriptsReady after _appendInline'); - }); - - it('handles widget:capture message for PNG export', () => { - assert.ok(srcdoc.includes("widget:capture"), 'should handle capture message'); - assert.ok(srcdoc.includes("widget:captured"), 'should respond with captured dataUrl'); - // Must convert live canvas to img before serialization - assert.ok(srcdoc.includes("toDataURL"), 'should convert canvas elements to images'); - }); -}); - // ── CDN whitelist ─────────────────────────────────────────────────────── describe('CDN_WHITELIST', () => { @@ -376,7 +314,7 @@ describe('WIDGET_SYSTEM_PROMPT', () => { it('is smaller than the original full prompt but includes core rules', () => { assert.ok(WIDGET_SYSTEM_PROMPT.length > 500, 'should include core hard constraints'); - assert.ok(WIDGET_SYSTEM_PROMPT.length < 2000, 'should be smaller than original ~2500 char full prompt'); + assert.ok(WIDGET_SYSTEM_PROMPT.length < 1500, 'should be smaller than original ~2500 char full prompt'); }); it('includes critical hard constraints for valid widget output', () => { diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index fb24bed5..96c07620 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -185,11 +185,6 @@ export async function POST(request: NextRequest) { const abortController = new AbortController(); - // Handle client disconnect - request.signal.addEventListener('abort', () => { - abortController.abort(); - }); - // Convert file attachments to the format expected by streamClaude. // Include filePath from the already-saved files so claude-client can // reference the on-disk copies instead of writing them again. diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts deleted file mode 100644 index 0b5781c4..00000000 --- a/src/app/api/providers/[id]/models/route.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getProvider, getModelsForProvider, upsertProviderModel, deleteProviderModel } from '@/lib/db'; -import type { ErrorResponse } from '@/types'; - -/** - * GET /api/providers/[id]/models - * List all custom models for a provider (from provider_models table). - */ -export async function GET( - _request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const { id } = await params; - const provider = getProvider(id); - if (!provider) { - return NextResponse.json({ error: 'Provider not found' }, { status: 404 }); - } - const models = getModelsForProvider(id); - return NextResponse.json({ models }); -} - -/** - * POST /api/providers/[id]/models - * Add or update a custom model for a provider. - */ -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const { id } = await params; - const provider = getProvider(id); - if (!provider) { - return NextResponse.json({ error: 'Provider not found' }, { status: 404 }); - } - - const body = await request.json(); - const { model_id, upstream_model_id, display_name, capabilities_json, sort_order } = body; - - if (!model_id) { - return NextResponse.json({ error: 'model_id is required' }, { status: 400 }); - } - - upsertProviderModel({ - provider_id: id, - model_id, - upstream_model_id: upstream_model_id || '', - display_name: display_name || model_id, - capabilities_json: capabilities_json || '{}', - sort_order: sort_order ?? 0, - }); - - const models = getModelsForProvider(id); - return NextResponse.json({ models }); -} - -/** - * DELETE /api/providers/[id]/models - * Remove a custom model from a provider. - * Body: { model_id: string } - */ -export async function DELETE( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const { id } = await params; - const provider = getProvider(id); - if (!provider) { - return NextResponse.json({ error: 'Provider not found' }, { status: 404 }); - } - - const body = await request.json(); - const { model_id } = body; - - if (!model_id) { - return NextResponse.json({ error: 'model_id is required' }, { status: 400 }); - } - - const deleted = deleteProviderModel(id, model_id); - if (!deleted) { - return NextResponse.json({ error: 'Model not found' }, { status: 404 }); - } - - const models = getModelsForProvider(id); - return NextResponse.json({ models }); -} diff --git a/src/app/api/providers/test/route.ts b/src/app/api/providers/test/route.ts deleted file mode 100644 index 60dcefae..00000000 --- a/src/app/api/providers/test/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { testProviderConnection } from '@/lib/claude-client'; -import { getPreset } from '@/lib/provider-catalog'; -import type { ErrorResponse } from '@/types'; - -/** - * POST /api/providers/test - * - * Test a provider connection without saving to DB. - * Sends a minimal SDK query and returns structured success/error. - */ -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { presetKey, apiKey, baseUrl, protocol, authStyle, envOverrides, providerName, modelName } = body; - - if (!apiKey && authStyle !== 'env_only') { - return NextResponse.json({ success: false, error: { code: 'NO_CREDENTIALS', message: 'API Key is required', suggestion: 'Please enter your API key' } }); - } - - // Look up preset meta for recovery action URLs - const preset = presetKey ? getPreset(presetKey) : undefined; - const meta = preset?.meta; - - const result = await testProviderConnection({ - apiKey: apiKey || '', - baseUrl: baseUrl || '', - protocol: protocol || 'anthropic', - authStyle: authStyle || 'api_key', - envOverrides: envOverrides || {}, - modelName: modelName || undefined, - presetKey: presetKey || undefined, - providerName: providerName || preset?.name || 'Unknown', - providerMeta: meta ? { apiKeyUrl: meta.apiKeyUrl, docsUrl: meta.docsUrl, pricingUrl: meta.pricingUrl } : undefined, - }); - - return NextResponse.json(result); - } catch (err) { - return NextResponse.json( - { error: 'Failed to test connection', details: String(err) } as ErrorResponse, - { status: 500 }, - ); - } -} diff --git a/src/app/api/settings/sentry/route.ts b/src/app/api/settings/sentry/route.ts deleted file mode 100644 index b913af73..00000000 --- a/src/app/api/settings/sentry/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; - -/** Path to the Sentry opt-out marker file, read by Electron main process at startup */ -function getSentryMarkerPath() { - return path.join(os.homedir(), '.codepilot', 'sentry-disabled'); -} - -/** GET /api/settings/sentry — read opt-out state */ -export async function GET() { - try { - const markerPath = getSentryMarkerPath(); - const disabled = fs.existsSync(markerPath) && fs.readFileSync(markerPath, 'utf-8').trim() === 'true'; - return NextResponse.json({ disabled }); - } catch { - return NextResponse.json({ disabled: false }); - } -} - -/** POST /api/settings/sentry — write opt-out state */ -export async function POST(request: NextRequest) { - try { - const { disabled } = await request.json(); - const markerPath = getSentryMarkerPath(); - const dir = path.dirname(markerPath); - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(markerPath, disabled ? 'true' : 'false', 'utf-8'); - return NextResponse.json({ ok: true }); - } catch (err) { - return NextResponse.json({ error: String(err) }, { status: 500 }); - } -} diff --git a/src/components/chat/ModelSelectorDropdown.tsx b/src/components/chat/ModelSelectorDropdown.tsx index 391f5074..127661aa 100644 --- a/src/components/chat/ModelSelectorDropdown.tsx +++ b/src/components/chat/ModelSelectorDropdown.tsx @@ -166,7 +166,7 @@ export function ModelSelectorDropdown({ )} - { setModelMenuOpen(false); setModelSearch(''); window.location.href = '/settings#providers'; }}> + { setModelMenuOpen(false); setModelSearch(''); window.location.href = '/settings'; }}> {t('composer.manageProviders' as TranslationKey)} diff --git a/src/components/chat/StreamingMessage.tsx b/src/components/chat/StreamingMessage.tsx index 849d5317..9abb2abf 100644 --- a/src/components/chat/StreamingMessage.tsx +++ b/src/components/chat/StreamingMessage.tsx @@ -325,30 +325,15 @@ export function StreamingMessage({ // ── Show-widget handling ── // During streaming: detect partial fences FIRST to avoid premature script execution. // After streaming: use parseAllShowWidgets for completed fences only. - const hasWidgetFence = /`{1,3}show-widget/.test(content); + const hasWidgetFence = /```show-widget/.test(content); if (hasWidgetFence && isStreaming) { - // Fence-agnostic: find the last show-widget marker - const lastMarkerMatch = [...content.matchAll(/`{1,3}show-widget/g)].pop(); - if (!lastMarkerMatch) return {content}; - - const lastFenceStart = lastMarkerMatch.index!; + // Streaming mode: find the last ```show-widget fence. + // If it's closed, all fences are complete → render them all. + // If it's open, render completed fences before it + partial preview for the open one. + const lastFenceStart = content.lastIndexOf('```show-widget'); const afterLastFence = content.slice(lastFenceStart); - // Check if JSON is complete (has matching closing brace) - const jsonStart = afterLastFence.indexOf('{'); - let lastFenceClosed = false; - if (jsonStart !== -1) { - let depth = 0, inStr = false, esc = false; - for (let i = jsonStart; i < afterLastFence.length; i++) { - const ch = afterLastFence[i]; - if (esc) { esc = false; continue; } - if (ch === '\\' && inStr) { esc = true; continue; } - if (ch === '"') { inStr = !inStr; continue; } - if (inStr) continue; - if (ch === '{') depth++; - else if (ch === '}') { depth--; if (depth === 0) { lastFenceClosed = true; break; } } - } - } + const lastFenceClosed = /```show-widget\s*\n?[\s\S]*?\n?\s*```/.test(afterLastFence); if (lastFenceClosed) { // All fences complete — parse and render the full content @@ -367,12 +352,11 @@ export function StreamingMessage({ // Last fence is still being streamed. // Parse everything BEFORE it (completed fences + interleaved text). const beforePart = content.slice(0, lastFenceStart).trim(); - const hasCompletedFences = beforePart && /`{1,3}show-widget/.test(beforePart); + const hasCompletedFences = beforePart && /```show-widget/.test(beforePart); const completedSegments = hasCompletedFences ? parseAllShowWidgets(beforePart) : []; - // Extract partial widget_code from the open fence (skip marker) - const markerEnd = afterLastFence.match(/^`{1,3}show-widget`{0,3}\s*(?:\n\s*`{3}(?:json)?\s*)?\n?/); - const fenceBody = markerEnd ? afterLastFence.slice(markerEnd[0].length).trim() : afterLastFence.trim(); + // Extract partial widget_code from the open fence + const fenceBody = content.slice(lastFenceStart + '```show-widget'.length).trim(); let partialCode: string | null = null; const keyIdx = fenceBody.indexOf('"widget_code"'); if (keyIdx !== -1) { @@ -390,7 +374,6 @@ export function StreamingMessage({ .replace(/\\t/g, '\t') .replace(/\\r/g, '\r') .replace(/\\"/g, '"') - .replace(/\\u([0-9a-fA-F]{4})/g, (_: string, hex: string) => String.fromCharCode(parseInt(hex, 16))) .replace(/\x00BACKSLASH\x00/g, '\\'); } catch { partialCode = null; } } diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index e22ae2df..86819f26 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -18,7 +18,6 @@ import { BatchImageGenContext, useBatchImageGenState } from "@/hooks/useBatchIma import { SplitContext, type SplitSession } from "@/hooks/useSplit"; import { SplitChatContainer } from "./SplitChatContainer"; import { ErrorBoundary } from "./ErrorBoundary"; -import { SentryInit } from "./SentryInit"; import { getActiveSessionIds, getSnapshot } from "@/lib/stream-session-manager"; import { useGitStatus } from "@/hooks/useGitStatus"; import { SetupCenter } from '@/components/setup/SetupCenter'; @@ -109,6 +108,13 @@ export function AppShell({ children }: { children: React.ReactNode }) { }, []); /* eslint-enable react-hooks/set-state-in-effect */ + // Listen for mobile close events + useEffect(() => { + const handler = () => setChatListOpenRaw(false); + window.addEventListener("chatlist-close", handler); + return () => window.removeEventListener("chatlist-close", handler); + }, []); + // Panel width state with localStorage persistence const [chatListWidth, setChatListWidth] = useState(240); @@ -415,8 +421,10 @@ export function AppShell({ children }: { children: React.ReactNode }) { setPreviewFile, previewViewMode, setPreviewViewMode, + chatListOpen, + setChatListOpen, }), - [fileTreeOpen, gitPanelOpen, previewOpen, terminalOpen, dashboardPanelOpen, assistantPanelOpen, isAssistantWorkspace, currentBranch, gitDirtyCount, currentWorktreeLabel, workingDirectory, sessionId, sessionTitle, streamingSessionId, pendingApprovalSessionId, activeStreamingSessions, pendingApprovalSessionIds, previewFile, setPreviewFile, previewViewMode] + [fileTreeOpen, gitPanelOpen, previewOpen, terminalOpen, dashboardPanelOpen, assistantPanelOpen, isAssistantWorkspace, currentBranch, gitDirtyCount, currentWorktreeLabel, workingDirectory, sessionId, sessionTitle, streamingSessionId, pendingApprovalSessionId, activeStreamingSessions, pendingApprovalSessionIds, previewFile, setPreviewFile, previewViewMode, chatListOpen, setChatListOpen] ); const imageGenValue = useImageGenState(); @@ -424,7 +432,6 @@ export function AppShell({ children }: { children: React.ReactNode }) { return ( - diff --git a/src/components/layout/ChatListPanel.tsx b/src/components/layout/ChatListPanel.tsx index 7ee88301..523b8e65 100644 --- a/src/components/layout/ChatListPanel.tsx +++ b/src/components/layout/ChatListPanel.tsx @@ -8,6 +8,7 @@ import { MagnifyingGlass, FileArrowDown, Plus, + FolderOpen, FolderPlus, Lightning, Plug, @@ -15,6 +16,7 @@ import { Image, WifiHigh, Gear, + X, } from "@/components/ui/icon"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -448,14 +450,37 @@ export function ChatListPanel({ open, width, hasUpdate, readyToInstall }: ChatLi ]; return ( - + ); } diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx index 97bc4e10..d11582cf 100644 --- a/src/components/layout/ErrorBoundary.tsx +++ b/src/components/layout/ErrorBoundary.tsx @@ -13,19 +13,8 @@ interface ErrorBoundaryProps { interface ErrorBoundaryState { hasError: boolean; error: Error | null; - /** Number of auto-recovery attempts for DOM errors */ - domRetries: number; } -/** DOM operation errors that can often be recovered by re-rendering */ -const DOM_ERROR_PATTERNS = [ - 'insertBefore', - 'removeChild', - 'appendChild', - 'replaceChild', - 'is not a child of this node', -]; - /* ── Fallback UI (functional, so it can use hooks) ──────────── */ function ErrorFallback({ @@ -93,7 +82,7 @@ export class ErrorBoundary extends React.Component< > { constructor(props: ErrorBoundaryProps) { super(props); - this.state = { hasError: false, error: null, domRetries: 0 }; + this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error: Error): Partial { @@ -101,24 +90,8 @@ export class ErrorBoundary extends React.Component< } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - const msg = error.message || ''; - const isDomError = DOM_ERROR_PATTERNS.some(p => msg.includes(p)); - - // Auto-recover from DOM errors (hydration mismatch, stale node refs) - // by clearing the error state and letting React re-render cleanly. - // Max 2 retries to prevent infinite loops. - if (isDomError && this.state.domRetries < 2) { - console.warn("[ErrorBoundary] DOM operation error, auto-recovering:", msg); - this.setState(prev => ({ hasError: false, error: null, domRetries: prev.domRetries + 1 })); - return; - } - console.error("[ErrorBoundary] Uncaught error:", error); console.error("[ErrorBoundary] Component stack:", errorInfo.componentStack); - // Report to Sentry if available - import('@sentry/browser').then((Sentry) => { - Sentry.captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack } } }); - }).catch(() => { /* Sentry not available */ }); } handleReset = () => { diff --git a/src/components/layout/PanelZone.tsx b/src/components/layout/PanelZone.tsx index 9f83e755..5a465e05 100644 --- a/src/components/layout/PanelZone.tsx +++ b/src/components/layout/PanelZone.tsx @@ -22,7 +22,6 @@ export function PanelZone() { {previewOpen && previewFile && } {gitPanelOpen && } {fileTreeOpen && } - {dashboardPanelOpen && } ); } diff --git a/src/components/layout/SentryInit.tsx b/src/components/layout/SentryInit.tsx deleted file mode 100644 index d3b80b8e..00000000 --- a/src/components/layout/SentryInit.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; - -import { useEffect } from "react"; - -/** - * Client-side Sentry initialization component. - * Must be rendered in the client tree (inside a "use client" boundary). - * No-ops gracefully when NEXT_PUBLIC_SENTRY_DSN is not set. - */ -export function SentryInit() { - useEffect(() => { - const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN; - if (!dsn) return; - - // Check user opt-out - try { - if (localStorage.getItem("codepilot:sentry-disabled") === "true") return; - } catch { - /* ignore */ - } - - // Dynamic import to avoid bundling Sentry when DSN is absent - import("@sentry/browser").then((Sentry) => { - if (Sentry.isInitialized()) return; - Sentry.init({ - dsn, - environment: process.env.NODE_ENV, - release: `codepilot@${process.env.NEXT_PUBLIC_APP_VERSION}`, - tracesSampleRate: 0, - beforeBreadcrumb(breadcrumb) { - if (breadcrumb.category === "ui.input") return null; - return breadcrumb; - }, - ignoreErrors: [ - // Expected/non-actionable errors — don't waste Sentry quota - 'AbortError', - 'Operation aborted', - 'The operation was aborted', - 'signal is aborted', - 'prompt() is not supported', // Electron doesn't support window.prompt - 'ResizeObserver loop', // Browser quirk, not a real error - /^Object \[object Object\] has no method/, // Sentry's own frontend bug - ], - beforeSend(event) { - // Respect opt-out - try { - if (localStorage.getItem("codepilot:sentry-disabled") === "true") return null; - } catch { - /* ignore */ - } - // Strip auth headers - if (event.request?.headers) { - delete event.request.headers["x-api-key"]; - delete event.request.headers["authorization"]; - delete event.request.headers["anthropic-api-key"]; - } - // Add useful context tags - event.tags = { - ...event.tags, - platform: navigator.platform, - electron: typeof window !== 'undefined' && 'electronAPI' in window ? 'yes' : 'no', - }; - return event; - }, - }); - }).catch(() => { - /* Sentry not available */ - }); - }, []); - - return null; -} diff --git a/src/components/layout/UnifiedTopBar.tsx b/src/components/layout/UnifiedTopBar.tsx index 2dc155ae..b1960eac 100644 --- a/src/components/layout/UnifiedTopBar.tsx +++ b/src/components/layout/UnifiedTopBar.tsx @@ -9,6 +9,7 @@ import { DotOutline, ChartBar, Brain, + SidebarSimple, } from "@/components/ui/icon"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -40,6 +41,8 @@ export function UnifiedTopBar() { isAssistantWorkspace, currentBranch, gitDirtyCount, + chatListOpen, + setChatListOpen, } = usePanel(); const { t } = useTranslation(); const { isWindows } = useClientPlatform(); @@ -114,12 +117,22 @@ export function UnifiedTopBar() { // On non-chat routes, render only a thin drag region (no visible bar) if (!isChatRoute) { - // Thin drag region for macOS window dragging — just enough for traffic light area return (
+ > + +
); } @@ -129,11 +142,20 @@ export function UnifiedTopBar() { className="flex h-12 shrink-0 items-center gap-2 bg-background px-3" style={{ WebkitAppRegion: 'drag' } as React.CSSProperties} > - {/* Left: chat title + project folder */} + {/* Left: sidebar toggle (mobile) + chat title + project folder */}
+ {isChatRoute && sessionTitle && ( isEditingTitle ? (
diff --git a/src/components/patterns/CommandList.tsx b/src/components/patterns/CommandList.tsx index 57b9f34a..68105a8a 100644 --- a/src/components/patterns/CommandList.tsx +++ b/src/components/patterns/CommandList.tsx @@ -69,7 +69,7 @@ interface CommandListItemsProps { export function CommandListItems({ children, className }: CommandListItemsProps) { return ( -
+
{children}
); diff --git a/src/components/settings/GeneralSection.tsx b/src/components/settings/GeneralSection.tsx index b0fe49bc..8c3a7c56 100644 --- a/src/components/settings/GeneralSection.tsx +++ b/src/components/settings/GeneralSection.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useEffect, useSyncExternalStore } from "react"; +import { useState, useCallback, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { @@ -305,9 +305,6 @@ export function GeneralSection() { - {/* Error Reporting — right after Setup Center */} - - {/* Appearance */} @@ -368,46 +365,6 @@ export function GeneralSection() { -
); } - -/* ── Sentry opt-out toggle (isolated state) ──────────────────── */ - -const sentrySubscribe = (cb: () => void) => { - window.addEventListener('storage', cb); - return () => window.removeEventListener('storage', cb); -}; -const getSentryEnabled = () => { - try { return localStorage.getItem('codepilot:sentry-disabled') !== 'true'; } catch { return true; } -}; -const getSentryEnabledServer = () => true; // SSR default - -function SentryToggle({ locale, t }: { locale: string; t: (key: TranslationKey) => string }) { - const enabled = useSyncExternalStore(sentrySubscribe, getSentryEnabled, getSentryEnabledServer); - - return ( - - { - const disabled = !checked; - try { - localStorage.setItem('codepilot:sentry-disabled', disabled ? 'true' : 'false'); - window.dispatchEvent(new StorageEvent('storage')); - } catch { /* ignore */ } - fetch('/api/settings/sentry', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ disabled }), - }).catch(() => { /* ignore */ }); - }} - /> - - ); -} diff --git a/src/components/settings/PresetConnectDialog.tsx b/src/components/settings/PresetConnectDialog.tsx index 482bef61..e08fb841 100644 --- a/src/components/settings/PresetConnectDialog.tsx +++ b/src/components/settings/PresetConnectDialog.tsx @@ -20,7 +20,7 @@ import { DialogDescription, DialogFooter, } from "@/components/ui/dialog"; -import { SpinnerGap, CaretDown, CaretUp, ArrowSquareOut, CheckCircle, XCircle, Warning, Lightning } from "@/components/ui/icon"; +import { SpinnerGap, CaretDown, CaretUp } from "@/components/ui/icon"; import type { ProviderFormData } from "./ProviderForm"; import type { QuickPreset } from "./provider-presets"; import { QUICK_PRESETS } from "./provider-presets"; @@ -28,7 +28,7 @@ import type { ApiProvider } from "@/types"; import { useTranslation } from "@/hooks/useTranslation"; import type { TranslationKey } from "@/i18n"; -/** Infer auth style from base URL by fuzzy-matching preset hostnames */ +/** Infer auth style from base URL by fuzzy-matching VENDOR_PRESETS hostnames */ function inferAuthStyleFromUrl(url: string): "api_key" | "auth_token" | null { if (!url) return null; const urlLower = url.toLowerCase(); @@ -37,7 +37,10 @@ function inferAuthStyleFromUrl(url: string): "api_key" | "auth_token" | null { try { const presetHost = new URL(p.base_url).hostname; if (urlLower.includes(presetHost)) { - return p.authStyle as "api_key" | "auth_token"; + // Map preset's known auth style + const env = JSON.parse(p.extra_env || '{}'); + if ('ANTHROPIC_AUTH_TOKEN' in env) return 'auth_token'; + return 'api_key'; } } catch { /* skip invalid URLs */ } } @@ -81,65 +84,26 @@ export function PresetConnectDialog({ const [showAdvanced, setShowAdvanced] = useState(false); const [error, setError] = useState(null); const [saving, setSaving] = useState(false); - const [testing, setTesting] = useState(false); - const [testResult, setTestResult] = useState<{ success: boolean; error?: { code: string; message: string; suggestion: string; recoveryActions?: Array<{ label: string; url?: string; action?: string }> } } | null>(null); const { t } = useTranslation(); const isZh = t('nav.chats') === '对话'; - const handleTestConnection = async () => { - setTesting(true); - setTestResult(null); - try { - const envOverrides: Record = {}; - try { - const parsed = JSON.parse(extraEnv || '{}'); - Object.assign(envOverrides, parsed); - } catch { /* ignore */ } - const res = await fetch('/api/providers/test', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - presetKey: preset?.key, - apiKey: apiKey || undefined, - baseUrl: baseUrl || preset?.base_url || '', - protocol: preset?.protocol || 'anthropic', - authStyle: preset?.key === 'anthropic-thirdparty' ? authStyle : (preset?.authStyle || authStyle), - envOverrides, - modelName: modelName || undefined, - providerName: name || preset?.name, - }), - }); - const data = await res.json(); - setTestResult(data); - } catch (err) { - setTestResult({ success: false, error: { code: 'NETWORK_ERROR', message: 'Failed to reach test endpoint', suggestion: 'Check if the app is running' } }); - } finally { - setTesting(false); - } - }; - // Reset form when dialog opens useEffect(() => { if (!open || !preset) return; setError(null); setSaving(false); - setTesting(false); - setTestResult(null); if (isEdit && editProvider) { // Edit mode — pre-fill from existing provider setName(editProvider.name); setBaseUrl(editProvider.base_url); setExtraEnv(editProvider.extra_env || preset.extra_env); - // Use preset authStyle as source of truth; fall back to extra_env inference for legacy records - let detected: 'auth_token' | 'api_key' = preset.authStyle === 'auth_token' ? 'auth_token' : 'api_key'; - if (preset.key === 'anthropic-thirdparty') { - // Thirdparty presets: infer from stored extra_env since user chose the style - try { - const env = JSON.parse(editProvider.extra_env || "{}"); - detected = "ANTHROPIC_AUTH_TOKEN" in env ? "auth_token" : "api_key"; - } catch { /* keep preset default */ } - } + // Detect auth style from existing extra_env + let detected: 'auth_token' | 'api_key' = 'api_key'; + try { + const env = JSON.parse(editProvider.extra_env || "{}"); + detected = "ANTHROPIC_AUTH_TOKEN" in env ? "auth_token" : "api_key"; + } catch { /* keep default */ } setAuthStyle(detected); setInitialAuthStyle(detected); // If api_key field isn't shown and stored key is empty, use preset default @@ -196,12 +160,12 @@ export function PresetConnectDialog({ setName(preset.name); setExtraEnv(preset.extra_env); setModelName(""); - // Use authStyle directly from preset (single source of truth) - const detectedStyle = (preset.authStyle === 'auth_token' ? 'auth_token' : 'api_key') as 'api_key' | 'auth_token'; + // Auto-detect auth style from preset's extra_env + const presetEnv = (() => { try { return JSON.parse(preset.extra_env || '{}'); } catch { return {}; } })(); + const detectedStyle = 'ANTHROPIC_AUTH_TOKEN' in presetEnv ? 'auth_token' as const : 'api_key' as const; // If preset doesn't expose api_key field, pre-fill from extra_env default // (e.g. Ollama needs ANTHROPIC_AUTH_TOKEN='ollama' without user input) if (!preset.fields.includes("api_key")) { - const presetEnv = (() => { try { return JSON.parse(preset.extra_env || '{}'); } catch { return {}; } })(); const defaultToken = detectedStyle === 'auth_token' ? (presetEnv['ANTHROPIC_AUTH_TOKEN'] || '') : (presetEnv['ANTHROPIC_API_KEY'] || ''); @@ -350,46 +314,6 @@ export function PresetConnectDialog({ - {/* Meta info panel — API key link, billing badge, notes */} - {preset.meta && ( -
-
- {preset.meta.billingModel && ( - - {preset.meta.billingModel === 'pay_as_you_go' ? (isZh ? '按量付费' : 'Pay-as-you-go') - : preset.meta.billingModel === 'coding_plan' ? 'Coding Plan' - : preset.meta.billingModel === 'token_plan' ? 'Token Plan' - : preset.meta.billingModel === 'free' ? (isZh ? '免费' : 'Free') - : preset.meta.billingModel === 'self_hosted' ? (isZh ? '自托管' : 'Self-hosted') - : preset.meta.billingModel} - - )} - {preset.meta.apiKeyUrl && ( - - - {isZh ? '获取 API Key' : 'Get API Key'} - - )} - - - {isZh ? '配置指南' : 'Setup Guide'} - -
- {preset.meta.notes && preset.meta.notes.length > 0 && ( -
- {preset.meta.notes.map((note, i) => ( -

- - {note} -

- ))} -
- )} -
- )} -
{/* Name field — custom/thirdparty */} {preset.fields.includes("name") && ( @@ -622,67 +546,19 @@ export function PresetConnectDialog({ {error &&

{error}

} - {/* Connection test result */} - {testResult && (() => { - const isSkipped = testResult.error?.code === 'SKIPPED'; - const bgClass = testResult.success - ? 'bg-emerald-500/10 border border-emerald-500/20' // lint-allow-raw-color - : isSkipped - ? 'bg-muted border border-border' - : 'bg-destructive/10 border border-destructive/20'; - return ( -
-
- {testResult.success - ? <>{/* lint-allow-raw-color */}{/* lint-allow-raw-color */}{isZh ? '连接成功' : 'Connection successful'} - : isSkipped - ? <>{isZh ? '此服务商类型无法进行连接测试,请保存配置后发送消息验证' : 'Connection test not available for this provider type'} - : <>{testResult.error?.message || 'Connection failed'} - } -
- {!testResult.success && !isSkipped && testResult.error?.suggestion && ( -

{testResult.error.suggestion}

- )} - {!testResult.success && !isSkipped && testResult.error?.recoveryActions && testResult.error.recoveryActions.length > 0 && ( -
- {testResult.error.recoveryActions.filter(a => a.url).map((action, i) => ( - - - {action.label} - - ))} -
- )} -
- ); - })()} - - + -
- - -
+
diff --git a/src/components/settings/ProviderManager.tsx b/src/components/settings/ProviderManager.tsx index cbb844de..3ba1cefc 100644 --- a/src/components/settings/ProviderManager.tsx +++ b/src/components/settings/ProviderManager.tsx @@ -378,7 +378,7 @@ export function ProviderManager() { {provider.name} {provider.api_key - ? (findMatchingPreset(provider)?.authStyle === 'auth_token' ? "Auth Token" : "API Key") + ? (provider.extra_env?.includes("ANTHROPIC_AUTH_TOKEN") ? "Auth Token" : "API Key") : t('provider.configured')}
diff --git a/src/components/settings/provider-presets.tsx b/src/components/settings/provider-presets.tsx index 198644fd..5265a261 100644 --- a/src/components/settings/provider-presets.tsx +++ b/src/components/settings/provider-presets.tsx @@ -3,8 +3,6 @@ import type { ReactNode } from "react"; import { HardDrives } from "@/components/ui/icon"; import type { ApiProvider } from "@/types"; -import { VENDOR_PRESETS } from "@/lib/provider-catalog"; -import type { VendorPreset } from "@/lib/provider-catalog"; import Anthropic from "@lobehub/icons/es/Anthropic"; import OpenRouter from "@lobehub/icons/es/OpenRouter"; import Zhipu from "@lobehub/icons/es/Zhipu"; @@ -50,71 +48,251 @@ export function getProviderIcon(name: string, baseUrl: string): ReactNode { } // --------------------------------------------------------------------------- -// Quick-add preset definitions — generated from VENDOR_PRESETS (single source of truth) +// Quick-add preset definitions // --------------------------------------------------------------------------- export interface QuickPreset { - key: string; + key: string; // unique key name: string; description: string; descriptionZh: string; icon: ReactNode; + // Pre-filled provider data provider_type: string; + /** Wire protocol — determines how the provider is dispatched at runtime */ protocol: string; - /** Auth style from catalog — frontend should use this instead of inferring from extra_env */ - authStyle: string; base_url: string; extra_env: string; + // Which fields user must fill fields: ("name" | "api_key" | "base_url" | "extra_env" | "model_names" | "model_mapping")[]; + // Category: 'chat' (default) or 'media' category?: "chat" | "media"; - /** Provider meta info from catalog (for user guidance) */ - meta?: VendorPreset['meta']; } -/** Map iconKey from VENDOR_PRESETS to React icon component */ -function resolveIcon(iconKey: string): ReactNode { - const ICON_MAP: Record = { - anthropic: , - openrouter: , - zhipu: , - kimi: , - moonshot: , - minimax: , - bedrock: , - google: , - volcengine: , - bailian: , - 'xiaomi-mimo': , - ollama: , - server: , - }; - return ICON_MAP[iconKey] || ; -} - -/** Convert a VendorPreset to the frontend QuickPreset format */ -function toQuickPreset(vp: VendorPreset): QuickPreset { - return { - key: vp.key, - name: vp.name, - description: vp.description, - descriptionZh: vp.descriptionZh, - icon: resolveIcon(vp.iconKey), - provider_type: vp.protocol === 'openrouter' ? 'openrouter' - : vp.protocol === 'bedrock' ? 'bedrock' - : vp.protocol === 'vertex' ? 'vertex' - : vp.protocol === 'gemini-image' ? 'gemini-image' - : 'anthropic', - protocol: vp.protocol, - authStyle: vp.authStyle, - base_url: vp.baseUrl, - extra_env: JSON.stringify(vp.defaultEnvOverrides), - fields: vp.fields as QuickPreset['fields'], - category: vp.category, - meta: vp.meta, - }; -} - -export const QUICK_PRESETS: QuickPreset[] = VENDOR_PRESETS.map(toQuickPreset); +export const QUICK_PRESETS: QuickPreset[] = [ + // ── Anthropic-compatible services ── + { + key: "anthropic-thirdparty", + name: "Anthropic Third-party API", + description: "Anthropic-compatible API — provide URL and Key", + descriptionZh: "Anthropic 兼容第三方 API — 填写地址和密钥", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "", + extra_env: '{"ANTHROPIC_API_KEY":""}', + fields: ["name", "api_key", "base_url", "model_mapping"], + }, + { + key: "anthropic-official", + name: "Anthropic", + description: "Official Anthropic API", + descriptionZh: "Anthropic 官方 API", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "https://api.anthropic.com", + extra_env: "{}", + fields: ["api_key"], + }, + { + key: "openrouter", + name: "OpenRouter", + description: "Use OpenRouter to access multiple models", + descriptionZh: "通过 OpenRouter 访问多种模型", + icon: , + provider_type: "openrouter", + protocol: "openrouter", + base_url: "https://openrouter.ai/api", + extra_env: '{"ANTHROPIC_API_KEY":""}', + fields: ["api_key"], + }, + { + key: "glm-cn", + name: "GLM (CN)", + description: "Zhipu GLM Code Plan — China region", + descriptionZh: "智谱 GLM 编程套餐 — 中国区", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "https://open.bigmodel.cn/api/anthropic", + extra_env: '{"API_TIMEOUT_MS":"3000000","ANTHROPIC_API_KEY":""}', + fields: ["api_key"], + }, + { + key: "glm-global", + name: "GLM (Global)", + description: "Zhipu GLM Code Plan — Global region", + descriptionZh: "智谱 GLM 编程套餐 — 国际区", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "https://api.z.ai/api/anthropic", + extra_env: '{"API_TIMEOUT_MS":"3000000","ANTHROPIC_API_KEY":""}', + fields: ["api_key"], + }, + { + key: "kimi", + name: "Kimi Coding Plan", + description: "Kimi Coding Plan API", + descriptionZh: "Kimi 编程计划 API", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "https://api.kimi.com/coding/", + extra_env: '{"ANTHROPIC_AUTH_TOKEN":""}', + fields: ["api_key"], + }, + { + key: "moonshot", + name: "Moonshot", + description: "Moonshot AI API", + descriptionZh: "月之暗面 API", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "https://api.moonshot.cn/anthropic", + extra_env: '{"ANTHROPIC_API_KEY":""}', + fields: ["api_key"], + }, + { + key: "minimax-cn", + name: "MiniMax (CN)", + description: "MiniMax Code Plan — China region", + descriptionZh: "MiniMax 编程套餐 — 中国区", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "https://api.minimaxi.com/anthropic", + extra_env: '{"API_TIMEOUT_MS":"3000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1","ANTHROPIC_AUTH_TOKEN":""}', + fields: ["api_key"], + }, + { + key: "minimax-global", + name: "MiniMax (Global)", + description: "MiniMax Code Plan — Global region", + descriptionZh: "MiniMax 编程套餐 — 国际区", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "https://api.minimax.io/anthropic", + extra_env: '{"API_TIMEOUT_MS":"3000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1","ANTHROPIC_AUTH_TOKEN":""}', + fields: ["api_key"], + }, + { + key: "volcengine", + name: "Volcengine Ark", + description: "Volcengine Ark Coding Plan — Doubao, GLM, DeepSeek, Kimi", + descriptionZh: "字节火山方舟 Coding Plan — 豆包、GLM、DeepSeek、Kimi", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "https://ark.cn-beijing.volces.com/api/coding", + extra_env: '{"ANTHROPIC_AUTH_TOKEN":""}', + fields: ["api_key", "model_names"], + }, + { + key: "xiaomi-mimo", + name: "Xiaomi MiMo", + description: "Xiaomi MiMo Pay-as-you-go API — MiMo-V2-Pro", + descriptionZh: "小米 MiMo 按量付费 — MiMo-V2-Pro", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "https://api.xiaomimimo.com/anthropic", + extra_env: '{"ANTHROPIC_AUTH_TOKEN":""}', + fields: ["api_key"], + }, + { + key: "xiaomi-mimo-token-plan", + name: "Xiaomi MiMo Token Plan", + description: "Xiaomi MiMo Token Plan subscription — MiMo-V2-Pro", + descriptionZh: "小米 MiMo Token Plan 订阅套餐 — MiMo-V2-Pro", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "https://token-plan-cn.xiaomimimo.com/anthropic", + extra_env: '{"ANTHROPIC_AUTH_TOKEN":""}', + fields: ["api_key"], + }, + { + key: "bailian", + name: "Aliyun Bailian", + description: "Aliyun Bailian Coding Plan — Qwen, GLM, Kimi, MiniMax", + descriptionZh: "阿里云百炼 Coding Plan — 通义千问、GLM、Kimi、MiniMax", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "https://coding.dashscope.aliyuncs.com/apps/anthropic", + extra_env: '{"ANTHROPIC_API_KEY":""}', + fields: ["api_key"], + }, + // ── Cloud platform providers ── + { + key: "bedrock", + name: "AWS Bedrock", + description: "Amazon Bedrock — requires AWS credentials", + descriptionZh: "Amazon Bedrock — 需要 AWS 凭证", + icon: , + provider_type: "bedrock", + protocol: "bedrock", + base_url: "", + extra_env: '{"CLAUDE_CODE_USE_BEDROCK":"1","AWS_REGION":"us-east-1","CLAUDE_CODE_SKIP_BEDROCK_AUTH":"1"}', + fields: ["extra_env"], + }, + { + key: "vertex", + name: "Google Vertex", + description: "Google Vertex AI — requires GCP credentials", + descriptionZh: "Google Vertex AI — 需要 GCP 凭证", + icon: , + provider_type: "vertex", + protocol: "vertex", + base_url: "", + extra_env: '{"CLAUDE_CODE_USE_VERTEX":"1","CLOUD_ML_REGION":"us-east5","CLAUDE_CODE_SKIP_VERTEX_AUTH":"1"}', + fields: ["extra_env"], + }, + // ── Local / self-hosted ── + { + key: "ollama", + name: "Ollama", + description: "Ollama — run local models with Anthropic-compatible API", + descriptionZh: "Ollama — 本地运行模型,Anthropic 兼容 API", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "http://localhost:11434", + extra_env: '{"ANTHROPIC_AUTH_TOKEN":"ollama","ANTHROPIC_API_KEY":""}', + fields: ["base_url", "model_names"], + }, + // ── Proxy / gateway ── + { + key: "litellm", + name: "LiteLLM", + description: "LiteLLM proxy — local or remote", + descriptionZh: "LiteLLM 代理 — 本地或远程", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "http://localhost:4000", + extra_env: "{}", + fields: ["api_key", "base_url"], + }, + // ── Media providers ── + { + key: "gemini-image", + name: "Google Gemini (Image)", + description: "Nano Banana Pro — AI image generation by Google Gemini", + descriptionZh: "Nano Banana Pro — Google Gemini AI 图片生成", + icon: , + provider_type: "gemini-image", + protocol: "gemini-image", + base_url: "https://generativelanguage.googleapis.com/v1beta", + extra_env: '{"GEMINI_API_KEY":""}', + fields: ["api_key"], + category: "media", + }, +]; // --------------------------------------------------------------------------- // Gemini image model definitions diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index a9cf7dc4..974bf663 100644 --- a/src/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -1,6 +1,6 @@ 'use client'; -import { X, CheckCircle, XCircle, Warning, Info, ArrowClockwise } from '@/components/ui/icon'; +import { X, CheckCircle, XCircle, Warning, Info } from '@/components/ui/icon'; import { Button } from '@/components/ui/button'; import { useToastState, type Toast } from '@/hooks/useToast'; import { cn } from '@/lib/utils'; @@ -10,7 +10,6 @@ const ICON_MAP = { error: XCircle, warning: Warning, info: Info, - loading: ArrowClockwise, }; const STYLE_MAP = { @@ -18,7 +17,6 @@ const STYLE_MAP = { error: 'border-destructive/30 bg-destructive/10 text-destructive', warning: 'border-status-warning/30 bg-status-warning-muted text-status-warning-foreground', info: 'border-border bg-muted text-foreground', - loading: 'border-border bg-muted text-foreground', }; function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }) { @@ -30,7 +28,7 @@ function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void } STYLE_MAP[toast.type] )} > - + {toast.message} {toast.action && (