32 KiB
Machine Actor 实现计划
For agentic workers: REQUIRED: Use superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 用 Actor 模型(goroutine + channel)替换 Station/SignalWatcher/TempSlotAllocator,每个设备自治管理槽位状态。
Architecture: 新建 internal/actor/ 包,包含 MachineActor、TempStoreActor、SignalRouter 和共享类型。EventLoop 移除 equipment_slot 写入逻辑,调度器从 Actor 快照读设备/暂存台状态。
Tech Stack: Go 1.25, ent ORM, go-zero, PostgreSQL
Spec: docs/superpowers/specs/2026-05-16-machine-actor-design.md
文件结构
| 文件 | 操作 | 职责 |
|---|---|---|
internal/actor/types.go |
新建 | 共享类型(MachineMsg, MachineSnapshot, ActorStatus, SignalEvent) |
internal/actor/signal_router.go |
新建 | SignalRouter 集中 PLC 轮询 + 路由 |
internal/actor/machine_actor.go |
新建 | MachineActor 状态机 |
internal/actor/machine_actor_test.go |
新建 | MachineActor 单元测试 |
internal/actor/tempstore_actor.go |
新建 | TempStoreActor 暂存台管理 |
internal/actor/tempstore_actor_test.go |
新建 | TempStoreActor 单元测试 |
internal/eventloop/dbstate.go |
修改 | 新增 SetJobTempSlot / ClearJobTempSlot |
internal/eventloop/loop.go |
修改 | 移除 updateStationDone/batchMachines/inspectionMachines;handleMachineDone 精简 |
internal/eventloop/scheduler_bridge.go |
修改 | buildSystemState 改读 Actor 快照 |
internal/eventloop/worker_dispatch.go |
修改 | 上料/下料完成后通知 Actor |
internal/svc/service_context.go |
修改 | 用 Actor 替换 Station/SignalWatcher |
internal/station/*.go |
删除 | 保留 HandheldTool 接口移到新位置 |
internal/processor/signal_watcher.go |
删除 | 被 SignalRouter 替代 |
internal/processor/replenisher.go |
修改 | 暂存台查询改为读 TempStoreActor 快照 |
Chunk 1: Actor 类型 + DBState 扩展
Task 1.1: 定义 Actor 共享类型
Files:
-
Create:
internal/actor/types.go -
Step 1: 创建 types.go
package actor
import "time"
// ActorStatus 工站状态
type ActorStatus string
const (
ActorIdle ActorStatus = "Idle"
ActorProcessing ActorStatus = "Processing"
ActorFull ActorStatus = "Full"
ActorWaiting ActorStatus = "Waiting"
ActorFault ActorStatus = "Fault"
)
// SlotStatus 槽位状态
type SlotStatus string
const (
SlotEmpty SlotStatus = "Empty"
SlotOccupied SlotStatus = "Occupied"
SlotDone SlotStatus = "Done"
)
// MachineMsg 外部发给 Actor 的命令
type MachineMsg struct {
Type string // LOAD_COMPLETE / UNLOAD_COMPLETE / RESTORE / ALLOCATE / RELEASE
JobID int
SlotNo int
Params map[string]any
}
// MachineSnapshot 设备 Actor 只读快照
type MachineSnapshot struct {
ID int
Type string
Status ActorStatus
Slots []SlotSnapshot
UpdatedAt time.Time
}
// SlotSnapshot 槽位快照
type SlotSnapshot struct {
SlotNo int
Status SlotStatus
JobID int
OccupiedAt time.Time
}
// TempStoreSnapshot 暂存台快照
type TempStoreSnapshot struct {
Capacity int
Slots []int // slotNo-1 → jobID (0 = 空), 复制
}
// SignalEvent PLC 信号事件
type SignalEvent struct {
Type SignalType
Value bool
}
type SignalType int
const (
SignalDone SignalType = iota
SignalNG
)
- Step 2: 验证编译
go build ./internal/actor/...
Task 1.2: DBState 新增暂存台 DB 方法
Files:
-
Modify:
internal/eventloop/dbstate.go -
Step 1: 添加 SetJobTempSlot / ClearJobTempSlot
在 internal/eventloop/dbstate.go 末尾添加:
// SetJobTempSlot 设置 job.temp_slot_no
func (d *DBState) SetJobTempSlot(ctx context.Context, jobID, slotNo int) error {
_, err := d.client.Job.UpdateOneID(jobID).
SetTempSlotNo(slotNo).
AddVersion(1).
Save(ctx)
if err != nil {
return fmt.Errorf("set job %d temp slot %d: %w", jobID, slotNo, err)
}
return nil
}
// ClearJobTempSlot 清除 job.temp_slot_no
func (d *DBState) ClearJobTempSlot(ctx context.Context, jobID int) error {
_, err := d.client.Job.UpdateOneID(jobID).
ClearTempSlotNo().
AddVersion(1).
Save(ctx)
if err != nil {
return fmt.Errorf("clear job %d temp slot: %w", jobID, err)
}
return nil
}
- Step 2: 验证编译和已有测试
go build ./...
go test ./internal/eventloop/...
- Step 3: 提交
git add internal/actor/types.go internal/eventloop/dbstate.go
git commit -m "feat: add Actor types and DBState temp slot methods"
Chunk 2: SignalRouter
Task 2.1: 实现 SignalRouter
Files:
-
Create:
internal/actor/signal_router.go -
Step 1: 实现 SignalRouter
package actor
import (
"context"
"sync"
goplc "bjhardman.cn/bjhardman/goplc"
)
type SignalRouter struct {
plc goplc.Client
mu sync.RWMutex
watches map[int]*SignalWatch
}
type SignalWatch struct {
DoneAddr string
NGAddr string
Ch chan SignalEvent
}
func NewSignalRouter(plc goplc.Client) *SignalRouter {
return &SignalRouter{
plc: plc,
watches: make(map[int]*SignalWatch),
}
}
// Watch 注册设备信号监听,返回 Actor 用于接收信号的 channel
func (r *SignalRouter) Watch(machineID int, doneAddr, ngAddr string) chan SignalEvent {
r.mu.Lock()
defer r.mu.Unlock()
ch := make(chan SignalEvent, 1)
r.watches[machineID] = &SignalWatch{
DoneAddr: doneAddr,
NGAddr: ngAddr,
Ch: ch,
}
return ch
}
// Run 轮询 PLC 信号,阻塞直到 ctx 取消
func (r *SignalRouter) Run(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
lastState := make(map[int]bool) // machineID → 上次Done值
lastNG := make(map[int]bool) // -machineID → 上次NG值
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
r.poll(lastState, lastNG)
}
}
}
func (r *SignalRouter) poll(lastState, lastNG map[int]bool) {
if r.plc == nil || !r.plc.IsConnected() {
return
}
r.mu.RLock()
defer r.mu.RUnlock()
for machineID, w := range r.watches {
if w.DoneAddr == "" {
continue
}
current, err := r.plc.ReadBool(w.DoneAddr)
if err != nil {
continue
}
prev := lastState[machineID]
lastState[machineID] = current
// 上升沿
if current && !prev {
r.sendNonBlock(w.Ch, SignalEvent{Type: SignalDone, Value: true})
}
// NG 信号
if w.NGAddr != "" {
ng, err := r.plc.ReadBool(w.NGAddr)
if err != nil {
continue
}
key := -machineID
ngPrev := lastNG[key]
lastNG[key] = ng
if ng && !ngPrev {
r.sendNonBlock(w.Ch, SignalEvent{Type: SignalNG, Value: true})
}
}
}
}
func (r *SignalRouter) sendNonBlock(ch chan SignalEvent, evt SignalEvent) {
select {
case ch <- evt:
default:
// 缓冲区满,丢弃(表示上一事件未被消费)
}
}
- Step 2: 验证编译
go build ./internal/actor/...
Chunk 3: MachineActor
Task 3.1: 实现 MachineActor
Files:
-
Create:
internal/actor/machine_actor.go -
Create:
internal/actor/machine_actor_test.go -
Step 1: 写失败测试
// internal/actor/machine_actor_test.go
package actor
import (
"context"
"testing"
"time"
)
func TestMachineActor_IdleToProcessing(t *testing.T) {
cfg := MachineActorConfig{
ID: 1,
Type: "CNC",
Capacity: 2,
SignalCh: make(chan SignalEvent),
}
actor := NewMachineActor(cfg)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go actor.Run(ctx)
// Load first job
actor.Send(MachineMsg{Type: "LOAD_COMPLETE", JobID: 100, SlotNo: 1})
time.Sleep(10 * time.Millisecond)
snap := actor.Snapshot()
if snap.Status != ActorProcessing {
t.Fatalf("expected Processing, got %s", snap.Status)
}
if snap.Slots[0].Status != SlotOccupied || snap.Slots[0].JobID != 100 {
t.Fatalf("expected slot 1 Occupied with job 100, got %v", snap.Slots[0])
}
}
func TestMachineActor_FullOnAllOccupied(t *testing.T) {
cfg := MachineActorConfig{
ID: 2,
Type: "CNC",
Capacity: 1,
SignalCh: make(chan SignalEvent),
}
actor := NewMachineActor(cfg)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go actor.Run(ctx)
actor.Send(MachineMsg{Type: "LOAD_COMPLETE", JobID: 200, SlotNo: 1})
time.Sleep(10 * time.Millisecond)
snap := actor.Snapshot()
if snap.Status != ActorFull {
t.Fatalf("expected Full for 1-slot CNC, got %s", snap.Status)
}
}
func TestMachineActor_DoneSignal(t *testing.T) {
signalCh := make(chan SignalEvent, 1)
cfg := MachineActorConfig{
ID: 3,
Type: "CNC",
Capacity: 1,
SignalCh: signalCh,
}
actor := NewMachineActor(cfg)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go actor.Run(ctx)
actor.Send(MachineMsg{Type: "LOAD_COMPLETE", JobID: 300, SlotNo: 1})
time.Sleep(10 * time.Millisecond)
// Simulate PLC done signal
signalCh <- SignalEvent{Type: SignalDone, Value: true}
time.Sleep(10 * time.Millisecond)
snap := actor.Snapshot()
if snap.Status != ActorWaiting {
t.Fatalf("expected Waiting, got %s", snap.Status)
}
if snap.Slots[0].Status != SlotDone {
t.Fatalf("expected slot Done, got %s", snap.Slots[0].Status)
}
}
func TestMachineActor_BatchDoneSignal(t *testing.T) {
signalCh := make(chan SignalEvent, 1)
cfg := MachineActorConfig{
ID: 5,
Type: "WASHER_HP",
Capacity: 2,
Batch: true,
SignalCh: signalCh,
}
actor := NewMachineActor(cfg)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go actor.Run(ctx)
actor.Send(MachineMsg{Type: "LOAD_COMPLETE", JobID: 400, SlotNo: 1})
actor.Send(MachineMsg{Type: "LOAD_COMPLETE", JobID: 401, SlotNo: 2})
time.Sleep(10 * time.Millisecond)
signalCh <- SignalEvent{Type: SignalDone, Value: true}
time.Sleep(10 * time.Millisecond)
snap := actor.Snapshot()
if snap.Slots[0].Status != SlotDone {
t.Fatalf("expected slot 1 Done, got %s", snap.Slots[0].Status)
}
if snap.Slots[1].Status != SlotDone {
t.Fatalf("expected slot 2 Done, got %s", snap.Slots[1].Status)
}
}
- Step 2: 运行测试验证失败
go test ./internal/actor/... -run TestMachineActor -v
Expected: FAIL (MachineActor not defined)
- Step 3: 实现 MachineActor
// internal/actor/machine_actor.go
package actor
import (
"context"
"sync/atomic"
"time"
"hougai/constants"
"hougai/internal/eventbus"
)
type MachineActorConfig struct {
ID int
Type string
Capacity int
Batch bool
Inspection bool
SignalCh chan SignalEvent
DB interface { // DBState 的子集,避免循环依赖
SetEquipmentSlot(ctx context.Context, equipmentID, slotNo int, expected, next constants.SlotStatus, jobID int) error
}
Bus eventbus.Bus
}
type machineActor struct {
id int
typ string
capacity int
batch bool
inspection bool
signalCh chan SignalEvent
msgCh chan MachineMsg
slots []SlotStatus
slotJobs []int
slotOccupiedAt []time.Time
status atomic.Value // ActorStatus
db interface {
SetEquipmentSlot(ctx context.Context, equipmentID, slotNo int, expected, next constants.SlotStatus, jobID int) error
}
bus eventbus.Bus
}
func NewMachineActor(cfg MachineActorConfig) *machineActor {
slots := make([]SlotStatus, cfg.Capacity)
slotJobs := make([]int, cfg.Capacity)
slotOccupiedAt := make([]time.Time, cfg.Capacity)
for i := range slots {
slots[i] = SlotEmpty
}
a := &machineActor{
id: cfg.ID,
typ: cfg.Type,
capacity: cfg.Capacity,
batch: cfg.Batch,
inspection: cfg.Inspection,
signalCh: cfg.SignalCh,
msgCh: make(chan MachineMsg, 8),
slots: slots,
slotJobs: slotJobs,
slotOccupiedAt: slotOccupiedAt,
db: cfg.DB,
bus: cfg.Bus,
}
a.status.Store(ActorIdle)
return a
}
func (m *machineActor) ID() int { return m.id }
func (m *machineActor) Type() string { return m.typ }
func (m *machineActor) Send(msg MachineMsg) {
select {
case m.msgCh <- msg:
default:
}
}
func (m *machineActor) Snapshot() MachineSnapshot {
slots := make([]SlotSnapshot, m.capacity)
for i := range m.slots {
slots[i] = SlotSnapshot{
SlotNo: i + 1,
Status: m.slots[i],
JobID: m.slotJobs[i],
OccupiedAt: m.slotOccupiedAt[i],
}
}
return MachineSnapshot{
ID: m.id,
Type: m.typ,
Status: m.status.Load().(ActorStatus),
Slots: slots,
UpdatedAt: time.Now(),
}
}
func (m *machineActor) Run(ctx context.Context) {
for {
select {
case msg := <-m.msgCh:
m.handleMessage(ctx, msg)
case evt := <-m.signalCh:
m.handleSignal(ctx, evt)
case <-ctx.Done():
return
}
}
}
func (m *machineActor) handleMessage(ctx context.Context, msg MachineMsg) {
switch msg.Type {
case "LOAD_COMPLETE":
m.slots[msg.SlotNo-1] = SlotOccupied
m.slotJobs[msg.SlotNo-1] = msg.JobID
m.slotOccupiedAt[msg.SlotNo-1] = time.Now()
m.updateStatus()
if m.db != nil {
m.db.SetEquipmentSlot(ctx, m.id, msg.SlotNo, constants.SlotStatus_Empty, constants.SlotStatus_Occupied, msg.JobID)
}
case "UNLOAD_COMPLETE":
if m.db != nil {
m.db.SetEquipmentSlot(ctx, m.id, msg.SlotNo, constants.SlotStatus_Done, constants.SlotStatus_Empty, 0)
}
m.slots[msg.SlotNo-1] = SlotEmpty
m.slotJobs[msg.SlotNo-1] = 0
m.slotOccupiedAt[msg.SlotNo-1] = time.Time{}
m.updateStatus()
case "RESTORE":
m.slots[msg.SlotNo-1] = parseSlotStatus(msg.Params["status"])
m.slotJobs[msg.SlotNo-1] = msg.JobID
m.updateStatus()
}
}
func (m *machineActor) handleSignal(ctx context.Context, evt SignalEvent) {
if evt.Type == SignalNG && evt.Value && m.inspection {
if m.bus != nil {
m.bus.Publish(eventbus.Event{
Type: eventbus.EventInspectionResult,
Data: map[string]any{"machineId": m.id, "pass": false},
})
}
return
}
if evt.Type == SignalDone && evt.Value {
if m.inspection {
for i := range m.slots {
if m.slots[i] == SlotOccupied {
m.slots[i] = SlotDone
if m.db != nil {
m.db.SetEquipmentSlot(ctx, m.id, i+1, constants.SlotStatus_Occupied, constants.SlotStatus_Done, m.slotJobs[i])
}
}
}
m.updateStatus()
if m.bus != nil {
m.bus.Publish(eventbus.Event{
Type: eventbus.EventInspectionResult,
Data: map[string]any{"machineId": m.id, "pass": true},
})
}
return
}
var doneSlots []int
if m.batch {
for i := range m.slots {
if m.slots[i] == SlotOccupied {
m.slots[i] = SlotDone
doneSlots = append(doneSlots, i+1)
}
}
} else {
slotNo := m.findFirstOccupied()
if slotNo > 0 {
m.slots[slotNo-1] = SlotDone
doneSlots = append(doneSlots, slotNo)
}
}
m.updateStatus()
for _, slotNo := range doneSlots {
if m.db != nil {
m.db.SetEquipmentSlot(ctx, m.id, slotNo, constants.SlotStatus_Occupied, constants.SlotStatus_Done, m.slotJobs[slotNo-1])
}
}
if m.bus != nil {
for _, slotNo := range doneSlots {
m.bus.Publish(eventbus.Event{
Type: eventbus.EventMachineDone,
Data: map[string]any{"machineId": m.id, "slotNo": slotNo, "jobId": m.slotJobs[slotNo-1]},
})
}
}
}
}
func (m *machineActor) findFirstOccupied() int {
earliest := time.Time{}
earliestSlot := 0
for i, s := range m.slots {
if s != SlotOccupied {
continue
}
if t := m.slotOccupiedAt[i]; earliestSlot == 0 || t.Before(earliest) {
earliest = t
earliestSlot = i + 1
}
}
return earliestSlot
}
func (m *machineActor) updateStatus() {
hasEmpty := false
hasOccupied := false
hasDone := false
for _, s := range m.slots {
switch s {
case SlotEmpty:
hasEmpty = true
case SlotOccupied:
hasOccupied = true
case SlotDone:
hasDone = true
}
}
var s ActorStatus
switch {
case !hasOccupied && !hasDone:
s = ActorIdle
case hasDone && !hasOccupied:
s = ActorWaiting
case !hasEmpty:
s = ActorFull
default:
s = ActorProcessing
}
m.status.Store(s)
}
func parseSlotStatus(s string) SlotStatus {
if s == "OCCUPIED" {
return SlotOccupied
}
if s == "DONE" {
return SlotDone
}
return SlotEmpty
}
- Step 4: 运行测试
go test ./internal/actor/... -run TestMachineActor -v
Expected: PASS (4 tests)
- Step 5: 提交
git add internal/actor/machine_actor.go internal/actor/machine_actor_test.go
git commit -m "feat: add MachineActor with state machine and PLC signal handling"
Chunk 4: TempStoreActor
Task 4.1: 实现 TempStoreActor
Files:
-
Create:
internal/actor/tempstore_actor.go -
Create:
internal/actor/tempstore_actor_test.go -
Step 1: 写失败测试
// internal/actor/tempstore_actor_test.go
package actor
import (
"context"
"testing"
"time"
)
func TestTempStoreActor_AllocateRelease(t *testing.T) {
actor := NewTempStoreActor(8, nil)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go actor.Run(ctx)
actor.Send(MachineMsg{Type: "ALLOCATE", JobID: 100})
time.Sleep(10 * time.Millisecond)
snap := actor.Snapshot()
found := false
for slotNo, jobID := range snap.Slots {
if jobID == 100 {
if slotNo+1 < 1 || slotNo+1 > 8 {
t.Fatalf("invalid slot %d", slotNo+1)
}
found = true
break
}
}
if !found {
t.Fatalf("job 100 not allocated in temp store")
}
actor.Send(MachineMsg{Type: "RELEASE", JobID: 100})
time.Sleep(10 * time.Millisecond)
snap = actor.Snapshot()
for _, jobID := range snap.Slots {
if jobID == 100 {
t.Fatalf("job 100 should have been released")
}
}
}
func TestTempStoreActor_Full(t *testing.T) {
actor := NewTempStoreActor(2, nil)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go actor.Run(ctx)
actor.Send(MachineMsg{Type: "ALLOCATE", JobID: 1})
actor.Send(MachineMsg{Type: "ALLOCATE", JobID: 2})
time.Sleep(10 * time.Millisecond)
snap := actor.Snapshot()
free := 0
for _, jobID := range snap.Slots {
if jobID == 0 {
free++
}
}
if free != 0 {
t.Fatalf("expected 0 free slots, got %d", free)
}
}
- Step 2: 运行测试验证失败
go test ./internal/actor/... -run TestTempStore -v
Expected: FAIL
- Step 3: 实现 TempStoreActor
// internal/actor/tempstore_actor.go
package actor
import "context"
type TempStoreActor struct {
capacity int
slots []int
msgCh chan MachineMsg
db interface {
SetJobTempSlot(ctx context.Context, jobID, slotNo int) error
ClearJobTempSlot(ctx context.Context, jobID int) error
}
}
func NewTempStoreActor(capacity int, db interface {
SetJobTempSlot(ctx context.Context, jobID, slotNo int) error
ClearJobTempSlot(ctx context.Context, jobID int) error
}) *TempStoreActor {
return &TempStoreActor{
capacity: capacity,
slots: make([]int, capacity),
msgCh: make(chan MachineMsg, 8),
db: db,
}
}
func (t *TempStoreActor) Send(msg MachineMsg) {
select {
case t.msgCh <- msg:
default:
}
}
func (t *TempStoreActor) Snapshot() TempStoreSnapshot {
slots := make([]int, t.capacity)
copy(slots, t.slots)
return TempStoreSnapshot{
Capacity: t.capacity,
Slots: slots,
}
}
func (t *TempStoreActor) Run(ctx context.Context) {
for {
select {
case msg := <-t.msgCh:
switch msg.Type {
case "ALLOCATE":
t.allocate(ctx, msg.JobID)
case "RELEASE":
t.release(ctx, msg.JobID)
case "RESTORE":
t.slots[msg.SlotNo-1] = msg.JobID
}
case <-ctx.Done():
return
}
}
}
func (t *TempStoreActor) allocate(ctx context.Context, jobID int) {
for i := range t.slots {
if t.slots[i] == 0 {
t.slots[i] = jobID
if t.db != nil {
t.db.SetJobTempSlot(ctx, jobID, i+1)
}
return
}
}
}
func (t *TempStoreActor) release(ctx context.Context, jobID int) {
for i := range t.slots {
if t.slots[i] == jobID {
t.slots[i] = 0
if t.db != nil {
t.db.ClearJobTempSlot(ctx, jobID)
}
return
}
}
}
- Step 4: 运行测试
go test ./internal/actor/... -run TestTempStore -v
Expected: PASS (2 tests)
- Step 5: 提交
git add internal/actor/tempstore_actor.go internal/actor/tempstore_actor_test.go
git commit -m "feat: add TempStoreActor for temp slot allocation"
Chunk 5: EventLoop 精简
Task 5.1: 更新 ProductionEventLoop 结构
Files:
-
Modify:
internal/eventloop/loop.go -
Step 1: 替换 struct 字段
将 registry, batchMachines, inspectionMachines 替换为:
machineActors map[int]actor.MachineActor // 设备 Actor map
tempStoreActor *actor.TempStoreActor // 暂存台 Actor
更新 NewProductionEventLoop 签名,接收 machineActors 和 tempStoreActor 替代旧的三个参数。
- Step 2: 删除
updateStationDone方法
删除 loop.go:363-386 的 updateStationDone 方法。
- Step 3: 精简
handleMachineDone
移除 DB equipment_slot 更新(Actor 已写)、移除 updateStationDone 调用、移除 batch/non-batch slot 查找。精简为:
func (l *ProductionEventLoop) handleMachineDone(ctx context.Context, msg EventLoopMessage) {
machineID := intFromPayload(msg.Payload, "machineId")
if machineID == 0 {
return
}
// 检测设备走 handleInspectionDone
if l.inspectionActors[machineID] {
return
}
slotNo := intFromPayload(msg.Payload, "slotNo")
jobID := intFromPayload(msg.Payload, "jobId")
job, err := l.entClient.Job.Get(ctx, jobID)
if err != nil || job.Status != constants.JobStatus_Processing {
return
}
if l.isOperationStep(jobID) {
l.db.SetJobWaitingUnload(ctx, jobID)
} else {
l.db.AdvanceStep(ctx, jobID, nil, "", "")
l.entClient.Job.UpdateOneID(jobID).
SetStatus(constants.JobStatus_WaitingUnload).
Save(ctx)
}
if l.jobOps != nil {
l.jobOps.WakeJob(jobID)
}
l.trySchedule(ctx)
}
- Step 4: 处理 Worker 结果时通知 Actor
在 handleWorkerResult 的 Load 成功路径添加:
// 通知 Actor 上料完成
actor, ok := l.machineActors[targetID]
if ok {
actor.Send(actor.MachineMsg{
Type: "LOAD_COMPLETE",
JobID: jobID,
SlotNo: slotNo,
})
}
在 Unload 成功路径添加:
// 通知 Actor 下料完成
actor, ok := l.machineActors[targetID]
if ok {
actor.Send(actor.MachineMsg{
Type: "UNLOAD_COMPLETE",
JobID: jobID,
SlotNo: slotNo,
})
}
- Step 5: 验证编译
go build ./internal/eventloop/...
Task 5.2: 更新调度桥接
Files:
-
Modify:
internal/eventloop/scheduler_bridge.go -
Step 1: 更新
buildSystemState
改为从 Actor 快照读:
func (l *ProductionEventLoop) buildSystemState(ctx context.Context) scheduler.SystemState {
state := scheduler.SystemState{
MachineBusy: make(map[int]bool),
MachineHasJob: make(map[int]int),
TempSlotJobs: make(map[int]int),
}
for _, a := range l.machineActors {
snap := a.Snapshot()
full := snap.Status == actor.ActorFull || snap.Status == actor.ActorWaiting
state.MachineBusy[snap.ID] = full
for _, slot := range snap.Slots {
if slot.JobID > 0 && slot.Status != actor.SlotEmpty {
state.MachineHasJob[snap.ID] = slot.JobID
}
}
}
// 暂存台
if l.tempStoreActor != nil {
tsSnap := l.tempStoreActor.Snapshot()
occupied := 0
for slotNo, jobID := range tsSnap.Slots {
if jobID > 0 {
state.TempSlotJobs[slotNo+1] = jobID
occupied++
}
}
state.TempSlotFree = tsSnap.Capacity - occupied
}
// ... 后续逻辑不变(OrderPaused, JobSuspended)
return state
}
- Step 2: 更新
findIdleMachine
改为从 Actor 快照查:
func (l *ProductionEventLoop) findIdleMachine(resourceType string, productTypeID int) int {
for _, a := range l.machineActors {
snap := a.Snapshot()
if snap.Type != resourceType {
continue
}
if snap.Status == actor.ActorFault {
continue
}
// 有空槽位即可接受
for _, slot := range snap.Slots {
if slot.Status == actor.SlotEmpty {
return snap.ID
}
}
}
return 0
}
- Step 3: 移除
findDoneJobIDsOnMachineDB 查询
func (l *ProductionEventLoop) findDoneJobIDsOnMachine(ctx context.Context, machineID int) []int {
a, ok := l.machineActors[machineID]
if !ok {
return nil
}
snap := a.Snapshot()
var ids []int
for _, slot := range snap.Slots {
if slot.Status == actor.SlotDone && slot.JobID > 0 {
ids = append(ids, slot.JobID)
}
}
return ids
}
- Step 4: 验证编译和已有测试
go build ./...
go test ./internal/eventloop/... -v
- Step 5: 提交
git add internal/eventloop/
git commit -m "refactor: EventLoop reads from Actor snapshots, removes equipment_slot writes"
Chunk 6: ServiceContext 组装
Task 6.1: 更新服务初始化
Files:
-
Modify:
internal/svc/service_context.go -
Step 1: 创建 Actor 并在 ServiceContext 中替换 Station/SignalWatcher
// 替换 BuildProductionLine 调用为 BuildMachineActors
import "hougai/internal/actor"
// 创建 SignalRouter
signalRouter := actor.NewSignalRouter(plcManager.GetPlc())
// 从 DB 构建信号映射并注册
doneSignals, ngSignals := buildDoneSignalMaps(ctx, entClient)
for machineID, addr := range doneSignals {
ngAddr := ngSignals[machineID]
signalRouter.Watch(machineID, addr, ngAddr)
}
// 创建 MachineActor
machineActors := make(map[int]actor.MachineActor)
equipments, _ := entClient.Equipment.Query().WithEquipmentType().All(ctx)
for _, eq := range equipments {
cfg := actor.MachineActorConfig{
ID: eq.ID,
Type: eq.Edges.EquipmentType.Code,
Capacity: eq.SlotCount,
Batch: eq.Batch,
Inspection: eq.Edges.EquipmentType.Code == "INSPECTION" || eq.Edges.EquipmentType.Code == "SAMPLING",
SignalCh: signalRouter.Watch(eq.ID, doneSignals[eq.ID], ngSignals[eq.ID]),
DB: dbState,
Bus: eventBus,
}
machineActors[eq.ID] = actor.NewMachineActor(cfg)
}
// 创建 TempStoreActor
tempStoreActor := actor.NewTempStoreActor(dbState.TempSlotCapacity(ctx), dbState)
// 启动 SignalRouter
go signalRouter.Run(ctx, 1*time.Second)
// 启动所有 Actor
for _, a := range machineActors {
go a.Run(ctx)
}
go tempStoreActor.Run(ctx)
// 恢复 Actor 状态
dbState.RecoverMachineActors(ctx, machineActors, tempStoreActor)
// 更新 NewProductionEventLoop 调用
eventLoop := eventloop.NewProductionEventLoop(entClient, worker, machineActors, tempStoreActor, sched)
- Step 2: 移除 Station 初始化
删除 BuildProductionLine、RestoreStationSlots、configureCNCProductTypes 调用。
- Step 3: 移除 SignalWatcher 初始化
删除 NewSignalWatcher 调用和回调绑定。
- Step 4: 移除 queryBatchMachines / queryInspectionMachines
删除这些辅助函数,逻辑已内化到 Actor 配置中。
- Step 5: 验证编译
go build ./...
- Step 6: 提交
git add internal/svc/service_context.go
git commit -m "refactor: wire Actors into ServiceContext, remove Station/SignalWatcher"
Chunk 7: 清理旧代码
Task 7.1: 删除 Station 和 SignalWatcher
- Step 1: 删除文件
rm internal/station/base.go
rm internal/station/cnc.go
rm internal/station/cleaning.go
rm internal/station/washer.go
rm internal/station/inspection.go
rm internal/station/deburr.go
rm internal/station/sampling.go
rm internal/station/interface.go # Station 接口(保留 HandheldTool 到 action 包)
rm internal/station/registry.go
rm internal/station/helpers.go
rm internal/station/scanner.go
rm internal/station/laser_marker.go
# 保留: internal/station/interface_test.go 中 HandheldTool 相关
rm internal/processor/signal_watcher.go
rm internal/processor/step_timeout.go
- Step 2: 将 HandheldTool 接口移到 action 包
// internal/action/tool.go (或保留在 station 包的最小文件中)
type HandheldTool interface {
ID() string
Type() string
Name() string
Execute(cmd StationCommand) error
}
更新所有 import 路径。
- Step 3: 清理 processor 包引用
移除 internal/processor/job_processor.go 中的 registry *station.StationRegistry 字段。移除 internal/processor/robot_worker.go 中的 registry 引用(上料卸载涉及 station 的逻辑改为通过 Actor 发送消息)。
- Step 4: 更新 Replenisher
internal/processor/replenisher.go 中的 findFreeTempSlot / freeSlotsFromDB 改为从 TempStoreActor 快照查。
- Step 5: 验证全量编译
go build ./...
- Step 6: 运行所有测试
go test ./internal/eventloop/... ./internal/actor/... ./internal/scheduler/... ./internal/processor/...
- Step 7: 提交
git add -A
git commit -m "refactor: remove Station/SignalWatcher, migrate to Actor model"
Chunk 8: 端到端验证
Task 8.1: Mockrun 集成验证
- Step 1: 确认 Mock.Enable=true
grep "Enable:" etc/hougai-api.yaml
- Step 2: 运行端到端测试
go run cmd/mockrun/main.go -jobs 2 -timeout 5m
Expected: 2 个工件全部完成,无超时、无报错。
- Step 3: 修复任何集成问题
根据 mockrun 输出修复 Actor 与 EventLoop 之间的消息传递、事件路由等问题。
- Step 4: 最终提交
git add -A
git commit -m "fix: Actor integration issues from mockrun verification"
回滚策略
如果遇到不可逾越的阻塞,所有变更在 actor模型 分支上,可随时回到 master:
git checkout master