@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,527 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { readCache } from "../../core/config/cache-store.js";
5
+ import { listProviders } from "../../core/providers/provider-service.js";
6
+ import { resolveProviderForTool } from "../../core/runner/fallback.js";
7
+ import { updateTelegramConfig } from "../config.js";
8
+ import { safeSetChatCommandMenu } from "../interaction/command-menu.js";
9
+ import { Formatter } from "../interaction/formatter.js";
10
+ import { buildAgentProfileBackKeyboard, buildAgentProfileKeyboard, buildModelPickerKeyboard, buildProviderPickerKeyboard, buildReadyKeyboard, buildReadyModeKeyboard, buildWorkdirListKeyboard, buildWorkdirStartKeyboard } from "../interaction/keyboards.js";
11
+ import { buildFullProfilePrompt, buildPersonaPrompt, splitProfileAndPersona } from "../memory/memory-service.js";
12
+ export const AGENT_PROFILE_GENERATION_TIMEOUT_MS = 3 * 60 * 1000;
13
+ export async function startSetup(ctx, deps) {
14
+ const chatId = ctx.chat?.id;
15
+ if (!chatId)
16
+ return;
17
+ deps.sessionManager.startSetup(chatId);
18
+ await showSetupProviderPicker(ctx, deps);
19
+ }
20
+ export async function showSetupProviderPicker(ctx, deps, options = {}) {
21
+ const chatId = ctx.chat?.id;
22
+ if (!chatId)
23
+ return;
24
+ const session = deps.sessionManager.getOrCreate(chatId);
25
+ if (session.setupStep !== "ready") {
26
+ deps.sessionManager.update(session.id, { setupStep: "model" });
27
+ }
28
+ const choices = await getProviderChoices(options.forcedEngine);
29
+ const extra = {
30
+ parse_mode: "HTML",
31
+ reply_markup: buildProviderPickerKeyboard(choices.map((choice) => choice.provider.name), session.setupStep === "ready" ? "ready" : "setup", options.forcedEngine)
32
+ };
33
+ const text = choices.length
34
+ ? Formatter.modelPrompt(options.reselect)
35
+ : "🤖 没有可用的 Provider / Model";
36
+ if (options.edit) {
37
+ try {
38
+ await ctx.editMessageText(text, extra);
39
+ return;
40
+ }
41
+ catch {
42
+ // The original message may no longer be editable; fall back to a fresh prompt.
43
+ }
44
+ }
45
+ await ctx.reply(text, extra);
46
+ }
47
+ export async function showSetupModelPicker(ctx, deps, providerIndex, options = {}) {
48
+ const choices = await getProviderChoices(options.forcedEngine);
49
+ const choice = choices[providerIndex];
50
+ if (!choice) {
51
+ await ctx.answerCallbackQuery?.({ text: "Provider 不存在" }).catch(() => undefined);
52
+ await showSetupProviderPicker(ctx, deps, { edit: true, forcedEngine: options.forcedEngine });
53
+ return;
54
+ }
55
+ if (!choice.models.length) {
56
+ await ctx.answerCallbackQuery?.({ text: "没有可用模型" }).catch(() => undefined);
57
+ await showSetupProviderPicker(ctx, deps, { edit: true, forcedEngine: options.forcedEngine });
58
+ return;
59
+ }
60
+ await editOrReply(ctx, `🤖 选择 ${choice.provider.name} 的模型吧`, {
61
+ parse_mode: "HTML",
62
+ reply_markup: buildModelPickerKeyboard(choice.models, providerIndex, "setup", options.forcedEngine)
63
+ });
64
+ }
65
+ export async function selectSetupModel(ctx, deps, providerIndex, modelIndex, options = {}) {
66
+ const chatId = ctx.chat?.id;
67
+ if (!chatId)
68
+ return;
69
+ const session = deps.sessionManager.getOrCreate(chatId);
70
+ const choices = await getProviderChoices(options.forcedEngine);
71
+ const choice = choices[providerIndex];
72
+ const model = choice?.models[modelIndex];
73
+ if (!choice || !model) {
74
+ await ctx.answerCallbackQuery?.({ text: "模型不存在" }).catch(() => undefined);
75
+ await showSetupProviderPicker(ctx, deps, { edit: true, forcedEngine: options.forcedEngine });
76
+ return;
77
+ }
78
+ const engine = options.forcedEngine ?? inferEngineFromModel(model);
79
+ await updateRuntimeTelegramConfig(deps, {
80
+ provider: choice.provider.name,
81
+ model,
82
+ defaultEngine: engine
83
+ });
84
+ deps.sessionManager.update(session.id, {
85
+ engine,
86
+ engineSessionId: null,
87
+ botContextFingerprint: null,
88
+ setupStep: "workdir"
89
+ });
90
+ await showWorkdirStart(ctx, deps, { edit: true });
91
+ }
92
+ export async function showWorkdirStart(ctx, deps, options = {}) {
93
+ const chatId = ctx.chat?.id;
94
+ if (!chatId)
95
+ return;
96
+ const workdirs = deps.sessionManager.getWorkdirs(chatId);
97
+ const text = options.prompt ?? Formatter.workdirList(workdirs);
98
+ const extra = {
99
+ parse_mode: "HTML",
100
+ reply_markup: buildWorkdirStartKeyboard(workdirs, options.scope ?? "setup")
101
+ };
102
+ if (options.edit) {
103
+ try {
104
+ await ctx.editMessageText(text, extra);
105
+ return;
106
+ }
107
+ catch {
108
+ // The original message may no longer be editable; fall back to a fresh prompt.
109
+ }
110
+ }
111
+ await ctx.reply(text, extra);
112
+ }
113
+ export async function showWorkdirList(ctx, deps, options = {}) {
114
+ const chatId = ctx.chat?.id;
115
+ if (!chatId)
116
+ return;
117
+ const workdirs = deps.sessionManager.getWorkdirs(chatId);
118
+ const extra = {
119
+ parse_mode: "HTML",
120
+ reply_markup: buildWorkdirListKeyboard(workdirs, options.scope ?? "setup")
121
+ };
122
+ if (options.edit) {
123
+ try {
124
+ await ctx.editMessageText(Formatter.workdirList(workdirs), extra);
125
+ return;
126
+ }
127
+ catch {
128
+ // The original message may no longer be editable; fall back to a fresh prompt.
129
+ }
130
+ }
131
+ await ctx.reply(Formatter.workdirList(workdirs), extra);
132
+ }
133
+ export async function askCustomWorkdir(ctx, deps, options = {}) {
134
+ const chatId = ctx.chat?.id;
135
+ if (!chatId)
136
+ return;
137
+ const session = deps.sessionManager.getOrCreate(chatId);
138
+ deps.sessionManager.update(session.id, { setupStep: options.returnToReady ? "edit_custom_workdir" : "custom_workdir" });
139
+ const extra = { parse_mode: "HTML" };
140
+ if (options.edit) {
141
+ try {
142
+ await ctx.editMessageText(Formatter.customWorkdirPrompt(), extra);
143
+ return;
144
+ }
145
+ catch {
146
+ // The original message may no longer be editable; fall back to a fresh prompt.
147
+ }
148
+ }
149
+ await ctx.reply(Formatter.customWorkdirPrompt(), extra);
150
+ }
151
+ export async function setWorkdir(ctx, deps, rawPath, options = {}) {
152
+ const chatId = ctx.chat?.id;
153
+ if (!chatId)
154
+ return null;
155
+ const session = deps.sessionManager.getOrCreate(chatId);
156
+ const cwd = options.fromCustomInput ? resolveHomeChild(rawPath) : expandHome(rawPath.trim());
157
+ if (!cwd || cwd === os.homedir()) {
158
+ await ctx.reply(Formatter.workdirRequired(), { parse_mode: "HTML" });
159
+ return null;
160
+ }
161
+ fs.mkdirSync(cwd, { recursive: true });
162
+ const next = deps.sessionManager.update(session.id, {
163
+ cwd,
164
+ engineSessionId: null,
165
+ botContextFingerprint: null,
166
+ setupStep: options.returnToReady ? "ready" : "agent_profile"
167
+ });
168
+ deps.sessionManager.rememberWorkdir(chatId, cwd);
169
+ await options.afterSet?.(next);
170
+ if (deps.bot) {
171
+ const customCommands = deps.sessionManager.listCustomCommands(chatId, cwd);
172
+ await safeSetChatCommandMenu(deps.bot, chatId, next.engine, customCommands);
173
+ }
174
+ if (options.returnToReady) {
175
+ await showReadyCard(ctx, deps, next, { edit: options.edit });
176
+ return next;
177
+ }
178
+ await showAgentProfilePrompt(ctx, { edit: options.edit });
179
+ return next;
180
+ }
181
+ export async function selectCli(ctx, deps, engine) {
182
+ const chatId = ctx.chat?.id;
183
+ if (!chatId)
184
+ return;
185
+ const session = deps.sessionManager.getOrCreate(chatId);
186
+ await showSetupProviderPicker(ctx, deps, {
187
+ forcedEngine: engine,
188
+ reselect: true,
189
+ edit: session.setupStep === "ready"
190
+ });
191
+ }
192
+ export async function showAgentProfilePrompt(ctx, options = {}) {
193
+ const extra = {
194
+ parse_mode: "HTML",
195
+ reply_markup: buildAgentProfileKeyboard()
196
+ };
197
+ if (options.edit) {
198
+ try {
199
+ await ctx.editMessageText(Formatter.agentProfilePrompt(), extra);
200
+ return;
201
+ }
202
+ catch {
203
+ // The original message may no longer be editable; fall back to a fresh prompt.
204
+ }
205
+ }
206
+ await ctx.reply(Formatter.agentProfilePrompt(), extra);
207
+ }
208
+ export async function completeAgentProfile(ctx, deps, description, options = {}) {
209
+ const chatId = ctx.chat?.id;
210
+ if (!chatId)
211
+ return null;
212
+ const session = deps.sessionManager.getOrCreate(chatId);
213
+ const rawDescription = description?.trim();
214
+ if (rawDescription && deps.memoryService) {
215
+ await saveGeneratedProfileMemory(ctx, deps, session, rawDescription);
216
+ }
217
+ const next = deps.sessionManager.update(session.id, { setupStep: "ready" });
218
+ if (deps.bot) {
219
+ const customCommands = deps.sessionManager.listCustomCommands(chatId, next.cwd);
220
+ await safeSetChatCommandMenu(deps.bot, chatId, next.engine, customCommands);
221
+ }
222
+ if (options.showReady !== false) {
223
+ await showReadyCard(ctx, deps, next, { edit: options.edit });
224
+ }
225
+ return next;
226
+ }
227
+ export async function remindSetup(ctx, deps, session) {
228
+ if (session.setupStep === "model") {
229
+ await showSetupProviderPicker(ctx, deps);
230
+ return true;
231
+ }
232
+ if (session.setupStep === "workdir" || session.setupStep === "custom_workdir" || session.setupStep === "edit_custom_workdir") {
233
+ await ctx.reply(Formatter.workdirRequired(), { parse_mode: "HTML" });
234
+ return true;
235
+ }
236
+ if (session.setupStep === "cli") {
237
+ await showSetupProviderPicker(ctx, deps);
238
+ return true;
239
+ }
240
+ if (session.setupStep === "agent_profile") {
241
+ await ctx.reply(Formatter.agentProfilePrompt(), {
242
+ parse_mode: "HTML",
243
+ reply_markup: buildAgentProfileKeyboard()
244
+ });
245
+ return true;
246
+ }
247
+ return false;
248
+ }
249
+ export async function showReadyCard(ctx, deps, session, options = {}) {
250
+ const resolved = await getResolvedProvider(session.engine, deps.config);
251
+ const extra = buildConfigExtra(session, resolved.provider);
252
+ if (options.edit) {
253
+ try {
254
+ await ctx.editMessageText(Formatter.sessionCard(session, resolved.provider, resolved.error), extra);
255
+ return;
256
+ }
257
+ catch {
258
+ // The original message may no longer be editable; fall back to a fresh card.
259
+ }
260
+ }
261
+ await ctx.reply(Formatter.sessionCard(session, resolved.provider, resolved.error), extra);
262
+ }
263
+ export async function showConfigPanel(ctx, deps, session, options = {}) {
264
+ const resolved = await getResolvedProvider(session.engine, deps.config);
265
+ const extra = buildConfigExtra(session, resolved.provider);
266
+ if (options.edit) {
267
+ try {
268
+ await ctx.editMessageText(Formatter.configPanel(), extra);
269
+ return;
270
+ }
271
+ catch {
272
+ // The original message may no longer be editable; fall back to a fresh panel.
273
+ }
274
+ }
275
+ await ctx.reply(Formatter.configPanel(), extra);
276
+ }
277
+ export async function showAgentProfile(ctx, deps) {
278
+ const chatId = ctx.chat?.id;
279
+ if (!chatId)
280
+ return;
281
+ const content = deps.memoryService
282
+ ? formatProfileMemory(deps.memoryService, chatId)
283
+ : "暂未启用 Bot Memory。";
284
+ await editOrReply(ctx, Formatter.agentProfile(content), {
285
+ parse_mode: "HTML",
286
+ reply_markup: buildAgentProfileBackKeyboard()
287
+ });
288
+ }
289
+ export async function showReadyCliPicker(ctx, deps) {
290
+ const chatId = ctx.chat?.id;
291
+ if (!chatId)
292
+ return;
293
+ const session = deps.sessionManager.getOrCreate(chatId);
294
+ await showReadyProviderPicker(ctx, deps, { forcedEngine: session.engine, reselect: true });
295
+ }
296
+ export async function selectReadyCli(ctx, deps, engine) {
297
+ const chatId = ctx.chat?.id;
298
+ if (!chatId)
299
+ return;
300
+ await showReadyProviderPicker(ctx, deps, { forcedEngine: engine, reselect: true });
301
+ }
302
+ export async function showReadyProviderPicker(ctx, deps, options = {}) {
303
+ const chatId = ctx.chat?.id;
304
+ if (!chatId)
305
+ return;
306
+ const choices = await getProviderChoices(options.forcedEngine);
307
+ if (!choices.length) {
308
+ await editOrReply(ctx, "🤖 没有可用的 Provider / Model", {
309
+ parse_mode: "HTML",
310
+ reply_markup: buildProviderPickerKeyboard([], "ready", options.forcedEngine)
311
+ });
312
+ return;
313
+ }
314
+ await editOrReply(ctx, Formatter.modelPrompt(options.reselect), {
315
+ parse_mode: "HTML",
316
+ reply_markup: buildProviderPickerKeyboard(choices.map((choice) => choice.provider.name), "ready", options.forcedEngine)
317
+ });
318
+ }
319
+ export async function showReadyModelPicker(ctx, deps, providerIndex, options = {}) {
320
+ const chatId = ctx.chat?.id;
321
+ if (!chatId)
322
+ return;
323
+ const choices = await getProviderChoices(options.forcedEngine);
324
+ const choice = choices[providerIndex];
325
+ if (!choice) {
326
+ await ctx.answerCallbackQuery?.({ text: "Provider 不存在" }).catch(() => undefined);
327
+ await showReadyProviderPicker(ctx, deps, { forcedEngine: options.forcedEngine });
328
+ return;
329
+ }
330
+ if (!choice.models.length) {
331
+ await ctx.answerCallbackQuery?.({ text: "没有可用模型" }).catch(() => undefined);
332
+ await showReadyProviderPicker(ctx, deps, { forcedEngine: options.forcedEngine });
333
+ return;
334
+ }
335
+ await editOrReply(ctx, `🤖 选择 ${choice.provider.name} 的默认模型吧`, {
336
+ parse_mode: "HTML",
337
+ reply_markup: buildModelPickerKeyboard(choice.models, providerIndex, "ready", options.forcedEngine)
338
+ });
339
+ }
340
+ export async function selectReadyModel(ctx, deps, providerIndex, modelIndex, options = {}) {
341
+ const chatId = ctx.chat?.id;
342
+ if (!chatId)
343
+ return;
344
+ const session = deps.sessionManager.getOrCreate(chatId);
345
+ const choices = await getProviderChoices(options.forcedEngine);
346
+ const choice = choices[providerIndex];
347
+ const model = choice?.models[modelIndex];
348
+ if (!choice || !model) {
349
+ await ctx.answerCallbackQuery?.({ text: "模型不存在" }).catch(() => undefined);
350
+ await showReadyProviderPicker(ctx, deps, { forcedEngine: options.forcedEngine });
351
+ return;
352
+ }
353
+ const engine = options.forcedEngine ?? inferEngineFromModel(model);
354
+ await updateRuntimeTelegramConfig(deps, {
355
+ provider: choice.provider.name,
356
+ model,
357
+ defaultEngine: engine
358
+ });
359
+ const next = deps.sessionManager.update(session.id, {
360
+ engine,
361
+ engineSessionId: null,
362
+ botContextFingerprint: null,
363
+ setupStep: "ready"
364
+ });
365
+ if (deps.bot) {
366
+ const customCommands = deps.sessionManager.listCustomCommands(chatId, next.cwd);
367
+ await safeSetChatCommandMenu(deps.bot, chatId, engine, customCommands);
368
+ }
369
+ await showReadyCard(ctx, deps, next, { edit: true });
370
+ }
371
+ export async function showReadyModePicker(ctx) {
372
+ await editOrReply(ctx, "🤖 选择权限模式吧", {
373
+ parse_mode: "HTML",
374
+ reply_markup: buildReadyModeKeyboard()
375
+ });
376
+ }
377
+ export async function selectReadyMode(ctx, deps, mode) {
378
+ const chatId = ctx.chat?.id;
379
+ if (!chatId)
380
+ return;
381
+ const session = deps.sessionManager.getOrCreate(chatId);
382
+ const next = deps.sessionManager.update(session.id, { permissionMode: mode, setupStep: "ready" });
383
+ await updateRuntimeTelegramConfig(deps, { defaultPermissionMode: mode });
384
+ await showReadyCard(ctx, deps, next, { edit: true });
385
+ }
386
+ async function getResolvedProvider(engine, config) {
387
+ try {
388
+ const resolved = await resolveProviderForTool(engine, config?.provider, config?.model);
389
+ return { provider: { name: resolved.provider.name, model: resolved.model } };
390
+ }
391
+ catch (error) {
392
+ return { error: error instanceof Error ? error.message : String(error) };
393
+ }
394
+ }
395
+ function buildConfigExtra(session, provider) {
396
+ const providerModel = provider
397
+ ? `${provider.name} · ${provider.model}`
398
+ : "配置 Provider / Model";
399
+ return {
400
+ parse_mode: "HTML",
401
+ reply_markup: buildReadyKeyboard({
402
+ cli: session.engine === "claude" ? "Claude Code" : "Codex",
403
+ providerModel,
404
+ permissionMode: session.permissionMode,
405
+ cwd: session.cwd
406
+ })
407
+ };
408
+ }
409
+ async function getProviderChoices(engine) {
410
+ const [providers, cache] = await Promise.all([listProviders(), readCache()]);
411
+ return providers
412
+ .map((provider) => ({
413
+ provider,
414
+ models: getSelectableModels(provider, cache.providers[provider.name]?.models ?? [], engine)
415
+ }))
416
+ .filter((choice) => choice.models.length > 0);
417
+ }
418
+ function getSelectableModels(provider, cachedModels, engine) {
419
+ const models = [
420
+ provider.model,
421
+ ...cachedModels
422
+ ].filter((model) => Boolean(model));
423
+ const unique = [...new Set(models)];
424
+ if (!engine)
425
+ return unique;
426
+ return unique.filter((model) => inferEngineFromModel(model) === engine);
427
+ }
428
+ export function inferEngineFromModel(model) {
429
+ return isGptLikeModel(model) ? "codex" : "claude";
430
+ }
431
+ function isGptLikeModel(model) {
432
+ const value = model.toLowerCase();
433
+ return value.includes("gpt")
434
+ || value.includes("codex")
435
+ || value.includes("openai")
436
+ || /^o\d/.test(value);
437
+ }
438
+ async function updateRuntimeTelegramConfig(deps, patch) {
439
+ const updated = deps.updateConfig
440
+ ? await deps.updateConfig(patch)
441
+ : await updateTelegramConfig(patch);
442
+ if (deps.config) {
443
+ Object.assign(deps.config, updated);
444
+ }
445
+ }
446
+ async function editOrReply(ctx, text, extra) {
447
+ try {
448
+ await ctx.editMessageText(text, extra);
449
+ return;
450
+ }
451
+ catch {
452
+ // The original message may no longer be editable; fall back to a fresh message.
453
+ }
454
+ await ctx.reply(text, extra);
455
+ }
456
+ async function saveGeneratedProfileMemory(ctx, deps, session, rawDescription) {
457
+ const memoryService = deps.memoryService;
458
+ if (!memoryService)
459
+ return;
460
+ memoryService.saveRawDescription(session.chatId, rawDescription);
461
+ const existingProfile = memoryService.getProfile(session.chatId);
462
+ if (existingProfile) {
463
+ const prompt = buildPersonaPrompt(rawDescription, existingProfile.content);
464
+ const generated = await generateProfileText(ctx, deps, session, prompt);
465
+ memoryService.savePersona(session.chatId, generated || rawDescription);
466
+ return;
467
+ }
468
+ const prompt = buildFullProfilePrompt(rawDescription);
469
+ const generated = await generateProfileText(ctx, deps, session, prompt);
470
+ if (!generated) {
471
+ memoryService.saveProfile(session.chatId, rawDescription);
472
+ return;
473
+ }
474
+ const { profile, persona } = splitProfileAndPersona(generated);
475
+ memoryService.saveProfile(session.chatId, profile || rawDescription);
476
+ if (persona) {
477
+ memoryService.savePersona(session.chatId, persona);
478
+ }
479
+ }
480
+ async function generateProfileText(ctx, deps, session, prompt) {
481
+ const engine = resolveSetupEngine(deps, session.engine);
482
+ if (!engine)
483
+ return null;
484
+ try {
485
+ const result = await engine.execute({
486
+ chatId: ctx.chat?.id ?? session.chatId,
487
+ prompt,
488
+ cwd: session.cwd,
489
+ resume: null,
490
+ permissionMode: session.permissionMode,
491
+ providerName: deps.config?.provider,
492
+ modelName: deps.config?.model,
493
+ timeoutMs: AGENT_PROFILE_GENERATION_TIMEOUT_MS,
494
+ onPermission: async () => true
495
+ });
496
+ return result.response.trim() || null;
497
+ }
498
+ catch {
499
+ return null;
500
+ }
501
+ }
502
+ function resolveSetupEngine(deps, engine) {
503
+ if (deps.engines?.[engine])
504
+ return deps.engines[engine];
505
+ return engine === "codex" ? deps.codexEngine : deps.claudeEngine;
506
+ }
507
+ function formatProfileMemory(memoryService, chatId) {
508
+ const profile = memoryService.getProfile(chatId);
509
+ const persona = memoryService.getPersona(chatId);
510
+ if (!profile && !persona)
511
+ return "暂未生成记忆画像。";
512
+ return [
513
+ profile ? ["# 用户画像", "", profile.content].join("\n") : "",
514
+ persona ? ["# Bot 人设", "", persona.content].join("\n") : ""
515
+ ].filter(Boolean).join("\n\n");
516
+ }
517
+ function resolveHomeChild(value) {
518
+ const name = value.trim().replace(/^~\//, "").replace(/^\/+/, "");
519
+ return path.join(os.homedir(), name);
520
+ }
521
+ function expandHome(value) {
522
+ if (value === "~")
523
+ return os.homedir();
524
+ if (value.startsWith("~/"))
525
+ return path.join(os.homedir(), value.slice(2));
526
+ return value;
527
+ }
@@ -0,0 +1,124 @@
1
+ import { resolveProviderForTool } from "../../core/runner/fallback.js";
2
+ import { estimateTokenCostUsdFromConfig, upsertModelPrice } from "../../core/usage/pricing.js";
3
+ import { Formatter, formatUsagePriceInvalid, formatUsagePricePrompt, formatUsagePriceSaved } from "../interaction/formatter.js";
4
+ import { buildUsagePricingKeyboard } from "../interaction/keyboards.js";
5
+ export async function showUsagePanel(ctx, deps, options = {}) {
6
+ const chatId = ctx.chat?.id;
7
+ if (!chatId)
8
+ return;
9
+ const session = deps.sessionManager.getOrCreate(chatId);
10
+ const resolved = await getResolvedProvider(session, deps.config);
11
+ const usage = resolved.provider ? findProviderUsage(session, resolved.provider.name) : undefined;
12
+ const estimatedCostUsd = resolved.provider && usage
13
+ ? await estimateStoredUsageCost(resolved.provider.name, resolved.provider.model, usage)
14
+ : undefined;
15
+ const missingPrice = Boolean(resolved.provider
16
+ && usage
17
+ && usage.costUsd <= 0
18
+ && usage.unknownCostCount > 0
19
+ && estimatedCostUsd === undefined
20
+ && !options.tokenOnly);
21
+ const extra = {
22
+ parse_mode: "HTML",
23
+ ...(missingPrice ? { reply_markup: buildUsagePricingKeyboard() } : {})
24
+ };
25
+ const text = Formatter.sessionUsage(session, resolved.provider, resolved.error, {
26
+ estimatedCostUsd,
27
+ tokenOnly: options.tokenOnly,
28
+ missingPrice
29
+ });
30
+ if (options.edit) {
31
+ try {
32
+ await ctx.editMessageText(text, extra);
33
+ return;
34
+ }
35
+ catch {
36
+ // The original usage message may no longer be editable.
37
+ }
38
+ }
39
+ await ctx.reply(text, extra);
40
+ }
41
+ export async function askUsagePrice(ctx, deps, options = {}) {
42
+ const chatId = ctx.chat?.id;
43
+ if (!chatId)
44
+ return;
45
+ const session = deps.sessionManager.getOrCreate(chatId);
46
+ const resolved = await getResolvedProvider(session, deps.config);
47
+ if (!resolved.provider) {
48
+ await ctx.reply(Formatter.sessionUsage(session, undefined, resolved.error), { parse_mode: "HTML" });
49
+ return;
50
+ }
51
+ deps.sessionManager.update(session.id, { setupStep: "usage_price_input" });
52
+ const text = formatUsagePricePrompt(resolved.provider);
53
+ const extra = { parse_mode: "HTML" };
54
+ if (options.edit) {
55
+ try {
56
+ await ctx.editMessageText(text, extra);
57
+ return;
58
+ }
59
+ catch {
60
+ // The original usage message may no longer be editable.
61
+ }
62
+ }
63
+ await ctx.reply(text, extra);
64
+ }
65
+ export async function handleUsagePriceInput(ctx, deps, rawText) {
66
+ const chatId = ctx.chat?.id;
67
+ if (!chatId)
68
+ return;
69
+ const session = deps.sessionManager.getOrCreate(chatId);
70
+ const resolved = await getResolvedProvider(session, deps.config);
71
+ if (!resolved.provider) {
72
+ deps.sessionManager.update(session.id, { setupStep: "ready" });
73
+ await ctx.reply(Formatter.sessionUsage(session, undefined, resolved.error), { parse_mode: "HTML" });
74
+ return;
75
+ }
76
+ const parsed = parseModelPriceInput(rawText);
77
+ if (!parsed) {
78
+ await ctx.reply(formatUsagePriceInvalid(), { parse_mode: "HTML" });
79
+ return;
80
+ }
81
+ await upsertModelPrice(resolved.provider.name, resolved.provider.model, {
82
+ inputPerMillionUsd: parsed.inputPerMillionUsd,
83
+ outputPerMillionUsd: parsed.outputPerMillionUsd,
84
+ cacheReadPerMillionUsd: parsed.cacheReadPerMillionUsd
85
+ });
86
+ deps.sessionManager.update(session.id, { setupStep: "ready" });
87
+ await ctx.reply(formatUsagePriceSaved(resolved.provider, parsed.inputPerMillionUsd, parsed.outputPerMillionUsd, parsed.cacheReadPerMillionUsd), { parse_mode: "HTML" });
88
+ await showUsagePanel(ctx, deps);
89
+ }
90
+ export function parseModelPriceInput(rawText) {
91
+ const numbers = rawText
92
+ .replace(/\$/g, "")
93
+ .match(/\d+(?:\.\d+)?/g)
94
+ ?.map((item) => Number(item))
95
+ .filter((item) => Number.isFinite(item) && item >= 0) ?? [];
96
+ if (numbers.length < 3)
97
+ return null;
98
+ return {
99
+ inputPerMillionUsd: numbers[0],
100
+ outputPerMillionUsd: numbers[1],
101
+ cacheReadPerMillionUsd: numbers[2]
102
+ };
103
+ }
104
+ async function getResolvedProvider(session, config) {
105
+ try {
106
+ const resolved = await resolveProviderForTool(session.engine, config?.provider, config?.model);
107
+ return { provider: { name: resolved.provider.name, model: resolved.model } };
108
+ }
109
+ catch (error) {
110
+ return { error: error instanceof Error ? error.message : String(error) };
111
+ }
112
+ }
113
+ function findProviderUsage(session, providerName) {
114
+ return session.providerUsage.find((item) => item.provider === providerName);
115
+ }
116
+ async function estimateStoredUsageCost(providerName, model, usage) {
117
+ const tokenUsage = {
118
+ inputTokens: usage.inputTokens,
119
+ outputTokens: usage.outputTokens,
120
+ totalTokens: usage.totalTokens,
121
+ cacheReadInputTokens: usage.cacheReadInputTokens
122
+ };
123
+ return estimateTokenCostUsdFromConfig(providerName, model, tokenUsage);
124
+ }