@downcity/agent 1.1.104 → 1.1.106
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/DowncitySchema.d.ts.map +1 -1
- package/bin/config/DowncitySchema.js +0 -69
- package/bin/config/DowncitySchema.js.map +1 -1
- package/bin/config/Paths.d.ts +1 -1
- package/bin/config/Paths.js +1 -1
- package/bin/executor/composer/system/default/assets/plugin.prompt.d.ts +1 -1
- package/bin/executor/composer/system/default/assets/plugin.prompt.d.ts.map +1 -1
- package/bin/executor/composer/system/default/assets/plugin.prompt.js +1 -1
- package/bin/executor/composer/system/default/assets/plugin.prompt.js.map +1 -1
- package/bin/executor/core-engine/CoreEngineMessageState.d.ts +8 -0
- package/bin/executor/core-engine/CoreEngineMessageState.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineMessageState.js +9 -3
- package/bin/executor/core-engine/CoreEngineMessageState.js.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.js +1 -0
- package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
- package/bin/executor/messages/SessionAttachmentMapper.d.ts +4 -3
- package/bin/executor/messages/SessionAttachmentMapper.d.ts.map +1 -1
- package/bin/executor/messages/SessionAttachmentMapper.js +24 -7
- package/bin/executor/messages/SessionAttachmentMapper.js.map +1 -1
- package/bin/executor/messages/SessionMessageCodec.d.ts +1 -1
- package/bin/executor/messages/SessionMessageCodec.d.ts.map +1 -1
- package/bin/executor/messages/SessionMessageCodec.js +3 -3
- package/bin/executor/messages/SessionMessageCodec.js.map +1 -1
- package/package.json +2 -2
- package/scripts/assistant-file-resource.test.mjs +107 -18
- package/src/config/DowncitySchema.ts +0 -69
- package/src/config/Paths.ts +1 -1
- package/src/executor/composer/system/default/assets/plugin.prompt.ts +1 -1
- package/src/executor/composer/system/default/assets/plugin.prompt.ts.txt +1 -1
- package/src/executor/core-engine/CoreEngineMessageState.ts +26 -2
- package/src/executor/core-engine/CoreEngineRunner.ts +1 -0
- package/src/executor/messages/SessionAttachmentMapper.ts +29 -6
- package/src/executor/messages/SessionMessageCodec.ts +6 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file 验证 assistant file part
|
|
2
|
+
* @file 验证 assistant file part 会把资源落盘为 resources:// URL。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - 历史消息不应长期保存图片 base64。
|
|
6
|
-
* - 送模型前可以从
|
|
6
|
+
* - 送模型前可以从 resources:// 临时 hydrate 回 data URL。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import test from "node:test";
|
|
@@ -11,11 +11,16 @@ import assert from "node:assert/strict";
|
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import fs from "node:fs/promises";
|
|
14
|
-
import
|
|
14
|
+
import http from "node:http";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
15
16
|
|
|
16
17
|
import { materializeAssistantFileParts } from "../bin/executor/messages/AssistantFileResource.js";
|
|
17
18
|
import { hydrateFileUrlPartsForModel } from "../bin/executor/messages/SessionAttachmentMapper.js";
|
|
18
19
|
|
|
20
|
+
function resource_path_from_url(project_root, url) {
|
|
21
|
+
return path.join(project_root, String(url || "").replace(/^resources:\/\//, ""));
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
test("materializeAssistantFileParts stores data URL images under .downcity/resources", async () => {
|
|
20
25
|
const project_root = await fs.mkdtemp(
|
|
21
26
|
path.join(os.tmpdir(), "downcity-agent-assistant-resource-"),
|
|
@@ -39,10 +44,10 @@ test("materializeAssistantFileParts stores data URL images under .downcity/resou
|
|
|
39
44
|
assert.equal(parts[0].type, "file");
|
|
40
45
|
assert.equal(parts[0].mediaType, "image/png");
|
|
41
46
|
assert.equal(parts[0].filename, "image-1.png");
|
|
42
|
-
assert.match(parts[0].url, /^
|
|
47
|
+
assert.match(parts[0].url, /^resources:\/\/\.downcity\/resources\//);
|
|
43
48
|
assert.equal(parts[0].url.includes("base64"), false);
|
|
44
49
|
|
|
45
|
-
const resource_path =
|
|
50
|
+
const resource_path = resource_path_from_url(project_root, parts[0].url);
|
|
46
51
|
assert.equal(
|
|
47
52
|
path.dirname(resource_path),
|
|
48
53
|
path.join(project_root, ".downcity", "resources"),
|
|
@@ -50,7 +55,49 @@ test("materializeAssistantFileParts stores data URL images under .downcity/resou
|
|
|
50
55
|
assert.deepEqual(await fs.readFile(resource_path), bytes);
|
|
51
56
|
});
|
|
52
57
|
|
|
53
|
-
test("
|
|
58
|
+
test("materializeAssistantFileParts downloads remote file URLs into resources", async () => {
|
|
59
|
+
const project_root = await fs.mkdtemp(
|
|
60
|
+
path.join(os.tmpdir(), "downcity-agent-assistant-remote-resource-"),
|
|
61
|
+
);
|
|
62
|
+
const bytes = Buffer.from("remote-png-bytes-for-test", "utf8");
|
|
63
|
+
const server = http.createServer((_, response) => {
|
|
64
|
+
response.writeHead(200, { "content-type": "image/png" });
|
|
65
|
+
response.end(bytes);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
69
|
+
try {
|
|
70
|
+
const address = server.address();
|
|
71
|
+
assert.equal(typeof address, "object");
|
|
72
|
+
assert.ok(address);
|
|
73
|
+
const remote_url = `http://127.0.0.1:${address.port}/image.png`;
|
|
74
|
+
|
|
75
|
+
const parts = await materializeAssistantFileParts({
|
|
76
|
+
projectRoot: project_root,
|
|
77
|
+
parts: [
|
|
78
|
+
{
|
|
79
|
+
type: "file",
|
|
80
|
+
mediaType: "image/png",
|
|
81
|
+
filename: "remote.png",
|
|
82
|
+
url: remote_url,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
assert.match(parts[0].url, /^resources:\/\/\.downcity\/resources\//);
|
|
88
|
+
assert.equal(parts[0].url.startsWith("http://"), false);
|
|
89
|
+
assert.deepEqual(
|
|
90
|
+
await fs.readFile(resource_path_from_url(project_root, parts[0].url)),
|
|
91
|
+
bytes,
|
|
92
|
+
);
|
|
93
|
+
} finally {
|
|
94
|
+
await new Promise((resolve, reject) =>
|
|
95
|
+
server.close((error) => (error ? reject(error) : resolve())),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("hydrateFileUrlPartsForModel converts resources URLs back to data URLs in memory", async () => {
|
|
54
101
|
const project_root = await fs.mkdtemp(
|
|
55
102
|
path.join(os.tmpdir(), "downcity-agent-assistant-hydrate-"),
|
|
56
103
|
);
|
|
@@ -67,18 +114,21 @@ test("hydrateFileUrlPartsForModel converts persisted file URLs back to data URLs
|
|
|
67
114
|
],
|
|
68
115
|
});
|
|
69
116
|
|
|
70
|
-
const messages = await hydrateFileUrlPartsForModel(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
117
|
+
const messages = await hydrateFileUrlPartsForModel(
|
|
118
|
+
[
|
|
119
|
+
{
|
|
120
|
+
id: "a:test:1",
|
|
121
|
+
role: "assistant",
|
|
122
|
+
metadata: {
|
|
123
|
+
v: 1,
|
|
124
|
+
ts: Date.now(),
|
|
125
|
+
sessionId: "session_test",
|
|
126
|
+
},
|
|
127
|
+
parts: materialized,
|
|
78
128
|
},
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
129
|
+
],
|
|
130
|
+
project_root,
|
|
131
|
+
);
|
|
82
132
|
|
|
83
133
|
const hydrated_part = messages[0]?.parts[0];
|
|
84
134
|
assert.equal(hydrated_part?.type, "file");
|
|
@@ -87,5 +137,44 @@ test("hydrateFileUrlPartsForModel converts persisted file URLs back to data URLs
|
|
|
87
137
|
hydrated_part?.url,
|
|
88
138
|
`data:image/png;base64,${bytes.toString("base64")}`,
|
|
89
139
|
);
|
|
90
|
-
assert.match(materialized[0].url, /^
|
|
140
|
+
assert.match(materialized[0].url, /^resources:\/\/\.downcity\/resources\//);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("hydrateFileUrlPartsForModel keeps old file URLs compatible", async () => {
|
|
144
|
+
const project_root = await fs.mkdtemp(
|
|
145
|
+
path.join(os.tmpdir(), "downcity-agent-assistant-file-url-"),
|
|
146
|
+
);
|
|
147
|
+
const bytes = Buffer.from("legacy-file-url-bytes", "utf8");
|
|
148
|
+
const file_path = path.join(project_root, "legacy.png");
|
|
149
|
+
await fs.writeFile(file_path, bytes);
|
|
150
|
+
|
|
151
|
+
const messages = await hydrateFileUrlPartsForModel(
|
|
152
|
+
[
|
|
153
|
+
{
|
|
154
|
+
id: "a:test:legacy",
|
|
155
|
+
role: "assistant",
|
|
156
|
+
metadata: {
|
|
157
|
+
v: 1,
|
|
158
|
+
ts: Date.now(),
|
|
159
|
+
sessionId: "session_test",
|
|
160
|
+
},
|
|
161
|
+
parts: [
|
|
162
|
+
{
|
|
163
|
+
type: "file",
|
|
164
|
+
mediaType: "image/png",
|
|
165
|
+
filename: "legacy.png",
|
|
166
|
+
url: pathToFileURL(file_path).href,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
project_root,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const hydrated_part = messages[0]?.parts[0];
|
|
175
|
+
assert.equal(hydrated_part?.type, "file");
|
|
176
|
+
assert.equal(
|
|
177
|
+
hydrated_part?.url,
|
|
178
|
+
`data:image/png;base64,${bytes.toString("base64")}`,
|
|
179
|
+
);
|
|
91
180
|
});
|
|
@@ -141,75 +141,6 @@ export const DOWNCITY_JSON_SCHEMA: JsonObject = {
|
|
|
141
141
|
},
|
|
142
142
|
},
|
|
143
143
|
},
|
|
144
|
-
asr: {
|
|
145
|
-
type: "object",
|
|
146
|
-
additionalProperties: true,
|
|
147
|
-
properties: {
|
|
148
|
-
injectPrompt: { type: "boolean" },
|
|
149
|
-
augmentMessage: { type: "boolean" },
|
|
150
|
-
provider: { type: "string", enum: ["local", "command"] },
|
|
151
|
-
modelId: {
|
|
152
|
-
type: "string",
|
|
153
|
-
enum: [
|
|
154
|
-
"SenseVoiceSmall",
|
|
155
|
-
"paraformer-zh-streaming",
|
|
156
|
-
"whisper-large-v3-turbo",
|
|
157
|
-
],
|
|
158
|
-
},
|
|
159
|
-
modelsDir: { type: "string" },
|
|
160
|
-
pythonBin: { type: "string" },
|
|
161
|
-
command: { type: "string" },
|
|
162
|
-
language: { type: "string" },
|
|
163
|
-
timeoutMs: { type: "integer", minimum: 1000, maximum: 600000 },
|
|
164
|
-
strategy: {
|
|
165
|
-
type: "string",
|
|
166
|
-
enum: ["auto", "funasr", "transformers-whisper", "command"],
|
|
167
|
-
},
|
|
168
|
-
installedModels: {
|
|
169
|
-
type: "array",
|
|
170
|
-
items: {
|
|
171
|
-
type: "string",
|
|
172
|
-
enum: [
|
|
173
|
-
"SenseVoiceSmall",
|
|
174
|
-
"paraformer-zh-streaming",
|
|
175
|
-
"whisper-large-v3-turbo",
|
|
176
|
-
],
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
tts: {
|
|
182
|
-
type: "object",
|
|
183
|
-
additionalProperties: true,
|
|
184
|
-
properties: {
|
|
185
|
-
provider: {
|
|
186
|
-
type: "string",
|
|
187
|
-
enum: ["local"],
|
|
188
|
-
},
|
|
189
|
-
modelId: {
|
|
190
|
-
type: "string",
|
|
191
|
-
enum: ["qwen3-tts-0.6b", "kokoro-82m", "qwen3-tts-1.7b"],
|
|
192
|
-
},
|
|
193
|
-
modelsDir: { type: "string" },
|
|
194
|
-
pythonBin: { type: "string" },
|
|
195
|
-
language: { type: "string" },
|
|
196
|
-
voice: { type: "string" },
|
|
197
|
-
format: {
|
|
198
|
-
type: "string",
|
|
199
|
-
enum: ["wav", "flac"],
|
|
200
|
-
},
|
|
201
|
-
speed: { type: "number", minimum: 0.5, maximum: 2 },
|
|
202
|
-
outputDir: { type: "string" },
|
|
203
|
-
timeoutMs: { type: "integer", minimum: 5000, maximum: 900000 },
|
|
204
|
-
installedModels: {
|
|
205
|
-
type: "array",
|
|
206
|
-
items: {
|
|
207
|
-
type: "string",
|
|
208
|
-
enum: ["qwen3-tts-0.6b", "kokoro-82m", "qwen3-tts-1.7b"],
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
144
|
},
|
|
214
145
|
},
|
|
215
146
|
llm: {
|
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` 只保存 `resources://` 相对 URL,避免暴露本机绝对路径或长期保存 base64。
|
|
340
340
|
*/
|
|
341
341
|
export function getDowncityResourcesDirPath(cwd: string): string {
|
|
342
342
|
return path.join(getDowncityDirPath(cwd), "resources");
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// Source: src/executor/composer/system/default/assets/plugin.prompt.ts.txt
|
|
7
|
-
const TEXT_MODULE_CONTENT = "# Plugin State\n\n你正在一个基于 plugin 的执行环境中工作。\n\n## Plugin 调用规则\n\n- 当你需要使用 plugin 能力时,优先通过可用的 tool 调用 plugin action。\n- 若当前工具集中存在 `plugin_call`,使用 `plugin_call({ plugin, action, payload })` 触发对应 plugin action。\n- `plugin_call.plugin` 是 plugin 名称,例如 `skill`、`task`、`memory`、`contact`。\n- `plugin_call.action` 是 action 名称,例如 `list`、`lookup`、`create`、`run`。\n- `plugin_call.payload` 是结构化 JSON payload;没有参数时传 `{}`。\n- ActionSchedule 是 Agent 内部用于延迟执行 plugin action 的能力,不是独立 plugin。\n\n## 可用 plugin 概览\n\n- 当前内建托管 plugin:`shell` / `chat` / `task` / `memory` / `contact`。\n- 当前内建本地 plugin:`auth` / `skill
|
|
7
|
+
const TEXT_MODULE_CONTENT = "# Plugin State\n\n你正在一个基于 plugin 的执行环境中工作。\n\n## Plugin 调用规则\n\n- 当你需要使用 plugin 能力时,优先通过可用的 tool 调用 plugin action。\n- 若当前工具集中存在 `plugin_call`,使用 `plugin_call({ plugin, action, payload })` 触发对应 plugin action。\n- `plugin_call.plugin` 是 plugin 名称,例如 `skill`、`task`、`memory`、`contact`。\n- `plugin_call.action` 是 action 名称,例如 `list`、`lookup`、`create`、`run`。\n- `plugin_call.payload` 是结构化 JSON payload;没有参数时传 `{}`。\n- ActionSchedule 是 Agent 内部用于延迟执行 plugin action 的能力,不是独立 plugin。\n\n## 可用 plugin 概览\n\n- 当前内建托管 plugin:`shell` / `chat` / `task` / `memory` / `contact`。\n- 当前内建本地 plugin:`auth` / `skill`。\n\n具体 plugin 能力以该 plugin 的 action 和 system 提示为准。\n";
|
|
8
8
|
|
|
9
9
|
export default TEXT_MODULE_CONTENT;
|
|
@@ -33,6 +33,11 @@ export class CoreEngineMessageState {
|
|
|
33
33
|
*/
|
|
34
34
|
private readonly tools: Record<string, Tool>;
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* 当前项目根目录,用于解析历史中的 `resources://` file part。
|
|
38
|
+
*/
|
|
39
|
+
private readonly projectRoot?: string;
|
|
40
|
+
|
|
36
41
|
private constructor(params: {
|
|
37
42
|
/**
|
|
38
43
|
* 当前运行时 session 语义消息。
|
|
@@ -46,10 +51,15 @@ export class CoreEngineMessageState {
|
|
|
46
51
|
* 当前轮可用工具集合。
|
|
47
52
|
*/
|
|
48
53
|
tools: Record<string, Tool>;
|
|
54
|
+
/**
|
|
55
|
+
* 当前项目根目录。
|
|
56
|
+
*/
|
|
57
|
+
projectRoot?: string;
|
|
49
58
|
}) {
|
|
50
59
|
this.sessionMessages = params.sessionMessages;
|
|
51
60
|
this.currentModelMessages = params.modelMessages;
|
|
52
61
|
this.tools = params.tools;
|
|
62
|
+
this.projectRoot = params.projectRoot;
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
/**
|
|
@@ -64,14 +74,23 @@ export class CoreEngineMessageState {
|
|
|
64
74
|
* 当前轮可用工具集合。
|
|
65
75
|
*/
|
|
66
76
|
tools: Record<string, Tool>;
|
|
77
|
+
/**
|
|
78
|
+
* 当前项目根目录。
|
|
79
|
+
*/
|
|
80
|
+
projectRoot?: string;
|
|
67
81
|
}): Promise<CoreEngineMessageState> {
|
|
68
82
|
const sessionMessages = Array.isArray(params.messages)
|
|
69
83
|
? [...params.messages]
|
|
70
84
|
: [];
|
|
71
85
|
return new CoreEngineMessageState({
|
|
72
86
|
sessionMessages,
|
|
73
|
-
modelMessages: await toModelMessages(
|
|
87
|
+
modelMessages: await toModelMessages(
|
|
88
|
+
sessionMessages,
|
|
89
|
+
params.tools,
|
|
90
|
+
params.projectRoot,
|
|
91
|
+
),
|
|
74
92
|
tools: params.tools,
|
|
93
|
+
projectRoot: params.projectRoot,
|
|
75
94
|
});
|
|
76
95
|
}
|
|
77
96
|
|
|
@@ -119,7 +138,11 @@ export class CoreEngineMessageState {
|
|
|
119
138
|
messages: SessionMessageV1[],
|
|
120
139
|
): Promise<ModelMessage[]> {
|
|
121
140
|
this.sessionMessages = [...this.sessionMessages, ...messages];
|
|
122
|
-
const modelMessages = await toModelMessages(
|
|
141
|
+
const modelMessages = await toModelMessages(
|
|
142
|
+
messages,
|
|
143
|
+
this.tools,
|
|
144
|
+
this.projectRoot,
|
|
145
|
+
);
|
|
123
146
|
if (modelMessages.length > 0) {
|
|
124
147
|
this.currentModelMessages = [...this.currentModelMessages, ...modelMessages];
|
|
125
148
|
return modelMessages;
|
|
@@ -127,6 +150,7 @@ export class CoreEngineMessageState {
|
|
|
127
150
|
this.currentModelMessages = await toModelMessages(
|
|
128
151
|
this.sessionMessages,
|
|
129
152
|
this.tools,
|
|
153
|
+
this.projectRoot,
|
|
130
154
|
);
|
|
131
155
|
return [];
|
|
132
156
|
}
|
|
@@ -154,6 +154,7 @@ export class CoreEngineRunner {
|
|
|
154
154
|
const message_state = await CoreEngineMessageState.create({
|
|
155
155
|
messages: input.execute_input.messages,
|
|
156
156
|
tools,
|
|
157
|
+
projectRoot: input.run_context.projectRoot,
|
|
157
158
|
});
|
|
158
159
|
|
|
159
160
|
const append_merged_user_messages = (messages: SessionMessageV1[]) =>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* - 兼容 Telegram / Feishu / TUI 等统一的 `<file>` 协议入口。
|
|
6
6
|
* - 仅在本轮执行的内存消息上追加 file parts,不修改持久化历史。
|
|
7
7
|
* - 当前只为图片与 PDF 注入 file part,保持多模态模型可直接消费。
|
|
8
|
+
* - 历史中的 `resources://` 与旧版 `file://` 会在喂给模型前临时 hydrate。
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import fs from "fs-extra";
|
|
@@ -62,11 +63,32 @@ function buildDataUrl(mediaType: string, buffer: Buffer): string {
|
|
|
62
63
|
return `data:${safeType};base64,${base64}`;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
function resolveResourcesUrlPath(
|
|
67
|
+
projectRoot: string | undefined,
|
|
68
|
+
rawUrl: string,
|
|
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;
|
|
75
|
+
|
|
76
|
+
const root = path.resolve(String(projectRoot || "").trim() || process.cwd());
|
|
77
|
+
const absPath = path.resolve(root, relative);
|
|
78
|
+
const rel = path.relative(root, absPath);
|
|
79
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
80
|
+
return absPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function hydrateFileUrlPart(
|
|
84
|
+
part: FileUIPart,
|
|
85
|
+
projectRoot?: string,
|
|
86
|
+
): Promise<FileUIPart> {
|
|
66
87
|
const url = String(part.url || "").trim();
|
|
67
|
-
|
|
88
|
+
const resourcesPath = resolveResourcesUrlPath(projectRoot, url);
|
|
89
|
+
if (!url.startsWith("file://") && !resourcesPath) return part;
|
|
68
90
|
try {
|
|
69
|
-
const absPath = fileURLToPath(url);
|
|
91
|
+
const absPath = resourcesPath || fileURLToPath(url);
|
|
70
92
|
const buffer = await fs.readFile(absPath);
|
|
71
93
|
const mediaType =
|
|
72
94
|
String(part.mediaType || "").trim() ||
|
|
@@ -83,14 +105,15 @@ async function hydrateFileUrlPart(part: FileUIPart): Promise<FileUIPart> {
|
|
|
83
105
|
}
|
|
84
106
|
|
|
85
107
|
/**
|
|
86
|
-
*
|
|
108
|
+
* 将历史中的资源 file part 临时转换为模型可消费的 data URL。
|
|
87
109
|
*
|
|
88
110
|
* 关键点(中文)
|
|
89
111
|
* - 该函数只修改本轮内存消息,不回写历史。
|
|
90
|
-
* -
|
|
112
|
+
* - 新历史保留 `resources://` 相对 URL,旧历史的 `file://` 仍继续兼容。
|
|
91
113
|
*/
|
|
92
114
|
export async function hydrateFileUrlPartsForModel(
|
|
93
115
|
messages: SessionMessageV1[],
|
|
116
|
+
projectRoot?: string,
|
|
94
117
|
): Promise<SessionMessageV1[]> {
|
|
95
118
|
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
96
119
|
|
|
@@ -109,7 +132,7 @@ export async function hydrateFileUrlPartsForModel(
|
|
|
109
132
|
nextParts.push(part);
|
|
110
133
|
continue;
|
|
111
134
|
}
|
|
112
|
-
const nextPart = await hydrateFileUrlPart(part as FileUIPart);
|
|
135
|
+
const nextPart = await hydrateFileUrlPart(part as FileUIPart, projectRoot);
|
|
113
136
|
if (nextPart !== part) changed = true;
|
|
114
137
|
nextParts.push(nextPart as SessionMessageV1["parts"][number]);
|
|
115
138
|
}
|
|
@@ -72,6 +72,7 @@ export function pickMergedUserMessages(
|
|
|
72
72
|
export async function toModelMessages(
|
|
73
73
|
messages: SessionMessageV1[],
|
|
74
74
|
tools: Record<string, Tool>,
|
|
75
|
+
projectRoot?: string,
|
|
75
76
|
): Promise<ModelMessage[]> {
|
|
76
77
|
// 空输入快速返回,避免调用转换器的额外开销。
|
|
77
78
|
if (!Array.isArray(messages) || messages.length === 0) return [];
|
|
@@ -79,8 +80,11 @@ export async function toModelMessages(
|
|
|
79
80
|
// 第一步(中文):在 user 消息上注入 file parts(多模态附件)。
|
|
80
81
|
const enrichedMessages = await injectFilePartsFromAttachments(messages);
|
|
81
82
|
|
|
82
|
-
//
|
|
83
|
-
const hydratedMessages = await hydrateFileUrlPartsForModel(
|
|
83
|
+
// 第二步(中文):把历史里的资源 URL 在内存中 hydrate 成模型可消费的 data URL。
|
|
84
|
+
const hydratedMessages = await hydrateFileUrlPartsForModel(
|
|
85
|
+
enrichedMessages,
|
|
86
|
+
projectRoot,
|
|
87
|
+
);
|
|
84
88
|
|
|
85
89
|
// 第三步(中文):转换前先剔除 UI 层 id 字段,仅保留模型需要的数据结构。
|
|
86
90
|
const input = hydratedMessages.map((message) => {
|