| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791 |
- /**
- * 领域模型 —— 前端唯一权威类型定义
- *
- * 依据《数据访问层设计.md》§三。云函数返回的原始形状由 `ParseApi` 的映射函数
- * 转成本文件定义的模型,页面只见模型,不见原始字段。
- *
- * 🔴 禁止在页面/组件内另建同义类型;禁止把云函数的原始 JSON 形状泄漏到视图层。
- */
- // ---------- 通用 ----------
- /** 加载状态三态 + 空闲 */
- export type LoadState = 'idle' | 'loading' | 'ready' | 'empty' | 'error';
- // ---------- 数字生命 ----------
- /** 数字生命运行状态(与云函数 xlosi3m46q 的 status 取值一致) */
- export type AgentStatus = 'running' | 'idle' | 'error' | 'offline' | 'stopped';
- export interface HostMetrics {
- cpuPercent?: number;
- memPercent?: number;
- diskPercent?: number;
- }
- export interface Balance {
- /** 服务端换算后的余额(元)。映射层负责归一:兼容旧键 remainRmb 后统一写入此处 */
- remainCny?: number;
- /** 业务再算页面外链(FR-07 AC-07.4) */
- pageUrl?: string;
- }
- export interface ChannelConfig {
- matrix?: boolean;
- homeserver?: string;
- matrixConfig?: {
- userId?: string;
- homeserver?: string;
- serverName?: string;
- /**
- * 容器侧上报的通道状态(实测数据里有,此前类型未声明)。
- * `'connected'` = 容器 Matrix 通道在跑;缺失 = 未上报(可能离线)。
- */
- channelState?: string;
- /** listener 健康检查通过(旧字段,与 channelState 二选一为真即在线) */
- listenerOk?: boolean;
- /** 更细的状态明细(gateway / matrix / wecom 各自状态) */
- channelDetail?: {
- matrixState?: string;
- gatewayState?: string;
- wecomState?: string;
- codeVersion?: string;
- source?: string;
- };
- };
- /** 企业微信通道是否点亮(数组形态归一后得出) */
- wecom?: boolean;
- /** 企微机器人 BotID(数组形态下仅知通道名、无此值,故可选) */
- wecomBotId?: string;
- }
- /**
- * Token 用量。
- * 🔴 映射口径:云函数返回的 `usage` / `usageSessions` 是**字符串形态的 JSON**,
- * 且字段名未与服务端逐字段核对(《API-契约》§六-4)。映射函数负责
- * ① 兼容「已是对象 / 是 JSON 字符串」;② 归一字段名;③ 未识别字段保留在 raw。
- */
- export interface Usage {
- promptTokens?: number;
- completionTokens?: number;
- totalTokens?: number;
- updatedAt?: string;
- /** 原始未归一字段(排查用,UI 不直接消费) */
- raw?: Record<string, unknown>;
- }
- /** 单次会话的 token 用量(云函数 `usageSessions` 元素,契约见 reporter 字段规范 §3.4) */
- export interface UsageSession {
- sessionId?: string;
- title?: string;
- model?: string;
- messageCount?: number;
- toolCallCount?: number;
- inputTokens?: number;
- outputTokens?: number;
- cacheReadTokens?: number;
- reasoningTokens?: number;
- estimatedCostUsd?: number;
- costStatus?: string;
- /** epoch 数字(服务端不转换) */
- startedAt?: number;
- }
- /**
- * 容器上报的**单个会话**运行状态(2026-11-09,方案 A)。
- *
- * 由容器在 `pre_llm_call` / `post_llm_call` 钩子里维护,写进 `extra.currentSessions`。
- * `sessionKey` 是容器的 `build_session_key()` 产物,形如:
- * `agent:main:matrix:dm:<!roomId>[:<threadId>]`
- *
- * 🔴 App 的会话 id 是 Matrix 的 **thread 根 event_id**,与这里的 `sessionKey`
- * 是否对得上**必须实测验证** —— 对不上就是「报了但匹配不到会话」。
- */
- export interface AgentSessionState {
- /** 容器的 session_key 原文 */
- sessionKey: string;
- /** 该会话当前是否在跑 */
- status: 'running' | 'idle';
- /** 本轮开始的 epoch 毫秒(idle 时可缺省) */
- turnStartAt?: number;
- }
- export interface Person {
- id: string;
- name: string;
- avatar?: string;
- mobile?: string;
- /** 同事行展开时用 */
- agents?: Agent[];
- /**
- * 所属公司 id(`Company.objectId`)。
- * 通讯录按公司分组时使用;缺失时归入「未归属」。
- */
- companyId?: string;
- /**
- * 所属部门。
- *
- * 🔴 **预留字段**(2026-11-04):服务端目前没有部门维度,
- * 该值恒为 undefined,通讯录会归入「未分组」。
- * 待组织架构落地后由 `_User` / `Company` 表补充,前端不用改结构。
- */
- department?: string;
- }
- export interface Agent {
- id: string;
- name: string;
- avatar?: string;
- /** 专长描述(原型 specialty) */
- specialty?: string;
- gender?: 'male' | 'female' | 'unknown';
- status: AgentStatus;
- /** 状态中文文案(由 core/format 统一给出,不在组件里拼) */
- statusText?: string;
- currentTask?: string;
- currentAction?: string;
- progress?: number;
- heartbeatAt?: string;
- /**
- * 心跳距今**秒数**(2026-11-07 新增)。
- *
- * 云函数 `xlosi3m46q` 直接返回这个字段(实测它有值,而 `heartbeatAt` 为 undefined)。
- * 容器掉线后**不会**把 `status` 改成 offline,只会停止心跳 ——
- * 实测两个容器 `heartbeatAgeSec = 3914610`(45 天)却仍报 `running`。
- * 状态展示(`core/agent-state.ts`)用它判定「其实已离线」。
- */
- heartbeatAgeSec?: number;
- updatedAt?: string;
- host?: HostMetrics;
- balance?: Balance;
- channels?: ChannelConfig;
- usage?: Usage;
- /**
- * 会话级用量明细(云函数 `usageSessions`,JSON 字符串形态)。
- * 用于「Token 用量」下钻,缺失时不影响总量展示。
- */
- usageSessions?: UsageSession[];
- owner?: Person;
- companyId?: string;
- /**
- * 会话级运行状态(2026-11-09,方案 A)。
- *
- * 🔴 为什么需要:容器上报的 `status` 是**整个容器一个值** ——
- * 同一容器的多个会话共用它,于是「会话 A 在跑」时,会话 B 的界面也显示
- * 「正在执行」(用户实测)。
- *
- * 容器把每个会话的状态塞进 `extra.currentSessions`(`extra` 是平台
- * 云函数字段白名单里的合法字段,无需改云函数),这里解析出来供按会话展示。
- *
- * 容器尚未上报时为 `undefined` —— 调用方必须回落到全局 `status`,
- * 不能因为缺这个字段就显示「未知」。
- */
- currentSessions?: AgentSessionState[];
- /** 所属部门 —— **预留字段**(2026-11-04)。
- * 服务端暂无此维度,恒为 undefined;通讯录归入「未分组」。
- */
- department?: string;
- /** 容器标识(云函数 `containerRef`,Docker 短 ID / 主机名,排障时用来反查来源) */
- containerRef?: string;
- /**
- * Agent 自身 token 健康灯(云函数 `sessionStatus`)。
- * `ok` / `expired` / `unknown` —— expired 时余额等依赖 token 的字段会降级。
- */
- sessionStatus?: 'ok' | 'expired' | 'unknown';
- /** 是否已接入 FMODE(决定「进会话 vs 进详情」,见 AgentRouteService) */
- matrixReady: boolean;
- /** 会话未读数 */
- unread: number;
- }
- // ---------- 会话与消息 ----------
- /**
- * 会话。
- *
- * ## 两种形态(2026-10-21 新增群聊)
- *
- * | kind | 说明 | 用到哪些字段 |
- * |---|---|---|
- * | `dm` | 用户 ↔ 单个数字生命 | `agentId` / `peer` |
- * | `group` | 一个房间里有多个数字生命 | `members` / `name` |
- *
- * 🔴 群聊里**同一个房间里会有多个 agent 发言**,所以消息必须能标出「是谁说的」。
- * 此前消息只分 `me` / `agent`,无法区分多个 agent(见 `Message.senderId`)。
- */
- export interface Conversation {
- /** Matrix roomId(演示态为合成 id) */
- id: string;
- /** 会话形态;缺省视为 dm(兼容旧数据) */
- kind?: 'dm' | 'group';
- /** dm:对方的 agentId。群聊时为空 */
- agentId?: string;
- /** dm:对方 */
- peer?: Agent;
- /** group:群名称 */
- name?: string;
- /** group:房间内的 agent 成员(不含自己) */
- members?: Agent[];
- lastMessagePreview?: string;
- lastMessageAt?: string;
- unread: number;
- }
- export type MessageKind = 'text' | 'notice' | 'emote';
- export type MessageState = 'sending' | 'sent' | 'failed';
- export interface FileCard {
- kind: 'file';
- name: string;
- size: number;
- mimeType: string;
- url: string;
- thumb?: string;
- /**
- * 云盘(OBS/S3)公网地址(2026-11-09)。
- *
- * 用户要求「之后查看都走云盘」:有这个地址就优先用它,
- * 没有则回落到 Matrix 的 mxc 转换地址。缺失说明当时 S3 上传失败。
- */
- cloudUrl?: string;
- }
- export interface Message {
- id: string;
- conversationId: string;
- /**
- * 发送方类别。
- * 🔴 `agent` 不区分具体是哪个 agent —— 群聊里要结合 `senderId` / `senderName` 才能区分。
- */
- sender: 'me' | 'agent';
- /** 群聊:具体发送者的 Matrix ID(dm 下可省略) */
- senderId?: string;
- /** 群聊:展示用名字(agent 的 botName,或人类昵称) */
- senderName?: string;
- /** 这条消息 @ 了哪些人(Matrix ID)。群聊 UI 用来高亮「在叫我 / 叫我同事」 */
- mentions?: string[];
- kind: MessageKind;
- text?: string;
- file?: FileCard;
- artifactId?: string;
- taskId?: string;
- ts: string;
- state: MessageState;
- /** 失败原因(AC-04.8:失败消息不丢失,可重试) */
- error?: string;
- /**
- * 发送时写入 Matrix 事件的本地 id(`com.fmode.local`)。
- *
- * 服务端 echo 会原样带回它 —— 回读时据此**认领**本地那条乐观消息,
- * 避免同一句话同时以「本地一条 + 回读一条」出现两遍(2026-11-09)。
- * 容器 / 历史事件没有这个字段。
- */
- localId?: string;
- }
- // ---------- 成果与项目 ----------
- export interface Artifact {
- name: string;
- url: string;
- type: string;
- /**
- * 容器内的原始路径(非 URL 时才有)。
- * 该 artifact **尚未上传到云盘**,由 `ArtifactSyncService` 触发同步;
- * `url` 为空时 UI 不得提供下载(避免给一个打不开的链接)。
- */
- source?: string;
- }
- export type DeliverableState = 'delivered' | 'failed';
- /**
- * 文件台账条目(云函数 fmode-file-hub,2026-11-10)
- *
- * 用户裁决:消息不入库,只有**文件与成果**入库,让右侧栏能按会话直接加载。
- * 只存访问链接与元信息,不存文件内容。
- */
- export interface FileEntry {
- /** 会话归属(App 的会话 id = Matrix thread 根),核心字段 */
- threadRoot: string;
- /** 产出的容器;用户上传的文件也要带,便于按容器聚合 */
- agentId?: string;
- roomId?: string;
- name: string;
- /** 云盘访问链接(https,必须是 s3.fmode.cn / file-cloud.fmode.cn) */
- url: string;
- /** 云盘对象键,url 只是它的投影 */
- key?: string;
- mimeType?: string;
- size?: number;
- /** 谁放进来的:用户自己传的,还是容器产出的 */
- uploadedBy: 'user' | 'agent';
- /** 容器内的原始路径(仅容器产出时有) */
- source?: string;
- }
- /** 从文件台账拉回的一条记录(服务端已归一化) */
- export interface SessionFile {
- id: string;
- threadRoot: string;
- agentId: string;
- roomId: string;
- name: string;
- /** 未知大小用 -1(与云函数口径一致) */
- size: number;
- mimeType: string;
- url: string;
- storageKey: string;
- uploadedBy: 'user' | 'agent';
- status: string;
- /** epoch 毫秒 */
- deliveredAt: number;
- }
- export interface Deliverable {
- id: string;
- agentId: string;
- title: string;
- subtitle?: string;
- summary?: string;
- projectId?: string;
- projectName?: string;
- artifacts: Artifact[];
- tags: string[];
- deliveredAt: string;
- state: DeliverableState;
- /** 成果正文(演示态用;真实态由 artifacts 外链承载) */
- content?: string;
- /** 文件类型标签(DOC / CSV …) */
- fileType?: string;
- filename?: string;
- size?: string;
- version?: string;
- }
- export interface DeliverableGroup {
- project?: string;
- projectName: string;
- count: number;
- items: Deliverable[];
- }
- export interface Project {
- id: string;
- name: string;
- code?: string;
- description?: string;
- brief?: string;
- status?: string;
- owner?: string;
- due?: string;
- updated?: string;
- agentIds: string[];
- deliverableIds: string[];
- hasBlocked: boolean;
- }
- // ---------- 事件(派生,非独立表 —— SPEC FR-06 口径) ----------
- export type EventCategory = 'delivery' | 'execution' | 'alert';
- export interface EventItem {
- id: string;
- category: EventCategory;
- title: string;
- description?: string;
- ts: string;
- agentId: string;
- artifactId?: string;
- taskId?: string;
- }
- // ---------- 任务看板(FR-07b) ----------
- export type TaskStatus =
- | 'triage'
- | 'todo'
- | 'scheduled'
- | 'ready'
- | 'running'
- | 'blocked'
- | 'done';
- export interface TaskRun {
- id: string;
- status: string;
- outcome?: string;
- stepKey?: string;
- title?: string;
- detail?: string;
- summary?: string;
- error?: string;
- startedAt?: string;
- endedAt?: string;
- }
- export interface TaskComment {
- id: string;
- author?: string;
- body: string;
- createdAt: string;
- }
- export interface TaskAttachment {
- id: string;
- filename: string;
- size?: number;
- contentType?: string;
- /** 可选下载地址;真实接口未提供时保持空值,避免伪造下载能力。 */
- url?: string;
- }
- export interface Task {
- id: string;
- agentId: string;
- projectId?: string;
- title: string;
- status: TaskStatus;
- assignee?: string;
- owner?: string;
- branchName?: string;
- body?: string;
- blockKind?: string;
- consecutiveFailures?: number;
- /** 已阻塞时长(小时,用于「已阻塞 436h」表达) */
- blockedHours?: number;
- createdAt?: string;
- updatedAt?: string;
- startedAt?: string;
- completedAt?: string;
- runs: TaskRun[];
- comments: TaskComment[];
- attachments: TaskAttachment[];
- }
- // ---------- 工作组(多容器协作) ----------
- /**
- * 工作组 —— 一次任务的临时组合(员工团队)。
- *
- * 与 `Conversation` 的区别:会话是「和谁聊」,工作组是「谁在一起干活」。
- * 数据源是云函数 `fmode-workgroup`(服务端建房),不是 Matrix 直读。
- */
- export interface WorkGroup {
- id: string;
- name: string;
- goal?: string;
- roomId: string;
- /** 成员 agentId 列表 */
- members: string[];
- status?: string;
- }
- // ---------- 登录 ----------
- export interface Me {
- id: string;
- name: string;
- mobile?: string;
- avatar?: string;
- companyId?: string;
- /**
- * 所属组织名(Company.name)。
- *
- * 🔴 此前「我的」页显示硬编码的「企业空间」——是**假文案**。
- * 实测 `Company` 表可拿到 `name`(如 `E4KpGvTEto → 飞码AI`),故改为真实数据。
- * 取不到时为 `undefined`,UI 显示「—」而非编造。
- */
- companyName?: string;
- /** 仅用于决定请求哪个 scope 与界面提示;**不得**作为授权依据(FR-20 AC-20.2) */
- roles?: string[];
- }
- export interface AuthSession {
- token: string;
- me: Me;
- }
- // ---------- Matrix 开通与上传(字段与《API-契约》§2.3/§2.5 对齐) ----------
- /** 对应云函数 RP5Ht55NeX 的返回 */
- export interface MatrixProvision {
- cached?: boolean;
- bindingId?: string;
- status?: string;
- userMxId: string | null;
- agentMxId: string | null;
- homeserver: string;
- accessToken: string;
- deviceId: string;
- roomId: string | null;
- userCreated?: boolean;
- }
- /**
- * 成果文件搬运任务(表 `FmodeArtifactJob`)。
- *
- * 由 **App(前端)** 创建、由 **容器** 领取执行:
- *
- * ```
- * App: 发现成果附件只有容器路径 → 签好 PUT 地址 → 建任务(pending)
- * 容器: 轮询 pending → 读 source → 按 headers 原样 PUT 到 uploadUrl → 回执 done
- * App: 下一次刷新时,成果的 artifacts[i].url 已变成云盘地址
- * ```
- *
- * 🔴 `uploadUrl` 是**限时(5 分钟)单对象写入授权**,不是长期凭据;
- * 过期后由 App 重新签发,容器不需要也不应该保存它。
- */
- export interface ArtifactJob {
- id: string;
- deliverableId: string;
- agentId: string;
- /** 容器内文件路径 */
- source: string;
- filename: string;
- mimeType: string;
- /** 🔴 以下三项由**云函数**签名后写入;客户端只读,不得自行构造 */
- uploadUrl: string;
- publicUrl: string;
- headers: Record<string, string>;
- /** epoch 秒(预签名有效期,默认 5 分钟) */
- expiresAt: number;
- status?: 'pending' | 'done' | 'failed';
- error?: string;
- }
- /** 对应云函数 AlP56LCKFm 的返回(取签名阶段) */
- export interface UploadTicket { uploadUrl: string;
- key: string;
- publicUrl: string;
- headers?: Record<string, string>;
- }
- // ---------- 整体配色(ADR-006 v2.0) ----------
- export type PaletteId = 'platinum' | 'ice' | 'clay' | 'slate';
- export interface PaletteOption {
- id: PaletteId;
- name: string;
- description: string;
- }
- // ---------- 数字生命 / 任务的业务文案与语义色映射(唯一实现) ----------
- export const AGENT_STATUS_TEXT: Record<AgentStatus, string> = {
- running: '正在执行',
- idle: '空闲',
- error: '异常',
- offline: '离线',
- stopped: '已停止',
- };
- /**
- * 任务状态中文文案(对应源库 tasks.status 七态)。
- * 依据《设计令牌.md》v3.0 §5.1 与 statuses 表。
- */
- export const TASK_STATUS_TEXT: Record<TaskStatus, string> = {
- triage: '待分诊',
- todo: '待办',
- scheduled: '已排期',
- ready: '就绪',
- running: '执行中',
- blocked: '已阻塞',
- done: '已完成',
- };
- /** 任务状态筛选顺序(UI 用) */
- export const TASK_STATUS_ORDER: TaskStatus[] = [
- 'triage',
- 'todo',
- 'scheduled',
- 'ready',
- 'running',
- 'blocked',
- 'done',
- ];
- /**
- * 语义色断言(《设计令牌.md》v3.0 §5.1)。
- * 🔴 阻塞 = danger(用户裁决 2026-10-14);运行中 = info(不用品牌色,避免与 accent 撞色)。
- */
- export type SemanticTone = 'success' | 'danger' | 'warning' | 'info' | 'neutral';
- export function agentStatusTone(status: AgentStatus): SemanticTone {
- switch (status) {
- case 'running':
- return 'success';
- case 'idle':
- return 'warning';
- case 'error':
- case 'offline':
- case 'stopped':
- return 'danger';
- default:
- return 'neutral';
- }
- }
- export function taskStatusTone(status: TaskStatus): SemanticTone {
- switch (status) {
- case 'running':
- return 'info';
- case 'blocked':
- return 'danger';
- case 'done':
- return 'success';
- default:
- return 'neutral';
- }
- }
- // ---------- 数字生命「在线可对话」判定(唯一实现) ----------
- /**
- * 数字生命的**可对话在线状态**。
- *
- * ## 🔴 为什么需要这一层(实测教训)
- *
- * 项目里存在**两套语义不同的状态**,此前被混用,导致同一容器在
- * 「通讯录」与「会话列表」显示不一致(用户实测反馈):
- *
- * | 维度 | 字段 | 含义 | 实测问题 |
- * |---|---|---|---|
- * | 运行态 | `Agent.status` | reporter 上报的容器运行状态 | **是历史值**:芯葶 `heartbeatAt` 停在 2026-09-23(近一月前)却仍为 `online` |
- * | 接入态 | `channels.matrixConfig.channelState` / `listenerOk` | 容器 Matrix 通道是否在跑 | 决定**能否真的对话** |
- *
- * 用户能感知的是「**能不能对话**」→ 属**接入态**。
- * 故本判定**以接入态为准**,运行态只作辅助(在工作台/详情展示)。
- *
- * ## 口径
- *
- * ```
- * online 接入态 connected / listenerOk 为真 → 可真对话
- * unreported 有绑定但通道状态未上报 → 能连 Matrix,对方可能不在
- * offline 无绑定(matrixReady 为假) → 不可对话
- * ```
- */
- export type AgentPresence = 'online' | 'unreported' | 'offline';
- /** 判定可对话状态(**全项目唯一实现**,禁止各页另算) */
- export function agentPresence(a: Agent | undefined | null): AgentPresence {
- if (!a || !a.matrixReady) return 'offline';
- const cfg = a.channels?.matrixConfig;
- const state = cfg?.channelState ?? cfg?.channelDetail?.matrixState;
- if (state === 'connected' || cfg?.listenerOk) return 'online';
- // 有绑定但容器未上报通道状态
- return 'unreported';
- }
- /** 可对话状态 → 中文文案(UI 展示,配颜色使用) */
- export const AGENT_PRESENCE_TEXT: Record<AgentPresence, string> = {
- online: '在线',
- unreported: '未上报',
- offline: '未接入',
- };
- /** 可对话状态 → 语义色(复用单轨映射) */
- export function agentPresenceTone(p: AgentPresence): SemanticTone {
- switch (p) {
- case 'online':
- return 'success';
- case 'unreported':
- return 'warning';
- default:
- return 'neutral';
- }
- }
- /**
- * 云端工作区记录(表 `FmodeWorkspace`,云函数 `fmodeagent-chatthreads` 的 wsList)。
- *
- * 2026-11-01 上云:工作区此前只存 localStorage,换设备后文件夹全丢,
- * 而会话的 workspaceId 还在 —— 归属无处安放,只能落进「孤儿」区。
- */
- export interface WorkspaceRecord {
- /** 客户端生成的工作区 id(`ws_<ts>_<rand>`),跨设备稳定 */
- id: string;
- name: string;
- /** 毫秒时间戳;用于稳定排序 */
- createdAt: number;
- }
- /** 工作区 upsert 载荷 */
- export interface WorkspaceUpsert {
- id: string;
- name: string;
- createdAt: number;
- }
- /**
- * 云端会话目录记录(表 `FmodeChatThread`,云函数 `fmodeagent-chatthreads`)。
- *
- * 2026-10-31 引入:会话索引此前只存在本地 localStorage,换浏览器/清缓存即丢失。
- * 云端记录是**唯一真相源**,本地只作缓存。
- *
- * 🔴 归属由服务端按 sessionToken 强制确定,客户端不传也不信任任何 user 字段。
- */
- export interface ChatThreadRecord {
- /** 云端记录 id */
- objectId: string;
- agentId: string;
- roomId: string;
- /** 线程根 event_id(App 内部称 sessionId) */
- threadRoot: string;
- title: string;
- /** 用户手动改过标题 → upsert 不覆盖 */
- titleLocked: boolean;
- messageCount: number;
- /** 毫秒时间戳 */
- lastMessageAt: number;
- archived: boolean;
- lastPreview: string;
- /** 归属工作区 id(`''` / 缺省 = 日常对话);换设备后仍能还原层级 */
- workspaceId?: string; /** 会话形态(2026-11-05):缺省 = dm */
- kind?: 'dm' | 'group';
- /** 群会话成员容器 id 列表 */
- memberIds?: string[];
- /**
- * 本会话关联的成果链接(云盘可访问地址)。
- *
- * 🔴 2026-11-09 用户裁决:成果按会话存放 —— 每条会话在库里有一条记录,
- * 记录里放该会话成果的在线链接数组,查询时读出来即可。
- * 这样不依赖容器上报 threadRoot,「这条对话的成果」由数据本身决定。
- */
- deliverableUrls?: string[];
- }
- /** upsert 提交项(不含服务端生成字段) */
- export interface ChatThreadUpsert {
- agentId: string;
- roomId: string;
- threadRoot: string;
- title?: string;
- messageCount?: number;
- /** 毫秒时间戳 */
- lastMessageAt?: number;
- lastPreview?: string;
- /**
- * 归属的工作区 id(`null` / 缺省 = 日常对话)。
- *
- * 🔴 必须上云(2026-11-01):此前只存本地 localStorage,导致两个问题:
- * ① 每次云端重建列表时归属靠本地缓存「捡」,捡不到就掉回日常对话;
- * ② 换浏览器/清缓存后归属永久丢失,用户以为「工作区里的对话没了」。
- */
- workspaceId?: string | null;
- /** 会话形态(2026-11-05):缺省 = dm,兼容存量 */
- kind?: 'dm' | 'group';
- /** 群会话成员容器 id 列表;单聊为空 */
- memberIds?: string[];
- /**
- * 本会话关联的成果链接(云盘可访问地址)。
- *
- * 🔴 2026-11-09 用户裁决:成果按会话存放 —— 每条会话在库里有一条记录,
- * 记录里放该会话成果的在线链接数组,查询时读出来即可。
- * 这样不依赖容器上报 threadRoot,「这条对话的成果」由数据本身决定。
- */
- deliverableUrls?: string[];
- /** 重新出现时撤销软删 */
- revive?: boolean;
- }
|