This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/apps/api/src/app/mastra-spans.ts
T
ChaiHaoandClaude Fable 5 5fc6dfdfe1 仿真测试台(后端):节点指派 + 全链 Trace —— 起单定向到指定账号,真实任务落工作台
用户定的形态:测试台是**后端检测活动与流程**的,真实任务照旧到工作台
(不然指定账号干嘛)—— 所以测试台只管「起单 + 看穿」,人那一环还是真的。

**节点指派**(迁移 0016 workflow.assignments):
- StartWorkflowInput 增 `assignments {节点id → 员工id}`;分派改三级取人:
  ①节点指派(点名的人在岗就是他)②员工组轮值 ③全员轮值 —— 指派是加法不是替换,
  点名的人不在岗退回轮值(单不能没人接);事件里如实记 `(节点指派)`
- **顺带修一个真 bug**:`node_instance.assignee_id` 写了但 `listInstances` 从没读回来
  (审批箱收件、员工面链条、治理列表全按它认人 —— 写了不读等于没写)

**全链 Trace**(新模块 trace,L2;迁移 0017 sys.trace_span):
- span 由 **Mastra Observability 组件**产出(执行不经 Mastra 图,所以手动打点),
  一开就落库、一收就补 —— 观测面不该比被观测的事晚(不走 exporter 的批量异步路)
- 打点覆盖:工作流 root → 节点(记着派给了谁)→ 活动内每次工具调用 / 模型调用 / 人卡
- **root span 挂在 `runtime.begin`**:那是「单已建好、还没进第一个节点」那一刻 ——
  挂在 start 返回之后就晚了,起单同步走到第一个节点,节点 span 会先出生、另起孤树(实测)
- 观测尽力而为:写 span 失败吞掉,不掀翻业务
- 三样东西各管各的:trace(机器读时间轴)/ 任务记忆(人读留痕)/
  **工作区报告(活动自持的 HTML)** —— 谁也不替代谁

路由:`POST /api/gov/testbed/run`(带指派起单)、`GET /api/gov/workflows/:id/trace`。

端到端六例(真库):指派生效 / 任务真落到被指派人手上并由他答单 / 下游节点同样生效 /
全链 span 逐层挂得上且耗时齐备 / 一条流一个 traceId / 不指派照旧轮值。五闸全过。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-29 01:43:49 +08:00

67 lines
2.7 KiB
TypeScript

/**
* `SpanFactory` 的 **Mastra 实现** —— 用 Mastra 的 Observability 组件产 span
* (`@mastra` 只准 adapter 与 app,§6.2)。
*
* 我们的执行不经 Mastra 图,所以它的自动 tracing 不会产出任何东西;
* 但 span 的**形状与 id 体系**(traceId / spanId / 父子链)照用它的 ——
* 将来真接了 Mastra workflow,这些 span 与它自己产的能拼进同一棵树。
*
* 这里只要 id 与父子关系:落库、耗时、入出参由 trace 模块自己管
* (它要「一开就落」,而 Mastra 的 exporter 是缓冲 + 定时冲刷的批量路)。
*/
import { DefaultObservabilityInstance } from '@mastra/observability';
import type { SpanFactory } from '@modules/trace';
/**
* Mastra span 对象上我们要用的那几个字段。
* Mastra 的 `Span<TType>` 泛型极深且随版本变;这里只声明**我们真正用到的**面,
* 由 `instance` 的窄化包装一次性对上 —— 断言收在 `spanApi` 这一处。
*/
type MastraSpan = {
id: string;
traceId: string;
createChildSpan(options: { type: string; name: string }): MastraSpan;
};
/** Mastra 实例的窄化面:类型桥收在这一处,别撒到调用点上 */
type SpanApi = { startSpan(options: { type: string; name: string }): MastraSpan };
export function createMastraSpanFactory(serviceName = 'jcai'): SpanFactory {
const instance: SpanApi = new DefaultObservabilityInstance({
config: { serviceName, sampling: { type: 'always' }, exporters: [] },
} as never) as SpanApi;
/*
* Mastra 的 span 对象要留着 —— 子 span 得从父对象上开(`createChildSpan`),
* 光有 id 拼不出父子链。这张表按 spanId 存活着的 span;
* 观测是**尽力而为**的:表里查不到父亲(进程重启、span 太老被清)就另起一棵树,
* 不抛错、不让业务停摆。
*/
const live = new Map<string, MastraSpan>();
const remember = (span: MastraSpan): { spanId: string; traceId: string } => {
live.set(span.id, span);
// 只留最近的一批:测试台一次跑一条流,几十个 span 足够;不设上限会随长跑泄漏
if (live.size > 2000) {
const oldest = live.keys().next().value;
if (oldest !== undefined) live.delete(oldest);
}
return { spanId: span.id, traceId: span.traceId };
};
return {
root(name) {
return remember(instance.startSpan({ type: 'generic', name }));
},
child(parent, name) {
const held = live.get(parent.spanId);
if (held === undefined) {
// 父亲不在手上:另起一棵树,别把整条链丢掉(观测尽力而为)
return remember(instance.startSpan({ type: 'generic', name }));
}
return remember(held.createChildSpan({ type: 'generic', name }));
},
};
}