chore(ops): add script to sync local deepseek-cp config to production
Memind CI / Test, build, and release guards (push) Successful in 4m47s

Automates copying the local deepseek-cp provider key, rebinding Goose executors, and syncing all goosed targets on 103.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-29 14:23:37 +08:00
parent 6db460eb89
commit 073185f57c
+259
View File
@@ -0,0 +1,259 @@
#!/usr/bin/env node
/**
* Copy local deepseek-cp provider config to production and rebind Goose executors.
*
* Usage (from dev machine):
* eval "$(ssh john@58.38.22.103 'grep -E "^(DATABASE_URL|TKMIND_API_TARGET|TKMIND_API_TARGETS|TKMIND_SERVER__SECRET_KEY)=" /Users/john/Project/Memind/.env | sed "s/^/TARGET_/"')"
* node scripts/sync-deepseek-cp-prod.mjs
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import mysql from 'mysql2/promise';
import { Agent, fetch } from 'undici';
import { createDbPool } from '../db.mjs';
import {
createLlmProviderService,
decryptSecret,
CUSTOM_PROVIDER_ID,
} from '../llm-providers.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const CONFIG_NAME = 'deepseek-cp';
const OLD_CONFIG_NAME = 'DeepSeek 直连';
const MODEL = 'deepseek-v4-pro';
const MODELS = ['deepseek-v4-pro', 'deepseek-v4-flash'];
const API_URL = 'https://card.nassaapi.xyz/v1';
const EXECUTORS = ['goose', 'aider', 'openhands'];
function loadEnvFile(filePath) {
if (!fs.existsSync(filePath)) return;
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq < 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1).trim();
if (!process.env[key]) process.env[key] = value;
}
}
function parseApiTargets(raw, fallback) {
const values = String(raw ?? '')
.split(',')
.map((item) => item.trim())
.filter(Boolean);
return [...new Set([...(values.length ? values : []), fallback].filter(Boolean))];
}
async function readApiKeyFromLocal(pool) {
const [rows] = await pool.query(
'SELECT * FROM h5_llm_provider_keys WHERE name = ? LIMIT 1',
[CONFIG_NAME],
);
const row = rows[0];
if (!row) throw new Error(`本地未找到 ${CONFIG_NAME} 配置`);
const apiKey = decryptSecret(
{
ciphertext: row.api_key_ciphertext,
iv: row.api_key_iv,
tag: row.api_key_tag,
},
process.env.TKMIND_SERVER__SECRET_KEY,
);
if (!apiKey) throw new Error(`无法解密本地 ${CONFIG_NAME} API Key`);
return apiKey;
}
async function upsertDeepseekCp(svc, apiKey) {
const keys = await svc.listKeys();
let row = keys.find((item) => item.name === CONFIG_NAME);
if (row) {
const updated = await svc.updateKey(row.id, {
name: CONFIG_NAME,
apiKey,
apiUrl: API_URL,
defaultModel: MODEL,
models: MODELS,
status: 'active',
});
if (!updated.ok) throw new Error(`更新 ${CONFIG_NAME} 失败: ${updated.message}`);
row = updated.key;
console.log(`已更新生产 ${CONFIG_NAME}: ${row.id}`);
} else {
const created = await svc.createKey({
providerId: CUSTOM_PROVIDER_ID,
providerKind: 'custom',
name: CONFIG_NAME,
apiKey,
apiUrl: API_URL,
defaultModel: MODEL,
models: MODELS,
});
if (!created.ok) throw new Error(`创建 ${CONFIG_NAME} 失败: ${created.message}`);
row = created.key;
console.log(`已创建生产 ${CONFIG_NAME}: ${row.id}`);
}
return row.id;
}
async function replaceProviderKeyReferences(pool, oldKeyId, newKeyId) {
if (!oldKeyId || oldKeyId === newKeyId) return;
const [pluginRows] = await pool.query(
'SELECT plugin_id, llm_provider_key_id FROM h5_asset_plugin_configs WHERE llm_provider_key_id = ?',
[oldKeyId],
);
if (pluginRows.length) {
await pool.query(
'UPDATE h5_asset_plugin_configs SET llm_provider_key_id = ?, updated_at = ? WHERE llm_provider_key_id = ?',
[newKeyId, Date.now(), oldKeyId],
);
console.log(`已更新 asset plugin 引用: ${pluginRows.length}`);
}
const [memRows] = await pool.query(
'SELECT config_scope, config_json FROM h5_memory_v2_admin_config WHERE config_json LIKE ?',
[`%${oldKeyId}%`],
);
for (const row of memRows) {
const config = typeof row.config_json === 'string'
? JSON.parse(row.config_json)
: row.config_json;
let changed = false;
for (const section of Object.values(config ?? {})) {
if (section && typeof section === 'object' && section.modelProviderKeyId === oldKeyId) {
section.modelProviderKeyId = newKeyId;
if (section.model === 'deepseek-chat') section.model = 'deepseek-v4-flash';
changed = true;
}
}
if (changed) {
await pool.query(
'UPDATE h5_memory_v2_admin_config SET config_json = ?, updated_at = ? WHERE config_scope = ?',
[JSON.stringify(config), Date.now(), row.config_scope],
);
console.log(`已更新 memory-v2 配置: ${row.config_scope}`);
}
}
const [wechatRows] = await pool.query(
'SELECT config_key, config_json FROM h5_wechat_admin_config WHERE config_json LIKE ?',
[`%${oldKeyId}%`],
);
for (const row of wechatRows) {
const config = typeof row.config_json === 'string'
? JSON.parse(row.config_json)
: row.config_json;
if (config?.modelProviderKeyId === oldKeyId) {
config.modelProviderKeyId = newKeyId;
await pool.query(
'UPDATE h5_wechat_admin_config SET config_json = ?, updated_at = ? WHERE config_key = ?',
[JSON.stringify(config), Date.now(), row.config_key],
);
console.log(`已更新 wechat admin 配置: ${row.config_key}`);
}
}
}
async function testChatCompletion(svc, keyId) {
const result = await svc.createChatCompletion({
providerKeyId: keyId,
model: MODEL,
temperature: 0,
messages: [{ role: 'user', content: 'Reply with OK only.' }],
});
return result;
}
loadEnvFile(path.join(root, '.env'));
loadEnvFile(path.join(root, '.env.local'));
const targetDatabaseUrl = String(
process.env.TARGET_DATABASE_URL ?? process.env.PROD_DATABASE_URL ?? '',
).trim();
const targetApiTarget = String(
process.env.TARGET_TKMIND_API_TARGET ?? process.env.TKMIND_API_TARGET ?? '',
).trim();
const targetApiTargets = parseApiTargets(
process.env.TARGET_TKMIND_API_TARGETS ?? process.env.TKMIND_API_TARGETS,
targetApiTarget,
);
const targetApiSecret = String(
process.env.TARGET_TKMIND_SERVER__SECRET_KEY
?? process.env.TKMIND_SERVER__SECRET_KEY
?? '',
).trim();
if (!targetDatabaseUrl) {
console.error('缺少 TARGET_DATABASE_URL(或 PROD_DATABASE_URL');
process.exit(1);
}
if (!targetApiTarget || !targetApiSecret) {
console.error('缺少 TARGET_TKMIND_API_TARGET / TARGET_TKMIND_SERVER__SECRET_KEY');
process.exit(1);
}
const localPool = createDbPool();
const prodPool = mysql.createPool({ uri: targetDatabaseUrl, connectionLimit: 4 });
try {
const apiKey = await readApiKeyFromLocal(localPool);
console.log(`已读取本地 ${CONFIG_NAME} API Key`);
const prodSvc = createLlmProviderService(prodPool, {
apiTarget: targetApiTarget,
apiTargets: targetApiTargets,
apiSecret: targetApiSecret,
});
const prodKeysBefore = await prodSvc.listKeys();
const oldKey = prodKeysBefore.find((item) => item.name === OLD_CONFIG_NAME);
const oldKeyId = oldKey?.id ?? null;
const newKeyId = await upsertDeepseekCp(prodSvc, apiKey);
const selected = await prodSvc.selectKey(newKeyId);
if (!selected.ok) throw new Error(`启用 ${CONFIG_NAME} 失败: ${selected.message}`);
console.log(`已将 ${CONFIG_NAME} 设为全局默认`);
for (const executor of EXECUTORS) {
const bound = await prodSvc.setExecutorBinding(executor, {
providerKeyId: newKeyId,
model: MODEL,
enabled: true,
});
if (!bound.ok) throw new Error(`绑定 ${executor} 失败: ${bound.message}`);
console.log(`已绑定 ${executor} -> ${CONFIG_NAME} / ${MODEL}`);
}
await replaceProviderKeyReferences(prodPool, oldKeyId, newKeyId);
const synced = await prodSvc.syncSelectedToGoosed();
if (!synced.ok) throw new Error(`同步 goosed 失败: ${synced.message}`);
console.log(
'goosed 已同步:',
synced.providerId,
synced.model,
`targets=${(synced.targets ?? []).length}`,
);
const test = await testChatCompletion(prodSvc, newKeyId);
console.log(
`${CONFIG_NAME} 联通测试:`,
test.ok ? `OK model=${test.model}` : `失败 ${test.status ?? ''} ${test.message ?? ''}`,
);
const gooseRuntime = await prodSvc.getExecutorRuntimeConfig('goose', { includeSecret: false });
console.log('Goose runtime:', JSON.stringify({
providerId: gooseRuntime.providerId,
providerName: gooseRuntime.providerName,
model: gooseRuntime.model,
apiUrl: gooseRuntime.apiUrl,
}));
if (!test.ok) process.exit(1);
} finally {
await localPool.end().catch(() => {});
await prodPool.end().catch(() => {});
}