member-task-timeout.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import { Message } from './api/models';
  2. /**
  3. * 组员超时检测 —— 组长派活后组员一直不回。
  4. *
  5. * ## 为什么需要
  6. *
  7. * 组长制下组长会「限时轮询」等组员。但实测量到:
  8. *
  9. * - 组员忙的时候消息在排队(可能排 20 分钟)
  10. * - 组员可能压根没收到(曾出现同一消息别的容器收到、它没收到)
  11. *
  12. * 用户看到的是「组长一直在等」,但不知道是组员慢还是死了。
  13. *
  14. * ## 这个模块只做判断,不做动作
  15. *
  16. * | 状态 | 含义 | App 该做什么 |
  17. * |---|---|---|
  18. * | `pending` | 刚派活,正常等待 | 什么都不做 |
  19. * | `slow` | 等得有点久了 | 轻提示「组员正在处理」|
  20. * | `stuck` | 等太久了 | 提示「可能卡住了,建议问问组长」|
  21. *
  22. * 🔴 **不自动重发** —— 重发可能让组员做两遍,产生重复产出。
  23. */
  24. /** 多久算「有点久」 */
  25. export const SLOW_MS = 5 * 60_000;
  26. /** 多久算「卡住了」 */
  27. export const STUCK_MS = 15 * 60_000;
  28. export type MemberTaskState = 'idle' | 'pending' | 'slow' | 'stuck';
  29. export interface MemberTaskStatus {
  30. state: MemberTaskState;
  31. /** 已经等了多久(毫秒),没有在等时为 0 */
  32. waitedMs: number;
  33. /** 给用户看的提示 */
  34. hint: string;
  35. /** 卡住的组员名字(多组员时用户要知道找谁) */
  36. memberName?: string;
  37. }
  38. /**
  39. * 判断「组长派给组员的活」等多久了。
  40. *
  41. * @param messages 群消息(时间升序)
  42. * @param leaderMxId 组长的 Matrix ID(用于识别「组长派的活」)
  43. * @param targetMxId 组员的 Matrix ID(用于识别「组员回了没」)
  44. * @param now 当前时刻
  45. */
  46. export function memberTaskStatus(
  47. messages: Message[],
  48. leaderMxId: string,
  49. targetMxId: string,
  50. now = Date.now(),
  51. memberName?: string,
  52. ): MemberTaskStatus {
  53. const idle: MemberTaskStatus = { state: 'idle', waitedMs: 0, hint: '' };
  54. if (!leaderMxId || !targetMxId) return idle;
  55. // 从后往前找:最近一次「组长 @ 该组员」
  56. let assignedAt = 0;
  57. let assignedIdx = -1;
  58. for (let i = messages.length - 1; i >= 0; i--) {
  59. const m = messages[i];
  60. if (m.senderId !== leaderMxId) continue;
  61. if (!m.mentions?.includes(targetMxId)) continue;
  62. assignedAt = Date.parse(m.ts);
  63. assignedIdx = i;
  64. break;
  65. }
  66. if (assignedIdx < 0 || !Number.isFinite(assignedAt)) return idle;
  67. // 派活之后,该组员有没有说过话
  68. const replied = messages
  69. .slice(assignedIdx + 1)
  70. .some((m) => m.senderId === targetMxId && !!m.text?.trim());
  71. if (replied) return idle;
  72. const waitedMs = Math.max(0, now - assignedAt);
  73. // 多组员时带上名字,否则用户不知道该找谁
  74. const who = memberName ? `${memberName} ` : '';
  75. if (waitedMs >= STUCK_MS) {
  76. return {
  77. state: 'stuck',
  78. waitedMs,
  79. memberName,
  80. hint: `${who}已经等了 ${Math.round(waitedMs / 60_000)} 分钟还没动静,可能卡住了,建议问问组长`,
  81. };
  82. }
  83. if (waitedMs >= SLOW_MS) {
  84. return {
  85. state: 'slow',
  86. waitedMs,
  87. memberName,
  88. hint: `${who}还在处理组长派的活(已 ${Math.round(waitedMs / 60_000)} 分钟)`,
  89. };
  90. }
  91. return { state: 'pending', waitedMs, hint: '' };
  92. }