Files
geo/docs/media-binding-and-publish-v1.md
T

666 lines
15 KiB
Markdown
Raw Normal View History

# 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": "<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`
请求体:
```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<string, unknown>;
};
```
### 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<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 注入与通信
- 只注入本系统后台域名
- 通过受控 `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` 接口
- 文章接口
- 插件回调接口
- 发送记录查询接口
### 阶段 2Web 前端
- 媒体管理页
- 文章列表与发送按钮
- 发送记录页
- 插件可用性检测与降级提示
### 阶段 3:插件基础层
- 页面通信
- `plugin.ping`
- 打开登录页
- 回传绑定结果
- 回调后端接口
### 阶段 4:首个平台 adapter
- 先打通绑定
- 再打通发文
- 最后接状态查询
### 阶段 5:扩展更多平台
- 复制 adapter 结构
- 按平台差异逐个接入
## 17. V1 验收标准
### 17.1 绑定媒体
- 可成功绑定至少 1 个平台
- 可显示账号昵称和头像
- 可执行重新检测
- 插件能通过回调接口落库绑定关系
### 17.2 手动发送
- 从后台文章列表点击发送
- 插件接收文章内容并发文
- 平台返回文章 ID
- 插件能通过回调接口保存发送记录
- 前端可看到发送成功或失败状态
## 18. 后续演进方向
V2 可扩展:
- 多平台批量发送
- 发送前 AI 优化
- 发送后状态轮询
- 草稿箱管理
V3 可扩展:
- 本地 agent 常驻
- 自动定时发送
- 多执行器调度
- 失败重试与补偿