@xcanwin/manyoyo 5.9.0 → 5.9.1

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.
@@ -92,6 +92,10 @@
92
92
  controller: null,
93
93
  traceMessageId: ''
94
94
  },
95
+ agentRecovery: {
96
+ sessionName: '',
97
+ timer: null
98
+ },
95
99
  terminal: {
96
100
  term: null,
97
101
  fitAddon: null,
@@ -1334,6 +1338,114 @@
1334
1338
  );
1335
1339
  }
1336
1340
 
1341
+ function clearAgentRecoveryPoll() {
1342
+ if (state.agentRecovery && state.agentRecovery.timer) {
1343
+ window.clearTimeout(state.agentRecovery.timer);
1344
+ state.agentRecovery.timer = null;
1345
+ }
1346
+ if (state.agentRecovery) {
1347
+ state.agentRecovery.sessionName = '';
1348
+ }
1349
+ }
1350
+
1351
+ function hasPendingAgentMessagesForSession(sessionName) {
1352
+ const targetSession = String(sessionName || '').trim();
1353
+ if (!targetSession || state.active !== targetSession) {
1354
+ return false;
1355
+ }
1356
+ return state.messages.some(function (message) {
1357
+ return Boolean(
1358
+ message
1359
+ && message.mode === 'agent'
1360
+ && (message.pending === true || message.streamingReply === true)
1361
+ );
1362
+ });
1363
+ }
1364
+
1365
+ function scheduleAgentRecoveryPoll(sessionName, immediate) {
1366
+ const targetSession = String(sessionName || '').trim();
1367
+ if (!targetSession || state.active !== targetSession) {
1368
+ clearAgentRecoveryPoll();
1369
+ return;
1370
+ }
1371
+ if (isAgentRunActiveForSession(targetSession)) {
1372
+ clearAgentRecoveryPoll();
1373
+ return;
1374
+ }
1375
+ if (!hasPendingAgentMessagesForSession(targetSession)) {
1376
+ clearAgentRecoveryPoll();
1377
+ return;
1378
+ }
1379
+ if (state.agentRecovery.sessionName !== targetSession) {
1380
+ clearAgentRecoveryPoll();
1381
+ state.agentRecovery.sessionName = targetSession;
1382
+ }
1383
+ if (state.agentRecovery.timer) {
1384
+ return;
1385
+ }
1386
+
1387
+ const delay = immediate === true ? 0 : 1500;
1388
+ state.agentRecovery.timer = window.setTimeout(async function () {
1389
+ state.agentRecovery.timer = null;
1390
+ if (state.active !== targetSession || isAgentRunActiveForSession(targetSession)) {
1391
+ clearAgentRecoveryPoll();
1392
+ syncUi();
1393
+ return;
1394
+ }
1395
+ try {
1396
+ await refreshSessionsSilent({ preferredName: targetSession });
1397
+ await Promise.all([
1398
+ loadMessagesForSession(targetSession, { silent: true }),
1399
+ loadSessionDetailForSession(targetSession)
1400
+ ]);
1401
+ } catch (e) {
1402
+ // 静默失败,等待下一轮轮询
1403
+ }
1404
+ if (state.active === targetSession && !isAgentRunActiveForSession(targetSession) && hasPendingAgentMessagesForSession(targetSession)) {
1405
+ scheduleAgentRecoveryPoll(targetSession, false);
1406
+ } else {
1407
+ clearAgentRecoveryPoll();
1408
+ }
1409
+ syncUi();
1410
+ }, delay);
1411
+ }
1412
+
1413
+ function syncAgentRecoveryForSession(sessionName) {
1414
+ const targetSession = String(sessionName || '').trim();
1415
+ if (!targetSession || state.active !== targetSession) {
1416
+ clearAgentRecoveryPoll();
1417
+ return;
1418
+ }
1419
+ if (isAgentRunActiveForSession(targetSession)) {
1420
+ clearAgentRecoveryPoll();
1421
+ return;
1422
+ }
1423
+ if (hasPendingAgentMessagesForSession(targetSession)) {
1424
+ scheduleAgentRecoveryPoll(targetSession, false);
1425
+ return;
1426
+ }
1427
+ if (state.agentRecovery.sessionName === targetSession) {
1428
+ clearAgentRecoveryPoll();
1429
+ }
1430
+ }
1431
+
1432
+ async function recoverAgentRunFromServer(sessionName) {
1433
+ const targetSession = String(sessionName || '').trim();
1434
+ if (!targetSession) {
1435
+ return false;
1436
+ }
1437
+ try {
1438
+ await refreshSessionsSilent({ preferredName: targetSession });
1439
+ await Promise.all([
1440
+ loadMessagesForSession(targetSession, { silent: true }),
1441
+ loadSessionDetailForSession(targetSession)
1442
+ ]);
1443
+ } catch (e) {
1444
+ return false;
1445
+ }
1446
+ return hasPendingAgentMessagesForSession(targetSession);
1447
+ }
1448
+
1337
1449
  function isActiveSessionHistoryOnly() {
1338
1450
  const session = getActiveSession();
1339
1451
  return sessionStatusInfo(session && session.status).tone === 'history';
@@ -2076,7 +2188,7 @@
2076
2188
  });
2077
2189
  }
2078
2190
 
2079
- const activeAgentRunning = isAgentRunActiveForSession(state.active);
2191
+ const activeAgentRunning = isAgentRunActiveForSession(state.active) || hasPendingAgentMessagesForSession(state.active);
2080
2192
  const busy = state.loadingSessions || state.loadingMessages || state.sending;
2081
2193
  refreshBtn.disabled = busy;
2082
2194
  if (addAgentBtn) {
@@ -2084,14 +2196,14 @@
2084
2196
  }
2085
2197
  removeBtn.disabled = !state.active || busy;
2086
2198
  removeAllBtn.disabled = !state.active || busy;
2087
- sendBtn.disabled = !activityTab || !state.active || busy || (agentMode && !agentEnabled);
2199
+ sendBtn.disabled = !activityTab || !state.active || busy || (agentMode && (!agentEnabled || activeAgentRunning));
2088
2200
  if (stopBtn) {
2089
2201
  stopBtn.disabled = !activityTab || !agentMode || !activeAgentRunning || state.agentRun.stopping;
2090
2202
  }
2091
2203
  if (agentTemplateBtn) {
2092
2204
  agentTemplateBtn.disabled = !state.active || state.agentTemplateSaving;
2093
2205
  }
2094
- commandInput.disabled = !activityTab || !state.active || (agentMode && !agentEnabled);
2206
+ commandInput.disabled = !activityTab || !state.active || (agentMode && (!agentEnabled || activeAgentRunning));
2095
2207
  if (commandInput) {
2096
2208
  commandInput.placeholder = agentMode
2097
2209
  ? '输入提示词,例如:请帮我分析当前项目结构并给出重构建议'
@@ -3282,6 +3394,7 @@
3282
3394
  state.messageRequestId += 1;
3283
3395
  state.messages = [];
3284
3396
  state.loadingMessages = false;
3397
+ clearAgentRecoveryPoll();
3285
3398
  renderMessages(state.messages);
3286
3399
  syncUi();
3287
3400
  return;
@@ -3314,6 +3427,9 @@
3314
3427
  state.messages = Array.isArray(data && data.messages) ? data.messages : [];
3315
3428
  }
3316
3429
  if (!requestError || targetSession === state.active) {
3430
+ if (targetSession === state.active) {
3431
+ syncAgentRecoveryForSession(targetSession);
3432
+ }
3317
3433
  renderMessages(state.messages);
3318
3434
  syncUi();
3319
3435
  } else {
@@ -3451,6 +3567,7 @@
3451
3567
  state.agentRun.stopping = false;
3452
3568
  state.agentRun.sessionName = '';
3453
3569
  state.agentRun.traceMessageId = '';
3570
+ clearAgentRecoveryPoll();
3454
3571
  }
3455
3572
 
3456
3573
  function appendAssistantMessageLocal(sessionName, result, mode) {
@@ -3524,6 +3641,7 @@
3524
3641
  state.agentRun.stopping = false;
3525
3642
  state.agentRun.controller = new window.AbortController();
3526
3643
  state.agentRun.traceMessageId = traceMessageId;
3644
+ clearAgentRecoveryPoll();
3527
3645
  renderMessages(state.messages, { stickToBottom: true });
3528
3646
  syncUi();
3529
3647
 
@@ -3884,13 +4002,20 @@
3884
4002
  });
3885
4003
  }
3886
4004
  } catch (e) {
3887
- if (state.active === submitSession) {
3888
- state.messages = state.messages.filter(function (message) {
3889
- return !(message && message.id === pendingMessage.id);
3890
- });
3891
- renderMessages(state.messages, { stickToBottom: true });
4005
+ if (mode === 'agent') {
4006
+ const recovered = await recoverAgentRunFromServer(submitSession);
4007
+ if (!recovered && document.visibilityState !== 'hidden') {
4008
+ alert(e.message);
4009
+ }
4010
+ } else {
4011
+ if (state.active === submitSession) {
4012
+ state.messages = state.messages.filter(function (message) {
4013
+ return !(message && message.id === pendingMessage.id);
4014
+ });
4015
+ renderMessages(state.messages, { stickToBottom: true });
4016
+ }
4017
+ alert(e.message);
3892
4018
  }
3893
- alert(e.message);
3894
4019
  } finally {
3895
4020
  state.sending = false;
3896
4021
  syncUi();
@@ -4153,6 +4278,24 @@
4153
4278
  }
4154
4279
  });
4155
4280
 
4281
+ document.addEventListener('visibilitychange', function () {
4282
+ if (document.visibilityState !== 'visible' || !state.active) {
4283
+ return;
4284
+ }
4285
+ recoverAgentRunFromServer(state.active).catch(function () {
4286
+ // 静默恢复失败不打断当前交互
4287
+ });
4288
+ });
4289
+
4290
+ window.addEventListener('focus', function () {
4291
+ if (!state.active) {
4292
+ return;
4293
+ }
4294
+ recoverAgentRunFromServer(state.active).catch(function () {
4295
+ // 静默恢复失败不打断当前交互
4296
+ });
4297
+ });
4298
+
4156
4299
  if (typeof MOBILE_LAYOUT_MEDIA.addEventListener === 'function') {
4157
4300
  MOBILE_LAYOUT_MEDIA.addEventListener('change', onLayoutMediaChange);
4158
4301
  } else if (typeof MOBILE_LAYOUT_MEDIA.addListener === 'function') {
package/lib/web/server.js CHANGED
@@ -384,6 +384,10 @@ function listWebAgentSessions(history, options = {}) {
384
384
  });
385
385
  }
386
386
 
387
+ function createWebSessionMessageId() {
388
+ return `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
389
+ }
390
+
387
391
  function appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, role, content, extra = {}) {
388
392
  const sessionRef = typeof sessionRefOrContainerName === 'string'
389
393
  ? { containerName: sessionRefOrContainerName, agentId: WEB_DEFAULT_AGENT_ID }
@@ -391,13 +395,14 @@ function appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, role,
391
395
  const history = loadWebSessionHistory(webHistoryDir, sessionRef.containerName);
392
396
  const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true });
393
397
  const timestamp = new Date().toISOString();
394
- agentSession.messages.push({
395
- id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
398
+ const message = {
399
+ id: createWebSessionMessageId(),
396
400
  role,
397
401
  content,
398
402
  timestamp,
399
403
  ...extra
400
- });
404
+ };
405
+ agentSession.messages.push(message);
401
406
 
402
407
  if (agentSession.messages.length > WEB_HISTORY_MAX_MESSAGES) {
403
408
  agentSession.messages = agentSession.messages.slice(-WEB_HISTORY_MAX_MESSAGES);
@@ -406,6 +411,57 @@ function appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, role,
406
411
  agentSession.updatedAt = timestamp;
407
412
  history.updatedAt = timestamp;
408
413
  saveWebSessionHistory(webHistoryDir, sessionRef.containerName, history);
414
+ return message;
415
+ }
416
+
417
+ function patchWebSessionMessage(webHistoryDir, sessionRefOrContainerName, messageId, patch) {
418
+ const sessionRef = typeof sessionRefOrContainerName === 'string'
419
+ ? { containerName: sessionRefOrContainerName, agentId: WEB_DEFAULT_AGENT_ID }
420
+ : sessionRefOrContainerName;
421
+ const targetId = String(messageId || '').trim();
422
+ if (!targetId) {
423
+ return null;
424
+ }
425
+ const history = loadWebSessionHistory(webHistoryDir, sessionRef.containerName);
426
+ const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true });
427
+ const nextPatch = patch && typeof patch === 'object' ? patch : {};
428
+ const message = agentSession.messages.find(item => item && String(item.id || '') === targetId);
429
+ if (!message) {
430
+ return null;
431
+ }
432
+ Object.keys(nextPatch).forEach(key => {
433
+ if (key === 'id') {
434
+ return;
435
+ }
436
+ message[key] = nextPatch[key];
437
+ });
438
+ const timestamp = new Date().toISOString();
439
+ agentSession.updatedAt = timestamp;
440
+ history.updatedAt = timestamp;
441
+ saveWebSessionHistory(webHistoryDir, sessionRef.containerName, history);
442
+ return message;
443
+ }
444
+
445
+ function removeWebSessionMessage(webHistoryDir, sessionRefOrContainerName, messageId) {
446
+ const sessionRef = typeof sessionRefOrContainerName === 'string'
447
+ ? { containerName: sessionRefOrContainerName, agentId: WEB_DEFAULT_AGENT_ID }
448
+ : sessionRefOrContainerName;
449
+ const targetId = String(messageId || '').trim();
450
+ if (!targetId) {
451
+ return null;
452
+ }
453
+ const history = loadWebSessionHistory(webHistoryDir, sessionRef.containerName);
454
+ const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true });
455
+ const index = agentSession.messages.findIndex(item => item && String(item.id || '') === targetId);
456
+ if (index === -1) {
457
+ return null;
458
+ }
459
+ const removed = agentSession.messages.splice(index, 1)[0] || null;
460
+ const timestamp = new Date().toISOString();
461
+ agentSession.updatedAt = timestamp;
462
+ history.updatedAt = timestamp;
463
+ saveWebSessionHistory(webHistoryDir, sessionRef.containerName, history);
464
+ return removed;
409
465
  }
410
466
 
411
467
  function setWebSessionAgentPromptCommand(webHistoryDir, containerName, agentPromptCommand) {
@@ -816,6 +872,7 @@ function hasAgentConversationHistory(history) {
816
872
  for (const message of messages) {
817
873
  if (!message || typeof message !== 'object') continue;
818
874
  if (message.mode !== 'agent') continue;
875
+ if (message.pending === true) continue;
819
876
  if (message.streamTrace === true) continue;
820
877
  if (message.role === 'user' || message.role === 'assistant') {
821
878
  return true;
@@ -838,6 +895,7 @@ function buildAgentPromptWithHistory(history, prompt) {
838
895
  .filter(message => (
839
896
  message
840
897
  && message.mode === 'agent'
898
+ && message.pending !== true
841
899
  && message.streamTrace !== true
842
900
  && (message.role === 'user' || message.role === 'assistant')
843
901
  ))
@@ -1547,9 +1605,9 @@ function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, m
1547
1605
  function appendWebAgentTraceMessage(webHistoryDir, sessionRefOrContainerName, content, extra = {}) {
1548
1606
  const text = String(content || '').trim();
1549
1607
  if (!text) {
1550
- return;
1608
+ return null;
1551
1609
  }
1552
- appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, 'assistant', text, {
1610
+ return appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, 'assistant', text, {
1553
1611
  mode: 'agent',
1554
1612
  streamTrace: true,
1555
1613
  ...extra
@@ -3827,10 +3885,26 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3827
3885
  return;
3828
3886
  }
3829
3887
 
3888
+ const userMessage = appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', prompt, {
3889
+ mode: 'agent',
3890
+ pending: true
3891
+ });
3892
+ const traceMessage = appendWebAgentTraceMessage(
3893
+ state.webHistoryDir,
3894
+ sessionRef,
3895
+ '[执行过程]\n等待 Agent 启动…',
3896
+ {
3897
+ traceEvents: [],
3898
+ pending: true
3899
+ }
3900
+ );
3901
+
3830
3902
  let prepared = null;
3831
3903
  try {
3832
3904
  prepared = await prepareWebAgentExecution(ctx, state, sessionRef, prompt);
3833
3905
  } catch (e) {
3906
+ removeWebSessionMessage(state.webHistoryDir, sessionRef, traceMessage && traceMessage.id);
3907
+ removeWebSessionMessage(state.webHistoryDir, sessionRef, userMessage && userMessage.id);
3834
3908
  sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
3835
3909
  return;
3836
3910
  }
@@ -3838,8 +3912,9 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3838
3912
  const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
3839
3913
  const traceLines = ['[执行过程]'];
3840
3914
  const traceEvents = [];
3841
- appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', prompt, {
3842
- mode: 'agent',
3915
+ let streamingReplyMessageId = '';
3916
+ patchWebSessionMessage(state.webHistoryDir, sessionRef, userMessage && userMessage.id, {
3917
+ pending: true,
3843
3918
  contextMode
3844
3919
  });
3845
3920
 
@@ -3863,6 +3938,14 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3863
3938
  if (resumeAttempted) {
3864
3939
  traceLines.push(resumeSucceeded ? '会话恢复成功' : '会话恢复失败,已回退到历史注入');
3865
3940
  }
3941
+ patchWebSessionMessage(state.webHistoryDir, sessionRef, traceMessage && traceMessage.id, {
3942
+ content: traceLines.join('\n'),
3943
+ traceEvents: traceEvents.slice(),
3944
+ contextMode,
3945
+ resumeAttempted,
3946
+ resumeSucceeded,
3947
+ pending: true
3948
+ });
3866
3949
 
3867
3950
  try {
3868
3951
  const result = await execAgentInWebContainerStream(ctx, state, sessionRef, command, {
@@ -3873,18 +3956,57 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3873
3956
  if (event.traceEvent && typeof event.traceEvent === 'object') {
3874
3957
  traceEvents.push(event.traceEvent);
3875
3958
  }
3959
+ patchWebSessionMessage(state.webHistoryDir, sessionRef, traceMessage && traceMessage.id, {
3960
+ content: traceLines.join('\n'),
3961
+ traceEvents: traceEvents.slice(),
3962
+ pending: true
3963
+ });
3964
+ }
3965
+ if (event && event.type === 'content_delta' && typeof event.content === 'string') {
3966
+ if (!streamingReplyMessageId) {
3967
+ const streamingReplyMessage = appendWebSessionMessage(
3968
+ state.webHistoryDir,
3969
+ sessionRef,
3970
+ 'assistant',
3971
+ event.content,
3972
+ {
3973
+ mode: 'agent',
3974
+ streamingReply: true,
3975
+ pending: true
3976
+ }
3977
+ );
3978
+ streamingReplyMessageId = streamingReplyMessage && streamingReplyMessage.id
3979
+ ? streamingReplyMessage.id
3980
+ : '';
3981
+ } else {
3982
+ patchWebSessionMessage(state.webHistoryDir, sessionRef, streamingReplyMessageId, {
3983
+ content: event.content,
3984
+ pending: true
3985
+ });
3986
+ }
3876
3987
  }
3877
3988
  sendNdjson(res, event);
3878
3989
  }
3879
3990
  });
3880
3991
  traceLines.push(result.interrupted === true ? '[任务] 已停止' : '[任务] 已完成');
3881
- appendWebAgentTraceMessage(state.webHistoryDir, sessionRef, traceLines.join('\n'), {
3992
+ patchWebSessionMessage(state.webHistoryDir, sessionRef, userMessage && userMessage.id, {
3993
+ pending: false,
3994
+ contextMode,
3995
+ resumeAttempted,
3996
+ resumeSucceeded
3997
+ });
3998
+ patchWebSessionMessage(state.webHistoryDir, sessionRef, traceMessage && traceMessage.id, {
3999
+ content: traceLines.join('\n'),
3882
4000
  traceEvents,
3883
4001
  contextMode,
3884
4002
  resumeAttempted,
3885
4003
  resumeSucceeded,
3886
- interrupted: result.interrupted === true
4004
+ interrupted: result.interrupted === true,
4005
+ pending: false
3887
4006
  });
4007
+ if (streamingReplyMessageId) {
4008
+ removeWebSessionMessage(state.webHistoryDir, sessionRef, streamingReplyMessageId);
4009
+ }
3888
4010
  finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, {
3889
4011
  contextMode,
3890
4012
  resumeAttempted,
@@ -3902,13 +4024,24 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3902
4024
  });
3903
4025
  } catch (e) {
3904
4026
  traceLines.push(`[错误] ${e && e.message ? e.message : 'Agent 执行失败'}`);
3905
- appendWebAgentTraceMessage(state.webHistoryDir, sessionRef, traceLines.join('\n'), {
4027
+ patchWebSessionMessage(state.webHistoryDir, sessionRef, userMessage && userMessage.id, {
4028
+ pending: false,
4029
+ contextMode,
4030
+ resumeAttempted,
4031
+ resumeSucceeded
4032
+ });
4033
+ patchWebSessionMessage(state.webHistoryDir, sessionRef, traceMessage && traceMessage.id, {
4034
+ content: traceLines.join('\n'),
3906
4035
  traceEvents,
3907
4036
  contextMode,
3908
4037
  resumeAttempted,
3909
4038
  resumeSucceeded,
3910
- interrupted: true
4039
+ interrupted: true,
4040
+ pending: false
3911
4041
  });
4042
+ if (streamingReplyMessageId) {
4043
+ removeWebSessionMessage(state.webHistoryDir, sessionRef, streamingReplyMessageId);
4044
+ }
3912
4045
  sendNdjson(res, {
3913
4046
  type: 'error',
3914
4047
  error: e && e.message ? e.message : 'Agent 执行失败'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.9.0",
3
+ "version": "5.9.1",
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",