chore: 添加远程桌面控制组件、文档和构建产物

This commit is contained in:
2026-03-13 16:04:21 +08:00
parent 67a19d486b
commit 8bb2e643d8
36 changed files with 7280 additions and 582 deletions

6
console.txt Normal file
View File

@@ -0,0 +1,6 @@
Total messages: 5 (Errors: 3, Warnings: 0)
Returning 3 messages for level "error"
[ERROR] Manifest: Line: 1, column: 1, Syntax error. @ http://localhost:3000/site.webmanifest:0
[ERROR] Manifest: Line: 1, column: 1, Syntax error. @ http://localhost:3000/site.webmanifest:0
The requested module '/node_modules/lru_map/dist/lru.js?v=e2db4b71' does not provide an export named 'default'

156
counter.py Normal file
View File

@@ -0,0 +1,156 @@
#!/usr/bin/env python3
import os
import sys
import argparse
from pathlib import Path
if sys.platform == "win32":
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
def count_lines(file_path: Path) -> int:
try:
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
return sum(1 for _ in f)
except Exception:
return 0
def should_include(file_path: Path, extensions: list[str]) -> bool:
if not extensions:
return True
return file_path.suffix in extensions
def walk_dir(root_path: Path, extensions: list[str], exclude_dirs: set[str]):
results = []
total_lines = 0
for root, dirs, files in os.walk(root_path):
dirs[:] = [d for d in dirs if d not in exclude_dirs]
rel_root = Path(root).relative_to(root_path)
indent = len(rel_root.parts) if str(rel_root) != "." else 0
prefix = " " * indent
if indent > 0:
print(f"{prefix}└── {rel_root.name}/")
for i, file in enumerate(files):
file_path = Path(root) / file
if not should_include(file_path, extensions):
continue
lines = count_lines(file_path)
total_lines += lines
results.append((file_path, lines, indent + 1))
is_last = (i == len(files) - 1) and not any(
should_include(Path(root) / f, extensions) for f in dirs
)
connector = "└──" if is_last else "├──"
print(f"{' ' * (indent + 1)}{connector} {file} ({lines} lines)")
return total_lines
def main_all():
directories = {
"src": [".ts", ".tsx", ".vue", ".js"],
"api": [".ts", ".py"],
"electron": [".ts"],
"remote": [".js"],
"shared": [".ts"],
"tools": [".py"],
}
total_all = 0
results = []
for directory, extensions in directories.items():
root_path = Path(directory)
if not root_path.exists():
continue
exclude_dirs = {
".git",
"node_modules",
"dist",
"dist-api",
"dist-electron",
"__pycache__",
".ruff_cache",
}
print(f"\n{'=' * 60}")
print(f"项目文件统计: {root_path}")
print(f"后缀过滤: {extensions}")
print(f"{'=' * 60}\n")
total = walk_dir(root_path, extensions, exclude_dirs)
results.append((directory, total))
total_all += total
print(f"\n{'=' * 60}")
print("汇总统计")
print(f"{'=' * 60}")
for name, lines in results:
print(f"{name:15} {lines:>10,}")
print(f"{'=' * 60}")
print(f"{'总计':15} {total_all:>10,}")
print(f"{'=' * 60}\n")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="统计项目文件行数")
parser.add_argument(
"-e", "--extension", action="append", help="指定后缀名,如: .py .ts .js"
)
parser.add_argument("-d", "--directory", default=".", help="指定子文件夹路径")
parser.add_argument(
"-x", "--exclude", action="append", default=[], help="排除的文件夹"
)
parser.add_argument(
"--all",
action="store_true",
help="统计所有源码目录 (src/api/electron/remote/shared/tools)",
)
args = parser.parse_args()
if args.all:
main_all()
else:
root_path = Path(args.directory).resolve()
extensions = args.extension
exclude_dirs = {
".git",
"node_modules",
"dist",
"dist-api",
"dist-electron",
"__pycache__",
".ruff_cache",
}
exclude_dirs.update(args.exclude)
if not root_path.exists():
print(f"错误: 目录 {root_path} 不存在")
sys.exit(1)
print(f"\n{'=' * 60}")
print(f"项目文件统计: {root_path}")
if extensions:
print(f"后缀过滤: {extensions}")
print(f"{'=' * 60}\n")
total = walk_dir(root_path, extensions, exclude_dirs)
print(f"\n{'=' * 60}")
print(f"总行数: {total}")
print(f"{'=' * 60}\n")

12
dist-api/ai-GLAEBMJM.js Normal file
View File

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

View File

@@ -0,0 +1,15 @@
// api/utils/logger.ts
var createLogger = () => {
const isProd = process.env.NODE_ENV === "production";
const debug = isProd ? () => {
} : console.debug.bind(console);
const info = console.info.bind(console);
const warn = console.warn.bind(console);
const error = console.error.bind(console);
return { debug, info, warn, error };
};
var logger = createLogger();
export {
logger
};

View File

@@ -0,0 +1,39 @@
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
};

110
dist-api/chunk-74TMTGBG.js Normal file
View File

@@ -0,0 +1,110 @@
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
};

163
dist-api/chunk-ER4KPD22.js Normal file
View File

@@ -0,0 +1,163 @@
import {
NOTEBOOK_ROOT
} from "./chunk-74TMTGBG.js";
// shared/errors/index.ts
var AppError = class extends Error {
constructor(code, message, statusCode = 500, details) {
super(message);
this.code = code;
this.name = "AppError";
this.statusCode = statusCode;
this.details = details;
}
statusCode;
details;
toJSON() {
return {
name: this.name,
code: this.code,
message: this.message,
statusCode: this.statusCode,
details: this.details
};
}
};
var ValidationError = class extends AppError {
constructor(message, details) {
super("VALIDATION_ERROR", message, 400, details);
this.name = "ValidationError";
}
};
var NotFoundError = class extends AppError {
constructor(message = "Resource not found", details) {
super("NOT_FOUND", message, 404, details);
this.name = "NotFoundError";
}
};
var AccessDeniedError = class extends AppError {
constructor(message = "Access denied", details) {
super("ACCESS_DENIED", message, 403, details);
this.name = "AccessDeniedError";
}
};
var BadRequestError = class extends AppError {
constructor(message, details) {
super("BAD_REQUEST", message, 400, details);
this.name = "BadRequestError";
}
};
var NotADirectoryError = class extends AppError {
constructor(message = "\u4E0D\u662F\u76EE\u5F55", details) {
super("NOT_A_DIRECTORY", message, 400, details);
this.name = "NotADirectoryError";
}
};
var AlreadyExistsError = class extends AppError {
constructor(message = "Resource already exists", details) {
super("ALREADY_EXISTS", message, 409, details);
this.name = "AlreadyExistsError";
}
};
var ForbiddenError = class extends AppError {
constructor(message = "\u7981\u6B62\u8BBF\u95EE", details) {
super("FORBIDDEN", message, 403, details);
this.name = "ForbiddenError";
}
};
var UnsupportedMediaTypeError = class extends AppError {
constructor(message = "\u4E0D\u652F\u6301\u7684\u5A92\u4F53\u7C7B\u578B", details) {
super("UNSUPPORTED_MEDIA_TYPE", message, 415, details);
this.name = "UnsupportedMediaTypeError";
}
};
var ResourceLockedError = class extends AppError {
constructor(message = "\u8D44\u6E90\u5DF2\u9501\u5B9A", details) {
super("RESOURCE_LOCKED", message, 423, details);
this.name = "ResourceLockedError";
}
};
var InternalError = class extends AppError {
constructor(message = "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF", details) {
super("INTERNAL_ERROR", message, 500, details);
this.name = "InternalError";
}
};
function isAppError(error) {
return error instanceof AppError;
}
function isNodeError(error) {
return error instanceof Error && "code" in error;
}
// api/utils/pathSafety.ts
import path from "path";
var DANGEROUS_PATTERNS = [
/\.\./,
/\0/,
/%2e%2e[%/]/i,
/%252e%252e[%/]/i,
/\.\.%2f/i,
/\.\.%5c/i,
/%c0%ae/i,
/%c1%9c/i,
/%c0%ae%c0%ae/i,
/%c1%9c%c1%9c/i,
/\.\.%c0%af/i,
/\.\.%c1%9c/i,
/%252e/i,
/%uff0e/i,
/%u002e/i
];
var DOUBLE_ENCODE_PATTERNS = [
/%25[0-9a-fA-F]{2}/,
/%[0-9a-fA-F]{2}%[0-9a-fA-F]{2}/
];
var normalizeRelPath = (input) => {
const trimmed = input.replace(/\0/g, "").trim();
return trimmed.replace(/^[/\\]+/, "");
};
var containsPathTraversal = (input) => {
const decoded = decodeURIComponentSafe(input);
return DANGEROUS_PATTERNS.some((pattern) => pattern.test(input) || pattern.test(decoded));
};
var containsDoubleEncoding = (input) => {
return DOUBLE_ENCODE_PATTERNS.some((pattern) => pattern.test(input));
};
var hasPathSecurityIssues = (input) => {
return containsPathTraversal(input) || containsDoubleEncoding(input);
};
var decodeURIComponentSafe = (input) => {
try {
return decodeURIComponent(input);
} catch {
return input;
}
};
var resolveNotebookPath = (relPath) => {
if (hasPathSecurityIssues(relPath)) {
throw new AccessDeniedError("Path traversal detected");
}
const safeRelPath = normalizeRelPath(relPath);
const notebookRoot = path.resolve(NOTEBOOK_ROOT);
const fullPath = path.resolve(notebookRoot, safeRelPath);
if (!fullPath.startsWith(notebookRoot)) {
throw new AccessDeniedError("Access denied");
}
return { safeRelPath, fullPath };
};
export {
ValidationError,
NotFoundError,
BadRequestError,
NotADirectoryError,
AlreadyExistsError,
ForbiddenError,
UnsupportedMediaTypeError,
ResourceLockedError,
InternalError,
isAppError,
isNodeError,
resolveNotebookPath
};

View File

@@ -0,0 +1,20 @@
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
};

234
dist-api/chunk-M2SZ5AIA.js Normal file
View File

@@ -0,0 +1,234 @@
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
};

912
dist-api/chunk-QS2CMBFP.js Normal file
View File

@@ -0,0 +1,912 @@
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
};

586
dist-api/chunk-R5LQJNQE.js Normal file
View File

@@ -0,0 +1,586 @@
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
};

313
dist-api/chunk-T5RPAMG6.js Normal file
View File

@@ -0,0 +1,313 @@
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
};

125
dist-api/chunk-TSQNCXAS.js Normal file
View File

@@ -0,0 +1,125 @@
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
};

399
dist-api/chunk-V2OWYGQG.js Normal file
View File

@@ -0,0 +1,399 @@
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
};

280
dist-api/chunk-W5TDYTXE.js Normal file
View File

@@ -0,0 +1,280 @@
import {
resolveNotebookPath
} from "./chunk-ER4KPD22.js";
import {
asyncHandler,
createApiModule,
defineApiModule,
defineEndpoints,
successResponse
} from "./chunk-74TMTGBG.js";
// shared/modules/remote/api.ts
var REMOTE_ENDPOINTS = defineEndpoints({
getConfig: { path: "/config", method: "GET" },
saveConfig: { path: "/config", method: "POST" },
getScreenshot: { path: "/screenshot", method: "GET" },
saveScreenshot: { path: "/screenshot", method: "POST" },
getData: { path: "/data", method: "GET" },
saveData: { path: "/data", method: "POST" }
});
// shared/modules/remote/index.ts
var REMOTE_MODULE = defineApiModule({
id: "remote",
name: "\u8FDC\u7A0B",
basePath: "/remote",
order: 25,
version: "1.0.0",
endpoints: REMOTE_ENDPOINTS
});
// api/modules/remote/service.ts
import fs from "fs/promises";
import path from "path";
var REMOTE_DIR = "remote";
var RemoteService = class {
constructor(deps = {}) {
this.deps = deps;
}
getRemoteDir() {
const { fullPath } = resolveNotebookPath(REMOTE_DIR);
return { relPath: REMOTE_DIR, fullPath };
}
getDeviceDir(deviceName) {
const safeName = this.sanitizeFileName(deviceName);
const { fullPath } = resolveNotebookPath(path.join(REMOTE_DIR, safeName));
return { relPath: path.join(REMOTE_DIR, safeName), fullPath };
}
sanitizeFileName(name) {
return name.replace(/[<>:"/\\|?*]/g, "_").trim() || "unnamed";
}
getDeviceConfigPath(deviceName) {
const { fullPath } = this.getDeviceDir(deviceName);
return path.join(fullPath, "config.json");
}
getDeviceScreenshotPath(deviceName) {
const { fullPath } = this.getDeviceDir(deviceName);
return path.join(fullPath, "screenshot.png");
}
getDeviceDataPath(deviceName) {
const { fullPath } = this.getDeviceDir(deviceName);
return path.join(fullPath, "data.json");
}
async ensureDir(dirPath) {
await fs.mkdir(dirPath, { recursive: true });
}
async getDeviceNames() {
const { fullPath } = this.getRemoteDir();
try {
const entries = await fs.readdir(fullPath, { withFileTypes: true });
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
return dirs;
} catch {
return [];
}
}
async getConfig() {
const deviceNames = await this.getDeviceNames();
const devices = await Promise.all(
deviceNames.map(async (name) => {
try {
const configPath = this.getDeviceConfigPath(name);
const content = await fs.readFile(configPath, "utf-8");
const deviceConfig = JSON.parse(content);
return {
id: deviceConfig.id || name,
deviceName: name,
serverHost: deviceConfig.serverHost || "",
desktopPort: deviceConfig.desktopPort || 3e3,
gitPort: deviceConfig.gitPort || 3001,
openCodePort: deviceConfig.openCodePort || 3002,
fileTransferPort: deviceConfig.fileTransferPort || 3003,
password: deviceConfig.password || ""
};
} catch {
return {
id: name,
deviceName: name,
serverHost: "",
desktopPort: 3e3,
gitPort: 3001,
openCodePort: 3002,
fileTransferPort: 3003,
password: ""
};
}
})
);
return { devices };
}
async saveConfig(config) {
const { fullPath: remoteDirFullPath } = this.getRemoteDir();
await this.ensureDir(remoteDirFullPath);
const existingDevices = await this.getDeviceNames();
const newDeviceNames = config.devices.map((d) => this.sanitizeFileName(d.deviceName));
for (const oldDevice of existingDevices) {
if (!newDeviceNames.includes(oldDevice)) {
try {
const oldDir = path.join(remoteDirFullPath, oldDevice);
await fs.rm(oldDir, { recursive: true, force: true });
} catch {
}
}
}
for (const device of config.devices) {
const deviceDir = this.getDeviceDir(device.deviceName);
await this.ensureDir(deviceDir.fullPath);
const deviceConfigPath = this.getDeviceConfigPath(device.deviceName);
const deviceConfig = {
id: device.id,
serverHost: device.serverHost,
desktopPort: device.desktopPort,
gitPort: device.gitPort,
openCodePort: device.openCodePort,
fileTransferPort: device.fileTransferPort,
password: device.password || ""
};
await fs.writeFile(deviceConfigPath, JSON.stringify(deviceConfig, null, 2), "utf-8");
}
}
async getScreenshot(deviceName) {
if (!deviceName) {
return null;
}
const screenshotPath = this.getDeviceScreenshotPath(deviceName);
try {
return await fs.readFile(screenshotPath);
} catch {
return null;
}
}
async saveScreenshot(dataUrl, deviceName) {
console.log("[RemoteService] saveScreenshot:", { deviceName, dataUrlLength: dataUrl?.length });
if (!deviceName || deviceName.trim() === "") {
console.warn("[RemoteService] saveScreenshot skipped: no deviceName");
return;
}
const deviceDir = this.getDeviceDir(deviceName);
await this.ensureDir(deviceDir.fullPath);
const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
const buffer = Buffer.from(base64Data, "base64");
const screenshotPath = this.getDeviceScreenshotPath(deviceName);
await fs.writeFile(screenshotPath, buffer);
}
async getData(deviceName) {
if (!deviceName || deviceName.trim() === "") {
return null;
}
const dataPath = this.getDeviceDataPath(deviceName);
try {
const content = await fs.readFile(dataPath, "utf-8");
return JSON.parse(content);
} catch {
return null;
}
}
async saveData(data, deviceName) {
if (!deviceName || deviceName.trim() === "") {
console.warn("[RemoteService] saveData skipped: no deviceName");
return;
}
const deviceDir = this.getDeviceDir(deviceName);
await this.ensureDir(deviceDir.fullPath);
const dataPath = this.getDeviceDataPath(deviceName);
await fs.writeFile(dataPath, JSON.stringify(data, null, 2), "utf-8");
}
};
var createRemoteService = (deps) => {
return new RemoteService(deps);
};
// api/modules/remote/routes.ts
import express from "express";
var createRemoteRoutes = (deps) => {
const router = express.Router();
const { remoteService: remoteService2 } = deps;
router.get(
"/config",
asyncHandler(async (req, res) => {
const config = await remoteService2.getConfig();
successResponse(res, config);
})
);
router.post(
"/config",
asyncHandler(async (req, res) => {
const config = req.body;
await remoteService2.saveConfig(config);
successResponse(res, null);
})
);
router.get(
"/screenshot",
asyncHandler(async (req, res) => {
const deviceName = req.query.device;
const buffer = await remoteService2.getScreenshot(deviceName);
if (!buffer) {
return successResponse(res, "");
}
const base64 = `data:image/png;base64,${buffer.toString("base64")}`;
successResponse(res, base64);
})
);
router.post(
"/screenshot",
asyncHandler(async (req, res) => {
const { dataUrl, deviceName } = req.body;
console.log("[Remote] saveScreenshot called:", { deviceName, hasDataUrl: !!dataUrl });
await remoteService2.saveScreenshot(dataUrl, deviceName);
successResponse(res, null);
})
);
router.get(
"/data",
asyncHandler(async (req, res) => {
const deviceName = req.query.device;
const data = await remoteService2.getData(deviceName);
successResponse(res, data);
})
);
router.post(
"/data",
asyncHandler(async (req, res) => {
const { deviceName, lastConnected } = req.body;
const data = {};
if (lastConnected !== void 0) {
data.lastConnected = lastConnected;
}
await remoteService2.saveData(data, deviceName);
successResponse(res, null);
})
);
return router;
};
var remoteService = new RemoteService();
var routes_default = createRemoteRoutes({ remoteService });
// api/modules/remote/index.ts
var createRemoteModule = () => {
return createApiModule(REMOTE_MODULE, {
routes: (container) => {
const remoteService2 = container.getSync("remoteService");
return createRemoteRoutes({ remoteService: remoteService2 });
},
lifecycle: {
onLoad: (container) => {
container.register("remoteService", () => new RemoteService());
}
}
});
};
var remote_default = createRemoteModule;
export {
RemoteService,
createRemoteService,
createRemoteRoutes,
createRemoteModule,
remote_default
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
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
};

View File

@@ -0,0 +1,28 @@
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
};

33
dist-api/todo-GDTZZEQA.js Normal file
View File

@@ -0,0 +1,33 @@
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
};

2114
docs/vercel/VERCEL_GUIDE.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,478 @@
# Vercel Marketplace 集成文档
> 更新时间2026年3月12日
> 数据来源https://vercel.com/marketplace
---
## 一、定价模式说明
| 标记 | 含义 | 说明 |
|------|------|------|
| 🆓 | 免费开源 | 完全免费,开源项目 |
| 🆓Free | 免费套餐 | 有免费额度,付费升级 |
| 💰 | 付费订阅 | 无免费层或限制大 |
| 🏢 | 企业定价 | 需联系销售 |
| ❓ | 需查询 | 定价不明确,需查看官网 |
---
## 二、分类列表共22个
| 分类 | URL |
|------|-----|
| AI Agents & Services | /marketplace/category/agents |
| AI | /marketplace/category/ai |
| Analytics | /marketplace/category/analytics |
| Authentication | /marketplace/category/authentication |
| CMS | /marketplace/category/cms |
| Commerce | /marketplace/category/commerce |
| Database | /marketplace/category/database |
| DevTools | /marketplace/category/dev-tools |
| Experimentation | /marketplace/category/experimentation |
| Flags | /marketplace/category/flags |
| Logging | /marketplace/category/logging |
| Messaging | /marketplace/category/messaging |
| Monitoring | /marketplace/category/monitoring |
| Observability | /marketplace/category/observability |
| Payments | /marketplace/category/payments |
| Productivity | /marketplace/category/productivity |
| Searching | /marketplace/category/searching |
| Security | /marketplace/category/security |
| Storage | /marketplace/category/storage |
| Testing | /marketplace/category/testing |
| Video | /marketplace/category/video |
| Workflow | /marketplace/category/workflow |
---
## 三、推荐模板Next.js专用
| 模板名称 | 定价 | 描述 | URL |
|----------|------|------|-----|
| Next.js AI Chatbot | 🆓 | 全功能的可定制 Next.js AI 聊天机器人 | https://vercel.com/templates/ai/nextjs-ai-chatbot |
| Statsig + Flags SDK | 🆓Free | 使用 Statsig 和 Flags SDK 在静态页面上运行实验 | https://vercel.com/templates/edge-config/statsig-experimentation-with-flags-sdk |
| Next.js + Mux Video | 🆓Free | 使用 Mux 为 Next.js 应用添加视频 | https://vercel.com/templates/next.js/next-video-starter |
---
## 四、Native Integrations原生集成
### 4.1 Storage存储- 12个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Neon** | 🆓Free | Postgres无服务器平台0.5GB免费 | /marketplace/neon |
| **AWS** | 🏢 | 亚马逊云服务 | /marketplace/aws |
| **Upstash** | 🆓Free | 无服务器Redis/Vector10K命令/天免费 | /marketplace/upstash |
| **Supabase** | 🆓 | 开源Postgres开发平台 | /marketplace/supabase |
| **Redis** | 🆓Free | 无服务器Redis30MB免费 | /marketplace/redis |
| **Nile** | 🆓Free | PostgreSQL for B2B | /marketplace/nile |
| **MotherDuck** | ❓ | 分析的无服务器后端 | /marketplace/motherduck |
| **Convex** | 🆓 | 开源后端平台 | /marketplace/convex |
| **Prisma** | 🆓 | 开源ORM/Postgres | /marketplace/prisma |
| **Turso Cloud** | 🆓Free | SQLite500MB免费 | /marketplace/tursocloud |
| **MongoDB Atlas** | 🏢 | MongoDB官方NoSQL数据库 | /marketplace/mongodbatlas |
| **Mixedbread** | ❓ | 多模态搜索API | /marketplace/mixedbread |
### 4.2 AI人工智能- 4个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **xAI (Grok)** | 💰 | xAI的Grok AI | /marketplace/xai |
| **Groq** | ❓ | AI快速推理服务 | /marketplace/groq |
| **fal** | ❓ | 生成式媒体平台 | /marketplace/fal |
| **Deep Infra** | ❓ | Deep Infra AI集成 | /marketplace/deepinfra |
### 4.3 Observability可观测性- 5个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Sentry** | 🆓Free | 错误监控5K错误/月免费 | /marketplace/sentry |
| **Dash0** | ❓ | 日志、追踪、指标 | /marketplace/dash0 |
| **Braintrust** | ❓ | AI评估监控平台 | /marketplace/braintrust |
| **Kubiks** | ❓ | 日志追踪、仪表盘、警报 | /marketplace/kubiks |
| **Rollbar** | 🆓Free | 实时崩溃报告,$0/月 | /marketplace/rollbar |
### 4.4 Monitoring监控- 1个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Checkly** | 💰 | Playwright监控$29/月起 | /marketplace/checkly |
### 4.5 Web Automation网页自动化- 2个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Browserbase** | ❓ | 无服务器浏览器自动化 | /marketplace/browserbase |
| **Kernel** | ❓ | AI代理互联网访问infra | /marketplace/kernel |
### 4.6 Code Review代码审查- 3个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **cubic** | ❓ | 复杂代码库AI审查 | /marketplace/cubic |
| **CodeRabbit** | 🆓Free | 代码审查500行/月免费,$9/月起 | /marketplace/coderabbit |
| **Sourcery** | 🆓Free | AI代码审查免费版无限 | /marketplace/sourcery |
### 4.7 Support Agent客服代理- 2个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **AssistLoop** | ❓ | AI客服代理 | /marketplace/assistloop |
| **Chatbase** | ❓ | 免费AI客服代理 | /marketplace/chatbase |
### 4.8 Searching搜索- 1个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Parallel Web Systems** | ❓ | AI网页搜索 | /marketplace/parallel |
### 4.9 Code Security代码安全- 1个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Corridor** | ❓ | AI编码安全审查 | /marketplace/corridor |
### 4.10 Payments支付- 1个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Stripe** | 🆓Free | 支付平台,交易手续费 | /marketplace/stripe |
### 4.11 Testing测试- 1个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Autonoma AI** | ❓ | AI端到端测试 | /marketplace/autonoma-ai |
### 4.12 Analytics分析- 3个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Statsig** | ❓ | 特性标志、实验、分析 | /marketplace/statsig |
| **Hypertune** | ❓ | 类型安全特性标志 | /marketplace/hypertune |
| **PostHog** | 🆓Free | 分析、会话回放1M事件/月免费 | /marketplace/posthog |
### 4.13 Authentication认证- 2个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Descope** | 🆓Free | 拖放式认证 | /marketplace/descope |
| **Clerk** | 🆓Free | 认证服务10K月活免费 | /marketplace/clerk |
### 4.14 DevTools开发工具- 1个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Inngest** | 🆓 | 开源AI工作流平台 | /marketplace/inngest |
### 4.15 Experimentation实验- 1个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **GrowthBook** | 🆓 | 开源特性标志实验 | /marketplace/growthbook |
### 4.16 Video视频- 1个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Mux** | 🆓Free | 视频服务1K分钟免费 | /marketplace/mux |
### 4.17 CMS内容管理- 1个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Sanity** | 🆓Free | 内容平台10K文档免费 | /marketplace/sanity |
---
## 五、External Integrations外部集成
### 5.1 AI人工智能- 6个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Deep Infra** | ❓ | AI集成 | /marketplace/deepinfra |
| **ElevenLabs** | 💰 | AI文本转语音 | /marketplace/elevenlabs |
| **LMNT** | ❓ | 文本转语音&语音克隆 | /marketplace/lmnt |
| **Perplexity API** | ❓ | Perplexity LLM | /marketplace/pplx-api |
| **Replicate** | 🆓Free | AI模型运行有免费额度 | /marketplace/replicate |
| **Together AI** | 💰 | 生成式AI云平台 | /marketplace/together-ai |
### 5.2 Analytics分析- 7个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Hypertune** | ❓ | 特性标志 | /marketplace/hypertune |
| **LaunchDarkly** | 💰 | 特性标志,$30/月起 | /marketplace/launchdarkly |
| **Statsig** | ❓ | 特性标志实验 | /marketplace/statsig |
| **Vercel Web Analytics** | 💰 | 第一方分析,$20/月 | /marketplace/vercel-analytics |
| **DevCycle** | ❓ | 特性标志 | /marketplace/devcycle |
| **Kameleoon** | ❓ | A/B测试 | /marketplace/kameleoon |
| **Split** | 💰 | 特性标志,$49/月起 | /marketplace/split |
### 5.3 Authentication认证- 2个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Auth0** | 🆓Free | 认证服务7K月活免费 | /marketplace/auth0 |
| **Clerk** | 🆓Free | 认证服务10K月活免费 | /marketplace/clerk |
> ⚠️ **页面显示错误**Neon 和 Supabase 在页面上被错误地归类为 Authentication它们实际是 Storage 服务(提供 Postgres 数据库)。
### 5.4 CMS内容管理- 10个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Agility CMS** | ❓ | Headless CMS | /marketplace/agility-cms |
| **Builder.io** | 🏢 | 可视化CMS企业定价 | https://vercel.com/new/templates?search=builder&cms=builder.io |
| **ButterCMS** | ❓ | Headless CMS | /marketplace/buttercms |
| **Contentful** | 🆓Free | 内容平台25K记录免费 | /marketplace/contentful |
| **Contentstack** | 🏢 | 全渠道CMS企业 | https://vercel.com/guides/integrate-vercel-and-contentstack |
| **DatoCMS** | ❓ | Headless CMS | /marketplace/datocms |
| **Formspree** | ❓ | 表单后端 | /marketplace/formspree |
| **Makeswift** | ❓ | Next.js可视化构建 | /marketplace/makeswift |
| **Sanity** | 🆓Free | 内容平台10K文档免费 | /marketplace/sanity |
| **Sitecore XM Cloud** | 🏢 | SaaS CMS | https://vercel.com/docs/integrations/sitecore |
### 5.5 Commerce商务- 7个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **BigCommerce** | 💰 | 电商平台,$29/月起 | https://vercel.com/templates/next.js/nextjs-commerce |
| **Saleor** | 🆓 | 开源商务API | https://vercel.com/templates/next.js/nextjs-saleor-commerce |
| **Salesforce Commerce Cloud** | 🏢 | 商务平台,企业 | https://vercel.com/templates/next.js/salesforce-commerce-cloud-starter |
| **Shopify** | 💰 | Headless电商$29/月起 | https://vercel.com/docs/integrations/shopify |
| **Sitecore OrderCloud** | 🏢 | B2X商务 | /marketplace/ordercloud |
| **Swell** | 🆓 | 开源Headless电商 | /marketplace/swell |
| **Wix** | 💰 | 业务解决方案 | /marketplace/wix |
### 5.6 DevTools开发工具- 8个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Deploy Summary** | 🆓 | Vercel部署摘要 | /marketplace/deploy-summary |
| **Doppler** | ❓ | 密钥管理 | /marketplace/doppler |
| **Inngest** | 🆓 | 开源工作流平台 | /marketplace/inngest |
| **Liveblocks** | 🆓Free | 实时协作,有免费层 | https://vercel.com/templates/next.js/liveblocks-starter-kit |
| **Railway** | 🆓Free | 部署平台,$5额度免费 | /marketplace/railway |
| **Raycast** | ❓ | Vercel项目集成 | /marketplace/raycast |
| **Svix** | ❓ | Webhook服务 | /marketplace/svix |
| **Terraform** | 🆓 | 开源IaC工具 | /marketplace/terraform |
> ⚠️ **注**AWS 不在 External Integrations 列表中(通过 /partners/aws 跳转)
### 5.7 Logging日志- 8个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Axiom** | 🏢 | 日志服务,企业 | /marketplace/axiom |
| **Baselime** | 🏢 | 日志查询,企业 | /marketplace/baselime |
| **Better Stack** | ❓ | 日志服务 | /marketplace/betterstack |
| **GraphJSON** | ❓ | 日志可视化 | /marketplace/graphjson |
| **Logalert** | ❓ | 日志警报 | /marketplace/logalert |
| **Logflare** | ❓ | 日志服务 | /marketplace/logflare |
| **Profound** | ❓ | AI活动监控 | /marketplace/profound |
| **Sematext Logs** | ❓ | 日志服务 | /marketplace/sematext-logs |
### 5.8 Messaging消息- 4个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Knock** | ❓ | 消息API | /marketplace/knock |
| **Novu** | 🆓 | 开源通知基础设施 | /marketplace/novu |
| **Resend** | 🆓Free | 邮件服务3K邮件免费 | /marketplace/resend |
| **Slack** | 🆓Free | 消息通知,免费版可用 | /marketplace/slack |
### 5.9 Monitoring监控- 3个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Checkly** | 💰 | 监控平台,$29/月起 | /marketplace/checkly |
| **DebugBear** | 💰 | 性能监控,$39/月起 | /marketplace/debugbear |
| **Zeitgeist** | ❓ | Vercel部署管理 | /marketplace/zeitgeist-app |
### 5.10 Observability可观测性- 8个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Dash0** | ❓ | 可观测平台 | /marketplace/dash0 |
| **Datadog** | 💰 | 监控平台,$15/月起 | /marketplace/datadog |
| **Highlight** | ❓ | 调试平台 | /marketplace/highlight |
| **HyperDX** | ❓ | 可观测平台 | /marketplace/hyperdx |
| **New Relic** | 💰 | 监控平台,$49/月起 | /marketplace/newrelic |
| **Sentry** | 🆓Free | 错误监控5K错误/月免费 | /marketplace/sentry |
| **shipshape** | ❓ | 部署仪表盘 | /marketplace/shipshape |
| **Middleware** | ❓ | AI云可观测平台 | /marketplace/middleware |
### 5.11 Productivity生产力- 3个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **GitHub Issues** | 🆓 | 评论转GitHub Issue | /marketplace/gh-issues |
| **Jira** | 🆓 | 评论转Jira | /marketplace/jira |
| **Linear** | 🆓 | 评论转Linear | /marketplace/linear |
### 5.12 Searching搜索- 2个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Meilisearch Cloud** | 🏢 | 搜索服务,企业 | /marketplace/meilisearch-cloud |
| **Upstash** | 🆓Free | Redis搜索10K命令/天免费 | /marketplace/upstash |
### 5.13 Security安全- 1个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Arcjet** | ❓ | 代码化安全 | /marketplace/arcjet |
### 5.14 Storage存储- 13个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **AWS S3** | 🏢 | 对象存储 | https://vercel.com/templates/next.js/aws-s3-image-upload-nextjs |
| **Azure Cosmos DB** | 🏢 | NoSQL数据库 | /marketplace/azurecosmosdb |
| **Couchbase Capella** | 🏢 | NoSQL云数据库 | /marketplace/couchbase-capella |
| **DataStax Astra DB** | 🏢 | 向量数据库 | /marketplace/datastax-astra-db |
| **Hasura** | 🆓 | 开源GraphQL API | /marketplace/hasura |
| **Pinecone** | 🏢 | 向量数据库 | /marketplace/pinecone |
| **SingleStoreDB Cloud** | 🏢 | 时序数据库 | /marketplace/singlestoredb-cloud |
| **StepZen** | 🆓 | 开源GraphQL | /marketplace/stepzen |
| **Thin Backend** | 🆓 | 开源Postgres后端 | /marketplace/thin |
| **TiDB Cloud** | 🏢 | 向量MySQL | /marketplace/tidb-cloud |
| **Tinybird** | 🏢 | 实时分析 | /marketplace/tinybird |
| **Xata** | ❓ | 数据库服务 | /marketplace/xata |
| **Tigris** | ❓ | 对象存储 | /marketplace/tigris |
### 5.15 Testing测试- 1个
| 名称 | 定价 | 描述 | URL |
|------|------|------|-----|
| **Meticulous AI** | ❓ | AI端到端测试 | /marketplace/meticulous |
---
## 六、Coming Soon即将推出
| 名称 | 定价 | 描述 |
|------|------|------|
| **Browser Use** | ❓ | 网页自动化,可订阅通知 |
---
## 七、统计汇总
| 类别 | 数量 |
|------|------|
| **总分类数** | 22 |
| **模板数** | 3 |
| **Native Integrations** | 42 |
| **External Integrations** | 90 |
| 🆓 免费开源 | ~15 |
| 🆓Free 免费套餐 | ~25 |
| 💰 付费订阅 | ~15 |
| 🏢 企业定价 | ~20 |
| ❓ 需查询定价 | ~35 |
---
## 八、Native Integrations 完整列表42个
### Storage存储- 12个
Neon, AWS, Upstash, Supabase, Redis, Nile, MotherDuck, Convex, Prisma, Turso Cloud, MongoDB Atlas, Mixedbread
### AI人工智能- 4个
xAI (Grok), Groq, fal, Deep Infra
### Observability可观测性- 5个
Sentry, Dash0, Braintrust, Kubiks, Rollbar
### Monitoring监控- 1个
Checkly
### Web Automation网页自动化- 2个
Browserbase, Kernel
### Code Review代码审查- 3个
cubic, CodeRabbit, Sourcery
### Support Agent客服代理- 2个
AssistLoop, Chatbase
### Searching搜索- 1个
Parallel Web Systems
### Code Security代码安全- 1个
Corridor
### Payments支付- 1个
Stripe
### Testing测试- 1个
Autonoma AI
### Analytics分析- 3个
Statsig, Hypertune, PostHog
### Authentication认证- 2个
Descope, Clerk
### DevTools开发工具- 1个
Inngest
### Experimentation实验- 1个
GrowthBook
### Video视频- 1个
Mux
### CMS内容管理- 1个
Sanity
---
## 九、External Integrations 分类统计89个
| 类别 | 数量 |
|------|------|
| AI | 6 |
| Analytics | 7 |
| Authentication | 2 |
| CMS | 10 |
| Commerce | 7 |
| DevTools | 8 |
| Logging | 8 |
| Messaging | 4 |
| Monitoring | 3 |
| Observability | 8 |
| Productivity | 3 |
| Searching | 2 |
| Security | 1 |
| Storage | 13 |
| Testing | 1 |
---
## 十、快速参考
### ⚠️ Vercel专有仅6个
这些服务只能在Vercel平台使用
- Vercel Web Analytics
- Deploy Summary
- Raycast
- GitHub Issues
- Jira
- Linear
### 🆓 免费开源(完全免费)
Supabase, Convex, Inngest, Novu, GrowthBook, Saleor, Swell, Hasura, StepZen, Thin Backend, Terraform, Prisma, Sourcery
### 🆓Free 免费套餐(适合个人项目)
Neon, Upstash, Clerk, Auth0, Sentry, PostHog, Mux, Stripe, Resend, Railway, CodeRabbit, Sanity, Contentful, Turso, Redis, Descope, Replicate, Liveblocks, Rollbar
### 💰 付费订阅
Vercel Analytics($20/月), Checkly($29/月), DebugBear($39/月), Datadog($15/月), New Relic($49/月), LaunchDarkly($30/月), BigCommerce($29/月), Shopify($29/月), ElevenLabs, Together AI
### 🏢 企业定价
AWS, Azure, MongoDB Atlas, Pinecone, DataStax, Couchbase, TiDB, Tinybird, Meilisearch, Contentstack, Builder.io, Sitecore, Salesforce

View File

@@ -0,0 +1,983 @@
# Vercel MVP Idea 验证系统 v2.0
> 精简版设计文档 - 专注核心价值
---
## 一、系统定位
**一句话定义**:帮你分析 idea 可行性,生成网站设计文档,追踪验证数据。
**核心价值**
1. **AI 深度分析** - 不是简单的评分,而是给出具体的市场分析和建议
2. **设计文档生成** - 输出专业的网站设计文档,你用专业工具构建
3. **数据追踪** - 部署后追踪访问和转化数据
---
## 二、核心流程
```
┌─────────────────────────────────────────────────────────────────┐
│ 简化版流程 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 1. 输入 │ ──▶ │ 2. AI │ ──▶ │ 3. 生成 │
│ Idea │ │ 深度分析 │ │ 设计文档 │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐
│ 4. 你自己 │
│ 构建部署 │
└─────────────┘
┌─────────────┐
│ 5. 追踪 │
│ 验证数据 │
└─────────────┘
```
---
## 三、功能模块
### 3.1 Idea 管理
#### 数据模型
```typescript
interface Idea {
id: string
name: string // "AI 写作助手"
description: string // 一句话描述
category: 'tool' | 'content' | 'saas' | 'ecommerce' | 'other'
targetKeywords: string[] // 目标关键词
status: 'draft' | 'analyzing' | 'ready' | 'deployed' | 'validated' | 'abandoned'
// AI 分析结果
analysis?: IdeaAnalysis
// 设计文档
designDoc?: DesignDocument
// 部署信息
deployment?: {
url: string
deployedAt: Date
}
// 验证数据
validation?: {
visits: number
signups: number
lastChecked: Date
}
createdAt: Date
updatedAt: Date
}
```
#### 功能
- 创建 / 编辑 / 删除 Idea
- 状态管理(草稿 → 分析中 → 就绪 → 已部署 → 验证中 → 已验证 / 已放弃)
- 列表展示 + 筛选
---
### 3.2 AI 深度分析
#### 分析维度
```
┌─────────────────────────────────────────────────────────────────┐
│ AI 分析输出 │
└─────────────────────────────────────────────────────────────────┘
1. 市场分析
├── 市场规模估算
├── 目标用户画像
├── 用户痛点分析
└── 竞品概览3-5个主要竞品
2. 可行性评估
├── 技术难度 (1-5星)
├── 资金需求 (低/中/高)
├── 时间成本估算
└── 总体可行性得分 (0-100)
3. 差异化建议
├── 现有竞品不足之处
├── 你的独特切入点
└── MVP 功能建议
4. 变现路径
├── 建议定价
├── 收入模型
└── 预估时间线
5. 风险提示
├── 主要风险点
└── 应对建议
6. 行动建议
├── 是否值得继续
├── 下一步行动清单
└── 推荐验证方式
```
#### 数据模型
```typescript
interface IdeaAnalysis {
// 市场分析
market: {
size: string // "中小型市场,预计月搜索量 10K-50K"
targetUsers: string[] // ["内容创作者", "营销人员", "自由职业者"]
painPoints: string[] // 用户痛点
competitors: Competitor[] // 竞品列表
}
// 可行性
feasibility: {
technicalDifficulty: 1 | 2 | 3 | 4 | 5
fundRequirement: 'low' | 'medium' | 'high'
timeEstimate: string // "2-4周可完成 MVP"
overallScore: number // 0-100
}
// 差异化
differentiation: {
competitorGaps: string[] // 竞品不足
uniqueAngle: string // 你的切入点
mvpFeatures: string[] // 建议 MVP 功能
}
// 变现
monetization: {
pricingModel: 'subscription' | 'one-time' | 'freemium' | 'usage-based'
suggestedPrice: { min: number, max: number, currency: string }
revenueModel: string // 收入模型说明
timeline: string // 预估盈利时间
}
// 风险
risks: {
main: string // 主要风险
mitigation: string // 应对建议
}[]
// 行动建议
recommendation: {
shouldProceed: boolean // 是否建议继续
confidence: number // 信心指数 0-100
nextSteps: string[] // 下一步行动
validationMethod: string // 推荐验证方式
}
analyzedAt: Date
}
interface Competitor {
name: string
url?: string
description: string
strengths: string[]
weaknesses: string[]
pricing?: string
}
```
#### AI Prompt 设计
```typescript
const ANALYSIS_PROMPT = `
你是一位资深的创业顾问和产品经理。请对以下 idea 进行深度分析:
## Idea 信息
- 名称:{name}
- 描述:{description}
- 分类:{category}
- 目标关键词:{keywords}
## 请输出以下内容:
### 1. 市场分析
- 市场规模(基于你的知识判断)
- 目标用户画像3-5类
- 用户痛点3-5个
- 主要竞品3-5个包括名称、简介、优缺点、定价
### 2. 可行性评估
- 技术难度1-5星
- 资金需求(低/中/高)
- 时间成本估算
- 总体可行性得分0-100分
### 3. 差异化建议
- 竞品的不足之处
- 建议的独特切入点
- MVP 核心功能建议3-5个
### 4. 变现路径
- 建议定价模式
- 价格区间
- 收入模型
- 预估盈利时间
### 5. 风险提示
- 主要风险2-3个
- 应对建议
### 6. 行动建议
- 是否值得继续(是/否)
- 信心指数0-100
- 下一步行动清单
- 推荐验证方式
请以 JSON 格式输出。
`
```
---
### 3.3 设计文档生成
#### 文档结构
```typescript
interface DesignDocument {
id: string
ideaId: string
// 基本信息
projectName: string
projectSlug: string // URL 友好的名称
tagline: string // 一句话介绍
// 网站类型
siteType: 'waitlist' | 'landing' | 'pricing' | 'blog' | 'portfolio'
// 目标
primaryGoal: 'collect_emails' | 'show_product' | 'sell' | 'content'
// 页面结构
pages: PageDefinition[]
// 设计规范
design: {
colorScheme: ColorScheme
typography: Typography
components: ComponentSpec[]
}
// 内容
content: {
hero: HeroSection
features: FeatureSection
pricing?: PricingSection
faq?: FaqSection
cta: CtaSection
}
// SEO
seo: {
title: string
description: string
keywords: string[]
ogImage?: string
}
// 技术建议
techStack: {
framework: string // 推荐框架
hosting: string // 推荐托管
analytics: string[] // 分析工具
integrations: string[] // 集成建议
}
createdAt: Date
}
interface PageDefinition {
name: string // "首页", "定价页"
path: string // "/", "/pricing"
sections: string[] // 页面包含的区块
purpose: string // 页面目的
}
interface ColorScheme {
primary: string // 主色
secondary: string // 辅色
accent: string // 强调色
background: string // 背景色
text: string // 文字色
}
interface Typography {
headingFont: string
bodyFont: string
headingSizes: { h1: string, h2: string, h3: string }
bodySize: string
}
interface HeroSection {
headline: string
subheadline: string
cta: {
text: string
action: string // "signup" | "learn_more" | "get_started"
}
visual: {
type: 'image' | 'video' | 'illustration' | 'screenshot'
description: string
}
}
interface FeatureSection {
title: string
subtitle?: string
features: {
icon: string // 图标建议
title: string
description: string
}[]
}
interface PricingSection {
plans: {
name: string // "Free", "Pro", "Enterprise"
price: string // "$0", "$9.99/mo"
features: string[]
highlighted: boolean // 是否推荐
cta: string // 按钮文字
}[]
}
interface FaqSection {
items: {
question: string
answer: string
}[]
}
interface CtaSection {
headline: string
description: string
buttonText: string
}
```
#### 网站类型模板
| 类型 | 适用场景 | 核心目标 | 页面结构 |
|------|----------|----------|----------|
| **Waitlist** | 产品未上线 | 收集邮箱 | Hero + Features + Waitlist Form |
| **Landing** | 产品展示 | 展示 + 转化 | Hero + Features + Testimonials + CTA |
| **Pricing** | SaaS 产品 | 销售订阅 | Hero + Features + Pricing + FAQ |
| **Blog** | 内容/SEO | 流量 + 订阅 | Hero + Articles + Newsletter |
| **Portfolio** | 个人/作品 | 展示能力 | Hero + Projects + About + Contact |
---
### 3.4 数据追踪
#### 追踪内容
```typescript
interface ValidationData {
ideaId: string
// 访问数据
traffic: {
totalVisits: number
uniqueVisitors: number
avgDuration: string // "2:34"
bounceRate: number // 百分比
trend: 'up' | 'down' | 'stable'
}
// 转化数据
conversion: {
emailSignups: number
conversionRate: number // 百分比
sources: {
direct: number
organic: number
social: number
referral: number
}
}
// 时间线
timeline: {
date: string
visits: number
signups: number
}[]
lastUpdated: Date
}
```
#### 数据来源
- **Vercel Analytics API** - 访问量、访客数
- **自定义追踪** - 表单提交、按钮点击
- **UTM 参数** - 流量来源追踪
---
## 四、数据流设计
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 数据流 │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐
│ 用户输入 Idea │
│ - 名称 │
│ - 描述 │
│ - 分类 │
│ - 关键词 │
└────────┬─────────┘
┌──────────────────────────────┐
│ AI 分析引擎 │
│ (调用 LLM API) │
│ │
│ 输出: │
│ - 市场分析 │
│ - 可行性评估 │
│ - 差异化建议 │
│ - 变现路径 │
│ - 风险提示 │
│ - 行动建议 │
└──────────────┬───────────────┘
┌──────────────┴───────────────┐
│ 用户决定是否继续 │
└──────────────┬───────────────┘
│ 继续
┌──────────────────────────────┐
│ 选择网站类型 + 目标 │
│ - Waitlist / Landing / ... │
│ - 收集邮箱 / 展示 / 销售 │
└──────────────┬───────────────┘
┌──────────────────────────────┐
│ 生成设计文档 │
│ │
│ 包含: │
│ - 页面结构 │
│ - 设计规范 │
│ - 内容文案 │
│ - SEO 配置 │
│ - 技术建议 │
└──────────────┬───────────────┘
┌──────────────────────────────┐
│ 导出设计文档 │
│ - Markdown 格式 │
│ - JSON 格式 │
│ - 复制到剪贴板 │
└──────────────┬───────────────┘
┌──────────────────────────────┐
│ 你自己构建和部署 │
│ (使用 v0 / Cursor / 等) │
└──────────────┬───────────────┘
┌──────────────────────────────┐
│ 录入部署 URL │
│ 系统开始追踪数据 │
└──────────────────────────────┘
```
---
## 五、技术架构
### 5.1 技术栈
| 层级 | 技术 | 说明 |
|------|------|------|
| 桌面端 | Electron | 现有 |
| 前端 | React + TypeScript | 现有 |
| 样式 | Tailwind CSS | 现有 |
| 后端 | Express | 现有 |
| 数据库 | SQLite | 本地存储 |
| AI | OpenAI API / Anthropic API | LLM 调用 |
| 数据 | Vercel Analytics API | 访问数据 |
### 5.2 模块结构
```
api/modules/idea-validator/
├── index.ts # 模块导出
├── routes.ts # API 路由
├── service.ts # 业务逻辑
├── analyzer.ts # AI 分析逻辑
├── generator.ts # 设计文档生成
├── tracker.ts # 数据追踪
├── prompts.ts # AI Prompt 模板
├── types.ts # 类型定义
└── db/
├── ideas.ts # Idea 数据操作
├── analysis.ts # 分析结果存储
└── documents.ts # 设计文档存储
src/modules/idea-validator/
├── index.ts
├── pages/
│ └── IdeaValidator.tsx # 主面板
├── components/
│ ├── IdeaList.tsx # Idea 列表
│ ├── IdeaCard.tsx # Idea 卡片
│ ├── IdeaForm.tsx # 创建/编辑表单
│ ├── AnalysisView.tsx # 分析结果展示
│ ├── DesignDocView.tsx # 设计文档预览
│ ├── ExportPanel.tsx # 导出面板
│ ├── ValidationChart.tsx # 验证数据图表
│ └── Recommendation.tsx # AI 建议展示
└── stores/
└── ideaStore.ts # 状态管理
```
---
## 六、API 设计
### Idea 管理
```
GET /api/ideas # 获取所有 Idea
POST /api/ideas # 创建 Idea
GET /api/ideas/:id # 获取单个 Idea
PUT /api/ideas/:id # 更新 Idea
DELETE /api/ideas/:id # 删除 Idea
```
### AI 分析
```
POST /api/ideas/:id/analyze # 执行 AI 分析
GET /api/ideas/:id/analysis # 获取分析结果
```
### 设计文档
```
POST /api/ideas/:id/design # 生成设计文档
GET /api/ideas/:id/design # 获取设计文档
PUT /api/ideas/:id/design # 更新设计文档
GET /api/ideas/:id/design/export # 导出文档 (markdown/json)
```
### 数据追踪
```
POST /api/ideas/:id/deployment # 录入部署信息
GET /api/ideas/:id/validation # 获取验证数据
POST /api/ideas/:id/sync-analytics # 同步 Vercel 数据
```
---
## 七、页面设计
### 7.1 主面板
```
┌─────────────────────────────────────────────────────────────────────┐
│ Idea Validator [+ 新建 Idea] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ 💡 总 Idea │ │ ✅ 已验证 │ │ 🚀 已部署 │ │ 📧 邮箱 │ │
│ │ 12 │ │ 3 │ │ 5 │ │ 1,234 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 我的 Ideas 筛选 ▾ │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 💡 AI 写作助手 │ │
│ │ 可行性: 78/100 │ 状态: 🟢 已验证 │ 邮箱: 234 │ │
│ │ [查看分析] [设计文档] [验证数据] │ │
│ │ │ │
│ │ 💡 PDF 转换工具 │ │
│ │ 可行性: 65/100 │ 状态: 🚀 已部署 │ 邮箱: 89 │ │
│ │ [查看分析] [设计文档] [验证数据] │ │
│ │ │ │
│ │ 💡 时间追踪 App │ │
│ │ 可行性: 45/100 │ 状态: 📝 草稿 │ │ │
│ │ [继续分析] [删除] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### 7.2 创建 Idea 流程
```
Step 1: 基本信息
┌─────────────────────────────────────────────────────────────────────┐
│ 创建新 Idea Step 1/3 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Idea 名称 * │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ AI 写作助手 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 一句话描述 * │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 帮助内容创作者快速生成高质量文章的 AI 工具 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 分类 │
│ ○ 工具类 ● SaaS ○ 内容类 ○ 电商 ○ 其他 │
│ │
│ 目标关键词(按回车添加) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ AI writing ✕ content generator ✕ 写作工具 ✕ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ [取消] [下一步 →] │
└─────────────────────────────────────────────────────────────────────┘
Step 2: AI 分析中
┌─────────────────────────────────────────────────────────────────────┐
│ AI 正在分析... │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ │ │
│ │ 分析市场中... │ │
│ │ │ │
│ └─────────────────┘ │
│ │
│ ████████░░░░░░░░ 45% │
│ │
│ 正在分析竞品... │
│ │
└─────────────────────────────────────────────────────────────────────┘
Step 3: 分析结果
┌─────────────────────────────────────────────────────────────────────┐
│ 分析结果 [重新分析] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 🎯 可行性评分 │ │
│ │ │ │
│ │ ████████████████████████████░░░░░░░░░░ 78/100 │ │
│ │ │ │
│ │ 💡 AI 建议:这个 idea 值得尝试! │ │
│ │ 信心指数72% │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 📊 市场分析 │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ 市场规模:中小型,月搜索量约 20K-50K │ │
│ │ 目标用户:内容创作者、营销人员、自由职业者 │ │
│ │ 用户痛点写作效率低、灵感枯竭、SEO优化困难 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 🏆 竞品分析 │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ 1. Jasper AI - 功能强大但价格贵 ($49/月起) │ │
│ │ 2. Copy.ai - 简单易用但专业度不够 │ │
│ │ 3. Writesonic - 性价比高但中文支持弱 │ │
│ │ │ │
│ │ 💡 差异化机会:专注中文市场 + 更低价格 + 更简洁的体验 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 💰 变现建议 │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ 定价模式Freemium + 订阅 │ │
│ │ 建议价格:免费版 + $9.99/月 Pro版 │ │
│ │ 预估盈利6-12个月 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ⚠️ 风险提示 │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ 1. AI 领域竞争激烈,需快速迭代 │ │
│ │ 2. 依赖大模型 API成本需控制 │ │
│ │ │ │
│ │ 建议:从细分场景切入,如"小红书文案生成" │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 📋 下一步行动 │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ ✅ 选择一个细分场景(如小红书文案) │ │
│ │ ✅ 生成设计文档 │ │
│ │ ✅ 构建简单落地页,验证需求 │ │
│ │ ✅ 收集 100 个邮箱后再开发产品 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ [放弃] [生成设计文档 →] │
└─────────────────────────────────────────────────────────────────────┘
```
### 7.3 设计文档页面
```
┌─────────────────────────────────────────────────────────────────────┐
│ 设计文档 - AI 写作助手 [导出 ▾] [编辑] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 基本信息 │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ 项目名称AIWriter │ │
│ │ 项目 Slugai-writer │ │
│ │ 标语:让写作更简单,让创作更高效 │ │
│ │ 网站类型Waitlist (收集邮箱) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 页面结构 │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ 首页 (/) │ │
│ │ ├── Hero Section (主视觉区) │ │
│ │ ├── Features Section (功能特点) │ │
│ │ ├── How It Works (使用流程) │ │
│ │ ├── Waitlist Form (邮箱收集) │ │
│ │ └── Footer (页脚) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Hero Section │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ 主标题AI 驱动的智能写作助手 │ │
│ │ 副标题告别写作焦虑3分钟生成高质量文章 │ │
│ │ CTA 按钮:加入等待名单 │ │
│ │ 视觉元素:产品截图 / 动态演示 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Features │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ 1. 🚀 快速生成 - 3秒内生成完整文章 │ │
│ │ 2. 🎯 SEO优化 - 自动优化关键词和结构 │ │
│ │ 3. 🌍 多语言 - 支持10+种语言 │ │
│ │ 4. ✏️ 风格定制 - 适配你的写作风格 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 设计规范 │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ 主色:#6366F1 (Indigo) │ │
│ │ 辅色:#EC4899 (Pink) │ │
│ │ 背景:#FFFFFF / #F8FAFC │ │
│ │ 字体Inter (英文) / Noto Sans SC (中文) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ SEO 配置 │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ Title: AI 写作助手 - 智能内容生成工具 │ │
│ │ Description: AI 驱动的写作助手,帮助内容创作者... │ │
│ │ Keywords: AI写作, 内容生成, 文案工具, ... │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 技术建议 │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ 框架Next.js 14 (App Router) │ │
│ │ 托管Vercel │ │
│ │ 样式Tailwind CSS │ │
│ │ 分析Vercel Analytics + 自定义追踪 │ │
│ │ 邮箱收集Resend / ConvertKit │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### 7.4 导出面板
```
┌─────────────────────────────────────────────────────────────────────┐
│ 导出设计文档 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 格式选择: │
│ ● Markdown ○ JSON ○ PDF │
│ │
│ 包含内容: │
│ ☑ 基本信息 │
│ ☑ 页面结构 │
│ ☑ 内容文案 │
│ ☑ 设计规范 │
│ ☑ SEO 配置 │
│ ☑ 技术建议 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 预览 │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ # AI Writer - 设计文档 │ │
│ │ │ │
│ │ ## 基本信息 │ │
│ │ - 项目名称AIWriter │ │
│ │ - 标语:让写作更简单,让创作更高效 │ │
│ │ ... │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ [复制到剪贴板] [下载文件] [发送到邮箱] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### 7.5 验证数据页面
```
┌─────────────────────────────────────────────────────────────────────┐
│ 验证数据 - AI 写作助手 [同步数据] [设置] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 部署 URLhttps://ai-writer.vercel.app [打开] [编辑] │
│ 部署时间2024-03-10 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ 👁️ 总访问 │ │ 👥 独立访客 │ │ 📧 邮箱 │ │ 📈 转化率 │ │
│ │ 1,234 │ │ 892 │ │ 234 │ │ 26.2% │ │
│ │ ↑ 12% │ │ ↑ 8% │ │ ↑ 15% │ │ ↑ 2.1% │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 访问趋势 (最近7天) │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 200 ┤ │ │
│ │ 150 ┤ ╭──╮ │ │
│ │ 100 ┤ ╭──╯ ╰──╮ │ │
│ │ 50 ┤───╯ ╰───╮ │ │
│ │ 0 ┼──────────────────╰─── │ │
│ │ Mon Tue Wed Thu Fri Sat Sun │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 流量来源 │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ Direct ████████████████████░░░░░░░░░░░░░░░░░░░░ 45% │ │
│ │ Social ██████████████░░░░░░░░░░░░░░░░░░░░░░░░░░ 30% │ │
│ │ Organic ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 20% │ │
│ │ Referral ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 5% │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 📊 验证评估 │ │
│ │ ───────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 当前数据表明这个 idea 有一定潜力: │ │
│ │ ✅ 转化率 26% 高于行业平均 (15-20%) │ │
│ │ ✅ 流量稳步增长 │ │
│ │ ⚠️ 社交媒体流量占比较高,需关注长期留存 │ │
│ │ │ │
│ │ 建议: │ │
│ │ • 继续收集数据,目标 500+ 邮箱 │ │
│ │ • 开始开发 MVP 核心功能 │ │
│ │ • 尝试 SEO 获取更多自然流量 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 八、实施计划
### 第一阶段核心功能2周
**Week 1**
- [ ] Idea CRUD创建/读取/更新/删除)
- [ ] SQLite 数据存储
- [ ] 基础 UI 组件
**Week 2**
- [ ] AI 分析功能
- [ ] 分析结果展示
- [ ] 设计文档生成(基础版)
### 第二阶段完善功能2周
**Week 3**
- [ ] 设计文档编辑
- [ ] 导出功能Markdown/JSON
- [ ] 网站类型模板优化
**Week 4**
- [ ] Vercel 数据追踪集成
- [ ] 验证数据展示
- [ ] 数据同步功能
### 第三阶段:优化迭代(按需)
- [ ] 更丰富的分析维度
- [ ] 设计文档模板库
- [ ] 数据可视化增强
- [ ] AI 建议优化
---
## 九、配置需求
### AI 服务配置
```typescript
// 配置项
interface AIConfig {
provider: 'openai' | 'anthropic' | 'azure'
apiKey: string
model: string // 'gpt-4' | 'claude-3-opus' 等
maxTokens: number
temperature: number
}
```
### Vercel 配置
```typescript
// Vercel API 配置
interface VercelConfig {
apiToken: string // Vercel API Token
teamId?: string // 团队 ID可选
}
```
---
## 十、后续扩展方向
### 可选功能(按需添加)
1. **多语言支持** - 分析结果和设计文档多语言
2. **设计文档版本** - 保存多个版本,对比选择
3. **团队协作** - 分享 idea 和分析结果
4. **模板市场** - 用户分享设计文档模板
5. **自动化建议** - 基于数据自动给出优化建议
6. **竞品监控** - 定期更新竞品信息
---
*文档版本v2.0*
*最后更新2024-03-12*

View File

@@ -1,22 +0,0 @@
const WebSocket = require('ws');
console.log('Testing connection to remote...');
const ws = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525', {
handshakeTimeout: 5000
});
ws.on('open', () => {
console.log('Connected!');
ws.close();
process.exit(0);
});
ws.on('error', (err) => {
console.log('Error:', err.message);
process.exit(1);
});
setTimeout(() => {
console.log('Connection timeout');
process.exit(1);
}, 8000);

View File

@@ -1,268 +0,0 @@
const http = require('http');
const https = require('https');
const BASE_URL = 'http://localhost:3000';
const PASSWORD = 'wzw20040525';
const CHUNK_SIZE = 5 * 1024 * 1024;
function request(options, body = null) {
return new Promise((resolve, reject) => {
const url = new URL(options.path, BASE_URL);
url.searchParams.set('password', PASSWORD);
const client = url.protocol === 'https:' ? https : http;
const reqOptions = {
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method: options.method,
headers: options.headers || {}
};
const req = client.request(reqOptions, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve({ status: res.statusCode, data: JSON.parse(data) });
} catch {
resolve({ status: res.statusCode, data });
}
});
});
req.on('error', reject);
if (body) {
if (body instanceof FormData) {
req.write(body.getBuffer());
} else if (Buffer.isBuffer(body)) {
req.write(body);
} else {
req.write(JSON.stringify(body));
}
}
req.end();
});
}
async function testGetDrives() {
console.log('\n=== 测试1: 获取驱动器列表 ===');
try {
const driveUrl = new URL('/api/files/browse', BASE_URL);
driveUrl.searchParams.set('allowSystem', 'true');
driveUrl.searchParams.set('password', PASSWORD);
const res = await request({
method: 'GET',
path: driveUrl.pathname + driveUrl.search
});
console.log('状态:', res.status);
console.log('currentPath:', res.data.currentPath);
console.log('parentPath:', res.data.parentPath);
// 检查是否返回了驱动器(盘符如 C:, D:
const drives = res.data.items?.filter(item => item.name.match(/^[A-Z]:$/i));
if (drives && drives.length > 0) {
console.log('✓ 驱动器列表:', drives.map(d => d.name).join(', '));
return true;
}
// 如果返回的是目录列表而非驱动器,说明 allowSystem 未生效
console.log('✗ 未获取到驱动器列表');
console.log('返回的项目:', res.data.items?.slice(0, 5).map(i => i.name).join(', '));
return false;
} catch (err) {
console.error('错误:', err.message);
return false;
}
}
async function testUploadToSystemDir() {
console.log('\n=== 测试2: 上传到系统目录 ===');
const testContent = 'Hello Remote System Directory Test ' + Date.now();
const filename = 'D:\\xc_test_file.txt';
try {
// 1. 开始上传
console.log('1. 开始上传...');
const startUrl = new URL('/api/files/upload/start', BASE_URL);
startUrl.searchParams.set('password', PASSWORD);
const startRes = await new Promise((resolve, reject) => {
const req = http.request({
hostname: startUrl.hostname,
port: startUrl.port,
path: startUrl.pathname + startUrl.search,
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
catch { resolve({ status: res.statusCode, data }); }
});
});
req.on('error', reject);
req.write(JSON.stringify({
filename: filename,
totalChunks: 1,
fileSize: Buffer.byteLength(testContent)
}));
req.end();
});
console.log('开始上传响应:', startRes.data);
if (!startRes.data.fileId) {
console.error('未获取到fileId');
return false;
}
const fileId = startRes.data.fileId;
// 2. 上传分块
console.log('2. 上传分块...');
const chunk = Buffer.from(testContent);
const chunkUrl = new URL('/api/files/upload/chunk', BASE_URL);
chunkUrl.searchParams.set('password', PASSWORD);
const FormData = require('form-data');
const form = new FormData();
form.append('fileId', fileId);
form.append('chunkIndex', '0');
form.append('chunk', chunk, { filename: 'chunk', contentType: 'application/octet-stream' });
const chunkRes = await new Promise((resolve, reject) => {
const req = http.request({
hostname: chunkUrl.hostname,
port: chunkUrl.port,
path: chunkUrl.pathname + chunkUrl.search,
method: 'POST',
headers: form.getHeaders()
}, (res) => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => {
try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
catch { resolve({ status: res.statusCode, data }); }
});
});
req.on('error', reject);
req.write(form.getBuffer());
req.end();
});
console.log('分块上传响应:', chunkRes.data);
// 3. 合并文件
console.log('3. 合并文件...');
const mergeUrl = new URL('/api/files/upload/merge', BASE_URL);
mergeUrl.searchParams.set('password', PASSWORD);
const mergeRes = await new Promise((resolve, reject) => {
const req = http.request({
hostname: mergeUrl.hostname,
port: mergeUrl.port,
path: mergeUrl.pathname + mergeUrl.search,
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, (res) => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => {
try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
catch { resolve({ status: res.statusCode, data }); }
});
});
req.on('error', reject);
req.write(JSON.stringify({
fileId: fileId,
totalChunks: 1,
filename: filename
}));
req.end();
});
console.log('合并响应:', mergeRes.data);
return mergeRes.data.success === true;
} catch (err) {
console.error('错误:', err.message);
return false;
}
}
async function testDownloadFromSystemDir() {
console.log('\n=== 测试3: 从系统目录下载 ===');
const filename = 'D:\\xc_test_file.txt';
try {
const encodedPath = encodeURIComponent(filename);
const res = await request({
method: 'GET',
path: `/api/files/${encodedPath}`
});
console.log('状态:', res.status);
console.log('响应类型:', typeof res.data);
return res.status === 200;
} catch (err) {
console.error('错误:', err.message);
return false;
}
}
async function testDeleteFile() {
console.log('\n=== 测试4: 删除测试文件 ===');
const filename = 'D:\\xc_test_file.txt';
try {
const encodedPath = encodeURIComponent(filename);
const res = await request({
method: 'DELETE',
path: `/api/files/${encodedPath}`
});
console.log('状态:', res.status);
console.log('响应:', res.data);
return res.data.success === true || res.status === 200;
} catch (err) {
console.error('错误:', err.message);
return false;
}
}
async function main() {
console.log('========================================');
console.log('远程文件传输功能测试');
console.log('目标服务器: 146.56.248.142:8080');
console.log('========================================');
const results = [];
// 测试1: 获取驱动器列表
results.push({ name: '获取驱动器列表', pass: await testGetDrives() });
// 测试2: 上传到系统目录
results.push({ name: '上传到系统目录', pass: await testUploadToSystemDir() });
// 测试3: 从系统目录下载
results.push({ name: '从系统目录下载', pass: await testDownloadFromSystemDir() });
// 测试4: 删除测试文件
results.push({ name: '删除测试文件', pass: await testDeleteFile() });
// 汇总结果
console.log('\n========================================');
console.log('测试结果汇总:');
console.log('========================================');
results.forEach(r => {
console.log(`${r.pass ? '✓' : '✗'} ${r.name}`);
});
const allPass = results.every(r => r.pass);
console.log(`\n总体结果: ${allPass ? '全部通过' : '存在失败'}`);
process.exit(allPass ? 0 : 1);
}
main();

View File

@@ -1,89 +0,0 @@
const WebSocket = require('ws');
const fs = require('fs');
const ws = new WebSocket('ws://127.0.0.1:3003/ws?password=wzw20040525');
let fileId = 'upload_test_' + Date.now();
const testContent = Buffer.from('Hello World Test File Content');
ws.on('open', () => {
console.log('=== Connected, starting upload test ===');
ws.send(JSON.stringify({
type: 'fileUploadStart',
fileId: fileId,
filename: 'test.txt',
totalChunks: 1,
fileSize: testContent.length,
requestId: 'req1'
}));
});
ws.on('message', (data, isBinary) => {
if (isBinary) {
console.log('Binary data received');
return;
}
const msg = JSON.parse(data.toString());
console.log('Received:', msg.type, msg.fileId || '');
if (msg.type === 'fileUploadStart') {
console.log('Session started, sending chunk...');
ws.send(JSON.stringify({
type: 'fileUploadChunk',
fileId: fileId,
chunkIndex: 0,
data: testContent.toString('base64'),
requestId: 'req2'
}));
}
if (msg.type === 'fileUploadChunk') {
console.log('Chunk sent, sending merge...');
ws.send(JSON.stringify({
type: 'fileUploadMerge',
fileId: fileId,
filename: 'test.txt',
totalChunks: 1,
requestId: 'req3'
}));
}
if (msg.type === 'fileUploadResult') {
console.log('=== Upload Result:', msg.success ? 'SUCCESS' : 'FAILED', msg.filename || msg.error);
if (msg.success) {
console.log('\n=== Testing download ===');
ws.send(JSON.stringify({
type: 'fileDownloadStart',
filename: 'test.txt',
filePath: 'test.txt',
allowSystem: true,
requestId: 'req4'
}));
} else {
ws.close();
process.exit(1);
}
}
if (msg.type === 'fileDownloadStart') {
console.log('Download started, size:', msg.size);
}
if (msg.type === 'fileDownloadChunk') {
console.log('Download chunk:', msg.chunkIndex, 'progress:', msg.progress + '%');
}
if (msg.type === 'fileDownloadComplete') {
console.log('=== Download Result:', msg.success ? 'SUCCESS' : 'FAILED');
console.log('=== ALL TESTS PASSED ===');
ws.close();
process.exit(0);
}
});
ws.on('error', (err) => { console.error('Error:', err.message); });
setTimeout(() => { console.log('=== TIMEOUT ==='); process.exit(1); }, 20000);

View File

@@ -1,13 +0,0 @@
const WebSocket = require('ws');
console.log('Testing port 8080...');
const ws1 = new WebSocket('ws://146.56.248.142:8080/ws?password=wzw20040525');
ws1.on('open', () => console.log('8080: Connected'));
ws1.on('error', () => console.log('8080: Failed'));
ws1.on('close', () => {
console.log('Testing port 8083...');
const ws2 = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525');
ws2.on('open', () => console.log('8083: Connected'));
ws2.on('error', () => console.log('8083: Failed'));
setTimeout(() => process.exit(0), 3000);
});

View File

@@ -1,71 +0,0 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525');
let fileId = 'test_' + Date.now();
const testContent = Buffer.from('Hello Remote Test');
ws.on('open', () => {
console.log('=== Connected to 146.56.248.142:8083 ===');
ws.send(JSON.stringify({
type: 'fileUploadStart',
fileId: fileId,
filename: 'test.txt',
totalChunks: 1,
fileSize: testContent.length,
requestId: 'req1'
}));
});
ws.on('message', (data, isBinary) => {
if (isBinary) return;
const msg = JSON.parse(data.toString());
console.log('Received:', msg.type);
if (msg.type === 'fileUploadStart') {
ws.send(JSON.stringify({
type: 'fileUploadChunk',
fileId: fileId,
chunkIndex: 0,
data: testContent.toString('base64'),
requestId: 'req2'
}));
}
if (msg.type === 'fileUploadChunk') {
ws.send(JSON.stringify({
type: 'fileUploadMerge',
fileId: fileId,
filename: 'test.txt',
totalChunks: 1,
requestId: 'req3'
}));
}
if (msg.type === 'fileUploadResult') {
console.log('=== Upload:', msg.success ? 'SUCCESS' : 'FAILED');
if (msg.success) {
ws.send(JSON.stringify({
type: 'fileDownloadStart',
filename: 'test.txt',
filePath: 'test.txt',
allowSystem: false,
requestId: 'req4'
}));
} else {
ws.close();
process.exit(1);
}
}
if (msg.type === 'fileDownloadComplete') {
console.log('=== Download:', msg.success ? 'SUCCESS' : 'FAILED');
console.log('=== ALL TESTS PASSED ===');
ws.close();
process.exit(0);
}
});
ws.on('error', err => console.error('Error:', err.message));
setTimeout(() => { console.log('TIMEOUT'); process.exit(1); }, 15000);

View File

@@ -1,30 +0,0 @@
const WebSocket = require('ws');
console.log('Starting...');
const ws = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525');
ws.on('open', () => {
console.log('Connected!');
let fileId = 'test_' + Date.now();
const testContent = Buffer.from('Hello');
console.log('Sending fileUploadStart...');
ws.send(JSON.stringify({
type: 'fileUploadStart',
fileId: fileId,
filename: 'F:/test.txt',
totalChunks: 1,
fileSize: 5,
requestId: 'req1'
}));
});
ws.on('message', (data, isBinary) => {
console.log('Got message:', isBinary ? 'binary' : data.toString().substring(0,80));
});
ws.on('error', e => console.log('Error:', e.message));
ws.on('close', () => console.log('Closed'));
setTimeout(() => { console.log('Timeout'); process.exit(1); }, 10000);

View File

@@ -1,57 +0,0 @@
const WebSocket = require('ws');
console.log('Testing upload to remote 8083...');
const ws = new WebSocket('ws://146.56.248.142:8083/ws?password=wzw20040525');
let fileId = 'test_' + Date.now();
const testContent = Buffer.from('Hello Test 中文');
ws.on('open', () => {
console.log('Connected, sending fileUploadStart with F:/xxx.txt...');
ws.send(JSON.stringify({
type: 'fileUploadStart',
fileId: fileId,
filename: 'F:/小问题.txt',
totalChunks: 1,
fileSize: testContent.length,
requestId: 'req1'
}));
});
ws.on('message', (data, isBinary) => {
if (isBinary) return;
const msg = JSON.parse(data.toString());
console.log('Received:', msg.type);
if (msg.type === 'fileUploadStart') {
console.log('Session started, sending chunk...');
ws.send(JSON.stringify({
type: 'fileUploadChunk',
fileId: fileId,
chunkIndex: 0,
data: testContent.toString('base64'),
requestId: 'req2'
}));
}
if (msg.type === 'fileUploadChunk') {
console.log('Chunk sent, sending merge...');
ws.send(JSON.stringify({
type: 'fileUploadMerge',
fileId: fileId,
filename: 'F:/小问题.txt',
totalChunks: 1,
requestId: 'req3'
}));
}
if (msg.type === 'fileUploadResult') {
console.log('=== Upload Result:', msg.success ? 'SUCCESS' : 'FAILED', msg.error || '');
ws.close();
process.exit(0);
}
});
ws.on('error', err => console.error('Error:', err.message));
setTimeout(() => { console.log('Timeout'); process.exit(1); }, 15000);

View File

@@ -1,32 +0,0 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://127.0.0.1:3001/ws?password=wzw20040525');
ws.on('open', () => {
console.log('Connected, sending fileBrowse...');
ws.send(JSON.stringify({ type: 'fileBrowse', path: 'C:\\', allowSystem: true, requestId: 'test1' }));
});
ws.on('message', (data) => {
console.log('Received raw:', data.toString().substring(0, 200));
if (Buffer.isBuffer(data) || data instanceof ArrayBuffer) {
console.log('Binary data');
return;
}
try {
const msg = JSON.parse(data);
console.log('Received:', msg.type);
if (msg.type === 'fileBrowseResult') {
console.log('Items:', msg.items?.slice(0,3));
ws.close();
process.exit(0);
}
} catch(e) {
console.log('Parse error:', e.message);
}
});
ws.on('error', (err) => { console.error('Error:', err.message); });
setTimeout(() => { console.log('Timeout'); process.exit(1); }, 10000);

View File

@@ -0,0 +1,99 @@
# 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.

View File

@@ -0,0 +1,99 @@
# 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.