Files
memind/asset-gateway.mjs

214 lines
8.0 KiB
JavaScript

import crypto from 'node:crypto';
export const ASSET_PLUGIN_CATALOG = [
{
id: 'asset-search',
label: '素材检索',
description: '从已授权图库检索素材;不改变现有页面默认生成流程。',
providers: ['pexels', 'unsplash'],
supportsLlm: true,
},
{
id: 'asset-generate',
label: '图片生成',
description: '异步生成页面所需图片;关闭或失败时必须降级为无图页面。',
providers: ['flux-schnell', 'comfyui'],
supportsLlm: true,
},
{
id: 'asset-transform',
label: '图片处理',
description: '压缩、裁剪和格式转换等确定性处理。',
providers: ['sharp'],
supportsLlm: false,
},
{
id: 'asset-analyze',
label: '图片理解',
description: '对图片进行描述、结构分析或辅助可访问性标注。',
providers: ['qwen25-vl'],
supportsLlm: true,
},
];
const pluginById = new Map(ASSET_PLUGIN_CATALOG.map((plugin) => [plugin.id, plugin]));
function normalizePluginId(value) {
const id = String(value ?? '').trim().toLowerCase();
return pluginById.has(id) ? id : null;
}
function normalizeProvider(plugin, value) {
const provider = String(value ?? '').trim().toLowerCase();
return plugin?.providers.includes(provider) ? provider : null;
}
function toBoolean(value, defaultValue = false) {
if (value === undefined || value === null) return defaultValue;
return Boolean(value);
}
function rowToPluginConfig(row, providerKey = null) {
const plugin = pluginById.get(row.plugin_id);
return {
pluginId: row.plugin_id,
label: plugin?.label ?? row.plugin_id,
description: plugin?.description ?? '',
providers: plugin?.providers ?? [],
supportsLlm: Boolean(plugin?.supportsLlm),
enabled: Boolean(row.enabled),
provider: row.provider,
llmProviderKeyId: row.llm_provider_key_id ?? null,
llmProviderName: providerKey?.name ?? null,
llmProviderId: providerKey?.providerId ?? null,
llmModel: row.llm_model ?? null,
updatedAt: Number(row.updated_at ?? 0) || null,
};
}
/**
* Configuration-only control plane for optional asset plugins.
*
* This service deliberately does not invoke a model, external API, queue, or
* storage. Callers must opt in to it, which keeps the current chat and page
* generation path unchanged until a later adapter explicitly uses a plugin.
*/
export function createAssetGatewayConfigService(pool, { llmProviderService = null } = {}) {
async function getGlobalRow() {
const [rows] = await pool.query(
'SELECT * FROM h5_asset_gateway_config WHERE config_key = ? LIMIT 1',
['default'],
);
return rows[0] ?? null;
}
async function listRows() {
const [rows] = await pool.query('SELECT * FROM h5_asset_plugin_configs ORDER BY plugin_id');
return rows;
}
async function getRow(pluginId) {
const [rows] = await pool.query(
'SELECT * FROM h5_asset_plugin_configs WHERE plugin_id = ? LIMIT 1',
[pluginId],
);
return rows[0] ?? null;
}
async function listKeyMap() {
if (!llmProviderService?.listKeys) return new Map();
const keys = await llmProviderService.listKeys();
return new Map(keys.map((key) => [key.id, key]));
}
async function ensureLlmBinding({ plugin, keyId, model, enabled }) {
if (!plugin.supportsLlm && (keyId || model)) {
return { ok: false, message: `${plugin.label}不支持 LLM 绑定` };
}
if (!enabled || (!keyId && !model)) return { ok: true };
if (!keyId || !model) return { ok: false, message: 'LLM Provider 与模型必须同时选择' };
if (!llmProviderService?.listKeys) return { ok: false, message: 'LLM Provider 服务未启用' };
const key = (await llmProviderService.listKeys()).find((item) => item.id === keyId);
if (!key || key.status !== 'active') return { ok: false, message: 'LLM Provider 不存在或已禁用' };
if (!key.models.includes(model)) return { ok: false, message: '模型不在该 Provider 支持列表中' };
return { ok: true };
}
return {
catalog: ASSET_PLUGIN_CATALOG,
async getConfig() {
const [globalRow, rows, keyMap] = await Promise.all([getGlobalRow(), listRows(), listKeyMap()]);
const byPlugin = new Map(rows.map((row) => [row.plugin_id, row]));
return {
enabled: Boolean(globalRow?.enabled),
updatedAt: Number(globalRow?.updated_at ?? 0) || null,
plugins: ASSET_PLUGIN_CATALOG.map((plugin) => {
const row = byPlugin.get(plugin.id);
return row
? rowToPluginConfig(row, keyMap.get(row.llm_provider_key_id) ?? null)
: {
pluginId: plugin.id,
label: plugin.label,
description: plugin.description,
providers: plugin.providers,
supportsLlm: plugin.supportsLlm,
enabled: false,
provider: null,
llmProviderKeyId: null,
llmProviderName: null,
llmProviderId: null,
llmModel: null,
updatedAt: null,
};
}),
};
},
async updateGlobalConfig(payload = {}, { updatedBy = null } = {}) {
const enabled = toBoolean(payload.enabled);
const now = Date.now();
const existing = await getGlobalRow();
if (existing) {
await pool.query(
'UPDATE h5_asset_gateway_config SET enabled = ?, updated_by = ?, updated_at = ? WHERE config_key = ?',
[enabled ? 1 : 0, updatedBy, now, 'default'],
);
} else {
await pool.query(
'INSERT INTO h5_asset_gateway_config (config_key, enabled, updated_by, updated_at) VALUES (?, ?, ?, ?)',
['default', enabled ? 1 : 0, updatedBy, now],
);
}
return this.getConfig();
},
async updatePluginConfig(rawPluginId, payload = {}, { updatedBy = null } = {}) {
const pluginId = normalizePluginId(rawPluginId);
if (!pluginId) return { ok: false, message: '不支持的资产插件' };
const plugin = pluginById.get(pluginId);
const enabled = toBoolean(payload.enabled);
const provider = normalizeProvider(plugin, payload.provider);
if (enabled && !provider) return { ok: false, message: '请选择该插件支持的 Provider' };
const keyId = String(payload.llmProviderKeyId ?? payload.keyId ?? '').trim() || null;
const model = String(payload.llmModel ?? payload.model ?? '').trim() || null;
const binding = await ensureLlmBinding({ plugin, keyId, model, enabled });
if (!binding.ok) return binding;
const existing = await getRow(pluginId);
const now = Date.now();
if (existing) {
await pool.query(
`UPDATE h5_asset_plugin_configs
SET enabled = ?, provider = ?, llm_provider_key_id = ?, llm_model = ?, updated_by = ?, updated_at = ?
WHERE plugin_id = ?`,
[enabled ? 1 : 0, provider, keyId, model, updatedBy, now, pluginId],
);
} else {
await pool.query(
`INSERT INTO h5_asset_plugin_configs
(id, plugin_id, enabled, provider, llm_provider_key_id, llm_model, updated_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[crypto.randomUUID(), pluginId, enabled ? 1 : 0, provider, keyId, model, updatedBy, now, now],
);
}
return { ok: true, config: await this.getConfig() };
},
async resolvePlugin(rawPluginId) {
const pluginId = normalizePluginId(rawPluginId);
if (!pluginId) return { ok: false, code: 'unsupported_plugin', message: '不支持的资产插件' };
const global = await getGlobalRow();
if (!global?.enabled) {
return { ok: false, code: 'gateway_disabled', message: '资产能力未启用,可安全降级到原有流程' };
}
const row = await getRow(pluginId);
if (!row?.enabled) {
return { ok: false, code: 'plugin_disabled', message: '资产插件未启用,可安全降级到原有流程' };
}
return { ok: true, plugin: rowToPluginConfig(row) };
},
};
}