@keyou007/agent-bot 0.1.15-alpha.0 → 0.1.15-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 +10 -0
- package/README.md +23 -20
- package/README.zh.md +23 -20
- package/config.example.yaml +1 -0
- package/dist/cli/FeishuAppConfiguration.js +42 -3
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/feishu/CardRenderer.js +51 -8
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +17 -5
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +58 -6
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +33 -4
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +7 -6
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/FinalResponseBranding.js +15 -0
- package/dist/feishu/FinalResponseBranding.js.map +1 -0
- package/dist/logging/DailyLogStream.js +73 -0
- package/dist/logging/DailyLogStream.js.map +1 -0
- package/dist/logging/logger.js +2 -1
- package/dist/logging/logger.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +192 -36
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +126 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +11 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SupervisorDiagnostics.js +37 -21
- package/dist/supervision/SupervisorDiagnostics.js.map +1 -1
- package/dist/supervisor.js +11 -4
- package/dist/supervisor.js.map +1 -1
- package/dist/utils/executeShellCommand.js +83 -16
- package/dist/utils/executeShellCommand.js.map +1 -1
- package/docs/technical-reference.md +9 -8
- package/docs/technical-reference.zh.md +9 -8
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/cli/FeishuAppConfiguration.ts +76 -3
- package/src/cli.ts +6 -1
- package/src/feishu/CardRenderer.ts +75 -9
- package/src/feishu/FeishuConnector.ts +28 -6
- package/src/feishu/FeishuMarkdown.ts +64 -5
- package/src/feishu/FeishuMessageClient.ts +41 -12
- package/src/feishu/FeishuTurnPresenter.ts +9 -6
- package/src/feishu/FinalResponseBranding.ts +18 -0
- package/src/logging/DailyLogStream.ts +88 -0
- package/src/logging/logger.ts +2 -1
- package/src/proxy/ProxySessionController.ts +208 -35
- package/src/state/StateStore.ts +180 -0
- package/src/state/migrations.ts +11 -0
- package/src/supervision/SupervisorDiagnostics.ts +41 -25
- package/src/supervisor.ts +10 -4
- package/src/utils/executeShellCommand.ts +103 -15
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { AppConfig } from "../config/schema.js";
|
|
4
|
+
import { DailyLogStream, dailyLogPath } from "../logging/DailyLogStream.js";
|
|
4
5
|
|
|
5
6
|
const MAX_LOG_BYTES = 10 * 1024 * 1024;
|
|
6
7
|
const LOG_BACKUP_COUNT = 3;
|
|
@@ -42,8 +43,12 @@ type ExtendedProcessReport = typeof process.report & {
|
|
|
42
43
|
|
|
43
44
|
export class SupervisorDiagnostics {
|
|
44
45
|
readonly paths: SupervisorDiagnosticsPaths;
|
|
46
|
+
private workerStderrStream?: DailyLogStream;
|
|
45
47
|
|
|
46
|
-
constructor(
|
|
48
|
+
constructor(
|
|
49
|
+
config: Pick<AppConfig, "logging" | "storage">,
|
|
50
|
+
private readonly clock: () => Date = () => new Date(),
|
|
51
|
+
) {
|
|
47
52
|
this.paths = resolveSupervisorDiagnosticsPaths(config);
|
|
48
53
|
}
|
|
49
54
|
|
|
@@ -51,13 +56,13 @@ export class SupervisorDiagnostics {
|
|
|
51
56
|
try {
|
|
52
57
|
ensurePrivateDirectory(path.dirname(this.paths.supervisorLogPath));
|
|
53
58
|
ensurePrivateDirectory(this.paths.crashReportDirectory);
|
|
54
|
-
rotateLogFile(this.
|
|
55
|
-
rotateLogFile(this.
|
|
59
|
+
rotateLogFile(this.currentSupervisorLogPath());
|
|
60
|
+
rotateLogFile(this.currentWorkerStderrPath());
|
|
56
61
|
this.configureCurrentProcessReports();
|
|
57
62
|
this.writeEvent("diagnostics_initialized", {
|
|
58
63
|
nodeVersion: process.versions.node,
|
|
59
|
-
supervisorLogPath: this.
|
|
60
|
-
workerStderrPath: this.
|
|
64
|
+
supervisorLogPath: this.currentSupervisorLogPath(),
|
|
65
|
+
workerStderrPath: this.currentWorkerStderrPath(),
|
|
61
66
|
crashReportDirectory: this.paths.crashReportDirectory,
|
|
62
67
|
});
|
|
63
68
|
} catch (error) {
|
|
@@ -73,40 +78,51 @@ export class SupervisorDiagnostics {
|
|
|
73
78
|
...data,
|
|
74
79
|
})}\n`;
|
|
75
80
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
const logPath = this.currentSupervisorLogPath();
|
|
82
|
+
rotateLogFile(logPath);
|
|
83
|
+
fs.appendFileSync(logPath, line, { encoding: "utf8", mode: 0o600 });
|
|
84
|
+
restrictFilePermissions(logPath);
|
|
78
85
|
} catch (error) {
|
|
79
86
|
writeFallbackDiagnostic(event, error);
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
openWorkerStderr():
|
|
90
|
+
openWorkerStderr(): DailyLogStream | "ignore" {
|
|
84
91
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
const stream = new DailyLogStream(this.paths.workerStderrPath, {
|
|
93
|
+
clock: this.clock,
|
|
94
|
+
mode: 0o600,
|
|
95
|
+
beforeOpen: (filePath) => rotateLogFile(filePath),
|
|
96
|
+
});
|
|
97
|
+
stream.on("error", (error) => {
|
|
98
|
+
this.writeEvent("worker_stderr_write_failed", { error: errorMessage(error) });
|
|
99
|
+
});
|
|
100
|
+
stream.write(`${JSON.stringify({ event: "worker_started", time: this.clock().toISOString() })}\n`);
|
|
101
|
+
this.workerStderrStream = stream;
|
|
102
|
+
return stream;
|
|
93
103
|
} catch (error) {
|
|
94
104
|
this.writeEvent("worker_stderr_open_failed", { error: errorMessage(error) });
|
|
95
105
|
return "ignore";
|
|
96
106
|
}
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
closeWorkerStderr(target:
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
closeWorkerStderr(target: DailyLogStream | "ignore"): Promise<void> {
|
|
110
|
+
if (target === "ignore") return Promise.resolve();
|
|
111
|
+
return new Promise((resolve) => target.end(resolve));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
currentSupervisorLogPath(date = this.clock()): string {
|
|
115
|
+
return dailyLogPath(this.paths.supervisorLogPath, date);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
currentWorkerStderrPath(date = this.clock()): string {
|
|
119
|
+
return dailyLogPath(this.paths.workerStderrPath, date);
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
recordCrash(context: WorkerCrashContext): WorkerCrashRecord | undefined {
|
|
109
123
|
try {
|
|
124
|
+
const exitedAt = new Date(context.exitedAt);
|
|
125
|
+
const crashDate = Number.isNaN(exitedAt.getTime()) ? this.clock() : exitedAt;
|
|
110
126
|
const record: WorkerCrashRecord = {
|
|
111
127
|
version: 1,
|
|
112
128
|
...context,
|
|
@@ -114,8 +130,8 @@ export class SupervisorDiagnostics {
|
|
|
114
130
|
nodeVersion: process.versions.node,
|
|
115
131
|
executablePath: process.execPath,
|
|
116
132
|
cwd: process.cwd(),
|
|
117
|
-
supervisorLogPath: this.
|
|
118
|
-
workerStderrPath: this.
|
|
133
|
+
supervisorLogPath: this.currentSupervisorLogPath(crashDate),
|
|
134
|
+
workerStderrPath: this.workerStderrStream?.filePath ?? this.currentWorkerStderrPath(crashDate),
|
|
119
135
|
crashReportDirectory: this.paths.crashReportDirectory,
|
|
120
136
|
diagnosticReports: findDiagnosticReports(
|
|
121
137
|
this.paths.crashReportDirectory,
|
package/src/supervisor.ts
CHANGED
|
@@ -70,18 +70,24 @@ async function startChild(): Promise<void> {
|
|
|
70
70
|
AGENT_BOT_SUPERVISED: "1",
|
|
71
71
|
AGENT_BOT_RESTART_REASON: restartReason,
|
|
72
72
|
}, restartNotificationTargets),
|
|
73
|
-
stdio: ["ignore", "ignore",
|
|
73
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
74
74
|
windowsHide: true,
|
|
75
75
|
});
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
if (workerStderr === "ignore") {
|
|
77
|
+
child.stderr?.resume();
|
|
78
|
+
} else {
|
|
79
|
+
child.stderr?.pipe(workerStderr);
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
void diagnostics.closeWorkerStderr(workerStderr);
|
|
83
|
+
throw error;
|
|
78
84
|
}
|
|
79
85
|
const workerPid = child.pid;
|
|
80
86
|
writeSupervisorLog("started", {
|
|
81
87
|
pid: child.pid,
|
|
82
88
|
restartReason,
|
|
83
89
|
restartNotificationTargets,
|
|
84
|
-
workerStderrPath: diagnostics.
|
|
90
|
+
workerStderrPath: diagnostics.currentWorkerStderrPath(),
|
|
85
91
|
crashReportDirectory: diagnostics.paths.crashReportDirectory,
|
|
86
92
|
});
|
|
87
93
|
|
|
@@ -11,10 +11,16 @@ export interface ShellCommandResult {
|
|
|
11
11
|
outputTruncated: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export type ShellCommandOutputSnapshot = Pick<
|
|
15
|
+
ShellCommandResult,
|
|
16
|
+
"stdout" | "stderr" | "outputTruncated"
|
|
17
|
+
>;
|
|
18
|
+
|
|
14
19
|
export interface ShellCommandOptions {
|
|
15
20
|
timeoutMs?: number;
|
|
16
21
|
maxOutputBytes?: number;
|
|
17
22
|
platform?: NodeJS.Platform;
|
|
23
|
+
onOutput?: (snapshot: ShellCommandOutputSnapshot) => void;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
export function executeShellCommand(
|
|
@@ -40,8 +46,16 @@ export function executeShellCommand(
|
|
|
40
46
|
child.kill();
|
|
41
47
|
}, timeoutMs);
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
const appendOutput = (stream: "stdout" | "stderr", chunk: Buffer): void => {
|
|
50
|
+
output.append(stream, chunk);
|
|
51
|
+
try {
|
|
52
|
+
options.onOutput?.(output.result());
|
|
53
|
+
} catch {
|
|
54
|
+
// Output presentation must never interrupt the child process.
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
child.stdout.on("data", (chunk: Buffer) => appendOutput("stdout", chunk));
|
|
58
|
+
child.stderr.on("data", (chunk: Buffer) => appendOutput("stderr", chunk));
|
|
45
59
|
child.once("error", (error) => {
|
|
46
60
|
clearTimeout(timeout);
|
|
47
61
|
reject(error);
|
|
@@ -68,30 +82,104 @@ function prepareShellCommand(command: string, platform: NodeJS.Platform): { comm
|
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
class BoundedOutput {
|
|
71
|
-
private
|
|
72
|
-
private readonly stderr:
|
|
73
|
-
private
|
|
85
|
+
private retained: OutputChunk[] = [];
|
|
86
|
+
private readonly seenBytes = { stdout: 0, stderr: 0 };
|
|
87
|
+
private readonly headBytes: number;
|
|
88
|
+
private readonly tailBytes: number;
|
|
74
89
|
private truncated = false;
|
|
75
90
|
|
|
76
|
-
constructor(
|
|
91
|
+
constructor(maxBytes: number) {
|
|
92
|
+
const normalizedMaxBytes = Math.max(1, Math.floor(maxBytes));
|
|
93
|
+
this.headBytes = Math.ceil(normalizedMaxBytes / 3);
|
|
94
|
+
this.tailBytes = normalizedMaxBytes - this.headBytes;
|
|
95
|
+
}
|
|
77
96
|
|
|
78
97
|
append(stream: "stdout" | "stderr", chunk: Buffer): void {
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
if (chunk.length === 0) return;
|
|
99
|
+
this.seenBytes[stream] += chunk.length;
|
|
100
|
+
|
|
101
|
+
if (!this.truncated) {
|
|
102
|
+
const next = [...this.retained, { stream, data: chunk }];
|
|
103
|
+
if (outputChunksLength(next) <= this.headBytes + this.tailBytes) {
|
|
104
|
+
this.retained = next;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
81
107
|
this.truncated = true;
|
|
108
|
+
this.retained = [
|
|
109
|
+
...takeOutputChunkHead(next, this.headBytes),
|
|
110
|
+
...takeOutputChunkTail(next, this.tailBytes),
|
|
111
|
+
];
|
|
82
112
|
return;
|
|
83
113
|
}
|
|
84
|
-
|
|
85
|
-
this
|
|
86
|
-
this.
|
|
87
|
-
|
|
114
|
+
|
|
115
|
+
const head = takeOutputChunkHead(this.retained, this.headBytes);
|
|
116
|
+
const previousTail = takeOutputChunkTail(this.retained, this.tailBytes);
|
|
117
|
+
const tail = takeOutputChunkTail([...previousTail, { stream, data: chunk }], this.tailBytes);
|
|
118
|
+
this.retained = [...head, ...tail];
|
|
88
119
|
}
|
|
89
120
|
|
|
90
|
-
result():
|
|
121
|
+
result(): ShellCommandOutputSnapshot {
|
|
91
122
|
return {
|
|
92
|
-
stdout:
|
|
93
|
-
stderr:
|
|
123
|
+
stdout: this.streamResult("stdout"),
|
|
124
|
+
stderr: this.streamResult("stderr"),
|
|
94
125
|
outputTruncated: this.truncated,
|
|
95
126
|
};
|
|
96
127
|
}
|
|
128
|
+
|
|
129
|
+
private streamResult(stream: "stdout" | "stderr"): string {
|
|
130
|
+
if (!this.truncated) {
|
|
131
|
+
return Buffer.concat(
|
|
132
|
+
this.retained.filter((chunk) => chunk.stream === stream).map((chunk) => chunk.data),
|
|
133
|
+
).toString("utf8");
|
|
134
|
+
}
|
|
135
|
+
const head = takeOutputChunkHead(this.retained, this.headBytes)
|
|
136
|
+
.filter((chunk) => chunk.stream === stream);
|
|
137
|
+
const tail = takeOutputChunkTail(this.retained, this.tailBytes)
|
|
138
|
+
.filter((chunk) => chunk.stream === stream);
|
|
139
|
+
|
|
140
|
+
const retainedBytes = outputChunksLength([...head, ...tail]);
|
|
141
|
+
const marker = this.seenBytes[stream] > retainedBytes
|
|
142
|
+
? Buffer.from("\n... output truncated ...\n", "utf8")
|
|
143
|
+
: Buffer.alloc(0);
|
|
144
|
+
return Buffer.concat([
|
|
145
|
+
...head.map((chunk) => chunk.data),
|
|
146
|
+
marker,
|
|
147
|
+
...tail.map((chunk) => chunk.data),
|
|
148
|
+
]).toString("utf8");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface OutputChunk {
|
|
153
|
+
stream: "stdout" | "stderr";
|
|
154
|
+
data: Buffer;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function outputChunksLength(chunks: OutputChunk[]): number {
|
|
158
|
+
return chunks.reduce((total, chunk) => total + chunk.data.length, 0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function takeOutputChunkHead(chunks: OutputChunk[], maxBytes: number): OutputChunk[] {
|
|
162
|
+
const retained: OutputChunk[] = [];
|
|
163
|
+
let remaining = maxBytes;
|
|
164
|
+
for (const chunk of chunks) {
|
|
165
|
+
if (remaining <= 0) break;
|
|
166
|
+
const data = chunk.data.length <= remaining ? chunk.data : chunk.data.subarray(0, remaining);
|
|
167
|
+
retained.push({ stream: chunk.stream, data });
|
|
168
|
+
remaining -= data.length;
|
|
169
|
+
}
|
|
170
|
+
return retained;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function takeOutputChunkTail(chunks: OutputChunk[], maxBytes: number): OutputChunk[] {
|
|
174
|
+
const retained: OutputChunk[] = [];
|
|
175
|
+
let remaining = maxBytes;
|
|
176
|
+
for (let index = chunks.length - 1; index >= 0 && remaining > 0; index -= 1) {
|
|
177
|
+
const chunk = chunks[index]!;
|
|
178
|
+
const data = chunk.data.length <= remaining
|
|
179
|
+
? chunk.data
|
|
180
|
+
: chunk.data.subarray(chunk.data.length - remaining);
|
|
181
|
+
retained.unshift({ stream: chunk.stream, data });
|
|
182
|
+
remaining -= data.length;
|
|
183
|
+
}
|
|
184
|
+
return retained;
|
|
97
185
|
}
|