dsclaw 0.1.0
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 +1901 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +707 -0
- package/dist/index.js +40149 -0
- package/dist/index.js.map +1 -0
- package/dist/web/chat.html +406 -0
- package/openclaw.plugin.json +19 -0
- package/package.json +73 -0
|
@@ -0,0 +1,1901 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
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";
|
|
17
|
+
|
|
18
|
+
// src/shared/tracer.ts
|
|
19
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
20
|
+
import { nanoid } from "nanoid";
|
|
21
|
+
var storage = new AsyncLocalStorage();
|
|
22
|
+
function runWithTrace(ctx, fn) {
|
|
23
|
+
const traceId = ctx.traceId ?? nanoid(12);
|
|
24
|
+
return storage.run({ traceId, ...ctx }, fn);
|
|
25
|
+
}
|
|
26
|
+
function getTraceContext() {
|
|
27
|
+
return storage.getStore() ?? { traceId: nanoid(12) };
|
|
28
|
+
}
|
|
29
|
+
function getTraceId() {
|
|
30
|
+
return getTraceContext().traceId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/shared/logger.ts
|
|
34
|
+
var rootLogger = pino({
|
|
35
|
+
level: process.env["LOG_LEVEL"] ?? "info",
|
|
36
|
+
transport: process.env["NODE_ENV"] !== "production" ? { target: "pino-pretty", options: { colorize: true } } : void 0,
|
|
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
|
+
};
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
function createLogger(module) {
|
|
48
|
+
return rootLogger.child({ module });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/gateway/config.ts
|
|
52
|
+
var log = createLogger("config");
|
|
53
|
+
var CONFIG_DIR = join(homedir(), ".dropclaw");
|
|
54
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
55
|
+
var ConfigSchema = z.object({
|
|
56
|
+
port: z.number().default(3e3),
|
|
57
|
+
telegramBotToken: z.string().optional()
|
|
58
|
+
});
|
|
59
|
+
var DEFAULT_CONFIG = { port: 3e3 };
|
|
60
|
+
function ensureConfigDir() {
|
|
61
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
62
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function loadConfig(path) {
|
|
66
|
+
const configPath = path ?? CONFIG_PATH;
|
|
67
|
+
if (!existsSync(configPath)) {
|
|
68
|
+
log.info("No config file found \u2014 using defaults (web chat on port 3000)");
|
|
69
|
+
return DEFAULT_CONFIG;
|
|
70
|
+
}
|
|
71
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
72
|
+
const parsed = JSON.parse(raw);
|
|
73
|
+
const result = ConfigSchema.safeParse(parsed);
|
|
74
|
+
if (!result.success) {
|
|
75
|
+
log.warn("Config validation failed \u2014 using defaults");
|
|
76
|
+
return DEFAULT_CONFIG;
|
|
77
|
+
}
|
|
78
|
+
log.info("Configuration loaded");
|
|
79
|
+
return result.data;
|
|
80
|
+
}
|
|
81
|
+
function saveConfig(config) {
|
|
82
|
+
ensureConfigDir();
|
|
83
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
|
|
84
|
+
log.info({ path: CONFIG_PATH }, "Configuration saved");
|
|
85
|
+
}
|
|
86
|
+
function configExists() {
|
|
87
|
+
return existsSync(CONFIG_PATH);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/cli/init.ts
|
|
91
|
+
async function initCommand() {
|
|
92
|
+
console.log("\n DropClaw Setup (optional)\n");
|
|
93
|
+
console.log(" Web chat works out of the box \u2014 this is for extra settings.\n");
|
|
94
|
+
if (configExists()) {
|
|
95
|
+
const { overwrite } = await inquirer.prompt([
|
|
96
|
+
{
|
|
97
|
+
type: "confirm",
|
|
98
|
+
name: "overwrite",
|
|
99
|
+
message: `Config already exists at ${CONFIG_PATH}. Overwrite?`,
|
|
100
|
+
default: false
|
|
101
|
+
}
|
|
102
|
+
]);
|
|
103
|
+
if (!overwrite) {
|
|
104
|
+
console.log(" Cancelled.");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const { port } = await inquirer.prompt({
|
|
109
|
+
type: "number",
|
|
110
|
+
name: "port",
|
|
111
|
+
message: "Web chat port (default 3000):",
|
|
112
|
+
default: 3e3
|
|
113
|
+
});
|
|
114
|
+
const { addTelegram } = await inquirer.prompt({
|
|
115
|
+
type: "confirm",
|
|
116
|
+
name: "addTelegram",
|
|
117
|
+
message: "Add Telegram bot?",
|
|
118
|
+
default: false
|
|
119
|
+
});
|
|
120
|
+
const config = { port };
|
|
121
|
+
if (addTelegram) {
|
|
122
|
+
const { botToken } = await inquirer.prompt({
|
|
123
|
+
type: "password",
|
|
124
|
+
name: "botToken",
|
|
125
|
+
message: "Telegram Bot Token (from @BotFather):",
|
|
126
|
+
mask: "*",
|
|
127
|
+
validate: (v) => v.includes(":") ? true : "Token format: 123456:ABC-DEF..."
|
|
128
|
+
});
|
|
129
|
+
config.telegramBotToken = botToken;
|
|
130
|
+
}
|
|
131
|
+
saveConfig(config);
|
|
132
|
+
console.log(`
|
|
133
|
+
Config saved to ${CONFIG_PATH}`);
|
|
134
|
+
console.log(" Run 'dropclaw start' to launch.\n");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/gateway/user-session.ts
|
|
138
|
+
import {
|
|
139
|
+
randomBytes,
|
|
140
|
+
createCipheriv,
|
|
141
|
+
createDecipheriv,
|
|
142
|
+
createHash
|
|
143
|
+
} from "crypto";
|
|
144
|
+
import {
|
|
145
|
+
readFileSync as readFileSync2,
|
|
146
|
+
writeFileSync as writeFileSync2,
|
|
147
|
+
mkdirSync as mkdirSync2,
|
|
148
|
+
existsSync as existsSync2,
|
|
149
|
+
unlinkSync
|
|
150
|
+
} from "fs";
|
|
151
|
+
import { join as join2 } from "path";
|
|
152
|
+
import { hostname, userInfo } from "os";
|
|
153
|
+
var log2 = createLogger("user-session");
|
|
154
|
+
var ALG = "aes-256-gcm";
|
|
155
|
+
var IV_LEN = 12;
|
|
156
|
+
var TAG_LEN = 16;
|
|
157
|
+
var KEY_SEED = "dropclaw-v1";
|
|
158
|
+
var SESSIONS_DIR = join2(CONFIG_DIR, "sessions");
|
|
159
|
+
function deriveKey() {
|
|
160
|
+
let user = "";
|
|
161
|
+
try {
|
|
162
|
+
user = userInfo().username;
|
|
163
|
+
} catch {
|
|
164
|
+
user = process.env["USER"] ?? process.env["USERNAME"] ?? "default";
|
|
165
|
+
}
|
|
166
|
+
return createHash("sha256").update(`${KEY_SEED}:${hostname()}:${user}`).digest();
|
|
167
|
+
}
|
|
168
|
+
function encrypt(data) {
|
|
169
|
+
const key = deriveKey();
|
|
170
|
+
const iv = randomBytes(IV_LEN);
|
|
171
|
+
const cipher = createCipheriv(ALG, key, iv, { authTagLength: TAG_LEN });
|
|
172
|
+
const ct = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
173
|
+
const tag = cipher.getAuthTag();
|
|
174
|
+
return Buffer.concat([iv, tag, ct]).toString("base64");
|
|
175
|
+
}
|
|
176
|
+
function decrypt(encoded) {
|
|
177
|
+
try {
|
|
178
|
+
const key = deriveKey();
|
|
179
|
+
const buf = Buffer.from(encoded, "base64");
|
|
180
|
+
if (buf.length < IV_LEN + TAG_LEN + 1) return null;
|
|
181
|
+
const iv = buf.subarray(0, IV_LEN);
|
|
182
|
+
const tag = buf.subarray(IV_LEN, IV_LEN + TAG_LEN);
|
|
183
|
+
const ct = buf.subarray(IV_LEN + TAG_LEN);
|
|
184
|
+
const decipher = createDecipheriv(ALG, key, iv, { authTagLength: TAG_LEN });
|
|
185
|
+
decipher.setAuthTag(tag);
|
|
186
|
+
return Buffer.concat([decipher.update(ct), decipher.final()]).toString(
|
|
187
|
+
"utf8"
|
|
188
|
+
);
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function ensureSessionsDir() {
|
|
194
|
+
if (!existsSync2(SESSIONS_DIR)) {
|
|
195
|
+
mkdirSync2(SESSIONS_DIR, { recursive: true, mode: 448 });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function sessionPath(userId) {
|
|
199
|
+
const safe = userId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
200
|
+
return join2(SESSIONS_DIR, `${safe}.enc`);
|
|
201
|
+
}
|
|
202
|
+
function loadSession(userId) {
|
|
203
|
+
const p = sessionPath(userId);
|
|
204
|
+
if (!existsSync2(p)) {
|
|
205
|
+
return { state: "new" };
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const encrypted = readFileSync2(p, "utf-8").trim();
|
|
209
|
+
const json = decrypt(encrypted);
|
|
210
|
+
if (!json) {
|
|
211
|
+
log2.warn({ userId }, "Failed to decrypt session \u2014 starting fresh");
|
|
212
|
+
return { state: "new" };
|
|
213
|
+
}
|
|
214
|
+
return JSON.parse(json);
|
|
215
|
+
} catch {
|
|
216
|
+
return { state: "new" };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function saveSession(userId, session) {
|
|
220
|
+
ensureSessionsDir();
|
|
221
|
+
const json = JSON.stringify(session);
|
|
222
|
+
const encrypted = encrypt(json);
|
|
223
|
+
writeFileSync2(sessionPath(userId), encrypted, { mode: 384 });
|
|
224
|
+
log2.debug({ userId, state: session.state }, "Session saved");
|
|
225
|
+
}
|
|
226
|
+
function isOnboarding(state) {
|
|
227
|
+
return state !== "ready";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/dsers/auth.ts
|
|
231
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, chmodSync } from "fs";
|
|
232
|
+
import { dirname } from "path";
|
|
233
|
+
|
|
234
|
+
// src/shared/errors.ts
|
|
235
|
+
var ErrorCategory = {
|
|
236
|
+
RETRYABLE: "RETRYABLE",
|
|
237
|
+
NEEDS_HUMAN: "NEEDS_HUMAN",
|
|
238
|
+
NEEDS_DATA: "NEEDS_DATA",
|
|
239
|
+
FATAL: "FATAL",
|
|
240
|
+
DEGRADED: "DEGRADED"
|
|
241
|
+
};
|
|
242
|
+
var DropClawError = class extends Error {
|
|
243
|
+
category;
|
|
244
|
+
code;
|
|
245
|
+
retryable;
|
|
246
|
+
details;
|
|
247
|
+
constructor(opts) {
|
|
248
|
+
super(opts.message);
|
|
249
|
+
this.name = "DropClawError";
|
|
250
|
+
this.category = opts.category;
|
|
251
|
+
this.code = opts.code;
|
|
252
|
+
this.retryable = opts.retryable;
|
|
253
|
+
this.details = opts.details;
|
|
254
|
+
if (opts.cause) this.cause = opts.cause;
|
|
255
|
+
}
|
|
256
|
+
toJSON() {
|
|
257
|
+
return {
|
|
258
|
+
category: this.category,
|
|
259
|
+
code: this.code,
|
|
260
|
+
message: this.message,
|
|
261
|
+
retryable: this.retryable,
|
|
262
|
+
details: this.details
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
function classifyHttpError(status, body) {
|
|
267
|
+
if (status === 429) return ErrorCategory.RETRYABLE;
|
|
268
|
+
if (status >= 500) return ErrorCategory.RETRYABLE;
|
|
269
|
+
if (status === 401 || status === 403) return ErrorCategory.NEEDS_HUMAN;
|
|
270
|
+
if (status === 404 || status === 422) return ErrorCategory.NEEDS_DATA;
|
|
271
|
+
if (status === 400) return ErrorCategory.NEEDS_DATA;
|
|
272
|
+
return ErrorCategory.FATAL;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/dsers/auth.ts
|
|
276
|
+
var log3 = createLogger("dsers:auth");
|
|
277
|
+
var SESSION_TTL = 3600 * 6 * 1e3;
|
|
278
|
+
var DSersAuth = class {
|
|
279
|
+
config;
|
|
280
|
+
sessionId = null;
|
|
281
|
+
state = null;
|
|
282
|
+
fetchedAt = 0;
|
|
283
|
+
constructor(config) {
|
|
284
|
+
this.config = config;
|
|
285
|
+
if (config.sessionId) {
|
|
286
|
+
this.sessionId = config.sessionId;
|
|
287
|
+
this.state = config.sessionState ?? "";
|
|
288
|
+
this.fetchedAt = Date.now();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async getSession() {
|
|
292
|
+
if (this.sessionId && Date.now() - this.fetchedAt < SESSION_TTL) {
|
|
293
|
+
return [this.sessionId, this.state ?? ""];
|
|
294
|
+
}
|
|
295
|
+
const cached = this.readCache();
|
|
296
|
+
if (cached) {
|
|
297
|
+
[this.sessionId, this.state, this.fetchedAt] = cached;
|
|
298
|
+
return [this.sessionId, this.state];
|
|
299
|
+
}
|
|
300
|
+
return this.login();
|
|
301
|
+
}
|
|
302
|
+
async login() {
|
|
303
|
+
if (!this.config.email || !this.config.password) {
|
|
304
|
+
throw new DropClawError({
|
|
305
|
+
category: ErrorCategory.NEEDS_HUMAN,
|
|
306
|
+
code: "DSERS_NO_CREDENTIALS",
|
|
307
|
+
message: "DSers email and password not configured.",
|
|
308
|
+
retryable: false
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
log3.info("Authenticating with DSers...");
|
|
312
|
+
const resp = await fetch(
|
|
313
|
+
`${this.config.baseUrl}/account-user-bff/v1/users/login`,
|
|
314
|
+
{
|
|
315
|
+
method: "POST",
|
|
316
|
+
headers: { "Content-Type": "application/json" },
|
|
317
|
+
body: JSON.stringify({
|
|
318
|
+
email: this.config.email,
|
|
319
|
+
password: this.config.password
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
if (!resp.ok) {
|
|
324
|
+
const body = await resp.text();
|
|
325
|
+
throw new DropClawError({
|
|
326
|
+
category: resp.status === 401 || resp.status === 403 ? ErrorCategory.NEEDS_HUMAN : ErrorCategory.RETRYABLE,
|
|
327
|
+
code: `DSERS_LOGIN_${resp.status}`,
|
|
328
|
+
message: `DSers login failed (HTTP ${resp.status}): ${body.slice(0, 200)}`,
|
|
329
|
+
retryable: resp.status >= 500
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const data = await resp.json();
|
|
333
|
+
const inner = data["data"];
|
|
334
|
+
if (!inner?.["sessionId"]) {
|
|
335
|
+
throw new DropClawError({
|
|
336
|
+
category: ErrorCategory.NEEDS_HUMAN,
|
|
337
|
+
code: "DSERS_LOGIN_NO_SESSION",
|
|
338
|
+
message: "DSers login succeeded but no session returned.",
|
|
339
|
+
retryable: false,
|
|
340
|
+
details: { response: data }
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
this.sessionId = inner["sessionId"];
|
|
344
|
+
this.state = inner["state"] ?? "";
|
|
345
|
+
this.fetchedAt = Date.now();
|
|
346
|
+
this.writeCache();
|
|
347
|
+
log3.info("DSers authentication successful");
|
|
348
|
+
return [this.sessionId, this.state];
|
|
349
|
+
}
|
|
350
|
+
invalidate() {
|
|
351
|
+
this.sessionId = null;
|
|
352
|
+
this.state = null;
|
|
353
|
+
this.fetchedAt = 0;
|
|
354
|
+
log3.debug("Session invalidated");
|
|
355
|
+
}
|
|
356
|
+
readCache() {
|
|
357
|
+
try {
|
|
358
|
+
const p = this.config.sessionFile;
|
|
359
|
+
if (!existsSync3(p)) return null;
|
|
360
|
+
const obj = JSON.parse(readFileSync3(p, "utf-8"));
|
|
361
|
+
if (Date.now() - obj.ts > SESSION_TTL) return null;
|
|
362
|
+
return [obj.session_id, obj.state ?? "", obj.ts];
|
|
363
|
+
} catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
writeCache() {
|
|
368
|
+
if (this.config.sessionId) return;
|
|
369
|
+
try {
|
|
370
|
+
const p = this.config.sessionFile;
|
|
371
|
+
mkdirSync3(dirname(p), { recursive: true });
|
|
372
|
+
const payload = {
|
|
373
|
+
session_id: this.sessionId,
|
|
374
|
+
state: this.state ?? "",
|
|
375
|
+
ts: this.fetchedAt
|
|
376
|
+
};
|
|
377
|
+
writeFileSync3(p, JSON.stringify(payload), { encoding: "utf-8", mode: 384 });
|
|
378
|
+
try {
|
|
379
|
+
chmodSync(p, 384);
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
log3.warn("Failed to write session cache");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// src/shared/utils.ts
|
|
389
|
+
import { createHash as createHash2 } from "crypto";
|
|
390
|
+
function sleep(ms) {
|
|
391
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
392
|
+
}
|
|
393
|
+
function parseRetryAfter(value) {
|
|
394
|
+
if (!value) return null;
|
|
395
|
+
const seconds = Number(value);
|
|
396
|
+
if (!Number.isNaN(seconds)) return seconds * 1e3;
|
|
397
|
+
const date = Date.parse(value);
|
|
398
|
+
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/resilience/rate-limiter.ts
|
|
403
|
+
import pLimit from "p-limit";
|
|
404
|
+
var log4 = createLogger("rate-limiter");
|
|
405
|
+
var DEFAULT_OUTBOUND_LIMITS = {
|
|
406
|
+
dsers: 20,
|
|
407
|
+
llm: 5,
|
|
408
|
+
mem0: 10
|
|
409
|
+
};
|
|
410
|
+
var outboundLimiters = /* @__PURE__ */ new Map();
|
|
411
|
+
function getOutboundLimiter(service, customLimits) {
|
|
412
|
+
if (!outboundLimiters.has(service)) {
|
|
413
|
+
const limits = { ...DEFAULT_OUTBOUND_LIMITS, ...customLimits };
|
|
414
|
+
const limiter = pLimit(limits[service]);
|
|
415
|
+
outboundLimiters.set(service, limiter);
|
|
416
|
+
log4.info({ service, concurrency: limits[service] }, "Outbound limiter created");
|
|
417
|
+
}
|
|
418
|
+
return outboundLimiters.get(service);
|
|
419
|
+
}
|
|
420
|
+
var userQueues = /* @__PURE__ */ new Map();
|
|
421
|
+
var debounceTimers = /* @__PURE__ */ new Map();
|
|
422
|
+
async function acquireUserLock(userId) {
|
|
423
|
+
while (userQueues.has(userId)) {
|
|
424
|
+
await userQueues.get(userId);
|
|
425
|
+
}
|
|
426
|
+
let release;
|
|
427
|
+
const lock = new Promise((resolve) => {
|
|
428
|
+
release = () => {
|
|
429
|
+
userQueues.delete(userId);
|
|
430
|
+
resolve();
|
|
431
|
+
};
|
|
432
|
+
});
|
|
433
|
+
userQueues.set(userId, lock);
|
|
434
|
+
return release;
|
|
435
|
+
}
|
|
436
|
+
function debounceUser(userId, delayMs = 500) {
|
|
437
|
+
return new Promise((resolve) => {
|
|
438
|
+
const existing = debounceTimers.get(userId);
|
|
439
|
+
if (existing) clearTimeout(existing);
|
|
440
|
+
debounceTimers.set(
|
|
441
|
+
userId,
|
|
442
|
+
setTimeout(() => {
|
|
443
|
+
debounceTimers.delete(userId);
|
|
444
|
+
resolve();
|
|
445
|
+
}, delayMs)
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
var inboundWindows = /* @__PURE__ */ new Map();
|
|
450
|
+
function checkInboundLimit(sourceId, maxRequests = 30, windowMs = 6e4) {
|
|
451
|
+
const now = Date.now();
|
|
452
|
+
const entry = inboundWindows.get(sourceId) ?? { timestamps: [] };
|
|
453
|
+
entry.timestamps = entry.timestamps.filter((t) => now - t < windowMs);
|
|
454
|
+
if (entry.timestamps.length >= maxRequests) {
|
|
455
|
+
log4.warn({ sourceId, count: entry.timestamps.length }, "Inbound rate limit hit");
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
entry.timestamps.push(now);
|
|
459
|
+
inboundWindows.set(sourceId, entry);
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/dsers/client.ts
|
|
464
|
+
var log5 = createLogger("dsers:client");
|
|
465
|
+
var RETRYABLE_REASONS = /* @__PURE__ */ new Set([
|
|
466
|
+
"TOKEN_NOT_FOUND",
|
|
467
|
+
"TOKEN_EXPIRED",
|
|
468
|
+
"UNAUTHORIZED",
|
|
469
|
+
"INVALID_TOKEN"
|
|
470
|
+
]);
|
|
471
|
+
var MAX_RETRIES = 2;
|
|
472
|
+
var DSersClient = class {
|
|
473
|
+
config;
|
|
474
|
+
auth;
|
|
475
|
+
limiter;
|
|
476
|
+
constructor(config) {
|
|
477
|
+
this.config = config;
|
|
478
|
+
this.auth = new DSersAuth(config);
|
|
479
|
+
this.limiter = getOutboundLimiter("dsers");
|
|
480
|
+
}
|
|
481
|
+
async request(method, path, opts, attempt = 0) {
|
|
482
|
+
return this.limiter(async () => {
|
|
483
|
+
const [sessionId, state] = await this.auth.getSession();
|
|
484
|
+
const traceId = getTraceId();
|
|
485
|
+
const url = new URL(`${this.config.baseUrl}${path}`);
|
|
486
|
+
if (opts?.params) {
|
|
487
|
+
for (const [k, v] of Object.entries(opts.params)) {
|
|
488
|
+
if (v != null) url.searchParams.set(k, String(v));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const headers = {
|
|
492
|
+
"Content-Type": "application/json",
|
|
493
|
+
Authorization: `Bearer ${sessionId}`,
|
|
494
|
+
Cookie: `session_id=${sessionId}; state=${state}`,
|
|
495
|
+
"X-Trace-Id": traceId
|
|
496
|
+
};
|
|
497
|
+
log5.debug({ method, path, traceId, attempt }, "DSers API request");
|
|
498
|
+
const resp = await fetch(url.toString(), {
|
|
499
|
+
method,
|
|
500
|
+
headers,
|
|
501
|
+
body: opts?.json ? JSON.stringify(opts.json) : void 0
|
|
502
|
+
});
|
|
503
|
+
const bodyText = await resp.text();
|
|
504
|
+
if (resp.status === 400 && attempt === 0) {
|
|
505
|
+
try {
|
|
506
|
+
const body = JSON.parse(bodyText);
|
|
507
|
+
if (RETRYABLE_REASONS.has(body["reason"])) {
|
|
508
|
+
log5.info({ reason: body["reason"] }, "Session expired, re-authenticating");
|
|
509
|
+
this.auth.invalidate();
|
|
510
|
+
return this.request(method, path, opts, 1);
|
|
511
|
+
}
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (resp.status === 429) {
|
|
516
|
+
const retryAfterMs = parseRetryAfter(resp.headers.get("Retry-After")) ?? 5e3;
|
|
517
|
+
if (attempt < MAX_RETRIES) {
|
|
518
|
+
log5.warn({ retryAfterMs, attempt }, "Rate limited by DSers");
|
|
519
|
+
await sleep(retryAfterMs);
|
|
520
|
+
return this.request(method, path, opts, attempt + 1);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (resp.status >= 500 && attempt < MAX_RETRIES) {
|
|
524
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt) + Math.random() * 500, 15e3);
|
|
525
|
+
log5.warn({ status: resp.status, attempt }, "DSers server error, retrying");
|
|
526
|
+
await sleep(delay);
|
|
527
|
+
return this.request(method, path, opts, attempt + 1);
|
|
528
|
+
}
|
|
529
|
+
if (resp.status >= 400) {
|
|
530
|
+
const category = classifyHttpError(resp.status, bodyText);
|
|
531
|
+
throw new DropClawError({
|
|
532
|
+
category,
|
|
533
|
+
code: `DSERS_API_${resp.status}`,
|
|
534
|
+
message: `DSers API ${method} ${path} returned ${resp.status}: ${bodyText.slice(0, 300)}`,
|
|
535
|
+
retryable: category === ErrorCategory.RETRYABLE,
|
|
536
|
+
details: { status: resp.status, path, method }
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return JSON.parse(bodyText);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
async get(path, params) {
|
|
543
|
+
return this.request("GET", path, { params });
|
|
544
|
+
}
|
|
545
|
+
async post(path, json, params) {
|
|
546
|
+
return this.request("POST", path, { json, params });
|
|
547
|
+
}
|
|
548
|
+
async put(path, json, params) {
|
|
549
|
+
return this.request("PUT", path, { json, params });
|
|
550
|
+
}
|
|
551
|
+
async del(path, params) {
|
|
552
|
+
return this.request("DELETE", path, { params });
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// src/dsers/config.ts
|
|
557
|
+
import { join as join3 } from "path";
|
|
558
|
+
var PROD_URL = "https://bff-api-gw.dsers.com";
|
|
559
|
+
function createDSersConfig(email, password, baseUrl) {
|
|
560
|
+
const safeEmail = email.replace(/[^a-zA-Z0-9_@.-]/g, "_");
|
|
561
|
+
return {
|
|
562
|
+
baseUrl: baseUrl ?? PROD_URL,
|
|
563
|
+
email,
|
|
564
|
+
password,
|
|
565
|
+
sessionFile: join3(CONFIG_DIR, `session-${safeEmail}.json`)
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/agents/core-agent.ts
|
|
570
|
+
import { generateText, streamText, tool as aiTool, stepCountIs } from "ai";
|
|
571
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
572
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
573
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
574
|
+
import { z as z2 } from "zod";
|
|
575
|
+
|
|
576
|
+
// src/shared/audit.ts
|
|
577
|
+
import { appendFileSync, mkdirSync as mkdirSync4, existsSync as existsSync4 } from "fs";
|
|
578
|
+
import { join as join4 } from "path";
|
|
579
|
+
var AUDIT_DIR = join4(CONFIG_DIR, "audit");
|
|
580
|
+
function ensureAuditDir() {
|
|
581
|
+
if (!existsSync4(AUDIT_DIR)) {
|
|
582
|
+
mkdirSync4(AUDIT_DIR, { recursive: true });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function writeAuditLog(entry) {
|
|
586
|
+
ensureAuditDir();
|
|
587
|
+
const full = {
|
|
588
|
+
...entry,
|
|
589
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
590
|
+
traceId: getTraceId()
|
|
591
|
+
};
|
|
592
|
+
const date = full.timestamp.slice(0, 10);
|
|
593
|
+
const file = join4(AUDIT_DIR, `${date}.jsonl`);
|
|
594
|
+
try {
|
|
595
|
+
appendFileSync(file, JSON.stringify(full) + "\n");
|
|
596
|
+
} catch {
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/dsers/product.ts
|
|
601
|
+
async function getImportList(client, params) {
|
|
602
|
+
const filtered = params ? Object.fromEntries(Object.entries(params).filter(([, v]) => v != null)) : {};
|
|
603
|
+
return client.get("/dsers-product-bff/import-list", filtered);
|
|
604
|
+
}
|
|
605
|
+
async function importByProductId(client, body) {
|
|
606
|
+
return client.post("/dsers-product-bff/import-list/product-id", body);
|
|
607
|
+
}
|
|
608
|
+
async function pushToStore(client, payload) {
|
|
609
|
+
return client.post("/dsers-product-bff/import-list/push", {
|
|
610
|
+
data: payload
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
async function getMyProducts(client, params) {
|
|
614
|
+
return client.get("/dsers-product-bff/my-product", params);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/dsers/account.ts
|
|
618
|
+
async function listStores(client) {
|
|
619
|
+
return client.post("/account-user-bff/v1/stores/user/list");
|
|
620
|
+
}
|
|
621
|
+
async function getUserInfo(client) {
|
|
622
|
+
return client.get("/account-user-bff/v1/users/info");
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/dsers/settings.ts
|
|
626
|
+
async function getGlobalSettings(client) {
|
|
627
|
+
return client.get("/infra-setting-bff/setting/list");
|
|
628
|
+
}
|
|
629
|
+
async function getPricingRules(client, storeId) {
|
|
630
|
+
return client.get("/dsers-settings-bff/product/pricing-rule", { storeId });
|
|
631
|
+
}
|
|
632
|
+
async function getCurrentPlan(client) {
|
|
633
|
+
return client.get("/dsers-plan-bff/plan");
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/agents/core-agent.ts
|
|
637
|
+
var log6 = createLogger("agent:core");
|
|
638
|
+
function buildModel(llm) {
|
|
639
|
+
switch (llm.provider) {
|
|
640
|
+
case "openai":
|
|
641
|
+
return createOpenAI({ apiKey: llm.apiKey })(llm.model);
|
|
642
|
+
case "anthropic":
|
|
643
|
+
return createAnthropic({ apiKey: llm.apiKey })(llm.model);
|
|
644
|
+
case "google":
|
|
645
|
+
return createGoogleGenerativeAI({ apiKey: llm.apiKey })(llm.model);
|
|
646
|
+
default:
|
|
647
|
+
return createOpenAI({ apiKey: llm.apiKey })(llm.model);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function getDefaultModel(provider) {
|
|
651
|
+
switch (provider) {
|
|
652
|
+
case "openai":
|
|
653
|
+
return "gpt-4o";
|
|
654
|
+
case "anthropic":
|
|
655
|
+
return "claude-sonnet-4-20250514";
|
|
656
|
+
case "google":
|
|
657
|
+
return "gemini-2.0-flash";
|
|
658
|
+
default:
|
|
659
|
+
return "gpt-4o";
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
var SYSTEM_PROMPT = `You are DropClaw, a friendly AI assistant that manages dropshipping stores through DSers.
|
|
663
|
+
Your user has ZERO technical background \u2014 they are a small business owner or solo entrepreneur.
|
|
664
|
+
|
|
665
|
+
COMMUNICATION RULES:
|
|
666
|
+
- Always respond in the SAME LANGUAGE the user writes in
|
|
667
|
+
- Use simple, warm, conversational language \u2014 no jargon
|
|
668
|
+
- When showing data (products, stores, orders), format it clearly with bullet points or numbered lists
|
|
669
|
+
- Always confirm before executing write operations (push, delete, update pricing)
|
|
670
|
+
- If something fails, explain what happened in plain language and suggest what to do next
|
|
671
|
+
- Proactively offer helpful next steps after completing a task
|
|
672
|
+
|
|
673
|
+
YOUR CAPABILITIES:
|
|
674
|
+
- View connected stores and store details
|
|
675
|
+
- Search and browse the import list (products from suppliers)
|
|
676
|
+
- Import new products from AliExpress, Temu, or 1688
|
|
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
|
|
682
|
+
|
|
683
|
+
GUIDELINES:
|
|
684
|
+
- When users describe products vaguely, search first and present options
|
|
685
|
+
- For bulk operations, show a preview and ask for confirmation
|
|
686
|
+
- Use simple summaries: "You have 15 products waiting to be pushed" instead of raw JSON
|
|
687
|
+
- If the user seems confused, offer guided suggestions like "Would you like me to show your stores first?"`;
|
|
688
|
+
var DropClawCoreAgent = class {
|
|
689
|
+
id = "dropclaw-core";
|
|
690
|
+
name = "DropClaw Core Agent";
|
|
691
|
+
dsers;
|
|
692
|
+
memory;
|
|
693
|
+
llm;
|
|
694
|
+
customTools = /* @__PURE__ */ new Map();
|
|
695
|
+
constructor(dsers, memory, llm) {
|
|
696
|
+
this.dsers = dsers;
|
|
697
|
+
this.memory = memory;
|
|
698
|
+
this.llm = llm;
|
|
699
|
+
}
|
|
700
|
+
registerTool(t) {
|
|
701
|
+
this.customTools.set(t.name, t);
|
|
702
|
+
}
|
|
703
|
+
removeTool(name) {
|
|
704
|
+
this.customTools.delete(name);
|
|
705
|
+
}
|
|
706
|
+
getTools() {
|
|
707
|
+
return Array.from(this.customTools.values());
|
|
708
|
+
}
|
|
709
|
+
async process(message, context) {
|
|
710
|
+
const model = buildModel(this.llm);
|
|
711
|
+
const memories = await this.memory.search(message, { userId: context.userId, limit: 5 }).catch(() => []);
|
|
712
|
+
const memoryContext = memories.length > 0 ? `
|
|
713
|
+
|
|
714
|
+
Relevant memories:
|
|
715
|
+
${memories.map((m) => `- ${m.content}`).join("\n")}` : "";
|
|
716
|
+
const messages = [
|
|
717
|
+
{ role: "system", content: SYSTEM_PROMPT + memoryContext },
|
|
718
|
+
...context.history.map(
|
|
719
|
+
(h) => ({
|
|
720
|
+
role: h.role,
|
|
721
|
+
content: h.content
|
|
722
|
+
})
|
|
723
|
+
),
|
|
724
|
+
{ role: "user", content: message }
|
|
725
|
+
];
|
|
726
|
+
const tools = this.buildAITools(context);
|
|
727
|
+
log6.info(
|
|
728
|
+
{ userId: context.userId, messageLen: message.length, toolCount: Object.keys(tools).length },
|
|
729
|
+
"Processing message"
|
|
730
|
+
);
|
|
731
|
+
try {
|
|
732
|
+
const result = await generateText({
|
|
733
|
+
model,
|
|
734
|
+
messages,
|
|
735
|
+
tools,
|
|
736
|
+
stopWhen: stepCountIs(5)
|
|
737
|
+
});
|
|
738
|
+
const toolCalls = result.steps?.flatMap(
|
|
739
|
+
(step) => step.toolCalls?.map((tc) => ({
|
|
740
|
+
tool: tc.toolName,
|
|
741
|
+
params: "input" in tc ? tc.input : {},
|
|
742
|
+
result: {
|
|
743
|
+
success: true,
|
|
744
|
+
data: step.toolResults?.find(
|
|
745
|
+
(tr) => tr.toolCallId === tc.toolCallId
|
|
746
|
+
)?.output
|
|
747
|
+
}
|
|
748
|
+
}))
|
|
749
|
+
).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
|
+
return {
|
|
759
|
+
text: result.text,
|
|
760
|
+
toolCalls
|
|
761
|
+
};
|
|
762
|
+
} catch (error) {
|
|
763
|
+
log6.error({ error, userId: context.userId }, "Agent processing failed");
|
|
764
|
+
writeAuditLog({
|
|
765
|
+
userId: context.userId,
|
|
766
|
+
agentId: this.id,
|
|
767
|
+
action: "process_message",
|
|
768
|
+
target: "llm",
|
|
769
|
+
result: "failed",
|
|
770
|
+
error: error instanceof Error ? error.message : String(error)
|
|
771
|
+
});
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Streaming version of process() — yields text chunks for real-time display.
|
|
777
|
+
* Returns textStream (async iterable) + response promise (resolves after completion).
|
|
778
|
+
*/
|
|
779
|
+
processStream(message, context) {
|
|
780
|
+
const model = buildModel(this.llm);
|
|
781
|
+
const memories = this.memory.search(message, { userId: context.userId, limit: 5 }).catch(() => []);
|
|
782
|
+
const tools = this.buildAITools(context);
|
|
783
|
+
const self = this;
|
|
784
|
+
const streamPromise = memories.then((mems) => {
|
|
785
|
+
const memoryContext = mems.length > 0 ? `
|
|
786
|
+
|
|
787
|
+
Relevant memories:
|
|
788
|
+
${mems.map((m) => `- ${m.content}`).join("\n")}` : "";
|
|
789
|
+
const messages = [
|
|
790
|
+
{ role: "system", content: SYSTEM_PROMPT + memoryContext },
|
|
791
|
+
...context.history.map(
|
|
792
|
+
(h) => ({
|
|
793
|
+
role: h.role,
|
|
794
|
+
content: h.content
|
|
795
|
+
})
|
|
796
|
+
),
|
|
797
|
+
{ role: "user", content: message }
|
|
798
|
+
];
|
|
799
|
+
log6.info(
|
|
800
|
+
{ userId: context.userId, messageLen: message.length, streaming: true },
|
|
801
|
+
"Processing message (stream)"
|
|
802
|
+
);
|
|
803
|
+
return streamText({
|
|
804
|
+
model,
|
|
805
|
+
messages,
|
|
806
|
+
tools,
|
|
807
|
+
stopWhen: stepCountIs(5)
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
const textStream = {
|
|
811
|
+
[Symbol.asyncIterator]() {
|
|
812
|
+
let innerIterator = null;
|
|
813
|
+
return {
|
|
814
|
+
async next() {
|
|
815
|
+
if (!innerIterator) {
|
|
816
|
+
const result = await streamPromise;
|
|
817
|
+
innerIterator = result.textStream[Symbol.asyncIterator]();
|
|
818
|
+
}
|
|
819
|
+
return innerIterator.next();
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
const response = streamPromise.then(async (result) => {
|
|
825
|
+
const text = await result.text;
|
|
826
|
+
const steps = await result.steps;
|
|
827
|
+
const toolCalls = steps?.flatMap(
|
|
828
|
+
(step) => step.toolCalls?.map((tc) => ({
|
|
829
|
+
tool: tc.toolName,
|
|
830
|
+
params: "input" in tc ? tc.input : {},
|
|
831
|
+
result: {
|
|
832
|
+
success: true,
|
|
833
|
+
data: step.toolResults?.find(
|
|
834
|
+
(tr) => tr.toolCallId === tc.toolCallId
|
|
835
|
+
)?.output
|
|
836
|
+
}
|
|
837
|
+
}))
|
|
838
|
+
).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
|
+
return {
|
|
848
|
+
text,
|
|
849
|
+
toolCalls
|
|
850
|
+
};
|
|
851
|
+
});
|
|
852
|
+
return { textStream, response };
|
|
853
|
+
}
|
|
854
|
+
buildAITools(context) {
|
|
855
|
+
const dsers = this.dsers;
|
|
856
|
+
const audit = (action, target, params, result = "pending") => writeAuditLog({ userId: context.userId, agentId: this.id, action, target, params, result });
|
|
857
|
+
return {
|
|
858
|
+
listStores: aiTool({
|
|
859
|
+
description: "List all connected stores (Shopify, WooCommerce, etc.) with their IDs and status.",
|
|
860
|
+
inputSchema: z2.object({}),
|
|
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).",
|
|
894
|
+
inputSchema: z2.object({
|
|
895
|
+
supplyProductId: z2.string().describe("Supplier product ID"),
|
|
896
|
+
supplyAppId: z2.number().describe("1=AliExpress, 2=Temu, 3=1688"),
|
|
897
|
+
country: z2.string().default("US").describe("Target country")
|
|
898
|
+
}),
|
|
899
|
+
execute: async (input) => {
|
|
900
|
+
audit("importProduct", "dsers:product", input);
|
|
901
|
+
const result = await importByProductId(dsers, input);
|
|
902
|
+
audit("importProduct", "dsers:product", void 0, "success");
|
|
903
|
+
return result;
|
|
904
|
+
}
|
|
905
|
+
}),
|
|
906
|
+
pushToStore: aiTool({
|
|
907
|
+
description: "Push products from import list to a connected store. ALWAYS confirm with user first.",
|
|
908
|
+
inputSchema: z2.object({
|
|
909
|
+
importListIds: z2.array(z2.string()).describe("Import list item IDs"),
|
|
910
|
+
storeIds: z2.array(z2.string()).describe("Target store IDs"),
|
|
911
|
+
pushStatus: z2.enum(["ACTIVE", "DRAFT"]).default("DRAFT")
|
|
912
|
+
}),
|
|
913
|
+
execute: async (input) => {
|
|
914
|
+
audit("pushToStore", "dsers:product", input);
|
|
915
|
+
const result = await pushToStore(dsers, input);
|
|
916
|
+
audit("pushToStore", "dsers:product", void 0, "success");
|
|
917
|
+
return result;
|
|
918
|
+
}
|
|
919
|
+
}),
|
|
920
|
+
getMyProducts: aiTool({
|
|
921
|
+
description: "Get products already pushed to stores. Check status, inventory, or find products to update.",
|
|
922
|
+
inputSchema: z2.object({
|
|
923
|
+
page: z2.number().optional(),
|
|
924
|
+
pageSize: z2.number().optional(),
|
|
925
|
+
keyword: z2.string().optional(),
|
|
926
|
+
storeId: z2.string().optional()
|
|
927
|
+
}),
|
|
928
|
+
execute: async (input) => {
|
|
929
|
+
audit("getMyProducts", "dsers:product", input);
|
|
930
|
+
const result = await getMyProducts(dsers, input);
|
|
931
|
+
audit("getMyProducts", "dsers:product", void 0, "success");
|
|
932
|
+
return result;
|
|
933
|
+
}
|
|
934
|
+
}),
|
|
935
|
+
getPricingRules: aiTool({
|
|
936
|
+
description: "Get pricing rules for a store (markup, rounding, etc.).",
|
|
937
|
+
inputSchema: z2.object({
|
|
938
|
+
storeId: z2.string().describe("Store ID to get pricing rules for")
|
|
939
|
+
}),
|
|
940
|
+
execute: async (input) => {
|
|
941
|
+
audit("getPricingRules", "dsers:settings", input);
|
|
942
|
+
const result = await getPricingRules(dsers, input.storeId);
|
|
943
|
+
audit("getPricingRules", "dsers:settings", void 0, "success");
|
|
944
|
+
return result;
|
|
945
|
+
}
|
|
946
|
+
}),
|
|
947
|
+
getGlobalSettings: aiTool({
|
|
948
|
+
description: "Get global DSers settings (notifications, sync preferences, etc.).",
|
|
949
|
+
inputSchema: z2.object({}),
|
|
950
|
+
execute: async () => {
|
|
951
|
+
audit("getGlobalSettings", "dsers:settings");
|
|
952
|
+
const result = await getGlobalSettings(dsers);
|
|
953
|
+
audit("getGlobalSettings", "dsers:settings", void 0, "success");
|
|
954
|
+
return result;
|
|
955
|
+
}
|
|
956
|
+
}),
|
|
957
|
+
getCurrentPlan: aiTool({
|
|
958
|
+
description: "Check the user's current DSers subscription plan and limits.",
|
|
959
|
+
inputSchema: z2.object({}),
|
|
960
|
+
execute: async () => {
|
|
961
|
+
audit("getCurrentPlan", "dsers:settings");
|
|
962
|
+
const result = await getCurrentPlan(dsers);
|
|
963
|
+
audit("getCurrentPlan", "dsers:settings", void 0, "success");
|
|
964
|
+
return result;
|
|
965
|
+
}
|
|
966
|
+
})
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
// src/web/server.ts
|
|
972
|
+
import { createServer } from "http";
|
|
973
|
+
import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
|
|
974
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
975
|
+
import { fileURLToPath } from "url";
|
|
976
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
977
|
+
import { v4 as uuid } from "uuid";
|
|
978
|
+
var log7 = createLogger("channel:web");
|
|
979
|
+
function findChatHtml() {
|
|
980
|
+
const thisDir = dirname2(fileURLToPath(import.meta.url));
|
|
981
|
+
const candidates = [
|
|
982
|
+
join5(thisDir, "chat.html"),
|
|
983
|
+
join5(thisDir, "web", "chat.html"),
|
|
984
|
+
join5(thisDir, "..", "web", "chat.html"),
|
|
985
|
+
join5(thisDir, "..", "src", "web", "chat.html"),
|
|
986
|
+
join5(thisDir, "..", "..", "src", "web", "chat.html")
|
|
987
|
+
];
|
|
988
|
+
for (const p of candidates) {
|
|
989
|
+
if (existsSync5(p)) {
|
|
990
|
+
return readFileSync4(p, "utf-8");
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
throw new Error(
|
|
994
|
+
"chat.html not found. Looked in:\n" + candidates.join("\n")
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
var WebChatServer = class {
|
|
998
|
+
name = "web";
|
|
999
|
+
server = null;
|
|
1000
|
+
wss = null;
|
|
1001
|
+
handler = null;
|
|
1002
|
+
connections = /* @__PURE__ */ new Map();
|
|
1003
|
+
_connected = false;
|
|
1004
|
+
port = 3e3;
|
|
1005
|
+
get connected() {
|
|
1006
|
+
return this._connected;
|
|
1007
|
+
}
|
|
1008
|
+
async connect(config) {
|
|
1009
|
+
this.port = config["port"] ?? 3e3;
|
|
1010
|
+
const chatHtml = findChatHtml();
|
|
1011
|
+
this.server = createServer((req, res) => {
|
|
1012
|
+
if (req.url === "/" || req.url === "/index.html") {
|
|
1013
|
+
res.writeHead(200, {
|
|
1014
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1015
|
+
"Cache-Control": "no-cache"
|
|
1016
|
+
});
|
|
1017
|
+
res.end(chatHtml);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (req.url === "/health") {
|
|
1021
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1022
|
+
res.end(JSON.stringify({ ok: true, clients: this.connections.size }));
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
res.writeHead(404);
|
|
1026
|
+
res.end("Not Found");
|
|
1027
|
+
});
|
|
1028
|
+
this.wss = new WebSocketServer({ server: this.server, path: "/ws" });
|
|
1029
|
+
this.wss.on("connection", (ws, req) => {
|
|
1030
|
+
const url = new URL(req.url ?? "/", `http://localhost:${this.port}`);
|
|
1031
|
+
let clientId = url.searchParams.get("userId") || uuid();
|
|
1032
|
+
this.wsSend(ws, { type: "session", userId: clientId });
|
|
1033
|
+
this.connections.set(clientId, ws);
|
|
1034
|
+
log7.info({ userId: clientId }, "Web client connected");
|
|
1035
|
+
ws.on("message", async (raw) => {
|
|
1036
|
+
try {
|
|
1037
|
+
const msg = JSON.parse(raw.toString());
|
|
1038
|
+
if (msg["userId"]) clientId = msg["userId"];
|
|
1039
|
+
this.connections.set(clientId, ws);
|
|
1040
|
+
if (!this.handler) return;
|
|
1041
|
+
const msgType = msg["type"];
|
|
1042
|
+
if (msgType === "message" || msgType === "callback") {
|
|
1043
|
+
const normalized = {
|
|
1044
|
+
traceId: uuid(),
|
|
1045
|
+
channelId: "web",
|
|
1046
|
+
channelName: "Web Chat",
|
|
1047
|
+
userId: clientId,
|
|
1048
|
+
text: msgType === "callback" ? msg["data"] : msg["text"],
|
|
1049
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1050
|
+
metadata: {
|
|
1051
|
+
isCallback: msgType === "callback"
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
await this.handler(normalized);
|
|
1055
|
+
}
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
log7.error({ error }, "WS message handler failed");
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
ws.on("close", () => {
|
|
1061
|
+
this.connections.delete(clientId);
|
|
1062
|
+
log7.debug({ userId: clientId }, "Web client disconnected");
|
|
1063
|
+
});
|
|
1064
|
+
ws.on("error", (err) => {
|
|
1065
|
+
log7.warn({ error: err.message, userId: clientId }, "WS error");
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
return new Promise((resolve, reject) => {
|
|
1069
|
+
this.server.listen(this.port, () => {
|
|
1070
|
+
this._connected = true;
|
|
1071
|
+
log7.info({ port: this.port }, "Web chat server started");
|
|
1072
|
+
resolve();
|
|
1073
|
+
});
|
|
1074
|
+
this.server.on("error", reject);
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
async disconnect() {
|
|
1078
|
+
for (const ws of this.connections.values()) {
|
|
1079
|
+
ws.close();
|
|
1080
|
+
}
|
|
1081
|
+
this.connections.clear();
|
|
1082
|
+
this.wss?.close();
|
|
1083
|
+
await new Promise((resolve) => {
|
|
1084
|
+
if (this.server) this.server.close(() => resolve());
|
|
1085
|
+
else resolve();
|
|
1086
|
+
});
|
|
1087
|
+
this._connected = false;
|
|
1088
|
+
log7.info("Web chat server stopped");
|
|
1089
|
+
}
|
|
1090
|
+
async reconnect() {
|
|
1091
|
+
await this.disconnect();
|
|
1092
|
+
await this.connect({ port: this.port });
|
|
1093
|
+
}
|
|
1094
|
+
onMessage(handler) {
|
|
1095
|
+
this.handler = handler;
|
|
1096
|
+
}
|
|
1097
|
+
async send(targetUserId, message) {
|
|
1098
|
+
if (message.card) {
|
|
1099
|
+
await this.sendCard(targetUserId, message.card);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (message.text) {
|
|
1103
|
+
this.wsSendTo(targetUserId, { type: "message", text: message.text });
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
async sendCard(targetUserId, card) {
|
|
1107
|
+
this.wsSendTo(targetUserId, {
|
|
1108
|
+
type: "card",
|
|
1109
|
+
title: card.title,
|
|
1110
|
+
summary: card.summary,
|
|
1111
|
+
actions: card.actions
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
async deleteMessage() {
|
|
1115
|
+
}
|
|
1116
|
+
// ─── Streaming Methods ─────────────────────────────────────
|
|
1117
|
+
sendStreamStart(userId) {
|
|
1118
|
+
this.wsSendTo(userId, { type: "stream_start" });
|
|
1119
|
+
}
|
|
1120
|
+
sendStreamDelta(userId, delta) {
|
|
1121
|
+
this.wsSendTo(userId, { type: "stream_delta", delta });
|
|
1122
|
+
}
|
|
1123
|
+
sendStreamEnd(userId) {
|
|
1124
|
+
this.wsSendTo(userId, { type: "stream_end" });
|
|
1125
|
+
}
|
|
1126
|
+
// ─── Web-Specific Methods ──────────────────────────────────
|
|
1127
|
+
sendTyping(userId, active) {
|
|
1128
|
+
this.wsSendTo(userId, { type: "typing", active });
|
|
1129
|
+
}
|
|
1130
|
+
sendClearInput(userId) {
|
|
1131
|
+
this.wsSendTo(userId, { type: "clear_input" });
|
|
1132
|
+
}
|
|
1133
|
+
sendSetInputType(userId, inputType) {
|
|
1134
|
+
this.wsSendTo(userId, { type: "set_input_type", inputType });
|
|
1135
|
+
}
|
|
1136
|
+
// ─── Internal ──────────────────────────────────────────────
|
|
1137
|
+
wsSendTo(userId, data) {
|
|
1138
|
+
const ws = this.connections.get(userId);
|
|
1139
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1140
|
+
this.wsSend(ws, data);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
wsSend(ws, data) {
|
|
1144
|
+
ws.send(JSON.stringify(data));
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
// src/channels/telegram.ts
|
|
1149
|
+
import { Bot, InlineKeyboard } from "grammy";
|
|
1150
|
+
import { v4 as uuid2 } from "uuid";
|
|
1151
|
+
var log8 = createLogger("channel:telegram");
|
|
1152
|
+
var TelegramAdapter = class {
|
|
1153
|
+
name = "telegram";
|
|
1154
|
+
bot = null;
|
|
1155
|
+
handler = null;
|
|
1156
|
+
_connected = false;
|
|
1157
|
+
botToken = "";
|
|
1158
|
+
get connected() {
|
|
1159
|
+
return this._connected;
|
|
1160
|
+
}
|
|
1161
|
+
async connect(config) {
|
|
1162
|
+
this.botToken = config["botToken"];
|
|
1163
|
+
if (!this.botToken) throw new Error("Telegram botToken is required");
|
|
1164
|
+
this.bot = new Bot(this.botToken);
|
|
1165
|
+
this.bot.on("message:text", async (ctx) => {
|
|
1166
|
+
if (!this.handler) return;
|
|
1167
|
+
const normalized = {
|
|
1168
|
+
traceId: uuid2(),
|
|
1169
|
+
channelId: "telegram",
|
|
1170
|
+
channelName: "Telegram",
|
|
1171
|
+
userId: String(ctx.from.id),
|
|
1172
|
+
userName: ctx.from.username ?? [ctx.from.first_name, ctx.from.last_name].filter(Boolean).join(" "),
|
|
1173
|
+
text: ctx.message.text,
|
|
1174
|
+
timestamp: new Date(ctx.message.date * 1e3),
|
|
1175
|
+
metadata: {
|
|
1176
|
+
chatId: ctx.chat.id,
|
|
1177
|
+
messageId: ctx.message.message_id
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
try {
|
|
1181
|
+
await this.handler(normalized);
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
log8.error({ error, userId: normalized.userId }, "Message handler error");
|
|
1184
|
+
await ctx.reply("Sorry, something went wrong. Please try again.");
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
this.bot.on("callback_query:data", async (ctx) => {
|
|
1188
|
+
if (!this.handler) return;
|
|
1189
|
+
await ctx.answerCallbackQuery();
|
|
1190
|
+
const normalized = {
|
|
1191
|
+
traceId: uuid2(),
|
|
1192
|
+
channelId: "telegram",
|
|
1193
|
+
channelName: "Telegram",
|
|
1194
|
+
userId: String(ctx.from.id),
|
|
1195
|
+
userName: ctx.from.username ?? ctx.from.first_name,
|
|
1196
|
+
text: ctx.callbackQuery.data,
|
|
1197
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1198
|
+
metadata: {
|
|
1199
|
+
chatId: ctx.chat?.id,
|
|
1200
|
+
isCallback: true
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
try {
|
|
1204
|
+
await this.handler(normalized);
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
log8.error({ error }, "Callback handler error");
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
this.bot.catch((err) => {
|
|
1210
|
+
log8.error({ error: err.message }, "Bot error \u2014 reconnecting...");
|
|
1211
|
+
this._connected = false;
|
|
1212
|
+
setTimeout(() => this.reconnect(), 5e3);
|
|
1213
|
+
});
|
|
1214
|
+
await this.bot.init();
|
|
1215
|
+
log8.info({ botName: this.bot.botInfo.username }, "Telegram bot initialized");
|
|
1216
|
+
this.bot.start({
|
|
1217
|
+
onStart: () => {
|
|
1218
|
+
this._connected = true;
|
|
1219
|
+
log8.info("Telegram bot polling started");
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
async disconnect() {
|
|
1224
|
+
this.bot?.stop();
|
|
1225
|
+
this._connected = false;
|
|
1226
|
+
log8.info("Telegram bot disconnected");
|
|
1227
|
+
}
|
|
1228
|
+
async reconnect() {
|
|
1229
|
+
log8.info("Reconnecting Telegram bot...");
|
|
1230
|
+
await this.disconnect();
|
|
1231
|
+
await this.connect({ botToken: this.botToken });
|
|
1232
|
+
}
|
|
1233
|
+
onMessage(handler) {
|
|
1234
|
+
this.handler = handler;
|
|
1235
|
+
}
|
|
1236
|
+
async send(targetUserId, message) {
|
|
1237
|
+
if (!this.bot) throw new Error("Bot not connected");
|
|
1238
|
+
if (message.card) {
|
|
1239
|
+
await this.sendCard(targetUserId, message.card);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (message.text) {
|
|
1243
|
+
await this.bot.api.sendMessage(targetUserId, message.text, {
|
|
1244
|
+
parse_mode: "Markdown"
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
async sendCard(targetUserId, card) {
|
|
1249
|
+
if (!this.bot) throw new Error("Bot not connected");
|
|
1250
|
+
const keyboard = new InlineKeyboard();
|
|
1251
|
+
for (const action of card.actions) {
|
|
1252
|
+
keyboard.text(action.label, action.callbackData).row();
|
|
1253
|
+
}
|
|
1254
|
+
const text = `*${card.title}*
|
|
1255
|
+
|
|
1256
|
+
${card.summary}`;
|
|
1257
|
+
await this.bot.api.sendMessage(targetUserId, text, {
|
|
1258
|
+
parse_mode: "Markdown",
|
|
1259
|
+
reply_markup: keyboard
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
async deleteMessage(chatId, messageId) {
|
|
1263
|
+
if (!this.bot) return;
|
|
1264
|
+
try {
|
|
1265
|
+
await this.bot.api.deleteMessage(chatId, messageId);
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
log8.warn({ chatId, messageId, error }, "Failed to delete message");
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
// src/memory/file-provider.ts
|
|
1273
|
+
import {
|
|
1274
|
+
readFileSync as readFileSync5,
|
|
1275
|
+
writeFileSync as writeFileSync4,
|
|
1276
|
+
appendFileSync as appendFileSync2,
|
|
1277
|
+
existsSync as existsSync6,
|
|
1278
|
+
mkdirSync as mkdirSync5,
|
|
1279
|
+
readdirSync
|
|
1280
|
+
} from "fs";
|
|
1281
|
+
import { join as join6 } from "path";
|
|
1282
|
+
import { randomUUID } from "crypto";
|
|
1283
|
+
var log9 = createLogger("memory:file");
|
|
1284
|
+
var MEMORY_DIR = join6(CONFIG_DIR, "memories");
|
|
1285
|
+
var FileMemoryProvider = class {
|
|
1286
|
+
name = "file";
|
|
1287
|
+
degraded = false;
|
|
1288
|
+
constructor() {
|
|
1289
|
+
if (!existsSync6(MEMORY_DIR)) {
|
|
1290
|
+
mkdirSync5(MEMORY_DIR, { recursive: true });
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
async add(content, metadata) {
|
|
1294
|
+
const id = randomUUID();
|
|
1295
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1296
|
+
const entry = {
|
|
1297
|
+
id,
|
|
1298
|
+
content,
|
|
1299
|
+
metadata,
|
|
1300
|
+
createdAt: now,
|
|
1301
|
+
updatedAt: now
|
|
1302
|
+
};
|
|
1303
|
+
const file = this.userFile(metadata.userId);
|
|
1304
|
+
appendFileSync2(file, JSON.stringify(entry) + "\n");
|
|
1305
|
+
log9.debug({ id, userId: metadata.userId }, "Memory added");
|
|
1306
|
+
return id;
|
|
1307
|
+
}
|
|
1308
|
+
async search(query, filters) {
|
|
1309
|
+
const all = await this.getAll(filters);
|
|
1310
|
+
const keywords = query.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
1311
|
+
if (keywords.length === 0) {
|
|
1312
|
+
return all.slice(0, filters.limit ?? 10);
|
|
1313
|
+
}
|
|
1314
|
+
const scored = all.map((m) => {
|
|
1315
|
+
const text = m.content.toLowerCase();
|
|
1316
|
+
const matchCount = keywords.filter((k) => text.includes(k)).length;
|
|
1317
|
+
return { memory: m, score: matchCount / keywords.length };
|
|
1318
|
+
}).filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, filters.limit ?? 10);
|
|
1319
|
+
return scored.map((s) => ({ ...s.memory, score: s.score }));
|
|
1320
|
+
}
|
|
1321
|
+
async getAll(filters) {
|
|
1322
|
+
const files = filters.userId ? [this.userFile(filters.userId)] : readdirSync(MEMORY_DIR).filter((f) => f.endsWith(".jsonl")).map((f) => join6(MEMORY_DIR, f));
|
|
1323
|
+
const results = [];
|
|
1324
|
+
for (const file of files) {
|
|
1325
|
+
if (!existsSync6(file)) continue;
|
|
1326
|
+
const lines = readFileSync5(file, "utf-8").split("\n").filter((l) => l.trim());
|
|
1327
|
+
for (const line of lines) {
|
|
1328
|
+
try {
|
|
1329
|
+
const stored = JSON.parse(line);
|
|
1330
|
+
if (filters.category && stored.metadata.category !== filters.category) continue;
|
|
1331
|
+
if (filters.sessionId && stored.metadata.sessionId !== filters.sessionId) continue;
|
|
1332
|
+
if (filters.tags && filters.tags.length > 0 && !filters.tags.some((t) => stored.metadata.tags?.includes(t))) continue;
|
|
1333
|
+
results.push({
|
|
1334
|
+
id: stored.id,
|
|
1335
|
+
content: stored.content,
|
|
1336
|
+
metadata: stored.metadata,
|
|
1337
|
+
createdAt: new Date(stored.createdAt),
|
|
1338
|
+
updatedAt: new Date(stored.updatedAt)
|
|
1339
|
+
});
|
|
1340
|
+
} catch {
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return results.slice(0, filters.limit ?? 100);
|
|
1345
|
+
}
|
|
1346
|
+
async update(id, content) {
|
|
1347
|
+
const files = readdirSync(MEMORY_DIR).filter((f) => f.endsWith(".jsonl")).map((f) => join6(MEMORY_DIR, f));
|
|
1348
|
+
for (const file of files) {
|
|
1349
|
+
if (!existsSync6(file)) continue;
|
|
1350
|
+
const lines = readFileSync5(file, "utf-8").split("\n").filter((l) => l.trim());
|
|
1351
|
+
const updated = lines.map((line) => {
|
|
1352
|
+
try {
|
|
1353
|
+
const stored = JSON.parse(line);
|
|
1354
|
+
if (stored.id === id) {
|
|
1355
|
+
stored.content = content;
|
|
1356
|
+
stored.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1357
|
+
}
|
|
1358
|
+
return JSON.stringify(stored);
|
|
1359
|
+
} catch {
|
|
1360
|
+
return line;
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
writeFileSync4(file, updated.join("\n") + "\n");
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
async delete(id) {
|
|
1367
|
+
const files = readdirSync(MEMORY_DIR).filter((f) => f.endsWith(".jsonl")).map((f) => join6(MEMORY_DIR, f));
|
|
1368
|
+
for (const file of files) {
|
|
1369
|
+
if (!existsSync6(file)) continue;
|
|
1370
|
+
const lines = readFileSync5(file, "utf-8").split("\n").filter((l) => l.trim());
|
|
1371
|
+
const filtered = lines.filter((line) => {
|
|
1372
|
+
try {
|
|
1373
|
+
const stored = JSON.parse(line);
|
|
1374
|
+
return stored.id !== id;
|
|
1375
|
+
} catch {
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
writeFileSync4(file, filtered.join("\n") + (filtered.length > 0 ? "\n" : ""));
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
userFile(userId) {
|
|
1383
|
+
const safe = userId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1384
|
+
return join6(MEMORY_DIR, `${safe}.jsonl`);
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
// src/resilience/degradation.ts
|
|
1389
|
+
var log10 = createLogger("degradation");
|
|
1390
|
+
var states = /* @__PURE__ */ new Map();
|
|
1391
|
+
function isDegraded(service) {
|
|
1392
|
+
return states.get(service)?.degraded ?? false;
|
|
1393
|
+
}
|
|
1394
|
+
function degradationMessage(service) {
|
|
1395
|
+
switch (service) {
|
|
1396
|
+
case "dsers":
|
|
1397
|
+
return "DSers service is temporarily unavailable. Your request has been queued and will be processed when the service recovers.";
|
|
1398
|
+
case "llm":
|
|
1399
|
+
return "AI service is experiencing issues. Using simplified responses until it recovers.";
|
|
1400
|
+
case "mem0":
|
|
1401
|
+
return "Memory service is temporarily offline. I can still help but won't remember preferences from this conversation.";
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/gateway/gateway.ts
|
|
1406
|
+
var log11 = createLogger("gateway");
|
|
1407
|
+
var sessionHistories = /* @__PURE__ */ new Map();
|
|
1408
|
+
var MAX_HISTORY = 20;
|
|
1409
|
+
var DropClawGateway = class {
|
|
1410
|
+
config;
|
|
1411
|
+
channels = [];
|
|
1412
|
+
webChannel = null;
|
|
1413
|
+
memory;
|
|
1414
|
+
agents = /* @__PURE__ */ new Map();
|
|
1415
|
+
dsersClients = /* @__PURE__ */ new Map();
|
|
1416
|
+
running = false;
|
|
1417
|
+
constructor(config) {
|
|
1418
|
+
this.config = config;
|
|
1419
|
+
this.memory = new FileMemoryProvider();
|
|
1420
|
+
}
|
|
1421
|
+
async start() {
|
|
1422
|
+
log11.info("Starting DropClaw Gateway...");
|
|
1423
|
+
const web = new WebChatServer();
|
|
1424
|
+
web.onMessage((msg) => this.handleMessage(web, msg));
|
|
1425
|
+
await web.connect({ port: this.config.port });
|
|
1426
|
+
this.webChannel = web;
|
|
1427
|
+
this.channels.push(web);
|
|
1428
|
+
log11.info({ port: this.config.port }, "Web chat channel connected");
|
|
1429
|
+
if (this.config.telegramBotToken) {
|
|
1430
|
+
try {
|
|
1431
|
+
const tg = new TelegramAdapter();
|
|
1432
|
+
tg.onMessage((msg) => this.handleMessage(tg, msg));
|
|
1433
|
+
await tg.connect({ botToken: this.config.telegramBotToken });
|
|
1434
|
+
this.channels.push(tg);
|
|
1435
|
+
log11.info("Telegram channel connected");
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
log11.warn({ error }, "Telegram failed to connect \u2014 web chat still available");
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
this.running = true;
|
|
1441
|
+
log11.info("DropClaw Gateway started");
|
|
1442
|
+
}
|
|
1443
|
+
async handleMessage(channel, message) {
|
|
1444
|
+
await runWithTrace(
|
|
1445
|
+
{
|
|
1446
|
+
traceId: message.traceId,
|
|
1447
|
+
userId: message.userId,
|
|
1448
|
+
channelId: message.channelId
|
|
1449
|
+
},
|
|
1450
|
+
async () => {
|
|
1451
|
+
const userId = message.userId;
|
|
1452
|
+
if (!checkInboundLimit(userId)) {
|
|
1453
|
+
log11.warn({ userId }, "Rate limit exceeded");
|
|
1454
|
+
await channel.send(userId, { text: "Too many messages. Please wait a moment." });
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
await debounceUser(userId);
|
|
1458
|
+
const release = await acquireUserLock(userId);
|
|
1459
|
+
try {
|
|
1460
|
+
const session = loadSession(userId);
|
|
1461
|
+
if (isOnboarding(session.state)) {
|
|
1462
|
+
await this.handleOnboarding(channel, message, session);
|
|
1463
|
+
} else {
|
|
1464
|
+
if (isDegraded("llm")) {
|
|
1465
|
+
await channel.send(userId, { text: degradationMessage("llm") });
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
await this.handleReady(channel, message, session);
|
|
1469
|
+
}
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
log11.error({ error, userId }, "Failed to handle message");
|
|
1472
|
+
await channel.send(userId, {
|
|
1473
|
+
text: "Something went wrong. Please try again."
|
|
1474
|
+
});
|
|
1475
|
+
} finally {
|
|
1476
|
+
release();
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
// ─── Onboarding State Machine ─────────────────────────────────────
|
|
1482
|
+
async handleOnboarding(channel, message, session) {
|
|
1483
|
+
const userId = message.userId;
|
|
1484
|
+
const text = message.text.trim();
|
|
1485
|
+
const isCallback = message.metadata["isCallback"] === true;
|
|
1486
|
+
const isWeb = channel instanceof WebChatServer;
|
|
1487
|
+
switch (session.state) {
|
|
1488
|
+
case "new":
|
|
1489
|
+
case "onboard_demo":
|
|
1490
|
+
await this.onboardWelcome(channel, userId, session);
|
|
1491
|
+
break;
|
|
1492
|
+
case "onboard_dsers_email":
|
|
1493
|
+
await this.onboardEmail(channel, userId, text, session);
|
|
1494
|
+
break;
|
|
1495
|
+
case "onboard_dsers_password":
|
|
1496
|
+
await this.onboardPassword(channel, message, session, isWeb);
|
|
1497
|
+
break;
|
|
1498
|
+
case "onboard_llm":
|
|
1499
|
+
await this.onboardLLM(channel, message, session, isCallback, isWeb);
|
|
1500
|
+
break;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
async onboardWelcome(channel, userId, session) {
|
|
1504
|
+
const welcome = "Welcome to *DropClaw*! 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- ...all through this chat!\n\nLet's get you set up. It takes about 1 minute.\n\n*Step 1/3:* What is your DSers account email?";
|
|
1505
|
+
await channel.send(userId, { text: welcome });
|
|
1506
|
+
session.state = "onboard_dsers_email";
|
|
1507
|
+
saveSession(userId, session);
|
|
1508
|
+
}
|
|
1509
|
+
async onboardEmail(channel, userId, text, session) {
|
|
1510
|
+
if (!text.includes("@") || !text.includes(".")) {
|
|
1511
|
+
await channel.send(userId, {
|
|
1512
|
+
text: "That doesn't look like an email address. Please enter your DSers login email:"
|
|
1513
|
+
});
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
session.dspiEmail = text;
|
|
1517
|
+
session.state = "onboard_dsers_password";
|
|
1518
|
+
saveSession(userId, session);
|
|
1519
|
+
if (channel instanceof WebChatServer) {
|
|
1520
|
+
channel.sendSetInputType(userId, "password");
|
|
1521
|
+
}
|
|
1522
|
+
await channel.send(userId, {
|
|
1523
|
+
text: "Got it!\n\n*Step 2/3:* Now enter your DSers password." + (channel instanceof WebChatServer ? "" : "\n_I will delete your message immediately for security._")
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
async onboardPassword(channel, message, session, isWeb) {
|
|
1527
|
+
const userId = message.userId;
|
|
1528
|
+
const password = message.text.trim();
|
|
1529
|
+
if (isWeb) {
|
|
1530
|
+
channel.sendClearInput(userId);
|
|
1531
|
+
channel.sendSetInputType(userId, "text");
|
|
1532
|
+
} else {
|
|
1533
|
+
await channel.deleteMessage(
|
|
1534
|
+
message.metadata["chatId"],
|
|
1535
|
+
message.metadata["messageId"]
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
if (password.length < 1) {
|
|
1539
|
+
await channel.send(userId, { text: "Password cannot be empty. Please try again:" });
|
|
1540
|
+
if (isWeb) channel.sendSetInputType(userId, "password");
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
session.dspiPassword = password;
|
|
1544
|
+
saveSession(userId, session);
|
|
1545
|
+
await channel.send(userId, { text: "Verifying with DSers..." });
|
|
1546
|
+
try {
|
|
1547
|
+
const dsersConfig = createDSersConfig(session.dspiEmail, password);
|
|
1548
|
+
const client = new DSersClient(dsersConfig);
|
|
1549
|
+
await client.get("/account-user-bff/v1/users/info");
|
|
1550
|
+
this.dsersClients.set(userId, client);
|
|
1551
|
+
session.state = "onboard_llm";
|
|
1552
|
+
saveSession(userId, session);
|
|
1553
|
+
await channel.send(userId, {
|
|
1554
|
+
card: {
|
|
1555
|
+
title: "DSers Connected!",
|
|
1556
|
+
summary: "Your DSers account is verified.\n\n*Step 3/3:* Choose your AI provider.\nThis powers my intelligence. You can get a free API key from any provider.",
|
|
1557
|
+
actions: [
|
|
1558
|
+
{ label: "OpenAI (GPT-4o)", callbackData: "llm:openai" },
|
|
1559
|
+
{ label: "Anthropic (Claude)", callbackData: "llm:anthropic" },
|
|
1560
|
+
{ label: "Google (Gemini)", callbackData: "llm:google" }
|
|
1561
|
+
]
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
session.dspiPassword = void 0;
|
|
1566
|
+
session.state = "onboard_dsers_password";
|
|
1567
|
+
saveSession(userId, session);
|
|
1568
|
+
if (isWeb) channel.sendSetInputType(userId, "password");
|
|
1569
|
+
const msg = error instanceof Error && error.message.includes("401") ? "Wrong email or password. Please re-enter your password:" : `Login failed: ${error instanceof Error ? error.message : "unknown error"}. Please try again:`;
|
|
1570
|
+
await channel.send(userId, { text: msg });
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
async onboardLLM(channel, message, session, isCallback, isWeb) {
|
|
1574
|
+
const userId = message.userId;
|
|
1575
|
+
const text = message.text.trim();
|
|
1576
|
+
if (isCallback && text.startsWith("llm:")) {
|
|
1577
|
+
const provider = text.replace("llm:", "");
|
|
1578
|
+
session.llmProvider = provider;
|
|
1579
|
+
saveSession(userId, session);
|
|
1580
|
+
const providerName = provider === "openai" ? "OpenAI" : provider === "anthropic" ? "Anthropic" : "Google";
|
|
1581
|
+
if (isWeb) channel.sendSetInputType(userId, "password");
|
|
1582
|
+
await channel.send(userId, {
|
|
1583
|
+
text: `You chose *${providerName}*. Now paste your API key below.` + (isWeb ? "" : "\n_I will delete your message immediately._")
|
|
1584
|
+
});
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (!session.llmProvider) {
|
|
1588
|
+
await channel.send(userId, {
|
|
1589
|
+
card: {
|
|
1590
|
+
title: "Choose AI Provider",
|
|
1591
|
+
summary: "Please select your AI provider first:",
|
|
1592
|
+
actions: [
|
|
1593
|
+
{ label: "OpenAI (GPT-4o)", callbackData: "llm:openai" },
|
|
1594
|
+
{ label: "Anthropic (Claude)", callbackData: "llm:anthropic" },
|
|
1595
|
+
{ label: "Google (Gemini)", callbackData: "llm:google" }
|
|
1596
|
+
]
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
if (isWeb) {
|
|
1602
|
+
channel.sendClearInput(userId);
|
|
1603
|
+
channel.sendSetInputType(userId, "text");
|
|
1604
|
+
} else {
|
|
1605
|
+
await channel.deleteMessage(
|
|
1606
|
+
message.metadata["chatId"],
|
|
1607
|
+
message.metadata["messageId"]
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
if (text.length < 10) {
|
|
1611
|
+
await channel.send(userId, { text: "That doesn't look like a valid API key. Please paste your key:" });
|
|
1612
|
+
if (isWeb) channel.sendSetInputType(userId, "password");
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
session.llmApiKey = text;
|
|
1616
|
+
session.llmModel = getDefaultModel(session.llmProvider);
|
|
1617
|
+
session.state = "ready";
|
|
1618
|
+
saveSession(userId, session);
|
|
1619
|
+
await channel.send(userId, {
|
|
1620
|
+
text: `All set! DropClaw is ready.
|
|
1621
|
+
|
|
1622
|
+
Try these:
|
|
1623
|
+
- "Show me my stores"
|
|
1624
|
+
- "What's in my import list?"
|
|
1625
|
+
- "Search for phone cases on AliExpress"
|
|
1626
|
+
- "Check my pricing rules"
|
|
1627
|
+
|
|
1628
|
+
Just type naturally \u2014 I understand everyday language!`
|
|
1629
|
+
});
|
|
1630
|
+
writeAuditLog({
|
|
1631
|
+
userId,
|
|
1632
|
+
agentId: "gateway",
|
|
1633
|
+
action: "onboarding_complete",
|
|
1634
|
+
target: "user-session",
|
|
1635
|
+
result: "success"
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
// ─── Ready State: Route to AI Agent ───────────────────────────────
|
|
1639
|
+
async handleReady(channel, message, session) {
|
|
1640
|
+
const userId = message.userId;
|
|
1641
|
+
if (message.text.trim().toLowerCase() === "/reset") {
|
|
1642
|
+
this.agents.delete(userId);
|
|
1643
|
+
this.dsersClients.delete(userId);
|
|
1644
|
+
sessionHistories.delete(userId);
|
|
1645
|
+
saveSession(userId, { state: "new" });
|
|
1646
|
+
await channel.send(userId, { text: "Account reset. Send any message to start over." });
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const agent = this.getOrCreateAgent(userId, session);
|
|
1650
|
+
const sessionKey = `${channel.name}:${userId}`;
|
|
1651
|
+
const history = sessionHistories.get(sessionKey) ?? [];
|
|
1652
|
+
const context = {
|
|
1653
|
+
userId,
|
|
1654
|
+
sessionId: sessionKey,
|
|
1655
|
+
channelId: channel.name,
|
|
1656
|
+
history: history.slice(-MAX_HISTORY)
|
|
1657
|
+
};
|
|
1658
|
+
writeAuditLog({
|
|
1659
|
+
userId,
|
|
1660
|
+
agentId: agent.id,
|
|
1661
|
+
action: "receive_message",
|
|
1662
|
+
target: channel.name,
|
|
1663
|
+
params: { text: message.text.slice(0, 100) },
|
|
1664
|
+
result: "success"
|
|
1665
|
+
});
|
|
1666
|
+
try {
|
|
1667
|
+
if (channel instanceof WebChatServer) {
|
|
1668
|
+
await this.handleReadyStreaming(channel, userId, message.text, agent, context, history, sessionKey);
|
|
1669
|
+
} else {
|
|
1670
|
+
await this.handleReadyBatch(channel, userId, message.text, agent, context, history, sessionKey);
|
|
1671
|
+
}
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
log11.error({ error, userId }, "Agent processing failed");
|
|
1674
|
+
writeAuditLog({
|
|
1675
|
+
userId,
|
|
1676
|
+
agentId: agent.id,
|
|
1677
|
+
action: "process_failed",
|
|
1678
|
+
target: channel.name,
|
|
1679
|
+
result: "failed",
|
|
1680
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1681
|
+
});
|
|
1682
|
+
const errMsg = error instanceof Error ? error.message : "";
|
|
1683
|
+
if (errMsg.includes("401") || errMsg.includes("API key")) {
|
|
1684
|
+
await channel.send(userId, {
|
|
1685
|
+
text: "Your API key may be invalid or expired. Send /reset to reconfigure."
|
|
1686
|
+
});
|
|
1687
|
+
} else {
|
|
1688
|
+
await channel.send(userId, {
|
|
1689
|
+
text: "I'm having trouble right now. Please try again in a moment."
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
async handleReadyStreaming(channel, userId, text, agent, context, history, sessionKey) {
|
|
1695
|
+
channel.sendTyping(userId, true);
|
|
1696
|
+
const { textStream, response } = agent.processStream(text, context);
|
|
1697
|
+
channel.sendTyping(userId, false);
|
|
1698
|
+
channel.sendStreamStart(userId);
|
|
1699
|
+
for await (const delta of textStream) {
|
|
1700
|
+
channel.sendStreamDelta(userId, delta);
|
|
1701
|
+
}
|
|
1702
|
+
channel.sendStreamEnd(userId);
|
|
1703
|
+
const result = await response;
|
|
1704
|
+
history.push({ role: "user", content: text });
|
|
1705
|
+
history.push({ role: "assistant", content: result.text });
|
|
1706
|
+
sessionHistories.set(sessionKey, history.slice(-MAX_HISTORY * 2));
|
|
1707
|
+
writeAuditLog({
|
|
1708
|
+
userId,
|
|
1709
|
+
agentId: agent.id,
|
|
1710
|
+
action: "send_response",
|
|
1711
|
+
target: "web",
|
|
1712
|
+
params: {
|
|
1713
|
+
responseLen: result.text.length,
|
|
1714
|
+
toolCalls: result.toolCalls?.length ?? 0,
|
|
1715
|
+
streaming: true
|
|
1716
|
+
},
|
|
1717
|
+
result: "success"
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
async handleReadyBatch(channel, userId, text, agent, context, history, sessionKey) {
|
|
1721
|
+
const result = await agent.process(text, context);
|
|
1722
|
+
history.push({ role: "user", content: text });
|
|
1723
|
+
history.push({ role: "assistant", content: result.text });
|
|
1724
|
+
sessionHistories.set(sessionKey, history.slice(-MAX_HISTORY * 2));
|
|
1725
|
+
if (result.text) {
|
|
1726
|
+
await channel.send(userId, { text: result.text });
|
|
1727
|
+
}
|
|
1728
|
+
writeAuditLog({
|
|
1729
|
+
userId,
|
|
1730
|
+
agentId: agent.id,
|
|
1731
|
+
action: "send_response",
|
|
1732
|
+
target: channel.name,
|
|
1733
|
+
params: {
|
|
1734
|
+
responseLen: result.text.length,
|
|
1735
|
+
toolCalls: result.toolCalls?.length ?? 0
|
|
1736
|
+
},
|
|
1737
|
+
result: "success"
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
getOrCreateAgent(userId, session) {
|
|
1741
|
+
let agent = this.agents.get(userId);
|
|
1742
|
+
if (agent) return agent;
|
|
1743
|
+
const dsersConfig = createDSersConfig(
|
|
1744
|
+
session.dspiEmail,
|
|
1745
|
+
session.dspiPassword
|
|
1746
|
+
);
|
|
1747
|
+
const dsersClient = new DSersClient(dsersConfig);
|
|
1748
|
+
this.dsersClients.set(userId, dsersClient);
|
|
1749
|
+
const llm = {
|
|
1750
|
+
provider: session.llmProvider,
|
|
1751
|
+
apiKey: session.llmApiKey,
|
|
1752
|
+
model: session.llmModel ?? getDefaultModel(session.llmProvider)
|
|
1753
|
+
};
|
|
1754
|
+
agent = new DropClawCoreAgent(dsersClient, this.memory, llm);
|
|
1755
|
+
this.agents.set(userId, agent);
|
|
1756
|
+
return agent;
|
|
1757
|
+
}
|
|
1758
|
+
async stop() {
|
|
1759
|
+
log11.info("Stopping DropClaw Gateway...");
|
|
1760
|
+
for (const ch of this.channels) {
|
|
1761
|
+
await ch.disconnect();
|
|
1762
|
+
}
|
|
1763
|
+
this.running = false;
|
|
1764
|
+
log11.info("DropClaw Gateway stopped");
|
|
1765
|
+
}
|
|
1766
|
+
isRunning() {
|
|
1767
|
+
return this.running;
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
|
|
1771
|
+
// src/cli/start.ts
|
|
1772
|
+
var log12 = createLogger("cli:start");
|
|
1773
|
+
async function openBrowser(url) {
|
|
1774
|
+
const { exec } = await import("child_process");
|
|
1775
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1776
|
+
exec(`${cmd} ${url}`, () => {
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
async function startCommand(opts) {
|
|
1780
|
+
try {
|
|
1781
|
+
const config = loadConfig(opts.config);
|
|
1782
|
+
const url = `http://localhost:${config.port}`;
|
|
1783
|
+
console.log("\n DropClaw");
|
|
1784
|
+
console.log(` Web Chat: ${url}`);
|
|
1785
|
+
if (config.telegramBotToken) {
|
|
1786
|
+
console.log(" Telegram: enabled");
|
|
1787
|
+
}
|
|
1788
|
+
console.log();
|
|
1789
|
+
const gateway = new DropClawGateway(config);
|
|
1790
|
+
await gateway.start();
|
|
1791
|
+
if (opts.open !== false) {
|
|
1792
|
+
await openBrowser(url);
|
|
1793
|
+
}
|
|
1794
|
+
console.log(` DropClaw is running. Chat at ${url}
|
|
1795
|
+
`);
|
|
1796
|
+
const shutdown = async () => {
|
|
1797
|
+
log12.info("Shutting down gracefully...");
|
|
1798
|
+
await gateway.stop();
|
|
1799
|
+
process.exit(0);
|
|
1800
|
+
};
|
|
1801
|
+
process.on("SIGINT", shutdown);
|
|
1802
|
+
process.on("SIGTERM", shutdown);
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
log12.fatal({ error }, "Failed to start");
|
|
1805
|
+
console.error(
|
|
1806
|
+
`
|
|
1807
|
+
Failed to start: ${error instanceof Error ? error.message : error}`
|
|
1808
|
+
);
|
|
1809
|
+
process.exit(1);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/cli/doctor.ts
|
|
1814
|
+
async function doctorCommand() {
|
|
1815
|
+
console.log("\n DropClaw Doctor\n");
|
|
1816
|
+
const results = [];
|
|
1817
|
+
if (configExists()) {
|
|
1818
|
+
try {
|
|
1819
|
+
const config = loadConfig();
|
|
1820
|
+
results.push({ name: "Config file", status: "ok", message: CONFIG_PATH });
|
|
1821
|
+
results.push({
|
|
1822
|
+
name: "Web chat",
|
|
1823
|
+
status: "ok",
|
|
1824
|
+
message: `Port ${config.port}`
|
|
1825
|
+
});
|
|
1826
|
+
if (config.telegramBotToken) {
|
|
1827
|
+
const tokenValid = config.telegramBotToken.includes(":");
|
|
1828
|
+
results.push({
|
|
1829
|
+
name: "Telegram token",
|
|
1830
|
+
status: tokenValid ? "ok" : "warn",
|
|
1831
|
+
message: tokenValid ? "Format valid" : "Format suspicious"
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
} catch (e) {
|
|
1835
|
+
results.push({
|
|
1836
|
+
name: "Config file",
|
|
1837
|
+
status: "fail",
|
|
1838
|
+
message: e instanceof Error ? e.message : String(e)
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
} else {
|
|
1842
|
+
results.push({
|
|
1843
|
+
name: "Config file",
|
|
1844
|
+
status: "fail",
|
|
1845
|
+
message: "Not found. Run 'dropclaw init' first."
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
try {
|
|
1849
|
+
const resp = await fetch("https://bff-api-gw.dsers.com/", {
|
|
1850
|
+
method: "HEAD",
|
|
1851
|
+
signal: AbortSignal.timeout(5e3)
|
|
1852
|
+
});
|
|
1853
|
+
results.push({
|
|
1854
|
+
name: "DSers API",
|
|
1855
|
+
status: resp.status < 500 ? "ok" : "warn",
|
|
1856
|
+
message: `Reachable (HTTP ${resp.status})`
|
|
1857
|
+
});
|
|
1858
|
+
} catch (e) {
|
|
1859
|
+
results.push({
|
|
1860
|
+
name: "DSers API",
|
|
1861
|
+
status: "warn",
|
|
1862
|
+
message: e instanceof Error ? e.message : "Unreachable"
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
for (const r of results) {
|
|
1866
|
+
const icon = r.status === "ok" ? " [OK]" : r.status === "warn" ? " [!!]" : "[FAIL]";
|
|
1867
|
+
console.log(` ${icon} ${r.name}: ${r.message}`);
|
|
1868
|
+
}
|
|
1869
|
+
const failed = results.filter((r) => r.status === "fail").length;
|
|
1870
|
+
console.log(
|
|
1871
|
+
failed === 0 ? "\n All checks passed.\n" : `
|
|
1872
|
+
${failed} check(s) failed. Fix issues above.
|
|
1873
|
+
`
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// src/cli/index.ts
|
|
1878
|
+
var program = new Command();
|
|
1879
|
+
program.name("dsclaw").description("AI-powered dropshipping agent \u2014 chat with your DSers store.").version("0.1.0");
|
|
1880
|
+
program.action(() => startCommand({}));
|
|
1881
|
+
program.command("init").description("Setup wizard \u2014 configure Telegram (optional)").action(initCommand);
|
|
1882
|
+
program.command("start").description("Start the DropClaw bot").option("-c, --config <path>", "Path to config file").option("--no-open", "Don't auto-open browser").action(startCommand);
|
|
1883
|
+
program.command("stop").description("Stop the DropClaw bot (PM2)").action(async () => {
|
|
1884
|
+
const { execSync } = await import("child_process");
|
|
1885
|
+
try {
|
|
1886
|
+
execSync("pm2 stop dsclaw", { stdio: "inherit" });
|
|
1887
|
+
} catch {
|
|
1888
|
+
console.log(" Bot is not running or PM2 is not installed.");
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
program.command("status").description("Show bot status").action(async () => {
|
|
1892
|
+
const { execSync } = await import("child_process");
|
|
1893
|
+
try {
|
|
1894
|
+
execSync("pm2 describe dsclaw", { stdio: "inherit" });
|
|
1895
|
+
} catch {
|
|
1896
|
+
console.log(" Bot is not running. Start with: dsclaw start");
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
program.command("doctor").description("Verify configuration").action(doctorCommand);
|
|
1900
|
+
program.parse();
|
|
1901
|
+
//# sourceMappingURL=index.js.map
|