feat(image): complete reviewed generation delivery
Memind CI / Test, build, and release guards (pull_request) Successful in 2m47s

This commit is contained in:
john
2026-07-20 20:04:44 +08:00
parent fc3d27aae0
commit 4b15610aa8
42 changed files with 1764 additions and 97 deletions
+379
View File
@@ -0,0 +1,379 @@
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;
},
};
}