| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- import { Injectable } from '@angular/core';
- import { environment } from '../../../environments/environment';
- import { FmodeApi } from './fmode-api';
- import {
- DEMO_AGENTS,
- DEMO_DELIVERABLES,
- DEMO_ME,
- DEMO_TASKS,
- } from './demo-data';
- import {
- Agent,
- ArtifactJob,
- AuthSession,
- ChatThreadRecord,
- ChatThreadUpsert,
- Deliverable,
- DeliverableGroup,
- FileEntry,
- MatrixProvision,
- Me,
- SessionFile,
- OrgMembership,
- OrgUser,
- OrgUserPage,
- Task,
- UploadTicket,
- WorkGroup,
- WorkspaceRecord,
- WorkspaceUpsert,
- } from './models';
- /**
- * 演示数据源实现(`environment.useMockData === true` 时启用)
- *
- * 行为约定:
- * · 模拟网络延迟(`environment.mockLatency`)→ 让加载态真实可见
- * · 支持失败注入(`environment.mockFailureRate`)→ 让错误态与重试真实可测
- * · **不伪造成功**:任务看板按 SPEC FR-07b 的依赖声明,
- * 演示数据来自 data.js(用户授权的演示场景),并已在 UI 上标注「演示数据」
- *
- * 🔴 本文件仅在演示模式下被实例化;真实模式走 ParseApi(当前为显式抛错的占位)。
- */
- @Injectable({ providedIn: 'root' })
- export class MockApi extends FmodeApi {
- // ---------- 鉴权 ----------
- async sendMobileCode(mobile: string): Promise<void> {
- await this.delay();
- if (!/^1\d{10}$/.test(mobile)) {
- throw new Error('手机号格式不正确');
- }
- // 演示:不发送短信
- }
- async loginWithMobile(mobile: string, code: string): Promise<AuthSession> {
- await this.delay();
- if (!/^1\d{10}$/.test(mobile)) throw new Error('手机号格式不正确');
- if (code === '000000') throw new Error('验证码已过期,请重新获取');
- if (code !== '123456') throw new Error('验证码错误,请检查后重试');
- return { token: 'demo-fmode', me: DEMO_ME };
- }
- async loginWithToken(token: string): Promise<AuthSession> {
- await this.delay();
- if (!token) throw new Error('Token 不能为空');
- if (token !== 'demo-fmode') throw new Error('Token 无效或已失效');
- return { token, me: DEMO_ME };
- }
- async fetchMe(token: string): Promise<Me> {
- await this.delay();
- if (!token) throw new Error('未登录');
- return DEMO_ME;
- }
- // ---------- 数字生命 ----------
- async fetchAgents(): Promise<Agent[]> {
- await this.delay();
- return DEMO_AGENTS.map((a) => ({ ...a }));
- }
- async fetchAgentStatus(agentId: string): Promise<Agent | null> {
- await this.delay();
- const found = DEMO_AGENTS.find((a) => a.id === agentId);
- return found ? { ...found } : null;
- }
- /**
- * 重命名容器(演示态)。
- *
- * 演示数据是静态常量,这里只**模拟成功返回**、不改 DEMO_AGENTS ——
- * 刷新即恢复原名,与真实环境「写回服务器」的行为明确区分,
- * 避免演示时误以为改了真实数据。
- */
- async renameAgent(agentId: string, displayName: string): Promise<Agent | null> {
- await this.delay();
- const name = displayName.trim();
- if (!name) throw new Error('名称不能为空');
- const found = DEMO_AGENTS.find((a) => a.id === agentId);
- if (!found) throw new Error('容器不存在');
- return { ...found, name };
- }
- // ---------- Matrix ----------
- async provisionMatrix(agentId: string): Promise<MatrixProvision> {
- await this.delay();
- const agent = DEMO_AGENTS.find((a) => a.id === agentId);
- if (!agent || !agent.matrixReady) {
- throw new Error('该数字生命尚未接入 FMODE');
- }
- return {
- cached: true,
- bindingId: `demo-binding-${agentId}`,
- status: 'ok',
- userMxId: `@demo-user:fmode.cn`,
- agentMxId: `@${agentId}:fmode.cn`,
- homeserver: 'https://matrix.fmode.cn',
- accessToken: 'demo-access-token',
- deviceId: 'DEMODEVICE',
- roomId: `!demo-${agentId}:fmode.cn`,
- };
- }
- async validateMatrix(agentId: string): Promise<MatrixProvision> {
- return this.provisionMatrix(agentId);
- }
- // ---------- 交付成果 ----------
- async fetchDeliverables(opts?: { agentId?: string; limit?: number }): Promise<Deliverable[]> {
- await this.delay();
- let items = DEMO_DELIVERABLES.map((d) => ({ ...d }));
- if (opts?.agentId) items = items.filter((d) => d.agentId === opts.agentId);
- items.sort((a, b) => b.deliveredAt.localeCompare(a.deliveredAt));
- if (opts?.limit) items = items.slice(0, opts.limit);
- return items;
- }
- async fetchDeliverablesByProject(limit?: number): Promise<DeliverableGroup[]> {
- const all = await this.fetchDeliverables();
- const map = new Map<string, DeliverableGroup>();
- for (const d of all) {
- const key = d.projectId ?? '__none__';
- if (!map.has(key)) {
- map.set(key, { project: d.projectId, projectName: d.projectName ?? '未归类', count: 0, items: [] });
- }
- const g = map.get(key)!;
- g.items.push(d);
- g.count += 1;
- }
- let groups = [...map.values()];
- groups.sort((a, b) => b.items[0].deliveredAt.localeCompare(a.items[0].deliveredAt));
- if (limit) groups = groups.slice(0, limit);
- return groups;
- }
- // ---------- 文件台账(演示态:内存存储,行为与云端一致) ----------
- /** 演示态文件台账:会话 id → 文件列表 */
- private readonly mockFiles = new Map<string, SessionFile[]>();
- private mockFileSeq = 1;
- async registerFile(entry: FileEntry): Promise<{ id: string; duplicated?: boolean }> {
- const list = this.mockFiles.get(entry.threadRoot) ?? [];
- const hit = list.find((f) => f.url === entry.url);
- if (hit) return { id: hit.id, duplicated: true };
- const id = `mock-file-${this.mockFileSeq++}`;
- list.unshift({
- id,
- threadRoot: entry.threadRoot,
- agentId: entry.agentId ?? '',
- roomId: entry.roomId ?? '',
- name: entry.name,
- size: entry.size ?? -1,
- mimeType: entry.mimeType ?? '',
- url: entry.url,
- storageKey: entry.key ?? '',
- uploadedBy: entry.uploadedBy,
- status: 'delivered',
- deliveredAt: Date.now(),
- });
- this.mockFiles.set(entry.threadRoot, list);
- return { id };
- }
- async fetchFilesBySession(threadRoot: string, limit = 100): Promise<SessionFile[]> {
- return (this.mockFiles.get(threadRoot) ?? []).slice(0, limit);
- }
- /** 演示态按群拉(2026-11-11):与 threadRoot 同口径 */
- async fetchFilesByGroup(groupId: string, limit = 200): Promise<SessionFile[]> {
- return (this.mockFiles.get(groupId) ?? []).slice(0, limit);
- }
- /** 演示态不需要补归并(没有 DM thread 与群的两套归属) */
- async claimFilesToGroup(): Promise<{ claimed: number }> {
- return { claimed: 0 };
- }
- /**
- * 演示态读文件正文(2026-11-11)。
- *
- * 演示环境的文件是本地 blob,本来就能直读;这里保持接口一致,
- * 避免预览组件在演示态下走「服务未部署」的降级路径。
- */
- async readText(url: string): Promise<string> {
- const res = await fetch(url);
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
- return res.text();
- }
- // ---------- 会话目录(演示态:内存存储,行为与云端一致) ----------
- /**
- * 演示态的会话目录。
- *
- * 2026-11-02:改为 **localStorage 持久化**,模拟「云端目录」。
- * · 原实现是进程内存,整页刷新即清空 —— 用户建了会话一刷新就消失,
- * 与线上(云端持久)行为不一致,本地调试会被误判成缺陷。
- * · 键名带 `mock/` 前缀,与业务缓存(`fmode_*`)区分,清理时不会误删。
- */
- private static readonly THREADS_KEY = 'mock/chat-threads';
- private _threads: Map<string, ChatThreadRecord> | null = null;
- private get mockThreads(): Map<string, ChatThreadRecord> {
- if (this._threads) return this._threads;
- this._threads = new Map();
- try {
- const raw = localStorage.getItem(MockApi.THREADS_KEY);
- const arr = raw ? (JSON.parse(raw) as ChatThreadRecord[]) : [];
- for (const t of arr) this._threads.set(`${t.roomId}|${t.threadRoot}`, t);
- } catch {
- /* 坏数据 / 隐私模式:从空目录开始 */
- }
- return this._threads;
- }
- private persistThreads(): void {
- try {
- localStorage.setItem(MockApi.THREADS_KEY, JSON.stringify([...this.mockThreads.values()]));
- } catch {
- /* 隐私模式:退化为内存态 */
- }
- }
- async fetchChatThreads(agentId: string, _opts?: { includeDeleted?: boolean }): Promise<ChatThreadRecord[]> {
- await this.delay();
- return [...this.mockThreads.values()]
- .filter((t) => t.agentId === agentId)
- .sort((a, b) => b.lastMessageAt - a.lastMessageAt)
- .map((t) => ({ ...t }));
- }
- async upsertChatThreads(items: ChatThreadUpsert[]): Promise<void> {
- await this.delay();
- for (const it of items) {
- const key = `${it.roomId}|${it.threadRoot}`;
- const cur = this.mockThreads.get(key);
- this.mockThreads.set(key, {
- objectId: cur?.objectId ?? `mock_${key}`,
- agentId: it.agentId,
- roomId: it.roomId,
- threadRoot: it.threadRoot,
- title: cur?.titleLocked ? cur.title : it.title || cur?.title || '',
- titleLocked: cur?.titleLocked ?? false,
- messageCount: it.messageCount ?? cur?.messageCount ?? 0,
- lastMessageAt: it.lastMessageAt ?? cur?.lastMessageAt ?? Date.now(),
- archived: cur?.archived ?? false,
- lastPreview: it.lastPreview ?? cur?.lastPreview ?? '',
- });
- }
- this.persistThreads();
- }
- async patchChatThread(input: {
- threadRoot: string;
- title?: string;
- archived?: boolean;
- workspaceId?: string;
- }): Promise<void> {
- await this.delay();
- for (const [key, t] of this.mockThreads) {
- if (t.threadRoot !== input.threadRoot) continue;
- this.mockThreads.set(key, {
- ...t,
- title: input.title ?? t.title,
- titleLocked: input.title != null ? true : t.titleLocked,
- archived: input.archived ?? t.archived,
- workspaceId: input.workspaceId ?? t.workspaceId,
- });
- }
- this.persistThreads();
- }
- async removeChatThread(input: { threadRoot: string; roomId?: string; agentId?: string }): Promise<void> {
- await this.delay();
- for (const [key, t] of this.mockThreads) {
- if (t.threadRoot === input.threadRoot) this.mockThreads.delete(key);
- }
- this.persistThreads();
- }
- // ---------- 工作区(2026-11-01 上云) ----------
- private readonly mockWorkspaces = new Map<string, WorkspaceRecord>();
- async fetchWorkspaces(): Promise<WorkspaceRecord[]> {
- await this.delay();
- return [...this.mockWorkspaces.values()].sort((a, b) => a.createdAt - b.createdAt);
- }
- async upsertWorkspaces(items: WorkspaceUpsert[]): Promise<void> {
- await this.delay();
- for (const it of items) this.mockWorkspaces.set(it.id, { ...it });
- }
- async removeWorkspace(id: string): Promise<void> {
- await this.delay();
- this.mockWorkspaces.delete(id);
- }
- // ---------- 任务看板(FR-07b)----------
- async fetchTasks(agentId: string): Promise<Task[]> {
- await this.delay();
- return DEMO_TASKS.filter((t) => t.agentId === agentId)
- .map((t) => ({ ...t }))
- .sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''));
- }
- async fetchTaskDetail(taskId: string): Promise<Task | null> {
- await this.delay();
- const found = DEMO_TASKS.find((t) => t.id === taskId);
- return found ? { ...found } : null;
- }
- // ---------- 上传 ----------
- async requestUploadUrl(filename: string, mimeType: string, size: number): Promise<UploadTicket> {
- await this.delay();
- if (!filename) throw new Error('缺少文件名');
- const key = `demo/${Date.now()}-${filename}`;
- return {
- uploadUrl: `#demo-upload`,
- key,
- publicUrl: `https://s3.fmode.cn/${key}`,
- headers: { 'Content-Type': mimeType, 'x-obs-acl': 'public-read' },
- };
- }
- async uploadFile(
- file: File,
- onProgress?: (pct: number) => void,
- ): Promise<{ key: string; publicUrl: string }> {
- const ticket = await this.requestUploadUrl(file.name, file.type, file.size);
- // 演示:不真正上传,仅模拟进度(README 载明)
- for (let pct = 0; pct <= 100; pct += 20) {
- onProgress?.(pct);
- await new Promise((r) => setTimeout(r, 120));
- }
- return { key: ticket.key, publicUrl: ticket.publicUrl };
- }
- async enqueueArtifactJob(_job: {
- deliverableId: string;
- agentId: string;
- source: string;
- filename: string;
- index: number;
- }): Promise<{ id: string }> {
- return { id: `mock-job-${Date.now()}` };
- }
- async fetchArtifactJobs(_agentId: string): Promise<ArtifactJob[]> {
- return [];
- }
- async ackArtifactJob(_input: {
- id: string;
- ok: boolean;
- publicUrl?: string;
- error?: string;
- }): Promise<void> {
- // 演示态无副作用
- }
- // ---------- 工作组(演示态内存实现) ----------
- private readonly mockGroups: WorkGroup[] = [];
- async createWorkGroup(input: {
- name: string;
- goal?: string;
- memberAgentIds: string[];
- }): Promise<{ id: string; roomId: string }> {
- const id = `wg-${Date.now()}`;
- const roomId = `!demo-${id}:fmode.cn`;
- this.mockGroups.push({
- id,
- name: input.name,
- goal: input.goal,
- roomId,
- members: input.memberAgentIds,
- status: 'active',
- });
- return { id, roomId };
- }
- async listWorkGroups(): Promise<WorkGroup[]> {
- return [...this.mockGroups];
- }
- async inviteToWorkGroup(groupId: string, agentIds: string[]): Promise<void> {
- const g = this.mockGroups.find((x) => x.id === groupId);
- if (g) g.members = [...g.members, ...agentIds];
- }
- async removeFromWorkGroup(groupId: string, agentId: string): Promise<void> {
- const g = this.mockGroups.find((x) => x.id === groupId);
- if (g) g.members = g.members.filter((m) => m !== agentId);
- }
- async archiveWorkGroup(groupId: string): Promise<void> {
- const g = this.mockGroups.find((x) => x.id === groupId);
- if (g) g.status = 'archived';
- }
- // ---------- 组织成员管理(2026-11-10,演示态内存实现) ----------
- /** 演示数据:有组织成员管理的入口才看得到真实交互 */
- private readonly mockOrgUsers: OrgUser[] = [
- { id: 'demo-user-01', username: 'zhangsan', nickname: '张三', mobile: '13800000001', isDeleted: false, createdAt: Date.now() - 86400_000 * 30 },
- { id: 'demo-user-02', username: 'lisi', nickname: '李四', mobile: '13800000002', isDeleted: false, createdAt: Date.now() - 86400_000 * 20 },
- { id: 'demo-user-03', username: 'wangwu', nickname: '王五', mobile: '13800000003', isDeleted: false, createdAt: Date.now() - 86400_000 * 10 },
- ];
- async listOrgUsers(query: {
- keyword?: string;
- page?: number;
- pageSize?: number;
- }): Promise<OrgUserPage> {
- const keyword = (query.keyword ?? '').trim().toLowerCase();
- const page = Math.max(1, query.page ?? 1);
- const pageSize = Math.max(1, query.pageSize ?? 20);
- const all = this.mockOrgUsers.filter(
- (u) =>
- !u.isDeleted &&
- (!keyword ||
- u.nickname.toLowerCase().includes(keyword) ||
- u.mobile.includes(keyword) ||
- u.id.toLowerCase().includes(keyword)),
- );
- const start = (page - 1) * pageSize;
- return { items: all.slice(start, start + pageSize), total: all.length, page, pageSize };
- }
- async addOrgUser(userId: string): Promise<OrgUser> {
- const existed = this.mockOrgUsers.find((u) => u.id === userId);
- if (existed) {
- existed.isDeleted = false;
- return existed;
- }
- const created: OrgUser = {
- id: userId,
- username: userId,
- nickname: '新成员',
- mobile: '',
- isDeleted: false,
- createdAt: Date.now(),
- };
- this.mockOrgUsers.push(created);
- return created;
- }
- async updateOrgUser(
- userId: string,
- patch: { nickname?: string; mobile?: string; password?: string },
- ): Promise<OrgUser> {
- const u = this.mockOrgUsers.find((x) => x.id === userId);
- if (!u) throw new Error('用户不存在');
- if (patch.nickname != null) u.nickname = patch.nickname;
- if (patch.mobile != null) u.mobile = patch.mobile;
- return { ...u };
- }
- async removeOrgUser(userId: string): Promise<void> {
- const u = this.mockOrgUsers.find((x) => x.id === userId);
- if (u) u.isDeleted = true;
- }
- /** 演示态:当前用户视为已加入组织 */
- async fetchOrgMembership(): Promise<OrgMembership> {
- return { userId: 'demo-me', hasOrg: true, companyId: 'demo-company', companyName: '演示组织' };
- }
- // ---------- 内部 ----------
- private delay(): Promise<void> {
- this.maybeFail();
- const ms = environment.mockLatency;
- return ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve();
- }
- private maybeFail(): void {
- const rate = environment.mockFailureRate;
- if (rate > 0 && Math.random() < rate) {
- throw new Error('演示模式:模拟网络请求失败');
- }
- }
- }
|