chainlesschain 0.45.5 → 0.45.6
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/package.json +1 -1
- package/src/commands/ui.js +6 -1
- package/src/lib/web-ui-server.js +118 -3
package/package.json
CHANGED
package/src/commands/ui.js
CHANGED
|
@@ -47,6 +47,10 @@ export function registerUiCommand(program) {
|
|
|
47
47
|
"--token <token>",
|
|
48
48
|
"Authentication token for WebSocket (recommended for security)",
|
|
49
49
|
)
|
|
50
|
+
.option(
|
|
51
|
+
"--web-panel-dir <dir>",
|
|
52
|
+
"Path to built web-panel dist/ directory (auto-detected by default)",
|
|
53
|
+
)
|
|
50
54
|
.action(async (opts) => {
|
|
51
55
|
const httpPort = parseInt(opts.port, 10);
|
|
52
56
|
const wsPort = parseInt(opts.wsPort, 10);
|
|
@@ -114,6 +118,7 @@ export function registerUiCommand(program) {
|
|
|
114
118
|
projectRoot,
|
|
115
119
|
projectName,
|
|
116
120
|
mode,
|
|
121
|
+
staticDir: opts.webPanelDir || null,
|
|
117
122
|
});
|
|
118
123
|
|
|
119
124
|
try {
|
|
@@ -130,7 +135,7 @@ export function registerUiCommand(program) {
|
|
|
130
135
|
const uiUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${httpPort}`;
|
|
131
136
|
|
|
132
137
|
logger.log("");
|
|
133
|
-
logger.log(chalk.bold(" ChainlessChain
|
|
138
|
+
logger.log(chalk.bold(" ChainlessChain 管理面板"));
|
|
134
139
|
logger.log("");
|
|
135
140
|
if (mode === "project") {
|
|
136
141
|
logger.log(
|
package/src/lib/web-ui-server.js
CHANGED
|
@@ -9,6 +9,28 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import http from "http";
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
// MIME type map for static file serving
|
|
19
|
+
const MIME_TYPES = {
|
|
20
|
+
".html": "text/html; charset=utf-8",
|
|
21
|
+
".js": "application/javascript; charset=utf-8",
|
|
22
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
23
|
+
".css": "text/css; charset=utf-8",
|
|
24
|
+
".json": "application/json; charset=utf-8",
|
|
25
|
+
".svg": "image/svg+xml",
|
|
26
|
+
".png": "image/png",
|
|
27
|
+
".jpg": "image/jpeg",
|
|
28
|
+
".ico": "image/x-icon",
|
|
29
|
+
".woff": "font/woff",
|
|
30
|
+
".woff2": "font/woff2",
|
|
31
|
+
".ttf": "font/ttf",
|
|
32
|
+
".map": "application/json",
|
|
33
|
+
};
|
|
12
34
|
|
|
13
35
|
/**
|
|
14
36
|
* Build the full HTML page with runtime config injected.
|
|
@@ -1121,9 +1143,45 @@ function escapeHtml(str) {
|
|
|
1121
1143
|
.replace(/"/g, """);
|
|
1122
1144
|
}
|
|
1123
1145
|
|
|
1146
|
+
/**
|
|
1147
|
+
* Build the runtime config JSON string, safe for embedding in a <script> tag.
|
|
1148
|
+
*/
|
|
1149
|
+
function buildConfigJson(opts) {
|
|
1150
|
+
return JSON.stringify({
|
|
1151
|
+
wsPort: opts.wsPort,
|
|
1152
|
+
wsToken: opts.wsToken,
|
|
1153
|
+
wsHost: opts.wsHost,
|
|
1154
|
+
projectRoot: opts.projectRoot,
|
|
1155
|
+
projectName: opts.projectName,
|
|
1156
|
+
mode: opts.mode,
|
|
1157
|
+
})
|
|
1158
|
+
.replace(/</g, "\\u003c")
|
|
1159
|
+
.replace(/>/g, "\\u003e")
|
|
1160
|
+
.replace(/&/g, "\\u0026");
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Try to locate the built web-panel dist directory.
|
|
1165
|
+
* Returns the absolute path if found, or null.
|
|
1166
|
+
*/
|
|
1167
|
+
function findWebPanelDist(staticDir) {
|
|
1168
|
+
if (staticDir) {
|
|
1169
|
+
return fs.existsSync(path.join(staticDir, "index.html")) ? staticDir : null;
|
|
1170
|
+
}
|
|
1171
|
+
// Default: packages/web-panel/dist/ relative to this file's location
|
|
1172
|
+
const defaultDist = path.resolve(__dirname, "../../web-panel/dist");
|
|
1173
|
+
return fs.existsSync(path.join(defaultDist, "index.html"))
|
|
1174
|
+
? defaultDist
|
|
1175
|
+
: null;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1124
1178
|
/**
|
|
1125
1179
|
* Create and return a Node.js HTTP server that serves the Web UI.
|
|
1126
1180
|
*
|
|
1181
|
+
* When packages/web-panel/dist/ is present (built Vue3 app), it is served as
|
|
1182
|
+
* a SPA with the runtime config injected into index.html.
|
|
1183
|
+
* Otherwise falls back to the embedded single-page HTML.
|
|
1184
|
+
*
|
|
1127
1185
|
* @param {object} opts
|
|
1128
1186
|
* @param {number} opts.wsPort
|
|
1129
1187
|
* @param {string|null} opts.wsToken
|
|
@@ -1131,13 +1189,71 @@ function escapeHtml(str) {
|
|
|
1131
1189
|
* @param {string|null} opts.projectRoot
|
|
1132
1190
|
* @param {string|null} opts.projectName
|
|
1133
1191
|
* @param {"project"|"global"} opts.mode
|
|
1192
|
+
* @param {string|null} [opts.staticDir] - Optional override for dist directory
|
|
1134
1193
|
* @returns {import("http").Server}
|
|
1135
1194
|
*/
|
|
1136
1195
|
export function createWebUIServer(opts) {
|
|
1137
|
-
const
|
|
1196
|
+
const distDir = findWebPanelDist(opts.staticDir || null);
|
|
1197
|
+
const configJson = buildConfigJson(opts);
|
|
1198
|
+
|
|
1199
|
+
if (distDir) {
|
|
1200
|
+
// ── Serve built Vue3 web panel ──────────────────────────────────────────
|
|
1201
|
+
return http.createServer((req, res) => {
|
|
1202
|
+
if (req.method !== "GET") {
|
|
1203
|
+
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
1204
|
+
res.end("Method Not Allowed");
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const urlPath = req.url.split("?")[0];
|
|
1209
|
+
|
|
1210
|
+
// Resolve requested file path (prevent path traversal)
|
|
1211
|
+
const safePath = path.normalize(urlPath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
1212
|
+
const filePath = path.join(distDir, safePath);
|
|
1213
|
+
|
|
1214
|
+
// Serve static assets (js, css, fonts, etc.)
|
|
1215
|
+
if (urlPath !== "/" && urlPath !== "/index.html") {
|
|
1216
|
+
try {
|
|
1217
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
1218
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1219
|
+
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
1220
|
+
const isAsset = urlPath.startsWith("/assets/");
|
|
1221
|
+
res.writeHead(200, {
|
|
1222
|
+
"Content-Type": mime,
|
|
1223
|
+
"Cache-Control": isAsset
|
|
1224
|
+
? "public, max-age=31536000, immutable"
|
|
1225
|
+
: "no-store",
|
|
1226
|
+
"X-Content-Type-Options": "nosniff",
|
|
1227
|
+
});
|
|
1228
|
+
res.end(fs.readFileSync(filePath));
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
} catch (_) {
|
|
1232
|
+
// Fall through to SPA index
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1138
1235
|
|
|
1236
|
+
// SPA fallback: serve index.html with injected config
|
|
1237
|
+
try {
|
|
1238
|
+
let html = fs.readFileSync(path.join(distDir, "index.html"), "utf-8");
|
|
1239
|
+
// Replace the placeholder with actual runtime config
|
|
1240
|
+
html = html.replace("__CC_CONFIG_PLACEHOLDER__", configJson);
|
|
1241
|
+
res.writeHead(200, {
|
|
1242
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1243
|
+
"Cache-Control": "no-store",
|
|
1244
|
+
"X-Content-Type-Options": "nosniff",
|
|
1245
|
+
});
|
|
1246
|
+
res.end(html, "utf-8");
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1249
|
+
res.end(`Failed to read index.html: ${err.message}`);
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// ── Fallback: embedded classic single-page HTML ─────────────────────────
|
|
1255
|
+
const html = buildHtml(opts);
|
|
1139
1256
|
return http.createServer((req, res) => {
|
|
1140
|
-
// Only serve GET / and GET /index.html (strip query string for comparison)
|
|
1141
1257
|
const urlPath = req.url.split("?")[0];
|
|
1142
1258
|
if (
|
|
1143
1259
|
req.method !== "GET" ||
|
|
@@ -1147,7 +1263,6 @@ export function createWebUIServer(opts) {
|
|
|
1147
1263
|
res.end("Not Found");
|
|
1148
1264
|
return;
|
|
1149
1265
|
}
|
|
1150
|
-
|
|
1151
1266
|
res.writeHead(200, {
|
|
1152
1267
|
"Content-Type": "text/html; charset=utf-8",
|
|
1153
1268
|
"Cache-Control": "no-store",
|