mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
feat: add Bright conversation profile read model
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
# Bright OneTalk 会话读取 API
|
||||
|
||||
> 状态:目标接口契约。接口只读取 Bright 已持久化的 direct 会话、消息和联系人资料;不读取 Mind 数据库、不代理插件,也不使用同步锚点作为页面游标。
|
||||
|
||||
## 通用授权、CORS 与读取范围
|
||||
|
||||
所有接口路径中的 channelAccountId 都必须与当前 Mind Session 授权出的账号完全一致。scope 永远来自该授权结果,不信任页面提交的用户、工作区或设备字段。
|
||||
|
||||
当浏览器带 Origin 时,Bright 只接受精确的 Mind 页面 Origin;成功的跨域响应包含:
|
||||
|
||||
```http
|
||||
Access-Control-Allow-Origin: <configured Mind Origin>
|
||||
Access-Control-Allow-Credentials: true
|
||||
Vary: Origin
|
||||
```
|
||||
|
||||
OPTIONS 允许 GET、OPTIONS 和 Content-Type、X-Mind-Purpose。若浏览器的 Access-Control-Request-Headers 包含任一其它 header(比较不区分大小写)则返回 403;不允许的 Origin 同样返回 403,且不会调用读取服务或授权读取。
|
||||
|
||||
```ts
|
||||
type Scope = {
|
||||
workspaceId: string;
|
||||
mindUserId: string;
|
||||
channelAccountId: string;
|
||||
};
|
||||
|
||||
type Plugin = {
|
||||
status: "online" | "offline";
|
||||
};
|
||||
```
|
||||
|
||||
plugin.status 只表示当前插件连接;插件离线不影响对已持久化 Bright 数据的读取。
|
||||
|
||||
## 会话列表
|
||||
|
||||
```http
|
||||
GET /api/bright/onetalk/accounts/:channelAccountId/conversations
|
||||
?cursor=:opaqueCursor
|
||||
&limit=50
|
||||
&query=:optionalQuery
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 默认值 | 语义 |
|
||||
| ------ | ------- | ------ | ------------------------------------------------------------------------- |
|
||||
| cursor | string | 无 | 不透明列表游标。只能原样回传同一条件的 nextCursor,客户端不得解析或构造。 |
|
||||
| limit | integer | 50 | 1..100。 |
|
||||
| query | string | 无 | 先 trim、规范化后匹配实时 name 或 conversationId。空白 query 等同未筛选。 |
|
||||
|
||||
```ts
|
||||
type ConversationListResponse = {
|
||||
scope: Scope;
|
||||
plugin: Plugin;
|
||||
conversations: CenterConversation[];
|
||||
page: Page;
|
||||
};
|
||||
|
||||
type Page = {
|
||||
hasMore: boolean;
|
||||
nextCursor: string | null;
|
||||
};
|
||||
|
||||
type CenterConversation = {
|
||||
channelAccountId: string;
|
||||
conversationId: string;
|
||||
conversationType: "direct";
|
||||
|
||||
name: string | null;
|
||||
avatarUrl: string | null;
|
||||
participantIds: [];
|
||||
|
||||
latestMessageId: string | null;
|
||||
latestMessageAtMs: number | null;
|
||||
|
||||
unreadCount: 0;
|
||||
messageCount: number;
|
||||
historyComplete: boolean;
|
||||
syncPhase: "initial" | "incremental";
|
||||
syncResult: "incomplete" | "succeeded" | "succeeded_with_anomalies" | "failed";
|
||||
};
|
||||
```
|
||||
|
||||
列表按 latestMessageAtMs DESC NULLS LAST、conversationId ASC 排序。latestMessageId 与 latestMessageAtMs 必须来自同一条真实持久化消息;无消息时二者都为 null。participantIds 固定为空数组,unreadCount 固定为 0。
|
||||
|
||||
列表首次请求固定 asOf 事实快照,后续游标绑定账号、规范化 query、快照和 keyset。会话发现、消息入库或排序变化不会使同一 cursor 链重复或遗漏;服务端以 limit + 1 判断下一页。
|
||||
|
||||
联系人资料是唯一的明确例外:每一页都 left join 当前 profile table。因此资料 name、avatarUrl 的变化会立即显示,且资料名称变化可能改变后续页的 query 筛选结果;接口不保存 profile 历史或分页缓存。
|
||||
|
||||
## 单会话详情
|
||||
|
||||
```http
|
||||
GET /api/bright/onetalk/accounts/:channelAccountId/conversations/:conversationId
|
||||
```
|
||||
|
||||
```ts
|
||||
type ConversationResponse = {
|
||||
scope: Scope;
|
||||
plugin: Plugin;
|
||||
conversation: CenterConversation;
|
||||
};
|
||||
```
|
||||
|
||||
详情和列表使用同一个 CenterConversation projection。若当前授权账号下没有这个 direct 会话:
|
||||
|
||||
```http
|
||||
404 Not Found
|
||||
```
|
||||
|
||||
```ts
|
||||
{
|
||||
error: {
|
||||
code: "conversation_not_found";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 消息读取
|
||||
|
||||
```http
|
||||
GET /api/bright/onetalk/accounts/:channelAccountId/conversations/:conversationId/messages
|
||||
?fromSentAtMs=:inclusiveEpochMs
|
||||
&toSentAtMs=:exclusiveEpochMs
|
||||
&cursor=:opaqueCursor
|
||||
&limit=50
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 默认值 | 语义 |
|
||||
| ------------ | ------------ | ------ | --------------------------------------------------------------------- |
|
||||
| fromSentAtMs | safe integer | 无 | 可选下界,包含该毫秒。 |
|
||||
| toSentAtMs | safe integer | 无 | 可选上界,不包含该毫秒。 |
|
||||
| cursor | string | 无 | 不透明历史游标。只能原样回传前一页 nextCursor,客户端不得解析或构造。 |
|
||||
| limit | integer | 50 | 1..100。 |
|
||||
|
||||
时间窗口是半开区间:fromSentAtMs <= sentAtMs < toSentAtMs。两端同时存在时必须 fromSentAtMs < toSentAtMs。历史游标独立于列表游标,并绑定账号、会话、时间窗、asOf 和 (sentAtMs, messageId) keyset;任一条件不一致都无效。
|
||||
|
||||
```ts
|
||||
type MessagePageResponse = {
|
||||
scope: Scope;
|
||||
conversationId: string;
|
||||
messages: CenterMessage[];
|
||||
page: Page;
|
||||
};
|
||||
|
||||
type CenterMessageBase = {
|
||||
messageId: string;
|
||||
conversationId: string;
|
||||
senderId: string;
|
||||
participantIds: string[];
|
||||
direction: "received" | "sent";
|
||||
sentAtMs: number;
|
||||
readStatus: "read" | "unread";
|
||||
};
|
||||
|
||||
type CenterMessage =
|
||||
| (CenterMessageBase & {
|
||||
contentType: "text";
|
||||
content: { text: string | null };
|
||||
})
|
||||
| (CenterMessageBase & {
|
||||
contentType: "img";
|
||||
content: { url: string | null };
|
||||
})
|
||||
| (CenterMessageBase & {
|
||||
contentType: "attachment";
|
||||
content: { name: string | null; url: string | null };
|
||||
})
|
||||
| (CenterMessageBase & {
|
||||
contentType: "unknown";
|
||||
content: null;
|
||||
});
|
||||
```
|
||||
|
||||
消息按 sentAtMs ASC、messageId ASC 排序,首次读取固定事实 asOf,服务端以 limit + 1 判断下一页。任何未被验证为 text、img 或 attachment 的合法原始消息都保留为 unknown,不会猜测为另一种语义。
|
||||
|
||||
响应绝不暴露原始数字或原始顶层字段,包括原始 contentType、readStatus、messageStatus、unreadCount、messageRevision、conversationRevision、updatedAtMs、recalledAtMs、deliveryStatus、顶层 text、顶层 subject 和 attachmentSummaryText。
|
||||
|
||||
## 纪要读取门槛
|
||||
|
||||
普通页面读取可省略时间窗,并可读取已持久化的部分历史。Mind 读取纪要窗口时必须同时提供两个时间端点,并发送精确 header:
|
||||
|
||||
```http
|
||||
X-Mind-Purpose: communication_summary_read
|
||||
```
|
||||
|
||||
若该会话 historyComplete 为 false,Bright 返回:
|
||||
|
||||
```http
|
||||
503 Service Unavailable
|
||||
Retry-After: 30
|
||||
```
|
||||
|
||||
```ts
|
||||
{
|
||||
error: {
|
||||
code: "history_incomplete";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 分页与错误
|
||||
|
||||
对所有页面,hasMore 为 true 时 nextCursor 必须是非空字符串;hasMore 为 false 时必须是 null。客户端只能保存并回传该 opaque 字符串,不能从会话 ID、消息 ID、同步 anchor 或任何响应字段推导游标。
|
||||
|
||||
| HTTP 状态 | error.code | 条件 |
|
||||
| --------- | ---------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| 400 | invalid_limit | limit 不是 1..100。 |
|
||||
| 400 | invalid_cursor | cursor 非法或不匹配账号、query、会话、窗口或 snapshot。 |
|
||||
| 400 | invalid_time_range | 时间不是安全整数,或窗口不是有效半开区间。 |
|
||||
| 401 | auth_required | 缺少有效 Mind Session。 |
|
||||
| 403 | scope_mismatch | 路径账号、Session scope 或 Origin 不匹配。 |
|
||||
| 403 | authorization_rejected、binding_revoked、authorization_version_changed | Mind 授权拒绝。 |
|
||||
| 404 | conversation_not_found | 授权范围内不存在该 direct 会话。 |
|
||||
| 503 | authorization_unavailable | 授权依赖不可用或 Bright v2 被暂停。 |
|
||||
| 503 | database_unavailable | Bright 读取数据库暂时不可用。 |
|
||||
| 503 | history_incomplete | 纪要读取请求到尚未完整的历史;带 Retry-After: 30。 |
|
||||
| 500 | internal_error | 未预期错误;不返回内部异常、Cookie、连接串或原始正文。 |
|
||||
Reference in New Issue
Block a user