@keyou007/agent-bot 0.1.6 → 0.1.8
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 +10 -0
- package/README.md +5 -1
- package/README.zh.md +5 -1
- package/config.example.yaml +2 -0
- package/dist/cli/FeishuAppConfiguration.js +71 -21
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/OptionalAuthorizationSkip.js +30 -0
- package/dist/cli/OptionalAuthorizationSkip.js.map +1 -0
- package/dist/cli/VerificationOutput.js +9 -0
- package/dist/cli/VerificationOutput.js.map +1 -0
- package/dist/cli.js +23 -18
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexEventMapper.js +13 -0
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexRuntime.js +33 -4
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/config/loadConfig.js +1 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +1 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +2 -2
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuBotIdentity.js +35 -0
- package/dist/feishu/FeishuBotIdentity.js.map +1 -0
- package/dist/feishu/FeishuConnector.js +20 -5
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +2 -1
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +5 -1
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/utils/generatedImageMarkdown.js +11 -0
- package/dist/utils/generatedImageMarkdown.js.map +1 -0
- package/docs/technical-reference.md +8 -2
- package/docs/technical-reference.zh.md +8 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +7 -3
- package/src/cli/FeishuAppConfiguration.ts +131 -19
- package/src/cli/OptionalAuthorizationSkip.ts +38 -0
- package/src/cli/VerificationOutput.ts +22 -0
- package/src/cli.ts +30 -18
- package/src/codex/CodexEventMapper.ts +13 -0
- package/src/codex/CodexRuntime.ts +48 -5
- package/src/config/loadConfig.ts +1 -0
- package/src/config/schema.ts +1 -0
- package/src/feishu/CardRenderer.ts +2 -2
- package/src/feishu/FeishuBotIdentity.ts +55 -0
- package/src/feishu/FeishuConnector.ts +24 -4
- package/src/presentation/TurnStateReducer.ts +6 -1
- package/src/proxy/ProxySessionController.ts +4 -1
- package/src/utils/generatedImageMarkdown.ts +10 -0
|
@@ -76,7 +76,9 @@ When complete credentials are absent, initialization uses Feishu one-click regis
|
|
|
76
76
|
Missing app configuration is handled in two stages:
|
|
77
77
|
|
|
78
78
|
1. Core configuration is requested and polled until it becomes available.
|
|
79
|
-
2. Remaining optional configuration is requested
|
|
79
|
+
2. Remaining optional configuration is requested. The CLI prints its QR code followed by the authorization URL, then immediately polls for up to five minutes. An interactive terminal offers only `Y` to skip optional authorization and continue; otherwise the user completes authorization in the browser while polling continues.
|
|
80
|
+
|
|
81
|
+
Optional polling failures and timeouts return a partial configuration result instead of failing initialization. When stdin is not an interactive terminal, no skip input is available and polling continues until configuration becomes active or times out. Verification URLs and prompts use stderr, so `--json` keeps its final stdout machine-readable.
|
|
80
82
|
|
|
81
83
|
The generated authorization links do not contain the app secret.
|
|
82
84
|
|
|
@@ -91,7 +93,8 @@ Core configuration required for basic messaging:
|
|
|
91
93
|
- Bot capability
|
|
92
94
|
- Persistent-connection event delivery
|
|
93
95
|
- `im.message.receive_v1`
|
|
94
|
-
-
|
|
96
|
+
- `im:message.group_msg:readonly` for all user messages in groups containing the bot
|
|
97
|
+
- Tenant permission for private messages
|
|
95
98
|
- `im:message:send_as_bot` or a broader equivalent
|
|
96
99
|
- `application:application:self_manage` so initialization can inspect the published version
|
|
97
100
|
|
|
@@ -118,6 +121,7 @@ feishu:
|
|
|
118
121
|
appId: "${FEISHU_APP_ID}"
|
|
119
122
|
appSecret: "${FEISHU_APP_SECRET}"
|
|
120
123
|
userOpenId: "${FEISHU_USER_OPEN_ID}"
|
|
124
|
+
respondToAllGroupMessages: true
|
|
121
125
|
|
|
122
126
|
console:
|
|
123
127
|
enabled: true
|
|
@@ -142,6 +146,8 @@ logging:
|
|
|
142
146
|
path: "./logs/agent-bot.log"
|
|
143
147
|
```
|
|
144
148
|
|
|
149
|
+
`feishu.respondToAllGroupMessages` defaults to `true`. Set it to `false` to ignore 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. Private messages are always accepted. Initialization requests all-user group-message delivery regardless of this runtime option, allowing it to be changed later without another authorization.
|
|
150
|
+
|
|
145
151
|
`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.
|
|
146
152
|
|
|
147
153
|
At least one agent must be configured. `defaults.agent` must name a configured agent.
|
|
@@ -76,7 +76,9 @@ CLI 界面文本不受系统语言影响,统一使用英文。系统生成的
|
|
|
76
76
|
缺失配置分两阶段处理:
|
|
77
77
|
|
|
78
78
|
1. 请求核心配置并轮询,直到基础能力生效
|
|
79
|
-
2.
|
|
79
|
+
2. 请求其余可选配置。CLI 会先显示二维码,随后显示授权链接,然后立即开始最多 5 分钟的轮询。交互式终端只提供 `Y` 选项,用于跳过可选授权并继续;否则用户直接在浏览器中完成授权,轮询会持续进行
|
|
80
|
+
|
|
81
|
+
可选配置轮询失败或超时会返回部分配置结果,不会导致初始化失败。stdin 不是交互式终端时无法输入跳过指令,轮询会持续到配置生效或超时。验证链接和提示写入 stderr,因此 `--json` 的最终 stdout 仍可直接供程序读取。
|
|
80
82
|
|
|
81
83
|
生成的授权链接不包含 App Secret。
|
|
82
84
|
|
|
@@ -91,7 +93,8 @@ CLI 界面文本不受系统语言影响,统一使用英文。系统生成的
|
|
|
91
93
|
- 机器人能力
|
|
92
94
|
- 长连接事件接收
|
|
93
95
|
- `im.message.receive_v1`
|
|
94
|
-
-
|
|
96
|
+
- `im:message.group_msg:readonly`,用于接收机器人所在群内的全部用户消息
|
|
97
|
+
- 接收私聊消息所需的租户权限
|
|
95
98
|
- `im:message:send_as_bot` 或覆盖它的更大权限
|
|
96
99
|
- `application:application:self_manage`,供初始化检查已发布版本
|
|
97
100
|
|
|
@@ -118,6 +121,7 @@ feishu:
|
|
|
118
121
|
appId: "${FEISHU_APP_ID}"
|
|
119
122
|
appSecret: "${FEISHU_APP_SECRET}"
|
|
120
123
|
userOpenId: "${FEISHU_USER_OPEN_ID}"
|
|
124
|
+
respondToAllGroupMessages: true
|
|
121
125
|
|
|
122
126
|
console:
|
|
123
127
|
enabled: true
|
|
@@ -142,6 +146,8 @@ logging:
|
|
|
142
146
|
path: "./logs/agent-bot.log"
|
|
143
147
|
```
|
|
144
148
|
|
|
149
|
+
`feishu.respondToAllGroupMessages` 默认为 `true`。设为 `false` 后,未 @ 当前机器人的群消息会被忽略;Worker 启动时会解析机器人的 Open ID,因此 @ 其他成员不会误触发。私聊始终正常处理。无论该运行时配置如何,初始化都会申请接收全部群消息的权限,之后切换配置无需再次授权。
|
|
150
|
+
|
|
145
151
|
`agent-bot server start` 要求同时配置飞书 `appId` 和 `appSecret`。Worker 启动 SDK 长连接时不读取 SDK 日志或私有连接状态,随后通过发送启动状态卡片检查出站能力。每张启动卡片都会显示从已安装包元数据读取的 Agent Bot 版本。通知通常发往已知私聊和最近活跃的群聊;数据库里尚无已知会话时,改用 `feishu.userOpenId`,按 `open_id` 私聊发送。存在通知目标时,至少一张卡片发送成功后 Server 才报告就绪,单个目标发送失败仍相互隔离。如果既没有已知会话,也没有 `feishu.userOpenId`,启动会跳过出站检查并继续。缺少凭据时仍会启动失败并提示先初始化。`agent-bot console` 是明确的纯本地入口,不需要飞书凭据。
|
|
146
152
|
|
|
147
153
|
必须至少配置一个 Agent,`defaults.agent` 必须指向已配置的 Agent。
|
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.8",
|
|
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.8",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@agentclientprotocol/sdk": "1.2.1",
|
package/package.json
CHANGED
|
@@ -46,11 +46,15 @@ Run initialization once after installing or linking the CLI:
|
|
|
46
46
|
agent-bot init
|
|
47
47
|
```
|
|
48
48
|
|
|
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
|
|
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 a QR code followed by its authorization URL, 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. Core configuration is always polled until ready. For optional configuration, the CLI prints the QR code followed by the authorization URL and immediately waits up to five minutes for activation. The only terminal choice is `Y`, which skips optional authorization and continues initialization.
|
|
50
|
+
|
|
51
|
+
Core messaging configuration includes `im:message.group_msg:readonly`, not merely the narrower group-mention scope. Agent Bot groups accept ordinary messages without requiring users to @ the bot, so initialization must keep waiting until all-user group-message delivery is active.
|
|
52
|
+
|
|
53
|
+
Initialization always requests all-user group-message delivery. The runtime option `feishu.respondToAllGroupMessages` controls whether Agent Bot uses it: the default `true` responds to ordinary group messages, while `false` requires a mention of the current bot. Private messages are unaffected.
|
|
50
54
|
|
|
51
55
|
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
56
|
|
|
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.
|
|
57
|
+
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. Optional polling starts automatically after the link is shown. Do not enter `Y` unless the user explicitly asks to skip optional authorization; there is no separate accept action. Optional capabilities such as group creation, group-title synchronization, reactions, images, and card actions never block initialization when skipped, timed out, or still unavailable; relay the final affected-feature warnings. In a non-interactive terminal, optional polling continues until activation or timeout because skip input is unavailable. Neither URL contains the app secret, and the secret itself is never printed.
|
|
54
58
|
|
|
55
59
|
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
60
|
|
|
@@ -151,7 +155,7 @@ Keep action labels placed to the right of card content or at the bottom of a car
|
|
|
151
155
|
|
|
152
156
|
Messages whose trimmed text starts with `/` are always parsed as Agent Bot commands. Unknown slash commands are reported to the user with a `/help` hint and must never fall through to the model, including slash-prefixed messages that also contain images.
|
|
153
157
|
|
|
154
|
-
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.
|
|
158
|
+
When a group body has a current task, renaming the Feishu group to `[agent-name] [project directory] new title` also renames that current task if the Agent prefix matches the task's configured agent. Only `new title` is written to the task; the Agent and project-directory prefixes remain group metadata. The older `[agent-name] new title` shape remains supported. Malformed names, agent mismatches, empty groups, and thread-specific tasks are ignored.
|
|
155
159
|
|
|
156
160
|
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.
|
|
157
161
|
|
|
@@ -6,6 +6,7 @@ const TENANT_TOKEN_PATH = "/open-apis/auth/v3/tenant_access_token/internal";
|
|
|
6
6
|
const APPLICATION_PATH_PREFIX = "/open-apis/application/v6/applications";
|
|
7
7
|
const DEFAULT_POLL_INTERVAL_MS = 5_000;
|
|
8
8
|
const DEFAULT_COMPLETION_TIMEOUT_MS = 10 * 60_000;
|
|
9
|
+
const DEFAULT_OPTIONAL_COMPLETION_TIMEOUT_MS = 5 * 60_000;
|
|
9
10
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
10
11
|
|
|
11
12
|
interface ScopeRequirement {
|
|
@@ -27,11 +28,8 @@ const REQUIRED_SCOPE_REQUIREMENTS: ScopeRequirement[] = [
|
|
|
27
28
|
accepted: ["im:chat:read", "im:chat:readonly", "im:chat"],
|
|
28
29
|
},
|
|
29
30
|
{
|
|
30
|
-
requested: "im:message.
|
|
31
|
+
requested: "im:message.group_msg:readonly",
|
|
31
32
|
accepted: [
|
|
32
|
-
"im:message.group_at_msg:readonly",
|
|
33
|
-
"im:message.group_at_msg.include_bot:readonly",
|
|
34
|
-
"im:message.group_at_msg",
|
|
35
33
|
"im:message.group_msg:readonly",
|
|
36
34
|
"im:message.group_msg",
|
|
37
35
|
],
|
|
@@ -68,7 +66,7 @@ export const REQUIRED_FEISHU_CALLBACKS = ["card.action.trigger"] as const;
|
|
|
68
66
|
|
|
69
67
|
const CORE_FEISHU_SCOPES = new Set([
|
|
70
68
|
"application:application:self_manage",
|
|
71
|
-
"im:message.
|
|
69
|
+
"im:message.group_msg:readonly",
|
|
72
70
|
"im:message.p2p_msg:readonly",
|
|
73
71
|
"im:message:send_as_bot",
|
|
74
72
|
]);
|
|
@@ -96,8 +94,10 @@ export interface EnsureFeishuAppConfigurationOptions {
|
|
|
96
94
|
fetch?: typeof globalThis.fetch;
|
|
97
95
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
98
96
|
signal?: AbortSignal;
|
|
97
|
+
optionalSkipSignal?: AbortSignal;
|
|
99
98
|
pollIntervalMs?: number;
|
|
100
99
|
timeoutMs?: number;
|
|
100
|
+
optionalTimeoutMs?: number;
|
|
101
101
|
onVerification?: (challenge: FeishuConfigurationChallenge) => void | Promise<void>;
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -118,6 +118,8 @@ export async function ensureFeishuAppConfiguration(
|
|
|
118
118
|
const sleep = options.sleep ?? delay;
|
|
119
119
|
const pollIntervalMs = positiveInteger(options.pollIntervalMs) ?? DEFAULT_POLL_INTERVAL_MS;
|
|
120
120
|
const timeoutMs = positiveInteger(options.timeoutMs) ?? DEFAULT_COMPLETION_TIMEOUT_MS;
|
|
121
|
+
const optionalTimeoutMs = positiveInteger(options.optionalTimeoutMs)
|
|
122
|
+
?? DEFAULT_OPTIONAL_COMPLETION_TIMEOUT_MS;
|
|
121
123
|
|
|
122
124
|
let configuration: FeishuAppConfiguration;
|
|
123
125
|
try {
|
|
@@ -139,13 +141,16 @@ export async function ensureFeishuAppConfiguration(
|
|
|
139
141
|
|
|
140
142
|
const coreMissing = coreMissingFeishuAppConfiguration(missing);
|
|
141
143
|
if (!hasMissingConfiguration(coreMissing)) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
status: "partial",
|
|
144
|
+
return completeOptionalFeishuConfiguration(
|
|
145
|
+
credentials,
|
|
145
146
|
configuration,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
missing,
|
|
148
|
+
fetchImpl,
|
|
149
|
+
sleep,
|
|
150
|
+
pollIntervalMs,
|
|
151
|
+
optionalTimeoutMs,
|
|
152
|
+
options,
|
|
153
|
+
);
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
await requestMissingFeishuConfiguration(credentials.appId, coreMissing, true, options.onVerification);
|
|
@@ -161,15 +166,16 @@ export async function ensureFeishuAppConfiguration(
|
|
|
161
166
|
configuration = await readFeishuAppConfiguration(credentials, fetchImpl, options.signal);
|
|
162
167
|
const remaining = missingFeishuAppConfiguration(configuration);
|
|
163
168
|
if (!hasMissingConfiguration(coreMissingFeishuAppConfiguration(remaining))) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
status: hasMissingConfiguration(remaining) ? "partial" : "updated",
|
|
169
|
+
return completeOptionalFeishuConfiguration(
|
|
170
|
+
credentials,
|
|
169
171
|
configuration,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
missing,
|
|
173
|
+
fetchImpl,
|
|
174
|
+
sleep,
|
|
175
|
+
pollIntervalMs,
|
|
176
|
+
optionalTimeoutMs,
|
|
177
|
+
options,
|
|
178
|
+
);
|
|
173
179
|
}
|
|
174
180
|
lastError = undefined;
|
|
175
181
|
} catch (error) {
|
|
@@ -192,6 +198,112 @@ async function requestMissingFeishuConfiguration(
|
|
|
192
198
|
await onVerification?.({ verificationUrl, missing, blocking });
|
|
193
199
|
}
|
|
194
200
|
|
|
201
|
+
async function completeOptionalFeishuConfiguration(
|
|
202
|
+
credentials: FeishuAppCredentials,
|
|
203
|
+
initialConfiguration: FeishuAppConfiguration,
|
|
204
|
+
initialMissing: MissingFeishuAppConfiguration,
|
|
205
|
+
fetchImpl: typeof globalThis.fetch,
|
|
206
|
+
sleep: (milliseconds: number) => Promise<void>,
|
|
207
|
+
pollIntervalMs: number,
|
|
208
|
+
timeoutMs: number,
|
|
209
|
+
options: EnsureFeishuAppConfigurationOptions,
|
|
210
|
+
): Promise<EnsureFeishuAppConfigurationResult> {
|
|
211
|
+
let configuration = initialConfiguration;
|
|
212
|
+
let remaining = missingFeishuAppConfiguration(configuration);
|
|
213
|
+
if (hasMissingConfiguration(remaining)) {
|
|
214
|
+
await requestMissingFeishuConfiguration(
|
|
215
|
+
credentials.appId,
|
|
216
|
+
remaining,
|
|
217
|
+
false,
|
|
218
|
+
options.onVerification,
|
|
219
|
+
);
|
|
220
|
+
if (!options.optionalSkipSignal?.aborted) {
|
|
221
|
+
const completed = await waitForOptionalFeishuConfiguration(
|
|
222
|
+
credentials,
|
|
223
|
+
configuration,
|
|
224
|
+
fetchImpl,
|
|
225
|
+
sleep,
|
|
226
|
+
pollIntervalMs,
|
|
227
|
+
timeoutMs,
|
|
228
|
+
options.signal,
|
|
229
|
+
options.optionalSkipSignal,
|
|
230
|
+
);
|
|
231
|
+
configuration = completed.configuration;
|
|
232
|
+
remaining = completed.remaining;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
status: hasMissingConfiguration(remaining) ? "partial" : "updated",
|
|
237
|
+
configuration,
|
|
238
|
+
added: resolvedMissingConfiguration(initialMissing, remaining),
|
|
239
|
+
remaining,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function waitForOptionalFeishuConfiguration(
|
|
244
|
+
credentials: FeishuAppCredentials,
|
|
245
|
+
initialConfiguration: FeishuAppConfiguration,
|
|
246
|
+
fetchImpl: typeof globalThis.fetch,
|
|
247
|
+
sleep: (milliseconds: number) => Promise<void>,
|
|
248
|
+
pollIntervalMs: number,
|
|
249
|
+
timeoutMs: number,
|
|
250
|
+
signal?: AbortSignal,
|
|
251
|
+
optionalSkipSignal?: AbortSignal,
|
|
252
|
+
): Promise<{
|
|
253
|
+
configuration: FeishuAppConfiguration;
|
|
254
|
+
remaining: MissingFeishuAppConfiguration;
|
|
255
|
+
}> {
|
|
256
|
+
let configuration = initialConfiguration;
|
|
257
|
+
let remaining = missingFeishuAppConfiguration(configuration);
|
|
258
|
+
let remainingMs = timeoutMs;
|
|
259
|
+
while (remainingMs > 0 && hasMissingConfiguration(remaining)) {
|
|
260
|
+
if (optionalSkipSignal?.aborted) break;
|
|
261
|
+
const waitMs = Math.min(pollIntervalMs, remainingMs);
|
|
262
|
+
const waitResult = await sleepWithOptionalSkip(
|
|
263
|
+
sleep,
|
|
264
|
+
waitMs,
|
|
265
|
+
signal,
|
|
266
|
+
optionalSkipSignal,
|
|
267
|
+
);
|
|
268
|
+
if (waitResult === "skipped") break;
|
|
269
|
+
remainingMs -= waitMs;
|
|
270
|
+
try {
|
|
271
|
+
configuration = await readFeishuAppConfiguration(credentials, fetchImpl, signal);
|
|
272
|
+
remaining = missingFeishuAppConfiguration(configuration);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (signal?.aborted) throw abortError(signal);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return { configuration, remaining };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function sleepWithOptionalSkip(
|
|
281
|
+
sleep: (milliseconds: number) => Promise<void>,
|
|
282
|
+
milliseconds: number,
|
|
283
|
+
signal?: AbortSignal,
|
|
284
|
+
optionalSkipSignal?: AbortSignal,
|
|
285
|
+
): Promise<"elapsed" | "skipped"> {
|
|
286
|
+
if (!optionalSkipSignal) {
|
|
287
|
+
await sleepWithAbort(sleep, milliseconds, signal);
|
|
288
|
+
return "elapsed";
|
|
289
|
+
}
|
|
290
|
+
if (optionalSkipSignal.aborted) return "skipped";
|
|
291
|
+
|
|
292
|
+
let onSkip: (() => void) | undefined;
|
|
293
|
+
const skipped = new Promise<"skipped">((resolve) => {
|
|
294
|
+
onSkip = () => resolve("skipped");
|
|
295
|
+
optionalSkipSignal.addEventListener("abort", onSkip, { once: true });
|
|
296
|
+
});
|
|
297
|
+
try {
|
|
298
|
+
return await Promise.race([
|
|
299
|
+
sleepWithAbort(sleep, milliseconds, signal).then(() => "elapsed" as const),
|
|
300
|
+
skipped,
|
|
301
|
+
]);
|
|
302
|
+
} finally {
|
|
303
|
+
if (onSkip) optionalSkipSignal.removeEventListener("abort", onSkip);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
195
307
|
export function missingFeishuAppConfiguration(configuration: FeishuAppConfiguration): MissingFeishuAppConfiguration {
|
|
196
308
|
const scopes = new Set(configuration.scopes);
|
|
197
309
|
const events = new Set(configuration.events);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createInterface, type Interface } from "node:readline";
|
|
2
|
+
|
|
3
|
+
export interface OptionalAuthorizationSkipListener {
|
|
4
|
+
close(): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function listenForOptionalAuthorizationSkip(
|
|
8
|
+
onSkip: () => void,
|
|
9
|
+
): OptionalAuthorizationSkipListener {
|
|
10
|
+
if (!process.stdin.isTTY) return { close: () => undefined };
|
|
11
|
+
|
|
12
|
+
const readline = createInterface({
|
|
13
|
+
input: process.stdin,
|
|
14
|
+
output: process.stderr,
|
|
15
|
+
});
|
|
16
|
+
const onLine = (value: string): void => {
|
|
17
|
+
if (!isOptionalAuthorizationSkip(value)) {
|
|
18
|
+
process.stderr.write("Enter Y to skip optional authorization, or keep waiting.\n");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
process.stderr.write("Optional authorization skipped. Initialization will continue.\n");
|
|
22
|
+
onSkip();
|
|
23
|
+
};
|
|
24
|
+
readline.on("line", onLine);
|
|
25
|
+
process.stderr.write("Skip optional authorization? Enter Y and press Enter to skip.\n");
|
|
26
|
+
return {
|
|
27
|
+
close: () => closeReadline(readline, onLine),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isOptionalAuthorizationSkip(value: string): boolean {
|
|
32
|
+
return value.trim().toLowerCase() === "y";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function closeReadline(readline: Interface, onLine: (value: string) => void): void {
|
|
36
|
+
readline.removeListener("line", onLine);
|
|
37
|
+
readline.close();
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import qrcode from "qrcode-terminal";
|
|
2
|
+
|
|
3
|
+
export interface VerificationOutputOptions {
|
|
4
|
+
verificationUrl: string;
|
|
5
|
+
json: boolean;
|
|
6
|
+
qrInstruction: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function printVerificationQrAndLink(
|
|
10
|
+
options: VerificationOutputOptions,
|
|
11
|
+
write: (value: string) => void = (value) => process.stderr.write(value),
|
|
12
|
+
renderQr: (
|
|
13
|
+
value: string,
|
|
14
|
+
callback: (output: string) => void,
|
|
15
|
+
) => void = (value, callback) => qrcode.generate(value, { small: true }, callback),
|
|
16
|
+
): void {
|
|
17
|
+
if (!options.json) {
|
|
18
|
+
write(`\n${options.qrInstruction}\n\n`);
|
|
19
|
+
renderQr(options.verificationUrl, (output) => write(`${output}\n`));
|
|
20
|
+
}
|
|
21
|
+
write(`Authorization link:\n${options.verificationUrl}\n\n`);
|
|
22
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import qrcode from "qrcode-terminal";
|
|
6
5
|
import { loadConfig, loadConfigWithoutEnvironmentMutation } from "./config/loadConfig.js";
|
|
7
6
|
import { sendControlRequest, isServerReachable } from "./cli/LocalControlClient.js";
|
|
8
7
|
import {
|
|
@@ -21,6 +20,10 @@ import {
|
|
|
21
20
|
type InitializationStatus,
|
|
22
21
|
} from "./cli/Initializer.js";
|
|
23
22
|
import { parseInitCommandOptions, type InitCommandOptions } from "./cli/initOptions.js";
|
|
23
|
+
import {
|
|
24
|
+
listenForOptionalAuthorizationSkip,
|
|
25
|
+
type OptionalAuthorizationSkipListener,
|
|
26
|
+
} from "./cli/OptionalAuthorizationSkip.js";
|
|
24
27
|
import {
|
|
25
28
|
registerFeishuApp,
|
|
26
29
|
type FeishuAppCredentials,
|
|
@@ -36,6 +39,7 @@ import { formatServerStatus, withConfiguredFeishuAppId } from "./cli/serverStatu
|
|
|
36
39
|
import { resolveSystemSkillsRoot, SkillRegistry, type SkillRegistrationStatus } from "./cli/SkillRegistry.js";
|
|
37
40
|
import { readPackageVersion } from "./cli/packageVersion.js";
|
|
38
41
|
import { applyExplicitProfile, parseGlobalOptions } from "./cli/profile.js";
|
|
42
|
+
import { printVerificationQrAndLink } from "./cli/VerificationOutput.js";
|
|
39
43
|
import {
|
|
40
44
|
startInitializedServer,
|
|
41
45
|
startServer,
|
|
@@ -178,10 +182,18 @@ async function initializeFeishu(
|
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
if (!options.json) process.stdout.write("\nChecking Lark app permissions, events, and callbacks...\n");
|
|
185
|
+
const optionalSkip = new AbortController();
|
|
186
|
+
let skipListener: OptionalAuthorizationSkipListener | undefined;
|
|
181
187
|
const configuration = await ensureFeishuAppConfiguration(credentials, {
|
|
182
188
|
signal: controller.signal,
|
|
183
|
-
|
|
184
|
-
|
|
189
|
+
optionalSkipSignal: optionalSkip.signal,
|
|
190
|
+
onVerification: (challenge) => {
|
|
191
|
+
printFeishuConfigurationVerification(challenge, options.json);
|
|
192
|
+
if (!challenge.blocking) {
|
|
193
|
+
skipListener = listenForOptionalAuthorizationSkip(() => optionalSkip.abort());
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
}).finally(() => skipListener?.close());
|
|
185
197
|
return {
|
|
186
198
|
status,
|
|
187
199
|
appId: credentials.appId,
|
|
@@ -544,13 +556,13 @@ function printServerStartResult(result: ServerStartResult): void {
|
|
|
544
556
|
}
|
|
545
557
|
|
|
546
558
|
function printFeishuVerification(challenge: FeishuAppRegistrationChallenge, json: boolean): void {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
|
|
559
|
+
printVerificationQrAndLink(
|
|
560
|
+
{
|
|
561
|
+
verificationUrl: challenge.verificationUrl,
|
|
562
|
+
json,
|
|
563
|
+
qrInstruction: "Scan this QR code with Lark to create the bot app:",
|
|
564
|
+
},
|
|
565
|
+
);
|
|
554
566
|
process.stderr.write(`The link expires in about ${Math.ceil(challenge.expiresIn / 60)} minutes. Waiting for confirmation...\n`);
|
|
555
567
|
}
|
|
556
568
|
|
|
@@ -568,17 +580,17 @@ function printFeishuConfigurationVerification(
|
|
|
568
580
|
if (challenge.missing.callbacks.length > 0) {
|
|
569
581
|
process.stderr.write(`Callbacks: ${challenge.missing.callbacks.join(", ")}\n`);
|
|
570
582
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
|
|
583
|
+
printVerificationQrAndLink(
|
|
584
|
+
{
|
|
585
|
+
verificationUrl: challenge.verificationUrl,
|
|
586
|
+
json,
|
|
587
|
+
qrInstruction: "Scan this QR code with Lark to complete the configuration:",
|
|
588
|
+
},
|
|
589
|
+
);
|
|
578
590
|
if (challenge.blocking) {
|
|
579
591
|
process.stderr.write("Waiting for the core scopes and message event to become active...\n");
|
|
580
592
|
} else {
|
|
581
|
-
process.stderr.write("
|
|
593
|
+
process.stderr.write("Waiting up to 5 minutes for these optional items to become active...\n");
|
|
582
594
|
}
|
|
583
595
|
}
|
|
584
596
|
|
|
@@ -201,6 +201,19 @@ function mapTool(
|
|
|
201
201
|
completedAt,
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
|
+
if (type === "imageGeneration") {
|
|
205
|
+
const imagePath = nonEmptyString(item.savedPath);
|
|
206
|
+
return {
|
|
207
|
+
id,
|
|
208
|
+
title: "生成图片",
|
|
209
|
+
kind: "image_generation",
|
|
210
|
+
status,
|
|
211
|
+
command: nonEmptyString(item.revisedPrompt),
|
|
212
|
+
imagePath,
|
|
213
|
+
startedAt,
|
|
214
|
+
completedAt,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
204
217
|
return undefined;
|
|
205
218
|
}
|
|
206
219
|
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
RuntimeSession,
|
|
18
18
|
RuntimeSessionMetadata,
|
|
19
19
|
} from "../runtime/types.js";
|
|
20
|
+
import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
|
|
20
21
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
21
22
|
import { mapCodexNotification } from "./CodexEventMapper.js";
|
|
22
23
|
import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
|
|
@@ -59,6 +60,7 @@ interface CodexSession extends RuntimeSession {
|
|
|
59
60
|
activeTurnStartedAt?: number;
|
|
60
61
|
terminalTurnIds: Set<string>;
|
|
61
62
|
finalText: string;
|
|
63
|
+
generatedImagePaths: string[];
|
|
62
64
|
messagePhases: Map<string, "commentary" | "final_answer">;
|
|
63
65
|
needsResume: boolean;
|
|
64
66
|
}
|
|
@@ -548,6 +550,14 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
548
550
|
} else if (mapped.kind === "plan") {
|
|
549
551
|
this.emit({ type: "plan_updated", sessionId, turnId: mapped.turnId, steps: mapped.steps });
|
|
550
552
|
} else if (mapped.kind === "tool") {
|
|
553
|
+
if (
|
|
554
|
+
mapped.phase === "updated"
|
|
555
|
+
&& mapped.tool.kind === "image_generation"
|
|
556
|
+
&& mapped.tool.status === "completed"
|
|
557
|
+
&& mapped.tool.imagePath
|
|
558
|
+
) {
|
|
559
|
+
session.generatedImagePaths = uniqueStrings([...session.generatedImagePaths, mapped.tool.imagePath]);
|
|
560
|
+
}
|
|
551
561
|
this.emit({
|
|
552
562
|
type: mapped.phase === "started" ? "tool_started" : "tool_updated",
|
|
553
563
|
sessionId,
|
|
@@ -572,14 +582,16 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
572
582
|
} else if (mapped.status === "failed") {
|
|
573
583
|
this.emit({ type: "turn_failed", sessionId, turnId: mapped.turnId, message: mapped.error ?? "Codex turn failed." });
|
|
574
584
|
} else {
|
|
585
|
+
const finalResponse = appendGeneratedImageMarkdown(session.finalText, session.generatedImagePaths);
|
|
575
586
|
this.emit({
|
|
576
587
|
type: "turn_completed",
|
|
577
588
|
sessionId,
|
|
578
589
|
turnId: mapped.turnId,
|
|
579
|
-
finalResponse
|
|
590
|
+
finalResponse,
|
|
580
591
|
durationMs: mapped.durationMs,
|
|
581
592
|
});
|
|
582
593
|
}
|
|
594
|
+
session.generatedImagePaths = [];
|
|
583
595
|
}
|
|
584
596
|
}
|
|
585
597
|
|
|
@@ -693,6 +705,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
693
705
|
session.activeTurnId = turnId;
|
|
694
706
|
session.activeTurnStartedAt = startedAt;
|
|
695
707
|
session.finalText = "";
|
|
708
|
+
session.generatedImagePaths = [];
|
|
696
709
|
session.messagePhases.clear();
|
|
697
710
|
this.emit({ type: "turn_started", sessionId: session.localSessionId, turnId, startedAt });
|
|
698
711
|
}
|
|
@@ -726,8 +739,16 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
726
739
|
});
|
|
727
740
|
return;
|
|
728
741
|
}
|
|
729
|
-
const
|
|
742
|
+
const generatedImagePaths = uniqueStrings([
|
|
743
|
+
...session.generatedImagePaths,
|
|
744
|
+
...extractGeneratedImagePaths(turn),
|
|
745
|
+
]);
|
|
746
|
+
const finalResponse = appendGeneratedImageMarkdown(
|
|
747
|
+
extractFinalResponse(turn) || session.finalText,
|
|
748
|
+
generatedImagePaths,
|
|
749
|
+
);
|
|
730
750
|
session.finalText = finalResponse;
|
|
751
|
+
session.generatedImagePaths = [];
|
|
731
752
|
this.emit({
|
|
732
753
|
type: "turn_completed",
|
|
733
754
|
sessionId: session.localSessionId,
|
|
@@ -765,6 +786,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
765
786
|
: [],
|
|
766
787
|
),
|
|
767
788
|
finalText: "",
|
|
789
|
+
generatedImagePaths: [],
|
|
768
790
|
messagePhases: new Map(),
|
|
769
791
|
needsResume: false,
|
|
770
792
|
};
|
|
@@ -796,6 +818,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
796
818
|
if (!turnId) continue;
|
|
797
819
|
session.activeTurnId = undefined;
|
|
798
820
|
session.activeTurnStartedAt = undefined;
|
|
821
|
+
session.generatedImagePaths = [];
|
|
799
822
|
this.emit({
|
|
800
823
|
type: "turn_failed",
|
|
801
824
|
sessionId: session.localSessionId,
|
|
@@ -841,7 +864,13 @@ interface CodexThreadSnapshot {
|
|
|
841
864
|
interface CodexTurnSnapshot {
|
|
842
865
|
id: string;
|
|
843
866
|
status: "completed" | "interrupted" | "failed" | "inProgress";
|
|
844
|
-
items?: Array<{
|
|
867
|
+
items?: Array<{
|
|
868
|
+
type?: string;
|
|
869
|
+
text?: string;
|
|
870
|
+
phase?: string | null;
|
|
871
|
+
status?: string;
|
|
872
|
+
savedPath?: string;
|
|
873
|
+
}>;
|
|
845
874
|
error?: { message?: string } | null;
|
|
846
875
|
startedAt?: number | null;
|
|
847
876
|
durationMs?: number | null;
|
|
@@ -893,6 +922,17 @@ function extractFinalResponse(turn: CodexTurnSnapshot): string {
|
|
|
893
922
|
.join("\n\n");
|
|
894
923
|
}
|
|
895
924
|
|
|
925
|
+
function extractGeneratedImagePaths(turn: CodexTurnSnapshot): string[] {
|
|
926
|
+
return uniqueStrings((turn.items ?? []).flatMap((item) => {
|
|
927
|
+
const savedPath = stringValue(item.savedPath)?.trim();
|
|
928
|
+
return item.type === "imageGeneration" && item.status !== "failed" && savedPath ? [savedPath] : [];
|
|
929
|
+
}));
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function uniqueStrings(values: string[]): string[] {
|
|
933
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
934
|
+
}
|
|
935
|
+
|
|
896
936
|
function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary {
|
|
897
937
|
const lastTurn = thread.turns?.at(-1);
|
|
898
938
|
const lastCompletedTurn = [...(thread.turns ?? [])]
|
|
@@ -923,7 +963,9 @@ function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary
|
|
|
923
963
|
lastCompletedTurnId: lastCompletedTurn?.id,
|
|
924
964
|
lastTurnStatus,
|
|
925
965
|
lastActivity: lastText,
|
|
926
|
-
finalResponse: lastTurn && lastTurn.status !== "inProgress"
|
|
966
|
+
finalResponse: lastTurn && lastTurn.status !== "inProgress"
|
|
967
|
+
? appendGeneratedImageMarkdown(extractFinalResponse(lastTurn), extractGeneratedImagePaths(lastTurn)) || undefined
|
|
968
|
+
: undefined,
|
|
927
969
|
lastError: lastTurn?.error?.message,
|
|
928
970
|
lastTurnToolCount: toolCounts?.total,
|
|
929
971
|
lastTurnCompletedToolCount: toolCounts?.completed,
|
|
@@ -956,7 +998,8 @@ function isToolItemType(type: string | undefined): boolean {
|
|
|
956
998
|
|| type === "mcpToolCall"
|
|
957
999
|
|| type === "dynamicToolCall"
|
|
958
1000
|
|| type === "webSearch"
|
|
959
|
-
|| type === "imageView"
|
|
1001
|
+
|| type === "imageView"
|
|
1002
|
+
|| type === "imageGeneration";
|
|
960
1003
|
}
|
|
961
1004
|
|
|
962
1005
|
function mergeRemoteSessionSummary(
|