@downcity/agent 1.1.160 → 1.1.161
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/config/Paths.d.ts +1 -1
- package/bin/config/Paths.js +1 -1
- package/bin/executor/core-engine/CoreEngineMessageState.d.ts +1 -1
- package/bin/executor/core-engine/CoreEngineMessageState.js +1 -1
- package/bin/executor/messages/AssistantFileResource.d.ts +35 -4
- package/bin/executor/messages/AssistantFileResource.d.ts.map +1 -1
- package/bin/executor/messages/AssistantFileResource.js +47 -10
- package/bin/executor/messages/AssistantFileResource.js.map +1 -1
- package/bin/executor/messages/SessionAttachmentMapper.d.ts +3 -3
- package/bin/executor/messages/SessionAttachmentMapper.d.ts.map +1 -1
- package/bin/executor/messages/SessionAttachmentMapper.js +17 -18
- package/bin/executor/messages/SessionAttachmentMapper.js.map +1 -1
- package/bin/executor/messages/SessionMessageCodec.d.ts.map +1 -1
- package/bin/executor/messages/SessionMessageCodec.js +1 -1
- package/bin/executor/messages/SessionMessageCodec.js.map +1 -1
- package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
- package/bin/executor/tools/plugin/PluginToolBridge.js +14 -25
- package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
- package/bin/executor/tools/plugin/types/PluginTool.d.ts +3 -3
- package/bin/executor/tools/plugin/types/PluginTool.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/assistant-file-resource.test.mjs +77 -11
- package/scripts/image-plugin-job.test.mjs +2 -1
- package/scripts/plugin-tool-bridge.test.mjs +4 -4
- package/src/config/Paths.ts +1 -1
- package/src/executor/core-engine/CoreEngineMessageState.ts +1 -1
- package/src/executor/messages/AssistantFileResource.ts +65 -12
- package/src/executor/messages/SessionAttachmentMapper.ts +16 -17
- package/src/executor/messages/SessionMessageCodec.ts +4 -1
- package/src/executor/tools/plugin/PluginToolBridge.ts +17 -27
- package/src/executor/tools/plugin/types/PluginTool.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file 验证 assistant file part 会把资源落盘为
|
|
2
|
+
* @file 验证 assistant file part 会把资源落盘为 Agent 根目录相对路径。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - 历史消息不应长期保存图片 base64。
|
|
6
|
-
* - 送模型前可以从
|
|
6
|
+
* - 送模型前可以从 Agent 根目录相对路径临时 hydrate 回 data URL。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import test from "node:test";
|
|
@@ -15,10 +15,13 @@ import http from "node:http";
|
|
|
15
15
|
import { pathToFileURL } from "node:url";
|
|
16
16
|
|
|
17
17
|
import { materializeAssistantFileParts } from "../bin/executor/messages/AssistantFileResource.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
hydrateFileUrlPartsForModel,
|
|
20
|
+
injectFilePartsFromAttachments,
|
|
21
|
+
} from "../bin/executor/messages/SessionAttachmentMapper.js";
|
|
19
22
|
|
|
20
|
-
function
|
|
21
|
-
return path.join(project_root, String(
|
|
23
|
+
function resource_path_from_relative_path(project_root, relative_path) {
|
|
24
|
+
return path.join(project_root, String(relative_path || ""));
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
test("materializeAssistantFileParts stores data URL images under .downcity/resources", async () => {
|
|
@@ -44,10 +47,10 @@ test("materializeAssistantFileParts stores data URL images under .downcity/resou
|
|
|
44
47
|
assert.equal(parts[0].type, "file");
|
|
45
48
|
assert.equal(parts[0].mediaType, "image/png");
|
|
46
49
|
assert.equal(parts[0].filename, "image-1.png");
|
|
47
|
-
assert.match(parts[0].url,
|
|
50
|
+
assert.match(parts[0].url, /^\.downcity\/resources\//);
|
|
48
51
|
assert.equal(parts[0].url.includes("base64"), false);
|
|
49
52
|
|
|
50
|
-
const resource_path =
|
|
53
|
+
const resource_path = resource_path_from_relative_path(project_root, parts[0].url);
|
|
51
54
|
assert.equal(
|
|
52
55
|
path.dirname(resource_path),
|
|
53
56
|
path.join(project_root, ".downcity", "resources"),
|
|
@@ -84,10 +87,10 @@ test("materializeAssistantFileParts downloads remote file URLs into resources",
|
|
|
84
87
|
],
|
|
85
88
|
});
|
|
86
89
|
|
|
87
|
-
assert.match(parts[0].url,
|
|
90
|
+
assert.match(parts[0].url, /^\.downcity\/resources\//);
|
|
88
91
|
assert.equal(parts[0].url.startsWith("http://"), false);
|
|
89
92
|
assert.deepEqual(
|
|
90
|
-
await fs.readFile(
|
|
93
|
+
await fs.readFile(resource_path_from_relative_path(project_root, parts[0].url)),
|
|
91
94
|
bytes,
|
|
92
95
|
);
|
|
93
96
|
} finally {
|
|
@@ -97,7 +100,33 @@ test("materializeAssistantFileParts downloads remote file URLs into resources",
|
|
|
97
100
|
}
|
|
98
101
|
});
|
|
99
102
|
|
|
100
|
-
test("
|
|
103
|
+
test("materializeAssistantFileParts resolves relative local file URLs from agent project root", async () => {
|
|
104
|
+
const project_root = await fs.mkdtemp(
|
|
105
|
+
path.join(os.tmpdir(), "downcity-agent-assistant-relative-resource-"),
|
|
106
|
+
);
|
|
107
|
+
const bytes = Buffer.from("relative-local-png-bytes", "utf8");
|
|
108
|
+
await fs.writeFile(path.join(project_root, "input.png"), bytes);
|
|
109
|
+
|
|
110
|
+
const parts = await materializeAssistantFileParts({
|
|
111
|
+
projectRoot: project_root,
|
|
112
|
+
parts: [
|
|
113
|
+
{
|
|
114
|
+
type: "file",
|
|
115
|
+
mediaType: "image/png",
|
|
116
|
+
filename: "input.png",
|
|
117
|
+
url: "./input.png",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
assert.match(parts[0].url, /^\.downcity\/resources\//);
|
|
123
|
+
assert.deepEqual(
|
|
124
|
+
await fs.readFile(resource_path_from_relative_path(project_root, parts[0].url)),
|
|
125
|
+
bytes,
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("hydrateFileUrlPartsForModel converts relative resource paths back to data URLs in memory", async () => {
|
|
101
130
|
const project_root = await fs.mkdtemp(
|
|
102
131
|
path.join(os.tmpdir(), "downcity-agent-assistant-hydrate-"),
|
|
103
132
|
);
|
|
@@ -137,7 +166,7 @@ test("hydrateFileUrlPartsForModel converts resources URLs back to data URLs in m
|
|
|
137
166
|
hydrated_part?.url,
|
|
138
167
|
`data:image/png;base64,${bytes.toString("base64")}`,
|
|
139
168
|
);
|
|
140
|
-
assert.match(materialized[0].url,
|
|
169
|
+
assert.match(materialized[0].url, /^\.downcity\/resources\//);
|
|
141
170
|
});
|
|
142
171
|
|
|
143
172
|
test("hydrateFileUrlPartsForModel keeps old file URLs compatible", async () => {
|
|
@@ -178,3 +207,40 @@ test("hydrateFileUrlPartsForModel keeps old file URLs compatible", async () => {
|
|
|
178
207
|
`data:image/png;base64,${bytes.toString("base64")}`,
|
|
179
208
|
);
|
|
180
209
|
});
|
|
210
|
+
|
|
211
|
+
test("injectFilePartsFromAttachments resolves relative file tags from agent project root", async () => {
|
|
212
|
+
const project_root = await fs.mkdtemp(
|
|
213
|
+
path.join(os.tmpdir(), "downcity-agent-attachment-relative-"),
|
|
214
|
+
);
|
|
215
|
+
const bytes = Buffer.from("attachment-relative-png-bytes", "utf8");
|
|
216
|
+
await fs.writeFile(path.join(project_root, "input.png"), bytes);
|
|
217
|
+
|
|
218
|
+
const messages = await injectFilePartsFromAttachments(
|
|
219
|
+
[
|
|
220
|
+
{
|
|
221
|
+
id: "u:test:attachment",
|
|
222
|
+
role: "user",
|
|
223
|
+
metadata: {
|
|
224
|
+
v: 1,
|
|
225
|
+
ts: Date.now(),
|
|
226
|
+
sessionId: "session_test",
|
|
227
|
+
},
|
|
228
|
+
parts: [
|
|
229
|
+
{
|
|
230
|
+
type: "text",
|
|
231
|
+
text: '<file type="photo">./input.png</file>',
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
project_root,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const injected_part = messages[0]?.parts[1];
|
|
240
|
+
assert.equal(injected_part?.type, "file");
|
|
241
|
+
assert.equal(injected_part?.mediaType, "image/png");
|
|
242
|
+
assert.equal(
|
|
243
|
+
injected_part?.url,
|
|
244
|
+
`data:image/png;base64,${bytes.toString("base64")}`,
|
|
245
|
+
);
|
|
246
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - 插件只暴露 image_create / image_result 两个任务 action。
|
|
6
|
-
* - image_result
|
|
6
|
+
* - image_result 默认只读取一次当前状态;传 until_done=true 时会在 plugin 层等待终态。
|
|
7
7
|
* - 成功图片仍以 UIMessage 返回,后续由 plugin bridge 落盘 file parts。
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -132,6 +132,7 @@ test("ImagePlugin exposes action metadata through plugin registry", async () =>
|
|
|
132
132
|
assert.equal(metadata.actions[0].name, "image_create");
|
|
133
133
|
assert.equal(metadata.actions[0].has_input_schema, true);
|
|
134
134
|
assert.match(metadata.actions[0].description, /Create an async image job/);
|
|
135
|
+
assert.match(metadata.actions[0].description, /explicit user confirmation/);
|
|
135
136
|
assert.equal(metadata.actions[0].examples[0].payload.prompt.includes("rainy city"), true);
|
|
136
137
|
});
|
|
137
138
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @file 验证 plugin tool bridge 会把生成文件摘要为可打开路径。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
|
-
* - assistant message
|
|
6
|
-
* - tool result
|
|
5
|
+
* - assistant message 使用 Agent 根目录相对路径,避免历史暴露本机路径。
|
|
6
|
+
* - tool result 同时返回相对路径与本机绝对路径,便于模型与用户明确知道文件位置。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import test from "node:test";
|
|
@@ -78,7 +78,7 @@ test("invokePluginCallTool returns absolute paths for materialized file parts",
|
|
|
78
78
|
assert.equal(result.success, true);
|
|
79
79
|
assert.equal(result.assistant_file_count, 1);
|
|
80
80
|
assert.equal(result.files?.length, 1);
|
|
81
|
-
assert.match(result.files[0].
|
|
81
|
+
assert.match(result.files[0].relative_path, /^\.downcity\/resources\//);
|
|
82
82
|
assert.equal(path.isAbsolute(result.files[0].path), true);
|
|
83
83
|
assert.equal(
|
|
84
84
|
path.dirname(result.files[0].path),
|
|
@@ -89,7 +89,7 @@ test("invokePluginCallTool returns absolute paths for materialized file parts",
|
|
|
89
89
|
|
|
90
90
|
const pending_parts = run_context.pendingAssistantFileParts;
|
|
91
91
|
assert.equal(pending_parts.length, 1);
|
|
92
|
-
assert.equal(pending_parts[0].url, result.files[0].
|
|
92
|
+
assert.equal(pending_parts[0].url, result.files[0].relative_path);
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
test("invokePluginReadTool returns plugin action metadata", async () => {
|
package/src/config/Paths.ts
CHANGED
|
@@ -336,7 +336,7 @@ export function getDowncityPublicDirPath(cwd: string): string {
|
|
|
336
336
|
*
|
|
337
337
|
* 关键点(中文)
|
|
338
338
|
* - 该目录用于存放会话历史引用的二进制资源,例如图片生成结果。
|
|
339
|
-
* - `messages.jsonl` 只保存
|
|
339
|
+
* - `messages.jsonl` 只保存 Agent 根目录相对路径,避免暴露本机绝对路径或长期保存 base64。
|
|
340
340
|
*/
|
|
341
341
|
export function getDowncityResourcesDirPath(cwd: string): string {
|
|
342
342
|
return path.join(getDowncityDirPath(cwd), "resources");
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - 只处理运行期产生的 assistant file part,不参与 user 附件注入。
|
|
6
6
|
* - 将 data URL、远程 URL 与本地文件统一写入 `.downcity/resources`。
|
|
7
|
-
* -
|
|
7
|
+
* - 历史中只保留基于 Agent 项目根目录的相对路径,避免暴露本机绝对路径。
|
|
8
8
|
* - 资源文件按内容 hash 命名,天然去重并避免重复写入大文件。
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -34,8 +34,8 @@ export interface MaterializeAssistantFilePartsParams {
|
|
|
34
34
|
* 当前项目根目录。
|
|
35
35
|
*
|
|
36
36
|
* 关键点(中文)
|
|
37
|
-
* - 正常 session run
|
|
38
|
-
* -
|
|
37
|
+
* - 正常 session run 必须显式传入 projectRoot。
|
|
38
|
+
* - 旧入口未传时仅为兼容回退到 `process.cwd()`。
|
|
39
39
|
*/
|
|
40
40
|
projectRoot?: string;
|
|
41
41
|
|
|
@@ -102,12 +102,58 @@ function filename_from_url(raw_url: string): string | undefined {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
/**
|
|
106
|
+
* 将项目内文件路径转换为基于 Agent 根目录的相对路径。
|
|
107
|
+
*/
|
|
108
|
+
export function toAgentRelativePath(params: {
|
|
109
|
+
/**
|
|
110
|
+
* 当前 Agent 项目根目录。
|
|
111
|
+
*/
|
|
112
|
+
projectRoot: string;
|
|
113
|
+
/**
|
|
114
|
+
* 项目内文件绝对路径。
|
|
115
|
+
*/
|
|
116
|
+
filePath: string;
|
|
117
|
+
}): string {
|
|
118
|
+
const project_root = resolve_project_root(params.projectRoot);
|
|
106
119
|
const relative = path
|
|
107
|
-
.relative(
|
|
120
|
+
.relative(project_root, params.filePath)
|
|
108
121
|
.split(path.sep)
|
|
109
122
|
.join("/");
|
|
110
|
-
|
|
123
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
124
|
+
throw new Error(`Assistant resource path is outside agent root: ${params.filePath}`);
|
|
125
|
+
}
|
|
126
|
+
return relative;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 将相对路径解析为 Agent 项目内绝对路径。
|
|
131
|
+
*
|
|
132
|
+
* 关键点(中文)
|
|
133
|
+
* - 相对路径统一基于 Agent 项目根目录解析。
|
|
134
|
+
* - 绝对路径原样归一化后返回。
|
|
135
|
+
* - 越界路径返回空字符串,由调用方决定是否忽略。
|
|
136
|
+
*/
|
|
137
|
+
export function resolveAgentFilePath(params: {
|
|
138
|
+
/**
|
|
139
|
+
* 当前 Agent 项目根目录。
|
|
140
|
+
*/
|
|
141
|
+
projectRoot: string;
|
|
142
|
+
/**
|
|
143
|
+
* 待解析的相对路径或绝对路径。
|
|
144
|
+
*/
|
|
145
|
+
filePath: string;
|
|
146
|
+
}): string {
|
|
147
|
+
const raw = String(params.filePath || "").trim();
|
|
148
|
+
if (!raw) return "";
|
|
149
|
+
const project_root = resolve_project_root(params.projectRoot);
|
|
150
|
+
const file_path = path.isAbsolute(raw)
|
|
151
|
+
? path.resolve(raw)
|
|
152
|
+
: path.resolve(project_root, raw);
|
|
153
|
+
if (path.isAbsolute(raw)) return file_path;
|
|
154
|
+
const rel = path.relative(project_root, file_path);
|
|
155
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return "";
|
|
156
|
+
return file_path;
|
|
111
157
|
}
|
|
112
158
|
|
|
113
159
|
async function write_resource_file(params: {
|
|
@@ -258,8 +304,6 @@ async function materialize_file_part(params: {
|
|
|
258
304
|
if (!raw_url) {
|
|
259
305
|
throw new Error("Assistant file part url is required");
|
|
260
306
|
}
|
|
261
|
-
if (raw_url.startsWith("resources://")) return params.part;
|
|
262
|
-
|
|
263
307
|
const parsed_data_url = parse_data_url(raw_url);
|
|
264
308
|
if (parsed_data_url) {
|
|
265
309
|
const media_type =
|
|
@@ -275,7 +319,10 @@ async function materialize_file_part(params: {
|
|
|
275
319
|
return {
|
|
276
320
|
...params.part,
|
|
277
321
|
mediaType: media_type,
|
|
278
|
-
url:
|
|
322
|
+
url: toAgentRelativePath({
|
|
323
|
+
projectRoot: params.projectRoot,
|
|
324
|
+
filePath: file_path,
|
|
325
|
+
}),
|
|
279
326
|
};
|
|
280
327
|
}
|
|
281
328
|
|
|
@@ -297,7 +344,10 @@ async function materialize_file_part(params: {
|
|
|
297
344
|
return {
|
|
298
345
|
...params.part,
|
|
299
346
|
mediaType: media_type,
|
|
300
|
-
url:
|
|
347
|
+
url: toAgentRelativePath({
|
|
348
|
+
projectRoot: params.projectRoot,
|
|
349
|
+
filePath: file_path,
|
|
350
|
+
}),
|
|
301
351
|
};
|
|
302
352
|
} catch (error) {
|
|
303
353
|
// 关键点(中文):远程下载失败时保留原始 URL,不让整张图导致 action 失败。
|
|
@@ -325,12 +375,15 @@ async function materialize_file_part(params: {
|
|
|
325
375
|
return {
|
|
326
376
|
...params.part,
|
|
327
377
|
mediaType: media_type,
|
|
328
|
-
url:
|
|
378
|
+
url: toAgentRelativePath({
|
|
379
|
+
projectRoot: params.projectRoot,
|
|
380
|
+
filePath: file_path,
|
|
381
|
+
}),
|
|
329
382
|
};
|
|
330
383
|
}
|
|
331
384
|
|
|
332
385
|
/**
|
|
333
|
-
* 将 assistant file part 中的资源统一落盘为
|
|
386
|
+
* 将 assistant file part 中的资源统一落盘为 Agent 根目录相对路径。
|
|
334
387
|
*/
|
|
335
388
|
export async function materializeAssistantFileParts(
|
|
336
389
|
params: MaterializeAssistantFilePartsParams,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - 兼容 Telegram / Feishu / TUI 等统一的 `<file>` 协议入口。
|
|
6
6
|
* - 仅在本轮执行的内存消息上追加 file parts,不修改持久化历史。
|
|
7
7
|
* - 当前只为图片与 PDF 注入 file part,保持多模态模型可直接消费。
|
|
8
|
-
* -
|
|
8
|
+
* - 历史中的相对路径与旧版 `file://` 会在喂给模型前临时 hydrate。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import fs from "fs-extra";
|
|
@@ -63,18 +63,17 @@ function buildDataUrl(mediaType: string, buffer: Buffer): string {
|
|
|
63
63
|
return `data:${safeType};base64,${base64}`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function
|
|
66
|
+
function resolveHydratableFilePath(
|
|
67
67
|
projectRoot: string | undefined,
|
|
68
|
-
|
|
68
|
+
rawPath: string,
|
|
69
69
|
): string | null {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
if (!relative) return null;
|
|
70
|
+
const raw = String(rawPath || "").trim();
|
|
71
|
+
if (!raw || raw.startsWith("data:") || /^https?:\/\//i.test(raw)) return null;
|
|
72
|
+
if (raw.startsWith("file://")) return fileURLToPath(raw);
|
|
73
|
+
if (path.isAbsolute(raw)) return path.resolve(raw);
|
|
75
74
|
|
|
76
75
|
const root = path.resolve(String(projectRoot || "").trim() || process.cwd());
|
|
77
|
-
const absPath = path.resolve(root,
|
|
76
|
+
const absPath = path.resolve(root, raw);
|
|
78
77
|
const rel = path.relative(root, absPath);
|
|
79
78
|
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
80
79
|
return absPath;
|
|
@@ -85,14 +84,13 @@ async function hydrateFileUrlPart(
|
|
|
85
84
|
projectRoot?: string,
|
|
86
85
|
): Promise<FileUIPart> {
|
|
87
86
|
const url = String(part.url || "").trim();
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
87
|
+
const filePath = resolveHydratableFilePath(projectRoot, url);
|
|
88
|
+
if (!filePath) return part;
|
|
90
89
|
try {
|
|
91
|
-
const
|
|
92
|
-
const buffer = await fs.readFile(absPath);
|
|
90
|
+
const buffer = await fs.readFile(filePath);
|
|
93
91
|
const mediaType =
|
|
94
92
|
String(part.mediaType || "").trim() ||
|
|
95
|
-
guessAttachmentMediaTypeFromPath(
|
|
93
|
+
guessAttachmentMediaTypeFromPath(filePath) ||
|
|
96
94
|
"application/octet-stream";
|
|
97
95
|
return {
|
|
98
96
|
...part,
|
|
@@ -109,7 +107,7 @@ async function hydrateFileUrlPart(
|
|
|
109
107
|
*
|
|
110
108
|
* 关键点(中文)
|
|
111
109
|
* - 该函数只修改本轮内存消息,不回写历史。
|
|
112
|
-
* - 新历史保留
|
|
110
|
+
* - 新历史保留 Agent 根目录相对路径,旧历史的 `file://` 仍继续兼容。
|
|
113
111
|
*/
|
|
114
112
|
export async function hydrateFileUrlPartsForModel(
|
|
115
113
|
messages: SessionMessageV1[],
|
|
@@ -148,10 +146,11 @@ export async function hydrateFileUrlPartsForModel(
|
|
|
148
146
|
*/
|
|
149
147
|
export async function injectFilePartsFromAttachments(
|
|
150
148
|
messages: SessionMessageV1[],
|
|
149
|
+
projectRoot?: string,
|
|
151
150
|
): Promise<SessionMessageV1[]> {
|
|
152
151
|
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
153
152
|
|
|
154
|
-
const
|
|
153
|
+
const root = path.resolve(String(projectRoot || "").trim() || process.cwd());
|
|
155
154
|
const out: SessionMessageV1[] = [];
|
|
156
155
|
|
|
157
156
|
for (const message of messages) {
|
|
@@ -203,7 +202,7 @@ export async function injectFilePartsFromAttachments(
|
|
|
203
202
|
|
|
204
203
|
const absPath = path.isAbsolute(attachment.path)
|
|
205
204
|
? attachment.path
|
|
206
|
-
: path.resolve(
|
|
205
|
+
: path.resolve(root, attachment.path);
|
|
207
206
|
try {
|
|
208
207
|
const exists = await fs.pathExists(absPath);
|
|
209
208
|
if (!exists) continue;
|
|
@@ -78,7 +78,10 @@ export async function toModelMessages(
|
|
|
78
78
|
if (!Array.isArray(messages) || messages.length === 0) return [];
|
|
79
79
|
|
|
80
80
|
// 第一步(中文):在 user 消息上注入 file parts(多模态附件)。
|
|
81
|
-
const enrichedMessages = await injectFilePartsFromAttachments(
|
|
81
|
+
const enrichedMessages = await injectFilePartsFromAttachments(
|
|
82
|
+
messages,
|
|
83
|
+
projectRoot,
|
|
84
|
+
);
|
|
82
85
|
|
|
83
86
|
// 第二步(中文):把历史里的资源 URL 在内存中 hydrate 成模型可消费的 data URL。
|
|
84
87
|
const hydratedMessages = await hydrateFileUrlPartsForModel(
|
|
@@ -18,7 +18,10 @@ import type {
|
|
|
18
18
|
PluginReadInput,
|
|
19
19
|
PluginReadToolResult,
|
|
20
20
|
} from "@/executor/tools/plugin/types/PluginTool.js";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
materializeAssistantFileParts,
|
|
23
|
+
resolveAgentFilePath,
|
|
24
|
+
} from "@executor/messages/AssistantFileResource.js";
|
|
22
25
|
import {
|
|
23
26
|
enqueueAssistantFileParts,
|
|
24
27
|
getSessionRunContext,
|
|
@@ -81,25 +84,6 @@ function resolve_project_root(project_root: string | undefined): string {
|
|
|
81
84
|
return path.resolve(raw || process.cwd());
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
/**
|
|
85
|
-
* 将 `resources://` URL 转成本机绝对路径。
|
|
86
|
-
*/
|
|
87
|
-
function resolve_resources_file_path(
|
|
88
|
-
project_root: string,
|
|
89
|
-
raw_url: string,
|
|
90
|
-
): string {
|
|
91
|
-
const prefix = "resources://";
|
|
92
|
-
const raw = String(raw_url || "").trim();
|
|
93
|
-
if (!raw.startsWith(prefix)) return "";
|
|
94
|
-
const relative = raw.slice(prefix.length).replace(/^\/+/, "");
|
|
95
|
-
if (!relative) return "";
|
|
96
|
-
|
|
97
|
-
const file_path = path.resolve(project_root, relative);
|
|
98
|
-
const rel = path.relative(project_root, file_path);
|
|
99
|
-
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return "";
|
|
100
|
-
return file_path;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
87
|
/**
|
|
104
88
|
* 构建返回给模型和用户可见的文件摘要。
|
|
105
89
|
*/
|
|
@@ -107,13 +91,19 @@ function summarize_materialized_files(
|
|
|
107
91
|
parts: FileUIPart[],
|
|
108
92
|
project_root: string,
|
|
109
93
|
): PluginCallToolFileResult[] {
|
|
110
|
-
return parts.map((part, index) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
94
|
+
return parts.map((part, index) => {
|
|
95
|
+
const relative_path = String(part.url || "");
|
|
96
|
+
return {
|
|
97
|
+
index,
|
|
98
|
+
media_type: part.mediaType,
|
|
99
|
+
filename: typeof part.filename === "string" ? part.filename : "",
|
|
100
|
+
relative_path,
|
|
101
|
+
path: resolveAgentFilePath({
|
|
102
|
+
projectRoot: project_root,
|
|
103
|
+
filePath: relative_path,
|
|
104
|
+
}),
|
|
105
|
+
};
|
|
106
|
+
});
|
|
117
107
|
}
|
|
118
108
|
|
|
119
109
|
/**
|
|
@@ -40,9 +40,9 @@ export interface PluginCallToolFileResult {
|
|
|
40
40
|
media_type: string;
|
|
41
41
|
/** 原始文件名;若上游未提供则为空字符串。 */
|
|
42
42
|
filename: string;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
/**
|
|
43
|
+
/** 基于 Agent 项目根目录的相对路径,例如 `.downcity/resources/xxx.png`。 */
|
|
44
|
+
relative_path: string;
|
|
45
|
+
/** 当前机器可直接打开的绝对文件路径,按 Agent 项目根目录解析。 */
|
|
46
46
|
path: string;
|
|
47
47
|
}
|
|
48
48
|
|