ec-web 0.0.20 → 0.0.22

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.
@@ -77,6 +77,128 @@ function getDb() {
77
77
  }
78
78
  return _db;
79
79
  }
80
+ function messageContentText(content) {
81
+ if (typeof content === 'string')
82
+ return content;
83
+ if (content && typeof content === 'object' && !Array.isArray(content)) {
84
+ return typeof content.text === 'string' ? content.text.trim() : '';
85
+ }
86
+ if (!Array.isArray(content))
87
+ return '';
88
+ return content
89
+ .map((part) => {
90
+ if (typeof part === 'string')
91
+ return part;
92
+ if (!part || typeof part !== 'object')
93
+ return '';
94
+ if (typeof part.text === 'string')
95
+ return part.text;
96
+ return '';
97
+ })
98
+ .filter(Boolean)
99
+ .join('\n')
100
+ .trim();
101
+ }
102
+ function parseRolloutEvents(raw) {
103
+ const events = [];
104
+ for (const line of raw.split('\n')) {
105
+ if (!line.trim())
106
+ continue;
107
+ try {
108
+ events.push(JSON.parse(line));
109
+ }
110
+ catch { }
111
+ }
112
+ return events;
113
+ }
114
+ /**
115
+ * Normalize both Codex CLI events and Codex app-server lifecycle events.
116
+ * The app-server writes the same message twice: response_item.message and
117
+ * event_msg.item_completed. We intentionally consume the latter because it
118
+ * carries the canonical UserMessage/AgentMessage item and stable id.
119
+ */
120
+ export function parseCodexMessageEvent(event) {
121
+ if (!event || !event.payload)
122
+ return null;
123
+ const payload = event.payload;
124
+ if (event.type === 'response_item') {
125
+ if (payload.type !== 'message' || (payload.role !== 'user' && payload.role !== 'assistant'))
126
+ return null;
127
+ const text = messageContentText(payload.content ?? payload.message ?? payload.text);
128
+ if (!text)
129
+ return null;
130
+ return {
131
+ role: payload.role,
132
+ text,
133
+ id: String(payload.id || ''),
134
+ };
135
+ }
136
+ if (event.type !== 'event_msg')
137
+ return null;
138
+ if (payload.type === 'user_message' && payload.message) {
139
+ const text = messageContentText(payload.message);
140
+ if (!text)
141
+ return null;
142
+ return {
143
+ role: 'user',
144
+ text,
145
+ id: String(payload.id || ''),
146
+ };
147
+ }
148
+ if (payload.type === 'agent_message' && payload.message) {
149
+ const text = messageContentText(payload.message);
150
+ if (!text)
151
+ return null;
152
+ return {
153
+ role: 'assistant',
154
+ text,
155
+ id: String(payload.id || ''),
156
+ };
157
+ }
158
+ if (payload.type !== 'item_completed' || !payload.item)
159
+ return null;
160
+ const item = payload.item;
161
+ const role = item.type === 'UserMessage'
162
+ ? 'user'
163
+ : item.type === 'AgentMessage'
164
+ ? 'assistant'
165
+ : null;
166
+ if (!role)
167
+ return null;
168
+ const text = messageContentText(item.content ?? item.message ?? item.text);
169
+ if (!text)
170
+ return null;
171
+ return {
172
+ role,
173
+ text,
174
+ id: String(item.id || payload.id || ''),
175
+ };
176
+ }
177
+ function normalizedCodexMessages(events) {
178
+ // app-server emits response_item.message followed by item_completed for
179
+ // the same logical message; prefer lifecycle items when available. This
180
+ // also prevents injected context response items from being counted as user
181
+ // turns in an app-server rollout.
182
+ const hasCanonicalMessages = events.some(event => event?.type === 'event_msg' && parseCodexMessageEvent(event) !== null);
183
+ const out = [];
184
+ for (const event of events) {
185
+ const message = parseCodexMessageEvent(event);
186
+ if (!message)
187
+ continue;
188
+ if (event.type === 'response_item' && hasCanonicalMessages)
189
+ continue;
190
+ if (event.type === 'response_item' && message.role === 'user' && isSyntheticCodexContext(message.text))
191
+ continue;
192
+ out.push({ event, message });
193
+ }
194
+ return out;
195
+ }
196
+ function isSyntheticCodexContext(text) {
197
+ const trimmed = text.trimStart();
198
+ return trimmed.startsWith('# AGENTS.md instructions')
199
+ || trimmed.startsWith('<system-reminder>')
200
+ || trimmed.startsWith('<environment_context>');
201
+ }
80
202
  // ── 项目列表 ──
81
203
  function listProjects() {
82
204
  const db = getDb();
@@ -110,7 +232,7 @@ function listProjects() {
110
232
  }
111
233
  }
112
234
  // ── 会话列表 ──
113
- const CACHE_VERSION = 2;
235
+ const CACHE_VERSION = 4;
114
236
  const _metaCache = new Map();
115
237
  const _detailCache = new Map();
116
238
  const MAX_DETAIL_CACHE_ENTRIES = 6;
@@ -186,36 +308,29 @@ function extractMetaFromRollout(file, id) {
186
308
  catch { }
187
309
  let title = '', firstUser = '', gitBranch = '', version = '', userMsgs = 0, totalMsgs = 0;
188
310
  let hasRealUserInput = false;
189
- const lines = raw.split('\n');
190
- for (const line of lines) {
191
- if (!line.trim())
192
- continue;
193
- try {
194
- const event = JSON.parse(line);
195
- // 提取 cwd 和 version
196
- if (event.type === 'session_meta' && event.payload) {
197
- if (!gitBranch && event.payload.cwd)
198
- gitBranch = event.payload.cwd;
199
- if (!version && event.payload.cli_version)
200
- version = event.payload.cli_version;
201
- }
202
- // 提取 title(从第一个 user_message)
203
- if (event.type === 'event_msg' && event.payload?.type === 'user_message' && event.payload.message) {
204
- totalMsgs++;
205
- userMsgs++;
206
- if (!isSyntheticCliPrompt(event.payload.message, 'codex'))
207
- hasRealUserInput = true;
208
- if (!firstUser) {
209
- const text = event.payload.message.trim().replace(/\s+/g, ' ');
210
- firstUser = text.substring(0, 120);
211
- }
212
- }
213
- // 统计 assistant 消息
214
- if (event.type === 'event_msg' && event.payload?.type === 'assistant_message') {
215
- totalMsgs++;
311
+ const events = parseRolloutEvents(raw);
312
+ for (const event of events) {
313
+ // 提取 cwd 和 version
314
+ if (event.type === 'session_meta' && event.payload) {
315
+ if (!gitBranch && event.payload.cwd)
316
+ gitBranch = event.payload.cwd;
317
+ if (!version && event.payload.cli_version)
318
+ version = event.payload.cli_version;
319
+ }
320
+ }
321
+ for (const { message } of normalizedCodexMessages(events)) {
322
+ if (message.role === 'user') {
323
+ totalMsgs++;
324
+ userMsgs++;
325
+ if (!isSyntheticCliPrompt(message.text, 'codex'))
326
+ hasRealUserInput = true;
327
+ if (!firstUser) {
328
+ firstUser = message.text.replace(/\s+/g, ' ').substring(0, 120);
216
329
  }
217
330
  }
218
- catch { }
331
+ else {
332
+ totalMsgs++;
333
+ }
219
334
  }
220
335
  let stat;
221
336
  try {
@@ -241,8 +356,15 @@ function extractMetaFromRollout(file, id) {
241
356
  };
242
357
  }
243
358
  function withThreadSource(meta, row) {
359
+ const dbFirstUser = typeof row.first_user_message === 'string'
360
+ ? row.first_user_message.trim().replace(/\s+/g, ' ').substring(0, 120)
361
+ : '';
362
+ const dbTitle = typeof row.title === 'string' ? row.title.trim() : '';
244
363
  return {
245
364
  ...meta,
365
+ title: dbTitle || meta.title,
366
+ firstUser: dbFirstUser || meta.firstUser,
367
+ hasRealUserInput: meta.hasRealUserInput || (!!dbFirstUser && !isSyntheticCliPrompt(dbFirstUser, 'codex')),
246
368
  backendSource: typeof row.source === 'string' ? row.source : '',
247
369
  threadSource: typeof row.thread_source === 'string' ? row.thread_source : '',
248
370
  evolcoreRouted: isEvolcoreRoutedMessage(row.first_user_message),
@@ -459,20 +581,32 @@ function approvalAuditFromPayload(payload) {
459
581
  }
460
582
  function classifyCodexToolRejection(text, payload) {
461
583
  const normalized = text.replace(/\\(["'])/g, '$1');
462
- if (!/Rejected\(\s*["']rejected by user["']\s*\)/i.test(normalized))
584
+ const hookMatch = normalized.match(/(?:Command|Tool call) blocked by PreToolUse hook:\s*(.+?)(?:\.\s+(?:Command|Tool):|$)/is);
585
+ const genericRejected = /Rejected\(\s*["']rejected by user["']\s*\)/i.test(normalized);
586
+ if (!hookMatch && !genericRejected)
463
587
  return undefined;
464
588
  const audit = approvalAuditFromPayload(payload);
465
- const source = audit.policyCode || audit.decisionSource?.toLowerCase().includes('policy')
589
+ const source = hookMatch || audit.policyCode || audit.decisionSource?.toLowerCase().includes('policy')
466
590
  ? 'evolcore-policy'
467
591
  : 'codex-approval';
592
+ const backendReason = hookMatch?.[1]?.trim()
593
+ || 'Rejected("rejected by user")';
594
+ const inferredPolicyCode = audit.policyCode
595
+ || (hookMatch && /工具调用已达到\s*\d+\s*次/.test(backendReason)
596
+ ? 'proactive_tool_report_required'
597
+ : hookMatch && /请先用\s+ec\s+(?:group\s+send|msg\s+send)/.test(backendReason)
598
+ ? 'proactive_first_send_required'
599
+ : undefined);
468
600
  return {
469
- kind: 'approval_rejected',
601
+ kind: source === 'evolcore-policy' ? 'policy_blocked' : 'approval_rejected',
470
602
  source,
471
- reason: 'rejected_by_user',
603
+ reason: source === 'evolcore-policy' ? 'policy_denied' : 'rejected_by_user',
472
604
  executed: false,
473
605
  evidence: audit.available ? 'structured_metadata' : 'backend_text',
474
- backendReason: 'Rejected("rejected by user")',
475
- audit,
606
+ backendReason,
607
+ audit: inferredPolicyCode && !audit.policyCode
608
+ ? { ...audit, policyCode: inferredPolicyCode }
609
+ : audit,
476
610
  };
477
611
  }
478
612
  export function parseCodexToolEvent(event) {
@@ -584,16 +718,9 @@ function readTranscriptFile(threadId, cwd, readStats) {
584
718
  let lastUsageKey = '';
585
719
  const seenToolCalls = new Set();
586
720
  const seenToolResults = new Set();
587
- for (const line of raw.split('\n')) {
588
- if (!line.trim())
589
- continue;
590
- let event;
591
- try {
592
- event = JSON.parse(line);
593
- }
594
- catch {
595
- continue;
596
- }
721
+ const events = parseRolloutEvents(raw);
722
+ const messagesByEvent = new Map(normalizedCodexMessages(events).map(({ event, message }) => [event, message]));
723
+ for (const event of events) {
597
724
  // 提取 session_meta
598
725
  if (event.type === 'session_meta' && event.payload) {
599
726
  if (!version && event.payload.cli_version)
@@ -624,44 +751,37 @@ function readTranscriptFile(threadId, cwd, readStats) {
624
751
  }
625
752
  }
626
753
  }
627
- // 处理消息事件
628
- if (event.type === 'event_msg' && event.payload) {
629
- const payload = event.payload;
630
- if (payload.type === 'user_message' && payload.message) {
631
- totalMsgs++;
632
- userMsgs++;
633
- counts.userInput++;
634
- const text = payload.message.trim();
635
- turns.push({
636
- role: 'user',
637
- ts: event.timestamp ? Date.parse(event.timestamp) : 0,
638
- uuid: payload.id || '',
639
- category: 'user_input',
640
- blocks: [{ kind: 'text', text }],
641
- });
642
- }
643
- else if (payload.type === 'agent_message' && payload.message) {
644
- totalMsgs++;
645
- counts.modelOutput++;
646
- const blocks = [];
647
- // 提取 message
648
- if (payload.message) {
649
- blocks.push({ kind: 'text', text: payload.message });
650
- }
651
- turns.push({
652
- role: 'assistant',
653
- ts: event.timestamp ? Date.parse(event.timestamp) : 0,
654
- uuid: payload.id || '',
655
- category: 'model_output',
656
- blocks,
657
- });
658
- }
659
- else if (payload.type === 'reasoning' && payload.message) {
660
- // Codex reasoning 对应 Claude 的 thinking
661
- const lastTurn = turns[turns.length - 1];
662
- if (lastTurn && lastTurn.role === 'assistant') {
663
- lastTurn.blocks.unshift({ kind: 'thinking', text: payload.message });
664
- }
754
+ // 处理用户/助手消息。托管 app-server 使用 item_completed,旧版 CLI
755
+ // 使用 payload.type=user_message/agent_message;统一入口避免重复解析。
756
+ const message = messagesByEvent.get(event);
757
+ if (message?.role === 'user') {
758
+ totalMsgs++;
759
+ userMsgs++;
760
+ counts.userInput++;
761
+ turns.push({
762
+ role: 'user',
763
+ ts: event.timestamp ? Date.parse(event.timestamp) : 0,
764
+ uuid: message.id,
765
+ category: 'user_input',
766
+ blocks: [{ kind: 'text', text: message.text }],
767
+ });
768
+ }
769
+ else if (message?.role === 'assistant') {
770
+ totalMsgs++;
771
+ counts.modelOutput++;
772
+ turns.push({
773
+ role: 'assistant',
774
+ ts: event.timestamp ? Date.parse(event.timestamp) : 0,
775
+ uuid: message.id,
776
+ category: 'model_output',
777
+ blocks: [{ kind: 'text', text: message.text }],
778
+ });
779
+ }
780
+ else if (event.type === 'event_msg' && event.payload?.type === 'reasoning' && event.payload.message) {
781
+ // Codex reasoning 对应 Claude 的 thinking
782
+ const lastTurn = turns[turns.length - 1];
783
+ if (lastTurn && lastTurn.role === 'assistant') {
784
+ lastTurn.blocks.unshift({ kind: 'thinking', text: event.payload.message });
665
785
  }
666
786
  }
667
787
  const toolEvent = parseCodexToolEvent(event);
@@ -748,6 +868,9 @@ async function buildSnapshot(params) {
748
868
  metrics.totalMs = Date.now() - startedAt;
749
869
  return {
750
870
  ...m,
871
+ // EvolCore topics carry the user-facing name; Codex's DB title is often
872
+ // the routed envelope text for AUN-hosted sessions.
873
+ title: bind?.name?.trim() || m.title,
751
874
  sourceType: classifyTranscriptSource(bind, isCodexCliCandidate(m)),
752
875
  bound: !!bind,
753
876
  boundChannel: bind?.channelType ?? null,
@@ -781,7 +904,7 @@ async function buildSnapshot(params) {
781
904
  const transcript = metas.find(meta => meta.id === sessionId);
782
905
  const header = {
783
906
  sessionId,
784
- title: detail.title,
907
+ title: bind?.name?.trim() || detail.title,
785
908
  model: detail.model,
786
909
  gitBranch: detail.gitBranch,
787
910
  version: detail.version,
@@ -11,6 +11,7 @@ import fs from 'fs';
11
11
  import path from 'path';
12
12
  import { resolvePaths } from '../paths.js';
13
13
  import { ipcQuery } from '../ipc-client.js';
14
+ import { watchMenuRequestAuth } from './types.js';
14
15
  function readDefaultBaseagents() {
15
16
  try {
16
17
  const p = resolvePaths();
@@ -34,15 +35,15 @@ function readDefaultBaseagents() {
34
35
  return [];
35
36
  }
36
37
  }
37
- async function menuExec(payload) {
38
+ async function menuExec(payload, params) {
38
39
  const p = resolvePaths();
39
- const r = await ipcQuery(p.socket, { type: 'menu.exec', payload, auth: { source: 'ecweb', localDirect: true } }, 5000);
40
+ const r = await ipcQuery(p.socket, { type: 'menu.exec', payload, auth: watchMenuRequestAuth(params) }, 5000);
40
41
  return r?.ok ? r.response : null;
41
42
  }
42
- async function buildSnapshot() {
43
+ async function buildSnapshot(params = {}) {
43
44
  const [listResp, sysResp] = await Promise.all([
44
- menuExec({ type: 'menu.list', id: 'sys-list' }),
45
- menuExec({ type: 'menu.query', id: 'sys-q', name: 'system' }),
45
+ menuExec({ type: 'menu.list', id: 'sys-list' }, params),
46
+ menuExec({ type: 'menu.query', id: 'sys-q', name: 'system' }, params),
46
47
  ]);
47
48
  const daemonRunning = listResp !== null;
48
49
  // sysResp 形如 { type:'menu.response', id, name, data | error }
@@ -58,17 +59,17 @@ async function buildSnapshot() {
58
59
  }
59
60
  export const systemSource = {
60
61
  kind: 'system',
61
- async snapshot() {
62
- return buildSnapshot();
62
+ async snapshot(params = {}) {
63
+ return buildSnapshot(params);
63
64
  },
64
- subscribe(_params, push) {
65
+ subscribe(params, push) {
65
66
  let lastJson = '';
66
67
  let stopped = false;
67
68
  const tick = async () => {
68
69
  if (stopped)
69
70
  return;
70
71
  try {
71
- const snap = await buildSnapshot();
72
+ const snap = await buildSnapshot(params);
72
73
  const json = JSON.stringify(snap);
73
74
  if (json !== lastJson) {
74
75
  lastJson = json;
@@ -11,9 +11,10 @@ import fs from 'fs';
11
11
  import path from 'path';
12
12
  import { resolvePaths } from '../paths.js';
13
13
  import { ipcQuery } from '../ipc-client.js';
14
- async function menuExec(payload) {
14
+ import { watchMenuAuth, watchMenuRequestAuth } from './types.js';
15
+ async function menuExec(payload, params) {
15
16
  const p = resolvePaths();
16
- const r = await ipcQuery(p.socket, { type: 'menu.exec', payload, auth: { source: 'ecweb', localDirect: true } }, 5000);
17
+ const r = await ipcQuery(p.socket, { type: 'menu.exec', payload, auth: watchMenuRequestAuth(params) }, 5000);
17
18
  return r?.ok ? r.response : null;
18
19
  }
19
20
  function readTriggerControlToken(instanceDir) {
@@ -38,6 +39,15 @@ function readDaemonOwnerAid(p) {
38
39
  return null;
39
40
  }
40
41
  }
42
+ function readFullAccessEnabled(p) {
43
+ try {
44
+ const raw = JSON.parse(fs.readFileSync(p.daemonConfig, 'utf8'));
45
+ return raw?.fullaccess?.enabled === true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
41
51
  function normalizeAgent(item) {
42
52
  const aid = String(item?.value || item?.aid || '').trim();
43
53
  if (!aid)
@@ -50,17 +60,71 @@ function normalizeAgent(item) {
50
60
  baseagent: typeof item?.baseagent === 'string' ? item.baseagent : undefined,
51
61
  };
52
62
  }
63
+ function channelTypeFromKey(channelKey) {
64
+ const value = typeof channelKey === 'string' ? channelKey.trim() : '';
65
+ if (!value)
66
+ return '';
67
+ const separator = value.indexOf('#');
68
+ return separator > 0 ? value.slice(0, separator) : value;
69
+ }
70
+ function triggerScheduleView(definition, fallback) {
71
+ const source = definition?.source ?? fallback?.source;
72
+ if (source && typeof source === 'object') {
73
+ switch (source.type) {
74
+ case 'once': return { scheduleType: 'once', scheduleValue: '' };
75
+ case 'delay': return { scheduleType: 'delay', scheduleValue: String(source.afterMs ?? '') };
76
+ case 'at': return { scheduleType: 'at', scheduleValue: String(source.at ?? '') };
77
+ case 'cron': return { scheduleType: 'cron', scheduleValue: String(source.expression ?? '') };
78
+ case 'interval': return { scheduleType: 'interval', scheduleValue: String(source.everyMs ?? '') };
79
+ case 'event': return { scheduleType: 'event', scheduleValue: String(source.eventPattern ?? '') };
80
+ default: break;
81
+ }
82
+ }
83
+ return {
84
+ scheduleType: String(fallback?.scheduleType ?? ''),
85
+ scheduleValue: String(fallback?.scheduleValue ?? ''),
86
+ };
87
+ }
88
+ function triggerDisplayFields(definition, fallback) {
89
+ const feedback = definition?.feedback ?? fallback?.feedback;
90
+ const origin = definition?.origin;
91
+ const target = feedback?.strategy === 'target'
92
+ ? (feedback.target ?? fallback?.feedback?.target)
93
+ : feedback?.strategy === 'origin'
94
+ ? {
95
+ channelKey: origin?.channelKey,
96
+ channelId: origin?.channelId ?? origin?.peerId,
97
+ channelType: origin?.channelType,
98
+ }
99
+ : null;
100
+ const schedule = triggerScheduleView(definition, fallback);
101
+ const targetChannel = target?.channelKey ?? fallback?.targetChannel ?? '';
102
+ const targetChannelType = target?.channelType
103
+ ?? fallback?.targetChannelType
104
+ ?? (feedback?.strategy === 'target'
105
+ ? channelTypeFromKey(targetChannel)
106
+ : (origin?.channelType ?? channelTypeFromKey(targetChannel)));
107
+ return {
108
+ scheduleType: schedule.scheduleType,
109
+ scheduleValue: schedule.scheduleValue,
110
+ targetChannel,
111
+ targetChannelName: targetChannel || fallback?.targetChannelName || '',
112
+ targetChannelId: target?.channelId ?? fallback?.targetChannelId ?? '',
113
+ targetChannelType,
114
+ createdByPeerId: origin?.peerId ?? fallback?.createdByPeerId ?? '',
115
+ createdByChannel: origin?.channelKey ?? fallback?.createdByChannel ?? '',
116
+ createdAt: definition?.createdAt ?? fallback?.createdAt,
117
+ updatedAt: definition?.updatedAt ?? fallback?.updatedAt,
118
+ };
119
+ }
53
120
  async function listTriggersForAgent(agentAid, agentLabel) {
54
121
  const p = resolvePaths();
55
122
  const controlToken = readTriggerControlToken(p.instanceDir);
56
- const resp = await menuExec({
57
- type: 'menu.options',
58
- id: `tr-list-${agentAid}`,
59
- name: 'trigger',
60
- args: { options: 'all' },
61
- agent: agentAid,
62
- });
63
- const items = Array.isArray(resp?.data) ? resp.data : [];
123
+ // ECWeb is a management surface, so use the canonical Trigger IPC rather
124
+ // than routing through a regular Agent channel. This also covers the
125
+ // daemon-owned scheduler, which has no self-agent channel in the registry.
126
+ const resp = await ipcQuery(p.socket, { type: 'trigger.list', agentAid, all: true, controlToken }, 5000);
127
+ const items = Array.isArray(resp?.triggers) ? resp.triggers : [];
64
128
  return Promise.all(items.map(async (trigger) => {
65
129
  const effectiveAgentAid = trigger.agentAid || trigger.schedulerAid || agentAid;
66
130
  let definition = null;
@@ -99,14 +163,19 @@ async function listTriggersForAgent(agentAid, agentLabel) {
99
163
  revision,
100
164
  definitionError,
101
165
  subscription,
166
+ ...triggerDisplayFields(definition, trigger),
102
167
  };
103
168
  }));
104
169
  }
105
170
  async function buildSnapshot(params) {
106
171
  const p = resolvePaths();
172
+ const fullaccessEnabled = readFullAccessEnabled(p);
173
+ // This capability is derived from the server-injected ECWeb connection
174
+ // context; browser-supplied subscription parameters are never trusted for it.
175
+ const canApprovePersistentFullAccess = watchMenuAuth(params).canApprovePersistentFullAccess;
107
176
  const agent = typeof params?.agent === 'string' && params.agent ? params.agent : null;
108
177
  const [agentsResp, runtimeAgentsResp] = await Promise.all([
109
- menuExec({ type: 'menu.options', id: 'tr-agents', name: 'agent' }),
178
+ menuExec({ type: 'menu.options', id: 'tr-agents', name: 'agent' }, params),
110
179
  ipcQuery(p.socket, { type: 'evolagent.list' }, 5000),
111
180
  ]);
112
181
  // menu.options 响应形如 { type:'menu.response', id, name, data: MenuItem[] }
@@ -132,11 +201,11 @@ async function buildSnapshot(params) {
132
201
  if (agent) {
133
202
  const found = agentInfos.find((x) => x.aid === agent);
134
203
  const triggers = await listTriggersForAgent(agent, found?.label || agent);
135
- return { agents: agentInfos, triggers, selectedAgent: agent };
204
+ return { agents: agentInfos, triggers, selectedAgent: agent, fullaccessEnabled, canApprovePersistentFullAccess };
136
205
  }
137
206
  const batches = await Promise.all(agentInfos.map((x) => listTriggersForAgent(x.aid, x.label)));
138
207
  const triggers = batches.flat();
139
- return { agents: agentInfos, triggers, selectedAgent: agent };
208
+ return { agents: agentInfos, triggers, selectedAgent: agent, fullaccessEnabled, canApprovePersistentFullAccess };
140
209
  }
141
210
  export const triggersSource = {
142
211
  kind: 'triggers',
@@ -7,4 +7,23 @@
7
7
  *
8
8
  * aid/cache 走 IPC 轮询(无推送),msg/session 走 fs.watch 文件监听。
9
9
  */
10
- export {};
10
+ /** Read only the server-injected auth marker; browser parameters never decide it. */
11
+ export function watchMenuAuth(params) {
12
+ const auth = params?.__menuAuth;
13
+ return {
14
+ localDirect: auth?.localDirect === true,
15
+ actorAid: typeof auth?.actorAid === 'string' ? auth.actorAid : null,
16
+ // This marker is injected by the ECWeb server, never read from browser
17
+ // request parameters. Authenticated ECWeb has daemon-owner authority.
18
+ canApprovePersistentFullAccess: auth?.canApprovePersistentFullAccess === true,
19
+ };
20
+ }
21
+ export function watchMenuRequestAuth(params) {
22
+ const auth = watchMenuAuth(params);
23
+ return {
24
+ source: 'ecweb',
25
+ localDirect: auth.localDirect,
26
+ actorAid: auth.actorAid ?? null,
27
+ canApprovePersistentFullAccess: auth.canApprovePersistentFullAccess,
28
+ };
29
+ }