@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +19 -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 +4262 -1
  45. package/dist/index.html +2 -2
  46. package/dist/radix-ui.js +1261 -1262
  47. package/dist/react-dom-client.js +2243 -2240
  48. package/dist/react-dom.js +15 -15
  49. package/dist/style.css +1 -3
  50. package/lib/index.js +1010 -81
  51. package/lib/transform.js +16 -2
  52. package/lib/websocket.js +845 -28
  53. package/node.tsconfig.json +18 -0
  54. package/package.json +15 -16
  55. package/src/bin.ts +24 -0
  56. package/src/bot-db-models.ts +74 -0
  57. package/src/bot-hub.ts +240 -0
  58. package/src/bot-persistence.ts +270 -0
  59. package/src/build.ts +90 -0
  60. package/src/dev.ts +107 -0
  61. package/src/index.ts +337 -0
  62. package/src/transform.ts +199 -0
  63. package/src/websocket.ts +1369 -0
  64. package/client/src/pages/database.tsx +0 -708
  65. package/client/src/pages/files.tsx +0 -470
  66. package/client/src/pages/login-assist.tsx +0 -225
  67. package/dist/index.js +0 -124
package/src/build.ts ADDED
@@ -0,0 +1,90 @@
1
+ import * as vite from 'vite'
2
+ import { existsSync, promises as fs } from 'fs'
3
+ import {join} from 'path'
4
+ import react from '@vitejs/plugin-react'
5
+ import tailwindcss from "@tailwindcss/vite";
6
+ export async function build(root: string, config: vite.UserConfig = {}) {
7
+ if (!existsSync(root + '/client')) return
8
+
9
+ const outDir = root + '/dist'
10
+ if (existsSync(outDir)) {
11
+ await fs.rm(outDir, { recursive: true })
12
+ }
13
+ const maybeFiles=[
14
+ join(root, 'client', 'index.tsx'),
15
+ join(root, 'client', 'index.ts'),
16
+ join(root, 'client', 'index.js'),
17
+ join(root, 'client', 'index.jsx'),
18
+ ]
19
+ const entry = maybeFiles.find(file => existsSync(file))
20
+ if (!entry) {
21
+ throw new Error('No entry file found')
22
+ }
23
+ await fs.mkdir(root + '/dist', { recursive: true })
24
+
25
+ const results = await vite.build(vite.mergeConfig({
26
+ root,
27
+ build: {
28
+ write: false,
29
+ outDir: 'dist',
30
+ assetsDir: '',
31
+ minify: true,
32
+ emptyOutDir: true,
33
+ commonjsOptions: {
34
+ strictRequires: true,
35
+ },
36
+ lib: {
37
+ entry,
38
+ fileName: 'index',
39
+ formats: ['es'],
40
+ },
41
+ rollupOptions: {
42
+ makeAbsoluteExternalsRelative: true,
43
+ external: [
44
+ 'react',
45
+ 'react-dom',
46
+ 'react/jsx-runtime',
47
+ 'react/jsx-dev-runtime',
48
+ 'radix-ui',
49
+ 'class-variance-authority',
50
+ 'lucide-react',
51
+ "@zhin.js/client",
52
+ ],
53
+ resolve: {
54
+ alias: {
55
+ 'react/jsx-runtime': root + '/react-jsx-runtime.js',
56
+ 'react/jsx-dev-runtime': root + '/react-jsx-dev-runtime.js',
57
+ 'react': root + '/react.js',
58
+ 'react-dom': root + '/react-dom.js',
59
+ 'radix-ui': root + '/radix-ui.js',
60
+ 'class-variance-authority': root + '/cva.js',
61
+ },
62
+ },
63
+ output: {
64
+ format: 'iife',
65
+ },
66
+ },
67
+ },
68
+ plugins: [
69
+ react(),
70
+ tailwindcss(),
71
+ ],
72
+ define: {
73
+ 'process.env.NODE_ENV': '"production"',
74
+ },
75
+ } as vite.InlineConfig, config)) as vite.Rollup.RollupOutput[]
76
+
77
+ for (const item of results[0].output) {
78
+ if (item.fileName === 'index.mjs') item.fileName = 'index.js'
79
+ const dest = root + '/dist/' + item.fileName
80
+ if (item.type === 'asset') {
81
+ await fs.writeFile(dest, item.source)
82
+ } else {
83
+ const result = await vite.transformWithEsbuild(item.code, dest, {
84
+ minifyWhitespace: true,
85
+ charset: 'utf8',
86
+ })
87
+ await fs.writeFile(dest, result.code)
88
+ }
89
+ }
90
+ }
package/src/dev.ts ADDED
@@ -0,0 +1,107 @@
1
+ import type { ViteDevServer } from "vite";
2
+ import path from "path";
3
+ import fs from "fs";
4
+
5
+ export interface DevServerOptions {
6
+ /** 客户端代码根目录 */
7
+ root: string;
8
+ /** 基础路径,默认 /vite/ */
9
+ base?: string;
10
+ /** 是否启用 tailwindcss,默认 true */
11
+ enableTailwind?: boolean;
12
+ }
13
+
14
+ /**
15
+ * 创建 Vite 开发服务器
16
+ * @param options 开发服务器选项
17
+ * @returns Vite 开发服务器实例
18
+ */
19
+ export async function createViteDevServer(
20
+ options: DevServerOptions
21
+ ): Promise<ViteDevServer> {
22
+ const { root, base = "/vite/", enableTailwind = true } = options;
23
+
24
+ try {
25
+ // 动态导入所有 Vite 相关依赖(避免提前加载)
26
+ const [
27
+ { createServer, searchForWorkspaceRoot },
28
+ { default: react },
29
+ { default: tailwindcss }
30
+ ] = await Promise.all([
31
+ import('vite'),
32
+ import('@vitejs/plugin-react'),
33
+ import('@tailwindcss/vite')
34
+ ]);
35
+
36
+ const plugins = [react()];
37
+ if (enableTailwind) {
38
+ plugins.push(tailwindcss());
39
+ }
40
+ const clientPath = path.resolve(process.cwd(), 'node_modules/@zhin.js/client/client')
41
+ if (!fs.existsSync(clientPath)) {
42
+ throw new Error('@zhin.js/client not found')
43
+ }
44
+ return await createServer({
45
+ root,
46
+ base,
47
+ plugins: [react(), tailwindcss()],
48
+ server: {
49
+ middlewareMode: true,
50
+ allowedHosts: true,
51
+ fs: {
52
+ strict: false,
53
+ // 添加文件访问过滤,避免访问特殊文件
54
+ allow: [
55
+ // 允许访问的目录
56
+ root,
57
+ searchForWorkspaceRoot(root),
58
+ path.resolve(process.cwd(), 'node_modules'),
59
+ path.resolve(process.cwd(), 'client'),
60
+ path.resolve(process.cwd(), 'src'),
61
+ ],
62
+ // 拒绝访问某些文件模式
63
+ deny: [
64
+ '**/.git/**',
65
+ '**/node_modules/.cache/**',
66
+ '**/*.socket',
67
+ '**/*.pipe',
68
+ '**/Dockerfile*',
69
+ '**/.env*',
70
+ ],
71
+ },
72
+ },
73
+ resolve: {
74
+ dedupe: [
75
+ "react",
76
+ "react-dom",
77
+ "clsx",
78
+ "tailwind-merge",
79
+ "@reduxjs/toolkit",
80
+ "react-router",
81
+ "react-redux",
82
+ "redux-persist",
83
+ ],
84
+ alias: {
85
+ "@zhin.js/client": path.resolve(process.cwd(), "node_modules/@zhin.js/client/client"),
86
+ "@": path.resolve(root, "../client/src"),
87
+ },
88
+ },
89
+ optimizeDeps: {
90
+ include: ["react", "react-dom"],
91
+ },
92
+ build: {
93
+ rollupOptions: {
94
+ input: root + "/index.html",
95
+ },
96
+ },
97
+ });
98
+ } catch (error) {
99
+ throw new Error(
100
+ `Failed to create Vite dev server. ` +
101
+ `Make sure all development dependencies are installed: ` +
102
+ `vite, @vitejs/plugin-react, @tailwindcss/vite. ` +
103
+ `Run: pnpm install --include=optional\n` +
104
+ `Original error: ${(error as Error).message}`
105
+ );
106
+ }
107
+ }
package/src/index.ts ADDED
@@ -0,0 +1,337 @@
1
+ import { usePlugin } from "@zhin.js/core";
2
+ import { WebSocketServer } from "ws";
3
+ import mime from "mime";
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import * as crypto from "crypto";
7
+ import { setupWebSocket, notifyDataUpdate } from "./websocket.js";
8
+ import { transformFile, isTransformable } from "./transform.js";
9
+ import { registerBotModels } from "./bot-db-models.js";
10
+ import { initBotPersistence } from "./bot-persistence.js";
11
+
12
+ export interface ConsoleConfig {
13
+ /** 是否启用控制台插件,默认 true */
14
+ enabled?: boolean;
15
+ /** 端口号(继承自 http 配置) */
16
+ port?: number;
17
+ }
18
+
19
+ export type WebEntry =
20
+ | string
21
+ | {
22
+ production: string;
23
+ development: string;
24
+ };
25
+
26
+ export interface WebServer {
27
+ addEntry(entry: WebEntry): () => void;
28
+ entries: Record<string, string>;
29
+ ws: WebSocketServer;
30
+ }
31
+
32
+ declare module "@zhin.js/core" {
33
+ namespace Plugin {
34
+ interface Contexts {
35
+ web: WebServer;
36
+ router: import("@zhin.js/http").Router;
37
+ }
38
+ }
39
+ }
40
+
41
+ interface WebSocketMessage {
42
+ type: string;
43
+ requestId?: string;
44
+ data?: unknown;
45
+ error?: string;
46
+ }
47
+
48
+ interface SyncMessage {
49
+ type: "sync";
50
+ data: {
51
+ key: string;
52
+ value: unknown;
53
+ };
54
+ }
55
+
56
+ interface AddMessage {
57
+ type: "add";
58
+ data: {
59
+ key: string;
60
+ value: unknown;
61
+ };
62
+ }
63
+
64
+ interface DeleteMessage {
65
+ type: "delete";
66
+ data: {
67
+ key: string;
68
+ value: unknown;
69
+ };
70
+ }
71
+
72
+ const { provide, root, useContext, logger, inject, onDispose } = usePlugin();
73
+
74
+ // 读取配置
75
+ const configService = inject('config');
76
+ const appConfig = (configService?.get('zhin.config.yml') || {}) as any;
77
+ const consoleConfig: ConsoleConfig = appConfig.plugins?.console || {};
78
+ const {
79
+ enabled = true,// 默认不延迟加载,避免 addEntry 等功能不可用
80
+ } = consoleConfig;
81
+
82
+ if (enabled) {
83
+ registerBotModels(root as { defineModel?: (name: string, def: unknown) => void });
84
+ initBotPersistence(root as { inject: (key: string) => unknown });
85
+
86
+ const createSyncMsg = (key: string, value: unknown): SyncMessage => ({
87
+ type: "sync",
88
+ data: { key, value },
89
+ });
90
+
91
+ const createAddMsg = (key: string, value: unknown): AddMessage => ({
92
+ type: "add",
93
+ data: { key, value },
94
+ });
95
+
96
+ const createDeleteMsg = (key: string, value: unknown): DeleteMessage => ({
97
+ type: "delete",
98
+ data: { key, value },
99
+ });
100
+
101
+ useContext("router", async (router) => {
102
+ const base = "/vite/";
103
+
104
+ const isDev = process.env.NODE_ENV === "development";
105
+
106
+ // ── Token 映射:隐藏绝对路径 ──────────────────────────────────────────
107
+ // token → 目录绝对路径。URL 只暴露 token + 相对文件名,不暴露服务器路径。
108
+ const entryBases = new Map<string, string>();
109
+ const genToken = () => crypto.randomBytes(6).toString('hex');
110
+
111
+ // ── 开发模式文件监听(轻量 HMR:文件变更 → 通知客户端刷新) ──────────────
112
+ const watchedDirs = new Set<string>();
113
+ const watchers: fs.FSWatcher[] = [];
114
+ let reloadTimer: ReturnType<typeof setTimeout> | null = null;
115
+
116
+ /** 广播刷新通知给所有 WebSocket 客户端,300ms 防抖 */
117
+ const broadcastReload = (file: string) => {
118
+ if (reloadTimer) clearTimeout(reloadTimer);
119
+ reloadTimer = setTimeout(() => {
120
+ const msg = JSON.stringify({ type: "hmr:reload", data: { file } });
121
+ // webServer.ws 可能还未初始化,延迟访问
122
+ const wss = webServer?.ws;
123
+ if (wss) {
124
+ for (const ws of wss.clients || []) {
125
+ ws.send(msg);
126
+ }
127
+ }
128
+ logger.info(`[HMR] 文件变更: ${file},已通知客户端刷新`);
129
+ }, 300);
130
+ };
131
+
132
+ /** 对一个目录启动 recursive watch,排除 node_modules */
133
+ const watchDir = (dir: string) => {
134
+ if (watchedDirs.has(dir)) return;
135
+ watchedDirs.add(dir);
136
+ try {
137
+ const watcher = fs.watch(dir, { recursive: true }, (event, filename) => {
138
+ if (!filename) return;
139
+ // 排除 node_modules 内的变更
140
+ if (filename.includes("node_modules")) return;
141
+ broadcastReload(filename);
142
+ });
143
+ watcher.on("error", (err) => {
144
+ logger.warn(`[HMR] 文件监听错误 (${dir}):`, (err as Error).message);
145
+ });
146
+ watchers.push(watcher);
147
+ logger.info(`[HMR] 正在监听目录: ${dir}`);
148
+ } catch (err) {
149
+ logger.warn(`[HMR] 无法监听目录 (${dir}):`, (err as Error).message);
150
+ }
151
+ };
152
+
153
+ // ── 双目录解析 ─────────────────────────────────────────────────────────
154
+ // dev → 源码从 client/,vendor 预构建文件从 dist/
155
+ // prod → 全部从 dist/
156
+ const clientDir = path.join(import.meta.dirname, "../client");
157
+ const distDir = path.join(import.meta.dirname, "../dist");
158
+
159
+ /** 解析文件路径:dev 先查 client/,再查 dist/;prod 只查 dist/ */
160
+ const resolveFile = (name: string): string | null => {
161
+ if (isDev) {
162
+ const clientPath = path.resolve(clientDir, name);
163
+ if (fs.existsSync(clientPath)) return clientPath;
164
+ }
165
+ const distPath = path.resolve(distDir, name);
166
+ if (fs.existsSync(distPath)) return distPath;
167
+ return null;
168
+ };
169
+
170
+ // 初始化 WebServer 对象
171
+ const webServer: WebServer = {
172
+ entries: {},
173
+ addEntry(entry) {
174
+ const hash = crypto.randomBytes(8).toString('hex');
175
+ const entryFile =
176
+ typeof entry === "string"
177
+ ? entry
178
+ : isDev
179
+ ? entry.development
180
+ : entry.production;
181
+
182
+ // 统一使用 token URL,dev/prod 都不暴露绝对路径
183
+ const dir = path.dirname(entryFile);
184
+ const filename = path.basename(entryFile);
185
+ let token: string | undefined;
186
+ for (const [t, d] of entryBases) {
187
+ if (d === dir) { token = t; break; }
188
+ }
189
+ if (!token) { token = genToken(); entryBases.set(token, dir); }
190
+ this.entries[hash] = `/vite/@ext/${token}/${filename}`;
191
+
192
+ // 开发模式:监听入口文件所在目录,文件变更时通知客户端刷新
193
+ if (isDev) watchDir(dir);
194
+
195
+ // 延迟访问 ws,确保它已初始化
196
+ if (this.ws) {
197
+ for (const ws of this.ws.clients || []) {
198
+ ws.send(JSON.stringify(createAddMsg("entries", this.entries[hash])));
199
+ }
200
+ }
201
+ return () => {
202
+ if (this.ws) {
203
+ for (const ws of this.ws.clients || []) {
204
+ ws.send(JSON.stringify(createDeleteMsg("entries", this.entries[hash])));
205
+ }
206
+ }
207
+ delete this.entries[hash];
208
+ };
209
+ },
210
+ ws: router.ws("/server"),
211
+ } as WebServer;
212
+
213
+ logger.info(`Web 控制台已启动 (${isDev ? "开发模式, esbuild 按需转译 + 文件监听" : "生产模式, 静态文件 + 按需转译"})`);
214
+
215
+ // SPA 回退路由 - 处理所有未匹配的路由
216
+ router.all("*all", async (ctx, next) => {
217
+ // 跳过 API、/pub 等后端路由,交给其他路由处理器
218
+ if (ctx.path.startsWith("/api/") || ctx.path === "/api" || ctx.path.startsWith("/pub/") || ctx.path === "/pub") {
219
+ return next();
220
+ }
221
+
222
+ // 跳过其他插件注册的路由(如 /mcp),避免 SPA fallback 拦截
223
+ const whiteList: (string | RegExp)[] = (router as any).whiteList || [];
224
+ const isRegistered = whiteList.some(p =>
225
+ typeof p === 'string' && !p.includes('*') && !p.startsWith('/vite') && ctx.path.startsWith(p)
226
+ );
227
+ if (isRegistered) return next();
228
+
229
+ const name = ctx.path.slice(1);
230
+
231
+ const sendFile = async (filename: string) => {
232
+ // 安全检查:确保是常规文件
233
+ try {
234
+ const stat = fs.statSync(filename);
235
+ if (!stat.isFile()) {
236
+ ctx.status = 404;
237
+ return;
238
+ }
239
+ } catch (error) {
240
+ ctx.status = 404;
241
+ return;
242
+ }
243
+
244
+ // TSX/TS/JSX 按需转译(dev 和 prod 都启用)
245
+ if (isTransformable(filename)) {
246
+ try {
247
+ const code = await transformFile(filename);
248
+ ctx.type = "application/javascript";
249
+ ctx.body = code;
250
+ return;
251
+ } catch (e) {
252
+ logger.warn(`转译失败: ${filename}`, (e as Error).message);
253
+ ctx.status = 500;
254
+ ctx.body = `/* transform error: ${(e as Error).message} */`;
255
+ return;
256
+ }
257
+ }
258
+
259
+ ctx.type = path.extname(filename);
260
+ ctx.type = mime.getType(filename) || ctx.type;
261
+ return (ctx.body = fs.createReadStream(filename));
262
+ };
263
+
264
+ // 0. 处理 /vite/@ext/ 路径(token 隐藏真实路径,dev/prod 均启用)
265
+ if (ctx.path.startsWith("/vite/@ext/")) {
266
+ const rest = ctx.path.replace("/vite/@ext/", "");
267
+ const slashIdx = rest.indexOf("/");
268
+ if (slashIdx === -1) { ctx.status = 404; return; }
269
+
270
+ const token = rest.slice(0, slashIdx);
271
+ const relPath = rest.slice(slashIdx + 1);
272
+ const baseDir = entryBases.get(token);
273
+ if (!baseDir) { ctx.status = 404; return; }
274
+
275
+ const fullPath = path.resolve(baseDir, relPath);
276
+ // 安全检查:防止 ../../ 路径穿越
277
+ const safePfx = baseDir.endsWith(path.sep) ? baseDir : baseDir + path.sep;
278
+ if (!fullPath.startsWith(safePfx) && fullPath !== baseDir) { ctx.status = 403; return; }
279
+
280
+ if (fs.existsSync(fullPath)) {
281
+ return sendFile(fullPath);
282
+ }
283
+ ctx.status = 404;
284
+ return;
285
+ }
286
+
287
+ // 1. 静态文件解析(dev: client/ → dist/ 双目录,prod: dist/ 单目录)
288
+ const resolved = resolveFile(name);
289
+ if (resolved) {
290
+ try {
291
+ const fileState = fs.statSync(resolved);
292
+ if (fileState.isFile() && !fileState.isSocket() && !fileState.isFIFO()) {
293
+ return sendFile(resolved);
294
+ }
295
+ } catch (error) {
296
+ logger.warn(`文件访问错误: ${resolved}`, (error as Error).message);
297
+ }
298
+ }
299
+
300
+ // 2. 所有其他路径(包括 SPA 路由)都返回 index.html
301
+ const indexFile = resolveFile("index.html");
302
+ if (indexFile) return sendFile(indexFile);
303
+ ctx.status = 404;
304
+ });
305
+
306
+ // 初始化 WebSocket(触发 getter)
307
+ const _ = webServer.ws;
308
+
309
+ // 定时通知客户端更新数据
310
+ const dataUpdateInterval = setInterval(() => {
311
+ notifyDataUpdate(webServer);
312
+ }, 5000); // 每5秒通知一次更新
313
+
314
+ setupWebSocket(webServer);
315
+
316
+ // 插件卸载时清理定时器和文件监听(使用 onDispose 而不是 process.on,支持热重载)
317
+ onDispose(() => {
318
+ clearInterval(dataUpdateInterval);
319
+ if (reloadTimer) clearTimeout(reloadTimer);
320
+ for (const w of watchers) w.close();
321
+ watchers.length = 0;
322
+ watchedDirs.clear();
323
+ });
324
+
325
+ // 注册 web 上下文
326
+ provide({
327
+ name: "web",
328
+ description: "web服务",
329
+ value: webServer,
330
+ dispose(server) {
331
+ return new Promise<void>((resolve) => {
332
+ server.ws.close(() => resolve());
333
+ });
334
+ },
335
+ });
336
+ });
337
+ }