@downcity/plugins 1.0.96 → 1.0.108
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/bin/asr/Plugin.d.ts +1 -15
- package/bin/asr/Plugin.d.ts.map +1 -1
- package/bin/asr/Plugin.js +36 -5
- package/bin/asr/Plugin.js.map +1 -1
- package/bin/chat/runtime/ChatAuthorizationRuntime.d.ts.map +1 -1
- package/bin/chat/runtime/ChatAuthorizationRuntime.js +53 -12
- package/bin/chat/runtime/ChatAuthorizationRuntime.js.map +1 -1
- package/bin/chat/runtime/ChatPluginActionRegistry.d.ts.map +1 -1
- package/bin/chat/runtime/ChatPluginActionRegistry.js +114 -42
- package/bin/chat/runtime/ChatPluginActionRegistry.js.map +1 -1
- package/bin/contact/Action.d.ts.map +1 -1
- package/bin/contact/Action.js +159 -37
- package/bin/contact/Action.js.map +1 -1
- package/bin/image/ImagePlugin.d.ts +14 -67
- package/bin/image/ImagePlugin.d.ts.map +1 -1
- package/bin/image/ImagePlugin.js +432 -151
- package/bin/image/ImagePlugin.js.map +1 -1
- package/bin/image/types/ImagePlugin.d.ts +87 -32
- package/bin/image/types/ImagePlugin.d.ts.map +1 -1
- package/bin/image/types/ImagePlugin.js +1 -1
- package/bin/index.d.ts +1 -1
- package/bin/index.d.ts.map +1 -1
- package/bin/memory/MemoryPlugin.d.ts.map +1 -1
- package/bin/memory/MemoryPlugin.js +130 -17
- package/bin/memory/MemoryPlugin.js.map +1 -1
- package/bin/skill/Plugin.d.ts.map +1 -1
- package/bin/skill/Plugin.js +90 -11
- package/bin/skill/Plugin.js.map +1 -1
- package/bin/task/runtime/TaskPluginActionRegistry.d.ts.map +1 -1
- package/bin/task/runtime/TaskPluginActionRegistry.js +202 -24
- package/bin/task/runtime/TaskPluginActionRegistry.js.map +1 -1
- package/bin/tts/Plugin.d.ts +1 -15
- package/bin/tts/Plugin.d.ts.map +1 -1
- package/bin/tts/Plugin.js +38 -5
- package/bin/tts/Plugin.js.map +1 -1
- package/bin/web/Plugin.d.ts +2 -19
- package/bin/web/Plugin.d.ts.map +1 -1
- package/bin/web/Plugin.js +39 -5
- package/bin/web/Plugin.js.map +1 -1
- package/bin/workboard/Plugin.d.ts.map +1 -1
- package/bin/workboard/Plugin.js +10 -2
- package/bin/workboard/Plugin.js.map +1 -1
- package/package.json +3 -3
- package/src/asr/Plugin.ts +37 -5
- package/src/chat/runtime/ChatAuthorizationRuntime.ts +53 -12
- package/src/chat/runtime/ChatPluginActionRegistry.ts +114 -42
- package/src/contact/Action.ts +159 -37
- package/src/image/ImagePlugin.ts +477 -222
- package/src/image/types/ImagePlugin.ts +91 -32
- package/src/index.ts +3 -1
- package/src/memory/MemoryPlugin.ts +130 -17
- package/src/skill/Plugin.ts +101 -21
- package/src/task/runtime/TaskPluginActionRegistry.ts +209 -24
- package/src/tts/Plugin.ts +39 -5
- package/src/web/Plugin.ts +39 -5
- package/src/workboard/Plugin.ts +10 -2
package/bin/image/ImagePlugin.js
CHANGED
|
@@ -2,17 +2,123 @@
|
|
|
2
2
|
* ImagePlugin:图片生成插件。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
|
-
* - 对 Agent
|
|
5
|
+
* - 对 Agent 暴露 `image_create` / `image_result` 两步式任务 action。
|
|
6
6
|
* - City / provider 的图片能力通过 image_create / image_result 任务函数注入。
|
|
7
|
-
* -
|
|
7
|
+
* - 成功结果返回 AI SDK UIMessage,后续由 plugin tool bridge 抽取 file parts 写回 assistant 消息。
|
|
8
8
|
*/
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { createAction } from "@downcity/agent/internal/plugin/core/PluginActionFactory.js";
|
|
9
13
|
import { BasePlugin } from "@downcity/agent/internal/plugin/core/BasePlugin.js";
|
|
10
14
|
const DEFAULT_IMAGE_PLUGIN_NAME = "image";
|
|
11
15
|
const DEFAULT_IMAGE_PLUGIN_TITLE = "Image";
|
|
12
16
|
const DEFAULT_IMAGE_PLUGIN_DESCRIPTION = "Generate images and return them as assistant file parts.";
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
17
|
+
const HTTP_URL_RE = /^https?:\/\//i;
|
|
18
|
+
const DEFAULT_IMAGE_MEDIA_TYPE = "image/png";
|
|
19
|
+
/**
|
|
20
|
+
* `image_result` 阻塞等待时的默认参数。
|
|
21
|
+
*
|
|
22
|
+
* 关键点(中文)
|
|
23
|
+
* - DEFAULT_IMAGE_WAIT_MS:总等待上限(毫秒),避免 agent 单次调用挂太久。
|
|
24
|
+
* - DEFAULT_IMAGE_POLL_MS:相邻两次轮询的最小间隔(毫秒)。
|
|
25
|
+
* - MAX_IMAGE_WAIT_MS:用户参数硬上限,防止异常值导致 agent 永远等下去。
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_IMAGE_WAIT_MS = 60_000;
|
|
28
|
+
const DEFAULT_IMAGE_POLL_MS = 1_500;
|
|
29
|
+
const MAX_IMAGE_WAIT_MS = 10 * 60_000;
|
|
30
|
+
/**
|
|
31
|
+
* 判断任务状态是否已到达终态。
|
|
32
|
+
*/
|
|
33
|
+
function is_terminal_status(status) {
|
|
34
|
+
return status === "succeeded" || status === "failed";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* sleep 工具。
|
|
38
|
+
*/
|
|
39
|
+
function delay_ms(ms) {
|
|
40
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 把外部传入的等待参数夹到合法区间。
|
|
44
|
+
*/
|
|
45
|
+
function clamp_wait_ms(value) {
|
|
46
|
+
if (!Number.isFinite(value) || value < 0)
|
|
47
|
+
return 0;
|
|
48
|
+
if (value > MAX_IMAGE_WAIT_MS)
|
|
49
|
+
return MAX_IMAGE_WAIT_MS;
|
|
50
|
+
return Math.floor(value);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 把异常完整描述为字符串,保留 `error.cause` 链上的诊断信息。
|
|
54
|
+
*
|
|
55
|
+
* 关键点(中文)
|
|
56
|
+
* - Node fetch 抛出的 `TypeError: fetch failed` 把真正的根因放在 `error.cause`;
|
|
57
|
+
* `String(error)` 只会拿到 message,会让上层只看到一个干瘪的 "fetch failed"。
|
|
58
|
+
* - 这里递归读取 cause 链,并取每一层的 `code` + `message`,方便在 agent 输出里直接定位问题。
|
|
59
|
+
*/
|
|
60
|
+
function describe_error(error) {
|
|
61
|
+
if (!(error instanceof Error))
|
|
62
|
+
return String(error);
|
|
63
|
+
const parts = [error.message || error.name || "Error"];
|
|
64
|
+
let current = error.cause;
|
|
65
|
+
let depth = 0;
|
|
66
|
+
while (current && depth < 3) {
|
|
67
|
+
if (current instanceof Error) {
|
|
68
|
+
const code = current.code;
|
|
69
|
+
const code_text = typeof code === "string" && code ? `[${code}] ` : "";
|
|
70
|
+
parts.push(`${code_text}${current.message || current.name}`.trim());
|
|
71
|
+
current = current.cause;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
parts.push(String(current));
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
depth += 1;
|
|
78
|
+
}
|
|
79
|
+
return parts.filter(Boolean).join(" :: ");
|
|
80
|
+
}
|
|
81
|
+
const IMAGE_MEDIA_TYPES = {
|
|
82
|
+
".apng": "image/apng",
|
|
83
|
+
".avif": "image/avif",
|
|
84
|
+
".gif": "image/gif",
|
|
85
|
+
".jpg": "image/jpeg",
|
|
86
|
+
".jpeg": "image/jpeg",
|
|
87
|
+
".png": "image/png",
|
|
88
|
+
".webp": "image/webp",
|
|
89
|
+
};
|
|
90
|
+
const IMAGE_TEXT_CONTENT_SCHEMA = z.object({
|
|
91
|
+
type: z.literal("text"),
|
|
92
|
+
text: z.string(),
|
|
93
|
+
});
|
|
94
|
+
const IMAGE_FILE_CONTENT_SCHEMA = z.object({
|
|
95
|
+
type: z.literal("image"),
|
|
96
|
+
url: z.string(),
|
|
97
|
+
media_type: z.string().optional(),
|
|
98
|
+
});
|
|
99
|
+
const IMAGE_CREATE_INPUT_SCHEMA = z.object({
|
|
100
|
+
model: z.string().optional(),
|
|
101
|
+
prompt: z.string().optional(),
|
|
102
|
+
content: z.array(z.union([
|
|
103
|
+
IMAGE_TEXT_CONTENT_SCHEMA,
|
|
104
|
+
IMAGE_FILE_CONTENT_SCHEMA,
|
|
105
|
+
])).optional(),
|
|
106
|
+
n: z.number().optional(),
|
|
107
|
+
count: z.number().optional(),
|
|
108
|
+
size: z.string().optional(),
|
|
109
|
+
aspect_ratio: z.string().optional(),
|
|
110
|
+
ratio: z.string().optional(),
|
|
111
|
+
quality: z.string().optional(),
|
|
112
|
+
seed: z.number().optional(),
|
|
113
|
+
client_job_id: z.string().optional(),
|
|
114
|
+
provider_options: z.object({}).passthrough().optional(),
|
|
115
|
+
}).passthrough();
|
|
116
|
+
const IMAGE_RESULT_INPUT_SCHEMA = z.object({
|
|
117
|
+
job_id: z.string(),
|
|
118
|
+
until_done: z.boolean().optional(),
|
|
119
|
+
max_wait_ms: z.number().optional(),
|
|
120
|
+
poll_interval_ms: z.number().optional(),
|
|
121
|
+
}).passthrough();
|
|
16
122
|
/**
|
|
17
123
|
* 判断值是否为普通对象。
|
|
18
124
|
*/
|
|
@@ -31,6 +137,110 @@ function normalize_image_payload(payload) {
|
|
|
31
137
|
}
|
|
32
138
|
return { ...record };
|
|
33
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* 根据文件扩展名推断图片 MIME 类型。
|
|
142
|
+
*/
|
|
143
|
+
function infer_image_media_type(file_path, fallback) {
|
|
144
|
+
if (fallback && fallback.trim())
|
|
145
|
+
return fallback.trim();
|
|
146
|
+
const ext = path.extname(file_path).toLowerCase();
|
|
147
|
+
return IMAGE_MEDIA_TYPES[ext] ?? DEFAULT_IMAGE_MEDIA_TYPE;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 解析图片本地路径。
|
|
151
|
+
*/
|
|
152
|
+
function resolve_image_file_path(root_path, image_url) {
|
|
153
|
+
const raw = image_url.trim();
|
|
154
|
+
if (!raw)
|
|
155
|
+
throw new TypeError("ImagePlugin image content url is required");
|
|
156
|
+
return path.isAbsolute(raw) ? raw : path.resolve(root_path, raw);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 把本地图片读取为 data URL。
|
|
160
|
+
*/
|
|
161
|
+
async function local_image_to_data_url(input) {
|
|
162
|
+
const file_path = resolve_image_file_path(input.root_path, input.image_url);
|
|
163
|
+
const media_type = infer_image_media_type(file_path, input.media_type);
|
|
164
|
+
const bytes = await fs.readFile(file_path);
|
|
165
|
+
return {
|
|
166
|
+
data_url: `${media_type.includes("/") ? `data:${media_type};base64,` : "data:image/png;base64,"}${bytes.toString("base64")}`,
|
|
167
|
+
media_type,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 归一化单个图片内容片段。
|
|
172
|
+
*/
|
|
173
|
+
async function normalize_image_content_part(context, part) {
|
|
174
|
+
if (part.type === "text")
|
|
175
|
+
return part;
|
|
176
|
+
const url = String(part.url || "").trim();
|
|
177
|
+
if (!url)
|
|
178
|
+
throw new TypeError("ImagePlugin image content url is required");
|
|
179
|
+
if (url.startsWith("data:")) {
|
|
180
|
+
throw new TypeError("ImagePlugin content image url does not accept data URLs; pass an online URL or a local file path");
|
|
181
|
+
}
|
|
182
|
+
if (HTTP_URL_RE.test(url)) {
|
|
183
|
+
return {
|
|
184
|
+
type: "image",
|
|
185
|
+
url,
|
|
186
|
+
...(part.media_type ? { media_type: part.media_type } : {}),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const local = await local_image_to_data_url({
|
|
190
|
+
root_path: context.rootPath,
|
|
191
|
+
image_url: url,
|
|
192
|
+
media_type: part.media_type,
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
type: "image",
|
|
196
|
+
data_url: local.data_url,
|
|
197
|
+
media_type: local.media_type,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 拒绝旧版或内部协议字段,避免 Agent 继续依赖兼容层。
|
|
202
|
+
*/
|
|
203
|
+
function assert_public_image_create_input(input) {
|
|
204
|
+
const record = input;
|
|
205
|
+
if ("messages" in record) {
|
|
206
|
+
throw new TypeError("ImagePlugin image_create uses prompt or content; messages is not supported");
|
|
207
|
+
}
|
|
208
|
+
const content = record.content;
|
|
209
|
+
if (!Array.isArray(content))
|
|
210
|
+
return;
|
|
211
|
+
for (const part of content) {
|
|
212
|
+
const part_record = to_record(part);
|
|
213
|
+
if (part_record && "data_url" in part_record) {
|
|
214
|
+
throw new TypeError("ImagePlugin content image uses url only; data_url is not supported");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 复制公开输入中的通用字段,剥离 Agent 不应传给下游的公开 content。
|
|
220
|
+
*/
|
|
221
|
+
function copy_resolved_image_input(input) {
|
|
222
|
+
const { content: _content, messages: _messages, ...rest } = input;
|
|
223
|
+
return rest;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* 把 Agent 友好的公开输入转成 City 图片任务使用的输入。
|
|
227
|
+
*/
|
|
228
|
+
async function normalize_image_create_input(context, input) {
|
|
229
|
+
assert_public_image_create_input(input);
|
|
230
|
+
if (!Array.isArray(input.content))
|
|
231
|
+
return copy_resolved_image_input(input);
|
|
232
|
+
const content = await Promise.all(input.content.map((part) => normalize_image_content_part(context, part)));
|
|
233
|
+
const { prompt: _prompt, ...rest } = copy_resolved_image_input(input);
|
|
234
|
+
return {
|
|
235
|
+
...rest,
|
|
236
|
+
messages: [
|
|
237
|
+
{
|
|
238
|
+
role: "user",
|
|
239
|
+
content,
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
34
244
|
/**
|
|
35
245
|
* 归一化图片任务查询 payload。
|
|
36
246
|
*/
|
|
@@ -43,9 +253,20 @@ function normalize_image_result_payload(payload) {
|
|
|
43
253
|
if (!job_id) {
|
|
44
254
|
throw new TypeError("ImagePlugin.image_result payload must include job_id");
|
|
45
255
|
}
|
|
256
|
+
const until_done = record.until_done === true;
|
|
257
|
+
const max_wait_ms = typeof record.max_wait_ms === "number" && Number.isFinite(record.max_wait_ms)
|
|
258
|
+
? Math.max(0, Math.floor(record.max_wait_ms))
|
|
259
|
+
: undefined;
|
|
260
|
+
const poll_interval_ms = typeof record.poll_interval_ms === "number" &&
|
|
261
|
+
Number.isFinite(record.poll_interval_ms)
|
|
262
|
+
? Math.max(0, Math.floor(record.poll_interval_ms))
|
|
263
|
+
: undefined;
|
|
46
264
|
return {
|
|
47
265
|
...record,
|
|
48
266
|
job_id,
|
|
267
|
+
...(until_done ? { until_done: true } : {}),
|
|
268
|
+
...(max_wait_ms !== undefined ? { max_wait_ms } : {}),
|
|
269
|
+
...(poll_interval_ms !== undefined ? { poll_interval_ms } : {}),
|
|
49
270
|
};
|
|
50
271
|
}
|
|
51
272
|
/**
|
|
@@ -58,33 +279,6 @@ function normalize_image_result(result) {
|
|
|
58
279
|
}
|
|
59
280
|
return result;
|
|
60
281
|
}
|
|
61
|
-
/**
|
|
62
|
-
* 等待指定毫秒数。
|
|
63
|
-
*/
|
|
64
|
-
function sleep(ms) {
|
|
65
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* 限制轮询间隔,避免服务端异常值导致过快或过慢轮询。
|
|
69
|
-
*/
|
|
70
|
-
function clamp_poll_interval(value, min_ms, max_ms) {
|
|
71
|
-
const n = typeof value === "number" && Number.isFinite(value) ? value : min_ms;
|
|
72
|
-
return Math.max(min_ms, Math.min(max_ms, n));
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* 归一化正数配置。
|
|
76
|
-
*/
|
|
77
|
-
function normalize_positive_number(value, fallback) {
|
|
78
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
79
|
-
? value
|
|
80
|
-
: fallback;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* 读取可选布尔值。
|
|
84
|
-
*/
|
|
85
|
-
function normalize_boolean(value, fallback) {
|
|
86
|
-
return typeof value === "boolean" ? value : fallback;
|
|
87
|
-
}
|
|
88
282
|
/**
|
|
89
283
|
* 归一化模型元数据为 JSON 对象。
|
|
90
284
|
*/
|
|
@@ -196,9 +390,7 @@ export class ImagePlugin extends BasePlugin {
|
|
|
196
390
|
image_create;
|
|
197
391
|
image_result;
|
|
198
392
|
list_models;
|
|
199
|
-
|
|
200
|
-
min_poll_interval_ms;
|
|
201
|
-
max_poll_interval_ms;
|
|
393
|
+
default_model_id;
|
|
202
394
|
constructor(options) {
|
|
203
395
|
super();
|
|
204
396
|
const name = String(options.name || DEFAULT_IMAGE_PLUGIN_NAME).trim();
|
|
@@ -217,9 +409,6 @@ export class ImagePlugin extends BasePlugin {
|
|
|
217
409
|
this.image_create = options.image_create;
|
|
218
410
|
this.image_result = options.image_result;
|
|
219
411
|
this.list_models = options.list_models;
|
|
220
|
-
this.timeout_ms = normalize_positive_number(options.timeout_ms, DEFAULT_TIMEOUT_MS);
|
|
221
|
-
this.min_poll_interval_ms = normalize_positive_number(options.min_poll_interval_ms, DEFAULT_MIN_POLL_INTERVAL_MS);
|
|
222
|
-
this.max_poll_interval_ms = normalize_positive_number(options.max_poll_interval_ms, DEFAULT_MAX_POLL_INTERVAL_MS);
|
|
223
412
|
}
|
|
224
413
|
/**
|
|
225
414
|
* 图片插件给模型的使用说明。
|
|
@@ -228,102 +417,100 @@ export class ImagePlugin extends BasePlugin {
|
|
|
228
417
|
return [
|
|
229
418
|
"# Image Plugin",
|
|
230
419
|
"",
|
|
231
|
-
"Use this plugin only when the user asks to
|
|
232
|
-
"Do not call it for ordinary
|
|
420
|
+
"Use this plugin only when the user asks to create, edit, or otherwise produce an image.",
|
|
421
|
+
"Do not call it for ordinary text answers, even if the message mentions visual ideas.",
|
|
422
|
+
"",
|
|
423
|
+
"## Actions",
|
|
424
|
+
"",
|
|
425
|
+
"- `models`:列出当前可用的图片模型,返回 `{ models, default_model_id }`。",
|
|
426
|
+
" 仅当你需要让用户挑模型,或当前没有默认模型可用时才调用。",
|
|
427
|
+
"- `image_create`:创建一个异步图片任务,返回 `{ job_id, status }`。",
|
|
428
|
+
" - 纯文本生成:填 `prompt`(字符串)。",
|
|
429
|
+
" - 编辑 / 参考图:填 `content`,格式:",
|
|
430
|
+
" `[{ type: \"text\", text }, { type: \"image\", url }]`。可以多段文本和多张图任意顺序混排。",
|
|
431
|
+
" - 同时给了 `content` 和 `prompt` 时,`content` 生效,`prompt` 被忽略。",
|
|
432
|
+
" - `url` 支持三种写法:在线 URL、绝对本地路径、相对 Agent 项目根目录的相对路径。",
|
|
433
|
+
" 不要传 base64 / data URL,也不要再用旧的 `messages` 字段。",
|
|
434
|
+
" - 可选参数:`model`、`aspect_ratio`(如 `16:9`)、`size`(如 `1024x1024`)、`quality`、`seed`。",
|
|
435
|
+
"- `image_result`:用 `job_id` 读取任务状态。",
|
|
436
|
+
" - 默认读取一次。`queued` / `running` 时保存好 `job_id`,下一轮再查,不要在同一轮里反复轮询。",
|
|
437
|
+
" - 想直接等到出图,可以传 `until_done: true`,可选 `max_wait_ms`(默认 60000,最长 600000)与 `poll_interval_ms`(默认 1500)。超时仍未完成会返回最后一次的中间状态。",
|
|
438
|
+
" - `succeeded`:返回的图片 file part 会自动落盘并附加到下一条 assistant 消息,无需自己再拼图。",
|
|
439
|
+
" - `failed`:把 `error` 信息如实回报给用户,不要编造图片结果。",
|
|
233
440
|
"",
|
|
234
|
-
"
|
|
441
|
+
"## Flow",
|
|
235
442
|
"",
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
`
|
|
239
|
-
' action: "generate",',
|
|
240
|
-
" payload: {",
|
|
241
|
-
' prompt: "...",',
|
|
242
|
-
" },",
|
|
243
|
-
"});",
|
|
244
|
-
"```",
|
|
443
|
+
"1. 需要选模型时先 `models`,否则直接进入第 2 步。",
|
|
444
|
+
"2. `image_create` 拿到 `job_id`。",
|
|
445
|
+
"3. `image_result` 查询;短任务可加 `until_done: true` 一次拿结果,长任务保存 `job_id` 下一轮再查。",
|
|
245
446
|
"",
|
|
246
|
-
"
|
|
247
|
-
"- `prompt` is required unless `messages` provides the full multimodal image context.",
|
|
248
|
-
"- Optional common fields: `messages`, `size`, `aspect_ratio`, `ratio`, `quality`, `n`, `count`, `seed`, `provider_options`.",
|
|
249
|
-
"- To choose a model, call `models` and pass the selected model `id` as payload `model`.",
|
|
250
|
-
"- Preserve the user's creative intent; do not over-rewrite the prompt unless clarification is necessary.",
|
|
251
|
-
"- For a two-step flow, call `image_create` first, then call `image_result` with `job_id`; `image_result` waits until the job finishes by default.",
|
|
252
|
-
"- `generate` is a convenience action that creates the job and waits for the final image in one call.",
|
|
253
|
-
"- Generated image file parts are saved under project `.downcity/resources` and attached to the final assistant message automatically.",
|
|
447
|
+
"如有疑问可用 `plugin_read { plugin: \"image\", action: \"...\" }` 查看每个 action 完整的输入 schema 与示例。",
|
|
254
448
|
].join("\n");
|
|
255
449
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
450
|
+
/**
|
|
451
|
+
* 当调用方未显式指定模型时,使用模型目录中的图片默认模型。
|
|
452
|
+
*/
|
|
453
|
+
async with_default_model(input) {
|
|
454
|
+
if (typeof input.model === "string" && input.model.trim())
|
|
455
|
+
return input;
|
|
456
|
+
const default_model_id = await this.resolve_default_model_id();
|
|
457
|
+
return default_model_id ? { ...input, model: default_model_id } : input;
|
|
458
|
+
}
|
|
459
|
+
async resolve_default_model_id() {
|
|
460
|
+
if (this.default_model_id)
|
|
461
|
+
return this.default_model_id;
|
|
462
|
+
if (!this.list_models)
|
|
463
|
+
return undefined;
|
|
464
|
+
const result = normalize_image_models(await this.list_models());
|
|
465
|
+
this.default_model_id = result.default_model_id;
|
|
466
|
+
return this.default_model_id;
|
|
267
467
|
}
|
|
268
468
|
/**
|
|
269
|
-
*
|
|
469
|
+
* 查询图片任务当前状态。
|
|
270
470
|
*/
|
|
271
|
-
async read_image_result(input
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
const max_poll_interval_ms = normalize_positive_number(input.max_poll_interval_ms, this.max_poll_interval_ms);
|
|
275
|
-
const until_finish = normalize_boolean(input.until_finish, options.default_until_finish);
|
|
276
|
-
const first = await this.image_result({ job_id: input.job_id });
|
|
277
|
-
validate_job_result(first);
|
|
278
|
-
if (!until_finish || first.status === "succeeded") {
|
|
279
|
-
if (first.status === "succeeded" && first.result) {
|
|
280
|
-
normalize_image_result(first.result);
|
|
281
|
-
}
|
|
471
|
+
async read_image_result(input) {
|
|
472
|
+
const first = await this.fetch_job_once(input.job_id);
|
|
473
|
+
if (!input.until_done)
|
|
282
474
|
return first;
|
|
475
|
+
if (is_terminal_status(first.status))
|
|
476
|
+
return first;
|
|
477
|
+
const deadline = Date.now() + clamp_wait_ms(input.max_wait_ms ?? DEFAULT_IMAGE_WAIT_MS);
|
|
478
|
+
const base_interval = clamp_wait_ms(input.poll_interval_ms ?? DEFAULT_IMAGE_POLL_MS);
|
|
479
|
+
let current = first;
|
|
480
|
+
while (Date.now() < deadline) {
|
|
481
|
+
const provider_hint = typeof current.poll_after_ms === "number" && current.poll_after_ms > 0
|
|
482
|
+
? Math.floor(current.poll_after_ms)
|
|
483
|
+
: 0;
|
|
484
|
+
const wait_ms = Math.max(base_interval, provider_hint);
|
|
485
|
+
const remaining = Math.max(0, deadline - Date.now());
|
|
486
|
+
const sleep_ms = Math.min(wait_ms, remaining);
|
|
487
|
+
if (sleep_ms === 0)
|
|
488
|
+
break;
|
|
489
|
+
await delay_ms(sleep_ms);
|
|
490
|
+
current = await this.fetch_job_once(input.job_id);
|
|
491
|
+
if (is_terminal_status(current.status))
|
|
492
|
+
return current;
|
|
283
493
|
}
|
|
284
|
-
|
|
285
|
-
throw new Error(`Image job failed: ${first.error ?? first.message ?? input.job_id}`);
|
|
286
|
-
}
|
|
287
|
-
return await this.wait_for_image_result({
|
|
288
|
-
job_id: input.job_id,
|
|
289
|
-
poll_after_ms: first.poll_after_ms,
|
|
290
|
-
timeout_ms,
|
|
291
|
-
min_poll_interval_ms,
|
|
292
|
-
max_poll_interval_ms,
|
|
293
|
-
});
|
|
494
|
+
return current;
|
|
294
495
|
}
|
|
295
496
|
/**
|
|
296
|
-
*
|
|
497
|
+
* 拉取一次任务状态并校验。
|
|
297
498
|
*/
|
|
298
|
-
async
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
let poll_after_ms = input.poll_after_ms;
|
|
304
|
-
while (Date.now() < deadline) {
|
|
305
|
-
await sleep(clamp_poll_interval(poll_after_ms, min_poll_interval_ms, max_poll_interval_ms));
|
|
306
|
-
const current = await this.image_result({ job_id: input.job_id });
|
|
307
|
-
validate_job_result(current);
|
|
308
|
-
poll_after_ms = current.poll_after_ms;
|
|
309
|
-
if (current.status === "succeeded") {
|
|
310
|
-
if (!current.result) {
|
|
311
|
-
throw new Error(`Image job ${input.job_id} succeeded without result`);
|
|
312
|
-
}
|
|
313
|
-
normalize_image_result(current.result);
|
|
314
|
-
return current;
|
|
315
|
-
}
|
|
316
|
-
if (current.status === "failed") {
|
|
317
|
-
throw new Error(`Image job failed: ${current.error ?? current.message ?? input.job_id}`);
|
|
318
|
-
}
|
|
499
|
+
async fetch_job_once(job_id) {
|
|
500
|
+
const current = await this.image_result({ job_id });
|
|
501
|
+
validate_job_result(current);
|
|
502
|
+
if (current.status === "succeeded" && current.result) {
|
|
503
|
+
normalize_image_result(current.result);
|
|
319
504
|
}
|
|
320
|
-
|
|
505
|
+
return current;
|
|
321
506
|
}
|
|
322
507
|
/**
|
|
323
508
|
* 显式 action 集合。
|
|
324
509
|
*/
|
|
325
510
|
actions = {
|
|
326
|
-
models: {
|
|
511
|
+
models: createAction({
|
|
512
|
+
description: "List image-capable models available to ImagePlugin.",
|
|
513
|
+
input_schema: z.object({}).passthrough(),
|
|
327
514
|
execute: async () => {
|
|
328
515
|
try {
|
|
329
516
|
if (!this.list_models) {
|
|
@@ -344,17 +531,83 @@ export class ImagePlugin extends BasePlugin {
|
|
|
344
531
|
catch (error) {
|
|
345
532
|
return {
|
|
346
533
|
success: false,
|
|
347
|
-
error:
|
|
348
|
-
message:
|
|
534
|
+
error: describe_error(error),
|
|
535
|
+
message: describe_error(error),
|
|
349
536
|
};
|
|
350
537
|
}
|
|
351
538
|
},
|
|
352
|
-
},
|
|
353
|
-
image_create: {
|
|
354
|
-
|
|
539
|
+
}),
|
|
540
|
+
image_create: createAction({
|
|
541
|
+
description: "Create an async image job. Use prompt for text-only generation, or content for reference images and edits.",
|
|
542
|
+
input_schema: {
|
|
543
|
+
zod: IMAGE_CREATE_INPUT_SCHEMA,
|
|
544
|
+
json_schema: {
|
|
545
|
+
type: "object",
|
|
546
|
+
additionalProperties: true,
|
|
547
|
+
properties: {
|
|
548
|
+
model: { type: "string", description: "Image model id." },
|
|
549
|
+
prompt: {
|
|
550
|
+
type: "string",
|
|
551
|
+
description: "Text-only image prompt. Ignored when content is present.",
|
|
552
|
+
},
|
|
553
|
+
content: {
|
|
554
|
+
type: "array",
|
|
555
|
+
description: "Multimodal content for image edits or reference images.",
|
|
556
|
+
items: {
|
|
557
|
+
oneOf: [
|
|
558
|
+
{
|
|
559
|
+
type: "object",
|
|
560
|
+
required: ["type", "text"],
|
|
561
|
+
properties: {
|
|
562
|
+
type: { const: "text" },
|
|
563
|
+
text: { type: "string" },
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
type: "object",
|
|
568
|
+
required: ["type", "url"],
|
|
569
|
+
properties: {
|
|
570
|
+
type: { const: "image" },
|
|
571
|
+
url: {
|
|
572
|
+
type: "string",
|
|
573
|
+
description: "Online URL, absolute local path, or path relative to the Agent project root.",
|
|
574
|
+
},
|
|
575
|
+
media_type: { type: "string" },
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
aspect_ratio: { type: "string", description: "Aspect ratio, for example 16:9." },
|
|
582
|
+
size: { type: "string", description: "Image size, for example 1024x1024." },
|
|
583
|
+
quality: { type: "string", description: "Image quality." },
|
|
584
|
+
seed: { type: "number", description: "Random seed." },
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
examples: [
|
|
589
|
+
{
|
|
590
|
+
title: "Text-only image",
|
|
591
|
+
payload: {
|
|
592
|
+
prompt: "A cinematic illustration of a rainy city corner at night",
|
|
593
|
+
aspect_ratio: "16:9",
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
title: "Edit image with local reference",
|
|
598
|
+
payload: {
|
|
599
|
+
content: [
|
|
600
|
+
{ type: "text", text: "Change this image to a white studio background" },
|
|
601
|
+
{ type: "image", url: "./input.png" },
|
|
602
|
+
],
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
],
|
|
606
|
+
execute: async ({ context, payload }) => {
|
|
355
607
|
try {
|
|
356
608
|
const input = normalize_image_payload(payload);
|
|
357
|
-
const
|
|
609
|
+
const normalized_input = await normalize_image_create_input(context, input);
|
|
610
|
+
const created = await this.image_create(await this.with_default_model(normalized_input));
|
|
358
611
|
validate_created_job(created);
|
|
359
612
|
return {
|
|
360
613
|
success: true,
|
|
@@ -365,19 +618,67 @@ export class ImagePlugin extends BasePlugin {
|
|
|
365
618
|
catch (error) {
|
|
366
619
|
return {
|
|
367
620
|
success: false,
|
|
368
|
-
error:
|
|
369
|
-
message:
|
|
621
|
+
error: describe_error(error),
|
|
622
|
+
message: describe_error(error),
|
|
370
623
|
};
|
|
371
624
|
}
|
|
372
625
|
},
|
|
373
|
-
},
|
|
374
|
-
image_result: {
|
|
626
|
+
}),
|
|
627
|
+
image_result: createAction({
|
|
628
|
+
description: "Read an async image job. By default reads once; pass until_done=true to block-wait until succeeded/failed or max_wait_ms times out.",
|
|
629
|
+
input_schema: {
|
|
630
|
+
zod: IMAGE_RESULT_INPUT_SCHEMA,
|
|
631
|
+
json_schema: {
|
|
632
|
+
type: "object",
|
|
633
|
+
required: ["job_id"],
|
|
634
|
+
properties: {
|
|
635
|
+
job_id: {
|
|
636
|
+
type: "string",
|
|
637
|
+
description: "Image job id returned by image_create.",
|
|
638
|
+
},
|
|
639
|
+
until_done: {
|
|
640
|
+
type: "boolean",
|
|
641
|
+
description: "If true, the plugin internally polls until the job reaches a terminal state (succeeded/failed) or max_wait_ms elapses.",
|
|
642
|
+
},
|
|
643
|
+
max_wait_ms: {
|
|
644
|
+
type: "number",
|
|
645
|
+
description: "Total wait budget in milliseconds when until_done is true. Default 60000, hard cap 600000.",
|
|
646
|
+
},
|
|
647
|
+
poll_interval_ms: {
|
|
648
|
+
type: "number",
|
|
649
|
+
description: "Minimum delay between polls in milliseconds when until_done is true. Default 1500. Provider poll_after_ms overrides when larger.",
|
|
650
|
+
},
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
examples: [
|
|
655
|
+
{
|
|
656
|
+
title: "Read image job",
|
|
657
|
+
payload: {
|
|
658
|
+
job_id: "img_123",
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
title: "Wait until done",
|
|
663
|
+
payload: {
|
|
664
|
+
job_id: "img_123",
|
|
665
|
+
until_done: true,
|
|
666
|
+
max_wait_ms: 30000,
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
],
|
|
375
670
|
execute: async ({ payload }) => {
|
|
376
671
|
try {
|
|
377
672
|
const input = normalize_image_result_payload(payload);
|
|
378
|
-
const current = await this.read_image_result(input
|
|
379
|
-
|
|
380
|
-
|
|
673
|
+
const current = await this.read_image_result(input);
|
|
674
|
+
if (current.status === "failed") {
|
|
675
|
+
return {
|
|
676
|
+
success: false,
|
|
677
|
+
data: current,
|
|
678
|
+
error: current.error ?? current.message ?? input.job_id,
|
|
679
|
+
message: current.error ?? current.message ?? "image job failed",
|
|
680
|
+
};
|
|
681
|
+
}
|
|
381
682
|
const data = current.status === "succeeded" && current.result
|
|
382
683
|
? current.result
|
|
383
684
|
: current;
|
|
@@ -392,32 +693,12 @@ export class ImagePlugin extends BasePlugin {
|
|
|
392
693
|
catch (error) {
|
|
393
694
|
return {
|
|
394
695
|
success: false,
|
|
395
|
-
error:
|
|
396
|
-
message:
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
|
-
},
|
|
401
|
-
generate: {
|
|
402
|
-
execute: async ({ payload }) => {
|
|
403
|
-
try {
|
|
404
|
-
const input = normalize_image_payload(payload);
|
|
405
|
-
const message = await this.generate_image(input);
|
|
406
|
-
return {
|
|
407
|
-
success: true,
|
|
408
|
-
data: message,
|
|
409
|
-
message: "image generated",
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
catch (error) {
|
|
413
|
-
return {
|
|
414
|
-
success: false,
|
|
415
|
-
error: String(error),
|
|
416
|
-
message: String(error),
|
|
696
|
+
error: describe_error(error),
|
|
697
|
+
message: describe_error(error),
|
|
417
698
|
};
|
|
418
699
|
}
|
|
419
700
|
},
|
|
420
|
-
},
|
|
701
|
+
}),
|
|
421
702
|
};
|
|
422
703
|
}
|
|
423
704
|
//# sourceMappingURL=ImagePlugin.js.map
|