agim-cli 1.0.3 → 1.0.5
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.
- package/CHANGELOG.md +70 -0
- package/dist/cli-ui/cmd-handlers.d.ts +2 -0
- package/dist/cli-ui/cmd-handlers.d.ts.map +1 -1
- package/dist/cli-ui/cmd-handlers.js +113 -0
- package/dist/cli-ui/cmd-handlers.js.map +1 -1
- package/dist/cli-ui/config-wizard.d.ts.map +1 -1
- package/dist/cli-ui/config-wizard.js +5 -0
- package/dist/cli-ui/config-wizard.js.map +1 -1
- package/dist/cli-ui/i18n.d.ts +11 -0
- package/dist/cli-ui/i18n.d.ts.map +1 -1
- package/dist/cli-ui/i18n.js +24 -0
- package/dist/cli-ui/i18n.js.map +1 -1
- package/dist/plugins/messengers/wechat/ilink-adapter.d.ts +14 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.d.ts.map +1 -1
- package/dist/plugins/messengers/wechat/ilink-adapter.js +26 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.js.map +1 -1
- package/dist/web/public/settings.html +415 -5
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +241 -0
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
package/dist/web/server.js
CHANGED
|
@@ -402,6 +402,35 @@ export async function startWebServer(options) {
|
|
|
402
402
|
if (url.pathname === '/api/invoke' && req.method === 'POST') {
|
|
403
403
|
return handleInvoke(req, res, options.defaultAgent);
|
|
404
404
|
}
|
|
405
|
+
// WeChat QR login — drives the "扫码登录" button in the web settings
|
|
406
|
+
// page. /qr-start returns the QR image URL + the token used to poll;
|
|
407
|
+
// /qr-status polls one cycle and, on confirmation, persists the
|
|
408
|
+
// credentials to disk AND adds 'wechat-ilink' into config.messengers
|
|
409
|
+
// so the next service restart picks the channel up.
|
|
410
|
+
if (url.pathname === '/api/messengers/wechat/qr-start' && req.method === 'POST') {
|
|
411
|
+
return handleWechatQrStart(res);
|
|
412
|
+
}
|
|
413
|
+
if (url.pathname === '/api/messengers/wechat/qr-status' && req.method === 'GET') {
|
|
414
|
+
return handleWechatQrStatus(res, url);
|
|
415
|
+
}
|
|
416
|
+
// Service management — status / start / stop / restart for the
|
|
417
|
+
// "service controls" card in the web settings page. start/stop are
|
|
418
|
+
// mostly no-ops from the web context (the web server itself IS the
|
|
419
|
+
// service, so the page wouldn't render if nothing was running), but
|
|
420
|
+
// restart is the headline use case: change a setting, click restart,
|
|
421
|
+
// wait ~2s, browser auto-reconnects.
|
|
422
|
+
if (url.pathname === '/api/service/status' && req.method === 'GET') {
|
|
423
|
+
return handleServiceStatus(res);
|
|
424
|
+
}
|
|
425
|
+
if (url.pathname === '/api/service/start' && req.method === 'POST') {
|
|
426
|
+
return handleServiceStart(res);
|
|
427
|
+
}
|
|
428
|
+
if (url.pathname === '/api/service/stop' && req.method === 'POST') {
|
|
429
|
+
return handleServiceStop(res);
|
|
430
|
+
}
|
|
431
|
+
if (url.pathname === '/api/service/restart' && req.method === 'POST') {
|
|
432
|
+
return handleServiceRestart(res);
|
|
433
|
+
}
|
|
405
434
|
res.writeHead(404);
|
|
406
435
|
res.end('Not found');
|
|
407
436
|
});
|
|
@@ -640,6 +669,17 @@ async function handleGetConfig(_req, res) {
|
|
|
640
669
|
feishu: config.feishu
|
|
641
670
|
? { appId: config.feishu.appId, appSecret: mask(config.feishu.appSecret) }
|
|
642
671
|
: undefined,
|
|
672
|
+
dingtalk: config.dingtalk
|
|
673
|
+
? { clientId: config.dingtalk.clientId, clientSecret: mask(config.dingtalk.clientSecret), channelId: config.dingtalk.channelId }
|
|
674
|
+
: undefined,
|
|
675
|
+
discord: config.discord
|
|
676
|
+
? {
|
|
677
|
+
botToken: mask(config.discord.botToken),
|
|
678
|
+
channelId: config.discord.channelId,
|
|
679
|
+
allowedGuilds: config.discord.allowedGuilds,
|
|
680
|
+
allowedChannels: config.discord.allowedChannels,
|
|
681
|
+
}
|
|
682
|
+
: undefined,
|
|
643
683
|
acpAgents: config.acpAgents?.map(a => ({
|
|
644
684
|
...a,
|
|
645
685
|
auth: a.auth
|
|
@@ -681,6 +721,24 @@ async function handlePutConfig(req, res) {
|
|
|
681
721
|
};
|
|
682
722
|
continue;
|
|
683
723
|
}
|
|
724
|
+
if (key === 'dingtalk' && typeof val === 'object' && val !== null) {
|
|
725
|
+
const d = val;
|
|
726
|
+
merged.dingtalk = {
|
|
727
|
+
...(existing.dingtalk || {}),
|
|
728
|
+
...d,
|
|
729
|
+
clientSecret: typeof d.clientSecret === 'string' && isMasked(d.clientSecret) ? existing.dingtalk?.clientSecret : d.clientSecret,
|
|
730
|
+
};
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (key === 'discord' && typeof val === 'object' && val !== null) {
|
|
734
|
+
const d = val;
|
|
735
|
+
merged.discord = {
|
|
736
|
+
...(existing.discord || {}),
|
|
737
|
+
...d,
|
|
738
|
+
botToken: typeof d.botToken === 'string' && isMasked(d.botToken) ? existing.discord?.botToken : d.botToken,
|
|
739
|
+
};
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
684
742
|
if (key === 'acpAgents' && Array.isArray(val)) {
|
|
685
743
|
merged.acpAgents = val.map((item, i) => {
|
|
686
744
|
const a = item;
|
|
@@ -845,6 +903,189 @@ async function handleMetrics(_req, res, url) {
|
|
|
845
903
|
sendJson(res, 500, { error: msg });
|
|
846
904
|
}
|
|
847
905
|
}
|
|
906
|
+
// ============================================================
|
|
907
|
+
// WeChat QR-login endpoints (drive the "扫码登录" web button)
|
|
908
|
+
// ============================================================
|
|
909
|
+
async function handleWechatQrStart(res) {
|
|
910
|
+
try {
|
|
911
|
+
const { ILinkWeChatAdapter } = await import('../plugins/messengers/wechat/ilink-adapter.js');
|
|
912
|
+
const adapter = new ILinkWeChatAdapter();
|
|
913
|
+
const { qrUrl, qrToken } = await adapter.startQRLogin();
|
|
914
|
+
sendJson(res, 200, { qrUrl, qrToken });
|
|
915
|
+
}
|
|
916
|
+
catch (err) {
|
|
917
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
918
|
+
sendJson(res, 500, { error: msg });
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async function handleWechatQrStatus(res, url) {
|
|
922
|
+
const qrToken = url.searchParams.get('token') || '';
|
|
923
|
+
if (!qrToken) {
|
|
924
|
+
sendJson(res, 400, { error: 'token query param required' });
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
try {
|
|
928
|
+
const { ILinkWeChatAdapter } = await import('../plugins/messengers/wechat/ilink-adapter.js');
|
|
929
|
+
const adapter = new ILinkWeChatAdapter();
|
|
930
|
+
const result = await adapter.pollQRLogin(qrToken);
|
|
931
|
+
if (result.status === 'confirmed') {
|
|
932
|
+
// Add 'wechat-ilink' into config.messengers so the next restart of
|
|
933
|
+
// the service brings the channel up. The credentials file was
|
|
934
|
+
// already written by adapter.pollQRLogin().
|
|
935
|
+
try {
|
|
936
|
+
const cfg = await loadConfig();
|
|
937
|
+
if (!(cfg.messengers || []).includes('wechat-ilink')) {
|
|
938
|
+
const next = {
|
|
939
|
+
...cfg,
|
|
940
|
+
messengers: [...(cfg.messengers || []), 'wechat-ilink'],
|
|
941
|
+
};
|
|
942
|
+
await saveConfig(next);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
catch (err) {
|
|
946
|
+
webLog.warn({ event: 'wechat.qr.config_persist_failed', err: String(err) });
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
sendJson(res, 200, result);
|
|
950
|
+
}
|
|
951
|
+
catch (err) {
|
|
952
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
953
|
+
sendJson(res, 500, { error: msg });
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
// ============================================================
|
|
957
|
+
// Service-management endpoints (start / stop / restart / status)
|
|
958
|
+
// ============================================================
|
|
959
|
+
//
|
|
960
|
+
// The web server itself runs inside the service process, so /stop and
|
|
961
|
+
// /restart kill the very process handling the request. We respond
|
|
962
|
+
// first, then schedule the kill in a tick so the HTTP reply has time
|
|
963
|
+
// to flush. For /restart we additionally spawn a detached "wait + start"
|
|
964
|
+
// child so the daemon comes back without manual intervention.
|
|
965
|
+
async function handleServiceStatus(res) {
|
|
966
|
+
try {
|
|
967
|
+
const { detectService, formatUptime, readWebEndpoint } = await import('../cli-ui/service.js');
|
|
968
|
+
const st = detectService();
|
|
969
|
+
const web = readWebEndpoint();
|
|
970
|
+
sendJson(res, 200, {
|
|
971
|
+
mode: st.mode,
|
|
972
|
+
pid: st.pid,
|
|
973
|
+
uptimeSec: st.uptimeSec ?? null,
|
|
974
|
+
uptime: typeof st.uptimeSec === 'number' ? formatUptime(st.uptimeSec) : null,
|
|
975
|
+
web,
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
catch (err) {
|
|
979
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
async function handleServiceStart(res) {
|
|
983
|
+
try {
|
|
984
|
+
const { detectService, spawnBackground } = await import('../cli-ui/service.js');
|
|
985
|
+
const st = detectService();
|
|
986
|
+
if (st.mode !== 'none') {
|
|
987
|
+
sendJson(res, 200, { ok: true, alreadyRunning: true, mode: st.mode, pid: st.pid });
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const { fileURLToPath: f } = await import('node:url');
|
|
991
|
+
const __dn = dirname(f(import.meta.url));
|
|
992
|
+
const cliJs = join(__dn, '..', 'cli.js');
|
|
993
|
+
const { pid } = spawnBackground(process.execPath, [cliJs, 'start']);
|
|
994
|
+
sendJson(res, 200, { ok: true, started: true, pid });
|
|
995
|
+
}
|
|
996
|
+
catch (err) {
|
|
997
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
async function handleServiceStop(res) {
|
|
1001
|
+
const { detectService } = await import('../cli-ui/service.js');
|
|
1002
|
+
const st = detectService();
|
|
1003
|
+
if (st.mode === 'systemd') {
|
|
1004
|
+
// systemctl stop handles the kill for us — no self-exit needed.
|
|
1005
|
+
try {
|
|
1006
|
+
const { execSync } = await import('node:child_process');
|
|
1007
|
+
// Match service.ts's detection: prefer agim.service, fall back.
|
|
1008
|
+
const unitName = existsSync('/etc/systemd/system/agim.service') ? 'agim.service' : 'im-hub.service';
|
|
1009
|
+
execSync(`systemctl stop ${unitName}`);
|
|
1010
|
+
sendJson(res, 200, { ok: true, mode: 'systemd' });
|
|
1011
|
+
}
|
|
1012
|
+
catch (err) {
|
|
1013
|
+
sendJson(res, 500, { error: 'systemctl stop failed: ' + (err instanceof Error ? err.message : String(err)) });
|
|
1014
|
+
}
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
// Background / foreground: respond, then SIGTERM self in next tick.
|
|
1018
|
+
sendJson(res, 200, { ok: true, mode: st.mode, exiting: true });
|
|
1019
|
+
setTimeout(() => {
|
|
1020
|
+
try {
|
|
1021
|
+
process.kill(process.pid, 'SIGTERM');
|
|
1022
|
+
}
|
|
1023
|
+
catch {
|
|
1024
|
+
process.exit(0);
|
|
1025
|
+
}
|
|
1026
|
+
}, 200);
|
|
1027
|
+
}
|
|
1028
|
+
async function handleServiceRestart(res) {
|
|
1029
|
+
const { detectService, PID_FILE, LOG_FILE } = await import('../cli-ui/service.js');
|
|
1030
|
+
const st = detectService();
|
|
1031
|
+
if (st.mode === 'systemd') {
|
|
1032
|
+
try {
|
|
1033
|
+
const { execSync } = await import('node:child_process');
|
|
1034
|
+
const unitName = existsSync('/etc/systemd/system/agim.service') ? 'agim.service' : 'im-hub.service';
|
|
1035
|
+
execSync(`systemctl restart ${unitName}`);
|
|
1036
|
+
sendJson(res, 200, { ok: true, mode: 'systemd' });
|
|
1037
|
+
}
|
|
1038
|
+
catch (err) {
|
|
1039
|
+
sendJson(res, 500, { error: 'systemctl restart failed: ' + (err instanceof Error ? err.message : String(err)) });
|
|
1040
|
+
}
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (st.mode === 'foreground') {
|
|
1044
|
+
// Can't safely restart someone else's foreground process from the
|
|
1045
|
+
// web context — the owning shell still attached to its stdio
|
|
1046
|
+
// expects to Ctrl-C it manually.
|
|
1047
|
+
sendJson(res, 409, { error: 'foreground service is running in another terminal; restart it manually (Ctrl-C + re-run)' });
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
// background or 'none': spawn a detached node -e helper that waits
|
|
1051
|
+
// for our PID to die, then starts a fresh daemon. Then SIGTERM self
|
|
1052
|
+
// so the helper's wait-loop unblocks and the new daemon takes over.
|
|
1053
|
+
const __dn = dirname(fileURLToPath(import.meta.url));
|
|
1054
|
+
const cliJs = join(__dn, '..', 'cli.js');
|
|
1055
|
+
const parentPid = process.pid;
|
|
1056
|
+
const helperCode = 'const{spawn}=require("child_process");' +
|
|
1057
|
+
'const fs=require("fs");' +
|
|
1058
|
+
`const pid=${parentPid};` +
|
|
1059
|
+
'let n=0;' +
|
|
1060
|
+
'const t=setInterval(()=>{' +
|
|
1061
|
+
' try{process.kill(pid,0)}catch{' +
|
|
1062
|
+
' clearInterval(t);' +
|
|
1063
|
+
` const fd=fs.openSync(${JSON.stringify(LOG_FILE)},"a");` +
|
|
1064
|
+
` const c=spawn(${JSON.stringify(process.execPath)},[${JSON.stringify(cliJs)},"start"],{detached:true,stdio:["ignore",fd,fd],env:process.env});` +
|
|
1065
|
+
' c.unref();' +
|
|
1066
|
+
` fs.writeFileSync(${JSON.stringify(PID_FILE)},String(c.pid)+"\\n");` +
|
|
1067
|
+
' fs.closeSync(fd);' +
|
|
1068
|
+
' process.exit(0);' +
|
|
1069
|
+
' }' +
|
|
1070
|
+
' if(++n>150){clearInterval(t);process.exit(1)}' +
|
|
1071
|
+
'},200);';
|
|
1072
|
+
const { spawn: spawnChild } = await import('node:child_process');
|
|
1073
|
+
const helper = spawnChild(process.execPath, ['-e', helperCode], {
|
|
1074
|
+
detached: true,
|
|
1075
|
+
stdio: 'ignore',
|
|
1076
|
+
env: process.env,
|
|
1077
|
+
});
|
|
1078
|
+
helper.unref();
|
|
1079
|
+
sendJson(res, 200, { ok: true, mode: st.mode, restarting: true });
|
|
1080
|
+
setTimeout(() => {
|
|
1081
|
+
try {
|
|
1082
|
+
process.kill(process.pid, 'SIGTERM');
|
|
1083
|
+
}
|
|
1084
|
+
catch {
|
|
1085
|
+
process.exit(0);
|
|
1086
|
+
}
|
|
1087
|
+
}, 300);
|
|
1088
|
+
}
|
|
848
1089
|
/**
|
|
849
1090
|
* POST /api/notify → push a message to an IM thread.
|
|
850
1091
|
*
|