Files
ONE-OS/axhub-make/vite-plugins/virtualHtml/handlers/textReplaceHandler.ts
王冕 a27e3b8e43 feat: sync full workspace including web modules, docs, and configurations to Gitea
Optimized the root .gitignore to exclude virtual environments, node modules,
and temp folders to ensure clean and lightweight version tracking.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 18:12:25 +08:00

163 lines
5.4 KiB
TypeScript

import type { IncomingMessage, ServerResponse } from 'http';
import fs from 'fs';
import path from 'path';
const TARGET_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx'];
async function getAllFilePaths(dirPath: string): Promise<string[]> {
let files: string[] = [];
try {
const items = await fs.promises.readdir(dirPath, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dirPath, item.name);
if (item.isDirectory()) {
files = files.concat(await getAllFilePaths(fullPath));
} else if (item.isFile()) {
const ext = path.extname(item.name).toLowerCase();
if (TARGET_EXTENSIONS.includes(ext)) {
files.push(fullPath);
}
}
}
} catch (err) {
console.error(`读取目录失败: ${dirPath}`, err);
}
return files;
}
type TextReplacement = { searchText: string; replaceText: string };
async function replaceMatches(dirPath: string, searchText: string, replaceText: string): Promise<number> {
let changedFilesCount = 0;
const files = await getAllFilePaths(dirPath);
for (const file of files) {
try {
const content = await fs.promises.readFile(file, 'utf-8');
if (content.includes(searchText)) {
const newContent = content.replaceAll(searchText, replaceText);
await fs.promises.writeFile(file, newContent, 'utf-8');
console.log(`[API] ✅ 已修改: ${file}`);
changedFilesCount++;
}
} catch (err) {
console.error(`处理文件失败: ${file}`, err);
}
}
return changedFilesCount;
}
async function replaceMatchesBatch(dirPath: string, replacements: TextReplacement[]): Promise<number> {
let changedFilesCount = 0;
const files = await getAllFilePaths(dirPath);
for (const file of files) {
try {
const content = await fs.promises.readFile(file, 'utf-8');
let newContent = content;
let changed = false;
for (const { searchText, replaceText } of replacements) {
if (!searchText) continue;
if (newContent.includes(searchText)) {
newContent = newContent.replaceAll(searchText, replaceText);
changed = true;
}
}
if (changed && newContent !== content) {
await fs.promises.writeFile(file, newContent, 'utf-8');
console.log(`[API] ✅ 已修改: ${file}`);
changedFilesCount++;
}
} catch (err) {
console.error(`处理文件失败: ${file}`, err);
}
}
return changedFilesCount;
}
export function handleTextReplace(req: IncomingMessage, res: ServerResponse): boolean {
if (req.method === 'POST' && req.url?.startsWith('/api/text-replace/replace')) {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', async () => {
try {
const { path: targetPath, searchText, replaceText, replacements } = JSON.parse(body);
if (!targetPath || (!searchText && !Array.isArray(replacements))) {
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'path and replacement data are required' }));
return;
}
const pathParts = targetPath.split('/').filter(Boolean);
if (pathParts.length < 2 || !['components', 'prototypes'].includes(pathParts[0])) {
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Invalid path format. Expected: components/xxx or prototypes/xxx' }));
return;
}
const fullPath = path.resolve(process.cwd(), 'src', targetPath);
if (!fs.existsSync(fullPath)) {
res.statusCode = 404;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Path not found' }));
return;
}
let changedFiles = 0;
if (Array.isArray(replacements)) {
const cleaned = replacements
.filter((item: any) => item && typeof item.searchText === 'string' && item.replaceText !== undefined)
.map((item: any) => ({
searchText: item.searchText,
replaceText: String(item.replaceText ?? '')
}));
if (cleaned.length === 0) {
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'replacements is empty' }));
return;
}
changedFiles = await replaceMatchesBatch(fullPath, cleaned);
} else {
if (!searchText || replaceText === undefined) {
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'searchText and replaceText are required' }));
return;
}
changedFiles = await replaceMatches(fullPath, searchText, replaceText);
}
console.log(`[API] 替换完成: 共修改了 ${changedFiles} 个文件`);
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true, changedFiles }));
} catch (err) {
console.error('[API] ❌ 替换文本失败:', err);
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Failed to replace text' }));
}
});
return true;
}
return false;
}