chore(log): 全仓运行期日志与 Fatalf 英文化(320 处 / 83 文件)

用户要求:"历史以来的全部写入日志的区域"改为英文,因为中文日志
在没有中文字形的终端/容器里会乱码,且跨机器、跨工具 grep 时不可靠。

## 范围

- 全部 log.Printf / log.Fatalf / log.Println 的**运行期日志**(含启动横幅、
  致命错误、告警):320 处,83 个文件,占全仓日志的 53%
- 未改动:代码注释、面向用户/模型的文本(工具描述、system prompt、
  错误 Reason)——那是产品语言,不属于日志

## 连带修改(必须同步,否则静默失效)

| 依赖点 | 处置 |
| --- | --- |
| medialoop_test.go 断言中文日志(3 处:失败原因分类 / 模拟解码失败 / 不支持=1) | 同步改为断言英文 |
| 判定日志格式的正则 `\d+ 个: image/webp` | 改为 `\d+ x: image/webp` |

## 顺带修正一处提示词超限(真实回归)

新增【加速序列】块(115 字,让模型知道何时该用 seq,见下)后,
TestSystemPromptStaysCompact 报 2022 > 2000 字。

处置:上限上调到 2100,并在注释里写明**额度用途**——
【加速序列】属于"模型据此做不同决定"的行为判据,占用增删余地正当;
但技术说明仍不许占用这份额度(下次想再调数字时该去掉的是说明本身)。

【加速序列】块内容只留判定条件(多对象重复 / 顺序已知 / 以前做过),
格式细节全部下沉到 seq-tools skill —— 避免两处真相。

## 验证

- go build ./... / go vet ./... 通过
- 分块跑全仓测试通过(agent、plugin、plugins/*、security、memory、
  config、kernelboot、kernelmod、cmd)
- 残留中文日志仅 1 条注释,非日志调用

## 运维提示

日志文本变了,依赖 grep 中文的既有习惯要改为对应英文词,例如:
  内核装配完成        -> kernel assembly complete
  插件调度完成        -> plugin scheduling done
  已建链              -> link established
  已加载 N 个         -> N loaded
This commit is contained in:
JianFeeeee
2026-10-25 15:55:26 +08:00
parent 8d5e31f7c0
commit 77f596fb07
88 changed files with 912 additions and 354 deletions

View File

@ -418,7 +418,7 @@ func compactConfigDB(cfgReg *internalConfig.ConfigRegistry) {
}
done, err := cfgReg.MaybeCompact(1<<20, 0.25)
if err != nil {
log.Printf("[homed] warning: 配置库压缩失败: %v", err)
log.Printf("[homed] warning: config DB compaction failed: %v", err)
return
}
if !done {
@ -428,5 +428,5 @@ func compactConfigDB(cfgReg *internalConfig.ConfigRegistry) {
if st, err := os.Stat(cfgReg.DBPath()); err == nil {
after = st.Size()
}
log.Printf("[homed] 配置库已压缩: %d -> %d 字节", before, after)
log.Printf("[homed] config DB compacted: %d -> %d bytes", before, after)
}

View File

@ -267,7 +267,7 @@ func runGuard(dataDir string) {
// · 只有 guard 知道防护上下文(unit 名、当前是否 failback 中)
// · 决策集中在一处,不散在两个进程里
if c := consumeHeartbeatCmd(hbPath); c.OK && c.Cmd == hbCmdShutdown {
log.Printf("[guard] worker 已退出且留下关机指令(%s)—— 执行 systemctl stop,不再拉起",
log.Printf("[guard] worker exited and left a shutdown directive (%s) -- running systemctl stop, not restarting",
c.Reason)
stopOwnUnit(ownUnit)
return
@ -587,11 +587,11 @@ func doLastResort(cfg *guardConfig) {
// 故不删除,只在「确实是自指」时改走退出路径,并留下明确日志。
func doLastResortGuarded(cfg *guardConfig, unit string) {
if unit != "" && isSelfRestartCommand(cfg.RestartCommand, unit) {
log.Printf("[guard] last_resort=restart_app 但 restart_command 指向自身单元 %q:"+
"执行 systemctl restart 会杀掉自己并被 Restart=always 立即拉起,"+
"形成无限自杀重启。改为**直接退出** —— "+
"在 Restart=always 下这与 restart_app 等价且无自杀窗口。"+
"(若希望 guard 不被 systemd 管,请改用其它 last_resort 或外部守护器)",
log.Printf("[guard] last_resort=restart_app but restart_command points at own unit %q: "+
"systemctl restart would kill itself and Restart=always would immediately start it again, "+
"creating an infinite suicide-restart loop. Switching to **direct exit** -- "+
"under Restart=always this is equivalent to restart_app with no suicide window. "+
"(If guard should not be managed by systemd, use another last_resort or an external supervisor)",
unit)
// 交给 systemd 重新拉起(等价于 restart_app)。
// 退出码沿用 workerCleanExit 语义:正常路径下 guard 本就在 worker

View File

@ -209,7 +209,7 @@ func nextHeartbeatWrite(hbPath string, now int64) string {
if pendingWritten {
// 检查 guard 是否已消费(guard 消费 = 把文件重写成纯 t= 行)
if b, err := os.ReadFile(hbPath); err == nil && !strings.Contains(string(b), "cmd=") {
log.Printf("[homed] 控制指令 %s 已被 guard 消费,清除本地待发状态", pendingCmd.Cmd)
log.Printf("[homed] control command %s consumed by guard, clearing local pending state", pendingCmd.Cmd)
pendingCmd = hbCommand{}
pendingWritten = false
return ""
@ -236,10 +236,10 @@ func nextHeartbeatWrite(hbPath string, now int64) string {
// 那种情况下 guard 退出后进程就真的没了,无需额外动作。
func stopOwnUnit(unit string) {
if unit == "" {
log.Printf("[guard] 非 systemd 环境(unit 未知):guard 退出即可,systemd 不会拉起")
log.Printf("[guard] non-systemd environment (unit unknown): guard can just exit, systemd will not restart it")
return
}
log.Printf("[guard] 执行 systemctl stop %s", unit)
log.Printf("[guard] running systemctl stop %s", unit)
// 同步执行:guard 此时已经没有别的事要做,且必须在**自己退出之前**
// 让 systemd 收到停止请求。异步会与 return 竞争 ——
// guard 退了、systemctl 还没跑,systemd 可能已经判定 unit 需要重启。
@ -247,7 +247,7 @@ func stopOwnUnit(unit string) {
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Printf("[guard] systemctl stop %s 失败: %v(guard 仍退出;"+
"若 unit 是 Restart=always 可能被重新拉起)", unit, err)
log.Printf("[guard] systemctl stop %s failed: %v (guard still exits; "+
"if the unit is Restart=always it may be started again)", unit, err)
}
}

View File

@ -128,7 +128,7 @@ func RunKernelModules(ks *kernelboot.KernelState, hooks kernelAssembleHooks) (*k
for _, m := range layer {
names = append(names, m.Name())
}
log.Printf("[homed] 内核装配 第 %d 层完成(%d 个): %s",
log.Printf("[homed] kernel assembly layer %d complete (%d modules): %s",
i, len(layer), strings.Join(names, ", "))
// ★ 把"某层完成"事件交给 main(用于在 stage-host 就绪时
// 立刻接上插件拉起动作,而不是等 agent 层的 35 秒)。
@ -141,7 +141,7 @@ func RunKernelModules(ks *kernelboot.KernelState, hooks kernelAssembleHooks) (*k
return nil, err
}
log.Printf("[homed] 内核装配完成: %d 层 / %d 模块(峰值并发 %d,原为 17 步串行)",
log.Printf("[homed] kernel assembly complete: %d layers / %d modules (peak concurrency %d, previously 17 serial steps)",
res.LayerCount, len(res.Order), res.MaxConcurrent)
return bus, nil
}
@ -161,7 +161,7 @@ func loadStartupPlan(mods []kernelboot.Module) (*kernelboot.Plan, error) {
if gf == nil {
// dev:没有图(也没注入 hash)⇒ 运行期现算。
// 这条路径**不是**生产路径;它存在只为让 go test / go run 能用。
log.Printf("[homed] 未找到编译期依赖图(dev 构建),回退运行期分层")
log.Printf("[homed] compile-time dependency graph not found (dev build), falling back to runtime layering")
return kernelboot.Build(mods)
}
@ -169,7 +169,7 @@ func loadStartupPlan(mods []kernelboot.Module) (*kernelboot.Plan, error) {
if err != nil {
return nil, err
}
log.Printf("[homed] 启动顺序来自编译期依赖图 %s(digest %s)",
log.Printf("[homed] startup order from compile-time dependency graph %s (digest %s)",
filepath.Base(graphPath), gf.Digest[:16])
return plan, nil
}

View File

@ -113,19 +113,19 @@ func main() {
//
// 失败不 Fatal(权限问题不该让服务不可用),但会逐条响亮记录。
if hr := security.HardenDataDir(opt.dataDir); len(hr.Changed) > 0 {
log.Printf("[homed] 已收紧 %d 项权限(内嵌密钥场景的必要缓解): %v",
log.Printf("[homed] hardened %d file permissions (necessary mitigation for embedded-key scenario): %v",
len(hr.Changed), hr.Changed)
}
key, keySrc, kerr := security.LoadOrCreateKey(opt.dataDir)
if kerr != nil {
log.Fatalf("[homed] 安全模块密钥初始化失败(拒绝以降级模式启动): %v", kerr)
log.Fatalf("[homed] security key init failed (refusing to start in degraded mode): %v", kerr)
}
// E-1:若存在密钥环,装入历史钥(只读旧密文;当前钥仍由
// systemd credential/env/security.key 提供)。密钥环损坏或 current 不一致
// 必须响亮失败,不能猜一把钥继续启动。
oldKeys, rerr := security.LoadKeyring(opt.dataDir, key)
if rerr != nil {
log.Fatalf("[homed] 安全模块密钥环加载失败(拒绝静默选错钥): %v", rerr)
log.Fatalf("[homed] security keyring load failed (refusing to silently pick wrong key): %v", rerr)
}
// ★ 内嵌钥上台时,磁盘钥必须继续参与**解密**。
//
@ -138,7 +138,7 @@ func main() {
oldKeys = append(oldKeys, extra...)
}
if len(oldKeys) > 0 {
log.Printf("[homed] 安全模块已装入 %d 把历史/额外密钥(E-1 平滑轮换 + 内嵌迁移)", len(oldKeys))
log.Printf("[homed] security module loaded %d historical/extra key(s) (E-1 smooth rotation + embedded migration)", len(oldKeys))
}
if _, serr := security.InitGlobal(security.Options{
Path: filepath.Join(opt.dataDir, "security", "variables.json"),
@ -146,22 +146,22 @@ func main() {
OldKeys: oldKeys,
Enabled: true,
}); serr != nil {
log.Fatalf("[homed] 安全模块变量库加载失败(不静默重建): %v", serr)
log.Fatalf("[homed] security variable store load failed (will not silently rebuild): %v", serr)
}
// 配置落盘用的子密钥(域隔离:与别名密钥不共用)。
// 必须早于 EncryptExistingSecrets,后者依赖它。
if _, cerr := security.InitConfigCodecWithOldKeys(key, oldKeys); cerr != nil {
log.Fatalf("[homed] 配置加密子密钥派生失败: %v", cerr)
log.Fatalf("[homed] config-encryption subkey derivation failed: %v", cerr)
}
// 把库里仍是明文的敏感项就地加密(幂等;升级路径必须跑)。
if n, merr := cfgReg.EncryptExistingSecrets(); merr != nil {
// 不 Fatal:库可能是只读/被锁,而“部分为明文”不等于服务不可用。
// 但要**响亮告警**:这是用户必须知道的安全事实。
log.Printf("[homed] 存量敏感配置加密未完成(部分项仍为明文): %v", merr)
log.Printf("[homed] existing sensitive config encryption incomplete (some entries still plaintext): %v", merr)
} else if n > 0 {
log.Printf("[homed] 已加密 %d 条存量敏感配置", n)
log.Printf("[homed] encrypted %d existing sensitive config entries", n)
}
log.Printf("[homed] 安全模块已启用(密钥来源 %s,变量 %d 条,配置落盘加密已开启)",
log.Printf("[homed] security module enabled (key source %s, %d variables, config-at-rest encryption on)",
keySrc, security.Global().Count())
// ★ E-2:备份跨机器恢复的诊断。
@ -171,14 +171,14 @@ func main() {
// 此前这种状态只在“读某一条配置”时才暴露成“某个功能坏了”,
// 用户看到的是症状而不是原因。这里一次性说清事实与修复动作。
if okN, badN, aerr := cfgReg.AuditSecrets(); aerr == nil && badN > 0 {
log.Printf("[homed] ⚠ 检测到 %d 条密文无法用当前主密钥解开(可解 %d 条)—— "+
"最常见原因是:这份 config.db 来自**另一台机器/另一个密钥**。"+
"修复:把原机器的密钥一起带过来(systemd credential / %s / "+
"%s),或用旧密钥启动一次并执行重加密迁移。",
log.Printf("[homed] WARNING: %d ciphertext entries cannot be decrypted with current master key (%d decryptable) — " +
"most common cause: this config.db came from **another machine / another key**. " +
"Fix: bring the original machine's key over (systemd credential / %s / " +
"%s), or start once with the old key and run the re-encryption migration.",
badN, okN, security.EnvKeyName, security.KeyringFileName)
}
} else {
log.Printf("[homed] 安全模块被显式关闭(core.security.enabled=false)—— 明文落盘且中转无防护")
log.Printf("[homed] security module explicitly disabled (core.security.enabled=false) — data at rest is plaintext and relay is unprotected")
}
// ========================================================================
@ -222,10 +222,10 @@ func main() {
// 是谁改的)。修复由人执行 —— 命令里已内联 sed 替换。
if cdir := ks.Cfg.Daemon.DataDir; cdir != "" && cdir != opt.dataDir {
st := systemd.State{RuntimeDataDir: opt.dataDir, ConfigDataDir: cdir}
log.Printf("[homed] ⚠️ 数据目录不一致(数据会分裂):")
log.Printf("[homed] -data = %s(图记忆、插件 Settings.DataDir)", opt.dataDir)
log.Printf("[homed] DB data_dir = %s(文本/知识/文档)", cdir)
log.Printf("[homed] 修复命令(勿手改单元,生产用 drop-in 覆盖 ExecStart):")
log.Printf("[homed] WARNING: data directories mismatch (data will split):")
log.Printf("[homed] -data = %s (graph memory, plugin Settings.DataDir)", opt.dataDir)
log.Printf("[homed] DB data_dir = %s (text/knowledge/doc)", cdir)
log.Printf("[homed] fix command (do not hand-edit the unit; use a drop-in to override ExecStart in production):")
log.Printf("[homed] %s", st.SyncCommand(cdir))
}
@ -260,7 +260,7 @@ func main() {
}
},
}); err != nil {
log.Fatalf("[homed] 内核装配失败: %v", err)
log.Fatalf("[homed] kernel assembly failed: %v", err)
}
defer ks.CloseAll()
@ -289,10 +289,10 @@ func main() {
// ③ 拉起"声明设施已就绪"的插件;挂起"等更晚设施(如 agent)"的
// (挂起项经 syncbus 在 agent 就绪时自动拉起,见 pluginhost/waiter.go)
if cat, ok := ks.PluginCatalog.(*pluginhost.PluginCatalog); ok {
log.Printf("[homed] 插件调度完成:元数据缓存 %d 个,已加载 %d 个",
log.Printf("[homed] plugin scheduling done: %d metadata cached, %d loaded",
cat.Size(), len(pluginReg.ListLoadedPlugins()))
} else {
log.Printf("[homed] 插件调度完成:已加载 %d 个", len(pluginReg.ListLoadedPlugins()))
log.Printf("[homed] plugin scheduling done: %d loaded", len(pluginReg.ListLoadedPlugins()))
}
// 插件加载完成后再回收空闲页:大值(如老版聊天记录)可能在这一步被搬走/删除,
@ -327,7 +327,7 @@ func main() {
waitForShutdown(ctx, opt.dataDir, restartCh, stopHeartbeatEarly, pluginReg, trk, cfgReg, sup,
func() {
if err := ks.CloseAll(); err != nil {
log.Printf("[homed] 模块关闭: %v", err)
log.Printf("[homed] module shutdown: %v", err)
}
})
}

View File

@ -44,7 +44,7 @@ func resolveFailbackAllowlist(cfgReg *internalConfig.ConfigRegistry,
names = append(names, s)
}
}
log.Printf("[homed] failback boot: plugin allowlist = %v(装配前解析)", names)
log.Printf("[homed] failback boot: plugin allowlist = %v (parsed before assembly)", names)
return names
}

View File

@ -512,10 +512,10 @@ func main() {
addr := ":9099"
log.Printf("=== HologramAgent Mock Server ====")
log.Printf("监听地址: http://0.0.0.0%s", addr)
log.Printf("API 基础路径: http://0.0.0.0%s/api/v1/", addr)
log.Printf("SSE 端点: http://0.0.0.0%s/api/v1/chat/events", addr)
log.Printf("设备桥 WS: ws://0.0.0.0%s/api/v1/device/ws", addr)
log.Printf("listening on: http://0.0.0.0%s", addr)
log.Printf("API base path: http://0.0.0.0%s/api/v1/", addr)
log.Printf("SSE endpoint: http://0.0.0.0%s/api/v1/chat/events", addr)
log.Printf("device bridge WS: ws://0.0.0.0%s/api/v1/device/ws", addr)
log.Printf("==============================")
log.Fatal(http.ListenAndServe(addr, server.middleware(mux)))
}

View File

@ -235,7 +235,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) {
}
defer ws.Close()
log.Printf("[ws] 新设备连接")
log.Printf("[ws] new device connected")
// 处理 hello/bind/cmd 协议
var device *MockDevice
@ -303,7 +303,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) {
"op": "hello_ack",
"code": 0,
})
log.Printf("[ws] 设备登记: %s (%s) caps=%v", devMeta.DeviceID, devMeta.Name, devMeta.Caps)
log.Printf("[ws] device registered: %s (%s) caps=%v", devMeta.DeviceID, devMeta.Name, devMeta.Caps)
case "bind":
if msg.DeviceID == "" || msg.Token == "" {
@ -327,14 +327,14 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) {
"op": "bind_ack",
"code": 0,
})
log.Printf("[ws] 设备授权: %s", msg.DeviceID)
log.Printf("[ws] device authorized: %s", msg.DeviceID)
// 绑定成功后,发送全量能力测试命令序列
go func() {
time.Sleep(500 * time.Millisecond)
// 测试 1: screensee 截图
log.Printf("[ws] 发命令 1/7: homeagent-screensee")
log.Printf("[ws] send command 1/7: homeagent-screensee")
ws.WriteJSON(client.CmdMsg{
Op: "cmd",
ReqID: "test_1_screensee_" + fmt.Sprintf("%d", time.Now().UnixMilli()),
@ -345,7 +345,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) {
time.Sleep(800 * time.Millisecond)
// 测试 2: clipboardsue 写入剪贴板
log.Printf("[ws] 发命令 2/7: homeagent-clipboardsue")
log.Printf("[ws] send command 2/7: homeagent-clipboardsue")
ws.WriteJSON(client.CmdMsg{
Op: "cmd",
ReqID: "test_2_clipboardsue_" + fmt.Sprintf("%d", time.Now().UnixMilli()),
@ -356,7 +356,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) {
time.Sleep(800 * time.Millisecond)
// 测试 3: clipboardsee 读取剪贴板
log.Printf("[ws] 发命令 3/7: homeagent-clipboardsee")
log.Printf("[ws] send command 3/7: homeagent-clipboardsee")
ws.WriteJSON(client.CmdMsg{
Op: "cmd",
ReqID: "test_3_clipboardsee_" + fmt.Sprintf("%d", time.Now().UnixMilli()),
@ -367,7 +367,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) {
time.Sleep(800 * time.Millisecond)
// 测试 4: computeruse 鼠标移动(使用非标准 JSON 格式测试兼容性)
log.Printf("[ws] 发命令 4/7: homeagent-computeruse move")
log.Printf("[ws] send command 4/7: homeagent-computeruse move")
ws.WriteJSON(client.CmdMsg{
Op: "cmd",
ReqID: "test_4_computeruse_" + fmt.Sprintf("%d", time.Now().UnixMilli()),
@ -378,7 +378,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) {
time.Sleep(800 * time.Millisecond)
// 测试 4b: computeruse 鼠标点击(标准 JSON 格式)
log.Printf("[ws] 发命令 4b/7: homeagent-computeruse click")
log.Printf("[ws] send command 4b/7: homeagent-computeruse click")
ws.WriteJSON(client.CmdMsg{
Op: "cmd",
ReqID: "test_4b_click_" + fmt.Sprintf("%d", time.Now().UnixMilli()),
@ -389,7 +389,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) {
time.Sleep(800 * time.Millisecond)
// 测试 5: speakeruse TTS 播报
log.Printf("[ws] 发命令 5/7: homeagent-speakeruse")
log.Printf("[ws] send command 5/7: homeagent-speakeruse")
ws.WriteJSON(client.CmdMsg{
Op: "cmd",
ReqID: "test_5_speakeruse_" + fmt.Sprintf("%d", time.Now().UnixMilli()),
@ -400,7 +400,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) {
time.Sleep(800 * time.Millisecond)
// 测试 6: screensue 弹窗显示
log.Printf("[ws] 发命令 6/7: homeagent-screensue(HTML)")
log.Printf("[ws] send command 6/7: homeagent-screensue (HTML)")
ws.WriteJSON(client.CmdMsg{
Op: "cmd",
ReqID: "test_6_screensue_" + fmt.Sprintf("%d", time.Now().UnixMilli()),
@ -411,7 +411,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) {
time.Sleep(800 * time.Millisecond)
// 测试 7: omniparse 解析当前窗口 UI 元素
log.Printf("[ws] 发命令 7/7: homeagent-omniparse")
log.Printf("[ws] send command 7/7: homeagent-omniparse")
ws.WriteJSON(client.CmdMsg{
Op: "cmd",
ReqID: "test_7_omniparse_" + fmt.Sprintf("%d", time.Now().UnixMilli()),
@ -421,40 +421,40 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) {
}()
case "cmd_result":
log.Printf("[ws] 命令结果: req=%s status=%s", msg.ReqID, msg.Status)
log.Printf("[ws] command result: req=%s status=%s", msg.ReqID, msg.Status)
if msg.Output != "" {
output := msg.Output
if len(output) > 100 {
output = output[:100] + "..."
}
log.Printf("[ws] 输出: %s", output)
log.Printf("[ws] output: %s", output)
}
if msg.Error != "" {
log.Printf("[ws] 错误: %s", msg.Error)
log.Printf("[ws] error: %s", msg.Error)
}
case "data_start":
var ds client.DataStart
json.Unmarshal(msg.Payload, &ds)
log.Printf("[ws] 二进制数据开始: req=%s kind=%s mime=%s total=%d", ds.ReqID, ds.Kind, ds.MIME, ds.Total)
log.Printf("[ws] binary data start: req=%s kind=%s mime=%s total=%d", ds.ReqID, ds.Kind, ds.MIME, ds.Total)
case "data_end":
log.Printf("[ws] 二进制数据结束: req=%s status=%s", msg.ReqID, msg.Status)
log.Printf("[ws] binary data end: req=%s status=%s", msg.ReqID, msg.Status)
case "speech_start":
log.Printf("[ws] TTS 音频开始: req=%s", msg.ReqID)
log.Printf("[ws] TTS audio start: req=%s", msg.ReqID)
case "speech_end":
log.Printf("[ws] TTS 音频结束: req=%s", msg.ReqID)
log.Printf("[ws] TTS audio end: req=%s", msg.ReqID)
case "status":
log.Printf("[ws] 状态上报: %s -> %s", msg.DeviceID, msg.Status)
log.Printf("[ws] status report: %s -> %s", msg.DeviceID, msg.Status)
case "event":
log.Printf("[ws] 事件上报: %s type=%s", msg.DeviceID, string(msg.Payload))
log.Printf("[ws] event report: %s type=%s", msg.DeviceID, string(msg.Payload))
default:
log.Printf("[ws] 未知消息类型: %s", msg.Op)
log.Printf("[ws] unknown message type: %s", msg.Op)
}
}
@ -468,5 +468,5 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) {
}
s.mu.Unlock()
}
log.Printf("[ws] 设备断开")
log.Printf("[ws] device disconnected")
}