init: TriliumNext × WechatSync 多平台发布桥接
- bridge/ 常驻桥接服务器(WebSocket 9600 + HTTP 9601,平台状态缓存) - patch/ TriliumNext 补丁(publisher 服务、API 路由、发布控制台前端) - install.sh 一键安装 + Docker 容器 CLI 处理 - 全链路使用 host.docker.internal 解决容器访问宿主机 - mcp/ AI Agent 集成说明
This commit is contained in:
commit
b5acbed047
11 files changed
+1266
No files matched your search
@@ -0,0 +1,94 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in new issue
Block a user