# V1 技术方案:绑定媒体账号与手动发送文章 > 口径说明:本文的插件执行流程、接口路径与会话模型已同步到主架构文档 [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