Files
memind/scripts/sync-deepseek-cp-prod.mjs
T
john 073185f57c
Memind CI / Test, build, and release guards (push) Successful in 4m47s
chore(ops): add script to sync local deepseek-cp config to production
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>
2026-08-29 14:23:37 +08:00

260 lines
8.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.
#!/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(() => {});
}