From d2dedd2baf677f4ee8d9111957e4b845a8c61264 Mon Sep 17 00:00:00 2001 From: Kris Xia Date: Wed, 20 May 2026 16:15:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(openclaw):=20=E5=A2=9E=E5=8A=A0=20OpenClaw?= =?UTF-8?q?=20=E5=B7=A5=E5=85=B7=E9=85=8D=E7=BD=AE=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 85 +++++++++++++++-- README.zh-CN.md | 93 ++++++++++++++++-- src/commands/auth.ts | 1 + src/lib/command.ts | 1 + src/lib/tool-manager.ts | 2 + src/lib/tools/index.ts | 1 + src/lib/tools/openclaw-tool.ts | 170 +++++++++++++++++++++++++++++++++ tests/openclaw-tool.test.mjs | 51 ++++++++++ 8 files changed, 392 insertions(+), 12 deletions(-) create mode 100644 src/lib/tools/openclaw-tool.ts create mode 100644 tests/openclaw-tool.test.mjs diff --git a/README.md b/README.md index 3c44326..635ece6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) [![Node.js Version](https://img.shields.io/node/v/qiniu-coding-helper.svg)](https://nodejs.org) -**The CLI helper for configuring Claude Code with Qiniu AI API endpoints** +**The CLI helper for configuring AI coding assistants with Qiniu AI API endpoints** [简体中文](README.zh-CN.md) · [Features](#-features) · [Quick Start](#-quick-start) · [Commands](#-commands) · [Configuration](#-configuration) · [FAQ](#-faq) @@ -21,6 +21,8 @@ - **🔐 API Key Management** — Input, validate, save, and revoke API keys - **📦 Model Configuration** — Fetch available models from the API, with manual model ID input support - **⚡ Claude Code Integration** — Automatically writes environment variables to `~/.claude/settings.json` +- **🧩 Codex Integration** — Writes Qiniu provider settings to `~/.codex/config.toml` and stores credentials in Codex auth +- **🦀 OpenClaw Integration** — Uses OpenClaw's config CLI to register a Qiniu OpenAI-compatible provider and default model - **🔍 Health Check** — Built-in `doctor` command to verify config, API Key, network, tools, Git, and Node.js - **🌍 Internationalization** — Supports Chinese (zh_CN) and English (en_US) @@ -29,7 +31,7 @@ Before you begin, ensure you have: - **Node.js** 18 or later ([Download](https://nodejs.org/)) -- **Claude Code CLI** installed ([Get it here](https://claude.ai/download)) +- **Claude Code CLI** installed ([Get it here](https://claude.ai/download)), **Codex CLI** installed (`npm install -g @openai/codex`), and/or **OpenClaw CLI** installed - **Qiniu API Key** ([Get one here](https://portal.qiniu.com/)) ## 🚀 Quick Start @@ -47,19 +49,27 @@ npx qiniu-coding-helper - Select your language (Chinese / English) - Choose your endpoint (Domestic / International) - Enter and validate your API Key +- Select the coding assistant you want to configure - Pick a model -### 3️⃣ Restart Claude Code +### 3️⃣ Restart Your Coding Assistant -If Claude Code is running, restart it to apply changes. +If Claude Code, Codex, or OpenClaw is running, restart it to apply changes when needed. ### 4️⃣ Start Coding! 🎉 ```bash +# Claude Code claude + +# Codex +codex + +# OpenClaw +openclaw ``` -That's it! You're now using Qiniu AI endpoints in Claude Code. +That's it! You're now using Qiniu AI endpoints in your coding assistant. --- @@ -91,6 +101,12 @@ npx qiniu-coding-helper auth revoke # Reload configuration to Claude Code npx qiniu-coding-helper auth reload claude + +# Reload configuration to Codex +npx qiniu-coding-helper auth reload codex + +# Reload configuration to OpenClaw +npx qiniu-coding-helper auth reload openclaw ``` --- @@ -120,7 +136,7 @@ Run system health check to diagnose configuration issues. npx qiniu-coding-helper doctor ``` -**Checks:** Config files · API Key validity · Network connectivity · Claude Code installation · Git · Node.js +**Checks:** Config files · API Key validity · Network connectivity · configured tool installations · Git · Node.js --- @@ -134,6 +150,12 @@ npx qiniu-coding-helper enter # Claude Code configuration menu npx qiniu-coding-helper enter claude-code + +# Codex configuration menu +npx qiniu-coding-helper enter codex + +# OpenClaw configuration menu +npx qiniu-coding-helper enter openclaw ``` --- @@ -149,6 +171,7 @@ npx qiniu-coding-helper enter claude-code | **Claude Code Onboarding** | `~/.claude.json` | Onboarding completion flag | | **Codex Config** | `~/.codex/config.toml` | Qiniu model provider and profile settings | | **Codex Auth** | `~/.codex/auth.json` | Codex API key auth cache | +| **OpenClaw Config** | `~/.openclaw/openclaw.json` | Qiniu custom provider, API key env, and default model | ### 🌍 Region Endpoints @@ -180,6 +203,45 @@ When Codex configuration is applied, Coding Helper writes the Qiniu model provid Treat `~/.codex/auth.json` like a password because it contains API credentials. +### 🔧 OpenClaw Configuration + +When OpenClaw configuration is applied, Coding Helper calls `openclaw config set` to register a Qiniu OpenAI-compatible provider under `models.providers.qnaigc`, stores the API Key as `env.QINIU_API_KEY`, and sets the default model to `qnaigc/`. + +The managed OpenClaw provider uses the Qiniu OpenAI-compatible endpoint: + +```json +{ + "env": { + "QINIU_API_KEY": "" + }, + "models": { + "mode": "merge", + "providers": { + "qnaigc": { + "baseUrl": "https://api.qnaigc.com/v1", + "apiKey": "${QINIU_API_KEY}", + "api": "openai-completions", + "models": [ + { + "id": "", + "name": "" + } + ] + } + } + }, + "agents": { + "defaults": { + "model": { + "primary": "qnaigc/" + } + } + } +} +``` + +OpenClaw gateway, channels, daemon, workspace, and pairing setup remain managed by OpenClaw itself. + --- ## ❓ FAQ @@ -191,6 +253,13 @@ Treat `~/.codex/auth.json` like a password because it contains API credentials. 3. **Restart Claude Code completely** 4. Ensure `ANTHROPIC_BASE_URL` is not set in your shell environment (`unset ANTHROPIC_BASE_URL`) +### OpenClaw doesn't use the configured endpoint + +1. Run `npx qiniu-coding-helper doctor` to check OpenClaw installation and configuration +2. Run `npx qiniu-coding-helper auth reload openclaw` to reapply settings +3. Confirm `~/.openclaw/openclaw.json` contains `models.providers.qnaigc` +4. Run `openclaw doctor` if the OpenClaw gateway reports a config validation error + ### API errors or authentication failures 1. Verify your API Key: check `~/.coding-helper/config.yaml` @@ -202,6 +271,9 @@ Treat `~/.codex/auth.json` like a password because it contains API credentials. ```bash chmod 600 ~/.claude/settings.json chmod 600 ~/.coding-helper/config.yaml +chmod 600 ~/.codex/config.toml +chmod 600 ~/.codex/auth.json +chmod 600 ~/.openclaw/openclaw.json ``` --- @@ -211,6 +283,7 @@ chmod 600 ~/.coding-helper/config.yaml ```bash pnpm install # Install dependencies pnpm build # Build (tsc + copy locales to dist/) +pnpm test # Build and run node:test tests pnpm dev # Watch mode pnpm start # Run CLI (node dist/cli.js) pnpm clean # Clean dist/ diff --git a/README.zh-CN.md b/README.zh-CN.md index 199e104..ab930eb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -6,7 +6,7 @@ [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) [![Node.js Version](https://img.shields.io/node/v/qiniu-coding-helper.svg)](https://nodejs.org) -**一站式配置 Claude Code 使用七牛 AI API 端点的 CLI 工具** +**一站式配置 AI 编程助手使用七牛 AI API 端点的 CLI 工具** [English](README.md) · [功能特性](#-功能特性) · [快速开始](#-快速开始) · [命令一览](#-命令一览) · [配置说明](#-配置说明) · [常见问题](#-常见问题) @@ -21,6 +21,8 @@ - **🔐 API Key 管理** — 输入、验证、保存、删除,一条龙管理 - **📦 模型配置** — 从 API 获取可用模型列表,也支持手动输入模型 ID - **⚡ Claude Code 集成** — 自动将环境变量写入 `~/.claude/settings.json` +- **🧩 Codex 集成** — 将七牛 provider 写入 `~/.codex/config.toml`,并把凭证保存到 Codex auth +- **🦀 OpenClaw 集成** — 通过 OpenClaw 配置 CLI 注册七牛 OpenAI 兼容 provider 和默认模型 - **🔍 健康检查** — 内置 `doctor` 命令,检测配置文件、API Key、网络、工具安装状态 - **🌍 国际化** — 支持中文 (zh_CN) 和英文 (en_US) @@ -29,7 +31,7 @@ 开始之前,请确保已安装: - **Node.js** 18 或更高版本 ([下载](https://nodejs.org/)) -- **Claude Code CLI** ([安装地址](https://claude.ai/download)) +- **Claude Code CLI** ([安装地址](https://claude.ai/download))、**Codex CLI** (`npm install -g @openai/codex`) 和/或 **OpenClaw CLI** - **七牛 API Key** ([获取地址](https://portal.qiniu.com/)) ## 🚀 快速开始 @@ -47,19 +49,27 @@ npx qiniu-coding-helper - 选择语言(中文 / 英文) - 选择线路(国内 / 海外) - 输入并验证 API Key +- 选择要配置的编程助手 - 选择模型 -### 3️⃣ 重启 Claude Code +### 3️⃣ 重启编程助手 -如果 Claude Code 正在运行,请重启以应用配置。 +如果 Claude Code、Codex 或 OpenClaw 正在运行,请按需重启以应用配置。 ### 4️⃣ 开始编程! 🎉 ```bash +# Claude Code claude + +# Codex +codex + +# OpenClaw +openclaw ``` -配置完成!现在 Claude Code 已通过七牛 AI 端点运行。 +配置完成!现在你的编程助手已通过七牛 AI 端点运行。 --- @@ -91,6 +101,12 @@ npx qiniu-coding-helper auth revoke # 重新加载配置到 Claude Code npx qiniu-coding-helper auth reload claude + +# 重新加载配置到 Codex +npx qiniu-coding-helper auth reload codex + +# 重新加载配置到 OpenClaw +npx qiniu-coding-helper auth reload openclaw ``` --- @@ -120,7 +136,7 @@ npx qiniu-coding-helper lang set en_US npx qiniu-coding-helper doctor ``` -**检查项:** 配置文件 · API Key 有效性 · 网络连通性 · Claude Code 安装 · Git · Node.js +**检查项:** 配置文件 · API Key 有效性 · 网络连通性 · 已配置工具安装状态 · Git · Node.js --- @@ -134,6 +150,12 @@ npx qiniu-coding-helper enter # 进入 Claude Code 配置菜单 npx qiniu-coding-helper enter claude-code + +# 进入 Codex 配置菜单 +npx qiniu-coding-helper enter codex + +# 进入 OpenClaw 配置菜单 +npx qiniu-coding-helper enter openclaw ``` --- @@ -147,6 +169,9 @@ npx qiniu-coding-helper enter claude-code | **Coding Helper 配置** | `~/.coding-helper/config.yaml` | 语言、线路、API Key、模型设置 | | **Claude Code 设置** | `~/.claude/settings.json` | API 端点环境变量 | | **Claude Code Onboarding** | `~/.claude.json` | 初始化完成标记 | +| **Codex 配置** | `~/.codex/config.toml` | 七牛模型 provider 和 profile 设置 | +| **Codex Auth** | `~/.codex/auth.json` | Codex API Key 认证缓存 | +| **OpenClaw 配置** | `~/.openclaw/openclaw.json` | 七牛 custom provider、API Key env 和默认模型 | ### 🌍 线路端点 @@ -172,6 +197,51 @@ npx qiniu-coding-helper enter claude-code > **说明:** 工具还会设置 `API_TIMEOUT_MS`(50 分钟,适配长时间推理请求)、禁用非必要流量(第三方端点不需要 Anthropic 遥测)、禁用 commit/PR 归属标注(通过代理端点运行时标注不准确)。 +### 🔧 Codex 配置 + +配置应用到 Codex 时,Coding Helper 会将七牛模型 provider 写入 `~/.codex/config.toml`,并把 API Key 保存到 Codex 自己的 `~/.codex/auth.json` 认证缓存,不依赖 `QINIU_API_KEY` shell 环境变量。 + +请像保管密码一样保管 `~/.codex/auth.json`,因为它包含 API 凭证。 + +### 🔧 OpenClaw 配置 + +配置应用到 OpenClaw 时,Coding Helper 会调用 `openclaw config set`,在 `models.providers.qnaigc` 下注册七牛 OpenAI 兼容 provider,把 API Key 保存为 `env.QINIU_API_KEY`,并将默认模型设置为 `qnaigc/`。 + +托管的 OpenClaw provider 使用七牛 OpenAI 兼容端点: + +```json +{ + "env": { + "QINIU_API_KEY": "" + }, + "models": { + "mode": "merge", + "providers": { + "qnaigc": { + "baseUrl": "https://api.qnaigc.com/v1", + "apiKey": "${QINIU_API_KEY}", + "api": "openai-completions", + "models": [ + { + "id": "", + "name": "" + } + ] + } + } + }, + "agents": { + "defaults": { + "model": { + "primary": "qnaigc/" + } + } + } +} +``` + +OpenClaw 的 gateway、channels、daemon、workspace 和 pairing 设置仍由 OpenClaw 自身管理。 + --- ## ❓ 常见问题 @@ -183,6 +253,13 @@ npx qiniu-coding-helper enter claude-code 3. **完全重启 Claude Code** 4. 确认 shell 环境中没有设置 `ANTHROPIC_BASE_URL`(运行 `unset ANTHROPIC_BASE_URL`) +### OpenClaw 没有使用配置的端点 + +1. 运行 `npx qiniu-coding-helper doctor` 检查 OpenClaw 安装和配置 +2. 运行 `npx qiniu-coding-helper auth reload openclaw` 重新应用设置 +3. 确认 `~/.openclaw/openclaw.json` 包含 `models.providers.qnaigc` +4. 如果 OpenClaw gateway 报配置校验错误,运行 `openclaw doctor` 查看原因 + ### API 报错或认证失败 1. 检查 API Key:查看 `~/.coding-helper/config.yaml` @@ -194,6 +271,9 @@ npx qiniu-coding-helper enter claude-code ```bash chmod 600 ~/.claude/settings.json chmod 600 ~/.coding-helper/config.yaml +chmod 600 ~/.codex/config.toml +chmod 600 ~/.codex/auth.json +chmod 600 ~/.openclaw/openclaw.json ``` --- @@ -203,6 +283,7 @@ chmod 600 ~/.coding-helper/config.yaml ```bash pnpm install # 安装依赖 pnpm build # 编译 (tsc + 复制 locales 到 dist/) +pnpm test # 编译并运行 node:test 测试 pnpm dev # 监听模式 pnpm start # 运行 CLI (node dist/cli.js) pnpm clean # 清理 dist/ diff --git a/src/commands/auth.ts b/src/commands/auth.ts index 51c7c59..8ff1ed0 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -11,6 +11,7 @@ import ora from 'ora'; function resolveToolName(tool: string): string { if (tool === 'claude' || tool === 'claude-code') return 'claude-code'; if (tool === 'codex' || tool === 'openai-codex') return 'codex'; + if (tool === 'openclaw' || tool === 'claw') return 'openclaw'; return tool; } diff --git a/src/lib/command.ts b/src/lib/command.ts index 153f4a5..435a761 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -10,6 +10,7 @@ import { VERSION } from './version.js'; function resolveToolName(tool: string): string { if (tool === 'claude' || tool === 'claude-code') return 'claude-code'; if (tool === 'codex' || tool === 'openai-codex') return 'codex'; + if (tool === 'openclaw' || tool === 'claw') return 'openclaw'; return tool; } diff --git a/src/lib/tool-manager.ts b/src/lib/tool-manager.ts index 8ba6218..7a9a80d 100644 --- a/src/lib/tool-manager.ts +++ b/src/lib/tool-manager.ts @@ -1,6 +1,7 @@ import type { ITool } from './tools/base-tool.js'; import { ClaudeCodeTool } from './tools/claude-code-tool.js'; import { CodexTool } from './tools/codex-tool.js'; +import { OpenClawTool } from './tools/openclaw-tool.js'; // 工具注册中心 class ToolManager { @@ -10,6 +11,7 @@ class ToolManager { // 注册默认工具 this.register(new ClaudeCodeTool()); this.register(new CodexTool()); + this.register(new OpenClawTool()); } // 注册工具 diff --git a/src/lib/tools/index.ts b/src/lib/tools/index.ts index 8f0d19f..dfb231a 100644 --- a/src/lib/tools/index.ts +++ b/src/lib/tools/index.ts @@ -1,3 +1,4 @@ export type { ITool } from './base-tool.js'; export { ClaudeCodeTool } from './claude-code-tool.js'; export { CodexTool } from './codex-tool.js'; +export { OpenClawTool } from './openclaw-tool.js'; diff --git a/src/lib/tools/openclaw-tool.ts b/src/lib/tools/openclaw-tool.ts new file mode 100644 index 0000000..31a1188 --- /dev/null +++ b/src/lib/tools/openclaw-tool.ts @@ -0,0 +1,170 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import { execFileSync } from 'node:child_process'; +import type { ITool } from './base-tool.js'; +import type { ModelConfig } from '../config.js'; + +const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw'); +const OPENCLAW_CONFIG_FILE = path.join(OPENCLAW_DIR, 'openclaw.json'); +const PROVIDER_NAME = 'qnaigc'; +const API_KEY_ENV_NAME = 'QINIU_API_KEY'; +const PROVIDER_ALIAS = 'Qiniu'; + +type OpenClawOperation = string[]; + +// OpenClaw 工具实现 +export class OpenClawTool implements ITool { + name = 'openclaw'; + displayName = 'OpenClaw'; + command = 'openclaw'; + installCommand = 'npm install -g openclaw@latest'; + updateCommand = 'npm install -g openclaw@latest'; + npmPackageName = 'openclaw'; + + getVersion(): string | null { + try { + const output = execFileSync('openclaw', ['--version'], { stdio: 'pipe', encoding: 'utf-8' }).trim(); + const match = output.match(/(\d+\.\d+\.\d+)/); + return match ? match[1] : output; + } catch { + return null; + } + } + + isInstalled(): boolean { + try { + const command = process.platform === 'win32' ? 'where' : 'which'; + execFileSync(command, ['openclaw'], { stdio: 'pipe' }); + return true; + } catch { + return false; + } + } + + getConfig(): Record { + return { + configPath: getOpenClawConfigPath(), + configured: this.getModelConfig()?.sonnetModel?.startsWith(`${PROVIDER_NAME}/`) || hasOpenClawConfigFile(), + modelConfig: this.getModelConfig(), + }; + } + + async saveModelConfig(config: ModelConfig): Promise { + const model = getOpenClawModel(config); + if (!model) return; + runOpenClawOperations([ + ['config', 'set', 'agents.defaults.model.primary', JSON.stringify(buildOpenClawModelRef(model)), '--strict-json'], + ['config', 'set', buildOpenClawModelAliasPath(buildOpenClawModelRef(model)), JSON.stringify({ alias: PROVIDER_ALIAS }), '--strict-json'], + ]); + } + + getModelConfig(): ModelConfig | null { + const primary = getOpenClawConfigValue('agents.defaults.model.primary'); + if (!primary?.startsWith(`${PROVIDER_NAME}/`)) return null; + return { sonnetModel: primary.slice(`${PROVIDER_NAME}/`.length) }; + } + + clearModelConfig(): void { + runOpenClawOperations(buildOpenClawUnloadOperations(getOpenClawConfigValue('agents.defaults.model.primary'))); + } + + async loadConfig(apiKey: string, baseUrl: string, models: ModelConfig): Promise { + const model = getOpenClawModel(models); + if (!model) return; + runOpenClawOperations(buildOpenClawLoadOperations(apiKey, baseUrl, model)); + } + + unloadConfig(): void { + this.clearModelConfig(); + } +} + +export function buildOpenClawProviderConfig(baseUrl: string, model: string): Record { + return { + baseUrl: `${baseUrl.replace(/\/+$/, '')}/v1`, + apiKey: `\${${API_KEY_ENV_NAME}}`, + api: 'openai-completions', + models: [ + { + id: model, + name: model, + }, + ], + }; +} + +export function buildOpenClawModelRef(model: string): string { + return `${PROVIDER_NAME}/${model}`; +} + +export function buildOpenClawLoadOperations(apiKey: string, baseUrl: string, model: string): OpenClawOperation[] { + const modelRef = buildOpenClawModelRef(model); + return [ + ['config', 'set', `env.${API_KEY_ENV_NAME}`, JSON.stringify(apiKey), '--strict-json'], + ['config', 'set', 'models.mode', JSON.stringify('merge'), '--strict-json'], + ['config', 'set', `models.providers.${PROVIDER_NAME}`, JSON.stringify(buildOpenClawProviderConfig(baseUrl, model)), '--strict-json'], + ['config', 'set', 'agents.defaults.model.primary', JSON.stringify(modelRef), '--strict-json'], + ['config', 'set', buildOpenClawModelAliasPath(modelRef), JSON.stringify({ alias: PROVIDER_ALIAS }), '--strict-json'], + ]; +} + +export function buildOpenClawUnloadOperations(currentModelRef?: string | null): OpenClawOperation[] { + const operations = [ + ['config', 'unset', `env.${API_KEY_ENV_NAME}`], + ['config', 'unset', `models.providers.${PROVIDER_NAME}`], + ]; + + if (currentModelRef?.startsWith(`${PROVIDER_NAME}/`)) { + operations.push( + ['config', 'unset', 'agents.defaults.model.primary'], + ['config', 'unset', buildOpenClawModelAliasPath(currentModelRef)], + ); + } + + return operations; +} + +function buildOpenClawModelAliasPath(modelRef: string): string { + return `agents.defaults.models[${JSON.stringify(modelRef)}]`; +} + +function getOpenClawModel(models: ModelConfig): string | undefined { + return models.sonnetModel || models.opusModel || models.haikuModel || models.subagentModel; +} + +function getOpenClawConfigValue(pathName: string): string | null { + try { + const output = execFileSync('openclaw', ['config', 'get', pathName], { stdio: 'pipe', encoding: 'utf-8' }).trim(); + return parseOpenClawConfigValue(output); + } catch { + return null; + } +} + +function parseOpenClawConfigValue(value: string): string { + try { + const parsed = JSON.parse(value); + return typeof parsed === 'string' ? parsed : value; + } catch { + return value; + } +} + +function runOpenClawOperations(operations: OpenClawOperation[]): void { + for (const args of operations) { + execFileSync('openclaw', args, { stdio: 'pipe' }); + } +} + +function getOpenClawConfigPath(): string { + return process.env.OPENCLAW_CONFIG_PATH || OPENCLAW_CONFIG_FILE; +} + +function hasOpenClawConfigFile(): boolean { + try { + return fs.existsSync(getOpenClawConfigPath()); + } catch { + return false; + } +} diff --git a/tests/openclaw-tool.test.mjs b/tests/openclaw-tool.test.mjs new file mode 100644 index 0000000..0cc82b5 --- /dev/null +++ b/tests/openclaw-tool.test.mjs @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + buildOpenClawLoadOperations, + buildOpenClawProviderConfig, + buildOpenClawModelRef, + buildOpenClawUnloadOperations, +} from '../dist/lib/tools/openclaw-tool.js'; + +test('buildOpenClawProviderConfig creates an OpenAI-compatible Qiniu provider', () => { + assert.deepEqual(buildOpenClawProviderConfig('https://api.qnaigc.com/', 'openai/gpt-5.2'), { + baseUrl: 'https://api.qnaigc.com/v1', + apiKey: '${QINIU_API_KEY}', + api: 'openai-completions', + models: [ + { + id: 'openai/gpt-5.2', + name: 'openai/gpt-5.2', + }, + ], + }); +}); + +test('buildOpenClawModelRef prefixes selected Qiniu model with provider id', () => { + assert.equal(buildOpenClawModelRef('openai/gpt-5.2'), 'qnaigc/openai/gpt-5.2'); +}); + +test('buildOpenClawLoadOperations writes env, provider, default model, and allowlist alias', () => { + assert.deepEqual(buildOpenClawLoadOperations('qiniu-key', 'https://api.qnaigc.com', 'openai/gpt-5.2'), [ + ['config', 'set', 'env.QINIU_API_KEY', '"qiniu-key"', '--strict-json'], + ['config', 'set', 'models.mode', '"merge"', '--strict-json'], + ['config', 'set', 'models.providers.qnaigc', '{"baseUrl":"https://api.qnaigc.com/v1","apiKey":"${QINIU_API_KEY}","api":"openai-completions","models":[{"id":"openai/gpt-5.2","name":"openai/gpt-5.2"}]}', '--strict-json'], + ['config', 'set', 'agents.defaults.model.primary', '"qnaigc/openai/gpt-5.2"', '--strict-json'], + ['config', 'set', 'agents.defaults.models["qnaigc/openai/gpt-5.2"]', '{"alias":"Qiniu"}', '--strict-json'], + ]); +}); + +test('buildOpenClawUnloadOperations removes only helper-managed Qiniu settings', () => { + assert.deepEqual(buildOpenClawUnloadOperations('qnaigc/openai/gpt-5.2'), [ + ['config', 'unset', 'env.QINIU_API_KEY'], + ['config', 'unset', 'models.providers.qnaigc'], + ['config', 'unset', 'agents.defaults.model.primary'], + ['config', 'unset', 'agents.defaults.models["qnaigc/openai/gpt-5.2"]'], + ]); + + assert.deepEqual(buildOpenClawUnloadOperations('openai/gpt-5.2'), [ + ['config', 'unset', 'env.QINIU_API_KEY'], + ['config', 'unset', 'models.providers.qnaigc'], + ]); +});