@offerpilot/axiomruntime 0.0.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 (99) hide show
  1. package/README.md +185 -0
  2. package/dist/cli/commands/add.js +29 -0
  3. package/dist/cli/commands/context.js +29 -0
  4. package/dist/cli/commands/doctor.js +62 -0
  5. package/dist/cli/commands/edit.js +85 -0
  6. package/dist/cli/commands/help.js +16 -0
  7. package/dist/cli/commands/list.js +25 -0
  8. package/dist/cli/commands/log.js +63 -0
  9. package/dist/cli/commands/memory.js +241 -0
  10. package/dist/cli/commands/report.js +35 -0
  11. package/dist/cli/commands/session.js +74 -0
  12. package/dist/cli/commands/setup.js +86 -0
  13. package/dist/cli/commands/status.js +50 -0
  14. package/dist/cli/commands/telegram.js +701 -0
  15. package/dist/cli/commands/use.js +108 -0
  16. package/dist/cli/commands/version.js +12 -0
  17. package/dist/cli/index.js +276 -0
  18. package/dist/cli/output/table.js +22 -0
  19. package/dist/cli/prompts/prompt.js +37 -0
  20. package/dist/cli/registry.js +16 -0
  21. package/dist/core/config/cache-store.js +193 -0
  22. package/dist/core/config/json-store.js +114 -0
  23. package/dist/core/config/paths.js +85 -0
  24. package/dist/core/config/providers-store.js +89 -0
  25. package/dist/core/config/schema.js +60 -0
  26. package/dist/core/config/session-store.js +30 -0
  27. package/dist/core/config/usage-store.js +18 -0
  28. package/dist/core/context/context-service.js +186 -0
  29. package/dist/core/integrations/integration-state.js +105 -0
  30. package/dist/core/logs/log-service.js +56 -0
  31. package/dist/core/memory/embedding-check.js +121 -0
  32. package/dist/core/memory/memory-config.js +122 -0
  33. package/dist/core/models/model-discovery.js +430 -0
  34. package/dist/core/models/model-filter.js +13 -0
  35. package/dist/core/providers/provider-service.js +212 -0
  36. package/dist/core/reports/report-service.js +166 -0
  37. package/dist/core/runner/command-resolver.js +60 -0
  38. package/dist/core/runner/engine-registry.js +93 -0
  39. package/dist/core/runner/fallback.js +114 -0
  40. package/dist/core/runner/openai-usage-http.js +82 -0
  41. package/dist/core/runner/openai-usage-proxy.js +1 -0
  42. package/dist/core/runner/openai-usage-recording.js +172 -0
  43. package/dist/core/runner/openai-usage-responses.js +469 -0
  44. package/dist/core/runner/openai-usage-server.js +319 -0
  45. package/dist/core/runner/openai-usage-types.js +1 -0
  46. package/dist/core/runner/tool-runner.js +138 -0
  47. package/dist/core/sessions/session-service.js +47 -0
  48. package/dist/core/status/doctor-service.js +391 -0
  49. package/dist/core/status/status-service.js +60 -0
  50. package/dist/core/types.js +1 -0
  51. package/dist/core/usage/pricing.js +113 -0
  52. package/dist/core/usage/usage-service.js +30 -0
  53. package/dist/core/utils/is-record.js +3 -0
  54. package/dist/server/index.js +28 -0
  55. package/dist/server/runtime-server.js +430 -0
  56. package/dist/telegram/bot-registry.js +80 -0
  57. package/dist/telegram/bot.js +128 -0
  58. package/dist/telegram/config.js +235 -0
  59. package/dist/telegram/engine/claude-engine.js +240 -0
  60. package/dist/telegram/engine/codex-engine.js +437 -0
  61. package/dist/telegram/engine/engine-utils.js +67 -0
  62. package/dist/telegram/engine/process-utils.js +132 -0
  63. package/dist/telegram/engine/registry.js +31 -0
  64. package/dist/telegram/engine/types.js +1 -0
  65. package/dist/telegram/handler-registry.js +28 -0
  66. package/dist/telegram/handlers/callback.js +311 -0
  67. package/dist/telegram/handlers/command.js +272 -0
  68. package/dist/telegram/handlers/document.js +108 -0
  69. package/dist/telegram/handlers/memory.js +305 -0
  70. package/dist/telegram/handlers/message.js +701 -0
  71. package/dist/telegram/handlers/provider.js +332 -0
  72. package/dist/telegram/handlers/setup.js +527 -0
  73. package/dist/telegram/handlers/usage.js +124 -0
  74. package/dist/telegram/index.js +93 -0
  75. package/dist/telegram/interaction/approval.js +108 -0
  76. package/dist/telegram/interaction/command-menu.js +253 -0
  77. package/dist/telegram/interaction/formatter.js +487 -0
  78. package/dist/telegram/interaction/keyboards.js +145 -0
  79. package/dist/telegram/interaction/progress-reporter.js +160 -0
  80. package/dist/telegram/interaction/prompt-middleware.js +168 -0
  81. package/dist/telegram/interaction/result-store.js +41 -0
  82. package/dist/telegram/interaction/token-budget.js +21 -0
  83. package/dist/telegram/interaction/tool-name.js +41 -0
  84. package/dist/telegram/lifecycle-registry.js +47 -0
  85. package/dist/telegram/log.js +46 -0
  86. package/dist/telegram/memory/memory-inject.js +52 -0
  87. package/dist/telegram/memory/memory-service.js +413 -0
  88. package/dist/telegram/memory/memory-store.js +216 -0
  89. package/dist/telegram/memory/types.js +1 -0
  90. package/dist/telegram/network-retry.js +22 -0
  91. package/dist/telegram/network.js +53 -0
  92. package/dist/telegram/session/manager.js +229 -0
  93. package/dist/telegram/session/store.js +363 -0
  94. package/dist/telegram/session/types.js +1 -0
  95. package/dist/telegram/supervisor.js +57 -0
  96. package/dist/telegram/templates/messages.js +1 -0
  97. package/docs/README.md +98 -0
  98. package/docs/USAGE.html +853 -0
  99. package/package.json +57 -0
@@ -0,0 +1,437 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { cleanupRuntime, createCommandPermissionRequest, formatEngineTimeout, prepareEngineRuntime, summarizeRuntime } from "./engine-utils.js";
5
+ import { createJsonLineAccumulator, extractJsonLines, spawnCapture, stripAnsi, terminateProcessTree } from "./process-utils.js";
6
+ import { startOpenAiUsageProxy } from "../../core/runner/openai-usage-proxy.js";
7
+ import { requiresDangerousConfirmation } from "../interaction/approval.js";
8
+ import { writeTelegramLog } from "../log.js";
9
+ import { isRecord } from "../../core/utils/is-record.js";
10
+ export class CodexEngine {
11
+ name = "codex";
12
+ processes = new Map();
13
+ controllers = new Map();
14
+ async execute(params) {
15
+ const runtime = await prepareEngineRuntime("codex", params.providerName, params.modelName);
16
+ const controller = new AbortController();
17
+ this.controllers.set(params.chatId, controller);
18
+ const signal = params.signal ?? controller.signal;
19
+ const startTime = Date.now();
20
+ const permissionMode = params.permissionMode ?? "default";
21
+ if (permissionMode === "bypassPermissions" && requiresDangerousConfirmation(params.prompt)) {
22
+ const approved = await params.onPermission(createCommandPermissionRequest("Codex prompt", params.prompt));
23
+ if (!approved) {
24
+ cleanupRuntime(runtime);
25
+ throw new Error("Dangerous command denied.");
26
+ }
27
+ }
28
+ let usageProxy = null;
29
+ await params.onProgress?.({ phase: "thinking", message: "Codex started" });
30
+ await writeTelegramLog({
31
+ action: "codex_start",
32
+ message: `Starting Codex for Telegram chat ${params.chatId}`,
33
+ metadata: { ...summarizeRuntime(runtime.provider, runtime.model, "codex"), cwd: params.cwd }
34
+ });
35
+ try {
36
+ usageProxy = await startOpenAiUsageProxy({
37
+ provider: runtime.provider,
38
+ model: runtime.model,
39
+ tool: "telegram"
40
+ });
41
+ const args = buildCodexArgs(params.prompt, runtime.model, params.cwd, permissionMode, {
42
+ baseUrl: usageProxy.baseUrl
43
+ }, params.resume, params.imagePaths);
44
+ const progressParser = createJsonLineAccumulator((event) => handleCodexProgressEvent(event, params.onProgress));
45
+ const result = await spawnCapture({
46
+ command: runtime.command,
47
+ args,
48
+ cwd: params.cwd,
49
+ env: {
50
+ ...runtime.env,
51
+ OPENAI_API_KEY: runtime.provider.apiKey,
52
+ OPENAI_BASE_URL: usageProxy.baseUrl,
53
+ AI_GATEWAY_BASE_URL: usageProxy.baseUrl
54
+ },
55
+ signal,
56
+ timeoutMs: params.timeoutMs,
57
+ onStdout: (chunk) => progressParser.push(chunk)
58
+ }, (child) => this.processes.set(params.chatId, child));
59
+ progressParser.end();
60
+ if (result.timedOut) {
61
+ throw new Error(formatEngineTimeout("Codex", params.timeoutMs, runtime.provider.name, runtime.model));
62
+ }
63
+ if (result.aborted) {
64
+ throw new Error("任务已停止。");
65
+ }
66
+ if (result.code !== 0) {
67
+ throw new Error(formatCodexFailure([result.stderr, result.stdout].filter((value) => value.trim()).join("\n") || `Codex exited with code ${result.code}`, runtime.provider.name, runtime.model));
68
+ }
69
+ await params.onProgress?.({ phase: "done" });
70
+ const parsed = parseCodexOutput(result.stdout, Date.now() - startTime);
71
+ const sessionId = parsed.sessionId ?? findLatestCodexSessionId({
72
+ cwd: params.cwd,
73
+ startedAtMs: startTime,
74
+ finishedAtMs: Date.now()
75
+ });
76
+ if (sessionId && !parsed.sessionId) {
77
+ await writeTelegramLog({
78
+ action: "codex_session_detected",
79
+ message: `Detected Codex session ${sessionId} from local history.`,
80
+ metadata: { chatId: params.chatId, cwd: params.cwd }
81
+ });
82
+ }
83
+ return {
84
+ ...parsed,
85
+ sessionId,
86
+ cost: sumKnownProxyCosts(usageProxy.getRecords()),
87
+ usage: sumProxyUsage(usageProxy.getRecords()),
88
+ provider: runtime.provider.name,
89
+ model: runtime.model
90
+ };
91
+ }
92
+ finally {
93
+ this.processes.delete(params.chatId);
94
+ this.controllers.delete(params.chatId);
95
+ await usageProxy?.close().catch(() => undefined);
96
+ cleanupRuntime(runtime);
97
+ }
98
+ }
99
+ abort(chatId) {
100
+ if (!Number.isFinite(chatId)) {
101
+ for (const id of new Set([...this.controllers.keys(), ...this.processes.keys()])) {
102
+ this.abort(id);
103
+ }
104
+ return;
105
+ }
106
+ this.controllers.get(chatId)?.abort();
107
+ const child = this.processes.get(chatId);
108
+ if (child)
109
+ terminateProcessTree(child, "SIGTERM");
110
+ this.controllers.delete(chatId);
111
+ this.processes.delete(chatId);
112
+ }
113
+ }
114
+ export function buildCodexArgs(prompt, model, cwd, permissionMode, providerOverride, resume, imagePaths = []) {
115
+ const args = [
116
+ "exec",
117
+ ...(resume ? ["resume"] : []),
118
+ ...buildCodexProviderOverrideArgs(providerOverride),
119
+ "--json",
120
+ "--skip-git-repo-check",
121
+ "-m",
122
+ model,
123
+ ...(resume ? [] : ["-C", cwd])
124
+ ];
125
+ if (permissionMode === "readOnly") {
126
+ args.unshift("-a", "never", "-s", "read-only");
127
+ }
128
+ else if (permissionMode === "bypassPermissions") {
129
+ args.push("--dangerously-bypass-approvals-and-sandbox");
130
+ }
131
+ else if (permissionMode === "acceptEdits") {
132
+ args.unshift("-a", "never");
133
+ if (resume) {
134
+ args.unshift("-s", "workspace-write");
135
+ }
136
+ else {
137
+ args.push("-s", "workspace-write");
138
+ }
139
+ }
140
+ else {
141
+ args.unshift("-a", "on-request");
142
+ if (resume) {
143
+ args.unshift("-s", "workspace-write");
144
+ }
145
+ else {
146
+ args.push("-s", "workspace-write");
147
+ }
148
+ }
149
+ if (resume) {
150
+ args.push(resume);
151
+ }
152
+ for (const imagePath of imagePaths) {
153
+ args.push("--image", imagePath);
154
+ }
155
+ args.push(prompt);
156
+ return args;
157
+ }
158
+ function buildCodexProviderOverrideArgs(providerOverride) {
159
+ if (!providerOverride)
160
+ return [];
161
+ const providerId = providerOverride.providerId ?? "ai_gateway_proxy";
162
+ return [
163
+ "-c",
164
+ `model_provider=${tomlString(providerId)}`,
165
+ "-c",
166
+ `model_providers.${providerId}.name=${tomlString("AI Gateway Proxy")}`,
167
+ "-c",
168
+ `model_providers.${providerId}.base_url=${tomlString(providerOverride.baseUrl)}`,
169
+ "-c",
170
+ `model_providers.${providerId}.env_key=${tomlString("OPENAI_API_KEY")}`,
171
+ "-c",
172
+ `model_providers.${providerId}.requires_openai_auth=true`,
173
+ "-c",
174
+ `model_providers.${providerId}.wire_api=${tomlString("responses")}`
175
+ ];
176
+ }
177
+ function tomlString(value) {
178
+ return JSON.stringify(value);
179
+ }
180
+ export function parseCodexOutput(raw, duration) {
181
+ const events = extractJsonLines(raw);
182
+ let response = "";
183
+ let sessionId;
184
+ const toolCalls = [];
185
+ const filesChanged = new Set();
186
+ for (const event of events) {
187
+ if (!isRecord(event))
188
+ continue;
189
+ sessionId = readCodexSessionId(event) ?? sessionId;
190
+ const message = readCodexMessage(event);
191
+ if (message)
192
+ response = message;
193
+ const toolCall = readCodexToolCall(event);
194
+ if (toolCall)
195
+ toolCalls.push(toolCall);
196
+ for (const filePath of readChangedFiles(event)) {
197
+ filesChanged.add(filePath);
198
+ }
199
+ }
200
+ if (!response.trim()) {
201
+ response = stripAnsi(raw).trim();
202
+ }
203
+ return {
204
+ sessionId,
205
+ response,
206
+ toolCalls,
207
+ filesChanged: [...filesChanged],
208
+ duration
209
+ };
210
+ }
211
+ export function findLatestCodexSessionId(input) {
212
+ const codexHome = input.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
213
+ const sessionsRoot = path.join(codexHome, "sessions");
214
+ const finishedAtMs = input.finishedAtMs ?? Date.now();
215
+ const files = listCandidateCodexSessionFiles(sessionsRoot, input.startedAtMs, finishedAtMs);
216
+ const startedWindow = input.startedAtMs - 10_000;
217
+ const finishedWindow = finishedAtMs + 30_000;
218
+ for (const filePath of files) {
219
+ const stat = safeStat(filePath);
220
+ if (!stat || stat.mtimeMs < startedWindow || stat.mtimeMs > finishedWindow)
221
+ continue;
222
+ const session = readCodexSessionMeta(filePath);
223
+ if (!session)
224
+ continue;
225
+ if (path.resolve(session.cwd) !== path.resolve(input.cwd))
226
+ continue;
227
+ if (session.source && session.source !== "exec")
228
+ continue;
229
+ return session.sessionId;
230
+ }
231
+ return undefined;
232
+ }
233
+ export function formatCodexFailure(raw, providerName, model) {
234
+ const text = stripAnsi(raw).trim();
235
+ if (/stream disconnected/i.test(text)) {
236
+ return [
237
+ "Codex 请求失败:模型流式响应连续断开,已重试 5 次。",
238
+ providerName && model ? `Provider:${providerName} / ${model}` : "",
239
+ "这通常是 provider 上游或网络连接不稳定,不是 Telegram 输入格式问题。"
240
+ ].filter(Boolean).join("\n");
241
+ }
242
+ const meaningful = text
243
+ .split(/\r?\n/)
244
+ .map((line) => line.trim())
245
+ .filter((line) => line && !isBenignCodexDiagnostic(line))
246
+ .join("\n")
247
+ .trim();
248
+ return meaningful || text || "Codex exited without a diagnostic message.";
249
+ }
250
+ function isBenignCodexDiagnostic(line) {
251
+ return line === "Reading additional input from stdin..."
252
+ || /codex_core_plugins::remote::remote_installed_plugin_sync/.test(line)
253
+ || /codex_features: unknown feature key in config/.test(line)
254
+ || /codex_core_plugins::manifest: ignoring interface\.defaultPrompt/.test(line)
255
+ || /codex_core_plugins::manager: failed to warm featured plugin ids cache/.test(line);
256
+ }
257
+ function listCandidateCodexSessionFiles(sessionsRoot, startedAtMs, finishedAtMs) {
258
+ const dayDirs = new Set();
259
+ for (const ms of [startedAtMs, finishedAtMs, startedAtMs - 24 * 60 * 60 * 1000, finishedAtMs + 24 * 60 * 60 * 1000]) {
260
+ const date = new Date(ms);
261
+ dayDirs.add(path.join(sessionsRoot, String(date.getFullYear()), String(date.getMonth() + 1).padStart(2, "0"), String(date.getDate()).padStart(2, "0")));
262
+ }
263
+ const files = [];
264
+ for (const dir of dayDirs) {
265
+ for (const filePath of listJsonlFiles(dir)) {
266
+ const stat = safeStat(filePath);
267
+ if (stat)
268
+ files.push({ path: filePath, mtimeMs: stat.mtimeMs });
269
+ }
270
+ }
271
+ return files.sort((a, b) => b.mtimeMs - a.mtimeMs).map((file) => file.path);
272
+ }
273
+ function listJsonlFiles(dir) {
274
+ try {
275
+ return fs.readdirSync(dir, { withFileTypes: true })
276
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
277
+ .map((entry) => path.join(dir, entry.name));
278
+ }
279
+ catch {
280
+ return [];
281
+ }
282
+ }
283
+ function safeStat(filePath) {
284
+ try {
285
+ return fs.statSync(filePath);
286
+ }
287
+ catch {
288
+ return null;
289
+ }
290
+ }
291
+ function readCodexSessionMeta(filePath) {
292
+ try {
293
+ const text = fs.readFileSync(filePath, "utf8");
294
+ for (const line of text.split(/\r?\n/).slice(0, 20)) {
295
+ if (!line.trim())
296
+ continue;
297
+ const event = JSON.parse(line);
298
+ if (!isRecord(event) || event.type !== "session_meta" || !isRecord(event.payload))
299
+ continue;
300
+ const sessionId = readCodexSessionId(event);
301
+ const cwd = typeof event.payload.cwd === "string" ? event.payload.cwd : "";
302
+ if (sessionId && cwd) {
303
+ return {
304
+ sessionId,
305
+ cwd,
306
+ source: typeof event.payload.source === "string" ? event.payload.source : undefined
307
+ };
308
+ }
309
+ }
310
+ }
311
+ catch {
312
+ return null;
313
+ }
314
+ return null;
315
+ }
316
+ function handleCodexProgressEvent(event, onProgress) {
317
+ if (!onProgress)
318
+ return;
319
+ if (!isRecord(event))
320
+ return;
321
+ const toolCall = readCodexToolCall(event);
322
+ if (toolCall) {
323
+ void onProgress(readCodexProgressUpdate(event, toolCall.tool));
324
+ return;
325
+ }
326
+ const responseText = readCodexMessage(event);
327
+ if (responseText || String(event.type ?? "").includes("message")) {
328
+ void onProgress({ phase: "writing", responseText: responseText ?? undefined });
329
+ }
330
+ }
331
+ function readCodexProgressUpdate(event, fallbackTool) {
332
+ const type = String(event.type ?? event.kind ?? "");
333
+ const input = isRecord(event.input) ? event.input : {};
334
+ const command = readFirstString(event.command, input.command, event.cmd, input.cmd);
335
+ if (command || /exec|command|shell/i.test(type)) {
336
+ return {
337
+ phase: "tool_calling",
338
+ tool: command ?? fallbackTool,
339
+ toolKind: "command"
340
+ };
341
+ }
342
+ const mcpServer = readFirstString(event.mcpServer, event.mcp_server, event.server, input.mcpServer, input.server);
343
+ const mcpTool = readFirstString(event.tool, event.name, input.tool, input.name);
344
+ if (mcpServer || /mcp/i.test(type)) {
345
+ return {
346
+ phase: "tool_calling",
347
+ tool: mcpTool ?? fallbackTool,
348
+ toolKind: "mcp",
349
+ mcpServer
350
+ };
351
+ }
352
+ return {
353
+ phase: "tool_calling",
354
+ tool: fallbackTool,
355
+ toolKind: "builtin"
356
+ };
357
+ }
358
+ function readFirstString(...values) {
359
+ for (const value of values) {
360
+ if (typeof value === "string" && value.trim())
361
+ return value.trim();
362
+ }
363
+ return undefined;
364
+ }
365
+ function readCodexSessionId(event, depth = 0) {
366
+ if (depth > 4)
367
+ return null;
368
+ for (const key of ["session_id", "sessionId"]) {
369
+ const value = event[key];
370
+ if (typeof value === "string" && value.trim())
371
+ return value;
372
+ }
373
+ const id = event.id;
374
+ if (typeof id === "string" && isUuidLike(id))
375
+ return id;
376
+ for (const key of ["payload", "item", "message", "response"]) {
377
+ const value = event[key];
378
+ if (isRecord(value)) {
379
+ const nested = readCodexSessionId(value, depth + 1);
380
+ if (nested)
381
+ return nested;
382
+ }
383
+ }
384
+ return null;
385
+ }
386
+ function isUuidLike(value) {
387
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
388
+ }
389
+ function readCodexMessage(event) {
390
+ for (const key of ["last_message", "message", "summary", "output", "text", "content"]) {
391
+ const value = event[key];
392
+ if (typeof value === "string" && value.trim())
393
+ return value.trim();
394
+ }
395
+ if (isRecord(event.item)) {
396
+ return readCodexMessage(event.item);
397
+ }
398
+ return null;
399
+ }
400
+ function readCodexToolCall(event) {
401
+ const type = String(event.type ?? event.kind ?? "");
402
+ if (!/tool|command|exec|patch/i.test(type))
403
+ return null;
404
+ const tool = String(event.tool ?? event.name ?? event.command ?? type);
405
+ return {
406
+ tool,
407
+ input: isRecord(event.input) ? event.input : {},
408
+ output: typeof event.output === "string" ? event.output : "",
409
+ status: String(event.status ?? "success").includes("error") ? "error" : "success"
410
+ };
411
+ }
412
+ function readChangedFiles(event) {
413
+ const value = event.files_changed ?? event.filesChanged ?? event.changed_files;
414
+ if (Array.isArray(value))
415
+ return value.filter((item) => typeof item === "string");
416
+ if (typeof event.path === "string" && /patch|edit|write/i.test(String(event.type ?? "")))
417
+ return [event.path];
418
+ return [];
419
+ }
420
+ function sumKnownProxyCosts(records) {
421
+ const knownCosts = records
422
+ .map((record) => record.costUsd)
423
+ .filter((cost) => typeof cost === "number" && Number.isFinite(cost) && cost >= 0);
424
+ if (!knownCosts.length)
425
+ return undefined;
426
+ return Number(knownCosts.reduce((sum, cost) => sum + cost, 0).toFixed(8));
427
+ }
428
+ function sumProxyUsage(records) {
429
+ if (!records.length)
430
+ return undefined;
431
+ return records.reduce((sum, record) => ({
432
+ inputTokens: sum.inputTokens + record.usage.inputTokens,
433
+ outputTokens: sum.outputTokens + record.usage.outputTokens,
434
+ totalTokens: sum.totalTokens + record.usage.totalTokens,
435
+ cacheReadInputTokens: (sum.cacheReadInputTokens ?? 0) + (record.usage.cacheReadInputTokens ?? 0)
436
+ }), { inputTokens: 0, outputTokens: 0, totalTokens: 0, cacheReadInputTokens: 0 });
437
+ }
@@ -0,0 +1,67 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { resolveExactProviderForTool, resolveProviderForTool } from "../../core/runner/fallback.js";
3
+ import { buildRunnerEnv, cleanupTemporaryFiles, getToolBaseUrl, writeTemporaryClaudeSettings } from "../../core/runner/tool-runner.js";
4
+ import { resolveToolCommand } from "../../core/runner/command-resolver.js";
5
+ export async function prepareEngineRuntime(tool, providerName, modelName, permissionMode) {
6
+ // Telegram resolves and orders fallback candidates before invoking an
7
+ // engine. Once a candidate is selected, execute it exactly so the engine
8
+ // cannot perform a second, conflicting provider fallback.
9
+ const { provider, model } = providerName
10
+ ? await resolveExactProviderForTool(tool, providerName, modelName)
11
+ : await resolveProviderForTool(tool, providerName, modelName);
12
+ const command = resolveWorkingCommand(tool);
13
+ if (!command) {
14
+ throw new Error(`Failed to find ${tool} CLI. Run \`ai doctor\` to check local CLI installation.`);
15
+ }
16
+ const env = {
17
+ ...buildRunnerEnv(tool, provider, model),
18
+ AI_GATEWAY_SOURCE: "telegram"
19
+ };
20
+ const temporaryFiles = [];
21
+ const claudeSettingsPath = tool === "claude"
22
+ ? writeTemporaryClaudeSettings(provider, { permissionMode: permissionMode === "readOnly" ? "readOnly" : "provider" })
23
+ : null;
24
+ if (claudeSettingsPath)
25
+ temporaryFiles.push(claudeSettingsPath);
26
+ return { provider, model, env, command, temporaryFiles, claudeSettingsPath };
27
+ }
28
+ export function cleanupRuntime(runtime) {
29
+ cleanupTemporaryFiles(runtime.temporaryFiles);
30
+ }
31
+ export function resolveWorkingCommand(tool) {
32
+ return resolveToolCommand(tool);
33
+ }
34
+ export function createCommandPermissionRequest(tool, command) {
35
+ return {
36
+ id: randomUUID(),
37
+ tool,
38
+ description: `Confirm command before running in Telegram ${tool}.`,
39
+ command,
40
+ params: { command }
41
+ };
42
+ }
43
+ export function summarizeRuntime(provider, model, tool) {
44
+ return {
45
+ provider: provider.name,
46
+ model,
47
+ baseUrl: getToolBaseUrl(tool, provider)
48
+ };
49
+ }
50
+ export function formatEngineTimeout(engineLabel, timeoutMs, providerName, model) {
51
+ return [
52
+ `${engineLabel} 请求超时,已自动停止。`,
53
+ timeoutMs ? `超时时间:${formatDuration(timeoutMs)}` : "",
54
+ providerName && model ? `Provider:${providerName} / ${model}` : "",
55
+ "这通常是 CLI 子进程、provider 上游或网络连接卡住导致的。"
56
+ ].filter(Boolean).join("\n");
57
+ }
58
+ function formatDuration(ms) {
59
+ if (ms < 1000)
60
+ return `${ms}ms`;
61
+ const seconds = Math.round(ms / 1000);
62
+ if (seconds < 60)
63
+ return `${seconds}s`;
64
+ const minutes = Math.floor(seconds / 60);
65
+ const remaining = seconds % 60;
66
+ return remaining ? `${minutes}m ${remaining}s` : `${minutes}m`;
67
+ }
@@ -0,0 +1,132 @@
1
+ import { spawn } from "node:child_process";
2
+ import { StringDecoder } from "node:string_decoder";
3
+ export async function spawnCapture(options, onProcess) {
4
+ const child = spawn(options.command, options.args, {
5
+ cwd: options.cwd,
6
+ env: options.env,
7
+ stdio: ["ignore", "pipe", "pipe"],
8
+ detached: process.platform !== "win32"
9
+ });
10
+ onProcess?.(child);
11
+ let stdout = "";
12
+ let stderr = "";
13
+ let timedOut = false;
14
+ let aborted = false;
15
+ let killTimer;
16
+ const stdoutDecoder = new StringDecoder("utf8");
17
+ const stderrDecoder = new StringDecoder("utf8");
18
+ child.stdout?.on("data", (chunk) => {
19
+ const text = stdoutDecoder.write(chunk);
20
+ stdout += text;
21
+ void options.onStdout?.(text);
22
+ });
23
+ child.stderr?.on("data", (chunk) => {
24
+ const text = stderrDecoder.write(chunk);
25
+ stderr += text;
26
+ void options.onStderr?.(text);
27
+ });
28
+ const requestTermination = (reason) => {
29
+ if (reason === "timeout")
30
+ timedOut = true;
31
+ if (reason === "abort")
32
+ aborted = true;
33
+ terminateProcessTree(child, "SIGTERM");
34
+ killTimer ??= setTimeout(() => terminateProcessTree(child, "SIGKILL"), options.killGraceMs ?? 3000);
35
+ killTimer.unref?.();
36
+ };
37
+ const abortHandler = () => requestTermination("abort");
38
+ if (options.signal?.aborted) {
39
+ requestTermination("abort");
40
+ }
41
+ else {
42
+ options.signal?.addEventListener("abort", abortHandler, { once: true });
43
+ }
44
+ let timeout;
45
+ if (options.timeoutMs && options.timeoutMs > 0) {
46
+ timeout = setTimeout(() => requestTermination("timeout"), options.timeoutMs);
47
+ timeout.unref?.();
48
+ }
49
+ try {
50
+ const [code, signal] = await new Promise((resolve, reject) => {
51
+ child.once("error", reject);
52
+ child.once("close", (code, signal) => resolve([code, signal]));
53
+ });
54
+ stdout += stdoutDecoder.end();
55
+ stderr += stderrDecoder.end();
56
+ return { stdout, stderr, code, signal, timedOut, aborted };
57
+ }
58
+ finally {
59
+ if (timeout)
60
+ clearTimeout(timeout);
61
+ if (killTimer)
62
+ clearTimeout(killTimer);
63
+ options.signal?.removeEventListener("abort", abortHandler);
64
+ }
65
+ }
66
+ export function terminateProcessTree(child, signal) {
67
+ const pid = child.pid;
68
+ if (!pid)
69
+ return;
70
+ try {
71
+ if (process.platform !== "win32") {
72
+ process.kill(-pid, signal);
73
+ return;
74
+ }
75
+ }
76
+ catch {
77
+ // Fall back to killing the direct child below.
78
+ }
79
+ try {
80
+ child.kill(signal);
81
+ }
82
+ catch {
83
+ // The process may have already exited.
84
+ }
85
+ }
86
+ export function extractJsonLines(raw) {
87
+ const events = [];
88
+ for (const line of raw.split("\n")) {
89
+ const trimmed = line.trim();
90
+ if (!trimmed)
91
+ continue;
92
+ try {
93
+ events.push(JSON.parse(trimmed));
94
+ }
95
+ catch {
96
+ // Ignore non-JSON progress lines.
97
+ }
98
+ }
99
+ return events;
100
+ }
101
+ export function createJsonLineAccumulator(onEvent) {
102
+ let pending = "";
103
+ const emitLine = (line) => {
104
+ const trimmed = line.trim();
105
+ if (!trimmed)
106
+ return;
107
+ try {
108
+ onEvent(JSON.parse(trimmed));
109
+ }
110
+ catch {
111
+ // Ignore non-JSON progress lines. The complete stdout remains available
112
+ // to the final result/error parser.
113
+ }
114
+ };
115
+ return {
116
+ push(chunk) {
117
+ pending += chunk;
118
+ const lines = pending.split("\n");
119
+ pending = lines.pop() ?? "";
120
+ for (const line of lines)
121
+ emitLine(line);
122
+ },
123
+ end() {
124
+ emitLine(pending);
125
+ pending = "";
126
+ }
127
+ };
128
+ }
129
+ export function stripAnsi(value) {
130
+ // eslint-disable-next-line no-control-regex
131
+ return value.replace(/\u001b(?:\[[0-9;?]*[A-Za-z]|\][^\u0007]*\u0007|[()][A-Z0-9]|[ -\/]*[0-~])/g, "");
132
+ }
@@ -0,0 +1,31 @@
1
+ import { ClaudeEngine } from "./claude-engine.js";
2
+ import { CodexEngine } from "./codex-engine.js";
3
+ const factories = new Map();
4
+ export function registerTelegramEngine(name, factory) {
5
+ factories.set(name, factory);
6
+ }
7
+ export function createTelegramEngines() {
8
+ return {
9
+ claude: createTelegramEngine("claude"),
10
+ codex: createTelegramEngine("codex")
11
+ };
12
+ }
13
+ export function createTelegramEngine(name) {
14
+ const factory = factories.get(name);
15
+ if (!factory) {
16
+ throw new Error(`Unknown Telegram engine: ${name}`);
17
+ }
18
+ return factory();
19
+ }
20
+ export function getTelegramEngine(engines, name) {
21
+ const engine = engines[name];
22
+ if (!engine) {
23
+ throw new Error(`Telegram engine is not registered: ${name}`);
24
+ }
25
+ return engine;
26
+ }
27
+ export function listTelegramEngineNames() {
28
+ return [...factories.keys()];
29
+ }
30
+ registerTelegramEngine("claude", () => new ClaudeEngine());
31
+ registerTelegramEngine("codex", () => new CodexEngine());
@@ -0,0 +1 @@
1
+ export {};