@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,413 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getMemoryBackupDir } from "../../core/config/paths.js";
4
+ import { escapeHtml, truncateText } from "../interaction/formatter.js";
5
+ import { MemoryStore } from "./memory-store.js";
6
+ const DEFAULT_INJECTION_CHAR_BUDGET = 3000;
7
+ const DEFAULT_SHORT_TERM_TTL_MS = 72 * 60 * 60 * 1000;
8
+ const PROFILE_TITLE = "用户画像";
9
+ const PERSONA_TITLE = "Bot 人设";
10
+ const RAW_DESCRIPTION_TITLE = "用户原始自我描述";
11
+ export class MemoryService {
12
+ botId;
13
+ store;
14
+ ownsStore;
15
+ injectionCharBudget;
16
+ shortTermTtlMs;
17
+ constructor(options) {
18
+ this.botId = options.botId || "default";
19
+ this.store = options.store ?? new MemoryStore();
20
+ this.ownsStore = !options.store;
21
+ this.injectionCharBudget = options.injectionCharBudget ?? DEFAULT_INJECTION_CHAR_BUDGET;
22
+ this.shortTermTtlMs = options.shortTermTtlMs ?? DEFAULT_SHORT_TERM_TTL_MS;
23
+ }
24
+ close() {
25
+ if (this.ownsStore)
26
+ this.store.close();
27
+ }
28
+ getStorePath() {
29
+ return this.store.getPath();
30
+ }
31
+ save(chatId, input) {
32
+ const normalized = this.normalizeSaveInput(chatId, input);
33
+ return this.store.save(normalized);
34
+ }
35
+ remember(chatId, input) {
36
+ const category = input.category ?? classifyCategory(input.content);
37
+ if (category === "credential") {
38
+ throw new Error("加密记忆尚未启用,凭证类内容不会明文保存。");
39
+ }
40
+ const scope = input.scope ?? defaultScopeForCategory(category);
41
+ const duration = input.duration ?? "permanent";
42
+ const expiresAt = duration === "short_term"
43
+ ? Date.now() + (input.ttlMs ?? this.shortTermTtlMs)
44
+ : null;
45
+ return this.save(chatId, {
46
+ scope,
47
+ category,
48
+ title: input.title ?? inferTitle(input.content, category),
49
+ content: input.content,
50
+ tags: input.tags ?? inferTags(input.content, category),
51
+ duration,
52
+ expiresAt,
53
+ priority: category === "profile" || category === "persona" ? 1 : 0
54
+ });
55
+ }
56
+ saveProfile(chatId, content) {
57
+ return this.store.upsertSingleton(this.normalizeSaveInput(chatId, {
58
+ scope: "global",
59
+ category: "profile",
60
+ title: PROFILE_TITLE,
61
+ content,
62
+ tags: ["profile"],
63
+ priority: 1,
64
+ duration: "permanent"
65
+ }));
66
+ }
67
+ savePersona(chatId, content) {
68
+ return this.store.upsertSingleton(this.normalizeSaveInput(chatId, {
69
+ scope: "bot",
70
+ category: "persona",
71
+ title: PERSONA_TITLE,
72
+ content,
73
+ tags: ["persona", this.botId],
74
+ priority: 1,
75
+ duration: "permanent"
76
+ }));
77
+ }
78
+ saveRawDescription(chatId, content) {
79
+ return this.store.upsertSingleton(this.normalizeSaveInput(chatId, {
80
+ scope: "bot",
81
+ category: "fact",
82
+ title: RAW_DESCRIPTION_TITLE,
83
+ content,
84
+ tags: ["profile", "raw"],
85
+ priority: 0,
86
+ duration: "permanent"
87
+ }));
88
+ }
89
+ getProfile(chatId) {
90
+ return this.store.getProfile(normalizeChatId(chatId));
91
+ }
92
+ getPersona(chatId) {
93
+ return this.store.getPersona(normalizeChatId(chatId), this.botId);
94
+ }
95
+ listVisible(chatId, query, limit = 50) {
96
+ return this.store.listVisible({
97
+ chatId: normalizeChatId(chatId),
98
+ botId: this.botId,
99
+ query,
100
+ limit
101
+ });
102
+ }
103
+ retrieveForPrompt(chatId, prompt) {
104
+ const visible = this.store.listVisible({
105
+ chatId: normalizeChatId(chatId),
106
+ botId: this.botId,
107
+ limit: 200
108
+ });
109
+ if (!visible.length)
110
+ return [];
111
+ const required = visible.filter((entry) => entry.priority > 0);
112
+ const keywords = extractKeywords(prompt);
113
+ const scored = visible
114
+ .filter((entry) => entry.priority <= 0)
115
+ .map((entry) => ({ entry, score: scoreMemory(entry, keywords) }))
116
+ .filter((item) => item.score > 0)
117
+ .sort((a, b) => b.score - a.score || b.entry.updatedAt - a.entry.updatedAt)
118
+ .slice(0, 8)
119
+ .map((item) => item.entry);
120
+ return trimEntriesToBudget(dedupeEntries([
121
+ ...sortRequiredEntries(required),
122
+ ...scored
123
+ ]), this.injectionCharBudget);
124
+ }
125
+ delete(chatId, selector) {
126
+ const value = selector.trim();
127
+ if (!value)
128
+ return 0;
129
+ if (/^\d+$/.test(value)) {
130
+ return this.store.deleteVisibleById(normalizeChatId(chatId), this.botId, Number(value));
131
+ }
132
+ if (value === "profile") {
133
+ const profile = this.getProfile(chatId);
134
+ return profile ? this.store.deleteVisibleById(normalizeChatId(chatId), this.botId, profile.id) : 0;
135
+ }
136
+ if (value === "persona") {
137
+ const persona = this.getPersona(chatId);
138
+ return persona ? this.store.deleteVisibleById(normalizeChatId(chatId), this.botId, persona.id) : 0;
139
+ }
140
+ return this.store.deleteVisibleByKeyword(normalizeChatId(chatId), this.botId, value);
141
+ }
142
+ promote(chatId, id) {
143
+ return this.store.promote(normalizeChatId(chatId), this.botId, id);
144
+ }
145
+ clearScope(chatId, scope) {
146
+ return this.store.clearScope(normalizeChatId(chatId), this.botId, scope);
147
+ }
148
+ cleanupExpired(now = Date.now()) {
149
+ return this.store.deleteExpired(now);
150
+ }
151
+ async backup(retain = 7) {
152
+ const dbPath = this.store.getPath();
153
+ try {
154
+ await fs.access(dbPath);
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ const backupDir = getMemoryBackupDir();
160
+ await fs.mkdir(backupDir, { recursive: true });
161
+ const stamp = new Date().toISOString().slice(0, 10);
162
+ const backupPath = path.join(backupDir, `memory-${stamp}.sqlite`);
163
+ try {
164
+ await fs.copyFile(dbPath, backupPath);
165
+ await pruneBackups(backupDir, retain);
166
+ return backupPath;
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ }
172
+ formatList(chatId, query) {
173
+ const entries = this.listVisible(chatId, query, 30);
174
+ if (!entries.length)
175
+ return "🤖 还没有记忆。";
176
+ return [
177
+ "🤖 当前记忆",
178
+ "",
179
+ ...entries.map(formatMemorySummary)
180
+ ].join("\n\n");
181
+ }
182
+ normalizeSaveInput(chatId, input) {
183
+ if (!input.content.trim()) {
184
+ throw new Error("记忆内容不能为空。");
185
+ }
186
+ const category = input.category ?? "fact";
187
+ if (category === "credential" && !input.encrypted) {
188
+ throw new Error("加密记忆尚未启用,凭证类内容不会明文保存。");
189
+ }
190
+ const scope = normalizeScope(input.scope ?? defaultScopeForCategory(category), category);
191
+ const duration = input.duration ?? "permanent";
192
+ const expiresAt = duration === "short_term"
193
+ ? input.expiresAt ?? Date.now() + this.shortTermTtlMs
194
+ : null;
195
+ return {
196
+ chatId: normalizeChatId(chatId),
197
+ scope,
198
+ botId: scope === "bot" ? this.botId : null,
199
+ category,
200
+ title: (input.title ?? inferTitle(input.content, category)).trim(),
201
+ content: input.content.trim(),
202
+ tags: normalizeTags(input.tags ?? []),
203
+ priority: input.priority ?? 0,
204
+ duration,
205
+ encrypted: input.encrypted ?? false,
206
+ expiresAt
207
+ };
208
+ }
209
+ }
210
+ export function getBotIdFromToken(token) {
211
+ const value = String(token ?? "").trim();
212
+ if (!value)
213
+ return "default";
214
+ return value.split(":", 1)[0] || "default";
215
+ }
216
+ export function buildFullProfilePrompt(rawDescription) {
217
+ return [
218
+ "根据以下用户的自我描述,生成两份内容,用分隔符 '=====PERSONA=====' 分开。",
219
+ "",
220
+ "<user_description>",
221
+ rawDescription,
222
+ "</user_description>",
223
+ "",
224
+ "第一部分【用户画像】(跨所有助手共享,只描述用户本人):",
225
+ "- 用户身份、技术背景、通用偏好、通用工作方式",
226
+ "- 不要包含任何特定助手的角色定位",
227
+ "- 500 字符以内",
228
+ "",
229
+ "建议结构:",
230
+ "## 用户背景",
231
+ "## 通用偏好",
232
+ "",
233
+ "第二部分【Bot 人设】(仅本助手使用):",
234
+ "- 本助手的职责域、专业风格、风险边界",
235
+ "- 400 字符以内",
236
+ "",
237
+ "建议结构:",
238
+ "## 角色与职责",
239
+ "## 工作风格",
240
+ "## 风险边界",
241
+ "",
242
+ "通用要求:输出纯文本;中文输入就使用中文;只输出内容本身,不要多余说明。"
243
+ ].join("\n");
244
+ }
245
+ export function buildPersonaPrompt(rawDescription, existingProfile) {
246
+ return [
247
+ "已知用户画像如下(勿修改、勿重复其中内容):",
248
+ "<existing_profile>",
249
+ existingProfile,
250
+ "</existing_profile>",
251
+ "",
252
+ "用户希望本助手承担的职责描述:",
253
+ "<bot_description>",
254
+ rawDescription,
255
+ "</bot_description>",
256
+ "",
257
+ "生成本助手的 Bot 人设,要求:",
258
+ "- 只描述本助手的职责域、专业风格、风险边界",
259
+ "- 400 字符以内;中文输入就使用中文",
260
+ "",
261
+ "建议结构:",
262
+ "## 角色与职责",
263
+ "## 工作风格",
264
+ "## 风险边界",
265
+ "",
266
+ "只输出人设内容,不要多余说明。"
267
+ ].join("\n");
268
+ }
269
+ export function splitProfileAndPersona(value) {
270
+ const [profile, ...rest] = value.split("=====PERSONA=====");
271
+ return {
272
+ profile: profile?.trim() || value.trim(),
273
+ persona: rest.join("=====PERSONA=====").trim()
274
+ };
275
+ }
276
+ function normalizeChatId(chatId) {
277
+ return String(chatId);
278
+ }
279
+ function normalizeScope(scope, category) {
280
+ if (category === "profile")
281
+ return "global";
282
+ if (category === "persona" || category === "credential")
283
+ return "bot";
284
+ return scope;
285
+ }
286
+ function defaultScopeForCategory(category) {
287
+ if (category === "profile")
288
+ return "global";
289
+ if (category === "persona" || category === "credential" || category === "note" || category === "skill")
290
+ return "bot";
291
+ return "global";
292
+ }
293
+ function classifyCategory(content) {
294
+ const text = content.toLowerCase();
295
+ if (/密码|账号|token|api[_ -]?key|secret|credential|password/.test(text))
296
+ return "credential";
297
+ if (/偏好|喜欢|习惯|prefer|preference/.test(text))
298
+ return "preference";
299
+ if (/明天|今天|本周|临时|提醒|todo|计划/.test(text))
300
+ return "note";
301
+ return "fact";
302
+ }
303
+ function inferTitle(content, category) {
304
+ const firstLine = content.trim().split(/\r?\n/, 1)[0] ?? "";
305
+ const title = firstLine.replace(/^#+\s*/, "").trim();
306
+ if (title)
307
+ return truncateText(title, 40);
308
+ if (category === "preference")
309
+ return "偏好";
310
+ if (category === "note")
311
+ return "临时笔记";
312
+ return "记忆";
313
+ }
314
+ function inferTags(content, category) {
315
+ const tags = new Set([category]);
316
+ for (const match of content.matchAll(/#([\p{L}\p{N}_-]+)/gu)) {
317
+ tags.add(match[1]);
318
+ }
319
+ return [...tags].slice(0, 8);
320
+ }
321
+ function normalizeTags(tags) {
322
+ const seen = new Set();
323
+ const result = [];
324
+ for (const tag of tags) {
325
+ const value = String(tag).trim();
326
+ if (!value || seen.has(value))
327
+ continue;
328
+ seen.add(value);
329
+ result.push(value);
330
+ if (result.length >= 12)
331
+ break;
332
+ }
333
+ return result;
334
+ }
335
+ function extractKeywords(prompt) {
336
+ const words = prompt
337
+ .toLowerCase()
338
+ .replace(/[^\p{L}\p{N}_-]+/gu, " ")
339
+ .split(/\s+/)
340
+ .filter((word) => word.length >= 2);
341
+ return [...new Set(words)].slice(0, 20);
342
+ }
343
+ function scoreMemory(entry, keywords) {
344
+ if (!keywords.length)
345
+ return 0;
346
+ const haystack = `${entry.title}\n${entry.content}\n${entry.tags.join(" ")}`.toLowerCase();
347
+ let score = 0;
348
+ for (const keyword of keywords) {
349
+ if (haystack.includes(keyword))
350
+ score += entry.title.toLowerCase().includes(keyword) ? 3 : 1;
351
+ }
352
+ if (entry.category === "preference")
353
+ score += 1;
354
+ if (entry.category === "note")
355
+ score += 0.5;
356
+ return score;
357
+ }
358
+ function sortRequiredEntries(entries) {
359
+ const order = {
360
+ persona: 0,
361
+ profile: 1,
362
+ preference: 2,
363
+ fact: 3,
364
+ note: 4,
365
+ skill: 5,
366
+ credential: 6
367
+ };
368
+ return [...entries].sort((a, b) => order[a.category] - order[b.category] || b.updatedAt - a.updatedAt);
369
+ }
370
+ function dedupeEntries(entries) {
371
+ const seen = new Set();
372
+ const result = [];
373
+ for (const entry of entries) {
374
+ if (seen.has(entry.id))
375
+ continue;
376
+ seen.add(entry.id);
377
+ result.push(entry);
378
+ }
379
+ return result;
380
+ }
381
+ function trimEntriesToBudget(entries, budget) {
382
+ const required = entries.filter((entry) => entry.priority > 0);
383
+ const optional = entries.filter((entry) => entry.priority <= 0);
384
+ const result = [...required];
385
+ let used = required.reduce((sum, entry) => sum + entry.content.length + entry.title.length, 0);
386
+ for (const entry of optional) {
387
+ const cost = entry.content.length + entry.title.length;
388
+ if (used + cost > budget && result.length)
389
+ continue;
390
+ result.push(entry);
391
+ used += cost;
392
+ if (used >= budget)
393
+ break;
394
+ }
395
+ return result;
396
+ }
397
+ function formatMemorySummary(entry) {
398
+ const scope = entry.scope === "global" ? "global" : "bot";
399
+ const expiry = entry.expiresAt ? `\nExpires:<code>${escapeHtml(new Date(entry.expiresAt).toISOString())}</code>` : "";
400
+ return [
401
+ `<b>#${entry.id} ${escapeHtml(entry.title)}</b>`,
402
+ `Scope:<code>${scope}</code> · Category:<code>${entry.category}</code>`,
403
+ `Content:${escapeHtml(truncateText(entry.encrypted ? "(encrypted)" : entry.content, 220))}${expiry}`
404
+ ].join("\n");
405
+ }
406
+ async function pruneBackups(backupDir, retain) {
407
+ const entries = await fs.readdir(backupDir).catch(() => []);
408
+ const files = entries
409
+ .filter((name) => /^memory-\d{4}-\d{2}-\d{2}\.sqlite$/.test(name))
410
+ .sort()
411
+ .reverse();
412
+ await Promise.all(files.slice(retain).map((name) => fs.rm(path.join(backupDir, name), { force: true })));
413
+ }
@@ -0,0 +1,216 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import Database from "better-sqlite3";
4
+ import { getMemoryDbPath } from "../../core/config/paths.js";
5
+ export class MemoryStore {
6
+ dbPath;
7
+ db;
8
+ constructor(dbPath = getMemoryDbPath()) {
9
+ this.dbPath = dbPath;
10
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
11
+ this.db = new Database(dbPath);
12
+ this.db.pragma("journal_mode = WAL");
13
+ this.db.pragma("busy_timeout = 5000");
14
+ this.migrate();
15
+ }
16
+ close() {
17
+ this.db.close();
18
+ }
19
+ getPath() {
20
+ return this.dbPath;
21
+ }
22
+ save(input) {
23
+ const now = Date.now();
24
+ const result = this.db.prepare(`insert into memories (
25
+ chatId, scope, botId, category, title, content, tags, priority, duration,
26
+ encrypted, expiresAt, createdAt, updatedAt
27
+ ) values (
28
+ @chatId, @scope, @botId, @category, @title, @content, @tagsJson, @priority, @duration,
29
+ @encryptedValue, @expiresAt, @now, @now
30
+ )`).run({
31
+ ...input,
32
+ tagsJson: JSON.stringify(input.tags),
33
+ encryptedValue: input.encrypted ? 1 : 0,
34
+ now
35
+ });
36
+ return this.getById(Number(result.lastInsertRowid));
37
+ }
38
+ upsertSingleton(input) {
39
+ const current = this.getSingleton(input.chatId, input.scope, input.botId, input.category, input.title);
40
+ if (current) {
41
+ const now = Date.now();
42
+ this.db.prepare(`update memories set
43
+ content = @content,
44
+ tags = @tagsJson,
45
+ priority = @priority,
46
+ duration = @duration,
47
+ encrypted = @encryptedValue,
48
+ expiresAt = @expiresAt,
49
+ updatedAt = @now
50
+ where id = @id`).run({
51
+ id: current.id,
52
+ content: input.content,
53
+ tagsJson: JSON.stringify(input.tags),
54
+ priority: input.priority,
55
+ duration: input.duration,
56
+ encryptedValue: input.encrypted ? 1 : 0,
57
+ expiresAt: input.expiresAt,
58
+ now
59
+ });
60
+ return this.getById(current.id);
61
+ }
62
+ const now = Date.now();
63
+ this.db.prepare(`insert into memories (
64
+ chatId, scope, botId, category, title, content, tags, priority, duration,
65
+ encrypted, expiresAt, createdAt, updatedAt
66
+ ) values (
67
+ @chatId, @scope, @botId, @category, @title, @content, @tagsJson, @priority, @duration,
68
+ @encryptedValue, @expiresAt, @now, @now
69
+ )`).run({
70
+ ...input,
71
+ tagsJson: JSON.stringify(input.tags),
72
+ encryptedValue: input.encrypted ? 1 : 0,
73
+ now
74
+ });
75
+ return this.getSingleton(input.chatId, input.scope, input.botId, input.category, input.title);
76
+ }
77
+ getById(id) {
78
+ const row = this.db.prepare("select * from memories where id = ?").get(id);
79
+ return row ? rowToEntry(row) : null;
80
+ }
81
+ getSingleton(chatId, scope, botId, category, title) {
82
+ const row = this.db.prepare(`select * from memories
83
+ where chatId = ? and scope = ? and botId is ? and category = ? and title = ?
84
+ limit 1`).get(chatId, scope, botId, category, title);
85
+ return row ? rowToEntry(row) : null;
86
+ }
87
+ getProfile(chatId) {
88
+ const row = this.db.prepare("select * from memories where chatId = ? and scope = 'global' and category = 'profile' order by updatedAt desc limit 1").get(chatId);
89
+ return row ? rowToEntry(row) : null;
90
+ }
91
+ getPersona(chatId, botId) {
92
+ const row = this.db.prepare("select * from memories where chatId = ? and scope = 'bot' and botId = ? and category = 'persona' order by updatedAt desc limit 1").get(chatId, botId);
93
+ return row ? rowToEntry(row) : null;
94
+ }
95
+ listVisible(input) {
96
+ const now = Date.now();
97
+ const params = {
98
+ chatId: input.chatId,
99
+ botId: input.botId,
100
+ now,
101
+ limit: input.limit ?? 100
102
+ };
103
+ const query = input.query?.trim().toLowerCase();
104
+ const searchClause = query
105
+ ? "and (lower(title) like @query or lower(content) like @query or lower(tags) like @query)"
106
+ : "";
107
+ if (query)
108
+ params.query = `%${query}%`;
109
+ const rows = this.db.prepare(`select * from memories
110
+ where ${visibilityClause()} and (expiresAt is null or expiresAt > @now)
111
+ ${searchClause}
112
+ order by priority desc, updatedAt desc, id desc
113
+ limit @limit`).all(params);
114
+ return rows.map(rowToEntry);
115
+ }
116
+ deleteVisibleById(chatId, botId, id) {
117
+ const result = this.db.prepare(`delete from memories
118
+ where id = @id and ${visibilityClause()}`).run({ chatId, botId, id });
119
+ return result.changes;
120
+ }
121
+ deleteVisibleByKeyword(chatId, botId, keyword) {
122
+ if (keyword.trim().length < 2)
123
+ return 0;
124
+ const query = `%${keyword.trim().toLowerCase()}%`;
125
+ const result = this.db.prepare(`delete from memories
126
+ where ${visibilityClause()}
127
+ and (lower(title) like @query or lower(content) like @query or lower(tags) like @query)`).run({ chatId, botId, query });
128
+ return result.changes;
129
+ }
130
+ clearScope(chatId, botId, scope) {
131
+ const result = scope === "global"
132
+ ? this.db.prepare("delete from memories where chatId = ? and scope = 'global'").run(chatId)
133
+ : this.db.prepare("delete from memories where chatId = ? and scope = 'bot' and botId = ?").run(chatId, botId);
134
+ return result.changes;
135
+ }
136
+ promote(chatId, botId, id) {
137
+ const entry = this.getById(id);
138
+ if (!entry || entry.chatId !== chatId || entry.scope !== "bot" || entry.botId !== botId)
139
+ return null;
140
+ if (entry.category === "persona") {
141
+ throw new Error("Bot 人设不能升级为 global。");
142
+ }
143
+ this.db.prepare("update memories set scope = 'global', botId = null, updatedAt = ? where id = ?").run(Date.now(), id);
144
+ return this.getById(id);
145
+ }
146
+ deleteExpired(now = Date.now()) {
147
+ const result = this.db.prepare("delete from memories where expiresAt is not null and expiresAt <= ?").run(now);
148
+ return result.changes;
149
+ }
150
+ migrate() {
151
+ this.db.exec(`
152
+ create table if not exists memories (
153
+ id integer primary key autoincrement,
154
+ chatId text not null,
155
+ scope text not null default 'bot' check (scope in ('global', 'bot')),
156
+ botId text,
157
+ category text not null check (category in ('profile', 'persona', 'credential', 'fact', 'preference', 'note', 'skill')),
158
+ title text not null,
159
+ content text not null,
160
+ tags text not null default '[]',
161
+ priority integer not null default 0,
162
+ duration text not null default 'permanent' check (duration in ('permanent', 'short_term')),
163
+ encrypted integer not null default 0,
164
+ expiresAt integer,
165
+ createdAt integer not null,
166
+ updatedAt integer not null,
167
+ check (scope = 'global' or botId is not null),
168
+ unique (chatId, scope, botId, category, title)
169
+ );
170
+ create index if not exists idx_memories_owner on memories (chatId, scope, botId);
171
+ create index if not exists idx_memories_expiry on memories (expiresAt) where expiresAt is not null;
172
+
173
+ create table if not exists memory_keys (
174
+ chatId text primary key,
175
+ salt text not null,
176
+ verifier text not null,
177
+ failCount integer not null default 0,
178
+ lockedUntil integer,
179
+ createdAt integer not null,
180
+ updatedAt integer not null
181
+ );
182
+ `);
183
+ }
184
+ }
185
+ function visibilityClause() {
186
+ return "chatId = @chatId and (scope = 'global' or (scope = 'bot' and botId = @botId))";
187
+ }
188
+ function rowToEntry(row) {
189
+ return {
190
+ id: row.id,
191
+ chatId: row.chatId,
192
+ scope: row.scope,
193
+ botId: row.botId,
194
+ category: row.category,
195
+ title: row.title,
196
+ content: row.content,
197
+ tags: parseTags(row.tags),
198
+ priority: row.priority,
199
+ duration: row.duration,
200
+ encrypted: row.encrypted === 1,
201
+ expiresAt: row.expiresAt,
202
+ createdAt: row.createdAt,
203
+ updatedAt: row.updatedAt
204
+ };
205
+ }
206
+ function parseTags(value) {
207
+ try {
208
+ const parsed = JSON.parse(value);
209
+ return Array.isArray(parsed)
210
+ ? parsed.filter((item) => typeof item === "string")
211
+ : [];
212
+ }
213
+ catch {
214
+ return [];
215
+ }
216
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ const DEFAULT_RETRY_DELAYS_MS = [250, 750];
2
+ export async function withTelegramNetworkRetry(operation, options = {}) {
3
+ const delays = options.delaysMs ?? DEFAULT_RETRY_DELAYS_MS;
4
+ for (let attempt = 0;; attempt += 1) {
5
+ try {
6
+ return await operation();
7
+ }
8
+ catch (error) {
9
+ if (attempt >= delays.length || !isRetryableTelegramNetworkError(error)) {
10
+ throw error;
11
+ }
12
+ await delay(delays[attempt]);
13
+ }
14
+ }
15
+ }
16
+ export function isRetryableTelegramNetworkError(error) {
17
+ const message = error instanceof Error ? error.message : String(error);
18
+ return /Network request|fetch failed|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|socket hang up|\b5\d\d\b/i.test(message);
19
+ }
20
+ function delay(ms) {
21
+ return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
22
+ }