@keyou007/agent-bot 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +14 -1
- package/README.zh.md +14 -1
- package/dist/cli/FeishuAppConfiguration.js +10 -10
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/FeishuAppRegistration.js +9 -9
- package/dist/cli/FeishuAppRegistration.js.map +1 -1
- package/dist/cli/Initializer.js +64 -5
- 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 +3 -3
- package/dist/cli/ServerStarter.js.map +1 -1
- package/dist/cli/SkillRegistry.js +4 -4
- package/dist/cli/SkillRegistry.js.map +1 -1
- package/dist/cli/help.js +9 -57
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/initOptions.js +24 -0
- package/dist/cli/initOptions.js.map +1 -0
- package/dist/cli/packageVersion.js +1 -1
- package/dist/cli/packageVersion.js.map +1 -1
- package/dist/cli/profile.js +3 -3
- package/dist/cli/profile.js.map +1 -1
- 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 +154 -136
- package/dist/cli.js.map +1 -1
- package/dist/feishu/CardRenderer.js +1 -1
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/transport.js +1 -1
- package/dist/feishu/transport.js.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +10 -10
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/startup/StartupNotifier.js +1 -0
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/docs/technical-reference.md +5 -1
- package/docs/technical-reference.zh.md +5 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +3 -1
- package/src/cli/FeishuAppConfiguration.ts +10 -10
- package/src/cli/FeishuAppRegistration.ts +9 -9
- package/src/cli/Initializer.ts +76 -6
- package/src/cli/LocalControlClient.ts +2 -2
- package/src/cli/LocalControlServer.ts +1 -1
- package/src/cli/ServerStarter.ts +3 -3
- package/src/cli/SkillRegistry.ts +4 -4
- package/src/cli/help.ts +9 -66
- package/src/cli/initOptions.ts +33 -0
- package/src/cli/packageVersion.ts +1 -1
- package/src/cli/profile.ts +3 -3
- package/src/cli/serverStatus.ts +39 -0
- package/src/cli/taskChatRoute.ts +2 -2
- package/src/cli.ts +158 -139
- package/src/feishu/CardRenderer.ts +2 -0
- package/src/feishu/transport.ts +1 -1
- package/src/index.ts +8 -4
- package/src/proxy/ProxySessionController.ts +10 -10
- package/src/startup/StartupNotifier.ts +2 -0
|
@@ -35,6 +35,8 @@ The default user-data root is `~/.agent-bot`. `AGENT_BOT_HOME` replaces that roo
|
|
|
35
35
|
|
|
36
36
|
The CLI also supports explicit directory-based profiles. Without `--profile`, commands use the main profile and the normal environment-based path rules. `--profile <directory>` pins both `AGENT_BOT_HOME` and `AGENT_BOT_CONFIG` for the command and every spawned supervisor or worker, with the configuration fixed at `<directory>/config.yaml`. It also clears inherited Feishu credential variables before loading the selected profile's `.env`, which prevents a secondary service launched from inside the primary Agent Bot process tree from accidentally reusing the primary bot. It cannot be combined with `--config`. Alternative profiles must be selected explicitly on every command; Agent Bot does not maintain a named-profile registry.
|
|
37
37
|
|
|
38
|
+
All CLI interface text is English regardless of system locale. System-generated restart reasons remain Chinese because they are rendered in Chinese Lark status cards; an explicit `--reason` is preserved verbatim. `agent-bot server status` reports the running worker's Lark App ID as `feishuAppId` in JSON; when the server is stopped or predates that health field, the CLI falls back to the selected profile's configured App ID.
|
|
39
|
+
|
|
38
40
|
| Default path | Contents |
|
|
39
41
|
| ------------------------------------ | ---------------------------------- |
|
|
40
42
|
| `~/.agent-bot/config.yaml` | Main YAML configuration |
|
|
@@ -78,6 +80,8 @@ Missing app configuration is handled in two stages:
|
|
|
78
80
|
|
|
79
81
|
The generated authorization links do not contain the app secret.
|
|
80
82
|
|
|
83
|
+
`agent-bot --profile <directory> init --reset` fully resets an explicitly selected Profile. The Profile server must be stopped first. The command moves the active `config.yaml`, `.env`, `data/`, and `logs/` into a unique timestamped directory under `<profile>/.reset-backups/`, creates clean replacements from the packaged templates, and proceeds through normal initialization. Existing reset backups and unrelated files are retained. Because backups can contain old app secrets and conversation data, protect the Profile directory accordingly. The remote Feishu app is not deleted. `--reset --skip-feishu` creates a clean Console-only Profile, while `--reset` and `--reconfigure-feishu` cannot be combined.
|
|
84
|
+
|
|
81
85
|
After Feishu initialization succeeds, the CLI releases the initialization lock and starts the detached supervisor through the same readiness path as `agent-bot server start`. It waits up to 45 seconds for the worker to connect to Feishu and become ready. If the selected profile is already running, no second supervisor is created. `--skip-feishu` skips automatic server startup, and `--json` includes the resulting `server.status` without adding non-JSON output.
|
|
82
86
|
|
|
83
87
|
## Feishu App Requirements
|
|
@@ -138,7 +142,7 @@ logging:
|
|
|
138
142
|
path: "./logs/agent-bot.log"
|
|
139
143
|
```
|
|
140
144
|
|
|
141
|
-
`agent-bot server start` requires both Feishu credentials. The worker starts the SDK's persistent connection without inspecting SDK logs or private connection state, then sends startup status cards as an outbound readiness check. It normally targets known private chats and recently active groups. If the database has no known chat yet, it sends the card to `feishu.userOpenId` using an `open_id` private message. When notification targets exist, at least one card must be delivered before the server reports ready; individual target failures remain isolated. If neither a known chat nor `feishu.userOpenId` is available, startup continues without the outbound check. Missing credentials still fail startup with an initialization hint. `agent-bot console` is the explicit local-only path and does not require Feishu credentials.
|
|
145
|
+
`agent-bot server start` requires both Feishu credentials. The worker starts the SDK's persistent connection without inspecting SDK logs or private connection state, then sends startup status cards as an outbound readiness check. Each startup card includes the Agent Bot version read from the installed package metadata. It normally targets known private chats and recently active groups. If the database has no known chat yet, it sends the card to `feishu.userOpenId` using an `open_id` private message. When notification targets exist, at least one card must be delivered before the server reports ready; individual target failures remain isolated. If neither a known chat nor `feishu.userOpenId` is available, startup continues without the outbound check. Missing credentials still fail startup with an initialization hint. `agent-bot console` is the explicit local-only path and does not require Feishu credentials.
|
|
142
146
|
|
|
143
147
|
At least one agent must be configured. `defaults.agent` must name a configured agent.
|
|
144
148
|
|
|
@@ -35,6 +35,8 @@ Agent Bot 是基于 Node.js 22+、ESM 和 TypeScript 的应用,主要组件如
|
|
|
35
35
|
|
|
36
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
37
|
|
|
38
|
+
CLI 界面文本不受系统语言影响,统一使用英文。系统生成的重启原因仍使用中文,因为它会显示在中文 Lark 状态卡中;显式传入的 `--reason` 保持原样。`agent-bot server status` 会报告运行中 Worker 的 Lark App ID,JSON 字段名为 `feishuAppId`;服务停止或旧版健康协议尚未提供该字段时,CLI 会回退到当前 Profile 配置的 App ID。
|
|
39
|
+
|
|
38
40
|
| 默认路径 | 内容 |
|
|
39
41
|
| ------------------------------------ | -------------------- |
|
|
40
42
|
| `~/.agent-bot/config.yaml` | YAML 主配置 |
|
|
@@ -78,6 +80,8 @@ CLI 还支持显式指定目录的 Profile。不使用 `--profile` 时,命令
|
|
|
78
80
|
|
|
79
81
|
生成的授权链接不包含 App Secret。
|
|
80
82
|
|
|
83
|
+
`agent-bot --profile <目录> init --reset` 会完整重置显式指定的 Profile。执行前必须先停止该 Profile 的 Server。命令会把当前 `config.yaml`、`.env`、`data/` 和 `logs/` 移入 `<profile>/.reset-backups/` 下唯一的时间戳目录,从随包模板创建干净的新文件和目录,再继续正常初始化。已有重置备份及其他无关文件会保留。备份中可能含有旧 App Secret 和会话数据,应妥善保护 Profile 目录。远端旧飞书应用不会被删除。`--reset --skip-feishu` 可创建干净的 Console-only Profile,`--reset` 不能与 `--reconfigure-feishu` 同时使用。
|
|
84
|
+
|
|
81
85
|
飞书初始化成功后,CLI 会先释放初始化锁,再通过与 `agent-bot server start` 相同的就绪检查流程启动后台 Supervisor,并等待最多 45 秒,直到 Worker 连接飞书并进入就绪状态。如果当前 Profile 的服务已经运行,不会创建第二个 Supervisor。`--skip-feishu` 会跳过自动启动;`--json` 会把结果写入 `server.status`,不会混入非 JSON 文本。
|
|
82
86
|
|
|
83
87
|
## 飞书应用要求
|
|
@@ -138,7 +142,7 @@ logging:
|
|
|
138
142
|
path: "./logs/agent-bot.log"
|
|
139
143
|
```
|
|
140
144
|
|
|
141
|
-
`agent-bot server start` 要求同时配置飞书 `appId` 和 `appSecret`。Worker 启动 SDK 长连接时不读取 SDK
|
|
145
|
+
`agent-bot server start` 要求同时配置飞书 `appId` 和 `appSecret`。Worker 启动 SDK 长连接时不读取 SDK 日志或私有连接状态,随后通过发送启动状态卡片检查出站能力。每张启动卡片都会显示从已安装包元数据读取的 Agent Bot 版本。通知通常发往已知私聊和最近活跃的群聊;数据库里尚无已知会话时,改用 `feishu.userOpenId`,按 `open_id` 私聊发送。存在通知目标时,至少一张卡片发送成功后 Server 才报告就绪,单个目标发送失败仍相互隔离。如果既没有已知会话,也没有 `feishu.userOpenId`,启动会跳过出站检查并继续。缺少凭据时仍会启动失败并提示先初始化。`agent-bot console` 是明确的纯本地入口,不需要飞书凭据。
|
|
142
146
|
|
|
143
147
|
必须至少配置一个 Agent,`defaults.agent` 必须指向已配置的 Agent。
|
|
144
148
|
|
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.5",
|
|
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.5",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@agentclientprotocol/sdk": "1.2.1",
|
package/package.json
CHANGED
|
@@ -54,6 +54,8 @@ When running `init` on the user's behalf, relay every exact authorization or inc
|
|
|
54
54
|
|
|
55
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.
|
|
56
56
|
|
|
57
|
+
Use `agent-bot --profile <directory> init --reset` only when the user explicitly requests a full Profile reset. `--profile` is mandatory even for the primary Profile, and its server must be stopped first. The command moves the active `config.yaml`, `.env`, `data/`, and `logs/` into a new timestamped `.reset-backups` directory, retains all older backups and unrelated files, then initializes a clean Profile. Relay the reported backup path. Do not delete `.reset-backups`; it may contain old credentials and task data. `--reset --skip-feishu` is valid for a clean Console-only Profile, but `--reset` cannot be combined with `--reconfigure-feishu`.
|
|
58
|
+
|
|
57
59
|
For an isolated secondary bot, pass its profile directory explicitly on every command:
|
|
58
60
|
|
|
59
61
|
```powershell
|
|
@@ -74,7 +76,7 @@ agent-bot server status
|
|
|
74
76
|
agent-bot task list
|
|
75
77
|
```
|
|
76
78
|
|
|
77
|
-
`agent-bot
|
|
79
|
+
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`.
|
|
78
80
|
|
|
79
81
|
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.
|
|
80
82
|
|
|
@@ -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> {
|
|
@@ -41,12 +41,12 @@ export async function registerFeishuApp(options: FeishuAppRegistrationOptions =
|
|
|
41
41
|
},
|
|
42
42
|
options.signal,
|
|
43
43
|
);
|
|
44
|
-
throwRegistrationError(begin, "
|
|
44
|
+
throwRegistrationError(begin, "Could not start Lark app creation");
|
|
45
45
|
|
|
46
46
|
const deviceCode = readString(begin, "device_code");
|
|
47
47
|
const userCode = readString(begin, "user_code");
|
|
48
48
|
if (!deviceCode || !userCode) {
|
|
49
|
-
throw new Error("
|
|
49
|
+
throw new Error("The Lark app creation response is missing device_code or user_code.");
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const expiresIn =
|
|
@@ -113,16 +113,16 @@ export async function registerFeishuApp(options: FeishuAppRegistrationOptions =
|
|
|
113
113
|
intervalSeconds = Math.min(intervalSeconds + 5, MAX_POLL_INTERVAL_SECONDS);
|
|
114
114
|
continue;
|
|
115
115
|
case "access_denied":
|
|
116
|
-
throw new Error("
|
|
116
|
+
throw new Error("Lark app creation was cancelled.");
|
|
117
117
|
case "expired_token":
|
|
118
118
|
case "invalid_grant":
|
|
119
|
-
throw new Error("
|
|
119
|
+
throw new Error("The Lark app creation link expired. Run agent-bot init again.");
|
|
120
120
|
default:
|
|
121
|
-
throwRegistrationError(payload, "
|
|
121
|
+
throwRegistrationError(payload, "Lark app creation failed");
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
throw new Error("
|
|
125
|
+
throw new Error("Timed out waiting for Lark app creation. Run agent-bot init again.");
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
function buildVerificationUrl(userCode: string): string {
|
|
@@ -152,7 +152,7 @@ async function postRegistration(
|
|
|
152
152
|
});
|
|
153
153
|
const payload = (await response.json().catch(() => undefined)) as unknown;
|
|
154
154
|
if (!isRecord(payload)) {
|
|
155
|
-
throw new Error(
|
|
155
|
+
throw new Error(`The Lark app registration endpoint returned an invalid response (HTTP ${response.status}).`);
|
|
156
156
|
}
|
|
157
157
|
return payload;
|
|
158
158
|
} finally {
|
|
@@ -165,7 +165,7 @@ function throwRegistrationError(payload: RegistrationPayload, prefix: string): v
|
|
|
165
165
|
const code = readString(payload, "error");
|
|
166
166
|
if (!code) return;
|
|
167
167
|
const description = readString(payload, "error_description") || code;
|
|
168
|
-
throw new Error(`${prefix}
|
|
168
|
+
throw new Error(`${prefix}: ${description}`);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
function readString(value: RegistrationPayload, key: string): string {
|
|
@@ -189,7 +189,7 @@ function isRecord(value: unknown): value is RegistrationPayload {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
function abortError(signal: AbortSignal): Error {
|
|
192
|
-
return signal.reason instanceof Error ? signal.reason : new Error("
|
|
192
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Lark app creation was cancelled.");
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
function delay(milliseconds: number): Promise<void> {
|
package/src/cli/Initializer.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveUserPath,
|
|
12
12
|
} from "../config/paths.js";
|
|
13
13
|
|
|
14
|
-
export type InitializationStatus = "created" | "existing";
|
|
14
|
+
export type InitializationStatus = "created" | "existing" | "reset";
|
|
15
15
|
|
|
16
16
|
export interface InitializedPath {
|
|
17
17
|
path: string;
|
|
@@ -24,6 +24,9 @@ export interface InitializationResult {
|
|
|
24
24
|
env: InitializedPath;
|
|
25
25
|
data: InitializedPath;
|
|
26
26
|
logs: InitializedPath;
|
|
27
|
+
reset?: {
|
|
28
|
+
backupPath: string;
|
|
29
|
+
};
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export interface InitializationOptions {
|
|
@@ -31,6 +34,7 @@ export interface InitializationOptions {
|
|
|
31
34
|
configPath?: string;
|
|
32
35
|
configTemplatePath?: string;
|
|
33
36
|
envTemplatePath?: string;
|
|
37
|
+
reset?: boolean;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
export type FeishuCredentialStatus = "configured" | "missing" | "incomplete";
|
|
@@ -73,6 +77,9 @@ export function initializeAgentBot(options: InitializationOptions = {}): Initial
|
|
|
73
77
|
|
|
74
78
|
const home = ensureDirectory(homePath);
|
|
75
79
|
fs.mkdirSync(configDirectory, { recursive: true });
|
|
80
|
+
const reset = options.reset
|
|
81
|
+
? resetProfileContents(homePath, configPath, envPath, configDirectory)
|
|
82
|
+
: undefined;
|
|
76
83
|
|
|
77
84
|
const config = copyIfMissing(configTemplatePath, configPath);
|
|
78
85
|
const envFile = copyIfMissing(envTemplatePath, envPath);
|
|
@@ -80,7 +87,14 @@ export function initializeAgentBot(options: InitializationOptions = {}): Initial
|
|
|
80
87
|
const data = ensureDirectory(path.join(configDirectory, "data"));
|
|
81
88
|
const logs = ensureDirectory(path.join(configDirectory, "logs"));
|
|
82
89
|
|
|
83
|
-
return {
|
|
90
|
+
return {
|
|
91
|
+
home,
|
|
92
|
+
config: withResetStatus(config, reset?.movedNames.has("config.yaml") ?? false),
|
|
93
|
+
env: withResetStatus(envFile, reset?.movedNames.has(".env") ?? false),
|
|
94
|
+
data: withResetStatus(data, reset?.movedNames.has("data") ?? false),
|
|
95
|
+
logs: withResetStatus(logs, reset?.movedNames.has("logs") ?? false),
|
|
96
|
+
...(reset ? { reset: { backupPath: reset.backupPath } } : {}),
|
|
97
|
+
};
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
export function readFeishuCredentials(envPath: string, env: NodeJS.ProcessEnv = process.env): FeishuCredentialState {
|
|
@@ -122,7 +136,7 @@ export function writeFeishuCredentials(envPath: string, credentials: FeishuCrede
|
|
|
122
136
|
|| persisted.appSecret !== credentials.appSecret
|
|
123
137
|
|| persisted.userOpenId !== credentials.userOpenId
|
|
124
138
|
) {
|
|
125
|
-
throw new Error("
|
|
139
|
+
throw new Error("Lark credential verification failed after writing the environment file.");
|
|
126
140
|
}
|
|
127
141
|
} finally {
|
|
128
142
|
if (temporaryFile !== undefined) fs.closeSync(temporaryFile);
|
|
@@ -169,13 +183,13 @@ export function acquireInitializationLock(lockDirectory: string): Initialization
|
|
|
169
183
|
if (created) fs.rmSync(lockPath, { force: true });
|
|
170
184
|
if (!isFileExistsError(error)) throw error;
|
|
171
185
|
if (!isStaleInitializationLock(lockPath)) {
|
|
172
|
-
throw new Error(
|
|
186
|
+
throw new Error(`Another agent-bot init process is running (lock file: ${lockPath}).`);
|
|
173
187
|
}
|
|
174
188
|
fs.rmSync(lockPath, { force: true });
|
|
175
189
|
}
|
|
176
190
|
}
|
|
177
191
|
|
|
178
|
-
throw new Error(
|
|
192
|
+
throw new Error(`Could not acquire the initialization lock: ${lockPath}`);
|
|
179
193
|
}
|
|
180
194
|
|
|
181
195
|
export function cleanupFeishuCredentialTemporaryFiles(envPath: string): number {
|
|
@@ -217,6 +231,62 @@ function copyIfMissing(sourcePath: string, targetPath: string): InitializedPath
|
|
|
217
231
|
}
|
|
218
232
|
}
|
|
219
233
|
|
|
234
|
+
function resetProfileContents(
|
|
235
|
+
homePath: string,
|
|
236
|
+
configPath: string,
|
|
237
|
+
envPath: string,
|
|
238
|
+
configDirectory: string,
|
|
239
|
+
): { backupPath: string; movedNames: Set<string> } | undefined {
|
|
240
|
+
const resolvedHome = path.resolve(homePath);
|
|
241
|
+
if (
|
|
242
|
+
path.resolve(configDirectory) !== resolvedHome
|
|
243
|
+
|| path.dirname(path.resolve(configPath)) !== resolvedHome
|
|
244
|
+
|| path.dirname(path.resolve(envPath)) !== resolvedHome
|
|
245
|
+
) {
|
|
246
|
+
throw new Error("--reset only supports a profile-owned config.yaml and .env.");
|
|
247
|
+
}
|
|
248
|
+
const targets = [
|
|
249
|
+
{ name: "config.yaml", sourcePath: configPath },
|
|
250
|
+
{ name: ".env", sourcePath: envPath },
|
|
251
|
+
{ name: "data", sourcePath: path.join(configDirectory, "data") },
|
|
252
|
+
{ name: "logs", sourcePath: path.join(configDirectory, "logs") },
|
|
253
|
+
].filter((target) => fs.existsSync(target.sourcePath));
|
|
254
|
+
if (targets.length === 0) return undefined;
|
|
255
|
+
|
|
256
|
+
const timestamp = new Date().toISOString().replaceAll(":", "-").replaceAll(".", "-");
|
|
257
|
+
const backupPath = path.join(homePath, ".reset-backups", `${timestamp}-${randomUUID()}`);
|
|
258
|
+
fs.mkdirSync(backupPath, { recursive: true, mode: 0o700 });
|
|
259
|
+
const moved: Array<{ sourcePath: string; backupPath: string }> = [];
|
|
260
|
+
try {
|
|
261
|
+
for (const target of targets) {
|
|
262
|
+
const targetBackupPath = path.join(backupPath, target.name);
|
|
263
|
+
fs.renameSync(target.sourcePath, targetBackupPath);
|
|
264
|
+
moved.push({ sourcePath: target.sourcePath, backupPath: targetBackupPath });
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
for (const entry of moved.reverse()) {
|
|
268
|
+
if (!fs.existsSync(entry.sourcePath) && fs.existsSync(entry.backupPath)) {
|
|
269
|
+
fs.renameSync(entry.backupPath, entry.sourcePath);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
fs.rmdirSync(backupPath);
|
|
274
|
+
fs.rmdirSync(path.dirname(backupPath));
|
|
275
|
+
} catch {
|
|
276
|
+
// Keep a non-empty backup directory when rollback itself cannot fully restore it.
|
|
277
|
+
}
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
backupPath,
|
|
282
|
+
movedNames: new Set(targets.map((target) => target.name)),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function withResetStatus(value: InitializedPath, reset: boolean): InitializedPath {
|
|
287
|
+
return reset ? { ...value, status: "reset" } : value;
|
|
288
|
+
}
|
|
289
|
+
|
|
220
290
|
function upsertDotEnvValue(contents: string, key: string, value: string): string {
|
|
221
291
|
const newline = contents.includes("\r\n") ? "\r\n" : "\n";
|
|
222
292
|
const lines = contents.length > 0 ? contents.split(/\r?\n/) : [];
|
|
@@ -234,7 +304,7 @@ function upsertDotEnvValue(contents: string, key: string, value: string): string
|
|
|
234
304
|
|
|
235
305
|
function assertDotEnvValue(key: string, value: string): void {
|
|
236
306
|
if (!value.trim() || /[\r\n]/.test(value)) {
|
|
237
|
-
throw new Error(`${key}
|
|
307
|
+
throw new Error(`${key} is not a valid single-line environment variable value.`);
|
|
238
308
|
}
|
|
239
309
|
}
|
|
240
310
|
|
|
@@ -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
|
}
|
package/src/cli/ServerStarter.ts
CHANGED
|
@@ -56,7 +56,7 @@ export async function startServer(
|
|
|
56
56
|
if (await dependencies.isReachable(endpoint)) {
|
|
57
57
|
const running = await dependencies.waitUntilRunning(endpoint, SERVER_START_TIMEOUT_MS);
|
|
58
58
|
if (!running) {
|
|
59
|
-
throw new Error("
|
|
59
|
+
throw new Error("The Agent Bot server started but could not connect to Lark. Check the logs.");
|
|
60
60
|
}
|
|
61
61
|
return { status: "started" };
|
|
62
62
|
}
|
|
@@ -64,7 +64,7 @@ export async function startServer(
|
|
|
64
64
|
dependencies.spawnSupervisor(config);
|
|
65
65
|
const running = await dependencies.waitUntilRunning(endpoint, SERVER_START_TIMEOUT_MS);
|
|
66
66
|
if (!running) {
|
|
67
|
-
throw new Error("
|
|
67
|
+
throw new Error("The Supervisor started, but the server did not connect to Lark within 45 seconds. Check the logs.");
|
|
68
68
|
}
|
|
69
69
|
return { status: "started" };
|
|
70
70
|
}
|
|
@@ -85,7 +85,7 @@ const defaultDependencies: ServerStarterDependencies = {
|
|
|
85
85
|
detached: true,
|
|
86
86
|
windowsHide: true,
|
|
87
87
|
stdio: "ignore",
|
|
88
|
-
env: { ...process.env, AGENT_BOT_RESTART_REASON: "通过
|
|
88
|
+
env: { ...process.env, AGENT_BOT_RESTART_REASON: "通过 Agent Bot CLI 启动" },
|
|
89
89
|
});
|
|
90
90
|
child.unref();
|
|
91
91
|
},
|
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
|
|
package/src/cli/help.ts
CHANGED
|
@@ -1,63 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
export function resolveCliLanguage(
|
|
4
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
5
|
-
systemLocale = Intl.DateTimeFormat().resolvedOptions().locale,
|
|
6
|
-
): CliLanguage {
|
|
7
|
-
const configuredLocale = [
|
|
8
|
-
env.LC_ALL,
|
|
9
|
-
env.LC_MESSAGES,
|
|
10
|
-
env.LANGUAGE,
|
|
11
|
-
env.LANG,
|
|
12
|
-
].find((value) => value?.trim());
|
|
13
|
-
return isChineseLocale(configuredLocale ?? systemLocale) ? "zh" : "en";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function renderCliHelp(version: string, language = resolveCliLanguage()): string {
|
|
17
|
-
return language === "zh" ? renderChineseHelp(version) : renderEnglishHelp(version);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function renderChineseHelp(version: string): string {
|
|
21
|
-
return `Agent Bot ${version}
|
|
22
|
-
通过飞书/Lark 使用本机 Codex 与 ACP Agent。
|
|
23
|
-
|
|
24
|
-
用法:
|
|
25
|
-
agent-bot [选项] <命令>
|
|
26
|
-
|
|
27
|
-
常用命令:
|
|
28
|
-
init 初始化配置和飞书应用,并启动服务
|
|
29
|
-
console 打开本地 Console
|
|
30
|
-
server start 在后台启动 Agent Bot
|
|
31
|
-
server status 查看服务和安全重启状态
|
|
32
|
-
server restart 安排安全重启
|
|
33
|
-
server stop 停止 Agent Bot
|
|
34
|
-
task list 列出任务
|
|
35
|
-
task status <task> 查看任务状态
|
|
36
|
-
task chat <task> 查看任务绑定的飞书会话
|
|
37
|
-
task prompt <task> <prompt> 向指定任务发送 Prompt
|
|
38
|
-
task stop <task> 停止指定任务
|
|
39
|
-
task title <task> <title> 修改任务标题
|
|
40
|
-
skills status 查看内置 Skill 状态
|
|
41
|
-
skills install 安装内置 Skill
|
|
42
|
-
|
|
43
|
-
选项:
|
|
44
|
-
--profile <directory> 使用指定目录中的独立 Profile
|
|
45
|
-
--config <path> 使用指定配置文件
|
|
46
|
-
--json 在支持的命令中输出 JSON
|
|
47
|
-
-h, --help 显示帮助
|
|
48
|
-
-v, --version 显示版本
|
|
49
|
-
|
|
50
|
-
示例:
|
|
51
|
-
agent-bot init
|
|
52
|
-
agent-bot --profile ~/.agent-bot-rescue init
|
|
53
|
-
agent-bot server start
|
|
54
|
-
agent-bot server status
|
|
55
|
-
agent-bot task list
|
|
56
|
-
agent-bot server restart --reason "升级 Agent Bot"
|
|
57
|
-
`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function renderEnglishHelp(version: string): string {
|
|
1
|
+
export function renderCliHelp(version: string): string {
|
|
61
2
|
return `Agent Bot ${version}
|
|
62
3
|
Use local Codex and ACP agents from Lark/Feishu.
|
|
63
4
|
|
|
@@ -66,9 +7,10 @@ Usage:
|
|
|
66
7
|
|
|
67
8
|
Common commands:
|
|
68
9
|
init Initialize the Lark app and start the service
|
|
10
|
+
init --reset Reset and reconfigure an explicit profile
|
|
69
11
|
console Open the local console
|
|
70
12
|
server start Start Agent Bot in the background
|
|
71
|
-
server status Show server and safe-restart status
|
|
13
|
+
server status Show the server, Lark App ID, and safe-restart status
|
|
72
14
|
server restart Schedule a safe restart
|
|
73
15
|
server stop Stop Agent Bot
|
|
74
16
|
task list List tasks
|
|
@@ -87,8 +29,14 @@ Options:
|
|
|
87
29
|
-h, --help Show help
|
|
88
30
|
-v, --version Show version
|
|
89
31
|
|
|
32
|
+
Init options:
|
|
33
|
+
--reset Back up and reset the selected explicit profile
|
|
34
|
+
--skip-feishu Initialize for Console only
|
|
35
|
+
--reconfigure-feishu Replace only the existing Lark credentials
|
|
36
|
+
|
|
90
37
|
Examples:
|
|
91
38
|
agent-bot init
|
|
39
|
+
agent-bot --profile ~/.agent-bot init --reset
|
|
92
40
|
agent-bot --profile ~/.agent-bot-rescue init
|
|
93
41
|
agent-bot server start
|
|
94
42
|
agent-bot server status
|
|
@@ -96,8 +44,3 @@ Examples:
|
|
|
96
44
|
agent-bot server restart --reason "Update Agent Bot"
|
|
97
45
|
`;
|
|
98
46
|
}
|
|
99
|
-
|
|
100
|
-
function isChineseLocale(value: string): boolean {
|
|
101
|
-
const locale = value.trim().split(":")[0]?.split(/[.@]/)[0]?.replaceAll("_", "-").toLowerCase();
|
|
102
|
-
return locale === "zh" || Boolean(locale?.startsWith("zh-"));
|
|
103
|
-
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface InitCommandOptions {
|
|
2
|
+
json: boolean;
|
|
3
|
+
skipFeishu: boolean;
|
|
4
|
+
reconfigureFeishu: boolean;
|
|
5
|
+
reset: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function parseInitCommandOptions(
|
|
9
|
+
input: string[],
|
|
10
|
+
explicitProfile: boolean,
|
|
11
|
+
): InitCommandOptions {
|
|
12
|
+
const supported = new Set(["--json", "--skip-feishu", "--reconfigure-feishu", "--reset"]);
|
|
13
|
+
const unsupported = input.filter((value) => !supported.has(value));
|
|
14
|
+
if (unsupported.length > 0) {
|
|
15
|
+
throw new Error(`Unsupported init options: ${unsupported.join(" ")}`);
|
|
16
|
+
}
|
|
17
|
+
const options = {
|
|
18
|
+
json: input.includes("--json"),
|
|
19
|
+
skipFeishu: input.includes("--skip-feishu"),
|
|
20
|
+
reconfigureFeishu: input.includes("--reconfigure-feishu"),
|
|
21
|
+
reset: input.includes("--reset"),
|
|
22
|
+
};
|
|
23
|
+
if (options.skipFeishu && options.reconfigureFeishu) {
|
|
24
|
+
throw new Error("--skip-feishu and --reconfigure-feishu cannot be used together.");
|
|
25
|
+
}
|
|
26
|
+
if (options.reset && !explicitProfile) {
|
|
27
|
+
throw new Error("--reset requires an explicit --profile <directory>.");
|
|
28
|
+
}
|
|
29
|
+
if (options.reset && options.reconfigureFeishu) {
|
|
30
|
+
throw new Error("--reset cannot be combined with --reconfigure-feishu because reset already replaces Lark credentials.");
|
|
31
|
+
}
|
|
32
|
+
return options;
|
|
33
|
+
}
|
|
@@ -8,7 +8,7 @@ export function readPackageVersion(
|
|
|
8
8
|
fs.readFileSync(fileURLToPath(packageJsonUrl), "utf8"),
|
|
9
9
|
) as { version?: unknown };
|
|
10
10
|
if (typeof metadata.version !== "string" || !metadata.version.trim()) {
|
|
11
|
-
throw new Error("package.json
|
|
11
|
+
throw new Error("package.json does not contain a valid version.");
|
|
12
12
|
}
|
|
13
13
|
return metadata.version;
|
|
14
14
|
}
|
package/src/cli/profile.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function parseGlobalOptions(input: string[]): ParsedGlobalOptions {
|
|
|
22
22
|
const configPath = removeOption(args, "--config");
|
|
23
23
|
const profilePath = removeOption(args, "--profile");
|
|
24
24
|
if (configPath && profilePath) {
|
|
25
|
-
throw new Error("--profile
|
|
25
|
+
throw new Error("--profile and --config cannot be used together. A profile always uses config.yaml in its directory.");
|
|
26
26
|
}
|
|
27
27
|
return {
|
|
28
28
|
args,
|
|
@@ -49,11 +49,11 @@ export function applyExplicitProfile(
|
|
|
49
49
|
|
|
50
50
|
function removeOption(args: string[], option: string): string | undefined {
|
|
51
51
|
const indexes = args.flatMap((value, index) => value === option ? [index] : []);
|
|
52
|
-
if (indexes.length > 1) throw new Error(`${option}
|
|
52
|
+
if (indexes.length > 1) throw new Error(`${option} can only be specified once.`);
|
|
53
53
|
const index = indexes[0];
|
|
54
54
|
if (index === undefined) return undefined;
|
|
55
55
|
const value = args[index + 1];
|
|
56
|
-
if (!value || value.startsWith("--")) throw new Error(`${option}
|
|
56
|
+
if (!value || value.startsWith("--")) throw new Error(`${option} requires a directory or file path.`);
|
|
57
57
|
args.splice(index, 2);
|
|
58
58
|
return value;
|
|
59
59
|
}
|