ec-web 0.0.26 → 0.0.28

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.
@@ -1,7 +1,6 @@
1
1
  /* EC Web — 前端 WS 客户端 + 多视图渲染 */
2
2
 
3
3
  const $ = (sel) => document.querySelector(sel);
4
- const TOKEN_KEY = 'ecWatchToken';
5
4
  const LANG_KEY = 'ecWatchLang';
6
5
  const VIEW_KEY = 'ecWatchCurrentView';
7
6
  const ROLE_SUBVIEW_KEY = 'ecWatchRoleSubview';
@@ -197,6 +196,8 @@ const translations = {
197
196
  'agents.th.lastActivity': '最后活动',
198
197
  'agents.th.operations': '操作',
199
198
  'agents.th.projectPath': '项目路径',
199
+ 'agents.lifecycle.created': '待初始化',
200
+ 'agents.lifecycle.bootstrapping': '初始化中',
200
201
 
201
202
  // Agent operations
202
203
  'agents.op.stopping': '停止中…',
@@ -322,6 +323,13 @@ const translations = {
322
323
  'sessions.empty.noMatch': '无匹配会话',
323
324
  'sessions.empty.noSessions': '该项目暂无会话',
324
325
  'sessions.empty.noContent': '该会话暂无内容',
326
+ 'sessions.empty.noBaseagent': '尚未配置默认 Base Agent(agents/defaults.json 的 active_baseagent)',
327
+ 'sessions.baseagentNone': '未配置 Base Agent',
328
+ 'sessions.baseagentDefault': '默认',
329
+ 'baseagents.claude': 'Claude',
330
+ 'baseagents.codex': 'Codex',
331
+ 'baseagents.gemini': 'Gemini',
332
+ 'baseagents.ecagent': 'EC Agent',
325
333
  'sessions.loadOlder': '加载更早内容',
326
334
  'sessions.loadFullResult': '加载完整结果',
327
335
  'sessions.header.project': '项目',
@@ -1128,6 +1136,8 @@ const translations = {
1128
1136
  'agents.th.lastActivity': 'Last Activity',
1129
1137
  'agents.th.operations': 'Operations',
1130
1138
  'agents.th.projectPath': 'Project Path',
1139
+ 'agents.lifecycle.created': 'Created',
1140
+ 'agents.lifecycle.bootstrapping': 'Bootstrapping',
1131
1141
 
1132
1142
  // Agent operations
1133
1143
  'agents.op.stopping': 'Stopping…',
@@ -1253,6 +1263,13 @@ const translations = {
1253
1263
  'sessions.empty.noMatch': 'No matching sessions',
1254
1264
  'sessions.empty.noSessions': 'No sessions in this project',
1255
1265
  'sessions.empty.noContent': 'No content in this session',
1266
+ 'sessions.empty.noBaseagent': 'No default Base Agent configured (active_baseagent in agents/defaults.json)',
1267
+ 'sessions.baseagentNone': 'No Base Agent configured',
1268
+ 'sessions.baseagentDefault': 'default',
1269
+ 'baseagents.claude': 'Claude',
1270
+ 'baseagents.codex': 'Codex',
1271
+ 'baseagents.gemini': 'Gemini',
1272
+ 'baseagents.ecagent': 'EC Agent',
1256
1273
  'sessions.loadOlder': 'Load older content',
1257
1274
  'sessions.loadFullResult': 'Load full result',
1258
1275
  'sessions.header.project': 'Project',
@@ -1921,7 +1938,7 @@ function toggleLang() {
1921
1938
  }
1922
1939
 
1923
1940
  // ── 基础路径 ──
1924
- // 本地直连时页面在 "/",经 AUN Service Proxy 时页面在 "/ecweb/"。
1941
+ // 本地直连时页面在 "/",经 AUN Service Proxy 时页面在 "/<host>/ecweb/"。
1925
1942
  // 取当前页面所在目录(含尾斜杠)作为所有 API/WS 的前缀,使绝对路径在两种
1926
1943
  // 部署下都正确(proxy-server 用首段路径选服务,前缀不能丢)。
1927
1944
  const BASE = (() => {
@@ -1934,6 +1951,9 @@ const BASE = (() => {
1934
1951
  }
1935
1952
  return location.pathname.replace(/[^/]*$/, '');
1936
1953
  })();
1954
+ // 同源的不同主机共享 localStorage,token 必须按完整代理路径隔离。
1955
+ // 旧 ecWatchToken 无法确定所属主机,不迁移;远程访问需重新配对一次。
1956
+ const TOKEN_KEY = `ecWatchToken:${BASE}`;
1937
1957
  const apiUrl = (p) => BASE + p.replace(/^\/+/, '');
1938
1958
  const authApiUrl = (p) => {
1939
1959
  const token = localStorage.getItem(TOKEN_KEY);
@@ -2096,36 +2116,25 @@ function connect() {
2096
2116
  ws.onopen = () => {
2097
2117
  setConnStatus('● ' + t('status.connected'), 'ok');
2098
2118
  reconnectDelay = 1000;
2099
- // 获取可用的 baseagent
2100
- fetch(`${BASE}api/available-baseagents`)
2101
- .then(r => r.json())
2102
- .then(data => {
2103
- availableBaseagents = data;
2104
- // 如果当前没有选中 baseagent,默认选第一个可用的
2105
- if (!sessSel.baseagent) {
2106
- sessSel.baseagent = data.claude ? 'claude' : (data.codex ? 'codex' : null);
2107
- }
2108
- console.log('[ecweb] Available baseagents:', availableBaseagents, 'Selected:', sessSel.baseagent);
2109
- // 重新订阅当前视图(带上正确的参数)
2110
- if (currentView === 'session') {
2111
- subscribe('session', sessSel);
2112
- } else if (currentView === 'roleManagement') {
2113
- subscribeRoleManagementSubview();
2114
- } else if (currentView === 'system' && systemContentMode === 'monitor') {
2115
- subscribe('monitor', { range: monRange });
2116
- } else {
2117
- subscribe(currentView, pendingSub || {});
2118
- }
2119
- })
2120
- .catch(err => {
2121
- console.warn('[ecweb] Failed to fetch available-baseagents:', err);
2122
- // 失败时默认使用 claude
2123
- availableBaseagents = { claude: true, codex: false };
2124
- if (!sessSel.baseagent) sessSel.baseagent = 'claude';
2125
- if (currentView === 'roleManagement') subscribeRoleManagementSubview();
2126
- else if (currentView === 'system' && systemContentMode === 'monitor') subscribe('monitor', { range: monRange });
2127
- else subscribe(currentView, pendingSub || {});
2128
- });
2119
+ // 获取 base agent 配置(defaults.json 的 active_baseagent + baseagents)
2120
+ // 配置清单(defaults.json)与最近活跃 Agent 并行取,后者决定会话页默认后端。
2121
+ Promise.all([
2122
+ menuSend({ type: 'menu.query', name: 'baseagents' }).then(response => {
2123
+ const result = mResp(response);
2124
+ if (result.error) throw new Error(result.error.message || 'Baseagent query failed');
2125
+ return result.data;
2126
+ }).catch(err => {
2127
+ console.warn('[ecweb] Failed to fetch baseagents:', err);
2128
+ return null;
2129
+ }),
2130
+ loadActiveAgentBaseagent(),
2131
+ ]).then(([data, activeBaseagent]) => {
2132
+ applyBaseagentConfig(data);
2133
+ // 没有显式选择时跟随最近活跃 Agent 的后端(其次才是 defaults 的默认后端)。
2134
+ if (!sessSel.baseagent) sessSel.baseagent = activeBaseagent || defaultSessionBaseagent();
2135
+ console.log('[ecweb] Base agents:', baseagentConfig, 'Active agent:', activeAgentAid, 'Selected:', sessSel.baseagent);
2136
+ resubscribeCurrentView();
2137
+ });
2129
2138
  };
2130
2139
 
2131
2140
  ws.onmessage = (ev) => {
@@ -2178,6 +2187,8 @@ function connect() {
2178
2187
  } else {
2179
2188
  state[msg.view] = msg.data;
2180
2189
  }
2190
+ // agent 列表本身就带 baseagent + lastActivity,顺手更新"最近活跃 Agent"。
2191
+ if (msg.view === 'agents') noteAgentActivity(msg.data?.agents);
2181
2192
  const messageMatchesCurrentView = msg.view === currentView
2182
2193
  || (currentView === 'system' && systemContentMode === 'monitor' && msg.view === 'monitor')
2183
2194
  || (currentView === 'roleManagement' && msg.view === roleManagementViewKind());
@@ -2284,7 +2295,38 @@ let sessSourceFilter = 'all'; // all / trigger / agent / cli / user / unknown
2284
2295
  let sessChatMode = false; // false=完整视图,true=对话视图(折叠处理过程)
2285
2296
  let monRange = '2m'; // Monitor 时间窗口:2m / 10m / 1h
2286
2297
  let logSel = { source: null, level: 'all', module: 'all', search: '', limit: 500, follow: true };
2287
- let availableBaseagents = { claude: false, codex: false }; // 可用的 baseagent
2298
+ // ECWeb 界面的 base agent 展示名统一走这里的 i18n 键,而不是父包别名表:
2299
+ // 别名表面向 CLI/日志(claude → "Claude Code"、ecagent → "ecagent"),
2300
+ // 面板里应保持简洁一致(Claude / Codex / Gemini / EC Agent)。
2301
+ function baseagentDisplayLabel(id) {
2302
+ const key = `baseagents.${id}`;
2303
+ const label = t(key);
2304
+ return label === key ? (id || 'Baseagent') : label;
2305
+ }
2306
+
2307
+ function applyBaseagentConfig(data) {
2308
+ const list = Array.isArray(data?.baseagents) ? data.baseagents : [];
2309
+ baseagentConfig = {
2310
+ active: typeof data?.active === 'string' && data.active ? data.active : null,
2311
+ baseagents: list
2312
+ .filter(entry => entry && typeof entry.id === 'string' && entry.id)
2313
+ .map(entry => ({
2314
+ id: entry.id,
2315
+ label: baseagentDisplayLabel(entry.id),
2316
+ detected: entry.detected === true,
2317
+ configured: entry.configured === true,
2318
+ loaded: entry.loaded === true,
2319
+ active: entry.active === true,
2320
+ })),
2321
+ };
2322
+ }
2323
+
2324
+ function resubscribeCurrentView() {
2325
+ if (currentView === 'session') subscribe('session', sessSel);
2326
+ else if (currentView === 'roleManagement') subscribeRoleManagementSubview();
2327
+ else if (currentView === 'system' && systemContentMode === 'monitor') subscribe('monitor', { range: monRange });
2328
+ else subscribe(currentView, pendingSub || {});
2329
+ }
2288
2330
  let rolesCurrentAgent = null;
2289
2331
  let rolesSearchTerm = '';
2290
2332
  let rolesVerifyFilter = '';
@@ -2694,6 +2736,11 @@ function agentStateBadge(s, agStatus, connStatus) {
2694
2736
  return `<span class="state-badge connected">${t('status.connected')}</span>`;
2695
2737
  }
2696
2738
 
2739
+ function agentLifecycleBadge(lifecycle) {
2740
+ if (lifecycle !== 'created' && lifecycle !== 'bootstrapping') return '';
2741
+ return `<span class="lifecycle-badge ${lifecycle}">${t(`agents.lifecycle.${lifecycle}`)}</span>`;
2742
+ }
2743
+
2697
2744
  // 发送方式图标标记
2698
2745
  const MSG_KIND_META = {
2699
2746
  send: { icon: '💬', label: () => t('messages.msgKind.reply') },
@@ -3100,7 +3147,7 @@ function renderAgents(data) {
3100
3147
  ? agentOpsBusyHtml(ag.aid, _agentOps.get(ag.aid))
3101
3148
  : agentDisabledOpsHtml(ag.aid);
3102
3149
  html += `<tr class="ag-main">` +
3103
- `<td><div class="ag-id"><span class="dot off"></span><span class="ag-id-text"><span class="ag-name">${esc(ag.displayName || shortAid(ag.aid))}</span><span class="ag-aid">${esc(ag.aid)}</span></span></div></td>` +
3150
+ `<td><div class="ag-id"><span class="dot off"></span><span class="ag-id-text"><span class="ag-name">${esc(ag.displayName || shortAid(ag.aid))}${agentLifecycleBadge(ag.lifecycle)}</span><span class="ag-aid">${esc(ag.aid)}</span></span></div></td>` +
3104
3151
  `<td style="font-size:11px;font-family:monospace">${esc(ag.projectPath || '—')}</td>` +
3105
3152
  `<td class="agent-ops-cell">${ops}</td></tr>`;
3106
3153
  }
@@ -3146,7 +3193,7 @@ function renderAgents(data) {
3146
3193
  const model = ag.model || ag.baseagent || '—';
3147
3194
 
3148
3195
  const idCell = `<div class="ag-id"><span class="dot ${dotCls}" title="${esc(connStatus)}"></span>` +
3149
- `<span class="ag-id-text"><span class="ag-name">${esc(name)}</span>` +
3196
+ `<span class="ag-id-text"><span class="ag-name">${esc(name)}${agentLifecycleBadge(ag.lifecycle)}</span>` +
3150
3197
  `<span class="ag-aid">${esc(ag.aid)}</span></span></div>`;
3151
3198
 
3152
3199
  html += `<tr class="ag-main">` +
@@ -3394,7 +3441,7 @@ function renderMsg(data) {
3394
3441
  : '';
3395
3442
  peersHtml += `<div class="list-item${sel}" data-peer="${esc(p.peerId)}">` +
3396
3443
  `<div class="name">${groupMark}${partitionMark}<span class="msg-peer-title" title="${esc(chatTitle)}">${esc(shortId(chatTitle))}</span>${channelTag}</div>` +
3397
- `<div class="sub">${p.partition === 'thread' ? `${esc(shortId(displayName))} · ` : ''}↓${p.inbound} ↑${p.outbound} · ${fmtAgo(p.lastAt)}</div></div>`;
3444
+ `<div class="sub">${p.lable ? `${esc(p.lable)} · ` : ''}${p.partition === 'thread' ? `${esc(shortId(displayName))} · ` : ''}↓${p.inbound} ↑${p.outbound} · ${fmtAgo(p.lastAt)}</div></div>`;
3398
3445
  }
3399
3446
  }
3400
3447
  $('#msg-peers').innerHTML = peersHtml;
@@ -3494,6 +3541,108 @@ function filterSessionTranscripts(transcripts, showAll, sourceType = 'all') {
3494
3541
  });
3495
3542
  }
3496
3543
 
3544
+ // Base agent 清单来自 EvolCore 的 baseagents menu 查询;前端不维护候选数组。
3545
+ let baseagentConfig = { active: null, baseagents: [] };
3546
+
3547
+ function baseagentConfigLoaded() {
3548
+ return Array.isArray(baseagentConfig.baseagents) && baseagentConfig.baseagents.length > 0;
3549
+ }
3550
+
3551
+ function configuredBaseagents() {
3552
+ return baseagentConfig.baseagents || [];
3553
+ }
3554
+
3555
+ function configuredActiveBaseagent() {
3556
+ return baseagentConfig.active || null;
3557
+ }
3558
+
3559
+ // ── 最近活跃 Agent 的后端 ──
3560
+ // 数据来自 agent 列表(menu.query name=agent,与 agents 页面同源):每条带
3561
+ // baseagent + lastActivity。谁最近被驱动过,会话页就默认看谁的后端。
3562
+ let activeAgentBaseagent = null; // 最近活跃 Agent 的 base agent
3563
+ let activeAgentAid = null; // 对应的 Agent AID
3564
+
3565
+ function pickActiveAgentBaseagent(agents) {
3566
+ let best = null;
3567
+ for (const ag of agents || []) {
3568
+ const baseagent = String(ag?.baseagent || '').trim();
3569
+ const lastActivity = Number(ag?.lastActivity) || 0;
3570
+ if (!baseagent || lastActivity <= 0) continue;
3571
+ if (!best || lastActivity > best.lastActivity) {
3572
+ best = { baseagent, lastActivity, aid: ag.aid || null };
3573
+ }
3574
+ }
3575
+ return best;
3576
+ }
3577
+
3578
+ function noteAgentActivity(agents) {
3579
+ const best = pickActiveAgentBaseagent(agents);
3580
+ activeAgentBaseagent = best?.baseagent || null;
3581
+ activeAgentAid = best?.aid || null;
3582
+ return activeAgentBaseagent;
3583
+ }
3584
+
3585
+ /** 拉取 agent 列表并算出最近活跃 Agent 的后端(失败则返回 null,不猜测)。 */
3586
+ async function loadActiveAgentBaseagent() {
3587
+ try {
3588
+ const r = mResp(await menuSend({
3589
+ type: 'menu.query',
3590
+ name: 'agent',
3591
+ args: { options: 'all' },
3592
+ }));
3593
+ if (r.error) return null;
3594
+ const agents = Array.isArray(r.data?.agents) ? r.data.agents : (Array.isArray(r.data) ? r.data : []);
3595
+ return noteAgentActivity(agents);
3596
+ } catch {
3597
+ return null;
3598
+ }
3599
+ }
3600
+
3601
+ /** 会话页可选的 base agent:配置里有的、本机可用的,以及最近活跃 Agent 用到的。 */
3602
+ function sessionBaseagentCandidates() {
3603
+ const list = [...configuredBaseagents()];
3604
+ const seen = new Set(list.map(entry => entry.id));
3605
+ for (const id of [activeAgentBaseagent]) {
3606
+ if (!id || seen.has(id)) continue;
3607
+ seen.add(id);
3608
+ list.push({
3609
+ id,
3610
+ label: baseagentDisplayLabel(id),
3611
+ detected: true,
3612
+ configured: false,
3613
+ active: false,
3614
+ });
3615
+ }
3616
+ return list;
3617
+ }
3618
+
3619
+ /**
3620
+ * 会话页默认后端:跟随最近活跃的 Agent(含 trigger 等后台任务驱动的);
3621
+ * 没有任何活跃记录时才用 defaults 的默认后端。两者都没有就不猜(返回 null)。
3622
+ */
3623
+ function defaultSessionBaseagent() {
3624
+ if (activeAgentBaseagent) return activeAgentBaseagent;
3625
+ return configuredActiveBaseagent();
3626
+ }
3627
+
3628
+ // 会话视图列出「已配置或本机可用」的 base agent:清单来自 defaults.json 的
3629
+ // baseagents 与本机检测,不维护候选数组,也不注入任何默认后端。
3630
+ // 默认后端(defaults.active_baseagent)单独标注,避免它与"当前选中"混淆。
3631
+ function sessionBaseagentOptions(selected) {
3632
+ let html = '';
3633
+ for (const entry of sessionBaseagentCandidates()) {
3634
+ if (!entry.configured && !entry.detected && !entry.loaded && !entry.active) continue;
3635
+ const label = entry.active
3636
+ ? `${entry.label} (${t('sessions.baseagentDefault')})`
3637
+ : entry.label;
3638
+ html += `<option value="${esc(entry.id)}"${selected === entry.id ? ' selected' : ''}>${esc(label)}</option>`;
3639
+ }
3640
+ if (!html) {
3641
+ return `<option value="" selected disabled>${esc(t('sessions.baseagentNone'))}</option>`;
3642
+ }
3643
+ return html;
3644
+ }
3645
+
3497
3646
  function renderSessionSourceBadge(sourceType) {
3498
3647
  const type = ['trigger', 'agent', 'cli', 'user'].includes(sourceType) ? sourceType : 'unknown';
3499
3648
  const label = t(`sessions.source.${type}`);
@@ -3522,13 +3671,7 @@ function renderSession(data) {
3522
3671
  ).join('');
3523
3672
 
3524
3673
  // baseagent 下拉选择器(只显示可用的)
3525
- let baseagentOpts = '';
3526
- if (availableBaseagents.claude) {
3527
- baseagentOpts += `<option value="claude"${sessSel.baseagent === 'claude' ? ' selected' : ''}>Claude</option>`;
3528
- }
3529
- if (availableBaseagents.codex) {
3530
- baseagentOpts += `<option value="codex"${sessSel.baseagent === 'codex' ? ' selected' : ''}>Codex</option>`;
3531
- }
3674
+ const baseagentOpts = sessionBaseagentOptions(sessSel.baseagent);
3532
3675
  const sourceTypes = ['all', 'user', 'agent', 'trigger', 'cli', 'unknown'];
3533
3676
  const sourceOpts = sourceTypes.map(type => {
3534
3677
  const label = type === 'all' ? t('sessions.filter.sourceAll') : t(`sessions.source.${type}`);
@@ -3554,7 +3697,10 @@ function renderSession(data) {
3554
3697
  '<div class="sess-items">';
3555
3698
 
3556
3699
  if (!filtered.length) {
3557
- listHtml += '<div class="empty">' + (transcripts.length ? t('sessions.empty.noMatch') : t('sessions.empty.noSessions')) + '</div>';
3700
+ const emptyText = data.noConfiguredBaseagent
3701
+ ? t('sessions.empty.noBaseagent')
3702
+ : (transcripts.length ? t('sessions.empty.noMatch') : t('sessions.empty.noSessions'));
3703
+ listHtml += '<div class="empty">' + emptyText + '</div>';
3558
3704
  }
3559
3705
  for (const t of filtered) {
3560
3706
  const sel = t.id === sessSel.sessionId ? ' sel' : '';
@@ -3740,7 +3886,7 @@ const CAT_META = {
3740
3886
 
3741
3887
  function renderSessHeader(h, baseagent) {
3742
3888
  if (!h || !h.sessionId) return '';
3743
- const baseagentLabel = baseagent === 'codex' ? 'Codex' : baseagent === 'claude' ? 'Claude' : (baseagent || 'Baseagent');
3889
+ const baseagentLabel = (configuredBaseagents().find(entry => entry.id === baseagent)?.label) || baseagent || 'Baseagent';
3744
3890
  const title = `${baseagentLabel} ${h.sessionId}`;
3745
3891
  const tok = (h.inputTokens || h.outputTokens)
3746
3892
  ? `<span class="sh-stat">🔢 in ${fmtNum(h.inputTokens)} / out ${fmtNum(h.outputTokens)}</span>` : '';
@@ -4190,7 +4336,18 @@ function agentOpNew() {
4190
4336
  openAgentCreateDrawer();
4191
4337
  }
4192
4338
 
4193
- const AGENT_BASEAGENT_TYPES = ['claude', 'codex', 'gemini', 'ecagent'];
4339
+ // 可选 base agent 由 defaults.json 的 baseagents + 本机检测决定(见 applyBaseagentConfig),
4340
+ // 这里不再维护写死的候选数组。
4341
+ function agentBaseagentTypes() {
4342
+ const ids = configuredBaseagents()
4343
+ .filter(entry => entry.configured || entry.detected || entry.active)
4344
+ .map(entry => entry.id);
4345
+ return ids.length ? ids : [];
4346
+ }
4347
+
4348
+ function agentBaseagentLabel(id) {
4349
+ return configuredBaseagents().find(entry => entry.id === id)?.label || id;
4350
+ }
4194
4351
  const AGENT_AID_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?){2,}$/;
4195
4352
  let _agentEdit = null;
4196
4353
  let _agentCreate = null;
@@ -4206,7 +4363,7 @@ function agentSafeConfig(data) {
4206
4363
 
4207
4364
  function agentActiveBaseagent(data) {
4208
4365
  const safe = agentSafeConfig(data);
4209
- return String(safe.active_baseagent || data?.config?.active_baseagent || data?.config?.baseagent || 'claude');
4366
+ return String(safe.active_baseagent || data?.config?.active_baseagent || data?.config?.baseagent || configuredActiveBaseagent() || '');
4210
4367
  }
4211
4368
 
4212
4369
  function agentBaseagentBlock(data, type) {
@@ -4232,6 +4389,7 @@ function agentOriginalEditState(aid, data) {
4232
4389
  return {
4233
4390
  name: String(data?.identity?.name || data?.displayName || shortAid(aid)),
4234
4391
  owners: owners.map(String),
4392
+ fullaccess: safe.fullaccess?.enabled === true,
4235
4393
  projectPath,
4236
4394
  activeBaseagent: active,
4237
4395
  model: block.model == null ? '' : String(block.model),
@@ -4264,7 +4422,7 @@ function agentBaseagentOptionValues(items) {
4264
4422
  return values;
4265
4423
  }
4266
4424
 
4267
- function agentBaseagentOptions(active, options = AGENT_BASEAGENT_TYPES) {
4425
+ function agentBaseagentOptions(active, options = agentBaseagentTypes()) {
4268
4426
  const seen = new Set();
4269
4427
  const list = [];
4270
4428
  const push = value => {
@@ -4276,9 +4434,10 @@ function agentBaseagentOptions(active, options = AGENT_BASEAGENT_TYPES) {
4276
4434
  (options || []).forEach(push);
4277
4435
  const activeWasProvided = seen.has(String(active || '').trim());
4278
4436
  if (active) push(active);
4279
- if (!list.length) AGENT_BASEAGENT_TYPES.forEach(push);
4280
4437
  return list.map(type => {
4281
- const label = !activeWasProvided && type === active ? `${type} (${t('common.currentSuffix')})` : type;
4438
+ const label = !activeWasProvided && type === active
4439
+ ? `${agentBaseagentLabel(type)} (${t('common.currentSuffix')})`
4440
+ : agentBaseagentLabel(type);
4282
4441
  return `<option value="${esc(type)}"${type === active ? ' selected' : ''}>${esc(label)}</option>`;
4283
4442
  }).join('');
4284
4443
  }
@@ -4657,7 +4816,11 @@ function setAgentSelectError(select, current) {
4657
4816
  }
4658
4817
 
4659
4818
  function agentCreateDefaultBaseagent() {
4660
- return AGENT_BASEAGENT_TYPES.find(type => availableBaseagents && availableBaseagents[type] === true) || 'claude';
4819
+ // 新 Agent 默认继承 defaults.json 的 active_baseagent;没有配置时留空,
4820
+ // 由校验拦下,而不是悄悄替用户选一个后端。
4821
+ return configuredActiveBaseagent()
4822
+ || configuredBaseagents().find(entry => entry.detected)?.id
4823
+ || '';
4661
4824
  }
4662
4825
 
4663
4826
  function readAgentCreateValues(root) {
@@ -4679,7 +4842,7 @@ function validateAgentCreateValues(values) {
4679
4842
  if (!values.name) return t('agents.edit.validation.nameRequired');
4680
4843
  if (!values.owner) return t('agents.edit.validation.ownerRequired');
4681
4844
  if (!AGENT_AID_RE.test(values.owner)) return t('agents.edit.validation.invalidOwner');
4682
- if (!AGENT_BASEAGENT_TYPES.includes(values.baseagent)) return tf('agents.edit.validation.invalidBaseagent', { baseagent: values.baseagent });
4845
+ if (!agentBaseagentTypes().includes(values.baseagent)) return tf('agents.edit.validation.invalidBaseagent', { baseagent: values.baseagent });
4683
4846
  return '';
4684
4847
  }
4685
4848
 
@@ -4889,8 +5052,9 @@ function readAgentEditValues(root) {
4889
5052
  });
4890
5053
  return {
4891
5054
  owners: agentOwnersFromText(root.querySelector('#ag-edit-owners')?.value || ''),
5055
+ fullaccess: root.querySelector('#ag-edit-fullaccess')?.checked,
4892
5056
  channelOwners,
4893
- activeBaseagent: root.querySelector('#ag-edit-active')?.value || 'claude',
5057
+ activeBaseagent: root.querySelector('#ag-edit-active')?.value || configuredActiveBaseagent() || '',
4894
5058
  model: root.querySelector('#ag-edit-model')?.value.trim() || '',
4895
5059
  effort: root.querySelector('#ag-edit-effort')?.value.trim() || '',
4896
5060
  };
@@ -4952,6 +5116,9 @@ function collectAgentEditPatch(ctx) {
4952
5116
  const configPatch = {};
4953
5117
 
4954
5118
  if (!sameStringArray(values.owners, ctx.original.owners)) configPatch.owners = values.owners;
5119
+ if (values.fullaccess !== undefined && values.fullaccess !== ctx.original.fullaccess) {
5120
+ configPatch.fullaccess = { enabled: values.fullaccess };
5121
+ }
4955
5122
  if (values.channelOwners.length > 0) configPatch.channelOwners = values.channelOwners;
4956
5123
  if (values.activeBaseagent !== ctx.original.activeBaseagent) {
4957
5124
  if (Array.isArray(ctx.availableBaseagents) && ctx.availableBaseagents.length && !ctx.availableBaseagents.includes(values.activeBaseagent)) {
@@ -4977,6 +5144,8 @@ function collectAgentEditPatch(ctx) {
4977
5144
  function setAgentEditSaving(ctx, saving) {
4978
5145
  ctx.saving = saving;
4979
5146
  ctx.backdrop.querySelectorAll('button, input, textarea, select').forEach(el => { el.disabled = saving; });
5147
+ const fullaccess = ctx.backdrop.querySelector('#ag-edit-fullaccess');
5148
+ if (fullaccess) fullaccess.disabled = saving || ctx.data?.fullaccessStatus?.canConfigure !== true;
4980
5149
  const save = ctx.backdrop.querySelector('#ag-edit-save');
4981
5150
  const saveReload = ctx.backdrop.querySelector('#ag-edit-save-reload');
4982
5151
  if (save) save.textContent = saving ? t('common.saving') : t('common.save');
@@ -5169,6 +5338,25 @@ function openAgentConfigDrawer(aid, data) {
5169
5338
  const active = original.activeBaseagent;
5170
5339
  const titleName = original.name || shortAid(aid);
5171
5340
  const jsonText = esc(JSON.stringify(safe, null, 2));
5341
+ const fullaccessStatus = data?.fullaccessStatus || {};
5342
+ const fullaccessHelp = currentLang === 'zh-CN'
5343
+ ? '开启后,同时属于该 Agent Owner 和 daemon Owner 的直接私聊任务可访问宿主环境及 daemon 范围数据。'
5344
+ : 'Enables host and daemon scope for direct private tasks whose sender is both an Agent Owner and a daemon Owner.';
5345
+ const fullaccessHelpLabel = currentLang === 'zh-CN' ? 'Fullaccess 说明' : 'Fullaccess info';
5346
+ const fullaccessState = enabled => currentLang === 'zh-CN'
5347
+ ? (enabled ? '开启' : '关闭')
5348
+ : (enabled ? 'Enabled' : 'Disabled');
5349
+ const fullaccessSection = fullaccessStatus.hostEnabled ?
5350
+ `<section class="ag-edit-section">` +
5351
+ `<div class="ag-edit-fullaccess-row">` +
5352
+ `<span class="ag-edit-field-label">Owner Fullaccess<span class="basic-settings-tip" tabindex="0" role="img" aria-label="${esc(fullaccessHelpLabel)}" data-tooltip="${esc(fullaccessHelp)}">?</span></span>` +
5353
+ `<label class="sess-switch ag-edit-fullaccess-switch">` +
5354
+ `<span id="ag-edit-fullaccess-state" aria-live="polite">${fullaccessState(original.fullaccess)}</span>` +
5355
+ `<input type="checkbox" role="switch" aria-label="Owner Fullaccess" id="ag-edit-fullaccess" ${original.fullaccess ? 'checked' : ''} ${fullaccessStatus.canConfigure === true ? '' : 'disabled'}>` +
5356
+ `<span class="sess-switch-track" aria-hidden="true"><span class="sess-switch-thumb"></span></span>` +
5357
+ `</label>` +
5358
+ `</div>` +
5359
+ `</section>` : '';
5172
5360
 
5173
5361
  const html = `<div class="ag-edit-backdrop" id="ag-edit-backdrop">` +
5174
5362
  `<aside class="ag-edit-drawer" role="dialog" aria-modal="true" aria-labelledby="ag-edit-title">` +
@@ -5183,6 +5371,7 @@ function openAgentConfigDrawer(aid, data) {
5183
5371
  `<section class="ag-edit-section">` +
5184
5372
  `<label class="ag-edit-field"><span>Owners</span><textarea id="ag-edit-owners" rows="2" spellcheck="false">${esc(original.owners.join('\n'))}</textarea></label>` +
5185
5373
  `</section>` +
5374
+ fullaccessSection +
5186
5375
  `<section class="ag-edit-section">` +
5187
5376
  `<h3>${t('agents.edit.section.runtime')}</h3>` +
5188
5377
  `<div class="ag-edit-grid">` +
@@ -5235,6 +5424,9 @@ function openAgentConfigDrawer(aid, data) {
5235
5424
  backdrop.querySelector('#ag-edit-cancel').addEventListener('click', closeAgentConfigDrawer);
5236
5425
  backdrop.querySelector('#ag-edit-save').addEventListener('click', () => saveAgentConfigDrawer(ctx, false));
5237
5426
  backdrop.querySelector('#ag-edit-save-reload').addEventListener('click', () => saveAgentConfigDrawer(ctx, true));
5427
+ backdrop.querySelector('#ag-edit-fullaccess')?.addEventListener('change', (event) => {
5428
+ backdrop.querySelector('#ag-edit-fullaccess-state').textContent = fullaccessState(event.currentTarget.checked);
5429
+ });
5238
5430
  backdrop.querySelector('#ag-edit-active').addEventListener('change', () => syncAgentBaseagentFields(ctx));
5239
5431
  backdrop.querySelector('#ag-edit-model').addEventListener('change', () => {
5240
5432
  const baseagent = backdrop.querySelector('#ag-edit-active')?.value || ctx.original.activeBaseagent;
@@ -5297,11 +5489,17 @@ function agentHealthCard(ag) {
5297
5489
  }
5298
5490
 
5299
5491
  function systemBaseagentCards(baseagents) {
5300
- const list = Array.isArray(baseagents) ? baseagents : [];
5492
+ const list = Array.isArray(baseagents) ? baseagents : (baseagentConfig.baseagents || []);
5301
5493
  return list.map(ba => {
5302
- const ver = ba.version ? `・${ba.version}` : '';
5303
- const title = `Baseagent・${ba.active ? '✓ ' : ''}${ba.name || 'unknown'}${ver}`;
5304
- const detail = [ba.model, ba.effort].filter(Boolean).map(esc).join(' · ') || t('system.baseagent.unspecified');
5494
+ const id = ba.id || ba.name || 'unknown';
5495
+ const ver = ba.version ? ` · ${ba.version}` : '';
5496
+ const title = `Baseagent · ${ba.active ? '✓ ' : ''}${baseagentDisplayLabel(id)}${ver}`;
5497
+ const states = [
5498
+ ba.configured && (currentLang === 'zh-CN' ? '已配置' : 'configured'),
5499
+ ba.detected && (currentLang === 'zh-CN' ? '已检测' : 'detected'),
5500
+ ba.loaded && (currentLang === 'zh-CN' ? '已加载' : 'loaded'),
5501
+ ].filter(Boolean);
5502
+ const detail = [...[ba.model, ba.effort].filter(Boolean), ...states].map(esc).join(' · ') || t('system.baseagent.unspecified');
5305
5503
  return `<div class="cache-card"><div class="card-label">${esc(title)}</div><div class="card-val">${detail}</div></div>`;
5306
5504
  }).join('');
5307
5505
  }
@@ -5436,7 +5634,7 @@ function bindSystemEvents(el, data) {
5436
5634
  // 数据来自 daemon menu.query name=gateway(apiKey 已掩码)。
5437
5635
  // 写操作走 menuSend({name:'gateway', ...}):update/test/delete。
5438
5636
 
5439
- const GATEWAY_TYPE_ICON = { claude: '🟣', codex: '🟢', gemini: '🔵' };
5637
+ const GATEWAY_TYPE_ICON = { claude: '🟣', codex: '🟢', gemini: '🔵', ecagent: '⚪' };
5440
5638
 
5441
5639
  // 标记每条网关的运行时测试结果:`${scope}#${type}` → { ok, latency, modelCount, error }
5442
5640
  const _gwTest = new Map();
@@ -6072,7 +6270,7 @@ function triggerComparableScheduleValue(type, value) {
6072
6270
  function triggerActiveBaseagentForAgent(agentAid) {
6073
6271
  const list = state.agents?.agents || [];
6074
6272
  const ag = list.find(x => x.aid === agentAid || x.value === agentAid);
6075
- return ag?.baseagent || ag?.activeBaseagent || ag?.active_baseagent || 'claude';
6273
+ return ag?.baseagent || ag?.activeBaseagent || ag?.active_baseagent || configuredActiveBaseagent() || '';
6076
6274
  }
6077
6275
 
6078
6276
  function triggerScheduleLabel(type) {
@@ -6911,12 +7109,18 @@ function closeTriggerEditDrawer() {
6911
7109
  _triggerEdit = null;
6912
7110
  }
6913
7111
 
7112
+ function triggerUsesModelOptions(ctx) {
7113
+ const executionType = ctx.backdrop.querySelector('#trig-edit-type')?.value || ctx.original.executionType;
7114
+ return executionType !== 'script';
7115
+ }
7116
+
6914
7117
  async function syncTriggerOptionFields(ctx, values) {
7118
+ if (!triggerUsesModelOptions(ctx)) return;
6915
7119
  const modelSelect = ctx.backdrop.querySelector('#trig-edit-model');
6916
7120
  const effortSelect = ctx.backdrop.querySelector('#trig-edit-effort');
6917
7121
  const currentModel = values.model || '';
6918
7122
  const currentEffort = values.effort || '';
6919
- const baseagent = values.baseagent || ctx.original.baseagent || 'claude';
7123
+ const baseagent = values.baseagent || ctx.original.baseagent || configuredActiveBaseagent() || '';
6920
7124
  const token = (ctx.optionLoadToken || 0) + 1;
6921
7125
  ctx.optionLoadToken = token;
6922
7126
 
@@ -6925,37 +7129,39 @@ async function syncTriggerOptionFields(ctx, values) {
6925
7129
 
6926
7130
  try {
6927
7131
  const models = await agentFetchOptions('model', ctx.original.agentAid, { baseagent, model: currentModel });
6928
- if (_triggerEdit !== ctx || ctx.optionLoadToken !== token) return;
7132
+ if (_triggerEdit !== ctx || ctx.optionLoadToken !== token || !triggerUsesModelOptions(ctx)) return;
6929
7133
  modelSelect.innerHTML = agentSelectOptions(models, currentModel, t('common.inherited'));
6930
7134
  } catch (e) {
6931
- if (_triggerEdit === ctx && ctx.optionLoadToken === token) {
7135
+ if (_triggerEdit === ctx && ctx.optionLoadToken === token && triggerUsesModelOptions(ctx)) {
6932
7136
  modelSelect.innerHTML = agentSelectOptions([], currentModel, t('common.inherited'));
6933
7137
  toast(e.message || String(e), true);
6934
7138
  }
6935
7139
  } finally {
6936
- if (_triggerEdit === ctx && ctx.optionLoadToken === token) modelSelect.disabled = Boolean(ctx.saving);
7140
+ if (_triggerEdit === ctx && ctx.optionLoadToken === token && triggerUsesModelOptions(ctx)) modelSelect.disabled = Boolean(ctx.saving);
6937
7141
  }
6938
7142
 
7143
+ if (!triggerUsesModelOptions(ctx)) return;
6939
7144
  const selectedModel = modelSelect?.value || currentModel;
6940
7145
  await syncTriggerEffortOptions(ctx, baseagent, selectedModel, currentEffort, token);
6941
7146
  }
6942
7147
 
6943
7148
  async function syncTriggerEffortOptions(ctx, baseagent, model, currentEffort, token) {
7149
+ if (!triggerUsesModelOptions(ctx)) return;
6944
7150
  const effortSelect = ctx.backdrop.querySelector('#trig-edit-effort');
6945
7151
  if (!effortSelect) return;
6946
7152
  setAgentSelectLoading(effortSelect, currentEffort, currentEffort ? `${currentEffort} (${t('common.loadingSuffix')})` : t('common.loading'));
6947
7153
  try {
6948
7154
  let efforts = await agentFetchOptions('effort', ctx.original.agentAid, { baseagent, model, effort: currentEffort || 'auto' });
6949
- if (_triggerEdit !== ctx || ctx.optionLoadToken !== token) return;
7155
+ if (_triggerEdit !== ctx || ctx.optionLoadToken !== token || !triggerUsesModelOptions(ctx)) return;
6950
7156
  effortSelect.innerHTML = agentSelectOptions(efforts, currentEffort, t('common.inherited'));
6951
7157
  } catch (e) {
6952
- if (_triggerEdit === ctx && ctx.optionLoadToken === token) {
7158
+ if (_triggerEdit === ctx && ctx.optionLoadToken === token && triggerUsesModelOptions(ctx)) {
6953
7159
  const fallback = [];
6954
7160
  effortSelect.innerHTML = agentSelectOptions(fallback, currentEffort, t('common.inherited'));
6955
7161
  toast(e.message || String(e), true);
6956
7162
  }
6957
7163
  } finally {
6958
- if (_triggerEdit === ctx && ctx.optionLoadToken === token) effortSelect.disabled = Boolean(ctx.saving);
7164
+ if (_triggerEdit === ctx && ctx.optionLoadToken === token && triggerUsesModelOptions(ctx)) effortSelect.disabled = Boolean(ctx.saving);
6959
7165
  }
6960
7166
  }
6961
7167
 
@@ -7498,12 +7704,23 @@ function openTriggerEditDrawer(id, agent) {
7498
7704
  triggerRefreshEventDatalists(ctx);
7499
7705
  triggerUpdateFilterJsonPreview(ctx);
7500
7706
  });
7501
- backdrop.querySelector('#trig-edit-type').addEventListener('change', () => syncTriggerEditFields(ctx));
7707
+ backdrop.querySelector('#trig-edit-type').addEventListener('change', () => {
7708
+ syncTriggerEditFields(ctx);
7709
+ if (!triggerUsesModelOptions(ctx)) {
7710
+ ctx.optionLoadToken = (ctx.optionLoadToken || 0) + 1;
7711
+ return;
7712
+ }
7713
+ syncTriggerOptionFields(ctx, {
7714
+ ...ctx.original,
7715
+ model: backdrop.querySelector('#trig-edit-model')?.value || '',
7716
+ effort: backdrop.querySelector('#trig-edit-effort')?.value || '',
7717
+ });
7718
+ });
7502
7719
  backdrop.querySelector('#trig-edit-delivery-enabled').addEventListener('change', () => syncTriggerEditFields(ctx));
7503
7720
  backdrop.querySelector('#trig-edit-model').addEventListener('change', () => {
7504
7721
  const token = (ctx.optionLoadToken || 0) + 1;
7505
7722
  ctx.optionLoadToken = token;
7506
- syncTriggerEffortOptions(ctx, ctx.original.baseagent || 'claude', backdrop.querySelector('#trig-edit-model')?.value || '', backdrop.querySelector('#trig-edit-effort')?.value || '', token);
7723
+ syncTriggerEffortOptions(ctx, ctx.original.baseagent || configuredActiveBaseagent() || '', backdrop.querySelector('#trig-edit-model')?.value || '', backdrop.querySelector('#trig-edit-effort')?.value || '', token);
7507
7724
  });
7508
7725
  backdrop.querySelector('#trig-edit-session-strategy').addEventListener('change', () => {
7509
7726
  const strategy = backdrop.querySelector('#trig-edit-session-strategy')?.value;
@@ -7542,7 +7759,7 @@ function openTriggerEditDrawer(id, agent) {
7542
7759
  triggerEnsureEventCatalog(ctx).catch(e => {
7543
7760
  if (_triggerEdit === ctx) toast(e.message || String(e), true);
7544
7761
  });
7545
- syncTriggerOptionFields(ctx, original);
7762
+ if (triggerUsesModelOptions(ctx)) syncTriggerOptionFields(ctx, original);
7546
7763
  setTimeout(() => backdrop.querySelector('#trig-edit-name')?.focus(), 0);
7547
7764
  }
7548
7765
 
@@ -10374,7 +10591,7 @@ function createRoleCard(roleName, roleDef) {
10374
10591
  const icon = ROLE_ICONS[roleName] || '📋';
10375
10592
  const nameKey = ROLE_NAMES[roleName] || roleName;
10376
10593
  const permMode = roleDef.permissions?.permissionMode?.default || '—';
10377
- const model = roleDef.permissions?.['baseagents.claude.model']?.default || '—';
10594
+ const model = getRoleModelPermission(roleDef)?.default || '—';
10378
10595
  const commandStats = getCommandPermissionStats(roleDef.commandPermissions);
10379
10596
 
10380
10597
  // Management identities and built-in user roles are both protected from
@@ -10543,15 +10760,29 @@ function initRoleDefinitionsTab() {
10543
10760
  });
10544
10761
  }
10545
10762
 
10546
- const CLAUDE_MODEL_PERMISSION_KEY = 'baseagents.claude.model';
10547
- const CLAUDE_ALLOWED_MODELS_LEGACY_KEY = 'baseagents.claude.allowedModels';
10548
- const DEFAULT_MODEL_PATTERN_OPTIONS = ['*', 'claude-opus-*', 'claude-sonnet-*', 'claude-haiku-*'];
10763
+ function roleModelPermissionKey() {
10764
+ return state.roleDefinitions?.modelPermissionField || null;
10765
+ }
10549
10766
 
10550
- function getClaudeModelPermission(roleDef) {
10551
- const perm = roleDef?.permissions?.[CLAUDE_MODEL_PERMISSION_KEY];
10767
+ function getRoleModelPermission(roleDef, field = roleModelPermissionKey()) {
10768
+ const perm = roleDef?.permissions?.[field];
10552
10769
  return perm && typeof perm === 'object' ? perm : null;
10553
10770
  }
10554
10771
 
10772
+ function rolePermissionMeta(field) {
10773
+ const descriptor = state.roleDefinitions?.permissionFields?.find(item => item.value === field);
10774
+ if (!descriptor) return null;
10775
+ return {
10776
+ label: descriptor.label,
10777
+ type: descriptor.valueType === 'enum' ? 'select' : 'text',
10778
+ options: descriptor.options || [],
10779
+ };
10780
+ }
10781
+
10782
+ function isRoleModelPermissionField(field) {
10783
+ return /^baseagents\.[a-z0-9_-]+\.(?:model|allowedModels)$/.test(field);
10784
+ }
10785
+
10555
10786
  function normalizeAllowedModels(value) {
10556
10787
  if (!Array.isArray(value)) return [];
10557
10788
  const seen = new Set();
@@ -10585,17 +10816,21 @@ function modelAllowedByPatterns(model, allowedModels) {
10585
10816
  }
10586
10817
 
10587
10818
  function renderRoleModelPermissionSection(roleDef) {
10588
- const perm = getClaudeModelPermission(roleDef);
10589
- if (!perm) return '';
10819
+ const field = roleModelPermissionKey();
10820
+ if (!field) return '';
10821
+ const existing = getRoleModelPermission(roleDef, field);
10822
+ const perm = existing || {};
10590
10823
 
10591
10824
  const allowedModels = normalizeAllowedModels(perm.allowedModels);
10592
10825
  const selectionMode = inferModelSelectionMode(allowedModels);
10593
- const patternOptions = DEFAULT_MODEL_PATTERN_OPTIONS;
10826
+ const patternOptions = state.roleDefinitions?.modelPatternOptions || ['*'];
10594
10827
 
10595
10828
  return `
10596
- <div class="form-group perm-group model-permissions-panel" data-model-permissions>
10829
+ <div class="form-group perm-group model-permissions-panel" data-model-permissions data-model-key="${esc(field)}" data-model-baseagent="${esc(state.roleDefinitions.baseagent)}">
10830
+ ${existing ? '' : `<label><input type="checkbox" data-model-enable> ${currentLang === 'zh-CN' ? '设置模型策略' : 'Set model policy'}</label>`}
10831
+ <fieldset data-model-editor${existing ? '' : ' disabled hidden'} style="border:0;padding:0;margin:0;min-width:0">
10597
10832
  <div class="perm-header">
10598
- <label class="perm-label">${esc(t('roleDefs.model'))}</label>
10833
+ <label class="perm-label">${esc(state.roleDefinitions.baseagent)} · ${esc(t('roleDefs.model'))}</label>
10599
10834
  <label class="perm-override">
10600
10835
  <input type="checkbox"
10601
10836
  data-model-field="allowOverride"
@@ -10657,13 +10892,14 @@ function renderRoleModelPermissionSection(roleDef) {
10657
10892
  </div>
10658
10893
 
10659
10894
  <div class="model-preview" data-model-preview>Loading preview...</div>
10895
+ </fieldset>
10660
10896
  </div>
10661
10897
  `;
10662
10898
  }
10663
10899
 
10664
- async function fetchRoleModelPermissionData(roleName) {
10900
+ async function fetchRoleModelPermissionData(roleName, baseagent) {
10665
10901
  const token = localStorage.getItem(TOKEN_KEY);
10666
- const res = await authFetch(roleDefinitionsApi(`api/role-definitions/${encodeURIComponent(roleName)}/configurable-models`), {
10902
+ const res = await authFetch(roleDefinitionsApi(`api/role-definitions/${encodeURIComponent(roleName)}/configurable-models?baseagent=${encodeURIComponent(baseagent)}`), {
10667
10903
  headers: { 'Authorization': `Bearer ${token}` }
10668
10904
  });
10669
10905
  if (!res.ok) throw new Error('Failed to load role model permissions');
@@ -10671,7 +10907,9 @@ async function fetchRoleModelPermissionData(roleName) {
10671
10907
  return json.data;
10672
10908
  }
10673
10909
 
10674
- async function fetchModelCatalogData(baseagent = 'claude', aid = '') {
10910
+ async function fetchModelCatalogData(baseagent = null, aid = '') {
10911
+ baseagent = baseagent || configuredActiveBaseagent();
10912
+ if (!baseagent) throw new Error(t('sessions.baseagentNone'));
10675
10913
  const token = localStorage.getItem(TOKEN_KEY);
10676
10914
  const res = await authFetch(`api/models/catalog?baseagent=${encodeURIComponent(baseagent)}${aid ? `&aid=${encodeURIComponent(aid)}` : ''}`, {
10677
10915
  headers: { 'Authorization': `Bearer ${token}` }
@@ -10766,11 +11004,14 @@ function collectRoleModelPermission(container) {
10766
11004
  ? container
10767
11005
  : container.querySelector('[data-model-permissions]');
10768
11006
  if (!section) return null;
11007
+ const enable = section.querySelector('[data-model-enable]');
11008
+ if (enable && !enable.checked) return null;
10769
11009
 
10770
11010
  const defaultInput = section.querySelector('[data-model-field="default"]');
10771
11011
  const overrideInput = section.querySelector('[data-model-field="allowOverride"]');
10772
11012
 
10773
11013
  return {
11014
+ field: section.dataset.modelKey,
10774
11015
  default: defaultInput?.value.trim() || '',
10775
11016
  allowOverride: !!overrideInput?.checked,
10776
11017
  allowedModels: getRoleAllowedModelsFromList(section)
@@ -10834,14 +11075,14 @@ async function initRoleModelPermissionEditor(container, roleName, roleDef) {
10834
11075
  const section = container.querySelector('[data-model-permissions]');
10835
11076
  if (!section) return;
10836
11077
 
10837
- const initialAllowed = normalizeAllowedModels(getClaudeModelPermission(roleDef)?.allowedModels);
11078
+ const initialAllowed = normalizeAllowedModels(getRoleModelPermission(roleDef, section.dataset.modelKey)?.allowedModels);
10838
11079
  let catalogModels = [];
10839
11080
  section._initialAllowedModels = initialAllowed;
10840
11081
 
10841
11082
  try {
10842
11083
  const data = roleName && roleName !== '__new__'
10843
- ? await fetchRoleModelPermissionData(roleName)
10844
- : await fetchModelCatalogData('claude', roleDefinitionsCurrentAgent || state.roleDefinitions?.aid || '');
11084
+ ? await fetchRoleModelPermissionData(roleName, section.dataset.modelBaseagent)
11085
+ : await fetchModelCatalogData(section.dataset.modelBaseagent, roleDefinitionsCurrentAgent || state.roleDefinitions?.aid || '');
10845
11086
  catalogModels = data?.catalog?.models || data?.models || [];
10846
11087
  } catch (err) {
10847
11088
  console.warn('[roleDefinitions] Failed to load model catalog:', err);
@@ -10880,6 +11121,11 @@ async function initRoleModelPermissionEditor(container, roleName, roleDef) {
10880
11121
 
10881
11122
  section.addEventListener('change', (e) => {
10882
11123
  const target = e.target;
11124
+ if (target?.matches?.('[data-model-enable]')) {
11125
+ const editor = section.querySelector('[data-model-editor]');
11126
+ editor.disabled = !target.checked;
11127
+ editor.hidden = !target.checked;
11128
+ }
10883
11129
  if (target?.classList?.contains('model-pattern-checkbox') || target?.classList?.contains('model-explicit-checkbox')) {
10884
11130
  const allowed = new Set(getRoleAllowedModelsFromList(section));
10885
11131
  if (target.checked) allowed.add(target.value);
@@ -10914,8 +11160,6 @@ async function showNewRoleModal() {
10914
11160
  description: '',
10915
11161
  permissions: {
10916
11162
  permissionMode: { default: 'request', allowOverride: false },
10917
- 'baseagents.claude.model': { default: 'claude-sonnet-4', allowOverride: false, allowedModels: ['claude-sonnet-*', 'claude-haiku-*'] },
10918
- dispatch: { default: 'mention', allowOverride: false }
10919
11163
  },
10920
11164
  commandPermissions: {
10921
11165
  'category:read': { allow: true },
@@ -11051,9 +11295,9 @@ async function showNewRoleModal() {
11051
11295
  body.innerHTML += renderRoleModelPermissionSection(defaultDef);
11052
11296
 
11053
11297
  Object.keys(permissions).forEach(permKey => {
11054
- if (permKey === CLAUDE_MODEL_PERMISSION_KEY || permKey === CLAUDE_ALLOWED_MODELS_LEGACY_KEY) return;
11298
+ if (isRoleModelPermissionField(permKey)) return;
11055
11299
  const perm = permissions[permKey];
11056
- const meta = permissionMeta[permKey] || { label: permKey, type: 'text' };
11300
+ const meta = rolePermissionMeta(permKey) || permissionMeta[permKey] || { label: permKey, type: 'text' };
11057
11301
 
11058
11302
  body.innerHTML += `
11059
11303
  <div class="form-group perm-group">
@@ -11279,9 +11523,9 @@ async function showRoleEditModal(roleName, roleDef) {
11279
11523
  formHtml += renderRoleModelPermissionSection(roleDef);
11280
11524
 
11281
11525
  Object.keys(permissions).forEach(permKey => {
11282
- if (permKey === CLAUDE_MODEL_PERMISSION_KEY || permKey === CLAUDE_ALLOWED_MODELS_LEGACY_KEY) return;
11526
+ if (isRoleModelPermissionField(permKey)) return;
11283
11527
  const perm = permissions[permKey];
11284
- const meta = permissionMeta[permKey] || { label: permKey, type: 'text' };
11528
+ const meta = rolePermissionMeta(permKey) || permissionMeta[permKey] || { label: permKey, type: 'text' };
11285
11529
 
11286
11530
  formHtml += `
11287
11531
  <div class="form-group perm-group">
@@ -11525,8 +11769,6 @@ async function saveRoleDefinition() {
11525
11769
  description: '',
11526
11770
  permissions: {
11527
11771
  permissionMode: { default: 'request', allowOverride: false },
11528
- [CLAUDE_MODEL_PERMISSION_KEY]: { default: 'claude-sonnet-4', allowOverride: false, allowedModels: ['claude-sonnet-*', 'claude-haiku-*'] },
11529
- dispatch: { default: 'mention', allowOverride: false }
11530
11772
  },
11531
11773
  commandPermissions: {
11532
11774
  'category:read': { allow: true },
@@ -11546,7 +11788,7 @@ async function saveRoleDefinition() {
11546
11788
  const allowAccess = allowAccessSelect ? (allowAccessSelect.value === 'true') : true;
11547
11789
 
11548
11790
  // 收集所有权限配置
11549
- const permissions = {};
11791
+ const permissions = { ...(currentDef.permissions || {}) };
11550
11792
 
11551
11793
  // 获取所有权限项的键
11552
11794
  const permKeys = new Set();
@@ -11604,13 +11846,13 @@ async function saveRoleDefinition() {
11604
11846
  alert(modelValidationError);
11605
11847
  return;
11606
11848
  }
11607
- permissions[CLAUDE_MODEL_PERMISSION_KEY] = {
11608
- ...(currentDef.permissions?.[CLAUDE_MODEL_PERMISSION_KEY] || {}),
11849
+ permissions[modelPermission.field] = {
11850
+ ...(currentDef.permissions?.[modelPermission.field] || {}),
11609
11851
  default: modelPermission.default,
11610
11852
  allowOverride: modelPermission.allowOverride,
11611
11853
  allowedModels: modelPermission.allowedModels
11612
11854
  };
11613
- delete permissions[CLAUDE_MODEL_PERMISSION_KEY].selectionMode;
11855
+ delete permissions[modelPermission.field].selectionMode;
11614
11856
  }
11615
11857
 
11616
11858
  const commandPermissionsResult = collectCommandPermissions(body, currentDef.commandPermissions);