@keyou007/agent-bot 0.1.17-alpha.1 → 0.1.17-alpha.2

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +3 -1
  3. package/README.zh.md +3 -1
  4. package/config.example.yaml +12 -0
  5. package/dist/config/loadConfig.js +10 -0
  6. package/dist/config/loadConfig.js.map +1 -1
  7. package/dist/config/schema.js +6 -0
  8. package/dist/config/schema.js.map +1 -1
  9. package/dist/feishu/FeishuMarkdown.js +42 -13
  10. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  11. package/dist/feishu/FeishuTurnPresenter.js +33 -10
  12. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  13. package/dist/feishu/LocalImageMarkdown.js +16 -2
  14. package/dist/feishu/LocalImageMarkdown.js.map +1 -1
  15. package/dist/index.js +33 -5
  16. package/dist/index.js.map +1 -1
  17. package/dist/local-files/LocalFileViewerServer.js +706 -0
  18. package/dist/local-files/LocalFileViewerServer.js.map +1 -0
  19. package/dist/local-files/NetworkAddressSelector.js +97 -0
  20. package/dist/local-files/NetworkAddressSelector.js.map +1 -0
  21. package/dist/proxy/ProxySessionController.js +43 -29
  22. package/dist/proxy/ProxySessionController.js.map +1 -1
  23. package/dist/supervision/SafeRestartScheduler.js +15 -2
  24. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  25. package/docs/technical-reference.md +8 -0
  26. package/docs/technical-reference.zh.md +8 -0
  27. package/npm-shrinkwrap.json +2 -2
  28. package/package.json +1 -1
  29. package/skills/agent-bot/SKILL.md +1 -1
  30. package/src/config/loadConfig.ts +10 -0
  31. package/src/config/schema.ts +9 -0
  32. package/src/feishu/FeishuMarkdown.ts +51 -12
  33. package/src/feishu/FeishuTurnPresenter.ts +41 -11
  34. package/src/feishu/LocalImageMarkdown.ts +23 -2
  35. package/src/index.ts +33 -4
  36. package/src/local-files/LocalFileViewerServer.ts +774 -0
  37. package/src/local-files/NetworkAddressSelector.ts +111 -0
  38. package/src/proxy/ProxySessionController.ts +33 -20
  39. package/src/supervision/SafeRestartScheduler.ts +23 -2
@@ -8,6 +8,7 @@ export class SafeRestartScheduler {
8
8
  idleSince;
9
9
  idleInboundAt;
10
10
  scheduleId = 0;
11
+ restartImmediatelyIfIdle = false;
11
12
  constructor(options) {
12
13
  this.options = options;
13
14
  }
@@ -17,10 +18,14 @@ export class SafeRestartScheduler {
17
18
  get pendingReason() {
18
19
  return this.reason;
19
20
  }
20
- schedule(reason, notificationTarget) {
21
+ schedule(reason, notificationTarget, options = {}) {
21
22
  const newlyScheduled = !this.reason;
22
23
  if (newlyScheduled) {
23
24
  this.notificationTargets = [];
25
+ this.restartImmediatelyIfIdle = options.restartImmediatelyIfIdle === true;
26
+ }
27
+ else if (options.restartImmediatelyIfIdle) {
28
+ this.restartImmediatelyIfIdle = true;
24
29
  }
25
30
  this.scheduleId += 1;
26
31
  this.addNotificationTarget(notificationTarget, reason);
@@ -65,6 +70,7 @@ export class SafeRestartScheduler {
65
70
  this.notificationTargets = [];
66
71
  this.idleSince = undefined;
67
72
  this.idleInboundAt = undefined;
73
+ this.restartImmediatelyIfIdle = false;
68
74
  }
69
75
  async poll() {
70
76
  const reason = this.reason;
@@ -84,6 +90,10 @@ export class SafeRestartScheduler {
84
90
  });
85
91
  return;
86
92
  }
93
+ if (this.restartImmediatelyIfIdle) {
94
+ await this.restart(reason, scheduleId, state);
95
+ return;
96
+ }
87
97
  const quietPeriodMs = this.options.quietPeriodMs ?? 15_000;
88
98
  const latestInboundAt = state.latestInboundAt ?? "";
89
99
  if (this.idleSince === undefined || this.idleInboundAt !== latestInboundAt) {
@@ -138,6 +148,9 @@ export class SafeRestartScheduler {
138
148
  }
139
149
  return;
140
150
  }
151
+ await this.restart(reason, scheduleId, confirmed);
152
+ }
153
+ async restart(reason, scheduleId, activity) {
141
154
  const notificationTargets = this.notificationTargetsSnapshot();
142
155
  this.clear();
143
156
  await this.emitStatus({
@@ -145,7 +158,7 @@ export class SafeRestartScheduler {
145
158
  reason,
146
159
  notificationTargets,
147
160
  phase: "restarting",
148
- activity: confirmed,
161
+ activity,
149
162
  remainingMs: 0,
150
163
  });
151
164
  await this.options.onReady(reason, notificationTargets);
@@ -1 +1 @@
1
- {"version":3,"file":"SafeRestartScheduler.js","sourceRoot":"","sources":["../../src/supervision/SafeRestartScheduler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SafeRestartScheduler.js","sourceRoot":"","sources":["../../src/supervision/SafeRestartScheduler.ts"],"names":[],"mappings":"AAyCA,MAAM,OAAO,oBAAoB;IAWF,OAAO;IAV5B,KAAK,CAAkB;IACvB,OAAO,CAAiB;IACxB,SAAS,GAAG,KAAK,CAAC;IAClB,MAAM,CAAU;IAChB,mBAAmB,GAAgC,EAAE,CAAC;IACtD,SAAS,CAAU;IACnB,aAAa,CAAU;IACvB,UAAU,GAAG,CAAC,CAAC;IACf,wBAAwB,GAAG,KAAK,CAAC;IAEzC,YAA6B,OAAoC;uBAApC,OAAO;IAAgC,CAAC;IAErE,IAAI,SAAS;QACX,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;IAED,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,QAAQ,CACN,MAAc,EACd,kBAA8C,EAC9C,OAAO,GAA+B,EAAE;QAExC,MAAM,cAAc,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;QACpC,IAAI,cAAc,EAAE,CAAC;YACnB,IAAI,CAAC,mBAAmB,GAAG,EAAE,CAAC;YAC9B,IAAI,CAAC,wBAAwB,GAAG,OAAO,CAAC,wBAAwB,KAAK,IAAI,CAAC;QAC5E,CAAC;aAAM,IAAI,OAAO,CAAC,wBAAwB,EAAE,CAAC;YAC5C,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAC;QACvC,CAAC;QACD,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;QACrB,IAAI,CAAC,qBAAqB,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;QACvD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,cAAc,IAAI,KAAK,CAAC,CAAC;QAC3F,CAAC;QACD,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,OAAO,cAAc,CAAC;IACxB,CAAC;IAED,MAAM;QACJ,IAAI,CAAC,KAAK,EAAE,CAAC;IACf,CAAC;IAED,KAAK,CAAC,aAAa;QACjB,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAC/B,OAAO,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,kBAA0B;QAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3B,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,KAAK,kBAAkB;YAAE,OAAO,KAAK,CAAC;QACpE,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;QAC7C,MAAM,mBAAmB,GAAG,IAAI,CAAC,2BAA2B,EAAE,CAAC;QAC/D,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,MAAM,IAAI,CAAC,UAAU,CAAC;YACpB,UAAU,EAAE,kBAAkB;YAC9B,MAAM;YACN,mBAAmB;YACnB,KAAK,EAAE,WAAW;YAClB,QAAQ;SACT,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,KAAK;QACX,IAAI,IAAI,CAAC,KAAK;YAAE,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QACxB,IAAI,CAAC,mBAAmB,GAAG,EAAE,CAAC;QAC9B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;QAC/B,IAAI,CAAC,wBAAwB,GAAG,KAAK,CAAC;IACxC,CAAC;IAEO,KAAK,CAAC,IAAI;QAChB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3B,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QACnC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;QAC1C,IAAI,KAAK,CAAC,eAAe,GAAG,CAAC,IAAI,KAAK,CAAC,sBAAsB,GAAG,CAAC,EAAE,CAAC;YAClE,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;YAC3B,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;YAC/B,MAAM,IAAI,CAAC,UAAU,CAAC;gBACpB,UAAU;gBACV,MAAM;gBACN,mBAAmB,EAAE,IAAI,CAAC,2BAA2B,EAAE;gBACvD,KAAK,EAAE,KAAK,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,kBAAkB;gBACvE,QAAQ,EAAE,KAAK;aAChB,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,wBAAwB,EAAE,CAAC;YAClC,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;YAC9C,OAAO;QACT,CAAC;QACD,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,IAAI,MAAM,CAAC;QAC3D,MAAM,eAAe,GAAG,KAAK,CAAC,eAAe,IAAI,EAAE,CAAC;QACpD,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,IAAI,CAAC,aAAa,KAAK,eAAe,EAAE,CAAC;YAC3E,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAC5B,IAAI,CAAC,aAAa,GAAG,eAAe,CAAC;YACrC,MAAM,IAAI,CAAC,UAAU,CAAC;gBACpB,UAAU;gBACV,MAAM;gBACN,mBAAmB,EAAE,IAAI,CAAC,2BAA2B,EAAE;gBACvD,KAAK,EAAE,WAAW;gBAClB,QAAQ,EAAE,KAAK;gBACf,WAAW,EAAE,aAAa;aAC3B,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,aAAa,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;QAC/E,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,CAAC,UAAU,CAAC;gBACpB,UAAU;gBACV,MAAM;gBACN,mBAAmB,EAAE,IAAI,CAAC,2BAA2B,EAAE;gBACvD,KAAK,EAAE,WAAW;gBAClB,QAAQ,EAAE,KAAK;gBACf,WAAW;aACZ,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;QAC9C,IACE,SAAS,CAAC,eAAe,GAAG,CAAC;eAC1B,SAAS,CAAC,sBAAsB,GAAG,CAAC;eACpC,CAAC,SAAS,CAAC,eAAe,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,aAAa,EAC3D,CAAC;YACD,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;YAC3B,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;YAC/B,IAAI,SAAS,CAAC,eAAe,GAAG,CAAC,IAAI,SAAS,CAAC,sBAAsB,GAAG,CAAC,EAAE,CAAC;gBAC1E,MAAM,IAAI,CAAC,UAAU,CAAC;oBACpB,UAAU;oBACV,MAAM;oBACN,mBAAmB,EAAE,IAAI,CAAC,2BAA2B,EAAE;oBACvD,KAAK,EAAE,SAAS,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,kBAAkB;oBAC3E,QAAQ,EAAE,SAAS;iBACpB,CAAC,CAAC;YACL,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,CAAC,UAAU,CAAC;oBACpB,UAAU;oBACV,MAAM;oBACN,mBAAmB,EAAE,IAAI,CAAC,2BAA2B,EAAE;oBACvD,KAAK,EAAE,WAAW;oBAClB,QAAQ,EAAE,SAAS;oBACnB,WAAW,EAAE,aAAa;iBAC3B,CAAC,CAAC;YACL,CAAC;YACD,OAAO;QACT,CAAC;QACD,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;IACpD,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,MAAc,EAAE,UAAkB,EAAE,QAA6B;QACrF,MAAM,mBAAmB,GAAG,IAAI,CAAC,2BAA2B,EAAE,CAAC;QAC/D,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,MAAM,IAAI,CAAC,UAAU,CAAC;YACpB,UAAU;YACV,MAAM;YACN,mBAAmB;YACnB,KAAK,EAAE,YAAY;YACnB,QAAQ;YACR,WAAW,EAAE,CAAC;SACf,CAAC,CAAC;QACH,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAC1D,CAAC;IAEO,WAAW;QACjB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YAC5C,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;YACzB,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM;gBAAE,IAAI,CAAC,WAAW,EAAE,CAAC;QACxD,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,UAAU;QACtB,GAAG,CAAC;YACF,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACvB,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QACpB,CAAC,QAAQ,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM,EAAE;IAC1C,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,MAAyB;QAChD,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ;YAAE,OAAO;QACnC,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YAC5E,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC,KAAK,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,qBAAqB,CAAC,KAA4C,EAAE,MAAc;QACxF,MAAM,MAAM,GAAG,4BAA4B,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC3D,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,aAAa,GAAG,IAAI,CAAC,mBAAmB,CAAC,SAAS,CACtD,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,UAAU,KAAK,MAAM,CAAC,UAAU,CAC1D,CAAC;QACF,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACtC,OAAO;QACT,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,mBAAmB,CAAC,aAAa,CAAE,CAAC;QAC1D,IAAI,CAAC,mBAAmB,CAAC,aAAa,CAAC,GAAG;YACxC,GAAG,QAAQ;YACX,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,GAAG,CAAC,MAAM,CAAC,cAAc;gBACvB,CAAC,CAAC,EAAE,cAAc,EAAE,MAAM,CAAC,cAAc,EAAE;gBAC3C,CAAC,CAAC,EAAE,CAAC;SACR,CAAC;IACJ,CAAC;IAEO,2BAA2B;QACjC,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC,CAAC;IACnE,CAAC;CACF;AAED,SAAS,4BAA4B,CACnC,KAA4C,EAC5C,MAAc;IAEd,MAAM,UAAU,GAAG,KAAK,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC;IAC5C,IAAI,CAAC,UAAU;QAAE,OAAO,SAAS,CAAC;IAClC,MAAM,cAAc,GAAG,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC;IACrD,OAAO;QACL,UAAU;QACV,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,MAAM;KACP,CAAC;AACJ,CAAC"}
@@ -156,6 +156,12 @@ feishu:
156
156
  console:
157
157
  enabled: true
158
158
 
159
+ fileViewer:
160
+ enabled: true
161
+ host: "127.0.0.1"
162
+ port: 0
163
+ # publicBaseUrl: "https://agentbot.example.com/files"
164
+
159
165
  agents:
160
166
  codex:
161
167
  kind: "app-server"
@@ -187,6 +193,8 @@ logging:
187
193
  path: "./logs/agent-bot.log"
188
194
  ```
189
195
 
196
+ `fileViewer` turns local non-image files and directories in final replies into signed, read-only HTTP links. The default `host: "127.0.0.1"` accepts connections only from the Agent Bot machine. With `0.0.0.0` or `::`, the server listens on every interface and automatically chooses a non-internal IPv4 link address in wired, Wi-Fi, other physical, then VPN order. Docker, Hyper-V, VMware, VirtualBox, WSL, loopback, and link-local interfaces are excluded; if no candidate remains, links fall back to loopback. `publicBaseUrl` has the highest priority and overrides automatic selection for domains, HTTPS reverse proxies, NAT, or port mappings. With `port: 0`, the first startup selects a free port and stores it in `<data>/file-viewer/port`; later restarts reuse it, while separate Profiles avoid a shared fixed port. The signing key is stored beside it as `secret`. File URLs can read only their signed absolute path. Directory URLs list direct children and allow downward navigation without exposing a parent-directory action. Text identity is detected from a content sample, and text pages preview the first 2 MiB with line anchors; images, PDFs, audio, and video use native browser previews; other binary files expose raw and download actions. Treat each signed URL as a bearer credential and do not share it publicly.
197
+
190
198
  `feishu.respondToOwnerOnly` defaults to `true`. It compares each message sender and card-action operator Open ID with `feishu.userOpenId`, ignoring non-owner input before durable event claims, reactions, downloads, commands, or Agent work. Set it to `false` to allow other users. If it is enabled without a configured owner Open ID, all Feishu messages and card actions are ignored; configure `FEISHU_USER_OPEN_ID` or temporarily disable the restriction to bootstrap from a private message.
191
199
 
192
200
  `feishu.respondToAllGroupMessages` defaults to `true` in the template, while first initialization and `init --reset` write the user's selected mode. Set it to `false` to ignore owner group messages unless they mention the current bot. The worker resolves the bot's Open ID at startup so mentioning another member does not trigger it. Owner private messages are unaffected. Initialization requests all-user group-message delivery only when this value is `true`; changing a mention-only Profile to `true` later requires rerunning `agentbot init` to add the permission.
@@ -156,6 +156,12 @@ feishu:
156
156
  console:
157
157
  enabled: true
158
158
 
159
+ fileViewer:
160
+ enabled: true
161
+ host: "127.0.0.1"
162
+ port: 0
163
+ # publicBaseUrl: "https://agentbot.example.com/files"
164
+
159
165
  agents:
160
166
  codex:
161
167
  kind: "app-server"
@@ -187,6 +193,8 @@ logging:
187
193
  path: "./logs/agent-bot.log"
188
194
  ```
189
195
 
196
+ `fileViewer` 为最终回答中的本地非图片文件和目录生成带签名的只读 HTTP 链接。默认 `host: "127.0.0.1"`,只能从运行 Agent Bot 的机器访问。设为 `0.0.0.0` 或 `::` 时,服务会监听所有网卡,并从非内部 IPv4 地址中按有线、Wi-Fi、其他物理网卡、VPN 的顺序自动选择链接地址;Docker、Hyper-V、VMware、VirtualBox、WSL、回环和链路本地地址会被排除,没有候选地址时回退到本机环回地址。`publicBaseUrl` 的优先级最高,用于域名、HTTPS 反向代理、NAT 或端口映射。`port: 0` 会在第一次启动时选择空闲端口,并保存到 `<data>/file-viewer/port`,以后重启继续使用,同一机器上的多个 Profile 不会争用固定端口。签名密钥保存在同目录的 `secret` 文件中。文件 URL 只能读取签发时对应的绝对路径;目录 URL 会列出其直接子项并允许继续向下浏览,但不提供向父目录跳转。文本身份由内容采样识别,页面显示带行号的开头 2 MiB;图片、PDF、音视频使用浏览器原生预览,其他二进制文件提供原始打开和下载入口。签名 URL 是持有者凭证,不应公开分享。
197
+
190
198
  `feishu.respondToOwnerOnly` 默认为 `true`。每条消息的发送者及卡片操作人的 Open ID 都会与 `feishu.userOpenId` 比较;非拥有者输入会在持久化事件、添加 reaction、下载图片、执行命令或启动 Agent 前被忽略。设为 `false` 可允许其他用户。开启后若未配置拥有者 Open ID,所有飞书消息和卡片操作都会被忽略;需先配置 `FEISHU_USER_OPEN_ID`,或临时关闭限制后通过私聊完成补全。
191
199
 
192
200
  `feishu.respondToAllGroupMessages` 在模板中默认为 `true`,第一次初始化和 `init --reset` 会按用户选择写入实际值。设为 `false` 后,拥有者未 @ 当前机器人的群消息也会被忽略;Worker 启动时会解析机器人的 Open ID,因此 @ 其他成员不会误触发。拥有者私聊不受影响。初始化只在该值为 `true` 时申请接收全部群消息的权限;仅 @ 响应的 Profile 后续改为 `true` 时,需要重新运行 `agentbot init` 补充权限。
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@keyou007/agent-bot",
3
- "version": "0.1.17-alpha.1",
3
+ "version": "0.1.17-alpha.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@keyou007/agent-bot",
9
- "version": "0.1.17-alpha.1",
9
+ "version": "0.1.17-alpha.2",
10
10
  "license": "GPL-3.0-only",
11
11
  "dependencies": {
12
12
  "@agentclientprotocol/sdk": "1.2.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyou007/agent-bot",
3
- "version": "0.1.17-alpha.1",
3
+ "version": "0.1.17-alpha.2",
4
4
  "type": "module",
5
5
  "description": "Use Codex, TraeX, and compatible coding agents from Lark/Feishu.",
6
6
  "license": "GPL-3.0-only",
@@ -144,7 +144,7 @@ agentbot server restart --task <task> --reason "<reason>"
144
144
  agentbot server stop
145
145
  ```
146
146
 
147
- `update` is only for npm-installed Agent Bot packages. It verifies the new package before waiting for a safe service stop and automatically restores the prior version if activation fails. It refuses source checkouts and `npm link` installations.
147
+ `update` is only for npm-installed Agent Bot packages. It verifies the new package, sends a safe-restart card, waits for active tasks, and restarts immediately when the service is already idle. It automatically restores the prior version if activation fails. It refuses source checkouts and `npm link` installations.
148
148
 
149
149
  Autostart is Profile-specific. Use `server autostart enable` for login startup, `server autostart enable --linger` on Linux only when the user explicitly requests startup before login, and `server autostart disable` to remove registration without stopping the current Server. Disabling Agent Bot autostart must not disable Linux user lingering because other services may use it.
150
150
 
@@ -26,6 +26,16 @@ const DEFAULT_CONFIG = `feishu:
26
26
  console:
27
27
  enabled: true
28
28
 
29
+ fileViewer:
30
+ enabled: true
31
+ # Use 0.0.0.0 to accept LAN connections and automatically prefer
32
+ # wired, Wi-Fi, other physical, then VPN addresses in generated links.
33
+ host: "127.0.0.1"
34
+ # 0 selects a free port once and reuses it for this Profile.
35
+ port: 0
36
+ # Overrides the automatically selected URL for domains or reverse proxies.
37
+ # publicBaseUrl: "https://agentbot.example.com/files"
38
+
29
39
  agents:
30
40
  codex:
31
41
  kind: "app-server"
@@ -62,6 +62,15 @@ export const appConfigSchema = z.object({
62
62
  enabled: z.boolean().default(true),
63
63
  })
64
64
  .default({ enabled: true }),
65
+ fileViewer: z.object({
66
+ enabled: z.boolean().default(true),
67
+ host: z.string().min(1).default("127.0.0.1"),
68
+ port: z.number().int().min(0).max(65_535).default(0),
69
+ publicBaseUrl: z.string().url().refine(
70
+ (value) => ["http:", "https:"].includes(new URL(value).protocol),
71
+ "fileViewer.publicBaseUrl must use http or https.",
72
+ ).optional(),
73
+ }).default({ enabled: true, host: "127.0.0.1", port: 0 }),
65
74
  feishu: z.object({
66
75
  transport: z.enum(["auto", "sdk", "console"]).default("auto"),
67
76
  appId: z.string().optional(),
@@ -8,10 +8,17 @@ interface FenceState {
8
8
  }
9
9
 
10
10
  const FENCE_OPENER = /^([ \t]*)((?:`{3,})|(?:~{3,}))(.*)$/;
11
- const MARKDOWN_LINK = /(!?)\[([^\]\r\n]*)\]\((<?[^)\r\n]+>?)\)/g;
11
+ const MARKDOWN_LINK = /(!?)\[([^\]\r\n]*)\]\((<[^>\r\n]+>|[^)\r\n]+)\)/g;
12
12
  const LOCAL_FILE_LINE_REFERENCE = /(:\d+(?::\d+)?)$/;
13
+ const LOCAL_IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".tiff", ".bmp", ".ico"]);
13
14
 
14
- export function normalizeFeishuMarkdown(markdown: string, projectCwd?: string): string {
15
+ export type LocalFileUrlResolver = (filePath: string, reference?: string) => string | undefined;
16
+
17
+ export function normalizeFeishuMarkdown(
18
+ markdown: string,
19
+ projectCwd?: string,
20
+ localFileUrl?: LocalFileUrlResolver,
21
+ ): string {
15
22
  const lines = markdown.match(/[^\r\n]*(?:\r\n|\n)|[^\r\n]+$/g) ?? [];
16
23
  let fence: FenceState | undefined;
17
24
 
@@ -21,7 +28,7 @@ export function normalizeFeishuMarkdown(markdown: string, projectCwd?: string):
21
28
 
22
29
  if (!fence) {
23
30
  const opener = FENCE_OPENER.exec(body);
24
- if (!opener?.[2]) return `${includeLocalFileLineReferences(body, projectCwd)}${ending}`;
31
+ if (!opener?.[2]) return `${includeLocalFileLineReferences(body, projectCwd, localFileUrl)}${ending}`;
25
32
 
26
33
  const indent = opener[1] ?? "";
27
34
  const delimiter = opener[2];
@@ -42,18 +49,38 @@ export function normalizeFeishuMarkdown(markdown: string, projectCwd?: string):
42
49
  }).join("");
43
50
  }
44
51
 
45
- function includeLocalFileLineReferences(markdown: string, projectCwd?: string): string {
52
+ function includeLocalFileLineReferences(
53
+ markdown: string,
54
+ projectCwd?: string,
55
+ localFileUrl?: LocalFileUrlResolver,
56
+ ): string {
46
57
  return markdown.replace(MARKDOWN_LINK, (link, imageMarker: string, label: string, rawTarget: string) => {
47
58
  if (imageMarker) return link;
48
59
 
49
60
  const target = unwrapMarkdownTarget(rawTarget.trim());
50
61
  if (!isLocalFileTarget(target)) return link;
62
+ if (isLocalImageTarget(target)) return link;
51
63
 
52
64
  const reference = LOCAL_FILE_LINE_REFERENCE.exec(target)?.[1];
65
+ const targetWithoutReference = reference ? target.slice(0, -reference.length) : target;
66
+ const filePath = localFilePath(targetWithoutReference);
67
+ const viewerUrl = filePath ? localFileUrl?.(filePath, reference) : undefined;
53
68
  const projectPathLabel = projectCwd ? projectFilePathLabel(target, projectCwd, reference) : undefined;
54
- const normalizedLabel = projectPathLabel
55
- ?? (reference && !label.endsWith(reference) ? `${label}${reference}` : label);
56
- return normalizedLabel === label ? link : `[${normalizedLabel}](${rawTarget})`;
69
+ if (!projectPathLabel) {
70
+ const normalizedLabel = reference && !label.endsWith(reference) ? `${label}${reference}` : label;
71
+ if (viewerUrl) return `[${normalizedLabel}](${viewerUrl})`;
72
+ return normalizedLabel === label ? link : `[${normalizedLabel}](${rawTarget})`;
73
+ }
74
+
75
+ if (projectPathLabel === label) return viewerUrl ? `[${label}](${viewerUrl})` : link;
76
+ if (reference && projectPathLabel === `${label}${reference}`) {
77
+ return `[${projectPathLabel}](${viewerUrl ?? rawTarget})`;
78
+ }
79
+
80
+ if (!label) return viewerUrl ? `[${projectPathLabel}](${viewerUrl})` : inlineCode(projectPathLabel);
81
+ return viewerUrl
82
+ ? `[${label}](${viewerUrl})(${inlineCode(projectPathLabel)})`
83
+ : `${label}(${inlineCode(projectPathLabel)})`;
57
84
  });
58
85
  }
59
86
 
@@ -71,11 +98,7 @@ function projectFilePathLabel(
71
98
 
72
99
  const pathApi = usesWindowsPaths(filePath, projectCwd) ? path.win32 : path.posix;
73
100
  const parsed = pathApi.parse(filePath);
74
- if (Array.from(parsed.name).length > 5) return undefined;
75
-
76
- const parentName = pathApi.basename(pathApi.dirname(filePath));
77
- if (!parentName) return undefined;
78
- return `${parentName}${pathApi.sep}${parsed.base}${reference ?? ""}`;
101
+ return `${parsed.base}${reference ?? ""}`;
79
102
  }
80
103
 
81
104
  function localFilePath(target: string): string | undefined {
@@ -108,10 +131,26 @@ function displayAbsolutePath(filePath: string): string {
108
131
  return path.win32.normalize(filePath);
109
132
  }
110
133
 
134
+ function inlineCode(value: string): string {
135
+ const longestDelimiter = Math.max(0, ...(value.match(/`+/g) ?? []).map((run) => run.length));
136
+ const delimiter = "`".repeat(longestDelimiter + 1);
137
+ const padding = value.startsWith("`") || value.endsWith("`") ? " " : "";
138
+ return `${delimiter}${padding}${value}${padding}${delimiter}`;
139
+ }
140
+
111
141
  function usesWindowsPaths(...values: string[]): boolean {
112
142
  return values.some((value) => /^\/?[a-z]:[\\/]/i.test(value) || /^\\\\/.test(value));
113
143
  }
114
144
 
145
+ function isLocalImageTarget(target: string): boolean {
146
+ const reference = LOCAL_FILE_LINE_REFERENCE.exec(target)?.[1];
147
+ const targetWithoutReference = reference ? target.slice(0, -reference.length) : target;
148
+ const filePath = localFilePath(targetWithoutReference);
149
+ if (!filePath) return false;
150
+ const pathApi = usesWindowsPaths(filePath) ? path.win32 : path.posix;
151
+ return LOCAL_IMAGE_EXTENSIONS.has(pathApi.extname(filePath).toLowerCase());
152
+ }
153
+
115
154
  function unwrapMarkdownTarget(target: string): string {
116
155
  return target.startsWith("<") && target.endsWith(">") ? target.slice(1, -1) : target;
117
156
  }
@@ -8,7 +8,7 @@ import { splitMarkdown } from "../presentation/splitMarkdown.js";
8
8
  import type { TurnViewState } from "../presentation/turnViewTypes.js";
9
9
  import { CardRenderer } from "./CardRenderer.js";
10
10
  import { CardUpdateScheduler } from "./CardUpdateScheduler.js";
11
- import { normalizeFeishuMarkdown } from "./FeishuMarkdown.js";
11
+ import { normalizeFeishuMarkdown, type LocalFileUrlResolver } from "./FeishuMarkdown.js";
12
12
  import type { FeishuOutbound, MessageReplyTarget } from "./types.js";
13
13
  import {
14
14
  FINAL_RESPONSE_BRANDING_BLOCK,
@@ -50,6 +50,7 @@ export interface FeishuTurnPresenterOptions {
50
50
  finalChunkLength?: number;
51
51
  onError?: (error: unknown) => void;
52
52
  finalRetryBackoffMs?: number[];
53
+ localFileUrl?: LocalFileUrlResolver;
53
54
  }
54
55
 
55
56
  const MAX_FINAL_TABLES_PER_CARD = 5;
@@ -215,7 +216,7 @@ export class FeishuTurnPresenter {
215
216
  }
216
217
  const delivery = this.store.getTurnDelivery(turnId);
217
218
  if (delivery?.progressMessageId) {
218
- await this.outbound.updateInteractiveCard(delivery.progressMessageId, this.renderer.renderTurn(state));
219
+ await this.outbound.updateInteractiveCard(delivery.progressMessageId, this.renderTurn(state));
219
220
  }
220
221
  }
221
222
 
@@ -332,7 +333,7 @@ export class FeishuTurnPresenter {
332
333
  await this.outbound.sendText(contextKey, "未找到这次执行的详情。可能已被清理。");
333
334
  return;
334
335
  }
335
- await this.outbound.sendInteractiveCard(contextKey, this.renderer.renderTurnDetails(snapshot));
336
+ await this.outbound.sendInteractiveCard(contextKey, this.renderTurnDetails(snapshot));
336
337
  }
337
338
 
338
339
  async showActivityPage(
@@ -355,9 +356,9 @@ export class FeishuTurnPresenter {
355
356
  entry.scheduler.invalidateRenderedCard();
356
357
  await entry.scheduler.flush(entry.state);
357
358
  } else if (targetMessageId) {
358
- await this.outbound.updateInteractiveCard(targetMessageId, this.renderer.renderTurn(entry.state));
359
+ await this.outbound.updateInteractiveCard(targetMessageId, this.renderTurn(entry.state));
359
360
  } else {
360
- await this.outbound.sendInteractiveCard(contextKey, this.renderer.renderTurn(entry.state));
361
+ await this.outbound.sendInteractiveCard(contextKey, this.renderTurn(entry.state));
361
362
  }
362
363
  return;
363
364
  }
@@ -366,7 +367,7 @@ export class FeishuTurnPresenter {
366
367
  entry.historySnapshot = entry.state;
367
368
  await entry.scheduler?.flush();
368
369
  }
369
- const card = this.renderer.renderActivityHistory(entry.historySnapshot, page);
370
+ const card = this.renderActivityHistory(entry.historySnapshot, page);
370
371
  if (targetMessageId) await this.outbound.updateInteractiveCard(targetMessageId, card);
371
372
  else await this.outbound.sendInteractiveCard(contextKey, card);
372
373
  return;
@@ -375,8 +376,8 @@ export class FeishuTurnPresenter {
375
376
  const snapshot = this.store.getTurnSnapshot(turnId);
376
377
  if (!isTurnViewState(snapshot)) throw new Error("未找到这次执行的活动历史。");
377
378
  const card = page === "latest"
378
- ? this.renderer.renderTurn(snapshot)
379
- : this.renderer.renderActivityHistory(snapshot, page);
379
+ ? this.renderTurn(snapshot)
380
+ : this.renderActivityHistory(snapshot, page);
380
381
  if (messageId) await this.outbound.updateInteractiveCard(messageId, card);
381
382
  else await this.outbound.sendInteractiveCard(contextKey, card);
382
383
  }
@@ -428,7 +429,7 @@ export class FeishuTurnPresenter {
428
429
 
429
430
  private async initializeEntry(entry: TurnEntry): Promise<void> {
430
431
  const sentState = entry.state;
431
- const card = this.renderer.renderTurn(sentState);
432
+ const card = this.renderTurn(sentState);
432
433
  const delivery = this.store.getTurnDelivery(sentState.turnId);
433
434
  const persistedMessageId = delivery?.progressMessageId;
434
435
  if (delivery?.finalDelivered && !persistedMessageId) return;
@@ -449,7 +450,7 @@ export class FeishuTurnPresenter {
449
450
  if (!messageId) return;
450
451
  this.store.saveTurnDelivery(entry.state.turnId, { progressMessageId: messageId });
451
452
  entry.scheduler = new CardUpdateScheduler<TurnViewState>({
452
- render: (state) => this.renderer.renderTurn(state),
453
+ render: (state) => this.renderTurn(state),
453
454
  write: (card) => this.outbound.updateInteractiveCard(messageId, card),
454
455
  normalIntervalMs: this.options.normalIntervalMs,
455
456
  criticalGapMs: this.options.criticalGapMs,
@@ -461,6 +462,18 @@ export class FeishuTurnPresenter {
461
462
  await this.syncThinkingCardReaction(entry);
462
463
  }
463
464
 
465
+ private renderTurn(state: TurnViewState): Record<string, unknown> {
466
+ return this.renderer.renderTurn(normalizeTurnCardMarkdown(state, this.options.localFileUrl));
467
+ }
468
+
469
+ private renderTurnDetails(state: TurnViewState): Record<string, unknown> {
470
+ return this.renderer.renderTurnDetails(normalizeTurnCardMarkdown(state, this.options.localFileUrl));
471
+ }
472
+
473
+ private renderActivityHistory(state: TurnViewState, page: number): Record<string, unknown> {
474
+ return this.renderer.renderActivityHistory(normalizeTurnCardMarkdown(state, this.options.localFileUrl), page);
475
+ }
476
+
464
477
  private startElapsedUpdates(entry: TurnEntry): void {
465
478
  if (entry.elapsedTimer || isTerminalViewStatus(entry.state.status)) return;
466
479
  const intervalMs = Math.max(1, this.options.elapsedUpdateIntervalMs ?? DEFAULT_ELAPSED_UPDATE_INTERVAL_MS);
@@ -539,7 +552,7 @@ export class FeishuTurnPresenter {
539
552
  if (delivery?.finalDelivered || !state.finalResponse) return;
540
553
  const finalChunkLength = this.options.finalChunkLength ?? 4_000;
541
554
  const chunks = brandedFinalChunks(
542
- normalizeFeishuMarkdown(state.finalResponse, state.projectCwd),
555
+ normalizeFeishuMarkdown(state.finalResponse, state.projectCwd, this.options.localFileUrl),
543
556
  finalChunkLength,
544
557
  );
545
558
  const messageIds = [...(delivery?.finalMessageIds ?? [])];
@@ -579,6 +592,23 @@ export class FeishuTurnPresenter {
579
592
  }
580
593
  }
581
594
 
595
+ function normalizeTurnCardMarkdown(
596
+ state: TurnViewState,
597
+ localFileUrl: LocalFileUrlResolver | undefined,
598
+ ): TurnViewState {
599
+ if (!localFileUrl) return state;
600
+ const normalize = (value: string): string => normalizeFeishuMarkdown(value, state.projectCwd, localFileUrl);
601
+ return {
602
+ ...state,
603
+ assistantText: normalize(state.assistantText),
604
+ ...(state.progressText !== undefined ? { progressText: normalize(state.progressText) } : {}),
605
+ plan: state.plan.map((step) => ({ ...step, text: normalize(step.text) })),
606
+ activities: state.activities.map((activity) => activity.kind === "tool"
607
+ ? activity
608
+ : { ...activity, text: normalize(activity.text) }),
609
+ };
610
+ }
611
+
582
612
  function brandedFinalChunks(response: string, maxLength: number): string[] {
583
613
  const chunks = splitMarkdown(response, maxLength, MAX_FINAL_TABLES_PER_CARD);
584
614
  if (chunks.length <= 1 && response.length <= FINAL_RESPONSE_BRANDING_THRESHOLD) return chunks;
@@ -29,7 +29,7 @@ export async function renderMarkdownWithLocalImages(
29
29
  continue;
30
30
  }
31
31
 
32
- appendMarkdown(elements, markdown.slice(cursor, matchIndex));
32
+ appendMarkdown(elements, markdownBeforeLocalImage(markdown, cursor, matchIndex, matchIndex + match[0].length));
33
33
  try {
34
34
  let upload = uploads.get(filePath);
35
35
  if (!upload) {
@@ -86,7 +86,28 @@ function unsupportedImageFallback(label: string, rawTarget: string): string {
86
86
  : `图片不可用:${label || "本地图片"}`;
87
87
  }
88
88
 
89
+ function markdownBeforeLocalImage(
90
+ markdown: string,
91
+ cursor: number,
92
+ matchStart: number,
93
+ matchEnd: number,
94
+ ): string {
95
+ const lineStart = markdown.lastIndexOf("\n", matchStart - 1) + 1;
96
+ if (lineStart < cursor) return markdown.slice(cursor, matchStart);
97
+
98
+ const lineEndIndex = markdown.indexOf("\n", matchEnd);
99
+ const lineEnd = lineEndIndex === -1 ? markdown.length : lineEndIndex;
100
+ const beforeLink = markdown.slice(lineStart, matchStart);
101
+ const afterLink = markdown.slice(matchEnd, lineEnd);
102
+ const standaloneListMarker = /^[ \t]*(?:[-+*]|\d+[.)])[ \t]+$/u.test(beforeLink);
103
+ if (!standaloneListMarker || !/^[ \t]*\r?$/u.test(afterLink)) {
104
+ return markdown.slice(cursor, matchStart);
105
+ }
106
+
107
+ return markdown.slice(cursor, lineStart);
108
+ }
109
+
89
110
  function appendMarkdown(elements: Array<Record<string, unknown>>, content: string): void {
90
- if (!content) return;
111
+ if (!content.trim()) return;
91
112
  elements.push({ tag: "markdown", content });
92
113
  }
package/src/index.ts CHANGED
@@ -44,6 +44,7 @@ import { SafeRestartScheduler } from "./supervision/SafeRestartScheduler.js";
44
44
  import type { RestartNotificationTarget } from "./supervision/SafeRestartScheduler.js";
45
45
  import { SafeRestartNotifier } from "./supervision/SafeRestartNotifier.js";
46
46
  import { privateRestartNotificationTarget } from "./supervision/restartNotificationTarget.js";
47
+ import { LocalFileViewerServer } from "./local-files/LocalFileViewerServer.js";
47
48
  import {
48
49
  nodeDiagnosticReportArguments,
49
50
  prepareCrashReportDirectory,
@@ -72,6 +73,22 @@ const config = loadConfig();
72
73
  const transport = consoleOnly ? "console" : requireServerFeishuTransport(config.feishu);
73
74
  const logger = createLogger(config);
74
75
  const store = new StateStore(config.storage.sqlitePath);
76
+ let localFileViewer: LocalFileViewerServer | undefined;
77
+ if (config.fileViewer.enabled) {
78
+ const viewer = new LocalFileViewerServer({
79
+ host: config.fileViewer.host,
80
+ port: config.fileViewer.port,
81
+ publicBaseUrl: config.fileViewer.publicBaseUrl,
82
+ stateDirectory: path.join(path.dirname(config.storage.sqlitePath), "file-viewer"),
83
+ });
84
+ try {
85
+ const address = await viewer.start();
86
+ localFileViewer = viewer;
87
+ logger.info(address, "Local file viewer started.");
88
+ } catch (error) {
89
+ logger.warn({ error }, "Local file viewer failed to start; local file links will remain plain text.");
90
+ }
91
+ }
75
92
 
76
93
  const configuredConfigPath = process.env.AGENT_BOT_CONFIG?.trim();
77
94
  const activeConfigPath = configuredConfigPath ? resolveUserPath(configuredConfigPath) : defaultConfigPath();
@@ -98,6 +115,7 @@ if (feishuOutbound) {
98
115
  normalIntervalMs: 2_000,
99
116
  criticalGapMs: 500,
100
117
  onError: (error) => logger.warn({ error }, "Failed to update Agent progress card."),
118
+ localFileUrl: (filePath, reference) => localFileViewer?.createFileUrl(filePath, reference),
101
119
  });
102
120
  routes.push({ matches: (contextKey) => !contextKey.startsWith("console:"), outbound: feishuOutbound, presenter });
103
121
  const defaultAgentName = config.defaults.agent!;
@@ -215,7 +233,7 @@ function recoverPendingSelfUpdate(): void {
215
233
  if (!pending || pendingSelfUpdatePlanPath || restartRequested || safeRestart.scheduled) return;
216
234
  let notificationTarget: RestartNotificationTarget | undefined;
217
235
  try {
218
- notificationTarget = inferControlRestartTarget(pending.plan.notificationSessionId);
236
+ notificationTarget = inferServerRestartTarget(pending.plan.notificationSessionId);
219
237
  } catch (error) {
220
238
  logger.warn({ error }, "Could not restore the self-update notification target; update recovery will continue.");
221
239
  }
@@ -223,6 +241,9 @@ function recoverPendingSelfUpdate(): void {
223
241
  safeRestart.schedule(
224
242
  pending.plan.reason ?? `Resume Agent Bot update to ${pending.plan.toVersion}`,
225
243
  notificationTarget,
244
+ {
245
+ restartImmediatelyIfIdle: isServerIdle(store.getServerActivityState()),
246
+ },
226
247
  );
227
248
  logger.warn(
228
249
  { planPath: pending.planPath, toVersion: pending.plan.toVersion },
@@ -390,12 +411,15 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
390
411
  return { ok: false, message: "A restart or update is already pending." };
391
412
  }
392
413
  assertSelfUpdatePlanPath(request.planPath, agentBotHome());
393
- const notificationTarget = inferControlRestartTarget(request.notificationSessionId);
414
+ const notificationTarget = inferServerRestartTarget(request.notificationSessionId);
415
+ const restartImmediatelyIfIdle = isServerIdle(store.getServerActivityState());
394
416
  pendingSelfUpdatePlanPath = request.planPath;
395
- safeRestart.schedule(request.reason, notificationTarget);
417
+ safeRestart.schedule(request.reason, notificationTarget, { restartImmediatelyIfIdle });
396
418
  return {
397
419
  ok: true,
398
- message: "Agent Bot update scheduled after active tasks finish and the server becomes idle.",
420
+ message: restartImmediatelyIfIdle
421
+ ? "Agent Bot update will start immediately because the server is idle."
422
+ : "Agent Bot update scheduled after active tasks finish and the server becomes idle.",
399
423
  };
400
424
  }
401
425
  case "server_stop":
@@ -517,6 +541,7 @@ async function shutdown(
517
541
  ]);
518
542
  runtimes.close();
519
543
  await controlServer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local control endpoint."));
544
+ await localFileViewer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local file viewer."));
520
545
  store.close();
521
546
  if (restartReason) startReplacementSupervisor(restartReason, restartNotificationTargets);
522
547
  process.exit(exitCode);
@@ -586,6 +611,10 @@ function inferServerRestartTarget(notificationSessionId?: string): RestartNotifi
586
611
  return privateRestartNotificationTarget(config.feishu.userOpenId);
587
612
  }
588
613
 
614
+ function isServerIdle(activity: { runningSessions: number; pendingFinalDeliveries: number }): boolean {
615
+ return activity.runningSessions === 0 && activity.pendingFinalDeliveries === 0;
616
+ }
617
+
589
618
  function restartNotificationTargetForSession(
590
619
  session: { localSessionId: string; contextKey: string },
591
620
  ): RestartNotificationTarget {