@coolkiller007/my-page-agent 0.2.8 → 0.2.9

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/dist/index.d.ts CHANGED
@@ -264,20 +264,14 @@ declare class AgentRuntime extends EventTarget {
264
264
  attachments?: Attachment[];
265
265
  }>;
266
266
  /**
267
- * `ask_user` 工具收到带附件的回答后暂存于此,供下一步 `#assembleUserPrompt` 注入 LLM prompt 后清空。
268
- * 非私有字段:需要被 `tools/index.ts` 中的 `ask_user` 工具从外部写入。
269
- */
270
- pendingAnswerAttachments: Attachment[];
271
- /**
272
- * 图片附件缓存(按附件名索引):所有出现过的图片附件在此登记原始 dataUrl,
273
- * 使原图在 prompt 里只出现一次之后,仍可被 `image_understand` 工具按名称"按需重传"重新查看,
267
+ * 图片附件缓存(按稳定附件 ID 索引):所有出现过的图片附件在此登记原始 dataUrl,
268
+ * 使原图在 prompt 里只出现一次之后,仍可被 `image_understand` 工具按稳定 ID“按需重传”重新查看,
274
269
  * 而不必每步都把原图重新塞进主对话历史(节省 token)。
275
270
  * @note 跨任务持久保留(不随 `execute()` 清空):缓存本身是纯内存 Map,不进 prompt 就不占 token,
276
271
  * 只有模型主动调用 `image_understand` 时才会产生一次性 token 成本。唯一真实开销是内存,
277
- * 因此按 `MAX_CACHED_IMAGES` 做容量上限,超出淘汰最早插入的一张(见 `#cacheImageAttachment`)。
278
- * 非私有字段:需要被 `tools/index.ts` 中的 `image_understand` 工具从外部读取。
272
+ * 因此按 `MAX_CACHED_IMAGES` 做容量上限,超出时淘汰最久未读取的一张。
279
273
  */
280
- imageAttachmentCache: Map<string, Attachment>;
274
+ readonly imageAttachmentCache: Map<string, StoredImageAttachment>;
281
275
  /** 构造函数:初始化配置、LLM、工具集,并注册重试事件监听与注入自定义工具 */
282
276
  constructor(config: AgentRuntimeConfig);
283
277
  /** 获取当前 Agent 状态 */
@@ -295,15 +289,17 @@ declare class AgentRuntime extends EventTarget {
295
289
  * Agent 内部错误会被捕获并加入历史,同时返回失败结果
296
290
  */
297
291
  execute(task: string, attachments?: Attachment[]): Promise<ExecutionResult>;
292
+ /* Excluded from this release type: setPendingAnswerAttachments */
293
+ /* Excluded from this release type: listAvailableAttachments */
298
294
  /**
299
295
  * 针对已缓存的图片附件发起一次独立的视觉问答,不进入主对话历史、不携带主链路的历史上下文。
300
296
  * 供 `image_understand` 工具使用:模型需要复看图片细节时按需调用,而不是每步都重发原图。
301
- * @param attachmentName - 附件名(即注入 prompt 时标注的名称)
297
+ * @param attachmentIdOrName - 优先使用稳定附件 ID;为兼容旧调用也接受文件名
302
298
  * @param query - 需要从图片中确认的具体问题
303
299
  * @param signal - 取消信号
304
300
  * @returns 视觉模型针对 query 给出的文字答案;附件不存在时返回错误提示
305
301
  */
306
- describeImage(attachmentName: string, query: string, signal: AbortSignal): Promise<string>;
302
+ describeImage(attachmentIdOrName: string, query: string, signal: AbortSignal): Promise<string>;
307
303
  /** 销毁 Agent:中止任务、释放浏览器控制器,并触发 dispose 事件(一次性) */
308
304
  dispose(): void;
309
305
  }
@@ -328,6 +324,8 @@ declare interface AgentStepEvent {
328
324
  name: string;
329
325
  input: any;
330
326
  output: string;
327
+ attachmentNames?: string[];
328
+ attachments?: AttachmentReference[];
331
329
  };
332
330
  usage: {
333
331
  promptTokens: number;
@@ -370,6 +368,13 @@ declare interface Attachment_2 {
370
368
  dataUrl: string;
371
369
  }
372
370
 
371
+ /** 历史与 prompt 中使用的轻量图片引用,不包含原图数据 */
372
+ declare interface AttachmentReference {
373
+ id: string;
374
+ name: string;
375
+ mimeType: string;
376
+ }
377
+
373
378
  /**
374
379
  * BrowserController 管理 DOM 状态和元素交互。
375
380
  * 它为所有 DOM 操作提供异步方法,并保持状态隔离。
@@ -798,6 +803,16 @@ declare interface RetryEvent {
798
803
  maxAttempts: number;
799
804
  }
800
805
 
806
+ /** 会话级图片仓库记录;原图仅保存在应用内存中,不进入普通任务 prompt */
807
+ declare interface StoredImageAttachment extends AttachmentReference {
808
+ dataUrl: string;
809
+ taskId: string;
810
+ task: string;
811
+ source: 'task' | 'answer';
812
+ createdAt: number;
813
+ lastAccessedAt: number;
814
+ }
815
+
801
816
  /** 支持的 UI 语言 */
802
817
  declare type SupportedLanguage = 'en-US' | 'zh-CN';
803
818
 
@@ -813,6 +828,7 @@ declare interface TaskStartEvent {
813
828
  type: 'task_start';
814
829
  task: string;
815
830
  attachmentNames?: string[];
831
+ attachments?: AttachmentReference[];
816
832
  }
817
833
 
818
834
  declare interface TextDomNode {
@@ -926,6 +942,12 @@ declare interface UIAdapter extends EventTarget {
926
942
  name: string;
927
943
  input: unknown;
928
944
  output: string;
945
+ attachmentNames?: string[];
946
+ attachments?: {
947
+ id: string;
948
+ name: string;
949
+ mimeType: string;
950
+ }[];
929
951
  };
930
952
  /** 仅用于 'observation' 类型 */
931
953
  content?: string;
@@ -933,6 +955,12 @@ declare interface UIAdapter extends EventTarget {
933
955
  task?: string;
934
956
  /** 仅用于 'task_start' 类型 */
935
957
  attachmentNames?: string[];
958
+ /** 仅用于 'task_start' 类型 */
959
+ attachments?: {
960
+ id: string;
961
+ name: string;
962
+ mimeType: string;
963
+ }[];
936
964
  /** 仅用于 'retry' 类型 */
937
965
  attempt?: number;
938
966
  maxAttempts?: number;
package/dist/index.js CHANGED
@@ -3494,6 +3494,7 @@ var UI = class {
3494
3494
  } else if (action.name === "ask_user") {
3495
3495
  const input = action.input;
3496
3496
  const answer = action.output.replace(/^User answered:\s*/i, "");
3497
+ const answerContent = action.attachmentNames?.length ? [`Answer: ${answer}`, this.#i18n.t("ui.attachmentsLabel", { names: action.attachmentNames.join(", ") })] : `Answer: ${answer}`;
3497
3498
  cards.push(createCard({
3498
3499
  icon: "❓",
3499
3500
  content: `Question: ${input.question || ""}`,
@@ -3502,7 +3503,7 @@ var UI = class {
3502
3503
  }));
3503
3504
  cards.push(createCard({
3504
3505
  icon: "💬",
3505
- content: `Answer: ${answer}`,
3506
+ content: answerContent,
3506
3507
  meta,
3507
3508
  type: "input"
3508
3509
  }));
@@ -4315,18 +4316,28 @@ tools.set("ask_user", tool({
4315
4316
  const raw = await this.onAskUser(input.question, { signal });
4316
4317
  const answer = typeof raw === "string" ? raw : raw.text;
4317
4318
  const attachments = typeof raw === "string" ? void 0 : raw.attachments;
4318
- if (attachments?.length) this.pendingAnswerAttachments = attachments;
4319
+ if (attachments?.length) this.setPendingAnswerAttachments(attachments);
4319
4320
  return `User answered: ${answer}`;
4320
4321
  }
4321
4322
  }));
4323
+ tools.set("list_attachments", tool({
4324
+ description: "List image attachments still available in this agent session without loading their image data. Use this when the relevant attachment ID is not present in the current context.",
4325
+ inputSchema: z.object({ query: z.string().optional() }),
4326
+ execute: async function(input) {
4327
+ return this.listAvailableAttachments(input.query);
4328
+ }
4329
+ }));
4322
4330
  tools.set("image_understand", tool({
4323
- description: "Re-examine a previously shown image attachment to answer a specific visual question about it. Images are only sent once — use this instead of assuming you can still \"see\" an image from an earlier step. Only call this when the info is NOT already recorded in your memory/history; do not call it repeatedly for the same question.",
4331
+ description: "Re-examine a previously shown image attachment to answer a specific visual question about it. Prefer attachment_id from available_attachments/list_attachments; attachment_name remains supported for compatibility. Images are loaded only by this tool — use it instead of assuming you can still \"see\" an image from an earlier step or task. Only call this when the info is NOT already recorded in your memory/history; do not call it repeatedly for the same question.",
4324
4332
  inputSchema: z.object({
4325
- attachment_name: z.string(),
4326
- query: z.string()
4333
+ attachment_id: z.string().optional(),
4334
+ attachment_name: z.string().optional(),
4335
+ query: z.string().min(1)
4327
4336
  }),
4328
4337
  execute: async function(input, { signal }) {
4329
- return await this.describeImage(input.attachment_name, input.query, signal);
4338
+ const identifier = input.attachment_id ?? input.attachment_name;
4339
+ if (!identifier) return "❌ attachment_id or attachment_name is required.";
4340
+ return await this.describeImage(identifier, input.query, signal);
4330
4341
  }
4331
4342
  }));
4332
4343
  tools.set("click_element_by_index", tool({
@@ -4407,8 +4418,10 @@ var ATTACHMENT_IMAGE_MAX_DIMENSION = 1568;
4407
4418
  var ATTACHMENT_IMAGE_JPEG_QUALITY = .85;
4408
4419
  /** 单次注入里最多内联发送的图片张数,超出的仅登记缓存,需模型主动调用 image_understand 查看 */
4409
4420
  var MAX_INLINE_IMAGES = 4;
4410
- /** 图片缓存跨任务最多保留的张数,超出时淘汰最早插入的一张(防止长会话内存无限增长;缓存本身不占 prompt token) */
4421
+ /** 图片仓库跨任务最多保留的张数,超出时按 LRU 淘汰(仓库本身不占 prompt token) */
4411
4422
  var MAX_CACHED_IMAGES = 20;
4423
+ /** 相同图片、相同问题的视觉结果缓存上限,避免重复视觉调用同时控制内存 */
4424
+ var MAX_IMAGE_UNDERSTANDING_CACHE_ENTRIES = 100;
4412
4425
  /** 历史任务 done 文本注入 prompt 时的最大字符数 */
4413
4426
  var PREVIOUS_TASK_TEXT_MAX_LENGTH = 300;
4414
4427
  /**
@@ -4480,21 +4493,27 @@ var AgentRuntime = class extends EventTarget {
4480
4493
  #observations = [];
4481
4494
  /** 本次任务上传的附件;仅在任务第一步注入 LLM prompt,之后步骤不重复携带原文件以节省 token */
4482
4495
  #taskAttachments = [];
4496
+ /** 本次任务图片在会话级仓库中的轻量引用 */
4497
+ #taskAttachmentReferences = [];
4483
4498
  /**
4484
4499
  * `ask_user` 工具收到带附件的回答后暂存于此,供下一步 `#assembleUserPrompt` 注入 LLM prompt 后清空。
4485
- * 非私有字段:需要被 `tools/index.ts` 中的 `ask_user` 工具从外部写入。
4486
4500
  */
4487
- pendingAnswerAttachments = [];
4501
+ #pendingAnswerAttachments = [];
4502
+ /** 追问回答中的图片在会话级仓库中的轻量引用 */
4503
+ #pendingAnswerAttachmentReferences = [];
4488
4504
  /**
4489
- * 图片附件缓存(按附件名索引):所有出现过的图片附件在此登记原始 dataUrl,
4490
- * 使原图在 prompt 里只出现一次之后,仍可被 `image_understand` 工具按名称"按需重传"重新查看,
4505
+ * 图片附件缓存(按稳定附件 ID 索引):所有出现过的图片附件在此登记原始 dataUrl,
4506
+ * 使原图在 prompt 里只出现一次之后,仍可被 `image_understand` 工具按稳定 ID“按需重传”重新查看,
4491
4507
  * 而不必每步都把原图重新塞进主对话历史(节省 token)。
4492
4508
  * @note 跨任务持久保留(不随 `execute()` 清空):缓存本身是纯内存 Map,不进 prompt 就不占 token,
4493
4509
  * 只有模型主动调用 `image_understand` 时才会产生一次性 token 成本。唯一真实开销是内存,
4494
- * 因此按 `MAX_CACHED_IMAGES` 做容量上限,超出淘汰最早插入的一张(见 `#cacheImageAttachment`)。
4495
- * 非私有字段:需要被 `tools/index.ts` 中的 `image_understand` 工具从外部读取。
4510
+ * 因此按 `MAX_CACHED_IMAGES` 做容量上限,超出时淘汰最久未读取的一张。
4496
4511
  */
4497
4512
  imageAttachmentCache = /* @__PURE__ */ new Map();
4513
+ /** 同一个附件对象只注册一次,避免首次注入 prompt 时重复生成 ID */
4514
+ #attachmentObjectIds = /* @__PURE__ */ new WeakMap();
4515
+ /** 独立视觉问答结果缓存,key 由附件 ID 和规范化后的问题组成 */
4516
+ #imageUnderstandingCache = /* @__PURE__ */ new Map();
4498
4517
  /** 当前任务在 history 数组中的起始下标(用于跨任务保留历史时,把 prompt/循环检测限定在当前任务范围内) */
4499
4518
  #taskHistoryStart = 0;
4500
4519
  /** 当前一次运行完全结束时 resolve。由 `stop()` 等待。 */
@@ -4613,12 +4632,15 @@ var AgentRuntime = class extends EventTarget {
4613
4632
  this.task = task;
4614
4633
  this.taskId = uid();
4615
4634
  this.#taskAttachments = attachments;
4616
- this.pendingAnswerAttachments = [];
4635
+ this.#taskAttachmentReferences = this.#registerImageAttachments(attachments, "task");
4636
+ this.#pendingAnswerAttachments = [];
4637
+ this.#pendingAnswerAttachmentReferences = [];
4617
4638
  this.#taskHistoryStart = this.history.length;
4618
4639
  this.#emitHistoryChange({
4619
4640
  type: "task_start",
4620
4641
  task,
4621
- ...attachments.length > 0 ? { attachmentNames: attachments.map((a) => a.name) } : {}
4642
+ ...attachments.length > 0 ? { attachmentNames: attachments.map((a) => a.name) } : {},
4643
+ ...this.#taskAttachmentReferences.length > 0 ? { attachments: this.#taskAttachmentReferences } : {}
4622
4644
  });
4623
4645
  this.#observations = [];
4624
4646
  this.#states = {
@@ -4700,7 +4722,9 @@ var AgentRuntime = class extends EventTarget {
4700
4722
  const action = {
4701
4723
  name: actionName,
4702
4724
  input: input.action[actionName],
4703
- output
4725
+ output,
4726
+ ...macroResult.attachmentNames?.length ? { attachmentNames: macroResult.attachmentNames } : {},
4727
+ ...macroResult.attachments?.length ? { attachments: macroResult.attachments } : {}
4704
4728
  };
4705
4729
  this.#emitHistoryChange({
4706
4730
  type: "step",
@@ -4826,9 +4850,13 @@ var AgentRuntime = class extends EventTarget {
4826
4850
  });
4827
4851
  if (toolName === "wait") this.#states.totalWaitTime += toolInput?.seconds || 0;
4828
4852
  else this.#states.totalWaitTime = 0;
4853
+ const attachmentNames = toolName === "ask_user" && this.#pendingAnswerAttachments.length > 0 ? this.#pendingAnswerAttachments.map((attachment) => attachment.name) : void 0;
4854
+ const attachments = toolName === "ask_user" && this.#pendingAnswerAttachmentReferences.length > 0 ? this.#pendingAnswerAttachmentReferences : void 0;
4829
4855
  return {
4830
4856
  input,
4831
- output: result
4857
+ output: result,
4858
+ ...attachmentNames ? { attachmentNames } : {},
4859
+ ...attachments ? { attachments } : {}
4832
4860
  };
4833
4861
  }
4834
4862
  };
@@ -4881,9 +4909,12 @@ var AgentRuntime = class extends EventTarget {
4881
4909
  attachmentNames: event.attachmentNames
4882
4910
  };
4883
4911
  summaries.push(current);
4884
- } else if (event.type === "step" && event.action.name === "done" && current) {
4885
- const input = event.action.input;
4886
- current.result = `${input?.success === false ? "" : "✅"} ${input?.text ?? ""}`;
4912
+ } else if (event.type === "step" && current) {
4913
+ if (event.action.name === "ask_user" && event.action.attachmentNames?.length) current.attachmentNames = [.../* @__PURE__ */ new Set([...current.attachmentNames ?? [], ...event.action.attachmentNames])];
4914
+ if (event.action.name === "done") {
4915
+ const input = event.action.input;
4916
+ current.result = `${input?.success === false ? "❌" : "✅"} ${input?.text ?? ""}`;
4917
+ }
4887
4918
  }
4888
4919
  const recent = summaries.slice(-5);
4889
4920
  if (recent.length === 0) return "";
@@ -4900,12 +4931,28 @@ var AgentRuntime = class extends EventTarget {
4900
4931
  #describeAttachmentAvailability(attachmentNames) {
4901
4932
  const viewable = [];
4902
4933
  const unavailable = [];
4903
- for (const name of attachmentNames) (this.imageAttachmentCache.has(name) ? viewable : unavailable).push(name);
4934
+ for (const name of attachmentNames) (this.#resolveImageAttachment(name, false) ? viewable : unavailable).push(name);
4904
4935
  const parts = [];
4905
4936
  if (viewable.length > 0) parts.push(`still viewable via image_understand: ${viewable.join(", ")}`);
4906
4937
  if (unavailable.length > 0) parts.push(`NOT available in this task, ask the user to re-upload if you need to see them again: ${unavailable.join(", ")}`);
4907
4938
  return `[attachments uploaded then — ${parts.join("; ")}]`;
4908
4939
  }
4940
+ /** 生成新任务首步使用的轻量图片清单;不包含 dataUrl。 */
4941
+ #buildAvailableAttachmentsManifest() {
4942
+ if (this.imageAttachmentCache.size === 0) return "";
4943
+ const attachments = [...this.imageAttachmentCache.values()].reverse().map((attachment) => ({
4944
+ id: attachment.id,
4945
+ name: attachment.name,
4946
+ mimeType: attachment.mimeType,
4947
+ sourceTask: truncate(attachment.task, 100),
4948
+ source: attachment.source
4949
+ }));
4950
+ return `<available_attachments>
4951
+ These session images are not included inline. Use image_understand with attachment_id only when visual details are needed.
4952
+ ${JSON.stringify(attachments)}\n</available_attachments>
4953
+
4954
+ `;
4955
+ }
4909
4956
  /**
4910
4957
  * 在每一步之前生成系统观察
4911
4958
  * @todo 控制台错误
@@ -4955,7 +5002,10 @@ var AgentRuntime = class extends EventTarget {
4955
5002
  prompt += await this.#getInstructions();
4956
5003
  const currentTaskHistory = this.#currentTaskHistory;
4957
5004
  const stepCount = currentTaskHistory.filter((e) => e.type === "step").length;
4958
- if (stepCount === 0) prompt += this.#buildPreviousTasksSummary();
5005
+ if (stepCount === 0) {
5006
+ prompt += this.#buildPreviousTasksSummary();
5007
+ prompt += this.#buildAvailableAttachmentsManifest();
5008
+ }
4959
5009
  prompt += "<agent_state>\n";
4960
5010
  prompt += "<user_request>\n";
4961
5011
  prompt += `${this.task}\n`;
@@ -4980,6 +5030,8 @@ var AgentRuntime = class extends EventTarget {
4980
5030
  prompt += `Next Goal: ${event.reflection.next_goal}\n`;
4981
5031
  prompt += `Action: ${event.action.name}(${JSON.stringify(event.action.input)})\n`;
4982
5032
  prompt += `Action Results: ${event.action.output}\n`;
5033
+ if (event.action.attachments?.length) prompt += `User-uploaded attachments: ${JSON.stringify(event.action.attachments)}\n`;
5034
+ else if (event.action.attachmentNames?.length) prompt += `User-uploaded attachment names: ${JSON.stringify(event.action.attachmentNames)}\n`;
4983
5035
  prompt += `</step_${stepIndex}>\n`;
4984
5036
  } else if (event.type === "observation") prompt += `<sys>${event.content}</sys>\n`;
4985
5037
  else if (event.type === "user_takeover") prompt += `<sys>User took over control and made changes to the page</sys>\n`;
@@ -4993,28 +5045,107 @@ var AgentRuntime = class extends EventTarget {
4993
5045
  prompt += browserState.footer + "\n\n";
4994
5046
  prompt += "</browser_state>\n\n";
4995
5047
  if (stepCount === 0 && this.#taskAttachments.length > 0) return await this.#injectAttachments(prompt, this.#taskAttachments);
4996
- if (this.pendingAnswerAttachments.length > 0) {
4997
- const attachments = this.pendingAnswerAttachments;
4998
- this.pendingAnswerAttachments = [];
5048
+ if (this.#pendingAnswerAttachments.length > 0) {
5049
+ const attachments = this.#pendingAnswerAttachments;
5050
+ this.#pendingAnswerAttachments = [];
5051
+ this.#pendingAnswerAttachmentReferences = [];
4999
5052
  return await this.#injectAttachments(prompt, attachments);
5000
5053
  }
5001
5054
  return prompt;
5002
5055
  }
5003
5056
  /**
5004
- * 把图片附件登记进 `imageAttachmentCache`;超出 `MAX_CACHED_IMAGES` 容量时淘汰最早插入的一张。
5005
- * 缓存跨任务持久,容量上限只为控内存,不影响 token(未被 `image_understand` 读取的缓存项不进 prompt)。
5057
+ * 保存 ask_user 回答附件,供下一个主循环步骤注入;图片会同时登记进跨任务仓库。
5058
+ * @internal 供内置 ask_user 工具调用。
5006
5059
  */
5007
- #cacheImageAttachment(attachment) {
5008
- if (!this.imageAttachmentCache.has(attachment.name) && this.imageAttachmentCache.size >= MAX_CACHED_IMAGES) {
5009
- const oldestName = this.imageAttachmentCache.keys().next().value;
5010
- if (oldestName !== void 0) this.imageAttachmentCache.delete(oldestName);
5060
+ setPendingAnswerAttachments(attachments) {
5061
+ this.#pendingAnswerAttachments = attachments;
5062
+ this.#pendingAnswerAttachmentReferences = this.#registerImageAttachments(attachments, "answer");
5063
+ }
5064
+ /** 把图片登记进按稳定 ID 索引的会话级 LRU 仓库,并返回不含原图的引用。 */
5065
+ #registerImageAttachments(attachments, source) {
5066
+ const references = [];
5067
+ for (const attachment of attachments) {
5068
+ if (!attachment.mimeType.startsWith("image/")) continue;
5069
+ const existingId = this.#attachmentObjectIds.get(attachment);
5070
+ const existing = existingId ? this.imageAttachmentCache.get(existingId) : void 0;
5071
+ if (existing) {
5072
+ this.#touchImageAttachment(existing.id);
5073
+ references.push(this.#toAttachmentReference(existing));
5074
+ continue;
5075
+ }
5076
+ while (this.imageAttachmentCache.size >= MAX_CACHED_IMAGES) {
5077
+ const oldestId = this.imageAttachmentCache.keys().next().value;
5078
+ if (oldestId === void 0) break;
5079
+ this.#evictImageAttachment(oldestId);
5080
+ }
5081
+ const id = `att_${uid()}`;
5082
+ const now = Date.now();
5083
+ const stored = {
5084
+ id,
5085
+ name: attachment.name,
5086
+ mimeType: attachment.mimeType,
5087
+ dataUrl: attachment.dataUrl,
5088
+ taskId: this.taskId,
5089
+ task: this.task,
5090
+ source,
5091
+ createdAt: now,
5092
+ lastAccessedAt: now
5093
+ };
5094
+ this.imageAttachmentCache.set(id, stored);
5095
+ this.#attachmentObjectIds.set(attachment, id);
5096
+ references.push(this.#toAttachmentReference(stored));
5011
5097
  }
5012
- this.imageAttachmentCache.set(attachment.name, attachment);
5098
+ return references;
5099
+ }
5100
+ #toAttachmentReference(attachment) {
5101
+ return {
5102
+ id: attachment.id,
5103
+ name: attachment.name,
5104
+ mimeType: attachment.mimeType
5105
+ };
5106
+ }
5107
+ /** 按 ID 精确查找;旧文件名调用兼容为最近上传的同名图片。 */
5108
+ #resolveImageAttachment(attachmentIdOrName, touch = true) {
5109
+ let attachment = this.imageAttachmentCache.get(attachmentIdOrName);
5110
+ if (!attachment) attachment = [...this.imageAttachmentCache.values()].reverse().find((candidate) => candidate.name === attachmentIdOrName);
5111
+ if (attachment && touch) this.#touchImageAttachment(attachment.id);
5112
+ return attachment;
5113
+ }
5114
+ /** 读取图片时移动到 Map 末尾,使容量淘汰遵循 LRU 而非单纯 FIFO。 */
5115
+ #touchImageAttachment(attachmentId) {
5116
+ const attachment = this.imageAttachmentCache.get(attachmentId);
5117
+ if (!attachment) return;
5118
+ attachment.lastAccessedAt = Date.now();
5119
+ this.imageAttachmentCache.delete(attachmentId);
5120
+ this.imageAttachmentCache.set(attachmentId, attachment);
5121
+ }
5122
+ #evictImageAttachment(attachmentId) {
5123
+ this.imageAttachmentCache.delete(attachmentId);
5124
+ for (const key of this.#imageUnderstandingCache.keys()) if (key.startsWith(`${attachmentId}\n`)) this.#imageUnderstandingCache.delete(key);
5125
+ }
5126
+ /**
5127
+ * 返回不含原图数据的附件列表,供 list_attachments 工具在任务中途重新发现历史图片。
5128
+ * @internal
5129
+ */
5130
+ listAvailableAttachments(query) {
5131
+ const needle = query?.trim().toLowerCase();
5132
+ const attachments = [...this.imageAttachmentCache.values()].reverse().filter((attachment) => {
5133
+ if (!needle) return true;
5134
+ return attachment.id.toLowerCase().includes(needle) || attachment.name.toLowerCase().includes(needle) || attachment.task.toLowerCase().includes(needle);
5135
+ }).map((attachment) => ({
5136
+ id: attachment.id,
5137
+ name: attachment.name,
5138
+ mimeType: attachment.mimeType,
5139
+ sourceTask: truncate(attachment.task, 100),
5140
+ source: attachment.source
5141
+ }));
5142
+ if (attachments.length === 0) return "No matching image attachments are available.";
5143
+ return `Available image attachments (metadata only): ${JSON.stringify(attachments)}`;
5013
5144
  }
5014
5145
  /**
5015
5146
  * 把附件内容注入 prompt:图片转为 image_url 片段;文本类文件解码后截断拼进文字部分。
5016
5147
  * 仅当存在图片附件时才返回多模态数组,纯文本附件保持返回字符串(不影响不支持 vision 的模型)。
5017
- * 图片附件会先登记进 `imageAttachmentCache`(存原图,供 `image_understand` 复看细节用),
5148
+ * 图片附件已在接收时登记进 `imageAttachmentCache`(存原图,供 `image_understand` 复看细节用),
5018
5149
  * 内联发给模型的图片先按 `ATTACHMENT_IMAGE_MAX_DIMENSION` 压缩降 token;
5019
5150
  * 单次超过 `MAX_INLINE_IMAGES` 张时,多出的图片只登记缓存 + 文字提示名称,不内联发送。
5020
5151
  * 之后步骤不再重复下发原图,需要复看细节时改由模型调用 `image_understand` 按需重传。
@@ -5025,14 +5156,14 @@ var AgentRuntime = class extends EventTarget {
5025
5156
  let inlinedImageCount = 0;
5026
5157
  for (const attachment of attachments) {
5027
5158
  if (attachment.mimeType.startsWith("image/")) {
5028
- this.#cacheImageAttachment(attachment);
5159
+ const attachmentId = this.#registerImageAttachments([attachment], "task")[0].id;
5029
5160
  if (inlinedImageCount >= MAX_INLINE_IMAGES) {
5030
- textSegments.push(`[Image attachment "${attachment.name}" was NOT shown inline (too many images in this batch). Call image_understand with attachment_name="${attachment.name}" if you need to see it.]`);
5161
+ textSegments.push(`[Image attachment "${attachment.name}" was NOT shown inline (too many images in this batch). Call image_understand with attachment_id="${attachmentId}" if you need to see it.]`);
5031
5162
  continue;
5032
5163
  }
5033
5164
  inlinedImageCount++;
5034
5165
  const resizedDataUrl = await resizeImageDataUrl(attachment.dataUrl, ATTACHMENT_IMAGE_MAX_DIMENSION, ATTACHMENT_IMAGE_JPEG_QUALITY);
5035
- textSegments.push(`[Image attachment "${attachment.name}" is shown below. It will NOT be resent in later steps — note only the key facts you need into memory, don't transcribe it verbatim. If you need to re-examine its visual details (e.g. exact small text) in a later step, call image_understand with attachment_name="${attachment.name}".]`);
5166
+ textSegments.push(`[Image attachment "${attachment.name}" (attachment_id="${attachmentId}") is shown below. It will NOT be resent in later steps — note only the key facts you need into memory, don't transcribe it verbatim. If you need to re-examine its visual details (e.g. exact small text) in a later step, call image_understand with attachment_id="${attachmentId}".]`);
5036
5167
  imageParts.push({
5037
5168
  type: "image_url",
5038
5169
  image_url: { url: resizedDataUrl }
@@ -5051,20 +5182,25 @@ var AgentRuntime = class extends EventTarget {
5051
5182
  /**
5052
5183
  * 针对已缓存的图片附件发起一次独立的视觉问答,不进入主对话历史、不携带主链路的历史上下文。
5053
5184
  * 供 `image_understand` 工具使用:模型需要复看图片细节时按需调用,而不是每步都重发原图。
5054
- * @param attachmentName - 附件名(即注入 prompt 时标注的名称)
5185
+ * @param attachmentIdOrName - 优先使用稳定附件 ID;为兼容旧调用也接受文件名
5055
5186
  * @param query - 需要从图片中确认的具体问题
5056
5187
  * @param signal - 取消信号
5057
5188
  * @returns 视觉模型针对 query 给出的文字答案;附件不存在时返回错误提示
5058
5189
  */
5059
- async describeImage(attachmentName, query, signal) {
5060
- const attachment = this.imageAttachmentCache.get(attachmentName);
5061
- if (!attachment) return `❌ Attachment "${attachmentName}" not found. Available image attachments: ${[...this.imageAttachmentCache.keys()].join(", ") || "none"}`;
5190
+ async describeImage(attachmentIdOrName, query, signal) {
5191
+ signal.throwIfAborted();
5192
+ const attachment = this.#resolveImageAttachment(attachmentIdOrName);
5193
+ if (!attachment) return `❌ Attachment "${attachmentIdOrName}" not found. ${this.listAvailableAttachments()}`;
5194
+ const normalizedQuery = query.trim().replace(/\s+/g, " ").toLowerCase();
5195
+ const cacheKey = `${attachment.id}\n${normalizedQuery}`;
5196
+ const cached = this.#imageUnderstandingCache.get(cacheKey);
5197
+ if (cached !== void 0) return cached;
5062
5198
  const answerTool = {
5063
5199
  description: "Answer the question about the image as precisely and completely as possible.",
5064
5200
  inputSchema: z.object({ answer: z.string() }),
5065
5201
  execute: async (args) => args
5066
5202
  };
5067
- return (await this.#llm.invoke([{
5203
+ const answer = (await this.#llm.invoke([{
5068
5204
  role: "user",
5069
5205
  content: [{
5070
5206
  type: "text",
@@ -5074,6 +5210,12 @@ var AgentRuntime = class extends EventTarget {
5074
5210
  image_url: { url: attachment.dataUrl }
5075
5211
  }]
5076
5212
  }], { answer_image_question: answerTool }, signal, { toolChoiceName: "answer_image_question" })).toolResult.answer;
5213
+ if (this.#imageUnderstandingCache.size >= MAX_IMAGE_UNDERSTANDING_CACHE_ENTRIES) {
5214
+ const oldestKey = this.#imageUnderstandingCache.keys().next().value;
5215
+ if (oldestKey !== void 0) this.#imageUnderstandingCache.delete(oldestKey);
5216
+ }
5217
+ this.#imageUnderstandingCache.set(cacheKey, answer);
5218
+ return answer;
5077
5219
  }
5078
5220
  /** 销毁 Agent:中止任务、释放浏览器控制器,并触发 dispose 事件(一次性) */
5079
5221
  dispose() {
@@ -5090,6 +5232,15 @@ var AgentRuntime = class extends EventTarget {
5090
5232
  };
5091
5233
  cleanUp(() => this.#abortController.abort());
5092
5234
  cleanUp(() => this.browserController.dispose());
5235
+ cleanUp(() => this.imageAttachmentCache.clear());
5236
+ cleanUp(() => this.#imageUnderstandingCache.clear());
5237
+ cleanUp(() => {
5238
+ this.#attachmentObjectIds = /* @__PURE__ */ new WeakMap();
5239
+ this.#taskAttachments = [];
5240
+ this.#taskAttachmentReferences = [];
5241
+ this.#pendingAnswerAttachments = [];
5242
+ this.#pendingAnswerAttachmentReferences = [];
5243
+ });
5093
5244
  cleanUp(() => this.dispatchEvent(new Event("dispose")));
5094
5245
  cleanUp(() => this.config.onDispose?.(this));
5095
5246
  if (errors.length === 1) throw errors[0];