agim-cli 1.0.4 → 1.0.6

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.
@@ -89,6 +89,45 @@
89
89
  workspaceDeleted: 'Workspace deleted',
90
90
  workspaceIdHelp: 'Letters / digits / _ / - only',
91
91
  workspaceDefaultLocked: 'default (locked)',
92
+
93
+ // ── messengers (extras for v1.0.5) ───────────────────────
94
+ wechatScan: 'Scan to log in',
95
+ wechatScanTitle: 'WeChat — scan to log in',
96
+ wechatGenerating: 'Generating QR code…',
97
+ wechatWaiting: 'Waiting for scan…',
98
+ wechatScanned: 'QR scanned — confirm on your phone…',
99
+ wechatConfirmed: '✓ Logged in (account {account})',
100
+ wechatExpired: 'QR code expired',
101
+ wechatRegen: 'Regenerate',
102
+ wechatClose: 'Close',
103
+ wechatFailed: 'Failed: {error}',
104
+ dingtalk: 'DingTalk',
105
+ dingtalkHint: 'Stream-mode app: Client ID + Client Secret',
106
+ dingtalkClientId: 'Client ID',
107
+ dingtalkClientSecret: 'Client Secret',
108
+ discord: 'Discord',
109
+ discordHint: 'Bot token from Discord Developer Portal',
110
+ discordToken: 'Bot Token',
111
+ discordGuilds: 'Allowed guilds (comma-separated IDs, blank = any)',
112
+ discordChannels: 'Allowed channels (comma-separated IDs, blank = any)',
113
+ configure: 'Configure',
114
+ reconfigure: 'Edit credentials',
115
+
116
+ // ── service control ──────────────────────────────────────
117
+ svcTitle: 'Service',
118
+ svcStateLoading: 'Checking…',
119
+ svcStateRunning: 'Running ({mode}, pid {pid}, up {uptime})',
120
+ svcStateRunningNoUp: 'Running ({mode}, pid {pid})',
121
+ svcStateNone: 'Not running',
122
+ svcStart: 'Start',
123
+ svcStop: 'Stop',
124
+ svcRestart: 'Restart',
125
+ svcConfirmStop: 'Stop Agim? The web console will go offline until you start it again from a terminal.',
126
+ svcConfirmRestart: 'Restart Agim? The web console will reconnect automatically in a few seconds.',
127
+ svcRestarting: 'Restarting — waiting for the service to come back…',
128
+ svcRestarted: '✓ Service restarted',
129
+ svcStopped: '✓ Service stopped (this page is now disconnected)',
130
+ svcFgWarning: 'Foreground service is running in another terminal — restart it there.',
92
131
  },
93
132
  zh: {
94
133
  title: 'Agim — 设置',
@@ -164,6 +203,45 @@
164
203
  workspaceDeleted: '工作区已删除',
165
204
  workspaceIdHelp: '仅支持字母 / 数字 / _ / -',
166
205
  workspaceDefaultLocked: '默认(不可改)',
206
+
207
+ // ── messengers (extras for v1.0.5) ───────────────────────
208
+ wechatScan: '扫码登录',
209
+ wechatScanTitle: '微信 — 扫码登录',
210
+ wechatGenerating: '生成二维码中…',
211
+ wechatWaiting: '等待扫描…',
212
+ wechatScanned: '已扫描,请在手机端确认…',
213
+ wechatConfirmed: '✓ 登录成功(账号 {account})',
214
+ wechatExpired: '二维码已过期',
215
+ wechatRegen: '重新生成',
216
+ wechatClose: '关闭',
217
+ wechatFailed: '失败:{error}',
218
+ dingtalk: '钉钉',
219
+ dingtalkHint: 'Stream 模式应用:Client ID + Client Secret',
220
+ dingtalkClientId: 'Client ID',
221
+ dingtalkClientSecret: 'Client Secret',
222
+ discord: 'Discord',
223
+ discordHint: '在 Discord Developer Portal 获取 Bot Token',
224
+ discordToken: 'Bot Token',
225
+ discordGuilds: '允许的服务器(逗号分隔 ID,空 = 不限)',
226
+ discordChannels: '允许的频道(逗号分隔 ID,空 = 不限)',
227
+ configure: '配置',
228
+ reconfigure: '修改凭据',
229
+
230
+ // ── service control ──────────────────────────────────────
231
+ svcTitle: '服务',
232
+ svcStateLoading: '检查中…',
233
+ svcStateRunning: '运行中({mode},pid {pid},已运行 {uptime})',
234
+ svcStateRunningNoUp: '运行中({mode},pid {pid})',
235
+ svcStateNone: '未运行',
236
+ svcStart: '启动',
237
+ svcStop: '停止',
238
+ svcRestart: '重启',
239
+ svcConfirmStop: '停止 Agim?停止后 web 控制台会离线,需要回终端用 `agim start` 再启动。',
240
+ svcConfirmRestart: '重启 Agim?web 控制台会在几秒后自动重连。',
241
+ svcRestarting: '重启中——等待服务恢复…',
242
+ svcRestarted: '✓ 服务已重启',
243
+ svcStopped: '✓ 服务已停止(页面已断开连接)',
244
+ svcFgWarning: '前台服务在另一个终端运行——回那个终端重启。',
167
245
  },
168
246
  };
169
247
  function t(key) { return T[window.__lang][key] || T.en[key] || key; }
@@ -603,6 +681,7 @@
603
681
  function render() {
604
682
  app.innerHTML = `
605
683
  <h1>${t('h1')}</h1>
684
+ ${renderServiceCard()}
606
685
  ${renderAgentsCard()}
607
686
  ${renderMessengersCard()}
608
687
  ${renderAcpCard()}
@@ -610,6 +689,28 @@
610
689
  ${renderGeneralCard()}
611
690
  `;
612
691
  bindEvents();
692
+ // Service status loads asynchronously; the card renders with a
693
+ // placeholder, populated by the first /api/service/status response.
694
+ void loadServiceStatus();
695
+ }
696
+
697
+ // ==========================================
698
+ // Service control card
699
+ // ==========================================
700
+ function renderServiceCard() {
701
+ return `
702
+ <div class="card">
703
+ <h2>${t('svcTitle')}</h2>
704
+ <div class="status" id="svc-state" style="margin-bottom:14px">
705
+ <span class="dot dot-off"></span>${t('svcStateLoading')}
706
+ </div>
707
+ <div class="actions">
708
+ <button type="button" class="btn btn-primary" id="svc-restart">${t('svcRestart')}</button>
709
+ <button type="button" class="btn" id="svc-stop">${t('svcStop')}</button>
710
+ <button type="button" class="btn" id="svc-start" disabled>${t('svcStart')}</button>
711
+ </div>
712
+ </div>
713
+ `;
613
714
  }
614
715
 
615
716
  // ==========================================
@@ -640,16 +741,24 @@
640
741
  `;
641
742
  }).join('');
642
743
 
744
+ // Default-agent picker only lists agents that are BOTH enabled and
745
+ // installed — defaulting to a missing binary or a disabled adapter
746
+ // both break routing the first time someone sends a message.
747
+ const eligibleDefaults = agents.filter(a => enabledAgents.includes(a) && agentStatus[a]);
748
+
643
749
  return `
644
750
  <div class="card">
645
751
  <h2>${t('agents')} <span class="badge">${agents.length}</span></h2>
646
752
  ${rows}
647
753
  <hr class="divider">
648
754
  <label>${t('defaultAgent')}</label>
649
- <select id="defaultAgent">
650
- ${agents.filter(a => agentStatus[a]).map(a =>
651
- `<option value="${esc(a)}" ${a === defaultAgent ? 'selected' : ''}>${esc(a)}</option>`
652
- ).join('')}
755
+ <select id="defaultAgent" ${eligibleDefaults.length === 0 ? 'disabled' : ''}>
756
+ ${eligibleDefaults.length === 0
757
+ ? `<option value="">— ${esc(t('agents'))} —</option>`
758
+ : eligibleDefaults.map(a =>
759
+ `<option value="${esc(a)}" ${a === defaultAgent ? 'selected' : ''}>${esc(a)}</option>`
760
+ ).join('')
761
+ }
653
762
  </select>
654
763
  <div class="actions">
655
764
  <button type="button" class="btn btn-primary" id="saveAgents">${t('saveAgents')}</button>
@@ -665,10 +774,14 @@
665
774
  const messengers = config.messengers || [];
666
775
  const tg = config.telegram || {};
667
776
  const fs = config.feishu || {};
777
+ const dt = config.dingtalk || {};
778
+ const dc = config.discord || {};
668
779
 
669
780
  const wechatEnabled = messengers.includes('wechat-ilink');
670
781
  const telegramEnabled = messengers.includes('telegram');
671
782
  const feishuEnabled = messengers.includes('feishu');
783
+ const dingtalkEnabled = messengers.includes('dingtalk');
784
+ const discordEnabled = messengers.includes('discord');
672
785
 
673
786
  return `
674
787
  <div class="card">
@@ -685,7 +798,11 @@
685
798
  </div>
686
799
  <div class="toggle ${wechatEnabled ? 'active' : ''}" data-toggle-messenger="wechat-ilink"></div>
687
800
  </div>
688
- ${wechatEnabled ? `<div class="hint-box">${t('wechatBox')}</div>` : ''}
801
+ ${wechatEnabled ? `
802
+ <div class="actions">
803
+ <button type="button" class="btn" id="wechatScanBtn">${t('wechatScan')}</button>
804
+ </div>
805
+ ` : ''}
689
806
  </div>
690
807
 
691
808
  <!-- Telegram -->
@@ -714,7 +831,7 @@
714
831
  </div>
715
832
 
716
833
  <!-- Feishu -->
717
- <div>
834
+ <div style="margin-bottom:16px">
718
835
  <div class="agent-row">
719
836
  <div class="left">
720
837
  <div>
@@ -738,6 +855,66 @@
738
855
  ` : ''}
739
856
  </div>
740
857
 
858
+ <!-- DingTalk -->
859
+ <div style="margin-bottom:16px">
860
+ <div class="agent-row">
861
+ <div class="left">
862
+ <div>
863
+ <div class="name">${t('dingtalk')}</div>
864
+ <div class="hint">${t('dingtalkHint')}</div>
865
+ </div>
866
+ </div>
867
+ <div class="toggle ${dingtalkEnabled ? 'active' : ''}" data-toggle-messenger="dingtalk"></div>
868
+ </div>
869
+ ${dingtalkEnabled ? `
870
+ <div class="row">
871
+ <div>
872
+ <label>${t('dingtalkClientId')}</label>
873
+ <input type="text" id="dtClientId" value="${esc(dt.clientId || '')}" placeholder="dingxxxxxx">
874
+ </div>
875
+ <div>
876
+ <label>${t('dingtalkClientSecret')}</label>
877
+ <input type="password" id="dtClientSecret" value="${esc(dt.clientSecret || '')}" placeholder="••••••••">
878
+ </div>
879
+ </div>
880
+ ` : ''}
881
+ </div>
882
+
883
+ <!-- Discord -->
884
+ <div>
885
+ <div class="agent-row">
886
+ <div class="left">
887
+ <div>
888
+ <div class="name">${t('discord')}</div>
889
+ <div class="hint">${t('discordHint')}</div>
890
+ </div>
891
+ </div>
892
+ <div class="toggle ${discordEnabled ? 'active' : ''}" data-toggle-messenger="discord"></div>
893
+ </div>
894
+ ${discordEnabled ? `
895
+ <div class="row">
896
+ <div>
897
+ <label>${t('discordToken')}</label>
898
+ <input type="password" id="dcToken" value="${esc(dc.botToken || '')}" placeholder="••••••••">
899
+ </div>
900
+ <div>
901
+ <label>${t('channelId')}</label>
902
+ <input type="text" id="dcChannel" value="${esc(dc.channelId || '')}" placeholder="default">
903
+ </div>
904
+ </div>
905
+ <div class="row">
906
+ <div>
907
+ <label>${t('discordGuilds')}</label>
908
+ <input type="text" id="dcGuilds" value="${esc((dc.allowedGuilds || []).join(', '))}" placeholder="123, 456">
909
+ </div>
910
+ <div>
911
+ <label>${t('discordChannels')}</label>
912
+ <input type="text" id="dcChannels" value="${esc((dc.allowedChannels || []).join(', '))}" placeholder="789, 101112">
913
+ </div>
914
+ </div>
915
+ ` : ''}
916
+ </div>
917
+
741
918
  <div class="actions">
742
919
  <button type="button" class="btn btn-primary" id="saveMessengers">${t('saveMessengers')}</button>
743
920
  </div>
@@ -1037,19 +1214,109 @@
1037
1214
  // the Reveal buttons re-fetch with ?reveal=1 on demand.
1038
1215
  void loadEnvSection();
1039
1216
 
1040
- // Agent toggles
1217
+ // Service control card
1218
+ document.getElementById('svc-restart')?.addEventListener('click', () => svcAction('restart'));
1219
+ document.getElementById('svc-stop')?.addEventListener('click', () => svcAction('stop'));
1220
+ document.getElementById('svc-start')?.addEventListener('click', () => svcAction('start'));
1221
+
1222
+ // Agent toggles — flip config.agents in memory + auto-manage
1223
+ // defaultAgent (promote / demote as needed), then re-render so the
1224
+ // default-agent dropdown reflects the new eligible set.
1041
1225
  document.querySelectorAll('[data-toggle-agent]').forEach(el => {
1042
- el.addEventListener('click', () => el.classList.toggle('active'));
1226
+ el.addEventListener('click', () => {
1227
+ const id = el.getAttribute('data-toggle-agent');
1228
+ const set = new Set(config.agents || []);
1229
+ if (set.has(id)) {
1230
+ set.delete(id);
1231
+ // If we just removed the default, promote the next still-enabled
1232
+ // agent (or clear). saveConfig() will sync to disk on Save.
1233
+ if (config.defaultAgent === id) {
1234
+ config.defaultAgent = Array.from(set)[0] || '';
1235
+ }
1236
+ } else {
1237
+ set.add(id);
1238
+ // First enabled agent inherits default when nothing was set.
1239
+ if (!config.defaultAgent) config.defaultAgent = id;
1240
+ }
1241
+ config.agents = Array.from(set);
1242
+ render();
1243
+ });
1244
+ });
1245
+
1246
+ // Default-agent dropdown — keep in-memory config in sync so the
1247
+ // Save button persists what the user just picked.
1248
+ document.getElementById('defaultAgent')?.addEventListener('change', (e) => {
1249
+ config.defaultAgent = e.target.value;
1250
+ });
1251
+
1252
+ // Save Agents button — pushes the in-memory agents[] + defaultAgent
1253
+ // to /api/config PUT. Mirrors the saveMessengers pattern.
1254
+ document.getElementById('saveAgents')?.addEventListener('click', async () => {
1255
+ await saveConfig();
1043
1256
  });
1044
1257
 
1045
- // Messenger toggles
1258
+ // Messenger toggles — flip in-memory config.messengers, then re-render.
1046
1259
  document.querySelectorAll('[data-toggle-messenger]').forEach(el => {
1047
1260
  el.addEventListener('click', () => {
1048
- el.classList.toggle('active');
1049
- syncMessengerToggles();
1261
+ const id = el.getAttribute('data-toggle-messenger');
1262
+ const set = new Set(config.messengers || []);
1263
+ if (set.has(id)) set.delete(id);
1264
+ else set.add(id);
1265
+ config.messengers = Array.from(set);
1266
+ render();
1050
1267
  });
1051
1268
  });
1052
1269
 
1270
+ // Save messengers — pull credentials from any visible fields, then PUT.
1271
+ document.getElementById('saveMessengers')?.addEventListener('click', async () => {
1272
+ const get = (id) => document.getElementById(id)?.value?.trim() ?? '';
1273
+ const messengers = (config.messengers || []);
1274
+
1275
+ if (messengers.includes('telegram')) {
1276
+ const botToken = get('tgToken');
1277
+ const channelId = get('tgChannel') || 'default';
1278
+ if (botToken) config.telegram = { ...(config.telegram || {}), botToken, channelId };
1279
+ else if (config.telegram?.channelId !== channelId) config.telegram = { ...(config.telegram || {}), channelId };
1280
+ }
1281
+ if (messengers.includes('feishu')) {
1282
+ const appId = get('fsAppId');
1283
+ const appSecret = get('fsAppSecret');
1284
+ config.feishu = {
1285
+ ...(config.feishu || {}),
1286
+ ...(appId ? { appId } : {}),
1287
+ ...(appSecret ? { appSecret } : {}),
1288
+ };
1289
+ }
1290
+ if (messengers.includes('dingtalk')) {
1291
+ const clientId = get('dtClientId');
1292
+ const clientSecret = get('dtClientSecret');
1293
+ config.dingtalk = {
1294
+ ...(config.dingtalk || {}),
1295
+ ...(clientId ? { clientId } : {}),
1296
+ ...(clientSecret ? { clientSecret } : {}),
1297
+ };
1298
+ }
1299
+ if (messengers.includes('discord')) {
1300
+ const botToken = get('dcToken');
1301
+ const channelId = get('dcChannel') || 'default';
1302
+ const guildsRaw = get('dcGuilds');
1303
+ const channelsRaw = get('dcChannels');
1304
+ config.discord = {
1305
+ ...(config.discord || {}),
1306
+ ...(botToken ? { botToken } : {}),
1307
+ channelId,
1308
+ allowedGuilds: guildsRaw ? guildsRaw.split(',').map(s => s.trim()).filter(Boolean) : undefined,
1309
+ allowedChannels: channelsRaw ? channelsRaw.split(',').map(s => s.trim()).filter(Boolean) : undefined,
1310
+ };
1311
+ }
1312
+ await saveConfig();
1313
+ });
1314
+
1315
+ // WeChat — open QR modal on click.
1316
+ document.getElementById('wechatScanBtn')?.addEventListener('click', () => {
1317
+ openWechatQrModal();
1318
+ });
1319
+
1053
1320
  // ACP toggles
1054
1321
  document.querySelectorAll('[data-toggle-acp]').forEach(el => {
1055
1322
  el.addEventListener('click', () => el.classList.toggle('active'));
@@ -1302,6 +1569,186 @@
1302
1569
  render();
1303
1570
  }
1304
1571
 
1572
+ // ==========================================
1573
+ // WeChat QR-login modal
1574
+ // ==========================================
1575
+ let wechatPollTimer = null;
1576
+ function closeWechatQrModal() {
1577
+ if (wechatPollTimer) { clearTimeout(wechatPollTimer); wechatPollTimer = null; }
1578
+ const m = document.getElementById('wechat-modal');
1579
+ if (m) m.remove();
1580
+ }
1581
+ async function openWechatQrModal() {
1582
+ // Tear down any prior modal first.
1583
+ closeWechatQrModal();
1584
+ const overlay = document.createElement('div');
1585
+ overlay.id = 'wechat-modal';
1586
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.55)';
1587
+ overlay.innerHTML = `
1588
+ <div style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:24px 28px;max-width:380px;width:90%;text-align:center">
1589
+ <div style="font-weight:600;font-size:15px;margin-bottom:14px">${esc(t('wechatScanTitle'))}</div>
1590
+ <div id="wechat-qr-wrap" style="min-height:240px;display:flex;align-items:center;justify-content:center;background:var(--surface2);border-radius:8px;margin-bottom:12px">
1591
+ <div style="color:var(--text-dim);font-size:13px">${esc(t('wechatGenerating'))}</div>
1592
+ </div>
1593
+ <div id="wechat-qr-status" style="font-size:13px;color:var(--text-dim);margin-bottom:14px;min-height:18px"></div>
1594
+ <div style="display:flex;gap:8px;justify-content:center">
1595
+ <button type="button" class="btn" id="wechat-regen">${esc(t('wechatRegen'))}</button>
1596
+ <button type="button" class="btn" id="wechat-close">${esc(t('wechatClose'))}</button>
1597
+ </div>
1598
+ </div>
1599
+ `;
1600
+ document.body.appendChild(overlay);
1601
+ document.getElementById('wechat-close')?.addEventListener('click', closeWechatQrModal);
1602
+ document.getElementById('wechat-regen')?.addEventListener('click', () => { void startWechatQr(); });
1603
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) closeWechatQrModal(); });
1604
+ await startWechatQr();
1605
+ }
1606
+
1607
+ async function startWechatQr() {
1608
+ if (wechatPollTimer) { clearTimeout(wechatPollTimer); wechatPollTimer = null; }
1609
+ const wrap = document.getElementById('wechat-qr-wrap');
1610
+ const statusEl = document.getElementById('wechat-qr-status');
1611
+ if (!wrap || !statusEl) return;
1612
+ wrap.innerHTML = `<div style="color:var(--text-dim);font-size:13px">${esc(t('wechatGenerating'))}</div>`;
1613
+ statusEl.textContent = '';
1614
+ let qrToken;
1615
+ try {
1616
+ const res = await authFetch('/api/messengers/wechat/qr-start', { method: 'POST' });
1617
+ if (!res.ok) {
1618
+ const j = await res.json().catch(() => ({}));
1619
+ throw new Error(j.error || res.statusText);
1620
+ }
1621
+ const data = await res.json();
1622
+ qrToken = data.qrToken;
1623
+ // qrUrl from iLink may be a data: URL (base64 png) or an https URL.
1624
+ // Either way, <img src=...> handles it directly.
1625
+ wrap.innerHTML = `<img src="${esc(data.qrUrl)}" alt="QR code" style="width:240px;height:240px;border-radius:6px;background:#fff;padding:8px">`;
1626
+ statusEl.textContent = t('wechatWaiting');
1627
+ } catch (err) {
1628
+ wrap.innerHTML = `<div style="color:var(--red);font-size:13px">${esc(t('wechatFailed').replace('{error}', err && err.message ? err.message : err))}</div>`;
1629
+ return;
1630
+ }
1631
+ // Begin polling.
1632
+ const poll = async () => {
1633
+ try {
1634
+ const res = await authFetch('/api/messengers/wechat/qr-status?token=' + encodeURIComponent(qrToken));
1635
+ if (!res.ok) {
1636
+ const j = await res.json().catch(() => ({}));
1637
+ throw new Error(j.error || res.statusText);
1638
+ }
1639
+ const data = await res.json();
1640
+ if (data.status === 'wait') {
1641
+ statusEl.textContent = t('wechatWaiting');
1642
+ wechatPollTimer = setTimeout(poll, 1500);
1643
+ return;
1644
+ }
1645
+ if (data.status === 'scaned') {
1646
+ statusEl.textContent = t('wechatScanned');
1647
+ wechatPollTimer = setTimeout(poll, 1000);
1648
+ return;
1649
+ }
1650
+ if (data.status === 'confirmed') {
1651
+ const account = (data.credentials && data.credentials.accountId) || '';
1652
+ statusEl.innerHTML = '<span style="color:var(--green)">' + esc(t('wechatConfirmed').replace('{account}', account)) + '</span>';
1653
+ // Refresh the underlying config (server added 'wechat-ilink' to messengers).
1654
+ toast(t('savedMsg'), 'success');
1655
+ setTimeout(() => { closeWechatQrModal(); void init(); }, 1500);
1656
+ return;
1657
+ }
1658
+ if (data.status === 'expired') {
1659
+ statusEl.innerHTML = '<span style="color:var(--red)">' + esc(t('wechatExpired')) + '</span>';
1660
+ return;
1661
+ }
1662
+ // Unknown status — keep polling once, then stop.
1663
+ wechatPollTimer = setTimeout(poll, 1500);
1664
+ } catch (err) {
1665
+ statusEl.innerHTML = '<span style="color:var(--red)">' + esc(t('wechatFailed').replace('{error}', err && err.message ? err.message : err)) + '</span>';
1666
+ }
1667
+ };
1668
+ wechatPollTimer = setTimeout(poll, 1000);
1669
+ }
1670
+
1671
+ // ==========================================
1672
+ // Service-control card (status + start/stop/restart)
1673
+ // ==========================================
1674
+ let svcPollTimer = null;
1675
+ async function loadServiceStatus() {
1676
+ const stateEl = document.getElementById('svc-state');
1677
+ const startBtn = document.getElementById('svc-start');
1678
+ const stopBtn = document.getElementById('svc-stop');
1679
+ const restartBtn = document.getElementById('svc-restart');
1680
+ if (!stateEl) return;
1681
+ try {
1682
+ const res = await authFetch('/api/service/status');
1683
+ if (!res.ok) throw new Error(res.statusText);
1684
+ const d = await res.json();
1685
+ if (d.mode === 'none') {
1686
+ stateEl.innerHTML = '<span class="dot dot-off"></span>' + esc(t('svcStateNone'));
1687
+ } else {
1688
+ const tpl = d.uptime ? t('svcStateRunning') : t('svcStateRunningNoUp');
1689
+ const label = tpl
1690
+ .replace('{mode}', d.mode || '?')
1691
+ .replace('{pid}', d.pid != null ? d.pid : '?')
1692
+ .replace('{uptime}', d.uptime || '');
1693
+ stateEl.innerHTML = '<span class="dot dot-on"></span>' + esc(label);
1694
+ }
1695
+ if (startBtn) startBtn.disabled = d.mode !== 'none';
1696
+ if (stopBtn) stopBtn.disabled = d.mode === 'none';
1697
+ if (restartBtn) restartBtn.disabled = d.mode === 'none' || d.mode === 'foreground';
1698
+ } catch (err) {
1699
+ stateEl.textContent = (t('error') + ': ' + (err && err.message ? err.message : err));
1700
+ }
1701
+ }
1702
+ async function svcAction(action) {
1703
+ const confirmKey = action === 'stop' ? 'svcConfirmStop' : action === 'restart' ? 'svcConfirmRestart' : null;
1704
+ if (confirmKey && !confirm(t(confirmKey))) return;
1705
+ const stateEl = document.getElementById('svc-state');
1706
+ try {
1707
+ const res = await authFetch('/api/service/' + action, { method: 'POST' });
1708
+ if (action === 'restart') {
1709
+ // The HTTP response may not arrive before the parent process
1710
+ // SIGTERMs itself. Either way, drop into a "wait for reconnect" loop.
1711
+ if (stateEl) stateEl.innerHTML = '<span class="dot dot-off"></span>' + esc(t('svcRestarting'));
1712
+ await waitForServiceBack(stateEl);
1713
+ if (stateEl) {
1714
+ // loadServiceStatus refreshes the badge to live state.
1715
+ await loadServiceStatus();
1716
+ toast(t('svcRestarted'), 'success');
1717
+ }
1718
+ return;
1719
+ }
1720
+ if (action === 'stop') {
1721
+ if (stateEl) stateEl.innerHTML = '<span class="dot dot-off"></span>' + esc(t('svcStopped'));
1722
+ // Don't poll — the server is gone.
1723
+ return;
1724
+ }
1725
+ if (!res.ok) {
1726
+ const j = await res.json().catch(() => ({}));
1727
+ throw new Error(j.error || res.statusText);
1728
+ }
1729
+ await loadServiceStatus();
1730
+ } catch (err) {
1731
+ toast(t('error') + ': ' + (err && err.message ? err.message : err), 'error');
1732
+ }
1733
+ }
1734
+ async function waitForServiceBack(stateEl) {
1735
+ // The new daemon takes ~2-4s to come back. Poll /api/service/status
1736
+ // every 700ms for up to 30s. Each successful response means the new
1737
+ // process has bound the port.
1738
+ const start = Date.now();
1739
+ for (;;) {
1740
+ if (Date.now() - start > 30000) {
1741
+ if (stateEl) stateEl.innerHTML = '<span class="dot dot-off"></span>' + esc(t('error'));
1742
+ return;
1743
+ }
1744
+ try {
1745
+ const r = await fetch('/api/service/status', { credentials: 'same-origin' });
1746
+ if (r.ok) return;
1747
+ } catch { /* not back yet */ }
1748
+ await new Promise(r => setTimeout(r, 700));
1749
+ }
1750
+ }
1751
+
1305
1752
  // ==========================================
1306
1753
  // API helpers
1307
1754
  // ==========================================
@@ -1313,6 +1760,8 @@
1313
1760
  defaultAgent: config.defaultAgent,
1314
1761
  telegram: config.telegram,
1315
1762
  feishu: config.feishu,
1763
+ dingtalk: config.dingtalk,
1764
+ discord: config.discord,
1316
1765
  acpAgents: config.acpAgents,
1317
1766
  webPort: config.webPort,
1318
1767
  };
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAqDA,wBAAgB,iBAAiB,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAmlB/C"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAqDA,wBAAgB,iBAAiB,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAknB/C"}