@clawling/clawchat-plugin-openclaw 2026.5.12-28
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/INSTALL.md +64 -0
- package/README.md +227 -0
- package/dist/index.js +20 -0
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +263 -0
- package/dist/src/api-types.js +17 -0
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/buffered-stream.js +177 -0
- package/dist/src/channel.js +66 -0
- package/dist/src/channel.setup.js +119 -0
- package/dist/src/clawchat-memory.js +403 -0
- package/dist/src/clawchat-metadata.js +310 -0
- package/dist/src/client.js +35 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +274 -0
- package/dist/src/group-message-coalescer.js +119 -0
- package/dist/src/inbound.js +170 -0
- package/dist/src/llm-context-debug.js +86 -0
- package/dist/src/login.runtime.js +204 -0
- package/dist/src/media-runtime.js +85 -0
- package/dist/src/message-mapper.js +146 -0
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +628 -0
- package/dist/src/plugin-prompts.js +89 -0
- package/dist/src/profile-prompt.js +269 -0
- package/dist/src/profile-sync.js +110 -0
- package/dist/src/prompt-injection.js +25 -0
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +33 -0
- package/dist/src/reply-dispatcher.js +422 -0
- package/dist/src/runtime.js +1254 -0
- package/dist/src/storage.js +525 -0
- package/dist/src/streaming.js +65 -0
- package/dist/src/terminal-send.js +36 -0
- package/dist/src/tools-schema.js +208 -0
- package/dist/src/tools.js +920 -0
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +24 -0
- package/openclaw.plugin.json +169 -0
- package/package.json +80 -0
- package/prompts/default-group-bio.md +19 -0
- package/prompts/default-owner-behavior.md +27 -0
- package/prompts/platform.md +13 -0
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +91 -0
- package/src/api-client.test.ts +827 -0
- package/src/api-client.ts +414 -0
- package/src/api-types.ts +146 -0
- package/src/channel.outbound.test.ts +433 -0
- package/src/channel.setup.ts +145 -0
- package/src/channel.test.ts +262 -0
- package/src/channel.ts +81 -0
- package/src/clawchat-memory.test.ts +480 -0
- package/src/clawchat-memory.ts +533 -0
- package/src/clawchat-metadata.test.ts +477 -0
- package/src/clawchat-metadata.ts +429 -0
- package/src/client.test.ts +169 -0
- package/src/client.ts +56 -0
- package/src/commands.test.ts +39 -0
- package/src/commands.ts +41 -0
- package/src/config.test.ts +344 -0
- package/src/config.ts +404 -0
- package/src/group-message-coalescer.test.ts +237 -0
- package/src/group-message-coalescer.ts +171 -0
- package/src/inbound.test.ts +508 -0
- package/src/inbound.ts +278 -0
- package/src/llm-context-debug.test.ts +55 -0
- package/src/llm-context-debug.ts +139 -0
- package/src/login.runtime.test.ts +737 -0
- package/src/login.runtime.ts +277 -0
- package/src/manifest.test.ts +352 -0
- package/src/media-runtime.test.ts +207 -0
- package/src/media-runtime.ts +152 -0
- package/src/message-mapper.test.ts +201 -0
- package/src/message-mapper.ts +174 -0
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +1269 -0
- package/src/outbound.ts +803 -0
- package/src/plugin-entry.test.ts +38 -0
- package/src/plugin-prompts.test.ts +94 -0
- package/src/plugin-prompts.ts +107 -0
- package/src/profile-prompt.test.ts +274 -0
- package/src/profile-prompt.ts +351 -0
- package/src/profile-sync.test.ts +539 -0
- package/src/profile-sync.ts +191 -0
- package/src/prompt-injection.test.ts +39 -0
- package/src/prompt-injection.ts +45 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.test.ts +39 -0
- package/src/protocol.ts +42 -0
- package/src/reply-dispatcher.test.ts +1324 -0
- package/src/reply-dispatcher.ts +555 -0
- package/src/runtime.test.ts +4719 -0
- package/src/runtime.ts +1493 -0
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +560 -0
- package/src/storage.ts +807 -0
- package/src/terminal-send.test.ts +81 -0
- package/src/terminal-send.ts +56 -0
- package/src/tools-schema.ts +337 -0
- package/src/tools.test.ts +933 -0
- package/src/tools.ts +1185 -0
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1217 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
package/src/storage.ts
ADDED
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
6
|
+
|
|
7
|
+
type Log = { error?: (message: string) => void };
|
|
8
|
+
|
|
9
|
+
export type ClawChatStoreOptions = {
|
|
10
|
+
dbPath?: string;
|
|
11
|
+
log?: Log;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ActivationInput = {
|
|
15
|
+
platform: string;
|
|
16
|
+
accountId: string;
|
|
17
|
+
userId?: string | null;
|
|
18
|
+
ownerUserId?: string | null;
|
|
19
|
+
accessToken?: string | null;
|
|
20
|
+
refreshToken?: string | null;
|
|
21
|
+
conversationId?: string | null;
|
|
22
|
+
activatedAt?: number;
|
|
23
|
+
loginMethod?: string | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ActivationBootstrapInput = {
|
|
27
|
+
platform: string;
|
|
28
|
+
accountId: string;
|
|
29
|
+
staleClaimMs?: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ActivationBootstrapConversation = { conversationId: string };
|
|
33
|
+
|
|
34
|
+
export type MarkActivationBootstrapSentInput = ActivationBootstrapInput & {
|
|
35
|
+
conversationId: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type ReleaseActivationBootstrapClaimInput = ActivationBootstrapInput & {
|
|
39
|
+
conversationId: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type MessageInput = {
|
|
43
|
+
platform: string;
|
|
44
|
+
accountId: string;
|
|
45
|
+
kind: string;
|
|
46
|
+
direction: string;
|
|
47
|
+
eventType: string;
|
|
48
|
+
traceId?: string | null;
|
|
49
|
+
chatId?: string | null;
|
|
50
|
+
messageId?: string | null;
|
|
51
|
+
text?: string | null;
|
|
52
|
+
raw?: unknown;
|
|
53
|
+
createdAt?: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type MessageAcknowledgedInput = {
|
|
57
|
+
accountId: string;
|
|
58
|
+
kind: string;
|
|
59
|
+
direction: string;
|
|
60
|
+
messageId: string;
|
|
61
|
+
protocolMessageId?: string | null;
|
|
62
|
+
ackedAt?: number | null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type StartConnectionInput = {
|
|
66
|
+
platform: string;
|
|
67
|
+
accountId: string;
|
|
68
|
+
attempt?: number | null;
|
|
69
|
+
reconnectCount?: number | null;
|
|
70
|
+
connectStartedAt?: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type ConnectionUpdateOptions = { at?: number };
|
|
74
|
+
|
|
75
|
+
export type ConnectionReadyOptions = ConnectionUpdateOptions & {
|
|
76
|
+
resolvedDeviceId?: string | null;
|
|
77
|
+
deliveryMode?: string | null;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type FinishConnectionInput = {
|
|
81
|
+
state: string;
|
|
82
|
+
disconnectedAt?: number;
|
|
83
|
+
closeCode?: number | null;
|
|
84
|
+
closeReason?: string | null;
|
|
85
|
+
error?: string | null;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type ToolCallInput = {
|
|
89
|
+
platform: string;
|
|
90
|
+
accountId?: string | null;
|
|
91
|
+
toolName: string;
|
|
92
|
+
args?: unknown;
|
|
93
|
+
result?: unknown;
|
|
94
|
+
error?: string | null;
|
|
95
|
+
startedAt?: number;
|
|
96
|
+
endedAt?: number | null;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type ConversationAccountInput = {
|
|
100
|
+
platform: string;
|
|
101
|
+
accountId: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type ActivationConversation = {
|
|
105
|
+
conversationId: string;
|
|
106
|
+
conversationType: string | null;
|
|
107
|
+
metadataVersion: number | null;
|
|
108
|
+
lastSeenAt: number | null;
|
|
109
|
+
lastRefreshedAt: number | null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
type Migration = { version: number; name: string; sql: string };
|
|
113
|
+
|
|
114
|
+
const DB_FILENAME = "clawchat.sqlite";
|
|
115
|
+
const DEFAULT_BOOTSTRAP_CLAIM_STALE_MS = 5 * 60 * 1000;
|
|
116
|
+
|
|
117
|
+
const MIGRATIONS: Migration[] = [
|
|
118
|
+
{
|
|
119
|
+
version: 1,
|
|
120
|
+
name: "initial_schema",
|
|
121
|
+
sql: `
|
|
122
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
123
|
+
version INTEGER PRIMARY KEY,
|
|
124
|
+
name TEXT NOT NULL,
|
|
125
|
+
applied_at INTEGER NOT NULL
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
CREATE TABLE IF NOT EXISTS clawchat_messages (
|
|
129
|
+
id INTEGER PRIMARY KEY,
|
|
130
|
+
platform TEXT NOT NULL,
|
|
131
|
+
account_id TEXT NOT NULL,
|
|
132
|
+
kind TEXT NOT NULL,
|
|
133
|
+
direction TEXT NOT NULL,
|
|
134
|
+
event_type TEXT NOT NULL,
|
|
135
|
+
trace_id TEXT,
|
|
136
|
+
chat_id TEXT,
|
|
137
|
+
message_id TEXT,
|
|
138
|
+
text TEXT,
|
|
139
|
+
raw_json TEXT,
|
|
140
|
+
created_at INTEGER NOT NULL
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS activations (
|
|
144
|
+
platform TEXT NOT NULL,
|
|
145
|
+
account_id TEXT NOT NULL,
|
|
146
|
+
user_id TEXT,
|
|
147
|
+
access_token TEXT,
|
|
148
|
+
refresh_token TEXT,
|
|
149
|
+
activated_at INTEGER NOT NULL,
|
|
150
|
+
login_method TEXT,
|
|
151
|
+
updated_at INTEGER NOT NULL,
|
|
152
|
+
PRIMARY KEY (platform, account_id)
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
CREATE TABLE IF NOT EXISTS connections (
|
|
156
|
+
id INTEGER PRIMARY KEY,
|
|
157
|
+
platform TEXT NOT NULL,
|
|
158
|
+
account_id TEXT NOT NULL,
|
|
159
|
+
attempt INTEGER,
|
|
160
|
+
reconnect_count INTEGER,
|
|
161
|
+
state TEXT NOT NULL,
|
|
162
|
+
connect_started_at INTEGER,
|
|
163
|
+
connect_sent_at INTEGER,
|
|
164
|
+
ready_at INTEGER,
|
|
165
|
+
disconnected_at INTEGER,
|
|
166
|
+
close_code INTEGER,
|
|
167
|
+
close_reason TEXT,
|
|
168
|
+
error TEXT,
|
|
169
|
+
created_at INTEGER NOT NULL,
|
|
170
|
+
updated_at INTEGER NOT NULL
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
174
|
+
id INTEGER PRIMARY KEY,
|
|
175
|
+
platform TEXT NOT NULL,
|
|
176
|
+
account_id TEXT,
|
|
177
|
+
tool_name TEXT NOT NULL,
|
|
178
|
+
args_json TEXT,
|
|
179
|
+
result_json TEXT,
|
|
180
|
+
error TEXT,
|
|
181
|
+
started_at INTEGER NOT NULL,
|
|
182
|
+
ended_at INTEGER,
|
|
183
|
+
duration_ms INTEGER,
|
|
184
|
+
created_at INTEGER NOT NULL
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_clawchat_messages_chat_created
|
|
188
|
+
ON clawchat_messages(chat_id, created_at);
|
|
189
|
+
CREATE INDEX IF NOT EXISTS idx_clawchat_messages_message_id
|
|
190
|
+
ON clawchat_messages(message_id);
|
|
191
|
+
CREATE INDEX IF NOT EXISTS idx_connections_account_created
|
|
192
|
+
ON connections(platform, account_id, created_at);
|
|
193
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_name_created
|
|
194
|
+
ON tool_calls(tool_name, created_at);
|
|
195
|
+
`,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
version: 2,
|
|
199
|
+
name: "message_idempotency",
|
|
200
|
+
sql: `
|
|
201
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_clawchat_messages_message_unique
|
|
202
|
+
ON clawchat_messages(account_id, direction, kind, message_id)
|
|
203
|
+
WHERE kind = 'message' AND message_id IS NOT NULL;
|
|
204
|
+
`,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
version: 3,
|
|
208
|
+
name: "activation_bootstrap",
|
|
209
|
+
sql: `
|
|
210
|
+
ALTER TABLE activations ADD COLUMN conversation_id TEXT;
|
|
211
|
+
ALTER TABLE activations ADD COLUMN bootstrap_sent INTEGER NOT NULL DEFAULT 1;
|
|
212
|
+
ALTER TABLE activations ADD COLUMN bootstrap_claimed_at INTEGER;
|
|
213
|
+
`,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
version: 4,
|
|
217
|
+
name: "activation_owner_user_id",
|
|
218
|
+
sql: `
|
|
219
|
+
ALTER TABLE activations ADD COLUMN owner_user_id TEXT;
|
|
220
|
+
`,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
version: 5,
|
|
224
|
+
name: "connection_metadata",
|
|
225
|
+
sql: `
|
|
226
|
+
ALTER TABLE connections ADD COLUMN resolved_device_id TEXT;
|
|
227
|
+
ALTER TABLE connections ADD COLUMN delivery_mode TEXT;
|
|
228
|
+
`,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
version: 6,
|
|
232
|
+
name: "message_ack_status",
|
|
233
|
+
sql: `
|
|
234
|
+
ALTER TABLE clawchat_messages ADD COLUMN send_status TEXT;
|
|
235
|
+
ALTER TABLE clawchat_messages ADD COLUMN protocol_message_id TEXT;
|
|
236
|
+
ALTER TABLE clawchat_messages ADD COLUMN acked_at INTEGER;
|
|
237
|
+
ALTER TABLE clawchat_messages ADD COLUMN send_error TEXT;
|
|
238
|
+
`,
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
function fallbackDbPath(): string {
|
|
243
|
+
const home = process.env.OPENCLAW_HOME || path.join(os.homedir(), ".openclaw");
|
|
244
|
+
return path.join(home, DB_FILENAME);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function clawChatDbPathForStateDir(stateDir: string): string {
|
|
248
|
+
return path.join(stateDir, DB_FILENAME);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function defaultDbPath(): string {
|
|
252
|
+
try {
|
|
253
|
+
return clawChatDbPathForStateDir(resolveStateDir());
|
|
254
|
+
} catch {
|
|
255
|
+
return fallbackDbPath();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function toNullableString(value: unknown): string | null {
|
|
260
|
+
return typeof value === "string" ? value : value == null ? null : String(value);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function toJson(value: unknown): string | null {
|
|
264
|
+
if (value === undefined) return null;
|
|
265
|
+
try {
|
|
266
|
+
return JSON.stringify(value);
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function hasOwn(value: object, key: PropertyKey): 0 | 1 {
|
|
273
|
+
return Object.prototype.hasOwnProperty.call(value, key) ? 1 : 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function safeErrorMessage(err: unknown): string {
|
|
277
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
278
|
+
return message
|
|
279
|
+
.replace(/(access[_-]?token["'\s:=]+)[^"'\s,}]+/gi, "$1[REDACTED]")
|
|
280
|
+
.replace(/(refresh[_-]?token["'\s:=]+)[^"'\s,}]+/gi, "$1[REDACTED]")
|
|
281
|
+
.replace(/(authorization["'\s:=]+bearer\s+)[^"'\s,}]+/gi, "$1[REDACTED]");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export class ClawChatStore {
|
|
285
|
+
readonly dbPath: string;
|
|
286
|
+
private readonly log?: Log;
|
|
287
|
+
private db: DatabaseSync | null = null;
|
|
288
|
+
private initialized = false;
|
|
289
|
+
private disabled = false;
|
|
290
|
+
|
|
291
|
+
constructor(options: ClawChatStoreOptions = {}) {
|
|
292
|
+
this.dbPath = options.dbPath ?? defaultDbPath();
|
|
293
|
+
this.log = options.log;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
initialize(): void {
|
|
297
|
+
this.ensureInitialized();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
listAppliedMigrations(): Array<{ version: number; name: string }> {
|
|
301
|
+
return this.read(() =>
|
|
302
|
+
this.requireDb()
|
|
303
|
+
.prepare("SELECT version, name FROM schema_migrations ORDER BY version")
|
|
304
|
+
.all() as Array<{ version: number; name: string }>,
|
|
305
|
+
) ?? [];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
upsertActivation(input: ActivationInput): void {
|
|
309
|
+
this.write(() => {
|
|
310
|
+
const now = input.activatedAt ?? Date.now();
|
|
311
|
+
const conversationId = input.conversationId?.trim() || null;
|
|
312
|
+
const userId = input.userId?.trim() || null;
|
|
313
|
+
const ownerUserId = input.ownerUserId?.trim() || null;
|
|
314
|
+
const accessToken = input.accessToken?.trim() || null;
|
|
315
|
+
const refreshToken = input.refreshToken?.trim() || null;
|
|
316
|
+
this.requireDb()
|
|
317
|
+
.prepare(
|
|
318
|
+
`INSERT INTO activations(
|
|
319
|
+
platform, account_id, user_id, owner_user_id, access_token, refresh_token,
|
|
320
|
+
activated_at, login_method, conversation_id, bootstrap_sent,
|
|
321
|
+
bootstrap_claimed_at, updated_at
|
|
322
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
323
|
+
ON CONFLICT(platform, account_id) DO UPDATE SET
|
|
324
|
+
user_id = excluded.user_id,
|
|
325
|
+
owner_user_id = excluded.owner_user_id,
|
|
326
|
+
access_token = excluded.access_token,
|
|
327
|
+
refresh_token = excluded.refresh_token,
|
|
328
|
+
activated_at = excluded.activated_at,
|
|
329
|
+
login_method = excluded.login_method,
|
|
330
|
+
conversation_id = excluded.conversation_id,
|
|
331
|
+
bootstrap_sent = excluded.bootstrap_sent,
|
|
332
|
+
bootstrap_claimed_at = NULL,
|
|
333
|
+
updated_at = excluded.updated_at`,
|
|
334
|
+
)
|
|
335
|
+
.run(
|
|
336
|
+
input.platform,
|
|
337
|
+
input.accountId,
|
|
338
|
+
userId,
|
|
339
|
+
ownerUserId,
|
|
340
|
+
accessToken,
|
|
341
|
+
refreshToken,
|
|
342
|
+
now,
|
|
343
|
+
input.loginMethod ?? null,
|
|
344
|
+
conversationId,
|
|
345
|
+
conversationId ? 0 : 1,
|
|
346
|
+
null,
|
|
347
|
+
now,
|
|
348
|
+
);
|
|
349
|
+
void conversationId;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
getActivationConversation(input: ConversationAccountInput): ActivationConversation | null {
|
|
354
|
+
return this.read(() => {
|
|
355
|
+
const row = this.requireDb()
|
|
356
|
+
.prepare(
|
|
357
|
+
`SELECT conversation_id
|
|
358
|
+
FROM activations
|
|
359
|
+
WHERE platform = ?
|
|
360
|
+
AND account_id = ?
|
|
361
|
+
AND conversation_id IS NOT NULL
|
|
362
|
+
AND conversation_id <> ''`,
|
|
363
|
+
)
|
|
364
|
+
.get(input.platform, input.accountId) as
|
|
365
|
+
| {
|
|
366
|
+
conversation_id?: unknown;
|
|
367
|
+
}
|
|
368
|
+
| undefined;
|
|
369
|
+
if (typeof row?.conversation_id !== "string") return null;
|
|
370
|
+
return {
|
|
371
|
+
conversationId: row.conversation_id,
|
|
372
|
+
conversationType: null,
|
|
373
|
+
metadataVersion: null,
|
|
374
|
+
lastSeenAt: null,
|
|
375
|
+
lastRefreshedAt: null,
|
|
376
|
+
};
|
|
377
|
+
}) ?? null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
claimPendingActivationBootstrap(
|
|
381
|
+
input: ActivationBootstrapInput,
|
|
382
|
+
): ActivationBootstrapConversation | null {
|
|
383
|
+
return this.write(() => {
|
|
384
|
+
const now = Date.now();
|
|
385
|
+
const staleMs = Math.max(0, input.staleClaimMs ?? DEFAULT_BOOTSTRAP_CLAIM_STALE_MS);
|
|
386
|
+
const staleBefore = now - staleMs;
|
|
387
|
+
const row = this.requireDb()
|
|
388
|
+
.prepare(
|
|
389
|
+
`UPDATE activations SET
|
|
390
|
+
bootstrap_claimed_at = ?, updated_at = ?
|
|
391
|
+
WHERE platform = ?
|
|
392
|
+
AND account_id = ?
|
|
393
|
+
AND conversation_id IS NOT NULL
|
|
394
|
+
AND conversation_id <> ''
|
|
395
|
+
AND bootstrap_sent = 0
|
|
396
|
+
AND (bootstrap_claimed_at IS NULL OR bootstrap_claimed_at <= ?)
|
|
397
|
+
RETURNING conversation_id`,
|
|
398
|
+
)
|
|
399
|
+
.get(now, now, input.platform, input.accountId, staleBefore) as
|
|
400
|
+
| { conversation_id?: unknown }
|
|
401
|
+
| undefined;
|
|
402
|
+
const conversationId = row?.conversation_id;
|
|
403
|
+
return typeof conversationId === "string" && conversationId.length > 0
|
|
404
|
+
? { conversationId }
|
|
405
|
+
: null;
|
|
406
|
+
}) ?? null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
releaseActivationBootstrapClaim(input: ReleaseActivationBootstrapClaimInput): boolean | null {
|
|
410
|
+
return this.write(() => {
|
|
411
|
+
const now = Date.now();
|
|
412
|
+
const result = this.requireDb()
|
|
413
|
+
.prepare(
|
|
414
|
+
`UPDATE activations SET
|
|
415
|
+
bootstrap_claimed_at = NULL,
|
|
416
|
+
updated_at = ?
|
|
417
|
+
WHERE platform = ?
|
|
418
|
+
AND account_id = ?
|
|
419
|
+
AND conversation_id = ?
|
|
420
|
+
AND bootstrap_sent = 0
|
|
421
|
+
AND bootstrap_claimed_at IS NOT NULL`,
|
|
422
|
+
)
|
|
423
|
+
.run(now, input.platform, input.accountId, input.conversationId);
|
|
424
|
+
return result.changes > 0;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
markActivationBootstrapSent(input: MarkActivationBootstrapSentInput): boolean | null {
|
|
429
|
+
return this.write(() => {
|
|
430
|
+
const now = Date.now();
|
|
431
|
+
const result = this.requireDb()
|
|
432
|
+
.prepare(
|
|
433
|
+
`UPDATE activations SET
|
|
434
|
+
bootstrap_sent = 1,
|
|
435
|
+
bootstrap_claimed_at = NULL,
|
|
436
|
+
updated_at = ?
|
|
437
|
+
WHERE platform = ?
|
|
438
|
+
AND account_id = ?
|
|
439
|
+
AND conversation_id = ?
|
|
440
|
+
AND bootstrap_sent = 0
|
|
441
|
+
AND bootstrap_claimed_at IS NOT NULL`,
|
|
442
|
+
)
|
|
443
|
+
.run(now, input.platform, input.accountId, input.conversationId);
|
|
444
|
+
return result.changes > 0;
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
insertMessage(input: MessageInput): boolean | null {
|
|
449
|
+
return this.write(() => {
|
|
450
|
+
this.requireDb()
|
|
451
|
+
.prepare(
|
|
452
|
+
`INSERT INTO clawchat_messages(
|
|
453
|
+
platform, account_id, kind, direction, event_type, trace_id, chat_id,
|
|
454
|
+
message_id, text, raw_json, created_at, send_status
|
|
455
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
456
|
+
)
|
|
457
|
+
.run(
|
|
458
|
+
input.platform,
|
|
459
|
+
input.accountId,
|
|
460
|
+
input.kind,
|
|
461
|
+
input.direction,
|
|
462
|
+
input.eventType,
|
|
463
|
+
input.traceId ?? null,
|
|
464
|
+
input.chatId ?? null,
|
|
465
|
+
input.messageId ?? null,
|
|
466
|
+
input.text ?? null,
|
|
467
|
+
toJson(input.raw),
|
|
468
|
+
input.createdAt ?? Date.now(),
|
|
469
|
+
input.direction === "outbound" ? "pending" : null,
|
|
470
|
+
);
|
|
471
|
+
return true;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
claimMessageOnce(input: MessageInput): true | false | null {
|
|
476
|
+
return this.write(() => {
|
|
477
|
+
const result = this.requireDb()
|
|
478
|
+
.prepare(
|
|
479
|
+
`INSERT OR IGNORE INTO clawchat_messages(
|
|
480
|
+
platform, account_id, kind, direction, event_type, trace_id, chat_id,
|
|
481
|
+
message_id, text, raw_json, created_at, send_status
|
|
482
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
483
|
+
)
|
|
484
|
+
.run(
|
|
485
|
+
input.platform,
|
|
486
|
+
input.accountId,
|
|
487
|
+
input.kind,
|
|
488
|
+
input.direction,
|
|
489
|
+
input.eventType,
|
|
490
|
+
input.traceId ?? null,
|
|
491
|
+
input.chatId ?? null,
|
|
492
|
+
input.messageId ?? null,
|
|
493
|
+
input.text ?? null,
|
|
494
|
+
toJson(input.raw),
|
|
495
|
+
input.createdAt ?? Date.now(),
|
|
496
|
+
input.direction === "outbound" ? "pending" : null,
|
|
497
|
+
);
|
|
498
|
+
return result.changes > 0;
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
markMessageAcknowledged(input: MessageAcknowledgedInput): boolean | null {
|
|
503
|
+
return this.write(() => {
|
|
504
|
+
const result = this.requireDb()
|
|
505
|
+
.prepare(
|
|
506
|
+
`UPDATE clawchat_messages SET
|
|
507
|
+
send_status = 'acknowledged',
|
|
508
|
+
protocol_message_id = ?,
|
|
509
|
+
acked_at = ?,
|
|
510
|
+
send_error = NULL
|
|
511
|
+
WHERE account_id = ? AND kind = ? AND direction = ? AND message_id = ?`,
|
|
512
|
+
)
|
|
513
|
+
.run(
|
|
514
|
+
input.protocolMessageId ?? input.messageId,
|
|
515
|
+
input.ackedAt ?? Date.now(),
|
|
516
|
+
input.accountId,
|
|
517
|
+
input.kind,
|
|
518
|
+
input.direction,
|
|
519
|
+
input.messageId,
|
|
520
|
+
);
|
|
521
|
+
return result.changes > 0;
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
updateMessageByIdentity(input: {
|
|
526
|
+
accountId: string;
|
|
527
|
+
kind: string;
|
|
528
|
+
direction: MessageInput["direction"];
|
|
529
|
+
messageId: string;
|
|
530
|
+
eventType: string;
|
|
531
|
+
traceId?: string | null;
|
|
532
|
+
chatId?: string | null;
|
|
533
|
+
text?: string | null;
|
|
534
|
+
raw?: unknown;
|
|
535
|
+
}): void {
|
|
536
|
+
this.write(() => {
|
|
537
|
+
this.requireDb()
|
|
538
|
+
.prepare(
|
|
539
|
+
`UPDATE clawchat_messages SET
|
|
540
|
+
event_type = ?, trace_id = ?, chat_id = ?, text = ?, raw_json = ?
|
|
541
|
+
WHERE account_id = ? AND kind = ? AND direction = ? AND message_id = ?`,
|
|
542
|
+
)
|
|
543
|
+
.run(
|
|
544
|
+
input.eventType,
|
|
545
|
+
input.traceId ?? null,
|
|
546
|
+
input.chatId ?? null,
|
|
547
|
+
input.text ?? null,
|
|
548
|
+
toJson(input.raw),
|
|
549
|
+
input.accountId,
|
|
550
|
+
input.kind,
|
|
551
|
+
input.direction,
|
|
552
|
+
input.messageId,
|
|
553
|
+
);
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
startConnection(input: StartConnectionInput): number | null {
|
|
558
|
+
return this.write(() => {
|
|
559
|
+
const now = input.connectStartedAt ?? Date.now();
|
|
560
|
+
const result = this.requireDb()
|
|
561
|
+
.prepare(
|
|
562
|
+
`INSERT INTO connections(
|
|
563
|
+
platform, account_id, attempt, reconnect_count, state,
|
|
564
|
+
connect_started_at, created_at, updated_at
|
|
565
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
566
|
+
)
|
|
567
|
+
.run(
|
|
568
|
+
input.platform,
|
|
569
|
+
input.accountId,
|
|
570
|
+
input.attempt ?? null,
|
|
571
|
+
input.reconnectCount ?? null,
|
|
572
|
+
"connecting",
|
|
573
|
+
now,
|
|
574
|
+
now,
|
|
575
|
+
now,
|
|
576
|
+
);
|
|
577
|
+
return Number(result.lastInsertRowid);
|
|
578
|
+
}) ?? null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
markConnectSent(connectionId: number | null | undefined, options: ConnectionUpdateOptions = {}): void {
|
|
582
|
+
if (typeof connectionId !== "number") return;
|
|
583
|
+
this.updateConnectionTime(connectionId, "connect_sent_at", options.at ?? Date.now());
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
markConnectionReady(connectionId: number | null | undefined, options: ConnectionReadyOptions = {}): void {
|
|
587
|
+
if (typeof connectionId !== "number") return;
|
|
588
|
+
this.write(() => {
|
|
589
|
+
const now = options.at ?? Date.now();
|
|
590
|
+
this.requireDb()
|
|
591
|
+
.prepare(
|
|
592
|
+
`UPDATE connections SET
|
|
593
|
+
state = ?,
|
|
594
|
+
ready_at = ?,
|
|
595
|
+
resolved_device_id = CASE WHEN ? THEN ? ELSE resolved_device_id END,
|
|
596
|
+
delivery_mode = CASE WHEN ? THEN ? ELSE delivery_mode END,
|
|
597
|
+
updated_at = ?
|
|
598
|
+
WHERE id = ?`,
|
|
599
|
+
)
|
|
600
|
+
.run(
|
|
601
|
+
"ready",
|
|
602
|
+
now,
|
|
603
|
+
hasOwn(options, "resolvedDeviceId"),
|
|
604
|
+
options.resolvedDeviceId ?? null,
|
|
605
|
+
hasOwn(options, "deliveryMode"),
|
|
606
|
+
options.deliveryMode ?? null,
|
|
607
|
+
now,
|
|
608
|
+
connectionId,
|
|
609
|
+
);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
finishConnection(connectionId: number | null | undefined, input: FinishConnectionInput): void {
|
|
614
|
+
if (typeof connectionId !== "number") return;
|
|
615
|
+
this.write(() => {
|
|
616
|
+
const now = input.disconnectedAt ?? Date.now();
|
|
617
|
+
this.requireDb()
|
|
618
|
+
.prepare(
|
|
619
|
+
`UPDATE connections SET
|
|
620
|
+
state = ?, disconnected_at = ?, close_code = ?, close_reason = ?, error = ?, updated_at = ?
|
|
621
|
+
WHERE id = ?`,
|
|
622
|
+
)
|
|
623
|
+
.run(
|
|
624
|
+
input.state,
|
|
625
|
+
now,
|
|
626
|
+
input.closeCode ?? null,
|
|
627
|
+
input.closeReason ?? null,
|
|
628
|
+
input.error ?? null,
|
|
629
|
+
now,
|
|
630
|
+
connectionId,
|
|
631
|
+
);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
recordToolCall(input: ToolCallInput): void {
|
|
636
|
+
this.write(() => {
|
|
637
|
+
const startedAt = input.startedAt ?? Date.now();
|
|
638
|
+
const endedAt = input.endedAt ?? null;
|
|
639
|
+
const durationMs = endedAt == null ? null : Math.max(0, endedAt - startedAt);
|
|
640
|
+
this.requireDb()
|
|
641
|
+
.prepare(
|
|
642
|
+
`INSERT INTO tool_calls(
|
|
643
|
+
platform, account_id, tool_name, args_json, result_json, error,
|
|
644
|
+
started_at, ended_at, duration_ms, created_at
|
|
645
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
646
|
+
)
|
|
647
|
+
.run(
|
|
648
|
+
input.platform,
|
|
649
|
+
input.accountId ?? null,
|
|
650
|
+
input.toolName,
|
|
651
|
+
toJson(input.args),
|
|
652
|
+
toJson(input.result),
|
|
653
|
+
toNullableString(input.error),
|
|
654
|
+
startedAt,
|
|
655
|
+
endedAt,
|
|
656
|
+
durationMs,
|
|
657
|
+
startedAt,
|
|
658
|
+
);
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
getActivationForTest(platform: string, accountId: string): Record<string, unknown> | null {
|
|
663
|
+
return this.read(() =>
|
|
664
|
+
this.requireDb()
|
|
665
|
+
.prepare("SELECT * FROM activations WHERE platform = ? AND account_id = ?")
|
|
666
|
+
.get(platform, accountId) as Record<string, unknown> | undefined,
|
|
667
|
+
) ?? null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
listMessagesForTest(): Record<string, unknown>[] {
|
|
671
|
+
return this.read(() =>
|
|
672
|
+
this.requireDb()
|
|
673
|
+
.prepare("SELECT * FROM clawchat_messages ORDER BY id")
|
|
674
|
+
.all() as Record<string, unknown>[],
|
|
675
|
+
) ?? [];
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
listConnectionsForTest(): Record<string, unknown>[] {
|
|
679
|
+
return this.read(() =>
|
|
680
|
+
this.requireDb().prepare("SELECT * FROM connections ORDER BY id").all() as Record<string, unknown>[],
|
|
681
|
+
) ?? [];
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
listToolCallsForTest(): Record<string, unknown>[] {
|
|
685
|
+
return this.read(() =>
|
|
686
|
+
this.requireDb().prepare("SELECT * FROM tool_calls ORDER BY id").all() as Record<string, unknown>[],
|
|
687
|
+
) ?? [];
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
close(): void {
|
|
691
|
+
this.db?.close();
|
|
692
|
+
this.db = null;
|
|
693
|
+
this.initialized = false;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private updateConnectionTime(connectionId: number, field: "connect_sent_at", at: number): void {
|
|
697
|
+
this.write(() => {
|
|
698
|
+
this.requireDb()
|
|
699
|
+
.prepare(`UPDATE connections SET ${field} = ?, updated_at = ? WHERE id = ?`)
|
|
700
|
+
.run(at, at, connectionId);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private read<T>(fn: () => T): T | null {
|
|
705
|
+
if (!this.ensureInitialized()) return null;
|
|
706
|
+
try {
|
|
707
|
+
return fn();
|
|
708
|
+
} catch (err) {
|
|
709
|
+
this.logFailure("clawchat-plugin-openclaw sqlite read failed", err);
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
private write<T>(fn: () => T): T | null {
|
|
715
|
+
if (!this.ensureInitialized()) return null;
|
|
716
|
+
try {
|
|
717
|
+
return fn();
|
|
718
|
+
} catch (err) {
|
|
719
|
+
this.logFailure("clawchat-plugin-openclaw sqlite write failed", err);
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private ensureInitialized(): boolean {
|
|
725
|
+
if (this.disabled) return false;
|
|
726
|
+
if (this.initialized) return true;
|
|
727
|
+
try {
|
|
728
|
+
fs.mkdirSync(path.dirname(this.dbPath), { recursive: true });
|
|
729
|
+
this.db = new DatabaseSync(this.dbPath);
|
|
730
|
+
this.db.exec("PRAGMA journal_mode=WAL");
|
|
731
|
+
try {
|
|
732
|
+
fs.chmodSync(this.dbPath, 0o600);
|
|
733
|
+
} catch {
|
|
734
|
+
// Best effort only; permissions vary by platform/filesystem.
|
|
735
|
+
}
|
|
736
|
+
this.applyMigrations();
|
|
737
|
+
this.initialized = true;
|
|
738
|
+
return true;
|
|
739
|
+
} catch (err) {
|
|
740
|
+
this.disabled = true;
|
|
741
|
+
this.logFailure("clawchat-plugin-openclaw sqlite disabled after initialization failure", err);
|
|
742
|
+
try {
|
|
743
|
+
this.db?.close();
|
|
744
|
+
} catch {
|
|
745
|
+
// best effort
|
|
746
|
+
}
|
|
747
|
+
this.db = null;
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
private applyMigrations(): void {
|
|
753
|
+
const db = this.requireDb();
|
|
754
|
+
db.exec(
|
|
755
|
+
`CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
756
|
+
version INTEGER PRIMARY KEY,
|
|
757
|
+
name TEXT NOT NULL,
|
|
758
|
+
applied_at INTEGER NOT NULL
|
|
759
|
+
)`,
|
|
760
|
+
);
|
|
761
|
+
const hasMigration = db.prepare("SELECT 1 FROM schema_migrations WHERE version = ?");
|
|
762
|
+
const insertMigration = db.prepare(
|
|
763
|
+
"INSERT INTO schema_migrations(version, name, applied_at) VALUES (?, ?, ?)",
|
|
764
|
+
);
|
|
765
|
+
for (const migration of MIGRATIONS) {
|
|
766
|
+
if (hasMigration.get(migration.version)) continue;
|
|
767
|
+
db.exec("BEGIN");
|
|
768
|
+
try {
|
|
769
|
+
db.exec(migration.sql);
|
|
770
|
+
insertMigration.run(migration.version, migration.name, Date.now());
|
|
771
|
+
db.exec("COMMIT");
|
|
772
|
+
} catch (err) {
|
|
773
|
+
db.exec("ROLLBACK");
|
|
774
|
+
throw err;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
private requireDb(): DatabaseSync {
|
|
780
|
+
if (!this.db) throw new Error("clawchat sqlite database is not open");
|
|
781
|
+
return this.db;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private logFailure(prefix: string, err: unknown): void {
|
|
785
|
+
this.log?.error?.(`${prefix}: ${safeErrorMessage(err)}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
let singleton: ClawChatStore | null = null;
|
|
790
|
+
|
|
791
|
+
export function createClawChatStore(options: ClawChatStoreOptions = {}): ClawChatStore {
|
|
792
|
+
return new ClawChatStore(options);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export function getClawChatStore(options: ClawChatStoreOptions = {}): ClawChatStore {
|
|
796
|
+
const dbPath = options.dbPath ?? defaultDbPath();
|
|
797
|
+
if (!singleton || singleton.dbPath !== dbPath) {
|
|
798
|
+
singleton?.close();
|
|
799
|
+
singleton = createClawChatStore({ ...options, dbPath });
|
|
800
|
+
}
|
|
801
|
+
return singleton;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export function resetClawChatStoreForTest(): void {
|
|
805
|
+
singleton?.close();
|
|
806
|
+
singleton = null;
|
|
807
|
+
}
|