@coolkiller007/my-page-agent 0.2.2 → 0.2.3

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
@@ -268,6 +268,13 @@ declare class AgentRuntime extends EventTarget {
268
268
  * 非私有字段:需要被 `tools/index.ts` 中的 `ask_user` 工具从外部写入。
269
269
  */
270
270
  pendingAnswerAttachments: Attachment[];
271
+ /**
272
+ * 图片附件缓存(按附件名索引):所有出现过的图片附件在此登记原始 dataUrl,
273
+ * 使原图在 prompt 里只出现一次之后,仍可被 `image_understand` 工具按名称"按需重传"重新查看,
274
+ * 而不必每步都把原图重新塞进主对话历史(节省 token)。
275
+ * 非私有字段:需要被 `tools/index.ts` 中的 `image_understand` 工具从外部读取。
276
+ */
277
+ imageAttachmentCache: Map<string, Attachment>;
271
278
  /** 构造函数:初始化配置、LLM、工具集,并注册重试事件监听与注入自定义工具 */
272
279
  constructor(config: AgentRuntimeConfig);
273
280
  /** 获取当前 Agent 状态 */
@@ -285,6 +292,15 @@ declare class AgentRuntime extends EventTarget {
285
292
  * Agent 内部错误会被捕获并加入历史,同时返回失败结果
286
293
  */
287
294
  execute(task: string, attachments?: Attachment[]): Promise<ExecutionResult>;
295
+ /**
296
+ * 针对已缓存的图片附件发起一次独立的视觉问答,不进入主对话历史、不携带主链路的历史上下文。
297
+ * 供 `image_understand` 工具使用:模型需要复看图片细节时按需调用,而不是每步都重发原图。
298
+ * @param attachmentName - 附件名(即注入 prompt 时标注的名称)
299
+ * @param query - 需要从图片中确认的具体问题
300
+ * @param signal - 取消信号
301
+ * @returns 视觉模型针对 query 给出的文字答案;附件不存在时返回错误提示
302
+ */
303
+ describeImage(attachmentName: string, query: string, signal: AbortSignal): Promise<string>;
288
304
  /** 销毁 Agent:中止任务、释放浏览器控制器,并触发 dispose 事件(一次性) */
289
305
  dispose(): void;
290
306
  }
package/dist/index.js CHANGED
@@ -4048,6 +4048,47 @@ function retrieveJsonFromString(str) {
4048
4048
  }
4049
4049
  }
4050
4050
  //#endregion
4051
+ //#region src/agent/utils/image.ts
4052
+ /**
4053
+ * 图片附件处理工具(纯浏览器 Canvas 实现,无额外依赖)
4054
+ */
4055
+ /**
4056
+ * 按需缩小图片:仅当长边超过 maxDimension 时才缩放并重新编码为 JPEG,
4057
+ * 用于降低多模态请求里图片消耗的 token;非浏览器环境或解码/绘制失败时原样返回。
4058
+ * @param dataUrl - 原始图片的 base64 data URL
4059
+ * @param maxDimension - 长边允许的最大像素
4060
+ * @param quality - 重新编码为 JPEG 时的质量(0-1)
4061
+ * @returns 缩放后的 data URL,未超阈值或失败时返回原 dataUrl
4062
+ */
4063
+ async function resizeImageDataUrl(dataUrl, maxDimension, quality) {
4064
+ if (typeof Image === "undefined" || typeof document === "undefined") return dataUrl;
4065
+ try {
4066
+ const img = await loadImage(dataUrl);
4067
+ const { naturalWidth: width, naturalHeight: height } = img;
4068
+ if (!width || !height || width <= maxDimension && height <= maxDimension) return dataUrl;
4069
+ const scale = maxDimension / Math.max(width, height);
4070
+ const targetWidth = Math.round(width * scale);
4071
+ const targetHeight = Math.round(height * scale);
4072
+ const canvas = document.createElement("canvas");
4073
+ canvas.width = targetWidth;
4074
+ canvas.height = targetHeight;
4075
+ const ctx = canvas.getContext("2d");
4076
+ if (!ctx) return dataUrl;
4077
+ ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
4078
+ return canvas.toDataURL("image/jpeg", quality);
4079
+ } catch {
4080
+ return dataUrl;
4081
+ }
4082
+ }
4083
+ function loadImage(dataUrl) {
4084
+ return new Promise((resolve, reject) => {
4085
+ const img = new Image();
4086
+ img.onload = () => resolve(img);
4087
+ img.onerror = () => reject(/* @__PURE__ */ new Error("Failed to load image"));
4088
+ img.src = dataUrl;
4089
+ });
4090
+ }
4091
+ //#endregion
4051
4092
  //#region src/agent/utils/index.ts
4052
4093
  /**
4053
4094
  * 等待 `seconds` 秒。若提供了 `signal`,则等待可被取消:
@@ -4202,6 +4243,16 @@ tools.set("ask_user", tool({
4202
4243
  return `User answered: ${answer}`;
4203
4244
  }
4204
4245
  }));
4246
+ tools.set("image_understand", tool({
4247
+ 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.",
4248
+ inputSchema: z.object({
4249
+ attachment_name: z.string(),
4250
+ query: z.string()
4251
+ }),
4252
+ execute: async function(input, { signal }) {
4253
+ return await this.describeImage(input.attachment_name, input.query, signal);
4254
+ }
4255
+ }));
4205
4256
  tools.set("click_element_by_index", tool({
4206
4257
  description: "Click element by index",
4207
4258
  inputSchema: z.object({ index: z.int().min(0) }),
@@ -4274,6 +4325,14 @@ tools.set("execute_javascript", tool({
4274
4325
  //#region src/agent/AgentRuntime.ts
4275
4326
  /** 文本类附件注入 prompt 时的最大字符数,避免超大文件把上下文撑爆 */
4276
4327
  var ATTACHMENT_TEXT_MAX_LENGTH = 2e4;
4328
+ /** 图片长边超过该像素才压缩(OpenAI vision 高清分块阈值附近,兼顾 token 成本与截图文字可读性) */
4329
+ var ATTACHMENT_IMAGE_MAX_DIMENSION = 1568;
4330
+ /** 图片压缩重编码为 JPEG 时的质量 */
4331
+ var ATTACHMENT_IMAGE_JPEG_QUALITY = .85;
4332
+ /** 单次注入里最多内联发送的图片张数,超出的仅登记缓存,需模型主动调用 image_understand 查看 */
4333
+ var MAX_INLINE_IMAGES = 4;
4334
+ /** 历史任务 done 文本注入 prompt 时的最大字符数 */
4335
+ var PREVIOUS_TASK_TEXT_MAX_LENGTH = 300;
4277
4336
  /**
4278
4337
  * 用于浏览器自动化的 AI 智能体。
4279
4338
  *
@@ -4348,6 +4407,13 @@ var AgentRuntime = class extends EventTarget {
4348
4407
  * 非私有字段:需要被 `tools/index.ts` 中的 `ask_user` 工具从外部写入。
4349
4408
  */
4350
4409
  pendingAnswerAttachments = [];
4410
+ /**
4411
+ * 图片附件缓存(按附件名索引):所有出现过的图片附件在此登记原始 dataUrl,
4412
+ * 使原图在 prompt 里只出现一次之后,仍可被 `image_understand` 工具按名称"按需重传"重新查看,
4413
+ * 而不必每步都把原图重新塞进主对话历史(节省 token)。
4414
+ * 非私有字段:需要被 `tools/index.ts` 中的 `image_understand` 工具从外部读取。
4415
+ */
4416
+ imageAttachmentCache = /* @__PURE__ */ new Map();
4351
4417
  /** 当前任务在 history 数组中的起始下标(用于跨任务保留历史时,把 prompt/循环检测限定在当前任务范围内) */
4352
4418
  #taskHistoryStart = 0;
4353
4419
  /** 当前一次运行完全结束时 resolve。由 `stop()` 等待。 */
@@ -4467,6 +4533,7 @@ var AgentRuntime = class extends EventTarget {
4467
4533
  this.taskId = uid();
4468
4534
  this.#taskAttachments = attachments;
4469
4535
  this.pendingAnswerAttachments = [];
4536
+ this.imageAttachmentCache = /* @__PURE__ */ new Map();
4470
4537
  this.#taskHistoryStart = this.history.length;
4471
4538
  this.#emitHistoryChange({
4472
4539
  type: "task_start",
@@ -4717,6 +4784,35 @@ var AgentRuntime = class extends EventTarget {
4717
4784
  return result;
4718
4785
  }
4719
4786
  /**
4787
+ * 汇总当前任务之前、同一 AgentRuntime 实例已完成过的历史任务,生成一段纯文字摘要。
4788
+ * 只带文字结论(任务描述 + done 文本 + 涉及的附件文件名),不携带图片原始数据 ——
4789
+ * 图片附件与 `imageAttachmentCache` 一样按任务隔离,跨任务需要复看时应提示用户重新上传。
4790
+ * 只取最近 `PREVIOUS_TASKS_SUMMARY_LIMIT` 个任务,避免任务数增多后 prompt 无限增长。
4791
+ */
4792
+ #buildPreviousTasksSummary() {
4793
+ const priorHistory = this.history.slice(0, this.#taskHistoryStart);
4794
+ if (priorHistory.length === 0) return "";
4795
+ const summaries = [];
4796
+ let current = null;
4797
+ for (const event of priorHistory) if (event.type === "task_start") {
4798
+ current = {
4799
+ task: event.task,
4800
+ attachmentNames: event.attachmentNames
4801
+ };
4802
+ summaries.push(current);
4803
+ } else if (event.type === "step" && event.action.name === "done" && current) {
4804
+ const input = event.action.input;
4805
+ current.result = `${input?.success === false ? "❌" : "✅"} ${input?.text ?? ""}`;
4806
+ }
4807
+ const recent = summaries.slice(-5);
4808
+ if (recent.length === 0) return "";
4809
+ return `<previous_tasks_summary>\n${recent.map((s) => {
4810
+ const attachmentNote = s.attachmentNames?.length ? ` [attachments uploaded then: ${s.attachmentNames.join(", ")} — NOT available in this task, ask the user to re-upload if you need to see them again]` : "";
4811
+ const result = s.result ? truncate(s.result, PREVIOUS_TASK_TEXT_MAX_LENGTH) : "(no result recorded — task may have been interrupted)";
4812
+ return `- Task: "${s.task}"${attachmentNote}\n Result: ${result}`;
4813
+ }).join("\n")}\n</previous_tasks_summary>\n\n`;
4814
+ }
4815
+ /**
4720
4816
  * 在每一步之前生成系统观察
4721
4817
  * @todo 控制台错误
4722
4818
  */
@@ -4757,6 +4853,7 @@ var AgentRuntime = class extends EventTarget {
4757
4853
  prompt += await this.#getInstructions();
4758
4854
  const currentTaskHistory = this.#currentTaskHistory;
4759
4855
  const stepCount = currentTaskHistory.filter((e) => e.type === "step").length;
4856
+ if (stepCount === 0) prompt += this.#buildPreviousTasksSummary();
4760
4857
  prompt += "<agent_state>\n";
4761
4858
  prompt += "<user_request>\n";
4762
4859
  prompt += `${this.task}\n`;
@@ -4779,6 +4876,7 @@ var AgentRuntime = class extends EventTarget {
4779
4876
  prompt += `Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}\n`;
4780
4877
  prompt += `Memory: ${event.reflection.memory}\n`;
4781
4878
  prompt += `Next Goal: ${event.reflection.next_goal}\n`;
4879
+ prompt += `Action: ${event.action.name}(${JSON.stringify(event.action.input)})\n`;
4782
4880
  prompt += `Action Results: ${event.action.output}\n`;
4783
4881
  prompt += `</step_${stepIndex}>\n`;
4784
4882
  } else if (event.type === "observation") prompt += `<sys>${event.content}</sys>\n`;
@@ -4803,15 +4901,28 @@ var AgentRuntime = class extends EventTarget {
4803
4901
  /**
4804
4902
  * 把附件内容注入 prompt:图片转为 image_url 片段;文本类文件解码后截断拼进文字部分。
4805
4903
  * 仅当存在图片附件时才返回多模态数组,纯文本附件保持返回字符串(不影响不支持 vision 的模型)。
4904
+ * 图片附件会先登记进 `imageAttachmentCache`(存原图,供 `image_understand` 复看细节用),
4905
+ * 内联发给模型的图片先按 `ATTACHMENT_IMAGE_MAX_DIMENSION` 压缩降 token;
4906
+ * 单次超过 `MAX_INLINE_IMAGES` 张时,多出的图片只登记缓存 + 文字提示名称,不内联发送。
4907
+ * 之后步骤不再重复下发原图,需要复看细节时改由模型调用 `image_understand` 按需重传。
4806
4908
  */
4807
4909
  async #injectAttachments(prompt, attachments) {
4808
4910
  const textSegments = [prompt];
4809
4911
  const imageParts = [];
4912
+ let inlinedImageCount = 0;
4810
4913
  for (const attachment of attachments) {
4811
4914
  if (attachment.mimeType.startsWith("image/")) {
4915
+ this.imageAttachmentCache.set(attachment.name, attachment);
4916
+ if (inlinedImageCount >= MAX_INLINE_IMAGES) {
4917
+ 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.]`);
4918
+ continue;
4919
+ }
4920
+ inlinedImageCount++;
4921
+ const resizedDataUrl = await resizeImageDataUrl(attachment.dataUrl, ATTACHMENT_IMAGE_MAX_DIMENSION, ATTACHMENT_IMAGE_JPEG_QUALITY);
4922
+ 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}".]`);
4812
4923
  imageParts.push({
4813
4924
  type: "image_url",
4814
- image_url: { url: attachment.dataUrl }
4925
+ image_url: { url: resizedDataUrl }
4815
4926
  });
4816
4927
  continue;
4817
4928
  }
@@ -4824,6 +4935,33 @@ var AgentRuntime = class extends EventTarget {
4824
4935
  text: textSegments.join("\n\n")
4825
4936
  }, ...imageParts];
4826
4937
  }
4938
+ /**
4939
+ * 针对已缓存的图片附件发起一次独立的视觉问答,不进入主对话历史、不携带主链路的历史上下文。
4940
+ * 供 `image_understand` 工具使用:模型需要复看图片细节时按需调用,而不是每步都重发原图。
4941
+ * @param attachmentName - 附件名(即注入 prompt 时标注的名称)
4942
+ * @param query - 需要从图片中确认的具体问题
4943
+ * @param signal - 取消信号
4944
+ * @returns 视觉模型针对 query 给出的文字答案;附件不存在时返回错误提示
4945
+ */
4946
+ async describeImage(attachmentName, query, signal) {
4947
+ const attachment = this.imageAttachmentCache.get(attachmentName);
4948
+ if (!attachment) return `❌ Attachment "${attachmentName}" not found. Available image attachments: ${[...this.imageAttachmentCache.keys()].join(", ") || "none"}`;
4949
+ const answerTool = {
4950
+ description: "Answer the question about the image as precisely and completely as possible.",
4951
+ inputSchema: z.object({ answer: z.string() }),
4952
+ execute: async (args) => args
4953
+ };
4954
+ return (await this.#llm.invoke([{
4955
+ role: "user",
4956
+ content: [{
4957
+ type: "text",
4958
+ text: query
4959
+ }, {
4960
+ type: "image_url",
4961
+ image_url: { url: attachment.dataUrl }
4962
+ }]
4963
+ }], { answer_image_question: answerTool }, signal, { toolChoiceName: "answer_image_question" })).toolResult.answer;
4964
+ }
4827
4965
  /** 销毁 Agent:中止任务、释放浏览器控制器,并触发 dispose 事件(一次性) */
4828
4966
  dispose() {
4829
4967
  if (this.disposed) return;