mock-api.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. import { Injectable } from '@angular/core';
  2. import { environment } from '../../../environments/environment';
  3. import { FmodeApi } from './fmode-api';
  4. import {
  5. DEMO_AGENTS,
  6. DEMO_DELIVERABLES,
  7. DEMO_ME,
  8. DEMO_TASKS,
  9. } from './demo-data';
  10. import {
  11. Agent,
  12. ArtifactJob,
  13. AuthSession,
  14. ChatThreadRecord,
  15. ChatThreadUpsert,
  16. Deliverable,
  17. DeliverableGroup,
  18. FileEntry,
  19. MatrixProvision,
  20. Me,
  21. SessionFile,
  22. OrgMembership,
  23. OrgUser,
  24. OrgUserPage,
  25. Task,
  26. UploadTicket,
  27. WorkGroup,
  28. WorkspaceRecord,
  29. WorkspaceUpsert,
  30. } from './models';
  31. /**
  32. * 演示数据源实现(`environment.useMockData === true` 时启用)
  33. *
  34. * 行为约定:
  35. * · 模拟网络延迟(`environment.mockLatency`)→ 让加载态真实可见
  36. * · 支持失败注入(`environment.mockFailureRate`)→ 让错误态与重试真实可测
  37. * · **不伪造成功**:任务看板按 SPEC FR-07b 的依赖声明,
  38. * 演示数据来自 data.js(用户授权的演示场景),并已在 UI 上标注「演示数据」
  39. *
  40. * 🔴 本文件仅在演示模式下被实例化;真实模式走 ParseApi(当前为显式抛错的占位)。
  41. */
  42. @Injectable({ providedIn: 'root' })
  43. export class MockApi extends FmodeApi {
  44. // ---------- 鉴权 ----------
  45. async sendMobileCode(mobile: string): Promise<void> {
  46. await this.delay();
  47. if (!/^1\d{10}$/.test(mobile)) {
  48. throw new Error('手机号格式不正确');
  49. }
  50. // 演示:不发送短信
  51. }
  52. async loginWithMobile(mobile: string, code: string): Promise<AuthSession> {
  53. await this.delay();
  54. if (!/^1\d{10}$/.test(mobile)) throw new Error('手机号格式不正确');
  55. if (code === '000000') throw new Error('验证码已过期,请重新获取');
  56. if (code !== '123456') throw new Error('验证码错误,请检查后重试');
  57. return { token: 'demo-fmode', me: DEMO_ME };
  58. }
  59. async loginWithToken(token: string): Promise<AuthSession> {
  60. await this.delay();
  61. if (!token) throw new Error('Token 不能为空');
  62. if (token !== 'demo-fmode') throw new Error('Token 无效或已失效');
  63. return { token, me: DEMO_ME };
  64. }
  65. async fetchMe(token: string): Promise<Me> {
  66. await this.delay();
  67. if (!token) throw new Error('未登录');
  68. return DEMO_ME;
  69. }
  70. // ---------- 数字生命 ----------
  71. async fetchAgents(): Promise<Agent[]> {
  72. await this.delay();
  73. return DEMO_AGENTS.map((a) => ({ ...a }));
  74. }
  75. async fetchAgentStatus(agentId: string): Promise<Agent | null> {
  76. await this.delay();
  77. const found = DEMO_AGENTS.find((a) => a.id === agentId);
  78. return found ? { ...found } : null;
  79. }
  80. /**
  81. * 重命名容器(演示态)。
  82. *
  83. * 演示数据是静态常量,这里只**模拟成功返回**、不改 DEMO_AGENTS ——
  84. * 刷新即恢复原名,与真实环境「写回服务器」的行为明确区分,
  85. * 避免演示时误以为改了真实数据。
  86. */
  87. async renameAgent(agentId: string, displayName: string): Promise<Agent | null> {
  88. await this.delay();
  89. const name = displayName.trim();
  90. if (!name) throw new Error('名称不能为空');
  91. const found = DEMO_AGENTS.find((a) => a.id === agentId);
  92. if (!found) throw new Error('容器不存在');
  93. return { ...found, name };
  94. }
  95. // ---------- Matrix ----------
  96. async provisionMatrix(agentId: string): Promise<MatrixProvision> {
  97. await this.delay();
  98. const agent = DEMO_AGENTS.find((a) => a.id === agentId);
  99. if (!agent || !agent.matrixReady) {
  100. throw new Error('该数字生命尚未接入 FMODE');
  101. }
  102. return {
  103. cached: true,
  104. bindingId: `demo-binding-${agentId}`,
  105. status: 'ok',
  106. userMxId: `@demo-user:fmode.cn`,
  107. agentMxId: `@${agentId}:fmode.cn`,
  108. homeserver: 'https://matrix.fmode.cn',
  109. accessToken: 'demo-access-token',
  110. deviceId: 'DEMODEVICE',
  111. roomId: `!demo-${agentId}:fmode.cn`,
  112. };
  113. }
  114. async validateMatrix(agentId: string): Promise<MatrixProvision> {
  115. return this.provisionMatrix(agentId);
  116. }
  117. // ---------- 交付成果 ----------
  118. async fetchDeliverables(opts?: { agentId?: string; limit?: number }): Promise<Deliverable[]> {
  119. await this.delay();
  120. let items = DEMO_DELIVERABLES.map((d) => ({ ...d }));
  121. if (opts?.agentId) items = items.filter((d) => d.agentId === opts.agentId);
  122. items.sort((a, b) => b.deliveredAt.localeCompare(a.deliveredAt));
  123. if (opts?.limit) items = items.slice(0, opts.limit);
  124. return items;
  125. }
  126. async fetchDeliverablesByProject(limit?: number): Promise<DeliverableGroup[]> {
  127. const all = await this.fetchDeliverables();
  128. const map = new Map<string, DeliverableGroup>();
  129. for (const d of all) {
  130. const key = d.projectId ?? '__none__';
  131. if (!map.has(key)) {
  132. map.set(key, { project: d.projectId, projectName: d.projectName ?? '未归类', count: 0, items: [] });
  133. }
  134. const g = map.get(key)!;
  135. g.items.push(d);
  136. g.count += 1;
  137. }
  138. let groups = [...map.values()];
  139. groups.sort((a, b) => b.items[0].deliveredAt.localeCompare(a.items[0].deliveredAt));
  140. if (limit) groups = groups.slice(0, limit);
  141. return groups;
  142. }
  143. // ---------- 文件台账(演示态:内存存储,行为与云端一致) ----------
  144. /** 演示态文件台账:会话 id → 文件列表 */
  145. private readonly mockFiles = new Map<string, SessionFile[]>();
  146. private mockFileSeq = 1;
  147. async registerFile(entry: FileEntry): Promise<{ id: string; duplicated?: boolean }> {
  148. const list = this.mockFiles.get(entry.threadRoot) ?? [];
  149. const hit = list.find((f) => f.url === entry.url);
  150. if (hit) return { id: hit.id, duplicated: true };
  151. const id = `mock-file-${this.mockFileSeq++}`;
  152. list.unshift({
  153. id,
  154. threadRoot: entry.threadRoot,
  155. agentId: entry.agentId ?? '',
  156. roomId: entry.roomId ?? '',
  157. name: entry.name,
  158. size: entry.size ?? -1,
  159. mimeType: entry.mimeType ?? '',
  160. url: entry.url,
  161. storageKey: entry.key ?? '',
  162. uploadedBy: entry.uploadedBy,
  163. status: 'delivered',
  164. deliveredAt: Date.now(),
  165. });
  166. this.mockFiles.set(entry.threadRoot, list);
  167. return { id };
  168. }
  169. async fetchFilesBySession(threadRoot: string, limit = 100): Promise<SessionFile[]> {
  170. return (this.mockFiles.get(threadRoot) ?? []).slice(0, limit);
  171. }
  172. /** 演示态按群拉(2026-11-11):与 threadRoot 同口径 */
  173. async fetchFilesByGroup(groupId: string, limit = 200): Promise<SessionFile[]> {
  174. return (this.mockFiles.get(groupId) ?? []).slice(0, limit);
  175. }
  176. /** 演示态不需要补归并(没有 DM thread 与群的两套归属) */
  177. async claimFilesToGroup(): Promise<{ claimed: number }> {
  178. return { claimed: 0 };
  179. }
  180. /**
  181. * 演示态读文件正文(2026-11-11)。
  182. *
  183. * 演示环境的文件是本地 blob,本来就能直读;这里保持接口一致,
  184. * 避免预览组件在演示态下走「服务未部署」的降级路径。
  185. */
  186. async readText(url: string): Promise<string> {
  187. const res = await fetch(url);
  188. if (!res.ok) throw new Error(`HTTP ${res.status}`);
  189. return res.text();
  190. }
  191. // ---------- 会话目录(演示态:内存存储,行为与云端一致) ----------
  192. /**
  193. * 演示态的会话目录。
  194. *
  195. * 2026-11-02:改为 **localStorage 持久化**,模拟「云端目录」。
  196. * · 原实现是进程内存,整页刷新即清空 —— 用户建了会话一刷新就消失,
  197. * 与线上(云端持久)行为不一致,本地调试会被误判成缺陷。
  198. * · 键名带 `mock/` 前缀,与业务缓存(`fmode_*`)区分,清理时不会误删。
  199. */
  200. private static readonly THREADS_KEY = 'mock/chat-threads';
  201. private _threads: Map<string, ChatThreadRecord> | null = null;
  202. private get mockThreads(): Map<string, ChatThreadRecord> {
  203. if (this._threads) return this._threads;
  204. this._threads = new Map();
  205. try {
  206. const raw = localStorage.getItem(MockApi.THREADS_KEY);
  207. const arr = raw ? (JSON.parse(raw) as ChatThreadRecord[]) : [];
  208. for (const t of arr) this._threads.set(`${t.roomId}|${t.threadRoot}`, t);
  209. } catch {
  210. /* 坏数据 / 隐私模式:从空目录开始 */
  211. }
  212. return this._threads;
  213. }
  214. private persistThreads(): void {
  215. try {
  216. localStorage.setItem(MockApi.THREADS_KEY, JSON.stringify([...this.mockThreads.values()]));
  217. } catch {
  218. /* 隐私模式:退化为内存态 */
  219. }
  220. }
  221. async fetchChatThreads(agentId: string, _opts?: { includeDeleted?: boolean }): Promise<ChatThreadRecord[]> {
  222. await this.delay();
  223. return [...this.mockThreads.values()]
  224. .filter((t) => t.agentId === agentId)
  225. .sort((a, b) => b.lastMessageAt - a.lastMessageAt)
  226. .map((t) => ({ ...t }));
  227. }
  228. async upsertChatThreads(items: ChatThreadUpsert[]): Promise<void> {
  229. await this.delay();
  230. for (const it of items) {
  231. const key = `${it.roomId}|${it.threadRoot}`;
  232. const cur = this.mockThreads.get(key);
  233. this.mockThreads.set(key, {
  234. objectId: cur?.objectId ?? `mock_${key}`,
  235. agentId: it.agentId,
  236. roomId: it.roomId,
  237. threadRoot: it.threadRoot,
  238. title: cur?.titleLocked ? cur.title : it.title || cur?.title || '',
  239. titleLocked: cur?.titleLocked ?? false,
  240. messageCount: it.messageCount ?? cur?.messageCount ?? 0,
  241. lastMessageAt: it.lastMessageAt ?? cur?.lastMessageAt ?? Date.now(),
  242. archived: cur?.archived ?? false,
  243. lastPreview: it.lastPreview ?? cur?.lastPreview ?? '',
  244. });
  245. }
  246. this.persistThreads();
  247. }
  248. async patchChatThread(input: {
  249. threadRoot: string;
  250. title?: string;
  251. archived?: boolean;
  252. workspaceId?: string;
  253. }): Promise<void> {
  254. await this.delay();
  255. for (const [key, t] of this.mockThreads) {
  256. if (t.threadRoot !== input.threadRoot) continue;
  257. this.mockThreads.set(key, {
  258. ...t,
  259. title: input.title ?? t.title,
  260. titleLocked: input.title != null ? true : t.titleLocked,
  261. archived: input.archived ?? t.archived,
  262. workspaceId: input.workspaceId ?? t.workspaceId,
  263. });
  264. }
  265. this.persistThreads();
  266. }
  267. async removeChatThread(input: { threadRoot: string; roomId?: string; agentId?: string }): Promise<void> {
  268. await this.delay();
  269. for (const [key, t] of this.mockThreads) {
  270. if (t.threadRoot === input.threadRoot) this.mockThreads.delete(key);
  271. }
  272. this.persistThreads();
  273. }
  274. // ---------- 工作区(2026-11-01 上云) ----------
  275. private readonly mockWorkspaces = new Map<string, WorkspaceRecord>();
  276. async fetchWorkspaces(): Promise<WorkspaceRecord[]> {
  277. await this.delay();
  278. return [...this.mockWorkspaces.values()].sort((a, b) => a.createdAt - b.createdAt);
  279. }
  280. async upsertWorkspaces(items: WorkspaceUpsert[]): Promise<void> {
  281. await this.delay();
  282. for (const it of items) this.mockWorkspaces.set(it.id, { ...it });
  283. }
  284. async removeWorkspace(id: string): Promise<void> {
  285. await this.delay();
  286. this.mockWorkspaces.delete(id);
  287. }
  288. // ---------- 任务看板(FR-07b)----------
  289. async fetchTasks(agentId: string): Promise<Task[]> {
  290. await this.delay();
  291. return DEMO_TASKS.filter((t) => t.agentId === agentId)
  292. .map((t) => ({ ...t }))
  293. .sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''));
  294. }
  295. async fetchTaskDetail(taskId: string): Promise<Task | null> {
  296. await this.delay();
  297. const found = DEMO_TASKS.find((t) => t.id === taskId);
  298. return found ? { ...found } : null;
  299. }
  300. // ---------- 上传 ----------
  301. async requestUploadUrl(filename: string, mimeType: string, size: number): Promise<UploadTicket> {
  302. await this.delay();
  303. if (!filename) throw new Error('缺少文件名');
  304. const key = `demo/${Date.now()}-${filename}`;
  305. return {
  306. uploadUrl: `#demo-upload`,
  307. key,
  308. publicUrl: `https://s3.fmode.cn/${key}`,
  309. headers: { 'Content-Type': mimeType, 'x-obs-acl': 'public-read' },
  310. };
  311. }
  312. async uploadFile(
  313. file: File,
  314. onProgress?: (pct: number) => void,
  315. ): Promise<{ key: string; publicUrl: string }> {
  316. const ticket = await this.requestUploadUrl(file.name, file.type, file.size);
  317. // 演示:不真正上传,仅模拟进度(README 载明)
  318. for (let pct = 0; pct <= 100; pct += 20) {
  319. onProgress?.(pct);
  320. await new Promise((r) => setTimeout(r, 120));
  321. }
  322. return { key: ticket.key, publicUrl: ticket.publicUrl };
  323. }
  324. async enqueueArtifactJob(_job: {
  325. deliverableId: string;
  326. agentId: string;
  327. source: string;
  328. filename: string;
  329. index: number;
  330. }): Promise<{ id: string }> {
  331. return { id: `mock-job-${Date.now()}` };
  332. }
  333. async fetchArtifactJobs(_agentId: string): Promise<ArtifactJob[]> {
  334. return [];
  335. }
  336. async ackArtifactJob(_input: {
  337. id: string;
  338. ok: boolean;
  339. publicUrl?: string;
  340. error?: string;
  341. }): Promise<void> {
  342. // 演示态无副作用
  343. }
  344. // ---------- 工作组(演示态内存实现) ----------
  345. private readonly mockGroups: WorkGroup[] = [];
  346. async createWorkGroup(input: {
  347. name: string;
  348. goal?: string;
  349. memberAgentIds: string[];
  350. }): Promise<{ id: string; roomId: string }> {
  351. const id = `wg-${Date.now()}`;
  352. const roomId = `!demo-${id}:fmode.cn`;
  353. this.mockGroups.push({
  354. id,
  355. name: input.name,
  356. goal: input.goal,
  357. roomId,
  358. members: input.memberAgentIds,
  359. status: 'active',
  360. });
  361. return { id, roomId };
  362. }
  363. async listWorkGroups(): Promise<WorkGroup[]> {
  364. return [...this.mockGroups];
  365. }
  366. async inviteToWorkGroup(groupId: string, agentIds: string[]): Promise<void> {
  367. const g = this.mockGroups.find((x) => x.id === groupId);
  368. if (g) g.members = [...g.members, ...agentIds];
  369. }
  370. async removeFromWorkGroup(groupId: string, agentId: string): Promise<void> {
  371. const g = this.mockGroups.find((x) => x.id === groupId);
  372. if (g) g.members = g.members.filter((m) => m !== agentId);
  373. }
  374. async archiveWorkGroup(groupId: string): Promise<void> {
  375. const g = this.mockGroups.find((x) => x.id === groupId);
  376. if (g) g.status = 'archived';
  377. }
  378. // ---------- 组织成员管理(2026-11-10,演示态内存实现) ----------
  379. /** 演示数据:有组织成员管理的入口才看得到真实交互 */
  380. private readonly mockOrgUsers: OrgUser[] = [
  381. { id: 'demo-user-01', username: 'zhangsan', nickname: '张三', mobile: '13800000001', isDeleted: false, createdAt: Date.now() - 86400_000 * 30 },
  382. { id: 'demo-user-02', username: 'lisi', nickname: '李四', mobile: '13800000002', isDeleted: false, createdAt: Date.now() - 86400_000 * 20 },
  383. { id: 'demo-user-03', username: 'wangwu', nickname: '王五', mobile: '13800000003', isDeleted: false, createdAt: Date.now() - 86400_000 * 10 },
  384. ];
  385. async listOrgUsers(query: {
  386. keyword?: string;
  387. page?: number;
  388. pageSize?: number;
  389. }): Promise<OrgUserPage> {
  390. const keyword = (query.keyword ?? '').trim().toLowerCase();
  391. const page = Math.max(1, query.page ?? 1);
  392. const pageSize = Math.max(1, query.pageSize ?? 20);
  393. const all = this.mockOrgUsers.filter(
  394. (u) =>
  395. !u.isDeleted &&
  396. (!keyword ||
  397. u.nickname.toLowerCase().includes(keyword) ||
  398. u.mobile.includes(keyword) ||
  399. u.id.toLowerCase().includes(keyword)),
  400. );
  401. const start = (page - 1) * pageSize;
  402. return { items: all.slice(start, start + pageSize), total: all.length, page, pageSize };
  403. }
  404. async addOrgUser(userId: string): Promise<OrgUser> {
  405. const existed = this.mockOrgUsers.find((u) => u.id === userId);
  406. if (existed) {
  407. existed.isDeleted = false;
  408. return existed;
  409. }
  410. const created: OrgUser = {
  411. id: userId,
  412. username: userId,
  413. nickname: '新成员',
  414. mobile: '',
  415. isDeleted: false,
  416. createdAt: Date.now(),
  417. };
  418. this.mockOrgUsers.push(created);
  419. return created;
  420. }
  421. async updateOrgUser(
  422. userId: string,
  423. patch: { nickname?: string; mobile?: string; password?: string },
  424. ): Promise<OrgUser> {
  425. const u = this.mockOrgUsers.find((x) => x.id === userId);
  426. if (!u) throw new Error('用户不存在');
  427. if (patch.nickname != null) u.nickname = patch.nickname;
  428. if (patch.mobile != null) u.mobile = patch.mobile;
  429. return { ...u };
  430. }
  431. async removeOrgUser(userId: string): Promise<void> {
  432. const u = this.mockOrgUsers.find((x) => x.id === userId);
  433. if (u) u.isDeleted = true;
  434. }
  435. /** 演示态:当前用户视为已加入组织 */
  436. async fetchOrgMembership(): Promise<OrgMembership> {
  437. return { userId: 'demo-me', hasOrg: true, companyId: 'demo-company', companyName: '演示组织' };
  438. }
  439. // ---------- 内部 ----------
  440. private delay(): Promise<void> {
  441. this.maybeFail();
  442. const ms = environment.mockLatency;
  443. return ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve();
  444. }
  445. private maybeFail(): void {
  446. const rate = environment.mockFailureRate;
  447. if (rate > 0 && Math.random() < rate) {
  448. throw new Error('演示模式:模拟网络请求失败');
  449. }
  450. }
  451. }