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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chainlesschain",
3
- "version": "0.45.5",
3
+ "version": "0.45.6",
4
4
  "description": "CLI for ChainlessChain - install, configure, and manage your personal AI management system",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 Web UI"));
138
+ logger.log(chalk.bold(" ChainlessChain 管理面板"));
134
139
  logger.log("");
135
140
  if (mode === "project") {
136
141
  logger.log(
@@ -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, "&quot;");
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 html = buildHtml(opts);
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",