mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 14:57:38 +00:00
用户要求:"历史以来的全部写入日志的区域"改为英文,因为中文日志 在没有中文字形的终端/容器里会乱码,且跨机器、跨工具 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
185 lines
6.4 KiB
Go
185 lines
6.4 KiB
Go
package plugin
|
||
|
||
// 内置插件随包 SKILL 的**统一生命周期管理**。
|
||
//
|
||
// ## 为什么放在内核(而不是让每个插件自己写)
|
||
//
|
||
// 第一版我让 seq 自己在 Start() 里写盘 + 发事件。那是错的,有两个硬伤:
|
||
//
|
||
// ① 每个内置插件都要重复实现一遍「释放 / 清理 / 幂等」,
|
||
// 而且**禁用插件后 skill 还留着** —— 模型看得到 skill,点进去工具却不存在
|
||
// ② 内置插件是按需加载的:未 `plugin_start` 时它的工具本就不在,
|
||
// skill 却已经出现在索引里,等于对模型说谎
|
||
//
|
||
// 正确的语义:**skill 是插件能力的一部分,生命周期必须与插件同生共死**。
|
||
// 所以由内核在插件启停处统一切换,插件作者一行都不用写。
|
||
//
|
||
// ## 挂载点
|
||
//
|
||
// StartPlugin / loadOne 成功 → InstallBuiltinSkill
|
||
// StopPlugin / StopAll → RemoveBuiltinSkill
|
||
// 持久禁用(plugin_disable) → RemoveBuiltinSkill
|
||
//
|
||
// ## 来源
|
||
//
|
||
// 源码树里约定 `<插件源码目录>/skill/<skillName>/SKILL.md`(与 SDK 示例
|
||
// third_party/homeagent-sdk/example/editdoc/skill/office-tools 一致)。
|
||
// 但内置插件是**编译期内置**,运行时没有源码树,所以 SKILL 正文必须
|
||
// 编进二进制(见各插件包里的 embed),由插件通过 SkillProvider 暴露。
|
||
//
|
||
// 现阶段用一个最小接口从插件实例上取:**插件只需实现一个方法**,
|
||
// 不需要自己碰文件系统、不需要发事件、不需要管清理。
|
||
|
||
import (
|
||
"log"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
|
||
)
|
||
|
||
// SkillProvider 是**可选**接口:内置插件实现它即可让自己的 SKILL 随启停出入。
|
||
//
|
||
// 只需返回 SKILL 文档集合(名字 → SKILL.md 全文),其余全由内核处理:
|
||
// 写盘位置、幂等、事件通知、停用时清理。
|
||
//
|
||
// 不实现该接口的插件不受任何影响(绝大多数插件没有 SKILL)。
|
||
type SkillProvider interface {
|
||
// BuiltinSkills 返回本插件随包的 SKILL 文档。
|
||
//
|
||
// map 的键是 skill 名(**必须**与 SKILL.md frontmatter 的 name 一致,
|
||
// 也用作目录名);值是 SKILL.md 全文(含 frontmatter)。
|
||
BuiltinSkills() map[string]string
|
||
}
|
||
|
||
// skillDirOf 返回某个内置 skill 在磁盘上的约定位置。
|
||
//
|
||
// 与 skillmgr 的扫描口径对齐:它扫 <data_dir>/skills/*,每个目录里要有 SKILL.md。
|
||
func (r *Registry) skillDirOf(skillName string) string {
|
||
r.mu.RLock()
|
||
dataDir := r.dataDir
|
||
r.mu.RUnlock()
|
||
if dataDir == "" {
|
||
return ""
|
||
}
|
||
return filepath.Join(dataDir, "skills", skillName)
|
||
}
|
||
|
||
// InstallBuiltinSkill 把某个插件的随包 SKILL 释放到 <data_dir>/skills/ 并通知 skillmgr。
|
||
//
|
||
// 幂等:
|
||
// - 目录已存在且 SKILL.md 非空 ⇒ 视为已安装,只补发一次事件(让 skillmgr 接管)
|
||
// - 内容为空/读取失败 ⇒ 重写
|
||
//
|
||
// 失败只记日志、不返回错误:skill 装不上不该阻止插件启动(工具本身仍可用)。
|
||
// 但必须**可见** —— 没有 skill 的插件模型基本不会去用,这是静默失效。
|
||
func (r *Registry) InstallBuiltinSkill(pluginName string, p sdk.Plugin) {
|
||
provider, ok := p.(SkillProvider)
|
||
if !ok {
|
||
return // 绝大多数插件没有 SKILL,这是常态,不是错误
|
||
}
|
||
skills := provider.BuiltinSkills()
|
||
if len(skills) == 0 {
|
||
return
|
||
}
|
||
|
||
for name, body := range skills {
|
||
if strings.TrimSpace(body) == "" {
|
||
log.Printf("[plugin] %s: skill %q has empty body, skipping (an empty SKILL gives the model nothing to read)", pluginName, name)
|
||
continue
|
||
}
|
||
dir := r.skillDirOf(name)
|
||
if dir == "" {
|
||
log.Printf("[plugin] %s: data_dir not set, cannot install skill %q", pluginName, name)
|
||
continue
|
||
}
|
||
file := filepath.Join(dir, "SKILL.md")
|
||
|
||
// 已存在且非空:保留(用户可能改过措辞)。但仍要发事件,
|
||
// 否则重启后 skillmgr 若没扫到,这条 skill 就永久消失。
|
||
cur, readErr := os.ReadFile(file)
|
||
if readErr != nil || len(cur) == 0 {
|
||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||
log.Printf("[plugin] %s: creating skill dir failed %s: %v", pluginName, dir, err)
|
||
continue
|
||
}
|
||
if err := os.WriteFile(file, []byte(body), 0644); err != nil {
|
||
log.Printf("[plugin] %s: writing skill failed %s: %v", pluginName, file, err)
|
||
continue
|
||
}
|
||
log.Printf("[plugin] %s: installed skill %q -> %s", pluginName, name, dir)
|
||
}
|
||
|
||
r.announceSkill(pluginName, dir)
|
||
}
|
||
}
|
||
|
||
// RemoveBuiltinSkill 在插件停用/禁用时移除其随包 SKILL。
|
||
//
|
||
// 为什么必须移除:否则模型在 skill 索引里仍看得到它,点进去照做,
|
||
// 而工具已被停用 —— 模型会拿到一连串"工具不存在"的错误,
|
||
// 且它无从知道原因是"插件被关了"。
|
||
func (r *Registry) RemoveBuiltinSkill(pluginName string, p sdk.Plugin) {
|
||
provider, ok := p.(SkillProvider)
|
||
if !ok {
|
||
return
|
||
}
|
||
for name := range provider.BuiltinSkills() {
|
||
dir := r.skillDirOf(name)
|
||
if dir == "" {
|
||
continue
|
||
}
|
||
if err := os.RemoveAll(dir); err != nil {
|
||
log.Printf("[plugin] %s: removing skill %q failed %s: %v", pluginName, name, dir, err)
|
||
continue
|
||
}
|
||
log.Printf("[plugin] %s: removed skill %q (plugin is stopped)", pluginName, name)
|
||
// 通知 skillmgr 从索引里摘掉(复用同一事件,负载带 removed 标记)。
|
||
r.announceSkillRemoved(pluginName, dir)
|
||
}
|
||
}
|
||
|
||
// announceSkill 发布 skill_detected,让 skillmgr **当场**接管。
|
||
//
|
||
// 不能只依赖 skillmgr 的启动全扫:它启动早于 plugin-host 层,
|
||
// 首次部署/停止后重启插件时都扫不到(实测:skillmgr 在内核第 4 层装配后
|
||
// 10s 完成扫描,此时插件才刚被加载)。
|
||
func (r *Registry) announceSkill(pluginName, dir string) {
|
||
r.mu.RLock()
|
||
bus := r.evBus
|
||
r.mu.RUnlock()
|
||
if bus == nil {
|
||
return
|
||
}
|
||
bus.Publish(&events.Event{
|
||
Type: events.EventSkillDetected,
|
||
Source: pluginName,
|
||
Payload: map[string]interface{}{
|
||
"path": dir,
|
||
"from_plugin": pluginName,
|
||
"builtin": true,
|
||
},
|
||
})
|
||
}
|
||
|
||
func (r *Registry) announceSkillRemoved(pluginName, dir string) {
|
||
r.mu.RLock()
|
||
bus := r.evBus
|
||
r.mu.RUnlock()
|
||
if bus == nil {
|
||
return
|
||
}
|
||
bus.Publish(&events.Event{
|
||
Type: events.EventSkillDetected,
|
||
Source: pluginName,
|
||
Payload: map[string]interface{}{
|
||
"path": dir,
|
||
"from_plugin": pluginName,
|
||
"builtin": true,
|
||
"removed": true,
|
||
},
|
||
})
|
||
}
|