@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.
- package/CHANGELOG.md +8 -0
- package/README.md +3 -1
- package/README.zh.md +3 -1
- package/config.example.yaml +12 -0
- package/dist/config/loadConfig.js +10 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +6 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +42 -13
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +33 -10
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/LocalImageMarkdown.js +16 -2
- package/dist/feishu/LocalImageMarkdown.js.map +1 -1
- package/dist/index.js +33 -5
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +706 -0
- package/dist/local-files/LocalFileViewerServer.js.map +1 -0
- package/dist/local-files/NetworkAddressSelector.js +97 -0
- package/dist/local-files/NetworkAddressSelector.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +43 -29
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +15 -2
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/docs/technical-reference.md +8 -0
- package/docs/technical-reference.zh.md +8 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +1 -1
- package/src/config/loadConfig.ts +10 -0
- package/src/config/schema.ts +9 -0
- package/src/feishu/FeishuMarkdown.ts +51 -12
- package/src/feishu/FeishuTurnPresenter.ts +41 -11
- package/src/feishu/LocalImageMarkdown.ts +23 -2
- package/src/index.ts +33 -4
- package/src/local-files/LocalFileViewerServer.ts +774 -0
- package/src/local-files/NetworkAddressSelector.ts +111 -0
- package/src/proxy/ProxySessionController.ts +33 -20
- 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
|
|
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":"
|
|
1
|
+
{"version":3,"file":"SafeRestartScheduler.js","sourceRoot":"","sources":["../../src/supervision/SafeRestartScheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -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` 补充权限。
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keyou007/agent-bot",
|
|
3
|
-
"version": "0.1.17-alpha.
|
|
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.
|
|
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
|
@@ -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
|
|
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
|
|
package/src/config/loadConfig.ts
CHANGED
|
@@ -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"
|
package/src/config/schema.ts
CHANGED
|
@@ -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]*)\]\((
|
|
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
|
|
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(
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
359
|
+
await this.outbound.updateInteractiveCard(targetMessageId, this.renderTurn(entry.state));
|
|
359
360
|
} else {
|
|
360
|
-
await this.outbound.sendInteractiveCard(contextKey, this.
|
|
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.
|
|
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.
|
|
379
|
-
: this.
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 {
|