Files
XCDesktop/dist-api/server.js
2026-03-08 01:34:54 +08:00

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
};