fix: 模型思考模式配置 + Unicode 截断 + 审计修复 (13 files)

模型模式:
- 新增 LLMConfig/Source.ThinkingEnabled 配置,通过 ExtraBody
  控制 DeepSeek thinking mode,默认关闭
- SeedDefaults/ToConfig 读写 core.llm.thinking_enabled
- deepseek.lua 移除硬编码 temperature=0

Unicode 截断:
- truncateStr 改按 rune 计数,修复中文截断乱码

审计修复 (Critical):
- graph.go: defer rows.Close 在 for 循环 → 显式 Close (连接池泄漏)
- cli/openclaw/plugin.go: bare type assertion → comma-ok (panic)
- channel.go: payload["type"].(string) → comma-ok (panic)
- webui/handler.go: .(string) → fmt.Sprint (panic)
- agent.go: 添加 nil provider 错误返回

审计修复 (High):
- events/bus.go: copy handler slice under RLock (data race)
- webui/handler.go: SSE 通过 channel 串行化写入 (data race)
- timer/plugin.go: time.Sleep → select with stopCh (Stop 阻塞)
- provider.go: stream ch <- 添加 select ctx.Done (goroutine 泄漏)
- main.go: outputCh goroutine 添加 ctx.Done 退出路径
This commit is contained in:
root
2026-07-03 20:46:04 +08:00
parent 197f932932
commit 239a22899b
13 changed files with 182 additions and 89 deletions

View File

@ -22,7 +22,11 @@ func init() {
plugin.RegisterFactory("cli", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
sock := DefaultSocket
if sock == "" {
sock = filepath.Join(config["data_dir"].(string), "cli.sock")
dataDir, ok := config["data_dir"].(string)
if !ok {
return nil, fmt.Errorf("cli plugin: config missing 'data_dir' or not a string")
}
sock = filepath.Join(dataDir, "cli.sock")
}
return New(name, sock), nil
})

View File

@ -17,7 +17,11 @@ func init() {
plugin.RegisterFactory("openclaw", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
dir := SkillsDir
if dir == "" {
dir = filepath.Join(config["data_dir"].(string), "skills")
dataDir, ok := config["data_dir"].(string)
if !ok {
return nil, fmt.Errorf("openclaw plugin: config missing 'data_dir' or not a string")
}
dir = filepath.Join(dataDir, "skills")
}
return New(name, dir), nil
})

View File

@ -17,9 +17,10 @@ func init() {
}
type Plugin struct {
name string
mu sync.Mutex
wg sync.WaitGroup
name string
mu sync.Mutex
wg sync.WaitGroup
stopCh chan struct{}
}
type timerTask struct {
@ -31,7 +32,7 @@ type timerTask struct {
}
func New(name string) *Plugin {
return &Plugin{name: name}
return &Plugin{name: name, stopCh: make(chan struct{})}
}
func (p *Plugin) Name() string { return p.name }
@ -75,9 +76,13 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
go func() {
defer p.wg.Done()
time.Sleep(dur)
log.Printf("[timer] firing: %s (%s later)", message, dur)
s.InjectInterruptText("timer", "timer", fmt.Sprintf("timer: %s", message))
select {
case <-time.After(dur):
log.Printf("[timer] firing: %s (%s later)", message, dur)
s.InjectInterruptText("timer", "timer", fmt.Sprintf("timer: %s", message))
case <-p.stopCh:
log.Printf("[timer] cancelled: %s", message)
}
}()
doneAt := time.Now().Add(dur)
@ -93,6 +98,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
}
func (p *Plugin) Stop() error {
close(p.stopCh)
p.wg.Wait()
return nil
}

View File

@ -558,10 +558,22 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
ticker := time.NewTicker(15 * time.Second)
defer ticker.Stop()
writeCh := make(chan string, 64)
defer close(writeCh)
go func() {
for line := range writeCh {
fmt.Fprintf(w, "%s\n", line)
flusher.Flush()
}
}()
unsub := h.eventBus.Subscribe(events.EventAll, func(evt *events.Event) {
data, _ := json.Marshal(evt)
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", evt.Type, data)
flusher.Flush()
select {
case writeCh <- fmt.Sprintf("event: %s\ndata: %s", evt.Type, string(data)):
default:
}
})
defer unsub()
@ -570,8 +582,10 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
case <-done:
return
case <-ticker.C:
fmt.Fprintf(w, ": heartbeat\n\n")
flusher.Flush()
select {
case writeCh <- ": heartbeat":
default:
}
}
}
}
@ -693,8 +707,8 @@ func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request
},
"usage": map[string]interface{}{
"prompt_tokens": len(lastMsg.Content) / 2,
"completion_tokens": len(response.Payload["content"].(string)) / 2,
"total_tokens": (len(lastMsg.Content) + len(response.Payload["content"].(string))) / 2,
"completion_tokens": len(fmt.Sprint(response.Payload["content"])) / 2,
"total_tokens": (len(lastMsg.Content) + len(fmt.Sprint(response.Payload["content"]))) / 2,
},
}