Files
memind/image-make-admin-config.mjs
T
john 4b15610aa8
Memind CI / Test, build, and release guards (pull_request) Successful in 2m47s
feat(image): complete reviewed generation delivery
2026-07-20 20:04:53 +08:00

380 lines
13 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.
import crypto from 'node:crypto';
import { decryptSecret, encryptSecret, maskApiKey } from './llm-providers.mjs';
const CONFIG_TABLE = 'h5_image_make_admin_config';
const CONFIG_SCOPE = 'global';
export const IMAGE_MAKE_PROVIDER_IDS = Object.freeze(['mock', 'aliyun_bailian', 'comfyui']);
export const IMAGE_MAKE_ALIYUN_MODELS = Object.freeze([
'qwen-image-plus',
'qwen-image',
]);
export const IMAGE_MAKE_DEFAULT_CONFIG = Object.freeze({
defaultProvider: 'mock',
jobDefaultTimeoutSeconds: 120,
providers: {
mock: { enabled: true },
aliyun_bailian: {
enabled: false,
model: 'qwen-image-plus',
apiBase: 'https://dashscope.aliyuncs.com/api/v1',
},
comfyui: {
enabled: false,
apiBase: 'http://127.0.0.1:8188',
checkpoint: '',
workflowPath: './config/comfyui/workflows/core_txt2img_v1.json',
outputNodeId: '9',
timeoutSeconds: 600,
steps: 20,
cfg: 7,
sampler: 'euler',
scheduler: 'normal',
},
},
});
const clone = (value) => JSON.parse(JSON.stringify(value));
function bool(value, fallback = false) {
if (typeof value === 'boolean') return value;
if (value == null || value === '') return fallback;
return /^(1|true|yes|on)$/i.test(String(value));
}
function clampInt(value, fallback, min, max) {
const number = Number(value);
if (!Number.isFinite(number)) return fallback;
return Math.max(min, Math.min(max, Math.floor(number)));
}
function clampFloat(value, fallback, min, max) {
const number = Number(value);
if (!Number.isFinite(number)) return fallback;
return Math.max(min, Math.min(max, number));
}
function trim(value) {
return String(value ?? '').trim();
}
function parseJsonLike(value, fallback = {}) {
if (value == null) return fallback;
if (typeof value === 'object' && !Array.isArray(value)) return value;
try {
const parsed = JSON.parse(String(value));
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback;
} catch {
return fallback;
}
}
function defaultSecrets() {
return { aliyun_bailian: { apiKey: '' } };
}
export function normalizeImageMakeAdminConfig(input = {}) {
const providers = clone(IMAGE_MAKE_DEFAULT_CONFIG.providers);
const rawProviders = input.providers && typeof input.providers === 'object' ? input.providers : {};
providers.mock.enabled = bool(rawProviders.mock?.enabled, providers.mock.enabled);
providers.aliyun_bailian.enabled = bool(rawProviders.aliyun_bailian?.enabled, providers.aliyun_bailian.enabled);
providers.aliyun_bailian.model = IMAGE_MAKE_ALIYUN_MODELS.includes(rawProviders.aliyun_bailian?.model)
? rawProviders.aliyun_bailian.model
: providers.aliyun_bailian.model;
providers.aliyun_bailian.apiBase = trim(rawProviders.aliyun_bailian?.apiBase || providers.aliyun_bailian.apiBase);
providers.comfyui.enabled = bool(rawProviders.comfyui?.enabled, providers.comfyui.enabled);
providers.comfyui.apiBase = trim(rawProviders.comfyui?.apiBase || providers.comfyui.apiBase);
providers.comfyui.checkpoint = trim(rawProviders.comfyui?.checkpoint);
providers.comfyui.workflowPath = trim(rawProviders.comfyui?.workflowPath || providers.comfyui.workflowPath);
providers.comfyui.outputNodeId = trim(rawProviders.comfyui?.outputNodeId || providers.comfyui.outputNodeId) || '9';
providers.comfyui.timeoutSeconds = clampInt(rawProviders.comfyui?.timeoutSeconds, providers.comfyui.timeoutSeconds, 2, 1800);
providers.comfyui.steps = clampInt(rawProviders.comfyui?.steps, providers.comfyui.steps, 1, 100);
providers.comfyui.cfg = clampFloat(rawProviders.comfyui?.cfg, providers.comfyui.cfg, 0, 30);
providers.comfyui.sampler = trim(rawProviders.comfyui?.sampler || providers.comfyui.sampler) || 'euler';
providers.comfyui.scheduler = trim(rawProviders.comfyui?.scheduler || providers.comfyui.scheduler) || 'normal';
let defaultProvider = trim(input.defaultProvider || IMAGE_MAKE_DEFAULT_CONFIG.defaultProvider);
if (!IMAGE_MAKE_PROVIDER_IDS.includes(defaultProvider)) defaultProvider = 'mock';
if (!providers[defaultProvider]?.enabled) {
defaultProvider = IMAGE_MAKE_PROVIDER_IDS.find((id) => providers[id]?.enabled) ?? 'mock';
}
return {
defaultProvider,
jobDefaultTimeoutSeconds: clampInt(
input.jobDefaultTimeoutSeconds,
IMAGE_MAKE_DEFAULT_CONFIG.jobDefaultTimeoutSeconds,
5,
1800,
),
providers,
};
}
function buildMaskedView(config, dashScopeCredentials) {
const view = clone(config);
const apiKeyMasked = dashScopeCredentials?.apiKeyMasked ?? '';
view.providers.aliyun_bailian.apiKeyConfigured = Boolean(dashScopeCredentials?.apiKey);
view.providers.aliyun_bailian.apiKeyMasked = apiKeyMasked;
view.providers.aliyun_bailian.dashScopeSource = dashScopeCredentials?.source ?? null;
view.providers.aliyun_bailian.dashScopeKeyName = dashScopeCredentials?.keyName ?? null;
return view;
}
async function resolveDashScopeCredentials(llmProviderService, secrets) {
if (llmProviderService?.resolveDashScopeCredentials) {
const resolved = await llmProviderService.resolveDashScopeCredentials();
if (resolved?.apiKey) return resolved;
}
const legacyKey = trim(secrets?.aliyun_bailian?.apiKey);
if (legacyKey) {
return {
apiKey: legacyKey,
keyId: null,
keyName: '历史 image_make 配置',
providerLabel: 'DashScope',
source: 'legacy',
apiKeyMasked: maskApiKey(legacyKey),
};
}
return null;
}
function validateRuntimeConfig(config, dashScopeCredentials) {
const defaultProvider = config.defaultProvider;
if (!config.providers[defaultProvider]?.enabled) {
return { ok: false, message: '默认 Provider 必须处于启用状态' };
}
if (config.providers.aliyun_bailian.enabled) {
if (!trim(config.providers.aliyun_bailian.apiBase)) {
return { ok: false, message: '百炼 Qwen-Image 启用时必须填写 API Base' };
}
if (!trim(dashScopeCredentials?.apiKey)) {
return {
ok: false,
message: '百炼 Qwen-Image 启用时需在「统一模型中心」配置 DashScope Provider(图片任务模型或 Qwen VL Key',
};
}
}
if (config.providers.comfyui.enabled) {
if (!trim(config.providers.comfyui.apiBase)) {
return { ok: false, message: 'ComfyUI 启用时必须填写 API Base' };
}
if (!trim(config.providers.comfyui.checkpoint)) {
return { ok: false, message: 'ComfyUI 启用时必须填写 Checkpoint 文件名' };
}
if (!trim(config.providers.comfyui.workflowPath)) {
return { ok: false, message: 'ComfyUI 启用时必须填写 Workflow 路径' };
}
}
return { ok: true };
}
function runtimeFingerprint(config, dashScopeCredentials) {
return crypto
.createHash('sha256')
.update(JSON.stringify({
config,
dashScopeKeyId: dashScopeCredentials?.keyId ?? null,
dashScopeSource: dashScopeCredentials?.source ?? null,
}))
.digest('hex');
}
async function ensureConfigTable(pool) {
await pool.query(`CREATE TABLE IF NOT EXISTS ${CONFIG_TABLE} (
config_scope VARCHAR(32) PRIMARY KEY,
config_json JSON NOT NULL,
secrets_ciphertext MEDIUMTEXT NULL,
secrets_iv VARCHAR(255) NULL,
secrets_tag VARCHAR(255) NULL,
updated_by CHAR(36) NULL,
updated_at BIGINT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`);
}
export async function ensureImageMakeAdminConfigSchema(pool) {
if (!pool) return;
await ensureConfigTable(pool);
}
export function createImageMakeAdminConfigService(pool, { env = process.env, llmProviderService = null } = {}) {
const serviceToken = trim(env.IMAGE_MAKE_TOKEN);
async function loadStoredState() {
if (!pool) return null;
await ensureConfigTable(pool);
const [rows] = await pool.query(
`SELECT config_json, secrets_ciphertext, secrets_iv, secrets_tag, updated_by, updated_at
FROM ${CONFIG_TABLE}
WHERE config_scope = ?
LIMIT 1`,
[CONFIG_SCOPE],
);
const row = rows?.[0];
if (!row) return null;
let secrets = defaultSecrets();
if (row.secrets_ciphertext && row.secrets_iv && row.secrets_tag) {
secrets = {
...defaultSecrets(),
...parseJsonLike(decryptSecret({
ciphertext: row.secrets_ciphertext,
iv: row.secrets_iv,
tag: row.secrets_tag,
}), {}),
};
}
return {
config: normalizeImageMakeAdminConfig(parseJsonLike(row.config_json, {})),
secrets,
updatedAt: Number(row.updated_at ?? 0) || null,
updatedBy: row.updated_by ?? null,
};
}
async function loadState() {
const stored = await loadStoredState();
if (!stored) {
return {
config: normalizeImageMakeAdminConfig({}),
secrets: defaultSecrets(),
updatedAt: null,
updatedBy: null,
source: 'default',
};
}
return { ...stored, source: 'admin-db' };
}
function mergePatch(currentConfig, currentSecrets, patch = {}) {
const nextConfig = normalizeImageMakeAdminConfig({
...currentConfig,
...patch,
providers: {
...currentConfig.providers,
...(patch.providers ?? {}),
mock: { ...currentConfig.providers.mock, ...(patch.providers?.mock ?? {}) },
aliyun_bailian: {
...currentConfig.providers.aliyun_bailian,
...(patch.providers?.aliyun_bailian ?? {}),
},
comfyui: {
...currentConfig.providers.comfyui,
...(patch.providers?.comfyui ?? {}),
},
},
});
const nextSecrets = clone(currentSecrets);
if (patch?.providers?.aliyun_bailian && 'apiKey' in patch.providers.aliyun_bailian) {
const apiKey = patch.providers.aliyun_bailian.apiKey;
if (apiKey !== undefined) {
nextSecrets.aliyun_bailian.apiKey = trim(apiKey);
}
}
return { nextConfig, nextSecrets };
}
return {
ensureSchema: () => ensureImageMakeAdminConfigSchema(pool),
async getAdminConfig() {
const state = await loadState();
const dashScopeCredentials = await resolveDashScopeCredentials(llmProviderService, state.secrets);
return {
config: buildMaskedView(state.config, dashScopeCredentials),
source: state.source,
updatedAt: state.updatedAt,
updatedBy: state.updatedBy,
};
},
async updateAdminConfig(patch = {}, { updatedBy = null } = {}) {
const state = await loadState();
const { nextConfig, nextSecrets } = mergePatch(state.config, state.secrets, patch);
const dashScopeCredentials = await resolveDashScopeCredentials(llmProviderService, nextSecrets);
const validation = validateRuntimeConfig(nextConfig, dashScopeCredentials);
if (!validation.ok) return validation;
if (!pool) {
return {
ok: true,
config: buildMaskedView(nextConfig, dashScopeCredentials),
source: 'default',
updatedAt: null,
updatedBy: null,
};
}
await ensureConfigTable(pool);
const now = Date.now();
const encryptedSecrets = encryptSecret(JSON.stringify(nextSecrets));
await pool.query(
`INSERT INTO ${CONFIG_TABLE}
(config_scope, config_json, secrets_ciphertext, secrets_iv, secrets_tag, updated_by, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
config_json = VALUES(config_json),
secrets_ciphertext = VALUES(secrets_ciphertext),
secrets_iv = VALUES(secrets_iv),
secrets_tag = VALUES(secrets_tag),
updated_by = VALUES(updated_by),
updated_at = VALUES(updated_at)`,
[
CONFIG_SCOPE,
JSON.stringify(nextConfig),
encryptedSecrets.ciphertext,
encryptedSecrets.iv,
encryptedSecrets.tag,
updatedBy,
now,
],
);
return {
ok: true,
...(await this.getAdminConfig()),
updatedAt: now,
updatedBy,
};
},
async getRuntimeConfig() {
const state = await loadState();
const dashScopeCredentials = await resolveDashScopeCredentials(llmProviderService, state.secrets);
const validation = validateRuntimeConfig(state.config, dashScopeCredentials);
return {
ok: validation.ok,
message: validation.message ?? null,
fingerprint: runtimeFingerprint(state.config, dashScopeCredentials),
source: state.source,
updatedAt: state.updatedAt,
defaultProvider: state.config.defaultProvider,
jobDefaultTimeoutSeconds: state.config.jobDefaultTimeoutSeconds,
providers: {
mock: { enabled: state.config.providers.mock.enabled },
aliyun_bailian: state.config.providers.aliyun_bailian.enabled
? {
enabled: true,
model: state.config.providers.aliyun_bailian.model,
apiBase: state.config.providers.aliyun_bailian.apiBase,
apiKey: trim(dashScopeCredentials?.apiKey),
}
: { enabled: false },
comfyui: state.config.providers.comfyui.enabled
? {
enabled: true,
...state.config.providers.comfyui,
}
: { enabled: false },
},
};
},
authorizeRuntimeRequest(token) {
if (!serviceToken) return false;
return trim(token) === serviceToken;
},
};
}