feat: add HTTP API endpoints for headless mode

This commit is contained in:
2026-03-19 16:21:05 +08:00
parent 207b56bf69
commit ad58bed418
3 changed files with 125 additions and 17 deletions

View File

@@ -63,6 +63,44 @@ function startHttpServer(port) {
? path.join(__dirname, '..', 'dist')
: path.join(__dirname, '..', 'dist');
// Reusable function for listing docs files
function listDocsFilesHandler(basePath) {
const docsPath = path.join(basePath, 'api');
if (!fs.existsSync(docsPath)) {
return [];
}
const result = [];
function walkDir(dir, baseDir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkDir(fullPath, baseDir);
} else if (entry.name.endsWith('.md')) {
const relativePath = path.relative(baseDir, fullPath);
result.push({
name: entry.name.replace('.md', ''),
path: fullPath,
relativePath: relativePath.replace(/\\/g, '/')
});
}
}
}
walkDir(docsPath, docsPath);
return result;
}
// Reusable function for reading doc file
function readDocFileHandler(filePath) {
if (!fs.existsSync(filePath)) {
return null;
}
return fs.readFileSync(filePath, 'utf-8');
}
const mimeTypes = {
'.html': 'text/html',
'.js': 'application/javascript',
@@ -81,6 +119,45 @@ function startHttpServer(port) {
};
const server = http.createServer((req, res) => {
// Handle API endpoints
const urlObj = new URL(req.url, `http://localhost:${port}`);
if (urlObj.pathname === '/api/list-docs-files') {
const basePath = urlObj.searchParams.get('basePath');
if (!basePath) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'basePath is required' }));
return;
}
try {
const files = listDocsFilesHandler(basePath);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(files));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
}
return;
}
if (urlObj.pathname === '/api/read-doc-file') {
const filePathParam = urlObj.searchParams.get('path');
if (!filePathParam) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'path is required' }));
return;
}
try {
const content = readDocFileHandler(filePathParam);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ content }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
}
return;
}
// Serve static files
let filePath = path.join(distPath, req.url === '/' ? 'index.html' : req.url);
const ext = path.extname(filePath);

View File

@@ -89,16 +89,23 @@ export const ApiDocViewer = ({ onDocsPathChange, showAddModal, onCloseAddModal }
return false
}
if (!window.electronAPI) {
setErrorMsg('Electron API 不可用,请使用打包后的应用')
return false
}
setIsLoading(true)
setErrorMsg(null)
try {
const files = await window.electronAPI.listDocsFiles(basePath)
let files: { name: string; path: string; relativePath: string }[] = []
if (window.electronAPI) {
files = await window.electronAPI.listDocsFiles(basePath)
} else {
// Use HTTP API in headless mode
const response = await fetch(`/api/list-docs-files?basePath=${encodeURIComponent(basePath)}`)
if (!response.ok) {
const err = await response.json()
throw new Error(err.error || 'Failed to list docs files')
}
files = await response.json()
}
if (files.length === 0) {
setErrorMsg(`路径 "${basePath}/api" 下没有找到 .md 文件`)
@@ -111,7 +118,19 @@ export const ApiDocViewer = ({ onDocsPathChange, showAddModal, onCloseAddModal }
const docs: ExternalDoc[] = []
for (const file of files) {
const content = await window.electronAPI.readDocFile(file.path)
let content: string | null = null
if (window.electronAPI) {
content = await window.electronAPI.readDocFile(file.path)
} else {
// Use HTTP API in headless mode
const response = await fetch(`/api/read-doc-file?path=${encodeURIComponent(file.path)}`)
if (response.ok) {
const data = await response.json()
content = data.content
}
}
if (content) {
docs.push({ name: file.name, path: file.path, relativePath: file.relativePath.replace(/^api\//, ''), content })
}

View File

@@ -12,10 +12,22 @@ export default function BlueprintPage({ docsPath }: BlueprintPageProps) {
const setBlueprintData = useBlueprintStore(state => state.setBlueprintData);
const loadBlueprint = useCallback(async () => {
if (!docsPath || !window.electronAPI) return;
if (!docsPath) return;
const blueprintPath = docsPath.replace(/\\/g, '/') + '/blueprint.md';
const content = await window.electronAPI.readDocFile(blueprintPath);
let content: string | null = null;
if (window.electronAPI) {
content = await window.electronAPI.readDocFile(blueprintPath);
} else {
// Use HTTP API in headless mode
const response = await fetch(`/api/read-doc-file?path=${encodeURIComponent(blueprintPath)}`);
if (response.ok) {
const data = await response.json();
content = data.content;
}
}
if (content) {
const data = parseBlueprintFromMd(content);
setBlueprintData(data);