@downcity/agent 1.1.152 → 1.1.153
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/executor/tools/plugin/PluginToolBridge.d.ts +1 -1
- package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
- package/bin/executor/tools/plugin/PluginToolBridge.js +45 -17
- package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
- package/bin/executor/tools/plugin/types/PluginTool.d.ts +17 -0
- package/bin/executor/tools/plugin/types/PluginTool.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/image-plugin-job.test.mjs +138 -3
- package/scripts/plugin-tool-bridge.test.mjs +88 -0
- package/src/executor/tools/plugin/PluginToolBridge.ts +54 -18
- package/src/executor/tools/plugin/types/PluginTool.ts +18 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - tool 层不直接持有 agent 或 plugin registry,只通过装配期注入的 AgentPlugins 调用 action。
|
|
6
6
|
* - 如果 action 返回 AI SDK UIMessage,则抽取 file parts 并入最终 assistant 消息。
|
|
7
|
-
* - 返回给模型的 tool result
|
|
7
|
+
* - 返回给模型的 tool result 只保留短摘要和本地绝对路径,避免 data URL 等大内容污染上下文。
|
|
8
8
|
*/
|
|
9
9
|
import type { AgentPlugins } from "../../../plugin/types/Plugin.js";
|
|
10
10
|
import type { PluginCallInput, PluginCallToolResult } from "../../../executor/tools/plugin/types/PluginTool.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluginToolBridge.d.ts","sourceRoot":"","sources":["../../../../src/executor/tools/plugin/PluginToolBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"PluginToolBridge.d.ts","sourceRoot":"","sources":["../../../../src/executor/tools/plugin/PluginToolBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EACV,eAAe,EAEf,oBAAoB,EACrB,MAAM,6CAA6C,CAAC;AASrD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAE7D;AAqGD;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,oBAAoB,CAAC,CAsE/B"}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - tool 层不直接持有 agent 或 plugin registry,只通过装配期注入的 AgentPlugins 调用 action。
|
|
6
6
|
* - 如果 action 返回 AI SDK UIMessage,则抽取 file parts 并入最终 assistant 消息。
|
|
7
|
-
* - 返回给模型的 tool result
|
|
7
|
+
* - 返回给模型的 tool result 只保留短摘要和本地绝对路径,避免 data URL 等大内容污染上下文。
|
|
8
8
|
*/
|
|
9
|
+
import path from "node:path";
|
|
9
10
|
import { materializeAssistantFileParts } from "../../messages/AssistantFileResource.js";
|
|
10
11
|
import { enqueueAssistantFileParts, getSessionRunContext, } from "../../SessionRunScope.js";
|
|
11
12
|
let plugin_tool_runtime = null;
|
|
@@ -50,27 +51,50 @@ function extract_assistant_file_parts(data) {
|
|
|
50
51
|
const parts = Array.isArray(message?.parts) ? message.parts : [];
|
|
51
52
|
return parts.filter(is_file_part);
|
|
52
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* 解析当前项目根目录,保持与 assistant 文件落盘逻辑一致。
|
|
56
|
+
*/
|
|
57
|
+
function resolve_project_root(project_root) {
|
|
58
|
+
const raw = String(project_root || "").trim();
|
|
59
|
+
return path.resolve(raw || process.cwd());
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 将 `resources://` URL 转成本机绝对路径。
|
|
63
|
+
*/
|
|
64
|
+
function resolve_resources_file_path(project_root, raw_url) {
|
|
65
|
+
const prefix = "resources://";
|
|
66
|
+
const raw = String(raw_url || "").trim();
|
|
67
|
+
if (!raw.startsWith(prefix))
|
|
68
|
+
return "";
|
|
69
|
+
const relative = raw.slice(prefix.length).replace(/^\/+/, "");
|
|
70
|
+
if (!relative)
|
|
71
|
+
return "";
|
|
72
|
+
const file_path = path.resolve(project_root, relative);
|
|
73
|
+
const rel = path.relative(project_root, file_path);
|
|
74
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel))
|
|
75
|
+
return "";
|
|
76
|
+
return file_path;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 构建返回给模型和用户可见的文件摘要。
|
|
80
|
+
*/
|
|
81
|
+
function summarize_materialized_files(parts, project_root) {
|
|
82
|
+
return parts.map((part, index) => ({
|
|
83
|
+
index,
|
|
84
|
+
media_type: part.mediaType,
|
|
85
|
+
filename: typeof part.filename === "string" ? part.filename : "",
|
|
86
|
+
url: String(part.url || ""),
|
|
87
|
+
path: resolve_resources_file_path(project_root, String(part.url || "")),
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
53
90
|
/**
|
|
54
91
|
* 生成给模型读取的短摘要。
|
|
55
92
|
*/
|
|
56
93
|
function summarize_action_data(data) {
|
|
57
94
|
const message = to_json_object(data);
|
|
58
95
|
const parts = Array.isArray(message?.parts) ? message.parts : [];
|
|
59
|
-
const file_parts = parts.filter(is_file_part);
|
|
60
96
|
if (parts.length > 0) {
|
|
61
|
-
return
|
|
62
|
-
kind: "ui_message",
|
|
63
|
-
role: typeof message?.role === "string" ? message.role : "assistant",
|
|
64
|
-
part_count: parts.length,
|
|
65
|
-
file_count: file_parts.length,
|
|
66
|
-
files: file_parts.map((part, index) => ({
|
|
67
|
-
index,
|
|
68
|
-
mediaType: part.mediaType,
|
|
69
|
-
filename: typeof part.filename === "string" ? part.filename : "",
|
|
70
|
-
// 关键点(中文):不把 data URL 或长 URL 原样返回给模型,完整内容只进入 assistant file part。
|
|
71
|
-
has_url: Boolean(part.url),
|
|
72
|
-
})),
|
|
73
|
-
};
|
|
97
|
+
return undefined;
|
|
74
98
|
}
|
|
75
99
|
if (data === undefined)
|
|
76
100
|
return {};
|
|
@@ -113,27 +137,31 @@ export async function invokePluginCallTool(input) {
|
|
|
113
137
|
action,
|
|
114
138
|
payload,
|
|
115
139
|
});
|
|
140
|
+
const project_root = resolve_project_root(getSessionRunContext()?.projectRoot);
|
|
116
141
|
const raw_file_parts = result.success
|
|
117
142
|
? extract_assistant_file_parts(result.data)
|
|
118
143
|
: [];
|
|
119
144
|
const file_parts = raw_file_parts.length > 0
|
|
120
145
|
? await materializeAssistantFileParts({
|
|
121
|
-
projectRoot:
|
|
146
|
+
projectRoot: project_root,
|
|
122
147
|
parts: raw_file_parts,
|
|
123
148
|
})
|
|
124
149
|
: [];
|
|
150
|
+
const files = summarize_materialized_files(file_parts, project_root);
|
|
125
151
|
if (file_parts.length > 0) {
|
|
126
152
|
enqueueAssistantFileParts(file_parts);
|
|
127
153
|
}
|
|
154
|
+
const data = summarize_action_data(result.data);
|
|
128
155
|
return {
|
|
129
156
|
success: result.success,
|
|
130
157
|
plugin,
|
|
131
158
|
action,
|
|
132
159
|
assistant_file_count: file_parts.length,
|
|
160
|
+
...(files.length > 0 ? { files } : {}),
|
|
133
161
|
message: String(result.message || result.error || "").trim() ||
|
|
134
162
|
(result.success ? "plugin action completed" : "plugin action failed"),
|
|
135
163
|
...(result.error ? { error: result.error } : {}),
|
|
136
|
-
data:
|
|
164
|
+
...(data === undefined ? {} : { data }),
|
|
137
165
|
};
|
|
138
166
|
}
|
|
139
167
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluginToolBridge.js","sourceRoot":"","sources":["../../../../src/executor/tools/plugin/PluginToolBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"PluginToolBridge.js","sourceRoot":"","sources":["../../../../src/executor/tools/plugin/PluginToolBridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,6 +17,21 @@ export interface PluginCallInput {
|
|
|
17
17
|
/** 传给 plugin action 的 JSON payload。 */
|
|
18
18
|
payload?: JsonObject;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* plugin_call 产生的 assistant 文件摘要。
|
|
22
|
+
*/
|
|
23
|
+
export interface PluginCallToolFileResult {
|
|
24
|
+
/** 文件在本轮 assistant 文件列表中的顺序。 */
|
|
25
|
+
index: number;
|
|
26
|
+
/** 文件 MIME 类型,例如 `image/png`。 */
|
|
27
|
+
media_type: string;
|
|
28
|
+
/** 原始文件名;若上游未提供则为空字符串。 */
|
|
29
|
+
filename: string;
|
|
30
|
+
/** 持久化到历史消息中的资源 URL,通常为 `resources://.downcity/resources/...`。 */
|
|
31
|
+
url: string;
|
|
32
|
+
/** 当前机器可直接打开的绝对文件路径。 */
|
|
33
|
+
path: string;
|
|
34
|
+
}
|
|
20
35
|
/**
|
|
21
36
|
* plugin_call 返回给模型的摘要结果。
|
|
22
37
|
*/
|
|
@@ -29,6 +44,8 @@ export interface PluginCallToolResult {
|
|
|
29
44
|
action: string;
|
|
30
45
|
/** 本次 action 产生并写入 assistant 消息的 file part 数量。 */
|
|
31
46
|
assistant_file_count: number;
|
|
47
|
+
/** 本次 action 产生的文件摘要,包含可直接打开的绝对路径。 */
|
|
48
|
+
files?: PluginCallToolFileResult[];
|
|
32
49
|
/** 人类可读消息。 */
|
|
33
50
|
message: string;
|
|
34
51
|
/** 错误信息。 */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluginTool.d.ts","sourceRoot":"","sources":["../../../../../src/executor/tools/plugin/types/PluginTool.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,OAAO,CAAC,EAAE,UAAU,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,cAAc;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,oBAAoB,EAAE,MAAM,CAAC;IAC7B,cAAc;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB"}
|
|
1
|
+
{"version":3,"file":"PluginTool.d.ts","sourceRoot":"","sources":["../../../../../src/executor/tools/plugin/types/PluginTool.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,OAAO,CAAC,EAAE,UAAU,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAC;IACZ,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,cAAc;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,oBAAoB,EAAE,MAAM,CAAC;IAC7B,sCAAsC;IACtC,KAAK,CAAC,EAAE,wBAAwB,EAAE,CAAC;IACnC,cAAc;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB"}
|
package/package.json
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* @file 验证 ImagePlugin 对 Agent 保持直接生图体验。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
5
|
+
* - 插件暴露 image_create / image_result 两个任务 action,并保留 generate 便捷 action。
|
|
6
|
+
* - image_result 默认轮询到终态,成功后返回 UIMessage 方便 tool bridge 自动挂载图片。
|
|
7
7
|
* - 成功后返回 UIMessage,后续由 plugin bridge 落盘 file parts。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import test from "node:test";
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
12
|
|
|
13
|
-
import { ImagePlugin } from "
|
|
13
|
+
import { ImagePlugin } from "../../plugins/bin/index.js";
|
|
14
14
|
|
|
15
15
|
function create_image_message() {
|
|
16
16
|
return {
|
|
@@ -61,6 +61,141 @@ test("ImagePlugin generate creates and polls image jobs", async () => {
|
|
|
61
61
|
]);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
test("ImagePlugin image_create returns image job", async () => {
|
|
65
|
+
const plugin = new ImagePlugin({
|
|
66
|
+
image_create: (input) => ({
|
|
67
|
+
job_id: "img_1",
|
|
68
|
+
status: "queued",
|
|
69
|
+
poll_after_ms: 1,
|
|
70
|
+
metadata: { prompt: input.prompt },
|
|
71
|
+
}),
|
|
72
|
+
image_result: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await plugin.actions.image_create.execute({
|
|
76
|
+
context: {},
|
|
77
|
+
payload: { prompt: "draw" },
|
|
78
|
+
pluginName: "image",
|
|
79
|
+
actionName: "image_create",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
assert.equal(result.success, true);
|
|
83
|
+
assert.equal(result.data.job_id, "img_1");
|
|
84
|
+
assert.equal(result.data.status, "queued");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("ImagePlugin models lists image-capable models", async () => {
|
|
88
|
+
const plugin = new ImagePlugin({
|
|
89
|
+
list_models: () => [
|
|
90
|
+
{
|
|
91
|
+
id: "text_1",
|
|
92
|
+
name: "Text",
|
|
93
|
+
description: "text only",
|
|
94
|
+
modalities: ["text"],
|
|
95
|
+
tags: ["general"],
|
|
96
|
+
meta: {},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "image_1",
|
|
100
|
+
name: "Image",
|
|
101
|
+
description: "image model",
|
|
102
|
+
modalities: ["image"],
|
|
103
|
+
tags: ["creative"],
|
|
104
|
+
meta: { provider: "test" },
|
|
105
|
+
default_modalities: ["image"],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
image_create: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
109
|
+
image_result: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const result = await plugin.actions.models.execute({
|
|
113
|
+
context: {},
|
|
114
|
+
payload: {},
|
|
115
|
+
pluginName: "image",
|
|
116
|
+
actionName: "models",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
assert.equal(result.success, true);
|
|
120
|
+
assert.equal(result.data.default_model_id, "image_1");
|
|
121
|
+
assert.deepEqual(result.data.items.map((item) => item.id), ["image_1"]);
|
|
122
|
+
assert.equal(result.data.items[0].meta.provider, "test");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("ImagePlugin image_result waits by default and returns final message", async () => {
|
|
126
|
+
const calls = [];
|
|
127
|
+
const message = create_image_message();
|
|
128
|
+
const plugin = new ImagePlugin({
|
|
129
|
+
min_poll_interval_ms: 1,
|
|
130
|
+
image_create: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
131
|
+
image_result: (input) => {
|
|
132
|
+
calls.push(input.job_id);
|
|
133
|
+
return calls.length === 1
|
|
134
|
+
? { job_id: input.job_id, status: "running", poll_after_ms: 1 }
|
|
135
|
+
: { job_id: input.job_id, status: "succeeded", result: message, poll_after_ms: 1 };
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const result = await plugin.actions.image_result.execute({
|
|
140
|
+
context: {},
|
|
141
|
+
payload: { job_id: "img_1" },
|
|
142
|
+
pluginName: "image",
|
|
143
|
+
actionName: "image_result",
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
assert.equal(result.success, true);
|
|
147
|
+
assert.equal(result.data.role, "assistant");
|
|
148
|
+
assert.equal(result.data.parts[0].type, "file");
|
|
149
|
+
assert.deepEqual(calls, ["img_1", "img_1"]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("ImagePlugin image_result can read once without waiting", async () => {
|
|
153
|
+
const calls = [];
|
|
154
|
+
const plugin = new ImagePlugin({
|
|
155
|
+
min_poll_interval_ms: 1,
|
|
156
|
+
image_create: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
157
|
+
image_result: (input) => {
|
|
158
|
+
calls.push(input.job_id);
|
|
159
|
+
return { job_id: input.job_id, status: "running", poll_after_ms: 1 };
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const result = await plugin.actions.image_result.execute({
|
|
164
|
+
context: {},
|
|
165
|
+
payload: { job_id: "img_1", until_finish: false },
|
|
166
|
+
pluginName: "image",
|
|
167
|
+
actionName: "image_result",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
assert.equal(result.success, true);
|
|
171
|
+
assert.equal(result.data.job_id, "img_1");
|
|
172
|
+
assert.equal(result.data.status, "running");
|
|
173
|
+
assert.deepEqual(calls, ["img_1"]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("ImagePlugin image_result reports failed terminal job by default", async () => {
|
|
177
|
+
const plugin = new ImagePlugin({
|
|
178
|
+
min_poll_interval_ms: 1,
|
|
179
|
+
image_create: () => ({ job_id: "img_1", status: "queued", poll_after_ms: 1 }),
|
|
180
|
+
image_result: (input) => ({
|
|
181
|
+
job_id: input.job_id,
|
|
182
|
+
status: "failed",
|
|
183
|
+
error: "provider failed",
|
|
184
|
+
poll_after_ms: 1,
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const result = await plugin.actions.image_result.execute({
|
|
189
|
+
context: {},
|
|
190
|
+
payload: { job_id: "img_1" },
|
|
191
|
+
pluginName: "image",
|
|
192
|
+
actionName: "image_result",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
assert.equal(result.success, false);
|
|
196
|
+
assert.match(result.error, /provider failed/);
|
|
197
|
+
});
|
|
198
|
+
|
|
64
199
|
test("ImagePlugin generate reports image failure", async () => {
|
|
65
200
|
const plugin = new ImagePlugin({
|
|
66
201
|
min_poll_interval_ms: 1,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 验证 plugin tool bridge 会把生成文件摘要为可打开路径。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - assistant message 仍然使用 `resources://` URL,避免历史暴露本机路径。
|
|
6
|
+
* - tool result 额外返回本机绝对路径,便于模型与用户明确知道文件位置。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
invokePluginCallTool,
|
|
17
|
+
setPluginToolRuntime,
|
|
18
|
+
} from "../bin/executor/tools/plugin/PluginToolBridge.js";
|
|
19
|
+
import { withSessionRunScope } from "../bin/executor/SessionRunScope.js";
|
|
20
|
+
|
|
21
|
+
function create_run_context(project_root) {
|
|
22
|
+
return {
|
|
23
|
+
sessionId: "session_test",
|
|
24
|
+
projectRoot: project_root,
|
|
25
|
+
injectedUserMessages: [],
|
|
26
|
+
deferredPersistedUserMessages: [],
|
|
27
|
+
pendingAssistantFileParts: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test("invokePluginCallTool returns absolute paths for materialized file parts", async () => {
|
|
32
|
+
const project_root = await fs.mkdtemp(
|
|
33
|
+
path.join(os.tmpdir(), "downcity-agent-plugin-tool-files-"),
|
|
34
|
+
);
|
|
35
|
+
const bytes = Buffer.from("png-bytes-for-plugin-tool", "utf8");
|
|
36
|
+
|
|
37
|
+
setPluginToolRuntime({
|
|
38
|
+
list: () => [],
|
|
39
|
+
availability: async () => ({ enabled: true, available: true, reasons: [] }),
|
|
40
|
+
runAction: async () => ({
|
|
41
|
+
success: true,
|
|
42
|
+
message: "image generated",
|
|
43
|
+
data: {
|
|
44
|
+
id: "msg_image_test",
|
|
45
|
+
role: "assistant",
|
|
46
|
+
parts: [
|
|
47
|
+
{
|
|
48
|
+
type: "file",
|
|
49
|
+
mediaType: "image/png",
|
|
50
|
+
filename: "image.png",
|
|
51
|
+
url: `data:image/png;base64,${bytes.toString("base64")}`,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
pipeline: async (_, value) => value,
|
|
57
|
+
guard: async () => {},
|
|
58
|
+
effect: async () => {},
|
|
59
|
+
resolve: async () => {
|
|
60
|
+
throw new Error("not implemented");
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const run_context = create_run_context(project_root);
|
|
65
|
+
const result = await withSessionRunScope({ runContext: run_context }, () =>
|
|
66
|
+
invokePluginCallTool({
|
|
67
|
+
plugin: "image",
|
|
68
|
+
action: "generate",
|
|
69
|
+
payload: { prompt: "draw" },
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
assert.equal(result.success, true);
|
|
74
|
+
assert.equal(result.assistant_file_count, 1);
|
|
75
|
+
assert.equal(result.files?.length, 1);
|
|
76
|
+
assert.match(result.files[0].url, /^resources:\/\/\.downcity\/resources\//);
|
|
77
|
+
assert.equal(path.isAbsolute(result.files[0].path), true);
|
|
78
|
+
assert.equal(
|
|
79
|
+
path.dirname(result.files[0].path),
|
|
80
|
+
path.join(project_root, ".downcity", "resources"),
|
|
81
|
+
);
|
|
82
|
+
assert.deepEqual(await fs.readFile(result.files[0].path), bytes);
|
|
83
|
+
assert.equal("data" in result, false);
|
|
84
|
+
|
|
85
|
+
const pending_parts = run_context.pendingAssistantFileParts;
|
|
86
|
+
assert.equal(pending_parts.length, 1);
|
|
87
|
+
assert.equal(pending_parts[0].url, result.files[0].url);
|
|
88
|
+
});
|
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - tool 层不直接持有 agent 或 plugin registry,只通过装配期注入的 AgentPlugins 调用 action。
|
|
6
6
|
* - 如果 action 返回 AI SDK UIMessage,则抽取 file parts 并入最终 assistant 消息。
|
|
7
|
-
* - 返回给模型的 tool result
|
|
7
|
+
* - 返回给模型的 tool result 只保留短摘要和本地绝对路径,避免 data URL 等大内容污染上下文。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import path from "node:path";
|
|
10
11
|
import type { FileUIPart } from "ai";
|
|
11
12
|
import type { JsonObject, JsonValue } from "@/types/common/Json.js";
|
|
12
13
|
import type { AgentPlugins } from "@/plugin/types/Plugin.js";
|
|
13
14
|
import type {
|
|
14
15
|
PluginCallInput,
|
|
16
|
+
PluginCallToolFileResult,
|
|
15
17
|
PluginCallToolResult,
|
|
16
18
|
} from "@/executor/tools/plugin/types/PluginTool.js";
|
|
17
19
|
import { materializeAssistantFileParts } from "@executor/messages/AssistantFileResource.js";
|
|
@@ -69,27 +71,57 @@ function extract_assistant_file_parts(data: JsonValue | undefined): FileUIPart[]
|
|
|
69
71
|
return parts.filter(is_file_part);
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* 解析当前项目根目录,保持与 assistant 文件落盘逻辑一致。
|
|
76
|
+
*/
|
|
77
|
+
function resolve_project_root(project_root: string | undefined): string {
|
|
78
|
+
const raw = String(project_root || "").trim();
|
|
79
|
+
return path.resolve(raw || process.cwd());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 将 `resources://` URL 转成本机绝对路径。
|
|
84
|
+
*/
|
|
85
|
+
function resolve_resources_file_path(
|
|
86
|
+
project_root: string,
|
|
87
|
+
raw_url: string,
|
|
88
|
+
): string {
|
|
89
|
+
const prefix = "resources://";
|
|
90
|
+
const raw = String(raw_url || "").trim();
|
|
91
|
+
if (!raw.startsWith(prefix)) return "";
|
|
92
|
+
const relative = raw.slice(prefix.length).replace(/^\/+/, "");
|
|
93
|
+
if (!relative) return "";
|
|
94
|
+
|
|
95
|
+
const file_path = path.resolve(project_root, relative);
|
|
96
|
+
const rel = path.relative(project_root, file_path);
|
|
97
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return "";
|
|
98
|
+
return file_path;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 构建返回给模型和用户可见的文件摘要。
|
|
103
|
+
*/
|
|
104
|
+
function summarize_materialized_files(
|
|
105
|
+
parts: FileUIPart[],
|
|
106
|
+
project_root: string,
|
|
107
|
+
): PluginCallToolFileResult[] {
|
|
108
|
+
return parts.map((part, index) => ({
|
|
109
|
+
index,
|
|
110
|
+
media_type: part.mediaType,
|
|
111
|
+
filename: typeof part.filename === "string" ? part.filename : "",
|
|
112
|
+
url: String(part.url || ""),
|
|
113
|
+
path: resolve_resources_file_path(project_root, String(part.url || "")),
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
72
117
|
/**
|
|
73
118
|
* 生成给模型读取的短摘要。
|
|
74
119
|
*/
|
|
75
|
-
function summarize_action_data(data: JsonValue | undefined): JsonObject {
|
|
120
|
+
function summarize_action_data(data: JsonValue | undefined): JsonObject | undefined {
|
|
76
121
|
const message = to_json_object(data);
|
|
77
122
|
const parts: unknown[] = Array.isArray(message?.parts) ? message.parts : [];
|
|
78
|
-
const file_parts = parts.filter(is_file_part);
|
|
79
123
|
if (parts.length > 0) {
|
|
80
|
-
return
|
|
81
|
-
kind: "ui_message",
|
|
82
|
-
role: typeof message?.role === "string" ? message.role : "assistant",
|
|
83
|
-
part_count: parts.length,
|
|
84
|
-
file_count: file_parts.length,
|
|
85
|
-
files: file_parts.map((part, index) => ({
|
|
86
|
-
index,
|
|
87
|
-
mediaType: part.mediaType,
|
|
88
|
-
filename: typeof part.filename === "string" ? part.filename : "",
|
|
89
|
-
// 关键点(中文):不把 data URL 或长 URL 原样返回给模型,完整内容只进入 assistant file part。
|
|
90
|
-
has_url: Boolean(part.url),
|
|
91
|
-
})),
|
|
92
|
-
};
|
|
124
|
+
return undefined;
|
|
93
125
|
}
|
|
94
126
|
if (data === undefined) return {};
|
|
95
127
|
return {
|
|
@@ -135,29 +167,33 @@ export async function invokePluginCallTool(
|
|
|
135
167
|
action,
|
|
136
168
|
payload,
|
|
137
169
|
});
|
|
170
|
+
const project_root = resolve_project_root(getSessionRunContext()?.projectRoot);
|
|
138
171
|
const raw_file_parts = result.success
|
|
139
172
|
? extract_assistant_file_parts(result.data)
|
|
140
173
|
: [];
|
|
141
174
|
const file_parts =
|
|
142
175
|
raw_file_parts.length > 0
|
|
143
176
|
? await materializeAssistantFileParts({
|
|
144
|
-
projectRoot:
|
|
177
|
+
projectRoot: project_root,
|
|
145
178
|
parts: raw_file_parts,
|
|
146
179
|
})
|
|
147
180
|
: [];
|
|
181
|
+
const files = summarize_materialized_files(file_parts, project_root);
|
|
148
182
|
if (file_parts.length > 0) {
|
|
149
183
|
enqueueAssistantFileParts(file_parts);
|
|
150
184
|
}
|
|
185
|
+
const data = summarize_action_data(result.data);
|
|
151
186
|
return {
|
|
152
187
|
success: result.success,
|
|
153
188
|
plugin,
|
|
154
189
|
action,
|
|
155
190
|
assistant_file_count: file_parts.length,
|
|
191
|
+
...(files.length > 0 ? { files } : {}),
|
|
156
192
|
message:
|
|
157
193
|
String(result.message || result.error || "").trim() ||
|
|
158
194
|
(result.success ? "plugin action completed" : "plugin action failed"),
|
|
159
195
|
...(result.error ? { error: result.error } : {}),
|
|
160
|
-
data:
|
|
196
|
+
...(data === undefined ? {} : { data }),
|
|
161
197
|
};
|
|
162
198
|
} catch (error) {
|
|
163
199
|
return {
|
|
@@ -20,6 +20,22 @@ export interface PluginCallInput {
|
|
|
20
20
|
payload?: JsonObject;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* plugin_call 产生的 assistant 文件摘要。
|
|
25
|
+
*/
|
|
26
|
+
export interface PluginCallToolFileResult {
|
|
27
|
+
/** 文件在本轮 assistant 文件列表中的顺序。 */
|
|
28
|
+
index: number;
|
|
29
|
+
/** 文件 MIME 类型,例如 `image/png`。 */
|
|
30
|
+
media_type: string;
|
|
31
|
+
/** 原始文件名;若上游未提供则为空字符串。 */
|
|
32
|
+
filename: string;
|
|
33
|
+
/** 持久化到历史消息中的资源 URL,通常为 `resources://.downcity/resources/...`。 */
|
|
34
|
+
url: string;
|
|
35
|
+
/** 当前机器可直接打开的绝对文件路径。 */
|
|
36
|
+
path: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
/**
|
|
24
40
|
* plugin_call 返回给模型的摘要结果。
|
|
25
41
|
*/
|
|
@@ -32,6 +48,8 @@ export interface PluginCallToolResult {
|
|
|
32
48
|
action: string;
|
|
33
49
|
/** 本次 action 产生并写入 assistant 消息的 file part 数量。 */
|
|
34
50
|
assistant_file_count: number;
|
|
51
|
+
/** 本次 action 产生的文件摘要,包含可直接打开的绝对路径。 */
|
|
52
|
+
files?: PluginCallToolFileResult[];
|
|
35
53
|
/** 人类可读消息。 */
|
|
36
54
|
message: string;
|
|
37
55
|
/** 错误信息。 */
|