chore: 移除构建产物,保持仓库精简
This commit is contained in:
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
3026
dist-api/server.js
3026
dist-api/server.js
File diff suppressed because it is too large
Load Diff
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
Binary file not shown.
@@ -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
|
|
||||||
Binary file not shown.
Reference in New Issue
Block a user