dsclaw 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +2680 -418
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1 -710
- package/dist/index.js +3652 -39763
- package/dist/index.js.map +1 -1
- package/dist/web/assets/index--NY6w2i6.js +40 -0
- package/dist/web/assets/index-DXkqRQn1.css +2 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/icon-192.png +0 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/icons.svg +24 -0
- package/dist/web/index.html +20 -0
- package/dist/web/manifest.json +28 -0
- package/dist/web/sw.js +37 -0
- package/openclaw.plugin.json +3 -3
- package/package.json +6 -5
- package/dist/web/chat.html +0 -419
package/dist/cli/index.js
CHANGED
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// src/cli/init.ts
|
|
7
|
-
import inquirer from "inquirer";
|
|
8
|
-
|
|
9
|
-
// src/gateway/config.ts
|
|
10
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
11
|
-
import { join } from "path";
|
|
12
|
-
import { homedir } from "os";
|
|
13
|
-
import { z } from "zod";
|
|
14
|
-
|
|
15
|
-
// src/shared/logger.ts
|
|
16
|
-
import pino from "pino";
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
17
6
|
|
|
18
7
|
// src/shared/tracer.ts
|
|
19
8
|
import { AsyncLocalStorage } from "async_hooks";
|
|
20
9
|
import { nanoid } from "nanoid";
|
|
21
|
-
var storage = new AsyncLocalStorage();
|
|
22
10
|
function runWithTrace(ctx, fn) {
|
|
23
11
|
const traceId = ctx.traceId ?? nanoid(12);
|
|
24
12
|
return storage.run({ traceId, ...ctx }, fn);
|
|
@@ -29,26 +17,52 @@ function getTraceContext() {
|
|
|
29
17
|
function getTraceId() {
|
|
30
18
|
return getTraceContext().traceId;
|
|
31
19
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
mixin() {
|
|
38
|
-
const ctx = getTraceContext();
|
|
39
|
-
return {
|
|
40
|
-
traceId: ctx.traceId,
|
|
41
|
-
...ctx.userId ? { userId: ctx.userId } : {},
|
|
42
|
-
...ctx.channelId ? { channelId: ctx.channelId } : {},
|
|
43
|
-
...ctx.agentId ? { agentId: ctx.agentId } : {}
|
|
44
|
-
};
|
|
20
|
+
var storage;
|
|
21
|
+
var init_tracer = __esm({
|
|
22
|
+
"src/shared/tracer.ts"() {
|
|
23
|
+
"use strict";
|
|
24
|
+
storage = new AsyncLocalStorage();
|
|
45
25
|
}
|
|
46
26
|
});
|
|
27
|
+
|
|
28
|
+
// src/shared/logger.ts
|
|
29
|
+
import pino from "pino";
|
|
47
30
|
function createLogger(module) {
|
|
48
31
|
return rootLogger.child({ module });
|
|
49
32
|
}
|
|
33
|
+
var rootLogger;
|
|
34
|
+
var init_logger = __esm({
|
|
35
|
+
"src/shared/logger.ts"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
init_tracer();
|
|
38
|
+
rootLogger = pino({
|
|
39
|
+
level: process.env["LOG_LEVEL"] ?? "info",
|
|
40
|
+
transport: process.env["NODE_ENV"] !== "production" ? { target: "pino-pretty", options: { colorize: true } } : void 0,
|
|
41
|
+
mixin() {
|
|
42
|
+
const ctx = getTraceContext();
|
|
43
|
+
return {
|
|
44
|
+
traceId: ctx.traceId,
|
|
45
|
+
...ctx.userId ? { userId: ctx.userId } : {},
|
|
46
|
+
...ctx.channelId ? { channelId: ctx.channelId } : {},
|
|
47
|
+
...ctx.agentId ? { agentId: ctx.agentId } : {}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// src/cli/index.ts
|
|
55
|
+
import { Command } from "commander";
|
|
56
|
+
|
|
57
|
+
// src/cli/init.ts
|
|
58
|
+
import inquirer from "inquirer";
|
|
50
59
|
|
|
51
60
|
// src/gateway/config.ts
|
|
61
|
+
init_logger();
|
|
62
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
63
|
+
import { join } from "path";
|
|
64
|
+
import { homedir } from "os";
|
|
65
|
+
import { z } from "zod";
|
|
52
66
|
var log = createLogger("config");
|
|
53
67
|
var CONFIG_DIR = join(homedir(), ".dsclaw");
|
|
54
68
|
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
@@ -150,20 +164,49 @@ import {
|
|
|
150
164
|
} from "fs";
|
|
151
165
|
import { join as join2 } from "path";
|
|
152
166
|
import { hostname, userInfo } from "os";
|
|
167
|
+
init_logger();
|
|
153
168
|
var log2 = createLogger("user-session");
|
|
154
169
|
var ALG = "aes-256-gcm";
|
|
155
170
|
var IV_LEN = 12;
|
|
156
171
|
var TAG_LEN = 16;
|
|
157
172
|
var KEY_SEED = "dsclaw-v1";
|
|
158
173
|
var SESSIONS_DIR = join2(CONFIG_DIR, "sessions");
|
|
159
|
-
|
|
174
|
+
var SALT_FILE = join2(CONFIG_DIR, ".salt");
|
|
175
|
+
function getOrCreateSalt() {
|
|
176
|
+
try {
|
|
177
|
+
if (existsSync2(SALT_FILE)) {
|
|
178
|
+
return readFileSync2(SALT_FILE, "utf-8").trim();
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
const salt = randomBytes(32).toString("hex");
|
|
183
|
+
try {
|
|
184
|
+
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
185
|
+
writeFileSync2(SALT_FILE, salt, { mode: 384 });
|
|
186
|
+
} catch (err) {
|
|
187
|
+
log2.warn({ err }, "Failed to persist salt file");
|
|
188
|
+
}
|
|
189
|
+
return salt;
|
|
190
|
+
}
|
|
191
|
+
var cachedSalt = null;
|
|
192
|
+
function getSalt() {
|
|
193
|
+
if (!cachedSalt) cachedSalt = getOrCreateSalt();
|
|
194
|
+
return cachedSalt;
|
|
195
|
+
}
|
|
196
|
+
function deriveKeyBase(extra) {
|
|
160
197
|
let user = "";
|
|
161
198
|
try {
|
|
162
199
|
user = userInfo().username;
|
|
163
200
|
} catch {
|
|
164
201
|
user = process.env["USER"] ?? process.env["USERNAME"] ?? "default";
|
|
165
202
|
}
|
|
166
|
-
return createHash("sha256").update(`${KEY_SEED}:${hostname()}:${user}`).digest();
|
|
203
|
+
return createHash("sha256").update(`${KEY_SEED}:${hostname()}:${user}${extra}`).digest();
|
|
204
|
+
}
|
|
205
|
+
function deriveKey() {
|
|
206
|
+
return deriveKeyBase(`:${getSalt()}`);
|
|
207
|
+
}
|
|
208
|
+
function deriveKeyLegacy() {
|
|
209
|
+
return deriveKeyBase("");
|
|
167
210
|
}
|
|
168
211
|
function encrypt(data) {
|
|
169
212
|
const key = deriveKey();
|
|
@@ -173,9 +216,8 @@ function encrypt(data) {
|
|
|
173
216
|
const tag = cipher.getAuthTag();
|
|
174
217
|
return Buffer.concat([iv, tag, ct]).toString("base64");
|
|
175
218
|
}
|
|
176
|
-
function
|
|
219
|
+
function decryptWithKey(encoded, key) {
|
|
177
220
|
try {
|
|
178
|
-
const key = deriveKey();
|
|
179
221
|
const buf = Buffer.from(encoded, "base64");
|
|
180
222
|
if (buf.length < IV_LEN + TAG_LEN + 1) return null;
|
|
181
223
|
const iv = buf.subarray(0, IV_LEN);
|
|
@@ -183,9 +225,7 @@ function decrypt(encoded) {
|
|
|
183
225
|
const ct = buf.subarray(IV_LEN + TAG_LEN);
|
|
184
226
|
const decipher = createDecipheriv(ALG, key, iv, { authTagLength: TAG_LEN });
|
|
185
227
|
decipher.setAuthTag(tag);
|
|
186
|
-
return Buffer.concat([decipher.update(ct), decipher.final()]).toString(
|
|
187
|
-
"utf8"
|
|
188
|
-
);
|
|
228
|
+
return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
|
|
189
229
|
} catch {
|
|
190
230
|
return null;
|
|
191
231
|
}
|
|
@@ -206,12 +246,22 @@ function loadSession(userId) {
|
|
|
206
246
|
}
|
|
207
247
|
try {
|
|
208
248
|
const encrypted = readFileSync2(p, "utf-8").trim();
|
|
209
|
-
|
|
249
|
+
let json = decryptWithKey(encrypted, deriveKey());
|
|
250
|
+
let needsMigration = false;
|
|
251
|
+
if (!json) {
|
|
252
|
+
json = decryptWithKey(encrypted, deriveKeyLegacy());
|
|
253
|
+
needsMigration = !!json;
|
|
254
|
+
}
|
|
210
255
|
if (!json) {
|
|
211
256
|
log2.warn({ userId }, "Failed to decrypt session \u2014 starting fresh");
|
|
212
257
|
return { state: "new" };
|
|
213
258
|
}
|
|
214
|
-
|
|
259
|
+
const session = JSON.parse(json);
|
|
260
|
+
if (needsMigration) {
|
|
261
|
+
log2.info({ userId }, "Migrating session to salted key");
|
|
262
|
+
saveSession(userId, session);
|
|
263
|
+
}
|
|
264
|
+
return session;
|
|
215
265
|
} catch {
|
|
216
266
|
return { state: "new" };
|
|
217
267
|
}
|
|
@@ -228,6 +278,7 @@ function isOnboarding(state) {
|
|
|
228
278
|
}
|
|
229
279
|
|
|
230
280
|
// src/dsers/auth.ts
|
|
281
|
+
init_logger();
|
|
231
282
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, chmodSync } from "fs";
|
|
232
283
|
import { dirname } from "path";
|
|
233
284
|
|
|
@@ -353,6 +404,60 @@ var DSersAuth = class {
|
|
|
353
404
|
this.fetchedAt = 0;
|
|
354
405
|
log3.debug("Session invalidated");
|
|
355
406
|
}
|
|
407
|
+
/** Fast session validity check — calls DSers account info endpoint. */
|
|
408
|
+
async checkHealth() {
|
|
409
|
+
if (!this.sessionId) {
|
|
410
|
+
return { valid: false, reason: "\u672A\u6388\u6743\uFF08\u65E0 session\uFF09" };
|
|
411
|
+
}
|
|
412
|
+
if (Date.now() - this.fetchedAt > SESSION_TTL) {
|
|
413
|
+
return { valid: false, reason: "session \u5DF2\u8FC7\u671F\uFF08\u8D85\u8FC7 6 \u5C0F\u65F6\uFF09" };
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
const resp = await fetch(
|
|
417
|
+
`${this.config.baseUrl}/account-user-bff/v1/users/current`,
|
|
418
|
+
{
|
|
419
|
+
headers: {
|
|
420
|
+
Cookie: `sessionid=${this.sessionId}; djdt=hide; state=${this.state}`
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
425
|
+
this.invalidate();
|
|
426
|
+
return { valid: false, reason: `DSers \u8FD4\u56DE ${resp.status}\uFF08token \u5931\u6548\uFF09\uFF0C\u8BF7\u91CD\u65B0\u6388\u6743` };
|
|
427
|
+
}
|
|
428
|
+
if (!resp.ok) {
|
|
429
|
+
return { valid: false, reason: `DSers \u670D\u52A1\u5F02\u5E38\uFF08HTTP ${resp.status}\uFF09` };
|
|
430
|
+
}
|
|
431
|
+
return { valid: true };
|
|
432
|
+
} catch (err) {
|
|
433
|
+
const reason = err instanceof Error ? err.message : "unknown";
|
|
434
|
+
return { valid: false, reason: `\u7F51\u7EDC\u9519\u8BEF\uFF08${reason}\uFF09\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5` };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Attempt session renewal for session-only users (no email/password).
|
|
439
|
+
* Returns success if the session is still valid; fails with guidance otherwise.
|
|
440
|
+
*/
|
|
441
|
+
async tryRenew() {
|
|
442
|
+
const health = await this.checkHealth();
|
|
443
|
+
if (health.valid) {
|
|
444
|
+
this.fetchedAt = Date.now();
|
|
445
|
+
return { renewed: true };
|
|
446
|
+
}
|
|
447
|
+
if (this.config.email && this.config.password) {
|
|
448
|
+
try {
|
|
449
|
+
await this.login();
|
|
450
|
+
return { renewed: true };
|
|
451
|
+
} catch (err) {
|
|
452
|
+
const reason = err instanceof Error ? err.message : "\u767B\u5F55\u5931\u8D25";
|
|
453
|
+
return { renewed: false, reason };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
renewed: false,
|
|
458
|
+
reason: `${health.reason}\u3002\u5F53\u524D\u4E3A session \u6A21\u5F0F\uFF0C\u9700\u8981\u901A\u8FC7\u6D4F\u89C8\u5668\u91CD\u65B0\u6388\u6743\u3002`
|
|
459
|
+
};
|
|
460
|
+
}
|
|
356
461
|
readCache() {
|
|
357
462
|
try {
|
|
358
463
|
const p = this.config.sessionFile;
|
|
@@ -385,6 +490,10 @@ var DSersAuth = class {
|
|
|
385
490
|
}
|
|
386
491
|
};
|
|
387
492
|
|
|
493
|
+
// src/dsers/client.ts
|
|
494
|
+
init_logger();
|
|
495
|
+
init_tracer();
|
|
496
|
+
|
|
388
497
|
// src/shared/utils.ts
|
|
389
498
|
import { createHash as createHash2 } from "crypto";
|
|
390
499
|
function sleep(ms) {
|
|
@@ -400,6 +509,7 @@ function parseRetryAfter(value) {
|
|
|
400
509
|
}
|
|
401
510
|
|
|
402
511
|
// src/resilience/rate-limiter.ts
|
|
512
|
+
init_logger();
|
|
403
513
|
import pLimit from "p-limit";
|
|
404
514
|
var log4 = createLogger("rate-limiter");
|
|
405
515
|
var DEFAULT_OUTBOUND_LIMITS = {
|
|
@@ -417,20 +527,40 @@ function getOutboundLimiter(service, customLimits) {
|
|
|
417
527
|
}
|
|
418
528
|
return outboundLimiters.get(service);
|
|
419
529
|
}
|
|
420
|
-
var
|
|
530
|
+
var userLocks = /* @__PURE__ */ new Map();
|
|
531
|
+
var lockVersion = 0;
|
|
421
532
|
var debounceTimers = /* @__PURE__ */ new Map();
|
|
422
|
-
async function acquireUserLock(userId) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
533
|
+
async function acquireUserLock(userId, timeoutMs = 18e4) {
|
|
534
|
+
const deadline = Date.now() + timeoutMs;
|
|
535
|
+
while (userLocks.has(userId)) {
|
|
536
|
+
const remaining = deadline - Date.now();
|
|
537
|
+
if (remaining <= 0) {
|
|
538
|
+
log4.warn({ userId }, "User lock wait timed out \u2014 forcing entry");
|
|
539
|
+
const stale = userLocks.get(userId);
|
|
540
|
+
if (stale) {
|
|
541
|
+
stale.resolve();
|
|
542
|
+
}
|
|
543
|
+
userLocks.delete(userId);
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
await Promise.race([
|
|
547
|
+
userLocks.get(userId).promise,
|
|
548
|
+
new Promise((r) => setTimeout(r, remaining))
|
|
549
|
+
]);
|
|
550
|
+
}
|
|
551
|
+
const myVersion = ++lockVersion;
|
|
552
|
+
let resolve;
|
|
553
|
+
const promise = new Promise((r) => {
|
|
554
|
+
resolve = r;
|
|
432
555
|
});
|
|
433
|
-
|
|
556
|
+
userLocks.set(userId, { promise, resolve, version: myVersion });
|
|
557
|
+
const release = () => {
|
|
558
|
+
const current = userLocks.get(userId);
|
|
559
|
+
if (current && current.version === myVersion) {
|
|
560
|
+
userLocks.delete(userId);
|
|
561
|
+
resolve();
|
|
562
|
+
}
|
|
563
|
+
};
|
|
434
564
|
return release;
|
|
435
565
|
}
|
|
436
566
|
function debounceUser(userId, delayMs = 500) {
|
|
@@ -450,7 +580,7 @@ var inboundWindows = /* @__PURE__ */ new Map();
|
|
|
450
580
|
function checkInboundLimit(sourceId, maxRequests = 30, windowMs = 6e4) {
|
|
451
581
|
const now = Date.now();
|
|
452
582
|
const entry = inboundWindows.get(sourceId) ?? { timestamps: [] };
|
|
453
|
-
entry.timestamps = entry.timestamps.filter((
|
|
583
|
+
entry.timestamps = entry.timestamps.filter((t2) => now - t2 < windowMs);
|
|
454
584
|
if (entry.timestamps.length >= maxRequests) {
|
|
455
585
|
log4.warn({ sourceId, count: entry.timestamps.length }, "Inbound rate limit hit");
|
|
456
586
|
return false;
|
|
@@ -566,7 +696,298 @@ function createDSersConfig(email, password, baseUrl) {
|
|
|
566
696
|
};
|
|
567
697
|
}
|
|
568
698
|
|
|
699
|
+
// src/dsers/browser-auth.ts
|
|
700
|
+
import { spawn } from "child_process";
|
|
701
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
702
|
+
import { join as join4 } from "path";
|
|
703
|
+
import { tmpdir } from "os";
|
|
704
|
+
import { WebSocket } from "ws";
|
|
705
|
+
|
|
706
|
+
// src/dsers/browser-finder.ts
|
|
707
|
+
import { existsSync as existsSync4 } from "fs";
|
|
708
|
+
var CANDIDATES = [
|
|
709
|
+
{
|
|
710
|
+
name: "Google Chrome",
|
|
711
|
+
paths: {
|
|
712
|
+
darwin: ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
|
|
713
|
+
win32: [
|
|
714
|
+
`${process.env["PROGRAMFILES"]}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
715
|
+
`${process.env["PROGRAMFILES(X86)"]}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
716
|
+
`${process.env["LOCALAPPDATA"]}\\Google\\Chrome\\Application\\chrome.exe`
|
|
717
|
+
],
|
|
718
|
+
linux: ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable"]
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
name: "Microsoft Edge",
|
|
723
|
+
paths: {
|
|
724
|
+
darwin: ["/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"],
|
|
725
|
+
win32: [
|
|
726
|
+
`${process.env["PROGRAMFILES(X86)"]}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
727
|
+
`${process.env["PROGRAMFILES"]}\\Microsoft\\Edge\\Application\\msedge.exe`
|
|
728
|
+
],
|
|
729
|
+
linux: ["/usr/bin/microsoft-edge", "/usr/bin/microsoft-edge-stable"]
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
name: "Brave",
|
|
734
|
+
paths: {
|
|
735
|
+
darwin: ["/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"],
|
|
736
|
+
win32: [
|
|
737
|
+
`${process.env["PROGRAMFILES"]}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
|
|
738
|
+
`${process.env["LOCALAPPDATA"]}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`
|
|
739
|
+
],
|
|
740
|
+
linux: ["/usr/bin/brave-browser"]
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
name: "Arc",
|
|
745
|
+
paths: {
|
|
746
|
+
darwin: ["/Applications/Arc.app/Contents/MacOS/Arc"]
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
name: "Chromium",
|
|
751
|
+
paths: {
|
|
752
|
+
darwin: ["/Applications/Chromium.app/Contents/MacOS/Chromium"],
|
|
753
|
+
linux: ["/usr/bin/chromium", "/usr/bin/chromium-browser"]
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
name: "Opera",
|
|
758
|
+
paths: {
|
|
759
|
+
darwin: ["/Applications/Opera.app/Contents/MacOS/Opera"],
|
|
760
|
+
win32: [
|
|
761
|
+
`${process.env["LOCALAPPDATA"]}\\Programs\\Opera\\opera.exe`
|
|
762
|
+
],
|
|
763
|
+
linux: ["/usr/bin/opera"]
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
name: "Vivaldi",
|
|
768
|
+
paths: {
|
|
769
|
+
darwin: ["/Applications/Vivaldi.app/Contents/MacOS/Vivaldi"],
|
|
770
|
+
win32: [
|
|
771
|
+
`${process.env["LOCALAPPDATA"]}\\Vivaldi\\Application\\vivaldi.exe`
|
|
772
|
+
],
|
|
773
|
+
linux: ["/usr/bin/vivaldi"]
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
];
|
|
777
|
+
function findChromium() {
|
|
778
|
+
const platform = process.platform;
|
|
779
|
+
for (const candidate of CANDIDATES) {
|
|
780
|
+
const platformPaths = candidate.paths[platform];
|
|
781
|
+
if (!platformPaths) continue;
|
|
782
|
+
for (const p of platformPaths) {
|
|
783
|
+
if (existsSync4(p)) {
|
|
784
|
+
return { name: candidate.name, executablePath: p };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/dsers/browser-auth.ts
|
|
792
|
+
init_logger();
|
|
793
|
+
var log6 = createLogger("dsers:browser-auth");
|
|
794
|
+
var DSERS_LOGIN_URL = "https://accounts.dsers.com/accounts/login";
|
|
795
|
+
var LOGIN_API_PATTERN = "/account-user-bff/v1/users/login";
|
|
796
|
+
var TIMEOUT = 18e4;
|
|
797
|
+
var activeProcess = null;
|
|
798
|
+
var activeTmpDir = null;
|
|
799
|
+
function isAuthInProgress() {
|
|
800
|
+
return activeProcess !== null && !activeProcess.killed;
|
|
801
|
+
}
|
|
802
|
+
function cancelAuth() {
|
|
803
|
+
cleanup();
|
|
804
|
+
}
|
|
805
|
+
function cleanup() {
|
|
806
|
+
if (activeProcess && !activeProcess.killed) {
|
|
807
|
+
try {
|
|
808
|
+
activeProcess.kill();
|
|
809
|
+
} catch {
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
activeProcess = null;
|
|
813
|
+
if (activeTmpDir) {
|
|
814
|
+
try {
|
|
815
|
+
rmSync(activeTmpDir, { recursive: true, force: true });
|
|
816
|
+
} catch {
|
|
817
|
+
}
|
|
818
|
+
activeTmpDir = null;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function parseWsUrl(stderrChunk) {
|
|
822
|
+
const match = stderrChunk.match(/DevTools listening on (ws:\/\/[^\s]+)/);
|
|
823
|
+
return match?.[1] ?? null;
|
|
824
|
+
}
|
|
825
|
+
function launchBrowser() {
|
|
826
|
+
const browser = findChromium();
|
|
827
|
+
if (!browser) {
|
|
828
|
+
throw new Error("No Chromium-based browser found. Please install Chrome, Edge, or Brave.");
|
|
829
|
+
}
|
|
830
|
+
const tmpDir = mkdtempSync(join4(tmpdir(), "dsclaw-auth-"));
|
|
831
|
+
activeTmpDir = tmpDir;
|
|
832
|
+
log6.info({ browser: browser.name }, "Launching browser for DSers auth");
|
|
833
|
+
const proc = spawn(browser.executablePath, [
|
|
834
|
+
`--app=${DSERS_LOGIN_URL}`,
|
|
835
|
+
"--remote-debugging-port=0",
|
|
836
|
+
`--user-data-dir=${tmpDir}`,
|
|
837
|
+
"--no-first-run",
|
|
838
|
+
"--no-default-browser-check",
|
|
839
|
+
"--disable-extensions",
|
|
840
|
+
"--mute-audio",
|
|
841
|
+
"--window-size=480,700"
|
|
842
|
+
], {
|
|
843
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
844
|
+
detached: false
|
|
845
|
+
});
|
|
846
|
+
activeProcess = proc;
|
|
847
|
+
const wsUrlPromise = new Promise((resolve, reject) => {
|
|
848
|
+
let stderr = "";
|
|
849
|
+
const timer = setTimeout(() => {
|
|
850
|
+
reject(new Error("Timed out waiting for Chrome DevTools"));
|
|
851
|
+
}, 15e3);
|
|
852
|
+
proc.stderr.on("data", (chunk) => {
|
|
853
|
+
stderr += chunk.toString();
|
|
854
|
+
const url = parseWsUrl(stderr);
|
|
855
|
+
if (url) {
|
|
856
|
+
clearTimeout(timer);
|
|
857
|
+
resolve(url);
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
proc.on("error", (err) => {
|
|
861
|
+
clearTimeout(timer);
|
|
862
|
+
reject(err);
|
|
863
|
+
});
|
|
864
|
+
proc.on("exit", () => {
|
|
865
|
+
clearTimeout(timer);
|
|
866
|
+
reject(new Error("Browser exited before DevTools was ready"));
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
return { proc, wsUrlPromise };
|
|
870
|
+
}
|
|
871
|
+
function watchLoginResponse(wsUrl) {
|
|
872
|
+
return new Promise((resolve, reject) => {
|
|
873
|
+
const ws = new WebSocket(wsUrl);
|
|
874
|
+
let msgId = 1;
|
|
875
|
+
let timeoutTimer = null;
|
|
876
|
+
let settled = false;
|
|
877
|
+
const pending = /* @__PURE__ */ new Map();
|
|
878
|
+
const requestMap = /* @__PURE__ */ new Map();
|
|
879
|
+
function cdpSend(method, params = {}) {
|
|
880
|
+
return new Promise((res) => {
|
|
881
|
+
const id = msgId++;
|
|
882
|
+
pending.set(id, res);
|
|
883
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
function finish(result, err) {
|
|
887
|
+
if (settled) return;
|
|
888
|
+
settled = true;
|
|
889
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
890
|
+
try {
|
|
891
|
+
ws.close();
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
if (result) resolve(result);
|
|
895
|
+
else reject(err ?? new Error("Unknown error"));
|
|
896
|
+
}
|
|
897
|
+
ws.on("open", async () => {
|
|
898
|
+
log6.info("CDP connected, intercepting login API responses");
|
|
899
|
+
await cdpSend("Network.enable");
|
|
900
|
+
timeoutTimer = setTimeout(() => {
|
|
901
|
+
finish(void 0, new Error("Login timed out (3 minutes). Please try again."));
|
|
902
|
+
cleanup();
|
|
903
|
+
}, TIMEOUT);
|
|
904
|
+
});
|
|
905
|
+
ws.on("message", async (raw) => {
|
|
906
|
+
try {
|
|
907
|
+
const msg = JSON.parse(raw.toString());
|
|
908
|
+
if (msg.id && pending.has(msg.id)) {
|
|
909
|
+
pending.get(msg.id)(msg.result);
|
|
910
|
+
pending.delete(msg.id);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (msg.method === "Network.requestWillBeSent") {
|
|
914
|
+
const p = msg.params;
|
|
915
|
+
if (p.request.url.includes(LOGIN_API_PATTERN) && p.request.method === "POST") {
|
|
916
|
+
requestMap.set(p.requestId, p.request.url);
|
|
917
|
+
log6.info("Detected DSers login request");
|
|
918
|
+
}
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (msg.method === "Network.responseReceived") {
|
|
922
|
+
const p = msg.params;
|
|
923
|
+
if (!requestMap.has(p.requestId)) return;
|
|
924
|
+
log6.info({ status: p.response.status }, "DSers login response received");
|
|
925
|
+
if (p.response.status !== 200) {
|
|
926
|
+
log6.warn("Login response was not 200, waiting for retry...");
|
|
927
|
+
requestMap.delete(p.requestId);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
931
|
+
try {
|
|
932
|
+
const bodyResp = await cdpSend("Network.getResponseBody", {
|
|
933
|
+
requestId: p.requestId
|
|
934
|
+
});
|
|
935
|
+
const body = bodyResp.base64Encoded ? Buffer.from(bodyResp.body, "base64").toString() : bodyResp.body;
|
|
936
|
+
const data = JSON.parse(body);
|
|
937
|
+
const sessionId = data?.data?.sessionId;
|
|
938
|
+
const state = data?.data?.state ?? "";
|
|
939
|
+
if (sessionId) {
|
|
940
|
+
log6.info("DSers session captured from login API response");
|
|
941
|
+
finish({ sessionId, state });
|
|
942
|
+
} else {
|
|
943
|
+
log6.warn({ responseData: body.slice(0, 300) }, "Login response missing sessionId");
|
|
944
|
+
}
|
|
945
|
+
} catch (e) {
|
|
946
|
+
log6.warn({ error: e instanceof Error ? e.message : String(e) }, "Failed to read login response body");
|
|
947
|
+
}
|
|
948
|
+
requestMap.delete(p.requestId);
|
|
949
|
+
}
|
|
950
|
+
} catch {
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
ws.on("error", () => {
|
|
954
|
+
finish(void 0, new Error("CDP connection lost"));
|
|
955
|
+
});
|
|
956
|
+
ws.on("close", () => {
|
|
957
|
+
finish(void 0, new Error("Browser was closed before login completed"));
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
async function loginViaCDP() {
|
|
962
|
+
if (isAuthInProgress()) {
|
|
963
|
+
throw new Error("Auth already in progress");
|
|
964
|
+
}
|
|
965
|
+
try {
|
|
966
|
+
const { proc, wsUrlPromise } = launchBrowser();
|
|
967
|
+
proc.on("exit", () => {
|
|
968
|
+
activeProcess = null;
|
|
969
|
+
});
|
|
970
|
+
const wsUrl = await wsUrlPromise;
|
|
971
|
+
log6.info({ wsUrl }, "DevTools endpoint acquired");
|
|
972
|
+
const httpUrl = wsUrl.replace("ws://", "http://").replace(/\/devtools\/browser\/.*/, "/json");
|
|
973
|
+
const resp = await fetch(httpUrl);
|
|
974
|
+
const tabs = await resp.json();
|
|
975
|
+
const dsersTab = tabs.find((t2) => t2.url.includes("dsers.com")) ?? tabs[0];
|
|
976
|
+
if (!dsersTab?.webSocketDebuggerUrl) {
|
|
977
|
+
throw new Error("Could not find browser tab");
|
|
978
|
+
}
|
|
979
|
+
log6.info({ tabUrl: dsersTab.url }, "Watching tab for login API response");
|
|
980
|
+
const result = await watchLoginResponse(dsersTab.webSocketDebuggerUrl);
|
|
981
|
+
cleanup();
|
|
982
|
+
return result;
|
|
983
|
+
} catch (error) {
|
|
984
|
+
cleanup();
|
|
985
|
+
throw error;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
569
989
|
// src/agents/core-agent.ts
|
|
990
|
+
init_logger();
|
|
570
991
|
import { generateText, streamText, tool as aiTool, stepCountIs } from "ai";
|
|
571
992
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
572
993
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
@@ -574,11 +995,12 @@ import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
|
574
995
|
import { z as z2 } from "zod";
|
|
575
996
|
|
|
576
997
|
// src/shared/audit.ts
|
|
577
|
-
import { appendFileSync, mkdirSync as mkdirSync4, existsSync as
|
|
578
|
-
import { join as
|
|
579
|
-
|
|
998
|
+
import { appendFileSync, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
|
|
999
|
+
import { join as join5 } from "path";
|
|
1000
|
+
init_tracer();
|
|
1001
|
+
var AUDIT_DIR = join5(CONFIG_DIR, "audit");
|
|
580
1002
|
function ensureAuditDir() {
|
|
581
|
-
if (!
|
|
1003
|
+
if (!existsSync5(AUDIT_DIR)) {
|
|
582
1004
|
mkdirSync4(AUDIT_DIR, { recursive: true });
|
|
583
1005
|
}
|
|
584
1006
|
}
|
|
@@ -590,7 +1012,7 @@ function writeAuditLog(entry) {
|
|
|
590
1012
|
traceId: getTraceId()
|
|
591
1013
|
};
|
|
592
1014
|
const date = full.timestamp.slice(0, 10);
|
|
593
|
-
const file =
|
|
1015
|
+
const file = join5(AUDIT_DIR, `${date}.jsonl`);
|
|
594
1016
|
try {
|
|
595
1017
|
appendFileSync(file, JSON.stringify(full) + "\n");
|
|
596
1018
|
} catch {
|
|
@@ -602,49 +1024,195 @@ async function getImportList(client, params) {
|
|
|
602
1024
|
const filtered = params ? Object.fromEntries(Object.entries(params).filter(([, v]) => v != null)) : {};
|
|
603
1025
|
return client.get("/dsers-product-bff/import-list", filtered);
|
|
604
1026
|
}
|
|
605
|
-
async function
|
|
606
|
-
return client.
|
|
607
|
-
}
|
|
608
|
-
async function pushToStore(client, payload) {
|
|
609
|
-
return client.post("/dsers-product-bff/import-list/push", {
|
|
610
|
-
data: payload
|
|
611
|
-
});
|
|
1027
|
+
async function getImportListItem(client, id) {
|
|
1028
|
+
return client.get(`/dsers-product-bff/import-list/${id}`);
|
|
612
1029
|
}
|
|
613
1030
|
async function getMyProducts(client, params) {
|
|
614
1031
|
return client.get("/dsers-product-bff/my-product", params);
|
|
615
1032
|
}
|
|
616
1033
|
|
|
617
|
-
// src/
|
|
618
|
-
|
|
619
|
-
|
|
1034
|
+
// src/agents/core-agent.ts
|
|
1035
|
+
import { configFromToken } from "@lofder/dsers-mcp-product/dist/dsers/config.js";
|
|
1036
|
+
import { buildProvider } from "@lofder/dsers-mcp-product/dist/provider.js";
|
|
1037
|
+
import { ImportFlowService } from "@lofder/dsers-mcp-product/dist/service.js";
|
|
1038
|
+
import { MemoryJobStore } from "@lofder/dsers-mcp-product/dist/job-store-memory.js";
|
|
1039
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, renameSync, existsSync as existsSync6 } from "fs";
|
|
1040
|
+
import { join as join6 } from "path";
|
|
1041
|
+
import { homedir as homedir2 } from "os";
|
|
1042
|
+
var log7 = createLogger("agent:core");
|
|
1043
|
+
var JOB_ID_MAP_DIR = join6(homedir2(), ".dsclaw");
|
|
1044
|
+
var JOB_ID_MAP_FILE = join6(JOB_ID_MAP_DIR, "job-id-map.json");
|
|
1045
|
+
var JOB_ID_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1046
|
+
function loadJobIdMap() {
|
|
1047
|
+
const map = /* @__PURE__ */ new Map();
|
|
1048
|
+
try {
|
|
1049
|
+
if (!existsSync6(JOB_ID_MAP_FILE)) return map;
|
|
1050
|
+
const raw = JSON.parse(readFileSync4(JOB_ID_MAP_FILE, "utf-8"));
|
|
1051
|
+
const now = Date.now();
|
|
1052
|
+
for (const [short, entry] of Object.entries(raw)) {
|
|
1053
|
+
if (now - entry.ts < JOB_ID_TTL_MS) {
|
|
1054
|
+
map.set(short, entry.full);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
log7.warn({ err }, "Failed to load job ID map from disk");
|
|
1059
|
+
}
|
|
1060
|
+
return map;
|
|
620
1061
|
}
|
|
621
|
-
|
|
622
|
-
|
|
1062
|
+
function persistJobIdMap(map) {
|
|
1063
|
+
try {
|
|
1064
|
+
mkdirSync5(JOB_ID_MAP_DIR, { recursive: true });
|
|
1065
|
+
const obj = {};
|
|
1066
|
+
const now = Date.now();
|
|
1067
|
+
for (const [short, full] of map) {
|
|
1068
|
+
obj[short] = { full, ts: now };
|
|
1069
|
+
}
|
|
1070
|
+
const tmp = JOB_ID_MAP_FILE + ".tmp";
|
|
1071
|
+
writeFileSync4(tmp, JSON.stringify(obj));
|
|
1072
|
+
renameSync(tmp, JOB_ID_MAP_FILE);
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
log7.warn({ err }, "Failed to persist job ID map");
|
|
1075
|
+
}
|
|
623
1076
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
1077
|
+
function normalizeBaseUrl(url) {
|
|
1078
|
+
if (!url) return void 0;
|
|
1079
|
+
let u = url.trim();
|
|
1080
|
+
if (!/^https?:\/\//i.test(u)) u = `https://${u}`;
|
|
1081
|
+
u = u.replace(/\/+$/, "");
|
|
1082
|
+
if (!/\/v\d/.test(u)) u += "/v1";
|
|
1083
|
+
return u;
|
|
628
1084
|
}
|
|
629
|
-
|
|
630
|
-
return
|
|
1085
|
+
function withTimeout(promise, ms, label) {
|
|
1086
|
+
return Promise.race([
|
|
1087
|
+
promise,
|
|
1088
|
+
new Promise((_, reject) => {
|
|
1089
|
+
const id = setTimeout(() => reject(new Error(`${label} timed out after ${Math.round(ms / 1e3)}s`)), ms);
|
|
1090
|
+
if (typeof id === "object" && "unref" in id) id.unref();
|
|
1091
|
+
})
|
|
1092
|
+
]);
|
|
631
1093
|
}
|
|
632
|
-
|
|
633
|
-
|
|
1094
|
+
function withToolAwareTimeout(promise, baseMs, isToolActive, label) {
|
|
1095
|
+
let timerId = null;
|
|
1096
|
+
const cleanup2 = () => {
|
|
1097
|
+
if (timerId !== null) {
|
|
1098
|
+
clearTimeout(timerId);
|
|
1099
|
+
timerId = null;
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1103
|
+
let deadline = Date.now() + baseMs;
|
|
1104
|
+
const tick = () => {
|
|
1105
|
+
if (isToolActive()) {
|
|
1106
|
+
deadline = Date.now() + baseMs;
|
|
1107
|
+
timerId = setTimeout(tick, 1e4);
|
|
1108
|
+
} else if (Date.now() >= deadline) {
|
|
1109
|
+
reject(new Error(`${label} timed out after ${Math.round(baseMs / 1e3)}s`));
|
|
1110
|
+
} else {
|
|
1111
|
+
timerId = setTimeout(tick, Math.min(deadline - Date.now(), 1e4));
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
timerId = setTimeout(tick, baseMs);
|
|
1115
|
+
});
|
|
1116
|
+
return Promise.race([promise.finally(cleanup2), timeoutPromise]);
|
|
1117
|
+
}
|
|
1118
|
+
var MCP_DROP_KEYS = /* @__PURE__ */ new Set([
|
|
1119
|
+
"image_urls",
|
|
1120
|
+
"description_html_snippet",
|
|
1121
|
+
"effective_rules_snapshot",
|
|
1122
|
+
"requested_rules",
|
|
1123
|
+
"original_draft",
|
|
1124
|
+
"resolved_source_url",
|
|
1125
|
+
"resolver_mode",
|
|
1126
|
+
"tags_before",
|
|
1127
|
+
"tags_after"
|
|
1128
|
+
]);
|
|
1129
|
+
function compactToolResult(result, jobIdMap) {
|
|
1130
|
+
if (!result || typeof result !== "object") return result;
|
|
1131
|
+
const isMcpPreview = "job_id" in result && ("title_after" in result || "status" in result);
|
|
1132
|
+
if (isMcpPreview) {
|
|
1133
|
+
const out2 = {};
|
|
1134
|
+
for (const [k, v] of Object.entries(result)) {
|
|
1135
|
+
if (MCP_DROP_KEYS.has(k)) continue;
|
|
1136
|
+
out2[k] = v;
|
|
1137
|
+
}
|
|
1138
|
+
if (out2.job_id && typeof out2.job_id === "string") {
|
|
1139
|
+
const dotIdx = out2.job_id.indexOf(".");
|
|
1140
|
+
if (dotIdx > 0) {
|
|
1141
|
+
const short = out2.job_id.slice(0, dotIdx);
|
|
1142
|
+
if (jobIdMap) jobIdMap.set(short, out2.job_id);
|
|
1143
|
+
out2.job_id = short;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
if (out2.skus && Array.isArray(out2.skus) && out2.skus.length > 1) {
|
|
1147
|
+
const [header, ...rows] = out2.skus;
|
|
1148
|
+
out2.variant_summary = rows.slice(0, 5).map((row) => {
|
|
1149
|
+
const obj = {};
|
|
1150
|
+
header.forEach((key, i) => {
|
|
1151
|
+
obj[key] = row[i];
|
|
1152
|
+
});
|
|
1153
|
+
return obj;
|
|
1154
|
+
});
|
|
1155
|
+
if (rows.length > 5) {
|
|
1156
|
+
out2.variant_summary_note = `Showing 5 of ${rows.length} variants`;
|
|
1157
|
+
}
|
|
1158
|
+
delete out2.skus;
|
|
1159
|
+
}
|
|
1160
|
+
if (out2.title_after) {
|
|
1161
|
+
out2._title_modified = true;
|
|
1162
|
+
out2._display_title = out2.title_after;
|
|
1163
|
+
}
|
|
1164
|
+
if (out2.desc_changed) {
|
|
1165
|
+
out2._description_modified = true;
|
|
1166
|
+
}
|
|
1167
|
+
if (out2.warnings?.length > 5) out2.warnings = out2.warnings.slice(0, 5);
|
|
1168
|
+
if (out2.stores) {
|
|
1169
|
+
out2.stores = out2.stores.map((s) => ({
|
|
1170
|
+
store_ref: s.store_ref,
|
|
1171
|
+
display_name: s.display_name,
|
|
1172
|
+
platform: s.platform
|
|
1173
|
+
}));
|
|
1174
|
+
}
|
|
1175
|
+
if (out2.account_info) {
|
|
1176
|
+
const ai = out2.account_info;
|
|
1177
|
+
out2.account_info = { plan: ai.plan, limits: ai.limits, aliexpress_auth: ai.aliexpress_auth };
|
|
1178
|
+
}
|
|
1179
|
+
return out2;
|
|
1180
|
+
}
|
|
1181
|
+
const json = JSON.stringify(result);
|
|
1182
|
+
if (json.length <= 4e3) return result;
|
|
1183
|
+
const out = {};
|
|
1184
|
+
for (const [k, v] of Object.entries(result)) {
|
|
1185
|
+
if (MCP_DROP_KEYS.has(k)) continue;
|
|
1186
|
+
if (Array.isArray(v) && v.length > 20) {
|
|
1187
|
+
out[k] = v.slice(0, 20);
|
|
1188
|
+
out[`_${k}_truncated`] = `${v.length} total, showing first 20`;
|
|
1189
|
+
} else {
|
|
1190
|
+
out[k] = v;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const outJson = JSON.stringify(out);
|
|
1194
|
+
if (outJson.length > 8e3) {
|
|
1195
|
+
return { _truncated: true, _original_size: json.length, summary: outJson.slice(0, 6e3) + "..." };
|
|
1196
|
+
}
|
|
1197
|
+
return out;
|
|
634
1198
|
}
|
|
635
|
-
|
|
636
|
-
// src/agents/core-agent.ts
|
|
637
|
-
var log6 = createLogger("agent:core");
|
|
638
1199
|
function buildModel(llm) {
|
|
1200
|
+
const baseURL = normalizeBaseUrl(llm.baseUrl);
|
|
1201
|
+
if (baseURL || llm.provider === "other") {
|
|
1202
|
+
return createOpenAI({
|
|
1203
|
+
apiKey: llm.apiKey,
|
|
1204
|
+
baseURL: baseURL ?? "http://localhost:11434/v1"
|
|
1205
|
+
}).chat(llm.model);
|
|
1206
|
+
}
|
|
639
1207
|
switch (llm.provider) {
|
|
640
1208
|
case "openai":
|
|
641
|
-
return createOpenAI({ apiKey: llm.apiKey })(llm.model);
|
|
1209
|
+
return createOpenAI({ apiKey: llm.apiKey }).chat(llm.model);
|
|
642
1210
|
case "anthropic":
|
|
643
1211
|
return createAnthropic({ apiKey: llm.apiKey })(llm.model);
|
|
644
1212
|
case "google":
|
|
645
1213
|
return createGoogleGenerativeAI({ apiKey: llm.apiKey })(llm.model);
|
|
646
1214
|
default:
|
|
647
|
-
return createOpenAI({ apiKey: llm.apiKey })(llm.model);
|
|
1215
|
+
return createOpenAI({ apiKey: llm.apiKey }).chat(llm.model);
|
|
648
1216
|
}
|
|
649
1217
|
}
|
|
650
1218
|
function getDefaultModel(provider) {
|
|
@@ -655,36 +1223,86 @@ function getDefaultModel(provider) {
|
|
|
655
1223
|
return "claude-sonnet-4-20250514";
|
|
656
1224
|
case "google":
|
|
657
1225
|
return "gemini-2.0-flash";
|
|
1226
|
+
case "other":
|
|
1227
|
+
return "gpt-3.5-turbo";
|
|
658
1228
|
default:
|
|
659
1229
|
return "gpt-4o";
|
|
660
1230
|
}
|
|
661
1231
|
}
|
|
662
|
-
var
|
|
663
|
-
|
|
1232
|
+
var TOOL_LABELS = {
|
|
1233
|
+
dsers_store_discover: "Discovering stores & capabilities...",
|
|
1234
|
+
dsers_product_import: "Importing product...",
|
|
1235
|
+
dsers_product_preview: "Loading preview...",
|
|
1236
|
+
dsers_product_delete: "Deleting product...",
|
|
1237
|
+
dsers_product_visibility: "Setting visibility...",
|
|
1238
|
+
dsers_store_push: "Pushing to store...",
|
|
1239
|
+
dsers_rules_validate: "Validating rules...",
|
|
1240
|
+
dsers_job_status: "Checking job status...",
|
|
1241
|
+
dsers_import_list_search: "Searching import list...",
|
|
1242
|
+
dsers_my_products: "Loading your products..."
|
|
1243
|
+
};
|
|
1244
|
+
var SYSTEM_PROMPT = `You are DSClaw, a friendly dropshipping AI for DSers. User has zero technical background.
|
|
1245
|
+
{{LANGUAGE_RULE}}
|
|
1246
|
+
Be warm, clear. Confirm before push/delete/bulk ops.
|
|
664
1247
|
|
|
665
|
-
|
|
666
|
-
-
|
|
667
|
-
-
|
|
668
|
-
-
|
|
669
|
-
-
|
|
670
|
-
-
|
|
671
|
-
|
|
1248
|
+
PRICES \u2014 always show separately:
|
|
1249
|
+
- cost (\u91C7\u8D2D\u4EF7): supplier price. Field: "cost"
|
|
1250
|
+
- sell_price (\u552E\u4EF7): store price. Field: "sell_price"
|
|
1251
|
+
- compare_at_price: strikethrough price
|
|
1252
|
+
- no_markup=true \u2192 warn user to set pricing rules
|
|
1253
|
+
- From dsers_import_list_search: use "cost_range" and "sell_price_range"
|
|
1254
|
+
When in Chinese: Format: \u91C7\u8D2D\u4EF7\uFF1A$2.51\u2013$4.00 | \u552E\u4EF7\uFF1A$5.02\u2013$8.00
|
|
1255
|
+
When in English: Format: Cost: $2.51\u2013$4.00 | Sell price: $5.02\u2013$8.00
|
|
672
1256
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
- Push products to connected stores (Shopify, WooCommerce, etc.)
|
|
678
|
-
- View products already in stores ("My Products")
|
|
679
|
-
- Check and update pricing rules
|
|
680
|
-
- View account plan and billing info
|
|
681
|
-
- Check global settings and shipping configuration
|
|
1257
|
+
WORKFLOWS:
|
|
1258
|
+
Import: dsers_store_discover \u2192 dsers_product_import(url) \u2192 (rules) \u2192 dsers_store_push(confirm first)
|
|
1259
|
+
Batch: source_urls_json / job_ids_json / target_stores_json
|
|
1260
|
+
Delete: dsers_product_delete without confirm \u2192 show details \u2192 dsers_product_delete with confirm=true
|
|
682
1261
|
|
|
683
|
-
|
|
684
|
-
-
|
|
685
|
-
-
|
|
686
|
-
|
|
687
|
-
|
|
1262
|
+
TOOL SELECTION \u2014 ALL modifications go through dsers_product_import:
|
|
1263
|
+
- dsers_import_list_search returns each item's "source_url". Use it for re-import.
|
|
1264
|
+
- Title/description/price changes on existing items:
|
|
1265
|
+
1. dsers_product_import(source_url from dsers_import_list_search, rules_json with changes)
|
|
1266
|
+
2. This re-imports and applies rules in one call, persisting changes via MCP.
|
|
1267
|
+
- PRICING (pick mode by user intent):
|
|
1268
|
+
| "set price $9.99" / "all $9.99" \u2192 fixed_price: {"pricing":{"mode":"fixed_price","fixed_price":9.99}}
|
|
1269
|
+
| "double the price" / "3x" \u2192 multiplier: {"pricing":{"mode":"multiplier","multiplier":2.0}}
|
|
1270
|
+
| "add $5 markup" \u2192 fixed_markup: {"pricing":{"mode":"fixed_markup","fixed_markup":5.00}}
|
|
1271
|
+
| "Red $9.99, Blue $12.99" \u2192 variant_overrides: {"variant_overrides":[{"match":"Red","sell_price":9.99},{"match":"Blue","sell_price":12.99}]}
|
|
1272
|
+
All modes accept optional round_digits (0-10).
|
|
1273
|
+
- CONTENT: title_override, title_prefix, title_suffix, description_override_html, description_append_html, tags_add (array)
|
|
1274
|
+
- IMAGES: drop_indexes, reorder, add_urls, keep_first_n. Only URLs, no base64.
|
|
1275
|
+
- VARIANT_OVERRIDES: array of {match, sell_price, compare_at_price, stock, title, image_url}. Applied AFTER global pricing.
|
|
1276
|
+
- OPTION_EDITS: array of {action, option_name, value_name?, new_name?}. Actions: rename_option, rename_value, remove_value (DESTRUCTIVE), remove_option.
|
|
1277
|
+
- Check response: title_after (changed title), _title_modified, sell_price, cost, desc_changed
|
|
1278
|
+
- Existing job_id: dsers_product_import(job_id + rules_json) to re-apply rules.
|
|
1279
|
+
|
|
1280
|
+
PUSH CONFIRMATION: Before calling dsers_store_push, ALWAYS show a confirmation checklist and wait for user approval:
|
|
1281
|
+
- Product: title, variant count
|
|
1282
|
+
- Price: sell vs cost range
|
|
1283
|
+
- Store: name
|
|
1284
|
+
- Visibility: Draft or Published (warn if Published)
|
|
1285
|
+
- Shipping profile: selected profile name, or "NOT SELECTED" with available options listed
|
|
1286
|
+
- Pricing Rule: MCP pricing or store rule (warn if conflict)
|
|
1287
|
+
If required info is missing (e.g. no shipping profile selected), show warning with available options and ask user to choose.
|
|
1288
|
+
ONLY call dsers_store_push after explicit user confirmation (e.g. "push", "go", "confirmed", "\u63A8\u9001", "\u786E\u8BA4").
|
|
1289
|
+
Exception: if user's ORIGINAL message already contains explicit push instruction ("push it", "\u76F4\u63A5\u63A8\u9001", "\u63A8\u9001\u5230\u5E97\u94FA"), treat as pre-confirmed \u2014 show checklist as summary, not blocker.
|
|
1290
|
+
|
|
1291
|
+
SHIPPING PROFILES: Shopify stores may have multiple shipping profiles.
|
|
1292
|
+
- Single profile \u2192 auto-selected, no action needed.
|
|
1293
|
+
- Multiple profiles + none specified \u2192 push BLOCKED with available_profiles list.
|
|
1294
|
+
- Use shipping_profile_name in push_options_json to select one.
|
|
1295
|
+
- Do NOT rely on "default" profiles \u2014 always show user available options when blocked.
|
|
1296
|
+
|
|
1297
|
+
ERROR RECOVERY:
|
|
1298
|
+
- Expired job_id \u2192 re-import with source_url
|
|
1299
|
+
- blocked array \u2192 hard stop, fix before retry
|
|
1300
|
+
- warnings array \u2192 soft alert, push proceeds
|
|
1301
|
+
- push_blocked_by_missing_shipping_profile \u2192 show available profiles, ask user to pick, retry with shipping_profile_name
|
|
1302
|
+
- Auth/401/session errors \u2192 call dsers_authorize to open browser login
|
|
1303
|
+
- After dsers_authorize succeeds: tell user authorization succeeded and ask them to resend their request. Do NOT retry in the same turn (session will refresh on next message).
|
|
1304
|
+
- If DSers session expired: tell user to go to Settings \u2192 Disconnect DSers \u2192 Reconnect. NEVER mention terminal commands (npx, npm, node) or environment variables (DSERS_TOKEN).
|
|
1305
|
+
- NEVER ask user to run terminal commands or npx`;
|
|
688
1306
|
var DSClawCoreAgent = class {
|
|
689
1307
|
id = "dsclaw-core";
|
|
690
1308
|
name = "DSClaw Core Agent";
|
|
@@ -692,13 +1310,44 @@ var DSClawCoreAgent = class {
|
|
|
692
1310
|
memory;
|
|
693
1311
|
llm;
|
|
694
1312
|
customTools = /* @__PURE__ */ new Map();
|
|
695
|
-
|
|
1313
|
+
importService = null;
|
|
1314
|
+
jobIdMap = loadJobIdMap();
|
|
1315
|
+
mcpAvailable = false;
|
|
1316
|
+
authCallback;
|
|
1317
|
+
constructor(dsers, memory, llm, dsersSession, authCallback, jobStore) {
|
|
696
1318
|
this.dsers = dsers;
|
|
697
1319
|
this.memory = memory;
|
|
698
1320
|
this.llm = llm;
|
|
1321
|
+
this.authCallback = authCallback;
|
|
1322
|
+
if (dsersSession?.sessionId) {
|
|
1323
|
+
try {
|
|
1324
|
+
const mcpConfig = configFromToken(dsersSession.sessionId, dsersSession.state);
|
|
1325
|
+
const provider = buildProvider(mcpConfig);
|
|
1326
|
+
this.importService = new ImportFlowService(provider, jobStore ?? new MemoryJobStore());
|
|
1327
|
+
this.mcpAvailable = true;
|
|
1328
|
+
log7.info("MCP ImportFlowService initialized");
|
|
1329
|
+
} catch (err) {
|
|
1330
|
+
log7.warn({ err }, "Failed to init MCP ImportFlowService \u2014 product tools unavailable");
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
getSystemPrompt() {
|
|
1335
|
+
const rule = "ALWAYS respond in the same language as the user's most recent message. If they write Chinese, respond in Chinese. If English, respond in English. Never switch languages unless the user does.";
|
|
1336
|
+
return SYSTEM_PROMPT.replace("{{LANGUAGE_RULE}}", `Language rule: ${rule}`);
|
|
1337
|
+
}
|
|
1338
|
+
shortenJobId(fullId) {
|
|
1339
|
+
const dotIdx = fullId.indexOf(".");
|
|
1340
|
+
if (dotIdx <= 0) return fullId;
|
|
1341
|
+
const short = fullId.slice(0, dotIdx);
|
|
1342
|
+
this.jobIdMap.set(short, fullId);
|
|
1343
|
+
persistJobIdMap(this.jobIdMap);
|
|
1344
|
+
return short;
|
|
699
1345
|
}
|
|
700
|
-
|
|
701
|
-
this.
|
|
1346
|
+
resolveJobId(maybeShort) {
|
|
1347
|
+
return this.jobIdMap.get(maybeShort) ?? maybeShort;
|
|
1348
|
+
}
|
|
1349
|
+
registerTool(t2) {
|
|
1350
|
+
this.customTools.set(t2.name, t2);
|
|
702
1351
|
}
|
|
703
1352
|
removeTool(name) {
|
|
704
1353
|
this.customTools.delete(name);
|
|
@@ -712,9 +1361,9 @@ var DSClawCoreAgent = class {
|
|
|
712
1361
|
const memoryContext = memories.length > 0 ? `
|
|
713
1362
|
|
|
714
1363
|
Relevant memories:
|
|
715
|
-
${memories.map((
|
|
1364
|
+
${memories.map((m2) => `- ${m2.content}`).join("\n")}` : "";
|
|
716
1365
|
const messages = [
|
|
717
|
-
{ role: "system", content:
|
|
1366
|
+
{ role: "system", content: this.getSystemPrompt() + memoryContext },
|
|
718
1367
|
...context.history.map(
|
|
719
1368
|
(h) => ({
|
|
720
1369
|
role: h.role,
|
|
@@ -724,17 +1373,21 @@ ${memories.map((m) => `- ${m.content}`).join("\n")}` : "";
|
|
|
724
1373
|
{ role: "user", content: message }
|
|
725
1374
|
];
|
|
726
1375
|
const tools = this.buildAITools(context);
|
|
727
|
-
|
|
1376
|
+
log7.info(
|
|
728
1377
|
{ userId: context.userId, messageLen: message.length, toolCount: Object.keys(tools).length },
|
|
729
1378
|
"Processing message"
|
|
730
1379
|
);
|
|
731
1380
|
try {
|
|
1381
|
+
const abort = new AbortController();
|
|
1382
|
+
const timer = setTimeout(() => abort.abort(), 12e4);
|
|
732
1383
|
const result = await generateText({
|
|
733
1384
|
model,
|
|
734
1385
|
messages,
|
|
735
1386
|
tools,
|
|
736
|
-
stopWhen: stepCountIs(
|
|
1387
|
+
stopWhen: stepCountIs(10),
|
|
1388
|
+
abortSignal: abort.signal
|
|
737
1389
|
});
|
|
1390
|
+
clearTimeout(timer);
|
|
738
1391
|
const toolCalls = result.steps?.flatMap(
|
|
739
1392
|
(step) => step.toolCalls?.map((tc) => ({
|
|
740
1393
|
tool: tc.toolName,
|
|
@@ -747,20 +1400,12 @@ ${memories.map((m) => `- ${m.content}`).join("\n")}` : "";
|
|
|
747
1400
|
}
|
|
748
1401
|
}))
|
|
749
1402
|
).filter(Boolean) ?? [];
|
|
750
|
-
if (result.text.length > 20) {
|
|
751
|
-
this.memory.add(`User: "${message.slice(0, 100)}" -> ${toolCalls.length} tools used`, {
|
|
752
|
-
userId: context.userId,
|
|
753
|
-
category: "pattern",
|
|
754
|
-
agentId: this.id
|
|
755
|
-
}).catch(() => {
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
1403
|
return {
|
|
759
1404
|
text: result.text,
|
|
760
1405
|
toolCalls
|
|
761
1406
|
};
|
|
762
1407
|
} catch (error) {
|
|
763
|
-
|
|
1408
|
+
log7.error({ error, userId: context.userId }, "Agent processing failed");
|
|
764
1409
|
writeAuditLog({
|
|
765
1410
|
userId: context.userId,
|
|
766
1411
|
agentId: this.id,
|
|
@@ -774,9 +1419,9 @@ ${memories.map((m) => `- ${m.content}`).join("\n")}` : "";
|
|
|
774
1419
|
}
|
|
775
1420
|
/**
|
|
776
1421
|
* Streaming version of process() — yields text chunks for real-time display.
|
|
777
|
-
*
|
|
1422
|
+
* Calls onStatus when tool calls happen so the UI can show activity.
|
|
778
1423
|
*/
|
|
779
|
-
processStream(message, context) {
|
|
1424
|
+
processStream(message, context, onStatus, callbacks, attachments) {
|
|
780
1425
|
const model = buildModel(this.llm);
|
|
781
1426
|
const memories = this.memory.search(message, { userId: context.userId, limit: 5 }).catch(() => []);
|
|
782
1427
|
const tools = this.buildAITools(context);
|
|
@@ -785,38 +1430,112 @@ ${memories.map((m) => `- ${m.content}`).join("\n")}` : "";
|
|
|
785
1430
|
const memoryContext = mems.length > 0 ? `
|
|
786
1431
|
|
|
787
1432
|
Relevant memories:
|
|
788
|
-
${mems.map((
|
|
1433
|
+
${mems.map((m2) => `- ${m2.content}`).join("\n")}` : "";
|
|
1434
|
+
const userContent = attachments?.length ? [
|
|
1435
|
+
...attachments.map((a) => ({
|
|
1436
|
+
type: "image",
|
|
1437
|
+
image: a.data ?? a.url,
|
|
1438
|
+
mimeType: a.mimeType
|
|
1439
|
+
})),
|
|
1440
|
+
{ type: "text", text: message }
|
|
1441
|
+
] : message;
|
|
789
1442
|
const messages = [
|
|
790
|
-
{ role: "system", content:
|
|
1443
|
+
{ role: "system", content: this.getSystemPrompt() + memoryContext },
|
|
791
1444
|
...context.history.map(
|
|
792
1445
|
(h) => ({
|
|
793
1446
|
role: h.role,
|
|
794
1447
|
content: h.content
|
|
795
1448
|
})
|
|
796
1449
|
),
|
|
797
|
-
{ role: "user", content:
|
|
1450
|
+
{ role: "user", content: userContent }
|
|
798
1451
|
];
|
|
799
|
-
|
|
800
|
-
|
|
1452
|
+
const totalMsgChars = messages.reduce((sum, m2) => sum + (typeof m2.content === "string" ? m2.content.length : JSON.stringify(m2.content).length), 0);
|
|
1453
|
+
const toolNames = Object.keys(tools);
|
|
1454
|
+
const toolSchemaChars = JSON.stringify(Object.fromEntries(
|
|
1455
|
+
Object.entries(tools).map(([k, v]) => [k, v.description ?? ""])
|
|
1456
|
+
)).length;
|
|
1457
|
+
log7.info(
|
|
1458
|
+
{
|
|
1459
|
+
userId: context.userId,
|
|
1460
|
+
messageLen: message.length,
|
|
1461
|
+
streaming: true,
|
|
1462
|
+
llmProvider: self.llm.provider,
|
|
1463
|
+
llmModel: self.llm.model,
|
|
1464
|
+
llmBaseUrl: self.llm.baseUrl,
|
|
1465
|
+
historyTurns: context.history.length,
|
|
1466
|
+
totalMsgChars,
|
|
1467
|
+
toolCount: toolNames.length,
|
|
1468
|
+
toolSchemaChars,
|
|
1469
|
+
approxTokens: Math.ceil(totalMsgChars / 3.5)
|
|
1470
|
+
},
|
|
801
1471
|
"Processing message (stream)"
|
|
802
1472
|
);
|
|
803
1473
|
return streamText({
|
|
804
1474
|
model,
|
|
805
1475
|
messages,
|
|
806
1476
|
tools,
|
|
807
|
-
stopWhen: stepCountIs(
|
|
1477
|
+
stopWhen: stepCountIs(10),
|
|
1478
|
+
onChunk({ chunk }) {
|
|
1479
|
+
if (chunk.type === "tool-call") {
|
|
1480
|
+
toolActive = true;
|
|
1481
|
+
const name = chunk.toolName ?? "";
|
|
1482
|
+
const args = chunk.args ?? chunk.input ?? {};
|
|
1483
|
+
log7.info({ tool: name, args: JSON.stringify(args).slice(0, 500) }, "Tool call");
|
|
1484
|
+
if (onStatus) {
|
|
1485
|
+
let label = TOOL_LABELS[name] ?? name;
|
|
1486
|
+
if (name === "dsers_product_import") {
|
|
1487
|
+
if (args.job_id && !args.source_url && !args.source_urls_json) {
|
|
1488
|
+
label = args.rules_json ? "Applying rules..." : "Refreshing preview...";
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
onStatus(label ?? "Working...");
|
|
1492
|
+
}
|
|
1493
|
+
if (callbacks?.onToolCall && chunk.toolCallId) {
|
|
1494
|
+
toolStartTimes.set(chunk.toolCallId, Date.now());
|
|
1495
|
+
callbacks.onToolCall(chunk.toolCallId, name, args);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
if (chunk.type === "tool-result") {
|
|
1499
|
+
toolActive = false;
|
|
1500
|
+
const name = chunk.toolName ?? "";
|
|
1501
|
+
const output = chunk.output ?? chunk.result;
|
|
1502
|
+
const resultStr = JSON.stringify(output ?? "").slice(0, 500);
|
|
1503
|
+
log7.info({ tool: name, resultSnippet: resultStr }, "Tool result");
|
|
1504
|
+
if (callbacks?.onToolResult && chunk.toolCallId) {
|
|
1505
|
+
const startTime = toolStartTimes.get(chunk.toolCallId) ?? Date.now();
|
|
1506
|
+
const durationMs = Date.now() - startTime;
|
|
1507
|
+
toolStartTimes.delete(chunk.toolCallId);
|
|
1508
|
+
callbacks.onToolResult(chunk.toolCallId, name, chunk.result, void 0, durationMs);
|
|
1509
|
+
}
|
|
1510
|
+
if (onStatus) onStatus("Thinking...");
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
808
1513
|
});
|
|
809
1514
|
});
|
|
1515
|
+
let toolActive = false;
|
|
1516
|
+
const toolStartTimes = /* @__PURE__ */ new Map();
|
|
810
1517
|
const textStream = {
|
|
811
1518
|
[Symbol.asyncIterator]() {
|
|
812
1519
|
let innerIterator = null;
|
|
1520
|
+
let gotFirstToken = false;
|
|
813
1521
|
return {
|
|
814
1522
|
async next() {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1523
|
+
try {
|
|
1524
|
+
if (!innerIterator) {
|
|
1525
|
+
const result = await withTimeout(streamPromise, 6e4, "LLM init");
|
|
1526
|
+
innerIterator = result.textStream[Symbol.asyncIterator]();
|
|
1527
|
+
}
|
|
1528
|
+
const timeout = gotFirstToken ? 6e4 : 12e4;
|
|
1529
|
+
const res = await withToolAwareTimeout(innerIterator.next(), timeout, () => toolActive, "LLM response");
|
|
1530
|
+
if (!res.done && res.value) {
|
|
1531
|
+
gotFirstToken = true;
|
|
1532
|
+
toolActive = false;
|
|
1533
|
+
}
|
|
1534
|
+
return res;
|
|
1535
|
+
} catch (err) {
|
|
1536
|
+
log7.error({ err, toolActive, gotFirstToken }, "Stream iterator error");
|
|
1537
|
+
throw err;
|
|
818
1538
|
}
|
|
819
|
-
return innerIterator.next();
|
|
820
1539
|
}
|
|
821
1540
|
};
|
|
822
1541
|
}
|
|
@@ -836,14 +1555,6 @@ ${mems.map((m) => `- ${m.content}`).join("\n")}` : "";
|
|
|
836
1555
|
}
|
|
837
1556
|
}))
|
|
838
1557
|
).filter(Boolean) ?? [];
|
|
839
|
-
if (text.length > 20) {
|
|
840
|
-
self.memory.add(`User: "${message.slice(0, 100)}" -> ${toolCalls.length} tools used`, {
|
|
841
|
-
userId: context.userId,
|
|
842
|
-
category: "pattern",
|
|
843
|
-
agentId: self.id
|
|
844
|
-
}).catch(() => {
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
1558
|
return {
|
|
848
1559
|
text,
|
|
849
1560
|
toolCalls
|
|
@@ -853,207 +1564,525 @@ ${mems.map((m) => `- ${m.content}`).join("\n")}` : "";
|
|
|
853
1564
|
}
|
|
854
1565
|
buildAITools(context) {
|
|
855
1566
|
const dsers = this.dsers;
|
|
1567
|
+
const svc = this.importService;
|
|
1568
|
+
const jmap = this.jobIdMap;
|
|
1569
|
+
const resolveJid = (id) => this.resolveJobId(id);
|
|
856
1570
|
const audit = (action, target, params, result = "pending") => writeAuditLog({ userId: context.userId, agentId: this.id, action, target, params, result });
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
execute: async () => {
|
|
862
|
-
audit("listStores", "dsers:account");
|
|
863
|
-
const result = await listStores(dsers);
|
|
864
|
-
audit("listStores", "dsers:account", void 0, "success");
|
|
865
|
-
return result;
|
|
866
|
-
}
|
|
867
|
-
}),
|
|
868
|
-
getUserInfo: aiTool({
|
|
869
|
-
description: "Get the user's DSers account info (email, name, plan).",
|
|
870
|
-
inputSchema: z2.object({}),
|
|
871
|
-
execute: async () => {
|
|
872
|
-
audit("getUserInfo", "dsers:account");
|
|
873
|
-
const result = await getUserInfo(dsers);
|
|
874
|
-
audit("getUserInfo", "dsers:account", void 0, "success");
|
|
875
|
-
return result;
|
|
876
|
-
}
|
|
877
|
-
}),
|
|
878
|
-
searchImportList: aiTool({
|
|
879
|
-
description: "Search the import list \u2014 products imported from suppliers but not yet pushed to stores.",
|
|
880
|
-
inputSchema: z2.object({
|
|
881
|
-
page: z2.number().optional().describe("Page number (default 1)"),
|
|
882
|
-
pageSize: z2.number().optional().describe("Items per page (default 20)"),
|
|
883
|
-
keyword: z2.string().optional().describe("Search keyword")
|
|
884
|
-
}),
|
|
885
|
-
execute: async (input) => {
|
|
886
|
-
audit("searchImportList", "dsers:product", input);
|
|
887
|
-
const result = await getImportList(dsers, input);
|
|
888
|
-
audit("searchImportList", "dsers:product", void 0, "success");
|
|
889
|
-
return result;
|
|
890
|
-
}
|
|
891
|
-
}),
|
|
892
|
-
importProduct: aiTool({
|
|
893
|
-
description: "Import a product from a supplier (AliExpress/Temu/1688) into the import list. Needs the supplier product ID and platform (1=AliExpress, 2=Temu, 3=1688).",
|
|
1571
|
+
const tools = {};
|
|
1572
|
+
if (svc) {
|
|
1573
|
+
tools.dsers_store_discover = aiTool({
|
|
1574
|
+
description: "Get connected stores, account info (plan/limits), and rule capabilities. Call first before import/push.",
|
|
894
1575
|
inputSchema: z2.object({
|
|
895
|
-
|
|
896
|
-
supplyAppId: z2.number().describe("1=AliExpress, 2=Temu, 3=1688"),
|
|
897
|
-
country: z2.string().default("US").describe("Target country")
|
|
1576
|
+
target_store: z2.string().optional().describe("Filter by store ID or name")
|
|
898
1577
|
}),
|
|
899
1578
|
execute: async (input) => {
|
|
900
|
-
audit("
|
|
901
|
-
const result = await
|
|
902
|
-
audit("
|
|
903
|
-
return result;
|
|
1579
|
+
audit("dsers_store_discover", "mcp");
|
|
1580
|
+
const result = await withTimeout(svc.getRuleCapabilities(input), 6e4, "Store discovery");
|
|
1581
|
+
audit("dsers_store_discover", "mcp", void 0, "success");
|
|
1582
|
+
return compactToolResult(result, jmap);
|
|
904
1583
|
}
|
|
905
|
-
})
|
|
906
|
-
|
|
907
|
-
description: "
|
|
1584
|
+
});
|
|
1585
|
+
tools.dsers_product_import = aiTool({
|
|
1586
|
+
description: "Import product by URL or re-apply rules to existing job. Modes: (1) source_url for new import, (2) job_id+rules_json to update rules, (3) job_id alone to refresh. Expired job_id \u2192 re-import with source_url. Returns: job_id, title (or title_before+title_after), sell_price, cost, variants, blocked, warnings.",
|
|
908
1587
|
inputSchema: z2.object({
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1588
|
+
source_url: z2.string().optional().describe("Supplier product URL"),
|
|
1589
|
+
source_urls_json: z2.string().optional().describe("Batch: JSON array of URLs"),
|
|
1590
|
+
job_id: z2.string().optional().describe("Existing job ID for rule updates"),
|
|
1591
|
+
source_hint: z2.string().optional().describe("auto|aliexpress|alibaba|accio"),
|
|
1592
|
+
country: z2.string().default("US").describe("Country code"),
|
|
1593
|
+
target_store: z2.string().optional().describe("Store ID or name"),
|
|
1594
|
+
visibility_mode: z2.string().optional().describe("backend_only|sell_immediately"),
|
|
1595
|
+
rules_json: z2.string().optional().describe(
|
|
1596
|
+
"JSON rules. Pricing: fixed_price({fixed_price:9.99}), multiplier({multiplier:2}), fixed_markup({fixed_markup:5}). Content: title_override, description_override_html. Images: keep_first_n, drop_indexes, add_urls. variant_overrides: [{match,sell_price,compare_at_price,stock}]. option_edits: [{action,option_name,value_name?,new_name?}]."
|
|
1597
|
+
)
|
|
912
1598
|
}),
|
|
913
1599
|
execute: async (input) => {
|
|
914
|
-
audit("
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
1600
|
+
audit("dsers_product_import", "mcp", input);
|
|
1601
|
+
const payload = {};
|
|
1602
|
+
if (input.job_id && !input.source_url && !input.source_urls_json) {
|
|
1603
|
+
payload.job_id = resolveJid(input.job_id);
|
|
1604
|
+
if (input.rules_json) {
|
|
1605
|
+
try {
|
|
1606
|
+
payload.rules = JSON.parse(input.rules_json);
|
|
1607
|
+
} catch {
|
|
1608
|
+
throw new Error(
|
|
1609
|
+
'Invalid JSON in rules_json. Expected: {"pricing":{"mode":"fixed_price","fixed_price":9.99}} or {"content":{"title_override":"New Title"}}'
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
} else {
|
|
1613
|
+
payload._keep_existing_rules = true;
|
|
1614
|
+
}
|
|
1615
|
+
if (input.target_store) payload.target_store = input.target_store;
|
|
1616
|
+
if (input.visibility_mode) payload.visibility_mode = input.visibility_mode;
|
|
1617
|
+
const result2 = await withTimeout(svc.reapplyRules(payload), 12e4, "Rule reapply");
|
|
1618
|
+
audit("dsers_product_import", "mcp", void 0, "success");
|
|
1619
|
+
return compactToolResult(result2, jmap);
|
|
1620
|
+
}
|
|
1621
|
+
if (input.source_url) payload.source_url = input.source_url;
|
|
1622
|
+
if (input.source_urls_json) {
|
|
1623
|
+
try {
|
|
1624
|
+
payload.source_urls = JSON.parse(input.source_urls_json);
|
|
1625
|
+
} catch {
|
|
1626
|
+
throw new Error("Invalid JSON in source_urls_json.");
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (input.source_hint) payload.source_hint = input.source_hint;
|
|
1630
|
+
if (input.country) payload.country = input.country;
|
|
1631
|
+
if (input.target_store) payload.target_store = input.target_store;
|
|
1632
|
+
payload.visibility_mode = input.visibility_mode || "backend_only";
|
|
1633
|
+
if (input.rules_json) {
|
|
1634
|
+
try {
|
|
1635
|
+
payload.rules = JSON.parse(input.rules_json);
|
|
1636
|
+
} catch {
|
|
1637
|
+
throw new Error("Invalid JSON in rules_json.");
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
const result = await withTimeout(svc.prepareImportCandidate(payload), 12e4, "Product import");
|
|
1641
|
+
audit("dsers_product_import", "mcp", void 0, "success");
|
|
1642
|
+
return compactToolResult(result, jmap);
|
|
918
1643
|
}
|
|
919
|
-
})
|
|
920
|
-
|
|
921
|
-
description: "
|
|
1644
|
+
});
|
|
1645
|
+
tools.dsers_product_preview = aiTool({
|
|
1646
|
+
description: "Reload preview for an imported job. Returns prices, variants, images.",
|
|
922
1647
|
inputSchema: z2.object({
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
storeId: z2.string().optional()
|
|
1648
|
+
job_id: z2.string().describe("Job ID from dsers_product_import"),
|
|
1649
|
+
variant_offset: z2.number().optional().describe("Start index for variant pagination"),
|
|
1650
|
+
variant_limit: z2.number().optional().describe("Max variants to return")
|
|
927
1651
|
}),
|
|
928
1652
|
execute: async (input) => {
|
|
929
|
-
audit("
|
|
930
|
-
const result = await
|
|
931
|
-
audit("
|
|
932
|
-
return result;
|
|
1653
|
+
audit("dsers_product_preview", "mcp", input);
|
|
1654
|
+
const result = await withTimeout(svc.getImportPreview({ ...input, job_id: resolveJid(input.job_id) }), 3e4, "Preview");
|
|
1655
|
+
audit("dsers_product_preview", "mcp", void 0, "success");
|
|
1656
|
+
return compactToolResult(result, jmap);
|
|
933
1657
|
}
|
|
934
|
-
})
|
|
935
|
-
|
|
936
|
-
description: "
|
|
1658
|
+
});
|
|
1659
|
+
tools.dsers_store_push = aiTool({
|
|
1660
|
+
description: "Push product(s) to store(s). Show confirmation checklist and get user approval first. May return blocked (shipping_profile, pricing_rule) or warnings. Use push_options_json with shipping_profile_name when store has multiple shipping profiles.",
|
|
937
1661
|
inputSchema: z2.object({
|
|
938
|
-
|
|
1662
|
+
job_id: z2.string().optional().describe("Job ID"),
|
|
1663
|
+
job_ids_json: z2.string().optional().describe("Batch: JSON array of job IDs"),
|
|
1664
|
+
target_store: z2.string().optional().describe("Store ID or name from dsers_store_discover"),
|
|
1665
|
+
target_stores_json: z2.string().optional().describe("Multi-store: JSON array of store names"),
|
|
1666
|
+
visibility_mode: z2.string().optional().describe("backend_only|sell_immediately"),
|
|
1667
|
+
force_push: z2.boolean().optional().describe("Override safety checks"),
|
|
1668
|
+
push_options_json: z2.string().optional().describe(
|
|
1669
|
+
'Push config JSON. Include shipping_profile_name when multiple profiles exist. E.g. {"shipping_profile_name":"General Profile","pricing_rule_behavior":"apply_store_pricing_rule"}'
|
|
1670
|
+
)
|
|
939
1671
|
}),
|
|
940
1672
|
execute: async (input) => {
|
|
941
|
-
audit("
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
1673
|
+
audit("dsers_store_push", "mcp", input);
|
|
1674
|
+
const payload = {};
|
|
1675
|
+
if (input.job_ids_json) {
|
|
1676
|
+
try {
|
|
1677
|
+
const ids = JSON.parse(input.job_ids_json);
|
|
1678
|
+
payload.job_ids = ids.map(resolveJid);
|
|
1679
|
+
} catch {
|
|
1680
|
+
throw new Error("Invalid JSON in job_ids_json");
|
|
1681
|
+
}
|
|
1682
|
+
} else if (input.job_id) {
|
|
1683
|
+
payload.job_id = resolveJid(input.job_id);
|
|
1684
|
+
}
|
|
1685
|
+
if (input.target_store) payload.target_store = input.target_store;
|
|
1686
|
+
if (input.target_stores_json) {
|
|
1687
|
+
try {
|
|
1688
|
+
payload.target_stores = JSON.parse(input.target_stores_json);
|
|
1689
|
+
} catch {
|
|
1690
|
+
throw new Error("Invalid JSON in target_stores_json");
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
if (input.visibility_mode) payload.visibility_mode = input.visibility_mode;
|
|
1694
|
+
if (input.force_push) payload.force_push = true;
|
|
1695
|
+
if (input.push_options_json) {
|
|
1696
|
+
try {
|
|
1697
|
+
payload.push_options = JSON.parse(input.push_options_json);
|
|
1698
|
+
} catch {
|
|
1699
|
+
throw new Error("Invalid JSON in push_options_json");
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
const result = await withTimeout(svc.confirmPushToStore(payload), 18e4, "Push to store");
|
|
1703
|
+
audit("dsers_store_push", "mcp", void 0, "success");
|
|
1704
|
+
return compactToolResult(result, jmap);
|
|
945
1705
|
}
|
|
946
|
-
})
|
|
947
|
-
|
|
948
|
-
description: "
|
|
1706
|
+
});
|
|
1707
|
+
tools.dsers_rules_validate = aiTool({
|
|
1708
|
+
description: "Validate pricing/content/image rules before importing.",
|
|
1709
|
+
inputSchema: z2.object({
|
|
1710
|
+
rules_json: z2.string().describe("Rules as JSON string"),
|
|
1711
|
+
target_store: z2.string().optional().describe("Store ID or name")
|
|
1712
|
+
}),
|
|
1713
|
+
execute: async (input) => {
|
|
1714
|
+
audit("dsers_rules_validate", "mcp");
|
|
1715
|
+
let rules;
|
|
1716
|
+
try {
|
|
1717
|
+
rules = JSON.parse(input.rules_json);
|
|
1718
|
+
} catch {
|
|
1719
|
+
throw new Error("Invalid JSON in rules_json");
|
|
1720
|
+
}
|
|
1721
|
+
const result = await withTimeout(svc.validateRules({ rules, target_store: input.target_store ?? null }), 3e4, "Rule validation");
|
|
1722
|
+
audit("dsers_rules_validate", "mcp", void 0, "success");
|
|
1723
|
+
return compactToolResult(result, jmap);
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
tools.dsers_job_status = aiTool({
|
|
1727
|
+
description: "Check job status: preview_ready \u2192 push_requested \u2192 completed/failed.",
|
|
1728
|
+
inputSchema: z2.object({
|
|
1729
|
+
job_id: z2.string().describe("Job ID")
|
|
1730
|
+
}),
|
|
1731
|
+
execute: async (input) => {
|
|
1732
|
+
audit("dsers_job_status", "mcp", input);
|
|
1733
|
+
const result = await withTimeout(svc.getJobStatus({ ...input, job_id: resolveJid(input.job_id) }), 3e4, "Job status");
|
|
1734
|
+
audit("dsers_job_status", "mcp", void 0, "success");
|
|
1735
|
+
return compactToolResult(result, jmap);
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
tools.dsers_product_visibility = aiTool({
|
|
1739
|
+
description: "Set job visibility: backend_only (draft) or sell_immediately (live).",
|
|
1740
|
+
inputSchema: z2.object({
|
|
1741
|
+
job_id: z2.string().describe("Job ID"),
|
|
1742
|
+
visibility_mode: z2.string().describe("backend_only|sell_immediately")
|
|
1743
|
+
}),
|
|
1744
|
+
execute: async (input) => {
|
|
1745
|
+
audit("dsers_product_visibility", "mcp", input);
|
|
1746
|
+
const result = await withTimeout(svc.setProductVisibility({ ...input, job_id: resolveJid(input.job_id) }), 3e4, "Set visibility");
|
|
1747
|
+
audit("dsers_product_visibility", "mcp", void 0, "success");
|
|
1748
|
+
return compactToolResult(result, jmap);
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
tools.dsers_product_delete = aiTool({
|
|
1752
|
+
description: "Delete product from import list. Call without confirm first, then with confirm=true after user approves.",
|
|
1753
|
+
inputSchema: z2.object({
|
|
1754
|
+
import_item_id: z2.string().describe("Item ID from dsers_import_list_search"),
|
|
1755
|
+
confirm: z2.boolean().optional().describe("true only after user approves")
|
|
1756
|
+
}),
|
|
1757
|
+
execute: async (input) => {
|
|
1758
|
+
audit("dsers_product_delete", "mcp", input);
|
|
1759
|
+
const result = await withTimeout(svc.deleteImportItem(input), 3e4, "Delete import item");
|
|
1760
|
+
audit("dsers_product_delete", "mcp", void 0, "success");
|
|
1761
|
+
return compactToolResult(result, jmap);
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
} else {
|
|
1765
|
+
log7.warn("MCP ImportFlowService not available \u2014 no MCP tools registered");
|
|
1766
|
+
}
|
|
1767
|
+
if (this.authCallback) {
|
|
1768
|
+
tools.dsers_authorize = aiTool({
|
|
1769
|
+
description: "Open browser for DSers login. Call when DSers ops fail with auth/401/session errors.",
|
|
949
1770
|
inputSchema: z2.object({}),
|
|
950
1771
|
execute: async () => {
|
|
951
|
-
|
|
952
|
-
const result = await getGlobalSettings(dsers);
|
|
953
|
-
audit("getGlobalSettings", "dsers:settings", void 0, "success");
|
|
1772
|
+
const result = await withTimeout(this.authCallback(), 2e5, "DSers authorization");
|
|
954
1773
|
return result;
|
|
955
1774
|
}
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
tools.dsers_import_list_search = aiTool({
|
|
1778
|
+
description: "Search import list. Returns items with id, source_url, cost_range, sell_price_range. Use source_url with dsers_product_import for modifications.",
|
|
1779
|
+
inputSchema: z2.object({
|
|
1780
|
+
page: z2.number().optional().describe("Page number (default 1)"),
|
|
1781
|
+
pageSize: z2.number().optional().describe("Items per page (default 20)"),
|
|
1782
|
+
keyword: z2.string().optional().describe("Search keyword")
|
|
956
1783
|
}),
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1784
|
+
execute: async (input) => {
|
|
1785
|
+
audit("dsers_import_list_search", "dsers:product", input);
|
|
1786
|
+
const result = await withTimeout(getImportList(dsers, input), 3e4, "Search import list");
|
|
1787
|
+
audit("dsers_import_list_search", "dsers:product", void 0, "success");
|
|
1788
|
+
const items = result?.data?.list ?? result?.data ?? [];
|
|
1789
|
+
if (Array.isArray(items) && items.length > 0) {
|
|
1790
|
+
const enriched = await Promise.allSettled(
|
|
1791
|
+
items.slice(0, 10).map(async (item) => {
|
|
1792
|
+
const itemId = item.id ?? item.importListId;
|
|
1793
|
+
if (!itemId) return item;
|
|
1794
|
+
try {
|
|
1795
|
+
const detail = await withTimeout(
|
|
1796
|
+
getImportListItem(dsers, String(itemId)),
|
|
1797
|
+
15e3,
|
|
1798
|
+
"Item detail"
|
|
1799
|
+
);
|
|
1800
|
+
const detailData = detail?.data ?? detail;
|
|
1801
|
+
const variants = detailData?.variants ?? detailData?.skuList ?? detailData?.variantList ?? [];
|
|
1802
|
+
if (!Array.isArray(variants) || !variants.length) return item;
|
|
1803
|
+
const sellPrices = [];
|
|
1804
|
+
const costPrices = [];
|
|
1805
|
+
for (const v of variants) {
|
|
1806
|
+
const rawSell = Number(v.sellPrice ?? v.salePrice ?? v.price);
|
|
1807
|
+
const rawCost = Number(v.supplierPrice ?? v.buyPrice ?? v.cost);
|
|
1808
|
+
const sell = rawSell > 100 ? rawSell / 100 : rawSell;
|
|
1809
|
+
const cost = rawCost > 100 ? rawCost / 100 : rawCost;
|
|
1810
|
+
if (Number.isFinite(sell) && sell > 0) sellPrices.push(sell);
|
|
1811
|
+
if (Number.isFinite(cost) && cost > 0) costPrices.push(cost);
|
|
1812
|
+
}
|
|
1813
|
+
const enrichedItem = { ...item };
|
|
1814
|
+
const supplyProductId = detailData?.supplyProductId ?? item?.supplyProductId;
|
|
1815
|
+
const supplyAppId = Number(detailData?.supplyAppId ?? item?.supplyAppId ?? 1);
|
|
1816
|
+
if (supplyProductId) {
|
|
1817
|
+
if (supplyAppId === 2) {
|
|
1818
|
+
enrichedItem.source_url = `https://www.temu.com/product/${supplyProductId}.html`;
|
|
1819
|
+
} else if (supplyAppId === 3) {
|
|
1820
|
+
enrichedItem.source_url = `https://detail.1688.com/offer/${supplyProductId}.html`;
|
|
1821
|
+
} else {
|
|
1822
|
+
enrichedItem.source_url = `https://www.aliexpress.com/item/${supplyProductId}.html`;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
if (costPrices.length) {
|
|
1826
|
+
enrichedItem.cost_range = `$${Math.min(...costPrices).toFixed(2)} \u2013 $${Math.max(...costPrices).toFixed(2)}`;
|
|
1827
|
+
}
|
|
1828
|
+
if (sellPrices.length) {
|
|
1829
|
+
enrichedItem.sell_price_range = `$${Math.min(...sellPrices).toFixed(2)} \u2013 $${Math.max(...sellPrices).toFixed(2)}`;
|
|
1830
|
+
enrichedItem.no_markup = sellPrices.every((s, i) => Math.abs(s - (costPrices[i] ?? s)) < 0.01);
|
|
1831
|
+
}
|
|
1832
|
+
enrichedItem.variant_count = variants.length;
|
|
1833
|
+
let totalStock = 0;
|
|
1834
|
+
const lowStockVariants = [];
|
|
1835
|
+
for (const v of variants) {
|
|
1836
|
+
const stock = Number(v.stock ?? v.quantity ?? v.inventory ?? 0);
|
|
1837
|
+
const safeStock = Number.isFinite(stock) ? stock : 0;
|
|
1838
|
+
totalStock += safeStock;
|
|
1839
|
+
if (safeStock <= 5) {
|
|
1840
|
+
lowStockVariants.push({
|
|
1841
|
+
id: String(v.id ?? v.variantId ?? v.skuId ?? ""),
|
|
1842
|
+
sku: String(v.sku ?? v.skuAttr ?? v.optionName ?? v.title ?? ""),
|
|
1843
|
+
stock: safeStock
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
enrichedItem.total_stock = totalStock;
|
|
1848
|
+
if (lowStockVariants.length > 0) {
|
|
1849
|
+
enrichedItem.low_stock_variants = lowStockVariants;
|
|
1850
|
+
enrichedItem.low_stock_warning = `${lowStockVariants.length} of ${variants.length} variants have stock \u2264 5`;
|
|
1851
|
+
}
|
|
1852
|
+
return enrichedItem;
|
|
1853
|
+
} catch (err) {
|
|
1854
|
+
log7.warn({ err, itemId }, "Failed to enrich import list item");
|
|
1855
|
+
return item;
|
|
1856
|
+
}
|
|
1857
|
+
})
|
|
1858
|
+
);
|
|
1859
|
+
const enrichedItems = enriched.map((r) => r.status === "fulfilled" ? r.value : items[0]);
|
|
1860
|
+
const cleanItems = enrichedItems.map((item) => ({
|
|
1861
|
+
id: item.id ?? item.importListId,
|
|
1862
|
+
title: item.title ?? "(untitled)",
|
|
1863
|
+
source_url: item.source_url,
|
|
1864
|
+
cost_range: item.cost_range,
|
|
1865
|
+
sell_price_range: item.sell_price_range,
|
|
1866
|
+
no_markup: item.no_markup,
|
|
1867
|
+
variant_count: item.variant_count,
|
|
1868
|
+
total_stock: item.total_stock,
|
|
1869
|
+
low_stock_warning: item.low_stock_warning,
|
|
1870
|
+
low_stock_variants: item.low_stock_variants
|
|
1871
|
+
}));
|
|
1872
|
+
return { items: cleanItems, total: result?.data?.total ?? cleanItems.length };
|
|
965
1873
|
}
|
|
966
|
-
|
|
967
|
-
|
|
1874
|
+
const rawItems = result?.data ?? [];
|
|
1875
|
+
if (Array.isArray(rawItems)) {
|
|
1876
|
+
return { items: rawItems.map((item) => ({
|
|
1877
|
+
id: item.id ?? item.importListId,
|
|
1878
|
+
title: item.title ?? "(untitled)"
|
|
1879
|
+
})), total: rawItems.length };
|
|
1880
|
+
}
|
|
1881
|
+
return compactToolResult(result);
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
tools.dsers_my_products = aiTool({
|
|
1885
|
+
description: "Get products already pushed to stores.",
|
|
1886
|
+
inputSchema: z2.object({
|
|
1887
|
+
page: z2.number().optional(),
|
|
1888
|
+
pageSize: z2.number().optional(),
|
|
1889
|
+
keyword: z2.string().optional(),
|
|
1890
|
+
storeId: z2.string().optional()
|
|
1891
|
+
}),
|
|
1892
|
+
execute: async (input) => {
|
|
1893
|
+
audit("dsers_my_products", "dsers:product", input);
|
|
1894
|
+
const result = await withTimeout(getMyProducts(dsers, input), 3e4, "My products");
|
|
1895
|
+
audit("dsers_my_products", "dsers:product", void 0, "success");
|
|
1896
|
+
return compactToolResult(result);
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
return tools;
|
|
968
1900
|
}
|
|
969
1901
|
};
|
|
970
1902
|
|
|
1903
|
+
// src/gateway/gateway.ts
|
|
1904
|
+
import { MemoryJobStore as MemoryJobStore2 } from "@lofder/dsers-mcp-product/dist/job-store-memory.js";
|
|
1905
|
+
|
|
971
1906
|
// src/web/server.ts
|
|
1907
|
+
init_logger();
|
|
972
1908
|
import { createServer } from "http";
|
|
973
|
-
import { readFileSync as
|
|
974
|
-
import { join as
|
|
1909
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, existsSync as existsSync7, statSync, readdirSync } from "fs";
|
|
1910
|
+
import { join as join7, dirname as dirname2, extname } from "path";
|
|
975
1911
|
import { fileURLToPath } from "url";
|
|
976
|
-
import {
|
|
977
|
-
import {
|
|
978
|
-
|
|
979
|
-
|
|
1912
|
+
import { homedir as homedir3 } from "os";
|
|
1913
|
+
import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
|
|
1914
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
1915
|
+
var log8 = createLogger("channel:web");
|
|
1916
|
+
var DEFAULT_WEB_USER = "web-default";
|
|
1917
|
+
var MIME_TYPES = {
|
|
1918
|
+
".html": "text/html; charset=utf-8",
|
|
1919
|
+
".js": "application/javascript; charset=utf-8",
|
|
1920
|
+
".css": "text/css; charset=utf-8",
|
|
1921
|
+
".json": "application/json",
|
|
1922
|
+
".svg": "image/svg+xml",
|
|
1923
|
+
".png": "image/png",
|
|
1924
|
+
".ico": "image/x-icon",
|
|
1925
|
+
".woff2": "font/woff2",
|
|
1926
|
+
".woff": "font/woff"
|
|
1927
|
+
};
|
|
1928
|
+
function findWebDir() {
|
|
980
1929
|
const thisDir = dirname2(fileURLToPath(import.meta.url));
|
|
981
1930
|
const candidates = [
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1931
|
+
join7(thisDir, "web"),
|
|
1932
|
+
join7(thisDir),
|
|
1933
|
+
join7(thisDir, "..", "web"),
|
|
1934
|
+
join7(thisDir, "..", "dist", "web"),
|
|
1935
|
+
join7(thisDir, "..", "..", "dist", "web")
|
|
987
1936
|
];
|
|
988
|
-
for (const
|
|
989
|
-
if (
|
|
990
|
-
return readFileSync4(p, "utf-8");
|
|
991
|
-
}
|
|
1937
|
+
for (const d of candidates) {
|
|
1938
|
+
if (existsSync7(join7(d, "index.html"))) return d;
|
|
992
1939
|
}
|
|
993
1940
|
throw new Error(
|
|
994
|
-
|
|
1941
|
+
`Web build not found (index.html). Looked in:
|
|
1942
|
+
` + candidates.join("\n")
|
|
995
1943
|
);
|
|
996
1944
|
}
|
|
997
|
-
var WebChatServer = class {
|
|
1945
|
+
var WebChatServer = class _WebChatServer {
|
|
998
1946
|
name = "web";
|
|
999
1947
|
server = null;
|
|
1000
1948
|
wss = null;
|
|
1001
1949
|
handler = null;
|
|
1002
1950
|
connectHandler = null;
|
|
1951
|
+
settingsHandler = null;
|
|
1003
1952
|
connections = /* @__PURE__ */ new Map();
|
|
1004
1953
|
_connected = false;
|
|
1005
1954
|
port = 3e3;
|
|
1006
1955
|
get connected() {
|
|
1007
1956
|
return this._connected;
|
|
1008
1957
|
}
|
|
1958
|
+
get actualPort() {
|
|
1959
|
+
return this.port;
|
|
1960
|
+
}
|
|
1009
1961
|
async connect(config) {
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
return;
|
|
1962
|
+
const basePort = config["port"] ?? 3e3;
|
|
1963
|
+
const webDir = findWebDir();
|
|
1964
|
+
const maxAttempts = 10;
|
|
1965
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1966
|
+
const tryPort = basePort + attempt;
|
|
1967
|
+
const ok = await this.tryListen(tryPort, webDir);
|
|
1968
|
+
if (ok) return;
|
|
1969
|
+
if (attempt < maxAttempts - 1) {
|
|
1970
|
+
log8.warn({ port: tryPort }, "Port in use, trying next");
|
|
1020
1971
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1972
|
+
}
|
|
1973
|
+
throw new Error(`No available port found (tried ${basePort}-${basePort + maxAttempts - 1})`);
|
|
1974
|
+
}
|
|
1975
|
+
tryListen(tryPort, webDir) {
|
|
1976
|
+
return new Promise((resolve) => {
|
|
1977
|
+
const indexHtml = readFileSync5(join7(webDir, "index.html"), "utf-8");
|
|
1978
|
+
const srv = createServer((req, res) => {
|
|
1979
|
+
if (req.url === "/health") {
|
|
1980
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1981
|
+
res.end(JSON.stringify({ ok: true, clients: this.connections.size }));
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
const urlPath = req.url?.split("?")[0] ?? "/";
|
|
1985
|
+
if (urlPath.startsWith("/uploads/")) {
|
|
1986
|
+
const uploadsDir = join7(homedir3(), ".dsclaw", "uploads");
|
|
1987
|
+
const fileName = urlPath.slice("/uploads/".length);
|
|
1988
|
+
if (/^[a-zA-Z0-9._-]+$/.test(fileName)) {
|
|
1989
|
+
const filePath2 = join7(uploadsDir, fileName);
|
|
1990
|
+
if (existsSync7(filePath2) && statSync(filePath2).isFile()) {
|
|
1991
|
+
const ext = extname(filePath2);
|
|
1992
|
+
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1993
|
+
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "public, max-age=86400" });
|
|
1994
|
+
res.end(readFileSync5(filePath2));
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
res.writeHead(404);
|
|
1999
|
+
res.end("Not found");
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
if (urlPath !== "/" && urlPath !== "/index.html") {
|
|
2003
|
+
const filePath2 = join7(webDir, urlPath);
|
|
2004
|
+
if (existsSync7(filePath2) && statSync(filePath2).isFile()) {
|
|
2005
|
+
const ext = extname(filePath2);
|
|
2006
|
+
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
2007
|
+
const cacheHeader = urlPath.includes("/assets/") ? "public, max-age=31536000, immutable" : "no-cache";
|
|
2008
|
+
res.writeHead(200, { "Content-Type": contentType, "Cache-Control": cacheHeader });
|
|
2009
|
+
res.end(readFileSync5(filePath2));
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
|
|
2014
|
+
res.end(indexHtml);
|
|
2015
|
+
});
|
|
2016
|
+
srv.once("error", (err) => {
|
|
2017
|
+
if (err.code === "EADDRINUSE") {
|
|
2018
|
+
resolve(false);
|
|
2019
|
+
} else {
|
|
2020
|
+
throw err;
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
srv.listen(tryPort, () => {
|
|
2024
|
+
this.server = srv;
|
|
2025
|
+
this.port = tryPort;
|
|
2026
|
+
this._connected = true;
|
|
2027
|
+
this.setupWebSocket();
|
|
2028
|
+
log8.info({ port: tryPort }, "Web chat server started");
|
|
2029
|
+
resolve(true);
|
|
2030
|
+
});
|
|
1028
2031
|
});
|
|
2032
|
+
}
|
|
2033
|
+
setupWebSocket() {
|
|
1029
2034
|
this.wss = new WebSocketServer({ server: this.server, path: "/ws" });
|
|
1030
|
-
this.wss.on("connection", (ws,
|
|
1031
|
-
|
|
1032
|
-
let clientId = url.searchParams.get("userId") || uuid();
|
|
2035
|
+
this.wss.on("connection", (ws, _req) => {
|
|
2036
|
+
let clientId = DEFAULT_WEB_USER;
|
|
1033
2037
|
this.wsSend(ws, { type: "session", userId: clientId });
|
|
1034
2038
|
this.connections.set(clientId, ws);
|
|
1035
|
-
|
|
2039
|
+
log8.info({ userId: clientId }, "Web client connected");
|
|
1036
2040
|
if (this.connectHandler) {
|
|
1037
2041
|
try {
|
|
1038
2042
|
this.connectHandler(clientId);
|
|
1039
2043
|
} catch (err) {
|
|
1040
|
-
|
|
2044
|
+
log8.error({ error: err, userId: clientId }, "Connect handler failed");
|
|
1041
2045
|
}
|
|
1042
2046
|
}
|
|
2047
|
+
try {
|
|
2048
|
+
const files = this.buildFileTree();
|
|
2049
|
+
this.wsSend(ws, { type: "file_list", files });
|
|
2050
|
+
} catch (err) {
|
|
2051
|
+
log8.warn({ error: err }, "Failed to push initial file_list");
|
|
2052
|
+
}
|
|
1043
2053
|
ws.on("message", async (raw) => {
|
|
1044
2054
|
try {
|
|
1045
2055
|
const msg = JSON.parse(raw.toString());
|
|
1046
|
-
if (msg["userId"]) clientId = msg["userId"];
|
|
1047
2056
|
this.connections.set(clientId, ws);
|
|
1048
|
-
if (!this.handler) return;
|
|
1049
2057
|
const msgType = msg["type"];
|
|
2058
|
+
if (msgType === "get_settings" || msgType === "start_dsers_auth" || msgType === "cancel_dsers_auth" || msgType === "save_dsers_session" || msgType === "save_llm" || msgType === "disconnect_dsers" || msgType === "set_language") {
|
|
2059
|
+
log8.info({ userId: clientId, action: msgType }, "Settings message received");
|
|
2060
|
+
if (this.settingsHandler) {
|
|
2061
|
+
await this.settingsHandler(clientId, msgType, msg);
|
|
2062
|
+
}
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
if (msgType === "list_files") {
|
|
2066
|
+
const files = this.buildFileTree();
|
|
2067
|
+
this.wsSend(ws, { type: "file_list", files });
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
if (msgType === "read_file") {
|
|
2071
|
+
const filePath2 = msg["path"];
|
|
2072
|
+
this.handleReadFile(ws, filePath2);
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
if (!this.handler) return;
|
|
1050
2076
|
if (msgType === "message" || msgType === "callback") {
|
|
2077
|
+
const rawAttachments = msg["attachments"];
|
|
2078
|
+
const attachments = rawAttachments?.length ? this.saveAttachments(rawAttachments) : void 0;
|
|
1051
2079
|
const normalized = {
|
|
1052
|
-
traceId:
|
|
2080
|
+
traceId: nanoid2(),
|
|
1053
2081
|
channelId: "web",
|
|
1054
2082
|
channelName: "Web Chat",
|
|
1055
2083
|
userId: clientId,
|
|
1056
2084
|
text: msgType === "callback" ? msg["data"] : msg["text"],
|
|
2085
|
+
attachments,
|
|
1057
2086
|
timestamp: /* @__PURE__ */ new Date(),
|
|
1058
2087
|
metadata: {
|
|
1059
2088
|
isCallback: msgType === "callback"
|
|
@@ -1062,24 +2091,16 @@ var WebChatServer = class {
|
|
|
1062
2091
|
await this.handler(normalized);
|
|
1063
2092
|
}
|
|
1064
2093
|
} catch (error) {
|
|
1065
|
-
|
|
2094
|
+
log8.error({ error }, "WS message handler failed");
|
|
1066
2095
|
}
|
|
1067
2096
|
});
|
|
1068
2097
|
ws.on("close", () => {
|
|
1069
2098
|
this.connections.delete(clientId);
|
|
1070
|
-
|
|
2099
|
+
log8.debug({ userId: clientId }, "Web client disconnected");
|
|
1071
2100
|
});
|
|
1072
2101
|
ws.on("error", (err) => {
|
|
1073
|
-
|
|
1074
|
-
});
|
|
1075
|
-
});
|
|
1076
|
-
return new Promise((resolve, reject) => {
|
|
1077
|
-
this.server.listen(this.port, () => {
|
|
1078
|
-
this._connected = true;
|
|
1079
|
-
log7.info({ port: this.port }, "Web chat server started");
|
|
1080
|
-
resolve();
|
|
2102
|
+
log8.warn({ error: err.message, userId: clientId }, "WS error");
|
|
1081
2103
|
});
|
|
1082
|
-
this.server.on("error", reject);
|
|
1083
2104
|
});
|
|
1084
2105
|
}
|
|
1085
2106
|
async disconnect() {
|
|
@@ -1093,7 +2114,7 @@ var WebChatServer = class {
|
|
|
1093
2114
|
else resolve();
|
|
1094
2115
|
});
|
|
1095
2116
|
this._connected = false;
|
|
1096
|
-
|
|
2117
|
+
log8.info("Web chat server stopped");
|
|
1097
2118
|
}
|
|
1098
2119
|
async reconnect() {
|
|
1099
2120
|
await this.disconnect();
|
|
@@ -1105,6 +2126,9 @@ var WebChatServer = class {
|
|
|
1105
2126
|
onConnect(handler) {
|
|
1106
2127
|
this.connectHandler = handler;
|
|
1107
2128
|
}
|
|
2129
|
+
onSettings(handler) {
|
|
2130
|
+
this.settingsHandler = handler;
|
|
2131
|
+
}
|
|
1108
2132
|
async send(targetUserId, message) {
|
|
1109
2133
|
if (message.card) {
|
|
1110
2134
|
await this.sendCard(targetUserId, message.card);
|
|
@@ -1114,6 +2138,9 @@ var WebChatServer = class {
|
|
|
1114
2138
|
this.wsSendTo(targetUserId, { type: "message", text: message.text });
|
|
1115
2139
|
}
|
|
1116
2140
|
}
|
|
2141
|
+
sendToast(targetUserId, text, level = "info", id) {
|
|
2142
|
+
this.wsSendTo(targetUserId, { type: "toast", text, level, ...id && { id } });
|
|
2143
|
+
}
|
|
1117
2144
|
async sendCard(targetUserId, card) {
|
|
1118
2145
|
this.wsSendTo(targetUserId, {
|
|
1119
2146
|
type: "card",
|
|
@@ -1138,28 +2165,154 @@ var WebChatServer = class {
|
|
|
1138
2165
|
sendTyping(userId, active) {
|
|
1139
2166
|
this.wsSendTo(userId, { type: "typing", active });
|
|
1140
2167
|
}
|
|
2168
|
+
sendStatus(userId, status) {
|
|
2169
|
+
this.wsSendTo(userId, { type: "status", status });
|
|
2170
|
+
}
|
|
2171
|
+
sendReset(userId) {
|
|
2172
|
+
this.wsSendTo(userId, { type: "reset" });
|
|
2173
|
+
}
|
|
1141
2174
|
sendClearInput(userId) {
|
|
1142
2175
|
this.wsSendTo(userId, { type: "clear_input" });
|
|
1143
2176
|
}
|
|
1144
2177
|
sendSetInputType(userId, inputType) {
|
|
1145
2178
|
this.wsSendTo(userId, { type: "set_input_type", inputType });
|
|
1146
2179
|
}
|
|
2180
|
+
sendSettingsState(userId, state) {
|
|
2181
|
+
this.wsSendTo(userId, { type: "settings_state", ...state });
|
|
2182
|
+
}
|
|
2183
|
+
sendSettingsResult(userId, result) {
|
|
2184
|
+
this.wsSendTo(userId, { type: "settings_result", ...result });
|
|
2185
|
+
}
|
|
2186
|
+
// ─── Tool Call Events ────────────────────────────────────────
|
|
2187
|
+
sendToolCallStart(userId, id, name, args) {
|
|
2188
|
+
this.wsSendTo(userId, { type: "tool_call_start", id, name, args });
|
|
2189
|
+
}
|
|
2190
|
+
sendToolCallEnd(userId, id, name, result, error, durationMs) {
|
|
2191
|
+
this.wsSendTo(userId, { type: "tool_call_end", id, name, result, error, durationMs });
|
|
2192
|
+
}
|
|
2193
|
+
// ─── Log & Init Progress ────────────────────────────────────
|
|
2194
|
+
sendLog(userId, entry) {
|
|
2195
|
+
this.wsSendTo(userId, { type: "log", ...entry });
|
|
2196
|
+
}
|
|
2197
|
+
sendInitProgress(userId, step) {
|
|
2198
|
+
this.wsSendTo(userId, { type: "init_progress", ...step });
|
|
2199
|
+
}
|
|
2200
|
+
// ─── Attachment Storage ────────────────────────────────────────
|
|
2201
|
+
saveAttachments(rawAttachments) {
|
|
2202
|
+
const uploadsDir = join7(homedir3(), ".dsclaw", "uploads");
|
|
2203
|
+
mkdirSync6(uploadsDir, { recursive: true });
|
|
2204
|
+
return rawAttachments.map((a) => {
|
|
2205
|
+
const ext = a.mimeType.split("/")[1]?.replace("jpeg", "jpg") ?? "png";
|
|
2206
|
+
const fileName = `${nanoid2()}.${ext}`;
|
|
2207
|
+
const filePath2 = join7(uploadsDir, fileName);
|
|
2208
|
+
const buffer = Buffer.from(a.data, "base64");
|
|
2209
|
+
writeFileSync5(filePath2, buffer);
|
|
2210
|
+
log8.info({ fileName, size: buffer.length }, "Saved attachment");
|
|
2211
|
+
return {
|
|
2212
|
+
type: "image",
|
|
2213
|
+
url: `/uploads/${fileName}`,
|
|
2214
|
+
data: buffer,
|
|
2215
|
+
mimeType: a.mimeType,
|
|
2216
|
+
fileName: a.name
|
|
2217
|
+
};
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
// ─── File Browsing ───────────────────────────────────────────
|
|
2221
|
+
buildFileTree() {
|
|
2222
|
+
const configDir = join7(homedir3(), ".dsclaw");
|
|
2223
|
+
if (!existsSync7(configDir)) return [];
|
|
2224
|
+
const scanDir = (dirPath, depth = 0) => {
|
|
2225
|
+
if (depth > 5) return [];
|
|
2226
|
+
try {
|
|
2227
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
2228
|
+
return entries.filter((e) => !e.name.startsWith(".")).sort((a, b) => {
|
|
2229
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
2230
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
2231
|
+
return a.name.localeCompare(b.name);
|
|
2232
|
+
}).map((entry) => {
|
|
2233
|
+
const fullPath = join7(dirPath, entry.name);
|
|
2234
|
+
if (entry.isDirectory()) {
|
|
2235
|
+
return {
|
|
2236
|
+
name: entry.name,
|
|
2237
|
+
path: fullPath,
|
|
2238
|
+
type: "dir",
|
|
2239
|
+
children: scanDir(fullPath, depth + 1)
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
let size;
|
|
2243
|
+
try {
|
|
2244
|
+
size = statSync(fullPath).size;
|
|
2245
|
+
} catch {
|
|
2246
|
+
}
|
|
2247
|
+
return { name: entry.name, path: fullPath, type: "file", size };
|
|
2248
|
+
});
|
|
2249
|
+
} catch {
|
|
2250
|
+
return [];
|
|
2251
|
+
}
|
|
2252
|
+
};
|
|
2253
|
+
return scanDir(configDir);
|
|
2254
|
+
}
|
|
2255
|
+
static IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
|
|
2256
|
+
static IMAGE_MIME = {
|
|
2257
|
+
".png": "image/png",
|
|
2258
|
+
".jpg": "image/jpeg",
|
|
2259
|
+
".jpeg": "image/jpeg",
|
|
2260
|
+
".gif": "image/gif",
|
|
2261
|
+
".webp": "image/webp",
|
|
2262
|
+
".svg": "image/svg+xml"
|
|
2263
|
+
};
|
|
2264
|
+
handleReadFile(ws, filePath2) {
|
|
2265
|
+
const configDir = join7(homedir3(), ".dsclaw");
|
|
2266
|
+
if (!filePath2.startsWith(configDir)) {
|
|
2267
|
+
this.wsSend(ws, { type: "file_content", path: filePath2, content: "[\u8BBF\u95EE\u88AB\u62D2\u7EDD\uFF1A\u53EA\u80FD\u8BFB\u53D6 ~/.dsclaw/ \u4E0B\u7684\u6587\u4EF6]" });
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
try {
|
|
2271
|
+
if (!existsSync7(filePath2) || !statSync(filePath2).isFile()) {
|
|
2272
|
+
this.wsSend(ws, { type: "file_content", path: filePath2, content: "[\u6587\u4EF6\u4E0D\u5B58\u5728]" });
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
const size = statSync(filePath2).size;
|
|
2276
|
+
if (size > 2 * 1024 * 1024) {
|
|
2277
|
+
this.wsSend(ws, { type: "file_content", path: filePath2, content: `[\u6587\u4EF6\u8FC7\u5927\uFF1A${(size / 1024).toFixed(0)}KB\uFF0C\u6700\u5927 2MB]` });
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
const ext = extname(filePath2).toLowerCase();
|
|
2281
|
+
if (_WebChatServer.IMAGE_EXTS.has(ext)) {
|
|
2282
|
+
const buf = readFileSync5(filePath2);
|
|
2283
|
+
const mime = _WebChatServer.IMAGE_MIME[ext] ?? "application/octet-stream";
|
|
2284
|
+
const dataUrl = `data:${mime};base64,${buf.toString("base64")}`;
|
|
2285
|
+
this.wsSend(ws, { type: "file_content", path: filePath2, content: dataUrl, isImage: true });
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
const content = readFileSync5(filePath2, "utf-8");
|
|
2289
|
+
this.wsSend(ws, { type: "file_content", path: filePath2, content });
|
|
2290
|
+
} catch (err) {
|
|
2291
|
+
const reason = err instanceof Error ? err.message : "unknown";
|
|
2292
|
+
this.wsSend(ws, { type: "file_content", path: filePath2, content: `[\u8BFB\u53D6\u5931\u8D25\uFF1A${reason}]` });
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
1147
2295
|
// ─── Internal ──────────────────────────────────────────────
|
|
1148
2296
|
wsSendTo(userId, data) {
|
|
1149
2297
|
const ws = this.connections.get(userId);
|
|
1150
|
-
if (ws && ws.readyState ===
|
|
2298
|
+
if (ws && ws.readyState === WebSocket2.OPEN) {
|
|
1151
2299
|
this.wsSend(ws, data);
|
|
1152
2300
|
}
|
|
1153
2301
|
}
|
|
1154
2302
|
wsSend(ws, data) {
|
|
1155
|
-
|
|
2303
|
+
try {
|
|
2304
|
+
ws.send(JSON.stringify(data));
|
|
2305
|
+
} catch (err) {
|
|
2306
|
+
log8.warn({ error: err instanceof Error ? err.message : String(err) }, "wsSend failed");
|
|
2307
|
+
}
|
|
1156
2308
|
}
|
|
1157
2309
|
};
|
|
1158
2310
|
|
|
1159
2311
|
// src/channels/telegram.ts
|
|
2312
|
+
init_logger();
|
|
1160
2313
|
import { Bot, InlineKeyboard } from "grammy";
|
|
1161
|
-
import {
|
|
1162
|
-
var
|
|
2314
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
2315
|
+
var log9 = createLogger("channel:telegram");
|
|
1163
2316
|
var TelegramAdapter = class {
|
|
1164
2317
|
name = "telegram";
|
|
1165
2318
|
bot = null;
|
|
@@ -1176,7 +2329,7 @@ var TelegramAdapter = class {
|
|
|
1176
2329
|
this.bot.on("message:text", async (ctx) => {
|
|
1177
2330
|
if (!this.handler) return;
|
|
1178
2331
|
const normalized = {
|
|
1179
|
-
traceId:
|
|
2332
|
+
traceId: nanoid3(),
|
|
1180
2333
|
channelId: "telegram",
|
|
1181
2334
|
channelName: "Telegram",
|
|
1182
2335
|
userId: String(ctx.from.id),
|
|
@@ -1191,7 +2344,7 @@ var TelegramAdapter = class {
|
|
|
1191
2344
|
try {
|
|
1192
2345
|
await this.handler(normalized);
|
|
1193
2346
|
} catch (error) {
|
|
1194
|
-
|
|
2347
|
+
log9.error({ error, userId: normalized.userId }, "Message handler error");
|
|
1195
2348
|
await ctx.reply("Sorry, something went wrong. Please try again.");
|
|
1196
2349
|
}
|
|
1197
2350
|
});
|
|
@@ -1199,7 +2352,7 @@ var TelegramAdapter = class {
|
|
|
1199
2352
|
if (!this.handler) return;
|
|
1200
2353
|
await ctx.answerCallbackQuery();
|
|
1201
2354
|
const normalized = {
|
|
1202
|
-
traceId:
|
|
2355
|
+
traceId: nanoid3(),
|
|
1203
2356
|
channelId: "telegram",
|
|
1204
2357
|
channelName: "Telegram",
|
|
1205
2358
|
userId: String(ctx.from.id),
|
|
@@ -1214,30 +2367,30 @@ var TelegramAdapter = class {
|
|
|
1214
2367
|
try {
|
|
1215
2368
|
await this.handler(normalized);
|
|
1216
2369
|
} catch (error) {
|
|
1217
|
-
|
|
2370
|
+
log9.error({ error }, "Callback handler error");
|
|
1218
2371
|
}
|
|
1219
2372
|
});
|
|
1220
2373
|
this.bot.catch((err) => {
|
|
1221
|
-
|
|
2374
|
+
log9.error({ error: err.message }, "Bot error \u2014 reconnecting...");
|
|
1222
2375
|
this._connected = false;
|
|
1223
2376
|
setTimeout(() => this.reconnect(), 5e3);
|
|
1224
2377
|
});
|
|
1225
2378
|
await this.bot.init();
|
|
1226
|
-
|
|
2379
|
+
log9.info({ botName: this.bot.botInfo.username }, "Telegram bot initialized");
|
|
1227
2380
|
this.bot.start({
|
|
1228
2381
|
onStart: () => {
|
|
1229
2382
|
this._connected = true;
|
|
1230
|
-
|
|
2383
|
+
log9.info("Telegram bot polling started");
|
|
1231
2384
|
}
|
|
1232
2385
|
});
|
|
1233
2386
|
}
|
|
1234
2387
|
async disconnect() {
|
|
1235
2388
|
this.bot?.stop();
|
|
1236
2389
|
this._connected = false;
|
|
1237
|
-
|
|
2390
|
+
log9.info("Telegram bot disconnected");
|
|
1238
2391
|
}
|
|
1239
2392
|
async reconnect() {
|
|
1240
|
-
|
|
2393
|
+
log9.info("Reconnecting Telegram bot...");
|
|
1241
2394
|
await this.disconnect();
|
|
1242
2395
|
await this.connect({ botToken: this.botToken });
|
|
1243
2396
|
}
|
|
@@ -1275,30 +2428,31 @@ ${card.summary}`;
|
|
|
1275
2428
|
try {
|
|
1276
2429
|
await this.bot.api.deleteMessage(chatId, messageId);
|
|
1277
2430
|
} catch (error) {
|
|
1278
|
-
|
|
2431
|
+
log9.warn({ chatId, messageId, error }, "Failed to delete message");
|
|
1279
2432
|
}
|
|
1280
2433
|
}
|
|
1281
2434
|
};
|
|
1282
2435
|
|
|
1283
2436
|
// src/memory/file-provider.ts
|
|
1284
2437
|
import {
|
|
1285
|
-
readFileSync as
|
|
1286
|
-
writeFileSync as
|
|
2438
|
+
readFileSync as readFileSync6,
|
|
2439
|
+
writeFileSync as writeFileSync6,
|
|
1287
2440
|
appendFileSync as appendFileSync2,
|
|
1288
|
-
existsSync as
|
|
1289
|
-
mkdirSync as
|
|
1290
|
-
readdirSync
|
|
2441
|
+
existsSync as existsSync8,
|
|
2442
|
+
mkdirSync as mkdirSync7,
|
|
2443
|
+
readdirSync as readdirSync2
|
|
1291
2444
|
} from "fs";
|
|
1292
|
-
import { join as
|
|
2445
|
+
import { join as join8 } from "path";
|
|
1293
2446
|
import { randomUUID } from "crypto";
|
|
1294
|
-
|
|
1295
|
-
var
|
|
2447
|
+
init_logger();
|
|
2448
|
+
var log10 = createLogger("memory:file");
|
|
2449
|
+
var MEMORY_DIR = join8(CONFIG_DIR, "memories");
|
|
1296
2450
|
var FileMemoryProvider = class {
|
|
1297
2451
|
name = "file";
|
|
1298
2452
|
degraded = false;
|
|
1299
2453
|
constructor() {
|
|
1300
|
-
if (!
|
|
1301
|
-
|
|
2454
|
+
if (!existsSync8(MEMORY_DIR)) {
|
|
2455
|
+
mkdirSync7(MEMORY_DIR, { recursive: true });
|
|
1302
2456
|
}
|
|
1303
2457
|
}
|
|
1304
2458
|
async add(content, metadata) {
|
|
@@ -1313,7 +2467,7 @@ var FileMemoryProvider = class {
|
|
|
1313
2467
|
};
|
|
1314
2468
|
const file = this.userFile(metadata.userId);
|
|
1315
2469
|
appendFileSync2(file, JSON.stringify(entry) + "\n");
|
|
1316
|
-
|
|
2470
|
+
log10.debug({ id, userId: metadata.userId }, "Memory added");
|
|
1317
2471
|
return id;
|
|
1318
2472
|
}
|
|
1319
2473
|
async search(query, filters) {
|
|
@@ -1322,25 +2476,25 @@ var FileMemoryProvider = class {
|
|
|
1322
2476
|
if (keywords.length === 0) {
|
|
1323
2477
|
return all.slice(0, filters.limit ?? 10);
|
|
1324
2478
|
}
|
|
1325
|
-
const scored = all.map((
|
|
1326
|
-
const text =
|
|
2479
|
+
const scored = all.map((m2) => {
|
|
2480
|
+
const text = m2.content.toLowerCase();
|
|
1327
2481
|
const matchCount = keywords.filter((k) => text.includes(k)).length;
|
|
1328
|
-
return { memory:
|
|
2482
|
+
return { memory: m2, score: matchCount / keywords.length };
|
|
1329
2483
|
}).filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, filters.limit ?? 10);
|
|
1330
2484
|
return scored.map((s) => ({ ...s.memory, score: s.score }));
|
|
1331
2485
|
}
|
|
1332
2486
|
async getAll(filters) {
|
|
1333
|
-
const files = filters.userId ? [this.userFile(filters.userId)] :
|
|
2487
|
+
const files = filters.userId ? [this.userFile(filters.userId)] : readdirSync2(MEMORY_DIR).filter((f) => f.endsWith(".jsonl")).map((f) => join8(MEMORY_DIR, f));
|
|
1334
2488
|
const results = [];
|
|
1335
2489
|
for (const file of files) {
|
|
1336
|
-
if (!
|
|
1337
|
-
const lines =
|
|
2490
|
+
if (!existsSync8(file)) continue;
|
|
2491
|
+
const lines = readFileSync6(file, "utf-8").split("\n").filter((l) => l.trim());
|
|
1338
2492
|
for (const line of lines) {
|
|
1339
2493
|
try {
|
|
1340
2494
|
const stored = JSON.parse(line);
|
|
1341
2495
|
if (filters.category && stored.metadata.category !== filters.category) continue;
|
|
1342
2496
|
if (filters.sessionId && stored.metadata.sessionId !== filters.sessionId) continue;
|
|
1343
|
-
if (filters.tags && filters.tags.length > 0 && !filters.tags.some((
|
|
2497
|
+
if (filters.tags && filters.tags.length > 0 && !filters.tags.some((t2) => stored.metadata.tags?.includes(t2))) continue;
|
|
1344
2498
|
results.push({
|
|
1345
2499
|
id: stored.id,
|
|
1346
2500
|
content: stored.content,
|
|
@@ -1355,10 +2509,10 @@ var FileMemoryProvider = class {
|
|
|
1355
2509
|
return results.slice(0, filters.limit ?? 100);
|
|
1356
2510
|
}
|
|
1357
2511
|
async update(id, content) {
|
|
1358
|
-
const files =
|
|
2512
|
+
const files = readdirSync2(MEMORY_DIR).filter((f) => f.endsWith(".jsonl")).map((f) => join8(MEMORY_DIR, f));
|
|
1359
2513
|
for (const file of files) {
|
|
1360
|
-
if (!
|
|
1361
|
-
const lines =
|
|
2514
|
+
if (!existsSync8(file)) continue;
|
|
2515
|
+
const lines = readFileSync6(file, "utf-8").split("\n").filter((l) => l.trim());
|
|
1362
2516
|
const updated = lines.map((line) => {
|
|
1363
2517
|
try {
|
|
1364
2518
|
const stored = JSON.parse(line);
|
|
@@ -1371,14 +2525,14 @@ var FileMemoryProvider = class {
|
|
|
1371
2525
|
return line;
|
|
1372
2526
|
}
|
|
1373
2527
|
});
|
|
1374
|
-
|
|
2528
|
+
writeFileSync6(file, updated.join("\n") + "\n");
|
|
1375
2529
|
}
|
|
1376
2530
|
}
|
|
1377
2531
|
async delete(id) {
|
|
1378
|
-
const files =
|
|
2532
|
+
const files = readdirSync2(MEMORY_DIR).filter((f) => f.endsWith(".jsonl")).map((f) => join8(MEMORY_DIR, f));
|
|
1379
2533
|
for (const file of files) {
|
|
1380
|
-
if (!
|
|
1381
|
-
const lines =
|
|
2534
|
+
if (!existsSync8(file)) continue;
|
|
2535
|
+
const lines = readFileSync6(file, "utf-8").split("\n").filter((l) => l.trim());
|
|
1382
2536
|
const filtered = lines.filter((line) => {
|
|
1383
2537
|
try {
|
|
1384
2538
|
const stored = JSON.parse(line);
|
|
@@ -1387,17 +2541,22 @@ var FileMemoryProvider = class {
|
|
|
1387
2541
|
return true;
|
|
1388
2542
|
}
|
|
1389
2543
|
});
|
|
1390
|
-
|
|
2544
|
+
writeFileSync6(file, filtered.join("\n") + (filtered.length > 0 ? "\n" : ""));
|
|
1391
2545
|
}
|
|
1392
2546
|
}
|
|
1393
2547
|
userFile(userId) {
|
|
1394
2548
|
const safe = userId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1395
|
-
return
|
|
2549
|
+
return join8(MEMORY_DIR, `${safe}.jsonl`);
|
|
1396
2550
|
}
|
|
1397
2551
|
};
|
|
1398
2552
|
|
|
2553
|
+
// src/gateway/gateway.ts
|
|
2554
|
+
init_logger();
|
|
2555
|
+
init_tracer();
|
|
2556
|
+
|
|
1399
2557
|
// src/resilience/degradation.ts
|
|
1400
|
-
|
|
2558
|
+
init_logger();
|
|
2559
|
+
var log11 = createLogger("degradation");
|
|
1401
2560
|
var states = /* @__PURE__ */ new Map();
|
|
1402
2561
|
function isDegraded(service) {
|
|
1403
2562
|
return states.get(service)?.degraded ?? false;
|
|
@@ -1413,10 +2572,468 @@ function degradationMessage(service) {
|
|
|
1413
2572
|
}
|
|
1414
2573
|
}
|
|
1415
2574
|
|
|
2575
|
+
// src/context/token-counter.ts
|
|
2576
|
+
init_logger();
|
|
2577
|
+
var log12 = createLogger("context:token");
|
|
2578
|
+
var encode = null;
|
|
2579
|
+
function countTokens(text) {
|
|
2580
|
+
try {
|
|
2581
|
+
if (encode) return encode(text).length;
|
|
2582
|
+
} catch {
|
|
2583
|
+
}
|
|
2584
|
+
return Math.ceil(text.length / 4);
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// src/context/context-budget.ts
|
|
2588
|
+
init_logger();
|
|
2589
|
+
var log13 = createLogger("context:budget");
|
|
2590
|
+
var FALLBACK_MAX_MESSAGES = 20;
|
|
2591
|
+
var DEFAULT_BUDGET = {
|
|
2592
|
+
contextWindow: 128e3,
|
|
2593
|
+
systemPromptReserve: 8e3,
|
|
2594
|
+
memoriesMax: 1e3,
|
|
2595
|
+
toolResultsMax: 6e3,
|
|
2596
|
+
compactionThreshold: 0.75
|
|
2597
|
+
};
|
|
2598
|
+
function allocateBudget(systemPrompt, memories, history, config = {}) {
|
|
2599
|
+
const cfg = { ...DEFAULT_BUDGET, ...config };
|
|
2600
|
+
try {
|
|
2601
|
+
const systemTokens = systemPrompt ? countTokens(systemPrompt) : cfg.systemPromptReserve;
|
|
2602
|
+
const memoriesTokens = memories ? Math.min(countTokens(memories), cfg.memoriesMax) : 0;
|
|
2603
|
+
const historyBudget = cfg.contextWindow - systemTokens - memoriesTokens - cfg.toolResultsMax;
|
|
2604
|
+
if (historyBudget <= 0) {
|
|
2605
|
+
log13.warn({ systemTokens, memoriesTokens }, "No budget left for history");
|
|
2606
|
+
return {
|
|
2607
|
+
messages: [],
|
|
2608
|
+
historyTokens: 0,
|
|
2609
|
+
totalTokens: systemTokens + memoriesTokens,
|
|
2610
|
+
remainingTokens: 0,
|
|
2611
|
+
shouldCompact: true,
|
|
2612
|
+
fallbackMode: false
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
const included = [];
|
|
2616
|
+
let historyTokens = 0;
|
|
2617
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
2618
|
+
const msgTokens = countTokens(history[i].content) + 4;
|
|
2619
|
+
if (historyTokens + msgTokens > historyBudget) break;
|
|
2620
|
+
included.unshift(history[i]);
|
|
2621
|
+
historyTokens += msgTokens;
|
|
2622
|
+
}
|
|
2623
|
+
const totalTokens = systemTokens + memoriesTokens + historyTokens;
|
|
2624
|
+
const threshold = cfg.contextWindow * cfg.compactionThreshold;
|
|
2625
|
+
return {
|
|
2626
|
+
messages: included,
|
|
2627
|
+
historyTokens,
|
|
2628
|
+
totalTokens,
|
|
2629
|
+
remainingTokens: cfg.contextWindow - totalTokens,
|
|
2630
|
+
shouldCompact: totalTokens >= threshold,
|
|
2631
|
+
fallbackMode: false
|
|
2632
|
+
};
|
|
2633
|
+
} catch (err) {
|
|
2634
|
+
log13.warn({ err }, "Budget allocation failed, using message-count fallback");
|
|
2635
|
+
return {
|
|
2636
|
+
messages: history.slice(-FALLBACK_MAX_MESSAGES),
|
|
2637
|
+
historyTokens: 0,
|
|
2638
|
+
totalTokens: 0,
|
|
2639
|
+
remainingTokens: 0,
|
|
2640
|
+
shouldCompact: false,
|
|
2641
|
+
fallbackMode: true
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// src/context/compaction.ts
|
|
2647
|
+
init_logger();
|
|
2648
|
+
var log14 = createLogger("context:compact");
|
|
2649
|
+
async function compactWithFlush(allMessages, keepCount, memory, userId) {
|
|
2650
|
+
if (allMessages.length <= keepCount) {
|
|
2651
|
+
return { messages: allMessages, removedCount: 0, factsExtracted: [] };
|
|
2652
|
+
}
|
|
2653
|
+
const evicted = allMessages.slice(0, allMessages.length - keepCount);
|
|
2654
|
+
const kept = allMessages.slice(-keepCount);
|
|
2655
|
+
const factsExtracted = [];
|
|
2656
|
+
const facts = extractKeyFacts(evicted);
|
|
2657
|
+
for (const fact of facts) {
|
|
2658
|
+
try {
|
|
2659
|
+
await memory.add(fact, {
|
|
2660
|
+
userId,
|
|
2661
|
+
category: "fact"
|
|
2662
|
+
});
|
|
2663
|
+
factsExtracted.push(fact);
|
|
2664
|
+
} catch (err) {
|
|
2665
|
+
log14.warn({ err, fact }, "Failed to save fact to memory");
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
log14.info(
|
|
2669
|
+
{
|
|
2670
|
+
userId,
|
|
2671
|
+
evictedMessages: evicted.length,
|
|
2672
|
+
keptMessages: kept.length,
|
|
2673
|
+
factsExtracted: factsExtracted.length
|
|
2674
|
+
},
|
|
2675
|
+
"Pre-compaction flush complete"
|
|
2676
|
+
);
|
|
2677
|
+
return {
|
|
2678
|
+
messages: kept,
|
|
2679
|
+
removedCount: evicted.length,
|
|
2680
|
+
factsExtracted
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
function extractKeyFacts(messages) {
|
|
2684
|
+
const facts = [];
|
|
2685
|
+
const fullText = messages.map((m2) => m2.content).join("\n");
|
|
2686
|
+
const hasChinese = /[\u4e00-\u9fff]/.test(fullText);
|
|
2687
|
+
const urlMatches = fullText.match(
|
|
2688
|
+
/(?:aliexpress|alibaba|1688|accio|temu)\.com\S*/gi
|
|
2689
|
+
);
|
|
2690
|
+
if (urlMatches) {
|
|
2691
|
+
const unique = [...new Set(urlMatches)].slice(0, 5);
|
|
2692
|
+
facts.push(
|
|
2693
|
+
hasChinese ? `\u7528\u6237\u66FE\u5BFC\u5165\u5546\u54C1\u94FE\u63A5\uFF1A${unique.join(", ")}` : `Product URLs imported: ${unique.join(", ")}`
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
2696
|
+
const pricePatterns = fullText.match(
|
|
2697
|
+
/(?:售价|定价|sell.*?price|pricing|set.*?price|markup|fixed.*?price).*?(?:\$[\d.]+|¥[\d.]+|[\d.]+\s*(?:元|美元|dollars?))/gi
|
|
2698
|
+
);
|
|
2699
|
+
if (pricePatterns) {
|
|
2700
|
+
const unique = [...new Set(pricePatterns)].slice(0, 3);
|
|
2701
|
+
facts.push(
|
|
2702
|
+
hasChinese ? `\u5B9A\u4EF7\u76F8\u5173\uFF1A${unique.join("; ")}` : `Pricing decisions: ${unique.join("; ")}`
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
const storeMatches = fullText.match(
|
|
2706
|
+
/(?:店铺|store|shop)\s*[::]?\s*[\w\s-]{3,30}/gi
|
|
2707
|
+
);
|
|
2708
|
+
if (storeMatches) {
|
|
2709
|
+
const unique = [...new Set(storeMatches)].slice(0, 3);
|
|
2710
|
+
facts.push(
|
|
2711
|
+
hasChinese ? `\u6D89\u53CA\u5E97\u94FA\uFF1A${unique.join(", ")}` : `Stores referenced: ${unique.join(", ")}`
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
const decisions = messages.filter((m2) => m2.role === "user").map((m2) => m2.content.trim()).filter(
|
|
2715
|
+
(t2) => /^(好|确认|执行|推送|是|yes|ok|confirm|push|no|不|取消|算了|cancel|skip)/i.test(t2) && t2.length < 50
|
|
2716
|
+
);
|
|
2717
|
+
if (decisions.length > 0) {
|
|
2718
|
+
facts.push(
|
|
2719
|
+
hasChinese ? `\u7528\u6237\u51B3\u7B56\u8BB0\u5F55\uFF1A${decisions.slice(0, 5).join("; ")}` : `User decisions: ${decisions.slice(0, 5).join("; ")}`
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
for (const m2 of messages) {
|
|
2723
|
+
if (m2.role !== "assistant") continue;
|
|
2724
|
+
const summary = m2.content.match(
|
|
2725
|
+
/(?:已导入|已推送|已修改|已更新|已删除|Imported|Pushed|Updated|Modified|Deleted|Set price|Changed title)\s*.{10,80}/
|
|
2726
|
+
);
|
|
2727
|
+
if (summary) {
|
|
2728
|
+
facts.push(summary[0]);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
return [...new Set(facts)].slice(0, 8);
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
// src/context/history-store.ts
|
|
2735
|
+
import {
|
|
2736
|
+
readFileSync as readFileSync7,
|
|
2737
|
+
writeFileSync as writeFileSync7,
|
|
2738
|
+
existsSync as existsSync9,
|
|
2739
|
+
mkdirSync as mkdirSync8,
|
|
2740
|
+
renameSync as renameSync2
|
|
2741
|
+
} from "fs";
|
|
2742
|
+
import { join as join9 } from "path";
|
|
2743
|
+
import {
|
|
2744
|
+
randomBytes as randomBytes2,
|
|
2745
|
+
createCipheriv as createCipheriv2,
|
|
2746
|
+
createDecipheriv as createDecipheriv2,
|
|
2747
|
+
createHash as createHash3
|
|
2748
|
+
} from "crypto";
|
|
2749
|
+
import { hostname as hostname2, userInfo as userInfo2 } from "os";
|
|
2750
|
+
init_logger();
|
|
2751
|
+
var log15 = createLogger("context:history");
|
|
2752
|
+
var HISTORY_DIR = join9(CONFIG_DIR, "history");
|
|
2753
|
+
var MAX_LINES = 200;
|
|
2754
|
+
var KEEP_AFTER_ROTATE = 100;
|
|
2755
|
+
var ALG2 = "aes-256-gcm";
|
|
2756
|
+
var IV_LEN2 = 12;
|
|
2757
|
+
var TAG_LEN2 = 16;
|
|
2758
|
+
var KEY_SEED2 = "dsclaw-history-v1";
|
|
2759
|
+
function deriveKey2() {
|
|
2760
|
+
let user = "";
|
|
2761
|
+
try {
|
|
2762
|
+
user = userInfo2().username;
|
|
2763
|
+
} catch {
|
|
2764
|
+
user = process.env["USER"] ?? process.env["USERNAME"] ?? "default";
|
|
2765
|
+
}
|
|
2766
|
+
return createHash3("sha256").update(`${KEY_SEED2}:${hostname2()}:${user}`).digest();
|
|
2767
|
+
}
|
|
2768
|
+
function encryptStr(data) {
|
|
2769
|
+
const key = deriveKey2();
|
|
2770
|
+
const iv = randomBytes2(IV_LEN2);
|
|
2771
|
+
const cipher = createCipheriv2(ALG2, key, iv, { authTagLength: TAG_LEN2 });
|
|
2772
|
+
const ct = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
2773
|
+
const tag = cipher.getAuthTag();
|
|
2774
|
+
return Buffer.concat([iv, tag, ct]).toString("base64");
|
|
2775
|
+
}
|
|
2776
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2777
|
+
function ensureDir() {
|
|
2778
|
+
if (!existsSync9(HISTORY_DIR)) {
|
|
2779
|
+
mkdirSync8(HISTORY_DIR, { recursive: true });
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
function filePath(sessionKey) {
|
|
2783
|
+
const safe = sessionKey.replace(/[^a-zA-Z0-9_:-]/g, "_");
|
|
2784
|
+
return join9(HISTORY_DIR, `${safe}.enc`);
|
|
2785
|
+
}
|
|
2786
|
+
function writeEncryptedFile(fp, messages) {
|
|
2787
|
+
const json = JSON.stringify(messages);
|
|
2788
|
+
const encrypted = encryptStr(json);
|
|
2789
|
+
const tmp = fp + ".tmp";
|
|
2790
|
+
writeFileSync7(tmp, encrypted);
|
|
2791
|
+
renameSync2(tmp, fp);
|
|
2792
|
+
}
|
|
2793
|
+
function appendHistory(sessionKey, ...messages) {
|
|
2794
|
+
const history = cache.get(sessionKey) ?? [];
|
|
2795
|
+
for (const m2 of messages) {
|
|
2796
|
+
history.push({ ...m2, ts: m2.ts ?? (/* @__PURE__ */ new Date()).toISOString() });
|
|
2797
|
+
}
|
|
2798
|
+
cache.set(sessionKey, history);
|
|
2799
|
+
Promise.resolve().then(() => {
|
|
2800
|
+
try {
|
|
2801
|
+
ensureDir();
|
|
2802
|
+
writeEncryptedFile(filePath(sessionKey), history);
|
|
2803
|
+
if (history.length > MAX_LINES) {
|
|
2804
|
+
rotateHistory(sessionKey);
|
|
2805
|
+
}
|
|
2806
|
+
} catch (err) {
|
|
2807
|
+
log15.warn({ sessionKey, err }, "Failed to write history to disk");
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
function clearHistory(sessionKeyOrUserId) {
|
|
2812
|
+
for (const key of cache.keys()) {
|
|
2813
|
+
if (key === sessionKeyOrUserId || key.endsWith(`:${sessionKeyOrUserId}`)) {
|
|
2814
|
+
cache.delete(key);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
function rotateHistory(sessionKey) {
|
|
2819
|
+
const history = cache.get(sessionKey);
|
|
2820
|
+
if (!history || history.length <= MAX_LINES) return;
|
|
2821
|
+
const trimmed = history.slice(-KEEP_AFTER_ROTATE);
|
|
2822
|
+
cache.set(sessionKey, trimmed);
|
|
2823
|
+
try {
|
|
2824
|
+
writeEncryptedFile(filePath(sessionKey), trimmed);
|
|
2825
|
+
log15.info(
|
|
2826
|
+
{ sessionKey, before: history.length, after: trimmed.length },
|
|
2827
|
+
"History rotated"
|
|
2828
|
+
);
|
|
2829
|
+
} catch (err) {
|
|
2830
|
+
log15.warn({ sessionKey, err }, "Failed to rotate history file");
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// src/gateway/i18n.ts
|
|
2835
|
+
var m = {
|
|
2836
|
+
// Rate limit
|
|
2837
|
+
"gateway.rateLimit": {
|
|
2838
|
+
en: "Too many messages. Please wait a moment.",
|
|
2839
|
+
zh: "\u6D88\u606F\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u7B49\u7247\u523B\u3002"
|
|
2840
|
+
},
|
|
2841
|
+
// No API key (BUG-009)
|
|
2842
|
+
"gateway.noApiKey.web": {
|
|
2843
|
+
en: "Please configure your AI model API Key in \u2699\uFE0F Settings (top-right) before chatting.",
|
|
2844
|
+
zh: "\u8BF7\u5148\u5728\u53F3\u4E0A\u89D2 \u2699\uFE0F \u8BBE\u7F6E\u4E2D\u914D\u7F6E AI \u6A21\u578B\u7684 API Key\uFF0C\u624D\u80FD\u5F00\u59CB\u5BF9\u8BDD\u3002"
|
|
2845
|
+
},
|
|
2846
|
+
"gateway.noApiKey.telegram": {
|
|
2847
|
+
en: "Please configure your AI API key first. Send /reset to restart setup.",
|
|
2848
|
+
zh: "\u8BF7\u5148\u914D\u7F6E AI API Key\u3002\u53D1\u9001 /reset \u91CD\u65B0\u8BBE\u7F6E\u3002"
|
|
2849
|
+
},
|
|
2850
|
+
// Welcome (Web)
|
|
2851
|
+
"gateway.welcome.web": {
|
|
2852
|
+
en: "Welcome to **DSClaw**! I'm your AI dropshipping assistant.\n\nClick the \u2699\uFE0F **Settings** button (top-right) to connect your **DSers account** and **AI provider**.\n\nOnce set up, I can help you:\n- Import products from AliExpress, Temu, 1688\n- Push to your Shopify / WooCommerce store\n- Manage orders, inventory, and pricing rules\n- ...all through this chat!",
|
|
2853
|
+
zh: "\u6B22\u8FCE\u4F7F\u7528 **DSClaw**\uFF01\u6211\u662F\u4F60\u7684 AI \u4EE3\u53D1\u52A9\u624B\u3002\n\n\u70B9\u51FB\u53F3\u4E0A\u89D2 \u2699\uFE0F **\u8BBE\u7F6E** \u6309\u94AE\uFF0C\u8FDE\u63A5\u4F60\u7684 **DSers \u8D26\u53F7**\u548C **AI \u6A21\u578B**\u3002\n\n\u8BBE\u7F6E\u5B8C\u6210\u540E\uFF0C\u6211\u53EF\u4EE5\u5E2E\u4F60\uFF1A\n- \u4ECE AliExpress\u3001Temu\u30011688 \u5BFC\u5165\u5546\u54C1\n- \u63A8\u9001\u5230\u4F60\u7684 Shopify / WooCommerce \u5E97\u94FA\n- \u7BA1\u7406\u8BA2\u5355\u3001\u5E93\u5B58\u548C\u5B9A\u4EF7\u89C4\u5219\n- ...\u5168\u90E8\u901A\u8FC7\u5BF9\u8BDD\u5B8C\u6210\uFF01"
|
|
2854
|
+
},
|
|
2855
|
+
// Welcome (Telegram)
|
|
2856
|
+
"gateway.welcome.telegram": {
|
|
2857
|
+
en: "Welcome to *DSClaw*! I'm your AI assistant for dropshipping.\n\nI can help you:\n- Import products from AliExpress, Temu, 1688\n- Push products to your Shopify/WooCommerce store\n- Check inventory, pricing rules, and orders\n\nLet's get you set up. It takes about 1 minute.\n\n*Step 1/3:* What is your DSers account email?",
|
|
2858
|
+
zh: "\u6B22\u8FCE\u4F7F\u7528 *DSClaw*\uFF01\u6211\u662F\u4F60\u7684 AI \u4EE3\u53D1\u52A9\u624B\u3002\n\n\u6211\u53EF\u4EE5\u5E2E\u4F60\uFF1A\n- \u4ECE AliExpress\u3001Temu\u30011688 \u5BFC\u5165\u5546\u54C1\n- \u63A8\u9001\u5230\u4F60\u7684 Shopify/WooCommerce \u5E97\u94FA\n- \u67E5\u8BE2\u5E93\u5B58\u3001\u5B9A\u4EF7\u89C4\u5219\u548C\u8BA2\u5355\n\n\u5F00\u59CB\u8BBE\u7F6E\u5427\uFF0C\u5927\u7EA6 1 \u5206\u949F\u3002\n\n*\u6B65\u9AA4 1/3\uFF1A* \u8BF7\u8F93\u5165\u4F60\u7684 DSers \u8D26\u53F7\u90AE\u7BB1\uFF1A"
|
|
2859
|
+
},
|
|
2860
|
+
// Ready — short toast version
|
|
2861
|
+
"gateway.ready.toast": {
|
|
2862
|
+
en: "All set! DSClaw is ready.",
|
|
2863
|
+
zh: "\u8BBE\u7F6E\u5B8C\u6210\uFF01DSClaw \u5DF2\u51C6\u5907\u5C31\u7EEA\u3002"
|
|
2864
|
+
},
|
|
2865
|
+
// Ready (BUG-010)
|
|
2866
|
+
"gateway.ready.web": {
|
|
2867
|
+
en: `All set! DSClaw is ready.
|
|
2868
|
+
|
|
2869
|
+
Try asking me:
|
|
2870
|
+
- "Show me my stores"
|
|
2871
|
+
- "What's in my import list?"
|
|
2872
|
+
- "Search for phone cases on AliExpress"
|
|
2873
|
+
|
|
2874
|
+
Just type naturally!`,
|
|
2875
|
+
zh: '\u8BBE\u7F6E\u5B8C\u6210\uFF01DSClaw \u5DF2\u51C6\u5907\u5C31\u7EEA\u3002\n\n\u8BD5\u8BD5\u5BF9\u6211\u8BF4\uFF1A\n- "\u5E2E\u6211\u770B\u770B\u6211\u7684\u5E97\u94FA"\n- "\u6211\u7684\u5BFC\u5165\u5217\u8868\u91CC\u6709\u4EC0\u4E48"\n- "\u4ECE\u901F\u5356\u901A\u641C\u7D22\u624B\u673A\u58F3"\n\n\u76F4\u63A5\u8F93\u5165\u5C31\u597D\uFF01'
|
|
2876
|
+
},
|
|
2877
|
+
"gateway.ready.telegram": {
|
|
2878
|
+
en: `All set! DSClaw is ready.
|
|
2879
|
+
|
|
2880
|
+
Try these:
|
|
2881
|
+
- "Show me my stores"
|
|
2882
|
+
- "What's in my import list?"
|
|
2883
|
+
- "Search for phone cases on AliExpress"
|
|
2884
|
+
- "Check my pricing rules"
|
|
2885
|
+
|
|
2886
|
+
Just type naturally \u2014 I understand everyday language!`,
|
|
2887
|
+
zh: '\u8BBE\u7F6E\u5B8C\u6210\uFF01DSClaw \u5DF2\u51C6\u5907\u5C31\u7EEA\u3002\n\n\u8BD5\u8BD5\u8FD9\u4E9B\uFF1A\n- "\u5E2E\u6211\u770B\u770B\u6211\u7684\u5E97\u94FA"\n- "\u6211\u7684\u5BFC\u5165\u5217\u8868\u91CC\u6709\u4EC0\u4E48"\n- "\u4ECE\u901F\u5356\u901A\u641C\u7D22\u624B\u673A\u58F3"\n- "\u67E5\u770B\u5B9A\u4EF7\u89C4\u5219"\n\n\u76F4\u63A5\u7528\u81EA\u7136\u8BED\u8A00\u8F93\u5165\u5C31\u597D\uFF01'
|
|
2888
|
+
},
|
|
2889
|
+
// Commands
|
|
2890
|
+
"gateway.cmd.reset": {
|
|
2891
|
+
en: "Conversation reset. DSers connection and AI config preserved.",
|
|
2892
|
+
zh: "\u5BF9\u8BDD\u5DF2\u91CD\u7F6E\uFF0CDSers \u8FDE\u63A5\u548C AI \u914D\u7F6E\u5DF2\u4FDD\u7559\u3002"
|
|
2893
|
+
},
|
|
2894
|
+
"gateway.cmd.logout": {
|
|
2895
|
+
en: "Logged out. All configuration cleared. Send any message to start over.",
|
|
2896
|
+
zh: "\u5DF2\u5B8C\u5168\u767B\u51FA\uFF0C\u6240\u6709\u914D\u7F6E\u5DF2\u6E05\u9664\u3002\u53D1\u9001\u4EFB\u610F\u6D88\u606F\u91CD\u65B0\u5F00\u59CB\u3002"
|
|
2897
|
+
},
|
|
2898
|
+
"gateway.cmd.retryEmpty": {
|
|
2899
|
+
en: "No message to retry.",
|
|
2900
|
+
zh: "\u6CA1\u6709\u53EF\u91CD\u8BD5\u7684\u6D88\u606F\u3002"
|
|
2901
|
+
},
|
|
2902
|
+
// Errors
|
|
2903
|
+
"gateway.error.generic": {
|
|
2904
|
+
en: "Something went wrong, please try again.",
|
|
2905
|
+
zh: "\u51FA\u4E86\u70B9\u95EE\u9898\uFF0C\u8BF7\u91CD\u8BD5\u3002"
|
|
2906
|
+
},
|
|
2907
|
+
"gateway.error.timeout": {
|
|
2908
|
+
en: "Model response timed out, please try again later.",
|
|
2909
|
+
zh: "\u6A21\u578B\u54CD\u5E94\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"
|
|
2910
|
+
},
|
|
2911
|
+
"gateway.error.apiKey": {
|
|
2912
|
+
en: "API key invalid or expired. Send /reset to reconfigure.",
|
|
2913
|
+
zh: "API \u5BC6\u94A5\u65E0\u6548\u6216\u8FC7\u671F\uFF0C\u8BF7\u53D1\u9001 /reset \u91CD\u65B0\u914D\u7F6E\u3002"
|
|
2914
|
+
},
|
|
2915
|
+
"gateway.error.rateLimitLlm": {
|
|
2916
|
+
en: "Too many requests, please wait a moment and try again.",
|
|
2917
|
+
zh: "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u7B49\u7247\u523B\u518D\u8BD5\u3002"
|
|
2918
|
+
},
|
|
2919
|
+
"gateway.error.dsers": {
|
|
2920
|
+
en: "DSers connection error, please send /reset to reconnect.",
|
|
2921
|
+
zh: "DSers \u8FDE\u63A5\u5F02\u5E38\uFF0C\u8BF7\u53D1\u9001 /reset \u91CD\u65B0\u767B\u5F55\u3002"
|
|
2922
|
+
},
|
|
2923
|
+
// Stream errors
|
|
2924
|
+
"gateway.stream.stageTimeout": {
|
|
2925
|
+
en: "{{stage}} timed out \u2014 LLM may be slow or unreachable. Type /retry to retry.",
|
|
2926
|
+
zh: "{{stage}}\u8D85\u65F6 \u2014 LLM \u670D\u52A1\u53EF\u80FD\u8F83\u6162\u6216\u4E0D\u53EF\u8FBE\u3002\u8F93\u5165 /retry \u91CD\u8BD5\u4E0A\u4E00\u6761\u6D88\u606F\u3002"
|
|
2927
|
+
},
|
|
2928
|
+
"gateway.stream.badApiKey": {
|
|
2929
|
+
en: "API key may be invalid or expired. Send /reset to reconfigure.",
|
|
2930
|
+
zh: "API key \u53EF\u80FD\u65E0\u6548\u6216\u5DF2\u8FC7\u671F\uFF0C\u8F93\u5165 /reset \u91CD\u65B0\u914D\u7F6E\u3002"
|
|
2931
|
+
},
|
|
2932
|
+
"gateway.stream.processFailed": {
|
|
2933
|
+
en: "Processing error, please try again. Type /retry to retry.",
|
|
2934
|
+
zh: "\u5904\u7406\u51FA\u9519\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002\u8F93\u5165 /retry \u91CD\u8BD5\u4E0A\u4E00\u6761\u6D88\u606F\u3002"
|
|
2935
|
+
},
|
|
2936
|
+
"gateway.stream.incomplete": {
|
|
2937
|
+
en: "\u26A0\uFE0F Response incomplete, please retry.",
|
|
2938
|
+
zh: "\u26A0\uFE0F \u54CD\u5E94\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u8BD5\u3002"
|
|
2939
|
+
},
|
|
2940
|
+
"gateway.stream.noReply": {
|
|
2941
|
+
en: "\u26A0\uFE0F No model reply received, please retry.",
|
|
2942
|
+
zh: "\u26A0\uFE0F \u672A\u6536\u5230\u6A21\u578B\u56DE\u590D\uFF0C\u8BF7\u91CD\u8BD5\u3002"
|
|
2943
|
+
},
|
|
2944
|
+
"gateway.stream.toolsNoText": {
|
|
2945
|
+
en: "Completed {{count}} operations ({{names}}), but the model did not generate a summary. Let me know if you'd like to see the results.",
|
|
2946
|
+
zh: "\u5DF2\u5B8C\u6210 {{count}} \u9879\u64CD\u4F5C\uFF08{{names}}\uFF09\uFF0C\u4F46\u6A21\u578B\u672A\u751F\u6210\u6587\u5B57\u603B\u7ED3\u3002\u5982\u9700\u67E5\u770B\u7ED3\u679C\u8BF7\u544A\u8BC9\u6211\u3002"
|
|
2947
|
+
},
|
|
2948
|
+
// Timeout stage names
|
|
2949
|
+
"gateway.stage.llmConnection": { en: "LLM connection", zh: "LLM \u8FDE\u63A5" },
|
|
2950
|
+
"gateway.stage.llmResponse": { en: "LLM response", zh: "LLM \u54CD\u5E94" },
|
|
2951
|
+
"gateway.stage.rulesApply": { en: "Rules application", zh: "\u89C4\u5219\u5E94\u7528" },
|
|
2952
|
+
"gateway.stage.productImport": { en: "Product import", zh: "\u5546\u54C1\u5BFC\u5165" },
|
|
2953
|
+
"gateway.stage.processing": { en: "Processing", zh: "\u5904\u7406" },
|
|
2954
|
+
// MCP
|
|
2955
|
+
"gateway.mcp.unavailable": {
|
|
2956
|
+
en: "\u26A0\uFE0F DSers connection issue: only basic search is available. Import/push/delete operations are unavailable. Try sending /reset to reconnect.",
|
|
2957
|
+
zh: "\u26A0\uFE0F DSers \u8FDE\u63A5\u5F02\u5E38\uFF1A\u4EC5\u57FA\u7840\u641C\u7D22\u53EF\u7528\uFF0C\u5BFC\u5165/\u63A8\u9001/\u5220\u9664\u7B49\u64CD\u4F5C\u6682\u4E0D\u53EF\u7528\u3002\u8BF7\u5C1D\u8BD5\u53D1\u9001 /reset \u91CD\u65B0\u8FDE\u63A5\u3002"
|
|
2958
|
+
},
|
|
2959
|
+
// Onboarding
|
|
2960
|
+
"gateway.onboard.invalidEmail": {
|
|
2961
|
+
en: "That doesn't look like an email address. Please enter your DSers login email:",
|
|
2962
|
+
zh: "\u8FD9\u4E0D\u50CF\u4E00\u4E2A\u90AE\u7BB1\u5730\u5740\u3002\u8BF7\u8F93\u5165\u4F60\u7684 DSers \u767B\u5F55\u90AE\u7BB1\uFF1A"
|
|
2963
|
+
},
|
|
2964
|
+
"gateway.onboard.passwordPrompt": {
|
|
2965
|
+
en: "Got it!\n\n*Step 2/3:* Now enter your DSers password.",
|
|
2966
|
+
zh: "\u6536\u5230\uFF01\n\n*\u6B65\u9AA4 2/3\uFF1A* \u8BF7\u8F93\u5165\u4F60\u7684 DSers \u5BC6\u7801\u3002"
|
|
2967
|
+
},
|
|
2968
|
+
"gateway.onboard.passwordPromptNote": {
|
|
2969
|
+
en: "\n_(Your password is encrypted locally and never sent to any third party.)_",
|
|
2970
|
+
zh: "\n_\uFF08\u5BC6\u7801\u52A0\u5BC6\u5B58\u50A8\u5728\u672C\u5730\uFF0C\u4E0D\u4F1A\u53D1\u9001\u7ED9\u7B2C\u4E09\u65B9\u3002\uFF09_"
|
|
2971
|
+
},
|
|
2972
|
+
"gateway.onboard.emptyPassword": {
|
|
2973
|
+
en: "Password cannot be empty. Please try again:",
|
|
2974
|
+
zh: "\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u8BF7\u91CD\u8BD5\uFF1A"
|
|
2975
|
+
},
|
|
2976
|
+
"gateway.onboard.verifying": {
|
|
2977
|
+
en: "Verifying with DSers...",
|
|
2978
|
+
zh: "\u6B63\u5728\u9A8C\u8BC1 DSers \u8D26\u53F7..."
|
|
2979
|
+
},
|
|
2980
|
+
"gateway.onboard.loginFailed": {
|
|
2981
|
+
en: "Wrong email or password. Please re-enter your password:",
|
|
2982
|
+
zh: "\u90AE\u7BB1\u6216\u5BC6\u7801\u9519\u8BEF\u3002\u8BF7\u91CD\u65B0\u8F93\u5165\u5BC6\u7801\uFF1A"
|
|
2983
|
+
},
|
|
2984
|
+
"gateway.onboard.loginFailedGeneric": {
|
|
2985
|
+
en: "Login failed: {{error}}. Try again:",
|
|
2986
|
+
zh: "\u767B\u5F55\u5931\u8D25\uFF1A{{error}}\u3002\u8BF7\u91CD\u8BD5\uFF1A"
|
|
2987
|
+
},
|
|
2988
|
+
"gateway.onboard.chooseProvider": {
|
|
2989
|
+
en: "You chose *{{provider}}*. Now paste your API key below.",
|
|
2990
|
+
zh: "\u4F60\u9009\u62E9\u4E86 *{{provider}}*\u3002\u8BF7\u5728\u4E0B\u65B9\u7C98\u8D34 API Key\u3002"
|
|
2991
|
+
},
|
|
2992
|
+
"gateway.onboard.invalidApiKey": {
|
|
2993
|
+
en: "That doesn't look like a valid API key. Please paste your key:",
|
|
2994
|
+
zh: "\u8FD9\u4E0D\u50CF\u4E00\u4E2A\u6709\u6548\u7684 API Key\u3002\u8BF7\u7C98\u8D34\u4F60\u7684 Key\uFF1A"
|
|
2995
|
+
}
|
|
2996
|
+
};
|
|
2997
|
+
function t(key, lang, vars) {
|
|
2998
|
+
const l = lang === "zh" ? "zh" : "en";
|
|
2999
|
+
const tpl = m[key]?.[l] ?? m[key]?.en ?? key;
|
|
3000
|
+
if (!vars) return tpl;
|
|
3001
|
+
return tpl.replace(/\{\{(\w+)\}\}/g, (_, k) => String(vars[k] ?? ""));
|
|
3002
|
+
}
|
|
3003
|
+
|
|
1416
3004
|
// src/gateway/gateway.ts
|
|
1417
|
-
var
|
|
3005
|
+
var log16 = createLogger("gateway");
|
|
3006
|
+
var SESSION_MAX_SIZE = 500;
|
|
3007
|
+
var SESSION_TTL_MS = 2 * 60 * 60 * 1e3;
|
|
1418
3008
|
var sessionHistories = /* @__PURE__ */ new Map();
|
|
3009
|
+
var sessionLastAccess = /* @__PURE__ */ new Map();
|
|
1419
3010
|
var MAX_HISTORY = 20;
|
|
3011
|
+
function touchSession(key) {
|
|
3012
|
+
sessionLastAccess.set(key, Date.now());
|
|
3013
|
+
if (sessionHistories.size > SESSION_MAX_SIZE) {
|
|
3014
|
+
let oldest = "";
|
|
3015
|
+
let oldestTs = Infinity;
|
|
3016
|
+
for (const [k, ts] of sessionLastAccess) {
|
|
3017
|
+
if (ts < oldestTs) {
|
|
3018
|
+
oldest = k;
|
|
3019
|
+
oldestTs = ts;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
if (oldest) {
|
|
3023
|
+
sessionHistories.delete(oldest);
|
|
3024
|
+
sessionLastAccess.delete(oldest);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
setInterval(() => {
|
|
3029
|
+
const now = Date.now();
|
|
3030
|
+
for (const [key, ts] of sessionLastAccess) {
|
|
3031
|
+
if (now - ts > SESSION_TTL_MS) {
|
|
3032
|
+
sessionHistories.delete(key);
|
|
3033
|
+
sessionLastAccess.delete(key);
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
}, 10 * 60 * 1e3).unref();
|
|
1420
3037
|
function detectProviderFromKey(key) {
|
|
1421
3038
|
if (key.startsWith("sk-ant-")) return "anthropic";
|
|
1422
3039
|
if (key.startsWith("sk-")) return "openai";
|
|
@@ -1429,34 +3046,41 @@ var DSClawGateway = class {
|
|
|
1429
3046
|
webChannel = null;
|
|
1430
3047
|
memory;
|
|
1431
3048
|
agents = /* @__PURE__ */ new Map();
|
|
3049
|
+
jobStores = /* @__PURE__ */ new Map();
|
|
1432
3050
|
dsersClients = /* @__PURE__ */ new Map();
|
|
3051
|
+
lastUserMessages = /* @__PURE__ */ new Map();
|
|
1433
3052
|
running = false;
|
|
1434
3053
|
constructor(config) {
|
|
1435
3054
|
this.config = config;
|
|
1436
3055
|
this.memory = new FileMemoryProvider();
|
|
1437
3056
|
}
|
|
3057
|
+
getLang(session, channel) {
|
|
3058
|
+
if (session.language === "zh" || session.language === "en") return session.language;
|
|
3059
|
+
return channel instanceof WebChatServer ? "en" : "en";
|
|
3060
|
+
}
|
|
1438
3061
|
async start() {
|
|
1439
|
-
|
|
3062
|
+
log16.info("Starting DSClaw Gateway...");
|
|
1440
3063
|
const web = new WebChatServer();
|
|
1441
3064
|
web.onMessage((msg) => this.handleMessage(web, msg));
|
|
1442
3065
|
web.onConnect((userId) => this.handleWebConnect(web, userId));
|
|
3066
|
+
web.onSettings((userId, action, data) => this.handleSettingsMessage(web, userId, action, data));
|
|
1443
3067
|
await web.connect({ port: this.config.port });
|
|
1444
3068
|
this.webChannel = web;
|
|
1445
3069
|
this.channels.push(web);
|
|
1446
|
-
|
|
3070
|
+
log16.info({ port: this.config.port }, "Web chat channel connected");
|
|
1447
3071
|
if (this.config.telegramBotToken) {
|
|
1448
3072
|
try {
|
|
1449
3073
|
const tg = new TelegramAdapter();
|
|
1450
3074
|
tg.onMessage((msg) => this.handleMessage(tg, msg));
|
|
1451
3075
|
await tg.connect({ botToken: this.config.telegramBotToken });
|
|
1452
3076
|
this.channels.push(tg);
|
|
1453
|
-
|
|
3077
|
+
log16.info("Telegram channel connected");
|
|
1454
3078
|
} catch (error) {
|
|
1455
|
-
|
|
3079
|
+
log16.warn({ error }, "Telegram failed to connect \u2014 web chat still available");
|
|
1456
3080
|
}
|
|
1457
3081
|
}
|
|
1458
3082
|
this.running = true;
|
|
1459
|
-
|
|
3083
|
+
log16.info("DSClaw Gateway started");
|
|
1460
3084
|
}
|
|
1461
3085
|
async handleMessage(channel, message) {
|
|
1462
3086
|
await runWithTrace(
|
|
@@ -1468,14 +3092,14 @@ var DSClawGateway = class {
|
|
|
1468
3092
|
async () => {
|
|
1469
3093
|
const userId = message.userId;
|
|
1470
3094
|
if (!checkInboundLimit(userId)) {
|
|
1471
|
-
|
|
1472
|
-
await channel.send(userId, { text: "
|
|
3095
|
+
log16.warn({ userId }, "Rate limit exceeded");
|
|
3096
|
+
await channel.send(userId, { text: t("gateway.rateLimit", loadSession(userId).language) });
|
|
1473
3097
|
return;
|
|
1474
3098
|
}
|
|
1475
3099
|
await debounceUser(userId);
|
|
1476
3100
|
const release = await acquireUserLock(userId);
|
|
3101
|
+
const session = loadSession(userId);
|
|
1477
3102
|
try {
|
|
1478
|
-
const session = loadSession(userId);
|
|
1479
3103
|
if (isOnboarding(session.state)) {
|
|
1480
3104
|
await this.handleOnboarding(channel, message, session);
|
|
1481
3105
|
} else {
|
|
@@ -1486,10 +3110,21 @@ var DSClawGateway = class {
|
|
|
1486
3110
|
await this.handleReady(channel, message, session);
|
|
1487
3111
|
}
|
|
1488
3112
|
} catch (error) {
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
3113
|
+
log16.error({ error, userId }, "Failed to handle message");
|
|
3114
|
+
let userMsg = t("gateway.error.generic", session.language);
|
|
3115
|
+
if (error instanceof Error) {
|
|
3116
|
+
const msg = error.message.toLowerCase();
|
|
3117
|
+
if (msg.includes("timeout") || msg.includes("timed out")) {
|
|
3118
|
+
userMsg = t("gateway.error.timeout", session.language);
|
|
3119
|
+
} else if (msg.includes("401") || msg.includes("unauthorized") || msg.includes("api key")) {
|
|
3120
|
+
userMsg = t("gateway.error.apiKey", session.language);
|
|
3121
|
+
} else if (msg.includes("429") || msg.includes("rate limit")) {
|
|
3122
|
+
userMsg = t("gateway.error.rateLimitLlm", session.language);
|
|
3123
|
+
} else if (msg.includes("session") || msg.includes("dsers")) {
|
|
3124
|
+
userMsg = t("gateway.error.dsers", session.language);
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
await channel.send(userId, { text: userMsg });
|
|
1493
3128
|
} finally {
|
|
1494
3129
|
release();
|
|
1495
3130
|
}
|
|
@@ -1499,12 +3134,207 @@ var DSClawGateway = class {
|
|
|
1499
3134
|
// ─── Auto-welcome on WebSocket connect ──────────────────────────
|
|
1500
3135
|
handleWebConnect(channel, userId) {
|
|
1501
3136
|
const session = loadSession(userId);
|
|
3137
|
+
this.pushSettingsState(channel, userId, session);
|
|
1502
3138
|
if (session.state === "new") {
|
|
1503
3139
|
this.onboardWelcome(channel, userId, session).catch((err) => {
|
|
1504
|
-
|
|
3140
|
+
log16.error({ error: err, userId }, "Auto-welcome failed");
|
|
1505
3141
|
});
|
|
1506
3142
|
}
|
|
1507
3143
|
}
|
|
3144
|
+
// ─── Settings Panel (Web Only) ─────────────────────────────────
|
|
3145
|
+
isDsersConnected(session) {
|
|
3146
|
+
return !!session.dspiSessionId;
|
|
3147
|
+
}
|
|
3148
|
+
pushSettingsState(channel, userId, session) {
|
|
3149
|
+
const dsersOk = this.isDsersConnected(session);
|
|
3150
|
+
channel.sendSettingsState(userId, {
|
|
3151
|
+
dsers: { configured: dsersOk, email: dsersOk ? session.dspiEmail : void 0 },
|
|
3152
|
+
llm: { configured: !!session.llmApiKey, provider: session.llmProvider, baseUrl: session.llmBaseUrl },
|
|
3153
|
+
ready: session.state === "ready"
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
async applyDSersSession(channel, userId, session, sessionId, sessionState) {
|
|
3157
|
+
const dsersConfig = createDSersConfig(session.dspiEmail ?? "browser");
|
|
3158
|
+
dsersConfig.sessionId = sessionId;
|
|
3159
|
+
dsersConfig.sessionState = sessionState;
|
|
3160
|
+
const client = new DSersClient(dsersConfig);
|
|
3161
|
+
const info = await client.get("/account-user-bff/v1/users/info");
|
|
3162
|
+
const data = info["data"];
|
|
3163
|
+
const email = data?.["email"] ?? session.dspiEmail ?? "connected";
|
|
3164
|
+
session.dspiEmail = email;
|
|
3165
|
+
session.dspiSessionId = sessionId;
|
|
3166
|
+
session.dspiSessionState = sessionState;
|
|
3167
|
+
session.dspiPassword = void 0;
|
|
3168
|
+
this.dsersClients.set(userId, client);
|
|
3169
|
+
if (session.llmApiKey) {
|
|
3170
|
+
session.state = "ready";
|
|
3171
|
+
} else if (!["onboard_llm", "ready"].includes(session.state)) {
|
|
3172
|
+
session.state = "onboard_llm";
|
|
3173
|
+
}
|
|
3174
|
+
saveSession(userId, session);
|
|
3175
|
+
channel.sendSettingsResult(userId, { section: "dsers", success: true });
|
|
3176
|
+
this.pushSettingsState(channel, userId, session);
|
|
3177
|
+
if (session.state === "ready") {
|
|
3178
|
+
await this.sendReadyMessage(channel, userId);
|
|
3179
|
+
}
|
|
3180
|
+
return true;
|
|
3181
|
+
}
|
|
3182
|
+
async triggerAuth(userId) {
|
|
3183
|
+
if (isAuthInProgress()) {
|
|
3184
|
+
return { success: false, reason: "in_progress" };
|
|
3185
|
+
}
|
|
3186
|
+
try {
|
|
3187
|
+
const result = await loginViaCDP();
|
|
3188
|
+
const session = loadSession(userId);
|
|
3189
|
+
const dsersConfig = createDSersConfig(session.dspiEmail ?? "browser");
|
|
3190
|
+
dsersConfig.sessionId = result.sessionId;
|
|
3191
|
+
dsersConfig.sessionState = result.state;
|
|
3192
|
+
const client = new DSersClient(dsersConfig);
|
|
3193
|
+
const info = await client.get("/account-user-bff/v1/users/info");
|
|
3194
|
+
const data = info?.["data"];
|
|
3195
|
+
const email = data?.["email"] ?? "connected";
|
|
3196
|
+
session.dspiEmail = email;
|
|
3197
|
+
session.dspiSessionId = result.sessionId;
|
|
3198
|
+
session.dspiSessionState = result.state;
|
|
3199
|
+
session.dspiPassword = void 0;
|
|
3200
|
+
if (session.llmApiKey) session.state = "ready";
|
|
3201
|
+
saveSession(userId, session);
|
|
3202
|
+
this.dsersClients.set(userId, client);
|
|
3203
|
+
this.agents.delete(userId);
|
|
3204
|
+
log16.info({ userId, email }, "triggerAuth succeeded \u2014 agent will be recreated on next message");
|
|
3205
|
+
return { success: true, email };
|
|
3206
|
+
} catch (err) {
|
|
3207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3208
|
+
log16.warn({ error: msg, userId }, "triggerAuth failed");
|
|
3209
|
+
if (msg.includes("cancelled") || msg.includes("closed")) {
|
|
3210
|
+
return { success: false, reason: "cancelled" };
|
|
3211
|
+
}
|
|
3212
|
+
if (msg.includes("timed out") || msg.includes("Timed out")) {
|
|
3213
|
+
return { success: false, reason: "timeout" };
|
|
3214
|
+
}
|
|
3215
|
+
if (msg.includes("No Chromium") || msg.includes("browser found")) {
|
|
3216
|
+
return { success: false, reason: "no_browser" };
|
|
3217
|
+
}
|
|
3218
|
+
return { success: false, reason: msg };
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
async handleSettingsMessage(channel, userId, action, data) {
|
|
3222
|
+
const session = loadSession(userId);
|
|
3223
|
+
switch (action) {
|
|
3224
|
+
case "set_language": {
|
|
3225
|
+
const lang = data["lang"];
|
|
3226
|
+
if (lang === "en" || lang === "zh") {
|
|
3227
|
+
session.language = lang;
|
|
3228
|
+
saveSession(userId, session);
|
|
3229
|
+
}
|
|
3230
|
+
break;
|
|
3231
|
+
}
|
|
3232
|
+
case "get_settings":
|
|
3233
|
+
this.pushSettingsState(channel, userId, session);
|
|
3234
|
+
break;
|
|
3235
|
+
case "start_dsers_auth": {
|
|
3236
|
+
if (isAuthInProgress()) {
|
|
3237
|
+
channel.sendSettingsResult(userId, { section: "dsers", success: false, error: "Auth already in progress." });
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
channel.sendSettingsResult(userId, { section: "dsers_auth_started", success: true });
|
|
3241
|
+
loginViaCDP().then(async (result) => {
|
|
3242
|
+
const freshSession = loadSession(userId);
|
|
3243
|
+
await this.applyDSersSession(channel, userId, freshSession, result.sessionId, result.state);
|
|
3244
|
+
}).catch((error) => {
|
|
3245
|
+
const msg = error instanceof Error ? error.message : "unknown error";
|
|
3246
|
+
log16.warn({ error: msg, userId }, "Browser auth failed");
|
|
3247
|
+
channel.sendSettingsResult(userId, { section: "dsers", success: false, error: msg });
|
|
3248
|
+
});
|
|
3249
|
+
break;
|
|
3250
|
+
}
|
|
3251
|
+
case "cancel_dsers_auth": {
|
|
3252
|
+
cancelAuth();
|
|
3253
|
+
channel.sendSettingsResult(userId, { section: "dsers_cancelled", success: true });
|
|
3254
|
+
break;
|
|
3255
|
+
}
|
|
3256
|
+
case "save_dsers_session": {
|
|
3257
|
+
const sessionId = data["sessionId"]?.trim();
|
|
3258
|
+
const sessionState = data["state"]?.trim() ?? "";
|
|
3259
|
+
if (!sessionId || sessionId.length < 10) {
|
|
3260
|
+
channel.sendSettingsResult(userId, { section: "dsers", success: false, error: "Invalid session token." });
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
try {
|
|
3264
|
+
await this.applyDSersSession(channel, userId, session, sessionId, sessionState);
|
|
3265
|
+
} catch {
|
|
3266
|
+
channel.sendSettingsResult(userId, { section: "dsers", success: false, error: "Session token invalid or expired." });
|
|
3267
|
+
}
|
|
3268
|
+
break;
|
|
3269
|
+
}
|
|
3270
|
+
case "disconnect_dsers": {
|
|
3271
|
+
session.dspiEmail = void 0;
|
|
3272
|
+
session.dspiSessionId = void 0;
|
|
3273
|
+
session.dspiSessionState = void 0;
|
|
3274
|
+
session.state = "new";
|
|
3275
|
+
saveSession(userId, session);
|
|
3276
|
+
this.agents.delete(userId);
|
|
3277
|
+
this.pushSettingsState(channel, userId, session);
|
|
3278
|
+
break;
|
|
3279
|
+
}
|
|
3280
|
+
case "save_llm": {
|
|
3281
|
+
let provider = data["provider"]?.trim();
|
|
3282
|
+
const apiKey = data["apiKey"]?.trim();
|
|
3283
|
+
let baseUrl = data["baseUrl"]?.trim() || void 0;
|
|
3284
|
+
const model = data["model"]?.trim() || void 0;
|
|
3285
|
+
if (baseUrl) {
|
|
3286
|
+
if (!/^https?:\/\//i.test(baseUrl)) baseUrl = `https://${baseUrl}`;
|
|
3287
|
+
baseUrl = baseUrl.replace(/\/+$/, "");
|
|
3288
|
+
if (!/\/v\d/.test(baseUrl)) baseUrl += "/v1";
|
|
3289
|
+
}
|
|
3290
|
+
if (!apiKey || apiKey.length < 3) {
|
|
3291
|
+
channel.sendSettingsResult(userId, { section: "llm", success: false, error: "Please enter a valid API key." });
|
|
3292
|
+
return;
|
|
3293
|
+
}
|
|
3294
|
+
if (provider === "other" && !baseUrl) {
|
|
3295
|
+
channel.sendSettingsResult(userId, { section: "llm", success: false, error: "Please enter the API base URL." });
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
if (!provider) {
|
|
3299
|
+
provider = detectProviderFromKey(apiKey) ?? "";
|
|
3300
|
+
}
|
|
3301
|
+
if (!provider) {
|
|
3302
|
+
channel.sendSettingsResult(userId, { section: "llm", success: false, error: "Cannot detect provider. Please select one." });
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3305
|
+
session.llmProvider = provider;
|
|
3306
|
+
session.llmApiKey = apiKey;
|
|
3307
|
+
session.llmBaseUrl = baseUrl;
|
|
3308
|
+
session.llmModel = model || getDefaultModel(provider);
|
|
3309
|
+
if (this.isDsersConnected(session)) {
|
|
3310
|
+
session.state = "ready";
|
|
3311
|
+
}
|
|
3312
|
+
saveSession(userId, session);
|
|
3313
|
+
this.agents.delete(userId);
|
|
3314
|
+
channel.sendSettingsResult(userId, { section: "llm", success: true });
|
|
3315
|
+
this.pushSettingsState(channel, userId, session);
|
|
3316
|
+
if (session.state === "ready") {
|
|
3317
|
+
await this.sendReadyMessage(channel, userId);
|
|
3318
|
+
}
|
|
3319
|
+
break;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
async sendReadyMessage(channel, userId) {
|
|
3324
|
+
const session = loadSession(userId);
|
|
3325
|
+
if (channel instanceof WebChatServer) {
|
|
3326
|
+
channel.sendToast(userId, t("gateway.ready.toast", session.language), "info", "sys-ready");
|
|
3327
|
+
} else {
|
|
3328
|
+
await channel.send(userId, { text: t("gateway.ready.telegram", session.language) });
|
|
3329
|
+
}
|
|
3330
|
+
writeAuditLog({
|
|
3331
|
+
userId,
|
|
3332
|
+
agentId: "gateway",
|
|
3333
|
+
action: "onboarding_complete",
|
|
3334
|
+
target: "user-session",
|
|
3335
|
+
result: "success"
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
1508
3338
|
// ─── Onboarding State Machine ─────────────────────────────────────
|
|
1509
3339
|
async handleOnboarding(channel, message, session) {
|
|
1510
3340
|
const userId = message.userId;
|
|
@@ -1528,15 +3358,16 @@ var DSClawGateway = class {
|
|
|
1528
3358
|
}
|
|
1529
3359
|
}
|
|
1530
3360
|
async onboardWelcome(channel, userId, session) {
|
|
1531
|
-
const
|
|
3361
|
+
const isWeb = channel instanceof WebChatServer;
|
|
3362
|
+
const welcome = t(isWeb ? "gateway.welcome.web" : "gateway.welcome.telegram", session.language);
|
|
1532
3363
|
await channel.send(userId, { text: welcome });
|
|
1533
|
-
session.state = "onboard_dsers_email";
|
|
3364
|
+
session.state = isWeb ? "ready" : "onboard_dsers_email";
|
|
1534
3365
|
saveSession(userId, session);
|
|
1535
3366
|
}
|
|
1536
3367
|
async onboardEmail(channel, userId, text, session) {
|
|
1537
3368
|
if (!text.includes("@") || !text.includes(".")) {
|
|
1538
3369
|
await channel.send(userId, {
|
|
1539
|
-
text: "
|
|
3370
|
+
text: t("gateway.onboard.invalidEmail", session.language)
|
|
1540
3371
|
});
|
|
1541
3372
|
return;
|
|
1542
3373
|
}
|
|
@@ -1547,7 +3378,7 @@ var DSClawGateway = class {
|
|
|
1547
3378
|
channel.sendSetInputType(userId, "password");
|
|
1548
3379
|
}
|
|
1549
3380
|
await channel.send(userId, {
|
|
1550
|
-
text: "
|
|
3381
|
+
text: t("gateway.onboard.passwordPrompt", session.language) + (channel instanceof WebChatServer ? "" : t("gateway.onboard.passwordPromptNote", session.language))
|
|
1551
3382
|
});
|
|
1552
3383
|
}
|
|
1553
3384
|
async onboardPassword(channel, message, session, isWeb) {
|
|
@@ -1563,19 +3394,20 @@ var DSClawGateway = class {
|
|
|
1563
3394
|
);
|
|
1564
3395
|
}
|
|
1565
3396
|
if (password.length < 1) {
|
|
1566
|
-
await channel.send(userId, { text: "
|
|
3397
|
+
await channel.send(userId, { text: t("gateway.onboard.emptyPassword", session.language) });
|
|
1567
3398
|
if (isWeb) channel.sendSetInputType(userId, "password");
|
|
1568
3399
|
return;
|
|
1569
3400
|
}
|
|
1570
3401
|
session.dspiPassword = password;
|
|
1571
3402
|
saveSession(userId, session);
|
|
1572
|
-
await channel.send(userId, { text: "
|
|
3403
|
+
await channel.send(userId, { text: t("gateway.onboard.verifying", session.language) });
|
|
1573
3404
|
try {
|
|
1574
3405
|
const dsersConfig = createDSersConfig(session.dspiEmail, password);
|
|
1575
3406
|
const client = new DSersClient(dsersConfig);
|
|
1576
3407
|
await client.get("/account-user-bff/v1/users/info");
|
|
1577
3408
|
this.dsersClients.set(userId, client);
|
|
1578
3409
|
session.state = "onboard_llm";
|
|
3410
|
+
session.dspiPassword = void 0;
|
|
1579
3411
|
saveSession(userId, session);
|
|
1580
3412
|
await channel.send(userId, {
|
|
1581
3413
|
card: {
|
|
@@ -1593,7 +3425,7 @@ var DSClawGateway = class {
|
|
|
1593
3425
|
session.state = "onboard_dsers_password";
|
|
1594
3426
|
saveSession(userId, session);
|
|
1595
3427
|
if (isWeb) channel.sendSetInputType(userId, "password");
|
|
1596
|
-
const msg = error instanceof Error && error.message.includes("401") ? "
|
|
3428
|
+
const msg = error instanceof Error && error.message.includes("401") ? t("gateway.onboard.loginFailed", session.language) : t("gateway.onboard.loginFailedGeneric", session.language, { error: error instanceof Error ? error.message : "unknown error" });
|
|
1597
3429
|
await channel.send(userId, { text: msg });
|
|
1598
3430
|
}
|
|
1599
3431
|
}
|
|
@@ -1607,7 +3439,7 @@ var DSClawGateway = class {
|
|
|
1607
3439
|
const providerName = provider === "openai" ? "OpenAI" : provider === "anthropic" ? "Anthropic" : "Google";
|
|
1608
3440
|
if (isWeb) channel.sendSetInputType(userId, "password");
|
|
1609
3441
|
await channel.send(userId, {
|
|
1610
|
-
text:
|
|
3442
|
+
text: t("gateway.onboard.chooseProvider", session.language, { provider: providerName }) + (isWeb ? "" : "\n_I will delete your message immediately._")
|
|
1611
3443
|
});
|
|
1612
3444
|
return;
|
|
1613
3445
|
}
|
|
@@ -1641,7 +3473,7 @@ var DSClawGateway = class {
|
|
|
1641
3473
|
);
|
|
1642
3474
|
}
|
|
1643
3475
|
if (text.length < 10) {
|
|
1644
|
-
await channel.send(userId, { text: "
|
|
3476
|
+
await channel.send(userId, { text: t("gateway.onboard.invalidApiKey", session.language) });
|
|
1645
3477
|
if (isWeb) channel.sendSetInputType(userId, "password");
|
|
1646
3478
|
return;
|
|
1647
3479
|
}
|
|
@@ -1650,15 +3482,7 @@ var DSClawGateway = class {
|
|
|
1650
3482
|
session.state = "ready";
|
|
1651
3483
|
saveSession(userId, session);
|
|
1652
3484
|
await channel.send(userId, {
|
|
1653
|
-
text:
|
|
1654
|
-
|
|
1655
|
-
Try these:
|
|
1656
|
-
- "Show me my stores"
|
|
1657
|
-
- "What's in my import list?"
|
|
1658
|
-
- "Search for phone cases on AliExpress"
|
|
1659
|
-
- "Check my pricing rules"
|
|
1660
|
-
|
|
1661
|
-
Just type naturally \u2014 I understand everyday language!`
|
|
3485
|
+
text: t("gateway.ready.telegram", session.language)
|
|
1662
3486
|
});
|
|
1663
3487
|
writeAuditLog({
|
|
1664
3488
|
userId,
|
|
@@ -1671,22 +3495,94 @@ Just type naturally \u2014 I understand everyday language!`
|
|
|
1671
3495
|
// ─── Ready State: Route to AI Agent ───────────────────────────────
|
|
1672
3496
|
async handleReady(channel, message, session) {
|
|
1673
3497
|
const userId = message.userId;
|
|
1674
|
-
|
|
3498
|
+
const cmd = message.text.trim().toLowerCase();
|
|
3499
|
+
if (cmd === "/reset") {
|
|
3500
|
+
this.agents.delete(userId);
|
|
3501
|
+
this.lastUserMessages.delete(userId);
|
|
3502
|
+
for (const key of sessionHistories.keys()) {
|
|
3503
|
+
if (key.endsWith(`:${userId}`)) sessionHistories.delete(key);
|
|
3504
|
+
}
|
|
3505
|
+
clearHistory(userId);
|
|
3506
|
+
const preserved = {
|
|
3507
|
+
state: "ready",
|
|
3508
|
+
dspiEmail: session.dspiEmail,
|
|
3509
|
+
dspiSessionId: session.dspiSessionId,
|
|
3510
|
+
dspiSessionState: session.dspiSessionState,
|
|
3511
|
+
llmProvider: session.llmProvider,
|
|
3512
|
+
llmApiKey: session.llmApiKey,
|
|
3513
|
+
llmModel: session.llmModel,
|
|
3514
|
+
llmBaseUrl: session.llmBaseUrl,
|
|
3515
|
+
language: session.language
|
|
3516
|
+
};
|
|
3517
|
+
saveSession(userId, preserved);
|
|
3518
|
+
if (channel instanceof WebChatServer) {
|
|
3519
|
+
channel.sendReset(userId);
|
|
3520
|
+
this.pushSettingsState(channel, userId, preserved);
|
|
3521
|
+
channel.sendToast(userId, t("gateway.cmd.reset", session.language), "info", "sys-reset");
|
|
3522
|
+
} else {
|
|
3523
|
+
await channel.send(userId, { text: t("gateway.cmd.reset", session.language) });
|
|
3524
|
+
}
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
if (cmd === "/logout") {
|
|
1675
3528
|
this.agents.delete(userId);
|
|
3529
|
+
this.jobStores.delete(userId);
|
|
1676
3530
|
this.dsersClients.delete(userId);
|
|
1677
|
-
|
|
3531
|
+
this.lastUserMessages.delete(userId);
|
|
3532
|
+
for (const key of sessionHistories.keys()) {
|
|
3533
|
+
if (key.endsWith(`:${userId}`)) sessionHistories.delete(key);
|
|
3534
|
+
}
|
|
3535
|
+
clearHistory(userId);
|
|
1678
3536
|
saveSession(userId, { state: "new" });
|
|
1679
|
-
|
|
3537
|
+
if (channel instanceof WebChatServer) {
|
|
3538
|
+
channel.sendReset(userId);
|
|
3539
|
+
this.pushSettingsState(channel, userId, { state: "new" });
|
|
3540
|
+
channel.sendToast(userId, t("gateway.cmd.logout", session.language), "info", "sys-logout");
|
|
3541
|
+
} else {
|
|
3542
|
+
await channel.send(userId, { text: t("gateway.cmd.logout", session.language) });
|
|
3543
|
+
}
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
if (cmd === "/retry") {
|
|
3547
|
+
const lastMsg = this.lastUserMessages.get(userId);
|
|
3548
|
+
if (!lastMsg) {
|
|
3549
|
+
if (channel instanceof WebChatServer) {
|
|
3550
|
+
channel.sendToast(userId, t("gateway.cmd.retryEmpty", session.language), "warn", "sys-retry-empty");
|
|
3551
|
+
} else {
|
|
3552
|
+
await channel.send(userId, { text: t("gateway.cmd.retryEmpty", session.language) });
|
|
3553
|
+
}
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
log16.info({ userId, retryText: lastMsg.slice(0, 80) }, "Retrying last message");
|
|
3557
|
+
message = { ...message, text: lastMsg };
|
|
3558
|
+
} else {
|
|
3559
|
+
this.lastUserMessages.set(userId, message.text);
|
|
3560
|
+
}
|
|
3561
|
+
if (!session.llmApiKey) {
|
|
3562
|
+
if (channel instanceof WebChatServer) {
|
|
3563
|
+
channel.sendToast(userId, t("gateway.noApiKey.web", session.language), "warn", "sys-no-apikey");
|
|
3564
|
+
} else {
|
|
3565
|
+
await channel.send(userId, { text: t("gateway.noApiKey.telegram", session.language) });
|
|
3566
|
+
}
|
|
1680
3567
|
return;
|
|
1681
3568
|
}
|
|
1682
3569
|
const agent = this.getOrCreateAgent(userId, session);
|
|
1683
3570
|
const sessionKey = `${channel.name}:${userId}`;
|
|
3571
|
+
touchSession(sessionKey);
|
|
1684
3572
|
const history = sessionHistories.get(sessionKey) ?? [];
|
|
3573
|
+
const budget = allocateBudget("", "", history, {});
|
|
3574
|
+
const budgetedHistory = budget.fallbackMode ? history.slice(-MAX_HISTORY) : budget.messages;
|
|
3575
|
+
if (budget.shouldCompact && !budget.fallbackMode) {
|
|
3576
|
+
log16.info(
|
|
3577
|
+
{ sessionKey, totalTokens: budget.totalTokens, remaining: budget.remainingTokens },
|
|
3578
|
+
"Context nearing limit \u2014 consider compaction"
|
|
3579
|
+
);
|
|
3580
|
+
}
|
|
1685
3581
|
const context = {
|
|
1686
3582
|
userId,
|
|
1687
3583
|
sessionId: sessionKey,
|
|
1688
3584
|
channelId: channel.name,
|
|
1689
|
-
history:
|
|
3585
|
+
history: budgetedHistory
|
|
1690
3586
|
};
|
|
1691
3587
|
writeAuditLog({
|
|
1692
3588
|
userId,
|
|
@@ -1698,12 +3594,12 @@ Just type naturally \u2014 I understand everyday language!`
|
|
|
1698
3594
|
});
|
|
1699
3595
|
try {
|
|
1700
3596
|
if (channel instanceof WebChatServer) {
|
|
1701
|
-
await this.handleReadyStreaming(channel, userId, message.text, agent, context, history, sessionKey);
|
|
3597
|
+
await this.handleReadyStreaming(channel, userId, message.text, agent, context, history, sessionKey, session, message.attachments);
|
|
1702
3598
|
} else {
|
|
1703
3599
|
await this.handleReadyBatch(channel, userId, message.text, agent, context, history, sessionKey);
|
|
1704
3600
|
}
|
|
1705
3601
|
} catch (error) {
|
|
1706
|
-
|
|
3602
|
+
log16.error({ error, userId }, "Agent processing failed");
|
|
1707
3603
|
writeAuditLog({
|
|
1708
3604
|
userId,
|
|
1709
3605
|
agentId: agent.id,
|
|
@@ -1712,31 +3608,164 @@ Just type naturally \u2014 I understand everyday language!`
|
|
|
1712
3608
|
result: "failed",
|
|
1713
3609
|
error: error instanceof Error ? error.message : String(error)
|
|
1714
3610
|
});
|
|
3611
|
+
if (channel instanceof WebChatServer) {
|
|
3612
|
+
channel.sendTyping(userId, false);
|
|
3613
|
+
channel.sendStatus(userId, "");
|
|
3614
|
+
channel.sendLog(userId, {
|
|
3615
|
+
level: "error",
|
|
3616
|
+
module: "gateway",
|
|
3617
|
+
msg: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
3618
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
1715
3621
|
const errMsg = error instanceof Error ? error.message : "";
|
|
1716
|
-
if (errMsg.includes("
|
|
3622
|
+
if (errMsg.includes("timed out")) {
|
|
3623
|
+
const stage = errMsg.includes("LLM init") ? t("gateway.stage.llmConnection", session.language) : errMsg.includes("LLM response") ? t("gateway.stage.llmResponse", session.language) : errMsg.includes("Rule reapply") ? t("gateway.stage.rulesApply", session.language) : errMsg.includes("Product import") ? t("gateway.stage.productImport", session.language) : t("gateway.stage.processing", session.language);
|
|
3624
|
+
await channel.send(userId, {
|
|
3625
|
+
text: t("gateway.stream.stageTimeout", session.language, { stage })
|
|
3626
|
+
});
|
|
3627
|
+
} else if (errMsg.includes("401") || errMsg.includes("API key")) {
|
|
1717
3628
|
await channel.send(userId, {
|
|
1718
|
-
text: "
|
|
3629
|
+
text: t("gateway.stream.badApiKey", session.language)
|
|
1719
3630
|
});
|
|
1720
3631
|
} else {
|
|
1721
3632
|
await channel.send(userId, {
|
|
1722
|
-
text: "
|
|
3633
|
+
text: t("gateway.stream.processFailed", session.language)
|
|
1723
3634
|
});
|
|
1724
3635
|
}
|
|
1725
3636
|
}
|
|
1726
3637
|
}
|
|
1727
|
-
async handleReadyStreaming(channel, userId, text, agent, context, history, sessionKey) {
|
|
3638
|
+
async handleReadyStreaming(channel, userId, text, agent, context, history, sessionKey, session, attachments) {
|
|
1728
3639
|
channel.sendTyping(userId, true);
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
channel.
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
3640
|
+
channel.sendStatus(userId, "Thinking...");
|
|
3641
|
+
const ts = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
3642
|
+
channel.sendLog(userId, { level: "info", module: "gateway", msg: `Processing: "${text.slice(0, 80)}"`, ts: ts() });
|
|
3643
|
+
const imageAttachments = attachments?.filter((a) => a.type === "image").map((a) => ({ url: a.url ?? "", data: a.data, mimeType: a.mimeType ?? "image/png" }));
|
|
3644
|
+
const runningTools = /* @__PURE__ */ new Map();
|
|
3645
|
+
let resolveToolsSettled = null;
|
|
3646
|
+
const toolsSettled = new Promise((r) => {
|
|
3647
|
+
resolveToolsSettled = r;
|
|
3648
|
+
});
|
|
3649
|
+
const { textStream, response } = agent.processStream(
|
|
3650
|
+
text,
|
|
3651
|
+
context,
|
|
3652
|
+
(status) => {
|
|
3653
|
+
channel.sendStatus(userId, status);
|
|
3654
|
+
},
|
|
3655
|
+
{
|
|
3656
|
+
onToolCall: (id, name, args) => {
|
|
3657
|
+
runningTools.set(id, { name, startedAt: Date.now() });
|
|
3658
|
+
channel.sendToolCallStart(userId, id, name, args);
|
|
3659
|
+
channel.sendLog(userId, { level: "info", module: "agent", msg: `Tool call: ${name}`, ts: ts() });
|
|
3660
|
+
},
|
|
3661
|
+
onToolResult: (id, name, result2, error, durationMs) => {
|
|
3662
|
+
runningTools.delete(id);
|
|
3663
|
+
channel.sendToolCallEnd(userId, id, name, result2, error, durationMs);
|
|
3664
|
+
channel.sendLog(userId, {
|
|
3665
|
+
level: error ? "error" : "info",
|
|
3666
|
+
module: "agent",
|
|
3667
|
+
msg: `Tool ${name} ${error ? "failed" : "done"} (${durationMs}ms)`,
|
|
3668
|
+
ts: ts()
|
|
3669
|
+
});
|
|
3670
|
+
if (runningTools.size === 0 && resolveToolsSettled) resolveToolsSettled();
|
|
3671
|
+
}
|
|
3672
|
+
},
|
|
3673
|
+
imageAttachments?.length ? imageAttachments : void 0
|
|
3674
|
+
);
|
|
3675
|
+
let streamStarted = false;
|
|
3676
|
+
try {
|
|
3677
|
+
for await (const chunk of textStream) {
|
|
3678
|
+
if (!streamStarted) {
|
|
3679
|
+
channel.sendTyping(userId, false);
|
|
3680
|
+
channel.sendStatus(userId, "");
|
|
3681
|
+
channel.sendStreamStart(userId);
|
|
3682
|
+
streamStarted = true;
|
|
3683
|
+
}
|
|
3684
|
+
channel.sendStreamDelta(userId, chunk);
|
|
3685
|
+
}
|
|
3686
|
+
} catch (streamErr) {
|
|
3687
|
+
log16.error({ error: streamErr, userId }, "Stream iteration failed");
|
|
3688
|
+
if (streamStarted) {
|
|
3689
|
+
const errMsg = streamErr instanceof Error ? streamErr.message : "Unknown error";
|
|
3690
|
+
channel.sendStreamDelta(userId, `
|
|
3691
|
+
|
|
3692
|
+
\u26A0\uFE0F ${errMsg}`);
|
|
3693
|
+
} else {
|
|
3694
|
+
response.catch(() => {
|
|
3695
|
+
});
|
|
3696
|
+
throw streamErr;
|
|
3697
|
+
}
|
|
3698
|
+
} finally {
|
|
3699
|
+
if (runningTools.size > 0) {
|
|
3700
|
+
try {
|
|
3701
|
+
await Promise.race([toolsSettled, new Promise((r) => setTimeout(r, 15e3))]);
|
|
3702
|
+
} catch {
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
for (const [id, info] of runningTools) {
|
|
3706
|
+
const elapsed = Date.now() - info.startedAt;
|
|
3707
|
+
channel.sendToolCallEnd(userId, id, info.name, void 0, "Aborted (stream terminated)", elapsed);
|
|
3708
|
+
}
|
|
3709
|
+
runningTools.clear();
|
|
3710
|
+
if (!streamStarted) {
|
|
3711
|
+
channel.sendTyping(userId, false);
|
|
3712
|
+
channel.sendStatus(userId, "");
|
|
3713
|
+
}
|
|
3714
|
+
channel.sendStreamEnd(userId);
|
|
3715
|
+
}
|
|
3716
|
+
log16.info({ userId, streamStarted }, "Stream loop finished, awaiting response finalization");
|
|
3717
|
+
let result;
|
|
3718
|
+
try {
|
|
3719
|
+
result = await Promise.race([
|
|
3720
|
+
response,
|
|
3721
|
+
new Promise(
|
|
3722
|
+
(_, reject) => setTimeout(() => reject(new Error("Response finalization timed out")), 6e4)
|
|
3723
|
+
)
|
|
3724
|
+
]);
|
|
3725
|
+
} catch (finalErr) {
|
|
3726
|
+
log16.warn({ error: finalErr, userId }, "Response finalization failed \u2014 saving partial history");
|
|
3727
|
+
history.push({ role: "user", content: text });
|
|
3728
|
+
history.push({ role: "assistant", content: "(response incomplete)" });
|
|
3729
|
+
sessionHistories.set(sessionKey, history.slice(-MAX_HISTORY * 2));
|
|
3730
|
+
if (!streamStarted && channel instanceof WebChatServer) {
|
|
3731
|
+
await channel.send(userId, { text: t("gateway.stream.incomplete", session.language) });
|
|
3732
|
+
}
|
|
3733
|
+
return;
|
|
3734
|
+
}
|
|
3735
|
+
const textLen = result.text?.length ?? 0;
|
|
3736
|
+
const toolCallCount = result.toolCalls?.length ?? 0;
|
|
3737
|
+
log16.info({ userId, textLen, toolCallCount }, "Response finalized");
|
|
3738
|
+
if (!streamStarted && channel instanceof WebChatServer) {
|
|
3739
|
+
if (result.text) {
|
|
3740
|
+
await channel.send(userId, { text: result.text });
|
|
3741
|
+
} else if (toolCallCount > 0) {
|
|
3742
|
+
const names = result.toolCalls.map((tc) => tc.tool).join(", ");
|
|
3743
|
+
await channel.send(userId, {
|
|
3744
|
+
text: t("gateway.stream.toolsNoText", session.language, { count: toolCallCount, names })
|
|
3745
|
+
});
|
|
3746
|
+
} else {
|
|
3747
|
+
await channel.send(userId, { text: t("gateway.stream.noReply", session.language) });
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
1737
3750
|
history.push({ role: "user", content: text });
|
|
1738
|
-
history.push({ role: "assistant", content: result.text });
|
|
1739
|
-
|
|
3751
|
+
history.push({ role: "assistant", content: result.text || "(tool calls completed)" });
|
|
3752
|
+
const postBudget = allocateBudget("", "", history, {});
|
|
3753
|
+
if (postBudget.shouldCompact && !postBudget.fallbackMode) {
|
|
3754
|
+
const compacted = await compactWithFlush(
|
|
3755
|
+
history,
|
|
3756
|
+
MAX_HISTORY * 2,
|
|
3757
|
+
this.memory,
|
|
3758
|
+
userId
|
|
3759
|
+
);
|
|
3760
|
+
sessionHistories.set(sessionKey, compacted.messages);
|
|
3761
|
+
} else {
|
|
3762
|
+
sessionHistories.set(sessionKey, history.slice(-MAX_HISTORY * 2));
|
|
3763
|
+
}
|
|
3764
|
+
appendHistory(
|
|
3765
|
+
sessionKey,
|
|
3766
|
+
{ role: "user", content: text },
|
|
3767
|
+
{ role: "assistant", content: result.text }
|
|
3768
|
+
);
|
|
1740
3769
|
writeAuditLog({
|
|
1741
3770
|
userId,
|
|
1742
3771
|
agentId: agent.id,
|
|
@@ -1754,7 +3783,23 @@ Just type naturally \u2014 I understand everyday language!`
|
|
|
1754
3783
|
const result = await agent.process(text, context);
|
|
1755
3784
|
history.push({ role: "user", content: text });
|
|
1756
3785
|
history.push({ role: "assistant", content: result.text });
|
|
1757
|
-
|
|
3786
|
+
const postBudget = allocateBudget("", "", history, {});
|
|
3787
|
+
if (postBudget.shouldCompact && !postBudget.fallbackMode) {
|
|
3788
|
+
const compacted = await compactWithFlush(
|
|
3789
|
+
history,
|
|
3790
|
+
MAX_HISTORY * 2,
|
|
3791
|
+
this.memory,
|
|
3792
|
+
userId
|
|
3793
|
+
);
|
|
3794
|
+
sessionHistories.set(sessionKey, compacted.messages);
|
|
3795
|
+
} else {
|
|
3796
|
+
sessionHistories.set(sessionKey, history.slice(-MAX_HISTORY * 2));
|
|
3797
|
+
}
|
|
3798
|
+
appendHistory(
|
|
3799
|
+
sessionKey,
|
|
3800
|
+
{ role: "user", content: text },
|
|
3801
|
+
{ role: "assistant", content: result.text }
|
|
3802
|
+
);
|
|
1758
3803
|
if (result.text) {
|
|
1759
3804
|
await channel.send(userId, { text: result.text });
|
|
1760
3805
|
}
|
|
@@ -1773,68 +3818,196 @@ Just type naturally \u2014 I understand everyday language!`
|
|
|
1773
3818
|
getOrCreateAgent(userId, session) {
|
|
1774
3819
|
let agent = this.agents.get(userId);
|
|
1775
3820
|
if (agent) return agent;
|
|
1776
|
-
const dsersConfig = createDSersConfig(
|
|
1777
|
-
|
|
1778
|
-
session.
|
|
1779
|
-
|
|
3821
|
+
const dsersConfig = createDSersConfig(session.dspiEmail ?? "unknown");
|
|
3822
|
+
if (session.dspiSessionId) {
|
|
3823
|
+
dsersConfig.sessionId = session.dspiSessionId;
|
|
3824
|
+
dsersConfig.sessionState = session.dspiSessionState ?? "";
|
|
3825
|
+
}
|
|
1780
3826
|
const dsersClient = new DSersClient(dsersConfig);
|
|
1781
3827
|
this.dsersClients.set(userId, dsersClient);
|
|
1782
3828
|
const llm = {
|
|
1783
3829
|
provider: session.llmProvider,
|
|
1784
3830
|
apiKey: session.llmApiKey,
|
|
1785
|
-
model: session.llmModel ?? getDefaultModel(session.llmProvider)
|
|
3831
|
+
model: session.llmModel ?? getDefaultModel(session.llmProvider),
|
|
3832
|
+
baseUrl: session.llmBaseUrl
|
|
1786
3833
|
};
|
|
1787
|
-
|
|
3834
|
+
const dsersSession = session.dspiSessionId ? { sessionId: session.dspiSessionId, state: session.dspiSessionState ?? "" } : void 0;
|
|
3835
|
+
const authCb = () => this.triggerAuth(userId);
|
|
3836
|
+
let jobStore = this.jobStores.get(userId);
|
|
3837
|
+
if (!jobStore) {
|
|
3838
|
+
jobStore = new MemoryJobStore2();
|
|
3839
|
+
this.jobStores.set(userId, jobStore);
|
|
3840
|
+
}
|
|
3841
|
+
agent = new DSClawCoreAgent(dsersClient, this.memory, llm, dsersSession, authCb, jobStore);
|
|
1788
3842
|
this.agents.set(userId, agent);
|
|
3843
|
+
if (!agent.mcpAvailable) {
|
|
3844
|
+
log16.warn({ userId }, "MCP unavailable for agent \u2014 notifying user");
|
|
3845
|
+
for (const ch of this.channels) {
|
|
3846
|
+
ch.send(userId, {
|
|
3847
|
+
text: t("gateway.mcp.unavailable", session.language)
|
|
3848
|
+
}).catch(() => {
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
1789
3852
|
return agent;
|
|
1790
3853
|
}
|
|
1791
3854
|
async stop() {
|
|
1792
|
-
|
|
3855
|
+
log16.info("Stopping DSClaw Gateway...");
|
|
1793
3856
|
for (const ch of this.channels) {
|
|
1794
3857
|
await ch.disconnect();
|
|
1795
3858
|
}
|
|
1796
3859
|
this.running = false;
|
|
1797
|
-
|
|
3860
|
+
log16.info("DSClaw Gateway stopped");
|
|
1798
3861
|
}
|
|
1799
3862
|
isRunning() {
|
|
1800
3863
|
return this.running;
|
|
1801
3864
|
}
|
|
3865
|
+
get actualPort() {
|
|
3866
|
+
return this.webChannel?.actualPort ?? this.config.port;
|
|
3867
|
+
}
|
|
1802
3868
|
};
|
|
1803
3869
|
|
|
1804
|
-
// src/cli/
|
|
1805
|
-
|
|
3870
|
+
// src/cli/pid.ts
|
|
3871
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync2, existsSync as existsSync10 } from "fs";
|
|
3872
|
+
import { join as join10 } from "path";
|
|
3873
|
+
var PID_PATH = join10(CONFIG_DIR, "dsclaw.pid");
|
|
3874
|
+
function writePid(port) {
|
|
3875
|
+
ensureConfigDir();
|
|
3876
|
+
const info = {
|
|
3877
|
+
pid: process.pid,
|
|
3878
|
+
port,
|
|
3879
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3880
|
+
};
|
|
3881
|
+
writeFileSync8(PID_PATH, JSON.stringify(info, null, 2), { mode: 384 });
|
|
3882
|
+
}
|
|
3883
|
+
function readPid() {
|
|
3884
|
+
if (!existsSync10(PID_PATH)) return null;
|
|
3885
|
+
try {
|
|
3886
|
+
const raw = readFileSync8(PID_PATH, "utf-8");
|
|
3887
|
+
return JSON.parse(raw);
|
|
3888
|
+
} catch {
|
|
3889
|
+
return null;
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
function removePid() {
|
|
3893
|
+
try {
|
|
3894
|
+
if (existsSync10(PID_PATH)) unlinkSync2(PID_PATH);
|
|
3895
|
+
} catch {
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
function isProcessAlive(pid) {
|
|
3899
|
+
try {
|
|
3900
|
+
process.kill(pid, 0);
|
|
3901
|
+
return true;
|
|
3902
|
+
} catch {
|
|
3903
|
+
return false;
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
function getRunningInstance() {
|
|
3907
|
+
const info = readPid();
|
|
3908
|
+
if (!info) return null;
|
|
3909
|
+
if (isProcessAlive(info.pid)) return info;
|
|
3910
|
+
removePid();
|
|
3911
|
+
return null;
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
// src/shared/open-browser.ts
|
|
3915
|
+
var BROWSERS_PRIORITY = [
|
|
3916
|
+
"Google Chrome",
|
|
3917
|
+
"Arc",
|
|
3918
|
+
"Firefox",
|
|
3919
|
+
"Brave Browser",
|
|
3920
|
+
"Microsoft Edge",
|
|
3921
|
+
"Opera",
|
|
3922
|
+
"Vivaldi",
|
|
3923
|
+
"Safari"
|
|
3924
|
+
];
|
|
1806
3925
|
async function openBrowser(url) {
|
|
1807
3926
|
const { exec } = await import("child_process");
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
3927
|
+
if (process.platform === "darwin") {
|
|
3928
|
+
exec(
|
|
3929
|
+
`osascript -e 'tell application "System Events" to get name of every application process whose visible is true'`,
|
|
3930
|
+
(err, stdout) => {
|
|
3931
|
+
if (err || !stdout) {
|
|
3932
|
+
exec(`open "${url}"`, () => {
|
|
3933
|
+
});
|
|
3934
|
+
return;
|
|
3935
|
+
}
|
|
3936
|
+
const apps = stdout.trim();
|
|
3937
|
+
const found = BROWSERS_PRIORITY.find((b) => apps.includes(b));
|
|
3938
|
+
if (found) {
|
|
3939
|
+
const script = `tell application "${found}"
|
|
3940
|
+
open location "${url}"
|
|
3941
|
+
activate
|
|
3942
|
+
end tell`;
|
|
3943
|
+
exec(`osascript -e '${script}'`, () => {
|
|
3944
|
+
});
|
|
3945
|
+
} else {
|
|
3946
|
+
exec(`open "${url}"`, () => {
|
|
3947
|
+
});
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
);
|
|
3951
|
+
} else if (process.platform === "win32") {
|
|
3952
|
+
exec(`start "" "${url}"`, () => {
|
|
3953
|
+
});
|
|
3954
|
+
} else {
|
|
3955
|
+
exec(`xdg-open "${url}"`, () => {
|
|
3956
|
+
});
|
|
3957
|
+
}
|
|
1811
3958
|
}
|
|
3959
|
+
|
|
3960
|
+
// src/cli/start.ts
|
|
3961
|
+
init_logger();
|
|
3962
|
+
var log17 = createLogger("cli:start");
|
|
1812
3963
|
async function startCommand(opts) {
|
|
1813
3964
|
try {
|
|
3965
|
+
const existing = getRunningInstance();
|
|
3966
|
+
if (existing) {
|
|
3967
|
+
const url2 = `http://localhost:${existing.port}`;
|
|
3968
|
+
console.log(`
|
|
3969
|
+
DSClaw is already running (PID ${existing.pid}, port ${existing.port})`);
|
|
3970
|
+
console.log(` Opening: ${url2}
|
|
3971
|
+
`);
|
|
3972
|
+
if (opts.open !== false) await openBrowser(url2);
|
|
3973
|
+
return;
|
|
3974
|
+
}
|
|
1814
3975
|
const config = loadConfig(opts.config);
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
3976
|
+
console.log("\n DSClaw starting...\n");
|
|
3977
|
+
const gateway = new DSClawGateway(config);
|
|
3978
|
+
await gateway.start();
|
|
3979
|
+
const port = gateway.actualPort;
|
|
3980
|
+
const url = `http://localhost:${port}`;
|
|
3981
|
+
writePid(port);
|
|
1818
3982
|
if (config.telegramBotToken) {
|
|
1819
3983
|
console.log(" Telegram: enabled");
|
|
1820
3984
|
}
|
|
1821
|
-
console.log(
|
|
1822
|
-
|
|
1823
|
-
|
|
3985
|
+
console.log(`
|
|
3986
|
+
DSClaw is running!
|
|
3987
|
+
`);
|
|
3988
|
+
console.log(` \u279C ${url}
|
|
3989
|
+
`);
|
|
3990
|
+
console.log(` Stop with: dsclaw stop
|
|
3991
|
+
`);
|
|
1824
3992
|
if (opts.open !== false) {
|
|
1825
3993
|
await openBrowser(url);
|
|
1826
3994
|
}
|
|
1827
|
-
|
|
1828
|
-
`);
|
|
3995
|
+
let shuttingDown = false;
|
|
1829
3996
|
const shutdown = async () => {
|
|
1830
|
-
|
|
3997
|
+
if (shuttingDown) return;
|
|
3998
|
+
shuttingDown = true;
|
|
3999
|
+
console.log("\n Shutting down...");
|
|
4000
|
+
log17.info("Shutting down gracefully...");
|
|
4001
|
+
removePid();
|
|
1831
4002
|
await gateway.stop();
|
|
1832
4003
|
process.exit(0);
|
|
1833
4004
|
};
|
|
1834
4005
|
process.on("SIGINT", shutdown);
|
|
1835
4006
|
process.on("SIGTERM", shutdown);
|
|
4007
|
+
process.on("exit", () => removePid());
|
|
1836
4008
|
} catch (error) {
|
|
1837
|
-
|
|
4009
|
+
removePid();
|
|
4010
|
+
log17.fatal({ error }, "Failed to start");
|
|
1838
4011
|
console.error(
|
|
1839
4012
|
`
|
|
1840
4013
|
Failed to start: ${error instanceof Error ? error.message : error}`
|
|
@@ -1843,6 +4016,108 @@ async function startCommand(opts) {
|
|
|
1843
4016
|
}
|
|
1844
4017
|
}
|
|
1845
4018
|
|
|
4019
|
+
// src/cli/stop.ts
|
|
4020
|
+
function sleep2(ms) {
|
|
4021
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
4022
|
+
}
|
|
4023
|
+
async function stopCommand() {
|
|
4024
|
+
const info = getRunningInstance();
|
|
4025
|
+
if (!info) {
|
|
4026
|
+
console.log("\n DSClaw is not running.\n");
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
console.log(`
|
|
4030
|
+
Stopping DSClaw (PID ${info.pid}, port ${info.port})...`);
|
|
4031
|
+
try {
|
|
4032
|
+
process.kill(info.pid, "SIGTERM");
|
|
4033
|
+
} catch {
|
|
4034
|
+
console.log(" Process already gone \u2014 cleaning up PID file.");
|
|
4035
|
+
removePid();
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4038
|
+
for (let i = 0; i < 10; i++) {
|
|
4039
|
+
await sleep2(500);
|
|
4040
|
+
if (!isProcessAlive(info.pid)) {
|
|
4041
|
+
removePid();
|
|
4042
|
+
console.log(" Stopped.\n");
|
|
4043
|
+
return;
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
try {
|
|
4047
|
+
process.kill(info.pid, "SIGKILL");
|
|
4048
|
+
} catch {
|
|
4049
|
+
}
|
|
4050
|
+
removePid();
|
|
4051
|
+
console.log(" Force-killed.\n");
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
// src/cli/status.ts
|
|
4055
|
+
function statusCommand() {
|
|
4056
|
+
const info = getRunningInstance();
|
|
4057
|
+
if (!info) {
|
|
4058
|
+
console.log("\n DSClaw is not running.");
|
|
4059
|
+
console.log(" Start with: dsclaw start\n");
|
|
4060
|
+
return;
|
|
4061
|
+
}
|
|
4062
|
+
const uptime = Date.now() - new Date(info.startedAt).getTime();
|
|
4063
|
+
const mins = Math.floor(uptime / 6e4);
|
|
4064
|
+
const hrs = Math.floor(mins / 60);
|
|
4065
|
+
const uptimeStr = hrs > 0 ? `${hrs}h ${mins % 60}m` : mins > 0 ? `${mins}m` : "<1m";
|
|
4066
|
+
console.log("\n DSClaw is running");
|
|
4067
|
+
console.log(` PID: ${info.pid}`);
|
|
4068
|
+
console.log(` Port: ${info.port}`);
|
|
4069
|
+
console.log(` Uptime: ${uptimeStr}`);
|
|
4070
|
+
console.log(` Chat: http://localhost:${info.port}`);
|
|
4071
|
+
console.log(`
|
|
4072
|
+
Stop with: dsclaw stop
|
|
4073
|
+
`);
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
// src/cli/reset.ts
|
|
4077
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
4078
|
+
import { join as join11 } from "path";
|
|
4079
|
+
function resetCommand(opts) {
|
|
4080
|
+
const running = getRunningInstance();
|
|
4081
|
+
if (running) {
|
|
4082
|
+
console.log(`
|
|
4083
|
+
DSClaw is still running (PID ${running.pid}).`);
|
|
4084
|
+
console.log(" Please run `dsclaw stop` first.\n");
|
|
4085
|
+
return;
|
|
4086
|
+
}
|
|
4087
|
+
const sessionsDir = join11(CONFIG_DIR, "sessions");
|
|
4088
|
+
let cleared = 0;
|
|
4089
|
+
if (existsSync11(sessionsDir)) {
|
|
4090
|
+
const files = readdirSync3(sessionsDir);
|
|
4091
|
+
for (const f of files) {
|
|
4092
|
+
try {
|
|
4093
|
+
unlinkSync3(join11(sessionsDir, f));
|
|
4094
|
+
cleared++;
|
|
4095
|
+
} catch {
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
console.log(`
|
|
4100
|
+
Cleared ${cleared} session(s).`);
|
|
4101
|
+
if (opts.hard) {
|
|
4102
|
+
if (existsSync11(CONFIG_PATH)) {
|
|
4103
|
+
try {
|
|
4104
|
+
unlinkSync3(CONFIG_PATH);
|
|
4105
|
+
console.log(" Deleted config file.");
|
|
4106
|
+
} catch {
|
|
4107
|
+
console.log(" Failed to delete config file.");
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
const pidPath = join11(CONFIG_DIR, "dsclaw.pid");
|
|
4111
|
+
if (existsSync11(pidPath)) {
|
|
4112
|
+
try {
|
|
4113
|
+
unlinkSync3(pidPath);
|
|
4114
|
+
} catch {
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
console.log(" Next run will start fresh onboarding.\n");
|
|
4119
|
+
}
|
|
4120
|
+
|
|
1846
4121
|
// src/cli/doctor.ts
|
|
1847
4122
|
async function doctorCommand() {
|
|
1848
4123
|
console.log("\n DSClaw Doctor\n");
|
|
@@ -1909,26 +4184,13 @@ async function doctorCommand() {
|
|
|
1909
4184
|
|
|
1910
4185
|
// src/cli/index.ts
|
|
1911
4186
|
var program = new Command();
|
|
1912
|
-
program.name("dsclaw").description("AI-powered dropshipping agent \u2014 chat with your DSers store.").version("0.1.
|
|
4187
|
+
program.name("dsclaw").description("AI-powered dropshipping agent \u2014 chat with your DSers store.").version("0.1.4");
|
|
1913
4188
|
program.action(() => startCommand({}));
|
|
1914
4189
|
program.command("init").description("Setup wizard \u2014 configure Telegram (optional)").action(initCommand);
|
|
1915
4190
|
program.command("start").description("Start the DSClaw bot").option("-c, --config <path>", "Path to config file").option("--no-open", "Don't auto-open browser").action(startCommand);
|
|
1916
|
-
program.command("stop").description("Stop the DSClaw
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
execSync("pm2 stop dsclaw", { stdio: "inherit" });
|
|
1920
|
-
} catch {
|
|
1921
|
-
console.log(" Bot is not running or PM2 is not installed.");
|
|
1922
|
-
}
|
|
1923
|
-
});
|
|
1924
|
-
program.command("status").description("Show bot status").action(async () => {
|
|
1925
|
-
const { execSync } = await import("child_process");
|
|
1926
|
-
try {
|
|
1927
|
-
execSync("pm2 describe dsclaw", { stdio: "inherit" });
|
|
1928
|
-
} catch {
|
|
1929
|
-
console.log(" Bot is not running. Start with: dsclaw start");
|
|
1930
|
-
}
|
|
1931
|
-
});
|
|
4191
|
+
program.command("stop").description("Stop the running DSClaw instance").action(stopCommand);
|
|
4192
|
+
program.command("status").description("Show whether DSClaw is running").action(statusCommand);
|
|
4193
|
+
program.command("reset").description("Clear all session data (re-do onboarding)").option("--hard", "Also delete config file").action(resetCommand);
|
|
1932
4194
|
program.command("doctor").description("Verify configuration").action(doctorCommand);
|
|
1933
4195
|
program.parse();
|
|
1934
4196
|
//# sourceMappingURL=index.js.map
|