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 44926ed972
commit 720a4dcd4e
13 changed files with 182 additions and 89 deletions

View File

@ -265,13 +265,17 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req *CompletionRequest)
}
if err := decoder.Decode(&line); err != nil {
break
return
}
if len(line.Choices) > 0 {
ch <- StreamChunk{
select {
case ch <- StreamChunk{
Content: line.Choices[0].Delta.Content,
Done: line.Choices[0].FinishReason != nil,
}:
case <-ctx.Done():
return
}
}
}
@ -498,9 +502,13 @@ func (p *LuaAdaptedProvider) ChatStream(ctx context.Context, req *CompletionRequ
continue
}
if len(raw.Choices) > 0 {
ch <- StreamChunk{
select {
case ch <- StreamChunk{
Content: raw.Choices[0].Delta.Content,
Done: raw.Choices[0].FinishReason != nil,
}:
case <-ctx.Done():
return
}
}
continue
@ -509,7 +517,11 @@ func (p *LuaAdaptedProvider) ChatStream(ctx context.Context, req *CompletionRequ
// Lua 返回了变换后的统一格式
var chunk StreamChunk
if err := json.Unmarshal([]byte(unified), &chunk); err == nil {
ch <- chunk
select {
case ch <- chunk:
case <-ctx.Done():
return
}
}
}
}()

View File

@ -7,6 +7,7 @@ import (
"strings"
"sync"
"time"
"unicode/utf8"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
agentPkg "gitcode.com/JianFeeeee/HomeAgent/internal/agent"
@ -89,6 +90,9 @@ type Agent struct {
// 进行中的 LLM 请求取消函数,interceptLoop 可调用以在请求中打断
cancelLLM context.CancelFunc
llmMu sync.Mutex
// 模型思考模式(thinking/reasoning)
thinkingEnabled bool
}
type AgentConfig struct {
@ -115,6 +119,7 @@ type AgentConfig struct {
ContextSavePath string // 上下文持久化路径,空则不持久化
StageHost *StageHost
EventBus *events.Bus
ThinkingEnabled bool
}
func New(cfg AgentConfig) *Agent {
@ -156,6 +161,7 @@ func New(cfg AgentConfig) *Agent {
selfInputCh: make(chan string, 64),
childResults: make(map[string]string),
interceptCh: make(chan string, 64),
thinkingEnabled: cfg.ThinkingEnabled,
}
}
@ -383,6 +389,10 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
a.mu.Lock()
defer a.mu.Unlock()
if a.provider == nil {
return "", nil, fmt.Errorf("agent: no LLM provider configured")
}
memContext := a.buildMemoryContext(input)
sysPrompt := a.buildSystemPrompt(memContext, input)
tools := a.buildToolDefs()
@ -418,14 +428,16 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
log.Printf("[agent] interrupt injected before LLM call (turn %d)", turn)
}
eb := map[string]interface{}{}
if !a.thinkingEnabled {
eb["thinking"] = map[string]interface{}{"type": "disabled"}
}
req := &agentAPI.CompletionRequest{
Messages: msgs,
MaxTokens: 4096,
Tools: tools,
ToolChoice: "auto",
ExtraBody: map[string]interface{}{
"thinking": map[string]interface{}{"type": "disabled"},
},
ExtraBody: eb,
}
// 可取消的 LLM 调用:interceptLoop 通过 cancelLLM 打断进行中的请求
@ -1839,6 +1851,10 @@ func (a *Agent) executeSpawnChild(tc agentAPI.ToolCall) string {
// runChildTask 后台运行子 Agent 任务,完成后将结果存储并通过 selfInputCh 通知主 Agent
func (a *Agent) runChildTask(taskID, task string) {
if a.provider == nil {
log.Printf("[child] %s failed: no LLM provider configured", taskID)
return
}
log.Printf("[child] %s started: %s", taskID, truncateStr(task, 80))
sysPrompt := fmt.Sprintf(`你是 HomeAgent 的子任务助手。
@ -1871,14 +1887,16 @@ func (a *Agent) runChildTask(taskID, task string) {
var finalResult string
for turn := 0; turn < 5; turn++ {
eb := map[string]interface{}{}
if !a.thinkingEnabled {
eb["thinking"] = map[string]interface{}{"type": "disabled"}
}
req := &agentAPI.CompletionRequest{
Messages: msgs,
MaxTokens: 4096,
Tools: childTools,
ToolChoice: "auto",
ExtraBody: map[string]interface{}{
"thinking": map[string]interface{}{"type": "disabled"},
},
ExtraBody: eb,
}
resp, err := a.provider.Chat(a.ctx, req)
@ -2038,10 +2056,17 @@ func getFloat(m map[string]interface{}, key string) float64 {
}
func truncateStr(s string, max int) string {
if len(s) <= max {
if utf8.RuneCountInString(s) <= max {
return s
}
return s[:max] + "..."
var truncated int
for i := range s {
if truncated >= max {
return s[:i] + "..."
}
truncated++
}
return s
}
// drainInterrupt 非阻塞读取 interceptCh 中的一条打断消息。

View File

@ -246,10 +246,11 @@ func (m *IOManager) InjectInterrupt(source, channel string, payload map[string]i
if payload == nil {
payload = map[string]interface{}{}
}
evtType, _ := payload["type"].(string)
m.interruptCh <- &InputEvent{
RequestID: m.nextRequestID(),
Source: source,
Type: payload["type"].(string),
Type: evtType,
Payload: payload,
OutputChannel: channel,
}