mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 08:47:27 +00:00
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:
184
internal/plugin/builtin_skill.go
Normal file
184
internal/plugin/builtin_skill.go
Normal file
@ -0,0 +1,184 @@
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
192
internal/plugin/builtin_skill_test.go
Normal file
192
internal/plugin/builtin_skill_test.go
Normal 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 分不清是新增还是移除)")
|
||||
}
|
||||
}
|
||||
@ -97,11 +97,11 @@ func (r *Registry) loadProc(dir, name string, config map[string]interface{}) (sd
|
||||
if mft := readManifest(dir); mft != nil {
|
||||
caps = mft.Capabilities
|
||||
if len(caps) > 0 {
|
||||
log.Printf("[plugin] %s 声明能力: %v", name, caps)
|
||||
log.Printf("[plugin] %s declares capabilities: %v", name, caps)
|
||||
}
|
||||
hsTimeout = mft.HandshakeTimeout(proc.DefaultHandshakeTimeout)
|
||||
if mft.HandshakeTimeoutSec > 0 {
|
||||
log.Printf("[plugin] %s 声明建链超时 %s(内核默认 %s)",
|
||||
log.Printf("[plugin] %s declares link timeout %s (kernel default %s)",
|
||||
name, hsTimeout, proc.DefaultHandshakeTimeout)
|
||||
}
|
||||
}
|
||||
@ -128,10 +128,10 @@ func (r *Registry) ensureProcHost() (*proc.Host, error) {
|
||||
if bus := r.eventBusOrNil(); bus != nil {
|
||||
er := NewEventRing(host.EvtRing(), int(host.Evtfd().Fd()), bus)
|
||||
host.SetEvtSubscriber(er)
|
||||
log.Printf("[plugin] 事件环已创建(Bus → EvtRing → eventfd)")
|
||||
log.Printf("[plugin] event ring created (Bus -> EvtRing -> eventfd)")
|
||||
}
|
||||
|
||||
log.Printf("[plugin] 共享段已创建(全部子进程插件共用一块,%d KB)", host.ShmSize()/1024)
|
||||
log.Printf("[plugin] shared segment created (shared by all child-process plugins, %d KB)", host.ShmSize()/1024)
|
||||
return host, nil
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ func (r *Registry) closeProcHost() {
|
||||
return
|
||||
}
|
||||
if err := r.procHost.Close(); err != nil {
|
||||
log.Printf("[plugin] 关闭共享段: %v", err)
|
||||
log.Printf("[plugin] closing shared segment: %v", err)
|
||||
}
|
||||
r.procHost = nil
|
||||
}
|
||||
@ -162,7 +162,7 @@ func (r *Registry) closeProcHost() {
|
||||
//
|
||||
// 所以崩溃回调必须做三件事:摘注册面、喂健康计数、排一次重启。
|
||||
func (r *Registry) onProcCrash(name string, err error) {
|
||||
log.Printf("[plugin] 子进程插件 %s 异常退出: %v(homed 未受影响)", name, err)
|
||||
log.Printf("[plugin] child-process plugin %s exited abnormally: %v (homed unaffected)", name, err)
|
||||
|
||||
// 1) 摘掉工具/stage/通道。**必须先做**:从这一刻起模型就不该再看到这些工具,
|
||||
// 否则在重启完成前的窗口里每次调用都是确定的失败。
|
||||
@ -212,13 +212,13 @@ func (r *Registry) scheduleProcRestart(name string, cause error) {
|
||||
return // 内核正在关停,不再拉起
|
||||
}
|
||||
if !r.AutoRestartEnabled(name) {
|
||||
log.Printf("[plugin] %s 声明了不自动重启,保持缺席状态", name)
|
||||
log.Printf("[plugin] %s declared no-auto-restart, staying absent", name)
|
||||
return
|
||||
}
|
||||
|
||||
n := r.noteCrash(name)
|
||||
if n > procMaxRestarts {
|
||||
log.Printf("[plugin] %s 在 %v 内崩溃 %d 次,停止自动重启(需人工介入)",
|
||||
log.Printf("[plugin] %s crashed %d times within %v, stopping auto-restart (manual intervention required)",
|
||||
name, procCrashWindow, n)
|
||||
return
|
||||
}
|
||||
@ -235,23 +235,23 @@ func (r *Registry) scheduleProcRestart(name string, cause error) {
|
||||
return
|
||||
}
|
||||
if r.isDisabled(name) {
|
||||
log.Printf("[plugin] %s 已被禁用,取消自动重启", name)
|
||||
log.Printf("[plugin] %s is disabled, cancelling auto-restart", name)
|
||||
return
|
||||
}
|
||||
r.mu.RLock()
|
||||
already := r.plugins[name] != nil
|
||||
r.mu.RUnlock()
|
||||
if already {
|
||||
log.Printf("[plugin] %s 已被其他路径重新加载,取消自动重启", name)
|
||||
log.Printf("[plugin] %s was reloaded via another path, cancelling auto-restart", name)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[plugin] 自动重启 %s(第 %d 次,退避 %v,起因: %v)", name, n, delay, cause)
|
||||
log.Printf("[plugin] auto-restarting %s (attempt %d, backoff %v, cause: %v)", name, n, delay, cause)
|
||||
if err := r.ReloadOne(name); err != nil {
|
||||
log.Printf("[plugin] %s 自动重启失败: %v", name, err)
|
||||
log.Printf("[plugin] %s auto-restart failed: %v", name, err)
|
||||
return
|
||||
}
|
||||
log.Printf("[plugin] %s 自动重启成功", name)
|
||||
log.Printf("[plugin] %s auto-restart succeeded", name)
|
||||
}
|
||||
|
||||
// noteCrash 记录一次崩溃并返回窗口内的累计次数。
|
||||
|
||||
@ -419,7 +419,7 @@ func (h *coreHandler) invokeCleanerText(scope, name, text string) (string, error
|
||||
if res.TextRef.Flags&sharedRefFlagExpand != 0 {
|
||||
defer func() {
|
||||
if err := arena.Free(h.owner, res.TextRef); err != nil {
|
||||
log.Printf("[proc] %s %s Cleaner 扩容块归还失败(arena 将缓慢泄漏): %v", scope, name, err)
|
||||
log.Printf("[proc] %s %s Cleaner expansion block return failed (arena will leak slowly): %v", scope, name, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@ -307,7 +307,7 @@ func (h *Host) endStage(coord *stageCoordinator) error {
|
||||
// · releaseIfLeaked —— 插件异常路径未 unlock 时,锁不会毒化下一轮
|
||||
// · unbind —— 下一轮的 stage.lock 不得再打到本轮的旧锁上
|
||||
if h.locks.releaseIfLeaked(coord.lock) {
|
||||
log.Printf("[proc] stage 协调器结束时清理了泄漏的 stage 锁")
|
||||
log.Printf("[proc] stage coordinator cleaned up a leaked stage lock at exit")
|
||||
}
|
||||
h.locks.unbind(coord.lock)
|
||||
return err
|
||||
@ -384,7 +384,7 @@ func (c *stageCoordinator) finish(sc *pubsdk.StageContext, written bool) error {
|
||||
return fmt.Errorf("回读共享段: %w", rErr)
|
||||
}
|
||||
if reclaimed := c.seg.Compact(); reclaimed > 0 {
|
||||
log.Printf("[proc] stage 结束,arena 压实回收 %d 字节", reclaimed)
|
||||
log.Printf("[proc] stage ended, arena compaction reclaimed %d bytes", reclaimed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -265,12 +265,12 @@ func (p *Plugin) Close() error {
|
||||
func (p *Plugin) handleExit(name string, err error) {
|
||||
if p.host != nil {
|
||||
if p.host.ForceReleaseLock(name) {
|
||||
log.Printf("[proc] %s 退出,内核已释放其持有的 stage 锁", name)
|
||||
log.Printf("[proc] %s exited, kernel released its stage locks", name)
|
||||
}
|
||||
// 回收该插件未归还的共享槽:崩溃的插件无法自己归还,
|
||||
// 不回收会让槽池慢慢耗尽,最终所有共享内存调用退化成内联 RPC。
|
||||
if n := p.host.ReclaimOwner(p.ownerID); n > 0 {
|
||||
log.Printf("[proc] %s 退出,回收 %d 个残留共享槽", name, n)
|
||||
log.Printf("[proc] %s exited, reclaimed %d leftover shared slot(s)", name, n)
|
||||
}
|
||||
}
|
||||
// 内核主动停止(Stop/Close,含宽限期超时后的 Kill)不算崩溃:
|
||||
@ -405,17 +405,17 @@ func (p *Plugin) noteCancelIgnored(callID uint64, grace time.Duration) {
|
||||
p.cancelIgnoredMu.Unlock()
|
||||
|
||||
if n < cancelIgnoredLimit {
|
||||
log.Printf("[proc] %s 取消后未停手(第 %d/%d 次,调用 %d):暂不重启,继续观察",
|
||||
log.Printf("[proc] %s did not stop after cancel (%d/%d, call %d): not restarting yet, still observing",
|
||||
p.name, n, cancelIgnoredLimit, callID)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[proc] %s 连续 %d 次在取消后 %v 内未停手,判定处于崩溃/不稳定态 —— 主动重启",
|
||||
log.Printf("[proc] %s failed to stop within %v after cancel %d times in a row, judged crashed/unstable -- restarting proactively",
|
||||
p.name, n, grace)
|
||||
// 走既有的崩溃重启链(含退避、限流、禁用手工介入检查):
|
||||
// 不在这里自己 Kill+Reload,否则会绕开那些保护。
|
||||
if p.onCrash != nil {
|
||||
p.onCrash(p.name, fmt.Errorf("取消后连续 %d 次无响应(最后调用 %d,宽限 %v)",
|
||||
p.onCrash(p.name, fmt.Errorf("unresponsive %d times in a row after cancel (last call %d, grace %v)",
|
||||
n, callID, grace))
|
||||
}
|
||||
}
|
||||
@ -426,7 +426,7 @@ func (p *Plugin) noteCancelIgnored(callID uint64, grace time.Duration) {
|
||||
// 之前是 `_ = arena.Free(...)`,把原因直接丢弃,线上排查代价极高。
|
||||
func (p *Plugin) freeExpand(what, name string, ref SharedRef) {
|
||||
if err := p.host.Arena().Free(p.ownerID, ref); err != nil {
|
||||
log.Printf("[proc] %s %s %s 扩容块归还失败(arena 将缓慢泄漏): %v",
|
||||
log.Printf("[proc] %s %s %s expansion block return failed (arena will leak slowly): %v",
|
||||
p.name, what, name, err)
|
||||
}
|
||||
}
|
||||
@ -449,7 +449,7 @@ func (p *Plugin) invokeStage(ctx context.Context, stage string, seq uint64) erro
|
||||
}
|
||||
}
|
||||
if res.DirtyFields > 0 {
|
||||
log.Printf("[proc] %s stage %s 改写了 %d 个字段", p.name, stage, res.DirtyFields)
|
||||
log.Printf("[proc] %s stage %s modified %d field(s)", p.name, stage, res.DirtyFields)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -307,7 +307,7 @@ func (p *Process) handshake(timeout time.Duration) error {
|
||||
return fmt.Errorf("proc: %s 主版本不匹配(插件 SDK %s,内核 %s)——请用配套 hmapdev 重编",
|
||||
p.name, res.SDKVersion, meta.Version)
|
||||
}
|
||||
log.Printf("[proc] %s 已建链(pid=%d sdk=%s core=%s)",
|
||||
log.Printf("[proc] %s link established (pid=%d sdk=%s core=%s)",
|
||||
p.name, p.PID(), res.SDKVersion, meta.Version)
|
||||
return nil
|
||||
}
|
||||
@ -383,12 +383,12 @@ func (p *Process) readLoop() {
|
||||
// ⇒ 现在把**内容摘要**与**可能原因 + 修法**一起打出来。
|
||||
// 限长是因为坏帧可能很大(插件打了长文本/二进制),
|
||||
// 截断到 160 字节足以认出"这是哪句日志"。
|
||||
log.Printf("[proc] %s 收到非法 JSON 帧(%d 字节): %v\n"+
|
||||
" ↑ 内容摘要: %s\n"+
|
||||
" 可能原因:插件往 **stdout** 打印了日志,而 stdout 是"+
|
||||
"JSON-RPC **协议通道**(stderr 才是日志通道)。\n"+
|
||||
" 修法:把插件里的 fmt.Printf/fmt.Println 改成 log.Printf"+
|
||||
"(脚手架模板 1.4.0 起已修正)。",
|
||||
log.Printf("[proc] %s received malformed JSON frame (%d bytes): %v\n"+
|
||||
" ^ content excerpt: %s\n"+
|
||||
" likely cause: the plugin printed logs to **stdout**, but stdout is the "+
|
||||
"JSON-RPC **protocol channel** (stderr is the log channel).\n"+
|
||||
" fix: change fmt.Printf/fmt.Println in the plugin to log.Printf "+
|
||||
"(fixed in scaffolding template since 1.4.0).",
|
||||
p.name, len(line), err, summarizeFrame(line, 160))
|
||||
continue
|
||||
}
|
||||
@ -403,14 +403,14 @@ func (p *Process) readLoop() {
|
||||
|
||||
var resp Response
|
||||
if err := json.Unmarshal(line, &resp); err != nil {
|
||||
log.Printf("[proc] %s 应答解析失败: %v", p.name, err)
|
||||
log.Printf("[proc] %s response parse failed: %v", p.name, err)
|
||||
continue
|
||||
}
|
||||
p.deliverResponse(&resp)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Printf("[proc] %s 读取 stdout 出错: %v", p.name, err)
|
||||
log.Printf("[proc] %s stdout read error: %v", p.name, err)
|
||||
}
|
||||
|
||||
// stdout 关闭(EOF)通常意味着进程结束——2.5ms 内即可感知(实验 6)。
|
||||
@ -460,9 +460,9 @@ func (p *Process) markExited() {
|
||||
if p.waitErr != nil {
|
||||
e := fmt.Errorf("插件进程 %s 异常退出: %w", p.name, p.waitErr)
|
||||
p.exitErr.Store(&e)
|
||||
log.Printf("[proc] %s 退出: %v", p.name, p.waitErr)
|
||||
log.Printf("[proc] %s exited: %v", p.name, p.waitErr)
|
||||
} else {
|
||||
log.Printf("[proc] %s 正常退出", p.name)
|
||||
log.Printf("[proc] %s exited normally", p.name)
|
||||
}
|
||||
|
||||
// 进程没了:所有在途调用都无法完成。按状态机收敛——
|
||||
@ -515,7 +515,7 @@ func (p *Process) markExited() {
|
||||
func (p *Process) serveRequest(line []byte) {
|
||||
var req Request
|
||||
if err := json.Unmarshal(line, &req); err != nil {
|
||||
log.Printf("[proc] %s 请求解析失败: %v", p.name, err)
|
||||
log.Printf("[proc] %s request parse failed: %v", p.name, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -538,7 +538,7 @@ func (p *Process) serveRequest(line []byte) {
|
||||
// ID==0 是通知,不回应答(§2.4 约束 B:post-and-forget)
|
||||
if req.ID == 0 {
|
||||
if err != nil {
|
||||
log.Printf("[proc] %s 通知 %s 处理失败: %v", p.name, req.Method, err)
|
||||
log.Printf("[proc] %s notification %s handling failed: %v", p.name, req.Method, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -554,7 +554,7 @@ func (p *Process) serveRequest(line []byte) {
|
||||
}
|
||||
}
|
||||
if wErr := p.writeFrame(&resp); wErr != nil {
|
||||
log.Printf("[proc] %s 回写应答失败: %v", p.name, wErr)
|
||||
log.Printf("[proc] %s writing response failed: %v", p.name, wErr)
|
||||
}
|
||||
}
|
||||
|
||||
@ -661,7 +661,7 @@ func (p *Process) deliverResponse(resp *Response) {
|
||||
c, ok := p.inflight[resp.ID]
|
||||
if ok && c.State() != callPending && c.State() != callCanceled {
|
||||
p.mu.Unlock()
|
||||
log.Printf("[proc] %s 丢弃重复应答 id=%d(该调用已处于 %s)",
|
||||
log.Printf("[proc] %s dropping duplicate response id=%d (call already in state %s)",
|
||||
p.name, resp.ID, c.State())
|
||||
return
|
||||
}
|
||||
@ -671,7 +671,7 @@ func (p *Process) deliverResponse(resp *Response) {
|
||||
p.mu.Unlock()
|
||||
|
||||
if !ok {
|
||||
log.Printf("[proc] %s 丢弃未知 id=%d 的应答(不属于任何在途调用)", p.name, resp.ID)
|
||||
log.Printf("[proc] %s dropping response with unknown id=%d (not owned by any in-flight call)", p.name, resp.ID)
|
||||
return
|
||||
}
|
||||
|
||||
@ -686,7 +686,7 @@ func (p *Process) deliverResponse(resp *Response) {
|
||||
}
|
||||
// 通道容量 1 且此处无读者,不写入(写了会阻塞 readLoop)。
|
||||
default:
|
||||
log.Printf("[proc] %s 丢弃无法归类的应答 id=%d(状态 %s)",
|
||||
log.Printf("[proc] %s dropping unclassifiable response id=%d (state %s)",
|
||||
p.name, resp.ID, c.State())
|
||||
}
|
||||
}
|
||||
@ -802,11 +802,11 @@ func (p *Process) expireCanceled(c *call) {
|
||||
}
|
||||
// ① 回收块:refs 的 owner 是插件,但回收决策来自内核(见 ReclaimRefs)。
|
||||
if n := p.reclaimRefs(c.takeBlocks()); n > 0 {
|
||||
log.Printf("[proc] %s 调用 %d 取消后无响应:已强制回收 %d 个扩容块", p.name, c.id, n)
|
||||
log.Printf("[proc] %s call %d unresponsive after cancel: forcibly reclaimed %d expansion block(s)", p.name, c.id, n)
|
||||
}
|
||||
// ② 上报:由上层判定稳定性并决定是否重启。
|
||||
// 这里只负责"如实汇报事实",不做重启决策——proc 不知道插件生命周期策略。
|
||||
log.Printf("[proc] %s 调用 %d 在取消后 %v 内未停手(插件失去响应)", p.name, c.id, cancelGrace)
|
||||
log.Printf("[proc] %s call %d did not stop within %v after cancel (plugin unresponsive)", p.name, c.id, cancelGrace)
|
||||
if p.onCancelIgnored != nil {
|
||||
p.onCancelIgnored(c.id, cancelGrace)
|
||||
}
|
||||
@ -852,7 +852,7 @@ func (p *Process) cancelCall(id uint64, cause error) {
|
||||
reason = cause.Error()
|
||||
}
|
||||
if err := p.Notify(MethodCallCancel, CallCancelParams{CallID: id, Reason: reason}); err != nil {
|
||||
log.Printf("[proc] %s 发送调用取消失败(id=%d,可能已退出): %v", p.name, id, err)
|
||||
log.Printf("[proc] %s failed to send call cancel (id=%d, likely already exited): %v", p.name, id, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -872,7 +872,7 @@ func (p *Process) Stop() error {
|
||||
if _, err := p.CallContext(ctx, MethodPluginStop, nil); err != nil {
|
||||
// 停止调用失败不影响后续 Kill——插件可能已经崩了
|
||||
if !errors.Is(err, ErrProcessExited) {
|
||||
log.Printf("[proc] %s plugin.stop 失败(将强制结束): %v", p.name, err)
|
||||
log.Printf("[proc] %s plugin.stop failed (will force terminate): %v", p.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -880,7 +880,7 @@ func (p *Process) Stop() error {
|
||||
case <-p.exited:
|
||||
return nil
|
||||
case <-time.After(stopGracePeriod):
|
||||
log.Printf("[proc] %s 宽限期内未退出,强制结束", p.name)
|
||||
log.Printf("[proc] %s did not exit within grace period, force terminating", p.name)
|
||||
return p.Kill()
|
||||
}
|
||||
}
|
||||
@ -919,7 +919,7 @@ func (p *Process) Kill() error {
|
||||
case <-time.After(killReapTimeout):
|
||||
// SIGKILL 后仍未收割:进程卡在不可中断的内核态(D 状态,如 NFS I/O)。
|
||||
// 不能无限等,否则重载路径整体挂死;留日志供定位。
|
||||
log.Printf("[proc] %s SIGKILL 后 %v 仍未被收割(进程可能卡在内核态)", p.name, killReapTimeout)
|
||||
log.Printf("[proc] %s still not reaped %v after SIGKILL (process may be stuck in kernel mode)", p.name, killReapTimeout)
|
||||
}
|
||||
// readerWG.Wait 必须有界:孙进程持有 stdout 写端时 readLoop 永不返回,
|
||||
// 无超时就是"任何插件泄漏一个孙进程都能拖死整个关停"。
|
||||
@ -934,7 +934,7 @@ func (p *Process) Kill() error {
|
||||
select {
|
||||
case <-readerDone:
|
||||
case <-time.After(killReapTimeout):
|
||||
log.Printf("[proc] %s 的 stdout 读取未在 %v 内结束(孙进程可能仍持有写端),强制关闭读端",
|
||||
log.Printf("[proc] %s stdout read did not finish within %v (grandchild process may still hold the write end), force-closing read end",
|
||||
p.name, killReapTimeout)
|
||||
if p.stdoutFile != nil {
|
||||
_ = p.stdoutFile.Close()
|
||||
@ -944,7 +944,7 @@ func (p *Process) Kill() error {
|
||||
case <-time.After(killReapTimeout):
|
||||
// 极端情况:关管道也没能让它退出。不再等 —— 宁可让这次
|
||||
// Stop 少等 2 秒,也不能把关停无限期挂住。
|
||||
log.Printf("[proc] %s 读端关闭后 readLoop 仍未退出,放弃等待", p.name)
|
||||
log.Printf("[proc] %s readLoop still did not exit after read end closed, giving up", p.name)
|
||||
}
|
||||
}
|
||||
if killErr != nil && !errors.Is(killErr, os.ErrProcessDone) {
|
||||
|
||||
@ -74,7 +74,7 @@ func (h *Host) AuditShmMappings(expectedPlugins int) (ShmAudit, bool) {
|
||||
a.ActualMappings, ok = h.countOurShmMappings()
|
||||
a.Delta = a.ActualMappings - a.ExpectedPlugins
|
||||
if ok && a.Delta > 0 {
|
||||
log.Printf("[proc] 共享内存映射审计:期望 %d 实测 %d(+delta=%d)—— 可能有泄漏句柄或未授权挂载",
|
||||
log.Printf("[proc] shared memory mapping audit: expected %d actual %d (+delta=%d) -- possible leaked handles or unauthorized mappings",
|
||||
a.ExpectedPlugins, a.ActualMappings, a.Delta)
|
||||
}
|
||||
return a, ok
|
||||
|
||||
@ -73,7 +73,7 @@ func (r *lockRegistry) releaseIfLeaked(l *stageLock) bool {
|
||||
return false
|
||||
}
|
||||
if l.ForceRelease(owner) {
|
||||
log.Printf("[proc] stage 协调器结束时发现 %s 仍持有 stage 锁(异常路径未 unlock),已代为释放", owner)
|
||||
log.Printf("[proc] stage coordinator found %s still holding stage lock at exit (abnormal path missed unlock), released on its behalf", owner)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -130,7 +130,7 @@ func (h *coreHandler) runStage(stage string, sc *pubsdk.StageContext) error {
|
||||
}
|
||||
defer func() {
|
||||
if endErr := h.host.endStage(coord); endErr != nil {
|
||||
log.Printf("[proc] %s stage %s 收尾失败: %v", h.name, stage, endErr)
|
||||
log.Printf("[proc] %s stage %s finalization failed: %v", h.name, stage, endErr)
|
||||
}
|
||||
}()
|
||||
|
||||
@ -141,7 +141,7 @@ func (h *coreHandler) runStage(stage string, sc *pubsdk.StageContext) error {
|
||||
// 插件可能在持锁时失败(崩溃/超时)——强制释放,避免后续插件死锁。
|
||||
// 这正是"锁仲裁回内核"的自愈价值(实验 9):无需 robust mutex。
|
||||
if h.host.ForceReleaseLock(h.name) {
|
||||
log.Printf("[proc] %s stage %s 失败后强制释放其持有的 stage 锁", h.name, stage)
|
||||
log.Printf("[proc] %s stage %s failed, force-releasing its stage lock", h.name, stage)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ func (s *Supervisor) track(p *Process) {
|
||||
return
|
||||
}
|
||||
if old, ok := s.procs[p.name]; ok && old != p {
|
||||
log.Printf("[proc] 台账中 %s 已有 pid=%d,被 pid=%d 覆盖", p.name, old.PID(), p.PID())
|
||||
log.Printf("[proc] ledger already has %s with pid=%d, overwritten by pid=%d", p.name, old.PID(), p.PID())
|
||||
}
|
||||
s.procs[p.name] = p
|
||||
}
|
||||
@ -124,7 +124,7 @@ func (s *Supervisor) StopAll(timeout time.Duration) {
|
||||
if len(procs) == 0 {
|
||||
return
|
||||
}
|
||||
log.Printf("[proc] 关停 %d 个子进程插件", len(procs))
|
||||
log.Printf("[proc] shutting down %d child-process plugin(s)", len(procs))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, p := range procs {
|
||||
@ -132,7 +132,7 @@ func (s *Supervisor) StopAll(timeout time.Duration) {
|
||||
go func(pr *Process) {
|
||||
defer wg.Done()
|
||||
if err := pr.Stop(); err != nil {
|
||||
log.Printf("[proc] 停止 %s: %v", pr.Name(), err)
|
||||
log.Printf("[proc] stopping %s: %v", pr.Name(), err)
|
||||
}
|
||||
}(p)
|
||||
}
|
||||
@ -169,7 +169,7 @@ func (s *Supervisor) StopAll(timeout time.Duration) {
|
||||
}
|
||||
}
|
||||
if len(stuck) > 0 {
|
||||
log.Printf("[proc] %v 内未优雅退出,强制结束: %v", timeout, stuck)
|
||||
log.Printf("[proc] did not exit gracefully within %v, force terminating: %v", timeout, stuck)
|
||||
}
|
||||
killers.Wait()
|
||||
}
|
||||
|
||||
@ -817,7 +817,7 @@ func (r *Registry) loadOne(plgDir, name string) bool {
|
||||
// 注意这一条也挡住 loadPlugins 的启动扫描 —— 若插件在启动前就被
|
||||
// 运行态关闭(理论上不会,但保持语义一致),它不该被拉起。
|
||||
if r.isRuntimeStopped(name) {
|
||||
log.Printf("[plugin] %s 处于运行态关闭(runtime stop),跳过加载", name)
|
||||
log.Printf("[plugin] %s is runtime-stopped, skipping load", name)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -885,6 +885,10 @@ func (r *Registry) loadOne(plgDir, name string) bool {
|
||||
}
|
||||
r.mu.Unlock()
|
||||
log.Printf("[plugin] loaded: %s", name)
|
||||
|
||||
// 随包 SKILL 与插件同生:加载成功才让它出现在模型可见的 skill 索引里。
|
||||
// 放在 Start() 之后(工具已注册)—— 否则模型会看到 skill 却调不到工具。
|
||||
r.InstallBuiltinSkill(name, plg)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -940,8 +944,8 @@ func (r *Registry) warnOutputOnlyChannels(plugin string) {
|
||||
r.channelsMu.Unlock()
|
||||
sort.Strings(only)
|
||||
for _, ch := range only {
|
||||
log.Printf("[plugin] %s 只声明了出站通道 %q(未 RegisterInputChannel);"+
|
||||
"内核已兜底登记 inputch,若这是有意为之可忽略", plugin, ch)
|
||||
log.Printf("[plugin] %s declared only outbound channel %q (no RegisterInputChannel); "+
|
||||
"kernel registered inputch as fallback, ignore if intentional", plugin, ch)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1027,12 +1031,12 @@ func (r *Registry) detachPlugin(name string) {
|
||||
tc.UnregisterPluginTools(name)
|
||||
if sc, ok := tc.(PluginStageCleaner); ok {
|
||||
if n := sc.UnregisterPluginStages(name); n > 0 {
|
||||
log.Printf("[plugin] %s: 摘除 %d 个 stage handler", name, n)
|
||||
log.Printf("[plugin] %s: removed %d stage handler(s)", name, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
if chans := r.releasePluginChannels(name); len(chans) > 0 {
|
||||
log.Printf("[plugin] %s: 摘除 IO 通道 %v", name, chans)
|
||||
log.Printf("[plugin] %s: removed IO channels %v", name, chans)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1078,6 +1082,11 @@ func (r *Registry) StopAll() {
|
||||
}
|
||||
stoppers = append(stoppers, stopTarget{plugin: p, sdkRef: r.sdkRefs[p.Name()]})
|
||||
}
|
||||
// 关停时摘掉所有随包 skill:进程要退出,留下 skill 会让下次启动
|
||||
// 出现"索引里有、插件却没起"的错位(尤其插件被配置禁用时)。
|
||||
for _, st := range stoppers {
|
||||
r.RemoveBuiltinSkill(st.plugin.Name(), st.plugin)
|
||||
}
|
||||
r.plugins = make(map[string]sdk.Plugin)
|
||||
r.instances = nil
|
||||
r.pluginAutoRestart = make(map[string]bool)
|
||||
@ -1564,6 +1573,10 @@ func (r *Registry) StopPlugin(name string) error {
|
||||
r.mu.Unlock()
|
||||
return fmt.Errorf("plugin %s 未在运行(已标记为运行态关闭)", name)
|
||||
}
|
||||
// 先摘 skill 再停插件:停用后模型若仍看到 skill,会照做并拿到
|
||||
// 一连串"工具不存在",且无从知道原因是插件被关了。
|
||||
r.RemoveBuiltinSkill(name, p)
|
||||
|
||||
r.runStopHandlers(name)
|
||||
stopErr := p.Stop()
|
||||
delete(r.plugins, name)
|
||||
@ -1583,9 +1596,9 @@ func (r *Registry) StopPlugin(name string) error {
|
||||
r.detachPlugin(name)
|
||||
|
||||
if stopErr != nil {
|
||||
log.Printf("[plugin] runtime stop %s(Stop 报错): %v", name, stopErr)
|
||||
log.Printf("[plugin] runtime stop %s (Stop returned error): %v", name, stopErr)
|
||||
}
|
||||
log.Printf("[plugin] runtime stopped: %s(不改持久状态)", name)
|
||||
log.Printf("[plugin] runtime stopped: %s (persistent state unchanged)", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1633,7 +1646,7 @@ func (r *Registry) StartPlugin(name string) error {
|
||||
plgDir := filepath.Join(r.pluginDirOrEmpty(), name)
|
||||
if r.loadOne(plgDir, name) {
|
||||
if wasStopped {
|
||||
log.Printf("[plugin] runtime started: %s(已清运行态关闭)", name)
|
||||
log.Printf("[plugin] runtime started: %s (runtime-stop flag cleared)", name)
|
||||
} else {
|
||||
log.Printf("[plugin] runtime started: %s", name)
|
||||
}
|
||||
@ -1897,7 +1910,7 @@ func (r *Registry) tryDynamic(plgDir, name string, config map[string]interface{}
|
||||
return nil, err
|
||||
}
|
||||
if plg != nil {
|
||||
log.Printf("[plugin] %s: 经 proc 通道加载(子进程)", name)
|
||||
log.Printf("[plugin] %s: loaded via proc channel (child process)", name)
|
||||
return plg, nil
|
||||
}
|
||||
return nil, fmt.Errorf("plugin %s: entry 声明 %s 但未找到可用二进制", name, binEntry)
|
||||
|
||||
Reference in New Issue
Block a user