@xcanwin/manyoyo 5.9.2 → 5.9.11

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/lib/web/server.js CHANGED
@@ -34,7 +34,8 @@ const WEB_TERMINAL_MIN_ROWS = 12;
34
34
  const WEB_AGENT_CONTEXT_MAX_MESSAGES = 24;
35
35
  const WEB_AGENT_CONTEXT_MAX_CHARS = 6000;
36
36
  const WEB_AGENT_CONTEXT_PER_MESSAGE_MAX_CHARS = 600;
37
- const WEB_FILE_PREVIEW_MAX_BYTES = 256 * 1024;
37
+ const WEB_FILE_PREVIEW_MAX_BYTES = 512 * 1024;
38
+ const WEB_FILE_EDIT_MAX_BYTES = 2 * 1024 * 1024;
38
39
  const WEB_AUTH_COOKIE_NAME = 'manyoyo_web_auth';
39
40
  const WEB_AUTH_TTL_SECONDS = 12 * 60 * 60;
40
41
  const WEB_SESSION_KEY_SEPARATOR = '~';
@@ -165,6 +166,7 @@ function createEmptyWebAgentSession(agentId, agentName) {
165
166
  agentId,
166
167
  agentName: normalizeWebAgentName(agentId, agentName),
167
168
  agentPromptCommand: '',
169
+ createdAt: null,
168
170
  updatedAt: null,
169
171
  messages: [],
170
172
  lastResumeAt: null,
@@ -181,6 +183,7 @@ function normalizeWebAgentSessionRecord(agentId, rawAgent) {
181
183
  agentPromptCommand: typeof source.agentPromptCommand === 'string'
182
184
  ? normalizeAgentPromptCommandTemplate(source.agentPromptCommand, `agents.${agentId}.agentPromptCommand`)
183
185
  : '',
186
+ createdAt: typeof source.createdAt === 'string' ? source.createdAt : null,
184
187
  updatedAt: typeof source.updatedAt === 'string' ? source.updatedAt : null,
185
188
  messages: Array.isArray(source.messages) ? source.messages : [],
186
189
  lastResumeAt: typeof source.lastResumeAt === 'string' ? source.lastResumeAt : null,
@@ -384,6 +387,45 @@ function listWebAgentSessions(history, options = {}) {
384
387
  });
385
388
  }
386
389
 
390
+ function getWebAgentCreationRank(agentId) {
391
+ if (agentId === WEB_DEFAULT_AGENT_ID) {
392
+ return 1;
393
+ }
394
+ const matched = String(agentId || '').match(/^agent-(\d+)$/);
395
+ return matched ? (Number(matched[1]) || 0) : 0;
396
+ }
397
+
398
+ function getWebSessionCreatedTime(sessionSummary) {
399
+ if (sessionSummary && sessionSummary.createdAt) {
400
+ const time = new Date(sessionSummary.createdAt).getTime();
401
+ if (Number.isFinite(time)) {
402
+ return time;
403
+ }
404
+ }
405
+ return 0;
406
+ }
407
+
408
+ function compareWebSessionCreatedDesc(a, b) {
409
+ const timeA = getWebSessionCreatedTime(a);
410
+ const timeB = getWebSessionCreatedTime(b);
411
+ if (timeA !== timeB) {
412
+ return timeB - timeA;
413
+ }
414
+ if (a && b && a.containerName === b.containerName) {
415
+ const rankA = getWebAgentCreationRank(a.agentId);
416
+ const rankB = getWebAgentCreationRank(b.agentId);
417
+ if (rankA !== rankB) {
418
+ return rankB - rankA;
419
+ }
420
+ }
421
+ const updatedA = a && a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
422
+ const updatedB = b && b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
423
+ if (updatedA !== updatedB) {
424
+ return updatedB - updatedA;
425
+ }
426
+ return String((a && a.name) || '').localeCompare(String((b && b.name) || ''), 'zh-CN');
427
+ }
428
+
387
429
  function createWebSessionMessageId() {
388
430
  return `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
389
431
  }
@@ -402,6 +444,9 @@ function appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, role,
402
444
  timestamp,
403
445
  ...extra
404
446
  };
447
+ if (!agentSession.createdAt) {
448
+ agentSession.createdAt = timestamp;
449
+ }
405
450
  agentSession.messages.push(message);
406
451
 
407
452
  if (agentSession.messages.length > WEB_HISTORY_MAX_MESSAGES) {
@@ -584,7 +629,11 @@ function createWebAgentSession(history) {
584
629
  }
585
630
  const agentId = `agent-${agentIndex}`;
586
631
  const agentSession = createEmptyWebAgentSession(agentId, `AGENT ${agentIndex}`);
632
+ const timestamp = new Date().toISOString();
633
+ agentSession.createdAt = timestamp;
634
+ agentSession.updatedAt = timestamp;
587
635
  sessionHistory.agents[agentId] = agentSession;
636
+ sessionHistory.updatedAt = timestamp;
588
637
  return agentSession;
589
638
  }
590
639
 
@@ -2609,7 +2658,12 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
2609
2658
  const extractedAgentMessage = extractAgentMessageFromStructuredOutput(agentProgram, clippedStdout);
2610
2659
  const cleanOutputSource = extractedAgentMessage || clippedRaw;
2611
2660
  const output = clipText(stripAnsi(cleanOutputSource).trim() || '(无输出)');
2612
- resolve({ exitCode, output });
2661
+ resolve({
2662
+ exitCode,
2663
+ output,
2664
+ stdout: clippedStdout,
2665
+ stderr: clippedStderr
2666
+ });
2613
2667
  });
2614
2668
  });
2615
2669
  }
@@ -2631,7 +2685,7 @@ async function execJsonCommandInWebContainer(ctx, containerName, command) {
2631
2685
  throw new Error(result.output || '容器命令执行失败');
2632
2686
  }
2633
2687
  try {
2634
- return JSON.parse(String(result.output || '{}'));
2688
+ return JSON.parse(String(result.stdout || '{}'));
2635
2689
  } catch (e) {
2636
2690
  throw new Error('容器返回了无法解析的 JSON');
2637
2691
  }
@@ -2700,13 +2754,17 @@ try {
2700
2754
  `);
2701
2755
  }
2702
2756
 
2703
- function buildContainerFileReadCommand(requestedPath) {
2757
+ function buildContainerFileReadCommand(requestedPath, options = {}) {
2758
+ const opts = options && typeof options === 'object' ? options : {};
2759
+ const maxBytes = Number.isFinite(opts.maxBytes) && opts.maxBytes > 0
2760
+ ? Math.floor(opts.maxBytes)
2761
+ : 0;
2704
2762
  return buildWebContainerNodeCommand(`
2705
2763
  // __MANYOYO_FS_READ__
2706
2764
  const fs = require('fs');
2707
2765
 
2708
2766
  const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
2709
- const maxBytes = ${String(WEB_FILE_PREVIEW_MAX_BYTES)};
2767
+ const maxBytes = ${String(maxBytes)};
2710
2768
 
2711
2769
  function looksBinary(buffer) {
2712
2770
  const length = Math.min(buffer.length, 4096);
@@ -2731,7 +2789,7 @@ try {
2731
2789
  }
2732
2790
 
2733
2791
  const size = stat.size;
2734
- const readBytes = Math.min(size, maxBytes);
2792
+ const readBytes = maxBytes > 0 ? Math.min(size, maxBytes) : size;
2735
2793
  const buffer = Buffer.alloc(readBytes);
2736
2794
  const fd = fs.openSync(realPath, 'r');
2737
2795
  try {
@@ -2745,14 +2803,14 @@ try {
2745
2803
  path: realPath,
2746
2804
  kind: 'binary',
2747
2805
  size,
2748
- truncated: size > maxBytes
2806
+ truncated: maxBytes > 0 && size > maxBytes
2749
2807
  }));
2750
2808
  } else {
2751
2809
  process.stdout.write(JSON.stringify({
2752
2810
  path: realPath,
2753
2811
  kind: 'text',
2754
2812
  size,
2755
- truncated: size > maxBytes,
2813
+ truncated: maxBytes > 0 && size > maxBytes,
2756
2814
  content: buffer.toString('utf8')
2757
2815
  }));
2758
2816
  }
@@ -2764,6 +2822,71 @@ try {
2764
2822
  `);
2765
2823
  }
2766
2824
 
2825
+ function buildContainerFileWriteCommand(requestedPath, content) {
2826
+ return buildWebContainerNodeCommand(`
2827
+ // __MANYOYO_FS_WRITE__
2828
+ const fs = require('fs');
2829
+
2830
+ const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
2831
+ const nextContent = ${JSON.stringify(String(content == null ? '' : content))};
2832
+
2833
+ try {
2834
+ const realPath = fs.realpathSync(requestedPath);
2835
+ const stat = fs.statSync(realPath);
2836
+ if (!stat.isFile()) {
2837
+ throw new Error('目标不是文件: ' + realPath);
2838
+ }
2839
+
2840
+ fs.writeFileSync(realPath, nextContent, 'utf8');
2841
+ const savedStat = fs.statSync(realPath);
2842
+ process.stdout.write(JSON.stringify({
2843
+ path: realPath,
2844
+ saved: true,
2845
+ size: savedStat.size
2846
+ }));
2847
+ } catch (e) {
2848
+ process.stdout.write(JSON.stringify({
2849
+ error: e && e.message ? e.message : '保存文件失败'
2850
+ }));
2851
+ }
2852
+ `);
2853
+ }
2854
+
2855
+ function buildContainerFileMkdirCommand(requestedPath) {
2856
+ return buildWebContainerNodeCommand(`
2857
+ // __MANYOYO_FS_MKDIR__
2858
+ const fs = require('fs');
2859
+ const path = require('path');
2860
+
2861
+ const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
2862
+
2863
+ try {
2864
+ const resolvedPath = path.resolve(requestedPath);
2865
+ const parentPath = path.dirname(resolvedPath);
2866
+ const realParentPath = fs.realpathSync(parentPath);
2867
+ const targetPath = path.join(realParentPath, path.basename(resolvedPath));
2868
+ if (fs.existsSync(targetPath)) {
2869
+ throw new Error('目录已存在: ' + targetPath);
2870
+ }
2871
+
2872
+ fs.mkdirSync(targetPath, { recursive: true });
2873
+ const stat = fs.statSync(targetPath);
2874
+ process.stdout.write(JSON.stringify({
2875
+ path: targetPath,
2876
+ name: path.basename(targetPath),
2877
+ kind: 'directory',
2878
+ size: 0,
2879
+ mtimeMs: stat.mtimeMs,
2880
+ created: true
2881
+ }));
2882
+ } catch (e) {
2883
+ process.stdout.write(JSON.stringify({
2884
+ error: e && e.message ? e.message : '创建目录失败'
2885
+ }));
2886
+ }
2887
+ `);
2888
+ }
2889
+
2767
2890
  async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerName, command, options = {}) {
2768
2891
  const opts = options && typeof options === 'object' ? options : {};
2769
2892
  const sessionRef = typeof sessionRefOrContainerName === 'string'
@@ -3041,7 +3164,13 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
3041
3164
  status: containerInfo.status || 'history',
3042
3165
  defaultCommand: containerInfo.defaultCommand || ''
3043
3166
  });
3044
- const updatedAt = agentSession.updatedAt || history.updatedAt || (latestMessage && latestMessage.timestamp) || containerInfo.createdAt || null;
3167
+ const createdAt = agentSession.createdAt || containerInfo.createdAt || null;
3168
+ const updatedAt = agentSession.updatedAt
3169
+ || (latestMessage && latestMessage.timestamp)
3170
+ || (agentId === WEB_DEFAULT_AGENT_ID
3171
+ ? (history.updatedAt || containerInfo.createdAt)
3172
+ : null)
3173
+ || null;
3045
3174
  return {
3046
3175
  name: buildWebSessionKey(containerName, agentId),
3047
3176
  containerName,
@@ -3049,6 +3178,7 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
3049
3178
  agentName: agentSession.agentName,
3050
3179
  status: containerInfo.status || 'history',
3051
3180
  image: containerInfo.image || '',
3181
+ createdAt,
3052
3182
  updatedAt,
3053
3183
  messageCount: agentSession.messages.length,
3054
3184
  agentEnabled: isAgentPromptCommandEnabled(effectiveAgentPromptCommand),
@@ -3533,6 +3663,25 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3533
3663
  });
3534
3664
  }
3535
3665
  },
3666
+ {
3667
+ method: 'POST',
3668
+ match: currentPath => currentPath === '/api/fs/directories/mkdir' ? [] : null,
3669
+ handler: async () => {
3670
+ const payload = await readJsonBody(req);
3671
+ const requestedPath = expandHomeAliasPath(String(payload && payload.path ? payload.path : '').trim());
3672
+ if (!requestedPath) {
3673
+ sendJson(res, 400, { error: 'path 不能为空' });
3674
+ return;
3675
+ }
3676
+
3677
+ const targetPath = path.resolve(requestedPath);
3678
+ fs.mkdirSync(targetPath, { recursive: true });
3679
+ sendJson(res, 200, {
3680
+ path: targetPath,
3681
+ created: true
3682
+ });
3683
+ }
3684
+ },
3536
3685
  {
3537
3686
  method: 'GET',
3538
3687
  match: currentPath => currentPath === '/api/config' ? [] : null,
@@ -3594,11 +3743,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3594
3743
  }))
3595
3744
  .filter(Boolean);
3596
3745
  })
3597
- .sort((a, b) => {
3598
- const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
3599
- const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
3600
- return timeB - timeA;
3601
- });
3746
+ .sort(compareWebSessionCreatedDesc);
3602
3747
 
3603
3748
  sendJson(res, 200, { sessions });
3604
3749
  }
@@ -3696,6 +3841,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3696
3841
  }
3697
3842
  const requestUrl = new URL(req.url || '/api/sessions/x/fs/read', 'http://localhost');
3698
3843
  const targetPath = String(requestUrl.searchParams.get('path') || '').trim();
3844
+ const fullRequested = ['1', 'true', 'yes'].includes(String(requestUrl.searchParams.get('full') || '').toLowerCase());
3699
3845
  if (!targetPath) {
3700
3846
  sendJson(res, 400, { error: 'path 不能为空' });
3701
3847
  return;
@@ -3705,7 +3851,9 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3705
3851
  const payload = await execJsonCommandInWebContainer(
3706
3852
  ctx,
3707
3853
  sessionRef.containerName,
3708
- buildContainerFileReadCommand(targetPath)
3854
+ buildContainerFileReadCommand(targetPath, {
3855
+ maxBytes: fullRequested ? 0 : WEB_FILE_PREVIEW_MAX_BYTES
3856
+ })
3709
3857
  );
3710
3858
  if (payload && payload.error) {
3711
3859
  sendJson(res, 400, { error: payload.error });
@@ -3713,10 +3861,77 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3713
3861
  }
3714
3862
  if (payload && payload.kind === 'text') {
3715
3863
  payload.language = inferFileLanguage(payload.path);
3864
+ payload.editable = payload.truncated !== true
3865
+ && Number(payload.size || 0) < WEB_FILE_EDIT_MAX_BYTES;
3716
3866
  }
3717
3867
  sendJson(res, 200, payload);
3718
3868
  }
3719
3869
  },
3870
+ {
3871
+ method: 'PUT',
3872
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/fs\/write$/),
3873
+ handler: async match => {
3874
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3875
+ if (!sessionRef) {
3876
+ return;
3877
+ }
3878
+ const payload = await readJsonBody(req);
3879
+ const targetPath = String(payload && payload.path ? payload.path : '').trim();
3880
+ const content = typeof payload.content === 'string' ? payload.content : null;
3881
+ if (!targetPath) {
3882
+ sendJson(res, 400, { error: 'path 不能为空' });
3883
+ return;
3884
+ }
3885
+ if (content === null) {
3886
+ sendJson(res, 400, { error: 'content 必须是字符串' });
3887
+ return;
3888
+ }
3889
+ if (Buffer.byteLength(content, 'utf8') >= WEB_FILE_EDIT_MAX_BYTES) {
3890
+ sendJson(res, 400, { error: '文件过大,当前仅支持编辑小于 2MB 的文本文件' });
3891
+ return;
3892
+ }
3893
+
3894
+ await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
3895
+ const result = await execJsonCommandInWebContainer(
3896
+ ctx,
3897
+ sessionRef.containerName,
3898
+ buildContainerFileWriteCommand(targetPath, content)
3899
+ );
3900
+ if (result && result.error) {
3901
+ sendJson(res, 400, { error: result.error });
3902
+ return;
3903
+ }
3904
+ sendJson(res, 200, result);
3905
+ }
3906
+ },
3907
+ {
3908
+ method: 'POST',
3909
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/fs\/mkdir$/),
3910
+ handler: async match => {
3911
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3912
+ if (!sessionRef) {
3913
+ return;
3914
+ }
3915
+ const payload = await readJsonBody(req);
3916
+ const targetPath = String(payload && payload.path ? payload.path : '').trim();
3917
+ if (!targetPath) {
3918
+ sendJson(res, 400, { error: 'path 不能为空' });
3919
+ return;
3920
+ }
3921
+
3922
+ await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
3923
+ const result = await execJsonCommandInWebContainer(
3924
+ ctx,
3925
+ sessionRef.containerName,
3926
+ buildContainerFileMkdirCommand(targetPath)
3927
+ );
3928
+ if (result && result.error) {
3929
+ sendJson(res, 400, { error: result.error });
3930
+ return;
3931
+ }
3932
+ sendJson(res, 200, result);
3933
+ }
3934
+ },
3720
3935
  {
3721
3936
  method: 'GET',
3722
3937
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/detail$/),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.9.2",
3
+ "version": "5.9.11",
4
4
  "imageVersion": "1.9.0-common",
5
5
  "playwrightCliVersion": "0.1.1",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",