| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- import { computed, effect, inject, Injectable, signal } from '@angular/core';
- import { Agent } from './api/models';
- import { GroupsService } from './groups.service';
- import { LiveService } from './live.service';
- import { MatrixClientService } from './matrix-client.service';
- /**
- * 工作组角色(组长 / 组员 + 职责)。
- *
- * ## 为什么需要单独一层
- *
- * 群里全是 agent,用户看不懂 **该找谁**:
- * 谁负责汇总?谁负责前端?谁只是打杂的?
- *
- * ## 存哪(选型理由)
- *
- * 用 Matrix **自定义状态事件** `fmode.workgroup.roles`,不用数据库表:
- *
- * | 方案 | 问题 |
- * |---|---|
- * | 新建 Parse 表 | 要服务端配合,且与房间生命周期不同步 |
- * | 写 `m.room.topic` | 只能放纯文本,解析脆弱 |
- * | **自定义状态事件** | ✅ 房间自带、随房间销毁、可版本化、任何客户端都能读 |
- *
- * 数据结构:
- * ```json
- * {
- * "leader": "!agentId", // 组长
- * "roles": { "!agentId": "前端" }, // 各成员职责
- * "version": 1
- * }
- * ```
- */
- export interface WorkGroupRoles {
- /** 组长 agentId(空表示未指定) */
- leader?: string;
- /** agentId → 职责描述(如「前端」「后端」「设计」) */
- roles: Record<string, string>;
- /**
- * 允许接受组长指令的 agentId 列表。
- *
- * 🔴 容器默认只认自己的主人。跨主人的组员必须先被「组长授权」才能接受指令,
- * 否则组长 @ 它也不会执行(实测会回「不是我主人的指令」)。
- * 这里记录**已授权**的成员,未在列表里的要提示用户。
- */
- authorized?: string[];
- /** 版本号,便于将来演进数据结构 */
- version: number;
- }
- const STATE_TYPE = 'fmode.workgroup.roles';
- @Injectable({ providedIn: 'root' })
- export class GroupRolesService {
- private readonly mx = inject(MatrixClientService);
- private readonly groups = inject(GroupsService);
- private readonly live = inject(LiveService);
- /** 各群的角色配置(roomId → roles) */
- private readonly _byRoom = signal<Record<string, WorkGroupRoles>>({});
- readonly byRoom = this._byRoom.asReadonly();
- /** 读取某群的角色;没有配置时返回空结构 */
- readonly rolesOf = computed(() => (roomId: string): WorkGroupRoles => {
- return this._byRoom()[roomId] ?? { roles: {}, version: 1 };
- });
- /** 某群谁是组长(返回 Agent,便于显示名字) */
- readonly leaderOf = computed(() => (roomId: string): Agent | undefined => {
- const id = this.rolesOf()(roomId).leader;
- return id ? this.live.agentById(id) : undefined;
- });
- /** 某成员的职责文案 */
- readonly dutyOf = computed(() => (roomId: string, agentId: string): string => {
- return this.rolesOf()(roomId).roles[agentId] ?? '';
- });
- constructor() {
- // 群列表变化时重新读角色(新建的群也要能读到)
- effect(() => {
- const rooms = this.groups.rooms();
- if (rooms.length === 0) return;
- void this.reloadAll(rooms.map((r) => r.roomId));
- });
- }
- private async reloadAll(roomIds: string[]): Promise<void> {
- const next: Record<string, WorkGroupRoles> = { ...this._byRoom() };
- let changed = false;
- for (const rid of roomIds) {
- const raw = await this.mx.getRoomState(rid, STATE_TYPE);
- const parsed = normalizeRoles(raw);
- if (JSON.stringify(next[rid]) !== JSON.stringify(parsed)) {
- next[rid] = parsed;
- changed = true;
- }
- }
- if (changed) this._byRoom.set(next);
- }
- /**
- * 写入某群的角色配置。
- *
- * 只有**房间里的人**能写(Matrix 状态事件需要足够的 power level),
- * 通常由群创建者(人类 owner)设置。
- */
- async save(roomId: string, roles: WorkGroupRoles): Promise<void> {
- await this.mx.setRoomState(roomId, STATE_TYPE, roles);
- this._byRoom.update((m) => ({ ...m, [roomId]: roles }));
- }
- /** 设为组长 */
- async setLeader(roomId: string, agentId: string): Promise<void> {
- const cur = this.rolesOf()(roomId);
- await this.save(roomId, { ...cur, leader: agentId, version: 1 });
- }
- /** 设置某成员的职责 */
- async setDuty(roomId: string, agentId: string, duty: string): Promise<void> {
- const cur = this.rolesOf()(roomId);
- const roles = { ...cur.roles, [agentId]: duty };
- await this.save(roomId, { ...cur, roles, version: 1 });
- }
- }
- /** 容错解析:外部数据不可信,字段缺失时给默认值 */
- function normalizeRoles(raw: unknown): WorkGroupRoles {
- const o = (raw ?? {}) as Record<string, unknown>;
- const roles: Record<string, string> = {};
- const r = o['roles'];
- if (r && typeof r === 'object' && !Array.isArray(r)) {
- for (const [k, v] of Object.entries(r as Record<string, unknown>)) {
- if (typeof v === 'string') roles[k] = v;
- }
- }
- const authorized = Array.isArray(o['authorized'])
- ? (o['authorized'] as unknown[]).filter((x): x is string => typeof x === 'string')
- : undefined;
- return {
- leader: typeof o['leader'] === 'string' ? o['leader'] : undefined,
- roles,
- authorized,
- version: typeof o['version'] === 'number' ? o['version'] : 1,
- };
- }
|