From be0967eff1de3efbf2925788082c807ca19838b8 Mon Sep 17 00:00:00 2001 From: wangnov Date: Sun, 7 Dec 2025 20:26:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor(docs):=20=E7=BB=9F=E4=B8=80=20AI=20Age?= =?UTF-8?q?nt=20=E9=A1=B9=E7=9B=AE=E6=96=87=E6=A1=A3=E4=B8=BA=20CLAUDE.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 AGENTS.md,改用 CLAUDE.md 作为所有 AI Agent 的统一项目文档。 主要改动: - 删除 AGENTS.md 及其同步脚本 ensure-guidelines-sync.mjs - 新增 ensure-agent-config.mjs 检查 Codex/Gemini 配置 - 支持增量覆盖逻辑:项目级配置优先,无字段时回退用户级 - 配置检查仅显示警告,不阻断 CI 流程 配置检查项: - Codex: project_doc_fallback_filenames 包含 CLAUDE.md - Gemini: context.fileName 包含 CLAUDE.md --- AGENTS.md | 188 ------------------ CLAUDE.md | 33 ++-- package.json | 4 +- scripts/ensure-agent-config.mjs | 302 +++++++++++++++++++++++++++++ scripts/ensure-guidelines-sync.mjs | 158 --------------- scripts/run-checks.mjs | 8 +- 6 files changed, 330 insertions(+), 363 deletions(-) delete mode 100644 AGENTS.md create mode 100644 scripts/ensure-agent-config.mjs delete mode 100644 scripts/ensure-guidelines-sync.mjs diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 8e8dfb0..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -agent: Codex -last-updated: 2025-12-07 ---- - -# DuckCoding 开发协作规范 - -> 本文档为指导 AI AGENT 的开发协作规范,同时也作为 AI AGENT 开发指南和持久化项目记忆存在。文档共有 `CLAUDE.md`、`AGENTS.md` 两份。两份规范文档的正文部分必须始终保持一致,yaml头部无需同步。 -> 本文档作为项目记忆文档,需要及时更新。**请务必在开发完成后根据代码的实际情况更新本文档需要修改的地方以反映真实代码情况!!!** - -## 核心命令一览 - -- `npm install`:安装前后端依赖(Node 18+ / npm 9+)。 -- `npm run check`:开发工具链主入口,统一调度 AI 记忆文档同步 → ESLint → Clippy → Prettier → cargo fmt,并输出中文摘要。若缺少 `dist/`,会自动尝试 `npm run build` 供 Tauri Clippy 使用。 -- `npm run check:fix`:修复版入口,顺序同上,遇可修复项会自动 `--fix`。 -- `npm run tauri dev`:本地启动 Tauri 应用进行端到端手动验证。 -- `npm run tauri build`: 本地构建 Tauri 应用安装包。 -- `npm run test` / `npm run test:rs`:后端 Rust 单测(当前无前端测试,test 等同 test:rs)。 -- `cargo test --locked`:Rust 单测执行器;缺乏覆盖时请补测试后再运行。 -- `npm run coverage:rs`:后端覆盖率检查(基于 cargo-llvm-cov,默认行覆盖阈值 90%,需先安装 llvm-tools-preview 与 cargo-llvm-cov,可运行 `npm run coverage:rs:setup` 自动安装依赖)。 - -## 日常开发流程 - -0. **fork项目**:在 github 找到本项目的上游仓库: 并 fork 最新的 main 分支(后续开发前需确保sync fork以避免冲突),clone 到本地。 -1. **创建分支**:`git switch -c feature/` 或 `refactor/`,避免多人在同一文件上叠加提交。 -2. **编码前**:阅读/更新对应任务的设计文档,确保拆分策略一致,减少冲突。 -3. **开发中**: - - 大改动请模块化提交,保持 main.rs / App.tsx 等中心文件最小改动范围。 - - 随手运行 `npm run check:fix`,保持 0 ESLint/Clippy 告警。 -4. **提交前**: - - 运行 `npm run check`;失败立即`npm run check:fix`尝试自动修复,若无法自动修复则手动修复,禁止忽略告警。 - - 运行 `cargo test --locked` 与必要的端测脚本。 - - 若有必要,更新 `AGENTS.md` / `CLAUDE.md` (根据所使用的 AI Agent 来决定),并执行 `npm run guidelines:fix` 自动同步另一份文档。 -5. **提交/PR**: - - commit/pr 需遵循 Conventional Commits 规范,description使用简体中文。 - - pr 描述需包含:动机、主要改动点、测试情况、风险评估。 - - 避免“修复 CI”类模糊描述,明确指出受影响模块。 - - 如有可关闭的 issue,应在 pr 内提及,以便在 merge 后自动关闭。 - -## 零警告与质量门禁 - -- ESLint、Clippy、Prettier、`cargo fmt` 必须全部通过,禁止忽略/跳过检查。 -- CI 未通过禁止合并;若需临时跳过必须在 PR 中详细说明原因并获 Reviewer 认可。 -- 引入第三方依赖需说明用途、体积和维护计划。 - -## 文档同步要求 - -- `AGENTS.md`、`CLAUDE.md` 用于不同协作者(全体/Claude/Codex),但内容必须完全一致。 -- `npm run guidelines:fix` / `npm run check:fix` 会以最近修改的正文为基准自动同步两份文档,YAML 头信息不参与同步。 -- GitHub Actions 会在 PR 中运行同样的脚本,若不一致将直接失败。 - -## PR 清单 - -- [ ] 已运行 `npm run check` 且全部通过。 -- [ ] Rust/前端测试已运行(或说明尚未覆盖的原因)。 -- [ ] 重要变更附测试或验证截图,方便 Reviewer。 - -## CI / PR 检查 - -- `.github/workflows/pr-check.yml` 在 pull_request / workflow_dispatch 下运行,矩阵覆盖 ubuntu-22.04、windows-latest、macos-14 (arm64)、macos-13 (x64),策略 `fail-fast: false`。 -- 每个平台执行 `npm ci` → `npm run check`;若首次检查失败,会继续跑 `npm run check:fix` 与复验 `npm run check` 以判断是否可自动修复,但只要初次检查失败,该平台作业仍标红以阻止合并。 -- PR 事件下只保留一条自动评论,双语表格固定展示四个平台;未跑完的平台显示“运行中...”,跑完后实时更新结果、check/fix/recheck 状态、run 链接与日志包名(artifact `pr-check-.zip`,含 `npm run check` / `check:fix` / `recheck` 输出)。文案提示:如首检失败请本地 `npm run check:fix` → `npm run check` 并提交修复;若 fix 仍失败则需本地排查;跨平台差异无法复现可复制日志给 AI 获取排查建议。 -- Linux 装 `libwebkit2gtk-4.1-dev`、`libjavascriptcoregtk-4.1-dev`、`patchelf` 等 Tauri v2 依赖;Windows 确保 WebView2 Runtime(先查注册表,winget 安装失败则回退微软官方静默安装包);Node 20.19.0,Rust stable(含 clippy / rustfmt),启用 npm 与 cargo 缓存。 -- CI 未通过不得合并;缺少 dist 时会在 `npm run check` 内自动触发 `npm run build` 以满足 Clippy 输入。 - -## 架构记忆(2025-11-29) - -- `src-tauri/src/main.rs` 仅保留应用启动与托盘事件注册,所有 Tauri Commands 拆分到 `src-tauri/src/commands/*`,服务实现位于 `services/*`,核心设施放在 `core/*`(HTTP、日志、错误)。 -- **工具管理系统**: - - 多环境架构:支持本地(Local)、WSL、SSH 三种环境的工具实例管理 - - 数据模型:`ToolType`(环境类型)、`ToolInstance`(工具实例)存储在 `models/tool.rs` - - **JSON 存储(2025-12-04)**:`tools.json` 存储所有工具实例,支持版本控制和多端同步(位于 `~/.duckcoding/tools.json`) - - 数据结构:按工具分组(`ToolGroup`),每个工具包含 `local_tools`、`wsl_tools`、`ssh_tools` 三个实例列表 - - 数据管理:`services/tool/db::ToolInstanceDB` 操作 JSON 文件,使用 `DataManager` 统一读写 - - 自动迁移:首次启动自动从 SQLite 迁移到 JSON,旧数据库备份为 `tool_instances.db.backup` - - 安装方式记录:`install_method` 字段记录实际安装方式(npm/brew/official),用于自动选择更新方法 - - WSL 支持:`utils/wsl_executor::WSLExecutor` 提供 Windows 下的 WSL 命令执行和工具检测(10秒超时) - - Tauri 命令:`get_tool_instances`、`refresh_tool_instances`、`add_wsl_tool_instance`、`add_ssh_tool_instance`、`delete_tool_instance`(位于 `commands/tool_management.rs`) - - 前端页面:`ToolManagementPage` 按工具(Claude Code/CodeX/Gemini CLI)分组展示,每个工具下列出所有环境实例,使用表格列表样式(`components/ToolListSection`) - - 功能支持:检测更新(仅 DuckCoding 管理 + 非 SSH)、版本管理(占位 UI)、删除实例(仅 SSH 非内置) - - 导航集成:AppSidebar 新增"工具管理"入口(Wrench 图标),原"安装工具"已注释 - - 类型安全:完整的 TypeScript 类型定义在 `types/tool-management.ts`,Hook `useToolManagement` 负责状态管理和操作 - - SSH 功能:本期仅保留 UI 和数据结构,实际功能禁用(`AddInstanceDialog` 和表格操作按钮灰显) - - **Trait-based Detector 架构(2025-12-04)**: - - `ToolDetector` trait 定义统一的检测、安装、配置管理接口(位于 `services/tool/detector_trait.rs`) - - 每个工具独立实现:`ClaudeCodeDetector`、`CodeXDetector`、`GeminiCLIDetector`(位于 `services/tool/detectors/`) - - `DetectorRegistry` 注册表管理所有 Detector 实例,提供 `get(tool_id)` 查询接口 - - `ToolRegistry` 和 `InstallerService` 优先使用 Detector,未注册的工具回退到旧逻辑(向后兼容) - - 新增工具仅需:1) 实现 ToolDetector trait,2) 注册到 DetectorRegistry,3) 添加 Tool 定义 - - 每个 Detector 文件包含完整的检测、安装、更新、配置管理逻辑,模块化且易测试 -- **透明代理已重构为多工具架构**: - - `ProxyManager` 统一管理三个工具(Claude Code、Codex、Gemini CLI)的代理实例 - - `HeadersProcessor` trait 定义工具特定的 headers 处理逻辑(位于 `services/proxy/headers/`) - - `ToolProxyConfig` 存储在 `GlobalConfig.proxy_configs` HashMap 中,每个工具独立配置 - - 支持三个代理同时运行,端口由用户配置(默认: claude-code=8787, codex=8788, gemini-cli=8789) - - 旧的 `transparent_proxy_*` 字段会在读取配置时自动迁移到新结构 - - 新命令:`start_tool_proxy`、`stop_tool_proxy`、`get_all_proxy_status` - - 旧命令保持兼容,内部使用新架构实现 -- `ToolProxyConfig` 额外存储 `real_profile_name`、`auto_start`、工具级 `session_endpoint_config_enabled`,全局配置新增 `hide_transparent_proxy_tip` 控制设置页横幅显示 -- `GlobalConfig.hide_session_config_hint` 持久化会话级端点提示的隐藏状态,`ProxyControlBar`/`ProxySettingsDialog`/`ClaudeContent` 通过 `open-proxy-settings` 与 `proxy-config-updated` 事件联动刷新视图 -- 日志系统支持完整配置管理:`GlobalConfig.log_config` 存储级别/格式/输出目标;`log_commands.rs` 提供查询与更新命令,`LogSettingsTab` 可热重载级别、保存文件输出设置;`core/logger.rs` 通过 `update_log_level` reload 机制动态调整 -- 应用启动时 `duckcoding::auto_start_proxies` 会读取配置,满足 `enabled && auto_start` 且存在 `local_api_key` 的代理会自动启动 -- `utils::config::migrate_session_config` 会将旧版 `GlobalConfig.session_endpoint_config_enabled` 自动迁移到各工具配置,确保升级过程不会丢开关 -- 全局配置读写统一走 `utils::config::{read_global_config, write_global_config, apply_proxy_if_configured}`,避免出现多份路径逻辑;任何命令要修改配置都应调用这些辅助函数。 -- UpdateService / 统计命令等都通过 `tauri::State` 注入复用,前端 ToolStatus 的结构保持轻量字段 `{id,name,installed,version}`。 -- **工具状态管理已统一到数据库架构(2025-12-04)**: - - `check_installations` 命令改为从 `ToolRegistry` 获取数据,优先读取数据库(< 10ms),首次启动自动检测并持久化(~1.3s) - - `refresh_tool_status` 命令重新检测所有本地工具并更新数据库(upsert + 删除已卸载) - - Dashboard 和 ToolManagement 现使用统一数据源,消除了双数据流问题 - - `ToolStatusCache` 标记为已废弃,保留仅用于向后兼容 - - 所有工具状态查询统一走 `ToolRegistry::get_local_tool_status()` 和 `refresh_and_get_local_status()` -- UI 相关的托盘/窗口操作集中在 `src-tauri/src/ui/*`,其它模块如需最小化到托盘请调用 `ui::hide_window_to_tray` 等封装方法。 -- 新增 `TransparentProxyPage` 与会话数据库:`SESSION_MANAGER` 使用 SQLite 记录每个代理会话的 endpoint/API Key,前端可按工具启停代理、查看历史并启用「会话级 Endpoint 配置」开关。页面内的 `ProxyControlBar`、`ProxySettingsDialog`、`ProxyConfigDialog` 负责代理启停、配置切换、工具级设置并内建缺失配置提示。 -- **余额监控页面(BalancePage)**: - - 后端提供通用 `fetch_api` 命令(位于 `commands/api_commands.rs`),支持 GET/POST、自定义 headers、超时控制 - - 前端使用 JavaScript `Function` 构造器执行用户自定义的 extractor 脚本(位于 `utils/extractor.ts`) - - 配置存储在 localStorage,API Key 仅保存在内存(`useApiKeys` hook) - - 支持预设模板(NewAPI、OpenAI、自定义),模板定义在 `templates/index.ts` - - `useBalanceMonitor` hook 负责自动刷新逻辑,支持配置级别的刷新间隔 - - 配置表单(`ConfigFormDialog`)支持模板选择、代码编辑、静态 headers(JSON 格式) - - 卡片视图(`ConfigCard`)展示余额信息、使用比例、到期时间、错误提示 -- **Profile 管理系统 v2.0(2025-12-06)**: - - **双文件 JSON 架构**:替代旧版分散式目录结构(profiles/、active/、metadata/) - - `~/.duckcoding/profiles.json`:统一存储所有工具的 Profile 数据仓库 - - `~/.duckcoding/active.json`:工具激活状态管理 - - **数据结构**: - - `ProfilesStore`:按工具分组(`claude_code`、`codex`、`gemini_cli`),每个工具包含 `HashMap` - - `ActiveStore`:每个工具一个 `Option`,记录当前激活的 Profile 名称和切换时间 - - `ProfilePayload`:Enum 类型,支持 Claude/Codex/Gemini 三种变体,存储工具特定配置和原生文件快照 - - **核心服务**(位于 `services/profile_manager/`): - - `ProfileManager`:统一的 Profile CRUD 接口,支持列表、创建、更新、删除、激活、导入导出 - - `NativeConfigSync`:原生配置文件参数同步 - - **激活操作**:仅替换工具原生配置文件中的 API Key 和 Base URL 两个参数,保留其他配置(如主题、快捷键等) - - **支持格式**:Claude(settings.json)、Codex(auth.json + config.toml)、Gemini(.env) - - **完整快照**:Profile 存储时保存完整原生文件快照(settings.json + config.json、config.toml + auth.json、settings.json + .env),用于导入导出和配置回滚 - - **迁移系统**(ProfileV2Migration): - - 支持从**两套旧系统**迁移到新架构: - 1. **原始工具配置**:`~/.claude/settings.{profile}.json`、`~/.codex/config.{profile}.toml + auth.{profile}.json`、`~/.gemini-cli/.env.{profile}` - 2. **旧 profiles/ 目录系统**:`~/.duckcoding/profiles/{tool}/{profile}.{ext}` + `active/{tool}.json` + `metadata/index.json` - - 迁移逻辑:先从原始配置迁移创建 Profile,再从 profiles/ 目录补充(跳过重复),最后合并激活状态 - - 清理机制:迁移完成后自动备份到 `backup_profile_v1_{timestamp}/` 并删除旧目录(profiles/、active/、metadata/) - - 手动清理:提供 `clean_legacy_backups` 命令删除备份的原始配置文件(settings.{profile}.json 等) - - **前端页面**(ProfileManagementPage): - - Tab 分组布局:按工具(Claude Code、Codex、Gemini CLI)水平分页 - - `ActiveProfileCard`:显示当前激活配置,支持工具实例选择器(Local/WSL/SSH)、版本信息、更新检测 - - Profile 列表:支持创建、编辑、删除、激活、导入导出操作 - - **Tauri 命令**(位于 `commands/profile_commands.rs`): - - Profile CRUD:`list_profiles`、`create_profile`、`update_profile`、`delete_profile`、`activate_profile` - - 导入导出:`import_profile`、`export_profile`、`import_from_native` - - 原生同步:`sync_to_native`、`sync_from_native` - - **类型定义**(`types/profile.ts`): - - `ProfileGroup`:工具分组,包含工具信息和 Profile 列表 - - `ProfileDescriptor`:Profile 元数据(名称、格式、创建/更新时间、来源) - - `ProfilePayload`:联合类型,支持 Claude/Codex/Gemini 配置 -- **新用户引导系统**: - - 首次启动强制引导,配置存储在 `GlobalConfig.onboarding_status: Option`(包含已完成版本、跳过步骤、完成时间) - - 版本化管理,支持增量更新(v1 -> v2 只展示新增内容),独立的引导内容版本号(与应用版本解耦) - - 前端定义引导步骤(`components/Onboarding/config/versions.ts`:`CURRENT_ONBOARDING_VERSION`、`VERSION_STEPS`、`getRequiredSteps`) - - Rust 命令:`get_onboarding_status`、`save_onboarding_progress`、`complete_onboarding`、`reset_onboarding`(位于 `commands/onboarding.rs`) - - 设置页「关于」标签可重新打开引导(调用 `reset_onboarding` 后刷新页面) - - v1 引导包含 4 步:欢迎页、代理配置(可跳过)、工具介绍、完成页;v2/v3 引导聚焦新增特性 - - 引导组件:`OnboardingOverlay`(全屏遮罩)、`OnboardingFlow`(流程控制)、步骤组件(`steps/v*/*`) - - App.tsx 启动时检查 `onboarding_status`,根据版本对比决定是否显示引导 -- **统一数据管理系统(DataManager)**: - - 模块位置:`src-tauri/src/data/*`,提供 JSON/TOML/ENV 格式的统一管理接口 - - 核心组件:`DataManager` 统一入口,`JsonManager`/`TomlManager`/`EnvManager` 格式管理器,LRU 缓存层(基于文件校验和) - - 使用模式: - - `manager.json()` - 带缓存的 JSON 操作,用于全局配置和 Profile - - `manager.json_uncached()` - 无缓存的 JSON 操作,用于工具原生配置(需实时更新) - - `manager.toml()` - TOML 操作,支持保留注释和格式(使用 `read_document()` / `write()` 配合 `toml_edit::DocumentMut`) - - `manager.env()` - .env 文件操作,自动排序和格式化 - - 自动化特性:目录创建、Unix 权限设置(0o600)、原子写入、基于 mtime 的缓存失效 - - 已迁移模块: - - `utils/config.rs`: 全局配置读写(`read_global_config`、`write_global_config`) - - `services/config.rs`: 工具配置管理(Claude/Codex/Gemini 的 read/save/apply 系列函数) - - `services/profile_store.rs`: Profile 存储管理(`save_profile_payload`、`load_profile_payload`、`read_active_state`、`save_active_state`) - - 测试覆盖:16 个迁移测试(`data::migration_tests`)+ 各模块原有测试全部通过 - - API 原则:所有新代码的文件 I/O 操作必须使用 DataManager,禁止直接使用 `fs::read_to_string`/`fs::write` - -### 透明代理扩展指南 - -添加新工具支持需要: - -1. 在 `services/proxy/headers/` 实现 `HeadersProcessor` trait -2. 在 `services/proxy/headers/mod.rs` 的 `create_headers_processor` 工厂函数中注册 -3. 在 `models/tool.rs` 添加工具定义(如已存在则跳过) -4. 在 `models/config.rs` 的 `default_proxy_configs` 函数中添加默认端口配置 -5. 无需修改 `ProxyManager` 和命令层代码(自动支持) diff --git a/CLAUDE.md b/CLAUDE.md index f383b3b..8270fad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,17 +1,17 @@ --- -agent: Claude Code +agents: Codex, Claude-Code, Gemini-Cli last-updated: 2025-12-07 --- # DuckCoding 开发协作规范 -> 本文档为指导 AI AGENT 的开发协作规范,同时也作为 AI AGENT 开发指南和持久化项目记忆存在。文档共有 `CLAUDE.md`、`AGENTS.md` 两份。两份规范文档的正文部分必须始终保持一致,yaml头部无需同步。 +> 本文档为指导 AI AGENT 的开发协作规范,同时也作为 AI AGENT 开发指南和持久化项目记忆存在。 > 本文档作为项目记忆文档,需要及时更新。**请务必在开发完成后根据代码的实际情况更新本文档需要修改的地方以反映真实代码情况!!!** ## 核心命令一览 - `npm install`:安装前后端依赖(Node 18+ / npm 9+)。 -- `npm run check`:开发工具链主入口,统一调度 AI 记忆文档同步 → ESLint → Clippy → Prettier → cargo fmt,并输出中文摘要。若缺少 `dist/`,会自动尝试 `npm run build` 供 Tauri Clippy 使用。 +- `npm run check`:开发工具链主入口,统一调度 AI Agent 配置检查 → ESLint → Clippy → Prettier → cargo fmt,并输出中文摘要。若缺少 `dist/`,会自动尝试 `npm run build` 供 Tauri Clippy 使用。 - `npm run check:fix`:修复版入口,顺序同上,遇可修复项会自动 `--fix`。 - `npm run tauri dev`:本地启动 Tauri 应用进行端到端手动验证。 - `npm run tauri build`: 本地构建 Tauri 应用安装包。 @@ -30,11 +30,11 @@ last-updated: 2025-12-07 4. **提交前**: - 运行 `npm run check`;失败立即`npm run check:fix`尝试自动修复,若无法自动修复则手动修复,禁止忽略告警。 - 运行 `cargo test --locked` 与必要的端测脚本。 - - 若有必要,更新 `AGENTS.md` / `CLAUDE.md` (根据所使用的 AI Agent 来决定),并执行 `npm run guidelines:fix` 自动同步另一份文档。 + - 若有必要,更新 `CLAUDE.md` 5. **提交/PR**: - commit/pr 需遵循 Conventional Commits 规范,description使用简体中文。 - pr 描述需包含:动机、主要改动点、测试情况、风险评估。 - - 避免“修复 CI”类模糊描述,明确指出受影响模块。 + - 避免"修复 CI"类模糊描述,明确指出受影响模块。 - 如有可关闭的 issue,应在 pr 内提及,以便在 merge 后自动关闭。 ## 零警告与质量门禁 @@ -43,11 +43,22 @@ last-updated: 2025-12-07 - CI 未通过禁止合并;若需临时跳过必须在 PR 中详细说明原因并获 Reviewer 认可。 - 引入第三方依赖需说明用途、体积和维护计划。 -## 文档同步要求 - -- `AGENTS.md`、`CLAUDE.md` 用于不同协作者(全体/Claude/Codex),但内容必须完全一致。 -- `npm run guidelines:fix` / `npm run check:fix` 会以最近修改的正文为基准自动同步两份文档,YAML 头信息不参与同步。 -- GitHub Actions 会在 PR 中运行同样的脚本,若不一致将直接失败。 +## AI 自动阅读文档前提 + +- `CLAUDE.md` 默认为 Claude-Code 使用 +- Codex 使用需要设置 ~/.codex/config.toml 中的 + ```toml + project_doc_fallback_filenames = ["CLAUDE.md"] + ``` +- Gemini-CLI 使用需要设置 ~/.gemini/settings.json 中的 + ```json + { + "context": { + "fileName": "CLAUDE.md" + } + } + ``` +- `npm run check` 会检查这两项配置(仅当检测到对应工具已安装时),未通过显示警告。可用 `npm run check:fix` 自动修复。 ## PR 清单 @@ -59,7 +70,7 @@ last-updated: 2025-12-07 - `.github/workflows/pr-check.yml` 在 pull_request / workflow_dispatch 下运行,矩阵覆盖 ubuntu-22.04、windows-latest、macos-14 (arm64)、macos-13 (x64),策略 `fail-fast: false`。 - 每个平台执行 `npm ci` → `npm run check`;若首次检查失败,会继续跑 `npm run check:fix` 与复验 `npm run check` 以判断是否可自动修复,但只要初次检查失败,该平台作业仍标红以阻止合并。 -- PR 事件下只保留一条自动评论,双语表格固定展示四个平台;未跑完的平台显示“运行中...”,跑完后实时更新结果、check/fix/recheck 状态、run 链接与日志包名(artifact `pr-check-.zip`,含 `npm run check` / `check:fix` / `recheck` 输出)。文案提示:如首检失败请本地 `npm run check:fix` → `npm run check` 并提交修复;若 fix 仍失败则需本地排查;跨平台差异无法复现可复制日志给 AI 获取排查建议。 +- PR 事件下只保留一条自动评论,双语表格固定展示四个平台;未跑完的平台显示"运行中...",跑完后实时更新结果、check/fix/recheck 状态、run 链接与日志包名(artifact `pr-check-.zip`,含 `npm run check` / `check:fix` / `recheck` 输出)。文案提示:如首检失败请本地 `npm run check:fix` → `npm run check` 并提交修复;若 fix 仍失败则需本地排查;跨平台差异无法复现可复制日志给 AI 获取排查建议。 - Linux 装 `libwebkit2gtk-4.1-dev`、`libjavascriptcoregtk-4.1-dev`、`patchelf` 等 Tauri v2 依赖;Windows 确保 WebView2 Runtime(先查注册表,winget 安装失败则回退微软官方静默安装包);Node 20.19.0,Rust stable(含 clippy / rustfmt),启用 npm 与 cargo 缓存。 - CI 未通过不得合并;缺少 dist 时会在 `npm run check` 内自动触发 `npm run build` 以满足 Clippy 输入。 diff --git a/package.json b/package.json index fc42da0..3eb37d1 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "test:rs": "cargo test --manifest-path src-tauri/Cargo.toml --workspace --locked", "coverage:rs": "cargo llvm-cov --manifest-path src-tauri/Cargo.toml --workspace --locked --fail-under-lines 90", "coverage:rs:setup": "rustup component add llvm-tools-preview && cargo install cargo-llvm-cov --locked", - "guidelines:check": "node scripts/ensure-guidelines-sync.mjs --mode=check", - "guidelines:fix": "node scripts/ensure-guidelines-sync.mjs --mode=fix", + "agent-config:check": "node scripts/ensure-agent-config.mjs --mode=check", + "agent-config:fix": "node scripts/ensure-agent-config.mjs --mode=fix", "check": "node scripts/run-checks.mjs", "check:fix": "node scripts/run-checks.mjs --fix" }, diff --git a/scripts/ensure-agent-config.mjs b/scripts/ensure-agent-config.mjs new file mode 100644 index 0000000..6e51a98 --- /dev/null +++ b/scripts/ensure-agent-config.mjs @@ -0,0 +1,302 @@ +#!/usr/bin/env node + +/** + * AI Agent 配置检查脚本 + * + * 检查 Codex 和 Gemini CLI 的配置是否正确引用了 CLAUDE.md 作为项目文档。 + * + * 检查逻辑: + * 1. 先检查项目级配置,没有则检查用户级配置 + * 2. 如果用户根本没有该工具的配置目录/文件,说明不使用该工具,跳过检查 + * 3. 配置不正确时显示警告,但不阻断 check 流程 + * + * 用法: + * node scripts/ensure-agent-config.mjs --mode=check # 检查模式(默认) + * node scripts/ensure-agent-config.mjs --mode=fix # 自动修复模式 + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; + +const MODE = parseMode(process.argv.slice(2)); +const PROJECT_ROOT = process.cwd(); +const HOME_DIR = os.homedir(); +const TARGET_FILE = 'CLAUDE.md'; + +// ==================== 主流程 ==================== + +const results = { + codex: checkCodex(), + gemini: checkGemini(), +}; + +// 过滤掉跳过的工具 +const activeResults = Object.entries(results).filter(([, r]) => !r.skipped); +const warnings = activeResults.filter(([, r]) => !r.ok); + +console.log('\nAI Agent 配置检查结果:'); + +if (activeResults.length === 0) { + console.log(' (未检测到 Codex 或 Gemini CLI 配置,跳过检查)'); +} else { + for (const [tool, result] of activeResults) { + const icon = result.ok ? '✓' : '⚠'; + console.log(` ${icon} ${tool}: ${result.message}`); + } +} + +// 显示跳过的工具 +const skippedTools = Object.entries(results).filter(([, r]) => r.skipped); +if (skippedTools.length > 0) { + const skippedNames = skippedTools.map(([name]) => name).join(', '); + console.log(` - 跳过: ${skippedNames}(未安装或未配置)`); +} + +if (warnings.length > 0 && MODE === 'check') { + console.log('\n提示: 运行 npm run check:fix 可自动修复上述警告。'); +} + +// 警告不阻断流程,始终返回成功 +console.log(''); + +// ==================== Codex 检查 ==================== + +function checkCodex() { + const projectConfigDir = path.join(PROJECT_ROOT, '.codex'); + const userConfigDir = path.join(HOME_DIR, '.codex'); + const configFileName = 'config.toml'; + + const projectConfig = path.join(projectConfigDir, configFileName); + const userConfig = path.join(userConfigDir, configFileName); + + const hasProjectConfig = fs.existsSync(projectConfig); + const hasUserConfig = fs.existsSync(userConfig); + + // 如果两个级别都没有配置文件,说明用户不使用 Codex,跳过 + if (!hasProjectConfig && !hasUserConfig) { + return { ok: true, skipped: true, message: '未安装或未配置' }; + } + + // 增量覆盖逻辑:先检查项目级,如果项目级没有该字段则回退到用户级 + const configsToCheck = []; + if (hasProjectConfig) configsToCheck.push({ path: projectConfig, level: '项目级' }); + if (hasUserConfig) configsToCheck.push({ path: userConfig, level: '用户级' }); + + for (const { path: configPath, level: configLevel } of configsToCheck) { + const content = fs.readFileSync(configPath, 'utf8'); + const arrayValue = parseTomlArray(content, 'project_doc_fallback_filenames'); + + // 如果该级别没有这个字段,继续检查下一级 + if (arrayValue === null) { + continue; + } + + // 找到了字段,检查是否包含目标文件 + if (arrayValue.includes(TARGET_FILE)) { + return { + ok: true, + skipped: false, + message: `project_doc_fallback_filenames 包含 ${TARGET_FILE} (${configLevel})`, + }; + } + + // 字段存在但不包含目标文件 + if (MODE === 'fix') { + return fixCodexConfig(configPath, content, configLevel, arrayValue); + } + + return { + ok: false, + skipped: false, + message: `${configLevel}配置的 project_doc_fallback_filenames 未包含 ${TARGET_FILE}`, + }; + } + + // 所有级别都没有该字段,需要添加(fix 模式添加到优先级最高的配置) + if (MODE === 'fix') { + const targetConfig = configsToCheck[0]; + const content = fs.readFileSync(targetConfig.path, 'utf8'); + return fixCodexConfig(targetConfig.path, content, targetConfig.level); + } + + return { + ok: false, + skipped: false, + message: '所有配置级别都缺少 project_doc_fallback_filenames 字段', + }; +} + +function fixCodexConfig(configPath, content, level, existingArray = null) { + const newArray = existingArray ? [...existingArray, TARGET_FILE] : [TARGET_FILE]; + const arrayStr = JSON.stringify(newArray); + + let newContent; + if (existingArray !== null) { + // 替换现有数组 + newContent = content.replace( + /project_doc_fallback_filenames\s*=\s*\[[^\]]*\]/, + `project_doc_fallback_filenames = ${arrayStr}`, + ); + } else { + // 在文件开头添加字段 + newContent = `project_doc_fallback_filenames = ${arrayStr}\n${content}`; + } + + fs.writeFileSync(configPath, newContent, 'utf8'); + return { + ok: true, + skipped: false, + message: `已修复${level}配置,添加 ${TARGET_FILE} 到 project_doc_fallback_filenames`, + }; +} + +// ==================== Gemini 检查 ==================== + +function checkGemini() { + const projectConfigDir = path.join(PROJECT_ROOT, '.gemini'); + const userConfigDir = path.join(HOME_DIR, '.gemini'); + const configFileName = 'settings.json'; + + const projectConfig = path.join(projectConfigDir, configFileName); + const userConfig = path.join(userConfigDir, configFileName); + + const hasProjectConfig = fs.existsSync(projectConfig); + const hasUserConfig = fs.existsSync(userConfig); + + // 如果两个级别都没有配置文件,说明用户不使用 Gemini,跳过 + if (!hasProjectConfig && !hasUserConfig) { + return { ok: true, skipped: true, message: '未安装或未配置' }; + } + + // 增量覆盖逻辑:先检查项目级,如果项目级没有该字段则回退到用户级 + const configsToCheck = []; + if (hasProjectConfig) configsToCheck.push({ path: projectConfig, level: '项目级' }); + if (hasUserConfig) configsToCheck.push({ path: userConfig, level: '用户级' }); + + for (const { path: configPath, level: configLevel } of configsToCheck) { + const content = fs.readFileSync(configPath, 'utf8'); + let config; + try { + config = JSON.parse(content); + } catch { + // JSON 解析失败,继续检查下一级 + continue; + } + + const fileName = config?.context?.fileName; + + // 如果该级别没有这个字段,继续检查下一级 + if (fileName === undefined) { + continue; + } + + // 找到了字段,检查是否包含目标文件 + const hasTarget = Array.isArray(fileName) + ? fileName.includes(TARGET_FILE) + : fileName === TARGET_FILE; + + if (hasTarget) { + const displayValue = Array.isArray(fileName) ? `[${fileName.join(', ')}]` : fileName; + return { + ok: true, + skipped: false, + message: `context.fileName = ${displayValue} (${configLevel})`, + }; + } + + // 字段存在但不包含目标文件 + if (MODE === 'fix') { + return fixGeminiConfig(configPath, config, configLevel); + } + + const displayValue = Array.isArray(fileName) ? `[${fileName.join(', ')}]` : fileName; + return { + ok: false, + skipped: false, + message: `${configLevel}配置的 context.fileName (${displayValue}) 未包含 ${TARGET_FILE}`, + }; + } + + // 所有级别都没有该字段,需要添加(fix 模式添加到优先级最高的配置) + if (MODE === 'fix') { + const targetConfig = configsToCheck[0]; + const content = fs.readFileSync(targetConfig.path, 'utf8'); + let config; + try { + config = JSON.parse(content); + } catch { + config = {}; + } + return fixGeminiConfig(targetConfig.path, config, targetConfig.level); + } + + return { + ok: false, + skipped: false, + message: '所有配置级别都缺少 context.fileName 字段', + }; +} + +function fixGeminiConfig(configPath, config, level) { + // 确保 context 对象存在 + if (!config.context) { + config.context = {}; + } + + const existingFileName = config.context.fileName; + + if (Array.isArray(existingFileName)) { + // 如果是数组,添加到数组中 + if (!existingFileName.includes(TARGET_FILE)) { + config.context.fileName = [...existingFileName, TARGET_FILE]; + } + } else if (typeof existingFileName === 'string' && existingFileName !== TARGET_FILE) { + // 如果是字符串且不是目标值,转为数组 + config.context.fileName = [existingFileName, TARGET_FILE]; + } else { + // 设置为目标值 + config.context.fileName = TARGET_FILE; + } + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8'); + return { + ok: true, + skipped: false, + message: `已修复${level}配置,设置 context.fileName 包含 ${TARGET_FILE}`, + }; +} + +// ==================== 工具函数 ==================== + +function parseMode(args) { + for (const arg of args) { + if (arg === '--fix' || arg === '--mode=fix') return 'fix'; + if (arg === '--check' || arg === '--mode=check') return 'check'; + } + return 'check'; +} + +/** + * 简单解析 TOML 数组值 + * 仅支持字符串数组,如: key = ["a", "b"] + */ +function parseTomlArray(content, key) { + const regex = new RegExp(`^${key}\\s*=\\s*\\[([^\\]]*)]`, 'm'); + const match = content.match(regex); + + if (!match) return null; + + const arrayContent = match[1].trim(); + if (!arrayContent) return []; + + // 解析数组元素 + const elements = []; + const elementRegex = /"([^"]*?)"/g; + let elementMatch; + while ((elementMatch = elementRegex.exec(arrayContent)) !== null) { + elements.push(elementMatch[1]); + } + + return elements; +} diff --git a/scripts/ensure-guidelines-sync.mjs b/scripts/ensure-guidelines-sync.mjs deleted file mode 100644 index 5a85bce..0000000 --- a/scripts/ensure-guidelines-sync.mjs +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env node - -import fs from 'node:fs'; -import path from 'node:path'; - -const MODE = parseMode(process.argv.slice(2)); -const FILES = ['AGENTS.md', 'CLAUDE.md']; - -const records = FILES.map(readGuidelineFile); -const existingRecords = records.filter((item) => item.exists); -const baseline = selectBaseline(existingRecords); - -if (!baseline) { - console.error('未找到基准文件,至少需要存在一个规范文档用于比对。'); - process.exit(1); -} - -const baselineBody = baseline.bodyNormalized; -const baselineRawBody = baseline.body; -let hasConflict = false; -let hasMissing = false; - -for (const record of records) { - if (!record.exists) { - hasMissing = true; - if (MODE === 'fix') { - writeFile(record.filePath, record.frontmatter, baselineRawBody); - console.log(`已补全缺失文件:${record.fileName}`); - } - continue; - } - - if (!bodiesMatch(baselineBody, record.bodyNormalized)) { - if (MODE === 'fix') { - writeFile(record.filePath, record.frontmatter, baselineRawBody); - console.log(`已同步正文:${record.fileName}`); - } else { - hasConflict = true; - reportConflict(record.fileName, baselineBody, record.bodyNormalized); - } - } -} - -if (hasConflict) { - process.exit(1); -} - -if (hasMissing && MODE === 'check') { - console.error('检测到规范文件缺失,请运行 npm run check:fix 同步。'); - process.exit(1); -} - -function parseMode(args) { - for (const arg of args) { - if (arg === '--fix' || arg === '--mode=fix') return 'fix'; - if (arg === '--check' || arg === '--mode=check') return 'check'; - } - return 'check'; -} - -function readGuidelineFile(fileName) { - const filePath = path.join(process.cwd(), fileName); - try { - const content = fs.readFileSync(filePath, 'utf8'); - const stats = fs.statSync(filePath); - const { frontmatter, body } = splitFrontmatter(content); - return { - fileName, - filePath, - exists: true, - frontmatter, - body, - bodyNormalized: normalizeBody(body), - mtimeMs: stats.mtimeMs, - }; - } catch (error) { - if (error.code === 'ENOENT') { - return { - fileName, - filePath, - exists: false, - frontmatter: '', - body: '', - bodyNormalized: '', - mtimeMs: 0, - }; - } - throw error; - } -} - -function selectBaseline(existing) { - if (existing.length === 0) return null; - return existing.reduce((latest, current) => { - if (!latest) return current; - if (current.mtimeMs > latest.mtimeMs) return current; - if (current.mtimeMs === latest.mtimeMs) { - return current.fileName < latest.fileName ? current : latest; - } - return latest; - }); -} - -function splitFrontmatter(content) { - if (content.startsWith('---')) { - const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/); - if (match) { - const frontmatter = match[0]; - const body = content.slice(frontmatter.length); - return { frontmatter, body }; - } - } - return { frontmatter: '', body: content }; -} - -function normalizeBody(body) { - return body.replace(/\r\n/g, '\n').trim(); -} - -function bodiesMatch(a, b) { - return a === b; -} - -function reportConflict(fileName, expected, actual) { - const [expectedLine, actualLine, lineNumber] = firstDifference(expected, actual); - console.error( - [ - `文档正文不一致:${fileName}`, - lineNumber - ? `首个差异行 ${lineNumber}:\n 基准> ${expectedLine}\n 当前> ${actualLine}` - : '无法定位差异(可能是空白差异),请手动同步。', - ].join('\n'), - ); -} - -function firstDifference(expected, actual) { - const expectedLines = expected.split('\n'); - const actualLines = actual.split('\n'); - const max = Math.max(expectedLines.length, actualLines.length); - for (let i = 0; i < max; i++) { - const exp = expectedLines[i] ?? ''; - const act = actualLines[i] ?? ''; - if (exp !== act) { - return [exp, act, i + 1]; - } - } - return ['', '', null]; -} - -function writeFile(filePath, frontmatter, body) { - const normalizedBody = normalizeBody(body); - const finalBody = normalizedBody ? `${normalizedBody}\n` : ''; - const hasFrontmatter = Boolean(frontmatter); - const needsNewline = hasFrontmatter && !frontmatter.endsWith('\n'); - const prefix = hasFrontmatter ? `${frontmatter}${needsNewline ? '\n' : ''}` : ''; - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, `${prefix}${finalBody}`, 'utf8'); -} diff --git a/scripts/run-checks.mjs b/scripts/run-checks.mjs index 9301e39..13233bb 100644 --- a/scripts/run-checks.mjs +++ b/scripts/run-checks.mjs @@ -11,8 +11,8 @@ const steps = mode === 'fix' ? [ { - name: 'AI 项目记忆文档同步修复', - command: ['node', 'scripts/ensure-guidelines-sync.mjs', '--mode=fix'], + name: 'AI Agent 配置修复', + command: ['node', 'scripts/ensure-agent-config.mjs', '--mode=fix'], }, { name: '前端 ESLint 修复', command: ['npm', 'run', 'lint:ts:fix'] }, { @@ -25,8 +25,8 @@ const steps = ] : [ { - name: 'AI 项目记忆文档同步检查', - command: ['node', 'scripts/ensure-guidelines-sync.mjs', '--mode=check'], + name: 'AI Agent 配置检查', + command: ['node', 'scripts/ensure-agent-config.mjs', '--mode=check'], }, { name: '前端 ESLint 检查', command: ['npm', 'run', 'lint:ts'] }, {