chore: 移除构建产物,保持仓库精简

This commit is contained in:
2026-03-13 16:09:35 +08:00
parent 3e360c1807
commit 517592e216
24 changed files with 0 additions and 6569 deletions

View File

@@ -1,12 +0,0 @@
import {
ai_default,
createAiModule,
createAiRoutes
} from "./chunk-TSQNCXAS.js";
import "./chunk-ER4KPD22.js";
import "./chunk-74TMTGBG.js";
export {
createAiModule,
createAiRoutes,
ai_default as default
};

View File

@@ -1,15 +0,0 @@
// 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();
export {
logger
};

View File

@@ -1,39 +0,0 @@
import {
ValidationError
} from "./chunk-ER4KPD22.js";
// 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);
}
}
};
};
export {
validateBody,
validateQuery
};

View File

@@ -1,110 +0,0 @@
var __glob = (map) => (path2) => {
var fn = map[path2];
if (fn) return fn();
throw new Error("Module not found in bundle: " + path2);
};
// api/infra/createModule.ts
function createApiModule(config2, options) {
const metadata = {
id: config2.id,
name: config2.name,
version: config2.version,
basePath: config2.basePath,
order: config2.order,
dependencies: config2.dependencies
};
const lifecycle = options.lifecycle ? options.lifecycle : options.services ? {
onLoad: options.services
} : void 0;
return {
metadata,
lifecycle,
createRouter: options.routes
};
}
// 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/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(), "xcdesktop_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/modules/types.ts
function defineApiModule(config2) {
return config2;
}
function defineEndpoints(endpoints) {
return endpoints;
}
export {
__glob,
asyncHandler,
successResponse,
config,
PATHS,
PROJECT_ROOT,
NOTEBOOK_ROOT,
TEMP_ROOT,
createApiModule,
defineApiModule,
defineEndpoints
};

View File

@@ -1,163 +0,0 @@
import {
NOTEBOOK_ROOT
} from "./chunk-74TMTGBG.js";
// 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
import path from "path";
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 = path.resolve(NOTEBOOK_ROOT);
const fullPath = path.resolve(notebookRoot, safeRelPath);
if (!fullPath.startsWith(notebookRoot)) {
throw new AccessDeniedError("Access denied");
}
return { safeRelPath, fullPath };
};
export {
ValidationError,
NotFoundError,
BadRequestError,
NotADirectoryError,
AlreadyExistsError,
ForbiddenError,
UnsupportedMediaTypeError,
ResourceLockedError,
InternalError,
isAppError,
isNodeError,
resolveNotebookPath
};

View File

@@ -1,20 +0,0 @@
import {
PATHS
} from "./chunk-74TMTGBG.js";
// 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;
};
export {
getTempDir
};

View File

@@ -1,234 +0,0 @@
import {
AlreadyExistsError,
BadRequestError,
NotFoundError,
ValidationError,
resolveNotebookPath
} from "./chunk-ER4KPD22.js";
import {
asyncHandler,
createApiModule,
defineApiModule,
defineEndpoints,
successResponse
} from "./chunk-74TMTGBG.js";
// 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 express from "express";
import fs2 from "fs/promises";
import path2 from "path";
// api/modules/recycle-bin/recycleBinService.ts
import fs from "fs/promises";
import path from "path";
async function restoreFile(srcPath, destPath, deletedDate, year, month, day) {
const { fullPath: imagesDir } = resolveNotebookPath(`images/${year}/${month}/${day}`);
let content = await fs.readFile(srcPath, "utf-8");
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
let match;
const imageReplacements = [];
while ((match = imageRegex.exec(content)) !== null) {
const imagePath = match[2];
const imageName = path.basename(imagePath);
const rbImageName = `${deletedDate}_${imageName}`;
const { fullPath: srcImagePath } = resolveNotebookPath(`RB/${rbImageName}`);
try {
await fs.access(srcImagePath);
await fs.mkdir(imagesDir, { recursive: true });
const destImagePath = path.join(imagesDir, imageName);
await fs.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 fs.writeFile(destPath, content, "utf-8");
await fs.unlink(srcPath);
}
async function restoreFolder(srcPath, destPath, deletedDate, year, month, day) {
await fs.mkdir(destPath, { recursive: true });
const entries = await fs.readdir(srcPath, { withFileTypes: true });
for (const entry of entries) {
const srcEntryPath = path.join(srcPath, entry.name);
const destEntryPath = path.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 fs.rename(srcEntryPath, destEntryPath);
}
}
const remaining = await fs.readdir(srcPath);
if (remaining.length === 0) {
await fs.rmdir(srcPath);
}
}
// api/modules/recycle-bin/routes.ts
var router = express.Router();
router.get(
"/",
asyncHandler(async (req, res) => {
const { fullPath: rbDir } = resolveNotebookPath("RB");
try {
await fs2.access(rbDir);
} catch {
successResponse(res, { groups: [] });
return;
}
const entries = await fs2.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 });
})
);
router.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 fs2.access(itemPath);
} catch {
throw new NotFoundError("Item not found in recycle bin");
}
const match = path2.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 fs2.mkdir(markdownsDir, { recursive: true });
const destPath = path2.join(markdownsDir, originalName);
const existing = await fs2.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);
})
);
router.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 fs2.access(itemPath);
} catch {
throw new NotFoundError("Item not found in recycle bin");
}
if (type === "dir") {
await fs2.rm(itemPath, { recursive: true, force: true });
} else {
await fs2.unlink(itemPath);
}
successResponse(res, null);
})
);
router.delete(
"/empty",
asyncHandler(async (req, res) => {
const { fullPath: rbDir } = resolveNotebookPath("RB");
try {
await fs2.access(rbDir);
} catch {
successResponse(res, null);
return;
}
const entries = await fs2.readdir(rbDir, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path2.join(rbDir, entry.name);
if (entry.isDirectory()) {
await fs2.rm(entryPath, { recursive: true, force: true });
} else {
await fs2.unlink(entryPath);
}
}
successResponse(res, null);
})
);
var routes_default = router;
// api/modules/recycle-bin/index.ts
var createRecycleBinModule = () => {
return createApiModule(RECYCLE_BIN_MODULE, {
routes: (_container) => {
return routes_default;
}
});
};
var recycle_bin_default = createRecycleBinModule;
export {
restoreFile,
restoreFolder,
createRecycleBinModule,
recycle_bin_default
};

View File

@@ -1,912 +0,0 @@
import {
logger
} from "./chunk-47DJ6YUB.js";
import {
NOTEBOOK_ROOT,
asyncHandler,
createApiModule,
defineApiModule,
defineEndpoints,
successResponse
} from "./chunk-74TMTGBG.js";
// 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
});
// shared/utils/tabType.ts
var KNOWN_MODULE_IDS = [
"home",
"settings",
"search",
"weread",
"recycle-bin",
"todo",
"time-tracking",
"pydemos"
];
function getTabTypeFromPath(filePath) {
if (!filePath) return "other";
if (filePath.startsWith("file-transfer-panel")) {
return "file-transfer";
}
if (filePath.startsWith("remote-git://")) {
return "remote-git";
}
if (filePath.startsWith("remote-desktop://")) {
return "remote-desktop";
}
if (filePath.startsWith("remote-") && filePath !== "remote-tab") {
return "remote-desktop";
}
if (filePath === "remote-tab" || filePath === "remote") {
return "remote";
}
for (const moduleId of KNOWN_MODULE_IDS) {
if (filePath === `${moduleId}-tab` || filePath === moduleId) {
if (moduleId === "home" || moduleId === "settings" || moduleId === "search" || moduleId === "weread") {
return "other";
}
return moduleId;
}
}
if (filePath.endsWith(".md")) {
return "markdown";
}
return "other";
}
function getFileNameFromPath(filePath) {
if (!filePath) return "\u672A\u77E5";
if (filePath.startsWith("file-transfer-panel")) {
const params = new URLSearchParams(filePath.split("?")[1] || "");
const deviceName = params.get("device") || "";
return deviceName ? `\u6587\u4EF6\u4F20\u8F93 - ${deviceName}` : "\u6587\u4EF6\u4F20\u8F93";
}
for (const moduleId of KNOWN_MODULE_IDS) {
if (filePath === `${moduleId}-tab` || filePath === moduleId) {
const names = {
"home": "\u9996\u9875",
"settings": "\u8BBE\u7F6E",
"search": "\u641C\u7D22",
"weread": "\u5FAE\u4FE1\u8BFB\u4E66",
"recycle-bin": "\u56DE\u6536\u7AD9",
"todo": "TODO",
"time-tracking": "\u65F6\u95F4\u7EDF\u8BA1",
"pydemos": "Python Demo",
"remote": "\u8FDC\u7A0B\u684C\u9762"
};
return names[moduleId] ?? moduleId;
}
}
const parts = filePath.split("/");
return parts[parts.length - 1] || filePath;
}
// api/modules/time-tracking/heartbeatService.ts
var DEFAULT_HEARTBEAT_INTERVAL = 6e4;
var HeartbeatService = class {
interval = null;
lastHeartbeat = /* @__PURE__ */ new Date();
intervalMs;
callback = null;
constructor(intervalMs = DEFAULT_HEARTBEAT_INTERVAL) {
this.intervalMs = intervalMs;
}
setCallback(callback) {
this.callback = callback;
}
start() {
if (this.interval) {
this.stop();
}
this.interval = setInterval(async () => {
if (this.callback) {
try {
this.lastHeartbeat = /* @__PURE__ */ new Date();
await this.callback();
} catch (err) {
logger.error("Heartbeat callback failed:", err);
}
}
}, this.intervalMs);
this.lastHeartbeat = /* @__PURE__ */ new Date();
}
stop() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
isRunning() {
return this.interval !== null;
}
getLastHeartbeat() {
return this.lastHeartbeat;
}
updateHeartbeat() {
this.lastHeartbeat = /* @__PURE__ */ new Date();
}
getState() {
return {
lastHeartbeat: this.lastHeartbeat,
isRunning: this.isRunning()
};
}
restoreState(state) {
this.lastHeartbeat = new Date(state.lastHeartbeat);
}
};
var createHeartbeatService = (intervalMs) => {
return new HeartbeatService(intervalMs);
};
// api/modules/time-tracking/sessionPersistence.ts
import fs from "fs/promises";
import path from "path";
var TIME_ROOT = path.join(NOTEBOOK_ROOT, "time");
var getDayFilePath = (year, month, day) => {
const monthStr = month.toString().padStart(2, "0");
const dayStr = day.toString().padStart(2, "0");
return path.join(TIME_ROOT, year.toString(), `${year}${monthStr}${dayStr}.json`);
};
var getMonthFilePath = (year, month) => {
const monthStr = month.toString().padStart(2, "0");
return path.join(TIME_ROOT, year.toString(), `${year}${monthStr}.json`);
};
var getYearFilePath = (year) => {
return path.join(TIME_ROOT, "summary", `${year}.json`);
};
var ensureDirExists = async (filePath) => {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
};
var createEmptyDayData = (year, month, day) => ({
date: `${year}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")}`,
totalDuration: 0,
sessions: [],
tabSummary: {},
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
});
var createEmptyMonthData = (year, month) => ({
year,
month,
days: {},
monthlyTotal: 0,
averageDaily: 0,
activeDays: 0,
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
});
var createEmptyYearData = (year) => ({
year,
months: {},
yearlyTotal: 0,
averageMonthly: 0,
averageDaily: 0,
totalActiveDays: 0
});
var SessionPersistenceService = class {
stateFilePath;
constructor() {
this.stateFilePath = path.join(TIME_ROOT, ".current-session.json");
}
async loadCurrentState() {
try {
const content = await fs.readFile(this.stateFilePath, "utf-8");
const state = JSON.parse(content);
return {
session: state.session || null,
currentTabRecord: state.currentTabRecord || null,
isPaused: state.isPaused || false,
lastHeartbeat: state.lastHeartbeat || (/* @__PURE__ */ new Date()).toISOString()
};
} catch (err) {
logger.debug("No existing session to load or session file corrupted");
return {
session: null,
currentTabRecord: null,
isPaused: false,
lastHeartbeat: (/* @__PURE__ */ new Date()).toISOString()
};
}
}
async saveCurrentState(state) {
await ensureDirExists(this.stateFilePath);
await fs.writeFile(this.stateFilePath, JSON.stringify({
session: state.session,
currentTabRecord: state.currentTabRecord,
isPaused: state.isPaused,
lastHeartbeat: state.lastHeartbeat
}), "utf-8");
}
async clearCurrentState() {
try {
await fs.unlink(this.stateFilePath);
} catch (err) {
logger.debug("Session state file already removed or does not exist");
}
}
async saveSessionToDay(session) {
const startTime = new Date(session.startTime);
const year = startTime.getFullYear();
const month = startTime.getMonth() + 1;
const day = startTime.getDate();
const filePath = getDayFilePath(year, month, day);
await ensureDirExists(filePath);
let dayData = await this.getDayData(year, month, day);
dayData.sessions.push(session);
dayData.totalDuration += session.duration;
for (const record of session.tabRecords) {
const key = record.filePath || record.fileName;
if (!dayData.tabSummary[key]) {
dayData.tabSummary[key] = {
fileName: record.fileName,
tabType: record.tabType,
totalDuration: 0,
focusCount: 0
};
}
dayData.tabSummary[key].totalDuration += record.duration;
dayData.tabSummary[key].focusCount += record.focusedPeriods.length;
}
dayData.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
await fs.writeFile(filePath, JSON.stringify(dayData, null, 2), "utf-8");
await this.updateMonthSummary(year, month, day, session.duration);
await this.updateYearSummary(year, month, session.duration);
}
async getDayData(year, month, day) {
const filePath = getDayFilePath(year, month, day);
try {
const content = await fs.readFile(filePath, "utf-8");
return JSON.parse(content);
} catch (err) {
return createEmptyDayData(year, month, day);
}
}
async getMonthData(year, month) {
const filePath = getMonthFilePath(year, month);
try {
const content = await fs.readFile(filePath, "utf-8");
const data = JSON.parse(content);
data.activeDays = Object.values(data.days).filter((d) => d.totalDuration > 0).length;
return data;
} catch (err) {
return createEmptyMonthData(year, month);
}
}
async getYearData(year) {
const filePath = getYearFilePath(year);
try {
const content = await fs.readFile(filePath, "utf-8");
const data = JSON.parse(content);
data.totalActiveDays = Object.values(data.months).filter((m) => m.totalDuration > 0).length;
return data;
} catch (err) {
return createEmptyYearData(year);
}
}
async updateDayDataRealtime(year, month, day, session, currentTabRecord) {
const filePath = getDayFilePath(year, month, day);
await ensureDirExists(filePath);
let dayData = await this.getDayData(year, month, day);
const currentSessionDuration = session.tabRecords.reduce((sum, r) => sum + r.duration, 0) + (currentTabRecord?.duration || 0);
const existingSessionIndex = dayData.sessions.findIndex((s) => s.id === session.id);
const realtimeSession = {
...session,
duration: currentSessionDuration,
tabRecords: currentTabRecord ? [...session.tabRecords, currentTabRecord] : session.tabRecords
};
if (existingSessionIndex >= 0) {
const oldDuration = dayData.sessions[existingSessionIndex].duration;
dayData.sessions[existingSessionIndex] = realtimeSession;
dayData.totalDuration = dayData.totalDuration - oldDuration + currentSessionDuration;
} else {
dayData.sessions.push(realtimeSession);
dayData.totalDuration += currentSessionDuration;
}
dayData.tabSummary = {};
for (const s of dayData.sessions) {
for (const record of s.tabRecords) {
const key = record.filePath || record.fileName;
if (!dayData.tabSummary[key]) {
dayData.tabSummary[key] = {
fileName: record.fileName,
tabType: record.tabType,
totalDuration: 0,
focusCount: 0
};
}
dayData.tabSummary[key].totalDuration += record.duration;
dayData.tabSummary[key].focusCount += record.focusedPeriods.length;
}
}
dayData.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
await fs.writeFile(filePath, JSON.stringify(dayData, null, 2), "utf-8");
return dayData;
}
async updateMonthSummary(year, month, day, duration) {
const filePath = getMonthFilePath(year, month);
await ensureDirExists(filePath);
let monthData = await this.getMonthData(year, month);
const dayStr = day.toString().padStart(2, "0");
if (!monthData.days[dayStr]) {
monthData.days[dayStr] = { totalDuration: 0, sessions: 0, topTabs: [] };
}
monthData.days[dayStr].totalDuration += duration;
monthData.days[dayStr].sessions += 1;
monthData.monthlyTotal += duration;
monthData.activeDays = Object.values(monthData.days).filter((d) => d.totalDuration > 0).length;
monthData.averageDaily = monthData.activeDays > 0 ? Math.floor(monthData.monthlyTotal / monthData.activeDays) : 0;
monthData.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
await fs.writeFile(filePath, JSON.stringify(monthData, null, 2), "utf-8");
}
async updateYearSummary(year, month, duration) {
const filePath = getYearFilePath(year);
await ensureDirExists(filePath);
let yearData = await this.getYearData(year);
const monthStr = month.toString().padStart(2, "0");
if (!yearData.months[monthStr]) {
yearData.months[monthStr] = { totalDuration: 0, activeDays: 0 };
}
yearData.months[monthStr].totalDuration += duration;
yearData.yearlyTotal += duration;
yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => {
const hasActiveDays = m.totalDuration > 0 ? 1 : 0;
return sum + hasActiveDays;
}, 0);
const activeMonthCount = Object.values(yearData.months).filter((m) => m.totalDuration > 0).length;
yearData.averageMonthly = activeMonthCount > 0 ? Math.floor(yearData.yearlyTotal / activeMonthCount) : 0;
yearData.averageDaily = yearData.totalActiveDays > 0 ? Math.floor(yearData.yearlyTotal / yearData.totalActiveDays) : 0;
await fs.writeFile(filePath, JSON.stringify(yearData, null, 2), "utf-8");
}
async recalculateMonthSummary(year, month, todayDuration) {
const monthFilePath = getMonthFilePath(year, month);
await ensureDirExists(monthFilePath);
let monthData = await this.getMonthData(year, month);
const dayStr = (/* @__PURE__ */ new Date()).getDate().toString().padStart(2, "0");
if (!monthData.days[dayStr]) {
monthData.days[dayStr] = { totalDuration: 0, sessions: 0, topTabs: [] };
}
const oldDayDuration = monthData.days[dayStr].totalDuration;
monthData.days[dayStr].totalDuration = todayDuration;
monthData.monthlyTotal = monthData.monthlyTotal - oldDayDuration + todayDuration;
monthData.activeDays = Object.values(monthData.days).filter((d) => d.totalDuration > 0).length;
monthData.averageDaily = monthData.activeDays > 0 ? Math.floor(monthData.monthlyTotal / monthData.activeDays) : 0;
monthData.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
await fs.writeFile(monthFilePath, JSON.stringify(monthData, null, 2), "utf-8");
}
async recalculateYearSummary(year) {
const yearFilePath = getYearFilePath(year);
await ensureDirExists(yearFilePath);
let yearData = await this.getYearData(year);
const monthStr = ((/* @__PURE__ */ new Date()).getMonth() + 1).toString().padStart(2, "0");
const monthFilePath = getMonthFilePath(year, (/* @__PURE__ */ new Date()).getMonth() + 1);
try {
const monthContent = await fs.readFile(monthFilePath, "utf-8");
const monthData = JSON.parse(monthContent);
if (!yearData.months[monthStr]) {
yearData.months[monthStr] = { totalDuration: 0, activeDays: 0 };
}
const oldMonthTotal = yearData.months[monthStr].totalDuration;
yearData.months[monthStr].totalDuration = monthData.monthlyTotal;
yearData.months[monthStr].activeDays = monthData.activeDays;
yearData.yearlyTotal = yearData.yearlyTotal - oldMonthTotal + monthData.monthlyTotal;
yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => {
const hasActiveDays = m.totalDuration > 0 ? 1 : 0;
return sum + hasActiveDays;
}, 0);
const activeMonthCount = Object.values(yearData.months).filter((m) => m.totalDuration > 0).length;
yearData.averageMonthly = activeMonthCount > 0 ? Math.floor(yearData.yearlyTotal / activeMonthCount) : 0;
yearData.averageDaily = yearData.totalActiveDays > 0 ? Math.floor(yearData.yearlyTotal / yearData.totalActiveDays) : 0;
} catch (err) {
logger.debug("Month file not found for year summary calculation");
}
await fs.writeFile(yearFilePath, JSON.stringify(yearData, null, 2), "utf-8");
}
};
var createSessionPersistence = () => {
return new SessionPersistenceService();
};
// api/modules/time-tracking/timeService.ts
var generateId = () => {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
};
var TimeTrackerService = class _TimeTrackerService {
currentSession = null;
currentTabRecord = null;
isPaused = false;
todayDuration = 0;
_initialized = false;
static _initializationPromise = null;
heartbeatService;
persistence;
constructor(dependencies) {
this.heartbeatService = dependencies.heartbeatService;
this.persistence = dependencies.persistence;
}
static async create(config) {
const heartbeatService = createHeartbeatService(config?.heartbeatIntervalMs);
const persistence = createSessionPersistence();
const instance = new _TimeTrackerService({
heartbeatService,
persistence
});
await instance.initialize();
return instance;
}
static async createWithDependencies(dependencies) {
const instance = new _TimeTrackerService(dependencies);
await instance.initialize();
return instance;
}
async initialize() {
if (this._initialized) {
return;
}
if (_TimeTrackerService._initializationPromise) {
await _TimeTrackerService._initializationPromise;
return;
}
_TimeTrackerService._initializationPromise = this.loadCurrentState();
await _TimeTrackerService._initializationPromise;
this._initialized = true;
_TimeTrackerService._initializationPromise = null;
this.heartbeatService.setCallback(async () => {
if (this.currentSession && !this.isPaused) {
try {
this.heartbeatService.updateHeartbeat();
await this.updateCurrentTabDuration();
await this.saveCurrentState();
await this.updateTodayDataRealtime();
} catch (err) {
logger.error("Heartbeat update failed:", err);
}
}
});
}
ensureInitialized() {
if (!this._initialized) {
throw new Error("TimeTrackerService \u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u4F7F\u7528 TimeTrackerService.create() \u521B\u5EFA\u5B9E\u4F8B");
}
}
async loadCurrentState() {
const now = /* @__PURE__ */ new Date();
const todayData = await this.persistence.getDayData(now.getFullYear(), now.getMonth() + 1, now.getDate());
this.todayDuration = todayData.totalDuration;
const state = await this.persistence.loadCurrentState();
if (state.session && state.session.status === "active") {
const sessionStart = new Date(state.session.startTime);
const now2 = /* @__PURE__ */ new Date();
if (now2.getTime() - sessionStart.getTime() < 24 * 60 * 60 * 1e3) {
this.currentSession = state.session;
this.isPaused = state.isPaused;
if (state.currentTabRecord) {
this.currentTabRecord = state.currentTabRecord;
}
this.heartbeatService.restoreState({ lastHeartbeat: state.lastHeartbeat });
} else {
await this.endSession();
}
}
}
async saveCurrentState() {
await this.persistence.saveCurrentState({
session: this.currentSession,
currentTabRecord: this.currentTabRecord,
isPaused: this.isPaused,
lastHeartbeat: this.heartbeatService.getLastHeartbeat().toISOString()
});
}
async startSession() {
if (this.currentSession && this.currentSession.status === "active") {
return this.currentSession;
}
const now = /* @__PURE__ */ new Date();
this.currentSession = {
id: generateId(),
startTime: now.toISOString(),
duration: 0,
status: "active",
tabRecords: []
};
this.isPaused = false;
this.heartbeatService.updateHeartbeat();
this.heartbeatService.start();
await this.saveCurrentState();
return this.currentSession;
}
async pauseSession() {
if (!this.currentSession || this.isPaused) return;
this.isPaused = true;
await this.updateCurrentTabDuration();
await this.saveCurrentState();
}
async resumeSession() {
if (!this.currentSession || !this.isPaused) return;
this.isPaused = false;
this.heartbeatService.updateHeartbeat();
if (this.currentTabRecord) {
const now = /* @__PURE__ */ new Date();
const timeStr = `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}`;
this.currentTabRecord.focusedPeriods.push({ start: timeStr, end: timeStr });
}
await this.saveCurrentState();
}
async endSession() {
if (!this.currentSession) return;
this.heartbeatService.stop();
await this.updateCurrentTabDuration();
const now = /* @__PURE__ */ new Date();
this.currentSession.endTime = now.toISOString();
this.currentSession.status = "ended";
const startTime = new Date(this.currentSession.startTime);
this.currentSession.duration = Math.floor((now.getTime() - startTime.getTime()) / 1e3);
await this.persistence.saveSessionToDay(this.currentSession);
this.todayDuration += this.currentSession.duration;
this.currentSession = null;
this.currentTabRecord = null;
this.isPaused = false;
await this.persistence.clearCurrentState();
}
async updateCurrentTabDuration() {
if (!this.currentSession || !this.currentTabRecord) return;
const now = /* @__PURE__ */ new Date();
const periods = this.currentTabRecord.focusedPeriods;
if (periods.length > 0) {
const lastPeriod = periods[periods.length - 1];
const [h, m, s] = lastPeriod.start.split(":").map(Number);
const startSeconds = h * 3600 + m * 60 + s;
const currentSeconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
this.currentTabRecord.duration = currentSeconds - startSeconds;
lastPeriod.end = `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}`;
}
}
async updateTodayDataRealtime() {
if (!this.currentSession) return;
const now = /* @__PURE__ */ new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const day = now.getDate();
const dayData = await this.persistence.updateDayDataRealtime(
year,
month,
day,
this.currentSession,
this.currentTabRecord
);
this.todayDuration = dayData.totalDuration;
await this.persistence.recalculateMonthSummary(year, month, this.todayDuration);
await this.persistence.recalculateYearSummary(year);
}
async handleTabSwitch(tabInfo) {
if (!this.currentSession || this.isPaused) return;
await this.updateCurrentTabDuration();
if (this.currentTabRecord && this.currentTabRecord.duration > 0) {
this.currentSession.tabRecords.push({ ...this.currentTabRecord });
}
const now = /* @__PURE__ */ new Date();
const timeStr = `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}`;
this.currentTabRecord = {
tabId: tabInfo.tabId,
filePath: tabInfo.filePath,
fileName: getFileNameFromPath(tabInfo.filePath),
tabType: getTabTypeFromPath(tabInfo.filePath),
duration: 0,
focusedPeriods: [{ start: timeStr, end: timeStr }]
};
await this.saveCurrentState();
}
async handleEvent(event) {
switch (event.type) {
case "window-focus":
if (!this.currentSession) {
await this.startSession();
if (event.tabInfo) {
await this.handleTabSwitch(event.tabInfo);
}
} else {
await this.resumeSession();
await this.updateTodayDataRealtime();
}
break;
case "window-blur":
await this.pauseSession();
await this.updateTodayDataRealtime();
break;
case "app-quit":
await this.endSession();
break;
case "tab-switch":
case "tab-open":
if (!this.currentSession) {
await this.startSession();
}
if (event.tabInfo) {
await this.handleTabSwitch(event.tabInfo);
}
await this.updateTodayDataRealtime();
break;
case "tab-close":
await this.updateCurrentTabDuration();
await this.updateTodayDataRealtime();
break;
case "heartbeat":
if (this.currentSession && !this.isPaused) {
this.heartbeatService.updateHeartbeat();
await this.updateCurrentTabDuration();
await this.saveCurrentState();
await this.updateTodayDataRealtime();
}
break;
}
}
async getDayData(year, month, day) {
return this.persistence.getDayData(year, month, day);
}
async getWeekData(startDate) {
const result = [];
for (let i = 0; i < 7; i++) {
const date = new Date(startDate);
date.setDate(date.getDate() + i);
const data = await this.persistence.getDayData(date.getFullYear(), date.getMonth() + 1, date.getDate());
result.push(data);
}
return result;
}
async getMonthData(year, month) {
return this.persistence.getMonthData(year, month);
}
async getYearData(year) {
return this.persistence.getYearData(year);
}
getCurrentState() {
return {
isRunning: this.currentSession !== null,
isPaused: this.isPaused,
currentSession: this.currentSession,
todayDuration: this.todayDuration,
currentTabRecord: this.currentTabRecord
};
}
async getStats(year, month) {
const now = /* @__PURE__ */ new Date();
const targetYear = year || now.getFullYear();
const targetMonth = month;
let totalDuration = 0;
let activeDays = 0;
let longestDay = null;
let longestSession = null;
const tabDurations = {};
const tabTypeDurations = {};
if (targetMonth) {
const monthData = await this.persistence.getMonthData(targetYear, targetMonth);
totalDuration = monthData.monthlyTotal;
activeDays = Object.values(monthData.days).filter((d) => d.totalDuration > 0).length;
for (const [day, summary] of Object.entries(monthData.days)) {
if (!longestDay || summary.totalDuration > longestDay.duration) {
longestDay = { date: `${targetYear}-${targetMonth.toString().padStart(2, "0")}-${day}`, duration: summary.totalDuration };
}
for (const tab of summary.topTabs || []) {
tabDurations[tab.fileName] = (tabDurations[tab.fileName] || 0) + tab.duration;
}
}
const dayData = await this.persistence.getDayData(targetYear, targetMonth, 1);
for (const session of dayData.sessions) {
for (const record of session.tabRecords) {
const key = record.filePath || record.fileName;
tabDurations[key] = (tabDurations[key] || 0) + record.duration;
tabTypeDurations[record.tabType] = (tabTypeDurations[record.tabType] || 0) + record.duration;
}
}
} else {
const yearData = await this.persistence.getYearData(targetYear);
totalDuration = yearData.yearlyTotal;
activeDays = Object.values(yearData.months).reduce((sum, m) => sum + m.activeDays, 0);
for (const [month2, summary] of Object.entries(yearData.months)) {
if (!longestDay || summary.totalDuration > longestDay.duration) {
longestDay = { date: `${targetYear}-${month2}`, duration: summary.totalDuration };
}
}
for (let m = 1; m <= 12; m++) {
const monthStr = m.toString().padStart(2, "0");
const monthData = await this.persistence.getMonthData(targetYear, m);
for (const dayData of Object.values(monthData.days)) {
for (const tab of dayData.topTabs || []) {
tabDurations[tab.fileName] = (tabDurations[tab.fileName] || 0) + tab.duration;
}
}
}
}
return {
totalDuration,
activeDays,
averageDaily: activeDays > 0 ? Math.floor(totalDuration / activeDays) : 0,
longestDay,
longestSession,
topTabs: Object.entries(tabDurations).map(([fileName, duration]) => ({
fileName,
duration,
percentage: totalDuration > 0 ? Math.round(duration / totalDuration * 100) : 0
})).sort((a, b) => b.duration - a.duration).slice(0, 10),
tabTypeDistribution: Object.entries(tabTypeDurations).map(([tabType, duration]) => ({
tabType,
duration,
percentage: totalDuration > 0 ? Math.round(duration / totalDuration * 100) : 0
})).sort((a, b) => b.duration - a.duration)
};
}
};
var _timeTrackerService = null;
var getTimeTrackerService = () => {
if (!_timeTrackerService) {
throw new Error("TimeTrackerService \u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 initializeTimeTrackerService()");
}
return _timeTrackerService;
};
var initializeTimeTrackerService = async (config) => {
if (_timeTrackerService) {
return _timeTrackerService;
}
_timeTrackerService = await TimeTrackerService.create(config);
return _timeTrackerService;
};
var initializeTimeTrackerServiceWithDependencies = async (dependencies) => {
if (_timeTrackerService) {
return _timeTrackerService;
}
_timeTrackerService = await TimeTrackerService.createWithDependencies(dependencies);
return _timeTrackerService;
};
// api/modules/time-tracking/routes.ts
import express from "express";
var createTimeTrackingRoutes = (deps) => {
const router = express.Router();
const { timeTrackerService } = deps;
router.get(
"/current",
asyncHandler(async (_req, res) => {
const state = timeTrackerService.getCurrentState();
successResponse(res, {
isRunning: state.isRunning,
isPaused: state.isPaused,
currentSession: state.currentSession ? {
id: state.currentSession.id,
startTime: state.currentSession.startTime,
duration: state.currentSession.duration,
currentTab: state.currentTabRecord ? {
tabId: state.currentTabRecord.tabId,
fileName: state.currentTabRecord.fileName,
tabType: state.currentTabRecord.tabType
} : null
} : null,
todayDuration: state.todayDuration
});
})
);
router.post(
"/event",
asyncHandler(async (req, res) => {
const event = req.body;
await timeTrackerService.handleEvent(event);
successResponse(res, null);
})
);
router.get(
"/day/:date",
asyncHandler(async (req, res) => {
const { date } = req.params;
const [year, month, day] = date.split("-").map(Number);
const data = await timeTrackerService.getDayData(year, month, day);
const sessionsCount = data.sessions.length;
const averageSessionDuration = sessionsCount > 0 ? Math.floor(data.totalDuration / sessionsCount) : 0;
const longestSession = data.sessions.reduce((max, s) => s.duration > max ? s.duration : max, 0);
const topTabs = Object.entries(data.tabSummary).map(([_, summary]) => ({
fileName: summary.fileName,
duration: summary.totalDuration
})).sort((a, b) => b.duration - a.duration).slice(0, 5);
successResponse(res, {
...data,
stats: {
sessionsCount,
averageSessionDuration,
longestSession,
topTabs
}
});
})
);
router.get(
"/week/:startDate",
asyncHandler(async (req, res) => {
const { startDate } = req.params;
const [year, month, day] = startDate.split("-").map(Number);
const start = new Date(year, month - 1, day);
const data = await timeTrackerService.getWeekData(start);
const totalDuration = data.reduce((sum, d) => sum + d.totalDuration, 0);
const activeDays = data.filter((d) => d.totalDuration > 0).length;
successResponse(res, {
days: data,
totalDuration,
activeDays,
averageDaily: activeDays > 0 ? Math.floor(totalDuration / activeDays) : 0
});
})
);
router.get(
"/month/:yearMonth",
asyncHandler(async (req, res) => {
const { yearMonth } = req.params;
const [year, month] = yearMonth.split("-").map(Number);
const data = await timeTrackerService.getMonthData(year, month);
successResponse(res, data);
})
);
router.get(
"/year/:year",
asyncHandler(async (req, res) => {
const { year } = req.params;
const data = await timeTrackerService.getYearData(parseInt(year));
successResponse(res, data);
})
);
router.get(
"/stats",
asyncHandler(async (req, res) => {
const year = req.query.year ? parseInt(req.query.year) : void 0;
const month = req.query.month ? parseInt(req.query.month) : void 0;
const stats = await timeTrackerService.getStats(year, month);
successResponse(res, stats);
})
);
return router;
};
// api/modules/time-tracking/index.ts
var createTimeTrackingModule = (moduleConfig = {}) => {
let serviceInstance;
return createApiModule(TIME_TRACKING_MODULE, {
routes: (container) => {
const timeTrackerService = container.getSync("timeTrackerService");
return createTimeTrackingRoutes({ timeTrackerService });
},
lifecycle: {
onLoad: async (container) => {
serviceInstance = await initializeTimeTrackerService(moduleConfig.config);
container.register("timeTrackerService", () => serviceInstance);
}
}
});
};
var time_tracking_default = createTimeTrackingModule;
export {
HeartbeatService,
createHeartbeatService,
SessionPersistenceService,
createSessionPersistence,
TimeTrackerService,
getTimeTrackerService,
initializeTimeTrackerService,
initializeTimeTrackerServiceWithDependencies,
createTimeTrackingRoutes,
createTimeTrackingModule,
time_tracking_default
};

View File

@@ -1,586 +0,0 @@
import {
logger
} from "./chunk-47DJ6YUB.js";
import {
getTempDir
} from "./chunk-FTVFWJFJ.js";
import {
InternalError,
ValidationError,
resolveNotebookPath
} from "./chunk-ER4KPD22.js";
import {
NOTEBOOK_ROOT,
PROJECT_ROOT,
TEMP_ROOT,
asyncHandler,
createApiModule,
defineApiModule,
successResponse
} from "./chunk-74TMTGBG.js";
// api/modules/document-parser/index.ts
import express3 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 express from "express";
import path3 from "path";
import fs3 from "fs/promises";
import { existsSync as existsSync2 } from "fs";
import axios from "axios";
// api/utils/file.ts
import fs from "fs/promises";
import path 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 = path.join(imagesDirFullPath, filename);
try {
await fs.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;
};
// 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/modules/document-parser/documentParser.ts
import path2 from "path";
import { spawn } from "child_process";
import fs2 from "fs/promises";
import { existsSync, mkdirSync } from "fs";
if (!existsSync(TEMP_ROOT)) {
mkdirSync(TEMP_ROOT, { recursive: true });
}
var createJobContext = async (prefix) => {
const now = /* @__PURE__ */ new Date();
const jobDir = path2.join(TEMP_ROOT, `${prefix}_${formatTimestamp(now)}`);
await fs2.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 = path2.join(NOTEBOOK_ROOT, imagesSubDir);
await fs2.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("/") ? [path2.join(jobDir, s2.slice(1)), path2.join(htmlDir, s2.slice(1))] : [path2.resolve(htmlDir, s2), path2.resolve(jobDir, s2)];
let foundFile = null;
for (const c of candidates) {
if (existsSync(c)) {
foundFile = c;
break;
}
}
if (!foundFile) return null;
const ext = path2.extname(foundFile) || ".jpg";
const baseName = formatTimestamp(now);
const newFilename = await getUniqueFilename(destImagesDir, baseName, ext);
const newPath = path2.join(destImagesDir, newFilename);
await fs2.copyFile(foundFile, newPath);
return { newLink: `/${imagesSubDir}/${newFilename}` };
};
var cleanupJob = async (jobDir, additionalPaths = []) => {
await fs2.rm(jobDir, { recursive: true, force: true }).catch(() => {
});
for (const p of additionalPaths) {
await fs2.unlink(p).catch(() => {
});
}
};
var getScriptPath = (toolName, scriptName) => {
return path2.join(PROJECT_ROOT, "tools", toolName, scriptName);
};
var ensureScriptExists = (scriptPath) => {
return existsSync(scriptPath);
};
// api/modules/document-parser/blogRoutes.ts
var router = express.Router();
var tempDir = getTempDir();
router.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 = path3.join(jobContext.jobDir, "input.html");
await fs3.copyFile(htmlPath, htmlPathInJob);
if (assetsDirName && assetsFiles && assetsFiles.length > 0) {
const assetsDirPath = path3.join(htmlDir, assetsDirName);
for (const relPath of assetsFiles) {
const srcPath = path3.join(assetsDirPath, relPath);
if (existsSync2(srcPath)) {
const destPath = path3.join(jobContext.jobDir, assetsDirName, relPath);
await fs3.mkdir(path3.dirname(destPath), { recursive: true });
await fs3.copyFile(srcPath, destPath);
}
}
}
} catch (err) {
await cleanupJob(jobContext.jobDir);
throw err;
}
processHtmlInBackground({
jobDir: jobContext.jobDir,
htmlPath: htmlPathInJob,
targetPath: fullTargetPath,
cwd: path3.dirname(scriptPath),
jobContext,
originalHtmlDir: htmlDir,
originalAssetsDirName: assetsDirName
}).catch((err) => {
logger.error("Background HTML processing failed:", err);
fs3.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 = path3.parse(htmlPath);
const markdownPath = path3.join(parsedPathObj.dir, `${parsedPathObj.name}.md`);
if (!existsSync2(markdownPath)) {
throw new Error("Markdown result file not found");
}
let mdContent = await fs3.readFile(markdownPath, "utf-8");
const ctx = await jobContext;
const htmlDir = path3.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 = path3.extname(originalSrc.split("?")[0]);
if (urlExt) ext = urlExt;
const baseName = formatTimestamp(ctx.now);
const newFilename = await getUniqueFilename(ctx.destImagesDir, baseName, ext);
const newPath = path3.join(ctx.destImagesDir, newFilename);
await fs3.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 = [
path3.join(originalHtmlDir, originalAssetsDirName, srcWithFiles),
path3.join(originalHtmlDir, originalAssetsDirName, path3.basename(srcWithFiles))
];
for (const p of possiblePaths) {
if (existsSync2(p)) {
const ext = path3.extname(p) || ".jpg";
const baseName = formatTimestamp(ctx.now);
const newFilename = await getUniqueFilename(ctx.destImagesDir, baseName, ext);
const newPath = path3.join(ctx.destImagesDir, newFilename);
await fs3.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 fs3.writeFile(targetPath, mdContent, "utf-8");
await fs3.unlink(markdownPath).catch(() => {
});
} finally {
await cleanupJob(jobDir);
}
}
var blogRoutes_default = router;
// api/modules/document-parser/mineruRoutes.ts
import express2 from "express";
import multer from "multer";
import path4 from "path";
import fs4 from "fs/promises";
import { existsSync as existsSync3 } from "fs";
var router2 = express2.Router();
var tempDir2 = getTempDir();
var upload = multer({
dest: tempDir2,
limits: {
fileSize: 50 * 1024 * 1024
}
});
router2.post(
"/parse",
upload.single("file"),
asyncHandler(async (req, res) => {
if (!req.file) {
throw new ValidationError("File is required");
}
const { targetPath } = req.body;
if (!targetPath) {
await fs4.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 fs4.unlink(req.file.path).catch(() => {
});
throw error;
}
const scriptPath = getScriptPath("mineru", "mineru_parser.py");
if (!ensureScriptExists(scriptPath)) {
await fs4.unlink(req.file.path).catch(() => {
});
throw new InternalError("Parser script not found");
}
processPdfInBackground(req.file.path, fullTargetPath, path4.dirname(scriptPath)).catch((err) => {
logger.error("Background PDF processing failed:", err);
fs4.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 (!existsSync3(markdownPath)) {
throw new Error("Markdown result file not found");
}
let mdContent = await fs4.readFile(markdownPath, "utf-8");
const imagesDir = path4.join(outputDir, "images");
if (existsSync3(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, path4.basename(originalSrc)];
let foundFile = null;
for (const fname of possibleFilenames) {
const localPath = path4.join(imagesDir, fname);
if (existsSync3(localPath)) {
foundFile = localPath;
break;
}
const directPath = path4.join(outputDir, originalSrc);
if (existsSync3(directPath)) {
foundFile = directPath;
break;
}
}
if (foundFile) {
const ext = path4.extname(foundFile);
const baseName = formatTimestamp(jobContext.now);
const newFilename = await getUniqueFilename(jobContext.destImagesDir, baseName, ext);
const newPath = path4.join(jobContext.destImagesDir, newFilename);
await fs4.copyFile(foundFile, newPath);
replacements.push({
start: dest.start,
end: dest.end,
original: originalSrc,
replacement: `${jobContext.imagesSubDir}/${newFilename}`
});
}
}
mdContent = applyReplacements(mdContent, replacements);
}
await fs4.writeFile(targetPath, mdContent, "utf-8");
await fs4.unlink(markdownPath).catch(() => {
});
if (outputDir && outputDir.includes("temp")) {
await fs4.rm(outputDir, { recursive: true, force: true }).catch(() => {
});
}
} finally {
await fs4.unlink(filePath).catch(() => {
});
}
}
var mineruRoutes_default = router2;
// api/modules/document-parser/index.ts
var createDocumentParserModule = () => {
return createApiModule(DOCUMENT_PARSER_MODULE, {
routes: (_container) => {
const router3 = express3.Router();
router3.use("/blog", blogRoutes_default);
router3.use("/mineru", mineruRoutes_default);
return router3;
}
});
};
var document_parser_default = createDocumentParserModule;
export {
pad2,
formatTimestamp,
getUniqueFilename,
mimeToExt,
validateImageBuffer,
detectImageMimeType,
createJobContext,
spawnPythonScript,
findImageDestinations,
applyReplacements,
copyLocalImage,
cleanupJob,
getScriptPath,
ensureScriptExists,
blogRoutes_default,
mineruRoutes_default,
createDocumentParserModule,
document_parser_default
};

View File

@@ -1,313 +0,0 @@
import {
validateBody,
validateQuery
} from "./chunk-5EGA6GHY.js";
import {
getTempDir
} from "./chunk-FTVFWJFJ.js";
import {
AlreadyExistsError,
NotFoundError,
ValidationError,
isNodeError,
resolveNotebookPath
} from "./chunk-ER4KPD22.js";
import {
asyncHandler,
createApiModule,
defineApiModule,
defineEndpoints,
successResponse
} from "./chunk-74TMTGBG.js";
// 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 express from "express";
import fs from "fs/promises";
import path from "path";
import multer from "multer";
// 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/modules/pydemos/routes.ts
var tempDir = getTempDir();
var upload = multer({
dest: tempDir,
limits: {
fileSize: 50 * 1024 * 1024
}
});
var toPosixPath = (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 fs.readdir(dirPath, { withFileTypes: true });
return entries.filter((e) => e.isFile()).length;
} catch {
return 0;
}
};
var createPyDemosRoutes = () => {
const router = express.Router();
router.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 fs.access(yearPath);
} catch {
successResponse(res, { months });
return;
}
const monthEntries = await fs.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 = path.join(yearPath, monthEntry.name);
const demoEntries = await fs.readdir(monthPath, { withFileTypes: true });
const demos = [];
for (const demoEntry of demoEntries) {
if (!demoEntry.isDirectory()) continue;
const demoPath = path.join(monthPath, demoEntry.name);
const relDemoPath = `pydemos/${year}/${monthEntry.name}/${demoEntry.name}`;
let created;
try {
const stats = await fs.stat(demoPath);
created = stats.birthtime.toISOString();
} catch {
created = (/* @__PURE__ */ new Date()).toISOString();
}
const fileCount = await countFilesInDir(demoPath);
demos.push({
name: demoEntry.name,
path: toPosixPath(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 });
})
);
router.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 = path.join(monthPath, name);
const relDemoPath = `${monthRelPath}/${name}`;
try {
await fs.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 fs.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 = path.join(demoPath, relativePath);
const targetDir = path.dirname(targetPath);
await fs.mkdir(targetDir, { recursive: true });
await fs.copyFile(file.path, targetPath);
await fs.unlink(file.path).catch(() => {
});
fileCount++;
}
}
successResponse(res, { path: toPosixPath(relDemoPath), fileCount });
})
);
router.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 fs.access(fullPath);
} catch {
throw new NotFoundError("Demo not found");
}
await fs.rm(fullPath, { recursive: true, force: true });
successResponse(res, null);
})
);
router.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 fs.access(oldFullPath);
} catch {
throw new NotFoundError("Demo not found");
}
const parentDir = path.dirname(oldFullPath);
const newFullPath = path.join(parentDir, newName);
const newPath = toPosixPath(path.join(path.dirname(oldPath), newName));
try {
await fs.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 fs.rename(oldFullPath, newFullPath);
successResponse(res, { newPath });
})
);
return router;
};
var routes_default = createPyDemosRoutes();
// api/modules/pydemos/index.ts
var createPyDemosModule = () => {
return createApiModule(PYDEMOS_MODULE, {
routes: (_container) => {
return createPyDemosRoutes();
}
});
};
var pydemos_default = createPyDemosModule;
export {
listFilesQuerySchema,
contentQuerySchema,
rawQuerySchema,
pathSchema,
saveFileSchema,
renameSchema,
createDirSchema,
createFileSchema,
createPyDemosRoutes,
createPyDemosModule,
pydemos_default
};

View File

@@ -1,125 +0,0 @@
import {
InternalError,
NotFoundError,
ValidationError,
resolveNotebookPath
} from "./chunk-ER4KPD22.js";
import {
asyncHandler,
createApiModule,
defineApiModule,
successResponse
} from "./chunk-74TMTGBG.js";
// 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 express from "express";
import { spawn } from "child_process";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs/promises";
import fsSync from "fs";
var __filename = fileURLToPath(import.meta.url);
var __dirname = path.dirname(__filename);
var router = express.Router();
var PYTHON_TIMEOUT_MS = 3e4;
var spawnPythonWithTimeout = (scriptPath, args, stdinContent, timeoutMs = PYTHON_TIMEOUT_MS) => {
return new Promise((resolve, reject) => {
const pythonProcess = spawn("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();
});
};
router.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 fs.access(fullPath);
} catch {
throw new NotFoundError("File not found");
}
const content = await fs.readFile(fullPath, "utf-8");
const projectRoot = path.resolve(__dirname, "..", "..", "..");
const scriptPath = path.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 fs.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}`);
}
})
);
var createAiRoutes = () => router;
// api/modules/ai/index.ts
var createAiModule = () => {
return createApiModule(AI_MODULE, {
routes: (_container) => {
return createAiRoutes();
}
});
};
var ai_default = createAiModule;
export {
createAiRoutes,
createAiModule,
ai_default
};

View File

@@ -1,399 +0,0 @@
import {
validateBody,
validateQuery
} from "./chunk-5EGA6GHY.js";
import {
NotFoundError,
resolveNotebookPath
} from "./chunk-ER4KPD22.js";
import {
asyncHandler,
createApiModule,
defineApiModule,
defineEndpoints,
successResponse
} from "./chunk-74TMTGBG.js";
// 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 fs from "fs/promises";
import path 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 = path.dirname(fullPath);
await fs.mkdir(dir, { recursive: true });
try {
await fs.access(fullPath);
} catch {
await fs.writeFile(fullPath, "", "utf-8");
}
}
async loadAndParseTodoFile(year, month) {
const { fullPath } = this.getTodoFilePath(year, month);
try {
await fs.access(fullPath);
} catch {
throw new NotFoundError("TODO file not found");
}
const content = await fs.readFile(fullPath, "utf-8");
return { fullPath, dayTodos: parseTodoContent(content) };
}
async saveTodoFile(fullPath, dayTodos) {
const content = generateTodoContent(dayTodos);
await fs.writeFile(fullPath, content, "utf-8");
}
async getTodo(year, month) {
const { fullPath } = this.getTodoFilePath(year, month);
let dayTodos = [];
try {
await fs.access(fullPath);
const content = await fs.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 fs.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 fs.writeFile(fullPath, content, "utf-8");
}
async addTodo(year, month, date, todoContent) {
const { fullPath } = this.getTodoFilePath(year, month);
await this.ensureTodoFileExists(fullPath);
let fileContent = await fs.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 fs.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;
}
};
var createTodoService = (deps) => {
return new TodoService(deps);
};
// api/modules/todo/routes.ts
import express from "express";
// api/modules/todo/schemas.ts
import { z } from "zod";
var todoItemSchema = z.object({
id: z.string(),
content: z.string(),
completed: z.boolean()
});
var dayTodoSchema = z.object({
date: z.string(),
items: z.array(todoItemSchema)
});
var getTodoQuerySchema = z.object({
year: z.string().optional(),
month: z.string().optional()
});
var saveTodoSchema = z.object({
year: z.number().int().positive(),
month: z.number().int().min(1).max(12),
dayTodos: z.array(dayTodoSchema)
});
var addTodoSchema = z.object({
year: z.number().int().positive(),
month: z.number().int().min(1).max(12),
date: z.string(),
content: z.string()
});
var toggleTodoSchema = z.object({
year: z.number().int().positive(),
month: z.number().int().min(1).max(12),
date: z.string(),
itemIndex: z.number().int().nonnegative(),
completed: z.boolean()
});
var updateTodoSchema = z.object({
year: z.number().int().positive(),
month: z.number().int().min(1).max(12),
date: z.string(),
itemIndex: z.number().int().nonnegative(),
content: z.string()
});
var deleteTodoSchema = z.object({
year: z.number().int().positive(),
month: z.number().int().min(1).max(12),
date: z.string(),
itemIndex: z.number().int().nonnegative()
});
// api/modules/todo/routes.ts
var createTodoRoutes = (deps) => {
const router = express.Router();
const { todoService: todoService2 } = deps;
router.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);
})
);
router.post(
"/save",
validateBody(saveTodoSchema),
asyncHandler(async (req, res) => {
const { year, month, dayTodos } = req.body;
await todoService2.saveTodo(year, month, dayTodos);
successResponse(res, null);
})
);
router.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 });
})
);
router.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 });
})
);
router.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 });
})
);
router.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 router;
};
var todoService = new TodoService();
var routes_default = createTodoRoutes({ todoService });
// api/modules/todo/index.ts
var createTodoModule = () => {
return createApiModule(TODO_MODULE, {
routes: (container) => {
const todoService2 = container.getSync("todoService");
return createTodoRoutes({ todoService: todoService2 });
},
lifecycle: {
onLoad: (container) => {
container.register("todoService", () => new TodoService());
}
}
});
};
var todo_default = createTodoModule;
export {
parseTodoContent,
generateTodoContent,
TodoService,
createTodoService,
getTodoQuerySchema,
saveTodoSchema,
addTodoSchema,
toggleTodoSchema,
updateTodoSchema,
deleteTodoSchema,
createTodoRoutes,
createTodoModule,
todo_default
};

View File

@@ -1,280 +0,0 @@
import {
resolveNotebookPath
} from "./chunk-ER4KPD22.js";
import {
asyncHandler,
createApiModule,
defineApiModule,
defineEndpoints,
successResponse
} from "./chunk-74TMTGBG.js";
// 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 fs from "fs/promises";
import path 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(path.join(REMOTE_DIR, safeName));
return { relPath: path.join(REMOTE_DIR, safeName), fullPath };
}
sanitizeFileName(name) {
return name.replace(/[<>:"/\\|?*]/g, "_").trim() || "unnamed";
}
getDeviceConfigPath(deviceName) {
const { fullPath } = this.getDeviceDir(deviceName);
return path.join(fullPath, "config.json");
}
getDeviceScreenshotPath(deviceName) {
const { fullPath } = this.getDeviceDir(deviceName);
return path.join(fullPath, "screenshot.png");
}
getDeviceDataPath(deviceName) {
const { fullPath } = this.getDeviceDir(deviceName);
return path.join(fullPath, "data.json");
}
async ensureDir(dirPath) {
await fs.mkdir(dirPath, { recursive: true });
}
async getDeviceNames() {
const { fullPath } = this.getRemoteDir();
try {
const entries = await fs.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 fs.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,
openCodePort: deviceConfig.openCodePort || 3002,
fileTransferPort: deviceConfig.fileTransferPort || 3003,
password: deviceConfig.password || ""
};
} catch {
return {
id: name,
deviceName: name,
serverHost: "",
desktopPort: 3e3,
gitPort: 3001,
openCodePort: 3002,
fileTransferPort: 3003,
password: ""
};
}
})
);
return { devices };
}
async saveConfig(config) {
const { fullPath: remoteDirFullPath } = this.getRemoteDir();
await this.ensureDir(remoteDirFullPath);
const existingDevices = await this.getDeviceNames();
const newDeviceNames = config.devices.map((d) => this.sanitizeFileName(d.deviceName));
for (const oldDevice of existingDevices) {
if (!newDeviceNames.includes(oldDevice)) {
try {
const oldDir = path.join(remoteDirFullPath, oldDevice);
await fs.rm(oldDir, { recursive: true, force: true });
} catch {
}
}
}
for (const device of config.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,
openCodePort: device.openCodePort,
fileTransferPort: device.fileTransferPort,
password: device.password || ""
};
await fs.writeFile(deviceConfigPath, JSON.stringify(deviceConfig, null, 2), "utf-8");
}
}
async getScreenshot(deviceName) {
if (!deviceName) {
return null;
}
const screenshotPath = this.getDeviceScreenshotPath(deviceName);
try {
return await fs.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 fs.writeFile(screenshotPath, buffer);
}
async getData(deviceName) {
if (!deviceName || deviceName.trim() === "") {
return null;
}
const dataPath = this.getDeviceDataPath(deviceName);
try {
const content = await fs.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 fs.writeFile(dataPath, JSON.stringify(data, null, 2), "utf-8");
}
};
var createRemoteService = (deps) => {
return new RemoteService(deps);
};
// api/modules/remote/routes.ts
import express from "express";
var createRemoteRoutes = (deps) => {
const router = express.Router();
const { remoteService: remoteService2 } = deps;
router.get(
"/config",
asyncHandler(async (req, res) => {
const config = await remoteService2.getConfig();
successResponse(res, config);
})
);
router.post(
"/config",
asyncHandler(async (req, res) => {
const config = req.body;
await remoteService2.saveConfig(config);
successResponse(res, null);
})
);
router.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);
})
);
router.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);
})
);
router.get(
"/data",
asyncHandler(async (req, res) => {
const deviceName = req.query.device;
const data = await remoteService2.getData(deviceName);
successResponse(res, data);
})
);
router.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 router;
};
var remoteService = new RemoteService();
var routes_default = createRemoteRoutes({ remoteService });
// api/modules/remote/index.ts
var createRemoteModule = () => {
return createApiModule(REMOTE_MODULE, {
routes: (container) => {
const remoteService2 = container.getSync("remoteService");
return createRemoteRoutes({ remoteService: remoteService2 });
},
lifecycle: {
onLoad: (container) => {
container.register("remoteService", () => new RemoteService());
}
}
});
};
var remote_default = createRemoteModule;
export {
RemoteService,
createRemoteService,
createRemoteRoutes,
createRemoteModule,
remote_default
};

View File

@@ -1,32 +0,0 @@
import {
applyReplacements,
blogRoutes_default,
cleanupJob,
copyLocalImage,
createDocumentParserModule,
createJobContext,
document_parser_default,
ensureScriptExists,
findImageDestinations,
getScriptPath,
mineruRoutes_default,
spawnPythonScript
} from "./chunk-R5LQJNQE.js";
import "./chunk-47DJ6YUB.js";
import "./chunk-FTVFWJFJ.js";
import "./chunk-ER4KPD22.js";
import "./chunk-74TMTGBG.js";
export {
applyReplacements,
blogRoutes_default as blogRoutes,
cleanupJob,
copyLocalImage,
createDocumentParserModule,
createJobContext,
document_parser_default as default,
ensureScriptExists,
findImageDestinations,
getScriptPath,
mineruRoutes_default as mineruRoutes,
spawnPythonScript
};

View File

@@ -1,14 +0,0 @@
import {
createPyDemosModule,
createPyDemosRoutes,
pydemos_default
} from "./chunk-T5RPAMG6.js";
import "./chunk-5EGA6GHY.js";
import "./chunk-FTVFWJFJ.js";
import "./chunk-ER4KPD22.js";
import "./chunk-74TMTGBG.js";
export {
createPyDemosModule,
createPyDemosRoutes,
pydemos_default as default
};

View File

@@ -1,14 +0,0 @@
import {
createRecycleBinModule,
recycle_bin_default,
restoreFile,
restoreFolder
} from "./chunk-M2SZ5AIA.js";
import "./chunk-ER4KPD22.js";
import "./chunk-74TMTGBG.js";
export {
createRecycleBinModule,
recycle_bin_default as default,
restoreFile,
restoreFolder
};

View File

@@ -1,16 +0,0 @@
import {
RemoteService,
createRemoteModule,
createRemoteRoutes,
createRemoteService,
remote_default
} from "./chunk-W5TDYTXE.js";
import "./chunk-ER4KPD22.js";
import "./chunk-74TMTGBG.js";
export {
RemoteService,
createRemoteModule,
createRemoteRoutes,
createRemoteService,
remote_default as default
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
import {
HeartbeatService,
SessionPersistenceService,
TimeTrackerService,
createHeartbeatService,
createSessionPersistence,
createTimeTrackingModule,
createTimeTrackingRoutes,
getTimeTrackerService,
initializeTimeTrackerService,
initializeTimeTrackerServiceWithDependencies,
time_tracking_default
} from "./chunk-QS2CMBFP.js";
import "./chunk-47DJ6YUB.js";
import "./chunk-74TMTGBG.js";
export {
HeartbeatService,
SessionPersistenceService,
TimeTrackerService,
createHeartbeatService,
createSessionPersistence,
createTimeTrackingModule,
createTimeTrackingRoutes,
time_tracking_default as default,
getTimeTrackerService,
initializeTimeTrackerService,
initializeTimeTrackerServiceWithDependencies
};

View File

@@ -1,33 +0,0 @@
import {
TodoService,
addTodoSchema,
createTodoModule,
createTodoRoutes,
createTodoService,
deleteTodoSchema,
generateTodoContent,
getTodoQuerySchema,
parseTodoContent,
saveTodoSchema,
todo_default,
toggleTodoSchema,
updateTodoSchema
} from "./chunk-V2OWYGQG.js";
import "./chunk-5EGA6GHY.js";
import "./chunk-ER4KPD22.js";
import "./chunk-74TMTGBG.js";
export {
TodoService,
addTodoSchema,
createTodoModule,
createTodoRoutes,
createTodoService,
todo_default as default,
deleteTodoSchema,
generateTodoContent,
getTodoQuerySchema,
parseTodoContent,
saveTodoSchema,
toggleTodoSchema,
updateTodoSchema
};

View File

@@ -1,99 +0,0 @@
# XCOpenCodeWeb
XCOpenCodeWeb 是一个基于 Web 的 AI 编程助手界面,用于与 OpenCode 服务器交互。
## 快速开始
### 使用单文件 exe推荐
直接下载 `XCOpenCodeWeb.exe`,双击运行:
```bash
# 默认端口 3000
XCOpenCodeWeb.exe
# 指定端口
XCOpenCodeWeb.exe --port 8080
# 查看帮助
XCOpenCodeWeb.exe --help
```
启动后访问 http://localhost:3000
### 从源码运行
需要安装 [Bun](https://bun.sh) 或 Node.js 20+
```bash
# 安装依赖
bun install
# 构建前端
bun run build
# 启动服务器
bun server/index.js --port 3000
```
## 构建单文件 exe
```bash
cd web
bun run build:exe
```
输出:`web/XCOpenCodeWeb.exe`(约 320MB
详细文档:[docs/single-file-exe-build.md](docs/single-file-exe-build.md)
## 项目结构
```
├── ui/ # 前端组件库
├── web/
│ ├── src/ # 前端源码
│ ├── server/ # 后端服务器
│ ├── bin/ # CLI 工具
│ └── dist/ # 构建输出
├── docs/ # 文档
└── AGENTS.md # AI Agent 参考文档
```
## 常用命令
```bash
# 开发模式
bun run dev # 前端热更新
bun run dev:server # 启动开发服务器
# 构建
bun run build # 构建前端
bun run build:exe # 构建单文件 exe
# 代码检查
bun run type-check:web # TypeScript 类型检查
bun run lint:web # ESLint 检查
```
## 依赖
- [Bun](https://bun.sh) - 运行时和打包工具
- [React](https://react.dev) - 前端框架
- [Express](https://expressjs.com) - 后端服务器
- [Tailwind CSS](https://tailwindcss.com) - 样式框架
## 配置
### 环境变量
| 变量 | 说明 |
|------|------|
| `XCOpenCodeWeb_PORT` | 服务器端口(默认 3000 |
| `OPENCODE_HOST` | 外部 OpenCode 服务器地址 |
| `OPENCODE_PORT` | 外部 OpenCode 端口 |
| `OPENCODE_SKIP_START` | 跳过启动 OpenCode |
## 许可证
MIT

View File

@@ -1,99 +0,0 @@
# XCOpenCodeWeb
XCOpenCodeWeb 是一个基于 Web 的 AI 编程助手界面,用于与 OpenCode 服务器交互。
## 快速开始
### 使用单文件 exe推荐
直接下载 `XCOpenCodeWeb.exe`,双击运行:
```bash
# 默认端口 3000
XCOpenCodeWeb.exe
# 指定端口
XCOpenCodeWeb.exe --port 8080
# 查看帮助
XCOpenCodeWeb.exe --help
```
启动后访问 http://localhost:3000
### 从源码运行
需要安装 [Bun](https://bun.sh) 或 Node.js 20+
```bash
# 安装依赖
bun install
# 构建前端
bun run build
# 启动服务器
bun server/index.js --port 3000
```
## 构建单文件 exe
```bash
cd web
bun run build:exe
```
输出:`web/XCOpenCodeWeb.exe`(约 320MB
详细文档:[docs/single-file-exe-build.md](docs/single-file-exe-build.md)
## 项目结构
```
├── ui/ # 前端组件库
├── web/
│ ├── src/ # 前端源码
│ ├── server/ # 后端服务器
│ ├── bin/ # CLI 工具
│ └── dist/ # 构建输出
├── docs/ # 文档
└── AGENTS.md # AI Agent 参考文档
```
## 常用命令
```bash
# 开发模式
bun run dev # 前端热更新
bun run dev:server # 启动开发服务器
# 构建
bun run build # 构建前端
bun run build:exe # 构建单文件 exe
# 代码检查
bun run type-check:web # TypeScript 类型检查
bun run lint:web # ESLint 检查
```
## 依赖
- [Bun](https://bun.sh) - 运行时和打包工具
- [React](https://react.dev) - 前端框架
- [Express](https://expressjs.com) - 后端服务器
- [Tailwind CSS](https://tailwindcss.com) - 样式框架
## 配置
### 环境变量
| 变量 | 说明 |
|------|------|
| `XCOpenCodeWeb_PORT` | 服务器端口(默认 3000 |
| `OPENCODE_HOST` | 外部 OpenCode 服务器地址 |
| `OPENCODE_PORT` | 外部 OpenCode 端口 |
| `OPENCODE_SKIP_START` | 跳过启动 OpenCode |
## 许可证
MIT