ec-web 0.0.10 → 0.0.12

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.
@@ -7,7 +7,22 @@
7
7
  * watch 用到的只读查询:aun-aids / aun-aid-stats / status / evolagent.list / ping
8
8
  */
9
9
  import net from 'net';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ const LOCAL_CONTROL_COMMANDS = new Set(['menu.exec', 'monitor-snapshot']);
10
13
  export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
14
+ let request = cmd;
15
+ if (LOCAL_CONTROL_COMMANDS.has(cmd.type) && typeof cmd.controlToken !== 'string') {
16
+ try {
17
+ const tokenPath = process.platform === 'win32'
18
+ ? path.join(process.env.EVOLCORE_HOME || '', 'data', 'instance', 'control.token')
19
+ : path.join(path.dirname(socketPath), 'control.token');
20
+ const controlToken = fs.readFileSync(tokenPath, 'utf8').trim();
21
+ if (controlToken)
22
+ request = { ...cmd, controlToken };
23
+ }
24
+ catch { }
25
+ }
11
26
  return new Promise((resolve) => {
12
27
  const conn = net.connect(socketPath);
13
28
  let buf = '';
@@ -16,7 +31,7 @@ export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
16
31
  resolve(null);
17
32
  }, timeoutMs);
18
33
  conn.on('connect', () => {
19
- conn.write(JSON.stringify(cmd) + '\n');
34
+ conn.write(JSON.stringify(request) + '\n');
20
35
  });
21
36
  conn.on('data', (data) => {
22
37
  buf += data.toString();
@@ -66,16 +66,30 @@ function getStartTimeLinux(pid) {
66
66
  const starttimeJiffies = parseInt(tail.split(' ')[19], 10);
67
67
  if (isNaN(starttimeJiffies))
68
68
  return null;
69
- let uptimeSec;
69
+ // Use the kernel boot timestamp, the same source as EvolCore's process
70
+ // introspection. Deriving boot time as `Date.now() - /proc/uptime` can
71
+ // drift when the host clock is stepped while ECWeb is starting, producing
72
+ // instance records tens of seconds away from the real PID start time.
73
+ let bootSec = null;
70
74
  try {
71
- uptimeSec = parseFloat(fs.readFileSync('/proc/uptime', 'utf-8').split(' ')[0]);
75
+ const statContent = fs.readFileSync('/proc/stat', 'utf-8');
76
+ const match = statContent.match(/^btime\s+(\d+)$/m);
77
+ if (match)
78
+ bootSec = parseInt(match[1], 10);
72
79
  }
73
- catch {
74
- return null;
80
+ catch { }
81
+ if (!bootSec || !Number.isFinite(bootSec)) {
82
+ let uptimeSec;
83
+ try {
84
+ uptimeSec = parseFloat(fs.readFileSync('/proc/uptime', 'utf-8').split(' ')[0]);
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ bootSec = (Date.now() - uptimeSec * 1000) / 1000;
75
90
  }
76
91
  const clkTck = 100; // 几乎所有 Linux 为 100
77
- const bootMs = Date.now() - uptimeSec * 1000;
78
- return Math.round(bootMs + (starttimeJiffies / clkTck) * 1000);
92
+ return Math.round((bootSec + (starttimeJiffies / clkTck)) * 1000);
79
93
  }
80
94
  function getStartTimeMacOS(pid) {
81
95
  const r = spawnSync('ps', ['-o', 'lstart=', '-p', String(pid)], { encoding: 'utf-8', timeout: 3000 });
@@ -804,20 +804,35 @@ function selectedChats(chats, scope) {
804
804
  return [];
805
805
  return chats.filter(chat => scopeId(chat) === scope);
806
806
  }
807
+ export function messageDisplayRoute(message) {
808
+ if (message.chatType !== 'group') {
809
+ return { displayFrom: message.from, displayTo: message.to };
810
+ }
811
+ return {
812
+ displayFrom: message.dir === 'out' ? (message.selfAID || message.from) : message.from,
813
+ displayTo: message.groupName || message.groupId || message.to,
814
+ };
815
+ }
807
816
  function messagesFor(chats, peer, before, limit) {
808
817
  const selected = peer
809
818
  ? chats.filter(chat => chatKey(chat) === peer)
810
819
  : chats;
811
- const messages = selected.flatMap(chat => chat.messages.map(msg => ({
812
- ...msg,
813
- channel: chat.channel,
814
- channelType: chat.channelType,
815
- selfAID: chat.selfAID,
816
- peerName: chat.peerName,
817
- groupName: chat.groupName,
818
- partition: chat.partition,
819
- threadName: chat.threadName,
820
- })));
820
+ const messages = selected.flatMap(chat => chat.messages.map(msg => {
821
+ const message = {
822
+ ...msg,
823
+ channelType: chat.channelType,
824
+ selfAID: chat.selfAID,
825
+ peerName: chat.peerName,
826
+ groupName: chat.groupName,
827
+ };
828
+ return {
829
+ ...message,
830
+ channel: chat.channel,
831
+ partition: chat.partition,
832
+ threadName: chat.threadName,
833
+ ...messageDisplayRoute(message),
834
+ };
835
+ }));
821
836
  messages.sort((a, b) => (a.ts || 0) - (b.ts || 0));
822
837
  const end = before == null ? messages.length : Math.min(Math.max(before, 0), messages.length);
823
838
  const start = Math.max(0, end - limit);
@@ -892,7 +907,7 @@ async function buildSessionContext(chat) {
892
907
  }
893
908
  catch { }
894
909
  return {
895
- sessionId: active.agentSessionId || active.id || null,
910
+ sessionId: active.id || null,
896
911
  evolcoreSessionId: active.id || null,
897
912
  name: active.name || null,
898
913
  baseagent: active.baseagent || active.agentType || null,
@@ -430,6 +430,18 @@ function toolOutputText(output) {
430
430
  return String(output);
431
431
  }
432
432
  }
433
+ function classifyCodexToolRejection(text) {
434
+ const normalized = text.replace(/\\(["'])/g, '$1');
435
+ if (!/Rejected\(\s*["']rejected by user["']\s*\)/i.test(normalized))
436
+ return undefined;
437
+ const isManagedEcSend = /(?:^|[/'\s])ec\s+(?:msg|group)\s+send\b/i.test(text);
438
+ return {
439
+ kind: 'approval_rejected',
440
+ source: isManagedEcSend ? 'evolcore-policy' : 'codex-approval',
441
+ reason: 'rejected_by_user',
442
+ executed: false,
443
+ };
444
+ }
433
445
  export function parseCodexToolEvent(event) {
434
446
  if (!event || (event.type !== 'event_msg' && event.type !== 'response_item') || !event.payload)
435
447
  return null;
@@ -457,6 +469,8 @@ export function parseCodexToolEvent(event) {
457
469
  };
458
470
  }
459
471
  if (payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output') {
472
+ const text = toolOutputText(payload.output);
473
+ const rejection = classifyCodexToolRejection(text);
460
474
  return {
461
475
  kind: 'result',
462
476
  callId,
@@ -467,8 +481,9 @@ export function parseCodexToolEvent(event) {
467
481
  category: 'tool_result',
468
482
  blocks: [{
469
483
  kind: 'tool_result',
470
- text: toolOutputText(payload.output),
471
- isError: payload.is_error === true || payload.status === 'failed',
484
+ text,
485
+ isError: payload.is_error === true || payload.status === 'failed' || !!rejection,
486
+ ...(rejection ? { rejection } : {}),
472
487
  }],
473
488
  },
474
489
  };
@@ -278,7 +278,7 @@ const translations = {
278
278
  'messages.msgType.command': '命令',
279
279
  'messages.groupCount': '群 {count}',
280
280
  'messages.privateCount': '单 {count}',
281
- 'messages.session.field.sessionId': '会话 ID',
281
+ 'messages.session.field.sessionId': 'EvolCore 会话 ID',
282
282
  'messages.session.field.baseagent': 'Baseagent',
283
283
  'messages.session.field.chatMode': '模式',
284
284
  'messages.session.field.permissionMode': '权限',
@@ -342,6 +342,11 @@ const translations = {
342
342
  'sessions.block.thinking': '思考',
343
343
  'sessions.block.result': '结果',
344
344
  'sessions.block.resultError': '结果',
345
+ 'sessions.block.rejection.title': '已拦截',
346
+ 'sessions.block.rejection.source.policy': 'EvolCore 策略自动拦截',
347
+ 'sessions.block.rejection.source.approval': '审批通道拒绝',
348
+ 'sessions.block.rejection.notExecuted': '命令未执行',
349
+ 'sessions.block.rejection.reason': '底层返回:Rejected("rejected by user")',
345
350
  'sessions.content.expand': '展开内容',
346
351
  'sessions.content.collapse': '收起内容',
347
352
  'sessions.contextTitle': '最后一轮喂给模型的完整上下文大小',
@@ -628,6 +633,7 @@ const translations = {
628
633
  'triggers.error.promptRequired': 'Prompt 不能为空',
629
634
  'triggers.error.channelRequired': '目标渠道不能为空',
630
635
  'triggers.error.channelIdRequired': '渠道 ID 不能为空',
636
+ 'triggers.error.chatTypeRequired': 'AUN 目标必须选择私聊或群聊',
631
637
  'triggers.error.scriptPathRequired': '脚本路径不能为空',
632
638
  'triggers.error.scriptRuntimeRequired': '脚本运行时不能为空',
633
639
  'triggers.error.threadRequired': '指定话题会话时必须填写话题会话 ID',
@@ -676,6 +682,9 @@ const translations = {
676
682
  'triggers.delivery.off': '禁用',
677
683
  'triggers.field.targetChannel': '渠道实例',
678
684
  'triggers.field.channelId': '渠道 ID',
685
+ 'triggers.field.chatType': '会话类型',
686
+ 'triggers.chatType.private': '私聊',
687
+ 'triggers.chatType.group': '群聊',
679
688
  'triggers.field.sessionStrategy': '会话选择',
680
689
  'triggers.field.threadId': '话题会话 ID',
681
690
  'triggers.field.maxRuns': '最大执行次数',
@@ -1166,7 +1175,7 @@ const translations = {
1166
1175
  'messages.msgType.command': 'Command',
1167
1176
  'messages.groupCount': '{count} groups',
1168
1177
  'messages.privateCount': '{count} private',
1169
- 'messages.session.field.sessionId': 'Session ID',
1178
+ 'messages.session.field.sessionId': 'EvolCore Session ID',
1170
1179
  'messages.session.field.baseagent': 'Baseagent',
1171
1180
  'messages.session.field.chatMode': 'Mode',
1172
1181
  'messages.session.field.permissionMode': 'Perm',
@@ -1230,6 +1239,11 @@ const translations = {
1230
1239
  'sessions.block.thinking': 'Thinking',
1231
1240
  'sessions.block.result': 'Result',
1232
1241
  'sessions.block.resultError': 'Result',
1242
+ 'sessions.block.rejection.title': 'Intercepted',
1243
+ 'sessions.block.rejection.source.policy': 'EvolCore policy auto-blocked',
1244
+ 'sessions.block.rejection.source.approval': 'Approval channel rejected',
1245
+ 'sessions.block.rejection.notExecuted': 'Command was not executed',
1246
+ 'sessions.block.rejection.reason': 'Backend response: Rejected("rejected by user")',
1233
1247
  'sessions.content.expand': 'Expand content',
1234
1248
  'sessions.content.collapse': 'Collapse content',
1235
1249
  'sessions.contextTitle': 'Full context size sent to the model in the last turn',
@@ -1516,6 +1530,7 @@ const translations = {
1516
1530
  'triggers.error.promptRequired': 'Prompt is required',
1517
1531
  'triggers.error.channelRequired': 'Target channel is required',
1518
1532
  'triggers.error.channelIdRequired': 'Channel ID is required',
1533
+ 'triggers.error.chatTypeRequired': 'AUN targets require Private or Group chat type',
1519
1534
  'triggers.error.scriptPathRequired': 'Script path is required',
1520
1535
  'triggers.error.scriptRuntimeRequired': 'Script runtime is required',
1521
1536
  'triggers.error.threadRequired': 'Topic session ID is required',
@@ -1564,6 +1579,9 @@ const translations = {
1564
1579
  'triggers.delivery.off': 'Disabled',
1565
1580
  'triggers.field.targetChannel': 'Channel instance',
1566
1581
  'triggers.field.channelId': 'Channel ID',
1582
+ 'triggers.field.chatType': 'Chat Type',
1583
+ 'triggers.chatType.private': 'Private',
1584
+ 'triggers.chatType.group': 'Group',
1567
1585
  'triggers.field.sessionStrategy': 'Session Selection',
1568
1586
  'triggers.field.threadId': 'Topic Session ID',
1569
1587
  'triggers.field.maxRuns': 'Max Runs',
@@ -3234,7 +3252,7 @@ function renderMsg(data) {
3234
3252
  for (const m of messages) {
3235
3253
  const cls = m.dir === 'in' ? 'in' : 'out';
3236
3254
  const arrow = m.dir === 'in' ? '↓' : '↑';
3237
- const from = shortId(m.from), to = shortId(m.to);
3255
+ const from = shortId(m.displayFrom || m.from), to = shortId(m.displayTo || m.to);
3238
3256
  const tags = [];
3239
3257
  if (m.channelType) tags.push(m.channelType);
3240
3258
  if (m.chatType === 'group') tags.push(t('messages.tag.group'));
@@ -3682,11 +3700,23 @@ function renderBlocks(blocks, options = {}) {
3682
3700
  : '';
3683
3701
  out += `<details class="blk blk-tool" open><summary class="tool-head">${icon} <span class="tool-name">${esc(tool)}</span></summary>${toolParams}</details>`;
3684
3702
  } else if (b.kind === 'tool_result') {
3685
- const cls = b.isError ? 'blk-result err' : 'blk-result';
3703
+ const rejection = b.rejection?.kind === 'approval_rejected' ? b.rejection : undefined;
3704
+ const cls = b.isError ? `blk-result err${rejection ? ' rejection' : ''}` : 'blk-result';
3686
3705
  const loadFull = b.truncated && b.resultKey
3687
3706
  ? `<button class="ctrl-btn result-load-full" type="button" data-result-key="${esc(b.resultKey)}">${t('sessions.loadFullResult') || '加载完整结果'}</button>`
3688
3707
  : '';
3689
- out += `<details class="blk ${cls}" open><summary>${b.isError ? '✗ ' + t('sessions.block.resultError') : '↳ ' + t('sessions.block.result')}</summary>` +
3708
+ const rejectionMeta = rejection
3709
+ ? '<div class="result-rejection-meta">' +
3710
+ `<span class="result-rejection-badge">⚠ ${t('sessions.block.rejection.title')}</span>` +
3711
+ `<span>${rejection.source === 'evolcore-policy' ? t('sessions.block.rejection.source.policy') : t('sessions.block.rejection.source.approval')}</span>` +
3712
+ `<span>${t('sessions.block.rejection.notExecuted')}</span>` +
3713
+ `<span class="result-rejection-reason">${t('sessions.block.rejection.reason')}</span>` +
3714
+ '</div>'
3715
+ : '';
3716
+ const resultLabel = rejection
3717
+ ? t('sessions.block.rejection.title')
3718
+ : b.isError ? t('sessions.block.resultError') : t('sessions.block.result');
3719
+ out += `<details class="blk ${cls}" open><summary>${b.isError ? '✗ ' : '↳ '}${resultLabel}</summary>${rejectionMeta}` +
3690
3720
  `<div class="result-content collapse-content"><pre class="result-body collapse-body">${esc(b.text)}</pre>${loadFull}<button class="collapse-arrow-toggle" type="button" hidden aria-expanded="false"><span class="collapse-arrow-icon" aria-hidden="true"></span></button></div></details>`;
3691
3721
  } else if (b.kind === 'subtask') {
3692
3722
  const knownStatuses = ['running', 'completed', 'failed', 'stopped', 'killed'];
@@ -5361,6 +5391,10 @@ const TRIGGER_TARGET_SESSIONS = [
5361
5391
  ['main', () => t('triggers.session.main')],
5362
5392
  ['thread', () => t('triggers.session.thread')],
5363
5393
  ];
5394
+ const TRIGGER_TARGET_CHAT_TYPES = [
5395
+ ['private', () => t('triggers.chatType.private')],
5396
+ ['group', () => t('triggers.chatType.group')],
5397
+ ];
5364
5398
  const TRIGGER_CONCURRENCY_OPTIONS = [
5365
5399
  ['forbid', () => t('triggers.concurrency.forbid')],
5366
5400
  ['replace', () => t('triggers.concurrency.replace')],
@@ -5873,6 +5907,7 @@ function triggerPrimaryTarget(definition) {
5873
5907
  channelId: definition.origin.channelId || definition.origin.peerId || '',
5874
5908
  session: definition.origin.session || 'main',
5875
5909
  threadId: definition.origin.threadId || '',
5910
+ chatType: definition.origin.delivery?.chatType || '',
5876
5911
  };
5877
5912
  }
5878
5913
  return null;
@@ -5949,6 +5984,7 @@ function triggerOriginalEditState(t) {
5949
5984
  templateFailure: String(feedback.onFailure?.template ?? ''),
5950
5985
  targetChannel: String(target.channelKey ?? t?.targetChannel ?? ''),
5951
5986
  targetChannelId: String(target.channelId ?? t?.targetChannelId ?? ''),
5987
+ targetChatType: String(target.chatType ?? ''),
5952
5988
  targetSessionStrategy: triggerUiSessionStrategy(target.session ?? t?.targetSessionStrategy),
5953
5989
  targetThreadId: String(target.threadId ?? t?.targetThreadId ?? ''),
5954
5990
  maxRuns: limits.maxRuns == null ? '' : String(limits.maxRuns),
@@ -6838,6 +6874,7 @@ function readTriggerEditValues(root) {
6838
6874
  templateFailure: root.querySelector('#trig-edit-template-failure')?.value || '',
6839
6875
  targetChannel: (root.querySelector('#trig-edit-channel')?.value || '').trim(),
6840
6876
  targetChannelId: (root.querySelector('#trig-edit-channel-id')?.value || '').trim(),
6877
+ targetChatType: root.querySelector('#trig-edit-chat-type')?.value || '',
6841
6878
  targetSessionStrategy: root.querySelector('#trig-edit-session-strategy')?.value || 'main',
6842
6879
  targetThreadId: (root.querySelector('#trig-edit-thread')?.value || '').trim(),
6843
6880
  maxRuns: (root.querySelector('#trig-edit-max-runs')?.value || '').trim(),
@@ -6907,6 +6944,7 @@ function triggerEditScheduleChanged(original, values) {
6907
6944
  function triggerEditTargetChanged(original, values) {
6908
6945
  return values.targetChannel !== original.targetChannel
6909
6946
  || values.targetChannelId !== original.targetChannelId
6947
+ || values.targetChatType !== original.targetChatType
6910
6948
  || values.targetSessionStrategy !== original.targetSessionStrategy
6911
6949
  || values.targetThreadId !== original.targetThreadId;
6912
6950
  }
@@ -6973,6 +7011,8 @@ function triggerDefinitionFromEdit(original, values, eventFilter, feedbackStrate
6973
7011
  const target = triggerEditableObject(feedback.target);
6974
7012
  target.channelKey = values.targetChannel;
6975
7013
  target.channelId = values.targetChannelId;
7014
+ if (values.targetChatType) target.chatType = values.targetChatType;
7015
+ else delete target.chatType;
6976
7016
  target.session = values.targetSessionStrategy;
6977
7017
  if (values.targetSessionStrategy === 'thread') target.threadId = values.targetThreadId;
6978
7018
  else delete target.threadId;
@@ -7046,6 +7086,9 @@ function collectTriggerEditPatch(ctx) {
7046
7086
  if (feedbackStrategy === 'target') {
7047
7087
  if (!values.targetChannel) return { error: t('triggers.error.channelRequired') };
7048
7088
  if (!values.targetChannelId) return { error: t('triggers.error.channelIdRequired') };
7089
+ if ((values.targetChannel === 'aun' || values.targetChannel.startsWith('aun#')) && !values.targetChatType) {
7090
+ return { error: t('triggers.error.chatTypeRequired') };
7091
+ }
7049
7092
  if (values.targetSessionStrategy === 'thread' && !values.targetThreadId) return { error: t('triggers.error.threadRequired') };
7050
7093
  }
7051
7094
  if (values.maxRuns) {
@@ -7191,6 +7234,7 @@ function openTriggerEditDrawer(id, agent) {
7191
7234
  `<div id="trig-edit-target-fields" class="ag-edit-grid trig-edit-grid">` +
7192
7235
  `<label class="ag-edit-field"><span>${t('triggers.field.targetChannel')}</span><select id="trig-edit-channel">${channelOptions}</select></label>` +
7193
7236
  `<label class="ag-edit-field"><span>${t('triggers.field.channelId')}</span><input id="trig-edit-channel-id" type="text" autocomplete="off" spellcheck="false" value="${esc(original.targetChannelId)}"></label>` +
7237
+ `<label class="ag-edit-field"><span>${t('triggers.field.chatType')}</span><select id="trig-edit-chat-type">${triggerOptionHtml(TRIGGER_TARGET_CHAT_TYPES, original.targetChatType, t('common.none'))}</select></label>` +
7194
7238
  `<label class="ag-edit-field"><span>${t('triggers.field.sessionStrategy')}</span><select id="trig-edit-session-strategy">${triggerOptionHtml(TRIGGER_TARGET_SESSIONS, original.targetSessionStrategy, null)}</select></label>` +
7195
7239
  `<label id="trig-edit-thread-field" class="ag-edit-field ag-edit-field-wide"><span>${t('triggers.field.threadId')}</span><input id="trig-edit-thread" type="text" autocomplete="off" spellcheck="false" value="${esc(original.targetThreadId)}"></label>` +
7196
7240
  `</div>` +
@@ -48,7 +48,7 @@
48
48
 
49
49
  </nav>
50
50
  <span style="flex:1"></span>
51
- 2026-08-11 03:04
51
+ 2026-08-21 14:14
52
52
  <span id="conn-status" class="conn-status" data-i18n="status.connecting">连接中…</span>
53
53
  <button id="lang-btn" class="theme-btn" data-i18n-title="language.buttonTitle" title="切换语言">🌐</button>
54
54
  <button id="theme-btn" class="theme-btn" data-i18n-title="theme.buttonTitle" title="主题">🌗</button>
@@ -540,6 +540,17 @@ tr:hover td { background: var(--bg2); }
540
540
  .blk-result { font-size: 12px; }
541
541
  .blk-result summary { cursor: pointer; color: var(--dim); }
542
542
  .blk-result.err summary { color: var(--red); }
543
+ .result-rejection-meta {
544
+ display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px;
545
+ margin-top: 7px; color: var(--dim); font-size: 11px;
546
+ }
547
+ .result-rejection-badge {
548
+ display: inline-flex; align-items: center; gap: 4px;
549
+ padding: 2px 7px; border: 1px solid color-mix(in srgb, var(--red) 45%, var(--border));
550
+ border-radius: 4px; color: var(--red); background: color-mix(in srgb, var(--red) 8%, transparent);
551
+ font-weight: 600;
552
+ }
553
+ .result-rejection-reason { flex-basis: 100%; overflow-wrap: anywhere; }
543
554
  .collapse-content { position: relative; }
544
555
  .result-content { --collapse-fade-bg: var(--bg); margin-top: 6px; }
545
556
  .collapse-content::after {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ec-web",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Web console and monitoring dashboard for EvolCore",
5
5
  "type": "module",
6
6
  "bin": {