/** * 日志行的呈现口径 —— **主语进内容**(2026-07-31 用户定)。 * * 日志一行读起来该是「谁 · 做了什么 · 什么内容」: * * 16:25 [触发事件] 系统:一封邮件触发了这条流程,系统指定你负责「A1 · 询价受理」。 * 16:26 [阶段结论] 任务助手:这封邮件来自青岛海洋电子,是新客户… * 16:30 [员工确认] 柴浩:写入 customer.profile * * 四字动作与归侧由 `memory` 的域函数算出(纯函数,可重算); * **只有人名算不出来** —— 域里零 IO,不认识人。所以域交出 `actorId`, * 在这一层换成名字:换不到就退回「员工」,不编名字(界面不撒谎)。 * * 放在 routes 而不是 memory 模块里:这是**呈现**口径,不是事实口径。 * 事实库里存的是 id,谁改了名、谁离职了,回看旧单都该按当时的 id 找当下的名字。 */ import type { LogLine } from '@modules/memory'; import { staffIdSchema } from '@contracts'; import type { OrgService } from '@modules/org'; export type DisplayLogLine = { readonly id: string; readonly at: string; readonly side: LogLine['side']; readonly sideLabel: string; /** 谁干的 —— 日志只认三档:负责人(绿)· 智能体(蓝)· 系统(灰) */ readonly who: LogLine['who']; readonly action: string; readonly title: string; readonly summary?: string; /** 展开才看的细节:一段一句人话(摘不出就是空数组 —— 前端据此不给展开钮) */ readonly detail: readonly string[]; }; /** * 把 `actorId` 换成名字并拼进内容。 * * 一次任务里出现的人极少(通常 0–2 个),所以按**出现过的 id** 逐个查, * 不整表捞:整表捞在任务详情这条热路径上是白花的一次全表查询。 */ export async function displayLog(lines: readonly LogLine[], org: OrgService): Promise { const ids = [...new Set(lines.flatMap((l) => (l.actorId === undefined ? [] : [l.actorId])))]; const names = new Map(); await Promise.all( ids.map(async (id) => { const parsed = staffIdSchema.safeParse(id); if (!parsed.success) return; const staff = await org.getStaff(parsed.data).catch(() => undefined); if (staff !== undefined) names.set(id, staff.name); }), ); return lines.map((l) => { // 人做的那些:换得到名字就用名字,换不到就写「员工」—— 不编,也不留空 const actor = l.actor !== '' ? l.actor : (l.actorId === undefined ? '员工' : (names.get(l.actorId) ?? '员工')); return { id: l.id, at: l.at, side: l.side, sideLabel: l.sideLabel, who: l.who, action: l.action, detail: l.detail, title: `${actor}:${l.title}`, ...(l.summary === undefined ? {} : { summary: l.summary }), }; }); }