@keyou007/agent-bot 0.1.18 → 0.1.19-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 +11 -0
- package/README.md +3 -0
- package/README.zh.md +3 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +2 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +48 -0
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +43 -5
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ThreadWriterProcess.js +344 -0
- package/dist/codex/ThreadWriterProcess.js.map +1 -0
- package/dist/commands/CommandRouter.js +5 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/console/ConsoleTurnPresenter.js +1 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +166 -0
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +13 -3
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/MergedForwardMessage.js +6 -1
- package/dist/feishu/MergedForwardMessage.js.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/logging/errorLogValue.js +18 -0
- package/dist/logging/errorLogValue.js.map +1 -0
- package/dist/logging/logger.js +4 -0
- package/dist/logging/logger.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +2 -2
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +778 -19
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +10 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +1 -1
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/docs/technical-reference.md +1 -1
- package/docs/technical-reference.zh.md +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +3 -0
- package/src/cli/controlProtocol.ts +8 -0
- package/src/cli/help.ts +2 -0
- package/src/cli.ts +33 -0
- package/src/codex/CodexProcessManager.ts +43 -0
- package/src/codex/CodexRuntime.ts +41 -4
- package/src/codex/ThreadWriterProcess.ts +385 -0
- package/src/commands/CommandRouter.ts +4 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/console/ConsoleTurnPresenter.ts +2 -1
- package/src/feishu/CardRenderer.ts +205 -0
- package/src/feishu/FeishuTurnPresenter.ts +16 -3
- package/src/feishu/MergedForwardMessage.ts +5 -1
- package/src/index.ts +4 -2
- package/src/logging/errorLogValue.ts +26 -0
- package/src/logging/logger.ts +4 -0
- package/src/presentation/OutboundRouter.ts +15 -3
- package/src/proxy/ProxySessionController.ts +1003 -30
- package/src/runtime/types.ts +7 -0
- package/src/state/StateStore.ts +11 -0
- package/src/supervision/SafeRestartScheduler.ts +1 -1
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { access, readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const PROCESS_INSPECTION_TIMEOUT_MS = 10_000;
|
|
6
|
+
const MAX_PROCESS_OUTPUT_BYTES = 1024 * 1024;
|
|
7
|
+
const LOCK_INSPECTION_CONCURRENCY = 4;
|
|
8
|
+
const THREAD_WRITER_LOCK_FILE_PATTERN = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.lock$/iu;
|
|
9
|
+
|
|
10
|
+
export interface ThreadWriterProcess {
|
|
11
|
+
writerPid: number;
|
|
12
|
+
writerProcessName: string;
|
|
13
|
+
writerStartedAt?: string;
|
|
14
|
+
applicationPid: number;
|
|
15
|
+
applicationProcessName: string;
|
|
16
|
+
applicationStartedAt?: string;
|
|
17
|
+
displayName: string;
|
|
18
|
+
canClose: boolean;
|
|
19
|
+
commandLine?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ThreadWriterProcessController {
|
|
23
|
+
inspect(lockPath: string): Promise<ThreadWriterProcess[]>;
|
|
24
|
+
inspectApplicationThreadIds?(lockPath: string, owner: ThreadWriterProcess): Promise<string[]>;
|
|
25
|
+
close(process: ThreadWriterProcess, force: boolean): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class SystemThreadWriterProcessController implements ThreadWriterProcessController {
|
|
29
|
+
async inspect(lockPath: string): Promise<ThreadWriterProcess[]> {
|
|
30
|
+
try {
|
|
31
|
+
await access(lockPath);
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return process.platform === "win32"
|
|
36
|
+
? inspectWindowsLockOwners(lockPath)
|
|
37
|
+
: inspectPosixLockOwners(lockPath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async inspectApplicationThreadIds(lockPath: string, owner: ThreadWriterProcess): Promise<string[]> {
|
|
41
|
+
if (!owner.applicationStartedAt) return [];
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = await readdir(path.dirname(lockPath), { withFileTypes: true });
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const candidates = entries.flatMap((entry) => {
|
|
49
|
+
if (!entry.isFile()) return [];
|
|
50
|
+
const match = THREAD_WRITER_LOCK_FILE_PATTERN.exec(entry.name);
|
|
51
|
+
return match?.[1] ? [{ threadId: match[1], lockPath: path.join(path.dirname(lockPath), entry.name) }] : [];
|
|
52
|
+
});
|
|
53
|
+
const ownedThreadIds: string[] = [];
|
|
54
|
+
for (let index = 0; index < candidates.length; index += LOCK_INSPECTION_CONCURRENCY) {
|
|
55
|
+
const batch = candidates.slice(index, index + LOCK_INSPECTION_CONCURRENCY);
|
|
56
|
+
const results = await Promise.all(batch.map(async (candidate) => {
|
|
57
|
+
const owners = samePath(candidate.lockPath, lockPath)
|
|
58
|
+
? [owner]
|
|
59
|
+
: await this.inspect(candidate.lockPath);
|
|
60
|
+
return owners.some((candidateOwner) => sameApplication(candidateOwner, owner))
|
|
61
|
+
? candidate.threadId
|
|
62
|
+
: undefined;
|
|
63
|
+
}));
|
|
64
|
+
ownedThreadIds.push(...results.filter((threadId): threadId is string => Boolean(threadId)));
|
|
65
|
+
}
|
|
66
|
+
return ownedThreadIds;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async close(owner: ThreadWriterProcess, force: boolean): Promise<void> {
|
|
70
|
+
const targetPid = owner.applicationPid;
|
|
71
|
+
if (!owner.canClose) throw new Error("The writer process is not recognized as a closable Agent application.");
|
|
72
|
+
if (!owner.applicationStartedAt) throw new Error("The writer process does not have a stable start-time fingerprint.");
|
|
73
|
+
if (!Number.isSafeInteger(targetPid) || targetPid <= 4 || targetPid === process.pid) {
|
|
74
|
+
throw new Error(`Refusing to close unsafe process PID ${targetPid}.`);
|
|
75
|
+
}
|
|
76
|
+
if (process.platform === "win32") {
|
|
77
|
+
await executeWindowsPowerShell(WINDOWS_CLOSE_PROCESS_SCRIPT, {
|
|
78
|
+
AGENT_BOT_CLOSE_PID: String(targetPid),
|
|
79
|
+
AGENT_BOT_CLOSE_STARTED_AT: owner.applicationStartedAt,
|
|
80
|
+
AGENT_BOT_FORCE_CLOSE: force ? "true" : "false",
|
|
81
|
+
AGENT_BOT_WRITER_PID: String(owner.writerPid),
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const current = await readPosixProcess(targetPid);
|
|
86
|
+
if (!current || current.startedAt !== owner.applicationStartedAt) {
|
|
87
|
+
throw new Error(`Process PID ${targetPid} no longer matches the inspected writer application.`);
|
|
88
|
+
}
|
|
89
|
+
process.kill(targetPid, force ? "SIGKILL" : "SIGTERM");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function threadWriterLockPath(codexHome: string, threadId: string): string | undefined {
|
|
94
|
+
if (!THREAD_WRITER_LOCK_FILE_PATTERN.test(`${threadId}.lock`)) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return path.join(codexHome, "thread-writer-locks", `${threadId}.lock`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function parseWindowsLockOwners(value: string): ThreadWriterProcess[] {
|
|
101
|
+
const trimmed = value.trim();
|
|
102
|
+
if (!trimmed) return [];
|
|
103
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
104
|
+
const entries = Array.isArray(parsed) ? parsed : [parsed];
|
|
105
|
+
return entries.flatMap((entry) => {
|
|
106
|
+
if (!entry || typeof entry !== "object") return [];
|
|
107
|
+
const record = entry as Record<string, unknown>;
|
|
108
|
+
const writerPid = positiveInteger(record.writerPid);
|
|
109
|
+
const applicationPid = positiveInteger(record.applicationPid);
|
|
110
|
+
const writerProcessName = stringValue(record.writerProcessName);
|
|
111
|
+
const applicationProcessName = stringValue(record.applicationProcessName);
|
|
112
|
+
if (!writerPid || !applicationPid || !writerProcessName || !applicationProcessName) return [];
|
|
113
|
+
return [{
|
|
114
|
+
writerPid,
|
|
115
|
+
writerProcessName,
|
|
116
|
+
writerStartedAt: stringValue(record.writerStartedAt),
|
|
117
|
+
applicationPid,
|
|
118
|
+
applicationProcessName,
|
|
119
|
+
applicationStartedAt: stringValue(record.applicationStartedAt),
|
|
120
|
+
displayName: stringValue(record.displayName) ?? applicationProcessName,
|
|
121
|
+
canClose: record.canClose === true,
|
|
122
|
+
commandLine: stringValue(record.commandLine),
|
|
123
|
+
}];
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function inspectWindowsLockOwners(lockPath: string): Promise<ThreadWriterProcess[]> {
|
|
128
|
+
const output = await executeWindowsPowerShell(WINDOWS_LOCK_OWNER_SCRIPT, {
|
|
129
|
+
AGENT_BOT_THREAD_LOCK_PATH: lockPath,
|
|
130
|
+
});
|
|
131
|
+
return parseWindowsLockOwners(output);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function inspectPosixLockOwners(lockPath: string): Promise<ThreadWriterProcess[]> {
|
|
135
|
+
const pids = await posixLockOwnerPids(lockPath);
|
|
136
|
+
const owners = await Promise.all(pids.map((pid) => inspectPosixProcess(pid)));
|
|
137
|
+
return owners.filter((owner): owner is ThreadWriterProcess => owner !== undefined);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function posixLockOwnerPids(lockPath: string): Promise<number[]> {
|
|
141
|
+
const lsof = await tryExecuteFile("lsof", ["-t", "--", lockPath]);
|
|
142
|
+
const raw = lsof.ok ? lsof.stdout : (await tryExecuteFile("fuser", [lockPath])).stdout;
|
|
143
|
+
return [...new Set(raw.match(/\d+/gu)?.map(Number).filter((pid) => Number.isSafeInteger(pid) && pid > 0) ?? [])];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function inspectPosixProcess(writerPid: number): Promise<ThreadWriterProcess | undefined> {
|
|
147
|
+
const writer = await readPosixProcess(writerPid);
|
|
148
|
+
if (!writer) return undefined;
|
|
149
|
+
let application = writer;
|
|
150
|
+
let cursor = writer;
|
|
151
|
+
for (let depth = 0; depth < 8 && cursor.parentPid > 1; depth += 1) {
|
|
152
|
+
const parent = await readPosixProcess(cursor.parentPid);
|
|
153
|
+
if (!parent) break;
|
|
154
|
+
if (/\/(?:Codex|ChatGPT)\.app\//iu.test(parent.commandLine)) {
|
|
155
|
+
application = parent;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
cursor = parent;
|
|
159
|
+
}
|
|
160
|
+
const canClose = isRecognizedWriter(writer.name, writer.commandLine);
|
|
161
|
+
return {
|
|
162
|
+
writerPid,
|
|
163
|
+
writerProcessName: writer.name,
|
|
164
|
+
writerStartedAt: writer.startedAt,
|
|
165
|
+
applicationPid: application.pid,
|
|
166
|
+
applicationProcessName: application.name,
|
|
167
|
+
applicationStartedAt: application.startedAt,
|
|
168
|
+
displayName: /\/(?:Codex|ChatGPT)\.app\//iu.test(application.commandLine)
|
|
169
|
+
? "Codex Desktop"
|
|
170
|
+
: writer.name,
|
|
171
|
+
canClose,
|
|
172
|
+
commandLine: writer.commandLine,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface PosixProcessInfo {
|
|
177
|
+
pid: number;
|
|
178
|
+
parentPid: number;
|
|
179
|
+
name: string;
|
|
180
|
+
startedAt?: string;
|
|
181
|
+
commandLine: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function readPosixProcess(pid: number): Promise<PosixProcessInfo | undefined> {
|
|
185
|
+
const [parent, name, startedAt, commandLine] = await Promise.all([
|
|
186
|
+
tryExecuteFile("ps", ["-p", String(pid), "-o", "ppid="]),
|
|
187
|
+
tryExecuteFile("ps", ["-p", String(pid), "-o", "comm="]),
|
|
188
|
+
tryExecuteFile("ps", ["-p", String(pid), "-o", "lstart="]),
|
|
189
|
+
tryExecuteFile("ps", ["-p", String(pid), "-o", "command="]),
|
|
190
|
+
]);
|
|
191
|
+
const parentPid = Number(parent.stdout.trim());
|
|
192
|
+
const processName = name.stdout.trim();
|
|
193
|
+
if (!parent.ok || !processName || !Number.isSafeInteger(parentPid)) return undefined;
|
|
194
|
+
return {
|
|
195
|
+
pid,
|
|
196
|
+
parentPid,
|
|
197
|
+
name: processName,
|
|
198
|
+
startedAt: startedAt.stdout.trim() || undefined,
|
|
199
|
+
commandLine: commandLine.stdout.trim(),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isRecognizedWriter(name: string, commandLine: string): boolean {
|
|
204
|
+
const candidate = `${name} ${commandLine}`;
|
|
205
|
+
return /(?:^|[\\/\s])(?:codex|trae(?:x|cli)?)(?:\.exe|\.js)?(?:\s|$)/iu.test(candidate)
|
|
206
|
+
&& /app-server/iu.test(candidate);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function executeWindowsPowerShell(script: string, extraEnv: Record<string, string>): Promise<string> {
|
|
210
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
211
|
+
return executeFile(
|
|
212
|
+
"powershell.exe",
|
|
213
|
+
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded],
|
|
214
|
+
extraEnv,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function executeFile(command: string, args: string[], extraEnv: Record<string, string> = {}): Promise<string> {
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
execFile(command, args, {
|
|
221
|
+
env: { ...process.env, ...extraEnv },
|
|
222
|
+
timeout: PROCESS_INSPECTION_TIMEOUT_MS,
|
|
223
|
+
maxBuffer: MAX_PROCESS_OUTPUT_BYTES,
|
|
224
|
+
windowsHide: true,
|
|
225
|
+
encoding: "utf8",
|
|
226
|
+
}, (error, stdout, stderr) => {
|
|
227
|
+
if (error) {
|
|
228
|
+
reject(new Error(`${command} failed: ${stderr.trim() || error.message}`, { cause: error }));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
resolve(stdout);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function tryExecuteFile(command: string, args: string[]): Promise<{ ok: boolean; stdout: string }> {
|
|
237
|
+
return executeFile(command, args).then(
|
|
238
|
+
(stdout) => ({ ok: true, stdout }),
|
|
239
|
+
() => ({ ok: false, stdout: "" }),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function positiveInteger(value: unknown): number | undefined {
|
|
244
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function stringValue(value: unknown): string | undefined {
|
|
248
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function sameApplication(left: ThreadWriterProcess, right: ThreadWriterProcess): boolean {
|
|
252
|
+
return left.applicationPid === right.applicationPid
|
|
253
|
+
&& Boolean(left.applicationStartedAt)
|
|
254
|
+
&& left.applicationStartedAt === right.applicationStartedAt;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function samePath(left: string, right: string): boolean {
|
|
258
|
+
const normalizedLeft = path.resolve(left);
|
|
259
|
+
const normalizedRight = path.resolve(right);
|
|
260
|
+
return process.platform === "win32"
|
|
261
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
262
|
+
: normalizedLeft === normalizedRight;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const WINDOWS_CLOSE_PROCESS_SCRIPT = String.raw`
|
|
266
|
+
$ErrorActionPreference = 'Stop'
|
|
267
|
+
$targetPid = [int]$env:AGENT_BOT_CLOSE_PID
|
|
268
|
+
$writerPid = [int]$env:AGENT_BOT_WRITER_PID
|
|
269
|
+
$expectedStartedAt = $env:AGENT_BOT_CLOSE_STARTED_AT
|
|
270
|
+
$forceClose = $env:AGENT_BOT_FORCE_CLOSE -eq 'true'
|
|
271
|
+
$process = Get-Process -Id $targetPid -ErrorAction Stop
|
|
272
|
+
$actualStartedAt = $process.StartTime.ToUniversalTime().ToString('o')
|
|
273
|
+
if ($actualStartedAt -ne $expectedStartedAt) {
|
|
274
|
+
throw "Process PID $targetPid no longer matches the inspected writer application."
|
|
275
|
+
}
|
|
276
|
+
if ($forceClose) {
|
|
277
|
+
& taskkill.exe /PID $targetPid /T /F | Out-Null
|
|
278
|
+
if ($LASTEXITCODE -ne 0) { throw "taskkill failed with exit code $LASTEXITCODE" }
|
|
279
|
+
} elseif ($targetPid -ne $writerPid -and $process.MainWindowHandle -ne 0) {
|
|
280
|
+
if (-not $process.CloseMainWindow()) {
|
|
281
|
+
throw "The application did not accept a close request."
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
Stop-Process -Id $targetPid -ErrorAction Stop
|
|
285
|
+
}
|
|
286
|
+
`;
|
|
287
|
+
|
|
288
|
+
const WINDOWS_LOCK_OWNER_SCRIPT = String.raw`
|
|
289
|
+
$ErrorActionPreference = 'Stop'
|
|
290
|
+
Add-Type @'
|
|
291
|
+
using System;
|
|
292
|
+
using System.Runtime.InteropServices;
|
|
293
|
+
using System.Text;
|
|
294
|
+
public static class AgentBotRestartManager {
|
|
295
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
296
|
+
public struct RM_UNIQUE_PROCESS {
|
|
297
|
+
public int dwProcessId;
|
|
298
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
|
|
299
|
+
}
|
|
300
|
+
public enum RM_APP_TYPE { UnknownApp=0, MainWindow=1, OtherWindow=2, Service=3, Explorer=4, Console=5, Critical=1000 }
|
|
301
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
|
|
302
|
+
public struct RM_PROCESS_INFO {
|
|
303
|
+
public RM_UNIQUE_PROCESS Process;
|
|
304
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)] public string strAppName;
|
|
305
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=64)] public string strServiceShortName;
|
|
306
|
+
public RM_APP_TYPE ApplicationType;
|
|
307
|
+
public uint AppStatus;
|
|
308
|
+
public uint TSSessionId;
|
|
309
|
+
[MarshalAs(UnmanagedType.Bool)] public bool bRestartable;
|
|
310
|
+
}
|
|
311
|
+
[DllImport("rstrtmgr.dll", CharSet=CharSet.Unicode)]
|
|
312
|
+
public static extern int RmStartSession(out uint sessionHandle, int sessionFlags, StringBuilder sessionKey);
|
|
313
|
+
[DllImport("rstrtmgr.dll", CharSet=CharSet.Unicode)]
|
|
314
|
+
public static extern int RmRegisterResources(uint sessionHandle, uint fileCount, string[] files, uint applicationCount, RM_UNIQUE_PROCESS[] applications, uint serviceCount, string[] services);
|
|
315
|
+
[DllImport("rstrtmgr.dll")]
|
|
316
|
+
public static extern int RmGetList(uint sessionHandle, out uint needed, ref uint count, [In, Out] RM_PROCESS_INFO[] processes, ref uint rebootReasons);
|
|
317
|
+
[DllImport("rstrtmgr.dll")]
|
|
318
|
+
public static extern int RmEndSession(uint sessionHandle);
|
|
319
|
+
}
|
|
320
|
+
'@
|
|
321
|
+
|
|
322
|
+
function Get-AgentBotProcess([int]$processId) {
|
|
323
|
+
Get-CimInstance Win32_Process -Filter ("ProcessId=" + $processId) -ErrorAction SilentlyContinue
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function Get-AgentBotStartTime([int]$processId) {
|
|
327
|
+
try { (Get-Process -Id $processId -ErrorAction Stop).StartTime.ToUniversalTime().ToString('o') } catch { $null }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
$lockPath = $env:AGENT_BOT_THREAD_LOCK_PATH
|
|
331
|
+
$key = New-Object Text.StringBuilder 64
|
|
332
|
+
[uint32]$handle = 0
|
|
333
|
+
$result = [AgentBotRestartManager]::RmStartSession([ref]$handle, 0, $key)
|
|
334
|
+
if ($result -ne 0) { throw "RmStartSession failed: $result" }
|
|
335
|
+
try {
|
|
336
|
+
$result = [AgentBotRestartManager]::RmRegisterResources($handle, 1, [string[]]@($lockPath), 0, $null, 0, $null)
|
|
337
|
+
if ($result -ne 0) { throw "RmRegisterResources failed: $result" }
|
|
338
|
+
[uint32]$needed = 0
|
|
339
|
+
[uint32]$count = 0
|
|
340
|
+
[uint32]$rebootReasons = 0
|
|
341
|
+
$result = [AgentBotRestartManager]::RmGetList($handle, [ref]$needed, [ref]$count, $null, [ref]$rebootReasons)
|
|
342
|
+
if ($result -eq 0) {
|
|
343
|
+
ConvertTo-Json -Compress -InputObject @()
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
if ($result -ne 234) { throw "RmGetList failed: $result" }
|
|
347
|
+
$items = New-Object 'AgentBotRestartManager+RM_PROCESS_INFO[]' $needed
|
|
348
|
+
$count = $needed
|
|
349
|
+
$result = [AgentBotRestartManager]::RmGetList($handle, [ref]$needed, [ref]$count, $items, [ref]$rebootReasons)
|
|
350
|
+
if ($result -ne 0) { throw "RmGetList failed: $result" }
|
|
351
|
+
$owners = @()
|
|
352
|
+
for ($index = 0; $index -lt $count; $index++) {
|
|
353
|
+
$writerPid = $items[$index].Process.dwProcessId
|
|
354
|
+
$writer = Get-AgentBotProcess $writerPid
|
|
355
|
+
if ($null -eq $writer) { continue }
|
|
356
|
+
$application = $writer
|
|
357
|
+
$cursor = $writer
|
|
358
|
+
for ($depth = 0; $depth -lt 8 -and $cursor.ParentProcessId -gt 1; $depth++) {
|
|
359
|
+
$parent = Get-AgentBotProcess $cursor.ParentProcessId
|
|
360
|
+
if ($null -eq $parent) { break }
|
|
361
|
+
if ($parent.Name -ieq 'ChatGPT.exe' -and $parent.ExecutablePath -match 'OpenAI[.\\]Codex') {
|
|
362
|
+
$application = $parent
|
|
363
|
+
break
|
|
364
|
+
}
|
|
365
|
+
$cursor = $parent
|
|
366
|
+
}
|
|
367
|
+
$recognized = (($writer.Name -match '^(codex|trae|traex|traecli)(.exe)?$') -or ($writer.CommandLine -match '(codex|trae|traex|traecli).*(app-server)'))
|
|
368
|
+
$desktop = $application.Name -ieq 'ChatGPT.exe' -and $application.ExecutablePath -match 'OpenAI[.\\]Codex'
|
|
369
|
+
$owners += [pscustomobject]@{
|
|
370
|
+
writerPid = $writerPid
|
|
371
|
+
writerProcessName = $writer.Name
|
|
372
|
+
writerStartedAt = Get-AgentBotStartTime $writerPid
|
|
373
|
+
applicationPid = [int]$application.ProcessId
|
|
374
|
+
applicationProcessName = $application.Name
|
|
375
|
+
applicationStartedAt = Get-AgentBotStartTime $application.ProcessId
|
|
376
|
+
displayName = $(if ($desktop) { 'Codex Desktop' } else { $writer.Name })
|
|
377
|
+
canClose = [bool]$recognized
|
|
378
|
+
commandLine = $writer.CommandLine
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
ConvertTo-Json -Compress -InputObject @($owners)
|
|
382
|
+
} finally {
|
|
383
|
+
[void][AgentBotRestartManager]::RmEndSession($handle)
|
|
384
|
+
}
|
|
385
|
+
`;
|
|
@@ -18,6 +18,7 @@ export const COMMAND_NAMES = [
|
|
|
18
18
|
"permissions",
|
|
19
19
|
"provider",
|
|
20
20
|
"queue",
|
|
21
|
+
"release",
|
|
21
22
|
"restart",
|
|
22
23
|
"sessions",
|
|
23
24
|
"status",
|
|
@@ -114,6 +115,9 @@ export class CommandRouter {
|
|
|
114
115
|
if (args.length === 0) return { type: "restart" };
|
|
115
116
|
if (args.length === 1 && args[0] === "--force") return { type: "restart", force: true };
|
|
116
117
|
throw new Error("/restart 只接受一个可选的 --force 参数。");
|
|
118
|
+
case "release":
|
|
119
|
+
if (args.length > 0) throw new Error("/release 不接受参数。");
|
|
120
|
+
return { type: "release" };
|
|
117
121
|
case "mute": {
|
|
118
122
|
if (args.length === 0 || (args.length === 1 && args[0]!.toLowerCase() === "on")) {
|
|
119
123
|
return { type: "mute", enabled: true };
|
|
@@ -19,6 +19,7 @@ export type Command =
|
|
|
19
19
|
| { type: "goal"; action: "set" | "edit"; objective: string }
|
|
20
20
|
| { type: "goal"; action: "pause" | "resume" | "clear" }
|
|
21
21
|
| { type: "restart"; force?: boolean }
|
|
22
|
+
| { type: "release" }
|
|
22
23
|
| { type: "mute"; enabled: boolean }
|
|
23
24
|
| { type: "turns" }
|
|
24
25
|
| { type: "model" }
|
|
@@ -32,9 +32,10 @@ export class ConsoleTurnPresenter implements TurnPresenter {
|
|
|
32
32
|
return undefined;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
async failPendingTurn(sessionId: string, message: string): Promise<
|
|
35
|
+
async failPendingTurn(sessionId: string, message: string): Promise<boolean> {
|
|
36
36
|
const contextKey = this.contexts.get(sessionId);
|
|
37
37
|
if (contextKey) await this.outbound.sendText(contextKey, `${this.agentLabel(sessionId)} failed to start: ${message}`);
|
|
38
|
+
return false;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
async interruptTurnForRecovery(): Promise<void> {}
|
|
@@ -255,6 +255,37 @@ export interface ShellCommandCardView {
|
|
|
255
255
|
outputTruncated: boolean;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
export interface ThreadWriterConflictCardView {
|
|
259
|
+
status: "occupied" | "force_required" | "released";
|
|
260
|
+
contextKey: string;
|
|
261
|
+
sessionId: string;
|
|
262
|
+
threadId: string;
|
|
263
|
+
taskTitle?: string;
|
|
264
|
+
notice?: string;
|
|
265
|
+
applicationIdle?: boolean;
|
|
266
|
+
owner?: {
|
|
267
|
+
displayName: string;
|
|
268
|
+
writerPid: number;
|
|
269
|
+
writerProcessName: string;
|
|
270
|
+
writerStartedAt?: string;
|
|
271
|
+
applicationPid: number;
|
|
272
|
+
applicationProcessName: string;
|
|
273
|
+
applicationStartedAt?: string;
|
|
274
|
+
canClose: boolean;
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface AppServerReleaseCardView {
|
|
279
|
+
status: "waiting" | "releasing" | "released" | "cancelled" | "failed";
|
|
280
|
+
contextKey: string;
|
|
281
|
+
scheduleId: number;
|
|
282
|
+
agentName: string;
|
|
283
|
+
blockingTaskCount?: number;
|
|
284
|
+
releaseInSeconds?: number;
|
|
285
|
+
forced?: boolean;
|
|
286
|
+
error?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
258
289
|
export class CardRenderer {
|
|
259
290
|
private readonly thinkingCardLayout: ThinkingCardLayout;
|
|
260
291
|
|
|
@@ -262,6 +293,180 @@ export class CardRenderer {
|
|
|
262
293
|
this.thinkingCardLayout = options.thinkingCardLayout ?? "grouped";
|
|
263
294
|
}
|
|
264
295
|
|
|
296
|
+
renderThreadWriterConflict(view: ThreadWriterConflictCardView): Record<string, unknown> {
|
|
297
|
+
if (view.status === "released") {
|
|
298
|
+
return sectionCard("任务占用已解除", [markdown([
|
|
299
|
+
view.taskTitle ? `**任务**:${inlineCode(view.taskTitle)}` : undefined,
|
|
300
|
+
view.notice ?? "请重新发送消息。",
|
|
301
|
+
].filter((line): line is string => Boolean(line)).join("\n"))], "green");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const owner = view.owner;
|
|
305
|
+
const lines = [
|
|
306
|
+
view.taskTitle ? `**任务**:${inlineCode(view.taskTitle)}` : undefined,
|
|
307
|
+
owner ? `**进程**:${inlineCode(`${owner.applicationProcessName} (${owner.applicationPid})`)}` : undefined,
|
|
308
|
+
view.notice,
|
|
309
|
+
owner
|
|
310
|
+
? view.applicationIdle === true
|
|
311
|
+
? "> 无执行中任务,可安全关闭;也可 **Fork** 后继续发送消息。"
|
|
312
|
+
: view.applicationIdle === false
|
|
313
|
+
? "> 有任务执行中,关闭会中断任务;可 **Fork** 后继续发送消息。"
|
|
314
|
+
: "> 可 **Fork** 后继续发送消息;关闭进程后请重发消息。"
|
|
315
|
+
: "> 可 **Fork** 后继续发送消息;或手动关闭占用任务。",
|
|
316
|
+
].filter((line): line is string => Boolean(line));
|
|
317
|
+
const elements: Record<string, unknown>[] = [markdown(lines.join("\n"))];
|
|
318
|
+
const canClose = owner?.canClose
|
|
319
|
+
&& Boolean(owner.writerStartedAt)
|
|
320
|
+
&& Boolean(owner.applicationStartedAt);
|
|
321
|
+
const actions: Record<string, unknown>[] = [{
|
|
322
|
+
tag: "button",
|
|
323
|
+
text: {
|
|
324
|
+
tag: "plain_text",
|
|
325
|
+
content: "Fork",
|
|
326
|
+
},
|
|
327
|
+
type: "default",
|
|
328
|
+
size: "small",
|
|
329
|
+
behaviors: [{
|
|
330
|
+
type: "callback",
|
|
331
|
+
value: {
|
|
332
|
+
action: "thread_writer_fork",
|
|
333
|
+
contextKey: view.contextKey,
|
|
334
|
+
sessionId: view.sessionId,
|
|
335
|
+
threadId: view.threadId,
|
|
336
|
+
},
|
|
337
|
+
}],
|
|
338
|
+
}];
|
|
339
|
+
if (canClose && owner) {
|
|
340
|
+
actions.push({
|
|
341
|
+
tag: "button",
|
|
342
|
+
text: {
|
|
343
|
+
tag: "plain_text",
|
|
344
|
+
content: view.status === "force_required"
|
|
345
|
+
? `Force ${owner.applicationPid}`
|
|
346
|
+
: `Close ${owner.applicationPid}`,
|
|
347
|
+
},
|
|
348
|
+
type: "danger",
|
|
349
|
+
size: "small",
|
|
350
|
+
behaviors: [{
|
|
351
|
+
type: "callback",
|
|
352
|
+
value: {
|
|
353
|
+
action: "thread_writer_close",
|
|
354
|
+
contextKey: view.contextKey,
|
|
355
|
+
sessionId: view.sessionId,
|
|
356
|
+
threadId: view.threadId,
|
|
357
|
+
writerPid: owner.writerPid,
|
|
358
|
+
writerStartedAt: owner.writerStartedAt,
|
|
359
|
+
applicationPid: owner.applicationPid,
|
|
360
|
+
applicationStartedAt: owner.applicationStartedAt,
|
|
361
|
+
force: view.status === "force_required",
|
|
362
|
+
},
|
|
363
|
+
}],
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
elements.push({
|
|
367
|
+
tag: "column_set",
|
|
368
|
+
flex_mode: "none",
|
|
369
|
+
horizontal_spacing: "8px",
|
|
370
|
+
vertical_align: "center",
|
|
371
|
+
columns: actions.map((button) => ({
|
|
372
|
+
tag: "column",
|
|
373
|
+
width: "auto",
|
|
374
|
+
vertical_align: "center",
|
|
375
|
+
elements: [button],
|
|
376
|
+
})),
|
|
377
|
+
});
|
|
378
|
+
return sectionCard(
|
|
379
|
+
view.status === "force_required" ? "仍被占用" : "任务被占用",
|
|
380
|
+
elements,
|
|
381
|
+
view.status === "force_required" ? "red" : "orange",
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
renderAppServerRelease(view: AppServerReleaseCardView): Record<string, unknown> {
|
|
386
|
+
const agent = inlineCode(view.agentName);
|
|
387
|
+
if (view.status === "released") {
|
|
388
|
+
return sectionCard("任务已释放", [markdown([
|
|
389
|
+
`**Agent**:${agent}`,
|
|
390
|
+
view.forced ? "执行中的任务已中断。" : undefined,
|
|
391
|
+
"> 请在 Codex Desktop 点击 **Retry**。",
|
|
392
|
+
].filter((line): line is string => Boolean(line)).join("\n"))], "green");
|
|
393
|
+
}
|
|
394
|
+
if (view.status === "cancelled") {
|
|
395
|
+
return sectionCard("已取消释放", [markdown(`**Agent**:${agent}\n> Agent Bot 将继续保持当前任务连接。`)], "grey");
|
|
396
|
+
}
|
|
397
|
+
if (view.status === "failed") {
|
|
398
|
+
return sectionCard("释放失败", [markdown([
|
|
399
|
+
`**Agent**:${agent}`,
|
|
400
|
+
view.error ? truncateText(view.error, 500) : "无法释放 App Server。",
|
|
401
|
+
].join("\n"))], "red");
|
|
402
|
+
}
|
|
403
|
+
if (view.status === "releasing") {
|
|
404
|
+
return sectionCard("正在释放", [markdown(`**Agent**:${agent}\n正在停止 Agent Bot 的 App Server。`)], "blue");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const blockingTaskCount = Math.max(0, view.blockingTaskCount ?? 0);
|
|
408
|
+
const releaseInSeconds = Math.max(1, view.releaseInSeconds ?? 1);
|
|
409
|
+
return sectionCard("等待释放", [
|
|
410
|
+
markdown([
|
|
411
|
+
`**Agent**:${agent}`,
|
|
412
|
+
blockingTaskCount > 0
|
|
413
|
+
? `**等待任务**:${blockingTaskCount}`
|
|
414
|
+
: `**自动释放**:${releaseInSeconds} 秒后`,
|
|
415
|
+
blockingTaskCount > 0
|
|
416
|
+
? "> 任务结束后自动释放;立即释放会中断执行。"
|
|
417
|
+
: "> 可立即释放或取消。",
|
|
418
|
+
].join("\n")),
|
|
419
|
+
{
|
|
420
|
+
tag: "column_set",
|
|
421
|
+
flex_mode: "none",
|
|
422
|
+
horizontal_spacing: "8px",
|
|
423
|
+
vertical_align: "center",
|
|
424
|
+
columns: [
|
|
425
|
+
{
|
|
426
|
+
tag: "column",
|
|
427
|
+
width: "auto",
|
|
428
|
+
vertical_align: "center",
|
|
429
|
+
elements: [{
|
|
430
|
+
tag: "button",
|
|
431
|
+
text: { tag: "plain_text", content: "Release Now" },
|
|
432
|
+
type: "danger",
|
|
433
|
+
size: "small",
|
|
434
|
+
behaviors: [{
|
|
435
|
+
type: "callback",
|
|
436
|
+
value: {
|
|
437
|
+
action: "app_server_release_now",
|
|
438
|
+
contextKey: view.contextKey,
|
|
439
|
+
scheduleId: view.scheduleId,
|
|
440
|
+
agentName: view.agentName,
|
|
441
|
+
},
|
|
442
|
+
}],
|
|
443
|
+
}],
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
tag: "column",
|
|
447
|
+
width: "auto",
|
|
448
|
+
vertical_align: "center",
|
|
449
|
+
elements: [{
|
|
450
|
+
tag: "button",
|
|
451
|
+
text: { tag: "plain_text", content: "Cancel" },
|
|
452
|
+
type: "default",
|
|
453
|
+
size: "small",
|
|
454
|
+
behaviors: [{
|
|
455
|
+
type: "callback",
|
|
456
|
+
value: {
|
|
457
|
+
action: "app_server_release_cancel",
|
|
458
|
+
contextKey: view.contextKey,
|
|
459
|
+
scheduleId: view.scheduleId,
|
|
460
|
+
agentName: view.agentName,
|
|
461
|
+
},
|
|
462
|
+
}],
|
|
463
|
+
}],
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
], "orange");
|
|
468
|
+
}
|
|
469
|
+
|
|
265
470
|
renderShellCommandCard(view: ShellCommandCardView): Record<string, unknown> {
|
|
266
471
|
const rawOutput = normalizeTerminalOutput(view.output).trimEnd();
|
|
267
472
|
const displayOutput = truncateMiddle(
|
|
@@ -164,9 +164,13 @@ export class FeishuTurnPresenter {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
async failPendingTurn(
|
|
167
|
+
async failPendingTurn(
|
|
168
|
+
sessionId: string,
|
|
169
|
+
message: string,
|
|
170
|
+
replacementCard?: Record<string, unknown>,
|
|
171
|
+
): Promise<boolean> {
|
|
168
172
|
const entry = this.pendingEntries.get(sessionId);
|
|
169
|
-
if (!entry) return;
|
|
173
|
+
if (!entry) return false;
|
|
170
174
|
this.pendingEntries.delete(sessionId);
|
|
171
175
|
this.stopElapsedUpdates(entry);
|
|
172
176
|
const completedAt = Date.now();
|
|
@@ -183,9 +187,18 @@ export class FeishuTurnPresenter {
|
|
|
183
187
|
await entry.scheduler?.flush(entry.state);
|
|
184
188
|
} catch (error) {
|
|
185
189
|
this.options.onError?.(error);
|
|
186
|
-
return;
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
if (replacementCard && entry.messageId) {
|
|
193
|
+
try {
|
|
194
|
+
await this.outbound.updateInteractiveCard(entry.messageId, replacementCard);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
this.options.onError?.(error);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
187
199
|
}
|
|
188
200
|
await this.syncThinkingCardReaction(entry);
|
|
201
|
+
return Boolean(replacementCard && entry.messageId);
|
|
189
202
|
}
|
|
190
203
|
|
|
191
204
|
async interruptTurnForRecovery(
|
|
@@ -273,8 +273,12 @@ function renderPost(
|
|
|
273
273
|
for (const rawElement of row) {
|
|
274
274
|
if (!isRecord(rawElement)) continue;
|
|
275
275
|
const tag = textValue(rawElement.tag);
|
|
276
|
-
if (tag === "text"
|
|
276
|
+
if (tag === "text") {
|
|
277
277
|
rowText += textValue(rawElement.text);
|
|
278
|
+
} else if (tag === "a") {
|
|
279
|
+
const text = textValue(rawElement.text);
|
|
280
|
+
const href = textValue(rawElement.href);
|
|
281
|
+
rowText += href && href !== text ? `${text || href} (${href})` : text || href;
|
|
278
282
|
} else if (tag === "at") {
|
|
279
283
|
rowText += renderMention(rawElement, mentions);
|
|
280
284
|
} else if (tag === "img") {
|