diff --git a/README.md b/README.md index 66f5d3a86..cd267bb97 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,69 @@ -# CN-CC: Chinese Model Backends for Claude Code +# CN-CC -Route tasks from Claude Code to **7 Chinese AI model backends**. Each backend runs as an isolated Claude Code instance with its own API provider and provider-specific profile controls. +Chinese model backends for Claude Code. -> Inspired by [openai/codex-plugin-cc](https://github.com/openai/codex-plugin-cc) — the plugin architecture that made multi-model delegation in Claude Code possible. Thank you, OpenAI. +CN-CC lets a Claude Code session delegate a task to isolated Claude Code wrappers backed by Chinese AI providers. It ships both a Claude Code plugin and a reproducible snapshot of the local `cc-*` launchers used to run those providers. -## Models +> Inspired by [openai/codex-plugin-cc](https://github.com/openai/codex-plugin-cc), whose plugin architecture made this style of multi-model delegation practical. -| Model | Backend (default tier) | Strength | Command | -|-------|---------|----------|---------| -| **Doubao** | doubao-seed-code-preview-latest | General Chinese coding, frontend/vision coding, Seed 2.0/router fallback | `/cn:doubao` | -| **Qwen** | qwen3-coder-next (opus → qwen3-coder-plus) | Agentic coding, SQL / Alibaba ecosystem, Token Plan/PayG routes | `/cn:qwen` | -| **Kimi** | kimi-for-coding | Stable Kimi Code route, long context, 64K out | `/cn:kimi` | -| **GLM** | glm-4.7 (opus → glm-5.1) | Reasoning / Chinese understanding, Z.ai Claude Code route | `/cn:glm` | -| **StepFun** | step-3.5-flash-2603 (sonnet+) | Math / logic, vision, **64K out** | `/cn:stepfun` | -| **MiniMax** | MiniMax-M2.7 | Stable M2 route, highspeed/cheap profiles, **64K Anthropic out** | `/cn:minimax` | -| **MiMo** | mimo-v2-pro (Token Plan SGP) | Xiaomi flagship, **1M context**, V2.5/Omni/Flash profiles | `/cn:mimo` | +## What This Repo Contains -## Install +| Area | Path | Purpose | +|------|------|---------| +| Plugin commands | `plugins/cn/commands/` | Slash commands such as `/cn:qwen`, `/cn:setup`, `/cn:doctor`, and `/cn:profiles` | +| Routing agent | `plugins/cn/agents/cn-dispatch.md` | Selects a backend for `/cn:ask` and auto-dispatch use cases | +| Routing skill | `plugins/cn/skills/cn-routing/` | Decision matrix for model selection | +| Companion runtime | `plugins/cn/scripts/cn-companion.mjs` | Runs setup, doctor, profile listing, and one-shot delegated tasks | +| Runtime tests | `plugins/cn/scripts/cn-companion.test.mjs` | Smoke tests for CLI parsing and profile handling | +| Launcher snapshot | `launchers/` | Reproducible copy of the local `~/bin/cc-*` wrappers and provider prompts | + +## Model Matrix + +All entries below refer to the validated local launcher snapshot in `launchers/` and the companion registry in `cn-companion.mjs`. + +| Backend | Default | Heavy tier | Fast tier | Best fit | Command | +|---------|---------|------------|-----------|----------|---------| +| Doubao | `doubao-seed-code-preview-latest` | `doubao-seed-2.0-code` | `doubao-seed-2.0-lite` | General coding, frontend/vision coding, Seed 2.0/router fallback | `/cn:doubao` | +| Qwen | `qwen3-coder-next` | `qwen3-coder-plus` / `qwen3.7-max` | `qwen3-coder-flash` | Agentic coding, SQL, Alibaba ecosystem, Coding Plan/Token Plan/PayG routes | `/cn:qwen` | +| Kimi | `kimi-for-coding` | `kimi-for-coding` | `kimi-for-coding` | Stable Kimi Code route, long-context coding, 64K output | `/cn:kimi` | +| GLM | `glm-4.7` | `glm-5.1` | `glm-4.5-air` | Chinese reasoning, long-horizon debugging, Z.ai Claude Code route | `/cn:glm` | +| StepFun | `step-3.5-flash-2603` | `step-3.5-flash-2603` | `step-3.5-flash` | Math, logic, reasoning effort control | `/cn:stepfun` | +| MiniMax | `MiniMax-M2.7` | `MiniMax-M2.7` | `MiniMax-M2.5-highspeed` | Stable M2 coding, highspeed/cheap profiles, 64K Anthropic output | `/cn:minimax` | +| MiMo | `mimo-v2-pro` | `mimo-v2.5-pro` | `mimo-v2-flash` where available | Xiaomi token-plan Pro, V2.5, Omni, Flash routes | `/cn:mimo` | + +## Profile Matrix + +Profiles are passed through to the corresponding wrapper environment variable. + +| Command | Env var | Profiles | +|---------|---------|----------| +| `/cn:doubao` | `DOUBAO_PROFILE` | `latest`, `agent`, `vision`, `frontend`, `pinned`, `router`, `seed20`, `reasoning`, `cheap` | +| `/cn:qwen` | `QWEN_PROFILE` | `coder`, `coding-plan`, `plan`, `token`, `token-plan`, `payg`, `intl`, `max`, `cheap`, `flash` | +| `/cn:kimi` | none | Stable `kimi-for-coding` route; use `KIMI_*` env knobs | +| `/cn:glm` | `GLM_PROFILE` | `balanced`, `max`, `opus`, `turbo`, `cheap`, `lite` | +| `/cn:stepfun` | `STEPFUN_PROFILE` | `reasoning`, `fast`, `flash`, `router` | +| `/cn:minimax` | `MINIMAX_PROFILE` | `stable`, `token`, `highspeed`, `payg`, `cheap`, `lite` | +| `/cn:mimo` | `MIMO_PROFILE` | `pro`, `latest`, `v25`, `multimodal`, `omni`, `fast`, `flash` | + +Examples: + +```bash +/cn:qwen --profile token 帮我做一次复杂代码审查 +/cn:glm --profile max 分析这个性能瓶颈 +/cn:doubao --profile vision 检查这个前端组件 +/cn:minimax --profile highspeed 快速总结这批日志 +/cn:mimo --profile latest 处理一个长上下文多模态任务 +``` + +If the prompt itself starts with flags, separate command options from prompt text with `--`: + +```bash +/cn:qwen --profile token -- --json 这个参数是什么意思? +``` + +## Install Plugin + +Inside Claude Code: ```bash /plugin marketplace add LeoLin990405/cn-cc @@ -25,7 +72,7 @@ Route tasks from Claude Code to **7 Chinese AI model backends**. Each backend ru /cn:setup ``` -Or add to `~/.claude/settings.json`: +Or add the marketplace manually to `~/.claude/settings.json`: ```json { @@ -43,174 +90,212 @@ Or add to `~/.claude/settings.json`: } ``` -## Usage +## Install Launcher Snapshot -### Check backends +The plugin expects executable wrappers such as `cc-qwen`, `cc-kimi`, and `cc-glm` in `~/bin`. + +Install the snapshot: ```bash -/cn:setup +./launchers/install.sh ``` +Install the snapshot and the pinned Claude Code package into each provider environment: + +```bash +./launchers/install.sh --install-claude-code ``` -CN Models Setup — 7/7 available - ✓ doubao Doubao (doubao-seed-code-preview-latest) 2.1.150 (Claude Code) - ✓ qwen Qwen (qwen3-coder-next; opus→qwen3-coder-plus) 2.1.150 (Claude Code) - ✓ kimi Kimi (kimi-for-coding) 2.1.150 (Claude Code) - ✓ glm GLM (glm-4.7; opus→glm-5.1) 2.1.150 (Claude Code) - ✓ stepfun StepFun (step-3.5-flash-2603) 2.1.150 (Claude Code) - ✓ minimax MiniMax (MiniMax-M2.7) 2.1.150 (Claude Code) - ✓ mimo MiMo (mimo-v2-pro) 2.1.150 (Claude Code) +Verify the snapshot without starting real model sessions: + +```bash +./launchers/verify.sh +cc-models doctor ``` -### Provider profiles +The snapshot intentionally excludes API keys, `node_modules`, Claude Code history, caches, and plugin state. + +## API Keys -Direct commands pass arguments through to `cn-companion.mjs`, so provider profiles can be selected inline: +Set provider keys in your shell environment or in `~/.config/cc-model-secrets.env`. ```bash -/cn:qwen --profile token 帮我做一次复杂代码审查 -/cn:glm --profile max 分析这个性能瓶颈 -/cn:doubao --profile vision 检查这个前端组件 -/cn:minimax --profile highspeed 快速总结这批日志 -/cn:mimo --profile latest 处理一个长上下文多模态任务 +export ARK_API_KEY="..." # Doubao / Volcengine +export DASHSCOPE_API_KEY="..." # Qwen / DashScope +export KIMI_API_KEY="..." # Kimi +export GLM_API_KEY="..." # GLM / Z.ai +export ZAI_API_KEY="..." # GLM / Z.ai alias +export DEEPSEEK_API_KEY="..." # DeepSeek launcher snapshot +export STEPFUN_API_KEY="..." # StepFun +export MINIMAX_API_KEY="..." # MiniMax +export MIMO_API_KEY="..." # MiMo +export LONGCAT_API_KEY="..." # LongCat launcher snapshot ``` -If the prompt itself starts with flags, separate command options from prompt text with `--`: +## Usage + +### Smart Routing ```bash -/cn:qwen --profile token -- --json 这个参数是什么意思? +/cn:ask 帮我写一个 Doris 数据仓库的 ETL SQL +/cn:ask 分析这篇 8 万字的研究报告 +/cn:ask 证明这个不等式 +/cn:ask 写一个 Python 爬虫 ``` -Use `/cn:setup --doctor` for the deeper wrapper health check, or run the companion directly: +Routing rules: + +| Signal | Route | Why | +|--------|-------|-----| +| SQL, Doris, ADB, PolarDB, RDS, DashScope | Qwen | Alibaba ecosystem native | +| Ultra-long context, multimodal, cross-repo analysis | MiMo | Token-plan Pro plus V2.5/Omni profiles | +| Long text, papers, contracts, document synthesis | Kimi | Stable Kimi Code long-context route | +| Math, proofs, logic, equations, derivations | StepFun | Reasoning effort controls | +| Chinese semantic reasoning and knowledge work | GLM | Strong Chinese reasoning route | +| Quick or low-latency jobs | MiniMax | Stable/highspeed M2 profiles | +| General Chinese coding | Doubao | Default all-round coder | + +### Direct Commands ```bash -node "${CLAUDE_PLUGIN_ROOT}/scripts/cn-companion.mjs" profiles +/cn:kimi +/cn:qwen [--profile token] +/cn:glm [--profile max] +/cn:doubao [--profile vision] +/cn:stepfun [--profile reasoning] +/cn:minimax [--profile highspeed] +/cn:mimo [--profile latest] ``` -### Smart routing +### Health And Discovery ```bash -/cn:ask 帮我写一个 Doris 数据仓库的 ETL SQL # → Qwen -/cn:ask 分析这篇 8 万字的研究报告 # → Kimi -/cn:ask 证明这个不等式 # → StepFun -/cn:ask 写一个 Python 爬虫 # → Doubao +/cn:setup # quick availability check +/cn:setup --doctor # availability check using wrapper doctors +/cn:doctor # deeper wrapper health panel +/cn:doctor --model qwen --profile token --json +/cn:profiles # provider profile map +/cn:status --json # concise machine-readable status ``` -The `cn-dispatch` agent reads task signals and picks the best model: - -| Signal | Routes to | Why | -|--------|-----------|-----| -| SQL / Doris / ADB / PolarDB | Qwen | Alibaba ecosystem native | -| Long text 50K–200K tokens | Kimi | stable Kimi Code long-context route | -| Ultra-long context / multimodal | MiMo | token-plan Pro plus V2.5/Omni profiles | -| Math / proofs / logic | StepFun | Math specialist | -| Deep reasoning / Chinese NLU | GLM | Strong Chinese reasoning | -| Quick / lightweight tasks | MiniMax | Stable/highspeed M2 profiles | -| General Chinese coding | Doubao | Best all-round (default) | +## Companion CLI -### Direct commands +The plugin commands call the companion script. You can also run it directly: ```bash -/cn:kimi # Long context -/cn:qwen # SQL / Alibaba -/cn:glm # Reasoning -/cn:doubao # General coding -/cn:stepfun # Math / logic -/cn:minimax # High-speed -/cn:mimo # Xiaomi MiMo, 1M ctx +node plugins/cn/scripts/cn-companion.mjs setup +node plugins/cn/scripts/cn-companion.mjs doctor --model qwen --profile token +node plugins/cn/scripts/cn-companion.mjs profiles +node plugins/cn/scripts/cn-companion.mjs task --model qwen --profile token --dry-run -- --json as literal prompt text ``` -### Auto-dispatch - -The `cn-dispatch` agent is also triggered automatically by Claude when it detects a task that would benefit from a Chinese model backend. No slash command needed. +`--dry-run` validates parsing without calling a model backend. ## Architecture +```mermaid +flowchart TD + A["Claude Code session"] --> B["/cn:ask or /cn: command"] + B --> C["cn-companion.mjs"] + B --> D["cn-dispatch agent"] + D --> E["cn-routing skill"] + E --> C + C --> F["~/bin/cc- wrapper"] + F --> G["isolated ~/.claude-envs/"] + G --> H["provider Anthropic-compatible API"] ``` -Claude Code (main session, Claude Opus/Sonnet) - │ - ├─ /cn:ask "prompt" ← user-triggered smart routing - │ └─ cn-dispatch agent - │ ├─ cn-routing skill → selects model - │ └─ cn-companion.mjs task --model "prompt" - │ └─ cc- -p "prompt" --max-turns 1 - │ └─ isolated CC instance → provider API - │ - ├─ /cn:kimi "prompt" ← user-triggered direct - │ └─ cn-companion.mjs task --model kimi "prompt" - │ - └─ cn-dispatch agent ← auto-triggered by Claude - └─ (same flow as /cn:ask) -``` -## Prerequisites +Each provider wrapper owns: + +- its own `HOME` under `~/.claude-envs/` +- its own Anthropic-compatible endpoint and API key label +- a provider-specific appended prompt +- model tier defaults for Opus/Sonnet/Haiku/Fast routing +- provider-specific compatibility switches such as thinking, output caps, and endpoint profiles -### CC wrapper scripts +## Launcher Operations -Each model needs a wrapper script in `~/bin/`: +Useful local commands: ```bash -#!/usr/bin/env bash -# ~/bin/cc- -REAL_HOME="$HOME" -export HOME="$REAL_HOME/.claude-envs/" -mkdir -p "$HOME" +cc-models list +cc-models matrix +cc-models profiles +cc-models research +cc-models doctor +cc-models audit +``` -export ANTHROPIC_BASE_URL="" -export ANTHROPIC_AUTH_TOKEN="$" -export ANTHROPIC_MODEL="" -export API_TIMEOUT_MS="3000000" -export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC="1" +`cc-models audit` checks syntax, list output, and wrapper doctors. It skips optional registry placeholders such as `cc-gemini`, `cc-grok`, and `cc-local` unless `CC_MODELS_AUDIT_REQUIRE_ALL=1` is set. -exec claude "$@" -``` +## Development -### API keys +Run the plugin tests: -Set these environment variables (e.g. in `~/.zshrc`): +```bash +npm test +``` + +Run the launcher snapshot checks: ```bash -export ARK_API_KEY="..." # Doubao (Volcengine) -export DASHSCOPE_API_KEY="..." # Qwen (Alibaba) -export KIMI_API_KEY="..." # Kimi (Moonshot) -export GLM_API_KEY="..." # GLM (Zhipu) -export STEPFUN_API_KEY="..." # StepFun -export MINIMAX_API_KEY="..." # MiniMax -export MIMO_API_KEY="tp-..." # MiMo Token Plan (SGP region, key starts with tp-) +./launchers/verify.sh ``` -## Plugin Structure +Run the full local wrapper audit: +```bash +cc-models audit ``` + +Recommended iteration loop: + +1. Update the local wrapper in `~/bin` and its prompt in `~/.claude-envs//prompts`. +2. Run targeted checks such as `QWEN_PROFILE=token cc-qwen --doctor`. +3. Copy the validated wrapper and prompt into `launchers/`. +4. Update `README.md`, `cc-model-registry.tsv`, and `cc-model-research.tsv`. +5. Run `npm test`, `./launchers/verify.sh`, and `cc-models audit`. +6. Push the branch and update the PR. + +## Repository Layout + +```text plugins/cn/ ├── agents/ -│ └── cn-dispatch.md # Smart routing agent +│ └── cn-dispatch.md ├── commands/ -│ ├── setup.md # /cn:setup -│ ├── ask.md # /cn:ask (smart routing) -│ ├── status.md # /cn:status -│ ├── doubao.md # /cn:doubao -│ ├── qwen.md # /cn:qwen -│ ├── kimi.md # /cn:kimi -│ ├── glm.md # /cn:glm -│ ├── stepfun.md # /cn:stepfun -│ ├── minimax.md # /cn:minimax -│ ├── mimo.md # /cn:mimo -│ └── profiles.md # /cn:profiles -├── skills/ -│ ├── cn-routing/SKILL.md # Model selection decision matrix -│ └── cn-result-handling/SKILL.md # Output formatting rules -└── scripts/ - ├── cn-companion.mjs # Core runtime - └── cn-companion.test.mjs # CLI parser smoke tests +│ ├── ask.md +│ ├── doctor.md +│ ├── setup.md +│ ├── status.md +│ ├── profiles.md +│ └── .md +├── scripts/ +│ ├── cn-companion.mjs +│ └── cn-companion.test.mjs +└── skills/ + ├── cn-result-handling/ + └── cn-routing/ + launchers/ -├── bin/ # Snapshot of the local cc-* wrappers -└── prompts/ # Provider-specific appended system prompts +├── bin/ +│ ├── cc-models +│ ├── cc-model-lib.sh +│ ├── cc-model-registry.tsv +│ ├── cc-model-research.tsv +│ └── -code +├── prompts/ +│ └── -proactive-tools.md +├── install.sh +└── verify.sh ``` -## Acknowledgements +## Security Notes -This project would not exist without the pioneering work of the [Codex plugin for Claude Code](https://github.com/openai/codex-plugin-cc) by OpenAI. Their plugin architecture — commands, agents, skills, and companion scripts — provided the blueprint that made multi-model delegation in Claude Code practical. We are grateful for their contribution to the open-source ecosystem. +- Do not commit API keys, shell profiles, Claude histories, caches, or `node_modules`. +- The launcher snapshot stores only scripts, prompts, and metadata. +- `--dangerously-skip-permissions` is used only for delegated isolated Claude Code wrappers; use the same caution you would use with any local coding agent. +- Prefer `/cn:doctor` and `cc-models doctor` before running a newly edited wrapper. ## License diff --git a/launchers/bin/cc-models b/launchers/bin/cc-models index 4c6a30ab2..6d08954c3 100755 --- a/launchers/bin/cc-models +++ b/launchers/bin/cc-models @@ -93,20 +93,27 @@ list_models() { doctor_all() { local only="${1:-}" - local cmd path normalized + local cmd path normalized failed=0 normalized="$(normalize_cmd "$only")" for cmd in "${COMMANDS[@]}"; do [ -n "$normalized" ] && [ "$cmd" != "$normalized" ] && continue path="$ROOT/$cmd" if [ ! -x "$path" ]; then - printf '%s: missing\n' "$cmd" + if [ -n "$normalized" ] || [ "${CC_MODELS_DOCTOR_REQUIRE_ALL:-0}" = "1" ]; then + printf '%s: missing\n' "$cmd" + failed=1 + else + printf '%s: missing (optional)\n' "$cmd" + fi continue fi if ! "$path" --doctor; then printf '%s: doctor failed\n' "$cmd" + failed=1 fi echo done + return "$failed" } env_one() { @@ -193,25 +200,25 @@ audit() { local cmd script failed=0 echo "[cc-models] syntax" bash -n "$0" "$ROOT/cc-model-lib.sh" || failed=1 - for cmd in "${COMMANDS[@]}"; do - script="$(script_path_for "$cmd")" - if [ -f "$script" ]; then - bash -n "$script" || failed=1 - else - if [ "${CC_MODELS_AUDIT_REQUIRE_ALL:-0}" = "1" ]; then - echo "[cc-models] missing script for $cmd: $script" >&2 - failed=1 - else - echo "[cc-models] skip missing optional launcher $cmd: $script" >&2 - fi - fi - done + for cmd in "${COMMANDS[@]}"; do + script="$(script_path_for "$cmd")" + if [ -f "$script" ]; then + bash -n "$script" || failed=1 + else + if [ "${CC_MODELS_AUDIT_REQUIRE_ALL:-0}" = "1" ]; then + echo "[cc-models] missing script for $cmd: $script" >&2 + failed=1 + else + echo "[cc-models] skip missing optional launcher $cmd: $script" >&2 + fi + fi + done echo echo "[cc-models] list" list_models || failed=1 echo echo "[cc-models] doctor" - doctor_all || failed=1 + CC_MODELS_DOCTOR_REQUIRE_ALL="${CC_MODELS_AUDIT_REQUIRE_ALL:-0}" doctor_all || failed=1 return "$failed" } diff --git a/package.json b/package.json index b3d94c321..749d027cd 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "scripts": { "setup": "node plugins/cn/scripts/cn-companion.mjs setup", "profiles": "node plugins/cn/scripts/cn-companion.mjs profiles", + "doctor": "node plugins/cn/scripts/cn-companion.mjs doctor", "test": "node --check plugins/cn/scripts/cn-companion.mjs && node plugins/cn/scripts/cn-companion.test.mjs" } } diff --git a/plugins/cn/commands/doctor.md b/plugins/cn/commands/doctor.md new file mode 100644 index 000000000..e4f6a8efd --- /dev/null +++ b/plugins/cn/commands/doctor.md @@ -0,0 +1,13 @@ +--- +description: Run deeper health checks for Chinese model wrappers +argument-hint: '[--model ] [--profile ] [--json]' +allowed-tools: Bash(node:*) +--- + +Run: + +```bash +node "${CLAUDE_PLUGIN_ROOT}/scripts/cn-companion.mjs" doctor $ARGUMENTS +``` + +Summarize which wrappers are healthy. If `--json` is provided, return the JSON output. diff --git a/plugins/cn/scripts/cn-companion.mjs b/plugins/cn/scripts/cn-companion.mjs index 6a0e3fe24..ceefc8cf1 100644 --- a/plugins/cn/scripts/cn-companion.mjs +++ b/plugins/cn/scripts/cn-companion.mjs @@ -107,6 +107,19 @@ function parseValueOption(argv, name) { return ""; } +function parseCommonModelArgs(argv) { + const flags = parseBoolFlags(argv, ["--json", "--doctor"]); + const modelName = (parseValueOption(argv, "--model") || "").toLowerCase(); + const profile = (parseValueOption(argv, "--profile") || "").toLowerCase(); + + return { + asJson: flags["--json"], + useDoctor: flags["--doctor"], + modelName, + profile, + }; +} + function parseTaskArgs(argv) { const opts = { asJson: false, @@ -190,6 +203,23 @@ function pingModel(name, opts = {}) { return { available: false, detail: err }; } +function doctorModel(name, opts = {}) { + const bin = MODELS[name].bin; + const result = spawnSync(bin, ["--doctor"], { + timeout: opts.timeout || 10_000, + env: envForModel(name, opts.profile), + stdio: ["ignore", "pipe", "pipe"], + }); + return { + model: name, + profile: opts.profile || null, + ok: result.status === 0, + exitCode: result.status, + stdout: (result.stdout ?? "").toString(), + stderr: (result.stderr ?? "").toString() || result.error?.message || "", + }; +} + function runTask(modelName, prompt, opts = {}) { return new Promise((resolve, reject) => { const bin = resolveBin(modelName); @@ -228,9 +258,7 @@ function runTask(modelName, prompt, opts = {}) { // ── Commands ──────────────────────────────────────────────────────────────── async function handleSetup(argv) { - const flags = parseBoolFlags(argv, ["--json", "--doctor"]); - const asJson = flags["--json"]; - const useDoctor = flags["--doctor"]; + const { asJson, useDoctor } = parseCommonModelArgs(argv); const results = {}; for (const name of MODEL_NAMES) { results[name] = { @@ -257,6 +285,50 @@ async function handleSetup(argv) { } } +function handleDoctor(argv) { + const { asJson, modelName, profile } = parseCommonModelArgs(argv); + const names = modelName ? [modelName] : MODEL_NAMES; + + if (modelName && !MODELS[modelName]) { + const msg = `Unknown model "${modelName}". Available: ${MODEL_NAMES.join(", ")}`; + if (asJson) { console.log(JSON.stringify({ error: msg })); } else { console.error(msg); } + process.exitCode = 1; + return; + } + + try { + if (profile) { + for (const name of names) assertProfile(name, profile); + } + } catch (err) { + const msg = err.message || String(err); + if (asJson) { console.log(JSON.stringify({ error: msg })); } else { console.error(msg); } + process.exitCode = 1; + return; + } + + const results = Object.fromEntries(names.map((name) => [name, doctorModel(name, { profile })])); + const okCount = Object.values(results).filter((result) => result.ok).length; + + if (asJson) { + console.log(JSON.stringify({ ok: okCount, total: names.length, models: results }, null, 2)); + } else { + console.log(`CN Models Doctor — ${okCount}/${names.length} ok\n`); + for (const [name, result] of Object.entries(results)) { + const icon = result.ok ? "✓" : "✗"; + const profileText = result.profile ? ` profile=${result.profile}` : ""; + console.log(` ${icon} ${name}${profileText}`); + const detail = result.ok ? result.stdout : `${result.stderr}\n${result.stdout}`; + const lines = detail.trim().split("\n").filter(Boolean).slice(0, 12); + for (const line of lines) console.log(` ${line}`); + if (detail.trim().split("\n").length > 12) console.log(" ..."); + console.log(""); + } + } + + if (okCount !== names.length) process.exitCode = 1; +} + async function handleTask(argv) { const { asJson, dangerously, dryRun, modelName, profile, cwd, timeout, prompt } = parseTaskArgs(argv); if (!MODELS[modelName]) { @@ -377,6 +449,7 @@ function printUsage() { console.log([ "Usage:", " node cn-companion.mjs setup [--json] [--doctor]", + " node cn-companion.mjs doctor [--model ] [--profile ] [--json]", " node cn-companion.mjs task --model [--profile ] [--timeout ] [--cwd ] [--json] [--dry-run] [--] ", " node cn-companion.mjs ping [--profile ]", " node cn-companion.mjs profiles", @@ -393,6 +466,9 @@ switch (subcommand) { case "setup": await handleSetup(argv); break; + case "doctor": + handleDoctor(argv); + break; case "task": await handleTask(argv); break; diff --git a/plugins/cn/scripts/cn-companion.test.mjs b/plugins/cn/scripts/cn-companion.test.mjs index 07ef6bfaa..71ca98399 100644 --- a/plugins/cn/scripts/cn-companion.test.mjs +++ b/plugins/cn/scripts/cn-companion.test.mjs @@ -72,4 +72,18 @@ function runJson(args) { assert.match(result.stdout, /doubao\s+DOUBAO_PROFILE/); } +{ + const result = run(["doctor", "--model", "not-a-model", "--json"]); + assert.notEqual(result.status, 0); + const parsed = JSON.parse(result.stdout); + assert.match(parsed.error, /Unknown model/); +} + +{ + const result = run(["doctor", "--model", "kimi", "--profile", "max", "--json"]); + assert.notEqual(result.status, 0); + const parsed = JSON.parse(result.stdout); + assert.match(parsed.error, /does not expose profile switching/); +} + console.log("cn-companion tests ok");