mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-20 23:13:51 +08:00
fix: persist Slack signing secret and sync models.json in web mode
Two critical correctness bugs from recent commits: 1. Hermes Slack signingSecret was stripped from config.yaml on save but never written to SLACK_SIGNING_SECRET in ~/.hermes/.env, causing data loss and broken HTTP webhook mode after any channel save. 2. Tauri write_openclaw_config syncs providers to agents/*/models.json (Issue #127 fix) but the dev-api web shim did not, leaving Gateway with stale models.json after Models page saves in browser mode. Add regression tests for both paths. Co-authored-by: 晴天 <1186258278@users.noreply.github.com>
This commit is contained in:
@@ -2247,10 +2247,70 @@ function mergeConfigsPreservingFields(existing, next) {
|
||||
return merged
|
||||
}
|
||||
|
||||
export function syncProvidersToAgentModels(config, openclawDir = OPENCLAW_DIR) {
|
||||
const srcProviders = config?.models?.providers
|
||||
if (!srcProviders || typeof srcProviders !== 'object' || Array.isArray(srcProviders)) return
|
||||
|
||||
const agentIds = ['main']
|
||||
for (const agent of Array.isArray(config?.agents?.list) ? config.agents.list : []) {
|
||||
const id = String(agent?.id || '').trim()
|
||||
if (id && id !== 'main') agentIds.push(id)
|
||||
}
|
||||
|
||||
const agentsDir = path.join(openclawDir, 'agents')
|
||||
for (const agentId of agentIds) {
|
||||
const modelsPath = path.join(agentsDir, agentId, 'agent', 'models.json')
|
||||
if (!fs.existsSync(modelsPath)) continue
|
||||
|
||||
let modelsJson
|
||||
try {
|
||||
modelsJson = JSON.parse(fs.readFileSync(modelsPath, 'utf8'))
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (!modelsJson || typeof modelsJson !== 'object' || Array.isArray(modelsJson)) continue
|
||||
|
||||
let changed = false
|
||||
if (!modelsJson.providers || typeof modelsJson.providers !== 'object' || Array.isArray(modelsJson.providers)) {
|
||||
modelsJson.providers = {}
|
||||
changed = true
|
||||
}
|
||||
|
||||
const dstProviders = modelsJson.providers
|
||||
for (const providerName of Object.keys(dstProviders)) {
|
||||
if (!Object.hasOwn(srcProviders, providerName)) {
|
||||
delete dstProviders[providerName]
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
for (const [providerName, srcProvider] of Object.entries(srcProviders)) {
|
||||
if (!Object.hasOwn(dstProviders, providerName)) {
|
||||
dstProviders[providerName] = srcProvider
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
const dstProvider = dstProviders[providerName]
|
||||
if (!dstProvider || typeof dstProvider !== 'object' || Array.isArray(dstProvider)) continue
|
||||
for (const field of ['baseUrl', 'apiKey', 'api']) {
|
||||
const srcVal = srcProvider?.[field]
|
||||
if (typeof srcVal === 'string' && dstProvider[field] !== srcVal) {
|
||||
dstProvider[field] = srcVal
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
fs.writeFileSync(modelsPath, JSON.stringify(modelsJson, null, 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeOpenclawConfigFile(config) {
|
||||
const cleaned = stripUiFields(config)
|
||||
if (fs.existsSync(CONFIG_PATH)) fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + '.bak')
|
||||
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cleaned, null, 2))
|
||||
syncProvidersToAgentModels(cleaned)
|
||||
}
|
||||
|
||||
function ensureAgentsList(config) {
|
||||
@@ -6313,6 +6373,7 @@ export function buildHermesChannelConfigValues(config = {}, envValues = {}) {
|
||||
putHermesString(form, extra, 'app_token')
|
||||
form.appToken = hermesEnvValue(envValues, 'SLACK_APP_TOKEN') || form.appToken || ''
|
||||
putHermesString(form, extra, 'signing_secret')
|
||||
form.signingSecret = hermesEnvValue(envValues, 'SLACK_SIGNING_SECRET') || form.signingSecret || ''
|
||||
putHermesString(form, extra, 'webhook_path')
|
||||
} else if (platform === 'feishu') {
|
||||
for (const key of ['app_id', 'app_secret', 'domain', 'connection_mode', 'webhook_path', 'reaction_notifications']) {
|
||||
@@ -6772,6 +6833,7 @@ export function buildHermesChannelEnvUpdates(platform, form = {}) {
|
||||
} else if (platform === 'slack') {
|
||||
updates.SLACK_BOT_TOKEN = String(form.botToken || '').trim()
|
||||
updates.SLACK_APP_TOKEN = String(form.appToken || '').trim()
|
||||
updates.SLACK_SIGNING_SECRET = String(form.signingSecret || '').trim()
|
||||
updates.SLACK_ALLOWED_USERS = csvEnvValue(form.allowFrom)
|
||||
if (Object.hasOwn(form, 'requireMention')) updates.SLACK_REQUIRE_MENTION = boolEnvValue(form.requireMention)
|
||||
} else if (platform === 'feishu') {
|
||||
|
||||
@@ -3402,6 +3402,10 @@ fn build_hermes_channel_config_values(
|
||||
.unwrap_or_default();
|
||||
form.insert("appToken".to_string(), Value::String(app_token));
|
||||
insert_json_string_if_present(&mut form, &extra, "signing_secret", "signingSecret");
|
||||
let signing_secret = hermes_env_value(env_values, "SLACK_SIGNING_SECRET")
|
||||
.or_else(|| json_form_string(&form, "signingSecret"))
|
||||
.unwrap_or_default();
|
||||
form.insert("signingSecret".to_string(), Value::String(signing_secret));
|
||||
insert_json_string_if_present(&mut form, &extra, "webhook_path", "webhookPath");
|
||||
}
|
||||
"feishu" => {
|
||||
@@ -11182,6 +11186,10 @@ fn build_hermes_channel_env_updates(platform: &str, form: &Value) -> Vec<(String
|
||||
"SLACK_APP_TOKEN",
|
||||
form_string(form, "appToken").unwrap_or_default(),
|
||||
);
|
||||
push(
|
||||
"SLACK_SIGNING_SECRET",
|
||||
form_string(form, "signingSecret").unwrap_or_default(),
|
||||
);
|
||||
push("SLACK_ALLOWED_USERS", csv_env_value(form, "allowFrom"));
|
||||
if let Some(value) = form_bool(form, "requireMention") {
|
||||
push("SLACK_REQUIRE_MENTION", bool_env_value(value));
|
||||
@@ -24517,6 +24525,41 @@ platforms:
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_env_updates_include_slack_signing_secret() {
|
||||
let env = build_hermes_channel_env_updates(
|
||||
"slack",
|
||||
&json!({
|
||||
"botToken": "xoxb-new",
|
||||
"appToken": "xapp-new",
|
||||
"signingSecret": "new-signing-secret",
|
||||
"allowFrom": ["U1"],
|
||||
"requireMention": true,
|
||||
}),
|
||||
);
|
||||
|
||||
assert!(env.contains(&(
|
||||
"SLACK_BOT_TOKEN".to_string(),
|
||||
"xoxb-new".to_string()
|
||||
)));
|
||||
assert!(env.contains(&(
|
||||
"SLACK_APP_TOKEN".to_string(),
|
||||
"xapp-new".to_string()
|
||||
)));
|
||||
assert!(env.contains(&(
|
||||
"SLACK_SIGNING_SECRET".to_string(),
|
||||
"new-signing-secret".to_string()
|
||||
)));
|
||||
assert!(env.contains(&(
|
||||
"SLACK_ALLOWED_USERS".to_string(),
|
||||
"U1".to_string()
|
||||
)));
|
||||
assert!(env.contains(&(
|
||||
"SLACK_REQUIRE_MENTION".to_string(),
|
||||
"true".to_string()
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_platform_values_prefer_env_and_preserve_yaml_runtime_fields() {
|
||||
let config: serde_yaml::Value = serde_yaml::from_str(
|
||||
|
||||
66
tests/dev-api-models-sync.test.js
Normal file
66
tests/dev-api-models-sync.test.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { syncProvidersToAgentModels } from '../scripts/dev-api.js'
|
||||
|
||||
test('Web API write 会同步 openclaw.json providers 到 agent models.json', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'clawpanel-models-sync-'))
|
||||
try {
|
||||
const modelsPath = path.join(tmp, 'agents', 'main', 'agent', 'models.json')
|
||||
fs.mkdirSync(path.dirname(modelsPath), { recursive: true })
|
||||
fs.writeFileSync(modelsPath, JSON.stringify({
|
||||
providers: {
|
||||
a: { baseUrl: 'http://old-a', models: [{ id: 'm1' }] },
|
||||
b: { baseUrl: 'http://old-b', models: [{ id: 'm2' }] },
|
||||
},
|
||||
}, null, 2))
|
||||
|
||||
syncProvidersToAgentModels({
|
||||
models: {
|
||||
providers: {
|
||||
a: { baseUrl: 'http://new-a', apiKey: 'key-a', models: [{ id: 'm1' }] },
|
||||
},
|
||||
},
|
||||
}, tmp)
|
||||
|
||||
const synced = JSON.parse(fs.readFileSync(modelsPath, 'utf8'))
|
||||
assert.equal(synced.providers.a.baseUrl, 'http://new-a')
|
||||
assert.equal(synced.providers.a.apiKey, 'key-a')
|
||||
assert.equal(synced.providers.b, undefined)
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('Web API provider sync 保留 agent models.json 中用户手动添加的 provider 模型列表', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'clawpanel-models-sync-'))
|
||||
try {
|
||||
const modelsPath = path.join(tmp, 'agents', 'main', 'agent', 'models.json')
|
||||
fs.mkdirSync(path.dirname(modelsPath), { recursive: true })
|
||||
fs.writeFileSync(modelsPath, JSON.stringify({
|
||||
providers: {
|
||||
a: {
|
||||
baseUrl: 'http://old-a',
|
||||
models: [{ id: 'm1' }, { id: 'custom-model' }],
|
||||
},
|
||||
},
|
||||
}, null, 2))
|
||||
|
||||
syncProvidersToAgentModels({
|
||||
models: {
|
||||
providers: {
|
||||
a: { baseUrl: 'http://new-a', models: [{ id: 'm1' }] },
|
||||
},
|
||||
},
|
||||
}, tmp)
|
||||
|
||||
const synced = JSON.parse(fs.readFileSync(modelsPath, 'utf8'))
|
||||
assert.equal(synced.providers.a.baseUrl, 'http://new-a')
|
||||
assert.deepEqual(synced.providers.a.models, [{ id: 'm1' }, { id: 'custom-model' }])
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
@@ -360,6 +360,43 @@ test('Hermes 渠道保存会从 YAML 清理旧凭证,避免覆盖 .env 运行
|
||||
assert.equal(next.platforms.slack.extra.unknown_option, 'keep-me')
|
||||
})
|
||||
|
||||
test('Hermes Slack 保存会将 signingSecret 写入 SLACK_SIGNING_SECRET 环境变量', () => {
|
||||
const env = buildHermesChannelEnvUpdates('slack', {
|
||||
botToken: 'xoxb-new',
|
||||
appToken: 'xapp-new',
|
||||
signingSecret: 'new-signing-secret',
|
||||
allowFrom: ['U1'],
|
||||
requireMention: true,
|
||||
})
|
||||
|
||||
assert.equal(env.SLACK_BOT_TOKEN, 'xoxb-new')
|
||||
assert.equal(env.SLACK_APP_TOKEN, 'xapp-new')
|
||||
assert.equal(env.SLACK_SIGNING_SECRET, 'new-signing-secret')
|
||||
assert.equal(env.SLACK_ALLOWED_USERS, 'U1')
|
||||
assert.equal(env.SLACK_REQUIRE_MENTION, 'true')
|
||||
})
|
||||
|
||||
test('Hermes Slack 读取会从 SLACK_SIGNING_SECRET 环境变量回填 signingSecret', () => {
|
||||
const values = buildHermesChannelConfigValues({
|
||||
platforms: {
|
||||
slack: {
|
||||
enabled: true,
|
||||
extra: {
|
||||
webhook_path: '/slack/events',
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
SLACK_BOT_TOKEN: 'xoxb-env',
|
||||
SLACK_APP_TOKEN: 'xapp-env',
|
||||
SLACK_SIGNING_SECRET: 'signing-from-env',
|
||||
})
|
||||
|
||||
assert.equal(values.slack.botToken, 'xoxb-env')
|
||||
assert.equal(values.slack.appToken, 'xapp-env')
|
||||
assert.equal(values.slack.signingSecret, 'signing-from-env')
|
||||
})
|
||||
|
||||
test('Hermes 钉钉保存会使用运行时实际读取的字段', () => {
|
||||
const next = mergeHermesChannelConfig({
|
||||
platforms: {
|
||||
|
||||
Reference in New Issue
Block a user