Files
geo/docs/media-binding-and-publish-v1.md
T
root 5ff2e2e74c refactor(tenant): drop legacy plugin_installations, migrate monitoring to desktop_clients
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>
2026-04-20 13:52:35 +08:00

668 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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": "<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 常驻
- 自动定时发送
- 多执行器调度
- 失败重试与补偿