ec-web 0.0.22 → 0.0.23

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.
@@ -11,6 +11,7 @@
11
11
  error: '',
12
12
  openAgents: new Set(['claude']),
13
13
  catalogs: Object.create(null),
14
+ catalogStatus: Object.create(null),
14
15
  changes: { daemon: new Map(), defaults: new Map() },
15
16
  mapDrafts: new Map(),
16
17
  serviceMetadataDrafts: new Map(),
@@ -29,7 +30,7 @@
29
30
  ecagent: 'EC Agent',
30
31
  };
31
32
  const baseagentMarks = { claude: 'C', codex: 'X', gemini: 'G', ecagent: 'E' };
32
- const effortOptions = ['low', 'medium', 'high', 'xhigh', 'max'];
33
+ const effortOptions = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
33
34
 
34
35
  const messages = {
35
36
  'zh-CN': {
@@ -146,6 +147,9 @@
146
147
  'basicSettings.queryParamsHelp': '每行一个 key = value,将附加到 Base Agent 请求 URL。',
147
148
  'basicSettings.readonly': '只读',
148
149
  'basicSettings.redetect': '重新检测',
150
+ 'basicSettings.detectAdded': '检测到新的 Base Agent:{value}。是否加入默认配置?',
151
+ 'basicSettings.detectRemoved': '检测到已移除的 Base Agent:{value}。是否从默认配置删除?',
152
+ 'basicSettings.detectUpdated': 'Base Agent 配置已更新',
149
153
  'basicSettings.reload': '重新读取',
150
154
  'basicSettings.reloadConfirm': '重新读取会放弃未保存的修改,是否继续?',
151
155
  'basicSettings.remove': '删除',
@@ -345,6 +349,9 @@
345
349
  'basicSettings.queryParamsHelp': 'One key = value per line, appended to Base Agent request URLs.',
346
350
  'basicSettings.readonly': 'Read only',
347
351
  'basicSettings.redetect': 'Detect again',
352
+ 'basicSettings.detectAdded': 'New Base Agent detected: {value}. Add it to the default configuration?',
353
+ 'basicSettings.detectRemoved': 'Base Agent no longer detected: {value}. Remove it from the default configuration?',
354
+ 'basicSettings.detectUpdated': 'Base Agent configuration updated',
348
355
  'basicSettings.reload': 'Reload',
349
356
  'basicSettings.reloadConfirm': 'Reloading discards unsaved changes. Continue?',
350
357
  'basicSettings.remove': 'Remove',
@@ -578,6 +585,11 @@
578
585
  }).join('');
579
586
  }
580
587
 
588
+ function catalogEfforts(agent, model) {
589
+ const entries = state.catalogs[agent] || [];
590
+ return entries.find(entry => entry.id === model)?.reasoningLevels || [];
591
+ }
592
+
581
593
  function modelOptions(agent, value) {
582
594
  const models = state.catalogs[agent] || [];
583
595
  const ids = models.map((item) => typeof item === 'string' ? item : item.id).filter(Boolean);
@@ -585,7 +597,11 @@
585
597
  if (!ids.length) return value
586
598
  ? `<option selected value="${htmlEscape(value)}">${htmlEscape(value)}</option>`
587
599
  : `<option value="" selected disabled>${htmlEscape(t('basicSettings.loading'))}</option>`;
588
- return ids.map((id) => `<option value="${htmlEscape(id)}"${id === value ? ' selected' : ''}>${htmlEscape(id)}</option>`).join('');
600
+ return ids.map((id) => {
601
+ const entry = models.find(item => item.id === id);
602
+ const suffix = !entry ? ' [current, not listed]' : entry.verification === 'unverified' ? ' [unverified]' : '';
603
+ return `<option value="${htmlEscape(id)}"${id === value ? ' selected' : ''}>${htmlEscape(id + suffix)}</option>`;
604
+ }).join('');
589
605
  }
590
606
 
591
607
  // Match the CLI watcher's shortLogName() so legacy rotated config values
@@ -659,7 +675,7 @@
659
675
 
660
676
  function fallbackModelsMarkup(agent, config) {
661
677
  const rows = Array.isArray(config?.fallbackModels) ? config.fallbackModels : [];
662
- const body = rows.map((row, index) => `<div class="basic-settings-list-row" data-bs-fallback-row data-agent="${agent}" data-index="${index}"><select class="basic-settings-control" data-bs-fallback-model>${modelOptions(agent, row?.model || '')}</select><select class="basic-settings-control" data-bs-fallback-effort>${selectOptions(effortOptions, row?.effort || 'medium')}</select><button type="button" class="basic-settings-remove" data-bs-remove-fallback aria-label="${htmlEscape(t('basicSettings.remove'))}">×</button></div>`).join('');
678
+ const body = rows.map((row, index) => `<div class="basic-settings-list-row" data-bs-fallback-row data-agent="${agent}" data-index="${index}"><select class="basic-settings-control" data-bs-fallback-model>${modelOptions(agent, row?.model || '')}</select><select class="basic-settings-control" data-bs-fallback-effort>${selectOptions(['', ...catalogEfforts(agent, row?.model)], row?.effort || '', { '': 'Auto' })}</select><button type="button" class="basic-settings-remove" data-bs-remove-fallback aria-label="${htmlEscape(t('basicSettings.remove'))}">×</button></div>`).join('');
663
679
  return `<div class="basic-settings-field full" data-bs-field data-path="baseagents.${agent}.fallbackModels" data-bs-fallback-list data-agent="${agent}">${fieldLabel(t('basicSettings.fallbackModels'), `baseagents.${agent}.fallbackModels[]`, t('basicSettings.fallbackHelp'), null)}<div class="basic-settings-list">${body}<button type="button" class="basic-settings-add" data-bs-add-fallback data-agent="${agent}">+ ${htmlEscape(t('basicSettings.addFallback'))}</button></div><div class="basic-settings-error"></div><div class="basic-settings-help">${htmlEscape(t('basicSettings.fallbackHelp'))}</div></div>`;
664
680
  }
665
681
 
@@ -705,15 +721,17 @@
705
721
  : t('basicSettings.configuredUndetected');
706
722
  const common = (name) => `baseagents.${agent}.${name}`;
707
723
  const model = scalarField({ file: 'defaults', path: common('model'), label: t('basicSettings.model'), type: 'model', value: config.model, optional: true });
708
- const effort = agent === 'gemini' ? '' : scalarField({ file: 'defaults', path: common('effort'), label: t('basicSettings.effort'), type: 'select', options: effortOptions, value: config.effort });
724
+ const effort = agent === 'gemini' ? '' : scalarField({ file: 'defaults', path: common('effort'), label: t('basicSettings.effort'), type: 'select', options: catalogEfforts(agent, config.model), value: config.effort });
709
725
  const auxiliary = scalarField({ file: 'defaults', path: common('auxiliaryModel'), label: t('basicSettings.auxiliaryModel'), type: 'model', value: config.auxiliaryModel, help: t('basicSettings.auxiliaryHelp') });
710
- const auxiliaryEffort = scalarField({ file: 'defaults', path: common('auxiliaryEffort'), label: t('basicSettings.auxiliaryEffort'), type: 'select', options: effortOptions, value: config.auxiliaryEffort });
726
+ const auxiliaryEffort = scalarField({ file: 'defaults', path: common('auxiliaryEffort'), label: t('basicSettings.auxiliaryEffort'), type: 'select', options: catalogEfforts(agent, config.auxiliaryModel), value: config.auxiliaryEffort });
711
727
  const connection = scalarField({ file: 'defaults', path: common('apiKey'), label: t('basicSettings.apiKey'), value: config.apiKey, sensitive: true, placeholder: t('basicSettings.apiKeyPlaceholder') }) +
712
728
  (agent === 'gemini' ? '' : scalarField({ file: 'defaults', path: common('baseUrl'), label: t('basicSettings.baseUrl'), value: config.baseUrl, placeholder: 'https://api.example.com' })) +
713
729
  (agent === 'gemini' ? '' : textAreaField({ file: 'defaults', path: common('headers'), label: t('basicSettings.headers'), value: config.headers, placeholder: t('basicSettings.keyValuePlaceholder'), help: t('basicSettings.headersHelp'), full: false })) +
714
730
  (['claude', 'codex', 'ecagent'].includes(agent) ? textAreaField({ file: 'defaults', path: common('queryParams'), label: t('basicSettings.queryParams'), value: config.queryParams, placeholder: t('basicSettings.keyValuePlaceholder'), help: t('basicSettings.queryParamsHelp'), full: false }) : '');
731
+ const catalogNote = state.catalogStatus[agent];
732
+ const catalogHint = catalogNote ? `${catalogNote.source} / ${catalogNote.freshness}${catalogNote.diagnostics?.length ? ': ' + catalogNote.diagnostics.map(d => d.message).join('; ') : ''}` : t('basicSettings.defaultFirst');
715
733
  const body = section(t('basicSettings.connection'), t('basicSettings.secretRedacted'), formGrid(connection)) +
716
- section(t('basicSettings.modelChain'), t('basicSettings.defaultFirst'), formGrid(model + effort + fallbackModelsMarkup(agent, config) + auxiliary + auxiliaryEffort)) +
734
+ section(t('basicSettings.modelChain'), catalogHint, formGrid(model + effort + fallbackModelsMarkup(agent, config) + auxiliary + auxiliaryEffort)) +
717
735
  section(t('basicSettings.specific'), `${baseagentLabels[agent]} runner`, agentSpecificMarkup(agent, config));
718
736
  return `<article class="basic-settings-agent${active ? ' active' : ''}${open ? ' open' : ''}${details.detected ? '' : ' unavailable'}" data-bs-agent="${agent}"><header class="basic-settings-agent-head" data-bs-agent-toggle><span class="basic-settings-agent-mark">${baseagentMarks[agent]}</span><span class="basic-settings-agent-title"><strong>${htmlEscape(baseagentLabels[agent])}</strong><small>${htmlEscape(details.detected ? t('basicSettings.detected') : t('basicSettings.notDetected'))} · ${htmlEscape(source)}</small></span><label class="basic-settings-agent-default"><input type="radio" name="basic-active-baseagent" value="${agent}" data-bs-active-baseagent${active ? ' checked' : ''}${details.detected ? '' : ' disabled'}>${htmlEscape(t('basicSettings.defaultBaseagent'))}</label><button type="button" class="basic-settings-chevron" data-bs-agent-toggle aria-expanded="${open ? 'true' : 'false'}" aria-label="${htmlEscape(t('basicSettings.expandCollapse'))}"></button></header><div class="basic-settings-agent-body">${body}</div></article>`;
719
737
  }
@@ -1072,7 +1090,7 @@
1072
1090
  if (force && state.changes.daemon.size + state.changes.defaults.size > 0 && !window.confirm(t('basicSettings.reloadConfirm'))) return;
1073
1091
  state.loading = true; state.error = ''; render();
1074
1092
  try {
1075
- const response = await fetch(authApiUrl('api/basic-settings'));
1093
+ const response = await authFetch('api/basic-settings');
1076
1094
  const payload = await response.json();
1077
1095
  if (!response.ok || !payload.success) throw new Error(payload.error || t('basicSettings.loadFailed'));
1078
1096
  state.snapshot = payload.data;
@@ -1095,9 +1113,12 @@
1095
1113
  async function loadCatalogs(agents) {
1096
1114
  await Promise.all(agents.map(async (item) => {
1097
1115
  try {
1098
- const response = await fetch(authApiUrl(`api/models/catalog?baseagent=${encodeURIComponent(item.id)}`));
1116
+ const response = await authFetch(`api/models/catalog?baseagent=${encodeURIComponent(item.id)}`);
1099
1117
  const payload = await response.json();
1100
- if (response.ok && payload.success) state.catalogs[item.id] = payload.data.models || [];
1118
+ if (response.ok && payload.success) {
1119
+ state.catalogs[item.id] = payload.data.models || [];
1120
+ state.catalogStatus[item.id] = payload.data;
1121
+ }
1101
1122
  } catch {}
1102
1123
  }));
1103
1124
  if (state.snapshot && !state.loading) render();
@@ -1105,15 +1126,61 @@
1105
1126
 
1106
1127
  async function redetect() {
1107
1128
  if (!state.snapshot || state.detecting) return;
1129
+ if (state.changes.daemon.size + state.changes.defaults.size > 0) {
1130
+ if (!window.confirm(t('basicSettings.reloadConfirm'))) return;
1131
+ state.changes.daemon.clear();
1132
+ state.changes.defaults.clear();
1133
+ state.mapDrafts.clear();
1134
+ state.serviceMetadataDrafts.clear();
1135
+ state.restartPaths = [];
1136
+ clearNotice();
1137
+ }
1108
1138
  state.detecting = true;
1109
1139
  state.detectionError = '';
1110
1140
  render();
1111
1141
  try {
1112
- const response = await fetch(authApiUrl('api/basic-settings'));
1142
+ const response = await authFetch('api/basic-settings');
1113
1143
  const payload = await response.json();
1114
1144
  if (!response.ok || !payload.success) throw new Error(payload.error || t('basicSettings.loadFailed'));
1115
1145
  const next = payload.data;
1116
- state.snapshot.baseagents = next.baseagents || [];
1146
+ const previous = state.snapshot;
1147
+ const previousById = new Map((previous.baseagents || []).map((item) => [item.id, item]));
1148
+ const nextBaseagents = next.baseagents || [];
1149
+ const added = nextBaseagents.filter((item) => item.detected && !previousById.get(item.id)?.configured);
1150
+ const active = getFileValue('defaults', 'active_baseagent');
1151
+ const removed = (previous.baseagents || []).filter((item) => item.configured && !nextBaseagents.find((candidate) => candidate.id === item.id && candidate.detected));
1152
+ const changes = [];
1153
+ for (const item of added) {
1154
+ if (window.confirm(tf('basicSettings.detectAdded', { value: baseagentLabels[item.id] || item.id }))) {
1155
+ changes.push({ path: `baseagents.${item.id}`, op: 'set', value: {} });
1156
+ }
1157
+ }
1158
+ for (const item of removed) {
1159
+ if (window.confirm(tf('basicSettings.detectRemoved', { value: baseagentLabels[item.id] || item.id }))) {
1160
+ // active_baseagent cannot be left pointing at a removed block. Switch
1161
+ // to the first still-detected agent in the same atomic PATCH.
1162
+ if (item.id === active) {
1163
+ const replacement = nextBaseagents.find((candidate) => candidate.detected && candidate.id !== item.id);
1164
+ if (!replacement) continue;
1165
+ changes.push({ path: 'active_baseagent', op: 'set', value: replacement.id });
1166
+ }
1167
+ changes.push({ path: `baseagents.${item.id}`, op: 'unset' });
1168
+ }
1169
+ }
1170
+ if (changes.length) {
1171
+ const response2 = await authFetch('api/basic-settings', {
1172
+ method: 'PATCH',
1173
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY) || ''}` },
1174
+ body: JSON.stringify({ file: 'defaults', revision: previous.revisions.defaults, changes }),
1175
+ });
1176
+ const saved = await response2.json();
1177
+ if (!response2.ok || !saved.success) throw new Error(saved.error || t('basicSettings.saveFailed'));
1178
+ state.snapshot = saved.data;
1179
+ state.notice = t('basicSettings.detectUpdated');
1180
+ state.noticeType = 'success';
1181
+ } else {
1182
+ state.snapshot.baseagents = nextBaseagents;
1183
+ }
1117
1184
  state.snapshot.logTypes = next.logTypes || [];
1118
1185
  state.snapshot.logTypeCounts = next.logTypeCounts || {};
1119
1186
  const detected = state.snapshot.baseagents.filter((item) => item.detected);
@@ -1274,7 +1341,7 @@
1274
1341
  updateBasicSettingsStatus();
1275
1342
  return;
1276
1343
  }
1277
- rows.push({ model: state.catalogs[agent]?.[0]?.id || '', effort: 'medium' });
1344
+ rows.push({ model: state.catalogs[agent]?.[0]?.id || '' });
1278
1345
  recordChange('defaults', `baseagents.${agent}.fallbackModels`, 'set', rows);
1279
1346
  render();
1280
1347
  }
@@ -1374,7 +1441,7 @@
1374
1441
  const restartPaths = [];
1375
1442
  for (const file of files) {
1376
1443
  const changes = [...state.changes[file]].map(([path, item]) => ({ path, ...item }));
1377
- const response = await fetch(authApiUrl('api/basic-settings'), {
1444
+ const response = await authFetch('api/basic-settings', {
1378
1445
  method: 'PATCH',
1379
1446
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY) || ''}` },
1380
1447
  body: JSON.stringify({ file, revision: state.snapshot.revisions[file], changes }),
@@ -1497,7 +1564,7 @@
1497
1564
  return;
1498
1565
  }
1499
1566
  secretToggle.disabled = true;
1500
- fetch(authApiUrl(`api/basic-settings/secret?file=${encodeURIComponent(file)}&path=${encodeURIComponent(fieldPath)}`))
1567
+ authFetch(`api/basic-settings/secret?file=${encodeURIComponent(file)}&path=${encodeURIComponent(fieldPath)}`)
1501
1568
  .then(async (response) => {
1502
1569
  const payload = await readApiJson(response, t('basicSettings.revealSecretFailed'));
1503
1570
  if (!response.ok || !payload.success || typeof payload.data?.value !== 'string') {
@@ -49,7 +49,7 @@
49
49
 
50
50
  </nav>
51
51
  <span style="flex:1"></span>
52
- 2026-09-05 04:51
52
+ 2026-09-09 03:48
53
53
  <span id="conn-status" class="conn-status" data-i18n="status.connecting">连接中…</span>
54
54
  <button id="lang-btn" class="theme-btn" data-i18n-title="language.buttonTitle" title="切换语言">🌐</button>
55
55
  <button id="theme-btn" class="theme-btn" data-i18n-title="theme.buttonTitle" title="主题">🌗</button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ec-web",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "Web console and monitoring dashboard for EvolCore",
5
5
  "type": "module",
6
6
  "bin": {