@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
@@ -0,0 +1,111 @@
1
+ import os from "node:os";
2
+
3
+ export type NetworkConnectionKind = "wired" | "wifi" | "other" | "vpn";
4
+
5
+ export interface PreferredNetworkAddress {
6
+ address: string;
7
+ interfaceName: string;
8
+ kind: NetworkConnectionKind;
9
+ }
10
+
11
+ type NetworkInterfaces = ReturnType<typeof os.networkInterfaces>;
12
+
13
+ const KIND_PRIORITY: Record<NetworkConnectionKind, number> = {
14
+ wired: 0,
15
+ wifi: 1,
16
+ other: 2,
17
+ vpn: 3,
18
+ };
19
+
20
+ const VIRTUAL_INTERFACE = /(?:loopback|hyper[- ]?v|vEthernet|vmware|virtualbox|docker|wsl|bluetooth|npcap|container|\bbridge\b|^br[-_]|^virbr|^veth|^awdl|^llw)/iu;
21
+ const VPN_INTERFACE = /(?:\bvpn\b|wireguard|tailscale|zerotier|hamachi|nordlynx|openvpn|anyconnect|fortinet|globalprotect|^utun\d*$|^tun\d*$|^tap\d*$|^wg\d*$|^ppp\d*$|^ipsec\d*$)/iu;
22
+ const WIFI_INTERFACE = /(?:wi-?fi|wireless|wlan|airport|无线|^wl[a-z0-9_-]*)/iu;
23
+ const WIRED_INTERFACE = /(?:ethernet|以太网|local area connection|本地连接|\blan\b|^(?:eth|enp|ens|eno|enx)[a-z0-9_-]*)/iu;
24
+
25
+ export function selectPreferredNetworkAddress(
26
+ interfaces: NetworkInterfaces = os.networkInterfaces(),
27
+ ): PreferredNetworkAddress | undefined {
28
+ const candidates: Array<PreferredNetworkAddress & { privateAddress: boolean }> = [];
29
+
30
+ for (const [interfaceName, addresses] of Object.entries(interfaces)) {
31
+ if (!addresses || isExcludedInterface(interfaceName)) continue;
32
+ const kind = classifyInterface(interfaceName);
33
+ for (const address of addresses) {
34
+ if (address.internal || address.family !== "IPv4" || !isUsableIpv4(address.address)) continue;
35
+ candidates.push({
36
+ address: address.address,
37
+ interfaceName,
38
+ kind,
39
+ privateAddress: isPrivateIpv4(address.address),
40
+ });
41
+ }
42
+ }
43
+
44
+ candidates.sort((left, right) => {
45
+ const kindDifference = KIND_PRIORITY[left.kind] - KIND_PRIORITY[right.kind];
46
+ if (kindDifference !== 0) return kindDifference;
47
+ if (left.privateAddress !== right.privateAddress) return left.privateAddress ? -1 : 1;
48
+ const interfaceDifference = compareText(left.interfaceName, right.interfaceName);
49
+ return interfaceDifference !== 0 ? interfaceDifference : compareIpv4(left.address, right.address);
50
+ });
51
+
52
+ const selected = candidates[0];
53
+ return selected
54
+ ? { address: selected.address, interfaceName: selected.interfaceName, kind: selected.kind }
55
+ : undefined;
56
+ }
57
+
58
+ export function classifyInterface(interfaceName: string): NetworkConnectionKind {
59
+ if (VPN_INTERFACE.test(interfaceName)) return "vpn";
60
+ if (WIFI_INTERFACE.test(interfaceName)) return "wifi";
61
+ if (WIRED_INTERFACE.test(interfaceName)) return "wired";
62
+ return "other";
63
+ }
64
+
65
+ function isExcludedInterface(interfaceName: string): boolean {
66
+ return /^lo\d*$/iu.test(interfaceName) || VIRTUAL_INTERFACE.test(interfaceName);
67
+ }
68
+
69
+ function isUsableIpv4(address: string): boolean {
70
+ const parts = ipv4Parts(address);
71
+ if (!parts) return false;
72
+ const [first, second] = parts;
73
+ return first !== 0
74
+ && first !== 127
75
+ && first < 224
76
+ && !(first === 169 && second === 254);
77
+ }
78
+
79
+ function isPrivateIpv4(address: string): boolean {
80
+ const parts = ipv4Parts(address);
81
+ if (!parts) return false;
82
+ const [first, second] = parts;
83
+ return first === 10
84
+ || (first === 172 && second >= 16 && second <= 31)
85
+ || (first === 192 && second === 168)
86
+ || (first === 100 && second >= 64 && second <= 127);
87
+ }
88
+
89
+ function ipv4Parts(address: string): [number, number, number, number] | undefined {
90
+ const values = address.split(".").map((part) => Number(part));
91
+ if (values.length !== 4 || values.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
92
+ return undefined;
93
+ }
94
+ return values as [number, number, number, number];
95
+ }
96
+
97
+ function compareText(left: string, right: string): number {
98
+ const normalizedLeft = left.toLocaleLowerCase("en-US");
99
+ const normalizedRight = right.toLocaleLowerCase("en-US");
100
+ return normalizedLeft < normalizedRight ? -1 : normalizedLeft > normalizedRight ? 1 : 0;
101
+ }
102
+
103
+ function compareIpv4(left: string, right: string): number {
104
+ const leftParts = ipv4Parts(left) ?? [0, 0, 0, 0];
105
+ const rightParts = ipv4Parts(right) ?? [0, 0, 0, 0];
106
+ for (let index = 0; index < leftParts.length; index += 1) {
107
+ const difference = leftParts[index]! - rightParts[index]!;
108
+ if (difference !== 0) return difference;
109
+ }
110
+ return 0;
111
+ }
@@ -41,6 +41,7 @@ import {
41
41
  } from "../feishu/GroupNameFormatter.js";
42
42
  import { normalizeFeishuPostText } from "../feishu/InboundText.js";
43
43
  import { allowsFeishuUser } from "../feishu/ownerAccess.js";
44
+ import { classifyFileContent } from "../local-files/LocalFileViewerServer.js";
44
45
  import type { OutboundRouter } from "../presentation/OutboundRouter.js";
45
46
  import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.js";
46
47
  import { buildTurnGraphRows } from "../presentation/turnGraph.js";
@@ -131,17 +132,12 @@ const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
131
132
  const FORWARD_ATTACHMENT_WINDOW_MS = 800;
132
133
  const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
133
134
  const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
135
+ const TURN_STOP_REQUESTED_MESSAGE = "正在停止当前任务,请稍候。状态卡片将在任务停止后自动更新。";
134
136
  const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
135
137
  const WINDOWS_DRIVES_DIRECTORY = "agentbot://windows-drives";
136
138
  const IMAGE_FILE_EXTENSIONS = new Set([
137
139
  ".avif", ".bmp", ".gif", ".heic", ".heif", ".ico", ".jpeg", ".jpg", ".png", ".svg", ".tif", ".tiff", ".webp",
138
140
  ]);
139
- const BINARY_FILE_EXTENSIONS = new Set([
140
- ".7z", ".a", ".apk", ".avi", ".bin", ".bz2", ".class", ".dat", ".db", ".deb", ".dll", ".dmg", ".doc", ".docx",
141
- ".dylib", ".eot", ".exe", ".flac", ".gz", ".ico", ".ipa", ".iso", ".jar", ".lib", ".mdb", ".mkv", ".mov", ".mp3",
142
- ".mp4", ".msi", ".o", ".obj", ".ogg", ".otf", ".pdf", ".ppt", ".pptx", ".rar", ".rpm", ".so", ".sqlite", ".tar",
143
- ".ttf", ".wav", ".webm", ".woff", ".woff2", ".xls", ".xlsx", ".xz", ".zip", ".zst",
144
- ]);
145
141
 
146
142
  interface HelpCommandDefinition {
147
143
  command: string;
@@ -1675,9 +1671,10 @@ export class ProxySessionController {
1675
1671
  await this.assertBrowsableDirectory(directory);
1676
1672
  const entries = (await fs.readdir(directory, { withFileTypes: true }))
1677
1673
  .map((entry) => ({
1674
+ dirent: entry,
1678
1675
  name: entry.name,
1679
1676
  path: path.join(directory, entry.name),
1680
- kind: directoryBrowserEntryKind(entry),
1677
+ kind: entry.isDirectory() ? "directory" as const : "file" as const,
1681
1678
  }))
1682
1679
  .sort((left, right) => Number(right.kind === "directory") - Number(left.kind === "directory")
1683
1680
  || left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: "base" }));
@@ -1690,7 +1687,15 @@ export class ProxySessionController {
1690
1687
  page,
1691
1688
  totalPages,
1692
1689
  totalEntries: entries.length,
1693
- entries: entries.slice(page * DIRECTORY_PAGE_SIZE, (page + 1) * DIRECTORY_PAGE_SIZE),
1690
+ entries: entries
1691
+ .slice(page * DIRECTORY_PAGE_SIZE, (page + 1) * DIRECTORY_PAGE_SIZE)
1692
+ .map((entry) => ({
1693
+ name: entry.name,
1694
+ path: entry.path,
1695
+ kind: entry.kind === "directory"
1696
+ ? "directory" as const
1697
+ : directoryBrowserEntryKind(entry.dirent, entry.path),
1698
+ })),
1694
1699
  };
1695
1700
  }
1696
1701
 
@@ -4247,7 +4252,7 @@ export class ProxySessionController {
4247
4252
  remoteSessionId: record.remoteSessionId,
4248
4253
  turnId,
4249
4254
  });
4250
- await this.outbound.sendText(record.contextKey, `已向 Agent 发送 Interrupt 请求:${turnId}`);
4255
+ await this.outbound.sendText(record.contextKey, TURN_STOP_REQUESTED_MESSAGE);
4251
4256
  return;
4252
4257
  }
4253
4258
 
@@ -5347,9 +5352,10 @@ export class ProxySessionController {
5347
5352
  }
5348
5353
  const entries = directoryEntries
5349
5354
  .map((entry) => ({
5355
+ dirent: entry,
5350
5356
  name: entry.name,
5351
5357
  path: path.join(directory, entry.name),
5352
- kind: directoryBrowserEntryKind(entry),
5358
+ kind: entry.isDirectory() ? "directory" as const : "file" as const,
5353
5359
  }))
5354
5360
  .sort((left, right) => Number(right.kind === "directory") - Number(left.kind === "directory")
5355
5361
  || left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: "base" }));
@@ -5377,8 +5383,9 @@ export class ProxySessionController {
5377
5383
  ...parentEntries,
5378
5384
  ...entries
5379
5385
  .slice(offset, offset + pageSize)
5380
- .map((entry): DirectoryBrowserCardEntry => entry.kind === "directory"
5381
- ? {
5386
+ .map((entry): DirectoryBrowserCardEntry => {
5387
+ if (entry.kind === "directory") {
5388
+ return {
5382
5389
  name: entry.name,
5383
5390
  kind: entry.kind,
5384
5391
  openAction: {
@@ -5389,10 +5396,11 @@ export class ProxySessionController {
5389
5396
  contextKey,
5390
5397
  },
5391
5398
  },
5392
- }
5393
- : {
5399
+ };
5400
+ }
5401
+ return {
5394
5402
  name: entry.name,
5395
- kind: entry.kind,
5403
+ kind: directoryBrowserEntryKind(entry.dirent, entry.path),
5396
5404
  openAction: {
5397
5405
  text: entry.name,
5398
5406
  value: {
@@ -5401,7 +5409,8 @@ export class ProxySessionController {
5401
5409
  contextKey,
5402
5410
  },
5403
5411
  },
5404
- }),
5412
+ };
5413
+ }),
5405
5414
  ];
5406
5415
  const navigationActions = directoryPaginationActions(directory, page, totalPages, contextKey);
5407
5416
  const directoryCount = entries.filter((entry) => entry.kind === "directory").length;
@@ -5903,7 +5912,7 @@ export class ProxySessionController {
5903
5912
  turnId,
5904
5913
  source: "sessions_card",
5905
5914
  });
5906
- await this.outbound.sendText(contextKey, `已向 Agent 发送 Interrupt 请求:${turnId}`);
5915
+ await this.outbound.sendText(contextKey, TURN_STOP_REQUESTED_MESSAGE);
5907
5916
  }
5908
5917
 
5909
5918
  private async archiveSessionReference(
@@ -7114,12 +7123,16 @@ function isWindowsDriveRoot(directory: string): boolean {
7114
7123
  return /^[a-z]:\\$/iu.test(path.win32.normalize(directory));
7115
7124
  }
7116
7125
 
7117
- function directoryBrowserEntryKind(entry: Dirent): DirectoryBrowserCardEntry["kind"] {
7126
+ function directoryBrowserEntryKind(entry: Dirent, filePath: string): DirectoryBrowserCardEntry["kind"] {
7118
7127
  if (entry.isDirectory()) return "directory";
7128
+ try {
7129
+ if (classifyFileContent(filePath).kind === "text") return "file";
7130
+ } catch {
7131
+ return "file";
7132
+ }
7119
7133
  const extension = path.extname(entry.name).toLowerCase();
7120
7134
  if (IMAGE_FILE_EXTENSIONS.has(extension)) return "image";
7121
- if (BINARY_FILE_EXTENSIONS.has(extension)) return "binary";
7122
- return "file";
7135
+ return "binary";
7123
7136
  }
7124
7137
 
7125
7138
  function directoryActionPath(value: unknown): string {
@@ -35,6 +35,10 @@ export interface SafeRestartSchedulerOptions {
35
35
  pollIntervalMs?: number;
36
36
  }
37
37
 
38
+ export interface SafeRestartScheduleOptions {
39
+ restartImmediatelyIfIdle?: boolean;
40
+ }
41
+
38
42
  export class SafeRestartScheduler {
39
43
  private timer?: NodeJS.Timeout;
40
44
  private polling?: Promise<void>;
@@ -44,6 +48,7 @@ export class SafeRestartScheduler {
44
48
  private idleSince?: number;
45
49
  private idleInboundAt?: string;
46
50
  private scheduleId = 0;
51
+ private restartImmediatelyIfIdle = false;
47
52
 
48
53
  constructor(private readonly options: SafeRestartSchedulerOptions) {}
49
54
 
@@ -55,10 +60,17 @@ export class SafeRestartScheduler {
55
60
  return this.reason;
56
61
  }
57
62
 
58
- schedule(reason: string, notificationTarget?: RestartNotificationTarget): boolean {
63
+ schedule(
64
+ reason: string,
65
+ notificationTarget?: RestartNotificationTarget,
66
+ options: SafeRestartScheduleOptions = {},
67
+ ): boolean {
59
68
  const newlyScheduled = !this.reason;
60
69
  if (newlyScheduled) {
61
70
  this.notificationTargets = [];
71
+ this.restartImmediatelyIfIdle = options.restartImmediatelyIfIdle === true;
72
+ } else if (options.restartImmediatelyIfIdle) {
73
+ this.restartImmediatelyIfIdle = true;
62
74
  }
63
75
  this.scheduleId += 1;
64
76
  this.addNotificationTarget(notificationTarget, reason);
@@ -104,6 +116,7 @@ export class SafeRestartScheduler {
104
116
  this.notificationTargets = [];
105
117
  this.idleSince = undefined;
106
118
  this.idleInboundAt = undefined;
119
+ this.restartImmediatelyIfIdle = false;
107
120
  }
108
121
 
109
122
  private async poll(): Promise<void> {
@@ -123,6 +136,10 @@ export class SafeRestartScheduler {
123
136
  });
124
137
  return;
125
138
  }
139
+ if (this.restartImmediatelyIfIdle) {
140
+ await this.restart(reason, scheduleId, state);
141
+ return;
142
+ }
126
143
  const quietPeriodMs = this.options.quietPeriodMs ?? 15_000;
127
144
  const latestInboundAt = state.latestInboundAt ?? "";
128
145
  if (this.idleSince === undefined || this.idleInboundAt !== latestInboundAt) {
@@ -178,6 +195,10 @@ export class SafeRestartScheduler {
178
195
  }
179
196
  return;
180
197
  }
198
+ await this.restart(reason, scheduleId, confirmed);
199
+ }
200
+
201
+ private async restart(reason: string, scheduleId: number, activity: ServerActivityState): Promise<void> {
181
202
  const notificationTargets = this.notificationTargetsSnapshot();
182
203
  this.clear();
183
204
  await this.emitStatus({
@@ -185,7 +206,7 @@ export class SafeRestartScheduler {
185
206
  reason,
186
207
  notificationTargets,
187
208
  phase: "restarting",
188
- activity: confirmed,
209
+ activity,
189
210
  remainingMs: 0,
190
211
  });
191
212
  await this.options.onReady(reason, notificationTargets);