156 lines
5.9 KiB
TypeScript
156 lines
5.9 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import simpleGit from 'simple-git';
|
|
|
|
const DOCUMENT_EXTENSIONS = new Set(['.md', '.markdown', '.txt', '.csv', '.json', '.yaml', '.yml']);
|
|
const ASSET_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg']);
|
|
const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', '.idea', '.vscode', '__macosx']);
|
|
const MAX_FILES = 1000;
|
|
const MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
|
|
export interface FolderImportResult {
|
|
cancelled: boolean;
|
|
sourceName?: string;
|
|
destination?: string;
|
|
imported: number;
|
|
assets: number;
|
|
skipped: number;
|
|
failed: string[];
|
|
firstDocument?: string;
|
|
}
|
|
|
|
function safeSegment(value: string): string {
|
|
const cleaned = value.normalize('NFC').replace(/[\\/:*?"<>|\u0000-\u001f]/g, '-').trim();
|
|
return cleaned || '未命名文件夹';
|
|
}
|
|
|
|
function markdownFrontmatter(title: string, source: string): string {
|
|
const now = new Date().toISOString();
|
|
return `---\ntitle: ${JSON.stringify(title)}\nauthor: HoloLake Importer\ncreatedAt: ${now}\nupdatedAt: ${now}\nsource: ${JSON.stringify(source)}\ntags:\n - imported\n---\n\n`;
|
|
}
|
|
|
|
function csvToMarkdown(input: string): string {
|
|
const rows = input
|
|
.split(/\r?\n/)
|
|
.filter(Boolean)
|
|
.map(line => line.split(',').map(cell => cell.trim().replace(/\|/g, '\\|')));
|
|
if (!rows.length) return '_空表格_\n';
|
|
const width = Math.max(...rows.map(row => row.length));
|
|
const padded = rows.map(row => [...row, ...Array(Math.max(0, width - row.length)).fill('')]);
|
|
return [
|
|
`| ${padded[0].join(' | ')} |`,
|
|
`| ${Array(width).fill('---').join(' | ')} |`,
|
|
...padded.slice(1).map(row => `| ${row.join(' | ')} |`),
|
|
].join('\n') + '\n';
|
|
}
|
|
|
|
function convertDocument(sourcePath: string, relativePath: string): { targetRelative: string; content: string } {
|
|
const extension = path.extname(sourcePath).toLowerCase();
|
|
const raw = fs.readFileSync(sourcePath, 'utf8');
|
|
const title = path.basename(sourcePath, extension);
|
|
const targetRelative = extension === '.md'
|
|
? relativePath
|
|
: relativePath.slice(0, -extension.length) + '.md';
|
|
|
|
if (extension === '.md' || extension === '.markdown') {
|
|
return {
|
|
targetRelative: targetRelative.replace(/\.markdown$/i, '.md'),
|
|
content: raw,
|
|
};
|
|
}
|
|
if (extension === '.txt') {
|
|
return { targetRelative, content: `${markdownFrontmatter(title, relativePath)}# ${title}\n\n${raw}\n` };
|
|
}
|
|
if (extension === '.csv') {
|
|
return { targetRelative, content: `${markdownFrontmatter(title, relativePath)}# ${title}\n\n${csvToMarkdown(raw)}` };
|
|
}
|
|
const language = extension === '.json' ? 'json' : 'yaml';
|
|
return {
|
|
targetRelative,
|
|
content: `${markdownFrontmatter(title, relativePath)}# ${title}\n\n\`\`\`${language}\n${raw}\n\`\`\`\n`,
|
|
};
|
|
}
|
|
|
|
function walk(root: string, current: string, files: string[]): void {
|
|
if (files.length >= MAX_FILES) return;
|
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
if (entry.name.startsWith('._') || entry.name === '.DS_Store') continue;
|
|
const fullPath = path.join(current, entry.name);
|
|
if (entry.isSymbolicLink()) continue;
|
|
if (entry.isDirectory()) {
|
|
if (!entry.name.startsWith('.') && !IGNORED_DIRECTORIES.has(entry.name.toLowerCase())) {
|
|
walk(root, fullPath, files);
|
|
}
|
|
continue;
|
|
}
|
|
files.push(fullPath);
|
|
if (files.length >= MAX_FILES) return;
|
|
}
|
|
}
|
|
|
|
export async function importKnowledgeFolder(sourcePath: string, repoPath: string): Promise<FolderImportResult> {
|
|
const sourceRoot = fs.realpathSync(sourcePath);
|
|
const sourceName = safeSegment(path.basename(sourceRoot));
|
|
const docsRoot = path.join(repoPath, 'docs');
|
|
const importRoot = path.join(docsRoot, '导入');
|
|
let destinationName = sourceName;
|
|
let destinationRoot = path.join(importRoot, destinationName);
|
|
if (fs.existsSync(destinationRoot)) {
|
|
const stamp = new Date().toISOString().replace(/[-:]/g, '').slice(0, 13);
|
|
destinationName = `${sourceName}-${stamp}`;
|
|
destinationRoot = path.join(importRoot, destinationName);
|
|
}
|
|
fs.mkdirSync(destinationRoot, { recursive: true });
|
|
|
|
const candidates: string[] = [];
|
|
walk(sourceRoot, sourceRoot, candidates);
|
|
const result: FolderImportResult = {
|
|
cancelled: false,
|
|
sourceName,
|
|
destination: `导入/${destinationName}`,
|
|
imported: 0,
|
|
assets: 0,
|
|
skipped: 0,
|
|
failed: [],
|
|
};
|
|
|
|
for (const sourceFile of candidates) {
|
|
const relative = path.relative(sourceRoot, sourceFile);
|
|
const safeRelative = relative.split(path.sep).map(safeSegment).join(path.sep);
|
|
const extension = path.extname(sourceFile).toLowerCase();
|
|
try {
|
|
if (fs.statSync(sourceFile).size > MAX_FILE_BYTES) {
|
|
result.skipped += 1;
|
|
continue;
|
|
}
|
|
if (DOCUMENT_EXTENSIONS.has(extension)) {
|
|
const converted = convertDocument(sourceFile, safeRelative);
|
|
const target = path.join(destinationRoot, converted.targetRelative);
|
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
fs.writeFileSync(target, converted.content, 'utf8');
|
|
const docPath = path.posix.join('导入', destinationName, converted.targetRelative.split(path.sep).join('/'));
|
|
result.firstDocument ||= docPath;
|
|
result.imported += 1;
|
|
} else if (ASSET_EXTENSIONS.has(extension)) {
|
|
const target = path.join(destinationRoot, safeRelative);
|
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
fs.copyFileSync(sourceFile, target);
|
|
result.assets += 1;
|
|
} else {
|
|
result.skipped += 1;
|
|
}
|
|
} catch (error) {
|
|
result.failed.push(`${relative}: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
if (result.imported === 0 && result.assets === 0) {
|
|
fs.rmSync(destinationRoot, { recursive: true, force: true });
|
|
return result;
|
|
}
|
|
|
|
const git = simpleGit(repoPath);
|
|
await git.add(path.relative(repoPath, destinationRoot));
|
|
await git.commit(`import: 导入本地文件夹 ${sourceName}`);
|
|
return result;
|
|
}
|