Files
geo/docs/media-binding-and-publish-v1.md
T
root de30497f59 feat: add tenant and user management with migrations, handlers, and tests
- Implemented tenant and user management features including:
  - Tenant creation and management with associated migrations.
  - User creation and management with associated migrations.
  - Tenant membership management with associated migrations.
  - Platform user roles management with associated migrations.
  - Quota management with associated migrations.
  - Article and template management with associated migrations.
- Added HTTP handlers for templates and workspaces.
- Created tests for protected and public routes.
- Introduced a script to check tenant scope in SQL queries.
- Documented task plan for backend completion and frontend foundation.
2026-04-01 00:58:42 +08:00

15 KiB
Raw Blame History

V1 技术方案:绑定媒体账号与手动发送文章

口径说明:本文的插件执行流程、接口路径与会话模型已同步到主架构文档 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 绑定执行细节

  1. 前端先向后端申请 plugin_session
  2. 后端返回 pluginSessionId + sessionToken + expireAt
  3. 前端发起插件调用 openBindPage
  4. 插件创建平台官网登录标签页
  5. 插件定时轮询平台账号状态
  6. 插件检测到 platformUid 后,先向网页回传绑定事件用于即时反馈
  7. 插件再调用 /api/callback/plugin/bind 可靠落库
  8. 后端写入 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 发送执行细节

  1. 前端先读取插件状态与最低兼容版本,不满足时直接阻断按钮
  2. 前端从后端创建 plugin_session
  3. 前端从后端读取文章详情
  4. 前端构造发送请求并调用插件
  5. 插件检查对应平台账号是否已绑定且仍有效
  6. 插件上传封面与正文图片
  7. 插件按平台 adapter 执行发布
  8. 插件先向页面返回结果,再回调 /api/callback/plugin/publish
  9. 页面通过 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 个平台打通全链路,再扩展其他平台。

推荐顺序:

  1. 知乎
  2. 头条号
  3. 百家号
  4. 网易号

12. 插件实现要求

12.1 注入与通信

  • 只注入本系统后台域名
  • 通过受控 postMessageruntime.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 绑定失败

常见原因:

  • 用户未完成平台登录
  • 平台页面结构变化
  • 平台接口返回异常
  • 插件未安装或版本过低

处理方式:

  • 前端提示失败原因
  • 保持账号状态为 pendinginvalid
  • 支持重新检测
  • 提供插件安装或升级引导

14.2 发送失败

常见原因:

  • 平台账号失效
  • 图片上传失败
  • 平台接口改版
  • 平台风控或审核限制
  • 插件回调超时

处理方式:

  • 记录失败原因
  • 返回具体 message
  • 发送记录保留原始结果
  • 回调失败进入补偿队列

15. 页面最小落地范围

15.1 媒体账号页

  • 平台列表
  • 绑定状态
  • 去绑定按钮
  • 重新检测按钮
  • 插件安装 / 版本状态提示

15.2 文章列表页

  • 文章标题
  • 文章状态
  • 发送按钮
  • 查看详情
  • 查看发送记录

15.3 发送记录页

  • 平台
  • 发送时间
  • 发送状态
  • 平台文章 ID
  • 错误原因

16. 开发顺序

阶段 1:业务后端

  • 建表
  • plugin_session 接口
  • 文章接口
  • 插件回调接口
  • 发送记录查询接口

阶段 2Web 前端

  • 媒体管理页
  • 文章列表与发送按钮
  • 发送记录页
  • 插件可用性检测与降级提示

阶段 3:插件基础层

  • 页面通信
  • plugin.ping
  • 打开登录页
  • 回传绑定结果
  • 回调后端接口

阶段 4:首个平台 adapter

  • 先打通绑定
  • 再打通发文
  • 最后接状态查询

阶段 5:扩展更多平台

  • 复制 adapter 结构
  • 按平台差异逐个接入

17. V1 验收标准

17.1 绑定媒体

  • 可成功绑定至少 1 个平台
  • 可显示账号昵称和头像
  • 可执行重新检测
  • 插件能通过回调接口落库绑定关系

17.2 手动发送

  • 从后台文章列表点击发送
  • 插件接收文章内容并发文
  • 平台返回文章 ID
  • 插件能通过回调接口保存发送记录
  • 前端可看到发送成功或失败状态

18. 后续演进方向

V2 可扩展:

  • 多平台批量发送
  • 发送前 AI 优化
  • 发送后状态轮询
  • 草稿箱管理

V3 可扩展:

  • 本地 agent 常驻
  • 自动定时发送
  • 多执行器调度
  • 失败重试与补偿