073185f57c
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>
260 lines
8.5 KiB
JavaScript
260 lines
8.5 KiB
JavaScript
#!/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(() => {});
|
||
}
|