- bridge/ 常驻桥接服务器(WebSocket 9600 + HTTP 9601,平台状态缓存) - patch/ TriliumNext 补丁(publisher 服务、API 路由、发布控制台前端) - install.sh 一键安装 + Docker 容器 CLI 处理 - 全链路使用 host.docker.internal 解决容器访问宿主机 - mcp/ AI Agent 集成说明
95 lines
5.0 KiB
TypeScript
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);
|
|
}
|