Files
jinghui b5acbed047 init: TriliumNext × WechatSync 多平台发布桥接
- bridge/ 常驻桥接服务器(WebSocket 9600 + HTTP 9601,平台状态缓存)
- patch/ TriliumNext 补丁(publisher 服务、API 路由、发布控制台前端)
- install.sh 一键安装 + Docker 容器 CLI 处理
- 全链路使用 host.docker.internal 解决容器访问宿主机
- mcp/ AI Agent 集成说明
2026-10-17 21:59:00 +08:00

95 lines
5.0 KiB
TypeScript

// 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<any>) => void,
asyncRoute: (m: string, p: string, mw: any[], h: (req: Request, res: Response) => Promise<any>, 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);
}