Files
clawpanel/tests/model-channels.test.js
晴天 dba69ee0fc feat(channels): 统一模型渠道——一处配置,同步 OpenClaw / Hermes / 晴辰助手
新增「模型渠道」页面(通用分组):用户只维护一份渠道配置
(服务商预设 / Base URL / API Key / 模型列表 / 默认模型),
通过显式同步推送到三个消费方,消灭重复配置。

实现要点:
- 渠道存储 src-tauri/src/commands/model_channels.rs:
  读取只回掩码(apiKeySaved/apiKeyMask),写入支持 __KEEP__ 哨兵保留旧 Key,
  明文 Key 仅经 reveal_model_channel_key 在同步时取出;
  文件位于 openclaw 数据目录下,便携迁移自动带走;含 6 个单元测试
- 同步逻辑 src/lib/model-channels.js 组合现有命令完成(各自带自动备份):
  OpenClaw 只 upsert 对应 provider 保留未知字段;
  Hermes 写 provider 环境变量 + 可选默认模型(仅 OpenAI/Anthropic/Gemini
  兼容 API Key 型,OAuth/SDK 型仍走 Hermes 向导);
  助手为一次性拷贝到 localStorage
- 页面 src/pages/model-channels.js:渠道卡片 + 三目标同步状态徽标
  (已同步/有未同步变更/未同步)+ 编辑弹窗(预设自动填充、在线拉取模型、
  默认模型联动)+ 从 OpenClaw 现有配置一键导入 + 全部操作确认弹窗
- Web 模式 dev-api.js 同构 handler + ALWAYS_LOCAL;i18n 简/繁/英 +
  侧栏 11 语言;tests/model-channels.test.js 锁定注册链与安全约束

验证:npm build / cargo fmt+check+clippy -D warnings / cargo test --lib
314 通过 / node --test 全部 427 通过

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 11:48:24 -07:00

60 lines
3.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
const read = rel => readFileSync(new URL(rel, import.meta.url), 'utf8')
const lib = read('../src/lib/model-channels.js')
const page = read('../src/pages/model-channels.js')
const mainJs = read('../src/main.js')
const sidebar = read('../src/components/sidebar.js')
const tauriApi = read('../src/lib/tauri-api.js')
const devApi = read('../scripts/dev-api.js')
const rustLib = read('../src-tauri/src/lib.rs')
const rustModule = read('../src-tauri/src/commands/model_channels.rs')
const localesIndex = read('../src/locales/index.js')
test('模型渠道命令注册链完整Rust + tauri-api + dev-api + ALWAYS_LOCAL', () => {
for (const cmd of ['read_model_channels', 'write_model_channels', 'reveal_model_channel_key']) {
assert.match(rustLib, new RegExp(`model_channels::${cmd}`), `lib.rs 缺少 ${cmd} 注册`)
assert.match(devApi, new RegExp(`${cmd}\\(`), `dev-api.js 缺少 ${cmd} handler`)
assert.match(devApi, new RegExp(`'${cmd}'`), `${cmd} 必须加入 ALWAYS_LOCAL本机属性不可代理远程`)
}
assert.match(tauriApi, /readModelChannels:/, 'tauri-api 缺少 readModelChannels 封装')
assert.match(tauriApi, /writeModelChannels:/, 'tauri-api 缺少 writeModelChannels 封装')
assert.match(tauriApi, /revealModelChannelKey:/, 'tauri-api 缺少 revealModelChannelKey 封装')
})
test('渠道读取只返回掩码,写入支持保留旧 Key 哨兵', () => {
assert.match(rustModule, /apiKeySaved/, '读取必须返回 apiKeySaved')
assert.match(rustModule, /apiKeyMask/, '读取必须返回 apiKeyMask')
assert.match(rustModule, /__KEEP__/, '写入必须支持 __KEEP__ 哨兵')
assert.match(devApi, /isChannelKeepSentinel/, 'dev-api 必须实现相同的哨兵语义')
})
test('Hermes 同步仅覆盖 API Key 型三类 transport', () => {
assert.match(lib, /'openai-completions':\s*\{\s*transport:\s*'openai_chat'/, 'openai transport 映射缺失')
assert.match(lib, /'anthropic-messages':\s*\{\s*transport:\s*'anthropic_messages'/, 'anthropic transport 映射缺失')
assert.match(lib, /'google-generative-ai':\s*\{\s*transport:\s*'google_gemini'/, 'gemini transport 映射缺失')
assert.match(lib, /authType === 'api_key'/, 'OAuth/SDK 型 provider 必须被排除在渠道同步之外')
})
test('OpenClaw 同步只 upsert 单个 provider 并保留未知字段', () => {
assert.match(lib, /\.\.\.existing,/, '写入 provider 时必须展开旧对象保留未知字段')
assert.match(lib, /config\.models\.providers\[providerKey\]/, '必须按 provider 键 upsert 而非整体重写')
})
test('同步与删除必须经过确认弹窗', () => {
assert.match(page, /showConfirm\(t\('modelChannels\.syncOpenclawConfirm'/, '同步 OpenClaw 前必须确认')
assert.match(page, /showConfirm\(t\('modelChannels\.syncHermesConfirm'/, '同步 Hermes 前必须确认')
assert.match(page, /showConfirm\(t\('modelChannels\.syncAssistantConfirm'/, '同步助手前必须确认')
assert.match(page, /showConfirm\(t\('modelChannels\.deleteConfirm'/, '删除渠道前必须确认')
})
test('页面注册链完整(路由 + 侧栏 + 语言包)', () => {
assert.match(mainJs, /registerRoute\('\/model-channels'/, 'main.js 缺少路由注册')
assert.match(sidebar, /route: '\/model-channels'/, '侧栏缺少入口')
assert.match(sidebar, /'channels-hub':/, '侧栏缺少图标')
assert.match(localesIndex, /modelChannels/, '语言包聚合缺少 modelChannels 模块')
})