feat: complete image_make admin controls
This commit is contained in:
@@ -7,6 +7,8 @@
|
|||||||
"dev": "node scripts/dev.mjs",
|
"dev": "node scripts/dev.mjs",
|
||||||
"dev:web": "vite",
|
"dev:web": "vite",
|
||||||
"dev:server": "node server/index.mjs",
|
"dev:server": "node server/index.mjs",
|
||||||
|
"test": "node --test server/*.test.mjs",
|
||||||
|
"test:image-make-admin": "node --test server/app.image-make.test.mjs",
|
||||||
"admin:init": "node scripts/init-admin.mjs",
|
"admin:init": "node scripts/init-admin.mjs",
|
||||||
"release:prod": "bash scripts/release-prod.sh",
|
"release:prod": "bash scripts/release-prod.sh",
|
||||||
"local_restart": "bash scripts/local_restart.sh",
|
"local_restart": "bash scripts/local_restart.sh",
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createAdminApp } from './app.mjs';
|
||||||
|
|
||||||
|
function createServices({ role = 'admin', imageMakeAdminConfigService } = {}) {
|
||||||
|
return {
|
||||||
|
ready: Promise.resolve(),
|
||||||
|
parseCookies: () => ({ adm_session: 'test-token' }),
|
||||||
|
USER_COOKIE: 'adm_session',
|
||||||
|
userLoginCookies: () => ['adm_session=test-token; Path=/; HttpOnly'],
|
||||||
|
clearUserSessionCookie: () => ['adm_session=; Path=/; Max-Age=0'],
|
||||||
|
resolveCookieDomainForRequest: () => null,
|
||||||
|
userAuth: {
|
||||||
|
getMe: async () => ({ id: 'admin-user-id', username: 'admin', role }),
|
||||||
|
},
|
||||||
|
imageMakeAdminConfigService,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withServer(services, callback) {
|
||||||
|
const app = createAdminApp(services);
|
||||||
|
const server = await new Promise((resolve) => {
|
||||||
|
const instance = app.listen(0, '127.0.0.1', () => resolve(instance));
|
||||||
|
});
|
||||||
|
const address = server.address();
|
||||||
|
const baseUrl = `http://127.0.0.1:${address.port}`;
|
||||||
|
try {
|
||||||
|
await callback(baseUrl);
|
||||||
|
} finally {
|
||||||
|
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminHeaders = {
|
||||||
|
cookie: 'adm_session=test-token',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('image_make admin routes read and update config with the admin identity', async () => {
|
||||||
|
let updateCall = null;
|
||||||
|
const service = {
|
||||||
|
getAdminConfig: async () => ({
|
||||||
|
source: 'default',
|
||||||
|
config: {
|
||||||
|
defaultProvider: 'mock',
|
||||||
|
jobDefaultTimeoutSeconds: 120,
|
||||||
|
providers: { mock: { enabled: true }, aliyun_bailian: { enabled: false }, comfyui: { enabled: false } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
updateAdminConfig: async (patch, context) => {
|
||||||
|
updateCall = { patch, context };
|
||||||
|
return { ok: true, source: 'admin-db', config: patch };
|
||||||
|
},
|
||||||
|
getRuntimeConfig: async () => ({
|
||||||
|
ok: true,
|
||||||
|
fingerprint: '1234567890abcdef',
|
||||||
|
defaultProvider: 'aliyun_bailian',
|
||||||
|
jobDefaultTimeoutSeconds: 120,
|
||||||
|
providers: {
|
||||||
|
mock: { enabled: true },
|
||||||
|
aliyun_bailian: {
|
||||||
|
enabled: true,
|
||||||
|
model: 'qwen-image-plus',
|
||||||
|
apiBase: 'https://dashscope.aliyuncs.com/api/v1',
|
||||||
|
apiKey: 'must-not-leak',
|
||||||
|
},
|
||||||
|
comfyui: { enabled: false },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await withServer(createServices({ imageMakeAdminConfigService: service }), async (baseUrl) => {
|
||||||
|
const configResponse = await fetch(`${baseUrl}/admin-api/image-make/config`, { headers: adminHeaders });
|
||||||
|
assert.equal(configResponse.status, 200);
|
||||||
|
assert.equal((await configResponse.json()).config.defaultProvider, 'mock');
|
||||||
|
|
||||||
|
const patch = { defaultProvider: 'mock', jobDefaultTimeoutSeconds: 180 };
|
||||||
|
const updateResponse = await fetch(`${baseUrl}/admin-api/image-make/config`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: adminHeaders,
|
||||||
|
body: JSON.stringify(patch),
|
||||||
|
});
|
||||||
|
assert.equal(updateResponse.status, 200);
|
||||||
|
assert.deepEqual(updateCall, { patch, context: { updatedBy: 'admin-user-id' } });
|
||||||
|
|
||||||
|
const runtimeResponse = await fetch(`${baseUrl}/admin-api/image-make/runtime`, { headers: adminHeaders });
|
||||||
|
assert.equal(runtimeResponse.status, 200);
|
||||||
|
const runtime = await runtimeResponse.json();
|
||||||
|
assert.equal(runtime.providers.aliyun_bailian.apiKeyConfigured, true);
|
||||||
|
assert.equal('apiKey' in runtime.providers.aliyun_bailian, false);
|
||||||
|
assert.equal(JSON.stringify(runtime).includes('must-not-leak'), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('image_make admin config validation failures are returned as 400', async () => {
|
||||||
|
const service = {
|
||||||
|
updateAdminConfig: async () => ({ ok: false, message: '默认 Provider 必须处于启用状态' }),
|
||||||
|
};
|
||||||
|
await withServer(createServices({ imageMakeAdminConfigService: service }), async (baseUrl) => {
|
||||||
|
const response = await fetch(`${baseUrl}/admin-api/image-make/config`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: adminHeaders,
|
||||||
|
body: JSON.stringify({ defaultProvider: 'comfyui' }),
|
||||||
|
});
|
||||||
|
assert.equal(response.status, 400);
|
||||||
|
assert.equal((await response.json()).message, '默认 Provider 必须处于启用状态');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('image_make admin routes require an admin user and an enabled service', async () => {
|
||||||
|
await withServer(createServices({ role: 'user', imageMakeAdminConfigService: {} }), async (baseUrl) => {
|
||||||
|
const forbidden = await fetch(`${baseUrl}/admin-api/image-make/config`, { headers: adminHeaders });
|
||||||
|
assert.equal(forbidden.status, 403);
|
||||||
|
});
|
||||||
|
|
||||||
|
await withServer(createServices({ imageMakeAdminConfigService: {} }), async (baseUrl) => {
|
||||||
|
const unavailable = await fetch(`${baseUrl}/admin-api/image-make/config`, { headers: adminHeaders });
|
||||||
|
assert.equal(unavailable.status, 503);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -93,6 +93,7 @@ export function createAdminApp(services) {
|
|||||||
userAuth,
|
userAuth,
|
||||||
llmProviderService,
|
llmProviderService,
|
||||||
assetGatewayConfigService,
|
assetGatewayConfigService,
|
||||||
|
imageMakeAdminConfigService,
|
||||||
pool,
|
pool,
|
||||||
ready,
|
ready,
|
||||||
wechatAdmin,
|
wechatAdmin,
|
||||||
@@ -369,6 +370,53 @@ export function createAdminApp(services) {
|
|||||||
res.json(result);
|
res.json(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
adminApi.get('/image-make/config', requireAdmin, async (_req, res) => {
|
||||||
|
if (!imageMakeAdminConfigService?.getAdminConfig) {
|
||||||
|
return res.status(503).json({ message: 'image_make 配置服务未启用' });
|
||||||
|
}
|
||||||
|
res.json(await imageMakeAdminConfigService.getAdminConfig());
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateImageMakeConfig = async (req, res) => {
|
||||||
|
if (!imageMakeAdminConfigService?.updateAdminConfig) {
|
||||||
|
return res.status(503).json({ message: 'image_make 配置服务未启用' });
|
||||||
|
}
|
||||||
|
const result = await imageMakeAdminConfigService.updateAdminConfig(req.body ?? {}, {
|
||||||
|
updatedBy: req.currentUser.id,
|
||||||
|
});
|
||||||
|
if (result.ok === false) return res.status(400).json({ message: result.message });
|
||||||
|
res.json(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
adminApi.put('/image-make/config', requireAdmin, updateImageMakeConfig);
|
||||||
|
adminApi.patch('/image-make/config', requireAdmin, updateImageMakeConfig);
|
||||||
|
|
||||||
|
adminApi.get('/image-make/runtime', requireAdmin, async (_req, res) => {
|
||||||
|
if (!imageMakeAdminConfigService?.getRuntimeConfig) {
|
||||||
|
return res.status(503).json({ message: 'image_make 配置服务未启用' });
|
||||||
|
}
|
||||||
|
const runtime = await imageMakeAdminConfigService.getRuntimeConfig();
|
||||||
|
if (!runtime.ok) return res.status(503).json({ message: runtime.message ?? 'image_make 运行时配置无效' });
|
||||||
|
const { providers, ...rest } = runtime;
|
||||||
|
res.json({
|
||||||
|
...rest,
|
||||||
|
providers: {
|
||||||
|
mock: { enabled: providers.mock.enabled },
|
||||||
|
aliyun_bailian: providers.aliyun_bailian.enabled
|
||||||
|
? {
|
||||||
|
enabled: true,
|
||||||
|
model: providers.aliyun_bailian.model,
|
||||||
|
apiBase: providers.aliyun_bailian.apiBase,
|
||||||
|
apiKeyConfigured: Boolean(providers.aliyun_bailian.apiKey),
|
||||||
|
}
|
||||||
|
: { enabled: false },
|
||||||
|
comfyui: providers.comfyui.enabled
|
||||||
|
? { enabled: true, ...providers.comfyui }
|
||||||
|
: { enabled: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
adminApi.get('/memory-v2/config', requireAdmin, async (_req, res) => {
|
adminApi.get('/memory-v2/config', requireAdmin, async (_req, res) => {
|
||||||
if (!memoryV2ConfigService) return res.status(503).json({ message: 'Memory V2 配置未启用' });
|
if (!memoryV2ConfigService) return res.status(503).json({ message: 'Memory V2 配置未启用' });
|
||||||
res.json(await memoryV2ConfigService.getAdminConfig());
|
res.json(await memoryV2ConfigService.getAdminConfig());
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export async function bootstrapAdminServices() {
|
|||||||
const { createPersonalMemoryCandidateStore } = await importMemind('memory-v2-personal-store.mjs');
|
const { createPersonalMemoryCandidateStore } = await importMemind('memory-v2-personal-store.mjs');
|
||||||
const { createSkillRuntimeAdminConfigService } = await importMemind('skill-runtime-admin-config.mjs');
|
const { createSkillRuntimeAdminConfigService } = await importMemind('skill-runtime-admin-config.mjs');
|
||||||
const { createAssetGatewayConfigService } = await importMemind('asset-gateway.mjs');
|
const { createAssetGatewayConfigService } = await importMemind('asset-gateway.mjs');
|
||||||
|
const { createImageMakeAdminConfigService } = await importMemind('image-make-admin-config.mjs');
|
||||||
const { ensureAssetGatewaySchema } = await importMemind('db.mjs');
|
const { ensureAssetGatewaySchema } = await importMemind('db.mjs');
|
||||||
const { createWechatScheduleLlmConfigService } = await importMemind('wechat-schedule-llm-config.mjs');
|
const { createWechatScheduleLlmConfigService } = await importMemind('wechat-schedule-llm-config.mjs');
|
||||||
const { createAdminSystemTestService } = await importMemind('admin-system-tests.mjs');
|
const { createAdminSystemTestService } = await importMemind('admin-system-tests.mjs');
|
||||||
@@ -93,6 +94,11 @@ export async function bootstrapAdminServices() {
|
|||||||
});
|
});
|
||||||
await ensureAssetGatewaySchema(pool);
|
await ensureAssetGatewaySchema(pool);
|
||||||
const assetGatewayConfigService = createAssetGatewayConfigService(pool, { llmProviderService });
|
const assetGatewayConfigService = createAssetGatewayConfigService(pool, { llmProviderService });
|
||||||
|
const imageMakeAdminConfigService = createImageMakeAdminConfigService(pool, {
|
||||||
|
env: process.env,
|
||||||
|
llmProviderService,
|
||||||
|
});
|
||||||
|
await imageMakeAdminConfigService.ensureSchema();
|
||||||
const memoryV2ConfigService = createMemoryV2AdminConfigService(pool, {
|
const memoryV2ConfigService = createMemoryV2AdminConfigService(pool, {
|
||||||
env: process.env,
|
env: process.env,
|
||||||
});
|
});
|
||||||
@@ -148,6 +154,7 @@ export async function bootstrapAdminServices() {
|
|||||||
userAuth,
|
userAuth,
|
||||||
llmProviderService,
|
llmProviderService,
|
||||||
assetGatewayConfigService,
|
assetGatewayConfigService,
|
||||||
|
imageMakeAdminConfigService,
|
||||||
plazaOps,
|
plazaOps,
|
||||||
createOpsApi,
|
createOpsApi,
|
||||||
wechatAdmin,
|
wechatAdmin,
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ ready
|
|||||||
userAuth,
|
userAuth,
|
||||||
llmProviderService,
|
llmProviderService,
|
||||||
assetGatewayConfigService,
|
assetGatewayConfigService,
|
||||||
|
imageMakeAdminConfigService,
|
||||||
plazaOps,
|
plazaOps,
|
||||||
createOpsApi,
|
createOpsApi,
|
||||||
wechatAdmin,
|
wechatAdmin,
|
||||||
@@ -125,6 +126,7 @@ ready
|
|||||||
userAuth,
|
userAuth,
|
||||||
llmProviderService,
|
llmProviderService,
|
||||||
assetGatewayConfigService,
|
assetGatewayConfigService,
|
||||||
|
imageMakeAdminConfigService,
|
||||||
plazaOps,
|
plazaOps,
|
||||||
createOpsApi,
|
createOpsApi,
|
||||||
wechatAdmin,
|
wechatAdmin,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
getAssetGatewayConfig,
|
getAssetGatewayConfig,
|
||||||
listLlmProviderKeys,
|
getImageMakeAdminConfig,
|
||||||
|
getImageMakeRuntimeStatus,
|
||||||
|
getLlmVisionSettings,
|
||||||
updateAssetGatewayConfig,
|
updateAssetGatewayConfig,
|
||||||
updateAssetPluginConfig,
|
updateAssetPluginConfig,
|
||||||
|
updateImageMakeAdminConfig,
|
||||||
} from '../../api/client';
|
} from '../../api/client';
|
||||||
import type { AssetGatewayConfig, AssetPluginConfig, LlmProviderKeyRow } from '../../types';
|
import type { AssetGatewayConfig, AssetPluginConfig, ImageMakeAdminConfig, LlmVisionSettings } from '../../types';
|
||||||
|
|
||||||
type PluginDraft = {
|
type PluginDraft = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -32,15 +36,44 @@ const pluginVisuals: Record<string, { icon: string; accent: string }> = {
|
|||||||
'asset-analyze': { icon: '◉', accent: 'green' },
|
'asset-analyze': { icon: '◉', accent: 'green' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultImageMakeConfig: ImageMakeAdminConfig = {
|
||||||
|
defaultProvider: 'mock',
|
||||||
|
jobDefaultTimeoutSeconds: 120,
|
||||||
|
providers: {
|
||||||
|
mock: { enabled: true },
|
||||||
|
aliyun_bailian: {
|
||||||
|
enabled: false,
|
||||||
|
model: 'qwen-image-plus',
|
||||||
|
apiBase: 'https://dashscope.aliyuncs.com/api/v1',
|
||||||
|
apiKeyConfigured: false,
|
||||||
|
apiKeyMasked: '',
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function AssetGatewayPage() {
|
export function AssetGatewayPage() {
|
||||||
const [config, setConfig] = useState<AssetGatewayConfig | null>(null);
|
const [config, setConfig] = useState<AssetGatewayConfig | null>(null);
|
||||||
const [keys, setKeys] = useState<LlmProviderKeyRow[]>([]);
|
|
||||||
const [drafts, setDrafts] = useState<Record<string, PluginDraft>>({});
|
const [drafts, setDrafts] = useState<Record<string, PluginDraft>>({});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [busy, setBusy] = useState<string | null>(null);
|
const [busy, setBusy] = useState<string | null>(null);
|
||||||
const [message, setMessage] = useState<string | null>(null);
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const activeKeys = useMemo(() => keys.filter((key) => key.status === 'active'), [keys]);
|
const [imageMakeConfig, setImageMakeConfig] = useState<ImageMakeAdminConfig>(defaultImageMakeConfig);
|
||||||
|
const [imageMakeMeta, setImageMakeMeta] = useState<{ source?: string; updatedAt?: number | null; updatedBy?: string | null }>({});
|
||||||
|
const [imageMakeRuntime, setImageMakeRuntime] = useState<string>('');
|
||||||
|
const [visionSettings, setVisionSettings] = useState<LlmVisionSettings | null>(null);
|
||||||
const orderedPlugins = useMemo(() => config?.plugins.slice().sort((left, right) => {
|
const orderedPlugins = useMemo(() => config?.plugins.slice().sort((left, right) => {
|
||||||
if (left.pluginId === 'asset-generate') return -1;
|
if (left.pluginId === 'asset-generate') return -1;
|
||||||
if (right.pluginId === 'asset-generate') return 1;
|
if (right.pluginId === 'asset-generate') return 1;
|
||||||
@@ -50,15 +83,44 @@ export function AssetGatewayPage() {
|
|||||||
const imageGenerateDraft = imageGeneratePlugin
|
const imageGenerateDraft = imageGeneratePlugin
|
||||||
? drafts[imageGeneratePlugin.pluginId] ?? draftFromPlugin(imageGeneratePlugin)
|
? drafts[imageGeneratePlugin.pluginId] ?? draftFromPlugin(imageGeneratePlugin)
|
||||||
: null;
|
: null;
|
||||||
|
const imageMakeControlsEnabled = Boolean(
|
||||||
|
imageGenerateDraft?.enabled && imageGenerateDraft.provider === 'image_make',
|
||||||
|
);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const [nextConfig, nextKeys] = await Promise.all([getAssetGatewayConfig(), listLlmProviderKeys()]);
|
const [nextConfig, nextVisionSettings, nextImageMakeConfig, nextImageMakeRuntime] = await Promise.all([
|
||||||
|
getAssetGatewayConfig(),
|
||||||
|
getLlmVisionSettings().catch(() => null),
|
||||||
|
getImageMakeAdminConfig(),
|
||||||
|
getImageMakeRuntimeStatus().catch(() => null),
|
||||||
|
]);
|
||||||
setConfig(nextConfig);
|
setConfig(nextConfig);
|
||||||
setKeys(nextKeys);
|
setVisionSettings(nextVisionSettings);
|
||||||
setDrafts(Object.fromEntries(nextConfig.plugins.map((plugin) => [plugin.pluginId, draftFromPlugin(plugin)])));
|
setDrafts(Object.fromEntries(nextConfig.plugins.map((plugin) => [plugin.pluginId, draftFromPlugin(plugin)])));
|
||||||
|
setImageMakeConfig({
|
||||||
|
...defaultImageMakeConfig,
|
||||||
|
...nextImageMakeConfig.config,
|
||||||
|
providers: {
|
||||||
|
...defaultImageMakeConfig.providers,
|
||||||
|
...nextImageMakeConfig.config.providers,
|
||||||
|
mock: { ...defaultImageMakeConfig.providers.mock, ...nextImageMakeConfig.config.providers.mock },
|
||||||
|
aliyun_bailian: {
|
||||||
|
...defaultImageMakeConfig.providers.aliyun_bailian,
|
||||||
|
...nextImageMakeConfig.config.providers.aliyun_bailian,
|
||||||
|
},
|
||||||
|
comfyui: {
|
||||||
|
...defaultImageMakeConfig.providers.comfyui,
|
||||||
|
...nextImageMakeConfig.config.providers.comfyui,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setImageMakeMeta(nextImageMakeConfig);
|
||||||
|
setImageMakeRuntime(nextImageMakeRuntime?.ok
|
||||||
|
? `默认 Provider:${nextImageMakeRuntime.defaultProvider} · 指纹 ${nextImageMakeRuntime.fingerprint?.slice(0, 12) ?? '—'}`
|
||||||
|
: nextImageMakeRuntime?.message ?? '运行时未就绪');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : '加载资产能力配置失败');
|
setError(err instanceof Error ? err.message : '加载资产能力配置失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -100,8 +162,6 @@ export function AssetGatewayPage() {
|
|||||||
const result = await updateAssetPluginConfig(plugin.pluginId, {
|
const result = await updateAssetPluginConfig(plugin.pluginId, {
|
||||||
enabled: draft.enabled,
|
enabled: draft.enabled,
|
||||||
provider: draft.provider || null,
|
provider: draft.provider || null,
|
||||||
llmProviderKeyId: draft.llmProviderKeyId || null,
|
|
||||||
llmModel: draft.llmModel || null,
|
|
||||||
purposes: draft.purposes,
|
purposes: draft.purposes,
|
||||||
});
|
});
|
||||||
setConfig(result.config);
|
setConfig(result.config);
|
||||||
@@ -111,6 +171,64 @@ export function AssetGatewayPage() {
|
|||||||
finally { setBusy(null); }
|
finally { setBusy(null); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveImageMakeConfig = async () => {
|
||||||
|
setBusy('image-make-config'); setMessage(null); setError(null);
|
||||||
|
try {
|
||||||
|
const payload: Parameters<typeof updateImageMakeAdminConfig>[0] = {
|
||||||
|
defaultProvider: imageMakeConfig.defaultProvider,
|
||||||
|
jobDefaultTimeoutSeconds: imageMakeConfig.jobDefaultTimeoutSeconds,
|
||||||
|
providers: {
|
||||||
|
mock: { enabled: imageMakeConfig.providers.mock.enabled },
|
||||||
|
aliyun_bailian: {
|
||||||
|
enabled: imageMakeConfig.providers.aliyun_bailian.enabled,
|
||||||
|
model: imageMakeConfig.providers.aliyun_bailian.model,
|
||||||
|
apiBase: imageMakeConfig.providers.aliyun_bailian.apiBase,
|
||||||
|
},
|
||||||
|
comfyui: {
|
||||||
|
enabled: imageMakeConfig.providers.comfyui.enabled,
|
||||||
|
apiBase: imageMakeConfig.providers.comfyui.apiBase,
|
||||||
|
checkpoint: imageMakeConfig.providers.comfyui.checkpoint,
|
||||||
|
workflowPath: imageMakeConfig.providers.comfyui.workflowPath,
|
||||||
|
outputNodeId: imageMakeConfig.providers.comfyui.outputNodeId,
|
||||||
|
timeoutSeconds: imageMakeConfig.providers.comfyui.timeoutSeconds,
|
||||||
|
steps: imageMakeConfig.providers.comfyui.steps,
|
||||||
|
cfg: imageMakeConfig.providers.comfyui.cfg,
|
||||||
|
sampler: imageMakeConfig.providers.comfyui.sampler,
|
||||||
|
scheduler: imageMakeConfig.providers.comfyui.scheduler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = await updateImageMakeAdminConfig(payload);
|
||||||
|
if (result.ok === false) throw new Error(result.message ?? '保存 image_make 配置失败');
|
||||||
|
setImageMakeConfig({
|
||||||
|
...defaultImageMakeConfig,
|
||||||
|
...result.config,
|
||||||
|
providers: {
|
||||||
|
...defaultImageMakeConfig.providers,
|
||||||
|
...result.config.providers,
|
||||||
|
mock: { ...defaultImageMakeConfig.providers.mock, ...result.config.providers.mock },
|
||||||
|
aliyun_bailian: {
|
||||||
|
...defaultImageMakeConfig.providers.aliyun_bailian,
|
||||||
|
...result.config.providers.aliyun_bailian,
|
||||||
|
},
|
||||||
|
comfyui: {
|
||||||
|
...defaultImageMakeConfig.providers.comfyui,
|
||||||
|
...result.config.providers.comfyui,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setImageMakeMeta(result);
|
||||||
|
const nextVisionSettings = await getLlmVisionSettings().catch(() => null);
|
||||||
|
if (nextVisionSettings) setVisionSettings(nextVisionSettings);
|
||||||
|
const runtime = await getImageMakeRuntimeStatus().catch(() => null);
|
||||||
|
setImageMakeRuntime(runtime?.ok
|
||||||
|
? `默认 Provider:${runtime.defaultProvider} · 指纹 ${runtime.fingerprint?.slice(0, 12) ?? '—'}`
|
||||||
|
: runtime?.message ?? '运行时未就绪');
|
||||||
|
setMessage('image_make 生图模型配置已保存;服务会在数秒内自动拉取新配置。');
|
||||||
|
} catch (err) { setError(err instanceof Error ? err.message : '保存 image_make 配置失败'); }
|
||||||
|
finally { setBusy(null); }
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) return <div className="admin-page"><p className="muted">加载资产能力配置…</p></div>;
|
if (loading) return <div className="admin-page"><p className="muted">加载资产能力配置…</p></div>;
|
||||||
if (!config) return <div className="admin-page"><p className="banner banner-error">{error ?? '无法加载配置'}</p></div>;
|
if (!config) return <div className="admin-page"><p className="banner banner-error">{error ?? '无法加载配置'}</p></div>;
|
||||||
|
|
||||||
@@ -134,89 +252,451 @@ export function AssetGatewayPage() {
|
|||||||
</label>
|
</label>
|
||||||
</section>
|
</section>
|
||||||
{imageGeneratePlugin && imageGenerateDraft ? (
|
{imageGeneratePlugin && imageGenerateDraft ? (
|
||||||
<section className="image-generation-settings">
|
<>
|
||||||
<div className="image-generation-settings-head">
|
<div className="model-center-section-title">
|
||||||
<div>
|
<h3>图片生成范围</h3>
|
||||||
<div className="asset-kicker">IMAGE GENERATION SCOPE</div>
|
<p>控制 Memind 是否调用 image_make,以及各输出位置是否启用 AI 图片。</p>
|
||||||
<h3>图片生成范围</h3>
|
|
||||||
<p>勾选才会在对应位置使用 AI 图片;卡片和缩略图默认复用页面头图,不增加 ComfyUI 任务。</p>
|
|
||||||
</div>
|
|
||||||
<label className="asset-mini-toggle" title="启用图片生成">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={imageGenerateDraft.enabled}
|
|
||||||
onChange={(event) => updateDraft(imageGeneratePlugin.pluginId, { enabled: event.target.checked })}
|
|
||||||
/>
|
|
||||||
<span>{imageGenerateDraft.enabled ? '图片生成已开启' : '图片生成已关闭'}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<label className="image-generation-provider">
|
<div className="asset-plugin-grid asset-plugin-grid--balanced">
|
||||||
运行 Provider
|
<section className="asset-plugin-card asset-plugin-card--violet">
|
||||||
<select
|
<div className="asset-plugin-card-head">
|
||||||
value={imageGenerateDraft.provider}
|
<div className="asset-plugin-icon" aria-hidden="true">✦</div>
|
||||||
disabled={!imageGenerateDraft.enabled}
|
<div>
|
||||||
onChange={(event) => {
|
<h3>{imageGeneratePlugin.label}</h3>
|
||||||
const provider = event.target.value;
|
<p>勾选才会在对应位置使用 AI 图片;卡片和缩略图默认复用页面头图。</p>
|
||||||
updateDraft(imageGeneratePlugin.pluginId, {
|
</div>
|
||||||
provider,
|
<label className="asset-mini-toggle" title="启用图片生成">
|
||||||
...(provider === 'image_make' ? { llmProviderKeyId: '', llmModel: '' } : {}),
|
<input
|
||||||
});
|
type="checkbox"
|
||||||
}}
|
checked={imageGenerateDraft.enabled}
|
||||||
>
|
onChange={(event) => updateDraft(imageGeneratePlugin.pluginId, { enabled: event.target.checked })}
|
||||||
<option value="">选择 Provider</option>
|
/>
|
||||||
{imageGeneratePlugin.providers.map((provider) => <option key={provider} value={provider}>{provider}</option>)}
|
<span>{imageGenerateDraft.enabled ? '已开启' : '已关闭'}</span>
|
||||||
</select>
|
</label>
|
||||||
</label>
|
</div>
|
||||||
<fieldset
|
<div className="asset-plugin-fields">
|
||||||
className="image-generation-scope-fieldset"
|
<label>
|
||||||
disabled={!imageGenerateDraft.enabled || imageGenerateDraft.provider !== 'image_make'}
|
运行 Provider
|
||||||
>
|
<select
|
||||||
<legend>按输出位置独立控制</legend>
|
value={imageGenerateDraft.provider}
|
||||||
<div className="image-generation-scope-grid">
|
disabled={!imageGenerateDraft.enabled}
|
||||||
{imageGeneratePlugin.purposeCatalog?.map((purpose) => {
|
onChange={(event) => {
|
||||||
const descriptions: Record<string, string> = {
|
const provider = event.target.value;
|
||||||
inline_image: '正文确有需要时单独生成;关闭可显著减少等待时间',
|
updateDraft(imageGeneratePlugin.pluginId, {
|
||||||
hero: '页面主视觉,最多调用一次 ComfyUI',
|
provider,
|
||||||
card_cover: '从页面头图裁剪,不额外调用 ComfyUI',
|
...(provider === 'image_make' ? { llmProviderKeyId: '', llmModel: '' } : {}),
|
||||||
feed_cover: '从页面头图裁剪,不额外调用 ComfyUI',
|
});
|
||||||
};
|
}}
|
||||||
return (
|
>
|
||||||
<label className="image-generation-scope-item" key={purpose.id}>
|
<option value="">选择 Provider</option>
|
||||||
|
{imageGeneratePlugin.providers.map((provider) => (
|
||||||
|
<option key={provider} value={provider}>{provider}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<fieldset
|
||||||
|
className="asset-purpose-fieldset"
|
||||||
|
disabled={!imageGenerateDraft.enabled || imageGenerateDraft.provider !== 'image_make'}
|
||||||
|
>
|
||||||
|
<legend>按输出位置独立控制</legend>
|
||||||
|
<div className="asset-purpose-grid">
|
||||||
|
{imageGeneratePlugin.purposeCatalog?.map((purpose) => {
|
||||||
|
const descriptions: Record<string, string> = {
|
||||||
|
inline_image: '正文确有需要时单独生成',
|
||||||
|
hero: '页面主视觉,最多生成一次',
|
||||||
|
card_cover: '从页面头图派生',
|
||||||
|
feed_cover: '从页面头图派生',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<label key={purpose.id}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(imageGenerateDraft.purposes[purpose.id])}
|
||||||
|
onChange={(event) => updatePurpose(
|
||||||
|
imageGeneratePlugin.pluginId,
|
||||||
|
purpose.id,
|
||||||
|
event.target.checked,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{purpose.label}
|
||||||
|
<small>{descriptions[purpose.id] ?? purpose.presetId}</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div className="asset-plugin-footer">
|
||||||
|
<span className={`asset-status ${imageGenerateDraft.enabled ? 'is-on' : ''}`}>
|
||||||
|
{imageGenerateDraft.enabled ? '待保存为启用' : '默认安全关闭'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="send-btn"
|
||||||
|
disabled={busy === imageGeneratePlugin.pluginId}
|
||||||
|
onClick={() => void savePlugin(imageGeneratePlugin)}
|
||||||
|
>
|
||||||
|
{busy === imageGeneratePlugin.pluginId ? '保存中…' : '保存图片生成配置'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="model-center-section-title">
|
||||||
|
<h3>生图模型与 Provider</h3>
|
||||||
|
<p>image_make 生图后端参数;DashScope Key 与 Qwen VL 审图共用,在「统一模型中心 → 图片任务模型」配置。</p>
|
||||||
|
</div>
|
||||||
|
<div className="asset-plugin-grid">
|
||||||
|
<section className={`asset-plugin-card asset-plugin-card--blue${imageMakeControlsEnabled ? '' : ' is-disabled'}`}>
|
||||||
|
<div className="asset-plugin-card-head">
|
||||||
|
<div className="asset-plugin-icon asset-plugin-icon--blue" aria-hidden="true">⟡</div>
|
||||||
|
<div>
|
||||||
|
<h3>运行时调度</h3>
|
||||||
|
<p>默认 Provider 与任务超时;image_make 按此处配置选择生图后端。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="asset-plugin-fields">
|
||||||
|
{!imageMakeControlsEnabled ? (
|
||||||
|
<div className="image-make-runtime-note">
|
||||||
|
请先在左侧卡片启用图片生成,并将运行 Provider 设为 image_make。
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<label>
|
||||||
|
默认生图 Provider
|
||||||
|
<select
|
||||||
|
value={imageMakeConfig.defaultProvider}
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
defaultProvider: event.target.value as ImageMakeAdminConfig['defaultProvider'],
|
||||||
|
}))}
|
||||||
|
>
|
||||||
|
<option value="aliyun_bailian">百炼 Qwen-Image</option>
|
||||||
|
<option value="comfyui">本地 ComfyUI</option>
|
||||||
|
<option value="mock">Mock(仅开发)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
任务超时(秒)
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={5}
|
||||||
|
max={1800}
|
||||||
|
value={imageMakeConfig.jobDefaultTimeoutSeconds}
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
jobDefaultTimeoutSeconds: Number(event.target.value),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="image-make-runtime-note">
|
||||||
|
{imageMakeRuntime || '运行时状态未知'}
|
||||||
|
{' · '}
|
||||||
|
配置来源:{imageMakeMeta.source === 'admin-db' ? '数据库' : '默认值'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="asset-plugin-footer">
|
||||||
|
<span className={`asset-status ${imageMakeControlsEnabled ? 'is-on' : ''}`}>
|
||||||
|
{imageMakeConfig.defaultProvider}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="send-btn"
|
||||||
|
disabled={busy === 'image-make-config' || !imageMakeControlsEnabled}
|
||||||
|
onClick={() => void saveImageMakeConfig()}
|
||||||
|
>
|
||||||
|
{busy === 'image-make-config' ? '保存中…' : '保存生图模型配置'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={`asset-plugin-card asset-plugin-card--amber${imageMakeControlsEnabled ? '' : ' is-disabled'}`}>
|
||||||
|
<div className="asset-plugin-card-head">
|
||||||
|
<div className="asset-plugin-icon asset-plugin-icon--amber" aria-hidden="true">云</div>
|
||||||
|
<div>
|
||||||
|
<h3>百炼 Qwen-Image</h3>
|
||||||
|
<p>阿里云 DashScope 文生图;与 Qwen VL 审图共用同一 DashScope Key,模型名不同。</p>
|
||||||
|
</div>
|
||||||
|
<label className="asset-mini-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={imageMakeConfig.providers.aliyun_bailian.enabled}
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
providers: {
|
||||||
|
...current.providers,
|
||||||
|
aliyun_bailian: { ...current.providers.aliyun_bailian, enabled: event.target.checked },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<span>{imageMakeConfig.providers.aliyun_bailian.enabled ? '已启用' : '未启用'}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="asset-plugin-fields">
|
||||||
|
<label>
|
||||||
|
模型
|
||||||
|
<select
|
||||||
|
value={imageMakeConfig.providers.aliyun_bailian.model ?? 'qwen-image-plus'}
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
providers: {
|
||||||
|
...current.providers,
|
||||||
|
aliyun_bailian: { ...current.providers.aliyun_bailian, model: event.target.value },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
>
|
||||||
|
<option value="qwen-image-plus">qwen-image-plus</option>
|
||||||
|
<option value="qwen-image">qwen-image</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
API Base
|
||||||
|
<input
|
||||||
|
value={imageMakeConfig.providers.aliyun_bailian.apiBase ?? ''}
|
||||||
|
placeholder="https://dashscope.aliyuncs.com/api/v1"
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
providers: {
|
||||||
|
...current.providers,
|
||||||
|
aliyun_bailian: { ...current.providers.aliyun_bailian, apiBase: event.target.value },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="image-make-runtime-note">
|
||||||
|
DashScope API Key 与 Qwen VL 审图共用,请在
|
||||||
|
{' '}
|
||||||
|
<Link to="/providers">统一模型中心</Link>
|
||||||
|
{' '}
|
||||||
|
的「图片任务模型」配置 Qwen Provider。
|
||||||
|
{imageMakeConfig.providers.aliyun_bailian.apiKeyConfigured ? (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
当前已绑定:
|
||||||
|
{imageMakeConfig.providers.aliyun_bailian.dashScopeKeyName
|
||||||
|
?? visionSettings?.keyName
|
||||||
|
?? 'DashScope'}
|
||||||
|
{' '}
|
||||||
|
(
|
||||||
|
{imageMakeConfig.providers.aliyun_bailian.apiKeyMasked
|
||||||
|
?? visionSettings?.apiKeyMasked
|
||||||
|
?? '已配置'}
|
||||||
|
)
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<> 当前未检测到 DashScope Key。</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="asset-plugin-footer">
|
||||||
|
<span className={`asset-status ${imageMakeConfig.providers.aliyun_bailian.enabled ? 'is-on' : ''}`}>
|
||||||
|
{imageMakeConfig.providers.aliyun_bailian.model ?? 'qwen-image-plus'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="send-btn"
|
||||||
|
disabled={busy === 'image-make-config' || !imageMakeControlsEnabled}
|
||||||
|
onClick={() => void saveImageMakeConfig()}
|
||||||
|
>
|
||||||
|
{busy === 'image-make-config' ? '保存中…' : '保存配置'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={`asset-plugin-card asset-plugin-card--green${imageMakeControlsEnabled ? '' : ' is-disabled'}`}>
|
||||||
|
<div className="asset-plugin-card-head">
|
||||||
|
<div className="asset-plugin-icon asset-plugin-icon--green" aria-hidden="true">⌘</div>
|
||||||
|
<div>
|
||||||
|
<h3>本地 ComfyUI</h3>
|
||||||
|
<p>本地 SD / Flux 工作流;适合开发调试与离线降级。</p>
|
||||||
|
</div>
|
||||||
|
<label className="asset-mini-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={imageMakeConfig.providers.comfyui.enabled}
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
providers: {
|
||||||
|
...current.providers,
|
||||||
|
comfyui: { ...current.providers.comfyui, enabled: event.target.checked },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<span>{imageMakeConfig.providers.comfyui.enabled ? '已启用' : '未启用'}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="asset-plugin-fields">
|
||||||
|
<label>
|
||||||
|
ComfyUI 地址
|
||||||
|
<input
|
||||||
|
value={imageMakeConfig.providers.comfyui.apiBase ?? ''}
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
providers: {
|
||||||
|
...current.providers,
|
||||||
|
comfyui: { ...current.providers.comfyui, apiBase: event.target.value },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Checkpoint 文件名
|
||||||
|
<input
|
||||||
|
value={imageMakeConfig.providers.comfyui.checkpoint ?? ''}
|
||||||
|
placeholder="v1-5-pruned-emaonly-fp16.safetensors"
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
providers: {
|
||||||
|
...current.providers,
|
||||||
|
comfyui: { ...current.providers.comfyui, checkpoint: event.target.value },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Workflow 路径
|
||||||
|
<input
|
||||||
|
value={imageMakeConfig.providers.comfyui.workflowPath ?? ''}
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
providers: {
|
||||||
|
...current.providers,
|
||||||
|
comfyui: { ...current.providers.comfyui, workflowPath: event.target.value },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
输出节点 ID
|
||||||
|
<input
|
||||||
|
value={imageMakeConfig.providers.comfyui.outputNodeId ?? '9'}
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
providers: {
|
||||||
|
...current.providers,
|
||||||
|
comfyui: { ...current.providers.comfyui, outputNodeId: event.target.value },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="image-generation-scope-grid">
|
||||||
|
<label>
|
||||||
|
Steps
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="number"
|
||||||
checked={Boolean(imageGenerateDraft.purposes[purpose.id])}
|
min={1}
|
||||||
onChange={(event) => updatePurpose(imageGeneratePlugin.pluginId, purpose.id, event.target.checked)}
|
max={100}
|
||||||
|
value={imageMakeConfig.providers.comfyui.steps ?? 20}
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
providers: {
|
||||||
|
...current.providers,
|
||||||
|
comfyui: { ...current.providers.comfyui, steps: Number(event.target.value) },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
<span>
|
|
||||||
<strong>{purpose.label}</strong>
|
|
||||||
<small>{descriptions[purpose.id] ?? purpose.presetId}</small>
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
);
|
<label>
|
||||||
})}
|
CFG
|
||||||
</div>
|
<input
|
||||||
</fieldset>
|
type="number"
|
||||||
<div className="image-generation-settings-footer">
|
min={0}
|
||||||
<span>未勾选的 AI 图片项不执行;原有无图页面和基础缩略图继续可用。</span>
|
max={30}
|
||||||
<button
|
step={0.5}
|
||||||
type="button"
|
value={imageMakeConfig.providers.comfyui.cfg ?? 7}
|
||||||
className="send-btn"
|
disabled={!imageMakeControlsEnabled}
|
||||||
disabled={busy === imageGeneratePlugin.pluginId}
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
onClick={() => void savePlugin(imageGeneratePlugin)}
|
...current,
|
||||||
>
|
providers: {
|
||||||
{busy === imageGeneratePlugin.pluginId ? '保存中…' : '保存图片生成配置'}
|
...current.providers,
|
||||||
</button>
|
comfyui: { ...current.providers.comfyui, cfg: Number(event.target.value) },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Sampler
|
||||||
|
<input
|
||||||
|
value={imageMakeConfig.providers.comfyui.sampler ?? 'euler'}
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
providers: {
|
||||||
|
...current.providers,
|
||||||
|
comfyui: { ...current.providers.comfyui, sampler: event.target.value },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Scheduler
|
||||||
|
<input
|
||||||
|
value={imageMakeConfig.providers.comfyui.scheduler ?? 'normal'}
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
providers: {
|
||||||
|
...current.providers,
|
||||||
|
comfyui: { ...current.providers.comfyui, scheduler: event.target.value },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
超时(秒)
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={2}
|
||||||
|
max={1800}
|
||||||
|
value={imageMakeConfig.providers.comfyui.timeoutSeconds ?? 600}
|
||||||
|
disabled={!imageMakeControlsEnabled}
|
||||||
|
onChange={(event) => setImageMakeConfig((current) => ({
|
||||||
|
...current,
|
||||||
|
providers: {
|
||||||
|
...current.providers,
|
||||||
|
comfyui: { ...current.providers.comfyui, timeoutSeconds: Number(event.target.value) },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="asset-plugin-footer">
|
||||||
|
<span className={`asset-status ${imageMakeConfig.providers.comfyui.enabled ? 'is-on' : ''}`}>
|
||||||
|
{imageMakeConfig.providers.comfyui.checkpoint || '未配置 checkpoint'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="send-btn"
|
||||||
|
disabled={busy === 'image-make-config' || !imageMakeControlsEnabled}
|
||||||
|
onClick={() => void saveImageMakeConfig()}
|
||||||
|
>
|
||||||
|
{busy === 'image-make-config' ? '保存中…' : '保存配置'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="model-center-section-title">
|
<div className="model-center-section-title">
|
||||||
<h3>其他资产能力</h3>
|
<h3>其他资产能力</h3>
|
||||||
<p>素材检索、图片处理和图片理解保持独立配置。</p>
|
<p>素材检索、图片处理和图片理解仅配置运行 Provider;大模型统一在「统一模型中心」管理。</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="asset-plugin-grid asset-plugin-grid--balanced">
|
<div className="asset-plugin-grid asset-plugin-grid--balanced">
|
||||||
{orderedPlugins.filter((plugin) => plugin.pluginId !== 'asset-generate').map((plugin) => {
|
{orderedPlugins.filter((plugin) => plugin.pluginId !== 'asset-generate').map((plugin) => {
|
||||||
const draft = drafts[plugin.pluginId] ?? draftFromPlugin(plugin);
|
const draft = drafts[plugin.pluginId] ?? draftFromPlugin(plugin);
|
||||||
const selectedKey = activeKeys.find((key) => key.id === draft.llmProviderKeyId);
|
|
||||||
const visual = pluginVisuals[plugin.pluginId] ?? { icon: '✦', accent: 'blue' };
|
const visual = pluginVisuals[plugin.pluginId] ?? { icon: '✦', accent: 'blue' };
|
||||||
return <section className={`asset-plugin-card asset-plugin-card--${visual.accent}`} key={plugin.pluginId}>
|
return <section className={`asset-plugin-card asset-plugin-card--${visual.accent}`} key={plugin.pluginId}>
|
||||||
<div className="asset-plugin-card-head">
|
<div className="asset-plugin-card-head">
|
||||||
@@ -229,29 +709,22 @@ export function AssetGatewayPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="asset-plugin-fields">
|
<div className="asset-plugin-fields">
|
||||||
<label>运行 Provider<select value={draft.provider} onChange={(event) => {
|
<label>运行 Provider<select value={draft.provider} onChange={(event) => {
|
||||||
const provider = event.target.value;
|
updateDraft(plugin.pluginId, { provider: event.target.value });
|
||||||
updateDraft(plugin.pluginId, {
|
|
||||||
provider,
|
|
||||||
...(provider === 'image_make' ? { llmProviderKeyId: '', llmModel: '' } : {}),
|
|
||||||
});
|
|
||||||
}} disabled={!draft.enabled}>
|
}} disabled={!draft.enabled}>
|
||||||
<option value="">选择 Provider</option>
|
<option value="">选择 Provider</option>
|
||||||
{plugin.providers.map((provider) => <option key={provider} value={provider}>{provider}</option>)}
|
{plugin.providers.map((provider) => <option key={provider} value={provider}>{provider}</option>)}
|
||||||
</select></label>
|
</select></label>
|
||||||
{plugin.pluginId === 'asset-generate' && draft.provider === 'image_make' ? (
|
{plugin.supportsLlm ? (
|
||||||
<div className="asset-image-make-note">
|
<div className="asset-image-make-note">
|
||||||
image_make 独立管理本地模型,不读取统一模型中心密钥。
|
大模型不在此单独配置;请在
|
||||||
|
{' '}
|
||||||
|
<Link to="/providers">统一模型中心</Link>
|
||||||
|
{' '}
|
||||||
|
设置默认模型与图片任务模型(Qwen VL)。
|
||||||
</div>
|
</div>
|
||||||
) : plugin.supportsLlm ? <>
|
) : (
|
||||||
<label>专属 LLM<select value={draft.llmProviderKeyId} onChange={(event) => updateDraft(plugin.pluginId, { llmProviderKeyId: event.target.value, llmModel: '' })} disabled={!draft.enabled}>
|
<div className="asset-no-llm">确定性处理 · 不调用 LLM</div>
|
||||||
<option value="">不使用 LLM</option>
|
)}
|
||||||
{activeKeys.map((key) => <option key={key.id} value={key.id}>{key.name}</option>)}
|
|
||||||
</select></label>
|
|
||||||
<label>模型<select value={draft.llmModel} onChange={(event) => updateDraft(plugin.pluginId, { llmModel: event.target.value })} disabled={!draft.enabled || !selectedKey}>
|
|
||||||
<option value="">选择模型</option>
|
|
||||||
{selectedKey?.models.map((model) => <option key={model} value={model}>{model}</option>)}
|
|
||||||
</select></label>
|
|
||||||
</> : <div className="asset-no-llm">确定性处理 · 不调用 LLM</div>}
|
|
||||||
{plugin.pluginId === 'asset-generate' && plugin.purposeCatalog?.length ? (
|
{plugin.pluginId === 'asset-generate' && plugin.purposeCatalog?.length ? (
|
||||||
<fieldset className="asset-purpose-fieldset" disabled={!draft.enabled || draft.provider !== 'image_make'}>
|
<fieldset className="asset-purpose-fieldset" disabled={!draft.enabled || draft.provider !== 'image_make'}>
|
||||||
<legend>AI 图片输出项</legend>
|
<legend>AI 图片输出项</legend>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
AssetGatewayConfig,
|
AssetGatewayConfig,
|
||||||
|
ImageMakeAdminConfig,
|
||||||
|
ImageMakeAdminConfigResponse,
|
||||||
|
ImageMakeRuntimeResponse,
|
||||||
AdminDashboardSummary,
|
AdminDashboardSummary,
|
||||||
AdminServiceRestartAction,
|
AdminServiceRestartAction,
|
||||||
AdminServiceRestartResult,
|
AdminServiceRestartResult,
|
||||||
@@ -316,6 +319,27 @@ export async function updateAssetPluginConfig(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getImageMakeAdminConfig(): Promise<ImageMakeAdminConfigResponse> {
|
||||||
|
return portalFetch('/admin-api/image-make/config');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateImageMakeAdminConfig(
|
||||||
|
payload: Partial<ImageMakeAdminConfig> & {
|
||||||
|
providers?: Partial<ImageMakeAdminConfig['providers']> & {
|
||||||
|
aliyun_bailian?: Partial<ImageMakeAdminConfig['providers']['aliyun_bailian']> & { apiKey?: string };
|
||||||
|
};
|
||||||
|
},
|
||||||
|
): Promise<ImageMakeAdminConfigResponse & { ok?: boolean; message?: string }> {
|
||||||
|
return portalFetch('/admin-api/image-make/config', {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getImageMakeRuntimeStatus(): Promise<ImageMakeRuntimeResponse> {
|
||||||
|
return portalFetch('/admin-api/image-make/runtime');
|
||||||
|
}
|
||||||
|
|
||||||
export async function getMindSpaceAdminConfig(): Promise<MindSpaceAdminConfig> {
|
export async function getMindSpaceAdminConfig(): Promise<MindSpaceAdminConfig> {
|
||||||
const result = await portalFetch<{ config: MindSpaceAdminConfig }>('/admin-api/mindspace/config');
|
const result = await portalFetch<{ config: MindSpaceAdminConfig }>('/admin-api/mindspace/config');
|
||||||
return result.config;
|
return result.config;
|
||||||
|
|||||||
@@ -1116,6 +1116,32 @@ body,
|
|||||||
.asset-plugin-card--amber { border-top: 3px solid #e8af55; }
|
.asset-plugin-card--amber { border-top: 3px solid #e8af55; }
|
||||||
.asset-plugin-card--green { border-top: 3px solid #55c79b; }
|
.asset-plugin-card--green { border-top: 3px solid #55c79b; }
|
||||||
|
|
||||||
|
.asset-plugin-card.is-disabled {
|
||||||
|
opacity: 0.68;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-plugin-card.is-disabled .asset-plugin-fields,
|
||||||
|
.asset-plugin-card.is-disabled .asset-plugin-footer button {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-plugin-card.is-disabled .asset-plugin-card-head .asset-mini-toggle {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-plugin-icon--blue { background: rgba(84, 168, 255, .17); color: #9fd0ff; }
|
||||||
|
.asset-plugin-icon--amber { background: rgba(232, 175, 85, .17); color: #f0cc84; }
|
||||||
|
.asset-plugin-icon--green { background: rgba(85, 199, 155, .17); color: #8fe0bc; }
|
||||||
|
|
||||||
|
.image-make-runtime-note {
|
||||||
|
padding: 9px 10px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: rgba(84, 168, 255, .1);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.asset-plugin-card-head {
|
.asset-plugin-card-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export type LlmVisionSettings = {
|
|||||||
providerLabel: string | null;
|
providerLabel: string | null;
|
||||||
visionModel: string | null;
|
visionModel: string | null;
|
||||||
availableModels: string[];
|
availableModels: string[];
|
||||||
|
apiKeyMasked?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LlmConnectionTestResult = {
|
export type LlmConnectionTestResult = {
|
||||||
@@ -224,6 +225,52 @@ export type AssetGatewayConfig = {
|
|||||||
plugins: AssetPluginConfig[];
|
plugins: AssetPluginConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ImageMakeProviderConfig = {
|
||||||
|
enabled: boolean;
|
||||||
|
model?: string;
|
||||||
|
apiBase?: string;
|
||||||
|
apiKeyConfigured?: boolean;
|
||||||
|
apiKeyMasked?: string;
|
||||||
|
dashScopeSource?: 'vision' | 'global' | null;
|
||||||
|
dashScopeKeyName?: string | null;
|
||||||
|
checkpoint?: string;
|
||||||
|
workflowPath?: string;
|
||||||
|
outputNodeId?: string;
|
||||||
|
timeoutSeconds?: number;
|
||||||
|
steps?: number;
|
||||||
|
cfg?: number;
|
||||||
|
sampler?: string;
|
||||||
|
scheduler?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageMakeAdminConfig = {
|
||||||
|
defaultProvider: 'mock' | 'aliyun_bailian' | 'comfyui';
|
||||||
|
jobDefaultTimeoutSeconds: number;
|
||||||
|
providers: {
|
||||||
|
mock: ImageMakeProviderConfig;
|
||||||
|
aliyun_bailian: ImageMakeProviderConfig;
|
||||||
|
comfyui: ImageMakeProviderConfig;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageMakeAdminConfigResponse = {
|
||||||
|
config: ImageMakeAdminConfig;
|
||||||
|
source?: string;
|
||||||
|
updatedAt?: number | null;
|
||||||
|
updatedBy?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageMakeRuntimeResponse = {
|
||||||
|
ok: boolean;
|
||||||
|
fingerprint?: string;
|
||||||
|
source?: string;
|
||||||
|
updatedAt?: number | null;
|
||||||
|
defaultProvider?: string;
|
||||||
|
jobDefaultTimeoutSeconds?: number;
|
||||||
|
providers?: ImageMakeAdminConfig['providers'];
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type AdminServiceRestartAction = 'local_restart' | 'pro_restart';
|
export type AdminServiceRestartAction = 'local_restart' | 'pro_restart';
|
||||||
|
|
||||||
export type AdminServiceRestartResult = {
|
export type AdminServiceRestartResult = {
|
||||||
|
|||||||
Reference in New Issue
Block a user