# V1 技术方案:绑定媒体账号与手动发送文章(旧版设计) > 旧版设计说明:本文描述的是基于浏览器插件回调、`plugin_sessions`、`/api/callback/plugin/*` 的旧方案。自 2026-04-20 起,当前实现已切换到 desktop client 架构。本文保留用于工作记录与方案追溯,不再作为当前实现依据。 > 口径说明:本文的插件执行流程、接口路径与会话模型已同步到主架构文档 [geo-platform-ops-admin-tech-architecture-v1.md](D:/project/geo/docs/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. 总体架构 ```mermaid 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 绑定流程 ```mermaid 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 发送流程 ```mermaid 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` 请求体: ```json { "actionType": "bind", "platformId": "zhihu" } ``` 响应: ```json { "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}` 返回文章详情: ```json { "id": "art_xxx", "title": "文章标题", "htmlContent": "

...

", "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` 请求体: ```json { "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` 请求体: ```json { "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 插件可用性检测 页面加载时先执行: ```ts type PluginPingResp = { installed: boolean; version?: string; capabilities?: Array<"bind" | "publish" | "checkStatus">; }; ``` 若未安装、被禁用或版本低于后端下发的 `minPluginVersion`,页面必须禁用绑定与发送按钮。 ### 9.2 绑定媒体 共享上下文: ```ts type ExecContext = { pluginSessionId: string; sessionToken: string; minPluginVersion: string; }; ``` 请求: ```ts type OpenBindPageReq = { platformId: string; loginUrl: string; ctx: ExecContext; }; ``` 回调事件: ```ts type BindingDetectedEvent = { pluginSessionId: string; platformId: string; platformUid: string; nickname: string; avatarUrl?: string; metadata?: Record; }; ``` ### 9.3 发送文章 请求: ```ts type PublishArticleReq = { articleId: string; platformId: string; platformAccountId: string; title: string; htmlContent: string; markdownContent?: string; coverUrl?: string; imageList?: string[]; publishType: "publish" | "draft"; ctx: ExecContext; }; ``` 响应: ```ts type PublishArticleResp = { success: boolean; pluginSessionId: string; platformId: string; externalArticleId?: string; status?: "draft" | "pending_review" | "published" | "failed"; message?: string; }; ``` ## 10. 数据模型 ### 10.1 平台账号表 ```sql 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 插件会话表 ```sql 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 文章表 ```sql 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 发送记录表 ```sql 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. 平台适配层设计 每个平台实现统一接口: ```ts interface PlatformAdapter { platformId: string; checkLogin(ctx: ExecContext): Promise; openBindPage(ctx: ExecContext): Promise; uploadImage(input: UploadInput, ctx: ExecContext): Promise<{ url: string }>; publish(input: PublishInput, ctx: ExecContext): Promise; checkStatus(input: CheckStatusInput, ctx: ExecContext): Promise<{ status: string }>; } ``` V1 推荐先做 1 个平台打通全链路,再扩展其他平台。 推荐顺序: 1. 知乎 2. 头条号 3. 百家号 4. 网易号 ## 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 常驻 - 自动定时发送 - 多执行器调度 - 失败重试与补偿