opencode-gateway 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +0 -0
- package/dist/index.js +20907 -52
- package/package.json +1 -1
- package/dist/binding/execution.js +0 -1
- package/dist/binding/gateway.js +0 -1
- package/dist/binding/index.js +0 -4
- package/dist/binding/opencode.js +0 -1
- package/dist/cli/args.js +0 -53
- package/dist/cli/doctor.js +0 -49
- package/dist/cli/init.js +0 -40
- package/dist/cli/opencode-config-file.js +0 -18
- package/dist/cli/opencode-config.js +0 -194
- package/dist/cli/paths.js +0 -22
- package/dist/cli/templates.js +0 -41
- package/dist/config/cron.js +0 -52
- package/dist/config/gateway.js +0 -148
- package/dist/config/memory.js +0 -105
- package/dist/config/paths.js +0 -39
- package/dist/config/telegram.js +0 -91
- package/dist/cron/runtime.js +0 -402
- package/dist/delivery/telegram.js +0 -75
- package/dist/delivery/text.js +0 -175
- package/dist/gateway.js +0 -117
- package/dist/host/file-sender.js +0 -59
- package/dist/host/logger.js +0 -53
- package/dist/host/transport.js +0 -35
- package/dist/mailbox/router.js +0 -16
- package/dist/media/mime.js +0 -45
- package/dist/memory/prompt.js +0 -122
- package/dist/opencode/adapter.js +0 -340
- package/dist/opencode/driver-hub.js +0 -82
- package/dist/opencode/event-normalize.js +0 -48
- package/dist/opencode/event-stream.js +0 -65
- package/dist/opencode/events.js +0 -1
- package/dist/questions/client.js +0 -36
- package/dist/questions/format.js +0 -36
- package/dist/questions/normalize.js +0 -45
- package/dist/questions/parser.js +0 -96
- package/dist/questions/runtime.js +0 -195
- package/dist/questions/types.js +0 -1
- package/dist/runtime/attachments.js +0 -12
- package/dist/runtime/conversation-coordinator.js +0 -22
- package/dist/runtime/executor.js +0 -407
- package/dist/runtime/mailbox.js +0 -112
- package/dist/runtime/opencode-runner.js +0 -79
- package/dist/runtime/runtime-singleton.js +0 -28
- package/dist/session/context.js +0 -23
- package/dist/session/conversation-key.js +0 -3
- package/dist/session/switcher.js +0 -59
- package/dist/session/system-prompt.js +0 -52
- package/dist/store/migrations.js +0 -197
- package/dist/store/sqlite.js +0 -777
- package/dist/telegram/client.js +0 -180
- package/dist/telegram/media.js +0 -65
- package/dist/telegram/normalize.js +0 -119
- package/dist/telegram/poller.js +0 -166
- package/dist/telegram/runtime.js +0 -157
- package/dist/telegram/state.js +0 -149
- package/dist/telegram/types.js +0 -1
- package/dist/tools/channel-new-session.js +0 -27
- package/dist/tools/channel-send-file.js +0 -27
- package/dist/tools/channel-target.js +0 -34
- package/dist/tools/cron-run.js +0 -20
- package/dist/tools/cron-upsert.js +0 -51
- package/dist/tools/gateway-dispatch-cron.js +0 -33
- package/dist/tools/gateway-status.js +0 -25
- package/dist/tools/schedule-cancel.js +0 -12
- package/dist/tools/schedule-format.js +0 -48
- package/dist/tools/schedule-list.js +0 -17
- package/dist/tools/schedule-once.js +0 -43
- package/dist/tools/schedule-status.js +0 -23
- package/dist/tools/telegram-send-test.js +0 -26
- package/dist/tools/telegram-status.js +0 -49
- package/dist/tools/time.js +0 -25
- package/dist/utils/error.js +0 -57
package/dist/store/sqlite.js
DELETED
|
@@ -1,777 +0,0 @@
|
|
|
1
|
-
import { Database } from "bun:sqlite";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
|
-
import { migrateGatewayDatabase } from "./migrations";
|
|
5
|
-
export class SqliteStore {
|
|
6
|
-
db;
|
|
7
|
-
constructor(db) {
|
|
8
|
-
this.db = db;
|
|
9
|
-
}
|
|
10
|
-
getSessionBinding(conversationKey) {
|
|
11
|
-
const row = this.db
|
|
12
|
-
.query("SELECT session_id FROM session_bindings WHERE conversation_key = ?1;")
|
|
13
|
-
.get(conversationKey);
|
|
14
|
-
return row?.session_id ?? null;
|
|
15
|
-
}
|
|
16
|
-
putSessionBinding(conversationKey, sessionId, recordedAtMs) {
|
|
17
|
-
this.db
|
|
18
|
-
.query(`
|
|
19
|
-
INSERT INTO session_bindings (conversation_key, session_id, updated_at_ms)
|
|
20
|
-
VALUES (?1, ?2, ?3)
|
|
21
|
-
ON CONFLICT(conversation_key) DO UPDATE SET
|
|
22
|
-
session_id = excluded.session_id,
|
|
23
|
-
updated_at_ms = excluded.updated_at_ms;
|
|
24
|
-
`)
|
|
25
|
-
.run(conversationKey, sessionId, recordedAtMs);
|
|
26
|
-
}
|
|
27
|
-
putSessionBindingIfUnchanged(conversationKey, expectedSessionId, nextSessionId, recordedAtMs) {
|
|
28
|
-
const result = expectedSessionId === null
|
|
29
|
-
? this.db
|
|
30
|
-
.query(`
|
|
31
|
-
INSERT INTO session_bindings (conversation_key, session_id, updated_at_ms)
|
|
32
|
-
VALUES (?1, ?2, ?3)
|
|
33
|
-
ON CONFLICT(conversation_key) DO NOTHING;
|
|
34
|
-
`)
|
|
35
|
-
.run(conversationKey, nextSessionId, recordedAtMs)
|
|
36
|
-
: this.db
|
|
37
|
-
.query(`
|
|
38
|
-
UPDATE session_bindings
|
|
39
|
-
SET session_id = ?2,
|
|
40
|
-
updated_at_ms = ?3
|
|
41
|
-
WHERE conversation_key = ?1
|
|
42
|
-
AND session_id = ?4;
|
|
43
|
-
`)
|
|
44
|
-
.run(conversationKey, nextSessionId, recordedAtMs, expectedSessionId);
|
|
45
|
-
return result.changes > 0;
|
|
46
|
-
}
|
|
47
|
-
deleteSessionBinding(conversationKey) {
|
|
48
|
-
this.db.query("DELETE FROM session_bindings WHERE conversation_key = ?1;").run(conversationKey);
|
|
49
|
-
}
|
|
50
|
-
clearSessionReplyTargets(sessionId) {
|
|
51
|
-
this.db.query("DELETE FROM session_reply_targets WHERE session_id = ?1;").run(sessionId);
|
|
52
|
-
}
|
|
53
|
-
replaceSessionReplyTargets(input) {
|
|
54
|
-
assertSafeInteger(input.recordedAtMs, "session reply-target recordedAtMs");
|
|
55
|
-
const deleteTargets = this.db.query("DELETE FROM session_reply_targets WHERE session_id = ?1;");
|
|
56
|
-
const insertTarget = this.db.query(`
|
|
57
|
-
INSERT INTO session_reply_targets (
|
|
58
|
-
session_id,
|
|
59
|
-
ordinal,
|
|
60
|
-
conversation_key,
|
|
61
|
-
delivery_channel,
|
|
62
|
-
delivery_target,
|
|
63
|
-
delivery_topic,
|
|
64
|
-
updated_at_ms
|
|
65
|
-
)
|
|
66
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);
|
|
67
|
-
`);
|
|
68
|
-
this.db.transaction((payload) => {
|
|
69
|
-
deleteTargets.run(payload.sessionId);
|
|
70
|
-
for (const [ordinal, target] of payload.targets.entries()) {
|
|
71
|
-
insertTarget.run(payload.sessionId, ordinal, payload.conversationKey, target.channel, target.target, normalizeKeyField(target.topic), payload.recordedAtMs);
|
|
72
|
-
}
|
|
73
|
-
})(input);
|
|
74
|
-
}
|
|
75
|
-
listSessionReplyTargets(sessionId) {
|
|
76
|
-
const rows = this.db
|
|
77
|
-
.query(`
|
|
78
|
-
SELECT
|
|
79
|
-
session_id,
|
|
80
|
-
ordinal,
|
|
81
|
-
conversation_key,
|
|
82
|
-
delivery_channel,
|
|
83
|
-
delivery_target,
|
|
84
|
-
delivery_topic,
|
|
85
|
-
updated_at_ms
|
|
86
|
-
FROM session_reply_targets
|
|
87
|
-
WHERE session_id = ?1
|
|
88
|
-
ORDER BY ordinal ASC;
|
|
89
|
-
`)
|
|
90
|
-
.all(sessionId);
|
|
91
|
-
return rows.map(mapSessionReplyTargetRow);
|
|
92
|
-
}
|
|
93
|
-
getDefaultSessionReplyTarget(sessionId) {
|
|
94
|
-
const row = this.db
|
|
95
|
-
.query(`
|
|
96
|
-
SELECT
|
|
97
|
-
session_id,
|
|
98
|
-
ordinal,
|
|
99
|
-
conversation_key,
|
|
100
|
-
delivery_channel,
|
|
101
|
-
delivery_target,
|
|
102
|
-
delivery_topic,
|
|
103
|
-
updated_at_ms
|
|
104
|
-
FROM session_reply_targets
|
|
105
|
-
WHERE session_id = ?1
|
|
106
|
-
ORDER BY ordinal ASC
|
|
107
|
-
LIMIT 1;
|
|
108
|
-
`)
|
|
109
|
-
.get(sessionId);
|
|
110
|
-
return row ? mapSessionReplyTargetRow(row) : null;
|
|
111
|
-
}
|
|
112
|
-
hasGatewaySession(sessionId) {
|
|
113
|
-
const binding = this.db
|
|
114
|
-
.query(`
|
|
115
|
-
SELECT 1 AS present
|
|
116
|
-
FROM session_bindings
|
|
117
|
-
WHERE session_id = ?1
|
|
118
|
-
LIMIT 1;
|
|
119
|
-
`)
|
|
120
|
-
.get(sessionId);
|
|
121
|
-
if (binding?.present === 1) {
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
const replyTarget = this.db
|
|
125
|
-
.query(`
|
|
126
|
-
SELECT 1 AS present
|
|
127
|
-
FROM session_reply_targets
|
|
128
|
-
WHERE session_id = ?1
|
|
129
|
-
LIMIT 1;
|
|
130
|
-
`)
|
|
131
|
-
.get(sessionId);
|
|
132
|
-
return replyTarget?.present === 1;
|
|
133
|
-
}
|
|
134
|
-
appendJournal(entry) {
|
|
135
|
-
this.db
|
|
136
|
-
.query(`
|
|
137
|
-
INSERT INTO runtime_journal (kind, recorded_at_ms, conversation_key, payload_json)
|
|
138
|
-
VALUES (?1, ?2, ?3, ?4);
|
|
139
|
-
`)
|
|
140
|
-
.run(entry.kind, entry.recordedAtMs, entry.conversationKey, JSON.stringify(entry.payload));
|
|
141
|
-
}
|
|
142
|
-
replacePendingQuestion(input) {
|
|
143
|
-
assertSafeInteger(input.recordedAtMs, "pending question recordedAtMs");
|
|
144
|
-
const deleteQuestion = this.db.query("DELETE FROM pending_questions WHERE request_id = ?1;");
|
|
145
|
-
const insertQuestion = this.db.query(`
|
|
146
|
-
INSERT INTO pending_questions (
|
|
147
|
-
request_id,
|
|
148
|
-
session_id,
|
|
149
|
-
delivery_channel,
|
|
150
|
-
delivery_target,
|
|
151
|
-
delivery_topic,
|
|
152
|
-
question_json,
|
|
153
|
-
telegram_message_id,
|
|
154
|
-
created_at_ms
|
|
155
|
-
)
|
|
156
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);
|
|
157
|
-
`);
|
|
158
|
-
this.db.transaction((payload) => {
|
|
159
|
-
deleteQuestion.run(payload.requestId);
|
|
160
|
-
for (const target of payload.targets) {
|
|
161
|
-
insertQuestion.run(payload.requestId, payload.sessionId, target.deliveryTarget.channel, target.deliveryTarget.target, normalizeKeyField(target.deliveryTarget.topic), JSON.stringify(payload.questions), target.telegramMessageId, payload.recordedAtMs);
|
|
162
|
-
}
|
|
163
|
-
})(input);
|
|
164
|
-
}
|
|
165
|
-
deletePendingQuestion(requestId) {
|
|
166
|
-
this.db.query("DELETE FROM pending_questions WHERE request_id = ?1;").run(requestId);
|
|
167
|
-
}
|
|
168
|
-
deletePendingQuestionsForSession(sessionId) {
|
|
169
|
-
this.db.query("DELETE FROM pending_questions WHERE session_id = ?1;").run(sessionId);
|
|
170
|
-
}
|
|
171
|
-
getPendingQuestionForTarget(target) {
|
|
172
|
-
const row = this.db
|
|
173
|
-
.query(`
|
|
174
|
-
SELECT
|
|
175
|
-
id,
|
|
176
|
-
request_id,
|
|
177
|
-
session_id,
|
|
178
|
-
delivery_channel,
|
|
179
|
-
delivery_target,
|
|
180
|
-
delivery_topic,
|
|
181
|
-
question_json,
|
|
182
|
-
telegram_message_id,
|
|
183
|
-
created_at_ms
|
|
184
|
-
FROM pending_questions
|
|
185
|
-
WHERE delivery_channel = ?1
|
|
186
|
-
AND delivery_target = ?2
|
|
187
|
-
AND delivery_topic = ?3
|
|
188
|
-
ORDER BY created_at_ms ASC, id ASC
|
|
189
|
-
LIMIT 1;
|
|
190
|
-
`)
|
|
191
|
-
.get(target.channel, target.target, normalizeKeyField(target.topic));
|
|
192
|
-
return row ? mapPendingQuestionRow(row) : null;
|
|
193
|
-
}
|
|
194
|
-
getPendingQuestionForTelegramMessage(target, telegramMessageId) {
|
|
195
|
-
assertSafeInteger(telegramMessageId, "pending question telegramMessageId");
|
|
196
|
-
const row = this.db
|
|
197
|
-
.query(`
|
|
198
|
-
SELECT
|
|
199
|
-
id,
|
|
200
|
-
request_id,
|
|
201
|
-
session_id,
|
|
202
|
-
delivery_channel,
|
|
203
|
-
delivery_target,
|
|
204
|
-
delivery_topic,
|
|
205
|
-
question_json,
|
|
206
|
-
telegram_message_id,
|
|
207
|
-
created_at_ms
|
|
208
|
-
FROM pending_questions
|
|
209
|
-
WHERE delivery_channel = ?1
|
|
210
|
-
AND delivery_target = ?2
|
|
211
|
-
AND delivery_topic = ?3
|
|
212
|
-
AND telegram_message_id = ?4
|
|
213
|
-
ORDER BY created_at_ms ASC, id ASC
|
|
214
|
-
LIMIT 1;
|
|
215
|
-
`)
|
|
216
|
-
.get(target.channel, target.target, normalizeKeyField(target.topic), telegramMessageId);
|
|
217
|
-
return row ? mapPendingQuestionRow(row) : null;
|
|
218
|
-
}
|
|
219
|
-
hasMailboxEntry(sourceKind, externalId) {
|
|
220
|
-
const row = this.db
|
|
221
|
-
.query(`
|
|
222
|
-
SELECT 1 AS present
|
|
223
|
-
FROM mailbox_entries
|
|
224
|
-
WHERE source_kind = ?1 AND external_id = ?2
|
|
225
|
-
LIMIT 1;
|
|
226
|
-
`)
|
|
227
|
-
.get(sourceKind, externalId);
|
|
228
|
-
return row?.present === 1;
|
|
229
|
-
}
|
|
230
|
-
enqueueMailboxEntry(input) {
|
|
231
|
-
assertSafeInteger(input.recordedAtMs, "mailbox recordedAtMs");
|
|
232
|
-
const insertEntry = this.db.query(`
|
|
233
|
-
INSERT INTO mailbox_entries (
|
|
234
|
-
mailbox_key,
|
|
235
|
-
source_kind,
|
|
236
|
-
external_id,
|
|
237
|
-
sender,
|
|
238
|
-
body,
|
|
239
|
-
reply_channel,
|
|
240
|
-
reply_target,
|
|
241
|
-
reply_topic,
|
|
242
|
-
created_at_ms
|
|
243
|
-
)
|
|
244
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
|
245
|
-
ON CONFLICT(source_kind, external_id) DO NOTHING;
|
|
246
|
-
`);
|
|
247
|
-
const insertAttachment = this.db.query(`
|
|
248
|
-
INSERT INTO mailbox_entry_attachments (
|
|
249
|
-
mailbox_entry_id,
|
|
250
|
-
ordinal,
|
|
251
|
-
kind,
|
|
252
|
-
mime_type,
|
|
253
|
-
file_name,
|
|
254
|
-
local_path
|
|
255
|
-
)
|
|
256
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6);
|
|
257
|
-
`);
|
|
258
|
-
this.db.transaction((payload) => {
|
|
259
|
-
const result = insertEntry.run(payload.mailboxKey, payload.sourceKind, payload.externalId, payload.sender, payload.text ?? "", payload.replyChannel, payload.replyTarget, payload.replyTopic, payload.recordedAtMs);
|
|
260
|
-
if (result.changes === 0) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const entryId = Number(result.lastInsertRowid);
|
|
264
|
-
for (const [index, attachment] of payload.attachments.entries()) {
|
|
265
|
-
insertAttachment.run(entryId, index, attachment.kind, attachment.mimeType, attachment.fileName, attachment.localPath);
|
|
266
|
-
}
|
|
267
|
-
})(input);
|
|
268
|
-
}
|
|
269
|
-
listPendingMailboxKeys() {
|
|
270
|
-
const rows = this.db
|
|
271
|
-
.query(`
|
|
272
|
-
SELECT DISTINCT mailbox_key
|
|
273
|
-
FROM mailbox_entries
|
|
274
|
-
ORDER BY mailbox_key ASC;
|
|
275
|
-
`)
|
|
276
|
-
.all();
|
|
277
|
-
return rows.map((row) => row.mailbox_key);
|
|
278
|
-
}
|
|
279
|
-
listMailboxEntries(mailboxKey) {
|
|
280
|
-
const rows = this.db
|
|
281
|
-
.query(`
|
|
282
|
-
SELECT
|
|
283
|
-
id,
|
|
284
|
-
mailbox_key,
|
|
285
|
-
source_kind,
|
|
286
|
-
external_id,
|
|
287
|
-
sender,
|
|
288
|
-
body,
|
|
289
|
-
reply_channel,
|
|
290
|
-
reply_target,
|
|
291
|
-
reply_topic,
|
|
292
|
-
created_at_ms
|
|
293
|
-
FROM mailbox_entries
|
|
294
|
-
WHERE mailbox_key = ?1
|
|
295
|
-
ORDER BY id ASC;
|
|
296
|
-
`)
|
|
297
|
-
.all(mailboxKey);
|
|
298
|
-
const attachments = listMailboxAttachments(this.db, rows.map((row) => row.id));
|
|
299
|
-
return rows.map((row) => mapMailboxEntryRow(row, attachments.get(row.id) ?? []));
|
|
300
|
-
}
|
|
301
|
-
deleteMailboxEntries(ids) {
|
|
302
|
-
if (ids.length === 0) {
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
for (const id of ids) {
|
|
306
|
-
assertSafeInteger(id, "mailbox entry id");
|
|
307
|
-
}
|
|
308
|
-
const placeholders = ids.map((_, index) => `?${index + 1}`).join(", ");
|
|
309
|
-
this.db.query(`DELETE FROM mailbox_entries WHERE id IN (${placeholders});`).run(...ids);
|
|
310
|
-
}
|
|
311
|
-
getTelegramUpdateOffset() {
|
|
312
|
-
const value = this.getStateValue("telegram.update_offset");
|
|
313
|
-
if (value === null) {
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
|
-
return parseStoredInteger(value, "stored telegram.update_offset");
|
|
317
|
-
}
|
|
318
|
-
putTelegramUpdateOffset(offset, recordedAtMs) {
|
|
319
|
-
assertSafeInteger(offset, "telegram update offset");
|
|
320
|
-
this.putStateValue("telegram.update_offset", String(offset), recordedAtMs);
|
|
321
|
-
}
|
|
322
|
-
getStateValue(key) {
|
|
323
|
-
const row = this.db.query("SELECT value FROM kv_state WHERE key = ?1;").get(key);
|
|
324
|
-
return row?.value ?? null;
|
|
325
|
-
}
|
|
326
|
-
putStateValue(key, value, recordedAtMs) {
|
|
327
|
-
this.db
|
|
328
|
-
.query(`
|
|
329
|
-
INSERT INTO kv_state (key, value, updated_at_ms)
|
|
330
|
-
VALUES (?1, ?2, ?3)
|
|
331
|
-
ON CONFLICT(key) DO UPDATE SET
|
|
332
|
-
value = excluded.value,
|
|
333
|
-
updated_at_ms = excluded.updated_at_ms;
|
|
334
|
-
`)
|
|
335
|
-
.run(key, value, recordedAtMs);
|
|
336
|
-
}
|
|
337
|
-
upsertCronJob(input) {
|
|
338
|
-
assertSafeInteger(input.nextRunAtMs, "cron next_run_at_ms");
|
|
339
|
-
assertSafeInteger(input.recordedAtMs, "cron recordedAtMs");
|
|
340
|
-
if (input.runAtMs !== null) {
|
|
341
|
-
assertSafeInteger(input.runAtMs, "cron run_at_ms");
|
|
342
|
-
}
|
|
343
|
-
this.db
|
|
344
|
-
.query(`
|
|
345
|
-
INSERT INTO cron_jobs (
|
|
346
|
-
id,
|
|
347
|
-
kind,
|
|
348
|
-
schedule,
|
|
349
|
-
run_at_ms,
|
|
350
|
-
prompt,
|
|
351
|
-
delivery_channel,
|
|
352
|
-
delivery_target,
|
|
353
|
-
delivery_topic,
|
|
354
|
-
enabled,
|
|
355
|
-
next_run_at_ms,
|
|
356
|
-
created_at_ms,
|
|
357
|
-
updated_at_ms
|
|
358
|
-
)
|
|
359
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?11)
|
|
360
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
361
|
-
kind = excluded.kind,
|
|
362
|
-
schedule = excluded.schedule,
|
|
363
|
-
run_at_ms = excluded.run_at_ms,
|
|
364
|
-
prompt = excluded.prompt,
|
|
365
|
-
delivery_channel = excluded.delivery_channel,
|
|
366
|
-
delivery_target = excluded.delivery_target,
|
|
367
|
-
delivery_topic = excluded.delivery_topic,
|
|
368
|
-
enabled = excluded.enabled,
|
|
369
|
-
next_run_at_ms = excluded.next_run_at_ms,
|
|
370
|
-
updated_at_ms = excluded.updated_at_ms;
|
|
371
|
-
`)
|
|
372
|
-
.run(input.id, input.kind, encodeStoredSchedule(input.kind, input.schedule), input.runAtMs, input.prompt, input.deliveryChannel, input.deliveryTarget, input.deliveryTopic, input.enabled ? 1 : 0, input.nextRunAtMs, input.recordedAtMs);
|
|
373
|
-
}
|
|
374
|
-
getCronJob(id) {
|
|
375
|
-
const row = this.db
|
|
376
|
-
.query(`
|
|
377
|
-
SELECT
|
|
378
|
-
id,
|
|
379
|
-
kind,
|
|
380
|
-
schedule,
|
|
381
|
-
run_at_ms,
|
|
382
|
-
prompt,
|
|
383
|
-
delivery_channel,
|
|
384
|
-
delivery_target,
|
|
385
|
-
delivery_topic,
|
|
386
|
-
enabled,
|
|
387
|
-
next_run_at_ms,
|
|
388
|
-
created_at_ms,
|
|
389
|
-
updated_at_ms
|
|
390
|
-
FROM cron_jobs
|
|
391
|
-
WHERE id = ?1;
|
|
392
|
-
`)
|
|
393
|
-
.get(id);
|
|
394
|
-
return row ? mapCronJobRow(row) : null;
|
|
395
|
-
}
|
|
396
|
-
listCronJobs() {
|
|
397
|
-
const rows = this.db
|
|
398
|
-
.query(`
|
|
399
|
-
SELECT
|
|
400
|
-
id,
|
|
401
|
-
kind,
|
|
402
|
-
schedule,
|
|
403
|
-
run_at_ms,
|
|
404
|
-
prompt,
|
|
405
|
-
delivery_channel,
|
|
406
|
-
delivery_target,
|
|
407
|
-
delivery_topic,
|
|
408
|
-
enabled,
|
|
409
|
-
next_run_at_ms,
|
|
410
|
-
created_at_ms,
|
|
411
|
-
updated_at_ms
|
|
412
|
-
FROM cron_jobs
|
|
413
|
-
ORDER BY id ASC;
|
|
414
|
-
`)
|
|
415
|
-
.all();
|
|
416
|
-
return rows.map(mapCronJobRow);
|
|
417
|
-
}
|
|
418
|
-
listOverdueCronJobs(nowMs) {
|
|
419
|
-
assertSafeInteger(nowMs, "cron nowMs");
|
|
420
|
-
const rows = this.db
|
|
421
|
-
.query(`
|
|
422
|
-
SELECT
|
|
423
|
-
id,
|
|
424
|
-
kind,
|
|
425
|
-
schedule,
|
|
426
|
-
run_at_ms,
|
|
427
|
-
prompt,
|
|
428
|
-
delivery_channel,
|
|
429
|
-
delivery_target,
|
|
430
|
-
delivery_topic,
|
|
431
|
-
enabled,
|
|
432
|
-
next_run_at_ms,
|
|
433
|
-
created_at_ms,
|
|
434
|
-
updated_at_ms
|
|
435
|
-
FROM cron_jobs
|
|
436
|
-
WHERE kind = 'cron' AND enabled = 1 AND next_run_at_ms <= ?1
|
|
437
|
-
ORDER BY next_run_at_ms ASC, id ASC;
|
|
438
|
-
`)
|
|
439
|
-
.all(nowMs);
|
|
440
|
-
return rows.map(mapCronJobRow);
|
|
441
|
-
}
|
|
442
|
-
listDueCronJobs(nowMs, limit) {
|
|
443
|
-
assertSafeInteger(nowMs, "cron nowMs");
|
|
444
|
-
assertSafeInteger(limit, "cron due-job limit");
|
|
445
|
-
const rows = this.db
|
|
446
|
-
.query(`
|
|
447
|
-
SELECT
|
|
448
|
-
id,
|
|
449
|
-
kind,
|
|
450
|
-
schedule,
|
|
451
|
-
run_at_ms,
|
|
452
|
-
prompt,
|
|
453
|
-
delivery_channel,
|
|
454
|
-
delivery_target,
|
|
455
|
-
delivery_topic,
|
|
456
|
-
enabled,
|
|
457
|
-
next_run_at_ms,
|
|
458
|
-
created_at_ms,
|
|
459
|
-
updated_at_ms
|
|
460
|
-
FROM cron_jobs
|
|
461
|
-
WHERE enabled = 1 AND next_run_at_ms <= ?1
|
|
462
|
-
ORDER BY next_run_at_ms ASC, id ASC
|
|
463
|
-
LIMIT ?2;
|
|
464
|
-
`)
|
|
465
|
-
.all(nowMs, limit);
|
|
466
|
-
return rows.map(mapCronJobRow);
|
|
467
|
-
}
|
|
468
|
-
removeCronJob(id) {
|
|
469
|
-
const result = this.db.query("DELETE FROM cron_jobs WHERE id = ?1;").run(id);
|
|
470
|
-
return result.changes > 0;
|
|
471
|
-
}
|
|
472
|
-
updateCronJobNextRun(id, nextRunAtMs, recordedAtMs) {
|
|
473
|
-
assertSafeInteger(nextRunAtMs, "cron next_run_at_ms");
|
|
474
|
-
assertSafeInteger(recordedAtMs, "cron recordedAtMs");
|
|
475
|
-
this.db
|
|
476
|
-
.query(`
|
|
477
|
-
UPDATE cron_jobs
|
|
478
|
-
SET next_run_at_ms = ?2, updated_at_ms = ?3
|
|
479
|
-
WHERE id = ?1;
|
|
480
|
-
`)
|
|
481
|
-
.run(id, nextRunAtMs, recordedAtMs);
|
|
482
|
-
}
|
|
483
|
-
setCronJobEnabled(id, enabled, recordedAtMs) {
|
|
484
|
-
assertSafeInteger(recordedAtMs, "cron recordedAtMs");
|
|
485
|
-
this.db
|
|
486
|
-
.query(`
|
|
487
|
-
UPDATE cron_jobs
|
|
488
|
-
SET enabled = ?2,
|
|
489
|
-
updated_at_ms = ?3
|
|
490
|
-
WHERE id = ?1;
|
|
491
|
-
`)
|
|
492
|
-
.run(id, enabled ? 1 : 0, recordedAtMs);
|
|
493
|
-
}
|
|
494
|
-
listCronRuns(jobId, limit) {
|
|
495
|
-
assertSafeInteger(limit, "cron run limit");
|
|
496
|
-
const rows = this.db
|
|
497
|
-
.query(`
|
|
498
|
-
SELECT
|
|
499
|
-
id,
|
|
500
|
-
job_id,
|
|
501
|
-
scheduled_for_ms,
|
|
502
|
-
started_at_ms,
|
|
503
|
-
finished_at_ms,
|
|
504
|
-
status,
|
|
505
|
-
response_text,
|
|
506
|
-
error_message
|
|
507
|
-
FROM cron_runs
|
|
508
|
-
WHERE job_id = ?1
|
|
509
|
-
ORDER BY started_at_ms DESC, id DESC
|
|
510
|
-
LIMIT ?2;
|
|
511
|
-
`)
|
|
512
|
-
.all(jobId, limit);
|
|
513
|
-
return rows.map((row) => ({
|
|
514
|
-
id: row.id,
|
|
515
|
-
jobId: row.job_id,
|
|
516
|
-
scheduledForMs: row.scheduled_for_ms,
|
|
517
|
-
startedAtMs: row.started_at_ms,
|
|
518
|
-
finishedAtMs: row.finished_at_ms,
|
|
519
|
-
status: row.status,
|
|
520
|
-
responseText: row.response_text,
|
|
521
|
-
errorMessage: row.error_message,
|
|
522
|
-
}));
|
|
523
|
-
}
|
|
524
|
-
insertCronRun(jobId, scheduledForMs, startedAtMs) {
|
|
525
|
-
assertSafeInteger(scheduledForMs, "cron scheduled_for_ms");
|
|
526
|
-
assertSafeInteger(startedAtMs, "cron started_at_ms");
|
|
527
|
-
const result = this.db
|
|
528
|
-
.query(`
|
|
529
|
-
INSERT INTO cron_runs (
|
|
530
|
-
job_id,
|
|
531
|
-
scheduled_for_ms,
|
|
532
|
-
started_at_ms,
|
|
533
|
-
finished_at_ms,
|
|
534
|
-
status,
|
|
535
|
-
response_text,
|
|
536
|
-
error_message
|
|
537
|
-
)
|
|
538
|
-
VALUES (?1, ?2, ?3, NULL, 'running', NULL, NULL);
|
|
539
|
-
`)
|
|
540
|
-
.run(jobId, scheduledForMs, startedAtMs);
|
|
541
|
-
return Number(result.lastInsertRowid);
|
|
542
|
-
}
|
|
543
|
-
finishCronRun(runId, status, finishedAtMs, responseText, errorMessage) {
|
|
544
|
-
assertSafeInteger(runId, "cron run id");
|
|
545
|
-
assertSafeInteger(finishedAtMs, "cron finished_at_ms");
|
|
546
|
-
this.db
|
|
547
|
-
.query(`
|
|
548
|
-
UPDATE cron_runs
|
|
549
|
-
SET
|
|
550
|
-
finished_at_ms = ?2,
|
|
551
|
-
status = ?3,
|
|
552
|
-
response_text = ?4,
|
|
553
|
-
error_message = ?5
|
|
554
|
-
WHERE id = ?1;
|
|
555
|
-
`)
|
|
556
|
-
.run(runId, finishedAtMs, status, responseText, errorMessage);
|
|
557
|
-
}
|
|
558
|
-
abandonRunningCronRuns(finishedAtMs) {
|
|
559
|
-
assertSafeInteger(finishedAtMs, "cron abandoned finished_at_ms");
|
|
560
|
-
const result = this.db
|
|
561
|
-
.query(`
|
|
562
|
-
UPDATE cron_runs
|
|
563
|
-
SET
|
|
564
|
-
finished_at_ms = ?1,
|
|
565
|
-
status = 'abandoned',
|
|
566
|
-
error_message = 'gateway process stopped before this run completed'
|
|
567
|
-
WHERE status = 'running';
|
|
568
|
-
`)
|
|
569
|
-
.run(finishedAtMs);
|
|
570
|
-
return result.changes;
|
|
571
|
-
}
|
|
572
|
-
close() {
|
|
573
|
-
this.db.close();
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
function mapCronJobRow(row) {
|
|
577
|
-
const kind = parseScheduleJobKind(row.kind);
|
|
578
|
-
return {
|
|
579
|
-
id: row.id,
|
|
580
|
-
kind,
|
|
581
|
-
schedule: kind === "cron" ? row.schedule : null,
|
|
582
|
-
runAtMs: row.run_at_ms,
|
|
583
|
-
prompt: row.prompt,
|
|
584
|
-
deliveryChannel: row.delivery_channel,
|
|
585
|
-
deliveryTarget: row.delivery_target,
|
|
586
|
-
deliveryTopic: row.delivery_topic,
|
|
587
|
-
enabled: row.enabled === 1,
|
|
588
|
-
nextRunAtMs: row.next_run_at_ms,
|
|
589
|
-
createdAtMs: row.created_at_ms,
|
|
590
|
-
updatedAtMs: row.updated_at_ms,
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
function parseScheduleJobKind(value) {
|
|
594
|
-
switch (value) {
|
|
595
|
-
case "cron":
|
|
596
|
-
case "once":
|
|
597
|
-
return value;
|
|
598
|
-
default:
|
|
599
|
-
throw new Error(`stored schedule job kind is invalid: ${value}`);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
function encodeStoredSchedule(kind, schedule) {
|
|
603
|
-
if (kind === "once") {
|
|
604
|
-
return "@once";
|
|
605
|
-
}
|
|
606
|
-
if (schedule === null) {
|
|
607
|
-
throw new Error("cron schedule must not be null");
|
|
608
|
-
}
|
|
609
|
-
return schedule;
|
|
610
|
-
}
|
|
611
|
-
function mapMailboxEntryRow(row, attachments) {
|
|
612
|
-
return {
|
|
613
|
-
id: row.id,
|
|
614
|
-
mailboxKey: row.mailbox_key,
|
|
615
|
-
sourceKind: row.source_kind,
|
|
616
|
-
externalId: row.external_id,
|
|
617
|
-
sender: row.sender,
|
|
618
|
-
text: normalizeStoredMailboxText(row.body),
|
|
619
|
-
attachments,
|
|
620
|
-
replyChannel: row.reply_channel,
|
|
621
|
-
replyTarget: row.reply_target,
|
|
622
|
-
replyTopic: row.reply_topic,
|
|
623
|
-
createdAtMs: row.created_at_ms,
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
function listMailboxAttachments(db, entryIds) {
|
|
627
|
-
if (entryIds.length === 0) {
|
|
628
|
-
return new Map();
|
|
629
|
-
}
|
|
630
|
-
for (const entryId of entryIds) {
|
|
631
|
-
assertSafeInteger(entryId, "mailbox entry id");
|
|
632
|
-
}
|
|
633
|
-
const placeholders = entryIds.map((_, index) => `?${index + 1}`).join(", ");
|
|
634
|
-
const rows = db
|
|
635
|
-
.query(`
|
|
636
|
-
SELECT
|
|
637
|
-
mailbox_entry_id,
|
|
638
|
-
ordinal,
|
|
639
|
-
kind,
|
|
640
|
-
mime_type,
|
|
641
|
-
file_name,
|
|
642
|
-
local_path
|
|
643
|
-
FROM mailbox_entry_attachments
|
|
644
|
-
WHERE mailbox_entry_id IN (${placeholders})
|
|
645
|
-
ORDER BY mailbox_entry_id ASC, ordinal ASC;
|
|
646
|
-
`)
|
|
647
|
-
.all(...entryIds);
|
|
648
|
-
const attachments = new Map();
|
|
649
|
-
for (const row of rows) {
|
|
650
|
-
const records = attachments.get(row.mailbox_entry_id) ?? [];
|
|
651
|
-
records.push(mapMailboxEntryAttachmentRow(row));
|
|
652
|
-
attachments.set(row.mailbox_entry_id, records);
|
|
653
|
-
}
|
|
654
|
-
return attachments;
|
|
655
|
-
}
|
|
656
|
-
function mapMailboxEntryAttachmentRow(row) {
|
|
657
|
-
switch (row.kind) {
|
|
658
|
-
case "image":
|
|
659
|
-
return {
|
|
660
|
-
kind: "image",
|
|
661
|
-
ordinal: row.ordinal,
|
|
662
|
-
mimeType: row.mime_type,
|
|
663
|
-
fileName: row.file_name,
|
|
664
|
-
localPath: row.local_path,
|
|
665
|
-
};
|
|
666
|
-
default:
|
|
667
|
-
throw new Error(`unsupported mailbox attachment kind: ${row.kind}`);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
function mapSessionReplyTargetRow(row) {
|
|
671
|
-
return {
|
|
672
|
-
channel: row.delivery_channel,
|
|
673
|
-
target: row.delivery_target,
|
|
674
|
-
topic: normalizeStoredKeyField(row.delivery_topic),
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
function mapPendingQuestionRow(row) {
|
|
678
|
-
return {
|
|
679
|
-
requestId: row.request_id,
|
|
680
|
-
sessionId: row.session_id,
|
|
681
|
-
questions: parsePendingQuestions(row.question_json),
|
|
682
|
-
deliveryTarget: {
|
|
683
|
-
channel: row.delivery_channel,
|
|
684
|
-
target: row.delivery_target,
|
|
685
|
-
topic: normalizeStoredKeyField(row.delivery_topic),
|
|
686
|
-
},
|
|
687
|
-
telegramMessageId: row.telegram_message_id,
|
|
688
|
-
createdAtMs: row.created_at_ms,
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
function parsePendingQuestions(value) {
|
|
692
|
-
const parsed = JSON.parse(value);
|
|
693
|
-
if (!Array.isArray(parsed)) {
|
|
694
|
-
throw new Error("stored pending question payload is invalid");
|
|
695
|
-
}
|
|
696
|
-
return parsed.map((question, index) => {
|
|
697
|
-
if (typeof question !== "object" || question === null) {
|
|
698
|
-
throw new Error(`stored pending question ${index} is invalid`);
|
|
699
|
-
}
|
|
700
|
-
const header = readRequiredStringField(question, "header");
|
|
701
|
-
const prompt = readRequiredStringField(question, "question");
|
|
702
|
-
const rawOptions = readArrayField(question, "options");
|
|
703
|
-
const options = rawOptions.map((option, optionIndex) => {
|
|
704
|
-
if (typeof option !== "object" || option === null) {
|
|
705
|
-
throw new Error(`stored pending question option ${index}:${optionIndex} is invalid`);
|
|
706
|
-
}
|
|
707
|
-
return {
|
|
708
|
-
label: readRequiredStringField(option, "label"),
|
|
709
|
-
description: readRequiredStringField(option, "description"),
|
|
710
|
-
};
|
|
711
|
-
});
|
|
712
|
-
return {
|
|
713
|
-
header,
|
|
714
|
-
question: prompt,
|
|
715
|
-
options,
|
|
716
|
-
multiple: readBooleanField(question, "multiple", false),
|
|
717
|
-
custom: readBooleanField(question, "custom", true),
|
|
718
|
-
};
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
function assertSafeInteger(value, field) {
|
|
722
|
-
if (!Number.isSafeInteger(value) || value < 0) {
|
|
723
|
-
throw new Error(`${field} is out of range: ${value}`);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
function parseStoredInteger(value, field) {
|
|
727
|
-
const parsed = Number.parseInt(value, 10);
|
|
728
|
-
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
729
|
-
throw new Error(`${field} is invalid: ${value}`);
|
|
730
|
-
}
|
|
731
|
-
return parsed;
|
|
732
|
-
}
|
|
733
|
-
function normalizeStoredMailboxText(value) {
|
|
734
|
-
const trimmed = value.trim();
|
|
735
|
-
return trimmed.length === 0 ? null : trimmed;
|
|
736
|
-
}
|
|
737
|
-
function normalizeKeyField(value) {
|
|
738
|
-
if (value === null) {
|
|
739
|
-
return "";
|
|
740
|
-
}
|
|
741
|
-
const trimmed = value.trim();
|
|
742
|
-
return trimmed.length === 0 ? "" : trimmed;
|
|
743
|
-
}
|
|
744
|
-
function normalizeStoredKeyField(value) {
|
|
745
|
-
const trimmed = value.trim();
|
|
746
|
-
return trimmed.length === 0 ? null : trimmed;
|
|
747
|
-
}
|
|
748
|
-
function readRequiredStringField(value, field) {
|
|
749
|
-
const raw = value[field];
|
|
750
|
-
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
751
|
-
throw new Error(`stored field ${field} is invalid`);
|
|
752
|
-
}
|
|
753
|
-
return raw;
|
|
754
|
-
}
|
|
755
|
-
function readArrayField(value, field) {
|
|
756
|
-
const raw = value[field];
|
|
757
|
-
if (!Array.isArray(raw)) {
|
|
758
|
-
throw new Error(`stored field ${field} is invalid`);
|
|
759
|
-
}
|
|
760
|
-
return raw;
|
|
761
|
-
}
|
|
762
|
-
function readBooleanField(value, field, fallback) {
|
|
763
|
-
const raw = value[field];
|
|
764
|
-
if (raw === undefined) {
|
|
765
|
-
return fallback;
|
|
766
|
-
}
|
|
767
|
-
if (typeof raw !== "boolean") {
|
|
768
|
-
throw new Error(`stored field ${field} is invalid`);
|
|
769
|
-
}
|
|
770
|
-
return raw;
|
|
771
|
-
}
|
|
772
|
-
export async function openSqliteStore(path) {
|
|
773
|
-
await mkdir(dirname(path), { recursive: true });
|
|
774
|
-
const db = new Database(path);
|
|
775
|
-
migrateGatewayDatabase(db);
|
|
776
|
-
return new SqliteStore(db);
|
|
777
|
-
}
|