@zhin.js/console 1.0.39 → 1.0.41

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/lib/websocket.js CHANGED
@@ -1,36 +1,41 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
1
3
  import WebSocket from 'ws';
2
4
  import { usePlugin } from '@zhin.js/core';
3
5
 
4
6
  // src/websocket.ts
5
7
  var { root, logger } = usePlugin();
8
+ var ENV_WHITELIST = [".env", ".env.development", ".env.production"];
9
+ function resolveConfigKey(pluginName) {
10
+ const schemaService = root.inject("schema");
11
+ return schemaService?.resolveConfigKey(pluginName) ?? pluginName;
12
+ }
13
+ function getPluginKeys() {
14
+ const schemaService = root.inject("schema");
15
+ if (!schemaService) return [];
16
+ const keys = /* @__PURE__ */ new Set();
17
+ for (const [, configKey] of schemaService.getPluginKeyMap()) {
18
+ keys.add(configKey);
19
+ }
20
+ return Array.from(keys);
21
+ }
22
+ function getConfigFilePath() {
23
+ return path.resolve(process.cwd(), "zhin.config.yml");
24
+ }
6
25
  function setupWebSocket(webServer) {
7
26
  webServer.ws.on("connection", (ws) => {
8
- ws.send(
9
- JSON.stringify({
10
- type: "sync",
11
- data: {
12
- key: "entries",
13
- value: Object.values(webServer.entries)
14
- }
15
- })
16
- );
17
- ws.send(
18
- JSON.stringify({
19
- type: "init-data",
20
- timestamp: Date.now()
21
- })
22
- );
27
+ ws.send(JSON.stringify({
28
+ type: "sync",
29
+ data: { key: "entries", value: Object.values(webServer.entries) }
30
+ }));
31
+ ws.send(JSON.stringify({ type: "init-data", timestamp: Date.now() }));
23
32
  ws.on("message", async (data) => {
24
33
  try {
25
34
  const message = JSON.parse(data.toString());
26
35
  await handleWebSocketMessage(ws, message, webServer);
27
36
  } catch (error) {
28
37
  console.error("WebSocket \u6D88\u606F\u5904\u7406\u9519\u8BEF:", error);
29
- ws.send(
30
- JSON.stringify({
31
- error: "Invalid message format"
32
- })
33
- );
38
+ ws.send(JSON.stringify({ error: "Invalid message format" }));
34
39
  }
35
40
  });
36
41
  ws.on("close", () => {
@@ -47,19 +52,50 @@ async function handleWebSocketMessage(ws, message, webServer) {
47
52
  ws.send(JSON.stringify({ type: "pong", requestId }));
48
53
  break;
49
54
  case "entries:get":
50
- ws.send(
51
- JSON.stringify({
52
- requestId,
53
- data: Object.values(webServer.entries)
54
- })
55
- );
55
+ ws.send(JSON.stringify({ requestId, data: Object.values(webServer.entries) }));
56
+ break;
57
+ // ================================================================
58
+ // 配置文件原始 YAML 读写(用于配置管理页面)
59
+ // ================================================================
60
+ case "config:get-yaml":
61
+ try {
62
+ const filePath = getConfigFilePath();
63
+ const yaml = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : "";
64
+ ws.send(JSON.stringify({ requestId, data: { yaml, pluginKeys: getPluginKeys() } }));
65
+ } catch (error) {
66
+ ws.send(JSON.stringify({ requestId, error: `Failed to read config: ${error.message}` }));
67
+ }
56
68
  break;
69
+ case "config:save-yaml":
70
+ try {
71
+ const { yaml } = message;
72
+ if (typeof yaml !== "string") {
73
+ ws.send(JSON.stringify({ requestId, error: "yaml field is required" }));
74
+ break;
75
+ }
76
+ const filePath = getConfigFilePath();
77
+ fs.writeFileSync(filePath, yaml, "utf-8");
78
+ const configService = root.inject("config");
79
+ const loader = configService.configs.get("zhin.config.yml");
80
+ if (loader) loader.load();
81
+ ws.send(JSON.stringify({ requestId, data: { success: true, message: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u9700\u91CD\u542F\u751F\u6548" } }));
82
+ } catch (error) {
83
+ ws.send(JSON.stringify({ requestId, error: `Failed to save config: ${error.message}` }));
84
+ }
85
+ break;
86
+ // ================================================================
87
+ // 插件配置(供 PluginConfigForm 使用)
88
+ // ================================================================
57
89
  case "config:get":
58
90
  try {
59
91
  const configService = root.inject("config");
60
- const appConfig = configService.get("zhin.config.yml");
61
- const config = pluginName ? appConfig[pluginName] || {} : appConfig;
62
- ws.send(JSON.stringify({ requestId, data: config }));
92
+ const rawConfig = configService.getRaw("zhin.config.yml");
93
+ if (!pluginName) {
94
+ ws.send(JSON.stringify({ requestId, data: rawConfig }));
95
+ } else {
96
+ const configKey = resolveConfigKey(pluginName);
97
+ ws.send(JSON.stringify({ requestId, data: rawConfig[configKey] || {} }));
98
+ }
63
99
  } catch (error) {
64
100
  ws.send(JSON.stringify({ requestId, error: `Failed to get config: ${error.message}` }));
65
101
  }
@@ -67,8 +103,17 @@ async function handleWebSocketMessage(ws, message, webServer) {
67
103
  case "config:get-all":
68
104
  try {
69
105
  const configService = root.inject("config");
70
- const appConfig = configService.get("zhin.config.yml");
71
- ws.send(JSON.stringify({ requestId, data: appConfig }));
106
+ const rawConfig = configService.getRaw("zhin.config.yml");
107
+ const allConfigs = { ...rawConfig };
108
+ const schemaService = root.inject("schema");
109
+ if (schemaService) {
110
+ for (const [pName, configKey] of schemaService.getPluginKeyMap()) {
111
+ if (pName !== configKey) {
112
+ allConfigs[pName] = rawConfig[configKey] || {};
113
+ }
114
+ }
115
+ }
116
+ ws.send(JSON.stringify({ requestId, data: allConfigs }));
72
117
  } catch (error) {
73
118
  ws.send(JSON.stringify({ requestId, error: `Failed to get all configs: ${error.message}` }));
74
119
  }
@@ -80,28 +125,45 @@ async function handleWebSocketMessage(ws, message, webServer) {
80
125
  ws.send(JSON.stringify({ requestId, error: "Plugin name is required" }));
81
126
  break;
82
127
  }
128
+ const configKey = resolveConfigKey(pluginName);
83
129
  const configService = root.inject("config");
84
- const appConfig = configService.get("zhin.config.yml");
85
- appConfig[pluginName] = data;
86
- configService.set("zhin.config.yml", appConfig);
87
- ws.send(JSON.stringify({ requestId, success: true }));
88
- broadcastToAll(webServer, {
89
- type: "config:updated",
90
- data: { pluginName, config: data }
91
- });
130
+ const loader = configService.configs.get("zhin.config.yml");
131
+ if (!loader) {
132
+ ws.send(JSON.stringify({ requestId, error: "Config file not loaded" }));
133
+ break;
134
+ }
135
+ loader.patchKey(configKey, data);
136
+ broadcastToAll(webServer, { type: "config:updated", data: { pluginName, config: data } });
137
+ const schemaService = root.inject("schema");
138
+ const reloadable = schemaService?.isReloadable?.(pluginName) ?? false;
139
+ if (reloadable) {
140
+ const target = findPluginByConfigKey(root, pluginName);
141
+ if (target) {
142
+ try {
143
+ await target.reload();
144
+ ws.send(JSON.stringify({ requestId, data: { success: true, reloaded: true } }));
145
+ } catch (reloadErr) {
146
+ logger.warn(`\u91CD\u8F7D\u63D2\u4EF6 ${pluginName} \u5931\u8D25: ${reloadErr.message}`);
147
+ ws.send(JSON.stringify({ requestId, data: { success: true, reloaded: false, message: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u4F46\u91CD\u8F7D\u5931\u8D25" } }));
148
+ }
149
+ } else {
150
+ ws.send(JSON.stringify({ requestId, data: { success: true, reloaded: false, message: "\u914D\u7F6E\u5DF2\u4FDD\u5B58" } }));
151
+ }
152
+ } else {
153
+ ws.send(JSON.stringify({ requestId, data: { success: true, reloaded: false, message: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u9700\u91CD\u542F\u8FDB\u7A0B\u624D\u80FD\u751F\u6548" } }));
154
+ }
92
155
  } catch (error) {
93
156
  ws.send(JSON.stringify({ requestId, error: `Failed to set config: ${error.message}` }));
94
157
  }
95
158
  break;
159
+ // ================================================================
160
+ // Schema(供 PluginConfigForm 使用)
161
+ // ================================================================
96
162
  case "schema:get":
97
163
  try {
98
164
  const schemaService = root.inject("schema");
99
165
  const schema = pluginName && schemaService ? schemaService.get(pluginName) : null;
100
- if (schema) {
101
- ws.send(JSON.stringify({ requestId, data: schema.toJSON() }));
102
- } else {
103
- ws.send(JSON.stringify({ requestId, data: null }));
104
- }
166
+ ws.send(JSON.stringify({ requestId, data: schema ? schema.toJSON() : null }));
105
167
  } catch (error) {
106
168
  ws.send(JSON.stringify({ requestId, error: `Failed to get schema: ${error.message}` }));
107
169
  }
@@ -111,8 +173,15 @@ async function handleWebSocketMessage(ws, message, webServer) {
111
173
  const schemaService = root.inject("schema");
112
174
  const schemas = {};
113
175
  if (schemaService) {
114
- for (const [name, schema] of schemaService.items.entries()) {
115
- schemas[name] = schema.toJSON();
176
+ for (const record of schemaService.items) {
177
+ if (record.key && record.schema) {
178
+ schemas[record.key] = record.schema.toJSON();
179
+ }
180
+ }
181
+ for (const [pName, configKey] of schemaService.getPluginKeyMap()) {
182
+ if (pName !== configKey && schemas[configKey]) {
183
+ schemas[pName] = schemas[configKey];
184
+ }
116
185
  }
117
186
  }
118
187
  ws.send(JSON.stringify({ requestId, data: schemas }));
@@ -120,14 +189,66 @@ async function handleWebSocketMessage(ws, message, webServer) {
120
189
  ws.send(JSON.stringify({ requestId, error: `Failed to get all schemas: ${error.message}` }));
121
190
  }
122
191
  break;
192
+ // ================================================================
193
+ // 环境变量文件管理
194
+ // ================================================================
195
+ case "env:list":
196
+ try {
197
+ const cwd = process.cwd();
198
+ const files = ENV_WHITELIST.map((name) => ({
199
+ name,
200
+ exists: fs.existsSync(path.resolve(cwd, name))
201
+ }));
202
+ ws.send(JSON.stringify({ requestId, data: { files } }));
203
+ } catch (error) {
204
+ ws.send(JSON.stringify({ requestId, error: `Failed to list env files: ${error.message}` }));
205
+ }
206
+ break;
207
+ case "env:get":
208
+ try {
209
+ const { filename } = message;
210
+ if (!filename || !ENV_WHITELIST.includes(filename)) {
211
+ ws.send(JSON.stringify({ requestId, error: `Invalid env file: ${filename}` }));
212
+ break;
213
+ }
214
+ const envPath = path.resolve(process.cwd(), filename);
215
+ const content = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8") : "";
216
+ ws.send(JSON.stringify({ requestId, data: { content } }));
217
+ } catch (error) {
218
+ ws.send(JSON.stringify({ requestId, error: `Failed to read env file: ${error.message}` }));
219
+ }
220
+ break;
221
+ case "env:save":
222
+ try {
223
+ const { filename, content } = message;
224
+ if (!filename || !ENV_WHITELIST.includes(filename)) {
225
+ ws.send(JSON.stringify({ requestId, error: `Invalid env file: ${filename}` }));
226
+ break;
227
+ }
228
+ if (typeof content !== "string") {
229
+ ws.send(JSON.stringify({ requestId, error: "content field is required" }));
230
+ break;
231
+ }
232
+ const envPath = path.resolve(process.cwd(), filename);
233
+ fs.writeFileSync(envPath, content, "utf-8");
234
+ ws.send(JSON.stringify({ requestId, data: { success: true, message: "\u73AF\u5883\u53D8\u91CF\u5DF2\u4FDD\u5B58\uFF0C\u9700\u91CD\u542F\u751F\u6548" } }));
235
+ } catch (error) {
236
+ ws.send(JSON.stringify({ requestId, error: `Failed to save env file: ${error.message}` }));
237
+ }
238
+ break;
123
239
  default:
124
- ws.send(
125
- JSON.stringify({
126
- requestId,
127
- error: `Unknown message type: ${type}`
128
- })
129
- );
240
+ ws.send(JSON.stringify({ requestId, error: `Unknown message type: ${type}` }));
241
+ }
242
+ }
243
+ function findPluginByConfigKey(rootPlugin, configKey) {
244
+ for (const child of rootPlugin.children) {
245
+ if (child.name === configKey || child.name.endsWith(`-${configKey}`) || child.name.includes(configKey)) {
246
+ return child;
247
+ }
248
+ const found = findPluginByConfigKey(child, configKey);
249
+ if (found) return found;
130
250
  }
251
+ return null;
131
252
  }
132
253
  function broadcastToAll(webServer, message) {
133
254
  for (const ws of webServer.ws.clients || []) {
@@ -137,10 +258,7 @@ function broadcastToAll(webServer, message) {
137
258
  }
138
259
  }
139
260
  function notifyDataUpdate(webServer) {
140
- broadcastToAll(webServer, {
141
- type: "data-update",
142
- timestamp: Date.now()
143
- });
261
+ broadcastToAll(webServer, { type: "data-update", timestamp: Date.now() });
144
262
  }
145
263
 
146
264
  export { broadcastToAll, notifyDataUpdate, setupWebSocket };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/console",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "description": "Web console service for Zhin.js with real-time monitoring",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -67,14 +67,15 @@
67
67
  "tailwindcss": "latest",
68
68
  "tsup": "^8.5.1",
69
69
  "vite": "^7.0.6",
70
- "zhin.js": "1.0.42"
70
+ "yaml": "^2.8.2",
71
+ "zhin.js": "1.0.43"
71
72
  },
72
73
  "peerDependencies": {
73
74
  "@types/ws": "^8.18.1",
74
- "@zhin.js/client": "^1.0.10",
75
- "@zhin.js/http": "^1.0.36",
76
- "zhin.js": "1.0.42",
77
- "@zhin.js/core": "^1.0.42"
75
+ "@zhin.js/client": "^1.0.11",
76
+ "@zhin.js/core": "^1.0.43",
77
+ "zhin.js": "1.0.43",
78
+ "@zhin.js/http": "^1.0.37"
78
79
  },
79
80
  "files": [
80
81
  "lib",