mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-07-13 16:32:54 +08:00
新增「模型渠道」页面(通用分组):用户只维护一份渠道配置 (服务商预设 / 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>
60 lines
3.5 KiB
JavaScript
60 lines
3.5 KiB
JavaScript
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 模块')
|
||
})
|