From ff1de0914821e502b4e24233f05c430d09182f9b Mon Sep 17 00:00:00 2001 From: Kris Xia Date: Mon, 13 Jul 2026 16:26:06 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor(endpoints):=20=E7=B2=BE=E7=AE=80?= =?UTF-8?q?=E4=B8=BA=E5=9B=BD=E5=86=85=E4=B8=8E=E6=B5=B7=E5=A4=96=E4=B8=A4?= =?UTF-8?q?=E6=9D=A1=E7=BA=BF=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 qiniu 与 sufy 线路,默认线路改回 china,海外线路统一使用 api.modelink.ai。 --- CLAUDE.md | 6 ++---- src/lib/endpoints.ts | 12 +----------- src/locales/en_US.json | 6 ++---- src/locales/zh_CN.json | 6 ++---- tests/hermes-tool.test.mjs | 6 +++--- 5 files changed, 10 insertions(+), 26 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b4cceb1..eadc324 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,10 +59,8 @@ node dist/cli.js lang show ## API Endpoints -- 国内线路: `https://openai.qiniu.com` -- 国内备用线路: `https://api.qnaigc.com` -- 海外线路: `https://openai.sufy.com` -- Modelink 线路: `https://api.modelink.ai` +- 国内线路: `https://api.qnaigc.com` +- 海外线路: `https://api.modelink.ai` - 支持接口: `/v1/chat/completions`、`/v1/messages`、`/v1/models` - API Key 校验: `/v2/stat/usage`(轻量级,验证 token 有效性) - 认证: Bearer token diff --git a/src/lib/endpoints.ts b/src/lib/endpoints.ts index 422b30f..7b7e5d6 100644 --- a/src/lib/endpoints.ts +++ b/src/lib/endpoints.ts @@ -7,11 +7,6 @@ export interface EndpointDefinition { } export const ENDPOINT_DEFINITIONS: Record = { - qiniu: { - id: 'qiniu', - i18nKey: 'endpoint_qiniu', - baseUrl: 'https://openai.qiniu.com', - }, china: { id: 'china', i18nKey: 'endpoint_china', @@ -20,16 +15,11 @@ export const ENDPOINT_DEFINITIONS: Record = { international: { id: 'international', i18nKey: 'endpoint_international', - baseUrl: 'https://openai.sufy.com', - }, - modelink: { - id: 'modelink', - i18nKey: 'endpoint_modelink', baseUrl: 'https://api.modelink.ai', }, }; -export const DEFAULT_ENDPOINT = 'qiniu'; +export const DEFAULT_ENDPOINT = 'china'; // 根据端点 ID 获取 base URL export function getBaseUrl(endpointId: string): string { diff --git a/src/locales/en_US.json b/src/locales/en_US.json index 90f097f..9ca3e85 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -6,10 +6,8 @@ "language_set": "Language set to: {{lang}}", "language_current": "Current language: {{lang}}", "endpoint_prompt": "Select API endpoint", - "endpoint_qiniu": "Domestic (openai.qiniu.com)", - "endpoint_china": "Domestic Backup (api.qnaigc.com)", - "endpoint_international": "International (openai.sufy.com)", - "endpoint_modelink": "Modelink (api.modelink.ai)", + "endpoint_china": "China (api.qnaigc.com)", + "endpoint_international": "International (api.modelink.ai)", "endpoint_set": "Endpoint set to: {{endpoint}}", "apikey_hint": "To get an API Key, visit: https://portal.qiniu.com/ai-inference/api-key", "apikey_prompt": "Enter your API Key", diff --git a/src/locales/zh_CN.json b/src/locales/zh_CN.json index 387ef23..ebb73f7 100644 --- a/src/locales/zh_CN.json +++ b/src/locales/zh_CN.json @@ -8,10 +8,8 @@ "language_current": "当前语言: {{lang}}", "endpoint_prompt": "请选择 API 线路", - "endpoint_qiniu": "国内线路 (openai.qiniu.com)", - "endpoint_china": "国内备用线路 (api.qnaigc.com)", - "endpoint_international": "海外线路 (openai.sufy.com)", - "endpoint_modelink": "Modelink 线路 (api.modelink.ai)", + "endpoint_china": "国内线路 (api.qnaigc.com)", + "endpoint_international": "海外线路 (api.modelink.ai)", "endpoint_set": "线路已设置为: {{endpoint}}", "apikey_hint": "如需获取 API Key,请访问: https://portal.qiniu.com/ai-inference/api-key", diff --git a/tests/hermes-tool.test.mjs b/tests/hermes-tool.test.mjs index 26966f9..c35d869 100644 --- a/tests/hermes-tool.test.mjs +++ b/tests/hermes-tool.test.mjs @@ -47,14 +47,14 @@ test('buildHermesConfig writes Qiniu custom endpoint and preserves unrelated con }); test('buildHermesConfig escapes scalar values that require YAML quoting', () => { - const next = buildHermesConfig('', 'https://openai.sufy.com/', 'key:with#chars', 'vendor/model:latest'); + const next = buildHermesConfig('', 'https://api.modelink.ai/', 'key:with#chars', 'vendor/model:latest'); const parsed = yaml.load(next); - assert.match(next, /base_url: https:\/\/openai\.sufy\.com\/v1/); + assert.match(next, /base_url: https:\/\/api\.modelink\.ai\/v1/); assert.deepEqual(parsed.model, { provider: 'custom', default: 'vendor/model:latest', - base_url: 'https://openai.sufy.com/v1', + base_url: 'https://api.modelink.ai/v1', api_key: 'key:with#chars', }); }); From 741dca4c762b175afb0d2e46fd88fdd995341e2b Mon Sep 17 00:00:00 2001 From: Kris Xia Date: Mon, 13 Jul 2026 21:44:25 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(endpoints):=20=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=97=A7=E7=BA=BF=E8=B7=AF=20id=20=E9=81=BF=E5=85=8D=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E5=90=8E=E5=9C=B0=E5=9D=80=E6=BC=82=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 精简线路后,老用户 config.yaml 中残留的 modelink / qiniu id 会命中不到定义表而回退到默认国内线路,导致原 Modelink 海外 用户被静默切到 api.qnaigc.com。 - 新增 normalizeEndpointId 与旧 id 映射(modelink→international、 qiniu→china),getBaseUrl 与 getEndpoint 读取时规范化 - 渲染层经规范化后正确显示线路,不再显示"未设置" - 同步更新中英文 README 线路文案与工具配置示例 - 新增 endpoints 迁移测试覆盖旧 id 映射与未知 id 回退 --- README.en-US.md | 16 +++++++--------- README.md | 16 +++++++--------- src/lib/config.ts | 5 +++-- src/lib/endpoints.ts | 14 +++++++++++++- tests/endpoints.test.mjs | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 tests/endpoints.test.mjs diff --git a/README.en-US.md b/README.en-US.md index 1726436..2ff5c8f 100644 --- a/README.en-US.md +++ b/README.en-US.md @@ -27,7 +27,7 @@ ## ✨ Features - **🎯 Interactive Wizard** — Guided step-by-step setup for language, endpoint, API Key, and model on first run -- **🌐 Multi-Region Support** — Domestic (openai.qiniu.com) / Domestic Backup (api.qnaigc.com) / International (openai.sufy.com) / Modelink (api.modelink.ai) endpoints +- **🌐 Multi-Region Support** — Domestic (api.qnaigc.com) / International (api.modelink.ai) endpoints - **🔐 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` @@ -208,10 +208,8 @@ npx qiniu-coding-helper enter hermes | Region | Base URL | Best For | |--------|----------|----------| -| 🇨🇳 **Domestic** | `https://openai.qiniu.com` | Users in China | -| 🇨🇳 **Domestic Backup** | `https://api.qnaigc.com` | Users in China | -| 🌍 **International** | `https://openai.sufy.com` | Users outside China | -| 🔁 **Modelink** | `https://api.modelink.ai` | Available in and outside China | +| 🇨🇳 **Domestic** | `https://api.qnaigc.com` | Users in China | +| 🌍 **International** | `https://api.modelink.ai` | Users outside China | ### 🔧 Claude Code Environment Variables @@ -220,7 +218,7 @@ When configuration is applied, these environment variables are set in `~/.claude ```json { "env": { - "ANTHROPIC_BASE_URL": "https://openai.qiniu.com", + "ANTHROPIC_BASE_URL": "https://api.qnaigc.com", "ANTHROPIC_AUTH_TOKEN": "", "API_TIMEOUT_MS": "3000000", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" @@ -241,7 +239,7 @@ model_provider = "qnaigc" [model_providers.qnaigc] name = "Qiniu" -base_url = "https://openai.qiniu.com/bypass/openai/v1" +base_url = "https://api.qnaigc.com/bypass/openai/v1" requires_openai_auth = true wire_api = "responses" @@ -265,7 +263,7 @@ The configuration flow lets you multi-select models from the Qiniu model market "id": "anthropic/claude-sonnet-4-5", "name": "Claude Sonnet 4.5", "vendor": "Qiniu", - "url": "https://openai.qiniu.com/v1/chat/completions", + "url": "https://api.qnaigc.com/v1/chat/completions", "apiKey": "", "maxInputTokens": 200000, "maxOutputTokens": 8192, @@ -302,7 +300,7 @@ Hermes Agent stores its configuration in `~/.hermes/config.yaml`. The configurat model: provider: custom default: anthropic/claude-sonnet-4-5 - base_url: https://openai.qiniu.com/v1 + base_url: https://api.qnaigc.com/v1 api_key: ``` diff --git a/README.md b/README.md index cdb7626..9d38b12 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ ## ✨ 功能特性 - **🎯 交互式向导** — 首次运行引导配置语言、线路、API Key 和模型,步步引导 -- **🌐 多线路支持** — 国内线路 (openai.qiniu.com) / 国内备用线路 (api.qnaigc.com) / 海外线路 (openai.sufy.com) / Modelink 线路 (api.modelink.ai) +- **🌐 多线路支持** — 国内线路 (api.qnaigc.com) / 海外线路 (api.modelink.ai) - **🔐 API Key 管理** — 输入、验证、保存、删除,一条龙管理 - **📦 模型配置** — 从 API 获取可用模型列表,也支持手动输入模型 ID - **⚡ Claude Code 集成** — 自动将环境变量写入 `~/.claude/settings.json` @@ -208,10 +208,8 @@ npx qiniu-coding-helper enter hermes | 线路 | 地址 | 适用场景 | |------|------|----------| -| 🇨🇳 **国内线路** | `https://openai.qiniu.com` | 中国大陆用户 | -| 🇨🇳 **国内备用线路** | `https://api.qnaigc.com` | 中国大陆用户 | -| 🌍 **海外线路** | `https://openai.sufy.com` | 海外用户 | -| 🔁 **Modelink 线路** | `https://api.modelink.ai` | 国内外通用 | +| 🇨🇳 **国内线路** | `https://api.qnaigc.com` | 中国大陆用户 | +| 🌍 **海外线路** | `https://api.modelink.ai` | 海外用户 | ### 🔧 Claude Code 环境变量 @@ -220,7 +218,7 @@ npx qiniu-coding-helper enter hermes ```json { "env": { - "ANTHROPIC_BASE_URL": "https://openai.qiniu.com", + "ANTHROPIC_BASE_URL": "https://api.qnaigc.com", "ANTHROPIC_AUTH_TOKEN": "", "API_TIMEOUT_MS": "3000000", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" @@ -241,7 +239,7 @@ model_provider = "qnaigc" [model_providers.qnaigc] name = "Qiniu" -base_url = "https://openai.qiniu.com/bypass/openai/v1" +base_url = "https://api.qnaigc.com/bypass/openai/v1" requires_openai_auth = true wire_api = "responses" @@ -265,7 +263,7 @@ CodeBuddy 是一个 CLI 工具,配置保存在 `~/.codebuddy/models.json`。 "id": "anthropic/claude-sonnet-4-5", "name": "Claude Sonnet 4.5", "vendor": "Qiniu", - "url": "https://openai.qiniu.com/v1/chat/completions", + "url": "https://api.qnaigc.com/v1/chat/completions", "apiKey": "", "maxInputTokens": 200000, "maxOutputTokens": 8192, @@ -300,7 +298,7 @@ Hermes Agent 配置保存在 `~/.hermes/config.yaml`。配置流程会从七牛 model: provider: custom default: anthropic/claude-sonnet-4-5 - base_url: https://openai.qiniu.com/v1 + base_url: https://api.qnaigc.com/v1 api_key: ``` diff --git a/src/lib/config.ts b/src/lib/config.ts index 6485840..8033c7d 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import yaml from 'js-yaml'; -import { DEFAULT_ENDPOINT, getBaseUrl } from './endpoints.js'; +import { DEFAULT_ENDPOINT, getBaseUrl, normalizeEndpointId } from './endpoints.js'; // 配置文件路径 const CONFIG_DIR = path.join(os.homedir(), '.coding-helper'); @@ -96,7 +96,8 @@ class ConfigManager { // 线路选择 getEndpoint(): string { this.load(); - return this.config.endpoint || DEFAULT_ENDPOINT; + // 规范化旧 id,兼容历史配置(如 modelink → international) + return normalizeEndpointId(this.config.endpoint || DEFAULT_ENDPOINT); } setEndpoint(endpoint: string): void { diff --git a/src/lib/endpoints.ts b/src/lib/endpoints.ts index 7b7e5d6..2c46ab4 100644 --- a/src/lib/endpoints.ts +++ b/src/lib/endpoints.ts @@ -21,9 +21,21 @@ export const ENDPOINT_DEFINITIONS: Record = { export const DEFAULT_ENDPOINT = 'china'; +// 旧线路 id 到新线路 id 的兼容映射(历史配置迁移) +// 老用户 config.yaml 里可能存有已移除的 id,读取时规范化到对应新线路 +const LEGACY_ENDPOINT_ALIASES: Record = { + qiniu: 'china', // 旧 openai.qiniu.com → 国内线路 api.qnaigc.com + modelink: 'international', // 旧 api.modelink.ai → 海外线路(地址不变) +}; + +// 规范化端点 id:已知旧 id 映射到新 id,其余原样返回 +export function normalizeEndpointId(endpointId: string): string { + return LEGACY_ENDPOINT_ALIASES[endpointId] ?? endpointId; +} + // 根据端点 ID 获取 base URL export function getBaseUrl(endpointId: string): string { - const endpoint = ENDPOINT_DEFINITIONS[endpointId]; + const endpoint = ENDPOINT_DEFINITIONS[normalizeEndpointId(endpointId)]; if (!endpoint) { return ENDPOINT_DEFINITIONS[DEFAULT_ENDPOINT].baseUrl; } diff --git a/tests/endpoints.test.mjs b/tests/endpoints.test.mjs new file mode 100644 index 0000000..f365fa0 --- /dev/null +++ b/tests/endpoints.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + DEFAULT_ENDPOINT, + ENDPOINT_DEFINITIONS, + getBaseUrl, + normalizeEndpointId, +} from '../dist/lib/endpoints.js'; + +test('normalizeEndpointId maps legacy modelink id to international', () => { + // 老用户配置里的 modelink 应规范化到新的海外线路 id + assert.equal(normalizeEndpointId('modelink'), 'international'); +}); + +test('getBaseUrl keeps legacy modelink users on api.modelink.ai', () => { + // 关键回归:老 Modelink 用户升级后地址不应漂移到国内线路 + assert.equal(getBaseUrl('modelink'), 'https://api.modelink.ai'); +}); + +test('normalizeEndpointId maps legacy qiniu id to china', () => { + assert.equal(normalizeEndpointId('qiniu'), 'china'); + assert.equal(getBaseUrl('qiniu'), 'https://api.qnaigc.com'); +}); + +test('normalizeEndpointId returns current ids unchanged', () => { + assert.equal(normalizeEndpointId('china'), 'china'); + assert.equal(normalizeEndpointId('international'), 'international'); +}); + +test('getBaseUrl falls back to default endpoint for unknown ids', () => { + const expected = ENDPOINT_DEFINITIONS[DEFAULT_ENDPOINT].baseUrl; + assert.equal(getBaseUrl('unknown-endpoint'), expected); +});