Files
HomeAgent/internal/plugins/timer/plugin.go
JianFeeeee f3232000f4 feat(scheduler): L4 也归内核级插件 —— WebUI 终止按钮可用“立即打断”
上一提交把 L4 写成“内核独占(panic / selfip)”,漏了内核级插件这类来源。
用户澄清:**内核级插件应当能声明 L4,用于实现中断能力**,例如 WebUI 的终止按钮。

判据(两道闸,纵深防御):
  1. proc 桥(外部进程唯一入口)一律把 L4 夹到 L3。在这里夹而不是只按 source 判,
     是因为 source 是插件自报字段、可以冒名;本函数所在位置能确知“来自外部进程”。
  2. core:isKernelLevelSource(source) 查 pluginReg.IsBuiltinPlugin,只有编译期内置
     插件(init() 自注册的工厂)才承认 L4。
source 约定 `插件名` 或 `插件名/实例`(webui/<deviceID>),判据取第一段——
否则带设备身份的 WebUI 来源会被误判成外部插件而拿不到 L4。

改动:
- core: interruptLevel(evt, privileged bool);新增 isKernelLevelSource;
  requestPreempt 不再夹取(级别已由 interruptLevel 解析,否则内核级插件的 L4 被削掉)。
- eventloop: 传入 a.isKernelLevelSource(evt.Source)。
- proc 桥: 新增 clampExternalPriority,pubSdkInjectOpts 一律夹取。
- internal/sdk: 再导出 PriorityL1..L4(内置插件用 sdk.PriorityL4)。
- webui handleChatInterrupt(终止按钮)声明 PriorityL4。
- timer 声明 PriorityL3:定时器是“时钟那种实时工作”,比 QQ 那类可无限等待的
  异步消息高(L1)——这是对用户“它不是时钟那种实时工作”的直接推论,可改。
- 测试: L4 特权矩阵(非特权夹取 / 特权承认)、source 判据(内置、内置/实例、
  外部、空、前缀不误匹配)、内核级插件 L4 一路到达调度器、proc 夹取两条。
- 设计稿 §2/§3.2/§11.1/§14/§15 按“L4 = 内核 + 内核级插件”更正。

验收:go build/vet 干净;go test ./... 37 包 ok 0 FAIL;-race 全绿(含 webui/timer)。
2026-09-13 07:18:03 +08:00

136 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package timer
import (
"fmt"
"log"
"sync"
"time"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
func init() {
plugin.RegisterPluginMeta("timer", "定时任务", "Timer")
plugin.RegisterFactory("timer", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
return New(name), nil
})
}
type Plugin struct {
name string
mu sync.Mutex
wg sync.WaitGroup
stopCh chan struct{}
maxDur time.Duration
}
type timerTask struct {
id int
dur time.Duration
message string
doneAt time.Time
s *sdk.PluginSDK
}
func New(name string) *Plugin {
return &Plugin{name: name, stopCh: make(chan struct{})}
}
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
s.SetAutoRestart(true)
p.maxDur = 24 * time.Hour
// 停止清理(取消倒计时)交由 stop handler内核在调用 Stop() 之前执行。
s.RegisterStopHandler(func() { close(p.stopCh) })
// 删除清理:移除插件自身配置(删除专用回调,重载不触发)。
s.RegisterOnRemoveHandler(func() {
if err := s.Settings().Remove("max_duration"); err != nil {
log.Printf("[timer] onRemove cleanup: %v", err)
}
})
s.Settings().RegisterDef(sdk.ConfigDef{
Key: "max_duration", Type: "string", DisplayName: "最大定时时长",
Description: "允许设置的最大定时时长,例如 24h, 7d, 1h默认 24h",
Default: "24h",
})
if v, _ := s.Settings().Get("max_duration"); v != nil {
if s, ok := v.(string); ok && s != "" {
if d, err := time.ParseDuration(s); err == nil && d > 0 {
p.maxDur = d
}
}
}
s.RegisterTool("timer_set", sdk.ToolDef{
Name: "timer_set",
Description: "设置一个定时提醒。倒计时结束后通过中断通道通知 agent。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"duration": map[string]interface{}{
"type": "string",
"description": "持续时间,例如 5s, 2m, 1h",
},
"message": map[string]interface{}{
"type": "string",
"description": "提醒内容",
},
},
"required": []string{"duration", "message"},
},
}, func(args map[string]interface{}) (interface{}, error) {
durStr, _ := args["duration"].(string)
message, _ := args["message"].(string)
if durStr == "" {
return map[string]interface{}{"error": "duration is required"}, nil
}
if message == "" {
return map[string]interface{}{"error": "message is required"}, nil
}
dur, err := time.ParseDuration(durStr)
if err != nil {
return map[string]interface{}{"error": fmt.Sprintf("invalid duration %q: %v", durStr, err)}, nil
}
if dur > p.maxDur {
return map[string]interface{}{"error": fmt.Sprintf("duration %v exceeds max %v", dur, p.maxDur)}, nil
}
p.mu.Lock()
p.wg.Add(1)
p.mu.Unlock()
go func() {
defer p.wg.Done()
select {
case <-time.After(dur):
log.Printf("[timer] firing: %s (%s later)", message, dur)
// NoMemory定时提醒是系统通知不是记忆内容。
// PriorityL3定时器是“时钟那种实时工作”——到点就该处理
// 比 QQ 那类可无限等待的异步消息高L3 vs L1
s.InjectInterruptTextOpts("timer", "timer", fmt.Sprintf("timer: %s", message),
sdk.InjectOptions{NoMemory: true, Priority: sdk.PriorityL3})
case <-p.stopCh:
log.Printf("[timer] cancelled: %s", message)
}
}()
doneAt := time.Now().Add(dur)
return map[string]interface{}{
"status": "timer_set",
"duration": durStr,
"message": message,
"done_at": doneAt.Format(time.RFC3339),
}, nil
})
return nil
}
func (p *Plugin) Stop() error {
p.wg.Wait()
return nil
}