// TriliumNext WechatSync Publisher — API routes // 补丁目标: apps/server/src/routes/api/publisher_frontend.ts // // 提供: // GET /api/publisher/registered 已注册发布器 // GET /api/publisher/platforms 已配置平台列表 // GET /api/publisher/status 桥接/扩展连接状态 // GET /api/publisher/platform-status 各平台登录状态(含缓存) // POST /api/publisher/platforms 保存平台配置 // GET /api/publisher/notes-with-children/:id 列出目录下笔记 // POST /api/publisher/publish 发布文章到平台 import { becca, options as optionService } from "@triliumnext/core"; import type { Request, Response } from "express"; import auth from "../../services/auth.js"; import { apiResultHandler } from "../route_api.js"; import { getRegisteredPublishers, parsePlatformConfigs, extractArticleContent, publishToPlatforms } from "../../services/publisher/core.js"; import { WechatSyncPublisher } from "../../services/publisher/wechatsync.js"; import type { PlatformConfig } from "../../services/publisher/types.js"; const OPT = "publisherConfigs"; const BRIDGE_HOST = process.env.BRIDGE_HOST || "host.docker.internal"; const BRIDGE_HTTP_PORT = parseInt(process.env.SYNC_WS_PORT || "9600", 10) + 1; const BRIDGE_URL = `http://${BRIDGE_HOST}:${BRIDGE_HTTP_PORT}`; export function registerPublisherApi( apiRoute: (m: string, p: string, h: (req: Request, res: Response) => any) => void, asyncApiRoute: (m: string, p: string, h: (req: Request, res: Response) => Promise) => void, asyncRoute: (m: string, p: string, mw: any[], h: (req: Request, res: Response) => Promise, rh: any) => void ) { apiRoute("get", "/api/publisher/registered", () => ({ publishers: getRegisteredPublishers() })); apiRoute("get", "/api/publisher/platforms", () => ({ platforms: parsePlatformConfigs(optionService.getOptionOrNull(OPT) || undefined) })); asyncApiRoute("get", "/api/publisher/status", async () => ({ bridge: await WechatSyncPublisher.checkBridge(), wechatsync: { installed: true } })); // 平台登录状态(从桥接扩展获取,带缓存支持) asyncApiRoute("get", "/api/publisher/platform-status", async () => { try { const resp = await fetch(`${BRIDGE_URL}/platforms`); const data = await resp.json() as any; return { platforms: data.platforms || [], error: data.error || null, cached: data.cached === true, cachedAt: data.cachedAt || null }; } catch (e: any) { return { platforms: [], error: e?.message || "Bridge not reachable", cached: false }; } }); asyncRoute("post", "/api/publisher/platforms", [auth.checkApiAuth], async (req) => { const { platforms } = req.body as { platforms: PlatformConfig[] }; if (!Array.isArray(platforms)) return [400, { error: "platforms must be an array" }] as any; optionService.setOption(OPT, JSON.stringify(platforms)); return { success: true, count: platforms.length }; }, apiResultHandler); apiRoute("get", "/api/publisher/notes-with-children/:noteId", (req) => { const { noteId } = req.params; const note = becca.getNote(noteId); if (!note) return [404, { error: "Note not found" }] as any; const results: { noteId: string; title: string }[] = []; function walk(n: any) { results.push({ noteId: n.noteId, title: n.title }); for (const child of n.getChildNotes()) walk(child); } walk(note); return { notes: results }; }); asyncRoute("post", "/api/publisher/publish", [auth.checkApiAuth], async (req) => { const { noteId, platforms: targetPlatforms } = req.body as { noteId: string; platforms?: string[] }; if (!noteId) return [400, { error: "noteId is required" }] as any; const note = becca.getNote(noteId); if (!note) return [404, { error: "Note not found" }] as any; const html = note.getContent() || ""; const article = extractArticleContent(noteId, note.title, html.toString()); const configs = parsePlatformConfigs(optionService.getOptionOrNull(OPT) || undefined); const matched = (targetPlatforms && targetPlatforms.length > 0) ? configs.filter(c => targetPlatforms.includes(c.platform) || targetPlatforms.includes(c.id)) : configs; if (matched.length === 0) return [400, { error: "没有匹配的发布平台" }] as any; const results = await publishToPlatforms(article, matched); const mapped = results.map((r, i) => ({ ...r, id: matched[i]?.id || r.platform })); return { success: results.some(r => r.status === "success"), total: results.length, succeeded: results.filter(r => r.status === "success").length, failed: results.filter(r => r.status === "failed").length, results: mapped }; }, apiResultHandler); }