01

数字员工是什么?

在写字楼里多了一位"智能新同事"——TA 有工号、有人设、有技能、有飞书号。

先想象一个场景

周一早上九点,你在飞书群里 @ 了一下"小K",问 TA:"上周华东区的退货率是不是又涨了?"

两秒后,小K 回了一段带表格的分析报告。看起来 TA 像个真人助理 —— 但 TA 其实是一个 agent,一个有名字、有人设、有技能、有渠道账号的"数字员工"。

💡
一句话定位

kweaver-dip 不是个"聊天机器人项目",而是企业级数字员工平台。它把 AI 抽象成一种"可治理的同事"——能上岗、能挂技能、能进飞书群、能被 HR 按规则管理。

数字员工的"四件套"

每个数字员工都由这四件东西定义。看完这一屏你会发现,"创建 AI 助手"这件事其实是 4 个独立可改的文件。

📇

IDENTITY.md

名片。写着 TA 叫什么、什么岗位、头像 ID。改这个文件 = 改"工卡正面信息"。

🧠

SOUL.md

灵魂。写着 TA 性格如何、说话语气、价值观。改这个文件 = 给 TA 换一个"职业人设"。

🛠️

Skills(技能包)

背包里装着的工具。比如"查 SQL"、"读 PDF"、"调用 BKN"。绑哪些技能,TA 就能干哪些活。

📱

Channel(渠道)

飞书号 / 钉钉号。绑了之后,同事就能在群里 @ TA 而不是登平台后台聊天。

这四件套就是企业级 AI 应用的最小积木 —— 一句"我要新建一个数字员工" = 我要写 4 个不同的字段 / 文件。

代码里的"创建一个数字员工"

下面这段代码是 Studio 后端真正"造出"一个数字员工时跑的核心 5 步。来源:studio/src/logic/digital-human.tscreateDigitalHuman 方法。

CODE
public async createDigitalHuman(
  request: CreateDigitalHumanRequest
): Promise<CreateDigitalHumanResult> {
  const id = request.id?.trim() || randomUUID();
  const template = buildTemplate(request);

  const workspace = resolveDefaultWorkspace(id);

  await this.openClawAgentsAdapter.createAgent({
    name: id,
    workspace
  });

  await this.writeTemplateViaOpenClawFilesRpc(id, template);

  const skills = normalizeCreateDigitalHumanSkills(request.skills);
  await this.agentSkillsLogic.updateAgentSkills(id, skills);
中文逐行

第 1 步:分配工号。 用户传了 id 就用,没传就生成一个 UUID——就像新员工入职 HR 给的工号。

第 2 步:渲染人设模板。 把请求里的 name / creature / soul 等字段渲染成 IDENTITY.md 和 SOUL.md 文档内容。

第 3 步:分配工位。 workspace 是 TA 工作时存文件的目录路径,类似公司给新员工分配工位。

第 4 步:在 OpenClaw 里"注册"这个员工。 OpenClaw 是底层的 agent 运行时,调它的 RPC 让员工在系统里挂上号。

第 5 步:写身份和灵魂文件。 把 IDENTITY.md / SOUL.md 通过 OpenClaw 的文件接口写进 TA 的工位。

第 6 步:装技能背包。 把绑定的技能列表登记好——TA 可以做哪些事就由这一步决定。

🔍
注意这段代码的"几乎没有 if"

5 步全是"调用别人完成一件事"——这种"不做事,只编排"的代码在工程里叫 编排(orchestration)。Studio 是个调度中心,不是干活的人。

一次群聊看懂"@ 数字员工"

当你在飞书里 @ 一下小K,下面这些"角色"轮流发言。点 下一条 看完整剧情。

小测一下:你跟 AI 工具说话能不能更精准

这一节的 quiz 不考你"记住了几个名词",而是考你能不能用学到的概念准确指挥 AI 编码工具。

问题 1:你想把数字员工的岗位改成"合同律师",应该改哪份文件?

问题 2:同事说"小K 在飞书里没收到消息",下面哪个最先该排查?

,不留歧义。它一听就知道改哪些字段,不需要再 thrash。" data-explanation-wrong="另外几句都太模糊。AI 工具会先猜你要"接 OpenAI",然后开始造一堆样板代码——你可能调试半天才发现它走歪了。">

问题 3:你要给 AI 编码工具下指令"加一个数字员工",下面哪句最精准、最不容易让 AI 理解错?

02

五大主角登场

一支乐队的五个声部 —— 每个声部用最适合自己的乐器,靠节拍合作。

为什么一个项目里挤了五种语言?

打开 kweaver-dip 仓库,根目录下你会看到 5 个完全不同语言写的子目录。乍一看像"团队混乱"——但这是有意为之。

🎻
把它当一支乐队

不是合唱团(一种声音重复几十遍),是乐队——每个声部用最擅长的乐器:键盘、贝斯、鼓、主唱、和声。它们靠节拍(HTTP API + WebSocket 协议)合奏。

🎯
Studio · TypeScript / Node

"鼓手" —— 节奏中枢。所有数字员工的对话流式编排都在这里。

🚪
Hub · Python / FastAPI

"键盘手" —— 统筹和声。用户登录、Token、应用门户都归它管。

🧬
GBKN · Go / Go-Zero

"贝斯手" —— 埋在底层却定义律动。把数据炼成业务知识,跑高并发管道。

⚖️
Workflow · Java / Spring Boot

"和声组" —— 织出审批规则。BPMN 引擎让流程随业务变。

🎨
Web · React / TypeScript

"主唱" —— 你看见的脸。所有界面的根源。

仓库长什么样

下面是真实仓库根目录里值得你认识的 7 个子文件夹。每一个对应一个"舞台"。

studio/ 数字员工调度中心(Express + TS),所有 OpenClaw 编排都从这里发起
hub/ 登录门户与应用列表(FastAPI + Python),轻、快、易扩展
gbkn/ 业务知识网络的语义抽取(Go-Zero),含 Kafka 异步管道
workflow/ BPMN 工作流与审批引擎(Spring Boot + Activiti)
web/ 所有面向用户的前端(React monorepo,含 dip / agent-web 等子应用)
deploy/ 一键部署脚本和 Helm Charts,K8s 部署的配方
docs/ 在线帮助、产品文档、架构设计
📦
这种结构叫 monorepo

monorepo 的意思是"多个项目共用一个 git 仓库"。kweaver-dip 在 monorepo 里塞了 4 种后端语言 + 1 种前端 —— 这种组合叫 polyglot monorepo。Google 用这种方式管几亿行代码。

看 Studio 是怎么"装乐器"的

来读一段真实的 Studio 入口代码,studio/src/app.ts 第 50-66 行。它一眼让你明白"调度中心"是什么意思。

CODE
export function createApp(options: AppOptions = {}): Express {
  const app = express();

  app.disable("x-powered-by");
  app.use(express.json());
  app.use(createHydraAuthMiddleware());
  app.use(createHealthRouter());
  app.use(createGuideRouter());
  app.use(createBknRouter());
  app.use(createCronRouter());
  app.use(createChatRouter());
  app.use(createSessionsRouter());
  app.use(createSkillsRouter());
  app.use(createChannelUserRouter());
  app.use(createDigitalHumanRouter());
  app.use(createChatUploadRouter());
  app.use(createChatAgentRouter());
中文逐行

建立一个 Express 应用实例,叫 app。它就是要装乐器的乐器架。

关掉一个泄露技术栈信息的响应头(小小的安全细节)。

告诉 app:"我要解析 JSON 请求体"。

第一个挂上去的是中间件 —— 鉴权关卡:每一个请求先验 Token 才放行。

健康检查路由(让 K8s 知道服务还活着)。

引导路由(首次部署完成 OpenClaw 配置的引导)。

业务知识网络(BKN)转发路由 —— 把 Web 的请求转给 GBKN。

计划任务(cron)路由 —— 让数字员工定时干活。

聊天历史查询路由。

会话列表路由。

技能管理路由 —— 装/卸技能包。

渠道用户路由(飞书/钉钉用户名册)。

数字员工 CRUD 路由(创建/查询/更新/删除)。

聊天附件上传路由。

最关键的"@ 数字员工对话"流式路由 —— 第 3 章主角。

🎯
关键观察:14 个 router

看见没——一个 17 行的函数挂了 14 个 router。这就是"调度中心"的样子:自己几乎不写业务,把请求按路径分配给负责的子模块。

看一段 Go 代码长什么样

不同语言写"读数据库"长得不一样。下面是 GBKN 用 Go 查询库表理解状态的最小例子,来源 gbkn/api/internal/logic/data_semantic/get_status_logic.go

CODE
func (l *GetStatusLogic) GetStatus(req *types.GetStatusReq) (resp *types.GetStatusResp, err error) {
	logx.Infof("GetStatus called with id: %s", req.Id)

	// 调用 Model 层查询 form_view 状态
	formViewModel := form_view.NewFormViewModel(l.svcCtx.DB)
	formViewData, err := formViewModel.FindOneById(l.ctx, req.Id)
	if err != nil {
		return nil, errorx.Detail(errorx.QueryFailed, err, "库表视图")
	}

	resp = &types.GetStatusResp{
		UnderstandStatus: formViewData.UnderstandStatus,
	}

	logx.Infof("GetStatus success: form_view_id=%s, status=%d",
		req.Id, formViewData.UnderstandStatus)

	return resp, nil
}
中文逐行

这是一个"查询库表理解状态"的函数。req 是入参,resp / err 是出参——Go 的函数可以同时返回多个值。

先打一行日志记录"我被谁、用什么 id 调用了"。

建一个 Model 层对象。Model 层是 Go-Zero 框架里专门跟数据库打交道的角色。

查数据库。FindOneById 顾名思义——按 id 找一条记录。

如果出错,返回一个带"上下文"的错误。Go 工程师习惯把 err 一层层包起来传上去,让调用方知道"在哪一层、为什么失败"。

把数据库里查到的状态字段塞进响应结构体。

再打一行成功日志。Go 项目对日志非常 verbose——这是工程纪律。

返回 resp 和 nil(nil 表示没出错)。

对比 Studio 的 TS 代码 vs GBKN 的 Go 代码——你能感觉到 Go 更"克制"、更"工业感"。这正是它适合干高并发管道的原因。

把语言和职责连起来

把 5 张技能牌(左)拖到 5 个职责(右)。在手机上长按然后拖动。

TypeScript / Node
Python / FastAPI
Go / Go-Zero
Java / Spring Boot
React / TypeScript

WebSocket 流式编排(数字员工对话)

把对应技能拖到这里

OAuth 登录门户、用户应用列表

把对应技能拖到这里

Kafka 高并发异步管道、知识图谱语义抽取

把对应技能拖到这里

BPMN 工作流引擎、企业审批

把对应技能拖到这里

用户界面、路由、表单

把对应技能拖到这里

小测:定位你要让 AI 改的"舞台"

问题 1:用户报告"飞书里和小K 聊天,对话不返回" —— 你最先该 grep 哪个目录的代码?

问题 2:你想加"自动从 PDF 抽合同条款入图"功能,主体逻辑应该写在哪个仓库?

问题 3:你想加"4 级审批流程",BPMN XML 应该让 AI 改哪个仓库?

03

一句话的旅程

地铁换乘网 —— 一句话从你按回车到 AI 回复,要换乘 5 次、过 2 道闸机。

把它想成早高峰地铁换乘

你按下回车的瞬间,请求并不是"飞向 AI"。它要从 1 号线(Web 浏览器)出发,到 4 号线(Hub 鉴权)换乘验票,再换 10 号线(Studio 转流),最终到达终点站(OpenClaw + 大模型)。

🚇
两步舞,不是一步登顶

对话不是一个 HTTP 请求就完事。第一步先 POST /chat/session 拿一个 sessionKey;第二步再 POST /chat/agent 用 sessionKey 开 SSE 流。这设计的好处:第一步纯 HTTP 易缓存,第二步是长连接。

完整数据流动画

下面这张动画追一句话从手指敲下到第一个字回来的全过程。点 下一步 看每一站发生什么。

🎨
Web
🚪
Hub / Hydra
🎯
Studio
🛰️
OpenClaw
🤖
Agent + LLM
点"下一步"开始

看 Studio 怎么处理这一切

下面是 studio/src/routes/chat.ts 真实的对话路由。26 行代码就是"两步舞"的第二步

CODE
router.post(
  "/api/dip-studio/v1/chat/agent",
  async (request, response, next) => {
    const abortController = new AbortController();
    attachDownstreamAbortHandlers(request, response, abortController);
    try {
      const requestBody = readChatAgentRequestBody(request.body);
      const sessionKey = readRequiredSessionKeyHeader(request.headers);
      const agentId = readAgentIdFromSessionKey(sessionKey);
      const sessionLabel = await resolveChatAgentSessionLabel(
        sessionsLogic, sessionKey, requestBody.message);
      const message = appendAttachmentHintsToMessage(
        requestBody.message, requestBody.attachments);
      const upstreamResponse = await chatAgentClient.createResponseStream({
          sessionKey, message,
          attachments: requestBody.attachments,
          idempotencyKey: randomUUID(),
          sessionLabel
        }, agentId, abortController.signal);
      writeEventStreamHeaders(response, upstreamResponse.status, upstreamResponse.headers);
      await pipeEventStream(upstreamResponse.body, response);
中文逐行

注册一条 POST 路由 /chat/agent

建一个 AbortController——一旦下游断开,能让上游 fetch 也跟着取消。

绑定取消信号到 request/response 的 close 事件。

从请求体里读出消息内容("上周华东区退货率…")。

从请求头读出 sessionKey。这是上一步拿到的"两步舞门票"。

从 sessionKey 里反向拿出 agent id(也就是要找哪个数字员工)。

如果是会话第一句,给会话生成一个标签(用消息前几个字 + 随机后缀)。

如果消息附带文件,把文件路径作为隐藏上下文塞进消息里——这样大模型能看到。

关键的一行:调上游 OpenClaw,开始一条流式响应。idempotencyKey 是防重发用的随机 UUID。

把上游的 SSE 响应头转给浏览器(包括 content-type、cache-control 等)。

最后这一行是真正的"管道"——上游字节进来就直接写给浏览器,不等不缓存。

SSE 头部里藏着的"反 buffer"魔法

如果你曾经被一个"流式接口卡住"折磨过,下面这段 18 行代码可能就是答案。来源:studio/src/routes/chat.ts

CODE
export function writeEventStreamHeaders(
  response: Response,
  statusCode: number,
  headers: Headers
): void {
  response.status(statusCode);
  response.setHeader(
    "content-type",
    headers.get("content-type") ?? "text/event-stream; charset=utf-8"
  );
  response.setHeader(
    "cache-control",
    headers.get("cache-control") ?? "no-cache, no-transform"
  );
  response.setHeader("connection", headers.get("connection") ?? "keep-alive");
  response.setHeader("x-accel-buffering", "no");
  response.flushHeaders?.();
}
中文逐行

这个函数负责设置流式响应的 HTTP 头部——一字一句往浏览器吐之前先告诉它"接下来是流"。

设置状态码(一般是 200)。

告诉浏览器响应体是 text/event-stream——浏览器看到这个就知道"用 EventSource 接"。

告诉浏览器和中间代理"不要缓存、不要变换内容"——AI 流式输出最怕被缓存住。

长连接保持活着,别一秒没数据就断开。

魔法行:x-accel-buffering: no。这是给 Nginx 这种代理看的——意思是"这次别 buffer,请字节级转发"。少了这一行,SSE 在生产可能完全失灵。

立刻把头部冲出去——别等响应体凑齐才发。

⚠️
"我本地能跑,生产卡住了"

90% 的"流式接口生产挂"都是 x-accel-buffering: no 没设、或者前面有一层代理(Nginx / Cloudflare / 公司网关)非要 buffer。下次让 AI 工具调试 SSE 时,第一时间检查这一行——你能省下几小时排查时间。

小测:给你 4 个"卡住不动"的场景

问题 1:一个聊天突然卡住没字符往外冒。下面哪个最不可能是原因?

问题 2:sessionKey 长这样 agent:foo:user:bar:direct:abc。其中 "foo" 是什么?

问题 3:为什么 Studio 在创建流之前要先生成 idempotencyKey?

问题 4:你改 Web 改了半天聊天卡住——但 Network 面板看到 /chat/agent 200 OK 而且每秒有数据进来。最可能问题在哪?

04

知识从数据中析出

金矿提炼车间 —— 从一堆带泥沙的矿石(库表字段)炼出可挂在墙上的成品(业务知识网络)。

把数据炼成知识,要过几道工序?

想象一张表叫 t_order_002_v3,里面 47 个字段名是缩写:"cust_id"、"amt"、"st"、"shp_fee"。AI 看一眼说"这是订单表"——但这个结论不能直接进知识图谱

因为业务规则不允许"AI 说啥就是啥"。在 kweaver-dip 里,每张库表都得走一条 6 阶状态机,AI 只是其中一道工序,最终归属必须人审才入图

⛏️
企业级 ≠ AI 全权决定

"AI 辅助识别 + 人工审核 + 可回滚"是企业级 AI 应用和玩具应用的分水岭。下次你的 AI 编码工具说"我帮你做个全自动入库管道",记得问一句:"状态机呢?人审呢?失败回滚呢?"

6 阶状态机:库表理解的一生

这是来自 gbkn/model/form_view/vars.go 真实定义的 6 个状态。每一步都对应一个明确的"事件"。

0
未理解

初始状态。表刚接入,AI 还没看过——像新矿石刚到车间。

1
理解中

用户点了"一键生成"。Go 服务把字段打包,扔给 AI 服务异步处理——矿石进了第一道滚筒。

2
待确认

AI 出了候选业务对象("客户"、"订单金额"、"运费")。等人审决定 AI 猜得对不对——这是质检员上班时间。

3
已完成

人审通过,业务对象拍板。还可以再次重新理解(回到 1)——成品入库但还没上架。

4
已发布

真正进入业务知识网络(BKN)。下游 Agent / RAG 检索时能看到——上架的成品。

5
理解失败

旁路。AI 调用挂了 / Kafka 拉不到消息 / 解析出错——状态会自动回退到这里,可重试。

代码里的"状态机闸门"

下面是 gbkn/api/internal/logic/data_semantic/generate_understanding_logic.go 一段惊人浓缩的代码——25 行里包含了"前置校验、防抖、状态过渡"三个企业级模式。

CODE
func (l *GenerateUnderstandingLogic) GenerateUnderstanding(req *types.GenerateUnderstandingReq) (resp *types.GenerateUnderstandingResp, err error) {
	logx.Infof("GenerateUnderstanding called with id: %s, fields count: %d", req.Id, len(req.Fields))

	// 1. 状态校验:只有状态 0(未理解)或 3(已完成)才允许生成
	formViewModel := form_view.NewFormViewModel(l.svcCtx.DB)
	formViewData, err := formViewModel.FindOneById(l.ctx, req.Id)
	if err != nil {
		return nil, fmt.Errorf("查询库表视图失败: %w", err)
	}

	currentStatus := formViewData.UnderstandStatus
	if currentStatus != form_view.StatusNotUnderstanding && currentStatus != form_view.StatusCompleted && currentStatus != form_view.StatusFailed {
		return nil, fmt.Errorf("当前状态不允许生成理解数据,当前状态: %d", currentStatus)
	}

	// 2. 限流检查(1秒窗口,防止重复点击)
	if !l.svcCtx.AllowRequest(req.Id) {
		return nil, fmt.Errorf("操作过于频繁,请稍后再试")
	}

	// 3. 更新状态为 1(理解中)
	err = formViewModel.UpdateUnderstandStatus(l.ctx, req.Id, form_view.StatusUnderstanding)
	if err != nil {
		return nil, fmt.Errorf("更新理解状态失败: %w", err)
	}
中文逐行

函数签名:接受一个"理解请求",返回响应或错误。

先打日志记录调用上下文(哪个表、多少字段)。

第 1 道闸门 · 状态校验。 先去数据库查这张表当前状态。

如果查询出错,直接返回。%w 是 Go 的"包装错误"语法——能保留底层 error 让上层追溯。

核心一行: 只有 0/3/5(未理解、已完成、失败)三种状态才允许重新生成。其他状态进来就拒。这就是状态机的精髓——不是 AI 想生成就生成。

第 2 道闸门 · 限流。 同一个表 1 秒内只允许触发一次生成——防止用户连点。

如果限流命中,直接拒。报错文案对用户友好。

第 3 步:提前置位。 在调 AI 之前先把状态置为 1(理解中)。

如果连状态置位都失败,立刻返回——后面就不要继续了。

🎯
"提前置位 = 占位"模式

注意第 3 步——为什么在调 AI 之前就把状态改成"理解中"?因为 AI 调用是耗时的,期间任何人来查"这张表在干嘛",都能看到"理解中"。这就是占位模式——并发场景下避免两个请求同时在炼同一张表。

如果 AI 调用挂了怎么办?

这是分布式系统最关键的场景:你已经把状态改成"理解中",结果 AI 服务返回 500。状态卡死了?不会——看这 5 行:

CODE
// 5. 调用 AI 服务 HTTP API(同步调用)
if err := l.callAIService(req.Id, formViewData, fields, len(req.Fields) > 0); err != nil {
    // 调用失败,回退状态
    _ = formViewModel.UpdateUnderstandStatus(l.ctx, req.Id, currentStatus)
    return nil, fmt.Errorf("调用 AI 服务失败: %w", err)
}
中文逐行

注释说明:第 5 步去调 AI 服务(同步调用)。

if 判断:如果 AI 调用返回错误—— err 不是 nil。

注释一句"调用失败,回退状态"。

关键行:把状态从"理解中"改回 currentStatus(之前那个 0/3/5)。前面的下划线 _ 表示"我故意忽略这个返回值"——回滚本身的失败已经处理不了了,先尽力一试。

把外层错误包装好返回给前端。

♟️
分布式系统的"悔棋原则"

这种"先占位、出错就回退"的模式有个正式名字,叫 补偿动作(compensating action)。当你的 AI 工具写"先 update DB、再调外部 API"的代码时,如果不加这个回退,就埋了一个"状态卡死"的坑。

车间群聊:一次完整冶炼

把上面的状态机配合 Kafka,串成一段群聊。点 下一条 看完整剧情。

小测:把"治理"塞进 AI 应用

问题 1:你点了"一键生成"按钮但没反应(显示"操作过于频繁")。这个限流的目的是?

问题 2:AI 服务返回失败时,代码做了什么?

问题 3:Kafka 在这个架构里扮演什么角色?

失去了什么:审计追溯没了、错了不知道怎么回滚、合规风险增加。这是 push back 的正确姿势。">

问题 4:你想让 AI 编码工具加一个"AI 自动跳过人审直接发布"的开关——一个有经验的工程师会怎么 push back?

05

工作流与审批

机场塔台调度 —— 业务流程像航班,按图纸走,塔台调度。

业务流程不是 if-else,是图纸

"我们这个合同要 4 级审批,第 2 级两人会签,法务部国庆节免审。"——你听到这种需求时,第一反应应该是什么?

错误反应:让 AI 写一堆嵌套 if-else。3 个月后业务说"加一级"——你得改代码、走发布、可能蹦回归测试一周。

正确反应:用一张图纸(BPMN XML)描述流程,让引擎(Activiti)读图执行。改流程 = 改 XML = 不动代码。

🛫
配置驱动 vs 代码驱动

kweaver-dip 的 workflow 模块是配置驱动架构的极致案例。它把流程从代码里"挤出来"放进 XML,引擎按图办事——这是企业级软件能撑 10 年不重写的关键。

workflow 模块的三层叠塔

workflow 仓库内部其实是三个 Maven 子模块,各管一段。

🎙️
workflow-rest

REST API 与 Web 层。把外部 HTTP 请求转成内部调用、做鉴权、做异常处理。塔台对外的"窗口"。

⚙️
workflow-code

核心业务逻辑 + Activiti 引擎集成。流程定义管理、任务分配、流程实例追踪。塔台里的"调度脑"。

📋
doc-audit-rest

文档审核管理模块。审核策略、审核人分配、第三方审核集成。塔台调度的"档案柜"。

REST API 的命名习惯

看看 workflow 暴露的几个核心端点。这些 URL 用的术语本身就在传授一种思维方式。

CODE
POST   /api/workflow-rest/v1/process-definitions
PUT    /api/workflow-rest/v1/process-definitions/{id}
POST   /api/workflow-rest/v1/process-definitions/{id}/deploy
POST   /api/workflow-rest/v1/process-instances
POST   /api/workflow-rest/v1/tasks/{id}/complete
中文逐行

创建一个流程"模板"。 注意这个词 process-definition——这是 BPMN 里的"图纸",还没开始跑。

更新一个已有图纸(修订版本)。

把图纸部署到引擎。 部署 = 让引擎能识别它。没部署的模板永远跑不起来。

启动一个流程实例。 这是关键 ——process-instance 是模板的一次"具体跑动"。一份合同提交 = 启动一个实例。

完成一个任务。 流程跑到某一步会生成 task,分给候选审核人。完成 task 的接口就是这一行——审核人点"同意"按钮就是调它。

📐
关键术语区分:definition vs instance

这是 BPMN / 工作流圈最重要的两个词。Definition 是图纸(一张),instance 是按图纸跑出来的航班(很多个)。下次你跟 AI 工具说"我要改流程",先想清楚是改图纸(definition)还是查某次实例(instance)——它会跟着你的精度走。

一次审批的 5 步生命周期

从画图到审完,一份合同要经历 5 个阶段。每一步都对应一个 API 调用。

1
画图

业务专家用 BPMN 工具画出流程("经理审 → 总监审 → 法务审")。这一步不用写代码——产物是 .bpmn 的 XML 文件。

2
部署

POST /process-definitions/{id}/deployActiviti 引擎读 XML,把节点表写进数据库。

3
启动实例

用户提交合同 → 调 POST /process-instances 启动一次。引擎给这次执行分配实例 id,开始执行第 1 个节点。

4
任务流转

引擎按图把 task 自动分配给候选审核人。会签时同时给多人,或签时只给一组里其中一个。

5
完成

每个审核人调 POST /tasks/{id}/complete 同意/打回,引擎自动推下一个节点,直到流程结束。

企业审批的 4 种核心模式

下面这 4 张卡是 BPMN 引擎送给你的"自由功能"——你不用写一行代码,引擎已经帮你想好了。

👥

会签

一个节点要多人都同意才能往下走。比如重要合同需要财务总监+法务总监一起签。引擎自动等齐所有人的"同意"。

或签

一组候选人,任一个先签就算通过。比如运维值班,5 个人轮流,谁先看见消息谁批。

↪️

委托

审核人忙不过来,把 task 转给同事代审。引擎记录"原审核人 + 代审核人",审计可追溯。

↩️

撤回

提交人发现填错了,在还没人审之前可以撤回。流程实例回到 draft 状态,不留审计噪音。

💡
这就是技术选型决定项目命运的瞬间

不用 BPMN 引擎,这 4 个模式每个都得你自己写代码。会签的并发计数、撤回的状态回退、委托的审计链——一个不小心就是 1000 行 if-else 加 3 张表。选对工具能省 80% 的代码

小测:跟 AI 工具讨论审批需求

问题 1:客户说"4 级审批,第二级两人会签,法务部国庆免审"——下面哪个最不是 AI 编码工具能直接帮你写出来的部分?

问题 2:同事在 Web 提交了合同,但管理后台看不到这条审批——可能是哪一步出问题?

问题 3:一个工作流系统只用 if-else 写在 Java 代码里,不用 BPMN 引擎——它最大的麻烦是?

06

跨语言架构的"为什么"

多国合作建桥 —— 把每件事交给最对的工具,靠统一图纸协作。

为什么不"统一用 Python 重写"?

看完前 5 章你可能还有一个疑问:为什么 kweaver-dip 用了 5 种语言?工程团队是不是有点"任性"?

不是。这是一个深思熟虑的取舍 —— 业内叫 polyglot architecture。它的核心信念:用语言匹配场景,比用一种语言搞定一切更划算

🌉
就像跨海大桥的多国合作

钢索工艺日本最强、混凝土技术德国最稳、水文勘测荷兰最熟、桥头雕刻意大利最美。统一让一个国家做反而更慢更贵——只要大家用同一份图纸(HTTP API + JSON)就能合作。

5 种语言,各擅胜场

TypeScript / Node

擅长:WebSocket 长连接、流式编排、事件循环。Studio 用它把 OpenClaw 的对话流转成 SSE 给前端——天作之合。

🚪

Python / FastAPI

擅长:OAuth 鉴权门户、快速原型、CRUD API。Hub 用它做登录 + 应用列表——半小时就能跑起来,新功能加得飞快。

🔥

Go / Go-Zero

擅长:高并发、Kafka 异步管道、内存效率。GBKN 每秒处理几千条 AI 识别消息——Go 的 goroutine 比 Python 线程便宜两个数量级。

⚖️

Java / Spring Boot

擅长:BPMN 引擎、企业中间件、长生命周期项目。Activiti 是 Java 独有的成熟工作流引擎——其他语言的同类生态差了一个数量级。

🎨

React / TypeScript

擅长:复杂前端状态、组件复用、monorepo 共享。Web 子目录里 7 个不同的 React app 共享一套设计系统——这是 React + monorepo 的甜区。

两个不同语言,相同的目录结构

下面这件事最有趣:Studio(TypeScript)和 Hub(Python)虽然语言完全不一样,但目录结构几乎一样。这不是巧合,是六边形架构(hexagonal architecture)这种设计纪律的功劳。

CODE
# Studio(TypeScript)
src/app.ts        # 组装 Express 应用、中间件和路由
src/server.ts     # 读环境变量并启动服务
src/utils/        # 通用工具与运行时配置
src/infra/        # 基础设施适配层(OpenClaw 客户端)
src/adapters/     # 外部资源适配器
src/routes/       # HTTP 路由定义
src/logic/        # 核心业务逻辑
src/middleware/   # 通用中间件(404、错误处理)
src/errors/       # 领域内可复用的错误类型

# Hub Backend(Python)
src/
├── domains/         # 领域层:核心业务逻辑和领域模型
├── ports/           # 端口层:接口定义
├── application/     # 应用层:用例和业务编排
├── adapters/        # 适配器层:端口的具体实现
├── routers/         # 路由层(入站适配器)
├── infrastructure/  # 基础设施层:配置、日志、容器
└── main.py
中文逐行

看左边的 Studio(TS)。 它把代码分成 9 层,每层职责清晰。

核心层:logic(业务)、adapters(外部依赖适配器)、infra(基础设施)。这就是六边形的内圈。

边界层:routes(HTTP 入口)、middleware(横切关注点)、errors(错误类型)。这是六边形的外圈。

看右边的 Hub(Python)。 同样的分层逻辑,连命名都几乎一样。

domains = TS 的 logic(核心业务)。ports = 接口("插座")。adapters = 接口的实现("插头")。

routers = HTTP 入口(叫"入站适配器"),跟 TS 的 routes 一样。

关键收获:架构纪律是跨语言的。换语言不用扔掉这套思维方式——你跟 AI 说"按六边形分层",它在 Java/Go/Rust 都能给你做对。

跨语言的"普通话" —— 错误响应规范

5 种语言能合作,靠的是约定一套通用"格式"。kweaver-dip 整个仓库的错误响应都长这样:

CODE
{
  "code": "DipStudio.SkillBadLayout",
  "description": "SKILL.md is missing required front matter metadata",
  "solution": "补充合法的 front matter,并确保包含 name 字段",
  "detail": {
    "upstream": {
      "service": "openclaw",
      "operation": "skills.install",
      "httpStatus": 400,
      "code": "BAD_LAYOUT"
    }
  },
  "link": "https://example.internal/docs/errors#DipStudio.SkillBadLayout"
}
中文逐行

code 是稳定的业务错误码。 DipStudio.SkillBadLayout 这个字符串永远不变——前端 if 判断只能看它,永远别看 description。

description 是给人看的。 文案可能改、可能翻译——做 if 判断会很脆。

solution 是 UI 提示。 直接显示给用户的"该怎么办"。

detail.upstream 才是上游原始错误。 这里看得到底层是哪个服务、什么错。

关键纪律:上游的 BAD_LAYOUT 只在 detail 里——不能直接当 Studio 的对外错误码。否则换上游服务,前端代码全爆炸。

link 字段给跳到帮助文档。

🚨
前端工程师常踩的雷

"如果错误描述里包含'权限'两个字就跳登录页"——这就是看 description 写逻辑的坏习惯。文案一改就全坏。永远用 code 字段做 if 判断。这条纪律救过无数线上事故。

谢幕群聊:5 种语言的协奏

课程的最后一段群聊:你提交一份合同审批,5 种不同语言的服务在 iMessage 群里"用 HTTP/JSON 普通话"协作。这是给整个课程的收尾。

最后小测:你已经能跟工程师对话了

问题 1:客户说"加一个高并发文件入库管道,每秒 5 万条"——AI 工具建议你用 Python multiprocessing。你应该 push back 改用什么?

问题 2:你想给 hub 的登录接口加一个新的 OAuth provider(Lark)。下面哪个改动最像 hexagonal 架构的"端口/适配器"做法?

问题 3:错误响应里同时有 code: "DipStudio.SkillBadLayout"detail.upstream.code: "BAD_LAYOUT"。前端 if 判断应该看哪一个?

主动的工程取舍:每种语言用在自己最强的场景里。">

问题 4:同事问你"为什么 kweaver-dip 不全用 Python 重写更省事?"你的最佳回答是?

收尾的话

🎓
你已经准备好了

6 章下来,你认识了数字员工的"四件套"、5 种语言各自的角色、一句话穿过 5 站的旅程、知识从数据中析出的状态机、BPMN 工作流引擎,以及为什么这一切要拆开成 polyglot 架构。

下次你跟 AI 编码工具聊企业 AI 系统时,你能用"sessionKey"、"补偿动作"、"process instance"、"hexagonal port/adapter"、"polyglot"这些词精准沟通。它会因此少走很多弯路 —— 而你也会因此从"vibe coder"逐渐变成"会驾驭 AI 的工程师"。

把今天学到的拿去用 —— 改你自己的项目。看你的项目用了多少这些模式,缺了哪些。这才是这门课真正的"作业"。