feat: add HTTP API endpoints for headless mode
This commit is contained in:
@@ -63,6 +63,44 @@ function startHttpServer(port) {
|
|||||||
? path.join(__dirname, '..', 'dist')
|
? path.join(__dirname, '..', 'dist')
|
||||||
: 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 = {
|
const mimeTypes = {
|
||||||
'.html': 'text/html',
|
'.html': 'text/html',
|
||||||
'.js': 'application/javascript',
|
'.js': 'application/javascript',
|
||||||
@@ -81,6 +119,45 @@ function startHttpServer(port) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const server = http.createServer((req, res) => {
|
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);
|
let filePath = path.join(distPath, req.url === '/' ? 'index.html' : req.url);
|
||||||
|
|
||||||
const ext = path.extname(filePath);
|
const ext = path.extname(filePath);
|
||||||
|
|||||||
@@ -89,16 +89,23 @@ export const ApiDocViewer = ({ onDocsPathChange, showAddModal, onCloseAddModal }
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!window.electronAPI) {
|
|
||||||
setErrorMsg('Electron API 不可用,请使用打包后的应用')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setErrorMsg(null)
|
setErrorMsg(null)
|
||||||
|
|
||||||
try {
|
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) {
|
if (files.length === 0) {
|
||||||
setErrorMsg(`路径 "${basePath}/api" 下没有找到 .md 文件`)
|
setErrorMsg(`路径 "${basePath}/api" 下没有找到 .md 文件`)
|
||||||
@@ -111,7 +118,19 @@ export const ApiDocViewer = ({ onDocsPathChange, showAddModal, onCloseAddModal }
|
|||||||
|
|
||||||
const docs: ExternalDoc[] = []
|
const docs: ExternalDoc[] = []
|
||||||
for (const file of files) {
|
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) {
|
if (content) {
|
||||||
docs.push({ name: file.name, path: file.path, relativePath: file.relativePath.replace(/^api\//, ''), content })
|
docs.push({ name: file.name, path: file.path, relativePath: file.relativePath.replace(/^api\//, ''), content })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,22 @@ export default function BlueprintPage({ docsPath }: BlueprintPageProps) {
|
|||||||
const setBlueprintData = useBlueprintStore(state => state.setBlueprintData);
|
const setBlueprintData = useBlueprintStore(state => state.setBlueprintData);
|
||||||
|
|
||||||
const loadBlueprint = useCallback(async () => {
|
const loadBlueprint = useCallback(async () => {
|
||||||
if (!docsPath || !window.electronAPI) return;
|
if (!docsPath) return;
|
||||||
|
|
||||||
const blueprintPath = docsPath.replace(/\\/g, '/') + '/blueprint.md';
|
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) {
|
if (content) {
|
||||||
const data = parseBlueprintFromMd(content);
|
const data = parseBlueprintFromMd(content);
|
||||||
setBlueprintData(data);
|
setBlueprintData(data);
|
||||||
|
|||||||
Reference in New Issue
Block a user