From ad58bed418dc01461fef86c4327b956a1e8d253a Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Thu, 19 Mar 2026 16:21:05 +0800 Subject: [PATCH] feat: add HTTP API endpoints for headless mode --- electron/main.mjs | 83 +++++++++++++++++++++- src/components/ApiDocViewer.tsx | 41 ++++++++--- src/components/blueprint/BlueprintPage.tsx | 18 ++++- 3 files changed, 125 insertions(+), 17 deletions(-) diff --git a/electron/main.mjs b/electron/main.mjs index 236bfb3..a74943c 100644 --- a/electron/main.mjs +++ b/electron/main.mjs @@ -59,10 +59,48 @@ function getIndexPath() { } function startHttpServer(port) { - const distPath = app.isPackaged + const distPath = app.isPackaged ? 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,8 +119,47 @@ 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); const contentType = mimeTypes[ext] || 'application/octet-stream'; diff --git a/src/components/ApiDocViewer.tsx b/src/components/ApiDocViewer.tsx index b6b2cab..efc1c5f 100644 --- a/src/components/ApiDocViewer.tsx +++ b/src/components/ApiDocViewer.tsx @@ -88,18 +88,25 @@ export const ApiDocViewer = ({ onDocsPathChange, showAddModal, onCloseAddModal } setErrorMsg('请输入文档路径') 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 文件`) setExternalDocs([]) @@ -108,10 +115,22 @@ export const ApiDocViewer = ({ onDocsPathChange, showAddModal, onCloseAddModal } setCurrentContent('') return false } - + 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 }) } diff --git a/src/components/blueprint/BlueprintPage.tsx b/src/components/blueprint/BlueprintPage.tsx index 2e20cc0..b65b3d9 100644 --- a/src/components/blueprint/BlueprintPage.tsx +++ b/src/components/blueprint/BlueprintPage.tsx @@ -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);