@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,487 @@
1
+ import { parseToolName } from "./tool-name.js";
2
+ export const TELEGRAM_MESSAGE_LIMIT = 4096;
3
+ export function escapeHtml(value) {
4
+ return value
5
+ .replace(/&/g, "&")
6
+ .replace(/</g, "&lt;")
7
+ .replace(/>/g, "&gt;");
8
+ }
9
+ export function truncateText(value, maxLength) {
10
+ if (value.length <= maxLength)
11
+ return value;
12
+ if (maxLength <= 3)
13
+ return value.slice(0, maxLength);
14
+ return `${value.slice(0, maxLength - 3)}...`;
15
+ }
16
+ export function formatCodeBlock(code, language = "") {
17
+ const className = language ? ` class="language-${escapeHtml(language)}"` : "";
18
+ return `<pre><code${className}>${escapeHtml(truncateText(code, 3500))}</code></pre>`;
19
+ }
20
+ export function markdownToTelegramHtml(value, maxLength = 2500) {
21
+ const text = truncateText((value || "(empty)").trim(), maxLength);
22
+ if (!text)
23
+ return "(empty)";
24
+ return renderMarkdownBlocks(text);
25
+ }
26
+ export function splitPlainText(value, maxLength = 3800) {
27
+ if (value.length <= maxLength)
28
+ return [value];
29
+ const chunks = [];
30
+ let remaining = value;
31
+ while (remaining.length > maxLength) {
32
+ const newlineIndex = remaining.lastIndexOf("\n", maxLength);
33
+ const splitAt = newlineIndex > maxLength * 0.6 ? newlineIndex + 1 : maxLength;
34
+ chunks.push(remaining.slice(0, splitAt));
35
+ remaining = remaining.slice(splitAt);
36
+ }
37
+ if (remaining)
38
+ chunks.push(remaining);
39
+ return chunks;
40
+ }
41
+ export const Formatter = {
42
+ modelPrompt(reselect = false) {
43
+ return reselect
44
+ ? "🤖 重新选择你要使用的大模型吧"
45
+ : "🤖 确定你要使用的大模型吧";
46
+ },
47
+ modelRequired() {
48
+ return "🤖 先确定你要使用的大模型吧";
49
+ },
50
+ workdirPrompt() {
51
+ return "🤖 你好呀,先确定工作路径吧";
52
+ },
53
+ workdirChangePrompt() {
54
+ return "🤖 输入你要设置的工作路径吧";
55
+ },
56
+ commandNamePrompt() {
57
+ return "🤖 请输入命令名称,如:/ci [type] [channel]";
58
+ },
59
+ commandTemplatePrompt(params) {
60
+ if (params.length === 0) {
61
+ return "🤖 请输入对应终端命令";
62
+ }
63
+ const example = params.map((p) => `[${p}]`).join(" ");
64
+ return `🤖 请输入对应终端命令,用 ${example} 作为占位符\n\n如:./gradlew createReleaseTag -Pmessage="[message]" -PbuildType=[type]`;
65
+ },
66
+ commandRegistered(name, description) {
67
+ return `✅ 命令已注册:<code>/${name}</code>\n用法:<code>${description}</code>`;
68
+ },
69
+ workdirRequired() {
70
+ return "🤖 请选确定工作路径哦";
71
+ },
72
+ workdirList(workdirs) {
73
+ if (!workdirs.length) {
74
+ return "🤖 还没有记录过工作路径。";
75
+ }
76
+ return "🤖 选择一个已存在的工作路径吧";
77
+ },
78
+ customWorkdirPrompt() {
79
+ return [
80
+ "✏️ <b>输入工作目录名称</b>",
81
+ "",
82
+ "我会固定创建或使用:",
83
+ "<code>~/你输入的名称</code>"
84
+ ].join("\n");
85
+ },
86
+ cliPrompt(_cwd) {
87
+ return "🤖 确定你要使用的CLI吧";
88
+ },
89
+ cliRequired() {
90
+ return "🤖 先确定你想使用的CLI哦。";
91
+ },
92
+ agentProfilePrompt() {
93
+ return "🤖 介绍一下我和你吧,可以帮你构建画像哦";
94
+ },
95
+ sessionCard(session, provider, error) {
96
+ const suffix = error && !provider ? `\n\n<i>${escapeHtml(truncateText(error, 160))}</i>` : "";
97
+ return `🤖 会话<code>${escapeHtml(session.id.slice(0, 8))}</code>已就绪,可以开始和我聊天啦${suffix}`;
98
+ },
99
+ sessionCleared(session) {
100
+ return `🤖 会话<code>${escapeHtml(session.id.slice(0, 8))}</code>聊天记录已清理`;
101
+ },
102
+ configPanel() {
103
+ return "🤖 Telegram Bot 配置";
104
+ },
105
+ agentProfile(content) {
106
+ return [
107
+ "<b>Bot Memory</b>",
108
+ "",
109
+ formatCodeBlock(content, "markdown")
110
+ ].join("\n");
111
+ },
112
+ sessionUsage(session, provider, error, options = {}) {
113
+ const providerName = provider?.name ?? "(unknown)";
114
+ const model = provider?.model ?? "(unknown)";
115
+ const usage = provider ? session.providerUsage.find((item) => item.provider === provider.name) : undefined;
116
+ if (options.tokenOnly && usage) {
117
+ return [
118
+ "🤖 当前会话 Token",
119
+ "",
120
+ `Session:<code>${escapeHtml(session.id.slice(0, 8))}</code>`,
121
+ `Provider:<code>${escapeHtml(providerName)}</code>`,
122
+ `Model:<code>${escapeHtml(model)}</code>`,
123
+ `Tokens:<code>${escapeHtml(formatTokenCount(usage.totalTokens))}</code>`
124
+ ].join("\n");
125
+ }
126
+ const lines = [
127
+ "🤖 当前会话用量",
128
+ "",
129
+ `Session:<code>${escapeHtml(session.id.slice(0, 8))}</code>`,
130
+ `Provider:<code>${escapeHtml(providerName)}</code>`,
131
+ `Model:<code>${escapeHtml(model)}</code>`,
132
+ `额度:<code>${escapeHtml(formatProviderCost(usage, options))}</code>`
133
+ ];
134
+ if (usage?.totalTokens) {
135
+ lines.push(`Tokens:<code>${escapeHtml(formatTokenCount(usage.totalTokens))}</code>`);
136
+ }
137
+ if (usage?.cacheReadInputTokens) {
138
+ lines.push(`Cache Read:<code>${escapeHtml(formatTokenCount(usage.cacheReadInputTokens))}</code>`);
139
+ }
140
+ if (options.missingPrice && usage) {
141
+ lines.push("", "当前模型还没有价格表。可以输入价格后计算额度,或选择未知只查看 token。");
142
+ }
143
+ if (error && !provider) {
144
+ lines.push("", `<i>${escapeHtml(truncateText(error, 160))}</i>`);
145
+ }
146
+ return lines.join("\n");
147
+ },
148
+ taskCompleted(engine, result, maxOutputLength = 2500) {
149
+ void engine;
150
+ return markdownToTelegramHtml(result.response || "(empty)", maxOutputLength);
151
+ },
152
+ resultCard(result) {
153
+ const lines = [];
154
+ const parts = [];
155
+ if (result.duration && result.duration > 0) {
156
+ const seconds = Math.round(result.duration / 1000);
157
+ parts.push(seconds >= 60 ? `${Math.floor(seconds / 60)}m${seconds % 60}s` : `${seconds}s`);
158
+ }
159
+ if (result.provider)
160
+ parts.push(result.provider);
161
+ if (result.model)
162
+ parts.push(result.model);
163
+ lines.push(`✅ <b>完成</b>${parts.length ? ` · ${parts.map(escapeHtml).join(" · ")}` : ""}`);
164
+ const summary = extractSummary(result.response, 200);
165
+ if (summary) {
166
+ lines.push("");
167
+ lines.push(escapeHtml(summary));
168
+ }
169
+ if (result.filesChanged?.length) {
170
+ lines.push("");
171
+ const stats = parseDiffStats(result.diff);
172
+ const header = stats
173
+ ? `📁 ${result.filesChanged.length} file(s) changed · <code>${stats}</code>`
174
+ : `📁 ${result.filesChanged.length} file(s) changed`;
175
+ lines.push(header);
176
+ const maxFiles = 6;
177
+ const shown = result.filesChanged.slice(0, maxFiles);
178
+ const fileLines = shown.map((f) => ` ${escapeHtml(f)}`);
179
+ if (result.filesChanged.length > maxFiles) {
180
+ fileLines.push(` … +${result.filesChanged.length - maxFiles} more`);
181
+ }
182
+ lines.push(`<pre>${fileLines.join("\n")}</pre>`);
183
+ }
184
+ return lines.join("\n");
185
+ },
186
+ taskFailed(error) {
187
+ return [`<b>Failed</b>`, "", `<pre>${escapeHtml(truncateText(error, 1500))}</pre>`].join("\n");
188
+ },
189
+ progress(update, options = {}) {
190
+ if (update.phase === "thinking") {
191
+ if (update.message && /fallback|切换|provider/i.test(update.message)) {
192
+ return `↻ ${escapeHtml(truncateText(update.message, 80))}`;
193
+ }
194
+ return "⏱️ 思考中…";
195
+ }
196
+ if (update.phase === "writing") {
197
+ return "✍️ 正在整理回复…";
198
+ }
199
+ if (update.phase === "tool_calling") {
200
+ const tool = parseToolName(update.tool, {
201
+ kind: update.toolKind,
202
+ mcpServer: update.mcpServer
203
+ });
204
+ const count = options.toolCount && options.toolCount > 0 ? `(第 ${options.toolCount} 个操作)` : "";
205
+ if (tool.kind === "mcp") {
206
+ const server = tool.mcpServer ? `${escapeHtml(tool.mcpServer)} / ` : "";
207
+ return `🔌 调用 MCP · <code>${server}${escapeHtml(tool.name)}</code>${count}`;
208
+ }
209
+ if (tool.kind === "command") {
210
+ return `⌨️ 执行命令 · <code>${escapeHtml(tool.name)}</code>`;
211
+ }
212
+ if (isEditingTool(tool.raw)) {
213
+ return `✏️ 编辑文件 · <code>${escapeHtml(tool.name)}</code>`;
214
+ }
215
+ return `🔧 运行 <code>${escapeHtml(tool.name)}</code>${count}`;
216
+ }
217
+ return "⏱️ 思考中…";
218
+ },
219
+ progressTimeline(entries) {
220
+ return entries.join("\n");
221
+ },
222
+ fileChangeSummary(filesChanged, diff) {
223
+ if (!filesChanged?.length)
224
+ return "";
225
+ const maxFiles = 10;
226
+ const shown = filesChanged.slice(0, maxFiles);
227
+ const lines = shown.map((f) => ` ${escapeHtml(f)}`);
228
+ if (filesChanged.length > maxFiles) {
229
+ lines.push(` … +${filesChanged.length - maxFiles} more`);
230
+ }
231
+ const stats = parseDiffStats(diff);
232
+ const header = stats
233
+ ? `📁 ${filesChanged.length} file(s) changed · <code>${stats}</code>`
234
+ : `📁 ${filesChanged.length} file(s) changed`;
235
+ return [header, `<pre>${lines.join("\n")}</pre>`].join("\n");
236
+ },
237
+ taskAborted() {
238
+ return "<b>Stopped</b>";
239
+ },
240
+ help() {
241
+ return [
242
+ "<b>AI Gateway Telegram Bot</b>",
243
+ "",
244
+ "首次使用会先选择大模型、工作路径和会话信息。",
245
+ "",
246
+ "<b>Commands</b>",
247
+ "/claude - 切换 Claude Code 并重新选择模型",
248
+ "/codex - 切换 Codex 并重新选择模型",
249
+ "/reset - 重置会话信息",
250
+ "/fresh - 清空聊天窗口",
251
+ "/cwd path - 查看或切换工作路径",
252
+ "/config - 查看或编辑 Bot 配置",
253
+ "/usage - 查看当前会话额度",
254
+ "/remember - 保存一条 Bot 记忆",
255
+ "/forget - 删除 Bot 记忆",
256
+ "/memories - 查看 Bot 记忆",
257
+ "/memory - 管理 Bot 记忆",
258
+ "/pin - 回复一条 Bot 回答并固定为关键上下文",
259
+ "/unpin - 取消固定上下文",
260
+ "/terminal command - 在当前工作目录执行终端命令",
261
+ "/restart - 重启 Bot",
262
+ "/help - 查看帮助"
263
+ ].join("\n");
264
+ },
265
+ welcome(name) {
266
+ return [
267
+ `Hello <b>${escapeHtml(name)}</b>`,
268
+ "",
269
+ "AI Gateway Bot is ready. Send /reset to reset session info."
270
+ ].join("\n");
271
+ }
272
+ };
273
+ function isEditingTool(tool) {
274
+ return /^(Write|Edit|MultiEdit|NotebookEdit|apply_patch|patch)$/i.test(tool);
275
+ }
276
+ function extractSummary(response, maxLength) {
277
+ if (!response)
278
+ return "";
279
+ const lines = response.trim().split("\n");
280
+ const contentLines = lines.filter((l) => l.trim() && !l.startsWith("#") && !l.startsWith("```"));
281
+ if (!contentLines.length)
282
+ return truncateText(lines.join(" ").trim(), maxLength);
283
+ let summary = contentLines[0].trim();
284
+ if (/[::…]$/.test(summary)) {
285
+ for (let i = 1; i < contentLines.length && summary.length < maxLength; i++) {
286
+ const line = contentLines[i].trim().replace(/^[-*\d.]+\s*/, "");
287
+ if (!line)
288
+ break;
289
+ summary += " " + line;
290
+ }
291
+ }
292
+ return truncateText(summary.trim(), maxLength);
293
+ }
294
+ function renderMarkdownBlocks(value) {
295
+ const lines = value.replace(/\r\n?/g, "\n").split("\n");
296
+ const output = [];
297
+ let index = 0;
298
+ while (index < lines.length) {
299
+ const line = lines[index];
300
+ const fence = line.match(/^\s*```([A-Za-z0-9_-]*)\s*$/);
301
+ if (fence) {
302
+ const language = fence[1] ?? "";
303
+ const codeLines = [];
304
+ index += 1;
305
+ while (index < lines.length && !/^\s*```\s*$/.test(lines[index])) {
306
+ codeLines.push(lines[index]);
307
+ index += 1;
308
+ }
309
+ if (index < lines.length)
310
+ index += 1;
311
+ output.push(formatCodeBlock(codeLines.join("\n"), language));
312
+ continue;
313
+ }
314
+ if (!line.trim()) {
315
+ output.push("");
316
+ index += 1;
317
+ continue;
318
+ }
319
+ if (isMarkdownTableStart(lines, index)) {
320
+ const tableLines = [];
321
+ while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
322
+ tableLines.push(lines[index]);
323
+ index += 1;
324
+ }
325
+ output.push(renderMarkdownTable(tableLines));
326
+ continue;
327
+ }
328
+ const heading = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/);
329
+ if (heading) {
330
+ output.push(`<b>${renderMarkdownInline(heading[1])}</b>`);
331
+ index += 1;
332
+ continue;
333
+ }
334
+ const unordered = line.match(/^(\s*)[-*+]\s+(.+)$/);
335
+ if (unordered) {
336
+ const indent = " ".repeat(Math.floor(unordered[1].length / 2));
337
+ output.push(`${indent}• ${renderMarkdownInline(unordered[2])}`);
338
+ index += 1;
339
+ continue;
340
+ }
341
+ const ordered = line.match(/^(\s*)\d+[.)]\s+(.+)$/);
342
+ if (ordered) {
343
+ const indent = " ".repeat(Math.floor(ordered[1].length / 2));
344
+ output.push(`${indent}${line.trimStart().match(/^\d+[.)]/)?.[0] ?? "1."} ${renderMarkdownInline(ordered[2])}`);
345
+ index += 1;
346
+ continue;
347
+ }
348
+ output.push(renderMarkdownInline(line));
349
+ index += 1;
350
+ }
351
+ return output.join("\n");
352
+ }
353
+ function renderMarkdownInline(value) {
354
+ const placeholders = [];
355
+ const protect = (html) => {
356
+ const token = `\u0000${placeholders.length}\u0000`;
357
+ placeholders.push(html);
358
+ return token;
359
+ };
360
+ let text = value.replace(/`([^`\n]+)`/g, (_match, code) => protect(`<code>${escapeHtml(code)}</code>`));
361
+ text = text.replace(/\[([^\]\n]+)]\((https?:\/\/[^\s)]+)\)/g, (_match, label, href) => {
362
+ const safeHref = escapeHtmlAttribute(href);
363
+ return protect(`<a href="${safeHref}">${escapeHtml(label)}</a>`);
364
+ });
365
+ text = escapeHtml(text);
366
+ text = text.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
367
+ text = text.replace(/__(.+?)__/g, "<b>$1</b>");
368
+ text = text.replace(/~~(.+?)~~/g, "<s>$1</s>");
369
+ text = text.replace(/(^|[^\*])\*([^*\n]+)\*(?!\*)/g, "$1<i>$2</i>");
370
+ return text.replace(/\u0000(\d+)\u0000/g, (_match, indexText) => placeholders[Number(indexText)] ?? "");
371
+ }
372
+ function escapeHtmlAttribute(value) {
373
+ return escapeHtml(value).replace(/"/g, "&quot;");
374
+ }
375
+ function isMarkdownTableStart(lines, index) {
376
+ const current = lines[index];
377
+ const next = lines[index + 1];
378
+ return Boolean(current?.includes("|") &&
379
+ next &&
380
+ /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(next));
381
+ }
382
+ function renderMarkdownTable(lines) {
383
+ const rows = lines
384
+ .filter((_line, index) => index !== 1)
385
+ .map(parseMarkdownTableRow)
386
+ .filter((row) => row.length > 0);
387
+ if (!rows.length)
388
+ return "";
389
+ const headers = rows[0] ?? [];
390
+ const dataRows = rows.slice(1);
391
+ if (!headers.length || !dataRows.length) {
392
+ return rows.map((row) => row.map(renderMarkdownInline).join(" / ")).join("\n");
393
+ }
394
+ return dataRows
395
+ .map((row, rowIndex) => renderMarkdownTableItem(headers, row, rowIndex))
396
+ .filter(Boolean)
397
+ .join("\n\n");
398
+ }
399
+ function parseMarkdownTableRow(line) {
400
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
401
+ return trimmed.split("|").map((cell) => cell.trim());
402
+ }
403
+ function renderMarkdownTableItem(headers, row, rowIndex) {
404
+ const title = row[0]?.trim() || `${stripInlineMarkdown(headers[0] ?? "项目")}${rowIndex + 1}`;
405
+ const lines = [`<b>${renderMarkdownInline(title)}</b>`];
406
+ headers.slice(1).forEach((header, index) => {
407
+ const value = row[index + 1]?.trim();
408
+ if (!value)
409
+ return;
410
+ lines.push(`${escapeHtml(stripInlineMarkdown(header))}:${renderMarkdownInline(value)}`);
411
+ });
412
+ return lines.join("\n");
413
+ }
414
+ function stripInlineMarkdown(value) {
415
+ return value
416
+ .replace(/\[([^\]\n]+)]\(([^)]+)\)/g, "$1")
417
+ .replace(/`([^`\n]+)`/g, "$1")
418
+ .replace(/\*\*(.+?)\*\*/g, "$1")
419
+ .replace(/__(.+?)__/g, "$1")
420
+ .replace(/~~(.+?)~~/g, "$1")
421
+ .replace(/(^|[^\*])\*([^*\n]+)\*(?!\*)/g, "$1$2");
422
+ }
423
+ export function formatUsagePricePrompt(provider) {
424
+ return [
425
+ "🤖 输入当前模型价格吧",
426
+ "",
427
+ `Provider:<code>${escapeHtml(provider.name)}</code>`,
428
+ `Model:<code>${escapeHtml(provider.model)}</code>`,
429
+ "",
430
+ "格式:<code>输入价格/输出价格/缓存读取价格</code>",
431
+ "单位:美元 / 百万 tokens",
432
+ "示例:<code>1/3/0.1</code>"
433
+ ].join("\n");
434
+ }
435
+ export function formatUsagePriceSaved(provider, inputPrice, outputPrice, cacheReadPrice) {
436
+ return [
437
+ "🤖 价格已保存",
438
+ "",
439
+ `Provider:<code>${escapeHtml(provider.name)}</code>`,
440
+ `Model:<code>${escapeHtml(provider.model)}</code>`,
441
+ `Input:<code>$${inputPrice}/1M tokens</code>`,
442
+ `Output:<code>$${outputPrice}/1M tokens</code>`,
443
+ `Cache Read:<code>$${cacheReadPrice}/1M tokens</code>`
444
+ ].join("\n");
445
+ }
446
+ export function formatUsagePriceInvalid() {
447
+ return [
448
+ "🤖 价格格式不对",
449
+ "",
450
+ "请按 <code>输入价格/输出价格/缓存读取价格</code> 输入,例如 <code>1/3/0.1</code>。"
451
+ ].join("\n");
452
+ }
453
+ function formatProviderCost(usage, options = {}) {
454
+ if (!usage)
455
+ return "$0.0000";
456
+ if (typeof options.estimatedCostUsd === "number" && Number.isFinite(options.estimatedCostUsd)) {
457
+ return `$${options.estimatedCostUsd.toFixed(4)}${options.estimatedCostLabel ?? "(按价格表估算)"}`;
458
+ }
459
+ if (usage.costUsd > 0) {
460
+ const suffix = usage.unknownCostCount > 0 ? ` + ${usage.unknownCostCount} 次未知` : "";
461
+ return `$${usage.costUsd.toFixed(4)}${suffix}`;
462
+ }
463
+ if (usage.unknownCostCount > 0) {
464
+ return usage.totalTokens > 0
465
+ ? `缺少价格表`
466
+ : "额度未知";
467
+ }
468
+ return "$0.0000";
469
+ }
470
+ function formatTokenCount(tokens) {
471
+ return Math.round(tokens).toLocaleString("en-US");
472
+ }
473
+ function parseDiffStats(diff) {
474
+ if (!diff)
475
+ return null;
476
+ let added = 0;
477
+ let removed = 0;
478
+ for (const line of diff.split("\n")) {
479
+ if (line.startsWith("+") && !line.startsWith("+++"))
480
+ added++;
481
+ else if (line.startsWith("-") && !line.startsWith("---"))
482
+ removed++;
483
+ }
484
+ if (added === 0 && removed === 0)
485
+ return null;
486
+ return `+${added} -${removed}`;
487
+ }
@@ -0,0 +1,145 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ export function buildApprovalKeyboard(id) {
3
+ return new InlineKeyboard()
4
+ .text("Allow", `perm:approve:${id}`)
5
+ .text("Deny", `perm:deny:${id}`)
6
+ .row()
7
+ .text("Allow all", `perm:approve_all:${id}`)
8
+ .text("Skip", `perm:skip:${id}`);
9
+ }
10
+ export function buildWorkdirStartKeyboard(workdirs, scope = "setup") {
11
+ const keyboard = new InlineKeyboard();
12
+ for (const [index, cwd] of workdirs.slice(0, 8).entries()) {
13
+ keyboard.text(cwd, `${scope}:workdir:pick:${index}`).row();
14
+ }
15
+ return keyboard.text("自定义", `${scope}:workdir:custom`);
16
+ }
17
+ export function buildWorkdirListKeyboard(workdirs, scope = "setup") {
18
+ const keyboard = new InlineKeyboard();
19
+ for (const [index, cwd] of workdirs.slice(0, 8).entries()) {
20
+ keyboard.text(cwd, `${scope}:workdir:pick:${index}`).row();
21
+ }
22
+ return keyboard.text("back", `${scope}:workdir:back`);
23
+ }
24
+ export function buildCliKeyboard() {
25
+ return new InlineKeyboard()
26
+ .text("Claude Code", "setup:cli:claude")
27
+ .text("Codex", "setup:cli:codex");
28
+ }
29
+ export function buildProviderPickerKeyboard(providers, scope, forcedEngine) {
30
+ const keyboard = new InlineKeyboard();
31
+ for (const [index, provider] of providers.entries()) {
32
+ keyboard.text(provider, buildProviderPickCallback(scope, index, forcedEngine));
33
+ if (index < providers.length - 1 || scope === "ready") {
34
+ keyboard.row();
35
+ }
36
+ }
37
+ if (scope === "ready") {
38
+ return keyboard.text("back", "ready:back");
39
+ }
40
+ return keyboard;
41
+ }
42
+ export function buildModelPickerKeyboard(models, providerIndex, scope, forcedEngine) {
43
+ const keyboard = new InlineKeyboard();
44
+ for (const [index, model] of models.entries()) {
45
+ keyboard.text(model, buildModelPickCallback(scope, providerIndex, index, forcedEngine)).row();
46
+ }
47
+ return keyboard.text("back", buildModelBackCallback(scope, forcedEngine));
48
+ }
49
+ export function buildAgentProfileKeyboard() {
50
+ return new InlineKeyboard()
51
+ .text("暂不提供", "setup:agent:skip");
52
+ }
53
+ export function buildAgentProfileBackKeyboard() {
54
+ return new InlineKeyboard()
55
+ .text("back", "ready:back");
56
+ }
57
+ export function buildReadyKeyboard(input) {
58
+ return new InlineKeyboard()
59
+ .text(input.cli, "ready:cli")
60
+ .row()
61
+ .text(input.providerModel, "ready:provider")
62
+ .row()
63
+ .text(formatPermissionModeLabel(input.permissionMode), "ready:mode")
64
+ .row()
65
+ .text(input.cwd, "ready:workdir");
66
+ }
67
+ export function buildReadyCliKeyboard() {
68
+ return new InlineKeyboard()
69
+ .text("Claude Code", "ready:cli:pick:claude")
70
+ .text("Codex", "ready:cli:pick:codex")
71
+ .row()
72
+ .text("back", "ready:back");
73
+ }
74
+ export function buildReadyProviderKeyboard(providers) {
75
+ return buildProviderPickerKeyboard(providers, "ready");
76
+ }
77
+ export function buildReadyModelKeyboard(models, providerIndex) {
78
+ return buildModelPickerKeyboard(models, providerIndex, "ready");
79
+ }
80
+ export function buildReadyModeKeyboard() {
81
+ return new InlineKeyboard()
82
+ .text("默认权限", "ready:mode:pick:default")
83
+ .row()
84
+ .text("接受编辑", "ready:mode:pick:acceptEdits")
85
+ .row()
86
+ .text("跳过权限", "ready:mode:pick:bypassPermissions")
87
+ .row()
88
+ .text("back", "ready:back");
89
+ }
90
+ export function buildUsagePricingKeyboard() {
91
+ return new InlineKeyboard()
92
+ .text("输入价格", "usage:price:input")
93
+ .text("未知", "usage:price:unknown");
94
+ }
95
+ export function buildRememberScopeKeyboard() {
96
+ return new InlineKeyboard()
97
+ .text("global", "memory:remember:scope:global")
98
+ .text("bot", "memory:remember:scope:bot")
99
+ .text("auto", "memory:remember:scope:auto");
100
+ }
101
+ export function buildMemoryManageKeyboard() {
102
+ return new InlineKeyboard()
103
+ .text("升级为 global", "memory:manage:promote")
104
+ .row()
105
+ .text("清空 bot 记忆", "memory:manage:clear:bot")
106
+ .row()
107
+ .text("清空 global 记忆", "memory:manage:clear:global");
108
+ }
109
+ export function buildMemoryClearConfirmKeyboard(scope) {
110
+ return new InlineKeyboard()
111
+ .text("确认清空", `memory:manage:clear_confirm:${scope}`)
112
+ .text("取消", "memory:manage:panel");
113
+ }
114
+ export function formatPermissionModeLabel(mode) {
115
+ if (mode === "acceptEdits")
116
+ return "接受编辑";
117
+ if (mode === "bypassPermissions")
118
+ return "跳过权限";
119
+ return "默认权限";
120
+ }
121
+ export function buildResultKeyboard(hasFullOutput, hasDiff) {
122
+ const kb = new InlineKeyboard();
123
+ if (hasFullOutput)
124
+ kb.text("📄 完整输出", "result:full");
125
+ if (hasDiff)
126
+ kb.text("📝 查看 Diff", "result:diff");
127
+ return kb;
128
+ }
129
+ function buildProviderPickCallback(scope, index, forcedEngine) {
130
+ return ["setup", "ready"].includes(scope) && forcedEngine
131
+ ? `${scope}:provider:pick:${index}:${forcedEngine}`
132
+ : `${scope}:provider:pick:${index}`;
133
+ }
134
+ function buildModelPickCallback(scope, providerIndex, modelIndex, forcedEngine) {
135
+ return forcedEngine
136
+ ? `${scope}:model:pick:${providerIndex}:${modelIndex}:${forcedEngine}`
137
+ : `${scope}:model:pick:${providerIndex}:${modelIndex}`;
138
+ }
139
+ function buildModelBackCallback(scope, forcedEngine) {
140
+ if (forcedEngine)
141
+ return `${scope}:provider:show:${forcedEngine}`;
142
+ if (scope === "setup")
143
+ return "setup:provider:show";
144
+ return `${scope}:provider`;
145
+ }