diff --git a/.blueprint/manifest.yaml b/.blueprint/manifest.yaml index 63b1216..61e5052 100644 --- a/.blueprint/manifest.yaml +++ b/.blueprint/manifest.yaml @@ -6,7 +6,7 @@ description: > 成为每个项目的贴身导师。炼境本身亦被纳入其中, 形成可自我进化的开发智能闭环。 iteration: 1 -updated: 2026-06-30 +updated: 2026-07-02 @@ -385,8 +385,17 @@ modules: area: backend status: done progress: 100 + updated: 2026-07-01 position: [3750, 0] + - id: github-decommission + name: GitHub 退役清理 + area: backend + status: done + progress: 100 + updated: 2026-07-02 + position: [3750, 150] + edges: # 前端依赖 - from: workspace-mgmt @@ -622,3 +631,6 @@ edges: - from: gitea-integration to: flywheel-intelligence type: related + - from: gitea-integration + to: github-decommission + type: related diff --git a/.blueprint/modules/agent-infrastructure.md b/.blueprint/modules/agent-infrastructure.md index 28006b0..e0e6870 100644 --- a/.blueprint/modules/agent-infrastructure.md +++ b/.blueprint/modules/agent-infrastructure.md @@ -118,3 +118,10 @@ - depends: C - files: src/components/dashboard/ProjectCard.tsx, src/lib/commands.ts - acceptance: ProjectCard 右上角显示腐化评分小圆点(🟢🟡🔴),读取 `doc_freshness_cache` 缓存(无缓存则不显示);不发起新的 git 扫描(仅读缓存) + +### ✅ F3. Agent 健康诊断 + 可复制修正指令 +- status: done +- complexity: M +- depends: C, D +- files: src-tauri/src/commands/agent_infra.rs, src-tauri/src/lib.rs, src/lib/commands.ts, src/components/dashboard/AgentInfraPanel.tsx +- acceptance: `get_agent_health(project_path, project_id)` 聚合 infra stack + doc freshness,生成问题列表 + Markdown 修正文本;AgentInfraPanel 加载时从 cache 读健康徽章,「重新检测 & 生成修正指令」按钮触发完整诊断,结果显示健康状态+可复制 textarea(含复制按钮) diff --git a/.blueprint/modules/gitea-integration.md b/.blueprint/modules/gitea-integration.md index f33edad..c1baee7 100644 --- a/.blueprint/modules/gitea-integration.md +++ b/.blueprint/modules/gitea-integration.md @@ -112,8 +112,8 @@ Gitea push → POST /webhook/gitea/:project_id - files: src-tauri/src/db.rs, src-tauri/src/commands/gitea.rs, src-tauri/src/lib.rs, src/lib/commands.ts - acceptance: gitea_instances + gitea_repos 表建好(migrate);save/list/delete/test_connection 命令可调;test_connection 调 GET /api/v1/user 返回 Gitea 用户名验证 token 有效性 -### 📋 G2. 仓库创建 / 迁移 / 绑定 -- status: todo +### 🔵 G2. 仓库创建 / 迁移 / 绑定 +- status: in_progress - complexity: M - depends: G1 - files: src-tauri/src/commands/gitea.rs, src/lib/commands.ts @@ -126,8 +126,8 @@ Gitea push → POST /webhook/gitea/:project_id - files: src-tauri/src/mcp_server.rs - acceptance: POST /webhook/gitea/:project_id 路由上线;HMAC-SHA256 验签通过后解析 commits 数组批量写 commit_metrics;验签失败记 log_event;单元测试覆盖 parse_push_payload + hmac_verify -### 📋 G4. Webhook 注册命令 -- status: todo +### 🔵 G4. Webhook 注册命令 +- status: in_progress - complexity: M - depends: G2, G3 - files: src-tauri/src/commands/gitea.rs, src/lib/commands.ts diff --git a/.blueprint/modules/github-auth.md b/.blueprint/modules/github-auth.md index 8c1bbbc..0ef8a99 100644 --- a/.blueprint/modules/github-auth.md +++ b/.blueprint/modules/github-auth.md @@ -5,7 +5,8 @@ status: deprecated progress: 100 --- -> ❄️ **已冻结(2026-06-19)**:全面统一自建 Gitea,弃用 GitHub。功能保留不删,但不再演进;新接入走 onboarding-pack / gitea-integration 模块。 +> ❄️ **已冻结(2026-06-19)**:全面统一自建 Gitea,弃用 GitHub。 +> 🗑️ **已退役(2026-07-02)**:代码全部移除(github.rs 删除、登录门槛拆除、OAuth 命令下线),详见 github-decommission 模块。本文件仅作历史记录。 ## 概述 GitHub OAuth Device Flow 认证,获取 token 后支持 API 操作和 Git 推送。 diff --git a/.blueprint/modules/github-decommission.md b/.blueprint/modules/github-decommission.md new file mode 100644 index 0000000..17febc7 --- /dev/null +++ b/.blueprint/modules/github-decommission.md @@ -0,0 +1,59 @@ +# GitHub 退役清理 + +围绕自建 Gitea 的战略确定后(见 gitea-integration),移除 GitHub 作为 git 后端的全部纠缠。 +排查发现纠缠比蓝图标记的深:GitHub OAuth 是应用登录门槛(App.tsx 无账号连界面都进不去), +spaces.json 每 5 分钟同步 GitHub 仓库,git_ops 所有凭据硬依赖 github_token。 + +## 边界(保留项) + +- `server_software.rs` 的 GitHub 镜像管理:用途是 Releases 下载加速,与 git 后端无关,保留 +- `gitea_migrate_repo` 对 GitHub 的引用:迁移功能需要读旧家,保留 +- `cicd.rs` 后端命令:保留,待 gitea-integration「Gitea Actions workflow 生成」卡改造 +- DB 表(github_mirrors、settings 旧 key 等):代码停用但表不删,避免旧版本升级 migration 断裂(R05) + +## 任务卡 + +### ✅ D1. 免登录改造(去掉 GitHub OAuth 登录门槛) +- status: done +- complexity: M +- files: src/App.tsx, src/components/auth/LoginPage.tsx(删), src/components/layout/Sidebar.tsx, src-tauri/src/commands/github.rs(删), src-tauri/src/commands/settings.rs, src-tauri/src/lib.rs, src/lib/commands.ts +- acceptance: 应用启动直接进主界面,无登录页;github.rs 及 8 个 OAuth 命令删除;Sidebar 账户区与「应用设置」GitHub OAuth 弹窗删除;健康检查移除 GitHub Token 项;GLOBAL_KEYS 移除 github_client_id/secret;typecheck + cargo test 通过 + +### ✅ D2. 移除 GitHub 发布 / 导入 / PR / CI 状态入口 +- status: done +- complexity: M +- files: src/components/dashboard/PublishModal.tsx(删), src/components/dashboard/ImportRepoModal.tsx(删), src/components/dashboard/ProjectCard.tsx, src/components/dashboard/RepoRegistryModal.tsx, src/components/dashboard/CicdModal.tsx, src/components/manage/ManagePage.tsx, src-tauri/src/commands/publish.rs, src/lib/commands.ts +- acceptance: 发布按钮 / 从 GitHub 导入 / PR compare 链接 / Actions 运行状态显示全部移除;publish.rs 仅删 github_create_repo / git_push_to_github / get_token,通用工具(WSL 路径、scan_local_git 等)保留;CicdModal 保留配置 tab、删 runs tab;typecheck 通过 + +### ✅ D3. 删除 spacesSync(单机化,跨空间 GitHub 云同步下线) +- status: done +- complexity: S +- files: src/lib/spacesSync.ts(删), src/hooks/useSpacesSync.ts(删), src/components/spaces/SpacesPage.tsx(删,已无引用的死代码), src/components/dashboard/Dashboard.tsx, src/components/dashboard/ProjectCard.tsx, src/components/dashboard/ProjectModal.tsx +- acceptance: spacesJson prop 链(App → Dashboard → ProjectCard)拆除;跨空间感知/接续 UI、可发现项目区块、push 后 catalog 上报全部移除;typecheck 通过 + +### ✅ D4. git_ops 凭据去 GitHub 化 +- status: done +- complexity: M +- files: src-tauri/src/commands/git_ops.rs +- acceptance: git_clone/fetch/pull/push/upstream_behind 不再硬依赖 settings.github_token;凭据链改为 git credential helper → gitea_instances token 前缀匹配 → ssh agent → default;无 token 时不再报「未登录 GitHub 账户」;cargo test 通过 + +## 决策记录 + +- 免登录而非换 Gitea 登录:炼境是单人本地工具,"登录"概念本身无收益;Gitea 身份已由 gitea_instances.access_token 承载,远端操作按需取凭据 +- spacesSync 直接删而非迁 Gitea:用户确认单机使用,跨空间感知无真实场景 +- git 凭据优先走 git credential helper:与用户 CLI 使用同一凭据源(Windows Credential Manager),GitHub/Gitea 通吃,炼境不再自管 git 凭据 + +## 复盘笔记 + +【复盘】GitHub 退役清理(D1-D4,2026-07-02) +任务类型: 架构调整 +复杂度: M×3 + S×1 +实际轮数: 1轮(预估 2 轮) +是否返工: 否 +有效策略: 先全量排查(grep 全代码库)再动手,发现蓝图未标记的两个深耦合(登录门槛、spacesSync 定时同步);按"前端入口 → 封装层 → 后端命令 → 凭据层"顺序拆除,每层完成即 typecheck +遗留风险: ① Gitea token 作 basic auth username 的凭据方式(userpass_plaintext(token, ""))未在真实 Gitea 实例上验证过 push;② CicdModal 仍生成 GitHub Actions 模板,待 gitea-integration 的 workflow 改造卡处理;③ 前端测试清零(parseRepoPath 测试随功能删除),vitest 配了 passWithNoTests +auto_applied_rules: R01, R05 +rule_effectiveness: R01=有效(删命令时同步删 commands.ts 封装,无断层), R05=有效(保留 DB 表不删避免 migration 断裂) + +【陷阱记录】servers.rs 存量测试 schema 漂移(顺手修复) +测试 setup 的 CREATE TABLE 手抄了 project_server_map 旧 schema,缺 deploy_type/container_name/start_command 三列,与 db.rs migrate() 不一致导致 2 个测试常红。已补齐。根因:测试建表语句是手抄副本而非复用 migrate(),schema 演进时必然漂移——后续新测试应从 db::migrate 复制对应段(CONVENTIONS 测试模板已有此要求)。 diff --git a/.blueprint/modules/github-publish.md b/.blueprint/modules/github-publish.md index 1ea1df5..2e8c240 100644 --- a/.blueprint/modules/github-publish.md +++ b/.blueprint/modules/github-publish.md @@ -1,6 +1,7 @@ # GitHub 发布 -> ❄️ **已冻结(2026-06-19)**:全面统一自建 Gitea,弃用 GitHub。功能保留不删,不再演进;项目发布走 Gitea(见 gitea-integration 模块)。 +> ❄️ **已冻结(2026-06-19)**:全面统一自建 Gitea,弃用 GitHub。 +> 🗑️ **已退役(2026-07-02)**:发布链路代码全部移除(PublishModal / ImportRepoModal / github_create_repo / git_push_to_github),详见 github-decommission 模块。本文件仅作历史记录。 在炼境内通过 GitHub OAuth 直接将本地项目推送到远程仓库,支持新建仓库或关联已有仓库,完成发布后自动回填 remote URL。 diff --git a/.blueprint/usage.json b/.blueprint/usage.json index b60a304..a96fb76 100644 --- a/.blueprint/usage.json +++ b/.blueprint/usage.json @@ -8102,6 +8102,222 @@ "todo": 1, "total": 182 } + }, + { + "at": "2026-07-01T00:19:51Z", + "blocked_reasons": [], + "conventions_version": "1.8.0", + "delta": { + "newly_blocked": [], + "newly_done": [ + "gitea-integration/G1. DB schema + Gitea 实例 CRUD" + ], + "unblocked": [] + }, + "iteration": 1, + "modules": { + "blocked": 0, + "concept": 0, + "done": 47, + "in_progress": 2, + "planned": 0, + "stalled": [ + "flywheel-intelligence" + ], + "total": 51 + }, + "task_ids": { + "blocked": [], + "done": [ + "agent-infrastructure/A. 框架层扫描 + 契约路径推断", + "agent-infrastructure/B. 三件套文件生成", + "agent-infrastructure/C. 文档腐化评分", + "agent-infrastructure/D. 栈确认 UI + 生成按钮", + "agent-infrastructure/E. 看板腐化指示器", + "api-keys/API 密钥 CRUD 与安全展示", + "auto-update/Capabilities 权限配置", + "auto-update/下载 & 安装", + "auto-update/更新检测", + "auto-update/自动检查更新", + "auto-update/错误提示", + "blueprint-buff/BlueprintView 跨项目切换", + "blueprint-buff/Buff 生命周期 Rust 命令", + "blueprint-buff/blueprint_buffs 数据表", + "blueprint-buff/git watcher + 任务匹配写回 + 飞轮快照", + "blueprint-buff/前端 Buff 管理 UI", + "blueprint-buff/前端实时刷新", + "blueprint-feedback/归档 command", + "blueprint-feedback/归档候选展示", + "blueprint-feedback/梦核分析入口(蓝图弹窗)", + "blueprint-feedback/梦核提示词增加停滞模块数据", + "blueprint-flywheel/get_flywheel_stats command", + "blueprint-flywheel/get_flywheel_stats 支持迭代感知", + "blueprint-flywheel/usage.json 埋点写入", + "blueprint-flywheel/梦核提示词生成 command", + "blueprint-flywheel/飞轮健康面板", + "blueprint-governance/CLAUDE.md 模板定义", + "blueprint-governance/CONVENTIONS.md 版本化", + "blueprint-governance/GovernancePanel 规则编辑器 UI", + "blueprint-governance/get_blueprint_status command", + "blueprint-governance/sync/梦核/summary 切换为 effective 内容", + "blueprint-governance/sync_blueprint_rules command", + "blueprint-governance/批量同步所有项目规则", + "blueprint-governance/用户版 CONVENTIONS 存储层初始化", + "blueprint-governance/用户版 CONVENTIONS 读写 command", + "blueprint-onboarding/generate_blueprint_prompt command", + "blueprint-onboarding/蓝图操作弹窗", + "blueprint-onboarding/项目列表蓝图状态徽章", + "blueprint-reader/合并返回完整蓝图数据", + "blueprint-reader/读取 manifest.yaml", + "blueprint-reader/读取 modules/*.md", + "blueprint-view/.blueprint/ 数据结构设计", + "blueprint-view/React Flow 节点图渲染", + "blueprint-view/blocked 状态支持", + "blueprint-view/下一步汇总面板", + "blueprint-view/任务卡复制派发", + "blueprint-view/批量执行提示词入口", + "blueprint-view/智能连线路由", + "blueprint-view/模块详情面板", + "blueprint-view/统计汇总条", + "blueprint-view/蓝图治理面板", + "blueprint-view/蓝图读取 Command", + "blueprint-view/边高亮与模块聚焦", + "blueprint-view/顶部状态行扩展", + "blueprint-view/项目卡片蓝图入口", + "cicd-workflow/Actions 运行历史", + "cicd-workflow/CI/CD 配置 CRUD", + "cicd-workflow/Tauri OSS 分发模板", + "cicd-workflow/Web SSH 部署模板", + "cicd-workflow/Workflow 文件生成", + "claude-autonomy-config/Beast 全局状态读取与展示", + "cloud-db-instances/云数据库实例信息管理", + "cloud-docker-apps/Docker 应用列表与状态管理", + "cloud-products/DB 迁移:cloud_products 表", + "cloud-products/Dashboard 接入:云产品标签页", + "cloud-products/Rust CRUD:cloud_products.rs", + "cloud-products/前端 Panel:CloudProductsPanel.tsx", + "cloud-products/接线:mod.rs + lib.rs + commands.ts", + "deploy-info/项目部署信息记录面板", + "devtools-scan/WSL 工具扫描", + "devtools-scan/Windows 工具扫描", + "devtools-scan/工具信息持久化", + "flywheel-intelligence/F1. Git 健康度后端——全量历史导入 + 跨项目聚合", + "flywheel-intelligence/F2. 飞轮面板——Git 健康度区块 + 全量导入按钮", + "flywheel-intelligence/MCP Server 支持 SSE 传输", + "flywheel-intelligence/复盘笔记覆盖率可见性", + "flywheel-intelligence/独立项目自动创建产品组", + "flywheel-intelligence/补测试:飞轮存量函数安全网", + "flywheel-intelligence/阶段一:复盘笔记接入梦核飞轮", + "flywheel-intelligence/阶段二:usage.json 扩展采集维度", + "git-ops/上游状态检测", + "git-ops/分支管理", + "git-ops/基础操作", + "gitea-integration/G1. DB schema + Gitea 实例 CRUD", + "github-auth/OAuth Device Flow", + "github-auth/Token 管理", + "github-publish/GitHub 仓库创建与推送", + "health-center/T1 · Backend 健康检查聚合命令", + "health-center/T2 · 质量指标聚合", + "health-center/T3 · 前端健康中心页面", + "health-center/T4 · 扩展 MCP get_diagnostics", + "host-apps/宿主机应用清单与快启", + "mcp-config-inject/加入产品组时注入配置", + "mcp-config-inject/端口变更时重新注入所有配置", + "mcp-config-inject/退出产品组时清除配置", + "mcp-server/MCP 端口可配置(Rust 侧)", + "mcp-server/启动时绑定 HTTP 端口", + "mcp-server/实现 MCP JSON-RPC 基础协议", + "mcp-server/实现产品组上下文工具", + "onboarding-pack/A 模板分发引擎", + "onboarding-pack/B apply_onboarding_pack 编排 + 前端入口", + "onboarding-pack/C1 commit_metrics 表 + Conventional 解析器", + "onboarding-pack/C2 挂接 buff watcher + 飞轮快照", + "onboarding-pack/D 数据可信度提示", + "product-group-mcp/产品组详情显示 MCP 注入状态", + "product-group-mcp/设置页添加 MCP 端口配置", + "project-dashboard/分组画布", + "project-dashboard/项目卡片列表", + "project-dashboard/项目发现面板", + "project-dashboard/项目快捷操作", + "project-env-scan/环境信息持久化", + "project-env-scan/项目环境检测", + "project-mentor/T1 · 后端聚合 command(MentorContext)", + "project-mentor/T10 · 启动健康扫描 + 前端 Alert Banner", + "project-mentor/T11 · 项目历史快照", + "project-mentor/T2 · MCP 项目导师查询接口", + "project-mentor/T3 · 前端导师面板", + "project-mentor/T4 · 炼境自指接入", + "project-mentor/T5 · project_notes 数据层 + 后端命令", + "project-mentor/T6 · MCP append_project_note 工具", + "project-mentor/T7 · MentorPanel 展示 AI 笔记", + "project-mentor/T8 · 后端全组巡检命令", + "project-mentor/T9 · 前端全组巡检按钮", + "project-mgmt/子文件夹扫描", + "project-mgmt/批量导入项目", + "project-mgmt/项目 CRUD", + "project-mgmt/项目档案(Profile)", + "project-templates/P1+P3 模板扩充(React 生态全覆盖)", + "project-templates/从模板创建项目", + "project-templates/内置模板种植", + "project-templates/模板 CRUD", + "project-tools/工具 CRUD", + "project-tools/工具启动", + "repo-registry/仓库导入", + "repo-registry/仓库挂载", + "repo-registry/仓库注册表管理", + "runtime-diagnostics/T1 · SQLite runtime_logs 表", + "runtime-diagnostics/T2 · 替换 eprintln! 为结构化日志", + "runtime-diagnostics/T3 · MCP Server 新增 get_diagnostics 工具", + "runtime-diagnostics/T4 · 注入失败结果返回前端", + "runtime-diagnostics/T5 · 设置页 MCP 真实健康检查", + "server-registry/服务器 CRUD 与项目关联", + "server-software/服务器软件扫描与展示", + "service-config/服务配置 CRUD 与环境分组", + "settings-page/WSL 发行版选择", + "settings-page/关于 & 更新", + "settings-page/终端配置", + "settings-page/编辑器检测", + "shell-launch/打开终端", + "shell-launch/打开编辑器", + "source-library/源码库分类管理与项目详情", + "sqlite-db/数据库迁移", + "sqlite-db/连接池初始化", + "ssh-config-sync/SSH Config 写入与同步", + "tags-groups/分组管理", + "tags-groups/标签管理", + "tray-service/关闭窗口最小化到托盘", + "tray-service/系统托盘图标与菜单", + "tray-service/集成 tauri-plugin-single-instance", + "user-project-todos/前端:备忘清单 UI 组件", + "user-project-todos/数据层:user_project_todos 表 + Rust commands", + "v2rayn-import/当前节点读取", + "v2rayn-import/数据库路径检测", + "v2rayn-import/节点列表扫描", + "vscode-remote/VS Code Remote SSH 一键连接", + "workspace-mgmt/空间 CRUD", + "workspace-mgmt/空间级设置", + "workspace-mgmt/部署命令管理", + "wsl-proxy/代理一致性测试", + "wsl-proxy/代理同步", + "wsl-proxy/代理清除", + "wsl-proxy/代理状态检测" + ] + }, + "tasks": { + "avg_rounds_M": null, + "blocked": 0, + "complexity_dist": { + "L": 6, + "M": 72, + "S": 49 + }, + "dispatched": 0, + "done": 172, + "in_progress": 2, + "rework_count": 23, + "todo": 4, + "total": 188 + } } ] } \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 9bde7af..cd3fbf3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,7 +46,7 @@ docs/templates/ 炼境接入包模板草案(agents-meta-rules - 代码重构/简化:/simplify → /code-review - 发现深层问题:/Dev-deep-think - + ## 项目蓝图 本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。 @@ -55,7 +55,6 @@ docs/templates/ 炼境接入包模板草案(agents-meta-rules 1. 读取 `.blueprint/manifest.yaml` 了解项目全景和当前状态 2. 读取 `.blueprint/CONVENTIONS.md` 了解完整工作流规则 -3. 若 `.blueprint/rules.md` 存在,读取并与全局规则合并应用(项目私有规则,优先级高于全局) ### 需求讨论 → 蓝图同步规则 @@ -63,7 +62,7 @@ docs/templates/ 炼境接入包模板草案(agents-meta-rules |----------|--------------| | "我想加个 XX 功能" | 确认需求后:manifest 新增模块(status: concept),创建 `.blueprint/modules/.md` | | "XX 功能需要 A、B、C" | 在模块文件中拆分为任务卡 | -| "这个方案确定了" | 模块 status → planned,补充任务卡 acceptance(files 可选) | +| "这个方案确定了" | 模块 status → planned,补充任务卡 files/acceptance | | "开始做 XX" | 模块 status → in_progress | | 没提蓝图但在讨论功能 | 主动提议更新蓝图 | @@ -71,7 +70,7 @@ docs/templates/ 炼境接入包模板草案(agents-meta-rules | complexity | 执行流程 | |-----------|---------| -| S / M | 直接拆任务卡,确保每张卡有 `acceptance` + `complexity` → 标记 📋(`files` 可选) | +| S / M | 直接拆任务卡,确保每张卡有 `files` + `acceptance` + `complexity` → 标记 📋 | | L(复杂系统设计) | 先 `/architect` 输出设计文档,用户确认后 `/splitter` 拆成 S/M 任务卡 | > L 级任务禁止直接写代码,必须先经过 `/architect` → `/splitter` 流程。 diff --git a/docs/ai-context/project-context.yaml b/docs/ai-context/project-context.yaml index 4088f68..67c0391 100644 --- a/docs/ai-context/project-context.yaml +++ b/docs/ai-context/project-context.yaml @@ -1,35 +1,21 @@ -# 炼境契约路径映射 -# 炼境自身版本,手工维护(等 agent-infrastructure 模块完成后由炼境自动生成) -# 供 AGENTS.md 元规则 T1 查询:「哪个文件是这个项目的契约?」 +# 项目契约路径映射 +# 由炼境根据技术栈扫描生成,技术栈变更后重新运行接入包生成 +# 供 AGENTS.md 元规则 T1 查询:「哪个文件是这个项目的外部契约?」 contracts: + - label: API 接口(Axum) + paths: + - "src-tauri/src/mcp_server.rs" + doc: "路由变更需同步 API 文档" + - label: Tauri command 接口(前后端契约) paths: - "src-tauri/src/commands/**/*.rs" - "src/lib/commands.ts" - doc: "新增 command 时两端必须同步;src/lib/commands.ts 是唯一前端调用入口" - - - label: 数据库 schema - paths: - - "src-tauri/src/db.rs" - doc: "SQLite 表结构变更必须在 migrate() 函数中补 migration 语句" - - - label: MCP Server 接口 - paths: - - "src-tauri/src/mcp_server.rs" - - "src-tauri/src/mcp_inject.rs" - doc: "MCP 接口变更影响 Claude Code 对炼境的调用,接口路由和参数需同步" - - - label: 接入包分发模板 - paths: - - "src-tauri/resources/onboarding/**" - doc: "模板变更需同步检查 docs/templates/agents-meta-rules.md(设计层来源)" - + doc: "新增 command 时前后端必须同步" architecture_docs: - "AGENTS.md" - - "CLAUDE.md" - - ".blueprint/manifest.yaml" agent_docs: - "AGENTS.md" diff --git a/src-tauri/src/commands/agent_infra.rs b/src-tauri/src/commands/agent_infra.rs index 4d8f8b6..7e1b68b 100644 --- a/src-tauri/src/commands/agent_infra.rs +++ b/src-tauri/src/commands/agent_infra.rs @@ -781,6 +781,172 @@ pub struct CachedFreshness { pub checked_at: String, } +// ── Agent 健康诊断 ───────────────────────────────────────────────────────────── + +#[derive(Debug, Serialize)] +pub struct AgentHealthIssue { + pub level: String, // "error" | "warn" + pub title: String, + pub fix: String, +} + +#[derive(Debug, Serialize)] +pub struct AgentHealthReport { + pub overall: String, // "green" | "yellow" | "red" + pub issues: Vec, + /// 可直接复制给子项目 agent 的修正指令文本(Markdown) + pub fix_prompt: String, + pub checked_at: String, +} + +/// 诊断项目 agent 友好度,生成可复制的修正指令。 +/// 内部调用 scan_agent_infra_stack + scan_doc_freshness,同时刷新 doc_freshness_cache。 +#[tauri::command] +pub fn get_agent_health( + project_path: String, + project_id: String, +) -> Result { + let root = Path::new(project_path.trim_end_matches(['/', '\\'])); + if !root.exists() { + return Err(format!("目录不存在: {}", root.display())); + } + + let stack = scan_agent_infra_stack(project_path.clone())?; + let freshness = scan_doc_freshness(project_path.clone(), project_id.clone())?; + + let mut issues: Vec = Vec::new(); + let mut overall = "green".to_string(); + + if !stack.context_yaml_exists { + issues.push(AgentHealthIssue { + level: "error".into(), + title: "缺少 docs/ai-context/project-context.yaml".into(), + fix: "在炼境「Agent 基础设施」面板点击「扫描技术栈」→「确认并生成三件套」".into(), + }); + overall = "red".to_string(); + } + + if !stack.meta_rules_present { + issues.push(AgentHealthIssue { + level: "warn".into(), + title: "AGENTS.md 缺少「文档防腐元规则」段落".into(), + fix: "在炼境「Agent 基础设施」面板生成三件套,或手动在 AGENTS.md 末尾添加 ## 文档防腐元规则 章节".into(), + }); + if overall != "red" { + overall = "yellow".to_string(); + } + } + + match freshness.level.as_str() { + "red" if !stack.context_yaml_exists => { + // yaml 不存在导致的 red 已在上面记录,不重复 + } + "red" => { + let stale_count = freshness.details.iter().filter(|d| d.level == "red").count(); + issues.push(AgentHealthIssue { + level: "error".into(), + title: format!("文档严重腐化:{stale_count} 个契约文件更新超 30 天未同步 project-context.yaml"), + fix: "检查并更新 docs/ai-context/project-context.yaml 的契约描述,或重新生成三件套".into(), + }); + overall = "red".to_string(); + } + "yellow" => { + let stale_count = freshness.details.iter() + .filter(|d| d.level == "yellow" || d.level == "red") + .count(); + issues.push(AgentHealthIssue { + level: "warn".into(), + title: format!("文档腐化风险:{stale_count} 个契约文件更新后 14-30 天未同步"), + fix: "检查并更新 docs/ai-context/project-context.yaml 中的契约描述".into(), + }); + if overall != "red" { + overall = "yellow".to_string(); + } + } + _ => {} + } + + let proj_name = root + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("未知项目"); + let fix_prompt = build_fix_prompt(proj_name, &overall, &issues, &stack, &freshness); + + Ok(AgentHealthReport { + overall, + issues, + fix_prompt, + checked_at: freshness.checked_at, + }) +} + +fn build_fix_prompt( + project_name: &str, + overall: &str, + issues: &[AgentHealthIssue], + stack: &AgentInfraStack, + freshness: &FreshnessResult, +) -> String { + let status_emoji = match overall { + "red" => "🔴", + "yellow" => "🟡", + _ => "🟢", + }; + let status_label = match overall { + "red" => "需要修复", + "yellow" => "存在风险", + _ => "良好", + }; + let date_label = freshness.checked_at.get(..10).unwrap_or(&freshness.checked_at); + + let mut lines = vec![ + format!("# Agent 友好度诊断 — {project_name}"), + format!("整体状态:{status_emoji} {status_label}(检测时间:{date_label})"), + String::new(), + ]; + + if issues.is_empty() { + lines.push("✅ 所有检查通过,项目 agent 友好度良好,无需修复。".into()); + } else { + lines.push("## 待修复项".into()); + lines.push(String::new()); + for issue in issues { + let icon = if issue.level == "error" { "❌" } else { "⚠️" }; + lines.push(format!("{icon} **{}**", issue.title)); + lines.push(format!(" 修复方法:{}", issue.fix)); + lines.push(String::new()); + } + } + + let mut passed = Vec::new(); + if stack.context_yaml_exists { + passed.push("docs/ai-context/project-context.yaml 已存在".to_string()); + } + if stack.meta_rules_present { + passed.push("AGENTS.md 已包含文档防腐元规则".to_string()); + } + if !stack.detected_contracts.is_empty() && freshness.level == "green" { + passed.push(format!( + "{} 个契约路径均在 14 天内同步", + stack.detected_contracts.len() + )); + } + + if !passed.is_empty() { + lines.push("## 已通过项".into()); + lines.push(String::new()); + for p in &passed { + lines.push(format!("✅ {p}")); + } + lines.push(String::new()); + } + + lines.push("---".into()); + lines.push("*由炼境 dev-manager 自动生成。请根据上述修复项逐项处理,完成后在炼境中点击「重新检测」刷新状态。*".into()); + + lines.join("\n") +} + // ── 单元测试 ────────────────────────────────────────────────────────────────── #[cfg(test)] diff --git a/src-tauri/src/commands/git_ops.rs b/src-tauri/src/commands/git_ops.rs index 6b2c606..be42987 100644 --- a/src-tauri/src/commands/git_ops.rs +++ b/src-tauri/src/commands/git_ops.rs @@ -12,39 +12,60 @@ pub struct GitStatus { pub last_commit_time: Option, } - -fn get_token() -> Result { - let conn = db::pool().get().map_err(|e| e.to_string())?; - conn.query_row( - "SELECT value FROM settings WHERE key = 'github_token'", - [], - |row| row.get::<_, String>(0), - ) - .map_err(|_| "未登录 GitHub 账户".to_string()) +/// 按 URL 前缀匹配已注册的 Gitea 实例,返回其 access_token +fn gitea_token_for_url(url: &str) -> Option { + let conn = db::pool().get().ok()?; + let mut stmt = conn.prepare("SELECT url, access_token FROM gitea_instances").ok()?; + let rows = stmt + .query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))) + .ok()?; + for row in rows.flatten() { + let (base, token) = row; + let base = base.trim_end_matches('/'); + if !base.is_empty() && url.starts_with(base) { + return Some(token); + } + } + None } -fn make_fetch_options(token: &str) -> FetchOptions<'static> { - let token = token.to_string(); +/// 凭据链:ssh agent → git credential helper(与 CLI 同一凭据源)→ Gitea 实例 token → default +/// 不再依赖 GitHub 登录,任何 remote(Gitea / GitHub / 其他)走系统已有凭据 +fn make_callbacks() -> RemoteCallbacks<'static> { let mut callbacks = RemoteCallbacks::new(); - callbacks.credentials(move |_url, _username, _allowed| { - git2::Cred::userpass_plaintext(&token, "x-oauth-basic") + callbacks.credentials(|url, username_from_url, allowed| { + if allowed.contains(git2::CredentialType::SSH_KEY) { + if let Ok(cred) = git2::Cred::ssh_key_from_agent(username_from_url.unwrap_or("git")) { + return Ok(cred); + } + } + if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) { + if let Ok(config) = git2::Config::open_default() { + if let Ok(cred) = git2::Cred::credential_helper(&config, url, username_from_url) { + return Ok(cred); + } + } + if let Some(token) = gitea_token_for_url(url) { + if let Ok(cred) = git2::Cred::userpass_plaintext(&token, "") { + return Ok(cred); + } + } + } + git2::Cred::default() }); + callbacks +} + +fn make_fetch_options() -> FetchOptions<'static> { let mut fetch_opts = FetchOptions::new(); - fetch_opts.remote_callbacks(callbacks); + fetch_opts.remote_callbacks(make_callbacks()); fetch_opts } /// clone 仓库到指定本地路径 #[tauri::command] pub fn git_clone(repo_url: String, local_path: String, upstream_url: Option) -> Result<(), String> { - let token = get_token()?; - let token_clone = token.clone(); - let mut callbacks = RemoteCallbacks::new(); - callbacks.credentials(move |_url, _username, _allowed| { - git2::Cred::userpass_plaintext(&token_clone, "x-oauth-basic") - }); - let mut fetch_opts = FetchOptions::new(); - fetch_opts.remote_callbacks(callbacks); + let fetch_opts = make_fetch_options(); let repo = RepoBuilder::new() .fetch_options(fetch_opts) @@ -111,11 +132,10 @@ pub fn git_status(local_path: String) -> Result { /// fetch 远端最新信息(不修改本地工作区) #[tauri::command] pub fn git_fetch(local_path: String) -> Result<(), String> { - let token = get_token()?; let repo = Repository::open(&local_path) .map_err(|e| format!("无法打开仓库: {}", e))?; - let mut fetch_opts = make_fetch_options(&token); + let mut fetch_opts = make_fetch_options(); repo.find_remote("origin") .map_err(|e| e.to_string())? .fetch(&[] as &[&str], Some(&mut fetch_opts), None) @@ -127,12 +147,11 @@ pub fn git_fetch(local_path: String) -> Result<(), String> { /// pull(fetch + merge fast-forward) #[tauri::command] pub fn git_pull(local_path: String) -> Result { - let token = get_token()?; let repo = Repository::open(&local_path) .map_err(|e| format!("无法打开仓库: {}", e))?; // fetch - let mut fetch_opts = make_fetch_options(&token); + let mut fetch_opts = make_fetch_options(); let mut remote = repo.find_remote("origin").map_err(|e| e.to_string())?; remote .fetch(&[] as &[&str], Some(&mut fetch_opts), None) @@ -194,7 +213,6 @@ pub fn git_head_commit(local_path: String) -> Result, String> { /// 检查 upstream 远端落后情况(公司仓库是否有新提交) #[tauri::command] pub fn git_upstream_behind(local_path: String, default_branch: String) -> Result { - let token = get_token()?; let repo = Repository::open(&local_path) .map_err(|e| format!("无法打开仓库: {}", e))?; @@ -204,13 +222,7 @@ pub fn git_upstream_behind(local_path: String, default_branch: String) -> Result } // fetch upstream - let mut callbacks = RemoteCallbacks::new(); - let token_clone = token.clone(); - callbacks.credentials(move |_url, _username, _allowed| { - git2::Cred::userpass_plaintext(&token_clone, "x-oauth-basic") - }); - let mut fetch_opts = FetchOptions::new(); - fetch_opts.remote_callbacks(callbacks); + let mut fetch_opts = make_fetch_options(); if let Ok(mut remote) = repo.find_remote("upstream") { let _ = remote.fetch(&[&default_branch], Some(&mut fetch_opts), None); @@ -236,7 +248,6 @@ pub fn git_upstream_behind(local_path: String, default_branch: String) -> Result /// push 当前分支到 origin #[tauri::command] pub fn git_push(local_path: String) -> Result { - let token = get_token()?; let repo = Repository::open(&local_path) .map_err(|e| format!("无法打开仓库: {}", e))?; @@ -248,14 +259,8 @@ pub fn git_push(local_path: String) -> Result { let refspec = format!("refs/heads/{}:refs/heads/{}", branch, branch); - let mut callbacks = RemoteCallbacks::new(); - let token_clone = token.clone(); - callbacks.credentials(move |_url, _username, _allowed| { - git2::Cred::userpass_plaintext(&token_clone, "x-oauth-basic") - }); - let mut push_opts = git2::PushOptions::new(); - push_opts.remote_callbacks(callbacks); + push_opts.remote_callbacks(make_callbacks()); repo.find_remote("origin") .map_err(|e| e.to_string())? diff --git a/src-tauri/src/commands/github.rs b/src-tauri/src/commands/github.rs deleted file mode 100644 index 170cda1..0000000 --- a/src-tauri/src/commands/github.rs +++ /dev/null @@ -1,359 +0,0 @@ -use crate::db; -use crate::models::GitAccount; -use once_cell::sync::Lazy; -use std::io::{Read, Write}; -use std::net::TcpListener; -use std::sync::{Arc, Mutex}; -use uuid::Uuid; - -// ── OAuth Authorization Code Flow 共享状态 ──────────────────────────────────── - -static OAUTH_RESULT: Lazy>>>> = - Lazy::new(|| Arc::new(Mutex::new(None))); - -fn read_setting(key: &str) -> String { - let conn = match db::pool().get() { - Ok(c) => c, - Err(_) => return String::new(), - }; - conn.query_row( - "SELECT value FROM settings WHERE key = ?1", - rusqlite::params![key], - |r| r.get::<_, String>(0), - ) - .unwrap_or_default() -} - -/// URL 编码(不依赖外部 crate) -fn url_encode(s: &str) -> String { - let mut out = String::new(); - for byte in s.as_bytes() { - match byte { - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' - | b'-' | b'_' | b'.' | b'~' => out.push(*byte as char), - b => out.push_str(&format!("%{:02X}", b)), - } - } - out -} - -/// 用 ureq 把 code 换成 access_token -fn exchange_code(client_id: &str, client_secret: &str, code: &str, port: u16) - -> Result -{ - let redirect_uri = format!("http://localhost:{}/callback", port); - let body = format!( - "client_id={}&client_secret={}&code={}&redirect_uri={}", - url_encode(client_id), - url_encode(client_secret), - url_encode(code), - url_encode(&redirect_uri), - ); - - let resp = ureq::post("https://github.com/login/oauth/access_token") - .set("Accept", "application/json") - .set("Content-Type", "application/x-www-form-urlencoded") - .send_string(&body) - .map_err(|e| format!("网络请求失败: {e}"))?; - - let json: serde_json::Value = resp.into_json() - .map_err(|e| format!("解析响应失败: {e}"))?; - - if let Some(token) = json["access_token"].as_str() { - return Ok(token.to_string()); - } - Err(json["error_description"] - .as_str() - .or_else(|| json["error"].as_str()) - .unwrap_or("token 交换失败") - .to_string()) -} - -/// 启动 Authorization Code Flow。 -/// 绑定随机端口,后台等待浏览器回调,返回 GitHub 授权 URL。 -#[tauri::command] -pub fn github_start_oauth() -> Result { - let client_id = read_setting("github_client_id"); - let client_secret = read_setting("github_client_secret"); - - if client_id.trim().is_empty() { - return Err("未配置 GitHub Client ID,请在设置中填写".to_string()); - } - if client_secret.trim().is_empty() { - return Err("未配置 GitHub Client Secret,请在设置中填写".to_string()); - } - - // 绑定随机可用端口 - let listener = TcpListener::bind("127.0.0.1:0") - .map_err(|e| format!("无法绑定本地端口: {e}"))?; - let port = listener.local_addr().unwrap().port(); - - // 重置上一次结果 - *OAUTH_RESULT.lock().unwrap() = None; - - let client_id_c = client_id.clone(); - let client_secret_c = client_secret.clone(); - - std::thread::spawn(move || { - if let Ok((mut stream, _)) = listener.accept() { - let mut buf = vec![0u8; 8192]; - let n = stream.read(&mut buf).unwrap_or(0); - let request = String::from_utf8_lossy(&buf[..n]).to_string(); - - // 从 "GET /callback?code=xxx HTTP/1.1" 提取 code - let code = request.lines().next().and_then(|line| { - let path = line.split_whitespace().nth(1)?; - let query = path.split('?').nth(1)?; - query - .split('&') - .find(|seg| seg.starts_with("code=")) - .map(|seg| seg[5..].to_string()) - }); - - let (status, html, result) = match code { - Some(code) => match exchange_code(&client_id_c, &client_secret_c, &code, port) { - Ok(token) => ( - "200 OK", - "

✅ 授权成功

可以关闭此标签页,返回应用继续操作。

", - Ok(token), - ), - Err(e) => ( - "400 Bad Request", - "

❌ 授权失败

请返回应用重试。

", - Err(e), - ), - }, - None => ( - "400 Bad Request", - "

❌ 未收到授权码

", - Err("GitHub 未返回授权码".to_string()), - ), - }; - - let response = format!( - "HTTP/1.1 {status}\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{html}", - html.len() - ); - let _ = stream.write_all(response.as_bytes()); - *OAUTH_RESULT.lock().unwrap() = Some(result); - } - }); - - let redirect_uri = format!("http://localhost:{}/callback", port); - Ok(format!( - "https://github.com/login/oauth/authorize?client_id={}&redirect_uri={}&scope=repo+user&allow_signup=false", - url_encode(&client_id), - url_encode(&redirect_uri), - )) -} - -/// 前端轮询:None = 等待中,Some(token) = 成功,Err = 失败 -#[tauri::command] -pub fn github_poll_oauth() -> Result, String> { - match &*OAUTH_RESULT.lock().unwrap() { - None => Ok(None), - Some(Ok(token)) => Ok(Some(token.clone())), - Some(Err(e)) => Err(e.clone()), - } -} - -/// 检测 github.com:443 是否可达(3 秒超时) -fn check_github_reachable() -> bool { - use std::net::{TcpStream, ToSocketAddrs}; - use std::time::Duration; - let Ok(addrs) = "github.com:443".to_socket_addrs() else { - return false; - }; - addrs - .into_iter() - .any(|addr| TcpStream::connect_timeout(&addr, Duration::from_secs(3)).is_ok()) -} - -/// 用系统默认浏览器打开 GitHub 授权页,TcpListener 接收回调(Authorization Code Flow) -/// 使用系统浏览器避免内嵌 WebView2 的代理/渲染问题 -#[tauri::command] -pub fn github_open_login_window(_app: tauri::AppHandle) -> Result<(), String> { - if !check_github_reachable() { - return Err("无法连接 GitHub(github.com:443 超时),请先确认代理已开启".to_string()); - } - - let client_id = read_setting("github_client_id"); - let client_secret = read_setting("github_client_secret"); - - if client_id.trim().is_empty() { - return Err("未配置 GitHub Client ID,请在设置中填写".to_string()); - } - if client_secret.trim().is_empty() { - return Err("未配置 GitHub Client Secret,请在设置中填写".to_string()); - } - - *OAUTH_RESULT.lock().unwrap() = None; - - // 绑定随机本地端口接收 GitHub 回调 - let listener = TcpListener::bind("127.0.0.1:0") - .map_err(|e| format!("无法绑定本地端口: {e}"))?; - let port = listener.local_addr().unwrap().port(); - - let redirect_uri = format!("http://localhost:{}/callback", port); - let auth_url = format!( - "https://github.com/login/oauth/authorize?client_id={}&redirect_uri={}&scope=repo+user&allow_signup=false", - url_encode(&client_id), - url_encode(&redirect_uri), - ); - - // 后台线程:等待 GitHub 回调,换取 token - std::thread::spawn(move || { - if let Ok((mut stream, _)) = listener.accept() { - let mut buf = vec![0u8; 8192]; - let n = stream.read(&mut buf).unwrap_or(0); - let request = String::from_utf8_lossy(&buf[..n]).to_string(); - - let code = request.lines().next().and_then(|line| { - let path = line.split_whitespace().nth(1)?; - let query = path.split('?').nth(1)?; - query - .split('&') - .find(|s| s.starts_with("code=")) - .map(|s| s[5..].to_string()) - }); - - let (status, html, result) = match code { - Some(code) => match exchange_code(&client_id, &client_secret, &code, port) { - Ok(token) => ( - "200 OK", - "

✅ 授权成功

可以关闭此标签页,返回应用继续操作。

", - Ok(token), - ), - Err(e) => ( - "400 Bad Request", - "

❌ 授权失败

请返回应用重试。

", - Err(e), - ), - }, - None => ( - "400 Bad Request", - "

❌ 未收到授权码

", - Err("GitHub 未返回授权码".to_string()), - ), - }; - - let response = format!( - "HTTP/1.1 {status}\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{html}", - html.len() - ); - let _ = stream.write_all(response.as_bytes()); - *OAUTH_RESULT.lock().unwrap() = Some(result); - } - }); - - // 用系统默认浏览器打开(走系统代理,不受 WebView2 限制) - open::that(&auth_url).map_err(|e| format!("无法打开浏览器: {e}"))?; - - Ok(()) -} - -/// 保存 GitHub 账户信息和 token(登录成功后由前端调用) -#[tauri::command] -pub fn github_save_account( - username: String, - avatar_url: String, - access_token: String, -) -> Result { - let conn = db::pool().get().map_err(|e| e.to_string())?; - - // 已存在则复用同一个 id - let existing_id: Option = conn - .query_row( - "SELECT id FROM git_accounts WHERE username = ?1", - rusqlite::params![username], - |row| row.get(0), - ) - .ok(); - - let id = existing_id.unwrap_or_else(|| Uuid::new_v4().to_string()); - - conn.execute( - "INSERT INTO git_accounts (id, username, avatar_url, provider) - VALUES (?1, ?2, ?3, 'github') - ON CONFLICT(username) DO UPDATE SET avatar_url = excluded.avatar_url", - rusqlite::params![id, username, avatar_url], - ) - .map_err(|e| e.to_string())?; - - // token 存入 settings 表 - conn.execute( - "INSERT OR REPLACE INTO settings (key, value) VALUES ('github_token', ?1)", - rusqlite::params![access_token], - ) - .map_err(|e| e.to_string())?; - - let account = conn - .query_row( - "SELECT id, username, avatar_url, provider, created_at FROM git_accounts WHERE id = ?1", - rusqlite::params![id], - GitAccount::from_row, - ) - .map_err(|e| e.to_string())?; - - Ok(account) -} - -/// 获取当前已登录账户(无则返回 None) -#[tauri::command] -pub fn github_get_account() -> Result, String> { - let conn = db::pool().get().map_err(|e| e.to_string())?; - match conn.query_row( - "SELECT id, username, avatar_url, provider, created_at FROM git_accounts LIMIT 1", - [], - GitAccount::from_row, - ) { - Ok(account) => Ok(Some(account)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(e.to_string()), - } -} - -/// 获取已存储的 access_token(前端调用 GitHub API 时使用) -#[tauri::command] -pub fn github_get_token() -> Result, String> { - let conn = db::pool().get().map_err(|e| e.to_string())?; - match conn.query_row( - "SELECT value FROM settings WHERE key = 'github_token'", - [], - |row| row.get::<_, String>(0), - ) { - Ok(token) if !token.is_empty() => Ok(Some(token)), - _ => Ok(None), - } -} - -/// 获取 GitHub Client ID(前端发起 Device Flow 时使用) -#[tauri::command] -pub fn github_get_client_id() -> Result { - let conn = db::pool().get().map_err(|e| e.to_string())?; - let value: String = conn - .query_row( - "SELECT value FROM settings WHERE key = 'github_client_id'", - [], - |row| row.get(0), - ) - .unwrap_or_default(); - if value.trim().is_empty() { - return Err("未配置 GitHub Client ID,请在「设置」页面填写后再登录".to_string()); - } - Ok(value) -} - -/// 登出:清除账户和 token -#[tauri::command] -pub fn github_logout() -> Result<(), String> { - let conn = db::pool().get().map_err(|e| e.to_string())?; - conn.execute("DELETE FROM git_accounts", []) - .map_err(|e| e.to_string())?; - conn.execute( - "DELETE FROM settings WHERE key = 'github_token'", - [], - ) - .map_err(|e| e.to_string())?; - Ok(()) -} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index a2d5ad7..2466c08 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -8,7 +8,6 @@ pub mod projects; pub mod settings; pub mod shell; pub mod spaces; -pub mod github; pub mod git_ops; pub mod proxy; pub mod git_repos; diff --git a/src-tauri/src/commands/publish.rs b/src-tauri/src/commands/publish.rs index 63b0db8..8c170e4 100644 --- a/src-tauri/src/commands/publish.rs +++ b/src-tauri/src/commands/publish.rs @@ -118,16 +118,6 @@ pub struct GitScanResult { // ── 内部工具 ───────────────────────────────────────────────────────────────── -fn get_token() -> Result { - let conn = db::pool().get().map_err(|e| e.to_string())?; - conn.query_row( - "SELECT value FROM settings WHERE key = 'github_token'", - [], - |row| row.get::<_, String>(0), - ) - .map_err(|_| "未登录 GitHub 账户".to_string()) -} - /// 检测疑似敏感文件(只扫一层 + 常见子目录,避免遍历 node_modules) fn scan_suspicious(root: &Path) -> Vec { let patterns = [ @@ -471,48 +461,6 @@ pub fn scan_local_git(path: String) -> Result { }) } -/// 调用 GitHub API 创建远端仓库,返回 clone_url -#[tauri::command] -pub fn github_create_repo( - name: String, - description: String, - private: bool, -) -> Result { - let token = get_token()?; - let body = serde_json::json!({ - "name": name, - "description": description, - "private": private, - "auto_init": false, - }); - - let resp = ureq::post("https://api.github.com/user/repos") - .set("Authorization", &format!("Bearer {}", token)) - .set("Accept", "application/vnd.github+json") - .set("User-Agent", "dev-manager-tauri/1.0") - .send_json(body) - .map_err(|e| match e { - ureq::Error::Status(422, resp) => { - let msg = resp - .into_json::() - .ok() - .and_then(|v| v["message"].as_str().map(|s| s.to_string())) - .unwrap_or("仓库名已存在或不合法".to_string()); - msg - } - other => format!("GitHub API 请求失败: {other}"), - })?; - - let json: serde_json::Value = resp - .into_json() - .map_err(|e| format!("解析响应失败: {e}"))?; - - json["clone_url"] - .as_str() - .map(|s| s.to_string()) - .ok_or_else(|| "未获取到仓库地址".to_string()) -} - /// git init(若还没有 .git)并提交所有改动 #[tauri::command] pub fn git_init_and_commit(path: String, message: String) -> Result<(), String> { @@ -569,32 +517,6 @@ pub fn git_remote_set(path: String, url: String) -> Result<(), String> { } } -/// push 到 origin,设置 upstream tracking -#[tauri::command] -pub fn git_push_to_github(path: String, branch: String) -> Result<(), String> { - let token = get_token()?; - let win_path = wsl_to_win(&path); - let repo = open_repo(&win_path)?; - - let refspec = format!("refs/heads/{}:refs/heads/{}", branch, branch); - - let mut callbacks = git2::RemoteCallbacks::new(); - let token_clone = token.clone(); - callbacks.credentials(move |_url, _username, _allowed| { - git2::Cred::userpass_plaintext(&token_clone, "x-oauth-basic") - }); - - let mut push_opts = git2::PushOptions::new(); - push_opts.remote_callbacks(callbacks); - - repo.find_remote("origin") - .map_err(|e| format!("未配置 origin remote: {e}"))? - .push(&[refspec.as_str()], Some(&mut push_opts)) - .map_err(|e| format!("push 失败: {e}"))?; - - Ok(()) -} - /// 写入 .gitignore 文件到指定目录 #[tauri::command] pub fn write_gitignore(dir: String, content: String) -> Result<(), String> { diff --git a/src-tauri/src/commands/servers.rs b/src-tauri/src/commands/servers.rs index 38f17eb..f932b44 100644 --- a/src-tauri/src/commands/servers.rs +++ b/src-tauri/src/commands/servers.rs @@ -320,7 +320,8 @@ mod tests { ); CREATE TABLE project_server_map ( project_id TEXT NOT NULL, server_id TEXT NOT NULL, - role TEXT DEFAULT 'production', deploy_path TEXT, notes TEXT, + role TEXT DEFAULT 'production', deploy_type TEXT DEFAULT 'bare', + deploy_path TEXT, container_name TEXT, start_command TEXT, notes TEXT, PRIMARY KEY (project_id, server_id) );", ) diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index 2596e76..c17ead6 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -6,7 +6,6 @@ use std::collections::HashMap; /// 全局 key(不按空间隔离,属于整个应用/机器级别配置) const GLOBAL_KEYS: &[&str] = &[ - "github_client_id", "github_client_secret", "wsl_distro", // 机器级别,与空间无关 "wsl_open_mode", // WSL 编辑器启动方式 "editors", "active_editors", "active_editor", @@ -17,7 +16,7 @@ const GLOBAL_KEYS: &[&str] = &[ ]; /// 获取 settings: -/// - 有 space_id:返回该空间的 space_settings + 全局 key(github_client_id/secret) +/// - 有 space_id:返回该空间的 space_settings + 全局 key(GLOBAL_KEYS) /// - 无 space_id:返回全局 settings 表 #[tauri::command] pub fn get_settings(space_id: Option) -> Result, String> { @@ -326,43 +325,6 @@ fn check_project_paths() -> HealthItem { result.unwrap_or_else(|e| err_item("project_paths", "项目本地路径", &e)) } -/// 检测 GitHub Token 是否有效(settings.github_token) -fn check_github_token() -> HealthItem { - let result: Result = (|| { - let conn = pool().get().map_err(|e| e.to_string())?; - let token: Option = conn.query_row( - "SELECT value FROM settings WHERE key = 'github_token'", - [], |row| row.get(0), - ).ok(); - - let token = match token { - Some(t) if !t.trim().is_empty() => t.trim().to_string(), - _ => return Ok(skip_item("github_token", "GitHub Token", "未登录 GitHub 账户")), - }; - - match ureq::builder().timeout(std::time::Duration::from_secs(8)).build() - .get("https://api.github.com/user") - .set("Authorization", &format!("Bearer {}", token)) - .set("User-Agent", "dev-manager-tauri") - .call() - { - Ok(resp) => { - let username = resp.into_json::().ok() - .and_then(|v| v["login"].as_str().map(|s| s.to_string())) - .unwrap_or_else(|| "未知账户".into()); - Ok(HealthItem { id: "github_token".into(), name: "GitHub Token".into(), status: "ok".into(), - detail: Some(format!("Token 有效,账户: {}", username)) }) - } - Err(ureq::Error::Status(401, _)) | Err(ureq::Error::Status(403, _)) => - Ok(HealthItem { id: "github_token".into(), name: "GitHub Token".into(), status: "error".into(), - detail: Some("Token 已失效或权限不足,请重新登录 GitHub 账户".into()) }), - Err(e) => Ok(HealthItem { id: "github_token".into(), name: "GitHub Token".into(), status: "warn".into(), - detail: Some(format!("Token 已存储但无法联通 GitHub API(可能需要代理): {}", e)) }), - } - })(); - result.unwrap_or_else(|e| err_item("github_token", "GitHub Token", &e)) -} - /// 检测活跃编辑器路径是否可用 /// 存储结构:editors = [{id,name,path}],active_editors = ["id1","id2"] fn check_editor() -> HealthItem { @@ -590,7 +552,6 @@ pub fn collect_health_report() -> HealthReport { let h_mcp = thread::spawn(check_mcp); let h_git = thread::spawn(check_git_paths); let h_proj = thread::spawn(check_project_paths); - let h_github = thread::spawn(check_github_token); let h_editor = thread::spawn(check_editor); let h_db = thread::spawn(check_db_latency); let h_quality = thread::spawn(collect_quality_metrics); @@ -599,7 +560,6 @@ pub fn collect_health_report() -> HealthReport { h_mcp .join().unwrap_or_else(|_| err_item("mcp", "MCP Server", "检测线程异常")), h_git .join().unwrap_or_else(|_| err_item("git_paths", "Git 仓库路径", "检测线程异常")), h_proj .join().unwrap_or_else(|_| err_item("project_paths", "项目本地路径", "检测线程异常")), - h_github.join().unwrap_or_else(|_| err_item("github_token", "GitHub Token", "检测线程异常")), h_editor.join().unwrap_or_else(|_| err_item("editor", "编辑器路径", "检测线程异常")), h_db .join().unwrap_or_else(|_| err_item("db_latency", "数据库延迟", "检测线程异常")), ]; @@ -626,9 +586,7 @@ mod tests { use super::*; #[test] - fn global_keys_contains_auth_and_infra() { - assert!(GLOBAL_KEYS.contains(&"github_client_id")); - assert!(GLOBAL_KEYS.contains(&"github_client_secret")); + fn global_keys_contains_infra() { assert!(GLOBAL_KEYS.contains(&"mcp_port")); assert!(GLOBAL_KEYS.contains(&"wsl_distro")); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8ea7857..7b66f33 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -18,7 +18,7 @@ pub(crate) fn silent_cmd(program: &str) -> std::process::Command { cmd } -use commands::{activity::*, blueprint::*, buff::{apply_blueprint_buff, remove_blueprint_buff, get_buff_status, list_buffed_projects}, canvas::*, cicd::*, claude_config::get_beast_global_status, devtools::*, git_ops::*, git_repos::*, github::*, groups::*, mentor::{add_project_note, get_group_mentor_report, get_mentor_context, get_project_notes, get_startup_alerts, run_startup_health_scan}, network::{get_wsl_ip, sync_wsl_proxy, get_wsl_proxy_status, clear_wsl_proxy, test_wsl_proxy_ip, test_win_proxy_ip}, project_scan::*, project_tools::*, projects::{batch_create_projects, create_project, delete_project, get_project, get_projects, scan_subfolders, update_project, update_project_tags}, proxy::*, publish::*, settings::{check_mcp_health, detect_editors, get_settings, run_health_check, search_editor_path, update_settings}, shell::*, spaces::*, tags::*, templates::*}; +use commands::{activity::*, blueprint::*, buff::{apply_blueprint_buff, remove_blueprint_buff, get_buff_status, list_buffed_projects}, canvas::*, cicd::*, claude_config::get_beast_global_status, devtools::*, git_ops::*, git_repos::*, groups::*, mentor::{add_project_note, get_group_mentor_report, get_mentor_context, get_project_notes, get_startup_alerts, run_startup_health_scan}, network::{get_wsl_ip, sync_wsl_proxy, get_wsl_proxy_status, clear_wsl_proxy, test_wsl_proxy_ip, test_win_proxy_ip}, project_scan::*, project_tools::*, projects::{batch_create_projects, create_project, delete_project, get_project, get_projects, scan_subfolders, update_project, update_project_tags}, proxy::*, publish::*, settings::{check_mcp_health, detect_editors, get_settings, run_health_check, search_editor_path, update_settings}, shell::*, spaces::*, tags::*, templates::*}; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { @@ -193,15 +193,6 @@ pub fn run() { git_create_branch, git_list_branches, git_checkout_branch, - // github - github_save_account, - github_get_account, - github_get_token, - github_get_client_id, - github_logout, - github_start_oauth, - github_poll_oauth, - github_open_login_window, // spaces get_spaces, create_space, @@ -218,12 +209,10 @@ pub fn run() { update_git_repo_deploy, mount_repo, delete_git_repo, - // publish to github + // 本地 git 工具 scan_local_git, - github_create_repo, git_init_and_commit, git_remote_set, - git_push_to_github, register_and_link_repo, write_gitignore, resolve_wsl_path, @@ -272,6 +261,7 @@ pub fn run() { commands::agent_infra::generate_agent_infra_files, commands::agent_infra::scan_doc_freshness, commands::agent_infra::get_doc_freshness_cache, + commands::agent_infra::get_agent_health, commands::commit_metrics::get_commit_stats, commands::commit_metrics::ingest_full_git_history, commands::commit_metrics::get_project_commit_health, diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 9aae0a9..03625d3 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -146,27 +146,6 @@ impl Space { } } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct GitAccount { - pub id: String, - pub username: String, - pub avatar_url: Option, - pub provider: String, - pub created_at: Option, -} - -impl GitAccount { - pub fn from_row(row: &Row<'_>) -> rusqlite::Result { - Ok(Self { - id: row.get("id")?, - username: row.get("username")?, - avatar_url: row.get("avatar_url")?, - provider: row.get("provider")?, - created_at: row.get("created_at")?, - }) - } -} - #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DeployCommand { pub id: String, diff --git a/src/App.tsx b/src/App.tsx index c264d38..b46e287 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,4 @@ import { useState, useEffect } from "react"; -import { useQueryClient } from "@tanstack/react-query"; import { OverviewPage } from "./components/overview/OverviewPage"; import { Dashboard } from "./components/dashboard/Dashboard"; import { ProjectModal } from "./components/dashboard/ProjectModal"; @@ -10,24 +9,16 @@ import { HealthPage } from "./components/health/HealthPage"; import { SourceLibraryPage } from "./components/source-library/SourceLibraryPage"; import { Sidebar, type Page } from "./components/layout/Sidebar"; import { Toast } from "./components/ui/Toast"; -import LoginPage from "./components/auth/LoginPage"; -import { githubGetAccount, githubLogout, getStartupAlerts, type GitAccount, type StartupAlert } from "./lib/commands"; -import { useSpacesSync } from "./hooks/useSpacesSync"; +import { getStartupAlerts, type StartupAlert } from "./lib/commands"; import { useUIStore } from "./store/ui"; import { check as checkUpdate } from "@tauri-apps/plugin-updater"; export default function App() { const [page, setPage] = useState("overview"); - const [account, setAccount] = useState(undefined); const [alerts, setAlerts] = useState([]); const [alertsDismissed, setAlertsDismissed] = useState(false); - const qc = useQueryClient(); const showToast = useUIStore((s) => s.showToast); - useEffect(() => { - githubGetAccount().then(setAccount).catch(() => setAccount(null)); - }, []); - // 启动时静默检查更新 useEffect(() => { checkUpdate() @@ -41,39 +32,17 @@ export default function App() { // 启动健康扫描结果(延迟 2s 等后端扫描完成) useEffect(() => { - if (account == null) return; const timer = setTimeout(() => { getStartupAlerts() .then((list) => { if (list.length > 0) setAlerts(list); }) .catch(() => {}); }, 2000); return () => clearTimeout(timer); - }, [account]); - - const handleLogout = async () => { - await githubLogout(); - qc.clear(); - setAccount(null); - }; - - const { data: spacesJson } = useSpacesSync(account !== null && account !== undefined); - - if (account === undefined) return null; - - if (account === null) { - return ( -
- - -
- ); - } + }, []); return (
@@ -98,7 +67,7 @@ export default function App() { )}
{page === "overview" && } - {page === "dashboard" && } + {page === "dashboard" && } {page === "source-library" && } {page === "devtools" && } {page === "proxy" && } diff --git a/src/components/auth/LoginPage.tsx b/src/components/auth/LoginPage.tsx deleted file mode 100644 index 1b7be4d..0000000 --- a/src/components/auth/LoginPage.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { useState, useRef, useEffect } from "react"; -import { - githubOpenLoginWindow, - githubPollOAuth, - githubFetchUser, - githubSaveAccount, - githubGetClientId, - updateSettings, - type GitAccount, -} from "../../lib/commands"; -import { useUIStore } from "../../store/ui"; - -type Step = "checking" | "setup" | "idle" | "waiting" | "done" | "error"; - -interface Props { - onLogin: (account: GitAccount) => void; -} - -export default function LoginPage({ onLogin }: Props) { - const showToast = useUIStore((s) => s.showToast); - const [step, setStep] = useState("checking"); - const [errorMsg, setErrorMsg] = useState(""); - const [clientId, setClientId] = useState(""); - const [clientSecret, setClientSecret] = useState(""); - const [savingConfig, setSavingConfig] = useState(false); - - const pollRef = useRef | null>(null); - const cancelledRef = useRef(false); - - // 启动时检查是否已配置 OAuth App - useEffect(() => { - githubGetClientId() - .then(() => setStep("idle")) - .catch(() => setStep("setup")); - }, []); - - useEffect(() => () => { - cancelledRef.current = true; - if (pollRef.current) clearInterval(pollRef.current); - }, []); - - const stopPolling = () => { - if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } - }; - - const handleSaveConfig = async () => { - if (!clientId.trim() || !clientSecret.trim()) { - showToast("❌ Client ID 和 Client Secret 均不能为空"); - return; - } - setSavingConfig(true); - try { - await updateSettings({ - github_client_id: clientId.trim(), - github_client_secret: clientSecret.trim(), - }); - showToast("配置已保存 ✓"); - setStep("idle"); - } catch (e) { - showToast(`❌ ${e}`); - } finally { - setSavingConfig(false); - } - }; - - const handleLogin = async () => { - cancelledRef.current = false; - setErrorMsg(""); - setStep("waiting"); - try { - await githubOpenLoginWindow(); - pollRef.current = setInterval(async () => { - if (cancelledRef.current) { stopPolling(); return; } - try { - const token = await githubPollOAuth(); - if (token === null) return; - stopPolling(); - const user = await githubFetchUser(token); - const account = await githubSaveAccount(user.login, user.avatar_url, token); - if (cancelledRef.current) return; - setStep("done"); - onLogin(account); - } catch (e) { - stopPolling(); - setStep("error"); - setErrorMsg(String(e)); - } - }, 2000); - } catch (e) { - setStep("error"); - setErrorMsg(String(e)); - } - }; - - const handleCancel = () => { - cancelledRef.current = true; - stopPolling(); - setStep("idle"); - setErrorMsg(""); - }; - - return ( -
-
-

Dev Space Manager

-

基于 Git 的本地开发空间管理器

-
- - {step === "checking" && ( -
- )} - - {step === "setup" && ( -
-
-

配置 GitHub OAuth App

-

- 前往 GitHub → Settings → Developer settings → OAuth Apps 创建应用。 - Callback URL 填 http://localhost, - 勾选 Enable Device Flow 无需勾选。 -

-
-
-
- - setClientId(e.target.value)} - placeholder="Ov23li..." - className="w-full bg-zinc-900 border border-zinc-600 rounded-lg px-3 py-2 text-sm text-white font-mono outline-none focus:ring-2 focus:ring-blue-500 placeholder:text-zinc-600" - /> -
-
- - setClientSecret(e.target.value)} - placeholder="••••••••••••••••••••••••••••••••••••" - className="w-full bg-zinc-900 border border-zinc-600 rounded-lg px-3 py-2 text-sm text-white font-mono outline-none focus:ring-2 focus:ring-blue-500" - /> -
-
-

Secret 仅存于本机 SQLite,由 Rust 完成 token 交换。

- -
- )} - - {step === "idle" && ( -
- - -
- )} - - {step === "waiting" && ( -
-
-

请在浏览器中完成 GitHub 授权

-

授权完成后应用自动登录

- -
- )} - - {step === "done" && ( -

登录成功!

- )} - - {step === "error" && ( -
-

{errorMsg}

- {errorMsg.includes("代理") && ( -

- 请确认 v2rayN 系统代理已开启后重试 -

- )} -
- - -
-
- )} -
- ); -} diff --git a/src/components/dashboard/AgentInfraPanel.tsx b/src/components/dashboard/AgentInfraPanel.tsx index 3908ded..a65bf02 100644 --- a/src/components/dashboard/AgentInfraPanel.tsx +++ b/src/components/dashboard/AgentInfraPanel.tsx @@ -1,10 +1,14 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { scanAgentInfraStack, generateAgentInfraFiles, + getDocFreshnessCache, + getAgentHealth, type AgentInfraStack, type ContractInput, type FileResult, + type CachedFreshness, + type AgentHealthReport, } from "../../lib/commands"; const CONFIDENCE_BADGE: Record = { @@ -31,6 +35,17 @@ const ACTION_COLOR: Record = { failed: "text-red-600", }; +const HEALTH_BADGE: Record = { + green: "bg-green-50 text-green-700 border-green-200", + yellow: "bg-amber-50 text-amber-700 border-amber-200", + red: "bg-red-50 text-red-700 border-red-200", +}; +const HEALTH_LABEL: Record = { + green: "🟢 良好", + yellow: "🟡 有风险", + red: "🔴 需修复", +}; + export function AgentInfraPanel({ projectPath, projectId, @@ -45,6 +60,20 @@ export function AgentInfraPanel({ const [report, setReport] = useState(null); const [error, setError] = useState(null); + // 健康诊断状态 + const [healthCache, setHealthCache] = useState(null); + const [healthReport, setHealthReport] = useState(null); + const [diagnosing, setDiagnosing] = useState(false); + const [copied, setCopied] = useState(false); + + // 组件挂载时读取缓存健康状态 + useEffect(() => { + if (!projectId) return; + getDocFreshnessCache(projectId).then((cached) => { + if (cached) setHealthCache(cached); + }); + }, [projectId]); + async function handleScan() { setLoading(true); setError(null); @@ -84,6 +113,29 @@ export function AgentInfraPanel({ } } + async function handleDiagnose() { + if (!projectId) return; + setDiagnosing(true); + setError(null); + setHealthReport(null); + try { + const result = await getAgentHealth(projectPath, projectId); + setHealthReport(result); + setHealthCache({ score: result.overall, details: "[]", checked_at: result.checked_at }); + } catch (e) { + setError(String(e)); + } finally { + setDiagnosing(false); + } + } + + async function handleCopy() { + if (!healthReport) return; + await navigator.clipboard.writeText(healthReport.fix_prompt); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + return (
{/* 标题行 */} @@ -213,6 +265,78 @@ export function AgentInfraPanel({
)} + {/* 健康诊断区块 */} + {projectId && ( +
+
+

+ Agent 健康诊断 +

+ {healthCache && ( + + {HEALTH_LABEL[healthCache.score] ?? healthCache.score} + + )} +
+ + {healthCache && !healthReport && ( +

+ 上次检测:{healthCache.checked_at.slice(0, 10)} +

+ )} + + + + {/* 诊断结果 */} + {healthReport && ( +
+

+ 检测时间:{healthReport.checked_at.slice(0, 10)} +

+ + {healthReport.issues.length === 0 ? ( +

✅ 所有检查通过

+ ) : ( +
+ {healthReport.issues.map((issue, idx) => ( +

+ + {issue.level === "error" ? "❌" : "⚠️"} + + {issue.title} +

+ ))} +
+ )} + + {/* 可复制文本框 */} +
+