# Temporal 多 worktree 协作模式

> **版本**: v1.0
> **最后更新**: 2026-05-16
> **下次复查触发条件**: Temporal Server 版本大升 / 新增 worktree namespace 命名约定 / Temporal 多机部署变化 / 季度复查
> **沉淀来源**: 2026-04 → 2026-05 累计 4 条 `.learnings/` 项目对接 Temporal 时反复踩坑
> **适用范围**: 所有使用 Temporal workflow 的开发任务（approval / form-management / meeting-attendance 等模块）；多 worktree 并行开发 Temporal 相关功能

---

## 0. 核心认知：Temporal Server 天然多租户，**多 worktree 共享一个 Server**

`Temporal Server` 天然支持多 namespace —— 一个 server 可以承载无数 namespace，workflow ID 在 namespace 内唯一，**跨 namespace 互不影响**。

**多 worktree 共享一个 Temporal server + 各自用独立 namespace 是 Temporal 的标准用法**——不要每个 worktree 起一套 Temporal Server。

### 反模式（已踩坑）

```bash
# ❌ 错——每个 worktree 试图起独立 Temporal
bash scripts/dev/dev.sh up --temporal
# Bind for 0.0.0.0:4002 failed: port is already allocated
```

每个 worktree 起一套 Temporal 不仅浪费资源（每个容器 ~700MB），还会撞端口、撞容器名。

### 正解（已落地）

```
[团队已存在的 Temporal 容器：ffoa-wt-dingtalk-temporal]
   └─ 7233 (gRPC)，已 expose 到 host port 3133
   └─ 多 namespace 共存：default / approval-form-polish / asset-mgmt / ...
       └─ 每个 worktree 注册独立 namespace
       └─ backend/.env 指过去：TEMPORAL_ADDRESS=localhost:3133 + TEMPORAL_NAMESPACE=<worktree-name>
```

---

## 1. 接入流程（新 worktree 起 Temporal workflow 必跑）

### Step 1: 确认共享 Temporal 容器在跑

```bash
docker ps --filter 'name=temporal' --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
# 期望看到 ffoa-wt-dingtalk-temporal（或同类名）+ host port 3133→7233
```

如果没在跑：参考最近一个用 Temporal 的 worktree（如 dingtalk），不要新起。

### Step 2: 注册本 worktree 的 namespace

```bash
docker exec ffoa-wt-dingtalk-temporal tctl --address temporal:7233 \
    --namespace <worktree-name> namespace register --retention 7
```

**`--retention 7`**：保留 workflow 历史 7 天（开发够用，生产建议 ≥ 30）。

**namespace 命名约定**：`<worktree-name>` 直接复用，避免短命名空间撞同名（如不要用 `dev`、`test` 这种通用名）。

### Step 3: 修 `backend/.env`

```env
TEMPORAL_ADDRESS=localhost:3133              # 已运行的 Temporal host port
TEMPORAL_NAMESPACE=<worktree-name>           # 独立 namespace 避免 ID 撞
TEMPORAL_TASK_QUEUE=<worktree-name>-tasks    # 可选：task queue 也用 worktree 名避免撞
```

### Step 4: 重启 backend

```bash
npm run start:dev
# 看 log：
# ✅ Temporal client connected, namespace: <worktree-name>
# ✅ Worker started, polling for tasks on queue: <worktree-name>-tasks
```

---

## 2. 不变量：Worker 与 Client 的 namespace **必须一致**

### 事故记录

worktree 接通 Temporal 后：
1. Worker 启动 OK，"Polling for approval workflow tasks" 一直在转
2. DB 里 `approval_instances.status=RUNNING`、`workflow_id` 已写入
3. 但 `tctl --namespace <worktree-name> workflow list` **完全为空**
4. `approval_tasks` 表也是空的（workflow 没推进到任务创建阶段）

切到 `default` namespace 也一样。

### 根因（典型 bug 实例）

`backend/src/core/workflow/temporal/worker.ts:67-72`：

```ts
const worker = await Worker.create({
  connection,
  workflowsPath: require.resolve('@engines/approval/temporal/workflows'),
  activities,
  taskQueue,
  // ❌ 缺 namespace 参数 — 默认走 'default'
});
```

而 `temporal.service.ts` 给 Client 传了 `namespace: configService.get('temporal.namespace')`。所以：

- Client 在 `approval-form-polish` 启动 workflow
- Worker 在 `default` 轮询 task queue
- **两边永远见不到面**

### 规则

**Worker.create() 必须显式传 namespace**，跟 Client 一致：

```ts
// ✅ 正确
const namespace = configService.get<string>('temporal.namespace') || 'default';

const worker = await Worker.create({
  connection,
  namespace,                  // ← 必须显式传
  workflowsPath: ...,
  activities,
  taskQueue,
});
```

### 自检命令

跑 backend 后立即用 `tctl` 验证 worker 真在自己 namespace 轮询：

```bash
# 看 namespace 下注册的 task queues + worker 数
docker exec ffoa-wt-dingtalk-temporal tctl \
    --namespace <worktree-name> taskqueue describe <worktree-name>-tasks
# 期望：Pollers 列表里有你的 worker
```

如果 `Pollers: []` —— Worker.create 漏 namespace（参考根因）。

参考 learning: [`.learnings/2026-04-30-temporal-worker-namespace-bug.md`](../../.learnings/2026-04-30-temporal-worker-namespace-bug.md)

---

## 3. 共享 Temporal Server 起不来时的 host-network 绕行

### 场景

某 worktree 没有自己的 Temporal 容器；`bash scripts/dev/dev.sh up --temporal` 报：

```
Bind for 0.0.0.0:4002 failed: port is already allocated
```

### 根因

worktree 的 docker compose 在 setup 时换过 `CONTAINER_PREFIX`（旧 `ffws-wt-pg-4002` / `ffws-wt-redis-4003` → 新 `ffoa-wt-<feature>-*`）：

- **老** pg/redis 容器仍在跑且占用 4002/4003 端口
- **新** prefix 的 named volumes 已存在但是空的
- **新** compose 想再创一套 pg/redis，端口冲突

直接 `docker rm -f ffws-wt-pg-4002` 会**丢已 seed 的开发库**（匿名 volume，跟容器一起删）。

### 解法（绕行）：不用 compose 起 Temporal，直接 `docker run --network host`

```bash
docker run -d --name <prefix>-temporal --network host --restart unless-stopped \
  -e DB=postgres12 \
  -e DB_PORT=4002 \
  -e POSTGRES_USER=ffoa_dev -e POSTGRES_PWD=dev_password \
  -e POSTGRES_SEEDS=localhost \
  -e BIND_ON_IP=0.0.0.0 \
  -e TEMPORAL_BROADCAST_ADDRESS=127.0.0.1 \
  -e FRONTEND_GRPC_PORT=4033 -e FRONTEND_HTTP_PORT=4034 \
  temporalio/auto-setup:1.25.2

# 注册 namespace（注意 4033，不是默认 7233）
docker exec <prefix>-temporal tctl --address localhost:4033 \
    --namespace <worktree-name> namespace register --retention 7
```

让 temporal 进程通过 **host 端口**连到现有 pg / 暴露 gRPC，跳过 compose 网络隔离。

### tctl 容器内调用细节

- `tctl --address temporal:7233` —— compose 网络内调用（同 stack 内可用）
- `tctl --address localhost:4033` —— host-network 模式调用（端口跟容器内部 7233 不同）
- 跨 worktree 共享场景：直接 `docker exec ffoa-wt-dingtalk-temporal tctl --address temporal:7233 ...`（compose 内部 DNS 跨 stack 用 host port）

### 注意：host-network 模式的副作用

- `--network host` **绕过 Docker 网络隔离**，容器直接用宿主网络栈
- Linux 才支持（macOS Docker Desktop 上有限制）
- 端口冲突要靠人工管理（compose 模式 docker 自动检测，host 模式不会）

**推荐**：除非有 compose 起不来的特殊情况，**优先用共享 Temporal Server**（§0）。host-network 是兜底。

参考 learning: [`.learnings/2026-05-03-temporal-host-network-bypass.md`](../../.learnings/2026-05-03-temporal-host-network-bypass.md)

---

## 4. workflow ID 命名约定

跨 namespace workflow ID **互不影响**，但同一 namespace 内必须唯一。约定：

```
<module>.<entity>.<id>           # 例：approval.instance.abc123
<module>.<entity>.<id>.<phase>   # 例：approval.instance.abc123.retry
```

**禁止**：

- ❌ 用纯 UUID（事后排查 `tctl workflow list` 看不出业务含义）
- ❌ 跨 module 共用前缀（如 `wf.123` 不带 module）—— 单 namespace 内会撞
- ❌ workflow ID 含时间戳（重启 / 重试时撞 dup ID）

---

## 5. 多 worktree 并行开发 Temporal 模块的 checklist

写新 Temporal workflow / 改现有 Temporal 模块前：

### 环境层

- [ ] 共享 Temporal Server 在跑（`docker ps | grep temporal`）
- [ ] 本 worktree namespace 已注册（`tctl --namespace <wt> namespace describe`）
- [ ] `backend/.env` 含 `TEMPORAL_ADDRESS` + `TEMPORAL_NAMESPACE` + `TEMPORAL_TASK_QUEUE`

### 代码层

- [ ] `Worker.create({ namespace, taskQueue, ... })` —— **namespace 必传**，跟 Client 一致
- [ ] workflow ID 命名 `<module>.<entity>.<id>`，不含纯 UUID 或时间戳
- [ ] activity 用模块前缀（`approvalActivities` / `meetingActivities`），不要全局命名
- [ ] task queue 用 `<worktree-name>-tasks` 或 `<module>-tasks`，避免多 worktree 撞

### 验证层

- [ ] 跑 backend 后用 `tctl --namespace <wt> taskqueue describe <queue> --type WORKFLOW` 看 Pollers 列表非空
- [ ] 触发一个 workflow 后 `tctl --namespace <wt> workflow list` 看见预期 ID
- [ ] **不要在多 worktree 间共用 namespace** —— 撞 ID 时调试痛苦

### 清理层（开发结束 / worktree release 前）

- [ ] 跑 `tctl --namespace <wt> namespace delete` 清理 namespace（可选，retention 到期自动清）
- [ ] 不要 `docker rm -f ffoa-wt-dingtalk-temporal` —— **共享容器**，删了所有 worktree 全瘫

---

## 6. 故障排查

| 现象 | 第一查 | 修法 |
|---|---|---|
| Worker 启动 OK 但 workflow 不推进 | `tctl --namespace <wt> taskqueue describe <queue>` Pollers 是否为空 | Worker.create 缺 namespace 参数（§2）|
| `tctl workflow list` 空 | Client / Worker namespace 是否一致 | 同上 |
| `Bind for 0.0.0.0:<port> failed` 起不来 | 是否多 worktree 都在起独立 Temporal | 走共享 Server（§0）或 host-network（§3） |
| `Connection refused 4033` | host port 跟容器内 port 不一致 | `--address localhost:4033`（host-network 暴露的）vs `temporal:7233`（compose 内）|
| 多 worktree workflow ID 撞 | 是否共用 namespace | 各 worktree 独立 namespace（§1 Step 2）|

更多日常操作（清理 stale workflow / 生产环境清理）见 [`temporal-ops/SKILL.md`](../../.agents/skills/temporal-ops/SKILL.md)。

---

## 7. 相关 learning

- [`.learnings/2026-04-30-temporal-worker-namespace-bug.md`](../../.learnings/2026-04-30-temporal-worker-namespace-bug.md)（Worker.create 缺 namespace）
- [`.learnings/2026-04-30-worktree-temporal-not-auto-started.md`](../../.learnings/2026-04-30-worktree-temporal-not-auto-started.md)（多 worktree 共享 Temporal Server 模式）
- [`.learnings/2026-05-03-temporal-host-network-bypass.md`](../../.learnings/2026-05-03-temporal-host-network-bypass.md)（host-network 绕行）

## 8. 配套阅读

- [`.agents/skills/temporal-ops/SKILL.md`](../../.agents/skills/temporal-ops/SKILL.md)（日常操作 / 清理 / 故障排查）
- [`docs/standards/02-backend-architecture.md`](./02-backend-architecture.md)（Temporal 在 backend 架构中的位置）
- [`docs/standards/10-agent-pool.md`](./10-agent-pool.md)（多 worktree 并行开发的资源协调）
