@zhin.js/console 1.0.51 → 1.0.52

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +22 -0
  3. package/browser.tsconfig.json +19 -0
  4. package/client/src/components/PageHeader.tsx +26 -0
  5. package/client/src/components/ui/accordion.tsx +2 -1
  6. package/client/src/components/ui/badge.tsx +1 -3
  7. package/client/src/components/ui/scroll-area.tsx +5 -2
  8. package/client/src/components/ui/select.tsx +7 -3
  9. package/client/src/components/ui/separator.tsx +5 -2
  10. package/client/src/components/ui/tabs.tsx +4 -2
  11. package/client/src/layouts/dashboard.tsx +223 -121
  12. package/client/src/main.tsx +34 -34
  13. package/client/src/pages/bot-detail/MessageBody.tsx +110 -0
  14. package/client/src/pages/bot-detail/date-utils.ts +8 -0
  15. package/client/src/pages/bot-detail/index.tsx +798 -0
  16. package/client/src/pages/bot-detail/types.ts +92 -0
  17. package/client/src/pages/bot-detail/useBotConsole.tsx +600 -0
  18. package/client/src/pages/bots.tsx +111 -73
  19. package/client/src/pages/database/constants.ts +16 -0
  20. package/client/src/pages/database/database-page.tsx +170 -0
  21. package/client/src/pages/database/document-collection-view.tsx +155 -0
  22. package/client/src/pages/database/index.tsx +1 -0
  23. package/client/src/pages/database/json-field.tsx +11 -0
  24. package/client/src/pages/database/kv-bucket-view.tsx +169 -0
  25. package/client/src/pages/database/related-table-view.tsx +221 -0
  26. package/client/src/pages/env.tsx +38 -28
  27. package/client/src/pages/files/code-editor.tsx +85 -0
  28. package/client/src/pages/files/editor-constants.ts +9 -0
  29. package/client/src/pages/files/file-editor.tsx +133 -0
  30. package/client/src/pages/files/file-icons.tsx +25 -0
  31. package/client/src/pages/files/files-page.tsx +92 -0
  32. package/client/src/pages/files/hljs-global.d.ts +10 -0
  33. package/client/src/pages/files/index.tsx +1 -0
  34. package/client/src/pages/files/language.ts +18 -0
  35. package/client/src/pages/files/tree-node.tsx +69 -0
  36. package/client/src/pages/files/use-hljs-theme.ts +23 -0
  37. package/client/src/pages/logs.tsx +77 -22
  38. package/client/src/style.css +144 -0
  39. package/client/src/utils/parseComposerContent.ts +57 -0
  40. package/client/tailwind.config.js +1 -0
  41. package/client/tsconfig.json +3 -1
  42. package/dist/assets/index-COKXlFo2.js +124 -0
  43. package/dist/assets/style-kkLO-vsa.css +3 -0
  44. package/dist/client.js +482 -464
  45. package/dist/index.html +2 -2
  46. package/dist/style.css +1 -1
  47. package/lib/index.js +1010 -81
  48. package/lib/transform.js +16 -2
  49. package/lib/websocket.js +845 -28
  50. package/node.tsconfig.json +18 -0
  51. package/package.json +13 -15
  52. package/src/bin.ts +24 -0
  53. package/src/bot-db-models.ts +74 -0
  54. package/src/bot-hub.ts +240 -0
  55. package/src/bot-persistence.ts +270 -0
  56. package/src/build.ts +90 -0
  57. package/src/dev.ts +107 -0
  58. package/src/index.ts +337 -0
  59. package/src/transform.ts +199 -0
  60. package/src/websocket.ts +1369 -0
  61. package/client/src/pages/database.tsx +0 -708
  62. package/client/src/pages/files.tsx +0 -470
  63. package/client/src/pages/login-assist.tsx +0 -225
  64. package/dist/assets/index-DS4RbHWX.js +0 -124
  65. package/dist/assets/style-DS-m6WEr.css +0 -3
package/lib/websocket.js CHANGED
@@ -1,10 +1,334 @@
1
- import fs from 'fs';
2
- import path from 'path';
1
+ import fs2 from 'fs';
2
+ import path2 from 'path';
3
3
  import WebSocket from 'ws';
4
- import { usePlugin } from '@zhin.js/core';
4
+ import { usePlugin, Adapter } from '@zhin.js/core';
5
+
6
+ // src/websocket.ts
7
+
8
+ // src/bot-persistence.ts
9
+ var DATA_DIR = path2.join(process.cwd(), "data");
10
+ var REQ_FILE = path2.join(DATA_DIR, "console_bot_requests.json");
11
+ var NOTICE_FILE = path2.join(DATA_DIR, "console_bot_notices.json");
12
+ function ensureDir() {
13
+ if (!fs2.existsSync(DATA_DIR)) fs2.mkdirSync(DATA_DIR, { recursive: true });
14
+ }
15
+ function loadFile(file, empty) {
16
+ try {
17
+ if (!fs2.existsSync(file)) return { ...empty };
18
+ const raw = fs2.readFileSync(file, "utf-8");
19
+ const j = JSON.parse(raw);
20
+ if (!j || !Array.isArray(j.rows)) return { ...empty };
21
+ return {
22
+ nextId: typeof j.nextId === "number" ? j.nextId : 1,
23
+ rows: j.rows
24
+ };
25
+ } catch {
26
+ return { ...empty };
27
+ }
28
+ }
29
+ function saveFile(file, store) {
30
+ ensureDir();
31
+ fs2.writeFileSync(file, JSON.stringify(store, null, 0), "utf-8");
32
+ }
33
+ async function insertRequest(row) {
34
+ const store = loadFile(REQ_FILE, { nextId: 1, rows: [] });
35
+ const id = store.nextId++;
36
+ const full = { ...row, id, consumed: 0 };
37
+ store.rows.push(full);
38
+ saveFile(REQ_FILE, store);
39
+ return full;
40
+ }
41
+ async function insertNotice(row) {
42
+ const store = loadFile(NOTICE_FILE, { nextId: 1, rows: [] });
43
+ const id = store.nextId++;
44
+ const full = { ...row, id, consumed: 0 };
45
+ store.rows.push(full);
46
+ saveFile(NOTICE_FILE, store);
47
+ return full;
48
+ }
49
+ async function listUnconsumedRequests() {
50
+ const store = loadFile(REQ_FILE, { nextId: 1, rows: [] });
51
+ return store.rows.filter((r) => r.consumed === 0).sort((a, b) => a.created_at - b.created_at);
52
+ }
53
+ async function listUnconsumedNotices() {
54
+ const store = loadFile(NOTICE_FILE, { nextId: 1, rows: [] });
55
+ return store.rows.filter((r) => r.consumed === 0).sort((a, b) => a.created_at - b.created_at);
56
+ }
57
+ async function listRequestsForBot(adapter, botId) {
58
+ const all = await listUnconsumedRequests();
59
+ return all.filter((r) => r.adapter === adapter && r.bot_id === botId);
60
+ }
61
+ async function markRequestsConsumed(ids) {
62
+ if (!ids.length) return;
63
+ const now = Date.now();
64
+ const store = loadFile(REQ_FILE, { nextId: 1, rows: [] });
65
+ const set = new Set(ids);
66
+ for (const r of store.rows) {
67
+ if (set.has(r.id) && r.consumed === 0) {
68
+ r.consumed = 1;
69
+ r.consumed_at = now;
70
+ }
71
+ }
72
+ saveFile(REQ_FILE, store);
73
+ }
74
+ async function markNoticesConsumed(ids) {
75
+ if (!ids.length) return;
76
+ const now = Date.now();
77
+ const store = loadFile(NOTICE_FILE, { nextId: 1, rows: [] });
78
+ const set = new Set(ids);
79
+ for (const r of store.rows) {
80
+ if (set.has(r.id) && r.consumed === 0) {
81
+ r.consumed = 1;
82
+ r.consumed_at = now;
83
+ }
84
+ }
85
+ saveFile(NOTICE_FILE, store);
86
+ }
87
+ async function findRequestRow(adapter, botId, platformRequestId) {
88
+ const store = loadFile(REQ_FILE, { nextId: 1, rows: [] });
89
+ const r = store.rows.find(
90
+ (x) => x.adapter === adapter && x.bot_id === botId && x.platform_request_id === platformRequestId && x.consumed === 0
91
+ );
92
+ return r;
93
+ }
94
+ async function getRequestRowById(id) {
95
+ const store = loadFile(REQ_FILE, { nextId: 1, rows: [] });
96
+ const r = store.rows.find((x) => x.id === id);
97
+ return r;
98
+ }
99
+
100
+ // src/bot-hub.ts
101
+ var wssRef = null;
102
+ var hubInited = false;
103
+ function setBotHubWss(wss) {
104
+ wssRef = wss;
105
+ }
106
+ function broadcast(obj) {
107
+ const msg = JSON.stringify(obj);
108
+ const clients = wssRef?.clients;
109
+ if (!clients) return;
110
+ const list = clients instanceof Set ? [...clients] : clients;
111
+ for (const ws of list) {
112
+ if (ws.readyState === 1) ws.send(msg);
113
+ }
114
+ }
115
+ function requestMemoryKey(adapter, botId, platformId) {
116
+ return `${adapter}:${botId}:${platformId}`;
117
+ }
118
+ var pendingRequestObjects = /* @__PURE__ */ new Map();
119
+ async function storePendingRequest(adapter, botId, req) {
120
+ const platformId = req.$id;
121
+ const key = requestMemoryKey(adapter, botId, platformId);
122
+ pendingRequestObjects.set(key, req);
123
+ const row = await insertRequest({
124
+ adapter,
125
+ bot_id: botId,
126
+ platform_request_id: platformId,
127
+ type: String(req.$type),
128
+ sender_id: String(req.$sender?.id ?? ""),
129
+ sender_name: String(req.$sender?.name ?? ""),
130
+ comment: String(req.$comment ?? ""),
131
+ channel_id: String(req.$channel?.id ?? ""),
132
+ channel_type: String(req.$channel?.type ?? "private"),
133
+ created_at: typeof req.$timestamp === "number" ? req.$timestamp : Date.now()
134
+ });
135
+ return row;
136
+ }
137
+ function rowToRequestPushData(row, canAct) {
138
+ return {
139
+ id: row.id,
140
+ adapter: row.adapter,
141
+ botId: row.bot_id,
142
+ platformRequestId: row.platform_request_id,
143
+ type: row.type,
144
+ sender: { id: row.sender_id, name: row.sender_name },
145
+ comment: row.comment,
146
+ channel: { id: row.channel_id, type: row.channel_type },
147
+ timestamp: row.created_at,
148
+ canAct
149
+ };
150
+ }
151
+ async function onRequestReceived(adapter, botId, req) {
152
+ const row = await storePendingRequest(adapter, botId, req);
153
+ const key = requestMemoryKey(adapter, botId, req.$id);
154
+ const canAct = pendingRequestObjects.has(key);
155
+ broadcast({ type: "bot:request", data: rowToRequestPushData(row, canAct) });
156
+ }
157
+ async function onNoticeReceived(adapter, botId, notice) {
158
+ let raw = {};
159
+ try {
160
+ raw = Object.fromEntries(
161
+ Object.entries(notice).filter(
162
+ ([k]) => !k.startsWith("$") && k !== "adapter" && k !== "bot"
163
+ )
164
+ );
165
+ } catch {
166
+ raw = {};
167
+ }
168
+ let payload;
169
+ try {
170
+ payload = JSON.stringify({
171
+ type: notice.$type,
172
+ subType: notice.$subType,
173
+ channel: notice.$channel,
174
+ raw
175
+ });
176
+ } catch {
177
+ payload = JSON.stringify({ type: notice.$type, error: "serialize_failed" });
178
+ }
179
+ const row = await insertNotice({
180
+ adapter,
181
+ bot_id: botId,
182
+ notice_type: String(notice.$type ?? "unknown"),
183
+ channel_type: String(notice.$channel?.type ?? ""),
184
+ channel_id: String(notice.$channel?.id ?? ""),
185
+ payload,
186
+ created_at: typeof notice.$timestamp === "number" ? notice.$timestamp : Date.now()
187
+ });
188
+ broadcast({
189
+ type: "bot:notice",
190
+ data: {
191
+ id: row.id,
192
+ adapter: row.adapter,
193
+ botId: row.bot_id,
194
+ noticeType: row.notice_type,
195
+ channel: { id: row.channel_id, type: row.channel_type },
196
+ payload: row.payload,
197
+ timestamp: row.created_at
198
+ }
199
+ });
200
+ }
201
+ function getPendingRequest(adapter, botId, platformRequestId) {
202
+ return pendingRequestObjects.get(requestMemoryKey(adapter, botId, platformRequestId));
203
+ }
204
+ function removePendingRequest(adapter, botId, platformRequestId) {
205
+ pendingRequestObjects.delete(requestMemoryKey(adapter, botId, platformRequestId));
206
+ }
207
+ async function markRequestConsumedByPlatformId(adapter, botId, platformRequestId) {
208
+ const row = await findRequestRow(adapter, botId, platformRequestId);
209
+ if (row) await markRequestsConsumed([row.id]);
210
+ removePendingRequest(adapter, botId, platformRequestId);
211
+ }
212
+ async function sendCatchUpToClient(ws) {
213
+ const reqs = await listUnconsumedRequests();
214
+ for (const row of reqs) {
215
+ const canAct = pendingRequestObjects.has(
216
+ requestMemoryKey(row.adapter, row.bot_id, row.platform_request_id)
217
+ );
218
+ if (ws.readyState === 1) {
219
+ ws.send(
220
+ JSON.stringify({
221
+ type: "bot:request",
222
+ data: rowToRequestPushData(row, canAct)
223
+ })
224
+ );
225
+ }
226
+ }
227
+ const notices = await listUnconsumedNotices();
228
+ for (const row of notices) {
229
+ if (ws.readyState === 1) {
230
+ ws.send(
231
+ JSON.stringify({
232
+ type: "bot:notice",
233
+ data: {
234
+ id: row.id,
235
+ adapter: row.adapter,
236
+ botId: row.bot_id,
237
+ noticeType: row.notice_type,
238
+ channel: { id: row.channel_id, type: row.channel_type },
239
+ payload: row.payload,
240
+ timestamp: row.created_at
241
+ }
242
+ })
243
+ );
244
+ }
245
+ }
246
+ }
247
+ function initBotHub(root2) {
248
+ if (hubInited) return;
249
+ hubInited = true;
250
+ const handlerReq = (req) => {
251
+ const adapter = String(req.$adapter);
252
+ const botId = String(req.$bot);
253
+ onRequestReceived(adapter, botId, req);
254
+ };
255
+ const handlerNotice = (notice) => {
256
+ const adapter = String(notice.$adapter);
257
+ const botId = String(notice.$bot);
258
+ onNoticeReceived(adapter, botId, notice);
259
+ };
260
+ root2.on("request.receive", handlerReq);
261
+ root2.on("notice.receive", handlerNotice);
262
+ const adapterNames = root2.adapters ? [...root2.adapters] : [];
263
+ const inject = root2.inject;
264
+ if (inject && typeof inject === "function" && adapterNames.length > 0) {
265
+ for (const name of adapterNames) {
266
+ try {
267
+ const ad = inject(name);
268
+ if (ad && typeof ad.on === "function") {
269
+ ad.on("message.receive", (msg) => {
270
+ const payload = {
271
+ type: "bot:message",
272
+ data: {
273
+ adapter: name,
274
+ botId: msg?.$bot,
275
+ channelId: msg?.$channel?.id,
276
+ channelType: msg?.$channel?.type,
277
+ sender: msg?.$sender,
278
+ content: msg?.$content ?? [],
279
+ timestamp: typeof msg?.$timestamp === "number" ? msg.$timestamp : Date.now()
280
+ }
281
+ };
282
+ broadcast(payload);
283
+ });
284
+ }
285
+ } catch {
286
+ }
287
+ }
288
+ }
289
+ }
5
290
 
6
291
  // src/websocket.ts
7
292
  var { root, logger } = usePlugin();
293
+ function collectBotsList() {
294
+ const bots = [];
295
+ for (const name of root.adapters) {
296
+ const adapter = root.inject(name);
297
+ if (adapter instanceof Adapter) {
298
+ for (const [botName, bot] of adapter.bots.entries()) {
299
+ bots.push({
300
+ name: botName,
301
+ adapter: String(name),
302
+ connected: !!bot.$connected,
303
+ status: bot.$connected ? "online" : "offline"
304
+ });
305
+ }
306
+ }
307
+ }
308
+ return bots;
309
+ }
310
+ async function collectBotsListWithPending() {
311
+ const bots = collectBotsList();
312
+ let reqs = [];
313
+ let notices = [];
314
+ try {
315
+ [reqs, notices] = await Promise.all([listUnconsumedRequests(), listUnconsumedNotices()]);
316
+ } catch {
317
+ }
318
+ return bots.map((bot) => {
319
+ const pendingRequestCount = reqs.filter(
320
+ (r) => r.adapter === bot.adapter && r.bot_id === bot.name
321
+ ).length;
322
+ const pendingNoticeCount = notices.filter(
323
+ (n) => n.adapter === bot.adapter && n.bot_id === bot.name
324
+ ).length;
325
+ return {
326
+ ...bot,
327
+ pendingRequestCount,
328
+ pendingNoticeCount
329
+ };
330
+ });
331
+ }
8
332
  var ENV_WHITELIST = [".env", ".env.development", ".env.production"];
9
333
  var FILE_MANAGER_ALLOWED = [
10
334
  "src",
@@ -28,7 +352,7 @@ var FILE_MANAGER_BLOCKED = /* @__PURE__ */ new Set([
28
352
  "coverage"
29
353
  ]);
30
354
  function isPathAllowed(relativePath) {
31
- if (relativePath.includes("..") || path.isAbsolute(relativePath)) return false;
355
+ if (relativePath.includes("..") || path2.isAbsolute(relativePath)) return false;
32
356
  const normalized = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
33
357
  const firstSegment = normalized.split("/")[0];
34
358
  if (FILE_MANAGER_BLOCKED.has(firstSegment)) return false;
@@ -48,15 +372,20 @@ function getPluginKeys() {
48
372
  return Array.from(keys);
49
373
  }
50
374
  function getConfigFilePath() {
51
- return path.resolve(process.cwd(), "zhin.config.yml");
375
+ return path2.resolve(process.cwd(), "zhin.config.yml");
52
376
  }
53
377
  function setupWebSocket(webServer) {
378
+ setBotHubWss(webServer.ws);
379
+ initBotHub(root);
54
380
  webServer.ws.on("connection", (ws) => {
55
381
  ws.send(JSON.stringify({
56
382
  type: "sync",
57
383
  data: { key: "entries", value: Object.values(webServer.entries) }
58
384
  }));
59
385
  ws.send(JSON.stringify({ type: "init-data", timestamp: Date.now() }));
386
+ void sendCatchUpToClient(ws).catch(
387
+ (e) => logger.warn("[console] bot catch-up failed", e.message)
388
+ );
60
389
  ws.on("message", async (data) => {
61
390
  try {
62
391
  const message = JSON.parse(data.toString());
@@ -88,7 +417,7 @@ async function handleWebSocketMessage(ws, message, webServer) {
88
417
  case "config:get-yaml":
89
418
  try {
90
419
  const filePath = getConfigFilePath();
91
- const yaml = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : "";
420
+ const yaml = fs2.existsSync(filePath) ? fs2.readFileSync(filePath, "utf-8") : "";
92
421
  ws.send(JSON.stringify({ requestId, data: { yaml, pluginKeys: getPluginKeys() } }));
93
422
  } catch (error) {
94
423
  ws.send(JSON.stringify({ requestId, error: `Failed to read config: ${error.message}` }));
@@ -102,7 +431,7 @@ async function handleWebSocketMessage(ws, message, webServer) {
102
431
  break;
103
432
  }
104
433
  const filePath = getConfigFilePath();
105
- fs.writeFileSync(filePath, yaml, "utf-8");
434
+ fs2.writeFileSync(filePath, yaml, "utf-8");
106
435
  const configService = root.inject("config");
107
436
  const loader = configService.configs.get("zhin.config.yml");
108
437
  if (loader) loader.load();
@@ -225,7 +554,7 @@ async function handleWebSocketMessage(ws, message, webServer) {
225
554
  const cwd = process.cwd();
226
555
  const files = ENV_WHITELIST.map((name) => ({
227
556
  name,
228
- exists: fs.existsSync(path.resolve(cwd, name))
557
+ exists: fs2.existsSync(path2.resolve(cwd, name))
229
558
  }));
230
559
  ws.send(JSON.stringify({ requestId, data: { files } }));
231
560
  } catch (error) {
@@ -239,8 +568,8 @@ async function handleWebSocketMessage(ws, message, webServer) {
239
568
  ws.send(JSON.stringify({ requestId, error: `Invalid env file: ${filename}` }));
240
569
  break;
241
570
  }
242
- const envPath = path.resolve(process.cwd(), filename);
243
- const content = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8") : "";
571
+ const envPath = path2.resolve(process.cwd(), filename);
572
+ const content = fs2.existsSync(envPath) ? fs2.readFileSync(envPath, "utf-8") : "";
244
573
  ws.send(JSON.stringify({ requestId, data: { content } }));
245
574
  } catch (error) {
246
575
  ws.send(JSON.stringify({ requestId, error: `Failed to read env file: ${error.message}` }));
@@ -257,8 +586,8 @@ async function handleWebSocketMessage(ws, message, webServer) {
257
586
  ws.send(JSON.stringify({ requestId, error: "content field is required" }));
258
587
  break;
259
588
  }
260
- const envPath = path.resolve(process.cwd(), filename);
261
- fs.writeFileSync(envPath, content, "utf-8");
589
+ const envPath = path2.resolve(process.cwd(), filename);
590
+ fs2.writeFileSync(envPath, content, "utf-8");
262
591
  ws.send(JSON.stringify({ requestId, data: { success: true, message: "\u73AF\u5883\u53D8\u91CF\u5DF2\u4FDD\u5B58\uFF0C\u9700\u91CD\u542F\u751F\u6548" } }));
263
592
  } catch (error) {
264
593
  ws.send(JSON.stringify({ requestId, error: `Failed to save env file: ${error.message}` }));
@@ -283,12 +612,12 @@ async function handleWebSocketMessage(ws, message, webServer) {
283
612
  ws.send(JSON.stringify({ requestId, error: `Access denied: ${fp}` }));
284
613
  break;
285
614
  }
286
- const absPath = path.resolve(process.cwd(), fp);
287
- if (!fs.existsSync(absPath)) {
615
+ const absPath = path2.resolve(process.cwd(), fp);
616
+ if (!fs2.existsSync(absPath)) {
288
617
  ws.send(JSON.stringify({ requestId, error: `File not found: ${fp}` }));
289
618
  break;
290
619
  }
291
- const stat = fs.statSync(absPath);
620
+ const stat = fs2.statSync(absPath);
292
621
  if (!stat.isFile()) {
293
622
  ws.send(JSON.stringify({ requestId, error: `Not a file: ${fp}` }));
294
623
  break;
@@ -297,7 +626,7 @@ async function handleWebSocketMessage(ws, message, webServer) {
297
626
  ws.send(JSON.stringify({ requestId, error: `File too large: ${(stat.size / 1024).toFixed(0)}KB (max 1MB)` }));
298
627
  break;
299
628
  }
300
- const fileContent = fs.readFileSync(absPath, "utf-8");
629
+ const fileContent = fs2.readFileSync(absPath, "utf-8");
301
630
  ws.send(JSON.stringify({ requestId, data: { content: fileContent, size: stat.size } }));
302
631
  } catch (error) {
303
632
  ws.send(JSON.stringify({ requestId, error: `Failed to read file: ${error.message}` }));
@@ -314,12 +643,12 @@ async function handleWebSocketMessage(ws, message, webServer) {
314
643
  ws.send(JSON.stringify({ requestId, error: "content field is required" }));
315
644
  break;
316
645
  }
317
- const absPath = path.resolve(process.cwd(), fp);
318
- const dir = path.dirname(absPath);
319
- if (!fs.existsSync(dir)) {
320
- fs.mkdirSync(dir, { recursive: true });
646
+ const absPath = path2.resolve(process.cwd(), fp);
647
+ const dir = path2.dirname(absPath);
648
+ if (!fs2.existsSync(dir)) {
649
+ fs2.mkdirSync(dir, { recursive: true });
321
650
  }
322
- fs.writeFileSync(absPath, fileContent, "utf-8");
651
+ fs2.writeFileSync(absPath, fileContent, "utf-8");
323
652
  ws.send(JSON.stringify({ requestId, data: { success: true, message: `\u6587\u4EF6\u5DF2\u4FDD\u5B58: ${fp}` } }));
324
653
  } catch (error) {
325
654
  ws.send(JSON.stringify({ requestId, error: `Failed to save file: ${error.message}` }));
@@ -462,6 +791,494 @@ async function handleWebSocketMessage(ws, message, webServer) {
462
791
  ws.send(JSON.stringify({ requestId, error: `Failed to get entries: ${error.message}` }));
463
792
  }
464
793
  break;
794
+ // ================================================================
795
+ // 机器人管理(WebSocket)
796
+ // ================================================================
797
+ case "bot:list": {
798
+ try {
799
+ const botsWithPending = await collectBotsListWithPending();
800
+ ws.send(JSON.stringify({ requestId, data: { bots: botsWithPending } }));
801
+ } catch (error) {
802
+ ws.send(JSON.stringify({ requestId, error: error.message }));
803
+ }
804
+ break;
805
+ }
806
+ case "bot:info": {
807
+ try {
808
+ const d = message.data || {};
809
+ const { adapter, botId } = d;
810
+ if (!adapter || !botId) {
811
+ ws.send(JSON.stringify({ requestId, error: "adapter and botId required" }));
812
+ break;
813
+ }
814
+ const ad = root.inject(adapter);
815
+ if (!(ad instanceof Adapter)) {
816
+ ws.send(JSON.stringify({ requestId, error: "adapter not found" }));
817
+ break;
818
+ }
819
+ const bot = ad.bots.get(botId);
820
+ if (!bot) {
821
+ ws.send(JSON.stringify({ requestId, error: "bot not found" }));
822
+ break;
823
+ }
824
+ ws.send(
825
+ JSON.stringify({
826
+ requestId,
827
+ data: {
828
+ name: botId,
829
+ adapter: String(adapter),
830
+ connected: !!bot.$connected,
831
+ status: bot.$connected ? "online" : "offline"
832
+ }
833
+ })
834
+ );
835
+ } catch (error) {
836
+ ws.send(JSON.stringify({ requestId, error: error.message }));
837
+ }
838
+ break;
839
+ }
840
+ case "bot:sendMessage": {
841
+ try {
842
+ const d = message.data || {};
843
+ const { adapter, botId, id, type: msgType, content } = d;
844
+ if (!adapter || !botId || !id || !msgType || content === void 0) {
845
+ ws.send(
846
+ JSON.stringify({
847
+ requestId,
848
+ error: "adapter, botId, id, type, content required"
849
+ })
850
+ );
851
+ break;
852
+ }
853
+ const ad = root.inject(adapter);
854
+ if (!(ad instanceof Adapter)) {
855
+ ws.send(JSON.stringify({ requestId, error: "adapter not found" }));
856
+ break;
857
+ }
858
+ const normalized = typeof content === "string" ? content : Array.isArray(content) ? content : String(content);
859
+ const messageId = await ad.sendMessage({
860
+ context: adapter,
861
+ bot: botId,
862
+ id: String(id),
863
+ type: msgType,
864
+ content: normalized
865
+ });
866
+ ws.send(JSON.stringify({ requestId, data: { messageId } }));
867
+ } catch (error) {
868
+ ws.send(JSON.stringify({ requestId, error: error.message }));
869
+ }
870
+ break;
871
+ }
872
+ case "bot:friends":
873
+ case "bot:groups": {
874
+ try {
875
+ const d = message.data || {};
876
+ const { adapter, botId } = d;
877
+ if (!adapter || !botId) {
878
+ ws.send(JSON.stringify({ requestId, error: "adapter and botId required" }));
879
+ break;
880
+ }
881
+ if (adapter !== "icqq") {
882
+ ws.send(JSON.stringify({ requestId, error: "not supported for this adapter" }));
883
+ break;
884
+ }
885
+ const ad = root.inject("icqq");
886
+ const bot = ad?.bots?.get?.(botId);
887
+ if (!bot) {
888
+ ws.send(JSON.stringify({ requestId, error: "bot not found" }));
889
+ break;
890
+ }
891
+ if (type === "bot:friends") {
892
+ const fl = bot.fl;
893
+ const friends = Array.from((fl || /* @__PURE__ */ new Map()).values()).map((f) => ({
894
+ user_id: f.user_id,
895
+ nickname: f.nickname,
896
+ remark: f.remark
897
+ }));
898
+ ws.send(JSON.stringify({ requestId, data: { friends, count: friends.length } }));
899
+ } else {
900
+ const gl = bot.gl;
901
+ const groups = Array.from((gl || /* @__PURE__ */ new Map()).values()).map((g) => ({
902
+ group_id: g.group_id,
903
+ name: g.name
904
+ }));
905
+ ws.send(JSON.stringify({ requestId, data: { groups, count: groups.length } }));
906
+ }
907
+ } catch (error) {
908
+ ws.send(JSON.stringify({ requestId, error: error.message }));
909
+ }
910
+ break;
911
+ }
912
+ case "bot:channels": {
913
+ try {
914
+ const d = message.data || {};
915
+ const { adapter, botId } = d;
916
+ if (!adapter || !botId) {
917
+ ws.send(JSON.stringify({ requestId, error: "adapter and botId required" }));
918
+ break;
919
+ }
920
+ if (adapter === "icqq") {
921
+ ws.send(JSON.stringify({ requestId, error: "channels not supported for icqq" }));
922
+ break;
923
+ }
924
+ const ad = root.inject(adapter);
925
+ const bot = ad?.bots?.get?.(botId);
926
+ if (!bot) {
927
+ ws.send(JSON.stringify({ requestId, error: "bot not found" }));
928
+ break;
929
+ }
930
+ const channels = [];
931
+ if (adapter === "qq" && typeof bot.getGuilds === "function" && typeof bot.getChannels === "function") {
932
+ const guilds = await bot.getGuilds() || [];
933
+ for (const g of guilds) {
934
+ const gid = g?.id ?? g?.guild_id ?? String(g);
935
+ const chs = await bot.getChannels(gid) || [];
936
+ for (const c of chs) {
937
+ channels.push({
938
+ id: String(c?.id ?? c?.channel_id ?? c),
939
+ name: String(c?.name ?? c?.channel_name ?? c?.id ?? "")
940
+ });
941
+ }
942
+ }
943
+ } else if (typeof ad?.listChannels === "function") {
944
+ const result = await ad.listChannels(botId);
945
+ if (Array.isArray(result)) channels.push(...result.map((c) => ({ id: String(c?.id ?? c), name: String(c?.name ?? c?.id ?? "") })));
946
+ else if (result?.channels) channels.push(...result.channels.map((c) => ({ id: String(c?.id ?? c), name: String(c?.name ?? c?.id ?? "") })));
947
+ }
948
+ ws.send(JSON.stringify({ requestId, data: { channels, count: channels.length } }));
949
+ } catch (error) {
950
+ ws.send(JSON.stringify({ requestId, error: error.message }));
951
+ }
952
+ break;
953
+ }
954
+ case "bot:deleteFriend": {
955
+ try {
956
+ const d = message.data || {};
957
+ const { adapter, botId, userId } = d;
958
+ if (!adapter || !botId || !userId) {
959
+ ws.send(JSON.stringify({ requestId, error: "adapter, botId, userId required" }));
960
+ break;
961
+ }
962
+ const ad = root.inject(adapter);
963
+ const bot = ad?.bots?.get?.(botId);
964
+ if (!bot) {
965
+ ws.send(JSON.stringify({ requestId, error: "bot not found" }));
966
+ break;
967
+ }
968
+ if (adapter === "icqq" && typeof bot.deleteFriend === "function") {
969
+ await bot.deleteFriend(Number(userId));
970
+ ws.send(JSON.stringify({ requestId, data: { success: true } }));
971
+ } else if (adapter === "icqq" && typeof bot.delete_friend === "function") {
972
+ await bot.delete_friend(Number(userId));
973
+ ws.send(JSON.stringify({ requestId, data: { success: true } }));
974
+ } else {
975
+ ws.send(JSON.stringify({ requestId, error: "\u5F53\u524D\u9002\u914D\u5668\u6682\u4E0D\u652F\u6301\u5220\u9664\u597D\u53CB" }));
976
+ }
977
+ } catch (error) {
978
+ ws.send(JSON.stringify({ requestId, error: error.message }));
979
+ }
980
+ break;
981
+ }
982
+ case "bot:requests": {
983
+ try {
984
+ const d = message.data || {};
985
+ const { adapter, botId } = d;
986
+ if (!adapter || !botId) {
987
+ ws.send(JSON.stringify({ requestId, error: "adapter and botId required" }));
988
+ break;
989
+ }
990
+ const rows = await listRequestsForBot(String(adapter), String(botId));
991
+ ws.send(
992
+ JSON.stringify({
993
+ requestId,
994
+ data: {
995
+ requests: rows.map((r) => ({
996
+ id: r.id,
997
+ platformRequestId: r.platform_request_id,
998
+ type: r.type,
999
+ sender: { id: r.sender_id, name: r.sender_name },
1000
+ comment: r.comment,
1001
+ channel: { id: r.channel_id, type: r.channel_type },
1002
+ timestamp: r.created_at
1003
+ }))
1004
+ }
1005
+ })
1006
+ );
1007
+ } catch (error) {
1008
+ ws.send(JSON.stringify({ requestId, error: error.message }));
1009
+ }
1010
+ break;
1011
+ }
1012
+ case "bot:requestApprove":
1013
+ case "bot:requestReject": {
1014
+ try {
1015
+ const d = message.data || {};
1016
+ const { adapter, botId, requestId: platformReqId, remark, reason } = d;
1017
+ if (!adapter || !botId || !platformReqId) {
1018
+ ws.send(
1019
+ JSON.stringify({
1020
+ requestId,
1021
+ error: "adapter, botId, requestId required"
1022
+ })
1023
+ );
1024
+ break;
1025
+ }
1026
+ const req = getPendingRequest(String(adapter), String(botId), String(platformReqId));
1027
+ if (!req) {
1028
+ ws.send(
1029
+ JSON.stringify({
1030
+ requestId,
1031
+ error: "request not in memory (restart?) \u2014 use bot:requestConsumed to dismiss"
1032
+ })
1033
+ );
1034
+ break;
1035
+ }
1036
+ if (type === "bot:requestApprove") await req.$approve(remark);
1037
+ else await req.$reject(reason);
1038
+ await markRequestConsumedByPlatformId(String(adapter), String(botId), String(platformReqId));
1039
+ ws.send(JSON.stringify({ requestId, data: { success: true } }));
1040
+ } catch (error) {
1041
+ ws.send(JSON.stringify({ requestId, error: error.message }));
1042
+ }
1043
+ break;
1044
+ }
1045
+ case "bot:requestConsumed": {
1046
+ try {
1047
+ const d = message.data || {};
1048
+ const ids = d.ids ?? (d.id != null ? [d.id] : []);
1049
+ if (!Array.isArray(ids) || !ids.length) {
1050
+ ws.send(JSON.stringify({ requestId, error: "id or ids required" }));
1051
+ break;
1052
+ }
1053
+ const numIds = ids.map(Number);
1054
+ for (const id of numIds) {
1055
+ const row = await getRequestRowById(id);
1056
+ if (row && row.consumed === 0) {
1057
+ removePendingRequest(row.adapter, row.bot_id, row.platform_request_id);
1058
+ }
1059
+ }
1060
+ await markRequestsConsumed(numIds);
1061
+ ws.send(JSON.stringify({ requestId, data: { success: true } }));
1062
+ } catch (error) {
1063
+ ws.send(JSON.stringify({ requestId, error: error.message }));
1064
+ }
1065
+ break;
1066
+ }
1067
+ case "bot:noticeConsumed": {
1068
+ try {
1069
+ const d = message.data || {};
1070
+ const ids = d.ids ?? (d.id != null ? [d.id] : []);
1071
+ if (!Array.isArray(ids) || !ids.length) {
1072
+ ws.send(JSON.stringify({ requestId, error: "id or ids required" }));
1073
+ break;
1074
+ }
1075
+ await markNoticesConsumed(ids.map(Number));
1076
+ ws.send(JSON.stringify({ requestId, data: { success: true } }));
1077
+ } catch (error) {
1078
+ ws.send(JSON.stringify({ requestId, error: error.message }));
1079
+ }
1080
+ break;
1081
+ }
1082
+ case "bot:inboxMessages": {
1083
+ try {
1084
+ const d = message.data || {};
1085
+ const { adapter, botId, channelId, channelType, limit = 50, beforeId, beforeTs } = d;
1086
+ if (!adapter || !botId || !channelId || !channelType) {
1087
+ ws.send(JSON.stringify({ requestId, error: "adapter, botId, channelId, channelType required" }));
1088
+ break;
1089
+ }
1090
+ let db;
1091
+ try {
1092
+ db = root.inject("database");
1093
+ } catch {
1094
+ ws.send(JSON.stringify({ requestId, data: { messages: [], inboxEnabled: false } }));
1095
+ break;
1096
+ }
1097
+ const MessageModel = db?.models?.get("unified_inbox_message");
1098
+ if (!MessageModel) {
1099
+ ws.send(JSON.stringify({ requestId, data: { messages: [], inboxEnabled: false } }));
1100
+ break;
1101
+ }
1102
+ const where = {
1103
+ adapter: String(adapter),
1104
+ bot_id: String(botId),
1105
+ channel_id: String(channelId),
1106
+ channel_type: String(channelType)
1107
+ };
1108
+ if (beforeTs != null) where.created_at = { $lt: Number(beforeTs) };
1109
+ if (beforeId != null) where.id = { $lt: Number(beforeId) };
1110
+ let q = MessageModel.select().where(where).orderBy("created_at", "DESC").limit(Math.min(Number(limit) || 50, 100));
1111
+ const rows = await (typeof q.then === "function" ? q : Promise.resolve(q));
1112
+ const messages = (rows || []).map((r) => ({
1113
+ id: r.id,
1114
+ platform_message_id: r.platform_message_id,
1115
+ sender_id: r.sender_id,
1116
+ sender_name: r.sender_name,
1117
+ content: r.content,
1118
+ raw: r.raw,
1119
+ created_at: r.created_at
1120
+ }));
1121
+ ws.send(JSON.stringify({ requestId, data: { messages, inboxEnabled: true } }));
1122
+ } catch (error) {
1123
+ ws.send(JSON.stringify({ requestId, error: error.message }));
1124
+ }
1125
+ break;
1126
+ }
1127
+ case "bot:inboxRequests": {
1128
+ try {
1129
+ const d = message.data || {};
1130
+ const { adapter, botId, limit = 30, offset = 0 } = d;
1131
+ if (!adapter || !botId) {
1132
+ ws.send(JSON.stringify({ requestId, error: "adapter and botId required" }));
1133
+ break;
1134
+ }
1135
+ let db;
1136
+ try {
1137
+ db = root.inject("database");
1138
+ } catch {
1139
+ ws.send(JSON.stringify({ requestId, data: { requests: [], inboxEnabled: false } }));
1140
+ break;
1141
+ }
1142
+ const RequestModel = db?.models?.get("unified_inbox_request");
1143
+ if (!RequestModel) {
1144
+ ws.send(JSON.stringify({ requestId, data: { requests: [], inboxEnabled: false } }));
1145
+ break;
1146
+ }
1147
+ const where = { adapter: String(adapter), bot_id: String(botId) };
1148
+ const limitNum = Math.min(Number(limit) || 30, 100);
1149
+ const offsetNum = Math.max(0, Number(offset) || 0);
1150
+ let q = RequestModel.select().where(where).orderBy("created_at", "DESC").limit(limitNum).offset(offsetNum);
1151
+ const rows = await (typeof q.then === "function" ? q : Promise.resolve(q));
1152
+ const requests = (rows || []).map((r) => ({
1153
+ id: r.id,
1154
+ platform_request_id: r.platform_request_id,
1155
+ type: r.type,
1156
+ sub_type: r.sub_type,
1157
+ channel_id: r.channel_id,
1158
+ channel_type: r.channel_type,
1159
+ sender_id: r.sender_id,
1160
+ sender_name: r.sender_name,
1161
+ comment: r.comment,
1162
+ created_at: r.created_at,
1163
+ resolved: r.resolved,
1164
+ resolved_at: r.resolved_at
1165
+ }));
1166
+ ws.send(JSON.stringify({ requestId, data: { requests, inboxEnabled: true } }));
1167
+ } catch (error) {
1168
+ ws.send(JSON.stringify({ requestId, error: error.message }));
1169
+ }
1170
+ break;
1171
+ }
1172
+ case "bot:inboxNotices": {
1173
+ try {
1174
+ const d = message.data || {};
1175
+ const { adapter, botId, limit = 30, offset = 0 } = d;
1176
+ if (!adapter || !botId) {
1177
+ ws.send(JSON.stringify({ requestId, error: "adapter and botId required" }));
1178
+ break;
1179
+ }
1180
+ let db;
1181
+ try {
1182
+ db = root.inject("database");
1183
+ } catch {
1184
+ ws.send(JSON.stringify({ requestId, data: { notices: [], inboxEnabled: false } }));
1185
+ break;
1186
+ }
1187
+ const NoticeModel = db?.models?.get("unified_inbox_notice");
1188
+ if (!NoticeModel) {
1189
+ ws.send(JSON.stringify({ requestId, data: { notices: [], inboxEnabled: false } }));
1190
+ break;
1191
+ }
1192
+ const where = { adapter: String(adapter), bot_id: String(botId) };
1193
+ const limitNum = Math.min(Number(limit) || 30, 100);
1194
+ const offsetNum = Math.max(0, Number(offset) || 0);
1195
+ let q = NoticeModel.select().where(where).orderBy("created_at", "DESC").limit(limitNum).offset(offsetNum);
1196
+ const rows = await (typeof q.then === "function" ? q : Promise.resolve(q));
1197
+ const notices = (rows || []).map((r) => ({
1198
+ id: r.id,
1199
+ platform_notice_id: r.platform_notice_id,
1200
+ type: r.type,
1201
+ sub_type: r.sub_type,
1202
+ channel_id: r.channel_id,
1203
+ channel_type: r.channel_type,
1204
+ operator_id: r.operator_id,
1205
+ operator_name: r.operator_name,
1206
+ target_id: r.target_id,
1207
+ target_name: r.target_name,
1208
+ payload: r.payload,
1209
+ created_at: r.created_at
1210
+ }));
1211
+ ws.send(JSON.stringify({ requestId, data: { notices, inboxEnabled: true } }));
1212
+ } catch (error) {
1213
+ ws.send(JSON.stringify({ requestId, error: error.message }));
1214
+ }
1215
+ break;
1216
+ }
1217
+ case "bot:groupMembers":
1218
+ case "bot:groupKick":
1219
+ case "bot:groupMute":
1220
+ case "bot:groupAdmin": {
1221
+ try {
1222
+ const d = message.data || {};
1223
+ const { adapter, botId, groupId, userId, duration, enable } = d;
1224
+ if (!adapter || !botId || !groupId) {
1225
+ ws.send(
1226
+ JSON.stringify({ requestId, error: "adapter, botId, groupId required" })
1227
+ );
1228
+ break;
1229
+ }
1230
+ const ad = root.inject(adapter);
1231
+ if (!ad) {
1232
+ ws.send(JSON.stringify({ requestId, error: "adapter not found" }));
1233
+ break;
1234
+ }
1235
+ const gid = String(groupId);
1236
+ if (type === "bot:groupMembers") {
1237
+ if (typeof ad.listMembers !== "function") {
1238
+ ws.send(JSON.stringify({ requestId, error: "adapter does not support listMembers" }));
1239
+ break;
1240
+ }
1241
+ const r = await ad.listMembers(botId, gid);
1242
+ ws.send(JSON.stringify({ requestId, data: r }));
1243
+ } else if (type === "bot:groupKick") {
1244
+ if (!userId) {
1245
+ ws.send(JSON.stringify({ requestId, error: "userId required" }));
1246
+ break;
1247
+ }
1248
+ if (typeof ad.kickMember !== "function") {
1249
+ ws.send(JSON.stringify({ requestId, error: "adapter does not support kickMember" }));
1250
+ break;
1251
+ }
1252
+ await ad.kickMember(botId, gid, String(userId));
1253
+ ws.send(JSON.stringify({ requestId, data: { success: true } }));
1254
+ } else if (type === "bot:groupMute") {
1255
+ if (!userId) {
1256
+ ws.send(JSON.stringify({ requestId, error: "userId required" }));
1257
+ break;
1258
+ }
1259
+ if (typeof ad.muteMember !== "function") {
1260
+ ws.send(JSON.stringify({ requestId, error: "adapter does not support muteMember" }));
1261
+ break;
1262
+ }
1263
+ await ad.muteMember(botId, gid, String(userId), duration ?? 600);
1264
+ ws.send(JSON.stringify({ requestId, data: { success: true } }));
1265
+ } else {
1266
+ if (!userId) {
1267
+ ws.send(JSON.stringify({ requestId, error: "userId required" }));
1268
+ break;
1269
+ }
1270
+ if (typeof ad.setAdmin !== "function") {
1271
+ ws.send(JSON.stringify({ requestId, error: "adapter does not support setAdmin" }));
1272
+ break;
1273
+ }
1274
+ await ad.setAdmin(botId, gid, String(userId), enable !== false);
1275
+ ws.send(JSON.stringify({ requestId, data: { success: true } }));
1276
+ }
1277
+ } catch (error) {
1278
+ ws.send(JSON.stringify({ requestId, error: error.message }));
1279
+ }
1280
+ break;
1281
+ }
465
1282
  default:
466
1283
  ws.send(JSON.stringify({ requestId, error: `Unknown message type: ${type}` }));
467
1284
  }
@@ -623,13 +1440,13 @@ async function kvGetEntries(table) {
623
1440
  }
624
1441
  function buildFileTree(cwd, relativePath, allowed) {
625
1442
  const tree = [];
626
- path.resolve(cwd, relativePath);
1443
+ path2.resolve(cwd, relativePath);
627
1444
  for (const entry of allowed) {
628
1445
  const entryRelative = entry;
629
1446
  if (entryRelative.includes("/")) continue;
630
- const absPath = path.resolve(cwd, entry);
631
- if (!fs.existsSync(absPath)) continue;
632
- const stat = fs.statSync(absPath);
1447
+ const absPath = path2.resolve(cwd, entry);
1448
+ if (!fs2.existsSync(absPath)) continue;
1449
+ const stat = fs2.statSync(absPath);
633
1450
  if (stat.isDirectory()) {
634
1451
  tree.push({
635
1452
  name: entryRelative,
@@ -648,9 +1465,9 @@ function buildFileTree(cwd, relativePath, allowed) {
648
1465
  }
649
1466
  function buildDirectoryTree(cwd, relativePath, maxDepth) {
650
1467
  if (maxDepth <= 0) return [];
651
- const absDir = path.resolve(cwd, relativePath);
652
- if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) return [];
653
- const entries = fs.readdirSync(absDir, { withFileTypes: true });
1468
+ const absDir = path2.resolve(cwd, relativePath);
1469
+ if (!fs2.existsSync(absDir) || !fs2.statSync(absDir).isDirectory()) return [];
1470
+ const entries = fs2.readdirSync(absDir, { withFileTypes: true });
654
1471
  const result = [];
655
1472
  for (const entry of entries) {
656
1473
  if (FILE_MANAGER_BLOCKED.has(entry.name) || entry.name.startsWith(".")) continue;