Files
ONE-OS/axhub-make/vite-plugins/virtualHtml/handlers/textReplaceCountHandler.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

159 lines
5.2 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 };
async function countMatches(dirPath: string, searchText: string): Promise<number> {
let totalCount = 0;
const files = await getAllFilePaths(dirPath);
for (const file of files) {
try {
const content = await fs.promises.readFile(file, 'utf-8');
const count = content.split(searchText).length - 1;
if (count > 0) {
totalCount += count;
}
} catch (err) {
console.error(`无法读取文件: ${file}`, err);
}
}
return totalCount;
}
async function countMatchesBatch(dirPath: string, replacements: TextReplacement[]): Promise<Record<string, number>> {
const counts: Record<string, number> = {};
const files = await getAllFilePaths(dirPath);
const searchTexts = replacements
.map(item => item.searchText)
.filter(text => typeof text === 'string' && text.length > 0);
searchTexts.forEach(text => {
counts[text] = 0;
});
if (searchTexts.length === 0) return counts;
for (const file of files) {
try {
const content = await fs.promises.readFile(file, 'utf-8');
for (const searchText of searchTexts) {
const count = content.split(searchText).length - 1;
if (count > 0) {
counts[searchText] += count;
}
}
} catch (err) {
console.error(`无法读取文件: ${file}`, err);
}
}
return counts;
}
export function handleTextReplaceCount(req: IncomingMessage, res: ServerResponse): boolean {
if (req.method === 'POST' && req.url?.startsWith('/api/text-replace/count')) {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', async () => {
try {
const { path: targetPath, searchText, searchTexts, replacements } = JSON.parse(body);
if (!targetPath || (!searchText && !Array.isArray(searchTexts) && !Array.isArray(replacements))) {
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'path and search text 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;
}
if (Array.isArray(replacements) || Array.isArray(searchTexts)) {
const list = Array.isArray(replacements)
? replacements
.filter((item: any) => item && typeof item.searchText === 'string')
.map((item: any) => ({ searchText: String(item.searchText) }))
: (searchTexts || [])
.filter((item: any) => typeof item === 'string')
.map((item: any) => ({ searchText: String(item) }));
if (list.length === 0) {
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'searchTexts is empty' }));
return;
}
const countsMap = await countMatchesBatch(fullPath, list);
const totalCount = Object.values(countsMap).reduce((sum, value) => sum + value, 0);
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true, counts: countsMap, totalCount }));
return;
}
const count = await countMatches(fullPath, searchText);
console.log(`[API] 统计文本 "${searchText}" 出现次数: ${count}`);
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true, count, totalCount: count }));
} catch (err) {
console.error('[API] ❌ 统计文本失败:', err);
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Failed to count text matches' }));
}
});
return true;
}
return false;
}