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

@ -0,0 +1,192 @@
package plugin
// 内置插件随包 SKILL 生命周期的判据。
//
// ## 为什么这些判据必须存在
//
// skill 没装上/没摘掉,**插件本身照常工作**(工具都在),只是模型的行为变了:
// · 缺 skill ⇒ 模型看不到能力摘要,几乎不会用("seq 很少被使用"的直接原因)
// · 停用后 skill 还在 ⇒ 模型看得到却调不到,拿到一串"工具不存在"且不知原因
//
// 两种都是**静默失效**:没有任何报错,只能靠判据钉住。
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/JianFeeeee/HologramAgent/internal/events"
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
)
// fakeSkillPlugin 是一个实现 SkillProvider 的最小插件替身。
type fakeSkillPlugin struct {
name string
skills map[string]string
}
func (f *fakeSkillPlugin) Name() string { return f.name }
func (f *fakeSkillPlugin) Start(*sdk.PluginSDK) error { return nil }
func (f *fakeSkillPlugin) Stop() error { return nil }
func (f *fakeSkillPlugin) BuiltinSkills() map[string]string { return f.skills }
// plainPlugin 不实现 SkillProvider —— 绝大多数插件是这样,必须不受影响。
type plainPlugin struct{ name string }
func (p *plainPlugin) Name() string { return p.name }
func (p *plainPlugin) Start(*sdk.PluginSDK) error { return nil }
func (p *plainPlugin) Stop() error { return nil }
// testDataDir 读回注入的 data_dir(同包测试可直接读私有字段)。
func (r *Registry) testDataDir() string {
r.mu.RLock()
defer r.mu.RUnlock()
return r.dataDir
}
func newTestRegistryForSkills(t *testing.T) *Registry {
t.Helper()
r := NewRegistry()
r.SetDataDir(t.TempDir())
r.SetEventBus(events.NewBus())
return r
}
const sampleSkill = `---
name: demo-tools
description: 演示用 skill。
---
演示用 skill 的正文段落,模型会读到这一行作为摘要。
`
// ── 安装:写到约定位置(skillmgr 的扫描口径)──
func TestBuiltinSkill_InstallsToConventionPath(t *testing.T) {
r := newTestRegistryForSkills(t)
dataDir := r.testDataDir()
p := &fakeSkillPlugin{name: "demo", skills: map[string]string{"demo-tools": sampleSkill}}
r.InstallBuiltinSkill("demo", p)
file := filepath.Join(dataDir, "skills", "demo-tools", "SKILL.md")
data, err := os.ReadFile(file)
if err != nil {
t.Fatalf("SKILL.md 未落到约定位置 %s: %v(skillmgr 扫不到它)", file, err)
}
if string(data) != sampleSkill {
t.Fatal("落盘内容与插件提供的不一致")
}
}
// 不实现 SkillProvider 的插件必须被安静跳过(不能报错、不能创建目录)。
func TestBuiltinSkill_NonProviderIsNoop(t *testing.T) {
r := newTestRegistryForSkills(t)
r.InstallBuiltinSkill("plain", &plainPlugin{name: "plain"})
skillsDir := filepath.Join(r.testDataDir(), "skills")
if entries, err := os.ReadDir(skillsDir); err == nil && len(entries) > 0 {
t.Fatalf("无 skill 的插件不应产生任何目录,实际 %d 项", len(entries))
}
}
// 幂等:重复安装不报错,且不覆盖用户已改过的内容。
func TestBuiltinSkill_IdempotentNoOverwrite(t *testing.T) {
r := newTestRegistryForSkills(t)
dataDir := r.testDataDir()
p := &fakeSkillPlugin{name: "demo", skills: map[string]string{"demo-tools": sampleSkill}}
r.InstallBuiltinSkill("demo", p)
file := filepath.Join(dataDir, "skills", "demo-tools", "SKILL.md")
const edited = "# 用户改过的内容\n"
if err := os.WriteFile(file, []byte(edited), 0644); err != nil {
t.Fatal(err)
}
r.InstallBuiltinSkill("demo", p)
got, _ := os.ReadFile(file)
if string(got) != edited {
t.Fatalf("重复安装覆盖了用户内容:期望 %q,实际 %q", edited, string(got))
}
}
// 空正文必须跳过(空的 SKILL 会让模型读到无内容,等于装了个假 skill)。
func TestBuiltinSkill_EmptyBodySkipped(t *testing.T) {
r := newTestRegistryForSkills(t)
r.InstallBuiltinSkill("demo", &fakeSkillPlugin{
name: "demo",
skills: map[string]string{"empty": " \n "},
})
dir := filepath.Join(r.testDataDir(), "skills", "empty")
if _, err := os.Stat(dir); err == nil {
t.Fatal("空正文不应创建 skill 目录")
}
}
// ── 卸载:停用时必须摘掉 ──
func TestBuiltinSkill_RemoveOnStop(t *testing.T) {
r := newTestRegistryForSkills(t)
dataDir := r.testDataDir()
p := &fakeSkillPlugin{name: "demo", skills: map[string]string{"demo-tools": sampleSkill}}
r.InstallBuiltinSkill("demo", p)
dir := filepath.Join(dataDir, "skills", "demo-tools")
if _, err := os.Stat(dir); err != nil {
t.Fatalf("前置:skill 应已安装: %v", err)
}
r.RemoveBuiltinSkill("demo", p)
if _, err := os.Stat(dir); !os.IsNotExist(err) {
t.Fatalf("停用后 skill 目录必须移除(否则模型看得到却调不到工具): err=%v", err)
}
}
// ── 事件:必须主动通告(启动全扫有时序空窗)──
func TestBuiltinSkill_AnnouncesDetected(t *testing.T) {
r := newTestRegistryForSkills(t)
bus := events.NewBus()
r.SetEventBus(bus)
var got []*events.Event
bus.Subscribe(events.EventSkillDetected, func(e *events.Event) { got = append(got, e) })
r.InstallBuiltinSkill("demo", &fakeSkillPlugin{
name: "demo",
skills: map[string]string{"demo-tools": sampleSkill},
})
if len(got) != 1 {
t.Fatalf("安装必须通告一次 skill_detected,实际 %d 次", len(got))
}
if p, _ := got[0].Payload["path"].(string); !strings.Contains(p, "demo-tools") {
t.Fatalf("payload.path 应指向 skill 目录,实际 %q", p)
}
if got[0].Source != "demo" {
t.Fatalf("Source 应为插件名,实际 %q", got[0].Source)
}
}
// 卸载时通告带 removed 标记:让 skillmgr 能从索引里摘掉。
func TestBuiltinSkill_AnnouncesRemoved(t *testing.T) {
r := newTestRegistryForSkills(t)
bus := events.NewBus()
r.SetEventBus(bus)
var got []*events.Event
bus.Subscribe(events.EventSkillDetected, func(e *events.Event) { got = append(got, e) })
p := &fakeSkillPlugin{name: "demo", skills: map[string]string{"demo-tools": sampleSkill}}
r.InstallBuiltinSkill("demo", p)
r.RemoveBuiltinSkill("demo", p)
if len(got) != 2 {
t.Fatalf("安装+卸载应各通告一次,实际 %d 次", len(got))
}
removed, _ := got[1].Payload["removed"].(bool)
if !removed {
t.Fatal("卸载通告必须带 removed=true(否则 skillmgr 分不清是新增还是移除)")
}
}