@keyou007/agent-bot 0.1.2 → 0.1.4
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/.env.example +3 -1
- package/CHANGELOG.md +15 -0
- package/README.md +23 -8
- package/README.zh.md +23 -8
- package/config.example.yaml +1 -0
- package/dist/cli/FeishuAppConfiguration.js +10 -10
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/FeishuAppRegistration.js +21 -11
- package/dist/cli/FeishuAppRegistration.js.map +1 -1
- package/dist/cli/Initializer.js +14 -8
- package/dist/cli/Initializer.js.map +1 -1
- package/dist/cli/LocalControlClient.js +2 -2
- package/dist/cli/LocalControlClient.js.map +1 -1
- package/dist/cli/LocalControlServer.js +1 -1
- package/dist/cli/LocalControlServer.js.map +1 -1
- package/dist/cli/ServerStarter.js +69 -0
- package/dist/cli/ServerStarter.js.map +1 -0
- package/dist/cli/SkillRegistry.js +4 -4
- package/dist/cli/SkillRegistry.js.map +1 -1
- package/dist/cli/help.js +5 -56
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/packageVersion.js +1 -1
- package/dist/cli/packageVersion.js.map +1 -1
- package/dist/cli/profile.js +40 -0
- package/dist/cli/profile.js.map +1 -0
- package/dist/cli/serverStatus.js +37 -0
- package/dist/cli/serverStatus.js.map +1 -0
- package/dist/cli/taskChatRoute.js +2 -2
- package/dist/cli/taskChatRoute.js.map +1 -1
- package/dist/cli.js +148 -168
- package/dist/cli.js.map +1 -1
- package/dist/config/loadConfig.js +7 -2
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/paths.js +1 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/schema.js +1 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +21 -3
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +7 -76
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/transport.js +1 -1
- package/dist/feishu/transport.js.map +1 -1
- package/dist/index.js +18 -9
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +28 -13
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/startup/StartupNotifier.js +13 -2
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/startup/startFeishu.js +1 -1
- package/dist/startup/startFeishu.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +23 -7
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +20 -2
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/supervision/SupervisorDiagnostics.js +209 -0
- package/dist/supervision/SupervisorDiagnostics.js.map +1 -0
- package/dist/supervisor.js +47 -12
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +25 -5
- package/docs/technical-reference.zh.md +25 -5
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +24 -4
- package/src/cli/FeishuAppConfiguration.ts +10 -10
- package/src/cli/FeishuAppRegistration.ts +23 -10
- package/src/cli/Initializer.ts +14 -7
- package/src/cli/LocalControlClient.ts +2 -2
- package/src/cli/LocalControlServer.ts +1 -1
- package/src/cli/ServerStarter.ts +106 -0
- package/src/cli/SkillRegistry.ts +4 -4
- package/src/cli/help.ts +5 -65
- package/src/cli/packageVersion.ts +1 -1
- package/src/cli/profile.ts +59 -0
- package/src/cli/serverStatus.ts +39 -0
- package/src/cli/taskChatRoute.ts +2 -2
- package/src/cli.ts +160 -167
- package/src/config/loadConfig.ts +12 -2
- package/src/config/paths.ts +1 -0
- package/src/config/schema.ts +1 -0
- package/src/feishu/CardRenderer.ts +25 -3
- package/src/feishu/FeishuConnector.ts +6 -87
- package/src/feishu/transport.ts +1 -1
- package/src/index.ts +22 -9
- package/src/proxy/ProxySessionController.ts +28 -13
- package/src/startup/StartupNotifier.ts +21 -4
- package/src/startup/startFeishu.ts +1 -1
- package/src/supervision/SafeRestartNotifier.ts +22 -7
- package/src/supervision/SafeRestartScheduler.ts +27 -3
- package/src/supervision/SupervisorDiagnostics.ts +268 -0
- package/src/supervisor.ts +46 -11
|
@@ -33,6 +33,10 @@ Agent Bot 是基于 Node.js 22+、ESM 和 TypeScript 的应用,主要组件如
|
|
|
33
33
|
|
|
34
34
|
默认用户数据根目录为 `~/.agent-bot`,可通过 `AGENT_BOT_HOME` 整体替换。
|
|
35
35
|
|
|
36
|
+
CLI 还支持显式指定目录的 Profile。不使用 `--profile` 时,命令使用主 Profile 以及原有的环境变量路径规则。`--profile <目录>` 会为当前命令及其启动的 Supervisor、Worker 固定 `AGENT_BOT_HOME` 和 `AGENT_BOT_CONFIG`,配置文件固定为 `<目录>/config.yaml`。它还会先清除继承的飞书凭据环境变量,再加载所选 Profile 的 `.env`,避免从主 Agent Bot 进程树中启动辅助实例时误用主机器人的凭据。`--profile` 不能和 `--config` 同时使用。其他 Profile 必须在每次命令中显式选择;Agent Bot 不维护按名称注册的 Profile。
|
|
37
|
+
|
|
38
|
+
CLI 界面文本不受系统语言影响,统一使用英文。系统生成的重启原因仍使用中文,因为它会显示在中文 Lark 状态卡中;显式传入的 `--reason` 保持原样。`agent-bot server status` 会报告运行中 Worker 的 Lark App ID,JSON 字段名为 `feishuAppId`;服务停止或旧版健康协议尚未提供该字段时,CLI 会回退到当前 Profile 配置的 App ID。
|
|
39
|
+
|
|
36
40
|
| 默认路径 | 内容 |
|
|
37
41
|
| ------------------------------------ | -------------------- |
|
|
38
42
|
| `~/.agent-bot/config.yaml` | YAML 主配置 |
|
|
@@ -43,9 +47,10 @@ Agent Bot 是基于 Node.js 22+、ESM 和 TypeScript 的应用,主要组件如
|
|
|
43
47
|
|
|
44
48
|
配置文件优先级:
|
|
45
49
|
|
|
46
|
-
1. CLI `--config
|
|
47
|
-
2.
|
|
48
|
-
3.
|
|
50
|
+
1. CLI `--profile <目录>` 选择 `<目录>/config.yaml`
|
|
51
|
+
2. CLI `--config <路径>`
|
|
52
|
+
3. `AGENT_BOT_CONFIG`
|
|
53
|
+
4. `<AGENT_BOT_HOME>/config.yaml`,默认为 `~/.agent-bot/config.yaml`
|
|
49
54
|
|
|
50
55
|
默认 `.env` 始终从 Agent Bot 用户目录加载。加载 `.env` 后,YAML 中的 `${NAME}` 会使用进程环境变量展开。
|
|
51
56
|
|
|
@@ -59,6 +64,7 @@ Agent Bot 是基于 Node.js 22+、ESM 和 TypeScript 的应用,主要组件如
|
|
|
59
64
|
|
|
60
65
|
- 进程环境变量优先于 `~/.agent-bot/.env`
|
|
61
66
|
- `FEISHU_APP_ID` 和 `FEISHU_APP_SECRET` 必须同时存在
|
|
67
|
+
- 一键注册还会把授权用户的 `open_id` 保存为 `FEISHU_USER_OPEN_ID`
|
|
62
68
|
- 除非使用 `--reconfigure-feishu`,完整凭据不会被替换
|
|
63
69
|
- 缺少凭据或只有一项时会重新创建应用
|
|
64
70
|
- 新创建的凭据经过 fsync、原子替换写入 `.env`,并在配置权限前读回校验
|
|
@@ -74,6 +80,8 @@ Agent Bot 是基于 Node.js 22+、ESM 和 TypeScript 的应用,主要组件如
|
|
|
74
80
|
|
|
75
81
|
生成的授权链接不包含 App Secret。
|
|
76
82
|
|
|
83
|
+
飞书初始化成功后,CLI 会先释放初始化锁,再通过与 `agent-bot server start` 相同的就绪检查流程启动后台 Supervisor,并等待最多 45 秒,直到 Worker 连接飞书并进入就绪状态。如果当前 Profile 的服务已经运行,不会创建第二个 Supervisor。`--skip-feishu` 会跳过自动启动;`--json` 会把结果写入 `server.status`,不会混入非 JSON 文本。
|
|
84
|
+
|
|
77
85
|
## 飞书应用要求
|
|
78
86
|
|
|
79
87
|
基础消息能力依赖以下核心配置:
|
|
@@ -107,6 +115,7 @@ Agent Bot 是基于 Node.js 22+、ESM 和 TypeScript 的应用,主要组件如
|
|
|
107
115
|
feishu:
|
|
108
116
|
appId: "${FEISHU_APP_ID}"
|
|
109
117
|
appSecret: "${FEISHU_APP_SECRET}"
|
|
118
|
+
userOpenId: "${FEISHU_USER_OPEN_ID}"
|
|
110
119
|
|
|
111
120
|
console:
|
|
112
121
|
enabled: true
|
|
@@ -131,7 +140,7 @@ logging:
|
|
|
131
140
|
path: "./logs/agent-bot.log"
|
|
132
141
|
```
|
|
133
142
|
|
|
134
|
-
`agent-bot server start` 要求同时配置飞书 `appId` 和 `appSecret
|
|
143
|
+
`agent-bot server start` 要求同时配置飞书 `appId` 和 `appSecret`。Worker 启动 SDK 长连接时不读取 SDK 日志或私有连接状态,随后通过发送启动状态卡片检查出站能力。通知通常发往已知私聊和最近活跃的群聊;数据库里尚无已知会话时,改用 `feishu.userOpenId`,按 `open_id` 私聊发送。存在通知目标时,至少一张卡片发送成功后 Server 才报告就绪,单个目标发送失败仍相互隔离。如果既没有已知会话,也没有 `feishu.userOpenId`,启动会跳过出站检查并继续。缺少凭据时仍会启动失败并提示先初始化。`agent-bot console` 是明确的纯本地入口,不需要飞书凭据。
|
|
135
144
|
|
|
136
145
|
必须至少配置一个 Agent,`defaults.agent` 必须指向已配置的 Agent。
|
|
137
146
|
|
|
@@ -175,7 +184,7 @@ AGENT_BOT=1
|
|
|
175
184
|
|
|
176
185
|
每个 turn 只有一张进度卡。普通更新最多每两秒一次,关键更新最短间隔 500 毫秒。完成时先把进度卡更新为终态,再单独发送 Markdown 最终回答。
|
|
177
186
|
|
|
178
|
-
|
|
187
|
+
完成持久化消息去重占位后,Agent Bot 会等待 `OnIt` 表情添加成功,再进行聊天信息持久化、图片下载、队列等待、命令执行或 Runtime 调用。Turn 成功、失败或取消时分别替换为 `DONE`、`ERROR` 或 `CrossMark`。表情操作失败会记录日志,但不会阻塞任务。
|
|
179
188
|
|
|
180
189
|
富文本图片会下载到输入图片缓存,并以 `localImage` 传给 Codex。纯图片消息使用默认 Prompt `请分析这张图片。`。ACP Runtime 不支持图片输入时会明确报错。
|
|
181
190
|
|
|
@@ -214,6 +223,15 @@ SQLite 保存:
|
|
|
214
223
|
|
|
215
224
|
`agent-bot server start` 启动后台 supervisor。Worker 意外退出后会自动拉起;连续崩溃时使用 1 到 30 秒的指数退避。
|
|
216
225
|
|
|
226
|
+
Supervisor 的崩溃诊断文件按当前 Profile 隔离:
|
|
227
|
+
|
|
228
|
+
- `logs/supervisor.log` 持久记录 Worker PID、退出码、运行时长、重启延迟和诊断文件路径
|
|
229
|
+
- `logs/worker.stderr.log` 保存后台进程原本会丢失的 Node/V8 fatal 输出;两类诊断日志达到 10 MiB 时轮转,保留三个备份
|
|
230
|
+
- `data/last-crash.json` 指向最近一次 Worker 异常退出,带时间戳的 `data/crash-reports/crash-*.json` 保留历史记录
|
|
231
|
+
- Node 能生成报告时,写入 `data/crash-reports/report.*.json`
|
|
232
|
+
|
|
233
|
+
Supervisor、Worker、替换 Supervisor 和 Console Worker 默认启用 Node fatal error 与未捕获异常报告。Node 版本支持时还会排除环境变量和网络接口,避免凭据进入报告。主动重启退出码 `75` 和停止退出码 `76` 不会创建崩溃清单。
|
|
234
|
+
|
|
217
235
|
安全重启会等待:
|
|
218
236
|
|
|
219
237
|
1. 活动任务完成
|
|
@@ -224,6 +242,8 @@ SQLite 保存:
|
|
|
224
242
|
|
|
225
243
|
首次安全重启状态卡会延迟 3 秒发送,让任务最终回答尽可能先到达;延迟期间的状态变化会合并到首张卡片。该延迟不阻塞调度器轮询,真正关闭前会立即 flush 尚未发送的卡片。
|
|
226
244
|
|
|
245
|
+
等待中的安全重启状态卡底部带有 `Cancel` 操作。回调会携带调度器单调递增的计划 ID,因此旧卡片不会取消较新的重启。取消成功后,所有已发送的状态卡都会原地更新并移除按钮;调度器进入不可逆的正在重启阶段后也不再显示该按钮。
|
|
246
|
+
|
|
227
247
|
本地控制服务负责修改任务和请求服务重启;只读任务查询直接访问 SQLite。
|
|
228
248
|
|
|
229
249
|
## 权限模式
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keyou007/agent-bot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@keyou007/agent-bot",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.4",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@agentclientprotocol/sdk": "1.2.1",
|
package/package.json
CHANGED
|
@@ -48,10 +48,21 @@ agent-bot init
|
|
|
48
48
|
|
|
49
49
|
Initialization creates the Agent Bot home, `config.yaml`, `.env`, `data/`, and `logs/` from the bundled examples. If Feishu credentials are absent, it starts Feishu one-click app registration, prints an authorization URL and QR code, waits for the user to approve the request, and stores the returned app ID and secret in `.env`. It then audits the app's currently published tenant permissions, event subscriptions, and callbacks. Core and optional missing items produce separate incremental-configuration URLs so optional approval cannot block core setup.
|
|
50
50
|
|
|
51
|
+
After Feishu initialization succeeds, `init` releases its initialization lock and automatically starts the selected profile's server, waiting until it is ready. It does not create another supervisor when that profile is already running. `init --skip-feishu` is the exception: it prepares Console-only files and does not start the server. With `--json`, inspect `server.status` for `started`, `already-running`, or `skipped`.
|
|
52
|
+
|
|
51
53
|
When running `init` on the user's behalf, relay every exact authorization or incremental-configuration URL and let the user complete it in their own browser. The command waits only for core message receipt and delivery capabilities. Optional capabilities such as group creation, group-title synchronization, reactions, images, and card actions do not block initialization; relay the final affected-feature warnings when they remain unavailable. Neither URL contains the app secret, and the secret itself is never printed.
|
|
52
54
|
|
|
53
55
|
Existing complete Feishu credentials are preserved but still audited and repaired when needed. Missing or incomplete credentials mean app creation did not complete, so a later `init` starts a new registration instead of resuming the previous device code. Once both credentials are durably saved, an interrupted permission audit resumes against the same remote app. `init` also holds a local lock to prevent concurrent registrations. Use `agent-bot init --skip-feishu` only for console-only initialization because it skips both registration and configuration auditing. Use `agent-bot init --reconfigure-feishu` only when the user explicitly wants to replace existing complete credentials. The command respects `AGENT_BOT_HOME`, `AGENT_BOT_CONFIG`, and `--config <path>`. Use `agent-bot init --json` when structured final output helps.
|
|
54
56
|
|
|
57
|
+
For an isolated secondary bot, pass its profile directory explicitly on every command:
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
agent-bot --profile ~/.agent-bot-rescue init
|
|
61
|
+
agent-bot --profile ~/.agent-bot-rescue server status
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
No `--profile` selects the main profile at `~/.agent-bot` by default. `--profile <directory>` selects that directory's `config.yaml`, `.env`, data, logs, and local control endpoint and propagates the selection to the supervisor and worker. Alternative profiles are not registered by name. Do not combine `--profile` with `--config`.
|
|
65
|
+
|
|
55
66
|
## Start with inspection
|
|
56
67
|
|
|
57
68
|
Verify the CLI and current service state before changing anything:
|
|
@@ -63,11 +74,11 @@ agent-bot server status
|
|
|
63
74
|
agent-bot task list
|
|
64
75
|
```
|
|
65
76
|
|
|
66
|
-
`agent-bot
|
|
77
|
+
All Agent Bot CLI interface text is English regardless of system locale. `agent-bot server status` also reports the running profile's Lark App ID; with `--json`, read it from `feishuAppId`.
|
|
67
78
|
|
|
68
79
|
Add `--json` to status and list commands when machine-readable output helps. If `agent-bot` is unavailable, report that the CLI is not installed or linked instead of guessing process state.
|
|
69
80
|
|
|
70
|
-
By default Agent Bot stores user-owned configuration and runtime state in `~/.agent-bot`: config at `~/.agent-bot/config.yaml`, environment variables at `~/.agent-bot/.env`, SQLite state at `~/.agent-bot/data/agent-bot.sqlite`, logs at `~/.agent-bot/logs/agent-bot.log`, and inbound image cache next to the SQLite database. `AGENT_BOT_HOME` changes this root.
|
|
81
|
+
By default Agent Bot stores user-owned configuration and runtime state in `~/.agent-bot`: config at `~/.agent-bot/config.yaml`, environment variables at `~/.agent-bot/.env`, SQLite state at `~/.agent-bot/data/agent-bot.sqlite`, logs at `~/.agent-bot/logs/agent-bot.log`, and inbound image cache next to the SQLite database. `AGENT_BOT_HOME` changes this root. Prefer `--profile <directory>` when controlling a complete isolated instance; use `--config <path>` only when selecting a non-default configuration without changing the whole profile.
|
|
71
82
|
|
|
72
83
|
## Manage the service
|
|
73
84
|
|
|
@@ -80,12 +91,21 @@ agent-bot server restart --reason "reason for restart"
|
|
|
80
91
|
agent-bot server stop
|
|
81
92
|
```
|
|
82
93
|
|
|
83
|
-
Treat `server restart` as the normal restart path. It schedules a safe restart and waits for active tasks, pending final-message delivery, and a quiet inbound-message window. The first safe-restart status card is delayed by three seconds so the active task's final response can usually be delivered first; this presentation delay does not postpone scheduler polling.
|
|
94
|
+
Treat `server restart` as the normal restart path. It schedules a safe restart and waits for active tasks, pending final-message delivery, and a quiet inbound-message window. The first safe-restart status card is delayed by three seconds so the active task's final response can usually be delivered first; this presentation delay does not postpone scheduler polling. While the restart is pending, its status card has a bottom `Cancel` button that conditionally cancels that exact schedule and updates the card in place.
|
|
84
95
|
|
|
85
96
|
Use `agent-bot server restart --immediate` only when the user explicitly requests an immediate restart or accepts interruption. Never use `taskkill`, `Stop-Process`, or equivalent commands for routine Agent Bot restart management.
|
|
86
97
|
|
|
87
98
|
`agent-bot console` opens the console UI only when the service is stopped. Do not use `--force` while the service is live unless the user explicitly accepts concurrent state access.
|
|
88
99
|
|
|
100
|
+
When a startup card says the Worker exited and the supervisor restarted it, inspect the selected profile's persisted crash evidence before guessing at a cause:
|
|
101
|
+
|
|
102
|
+
- `data/last-crash.json` for PID, exit code, uptime, restart delay, and linked report paths
|
|
103
|
+
- `logs/supervisor.log` for the restart timeline
|
|
104
|
+
- `logs/worker.stderr.log` for Node/V8 fatal output
|
|
105
|
+
- `data/crash-reports/report.*.json` for Node diagnostic reports
|
|
106
|
+
|
|
107
|
+
Normal safe restarts and explicit stops do not overwrite the latest crash record. Every explicit profile keeps its own diagnostic files; use the same `--profile <directory>` selection when correlating service status with those paths.
|
|
108
|
+
|
|
89
109
|
## Manage tasks
|
|
90
110
|
|
|
91
111
|
List tasks before resolving a numeric reference because task numbers follow the current list order:
|
|
@@ -131,7 +151,7 @@ Messages whose trimmed text starts with `/` are always parsed as Agent Bot comma
|
|
|
131
151
|
|
|
132
152
|
When a group body has a current task, renaming the Feishu group to `[agent-name] new title` also renames that current task if the prefix matches the task's configured agent. Malformed names, agent mismatches, empty groups, and thread-specific tasks are ignored.
|
|
133
153
|
|
|
134
|
-
Use `--context <key>` or `--status <status>` to narrow `task list`. Use `--
|
|
154
|
+
Use `--context <key>` or `--status <status>` to narrow `task list`. Use `--profile <directory>` before the command when controlling an isolated Agent Bot instance, or `--config <path>` for only a non-default configuration.
|
|
135
155
|
|
|
136
156
|
## Apply code changes safely
|
|
137
157
|
|
|
@@ -178,8 +178,8 @@ export async function ensureFeishuAppConfiguration(
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
const detail = lastError instanceof Error ? `
|
|
182
|
-
throw new Error(
|
|
181
|
+
const detail = lastError instanceof Error ? ` Last check failed: ${lastError.message}` : "";
|
|
182
|
+
throw new Error(`Timed out waiting for the core Lark scopes and message event. Run agent-bot init again.${detail}`);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
async function requestMissingFeishuConfiguration(
|
|
@@ -248,14 +248,14 @@ async function readFeishuAppConfiguration(
|
|
|
248
248
|
`${FEISHU_OPEN_BASE_URL}${APPLICATION_PATH_PREFIX}/${appId}?lang=zh_cn`,
|
|
249
249
|
{ headers },
|
|
250
250
|
signal,
|
|
251
|
-
"
|
|
251
|
+
"Read Lark app configuration",
|
|
252
252
|
),
|
|
253
253
|
fetchJson(
|
|
254
254
|
fetchImpl,
|
|
255
255
|
`${FEISHU_OPEN_BASE_URL}${APPLICATION_PATH_PREFIX}/${appId}/app_versions?lang=zh_cn&page_size=2`,
|
|
256
256
|
{ headers },
|
|
257
257
|
signal,
|
|
258
|
-
"
|
|
258
|
+
"Read Lark app versions",
|
|
259
259
|
),
|
|
260
260
|
]);
|
|
261
261
|
|
|
@@ -303,10 +303,10 @@ async function getTenantAccessToken(
|
|
|
303
303
|
}),
|
|
304
304
|
},
|
|
305
305
|
signal,
|
|
306
|
-
"
|
|
306
|
+
"Get the Lark tenant_access_token",
|
|
307
307
|
);
|
|
308
308
|
const token = readString(payload, "tenant_access_token");
|
|
309
|
-
if (!token) throw new Error("
|
|
309
|
+
if (!token) throw new Error("The Lark tenant_access_token response is missing the access token.");
|
|
310
310
|
return token;
|
|
311
311
|
}
|
|
312
312
|
|
|
@@ -327,12 +327,12 @@ async function fetchJson(
|
|
|
327
327
|
const response = await fetchImpl(url, { ...init, signal: controller.signal });
|
|
328
328
|
const payload = (await response.json().catch(() => undefined)) as unknown;
|
|
329
329
|
if (!isJsonObject(payload)) {
|
|
330
|
-
throw new Error(`${operation}
|
|
330
|
+
throw new Error(`${operation} returned an invalid response (HTTP ${response.status}).`);
|
|
331
331
|
}
|
|
332
332
|
const code = readNumber(payload, "code");
|
|
333
333
|
if (!response.ok || code !== 0) {
|
|
334
334
|
throw new FeishuConfigurationApiError(
|
|
335
|
-
`${operation}
|
|
335
|
+
`${operation} failed: ${readString(payload, "msg") || response.statusText || `HTTP ${response.status}`}`,
|
|
336
336
|
code,
|
|
337
337
|
response.status,
|
|
338
338
|
);
|
|
@@ -340,7 +340,7 @@ async function fetchJson(
|
|
|
340
340
|
return payload;
|
|
341
341
|
} catch (error) {
|
|
342
342
|
if (parentSignal?.aborted) throw abortError(parentSignal);
|
|
343
|
-
if (controller.signal.aborted) throw new Error(`${operation}
|
|
343
|
+
if (controller.signal.aborted) throw new Error(`${operation} timed out.`);
|
|
344
344
|
throw error;
|
|
345
345
|
} finally {
|
|
346
346
|
clearTimeout(timeout);
|
|
@@ -442,7 +442,7 @@ function positiveInteger(value: number | undefined): number | undefined {
|
|
|
442
442
|
}
|
|
443
443
|
|
|
444
444
|
function abortError(signal: AbortSignal): Error {
|
|
445
|
-
return signal.reason instanceof Error ? signal.reason : new Error("
|
|
445
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Lark app configuration was cancelled.");
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
function delay(milliseconds: number): Promise<void> {
|
|
@@ -16,6 +16,7 @@ export interface FeishuAppRegistrationChallenge {
|
|
|
16
16
|
export interface FeishuAppCredentials {
|
|
17
17
|
appId: string;
|
|
18
18
|
appSecret: string;
|
|
19
|
+
userOpenId?: string;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface FeishuAppRegistrationOptions {
|
|
@@ -40,12 +41,12 @@ export async function registerFeishuApp(options: FeishuAppRegistrationOptions =
|
|
|
40
41
|
},
|
|
41
42
|
options.signal,
|
|
42
43
|
);
|
|
43
|
-
throwRegistrationError(begin, "
|
|
44
|
+
throwRegistrationError(begin, "Could not start Lark app creation");
|
|
44
45
|
|
|
45
46
|
const deviceCode = readString(begin, "device_code");
|
|
46
47
|
const userCode = readString(begin, "user_code");
|
|
47
48
|
if (!deviceCode || !userCode) {
|
|
48
|
-
throw new Error("
|
|
49
|
+
throw new Error("The Lark app creation response is missing device_code or user_code.");
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
const expiresIn =
|
|
@@ -94,7 +95,14 @@ export async function registerFeishuApp(options: FeishuAppRegistrationOptions =
|
|
|
94
95
|
if (!errorCode) {
|
|
95
96
|
const appId = readString(payload, "client_id");
|
|
96
97
|
const appSecret = readString(payload, "client_secret");
|
|
97
|
-
if (appId && appSecret)
|
|
98
|
+
if (appId && appSecret) {
|
|
99
|
+
const userOpenId = readString(readObject(payload, "user_info"), "open_id");
|
|
100
|
+
return {
|
|
101
|
+
appId,
|
|
102
|
+
appSecret,
|
|
103
|
+
...(userOpenId ? { userOpenId } : {}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
98
106
|
continue;
|
|
99
107
|
}
|
|
100
108
|
|
|
@@ -105,16 +113,16 @@ export async function registerFeishuApp(options: FeishuAppRegistrationOptions =
|
|
|
105
113
|
intervalSeconds = Math.min(intervalSeconds + 5, MAX_POLL_INTERVAL_SECONDS);
|
|
106
114
|
continue;
|
|
107
115
|
case "access_denied":
|
|
108
|
-
throw new Error("
|
|
116
|
+
throw new Error("Lark app creation was cancelled.");
|
|
109
117
|
case "expired_token":
|
|
110
118
|
case "invalid_grant":
|
|
111
|
-
throw new Error("
|
|
119
|
+
throw new Error("The Lark app creation link expired. Run agent-bot init again.");
|
|
112
120
|
default:
|
|
113
|
-
throwRegistrationError(payload, "
|
|
121
|
+
throwRegistrationError(payload, "Lark app creation failed");
|
|
114
122
|
}
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
throw new Error("
|
|
125
|
+
throw new Error("Timed out waiting for Lark app creation. Run agent-bot init again.");
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
function buildVerificationUrl(userCode: string): string {
|
|
@@ -144,7 +152,7 @@ async function postRegistration(
|
|
|
144
152
|
});
|
|
145
153
|
const payload = (await response.json().catch(() => undefined)) as unknown;
|
|
146
154
|
if (!isRecord(payload)) {
|
|
147
|
-
throw new Error(
|
|
155
|
+
throw new Error(`The Lark app registration endpoint returned an invalid response (HTTP ${response.status}).`);
|
|
148
156
|
}
|
|
149
157
|
return payload;
|
|
150
158
|
} finally {
|
|
@@ -157,7 +165,7 @@ function throwRegistrationError(payload: RegistrationPayload, prefix: string): v
|
|
|
157
165
|
const code = readString(payload, "error");
|
|
158
166
|
if (!code) return;
|
|
159
167
|
const description = readString(payload, "error_description") || code;
|
|
160
|
-
throw new Error(`${prefix}
|
|
168
|
+
throw new Error(`${prefix}: ${description}`);
|
|
161
169
|
}
|
|
162
170
|
|
|
163
171
|
function readString(value: RegistrationPayload, key: string): string {
|
|
@@ -165,6 +173,11 @@ function readString(value: RegistrationPayload, key: string): string {
|
|
|
165
173
|
return typeof field === "string" ? field.trim() : "";
|
|
166
174
|
}
|
|
167
175
|
|
|
176
|
+
function readObject(value: RegistrationPayload, key: string): RegistrationPayload {
|
|
177
|
+
const field = value[key];
|
|
178
|
+
return isRecord(field) ? field : {};
|
|
179
|
+
}
|
|
180
|
+
|
|
168
181
|
function readPositiveInteger(value: RegistrationPayload, key: string): number | undefined {
|
|
169
182
|
const field = value[key];
|
|
170
183
|
const parsed = typeof field === "number" ? field : Number(field);
|
|
@@ -176,7 +189,7 @@ function isRecord(value: unknown): value is RegistrationPayload {
|
|
|
176
189
|
}
|
|
177
190
|
|
|
178
191
|
function abortError(signal: AbortSignal): Error {
|
|
179
|
-
return signal.reason instanceof Error ? signal.reason : new Error("
|
|
192
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Lark app creation was cancelled.");
|
|
180
193
|
}
|
|
181
194
|
|
|
182
195
|
function delay(milliseconds: number): Promise<void> {
|
package/src/cli/Initializer.ts
CHANGED
|
@@ -39,11 +39,13 @@ export interface FeishuCredentialState {
|
|
|
39
39
|
status: FeishuCredentialStatus;
|
|
40
40
|
appId?: string;
|
|
41
41
|
appSecret?: string;
|
|
42
|
+
userOpenId?: string;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export interface FeishuCredentials {
|
|
45
46
|
appId: string;
|
|
46
47
|
appSecret: string;
|
|
48
|
+
userOpenId?: string;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export interface InitializationLock {
|
|
@@ -85,17 +87,21 @@ export function readFeishuCredentials(envPath: string, env: NodeJS.ProcessEnv =
|
|
|
85
87
|
const fileValues = fs.existsSync(envPath) ? parseDotEnv(fs.readFileSync(envPath, "utf8")) : {};
|
|
86
88
|
const appId = firstNonBlank(env.FEISHU_APP_ID, fileValues.FEISHU_APP_ID);
|
|
87
89
|
const appSecret = firstNonBlank(env.FEISHU_APP_SECRET, fileValues.FEISHU_APP_SECRET);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return { status: "
|
|
90
|
+
const userOpenId = firstNonBlank(env.FEISHU_USER_OPEN_ID, fileValues.FEISHU_USER_OPEN_ID);
|
|
91
|
+
const user = userOpenId ? { userOpenId } : {};
|
|
92
|
+
if (appId && appSecret) return { status: "configured", appId, appSecret, ...user };
|
|
93
|
+
if (appId || appSecret) return { status: "incomplete", appId, appSecret, ...user };
|
|
94
|
+
return { status: "missing", ...user };
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
export function writeFeishuCredentials(envPath: string, credentials: FeishuCredentials): void {
|
|
94
98
|
assertDotEnvValue("FEISHU_APP_ID", credentials.appId);
|
|
95
99
|
assertDotEnvValue("FEISHU_APP_SECRET", credentials.appSecret);
|
|
100
|
+
if (credentials.userOpenId) assertDotEnvValue("FEISHU_USER_OPEN_ID", credentials.userOpenId);
|
|
96
101
|
const original = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
|
|
97
102
|
let updated = upsertDotEnvValue(original, "FEISHU_APP_ID", credentials.appId);
|
|
98
103
|
updated = upsertDotEnvValue(updated, "FEISHU_APP_SECRET", credentials.appSecret);
|
|
104
|
+
updated = upsertDotEnvValue(updated, "FEISHU_USER_OPEN_ID", credentials.userOpenId ?? "");
|
|
99
105
|
|
|
100
106
|
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
|
101
107
|
const temporaryPath = `${envPath}.${process.pid}.${Date.now()}.tmp`;
|
|
@@ -114,8 +120,9 @@ export function writeFeishuCredentials(envPath: string, credentials: FeishuCrede
|
|
|
114
120
|
persisted.status !== "configured"
|
|
115
121
|
|| persisted.appId !== credentials.appId
|
|
116
122
|
|| persisted.appSecret !== credentials.appSecret
|
|
123
|
+
|| persisted.userOpenId !== credentials.userOpenId
|
|
117
124
|
) {
|
|
118
|
-
throw new Error("
|
|
125
|
+
throw new Error("Lark credential verification failed after writing the environment file.");
|
|
119
126
|
}
|
|
120
127
|
} finally {
|
|
121
128
|
if (temporaryFile !== undefined) fs.closeSync(temporaryFile);
|
|
@@ -162,13 +169,13 @@ export function acquireInitializationLock(lockDirectory: string): Initialization
|
|
|
162
169
|
if (created) fs.rmSync(lockPath, { force: true });
|
|
163
170
|
if (!isFileExistsError(error)) throw error;
|
|
164
171
|
if (!isStaleInitializationLock(lockPath)) {
|
|
165
|
-
throw new Error(
|
|
172
|
+
throw new Error(`Another agent-bot init process is running (lock file: ${lockPath}).`);
|
|
166
173
|
}
|
|
167
174
|
fs.rmSync(lockPath, { force: true });
|
|
168
175
|
}
|
|
169
176
|
}
|
|
170
177
|
|
|
171
|
-
throw new Error(
|
|
178
|
+
throw new Error(`Could not acquire the initialization lock: ${lockPath}`);
|
|
172
179
|
}
|
|
173
180
|
|
|
174
181
|
export function cleanupFeishuCredentialTemporaryFiles(envPath: string): number {
|
|
@@ -227,7 +234,7 @@ function upsertDotEnvValue(contents: string, key: string, value: string): string
|
|
|
227
234
|
|
|
228
235
|
function assertDotEnvValue(key: string, value: string): void {
|
|
229
236
|
if (!value.trim() || /[\r\n]/.test(value)) {
|
|
230
|
-
throw new Error(`${key}
|
|
237
|
+
throw new Error(`${key} is not a valid single-line environment variable value.`);
|
|
231
238
|
}
|
|
232
239
|
}
|
|
233
240
|
|
|
@@ -17,7 +17,7 @@ export async function sendControlRequest(
|
|
|
17
17
|
socket.destroy();
|
|
18
18
|
operation();
|
|
19
19
|
};
|
|
20
|
-
const timer = setTimeout(() => finish(() => reject(new Error("
|
|
20
|
+
const timer = setTimeout(() => finish(() => reject(new Error("Timed out connecting to the Agent Bot control endpoint."))), timeoutMs);
|
|
21
21
|
socket.setEncoding("utf8");
|
|
22
22
|
socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
|
|
23
23
|
socket.on("data", (chunk: string) => {
|
|
@@ -33,7 +33,7 @@ export async function sendControlRequest(
|
|
|
33
33
|
});
|
|
34
34
|
socket.once("error", (error) => finish(() => reject(error)));
|
|
35
35
|
socket.once("end", () => {
|
|
36
|
-
if (!settled) finish(() => reject(new Error("Agent Bot
|
|
36
|
+
if (!settled) finish(() => reject(new Error("The Agent Bot control endpoint returned no response.")));
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
39
|
}
|
|
@@ -61,7 +61,7 @@ export class LocalControlServer {
|
|
|
61
61
|
function parseRequest(line: string): ControlRequest {
|
|
62
62
|
const value = JSON.parse(line) as unknown;
|
|
63
63
|
if (!value || typeof value !== "object" || !("action" in value) || typeof value.action !== "string") {
|
|
64
|
-
throw new Error("
|
|
64
|
+
throw new Error("Invalid local control request.");
|
|
65
65
|
}
|
|
66
66
|
return value as ControlRequest;
|
|
67
67
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { loadConfig } from "../config/loadConfig.js";
|
|
4
|
+
import type { AppConfig } from "../config/schema.js";
|
|
5
|
+
import { requireServerFeishuTransport } from "../feishu/transport.js";
|
|
6
|
+
import {
|
|
7
|
+
nodeDiagnosticReportArguments,
|
|
8
|
+
prepareCrashReportDirectory,
|
|
9
|
+
resolveSupervisorDiagnosticsPaths,
|
|
10
|
+
} from "../supervision/SupervisorDiagnostics.js";
|
|
11
|
+
import { controlEndpoint } from "./controlProtocol.js";
|
|
12
|
+
import { isServerReachable, isServerRunning } from "./LocalControlClient.js";
|
|
13
|
+
|
|
14
|
+
export interface ServerStartResult {
|
|
15
|
+
status: "started" | "already-running";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type InitializationServerResult =
|
|
19
|
+
| ServerStartResult
|
|
20
|
+
| { status: "skipped"; reason: "feishu-skipped" };
|
|
21
|
+
|
|
22
|
+
export interface ServerStarterDependencies {
|
|
23
|
+
isRunning(endpoint: string): Promise<boolean>;
|
|
24
|
+
isReachable(endpoint: string): Promise<boolean>;
|
|
25
|
+
waitUntilRunning(endpoint: string, timeoutMs: number): Promise<boolean>;
|
|
26
|
+
spawnSupervisor(config: AppConfig): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SERVER_START_TIMEOUT_MS = 45_000;
|
|
30
|
+
|
|
31
|
+
interface InitializationServerDependencies {
|
|
32
|
+
loadConfig(configPath?: string): AppConfig;
|
|
33
|
+
startServer(config: AppConfig): Promise<ServerStartResult>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function startInitializedServer(
|
|
37
|
+
options: { skipFeishu: boolean; configPath?: string },
|
|
38
|
+
dependencies: InitializationServerDependencies = defaultInitializationDependencies,
|
|
39
|
+
): Promise<InitializationServerResult> {
|
|
40
|
+
if (options.skipFeishu) {
|
|
41
|
+
return { status: "skipped", reason: "feishu-skipped" };
|
|
42
|
+
}
|
|
43
|
+
return dependencies.startServer(dependencies.loadConfig(options.configPath));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function startServer(
|
|
47
|
+
config: AppConfig,
|
|
48
|
+
dependencies: ServerStarterDependencies = defaultDependencies,
|
|
49
|
+
): Promise<ServerStartResult> {
|
|
50
|
+
requireServerFeishuTransport(config.feishu);
|
|
51
|
+
const endpoint = controlEndpoint(config.storage.sqlitePath);
|
|
52
|
+
|
|
53
|
+
if (await dependencies.isRunning(endpoint)) {
|
|
54
|
+
return { status: "already-running" };
|
|
55
|
+
}
|
|
56
|
+
if (await dependencies.isReachable(endpoint)) {
|
|
57
|
+
const running = await dependencies.waitUntilRunning(endpoint, SERVER_START_TIMEOUT_MS);
|
|
58
|
+
if (!running) {
|
|
59
|
+
throw new Error("The Agent Bot server started but could not connect to Lark. Check the logs.");
|
|
60
|
+
}
|
|
61
|
+
return { status: "started" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
dependencies.spawnSupervisor(config);
|
|
65
|
+
const running = await dependencies.waitUntilRunning(endpoint, SERVER_START_TIMEOUT_MS);
|
|
66
|
+
if (!running) {
|
|
67
|
+
throw new Error("The Supervisor started, but the server did not connect to Lark within 45 seconds. Check the logs.");
|
|
68
|
+
}
|
|
69
|
+
return { status: "started" };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const defaultDependencies: ServerStarterDependencies = {
|
|
73
|
+
isRunning: isServerRunning,
|
|
74
|
+
isReachable: isServerReachable,
|
|
75
|
+
waitUntilRunning: waitForServer,
|
|
76
|
+
spawnSupervisor: (config) => {
|
|
77
|
+
const entry = fileURLToPath(new URL("../supervisor.js", import.meta.url));
|
|
78
|
+
const reportDirectory = resolveSupervisorDiagnosticsPaths(config).crashReportDirectory;
|
|
79
|
+
prepareCrashReportDirectory(reportDirectory);
|
|
80
|
+
const child = spawn(process.execPath, [
|
|
81
|
+
...nodeDiagnosticReportArguments(reportDirectory),
|
|
82
|
+
entry,
|
|
83
|
+
], {
|
|
84
|
+
cwd: process.cwd(),
|
|
85
|
+
detached: true,
|
|
86
|
+
windowsHide: true,
|
|
87
|
+
stdio: "ignore",
|
|
88
|
+
env: { ...process.env, AGENT_BOT_RESTART_REASON: "通过 Agent Bot CLI 启动" },
|
|
89
|
+
});
|
|
90
|
+
child.unref();
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const defaultInitializationDependencies: InitializationServerDependencies = {
|
|
95
|
+
loadConfig,
|
|
96
|
+
startServer: (config) => startServer(config),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
async function waitForServer(endpoint: string, timeoutMs: number): Promise<boolean> {
|
|
100
|
+
const deadline = Date.now() + timeoutMs;
|
|
101
|
+
while (Date.now() < deadline) {
|
|
102
|
+
if (await isServerRunning(endpoint)) return true;
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
package/src/cli/SkillRegistry.ts
CHANGED
|
@@ -65,7 +65,7 @@ export class SkillRegistry {
|
|
|
65
65
|
this.assertValidSource();
|
|
66
66
|
const existing = fs.existsSync(this.targetPath);
|
|
67
67
|
if (existing && this.readMarker()?.managedBy !== "agent-bot") {
|
|
68
|
-
throw new Error(
|
|
68
|
+
throw new Error(`The target directory exists and is not managed by Agent Bot: ${this.targetPath}`);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
const before = existing ? this.status() : undefined;
|
|
@@ -93,7 +93,7 @@ export class SkillRegistry {
|
|
|
93
93
|
uninstall(): boolean {
|
|
94
94
|
if (!fs.existsSync(this.targetPath)) return false;
|
|
95
95
|
if (this.readMarker()?.managedBy !== "agent-bot") {
|
|
96
|
-
throw new Error(
|
|
96
|
+
throw new Error(`Refusing to remove a directory that is not managed by Agent Bot: ${this.targetPath}`);
|
|
97
97
|
}
|
|
98
98
|
this.removeManagedTarget();
|
|
99
99
|
return true;
|
|
@@ -101,13 +101,13 @@ export class SkillRegistry {
|
|
|
101
101
|
|
|
102
102
|
private assertValidSource(): void {
|
|
103
103
|
if (!fs.existsSync(path.join(this.sourcePath, "SKILL.md"))) {
|
|
104
|
-
throw new Error(
|
|
104
|
+
throw new Error(`The built-in Agent Bot Skill was not found: ${this.sourcePath}`);
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
private assertSafeTarget(): void {
|
|
109
109
|
if (path.dirname(this.targetPath) !== this.skillsRoot || path.basename(this.targetPath) !== SKILL_NAME) {
|
|
110
|
-
throw new Error(
|
|
110
|
+
throw new Error(`Unsafe Skill target path: ${this.targetPath}`);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|