Hard cutover from the browser-extension plugin flow to desktop clients: remove plugin_installations/plugin_sessions tables and related service, handler, router, and generated model code; migrate monitoring quotas and collector types to desktop_clients (UUID primary_client_id); recreate platform_access_snapshots keyed by client_id; update dev-seed and callback types accordingly; mark legacy design docs as historical. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
15 KiB
V1 技术方案:绑定媒体账号与手动发送文章(旧版设计)
旧版设计说明:本文描述的是基于浏览器插件回调、
plugin_sessions、/api/callback/plugin/*的旧方案。自 2026-04-20 起,当前实现已切换到 desktop client 架构。本文保留用于工作记录与方案追溯,不再作为当前实现依据。
口径说明:本文的插件执行流程、接口路径与会话模型已同步到主架构文档 geo-platform-ops-admin-tech-architecture-v1.md 的
§13.8 浏览器插件执行面闭环、§13.6.3 插件回调与内部接口、§13.6.4 统一错误响应与错误码。若两份文档存在任何冲突,以主架构文档为准。
1. 文档目标
本文档用于落地一套与文擎 GEO 当前形态接近的系统能力,聚焦两个核心功能:
- 绑定媒体账号
- 手动发送文章到媒体平台
当前版本边界:
- 支持平台账号绑定
- 支持后台文章列表中手动点击发送
- 支持插件执行真实发文
- 不支持自动定时发送到媒体平台
- 可支持后台定时生成文章,但生成与发送解耦
2. 产品边界
2.1 当前 V1 包含
- Web 管理后台
- 媒体账号绑定页
- 文章列表与文章详情页
- 发送按钮与发送结果展示
- 浏览器插件作为本地执行器
- 平台适配层
- 发送记录与状态记录
2.2 当前 V1 不包含
- 自动定时发送
- 云端代发
- 平台 cookie 或登录票据上传到服务端
- 复杂运营数据回流
- 多终端执行器调度
3. 总体架构
flowchart LR
A["Web 前端"] --> B["业务后端 API"]
A <---> C["浏览器插件"]
C --> D["平台适配层"]
D --> E["头条 / 百家 / 知乎 / 网易等平台"]
B --> F["PostgreSQL"]
B --> G["对象存储"]
C --> H["/api/callback/plugin/*"]
H --> B
4. 角色与职责
4.1 Web 前端
职责:
- 展示媒体账号绑定状态
- 展示文章列表和发送入口
- 调用后端读取文章详情、创建
plugin_session、展示即时执行结果 - 调用插件执行绑定与发文动作
前端不负责:
- 直接登录媒体平台
- 直接调用媒体平台发布接口
4.2 业务后端
职责:
- 租户、文章、平台账号绑定关系管理
plugin_session创建、校验与过期控制- 发送记录管理
- 文章详情与资源存储
- 结果审计与失败追踪
后端不负责:
- 代替用户持有平台登录态
- 直接模拟媒体平台发文
4.3 浏览器插件
职责:
- 打开平台官网登录页
- 检测平台登录状态
- 获取当前绑定账号信息
- 上传图片
- 调用平台接口或执行页面内发布逻辑
- 将真实执行结果双通道返回:即时回传 Web + 回调后端可靠落库
4.4 平台适配层
职责:
- 每个平台单独封装接入逻辑
- 对上暴露统一接口
- 对下适配各平台差异
5. 核心设计原则
- 生成与发送分离
- 控制面与执行面分离
- 网站负责业务状态,插件负责真实执行
- 服务端不存储平台 cookie
- 每个平台使用独立 adapter
6. 绑定媒体账号方案
6.1 业务目标
用户在后台点击“去绑定”后,前端先创建一次性 plugin_session。插件打开对应平台官网登录页。用户完成登录后,插件识别账号信息并即时回传前端,同时调用回调 API 持久化绑定关系。
6.2 绑定流程
sequenceDiagram
participant U as 用户
participant W as Web前端
participant P as 插件
participant M as 媒体平台
participant B as 后端
U->>W: 点击去绑定
W->>B: 创建 plugin_session(bind)
B-->>W: pluginSessionId + sessionToken
W->>P: openBindPage(platformId, loginUrl, ctx)
P->>M: 打开登录页
loop 轮询检测
P->>M: checkLogin / getUserInfo
M-->>P: 未登录或已登录账号信息
end
P-->>W: bindingDetected(platformId, platformUid, nickname, avatar)
P->>B: POST /api/callback/plugin/bind
B-->>P: 持久化成功
B-->>W: SSE / 轮询可见最新状态
6.3 绑定执行细节
- 前端先向后端申请
plugin_session - 后端返回
pluginSessionId + sessionToken + expireAt - 前端发起插件调用
openBindPage - 插件创建平台官网登录标签页
- 插件定时轮询平台账号状态
- 插件检测到
platformUid后,先向网页回传绑定事件用于即时反馈 - 插件再调用
/api/callback/plugin/bind可靠落库 - 后端写入
platform_accounts并更新页面状态
6.4 绑定状态定义
active:绑定有效invalid:账号失效或检测失败expired:账号登录态过期pending:等待绑定结果
7. 手动发送文章方案
7.1 业务目标
用户在文章列表页点击发送按钮后,前端先创建一次性 plugin_session,再由插件读取文章内容并执行真实发文。发布结果即时回传页面,同时通过回调接口可靠写入后台。
7.2 发送流程
sequenceDiagram
participant U as 用户
participant W as Web前端
participant B as 后端
participant P as 插件
participant M as 媒体平台
U->>W: 点击发送按钮
W->>B: 创建 plugin_session(publish)
B-->>W: pluginSessionId + sessionToken
W->>B: 获取文章详情
B-->>W: 返回标题/正文/封面/图片
W->>P: publishArticle(payload, ctx)
P->>P: 检查绑定账号与登录状态
P->>M: 上传图片
P->>M: 发布文章
M-->>P: 返回 articleId / status
P-->>W: 发布结果
P->>B: POST /api/callback/plugin/publish
B-->>P: 持久化成功
B-->>W: SSE / 轮询更新页面状态
7.3 发送执行细节
- 前端先读取插件状态与最低兼容版本,不满足时直接阻断按钮
- 前端从后端创建
plugin_session - 前端从后端读取文章详情
- 前端构造发送请求并调用插件
- 插件检查对应平台账号是否已绑定且仍有效
- 插件上传封面与正文图片
- 插件按平台 adapter 执行发布
- 插件先向页面返回结果,再回调
/api/callback/plugin/publish - 页面通过 SSE 或轮询刷新显示最新状态
7.4 发送状态定义
running:发送中success:发送成功failed:发送失败draft:保存草稿成功pending_review:平台审核中
8. 前后端接口设计
本节接口默认遵循主架构文档中的统一错误响应规范,失败时返回统一的 code / message / detail / request_id 结构。
8.1 绑定相关接口
POST /api/tenant/media/plugin-sessions
请求体:
{
"actionType": "bind",
"platformId": "zhihu"
}
响应:
{
"pluginSessionId": "ps_xxx",
"sessionToken": "st_xxx",
"expireAt": "2026-03-26T12:00:00+08:00",
"minPluginVersion": "1.0.0"
}
GET /api/tenant/media/platform-accounts
返回当前用户的媒体账号列表。
POST /api/tenant/media/platform-accounts/{id}/check
触发一次重新检测,用于刷新登录有效性。前端应先创建 plugin_session(actionType=check),再调用插件执行检查。
8.2 文章与发送接口
GET /api/tenant/articles/{id}
返回文章详情:
{
"id": "art_xxx",
"title": "文章标题",
"htmlContent": "<p>...</p>",
"markdownContent": "## ...",
"coverUrl": "https://xxx/cover.png",
"imageList": ["https://xxx/1.png"]
}
GET /api/tenant/media/publish-records?articleId=art_xxx
查询指定文章的所有发送记录。
8.3 插件回调接口
POST /api/callback/plugin/bind
请求体:
{
"pluginSessionId": "ps_xxx",
"sessionToken": "st_xxx",
"platformId": "zhihu",
"platformUid": "708996890603040768",
"nickname": "CharmingGeeker",
"avatarUrl": "https://xxx/avatar.png",
"clientVersion": "1.0.0",
"metadata": {}
}
POST /api/callback/plugin/publish
请求体:
{
"pluginSessionId": "ps_xxx",
"sessionToken": "st_xxx",
"articleId": "art_xxx",
"platformAccountId": "pa_xxx",
"platformId": "zhihu",
"status": "success",
"clientVersion": "1.0.0",
"requestPayload": {},
"responsePayload": {},
"externalArticleId": "1234567890",
"errorMessage": null
}
POST /api/callback/plugin/check
用于账号重检结果回写,负载与绑定接口类似,但语义是刷新已有 platform_account 状态。
9. 插件消息协议
9.1 插件可用性检测
页面加载时先执行:
type PluginPingResp = {
installed: boolean;
version?: string;
capabilities?: Array<"bind" | "publish" | "checkStatus">;
};
若未安装、被禁用或版本低于后端下发的 minPluginVersion,页面必须禁用绑定与发送按钮。
9.2 绑定媒体
共享上下文:
type ExecContext = {
pluginSessionId: string;
sessionToken: string;
minPluginVersion: string;
};
请求:
type OpenBindPageReq = {
platformId: string;
loginUrl: string;
ctx: ExecContext;
};
回调事件:
type BindingDetectedEvent = {
pluginSessionId: string;
platformId: string;
platformUid: string;
nickname: string;
avatarUrl?: string;
metadata?: Record<string, unknown>;
};
9.3 发送文章
请求:
type PublishArticleReq = {
articleId: string;
platformId: string;
platformAccountId: string;
title: string;
htmlContent: string;
markdownContent?: string;
coverUrl?: string;
imageList?: string[];
publishType: "publish" | "draft";
ctx: ExecContext;
};
响应:
type PublishArticleResp = {
success: boolean;
pluginSessionId: string;
platformId: string;
externalArticleId?: string;
status?: "draft" | "pending_review" | "published" | "failed";
message?: string;
};
10. 数据模型
10.1 平台账号表
create table platform_accounts (
id varchar(64) primary key,
tenant_id varchar(64) not null,
user_id varchar(64) not null,
platform_id varchar(64) not null,
platform_uid varchar(128) not null,
nickname varchar(255) not null,
avatar_url text null,
status varchar(32) not null,
metadata_json jsonb null,
last_check_at timestamp null,
created_at timestamp not null,
updated_at timestamp not null
);
唯一约束建议:
(tenant_id, user_id, platform_id, platform_uid)
10.2 插件会话表
create table plugin_sessions (
id varchar(64) primary key,
tenant_id varchar(64) not null,
user_id varchar(64) not null,
action_type varchar(32) not null,
platform_id varchar(64) not null,
session_token varchar(128) not null,
client_version varchar(32) null,
expire_at timestamp not null,
status varchar(32) not null,
created_at timestamp not null,
updated_at timestamp not null
);
10.3 文章表
create table articles (
id varchar(64) primary key,
tenant_id varchar(64) not null,
user_id varchar(64) not null,
title varchar(500) not null,
html_content text not null,
markdown_content text null,
cover_url text null,
image_list_json jsonb null,
status varchar(32) not null,
created_at timestamp not null,
updated_at timestamp not null
);
10.4 发送记录表
create table publish_records (
id varchar(64) primary key,
tenant_id varchar(64) not null,
article_id varchar(64) not null,
platform_account_id varchar(64) not null,
platform_id varchar(64) not null,
status varchar(32) not null,
request_payload_json jsonb null,
response_payload_json jsonb null,
external_article_id varchar(128) null,
error_message text null,
created_at timestamp not null,
updated_at timestamp not null
);
11. 平台适配层设计
每个平台实现统一接口:
interface PlatformAdapter {
platformId: string;
checkLogin(ctx: ExecContext): Promise<LoginInfo | null>;
openBindPage(ctx: ExecContext): Promise<void>;
uploadImage(input: UploadInput, ctx: ExecContext): Promise<{ url: string }>;
publish(input: PublishInput, ctx: ExecContext): Promise<PublishArticleResp>;
checkStatus(input: CheckStatusInput, ctx: ExecContext): Promise<{ status: string }>;
}
V1 推荐先做 1 个平台打通全链路,再扩展其他平台。
推荐顺序:
- 知乎
- 头条号
- 百家号
- 网易号
12. 插件实现要求
12.1 注入与通信
- 只注入本系统后台域名
- 通过受控
postMessage或runtime.sendMessage通信 - 所有消息必须校验来源域名和会话信息
- 必须支持
plugin.ping,返回安装状态、版本号和能力清单 - 所有绑定 / 检测 / 发布动作都必须带上
pluginSessionId + sessionToken
12.2 执行逻辑
- 打开登录页
- 轮询登录状态
- 上传图片
- 发布文章
- 返回结果
- 绑定和发布结果必须“先即时回传页面,再回调后端可靠落库”
- 回调接口必须按
plugin_session_id + callback_stage做幂等
12.3 V1 不要求
- 后台常驻拉任务
- 自动定时发送
- 云端远程控制
13. 安全要求
- 服务端不保存平台 cookie
- 插件与页面通信必须校验
origin - 所有绑定与发送操作写审计日志
- 后端保存文章与发送结果,便于追查
- 平台 UID、昵称、头像可持久化,敏感票据只存在本地执行环境
- 回调接口只信任
session_token,不信任浏览器 Cookie - 插件只允许向受信后台域名发消息与回调
14. 错误处理
14.1 绑定失败
常见原因:
- 用户未完成平台登录
- 平台页面结构变化
- 平台接口返回异常
- 插件未安装或版本过低
处理方式:
- 前端提示失败原因
- 保持账号状态为
pending或invalid - 支持重新检测
- 提供插件安装或升级引导
14.2 发送失败
常见原因:
- 平台账号失效
- 图片上传失败
- 平台接口改版
- 平台风控或审核限制
- 插件回调超时
处理方式:
- 记录失败原因
- 返回具体 message
- 发送记录保留原始结果
- 回调失败进入补偿队列
15. 页面最小落地范围
15.1 媒体账号页
- 平台列表
- 绑定状态
- 去绑定按钮
- 重新检测按钮
- 插件安装 / 版本状态提示
15.2 文章列表页
- 文章标题
- 文章状态
- 发送按钮
- 查看详情
- 查看发送记录
15.3 发送记录页
- 平台
- 发送时间
- 发送状态
- 平台文章 ID
- 错误原因
16. 开发顺序
阶段 1:业务后端
- 建表
plugin_session接口- 文章接口
- 插件回调接口
- 发送记录查询接口
阶段 2:Web 前端
- 媒体管理页
- 文章列表与发送按钮
- 发送记录页
- 插件可用性检测与降级提示
阶段 3:插件基础层
- 页面通信
plugin.ping- 打开登录页
- 回传绑定结果
- 回调后端接口
阶段 4:首个平台 adapter
- 先打通绑定
- 再打通发文
- 最后接状态查询
阶段 5:扩展更多平台
- 复制 adapter 结构
- 按平台差异逐个接入
17. V1 验收标准
17.1 绑定媒体
- 可成功绑定至少 1 个平台
- 可显示账号昵称和头像
- 可执行重新检测
- 插件能通过回调接口落库绑定关系
17.2 手动发送
- 从后台文章列表点击发送
- 插件接收文章内容并发文
- 平台返回文章 ID
- 插件能通过回调接口保存发送记录
- 前端可看到发送成功或失败状态
18. 后续演进方向
V2 可扩展:
- 多平台批量发送
- 发送前 AI 优化
- 发送后状态轮询
- 草稿箱管理
V3 可扩展:
- 本地 agent 常驻
- 自动定时发送
- 多执行器调度
- 失败重试与补偿