fmode-api.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import {
  2. Agent,
  3. ArtifactJob,
  4. AuthSession,
  5. ChatThreadRecord,
  6. ChatThreadUpsert,
  7. Deliverable,
  8. DeliverableGroup,
  9. FileEntry,
  10. MatrixProvision,
  11. Me,
  12. SessionFile,
  13. OrgUser,
  14. OrgMembership,
  15. OrgUserPage,
  16. Task,
  17. UploadTicket,
  18. WorkGroup,
  19. WorkspaceRecord,
  20. WorkspaceUpsert,
  21. } from './models';
  22. /**
  23. * 数据访问抽象接口 —— 应用层与数据源之间的**唯一闸门**
  24. *
  25. * 依据《数据访问层设计.md》§一/§四。硬约束:
  26. * ① 页面/components **禁止** inject(FmodeApi) 或其实现(FR-14 AC-14.2)
  27. * ② 业务服务**禁止**直接 fetch / 直接用 fmode-ng(黄金规则 5)
  28. * ③ 每个数据域**只有一个** owner 服务
  29. *
  30. * 实现:`ParseApi`(真实,走 fmode-ng)/ `MockApi`(演示数据)。
  31. * 切换由 `environment.useMockData` 单开关控制(api.provider.ts)。
  32. *
  33. * ⚠️ `Project` / `EventItem` **不在此接口**:它们是派生投影(由 Agent + Deliverable 组合),
  34. * 由对应服务计算,见《数据访问层设计.md》§四.1。
  35. */
  36. export abstract class FmodeApi {
  37. // ---------- 鉴权 ----------
  38. /** 发送手机验证码(REST R4:`code === 1` 才算成功) */
  39. abstract sendMobileCode(mobile: string): Promise<void>;
  40. /** 验证码登录(REST R5:`code === 200 && data.token` 才算成功) */
  41. abstract loginWithMobile(mobile: string, code: string): Promise<AuthSession>;
  42. /** Token 直登(REST R1:`POST /parse/users/me`) */
  43. abstract loginWithToken(token: string): Promise<AuthSession>;
  44. /** 用已有 token 取回身份 */
  45. abstract fetchMe(token: string): Promise<Me>;
  46. // ---------- 数字生命(实况) ----------
  47. /** 列表查询(云函数 xlosi3m46q,`op:'query'`) */
  48. abstract fetchAgents(): Promise<Agent[]>;
  49. /** 单条查询;`code` 404/403 时返回 null */
  50. abstract fetchAgentStatus(agentId: string): Promise<Agent | null>;
  51. /**
  52. * 重命名容器(**仅公司管理员 / 系统超管**)。
  53. *
  54. * ## 契约(2026-11-04 与后端约定,待实现)
  55. *
  56. * `POST /api/functions`,沿用 `xlosi3m46q` 的写入形态:
  57. * ```
  58. * { id: 'xlosi3m46q', token, params: { op: 'rename', agentId, displayName } }
  59. * → { code: 200, agent?: AgentStatusItem } // 成功
  60. * → { code: 403, msg: '...' } // 无权限
  61. * ```
  62. *
  63. * ## 为什么单独成方法而不是复用 update
  64. *
  65. * 重命名是**跨用户的可见变更**(同公司成员都会看到新名字),
  66. * 与「改自己的偏好」不是一回事 —— 独立方法便于单独做权限校验与审计。
  67. *
  68. * 🔴 服务端未就绪时实现方**必须抛错**,禁止静默改本地(那会制造
  69. * 「我这看着改了、别人看到的还是旧名」的假象)。
  70. */
  71. abstract renameAgent(agentId: string, displayName: string): Promise<Agent | null>;
  72. // ---------- 会话(Matrix) ----------
  73. /** 开通 Matrix 身份(云函数 RP5Ht55NeX);绑定表命中时 `cached: true` */
  74. abstract provisionMatrix(agentId: string): Promise<MatrixProvision>;
  75. /** 校验/刷新凭据(`validate: 1`) */
  76. abstract validateMatrix(agentId: string): Promise<MatrixProvision>;
  77. // ---------- 会话目录(云端持久化) ----------
  78. /**
  79. * 拉取当前用户的会话目录(云函数 `fmodeagent-chatthreads`,`op:'list'`)。
  80. *
  81. * 🔴 云端是会话目录的**唯一真相源**;本地 localStorage 只作渲染缓存。
  82. * 服务端按 sessionToken 强制归属当前用户,本方法不传 user 参数。
  83. */
  84. abstract fetchChatThreads(agentId: string, opts?: { includeDeleted?: boolean }): Promise<ChatThreadRecord[]>;
  85. /** 批量上报会话(新建或补计数/时间;标题仅在为空时写入) */
  86. abstract upsertChatThreads(items: ChatThreadUpsert[]): Promise<void>;
  87. /** 修改标题(置 titleLocked)或归档状态 */
  88. abstract patchChatThread(input: {
  89. threadRoot: string;
  90. title?: string;
  91. archived?: boolean;
  92. /** 归属工作区;`''` = 移回日常对话(2026-11-01 起上云) */
  93. workspaceId?: string;
  94. }): Promise<void>;
  95. /** 软删会话(isDeleted=true;再次收到消息可 revive) */
  96. /** 删除会话索引(云端物理删除 + 留墓碑阻止重建);Matrix 消息不受影响 */
  97. abstract removeChatThread(input: { threadRoot: string; roomId?: string; agentId?: string }): Promise<void>;
  98. // ---------- 工作区(2026-11-01 上云) ----------
  99. /**
  100. * 拉取用户的工作区列表。
  101. *
  102. * 工作区是用户自己整理对话的「文件夹」,此前只存 localStorage ——
  103. * 换设备/清缓存后文件夹全丢,会话归属还在但无处安放。
  104. */
  105. abstract fetchWorkspaces(): Promise<WorkspaceRecord[]>;
  106. /** 新建 / 更新工作区(按 id 幂等) */
  107. abstract upsertWorkspaces(items: WorkspaceUpsert[]): Promise<void>;
  108. /** 删除工作区(软删) */
  109. abstract removeWorkspace(id: string): Promise<void>;
  110. // ---------- 交付成果 ----------
  111. /** 列表(云函数 lfYlgU7SkK,`action:'list'`) */
  112. abstract fetchDeliverables(opts?: { agentId?: string; limit?: number }): Promise<Deliverable[]>;
  113. /** 按项目分组(`action:'byProject'`) */
  114. abstract fetchDeliverablesByProject(limit?: number): Promise<DeliverableGroup[]>;
  115. // ---------- 文件台账(云函数 fmode-file-hub,2026-11-10) ----------
  116. /**
  117. * 登记一条文件记录(用户上传 / 容器产出共用)。
  118. *
  119. * 幂等:同一 (用户, 会话, URL) 重复登记返回已有 id,不产生重复。
  120. */
  121. abstract registerFile(entry: FileEntry): Promise<{ id: string; duplicated?: boolean }>;
  122. /**
  123. * 按会话拉文件(右侧栏主路径)。
  124. *
  125. * 用户要求:每次直接从数据库加载,不依赖加载聊天历史。
  126. * 历史数据没有 threadRoot,查不到属预期(用户裁决:只保证新数据)。
  127. */
  128. abstract fetchFilesBySession(threadRoot: string, limit?: number): Promise<SessionFile[]>;
  129. // ---------- 任务看板(FR-07b) ----------
  130. /**
  131. * 任务列表(按 agentId 过滤)。
  132. * 🔴 云函数接收端**尚未实现**:就绪前实现方须返回**空数组**,**禁止伪造数据**
  133. * (SPEC FR-07b「依赖声明」)。
  134. */
  135. abstract fetchTasks(agentId: string): Promise<Task[]>;
  136. /** 任务详情(含 runs / comments / attachments) */
  137. abstract fetchTaskDetail(taskId: string): Promise<Task | null>;
  138. // ---------- 上传 ----------
  139. /** 取预签名(云函数 AlP56LCKFm);缺 uploadUrl 即抛错,不得静默降级 */
  140. abstract requestUploadUrl(filename: string, mimeType: string, size: number): Promise<UploadTicket>;
  141. /** 预签名直传(前端不经手任何 OBS 密钥) */
  142. abstract uploadFile(
  143. file: File,
  144. onProgress?: (pct: number) => void,
  145. ): Promise<{ key: string; publicUrl: string }>;
  146. // ---------- 成果文件入库(云盘同步) ----------
  147. // ---------- 成果文件搬运队列(容器侧轮询) ----------
  148. /**
  149. * 登记一次搬运:把容器里的 `source` 文件搬到云盘。
  150. *
  151. * 🔴 **只报「传哪个」,不报「传到哪」** —— 上传地址与必需的头由云函数
  152. * 用服务端密钥签好后写进队列,客户端不接触任何密钥
  153. * (2026-10-21 定稿;此前的「App 自签」方案已废弃)。
  154. *
  155. * @param index 附件在成果 `artifacts[]` 中的下标 —— 保证同名附件不互相覆盖
  156. */
  157. abstract enqueueArtifactJob(job: {
  158. deliverableId: string;
  159. agentId: string;
  160. /** 容器内的原始路径 */
  161. source: string;
  162. filename: string;
  163. index: number;
  164. }): Promise<{ id: string }>;
  165. /** 容器领取待办(按 agentId,只取未完成且未过期的) */
  166. abstract fetchArtifactJobs(agentId: string): Promise<ArtifactJob[]>;
  167. /** 容器回执:上传成功(带公网地址)/ 失败(带原因) */
  168. abstract ackArtifactJob(input: {
  169. id: string;
  170. ok: boolean;
  171. publicUrl?: string;
  172. error?: string;
  173. }): Promise<void>;
  174. // ---------- 工作组(多容器协作) ----------
  175. /** 创建工作组(服务端建房 + 邀请 + 写记录) */
  176. abstract createWorkGroup(input: {
  177. name: string;
  178. goal?: string;
  179. memberAgentIds: string[];
  180. }): Promise<{ id: string; roomId: string }>;
  181. /** 我参与的工作组 */
  182. abstract listWorkGroups(): Promise<WorkGroup[]>;
  183. /** 邀请成员 */
  184. abstract inviteToWorkGroup(groupId: string, agentIds: string[]): Promise<void>;
  185. /** 移出成员 */
  186. abstract removeFromWorkGroup(groupId: string, agentId: string): Promise<void>;
  187. /** 归档(不删房间) */
  188. abstract archiveWorkGroup(groupId: string): Promise<void>;
  189. // ---------- 组织成员管理(2026-11-10,仅管理员) ----------
  190. /**
  191. * 分页查询本组织成员。
  192. *
  193. * 🔴 **鉴权在服务端**:前端只是入口可见性,接口内部还会再校验一次管理员身份,
  194. * 并强制把作用域锁在请求者自己的组织。
  195. */
  196. abstract listOrgUsers(query: {
  197. keyword?: string;
  198. page?: number;
  199. pageSize?: number;
  200. }): Promise<OrgUserPage>;
  201. /** 把用户拉进当前组织 */
  202. abstract addOrgUser(userId: string): Promise<OrgUser>;
  203. /** 改资料 / 重置密码(`password` 为空表示不改) */
  204. abstract updateOrgUser(
  205. userId: string,
  206. patch: { nickname?: string; mobile?: string; password?: string },
  207. ): Promise<OrgUser>;
  208. /** 软删并移出组织 */
  209. abstract removeOrgUser(userId: string): Promise<void>;
  210. /**
  211. * 当前账号的组织归属状态(2026-11-10)。
  212. *
  213. * 🔴 不是「有没有 companyId」—— 新用户注册时系统会**自动建一个个人试用组织**,
  214. * 那种情况应视为**无组织**。判定在服务端做(见 `fmodeagent-users.js` 的 `opMe`)。
  215. */
  216. abstract fetchOrgMembership(): Promise<OrgMembership>;
  217. }