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