Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2fd5e369b |
@@ -1,5 +1,5 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# AgentDock 适配器 · Anthropic Claude Code(Wave 2 全字段)
|
# AgentDock 适配器 · Anthropic Claude Code(Wave 2.1)
|
||||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md §2 + 架构 §3.3
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md §2 + 架构 §3.3
|
||||||
# ============================================================
|
# ============================================================
|
||||||
id: claude-code
|
id: claude-code
|
||||||
@@ -7,7 +7,7 @@ name: Claude Code
|
|||||||
name_zh: Claude Code
|
name_zh: Claude Code
|
||||||
vendor: Anthropic
|
vendor: Anthropic
|
||||||
status: available
|
status: available
|
||||||
adapter_version: 1.0.0
|
adapter_version: 1.1.0
|
||||||
license: 专有(仅官方渠道安装、不重打包、不修改二进制)
|
license: 专有(仅官方渠道安装、不重打包、不修改二进制)
|
||||||
|
|
||||||
platforms:
|
platforms:
|
||||||
@@ -72,6 +72,9 @@ authorization:
|
|||||||
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
|
||||||
|
|
||||||
|
# 配置机制(调研底稿 §2「配置机制」):用户级 ~/.claude/settings.json(JSON)
|
||||||
|
# 官方确认:settings.json 内 env 块可注入环境变量;ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL /
|
||||||
|
# ANTHROPIC_AUTH_TOKEN / ANTHROPIC_CUSTOM_HEADERS;默认模型经 /model 命令调整(非 settings.json 字段)。
|
||||||
configuration:
|
configuration:
|
||||||
files:
|
files:
|
||||||
- path: "~/.claude/settings.json"
|
- path: "~/.claude/settings.json"
|
||||||
@@ -81,31 +84,37 @@ configuration:
|
|||||||
- key: ANTHROPIC_API_KEY
|
- key: ANTHROPIC_API_KEY
|
||||||
sensitive: true
|
sensitive: true
|
||||||
maps_to_field: api_key
|
maps_to_field: api_key
|
||||||
- key: ANTHROPIC_BASE_URL
|
- key: ANTHROPIC_AUTH_TOKEN
|
||||||
sensitive: false
|
sensitive: true
|
||||||
maps_to_field: base_url
|
maps_to_field: auth_token
|
||||||
fields:
|
fields:
|
||||||
- id: model
|
|
||||||
label_zh: 默认模型
|
|
||||||
help_zh: Claude Code 使用的默认模型
|
|
||||||
required: false
|
|
||||||
sensitive: false
|
|
||||||
type: string
|
|
||||||
storage: file
|
|
||||||
- id: env.ANTHROPIC_BASE_URL
|
- id: env.ANTHROPIC_BASE_URL
|
||||||
label_zh: Base URL
|
label_zh: Base URL
|
||||||
help_zh: 自定义 API 端点 / 网关 / 代理(写入 settings.json 的 env 块)
|
group: advanced
|
||||||
|
help_zh: 官方环境变量 ANTHROPIC_BASE_URL,写入 settings.json 的 env 块(自定义 API 端点/网关/代理)
|
||||||
required: false
|
required: false
|
||||||
sensitive: false
|
sensitive: false
|
||||||
type: url
|
type: url
|
||||||
storage: file
|
storage: file
|
||||||
|
docs_url: https://code.claude.com/docs/en/env-vars
|
||||||
- id: api_key
|
- id: api_key
|
||||||
label_zh: API Key
|
label_zh: API Key
|
||||||
help_zh: 存入系统密钥库(对应 ANTHROPIC_API_KEY)
|
group: auth
|
||||||
|
help_zh: 存入系统密钥库(对应 ANTHROPIC_API_KEY),设置后优先于订阅登录
|
||||||
required: false
|
required: false
|
||||||
sensitive: true
|
sensitive: true
|
||||||
type: string
|
type: string
|
||||||
storage: keyring
|
storage: keyring
|
||||||
|
docs_url: https://code.claude.com/docs/en/env-vars
|
||||||
|
- id: auth_token
|
||||||
|
label_zh: Bearer 令牌
|
||||||
|
group: auth
|
||||||
|
help_zh: 可选,存入系统密钥库(对应 ANTHROPIC_AUTH_TOKEN,Bearer 头);一般用户无需填写
|
||||||
|
required: false
|
||||||
|
sensitive: true
|
||||||
|
type: string
|
||||||
|
storage: keyring
|
||||||
|
docs_url: https://code.claude.com/docs/en/env-vars
|
||||||
|
|
||||||
diagnostics:
|
diagnostics:
|
||||||
- rule_id: path.not_installed
|
- rule_id: path.not_installed
|
||||||
@@ -115,13 +124,38 @@ diagnostics:
|
|||||||
|
|
||||||
documentation:
|
documentation:
|
||||||
quickstart_zh: 安装后运行 `claude` 登录,或用 `claude -p "提示词"` 无头执行。
|
quickstart_zh: 安装后运行 `claude` 登录,或用 `claude -p "提示词"` 无头执行。
|
||||||
|
install_zh: 推荐 winget(`winget install Anthropic.ClaudeCode`);也可走官方 PowerShell 脚本(irm https://claude.ai/install.ps1)或 npm(`@anthropic-ai/claude-code`,Node 22+)。
|
||||||
|
auth_zh: ① 浏览器 OAuth:运行 `claude` 按提示登录(Pro/Max/Team/Enterprise/Console 账号;免费版不含 Claude Code);② API Key:设置 ANTHROPIC_API_KEY 后 CLI 优先使用(非交互 -p 模式下有 key 即直接使用)。
|
||||||
commands:
|
commands:
|
||||||
- cmd: claude
|
- cmd: claude
|
||||||
desc_zh: 启动交互式会话
|
desc_zh: 启动交互式会话
|
||||||
- cmd: claude -p "提示词"
|
- cmd: claude -p "提示词"
|
||||||
desc_zh: 无头单次执行
|
desc_zh: 无头单次执行(--print)
|
||||||
|
- cmd: claude -p "提示词" --output-format json
|
||||||
|
desc_zh: 结构化 JSON 输出
|
||||||
|
- cmd: claude --allowedTools "Bash(git:*)"
|
||||||
|
desc_zh: 预授权指定工具
|
||||||
|
- cmd: claude --continue
|
||||||
|
desc_zh: 续接最近的会话
|
||||||
|
- cmd: claude --resume <会话ID>
|
||||||
|
desc_zh: 恢复指定会话
|
||||||
- cmd: claude doctor
|
- cmd: claude doctor
|
||||||
desc_zh: 环境诊断
|
desc_zh: 环境诊断
|
||||||
updated_at: "2026-08-24"
|
- cmd: claude --version
|
||||||
|
desc_zh: 显示版本号
|
||||||
|
params:
|
||||||
|
- param: -p / --print
|
||||||
|
desc_zh: 非交互模式,执行后退出
|
||||||
|
- param: --output-format
|
||||||
|
desc_zh: 输出格式(text / json / stream-json)
|
||||||
|
- param: --json-schema
|
||||||
|
desc_zh: 约束输出为指定 JSON Schema
|
||||||
|
- param: --allowedTools
|
||||||
|
desc_zh: 预授权工具列表
|
||||||
|
- param: --bare
|
||||||
|
desc_zh: 极简启动模式
|
||||||
|
updated_at: "2026-08-25"
|
||||||
risks_zh:
|
risks_zh:
|
||||||
- 专有许可:只引导官方渠道安装,不重打包、不修改二进制
|
- 专有许可:只引导官方渠道安装,不重打包、不修改二进制、不代付费
|
||||||
|
- 原生 Windows 无沙箱功能,需要沙箱请使用 WSL2(以官方文档为准)
|
||||||
|
- 默认模型经 /model 命令调整,非 settings.json 字段,故本表单未提供模型字段
|
||||||
|
|||||||
+67
-12
@@ -1,5 +1,5 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# AgentDock 适配器 · OpenAI Codex CLI(Wave 2 全字段)
|
# AgentDock 适配器 · OpenAI Codex CLI(Wave 2.1)
|
||||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md §1 + 架构 §3.3
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md §1 + 架构 §3.3
|
||||||
# ============================================================
|
# ============================================================
|
||||||
id: codex
|
id: codex
|
||||||
@@ -7,7 +7,7 @@ name: Codex CLI
|
|||||||
name_zh: Codex CLI
|
name_zh: Codex CLI
|
||||||
vendor: OpenAI
|
vendor: OpenAI
|
||||||
status: available
|
status: available
|
||||||
adapter_version: 1.0.0
|
adapter_version: 1.1.0
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
|
|
||||||
platforms:
|
platforms:
|
||||||
@@ -76,6 +76,10 @@ authorization:
|
|||||||
env_keys: [OPENAI_API_KEY]
|
env_keys: [OPENAI_API_KEY]
|
||||||
notes_zh: API Key 从系统密钥库读取,经 stdin 注入,不进 argv
|
notes_zh: API Key 从系统密钥库读取,经 stdin 注入,不进 argv
|
||||||
|
|
||||||
|
# 配置机制(调研底稿 §1「配置机制」):用户级 ~/.codex/config.toml(TOML)
|
||||||
|
# 官方确认可配:model / approval_policy / sandbox_mode / model_reasoning_effort /
|
||||||
|
# web_search / log_dir / [model_providers.<id>](base_url / env_key / wire_api)/
|
||||||
|
# 简化键 openai_base_url。表单字段名与官方文档一致。
|
||||||
configuration:
|
configuration:
|
||||||
files:
|
files:
|
||||||
- path: "~/.codex/config.toml"
|
- path: "~/.codex/config.toml"
|
||||||
@@ -85,31 +89,52 @@ configuration:
|
|||||||
- key: OPENAI_API_KEY
|
- key: OPENAI_API_KEY
|
||||||
sensitive: true
|
sensitive: true
|
||||||
maps_to_field: api_key
|
maps_to_field: api_key
|
||||||
- key: OPENAI_BASE_URL
|
|
||||||
sensitive: false
|
|
||||||
maps_to_field: openai_base_url
|
|
||||||
fields:
|
fields:
|
||||||
- id: model
|
- id: model
|
||||||
label_zh: 默认模型
|
label_zh: 默认模型
|
||||||
help_zh: Codex 使用的默认模型(如 gpt-5.6-terra)
|
group: common
|
||||||
|
help_zh: 官方键 model,如 gpt-5.6-terra(对应官方 config-basic 文档)
|
||||||
required: false
|
required: false
|
||||||
sensitive: false
|
sensitive: false
|
||||||
type: string
|
type: string
|
||||||
storage: file
|
storage: file
|
||||||
|
docs_url: https://learn.chatgpt.com/docs/config-file/config-basic
|
||||||
|
- id: approval_policy
|
||||||
|
label_zh: 审批策略
|
||||||
|
group: advanced
|
||||||
|
help_zh: 官方键 approval_policy,控制执行命令前是否需人工确认
|
||||||
|
required: false
|
||||||
|
sensitive: false
|
||||||
|
type: enum
|
||||||
|
storage: file
|
||||||
|
docs_url: https://learn.chatgpt.com/docs/config-file/config-basic
|
||||||
|
options:
|
||||||
|
- value: untrusted
|
||||||
|
label_zh: 不信任(全部确认)
|
||||||
|
- value: on-failure
|
||||||
|
label_zh: 失败时确认
|
||||||
|
- value: on-request
|
||||||
|
label_zh: 每次请求确认
|
||||||
|
- value: never
|
||||||
|
label_zh: 永不确认
|
||||||
- id: openai_base_url
|
- id: openai_base_url
|
||||||
label_zh: Base URL
|
label_zh: Base URL
|
||||||
help_zh: 自定义 OpenAI 兼容端点(简化键)
|
group: advanced
|
||||||
|
help_zh: 官方简化键 openai_base_url,自定义 OpenAI 兼容端点/网关/代理
|
||||||
required: false
|
required: false
|
||||||
sensitive: false
|
sensitive: false
|
||||||
type: url
|
type: url
|
||||||
storage: file
|
storage: file
|
||||||
|
docs_url: https://learn.chatgpt.com/docs/config-file/config-advanced
|
||||||
- id: api_key
|
- id: api_key
|
||||||
label_zh: API Key
|
label_zh: API Key
|
||||||
help_zh: 存入系统密钥库,写入后仅显示「已保存」
|
group: auth
|
||||||
|
help_zh: 存入系统密钥库(对应环境变量 OPENAI_API_KEY),写入后仅显示「已保存」;也可用 codex login --with-api-key 注入
|
||||||
required: false
|
required: false
|
||||||
sensitive: true
|
sensitive: true
|
||||||
type: string
|
type: string
|
||||||
storage: keyring
|
storage: keyring
|
||||||
|
docs_url: https://learn.chatgpt.com/docs/config-file/config-advanced
|
||||||
|
|
||||||
diagnostics:
|
diagnostics:
|
||||||
- rule_id: path.not_installed
|
- rule_id: path.not_installed
|
||||||
@@ -118,14 +143,44 @@ diagnostics:
|
|||||||
- rule_id: config.corrupt
|
- rule_id: config.corrupt
|
||||||
|
|
||||||
documentation:
|
documentation:
|
||||||
quickstart_zh: 安装后运行 `codex` 登录账号,或用 `codex exec "任务"` 无头执行。
|
quickstart_zh: 安装后运行 `codex` 登录 ChatGPT 账号,或用 `codex exec "任务"` 无头执行。
|
||||||
|
install_zh: 推荐 `npm install -g @openai/codex`(本适配器默认渠道);也可走官方 PowerShell 脚本(irm https://chatgpt.com/codex/install.ps1)或 GitHub Releases 二进制。
|
||||||
|
auth_zh: 三种授权:① 浏览器登录 ChatGPT 账号(`codex login`,需订阅计划);② 设备码(`codex login --device-auth`);③ API Key(`codex login --with-api-key`,从 stdin 读取,或经 OPENAI_API_KEY 环境变量)。
|
||||||
commands:
|
commands:
|
||||||
- cmd: codex
|
- cmd: codex
|
||||||
desc_zh: 启动交互式会话
|
desc_zh: 启动交互式会话
|
||||||
- cmd: codex exec "提示词"
|
- cmd: codex exec "提示词"
|
||||||
desc_zh: 无头单次执行
|
desc_zh: 无头单次执行(别名 codex e)
|
||||||
|
- cmd: codex exec --json "提示词"
|
||||||
|
desc_zh: 以 JSONL 事件流输出
|
||||||
|
- cmd: codex exec --output-schema <schema>
|
||||||
|
desc_zh: 约束输出为指定 JSON Schema
|
||||||
|
- cmd: codex exec -o 文件
|
||||||
|
desc_zh: 将输出写入文件
|
||||||
|
- cmd: codex exec --sandbox
|
||||||
|
desc_zh: 在沙箱中执行
|
||||||
|
- cmd: codex login
|
||||||
|
desc_zh: 浏览器登录 ChatGPT 账号
|
||||||
|
- cmd: codex login --device-auth
|
||||||
|
desc_zh: 设备码流程登录
|
||||||
|
- cmd: codex login --with-api-key
|
||||||
|
desc_zh: 从 stdin 读取 API Key 登录
|
||||||
- cmd: codex login status
|
- cmd: codex login status
|
||||||
desc_zh: 查询登录状态
|
desc_zh: 查询当前登录状态
|
||||||
updated_at: "2026-08-24"
|
params:
|
||||||
|
- param: --json
|
||||||
|
desc_zh: 以 JSONL 事件流输出(exec 子命令)
|
||||||
|
- param: --output-schema
|
||||||
|
desc_zh: 约束输出为指定 JSON Schema
|
||||||
|
- param: -o / --output
|
||||||
|
desc_zh: 输出写入文件
|
||||||
|
- param: --ephemeral
|
||||||
|
desc_zh: 使用不落盘的临时会话
|
||||||
|
- param: --sandbox
|
||||||
|
desc_zh: 在沙箱中执行
|
||||||
|
- param: --device-auth
|
||||||
|
desc_zh: 使用设备码流程登录(login 子命令)
|
||||||
|
updated_at: "2026-08-25"
|
||||||
risks_zh:
|
risks_zh:
|
||||||
- --version 官方文档未确认,适配器已标 version_unconfirmed 并实测兜底
|
- --version 官方文档未确认,适配器已标 version_unconfirmed 并实测兜底
|
||||||
|
- 表单覆盖常用配置键;model_providers 表等高级自定义 provider 请以官方文档为准(见上方链接)
|
||||||
|
|||||||
+43
-11
@@ -1,5 +1,5 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# AgentDock 适配器 · Google Gemini CLI(Wave 2 全字段)
|
# AgentDock 适配器 · Google Gemini CLI(Wave 2.1)
|
||||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md §3 + 架构 §3.3
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md §3 + 架构 §3.3
|
||||||
# ============================================================
|
# ============================================================
|
||||||
id: gemini
|
id: gemini
|
||||||
@@ -7,7 +7,7 @@ name: Gemini CLI
|
|||||||
name_zh: Gemini CLI
|
name_zh: Gemini CLI
|
||||||
vendor: Google
|
vendor: Google
|
||||||
status: available
|
status: available
|
||||||
adapter_version: 1.0.0
|
adapter_version: 1.1.0
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
|
|
||||||
platforms:
|
platforms:
|
||||||
@@ -61,6 +61,9 @@ authorization:
|
|||||||
env_keys: [GEMINI_API_KEY]
|
env_keys: [GEMINI_API_KEY]
|
||||||
notes_zh: 设置 GEMINI_API_KEY 环境变量(AI Studio 申请)
|
notes_zh: 设置 GEMINI_API_KEY 环境变量(AI Studio 申请)
|
||||||
|
|
||||||
|
# 配置机制(调研底稿 §3「配置机制」):用户级 ~/.gemini/settings.json(JSON,官方发布 JSON Schema)
|
||||||
|
# 关键键:model.name(默认模型)、security.auth.selectedType(认证方式);
|
||||||
|
# 环境变量:GEMINI_API_KEY / GOOGLE_GEMINI_BASE_URL(须 HTTPS)/ GEMINI_MODEL。
|
||||||
configuration:
|
configuration:
|
||||||
files:
|
files:
|
||||||
- path: "~/.gemini/settings.json"
|
- path: "~/.gemini/settings.json"
|
||||||
@@ -70,31 +73,34 @@ configuration:
|
|||||||
- key: GEMINI_API_KEY
|
- key: GEMINI_API_KEY
|
||||||
sensitive: true
|
sensitive: true
|
||||||
maps_to_field: api_key
|
maps_to_field: api_key
|
||||||
- key: GOOGLE_GEMINI_BASE_URL
|
|
||||||
sensitive: false
|
|
||||||
maps_to_field: base_url
|
|
||||||
fields:
|
fields:
|
||||||
- id: model.name
|
- id: model.name
|
||||||
label_zh: 默认模型
|
label_zh: 默认模型
|
||||||
help_zh: Gemini CLI 使用的默认模型
|
group: common
|
||||||
|
help_zh: 官方键 model.name,settings.json 中的默认模型
|
||||||
required: false
|
required: false
|
||||||
sensitive: false
|
sensitive: false
|
||||||
type: string
|
type: string
|
||||||
storage: file
|
storage: file
|
||||||
|
docs_url: https://geminicli.com/docs/reference/configuration/
|
||||||
- id: env.GOOGLE_GEMINI_BASE_URL
|
- id: env.GOOGLE_GEMINI_BASE_URL
|
||||||
label_zh: Base URL
|
label_zh: Base URL
|
||||||
help_zh: 覆盖 Gemini API 默认地址(须 HTTPS,代理/网关场景)
|
group: advanced
|
||||||
|
help_zh: 官方环境变量 GOOGLE_GEMINI_BASE_URL(覆盖 Gemini API 默认地址,须 HTTPS,代理/网关场景);写入 settings.json 的 env 块
|
||||||
required: false
|
required: false
|
||||||
sensitive: false
|
sensitive: false
|
||||||
type: url
|
type: url
|
||||||
storage: file
|
storage: file
|
||||||
|
docs_url: https://geminicli.com/docs/reference/configuration/
|
||||||
- id: api_key
|
- id: api_key
|
||||||
label_zh: API Key
|
label_zh: API Key
|
||||||
help_zh: 存入系统密钥库(对应 GEMINI_API_KEY)
|
group: auth
|
||||||
|
help_zh: 存入系统密钥库(对应环境变量 GEMINI_API_KEY,AI Studio 申请)
|
||||||
required: false
|
required: false
|
||||||
sensitive: true
|
sensitive: true
|
||||||
type: string
|
type: string
|
||||||
storage: keyring
|
storage: keyring
|
||||||
|
docs_url: https://geminicli.com/docs/get-started/authentication/
|
||||||
|
|
||||||
diagnostics:
|
diagnostics:
|
||||||
- rule_id: path.not_installed
|
- rule_id: path.not_installed
|
||||||
@@ -104,13 +110,39 @@ diagnostics:
|
|||||||
- rule_id: config.corrupt
|
- rule_id: config.corrupt
|
||||||
|
|
||||||
documentation:
|
documentation:
|
||||||
quickstart_zh: 安装后运行 `gemini` 登录,或用 `gemini -p "提示词"` 无头执行。
|
quickstart_zh: 安装后运行 `gemini` 选「Sign in with Google」登录,或用 `gemini -p "提示词"` 无头执行。
|
||||||
|
install_zh: 推荐 `npm install -g @google/gemini-cli`(需 Node 20+);免安装可用 `npx @google/gemini-cli` 临时试用。
|
||||||
|
auth_zh: ① Google 账号 OAuth:运行 `gemini` 选「Sign in with Google」浏览器登录(免费档 60 请求/分钟);② API Key:设置 GEMINI_API_KEY 环境变量(AI Studio 申请,免费档 1000 请求/天);③ Vertex AI 走 ADC/服务账号(本表单不覆盖)。
|
||||||
commands:
|
commands:
|
||||||
- cmd: gemini
|
- cmd: gemini
|
||||||
desc_zh: 启动交互式会话
|
desc_zh: 启动交互式会话
|
||||||
- cmd: gemini -p "提示词"
|
- cmd: gemini -p "提示词"
|
||||||
desc_zh: 强制非交互单次执行
|
desc_zh: 强制非交互模式,单次执行
|
||||||
updated_at: "2026-08-24"
|
- cmd: gemini -p "提示词" --output-format json
|
||||||
|
desc_zh: 以 JSON 输出结果
|
||||||
|
- cmd: gemini -m <模型>
|
||||||
|
desc_zh: 指定模型
|
||||||
|
- cmd: gemini -i
|
||||||
|
desc_zh: 执行后进入交互模式
|
||||||
|
- cmd: gemini --version
|
||||||
|
desc_zh: 显示版本号
|
||||||
|
- cmd: npx @google/gemini-cli
|
||||||
|
desc_zh: 免安装临时运行
|
||||||
|
- cmd: GEMINI_API_KEY=<key> gemini
|
||||||
|
desc_zh: 用 API Key 环境变量启动
|
||||||
|
params:
|
||||||
|
- param: -p / --prompt
|
||||||
|
desc_zh: 强制非交互模式
|
||||||
|
- param: --output-format
|
||||||
|
desc_zh: 输出格式(text / json / stream-json)
|
||||||
|
- param: -m / --model
|
||||||
|
desc_zh: 指定模型
|
||||||
|
- param: -i
|
||||||
|
desc_zh: 执行后转交互
|
||||||
|
- param: -v / --version
|
||||||
|
desc_zh: 显示版本号并退出
|
||||||
|
updated_at: "2026-08-25"
|
||||||
risks_zh:
|
risks_zh:
|
||||||
- 官方发行形态为 npm 包,依赖 Node 20+,适配器已声明运行时门槛
|
- 官方发行形态为 npm 包,依赖 Node 20+,适配器已声明运行时门槛
|
||||||
- Windows 官方要求 11 24H2+,低版本 Win10 显示「官方未支持」
|
- Windows 官方要求 11 24H2+,低版本 Win10 显示「官方未支持」
|
||||||
|
- Vertex AI(ADC/服务账号)与 security.auth.selectedType 高级配置本表单不覆盖,以官方文档为准
|
||||||
|
|||||||
+44
-10
@@ -1,5 +1,5 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# AgentDock 适配器 · Kimi CLI(月之暗面 Moonshot)(Wave 2 全字段)
|
# AgentDock 适配器 · Kimi CLI(月之暗面 Moonshot)(Wave 2.1)
|
||||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md(Kimi 章节)+ 架构 §3.3
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md(Kimi 章节)+ 架构 §3.3
|
||||||
# 铁律:只走官方 code.kimi.com 脚本 / PyPI kimi-cli,严禁 npm(仿名包)。
|
# 铁律:只走官方 code.kimi.com 脚本 / PyPI kimi-cli,严禁 npm(仿名包)。
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -8,7 +8,7 @@ name: Kimi CLI
|
|||||||
name_zh: Kimi CLI
|
name_zh: Kimi CLI
|
||||||
vendor: 月之暗面
|
vendor: 月之暗面
|
||||||
status: available
|
status: available
|
||||||
adapter_version: 1.0.0
|
adapter_version: 1.1.0
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
|
|
||||||
platforms:
|
platforms:
|
||||||
@@ -78,6 +78,9 @@ authorization:
|
|||||||
env_keys: [KIMI_API_KEY]
|
env_keys: [KIMI_API_KEY]
|
||||||
notes_zh: Moonshot 开放平台 API Key
|
notes_zh: Moonshot 开放平台 API Key
|
||||||
|
|
||||||
|
# 配置机制(调研底稿 Kimi 章节):~/.kimi/config.toml(TOML,兼容 JSON)
|
||||||
|
# 可配 default_model、providers(type/base_url/api_key)、models 等;
|
||||||
|
# 环境变量:KIMI_API_KEY / KIMI_BASE_URL / KIMI_MODEL_NAME / KIMI_MODEL_TEMPERATURE。
|
||||||
configuration:
|
configuration:
|
||||||
files:
|
files:
|
||||||
- path: "~/.kimi/config.toml"
|
- path: "~/.kimi/config.toml"
|
||||||
@@ -87,31 +90,34 @@ configuration:
|
|||||||
- key: KIMI_API_KEY
|
- key: KIMI_API_KEY
|
||||||
sensitive: true
|
sensitive: true
|
||||||
maps_to_field: api_key
|
maps_to_field: api_key
|
||||||
- key: KIMI_BASE_URL
|
|
||||||
sensitive: false
|
|
||||||
maps_to_field: base_url
|
|
||||||
fields:
|
fields:
|
||||||
- id: default_model
|
- id: default_model
|
||||||
label_zh: 默认模型
|
label_zh: 默认模型
|
||||||
help_zh: Kimi CLI 使用的默认模型
|
group: common
|
||||||
|
help_zh: 官方键 default_model,Kimi CLI 使用的默认模型
|
||||||
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
|
||||||
- id: env.KIMI_BASE_URL
|
- id: env.KIMI_BASE_URL
|
||||||
label_zh: Base URL
|
label_zh: Base URL
|
||||||
help_zh: 自定义 OpenAI 兼容端点
|
group: advanced
|
||||||
|
help_zh: 官方环境变量 KIMI_BASE_URL(自定义 OpenAI 兼容端点);写入 config.toml 的 env 表,若需进程级生效请以官方 env-vars 文档为准
|
||||||
required: false
|
required: false
|
||||||
sensitive: false
|
sensitive: false
|
||||||
type: url
|
type: url
|
||||||
storage: file
|
storage: file
|
||||||
|
docs_url: https://moonshotai.github.io/kimi-cli/en/configuration/env-vars.html
|
||||||
- id: api_key
|
- id: api_key
|
||||||
label_zh: API Key
|
label_zh: API Key
|
||||||
help_zh: 存入系统密钥库(对应 KIMI_API_KEY)
|
group: auth
|
||||||
|
help_zh: 存入系统密钥库(对应环境变量 KIMI_API_KEY,Moonshot 开放平台获取)
|
||||||
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
|
||||||
|
|
||||||
diagnostics:
|
diagnostics:
|
||||||
- rule_id: path.not_installed
|
- rule_id: path.not_installed
|
||||||
@@ -123,14 +129,42 @@ diagnostics:
|
|||||||
|
|
||||||
documentation:
|
documentation:
|
||||||
quickstart_zh: 安装后运行 `kimi login` 登录,或用 `kimi -p "提示词"` 无头执行。
|
quickstart_zh: 安装后运行 `kimi login` 登录,或用 `kimi -p "提示词"` 无头执行。
|
||||||
|
install_zh: 只走官方渠道:`uv tool install kimi-cli`(本适配器默认)或官方脚本 code.kimi.com/install.sh(install.ps1)。脚本会自动装 uv 并从 PyPI kimi-cli 安装。严禁 npm(npm 上的 kimi-code 是第三方仿名包)。
|
||||||
|
auth_zh: ① Kimi Code 浏览器 OAuth(推荐,`kimi login`);② Moonshot 开放平台 API Key(platform 侧获取后粘贴,经 KIMI_API_KEY 注入)。`kimi logout` 清除 OAuth 凭据。
|
||||||
commands:
|
commands:
|
||||||
- cmd: kimi
|
- cmd: kimi
|
||||||
desc_zh: 启动交互式会话
|
desc_zh: 启动交互式会话
|
||||||
- cmd: kimi login
|
- cmd: kimi login
|
||||||
desc_zh: 登录(浏览器 OAuth 或 API Key)
|
desc_zh: 登录(浏览器 OAuth 或 API Key)
|
||||||
|
- cmd: kimi logout
|
||||||
|
desc_zh: 清除 OAuth 凭据
|
||||||
- cmd: kimi -p "提示词"
|
- cmd: kimi -p "提示词"
|
||||||
desc_zh: 无头单次执行
|
desc_zh: 无头单次执行
|
||||||
updated_at: "2026-08-24"
|
- cmd: kimi --output-format stream-json -p "提示词"
|
||||||
|
desc_zh: print 模式 JSON 流输出
|
||||||
|
- cmd: kimi --quiet
|
||||||
|
desc_zh: 静默模式
|
||||||
|
- cmd: kimi --version
|
||||||
|
desc_zh: 显示版本号
|
||||||
|
- cmd: kimi info
|
||||||
|
desc_zh: 显示版本与协议信息
|
||||||
|
- cmd: kimi acp
|
||||||
|
desc_zh: IDE 集成 stdio(ACP 协议)
|
||||||
|
params:
|
||||||
|
- param: -p / --prompt
|
||||||
|
desc_zh: 处理后退出(无头模式)
|
||||||
|
- param: --print
|
||||||
|
desc_zh: 打印模式
|
||||||
|
- param: --output-format
|
||||||
|
desc_zh: 输出格式(如 stream-json)
|
||||||
|
- param: --quiet
|
||||||
|
desc_zh: 静默模式
|
||||||
|
- param: --afk
|
||||||
|
desc_zh: 离键模式
|
||||||
|
- param: --config-file
|
||||||
|
desc_zh: 指定配置文件路径
|
||||||
|
updated_at: "2026-08-25"
|
||||||
risks_zh:
|
risks_zh:
|
||||||
- 严禁 npm:npm 上的 kimi-code 是第三方仿名包,与月之暗面无关
|
- 严禁 npm:npm 上的 kimi-code 是第三方仿名包,与月之暗面无关
|
||||||
- 官方脚本会自动安装 uv 并从 PyPI kimi-cli 安装
|
- 官方脚本会自动安装 uv 并从 PyPI kimi-cli 安装(Python 3.12–3.14)
|
||||||
|
- providers 表(type/base_url/api_key 多 provider)等高级配置本表单不覆盖,以官方 config-files 文档为准
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# AgentDock 适配器 · OpenCode(opencode.ai)(Wave 2 全字段)
|
# AgentDock 适配器 · OpenCode(opencode.ai)(Wave 2.1)
|
||||||
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md(OpenCode 章节)+ 架构 §3.3
|
# 数据依据:调研底稿 agent-cli-survey-2026-08-24.md(OpenCode 章节)+ 架构 §3.3
|
||||||
# ============================================================
|
# ============================================================
|
||||||
id: opencode
|
id: opencode
|
||||||
@@ -7,7 +7,7 @@ name: OpenCode
|
|||||||
name_zh: OpenCode
|
name_zh: OpenCode
|
||||||
vendor: opencode.ai
|
vendor: opencode.ai
|
||||||
status: available
|
status: available
|
||||||
adapter_version: 1.0.0
|
adapter_version: 1.1.0
|
||||||
license: MIT
|
license: MIT
|
||||||
|
|
||||||
platforms:
|
platforms:
|
||||||
@@ -80,6 +80,9 @@ authorization:
|
|||||||
- mode: device_code
|
- mode: device_code
|
||||||
notes_zh: GitHub Copilot 设备码登录(github.com/login/device 输码)
|
notes_zh: GitHub Copilot 设备码登录(github.com/login/device 输码)
|
||||||
|
|
||||||
|
# 配置机制(调研底稿 OpenCode 章节):全局 ~/.config/opencode/opencode.json(JSON,带注释 jsonc 亦可)
|
||||||
|
# model 字段设默认模型;每个 provider 可单独改 baseURL(含本地 Ollama/LM Studio 等 OpenAI 兼容端点);
|
||||||
|
# API Key 用环境变量引用接入,也可在 TUI 用 /connect 交互保存(凭据落 ~/.local/share/opencode/auth.json)。
|
||||||
configuration:
|
configuration:
|
||||||
files:
|
files:
|
||||||
- path: "~/.config/opencode/opencode.json"
|
- path: "~/.config/opencode/opencode.json"
|
||||||
@@ -95,18 +98,31 @@ configuration:
|
|||||||
fields:
|
fields:
|
||||||
- id: model
|
- id: model
|
||||||
label_zh: 默认模型
|
label_zh: 默认模型
|
||||||
help_zh: OpenCode 使用的默认模型
|
group: common
|
||||||
|
help_zh: 官方键 model,全局默认模型(如 anthropic/claude-sonnet-4)
|
||||||
required: false
|
required: false
|
||||||
sensitive: false
|
sensitive: false
|
||||||
type: string
|
type: string
|
||||||
storage: file
|
storage: file
|
||||||
|
docs_url: https://opencode.ai/docs/config/
|
||||||
|
- id: provider
|
||||||
|
label_zh: 默认 Provider
|
||||||
|
group: common
|
||||||
|
help_zh: 官方键 provider,指定默认模型提供方(可选;留空则用模型名推断)
|
||||||
|
required: false
|
||||||
|
sensitive: false
|
||||||
|
type: string
|
||||||
|
storage: file
|
||||||
|
docs_url: https://opencode.ai/docs/providers/
|
||||||
- id: api_key
|
- id: api_key
|
||||||
label_zh: API Key
|
label_zh: API Key
|
||||||
help_zh: 存入系统密钥库(对应 OPENAI_API_KEY / ANTHROPIC_API_KEY)
|
group: auth
|
||||||
|
help_zh: 存入系统密钥库(对应 OPENAI_API_KEY / ANTHROPIC_API_KEY 等环境变量);也可在 TUI 用 /connect 交互保存
|
||||||
required: false
|
required: false
|
||||||
sensitive: true
|
sensitive: true
|
||||||
type: string
|
type: string
|
||||||
storage: keyring
|
storage: keyring
|
||||||
|
docs_url: https://opencode.ai/docs/providers/
|
||||||
|
|
||||||
diagnostics:
|
diagnostics:
|
||||||
- rule_id: path.not_installed
|
- rule_id: path.not_installed
|
||||||
@@ -115,14 +131,38 @@ diagnostics:
|
|||||||
- rule_id: config.corrupt
|
- rule_id: config.corrupt
|
||||||
|
|
||||||
documentation:
|
documentation:
|
||||||
quickstart_zh: 安装后运行 `opencode` 或 `opencode run "提示词"` 无头执行。
|
quickstart_zh: 安装后运行 `opencode`,或用 `opencode run "提示词"` 无头执行。
|
||||||
|
install_zh: 多渠道:npm `opencode-ai`(本适配器默认)、Windows choco/scoop、Linux 官方脚本、Homebrew。桌面版另有原生 .exe 安装器。
|
||||||
|
auth_zh: 授权面最全:① 纯 API Key(各家环境变量);② 浏览器 OAuth(Claude Pro/Max、ChatGPT 订阅);③ 设备码(GitHub Copilot,github.com/login/device 输码);④ 企业级 OAuth。不绑定特定厂商。
|
||||||
commands:
|
commands:
|
||||||
- cmd: opencode
|
- cmd: opencode
|
||||||
desc_zh: 启动交互式会话
|
desc_zh: 启动交互式会话
|
||||||
- cmd: opencode run "提示词"
|
- cmd: opencode run "提示词"
|
||||||
desc_zh: 无头单次执行
|
desc_zh: 无头单次执行
|
||||||
|
- cmd: opencode run "提示词" --model <模型>
|
||||||
|
desc_zh: 指定模型无头执行
|
||||||
|
- cmd: opencode auth login
|
||||||
|
desc_zh: 浏览器 OAuth 登录
|
||||||
- cmd: opencode auth list
|
- cmd: opencode auth list
|
||||||
desc_zh: 查看已配置凭据
|
desc_zh: 查看已配置凭据
|
||||||
updated_at: "2026-08-24"
|
- cmd: opencode upgrade
|
||||||
|
desc_zh: 自升级
|
||||||
|
- cmd: opencode serve
|
||||||
|
desc_zh: 无头 HTTP 服务器模式
|
||||||
|
- cmd: opencode --version
|
||||||
|
desc_zh: 显示版本号
|
||||||
|
params:
|
||||||
|
- param: run "提示词"
|
||||||
|
desc_zh: 一次性执行后退出
|
||||||
|
- param: --model
|
||||||
|
desc_zh: 指定模型
|
||||||
|
- param: --format json
|
||||||
|
desc_zh: JSON 事件流输出
|
||||||
|
- param: --auto-approve
|
||||||
|
desc_zh: 自动批准权限请求
|
||||||
|
- param: -v / --version
|
||||||
|
desc_zh: 显示版本号
|
||||||
|
updated_at: "2026-08-25"
|
||||||
risks_zh:
|
risks_zh:
|
||||||
- 官方未文档化安装包校验(SHA256/签名),适配器不强制校验
|
- 官方未文档化安装包校验(SHA256/签名),适配器不强制校验
|
||||||
|
- 每个 provider 的 baseURL 高级配置(本地 Ollama/LM Studio 等)本表单不覆盖,以官方 providers 文档为准
|
||||||
|
|||||||
@@ -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, AuthStatus, ConfigFormState, DetectResult, Engine, WriteResult};
|
use agentdock_core::{ActionEvent, ActionOpts, AuthStatus, ConfigFormState, ConfigVerifyResult, DetectResult, Engine, WriteResult};
|
||||||
use agentdock_diag::DiagnosticReport;
|
use agentdock_diag::DiagnosticReport;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
@@ -37,6 +37,12 @@ pub fn detect_cli(id: String, state: tauri::State<'_, Engine>) -> Result<DetectR
|
|||||||
state.detect(&id).map_err(|e| e.to_string())
|
state.detect(&id).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 批量检测全部 CLI(detectCliAll),供总览/目录/我的 CLI 接真机状态。
|
||||||
|
#[tauri::command(rename = "detectCliAll")]
|
||||||
|
pub fn detect_cli_all(state: tauri::State<'_, Engine>) -> Result<Vec<DetectResult>, String> {
|
||||||
|
state.detect_all().map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// 干燥运行(previewAction):返回将执行的命令与影响面,不真正执行。
|
/// 干燥运行(previewAction):返回将执行的命令与影响面,不真正执行。
|
||||||
#[tauri::command(rename = "previewAction")]
|
#[tauri::command(rename = "previewAction")]
|
||||||
pub fn preview_action(
|
pub fn preview_action(
|
||||||
@@ -107,6 +113,12 @@ pub fn write_config(
|
|||||||
state.write_config(&id, &patch).map_err(|e| e.to_string())
|
state.write_config(&id, &patch).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 配置写入后的「生效检查」(verifyConfig):重读配置文件 + CLI 版本探测。
|
||||||
|
#[tauri::command(rename = "verifyConfig")]
|
||||||
|
pub fn verify_config(id: String, state: tauri::State<'_, Engine>) -> Result<ConfigVerifyResult, String> {
|
||||||
|
state.verify_config(&id).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// 授权状态(authStatus)。
|
/// 授权状态(authStatus)。
|
||||||
#[tauri::command(rename = "authStatus")]
|
#[tauri::command(rename = "authStatus")]
|
||||||
pub fn auth_status(id: String, state: tauri::State<'_, Engine>) -> Result<AuthStatus, String> {
|
pub fn auth_status(id: String, state: tauri::State<'_, Engine>) -> Result<AuthStatus, String> {
|
||||||
|
|||||||
@@ -48,11 +48,13 @@ pub fn run() {
|
|||||||
commands::env::detect_env,
|
commands::env::detect_env,
|
||||||
commands::catalog::list_catalog,
|
commands::catalog::list_catalog,
|
||||||
commands::cli::detect_cli,
|
commands::cli::detect_cli,
|
||||||
|
commands::cli::detect_cli_all,
|
||||||
commands::cli::get_adapter,
|
commands::cli::get_adapter,
|
||||||
commands::cli::preview_action,
|
commands::cli::preview_action,
|
||||||
commands::cli::run_action,
|
commands::cli::run_action,
|
||||||
commands::cli::read_config,
|
commands::cli::read_config,
|
||||||
commands::cli::write_config,
|
commands::cli::write_config,
|
||||||
|
commands::cli::verify_config,
|
||||||
commands::cli::auth_status,
|
commands::cli::auth_status,
|
||||||
commands::cli::diagnose,
|
commands::cli::diagnose,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
switch (page) {
|
switch (page) {
|
||||||
case "overview":
|
case "overview":
|
||||||
return <OverviewPage onNavigate={navigate} />;
|
return <OverviewPage onNavigate={navigate} onOpenDetail={setDetailCliId} />;
|
||||||
case "catalog":
|
case "catalog":
|
||||||
return <CatalogPage onOpenDetail={setDetailCliId} />;
|
return <CatalogPage onOpenDetail={setDetailCliId} />;
|
||||||
case "my-cli":
|
case "my-cli":
|
||||||
return <MyCliPage onNavigate={navigate} />;
|
return <MyCliPage onNavigate={navigate} onOpenDetail={setDetailCliId} />;
|
||||||
case "config":
|
case "config":
|
||||||
return <ConfigCenterPage onNavigate={navigate} />;
|
return <ConfigCenterPage onNavigate={navigate} />;
|
||||||
case "backup":
|
case "backup":
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Check, Eye, EyeOff, Lock } from "lucide-react";
|
import { Check, ChevronDown, CircleAlert, ExternalLink, Eye, EyeOff, Lock, ShieldCheck } from "lucide-react";
|
||||||
import { readConfig, writeConfig } from "../ipc";
|
import { readConfig, verifyConfig, writeConfig } from "../ipc";
|
||||||
import type { ConfigFieldState, ConfigFormState, WriteResult } from "../ipc/types";
|
import type {
|
||||||
|
ConfigFieldState,
|
||||||
|
ConfigFormState,
|
||||||
|
ConfigVerifyResult,
|
||||||
|
WriteResult,
|
||||||
|
} from "../ipc/types";
|
||||||
import { MonoChip } from "./MonoChip";
|
import { MonoChip } from "./MonoChip";
|
||||||
|
|
||||||
/** 配置表单(视觉规范 §3.4:单列 ≤640px、敏感字段密码框 + 密钥库说明、吸底保存条) */
|
/** 官方授权方式中文名 */
|
||||||
|
function authModeLabel(mode: string): string {
|
||||||
|
switch (mode) {
|
||||||
|
case "browser_oauth":
|
||||||
|
return "账号授权(浏览器)";
|
||||||
|
case "device_code":
|
||||||
|
return "设备码";
|
||||||
|
case "api_key":
|
||||||
|
return "API Key";
|
||||||
|
case "local_tui":
|
||||||
|
return "本机终端授权";
|
||||||
|
default:
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 配置表单(视觉规范 §3.4:单列 ≤640px、敏感字段密码框 + 密钥库说明、吸底保存条)
|
||||||
|
* Wave 2.1:字段按官方配置方法渲染 + 保存后「生效检查」 */
|
||||||
export function ConfigForm({ id }: { id: string }) {
|
export function ConfigForm({ id }: { id: string }) {
|
||||||
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>>({});
|
||||||
@@ -13,7 +35,9 @@ export function ConfigForm({ id }: { id: string }) {
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [saved, setSaved] = useState(false);
|
const [saved, setSaved] = useState(false);
|
||||||
const [result, setResult] = useState<WriteResult | null>(null);
|
const [result, setResult] = useState<WriteResult | 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);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -52,11 +76,29 @@ export function ConfigForm({ id }: { id: string }) {
|
|||||||
if (!state) return null;
|
if (!state) return null;
|
||||||
|
|
||||||
const hasFields = state.fields.length > 0;
|
const hasFields = state.fields.length > 0;
|
||||||
|
const authFields = state.fields.filter((f) => f.group === "auth");
|
||||||
|
const commonFields = state.fields.filter((f) => f.group === "common");
|
||||||
|
const advancedFields = state.fields.filter((f) => f.group === "advanced");
|
||||||
|
|
||||||
|
function renderField(field: ConfigFieldState) {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
key={field.id}
|
||||||
|
field={field}
|
||||||
|
value={values[field.id] ?? ""}
|
||||||
|
saved={sensitiveSaved[field.id] ?? false}
|
||||||
|
revealed={!!revealed[field.id]}
|
||||||
|
onToggleReveal={() => setRevealed((r) => ({ ...r, [field.id]: !r[field.id] }))}
|
||||||
|
onChange={(v) => setValues((x) => ({ ...x, [field.id]: v }))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function onSave() {
|
async function onSave() {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setSaved(false);
|
setSaved(false);
|
||||||
setResult(null);
|
setResult(null);
|
||||||
|
setVerify(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
const patch: Record<string, string> = {};
|
const patch: Record<string, string> = {};
|
||||||
for (const f of state!.fields) {
|
for (const f of state!.fields) {
|
||||||
@@ -78,6 +120,13 @@ export function ConfigForm({ id }: { id: string }) {
|
|||||||
// 刷新回显
|
// 刷新回显
|
||||||
const s = await readConfig(id);
|
const s = await readConfig(id);
|
||||||
setState(s);
|
setState(s);
|
||||||
|
// 生效检查:写回的配置能否被 CLI 接受(配置文件解析 + CLI 版本探测)
|
||||||
|
try {
|
||||||
|
const v = await verifyConfig(id);
|
||||||
|
setVerify(v);
|
||||||
|
} catch {
|
||||||
|
setVerify(null);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(String(e));
|
setError(String(e));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -89,17 +138,59 @@ export function ConfigForm({ id }: { id: string }) {
|
|||||||
<div className="config-form">
|
<div className="config-form">
|
||||||
{!hasFields && <p className="panel-empty">该 CLI 未声明可配置的中文表单字段。</p>}
|
{!hasFields && <p className="panel-empty">该 CLI 未声明可配置的中文表单字段。</p>}
|
||||||
|
|
||||||
{state.fields.map((field) => (
|
{/* 第一层:授权 / 登录(官方授权方式引导 + API Key 类字段) */}
|
||||||
<Field
|
{(state.auth_modes.length > 0 || authFields.length > 0) && (
|
||||||
key={field.id}
|
<section className="config-section">
|
||||||
field={field}
|
<h4 className="config-section-title">授权 / 登录</h4>
|
||||||
value={values[field.id] ?? ""}
|
{state.auth_modes.length > 0 && (
|
||||||
saved={sensitiveSaved[field.id] ?? false}
|
<ul className="auth-modes">
|
||||||
revealed={!!revealed[field.id]}
|
{state.auth_modes.map((m) => (
|
||||||
onToggleReveal={() => setRevealed((r) => ({ ...r, [field.id]: !r[field.id] }))}
|
<li key={m.mode} className="auth-mode">
|
||||||
onChange={(v) => setValues((x) => ({ ...x, [field.id]: v }))}
|
<span className="auth-mode-name">{authModeLabel(m.mode)}</span>
|
||||||
/>
|
{m.notes_zh && <span className="auth-mode-note">{m.notes_zh}</span>}
|
||||||
))}
|
{m.command.length > 0 && (
|
||||||
|
<span className="auth-mode-cmd">
|
||||||
|
<MonoChip>{m.command.join(" ")}</MonoChip>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{authFields.map(renderField)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 第二层:常用配置 */}
|
||||||
|
{commonFields.length > 0 && (
|
||||||
|
<section className="config-section">
|
||||||
|
<h4 className="config-section-title">常用配置</h4>
|
||||||
|
{commonFields.map(renderField)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 第三层:高级配置(默认折叠) */}
|
||||||
|
{advancedFields.length > 0 && (
|
||||||
|
<section className="config-section">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="config-advanced-toggle"
|
||||||
|
onClick={() => setAdvancedOpen((v) => !v)}
|
||||||
|
aria-expanded={advancedOpen}
|
||||||
|
>
|
||||||
|
<span className="config-section-title">高级配置</span>
|
||||||
|
<span className="advanced-badge">高级</span>
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
className={advancedOpen ? "advanced-chevron open" : "advanced-chevron"}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<p className="advanced-hint">以下为官方支持但少数用户使用的高级项,一般无需修改。</p>
|
||||||
|
{advancedOpen && <div className="advanced-fields">{advancedFields.map(renderField)}</div>}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{state.files.length > 0 && (
|
{state.files.length > 0 && (
|
||||||
<div className="config-file-info">
|
<div className="config-file-info">
|
||||||
@@ -121,6 +212,16 @@ export function ConfigForm({ id }: { id: string }) {
|
|||||||
<Check size={14} strokeWidth={1.5} aria-hidden="true" /> 已保存
|
<Check size={14} strokeWidth={1.5} aria-hidden="true" /> 已保存
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{verify && (
|
||||||
|
<span className={verify.ok ? "verify-ok" : "verify-fail"}>
|
||||||
|
{verify.ok ? (
|
||||||
|
<ShieldCheck size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<CircleAlert size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
{verify.message_zh}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{result?.backup_path && (
|
{result?.backup_path && (
|
||||||
<span className="save-detail">
|
<span className="save-detail">
|
||||||
已备份原文件 · {result.backup_path.split(/[\\/]/).pop()}
|
已备份原文件 · {result.backup_path.split(/[\\/]/).pop()}
|
||||||
@@ -158,7 +259,8 @@ function Field({
|
|||||||
onToggleReveal: () => void;
|
onToggleReveal: () => void;
|
||||||
}) {
|
}) {
|
||||||
const isPassword = field.sensitive;
|
const isPassword = field.sensitive;
|
||||||
const inputType = isPassword ? (revealed ? "text" : "password") : field.field_type === "url" ? "text" : "text";
|
const isEnum = field.field_type === "enum";
|
||||||
|
const inputType = isPassword ? (revealed ? "text" : "password") : "text";
|
||||||
return (
|
return (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="field-label" htmlFor={`field-${field.id}`}>
|
<label className="field-label" htmlFor={`field-${field.id}`}>
|
||||||
@@ -171,15 +273,31 @@ function Field({
|
|||||||
<Lock size={12} strokeWidth={1.5} aria-hidden="true" />
|
<Lock size={12} strokeWidth={1.5} aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<input
|
{isEnum ? (
|
||||||
id={`field-${field.id}`}
|
<select
|
||||||
type={inputType}
|
id={`field-${field.id}`}
|
||||||
value={value}
|
className="field-select"
|
||||||
placeholder={isPassword ? (saved ? "已保存 · 留空则不变" : "输入 API Key") : ""}
|
value={value}
|
||||||
autoComplete="off"
|
onChange={(e) => onChange(e.target.value)}
|
||||||
spellCheck={false}
|
>
|
||||||
onChange={(e) => onChange(e.target.value)}
|
<option value="">(使用 CLI 默认)</option>
|
||||||
/>
|
{field.options.map((o) => (
|
||||||
|
<option key={o.value} value={o.value}>
|
||||||
|
{o.label_zh}({o.value})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
id={`field-${field.id}`}
|
||||||
|
type={inputType}
|
||||||
|
value={value}
|
||||||
|
placeholder={isPassword ? (saved ? "已保存 · 留空则不变" : "输入 API Key") : ""}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isPassword && (
|
{isPassword && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -196,9 +314,16 @@ function Field({
|
|||||||
{field.help_zh}
|
{field.help_zh}
|
||||||
{isPassword && (
|
{isPassword && (
|
||||||
<span className="field-keyring-note">
|
<span className="field-keyring-note">
|
||||||
{saved ? " · 已加密保存于系统密钥库" : " · 保存后写入系统密钥库,绝不明文落盘"}
|
{saved
|
||||||
|
? ` · 已加密保存于系统密钥库${field.env_key ? `(${field.env_key})` : ""}`
|
||||||
|
: ` · 保存后写入系统密钥库,绝不明文落盘${field.env_key ? `(${field.env_key})` : ""}`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{field.docs_url && (
|
||||||
|
<a className="field-docs-link" href={field.docs_url} target="_blank" rel="noreferrer">
|
||||||
|
官方文档 <ExternalLink size={11} strokeWidth={1.5} aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 顶层错误边界:任何渲染崩溃都不再整屏黑屏,而是给出可恢复提示 */
|
||||||
|
export class ErrorBoundary extends Component<Props, State> {
|
||||||
|
state: State = { error: null };
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||||
|
// 记录到控制台便于排查,不含敏感信息
|
||||||
|
console.error("[agentdock] render error:", error, info.componentStack);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.error) {
|
||||||
|
return (
|
||||||
|
<div className="crash-screen">
|
||||||
|
<h1 className="crash-title">界面出现异常</h1>
|
||||||
|
<p className="crash-desc">页面渲染时发生错误,点击下方按钮可返回恢复。</p>
|
||||||
|
<p className="crash-detail">{this.state.error.message}</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => this.setState({ error: null })}
|
||||||
|
>
|
||||||
|
重新加载界面
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { detectCliAll } from "../ipc";
|
||||||
|
import type { DetectResult } from "../ipc/types";
|
||||||
|
|
||||||
|
export interface UseDetectAllResult {
|
||||||
|
detectMap: Record<string, DetectResult>;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量检测全部 CLI(总览/目录/我的 CLI 接真机状态) */
|
||||||
|
export function useDetectAll(): UseDetectAllResult {
|
||||||
|
const [detectMap, setDetectMap] = useState<Record<string, DetectResult>>({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const list = await detectCliAll();
|
||||||
|
const map: Record<string, DetectResult> = {};
|
||||||
|
for (const d of list) map[d.cli_id] = d;
|
||||||
|
setDetectMap(map);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(String(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refresh();
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
|
return { detectMap, loading, error, refresh };
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
CliAction,
|
CliAction,
|
||||||
CliActionEvent,
|
CliActionEvent,
|
||||||
ConfigFormState,
|
ConfigFormState,
|
||||||
|
ConfigVerifyResult,
|
||||||
DetectResult,
|
DetectResult,
|
||||||
DiagnosticReport,
|
DiagnosticReport,
|
||||||
DryRunPlan,
|
DryRunPlan,
|
||||||
@@ -33,6 +34,7 @@ export async function listCatalog(): Promise<CatalogEntry[]> {
|
|||||||
// ---- Wave 2:CLI 全链路 ----
|
// ---- Wave 2:CLI 全链路 ----
|
||||||
|
|
||||||
export async function getAdapter(id: string): Promise<Adapter> {
|
export async function getAdapter(id: string): Promise<Adapter> {
|
||||||
|
if (!isTauri()) return mockAdapter(id);
|
||||||
return invoke<Adapter>("getAdapter", { id });
|
return invoke<Adapter>("getAdapter", { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,19 +43,27 @@ export async function detectCli(id: string): Promise<DetectResult> {
|
|||||||
return invoke<DetectResult>("detectCli", { id });
|
return invoke<DetectResult>("detectCli", { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function detectCliAll(): Promise<DetectResult[]> {
|
||||||
|
if (!isTauri()) return mockCatalog().map((c) => mockDetect(c.id));
|
||||||
|
return invoke<DetectResult[]>("detectCliAll");
|
||||||
|
}
|
||||||
|
|
||||||
export async function previewAction(
|
export async function previewAction(
|
||||||
id: string,
|
id: string,
|
||||||
action: CliAction,
|
action: CliAction,
|
||||||
channel?: string,
|
channel?: string,
|
||||||
): Promise<DryRunPlan> {
|
): Promise<DryRunPlan> {
|
||||||
|
if (!isTauri()) return mockDryRun(id, action);
|
||||||
return invoke<DryRunPlan>("previewAction", { id, action, channel });
|
return invoke<DryRunPlan>("previewAction", { id, action, channel });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runAction(id: string, action: CliAction, channel?: string): Promise<void> {
|
export async function runAction(id: string, action: CliAction, channel?: string): Promise<void> {
|
||||||
|
if (!isTauri()) return;
|
||||||
return invoke<void>("runAction", { id, action, channel });
|
return invoke<void>("runAction", { id, action, channel });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readConfig(id: string): Promise<ConfigFormState> {
|
export async function readConfig(id: string): Promise<ConfigFormState> {
|
||||||
|
if (!isTauri()) return mockConfig(id);
|
||||||
return invoke<ConfigFormState>("readConfig", { id });
|
return invoke<ConfigFormState>("readConfig", { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,15 +71,30 @@ export async function writeConfig(
|
|||||||
id: string,
|
id: string,
|
||||||
patch: Record<string, string>,
|
patch: Record<string, string>,
|
||||||
): Promise<WriteResult> {
|
): Promise<WriteResult> {
|
||||||
|
if (!isTauri()) return { backup_path: null, written_file: "~/.codex/config.toml", written_fields: Object.keys(patch), errors: [] };
|
||||||
return invoke<WriteResult>("writeConfig", { id, patch });
|
return invoke<WriteResult>("writeConfig", { id, patch });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function verifyConfig(id: string): Promise<ConfigVerifyResult> {
|
||||||
|
if (!isTauri()) {
|
||||||
|
return {
|
||||||
|
cli_id: id,
|
||||||
|
ok: true,
|
||||||
|
level: "config_parsed",
|
||||||
|
message_zh: "配置文件已写入且可解析;CLI 未安装,未做 CLI 实际校验(以官方文档为准)。",
|
||||||
|
detail: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return invoke<ConfigVerifyResult>("verifyConfig", { id });
|
||||||
|
}
|
||||||
|
|
||||||
export async function authStatus(id: string): Promise<AuthStatus> {
|
export async function authStatus(id: string): Promise<AuthStatus> {
|
||||||
if (!isTauri()) return mockAuth(id);
|
if (!isTauri()) return mockAuth(id);
|
||||||
return invoke<AuthStatus>("authStatus", { id });
|
return invoke<AuthStatus>("authStatus", { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function diagnose(id: string): Promise<DiagnosticReport> {
|
export async function diagnose(id: string): Promise<DiagnosticReport> {
|
||||||
|
if (!isTauri()) return { cli_id: id, findings: [] };
|
||||||
return invoke<DiagnosticReport>("diagnose", { id });
|
return invoke<DiagnosticReport>("diagnose", { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,3 +161,98 @@ const mockPlatformEnv = (): PlatformEnv => ({
|
|||||||
path_entries: ["C:\\Windows\\system32", "C:\\Windows", "C:\\Program Files\\nodejs"],
|
path_entries: ["C:\\Windows\\system32", "C:\\Windows", "C:\\Program Files\\nodejs"],
|
||||||
capabilities: { keyring: "ok", can_elevate: false },
|
capabilities: { keyring: "ok", can_elevate: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockAdapter = (id: string): Adapter => ({
|
||||||
|
id,
|
||||||
|
name: "Codex CLI",
|
||||||
|
name_zh: "Codex CLI",
|
||||||
|
vendor: "OpenAI",
|
||||||
|
status: "available",
|
||||||
|
adapter_version: "1.1.0",
|
||||||
|
license: "Apache-2.0",
|
||||||
|
platforms: {
|
||||||
|
windows: { architectures: ["x64"], notes: "原生支持(PowerShell 脚本安装)" },
|
||||||
|
linux: { distributions: ["ubuntu"], architectures: ["x64"] },
|
||||||
|
},
|
||||||
|
official: {
|
||||||
|
homepage: "https://github.com/openai/codex",
|
||||||
|
docs: "https://learn.chatgpt.com/docs/codex/cli",
|
||||||
|
allowed_hosts: ["chatgpt.com"],
|
||||||
|
},
|
||||||
|
runtime_deps: [],
|
||||||
|
install: {
|
||||||
|
preferred: "npm",
|
||||||
|
channels: [
|
||||||
|
{ id: "npm", platforms: ["windows", "linux"], command: ["npm", "install", "-g", "@openai/codex"], package: "@openai/codex", elevate: "never", post_checks: ["detect"] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
detect: { executable: "codex", version_args: ["--version"], version_regex: "^codex-cli (\\d+\\.\\d+\\.\\d+)", version_unconfirmed: true },
|
||||||
|
update: { method: "npm_update", command: ["npm", "update", "-g", "@openai/codex"] },
|
||||||
|
uninstall: { method: "npm_uninstall", command: ["npm", "uninstall", "-g", "@openai/codex"], keep_config_default: true },
|
||||||
|
authorization: {
|
||||||
|
modes: [
|
||||||
|
{ mode: "browser_oauth", command: ["codex", "login"], status_command: ["codex", "login", "status"], notes_zh: "浏览器登录 ChatGPT 账号" },
|
||||||
|
{ mode: "device_code", command: ["codex", "login", "--device-auth"], notes_zh: "设备码登录" },
|
||||||
|
{ mode: "api_key", command: ["codex", "login", "--with-api-key"], env_keys: ["OPENAI_API_KEY"], notes_zh: "API Key 经 stdin 注入" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
configuration: {
|
||||||
|
files: [{ path: "~/.codex/config.toml", format: "toml", scope: "user" }],
|
||||||
|
environment: [{ key: "OPENAI_API_KEY", sensitive: true, maps_to_field: "api_key" }],
|
||||||
|
fields: [
|
||||||
|
{ id: "model", label_zh: "默认模型", help_zh: "官方键 model,如 gpt-5.6-terra", type: "string", storage: "file", docs_url: "https://learn.chatgpt.com/docs/config-file/config-basic" },
|
||||||
|
{ id: "approval_policy", label_zh: "审批策略", help_zh: "官方键 approval_policy", type: "enum", storage: "file", docs_url: "https://learn.chatgpt.com/docs/config-file/config-basic", options: [{ value: "untrusted", label_zh: "不信任(全部确认)" }, { value: "on-failure", label_zh: "失败时确认" }, { value: "never", label_zh: "永不确认" }] },
|
||||||
|
{ id: "openai_base_url", label_zh: "Base URL", help_zh: "官方简化键 openai_base_url", type: "url", storage: "file", docs_url: "https://learn.chatgpt.com/docs/config-file/config-advanced" },
|
||||||
|
{ id: "api_key", label_zh: "API Key", help_zh: "存入系统密钥库(对应 OPENAI_API_KEY)", sensitive: true, type: "string", storage: "keyring", docs_url: "https://learn.chatgpt.com/docs/config-file/config-advanced" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
documentation: {
|
||||||
|
quickstart_zh: "安装后运行 `codex` 登录,或用 `codex exec \"任务\"` 无头执行。",
|
||||||
|
install_zh: "推荐 `npm install -g @openai/codex`;也可走官方 PowerShell 脚本或 GitHub Releases 二进制。",
|
||||||
|
auth_zh: "三种授权:浏览器登录、设备码、API Key。",
|
||||||
|
commands: [
|
||||||
|
{ cmd: "codex", desc_zh: "启动交互式会话" },
|
||||||
|
{ cmd: "codex exec \"提示词\"", desc_zh: "无头单次执行" },
|
||||||
|
{ cmd: "codex exec --json \"提示词\"", desc_zh: "JSONL 事件流输出" },
|
||||||
|
{ cmd: "codex exec --output-schema <schema>", desc_zh: "约束输出 JSON Schema" },
|
||||||
|
{ cmd: "codex exec -o 文件", desc_zh: "输出写入文件" },
|
||||||
|
{ cmd: "codex exec --sandbox", desc_zh: "沙箱执行" },
|
||||||
|
{ cmd: "codex login", desc_zh: "浏览器登录" },
|
||||||
|
{ cmd: "codex login status", desc_zh: "查询登录状态" },
|
||||||
|
],
|
||||||
|
params: [
|
||||||
|
{ param: "--json", desc_zh: "JSONL 事件流输出" },
|
||||||
|
{ param: "--output-schema", desc_zh: "约束输出 JSON Schema" },
|
||||||
|
{ param: "-o / --output", desc_zh: "输出写入文件" },
|
||||||
|
{ param: "--ephemeral", desc_zh: "临时会话" },
|
||||||
|
{ param: "--sandbox", desc_zh: "沙箱执行" },
|
||||||
|
],
|
||||||
|
updated_at: "2026-08-25",
|
||||||
|
risks_zh: ["--version 官方文档未确认,适配器已标 version_unconfirmed 并实测兜底"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockConfig = (id: string): ConfigFormState => ({
|
||||||
|
cli_id: id,
|
||||||
|
files: [{ path: "~/.codex/config.toml", format: "toml", scope: "user", exists: false, parse_ok: true, error: null }],
|
||||||
|
fields: [
|
||||||
|
{ id: "model", label_zh: "默认模型", help_zh: "官方键 model,如 gpt-5.6-terra", required: false, sensitive: false, field_type: "string", storage: "file", value: null, has_value: false, docs_url: "https://learn.chatgpt.com/docs/config-file/config-basic", options: [], env_key: null, group: "common" },
|
||||||
|
{ id: "approval_policy", label_zh: "审批策略", help_zh: "官方键 approval_policy", required: false, sensitive: false, field_type: "enum", storage: "file", value: null, has_value: false, docs_url: "https://learn.chatgpt.com/docs/config-file/config-basic", options: [{ value: "untrusted", label_zh: "不信任(全部确认)" }, { value: "on-failure", label_zh: "失败时确认" }, { value: "never", label_zh: "永不确认" }], env_key: null, group: "advanced" },
|
||||||
|
{ id: "openai_base_url", label_zh: "Base URL", help_zh: "官方简化键 openai_base_url", required: false, sensitive: false, field_type: "url", storage: "file", value: null, has_value: false, docs_url: "https://learn.chatgpt.com/docs/config-file/config-advanced", options: [], env_key: null, group: "advanced" },
|
||||||
|
{ id: "api_key", label_zh: "API Key", help_zh: "存入系统密钥库(对应 OPENAI_API_KEY)", required: false, sensitive: true, field_type: "string", storage: "keyring", value: null, has_value: false, docs_url: "https://learn.chatgpt.com/docs/config-file/config-advanced", options: [], env_key: "OPENAI_API_KEY", group: "auth" },
|
||||||
|
],
|
||||||
|
environment: [{ key: "OPENAI_API_KEY", sensitive: true, maps_to_field: "api_key" }],
|
||||||
|
auth_modes: [
|
||||||
|
{ mode: "browser_oauth", notes_zh: "浏览器登录 ChatGPT 账号(需订阅计划)", command: ["codex", "login"] },
|
||||||
|
{ mode: "device_code", notes_zh: "设备码登录,不弹浏览器", command: ["codex", "login", "--device-auth"] },
|
||||||
|
{ mode: "api_key", notes_zh: "API Key 从系统密钥库读取,经 stdin 注入", command: [] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockDryRun = (_id: string, action: CliAction): DryRunPlan => ({
|
||||||
|
commands: action === "install" ? [["npm", "install", "-g", "@openai/codex"]] : [["npm", "uninstall", "-g", "@openai/codex"]],
|
||||||
|
elevate: false,
|
||||||
|
elevate_reason_zh: null,
|
||||||
|
affected_files: ["~/.codex/config.toml"],
|
||||||
|
rollback_zh: action === "install" ? "可执行卸载命令回退;配置文件默认保留。" : "卸载默认保留配置,可重新安装恢复。",
|
||||||
|
});
|
||||||
|
|||||||
@@ -97,6 +97,15 @@ export interface ConfigFieldState {
|
|||||||
storage: string;
|
storage: string;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
has_value: boolean;
|
has_value: boolean;
|
||||||
|
docs_url: string | null;
|
||||||
|
options: ConfigFieldOption[];
|
||||||
|
env_key: string | null;
|
||||||
|
group: "auth" | "common" | "advanced" | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigFieldOption {
|
||||||
|
value: string;
|
||||||
|
label_zh: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigFileState {
|
export interface ConfigFileState {
|
||||||
@@ -119,6 +128,13 @@ export interface ConfigFormState {
|
|||||||
files: ConfigFileState[];
|
files: ConfigFileState[];
|
||||||
fields: ConfigFieldState[];
|
fields: ConfigFieldState[];
|
||||||
environment: EnvFieldState[];
|
environment: EnvFieldState[];
|
||||||
|
auth_modes: AuthModeInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthModeInfo {
|
||||||
|
mode: string;
|
||||||
|
notes_zh: string | null;
|
||||||
|
command: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WriteResult {
|
export interface WriteResult {
|
||||||
@@ -128,6 +144,14 @@ export interface WriteResult {
|
|||||||
errors: string[];
|
errors: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfigVerifyResult {
|
||||||
|
cli_id: string;
|
||||||
|
ok: boolean;
|
||||||
|
level: "cli_accepted" | "config_parsed" | "config_parse_failed" | "not_installed";
|
||||||
|
message_zh: string;
|
||||||
|
detail: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DryRunPlan {
|
export interface DryRunPlan {
|
||||||
commands: string[][];
|
commands: string[][];
|
||||||
elevate: boolean;
|
elevate: boolean;
|
||||||
@@ -141,6 +165,7 @@ export type ActionEventKind = "step" | "stdout" | "stderr" | "done" | "error";
|
|||||||
export interface ActionEvent {
|
export interface ActionEvent {
|
||||||
kind: ActionEventKind;
|
kind: ActionEventKind;
|
||||||
message: string;
|
message: string;
|
||||||
|
phase: string | null;
|
||||||
data: unknown;
|
data: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,11 +245,16 @@ export interface Adapter {
|
|||||||
storage: string;
|
storage: string;
|
||||||
platforms?: string[];
|
platforms?: string[];
|
||||||
docs_url?: string | null;
|
docs_url?: string | null;
|
||||||
|
options?: { value: string; label_zh: string }[];
|
||||||
|
group?: string | null;
|
||||||
}[];
|
}[];
|
||||||
} | null;
|
} | null;
|
||||||
documentation?: {
|
documentation?: {
|
||||||
quickstart_zh?: string | null;
|
quickstart_zh?: string | null;
|
||||||
|
install_zh?: string | null;
|
||||||
|
auth_zh?: string | null;
|
||||||
commands?: { cmd: string; desc_zh?: string | null }[];
|
commands?: { cmd: string; desc_zh?: string | null }[];
|
||||||
|
params?: { param: string; desc_zh?: string | null }[];
|
||||||
updated_at?: string | null;
|
updated_at?: string | null;
|
||||||
risks_zh?: string[];
|
risks_zh?: string[];
|
||||||
} | null;
|
} | null;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||||
|
|
||||||
// 设计 Token(视觉规范 v1.2,唯一品味依据)
|
// 设计 Token(视觉规范 v1.2,唯一品味依据)
|
||||||
import "./tokens/color.css";
|
import "./tokens/color.css";
|
||||||
@@ -25,6 +26,8 @@ applyFxTier();
|
|||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ErrorBoundary>
|
||||||
|
<App />
|
||||||
|
</ErrorBoundary>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { 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 { StatusBadge } from "../components/StatusBadge";
|
import { StatusBadge } from "../components/StatusBadge";
|
||||||
import { CliMonogram } from "../components/CliMonogram";
|
import { CliMonogram } from "../components/CliMonogram";
|
||||||
|
import { MonoChip } from "../components/MonoChip";
|
||||||
|
|
||||||
type StatusFilter = "all" | "installed" | "uninstalled" | "update";
|
type StatusFilter = "all" | "installed" | "uninstalled" | "update";
|
||||||
type PlatformFilter = "all" | "windows" | "linux";
|
type PlatformFilter = "all" | "windows" | "linux";
|
||||||
@@ -20,9 +22,10 @@ const PLATFORM_FILTERS: { key: PlatformFilter; label: string }[] = [
|
|||||||
{ key: "linux", label: "Linux" },
|
{ key: "linux", label: "Linux" },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** CLI 目录页(视觉规范 §3.2 + PRD §7):搜索 + 筛选 + 14 张卡片(数据来自 catalog.yaml) */
|
/** CLI 目录页(视觉规范 §3.2 + PRD §7):搜索 + 筛选 + 14 张卡片(数据来自 catalog.yaml + 真机 detect) */
|
||||||
export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => void }) {
|
export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => void }) {
|
||||||
const { entries, loading, error } = useCatalog();
|
const { entries, loading, error } = useCatalog();
|
||||||
|
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");
|
||||||
@@ -34,14 +37,17 @@ export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => vo
|
|||||||
const haystack = `${e.name_zh} ${e.name} ${e.vendor} ${e.id}`.toLowerCase();
|
const haystack = `${e.name_zh} ${e.name} ${e.vendor} ${e.id}`.toLowerCase();
|
||||||
if (!haystack.includes(q)) return false;
|
if (!haystack.includes(q)) return false;
|
||||||
}
|
}
|
||||||
// Wave 0:14 个均为未安装;已安装/可更新筛选结果为空(诚实占位)
|
const installed = detectMap[e.id]?.status === "installed";
|
||||||
if (status === "installed") return false;
|
// 状态筛选:接真机 detect 结果
|
||||||
|
if (status === "installed" && !installed) return false;
|
||||||
|
if (status === "uninstalled" && installed) return false;
|
||||||
|
// 可更新:Wave 2.1 不检测可更新,恒为空(诚实)
|
||||||
if (status === "update") return false;
|
if (status === "update") return false;
|
||||||
// 平台:全部 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]);
|
}, [entries, query, status, platform, detectMap]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="catalog">
|
<div className="catalog">
|
||||||
@@ -86,34 +92,45 @@ export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => vo
|
|||||||
{loading && !error && <p className="panel-empty">目录加载中…</p>}
|
{loading && !error && <p className="panel-empty">目录加载中…</p>}
|
||||||
|
|
||||||
<div className="catalog-grid">
|
<div className="catalog-grid">
|
||||||
{filtered.map((entry) => (
|
{filtered.map((entry) => {
|
||||||
<article
|
const detect = detectMap[entry.id];
|
||||||
className="catalog-card"
|
const installed = detect?.status === "installed";
|
||||||
key={entry.id}
|
return (
|
||||||
role="button"
|
<article
|
||||||
tabIndex={0}
|
className="catalog-card"
|
||||||
onClick={() => onOpenDetail(entry.id)}
|
key={entry.id}
|
||||||
onKeyDown={(e) => {
|
role="button"
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
tabIndex={0}
|
||||||
e.preventDefault();
|
onClick={() => onOpenDetail(entry.id)}
|
||||||
onOpenDetail(entry.id);
|
onKeyDown={(e) => {
|
||||||
}
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
}}
|
e.preventDefault();
|
||||||
>
|
onOpenDetail(entry.id);
|
||||||
<div className="catalog-card-top">
|
}
|
||||||
<CliMonogram id={entry.id} name={entry.name} size={40} />
|
}}
|
||||||
<div className="catalog-card-head">
|
>
|
||||||
<div className="catalog-card-name">{entry.name_zh}</div>
|
<div className="catalog-card-top">
|
||||||
<div className="catalog-card-vendor">{entry.vendor}</div>
|
<CliMonogram id={entry.id} name={entry.name} size={40} />
|
||||||
|
<div className="catalog-card-head">
|
||||||
|
<div className="catalog-card-name">{entry.name_zh}</div>
|
||||||
|
<div className="catalog-card-vendor">{entry.vendor}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p className="catalog-card-desc">官方渠道安装 · 全平台支持</p>
|
||||||
<p className="catalog-card-desc">官方渠道安装 · 全平台支持</p>
|
<div className="catalog-card-bottom">
|
||||||
<div className="catalog-card-bottom">
|
{installed ? (
|
||||||
<StatusBadge kind="uninstalled" label="未安装" />
|
<span className="catalog-version">
|
||||||
<span className="catalog-platforms">Windows · Linux</span>
|
<StatusBadge kind="installed" label="已安装" />
|
||||||
</div>
|
{detect?.version && <MonoChip>{detect.version}</MonoChip>}
|
||||||
</article>
|
</span>
|
||||||
))}
|
) : (
|
||||||
|
<StatusBadge kind="uninstalled" label={detecting ? "检测中…" : "未安装"} />
|
||||||
|
)}
|
||||||
|
<span className="catalog-platforms">Windows · Linux</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{!loading && filtered.length === 0 && (
|
{!loading && filtered.length === 0 && (
|
||||||
<p className="panel-empty catalog-empty">没有符合条件的 CLI</p>
|
<p className="panel-empty catalog-empty">没有符合条件的 CLI</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
@@ -35,6 +36,12 @@ const TABS: { key: Tab; label: string }[] = [
|
|||||||
{ key: "diag", label: "诊断" },
|
{ key: "diag", label: "诊断" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type RunPhase = "prepare" | "exec" | "verify";
|
||||||
|
|
||||||
|
type RunOutcome =
|
||||||
|
| { status: "success"; action: CliAction; detect: DetectResult | null }
|
||||||
|
| { status: "failed"; action: CliAction; message: string };
|
||||||
|
|
||||||
/** 授权状态灯(§3.3:已授权=绿、未授权=紫、可能过期=黄呼吸、未知=灰) */
|
/** 授权状态灯(§3.3:已授权=绿、未授权=紫、可能过期=黄呼吸、未知=灰) */
|
||||||
function AuthLight({ auth }: { auth: AuthStatus | null }) {
|
function AuthLight({ auth }: { auth: AuthStatus | null }) {
|
||||||
if (!auth) return <span className="status-dot unknown" aria-hidden="true" />;
|
if (!auth) return <span className="status-dot unknown" aria-hidden="true" />;
|
||||||
@@ -90,7 +97,9 @@ export function CliDetailPage({ id, onBack }: { id: string; onBack: () => void }
|
|||||||
const [running, setRunning] = useState(false);
|
const [running, setRunning] = useState(false);
|
||||||
const [runTitle, setRunTitle] = useState("");
|
const [runTitle, setRunTitle] = useState("");
|
||||||
const [log, setLog] = useState<ActionEvent[]>([]);
|
const [log, setLog] = useState<ActionEvent[]>([]);
|
||||||
const logEndRef = useRef<HTMLDivElement | null>(null);
|
const [phase, setPhase] = useState<RunPhase>("prepare");
|
||||||
|
const [outcome, setOutcome] = useState<RunOutcome | null>(null);
|
||||||
|
const currentActionRef = useRef<CliAction>("install");
|
||||||
|
|
||||||
const installed = detect?.status === "installed";
|
const installed = detect?.status === "installed";
|
||||||
|
|
||||||
@@ -102,14 +111,18 @@ export function CliDetailPage({ id, onBack }: { id: string; onBack: () => void }
|
|||||||
const ev = payload.event;
|
const ev = payload.event;
|
||||||
if (ev.kind === "done") {
|
if (ev.kind === "done") {
|
||||||
setRunning(false);
|
setRunning(false);
|
||||||
|
setPhase("verify");
|
||||||
|
const d = (ev.data as DetectResult | null) ?? null;
|
||||||
|
setOutcome({ status: "success", action: currentActionRef.current, detect: d });
|
||||||
void refresh();
|
void refresh();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ev.kind === "error") {
|
if (ev.kind === "error") {
|
||||||
setRunning(false);
|
setRunning(false);
|
||||||
setLog((l) => [...l, { kind: "error", message: ev.message, data: null }]);
|
setOutcome({ status: "failed", action: currentActionRef.current, message: ev.message });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (ev.phase) setPhase(ev.phase as RunPhase);
|
||||||
setLog((l) => [...l, ev]);
|
setLog((l) => [...l, ev]);
|
||||||
}).then((fn) => {
|
}).then((fn) => {
|
||||||
unlisten = fn;
|
unlisten = fn;
|
||||||
@@ -117,10 +130,6 @@ export function CliDetailPage({ id, onBack }: { id: string; onBack: () => void }
|
|||||||
return () => unlisten?.();
|
return () => unlisten?.();
|
||||||
}, [id, refresh]);
|
}, [id, refresh]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
||||||
}, [log]);
|
|
||||||
|
|
||||||
async function openConfirm(action: CliAction) {
|
async function openConfirm(action: CliAction) {
|
||||||
const plan = await previewAction(id, action);
|
const plan = await previewAction(id, action);
|
||||||
setConfirm({ action, plan });
|
setConfirm({ action, plan });
|
||||||
@@ -129,13 +138,22 @@ export function CliDetailPage({ id, onBack }: { id: string; onBack: () => void }
|
|||||||
async function confirmRun() {
|
async function confirmRun() {
|
||||||
if (!confirm) return;
|
if (!confirm) return;
|
||||||
const action = confirm.action;
|
const action = confirm.action;
|
||||||
|
currentActionRef.current = action;
|
||||||
setConfirm(null);
|
setConfirm(null);
|
||||||
setRunning(true);
|
setRunning(true);
|
||||||
|
setOutcome(null);
|
||||||
|
setPhase("prepare");
|
||||||
setLog([]);
|
setLog([]);
|
||||||
setRunTitle(action === "install" ? "正在安装" : action === "uninstall" ? "正在卸载" : "正在执行");
|
setRunTitle(action === "install" ? "正在安装" : action === "uninstall" ? "正在卸载" : "正在执行");
|
||||||
await runAction(id, action);
|
await runAction(id, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeRun() {
|
||||||
|
setRunning(false);
|
||||||
|
setOutcome(null);
|
||||||
|
setLog([]);
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <p className="panel-empty">加载中…</p>;
|
return <p className="panel-empty">加载中…</p>;
|
||||||
}
|
}
|
||||||
@@ -220,7 +238,7 @@ export function CliDetailPage({ id, onBack }: { id: string; onBack: () => void }
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<section className="cli-detail-body">
|
<section className="cli-detail-body">
|
||||||
{tab === "overview" && <OverviewTab adapter={adapter} detect={detect} />}
|
{tab === "overview" && <OverviewTab adapter={adapter} detect={detect} auth={auth} />}
|
||||||
{tab === "config" && <ConfigForm id={id} />}
|
{tab === "config" && <ConfigForm id={id} />}
|
||||||
{tab === "docs" && <DocsTab adapter={adapter} />}
|
{tab === "docs" && <DocsTab adapter={adapter} />}
|
||||||
{tab === "diag" && <DiagTab id={id} />}
|
{tab === "diag" && <DiagTab id={id} />}
|
||||||
@@ -246,32 +264,109 @@ export function CliDetailPage({ id, onBack }: { id: string; onBack: () => void }
|
|||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 流式执行日志弹窗 */}
|
{/* 流式执行日志弹窗(安装/卸载全程可见:步骤进度 + stdout/stderr + 终态) */}
|
||||||
{running && (
|
{(running || outcome) && (
|
||||||
<Modal title={`${runTitle} ${adapter.name_zh}…`} footer={null}>
|
<Modal
|
||||||
<div className="run-log" aria-live="polite">
|
title={outcome ? `${runTitle} ${adapter.name_zh} · ${outcome.status === "success" ? "完成" : "失败"}` : `${runTitle} ${adapter.name_zh}…`}
|
||||||
{log.length === 0 && (
|
onClose={running ? undefined : closeRun}
|
||||||
<div className="run-log-wait">
|
footer={
|
||||||
<Loader2 size={16} strokeWidth={1.5} className="spin" aria-hidden="true" />
|
running ? null : (
|
||||||
正在启动命令…
|
<button type="button" className="btn btn-primary" onClick={closeRun}>
|
||||||
</div>
|
完成
|
||||||
)}
|
</button>
|
||||||
{log.map((l, i) => (
|
)
|
||||||
<div key={i} className={`run-log-line ${l.kind}`}>
|
}
|
||||||
{l.kind === "step" ? <ClipboardList size={13} strokeWidth={1.5} aria-hidden="true" /> : null}
|
>
|
||||||
{l.kind === "stdout" ? <Terminal size={13} strokeWidth={1.5} aria-hidden="true" /> : null}
|
<RunBody phase={phase} log={log} outcome={outcome} />
|
||||||
{l.kind === "stderr" ? <ShieldAlert size={13} strokeWidth={1.5} aria-hidden="true" /> : null}
|
|
||||||
<span>{l.message}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div ref={logEndRef} />
|
|
||||||
</div>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 执行进度三阶段(准备 / 执行 / 复检) */
|
||||||
|
function PhaseSteps({ phase }: { phase: RunPhase }) {
|
||||||
|
const steps: { key: RunPhase; label: string }[] = [
|
||||||
|
{ key: "prepare", label: "准备" },
|
||||||
|
{ key: "exec", label: "执行" },
|
||||||
|
{ key: "verify", label: "复检" },
|
||||||
|
];
|
||||||
|
const order: Record<RunPhase, number> = { prepare: 0, exec: 1, verify: 2 };
|
||||||
|
const cur = order[phase];
|
||||||
|
return (
|
||||||
|
<div className="run-phases" aria-label="安装进度">
|
||||||
|
{steps.map((s, i) => (
|
||||||
|
<div key={s.key} className={`run-phase ${i < cur ? "done" : i === cur ? "active" : ""}`}>
|
||||||
|
<span className="run-phase-dot">{i < cur ? "✓" : i + 1}</span>
|
||||||
|
<span className="run-phase-label">{s.label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RunBody({
|
||||||
|
phase,
|
||||||
|
log,
|
||||||
|
outcome,
|
||||||
|
}: {
|
||||||
|
phase: RunPhase;
|
||||||
|
log: ActionEvent[];
|
||||||
|
outcome: RunOutcome | null;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="run-body">
|
||||||
|
<PhaseSteps phase={phase} />
|
||||||
|
<div className="run-log" aria-live="polite">
|
||||||
|
{log.length === 0 && !outcome && (
|
||||||
|
<div className="run-log-wait">
|
||||||
|
<Loader2 size={16} strokeWidth={1.5} className="spin" aria-hidden="true" />
|
||||||
|
正在启动命令…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{log.map((l, i) => (
|
||||||
|
<div key={i} className={`run-log-line ${l.kind}`}>
|
||||||
|
{l.kind === "step" ? <ClipboardList size={13} strokeWidth={1.5} aria-hidden="true" /> : null}
|
||||||
|
{l.kind === "stdout" ? <Terminal size={13} strokeWidth={1.5} aria-hidden="true" /> : null}
|
||||||
|
{l.kind === "stderr" ? <ShieldAlert size={13} strokeWidth={1.5} aria-hidden="true" /> : null}
|
||||||
|
<span>{l.message}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div ref={useAutoScroll(log)} />
|
||||||
|
</div>
|
||||||
|
{outcome?.status === "success" && (
|
||||||
|
<div className="run-result success">
|
||||||
|
<Check size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||||
|
<span>
|
||||||
|
{outcome.action === "install" ? "安装成功" : outcome.action === "uninstall" ? "卸载完成" : "执行完成"}
|
||||||
|
</span>
|
||||||
|
{outcome.detect?.status === "installed" && (
|
||||||
|
<span className="run-result-version">
|
||||||
|
已安装 {outcome.detect.version ?? ""}
|
||||||
|
{outcome.detect.executable ? ` · ${outcome.detect.executable}` : ""}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{outcome?.status === "failed" && (
|
||||||
|
<div className="run-result failed">
|
||||||
|
<ShieldAlert size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||||
|
<span>{outcome.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 自动滚动到底部(把 ref 挂在日志末尾) */
|
||||||
|
function useAutoScroll(dep: unknown) {
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}, [dep]);
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
|
||||||
function ConfirmBody({ plan, action }: { plan: DryRunPlan; action: CliAction }) {
|
function ConfirmBody({ plan, action }: { plan: DryRunPlan; action: CliAction }) {
|
||||||
return (
|
return (
|
||||||
<div className="confirm-body">
|
<div className="confirm-body">
|
||||||
@@ -320,8 +415,34 @@ function ConfirmBody({ plan, action }: { plan: DryRunPlan; action: CliAction })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function OverviewTab({ adapter, detect }: { adapter: Adapter; detect: DetectResult | null }) {
|
function OverviewTab({
|
||||||
|
adapter,
|
||||||
|
detect,
|
||||||
|
auth,
|
||||||
|
}: {
|
||||||
|
adapter: Adapter;
|
||||||
|
detect: DetectResult | null;
|
||||||
|
auth: AuthStatus | null;
|
||||||
|
}) {
|
||||||
const channels = adapter.install?.channels ?? [];
|
const channels = adapter.install?.channels ?? [];
|
||||||
|
const authModes = adapter.authorization?.modes ?? [];
|
||||||
|
const platforms: string[] = [];
|
||||||
|
if (adapter.platforms?.windows) platforms.push("Windows");
|
||||||
|
if (adapter.platforms?.linux) platforms.push("Ubuntu");
|
||||||
|
|
||||||
|
const rows: { label: string; value: ReactNode }[] = [
|
||||||
|
{ label: "厂商", value: adapter.vendor },
|
||||||
|
{ label: "版本", value: detect?.version ? <MonoChip>{detect.version}</MonoChip> : detectLabel(detect) },
|
||||||
|
{ label: "路径", value: detect?.executable ? <MonoChip>{detect.executable}</MonoChip> : "—" },
|
||||||
|
{ label: "授权状态", value: auth ? auth.detail_zh : "—" },
|
||||||
|
{ label: "支持平台", value: platforms.length > 0 ? platforms.join(" · ") : "—" },
|
||||||
|
{
|
||||||
|
label: "授权方式",
|
||||||
|
value: authModes.length > 0 ? authModes.map((m) => authModeLabel(m.mode)).join(" / ") : "—",
|
||||||
|
},
|
||||||
|
{ label: "适配器版本", value: adapter.adapter_version ?? "—" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="detail-overview">
|
<div className="detail-overview">
|
||||||
<div className="detail-block">
|
<div className="detail-block">
|
||||||
@@ -330,6 +451,19 @@ function OverviewTab({ adapter, detect }: { adapter: Adapter; detect: DetectResu
|
|||||||
{adapter.documentation?.quickstart_zh ?? "参见官方文档。"}
|
{adapter.documentation?.quickstart_zh ?? "参见官方文档。"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-block">
|
||||||
|
<h3 className="detail-block-title">基本信息</h3>
|
||||||
|
<dl className="detail-meta">
|
||||||
|
{rows.map((r) => (
|
||||||
|
<div key={r.label} className="detail-meta-row">
|
||||||
|
<dt className="detail-meta-label">{r.label}</dt>
|
||||||
|
<dd className="detail-meta-value">{r.value}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="detail-block">
|
<div className="detail-block">
|
||||||
<h3 className="detail-block-title">安装渠道</h3>
|
<h3 className="detail-block-title">安装渠道</h3>
|
||||||
{channels.length === 0 ? (
|
{channels.length === 0 ? (
|
||||||
@@ -346,6 +480,23 @@ function OverviewTab({ adapter, detect }: { adapter: Adapter; detect: DetectResu
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-block">
|
||||||
|
<h3 className="detail-block-title">官方链接</h3>
|
||||||
|
<div className="detail-links">
|
||||||
|
{adapter.official?.homepage && (
|
||||||
|
<a className="detail-link" href={adapter.official.homepage} target="_blank" rel="noreferrer">
|
||||||
|
官方主页
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{adapter.official?.docs && (
|
||||||
|
<a className="detail-link" href={adapter.official.docs} target="_blank" rel="noreferrer">
|
||||||
|
官方文档
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{detect && detect.status !== "installed" && detect.status !== "not_installed" && (
|
{detect && detect.status !== "installed" && detect.status !== "not_installed" && (
|
||||||
<div className="detail-block detail-note">
|
<div className="detail-block detail-note">
|
||||||
<CircleHelp size={14} strokeWidth={1.5} aria-hidden="true" />
|
<CircleHelp size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||||
@@ -356,10 +507,44 @@ function OverviewTab({ adapter, detect }: { adapter: Adapter; detect: DetectResu
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function authModeLabel(mode: string): string {
|
||||||
|
switch (mode) {
|
||||||
|
case "browser_oauth":
|
||||||
|
return "浏览器授权";
|
||||||
|
case "device_code":
|
||||||
|
return "设备码";
|
||||||
|
case "api_key":
|
||||||
|
return "API Key";
|
||||||
|
case "local_tui":
|
||||||
|
return "本机终端授权";
|
||||||
|
default:
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function DocsTab({ adapter }: { adapter: Adapter }) {
|
function DocsTab({ adapter }: { adapter: Adapter }) {
|
||||||
const doc = adapter.documentation;
|
const doc = adapter.documentation;
|
||||||
|
const docUrl = adapter.official?.docs;
|
||||||
return (
|
return (
|
||||||
<div className="detail-docs">
|
<div className="detail-docs">
|
||||||
|
{doc?.quickstart_zh && (
|
||||||
|
<div className="detail-block">
|
||||||
|
<h3 className="detail-block-title">快速开始</h3>
|
||||||
|
<p className="detail-block-text">{doc.quickstart_zh}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{doc?.install_zh && (
|
||||||
|
<div className="detail-block">
|
||||||
|
<h3 className="detail-block-title">安装说明</h3>
|
||||||
|
<p className="detail-block-text">{doc.install_zh}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{doc?.auth_zh && (
|
||||||
|
<div className="detail-block">
|
||||||
|
<h3 className="detail-block-title">授权说明</h3>
|
||||||
|
<p className="detail-block-text">{doc.auth_zh}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="detail-block">
|
<div className="detail-block">
|
||||||
<h3 className="detail-block-title">
|
<h3 className="detail-block-title">
|
||||||
<BookOpen size={14} strokeWidth={1.5} aria-hidden="true" /> 常用命令
|
<BookOpen size={14} strokeWidth={1.5} aria-hidden="true" /> 常用命令
|
||||||
@@ -377,6 +562,19 @@ function DocsTab({ adapter }: { adapter: Adapter }) {
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{(doc?.params ?? []).length > 0 && (
|
||||||
|
<div className="detail-block">
|
||||||
|
<h3 className="detail-block-title">常用参数</h3>
|
||||||
|
<ul className="detail-commands">
|
||||||
|
{doc!.params!.map((p) => (
|
||||||
|
<li key={p.param} className="detail-command">
|
||||||
|
<MonoChip>{p.param}</MonoChip>
|
||||||
|
{p.desc_zh && <span className="detail-command-desc">{p.desc_zh}</span>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{(doc?.risks_zh ?? []).length > 0 && (
|
{(doc?.risks_zh ?? []).length > 0 && (
|
||||||
<div className="detail-block detail-note">
|
<div className="detail-block detail-note">
|
||||||
<ShieldAlert size={14} strokeWidth={1.5} aria-hidden="true" />
|
<ShieldAlert size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||||
@@ -387,11 +585,16 @@ function DocsTab({ adapter }: { adapter: Adapter }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{adapter.official?.homepage && (
|
<div className="detail-block detail-doc-footer">
|
||||||
<div className="detail-block detail-block-text">
|
{docUrl && (
|
||||||
官方主页:<MonoChip>{adapter.official.homepage}</MonoChip>
|
<a className="detail-link" href={docUrl} target="_blank" rel="noreferrer">
|
||||||
</div>
|
官方文档原文
|
||||||
)}
|
</a>
|
||||||
|
)}
|
||||||
|
<span className="detail-doc-meta">
|
||||||
|
适配器版本 {adapter.adapter_version ?? "—"} · 文档更新 {doc?.updated_at ?? "—"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -437,7 +640,7 @@ function DiagTab({ id }: { id: string }) {
|
|||||||
if (error) return <p className="panel-empty">诊断失败:{error}</p>;
|
if (error) return <p className="panel-empty">诊断失败:{error}</p>;
|
||||||
if (!report) return null;
|
if (!report) return null;
|
||||||
|
|
||||||
const findings = useMemo(() => report.findings, [report]);
|
const findings = report.findings;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="diag-result">
|
<div className="diag-result">
|
||||||
|
|||||||
@@ -1,20 +1,107 @@
|
|||||||
import { SquareTerminal } from "lucide-react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { ChevronRight, SquareTerminal } from "lucide-react";
|
||||||
|
import { useCatalog } from "../hooks/useCatalog";
|
||||||
|
import { useDetectAll } from "../hooks/useDetectAll";
|
||||||
|
import { authStatus } from "../ipc";
|
||||||
|
import { CliMonogram } from "../components/CliMonogram";
|
||||||
|
import { MonoChip } from "../components/MonoChip";
|
||||||
|
import type { AuthStatus } from "../ipc/types";
|
||||||
import type { PageKey } from "../components/Sidebar";
|
import type { PageKey } from "../components/Sidebar";
|
||||||
|
|
||||||
/** 我的 CLI(PRD §7):版本 / 路径 / 授权状态。空态按 v1.3 §3.7。 */
|
/** 单个已安装 CLI 的授权状态(懒加载) */
|
||||||
export function MyCliPage({ onNavigate }: { onNavigate: (p: PageKey) => void }) {
|
function AuthBadge({ id }: { id: string }) {
|
||||||
|
const [auth, setAuth] = useState<AuthStatus | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
authStatus(id)
|
||||||
|
.then((a) => {
|
||||||
|
if (!cancelled) setAuth(a);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
if (!auth) return <span className="mycli-auth">授权状态检测中…</span>;
|
||||||
|
const ok = auth.status === "authorized";
|
||||||
return (
|
return (
|
||||||
<div className="placeholder-page">
|
<span className="mycli-auth">
|
||||||
<div className="placeholder-icon">
|
<span className={ok ? "status-dot ok" : "status-dot unknown"} aria-hidden="true" />
|
||||||
<SquareTerminal size={40} strokeWidth={1.5} aria-hidden="true" />
|
{ok ? "已授权" : "未授权"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 我的 CLI(PRD §7):展示已安装列表(版本 / 路径 / 授权状态)。空态按 v1.3 §3.7。 */
|
||||||
|
export function MyCliPage({
|
||||||
|
onNavigate,
|
||||||
|
onOpenDetail,
|
||||||
|
}: {
|
||||||
|
onNavigate: (p: PageKey) => void;
|
||||||
|
onOpenDetail: (id: string) => void;
|
||||||
|
}) {
|
||||||
|
const { entries } = useCatalog();
|
||||||
|
const { detectMap, loading } = useDetectAll();
|
||||||
|
|
||||||
|
const installed = entries.filter((e) => detectMap[e.id]?.status === "installed");
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <p className="panel-empty">检测已安装的 CLI…</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installed.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="placeholder-page">
|
||||||
|
<div className="placeholder-icon">
|
||||||
|
<SquareTerminal size={40} strokeWidth={1.5} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<h2 className="placeholder-title">还没有安装任何 CLI</h2>
|
||||||
|
<p className="placeholder-desc">
|
||||||
|
安装后,这里会展示每个 CLI 的版本、路径与授权状态。
|
||||||
|
</p>
|
||||||
|
<button type="button" className="btn btn-primary" onClick={() => onNavigate("catalog")}>
|
||||||
|
去 CLI 目录看看
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mycli">
|
||||||
|
<div className="panel mycli-list">
|
||||||
|
<div className="panel-head">
|
||||||
|
<h2 className="panel-title">已安装的 CLI({installed.length})</h2>
|
||||||
|
</div>
|
||||||
|
{installed.map((entry) => {
|
||||||
|
const d = detectMap[entry.id];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={entry.id}
|
||||||
|
type="button"
|
||||||
|
className="mycli-row"
|
||||||
|
onClick={() => onOpenDetail(entry.id)}
|
||||||
|
>
|
||||||
|
<CliMonogram id={entry.id} name={entry.name} size={36} />
|
||||||
|
<div className="mycli-row-main">
|
||||||
|
<span className="mycli-row-name">{entry.name_zh}</span>
|
||||||
|
{d?.version && (
|
||||||
|
<span className="mycli-row-version">
|
||||||
|
版本 <MonoChip>{d.version}</MonoChip>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{d?.executable && (
|
||||||
|
<span className="mycli-row-path">
|
||||||
|
<MonoChip>{d.executable}</MonoChip>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<AuthBadge id={entry.id} />
|
||||||
|
<ChevronRight size={16} strokeWidth={1.5} className="mycli-row-arrow" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<h2 className="placeholder-title">还没有安装任何 CLI</h2>
|
|
||||||
<p className="placeholder-desc">
|
|
||||||
安装后,这里会展示每个 CLI 的版本、路径与授权状态。
|
|
||||||
</p>
|
|
||||||
<button type="button" className="btn btn-primary" onClick={() => onNavigate("catalog")}>
|
|
||||||
去 CLI 目录看看
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { useState } from "react";
|
|||||||
import { Activity, Archive, Lock, Plus, ScanSearch } from "lucide-react";
|
import { Activity, Archive, 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 { KpiCard } from "../components/KpiCard";
|
import { KpiCard } from "../components/KpiCard";
|
||||||
import { MonoChip } from "../components/MonoChip";
|
import { MonoChip } from "../components/MonoChip";
|
||||||
import { StatusBadge } from "../components/StatusBadge";
|
import { StatusBadge } from "../components/StatusBadge";
|
||||||
import { CliMonogram } from "../components/CliMonogram";
|
import { CliMonogram } from "../components/CliMonogram";
|
||||||
import type { PlatformEnv, RuntimeInfo, RuntimeStatus, CatalogEntry } from "../ipc/types";
|
import type { CatalogEntry, DetectResult, PlatformEnv, RuntimeInfo, RuntimeStatus } from "../ipc/types";
|
||||||
import type { PageKey } from "../components/Sidebar";
|
import type { PageKey } from "../components/Sidebar";
|
||||||
|
|
||||||
/** 平台相关运行时集合(Windows 不看 apt,Linux 不看 winget) */
|
/** 平台相关运行时集合(Windows 不看 apt,Linux 不看 winget) */
|
||||||
@@ -292,30 +293,38 @@ function QuickActions({
|
|||||||
/** CLI 状态网格块(v1.3 §3.1.4;首跑精简为两行 + 安装入口,§3.1.5) */
|
/** CLI 状态网格块(v1.3 §3.1.4;首跑精简为两行 + 安装入口,§3.1.5) */
|
||||||
function CliBlock({
|
function CliBlock({
|
||||||
entry,
|
entry,
|
||||||
|
detect,
|
||||||
compact,
|
compact,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
|
onOpenDetail,
|
||||||
}: {
|
}: {
|
||||||
entry: CatalogEntry;
|
entry: CatalogEntry;
|
||||||
|
detect: DetectResult | undefined;
|
||||||
compact: boolean;
|
compact: boolean;
|
||||||
onNavigate: (p: PageKey) => void;
|
onNavigate: (p: PageKey) => void;
|
||||||
|
onOpenDetail: (id: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const installed = entry.status === "installed";
|
const installed = detect?.status === "installed";
|
||||||
return (
|
return (
|
||||||
<div className="cli-block">
|
<div className="cli-block" role="button" tabIndex={0} onClick={() => onOpenDetail(entry.id)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onOpenDetail(entry.id); } }}>
|
||||||
<div className="cli-block-top">
|
<div className="cli-block-top">
|
||||||
<CliMonogram id={entry.id} name={entry.name} size={32} />
|
<CliMonogram id={entry.id} name={entry.name} size={32} />
|
||||||
<span className="cli-block-name">{entry.name_zh}</span>
|
<span className="cli-block-name">{entry.name_zh}</span>
|
||||||
<StatusBadge kind={installed ? "installed" : "uninstalled"} label={installed ? "已安装" : "未安装"} />
|
<StatusBadge kind={installed ? "installed" : "uninstalled"} label={installed ? "已安装" : "未安装"} />
|
||||||
</div>
|
</div>
|
||||||
<div className="cli-block-version">
|
<div className="cli-block-version">
|
||||||
{installed ? null : <span className="version-missing">未安装</span>}
|
{installed ? (
|
||||||
|
detect?.version ? <MonoChip>{detect.version}</MonoChip> : null
|
||||||
|
) : (
|
||||||
|
<span className="version-missing">未安装</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!compact && (
|
{!compact && (
|
||||||
<div className="cli-block-auth">
|
<div className="cli-block-auth">
|
||||||
{installed ? (
|
{installed ? (
|
||||||
<>
|
<>
|
||||||
<span className="status-dot ok" aria-hidden="true" />
|
<span className="status-dot ok" aria-hidden="true" />
|
||||||
<span>已安装</span>
|
<span>{detect?.version ? `v${detect.version}` : "已安装"}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -327,7 +336,7 @@ function CliBlock({
|
|||||||
)}
|
)}
|
||||||
{compact && (
|
{compact && (
|
||||||
<div className="cli-block-install">
|
<div className="cli-block-install">
|
||||||
<button type="button" className="link-btn" onClick={() => onNavigate("catalog")}>
|
<button type="button" className="link-btn" onClick={(e) => { e.stopPropagation(); onNavigate("catalog"); }}>
|
||||||
安装
|
安装
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -338,14 +347,16 @@ function CliBlock({
|
|||||||
|
|
||||||
interface OverviewProps {
|
interface OverviewProps {
|
||||||
onNavigate: (p: PageKey) => void;
|
onNavigate: (p: PageKey) => void;
|
||||||
|
onOpenDetail: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 总览页(视觉规范 §3.1 + PRD §7):首跑空态 / KPI + 诊断/快速操作 + CLI 状态网格 + 本机环境 */
|
/** 总览页(视觉规范 §3.1 + PRD §7):首跑空态 / KPI + 诊断/快速操作 + CLI 状态网格 + 本机环境 */
|
||||||
export function OverviewPage({ onNavigate }: OverviewProps) {
|
export function OverviewPage({ onNavigate, onOpenDetail }: OverviewProps) {
|
||||||
const { env } = useEnv();
|
const { env } = useEnv();
|
||||||
const { entries } = useCatalog();
|
const { entries } = useCatalog();
|
||||||
|
const { detectMap } = useDetectAll();
|
||||||
|
|
||||||
const installedCount = entries.filter((e) => e.status === "installed").length;
|
const installedCount = entries.filter((e) => detectMap[e.id]?.status === "installed").length;
|
||||||
const firstRun = installedCount === 0; // 无安装且无诊断记录(当前无诊断引擎)
|
const firstRun = installedCount === 0; // 无安装且无诊断记录(当前无诊断引擎)
|
||||||
|
|
||||||
const warningCount = env ? runtimeProblems(env).length : 0;
|
const warningCount = env ? runtimeProblems(env).length : 0;
|
||||||
@@ -384,7 +395,14 @@ export function OverviewPage({ onNavigate }: OverviewProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="cli-status-grid">
|
<div className="cli-status-grid">
|
||||||
{entries.map((entry) => (
|
{entries.map((entry) => (
|
||||||
<CliBlock key={entry.id} entry={entry} compact={firstRun} onNavigate={onNavigate} />
|
<CliBlock
|
||||||
|
key={entry.id}
|
||||||
|
entry={entry}
|
||||||
|
detect={detectMap[entry.id]}
|
||||||
|
compact={firstRun}
|
||||||
|
onNavigate={onNavigate}
|
||||||
|
onOpenDetail={onOpenDetail}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1144,6 +1144,7 @@ button {
|
|||||||
|
|
||||||
.overview .overview-mid {
|
.overview .overview-mid {
|
||||||
grid-column: 1 / 3;
|
grid-column: 1 / 3;
|
||||||
|
grid-row: 2;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1153,6 +1154,7 @@ button {
|
|||||||
|
|
||||||
.overview .env-panel {
|
.overview .env-panel {
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
|
grid-row: 2;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1815,3 +1817,391 @@ button {
|
|||||||
.save-error {
|
.save-error {
|
||||||
color: var(--ad-danger);
|
color: var(--ad-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* Wave 2.1:安装进度 / 配置校验 / 详情信息 / 目录真机状态 / 我的 CLI
|
||||||
|
* 全部颜色来自 tokens/
|
||||||
|
* ============================================================ */
|
||||||
|
|
||||||
|
/* ---------- 执行进度三阶段(安装弹窗) ---------- */
|
||||||
|
.run-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--ad-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-phases {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ad-space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-phase {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ad-space-2);
|
||||||
|
color: var(--ad-text-3);
|
||||||
|
font-size: var(--ad-text-s-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-phase-dot {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid var(--ad-border);
|
||||||
|
font-size: var(--ad-text-xs-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-phase.done {
|
||||||
|
color: var(--ad-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-phase.done .run-phase-dot {
|
||||||
|
border-color: var(--ad-success);
|
||||||
|
color: var(--ad-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-phase.active {
|
||||||
|
color: var(--ad-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-phase.active .run-phase-dot {
|
||||||
|
border-color: var(--ad-primary);
|
||||||
|
color: var(--ad-primary);
|
||||||
|
box-shadow: var(--ad-glow-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-result {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--ad-space-2);
|
||||||
|
padding: var(--ad-space-3) var(--ad-space-4);
|
||||||
|
border-radius: var(--ad-radius-m);
|
||||||
|
font-size: var(--ad-text-s-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-result.success {
|
||||||
|
color: var(--ad-success);
|
||||||
|
border: 1px solid var(--ad-border);
|
||||||
|
background: var(--ad-bg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-result.failed {
|
||||||
|
color: var(--ad-danger);
|
||||||
|
border: 1px solid var(--ad-border);
|
||||||
|
background: var(--ad-bg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-result-version {
|
||||||
|
color: var(--ad-text-2);
|
||||||
|
font-family: var(--ad-font-mono);
|
||||||
|
font-size: var(--ad-text-xs-size);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 详情概览:基本信息表 + 官方链接 ---------- */
|
||||||
|
.detail-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--ad-space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ad-space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-meta-label {
|
||||||
|
width: 88px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--ad-text-2);
|
||||||
|
font-size: var(--ad-text-s-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-meta-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--ad-space-2);
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--ad-text-1);
|
||||||
|
font-size: var(--ad-text-m-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-links {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--ad-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-link {
|
||||||
|
color: var(--ad-primary);
|
||||||
|
font-size: var(--ad-text-m-size);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-doc-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--ad-space-3);
|
||||||
|
padding-top: var(--ad-space-4);
|
||||||
|
border-top: 1px solid var(--ad-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-doc-meta {
|
||||||
|
color: var(--ad-text-3);
|
||||||
|
font-size: var(--ad-text-xs-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 配置表单:下拉 + 官方链接 + 校验结果 ---------- */
|
||||||
|
.field-select {
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 var(--ad-space-3);
|
||||||
|
border: 1px solid var(--ad-border);
|
||||||
|
border-radius: var(--ad-radius-m);
|
||||||
|
background: var(--ad-bg-0);
|
||||||
|
color: var(--ad-text-1);
|
||||||
|
font-size: var(--ad-text-m-size);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-select:focus {
|
||||||
|
border-color: var(--ad-primary);
|
||||||
|
box-shadow: var(--ad-glow-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-docs-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
margin-left: var(--ad-space-2);
|
||||||
|
color: var(--ad-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-docs-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-ok {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--ad-space-1);
|
||||||
|
color: var(--ad-success);
|
||||||
|
font-size: var(--ad-text-xs-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-fail {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--ad-space-1);
|
||||||
|
color: var(--ad-danger);
|
||||||
|
font-size: var(--ad-text-xs-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 目录真机版本位 ---------- */
|
||||||
|
.catalog-version {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ad-space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 我的 CLI 列表 ---------- */
|
||||||
|
.mycli {
|
||||||
|
max-width: var(--ad-frame-max);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mycli-list {
|
||||||
|
padding: var(--ad-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mycli-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ad-space-3);
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--ad-space-3) var(--ad-space-2);
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--ad-border);
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mycli-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mycli-row:hover {
|
||||||
|
background: var(--ad-bg-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mycli-row-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mycli-row-name {
|
||||||
|
color: var(--ad-text-1);
|
||||||
|
font-size: var(--ad-text-m-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mycli-row-version {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ad-space-2);
|
||||||
|
color: var(--ad-text-2);
|
||||||
|
font-size: var(--ad-text-xs-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mycli-row-path {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mycli-auth {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ad-space-2);
|
||||||
|
color: var(--ad-text-2);
|
||||||
|
font-size: var(--ad-text-xs-size);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mycli-row-arrow {
|
||||||
|
color: var(--ad-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 错误边界(防黑屏) ---------- */
|
||||||
|
.crash-screen {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--ad-space-4);
|
||||||
|
padding: var(--ad-space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crash-title {
|
||||||
|
font-size: var(--ad-text-xl-size);
|
||||||
|
font-weight: var(--ad-text-xl-weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crash-desc {
|
||||||
|
color: var(--ad-text-2);
|
||||||
|
font-size: var(--ad-text-m-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crash-detail {
|
||||||
|
color: var(--ad-text-3);
|
||||||
|
font-family: var(--ad-font-mono);
|
||||||
|
font-size: var(--ad-text-xs-size);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 配置表单三层分组(授权/常用/高级) ---------- */
|
||||||
|
.config-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--ad-space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section-title {
|
||||||
|
font-size: var(--ad-text-m-size);
|
||||||
|
font-weight: var(--ad-text-m-weight);
|
||||||
|
color: var(--ad-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-modes {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--ad-space-2);
|
||||||
|
padding: var(--ad-space-3);
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid var(--ad-border);
|
||||||
|
border-radius: var(--ad-radius-m);
|
||||||
|
background: var(--ad-bg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-mode {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--ad-space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-mode-name {
|
||||||
|
color: var(--ad-primary);
|
||||||
|
font-size: var(--ad-text-s-size);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-mode-note {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--ad-text-2);
|
||||||
|
font-size: var(--ad-text-s-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-mode-cmd {
|
||||||
|
color: var(--ad-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-advanced-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ad-space-2);
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 var(--ad-space-2);
|
||||||
|
border-radius: var(--ad-radius-s);
|
||||||
|
background: var(--ad-attention);
|
||||||
|
color: var(--ad-bg-0);
|
||||||
|
font-size: var(--ad-text-xs-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-chevron {
|
||||||
|
color: var(--ad-text-2);
|
||||||
|
transition: transform var(--ad-dur-fast) var(--ad-ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-chevron.open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-hint {
|
||||||
|
color: var(--ad-text-3);
|
||||||
|
font-size: var(--ad-text-xs-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--ad-space-5);
|
||||||
|
}
|
||||||
|
|||||||
@@ -252,6 +252,21 @@ pub struct ConfigField {
|
|||||||
pub platforms: Vec<String>,
|
pub platforms: Vec<String>,
|
||||||
#[serde(rename = "docs_url", default)]
|
#[serde(rename = "docs_url", default)]
|
||||||
pub docs_url: Option<String>,
|
pub docs_url: Option<String>,
|
||||||
|
/// enum 类型的可选值(value + 中文名)
|
||||||
|
#[serde(default)]
|
||||||
|
pub options: Vec<FieldOption>,
|
||||||
|
/// 表单分组:auth(授权/登录)| common(常用配置)| advanced(高级配置,默认折叠)
|
||||||
|
/// 未声明时按 common 处理
|
||||||
|
#[serde(default)]
|
||||||
|
pub group: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct FieldOption {
|
||||||
|
pub value: String,
|
||||||
|
#[serde(rename = "label_zh")]
|
||||||
|
pub label_zh: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
@@ -266,8 +281,14 @@ pub struct Diagnostic {
|
|||||||
pub struct Documentation {
|
pub struct Documentation {
|
||||||
#[serde(rename = "quickstart_zh", default)]
|
#[serde(rename = "quickstart_zh", default)]
|
||||||
pub quickstart_zh: Option<String>,
|
pub quickstart_zh: Option<String>,
|
||||||
|
#[serde(rename = "install_zh", default)]
|
||||||
|
pub install_zh: Option<String>,
|
||||||
|
#[serde(rename = "auth_zh", default)]
|
||||||
|
pub auth_zh: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub commands: Vec<DocCommand>,
|
pub commands: Vec<DocCommand>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub params: Vec<DocParam>,
|
||||||
#[serde(rename = "updated_at", default)]
|
#[serde(rename = "updated_at", default)]
|
||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
#[serde(rename = "risks_zh", default)]
|
#[serde(rename = "risks_zh", default)]
|
||||||
@@ -282,6 +303,14 @@ pub struct DocCommand {
|
|||||||
pub desc_zh: Option<String>,
|
pub desc_zh: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct DocParam {
|
||||||
|
pub param: String,
|
||||||
|
#[serde(rename = "desc_zh", default)]
|
||||||
|
pub desc_zh: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
fn default_true() -> bool {
|
fn default_true() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ impl Engine {
|
|||||||
Ok(detect_one(&adapter))
|
Ok(detect_one(&adapter))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 批量检测全部适配器(总览/目录/我的 CLI 接入真机状态用)。
|
||||||
|
pub fn detect_all(&self) -> Result<Vec<DetectResult>, EngineError> {
|
||||||
|
let adapters = self.adapters()?;
|
||||||
|
Ok(adapters.iter().map(detect_one).collect())
|
||||||
|
}
|
||||||
|
|
||||||
// ---- dry-run ----
|
// ---- dry-run ----
|
||||||
|
|
||||||
pub fn preview(&self, id: &str, action: AdapterAction) -> Result<DryRunPlan, EngineError> {
|
pub fn preview(&self, id: &str, action: AdapterAction) -> Result<DryRunPlan, EngineError> {
|
||||||
@@ -75,6 +81,12 @@ impl Engine {
|
|||||||
write_config(&adapter, patch, self.secrets.as_ref())
|
write_config(&adapter, patch, self.secrets.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 配置写入后的「生效检查」:重读配置文件 + 解析 +(若已安装)CLI 版本探测。
|
||||||
|
pub fn verify_config(&self, id: &str) -> Result<ConfigVerifyResult, EngineError> {
|
||||||
|
let adapter = self.adapter(id)?;
|
||||||
|
Ok(verify_config(&adapter))
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 授权 ----
|
// ---- 授权 ----
|
||||||
|
|
||||||
pub fn auth_status(&self, id: &str) -> Result<AuthStatus, EngineError> {
|
pub fn auth_status(&self, id: &str) -> Result<AuthStatus, EngineError> {
|
||||||
@@ -262,14 +274,24 @@ pub fn read_config(adapter: &Adapter, secrets: &dyn SecretStore) -> Result<Confi
|
|||||||
|
|
||||||
// 字段当前值
|
// 字段当前值
|
||||||
for field in &cfg.fields {
|
for field in &cfg.fields {
|
||||||
|
let env_key = cfg
|
||||||
|
.environment
|
||||||
|
.iter()
|
||||||
|
.find(|e| e.maps_to_field.as_deref() == Some(field.id.as_str()))
|
||||||
|
.map(|e| e.key.clone());
|
||||||
let (value, has_value) = match field.storage.as_str() {
|
let (value, has_value) = match field.storage.as_str() {
|
||||||
"keyring" | "env" => (None, secrets.has(&service, &field.id)),
|
"keyring" => (None, secrets.has(&service, &field.id)),
|
||||||
"file" => {
|
"file" | "env" => {
|
||||||
let val = read_file_field(adapter, &field.id)?;
|
let val = read_file_field(adapter, &field.id)?;
|
||||||
(val, false)
|
(val, false)
|
||||||
}
|
}
|
||||||
_ => (None, false),
|
_ => (None, false),
|
||||||
};
|
};
|
||||||
|
let options: Vec<ConfigFieldOption> = field
|
||||||
|
.options
|
||||||
|
.iter()
|
||||||
|
.map(|o| ConfigFieldOption { value: o.value.clone(), label_zh: o.label_zh.clone() })
|
||||||
|
.collect();
|
||||||
fields.push(ConfigFieldState {
|
fields.push(ConfigFieldState {
|
||||||
id: field.id.clone(),
|
id: field.id.clone(),
|
||||||
label_zh: field.label_zh.clone(),
|
label_zh: field.label_zh.clone(),
|
||||||
@@ -280,6 +302,10 @@ pub fn read_config(adapter: &Adapter, secrets: &dyn SecretStore) -> Result<Confi
|
|||||||
storage: field.storage.clone(),
|
storage: field.storage.clone(),
|
||||||
value,
|
value,
|
||||||
has_value,
|
has_value,
|
||||||
|
docs_url: field.docs_url.clone(),
|
||||||
|
options,
|
||||||
|
env_key,
|
||||||
|
group: field.group.clone().unwrap_or_else(|| "common".into()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,7 +319,19 @@ pub fn read_config(adapter: &Adapter, secrets: &dyn SecretStore) -> Result<Confi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ConfigFormState { cli_id, files, fields, environment })
|
// 官方授权方式(授权/登录层引导)
|
||||||
|
let mut auth_modes = Vec::new();
|
||||||
|
if let Some(auth) = adapter.authorization.as_ref() {
|
||||||
|
for m in &auth.modes {
|
||||||
|
auth_modes.push(AuthModeInfo {
|
||||||
|
mode: m.mode.clone(),
|
||||||
|
notes_zh: m.notes_zh.clone(),
|
||||||
|
command: m.command.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ConfigFormState { cli_id, files, fields, environment, auth_modes })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从配置文件读取字段值(首个声明了该字段可写的文件)。
|
/// 从配置文件读取字段值(首个声明了该字段可写的文件)。
|
||||||
@@ -353,7 +391,7 @@ pub fn write_config(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
match field.storage.as_str() {
|
match field.storage.as_str() {
|
||||||
"keyring" | "env" => {
|
"keyring" => {
|
||||||
match secrets.set(&service, &field.id, value) {
|
match secrets.set(&service, &field.id, value) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
written_fields.push(field.id.clone());
|
written_fields.push(field.id.clone());
|
||||||
@@ -364,7 +402,7 @@ pub fn write_config(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"file" => {
|
"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| 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 {
|
||||||
@@ -406,6 +444,96 @@ pub fn write_config(
|
|||||||
Ok(WriteResult { backup_path, written_file, written_fields, errors })
|
Ok(WriteResult { backup_path, written_file, written_fields, errors })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// 配置「生效检查」(writeConfig 后验证,架构 §C「配置后验证」)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
/// 写配置后验证:配置文件能否被解析、CLI 本身是否认可(--version 探测)。
|
||||||
|
/// 不真调付费 API——验证到「CLI 认可配置」为止,并如实标注验证层级。
|
||||||
|
pub fn verify_config(adapter: &Adapter) -> ConfigVerifyResult {
|
||||||
|
let cli_id = adapter.id.clone();
|
||||||
|
|
||||||
|
// 1. 重读配置文件并解析(有配置文件声明时)
|
||||||
|
if let Some(cfg) = adapter.configuration.as_ref() {
|
||||||
|
for f in &cfg.files {
|
||||||
|
let path = cfg::resolve_path(&f.path);
|
||||||
|
if !path.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let text = match std::fs::read_to_string(&path) {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
return ConfigVerifyResult {
|
||||||
|
cli_id,
|
||||||
|
ok: false,
|
||||||
|
level: "config_parse_failed".into(),
|
||||||
|
message_zh: format!("配置文件读取失败:{e}"),
|
||||||
|
detail: Some(path.to_string_lossy().to_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = cfg::parse(cfg::ConfigFormat::from_str(&f.format), &text) {
|
||||||
|
return ConfigVerifyResult {
|
||||||
|
cli_id,
|
||||||
|
ok: false,
|
||||||
|
level: "config_parse_failed".into(),
|
||||||
|
message_zh: format!("配置文件解析失败:{}", f.path),
|
||||||
|
detail: Some(e.to_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 若 CLI 已安装,运行版本探测,确认 CLI 本身可正常启动(不真调 API)
|
||||||
|
let detect = detect_one(adapter);
|
||||||
|
if detect.status == "installed" {
|
||||||
|
let exe = detect.executable.as_deref().unwrap_or_default();
|
||||||
|
let version_args: Vec<String> = adapter
|
||||||
|
.detect
|
||||||
|
.as_ref()
|
||||||
|
.map(|d| d.version_args.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let args: Vec<String> = if version_args.is_empty() {
|
||||||
|
vec!["--version".to_string()]
|
||||||
|
} else {
|
||||||
|
version_args
|
||||||
|
};
|
||||||
|
match run_capture(std::path::Path::new(exe), &args) {
|
||||||
|
Ok((true, text)) => {
|
||||||
|
let v = detect.version.clone();
|
||||||
|
return ConfigVerifyResult {
|
||||||
|
cli_id,
|
||||||
|
ok: true,
|
||||||
|
level: "cli_accepted".into(),
|
||||||
|
message_zh: format!(
|
||||||
|
"配置文件已写入且可解析;{} 版本探测正常{}。未实际调用付费 API,验证到「CLI 认可配置」为止。",
|
||||||
|
adapter.name_zh,
|
||||||
|
v.as_deref().map(|v| format!("({v})")).unwrap_or_default()
|
||||||
|
),
|
||||||
|
detail: Some(text.trim().to_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok((false, _)) | Err(_) => {
|
||||||
|
return ConfigVerifyResult {
|
||||||
|
cli_id,
|
||||||
|
ok: true,
|
||||||
|
level: "config_parsed".into(),
|
||||||
|
message_zh: "配置文件已写入且可解析;CLI 版本探测未成功,仅验证到「配置可解析」一层。".into(),
|
||||||
|
detail: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigVerifyResult {
|
||||||
|
cli_id,
|
||||||
|
ok: true,
|
||||||
|
level: "not_installed".into(),
|
||||||
|
message_zh: "配置文件已写入且可解析;该 CLI 尚未安装,未做 CLI 实际校验(以官方文档为准)。".into(),
|
||||||
|
detail: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// 授权
|
// 授权
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@@ -660,7 +788,26 @@ where
|
|||||||
return Err(EngineError::NotSupported(format!("{} 未声明可执行的 {label} 命令", adapter.id)));
|
return Err(EngineError::NotSupported(format!("{} 未声明可执行的 {label} 命令", adapter.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(ActionEvent::step(format!("开始{label} {}({})", adapter.name_zh, adapter.id)));
|
// 渠道信息(安装时展示;更新/卸载无渠道概念)
|
||||||
|
let channel_hint = if action == AdapterAction::Install {
|
||||||
|
adapter
|
||||||
|
.install
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|i| pick_channel(i, opts.channel.as_deref()))
|
||||||
|
.map(|c| c.id.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
emit(ActionEvent::step_phase(
|
||||||
|
"prepare",
|
||||||
|
format!(
|
||||||
|
"准备{label} {}({}){}",
|
||||||
|
adapter.name_zh,
|
||||||
|
adapter.id,
|
||||||
|
channel_hint.as_deref().map(|c| format!("· 渠道 {c}")).unwrap_or_default()
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
for cmd in &commands {
|
for cmd in &commands {
|
||||||
let prog = &cmd[0];
|
let prog = &cmd[0];
|
||||||
@@ -671,30 +818,43 @@ where
|
|||||||
.first()
|
.first()
|
||||||
.map(|p| p.to_string_lossy().to_string())
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
.unwrap_or_else(|| prog.clone());
|
.unwrap_or_else(|| prog.clone());
|
||||||
emit(ActionEvent::step(format!(
|
emit(ActionEvent::step_phase(
|
||||||
"执行:{} {}",
|
"exec",
|
||||||
prog,
|
format!("执行命令:{} {}", prog, args.join(" ")),
|
||||||
args.join(" ")
|
));
|
||||||
)));
|
|
||||||
|
|
||||||
let ok = run_streaming(&exe, &args, |is_err, line| {
|
let result = run_streaming(&exe, &args, |is_err, line| {
|
||||||
let r = redact(line);
|
let r = redact(line);
|
||||||
if is_err {
|
if is_err {
|
||||||
emit(ActionEvent::stderr(r));
|
emit(ActionEvent::stderr(r));
|
||||||
} else {
|
} else {
|
||||||
emit(ActionEvent::stdout(r));
|
emit(ActionEvent::stdout(r));
|
||||||
}
|
}
|
||||||
})?;
|
});
|
||||||
|
|
||||||
|
let ok = match result {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(e) => {
|
||||||
|
let hint = diag_hint_for_install_failure(adapter, &e.to_string());
|
||||||
|
emit(ActionEvent::error(format!(
|
||||||
|
"{label}失败:无法启动命令({e})。{hint}"
|
||||||
|
)));
|
||||||
|
return Err(EngineError::Exec(format!("{label} 命令启动失败: {e}")));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
emit(ActionEvent::error(format!("{label} 命令退出码非 0")));
|
let hint = diag_hint_for_install_failure(adapter, "");
|
||||||
|
emit(ActionEvent::error(format!(
|
||||||
|
"{label}失败:命令退出码非 0(原始错误见上方 stderr 输出)。{hint}"
|
||||||
|
)));
|
||||||
return Err(EngineError::Exec(format!("{label} 命令退出码非 0")));
|
return Err(EngineError::Exec(format!("{label} 命令退出码非 0")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 安装后自动复检
|
// 安装后自动复检(当场复检并展示版本/路径,不只看事后 detect)
|
||||||
if action == AdapterAction::Install {
|
if action == AdapterAction::Install {
|
||||||
emit(ActionEvent::step("安装完成,自动复检…"));
|
emit(ActionEvent::step_phase("verify", "安装命令执行完成,正在复检安装结果…"));
|
||||||
let detect = detect_one(adapter);
|
let detect = detect_one(adapter);
|
||||||
emit(ActionEvent::done(Some(serde_json::to_value(&detect).unwrap_or(json!({})))));
|
emit(ActionEvent::done(Some(serde_json::to_value(&detect).unwrap_or(json!({})))));
|
||||||
} else {
|
} else {
|
||||||
@@ -704,6 +864,25 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 安装失败时的中文诊断建议(按适配器声明的依赖/渠道给针对性提示,不吞原始错误)。
|
||||||
|
fn diag_hint_for_install_failure(adapter: &Adapter, spawn_err: &str) -> String {
|
||||||
|
let mut parts: Vec<String> = Vec::new();
|
||||||
|
if !spawn_err.is_empty() {
|
||||||
|
parts.push("请确认所需命令已安装并加入 PATH(可到「诊断」页排查)".into());
|
||||||
|
}
|
||||||
|
let deps: Vec<&str> = adapter
|
||||||
|
.runtime_deps
|
||||||
|
.iter()
|
||||||
|
.filter(|d| d.required_for.iter().any(|r| r == "install" || r == "run"))
|
||||||
|
.map(|d| d.id.as_str())
|
||||||
|
.collect();
|
||||||
|
if !deps.is_empty() {
|
||||||
|
parts.push(format!("依赖检查:{}", deps.join("、")));
|
||||||
|
}
|
||||||
|
parts.push("常见原因:网络不通 / 依赖版本不符 / 需要管理员权限,可在「诊断」页查看详细结论。".into());
|
||||||
|
format!("{}", parts.join(";"))
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// 测试
|
// 测试
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@@ -845,6 +1024,40 @@ configuration:
|
|||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_official_codex_fields_to_file_and_keyring() {
|
||||||
|
// 用真实 adapters/tools/codex.yaml 验证官方字段(含 enum)写入 + 密钥铁律
|
||||||
|
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 == "codex").unwrap();
|
||||||
|
|
||||||
|
let tmp = std::env::temp_dir().join(format!("agentdock-core-official-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&tmp);
|
||||||
|
std::fs::create_dir_all(&tmp).unwrap();
|
||||||
|
let file = tmp.join("config.toml");
|
||||||
|
if let Some(c) = adapter.configuration.as_mut() {
|
||||||
|
c.files[0].path = file.to_string_lossy().to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let secrets = MockSecretStore::new();
|
||||||
|
let mut patch = std::collections::BTreeMap::new();
|
||||||
|
patch.insert("model".to_string(), "gpt-5.6-terra".to_string());
|
||||||
|
patch.insert("approval_policy".to_string(), "on-failure".to_string());
|
||||||
|
patch.insert("openai_base_url".to_string(), "http://proxy.example.com".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(), 4);
|
||||||
|
let text = std::fs::read_to_string(&file).unwrap();
|
||||||
|
assert!(text.contains("gpt-5.6-terra"), "默认模型应写入配置: {text}");
|
||||||
|
assert!(text.contains("on-failure"), "审批策略(enum)应写入配置: {text}");
|
||||||
|
assert!(text.contains("http://proxy.example.com"), "Base URL 应写入配置: {text}");
|
||||||
|
assert!(!text.contains("sk-test-xxx"), "API Key 不得写入配置文件");
|
||||||
|
assert!(secrets.has(&service_name("codex"), "api_key"));
|
||||||
|
|
||||||
|
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();
|
||||||
@@ -966,6 +1179,60 @@ configuration:
|
|||||||
.contains(&r.status.as_str()));
|
.contains(&r.status.as_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_config_reports_parse_failure() {
|
||||||
|
let adapter = codex_adapter();
|
||||||
|
let dir = std::env::temp_dir().join(format!("agentdock-core-verify-bad-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let file = dir.join("config.toml");
|
||||||
|
std::fs::write(&file, "model = [unclosed").unwrap();
|
||||||
|
let mut adapter = adapter;
|
||||||
|
if let Some(c) = adapter.configuration.as_mut() {
|
||||||
|
c.files[0].path = file.to_string_lossy().to_string();
|
||||||
|
}
|
||||||
|
let r = verify_config(&adapter);
|
||||||
|
assert_eq!(r.level, "config_parse_failed");
|
||||||
|
assert!(!r.ok);
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_config_valid_file_passes_without_cli() {
|
||||||
|
// CLI 未安装时应返回 ok=true(配置可解析),level 为 not_installed 或 cli_accepted
|
||||||
|
let adapter = codex_adapter();
|
||||||
|
let dir = std::env::temp_dir().join(format!("agentdock-core-verify-ok-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let file = dir.join("config.toml");
|
||||||
|
std::fs::write(&file, "model = \"gpt-5\"\n").unwrap();
|
||||||
|
let mut adapter = adapter;
|
||||||
|
if let Some(c) = adapter.configuration.as_mut() {
|
||||||
|
c.files[0].path = file.to_string_lossy().to_string();
|
||||||
|
}
|
||||||
|
let r = verify_config(&adapter);
|
||||||
|
assert!(r.ok);
|
||||||
|
assert!(["not_installed", "cli_accepted", "config_parsed"].contains(&r.level.as_str()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 真机配置「生效检查」:对已安装的 codex 跑 verify_config,打印验证层级。
|
||||||
|
/// 运行:cargo test -p agentdock-core real_machine_verify_codex -- --ignored --nocapture
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn real_machine_verify_codex() {
|
||||||
|
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../adapters");
|
||||||
|
let adapters = load_adapters(&dir).expect("适配器目录应可加载");
|
||||||
|
let codex = adapters.iter().find(|a| a.id == "codex").expect("缺少 codex");
|
||||||
|
let r = verify_config(codex);
|
||||||
|
println!("codex verify: level={} ok={}", r.level, r.ok);
|
||||||
|
println!(" message={}", r.message_zh);
|
||||||
|
if let Some(d) = &r.detail {
|
||||||
|
println!(" detail={}", d);
|
||||||
|
}
|
||||||
|
assert!(r.ok, "codex 已安装且配置可解析时应 ok=true");
|
||||||
|
}
|
||||||
|
|
||||||
/// 真机检测记录:对首批五件套逐项 detect 并打印结果(自测记录用)。
|
/// 真机检测记录:对首批五件套逐项 detect 并打印结果(自测记录用)。
|
||||||
/// 运行:cargo test -p agentdock-core real_machine_detect_five -- --ignored --nocapture
|
/// 运行:cargo test -p agentdock-core real_machine_detect_five -- --ignored --nocapture
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ pub mod error;
|
|||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
pub use engine::{ActionOpts, Engine, auth_status, authorize, detect_one, diagnose, parse_version, read_config, run_action, write_config};
|
pub use engine::{ActionOpts, Engine, auth_status, authorize, detect_one, diagnose, parse_version, read_config, run_action, verify_config, write_config};
|
||||||
pub use error::EngineError;
|
pub use error::EngineError;
|
||||||
pub use types::{
|
pub use types::{
|
||||||
ActionEvent, AuthStatus, ConfigFieldState, ConfigFileState, ConfigFormState, DetectResult,
|
ActionEvent, AuthStatus, AuthModeInfo, ConfigFieldState, ConfigFileState, ConfigFormState, DetectResult,
|
||||||
EnvFieldState, WriteResult, now_secs,
|
EnvFieldState, WriteResult, ConfigVerifyResult, now_secs,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,6 +48,20 @@ pub struct ConfigFieldState {
|
|||||||
pub value: Option<String>,
|
pub value: Option<String>,
|
||||||
/// 密钥库类字段:是否已有值
|
/// 密钥库类字段:是否已有值
|
||||||
pub has_value: bool,
|
pub has_value: bool,
|
||||||
|
/// 官方文档链接(配置项依据)
|
||||||
|
pub docs_url: Option<String>,
|
||||||
|
/// enum 类型的可选值(value + 中文名)
|
||||||
|
pub options: Vec<ConfigFieldOption>,
|
||||||
|
/// 该字段(keyring 类)对应的环境变量名(如 OPENAI_API_KEY),供界面说明
|
||||||
|
pub env_key: Option<String>,
|
||||||
|
/// 表单分组:auth | common | advanced(未声明按 common)
|
||||||
|
pub group: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConfigFieldOption {
|
||||||
|
pub value: String,
|
||||||
|
pub label_zh: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 配置文件解析状态。
|
/// 配置文件解析状态。
|
||||||
@@ -77,6 +91,18 @@ pub struct ConfigFormState {
|
|||||||
pub files: Vec<ConfigFileState>,
|
pub files: Vec<ConfigFileState>,
|
||||||
pub fields: Vec<ConfigFieldState>,
|
pub fields: Vec<ConfigFieldState>,
|
||||||
pub environment: Vec<EnvFieldState>,
|
pub environment: Vec<EnvFieldState>,
|
||||||
|
/// 官方授权方式(授权/登录层引导用)
|
||||||
|
pub auth_modes: Vec<AuthModeInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 官方授权方式(授权/登录层展示)。
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AuthModeInfo {
|
||||||
|
/// browser_oauth | device_code | api_key | local_tui
|
||||||
|
pub mode: String,
|
||||||
|
pub notes_zh: Option<String>,
|
||||||
|
/// 交互式登录命令(浏览器/设备码等;api_key 类常为空)
|
||||||
|
pub command: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写配置结果(writeConfig 返回)。
|
/// 写配置结果(writeConfig 返回)。
|
||||||
@@ -92,6 +118,20 @@ pub struct WriteResult {
|
|||||||
pub errors: Vec<String>,
|
pub errors: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 配置写入后的「生效检查」结果(verifyConfig 返回)。
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConfigVerifyResult {
|
||||||
|
pub cli_id: String,
|
||||||
|
/// 整体是否通过(配置文件可解析即视为「已写入成功」)
|
||||||
|
pub ok: bool,
|
||||||
|
/// 验证层级:cli_accepted | config_parsed | config_parse_failed | not_installed
|
||||||
|
pub level: String,
|
||||||
|
/// 中文结论(如实标注验证到哪一层)
|
||||||
|
pub message_zh: String,
|
||||||
|
/// 脱敏后的补充信息(CLI 版本输出 / 解析错误)
|
||||||
|
pub detail: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// 动作流事件(runAction 流式输出)。
|
/// 动作流事件(runAction 流式输出)。
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ActionEvent {
|
pub struct ActionEvent {
|
||||||
@@ -99,24 +139,29 @@ pub struct ActionEvent {
|
|||||||
pub kind: String,
|
pub kind: String,
|
||||||
/// 已脱敏的消息文本
|
/// 已脱敏的消息文本
|
||||||
pub message: String,
|
pub message: String,
|
||||||
|
/// 当前阶段:prepare | exec | verify(供界面渲染步骤进度)
|
||||||
|
pub phase: Option<String>,
|
||||||
pub data: Option<serde_json::Value>,
|
pub data: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionEvent {
|
impl ActionEvent {
|
||||||
pub fn step(msg: impl Into<String>) -> Self {
|
pub fn step(msg: impl Into<String>) -> Self {
|
||||||
ActionEvent { kind: "step".into(), message: msg.into(), data: None }
|
ActionEvent { kind: "step".into(), message: msg.into(), phase: None, data: None }
|
||||||
|
}
|
||||||
|
pub fn step_phase(phase: &str, msg: impl Into<String>) -> Self {
|
||||||
|
ActionEvent { kind: "step".into(), message: msg.into(), phase: Some(phase.into()), data: None }
|
||||||
}
|
}
|
||||||
pub fn stdout(line: impl Into<String>) -> Self {
|
pub fn stdout(line: impl Into<String>) -> Self {
|
||||||
ActionEvent { kind: "stdout".into(), message: line.into(), data: None }
|
ActionEvent { kind: "stdout".into(), message: line.into(), phase: Some("exec".into()), data: None }
|
||||||
}
|
}
|
||||||
pub fn stderr(line: impl Into<String>) -> Self {
|
pub fn stderr(line: impl Into<String>) -> Self {
|
||||||
ActionEvent { kind: "stderr".into(), message: line.into(), data: None }
|
ActionEvent { kind: "stderr".into(), message: line.into(), phase: Some("exec".into()), data: None }
|
||||||
}
|
}
|
||||||
pub fn done(data: Option<serde_json::Value>) -> Self {
|
pub fn done(data: Option<serde_json::Value>) -> Self {
|
||||||
ActionEvent { kind: "done".into(), message: String::new(), data }
|
ActionEvent { kind: "done".into(), message: String::new(), phase: Some("verify".into()), data }
|
||||||
}
|
}
|
||||||
pub fn error(msg: impl Into<String>) -> Self {
|
pub fn error(msg: impl Into<String>) -> Self {
|
||||||
ActionEvent { kind: "error".into(), message: msg.into(), data: None }
|
ActionEvent { kind: "error".into(), message: msg.into(), phase: None, data: None }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user