Files
bj_power/bj_power_mes/docs/superpowers/plans/2026-05-16-operation-step-plan.md
T

20 KiB
Raw Blame History

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_typeGenerator 根据 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

// 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() 集合
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_LoadStepType_MachiningStepType_WashStepType_DeburrStepType_RustWash

  • Step 3: 编译验证
go build ./...

预期:constants 包编译通过;references to deleted constants 的编译错误由后续 task 修复。

  • Step 4: Commit
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 代码,替换为:

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 代码

cd schema && go generate
  • Step 3: 清空 job 表(dev 环境无旧数据兼容)
DELETE FROM job;
  • Step 4: Commit
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 状态,记录位置:

// 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
// 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
git add internal/eventloop/dbstate.go
git commit -m "feat: 新增 SetJobProcessing / SetJobWaitingUnload"

Chunk 3: 调度器

Task 4: JobView 新增 RawStatusbuildJobViews 填充

Files:

  • Modify: internal/scheduler/types.go:14-21

  • Modify: internal/eventloop/scheduler_bridge.go:buildJobViews

  • Step 1: JobView 新增字段

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.gobuildJobViews 中,构建 JobView 时填充:

views = append(views, scheduler.JobView{
    // ...existing...
    State:     state,
    RawStatus: snap.Status, // 新增
    Context:   snap.Context,
})
  • Step 3: Commit
git add internal/scheduler/types.go internal/eventloop/scheduler_bridge.go
git commit -m "feat: JobView 新增 RawStatusbuildJobViews 填充"

Task 5: Generator 新增 OPERATION case

Files:

  • Modify: internal/scheduler/generator.go:66-86

  • Step 1: 删除旧的 LOAD/UNLOAD/MACHINING/WASH/... case,新增 OPERATION

替换 generateFromStep 中的 switch 分支:

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,
        })
    }

删除旧 caseStepType_LoadStepType_UnloadStepType_Machining/StepType_Wash/StepType_Deburr/StepType_RustWash

保留其他 caseStepType_BufferStageStepType_FinalScanStepType_Decision/StepType_JudgeStepType_LaserMark

  • Step 2: 编译验证
go build ./internal/scheduler/...
  • Step 3: Commit
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

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
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:

// 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 成功分流
// 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 上加一个辅助方法:

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 分流
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: 编译验证
go build ./internal/eventloop/...
  • Step 5: Commit
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 分支

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 中推进步骤分流
// 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
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 路径

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
git add internal/eventloop/loop.go
git commit -m "feat: handleInspectionDone OPERATION 步骤 pass→SetJobWaitingUnload"

Task 10: Unload 完成后保持 AdvanceStep

Unload 是 OPERATION 的出口,handleWorkerResulthandleMultiActionResult 中 Unload 成功的路径继续调用 AdvanceStep,不需要修改。确认现有代码正确即可。

  • Step 1: 验证 handleWorkerResult 中 Unload 路径不变
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 确认)
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
git commit --allow-empty -m "chore: 确认清洗机批量编排兼容 OPERATION"

Chunk 7: 编译、测试、清理

Task 12: 全量编译和修复编译错误

  • Step 1: 全量编译
go build ./...

预期:按 chunk 1-6 顺序实施后,此时应无编译错误。如有,逐个修复(主要是遗漏的常量引用)。

  • Step 2: 运行 eventloop 测试
go test ./internal/eventloop/ -count=1 -v
  • Step 3: 运行 scheduler 测试
go test ./internal/scheduler/ -count=1 -v
  • Step 4: Commit
git add -A
git commit -m "chore: 修复遗漏的编译错误,测试通过"

Task 13: Mock 运行联调

  • Step 1: 运行 mockrun 验证端到端流程
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: 搜索残留的已删除常量引用
rg "StepType_Load|StepType_Machining|StepType_Wash|StepType_Deburr|StepType_RustWash" --type go

确认全部清理干净或只在测试 mock 中有意保留。

  • Step 2: 删除未使用的 import 和函数
go vet ./...
  • Step 3: Commit
git add -A
git commit -m "chore: 清理死代码和未使用 import"