import crypto from 'node:crypto'; import { Agent, fetch as undiciFetch } from 'undici'; export const CUSTOM_PROVIDER_ID = '__custom__'; export const LLM_PROVIDER_CATALOG = [ { id: CUSTOM_PROVIDER_ID, label: '自定义 OpenAI 兼容', kind: 'custom', apiKeyEnv: null, defaultModel: '', models: [], }, { id: 'custom_deepseek', label: 'DeepSeek', kind: 'builtin', apiKeyEnv: 'DEEPSEEK_API_KEY', defaultModel: 'deepseek-chat', models: ['deepseek-chat', 'deepseek-reasoner'], }, { id: 'openai', label: 'OpenAI', kind: 'builtin', apiKeyEnv: 'OPENAI_API_KEY', defaultModel: 'gpt-4o', models: ['gpt-4o', 'gpt-4o-mini'], }, { id: 'openrouter', label: 'OpenRouter', kind: 'builtin', apiKeyEnv: 'OPENROUTER_API_KEY', defaultModel: 'anthropic/claude-sonnet-4', models: ['anthropic/claude-sonnet-4', 'openai/gpt-4o'], }, { id: 'anthropic', label: 'Anthropic', kind: 'builtin', apiKeyEnv: 'ANTHROPIC_API_KEY', defaultModel: 'claude-sonnet-4-20250514', models: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022'], }, ]; const catalogById = Object.fromEntries(LLM_PROVIDER_CATALOG.map((item) => [item.id, item])); const insecureDispatcher = new Agent({ connect: { rejectUnauthorized: false }, }); function resolveEncryptionKey(explicitKey) { const raw = explicitKey ?? process.env.H5_SETTINGS_ENCRYPTION_KEY ?? process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret'; return crypto.createHash('sha256').update(raw).digest(); } export function encryptSecret(plaintext, encryptionKey) { const key = resolveEncryptionKey(encryptionKey); const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); return { ciphertext: encrypted.toString('base64'), iv: iv.toString('base64'), tag: cipher.getAuthTag().toString('base64'), }; } export function decryptSecret({ ciphertext, iv, tag }, encryptionKey) { const key = resolveEncryptionKey(encryptionKey); const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'base64')); decipher.setAuthTag(Buffer.from(tag, 'base64')); const plain = Buffer.concat([ decipher.update(Buffer.from(ciphertext, 'base64')), decipher.final(), ]); return plain.toString('utf8'); } export function maskApiKey(apiKey) { if (!apiKey) return ''; if (apiKey.length <= 8) return '*'.repeat(apiKey.length); const head = apiKey.slice(0, 4); const tail = apiKey.slice(-4); return `${head}${'*'.repeat(Math.max(apiKey.length - 8, 4))}${tail}`; } export function parseModelList(raw) { if (Array.isArray(raw)) { return [...new Set(raw.map((item) => String(item).trim()).filter(Boolean))]; } return [ ...new Set( String(raw ?? '') .split(/[\n,]/) .map((item) => item.trim()) .filter(Boolean), ), ]; } export function normalizeApiUrl(raw) { const trimmed = String(raw ?? '').trim(); if (!trimmed) return ''; if (/^https?:\/\//i.test(trimmed)) return trimmed.replace(/\/+$/, ''); return `http://${trimmed.replace(/\/+$/, '')}`; } export function resolveChatCompletionsUrl(apiUrl) { const normalized = normalizeApiUrl(apiUrl); if (!normalized) return ''; if (normalized.endsWith('/chat/completions')) return normalized; if (normalized.endsWith('/v1')) return `${normalized}/chat/completions`; return `${normalized}/chat/completions`; } export const RELAY_BOOTSTRAP = { name: process.env.H5_RELAY_BOOTSTRAP_NAME ?? 'Relay Buyer Ollama', apiUrl: process.env.H5_RELAY_BOOTSTRAP_URL ?? 'http://127.0.0.1:18300/relay/buyer/v1/chat/completions', apiKey: process.env.H5_RELAY_BOOTSTRAP_API_KEY ?? 'UqyHPKSSEZq0-oPnl8sru-7hZcJ2anPUL1yAVk866Vo', models: parseModelList(process.env.H5_RELAY_BOOTSTRAP_MODELS ?? 'qwen2.5:3b'), defaultModel: process.env.H5_RELAY_BOOTSTRAP_MODEL ?? 'qwen2.5:3b', relayProvider: process.env.H5_RELAY_BOOTSTRAP_PROVIDER ?? 'ollama', }; export async function testRelayConnection( { apiUrl, apiKey, model, relayProvider }, fetchImpl = undiciFetch, ) { const url = resolveChatCompletionsUrl(apiUrl); if (!url) return { ok: false, message: 'API 地址无效' }; if (!apiKey) return { ok: false, message: '缺少 API Key' }; if (!model) return { ok: false, message: '缺少模型' }; const started = Date.now(); const body = { model, messages: [{ role: 'user', content: 'Hello' }], stream: false, ...(relayProvider ? { provider: relayProvider } : {}), }; const upstream = await fetchImpl(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify(body), dispatcher: url.startsWith('https://') ? insecureDispatcher : undefined, }); const latencyMs = Date.now() - started; const text = await upstream.text().catch(() => ''); if (!upstream.ok) { let detail = text.slice(0, 500) || '(空响应)'; if (detail === '{}') { detail = '空 JSON 响应'; } const statusHint = upstream.status === 401 ? 'Bearer Token 无效或已过期' : upstream.status === 404 ? '地址不存在,请检查 API URL' : upstream.status === 400 ? '请求参数被拒绝,请检查 model / provider' : `HTTP ${upstream.status}`; return { ok: false, latencyMs, message: `Relay ${upstream.status} ${statusHint}:${detail}`, }; } let data; try { data = JSON.parse(text); } catch { return { ok: false, latencyMs, message: '响应不是 JSON' }; } const reply = data?.choices?.[0]?.message?.content ?? data?.message?.content ?? data?.output ?? null; return { ok: true, latencyMs, model, reply: reply ? String(reply).slice(0, 300) : '(联通成功,无文本内容)', }; } function parseModelsJson(raw) { if (!raw) return []; try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parseModelList(parsed) : []; } catch { return []; } } function rowToPublic(row, catalogItem, apiKeyForMask) { const providerKind = row.provider_kind ?? 'builtin'; const models = providerKind === 'custom' ? parseModelsJson(row.models_json) : (catalogItem?.models ?? []); return { id: row.id, providerId: row.provider_id, providerKind, providerLabel: providerKind === 'custom' ? row.name : (catalogItem?.label ?? row.provider_id), name: row.name, defaultModel: row.default_model, models, apiUrl: row.api_url ?? null, basePath: row.base_path ?? null, engine: row.engine ?? 'openai', relayProvider: row.relay_provider ?? null, goosedProviderId: row.goosed_provider_id ?? null, status: row.status, isSelected: Boolean(row.is_selected), apiKeyMasked: maskApiKey(apiKeyForMask), createdAt: Number(row.created_at), updatedAt: Number(row.updated_at), }; } function profileFromRow(row, decryptRow) { const providerKind = row.provider_kind ?? 'builtin'; const models = providerKind === 'custom' ? parseModelsJson(row.models_json) : [row.default_model]; return { providerKind, providerId: row.provider_id, goosedProviderId: row.goosed_provider_id ?? null, name: row.name, defaultModel: row.default_model, apiKey: decryptRow(row), apiUrl: row.api_url ?? null, basePath: row.base_path ?? null, engine: row.engine ?? 'openai', relayProvider: row.relay_provider ?? null, models, }; } async function goosedApiFetch(apiTarget, apiSecret, pathname, init = {}, fetchImpl = undiciFetch) { const url = new URL(pathname, apiTarget); const headers = { ...(init.headers ?? {}), 'X-Secret-Key': apiSecret, }; if (init.body && !headers['Content-Type']) { headers['Content-Type'] = 'application/json'; } const dispatcher = apiTarget.startsWith('https://') ? insecureDispatcher : undefined; return fetchImpl(url, { ...init, headers, dispatcher }); } async function writeGoosedConfig(apiTarget, apiSecret, key, value, isSecret, fetchImpl) { const upstream = await goosedApiFetch( apiTarget, apiSecret, '/config/write', { method: 'POST', body: JSON.stringify({ key, value, is_secret: isSecret }), }, fetchImpl, ); if (!upstream.ok) { const text = await upstream.text().catch(() => ''); throw new Error(`同步 TKMind Agent 配置 ${key} 失败: ${text || upstream.status}`); } } async function upsertCustomProviderOnGoosed(apiTarget, apiSecret, profile, fetchImpl) { const headers = profile.relayProvider ? { 'X-Provider': profile.relayProvider } : undefined; const body = { engine: profile.engine || 'openai', display_name: profile.name, api_url: profile.apiUrl, api_key: profile.apiKey, models: profile.models, supports_streaming: true, requires_auth: true, ...(profile.basePath ? { base_path: profile.basePath } : {}), ...(headers ? { headers } : {}), }; if (profile.goosedProviderId) { const upstream = await goosedApiFetch( apiTarget, apiSecret, `/config/custom-providers/${encodeURIComponent(profile.goosedProviderId)}`, { method: 'PUT', body: JSON.stringify(body) }, fetchImpl, ); if (!upstream.ok) { const text = await upstream.text().catch(() => ''); throw new Error(`更新 TKMind Agent 自定义 provider 失败: ${text || upstream.status}`); } return profile.goosedProviderId; } const upstream = await goosedApiFetch( apiTarget, apiSecret, '/config/custom-providers', { method: 'POST', body: JSON.stringify(body) }, fetchImpl, ); if (!upstream.ok) { const text = await upstream.text().catch(() => ''); throw new Error(`创建 TKMind Agent 自定义 provider 失败: ${text || upstream.status}`); } const data = await upstream.json(); return data.provider_name; } async function removeCustomProviderOnGoosed(apiTarget, apiSecret, goosedProviderId, fetchImpl) { if (!goosedProviderId) return; const upstream = await goosedApiFetch( apiTarget, apiSecret, `/config/custom-providers/${encodeURIComponent(goosedProviderId)}`, { method: 'DELETE' }, fetchImpl, ); if (!upstream.ok && upstream.status !== 404) { const text = await upstream.text().catch(() => ''); throw new Error(`删除 TKMind Agent 自定义 provider 失败: ${text || upstream.status}`); } } async function syncBuiltinProfileToGoosed(apiTarget, apiSecret, profile, fetchImpl) { const catalogItem = catalogById[profile.providerId]; if (!catalogItem?.apiKeyEnv) { throw new Error(`未知内置 provider: ${profile.providerId}`); } await writeGoosedConfig( apiTarget, apiSecret, catalogItem.apiKeyEnv, profile.apiKey, true, fetchImpl, ); await writeGoosedConfig( apiTarget, apiSecret, 'GOOSE_PROVIDER', profile.providerId, false, fetchImpl, ); await writeGoosedConfig( apiTarget, apiSecret, 'GOOSE_MODEL', profile.defaultModel, false, fetchImpl, ); await writeGoosedConfig( apiTarget, apiSecret, 'TKMIND_PROVIDER', profile.providerId, false, fetchImpl, ); await writeGoosedConfig( apiTarget, apiSecret, 'TKMIND_MODEL', profile.defaultModel, false, fetchImpl, ); } export async function syncProfileToGoosed(apiTarget, apiSecret, profile, fetchImpl = undiciFetch) { if (profile.providerKind === 'custom') { const goosedProviderId = await upsertCustomProviderOnGoosed( apiTarget, apiSecret, profile, fetchImpl, ); await writeGoosedConfig( apiTarget, apiSecret, 'GOOSE_PROVIDER', goosedProviderId, false, fetchImpl, ); await writeGoosedConfig( apiTarget, apiSecret, 'GOOSE_MODEL', profile.defaultModel, false, fetchImpl, ); await writeGoosedConfig( apiTarget, apiSecret, 'TKMIND_PROVIDER', goosedProviderId, false, fetchImpl, ); await writeGoosedConfig( apiTarget, apiSecret, 'TKMIND_MODEL', profile.defaultModel, false, fetchImpl, ); return goosedProviderId; } await syncBuiltinProfileToGoosed(apiTarget, apiSecret, profile, fetchImpl); return profile.providerId; } function isCustomPayload(payload) { return payload?.providerId === CUSTOM_PROVIDER_ID || payload?.providerKind === 'custom'; } function validateCustomPayload(payload) { const name = String(payload?.name ?? '').trim(); const apiKey = String(payload?.apiKey ?? '').trim(); const apiUrl = normalizeApiUrl(payload?.apiUrl); const models = parseModelList(payload?.models); const defaultModel = String(payload?.defaultModel ?? '').trim() || models[0] || ''; if (!name) return { ok: false, message: '请填写配置名称' }; if (!apiUrl) return { ok: false, message: '请填写 API 地址' }; if (!apiKey) return { ok: false, message: '请填写 API Key / Bearer Token' }; if (models.length === 0) return { ok: false, message: '请至少填写一个模型' }; if (!models.includes(defaultModel)) { return { ok: false, message: '默认模型必须在模型列表中' }; } return { ok: true, value: { name, apiKey, apiUrl, models, defaultModel, basePath: String(payload?.basePath ?? '').trim() || null, engine: String(payload?.engine ?? 'openai').trim() || 'openai', relayProvider: String(payload?.relayProvider ?? '').trim() || null, }, }; } export function createLlmProviderService( pool, { apiTarget, apiSecret, encryptionKey, apiFetchImpl = undiciFetch } = {}, ) { function catalogItem(providerId) { return catalogById[providerId] ?? null; } function decryptRow(row) { return decryptSecret( { ciphertext: row.api_key_ciphertext, iv: row.api_key_iv, tag: row.api_key_tag, }, encryptionKey, ); } async function getRowById(id) { const [rows] = await pool.query('SELECT * FROM h5_llm_provider_keys WHERE id = ? LIMIT 1', [ id, ]); return rows[0] ?? null; } async function getSelectedRow() { const [rows] = await pool.query( 'SELECT * FROM h5_llm_provider_keys WHERE is_selected = 1 AND status = ? LIMIT 1', ['active'], ); return rows[0] ?? null; } async function clearSelected() { await pool.query('UPDATE h5_llm_provider_keys SET is_selected = 0, updated_at = ?', [ Date.now(), ]); } async function syncRow(row) { const profile = profileFromRow(row, decryptRow); const goosedProviderId = await syncProfileToGoosed( apiTarget, apiSecret, profile, apiFetchImpl, ); if (profile.providerKind === 'custom' && goosedProviderId !== row.goosed_provider_id) { await pool.query( 'UPDATE h5_llm_provider_keys SET goosed_provider_id = ?, provider_id = ?, updated_at = ? WHERE id = ?', [goosedProviderId, goosedProviderId, Date.now(), row.id], ); } return goosedProviderId; } return { catalog: LLM_PROVIDER_CATALOG, async listKeys() { const [rows] = await pool.query( 'SELECT * FROM h5_llm_provider_keys ORDER BY is_selected DESC, updated_at DESC', ); return rows.map((row) => rowToPublic(row, catalogItem(row.provider_id), decryptRow(row)), ); }, async createKey(payload) { const custom = isCustomPayload(payload); let providerKind = 'builtin'; let providerId = String(payload?.providerId ?? '').trim(); let insertName = String(payload?.name ?? '').trim(); let apiKey = String(payload?.apiKey ?? '').trim(); let defaultModel = String(payload?.defaultModel ?? '').trim(); let apiUrl = null; let basePath = null; let engine = 'openai'; let relayProvider = null; let modelsJson = null; let meta = catalogItem(providerId); if (custom) { const validated = validateCustomPayload(payload); if (!validated.ok) return validated; providerKind = 'custom'; providerId = CUSTOM_PROVIDER_ID; meta = catalogById[CUSTOM_PROVIDER_ID]; insertName = validated.value.name; apiKey = validated.value.apiKey; apiUrl = validated.value.apiUrl; basePath = validated.value.basePath; engine = validated.value.engine; relayProvider = validated.value.relayProvider; defaultModel = validated.value.defaultModel; modelsJson = JSON.stringify(validated.value.models); } else { if (!meta) return { ok: false, message: '不支持的 provider' }; if (!insertName) return { ok: false, message: '请填写配置名称' }; if (!apiKey) return { ok: false, message: '请填写 API Key' }; defaultModel = defaultModel || meta.defaultModel; if (!meta.models.includes(defaultModel)) { return { ok: false, message: '不支持的模型' }; } } const [existing] = await pool.query( 'SELECT id FROM h5_llm_provider_keys WHERE name = ? LIMIT 1', [insertName], ); if (existing.length > 0) return { ok: false, message: '配置名称已存在' }; const [countRows] = await pool.query('SELECT COUNT(*) AS total FROM h5_llm_provider_keys'); const shouldSelect = Number(countRows[0]?.total ?? 0) === 0; const encrypted = encryptSecret(apiKey, encryptionKey); const now = Date.now(); const id = crypto.randomUUID(); if (shouldSelect) await clearSelected(); await pool.query( `INSERT INTO h5_llm_provider_keys (id, provider_id, provider_kind, api_url, base_path, models_json, goosed_provider_id, engine, relay_provider, name, api_key_ciphertext, api_key_iv, api_key_tag, default_model, status, is_selected, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?)`, [ id, providerId, providerKind, apiUrl, basePath, modelsJson, engine, relayProvider, insertName, encrypted.ciphertext, encrypted.iv, encrypted.tag, defaultModel, shouldSelect ? 1 : 0, now, now, ], ); let row = await getRowById(id); if (shouldSelect) { await syncRow(row); row = await getRowById(id); } return { ok: true, key: rowToPublic(row, meta, apiKey) }; }, async updateKey(id, payload) { const row = await getRowById(id); if (!row) return { ok: false, message: '配置不存在' }; const providerKind = row.provider_kind ?? 'builtin'; const nextName = payload?.name !== undefined ? String(payload.name).trim() : row.name; const nextApiKey = payload?.apiKey !== undefined ? String(payload.apiKey).trim() : null; const nextStatus = payload?.status === 'disabled' || payload?.status === 'active' ? payload.status : row.status; if (!nextName) return { ok: false, message: '请填写配置名称' }; const [existing] = await pool.query( 'SELECT id FROM h5_llm_provider_keys WHERE name = ? AND id <> ? LIMIT 1', [nextName, id], ); if (existing.length > 0) return { ok: false, message: '配置名称已存在' }; if (row.is_selected && nextStatus === 'disabled') { return { ok: false, message: '当前启用的配置不能禁用,请先切换到其他配置' }; } let nextModel = row.default_model; let nextApiUrl = row.api_url; let nextBasePath = row.base_path; let nextEngine = row.engine ?? 'openai'; let nextRelayProvider = row.relay_provider; let nextModelsJson = row.models_json; if (providerKind === 'custom') { const models = payload?.models !== undefined ? parseModelList(payload.models) : parseModelsJson(row.models_json); nextModel = payload?.defaultModel !== undefined ? String(payload.defaultModel).trim() : row.default_model; nextApiUrl = payload?.apiUrl !== undefined ? normalizeApiUrl(payload.apiUrl) : row.api_url; nextBasePath = payload?.basePath !== undefined ? String(payload.basePath).trim() || null : row.base_path; nextEngine = payload?.engine !== undefined ? String(payload.engine).trim() || 'openai' : row.engine; nextRelayProvider = payload?.relayProvider !== undefined ? String(payload.relayProvider).trim() || null : row.relay_provider; if (models.length === 0) return { ok: false, message: '请至少保留一个模型' }; if (!models.includes(nextModel)) { return { ok: false, message: '默认模型必须在模型列表中' }; } if (!nextApiUrl) return { ok: false, message: '请填写 API 地址' }; nextModelsJson = JSON.stringify(models); } else { const meta = catalogItem(row.provider_id); if (!meta) return { ok: false, message: '不支持的 provider' }; nextModel = payload?.defaultModel !== undefined ? String(payload.defaultModel).trim() : row.default_model; if (!meta.models.includes(nextModel)) { return { ok: false, message: '不支持的模型' }; } } const encrypted = nextApiKey ? encryptSecret(nextApiKey, encryptionKey) : { ciphertext: row.api_key_ciphertext, iv: row.api_key_iv, tag: row.api_key_tag, }; const now = Date.now(); await pool.query( `UPDATE h5_llm_provider_keys SET name = ?, api_url = ?, base_path = ?, models_json = ?, engine = ?, relay_provider = ?, api_key_ciphertext = ?, api_key_iv = ?, api_key_tag = ?, default_model = ?, status = ?, updated_at = ? WHERE id = ?`, [ nextName, nextApiUrl, nextBasePath, nextModelsJson, nextEngine, nextRelayProvider, encrypted.ciphertext, encrypted.iv, encrypted.tag, nextModel, nextStatus, now, id, ], ); if (row.is_selected) { await syncRow(await getRowById(id)); } const updated = await getRowById(id); return { ok: true, key: rowToPublic(updated, catalogItem(updated.provider_id), nextApiKey || decryptRow(row)), }; }, async selectKey(id) { const row = await getRowById(id); if (!row) return { ok: false, message: '配置不存在' }; if (row.status !== 'active') { return { ok: false, message: '已禁用的配置不能启用' }; } await clearSelected(); const now = Date.now(); await pool.query( 'UPDATE h5_llm_provider_keys SET is_selected = 1, updated_at = ? WHERE id = ?', [now, id], ); await syncRow(row); const updated = await getRowById(id); return { ok: true, key: rowToPublic(updated, catalogItem(updated.provider_id), decryptRow(updated)), }; }, async deleteKey(id) { const row = await getRowById(id); if (!row) return { ok: false, message: '配置不存在' }; if (row.is_selected) { return { ok: false, message: '当前启用的配置不能删除,请先切换到其他配置' }; } if ((row.provider_kind ?? 'builtin') === 'custom' && row.goosed_provider_id) { await removeCustomProviderOnGoosed( apiTarget, apiSecret, row.goosed_provider_id, apiFetchImpl, ); } await pool.query('DELETE FROM h5_llm_provider_keys WHERE id = ?', [id]); return { ok: true }; }, async syncSelectedToGoosed() { const row = await getSelectedRow(); if (!row) return { ok: true, synced: false }; await syncRow(row); return { ok: true, synced: true }; }, async getGlobalSettings() { const row = await getSelectedRow(); if (!row) { return { keyId: null, keyName: null, providerLabel: null, globalModel: null, availableModels: [], }; } const publicRow = rowToPublic(row, catalogItem(row.provider_id), decryptRow(row)); return { keyId: publicRow.id, keyName: publicRow.name, providerLabel: publicRow.providerLabel, globalModel: publicRow.defaultModel, availableModels: publicRow.models, }; }, async setGlobalModel(model) { const nextModel = String(model ?? '').trim(); if (!nextModel) return { ok: false, message: '请选择全局模型' }; const row = await getSelectedRow(); if (!row) return { ok: false, message: '请先启用一个 LLM 配置' }; const publicRow = rowToPublic(row, catalogItem(row.provider_id), decryptRow(row)); if (!publicRow.models.includes(nextModel)) { return { ok: false, message: '模型不在当前 Provider 支持列表中' }; } await pool.query( 'UPDATE h5_llm_provider_keys SET default_model = ?, updated_at = ? WHERE id = ?', [nextModel, Date.now(), row.id], ); await syncRow(await getRowById(row.id)); return { ok: true, global: await this.getGlobalSettings() }; }, async testDraft(payload) { const custom = isCustomPayload(payload); if (!custom) { return { ok: false, message: '联通测试目前支持自定义 OpenAI 兼容配置' }; } const validated = validateCustomPayload(payload); if (!validated.ok) return validated; const model = String(payload?.testModel ?? payload?.defaultModel ?? validated.value.defaultModel).trim(); if (!validated.value.models.includes(model)) { return { ok: false, message: '测试模型不在模型列表中' }; } return testRelayConnection( { apiUrl: validated.value.apiUrl, apiKey: validated.value.apiKey, model, relayProvider: validated.value.relayProvider, }, apiFetchImpl, ); }, async testKey(id, testModel) { const row = await getRowById(id); if (!row) return { ok: false, message: '配置不存在' }; if ((row.provider_kind ?? 'builtin') !== 'custom') { return { ok: false, message: '内置 Provider 联通测试暂未开放' }; } const model = String(testModel ?? row.default_model).trim(); const models = parseModelsJson(row.models_json); if (!models.includes(model)) { return { ok: false, message: '测试模型不在配置列表中' }; } return testRelayConnection( { apiUrl: row.api_url, apiKey: decryptRow(row), model, relayProvider: row.relay_provider, }, apiFetchImpl, ); }, async ensureBootstrapRelay() { const [existing] = await pool.query( 'SELECT id FROM h5_llm_provider_keys WHERE name = ? LIMIT 1', [RELAY_BOOTSTRAP.name], ); if (existing.length > 0) { const row = await getRowById(existing[0].id); if (row && !row.is_selected) { await this.selectKey(row.id); } return { ok: true, created: false, keyId: existing[0].id }; } const result = await this.createKey({ providerId: CUSTOM_PROVIDER_ID, name: RELAY_BOOTSTRAP.name, apiKey: RELAY_BOOTSTRAP.apiKey, apiUrl: RELAY_BOOTSTRAP.apiUrl, models: RELAY_BOOTSTRAP.models, defaultModel: RELAY_BOOTSTRAP.defaultModel, relayProvider: RELAY_BOOTSTRAP.relayProvider, }); if (!result.ok) return result; if (result.key && !result.key.isSelected) { await this.selectKey(result.key.id); } return { ok: true, created: true, key: result.key }; }, }; }