@downcity/agent 1.1.160 → 1.1.162

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.
Files changed (32) hide show
  1. package/bin/config/Paths.d.ts +1 -1
  2. package/bin/config/Paths.js +1 -1
  3. package/bin/executor/core-engine/CoreEngineMessageState.d.ts +1 -1
  4. package/bin/executor/core-engine/CoreEngineMessageState.js +1 -1
  5. package/bin/executor/messages/AssistantFileResource.d.ts +35 -4
  6. package/bin/executor/messages/AssistantFileResource.d.ts.map +1 -1
  7. package/bin/executor/messages/AssistantFileResource.js +47 -10
  8. package/bin/executor/messages/AssistantFileResource.js.map +1 -1
  9. package/bin/executor/messages/SessionAttachmentMapper.d.ts +3 -3
  10. package/bin/executor/messages/SessionAttachmentMapper.d.ts.map +1 -1
  11. package/bin/executor/messages/SessionAttachmentMapper.js +17 -18
  12. package/bin/executor/messages/SessionAttachmentMapper.js.map +1 -1
  13. package/bin/executor/messages/SessionMessageCodec.d.ts.map +1 -1
  14. package/bin/executor/messages/SessionMessageCodec.js +1 -1
  15. package/bin/executor/messages/SessionMessageCodec.js.map +1 -1
  16. package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
  17. package/bin/executor/tools/plugin/PluginToolBridge.js +14 -25
  18. package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
  19. package/bin/executor/tools/plugin/types/PluginTool.d.ts +3 -3
  20. package/bin/executor/tools/plugin/types/PluginTool.d.ts.map +1 -1
  21. package/package.json +3 -4
  22. package/scripts/assistant-file-resource.test.mjs +77 -11
  23. package/scripts/image-plugin-job.test.mjs +2 -1
  24. package/scripts/plugin-tool-bridge.test.mjs +4 -4
  25. package/src/config/Paths.ts +1 -1
  26. package/src/executor/core-engine/CoreEngineMessageState.ts +1 -1
  27. package/src/executor/messages/AssistantFileResource.ts +65 -12
  28. package/src/executor/messages/SessionAttachmentMapper.ts +16 -17
  29. package/src/executor/messages/SessionMessageCodec.ts +4 -1
  30. package/src/executor/tools/plugin/PluginToolBridge.ts +17 -27
  31. package/src/executor/tools/plugin/types/PluginTool.ts +3 -3
  32. package/tsconfig.tsbuildinfo +1 -1
@@ -1,9 +1,9 @@
1
1
  /**
2
- * @file 验证 assistant file part 会把资源落盘为 resources:// URL。
2
+ * @file 验证 assistant file part 会把资源落盘为 Agent 根目录相对路径。
3
3
  *
4
4
  * 关键点(中文)
5
5
  * - 历史消息不应长期保存图片 base64。
6
- * - 送模型前可以从 resources:// 临时 hydrate 回 data URL。
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 { hydrateFileUrlPartsForModel } from "../bin/executor/messages/SessionAttachmentMapper.js";
18
+ import {
19
+ hydrateFileUrlPartsForModel,
20
+ injectFilePartsFromAttachments,
21
+ } from "../bin/executor/messages/SessionAttachmentMapper.js";
19
22
 
20
- function resource_path_from_url(project_root, url) {
21
- return path.join(project_root, String(url || "").replace(/^resources:\/\//, ""));
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, /^resources:\/\/\.downcity\/resources\//);
50
+ assert.match(parts[0].url, /^\.downcity\/resources\//);
48
51
  assert.equal(parts[0].url.includes("base64"), false);
49
52
 
50
- const resource_path = resource_path_from_url(project_root, parts[0].url);
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, /^resources:\/\/\.downcity\/resources\//);
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(resource_path_from_url(project_root, parts[0].url)),
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("hydrateFileUrlPartsForModel converts resources URLs back to data URLs in memory", async () => {
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, /^resources:\/\/\.downcity\/resources\//);
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 只读取一次当前状态,不在 plugin 层等待终态。
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 仍然使用 `resources://` URL,避免历史暴露本机路径。
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].url, /^resources:\/\/\.downcity\/resources\//);
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].url);
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 () => {
@@ -336,7 +336,7 @@ export function getDowncityPublicDirPath(cwd: string): string {
336
336
  *
337
337
  * 关键点(中文)
338
338
  * - 该目录用于存放会话历史引用的二进制资源,例如图片生成结果。
339
- * - `messages.jsonl` 只保存 `resources://` 相对 URL,避免暴露本机绝对路径或长期保存 base64。
339
+ * - `messages.jsonl` 只保存 Agent 根目录相对路径,避免暴露本机绝对路径或长期保存 base64。
340
340
  */
341
341
  export function getDowncityResourcesDirPath(cwd: string): string {
342
342
  return path.join(getDowncityDirPath(cwd), "resources");
@@ -34,7 +34,7 @@ export class CoreEngineMessageState {
34
34
  private readonly tools: Record<string, Tool>;
35
35
 
36
36
  /**
37
- * 当前项目根目录,用于解析历史中的 `resources://` file part。
37
+ * 当前项目根目录,用于解析历史中的相对路径 file part。
38
38
  */
39
39
  private readonly projectRoot?: string;
40
40
 
@@ -4,7 +4,7 @@
4
4
  * 关键点(中文)
5
5
  * - 只处理运行期产生的 assistant file part,不参与 user 附件注入。
6
6
  * - 将 data URL、远程 URL 与本地文件统一写入 `.downcity/resources`。
7
- * - 历史中只保留 `resources://<project-relative-path>`,避免暴露本机绝对路径。
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 会显式传入 projectRoot。
38
- * - 旧入口未传时回退到 `process.cwd()`,保证行为可用。
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
- function to_resources_url(projectRoot: string, filePath: string): string {
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(projectRoot, filePath)
120
+ .relative(project_root, params.filePath)
108
121
  .split(path.sep)
109
122
  .join("/");
110
- return `resources://${relative}`;
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: to_resources_url(params.projectRoot, file_path),
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: to_resources_url(params.projectRoot, file_path),
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: to_resources_url(params.projectRoot, file_path),
378
+ url: toAgentRelativePath({
379
+ projectRoot: params.projectRoot,
380
+ filePath: file_path,
381
+ }),
329
382
  };
330
383
  }
331
384
 
332
385
  /**
333
- * 将 assistant file part 中的资源统一落盘为 `resources://` 相对 URL。
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
- * - 历史中的 `resources://` 与旧版 `file://` 会在喂给模型前临时 hydrate。
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 resolveResourcesUrlPath(
66
+ function resolveHydratableFilePath(
67
67
  projectRoot: string | undefined,
68
- rawUrl: string,
68
+ rawPath: string,
69
69
  ): string | null {
70
- const prefix = "resources://";
71
- const raw = String(rawUrl || "").trim();
72
- if (!raw.startsWith(prefix)) return null;
73
- const relative = raw.slice(prefix.length).replace(/^\/+/, "");
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, relative);
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 resourcesPath = resolveResourcesUrlPath(projectRoot, url);
89
- if (!url.startsWith("file://") && !resourcesPath) return part;
87
+ const filePath = resolveHydratableFilePath(projectRoot, url);
88
+ if (!filePath) return part;
90
89
  try {
91
- const absPath = resourcesPath || fileURLToPath(url);
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(absPath) ||
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
- * - 新历史保留 `resources://` 相对 URL,旧历史的 `file://` 仍继续兼容。
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 cwd = process.cwd();
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(cwd, attachment.path);
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(messages);
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 { materializeAssistantFileParts } from "@executor/messages/AssistantFileResource.js";
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
- index,
112
- media_type: part.mediaType,
113
- filename: typeof part.filename === "string" ? part.filename : "",
114
- url: String(part.url || ""),
115
- path: resolve_resources_file_path(project_root, String(part.url || "")),
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
- /** 持久化到历史消息中的资源 URL,通常为 `resources://.downcity/resources/...`。 */
44
- url: string;
45
- /** 当前机器可直接打开的绝对文件路径。 */
43
+ /** 基于 Agent 项目根目录的相对路径,例如 `.downcity/resources/xxx.png`。 */
44
+ relative_path: string;
45
+ /** 当前机器可直接打开的绝对文件路径,按 Agent 项目根目录解析。 */
46
46
  path: string;
47
47
  }
48
48