// 假执行器:模拟客户端核心(配对 → WSS → 心跳 → 任务执行 → 挑战处理)。 // 用途:D4 本地闭环验收 + 联调诊断;D8 起由 client/core 真 Agent 取代。 package main import ( "bytes" "context" "crypto/ed25519" "crypto/rand" "encoding/base64" "encoding/hex" "encoding/json" "flag" "io" "log" "net/http" "os" "os/signal" "path/filepath" "strconv" "syscall" "time" "github.com/coder/websocket" "everypublish/shared/proto" ) var ( server = flag.String("server", "http://127.0.0.1:8090", "服务器地址") user = flag.String("user", "demo@everypublish.io", "登录邮箱") password = flag.String("pass", "demo-pass-2026", "登录密码") devName = flag.String("name", "fake-agent-01", "设备名") execMS = flag.Int("exec", 50, "模拟执行时长 ms") keyPath = flag.String("key", "./data/fake-agent.ed25519", "设备私钥文件") ) func main() { flag.Parse() log.SetFlags(log.Ltime | log.Lmicroseconds) access := login(*server, *user, *password) code := pairCode(*server, access) pub, priv := loadOrCreateKey(*keyPath) deviceID := pair(*server, code, pub) log.Printf("paired device %d", deviceID) wsURL := "ws" + (*server)[4:] + "/ws/agent" conn, _, err := websocket.Dial(context.Background(), wsURL, nil) if err != nil { log.Fatalf("dial: %v", err) } defer conn.Close(websocket.StatusNormalClosure, "bye") nonce := randHex(16) ts := time.Now().UnixMilli() sig := ed25519.Sign(priv, []byte(nonce+"|"+strconv.FormatInt(ts, 10))) hello := proto.NewEnvelope(randHex(8), proto.TypeHello, proto.DeviceHello{ DeviceID: strconv.FormatUint(deviceID, 10), Nonce: nonce, TS: ts, Sig: base64.StdEncoding.EncodeToString(sig), Version: "0.1.0-fake", }) if err = send(conn, hello); err != nil { log.Fatalf("hello: %v", err) } _, raw, err := conn.Read(context.Background()) if err != nil { log.Fatalf("hello.ack read: %v", err) } var ackEnv proto.Envelope _ = json.Unmarshal(raw, &ackEnv) if ackEnv.Type != proto.TypeHelloAck { log.Fatalf("expect hello.ack, got %s", ackEnv.Type) } var ack proto.HelloAck _ = json.Unmarshal(ackEnv.Payload, &ack) log.Printf("online (session %s)", ack.SessionID) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() go heartbeatLoop(ctx, conn, deviceID) for { _, raw, err = conn.Read(ctx) if err != nil { log.Fatalf("read: %v", err) } var env proto.Envelope if err = json.Unmarshal(raw, &env); err != nil { continue } switch env.Type { case proto.TypeTaskPush: var push proto.TaskPush _ = json.Unmarshal(env.Payload, &push) log.Printf("TASK push %s platform=%s title=%s", push.TaskID, push.Platform, push.Title) _ = send(conn, proto.NewEnvelope(env.ID, proto.TypeTaskAck, proto.TaskAck{TaskID: push.TaskID, Accept: true})) time.Sleep(time.Duration(*execMS) * time.Millisecond) result := proto.TaskResult{ TaskID: push.TaskID, Status: "success", PublishedURL: "https://example.com/fake/" + push.TaskID, Receipts: []string{}, FinishedAt: time.Now().UnixMilli(), } _ = send(conn, proto.NewEnvelope(randHex(8), proto.TypeTaskResult, result)) log.Printf("TASK %s done (%dms)", push.TaskID, *execMS) case proto.TypeChallenge: var ch proto.Challenge _ = json.Unmarshal(env.Payload, &ch) log.Printf("CHALLENGE %s kind=%s prompt=%s", ch.ChallengeID, ch.Kind, ch.Prompt) _ = send(conn, proto.NewEnvelope(env.ID, proto.TypeChallengeAck, proto.ChallengeAck{ChallengeID: ch.ChallengeID, Action: "accept"})) time.Sleep(time.Duration(*execMS) * time.Millisecond) _ = send(conn, proto.NewEnvelope(randHex(8), proto.TypeChallengeSolve, proto.ChallengeSolve{ChallengeID: ch.ChallengeID, Value: "fake-qr-token"})) log.Printf("CHALLENGE %s solved", ch.ChallengeID) case proto.TypeHeartbeatAck: default: log.Printf("ignore msg type %s", env.Type) } } } func heartbeatLoop(ctx context.Context, conn *websocket.Conn, deviceID uint64) { ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: _ = send(conn, proto.NewEnvelope(randHex(8), proto.TypeHeartbeat, proto.Heartbeat{DeviceID: strconv.FormatUint(deviceID, 10), TS: time.Now().UnixMilli()})) } } } func send(conn *websocket.Conn, msg *proto.Envelope) error { raw, err := json.Marshal(msg) if err != nil { return err } wctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() return conn.Write(wctx, websocket.MessageText, raw) } // ---- REST 辅助 ---- func httpJSON(method, url, token string, body interface{}) (int, []byte) { var buf bytes.Buffer if body != nil { raw, _ := json.Marshal(body) buf.Write(raw) } req, _ := http.NewRequest(method, url, &buf) req.Header.Set("Content-Type", "application/json") if token != "" { req.Header.Set("Authorization", "Bearer "+token) } client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { log.Fatalf("http %s %s: %v", method, url, err) } defer resp.Body.Close() raw, _ := io.ReadAll(resp.Body) return resp.StatusCode, raw } func field(data []byte, key string) string { var m map[string]interface{} _ = json.Unmarshal(data, &m) if d, ok := m["data"].(map[string]interface{}); ok { if v, ok := d[key]; ok { switch t := v.(type) { case string: return t case float64: return strconv.FormatInt(int64(t), 10) } } } return "" } func login(base, email, password string) string { code, raw := httpJSON("POST", base+"/api/v1/auth/login", "", ginH{"email": email, "password": password}) if code != 200 { log.Fatalf("login failed: %s", string(raw)) } log.Println("login ok") return field(raw, "accessToken") } func pairCode(base, token string) string { code, raw := httpJSON("POST", base+"/api/v1/agent/pair-code", token, nil) if code != 200 { log.Fatalf("pair-code failed: %s", string(raw)) } return field(raw, "code") } func pair(base, code, pub string) uint64 { httpCode, raw := httpJSON("POST", base+"/api/v1/agent/pair", "", ginH{"code": code, "deviceName": *devName, "os": "fake", "version": "0.1.0", "publicKey": pub}) if httpCode != 200 { log.Fatalf("pair failed: %s", string(raw)) } id, _ := strconv.ParseUint(field(raw, "deviceId"), 10, 64) return id } type ginH map[string]interface{} func loadOrCreateKey(path string) (string, ed25519.PrivateKey) { if raw, err := os.ReadFile(path); err == nil { seed, err := hex.DecodeString(string(raw)) if err == nil && len(seed) == ed25519.SeedSize { priv := ed25519.NewKeyFromSeed(seed) return base64.StdEncoding.EncodeToString(priv.Public().(ed25519.PublicKey)), priv } } _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { log.Fatalf("keygen: %v", err) } _ = os.MkdirAll(filepath.Dir(path), 0o755) _ = os.WriteFile(path, []byte(hex.EncodeToString(priv.Seed())), 0o600) return base64.StdEncoding.EncodeToString(priv.Public().(ed25519.PublicKey)), priv } func randHex(n int) string { b := make([]byte, n) _, _ = rand.Read(b) return hex.EncodeToString(b) }