@downcity/agent 1.1.92 → 1.1.97
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/index.d.ts +1 -1
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js.map +1 -1
- package/bin/plugin/core/ImagePlugin.d.ts +5 -63
- package/bin/plugin/core/ImagePlugin.d.ts.map +1 -1
- package/bin/plugin/core/ImagePlugin.js +27 -204
- package/bin/plugin/core/ImagePlugin.js.map +1 -1
- package/bin/types/plugin/ImagePlugin.d.ts +8 -33
- package/bin/types/plugin/ImagePlugin.d.ts.map +1 -1
- package/package.json +2 -2
- package/scripts/image-plugin-job.test.mjs +46 -100
- package/src/index.ts +0 -2
- package/src/plugin/core/ImagePlugin.ts +30 -257
- package/src/types/plugin/ImagePlugin.ts +8 -32
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file 验证 ImagePlugin
|
|
2
|
+
* @file 验证 ImagePlugin 对 Agent 保持直接生图体验。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
|
-
* - `
|
|
6
|
-
* -
|
|
5
|
+
* - Agent 只调用 `generate` action。
|
|
6
|
+
* - 插件内部负责 image_create/image_result 轮询。
|
|
7
|
+
* - 成功后返回 UIMessage,后续由 plugin bridge 落盘 file parts。
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import test from "node:test";
|
|
@@ -26,130 +27,75 @@ function create_image_message() {
|
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
test("ImagePlugin create/
|
|
30
|
-
|
|
31
|
-
const image_promise = new Promise((resolve) => {
|
|
32
|
-
finish_image = () => resolve(create_image_message());
|
|
33
|
-
});
|
|
30
|
+
test("ImagePlugin generate polls create/result until the image succeeds", async () => {
|
|
31
|
+
const calls = [];
|
|
34
32
|
const plugin = new ImagePlugin({
|
|
35
|
-
|
|
33
|
+
create: (input) => {
|
|
34
|
+
calls.push(["create", input.prompt]);
|
|
35
|
+
return {
|
|
36
|
+
job_id: "img_custom",
|
|
37
|
+
status: "running",
|
|
38
|
+
poll_after_ms: 1,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
result: () => {
|
|
42
|
+
calls.push(["result"]);
|
|
43
|
+
return calls.filter(([name]) => name === "result").length === 1
|
|
44
|
+
? {
|
|
45
|
+
job_id: "img_custom",
|
|
46
|
+
status: "running",
|
|
47
|
+
poll_after_ms: 1,
|
|
48
|
+
}
|
|
49
|
+
: {
|
|
50
|
+
job_id: "img_custom",
|
|
51
|
+
status: "succeeded",
|
|
52
|
+
result: create_image_message(),
|
|
53
|
+
};
|
|
54
|
+
},
|
|
36
55
|
poll_interval_ms: 1,
|
|
37
56
|
wait_timeout_ms: 100,
|
|
38
57
|
});
|
|
39
58
|
|
|
40
|
-
const
|
|
59
|
+
const result = await plugin.actions.generate.execute({
|
|
41
60
|
context: {},
|
|
42
61
|
payload: { prompt: "draw" },
|
|
43
62
|
pluginName: "image",
|
|
44
|
-
actionName: "
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
assert.equal(created.success, true);
|
|
48
|
-
assert.equal(created.data.status, "running");
|
|
49
|
-
assert.match(created.data.job_id, /^img_/);
|
|
50
|
-
|
|
51
|
-
const before = await plugin.actions.status.execute({
|
|
52
|
-
context: {},
|
|
53
|
-
payload: { job_id: created.data.job_id },
|
|
54
|
-
pluginName: "image",
|
|
55
|
-
actionName: "status",
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
assert.equal(before.success, true);
|
|
59
|
-
assert.equal(before.data.status, "running");
|
|
60
|
-
|
|
61
|
-
finish_image();
|
|
62
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
63
|
-
|
|
64
|
-
const after = await plugin.actions.status.execute({
|
|
65
|
-
context: {},
|
|
66
|
-
payload: { job_id: created.data.job_id },
|
|
67
|
-
pluginName: "image",
|
|
68
|
-
actionName: "status",
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
assert.equal(after.success, true);
|
|
72
|
-
assert.equal(after.data.status, "succeeded");
|
|
73
|
-
assert.equal("result" in after.data, false);
|
|
74
|
-
|
|
75
|
-
const result = await plugin.actions.result.execute({
|
|
76
|
-
context: {},
|
|
77
|
-
payload: { job_id: created.data.job_id },
|
|
78
|
-
pluginName: "image",
|
|
79
|
-
actionName: "result",
|
|
63
|
+
actionName: "generate",
|
|
80
64
|
});
|
|
81
65
|
|
|
82
66
|
assert.equal(result.success, true);
|
|
83
67
|
assert.equal(result.data.role, "assistant");
|
|
84
68
|
assert.equal(result.data.parts[0].type, "file");
|
|
69
|
+
assert.deepEqual(calls, [
|
|
70
|
+
["create", "draw"],
|
|
71
|
+
["result"],
|
|
72
|
+
["result"],
|
|
73
|
+
]);
|
|
85
74
|
});
|
|
86
75
|
|
|
87
|
-
test("ImagePlugin
|
|
88
|
-
const message = create_image_message();
|
|
76
|
+
test("ImagePlugin generate reports job failure", async () => {
|
|
89
77
|
const plugin = new ImagePlugin({
|
|
90
78
|
create: () => ({
|
|
91
|
-
job_id: "
|
|
79
|
+
job_id: "img_failed",
|
|
92
80
|
status: "running",
|
|
93
81
|
poll_after_ms: 1,
|
|
94
82
|
}),
|
|
95
|
-
status: () => ({
|
|
96
|
-
job_id: "img_custom",
|
|
97
|
-
status: "succeeded",
|
|
98
|
-
}),
|
|
99
83
|
result: () => ({
|
|
100
|
-
job_id: "
|
|
101
|
-
status: "
|
|
102
|
-
|
|
84
|
+
job_id: "img_failed",
|
|
85
|
+
status: "failed",
|
|
86
|
+
error: "provider failed",
|
|
103
87
|
}),
|
|
88
|
+
poll_interval_ms: 1,
|
|
89
|
+
wait_timeout_ms: 100,
|
|
104
90
|
});
|
|
105
91
|
|
|
106
|
-
const
|
|
92
|
+
const result = await plugin.actions.generate.execute({
|
|
107
93
|
context: {},
|
|
108
94
|
payload: { prompt: "draw" },
|
|
109
95
|
pluginName: "image",
|
|
110
|
-
actionName: "
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
assert.equal(created.success, true);
|
|
114
|
-
assert.equal(created.data.job_id, "img_custom");
|
|
115
|
-
|
|
116
|
-
const result = await plugin.actions.result.execute({
|
|
117
|
-
context: {},
|
|
118
|
-
payload: { job_id: "img_custom" },
|
|
119
|
-
pluginName: "image",
|
|
120
|
-
actionName: "result",
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
assert.equal(result.success, true);
|
|
124
|
-
assert.equal(result.data.parts[0].type, "file");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("ImagePlugin strips accidental result data from custom status responses", async () => {
|
|
128
|
-
const plugin = new ImagePlugin({
|
|
129
|
-
create: () => ({
|
|
130
|
-
job_id: "img_status_result",
|
|
131
|
-
status: "succeeded",
|
|
132
|
-
}),
|
|
133
|
-
status: () => ({
|
|
134
|
-
job_id: "img_status_result",
|
|
135
|
-
status: "succeeded",
|
|
136
|
-
result: create_image_message(),
|
|
137
|
-
}),
|
|
138
|
-
result: () => ({
|
|
139
|
-
job_id: "img_status_result",
|
|
140
|
-
status: "succeeded",
|
|
141
|
-
result: create_image_message(),
|
|
142
|
-
}),
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const status = await plugin.actions.status.execute({
|
|
146
|
-
context: {},
|
|
147
|
-
payload: { job_id: "img_status_result" },
|
|
148
|
-
pluginName: "image",
|
|
149
|
-
actionName: "status",
|
|
96
|
+
actionName: "generate",
|
|
150
97
|
});
|
|
151
98
|
|
|
152
|
-
assert.equal(
|
|
153
|
-
assert.
|
|
154
|
-
assert.equal("result" in status.data, false);
|
|
99
|
+
assert.equal(result.success, false);
|
|
100
|
+
assert.match(result.error, /provider failed/);
|
|
155
101
|
});
|
package/src/index.ts
CHANGED
|
@@ -232,10 +232,8 @@ export type {
|
|
|
232
232
|
ImagePluginJobCreateResult,
|
|
233
233
|
ImagePluginJobResult,
|
|
234
234
|
ImagePluginJobStatus,
|
|
235
|
-
ImagePluginJobStatusResult,
|
|
236
235
|
ImagePluginMessage,
|
|
237
236
|
ImagePluginOptions,
|
|
238
|
-
ImagePluginResult,
|
|
239
237
|
ImagePluginTextContent,
|
|
240
238
|
} from "./types/plugin/ImagePlugin.js";
|
|
241
239
|
|
|
@@ -2,19 +2,15 @@
|
|
|
2
2
|
* ImagePlugin:Agent 内置图片生成插件。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
5
|
+
* - 对 Agent 只暴露同步体验的 `generate` action。
|
|
6
|
+
* - City / provider 的异步任务细节由插件内部 create + result 轮询封装。
|
|
7
7
|
* - action 返回 AI SDK UIMessage,后续由 plugin tool bridge 抽取 file parts 写回 assistant 消息。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import crypto from "node:crypto";
|
|
11
10
|
import type { AgentContext } from "@/types/runtime/agent/AgentContext.js";
|
|
12
11
|
import type { JsonObject, JsonValue } from "@/types/common/Json.js";
|
|
13
12
|
import type {
|
|
14
13
|
ImagePluginInput,
|
|
15
|
-
ImagePluginJobCreateResult,
|
|
16
|
-
ImagePluginJobResult,
|
|
17
|
-
ImagePluginJobStatusResult,
|
|
18
14
|
ImagePluginOptions,
|
|
19
15
|
ImagePluginResult,
|
|
20
16
|
} from "@/types/plugin/ImagePlugin.js";
|
|
@@ -27,37 +23,6 @@ const DEFAULT_IMAGE_PLUGIN_DESCRIPTION =
|
|
|
27
23
|
const DEFAULT_WAIT_TIMEOUT_MS = 60_000;
|
|
28
24
|
const DEFAULT_POLL_INTERVAL_MS = 3_000;
|
|
29
25
|
|
|
30
|
-
type LocalImageJobRecord = {
|
|
31
|
-
/**
|
|
32
|
-
* 图片任务唯一 ID。
|
|
33
|
-
*/
|
|
34
|
-
job_id: string;
|
|
35
|
-
/**
|
|
36
|
-
* 当前任务状态。
|
|
37
|
-
*/
|
|
38
|
-
status: "queued" | "running" | "succeeded" | "failed";
|
|
39
|
-
/**
|
|
40
|
-
* 成功时的图片结果。
|
|
41
|
-
*/
|
|
42
|
-
result?: ImagePluginResult;
|
|
43
|
-
/**
|
|
44
|
-
* 失败时的错误信息。
|
|
45
|
-
*/
|
|
46
|
-
error?: string;
|
|
47
|
-
/**
|
|
48
|
-
* 人类可读状态说明。
|
|
49
|
-
*/
|
|
50
|
-
message?: string;
|
|
51
|
-
/**
|
|
52
|
-
* 任务创建时间。
|
|
53
|
-
*/
|
|
54
|
-
created_at: string;
|
|
55
|
-
/**
|
|
56
|
-
* 任务更新时间。
|
|
57
|
-
*/
|
|
58
|
-
updated_at: string;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
26
|
/**
|
|
62
27
|
* 判断值是否为普通对象。
|
|
63
28
|
*/
|
|
@@ -77,43 +42,22 @@ function normalize_image_payload(payload: JsonValue | undefined): ImagePluginInp
|
|
|
77
42
|
return { ...record } as ImagePluginInput;
|
|
78
43
|
}
|
|
79
44
|
|
|
80
|
-
function normalize_job_id_payload(payload: JsonValue | undefined): { job_id: string } {
|
|
81
|
-
const record = to_record(payload ?? {});
|
|
82
|
-
const job_id = String(record?.job_id || "").trim();
|
|
83
|
-
if (!job_id) {
|
|
84
|
-
throw new TypeError("ImagePlugin job action requires job_id");
|
|
85
|
-
}
|
|
86
|
-
return { job_id };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
45
|
/**
|
|
90
46
|
* 校验 image 函数返回的 UIMessage。
|
|
91
47
|
*/
|
|
92
48
|
function normalize_image_result(result: ImagePluginResult): ImagePluginResult {
|
|
93
49
|
const record = to_record(result);
|
|
94
50
|
if (!record || !Array.isArray(record.parts)) {
|
|
95
|
-
throw new TypeError("ImagePlugin image
|
|
51
|
+
throw new TypeError("ImagePlugin image provider must return an AI SDK UIMessage");
|
|
96
52
|
}
|
|
97
53
|
return result;
|
|
98
54
|
}
|
|
99
55
|
|
|
100
56
|
/**
|
|
101
|
-
*
|
|
57
|
+
* 等待指定毫秒数。
|
|
102
58
|
*/
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
): ImagePluginJobStatusResult {
|
|
106
|
-
return {
|
|
107
|
-
job_id: result.job_id,
|
|
108
|
-
status: result.status,
|
|
109
|
-
...(result.message ? { message: result.message } : {}),
|
|
110
|
-
...(result.error ? { error: result.error } : {}),
|
|
111
|
-
...(typeof result.poll_after_ms === "number"
|
|
112
|
-
? { poll_after_ms: result.poll_after_ms }
|
|
113
|
-
: {}),
|
|
114
|
-
...(result.created_at ? { created_at: result.created_at } : {}),
|
|
115
|
-
...(result.updated_at ? { updated_at: result.updated_at } : {}),
|
|
116
|
-
};
|
|
59
|
+
function sleep(ms: number): Promise<void> {
|
|
60
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
117
61
|
}
|
|
118
62
|
|
|
119
63
|
/**
|
|
@@ -135,13 +79,10 @@ export class ImagePlugin extends BasePlugin {
|
|
|
135
79
|
*/
|
|
136
80
|
readonly description: string;
|
|
137
81
|
|
|
138
|
-
private readonly
|
|
139
|
-
private readonly
|
|
140
|
-
private readonly read_job_status?: ImagePluginOptions["status"];
|
|
141
|
-
private readonly read_job_result?: ImagePluginOptions["result"];
|
|
82
|
+
private readonly create_job: NonNullable<ImagePluginOptions["create"]>;
|
|
83
|
+
private readonly read_job_result: NonNullable<ImagePluginOptions["result"]>;
|
|
142
84
|
private readonly wait_timeout_ms: number;
|
|
143
85
|
private readonly poll_interval_ms: number;
|
|
144
|
-
private readonly local_jobs = new Map<string, LocalImageJobRecord>();
|
|
145
86
|
|
|
146
87
|
constructor(options: ImagePluginOptions) {
|
|
147
88
|
super();
|
|
@@ -149,32 +90,15 @@ export class ImagePlugin extends BasePlugin {
|
|
|
149
90
|
if (!name) {
|
|
150
91
|
throw new Error("ImagePlugin requires a non-empty name");
|
|
151
92
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
);
|
|
155
|
-
if (
|
|
156
|
-
has_custom_job_api &&
|
|
157
|
-
(typeof options.create !== "function" ||
|
|
158
|
-
typeof options.status !== "function" ||
|
|
159
|
-
typeof options.result !== "function")
|
|
160
|
-
) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
"ImagePlugin custom job API requires create, status, and result functions",
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
if (!has_custom_job_api && typeof options.image !== "function") {
|
|
166
|
-
throw new Error(
|
|
167
|
-
"ImagePlugin requires either image(input) or create/status/result functions",
|
|
168
|
-
);
|
|
93
|
+
if (typeof options.create !== "function" || typeof options.result !== "function") {
|
|
94
|
+
throw new Error("ImagePlugin requires create and result functions");
|
|
169
95
|
}
|
|
170
96
|
this.name = name;
|
|
171
97
|
this.title = String(options.title || DEFAULT_IMAGE_PLUGIN_TITLE).trim();
|
|
172
98
|
this.description = String(
|
|
173
99
|
options.description || DEFAULT_IMAGE_PLUGIN_DESCRIPTION,
|
|
174
100
|
).trim();
|
|
175
|
-
this.image = options.image;
|
|
176
101
|
this.create_job = options.create;
|
|
177
|
-
this.read_job_status = options.status;
|
|
178
102
|
this.read_job_result = options.result;
|
|
179
103
|
this.wait_timeout_ms =
|
|
180
104
|
typeof options.wait_timeout_ms === "number" && options.wait_timeout_ms > 0
|
|
@@ -191,108 +115,38 @@ export class ImagePlugin extends BasePlugin {
|
|
|
191
115
|
*/
|
|
192
116
|
system(_context: AgentContext): string {
|
|
193
117
|
return [
|
|
194
|
-
"Image generation is available through the plugin_call tool
|
|
195
|
-
`Call plugin "${this.name}" action "
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"Use action \"generate\" only as a compatibility shortcut when you explicitly need to wait for completion.",
|
|
199
|
-
"Pass a JSON payload with prompt, optional size/aspect_ratio/quality/n, and optional provider_options to create/generate.",
|
|
118
|
+
"Image generation is available through the plugin_call tool.",
|
|
119
|
+
`Call plugin "${this.name}" action "generate" when the user asks to create, render, draw, or edit an image.`,
|
|
120
|
+
"Pass a JSON payload with prompt, optional size/aspect_ratio/quality/n, and optional provider_options.",
|
|
121
|
+
"The generated image files will be attached to the final assistant message automatically.",
|
|
200
122
|
].join("\n");
|
|
201
123
|
}
|
|
202
124
|
|
|
203
|
-
private
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const job_id = `img_${crypto.randomUUID()}`;
|
|
209
|
-
const record: LocalImageJobRecord = {
|
|
210
|
-
job_id,
|
|
211
|
-
status: "running",
|
|
212
|
-
message: "image job is running",
|
|
213
|
-
created_at: now,
|
|
214
|
-
updated_at: now,
|
|
215
|
-
};
|
|
216
|
-
this.local_jobs.set(job_id, record);
|
|
217
|
-
|
|
218
|
-
void this.run_local_job(record, input, this.image);
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
job_id,
|
|
222
|
-
status: record.status,
|
|
223
|
-
message: record.message,
|
|
224
|
-
poll_after_ms: this.poll_interval_ms,
|
|
225
|
-
created_at: record.created_at,
|
|
226
|
-
updated_at: record.updated_at,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
private async run_local_job(
|
|
231
|
-
record: LocalImageJobRecord,
|
|
232
|
-
input: ImagePluginInput,
|
|
233
|
-
image: NonNullable<ImagePluginOptions["image"]>,
|
|
234
|
-
): Promise<void> {
|
|
235
|
-
try {
|
|
236
|
-
const message = normalize_image_result(await image(input));
|
|
237
|
-
record.status = "succeeded";
|
|
238
|
-
record.result = message;
|
|
239
|
-
record.message = "image job succeeded";
|
|
240
|
-
record.updated_at = new Date().toISOString();
|
|
241
|
-
} catch (error) {
|
|
242
|
-
record.status = "failed";
|
|
243
|
-
record.error = String(error);
|
|
244
|
-
record.message = "image job failed";
|
|
245
|
-
record.updated_at = new Date().toISOString();
|
|
125
|
+
private async generate_image(input: ImagePluginInput): Promise<ImagePluginResult> {
|
|
126
|
+
const job = await this.create_job(input);
|
|
127
|
+
const job_id = String(job.job_id || "").trim();
|
|
128
|
+
if (!job_id) {
|
|
129
|
+
throw new Error("ImagePlugin image_create result requires job_id");
|
|
246
130
|
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
private read_local_job(job_id: string): LocalImageJobRecord {
|
|
250
|
-
const record = this.local_jobs.get(job_id);
|
|
251
|
-
if (!record) {
|
|
252
|
-
throw new Error(`Unknown image job: ${job_id}`);
|
|
253
|
-
}
|
|
254
|
-
return record;
|
|
255
|
-
}
|
|
256
131
|
|
|
257
|
-
private serialize_local_status(record: LocalImageJobRecord): ImagePluginJobStatusResult {
|
|
258
|
-
return {
|
|
259
|
-
job_id: record.job_id,
|
|
260
|
-
status: record.status,
|
|
261
|
-
...(record.message ? { message: record.message } : {}),
|
|
262
|
-
...(record.error ? { error: record.error } : {}),
|
|
263
|
-
...(record.status === "running" || record.status === "queued"
|
|
264
|
-
? { poll_after_ms: this.poll_interval_ms }
|
|
265
|
-
: {}),
|
|
266
|
-
created_at: record.created_at,
|
|
267
|
-
updated_at: record.updated_at,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
private serialize_local_result(record: LocalImageJobRecord): ImagePluginJobResult {
|
|
272
|
-
return {
|
|
273
|
-
job_id: record.job_id,
|
|
274
|
-
status: record.status,
|
|
275
|
-
...(record.result ? { result: record.result } : {}),
|
|
276
|
-
...(record.error ? { error: record.error } : {}),
|
|
277
|
-
...(record.message ? { message: record.message } : {}),
|
|
278
|
-
created_at: record.created_at,
|
|
279
|
-
updated_at: record.updated_at,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private async wait_for_job(job_id: string): Promise<ImagePluginResult> {
|
|
284
132
|
const deadline = Date.now() + this.wait_timeout_ms;
|
|
133
|
+
let poll_after_ms =
|
|
134
|
+
typeof job.poll_after_ms === "number" && job.poll_after_ms > 0
|
|
135
|
+
? job.poll_after_ms
|
|
136
|
+
: this.poll_interval_ms;
|
|
285
137
|
while (Date.now() <= deadline) {
|
|
286
|
-
const result = this.read_job_result
|
|
287
|
-
? await this.read_job_result({ job_id })
|
|
288
|
-
: this.serialize_local_result(this.read_local_job(job_id));
|
|
138
|
+
const result = await this.read_job_result({ job_id });
|
|
289
139
|
if (result.status === "succeeded" && result.result) {
|
|
290
140
|
return normalize_image_result(result.result);
|
|
291
141
|
}
|
|
292
142
|
if (result.status === "failed") {
|
|
293
143
|
throw new Error(result.error || result.message || "image job failed");
|
|
294
144
|
}
|
|
295
|
-
|
|
145
|
+
poll_after_ms =
|
|
146
|
+
typeof result.poll_after_ms === "number" && result.poll_after_ms > 0
|
|
147
|
+
? result.poll_after_ms
|
|
148
|
+
: this.poll_interval_ms;
|
|
149
|
+
await sleep(poll_after_ms);
|
|
296
150
|
}
|
|
297
151
|
throw new Error(`image job timed out: ${job_id}`);
|
|
298
152
|
}
|
|
@@ -301,92 +155,11 @@ export class ImagePlugin extends BasePlugin {
|
|
|
301
155
|
* 显式 action 集合。
|
|
302
156
|
*/
|
|
303
157
|
readonly actions = {
|
|
304
|
-
create: {
|
|
305
|
-
execute: async ({ payload }: { payload: JsonValue }) => {
|
|
306
|
-
try {
|
|
307
|
-
const input = normalize_image_payload(payload);
|
|
308
|
-
const result = this.create_job
|
|
309
|
-
? await this.create_job(input)
|
|
310
|
-
: this.create_local_job(input);
|
|
311
|
-
return {
|
|
312
|
-
success: true,
|
|
313
|
-
data: result as unknown as JsonObject,
|
|
314
|
-
message: result.message || "image job created",
|
|
315
|
-
};
|
|
316
|
-
} catch (error) {
|
|
317
|
-
return {
|
|
318
|
-
success: false,
|
|
319
|
-
error: String(error),
|
|
320
|
-
message: String(error),
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
status: {
|
|
326
|
-
execute: async ({ payload }: { payload: JsonValue }) => {
|
|
327
|
-
try {
|
|
328
|
-
const input = normalize_job_id_payload(payload);
|
|
329
|
-
const result = this.read_job_status
|
|
330
|
-
? normalize_job_status_result(await this.read_job_status(input))
|
|
331
|
-
: this.serialize_local_status(this.read_local_job(input.job_id));
|
|
332
|
-
return {
|
|
333
|
-
success: true,
|
|
334
|
-
data: result as unknown as JsonObject,
|
|
335
|
-
message: result.message || `image job ${result.status}`,
|
|
336
|
-
};
|
|
337
|
-
} catch (error) {
|
|
338
|
-
return {
|
|
339
|
-
success: false,
|
|
340
|
-
error: String(error),
|
|
341
|
-
message: String(error),
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
},
|
|
345
|
-
},
|
|
346
|
-
result: {
|
|
347
|
-
execute: async ({ payload }: { payload: JsonValue }) => {
|
|
348
|
-
try {
|
|
349
|
-
const input = normalize_job_id_payload(payload);
|
|
350
|
-
const result = this.read_job_result
|
|
351
|
-
? await this.read_job_result(input)
|
|
352
|
-
: this.serialize_local_result(this.read_local_job(input.job_id));
|
|
353
|
-
if (result.status === "succeeded" && result.result) {
|
|
354
|
-
return {
|
|
355
|
-
success: true,
|
|
356
|
-
data: result.result as unknown as JsonObject,
|
|
357
|
-
message: result.message || "image job succeeded",
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
if (result.status === "failed") {
|
|
361
|
-
return {
|
|
362
|
-
success: false,
|
|
363
|
-
data: result as unknown as JsonObject,
|
|
364
|
-
error: result.error || result.message || "image job failed",
|
|
365
|
-
message: result.message || "image job failed",
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
return {
|
|
369
|
-
success: true,
|
|
370
|
-
data: result as unknown as JsonObject,
|
|
371
|
-
message: result.message || `image job ${result.status}`,
|
|
372
|
-
};
|
|
373
|
-
} catch (error) {
|
|
374
|
-
return {
|
|
375
|
-
success: false,
|
|
376
|
-
error: String(error),
|
|
377
|
-
message: String(error),
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
},
|
|
381
|
-
},
|
|
382
158
|
generate: {
|
|
383
159
|
execute: async ({ payload }: { payload: JsonValue }) => {
|
|
384
160
|
try {
|
|
385
161
|
const input = normalize_image_payload(payload);
|
|
386
|
-
const
|
|
387
|
-
? await this.create_job(input)
|
|
388
|
-
: this.create_local_job(input);
|
|
389
|
-
const message = await this.wait_for_job(job.job_id);
|
|
162
|
+
const message = await this.generate_image(input);
|
|
390
163
|
return {
|
|
391
164
|
success: true,
|
|
392
165
|
data: message as unknown as JsonObject,
|
|
@@ -84,14 +84,14 @@ export interface ImagePluginInput {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* ImagePlugin
|
|
87
|
+
* ImagePlugin 图片任务状态。
|
|
88
88
|
*/
|
|
89
|
-
export type
|
|
89
|
+
export type ImagePluginJobStatus = "queued" | "running" | "succeeded" | "failed";
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
* ImagePlugin
|
|
92
|
+
* ImagePlugin 生成结果。
|
|
93
93
|
*/
|
|
94
|
-
export type
|
|
94
|
+
export type ImagePluginResult = UIMessage;
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* ImagePlugin 图片任务创建结果。
|
|
@@ -101,8 +101,6 @@ export interface ImagePluginJobCreateResult {
|
|
|
101
101
|
job_id: string;
|
|
102
102
|
/** 当前任务状态。 */
|
|
103
103
|
status: ImagePluginJobStatus;
|
|
104
|
-
/** 查询任务状态的路径或 URL。 */
|
|
105
|
-
status_path?: string;
|
|
106
104
|
/** 读取任务结果的路径或 URL。 */
|
|
107
105
|
result_path?: string;
|
|
108
106
|
/** 人类可读状态说明。 */
|
|
@@ -115,26 +113,6 @@ export interface ImagePluginJobCreateResult {
|
|
|
115
113
|
updated_at?: string;
|
|
116
114
|
}
|
|
117
115
|
|
|
118
|
-
/**
|
|
119
|
-
* ImagePlugin 图片任务状态查询结果。
|
|
120
|
-
*/
|
|
121
|
-
export interface ImagePluginJobStatusResult {
|
|
122
|
-
/** 图片任务唯一 ID。 */
|
|
123
|
-
job_id: string;
|
|
124
|
-
/** 当前任务状态。 */
|
|
125
|
-
status: ImagePluginJobStatus;
|
|
126
|
-
/** 人类可读状态说明。 */
|
|
127
|
-
message?: string;
|
|
128
|
-
/** 失败时的错误信息。 */
|
|
129
|
-
error?: string;
|
|
130
|
-
/** 建议下次轮询前等待的毫秒数。 */
|
|
131
|
-
poll_after_ms?: number;
|
|
132
|
-
/** 任务创建时间。 */
|
|
133
|
-
created_at?: string;
|
|
134
|
-
/** 任务更新时间。 */
|
|
135
|
-
updated_at?: string;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
116
|
/**
|
|
139
117
|
* ImagePlugin 图片任务结果查询结果。
|
|
140
118
|
*/
|
|
@@ -149,6 +127,8 @@ export interface ImagePluginJobResult {
|
|
|
149
127
|
error?: string;
|
|
150
128
|
/** 人类可读状态说明。 */
|
|
151
129
|
message?: string;
|
|
130
|
+
/** 任务未完成时建议下次轮询前等待的毫秒数。 */
|
|
131
|
+
poll_after_ms?: number;
|
|
152
132
|
/** 任务创建时间。 */
|
|
153
133
|
created_at?: string;
|
|
154
134
|
/** 任务更新时间。 */
|
|
@@ -165,13 +145,9 @@ export interface ImagePluginOptions {
|
|
|
165
145
|
title?: string;
|
|
166
146
|
/** Plugin 用途说明。 */
|
|
167
147
|
description?: string;
|
|
168
|
-
/**
|
|
169
|
-
image?: (input: ImagePluginInput) => Promise<ImagePluginResult> | ImagePluginResult;
|
|
170
|
-
/** 可选:创建图片生成任务,通常传入 `(input) => city.ai.imageJobCreate(input)`。 */
|
|
148
|
+
/** 可选:创建图片生成任务,通常传入 `(input) => city.ai.image_create(input)`。 */
|
|
171
149
|
create?: (input: ImagePluginInput) => Promise<ImagePluginJobCreateResult> | ImagePluginJobCreateResult;
|
|
172
|
-
/**
|
|
173
|
-
status?: (input: { job_id: string }) => Promise<ImagePluginJobStatusResult> | ImagePluginJobStatusResult;
|
|
174
|
-
/** 可选:读取图片生成任务结果,通常传入 `(input) => city.ai.imageJobResult(input)`。 */
|
|
150
|
+
/** 可选:读取图片生成任务结果,通常传入 `(input) => city.ai.image_result(input)`。 */
|
|
175
151
|
result?: (input: { job_id: string }) => Promise<ImagePluginJobResult> | ImagePluginJobResult;
|
|
176
152
|
/** 兼容 `generate` 动作等待任务完成的最长毫秒数。 */
|
|
177
153
|
wait_timeout_ms?: number;
|