@dickpy/dsh-imagegen 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -8
- package/docs/images/agent-chat-poster-workflow.png +0 -0
- package/docs/images/community-qq.png +0 -0
- package/lib/client.js +415 -227
- package/lib/client.js.map +1 -1
- package/lib/index.js +129 -42
- package/package.json +4 -3
- package/src/agent-image-tools.ts +68 -41
- package/src/client/ImageGenPanel.tsx +1 -1
- package/src/client/image-toolview.module.css +73 -0
- package/src/client/image-toolview.tsx +158 -0
- package/src/client/index.ts +13 -9
- package/src/client/locales.ts +2 -2
- package/src/engine.ts +86 -1
- package/src/image-models.ts +1 -1
- package/src/index.ts +3 -2
- package/src/protocol.ts +6 -4
package/lib/index.js
CHANGED
|
@@ -6,7 +6,6 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
-
import { createUserMessage } from "@deepseek-ai/dsh-llm/message";
|
|
10
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
11
10
|
//#region src/protocol.ts
|
|
12
11
|
/**
|
|
@@ -17,7 +16,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
|
17
16
|
/** Settings namespace this plugin owns (host settings seam + bridge). */
|
|
18
17
|
const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
|
|
19
18
|
/** Published package version shared by the host updater and the client UI. */
|
|
20
|
-
const PLUGIN_VERSION = "1.2.
|
|
19
|
+
const PLUGIN_VERSION = "1.2.3";
|
|
21
20
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
22
21
|
const SETTINGS_API = {
|
|
23
22
|
describe: "/api/dsh-imagegen/settings/describe",
|
|
@@ -141,7 +140,14 @@ async function enhancePrompt(config, prompt) {
|
|
|
141
140
|
* `/models` exposes candidates only: the configured list is the explicit
|
|
142
141
|
* allow-list because OpenAI-compatible gateways rarely advertise modalities.
|
|
143
142
|
*/
|
|
144
|
-
const DEFAULT_IMAGE_MODELS = [
|
|
143
|
+
const DEFAULT_IMAGE_MODELS = [
|
|
144
|
+
"gpt-image-2",
|
|
145
|
+
"grok-imagine-image",
|
|
146
|
+
"nanobanana2",
|
|
147
|
+
"nanobanana2-lite",
|
|
148
|
+
"nanobanana-pro",
|
|
149
|
+
"seedream-5.0-pro"
|
|
150
|
+
];
|
|
145
151
|
/** Normalize user-entered model identifiers and retain a usable legacy default. */
|
|
146
152
|
function normalizeImageModels(value) {
|
|
147
153
|
const candidates = Array.isArray(value) ? value : [];
|
|
@@ -184,6 +190,24 @@ const DALLE3_SIZES = /* @__PURE__ */ new Set([
|
|
|
184
190
|
function isGrokImagine(model) {
|
|
185
191
|
return /^grok-imagine(?:-|$)/.test(model);
|
|
186
192
|
}
|
|
193
|
+
/** Whether the model belongs to the Google Nano Banana family (nanobanana2 /
|
|
194
|
+
* nanobanana2-lite / nanobanana-pro, plus the official Gemini image IDs the
|
|
195
|
+
* gateways expose). OpenAI-compatible gateways serve these with their own
|
|
196
|
+
* aspect_ratio / image_size vocabulary instead of the OpenAI size/quality
|
|
197
|
+
* passthrough. */
|
|
198
|
+
function isNanoBanana(model) {
|
|
199
|
+
if (/^nanobanana/i.test(model)) return true;
|
|
200
|
+
return model === "gemini-3-pro-image" || model === "gemini-3-pro-image-preview" || model === "gemini-3.1-flash-image" || model === "gemini-3.1-flash-image-preview" || model === "gemini-3.1-flash-lite-image" || model === "gemini-2.5-flash-image";
|
|
201
|
+
}
|
|
202
|
+
/** Whether the model belongs to the ByteDance Seedream family (seedream-5.0-pro,
|
|
203
|
+
* seedream-5.0, seedream-4.x, doubao-seedream-…). OpenAI-compatible gateways
|
|
204
|
+
* serve Seedream through a unified generate-and-edit architecture:
|
|
205
|
+
* generation AND editing both go to /images/generations, reference images are
|
|
206
|
+
* a JSON URL / data-URL array, and the clarity tier is `resolution` while
|
|
207
|
+
* `size` carries the aspect ratio (or exact pixels). */
|
|
208
|
+
function isSeedream(model) {
|
|
209
|
+
return /^(?:doubao-)?seedream/i.test(model);
|
|
210
|
+
}
|
|
187
211
|
/** The panel's aspect ratios mapped to the closest OpenAI pixel size
|
|
188
212
|
* (gpt-image-2 / generic OpenAI-compatible endpoints). */
|
|
189
213
|
const OPENAI_SIZE_BY_RATIO = {
|
|
@@ -274,6 +298,18 @@ function effectiveParams(request) {
|
|
|
274
298
|
...request.quality !== "" && request.quality !== "auto" ? { resolution: request.quality === "4k" ? "2k" : request.quality } : {},
|
|
275
299
|
response_format: "b64_json"
|
|
276
300
|
};
|
|
301
|
+
if (isNanoBanana(model)) return {
|
|
302
|
+
model,
|
|
303
|
+
...request.size !== "" && request.size !== "auto" ? { aspect_ratio: request.size } : {},
|
|
304
|
+
...request.quality !== "" && request.quality !== "auto" ? { image_size: request.quality.toUpperCase() } : {},
|
|
305
|
+
response_format: "b64_json"
|
|
306
|
+
};
|
|
307
|
+
if (isSeedream(model)) return {
|
|
308
|
+
model,
|
|
309
|
+
...request.size !== "" && request.size !== "auto" ? { size: request.size } : {},
|
|
310
|
+
...request.quality !== "" && request.quality !== "auto" ? { resolution: request.quality === "4k" ? "2K" : request.quality.toUpperCase() } : {},
|
|
311
|
+
response_format: "b64_json"
|
|
312
|
+
};
|
|
277
313
|
return {
|
|
278
314
|
model,
|
|
279
315
|
...request.size !== "" && request.size !== "auto" && OPENAI_SIZE_BY_RATIO[request.size] !== void 0 ? { size: OPENAI_SIZE_BY_RATIO[request.size] } : {},
|
|
@@ -359,6 +395,24 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
359
395
|
...params.aspect_ratio !== void 0 ? { aspect_ratio: params.aspect_ratio } : {},
|
|
360
396
|
response_format: "b64_json"
|
|
361
397
|
});
|
|
398
|
+
} else if (isNanoBanana(params.model)) {
|
|
399
|
+
const form = new FormData();
|
|
400
|
+
form.append("image", new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf$2(parsed.mime)}`);
|
|
401
|
+
form.append("prompt", request.prompt);
|
|
402
|
+
form.append("model", params.model);
|
|
403
|
+
if (params.aspect_ratio !== void 0) form.append("aspect_ratio", params.aspect_ratio);
|
|
404
|
+
if (params.image_size !== void 0) form.append("image_size", params.image_size);
|
|
405
|
+
body = form;
|
|
406
|
+
} else if (isSeedream(params.model)) {
|
|
407
|
+
headers["content-type"] = "application/json";
|
|
408
|
+
body = JSON.stringify({
|
|
409
|
+
model: params.model,
|
|
410
|
+
prompt: request.prompt,
|
|
411
|
+
image: [request.image],
|
|
412
|
+
...params.size !== void 0 ? { size: params.size } : {},
|
|
413
|
+
...params.resolution !== void 0 ? { resolution: params.resolution } : {},
|
|
414
|
+
response_format: "b64_json"
|
|
415
|
+
});
|
|
362
416
|
} else {
|
|
363
417
|
const form = new FormData();
|
|
364
418
|
form.append("image", new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf$2(parsed.mime)}`);
|
|
@@ -379,7 +433,8 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
379
433
|
const budget = requestSignal(signal, UPSTREAM_TIMEOUT_MS);
|
|
380
434
|
let response;
|
|
381
435
|
try {
|
|
382
|
-
|
|
436
|
+
const endpoint = request.mode === "edit" && !isSeedream(params.model) ? "/images/edits" : "/images/generations";
|
|
437
|
+
response = await fetch(`${baseUrl}${endpoint}`, {
|
|
383
438
|
method: "POST",
|
|
384
439
|
headers,
|
|
385
440
|
body,
|
|
@@ -2297,6 +2352,8 @@ const taskResultSchema = {
|
|
|
2297
2352
|
}
|
|
2298
2353
|
}
|
|
2299
2354
|
};
|
|
2355
|
+
/** Agent calls stay pending until the provider and history write settle. */
|
|
2356
|
+
const AGENT_GENERATION_TIMEOUT_MS = 3e5;
|
|
2300
2357
|
function acceptedMediaType(value) {
|
|
2301
2358
|
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
2302
2359
|
}
|
|
@@ -2337,7 +2394,6 @@ function renderTaskResult(value) {
|
|
|
2337
2394
|
/** Register the global Agent tools and unregister them with the plugin lifecycle. */
|
|
2338
2395
|
function registerAgentImageTools(ctx, runtime, resolve) {
|
|
2339
2396
|
const attachmentRefs = /* @__PURE__ */ new Map();
|
|
2340
|
-
const taskSubscriptions = /* @__PURE__ */ new Set();
|
|
2341
2397
|
const ensureConfigured = () => {
|
|
2342
2398
|
const config = resolve();
|
|
2343
2399
|
if (!config.enabled) throw new ImageGenError("AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.", "plugin-disabled");
|
|
@@ -2366,7 +2422,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2366
2422
|
return {
|
|
2367
2423
|
task_id: task.id,
|
|
2368
2424
|
status: task.status,
|
|
2369
|
-
message: task.status === "completed" ? "Generation completed. The images are attached below and can be reused as source_image in edit_image." : task.status === "failed" ? "Generation failed." : task.status === "cancelled" ? "Generation was cancelled." : "Generation is still running.
|
|
2425
|
+
message: task.status === "completed" ? "Generation completed. The images are attached below and can be reused as source_image in edit_image." : task.status === "failed" ? "Generation failed." : task.status === "cancelled" ? "Generation was cancelled." : "Generation is still running. Query the task again when you need its current status.",
|
|
2370
2426
|
...task.error === void 0 ? {} : { error: task.error },
|
|
2371
2427
|
images
|
|
2372
2428
|
};
|
|
@@ -2376,39 +2432,66 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2376
2432
|
if (task === void 0) throw new ImageGenError(`Image generation task ${id} was not found.`, "task-not-found");
|
|
2377
2433
|
return task;
|
|
2378
2434
|
};
|
|
2379
|
-
const
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2435
|
+
const waitForTask = (id, signal) => new Promise((resolveTask, rejectTask) => {
|
|
2436
|
+
let settled = false;
|
|
2437
|
+
let dispose = () => {};
|
|
2438
|
+
let timer;
|
|
2439
|
+
let abort = () => {};
|
|
2440
|
+
const cleanup = () => {
|
|
2441
|
+
dispose();
|
|
2442
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2443
|
+
signal?.removeEventListener("abort", abort);
|
|
2444
|
+
};
|
|
2445
|
+
const resolve = (task) => {
|
|
2446
|
+
if (settled) return;
|
|
2447
|
+
settled = true;
|
|
2448
|
+
cleanup();
|
|
2449
|
+
resolveTask(task);
|
|
2450
|
+
};
|
|
2451
|
+
const reject = (error) => {
|
|
2452
|
+
if (settled) return;
|
|
2453
|
+
settled = true;
|
|
2454
|
+
cleanup();
|
|
2455
|
+
rejectTask(error);
|
|
2456
|
+
};
|
|
2457
|
+
abort = () => {
|
|
2458
|
+
if (settled) return;
|
|
2459
|
+
const reason = signal?.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Image generation was cancelled.");
|
|
2460
|
+
settled = true;
|
|
2461
|
+
cleanup();
|
|
2462
|
+
runtime.queue.cancel(id);
|
|
2463
|
+
rejectTask(reason);
|
|
2464
|
+
};
|
|
2397
2465
|
const onChange = (updated) => {
|
|
2398
|
-
if (updated.id
|
|
2399
|
-
dispose?.();
|
|
2400
|
-
if (dispose !== void 0) taskSubscriptions.delete(dispose);
|
|
2401
|
-
notifyCompletion(agent, updated).catch(() => {});
|
|
2466
|
+
if (updated.id === id && isFinalTask(updated)) resolve(updated);
|
|
2402
2467
|
};
|
|
2468
|
+
if (signal?.aborted === true) {
|
|
2469
|
+
abort();
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2403
2472
|
dispose = runtime.queue.subscribe(onChange);
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2473
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
2474
|
+
timer = setTimeout(() => {
|
|
2475
|
+
if (settled) return;
|
|
2476
|
+
const timeout = new ImageGenError(`Image generation task ${id} timed out after ${AGENT_GENERATION_TIMEOUT_MS / 1e3} seconds.`, "generation-timeout");
|
|
2477
|
+
settled = true;
|
|
2478
|
+
cleanup();
|
|
2479
|
+
runtime.queue.cancel(id);
|
|
2480
|
+
rejectTask(timeout);
|
|
2481
|
+
}, AGENT_GENERATION_TIMEOUT_MS);
|
|
2482
|
+
let current;
|
|
2483
|
+
try {
|
|
2484
|
+
current = findTask(id);
|
|
2485
|
+
} catch (error) {
|
|
2486
|
+
reject(error);
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
if (isFinalTask(current)) resolve(current);
|
|
2490
|
+
});
|
|
2408
2491
|
const disposers = [
|
|
2409
2492
|
ctx.tools.register(defineTool({
|
|
2410
2493
|
name: "generate_image",
|
|
2411
|
-
description: "
|
|
2494
|
+
description: "Generate an image. By default this tool call stays pending until the task reaches a final state, and completed images are returned directly as tool-result attachments without creating a user message. Set wait_for_completion to false for background mode, then use get_image_generation_task explicitly. Only use models configured for this plugin; omit model to use the first configured image model.",
|
|
2412
2495
|
parameters: {
|
|
2413
2496
|
prompt: {
|
|
2414
2497
|
type: "string",
|
|
@@ -2434,6 +2517,10 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2434
2517
|
detail: {
|
|
2435
2518
|
type: "string",
|
|
2436
2519
|
description: "Optional provider detail value, for example standard or high."
|
|
2520
|
+
},
|
|
2521
|
+
wait_for_completion: {
|
|
2522
|
+
type: "boolean",
|
|
2523
|
+
description: "Wait for images and return them in this tool result. Defaults to true; set false for background mode."
|
|
2437
2524
|
}
|
|
2438
2525
|
},
|
|
2439
2526
|
output: {
|
|
@@ -2451,13 +2538,12 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2451
2538
|
n: Math.min(4, Math.max(1, args.count ?? 1)),
|
|
2452
2539
|
detail: args.detail ?? ""
|
|
2453
2540
|
});
|
|
2454
|
-
|
|
2455
|
-
return taskResult(task);
|
|
2541
|
+
return taskResult(args.wait_for_completion === false ? task : await waitForTask(task.id, exec.signal));
|
|
2456
2542
|
}
|
|
2457
2543
|
})),
|
|
2458
2544
|
ctx.tools.register(defineTool({
|
|
2459
2545
|
name: "edit_image",
|
|
2460
|
-
description: "
|
|
2546
|
+
description: "Edit an image. By default this tool call stays pending until the task reaches a final state, and completed images are returned directly as tool-result attachments without creating a user message. Set wait_for_completion to false for background mode, then use get_image_generation_task explicitly. source_image must be an image reference returned by a completed generation or get_image_generation_task; pass that entire object unchanged. Only configured image models are allowed; omit model to use the first configured model.",
|
|
2461
2547
|
parameters: {
|
|
2462
2548
|
prompt: {
|
|
2463
2549
|
type: "string",
|
|
@@ -2488,6 +2574,10 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2488
2574
|
detail: {
|
|
2489
2575
|
type: "string",
|
|
2490
2576
|
description: "Optional provider detail value."
|
|
2577
|
+
},
|
|
2578
|
+
wait_for_completion: {
|
|
2579
|
+
type: "boolean",
|
|
2580
|
+
description: "Wait for images and return them in this tool result. Defaults to true; set false for background mode."
|
|
2491
2581
|
}
|
|
2492
2582
|
},
|
|
2493
2583
|
output: {
|
|
@@ -2508,13 +2598,12 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2508
2598
|
image: imageDataUrl(reference),
|
|
2509
2599
|
...reference.ref.name === void 0 ? {} : { refName: reference.ref.name }
|
|
2510
2600
|
});
|
|
2511
|
-
|
|
2512
|
-
return taskResult(task);
|
|
2601
|
+
return taskResult(args.wait_for_completion === false ? task : await waitForTask(task.id, exec.signal));
|
|
2513
2602
|
}
|
|
2514
2603
|
})),
|
|
2515
2604
|
ctx.tools.register(defineTool({
|
|
2516
2605
|
name: "get_image_generation_task",
|
|
2517
|
-
description: "
|
|
2606
|
+
description: "Check an image-generation task status. Completed tasks return image references and image attachments for edit_image. Generation tools normally wait for completion, so use this for explicit recovery or status checks.",
|
|
2518
2607
|
parameters: { task_id: {
|
|
2519
2608
|
type: "string",
|
|
2520
2609
|
required: true,
|
|
@@ -2550,8 +2639,6 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2550
2639
|
}))
|
|
2551
2640
|
];
|
|
2552
2641
|
return () => {
|
|
2553
|
-
for (const dispose of taskSubscriptions) dispose();
|
|
2554
|
-
taskSubscriptions.clear();
|
|
2555
2642
|
for (const dispose of disposers) dispose();
|
|
2556
2643
|
};
|
|
2557
2644
|
}
|
|
@@ -2592,7 +2679,7 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
|
|
|
2592
2679
|
/** Order of the announcement section within the tool-guidance band. */
|
|
2593
2680
|
const SECTION_ORDER = 150;
|
|
2594
2681
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
2595
|
-
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API,模型由用户在「设置 → 插件 → AI 生图」中检测或手动配置的生图模型列表决定;支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url
|
|
2682
|
+
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API,模型由用户在「设置 → 插件 → AI 生图」中检测或手动配置的生图模型列表决定;支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送,nanobanana 系列按 aspect_ratio / image_size 参数协议发送;seedream 系列统一走 /images/generations,参考图以 JSON image 数组发送)。API 地址与密钥在 GUI 设置中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用已配置的生图模型;模型出现在 /models 中不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选与复用。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片直接作为工具结果附件返回,不会额外伪造用户消息。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问 vibeui.top。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
|
|
2596
2683
|
/** Add the live allow-list so an Agent can honor a user's model choice. */
|
|
2597
2684
|
function guidanceFor(imageModels) {
|
|
2598
2685
|
return `${IMAGEGEN_GUIDANCE} 当前允许调用的生图模型:${imageModels.join("、")}。用户指定其中某个模型时,工具参数 model 必须使用该精确名称;未指定时使用列表中的第一个。`;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dickpy/dsh-imagegen",
|
|
3
|
-
"description": "AI 鐢熷浘 (image generation) plugin for the dsh web GUI: text-to-image and image-to-image through a configurable OpenAI-compatible endpoint (gpt-image-2 / grok-imagine-image / dall-e-3, with native xAI Grok Imagine request shaping), with a settings card for api_url / api_key and a sidebar entry opening a split-pane generation studio.",
|
|
4
|
-
"version": "1.2.
|
|
3
|
+
"description": "AI 鐢熷浘 (image generation) plugin for the dsh web GUI: text-to-image and image-to-image through a configurable OpenAI-compatible endpoint (gpt-image-2 / grok-imagine-image / nanobanana series / seedream-5.0-pro / dall-e-3, with native xAI Grok Imagine, Google Nano Banana and ByteDance Seedream request shaping), with a settings card for api_url / api_key and a sidebar entry opening a split-pane generation studio.",
|
|
4
|
+
"version": "1.2.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"inject": [
|
|
18
18
|
"@deepseek-ai/dsh-client-runtime",
|
|
19
19
|
"@deepseek-ai/dsh-client-connection",
|
|
20
|
-
"@deepseek-ai/dsh-client-ui-settings"
|
|
20
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
21
|
+
"@deepseek-ai/dsh-client-ui-tool"
|
|
21
22
|
],
|
|
22
23
|
"platform": "web"
|
|
23
24
|
}
|
package/src/agent-image-tools.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/** Agent-facing image-generation tools backed by the shared host queue. */
|
|
2
2
|
|
|
3
3
|
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm/message'
|
|
5
4
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
6
5
|
import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
|
|
7
6
|
import type {} from '@deepseek-ai/dsh-attachment'
|
|
@@ -61,6 +60,9 @@ const taskResultSchema = {
|
|
|
61
60
|
},
|
|
62
61
|
} as const
|
|
63
62
|
|
|
63
|
+
/** Agent calls stay pending until the provider and history write settle. */
|
|
64
|
+
const AGENT_GENERATION_TIMEOUT_MS = 300_000
|
|
65
|
+
|
|
64
66
|
function acceptedMediaType(value: string): value is ImageMediaType {
|
|
65
67
|
return value === 'image/png' || value === 'image/jpeg' || value === 'image/webp' || value === 'image/gif'
|
|
66
68
|
}
|
|
@@ -106,7 +108,6 @@ function renderTaskResult(value: AgentTaskResult): Array<{ type: 'text'; text: s
|
|
|
106
108
|
/** Register the global Agent tools and unregister them with the plugin lifecycle. */
|
|
107
109
|
export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRuntime, resolve: () => AgentImageToolConfig): () => void {
|
|
108
110
|
const attachmentRefs = new Map<string, Promise<AgentImageRef[]>>()
|
|
109
|
-
const taskSubscriptions = new Set<() => void>()
|
|
110
111
|
const ensureConfigured = (): void => {
|
|
111
112
|
const config = resolve()
|
|
112
113
|
if (!config.enabled) throw new ImageGenError('AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.', 'plugin-disabled')
|
|
@@ -144,7 +145,7 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
144
145
|
? 'Generation failed.'
|
|
145
146
|
: task.status === 'cancelled'
|
|
146
147
|
? 'Generation was cancelled.'
|
|
147
|
-
: 'Generation is still running.
|
|
148
|
+
: 'Generation is still running. Query the task again when you need its current status.',
|
|
148
149
|
...task.error === undefined ? {} : { error: task.error },
|
|
149
150
|
images,
|
|
150
151
|
}
|
|
@@ -154,42 +155,70 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
154
155
|
if (task === undefined) throw new ImageGenError(`Image generation task ${id} was not found.`, 'task-not-found')
|
|
155
156
|
return task
|
|
156
157
|
}
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
158
|
+
const waitForTask = (id: string, signal: AbortSignal | undefined): Promise<GenerationTask> => new Promise((resolveTask, rejectTask) => {
|
|
159
|
+
let settled = false
|
|
160
|
+
let dispose = (): void => {}
|
|
161
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
162
|
+
let abort = (): void => {}
|
|
163
|
+
|
|
164
|
+
const cleanup = (): void => {
|
|
165
|
+
dispose()
|
|
166
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
167
|
+
signal?.removeEventListener('abort', abort)
|
|
168
|
+
}
|
|
169
|
+
const resolve = (task: GenerationTask): void => {
|
|
170
|
+
if (settled) return
|
|
171
|
+
settled = true
|
|
172
|
+
cleanup()
|
|
173
|
+
resolveTask(task)
|
|
174
|
+
}
|
|
175
|
+
const reject = (error: unknown): void => {
|
|
176
|
+
if (settled) return
|
|
177
|
+
settled = true
|
|
178
|
+
cleanup()
|
|
179
|
+
rejectTask(error)
|
|
180
|
+
}
|
|
181
|
+
abort = (): void => {
|
|
182
|
+
if (settled) return
|
|
183
|
+
const reason = signal?.reason instanceof Error ? signal.reason : new Error('Image generation was cancelled.')
|
|
184
|
+
// Remove the listener before publishing cancellation so the queue event
|
|
185
|
+
// cannot turn an execution abort into a successful cancelled result.
|
|
186
|
+
settled = true
|
|
187
|
+
cleanup()
|
|
188
|
+
runtime.queue.cancel(id)
|
|
189
|
+
rejectTask(reason)
|
|
190
|
+
}
|
|
178
191
|
const onChange = (updated: GenerationTask): void => {
|
|
179
|
-
if (updated.id
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
192
|
+
if (updated.id === id && isFinalTask(updated)) resolve(updated)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (signal?.aborted === true) {
|
|
196
|
+
abort()
|
|
197
|
+
return
|
|
183
198
|
}
|
|
184
199
|
dispose = runtime.queue.subscribe(onChange)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
200
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
201
|
+
timer = setTimeout(() => {
|
|
202
|
+
if (settled) return
|
|
203
|
+
const timeout = new ImageGenError(`Image generation task ${id} timed out after ${AGENT_GENERATION_TIMEOUT_MS / 1000} seconds.`, 'generation-timeout')
|
|
204
|
+
settled = true
|
|
205
|
+
cleanup()
|
|
206
|
+
runtime.queue.cancel(id)
|
|
207
|
+
rejectTask(timeout)
|
|
208
|
+
}, AGENT_GENERATION_TIMEOUT_MS)
|
|
209
|
+
let current: GenerationTask
|
|
210
|
+
try {
|
|
211
|
+
current = findTask(id)
|
|
212
|
+
} catch (error) {
|
|
213
|
+
reject(error)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
if (isFinalTask(current)) resolve(current)
|
|
217
|
+
})
|
|
189
218
|
const disposers = [
|
|
190
219
|
ctx.tools.register(defineTool({
|
|
191
220
|
name: 'generate_image',
|
|
192
|
-
description: '
|
|
221
|
+
description: 'Generate an image. By default this tool call stays pending until the task reaches a final state, and completed images are returned directly as tool-result attachments without creating a user message. Set wait_for_completion to false for background mode, then use get_image_generation_task explicitly. Only use models configured for this plugin; omit model to use the first configured image model.',
|
|
193
222
|
parameters: {
|
|
194
223
|
prompt: { type: 'string', required: true, description: 'Detailed image-generation prompt.' },
|
|
195
224
|
model: { type: 'string', description: 'One of the configured image models. Defaults to the first configured model.' },
|
|
@@ -197,6 +226,7 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
197
226
|
quality: { type: 'string', description: 'auto, 1k, 2k, or 4k.' },
|
|
198
227
|
count: { type: 'integer', description: 'Number of images, 1 to 4. Defaults to 1.' },
|
|
199
228
|
detail: { type: 'string', description: 'Optional provider detail value, for example standard or high.' },
|
|
229
|
+
wait_for_completion: { type: 'boolean', description: 'Wait for images and return them in this tool result. Defaults to true; set false for background mode.' },
|
|
200
230
|
},
|
|
201
231
|
output: { schema: taskResultSchema, render: (_args, value) => renderTaskResult(value) },
|
|
202
232
|
async execute(args, exec) {
|
|
@@ -210,13 +240,12 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
210
240
|
n: Math.min(4, Math.max(1, args.count ?? 1)),
|
|
211
241
|
detail: args.detail ?? '',
|
|
212
242
|
})
|
|
213
|
-
|
|
214
|
-
return taskResult(task)
|
|
243
|
+
return taskResult(args.wait_for_completion === false ? task : await waitForTask(task.id, exec.signal))
|
|
215
244
|
},
|
|
216
245
|
})),
|
|
217
246
|
ctx.tools.register(defineTool({
|
|
218
247
|
name: 'edit_image',
|
|
219
|
-
description: '
|
|
248
|
+
description: 'Edit an image. By default this tool call stays pending until the task reaches a final state, and completed images are returned directly as tool-result attachments without creating a user message. Set wait_for_completion to false for background mode, then use get_image_generation_task explicitly. source_image must be an image reference returned by a completed generation or get_image_generation_task; pass that entire object unchanged. Only configured image models are allowed; omit model to use the first configured model.',
|
|
220
249
|
parameters: {
|
|
221
250
|
prompt: { type: 'string', required: true, description: 'How to transform the source image.' },
|
|
222
251
|
source_image: { ...imageRefSchema, required: true, description: 'Image reference returned by get_image_generation_task.' },
|
|
@@ -225,6 +254,7 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
225
254
|
quality: { type: 'string', description: 'auto, 1k, 2k, or 4k.' },
|
|
226
255
|
count: { type: 'integer', description: 'Number of images, 1 to 4. Defaults to 1.' },
|
|
227
256
|
detail: { type: 'string', description: 'Optional provider detail value.' },
|
|
257
|
+
wait_for_completion: { type: 'boolean', description: 'Wait for images and return them in this tool result. Defaults to true; set false for background mode.' },
|
|
228
258
|
},
|
|
229
259
|
output: { schema: taskResultSchema, render: (_args, value) => renderTaskResult(value) },
|
|
230
260
|
async execute(args, exec) {
|
|
@@ -241,13 +271,12 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
241
271
|
image: imageDataUrl(reference),
|
|
242
272
|
...reference.ref.name === undefined ? {} : { refName: reference.ref.name },
|
|
243
273
|
})
|
|
244
|
-
|
|
245
|
-
return taskResult(task)
|
|
274
|
+
return taskResult(args.wait_for_completion === false ? task : await waitForTask(task.id, exec.signal))
|
|
246
275
|
},
|
|
247
276
|
})),
|
|
248
277
|
ctx.tools.register(defineTool({
|
|
249
278
|
name: 'get_image_generation_task',
|
|
250
|
-
description: '
|
|
279
|
+
description: 'Check an image-generation task status. Completed tasks return image references and image attachments for edit_image. Generation tools normally wait for completion, so use this for explicit recovery or status checks.',
|
|
251
280
|
parameters: { task_id: { type: 'string', required: true, description: 'Task id returned by generate_image or edit_image.' } },
|
|
252
281
|
output: { schema: taskResultSchema, render: (_args, value) => renderTaskResult(value) },
|
|
253
282
|
async execute(args) {
|
|
@@ -269,8 +298,6 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
269
298
|
})),
|
|
270
299
|
]
|
|
271
300
|
return () => {
|
|
272
|
-
for (const dispose of taskSubscriptions) dispose()
|
|
273
|
-
taskSubscriptions.clear()
|
|
274
301
|
for (const dispose of disposers) dispose()
|
|
275
302
|
}
|
|
276
303
|
}
|
|
@@ -1127,7 +1127,7 @@ export function ImageGenPanel(props: {
|
|
|
1127
1127
|
<span className={css.galleryBadge}>{entry.mode === 'edit' ? tt('mode.edit') : tt('mode.text')}</span>
|
|
1128
1128
|
</button>
|
|
1129
1129
|
<div className={css.galleryCardFooter}>
|
|
1130
|
-
<span className={css.galleryAvatar}>{entry.model.startsWith('grok') ? 'G' : 'D'}</span>
|
|
1130
|
+
<span className={css.galleryAvatar}>{entry.model.toLowerCase().startsWith('nanobanana') ? 'N' : entry.model.toLowerCase().startsWith('seedream') ? 'S' : entry.model.startsWith('grok') ? 'G' : 'D'}</span>
|
|
1131
1131
|
<span className={css.galleryCardInfo}>
|
|
1132
1132
|
<strong>{entry.prompt || tt('gallery.untitled')}</strong>
|
|
1133
1133
|
<small>{entry.model} · {normalizeSize(entry.size)} · {formatTime(entry.createdAt)}</small>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 7px;
|
|
5
|
+
margin: 4px 0;
|
|
6
|
+
padding: 8px 10px 10px;
|
|
7
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
8
|
+
border-radius: 8px;
|
|
9
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.header {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: 7px;
|
|
16
|
+
min-height: 20px;
|
|
17
|
+
color: var(--dsw-alias-label-secondary);
|
|
18
|
+
font-size: 12px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.icon {
|
|
22
|
+
color: var(--dsw-alias-brand-primary);
|
|
23
|
+
font-size: 15px;
|
|
24
|
+
line-height: 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.status {
|
|
28
|
+
margin-left: auto;
|
|
29
|
+
color: var(--dsw-alias-label-tertiary);
|
|
30
|
+
font-size: 11px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.message,
|
|
34
|
+
.loading,
|
|
35
|
+
.error {
|
|
36
|
+
margin: 0;
|
|
37
|
+
color: var(--dsw-alias-label-tertiary);
|
|
38
|
+
font-size: 12px;
|
|
39
|
+
line-height: 1.5;
|
|
40
|
+
overflow-wrap: anywhere;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.images {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-wrap: wrap;
|
|
46
|
+
gap: 8px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.imageLink {
|
|
50
|
+
display: block;
|
|
51
|
+
max-width: min(280px, 100%);
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
54
|
+
border-radius: 6px;
|
|
55
|
+
background: var(--dsw-alias-bg-base);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.imageLink:hover {
|
|
59
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.image {
|
|
63
|
+
display: block;
|
|
64
|
+
width: auto;
|
|
65
|
+
max-width: 280px;
|
|
66
|
+
height: auto;
|
|
67
|
+
max-height: 220px;
|
|
68
|
+
object-fit: contain;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.error {
|
|
72
|
+
color: var(--dsw-alias-state-error-primary);
|
|
73
|
+
}
|