eventMapper.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import { randomUUID } from 'node:crypto';
  2. import type { AgentEvent, AgentEventKind } from './types';
  3. /**
  4. * 移植自 Noobi.ai src/main/eventMapper.ts。
  5. * 删掉了整块 stage 推断(那是游戏流水线专用)与 planner/implementer/reviewer 角色标签,
  6. * 事件维度改成 threadId;也删掉了 imageGeneration / dynamicToolCall 这类 Noobi 自有素材工具。
  7. */
  8. const UNTHREADED = 'unthreaded';
  9. export function threadIdOf(notification: { params?: unknown }): string {
  10. const params = asRecord(notification.params);
  11. const turn = asRecord(params?.turn);
  12. const item = asRecord(params?.item);
  13. return (
  14. readString(params?.threadId) ?? readString(turn?.threadId) ?? readString(item?.threadId) ?? UNTHREADED
  15. );
  16. }
  17. export function notificationToEvent(notification: {
  18. method: string;
  19. params?: unknown;
  20. }): AgentEvent | null {
  21. const params = asRecord(notification.params) ?? {};
  22. const item = asRecord(params.item);
  23. const turn = asRecord(params.turn);
  24. const threadId = threadIdOf(notification);
  25. const turnId = readString(params.turnId) ?? readString(turn?.id) ?? 'turn';
  26. const itemId = readString(params.itemId) ?? readString(item?.id) ?? undefined;
  27. const method = notification.method;
  28. let kind: AgentEventKind = 'lifecycle';
  29. let title = 'Codex';
  30. let message = '';
  31. let isDelta = false;
  32. switch (method) {
  33. case 'item/agentMessage/delta':
  34. kind = 'assistant';
  35. title = '回复';
  36. message = readString(params.delta) ?? '';
  37. isDelta = true;
  38. break;
  39. case 'item/reasoning/summaryTextDelta':
  40. kind = 'thought';
  41. title = '思考摘要';
  42. message = readString(params.delta) ?? '';
  43. isDelta = true;
  44. break;
  45. case 'item/commandExecution/outputDelta':
  46. kind = 'tool';
  47. title = '命令输出';
  48. message = readString(params.delta) ?? '';
  49. isDelta = true;
  50. break;
  51. case 'turn/plan/updated':
  52. kind = 'plan';
  53. title = '计划更新';
  54. message = describe(params.plan ?? params);
  55. break;
  56. case 'item/fileChange/patchUpdated':
  57. kind = 'file';
  58. title = '文件变更';
  59. message = readString(params.patch) ?? readString(params.diff) ?? '正在生成补丁';
  60. isDelta = true;
  61. break;
  62. case 'item/started':
  63. case 'item/completed': {
  64. const type = readString(item?.type) ?? 'item';
  65. const presentation = describeItem(item, type);
  66. kind = presentation.kind;
  67. title = presentation.title;
  68. message = presentation.message || (method === 'item/completed' ? '已完成' : '已开始');
  69. break;
  70. }
  71. case 'turn/started':
  72. title = '回合开始';
  73. message = 'Codex 已开始处理当前任务';
  74. break;
  75. case 'turn/completed':
  76. title = '回合结束';
  77. message = `状态:${readString(turn?.status) ?? 'completed'}`;
  78. break;
  79. case 'error':
  80. kind = 'error';
  81. title = '运行错误';
  82. message = readString(params.message) ?? readString(asRecord(params.error)?.message) ?? describe(params);
  83. break;
  84. case 'warning':
  85. case 'configWarning':
  86. kind = 'error';
  87. title = '警告';
  88. message = readString(params.message) ?? describe(params);
  89. break;
  90. default:
  91. return null;
  92. }
  93. if (!message) return null;
  94. return {
  95. id: itemId ? `${threadId}:${turnId}:${itemId}:${kind}` : randomUUID(),
  96. threadId,
  97. turnId,
  98. kind,
  99. title,
  100. message: clip(message),
  101. timestamp: new Date().toISOString(),
  102. method,
  103. ...(itemId ? { itemId } : {}),
  104. ...(isDelta ? { isDelta: true } : {}),
  105. };
  106. }
  107. function describeItem(
  108. item: Record<string, unknown> | null,
  109. type: string,
  110. ): { kind: AgentEventKind; title: string; message: string } {
  111. if (!item) return { kind: 'lifecycle', title: type, message: '' };
  112. switch (type) {
  113. case 'agentMessage':
  114. return { kind: 'assistant', title: '回复', message: readString(item.text) ?? '' };
  115. case 'reasoning':
  116. return {
  117. kind: 'thought',
  118. title: '推理摘要',
  119. message: readTextArray(item.summary) || readTextArray(item.content),
  120. };
  121. case 'commandExecution':
  122. return {
  123. kind: 'tool',
  124. title: '执行命令',
  125. message: readString(item.command) ?? describe(item.commandActions ?? item),
  126. };
  127. case 'fileChange':
  128. return { kind: 'file', title: '修改文件', message: describe(item.changes ?? item) };
  129. case 'mcpToolCall':
  130. return {
  131. kind: 'tool',
  132. title: `工具 ${cleanToolName(readString(item.tool))}`.trim(),
  133. message: describe(item.arguments ?? item.result ?? item),
  134. };
  135. case 'plan':
  136. return { kind: 'plan', title: '计划', message: describe(item) };
  137. default:
  138. return { kind: 'lifecycle', title: type, message: describe(item) };
  139. }
  140. }
  141. function readTextArray(value: unknown): string {
  142. if (!Array.isArray(value)) return '';
  143. return value
  144. .map((entry) => (typeof entry === 'string' ? entry : readString(asRecord(entry)?.text) ?? ''))
  145. .filter(Boolean)
  146. .join('\n');
  147. }
  148. function describe(value: unknown): string {
  149. if (typeof value === 'string') return value;
  150. try {
  151. return JSON.stringify(value, null, 2);
  152. } catch {
  153. return String(value);
  154. }
  155. }
  156. function clip(value: string, maxLength = 24_000): string {
  157. return value.length > maxLength ? `${value.slice(0, maxLength)}\n…(已截断)` : value;
  158. }
  159. function cleanToolName(value: string | null): string {
  160. return value?.replace(/[^A-Za-z0-9_.-]/gu, '').slice(0, 128) ?? '';
  161. }
  162. function asRecord(value: unknown): Record<string, unknown> | null {
  163. return value && typeof value === 'object' && !Array.isArray(value)
  164. ? (value as Record<string, unknown>)
  165. : null;
  166. }
  167. function readString(value: unknown): string | null {
  168. return typeof value === 'string' ? value : null;
  169. }