@keyou007/agent-bot 0.1.16 → 0.1.17-alpha.1
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 +14 -0
- package/README.md +4 -2
- package/README.zh.md +4 -2
- package/config.example.yaml +8 -0
- package/dist/cli/help.js +4 -4
- package/dist/cli/taskTarget.js +7 -0
- package/dist/cli/taskTarget.js.map +1 -1
- package/dist/cli.js +12 -12
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +2 -1
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +62 -14
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/config/AgentExecutionDefaultsStore.js +71 -0
- package/dist/config/AgentExecutionDefaultsStore.js.map +1 -0
- package/dist/config/loadConfig.js +8 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +7 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +29 -27
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +61 -5
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +2 -2
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +255 -30
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +62 -9
- package/dist/state/StateStore.js.map +1 -1
- package/dist/supervision/restartNotificationTarget.js +9 -0
- package/dist/supervision/restartNotificationTarget.js.map +1 -0
- package/dist/utils/spawnCommand.js +2 -1
- package/dist/utils/spawnCommand.js.map +1 -1
- package/docs/technical-reference.md +7 -3
- package/docs/technical-reference.zh.md +7 -3
- package/npm-shrinkwrap.json +9 -9
- package/package.json +2 -2
- package/skills/agent-bot/SKILL.md +3 -1
- package/src/cli/help.ts +4 -4
- package/src/cli/taskTarget.ts +11 -0
- package/src/cli.ts +16 -20
- package/src/codex/CodexProcessManager.ts +3 -1
- package/src/codex/CodexRuntime.ts +63 -12
- package/src/config/AgentExecutionDefaultsStore.ts +71 -0
- package/src/config/loadConfig.ts +8 -0
- package/src/config/schema.ts +13 -1
- package/src/feishu/CardRenderer.ts +30 -22
- package/src/feishu/FeishuConnector.ts +70 -5
- package/src/feishu/FeishuTurnPresenter.ts +2 -2
- package/src/index.ts +17 -2
- package/src/proxy/ProxySessionController.ts +330 -40
- package/src/runtime/types.ts +8 -0
- package/src/state/StateStore.ts +100 -9
- package/src/supervision/restartNotificationTarget.ts +12 -0
- package/src/utils/spawnCommand.ts +2 -0
package/src/cli.ts
CHANGED
|
@@ -100,6 +100,7 @@ import {
|
|
|
100
100
|
} from "./cli/taskListOutput.js";
|
|
101
101
|
import {
|
|
102
102
|
resolveCurrentTaskFromEnvironment,
|
|
103
|
+
resolveRestartNotificationTask,
|
|
103
104
|
resolveTask,
|
|
104
105
|
resolveTaskCommandTarget,
|
|
105
106
|
} from "./cli/taskTarget.js";
|
|
@@ -1186,33 +1187,28 @@ function resolveRestartNotificationSessionId(sqlitePath: string, args: string[])
|
|
|
1186
1187
|
const store = new StateStore(sqlitePath);
|
|
1187
1188
|
try {
|
|
1188
1189
|
const sessions = store.listAllSessions();
|
|
1190
|
+
let explicitReference: string | undefined;
|
|
1189
1191
|
if (args.includes("--task")) {
|
|
1190
1192
|
const reference = optionValue(args, "--task")?.trim();
|
|
1191
1193
|
if (!reference || reference.startsWith("--")) throw new Error(cliText(
|
|
1192
1194
|
"server restart --task requires a task number or task ID.",
|
|
1193
1195
|
"server restart --task 需要任务序号或任务 ID。",
|
|
1194
1196
|
));
|
|
1195
|
-
|
|
1196
|
-
if (
|
|
1197
|
-
isThreadContextKey(session.contextKey)
|
|
1198
|
-
&& !store.findLatestMessageIdForSession(session.localSessionId, session.contextKey)
|
|
1199
|
-
&& !store.findLatestMessageIdForContext(session.contextKey)
|
|
1200
|
-
) {
|
|
1201
|
-
throw new Error(cliText(
|
|
1202
|
-
"The task topic has no message anchor for restart notifications. Send /restart in that topic instead.",
|
|
1203
|
-
"该任务话题没有可用于重启通知的消息锚点,请直接在该话题中发送 /restart。",
|
|
1204
|
-
));
|
|
1205
|
-
}
|
|
1206
|
-
return session.localSessionId;
|
|
1197
|
+
explicitReference = reference;
|
|
1207
1198
|
}
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
)
|
|
1215
|
-
|
|
1199
|
+
const session = resolveRestartNotificationTask(sessions, explicitReference);
|
|
1200
|
+
if (!session) return undefined;
|
|
1201
|
+
if (
|
|
1202
|
+
isThreadContextKey(session.contextKey)
|
|
1203
|
+
&& !store.findLatestMessageIdForSession(session.localSessionId, session.contextKey)
|
|
1204
|
+
&& !store.findLatestMessageIdForContext(session.contextKey)
|
|
1205
|
+
) {
|
|
1206
|
+
throw new Error(cliText(
|
|
1207
|
+
"The task topic has no message anchor for restart notifications. Send /restart in that topic instead.",
|
|
1208
|
+
"该任务话题没有可用于重启通知的消息锚点,请直接在该话题中发送 /restart。",
|
|
1209
|
+
));
|
|
1210
|
+
}
|
|
1211
|
+
return session.localSessionId;
|
|
1216
1212
|
} finally {
|
|
1217
1213
|
store.close();
|
|
1218
1214
|
}
|
|
@@ -29,10 +29,12 @@ export class CodexProcessManager implements AppServerClientProvider {
|
|
|
29
29
|
async getClient(): Promise<AppServerClient> {
|
|
30
30
|
if (this.client) return this.client;
|
|
31
31
|
this.version = undefined;
|
|
32
|
+
const environmentContext = this.environmentContext();
|
|
32
33
|
const child = spawnStdioCommand(
|
|
33
34
|
this.command,
|
|
34
35
|
this.args,
|
|
35
|
-
agentBotEnvironment(process.env, this.env,
|
|
36
|
+
agentBotEnvironment(process.env, this.env, environmentContext),
|
|
37
|
+
environmentContext.profilePath ?? os.homedir(),
|
|
36
38
|
);
|
|
37
39
|
this.child = child;
|
|
38
40
|
const client = new AppServerConnection(child, this.logger.child({ component: "codex-app-server" }));
|
|
@@ -191,7 +191,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
191
191
|
const session = this.requireSession(sessionId);
|
|
192
192
|
const client = await this.client();
|
|
193
193
|
await this.ensureSessionResumed(session, client);
|
|
194
|
-
const
|
|
194
|
+
const start = () => client.request<{ turn: { id: string } }>("turn/start", {
|
|
195
195
|
threadId: session.remoteSessionId,
|
|
196
196
|
input: codexUserInput(prompt),
|
|
197
197
|
cwd: session.cwd,
|
|
@@ -200,6 +200,19 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
200
200
|
summary: "auto",
|
|
201
201
|
approvalPolicy: session.permissionMode === "auto" ? "never" : "on-request",
|
|
202
202
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
203
|
+
let response: { turn: { id: string } };
|
|
204
|
+
try {
|
|
205
|
+
response = await start();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (!isInvalidWorkingDirectoryError(error)) throw error;
|
|
208
|
+
this.logger.warn(
|
|
209
|
+
{ error, sessionId: session.localSessionId, cwd: session.cwd },
|
|
210
|
+
"App Server rejected the task working directory; refreshing the current thread before retrying.",
|
|
211
|
+
);
|
|
212
|
+
await this.resumeAppServerSession(session, client);
|
|
213
|
+
session.needsResume = false;
|
|
214
|
+
response = await start();
|
|
215
|
+
}
|
|
203
216
|
if (session.activeTurnId !== response.turn.id) {
|
|
204
217
|
this.adoptTurn(session, response.turn.id, Date.now());
|
|
205
218
|
}
|
|
@@ -746,6 +759,11 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
746
759
|
|
|
747
760
|
private async ensureSessionResumed(session: CodexSession, client: AppServerClient): Promise<void> {
|
|
748
761
|
if (!session.needsResume) return;
|
|
762
|
+
await this.resumeAppServerSession(session, client);
|
|
763
|
+
session.needsResume = false;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private async resumeAppServerSession(session: CodexSession, client: AppServerClient): Promise<void> {
|
|
749
767
|
await client.request("thread/resume", {
|
|
750
768
|
threadId: session.remoteSessionId,
|
|
751
769
|
excludeTurns: true,
|
|
@@ -755,7 +773,6 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
755
773
|
...threadLifecycleParams(session.cwd),
|
|
756
774
|
...permissionParams(session.permissionMode),
|
|
757
775
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
758
|
-
session.needsResume = false;
|
|
759
776
|
}
|
|
760
777
|
|
|
761
778
|
private async synchronizeSessionNow(sessionId: string): Promise<RuntimeSession> {
|
|
@@ -1084,6 +1101,12 @@ function isUnsupportedExcludeTurnsError(error: unknown): boolean {
|
|
|
1084
1101
|
|| /unknown|unrecognized|unexpected|unsupported|not supported|experimental/i.test(details);
|
|
1085
1102
|
}
|
|
1086
1103
|
|
|
1104
|
+
function isInvalidWorkingDirectoryError(error: unknown): boolean {
|
|
1105
|
+
return error instanceof AppServerRequestError
|
|
1106
|
+
&& error.method === "turn/start"
|
|
1107
|
+
&& /invalid cwd/i.test(`${error.serverMessage} ${error.data === undefined ? "" : JSON.stringify(error.data)}`);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1087
1110
|
function isUnsupportedRecencySortError(error: unknown): boolean {
|
|
1088
1111
|
if (!(error instanceof AppServerRequestError) || error.method !== "thread/list") return false;
|
|
1089
1112
|
const data = typeof error.data === "string"
|
|
@@ -1112,6 +1135,18 @@ function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary
|
|
|
1112
1135
|
const lastText = [...(lastTurn?.items ?? [])]
|
|
1113
1136
|
.reverse()
|
|
1114
1137
|
.find((item) => typeof item.text === "string" && item.text.trim())?.text?.trim();
|
|
1138
|
+
const completedTurns = (thread.turns ?? []).flatMap((turn) => {
|
|
1139
|
+
if (turn.status !== "completed") return [];
|
|
1140
|
+
const startedAt = remoteTurnStartedAt(turn);
|
|
1141
|
+
return [{
|
|
1142
|
+
id: turn.id,
|
|
1143
|
+
prompt: extractTurnUserPrompt(turn),
|
|
1144
|
+
startedAt,
|
|
1145
|
+
completedAt: startedAt === undefined
|
|
1146
|
+
? undefined
|
|
1147
|
+
: startedAt + Math.max(0, turn.durationMs ?? 0),
|
|
1148
|
+
}];
|
|
1149
|
+
});
|
|
1115
1150
|
return {
|
|
1116
1151
|
id: thread.id,
|
|
1117
1152
|
title: normalizeTaskTitle(thread.name) ?? normalizeTaskTitle(thread.preview),
|
|
@@ -1136,25 +1171,41 @@ function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary
|
|
|
1136
1171
|
lastTurnCompletedToolCount: toolCounts?.completed,
|
|
1137
1172
|
lastTurnFailedToolCount: toolCounts?.failed,
|
|
1138
1173
|
lastTurnRunningToolCount: toolCounts?.running,
|
|
1174
|
+
completedTurns,
|
|
1139
1175
|
};
|
|
1140
1176
|
}
|
|
1141
1177
|
|
|
1142
1178
|
function extractLastUserPrompt(turns: CodexTurnSnapshot[] | undefined): string | undefined {
|
|
1143
1179
|
for (const turn of [...(turns ?? [])].reverse()) {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1180
|
+
const prompt = extractTurnUserPrompt(turn);
|
|
1181
|
+
if (prompt) return prompt;
|
|
1182
|
+
}
|
|
1183
|
+
return undefined;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function extractTurnUserPrompt(turn: CodexTurnSnapshot): string | undefined {
|
|
1187
|
+
for (const item of [...(turn.items ?? [])].reverse()) {
|
|
1188
|
+
if (item.type !== "userMessage") continue;
|
|
1189
|
+
const text = item.content
|
|
1190
|
+
?.filter((content) => content.type === "text" && typeof content.text === "string")
|
|
1191
|
+
.map((content) => content.text!.trim())
|
|
1192
|
+
.filter(Boolean)
|
|
1193
|
+
.join("\n\n")
|
|
1194
|
+
|| item.text?.trim();
|
|
1195
|
+
if (text) return text;
|
|
1154
1196
|
}
|
|
1155
1197
|
return undefined;
|
|
1156
1198
|
}
|
|
1157
1199
|
|
|
1200
|
+
function remoteTurnStartedAt(turn: CodexTurnSnapshot): number | undefined {
|
|
1201
|
+
return remoteTimestampMs(turn.startedAt ?? undefined);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function remoteTimestampMs(value: number | undefined): number | undefined {
|
|
1205
|
+
if (value === undefined || !Number.isFinite(value)) return undefined;
|
|
1206
|
+
return value >= 10_000_000_000 ? value : value * 1_000;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1158
1209
|
function summarizeTurnTools(turn: CodexTurnSnapshot): {
|
|
1159
1210
|
total: number;
|
|
1160
1211
|
completed: number;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isMap, parseDocument } from "yaml";
|
|
4
|
+
import type { AgentExecutionDefaults } from "./schema.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_KEYS = ["modelProvider", "model", "reasoningEffort", "permissionMode"] as const;
|
|
7
|
+
|
|
8
|
+
export function writeAgentExecutionDefaults(
|
|
9
|
+
configPath: string,
|
|
10
|
+
agentName: string,
|
|
11
|
+
defaults: AgentExecutionDefaults,
|
|
12
|
+
): boolean {
|
|
13
|
+
const original = fs.readFileSync(configPath, "utf8");
|
|
14
|
+
const document = parseDocument(original);
|
|
15
|
+
if (document.errors.length > 0) {
|
|
16
|
+
throw new Error(`Could not update configuration file ${configPath}: ${document.errors[0]?.message ?? "invalid YAML"}`);
|
|
17
|
+
}
|
|
18
|
+
if (!isMap(document.contents)) {
|
|
19
|
+
throw new Error(`Configuration file must contain a YAML mapping: ${configPath}`);
|
|
20
|
+
}
|
|
21
|
+
const agent = document.getIn(["agents", agentName], true);
|
|
22
|
+
if (!isMap(agent)) {
|
|
23
|
+
throw new Error(`Cannot save defaults for an Agent that is not configured: ${agentName}`);
|
|
24
|
+
}
|
|
25
|
+
const existingDefaults = agent.get("defaults", true);
|
|
26
|
+
if (existingDefaults !== undefined && !isMap(existingDefaults)) {
|
|
27
|
+
throw new Error(`Configuration agents.${agentName}.defaults must contain a YAML mapping: ${configPath}`);
|
|
28
|
+
}
|
|
29
|
+
if (!isMap(existingDefaults)) agent.set("defaults", {});
|
|
30
|
+
const target = agent.get("defaults", true);
|
|
31
|
+
if (!isMap(target)) throw new Error(`Could not create defaults for Agent: ${agentName}`);
|
|
32
|
+
|
|
33
|
+
for (const key of DEFAULT_KEYS) {
|
|
34
|
+
const value = defaults[key];
|
|
35
|
+
if (value !== undefined) target.set(key, value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const updated = document.toString();
|
|
39
|
+
if (updated === original) return false;
|
|
40
|
+
writeFileAtomically(configPath, updated);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeFileAtomically(filePath: string, contents: string): void {
|
|
45
|
+
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
46
|
+
let temporaryFile: number | undefined;
|
|
47
|
+
try {
|
|
48
|
+
temporaryFile = fs.openSync(temporaryPath, "wx", fs.statSync(filePath).mode);
|
|
49
|
+
fs.writeFileSync(temporaryFile, contents, "utf8");
|
|
50
|
+
fs.fsyncSync(temporaryFile);
|
|
51
|
+
fs.closeSync(temporaryFile);
|
|
52
|
+
temporaryFile = undefined;
|
|
53
|
+
fs.renameSync(temporaryPath, filePath);
|
|
54
|
+
syncDirectory(path.dirname(filePath));
|
|
55
|
+
} finally {
|
|
56
|
+
if (temporaryFile !== undefined) fs.closeSync(temporaryFile);
|
|
57
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function syncDirectory(directory: string): void {
|
|
62
|
+
let handle: number | undefined;
|
|
63
|
+
try {
|
|
64
|
+
handle = fs.openSync(directory, "r");
|
|
65
|
+
fs.fsyncSync(handle);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (process.platform !== "win32") throw error;
|
|
68
|
+
} finally {
|
|
69
|
+
if (handle !== undefined) fs.closeSync(handle);
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/config/loadConfig.ts
CHANGED
|
@@ -38,6 +38,12 @@ agents:
|
|
|
38
38
|
- "--listen"
|
|
39
39
|
- "stdio://"
|
|
40
40
|
env: {}
|
|
41
|
+
# Updated automatically by the Provider, Model, Thinking, and Permission settings.
|
|
42
|
+
defaults:
|
|
43
|
+
# modelProvider: "openai"
|
|
44
|
+
# model: "gpt-5"
|
|
45
|
+
# reasoningEffort: "high"
|
|
46
|
+
permissionMode: "auto"
|
|
41
47
|
|
|
42
48
|
traex:
|
|
43
49
|
kind: "app-server"
|
|
@@ -48,6 +54,8 @@ agents:
|
|
|
48
54
|
- "--listen"
|
|
49
55
|
- "stdio://"
|
|
50
56
|
env: {}
|
|
57
|
+
defaults:
|
|
58
|
+
permissionMode: "auto"
|
|
51
59
|
|
|
52
60
|
defaults:
|
|
53
61
|
agent: "codex"
|
package/src/config/schema.ts
CHANGED
|
@@ -40,12 +40,20 @@ const agentKindSchema = z.union([
|
|
|
40
40
|
z.literal("codex").transform(() => "app-server" as const),
|
|
41
41
|
]).default("acp");
|
|
42
42
|
|
|
43
|
+
export const agentExecutionDefaultsSchema = z.object({
|
|
44
|
+
modelProvider: z.string().min(1).optional(),
|
|
45
|
+
model: z.string().min(1).optional(),
|
|
46
|
+
reasoningEffort: z.string().min(1).optional(),
|
|
47
|
+
permissionMode: z.enum(["auto", "confirm"]).optional(),
|
|
48
|
+
}).default({});
|
|
49
|
+
|
|
43
50
|
export const agentConfigSchema = z.object({
|
|
44
51
|
kind: agentKindSchema,
|
|
45
52
|
title: z.string().min(1),
|
|
46
53
|
command: z.string().min(1),
|
|
47
54
|
args: z.array(z.string()).default([]),
|
|
48
55
|
env: envRecordSchema,
|
|
56
|
+
defaults: agentExecutionDefaultsSchema,
|
|
49
57
|
});
|
|
50
58
|
|
|
51
59
|
export const appConfigSchema = z.object({
|
|
@@ -84,6 +92,10 @@ export const appConfigSchema = z.object({
|
|
|
84
92
|
}).default({ level: "info", path: DEFAULT_LOG_PATH }),
|
|
85
93
|
});
|
|
86
94
|
|
|
87
|
-
export type
|
|
95
|
+
export type AgentExecutionDefaults = z.infer<typeof agentExecutionDefaultsSchema>;
|
|
96
|
+
type ParsedAgentConfig = z.infer<typeof agentConfigSchema>;
|
|
97
|
+
export type AgentConfig = Omit<ParsedAgentConfig, "defaults"> & {
|
|
98
|
+
defaults?: AgentExecutionDefaults;
|
|
99
|
+
};
|
|
88
100
|
export type GroupNameFormatConfig = z.infer<typeof groupNameFormatSchema>;
|
|
89
101
|
export type AppConfig = z.infer<typeof appConfigSchema>;
|
|
@@ -225,7 +225,9 @@ export interface ResetHistoryCardEntry extends TaskListCardEntry {
|
|
|
225
225
|
sequence: number;
|
|
226
226
|
graphNodeLine: string;
|
|
227
227
|
graphConnectorLine?: string;
|
|
228
|
+
timestamp?: string;
|
|
228
229
|
running?: boolean;
|
|
230
|
+
resetting?: boolean;
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
export interface ResetHistoryCardView {
|
|
@@ -1328,9 +1330,11 @@ export class CardRenderer {
|
|
|
1328
1330
|
entry.graphNodeLine,
|
|
1329
1331
|
entry.graphConnectorLine,
|
|
1330
1332
|
entry.lines,
|
|
1333
|
+
entry.timestamp,
|
|
1331
1334
|
action,
|
|
1332
1335
|
entry.current === true,
|
|
1333
1336
|
entry.running === true,
|
|
1337
|
+
entry.resetting === true,
|
|
1334
1338
|
));
|
|
1335
1339
|
});
|
|
1336
1340
|
}
|
|
@@ -2415,15 +2419,8 @@ function inlineCode(value: string): string {
|
|
|
2415
2419
|
}
|
|
2416
2420
|
|
|
2417
2421
|
function formatStartupTime(value: Date): string {
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
month: "2-digit",
|
|
2421
|
-
day: "2-digit",
|
|
2422
|
-
hour: "2-digit",
|
|
2423
|
-
minute: "2-digit",
|
|
2424
|
-
second: "2-digit",
|
|
2425
|
-
hourCycle: "h23",
|
|
2426
|
-
}).format(value);
|
|
2422
|
+
const pad = (part: number) => String(part).padStart(2, "0");
|
|
2423
|
+
return `${value.getFullYear()}/${pad(value.getMonth() + 1)}/${pad(value.getDate())} ${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}`;
|
|
2427
2424
|
}
|
|
2428
2425
|
|
|
2429
2426
|
function permissionModeLabel(mode?: PermissionMode): string {
|
|
@@ -2633,16 +2630,30 @@ function resetHistoryEntryRow(
|
|
|
2633
2630
|
graphNodeLine: string,
|
|
2634
2631
|
graphConnectorLine: string | undefined,
|
|
2635
2632
|
lines: string[],
|
|
2633
|
+
timestamp: string | undefined,
|
|
2636
2634
|
action: TaskListCardAction | undefined,
|
|
2637
2635
|
current: boolean,
|
|
2638
2636
|
running: boolean,
|
|
2637
|
+
resetting: boolean,
|
|
2639
2638
|
): Record<string, unknown> {
|
|
2639
|
+
const branchConnectorLine = graphConnectorLine && /[╱╲]/u.test(graphConnectorLine)
|
|
2640
|
+
? graphConnectorLine
|
|
2641
|
+
: undefined;
|
|
2640
2642
|
const graph = [
|
|
2641
|
-
`<font color='${current ? "green" : running ? "orange" : "blue"}'>${escapeCardHtml(graphNodeLine)}</font>`,
|
|
2642
|
-
...(
|
|
2643
|
-
? [`<font color='grey'>${escapeCardHtml(
|
|
2643
|
+
`<font color='${current ? "green" : running || resetting ? "orange" : "blue"}'>${escapeCardHtml(graphNodeLine)}</font>`,
|
|
2644
|
+
...(branchConnectorLine
|
|
2645
|
+
? [`<font color='grey'>${escapeCardHtml(branchConnectorLine)}</font>`]
|
|
2644
2646
|
: []),
|
|
2645
2647
|
].join("\n");
|
|
2648
|
+
const trailingElements = current
|
|
2649
|
+
? [markdown("✅ 当前")]
|
|
2650
|
+
: resetting
|
|
2651
|
+
? [markdown("⏳ 正在 Reset")]
|
|
2652
|
+
: running
|
|
2653
|
+
? [markdown("⏳ 运行中")]
|
|
2654
|
+
: action
|
|
2655
|
+
? [taskActionElement(action)]
|
|
2656
|
+
: [];
|
|
2646
2657
|
return {
|
|
2647
2658
|
tag: "column_set",
|
|
2648
2659
|
flex_mode: "none",
|
|
@@ -2664,25 +2675,22 @@ function resetHistoryEntryRow(
|
|
|
2664
2675
|
weight: 1,
|
|
2665
2676
|
vertical_align: "top",
|
|
2666
2677
|
elements: [
|
|
2667
|
-
markdown(
|
|
2678
|
+
markdown([
|
|
2679
|
+
lines[0] ?? "",
|
|
2680
|
+
...(timestamp ? [`<font color='grey'>${escapeCardHtml(timestamp)}</font>`] : []),
|
|
2681
|
+
].join(" ")),
|
|
2668
2682
|
...lines.slice(1).map((line) => ({
|
|
2669
2683
|
...markdown(`<font color='grey'>${line}</font>`),
|
|
2670
2684
|
text_size: "notation",
|
|
2671
2685
|
})),
|
|
2672
2686
|
],
|
|
2673
2687
|
},
|
|
2674
|
-
{
|
|
2688
|
+
...(trailingElements.length > 0 ? [{
|
|
2675
2689
|
tag: "column",
|
|
2676
2690
|
width: "auto",
|
|
2677
2691
|
vertical_align: "top",
|
|
2678
|
-
elements:
|
|
2679
|
-
|
|
2680
|
-
: running
|
|
2681
|
-
? [markdown("⏳ 运行中")]
|
|
2682
|
-
: action
|
|
2683
|
-
? [taskActionElement(action)]
|
|
2684
|
-
: [],
|
|
2685
|
-
},
|
|
2692
|
+
elements: trailingElements,
|
|
2693
|
+
}] : []),
|
|
2686
2694
|
],
|
|
2687
2695
|
};
|
|
2688
2696
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Logger } from "pino";
|
|
2
|
+
import type { WSClient as LarkWsClient, WSConnectionStatus } from "@larksuiteoapi/node-sdk";
|
|
2
3
|
import type { AppConfig } from "../config/schema.js";
|
|
3
4
|
import { threadContextKey } from "./contextKey.js";
|
|
4
5
|
import { resolveFeishuBotOpenId } from "./FeishuBotIdentity.js";
|
|
@@ -9,7 +10,12 @@ import type { CardAction, ChatUpdatedEvent, FeishuEventHandler, IncomingMessage
|
|
|
9
10
|
type BotOpenIdResolver = (appId: string, appSecret: string) => Promise<string>;
|
|
10
11
|
type BotOpenIdListener = (botOpenId: string) => void;
|
|
11
12
|
|
|
13
|
+
const LARK_WS_PING_TIMEOUT_SECONDS = 60;
|
|
14
|
+
const LARK_WS_HANDSHAKE_TIMEOUT_MS = 60_000;
|
|
15
|
+
|
|
12
16
|
export class FeishuConnector {
|
|
17
|
+
private wsClient?: LarkWsClient;
|
|
18
|
+
|
|
13
19
|
constructor(
|
|
14
20
|
private readonly config: AppConfig,
|
|
15
21
|
private readonly handler: FeishuEventHandler,
|
|
@@ -39,7 +45,12 @@ export class FeishuConnector {
|
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
stop(): void {
|
|
42
|
-
|
|
48
|
+
this.wsClient?.close();
|
|
49
|
+
this.wsClient = undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getConnectionStatus(): WSConnectionStatus | undefined {
|
|
53
|
+
return this.wsClient?.getConnectionStatus();
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
private async startFeishuWs(
|
|
@@ -48,7 +59,7 @@ export class FeishuConnector {
|
|
|
48
59
|
botOpenId?: string,
|
|
49
60
|
requireMention = false,
|
|
50
61
|
): Promise<void> {
|
|
51
|
-
const lark =
|
|
62
|
+
const lark = await import("@larksuiteoapi/node-sdk");
|
|
52
63
|
const eventDispatcher = new lark.EventDispatcher({}).register({
|
|
53
64
|
"im.message.receive_v1": async (data: unknown) => {
|
|
54
65
|
const senderOpenId = getFeishuEvent(data)?.sender?.sender_id?.open_id;
|
|
@@ -117,12 +128,66 @@ export class FeishuConnector {
|
|
|
117
128
|
};
|
|
118
129
|
},
|
|
119
130
|
});
|
|
120
|
-
|
|
131
|
+
let resolveInitialConnection!: () => void;
|
|
132
|
+
let rejectInitialConnection!: (error: Error) => void;
|
|
133
|
+
let initialConnectionSettled = false;
|
|
134
|
+
const initialConnection = new Promise<void>((resolve, reject) => {
|
|
135
|
+
resolveInitialConnection = resolve;
|
|
136
|
+
rejectInitialConnection = reject;
|
|
137
|
+
});
|
|
138
|
+
let wsClient: LarkWsClient | undefined;
|
|
139
|
+
wsClient = new lark.WSClient({
|
|
121
140
|
appId,
|
|
122
141
|
appSecret,
|
|
142
|
+
handshakeTimeoutMs: LARK_WS_HANDSHAKE_TIMEOUT_MS,
|
|
143
|
+
wsConfig: {
|
|
144
|
+
pingTimeout: LARK_WS_PING_TIMEOUT_SECONDS,
|
|
145
|
+
},
|
|
146
|
+
onReady: () => {
|
|
147
|
+
if (initialConnectionSettled) return;
|
|
148
|
+
initialConnectionSettled = true;
|
|
149
|
+
resolveInitialConnection();
|
|
150
|
+
},
|
|
151
|
+
onError: (error) => {
|
|
152
|
+
this.logger.error(
|
|
153
|
+
{ error, connectionStatus: wsClient?.getConnectionStatus() },
|
|
154
|
+
"Feishu WebSocket connection failed.",
|
|
155
|
+
);
|
|
156
|
+
if (initialConnectionSettled) return;
|
|
157
|
+
initialConnectionSettled = true;
|
|
158
|
+
rejectInitialConnection(error);
|
|
159
|
+
},
|
|
160
|
+
onReconnecting: () => {
|
|
161
|
+
this.logger.warn(
|
|
162
|
+
{ connectionStatus: wsClient?.getConnectionStatus() },
|
|
163
|
+
"Feishu WebSocket connection lost; reconnecting.",
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
onReconnected: () => {
|
|
167
|
+
this.logger.info("Feishu WebSocket connection restored.");
|
|
168
|
+
},
|
|
123
169
|
});
|
|
124
|
-
|
|
125
|
-
|
|
170
|
+
this.wsClient = wsClient;
|
|
171
|
+
try {
|
|
172
|
+
await wsClient.start({ eventDispatcher });
|
|
173
|
+
const initialStatus = wsClient.getConnectionStatus();
|
|
174
|
+
if ((initialStatus.state === "idle" || initialStatus.state === "failed") && !initialConnectionSettled) {
|
|
175
|
+
throw new Error(`Feishu WebSocket failed to start (state: ${initialStatus.state}).`);
|
|
176
|
+
}
|
|
177
|
+
await initialConnection;
|
|
178
|
+
const connectedStatus = wsClient.getConnectionStatus();
|
|
179
|
+
if (connectedStatus.state !== "connected") {
|
|
180
|
+
throw new Error(`Feishu WebSocket failed to become ready (state: ${connectedStatus.state}).`);
|
|
181
|
+
}
|
|
182
|
+
this.logger.info(
|
|
183
|
+
{ connectionStatus: connectedStatus },
|
|
184
|
+
"Feishu WebSocket connector started.",
|
|
185
|
+
);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
wsClient.close({ force: true });
|
|
188
|
+
if (this.wsClient === wsClient) this.wsClient = undefined;
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
126
191
|
}
|
|
127
192
|
|
|
128
193
|
private deferCardAction(action: CardAction, receivedAt: number): void {
|
|
@@ -54,7 +54,7 @@ export interface FeishuTurnPresenterOptions {
|
|
|
54
54
|
|
|
55
55
|
const MAX_FINAL_TABLES_PER_CARD = 5;
|
|
56
56
|
const DEFAULT_ELAPSED_UPDATE_INTERVAL_MS = 3_000;
|
|
57
|
-
const
|
|
57
|
+
const FINAL_RESPONSE_BRANDING_THRESHOLD = 600;
|
|
58
58
|
|
|
59
59
|
interface TurnEntry {
|
|
60
60
|
contextKey: string;
|
|
@@ -581,7 +581,7 @@ export class FeishuTurnPresenter {
|
|
|
581
581
|
|
|
582
582
|
function brandedFinalChunks(response: string, maxLength: number): string[] {
|
|
583
583
|
const chunks = splitMarkdown(response, maxLength, MAX_FINAL_TABLES_PER_CARD);
|
|
584
|
-
if (chunks.length <= 1 && response.length
|
|
584
|
+
if (chunks.length <= 1 && response.length <= FINAL_RESPONSE_BRANDING_THRESHOLD) return chunks;
|
|
585
585
|
|
|
586
586
|
const last = chunks.pop();
|
|
587
587
|
if (last === undefined) return chunks;
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { controlEndpoint, type ControlRequest, type ControlResponse } from "./cl
|
|
|
6
6
|
import { readPackageVersion } from "./cli/packageVersion.js";
|
|
7
7
|
import { loadConfig } from "./config/loadConfig.js";
|
|
8
8
|
import { persistFeishuUserOpenIdIfMissing } from "./config/FeishuUserOpenIdStore.js";
|
|
9
|
+
import { writeAgentExecutionDefaults } from "./config/AgentExecutionDefaultsStore.js";
|
|
9
10
|
import {
|
|
10
11
|
agentBotHome,
|
|
11
12
|
defaultConfigPath,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
import { SafeRestartScheduler } from "./supervision/SafeRestartScheduler.js";
|
|
43
44
|
import type { RestartNotificationTarget } from "./supervision/SafeRestartScheduler.js";
|
|
44
45
|
import { SafeRestartNotifier } from "./supervision/SafeRestartNotifier.js";
|
|
46
|
+
import { privateRestartNotificationTarget } from "./supervision/restartNotificationTarget.js";
|
|
45
47
|
import {
|
|
46
48
|
nodeDiagnosticReportArguments,
|
|
47
49
|
prepareCrashReportDirectory,
|
|
@@ -72,9 +74,10 @@ const logger = createLogger(config);
|
|
|
72
74
|
const store = new StateStore(config.storage.sqlitePath);
|
|
73
75
|
|
|
74
76
|
const configuredConfigPath = process.env.AGENT_BOT_CONFIG?.trim();
|
|
77
|
+
const activeConfigPath = configuredConfigPath ? resolveUserPath(configuredConfigPath) : defaultConfigPath();
|
|
75
78
|
const agentEnvironmentContext: AgentEnvironmentContext = {
|
|
76
79
|
profilePath: agentBotHome(),
|
|
77
|
-
configPath:
|
|
80
|
+
configPath: activeConfigPath,
|
|
78
81
|
larkAppId: config.feishu.appId,
|
|
79
82
|
larkUserOpenId: config.feishu.userOpenId,
|
|
80
83
|
};
|
|
@@ -156,6 +159,10 @@ const controller = new ProxySessionController(config, store, runtimes, outbound,
|
|
|
156
159
|
"Stored the default Lark user Open ID from the first private chat message.",
|
|
157
160
|
);
|
|
158
161
|
},
|
|
162
|
+
persistAgentExecutionDefaults: (agentName, defaults) => {
|
|
163
|
+
const changed = writeAgentExecutionDefaults(activeConfigPath, agentName, defaults);
|
|
164
|
+
logger.info({ agentName, changed }, "Stored Agent execution defaults.");
|
|
165
|
+
},
|
|
159
166
|
});
|
|
160
167
|
|
|
161
168
|
if (feishuOutbound) {
|
|
@@ -362,7 +369,7 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
362
369
|
if (pendingSelfUpdatePlanPath) {
|
|
363
370
|
return { ok: false, message: "An Agent Bot update is already pending." };
|
|
364
371
|
}
|
|
365
|
-
const notificationTarget =
|
|
372
|
+
const notificationTarget = inferServerRestartTarget(request.notificationSessionId);
|
|
366
373
|
if (request.mode === "safe") {
|
|
367
374
|
const newlyScheduled = safeRestart.schedule(request.reason, notificationTarget);
|
|
368
375
|
return {
|
|
@@ -571,6 +578,14 @@ function inferControlRestartTarget(notificationSessionId?: string): RestartNotif
|
|
|
571
578
|
return session ? restartNotificationTargetForSession(session) : undefined;
|
|
572
579
|
}
|
|
573
580
|
|
|
581
|
+
function inferServerRestartTarget(notificationSessionId?: string): RestartNotificationTarget {
|
|
582
|
+
if (notificationSessionId?.trim()) {
|
|
583
|
+
const target = inferControlRestartTarget(notificationSessionId);
|
|
584
|
+
if (target) return target;
|
|
585
|
+
}
|
|
586
|
+
return privateRestartNotificationTarget(config.feishu.userOpenId);
|
|
587
|
+
}
|
|
588
|
+
|
|
574
589
|
function restartNotificationTargetForSession(
|
|
575
590
|
session: { localSessionId: string; contextKey: string },
|
|
576
591
|
): RestartNotificationTarget {
|