@clawchatsai/connector 0.0.87 → 0.0.89
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/gateway-bridge.d.ts +4 -0
- package/dist/index.js +216 -104
- package/package.json +3 -4
- package/prebuilds/darwin-arm64/node_datachannel.node +0 -0
- package/prebuilds/darwin-x64/node_datachannel.node +0 -0
- package/prebuilds/linux-arm/node_datachannel.node +0 -0
- package/prebuilds/linux-arm64/node_datachannel.node +0 -0
- package/prebuilds/linux-x64/node_datachannel.node +0 -0
- package/prebuilds/linuxmusl-arm64/node_datachannel.node +0 -0
- package/prebuilds/linuxmusl-x64/node_datachannel.node +0 -0
- package/prebuilds/win32-arm64/node_datachannel.node +0 -0
- package/prebuilds/win32-x64/node_datachannel.node +0 -0
- package/server/config.js +5 -4
- package/server/controllers/agents.js +20 -0
- package/server/controllers/settings.js +28 -0
- package/server/controllers/static.js +56 -0
- package/server/controllers/transcribe.js +3 -10
- package/server/index.js +24 -49
- package/server/providers/memory-config.js +52 -0
- package/server/providers/memory.js +3 -39
- package/server/store/workspace-store.js +31 -0
- package/dist/updater.d.ts +0 -21
- package/dist/updater.js +0 -64
- package/server.js +0 -2392
package/server.js
DELETED
|
@@ -1,2392 +0,0 @@
|
|
|
1
|
-
// ClawChats backend — built by esbuild, source: github.com/clawchatsai/connector
|
|
2
|
-
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
5
|
-
|
|
6
|
-
// server/index.js
|
|
7
|
-
import http from "node:http";
|
|
8
|
-
import fs13 from "node:fs";
|
|
9
|
-
import path14 from "node:path";
|
|
10
|
-
import os5 from "node:os";
|
|
11
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
12
|
-
import { WebSocket as WS2, WebSocketServer } from "ws";
|
|
13
|
-
|
|
14
|
-
// server/bootstrap/native.js
|
|
15
|
-
import { DatabaseSync as Database } from "node:sqlite";
|
|
16
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
|
-
var requestDbStore = new AsyncLocalStorage();
|
|
18
|
-
|
|
19
|
-
// server/config.js
|
|
20
|
-
import fs from "node:fs";
|
|
21
|
-
import path from "node:path";
|
|
22
|
-
import os from "node:os";
|
|
23
|
-
import { fileURLToPath } from "node:url";
|
|
24
|
-
var HOME = os.homedir();
|
|
25
|
-
var MAX_PREAMBLE_CHARS = 5e4;
|
|
26
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
27
|
-
var __dirname = path.dirname(__filename);
|
|
28
|
-
function parseConfigField(field) {
|
|
29
|
-
const candidates = [path.join(__dirname, "config.js"), path.join(__dirname, "..", "config.js")];
|
|
30
|
-
for (const configPath of candidates) {
|
|
31
|
-
try {
|
|
32
|
-
const configText = fs.readFileSync(configPath, "utf8");
|
|
33
|
-
const match = configText.match(new RegExp(`${field}:\\s*['"]([^'"]+)['"]`));
|
|
34
|
-
if (match) return match[1];
|
|
35
|
-
} catch {
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
__name(parseConfigField, "parseConfigField");
|
|
41
|
-
var AUTH_TOKEN = process.env.CLAWCHATS_AUTH_TOKEN || parseConfigField("authToken") || "";
|
|
42
|
-
function discoverGatewayWsUrl() {
|
|
43
|
-
if (process.env.GATEWAY_WS_URL) return process.env.GATEWAY_WS_URL;
|
|
44
|
-
for (const cfgPath of [path.join(HOME, ".openclaw", "openclaw.json"), "/etc/openclaw/openclaw.json"]) {
|
|
45
|
-
try {
|
|
46
|
-
const raw = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
47
|
-
const port = raw.gateway?.port || raw.port;
|
|
48
|
-
const host = raw.gateway?.host || raw.host || "localhost";
|
|
49
|
-
if (port) return `ws://${host}:${port}`;
|
|
50
|
-
} catch {
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return "ws://localhost:18789";
|
|
54
|
-
}
|
|
55
|
-
__name(discoverGatewayWsUrl, "discoverGatewayWsUrl");
|
|
56
|
-
var GATEWAY_WS_URL = discoverGatewayWsUrl();
|
|
57
|
-
var OPENCLAW_SESSIONS_DIR = process.env.OPENCLAW_SESSIONS_DIR || parseConfigField("sessionsDir") || path.join(HOME, ".openclaw", "agents", "main", "sessions");
|
|
58
|
-
function getSessionsDirForAgent(agentId) {
|
|
59
|
-
if (!agentId || agentId === "main") return OPENCLAW_SESSIONS_DIR;
|
|
60
|
-
return path.join(HOME, ".openclaw", "agents", agentId, "sessions");
|
|
61
|
-
}
|
|
62
|
-
__name(getSessionsDirForAgent, "getSessionsDirForAgent");
|
|
63
|
-
function validateAgent(agentId) {
|
|
64
|
-
if (!agentId) return "main";
|
|
65
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) throw new Error("Invalid agent ID");
|
|
66
|
-
const agentDir = path.join(HOME, ".openclaw", "agents", agentId);
|
|
67
|
-
if (!fs.existsSync(agentDir)) throw new Error(`Agent not found: ${agentId}`);
|
|
68
|
-
return agentId;
|
|
69
|
-
}
|
|
70
|
-
__name(validateAgent, "validateAgent");
|
|
71
|
-
|
|
72
|
-
// server/debug.js
|
|
73
|
-
import fs2 from "node:fs";
|
|
74
|
-
import path2 from "node:path";
|
|
75
|
-
var DebugLogger = class {
|
|
76
|
-
static {
|
|
77
|
-
__name(this, "DebugLogger");
|
|
78
|
-
}
|
|
79
|
-
constructor(baseDir) {
|
|
80
|
-
this.baseDir = path2.join(baseDir, "..", "debug");
|
|
81
|
-
this.active = false;
|
|
82
|
-
this.sessionId = null;
|
|
83
|
-
this.wsStream = null;
|
|
84
|
-
this.originatingClient = null;
|
|
85
|
-
}
|
|
86
|
-
start(ts, originatingClient) {
|
|
87
|
-
if (this.active) return { error: "already-active", sessionId: this.sessionId };
|
|
88
|
-
this.sessionId = ts.replace(/[:.]/g, "-");
|
|
89
|
-
this.originatingClient = originatingClient;
|
|
90
|
-
fs2.mkdirSync(this.baseDir, { recursive: true });
|
|
91
|
-
this.wsStream = fs2.createWriteStream(path2.join(this.baseDir, `session-${this.sessionId}-ws.log`), { flags: "a" });
|
|
92
|
-
this.active = true;
|
|
93
|
-
console.log(`Debug recording started: ${this.sessionId}`);
|
|
94
|
-
return { sessionId: this.sessionId };
|
|
95
|
-
}
|
|
96
|
-
logFrame(direction, data) {
|
|
97
|
-
if (this.active && this.wsStream) this.wsStream.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${direction} ${data}
|
|
98
|
-
`);
|
|
99
|
-
}
|
|
100
|
-
saveDump(payload) {
|
|
101
|
-
if (!this.sessionId) return { sessionId: null, files: [] };
|
|
102
|
-
const files = [];
|
|
103
|
-
const id = this.sessionId;
|
|
104
|
-
if (this.wsStream) {
|
|
105
|
-
this.wsStream.end();
|
|
106
|
-
this.wsStream = null;
|
|
107
|
-
files.push(`session-${id}-ws.log`);
|
|
108
|
-
}
|
|
109
|
-
let logContent = "";
|
|
110
|
-
for (const entry of payload.console || []) {
|
|
111
|
-
logContent += `${entry.ts} [${entry.level.toUpperCase()}] ${entry.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
|
|
112
|
-
`;
|
|
113
|
-
}
|
|
114
|
-
for (const err of payload.errors || []) logContent += `${err.ts} [UNHANDLED] ${err.message}
|
|
115
|
-
${err.stack || ""}
|
|
116
|
-
`;
|
|
117
|
-
if (logContent) {
|
|
118
|
-
fs2.writeFileSync(path2.join(this.baseDir, `session-${id}-client.log`), logContent);
|
|
119
|
-
files.push(`session-${id}-client.log`);
|
|
120
|
-
}
|
|
121
|
-
if (payload.state) {
|
|
122
|
-
fs2.writeFileSync(path2.join(this.baseDir, `session-${id}-state.json`), JSON.stringify(payload.state, null, 2));
|
|
123
|
-
files.push(`session-${id}-state.json`);
|
|
124
|
-
}
|
|
125
|
-
if (payload.screenshot) {
|
|
126
|
-
fs2.writeFileSync(path2.join(this.baseDir, `session-${id}-screenshot.jpg`), Buffer.from(payload.screenshot, "base64"));
|
|
127
|
-
files.push(`session-${id}-screenshot.jpg`);
|
|
128
|
-
}
|
|
129
|
-
const savedId = id;
|
|
130
|
-
this.active = false;
|
|
131
|
-
this.sessionId = null;
|
|
132
|
-
this.originatingClient = null;
|
|
133
|
-
console.log(`Debug session saved: ${files.join(", ")}`);
|
|
134
|
-
return { sessionId: savedId, files };
|
|
135
|
-
}
|
|
136
|
-
handleClientDisconnect(ws) {
|
|
137
|
-
if (this.active && this.originatingClient === ws) {
|
|
138
|
-
console.log(`Debug session ${this.sessionId} auto-closed: client disconnected`);
|
|
139
|
-
if (this.wsStream) {
|
|
140
|
-
this.wsStream.write(`${(/* @__PURE__ */ new Date()).toISOString()} SYSTEM Client disconnected \u2014 session auto-closed
|
|
141
|
-
`);
|
|
142
|
-
this.wsStream.end();
|
|
143
|
-
this.wsStream = null;
|
|
144
|
-
}
|
|
145
|
-
this.active = false;
|
|
146
|
-
this.sessionId = null;
|
|
147
|
-
this.originatingClient = null;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
// server/gateway.js
|
|
153
|
-
import path4 from "node:path";
|
|
154
|
-
import { WebSocket as WS } from "ws";
|
|
155
|
-
|
|
156
|
-
// server/bootstrap/identity.js
|
|
157
|
-
import crypto from "node:crypto";
|
|
158
|
-
import fs3 from "node:fs";
|
|
159
|
-
import path3 from "node:path";
|
|
160
|
-
var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
161
|
-
function derivePublicKeyRaw(publicKeyPem) {
|
|
162
|
-
const spki = crypto.createPublicKey(publicKeyPem).export({ type: "spki", format: "der" });
|
|
163
|
-
if (spki.length === ED25519_SPKI_PREFIX.length + 32 && spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
164
|
-
return spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
165
|
-
}
|
|
166
|
-
return spki;
|
|
167
|
-
}
|
|
168
|
-
__name(derivePublicKeyRaw, "derivePublicKeyRaw");
|
|
169
|
-
function fingerprintPublicKey(publicKeyPem) {
|
|
170
|
-
return crypto.createHash("sha256").update(derivePublicKeyRaw(publicKeyPem)).digest("hex");
|
|
171
|
-
}
|
|
172
|
-
__name(fingerprintPublicKey, "fingerprintPublicKey");
|
|
173
|
-
function base64UrlEncode(buf) {
|
|
174
|
-
return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
|
|
175
|
-
}
|
|
176
|
-
__name(base64UrlEncode, "base64UrlEncode");
|
|
177
|
-
function loadOrCreateDeviceIdentity(identityPath) {
|
|
178
|
-
try {
|
|
179
|
-
if (fs3.existsSync(identityPath)) {
|
|
180
|
-
const parsed = JSON.parse(fs3.readFileSync(identityPath, "utf8"));
|
|
181
|
-
if (parsed?.version === 1 && parsed.deviceId && parsed.publicKeyPem && parsed.privateKeyPem) return parsed;
|
|
182
|
-
}
|
|
183
|
-
} catch {
|
|
184
|
-
}
|
|
185
|
-
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
186
|
-
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
187
|
-
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
188
|
-
const identity = { version: 1, deviceId: fingerprintPublicKey(publicKeyPem), publicKeyPem, privateKeyPem, createdAtMs: Date.now() };
|
|
189
|
-
fs3.mkdirSync(path3.dirname(identityPath), { recursive: true });
|
|
190
|
-
fs3.writeFileSync(identityPath, JSON.stringify(identity, null, 2) + "\n", { mode: 384 });
|
|
191
|
-
return identity;
|
|
192
|
-
}
|
|
193
|
-
__name(loadOrCreateDeviceIdentity, "loadOrCreateDeviceIdentity");
|
|
194
|
-
function buildDeviceAuth(identity, { clientId, clientMode, role, scopes, token, nonce }) {
|
|
195
|
-
const signedAt = Date.now();
|
|
196
|
-
const payload = ["v2", identity.deviceId, clientId, clientMode, role, scopes.join(","), String(signedAt), token || "", nonce].join("|");
|
|
197
|
-
const privateKey = crypto.createPrivateKey(identity.privateKeyPem);
|
|
198
|
-
const signature = base64UrlEncode(crypto.sign(null, Buffer.from(payload, "utf8"), privateKey));
|
|
199
|
-
const publicKeyB64Url = base64UrlEncode(derivePublicKeyRaw(identity.publicKeyPem));
|
|
200
|
-
return { id: identity.deviceId, publicKey: publicKeyB64Url, signature, signedAt, nonce };
|
|
201
|
-
}
|
|
202
|
-
__name(buildDeviceAuth, "buildDeviceAuth");
|
|
203
|
-
|
|
204
|
-
// server/util/helpers.js
|
|
205
|
-
function syncThreadUnreadCount(db, threadId) {
|
|
206
|
-
const count = db.prepare("SELECT COUNT(*) as c FROM unread_messages WHERE thread_id = ?").get(threadId).c;
|
|
207
|
-
db.prepare("UPDATE threads SET unread_count = ? WHERE id = ?").run(count, threadId);
|
|
208
|
-
return count;
|
|
209
|
-
}
|
|
210
|
-
__name(syncThreadUnreadCount, "syncThreadUnreadCount");
|
|
211
|
-
function parseSessionKey(sessionKey) {
|
|
212
|
-
if (!sessionKey) return null;
|
|
213
|
-
const match = sessionKey.match(/^agent:([^:]+):([^:]+):chat:([^:]+)$/);
|
|
214
|
-
if (!match) return null;
|
|
215
|
-
return { agent: match[1], workspace: match[2], threadId: match[3] };
|
|
216
|
-
}
|
|
217
|
-
__name(parseSessionKey, "parseSessionKey");
|
|
218
|
-
function extractContent(message) {
|
|
219
|
-
if (!message) return "";
|
|
220
|
-
if (typeof message.content === "string") return message.content;
|
|
221
|
-
if (Array.isArray(message.content)) {
|
|
222
|
-
return message.content.filter((p) => p.type === "text").map((p) => p.text).join("");
|
|
223
|
-
}
|
|
224
|
-
return "";
|
|
225
|
-
}
|
|
226
|
-
__name(extractContent, "extractContent");
|
|
227
|
-
function isSilentReplyExact(text, token = "NO_REPLY") {
|
|
228
|
-
if (!text) return false;
|
|
229
|
-
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
230
|
-
return new RegExp(`^\\s*${escaped}\\s*$`).test(text);
|
|
231
|
-
}
|
|
232
|
-
__name(isSilentReplyExact, "isSilentReplyExact");
|
|
233
|
-
function isSilentReplyPrefix(text, token = "NO_REPLY") {
|
|
234
|
-
if (!text) return false;
|
|
235
|
-
const trimmed = text.trimStart();
|
|
236
|
-
if (!trimmed) return false;
|
|
237
|
-
if (trimmed !== trimmed.toUpperCase()) return false;
|
|
238
|
-
const normalized = trimmed.toUpperCase();
|
|
239
|
-
if (normalized.length < 2 || /[^A-Z_]/.test(normalized)) return false;
|
|
240
|
-
const tokenUpper = token.toUpperCase();
|
|
241
|
-
if (!tokenUpper.startsWith(normalized)) return false;
|
|
242
|
-
if (normalized.includes("_")) return true;
|
|
243
|
-
return tokenUpper === "NO_REPLY" && normalized === "NO";
|
|
244
|
-
}
|
|
245
|
-
__name(isSilentReplyPrefix, "isSilentReplyPrefix");
|
|
246
|
-
function stripTrailingSentinel(text, token = "NO_REPLY") {
|
|
247
|
-
if (!text) return text;
|
|
248
|
-
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
249
|
-
return text.replace(new RegExp(`(?:^|\\s+|\\*+)${escaped}\\s*$`), "").trim();
|
|
250
|
-
}
|
|
251
|
-
__name(stripTrailingSentinel, "stripTrailingSentinel");
|
|
252
|
-
function stripFinalTags(text) {
|
|
253
|
-
return text ? text.replace(/<\s*\/?\s*final\s*>/gi, "") : text;
|
|
254
|
-
}
|
|
255
|
-
__name(stripFinalTags, "stripFinalTags");
|
|
256
|
-
function sanitizeAssistantContent(text) {
|
|
257
|
-
if (!text) return text;
|
|
258
|
-
let out = stripFinalTags(text);
|
|
259
|
-
out = out.replace(/^(?:[ \t]*\r?\n)+/, "");
|
|
260
|
-
if (out.includes("NO_REPLY")) out = stripTrailingSentinel(out, "NO_REPLY");
|
|
261
|
-
if (out.includes("HEARTBEAT_OK")) out = stripTrailingSentinel(out, "HEARTBEAT_OK");
|
|
262
|
-
return out;
|
|
263
|
-
}
|
|
264
|
-
__name(sanitizeAssistantContent, "sanitizeAssistantContent");
|
|
265
|
-
function generateActivitySummary(steps) {
|
|
266
|
-
const toolSteps = steps.filter((s) => s.type === "tool" && s.phase !== "result" && s.phase !== "update");
|
|
267
|
-
const hasThinking = steps.some((s) => s.type === "thinking" && s.text);
|
|
268
|
-
const hasNarration = steps.some((s) => s.type === "assistant" && s.text?.trim());
|
|
269
|
-
if (toolSteps.length === 0 && !hasThinking && !hasNarration) return null;
|
|
270
|
-
if (toolSteps.length === 0 && hasThinking) return "Reasoned through the problem";
|
|
271
|
-
if (toolSteps.length === 0 && hasNarration) return "Processed in multiple steps";
|
|
272
|
-
const counts = {};
|
|
273
|
-
for (const s of toolSteps) {
|
|
274
|
-
const name = s.name || "unknown";
|
|
275
|
-
counts[name] = (counts[name] || 0) + 1;
|
|
276
|
-
}
|
|
277
|
-
const toolNames = { web_search: "searched the web", web_fetch: "fetched web pages", Read: "read files", read: "read files", Write: "wrote files", write: "wrote files", Edit: "edited files", edit: "edited files", exec: "ran commands", Bash: "ran commands", browser: "browsed the web", memory_search: "searched memory", memory_store: "saved to memory", image: "analyzed images", message: "sent messages", sessions_spawn: "spawned sub-agents", cron: "managed cron jobs", Grep: "searched code", grep: "searched code", Glob: "found files", glob: "found files" };
|
|
278
|
-
const parts = [];
|
|
279
|
-
for (const [name, count] of Object.entries(counts)) {
|
|
280
|
-
const friendly = toolNames[name];
|
|
281
|
-
parts.push(friendly ? count > 1 ? `${friendly} (${count}\xD7)` : friendly : count > 1 ? `used ${name} (${count}\xD7)` : `used ${name}`);
|
|
282
|
-
}
|
|
283
|
-
if (parts.length === 0) return null;
|
|
284
|
-
if (parts.length === 1) return parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
|
|
285
|
-
const last = parts.pop();
|
|
286
|
-
return (parts.join(", ") + " and " + last).replace(/^./, (c) => c.toUpperCase());
|
|
287
|
-
}
|
|
288
|
-
__name(generateActivitySummary, "generateActivitySummary");
|
|
289
|
-
function writeActivityToDb(getDbFn, broadcastFn, runId, log) {
|
|
290
|
-
if (!log._parsed) log._parsed = parseSessionKey(log.sessionKey);
|
|
291
|
-
const parsed = log._parsed;
|
|
292
|
-
if (!parsed) return;
|
|
293
|
-
const db = getDbFn(parsed.workspace);
|
|
294
|
-
if (!db) return;
|
|
295
|
-
const cleanSteps = log.steps.map((s) => {
|
|
296
|
-
const c = { ...s };
|
|
297
|
-
delete c._sealed;
|
|
298
|
-
return c;
|
|
299
|
-
});
|
|
300
|
-
const summary = generateActivitySummary(log.steps);
|
|
301
|
-
const now = Date.now();
|
|
302
|
-
if (!log._messageId) {
|
|
303
|
-
const thread = db.prepare("SELECT id FROM threads WHERE id = ?").get(parsed.threadId);
|
|
304
|
-
if (!thread) return;
|
|
305
|
-
const messageId = `gw-activity-${runId}`;
|
|
306
|
-
const metadata = { activityLog: cleanSteps, activitySummary: summary, pending: true };
|
|
307
|
-
try {
|
|
308
|
-
db.prepare(`INSERT OR IGNORE INTO messages (id, thread_id, role, content, status, metadata, timestamp, created_at) VALUES (?, ?, 'assistant', '', 'sent', ?, ?, ?)`).run(messageId, parsed.threadId, JSON.stringify(metadata), now, now);
|
|
309
|
-
log._messageId = messageId;
|
|
310
|
-
broadcastFn(JSON.stringify({ type: "clawchats", event: "message-saved", threadId: parsed.threadId, workspace: parsed.workspace, messageId, timestamp: now }));
|
|
311
|
-
} catch (err) {
|
|
312
|
-
console.error(`[activity] Failed to write activity ${messageId}:`, err.message);
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
const existing = db.prepare("SELECT metadata FROM messages WHERE id = ?").get(log._messageId);
|
|
316
|
-
const metadata = existing?.metadata ? JSON.parse(existing.metadata) : {};
|
|
317
|
-
metadata.activityLog = cleanSteps;
|
|
318
|
-
metadata.activitySummary = summary;
|
|
319
|
-
metadata.pending = true;
|
|
320
|
-
db.prepare("UPDATE messages SET metadata = ? WHERE id = ?").run(JSON.stringify(metadata), log._messageId);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
__name(writeActivityToDb, "writeActivityToDb");
|
|
324
|
-
|
|
325
|
-
// server/gateway.js
|
|
326
|
-
var GatewayClient = class {
|
|
327
|
-
static {
|
|
328
|
-
__name(this, "GatewayClient");
|
|
329
|
-
}
|
|
330
|
-
constructor({ getDb, getWorkspaces, dataDir, debugLogger, gatewayWsUrl, authToken, mediaStash }) {
|
|
331
|
-
this.getDb = getDb;
|
|
332
|
-
this.getWorkspaces = getWorkspaces;
|
|
333
|
-
this.dataDir = dataDir;
|
|
334
|
-
this.debugLogger = debugLogger;
|
|
335
|
-
this.gatewayWsUrl = gatewayWsUrl;
|
|
336
|
-
this.authToken = authToken;
|
|
337
|
-
this.mediaStash = mediaStash;
|
|
338
|
-
this.ws = null;
|
|
339
|
-
this.connected = false;
|
|
340
|
-
this.reconnectAttempts = 0;
|
|
341
|
-
this.maxReconnectDelay = 3e4;
|
|
342
|
-
this.browserClients = /* @__PURE__ */ new Map();
|
|
343
|
-
this._externalBroadcastTargets = [];
|
|
344
|
-
this.streamState = /* @__PURE__ */ new Map();
|
|
345
|
-
this.activityLogs = /* @__PURE__ */ new Map();
|
|
346
|
-
this._pendingTitleGens = /* @__PURE__ */ new Map();
|
|
347
|
-
setInterval(() => {
|
|
348
|
-
const cutoff = Date.now() - 10 * 60 * 1e3;
|
|
349
|
-
for (const [runId, log] of this.activityLogs) {
|
|
350
|
-
if (log.startTime < cutoff) {
|
|
351
|
-
if (log._messageId) {
|
|
352
|
-
const db = this.getDb(log._parsed?.workspace);
|
|
353
|
-
if (db) db.prepare(`UPDATE messages SET content = '[Response interrupted]', metadata = json_remove(metadata, '$.pending') WHERE id = ? AND content = ''`).run(log._messageId);
|
|
354
|
-
}
|
|
355
|
-
this.activityLogs.delete(runId);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}, 5 * 60 * 1e3);
|
|
359
|
-
}
|
|
360
|
-
connect() {
|
|
361
|
-
if (this.ws && (this.ws.readyState === WS.CONNECTING || this.ws.readyState === WS.OPEN)) return;
|
|
362
|
-
console.log(`Connecting to gateway at ${this.gatewayWsUrl}...`);
|
|
363
|
-
this.ws = new WS(this.gatewayWsUrl);
|
|
364
|
-
this.ws.on("open", () => {
|
|
365
|
-
console.log("Gateway WebSocket connected");
|
|
366
|
-
this.reconnectAttempts = 0;
|
|
367
|
-
});
|
|
368
|
-
this.ws.on("message", (data) => this.handleGatewayMessage(data.toString()));
|
|
369
|
-
this.ws.on("close", () => {
|
|
370
|
-
console.log("Gateway WebSocket closed");
|
|
371
|
-
this.connected = false;
|
|
372
|
-
this.broadcastGatewayStatus(false);
|
|
373
|
-
this.scheduleReconnect();
|
|
374
|
-
});
|
|
375
|
-
this.ws.on("error", (err) => console.error("Gateway WebSocket error:", err.message));
|
|
376
|
-
}
|
|
377
|
-
handleGatewayMessage(data) {
|
|
378
|
-
this.debugLogger.logFrame("GW\u2192SRV", data);
|
|
379
|
-
let msg;
|
|
380
|
-
try {
|
|
381
|
-
msg = JSON.parse(data);
|
|
382
|
-
} catch {
|
|
383
|
-
console.error("Invalid JSON from gateway:", data);
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
if (msg.type === "event" && msg.event === "connect.challenge") {
|
|
387
|
-
const identity = loadOrCreateDeviceIdentity(path4.join(this.dataDir, "device-identity.json"));
|
|
388
|
-
const device = buildDeviceAuth(identity, { clientId: "gateway-client", clientMode: "backend", role: "operator", scopes: ["operator.read", "operator.write", "operator.admin"], token: this.authToken, nonce: msg.payload?.nonce || "" });
|
|
389
|
-
this.ws.send(JSON.stringify({ type: "req", id: "gw-connect-1", method: "connect", params: { minProtocol: 3, maxProtocol: 3, client: { id: "gateway-client", version: "0.1.0", platform: "node", mode: "backend" }, role: "operator", scopes: ["operator.read", "operator.write", "operator.admin"], device, auth: { token: this.authToken }, caps: ["tool-events"] } }));
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
if (msg.type === "res" && msg.payload?.type === "hello-ok") {
|
|
393
|
-
console.log("Gateway handshake complete");
|
|
394
|
-
this.connected = true;
|
|
395
|
-
this.broadcastGatewayStatus(true);
|
|
396
|
-
}
|
|
397
|
-
if (msg.type === "event" && msg.event === "chat" && msg.payload) {
|
|
398
|
-
this.handleChatEvent(msg.payload, data);
|
|
399
|
-
} else {
|
|
400
|
-
this.broadcastToBrowsers(data);
|
|
401
|
-
}
|
|
402
|
-
if (msg.type === "event" && msg.event === "agent" && msg.payload) this.handleAgentEvent(msg.payload);
|
|
403
|
-
}
|
|
404
|
-
handleChatEvent(params, rawData) {
|
|
405
|
-
const { sessionKey, state, message, seq } = params;
|
|
406
|
-
if (state === "delta") {
|
|
407
|
-
const parsed = parseSessionKey(sessionKey);
|
|
408
|
-
if (parsed) {
|
|
409
|
-
const existing = this.streamState.get(sessionKey) || { buffer: "", threadId: parsed.threadId, state: "streaming", held: [] };
|
|
410
|
-
existing.buffer += extractContent(message);
|
|
411
|
-
if (isSilentReplyPrefix(existing.buffer, "NO_REPLY") || isSilentReplyPrefix(existing.buffer, "HEARTBEAT_OK")) {
|
|
412
|
-
existing.held = existing.held || [];
|
|
413
|
-
existing.held.push(rawData);
|
|
414
|
-
this.streamState.set(sessionKey, existing);
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
if (existing.held?.length > 0) {
|
|
418
|
-
for (const h of existing.held) this.broadcastToBrowsers(h);
|
|
419
|
-
existing.held = [];
|
|
420
|
-
}
|
|
421
|
-
this.streamState.set(sessionKey, existing);
|
|
422
|
-
}
|
|
423
|
-
this.broadcastToBrowsers(rawData);
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
const streamEntry = this.streamState.get(sessionKey);
|
|
427
|
-
if (state === "final" || state === "aborted" || state === "error") this.streamState.delete(sessionKey);
|
|
428
|
-
if (sessionKey?.includes("__clawchats_title_")) {
|
|
429
|
-
if (state === "final") {
|
|
430
|
-
const content = extractContent(message);
|
|
431
|
-
if (content && this.handleTitleResponse(sessionKey, content)) return;
|
|
432
|
-
} else if (state === "error" || state === "aborted") {
|
|
433
|
-
for (const key of this._pendingTitleGens.keys()) {
|
|
434
|
-
if (sessionKey === key || sessionKey.includes(key)) {
|
|
435
|
-
this._pendingTitleGens.delete(key);
|
|
436
|
-
break;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
if (state === "final") {
|
|
443
|
-
const rawContent = extractContent(message);
|
|
444
|
-
if (isSilentReplyExact(rawContent, "NO_REPLY") || isSilentReplyExact(rawContent, "HEARTBEAT_OK")) return;
|
|
445
|
-
if (streamEntry?.held?.length > 0) for (const h of streamEntry.held) this.broadcastToBrowsers(h);
|
|
446
|
-
this.broadcastToBrowsers(rawData);
|
|
447
|
-
this.saveAssistantMessage(sessionKey, message, seq);
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
if (state === "aborted" || state === "error") this.broadcastToBrowsers(rawData);
|
|
451
|
-
if (state === "error") this.saveErrorMarker(sessionKey, message);
|
|
452
|
-
}
|
|
453
|
-
saveAssistantMessage(sessionKey, message, seq) {
|
|
454
|
-
const parsed = parseSessionKey(sessionKey);
|
|
455
|
-
if (!parsed) return;
|
|
456
|
-
const ws = this.getWorkspaces();
|
|
457
|
-
if (!ws.workspaces[parsed.workspace]) {
|
|
458
|
-
console.log(`Ignoring response for deleted workspace: ${parsed.workspace}`);
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
const db = this.getDb(parsed.workspace);
|
|
462
|
-
if (!db.prepare("SELECT id FROM threads WHERE id = ?").get(parsed.threadId)) {
|
|
463
|
-
console.log(`Ignoring response for deleted thread: ${parsed.threadId}`);
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
let content = sanitizeAssistantContent(extractContent(message));
|
|
467
|
-
if (!content?.trim()) {
|
|
468
|
-
console.log(`Skipping empty assistant response for thread ${parsed.threadId}`);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
const pendingPaths = this.mediaStash?.get(sessionKey) ?? [];
|
|
472
|
-
this.mediaStash?.delete(sessionKey);
|
|
473
|
-
const IMAGE_EXTS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg", "ico", "avif", "tiff"]);
|
|
474
|
-
const AUDIO_EXTS = /* @__PURE__ */ new Set(["mp3", "wav", "ogg", "m4a", "flac", "aac", "opus", "wma"]);
|
|
475
|
-
const imagePaths = [], pendingAttachments = [];
|
|
476
|
-
for (const p of pendingPaths) {
|
|
477
|
-
const ext = (p.split(".").pop() || "").toLowerCase();
|
|
478
|
-
if (IMAGE_EXTS.has(ext)) imagePaths.push(p);
|
|
479
|
-
else pendingAttachments.push({ path: p, name: p.split("/").pop(), type: AUDIO_EXTS.has(ext) ? "audio" : "file" });
|
|
480
|
-
}
|
|
481
|
-
if (imagePaths.length > 0) content = content.trimEnd() + "\n\n" + imagePaths.map((p) => ``).join("\n");
|
|
482
|
-
if (pendingPaths.length > 0) console.log(`[clawchats] media-attach: ${imagePaths.length} image(s), ${pendingAttachments.length} attachment(s) for ${sessionKey}`);
|
|
483
|
-
const now = Date.now();
|
|
484
|
-
const pendingMsg = db.prepare(`SELECT id, metadata FROM messages WHERE thread_id = ? AND role = 'assistant' AND json_extract(metadata, '$.pending') = 1 ORDER BY timestamp DESC LIMIT 1`).get(parsed.threadId);
|
|
485
|
-
let messageId;
|
|
486
|
-
if (pendingMsg) {
|
|
487
|
-
const metadata = pendingMsg.metadata ? JSON.parse(pendingMsg.metadata) : {};
|
|
488
|
-
delete metadata.pending;
|
|
489
|
-
if (metadata.activityLog) {
|
|
490
|
-
const idx = metadata.activityLog.findLastIndex((s) => s.type === "assistant");
|
|
491
|
-
if (idx >= 0) metadata.activityLog.splice(idx, 1);
|
|
492
|
-
metadata.activitySummary = generateActivitySummary(metadata.activityLog);
|
|
493
|
-
}
|
|
494
|
-
if (pendingAttachments.length > 0) metadata.attachments = [...metadata.attachments || [], ...pendingAttachments];
|
|
495
|
-
db.prepare("UPDATE messages SET content = ?, metadata = ?, timestamp = ? WHERE id = ?").run(content, JSON.stringify(metadata), now, pendingMsg.id);
|
|
496
|
-
messageId = pendingMsg.id;
|
|
497
|
-
} else {
|
|
498
|
-
messageId = seq != null ? `gw-${parsed.threadId}-${seq}` : `gw-${parsed.threadId}-${now}`;
|
|
499
|
-
const newMeta = pendingAttachments.length > 0 ? JSON.stringify({ attachments: pendingAttachments }) : null;
|
|
500
|
-
db.prepare(`INSERT INTO messages (id, thread_id, role, content, status, metadata, timestamp, created_at) VALUES (?, ?, 'assistant', ?, 'sent', ?, ?, ?) ON CONFLICT(id) DO UPDATE SET content = excluded.content, metadata = COALESCE(excluded.metadata, metadata), timestamp = excluded.timestamp`).run(messageId, parsed.threadId, content, newMeta, now, now);
|
|
501
|
-
}
|
|
502
|
-
try {
|
|
503
|
-
db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(now, parsed.threadId);
|
|
504
|
-
db.prepare("INSERT OR IGNORE INTO unread_messages (thread_id, message_id, created_at) VALUES (?, ?, ?)").run(parsed.threadId, messageId, now);
|
|
505
|
-
syncThreadUnreadCount(db, parsed.threadId);
|
|
506
|
-
const threadInfo = db.prepare("SELECT title FROM threads WHERE id = ?").get(parsed.threadId);
|
|
507
|
-
const unreadCount = db.prepare("SELECT COUNT(*) as c FROM unread_messages WHERE thread_id = ?").get(parsed.threadId).c;
|
|
508
|
-
const preview = content.length > 120 ? content.substring(0, 120) + "..." : content;
|
|
509
|
-
this.broadcastToBrowsers(JSON.stringify({ type: "clawchats", event: "message-saved", threadId: parsed.threadId, workspace: parsed.workspace, messageId, timestamp: now, title: threadInfo?.title, preview, unreadCount, updatedContent: imagePaths.length > 0 ? content : void 0, updatedAttachments: pendingAttachments.length > 0 ? pendingAttachments : void 0 }));
|
|
510
|
-
this.broadcastToBrowsers(JSON.stringify({ type: "clawchats", event: "unread-update", workspace: parsed.workspace, threadId: parsed.threadId, messageId, action: "new", unreadCount, workspaceUnreadTotal: db.prepare("SELECT COALESCE(SUM(unread_count), 0) as total FROM threads").get().total, title: threadInfo?.title, preview, timestamp: now }));
|
|
511
|
-
console.log(`Saved assistant message to ${parsed.workspace}/${parsed.threadId} (${pendingMsg ? "merged into pending" : "seq: " + seq})`);
|
|
512
|
-
const msgCount = db.prepare("SELECT COUNT(*) as c FROM messages WHERE thread_id = ?").get(parsed.threadId).c;
|
|
513
|
-
if (msgCount === 2 || threadInfo?.title === "New chat") this.generateThreadTitle(db, parsed.threadId, parsed.workspace, true);
|
|
514
|
-
} catch (e) {
|
|
515
|
-
console.error("Failed to save assistant message:", e.message);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
saveErrorMarker(sessionKey, message) {
|
|
519
|
-
const parsed = parseSessionKey(sessionKey);
|
|
520
|
-
if (!parsed) return;
|
|
521
|
-
const ws = this.getWorkspaces();
|
|
522
|
-
if (!ws.workspaces[parsed.workspace]) return;
|
|
523
|
-
const db = this.getDb(parsed.workspace);
|
|
524
|
-
if (!db.prepare("SELECT id FROM threads WHERE id = ?").get(parsed.threadId)) return;
|
|
525
|
-
const now = Date.now();
|
|
526
|
-
try {
|
|
527
|
-
db.prepare("INSERT INTO messages (id, thread_id, role, content, status, metadata, timestamp, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(`gw-error-${parsed.threadId}-${now}`, parsed.threadId, "system", `[error] ${message?.error || message?.content || "Unknown error"}`, "sent", '{"transient":true}', now, now);
|
|
528
|
-
} catch (e) {
|
|
529
|
-
console.error("Failed to save error marker:", e.message);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
generateThreadTitle(db, threadId, workspace, skipHeuristic = false) {
|
|
533
|
-
if (!db.prepare("SELECT title FROM threads WHERE id = ?").get(threadId)) return;
|
|
534
|
-
const titleKey = `__clawchats_title_${threadId}`;
|
|
535
|
-
if (this._pendingTitleGens.has(titleKey)) return;
|
|
536
|
-
const firstUserMsg = db.prepare("SELECT content FROM messages WHERE thread_id = ? AND role = 'user' ORDER BY created_at ASC LIMIT 1").get(threadId);
|
|
537
|
-
if (!firstUserMsg?.content) return;
|
|
538
|
-
if (!skipHeuristic) {
|
|
539
|
-
const heuristic = firstUserMsg.content.replace(/\n.*/s, "").slice(0, 40).trim() + (firstUserMsg.content.length > 40 ? "..." : "");
|
|
540
|
-
db.prepare("UPDATE threads SET title = ? WHERE id = ?").run(heuristic, threadId);
|
|
541
|
-
this.broadcastToBrowsers(JSON.stringify({ type: "clawchats", event: "thread-title-updated", threadId, workspace, title: heuristic }));
|
|
542
|
-
}
|
|
543
|
-
const messages = db.prepare("SELECT role, content FROM messages WHERE thread_id = ? ORDER BY created_at ASC LIMIT 6").all(threadId);
|
|
544
|
-
if (messages.length < 2) return;
|
|
545
|
-
const conversation = messages.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content.length > 300 ? m.content.slice(0, 300) + "..." : m.content}`).join("\n\n");
|
|
546
|
-
const reqId = `title-${threadId}-${Date.now()}`;
|
|
547
|
-
this._pendingTitleGens.set(titleKey, { threadId, workspace, reqId });
|
|
548
|
-
setTimeout(() => {
|
|
549
|
-
if (this._pendingTitleGens.has(titleKey)) {
|
|
550
|
-
this._pendingTitleGens.delete(titleKey);
|
|
551
|
-
console.log(`Title gen timeout for ${threadId}`);
|
|
552
|
-
}
|
|
553
|
-
}, 3e4);
|
|
554
|
-
this.sendToGateway(JSON.stringify({ type: "req", id: reqId, method: "chat.send", params: { sessionKey: titleKey, message: `Based on this conversation, generate a concise 3-5 word title. Return ONLY the title text, no quotes, no explanation:
|
|
555
|
-
|
|
556
|
-
${conversation}
|
|
557
|
-
|
|
558
|
-
Title:`, deliver: false, idempotencyKey: reqId } }));
|
|
559
|
-
}
|
|
560
|
-
handleTitleResponse(sessionKey, content) {
|
|
561
|
-
let matchKey = null, pending = null;
|
|
562
|
-
for (const [key, val] of this._pendingTitleGens) {
|
|
563
|
-
if (sessionKey === key || sessionKey.includes(key)) {
|
|
564
|
-
matchKey = key;
|
|
565
|
-
pending = val;
|
|
566
|
-
break;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
if (!pending) return false;
|
|
570
|
-
this._pendingTitleGens.delete(matchKey);
|
|
571
|
-
let title = content.trim().replace(/^["']|["']$/g, "").replace(/^Title:\s*/i, "").replace(/\n.*/s, "").trim();
|
|
572
|
-
if (title.length > 50) title = title.substring(0, 47) + "...";
|
|
573
|
-
if (!title || title.length >= 100) return true;
|
|
574
|
-
const db = this.getDb(pending.workspace);
|
|
575
|
-
db.prepare("UPDATE threads SET title = ? WHERE id = ?").run(title, pending.threadId);
|
|
576
|
-
this.broadcastToBrowsers(JSON.stringify({ type: "clawchats", event: "thread-title-updated", threadId: pending.threadId, workspace: pending.workspace, title }));
|
|
577
|
-
console.log(`AI title generated for ${pending.threadId}: "${title}"`);
|
|
578
|
-
return true;
|
|
579
|
-
}
|
|
580
|
-
handleAgentEvent(payload) {
|
|
581
|
-
const { runId, stream, data, sessionKey } = payload;
|
|
582
|
-
if (!runId) return;
|
|
583
|
-
if (!this.activityLogs.has(runId)) this.activityLogs.set(runId, { sessionKey, steps: [], startTime: Date.now() });
|
|
584
|
-
const log = this.activityLogs.get(runId);
|
|
585
|
-
if (stream === "assistant") {
|
|
586
|
-
const text = data?.text || "";
|
|
587
|
-
if (text) {
|
|
588
|
-
let seg = log._currentAssistantSegment;
|
|
589
|
-
if (!seg || seg._sealed) {
|
|
590
|
-
seg = { type: "assistant", timestamp: Date.now(), text, _sealed: false };
|
|
591
|
-
log._currentAssistantSegment = seg;
|
|
592
|
-
log.steps.push(seg);
|
|
593
|
-
} else seg.text = text;
|
|
594
|
-
}
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
if (stream === "thinking") {
|
|
598
|
-
let step = log.steps.find((s) => s.type === "thinking");
|
|
599
|
-
if (step) step.text = data?.text || "";
|
|
600
|
-
else log.steps.push({ type: "thinking", timestamp: Date.now(), text: data?.text || "" });
|
|
601
|
-
writeActivityToDb(this.getDb, this.broadcastToBrowsers.bind(this), runId, log);
|
|
602
|
-
const now = Date.now();
|
|
603
|
-
if (!log._lastThinkingBroadcast || now - log._lastThinkingBroadcast >= 300) {
|
|
604
|
-
log._lastThinkingBroadcast = now;
|
|
605
|
-
this._broadcastActivityUpdate(runId, log);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
if (stream === "tool") {
|
|
609
|
-
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) log._currentAssistantSegment._sealed = true;
|
|
610
|
-
const argsMeta = data?.args ? data.args.command || data.args.path || data.args.query || data.args.url || Object.values(data.args).find((v) => typeof v === "string") || "" : "";
|
|
611
|
-
const step = { type: "tool", timestamp: Date.now(), name: data?.name || "unknown", phase: data?.phase || "start", toolCallId: data?.toolCallId, meta: data?.meta || (argsMeta ? String(argsMeta) : void 0), isError: data?.isError || false };
|
|
612
|
-
if (data?.phase === "result") {
|
|
613
|
-
const existing = log.steps.findLast((s) => s.toolCallId === data.toolCallId && (s.phase === "start" || s.phase === "running"));
|
|
614
|
-
if (existing) {
|
|
615
|
-
existing.phase = "done";
|
|
616
|
-
existing.resultMeta = data?.meta;
|
|
617
|
-
existing.isError = data?.isError || false;
|
|
618
|
-
existing.durationMs = Date.now() - existing.timestamp;
|
|
619
|
-
} else {
|
|
620
|
-
step.phase = "done";
|
|
621
|
-
log.steps.push(step);
|
|
622
|
-
}
|
|
623
|
-
} else if (data?.phase === "update") {
|
|
624
|
-
const existing = log.steps.findLast((s) => s.toolCallId === data.toolCallId);
|
|
625
|
-
if (existing) {
|
|
626
|
-
if (data?.meta) existing.resultMeta = data.meta;
|
|
627
|
-
if (data?.isError) existing.isError = true;
|
|
628
|
-
existing.phase = "running";
|
|
629
|
-
}
|
|
630
|
-
} else log.steps.push(step);
|
|
631
|
-
writeActivityToDb(this.getDb, this.broadcastToBrowsers.bind(this), runId, log);
|
|
632
|
-
this._broadcastActivityUpdate(runId, log);
|
|
633
|
-
}
|
|
634
|
-
if (stream === "lifecycle" && (data?.phase === "end" || data?.phase === "error")) {
|
|
635
|
-
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) log._currentAssistantSegment._sealed = true;
|
|
636
|
-
const idx = log.steps.findLastIndex((s) => s.type === "assistant");
|
|
637
|
-
if (idx >= 0) log.steps.splice(idx, 1);
|
|
638
|
-
writeActivityToDb(this.getDb, this.broadcastToBrowsers.bind(this), runId, log);
|
|
639
|
-
this.activityLogs.delete(runId);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
_broadcastActivityUpdate(runId, log) {
|
|
643
|
-
if (!log._parsed || !log._messageId) return;
|
|
644
|
-
const cleanSteps = log.steps.map((s) => {
|
|
645
|
-
const c = { ...s };
|
|
646
|
-
delete c._sealed;
|
|
647
|
-
return c;
|
|
648
|
-
});
|
|
649
|
-
this.broadcastToBrowsers(JSON.stringify({ type: "clawchats", event: "activity-updated", workspace: log._parsed.workspace, threadId: log._parsed.threadId, messageId: log._messageId, activityLog: cleanSteps, activitySummary: generateActivitySummary(log.steps) }));
|
|
650
|
-
}
|
|
651
|
-
broadcastToBrowsers(data) {
|
|
652
|
-
this.debugLogger.logFrame("SRV\u2192BR", data);
|
|
653
|
-
for (const client of this.browserClients.keys()) {
|
|
654
|
-
if (client.readyState === WS.OPEN) client.send(data);
|
|
655
|
-
}
|
|
656
|
-
for (const fn of this._externalBroadcastTargets) {
|
|
657
|
-
try {
|
|
658
|
-
fn(data);
|
|
659
|
-
} catch {
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
broadcastGatewayStatus(connected) {
|
|
664
|
-
this.broadcastToBrowsers(JSON.stringify({ type: "clawchats", event: "gateway-status", connected }));
|
|
665
|
-
}
|
|
666
|
-
sendToGateway(data) {
|
|
667
|
-
this.debugLogger.logFrame("SRV\u2192GW", data);
|
|
668
|
-
if (this.ws?.readyState === WS.OPEN) this.ws.send(data);
|
|
669
|
-
else console.error("Cannot send to gateway: not connected");
|
|
670
|
-
}
|
|
671
|
-
scheduleReconnect() {
|
|
672
|
-
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay);
|
|
673
|
-
this.reconnectAttempts++;
|
|
674
|
-
console.log(`Reconnecting to gateway in ${delay}ms (attempt ${this.reconnectAttempts})...`);
|
|
675
|
-
setTimeout(() => this.connect(), delay);
|
|
676
|
-
}
|
|
677
|
-
addBrowserClient(ws) {
|
|
678
|
-
this.browserClients.set(ws, { activeWorkspace: null, activeThreadId: null });
|
|
679
|
-
if (ws.readyState === WS.OPEN) {
|
|
680
|
-
ws.send(JSON.stringify({ type: "clawchats", event: "gateway-status", connected: this.connected }));
|
|
681
|
-
const streams = [];
|
|
682
|
-
for (const [sessionKey, state] of this.streamState.entries()) {
|
|
683
|
-
if (state.state === "streaming" && !(state.held?.length > 0)) streams.push({ sessionKey, threadId: state.threadId, buffer: state.buffer });
|
|
684
|
-
}
|
|
685
|
-
if (streams.length > 0) ws.send(JSON.stringify({ type: "clawchats", event: "stream-sync", streams }));
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
removeBrowserClient(ws) {
|
|
689
|
-
this.browserClients.delete(ws);
|
|
690
|
-
}
|
|
691
|
-
setActiveThread(ws, workspace, threadId) {
|
|
692
|
-
const client = ws ? this.browserClients.get(ws) : null;
|
|
693
|
-
if (client) {
|
|
694
|
-
client.activeWorkspace = workspace;
|
|
695
|
-
client.activeThreadId = threadId;
|
|
696
|
-
}
|
|
697
|
-
if (!workspace || !threadId) return;
|
|
698
|
-
try {
|
|
699
|
-
const wsData = this.getWorkspaces();
|
|
700
|
-
if (!wsData.workspaces[workspace]) return;
|
|
701
|
-
const db = this.getDb(workspace);
|
|
702
|
-
if (!db.prepare("SELECT id FROM threads WHERE id = ?").get(threadId)) return;
|
|
703
|
-
const deleted = db.prepare("DELETE FROM unread_messages WHERE thread_id = ?").run(threadId);
|
|
704
|
-
if (deleted.changes > 0) {
|
|
705
|
-
syncThreadUnreadCount(db, threadId);
|
|
706
|
-
this.broadcastToBrowsers(JSON.stringify({ type: "clawchats", event: "unread-update", workspace, threadId, action: "clear", unreadCount: 0, workspaceUnreadTotal: db.prepare("SELECT COALESCE(SUM(unread_count), 0) as total FROM threads").get().total, timestamp: Date.now() }));
|
|
707
|
-
}
|
|
708
|
-
} catch (e) {
|
|
709
|
-
console.error("Failed to auto-clear unreads on active-thread:", e.message);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
addBroadcastTarget(fn) {
|
|
713
|
-
this._externalBroadcastTargets.push(fn);
|
|
714
|
-
}
|
|
715
|
-
removeBroadcastTarget(fn) {
|
|
716
|
-
this._externalBroadcastTargets = this._externalBroadcastTargets.filter((f) => f !== fn);
|
|
717
|
-
}
|
|
718
|
-
};
|
|
719
|
-
|
|
720
|
-
// server/providers/memory.js
|
|
721
|
-
import fs4 from "node:fs";
|
|
722
|
-
import path5 from "node:path";
|
|
723
|
-
import os2 from "node:os";
|
|
724
|
-
function discoverMemoryConfig() {
|
|
725
|
-
const defaults = { provider: "qdrant", host: "localhost", port: 6333, collection: null };
|
|
726
|
-
let oc = null;
|
|
727
|
-
for (const cfgPath of [path5.join(os2.homedir(), ".openclaw", "openclaw.json"), "/etc/openclaw/openclaw.json"]) {
|
|
728
|
-
try {
|
|
729
|
-
oc = JSON.parse(fs4.readFileSync(cfgPath, "utf8"));
|
|
730
|
-
break;
|
|
731
|
-
} catch {
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
let cfg = { ...defaults };
|
|
735
|
-
if (oc) {
|
|
736
|
-
const vs = oc.plugins?.slots?.memory ? oc.plugins?.entries?.[oc.plugins.slots.memory]?.config?.oss?.vectorStore : null;
|
|
737
|
-
if (vs) {
|
|
738
|
-
if (vs.provider) cfg.provider = vs.provider;
|
|
739
|
-
if (vs.config?.host) cfg.host = vs.config.host;
|
|
740
|
-
if (vs.config?.port) cfg.port = vs.config.port;
|
|
741
|
-
if (vs.config?.collectionName) cfg.collection = vs.config.collectionName;
|
|
742
|
-
if (vs.config?.user) cfg.pgUser = vs.config.user;
|
|
743
|
-
if (vs.config?.password) cfg.pgPassword = vs.config.password;
|
|
744
|
-
if (vs.config?.dbname) cfg.pgDbName = vs.config.dbname;
|
|
745
|
-
}
|
|
746
|
-
const wsDir = oc.agents?.defaults?.workspace;
|
|
747
|
-
if (wsDir) cfg.workspaceDir = wsDir;
|
|
748
|
-
}
|
|
749
|
-
if (process.env.MEMORY_PROVIDER) cfg.provider = process.env.MEMORY_PROVIDER;
|
|
750
|
-
if (process.env.MEMORY_HOST || process.env.QDRANT_HOST) cfg.host = process.env.MEMORY_HOST || process.env.QDRANT_HOST;
|
|
751
|
-
if (process.env.MEMORY_PORT || process.env.QDRANT_PORT) cfg.port = parseInt(process.env.MEMORY_PORT || process.env.QDRANT_PORT, 10);
|
|
752
|
-
if (process.env.MEMORY_COLLECTION || process.env.QDRANT_COLLECTION) cfg.collection = process.env.MEMORY_COLLECTION || process.env.QDRANT_COLLECTION;
|
|
753
|
-
if (process.env.MEMORY_PG_URL) cfg.pgUrl = process.env.MEMORY_PG_URL;
|
|
754
|
-
if (process.env.QDRANT_URL && !process.env.MEMORY_HOST) {
|
|
755
|
-
try {
|
|
756
|
-
const u = new URL(process.env.QDRANT_URL);
|
|
757
|
-
cfg.host = u.hostname;
|
|
758
|
-
if (u.port) cfg.port = parseInt(u.port, 10);
|
|
759
|
-
} catch {
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
if (!cfg.workspaceDir) cfg.workspaceDir = path5.join(os2.homedir(), ".openclaw", "workspace");
|
|
763
|
-
return cfg;
|
|
764
|
-
}
|
|
765
|
-
__name(discoverMemoryConfig, "discoverMemoryConfig");
|
|
766
|
-
async function autoDetectQdrantCollection(config) {
|
|
767
|
-
if (config.collection) return config.collection;
|
|
768
|
-
try {
|
|
769
|
-
const r = await fetch(`http://${config.host}:${config.port}/collections`, { signal: AbortSignal.timeout(3e3) });
|
|
770
|
-
const data = await r.json();
|
|
771
|
-
const found = (data.result?.collections || []).map((c) => c.name).find((n) => !n.includes("migration"));
|
|
772
|
-
if (found) {
|
|
773
|
-
console.log(`Memory: auto-detected Qdrant collection "${found}"`);
|
|
774
|
-
return found;
|
|
775
|
-
}
|
|
776
|
-
} catch {
|
|
777
|
-
}
|
|
778
|
-
console.log('Memory: Qdrant unreachable or no collections, falling back to "memories"');
|
|
779
|
-
return "memories";
|
|
780
|
-
}
|
|
781
|
-
__name(autoDetectQdrantCollection, "autoDetectQdrantCollection");
|
|
782
|
-
function createQdrantProvider(config) {
|
|
783
|
-
const baseUrl = `http://${config.host}:${config.port}`;
|
|
784
|
-
let collection = config.collection;
|
|
785
|
-
return {
|
|
786
|
-
name: "qdrant",
|
|
787
|
-
config,
|
|
788
|
-
async init() {
|
|
789
|
-
collection = await autoDetectQdrantCollection(config);
|
|
790
|
-
config.collection = collection;
|
|
791
|
-
},
|
|
792
|
-
async list(limit, offset) {
|
|
793
|
-
const body = { limit, with_payload: true, with_vector: false };
|
|
794
|
-
if (offset) body.offset = offset;
|
|
795
|
-
const r = await fetch(`${baseUrl}/collections/${collection}/points/scroll`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
796
|
-
const data = await r.json();
|
|
797
|
-
return { memories: (data.result?.points || []).map((p) => ({ id: p.id, ...p.payload })), next_offset: data.result?.next_page_offset || null };
|
|
798
|
-
},
|
|
799
|
-
async search(query) {
|
|
800
|
-
const q = query.toLowerCase();
|
|
801
|
-
const matches = [];
|
|
802
|
-
let offset = null;
|
|
803
|
-
do {
|
|
804
|
-
const body = { limit: 100, with_payload: true, with_vector: false };
|
|
805
|
-
if (offset) body.offset = offset;
|
|
806
|
-
const r = await fetch(`${baseUrl}/collections/${collection}/points/scroll`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
807
|
-
const data = await r.json();
|
|
808
|
-
for (const p of data.result?.points || []) {
|
|
809
|
-
if ((p.payload?.data || "").toLowerCase().includes(q)) matches.push({ id: p.id, ...p.payload });
|
|
810
|
-
}
|
|
811
|
-
offset = data.result?.next_page_offset || null;
|
|
812
|
-
} while (offset);
|
|
813
|
-
return { memories: matches, next_offset: null };
|
|
814
|
-
},
|
|
815
|
-
async update(id, newData) {
|
|
816
|
-
const r = await fetch(`${baseUrl}/collections/${collection}/points/payload`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points: [id], payload: { data: newData } }) });
|
|
817
|
-
const data = await r.json();
|
|
818
|
-
if (data.status?.error) throw new Error(data.status.error);
|
|
819
|
-
return data.result;
|
|
820
|
-
},
|
|
821
|
-
async delete(id) {
|
|
822
|
-
const r = await fetch(`${baseUrl}/collections/${collection}/points/delete`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points: [id] }) });
|
|
823
|
-
return (await r.json()).result;
|
|
824
|
-
},
|
|
825
|
-
async status() {
|
|
826
|
-
try {
|
|
827
|
-
const r = await fetch(`${baseUrl}/collections/${collection}`, { signal: AbortSignal.timeout(3e3) });
|
|
828
|
-
const data = await r.json();
|
|
829
|
-
return { reachable: true, pointsCount: data.result?.points_count ?? null };
|
|
830
|
-
} catch {
|
|
831
|
-
return { reachable: false };
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
__name(createQdrantProvider, "createQdrantProvider");
|
|
837
|
-
function createPgProvider(config) {
|
|
838
|
-
let _pool = null;
|
|
839
|
-
const table = config.collection || "memories";
|
|
840
|
-
async function getPool() {
|
|
841
|
-
if (_pool) return _pool;
|
|
842
|
-
let pg;
|
|
843
|
-
try {
|
|
844
|
-
pg = await import("pg");
|
|
845
|
-
} catch {
|
|
846
|
-
throw new Error("pg package not installed. Run: npm install pg");
|
|
847
|
-
}
|
|
848
|
-
const Pool = pg.default?.Pool || pg.Pool;
|
|
849
|
-
_pool = config.pgUrl ? new Pool({ connectionString: config.pgUrl }) : new Pool({ host: config.host, port: config.port || 5432, user: config.pgUser || "mem0", password: config.pgPassword || "", database: config.pgDbName || "mem0" });
|
|
850
|
-
return _pool;
|
|
851
|
-
}
|
|
852
|
-
__name(getPool, "getPool");
|
|
853
|
-
return {
|
|
854
|
-
name: "postgres",
|
|
855
|
-
config,
|
|
856
|
-
async init() {
|
|
857
|
-
},
|
|
858
|
-
async list(limit, offset) {
|
|
859
|
-
const pool = await getPool();
|
|
860
|
-
const off = offset ? parseInt(offset, 10) : 0;
|
|
861
|
-
const { rows } = await pool.query(`SELECT id, payload FROM ${table} ORDER BY created_at DESC LIMIT $1 OFFSET $2`, [limit, off]);
|
|
862
|
-
return { memories: rows.map((r) => ({ id: r.id, ...typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload })), next_offset: rows.length === limit ? off + limit : null };
|
|
863
|
-
},
|
|
864
|
-
async search(query) {
|
|
865
|
-
const pool = await getPool();
|
|
866
|
-
const { rows } = await pool.query(`SELECT id, payload FROM ${table} WHERE payload->>'data' ILIKE $1 LIMIT 100`, [`%${query}%`]);
|
|
867
|
-
return { memories: rows.map((r) => ({ id: r.id, ...typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload })), next_offset: null };
|
|
868
|
-
},
|
|
869
|
-
async update(id, newData) {
|
|
870
|
-
const pool = await getPool();
|
|
871
|
-
const { rowCount } = await pool.query(`UPDATE ${table} SET payload = jsonb_set(payload, '{data}', $1::jsonb) WHERE id = $2`, [JSON.stringify(newData), id]);
|
|
872
|
-
if (rowCount === 0) throw new Error("Memory not found");
|
|
873
|
-
return { updated: true };
|
|
874
|
-
},
|
|
875
|
-
async delete(id) {
|
|
876
|
-
const pool = await getPool();
|
|
877
|
-
await pool.query(`DELETE FROM ${table} WHERE id = $1`, [id]);
|
|
878
|
-
return { deleted: true };
|
|
879
|
-
},
|
|
880
|
-
async status() {
|
|
881
|
-
try {
|
|
882
|
-
const pool = await getPool();
|
|
883
|
-
const { rows } = await pool.query(`SELECT COUNT(*) as count FROM ${table}`);
|
|
884
|
-
return { reachable: true, pointsCount: parseInt(rows[0].count, 10) };
|
|
885
|
-
} catch (err) {
|
|
886
|
-
return { reachable: false, error: err.message };
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
__name(createPgProvider, "createPgProvider");
|
|
892
|
-
function createMemoryProvider(config) {
|
|
893
|
-
if (config.provider === "postgres" || config.provider === "pgvector") return createPgProvider(config);
|
|
894
|
-
return createQdrantProvider(config);
|
|
895
|
-
}
|
|
896
|
-
__name(createMemoryProvider, "createMemoryProvider");
|
|
897
|
-
|
|
898
|
-
// server/controllers/workspaces.js
|
|
899
|
-
import fs6 from "node:fs";
|
|
900
|
-
import path7 from "node:path";
|
|
901
|
-
|
|
902
|
-
// server/util/http.js
|
|
903
|
-
import crypto2 from "node:crypto";
|
|
904
|
-
function parseBody(req) {
|
|
905
|
-
return new Promise((resolve, reject) => {
|
|
906
|
-
const chunks = [];
|
|
907
|
-
req.on("data", (c) => chunks.push(c));
|
|
908
|
-
req.on("end", () => {
|
|
909
|
-
const raw = Buffer.concat(chunks).toString();
|
|
910
|
-
if (!raw) return resolve({});
|
|
911
|
-
try {
|
|
912
|
-
resolve(JSON.parse(raw));
|
|
913
|
-
} catch {
|
|
914
|
-
reject(new Error("Invalid JSON"));
|
|
915
|
-
}
|
|
916
|
-
});
|
|
917
|
-
req.on("error", reject);
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
__name(parseBody, "parseBody");
|
|
921
|
-
function send(res, status, data) {
|
|
922
|
-
const body = JSON.stringify(data);
|
|
923
|
-
res.writeHead(status, {
|
|
924
|
-
"Content-Type": "application/json",
|
|
925
|
-
"Access-Control-Allow-Origin": "*",
|
|
926
|
-
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
927
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
928
|
-
});
|
|
929
|
-
res.end(body);
|
|
930
|
-
}
|
|
931
|
-
__name(send, "send");
|
|
932
|
-
function sendError(res, status, message) {
|
|
933
|
-
send(res, status, { error: message });
|
|
934
|
-
}
|
|
935
|
-
__name(sendError, "sendError");
|
|
936
|
-
function setCors(res) {
|
|
937
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
938
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS");
|
|
939
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
940
|
-
}
|
|
941
|
-
__name(setCors, "setCors");
|
|
942
|
-
function uuid() {
|
|
943
|
-
return crypto2.randomUUID();
|
|
944
|
-
}
|
|
945
|
-
__name(uuid, "uuid");
|
|
946
|
-
function matchRoute(method, url, pattern) {
|
|
947
|
-
const [pMethod, pPath] = pattern.split(" ");
|
|
948
|
-
if (method !== pMethod) return null;
|
|
949
|
-
const pParts = pPath.split("/").filter(Boolean);
|
|
950
|
-
const uParts = url.split("/").filter(Boolean);
|
|
951
|
-
if (pParts.length !== uParts.length) return null;
|
|
952
|
-
const params = {};
|
|
953
|
-
for (let i = 0; i < pParts.length; i++) {
|
|
954
|
-
if (pParts[i].startsWith(":")) {
|
|
955
|
-
params[pParts[i].slice(1)] = decodeURIComponent(uParts[i]);
|
|
956
|
-
} else if (pParts[i] !== uParts[i]) {
|
|
957
|
-
return null;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
return params;
|
|
961
|
-
}
|
|
962
|
-
__name(matchRoute, "matchRoute");
|
|
963
|
-
|
|
964
|
-
// server/gateway-cleanup.js
|
|
965
|
-
import fs5 from "node:fs";
|
|
966
|
-
import path6 from "node:path";
|
|
967
|
-
function cleanGatewaySession(sessionKey) {
|
|
968
|
-
try {
|
|
969
|
-
const agentMatch = (sessionKey || "").match(/^agent:([^:]+):/);
|
|
970
|
-
const sessionsDir = getSessionsDirForAgent(agentMatch?.[1]);
|
|
971
|
-
const sessionsPath = path6.join(sessionsDir, "sessions.json");
|
|
972
|
-
const store = JSON.parse(fs5.readFileSync(sessionsPath, "utf8"));
|
|
973
|
-
const entry = store[sessionKey];
|
|
974
|
-
if (!entry) return null;
|
|
975
|
-
if (entry.sessionId) {
|
|
976
|
-
try {
|
|
977
|
-
fs5.unlinkSync(path6.join(sessionsDir, `${entry.sessionId}.jsonl`));
|
|
978
|
-
} catch {
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
const sessionId = entry.sessionId || null;
|
|
982
|
-
delete store[sessionKey];
|
|
983
|
-
fs5.writeFileSync(sessionsPath, JSON.stringify(store, null, 2));
|
|
984
|
-
return sessionId;
|
|
985
|
-
} catch (err) {
|
|
986
|
-
console.warn(`cleanGatewaySession(${sessionKey}):`, err.message);
|
|
987
|
-
return null;
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
__name(cleanGatewaySession, "cleanGatewaySession");
|
|
991
|
-
function cleanGatewaySessionsByPrefix(prefix) {
|
|
992
|
-
try {
|
|
993
|
-
const agentMatch = (prefix || "").match(/^agent:([^:]+):/);
|
|
994
|
-
const sessionsDir = getSessionsDirForAgent(agentMatch?.[1]);
|
|
995
|
-
const sessionsPath = path6.join(sessionsDir, "sessions.json");
|
|
996
|
-
const store = JSON.parse(fs5.readFileSync(sessionsPath, "utf8"));
|
|
997
|
-
let cleaned = 0;
|
|
998
|
-
for (const key of Object.keys(store)) {
|
|
999
|
-
if (!key.startsWith(prefix)) continue;
|
|
1000
|
-
if (store[key]?.sessionId) {
|
|
1001
|
-
try {
|
|
1002
|
-
fs5.unlinkSync(path6.join(sessionsDir, `${store[key].sessionId}.jsonl`));
|
|
1003
|
-
} catch {
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
delete store[key];
|
|
1007
|
-
cleaned++;
|
|
1008
|
-
}
|
|
1009
|
-
if (cleaned > 0) fs5.writeFileSync(sessionsPath, JSON.stringify(store, null, 2));
|
|
1010
|
-
return cleaned;
|
|
1011
|
-
} catch (err) {
|
|
1012
|
-
console.warn(`cleanGatewaySessionsByPrefix(${prefix}):`, err.message);
|
|
1013
|
-
return 0;
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
__name(cleanGatewaySessionsByPrefix, "cleanGatewaySessionsByPrefix");
|
|
1017
|
-
|
|
1018
|
-
// server/controllers/workspaces.js
|
|
1019
|
-
var WorkspaceController = class {
|
|
1020
|
-
static {
|
|
1021
|
-
__name(this, "WorkspaceController");
|
|
1022
|
-
}
|
|
1023
|
-
constructor({ getDb, closeDb, getWorkspaces, setWorkspaces, dataDir, broadcast }) {
|
|
1024
|
-
this.getDb = getDb;
|
|
1025
|
-
this.closeDb = closeDb;
|
|
1026
|
-
this.getWorkspaces = getWorkspaces;
|
|
1027
|
-
this.setWorkspaces = setWorkspaces;
|
|
1028
|
-
this.dataDir = dataDir;
|
|
1029
|
-
this.broadcast = broadcast;
|
|
1030
|
-
}
|
|
1031
|
-
getAll(req, res) {
|
|
1032
|
-
const ws = this.getWorkspaces();
|
|
1033
|
-
const sorted = Object.values(ws.workspaces).sort((a, b) => (a.order ?? 999) - (b.order ?? 999));
|
|
1034
|
-
for (const workspace of sorted) {
|
|
1035
|
-
try {
|
|
1036
|
-
workspace.unread_count = this.getDb(workspace.name).prepare("SELECT COALESCE(SUM(unread_count), 0) as total FROM threads").get().total;
|
|
1037
|
-
} catch {
|
|
1038
|
-
workspace.unread_count = 0;
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
send(res, 200, { active: ws.active, workspaces: sorted });
|
|
1042
|
-
}
|
|
1043
|
-
async create(req, res) {
|
|
1044
|
-
const body = await parseBody(req);
|
|
1045
|
-
const { name, label } = body;
|
|
1046
|
-
if (!name || !/^[a-z0-9-]{1,32}$/.test(name)) return sendError(res, 400, "Name must be [a-z0-9-], 1-32 chars");
|
|
1047
|
-
const ws = this.getWorkspaces();
|
|
1048
|
-
if (ws.workspaces[name]) return sendError(res, 409, "Workspace already exists");
|
|
1049
|
-
let agent = "main";
|
|
1050
|
-
try {
|
|
1051
|
-
agent = validateAgent(body.agent || "main");
|
|
1052
|
-
} catch {
|
|
1053
|
-
agent = "main";
|
|
1054
|
-
}
|
|
1055
|
-
ws.workspaces[name] = { name, label: label || name, color: body.color || null, icon: body.icon || null, agent, createdAt: Date.now() };
|
|
1056
|
-
this.setWorkspaces(ws);
|
|
1057
|
-
this.getDb(name);
|
|
1058
|
-
send(res, 201, { workspace: ws.workspaces[name] });
|
|
1059
|
-
}
|
|
1060
|
-
async update(req, res, params) {
|
|
1061
|
-
const body = await parseBody(req);
|
|
1062
|
-
const ws = this.getWorkspaces();
|
|
1063
|
-
if (!ws.workspaces[params.name]) return sendError(res, 404, "Workspace not found");
|
|
1064
|
-
if (body.label !== void 0) ws.workspaces[params.name].label = body.label;
|
|
1065
|
-
if (body.color !== void 0) ws.workspaces[params.name].color = body.color;
|
|
1066
|
-
if (body.icon !== void 0) ws.workspaces[params.name].icon = body.icon;
|
|
1067
|
-
if (body.lastThread !== void 0) ws.workspaces[params.name].lastThread = body.lastThread;
|
|
1068
|
-
let migratedThreads = 0;
|
|
1069
|
-
if (body.agent !== void 0) {
|
|
1070
|
-
let newAgent;
|
|
1071
|
-
try {
|
|
1072
|
-
newAgent = validateAgent(body.agent);
|
|
1073
|
-
} catch (e) {
|
|
1074
|
-
return sendError(res, 400, e.message);
|
|
1075
|
-
}
|
|
1076
|
-
const oldAgent = ws.workspaces[params.name].agent || "main";
|
|
1077
|
-
if (newAgent !== oldAgent) {
|
|
1078
|
-
const db = this.getDb(params.name);
|
|
1079
|
-
const threads = db.prepare(`SELECT id, session_key FROM threads WHERE session_key LIKE ?`).all(`agent:${oldAgent}:${params.name}:chat:%`);
|
|
1080
|
-
db.prepare(`UPDATE threads SET session_key = replace(session_key, 'agent:' || ? || ':' || ? || ':chat:', 'agent:' || ? || ':' || ? || ':chat:') WHERE session_key LIKE 'agent:' || ? || ':' || ? || ':chat:%'`).run(oldAgent, params.name, newAgent, params.name, oldAgent, params.name);
|
|
1081
|
-
for (const t of threads) cleanGatewaySession(t.session_key);
|
|
1082
|
-
ws.workspaces[params.name].agent = newAgent;
|
|
1083
|
-
migratedThreads = threads.length;
|
|
1084
|
-
this.broadcast(JSON.stringify({ type: "clawchats", event: "workspace-agent-changed", workspace: params.name, agent: newAgent }));
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
this.setWorkspaces(ws);
|
|
1088
|
-
send(res, 200, { workspace: ws.workspaces[params.name], migratedThreads });
|
|
1089
|
-
}
|
|
1090
|
-
delete(req, res, params) {
|
|
1091
|
-
const ws = this.getWorkspaces();
|
|
1092
|
-
if (!ws.workspaces[params.name]) return sendError(res, 404, "Workspace not found");
|
|
1093
|
-
if (Object.keys(ws.workspaces).length <= 1) return sendError(res, 400, "Cannot delete the only workspace");
|
|
1094
|
-
this.closeDb(params.name);
|
|
1095
|
-
const dbPath = path7.join(this.dataDir, `${params.name}.db`);
|
|
1096
|
-
for (const suffix of ["", "-wal", "-shm"]) {
|
|
1097
|
-
try {
|
|
1098
|
-
fs6.unlinkSync(dbPath + suffix);
|
|
1099
|
-
} catch {
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
const wsAgent = ws.workspaces[params.name]?.agent || "main";
|
|
1103
|
-
const cleaned = cleanGatewaySessionsByPrefix(`agent:${wsAgent}:${params.name}:chat:`);
|
|
1104
|
-
if (cleaned > 0) console.log(`Cleaned ${cleaned} gateway sessions for workspace: ${params.name}`);
|
|
1105
|
-
delete ws.workspaces[params.name];
|
|
1106
|
-
if (ws.active === params.name) ws.active = Object.keys(ws.workspaces)[0] || null;
|
|
1107
|
-
this.setWorkspaces(ws);
|
|
1108
|
-
send(res, 200, { ok: true });
|
|
1109
|
-
}
|
|
1110
|
-
async reorder(req, res) {
|
|
1111
|
-
const body = await parseBody(req);
|
|
1112
|
-
if (!Array.isArray(body.order)) return sendError(res, 400, "order must be an array of workspace names");
|
|
1113
|
-
const ws = this.getWorkspaces();
|
|
1114
|
-
body.order.forEach((name, i) => {
|
|
1115
|
-
if (ws.workspaces[name]) ws.workspaces[name].order = i;
|
|
1116
|
-
});
|
|
1117
|
-
this.setWorkspaces(ws);
|
|
1118
|
-
send(res, 200, { ok: true, workspaces: Object.values(ws.workspaces) });
|
|
1119
|
-
}
|
|
1120
|
-
activate(req, res, params) {
|
|
1121
|
-
const ws = this.getWorkspaces();
|
|
1122
|
-
if (!ws.workspaces[params.name]) return sendError(res, 404, "Workspace not found");
|
|
1123
|
-
ws.active = params.name;
|
|
1124
|
-
this.setWorkspaces(ws);
|
|
1125
|
-
this.getDb(params.name);
|
|
1126
|
-
send(res, 200, { ok: true, workspace: ws.workspaces[params.name] });
|
|
1127
|
-
}
|
|
1128
|
-
};
|
|
1129
|
-
|
|
1130
|
-
// server/controllers/threads.js
|
|
1131
|
-
import fs7 from "node:fs";
|
|
1132
|
-
import path8 from "node:path";
|
|
1133
|
-
var ThreadController = class {
|
|
1134
|
-
static {
|
|
1135
|
-
__name(this, "ThreadController");
|
|
1136
|
-
}
|
|
1137
|
-
constructor({ getActiveDb, getWorkspaces, uploadsDir, broadcast }) {
|
|
1138
|
-
this.getActiveDb = getActiveDb;
|
|
1139
|
-
this.getWorkspaces = getWorkspaces;
|
|
1140
|
-
this.uploadsDir = uploadsDir;
|
|
1141
|
-
this.broadcast = broadcast;
|
|
1142
|
-
}
|
|
1143
|
-
getAll(req, res, params, query) {
|
|
1144
|
-
const db = this.getActiveDb();
|
|
1145
|
-
const page = parseInt(query.page || "1", 10);
|
|
1146
|
-
const limit = Math.min(parseInt(query.limit || "50", 10), 200);
|
|
1147
|
-
const offset = (page - 1) * limit;
|
|
1148
|
-
const search = query.search || "";
|
|
1149
|
-
let threads, total;
|
|
1150
|
-
if (search) {
|
|
1151
|
-
try {
|
|
1152
|
-
const matchingIds = db.prepare(`SELECT DISTINCT m.thread_id FROM messages m JOIN messages_fts ON messages_fts.rowid = m.rowid WHERE messages_fts MATCH ?`).all(search).map((r) => r.thread_id);
|
|
1153
|
-
if (!matchingIds.length) return send(res, 200, { threads: [], total: 0, page });
|
|
1154
|
-
const ph = matchingIds.map(() => "?").join(",");
|
|
1155
|
-
total = db.prepare(`SELECT COUNT(*) as c FROM threads WHERE id IN (${ph})`).get(...matchingIds).c;
|
|
1156
|
-
threads = db.prepare(`SELECT * FROM threads WHERE id IN (${ph}) ORDER BY pinned DESC, sort_order DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...matchingIds, limit, offset);
|
|
1157
|
-
} catch {
|
|
1158
|
-
return send(res, 200, { threads: [], total: 0, page });
|
|
1159
|
-
}
|
|
1160
|
-
} else {
|
|
1161
|
-
total = db.prepare("SELECT COUNT(*) as c FROM threads").get().c;
|
|
1162
|
-
threads = db.prepare("SELECT * FROM threads ORDER BY pinned DESC, sort_order DESC, updated_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
1163
|
-
}
|
|
1164
|
-
send(res, 200, { threads, total, page });
|
|
1165
|
-
}
|
|
1166
|
-
getUnread(req, res) {
|
|
1167
|
-
const db = this.getActiveDb();
|
|
1168
|
-
const threads = db.prepare(`SELECT t.id, t.title, t.unread_count, m.content as lastMessage FROM threads t LEFT JOIN messages m ON m.thread_id = t.id WHERE t.unread_count > 0 AND m.timestamp = (SELECT MAX(timestamp) FROM messages WHERE thread_id = t.id) ORDER BY t.updated_at DESC`).all();
|
|
1169
|
-
for (const t of threads) t.unreadMessageIds = db.prepare("SELECT message_id FROM unread_messages WHERE thread_id = ?").all(t.id).map((r) => r.message_id);
|
|
1170
|
-
send(res, 200, { threads });
|
|
1171
|
-
}
|
|
1172
|
-
async markRead(req, res, params) {
|
|
1173
|
-
const { messageIds } = await parseBody(req);
|
|
1174
|
-
if (!Array.isArray(messageIds) || !messageIds.length) return send(res, 400, { error: "messageIds array required" });
|
|
1175
|
-
const db = this.getActiveDb();
|
|
1176
|
-
const ph = messageIds.map(() => "?").join(",");
|
|
1177
|
-
db.prepare(`DELETE FROM unread_messages WHERE thread_id = ? AND message_id IN (${ph})`).run(params.id, ...messageIds);
|
|
1178
|
-
const remaining = syncThreadUnreadCount(db, params.id);
|
|
1179
|
-
this.broadcast(JSON.stringify({ type: "clawchats", event: "unread-update", workspace: this.getWorkspaces().active, threadId: params.id, action: "read", messageIds, unreadCount: remaining, timestamp: Date.now() }));
|
|
1180
|
-
send(res, 200, { unread_count: remaining });
|
|
1181
|
-
}
|
|
1182
|
-
async create(req, res) {
|
|
1183
|
-
const body = await parseBody(req);
|
|
1184
|
-
const db = this.getActiveDb();
|
|
1185
|
-
const ws = this.getWorkspaces();
|
|
1186
|
-
const id = body.id || uuid();
|
|
1187
|
-
const now = Date.now();
|
|
1188
|
-
const agent = ws.workspaces[ws.active]?.agent || "main";
|
|
1189
|
-
try {
|
|
1190
|
-
db.prepare("INSERT INTO threads (id, session_key, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(id, `agent:${agent}:${ws.active}:chat:${id}`, "New chat", now, now);
|
|
1191
|
-
} catch (e) {
|
|
1192
|
-
if (e.message.includes("UNIQUE constraint")) return sendError(res, 409, "Thread already exists");
|
|
1193
|
-
throw e;
|
|
1194
|
-
}
|
|
1195
|
-
send(res, 201, { thread: db.prepare("SELECT * FROM threads WHERE id = ?").get(id) });
|
|
1196
|
-
}
|
|
1197
|
-
get(req, res, params) {
|
|
1198
|
-
const thread = this.getActiveDb().prepare("SELECT * FROM threads WHERE id = ?").get(params.id);
|
|
1199
|
-
if (!thread) return sendError(res, 404, "Thread not found");
|
|
1200
|
-
send(res, 200, { thread });
|
|
1201
|
-
}
|
|
1202
|
-
async update(req, res, params) {
|
|
1203
|
-
const body = await parseBody(req);
|
|
1204
|
-
const db = this.getActiveDb();
|
|
1205
|
-
if (!db.prepare("SELECT id FROM threads WHERE id = ?").get(params.id)) return sendError(res, 404, "Thread not found");
|
|
1206
|
-
const fields = [], values = [];
|
|
1207
|
-
for (const [col, val] of [["title", body.title], ["model", body.model], ["last_session_id", body.last_session_id], ["unread_count", body.unread_count]]) {
|
|
1208
|
-
if (val !== void 0) {
|
|
1209
|
-
fields.push(`${col} = ?`);
|
|
1210
|
-
values.push(val);
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
if (body.pinned !== void 0) {
|
|
1214
|
-
fields.push("pinned = ?");
|
|
1215
|
-
values.push(body.pinned ? 1 : 0);
|
|
1216
|
-
}
|
|
1217
|
-
if (body.pin_order !== void 0) {
|
|
1218
|
-
fields.push("pin_order = ?");
|
|
1219
|
-
values.push(body.pin_order);
|
|
1220
|
-
}
|
|
1221
|
-
if (body.sort_order !== void 0) {
|
|
1222
|
-
fields.push("sort_order = ?");
|
|
1223
|
-
values.push(body.sort_order);
|
|
1224
|
-
}
|
|
1225
|
-
if (fields.length) {
|
|
1226
|
-
fields.push("updated_at = ?");
|
|
1227
|
-
values.push(Date.now(), params.id);
|
|
1228
|
-
db.prepare(`UPDATE threads SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
1229
|
-
}
|
|
1230
|
-
send(res, 200, { thread: db.prepare("SELECT * FROM threads WHERE id = ?").get(params.id) });
|
|
1231
|
-
}
|
|
1232
|
-
delete(req, res, params) {
|
|
1233
|
-
const db = this.getActiveDb();
|
|
1234
|
-
const thread = db.prepare("SELECT * FROM threads WHERE id = ?").get(params.id);
|
|
1235
|
-
if (!thread) return sendError(res, 404, "Thread not found");
|
|
1236
|
-
db.prepare("DELETE FROM threads WHERE id = ?").run(params.id);
|
|
1237
|
-
const agentMatch = (thread.session_key || "").match(/^agent:([^:]+):/);
|
|
1238
|
-
const sessionsDir = getSessionsDirForAgent(agentMatch?.[1]);
|
|
1239
|
-
let sessionIdToDelete = thread.last_session_id;
|
|
1240
|
-
if (!sessionIdToDelete) {
|
|
1241
|
-
try {
|
|
1242
|
-
sessionIdToDelete = JSON.parse(fs7.readFileSync(path8.join(sessionsDir, "sessions.json"), "utf8"))[thread.session_key]?.sessionId;
|
|
1243
|
-
} catch {
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
cleanGatewaySession(thread.session_key);
|
|
1247
|
-
if (sessionIdToDelete) {
|
|
1248
|
-
try {
|
|
1249
|
-
fs7.unlinkSync(path8.join(sessionsDir, `${sessionIdToDelete}.jsonl`));
|
|
1250
|
-
} catch {
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
try {
|
|
1254
|
-
fs7.rmSync(path8.join(this.uploadsDir, params.id), { recursive: true });
|
|
1255
|
-
} catch {
|
|
1256
|
-
}
|
|
1257
|
-
send(res, 200, { ok: true });
|
|
1258
|
-
}
|
|
1259
|
-
};
|
|
1260
|
-
|
|
1261
|
-
// server/util/context.js
|
|
1262
|
-
import fs8 from "node:fs";
|
|
1263
|
-
import path9 from "node:path";
|
|
1264
|
-
function buildContextPreamble(db, threadId, lastSessionId, sessionKey) {
|
|
1265
|
-
let summary = null;
|
|
1266
|
-
let method = "raw";
|
|
1267
|
-
if (lastSessionId) {
|
|
1268
|
-
const agentMatch = (sessionKey || "").match(/^agent:([^:]+):/);
|
|
1269
|
-
const sessionsDir = getSessionsDirForAgent(agentMatch?.[1]);
|
|
1270
|
-
try {
|
|
1271
|
-
const lines = fs8.readFileSync(path9.join(sessionsDir, `${lastSessionId}.jsonl`), "utf8").split("\n").filter(Boolean);
|
|
1272
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1273
|
-
try {
|
|
1274
|
-
const entry = JSON.parse(lines[i]);
|
|
1275
|
-
if (entry.type === "compaction" && entry.summary) {
|
|
1276
|
-
summary = entry.summary;
|
|
1277
|
-
method = "compaction";
|
|
1278
|
-
break;
|
|
1279
|
-
}
|
|
1280
|
-
} catch {
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
} catch {
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
let preamble = "";
|
|
1287
|
-
if (method === "compaction" && summary) {
|
|
1288
|
-
preamble += "[CONTEXT RECOVERY \u2014 This thread's agent session was reset. Below is a summary of the previous conversation followed by recent messages to restore context.]\n\n";
|
|
1289
|
-
preamble += "[CONVERSATION SUMMARY]\n" + summary + "\n\n";
|
|
1290
|
-
const msgs = db.prepare("SELECT role, content, timestamp FROM messages WHERE thread_id = ? ORDER BY timestamp DESC LIMIT 10").all(threadId).reverse();
|
|
1291
|
-
if (msgs.length) {
|
|
1292
|
-
preamble += "[RECENT MESSAGES]\n";
|
|
1293
|
-
for (const m of msgs) {
|
|
1294
|
-
const ts = new Date(m.timestamp).toISOString().replace("T", " ").slice(0, 16);
|
|
1295
|
-
preamble += `${m.role.charAt(0).toUpperCase() + m.role.slice(1)} (${ts}): ${m.content}
|
|
1296
|
-
`;
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
} else {
|
|
1300
|
-
preamble += "[CONTEXT RECOVERY \u2014 This thread's agent session was reset. Below are recent messages from the previous conversation to restore context.]\n\n";
|
|
1301
|
-
const msgs = db.prepare("SELECT role, content, timestamp FROM messages WHERE thread_id = ? ORDER BY timestamp DESC LIMIT 25").all(threadId).reverse();
|
|
1302
|
-
if (msgs.length) {
|
|
1303
|
-
preamble += "[PREVIOUS MESSAGES]\n";
|
|
1304
|
-
for (const m of msgs) {
|
|
1305
|
-
const ts = new Date(m.timestamp).toISOString().replace("T", " ").slice(0, 16);
|
|
1306
|
-
preamble += `${m.role.charAt(0).toUpperCase() + m.role.slice(1)} (${ts}): ${m.content}
|
|
1307
|
-
`;
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
if (preamble.length > MAX_PREAMBLE_CHARS) preamble = preamble.slice(preamble.length - MAX_PREAMBLE_CHARS);
|
|
1312
|
-
return { preamble, method };
|
|
1313
|
-
}
|
|
1314
|
-
__name(buildContextPreamble, "buildContextPreamble");
|
|
1315
|
-
|
|
1316
|
-
// server/controllers/messages.js
|
|
1317
|
-
var MessageController = class {
|
|
1318
|
-
static {
|
|
1319
|
-
__name(this, "MessageController");
|
|
1320
|
-
}
|
|
1321
|
-
constructor({ getActiveDb, getWorkspaces, broadcast }) {
|
|
1322
|
-
this.getActiveDb = getActiveDb;
|
|
1323
|
-
this.getWorkspaces = getWorkspaces;
|
|
1324
|
-
this.broadcast = broadcast;
|
|
1325
|
-
}
|
|
1326
|
-
getAll(req, res, params, query) {
|
|
1327
|
-
const db = this.getActiveDb();
|
|
1328
|
-
if (!db.prepare("SELECT id FROM threads WHERE id = ?").get(params.id)) return sendError(res, 404, "Thread not found");
|
|
1329
|
-
const limit = Math.min(parseInt(query.limit || "100", 10), 500);
|
|
1330
|
-
const before = query.before ? parseInt(query.before, 10) : null;
|
|
1331
|
-
const after = query.after ? parseInt(query.after, 10) : null;
|
|
1332
|
-
let sql = "SELECT * FROM messages WHERE thread_id = ?";
|
|
1333
|
-
const sqlParams = [params.id];
|
|
1334
|
-
if (before) {
|
|
1335
|
-
sql += " AND timestamp < ?";
|
|
1336
|
-
sqlParams.push(before);
|
|
1337
|
-
}
|
|
1338
|
-
if (after) {
|
|
1339
|
-
sql += " AND timestamp > ?";
|
|
1340
|
-
sqlParams.push(after);
|
|
1341
|
-
}
|
|
1342
|
-
const total = db.prepare(sql.replace("SELECT *", "SELECT COUNT(*) as c")).get(...sqlParams).c;
|
|
1343
|
-
const rows = db.prepare(sql + " ORDER BY timestamp DESC LIMIT ?").all(...sqlParams, limit + 1);
|
|
1344
|
-
const messages = rows.slice(0, limit).reverse();
|
|
1345
|
-
for (const m of messages) {
|
|
1346
|
-
if (m.metadata) {
|
|
1347
|
-
try {
|
|
1348
|
-
m.metadata = JSON.parse(m.metadata);
|
|
1349
|
-
} catch {
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
send(res, 200, { messages, hasMore: rows.length > limit });
|
|
1354
|
-
}
|
|
1355
|
-
async create(req, res, params) {
|
|
1356
|
-
const body = await parseBody(req);
|
|
1357
|
-
const db = this.getActiveDb();
|
|
1358
|
-
if (!db.prepare("SELECT id FROM threads WHERE id = ?").get(params.id)) return sendError(res, 404, "Thread not found");
|
|
1359
|
-
if (!body.id || !body.role || body.content === void 0 || !body.timestamp) return sendError(res, 400, "Required: id, role, content, timestamp");
|
|
1360
|
-
const metadata = body.metadata ? JSON.stringify(body.metadata) : null;
|
|
1361
|
-
const existing = db.prepare("SELECT id, status, metadata FROM messages WHERE id = ?").get(body.id);
|
|
1362
|
-
if (existing) {
|
|
1363
|
-
if (body.status && body.status !== existing.status) {
|
|
1364
|
-
db.prepare("UPDATE messages SET status = ?, content = ?, metadata = ? WHERE id = ?").run(body.status, body.content, metadata || existing.metadata, body.id);
|
|
1365
|
-
}
|
|
1366
|
-
} else {
|
|
1367
|
-
db.prepare("INSERT INTO messages (id, thread_id, role, content, status, metadata, seq, timestamp, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)").run(body.id, params.id, body.role, body.content, body.status || "sent", metadata, body.seq || null, body.timestamp, Date.now());
|
|
1368
|
-
db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(Date.now(), params.id);
|
|
1369
|
-
if (body.role === "user" && body.content) {
|
|
1370
|
-
const thread = db.prepare("SELECT title FROM threads WHERE id = ?").get(params.id);
|
|
1371
|
-
if (thread?.title === "New chat") {
|
|
1372
|
-
const title = body.content.replace(/\n.*/s, "").slice(0, 40).trim() + (body.content.length > 40 ? "..." : "");
|
|
1373
|
-
if (title) {
|
|
1374
|
-
db.prepare("UPDATE threads SET title = ? WHERE id = ?").run(title, params.id);
|
|
1375
|
-
this.broadcast(JSON.stringify({ type: "clawchats", event: "thread-title-updated", threadId: params.id, workspace: this.getWorkspaces().active, title }));
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
const message = db.prepare("SELECT * FROM messages WHERE id = ?").get(body.id);
|
|
1381
|
-
if (message?.metadata) {
|
|
1382
|
-
try {
|
|
1383
|
-
message.metadata = JSON.parse(message.metadata);
|
|
1384
|
-
} catch {
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
send(res, existing ? 200 : 201, { message });
|
|
1388
|
-
}
|
|
1389
|
-
delete(req, res, params) {
|
|
1390
|
-
const db = this.getActiveDb();
|
|
1391
|
-
if (!db.prepare("SELECT id FROM messages WHERE id = ? AND thread_id = ?").get(params.messageId, params.id)) return sendError(res, 404, "Message not found");
|
|
1392
|
-
db.prepare("DELETE FROM messages WHERE id = ?").run(params.messageId);
|
|
1393
|
-
send(res, 200, { ok: true });
|
|
1394
|
-
}
|
|
1395
|
-
contextFill(req, res, params) {
|
|
1396
|
-
const db = this.getActiveDb();
|
|
1397
|
-
const thread = db.prepare("SELECT * FROM threads WHERE id = ?").get(params.id);
|
|
1398
|
-
if (!thread) return sendError(res, 404, "Thread not found");
|
|
1399
|
-
send(res, 200, buildContextPreamble(db, params.id, thread.last_session_id, thread.session_key));
|
|
1400
|
-
}
|
|
1401
|
-
search(req, res, params, query) {
|
|
1402
|
-
const q = query.q || "";
|
|
1403
|
-
if (!q) return send(res, 200, { results: [], total: 0 });
|
|
1404
|
-
const db = this.getActiveDb();
|
|
1405
|
-
const page = parseInt(query.page || "1", 10);
|
|
1406
|
-
const limit = Math.min(parseInt(query.limit || "20", 10), 100);
|
|
1407
|
-
const offset = (page - 1) * limit;
|
|
1408
|
-
try {
|
|
1409
|
-
const results = db.prepare(`SELECT m.id as messageId, m.thread_id as threadId, t.title as threadTitle, m.role, snippet(messages_fts, 0, '<mark>', '</mark>', '...', 40) as content, m.timestamp FROM messages_fts JOIN messages m ON messages_fts.rowid = m.rowid JOIN threads t ON m.thread_id = t.id WHERE messages_fts MATCH ? ORDER BY rank LIMIT ? OFFSET ?`).all(q, limit, offset);
|
|
1410
|
-
const total = db.prepare(`SELECT COUNT(*) as c FROM messages_fts WHERE messages_fts MATCH ?`).get(q).c;
|
|
1411
|
-
send(res, 200, { results, total });
|
|
1412
|
-
} catch {
|
|
1413
|
-
send(res, 200, { results: [], total: 0 });
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
export(req, res) {
|
|
1417
|
-
const db = this.getActiveDb();
|
|
1418
|
-
const ws = this.getWorkspaces();
|
|
1419
|
-
const threads = db.prepare("SELECT * FROM threads ORDER BY updated_at DESC").all();
|
|
1420
|
-
send(res, 200, {
|
|
1421
|
-
workspace: ws.active,
|
|
1422
|
-
exportedAt: Date.now(),
|
|
1423
|
-
threads: threads.map((t) => {
|
|
1424
|
-
const messages = db.prepare("SELECT * FROM messages WHERE thread_id = ? ORDER BY timestamp ASC").all(t.id);
|
|
1425
|
-
for (const m of messages) {
|
|
1426
|
-
if (m.metadata) {
|
|
1427
|
-
try {
|
|
1428
|
-
m.metadata = JSON.parse(m.metadata);
|
|
1429
|
-
} catch {
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
return { ...t, messages };
|
|
1434
|
-
})
|
|
1435
|
-
});
|
|
1436
|
-
}
|
|
1437
|
-
async import(req, res) {
|
|
1438
|
-
const body = await parseBody(req);
|
|
1439
|
-
const db = this.getActiveDb();
|
|
1440
|
-
const ws = this.getWorkspaces();
|
|
1441
|
-
if (!body.threads || !Array.isArray(body.threads)) return sendError(res, 400, "Expected { threads: [...] }");
|
|
1442
|
-
let threadsImported = 0, messagesImported = 0;
|
|
1443
|
-
const insertThread = db.prepare("INSERT OR IGNORE INTO threads (id, session_key, title, pinned, pin_order, model, last_session_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
1444
|
-
const insertMsg = db.prepare("INSERT OR IGNORE INTO messages (id, thread_id, role, content, status, metadata, seq, timestamp, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
1445
|
-
db.exec("BEGIN");
|
|
1446
|
-
try {
|
|
1447
|
-
for (const t of body.threads) {
|
|
1448
|
-
if (!t.id) continue;
|
|
1449
|
-
const sessionKey = t.session_key || `agent:main:${ws.active}:chat:${t.id}`;
|
|
1450
|
-
if (insertThread.run(t.id, sessionKey, t.title || "Imported chat", t.pinned || 0, t.pin_order || 0, t.model || null, t.last_session_id || null, t.created_at || Date.now(), t.updated_at || Date.now()).changes > 0) threadsImported++;
|
|
1451
|
-
for (const m of t.messages || []) {
|
|
1452
|
-
if (!m.id || !m.role) continue;
|
|
1453
|
-
const meta = m.metadata ? typeof m.metadata === "string" ? m.metadata : JSON.stringify(m.metadata) : null;
|
|
1454
|
-
if (insertMsg.run(m.id, t.id, m.role, m.content || "", m.status || "sent", meta, m.seq || null, m.timestamp || Date.now(), m.created_at || Date.now()).changes > 0) messagesImported++;
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
db.exec("COMMIT");
|
|
1458
|
-
} catch (e) {
|
|
1459
|
-
db.exec("ROLLBACK");
|
|
1460
|
-
throw e;
|
|
1461
|
-
}
|
|
1462
|
-
send(res, 200, { ok: true, threadsImported, messagesImported });
|
|
1463
|
-
}
|
|
1464
|
-
};
|
|
1465
|
-
|
|
1466
|
-
// server/controllers/files.js
|
|
1467
|
-
import fs9 from "node:fs";
|
|
1468
|
-
import path10 from "node:path";
|
|
1469
|
-
|
|
1470
|
-
// server/util/multipart.js
|
|
1471
|
-
function parseMultipart(req) {
|
|
1472
|
-
return new Promise((resolve, reject) => {
|
|
1473
|
-
const contentType = req.headers["content-type"] || "";
|
|
1474
|
-
const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/);
|
|
1475
|
-
if (!boundaryMatch) return reject(new Error("No boundary in content-type"));
|
|
1476
|
-
const boundary = boundaryMatch[1] || boundaryMatch[2];
|
|
1477
|
-
const chunks = [];
|
|
1478
|
-
req.on("data", (c) => chunks.push(c));
|
|
1479
|
-
req.on("end", () => {
|
|
1480
|
-
const buf = Buffer.concat(chunks);
|
|
1481
|
-
const files = [];
|
|
1482
|
-
const delimiter = Buffer.from(`--${boundary}`);
|
|
1483
|
-
let pos = 0;
|
|
1484
|
-
while (pos < buf.length) {
|
|
1485
|
-
const start = buf.indexOf(delimiter, pos);
|
|
1486
|
-
if (start === -1) break;
|
|
1487
|
-
const nextStart = buf.indexOf(delimiter, start + delimiter.length);
|
|
1488
|
-
if (nextStart === -1) break;
|
|
1489
|
-
const part = buf.subarray(start + delimiter.length, nextStart);
|
|
1490
|
-
const headerEnd = part.indexOf("\r\n\r\n");
|
|
1491
|
-
if (headerEnd === -1) {
|
|
1492
|
-
pos = nextStart;
|
|
1493
|
-
continue;
|
|
1494
|
-
}
|
|
1495
|
-
const headerStr = part.subarray(0, headerEnd).toString();
|
|
1496
|
-
let body = part.subarray(headerEnd + 4);
|
|
1497
|
-
if (body.length >= 2 && body[body.length - 2] === 13 && body[body.length - 1] === 10) {
|
|
1498
|
-
body = body.subarray(0, body.length - 2);
|
|
1499
|
-
}
|
|
1500
|
-
const filenameMatch = headerStr.match(/filename="([^"]+)"/);
|
|
1501
|
-
const ctMatch = headerStr.match(/Content-Type:\s*(\S+)/i);
|
|
1502
|
-
if (filenameMatch) {
|
|
1503
|
-
files.push({
|
|
1504
|
-
filename: filenameMatch[1],
|
|
1505
|
-
mimeType: ctMatch ? ctMatch[1] : "application/octet-stream",
|
|
1506
|
-
data: body
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
pos = nextStart;
|
|
1510
|
-
}
|
|
1511
|
-
resolve(files);
|
|
1512
|
-
});
|
|
1513
|
-
req.on("error", reject);
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
1516
|
-
__name(parseMultipart, "parseMultipart");
|
|
1517
|
-
|
|
1518
|
-
// server/controllers/files.js
|
|
1519
|
-
var FileController = class {
|
|
1520
|
-
static {
|
|
1521
|
-
__name(this, "FileController");
|
|
1522
|
-
}
|
|
1523
|
-
constructor({ getActiveDb, getWorkspaces, uploadsDir, intelligenceDir }) {
|
|
1524
|
-
this.getActiveDb = getActiveDb;
|
|
1525
|
-
this.getWorkspaces = getWorkspaces;
|
|
1526
|
-
this.uploadsDir = uploadsDir;
|
|
1527
|
-
this.intelligenceDir = intelligenceDir;
|
|
1528
|
-
}
|
|
1529
|
-
async upload(req, res, params) {
|
|
1530
|
-
if (!this.getActiveDb().prepare("SELECT id FROM threads WHERE id = ?").get(params.id)) return sendError(res, 404, "Thread not found");
|
|
1531
|
-
const files = await parseMultipart(req);
|
|
1532
|
-
const dir = path10.join(this.uploadsDir, params.id);
|
|
1533
|
-
fs9.mkdirSync(dir, { recursive: true });
|
|
1534
|
-
const savedFiles = [];
|
|
1535
|
-
for (const file of files) {
|
|
1536
|
-
const fileId = uuid();
|
|
1537
|
-
const ext = path10.extname(file.filename) || "";
|
|
1538
|
-
fs9.writeFileSync(path10.join(dir, fileId + ext), file.data);
|
|
1539
|
-
savedFiles.push({ id: fileId, filename: file.filename, path: `/api/uploads/${params.id}/${fileId}${ext}`, mimeType: file.mimeType, size: file.data.length });
|
|
1540
|
-
}
|
|
1541
|
-
send(res, 200, { files: savedFiles });
|
|
1542
|
-
}
|
|
1543
|
-
serveUpload(req, res, params) {
|
|
1544
|
-
const base = path10.join(this.uploadsDir, params.threadId, params.fileId);
|
|
1545
|
-
let resolved = base;
|
|
1546
|
-
if (!fs9.existsSync(resolved)) {
|
|
1547
|
-
try {
|
|
1548
|
-
const match = fs9.readdirSync(path10.join(this.uploadsDir, params.threadId)).find((e) => e.startsWith(params.fileId.replace(/\.[^.]+$/, "")));
|
|
1549
|
-
if (match) resolved = path10.join(this.uploadsDir, params.threadId, match);
|
|
1550
|
-
} catch {
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
if (!fs9.existsSync(resolved)) return sendError(res, 404, "File not found");
|
|
1554
|
-
const MIME = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".pdf": "application/pdf", ".txt": "text/plain", ".json": "application/json" };
|
|
1555
|
-
const stat = fs9.statSync(resolved);
|
|
1556
|
-
res.writeHead(200, { "Content-Type": MIME[path10.extname(resolved).toLowerCase()] || "application/octet-stream", "Content-Length": stat.size, "Cache-Control": "public, max-age=86400", "Access-Control-Allow-Origin": "*" });
|
|
1557
|
-
fs9.createReadStream(resolved).pipe(res);
|
|
1558
|
-
}
|
|
1559
|
-
_intelligencePath(threadId) {
|
|
1560
|
-
return path10.join(this.intelligenceDir, this.getWorkspaces().active, `${threadId}.json`);
|
|
1561
|
-
}
|
|
1562
|
-
getIntelligence(req, res, params) {
|
|
1563
|
-
const filePath = this._intelligencePath(params.id);
|
|
1564
|
-
if (!fs9.existsSync(filePath)) return send(res, 200, { versions: [], currentVersion: -1 });
|
|
1565
|
-
try {
|
|
1566
|
-
return send(res, 200, JSON.parse(fs9.readFileSync(filePath, "utf8")));
|
|
1567
|
-
} catch {
|
|
1568
|
-
return send(res, 200, { versions: [], currentVersion: -1 });
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
async saveIntelligence(req, res, params) {
|
|
1572
|
-
const body = await parseBody(req);
|
|
1573
|
-
const filePath = this._intelligencePath(params.id);
|
|
1574
|
-
fs9.mkdirSync(path10.dirname(filePath), { recursive: true });
|
|
1575
|
-
const data = { versions: body.versions || [], currentVersion: body.currentVersion ?? -1, updatedAt: Date.now() };
|
|
1576
|
-
fs9.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
1577
|
-
send(res, 200, data);
|
|
1578
|
-
}
|
|
1579
|
-
};
|
|
1580
|
-
|
|
1581
|
-
// server/controllers/memory.js
|
|
1582
|
-
import fs10 from "node:fs";
|
|
1583
|
-
import path11 from "node:path";
|
|
1584
|
-
var MemoryController = class {
|
|
1585
|
-
static {
|
|
1586
|
-
__name(this, "MemoryController");
|
|
1587
|
-
}
|
|
1588
|
-
constructor({ memoryProvider, memoryFilesDir, memoryConfig }) {
|
|
1589
|
-
this.provider = memoryProvider;
|
|
1590
|
-
this.filesDir = memoryFilesDir;
|
|
1591
|
-
this.config = memoryConfig;
|
|
1592
|
-
}
|
|
1593
|
-
async list(req, res, query) {
|
|
1594
|
-
const limit = Math.min(parseInt(query.limit) || 20, 100);
|
|
1595
|
-
try {
|
|
1596
|
-
send(res, 200, await this.provider.list(limit, query.offset || null));
|
|
1597
|
-
} catch (err) {
|
|
1598
|
-
send(res, 502, { error: `Failed to reach ${this.provider.name}`, detail: err.message });
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
async search(req, res, query) {
|
|
1602
|
-
const q = (query.query || "").toLowerCase().trim();
|
|
1603
|
-
if (!q) return send(res, 400, { error: "Missing query parameter" });
|
|
1604
|
-
try {
|
|
1605
|
-
send(res, 200, await this.provider.search(q));
|
|
1606
|
-
} catch (err) {
|
|
1607
|
-
send(res, 502, { error: `Failed to reach ${this.provider.name}`, detail: err.message });
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
files(req, res, query) {
|
|
1611
|
-
const q = (query.query || "").toLowerCase().trim();
|
|
1612
|
-
const memories = this._parseFiles();
|
|
1613
|
-
const filtered = q ? memories.filter((m) => m.data.toLowerCase().includes(q) || m.title.toLowerCase().includes(q)) : memories;
|
|
1614
|
-
filtered.sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || ""));
|
|
1615
|
-
send(res, 200, { memories: filtered });
|
|
1616
|
-
}
|
|
1617
|
-
_parseFiles() {
|
|
1618
|
-
const memories = [];
|
|
1619
|
-
const scanDir = /* @__PURE__ */ __name((dir, prefix = "") => {
|
|
1620
|
-
let entries;
|
|
1621
|
-
try {
|
|
1622
|
-
entries = fs10.readdirSync(dir);
|
|
1623
|
-
} catch {
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
for (const entry of entries) {
|
|
1627
|
-
const fullPath = path11.join(dir, entry);
|
|
1628
|
-
const stat = (() => {
|
|
1629
|
-
try {
|
|
1630
|
-
return fs10.statSync(fullPath);
|
|
1631
|
-
} catch {
|
|
1632
|
-
return null;
|
|
1633
|
-
}
|
|
1634
|
-
})();
|
|
1635
|
-
if (!stat) continue;
|
|
1636
|
-
if (stat.isDirectory() && !prefix) {
|
|
1637
|
-
scanDir(fullPath, entry + "/");
|
|
1638
|
-
continue;
|
|
1639
|
-
}
|
|
1640
|
-
if (!entry.endsWith(".md") || !stat.isFile()) continue;
|
|
1641
|
-
const content = fs10.readFileSync(fullPath, "utf8");
|
|
1642
|
-
const basename = entry.replace(/\.md$/, "");
|
|
1643
|
-
const dateMatch = basename.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
1644
|
-
if (prefix) {
|
|
1645
|
-
memories.push({ id: `file:${prefix + basename}`, source: "file", file: prefix + basename, title: basename, data: content.trim(), createdAt: stat.mtime.toISOString() });
|
|
1646
|
-
} else {
|
|
1647
|
-
for (const section of content.split(/^(?=## )/m)) {
|
|
1648
|
-
const trimmed = section.trim();
|
|
1649
|
-
if (!trimmed) continue;
|
|
1650
|
-
const headingMatch = trimmed.match(/^##\s+(.+)/);
|
|
1651
|
-
const heading = headingMatch ? headingMatch[1].trim() : null;
|
|
1652
|
-
const body = headingMatch ? trimmed.slice(trimmed.indexOf("\n") + 1).trim() : trimmed;
|
|
1653
|
-
if (!heading && body.match(/^#\s+/) && body.split("\n").length <= 2) continue;
|
|
1654
|
-
const title = heading || basename;
|
|
1655
|
-
memories.push({ id: `file:${basename}:${title}`, source: "file", file: basename, title, data: heading ? `**${title}**
|
|
1656
|
-
${body}` : body, createdAt: dateMatch ? `${dateMatch[1]}T00:00:00Z` : stat.mtime.toISOString() });
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
}, "scanDir");
|
|
1661
|
-
scanDir(this.filesDir);
|
|
1662
|
-
return memories;
|
|
1663
|
-
}
|
|
1664
|
-
async update(req, res, params) {
|
|
1665
|
-
try {
|
|
1666
|
-
const chunks = [];
|
|
1667
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
1668
|
-
const { data } = JSON.parse(Buffer.concat(chunks).toString());
|
|
1669
|
-
if (!(data || "").trim()) return send(res, 400, { error: "Missing data field" });
|
|
1670
|
-
send(res, 200, { ok: true, result: await this.provider.update(params.id, data.trim()) });
|
|
1671
|
-
} catch (err) {
|
|
1672
|
-
send(res, 502, { error: "Failed to update memory", detail: err.message });
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
async delete(req, res, params) {
|
|
1676
|
-
try {
|
|
1677
|
-
send(res, 200, { ok: true, result: await this.provider.delete(params.id) });
|
|
1678
|
-
} catch (err) {
|
|
1679
|
-
send(res, 502, { error: `Failed to reach ${this.provider.name}`, detail: err.message });
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
async status(req, res) {
|
|
1683
|
-
const status = await this.provider.status();
|
|
1684
|
-
send(res, 200, { provider: this.provider.name, host: this.config.host, port: this.config.port, collection: this.config.collection, backend: status, memoryFilesDir: this.filesDir, memoryFilesDirExists: fs10.existsSync(this.filesDir) });
|
|
1685
|
-
}
|
|
1686
|
-
};
|
|
1687
|
-
|
|
1688
|
-
// server/controllers/filesystem.js
|
|
1689
|
-
import fs11 from "node:fs";
|
|
1690
|
-
import path12 from "node:path";
|
|
1691
|
-
import os3 from "node:os";
|
|
1692
|
-
var HOME2 = os3.homedir();
|
|
1693
|
-
var ALLOWED_FILE_DIRS = [HOME2, "/tmp"];
|
|
1694
|
-
function handleServeFile(req, res, query, memoryConfig) {
|
|
1695
|
-
const filePath = query.path;
|
|
1696
|
-
if (!filePath) return sendError(res, 400, "Missing path parameter");
|
|
1697
|
-
const resolved = filePath.startsWith("./") || filePath.startsWith("../") ? path12.resolve(memoryConfig.workspaceDir, filePath) : path12.resolve(filePath);
|
|
1698
|
-
if (!ALLOWED_FILE_DIRS.some((dir) => resolved.startsWith(dir + "/") || resolved === dir)) return sendError(res, 403, "Access denied: path not in allowed directories");
|
|
1699
|
-
if (!fs11.existsSync(resolved) || !fs11.statSync(resolved).isFile()) return sendError(res, 404, "File not found");
|
|
1700
|
-
const MIME = {
|
|
1701
|
-
".png": "image/png",
|
|
1702
|
-
".jpg": "image/jpeg",
|
|
1703
|
-
".jpeg": "image/jpeg",
|
|
1704
|
-
".gif": "image/gif",
|
|
1705
|
-
".webp": "image/webp",
|
|
1706
|
-
".svg": "image/svg+xml",
|
|
1707
|
-
".pdf": "application/pdf",
|
|
1708
|
-
".txt": "text/plain",
|
|
1709
|
-
".json": "application/json",
|
|
1710
|
-
".md": "text/markdown",
|
|
1711
|
-
".csv": "text/csv",
|
|
1712
|
-
".xml": "text/xml",
|
|
1713
|
-
".html": "text/html",
|
|
1714
|
-
".css": "text/css",
|
|
1715
|
-
".js": "text/javascript",
|
|
1716
|
-
".py": "text/x-python",
|
|
1717
|
-
".sh": "text/x-shellscript",
|
|
1718
|
-
".yaml": "text/yaml",
|
|
1719
|
-
".yml": "text/yaml",
|
|
1720
|
-
".toml": "text/toml",
|
|
1721
|
-
".zip": "application/zip",
|
|
1722
|
-
".gz": "application/gzip",
|
|
1723
|
-
".tar": "application/x-tar",
|
|
1724
|
-
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1725
|
-
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1726
|
-
".mp3": "audio/mpeg",
|
|
1727
|
-
".wav": "audio/wav",
|
|
1728
|
-
".ogg": "audio/ogg",
|
|
1729
|
-
".mp4": "video/mp4",
|
|
1730
|
-
".webm": "video/webm"
|
|
1731
|
-
};
|
|
1732
|
-
const stat = fs11.statSync(resolved);
|
|
1733
|
-
res.writeHead(200, { "Content-Type": MIME[path12.extname(resolved).toLowerCase()] || "application/octet-stream", "Content-Length": stat.size, "Cache-Control": "public, max-age=86400", "Access-Control-Allow-Origin": "*" });
|
|
1734
|
-
fs11.createReadStream(resolved).pipe(res);
|
|
1735
|
-
}
|
|
1736
|
-
__name(handleServeFile, "handleServeFile");
|
|
1737
|
-
function handleWorkspaceList(req, res, query) {
|
|
1738
|
-
const reqPath = query.path || "~/.openclaw/workspace";
|
|
1739
|
-
const depth = parseInt(query.depth || "2", 10);
|
|
1740
|
-
const showHidden = query.hidden === "1" || query.hidden === "true";
|
|
1741
|
-
const resolved = path12.resolve(reqPath.replace(/^~/, HOME2));
|
|
1742
|
-
if (!resolved.startsWith(HOME2)) return sendError(res, 403, "Access denied");
|
|
1743
|
-
if (!fs11.existsSync(resolved)) return sendError(res, 404, "Path not found");
|
|
1744
|
-
const files = [{ path: resolved + "/", type: "dir", name: path12.basename(resolved), size: 0 }];
|
|
1745
|
-
const walk = /* @__PURE__ */ __name((dir, d) => {
|
|
1746
|
-
if (d > depth) return;
|
|
1747
|
-
try {
|
|
1748
|
-
for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
|
|
1749
|
-
if (entry.name.startsWith(".") && entry.name !== ".openclaw" && !showHidden) continue;
|
|
1750
|
-
if (entry.name === "node_modules") continue;
|
|
1751
|
-
const fullPath = path12.join(dir, entry.name);
|
|
1752
|
-
const isDir = entry.isDirectory();
|
|
1753
|
-
files.push({ path: fullPath + (isDir ? "/" : ""), type: isDir ? "dir" : "file", name: entry.name, size: isDir ? 0 : (() => {
|
|
1754
|
-
try {
|
|
1755
|
-
return fs11.statSync(fullPath).size;
|
|
1756
|
-
} catch {
|
|
1757
|
-
return 0;
|
|
1758
|
-
}
|
|
1759
|
-
})() });
|
|
1760
|
-
if (isDir) walk(fullPath, d + 1);
|
|
1761
|
-
}
|
|
1762
|
-
} catch {
|
|
1763
|
-
}
|
|
1764
|
-
}, "walk");
|
|
1765
|
-
walk(resolved, 1);
|
|
1766
|
-
send(res, 200, { files, cwd: resolved });
|
|
1767
|
-
}
|
|
1768
|
-
__name(handleWorkspaceList, "handleWorkspaceList");
|
|
1769
|
-
function handleWorkspaceFileRead(req, res, query) {
|
|
1770
|
-
const filePath = query.path;
|
|
1771
|
-
if (!filePath) return sendError(res, 400, "Missing path parameter");
|
|
1772
|
-
const resolved = path12.resolve(filePath.replace(/^~/, HOME2));
|
|
1773
|
-
if (!resolved.startsWith(HOME2)) return sendError(res, 403, "Access denied");
|
|
1774
|
-
if (!fs11.existsSync(resolved) || !fs11.statSync(resolved).isFile()) return sendError(res, 404, "File not found");
|
|
1775
|
-
const stat = fs11.statSync(resolved);
|
|
1776
|
-
const ext = path12.extname(resolved).toLowerCase().slice(1);
|
|
1777
|
-
const binaryMime = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml", bmp: "image/bmp", ico: "image/x-icon", pdf: "application/pdf", mp3: "audio/mpeg", mp4: "video/mp4", wav: "audio/wav", ogg: "audio/ogg", webm: "video/webm" };
|
|
1778
|
-
const mime = binaryMime[ext];
|
|
1779
|
-
if (mime) {
|
|
1780
|
-
if (stat.size > 20 * 1024 * 1024) return sendError(res, 413, "File too large (max 20MB)");
|
|
1781
|
-
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "private, max-age=60" });
|
|
1782
|
-
res.end(fs11.readFileSync(resolved));
|
|
1783
|
-
} else {
|
|
1784
|
-
if (stat.size > 1024 * 1024) return sendError(res, 413, "File too large (max 1MB)");
|
|
1785
|
-
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1786
|
-
res.end(fs11.readFileSync(resolved, "utf8"));
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
__name(handleWorkspaceFileRead, "handleWorkspaceFileRead");
|
|
1790
|
-
async function handleWorkspaceFileWrite(req, res, query) {
|
|
1791
|
-
const filePath = query.path;
|
|
1792
|
-
if (!filePath) return sendError(res, 400, "Missing path parameter");
|
|
1793
|
-
const resolved = path12.resolve(filePath.replace(/^~/, HOME2));
|
|
1794
|
-
if (!resolved.startsWith(HOME2)) return sendError(res, 403, "Can only write to workspace directory");
|
|
1795
|
-
const chunks = [];
|
|
1796
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
1797
|
-
const dir = path12.dirname(resolved);
|
|
1798
|
-
if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
|
|
1799
|
-
fs11.writeFileSync(resolved, Buffer.concat(chunks).toString("utf8"), "utf8");
|
|
1800
|
-
send(res, 200, { ok: true });
|
|
1801
|
-
}
|
|
1802
|
-
__name(handleWorkspaceFileWrite, "handleWorkspaceFileWrite");
|
|
1803
|
-
function handleWorkspaceFileDelete(req, res, query) {
|
|
1804
|
-
const filePath = query.path;
|
|
1805
|
-
if (!filePath) return sendError(res, 400, "Missing path parameter");
|
|
1806
|
-
const resolved = path12.resolve(filePath.replace(/^~/, HOME2));
|
|
1807
|
-
if (!resolved.startsWith(HOME2)) return sendError(res, 403, "Access denied");
|
|
1808
|
-
if (!fs11.existsSync(resolved)) return sendError(res, 404, "Path not found");
|
|
1809
|
-
try {
|
|
1810
|
-
const stat = fs11.statSync(resolved);
|
|
1811
|
-
if (stat.isDirectory()) {
|
|
1812
|
-
fs11.rmSync(resolved, { recursive: true, force: true });
|
|
1813
|
-
send(res, 200, { ok: true, type: "dir" });
|
|
1814
|
-
} else {
|
|
1815
|
-
fs11.unlinkSync(resolved);
|
|
1816
|
-
send(res, 200, { ok: true, type: "file" });
|
|
1817
|
-
}
|
|
1818
|
-
} catch (err) {
|
|
1819
|
-
sendError(res, 500, "Delete failed: " + err.message);
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
__name(handleWorkspaceFileDelete, "handleWorkspaceFileDelete");
|
|
1823
|
-
async function handleWorkspaceUpload(req, res, query) {
|
|
1824
|
-
const targetDir = query.path;
|
|
1825
|
-
if (!targetDir) return sendError(res, 400, "Missing path parameter");
|
|
1826
|
-
const resolved = path12.resolve(targetDir.replace(/^~/, HOME2));
|
|
1827
|
-
if (!resolved.startsWith(HOME2)) return sendError(res, 403, "Access denied");
|
|
1828
|
-
if (!fs11.existsSync(resolved) || !fs11.statSync(resolved).isDirectory()) return sendError(res, 404, "Target directory not found");
|
|
1829
|
-
if (!(req.headers["content-type"] || "").includes("multipart/form-data")) return sendError(res, 400, "Expected multipart/form-data");
|
|
1830
|
-
let files;
|
|
1831
|
-
try {
|
|
1832
|
-
files = await parseMultipart(req);
|
|
1833
|
-
} catch (err) {
|
|
1834
|
-
return sendError(res, 400, "Invalid multipart data: " + err.message);
|
|
1835
|
-
}
|
|
1836
|
-
const uploaded = [];
|
|
1837
|
-
for (const { filename, data } of files) {
|
|
1838
|
-
if (!filename || !data.length) continue;
|
|
1839
|
-
const safeName = path12.basename(filename);
|
|
1840
|
-
let finalPath = path12.join(resolved, safeName);
|
|
1841
|
-
let counter = 1;
|
|
1842
|
-
while (fs11.existsSync(finalPath)) {
|
|
1843
|
-
const ext = path12.extname(safeName);
|
|
1844
|
-
finalPath = path12.join(resolved, `${path12.basename(safeName, ext)} (${counter++})${ext}`);
|
|
1845
|
-
}
|
|
1846
|
-
fs11.writeFileSync(finalPath, data);
|
|
1847
|
-
uploaded.push({ name: path12.basename(finalPath), size: data.length });
|
|
1848
|
-
}
|
|
1849
|
-
send(res, 200, { ok: true, uploaded });
|
|
1850
|
-
}
|
|
1851
|
-
__name(handleWorkspaceUpload, "handleWorkspaceUpload");
|
|
1852
|
-
|
|
1853
|
-
// server/controllers/transcribe.js
|
|
1854
|
-
import fs12 from "node:fs";
|
|
1855
|
-
import path13 from "node:path";
|
|
1856
|
-
import os4 from "node:os";
|
|
1857
|
-
async function handleTranscribe(req, res) {
|
|
1858
|
-
try {
|
|
1859
|
-
const chunks = [];
|
|
1860
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
1861
|
-
const audioBuffer = Buffer.concat(chunks);
|
|
1862
|
-
if (audioBuffer.length === 0) return send(res, 400, { error: "No audio data" });
|
|
1863
|
-
if (audioBuffer.length > 25 * 1024 * 1024) return send(res, 400, { error: "Audio too large (max 25MB)" });
|
|
1864
|
-
let apiKey;
|
|
1865
|
-
try {
|
|
1866
|
-
const ocConfig = JSON.parse(fs12.readFileSync(path13.join(os4.homedir(), ".openclaw", "openclaw.json"), "utf8"));
|
|
1867
|
-
apiKey = ocConfig?.skills?.entries?.["openai-whisper-api"]?.apiKey;
|
|
1868
|
-
} catch {
|
|
1869
|
-
}
|
|
1870
|
-
if (!apiKey) apiKey = process.env.OPENAI_API_KEY;
|
|
1871
|
-
if (!apiKey) return send(res, 500, { error: "No OpenAI API key configured" });
|
|
1872
|
-
const contentType = req.headers["content-type"] || "audio/webm";
|
|
1873
|
-
const ext = contentType.includes("wav") ? "wav" : contentType.includes("mp4") || contentType.includes("m4a") ? "m4a" : contentType.includes("ogg") ? "ogg" : "webm";
|
|
1874
|
-
const boundary = "----WhisperBoundary" + Date.now();
|
|
1875
|
-
const body = Buffer.concat([
|
|
1876
|
-
`--${boundary}\r
|
|
1877
|
-
Content-Disposition: form-data; name="file"; filename="audio.${ext}"\r
|
|
1878
|
-
Content-Type: ${contentType}\r
|
|
1879
|
-
\r
|
|
1880
|
-
`,
|
|
1881
|
-
audioBuffer,
|
|
1882
|
-
`\r
|
|
1883
|
-
--${boundary}\r
|
|
1884
|
-
Content-Disposition: form-data; name="model"\r
|
|
1885
|
-
\r
|
|
1886
|
-
whisper-1\r
|
|
1887
|
-
`,
|
|
1888
|
-
`--${boundary}\r
|
|
1889
|
-
Content-Disposition: form-data; name="response_format"\r
|
|
1890
|
-
\r
|
|
1891
|
-
json\r
|
|
1892
|
-
`,
|
|
1893
|
-
`--${boundary}--\r
|
|
1894
|
-
`
|
|
1895
|
-
].map((p) => typeof p === "string" ? Buffer.from(p) : p));
|
|
1896
|
-
const resp = await fetch("https://api.openai.com/v1/audio/transcriptions", {
|
|
1897
|
-
method: "POST",
|
|
1898
|
-
headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": `multipart/form-data; boundary=${boundary}` },
|
|
1899
|
-
body
|
|
1900
|
-
});
|
|
1901
|
-
if (!resp.ok) {
|
|
1902
|
-
console.error("Whisper API error:", resp.status, await resp.text());
|
|
1903
|
-
return send(res, 502, { error: `Whisper API error: ${resp.status}` });
|
|
1904
|
-
}
|
|
1905
|
-
const result = await resp.json();
|
|
1906
|
-
return send(res, 200, { text: result.text || "" });
|
|
1907
|
-
} catch (err) {
|
|
1908
|
-
console.error("Transcribe error:", err);
|
|
1909
|
-
return send(res, 500, { error: err.message });
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
__name(handleTranscribe, "handleTranscribe");
|
|
1913
|
-
|
|
1914
|
-
// server/index.js
|
|
1915
|
-
var HOME3 = os5.homedir();
|
|
1916
|
-
var PORT = parseInt(process.env.PORT || "3001", 10);
|
|
1917
|
-
var __dirname2 = path14.dirname(fileURLToPath2(import.meta.url));
|
|
1918
|
-
var PLUGIN_DIR = path14.resolve(__dirname2, "..");
|
|
1919
|
-
function createApp(config = {}) {
|
|
1920
|
-
const DATA_DIR = config.dataDir || path14.join(PLUGIN_DIR, "data");
|
|
1921
|
-
const UPLOADS_DIR = config.uploadsDir || path14.join(PLUGIN_DIR, "uploads");
|
|
1922
|
-
const WORKSPACES_FILE = path14.join(DATA_DIR, "workspaces.json");
|
|
1923
|
-
const SETTINGS_FILE = path14.join(DATA_DIR, "settings.json");
|
|
1924
|
-
const INTELLIGENCE_DIR = path14.join(DATA_DIR, "intelligence");
|
|
1925
|
-
const authToken = config.authToken !== void 0 ? config.authToken : AUTH_TOKEN;
|
|
1926
|
-
const gatewayToken = config.gatewayToken !== void 0 ? config.gatewayToken : authToken;
|
|
1927
|
-
const gatewayUrl = config.gatewayUrl || GATEWAY_WS_URL;
|
|
1928
|
-
fs13.mkdirSync(DATA_DIR, { recursive: true });
|
|
1929
|
-
fs13.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
1930
|
-
const dbCache = /* @__PURE__ */ new Map();
|
|
1931
|
-
function getDb(workspaceName) {
|
|
1932
|
-
if (dbCache.has(workspaceName)) return dbCache.get(workspaceName);
|
|
1933
|
-
const db = new Database(path14.join(DATA_DIR, `${workspaceName}.db`));
|
|
1934
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
1935
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
1936
|
-
migrate(db);
|
|
1937
|
-
dbCache.set(workspaceName, db);
|
|
1938
|
-
return db;
|
|
1939
|
-
}
|
|
1940
|
-
__name(getDb, "getDb");
|
|
1941
|
-
function getActiveDb() {
|
|
1942
|
-
return requestDbStore.getStore() || getDb(getWorkspaces().active);
|
|
1943
|
-
}
|
|
1944
|
-
__name(getActiveDb, "getActiveDb");
|
|
1945
|
-
function closeDb(name) {
|
|
1946
|
-
const db = dbCache.get(name);
|
|
1947
|
-
if (db) {
|
|
1948
|
-
db.close();
|
|
1949
|
-
dbCache.delete(name);
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
__name(closeDb, "closeDb");
|
|
1953
|
-
function closeAll() {
|
|
1954
|
-
for (const db of dbCache.values()) db.close();
|
|
1955
|
-
dbCache.clear();
|
|
1956
|
-
globalDbCache.close?.();
|
|
1957
|
-
}
|
|
1958
|
-
__name(closeAll, "closeAll");
|
|
1959
|
-
let _globalDb = null;
|
|
1960
|
-
const globalDbCache = {
|
|
1961
|
-
get() {
|
|
1962
|
-
if (_globalDb) return _globalDb;
|
|
1963
|
-
_globalDb = new Database(path14.join(DATA_DIR, "global.db"));
|
|
1964
|
-
_globalDb.exec("PRAGMA journal_mode = WAL");
|
|
1965
|
-
_globalDb.exec(`CREATE TABLE IF NOT EXISTS custom_emojis (name TEXT NOT NULL, pack TEXT NOT NULL DEFAULT 'slackmojis', url TEXT NOT NULL, mime_type TEXT, created_at INTEGER DEFAULT (strftime('%s','now')), PRIMARY KEY (name, pack))`);
|
|
1966
|
-
return _globalDb;
|
|
1967
|
-
},
|
|
1968
|
-
close() {
|
|
1969
|
-
if (_globalDb) {
|
|
1970
|
-
_globalDb.close();
|
|
1971
|
-
_globalDb = null;
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
};
|
|
1975
|
-
let workspacesConfig = null;
|
|
1976
|
-
function getWorkspaces() {
|
|
1977
|
-
if (!workspacesConfig) {
|
|
1978
|
-
try {
|
|
1979
|
-
workspacesConfig = JSON.parse(fs13.readFileSync(WORKSPACES_FILE, "utf8"));
|
|
1980
|
-
} catch {
|
|
1981
|
-
workspacesConfig = { active: "default", workspaces: { default: { name: "default", label: "Default", createdAt: Date.now() } } };
|
|
1982
|
-
fs13.writeFileSync(WORKSPACES_FILE, JSON.stringify(workspacesConfig, null, 2));
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
return workspacesConfig;
|
|
1986
|
-
}
|
|
1987
|
-
__name(getWorkspaces, "getWorkspaces");
|
|
1988
|
-
function setWorkspaces(data) {
|
|
1989
|
-
workspacesConfig = data;
|
|
1990
|
-
fs13.writeFileSync(WORKSPACES_FILE, JSON.stringify(data, null, 2));
|
|
1991
|
-
}
|
|
1992
|
-
__name(setWorkspaces, "setWorkspaces");
|
|
1993
|
-
const debugLogger = new DebugLogger(DATA_DIR);
|
|
1994
|
-
const mediaStash = /* @__PURE__ */ new Map();
|
|
1995
|
-
const memoryConfig = discoverMemoryConfig();
|
|
1996
|
-
const memoryProvider = createMemoryProvider(memoryConfig);
|
|
1997
|
-
memoryProvider.init().catch((err) => console.error("[createApp] Memory provider init error:", err.message));
|
|
1998
|
-
const MEMORY_FILES_DIR = path14.join(memoryConfig.workspaceDir, "memory");
|
|
1999
|
-
const gatewayClient = new GatewayClient({ getDb, getWorkspaces, dataDir: DATA_DIR, debugLogger, gatewayWsUrl: gatewayUrl, authToken: gatewayToken, mediaStash });
|
|
2000
|
-
const broadcast = /* @__PURE__ */ __name((msg) => gatewayClient.broadcastToBrowsers(msg), "broadcast");
|
|
2001
|
-
const workspaces = new WorkspaceController({ getDb, closeDb, getWorkspaces, setWorkspaces, dataDir: DATA_DIR, broadcast });
|
|
2002
|
-
const threads = new ThreadController({ getActiveDb, getWorkspaces, uploadsDir: UPLOADS_DIR, broadcast });
|
|
2003
|
-
const messages = new MessageController({ getActiveDb, getWorkspaces, broadcast });
|
|
2004
|
-
const files = new FileController({ getActiveDb, getWorkspaces, uploadsDir: UPLOADS_DIR, intelligenceDir: INTELLIGENCE_DIR });
|
|
2005
|
-
const memory = new MemoryController({ memoryProvider, memoryFilesDir: MEMORY_FILES_DIR, memoryConfig });
|
|
2006
|
-
function handleGetSettings(req, res) {
|
|
2007
|
-
try {
|
|
2008
|
-
send(res, 200, fs13.existsSync(SETTINGS_FILE) ? JSON.parse(fs13.readFileSync(SETTINGS_FILE, "utf8")) : {});
|
|
2009
|
-
} catch {
|
|
2010
|
-
send(res, 200, {});
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
__name(handleGetSettings, "handleGetSettings");
|
|
2014
|
-
async function handleSaveSettings(req, res) {
|
|
2015
|
-
const body = await parseBody(req);
|
|
2016
|
-
fs13.mkdirSync(path14.dirname(SETTINGS_FILE), { recursive: true });
|
|
2017
|
-
fs13.writeFileSync(SETTINGS_FILE, JSON.stringify(body, null, 2));
|
|
2018
|
-
send(res, 200, { ok: true });
|
|
2019
|
-
}
|
|
2020
|
-
__name(handleSaveSettings, "handleSaveSettings");
|
|
2021
|
-
function checkAuth(req, res) {
|
|
2022
|
-
if (!authToken) return true;
|
|
2023
|
-
const auth = req.headers.authorization;
|
|
2024
|
-
if (!auth?.startsWith("Bearer ")) {
|
|
2025
|
-
sendError(res, 401, "Missing or invalid Authorization header");
|
|
2026
|
-
return false;
|
|
2027
|
-
}
|
|
2028
|
-
if (auth.slice(7) !== authToken) {
|
|
2029
|
-
sendError(res, 401, "Invalid auth token");
|
|
2030
|
-
return false;
|
|
2031
|
-
}
|
|
2032
|
-
return true;
|
|
2033
|
-
}
|
|
2034
|
-
__name(checkAuth, "checkAuth");
|
|
2035
|
-
async function handleRequest(req, res) {
|
|
2036
|
-
const wsName = req.headers?.["x-workspace"];
|
|
2037
|
-
const db = wsName ? getDb(wsName) : getActiveDb();
|
|
2038
|
-
return requestDbStore.run(db, () => route(req, res));
|
|
2039
|
-
}
|
|
2040
|
-
__name(handleRequest, "handleRequest");
|
|
2041
|
-
async function route(req, res) {
|
|
2042
|
-
const [urlPath, queryString] = (req.url || "/").split("?");
|
|
2043
|
-
const query = {};
|
|
2044
|
-
if (queryString) for (const pair of queryString.split("&")) {
|
|
2045
|
-
const [k, v] = pair.split("=");
|
|
2046
|
-
if (k) query[decodeURIComponent(k)] = decodeURIComponent(v || "");
|
|
2047
|
-
}
|
|
2048
|
-
const method = req.method;
|
|
2049
|
-
let p;
|
|
2050
|
-
if (method === "OPTIONS") {
|
|
2051
|
-
setCors(res);
|
|
2052
|
-
res.writeHead(204);
|
|
2053
|
-
return res.end();
|
|
2054
|
-
}
|
|
2055
|
-
if (method === "GET" && !urlPath.startsWith("/api/")) {
|
|
2056
|
-
const STATIC = { "/": "index.html", "/index.html": "index.html", "/app.js": "app.js", "/style.css": "style.css", "/error-handler.js": "error-handler.js", "/manifest.json": "manifest.json", "/favicon.ico": "favicon.ico" };
|
|
2057
|
-
const fileName = STATIC[urlPath];
|
|
2058
|
-
const isAllowed = fileName || urlPath.startsWith("/icons/") || urlPath.startsWith("/lib/") || urlPath.startsWith("/frontend/") || urlPath.startsWith("/emoji/") || urlPath === "/config.js";
|
|
2059
|
-
if (isAllowed) {
|
|
2060
|
-
const staticPath = path14.join(PLUGIN_DIR, fileName || urlPath.slice(1));
|
|
2061
|
-
if (fs13.existsSync(staticPath) && fs13.statSync(staticPath).isFile()) {
|
|
2062
|
-
const MIME = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".json": "application/json", ".ico": "image/x-icon", ".png": "image/png", ".svg": "image/svg+xml", ".gif": "image/gif", ".webp": "image/webp" };
|
|
2063
|
-
const ext = path14.extname(staticPath).toLowerCase();
|
|
2064
|
-
const stat = fs13.statSync(staticPath);
|
|
2065
|
-
res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream", "Content-Length": stat.size, "Cache-Control": ext === ".html" ? "no-cache" : "public, max-age=3600" });
|
|
2066
|
-
return fs13.createReadStream(staticPath).pipe(res);
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
if (p = matchRoute(method, urlPath, "GET /api/uploads/:threadId/:fileId")) return files.serveUpload(req, res, p);
|
|
2071
|
-
if (method === "GET" && urlPath === "/api/emoji") {
|
|
2072
|
-
try {
|
|
2073
|
-
const rows = globalDbCache.get().prepare("SELECT name, pack, url, mime_type FROM custom_emojis ORDER BY created_at DESC").all();
|
|
2074
|
-
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "public, max-age=300" });
|
|
2075
|
-
return res.end(JSON.stringify(rows));
|
|
2076
|
-
} catch (e) {
|
|
2077
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2078
|
-
return res.end(JSON.stringify({ error: e.message }));
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
if (method === "GET" && urlPath === "/api/emoji/search") {
|
|
2082
|
-
const q = query.q || "";
|
|
2083
|
-
if (!q) {
|
|
2084
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2085
|
-
return res.end(JSON.stringify({ error: "Missing ?q=" }));
|
|
2086
|
-
}
|
|
2087
|
-
try {
|
|
2088
|
-
const https = await import("https");
|
|
2089
|
-
const html = await new Promise((resolve, reject) => {
|
|
2090
|
-
https.default.get(`https://slackmojis.com/emojis/search?query=${encodeURIComponent(q)}`, (resp) => {
|
|
2091
|
-
let body = "";
|
|
2092
|
-
resp.on("data", (c) => body += c);
|
|
2093
|
-
resp.on("end", () => resolve(body));
|
|
2094
|
-
}).on("error", reject);
|
|
2095
|
-
});
|
|
2096
|
-
const results = [];
|
|
2097
|
-
const regex = /data-emoji-id-name="([^"]+)"[^>]*href="([^"]+)"[\s\S]*?<img[^>]*src="([^"]+)"/g;
|
|
2098
|
-
let match;
|
|
2099
|
-
while ((match = regex.exec(html)) !== null && results.length < 50) results.push({ name: match[1].replace(/^\d+-/, ""), image_url: match[3], download_url: `https://slackmojis.com${match[2]}` });
|
|
2100
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2101
|
-
return res.end(JSON.stringify(results));
|
|
2102
|
-
} catch (e) {
|
|
2103
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2104
|
-
return res.end(JSON.stringify({ error: e.message }));
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
if (!checkAuth(req, res)) return;
|
|
2108
|
-
try {
|
|
2109
|
-
if (method === "POST" && urlPath === "/api/emoji/add") {
|
|
2110
|
-
const { url, name, pack } = await parseBody(req);
|
|
2111
|
-
if (!url || !name) {
|
|
2112
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2113
|
-
return res.end(JSON.stringify({ error: "Missing url or name" }));
|
|
2114
|
-
}
|
|
2115
|
-
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2116
|
-
const targetPack = pack || "slackmojis";
|
|
2117
|
-
const mimeType = url.toLowerCase().endsWith(".gif") ? "image/gif" : url.toLowerCase().endsWith(".webp") ? "image/webp" : url.toLowerCase().match(/\.jpe?g/) ? "image/jpeg" : "image/png";
|
|
2118
|
-
globalDbCache.get().prepare("INSERT OR REPLACE INTO custom_emojis (name, pack, url, mime_type) VALUES (?, ?, ?, ?)").run(safeName, targetPack, url, mimeType);
|
|
2119
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2120
|
-
return res.end(JSON.stringify({ name: safeName, pack: targetPack, url, mime_type: mimeType }));
|
|
2121
|
-
}
|
|
2122
|
-
if (method === "DELETE" && urlPath === "/api/emoji") {
|
|
2123
|
-
const { name, pack } = await parseBody(req);
|
|
2124
|
-
if (!name || !pack) {
|
|
2125
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2126
|
-
return res.end(JSON.stringify({ error: "Missing name or pack" }));
|
|
2127
|
-
}
|
|
2128
|
-
globalDbCache.get().prepare("DELETE FROM custom_emojis WHERE name = ? AND pack = ?").run(name, pack);
|
|
2129
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2130
|
-
return res.end(JSON.stringify({ ok: true }));
|
|
2131
|
-
}
|
|
2132
|
-
if (method === "GET" && urlPath === "/api/file") return handleServeFile(req, res, query, memoryConfig);
|
|
2133
|
-
if (method === "GET" && urlPath === "/api/workspace") return handleWorkspaceList(req, res, query);
|
|
2134
|
-
if (method === "GET" && urlPath === "/api/workspace/file") return handleWorkspaceFileRead(req, res, query);
|
|
2135
|
-
if (method === "PUT" && urlPath === "/api/workspace/file") return await handleWorkspaceFileWrite(req, res, query);
|
|
2136
|
-
if (method === "DELETE" && urlPath === "/api/workspace/file") return handleWorkspaceFileDelete(req, res, query);
|
|
2137
|
-
if (method === "POST" && urlPath === "/api/workspace/upload") return await handleWorkspaceUpload(req, res, query);
|
|
2138
|
-
if (method === "GET" && urlPath === "/api/memory/status") return await memory.status(req, res);
|
|
2139
|
-
if (method === "GET" && urlPath === "/api/memory/list") return await memory.list(req, res, query);
|
|
2140
|
-
if (method === "GET" && urlPath === "/api/memory/search") return await memory.search(req, res, query);
|
|
2141
|
-
if (method === "GET" && urlPath === "/api/memory/files") return memory.files(req, res, query);
|
|
2142
|
-
if (p = matchRoute(method, urlPath, "PUT /api/memory/:id")) return await memory.update(req, res, p);
|
|
2143
|
-
if (p = matchRoute(method, urlPath, "DELETE /api/memory/:id")) return await memory.delete(req, res, p);
|
|
2144
|
-
if (method === "GET" && urlPath === "/api/settings") return handleGetSettings(req, res);
|
|
2145
|
-
if (method === "PUT" && urlPath === "/api/settings") return await handleSaveSettings(req, res);
|
|
2146
|
-
if (method === "POST" && urlPath === "/api/transcribe") return await handleTranscribe(req, res);
|
|
2147
|
-
if (method === "GET" && urlPath === "/api/health") return send(res, 200, { ok: true, workspace: getWorkspaces().active, uptime: process.uptime() });
|
|
2148
|
-
if (method === "GET" && urlPath === "/api/agents") {
|
|
2149
|
-
try {
|
|
2150
|
-
send(res, 200, { agents: fs13.readdirSync(path14.join(HOME3, ".openclaw", "agents"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) });
|
|
2151
|
-
} catch {
|
|
2152
|
-
send(res, 200, { agents: ["main"] });
|
|
2153
|
-
}
|
|
2154
|
-
return;
|
|
2155
|
-
}
|
|
2156
|
-
if (method === "GET" && urlPath === "/api/workspaces") return workspaces.getAll(req, res);
|
|
2157
|
-
if (method === "POST" && urlPath === "/api/workspaces") return await workspaces.create(req, res);
|
|
2158
|
-
if (p = matchRoute(method, urlPath, "PATCH /api/workspaces/:name")) return await workspaces.update(req, res, p);
|
|
2159
|
-
if (p = matchRoute(method, urlPath, "DELETE /api/workspaces/:name")) return workspaces.delete(req, res, p);
|
|
2160
|
-
if (method === "POST" && urlPath === "/api/workspaces/reorder") return await workspaces.reorder(req, res);
|
|
2161
|
-
if (p = matchRoute(method, urlPath, "POST /api/workspaces/:name/activate")) return workspaces.activate(req, res, p);
|
|
2162
|
-
if (method === "GET" && urlPath === "/api/threads") return threads.getAll(req, res, {}, query);
|
|
2163
|
-
if (method === "GET" && urlPath === "/api/threads/unread") return threads.getUnread(req, res);
|
|
2164
|
-
if (method === "POST" && urlPath === "/api/threads") return await threads.create(req, res);
|
|
2165
|
-
if (p = matchRoute(method, urlPath, "POST /api/threads/:id/mark-read")) return await threads.markRead(req, res, p);
|
|
2166
|
-
if (p = matchRoute(method, urlPath, "GET /api/threads/:id/messages")) return messages.getAll(req, res, p, query);
|
|
2167
|
-
if (p = matchRoute(method, urlPath, "POST /api/threads/:id/messages")) return await messages.create(req, res, p);
|
|
2168
|
-
if (p = matchRoute(method, urlPath, "DELETE /api/threads/:id/messages/:messageId")) return messages.delete(req, res, p);
|
|
2169
|
-
if (p = matchRoute(method, urlPath, "POST /api/threads/:id/context-fill")) return messages.contextFill(req, res, p);
|
|
2170
|
-
if (p = matchRoute(method, urlPath, "POST /api/threads/:id/generate-title")) {
|
|
2171
|
-
const db = getActiveDb();
|
|
2172
|
-
const thread = db.prepare("SELECT * FROM threads WHERE id = ?").get(p.id);
|
|
2173
|
-
if (!thread) return sendError(res, 404, "Thread not found");
|
|
2174
|
-
gatewayClient.generateThreadTitle(db, p.id, getWorkspaces().active);
|
|
2175
|
-
return send(res, 200, { ok: true });
|
|
2176
|
-
}
|
|
2177
|
-
if (p = matchRoute(method, urlPath, "POST /api/threads/:id/upload")) return await files.upload(req, res, p);
|
|
2178
|
-
if (p = matchRoute(method, urlPath, "GET /api/threads/:id/intelligence")) return files.getIntelligence(req, res, p);
|
|
2179
|
-
if (p = matchRoute(method, urlPath, "POST /api/threads/:id/intelligence")) return await files.saveIntelligence(req, res, p);
|
|
2180
|
-
if (p = matchRoute(method, urlPath, "GET /api/threads/:id")) return threads.get(req, res, p);
|
|
2181
|
-
if (p = matchRoute(method, urlPath, "PATCH /api/threads/:id")) return await threads.update(req, res, p);
|
|
2182
|
-
if (p = matchRoute(method, urlPath, "DELETE /api/threads/:id")) return threads.delete(req, res, p);
|
|
2183
|
-
if (method === "GET" && urlPath === "/api/search") return messages.search(req, res, {}, query);
|
|
2184
|
-
if (method === "GET" && urlPath === "/api/export") return messages.export(req, res);
|
|
2185
|
-
if (method === "POST" && urlPath === "/api/import") return await messages.import(req, res);
|
|
2186
|
-
if (method === "POST" && urlPath === "/api/active-thread") {
|
|
2187
|
-
const body = await parseBody(req);
|
|
2188
|
-
if (body.threadId && body.workspace) gatewayClient.setActiveThread(null, body.workspace, body.threadId);
|
|
2189
|
-
return send(res, 200, { ok: true });
|
|
2190
|
-
}
|
|
2191
|
-
sendError(res, 404, `Not found: ${method} ${urlPath}`);
|
|
2192
|
-
} catch (err) {
|
|
2193
|
-
console.error(`Error handling ${method} ${urlPath}:`, err);
|
|
2194
|
-
if (err.message?.includes("UNIQUE constraint")) sendError(res, 409, "Conflict: " + err.message);
|
|
2195
|
-
else sendError(res, 500, err.message || "Internal server error");
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
__name(route, "route");
|
|
2199
|
-
function setupBrowserWs(wss) {
|
|
2200
|
-
wss.on("connection", (ws) => {
|
|
2201
|
-
console.log("Browser client connected");
|
|
2202
|
-
gatewayClient.addBrowserClient(ws);
|
|
2203
|
-
ws.send(JSON.stringify({ type: "event", event: "connect.challenge", payload: { nonce: uuid(), ts: Date.now() } }));
|
|
2204
|
-
ws.on("message", async (data) => {
|
|
2205
|
-
const msgStr = data.toString();
|
|
2206
|
-
debugLogger.logFrame("BR\u2192SRV", msgStr);
|
|
2207
|
-
let msgToForward = msgStr;
|
|
2208
|
-
try {
|
|
2209
|
-
const msg = JSON.parse(msgStr);
|
|
2210
|
-
if (msg.type === "req" && msg.method === "connect") {
|
|
2211
|
-
const token = msg.params?.auth?.token;
|
|
2212
|
-
if (token === authToken || !authToken) {
|
|
2213
|
-
ws.send(JSON.stringify({ type: "res", id: msg.id, ok: true, payload: { type: "hello-ok", protocol: 3, server: { version: "0.1.0", host: "clawchats-backend" } } }));
|
|
2214
|
-
} else {
|
|
2215
|
-
ws.send(JSON.stringify({ type: "res", id: msg.id, ok: false, error: { code: "AUTH_FAILED", message: "Invalid auth token" } }));
|
|
2216
|
-
ws.close();
|
|
2217
|
-
}
|
|
2218
|
-
return;
|
|
2219
|
-
}
|
|
2220
|
-
if (msg.type === "clawchats" || msg.type === "shellchat") {
|
|
2221
|
-
if (msg.action === "active-thread") {
|
|
2222
|
-
gatewayClient.setActiveThread(ws, msg.workspace, msg.threadId);
|
|
2223
|
-
return;
|
|
2224
|
-
}
|
|
2225
|
-
if (msg.action === "debug-start") {
|
|
2226
|
-
const r = debugLogger.start(msg.ts, ws);
|
|
2227
|
-
ws.send(JSON.stringify(r.error === "already-active" ? { type: "clawchats", event: "debug-error", error: "Recording already active in another tab", sessionId: r.sessionId } : { type: "clawchats", event: "debug-started", sessionId: r.sessionId }));
|
|
2228
|
-
return;
|
|
2229
|
-
}
|
|
2230
|
-
if (msg.action === "debug-dump") {
|
|
2231
|
-
const r = debugLogger.saveDump(msg);
|
|
2232
|
-
ws.send(JSON.stringify({ type: "clawchats", event: "debug-saved", sessionId: r.sessionId, files: r.files }));
|
|
2233
|
-
return;
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
if (msg.type === "req" && msg.method === "chat.send" && msg.params?.attachments?.length > 0) {
|
|
2237
|
-
const parsed = parseSessionKey(msg.params.sessionKey || "");
|
|
2238
|
-
const threadId = parsed?.threadId || "misc";
|
|
2239
|
-
const uploadDir = path14.join(UPLOADS_DIR, threadId);
|
|
2240
|
-
fs13.mkdirSync(uploadDir, { recursive: true });
|
|
2241
|
-
const extMap = { jpeg: "jpg", jpg: "jpg", png: "png", gif: "gif", webp: "webp", pdf: "pdf", "svg+xml": "svg", mp3: "mp3", mp4: "mp4", wav: "wav", webm: "webm" };
|
|
2242
|
-
const savedPaths = [];
|
|
2243
|
-
for (const att of msg.params.attachments) {
|
|
2244
|
-
if (!att.content || !att.mimeType) continue;
|
|
2245
|
-
try {
|
|
2246
|
-
const rawExt = att.mimeType.split("/")[1]?.split(";")[0] || "bin";
|
|
2247
|
-
const filePath = path14.join(uploadDir, `${Date.now()}_${Math.random().toString(36).slice(2, 6)}.${extMap[rawExt] || rawExt}`);
|
|
2248
|
-
fs13.writeFileSync(filePath, Buffer.from(att.content, "base64"));
|
|
2249
|
-
savedPaths.push(filePath);
|
|
2250
|
-
} catch (err) {
|
|
2251
|
-
console.error("[upload] Failed to save attachment:", err.message);
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
if (savedPaths.length > 0) {
|
|
2255
|
-
const note = `
|
|
2256
|
-
|
|
2257
|
-
[${savedPaths.length === 1 ? "Attached file saved on disk" : "Attached files saved on disk"}:
|
|
2258
|
-
${savedPaths.map((p) => `- ${p}`).join("\n")}]`;
|
|
2259
|
-
msgToForward = JSON.stringify({ ...msg, params: { ...msg.params, message: (msg.params.message || "") + note } });
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
} catch {
|
|
2263
|
-
}
|
|
2264
|
-
gatewayClient.sendToGateway(msgToForward);
|
|
2265
|
-
});
|
|
2266
|
-
ws.on("close", () => {
|
|
2267
|
-
console.log("Browser client disconnected");
|
|
2268
|
-
debugLogger.handleClientDisconnect(ws);
|
|
2269
|
-
gatewayClient.removeBrowserClient(ws);
|
|
2270
|
-
});
|
|
2271
|
-
ws.on("error", (err) => console.error("Browser WebSocket error:", err.message));
|
|
2272
|
-
});
|
|
2273
|
-
}
|
|
2274
|
-
__name(setupBrowserWs, "setupBrowserWs");
|
|
2275
|
-
return {
|
|
2276
|
-
handleRequest,
|
|
2277
|
-
getDb,
|
|
2278
|
-
getActiveDb,
|
|
2279
|
-
getWorkspaces,
|
|
2280
|
-
setWorkspaces,
|
|
2281
|
-
shutdown: closeAll,
|
|
2282
|
-
closeAllDbs: closeAll,
|
|
2283
|
-
gatewayClient,
|
|
2284
|
-
setupBrowserWs,
|
|
2285
|
-
dataDir: DATA_DIR
|
|
2286
|
-
};
|
|
2287
|
-
}
|
|
2288
|
-
__name(createApp, "createApp");
|
|
2289
|
-
function migrate(db) {
|
|
2290
|
-
db.exec(`
|
|
2291
|
-
CREATE TABLE IF NOT EXISTS threads (
|
|
2292
|
-
id TEXT PRIMARY KEY, session_key TEXT UNIQUE NOT NULL, title TEXT DEFAULT 'New chat',
|
|
2293
|
-
pinned INTEGER DEFAULT 0, pin_order INTEGER DEFAULT 0, model TEXT,
|
|
2294
|
-
last_session_id TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
2295
|
-
);
|
|
2296
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
2297
|
-
id TEXT PRIMARY KEY, thread_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL,
|
|
2298
|
-
status TEXT DEFAULT 'sent', metadata TEXT, seq INTEGER, timestamp INTEGER NOT NULL, created_at INTEGER NOT NULL,
|
|
2299
|
-
FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE
|
|
2300
|
-
);
|
|
2301
|
-
CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id, timestamp);
|
|
2302
|
-
CREATE INDEX IF NOT EXISTS idx_messages_dedup ON messages(thread_id, role, timestamp);
|
|
2303
|
-
`);
|
|
2304
|
-
try {
|
|
2305
|
-
db.exec("ALTER TABLE threads ADD COLUMN sort_order INTEGER DEFAULT 0");
|
|
2306
|
-
} catch {
|
|
2307
|
-
}
|
|
2308
|
-
try {
|
|
2309
|
-
db.exec("ALTER TABLE threads ADD COLUMN unread_count INTEGER DEFAULT 0");
|
|
2310
|
-
} catch {
|
|
2311
|
-
}
|
|
2312
|
-
db.exec(`CREATE TABLE IF NOT EXISTS unread_messages (thread_id TEXT NOT NULL, message_id TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (thread_id, message_id), FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE)`);
|
|
2313
|
-
db.exec("CREATE INDEX IF NOT EXISTS idx_unread_thread ON unread_messages(thread_id)");
|
|
2314
|
-
ensureFts(db);
|
|
2315
|
-
}
|
|
2316
|
-
__name(migrate, "migrate");
|
|
2317
|
-
function createFts(db) {
|
|
2318
|
-
db.exec(`CREATE VIRTUAL TABLE messages_fts USING fts5(content, content=messages, content_rowid=rowid, tokenize='porter unicode61 tokenchars x27')`);
|
|
2319
|
-
db.exec(`CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, new.content); END`);
|
|
2320
|
-
db.exec(`CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.rowid, old.content); END`);
|
|
2321
|
-
db.exec(`CREATE TRIGGER messages_au AFTER UPDATE ON messages BEGIN INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.rowid, old.content); INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, new.content); END`);
|
|
2322
|
-
}
|
|
2323
|
-
__name(createFts, "createFts");
|
|
2324
|
-
function dropFts(db) {
|
|
2325
|
-
db.exec("DROP TABLE IF EXISTS messages_fts; DROP TRIGGER IF EXISTS messages_ai; DROP TRIGGER IF EXISTS messages_ad; DROP TRIGGER IF EXISTS messages_au;");
|
|
2326
|
-
}
|
|
2327
|
-
__name(dropFts, "dropFts");
|
|
2328
|
-
function ensureFts(db) {
|
|
2329
|
-
const hasFts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
|
|
2330
|
-
if (!hasFts) {
|
|
2331
|
-
try {
|
|
2332
|
-
createFts(db);
|
|
2333
|
-
db.prepare("INSERT INTO messages_fts(messages_fts) VALUES('rebuild')").run();
|
|
2334
|
-
} catch (e) {
|
|
2335
|
-
console.error("[DB] messages_fts creation failed:", e.message);
|
|
2336
|
-
}
|
|
2337
|
-
return;
|
|
2338
|
-
}
|
|
2339
|
-
const schema = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
|
|
2340
|
-
if (schema && !schema.sql.includes("tokenchars")) {
|
|
2341
|
-
console.log("[DB] Upgrading messages_fts tokenizer...");
|
|
2342
|
-
try {
|
|
2343
|
-
dropFts(db);
|
|
2344
|
-
createFts(db);
|
|
2345
|
-
db.prepare("INSERT INTO messages_fts(messages_fts) VALUES('rebuild')").run();
|
|
2346
|
-
console.log("[DB] Upgrade complete");
|
|
2347
|
-
} catch (e) {
|
|
2348
|
-
console.error("[DB] Upgrade failed:", e.message);
|
|
2349
|
-
dropFts(db);
|
|
2350
|
-
}
|
|
2351
|
-
} else {
|
|
2352
|
-
try {
|
|
2353
|
-
db.prepare("INSERT INTO messages_fts(messages_fts) VALUES('integrity-check')").run();
|
|
2354
|
-
} catch {
|
|
2355
|
-
try {
|
|
2356
|
-
db.prepare("INSERT INTO messages_fts(messages_fts) VALUES('rebuild')").run();
|
|
2357
|
-
} catch (e) {
|
|
2358
|
-
console.error("[DB] FTS rebuild failed:", e.message);
|
|
2359
|
-
dropFts(db);
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
__name(ensureFts, "ensureFts");
|
|
2365
|
-
var isDirectRun = import.meta.url === `file://${process.argv[1]}`;
|
|
2366
|
-
if (isDirectRun) {
|
|
2367
|
-
const app = createApp();
|
|
2368
|
-
app.getActiveDb();
|
|
2369
|
-
const server = http.createServer(app.handleRequest);
|
|
2370
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
2371
|
-
app.setupBrowserWs(wss);
|
|
2372
|
-
server.on("upgrade", (req, socket, head) => {
|
|
2373
|
-
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
|
|
2374
|
-
});
|
|
2375
|
-
server.listen(PORT, () => {
|
|
2376
|
-
console.log(`ClawChats backend listening on port ${PORT}`);
|
|
2377
|
-
console.log(`Active workspace: ${app.getWorkspaces().active}`);
|
|
2378
|
-
console.log(`Data dir: ${app.dataDir}`);
|
|
2379
|
-
app.gatewayClient.connect();
|
|
2380
|
-
});
|
|
2381
|
-
const shutdown = /* @__PURE__ */ __name(() => {
|
|
2382
|
-
console.log("Shutting down...");
|
|
2383
|
-
app.shutdown();
|
|
2384
|
-
server.close(() => process.exit(0));
|
|
2385
|
-
setTimeout(() => process.exit(1), 5e3);
|
|
2386
|
-
}, "shutdown");
|
|
2387
|
-
process.on("SIGTERM", shutdown);
|
|
2388
|
-
process.on("SIGINT", shutdown);
|
|
2389
|
-
}
|
|
2390
|
-
export {
|
|
2391
|
-
createApp
|
|
2392
|
-
};
|