# OPERATION 复合步骤实现计划 > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 将 LOAD/EXECUTE/UNLOAD 三元组合并为 OPERATION 步骤,24 步 → 12 步 **Architecture:** 新增 OPERATION step_type,Generator 根据 job.RawStatus 分派 Load/MachineWait/Unload 候选;事件循环中 Load/MachineDone 不再调 AdvanceStep,改为 SetJobProcessing/SetJobWaitingUnload 原地改状态;Unload 完成后才 AdvanceStep 推进 **Tech Stack:** Go 1.25, ent ORM, PostgreSQL **Spec:** `docs/superpowers/specs/2026-05-16-operation-step-design.md` --- ## Chunk 1: 常量与 Schema 数据 ### Task 1: 更新 step_type 常量 **Files:** - Modify: `constants/constants.go:88-102` - [ ] **Step 1: 新增 OPERATION,删除 LOAD/MACHINING/WASH/DEBURR/RUST_WASH/INSPECTION** ```go // constants.go StepType 枚举 StepType_Operation StepType = "OPERATION" StepType_BufferStage StepType = "BUFFER_STAGE" StepType_Decision StepType = "DECISION" StepType_FinalScan StepType = "FINAL_SCAN" StepType_Inspection StepType = "INSPECTION" // 保留:SAMPLING 用 StepType_Judge StepType = "JUDGE" StepType_LaserMark StepType = "LASER_MARK" StepType_Unload StepType = "UNLOAD" ``` 注意:`StepType_Inspection` 保留——OP110 SAMPLING 的 resourceType 通过 `EquipmentTypeCode_Sampling` 区分,但 step_type 仍可为 OPERATION。`loadActionFor("SAMPLING")` 返回 `ActionLoadSampling`。 - [ ] **Step 2: 更新 `ValidStepTypes()` 集合** ```go func ValidStepTypes() []string { return []string{ string(StepType_Operation), string(StepType_BufferStage), string(StepType_Decision), string(StepType_FinalScan), string(StepType_Judge), string(StepType_LaserMark), string(StepType_Unload), } } ``` 移除 `StepType_Load`、`StepType_Machining`、`StepType_Wash`、`StepType_Deburr`、`StepType_RustWash`。 - [ ] **Step 3: 编译验证** ```bash go build ./... ``` 预期:constants 包编译通过;references to deleted constants 的编译错误由后续 task 修复。 - [ ] **Step 4: Commit** ```bash git add constants/constants.go git commit -m "refactor: 新增 StepType_Operation,删除 LOAD/MACHINING/WASH/DEBURR/RUST_WASH" ``` --- ### Task 2: 重写 recipe step 数据 **Files:** - Modify: `schema/tools/data.go` (recipe step 部分) - [ ] **Step 1: 删除旧 24 步,插入新 12 步** 定位 `schema/tools/data.go` 中 recipe step 的 bulk insert 代码,替换为: ```go steps := []struct { stepID, stepName, stepType, resourceType, nextStepDefault string stepIndex int nextStepBranches map[string]any processingParams map[string]any }{ {1, "OP10", "CNC加工", "OPERATION", string(constants.EquipmentTypeCode_CNC), "OP20"}, {2, "OP20", "高压清洗", "OPERATION", string(constants.EquipmentTypeCode_WashingMachine), "OP30"}, {3, "OP30", "内窥镜检测", "OPERATION", string(constants.EquipmentTypeCode_Inspection), "OP40"}, {4, "OP40", "检测判断", "JUDGE", "", "OP50", map[string]any{"MEASURE_NG": "OP120"}, map[string]any{"decisionType": "negate_bool", "setKey": "MEASURE_NG", "inputKey": "inspectionPass"}, }, {5, "OP50", "热能去毛刺", "OPERATION", string(constants.EquipmentTypeCode_Deburr), "OP60"}, {6, "OP60", "除锈清洗", "OPERATION", string(constants.EquipmentTypeCode_CleaningLine), "OP70"}, {7, "OP70", "终检扫码", "FINAL_SCAN", "", "OP80"}, {8, "OP80", "打标判断", "DECISION", "", "OP90", map[string]any{"SKIP_MARK": "OP120", "NEED_MARKING": "OP90"}, // NEED_MARKING 为 truthy → OP90, else → OP120 nil, // decisionType 由 FINAL_SCAN 步骤的 scan_mismatch 结果驱动 }, {9, "OP90", "激光打标", "LASER_MARK", "", "OP100"}, {10, "OP100", "抽检判断", "DECISION", "", "OP120", map[string]any{"NEED_SAMPLING": "OP110"}, map[string]any{"decisionType": "noop"}, // 规则由前置步骤 context 决定 }, {11, "OP110", "抽检", "OPERATION", string(constants.EquipmentTypeCode_Sampling), "OP120"}, {12, "OP120", "下料接驳台", "UNLOAD", "", ""}, } ``` **分支逻辑说明:** - OP40 (JUDGE): `inspectionPass=false` → nextStepBranches 匹配 `MEASURE_NG` → OP120(跳过后续,直接下料)。`inspectionPass=true` → nextStepDefault → OP50。 - OP80 (DECISION): `NEED_MARKING` 由 OP70 FINAL_SCAN 的 scan_mismatch 判定设置。存在 → OP90,不存在(skip)→ OP120。 - OP100 (DECISION): `NEED_SAMPLING` 由外部判定设置。存在 → OP110,不存在 → OP120。 - [ ] **Step 2: 重新生成 ent 代码** ```bash cd schema && go generate ``` - [ ] **Step 3: 清空 job 表(dev 环境无旧数据兼容)** ```sql DELETE FROM job; ``` - [ ] **Step 4: Commit** ```bash git add schema/tools/data.go schema/ ent/ git commit -m "refactor: recipe 步骤改为 12 步 OPERATION 模型" ``` --- ## Chunk 2: DBState 新增方法 ### Task 3: 新增 SetJobProcessing / SetJobWaitingUnload **Files:** - Modify: `internal/eventloop/dbstate.go` - [ ] **Step 1: 添加 SetJobProcessing** 在 `DBState` 上新增方法——设置 job 为 PROCESSING 状态,记录位置: ```go // SetJobProcessing 标记工件已上料到设备,开始加工 func (d *DBState) SetJobProcessing(ctx context.Context, jobID, equipmentID, slotNo int) error { _, err := d.client.Job.UpdateOneID(jobID). SetStatus(constants.JobStatus_Processing). SetPositionType(constants.PositionType_OnEquipment). SetPositionRefId(fmt.Sprintf("%d:%d", equipmentID, slotNo)). AddVersion(1). Save(ctx) if err != nil { return fmt.Errorf("set job %d processing: %w", jobID, err) } return nil } ``` - [ ] **Step 2: 添加 SetJobWaitingUnload** ```go // SetJobWaitingUnload 标记设备加工完成,等待机器人卸料 func (d *DBState) SetJobWaitingUnload(ctx context.Context, jobID int) error { _, err := d.client.Job.UpdateOneID(jobID). SetStatus(constants.JobStatus_WaitingUnload). AddVersion(1). Save(ctx) if err != nil { return fmt.Errorf("set job %d waiting unload: %w", jobID, err) } return nil } ``` - [ ] **Step 3: Commit** ```bash git add internal/eventloop/dbstate.go git commit -m "feat: 新增 SetJobProcessing / SetJobWaitingUnload" ``` --- ## Chunk 3: 调度器 ### Task 4: JobView 新增 RawStatus,buildJobViews 填充 **Files:** - Modify: `internal/scheduler/types.go:14-21` - Modify: `internal/eventloop/scheduler_bridge.go:buildJobViews` - [ ] **Step 1: JobView 新增字段** ```go type JobView struct { JobID int OrderID int ProductID int CurrentStep StepView State JobViewState RawStatus string // "ON_BUFFER" / "PROCESSING" / "WAITING_UNLOAD" Context map[string]any } ``` - [ ] **Step 2: buildJobViews 填充 RawStatus** 在 `scheduler_bridge.go` 的 `buildJobViews` 中,构建 JobView 时填充: ```go views = append(views, scheduler.JobView{ // ...existing... State: state, RawStatus: snap.Status, // 新增 Context: snap.Context, }) ``` - [ ] **Step 3: Commit** ```bash git add internal/scheduler/types.go internal/eventloop/scheduler_bridge.go git commit -m "feat: JobView 新增 RawStatus,buildJobViews 填充" ``` --- ### Task 5: Generator 新增 OPERATION case **Files:** - Modify: `internal/scheduler/generator.go:66-86` - [ ] **Step 1: 删除旧的 LOAD/UNLOAD/MACHINING/WASH/... case,新增 OPERATION** 替换 `generateFromStep` 中的 switch 分支: ```go case constants.StepType_Operation: switch job.RawStatus { case string(constants.JobStatus_OnBuffer): candidates = append(candidates, CandidateTask{ JobID: job.JobID, OrderID: job.OrderID, StepIndex: step.Index, Action: g.loadActionFor(step.ResourceType), TargetID: 0, // trySchedule 中 findIdleMachine 分配 Priority: PriorityNormal, }) case string(constants.JobStatus_Processing): candidates = append(candidates, CandidateTask{ JobID: job.JobID, OrderID: job.OrderID, StepIndex: step.Index, Action: ActionLoadCNC, // 占位 TargetID: step.TargetID, Priority: PriorityNormal, Meta: map[string]any{"machineWait": true}, }) case string(constants.JobStatus_WaitingUnload): candidates = append(candidates, CandidateTask{ JobID: job.JobID, OrderID: job.OrderID, StepIndex: step.Index, Action: g.unloadActionFor(step.ResourceType), TargetID: step.TargetID, Priority: PriorityHigh, }) } ``` 删除旧 case:`StepType_Load`、`StepType_Unload`、`StepType_Machining`/`StepType_Wash`/`StepType_Deburr`/`StepType_RustWash`。 保留其他 case:`StepType_BufferStage`、`StepType_FinalScan`、`StepType_Decision`/`StepType_Judge`、`StepType_LaserMark`。 - [ ] **Step 2: 编译验证** ```bash go build ./internal/scheduler/... ``` - [ ] **Step 3: Commit** ```bash git add internal/scheduler/generator.go git commit -m "feat: Generator 新增 OPERATION case,按 RawStatus 分派" ``` --- ### Task 6: findExchangePair 更新 **Files:** - Modify: `internal/eventloop/scheduler_bridge.go:332-353` (findExchangePair) - [ ] **Step 1: 将 `StepType_Load` 改为 `StepType_Operation && ON_BUFFER`** ```go func (l *ProductionEventLoop) findExchangePair(unloadSnap *RuntimeSnapshot, excludeLoadJobs ...map[int]bool) int { if unloadSnap == nil { return 0 } if unloadSnap.ResourceType != string(constants.EquipmentTypeCode_CNC) && unloadSnap.ResourceType != string(constants.EquipmentTypeCode_WashingMachine) && unloadSnap.ResourceType != string(constants.EquipmentTypeCode_CleaningLine) { return 0 } var excluded map[int]bool if len(excludeLoadJobs) > 0 { excluded = excludeLoadJobs[0] } for _, bufSnap := range l.jobRuntimes { if bufSnap.StepType == constants.StepType_Operation && bufSnap.Status == string(constants.JobStatus_OnBuffer) && bufSnap.PositionType == string(constants.PositionType_OnBuffer) && bufSnap.ResourceType == unloadSnap.ResourceType { if excluded != nil && excluded[bufSnap.JobID] { continue } return bufSnap.JobID } } return 0 } ``` - [ ] **Step 2: Commit** ```bash git add internal/eventloop/scheduler_bridge.go git commit -m "refactor: findExchangePair 改为匹配 StepType_Operation + ON_BUFFER" ``` --- ## Chunk 4: 事件循环 — Load/MachineDone 分支 ### Task 7: dispatchWorker Load 完成后分流 **Files:** - Modify: `internal/eventloop/worker_dispatch.go:100-160` (dispatchWorker Load 完成部分) - Modify: `internal/eventloop/loop.go:130-152` (handleWorkerResult Load 完成部分) - Modify: `internal/eventloop/worker_dispatch.go:313-350` (handleMultiActionResult load_to_machine case) 在三个位置,Load 完成后检测 stepType: - `dispatchWorker`: Load 成功后 `l.registry` 查槽位 → 如果是 OPERATION → `l.db.SetJobProcessing` - `handleWorkerResult`: 成功路径中 `act.IsLoad()` → 如果 stepType==OPERATION → 只改状态不调 AdvanceStep - `handleMultiActionResult`: `load_to_machine` case → 同样分流 - [ ] **Step 1: dispatchWorker 中 Load 成功后的槽位同步 + 状态更新** `dispatchWorker` 中 Load 成功后已经有从 Station 查槽位的逻辑。在此逻辑之后,检测 stepType: ```go // dispatchWorker 中,Load 成功后: if err == nil && l.registry != nil && ct.TargetID > 0 && ct.Action.IsLoad() { // ...existing slot query... if assignedSlot > 0 { l.db.SetEquipmentSlot(ctx, ct.TargetID, assignedSlot, Empty, Occupied, ct.JobID) } } // 新增:OPERATION 步骤不调 AdvanceStep snap := l.jobRuntimes[ct.JobID] if snap != nil && snap.StepType == constants.StepType_Operation && ct.Action.IsLoad() { l.db.SetJobProcessing(ctx, ct.JobID, ct.TargetID, assignedSlot) // 不调 AdvanceStep } else { // 原有 AdvanceStep 逻辑 } ``` 但注意:`dispatchWorker` 只是派遣,不更新 job 状态。状态更新在 `handleWorkerResult` 中。所以关键在于 `handleWorkerResult` 中 Load 成功的分支。 - [ ] **Step 2: handleWorkerResult 中 Load 成功分流** ```go // handleWorkerResult: success 路径中 Load 分支 case act.IsLoad(): if l.isOperationStep(jobID) { // OPERATION: 不调 AdvanceStep,只改状态 slot := intFromPayload(msg.Payload, "assignedSlot") if slot < 1 { slot = 1 } l.db.SetJobProcessing(ctx, jobID, targetID, slot) } else { // 原有逻辑:AdvanceStep newPosType = constants.PositionType_OnEquipment // ... l.db.AdvanceStep(ctx, jobID, ...) } ``` 需要在 `ProductionEventLoop` 上加一个辅助方法: ```go func (l *ProductionEventLoop) isOperationStep(jobID int) bool { snap := l.jobRuntimes[jobID] return snap != nil && snap.StepType == constants.StepType_Operation } ``` - [ ] **Step 3: handleMultiActionResult load_to_machine 分流** ```go case "load_to_machine": if snap != nil && snap.StepType == constants.StepType_Operation { l.db.SetJobProcessing(ctx, jobID, targetID, assignedSlot) } else { // 原有 AdvanceStep 逻辑 l.db.AdvanceStep(ctx, jobID, nil, OnEquipment, fmt.Sprintf("%d:%d", targetID, assignedSlot)) } ``` - [ ] **Step 4: 编译验证** ```bash go build ./internal/eventloop/... ``` - [ ] **Step 5: Commit** ```bash git add internal/eventloop/ git commit -m "feat: Load 完成后 OPERATION 步骤调用 SetJobProcessing" ``` --- ### Task 8: MachineDone 分流 **Files:** - Modify: `internal/eventloop/candidate_handlers.go:34-44` (handleMachineWaitCandidate) - Modify: `internal/eventloop/loop.go:327-341` (handleMachineDone 步骤推进部分) - [ ] **Step 1: handleMachineWaitCandidate OPERATION 分支** ```go func (l *ProductionEventLoop) handleMachineWaitCandidate(ctx context.Context, ct scheduler.CandidateTask) { // 检测设备不变 if l.inspectionMachines[ct.TargetID] { l.entClient.Job.UpdateOneID(ct.JobID).SetStatus(WaitingUnload).Save(ctx) return } // 检查 slot Done if ct.TargetID > 0 { slot, _ := l.entClient.EquipmentSlot.Query(). Where(equipmentslot.EquipmentIdEQ(ct.TargetID), equipmentslot.CurrentJobIdEQ(ct.JobID)).First(ctx) if slot == nil || slot.Status != constants.SlotStatus_Done { return } } // 分流 if l.isOperationStep(ct.JobID) { l.db.SetJobWaitingUnload(ctx, ct.JobID) } else { finished, err := l.db.AdvanceStep(ctx, ct.JobID, nil, "", "") if err != nil { slog.Error(...) return } if finished { l.checkOrderCompletion(ctx, ct.JobID) return } l.SendScheduleTick() } } ``` - [ ] **Step 2: handleMachineDone 中推进步骤分流** ```go // handleMachineDone 中 doneJobs 处理循环: for _, jobID := range doneJobs { job, _ := l.entClient.Job.Get(ctx, jobID) if job != nil && job.Status == constants.JobStatus_Processing { if l.isOperationStep(jobID) { l.db.SetJobWaitingUnload(ctx, jobID) } else { l.db.AdvanceStep(ctx, jobID, nil, "", "") l.entClient.Job.UpdateOneID(jobID).SetStatus(WaitingUnload).Save(ctx) } } if l.jobOps != nil { l.jobOps.WakeJob(jobID) } } ``` - [ ] **Step 3: Commit** ```bash git add internal/eventloop/ git commit -m "feat: MachineDone 时 OPERATION 步骤调用 SetJobWaitingUnload" ``` --- ## Chunk 5: 事件循环 — InspectionResult / Unload ### Task 9: handleInspectionDone OPERATION 分流 **Files:** - Modify: `internal/eventloop/loop.go:377-413` - [ ] **Step 1: 分流 pass 路径** ```go func (l *ProductionEventLoop) handleInspectionDone(ctx context.Context, machineID int, pass bool) { // ... slot/job 查询不变 ... if pass { if l.isOperationStep(jobID) { l.db.CompleteStep(ctx, jobID, job.CurrentStepIndex, map[string]any{"inspectionPass": true}) l.db.SetJobWaitingUnload(ctx, jobID) } else { finished, err := l.db.AdvanceStep(ctx, jobID, map[string]any{"inspectionPass": true}, "", "") if err != nil { ... } if finished { l.checkOrderCompletion(ctx, jobID) } } } else { // 不合格路径不变 ... } } ``` 注意:pass=true 时需要用 `CompleteStep` 先记录 `inspectionPass` 上下文(不推进 stepIndex),因为 `SetJobWaitingUnload` 不更新 context。 - [ ] **Step 2: Commit** ```bash git add internal/eventloop/loop.go git commit -m "feat: handleInspectionDone OPERATION 步骤 pass→SetJobWaitingUnload" ``` --- ### Task 10: Unload 完成后保持 AdvanceStep Unload 是 OPERATION 的出口,`handleWorkerResult` 和 `handleMultiActionResult` 中 Unload 成功的路径继续调用 `AdvanceStep`,不需要修改。确认现有代码正确即可。 - [ ] **Step 1: 验证 handleWorkerResult 中 Unload 路径不变** ```bash grep -n "IsUnload\|ActionExchange\|ActionUnloadDock" internal/eventloop/worker_dispatch.go ``` 确认这些路径的 `AdvanceStep` 调用不受影响。 - [ ] **Step 2: 验证 handleMultiActionResult 中 unload_to_buffer/exchange_full/unload_to_dock 路径不变** 同上。 - [ ] **Step 3: Commit (no-op 确认)** ```bash git commit --allow-empty -m "chore: 确认 Unload 完成后 AdvanceStep 路径不受 OPERATION 影响" ``` --- ## Chunk 6: 清洗机批量编排适配 ### Task 11: Washer batch 中 Load/MachineDone 分流 **Files:** - Modify: `internal/eventloop/washer_orchestrator.go` - Modify: `internal/eventloop/worker_dispatch.go:handleMultiActionResult` - [ ] **Step 1: 确认 buildWasherLoadActions 不变** 清洗机 Load 编排逻辑不变——仍然一次机器人行程上两个工件。变化在 Load 完成后的状态更新(已在 Task 7 handleMultiActionResult 中处理)。 - [ ] **Step 2: 确认 buildWasherUnloadActions 不变** 清洗机 Unload 编排不变。Unload 完成后 AdvanceStep(已在 Task 10 确认不受影响)。 - [ ] **Step 3: Commit** ```bash git commit --allow-empty -m "chore: 确认清洗机批量编排兼容 OPERATION" ``` --- ## Chunk 7: 编译、测试、清理 ### Task 12: 全量编译和修复编译错误 - [ ] **Step 1: 全量编译** ```bash go build ./... ``` 预期:按 chunk 1-6 顺序实施后,此时应无编译错误。如有,逐个修复(主要是遗漏的常量引用)。 - [ ] **Step 2: 运行 eventloop 测试** ```bash go test ./internal/eventloop/ -count=1 -v ``` - [ ] **Step 3: 运行 scheduler 测试** ```bash go test ./internal/scheduler/ -count=1 -v ``` - [ ] **Step 4: Commit** ```bash git add -A git commit -m "chore: 修复遗漏的编译错误,测试通过" ``` --- ### Task 13: Mock 运行联调 - [ ] **Step 1: 运行 mockrun 验证端到端流程** ```bash go run cmd/mockrun/main.go ``` 观察: - OPERATION 步骤 Load → PROCESSING → MachineDone → WAITING_UNLOAD → Unload → AdvanceStep - 检测步骤 InspectionResult 路径 - 清洗机批量编排 - [ ] **Step 2: 验证 Exchange 配对** 观察 mockrun 日志中 CNC 换料是否正常。 - [ ] **Step 3: Commit 任何 mockrun 发现的问题修复** --- ### Task 14: 清理死代码 - [ ] **Step 1: 搜索残留的已删除常量引用** ```bash rg "StepType_Load|StepType_Machining|StepType_Wash|StepType_Deburr|StepType_RustWash" --type go ``` 确认全部清理干净或只在测试 mock 中有意保留。 - [ ] **Step 2: 删除未使用的 import 和函数** ```bash go vet ./... ``` - [ ] **Step 3: Commit** ```bash git add -A git commit -m "chore: 清理死代码和未使用 import" ```