mocode-ai 1.4.6 → 1.5.0

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.
@@ -12,10 +12,36 @@ import { mkdir, readFile } from 'node:fs/promises';
12
12
  import { dirname } from 'node:path';
13
13
  import { jailResolve } from '../../sandbox/index.js';
14
14
  import { captureDesktop } from '../../runtime/screen-capture.js';
15
- import { decodePng, encodePng, downscale, fitToLongEdge, crop, normToPhysical, normRectToPhysical, physicalToNorm, isNormCoord, } from '../../runtime/screen-pipeline.js';
15
+ import { decodePng, encodePng, downscale, fitToLongEdge, crop, readPngSize, diffRatio, normToPhysical, normRectToPhysical, physicalToNorm, isNormCoord, } from '../../runtime/screen-pipeline.js';
16
16
  import { createInputInjector } from '../../runtime/input-injector.js';
17
- /** 喂模型的截图长边上限(Anthropic 推荐区间)。 */
18
- const MAX_EDGE = 1280;
17
+ /**
18
+ * 喂模型的截图长边上限。1280 是 Anthropic 的兼容下限,1568 对齐主流视觉模型原生分辨率 ——
19
+ * 4K 屏上小字/密集 UI 的定位精度提升明显,代价是每张图多约 300 token。可用
20
+ * MOCODE_CU_MAX_EDGE 覆盖。
21
+ */
22
+ const DEFAULT_MAX_EDGE = 1568;
23
+ function maxEdge() {
24
+ const raw = Number(process.env.MOCODE_CU_MAX_EDGE);
25
+ if (!Number.isFinite(raw) || raw < 256 || raw > 4096)
26
+ return DEFAULT_MAX_EDGE;
27
+ return Math.round(raw);
28
+ }
29
+ /**
30
+ * 帧间差异低于此比例就认为「屏幕没变」,跳过回灌。
31
+ * 0.005 = 0.5%,足以吃掉抗锯齿/光标抖动,又不会漏掉真实重绘(局部重绘通常 >1%)。
32
+ */
33
+ const DEFAULT_DIFF_THRESHOLD = 0.005;
34
+ function diffThreshold() {
35
+ // 空串必须显式回落:Number('') === 0,而 0 能通过下面的区间校验 —— 用户在 .env 里清空这个
36
+ // 变量(注释掉的常见写法)会静默把去重关掉。0 是合法的显式值(关闭去重),空不是。
37
+ const rawEnv = process.env.MOCODE_CU_DIFF_THRESHOLD;
38
+ if (rawEnv === undefined || rawEnv.trim() === '')
39
+ return DEFAULT_DIFF_THRESHOLD;
40
+ const raw = Number(rawEnv);
41
+ if (!Number.isFinite(raw) || raw < 0 || raw > 1)
42
+ return DEFAULT_DIFF_THRESHOLD;
43
+ return raw;
44
+ }
19
45
  const WAIT_MAX_MS = 10000;
20
46
  const SCROLL_MAX = 10;
21
47
  const ACTIONS = [
@@ -98,7 +124,22 @@ export function validateComputerArgs(args) {
98
124
  }
99
125
  return null;
100
126
  }
101
- /** 抓一次主屏并返回物理分辨率。 */
127
+ /**
128
+ * 会话级几何缓存。显示器分辨率/DPI 在一次会话里几乎不变,而每次调用都重抓一次屏只为拿
129
+ * 宽高是纯粹的浪费 —— 抓屏是整条链路最贵的一步(冷启动 PowerShell + 4K 位图 CopyFromScreen)。
130
+ * 缓存后每个输入动作只需**一次**抓屏(动作后的重截屏),比原来少一半。
131
+ *
132
+ * 失效:每次动作后的重截屏都会回传 geometry,与之不符就立即更新缓存并提示模型重试
133
+ * (插拔外接屏 / 改缩放比时,这一步的坐标是用旧几何算的,必须让模型知道)。
134
+ */
135
+ let geometryCache = null;
136
+ /** 会话级上一帧(已 downscale),用于差分。/cu off 时清空。 */
137
+ let lastFrame = null;
138
+ export function resetComputerState() {
139
+ geometryCache = null;
140
+ lastFrame = null;
141
+ }
142
+ /** 抓一次主屏并返回几何信息。Windows 走 captureDesktop 的 geometry(零解码);其余平台只读 IHDR。 */
102
143
  async function capturePrimary(signal) {
103
144
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
104
145
  const outputPath = jailResolve(`.mocode/screenshots/computer-${ts}-${Math.random().toString(36).slice(2, 8)}.png`);
@@ -107,24 +148,99 @@ async function capturePrimary(signal) {
107
148
  if (cap.status !== 'passed') {
108
149
  throw new Error(`screenshot capture failed: ${cap.detail}`);
109
150
  }
110
- const png = decodePng(await readFile(outputPath));
111
- return { path: outputPath, physW: png.width, physH: png.height };
151
+ const g = cap.geometry;
152
+ if (g && g.shotW > 0 && g.shotH > 0 && g.physW > 0 && g.physH > 0) {
153
+ return {
154
+ path: outputPath,
155
+ shotW: g.shotW,
156
+ shotH: g.shotH,
157
+ physW: g.physW,
158
+ physH: g.physH,
159
+ originX: g.originX,
160
+ originY: g.originY,
161
+ };
162
+ }
163
+ // 非 Windows 或 geometry 缺失:只读 IHDR,不要为了两个数字解码 14MB 像素。
164
+ const size = readPngSize(await readFile(outputPath));
165
+ if (!size)
166
+ throw new Error('screenshot captured but its dimensions could not be read');
167
+ return {
168
+ path: outputPath,
169
+ shotW: size.width,
170
+ shotH: size.height,
171
+ physW: size.width,
172
+ physH: size.height,
173
+ originX: 0,
174
+ originY: 0,
175
+ };
176
+ }
177
+ /** 取当前屏幕几何:命中缓存则不抓屏(path 为 undefined)。 */
178
+ async function resolveScreen(signal) {
179
+ if (geometryCache)
180
+ return { screen: geometryCache };
181
+ const shot = await capturePrimary(signal);
182
+ geometryCache = {
183
+ shotW: shot.shotW,
184
+ shotH: shot.shotH,
185
+ physW: shot.physW,
186
+ physH: shot.physH,
187
+ originX: shot.originX,
188
+ originY: shot.originY,
189
+ };
190
+ return { screen: geometryCache, path: shot.path };
112
191
  }
113
- /** 读 PNG 文件 → 缩放 → 编码 → dataUrl。 */
114
- async function toAttachment(path) {
192
+ /** 用最新一次抓屏的几何校正缓存;返回 true 表示本次动作的坐标可能是用过期几何算的。 */
193
+ function syncGeometry(shot) {
194
+ const prev = geometryCache;
195
+ const changed = !prev ||
196
+ prev.physW !== shot.physW ||
197
+ prev.physH !== shot.physH ||
198
+ prev.shotW !== shot.shotW ||
199
+ prev.shotH !== shot.shotH ||
200
+ prev.originX !== shot.originX ||
201
+ prev.originY !== shot.originY;
202
+ geometryCache = { ...shot };
203
+ return changed && prev !== null;
204
+ }
205
+ /** 读 PNG 文件 → 缩放 → 编码 → dataUrl,顺带算出与上一帧的差异。 */
206
+ async function toAttachment(path, m) {
207
+ let t = Date.now();
115
208
  const png = decodePng(await readFile(path));
116
- const { img } = downscale(png, MAX_EDGE);
209
+ if (m)
210
+ m.decodeMs += Date.now() - t;
211
+ t = Date.now();
212
+ const { img } = downscale(png, maxEdge());
213
+ if (m)
214
+ m.scaleMs += Date.now() - t;
215
+ const prev = lastFrame;
216
+ let diff;
217
+ if (prev && prev.width === img.width && prev.height === img.height) {
218
+ diff = diffRatio(prev, img);
219
+ }
220
+ t = Date.now();
117
221
  const buf = encodePng(img);
222
+ if (m)
223
+ m.encodeMs += Date.now() - t;
118
224
  return {
119
225
  dataUrl: `data:image/png;base64,${buf.toString('base64')}`,
120
226
  bytes: buf.length,
121
227
  dispW: img.width,
122
228
  dispH: img.height,
229
+ diff,
230
+ img,
123
231
  };
124
232
  }
233
+ /** 回灌了一帧:把它设为新的差分基准。 */
234
+ function commitFrame(img) {
235
+ lastFrame = img;
236
+ }
125
237
  async function executeAction(injector, action, args, screen) {
126
- const { physW, physH } = screen;
127
- const toPhys = (pair) => normToPhysical(pair[0], pair[1], physW, physH);
238
+ const { physW, physH, originX, originY } = screen;
239
+ // norm1000 → 虚拟桌面物理像素:先按物理分辨率缩放,再叠加主屏原点(多屏)。
240
+ const toPhys = (pair) => {
241
+ const [x, y] = normToPhysical(pair[0], pair[1], physW, physH);
242
+ return [originX + x, originY + y];
243
+ };
128
244
  switch (action) {
129
245
  case 'mouse_move': {
130
246
  const [x, y] = toPhys(args.coordinate);
@@ -183,17 +299,51 @@ async function executeAction(injector, action, args, screen) {
183
299
  }
184
300
  case 'cursor_position': {
185
301
  const pos = await injector.cursorPosition();
186
- const [nx, ny] = physicalToNorm(pos.x, pos.y, physW, physH);
302
+ const [nx, ny] = physicalToNorm(pos.x - originX, pos.y - originY, physW, physH);
187
303
  return `cursor at physical (${pos.x}, ${pos.y}) = normalized (${nx}, ${ny})`;
188
304
  }
189
305
  default:
190
306
  throw new Error(`no executor for action "${action}"`);
191
307
  }
192
308
  }
309
+ const METRIC_CAP = 50;
310
+ const metrics = [];
311
+ function recordMetric(m) {
312
+ metrics.push(m);
313
+ if (metrics.length > METRIC_CAP)
314
+ metrics.shift();
315
+ }
316
+ function pct(values, p) {
317
+ if (!values.length)
318
+ return 0;
319
+ const s = [...values].sort((a, b) => a - b);
320
+ const i = Math.min(s.length - 1, Math.max(0, Math.round((p / 100) * (s.length - 1))));
321
+ return Math.round(s[i]);
322
+ }
323
+ export function getComputerMetrics() {
324
+ const totals = metrics.map((m) => m.totalMs);
325
+ const caps = metrics.map((m) => m.captureMs);
326
+ const skipped = metrics.filter((m) => m.skipped).length;
327
+ const bytes = metrics.reduce((a, m) => a + m.bytes, 0);
328
+ return {
329
+ count: metrics.length,
330
+ totalP50: pct(totals, 50),
331
+ totalP95: pct(totals, 95),
332
+ captureP50: pct(caps, 50),
333
+ skippedRatio: metrics.length ? skipped / metrics.length : 0,
334
+ avgBytes: metrics.length ? Math.round(bytes / metrics.length) : 0,
335
+ last: metrics[metrics.length - 1],
336
+ };
337
+ }
338
+ export function resetComputerMetrics() {
339
+ metrics.length = 0;
340
+ }
193
341
  export const computerTool = {
194
342
  name: 'computer',
195
343
  description: 'Control the desktop GUI: move/click the mouse, type text, press keys, scroll, and zoom into screen regions. ' +
196
- 'Every action returns a fresh screenshot as a visual attachment — inspect it before the next action and self-correct. ' +
344
+ 'Every action re-captures the screen. If the screen is unchanged since the last screenshot you saw, no image is ' +
345
+ 'attached and the result says so — treat the previous screenshot as still current instead of assuming the action failed. ' +
346
+ 'Inspect each attached screenshot before the next action and self-correct. ' +
197
347
  'Coordinates use a normalized 0-1000 grid (x right, y down) over the primary screen; you never need the physical resolution. ' +
198
348
  'Use zoom on a tight region before clicking small or dense UI elements. Destructive or sensitive targets ' +
199
349
  '(form submit, payment, credentials, delete/send) require explicit user intent — ask first.',
@@ -242,9 +392,26 @@ export const computerTool = {
242
392
  async execute(args, ctx) {
243
393
  const invalid = validateComputerArgs(args);
244
394
  if (invalid) {
245
- return { status: 'error', code: 'INVALID_ARGUMENTS', retryable: false, output: `Invalid computer action: ${invalid}` };
395
+ return {
396
+ status: 'error',
397
+ code: 'INVALID_ARGUMENTS',
398
+ retryable: false,
399
+ output: `Invalid computer action: ${invalid}`,
400
+ };
246
401
  }
247
402
  const action = args.action;
403
+ const started = Date.now();
404
+ const m = {
405
+ action,
406
+ captureMs: 0,
407
+ decodeMs: 0,
408
+ scaleMs: 0,
409
+ encodeMs: 0,
410
+ injectMs: 0,
411
+ totalMs: 0,
412
+ bytes: 0,
413
+ skipped: false,
414
+ };
248
415
  let injector;
249
416
  try {
250
417
  injector = createInputInjector();
@@ -254,60 +421,135 @@ export const computerTool = {
254
421
  return { status: 'error', code: 'EXECUTION_ERROR', retryable: false, output: message };
255
422
  }
256
423
  injector.bindSignal(ctx?.signal);
424
+ // left_mouse_down 之后若中途失败/中断,鼠标会一直处于按下状态 —— 用户的桌面就废了。
425
+ // 这里统一在 finally 里兜底释放。
426
+ let pendingMouseDown = false;
257
427
  try {
258
- // 动作前截屏:拿物理分辨率(norm1000 → 物理像素换算的基础)。
259
- const before = await capturePrimary(ctx?.signal);
260
- const screen = { physW: before.physW, physH: before.physH };
261
- // zoom:不注入输入,裁 region 放大回灌。
262
- if (action === 'zoom') {
263
- const rect = normRectToPhysical(args.region, screen.physW, screen.physH);
264
- const png = decodePng(await readFile(before.path));
265
- const zoomed = fitToLongEdge(crop(png, rect.x, rect.y, rect.w, rect.h), MAX_EDGE);
428
+ // 只有 zoom / screenshot 真正需要一张新位图;输入动作只需要**几何**,命中会话缓存就不抓屏
429
+ // (抓屏是整条链路最贵的一步,省掉这一步等于把每步的本地开销砍半)。
430
+ let screen;
431
+ let freshPath;
432
+ if (action === 'zoom' || action === 'screenshot') {
433
+ const t = Date.now();
434
+ const shot = await capturePrimary(ctx?.signal);
435
+ m.captureMs += Date.now() - t;
436
+ screen = {
437
+ shotW: shot.shotW,
438
+ shotH: shot.shotH,
439
+ physW: shot.physW,
440
+ physH: shot.physH,
441
+ originX: shot.originX,
442
+ originY: shot.originY,
443
+ };
444
+ syncGeometry(screen);
445
+ freshPath = shot.path;
446
+ }
447
+ else {
448
+ const t = Date.now();
449
+ const r = await resolveScreen(ctx?.signal);
450
+ m.captureMs += Date.now() - t;
451
+ screen = r.screen;
452
+ freshPath = r.path;
453
+ }
454
+ // zoom:不注入输入,裁 region 放大回灌。裁剪发生在截图位图上,故用 shotW/shotH。
455
+ if (action === 'zoom' && freshPath) {
456
+ const rect = normRectToPhysical(args.region, screen.shotW, screen.shotH);
457
+ let t = Date.now();
458
+ const png = decodePng(await readFile(freshPath));
459
+ m.decodeMs += Date.now() - t;
460
+ t = Date.now();
461
+ const zoomed = fitToLongEdge(crop(png, rect.x, rect.y, rect.w, rect.h), maxEdge());
462
+ m.scaleMs += Date.now() - t;
463
+ t = Date.now();
266
464
  const buf = encodePng(zoomed);
465
+ m.encodeMs += Date.now() - t;
466
+ m.bytes = buf.length;
467
+ // zoom 出的图不是全屏基准,不能喂给差分。
468
+ lastFrame = null;
267
469
  return {
268
470
  status: 'success',
269
471
  code: 'OK',
270
472
  retryable: false,
271
- output: `Zoomed into region ${JSON.stringify(args.region)} → physical (${rect.x}, ${rect.y}, ${rect.w}×${rect.h}), ` +
473
+ output: `Zoomed into region ${JSON.stringify(args.region)} → screenshot pixels (${rect.x}, ${rect.y}, ${rect.w}×${rect.h}), ` +
272
474
  `magnified to ${zoomed.width}×${zoomed.height}. The attached image shows ONLY this region — ` +
273
475
  `coordinates you output still map to the FULL screen, so do not reuse this region's local coordinates for clicks.`,
274
476
  modelAttachments: [
275
- { type: 'image', name: 'computer-zoom.png', mime: 'image/png', dataUrl: `data:image/png;base64,${buf.toString('base64')}` },
477
+ {
478
+ type: 'image',
479
+ name: 'computer-zoom.png',
480
+ mime: 'image/png',
481
+ dataUrl: `data:image/png;base64,${buf.toString('base64')}`,
482
+ },
276
483
  ],
277
484
  };
278
485
  }
279
486
  // screenshot:只截屏回灌(target 可指定 all)。
280
487
  if (action === 'screenshot') {
281
- let shotPath = before.path;
488
+ let shotPath = freshPath;
282
489
  if (args.target === 'all') {
283
490
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
284
491
  const allPath = jailResolve(`.mocode/screenshots/computer-${ts}-all.png`);
285
492
  await mkdir(dirname(allPath), { recursive: true });
493
+ const t = Date.now();
286
494
  const cap = await captureDesktop(allPath, 'all', ctx?.signal);
495
+ m.captureMs += Date.now() - t;
287
496
  if (cap.status !== 'passed')
288
497
  throw new Error(`screenshot capture failed: ${cap.detail}`);
289
498
  shotPath = allPath;
290
499
  }
291
- const att = await toAttachment(shotPath);
500
+ const att = await toAttachment(shotPath, m);
501
+ commitFrame(att.img);
502
+ m.bytes = att.bytes;
292
503
  return {
293
504
  status: 'success',
294
505
  code: 'OK',
295
506
  retryable: false,
296
- output: `Screenshot captured (physical ${screen.physW}×${screen.physH}, shown at ${att.dispW}×${att.dispH}). ` +
507
+ output: `Screenshot captured (primary screen ${screen.physW}×${screen.physH} physical, shown at ${att.dispW}×${att.dispH}). ` +
297
508
  'Coordinates remain normalized 0-1000 over the primary screen. Visual content is attached to the next model request.',
298
- modelAttachments: [{ type: 'image', name: 'computer-screenshot.png', mime: 'image/png', dataUrl: att.dataUrl }],
509
+ modelAttachments: [
510
+ { type: 'image', name: 'computer-screenshot.png', mime: 'image/png', dataUrl: att.dataUrl },
511
+ ],
299
512
  };
300
513
  }
301
514
  // 输入动作:注入 → 重截屏回灌(wait 也回灌,等待后界面往往已变化)。
515
+ if (action === 'left_mouse_down')
516
+ pendingMouseDown = true;
517
+ let t = Date.now();
302
518
  const summary = await executeAction(injector, action, args, screen);
519
+ m.injectMs += Date.now() - t;
520
+ if (action === 'left_mouse_up')
521
+ pendingMouseDown = false;
522
+ t = Date.now();
303
523
  const after = await capturePrimary(ctx?.signal);
304
- const att = await toAttachment(after.path);
524
+ m.captureMs += Date.now() - t;
525
+ const geometryStale = syncGeometry(after);
526
+ const att = await toAttachment(after.path, m);
527
+ m.bytes = att.bytes;
528
+ m.diff = att.diff;
529
+ const staleNote = geometryStale
530
+ ? ' WARNING: display geometry changed mid-action (monitor or scaling change); ' +
531
+ 'coordinates for this action used the previous geometry — verify and repeat if the click landed wrong.'
532
+ : '';
533
+ // 差分命中:屏幕与模型上次看到的相比没变,再发一张同样的图只是白烧 token 和 prefill。
534
+ if (att.diff !== undefined && att.diff < diffThreshold()) {
535
+ m.skipped = true;
536
+ return {
537
+ status: 'success',
538
+ code: 'OK',
539
+ retryable: false,
540
+ output: `${summary}. No visible change on screen (frame diff ${(att.diff * 100).toFixed(2)}% < ` +
541
+ `${(diffThreshold() * 100).toFixed(2)}% threshold) — no new screenshot is attached; ` +
542
+ 'the previous screenshot is still current. If you expected a change, the action likely did not ' +
543
+ `take effect: re-check the target coordinates or try a different approach.${staleNote}`,
544
+ };
545
+ }
546
+ commitFrame(att.img);
305
547
  return {
306
548
  status: 'success',
307
549
  code: 'OK',
308
550
  retryable: false,
309
- output: `${summary}. Screen re-captured (physical ${screen.physW}×${screen.physH}, shown at ${att.dispW}×${att.dispH}): ` +
310
- 'inspect the attached screenshot to verify the result before the next action.',
551
+ output: `${summary}. Screen re-captured (primary screen ${screen.physW}×${screen.physH} physical, shown at ${att.dispW}×${att.dispH}): ` +
552
+ `inspect the attached screenshot to verify the result before the next action.${staleNote}`,
311
553
  modelAttachments: [{ type: 'image', name: 'computer-result.png', mime: 'image/png', dataUrl: att.dataUrl }],
312
554
  };
313
555
  }
@@ -316,7 +558,24 @@ export const computerTool = {
316
558
  if (ctx?.signal?.aborted || /aborted/i.test(message)) {
317
559
  return { status: 'aborted', code: 'ABORTED', retryable: false, output: message };
318
560
  }
319
- return { status: 'error', code: 'EXECUTION_ERROR', retryable: false, output: `computer action failed: ${message}` };
561
+ return {
562
+ status: 'error',
563
+ code: 'EXECUTION_ERROR',
564
+ retryable: false,
565
+ output: `computer action failed: ${message}`,
566
+ };
567
+ }
568
+ finally {
569
+ if (pendingMouseDown) {
570
+ try {
571
+ await injector.mouseUp('left');
572
+ }
573
+ catch {
574
+ // 释放失败也要让上层拿到原本的错误,不掩盖。
575
+ }
576
+ }
577
+ m.totalMs = Date.now() - started;
578
+ recordMetric(m);
320
579
  }
321
580
  },
322
581
  };
@@ -1,9 +1,12 @@
1
- import { mkdir } from 'node:fs/promises';
2
- import { dirname, extname } from 'node:path';
1
+ import { mkdir, readFile } from 'node:fs/promises';
2
+ import { basename, dirname, extname } from 'node:path';
3
3
  import { loadImageAttachment, MAX_INLINE_BYTES_DEFAULT } from '../../attachments/image.js';
4
4
  import { jailResolve } from '../../sandbox/index.js';
5
5
  // 平台捕获逻辑已抽到 runtime/screen-capture.ts(computer 工具闭环共用),本文件只做薄封装。
6
6
  import { captureDesktop } from '../../runtime/screen-capture.js';
7
+ import { decodePng, downscale, encodePng } from '../../runtime/screen-pipeline.js';
8
+ /** 原图超过内联上限时降级缩放的长边(对齐主流视觉模型的原生分辨率)。 */
9
+ const FALLBACK_MAX_EDGE = 1568;
7
10
  export const screenshotTool = {
8
11
  name: 'screenshot',
9
12
  description: 'Capture the desktop and inspect it as visual model input. Use this to diagnose visible UI state, dialogs, rendering problems, or applications that cannot be understood from source files alone. The user must approve each capture because screenshots may contain sensitive information.',
@@ -68,15 +71,41 @@ export const screenshotTool = {
68
71
  const loaded = await loadImageAttachment(outputPath, {
69
72
  maxBytes: MAX_INLINE_BYTES_DEFAULT,
70
73
  });
74
+ const detail = args.detail === 'low' || args.detail === 'auto' ? args.detail : 'high';
75
+ // 高分屏(尤其 DPI aware 后抓到物理分辨率)的 PNG 可能超过内联上限:
76
+ // 原图仍留在磁盘上,回灌改用缩放版本,不让工具因此失败。
71
77
  if (!loaded.ok) {
72
- return {
73
- status: 'error',
74
- code: 'EXECUTION_ERROR',
75
- retryable: false,
76
- output: `Screenshot saved to ${requestedPath}, but it could not be attached: ${loaded.reason}`,
77
- };
78
+ try {
79
+ const png = decodePng(await readFile(outputPath));
80
+ const { img } = downscale(png, FALLBACK_MAX_EDGE);
81
+ const buf = encodePng(img);
82
+ return {
83
+ status: 'success',
84
+ code: 'OK',
85
+ retryable: false,
86
+ output: `Captured ${target} display to ${requestedPath} (${png.width}×${png.height}); ` +
87
+ `it exceeded the inline limit, so the attached copy was resized to ${img.width}×${img.height}.`,
88
+ modelAttachments: [
89
+ {
90
+ type: 'image',
91
+ name: basename(outputPath),
92
+ mime: 'image/png',
93
+ dataUrl: `data:image/png;base64,${buf.toString('base64')}`,
94
+ detail,
95
+ },
96
+ ],
97
+ };
98
+ }
99
+ catch (error) {
100
+ const message = error instanceof Error ? error.message : String(error);
101
+ return {
102
+ status: 'error',
103
+ code: 'EXECUTION_ERROR',
104
+ retryable: false,
105
+ output: `Screenshot saved to ${requestedPath}, but it could not be attached: ${loaded.reason}${message ? ` (resize fallback failed: ${message})` : ''}`,
106
+ };
107
+ }
78
108
  }
79
- const detail = args.detail === 'low' || args.detail === 'auto' ? args.detail : 'high';
80
109
  const { att } = loaded;
81
110
  return {
82
111
  status: 'success',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.4.6",
4
- "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
3
+ "version": "1.5.0",
4
+ "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 25 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mocode": "bin/mocode.js",