ec-web 0.0.12 → 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.
@@ -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,
@@ -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': '最后一轮喂给模型的完整上下文大小',
@@ -1243,7 +1249,13 @@ const translations = {
1243
1249
  'sessions.block.rejection.source.policy': 'EvolCore policy auto-blocked',
1244
1250
  'sessions.block.rejection.source.approval': 'Approval channel rejected',
1245
1251
  'sessions.block.rejection.notExecuted': 'Command was not executed',
1246
- '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',
1247
1259
  'sessions.content.expand': 'Expand content',
1248
1260
  'sessions.content.collapse': 'Collapse content',
1249
1261
  'sessions.contextTitle': 'Full context size sent to the model in the last turn',
@@ -2298,6 +2310,41 @@ function renderLogFields(fields) {
2298
2310
  ).join('')}</span>`;
2299
2311
  }
2300
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
+
2301
2348
  function bindLogControls() {
2302
2349
  const source = $('#log-source');
2303
2350
  const level = $('#log-level');
@@ -2349,6 +2396,7 @@ function renderLogs(data) {
2349
2396
  const filterResult = $('#log-filter-result');
2350
2397
  const stream = $('#log-stream');
2351
2398
  if (!source || !stream) return;
2399
+ const scrollAnchor = captureLogScrollAnchor(stream);
2352
2400
  bindLogControls();
2353
2401
  level.value = logSel.level;
2354
2402
  search.value = logSel.search;
@@ -2431,7 +2479,7 @@ function renderLogs(data) {
2431
2479
  const event = line.event ? `<span class="log-event">${esc(line.event)}</span>` : '';
2432
2480
  const actor = line.actor ? `<span class="log-actor" title="${esc(line.actor)}">${esc(shortAid(line.actor))}</span>` : '';
2433
2481
  const message = line.message || (!line.fields?.length ? line.raw : '');
2434
- return `<div class="log-entry log-entry-${lineLevel}">` +
2482
+ return `<div class="log-entry log-entry-${lineLevel}" data-log-key="${esc(logEntryKey(line))}">` +
2435
2483
  '<div class="log-line">' +
2436
2484
  `<time class="log-time" title="${esc(line.timestamp || '')}">${esc(formatLogTime(line.timestamp))}</time>` +
2437
2485
  `<span class="log-level log-level-${lineLevel}">[${esc(levelLabel)}]</span>` +
@@ -2441,6 +2489,7 @@ function renderLogs(data) {
2441
2489
  '</div>';
2442
2490
  }).join('');
2443
2491
  if (logSel.follow) stream.scrollTop = stream.scrollHeight;
2492
+ else restoreLogScrollAnchor(stream, scrollAnchor);
2444
2493
  }
2445
2494
 
2446
2495
  // ── 工具 ──
@@ -3706,12 +3755,30 @@ function renderBlocks(blocks, options = {}) {
3706
3755
  ? `<button class="ctrl-btn result-load-full" type="button" data-result-key="${esc(b.resultKey)}">${t('sessions.loadFullResult') || '加载完整结果'}</button>`
3707
3756
  : '';
3708
3757
  const rejectionMeta = rejection
3709
- ? '<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">' +
3710
3775
  `<span class="result-rejection-badge">⚠ ${t('sessions.block.rejection.title')}</span>` +
3711
3776
  `<span>${rejection.source === 'evolcore-policy' ? t('sessions.block.rejection.source.policy') : t('sessions.block.rejection.source.approval')}</span>` +
3712
3777
  `<span>${t('sessions.block.rejection.notExecuted')}</span>` +
3713
- `<span class="result-rejection-reason">${t('sessions.block.rejection.reason')}</span>` +
3714
- '</div>'
3778
+ `<span>${t(`sessions.block.rejection.evidence.${evidenceKey}`)}</span>` +
3779
+ backendReason + auditText +
3780
+ '</div>';
3781
+ })()
3715
3782
  : '';
3716
3783
  const resultLabel = rejection
3717
3784
  ? t('sessions.block.rejection.title')
@@ -48,7 +48,7 @@
48
48
 
49
49
  </nav>
50
50
  <span style="flex:1"></span>
51
- 2026-08-21 14:14
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.12",
3
+ "version": "0.0.14",
4
4
  "description": "Web console and monitoring dashboard for EvolCore",
5
5
  "type": "module",
6
6
  "bin": {