@zhin.js/console 1.0.50 → 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.
- package/CHANGELOG.md +19 -0
- package/README.md +22 -0
- package/browser.tsconfig.json +19 -0
- package/client/src/components/PageHeader.tsx +26 -0
- package/client/src/components/ui/accordion.tsx +2 -1
- package/client/src/components/ui/badge.tsx +1 -3
- package/client/src/components/ui/scroll-area.tsx +5 -2
- package/client/src/components/ui/select.tsx +7 -3
- package/client/src/components/ui/separator.tsx +5 -2
- package/client/src/components/ui/tabs.tsx +4 -2
- package/client/src/layouts/dashboard.tsx +223 -121
- package/client/src/main.tsx +34 -34
- package/client/src/pages/bot-detail/MessageBody.tsx +110 -0
- package/client/src/pages/bot-detail/date-utils.ts +8 -0
- package/client/src/pages/bot-detail/index.tsx +798 -0
- package/client/src/pages/bot-detail/types.ts +92 -0
- package/client/src/pages/bot-detail/useBotConsole.tsx +600 -0
- package/client/src/pages/bots.tsx +111 -73
- package/client/src/pages/database/constants.ts +16 -0
- package/client/src/pages/database/database-page.tsx +170 -0
- package/client/src/pages/database/document-collection-view.tsx +155 -0
- package/client/src/pages/database/index.tsx +1 -0
- package/client/src/pages/database/json-field.tsx +11 -0
- package/client/src/pages/database/kv-bucket-view.tsx +169 -0
- package/client/src/pages/database/related-table-view.tsx +221 -0
- package/client/src/pages/env.tsx +38 -28
- package/client/src/pages/files/code-editor.tsx +85 -0
- package/client/src/pages/files/editor-constants.ts +9 -0
- package/client/src/pages/files/file-editor.tsx +133 -0
- package/client/src/pages/files/file-icons.tsx +25 -0
- package/client/src/pages/files/files-page.tsx +92 -0
- package/client/src/pages/files/hljs-global.d.ts +10 -0
- package/client/src/pages/files/index.tsx +1 -0
- package/client/src/pages/files/language.ts +18 -0
- package/client/src/pages/files/tree-node.tsx +69 -0
- package/client/src/pages/files/use-hljs-theme.ts +23 -0
- package/client/src/pages/logs.tsx +77 -22
- package/client/src/style.css +144 -0
- package/client/src/utils/parseComposerContent.ts +57 -0
- package/client/tailwind.config.js +1 -0
- package/client/tsconfig.json +3 -1
- package/dist/assets/index-COKXlFo2.js +124 -0
- package/dist/assets/style-kkLO-vsa.css +3 -0
- package/dist/client.js +4262 -1
- package/dist/index.html +2 -2
- package/dist/radix-ui.js +1261 -1262
- package/dist/react-dom-client.js +2243 -2240
- package/dist/react-dom.js +15 -15
- package/dist/style.css +1 -3
- package/lib/index.js +1010 -81
- package/lib/transform.js +16 -2
- package/lib/websocket.js +845 -28
- package/node.tsconfig.json +18 -0
- package/package.json +15 -16
- package/src/bin.ts +24 -0
- package/src/bot-db-models.ts +74 -0
- package/src/bot-hub.ts +240 -0
- package/src/bot-persistence.ts +270 -0
- package/src/build.ts +90 -0
- package/src/dev.ts +107 -0
- package/src/index.ts +337 -0
- package/src/transform.ts +199 -0
- package/src/websocket.ts +1369 -0
- package/client/src/pages/database.tsx +0 -708
- package/client/src/pages/files.tsx +0 -470
- package/client/src/pages/login-assist.tsx +0 -225
- package/dist/index.js +0 -124
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"display": "Zhin Console — 服务端 / Node 插件「src/」推荐基线",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"target": "ES2022",
|
|
6
|
+
"module": "NodeNext",
|
|
7
|
+
"moduleResolution": "nodenext",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/console",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.52",
|
|
4
4
|
"description": "Web console service for Zhin.js with real-time monitoring",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -35,12 +35,7 @@
|
|
|
35
35
|
"development": "./src/index.ts",
|
|
36
36
|
"import": "./lib/index.js"
|
|
37
37
|
},
|
|
38
|
-
"./
|
|
39
|
-
"types": "./lib/client/index.d.ts",
|
|
40
|
-
"development": "./client/index.tsx",
|
|
41
|
-
"import": "./lib/client/index.js"
|
|
42
|
-
},
|
|
43
|
-
"./browser.tsconfig.json": "./broswer.tsconfig.json",
|
|
38
|
+
"./browser.tsconfig.json": "./browser.tsconfig.json",
|
|
44
39
|
"./node.tsconfig.json": "./node.tsconfig.json"
|
|
45
40
|
},
|
|
46
41
|
"dependencies": {
|
|
@@ -66,23 +61,27 @@
|
|
|
66
61
|
"tailwind-merge": "^3.3.1",
|
|
67
62
|
"tailwindcss": "latest",
|
|
68
63
|
"tsup": "^8.5.1",
|
|
69
|
-
"
|
|
64
|
+
"rolldown": "^1.0.0-rc.9",
|
|
65
|
+
"vite": "8.0.0",
|
|
70
66
|
"yaml": "^2.8.2",
|
|
71
|
-
"zhin.js": "1.0.
|
|
67
|
+
"zhin.js": "1.0.52"
|
|
72
68
|
},
|
|
73
69
|
"peerDependencies": {
|
|
74
70
|
"@types/ws": "^8.18.1",
|
|
75
|
-
"@zhin.js/client": "^1.0.
|
|
76
|
-
"@zhin.js/core": "^1.0.
|
|
77
|
-
"@zhin.js/http": "^1.0.
|
|
78
|
-
"zhin.js": "1.0.
|
|
71
|
+
"@zhin.js/client": "^1.0.13",
|
|
72
|
+
"@zhin.js/core": "^1.0.52",
|
|
73
|
+
"@zhin.js/http": "^1.0.46",
|
|
74
|
+
"zhin.js": "1.0.52"
|
|
79
75
|
},
|
|
80
76
|
"files": [
|
|
77
|
+
"src",
|
|
81
78
|
"lib",
|
|
82
|
-
"node",
|
|
83
|
-
"README.md",
|
|
84
|
-
"dist",
|
|
85
79
|
"client",
|
|
80
|
+
"dist",
|
|
81
|
+
"skills",
|
|
82
|
+
"README.md",
|
|
83
|
+
"browser.tsconfig.json",
|
|
84
|
+
"node.tsconfig.json",
|
|
86
85
|
"CHANGELOG.md"
|
|
87
86
|
],
|
|
88
87
|
"optionalDependencies": {
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { build } from "./build.js";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const command = args[0];
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
try {
|
|
11
|
+
switch (command) {
|
|
12
|
+
case "build":
|
|
13
|
+
// 构建当前目录的插件客户端代码
|
|
14
|
+
console.log("🔨 Building plugin client...");
|
|
15
|
+
await build(process.cwd());
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error("❌ Build failed:", error);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
main();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 控制台机器人请求/通知的数据库表定义,供 zhin 内置数据库使用
|
|
3
|
+
*/
|
|
4
|
+
import type { Definition } from "@zhin.js/core";
|
|
5
|
+
|
|
6
|
+
export interface ConsoleBotRequestRow {
|
|
7
|
+
id?: number;
|
|
8
|
+
adapter: string;
|
|
9
|
+
bot_id: string;
|
|
10
|
+
platform_request_id: string;
|
|
11
|
+
type: string;
|
|
12
|
+
sender_id: string;
|
|
13
|
+
sender_name: string;
|
|
14
|
+
comment: string;
|
|
15
|
+
channel_id: string;
|
|
16
|
+
channel_type: string;
|
|
17
|
+
created_at: number;
|
|
18
|
+
consumed: number;
|
|
19
|
+
consumed_at?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ConsoleBotNoticeRow {
|
|
23
|
+
id?: number;
|
|
24
|
+
adapter: string;
|
|
25
|
+
bot_id: string;
|
|
26
|
+
notice_type: string;
|
|
27
|
+
channel_type: string;
|
|
28
|
+
channel_id: string;
|
|
29
|
+
payload: string;
|
|
30
|
+
created_at: number;
|
|
31
|
+
consumed: number;
|
|
32
|
+
consumed_at?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const ConsoleBotRequestDefinition: Definition<ConsoleBotRequestRow> = {
|
|
36
|
+
id: { type: "integer", primary: true, autoIncrement: true },
|
|
37
|
+
adapter: { type: "text", nullable: false },
|
|
38
|
+
bot_id: { type: "text", nullable: false },
|
|
39
|
+
platform_request_id: { type: "text", nullable: false },
|
|
40
|
+
type: { type: "text", nullable: false },
|
|
41
|
+
sender_id: { type: "text", nullable: false },
|
|
42
|
+
sender_name: { type: "text", nullable: false },
|
|
43
|
+
comment: { type: "text", nullable: false },
|
|
44
|
+
channel_id: { type: "text", nullable: false },
|
|
45
|
+
channel_type: { type: "text", nullable: false },
|
|
46
|
+
created_at: { type: "integer", nullable: false },
|
|
47
|
+
consumed: { type: "integer", nullable: false, default: 0 },
|
|
48
|
+
consumed_at: { type: "integer", nullable: true },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const ConsoleBotNoticeDefinition: Definition<ConsoleBotNoticeRow> = {
|
|
52
|
+
id: { type: "integer", primary: true, autoIncrement: true },
|
|
53
|
+
adapter: { type: "text", nullable: false },
|
|
54
|
+
bot_id: { type: "text", nullable: false },
|
|
55
|
+
notice_type: { type: "text", nullable: false },
|
|
56
|
+
channel_type: { type: "text", nullable: false },
|
|
57
|
+
channel_id: { type: "text", nullable: false },
|
|
58
|
+
payload: { type: "text", nullable: false },
|
|
59
|
+
created_at: { type: "integer", nullable: false },
|
|
60
|
+
consumed: { type: "integer", nullable: false, default: 0 },
|
|
61
|
+
consumed_at: { type: "integer", nullable: true },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const TABLE_REQUESTS = "console_bot_requests";
|
|
65
|
+
const TABLE_NOTICES = "console_bot_notices";
|
|
66
|
+
|
|
67
|
+
export function registerBotModels(root: { defineModel?: (name: string, def: Definition<unknown>) => void }) {
|
|
68
|
+
const defineModel = root.defineModel;
|
|
69
|
+
if (typeof defineModel !== "function") return;
|
|
70
|
+
defineModel(TABLE_REQUESTS, ConsoleBotRequestDefinition as Definition<unknown>);
|
|
71
|
+
defineModel(TABLE_NOTICES, ConsoleBotNoticeDefinition as Definition<unknown>);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { TABLE_REQUESTS, TABLE_NOTICES };
|
package/src/bot-hub.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 机器人请求/通知:内存 Request 映射 + WS 广播 + 事件注册
|
|
3
|
+
*/
|
|
4
|
+
import type WebSocket from "ws";
|
|
5
|
+
import type { Request as ZhinRequest } from "@zhin.js/core";
|
|
6
|
+
import {
|
|
7
|
+
insertRequest,
|
|
8
|
+
insertNotice,
|
|
9
|
+
listUnconsumedRequests,
|
|
10
|
+
listUnconsumedNotices,
|
|
11
|
+
markRequestsConsumed,
|
|
12
|
+
findRequestRow,
|
|
13
|
+
type StoredRequestRow,
|
|
14
|
+
} from "./bot-persistence.js";
|
|
15
|
+
|
|
16
|
+
type WsIterable = { clients?: Set<WebSocket> | WebSocket[] };
|
|
17
|
+
|
|
18
|
+
let wssRef: WsIterable | null = null;
|
|
19
|
+
let hubInited = false;
|
|
20
|
+
|
|
21
|
+
export function setBotHubWss(wss: WsIterable) {
|
|
22
|
+
wssRef = wss;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function broadcast(obj: object) {
|
|
26
|
+
const msg = JSON.stringify(obj);
|
|
27
|
+
const clients = wssRef?.clients;
|
|
28
|
+
if (!clients) return;
|
|
29
|
+
const list = clients instanceof Set ? [...clients] : clients;
|
|
30
|
+
for (const ws of list) {
|
|
31
|
+
if (ws.readyState === 1) ws.send(msg);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requestMemoryKey(adapter: string, botId: string, platformId: string) {
|
|
36
|
+
return `${adapter}:${botId}:${platformId}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const pendingRequestObjects = new Map<string, ZhinRequest>();
|
|
40
|
+
|
|
41
|
+
export async function storePendingRequest(
|
|
42
|
+
adapter: string,
|
|
43
|
+
botId: string,
|
|
44
|
+
req: ZhinRequest
|
|
45
|
+
): Promise<StoredRequestRow> {
|
|
46
|
+
const platformId = req.$id;
|
|
47
|
+
const key = requestMemoryKey(adapter, botId, platformId);
|
|
48
|
+
pendingRequestObjects.set(key, req);
|
|
49
|
+
const row = await insertRequest({
|
|
50
|
+
adapter,
|
|
51
|
+
bot_id: botId,
|
|
52
|
+
platform_request_id: platformId,
|
|
53
|
+
type: String(req.$type),
|
|
54
|
+
sender_id: String(req.$sender?.id ?? ""),
|
|
55
|
+
sender_name: String(req.$sender?.name ?? ""),
|
|
56
|
+
comment: String(req.$comment ?? ""),
|
|
57
|
+
channel_id: String(req.$channel?.id ?? ""),
|
|
58
|
+
channel_type: String(req.$channel?.type ?? "private"),
|
|
59
|
+
created_at: typeof req.$timestamp === "number" ? req.$timestamp : Date.now(),
|
|
60
|
+
});
|
|
61
|
+
return row;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function rowToRequestPushData(row: StoredRequestRow, canAct: boolean) {
|
|
65
|
+
return {
|
|
66
|
+
id: row.id,
|
|
67
|
+
adapter: row.adapter,
|
|
68
|
+
botId: row.bot_id,
|
|
69
|
+
platformRequestId: row.platform_request_id,
|
|
70
|
+
type: row.type,
|
|
71
|
+
sender: { id: row.sender_id, name: row.sender_name },
|
|
72
|
+
comment: row.comment,
|
|
73
|
+
channel: { id: row.channel_id, type: row.channel_type },
|
|
74
|
+
timestamp: row.created_at,
|
|
75
|
+
canAct,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function onRequestReceived(adapter: string, botId: string, req: ZhinRequest) {
|
|
80
|
+
const row = await storePendingRequest(adapter, botId, req);
|
|
81
|
+
const key = requestMemoryKey(adapter, botId, req.$id);
|
|
82
|
+
const canAct = pendingRequestObjects.has(key);
|
|
83
|
+
broadcast({ type: "bot:request", data: rowToRequestPushData(row, canAct) });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function onNoticeReceived(adapter: string, botId: string, notice: any) {
|
|
87
|
+
let raw: Record<string, unknown> = {};
|
|
88
|
+
try {
|
|
89
|
+
raw = Object.fromEntries(
|
|
90
|
+
Object.entries(notice).filter(
|
|
91
|
+
([k]) => !k.startsWith("$") && k !== "adapter" && k !== "bot"
|
|
92
|
+
)
|
|
93
|
+
) as Record<string, unknown>;
|
|
94
|
+
} catch {
|
|
95
|
+
raw = {};
|
|
96
|
+
}
|
|
97
|
+
let payload: string;
|
|
98
|
+
try {
|
|
99
|
+
payload = JSON.stringify({
|
|
100
|
+
type: notice.$type,
|
|
101
|
+
subType: notice.$subType,
|
|
102
|
+
channel: notice.$channel,
|
|
103
|
+
raw,
|
|
104
|
+
});
|
|
105
|
+
} catch {
|
|
106
|
+
payload = JSON.stringify({ type: notice.$type, error: "serialize_failed" });
|
|
107
|
+
}
|
|
108
|
+
const row = await insertNotice({
|
|
109
|
+
adapter,
|
|
110
|
+
bot_id: botId,
|
|
111
|
+
notice_type: String(notice.$type ?? "unknown"),
|
|
112
|
+
channel_type: String(notice.$channel?.type ?? ""),
|
|
113
|
+
channel_id: String(notice.$channel?.id ?? ""),
|
|
114
|
+
payload,
|
|
115
|
+
created_at: typeof notice.$timestamp === "number" ? notice.$timestamp : Date.now(),
|
|
116
|
+
});
|
|
117
|
+
broadcast({
|
|
118
|
+
type: "bot:notice",
|
|
119
|
+
data: {
|
|
120
|
+
id: row.id,
|
|
121
|
+
adapter: row.adapter,
|
|
122
|
+
botId: row.bot_id,
|
|
123
|
+
noticeType: row.notice_type,
|
|
124
|
+
channel: { id: row.channel_id, type: row.channel_type },
|
|
125
|
+
payload: row.payload,
|
|
126
|
+
timestamp: row.created_at,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getPendingRequest(
|
|
132
|
+
adapter: string,
|
|
133
|
+
botId: string,
|
|
134
|
+
platformRequestId: string
|
|
135
|
+
): ZhinRequest | undefined {
|
|
136
|
+
return pendingRequestObjects.get(requestMemoryKey(adapter, botId, platformRequestId));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function removePendingRequest(adapter: string, botId: string, platformRequestId: string) {
|
|
140
|
+
pendingRequestObjects.delete(requestMemoryKey(adapter, botId, platformRequestId));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function markRequestConsumedByPlatformId(
|
|
144
|
+
adapter: string,
|
|
145
|
+
botId: string,
|
|
146
|
+
platformRequestId: string
|
|
147
|
+
) {
|
|
148
|
+
const row = await findRequestRow(adapter, botId, platformRequestId);
|
|
149
|
+
if (row) await markRequestsConsumed([row.id]);
|
|
150
|
+
removePendingRequest(adapter, botId, platformRequestId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function sendCatchUpToClient(ws: WebSocket) {
|
|
154
|
+
const reqs = await listUnconsumedRequests();
|
|
155
|
+
for (const row of reqs) {
|
|
156
|
+
const canAct = pendingRequestObjects.has(
|
|
157
|
+
requestMemoryKey(row.adapter, row.bot_id, row.platform_request_id)
|
|
158
|
+
);
|
|
159
|
+
if (ws.readyState === 1) {
|
|
160
|
+
ws.send(
|
|
161
|
+
JSON.stringify({
|
|
162
|
+
type: "bot:request",
|
|
163
|
+
data: rowToRequestPushData(row, canAct),
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const notices = await listUnconsumedNotices();
|
|
169
|
+
for (const row of notices) {
|
|
170
|
+
if (ws.readyState === 1) {
|
|
171
|
+
ws.send(
|
|
172
|
+
JSON.stringify({
|
|
173
|
+
type: "bot:notice",
|
|
174
|
+
data: {
|
|
175
|
+
id: row.id,
|
|
176
|
+
adapter: row.adapter,
|
|
177
|
+
botId: row.bot_id,
|
|
178
|
+
noticeType: row.notice_type,
|
|
179
|
+
channel: { id: row.channel_id, type: row.channel_type },
|
|
180
|
+
payload: row.payload,
|
|
181
|
+
timestamp: row.created_at,
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function initBotHub(root: {
|
|
190
|
+
on: (ev: string, fn: (...a: any[]) => void) => void;
|
|
191
|
+
adapters?: Iterable<string>;
|
|
192
|
+
inject?: (key: string) => unknown;
|
|
193
|
+
}) {
|
|
194
|
+
if (hubInited) return;
|
|
195
|
+
hubInited = true;
|
|
196
|
+
|
|
197
|
+
const handlerReq = (req: ZhinRequest) => {
|
|
198
|
+
const adapter = String(req.$adapter);
|
|
199
|
+
const botId = String(req.$bot);
|
|
200
|
+
onRequestReceived(adapter, botId, req);
|
|
201
|
+
};
|
|
202
|
+
const handlerNotice = (notice: any) => {
|
|
203
|
+
const adapter = String(notice.$adapter);
|
|
204
|
+
const botId = String(notice.$bot);
|
|
205
|
+
onNoticeReceived(adapter, botId, notice);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
root.on("request.receive", handlerReq);
|
|
209
|
+
root.on("notice.receive", handlerNotice);
|
|
210
|
+
|
|
211
|
+
// 收消息推送:向控制台广播机器人收到的消息,供「收消息展示」使用
|
|
212
|
+
const adapterNames = root.adapters ? [...(root.adapters as Iterable<string>)] : [];
|
|
213
|
+
const inject = root.inject;
|
|
214
|
+
if (inject && typeof inject === "function" && adapterNames.length > 0) {
|
|
215
|
+
for (const name of adapterNames) {
|
|
216
|
+
try {
|
|
217
|
+
const ad = inject(name) as { on?: (ev: string, fn: (...a: any[]) => void) => void } | null;
|
|
218
|
+
if (ad && typeof ad.on === "function") {
|
|
219
|
+
ad.on("message.receive", (msg: any) => {
|
|
220
|
+
const payload = {
|
|
221
|
+
type: "bot:message",
|
|
222
|
+
data: {
|
|
223
|
+
adapter: name,
|
|
224
|
+
botId: msg?.$bot,
|
|
225
|
+
channelId: msg?.$channel?.id,
|
|
226
|
+
channelType: msg?.$channel?.type,
|
|
227
|
+
sender: msg?.$sender,
|
|
228
|
+
content: msg?.$content ?? [],
|
|
229
|
+
timestamp: typeof msg?.$timestamp === "number" ? msg.$timestamp : Date.now(),
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
broadcast(payload);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
// 忽略单个适配器注册失败
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 控制台机器人请求/通知持久化
|
|
3
|
+
* 优先使用 zhin 内置数据库(console_bot_requests / console_bot_notices 表),
|
|
4
|
+
* 无数据库时回退到 JSON 文件存储。
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import type { DatabaseFeature } from "@zhin.js/core";
|
|
9
|
+
import { TABLE_REQUESTS, TABLE_NOTICES } from "./bot-db-models.js";
|
|
10
|
+
|
|
11
|
+
export interface StoredRequestRow {
|
|
12
|
+
id: number;
|
|
13
|
+
adapter: string;
|
|
14
|
+
bot_id: string;
|
|
15
|
+
platform_request_id: string;
|
|
16
|
+
type: string;
|
|
17
|
+
sender_id: string;
|
|
18
|
+
sender_name: string;
|
|
19
|
+
comment: string;
|
|
20
|
+
channel_id: string;
|
|
21
|
+
channel_type: string;
|
|
22
|
+
created_at: number;
|
|
23
|
+
consumed: 0 | 1;
|
|
24
|
+
consumed_at?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface StoredNoticeRow {
|
|
28
|
+
id: number;
|
|
29
|
+
adapter: string;
|
|
30
|
+
bot_id: string;
|
|
31
|
+
notice_type: string;
|
|
32
|
+
channel_type: string;
|
|
33
|
+
channel_id: string;
|
|
34
|
+
payload: string;
|
|
35
|
+
created_at: number;
|
|
36
|
+
consumed: 0 | 1;
|
|
37
|
+
consumed_at?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface StoreFile<T> {
|
|
41
|
+
nextId: number;
|
|
42
|
+
rows: T[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DATA_DIR = path.join(process.cwd(), "data");
|
|
46
|
+
const REQ_FILE = path.join(DATA_DIR, "console_bot_requests.json");
|
|
47
|
+
const NOTICE_FILE = path.join(DATA_DIR, "console_bot_notices.json");
|
|
48
|
+
|
|
49
|
+
let dbRef: DatabaseFeature | null = null;
|
|
50
|
+
|
|
51
|
+
export function initBotPersistence(root: { inject: (key: string) => unknown }) {
|
|
52
|
+
try {
|
|
53
|
+
dbRef = root.inject("database") as DatabaseFeature | null;
|
|
54
|
+
} catch {
|
|
55
|
+
dbRef = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getReqModel() {
|
|
60
|
+
return dbRef?.db?.models?.get(TABLE_REQUESTS) as
|
|
61
|
+
| {
|
|
62
|
+
create: (row: Record<string, unknown>) => Promise<{ id: number }>;
|
|
63
|
+
select: () => { where: (q: Record<string, unknown>) => Promise<StoredRequestRow[]> };
|
|
64
|
+
update: (row: Record<string, unknown>) => { where: (q: Record<string, unknown>) => Promise<unknown> };
|
|
65
|
+
}
|
|
66
|
+
| undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getNoticeModel() {
|
|
70
|
+
return dbRef?.db?.models?.get(TABLE_NOTICES) as
|
|
71
|
+
| {
|
|
72
|
+
create: (row: Record<string, unknown>) => Promise<{ id: number }>;
|
|
73
|
+
select: () => { where: (q: Record<string, unknown>) => Promise<StoredNoticeRow[]> };
|
|
74
|
+
update: (row: Record<string, unknown>) => { where: (q: Record<string, unknown>) => Promise<unknown> };
|
|
75
|
+
}
|
|
76
|
+
| undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --------------- 文件回退 ---------------
|
|
80
|
+
function ensureDir() {
|
|
81
|
+
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadFile<T>(file: string, empty: StoreFile<T>): StoreFile<T> {
|
|
85
|
+
try {
|
|
86
|
+
if (!fs.existsSync(file)) return { ...empty };
|
|
87
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
88
|
+
const j = JSON.parse(raw) as StoreFile<T>;
|
|
89
|
+
if (!j || !Array.isArray(j.rows)) return { ...empty };
|
|
90
|
+
return {
|
|
91
|
+
nextId: typeof j.nextId === "number" ? j.nextId : 1,
|
|
92
|
+
rows: j.rows,
|
|
93
|
+
};
|
|
94
|
+
} catch {
|
|
95
|
+
return { ...empty };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function saveFile<T>(file: string, store: StoreFile<T>) {
|
|
100
|
+
ensureDir();
|
|
101
|
+
fs.writeFileSync(file, JSON.stringify(store, null, 0), "utf-8");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeReqRow(r: StoredRequestRow & { consumed?: number }): StoredRequestRow {
|
|
105
|
+
return {
|
|
106
|
+
...r,
|
|
107
|
+
id: r.id!,
|
|
108
|
+
consumed: (r.consumed === 1 ? 1 : 0) as 0 | 1,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeNoticeRow(r: StoredNoticeRow & { consumed?: number }): StoredNoticeRow {
|
|
113
|
+
return {
|
|
114
|
+
...r,
|
|
115
|
+
id: r.id!,
|
|
116
|
+
consumed: (r.consumed === 1 ? 1 : 0) as 0 | 1,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --------------- 请求 ---------------
|
|
121
|
+
export async function insertRequest(
|
|
122
|
+
row: Omit<StoredRequestRow, "id" | "consumed" | "consumed_at">
|
|
123
|
+
): Promise<StoredRequestRow> {
|
|
124
|
+
const model = getReqModel();
|
|
125
|
+
if (model) {
|
|
126
|
+
const created = await model.create({
|
|
127
|
+
...row,
|
|
128
|
+
consumed: 0,
|
|
129
|
+
});
|
|
130
|
+
const id = typeof created?.id === "number" ? created.id : (created as unknown as { id: number }).id;
|
|
131
|
+
return { ...row, id, consumed: 0 };
|
|
132
|
+
}
|
|
133
|
+
const store = loadFile<StoredRequestRow>(REQ_FILE, { nextId: 1, rows: [] });
|
|
134
|
+
const id = store.nextId++;
|
|
135
|
+
const full: StoredRequestRow = { ...row, id, consumed: 0 };
|
|
136
|
+
store.rows.push(full);
|
|
137
|
+
saveFile(REQ_FILE, store);
|
|
138
|
+
return full;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function insertNotice(
|
|
142
|
+
row: Omit<StoredNoticeRow, "id" | "consumed" | "consumed_at">
|
|
143
|
+
): Promise<StoredNoticeRow> {
|
|
144
|
+
const model = getNoticeModel();
|
|
145
|
+
if (model) {
|
|
146
|
+
const created = await model.create({
|
|
147
|
+
...row,
|
|
148
|
+
consumed: 0,
|
|
149
|
+
});
|
|
150
|
+
const id = typeof created?.id === "number" ? created.id : (created as unknown as { id: number }).id;
|
|
151
|
+
return { ...row, id, consumed: 0 };
|
|
152
|
+
}
|
|
153
|
+
const store = loadFile<StoredNoticeRow>(NOTICE_FILE, { nextId: 1, rows: [] });
|
|
154
|
+
const id = store.nextId++;
|
|
155
|
+
const full: StoredNoticeRow = { ...row, id, consumed: 0 };
|
|
156
|
+
store.rows.push(full);
|
|
157
|
+
saveFile(NOTICE_FILE, store);
|
|
158
|
+
return full;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function listUnconsumedRequests(): Promise<StoredRequestRow[]> {
|
|
162
|
+
const model = getReqModel();
|
|
163
|
+
if (model) {
|
|
164
|
+
const rows = await model.select().where({ consumed: 0 });
|
|
165
|
+
return (rows || []).map(normalizeReqRow).sort((a, b) => a.created_at - b.created_at);
|
|
166
|
+
}
|
|
167
|
+
const store = loadFile<StoredRequestRow>(REQ_FILE, { nextId: 1, rows: [] });
|
|
168
|
+
return store.rows
|
|
169
|
+
.filter((r) => r.consumed === 0)
|
|
170
|
+
.sort((a, b) => a.created_at - b.created_at);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function listUnconsumedNotices(): Promise<StoredNoticeRow[]> {
|
|
174
|
+
const model = getNoticeModel();
|
|
175
|
+
if (model) {
|
|
176
|
+
const rows = await model.select().where({ consumed: 0 });
|
|
177
|
+
return (rows || []).map(normalizeNoticeRow).sort((a, b) => a.created_at - b.created_at);
|
|
178
|
+
}
|
|
179
|
+
const store = loadFile<StoredNoticeRow>(NOTICE_FILE, { nextId: 1, rows: [] });
|
|
180
|
+
return store.rows
|
|
181
|
+
.filter((r) => r.consumed === 0)
|
|
182
|
+
.sort((a, b) => a.created_at - b.created_at);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function listRequestsForBot(
|
|
186
|
+
adapter: string,
|
|
187
|
+
botId: string
|
|
188
|
+
): Promise<StoredRequestRow[]> {
|
|
189
|
+
const all = await listUnconsumedRequests();
|
|
190
|
+
return all.filter((r) => r.adapter === adapter && r.bot_id === botId);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function markRequestsConsumed(ids: number[]): Promise<void> {
|
|
194
|
+
if (!ids.length) return;
|
|
195
|
+
const model = getReqModel();
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
if (model) {
|
|
198
|
+
for (const id of ids) {
|
|
199
|
+
await model.update({ consumed: 1, consumed_at: now }).where({ id });
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const store = loadFile<StoredRequestRow>(REQ_FILE, { nextId: 1, rows: [] });
|
|
204
|
+
const set = new Set(ids);
|
|
205
|
+
for (const r of store.rows) {
|
|
206
|
+
if (set.has(r.id) && r.consumed === 0) {
|
|
207
|
+
r.consumed = 1;
|
|
208
|
+
r.consumed_at = now;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
saveFile(REQ_FILE, store);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function markNoticesConsumed(ids: number[]): Promise<void> {
|
|
215
|
+
if (!ids.length) return;
|
|
216
|
+
const model = getNoticeModel();
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
if (model) {
|
|
219
|
+
for (const id of ids) {
|
|
220
|
+
await model.update({ consumed: 1, consumed_at: now }).where({ id });
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const store = loadFile<StoredNoticeRow>(NOTICE_FILE, { nextId: 1, rows: [] });
|
|
225
|
+
const set = new Set(ids);
|
|
226
|
+
for (const r of store.rows) {
|
|
227
|
+
if (set.has(r.id) && r.consumed === 0) {
|
|
228
|
+
r.consumed = 1;
|
|
229
|
+
r.consumed_at = now;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
saveFile(NOTICE_FILE, store);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function findRequestRow(
|
|
236
|
+
adapter: string,
|
|
237
|
+
botId: string,
|
|
238
|
+
platformRequestId: string
|
|
239
|
+
): Promise<StoredRequestRow | undefined> {
|
|
240
|
+
const model = getReqModel();
|
|
241
|
+
if (model) {
|
|
242
|
+
const rows = await model.select().where({
|
|
243
|
+
adapter,
|
|
244
|
+
bot_id: botId,
|
|
245
|
+
platform_request_id: platformRequestId,
|
|
246
|
+
consumed: 0,
|
|
247
|
+
});
|
|
248
|
+
return rows?.[0] ? normalizeReqRow(rows[0]) : undefined;
|
|
249
|
+
}
|
|
250
|
+
const store = loadFile<StoredRequestRow>(REQ_FILE, { nextId: 1, rows: [] });
|
|
251
|
+
const r = store.rows.find(
|
|
252
|
+
(x) =>
|
|
253
|
+
x.adapter === adapter &&
|
|
254
|
+
x.bot_id === botId &&
|
|
255
|
+
x.platform_request_id === platformRequestId &&
|
|
256
|
+
x.consumed === 0
|
|
257
|
+
);
|
|
258
|
+
return r;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function getRequestRowById(id: number): Promise<StoredRequestRow | undefined> {
|
|
262
|
+
const model = getReqModel();
|
|
263
|
+
if (model) {
|
|
264
|
+
const rows = await model.select().where({ id });
|
|
265
|
+
return rows?.[0] ? normalizeReqRow(rows[0]) : undefined;
|
|
266
|
+
}
|
|
267
|
+
const store = loadFile<StoredRequestRow>(REQ_FILE, { nextId: 1, rows: [] });
|
|
268
|
+
const r = store.rows.find((x) => x.id === id);
|
|
269
|
+
return r;
|
|
270
|
+
}
|