chore(ops): add script to sync local deepseek-cp config to production
Memind CI / Test, build, and release guards (push) Successful in 4m47s
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:
@@ -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(() => {});
|
||||
}
|
||||
Reference in New Issue
Block a user