@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +23 -20
  3. package/README.zh.md +23 -20
  4. package/config.example.yaml +1 -0
  5. package/dist/cli/FeishuAppConfiguration.js +42 -3
  6. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  7. package/dist/cli.js +4 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/feishu/CardRenderer.js +51 -8
  10. package/dist/feishu/CardRenderer.js.map +1 -1
  11. package/dist/feishu/FeishuConnector.js +17 -5
  12. package/dist/feishu/FeishuConnector.js.map +1 -1
  13. package/dist/feishu/FeishuMarkdown.js +58 -6
  14. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  15. package/dist/feishu/FeishuMessageClient.js +33 -4
  16. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  17. package/dist/feishu/FeishuTurnPresenter.js +7 -6
  18. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  19. package/dist/feishu/FinalResponseBranding.js +15 -0
  20. package/dist/feishu/FinalResponseBranding.js.map +1 -0
  21. package/dist/logging/DailyLogStream.js +73 -0
  22. package/dist/logging/DailyLogStream.js.map +1 -0
  23. package/dist/logging/logger.js +2 -1
  24. package/dist/logging/logger.js.map +1 -1
  25. package/dist/proxy/ProxySessionController.js +192 -36
  26. package/dist/proxy/ProxySessionController.js.map +1 -1
  27. package/dist/state/StateStore.js +126 -0
  28. package/dist/state/StateStore.js.map +1 -1
  29. package/dist/state/migrations.js +11 -0
  30. package/dist/state/migrations.js.map +1 -1
  31. package/dist/supervision/SupervisorDiagnostics.js +37 -21
  32. package/dist/supervision/SupervisorDiagnostics.js.map +1 -1
  33. package/dist/supervisor.js +11 -4
  34. package/dist/supervisor.js.map +1 -1
  35. package/dist/utils/executeShellCommand.js +83 -16
  36. package/dist/utils/executeShellCommand.js.map +1 -1
  37. package/docs/technical-reference.md +9 -8
  38. package/docs/technical-reference.zh.md +9 -8
  39. package/npm-shrinkwrap.json +2 -2
  40. package/package.json +1 -1
  41. package/src/cli/FeishuAppConfiguration.ts +76 -3
  42. package/src/cli.ts +6 -1
  43. package/src/feishu/CardRenderer.ts +75 -9
  44. package/src/feishu/FeishuConnector.ts +28 -6
  45. package/src/feishu/FeishuMarkdown.ts +64 -5
  46. package/src/feishu/FeishuMessageClient.ts +41 -12
  47. package/src/feishu/FeishuTurnPresenter.ts +9 -6
  48. package/src/feishu/FinalResponseBranding.ts +18 -0
  49. package/src/logging/DailyLogStream.ts +88 -0
  50. package/src/logging/logger.ts +2 -1
  51. package/src/proxy/ProxySessionController.ts +208 -35
  52. package/src/state/StateStore.ts +180 -0
  53. package/src/state/migrations.ts +11 -0
  54. package/src/supervision/SupervisorDiagnostics.ts +41 -25
  55. package/src/supervisor.ts +10 -4
  56. 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(config: Pick<AppConfig, "logging" | "storage">) {
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.paths.supervisorLogPath);
55
- rotateLogFile(this.paths.workerStderrPath);
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.paths.supervisorLogPath,
60
- workerStderrPath: this.paths.workerStderrPath,
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
- fs.appendFileSync(this.paths.supervisorLogPath, line, { encoding: "utf8", mode: 0o600 });
77
- restrictFilePermissions(this.paths.supervisorLogPath);
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(): number | "ignore" {
90
+ openWorkerStderr(): DailyLogStream | "ignore" {
84
91
  try {
85
- rotateLogFile(this.paths.workerStderrPath);
86
- fs.appendFileSync(
87
- this.paths.workerStderrPath,
88
- `${JSON.stringify({ event: "worker_started", time: new Date().toISOString() })}\n`,
89
- { encoding: "utf8", mode: 0o600 },
90
- );
91
- restrictFilePermissions(this.paths.workerStderrPath);
92
- return fs.openSync(this.paths.workerStderrPath, "a", 0o600);
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: number | "ignore"): void {
100
- if (typeof target !== "number") return;
101
- try {
102
- fs.closeSync(target);
103
- } catch (error) {
104
- this.writeEvent("worker_stderr_close_failed", { error: errorMessage(error) });
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.paths.supervisorLogPath,
118
- workerStderrPath: this.paths.workerStderrPath,
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", workerStderr],
73
+ stdio: ["ignore", "ignore", "pipe"],
74
74
  windowsHide: true,
75
75
  });
76
- } finally {
77
- diagnostics.closeWorkerStderr(workerStderr);
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.paths.workerStderrPath,
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
- child.stdout.on("data", (chunk: Buffer) => output.append("stdout", chunk));
44
- child.stderr.on("data", (chunk: Buffer) => output.append("stderr", chunk));
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 readonly stdout: Buffer[] = [];
72
- private readonly stderr: Buffer[] = [];
73
- private retainedBytes = 0;
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(private readonly maxBytes: number) {}
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
- const remaining = Math.max(0, this.maxBytes - this.retainedBytes);
80
- if (remaining === 0) {
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
- const retained = chunk.length <= remaining ? chunk : chunk.subarray(0, remaining);
85
- this[stream].push(retained);
86
- this.retainedBytes += retained.length;
87
- if (retained.length < chunk.length) this.truncated = true;
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(): Pick<ShellCommandResult, "stdout" | "stderr" | "outputTruncated"> {
121
+ result(): ShellCommandOutputSnapshot {
91
122
  return {
92
- stdout: Buffer.concat(this.stdout).toString("utf8"),
93
- stderr: Buffer.concat(this.stderr).toString("utf8"),
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
  }