group-roles.service.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { computed, effect, inject, Injectable, signal } from '@angular/core';
  2. import { Agent } from './api/models';
  3. import { GroupsService } from './groups.service';
  4. import { LiveService } from './live.service';
  5. import { MatrixClientService } from './matrix-client.service';
  6. /**
  7. * 工作组角色(组长 / 组员 + 职责)。
  8. *
  9. * ## 为什么需要单独一层
  10. *
  11. * 群里全是 agent,用户看不懂 **该找谁**:
  12. * 谁负责汇总?谁负责前端?谁只是打杂的?
  13. *
  14. * ## 存哪(选型理由)
  15. *
  16. * 用 Matrix **自定义状态事件** `fmode.workgroup.roles`,不用数据库表:
  17. *
  18. * | 方案 | 问题 |
  19. * |---|---|
  20. * | 新建 Parse 表 | 要服务端配合,且与房间生命周期不同步 |
  21. * | 写 `m.room.topic` | 只能放纯文本,解析脆弱 |
  22. * | **自定义状态事件** | ✅ 房间自带、随房间销毁、可版本化、任何客户端都能读 |
  23. *
  24. * 数据结构:
  25. * ```json
  26. * {
  27. * "leader": "!agentId", // 组长
  28. * "roles": { "!agentId": "前端" }, // 各成员职责
  29. * "version": 1
  30. * }
  31. * ```
  32. */
  33. export interface WorkGroupRoles {
  34. /** 组长 agentId(空表示未指定) */
  35. leader?: string;
  36. /** agentId → 职责描述(如「前端」「后端」「设计」) */
  37. roles: Record<string, string>;
  38. /**
  39. * 允许接受组长指令的 agentId 列表。
  40. *
  41. * 🔴 容器默认只认自己的主人。跨主人的组员必须先被「组长授权」才能接受指令,
  42. * 否则组长 @ 它也不会执行(实测会回「不是我主人的指令」)。
  43. * 这里记录**已授权**的成员,未在列表里的要提示用户。
  44. */
  45. authorized?: string[];
  46. /** 版本号,便于将来演进数据结构 */
  47. version: number;
  48. }
  49. const STATE_TYPE = 'fmode.workgroup.roles';
  50. @Injectable({ providedIn: 'root' })
  51. export class GroupRolesService {
  52. private readonly mx = inject(MatrixClientService);
  53. private readonly groups = inject(GroupsService);
  54. private readonly live = inject(LiveService);
  55. /** 各群的角色配置(roomId → roles) */
  56. private readonly _byRoom = signal<Record<string, WorkGroupRoles>>({});
  57. readonly byRoom = this._byRoom.asReadonly();
  58. /** 读取某群的角色;没有配置时返回空结构 */
  59. readonly rolesOf = computed(() => (roomId: string): WorkGroupRoles => {
  60. return this._byRoom()[roomId] ?? { roles: {}, version: 1 };
  61. });
  62. /** 某群谁是组长(返回 Agent,便于显示名字) */
  63. readonly leaderOf = computed(() => (roomId: string): Agent | undefined => {
  64. const id = this.rolesOf()(roomId).leader;
  65. return id ? this.live.agentById(id) : undefined;
  66. });
  67. /** 某成员的职责文案 */
  68. readonly dutyOf = computed(() => (roomId: string, agentId: string): string => {
  69. return this.rolesOf()(roomId).roles[agentId] ?? '';
  70. });
  71. constructor() {
  72. // 群列表变化时重新读角色(新建的群也要能读到)
  73. effect(() => {
  74. const rooms = this.groups.rooms();
  75. if (rooms.length === 0) return;
  76. void this.reloadAll(rooms.map((r) => r.roomId));
  77. });
  78. }
  79. private async reloadAll(roomIds: string[]): Promise<void> {
  80. const next: Record<string, WorkGroupRoles> = { ...this._byRoom() };
  81. let changed = false;
  82. for (const rid of roomIds) {
  83. const raw = await this.mx.getRoomState(rid, STATE_TYPE);
  84. const parsed = normalizeRoles(raw);
  85. if (JSON.stringify(next[rid]) !== JSON.stringify(parsed)) {
  86. next[rid] = parsed;
  87. changed = true;
  88. }
  89. }
  90. if (changed) this._byRoom.set(next);
  91. }
  92. /**
  93. * 写入某群的角色配置。
  94. *
  95. * 只有**房间里的人**能写(Matrix 状态事件需要足够的 power level),
  96. * 通常由群创建者(人类 owner)设置。
  97. */
  98. async save(roomId: string, roles: WorkGroupRoles): Promise<void> {
  99. await this.mx.setRoomState(roomId, STATE_TYPE, roles);
  100. this._byRoom.update((m) => ({ ...m, [roomId]: roles }));
  101. }
  102. /** 设为组长 */
  103. async setLeader(roomId: string, agentId: string): Promise<void> {
  104. const cur = this.rolesOf()(roomId);
  105. await this.save(roomId, { ...cur, leader: agentId, version: 1 });
  106. }
  107. /** 设置某成员的职责 */
  108. async setDuty(roomId: string, agentId: string, duty: string): Promise<void> {
  109. const cur = this.rolesOf()(roomId);
  110. const roles = { ...cur.roles, [agentId]: duty };
  111. await this.save(roomId, { ...cur, roles, version: 1 });
  112. }
  113. }
  114. /** 容错解析:外部数据不可信,字段缺失时给默认值 */
  115. function normalizeRoles(raw: unknown): WorkGroupRoles {
  116. const o = (raw ?? {}) as Record<string, unknown>;
  117. const roles: Record<string, string> = {};
  118. const r = o['roles'];
  119. if (r && typeof r === 'object' && !Array.isArray(r)) {
  120. for (const [k, v] of Object.entries(r as Record<string, unknown>)) {
  121. if (typeof v === 'string') roles[k] = v;
  122. }
  123. }
  124. const authorized = Array.isArray(o['authorized'])
  125. ? (o['authorized'] as unknown[]).filter((x): x is string => typeof x === 'string')
  126. : undefined;
  127. return {
  128. leader: typeof o['leader'] === 'string' ? o['leader'] : undefined,
  129. roles,
  130. authorized,
  131. version: typeof o['version'] === 'number' ? o['version'] : 1,
  132. };
  133. }