var __glob = (map) => (path21) => { var fn = map[path21]; if (fn) return fn(); throw new Error("Module not found in bundle: " + path21); }; // api/app.ts import express15 from "express"; import cors from "cors"; import dotenv from "dotenv"; // api/core/files/routes.ts import express from "express"; import fs from "fs/promises"; import path3 from "path"; // api/utils/asyncHandler.ts var asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; // api/utils/response.ts var successResponse = (res, data, statusCode = 200) => { const response = { success: true, data }; res.status(statusCode).json(response); }; // api/utils/pathSafety.ts import path2 from "path"; // api/config/index.ts import path from "path"; import { fileURLToPath } from "url"; import os from "os"; var __filename = fileURLToPath(import.meta.url); var __dirname = path.dirname(__filename); var config = { get projectRoot() { if (__dirname.includes("app.asar")) { return path.resolve(__dirname, "..").replace("app.asar", "app.asar.unpacked"); } return path.resolve(__dirname, "../../"); }, get notebookRoot() { return process.env.NOTEBOOK_ROOT ? path.resolve(process.env.NOTEBOOK_ROOT) : path.join(this.projectRoot, "notebook"); }, get tempRoot() { return path.join(os.tmpdir(), "xcnote_uploads"); }, get serverPort() { return parseInt(process.env.PORT || "3001", 10); }, get isVercel() { return !!process.env.VERCEL; }, get isElectron() { return __dirname.includes("app.asar"); }, get isDev() { return !this.isElectron && !this.isVercel; } }; var PATHS = { get PROJECT_ROOT() { return config.projectRoot; }, get NOTEBOOK_ROOT() { return config.notebookRoot; }, get TEMP_ROOT() { return config.tempRoot; } }; // api/config/paths.ts var PROJECT_ROOT = PATHS.PROJECT_ROOT; var NOTEBOOK_ROOT = PATHS.NOTEBOOK_ROOT; var TEMP_ROOT = PATHS.TEMP_ROOT; // shared/errors/index.ts var AppError = class extends Error { constructor(code, message, statusCode = 500, details) { super(message); this.code = code; this.name = "AppError"; this.statusCode = statusCode; this.details = details; } statusCode; details; toJSON() { return { name: this.name, code: this.code, message: this.message, statusCode: this.statusCode, details: this.details }; } }; var ValidationError = class extends AppError { constructor(message, details) { super("VALIDATION_ERROR", message, 400, details); this.name = "ValidationError"; } }; var NotFoundError = class extends AppError { constructor(message = "Resource not found", details) { super("NOT_FOUND", message, 404, details); this.name = "NotFoundError"; } }; var AccessDeniedError = class extends AppError { constructor(message = "Access denied", details) { super("ACCESS_DENIED", message, 403, details); this.name = "AccessDeniedError"; } }; var BadRequestError = class extends AppError { constructor(message, details) { super("BAD_REQUEST", message, 400, details); this.name = "BadRequestError"; } }; var NotADirectoryError = class extends AppError { constructor(message = "\u4E0D\u662F\u76EE\u5F55", details) { super("NOT_A_DIRECTORY", message, 400, details); this.name = "NotADirectoryError"; } }; var AlreadyExistsError = class extends AppError { constructor(message = "Resource already exists", details) { super("ALREADY_EXISTS", message, 409, details); this.name = "AlreadyExistsError"; } }; var ForbiddenError = class extends AppError { constructor(message = "\u7981\u6B62\u8BBF\u95EE", details) { super("FORBIDDEN", message, 403, details); this.name = "ForbiddenError"; } }; var UnsupportedMediaTypeError = class extends AppError { constructor(message = "\u4E0D\u652F\u6301\u7684\u5A92\u4F53\u7C7B\u578B", details) { super("UNSUPPORTED_MEDIA_TYPE", message, 415, details); this.name = "UnsupportedMediaTypeError"; } }; var ResourceLockedError = class extends AppError { constructor(message = "\u8D44\u6E90\u5DF2\u9501\u5B9A", details) { super("RESOURCE_LOCKED", message, 423, details); this.name = "ResourceLockedError"; } }; var InternalError = class extends AppError { constructor(message = "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF", details) { super("INTERNAL_ERROR", message, 500, details); this.name = "InternalError"; } }; function isAppError(error) { return error instanceof AppError; } function isNodeError(error) { return error instanceof Error && "code" in error; } // api/utils/pathSafety.ts var DANGEROUS_PATTERNS = [ /\.\./, /\0/, /%2e%2e[%/]/i, /%252e%252e[%/]/i, /\.\.%2f/i, /\.\.%5c/i, /%c0%ae/i, /%c1%9c/i, /%c0%ae%c0%ae/i, /%c1%9c%c1%9c/i, /\.\.%c0%af/i, /\.\.%c1%9c/i, /%252e/i, /%uff0e/i, /%u002e/i ]; var DOUBLE_ENCODE_PATTERNS = [ /%25[0-9a-fA-F]{2}/, /%[0-9a-fA-F]{2}%[0-9a-fA-F]{2}/ ]; var normalizeRelPath = (input) => { const trimmed = input.replace(/\0/g, "").trim(); return trimmed.replace(/^[/\\]+/, ""); }; var containsPathTraversal = (input) => { const decoded = decodeURIComponentSafe(input); return DANGEROUS_PATTERNS.some((pattern) => pattern.test(input) || pattern.test(decoded)); }; var containsDoubleEncoding = (input) => { return DOUBLE_ENCODE_PATTERNS.some((pattern) => pattern.test(input)); }; var hasPathSecurityIssues = (input) => { return containsPathTraversal(input) || containsDoubleEncoding(input); }; var decodeURIComponentSafe = (input) => { try { return decodeURIComponent(input); } catch { return input; } }; var resolveNotebookPath = (relPath) => { if (hasPathSecurityIssues(relPath)) { throw new AccessDeniedError("Path traversal detected"); } const safeRelPath = normalizeRelPath(relPath); const notebookRoot = path2.resolve(NOTEBOOK_ROOT); const fullPath = path2.resolve(notebookRoot, safeRelPath); if (!fullPath.startsWith(notebookRoot)) { throw new AccessDeniedError("Access denied"); } return { safeRelPath, fullPath }; }; // shared/utils/path.ts var toPosixPath = (p) => p.replace(/\\/g, "/"); // shared/utils/date.ts var pad2 = (n) => String(n).padStart(2, "0"); var pad3 = (n) => String(n).padStart(3, "0"); var formatTimestamp = (d) => { const yyyy = d.getFullYear(); const mm = pad2(d.getMonth() + 1); const dd = pad2(d.getDate()); const hh = pad2(d.getHours()); const mi = pad2(d.getMinutes()); const ss = pad2(d.getSeconds()); const ms = pad3(d.getMilliseconds()); return `${yyyy}${mm}${dd}_${hh}${mi}${ss}_${ms}`; }; // api/middlewares/validate.ts import { ZodError } from "zod"; var validateBody = (schema) => { return (req, _res, next) => { try { req.body = schema.parse(req.body); next(); } catch (error) { if (error instanceof ZodError) { next(new ValidationError("Request validation failed", { issues: error.issues })); } else { next(error); } } }; }; var validateQuery = (schema) => { return (req, _res, next) => { try { req.query = schema.parse(req.query); next(); } catch (error) { if (error instanceof ZodError) { next(new ValidationError("Query validation failed", { issues: error.issues })); } else { next(error); } } }; }; // api/schemas/files.ts import { z } from "zod"; var listFilesQuerySchema = z.object({ path: z.string().optional().default("") }); var contentQuerySchema = z.object({ path: z.string().min(1) }); var rawQuerySchema = z.object({ path: z.string().min(1) }); var pathSchema = z.object({ path: z.string().min(1) }); var saveFileSchema = z.object({ path: z.string().min(1), content: z.string() }); var renameSchema = z.object({ oldPath: z.string().min(1), newPath: z.string().min(1) }); var searchSchema = z.object({ keywords: z.array(z.string()).min(1) }); var existsSchema = z.object({ path: z.string().min(1) }); var createDirSchema = z.object({ path: z.string().min(1) }); var createFileSchema = z.object({ path: z.string().min(1) }); // api/schemas/pydemos.ts import { z as z2 } from "zod"; var listPyDemosQuerySchema = z2.object({ year: z2.string().optional() }); var createPyDemoSchema = z2.object({ name: z2.string().min(1), year: z2.string().min(1), month: z2.string().min(1), folderStructure: z2.string().optional() }); var deletePyDemoSchema = z2.object({ path: z2.string().min(1) }); var renamePyDemoSchema = z2.object({ oldPath: z2.string().min(1), newName: z2.string().min(1) }); // api/utils/logger.ts var createLogger = () => { const isProd = process.env.NODE_ENV === "production"; const debug = isProd ? () => { } : console.debug.bind(console); const info = console.info.bind(console); const warn = console.warn.bind(console); const error = console.error.bind(console); return { debug, info, warn, error }; }; var logger = createLogger(); // api/core/files/routes.ts var router = express.Router(); router.get( "/", validateQuery(listFilesQuerySchema), asyncHandler(async (req, res) => { const relPath = req.query.path; const { safeRelPath, fullPath } = resolveNotebookPath(relPath); try { await fs.access(fullPath); } catch { throw new NotFoundError("\u8DEF\u5F84\u4E0D\u5B58\u5728"); } const stats = await fs.stat(fullPath); if (!stats.isDirectory()) { throw new NotADirectoryError(); } const files = await fs.readdir(fullPath); const items = await Promise.all( files.map(async (name) => { const filePath = path3.join(fullPath, name); try { const fileStats = await fs.stat(filePath); return { name, type: fileStats.isDirectory() ? "dir" : "file", size: fileStats.size, modified: fileStats.mtime.toISOString(), path: toPosixPath(path3.join(safeRelPath, name)) }; } catch { return null; } }) ); const visibleItems = items.filter((i) => i !== null && !i.name.startsWith(".")); visibleItems.sort((a, b) => { if (a.type === b.type) return a.name.localeCompare(b.name); return a.type === "dir" ? -1 : 1; }); successResponse(res, { items: visibleItems }); }) ); router.get( "/content", validateQuery(contentQuerySchema), asyncHandler(async (req, res) => { const relPath = req.query.path; const { fullPath } = resolveNotebookPath(relPath); const stats = await fs.stat(fullPath).catch(() => { throw new NotFoundError("\u6587\u4EF6\u4E0D\u5B58\u5728"); }); if (!stats.isFile()) throw new BadRequestError("\u4E0D\u662F\u6587\u4EF6"); const content = await fs.readFile(fullPath, "utf-8"); successResponse(res, { content, metadata: { size: stats.size, modified: stats.mtime.toISOString() } }); }) ); router.get( "/raw", validateQuery(rawQuerySchema), asyncHandler(async (req, res) => { const relPath = req.query.path; const { fullPath } = resolveNotebookPath(relPath); const stats = await fs.stat(fullPath).catch(() => { throw new NotFoundError("\u6587\u4EF6\u4E0D\u5B58\u5728"); }); if (!stats.isFile()) throw new BadRequestError("\u4E0D\u662F\u6587\u4EF6"); const ext = path3.extname(fullPath).toLowerCase(); const mimeTypes = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".pdf": "application/pdf" }; const mimeType = mimeTypes[ext]; if (mimeType) res.setHeader("Content-Type", mimeType); res.sendFile(fullPath); }) ); router.post( "/save", validateBody(saveFileSchema), asyncHandler(async (req, res) => { const { path: relPath, content } = req.body; const { fullPath } = resolveNotebookPath(relPath); await fs.mkdir(path3.dirname(fullPath), { recursive: true }); await fs.writeFile(fullPath, content, "utf-8"); successResponse(res, null); }) ); router.delete( "/delete", validateBody(pathSchema), asyncHandler(async (req, res) => { const { path: relPath } = req.body; const { fullPath } = resolveNotebookPath(relPath); await fs.stat(fullPath).catch(() => { throw new NotFoundError("\u6587\u4EF6\u6216\u76EE\u5F55\u4E0D\u5B58\u5728"); }); const { fullPath: rbDir } = resolveNotebookPath("RB"); await fs.mkdir(rbDir, { recursive: true }); const originalName = path3.basename(fullPath); const now = /* @__PURE__ */ new Date(); const year = now.getFullYear(); const month = pad2(now.getMonth() + 1); const day = pad2(now.getDate()); const timestamp = `${year}${month}${day}`; const newName = `${timestamp}_${originalName}`; const rbDestPath = path3.join(rbDir, newName); await fs.rename(fullPath, rbDestPath); successResponse(res, null); }) ); router.post( "/exists", validateBody(pathSchema), asyncHandler(async (req, res) => { const { path: relPath } = req.body; const { fullPath } = resolveNotebookPath(relPath); try { const stats = await fs.stat(fullPath); const type = stats.isDirectory() ? "dir" : stats.isFile() ? "file" : null; successResponse(res, { exists: true, type }); } catch (err) { if (isNodeError(err) && err.code === "ENOENT") { successResponse(res, { exists: false, type: null }); return; } throw err; } }) ); router.post( "/create/dir", validateBody(createDirSchema), asyncHandler(async (req, res) => { const { path: relPath } = req.body; const { fullPath } = resolveNotebookPath(relPath); try { await fs.mkdir(fullPath, { recursive: true }); } catch (err) { if (isNodeError(err)) { if (err.code === "EEXIST") { throw new AlreadyExistsError("\u8DEF\u5F84\u5DF2\u5B58\u5728"); } if (err.code === "EACCES") { throw new ForbiddenError("\u6CA1\u6709\u6743\u9650\u521B\u5EFA\u76EE\u5F55"); } } throw err; } successResponse(res, null); }) ); router.post( "/create/file", validateBody(createFileSchema), asyncHandler(async (req, res) => { const { path: relPath } = req.body; const { fullPath } = resolveNotebookPath(relPath); await fs.mkdir(path3.dirname(fullPath), { recursive: true }); try { const fileName = path3.basename(relPath, ".md"); const content = `# ${fileName}`; await fs.writeFile(fullPath, content, { encoding: "utf-8", flag: "wx" }); } catch (err) { if (isNodeError(err)) { if (err.code === "EEXIST") throw new AlreadyExistsError("\u8DEF\u5F84\u5DF2\u5B58\u5728"); if (err.code === "EACCES") throw new ForbiddenError("\u6CA1\u6709\u6743\u9650\u521B\u5EFA\u6587\u4EF6"); } throw err; } successResponse(res, null); }) ); router.post( "/rename", validateBody(renameSchema), asyncHandler(async (req, res) => { const { oldPath, newPath } = req.body; const { fullPath: oldFullPath } = resolveNotebookPath(oldPath); const { fullPath: newFullPath } = resolveNotebookPath(newPath); await fs.mkdir(path3.dirname(newFullPath), { recursive: true }); try { await fs.rename(oldFullPath, newFullPath); } catch (err) { if (isNodeError(err)) { if (err.code === "ENOENT") { throw new NotFoundError("\u6587\u4EF6\u4E0D\u5B58\u5728"); } if (err.code === "EEXIST") { throw new AlreadyExistsError("\u8DEF\u5F84\u5DF2\u5B58\u5728"); } if (err.code === "EPERM" || err.code === "EACCES") { throw new ForbiddenError("\u6CA1\u6709\u6743\u9650\u91CD\u547D\u540D\u6587\u4EF6\u6216\u76EE\u5F55"); } if (err.code === "EBUSY") { throw new ResourceLockedError("\u6587\u4EF6\u6216\u76EE\u5F55\u6B63\u5728\u4F7F\u7528\u4E2D\u6216\u88AB\u9501\u5B9A"); } } logger.error("\u91CD\u547D\u540D\u9519\u8BEF:", err); throw new InternalError("\u91CD\u547D\u540D\u6587\u4EF6\u6216\u76EE\u5F55\u5931\u8D25"); } successResponse(res, null); }) ); var routes_default = router; // api/core/events/routes.ts import express2 from "express"; // api/events/eventBus.ts var clients = []; var eventBus = { addClient: (res) => { clients.push(res); logger.info(`SSE client connected. Total clients: ${clients.length}`); }, removeClient: (res) => { clients = clients.filter((c) => c !== res); logger.info(`SSE client disconnected. Total clients: ${clients.length}`); }, broadcast: (payload) => { const data = `data: ${JSON.stringify(payload)} `; logger.info(`Broadcasting to ${clients.length} clients: ${payload.event} - ${payload.path || ""}`); clients = clients.filter((client) => { try { client.write(data); return true; } catch (error) { logger.warn("SSE client write failed, removing"); return false; } }); } }; // api/core/events/routes.ts var router2 = express2.Router(); router2.get("/", (req, res) => { if (process.env.VERCEL) { const response = { success: false, error: { code: "SSE_UNSUPPORTED", message: "SSE\u5728\u65E0\u670D\u52A1\u5668\u8FD0\u884C\u65F6\u4E2D\u4E0D\u53D7\u652F\u6301" } }; return res.status(501).json(response); } const headers = { "Content-Type": "text/event-stream", "Connection": "keep-alive", "Cache-Control": "no-cache" }; res.writeHead(200, headers); res.write(`data: ${JSON.stringify({ event: "connected" })} `); eventBus.addClient(res); req.on("close", () => { eventBus.removeClient(res); }); }); var routes_default2 = router2; // api/core/settings/routes.ts import express3 from "express"; import fs2 from "fs/promises"; import path4 from "path"; var router3 = express3.Router(); var getSettingsPath = () => path4.join(NOTEBOOK_ROOT, ".config", "settings.json"); router3.get( "/", asyncHandler(async (req, res) => { const settingsPath = getSettingsPath(); try { const content = await fs2.readFile(settingsPath, "utf-8"); const settings = JSON.parse(content); successResponse(res, settings); } catch (error) { successResponse(res, {}); } }) ); router3.post( "/", asyncHandler(async (req, res) => { const settings = req.body; const settingsPath = getSettingsPath(); const configDir = path4.dirname(settingsPath); try { await fs2.mkdir(configDir, { recursive: true }); let existingSettings = {}; try { const content = await fs2.readFile(settingsPath, "utf-8"); existingSettings = JSON.parse(content); } catch { } const newSettings = { ...existingSettings, ...settings }; await fs2.writeFile(settingsPath, JSON.stringify(newSettings, null, 2), "utf-8"); successResponse(res, newSettings); } catch (error) { throw error; } }) ); var routes_default3 = router3; // api/core/upload/routes.ts import express4 from "express"; import fs4 from "fs/promises"; import path6 from "path"; // api/utils/file.ts import fs3 from "fs/promises"; import path5 from "path"; var getUniqueFilename = async (imagesDirFullPath, baseName, ext) => { const maxAttempts = 1e3; for (let i = 0; i < maxAttempts; i++) { const suffix = i === 0 ? "" : `-${i + 1}`; const filename = `${baseName}${suffix}${ext}`; const fullPath = path5.join(imagesDirFullPath, filename); try { await fs3.access(fullPath); } catch { return filename; } } throw new InternalError("Failed to generate unique filename"); }; var mimeToExt = { "image/png": ".png", "image/jpeg": ".jpg", "image/jpg": ".jpg", "image/gif": ".gif", "image/webp": ".webp" }; var IMAGE_MAGIC_BYTES = { "image/png": { bytes: [137, 80, 78, 71, 13, 10, 26, 10] }, "image/jpeg": { bytes: [255, 216, 255] }, "image/gif": { bytes: [71, 73, 70, 56] }, "image/webp": { bytes: [82, 73, 70, 70], offset: 0 } }; var WEBP_WEBP_MARKER = [87, 69, 66, 80]; var MIN_IMAGE_SIZE = 16; var MAX_IMAGE_SIZE = 8 * 1024 * 1024; var validateImageBuffer = (buffer, claimedMimeType) => { if (buffer.byteLength < MIN_IMAGE_SIZE) { throw new ValidationError("Image file is too small or corrupted"); } if (buffer.byteLength > MAX_IMAGE_SIZE) { throw new ValidationError("Image file is too large"); } const magicInfo = IMAGE_MAGIC_BYTES[claimedMimeType]; if (!magicInfo) { throw new ValidationError("Unsupported image type for content validation"); } const offset = magicInfo.offset || 0; const expectedBytes = magicInfo.bytes; for (let i = 0; i < expectedBytes.length; i++) { if (buffer[offset + i] !== expectedBytes[i]) { throw new ValidationError("Image content does not match the claimed file type"); } } if (claimedMimeType === "image/webp") { if (buffer.byteLength < 12) { throw new ValidationError("WebP image is corrupted"); } for (let i = 0; i < WEBP_WEBP_MARKER.length; i++) { if (buffer[8 + i] !== WEBP_WEBP_MARKER[i]) { throw new ValidationError("WebP image content is invalid"); } } } }; var detectImageMimeType = (buffer) => { if (buffer.byteLength < 8) return null; if (buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71 && buffer[4] === 13 && buffer[5] === 10 && buffer[6] === 26 && buffer[7] === 10) { return "image/png"; } if (buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) { return "image/jpeg"; } if (buffer[0] === 71 && buffer[1] === 73 && buffer[2] === 70 && buffer[3] === 56) { return "image/gif"; } if (buffer[0] === 82 && buffer[1] === 73 && buffer[2] === 70 && buffer[3] === 70 && buffer[8] === 87 && buffer[9] === 69 && buffer[10] === 66 && buffer[11] === 80) { return "image/webp"; } return null; }; // api/core/upload/routes.ts var router4 = express4.Router(); var parseImageDataUrl = (dataUrl) => { const match = dataUrl.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=\s]+)$/); if (!match) return null; const [, mimeType, base64Data] = match; return { mimeType, base64Data: base64Data.replace(/\s/g, "") }; }; router4.post( "/image", asyncHandler(async (req, res) => { const { image } = req.body; if (!image) throw new ValidationError("\u9700\u8981\u56FE\u7247\u6570\u636E"); const parsed = parseImageDataUrl(image); if (!parsed) { throw new ValidationError("\u65E0\u6548\u7684\u56FE\u7247\u6570\u636EURL"); } const ext = mimeToExt[parsed.mimeType]; if (!ext) { throw new UnsupportedMediaTypeError("\u4E0D\u652F\u6301\u7684\u56FE\u7247\u7C7B\u578B"); } const buffer = Buffer.from(parsed.base64Data, "base64"); validateImageBuffer(buffer, parsed.mimeType); const detectedMimeType = detectImageMimeType(buffer); if (!detectedMimeType || detectedMimeType !== parsed.mimeType) { throw new ValidationError("\u56FE\u7247\u5185\u5BB9\u7C7B\u578B\u4E0D\u5339\u914D\u6216\u56FE\u7247\u5DF2\u635F\u574F"); } const now = /* @__PURE__ */ new Date(); const year = now.getFullYear(); const month = pad2(now.getMonth() + 1); const day = pad2(now.getDate()); const imagesSubDir = `images/${year}/${month}/${day}`; const { fullPath: imagesDirFullPath } = resolveNotebookPath(imagesSubDir); await fs4.mkdir(imagesDirFullPath, { recursive: true }); const baseName = formatTimestamp(now); const filename = await getUniqueFilename(imagesDirFullPath, baseName, ext); const relPath = `${imagesSubDir}/${filename}`; const { fullPath } = resolveNotebookPath(relPath); await fs4.writeFile(fullPath, buffer); successResponse(res, { name: toPosixPath(relPath), path: toPosixPath(relPath) }); }) ); router4.post( "/wallpaper", asyncHandler(async (req, res) => { const { image } = req.body; if (!image) throw new ValidationError("\u9700\u8981\u56FE\u7247\u6570\u636E"); const parsed = parseImageDataUrl(image); if (!parsed) { throw new ValidationError("\u65E0\u6548\u7684\u56FE\u7247\u6570\u636EURL"); } const allowedWallpaperTypes = ["image/png", "image/jpeg", "image/webp"]; if (!allowedWallpaperTypes.includes(parsed.mimeType)) { throw new UnsupportedMediaTypeError("\u58C1\u7EB8\u53EA\u652F\u6301PNG\u3001JPEG\u548CWebP\u683C\u5F0F"); } const buffer = Buffer.from(parsed.base64Data, "base64"); validateImageBuffer(buffer, parsed.mimeType); const detectedMimeType = detectImageMimeType(buffer); if (!detectedMimeType || detectedMimeType !== parsed.mimeType) { throw new ValidationError("\u56FE\u7247\u5185\u5BB9\u7C7B\u578B\u4E0D\u5339\u914D\u6216\u56FE\u7247\u5DF2\u635F\u574F"); } const configDir = path6.join(NOTEBOOK_ROOT, ".config"); const backgroundPath = path6.join(configDir, "background.png"); await fs4.mkdir(configDir, { recursive: true }); await fs4.writeFile(backgroundPath, buffer); successResponse(res, { message: "\u58C1\u7EB8\u5DF2\u66F4\u65B0" }); }) ); var routes_default4 = router4; // api/core/search/routes.ts import express5 from "express"; import fs5 from "fs/promises"; import path7 from "path"; var router5 = express5.Router(); router5.post( "/", asyncHandler(async (req, res) => { const { keywords } = req.body; if (!keywords || !Array.isArray(keywords) || keywords.length === 0) { successResponse(res, { items: [] }); return; } const searchTerms = keywords.map((k) => k.trim().toLowerCase()).filter((k) => k.length > 0); if (searchTerms.length === 0) { successResponse(res, { items: [] }); return; } const { fullPath: rootPath } = resolveNotebookPath(""); const results = []; const maxResults = 100; const searchDir = async (dir, relativeDir) => { if (results.length >= maxResults) return; const entries = await fs5.readdir(dir, { withFileTypes: true }); for (const entry of entries) { if (results.length >= maxResults) break; const entryPath = path7.join(dir, entry.name); const entryRelativePath = path7.join(relativeDir, entry.name); if (entry.name.startsWith(".") || entry.name === "RB" || entry.name === "node_modules") continue; if (entry.isDirectory()) { await searchDir(entryPath, entryRelativePath); } else if (entry.isFile()) { const fileNameLower = entry.name.toLowerCase(); let contentLower = ""; let contentLoaded = false; const checkKeyword = async (term) => { if (fileNameLower.includes(term)) return true; if (entry.name.toLowerCase().endsWith(".md")) { if (!contentLoaded) { try { const content = await fs5.readFile(entryPath, "utf-8"); contentLower = content.toLowerCase(); contentLoaded = true; } catch { return false; } } return contentLower.includes(term); } return false; }; let allMatched = true; for (const term of searchTerms) { const matched = await checkKeyword(term); if (!matched) { allMatched = false; break; } } if (allMatched) { results.push({ name: entry.name, path: toPosixPath(entryRelativePath), type: "file", size: 0, modified: (/* @__PURE__ */ new Date()).toISOString() }); } } } }; await searchDir(rootPath, ""); successResponse(res, { items: results, limited: results.length >= maxResults }); }) ); var routes_default5 = router5; // shared/constants/errors.ts var ERROR_CODES = { PATH_NOT_FOUND: "PATH_NOT_FOUND", NOT_A_DIRECTORY: "NOT_A_DIRECTORY", ACCESS_DENIED: "ACCESS_DENIED", FILE_EXISTS: "FILE_EXISTS", INVALID_PATH: "INVALID_PATH", VALIDATION_ERROR: "VALIDATION_ERROR", INTERNAL_ERROR: "INTERNAL_ERROR", NOT_FOUND: "NOT_FOUND", BAD_REQUEST: "BAD_REQUEST", NAME_GENERATION_FAILED: "NAME_GENERATION_FAILED", SSE_UNSUPPORTED: "SSE_UNSUPPORTED", ALREADY_EXISTS: "ALREADY_EXISTS", NOT_A_FILE: "NOT_A_FILE", FORBIDDEN: "FORBIDDEN", UNSUPPORTED_MEDIA_TYPE: "UNSUPPORTED_MEDIA_TYPE", PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE", RESOURCE_LOCKED: "RESOURCE_LOCKED", INVALID_NAME: "INVALID_NAME" }; // api/middlewares/errorHandler.ts var errorHandler = (err, _req, res, _next) => { let statusCode = 500; let code = ERROR_CODES.INTERNAL_ERROR; let message = "Server internal error"; let details = void 0; if (isAppError(err)) { statusCode = err.statusCode; code = err.code; message = err.message; details = err.details; } else if (isNodeError(err)) { message = err.message; if (process.env.NODE_ENV !== "production") { details = { stack: err.stack, nodeErrorCode: err.code }; } } else if (err instanceof Error) { message = err.message; if (process.env.NODE_ENV !== "production") { details = { stack: err.stack }; } } logger.error(err); const response = { success: false, error: { code, message, details: process.env.NODE_ENV === "production" ? void 0 : details } }; res.status(statusCode).json(response); }; // api/infra/moduleManager.ts var ModuleManager = class { modules = /* @__PURE__ */ new Map(); activeModules = /* @__PURE__ */ new Set(); container; constructor(container2) { this.container = container2; } async register(module) { const { id, dependencies = [] } = module.metadata; for (const dep of dependencies) { if (!this.modules.has(dep)) { throw new Error(`Module '${id}' depends on '${dep}' which is not registered`); } } this.modules.set(id, module); if (module.lifecycle?.onLoad) { await module.lifecycle.onLoad(this.container); } } async activate(id) { const module = this.modules.get(id); if (!module) { throw new Error(`Module '${id}' not found`); } if (this.activeModules.has(id)) { return; } const { dependencies = [] } = module.metadata; for (const dep of dependencies) { await this.activate(dep); } if (module.lifecycle?.onActivate) { await module.lifecycle.onActivate(this.container); } this.activeModules.add(id); } async deactivate(id) { const module = this.modules.get(id); if (!module) return; if (!this.activeModules.has(id)) return; if (module.lifecycle?.onDeactivate) { await module.lifecycle.onDeactivate(this.container); } this.activeModules.delete(id); } getModule(id) { return this.modules.get(id); } getAllModules() { return Array.from(this.modules.values()).sort((a, b) => (a.metadata.order || 0) - (b.metadata.order || 0)); } getActiveModules() { return Array.from(this.activeModules); } }; // api/infra/container.ts var ServiceContainer = class { descriptors = /* @__PURE__ */ new Map(); singletonInstances = /* @__PURE__ */ new Map(); scopedInstances = /* @__PURE__ */ new Map(); resolutionStack = []; disposed = false; register(nameOrDescriptor, factory) { this.ensureNotDisposed(); if (typeof nameOrDescriptor === "string") { const descriptor = { name: nameOrDescriptor, factory, lifetime: "singleton" /* Singleton */ }; this.descriptors.set(nameOrDescriptor, descriptor); } else { this.descriptors.set(nameOrDescriptor.name, nameOrDescriptor); } } async get(name) { this.ensureNotDisposed(); return this.resolveInternal(name, null); } getSync(name) { this.ensureNotDisposed(); const descriptor = this.descriptors.get(name); if (!descriptor) { throw new Error(`Service '${name}' not registered`); } if (descriptor.lifetime === "singleton" /* Singleton */) { if (this.singletonInstances.has(name)) { return this.singletonInstances.get(name); } } const result = descriptor.factory(); if (result instanceof Promise) { throw new Error( `Service '${name}' has an async factory but getSync() was called. Use get() instead.` ); } if (descriptor.lifetime === "singleton" /* Singleton */) { this.singletonInstances.set(name, result); } return result; } createScope(scopeId) { this.ensureNotDisposed(); return new ServiceScope(this, scopeId); } has(name) { return this.descriptors.has(name); } async dispose() { if (this.disposed) { return; } for (const [name, instance] of this.singletonInstances) { const descriptor = this.descriptors.get(name); if (descriptor?.onDispose) { try { await descriptor.onDispose(instance); } catch (error) { console.error(`Error disposing service '${name}':`, error); } } } for (const [, scopeMap] of this.scopedInstances) { for (const [name, instance] of scopeMap) { const descriptor = this.descriptors.get(name); if (descriptor?.onDispose) { try { await descriptor.onDispose(instance); } catch (error) { console.error(`Error disposing scoped service '${name}':`, error); } } } } this.singletonInstances.clear(); this.scopedInstances.clear(); this.descriptors.clear(); this.resolutionStack = []; this.disposed = true; } clear() { this.singletonInstances.clear(); this.scopedInstances.clear(); this.descriptors.clear(); this.resolutionStack = []; } isDisposed() { return this.disposed; } async resolveInternal(name, scopeId) { if (this.resolutionStack.includes(name)) { const cycle = [...this.resolutionStack, name].join(" -> "); throw new Error(`Circular dependency detected: ${cycle}`); } const descriptor = this.descriptors.get(name); if (!descriptor) { throw new Error(`Service '${name}' not registered`); } if (descriptor.lifetime === "singleton" /* Singleton */) { if (this.singletonInstances.has(name)) { return this.singletonInstances.get(name); } this.resolutionStack.push(name); try { const instance = await descriptor.factory(); this.singletonInstances.set(name, instance); return instance; } finally { this.resolutionStack.pop(); } } if (descriptor.lifetime === "scoped" /* Scoped */) { if (!scopeId) { throw new Error( `Scoped service '${name}' cannot be resolved outside of a scope. Use createScope() first.` ); } let scopeMap = this.scopedInstances.get(scopeId); if (!scopeMap) { scopeMap = /* @__PURE__ */ new Map(); this.scopedInstances.set(scopeId, scopeMap); } if (scopeMap.has(name)) { return scopeMap.get(name); } this.resolutionStack.push(name); try { const instance = await descriptor.factory(); scopeMap.set(name, instance); return instance; } finally { this.resolutionStack.pop(); } } this.resolutionStack.push(name); try { const instance = await descriptor.factory(); return instance; } finally { this.resolutionStack.pop(); } } ensureNotDisposed() { if (this.disposed) { throw new Error("ServiceContainer has been disposed"); } } }; var ServiceScope = class { container; scopeId; disposed = false; constructor(container2, scopeId) { this.container = container2; this.scopeId = scopeId; } async get(name) { if (this.disposed) { throw new Error("ServiceScope has been disposed"); } return this.container["resolveInternal"](name, this.scopeId); } async dispose() { if (this.disposed) { return; } const scopeMap = this.container["scopedInstances"].get(this.scopeId); if (scopeMap) { for (const [name, instance] of scopeMap) { const descriptor = this.container["descriptors"].get(name); if (descriptor?.onDispose) { try { await descriptor.onDispose(instance); } catch (error) { console.error(`Error disposing scoped service '${name}':`, error); } } } this.container["scopedInstances"].delete(this.scopeId); } this.disposed = true; } isDisposed() { return this.disposed; } }; // api/modules/index.ts import { readdirSync, statSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath as fileURLToPath3 } from "url"; // shared/modules/types.ts function defineApiModule(config2) { return config2; } function defineEndpoints(endpoints) { return endpoints; } // shared/modules/todo/api.ts var TODO_ENDPOINTS = defineEndpoints({ list: { path: "/", method: "GET" }, save: { path: "/save", method: "POST" }, add: { path: "/add", method: "POST" }, toggle: { path: "/toggle", method: "POST" }, update: { path: "/update", method: "POST" }, delete: { path: "/delete", method: "DELETE" } }); // shared/modules/todo/index.ts var TODO_MODULE = defineApiModule({ id: "todo", name: "TODO", basePath: "/todo", order: 30, version: "1.0.0", endpoints: TODO_ENDPOINTS }); // api/modules/todo/service.ts import fs6 from "fs/promises"; import path8 from "path"; // api/modules/todo/parser.ts var parseTodoContent = (content) => { const lines = content.split("\n"); const result = []; let currentDate = null; let currentItems = []; let itemId = 0; for (const line of lines) { const dateMatch = line.match(/^##\s*(\d{4}-\d{2}-\d{2})/); if (dateMatch) { if (currentDate) { result.push({ date: currentDate, items: currentItems }); } currentDate = dateMatch[1]; currentItems = []; } else if (currentDate) { const todoMatch = line.match(/^- (√|○) (.*)$/); if (todoMatch) { currentItems.push({ id: `${currentDate}-${itemId++}`, content: todoMatch[2], completed: todoMatch[1] === "\u221A" }); } } } if (currentDate) { result.push({ date: currentDate, items: currentItems }); } return result; }; var generateTodoContent = (dayTodos) => { const lines = []; const sortedDays = [...dayTodos].sort((a, b) => a.date.localeCompare(b.date)); for (const day of sortedDays) { lines.push(`## ${day.date}`); for (const item of day.items) { const checkbox = item.completed ? "\u221A" : "\u25CB"; lines.push(`- ${checkbox} ${item.content}`); } lines.push(""); } return lines.join("\n").trimEnd(); }; // api/modules/todo/service.ts var TodoService = class { constructor(deps = {}) { this.deps = deps; } getTodoFilePath(year, month) { const yearStr = year.toString(); const monthStr = month.toString().padStart(2, "0"); const relPath = `TODO/${yearStr}/${yearStr}${monthStr}TODO.md`; const { fullPath } = resolveNotebookPath(relPath); return { relPath, fullPath }; } async ensureTodoFileExists(fullPath) { const dir = path8.dirname(fullPath); await fs6.mkdir(dir, { recursive: true }); try { await fs6.access(fullPath); } catch { await fs6.writeFile(fullPath, "", "utf-8"); } } async loadAndParseTodoFile(year, month) { const { fullPath } = this.getTodoFilePath(year, month); try { await fs6.access(fullPath); } catch { throw new NotFoundError("TODO file not found"); } const content = await fs6.readFile(fullPath, "utf-8"); return { fullPath, dayTodos: parseTodoContent(content) }; } async saveTodoFile(fullPath, dayTodos) { const content = generateTodoContent(dayTodos); await fs6.writeFile(fullPath, content, "utf-8"); } async getTodo(year, month) { const { fullPath } = this.getTodoFilePath(year, month); let dayTodos = []; try { await fs6.access(fullPath); const content = await fs6.readFile(fullPath, "utf-8"); dayTodos = parseTodoContent(content); } catch { } const now = /* @__PURE__ */ new Date(); const todayStr = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, "0")}-${now.getDate().toString().padStart(2, "0")}`; const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); const yesterdayStr = `${yesterday.getFullYear()}-${(yesterday.getMonth() + 1).toString().padStart(2, "0")}-${yesterday.getDate().toString().padStart(2, "0")}`; if (year === now.getFullYear() && month === now.getMonth() + 1) { const migrated = this.migrateIncompleteItems(dayTodos, todayStr, yesterdayStr); if (migrated) { const newContent = generateTodoContent(dayTodos); await this.ensureTodoFileExists(fullPath); await fs6.writeFile(fullPath, newContent, "utf-8"); } } return { dayTodos, year, month }; } migrateIncompleteItems(dayTodos, todayStr, yesterdayStr) { let migrated = false; const yesterdayTodo = dayTodos.find((d) => d.date === yesterdayStr); if (yesterdayTodo) { const incompleteItems = yesterdayTodo.items.filter((item) => !item.completed); if (incompleteItems.length > 0) { const todayTodo = dayTodos.find((d) => d.date === todayStr); if (todayTodo) { const existingIds = new Set(todayTodo.items.map((i) => i.id)); const itemsToAdd = incompleteItems.map((item, idx) => ({ ...item, id: existingIds.has(item.id) ? `${todayStr}-migrated-${idx}` : item.id })); todayTodo.items = [...itemsToAdd, ...todayTodo.items]; } else { dayTodos.push({ date: todayStr, items: incompleteItems.map((item, idx) => ({ ...item, id: `${todayStr}-migrated-${idx}` })) }); } yesterdayTodo.items = yesterdayTodo.items.filter((item) => item.completed); if (yesterdayTodo.items.length === 0) { const index = dayTodos.findIndex((d) => d.date === yesterdayStr); if (index !== -1) { dayTodos.splice(index, 1); } } migrated = true; } } return migrated; } async saveTodo(year, month, dayTodos) { const { fullPath } = this.getTodoFilePath(year, month); await this.ensureTodoFileExists(fullPath); const content = generateTodoContent(dayTodos); await fs6.writeFile(fullPath, content, "utf-8"); } async addTodo(year, month, date, todoContent) { const { fullPath } = this.getTodoFilePath(year, month); await this.ensureTodoFileExists(fullPath); let fileContent = await fs6.readFile(fullPath, "utf-8"); const dayTodos = parseTodoContent(fileContent); const existingDay = dayTodos.find((d) => d.date === date); if (existingDay) { const newId = `${date}-${existingDay.items.length}`; existingDay.items.push({ id: newId, content: todoContent, completed: false }); } else { dayTodos.push({ date, items: [{ id: `${date}-0`, content: todoContent, completed: false }] }); } fileContent = generateTodoContent(dayTodos); await fs6.writeFile(fullPath, fileContent, "utf-8"); return dayTodos; } async toggleTodo(year, month, date, itemIndex, completed) { const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month); const day = dayTodos.find((d) => d.date === date); if (!day || itemIndex >= day.items.length) { throw new NotFoundError("TODO item not found"); } day.items[itemIndex].completed = completed; await this.saveTodoFile(fullPath, dayTodos); return dayTodos; } async updateTodo(year, month, date, itemIndex, newContent) { const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month); const day = dayTodos.find((d) => d.date === date); if (!day || itemIndex >= day.items.length) { throw new NotFoundError("TODO item not found"); } day.items[itemIndex].content = newContent; await this.saveTodoFile(fullPath, dayTodos); return dayTodos; } async deleteTodo(year, month, date, itemIndex) { const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month); const dayIndex = dayTodos.findIndex((d) => d.date === date); if (dayIndex === -1) { throw new NotFoundError("Day not found"); } const day = dayTodos[dayIndex]; if (itemIndex >= day.items.length) { throw new NotFoundError("TODO item not found"); } day.items.splice(itemIndex, 1); if (day.items.length === 0) { dayTodos.splice(dayIndex, 1); } await this.saveTodoFile(fullPath, dayTodos); return dayTodos; } }; // api/modules/todo/routes.ts import express6 from "express"; // api/modules/todo/schemas.ts import { z as z3 } from "zod"; var todoItemSchema = z3.object({ id: z3.string(), content: z3.string(), completed: z3.boolean() }); var dayTodoSchema = z3.object({ date: z3.string(), items: z3.array(todoItemSchema) }); var getTodoQuerySchema = z3.object({ year: z3.string().optional(), month: z3.string().optional() }); var saveTodoSchema = z3.object({ year: z3.number().int().positive(), month: z3.number().int().min(1).max(12), dayTodos: z3.array(dayTodoSchema) }); var addTodoSchema = z3.object({ year: z3.number().int().positive(), month: z3.number().int().min(1).max(12), date: z3.string(), content: z3.string() }); var toggleTodoSchema = z3.object({ year: z3.number().int().positive(), month: z3.number().int().min(1).max(12), date: z3.string(), itemIndex: z3.number().int().nonnegative(), completed: z3.boolean() }); var updateTodoSchema = z3.object({ year: z3.number().int().positive(), month: z3.number().int().min(1).max(12), date: z3.string(), itemIndex: z3.number().int().nonnegative(), content: z3.string() }); var deleteTodoSchema = z3.object({ year: z3.number().int().positive(), month: z3.number().int().min(1).max(12), date: z3.string(), itemIndex: z3.number().int().nonnegative() }); // api/modules/todo/routes.ts var createTodoRoutes = (deps) => { const router10 = express6.Router(); const { todoService: todoService2 } = deps; router10.get( "/", validateQuery(getTodoQuerySchema), asyncHandler(async (req, res) => { const year = parseInt(req.query.year) || (/* @__PURE__ */ new Date()).getFullYear(); const month = parseInt(req.query.month) || (/* @__PURE__ */ new Date()).getMonth() + 1; const result = await todoService2.getTodo(year, month); successResponse(res, result); }) ); router10.post( "/save", validateBody(saveTodoSchema), asyncHandler(async (req, res) => { const { year, month, dayTodos } = req.body; await todoService2.saveTodo(year, month, dayTodos); successResponse(res, null); }) ); router10.post( "/add", validateBody(addTodoSchema), asyncHandler(async (req, res) => { const { year, month, date, content: todoContent } = req.body; const dayTodos = await todoService2.addTodo(year, month, date, todoContent); successResponse(res, { dayTodos }); }) ); router10.post( "/toggle", validateBody(toggleTodoSchema), asyncHandler(async (req, res) => { const { year, month, date, itemIndex, completed } = req.body; const dayTodos = await todoService2.toggleTodo(year, month, date, itemIndex, completed); successResponse(res, { dayTodos }); }) ); router10.post( "/update", validateBody(updateTodoSchema), asyncHandler(async (req, res) => { const { year, month, date, itemIndex, content: newContent } = req.body; const dayTodos = await todoService2.updateTodo(year, month, date, itemIndex, newContent); successResponse(res, { dayTodos }); }) ); router10.delete( "/delete", validateBody(deleteTodoSchema), asyncHandler(async (req, res) => { const { year, month, date, itemIndex } = req.body; const dayTodos = await todoService2.deleteTodo(year, month, date, itemIndex); successResponse(res, { dayTodos }); }) ); return router10; }; var todoService = new TodoService(); var routes_default6 = createTodoRoutes({ todoService }); // shared/modules/time-tracking/api.ts var TIME_TRACKING_ENDPOINTS = defineEndpoints({ current: { path: "/current", method: "GET" }, event: { path: "/event", method: "POST" }, day: { path: "/day/:date", method: "GET" }, week: { path: "/week/:startDate", method: "GET" }, month: { path: "/month/:yearMonth", method: "GET" }, year: { path: "/year/:year", method: "GET" }, stats: { path: "/stats", method: "GET" } }); // shared/modules/time-tracking/index.ts var TIME_TRACKING_MODULE = defineApiModule({ id: "time-tracking", name: "\u65F6\u95F4\u7EDF\u8BA1", basePath: "/time", order: 20, version: "1.0.0", endpoints: TIME_TRACKING_ENDPOINTS }); // api/modules/time-tracking/sessionPersistence.ts import path9 from "path"; var TIME_ROOT = path9.join(NOTEBOOK_ROOT, "time"); // api/modules/time-tracking/routes.ts import express7 from "express"; // shared/modules/recycle-bin/api.ts var RECYCLE_BIN_ENDPOINTS = defineEndpoints({ list: { path: "/", method: "GET" }, restore: { path: "/restore", method: "POST" }, permanent: { path: "/permanent", method: "DELETE" }, empty: { path: "/empty", method: "DELETE" } }); // shared/modules/recycle-bin/index.ts var RECYCLE_BIN_MODULE = defineApiModule({ id: "recycle-bin", name: "\u56DE\u6536\u7AD9", basePath: "/recycle-bin", order: 40, version: "1.0.0", endpoints: RECYCLE_BIN_ENDPOINTS }); // api/modules/recycle-bin/routes.ts import express8 from "express"; import fs8 from "fs/promises"; import path11 from "path"; // api/modules/recycle-bin/recycleBinService.ts import fs7 from "fs/promises"; import path10 from "path"; async function restoreFile(srcPath, destPath, deletedDate, year, month, day) { const { fullPath: imagesDir } = resolveNotebookPath(`images/${year}/${month}/${day}`); let content = await fs7.readFile(srcPath, "utf-8"); const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; let match; const imageReplacements = []; while ((match = imageRegex.exec(content)) !== null) { const imagePath = match[2]; const imageName = path10.basename(imagePath); const rbImageName = `${deletedDate}_${imageName}`; const { fullPath: srcImagePath } = resolveNotebookPath(`RB/${rbImageName}`); try { await fs7.access(srcImagePath); await fs7.mkdir(imagesDir, { recursive: true }); const destImagePath = path10.join(imagesDir, imageName); await fs7.rename(srcImagePath, destImagePath); const newImagePath = `images/${year}/${month}/${day}/${imageName}`; imageReplacements.push({ oldPath: imagePath, newPath: newImagePath }); } catch { } } for (const { oldPath, newPath } of imageReplacements) { content = content.replace(new RegExp(oldPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), newPath); } await fs7.writeFile(destPath, content, "utf-8"); await fs7.unlink(srcPath); } async function restoreFolder(srcPath, destPath, deletedDate, year, month, day) { await fs7.mkdir(destPath, { recursive: true }); const entries = await fs7.readdir(srcPath, { withFileTypes: true }); for (const entry of entries) { const srcEntryPath = path10.join(srcPath, entry.name); const destEntryPath = path10.join(destPath, entry.name); if (entry.isDirectory()) { await restoreFolder(srcEntryPath, destEntryPath, deletedDate, year, month, day); } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) { await restoreFile(srcEntryPath, destEntryPath, deletedDate, year, month, day); } else { await fs7.rename(srcEntryPath, destEntryPath); } } const remaining = await fs7.readdir(srcPath); if (remaining.length === 0) { await fs7.rmdir(srcPath); } } // api/modules/recycle-bin/routes.ts var router6 = express8.Router(); router6.get( "/", asyncHandler(async (req, res) => { const { fullPath: rbDir } = resolveNotebookPath("RB"); try { await fs8.access(rbDir); } catch { successResponse(res, { groups: [] }); return; } const entries = await fs8.readdir(rbDir, { withFileTypes: true }); const items = []; for (const entry of entries) { const match = entry.name.match(/^(\d{8})_(.+)$/); if (!match) continue; const [, dateStr, originalName] = match; if (entry.isDirectory()) { items.push({ name: entry.name, originalName, type: "dir", deletedDate: dateStr, path: `RB/${entry.name}` }); } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) { items.push({ name: entry.name, originalName, type: "file", deletedDate: dateStr, path: `RB/${entry.name}` }); } } const groupedMap = /* @__PURE__ */ new Map(); for (const item of items) { const existing = groupedMap.get(item.deletedDate) || []; existing.push(item); groupedMap.set(item.deletedDate, existing); } const groups = Array.from(groupedMap.entries()).map(([date, items2]) => ({ date, items: items2.sort((a, b) => a.originalName.localeCompare(b.originalName)) })).sort((a, b) => b.date.localeCompare(a.date)); successResponse(res, { groups }); }) ); router6.post( "/restore", asyncHandler(async (req, res) => { const { path: relPath, type } = req.body; if (!relPath || !type) { throw new ValidationError("Path and type are required"); } const { fullPath: itemPath } = resolveNotebookPath(relPath); try { await fs8.access(itemPath); } catch { throw new NotFoundError("Item not found in recycle bin"); } const match = path11.basename(itemPath).match(/^(\d{8})_(.+)$/); if (!match) { throw new BadRequestError("Invalid recycle bin item name"); } const [, dateStr, originalName] = match; const year = dateStr.substring(0, 4); const month = dateStr.substring(4, 6); const day = dateStr.substring(6, 8); const { fullPath: markdownsDir } = resolveNotebookPath("markdowns"); await fs8.mkdir(markdownsDir, { recursive: true }); const destPath = path11.join(markdownsDir, originalName); const existing = await fs8.stat(destPath).catch(() => null); if (existing) { throw new AlreadyExistsError("A file or folder with this name already exists"); } if (type === "dir") { await restoreFolder(itemPath, destPath, dateStr, year, month, day); } else { await restoreFile(itemPath, destPath, dateStr, year, month, day); } successResponse(res, null); }) ); router6.delete( "/permanent", asyncHandler(async (req, res) => { const { path: relPath, type } = req.body; if (!relPath || !type) { throw new ValidationError("Path and type are required"); } const { fullPath: itemPath } = resolveNotebookPath(relPath); try { await fs8.access(itemPath); } catch { throw new NotFoundError("Item not found in recycle bin"); } if (type === "dir") { await fs8.rm(itemPath, { recursive: true, force: true }); } else { await fs8.unlink(itemPath); } successResponse(res, null); }) ); router6.delete( "/empty", asyncHandler(async (req, res) => { const { fullPath: rbDir } = resolveNotebookPath("RB"); try { await fs8.access(rbDir); } catch { successResponse(res, null); return; } const entries = await fs8.readdir(rbDir, { withFileTypes: true }); for (const entry of entries) { const entryPath = path11.join(rbDir, entry.name); if (entry.isDirectory()) { await fs8.rm(entryPath, { recursive: true, force: true }); } else { await fs8.unlink(entryPath); } } successResponse(res, null); }) ); // shared/modules/pydemos/api.ts var PYDEMOS_ENDPOINTS = defineEndpoints({ list: { path: "/", method: "GET" }, create: { path: "/create", method: "POST" }, delete: { path: "/delete", method: "DELETE" }, rename: { path: "/rename", method: "POST" } }); // shared/modules/pydemos/index.ts var PYDEMOS_MODULE = defineApiModule({ id: "pydemos", name: "Python Demos", basePath: "/pydemos", order: 50, version: "1.0.0", endpoints: PYDEMOS_ENDPOINTS }); // api/modules/pydemos/routes.ts import express9 from "express"; import fs9 from "fs/promises"; import path12 from "path"; import multer from "multer"; // api/utils/tempDir.ts import { existsSync, mkdirSync } from "fs"; var tempDir = null; var getTempDir = () => { if (!tempDir) { tempDir = PATHS.TEMP_ROOT; if (!existsSync(tempDir)) { mkdirSync(tempDir, { recursive: true }); } } return tempDir; }; // api/modules/pydemos/routes.ts var tempDir2 = getTempDir(); var upload = multer({ dest: tempDir2, limits: { fileSize: 50 * 1024 * 1024 } }); var toPosixPath2 = (p) => p.replace(/\\/g, "/"); var getYearPath = (year) => { const relPath = `pydemos/${year}`; const { fullPath } = resolveNotebookPath(relPath); return { relPath, fullPath }; }; var getMonthPath = (year, month) => { const monthStr = month.toString().padStart(2, "0"); const relPath = `pydemos/${year}/${monthStr}`; const { fullPath } = resolveNotebookPath(relPath); return { relPath, fullPath }; }; var countFilesInDir = async (dirPath) => { try { const entries = await fs9.readdir(dirPath, { withFileTypes: true }); return entries.filter((e) => e.isFile()).length; } catch { return 0; } }; var createPyDemosRoutes = () => { const router10 = express9.Router(); router10.get( "/", validateQuery(listPyDemosQuerySchema), asyncHandler(async (req, res) => { const year = parseInt(req.query.year) || (/* @__PURE__ */ new Date()).getFullYear(); const { fullPath: yearPath } = getYearPath(year); const months = []; try { await fs9.access(yearPath); } catch { successResponse(res, { months }); return; } const monthEntries = await fs9.readdir(yearPath, { withFileTypes: true }); for (const monthEntry of monthEntries) { if (!monthEntry.isDirectory()) continue; const monthNum = parseInt(monthEntry.name); if (isNaN(monthNum) || monthNum < 1 || monthNum > 12) continue; const monthPath = path12.join(yearPath, monthEntry.name); const demoEntries = await fs9.readdir(monthPath, { withFileTypes: true }); const demos = []; for (const demoEntry of demoEntries) { if (!demoEntry.isDirectory()) continue; const demoPath = path12.join(monthPath, demoEntry.name); const relDemoPath = `pydemos/${year}/${monthEntry.name}/${demoEntry.name}`; let created; try { const stats = await fs9.stat(demoPath); created = stats.birthtime.toISOString(); } catch { created = (/* @__PURE__ */ new Date()).toISOString(); } const fileCount = await countFilesInDir(demoPath); demos.push({ name: demoEntry.name, path: toPosixPath2(relDemoPath), created, fileCount }); } demos.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime()); if (demos.length > 0) { months.push({ month: monthNum, demos }); } } months.sort((a, b) => a.month - b.month); successResponse(res, { months }); }) ); router10.post( "/create", upload.array("files"), validateBody(createPyDemoSchema), asyncHandler(async (req, res) => { const { name, year, month, folderStructure } = req.body; const yearNum = parseInt(year); const monthNum = parseInt(month); if (!/^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$/.test(name)) { throw new ValidationError("Invalid name format"); } const { fullPath: monthPath, relPath: monthRelPath } = getMonthPath(yearNum, monthNum); const demoPath = path12.join(monthPath, name); const relDemoPath = `${monthRelPath}/${name}`; try { await fs9.access(demoPath); throw new AlreadyExistsError("Demo already exists"); } catch (err) { if (isNodeError(err) && err.code === "ENOENT") { } else if (err instanceof AlreadyExistsError) { throw err; } else { throw err; } } await fs9.mkdir(demoPath, { recursive: true }); const files = req.files; let fileCount = 0; if (files && files.length > 0) { let structure = {}; if (folderStructure) { try { structure = JSON.parse(folderStructure); } catch { structure = {}; } } for (const file of files) { const relativePath = structure[file.originalname] || file.originalname; const targetPath = path12.join(demoPath, relativePath); const targetDir = path12.dirname(targetPath); await fs9.mkdir(targetDir, { recursive: true }); await fs9.copyFile(file.path, targetPath); await fs9.unlink(file.path).catch(() => { }); fileCount++; } } successResponse(res, { path: toPosixPath2(relDemoPath), fileCount }); }) ); router10.delete( "/delete", validateBody(deletePyDemoSchema), asyncHandler(async (req, res) => { const { path: demoPath } = req.body; if (!demoPath.startsWith("pydemos/")) { throw new ValidationError("Invalid path"); } const { fullPath } = resolveNotebookPath(demoPath); try { await fs9.access(fullPath); } catch { throw new NotFoundError("Demo not found"); } await fs9.rm(fullPath, { recursive: true, force: true }); successResponse(res, null); }) ); router10.post( "/rename", validateBody(renamePyDemoSchema), asyncHandler(async (req, res) => { const { oldPath, newName } = req.body; if (!oldPath.startsWith("pydemos/")) { throw new ValidationError("Invalid path"); } if (!/^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$/.test(newName)) { throw new ValidationError("Invalid name format"); } const { fullPath: oldFullPath } = resolveNotebookPath(oldPath); try { await fs9.access(oldFullPath); } catch { throw new NotFoundError("Demo not found"); } const parentDir = path12.dirname(oldFullPath); const newFullPath = path12.join(parentDir, newName); const newPath = toPosixPath2(path12.join(path12.dirname(oldPath), newName)); try { await fs9.access(newFullPath); throw new AlreadyExistsError("Demo with this name already exists"); } catch (err) { if (isNodeError(err) && err.code === "ENOENT") { } else if (err instanceof AlreadyExistsError) { throw err; } else { throw err; } } await fs9.rename(oldFullPath, newFullPath); successResponse(res, { newPath }); }) ); return router10; }; var routes_default8 = createPyDemosRoutes(); // api/modules/document-parser/index.ts import express12 from "express"; // shared/modules/document-parser/index.ts var DOCUMENT_PARSER_MODULE = defineApiModule({ id: "document-parser", name: "Document Parser", basePath: "/document-parser", order: 60, version: "1.0.0", frontend: { enabled: false }, backend: { enabled: true } }); // api/modules/document-parser/blogRoutes.ts import express10 from "express"; import path14 from "path"; import fs11 from "fs/promises"; import { existsSync as existsSync3 } from "fs"; import axios from "axios"; // api/modules/document-parser/documentParser.ts import path13 from "path"; import { spawn } from "child_process"; import fs10 from "fs/promises"; import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs"; if (!existsSync2(TEMP_ROOT)) { mkdirSync2(TEMP_ROOT, { recursive: true }); } var createJobContext = async (prefix) => { const now = /* @__PURE__ */ new Date(); const jobDir = path13.join(TEMP_ROOT, `${prefix}_${formatTimestamp(now)}`); await fs10.mkdir(jobDir, { recursive: true }); const year = now.getFullYear(); const month = pad2(now.getMonth() + 1); const day = pad2(now.getDate()); const imagesSubDir = `images/${year}/${month}/${day}`; const destImagesDir = path13.join(NOTEBOOK_ROOT, imagesSubDir); await fs10.mkdir(destImagesDir, { recursive: true }); return { jobDir, now, imagesSubDir, destImagesDir }; }; var spawnPythonScript = async (options) => { const { scriptPath, args, cwd, inputContent } = options; return new Promise((resolve, reject) => { const pythonProcess = spawn("python", ["-X", "utf8", scriptPath, ...args], { cwd, env: { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" } }); let stdout = ""; let stderr = ""; pythonProcess.stdout.on("data", (data) => { stdout += data.toString(); }); pythonProcess.stderr.on("data", (data) => { stderr += data.toString(); }); pythonProcess.on("close", (code) => { if (code !== 0) { logger.error("Python script error:", stderr); reject(new Error(`Process exited with code ${code}. Error: ${stderr}`)); } else { resolve(stdout); } }); pythonProcess.on("error", (err) => { reject(err); }); if (inputContent !== void 0) { pythonProcess.stdin.write(inputContent); pythonProcess.stdin.end(); } }); }; var findImageDestinations = (md) => { const results = []; let i = 0; while (i < md.length) { const bang = md.indexOf("![", i); if (bang === -1) break; const closeBracket = md.indexOf("]", bang + 2); if (closeBracket === -1) break; if (md[closeBracket + 1] !== "(") { i = closeBracket + 1; continue; } const urlStart = closeBracket + 2; let depth = 1; let j = urlStart; for (; j < md.length; j++) { const ch = md[j]; if (ch === "(") depth++; else if (ch === ")") { depth--; if (depth === 0) break; } } if (depth !== 0) break; results.push({ url: md.slice(urlStart, j), start: urlStart, end: j }); i = j + 1; } return results; }; var applyReplacements = (md, replacements) => { const sorted = [...replacements].sort((a, b) => b.start - a.start); let result = md; for (const r of sorted) { result = `${result.slice(0, r.start)}${r.replacement}${result.slice(r.end)}`; } return result; }; var copyLocalImage = async (src, jobDir, htmlDir, destImagesDir, imagesSubDir, now) => { const s0 = src.trim().replace(/^<|>$/g, ""); if (!s0) return null; let decoded = s0; try { decoded = decodeURI(s0); } catch { } const s1 = decoded.replace(/\\/g, "/"); const s2 = s1.startsWith("./") ? s1.slice(2) : s1; const candidates = s2.startsWith("/") ? [path13.join(jobDir, s2.slice(1)), path13.join(htmlDir, s2.slice(1))] : [path13.resolve(htmlDir, s2), path13.resolve(jobDir, s2)]; let foundFile = null; for (const c of candidates) { if (existsSync2(c)) { foundFile = c; break; } } if (!foundFile) return null; const ext = path13.extname(foundFile) || ".jpg"; const baseName = formatTimestamp(now); const newFilename = await getUniqueFilename(destImagesDir, baseName, ext); const newPath = path13.join(destImagesDir, newFilename); await fs10.copyFile(foundFile, newPath); return { newLink: `/${imagesSubDir}/${newFilename}` }; }; var cleanupJob = async (jobDir, additionalPaths = []) => { await fs10.rm(jobDir, { recursive: true, force: true }).catch(() => { }); for (const p of additionalPaths) { await fs10.unlink(p).catch(() => { }); } }; var getScriptPath = (toolName, scriptName) => { return path13.join(PROJECT_ROOT, "tools", toolName, scriptName); }; var ensureScriptExists = (scriptPath) => { return existsSync2(scriptPath); }; // api/modules/document-parser/blogRoutes.ts var router7 = express10.Router(); var tempDir3 = getTempDir(); router7.post( "/parse-local", asyncHandler(async (req, res) => { const { htmlPath, htmlDir, assetsDirName, assetsFiles, targetPath } = req.body; if (!htmlPath || !htmlDir || !targetPath) { throw new ValidationError("htmlPath, htmlDir and targetPath are required"); } let fullTargetPath; try { const resolved = resolveNotebookPath(targetPath); fullTargetPath = resolved.fullPath; } catch (error) { throw error; } const scriptPath = getScriptPath("blog", "parse_blog.py"); if (!ensureScriptExists(scriptPath)) { throw new InternalError("Parser script not found"); } const jobContext = await createJobContext("blog"); let htmlPathInJob = ""; try { htmlPathInJob = path14.join(jobContext.jobDir, "input.html"); await fs11.copyFile(htmlPath, htmlPathInJob); if (assetsDirName && assetsFiles && assetsFiles.length > 0) { const assetsDirPath = path14.join(htmlDir, assetsDirName); for (const relPath of assetsFiles) { const srcPath = path14.join(assetsDirPath, relPath); if (existsSync3(srcPath)) { const destPath = path14.join(jobContext.jobDir, assetsDirName, relPath); await fs11.mkdir(path14.dirname(destPath), { recursive: true }); await fs11.copyFile(srcPath, destPath); } } } } catch (err) { await cleanupJob(jobContext.jobDir); throw err; } processHtmlInBackground({ jobDir: jobContext.jobDir, htmlPath: htmlPathInJob, targetPath: fullTargetPath, cwd: path14.dirname(scriptPath), jobContext, originalHtmlDir: htmlDir, originalAssetsDirName: assetsDirName }).catch((err) => { logger.error("Background HTML processing failed:", err); fs11.writeFile(fullTargetPath, `# \u89E3\u6790\u5931\u8D25 > \u9519\u8BEF\u4FE1\u606F: ${err.message}`, "utf-8").catch(() => { }); cleanupJob(jobContext.jobDir).catch(() => { }); }); successResponse(res, { message: "HTML parsing started in background.", status: "processing" }); }) ); async function processHtmlInBackground(args) { const { jobDir, htmlPath, targetPath, cwd, jobContext, originalHtmlDir, originalAssetsDirName } = args; try { await spawnPythonScript({ scriptPath: "parse_blog.py", args: [htmlPath], cwd }); const parsedPathObj = path14.parse(htmlPath); const markdownPath = path14.join(parsedPathObj.dir, `${parsedPathObj.name}.md`); if (!existsSync3(markdownPath)) { throw new Error("Markdown result file not found"); } let mdContent = await fs11.readFile(markdownPath, "utf-8"); const ctx = await jobContext; const htmlDir = path14.dirname(htmlPath); const replacements = []; const destinations = findImageDestinations(mdContent); for (const dest of destinations) { const originalSrc = dest.url; if (!originalSrc) continue; if (originalSrc.startsWith("http://") || originalSrc.startsWith("https://")) { try { const response = await axios.get(originalSrc, { responseType: "arraybuffer", timeout: 1e4 }); const contentType = response.headers["content-type"]; let ext = ".jpg"; if (contentType) { if (contentType.includes("png")) ext = ".png"; else if (contentType.includes("gif")) ext = ".gif"; else if (contentType.includes("webp")) ext = ".webp"; else if (contentType.includes("svg")) ext = ".svg"; else if (contentType.includes("jpeg") || contentType.includes("jpg")) ext = ".jpg"; } const urlExt = path14.extname(originalSrc.split("?")[0]); if (urlExt) ext = urlExt; const baseName = formatTimestamp(ctx.now); const newFilename = await getUniqueFilename(ctx.destImagesDir, baseName, ext); const newPath = path14.join(ctx.destImagesDir, newFilename); await fs11.writeFile(newPath, response.data); replacements.push({ start: dest.start, end: dest.end, original: originalSrc, replacement: `/${ctx.imagesSubDir}/${newFilename}` }); } catch { } continue; } if (originalSrc.startsWith("data:")) continue; let result = await copyLocalImage( originalSrc, jobDir, htmlDir, ctx.destImagesDir, ctx.imagesSubDir, ctx.now ); if (!result && originalHtmlDir && originalAssetsDirName) { const srcWithFiles = originalSrc.replace(/^\.\//, "").replace(/^\//, ""); const possiblePaths = [ path14.join(originalHtmlDir, originalAssetsDirName, srcWithFiles), path14.join(originalHtmlDir, originalAssetsDirName, path14.basename(srcWithFiles)) ]; for (const p of possiblePaths) { if (existsSync3(p)) { const ext = path14.extname(p) || ".jpg"; const baseName = formatTimestamp(ctx.now); const newFilename = await getUniqueFilename(ctx.destImagesDir, baseName, ext); const newPath = path14.join(ctx.destImagesDir, newFilename); await fs11.copyFile(p, newPath); result = { newLink: `/${ctx.imagesSubDir}/${newFilename}` }; break; } } } if (result) { replacements.push({ start: dest.start, end: dest.end, original: originalSrc, replacement: result.newLink }); } } mdContent = applyReplacements(mdContent, replacements); await fs11.writeFile(targetPath, mdContent, "utf-8"); await fs11.unlink(markdownPath).catch(() => { }); } finally { await cleanupJob(jobDir); } } // api/modules/document-parser/mineruRoutes.ts import express11 from "express"; import multer2 from "multer"; import path15 from "path"; import fs12 from "fs/promises"; import { existsSync as existsSync4 } from "fs"; var router8 = express11.Router(); var tempDir4 = getTempDir(); var upload2 = multer2({ dest: tempDir4, limits: { fileSize: 50 * 1024 * 1024 } }); router8.post( "/parse", upload2.single("file"), asyncHandler(async (req, res) => { if (!req.file) { throw new ValidationError("File is required"); } const { targetPath } = req.body; if (!targetPath) { await fs12.unlink(req.file.path).catch(() => { }); throw new ValidationError("Target path is required"); } let fullTargetPath; try { const resolved = resolveNotebookPath(targetPath); fullTargetPath = resolved.fullPath; } catch (error) { await fs12.unlink(req.file.path).catch(() => { }); throw error; } const scriptPath = getScriptPath("mineru", "mineru_parser.py"); if (!ensureScriptExists(scriptPath)) { await fs12.unlink(req.file.path).catch(() => { }); throw new InternalError("Parser script not found"); } processPdfInBackground(req.file.path, fullTargetPath, path15.dirname(scriptPath)).catch((err) => { logger.error("Background PDF processing failed:", err); fs12.writeFile(fullTargetPath, `# \u89E3\u6790\u5931\u8D25 > \u9519\u8BEF\u4FE1\u606F: ${err.message}`, "utf-8").catch(() => { }); }); successResponse(res, { message: "PDF upload successful. Parsing started in background.", status: "processing" }); }) ); async function processPdfInBackground(filePath, targetPath, cwd) { try { const output = await spawnPythonScript({ scriptPath: "mineru_parser.py", args: [filePath], cwd }); const match = output.match(/JSON_RESULT:(.*)/); if (!match) { throw new Error("Failed to parse Python script output: JSON_RESULT not found"); } const result = JSON.parse(match[1]); const markdownPath = result.markdown_file; const outputDir = result.output_dir; if (!existsSync4(markdownPath)) { throw new Error("Markdown result file not found"); } let mdContent = await fs12.readFile(markdownPath, "utf-8"); const imagesDir = path15.join(outputDir, "images"); if (existsSync4(imagesDir)) { const jobContext = await createJobContext("pdf_images"); const destinations = findImageDestinations(mdContent); const replacements = []; for (const dest of destinations) { const originalSrc = dest.url; if (!originalSrc) continue; const possibleFilenames = [originalSrc, path15.basename(originalSrc)]; let foundFile = null; for (const fname of possibleFilenames) { const localPath = path15.join(imagesDir, fname); if (existsSync4(localPath)) { foundFile = localPath; break; } const directPath = path15.join(outputDir, originalSrc); if (existsSync4(directPath)) { foundFile = directPath; break; } } if (foundFile) { const ext = path15.extname(foundFile); const baseName = formatTimestamp(jobContext.now); const newFilename = await getUniqueFilename(jobContext.destImagesDir, baseName, ext); const newPath = path15.join(jobContext.destImagesDir, newFilename); await fs12.copyFile(foundFile, newPath); replacements.push({ start: dest.start, end: dest.end, original: originalSrc, replacement: `${jobContext.imagesSubDir}/${newFilename}` }); } } mdContent = applyReplacements(mdContent, replacements); } await fs12.writeFile(targetPath, mdContent, "utf-8"); await fs12.unlink(markdownPath).catch(() => { }); if (outputDir && outputDir.includes("temp")) { await fs12.rm(outputDir, { recursive: true, force: true }).catch(() => { }); } } finally { await fs12.unlink(filePath).catch(() => { }); } } // shared/modules/ai/index.ts var AI_MODULE = defineApiModule({ id: "ai", name: "AI", basePath: "/ai", order: 70, version: "1.0.0", frontend: { enabled: false }, backend: { enabled: true } }); // api/modules/ai/routes.ts import express13 from "express"; import { spawn as spawn2 } from "child_process"; import path16 from "path"; import { fileURLToPath as fileURLToPath2 } from "url"; import fs13 from "fs/promises"; import fsSync from "fs"; var __filename2 = fileURLToPath2(import.meta.url); var __dirname2 = path16.dirname(__filename2); var router9 = express13.Router(); var PYTHON_TIMEOUT_MS = 3e4; var spawnPythonWithTimeout = (scriptPath, args, stdinContent, timeoutMs = PYTHON_TIMEOUT_MS) => { return new Promise((resolve, reject) => { const pythonProcess = spawn2("python", args, { env: { ...process.env } }); let stdout = ""; let stderr = ""; let timeoutId = null; const cleanup = () => { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } }; timeoutId = setTimeout(() => { cleanup(); pythonProcess.kill(); reject(new Error(`Python script timed out after ${timeoutMs}ms`)); }, timeoutMs); pythonProcess.stdout.on("data", (data) => { stdout += data.toString(); }); pythonProcess.stderr.on("data", (data) => { stderr += data.toString(); }); pythonProcess.on("close", (code) => { cleanup(); if (code !== 0) { reject(new Error(`Python script exited with code ${code}. Stderr: ${stderr}`)); } else { resolve(stdout); } }); pythonProcess.on("error", (err) => { cleanup(); reject(new Error(`Failed to start python process: ${err.message}`)); }); pythonProcess.stdin.write(stdinContent); pythonProcess.stdin.end(); }); }; router9.post( "/doubao", asyncHandler(async (req, res) => { const { task, path: relPath } = req.body; if (!task) throw new ValidationError("Task is required"); if (!relPath) throw new ValidationError("Path is required"); const { fullPath } = resolveNotebookPath(relPath); try { await fs13.access(fullPath); } catch { throw new NotFoundError("File not found"); } const content = await fs13.readFile(fullPath, "utf-8"); const projectRoot = path16.resolve(__dirname2, "..", "..", ".."); const scriptPath = path16.join(projectRoot, "tools", "doubao", "main.py"); if (!fsSync.existsSync(scriptPath)) { throw new InternalError(`Python script not found: ${scriptPath}`); } try { const result = await spawnPythonWithTimeout(scriptPath, ["--task", task], content); await fs13.writeFile(fullPath, result, "utf-8"); successResponse(res, { message: "Task completed successfully" }); } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; throw new InternalError(`AI task failed: ${message}`); } }) ); // shared/modules/remote/api.ts var REMOTE_ENDPOINTS = defineEndpoints({ getConfig: { path: "/config", method: "GET" }, saveConfig: { path: "/config", method: "POST" }, getScreenshot: { path: "/screenshot", method: "GET" }, saveScreenshot: { path: "/screenshot", method: "POST" }, getData: { path: "/data", method: "GET" }, saveData: { path: "/data", method: "POST" } }); // shared/modules/remote/index.ts var REMOTE_MODULE = defineApiModule({ id: "remote", name: "\u8FDC\u7A0B", basePath: "/remote", order: 25, version: "1.0.0", endpoints: REMOTE_ENDPOINTS }); // api/modules/remote/service.ts import fs14 from "fs/promises"; import path17 from "path"; var REMOTE_DIR = "remote"; var RemoteService = class { constructor(deps = {}) { this.deps = deps; } getRemoteDir() { const { fullPath } = resolveNotebookPath(REMOTE_DIR); return { relPath: REMOTE_DIR, fullPath }; } getDeviceDir(deviceName) { const safeName = this.sanitizeFileName(deviceName); const { fullPath } = resolveNotebookPath(path17.join(REMOTE_DIR, safeName)); return { relPath: path17.join(REMOTE_DIR, safeName), fullPath }; } sanitizeFileName(name) { return name.replace(/[<>:"/\\|?*]/g, "_").trim() || "unnamed"; } getDeviceConfigPath(deviceName) { const { fullPath } = this.getDeviceDir(deviceName); return path17.join(fullPath, "config.json"); } getDeviceScreenshotPath(deviceName) { const { fullPath } = this.getDeviceDir(deviceName); return path17.join(fullPath, "screenshot.png"); } getDeviceDataPath(deviceName) { const { fullPath } = this.getDeviceDir(deviceName); return path17.join(fullPath, "data.json"); } async ensureDir(dirPath) { await fs14.mkdir(dirPath, { recursive: true }); } async getDeviceNames() { const { fullPath } = this.getRemoteDir(); try { const entries = await fs14.readdir(fullPath, { withFileTypes: true }); const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name); return dirs; } catch { return []; } } async getConfig() { const deviceNames = await this.getDeviceNames(); const devices = await Promise.all( deviceNames.map(async (name) => { try { const configPath = this.getDeviceConfigPath(name); const content = await fs14.readFile(configPath, "utf-8"); const deviceConfig = JSON.parse(content); return { id: deviceConfig.id || name, deviceName: name, serverHost: deviceConfig.serverHost || "", desktopPort: deviceConfig.desktopPort || 3e3, gitPort: deviceConfig.gitPort || 3001 }; } catch { return { id: name, deviceName: name, serverHost: "", desktopPort: 3e3, gitPort: 3001 }; } }) ); return { devices }; } async saveConfig(config2) { const { fullPath: remoteDirFullPath } = this.getRemoteDir(); await this.ensureDir(remoteDirFullPath); const existingDevices = await this.getDeviceNames(); const newDeviceNames = config2.devices.map((d) => this.sanitizeFileName(d.deviceName)); for (const oldDevice of existingDevices) { if (!newDeviceNames.includes(oldDevice)) { try { const oldDir = path17.join(remoteDirFullPath, oldDevice); await fs14.rm(oldDir, { recursive: true, force: true }); } catch { } } } for (const device of config2.devices) { const deviceDir = this.getDeviceDir(device.deviceName); await this.ensureDir(deviceDir.fullPath); const deviceConfigPath = this.getDeviceConfigPath(device.deviceName); const deviceConfig = { id: device.id, serverHost: device.serverHost, desktopPort: device.desktopPort, gitPort: device.gitPort }; await fs14.writeFile(deviceConfigPath, JSON.stringify(deviceConfig, null, 2), "utf-8"); } } async getScreenshot(deviceName) { if (!deviceName) { return null; } const screenshotPath = this.getDeviceScreenshotPath(deviceName); try { return await fs14.readFile(screenshotPath); } catch { return null; } } async saveScreenshot(dataUrl, deviceName) { console.log("[RemoteService] saveScreenshot:", { deviceName, dataUrlLength: dataUrl?.length }); if (!deviceName || deviceName.trim() === "") { console.warn("[RemoteService] saveScreenshot skipped: no deviceName"); return; } const deviceDir = this.getDeviceDir(deviceName); await this.ensureDir(deviceDir.fullPath); const base64Data = dataUrl.replace(/^data:image\/png;base64,/, ""); const buffer = Buffer.from(base64Data, "base64"); const screenshotPath = this.getDeviceScreenshotPath(deviceName); await fs14.writeFile(screenshotPath, buffer); } async getData(deviceName) { if (!deviceName || deviceName.trim() === "") { return null; } const dataPath = this.getDeviceDataPath(deviceName); try { const content = await fs14.readFile(dataPath, "utf-8"); return JSON.parse(content); } catch { return null; } } async saveData(data, deviceName) { if (!deviceName || deviceName.trim() === "") { console.warn("[RemoteService] saveData skipped: no deviceName"); return; } const deviceDir = this.getDeviceDir(deviceName); await this.ensureDir(deviceDir.fullPath); const dataPath = this.getDeviceDataPath(deviceName); await fs14.writeFile(dataPath, JSON.stringify(data, null, 2), "utf-8"); } }; // api/modules/remote/routes.ts import express14 from "express"; var createRemoteRoutes = (deps) => { const router10 = express14.Router(); const { remoteService: remoteService2 } = deps; router10.get( "/config", asyncHandler(async (req, res) => { const config2 = await remoteService2.getConfig(); successResponse(res, config2); }) ); router10.post( "/config", asyncHandler(async (req, res) => { const config2 = req.body; await remoteService2.saveConfig(config2); successResponse(res, null); }) ); router10.get( "/screenshot", asyncHandler(async (req, res) => { const deviceName = req.query.device; const buffer = await remoteService2.getScreenshot(deviceName); if (!buffer) { return successResponse(res, ""); } const base64 = `data:image/png;base64,${buffer.toString("base64")}`; successResponse(res, base64); }) ); router10.post( "/screenshot", asyncHandler(async (req, res) => { const { dataUrl, deviceName } = req.body; console.log("[Remote] saveScreenshot called:", { deviceName, hasDataUrl: !!dataUrl }); await remoteService2.saveScreenshot(dataUrl, deviceName); successResponse(res, null); }) ); router10.get( "/data", asyncHandler(async (req, res) => { const deviceName = req.query.device; const data = await remoteService2.getData(deviceName); successResponse(res, data); }) ); router10.post( "/data", asyncHandler(async (req, res) => { const { deviceName, lastConnected } = req.body; const data = {}; if (lastConnected !== void 0) { data.lastConnected = lastConnected; } await remoteService2.saveData(data, deviceName); successResponse(res, null); }) ); return router10; }; var remoteService = new RemoteService(); var routes_default9 = createRemoteRoutes({ remoteService }); // import("./**/*/index.js") in api/modules/index.ts var globImport_index_js = __glob({}); // api/modules/index.ts var __filename3 = fileURLToPath3(import.meta.url); var __dirname3 = dirname(__filename3); var moduleFactoryPattern = /^create\w+Module$/; async function discoverModules() { const modules = []; const entries = readdirSync(__dirname3); for (const entry of entries) { const entryPath = join(__dirname3, entry); try { const stats = statSync(entryPath); if (!stats.isDirectory()) { continue; } const moduleIndexPath = join(entryPath, "index.ts"); let moduleIndexStats; try { moduleIndexStats = statSync(moduleIndexPath); } catch { continue; } if (!moduleIndexStats.isFile()) { continue; } const moduleExports = await globImport_index_js(`./${entry}/index.js`); for (const exportName of Object.keys(moduleExports)) { if (moduleFactoryPattern.test(exportName)) { const factory = moduleExports[exportName]; if (typeof factory === "function") { const module = factory(); modules.push(module); } } } } catch (error) { console.warn(`[ModuleLoader] Failed to load module '${entry}':`, error); } } modules.sort((a, b) => { const orderA = a.metadata.order ?? 0; const orderB = b.metadata.order ?? 0; return orderA - orderB; }); return modules; } var apiModules = await discoverModules(); // api/infra/moduleValidator.ts import { readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync5 } from "fs"; import { join as join2, dirname as dirname2 } from "path"; import { fileURLToPath as fileURLToPath4 } from "url"; // import("../../shared/modules/**/*/index.js") in api/infra/moduleValidator.ts var globImport_shared_modules_index_js = __glob({}); // api/infra/moduleValidator.ts var __filename4 = fileURLToPath4(import.meta.url); var __dirname4 = dirname2(__filename4); function getSharedModulesPath() { const possiblePaths = [ join2(__dirname4, "../../shared/modules"), join2(__dirname4, "../../../shared/modules"), join2(process?.resourcesPath || "", "shared/modules") ]; for (const p of possiblePaths) { if (existsSync5(p)) { return p; } } return null; } function needsBackendImplementation(moduleDef) { return moduleDef.backend?.enabled !== false; } async function loadModuleDefinitions() { const modules = []; const sharedModulesPath = getSharedModulesPath(); if (!sharedModulesPath) { return modules; } const entries = readdirSync2(sharedModulesPath); for (const entry of entries) { const entryPath = join2(sharedModulesPath, entry); const stat = statSync2(entryPath); if (!stat.isDirectory()) { continue; } try { const moduleExports = await globImport_shared_modules_index_js(`../../shared/modules/${entry}/index.js`); for (const key of Object.keys(moduleExports)) { if (key.endsWith("_MODULE")) { const moduleDef = moduleExports[key]; if (moduleDef && moduleDef.id) { modules.push({ id: moduleDef.id, name: moduleDef.name, backend: moduleDef.backend }); } } } } catch { } } return modules; } async function validateModuleConsistency(apiModules2) { const sharedModules = await loadModuleDefinitions(); if (sharedModules.length === 0) { console.log("[ModuleValidator] Skipping validation (shared modules not found, likely packaged mode)"); return; } const apiModuleIds = new Set(apiModules2.map((m) => m.metadata.id)); const errors = []; for (const sharedModule of sharedModules) { const needsBackend = needsBackendImplementation(sharedModule); const hasApiModule = apiModuleIds.has(sharedModule.id); if (needsBackend && !hasApiModule) { errors.push( `Module '${sharedModule.id}' is defined in shared but not registered in API modules` ); } if (!needsBackend && hasApiModule) { errors.push( `Module '${sharedModule.id}' has backend disabled but is registered in API modules` ); } } if (errors.length > 0) { throw new Error(`Module consistency validation failed: - ${errors.join("\n - ")}`); } console.log( `[ModuleValidator] \u2713 Module consistency validated: ${sharedModules.length} shared, ${apiModules2.length} API` ); } // api/app.ts import path18 from "path"; import fs15 from "fs"; dotenv.config(); var app = express15(); var container = new ServiceContainer(); var moduleManager = new ModuleManager(container); app.use(cors()); app.use(express15.json({ limit: "200mb" })); app.use(express15.urlencoded({ extended: true, limit: "200mb" })); app.use("/api/files", routes_default); app.use("/api/events", routes_default2); app.use("/api/settings", routes_default3); app.use("/api/upload", routes_default4); app.use("/api/search", routes_default5); for (const module of apiModules) { await moduleManager.register(module); } await validateModuleConsistency(apiModules); for (const module of moduleManager.getAllModules()) { await moduleManager.activate(module.metadata.id); const router10 = await module.createRouter(container); app.use("/api" + module.metadata.basePath, router10); } app.get("/background.png", (req, res, next) => { const customBgPath = path18.join(NOTEBOOK_ROOT, ".config", "background.png"); if (fs15.existsSync(customBgPath)) { res.sendFile(customBgPath); } else { next(); } }); app.use( "/api/health", (_req, res) => { const response = { success: true, data: { message: "ok" } }; res.status(200).json(response); } ); app.use((req, res, next) => { if (req.path.startsWith("/api")) { const response = { success: false, error: { code: "NOT_FOUND", message: "API\u4E0D\u5B58\u5728" } }; res.status(404).json(response); } else { next(); } }); app.use(errorHandler); var app_default = app; // electron/server.ts import path20 from "path"; import express16 from "express"; import { fileURLToPath as fileURLToPath5 } from "url"; // api/watcher/watcher.ts import chokidar from "chokidar"; import path19 from "path"; var watcher = null; var startWatcher = () => { if (watcher) return; logger.info(`Starting file watcher for: ${NOTEBOOK_ROOT}`); watcher = chokidar.watch(NOTEBOOK_ROOT, { ignored: /(^|[\/\\])\../, persistent: true, ignoreInitial: true }); const broadcast = (event, changedPath) => { const rel = path19.relative(NOTEBOOK_ROOT, changedPath); if (!rel || rel.startsWith("..") || path19.isAbsolute(rel)) return; logger.info(`File event: ${event} - ${rel}`); eventBus.broadcast({ event, path: toPosixPath(rel) }); }; watcher.on("add", (p) => broadcast("add", p)).on("change", (p) => broadcast("change", p)).on("unlink", (p) => broadcast("unlink", p)).on("addDir", (p) => broadcast("addDir", p)).on("unlinkDir", (p) => broadcast("unlinkDir", p)).on("ready", () => logger.info("File watcher ready")).on("error", (err) => logger.error("File watcher error:", err)); }; // electron/server.ts var __filename5 = fileURLToPath5(import.meta.url); var __dirname5 = path20.dirname(__filename5); startWatcher(); var distPath = path20.join(__dirname5, "../dist"); app_default.use(express16.static(distPath)); app_default.get("*", (req, res) => { res.sendFile(path20.join(distPath, "index.html")); }); var startServer = () => { return new Promise((resolve, reject) => { const server = app_default.listen(0, () => { const address = server.address(); const port = address.port; logger.info(`Electron internal server running on port ${port}`); resolve(port); }); server.on("error", (err) => { logger.error("Failed to start server:", err); reject(err); }); }); }; export { startServer };