3027 lines
95 KiB
JavaScript
3027 lines
95 KiB
JavaScript
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
|
|
};
|