@keyou007/agent-bot 0.1.24-alpha.2 → 0.1.24-alpha.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/CHANGELOG.md +103 -0
- package/README.md +33 -5
- package/README.zh.md +32 -5
- package/dist/acp/AcpJsonRpcConnection.js +3 -1
- package/dist/acp/AcpJsonRpcConnection.js.map +1 -1
- package/dist/acp/AcpSessionManager.js +0 -14
- package/dist/acp/AcpSessionManager.js.map +1 -1
- package/dist/acp/acpTypes.js.map +1 -1
- package/dist/cli/SelfUpdateRunner.js.map +1 -1
- package/dist/cli/SelfUpdater.js +2 -0
- package/dist/cli/SelfUpdater.js.map +1 -1
- package/dist/codex/AppServerConnection.js +3 -1
- package/dist/codex/AppServerConnection.js.map +1 -1
- package/dist/codex/CodexEventMapper.js +28 -1
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexRuntime.js +92 -0
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +5 -3
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +45 -23
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +2 -0
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +44 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +114 -37
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/RecentMessageContext.js +70 -0
- package/dist/feishu/RecentMessageContext.js.map +1 -0
- package/dist/index.js +62 -24
- package/dist/index.js.map +1 -1
- package/dist/local-files/DiagramPreview.js +171 -0
- package/dist/local-files/DiagramPreview.js.map +1 -0
- package/dist/local-files/LocalFileViewerServer.js +141 -3
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/PreviewSyntaxHighlight.js +57 -0
- package/dist/local-files/PreviewSyntaxHighlight.js.map +1 -0
- package/dist/local-files/TurnPreviewPage.js +497 -62
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +18 -9
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +155 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/presentation/turnReasoning.js +23 -0
- package/dist/presentation/turnReasoning.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +466 -38
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +43 -2
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/TurnPreviewJournal.js +469 -0
- package/dist/state/TurnPreviewJournal.js.map +1 -0
- package/dist/state/migrations.js +10 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/updates/AutomaticUpdatePreparer.js +16 -7
- package/dist/updates/AutomaticUpdatePreparer.js.map +1 -1
- package/dist/updates/ManualUpdateController.js +102 -0
- package/dist/updates/ManualUpdateController.js.map +1 -0
- package/dist/updates/PublishedRelease.js +42 -4
- package/dist/updates/PublishedRelease.js.map +1 -1
- package/dist/utils/ProtocolLineWarning.js +15 -0
- package/dist/utils/ProtocolLineWarning.js.map +1 -0
- package/docs/technical-reference.md +21 -1
- package/docs/technical-reference.zh.md +21 -1
- package/npm-shrinkwrap.json +1401 -10
- package/package.json +3 -1
- package/skills/agent-bot/SKILL.md +12 -1
- package/src/acp/AcpJsonRpcConnection.ts +3 -1
- package/src/acp/AcpSessionManager.ts +0 -17
- package/src/acp/acpTypes.ts +0 -1
- package/src/cli/SelfUpdateRunner.ts +1 -0
- package/src/cli/SelfUpdater.ts +2 -0
- package/src/codex/AppServerConnection.ts +3 -1
- package/src/codex/CodexEventMapper.ts +29 -1
- package/src/codex/CodexRuntime.ts +87 -0
- package/src/commands/CommandRouter.ts +4 -3
- package/src/commands/commandTypes.ts +1 -0
- package/src/feishu/CardRenderer.ts +43 -21
- package/src/feishu/FeishuConnector.ts +2 -0
- package/src/feishu/FeishuMessageClient.ts +44 -0
- package/src/feishu/FeishuTurnPresenter.ts +107 -31
- package/src/feishu/RecentMessageContext.ts +90 -0
- package/src/feishu/types.ts +5 -0
- package/src/index.ts +62 -22
- package/src/local-files/DiagramPreview.ts +173 -0
- package/src/local-files/LocalFileViewerServer.ts +129 -2
- package/src/local-files/PreviewSyntaxHighlight.ts +58 -0
- package/src/local-files/TurnPreviewPage.ts +537 -59
- package/src/presentation/OutboundRouter.ts +25 -7
- package/src/presentation/TurnStateReducer.ts +151 -7
- package/src/presentation/turnReasoning.ts +21 -0
- package/src/presentation/turnViewTypes.ts +19 -1
- package/src/proxy/ProxySessionController.ts +457 -27
- package/src/runtime/types.ts +21 -0
- package/src/state/StateStore.ts +45 -2
- package/src/state/TurnPreviewJournal.ts +398 -0
- package/src/state/migrations.ts +10 -0
- package/src/updates/AutomaticUpdatePreparer.ts +21 -7
- package/src/updates/ManualUpdateController.ts +116 -0
- package/src/updates/PublishedRelease.ts +49 -3
- package/src/utils/ProtocolLineWarning.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keyou007/agent-bot",
|
|
3
|
-
"version": "0.1.24-alpha.
|
|
3
|
+
"version": "0.1.24-alpha.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Use Codex, TraeX, and compatible coding agents from Lark/Feishu.",
|
|
6
6
|
"license": "GPL-3.0-only",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"dotenv": "17.4.2",
|
|
76
76
|
"highlight.js": "11.12.0",
|
|
77
77
|
"markdown-it": "15.0.1",
|
|
78
|
+
"mermaid": "11.17.2",
|
|
78
79
|
"pino": "10.3.1",
|
|
79
80
|
"yaml": "2.9.0",
|
|
80
81
|
"zod": "4.4.3"
|
|
@@ -83,6 +84,7 @@
|
|
|
83
84
|
"@types/better-sqlite3": "7.6.13",
|
|
84
85
|
"@types/node": "22.18.11",
|
|
85
86
|
"eslint": "10.6.0",
|
|
87
|
+
"linkedom": "0.18.13",
|
|
86
88
|
"prettier": "3.9.5",
|
|
87
89
|
"tsx": "4.23.0",
|
|
88
90
|
"typescript": "7.0.2",
|
|
@@ -73,6 +73,7 @@ agentbot task title "<title>"
|
|
|
73
73
|
|
|
74
74
|
- `prompt` posts the Prompt to the task's Feishu conversation before submitting it.
|
|
75
75
|
- `queue` creates a later turn instead of steering the active turn. `nosteer` is an alias.
|
|
76
|
+
- The Feishu `/status` card includes `Preview` for the active or latest Turn when a saved preview and the file viewer are available; opening it does not switch or control the task.
|
|
76
77
|
- `stop` requests an Agent interrupt; it does not kill the Agent process.
|
|
77
78
|
- `release` sends a Lark confirmation card for releasing the Agent's shared App Server. It never releases automatically; the user must click **Release** or **Release Now** on the card.
|
|
78
79
|
- `dismiss --yes` irreversibly dissolves the task's current Feishu group and archives its current task. Use it only after the user explicitly confirms.
|
|
@@ -109,12 +110,16 @@ These commands export only user Prompts and final answers through the latest com
|
|
|
109
110
|
|
|
110
111
|
Feishu `/sessions [keyword]` lists and searches tasks across projects and all Providers, not just the App Server's current Provider. Switching Provider does not hide existing tasks; listing still uses paged metadata and the latest Turn summary only.
|
|
111
112
|
|
|
113
|
+
Feishu group renames update only the group's currently bound task title, not topic or historical tasks. Users may remove generated prefixes or use another format: recognized configured names or matching Agent prefixes yield the task title, otherwise the normalized complete group name is used. A renamed prefix does not change the task's Agent. New-group naming rules are unchanged.
|
|
114
|
+
|
|
112
115
|
Fork creation stores source-task and branch-Turn references without synchronizing the full local Turn list. The Feishu Turn card reuses local records and loads only the summary pages needed for the requested page, not the entire history. `task turns` loads the first page if necessary and returns available local records. This does not change the Agent's inherited context. Retry listing Turns after a temporary history-read failure; unsupported summary pagination never falls back to downloading full history.
|
|
113
116
|
|
|
114
117
|
If a TraeX task stops producing output, check its progress card for a pending plan-mode confirmation. `Approve Plan` / `Enter Plan Mode`, `Reject`, and `Cancel Request` require the user's explicit choice, even with automatic permissions. Preview displays the plan but cannot approve it. Cancelling a request does not itself stop the task; waiting turns still block safe restart. Never approve, stop, or force-restart merely to clear that wait. Notifications dropped by an older running version are not automatically replayed after upgrading.
|
|
115
118
|
|
|
116
119
|
## Change Settings
|
|
117
120
|
|
|
121
|
+
Feishu slash commands accept unique prefixes: `/mo`, `/mod`, and `/mode` open the `/model` settings card and do not accept arguments. This is model selection, not an Agent mode switch. CLI `task model [model]` keeps its documented argument behavior.
|
|
122
|
+
|
|
118
123
|
```powershell
|
|
119
124
|
agentbot task agent [name]
|
|
120
125
|
agentbot task provider [provider]
|
|
@@ -144,6 +149,10 @@ agentbot task reset <turn-id>
|
|
|
144
149
|
|
|
145
150
|
In Feishu, `/turn` (also `/turns`) opens history. `/turn <Turn ID>` or `/turn <index>` shows saved runtime details for the current task without resetting, resuming, or interrupting it. Indices match the history card: one-based, newest first, including a running Turn at the top. Missing records use only necessary summary pages; Fork queries stay within the branch anchor. Summary-only external Turns do not contain unrecorded tool output. This does not change the CLI's `task turns [task]` task-reference argument.
|
|
146
151
|
|
|
152
|
+
The Feishu `/sessions` card also has `SwitchGroup`: create a new group bound to the selected existing task, without forking or creating a new Agent task. Source bindings remain unchanged, and active Agent Bot Turns keep their original delivery route; messages from the new group queue instead of steering them. External active tasks are not taken over.
|
|
153
|
+
|
|
154
|
+
The Feishu `/sessions` card provides `Turns` for each task, including unbound external tasks. Browsing and pagination never switch, resume, or bind a task; Reset is available only for the current task.
|
|
155
|
+
|
|
147
156
|
Use `turns` to obtain a real Turn ID before `reset`. Reset changes conversation context only; it does not revert local files. Agent Bot announces an interactive Reset when it starts and queues every new message after its Reaction until the replacement thread is ready.
|
|
148
157
|
|
|
149
158
|
## Files And Local Commands
|
|
@@ -156,7 +165,7 @@ agentbot task shell "<command>"
|
|
|
156
165
|
|
|
157
166
|
Paths are resolved from the selected task's working directory; `~` means the operating-system user's home directory. `file` sends the file to the task's Feishu conversation. `shell` runs in the task directory.
|
|
158
167
|
|
|
159
|
-
For group mention-only mode:
|
|
168
|
+
For group mention-only mode (ordinary @ Prompts also receive bounded recent text/images from the current group or topic; scan newest to oldest and stop at the first message already submitted to the current task/branch, without reading older pages; requires Feishu history/group-message access; `/mute off` disables enrichment):
|
|
160
169
|
|
|
161
170
|
```powershell
|
|
162
171
|
agentbot task mute on|off
|
|
@@ -173,6 +182,8 @@ agentbot server restart --task <task> --reason "<reason>"
|
|
|
173
182
|
agentbot server stop
|
|
174
183
|
```
|
|
175
184
|
|
|
185
|
+
In Feishu, `/update` checks npm’s latest stable and Alpha releases and displays exact-version update buttons and release notes. It does not install on its own. Only the configured owner can select an update; choices expire after 30 minutes and never permit downgrade. It is available even when automatic checks are disabled. Let the user choose a version; do not click an update button merely to test this command. Source and npm-link installations can inspect releases but cannot use self-update.
|
|
186
|
+
|
|
176
187
|
`update` is only for npm-installed Agent Bot packages. It verifies the new package, sends a safe-restart card, waits for active tasks, and restarts immediately when the service is already idle. It automatically restores the prior version if activation fails. It refuses source checkouts and `npm link` installations.
|
|
177
188
|
|
|
178
189
|
The Feishu service also checks stable npm `latest` once daily at a persisted random time between 10:00 and 17:00 (server local time). A new version sends the owner a private release-notes card with 60 seconds to cancel. If not cancelled, preparation runs in the background and activation waits for tasks, final deliveries, and the quiet inbound window before restarting. Each version is notified once per Profile; cancellation survives restarts. An interrupted countdown resumes with a fresh minute on the same card. No private recipient, unconfirmed notification delivery, or missing release notes means no automatic installation. Source/npm-link protections still apply. Set `updates.enabled: false` in the Profile config and safely restart to disable checks. Do not bypass the countdown or idle gate.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProtocolLineWarning } from "../utils/ProtocolLineWarning.js";
|
|
1
2
|
import { EventEmitter } from "node:events";
|
|
2
3
|
import readline from "node:readline";
|
|
3
4
|
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
@@ -26,6 +27,7 @@ export interface AcpJsonRpcConnectionEvents {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export class AcpJsonRpcConnection extends EventEmitter {
|
|
30
|
+
private readonly invalidLineWarning = new ProtocolLineWarning();
|
|
29
31
|
private nextId = 1;
|
|
30
32
|
private readonly pending = new Map<string | number, PendingRequest>();
|
|
31
33
|
private readonly handlers = new Map<string, ClientMethodHandler>();
|
|
@@ -129,7 +131,7 @@ export class AcpJsonRpcConnection extends EventEmitter {
|
|
|
129
131
|
try {
|
|
130
132
|
message = JSON.parse(line) as JsonRpcMessage;
|
|
131
133
|
} catch (error) {
|
|
132
|
-
this.
|
|
134
|
+
this.invalidLineWarning.warn(this.logger, line, "Ignoring non-JSON ACP stdout line.");
|
|
133
135
|
return;
|
|
134
136
|
}
|
|
135
137
|
|
|
@@ -18,7 +18,6 @@ export interface RuntimeSession {
|
|
|
18
18
|
acpSessionId: string;
|
|
19
19
|
managed: ManagedAcpProcess;
|
|
20
20
|
running: boolean;
|
|
21
|
-
modes?: JsonValue;
|
|
22
21
|
configOptions?: JsonValue;
|
|
23
22
|
availableCommands?: JsonValue;
|
|
24
23
|
onUpdate: (session: RuntimeSession, update: Record<string, JsonValue>) => void;
|
|
@@ -81,7 +80,6 @@ export class AcpSessionManager {
|
|
|
81
80
|
acpSessionId: newSessionResult.sessionId,
|
|
82
81
|
managed,
|
|
83
82
|
running: false,
|
|
84
|
-
modes: newSessionResult.modes,
|
|
85
83
|
configOptions: newSessionResult.configOptions,
|
|
86
84
|
onUpdate: input.onUpdate,
|
|
87
85
|
onPermissionRequest: input.onPermissionRequest,
|
|
@@ -147,14 +145,6 @@ export class AcpSessionManager {
|
|
|
147
145
|
this.processManager.stopAll();
|
|
148
146
|
}
|
|
149
147
|
|
|
150
|
-
async setMode(localSessionId: string, modeId: string): Promise<void> {
|
|
151
|
-
const session = this.requireSession(localSessionId);
|
|
152
|
-
await session.managed.connection.request("session/set_mode", {
|
|
153
|
-
sessionId: session.acpSessionId,
|
|
154
|
-
modeId,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
148
|
async setConfigOption(localSessionId: string, configId: string, value: string): Promise<JsonValue> {
|
|
159
149
|
const session = this.requireSession(localSessionId);
|
|
160
150
|
const result = await session.managed.connection.request<JsonValue>("session/set_config_option", {
|
|
@@ -242,13 +232,6 @@ export class AcpSessionManager {
|
|
|
242
232
|
session.configOptions = update.configOptions;
|
|
243
233
|
}
|
|
244
234
|
|
|
245
|
-
if (updateType === "current_mode_update" && session.modes && isObject(session.modes)) {
|
|
246
|
-
session.modes = {
|
|
247
|
-
...session.modes,
|
|
248
|
-
currentModeId: update.modeId,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
235
|
if (updateType === "available_commands_update" && "availableCommands" in update) {
|
|
253
236
|
session.availableCommands = update.availableCommands;
|
|
254
237
|
}
|
package/src/acp/acpTypes.ts
CHANGED
package/src/cli/SelfUpdater.ts
CHANGED
|
@@ -316,6 +316,7 @@ export function finalizeSelfUpdatePlan(
|
|
|
316
316
|
workingDirectory?: string;
|
|
317
317
|
reason?: string;
|
|
318
318
|
notificationSessionId?: string;
|
|
319
|
+
notificationTarget?: SelfUpdatePlan["notificationTarget"];
|
|
319
320
|
},
|
|
320
321
|
): void {
|
|
321
322
|
const plan = readSelfUpdatePlan(planPath);
|
|
@@ -324,6 +325,7 @@ export function finalizeSelfUpdatePlan(
|
|
|
324
325
|
plan.restartService = values.restartService;
|
|
325
326
|
if (values.workingDirectory) plan.workingDirectory = values.workingDirectory;
|
|
326
327
|
if (values.reason) plan.reason = values.reason;
|
|
328
|
+
if (values.notificationTarget) plan.notificationTarget = values.notificationTarget;
|
|
327
329
|
if (values.notificationSessionId)
|
|
328
330
|
plan.notificationSessionId = values.notificationSessionId;
|
|
329
331
|
fs.writeFileSync(planPath, `${JSON.stringify(plan, null, 2)}\n`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProtocolLineWarning } from "../utils/ProtocolLineWarning.js";
|
|
1
2
|
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
3
|
import readline from "node:readline";
|
|
3
4
|
import type { Logger } from "pino";
|
|
@@ -42,6 +43,7 @@ export class AppServerRequestError extends Error {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export class AppServerConnection {
|
|
46
|
+
private readonly invalidLineWarning = new ProtocolLineWarning();
|
|
45
47
|
private nextId = 1;
|
|
46
48
|
private closed = false;
|
|
47
49
|
private readonly pending = new Map<AppServerRequestId, PendingRequest>();
|
|
@@ -108,7 +110,7 @@ export class AppServerConnection {
|
|
|
108
110
|
try {
|
|
109
111
|
message = JSON.parse(line) as AppServerMessage;
|
|
110
112
|
} catch (error) {
|
|
111
|
-
this.
|
|
113
|
+
this.invalidLineWarning.warn(this.logger, line, "Ignoring non-JSON App Server stdout line.");
|
|
112
114
|
return;
|
|
113
115
|
}
|
|
114
116
|
this.handleMessage(message);
|
|
@@ -51,7 +51,10 @@ export type MappedCodexNotification =
|
|
|
51
51
|
activityId: string;
|
|
52
52
|
text: string;
|
|
53
53
|
append: boolean;
|
|
54
|
+
reasoning?: { itemId: string; summaryIndex: number };
|
|
54
55
|
}
|
|
56
|
+
| { kind: "reasoning_delta"; threadId: string; turnId: string; itemId: string; contentIndex: number; text: string }
|
|
57
|
+
| { kind: "reasoning_completed"; threadId: string; turnId: string; itemId: string; summary: string[]; content: string[] }
|
|
55
58
|
| ({ kind: "mode_change_requested"; threadId: string; turnId: string } & z.infer<typeof modeChangeRequest>)
|
|
56
59
|
| ({ kind: "mode_change_resolved"; threadId: string; turnId: string } & z.infer<typeof modeChangeResolution>)
|
|
57
60
|
| { kind: "plan"; threadId: string; turnId: string; steps: PlanStep[] }
|
|
@@ -146,16 +149,24 @@ export function mapCodexNotification(method: string, params: unknown): MappedCod
|
|
|
146
149
|
const text = stringValue(params.delta);
|
|
147
150
|
const itemId = stringValue(params.itemId);
|
|
148
151
|
const summaryIndex = numberValue(params.summaryIndex);
|
|
149
|
-
if (text === undefined || !itemId || summaryIndex
|
|
152
|
+
if (text === undefined || !itemId || !isReasoningIndex(summaryIndex)) return undefined;
|
|
150
153
|
return {
|
|
151
154
|
kind: "progress",
|
|
152
155
|
threadId,
|
|
153
156
|
turnId,
|
|
154
157
|
activityId: `reasoning:${itemId}:${summaryIndex}`,
|
|
158
|
+
reasoning: { itemId, summaryIndex },
|
|
155
159
|
text,
|
|
156
160
|
append: true,
|
|
157
161
|
};
|
|
158
162
|
}
|
|
163
|
+
if (method === "item/reasoning/textDelta") {
|
|
164
|
+
const text = stringValue(params.delta);
|
|
165
|
+
const itemId = stringValue(params.itemId);
|
|
166
|
+
const contentIndex = numberValue(params.contentIndex);
|
|
167
|
+
if (text === undefined || !itemId || !isReasoningIndex(contentIndex)) return undefined;
|
|
168
|
+
return { kind: "reasoning_delta", threadId, turnId, itemId, contentIndex, text };
|
|
169
|
+
}
|
|
159
170
|
if (method === "turn/plan/updated" && Array.isArray(params.plan)) {
|
|
160
171
|
const steps = params.plan.flatMap((value): PlanStep[] => {
|
|
161
172
|
if (!isRecord(value) || typeof value.step !== "string") return [];
|
|
@@ -164,6 +175,14 @@ export function mapCodexNotification(method: string, params: unknown): MappedCod
|
|
|
164
175
|
return { kind: "plan", threadId, turnId, steps };
|
|
165
176
|
}
|
|
166
177
|
if ((method === "item/started" || method === "item/completed") && isRecord(params.item)) {
|
|
178
|
+
if (params.item.type === "reasoning") {
|
|
179
|
+
if (method !== "item/completed") return undefined;
|
|
180
|
+
const itemId = stringValue(params.item.id);
|
|
181
|
+
const summary = reasoningSections(params.item.summary);
|
|
182
|
+
const content = reasoningSections(params.item.content);
|
|
183
|
+
if (!itemId || !summary || !content) return undefined;
|
|
184
|
+
return { kind: "reasoning_completed", threadId, turnId, itemId, summary, content };
|
|
185
|
+
}
|
|
167
186
|
if (params.item.type === "plan") {
|
|
168
187
|
const itemId = stringValue(params.item.id);
|
|
169
188
|
const text = stringValue(params.item.text);
|
|
@@ -213,6 +232,15 @@ export function mapCodexNotification(method: string, params: unknown): MappedCod
|
|
|
213
232
|
return undefined;
|
|
214
233
|
}
|
|
215
234
|
|
|
235
|
+
function isReasoningIndex(value: number | undefined): value is number {
|
|
236
|
+
return value !== undefined && Number.isSafeInteger(value) && value >= 0 && value < 10_000;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function reasoningSections(value: unknown): string[] | undefined {
|
|
240
|
+
if (value === undefined) return [];
|
|
241
|
+
return Array.isArray(value) && value.every((section) => typeof section === "string") ? value : undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
216
244
|
export function formatCodexError(error: unknown): string | undefined {
|
|
217
245
|
if (!isRecord(error)) return undefined;
|
|
218
246
|
const message = stringValue(error.message)?.trim();
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
RemoteSessionPage,
|
|
17
17
|
RemoteSessionSummary,
|
|
18
18
|
RemoteTurnPage,
|
|
19
|
+
RemoteTurnDetails,
|
|
19
20
|
ResumeRuntimeSessionInput,
|
|
20
21
|
RuntimeGoal,
|
|
21
22
|
RuntimeGoalUpdate,
|
|
@@ -398,6 +399,48 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
398
399
|
};
|
|
399
400
|
}
|
|
400
401
|
|
|
402
|
+
async readRemoteTurn(remoteSessionId: string, turnId: string): Promise<RemoteTurnDetails> {
|
|
403
|
+
const client = await this.client();
|
|
404
|
+
const seenCursors = new Set<string>();
|
|
405
|
+
let cursor: string | undefined;
|
|
406
|
+
do {
|
|
407
|
+
const page = await this.readTurnPage(client, remoteSessionId, {
|
|
408
|
+
cursor, limit: 50, itemsView: "summary",
|
|
409
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
410
|
+
const index = page.data.findIndex((turn) => turn.id === turnId);
|
|
411
|
+
if (index >= 0) {
|
|
412
|
+
// Cursors are opaque; ask for the prefix to obtain the target's boundary.
|
|
413
|
+
if (index > 0) {
|
|
414
|
+
const prefix = await this.readTurnPage(client, remoteSessionId, {
|
|
415
|
+
cursor, limit: index, itemsView: "summary",
|
|
416
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
417
|
+
if (!prefix.nextCursor || prefix.data.at(-1)?.id !== page.data[index - 1]?.id) {
|
|
418
|
+
throw new Error("轮次历史发生变化,请重新打开重试。");
|
|
419
|
+
}
|
|
420
|
+
cursor = prefix.nextCursor;
|
|
421
|
+
}
|
|
422
|
+
const full = await this.readTurnPage(client, remoteSessionId, {
|
|
423
|
+
cursor, limit: 1, itemsView: "full",
|
|
424
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
425
|
+
const turn = full.data[0];
|
|
426
|
+
if (!turn || turn.id !== turnId) throw new Error("轮次历史发生变化,请重新打开重试。");
|
|
427
|
+
if (turn.itemsView === "summary" || !Array.isArray(turn.items)) {
|
|
428
|
+
throw new Error("Agent 未返回完整的轮次执行记录。");
|
|
429
|
+
}
|
|
430
|
+
if (turn.status === "inProgress") throw new Error("该历史轮次仍在执行,请稍后重试。");
|
|
431
|
+
return remoteTurnDetails(remoteSessionId, turn);
|
|
432
|
+
}
|
|
433
|
+
const nextCursor = page.nextCursor ?? undefined;
|
|
434
|
+
if (!nextCursor) break;
|
|
435
|
+
if (nextCursor === cursor || seenCursors.has(nextCursor)) {
|
|
436
|
+
throw new Error("App Server repeated a Turn history cursor.");
|
|
437
|
+
}
|
|
438
|
+
seenCursors.add(nextCursor);
|
|
439
|
+
cursor = nextCursor;
|
|
440
|
+
} while (cursor);
|
|
441
|
+
throw new Error(`Agent 历史中未找到 Turn:${turnId}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
401
444
|
async *readConversation(remoteSessionId: string, throughTurnId?: string): AsyncIterable<ConversationTurn> {
|
|
402
445
|
const client = await this.client();
|
|
403
446
|
const seenCursors = new Set<string>();
|
|
@@ -1211,12 +1254,19 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1211
1254
|
append: false,
|
|
1212
1255
|
severity: "warning",
|
|
1213
1256
|
});
|
|
1257
|
+
} else if (mapped.kind === "reasoning_delta") {
|
|
1258
|
+
this.emit({ type: "reasoning_delta", sessionId, turnId: mapped.turnId,
|
|
1259
|
+
itemId: mapped.itemId, contentIndex: mapped.contentIndex, text: mapped.text });
|
|
1260
|
+
} else if (mapped.kind === "reasoning_completed") {
|
|
1261
|
+
this.emit({ type: "reasoning_completed", sessionId, turnId: mapped.turnId,
|
|
1262
|
+
itemId: mapped.itemId, summary: mapped.summary, content: mapped.content });
|
|
1214
1263
|
} else if (mapped.kind === "progress") {
|
|
1215
1264
|
this.emit({
|
|
1216
1265
|
type: "progress",
|
|
1217
1266
|
sessionId,
|
|
1218
1267
|
turnId: mapped.turnId,
|
|
1219
1268
|
activityId: mapped.activityId,
|
|
1269
|
+
...(mapped.reasoning ? { reasoning: mapped.reasoning } : {}),
|
|
1220
1270
|
text: mapped.text,
|
|
1221
1271
|
append: mapped.append,
|
|
1222
1272
|
});
|
|
@@ -1626,6 +1676,8 @@ interface CodexThreadSnapshot {
|
|
|
1626
1676
|
|
|
1627
1677
|
interface CodexTurnSnapshot {
|
|
1628
1678
|
id: string;
|
|
1679
|
+
itemsView?: "summary" | "full";
|
|
1680
|
+
completedAt?: number | null;
|
|
1629
1681
|
status: "completed" | "interrupted" | "failed" | "inProgress";
|
|
1630
1682
|
items?: Array<{
|
|
1631
1683
|
id?: string;
|
|
@@ -1722,6 +1774,41 @@ function turnStartedAt(turn: CodexTurnSnapshot): number {
|
|
|
1722
1774
|
return typeof turn.startedAt === "number" ? turn.startedAt * 1_000 : Date.now();
|
|
1723
1775
|
}
|
|
1724
1776
|
|
|
1777
|
+
function remoteTurnDetails(remoteSessionId: string, turn: CodexTurnSnapshot): RemoteTurnDetails {
|
|
1778
|
+
const items: RemoteTurnDetails["items"] = [];
|
|
1779
|
+
const finalMessages = new Set(finalResponseMessages(turn));
|
|
1780
|
+
for (const [index, item] of (turn.items ?? []).entries()) {
|
|
1781
|
+
const id = item.id ?? `history:${index}`;
|
|
1782
|
+
if (item.type === "userMessage") {
|
|
1783
|
+
const content: unknown[] = item.content ?? [];
|
|
1784
|
+
const text = content.flatMap((block) => isRecord(block) && block.type === "text" && typeof block.text === "string" ? [block.text] : []).join("\n");
|
|
1785
|
+
const localImagePaths = content.flatMap((block) => isRecord(block) && block.type === "localImage" && typeof block.path === "string" ? [block.path] : []);
|
|
1786
|
+
items.push({ kind: "message", id, role: "user", text, ...(localImagePaths.length ? { localImagePaths } : {}) });
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
if (item.type === "agentMessage") {
|
|
1790
|
+
if (!finalMessages.has(item) && item.text) items.push({ kind: "message", id, role: "assistant", text: item.text });
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
const mapped = mapCodexNotification("item/completed", { threadId: remoteSessionId, turnId: turn.id, item });
|
|
1794
|
+
if (mapped?.kind === "tool") items.push({ kind: "tool", tool: mapped.tool });
|
|
1795
|
+
else if (mapped?.kind === "reasoning_completed") {
|
|
1796
|
+
items.push({ kind: "reasoning", id: mapped.itemId, summary: mapped.summary, content: mapped.content });
|
|
1797
|
+
} else if (mapped?.kind === "progress") items.push({ kind: "message", id, role: "assistant", text: mapped.text });
|
|
1798
|
+
else if (mapped?.kind === "plan") items.push({ kind: "plan", steps: mapped.steps });
|
|
1799
|
+
}
|
|
1800
|
+
return {
|
|
1801
|
+
turnId: turn.id,
|
|
1802
|
+
status: turn.status === "failed" ? "failed" : turn.status === "interrupted" ? "cancelled" : "completed",
|
|
1803
|
+
startedAt: typeof turn.startedAt === "number" ? turn.startedAt * 1_000 : undefined,
|
|
1804
|
+
completedAt: typeof turn.completedAt === "number" ? turn.completedAt * 1_000 : undefined,
|
|
1805
|
+
durationMs: turn.durationMs ?? undefined,
|
|
1806
|
+
finalResponse: appendGeneratedImageMarkdown(extractFinalResponse(turn), extractGeneratedImagePaths(turn)),
|
|
1807
|
+
error: formatCodexError(turn.error),
|
|
1808
|
+
items,
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1725
1812
|
function extractFinalResponse(turn: CodexTurnSnapshot): string {
|
|
1726
1813
|
return finalResponseMessages(turn).map((item) => item.text!.trim()).join("\n\n");
|
|
1727
1814
|
}
|
|
@@ -29,6 +29,7 @@ export const COMMAND_NAMES = [
|
|
|
29
29
|
"thinking",
|
|
30
30
|
"title",
|
|
31
31
|
"turns",
|
|
32
|
+
"update",
|
|
32
33
|
] as const;
|
|
33
34
|
|
|
34
35
|
type CommandName = (typeof COMMAND_NAMES)[number];
|
|
@@ -41,8 +42,6 @@ const COMMAND_INITIALISMS: Partial<Record<CommandName, string>> = {
|
|
|
41
42
|
nosteer: "ns",
|
|
42
43
|
};
|
|
43
44
|
|
|
44
|
-
const DISABLED_COMMAND_NAMES = new Set(["mode", "modes"]);
|
|
45
|
-
|
|
46
45
|
export class CommandRouter {
|
|
47
46
|
parse(text: string): Command {
|
|
48
47
|
const trimmed = text.trim();
|
|
@@ -121,6 +120,9 @@ export class CommandRouter {
|
|
|
121
120
|
if (args.length === 0) return { type: "restart" };
|
|
122
121
|
if (args.length === 1 && args[0] === "--force") return { type: "restart", force: true };
|
|
123
122
|
throw new Error("/restart 只接受一个可选的 --force 参数。");
|
|
123
|
+
case "update":
|
|
124
|
+
if (args.length > 0) throw new Error("/update 不接受参数,请在卡片中选择版本。");
|
|
125
|
+
return { type: "update" };
|
|
124
126
|
case "release":
|
|
125
127
|
if (args.length > 0) throw new Error("/release 不接受参数。");
|
|
126
128
|
return { type: "release" };
|
|
@@ -158,7 +160,6 @@ export class CommandRouter {
|
|
|
158
160
|
function resolveCommandName(rawCommand: string): CommandName {
|
|
159
161
|
const input = rawCommand.slice(1).toLowerCase();
|
|
160
162
|
if (!input) throw unknownCommand(rawCommand);
|
|
161
|
-
if (DISABLED_COMMAND_NAMES.has(input)) throw unknownCommand(rawCommand);
|
|
162
163
|
|
|
163
164
|
const exact = COMMAND_NAMES.find((command) => command === input);
|
|
164
165
|
if (exact) return exact;
|
|
@@ -21,6 +21,7 @@ export type Command =
|
|
|
21
21
|
| { type: "goal"; action: "pause" | "resume" | "clear" }
|
|
22
22
|
| { type: "restart"; force?: boolean }
|
|
23
23
|
| { type: "release" }
|
|
24
|
+
| { type: "update" }
|
|
24
25
|
| { type: "mute"; enabled: boolean }
|
|
25
26
|
| { type: "turns"; turnReference?: string }
|
|
26
27
|
| { type: "model" }
|
|
@@ -246,6 +246,8 @@ export interface ResetHistoryCardEntry extends TaskListCardEntry {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
export interface ResetHistoryCardView {
|
|
249
|
+
taskTitle?: string;
|
|
250
|
+
readOnly?: boolean;
|
|
249
251
|
entries: ResetHistoryCardEntry[];
|
|
250
252
|
footerLines: string[];
|
|
251
253
|
pageActions: TaskListCardAction[];
|
|
@@ -1267,6 +1269,11 @@ export class CardRenderer {
|
|
|
1267
1269
|
: "任务执行详情";
|
|
1268
1270
|
const elements = [markdown(`Turn: \`${state.turnId}\``)];
|
|
1269
1271
|
if (state.prompt) elements.push(markdown(`**Prompt**\n${truncateText(state.prompt, 1_000)}`));
|
|
1272
|
+
if (state.historyDetailError) {
|
|
1273
|
+
elements.push(markdown(`**历史执行详情读取失败**\n${truncateText(state.historyDetailError, 1_000)}\n请重新打开详情或 Preview 重试;本轮执行状态未改变。`));
|
|
1274
|
+
} else if (state.historyDetail === "summary") {
|
|
1275
|
+
elements.push(markdown("当前仅保存轮次摘要,尚未加载历史执行详情。"));
|
|
1276
|
+
}
|
|
1270
1277
|
elements.push(...renderTurnElements(state, "always"));
|
|
1271
1278
|
if (previewUrl) elements.push(taskActionRow([turnPreviewAction(previewUrl)]));
|
|
1272
1279
|
return turnCard(title, "blue", elements, renderTurnSubtitle(state));
|
|
@@ -1604,13 +1611,16 @@ export class CardRenderer {
|
|
|
1604
1611
|
renderResetHistoryCard(view: ResetHistoryCardView): Record<string, unknown> {
|
|
1605
1612
|
const elements: Record<string, unknown>[] = [
|
|
1606
1613
|
{
|
|
1607
|
-
...markdown(
|
|
1614
|
+
...markdown(view.readOnly
|
|
1615
|
+
? "<font color='grey'>仅查看此任务的轮次;如需 Reset,请先切换到此任务。</font>"
|
|
1616
|
+
: "<font color='grey'>Reset 会将当前任务的对话上下文恢复到所选轮次完成时;不会回退本地文件。</font>"),
|
|
1608
1617
|
text_size: "notation",
|
|
1609
1618
|
},
|
|
1610
1619
|
{ tag: "hr" },
|
|
1611
1620
|
];
|
|
1621
|
+
if (view.taskTitle) elements.unshift(markdown(`**任务**:${escapeCardActionText(view.taskTitle)}`));
|
|
1612
1622
|
if (view.entries.length === 0) {
|
|
1613
|
-
elements.push(markdown("当前任务还没有成功完成的 turn。"));
|
|
1623
|
+
elements.push(markdown(view.readOnly ? "此任务还没有可显示的 turn。" : "当前任务还没有成功完成的 turn。"));
|
|
1614
1624
|
} else {
|
|
1615
1625
|
const sequenceWidth = `${Math.max(16, ...view.entries.map((entry) => String(entry.sequence).length * 10))}px`;
|
|
1616
1626
|
view.entries.forEach((entry) => {
|
|
@@ -1674,13 +1684,13 @@ function renderTurnElements(
|
|
|
1674
1684
|
const pages = activityPages(allActivities);
|
|
1675
1685
|
const visibleActivities = pages.at(-1) ?? [];
|
|
1676
1686
|
if (state.plan.length > 0) elements.push(planPanel(state.plan));
|
|
1677
|
-
if (pages.length > 1) {
|
|
1687
|
+
if (pages.length > 1 || (state.previewJournal && state.activitiesTruncated)) {
|
|
1678
1688
|
elements.push(taskActionRow([{
|
|
1679
|
-
text: `查看历史思考(共 ${pages.length} 页)`,
|
|
1689
|
+
text: state.activitiesTruncated ? "查看历史思考" : `查看历史思考(共 ${pages.length} 页)`,
|
|
1680
1690
|
value: {
|
|
1681
1691
|
action: "activity_history",
|
|
1682
1692
|
turnId: state.turnId,
|
|
1683
|
-
page: String(pages.length - 2),
|
|
1693
|
+
page: String(state.activitiesTruncated ? Number.MAX_SAFE_INTEGER : pages.length - 2),
|
|
1684
1694
|
},
|
|
1685
1695
|
}]));
|
|
1686
1696
|
}
|
|
@@ -1743,18 +1753,20 @@ function renderGroupedTurnElements(
|
|
|
1743
1753
|
fullActivityText: true,
|
|
1744
1754
|
});
|
|
1745
1755
|
if (state.plan.length > 0) elements.push(planPanel(state.plan));
|
|
1746
|
-
if (historyPages.length > 0) {
|
|
1756
|
+
if (historyPages.length > 0 || (state.previewJournal && state.activitiesTruncated)) {
|
|
1747
1757
|
elements.push(taskActionRow([{
|
|
1748
|
-
text: `查看历史思考(共 ${historyPages.length + 1} 页)`,
|
|
1758
|
+
text: state.activitiesTruncated ? "查看历史思考" : `查看历史思考(共 ${historyPages.length + 1} 页)`,
|
|
1749
1759
|
value: {
|
|
1750
1760
|
action: "activity_history",
|
|
1751
1761
|
turnId: state.turnId,
|
|
1752
|
-
page: String(historyPages.length - 1),
|
|
1762
|
+
page: String(state.activitiesTruncated ? Number.MAX_SAFE_INTEGER : historyPages.length - 1),
|
|
1753
1763
|
},
|
|
1754
1764
|
}]));
|
|
1755
1765
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1766
|
+
const visibleGroups: GroupedTurnActivity[] = state.activitiesTruncated
|
|
1767
|
+
? [{ kind: "gap", id: "gap:truncated" }, ...visible.groups]
|
|
1768
|
+
: visible.groups;
|
|
1769
|
+
elements.push(...renderGroupedActivityGroups(visibleGroups, state.projectCwd));
|
|
1758
1770
|
if (state.fileSummary.length > 0) elements.push(fileSummaryPanel(state));
|
|
1759
1771
|
|
|
1760
1772
|
if (state.approval) {
|
|
@@ -1913,17 +1925,24 @@ function renderGroupedActivityGroups(
|
|
|
1913
1925
|
fullActivityText?: boolean;
|
|
1914
1926
|
} = {},
|
|
1915
1927
|
): Record<string, unknown>[] {
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
return renderReasoningGroup(reasonings);
|
|
1928
|
+
const elements: Record<string, unknown>[] = [];
|
|
1929
|
+
let previousWasGap = false;
|
|
1930
|
+
for (const group of groups) {
|
|
1931
|
+
if (group.kind === "gap") {
|
|
1932
|
+
if (!previousWasGap) elements.push(markdown("…"));
|
|
1933
|
+
previousWasGap = true;
|
|
1934
|
+
continue;
|
|
1924
1935
|
}
|
|
1925
|
-
|
|
1926
|
-
|
|
1936
|
+
const rendered = group.kind === "activity"
|
|
1937
|
+
? renderActivity(group.activity, projectCwd, options.fullActivityText === true)
|
|
1938
|
+
: group.tools.length === 0
|
|
1939
|
+
? renderReasoningGroup(group.latestReasoning ? [group.latestReasoning] : [])
|
|
1940
|
+
: [executionActivityPanel(group, projectCwd)];
|
|
1941
|
+
if (rendered.length === 0) continue;
|
|
1942
|
+
elements.push(...rendered);
|
|
1943
|
+
previousWasGap = false;
|
|
1944
|
+
}
|
|
1945
|
+
return elements;
|
|
1927
1946
|
}
|
|
1928
1947
|
|
|
1929
1948
|
function groupTurnActivities(activities: TurnActivity[]): GroupedTurnActivity[] {
|
|
@@ -2144,7 +2163,10 @@ function groupedLiveActivityPage(
|
|
|
2144
2163
|
}
|
|
2145
2164
|
|
|
2146
2165
|
function isCommentaryGroup(group: GroupedTurnActivity): boolean {
|
|
2147
|
-
return group.kind === "activity"
|
|
2166
|
+
return group.kind === "activity"
|
|
2167
|
+
&& group.activity.kind !== "tool"
|
|
2168
|
+
&& isCommentaryActivity(group.activity)
|
|
2169
|
+
&& group.activity.text.trim().length > 0;
|
|
2148
2170
|
}
|
|
2149
2171
|
|
|
2150
2172
|
function groupsWithGaps(
|
|
@@ -262,6 +262,8 @@ function toIncomingMessage(
|
|
|
262
262
|
const messageId = message.message_id ?? `${Date.now()}`;
|
|
263
263
|
return {
|
|
264
264
|
messageId,
|
|
265
|
+
...(Number.isFinite(Number(message.create_time)) && Number(message.create_time) > 0
|
|
266
|
+
? { createdAt: Number(message.create_time) } : {}),
|
|
265
267
|
contextKey: chatId
|
|
266
268
|
? threadContext
|
|
267
269
|
? threadContextKey(chatId, threadId!)
|
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
type MergedForwardMessageItem,
|
|
16
16
|
type ReferencedMessageContent,
|
|
17
17
|
} from "./MergedForwardMessage.js";
|
|
18
|
+
import { RECENT_CONTEXT_LIMIT, RECENT_CONTEXT_MAX_PAGES, RECENT_CONTEXT_PAGE_SIZE, recentMessagePageSchema, scanRecentContextMessages, selectRecentContextMessages } from "./RecentMessageContext.js";
|
|
18
19
|
import type {
|
|
20
|
+
RecentMessageRequest,
|
|
21
|
+
RecentContextMessage,
|
|
19
22
|
CreatedGroup,
|
|
20
23
|
CreateGroupInput,
|
|
21
24
|
FeishuOutbound,
|
|
@@ -251,6 +254,47 @@ export class FeishuMessageClient implements FeishuOutbound {
|
|
|
251
254
|
return renderMergedForwardPrompt(messageId, items);
|
|
252
255
|
}
|
|
253
256
|
|
|
257
|
+
async readRecentMessages(request: RecentMessageRequest): Promise<RecentContextMessage[]> {
|
|
258
|
+
const operation = "read recent group context";
|
|
259
|
+
try {
|
|
260
|
+
const token = await this.getTenantAccessToken();
|
|
261
|
+
const items: NonNullable<NonNullable<ReturnType<typeof recentMessagePageSchema.parse>["data"]>["items"]> = [];
|
|
262
|
+
let pageToken: string | undefined;
|
|
263
|
+
let selected: RecentContextMessage[] = [];
|
|
264
|
+
let reachedKnownMessage = false;
|
|
265
|
+
for (let page = 0; page < RECENT_CONTEXT_MAX_PAGES; page++) {
|
|
266
|
+
const url = new URL("https://open.feishu.cn/open-apis/im/v1/messages");
|
|
267
|
+
url.searchParams.set("container_id_type", request.threadId ? "thread" : "chat");
|
|
268
|
+
url.searchParams.set("container_id", request.threadId ?? request.chatId);
|
|
269
|
+
url.searchParams.set("sort_type", "ByCreateTimeDesc");
|
|
270
|
+
url.searchParams.set("page_size", String(RECENT_CONTEXT_PAGE_SIZE));
|
|
271
|
+
if (pageToken) url.searchParams.set("page_token", pageToken);
|
|
272
|
+
if (!request.threadId && request.beforeTimestamp) {
|
|
273
|
+
url.searchParams.set("end_time", String(Math.ceil(request.beforeTimestamp / 1000)));
|
|
274
|
+
}
|
|
275
|
+
const response = await fetch(url, {
|
|
276
|
+
headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(10_000),
|
|
277
|
+
});
|
|
278
|
+
const payload = recentMessagePageSchema.parse(await response.json());
|
|
279
|
+
if (!response.ok || payload.code !== 0) {
|
|
280
|
+
throw new FeishuApiError(payload.msg || "无法读取近期群消息;请检查飞书历史消息及群消息读取权限。", payload.code, { code: payload.code, msg: payload.msg ?? "" }, operation, response.status);
|
|
281
|
+
}
|
|
282
|
+
items.push(...(payload.data?.items ?? []));
|
|
283
|
+
if (request.beforeTimestamp || items.some((item) => item.message_id === request.beforeMessageId)) {
|
|
284
|
+
const scanned = scanRecentContextMessages(items, request);
|
|
285
|
+
selected = scanned.messages;
|
|
286
|
+
reachedKnownMessage = scanned.reachedKnownMessage;
|
|
287
|
+
}
|
|
288
|
+
const nextToken = payload.data?.page_token;
|
|
289
|
+
if (reachedKnownMessage || selected.length >= RECENT_CONTEXT_LIMIT || !payload.data?.has_more || !nextToken || nextToken === pageToken) break;
|
|
290
|
+
pageToken = nextToken;
|
|
291
|
+
}
|
|
292
|
+
return selectRecentContextMessages(items, request);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
throw normalizeTransportError(error, operation);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
254
298
|
async readReferencedMessage(messageId: string): Promise<ReferencedMessageContent> {
|
|
255
299
|
const items = await this.readMessageItems(messageId, "read referenced message");
|
|
256
300
|
return renderReferencedMessage(messageId, items);
|