ec-web 0.0.11 → 0.0.14

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.
@@ -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,16 +430,49 @@ function toolOutputText(output) {
430
430
  return String(output);
431
431
  }
432
432
  }
433
- function classifyCodexToolRejection(text) {
433
+ function firstString(...values) {
434
+ return values.find((value) => typeof value === 'string' && value.trim().length > 0)?.trim();
435
+ }
436
+ function approvalAuditFromPayload(payload) {
437
+ const candidates = [
438
+ payload.approval,
439
+ payload.approvalAudit,
440
+ payload.approval_audit,
441
+ payload.metadata,
442
+ payload.meta,
443
+ payload,
444
+ ].filter((value) => !!value && typeof value === 'object' && !Array.isArray(value));
445
+ const value = (key) => candidates.map(candidate => candidate[key]).find(candidate => candidate !== undefined);
446
+ const requestId = firstString(value('requestId'), value('request_id'), value('approvalRequestId'), value('approval_request_id'));
447
+ const method = firstString(value('method'), value('approvalMethod'), value('approval_method'));
448
+ const decision = firstString(value('decision'), value('approvalDecision'), value('approval_decision'));
449
+ const decisionSource = firstString(value('decisionSource'), value('decision_source'), value('approver'), value('approverType'));
450
+ const policyCode = firstString(value('policyCode'), value('policy_code'));
451
+ return {
452
+ available: !!(requestId || method || decision || decisionSource || policyCode),
453
+ ...(requestId ? { requestId } : {}),
454
+ ...(method ? { method } : {}),
455
+ ...(decision ? { decision } : {}),
456
+ ...(decisionSource ? { decisionSource } : {}),
457
+ ...(policyCode ? { policyCode } : {}),
458
+ };
459
+ }
460
+ function classifyCodexToolRejection(text, payload) {
434
461
  const normalized = text.replace(/\\(["'])/g, '$1');
435
462
  if (!/Rejected\(\s*["']rejected by user["']\s*\)/i.test(normalized))
436
463
  return undefined;
437
- const isManagedEcSend = /(?:^|[/'\s])ec\s+(?:msg|group)\s+send\b/i.test(text);
464
+ const audit = approvalAuditFromPayload(payload);
465
+ const source = audit.policyCode || audit.decisionSource?.toLowerCase().includes('policy')
466
+ ? 'evolcore-policy'
467
+ : 'codex-approval';
438
468
  return {
439
469
  kind: 'approval_rejected',
440
- source: isManagedEcSend ? 'evolcore-policy' : 'codex-approval',
470
+ source,
441
471
  reason: 'rejected_by_user',
442
472
  executed: false,
473
+ evidence: audit.available ? 'structured_metadata' : 'backend_text',
474
+ backendReason: 'Rejected("rejected by user")',
475
+ audit,
443
476
  };
444
477
  }
445
478
  export function parseCodexToolEvent(event) {
@@ -470,7 +503,7 @@ export function parseCodexToolEvent(event) {
470
503
  }
471
504
  if (payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output') {
472
505
  const text = toolOutputText(payload.output);
473
- const rejection = classifyCodexToolRejection(text);
506
+ const rejection = classifyCodexToolRejection(text, payload);
474
507
  return {
475
508
  kind: 'result',
476
509
  callId,
@@ -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': '权限',
@@ -346,7 +346,13 @@ const translations = {
346
346
  'sessions.block.rejection.source.policy': 'EvolCore 策略自动拦截',
347
347
  'sessions.block.rejection.source.approval': '审批通道拒绝',
348
348
  'sessions.block.rejection.notExecuted': '命令未执行',
349
- 'sessions.block.rejection.reason': '底层返回:Rejected("rejected by user")',
349
+ 'sessions.block.rejection.evidence.backend': '仅有底层文本证据,未提供审批审计',
350
+ 'sessions.block.rejection.evidence.structured': '已提供结构化审批审计',
351
+ 'sessions.block.rejection.audit.requestId': 'request_id',
352
+ 'sessions.block.rejection.audit.method': 'method',
353
+ 'sessions.block.rejection.audit.decision': 'decision',
354
+ 'sessions.block.rejection.audit.source': 'decision_source',
355
+ 'sessions.block.rejection.audit.policy': 'policy_code',
350
356
  'sessions.content.expand': '展开内容',
351
357
  'sessions.content.collapse': '收起内容',
352
358
  'sessions.contextTitle': '最后一轮喂给模型的完整上下文大小',
@@ -633,6 +639,7 @@ const translations = {
633
639
  'triggers.error.promptRequired': 'Prompt 不能为空',
634
640
  'triggers.error.channelRequired': '目标渠道不能为空',
635
641
  'triggers.error.channelIdRequired': '渠道 ID 不能为空',
642
+ 'triggers.error.chatTypeRequired': 'AUN 目标必须选择私聊或群聊',
636
643
  'triggers.error.scriptPathRequired': '脚本路径不能为空',
637
644
  'triggers.error.scriptRuntimeRequired': '脚本运行时不能为空',
638
645
  'triggers.error.threadRequired': '指定话题会话时必须填写话题会话 ID',
@@ -681,6 +688,9 @@ const translations = {
681
688
  'triggers.delivery.off': '禁用',
682
689
  'triggers.field.targetChannel': '渠道实例',
683
690
  'triggers.field.channelId': '渠道 ID',
691
+ 'triggers.field.chatType': '会话类型',
692
+ 'triggers.chatType.private': '私聊',
693
+ 'triggers.chatType.group': '群聊',
684
694
  'triggers.field.sessionStrategy': '会话选择',
685
695
  'triggers.field.threadId': '话题会话 ID',
686
696
  'triggers.field.maxRuns': '最大执行次数',
@@ -1171,7 +1181,7 @@ const translations = {
1171
1181
  'messages.msgType.command': 'Command',
1172
1182
  'messages.groupCount': '{count} groups',
1173
1183
  'messages.privateCount': '{count} private',
1174
- 'messages.session.field.sessionId': 'Session ID',
1184
+ 'messages.session.field.sessionId': 'EvolCore Session ID',
1175
1185
  'messages.session.field.baseagent': 'Baseagent',
1176
1186
  'messages.session.field.chatMode': 'Mode',
1177
1187
  'messages.session.field.permissionMode': 'Perm',
@@ -1239,7 +1249,13 @@ const translations = {
1239
1249
  'sessions.block.rejection.source.policy': 'EvolCore policy auto-blocked',
1240
1250
  'sessions.block.rejection.source.approval': 'Approval channel rejected',
1241
1251
  'sessions.block.rejection.notExecuted': 'Command was not executed',
1242
- 'sessions.block.rejection.reason': 'Backend response: Rejected("rejected by user")',
1252
+ 'sessions.block.rejection.evidence.backend': 'Backend text only; approval audit unavailable',
1253
+ 'sessions.block.rejection.evidence.structured': 'Structured approval audit available',
1254
+ 'sessions.block.rejection.audit.requestId': 'request_id',
1255
+ 'sessions.block.rejection.audit.method': 'method',
1256
+ 'sessions.block.rejection.audit.decision': 'decision',
1257
+ 'sessions.block.rejection.audit.source': 'decision_source',
1258
+ 'sessions.block.rejection.audit.policy': 'policy_code',
1243
1259
  'sessions.content.expand': 'Expand content',
1244
1260
  'sessions.content.collapse': 'Collapse content',
1245
1261
  'sessions.contextTitle': 'Full context size sent to the model in the last turn',
@@ -1526,6 +1542,7 @@ const translations = {
1526
1542
  'triggers.error.promptRequired': 'Prompt is required',
1527
1543
  'triggers.error.channelRequired': 'Target channel is required',
1528
1544
  'triggers.error.channelIdRequired': 'Channel ID is required',
1545
+ 'triggers.error.chatTypeRequired': 'AUN targets require Private or Group chat type',
1529
1546
  'triggers.error.scriptPathRequired': 'Script path is required',
1530
1547
  'triggers.error.scriptRuntimeRequired': 'Script runtime is required',
1531
1548
  'triggers.error.threadRequired': 'Topic session ID is required',
@@ -1574,6 +1591,9 @@ const translations = {
1574
1591
  'triggers.delivery.off': 'Disabled',
1575
1592
  'triggers.field.targetChannel': 'Channel instance',
1576
1593
  'triggers.field.channelId': 'Channel ID',
1594
+ 'triggers.field.chatType': 'Chat Type',
1595
+ 'triggers.chatType.private': 'Private',
1596
+ 'triggers.chatType.group': 'Group',
1577
1597
  'triggers.field.sessionStrategy': 'Session Selection',
1578
1598
  'triggers.field.threadId': 'Topic Session ID',
1579
1599
  'triggers.field.maxRuns': 'Max Runs',
@@ -2290,6 +2310,41 @@ function renderLogFields(fields) {
2290
2310
  ).join('')}</span>`;
2291
2311
  }
2292
2312
 
2313
+ function logEntryKey(line) {
2314
+ const text = `${line.file || ''}|${line.timestamp || ''}|${line.raw || ''}`;
2315
+ let hash = 2166136261;
2316
+ for (let index = 0; index < text.length; index++) {
2317
+ hash = Math.imul(hash ^ text.charCodeAt(index), 16777619);
2318
+ }
2319
+ return `${(hash >>> 0).toString(36)}-${text.length.toString(36)}`;
2320
+ }
2321
+
2322
+ function captureLogScrollAnchor(stream) {
2323
+ if (logSel.follow || !stream) return null;
2324
+ const scrollTop = stream.scrollTop;
2325
+ const streamTop = stream.getBoundingClientRect().top;
2326
+ const entry = [...stream.querySelectorAll('.log-entry')]
2327
+ .find(item => item.getBoundingClientRect().bottom > streamTop);
2328
+ return {
2329
+ key: entry?.dataset.logKey || null,
2330
+ offset: entry ? entry.getBoundingClientRect().top - streamTop : 0,
2331
+ scrollTop,
2332
+ };
2333
+ }
2334
+
2335
+ function restoreLogScrollAnchor(stream, anchor) {
2336
+ if (!stream || !anchor || logSel.follow) return;
2337
+ const entry = anchor.key
2338
+ ? [...stream.querySelectorAll('.log-entry')].find(item => item.dataset.logKey === anchor.key)
2339
+ : null;
2340
+ if (!entry) {
2341
+ stream.scrollTop = anchor.scrollTop;
2342
+ return;
2343
+ }
2344
+ const currentOffset = entry.getBoundingClientRect().top - stream.getBoundingClientRect().top;
2345
+ stream.scrollTop += currentOffset - anchor.offset;
2346
+ }
2347
+
2293
2348
  function bindLogControls() {
2294
2349
  const source = $('#log-source');
2295
2350
  const level = $('#log-level');
@@ -2341,6 +2396,7 @@ function renderLogs(data) {
2341
2396
  const filterResult = $('#log-filter-result');
2342
2397
  const stream = $('#log-stream');
2343
2398
  if (!source || !stream) return;
2399
+ const scrollAnchor = captureLogScrollAnchor(stream);
2344
2400
  bindLogControls();
2345
2401
  level.value = logSel.level;
2346
2402
  search.value = logSel.search;
@@ -2423,7 +2479,7 @@ function renderLogs(data) {
2423
2479
  const event = line.event ? `<span class="log-event">${esc(line.event)}</span>` : '';
2424
2480
  const actor = line.actor ? `<span class="log-actor" title="${esc(line.actor)}">${esc(shortAid(line.actor))}</span>` : '';
2425
2481
  const message = line.message || (!line.fields?.length ? line.raw : '');
2426
- return `<div class="log-entry log-entry-${lineLevel}">` +
2482
+ return `<div class="log-entry log-entry-${lineLevel}" data-log-key="${esc(logEntryKey(line))}">` +
2427
2483
  '<div class="log-line">' +
2428
2484
  `<time class="log-time" title="${esc(line.timestamp || '')}">${esc(formatLogTime(line.timestamp))}</time>` +
2429
2485
  `<span class="log-level log-level-${lineLevel}">[${esc(levelLabel)}]</span>` +
@@ -2433,6 +2489,7 @@ function renderLogs(data) {
2433
2489
  '</div>';
2434
2490
  }).join('');
2435
2491
  if (logSel.follow) stream.scrollTop = stream.scrollHeight;
2492
+ else restoreLogScrollAnchor(stream, scrollAnchor);
2436
2493
  }
2437
2494
 
2438
2495
  // ── 工具 ──
@@ -3244,7 +3301,7 @@ function renderMsg(data) {
3244
3301
  for (const m of messages) {
3245
3302
  const cls = m.dir === 'in' ? 'in' : 'out';
3246
3303
  const arrow = m.dir === 'in' ? '↓' : '↑';
3247
- const from = shortId(m.from), to = shortId(m.to);
3304
+ const from = shortId(m.displayFrom || m.from), to = shortId(m.displayTo || m.to);
3248
3305
  const tags = [];
3249
3306
  if (m.channelType) tags.push(m.channelType);
3250
3307
  if (m.chatType === 'group') tags.push(t('messages.tag.group'));
@@ -3698,12 +3755,30 @@ function renderBlocks(blocks, options = {}) {
3698
3755
  ? `<button class="ctrl-btn result-load-full" type="button" data-result-key="${esc(b.resultKey)}">${t('sessions.loadFullResult') || '加载完整结果'}</button>`
3699
3756
  : '';
3700
3757
  const rejectionMeta = rejection
3701
- ? '<div class="result-rejection-meta">' +
3758
+ ? (() => {
3759
+ const audit = rejection.audit && typeof rejection.audit === 'object' ? rejection.audit : {};
3760
+ const auditFields = [
3761
+ ['requestId', 'requestId'],
3762
+ ['method', 'method'],
3763
+ ['decision', 'decision'],
3764
+ ['decisionSource', 'source'],
3765
+ ['policyCode', 'policy'],
3766
+ ].filter(([key]) => typeof audit[key] === 'string' && audit[key]);
3767
+ const auditText = auditFields
3768
+ .map(([key, labelKey]) => `<span class="result-rejection-audit"><code>${t(`sessions.block.rejection.audit.${labelKey}`)}</code>=${esc(audit[key])}</span>`)
3769
+ .join('');
3770
+ const evidenceKey = rejection.evidence === 'structured_metadata' ? 'structured' : 'backend';
3771
+ const backendReason = typeof rejection.backendReason === 'string' && rejection.backendReason
3772
+ ? `<span class="result-rejection-reason">${esc(rejection.backendReason)}</span>`
3773
+ : '';
3774
+ return '<div class="result-rejection-meta">' +
3702
3775
  `<span class="result-rejection-badge">⚠ ${t('sessions.block.rejection.title')}</span>` +
3703
3776
  `<span>${rejection.source === 'evolcore-policy' ? t('sessions.block.rejection.source.policy') : t('sessions.block.rejection.source.approval')}</span>` +
3704
3777
  `<span>${t('sessions.block.rejection.notExecuted')}</span>` +
3705
- `<span class="result-rejection-reason">${t('sessions.block.rejection.reason')}</span>` +
3706
- '</div>'
3778
+ `<span>${t(`sessions.block.rejection.evidence.${evidenceKey}`)}</span>` +
3779
+ backendReason + auditText +
3780
+ '</div>';
3781
+ })()
3707
3782
  : '';
3708
3783
  const resultLabel = rejection
3709
3784
  ? t('sessions.block.rejection.title')
@@ -5383,6 +5458,10 @@ const TRIGGER_TARGET_SESSIONS = [
5383
5458
  ['main', () => t('triggers.session.main')],
5384
5459
  ['thread', () => t('triggers.session.thread')],
5385
5460
  ];
5461
+ const TRIGGER_TARGET_CHAT_TYPES = [
5462
+ ['private', () => t('triggers.chatType.private')],
5463
+ ['group', () => t('triggers.chatType.group')],
5464
+ ];
5386
5465
  const TRIGGER_CONCURRENCY_OPTIONS = [
5387
5466
  ['forbid', () => t('triggers.concurrency.forbid')],
5388
5467
  ['replace', () => t('triggers.concurrency.replace')],
@@ -5895,6 +5974,7 @@ function triggerPrimaryTarget(definition) {
5895
5974
  channelId: definition.origin.channelId || definition.origin.peerId || '',
5896
5975
  session: definition.origin.session || 'main',
5897
5976
  threadId: definition.origin.threadId || '',
5977
+ chatType: definition.origin.delivery?.chatType || '',
5898
5978
  };
5899
5979
  }
5900
5980
  return null;
@@ -5971,6 +6051,7 @@ function triggerOriginalEditState(t) {
5971
6051
  templateFailure: String(feedback.onFailure?.template ?? ''),
5972
6052
  targetChannel: String(target.channelKey ?? t?.targetChannel ?? ''),
5973
6053
  targetChannelId: String(target.channelId ?? t?.targetChannelId ?? ''),
6054
+ targetChatType: String(target.chatType ?? ''),
5974
6055
  targetSessionStrategy: triggerUiSessionStrategy(target.session ?? t?.targetSessionStrategy),
5975
6056
  targetThreadId: String(target.threadId ?? t?.targetThreadId ?? ''),
5976
6057
  maxRuns: limits.maxRuns == null ? '' : String(limits.maxRuns),
@@ -6860,6 +6941,7 @@ function readTriggerEditValues(root) {
6860
6941
  templateFailure: root.querySelector('#trig-edit-template-failure')?.value || '',
6861
6942
  targetChannel: (root.querySelector('#trig-edit-channel')?.value || '').trim(),
6862
6943
  targetChannelId: (root.querySelector('#trig-edit-channel-id')?.value || '').trim(),
6944
+ targetChatType: root.querySelector('#trig-edit-chat-type')?.value || '',
6863
6945
  targetSessionStrategy: root.querySelector('#trig-edit-session-strategy')?.value || 'main',
6864
6946
  targetThreadId: (root.querySelector('#trig-edit-thread')?.value || '').trim(),
6865
6947
  maxRuns: (root.querySelector('#trig-edit-max-runs')?.value || '').trim(),
@@ -6929,6 +7011,7 @@ function triggerEditScheduleChanged(original, values) {
6929
7011
  function triggerEditTargetChanged(original, values) {
6930
7012
  return values.targetChannel !== original.targetChannel
6931
7013
  || values.targetChannelId !== original.targetChannelId
7014
+ || values.targetChatType !== original.targetChatType
6932
7015
  || values.targetSessionStrategy !== original.targetSessionStrategy
6933
7016
  || values.targetThreadId !== original.targetThreadId;
6934
7017
  }
@@ -6995,6 +7078,8 @@ function triggerDefinitionFromEdit(original, values, eventFilter, feedbackStrate
6995
7078
  const target = triggerEditableObject(feedback.target);
6996
7079
  target.channelKey = values.targetChannel;
6997
7080
  target.channelId = values.targetChannelId;
7081
+ if (values.targetChatType) target.chatType = values.targetChatType;
7082
+ else delete target.chatType;
6998
7083
  target.session = values.targetSessionStrategy;
6999
7084
  if (values.targetSessionStrategy === 'thread') target.threadId = values.targetThreadId;
7000
7085
  else delete target.threadId;
@@ -7068,6 +7153,9 @@ function collectTriggerEditPatch(ctx) {
7068
7153
  if (feedbackStrategy === 'target') {
7069
7154
  if (!values.targetChannel) return { error: t('triggers.error.channelRequired') };
7070
7155
  if (!values.targetChannelId) return { error: t('triggers.error.channelIdRequired') };
7156
+ if ((values.targetChannel === 'aun' || values.targetChannel.startsWith('aun#')) && !values.targetChatType) {
7157
+ return { error: t('triggers.error.chatTypeRequired') };
7158
+ }
7071
7159
  if (values.targetSessionStrategy === 'thread' && !values.targetThreadId) return { error: t('triggers.error.threadRequired') };
7072
7160
  }
7073
7161
  if (values.maxRuns) {
@@ -7213,6 +7301,7 @@ function openTriggerEditDrawer(id, agent) {
7213
7301
  `<div id="trig-edit-target-fields" class="ag-edit-grid trig-edit-grid">` +
7214
7302
  `<label class="ag-edit-field"><span>${t('triggers.field.targetChannel')}</span><select id="trig-edit-channel">${channelOptions}</select></label>` +
7215
7303
  `<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>` +
7304
+ `<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>` +
7216
7305
  `<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>` +
7217
7306
  `<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>` +
7218
7307
  `</div>` +
@@ -48,7 +48,7 @@
48
48
 
49
49
  </nav>
50
50
  <span style="flex:1"></span>
51
- 2026-08-19 05:07
51
+ 2026-08-25 01:10
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>
@@ -199,7 +199,7 @@ body {
199
199
  .log-live i { width: 7px; height: 7px; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 15%, transparent); }
200
200
  .log-meta { display: flex; justify-content: space-between; gap: 12px; padding: 7px 16px; color: var(--dim); font-size: 11px; border-top: 1px solid var(--border); background: var(--bg2); flex-shrink: 0; font-variant-numeric: tabular-nums; }
201
201
  .log-meta-status { display: inline-flex; align-items: center; gap: 16px; margin-left: auto; }
202
- .log-stream { flex: 1; min-height: 0; overflow: auto; padding: 6px 0 16px; background: var(--bg); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; line-height: 1.5; }
202
+ .log-stream { flex: 1; min-height: 0; overflow: auto; overflow-anchor: none; padding: 6px 0 16px; background: var(--bg); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; line-height: 1.5; }
203
203
  .log-entry { border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent); }
204
204
  .log-line { display: grid; grid-template-columns: 112px 56px 150px minmax(320px, 1fr); gap: 10px; padding: 4px 16px; align-items: start; }
205
205
  .log-line:hover { background: var(--bg2); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ec-web",
3
- "version": "0.0.11",
3
+ "version": "0.0.14",
4
4
  "description": "Web console and monitoring dashboard for EvolCore",
5
5
  "type": "module",
6
6
  "bin": {