mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-04 00:03:59 +00:00
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:
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@ -162,34 +163,45 @@ func main() {
|
|||||||
log.Printf("[homed] text memory active at %s", filepath.Join(cfg.Daemon.DataDir, "memory", "text"))
|
log.Printf("[homed] text memory active at %s", filepath.Join(cfg.Daemon.DataDir, "memory", "text"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx, stop := context.WithCancel(context.Background())
|
||||||
|
defer stop()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for evt := range iom.OutputChan() {
|
for {
|
||||||
if evt.Target == "memory" && evt.Type == "memory_candidate" {
|
select {
|
||||||
source, _ := evt.Payload["source"].(string)
|
case <-ctx.Done():
|
||||||
input, _ := evt.Payload["input"].(string)
|
return
|
||||||
response, _ := evt.Payload["response"].(string)
|
case evt, ok := <-iom.OutputChan():
|
||||||
toolsUsed, _ := evt.Payload["tools_used"].([]string)
|
if !ok {
|
||||||
agentID, _ := evt.Payload["agent_id"].(string)
|
return
|
||||||
|
|
||||||
if input != "" && textMem != nil {
|
|
||||||
te := text.Event{
|
|
||||||
Timestamp: time.Now().Unix(),
|
|
||||||
Source: source,
|
|
||||||
Input: input,
|
|
||||||
Response: response,
|
|
||||||
ToolsUsed: toolsUsed,
|
|
||||||
AgentID: agentID,
|
|
||||||
}
|
|
||||||
if err := textMem.Append(te); err != nil {
|
|
||||||
log.Printf("[homed] text memory append: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if evt.Target == "memory" && evt.Type == "memory_candidate" {
|
||||||
|
source, _ := evt.Payload["source"].(string)
|
||||||
|
input, _ := evt.Payload["input"].(string)
|
||||||
|
response, _ := evt.Payload["response"].(string)
|
||||||
|
toolsUsed, _ := evt.Payload["tools_used"].([]string)
|
||||||
|
agentID, _ := evt.Payload["agent_id"].(string)
|
||||||
|
|
||||||
if input != "" {
|
if input != "" && textMem != nil {
|
||||||
distiller.Append("agent", "user", input)
|
te := text.Event{
|
||||||
}
|
Timestamp: time.Now().Unix(),
|
||||||
if response != "" {
|
Source: source,
|
||||||
distiller.Append("agent", "assistant", response)
|
Input: input,
|
||||||
|
Response: response,
|
||||||
|
ToolsUsed: toolsUsed,
|
||||||
|
AgentID: agentID,
|
||||||
|
}
|
||||||
|
if err := textMem.Append(te); err != nil {
|
||||||
|
log.Printf("[homed] text memory append: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if input != "" {
|
||||||
|
distiller.Append("agent", "user", input)
|
||||||
|
}
|
||||||
|
if response != "" {
|
||||||
|
distiller.Append("agent", "assistant", response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -345,6 +357,7 @@ func main() {
|
|||||||
ContextSavePath: filepath.Join(cfg.Daemon.DataDir, "memory", "context.json"),
|
ContextSavePath: filepath.Join(cfg.Daemon.DataDir, "memory", "context.json"),
|
||||||
StageHost: stageHost,
|
StageHost: stageHost,
|
||||||
EventBus: evBus,
|
EventBus: evBus,
|
||||||
|
ThinkingEnabled: cfg.LLM.ThinkingEnabled,
|
||||||
})
|
})
|
||||||
agent.Start()
|
agent.Start()
|
||||||
defer agent.Stop()
|
defer agent.Stop()
|
||||||
|
|||||||
@ -265,13 +265,17 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req *CompletionRequest)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := decoder.Decode(&line); err != nil {
|
if err := decoder.Decode(&line); err != nil {
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(line.Choices) > 0 {
|
if len(line.Choices) > 0 {
|
||||||
ch <- StreamChunk{
|
select {
|
||||||
|
case ch <- StreamChunk{
|
||||||
Content: line.Choices[0].Delta.Content,
|
Content: line.Choices[0].Delta.Content,
|
||||||
Done: line.Choices[0].FinishReason != nil,
|
Done: line.Choices[0].FinishReason != nil,
|
||||||
|
}:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -498,9 +502,13 @@ func (p *LuaAdaptedProvider) ChatStream(ctx context.Context, req *CompletionRequ
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(raw.Choices) > 0 {
|
if len(raw.Choices) > 0 {
|
||||||
ch <- StreamChunk{
|
select {
|
||||||
|
case ch <- StreamChunk{
|
||||||
Content: raw.Choices[0].Delta.Content,
|
Content: raw.Choices[0].Delta.Content,
|
||||||
Done: raw.Choices[0].FinishReason != nil,
|
Done: raw.Choices[0].FinishReason != nil,
|
||||||
|
}:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@ -509,7 +517,11 @@ func (p *LuaAdaptedProvider) ChatStream(ctx context.Context, req *CompletionRequ
|
|||||||
// Lua 返回了变换后的统一格式
|
// Lua 返回了变换后的统一格式
|
||||||
var chunk StreamChunk
|
var chunk StreamChunk
|
||||||
if err := json.Unmarshal([]byte(unified), &chunk); err == nil {
|
if err := json.Unmarshal([]byte(unified), &chunk); err == nil {
|
||||||
ch <- chunk
|
select {
|
||||||
|
case ch <- chunk:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||||
agentPkg "gitcode.com/JianFeeeee/HomeAgent/internal/agent"
|
agentPkg "gitcode.com/JianFeeeee/HomeAgent/internal/agent"
|
||||||
@ -89,6 +90,9 @@ type Agent struct {
|
|||||||
// 进行中的 LLM 请求取消函数,interceptLoop 可调用以在请求中打断
|
// 进行中的 LLM 请求取消函数,interceptLoop 可调用以在请求中打断
|
||||||
cancelLLM context.CancelFunc
|
cancelLLM context.CancelFunc
|
||||||
llmMu sync.Mutex
|
llmMu sync.Mutex
|
||||||
|
|
||||||
|
// 模型思考模式(thinking/reasoning)
|
||||||
|
thinkingEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentConfig struct {
|
type AgentConfig struct {
|
||||||
@ -115,6 +119,7 @@ type AgentConfig struct {
|
|||||||
ContextSavePath string // 上下文持久化路径,空则不持久化
|
ContextSavePath string // 上下文持久化路径,空则不持久化
|
||||||
StageHost *StageHost
|
StageHost *StageHost
|
||||||
EventBus *events.Bus
|
EventBus *events.Bus
|
||||||
|
ThinkingEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg AgentConfig) *Agent {
|
func New(cfg AgentConfig) *Agent {
|
||||||
@ -156,6 +161,7 @@ func New(cfg AgentConfig) *Agent {
|
|||||||
selfInputCh: make(chan string, 64),
|
selfInputCh: make(chan string, 64),
|
||||||
childResults: make(map[string]string),
|
childResults: make(map[string]string),
|
||||||
interceptCh: make(chan string, 64),
|
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()
|
a.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
|
if a.provider == nil {
|
||||||
|
return "", nil, fmt.Errorf("agent: no LLM provider configured")
|
||||||
|
}
|
||||||
|
|
||||||
memContext := a.buildMemoryContext(input)
|
memContext := a.buildMemoryContext(input)
|
||||||
sysPrompt := a.buildSystemPrompt(memContext, input)
|
sysPrompt := a.buildSystemPrompt(memContext, input)
|
||||||
tools := a.buildToolDefs()
|
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)
|
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{
|
req := &agentAPI.CompletionRequest{
|
||||||
Messages: msgs,
|
Messages: msgs,
|
||||||
MaxTokens: 4096,
|
MaxTokens: 4096,
|
||||||
Tools: tools,
|
Tools: tools,
|
||||||
ToolChoice: "auto",
|
ToolChoice: "auto",
|
||||||
ExtraBody: map[string]interface{}{
|
ExtraBody: eb,
|
||||||
"thinking": map[string]interface{}{"type": "disabled"},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可取消的 LLM 调用:interceptLoop 通过 cancelLLM 打断进行中的请求
|
// 可取消的 LLM 调用:interceptLoop 通过 cancelLLM 打断进行中的请求
|
||||||
@ -1839,6 +1851,10 @@ func (a *Agent) executeSpawnChild(tc agentAPI.ToolCall) string {
|
|||||||
|
|
||||||
// runChildTask 后台运行子 Agent 任务,完成后将结果存储并通过 selfInputCh 通知主 Agent
|
// runChildTask 后台运行子 Agent 任务,完成后将结果存储并通过 selfInputCh 通知主 Agent
|
||||||
func (a *Agent) runChildTask(taskID, task string) {
|
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))
|
log.Printf("[child] %s started: %s", taskID, truncateStr(task, 80))
|
||||||
|
|
||||||
sysPrompt := fmt.Sprintf(`你是 HomeAgent 的子任务助手。
|
sysPrompt := fmt.Sprintf(`你是 HomeAgent 的子任务助手。
|
||||||
@ -1871,14 +1887,16 @@ func (a *Agent) runChildTask(taskID, task string) {
|
|||||||
|
|
||||||
var finalResult string
|
var finalResult string
|
||||||
for turn := 0; turn < 5; turn++ {
|
for turn := 0; turn < 5; turn++ {
|
||||||
|
eb := map[string]interface{}{}
|
||||||
|
if !a.thinkingEnabled {
|
||||||
|
eb["thinking"] = map[string]interface{}{"type": "disabled"}
|
||||||
|
}
|
||||||
req := &agentAPI.CompletionRequest{
|
req := &agentAPI.CompletionRequest{
|
||||||
Messages: msgs,
|
Messages: msgs,
|
||||||
MaxTokens: 4096,
|
MaxTokens: 4096,
|
||||||
Tools: childTools,
|
Tools: childTools,
|
||||||
ToolChoice: "auto",
|
ToolChoice: "auto",
|
||||||
ExtraBody: map[string]interface{}{
|
ExtraBody: eb,
|
||||||
"thinking": map[string]interface{}{"type": "disabled"},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.provider.Chat(a.ctx, req)
|
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 {
|
func truncateStr(s string, max int) string {
|
||||||
if len(s) <= max {
|
if utf8.RuneCountInString(s) <= max {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
return s[:max] + "..."
|
var truncated int
|
||||||
|
for i := range s {
|
||||||
|
if truncated >= max {
|
||||||
|
return s[:i] + "..."
|
||||||
|
}
|
||||||
|
truncated++
|
||||||
|
}
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// drainInterrupt 非阻塞读取 interceptCh 中的一条打断消息。
|
// drainInterrupt 非阻塞读取 interceptCh 中的一条打断消息。
|
||||||
|
|||||||
@ -246,10 +246,11 @@ func (m *IOManager) InjectInterrupt(source, channel string, payload map[string]i
|
|||||||
if payload == nil {
|
if payload == nil {
|
||||||
payload = map[string]interface{}{}
|
payload = map[string]interface{}{}
|
||||||
}
|
}
|
||||||
|
evtType, _ := payload["type"].(string)
|
||||||
m.interruptCh <- &InputEvent{
|
m.interruptCh <- &InputEvent{
|
||||||
RequestID: m.nextRequestID(),
|
RequestID: m.nextRequestID(),
|
||||||
Source: source,
|
Source: source,
|
||||||
Type: payload["type"].(string),
|
Type: evtType,
|
||||||
Payload: payload,
|
Payload: payload,
|
||||||
OutputChannel: channel,
|
OutputChannel: channel,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -186,25 +186,29 @@ func (r *ConfigRegistry) SeedDefaults(dataDir string) {
|
|||||||
set("core.llm.provider", "deepseek")
|
set("core.llm.provider", "deepseek")
|
||||||
set("core.llm.model", "deepseek-v4-flash")
|
set("core.llm.model", "deepseek-v4-flash")
|
||||||
set("core.llm.base_url", "https://api.deepseek.com")
|
set("core.llm.base_url", "https://api.deepseek.com")
|
||||||
|
set("core.llm.api_key", "")
|
||||||
set("core.llm.adapter", "deepseek")
|
set("core.llm.adapter", "deepseek")
|
||||||
set("core.llm.temperature", "0.7")
|
set("core.llm.temperature", "0.7")
|
||||||
set("core.llm.max_tokens", "4096")
|
set("core.llm.max_tokens", "4096")
|
||||||
|
set("core.llm.thinking_enabled", "false")
|
||||||
|
|
||||||
// llm sources
|
// llm sources
|
||||||
sources := map[string]map[string]string{
|
sources := map[string]map[string]string{
|
||||||
"deepseek": {"base_url": "https://api.deepseek.com", "model": "deepseek-v4-flash", "adapter": "deepseek", "adapter_path": "adapters/deepseek.lua"},
|
"deepseek": {"base_url": "https://api.deepseek.com", "model": "deepseek-v4-flash", "api_key": "", "thinking_enabled": "false", "adapter": "deepseek", "adapter_path": "adapters/deepseek.lua"},
|
||||||
"openai": {"base_url": "https://api.openai.com/v1", "model": "gpt-4o", "adapter": "openai", "adapter_path": "adapters/openai.lua"},
|
"openai": {"base_url": "https://api.openai.com/v1", "model": "gpt-4o", "api_key": "", "thinking_enabled": "false", "adapter": "openai", "adapter_path": "adapters/openai.lua"},
|
||||||
"anthropic": {"base_url": "https://api.anthropic.com", "model": "claude-sonnet-4-20250514", "adapter": "anthropic", "adapter_path": "adapters/anthropic.lua"},
|
"anthropic": {"base_url": "https://api.anthropic.com", "model": "claude-sonnet-4-20250514", "api_key": "", "thinking_enabled": "false", "adapter": "anthropic", "adapter_path": "adapters/anthropic.lua"},
|
||||||
"gemini": {"base_url": "https://generativelanguage.googleapis.com", "model": "gemini-2.0-flash", "adapter": "gemini", "adapter_path": "adapters/gemini.lua"},
|
"gemini": {"base_url": "https://generativelanguage.googleapis.com", "model": "gemini-2.0-flash", "api_key": "", "thinking_enabled": "false", "adapter": "gemini", "adapter_path": "adapters/gemini.lua"},
|
||||||
"mistral": {"base_url": "https://api.mistral.ai", "model": "mistral-large-latest", "adapter": "mistral", "adapter_path": "adapters/mistral.lua"},
|
"mistral": {"base_url": "https://api.mistral.ai", "model": "mistral-large-latest", "api_key": "", "thinking_enabled": "false", "adapter": "mistral", "adapter_path": "adapters/mistral.lua"},
|
||||||
"groq": {"base_url": "https://api.groq.com", "model": "llama3-70b-8192", "adapter": "groq", "adapter_path": "adapters/groq.lua"},
|
"groq": {"base_url": "https://api.groq.com", "model": "llama3-70b-8192", "api_key": "", "thinking_enabled": "false", "adapter": "groq", "adapter_path": "adapters/groq.lua"},
|
||||||
"github": {"base_url": "https://models.inference.ai.azure.com", "model": "gpt-4o", "adapter": "github", "adapter_path": "adapters/github.lua"},
|
"github": {"base_url": "https://models.inference.ai.azure.com", "model": "gpt-4o", "api_key": "", "thinking_enabled": "false", "adapter": "github", "adapter_path": "adapters/github.lua"},
|
||||||
"ollama": {"base_url": "http://localhost:11434", "model": "llama3", "adapter": "ollama", "adapter_path": "adapters/ollama.lua"},
|
"ollama": {"base_url": "http://localhost:11434", "model": "llama3", "api_key": "", "thinking_enabled": "false", "adapter": "ollama", "adapter_path": "adapters/ollama.lua"},
|
||||||
}
|
}
|
||||||
for name, props := range sources {
|
for name, props := range sources {
|
||||||
p := "core.llm.sources." + name
|
p := "core.llm.sources." + name
|
||||||
set(p+".base_url", props["base_url"])
|
set(p+".base_url", props["base_url"])
|
||||||
set(p+".model", props["model"])
|
set(p+".model", props["model"])
|
||||||
|
set(p+".api_key", props["api_key"])
|
||||||
|
set(p+".thinking_enabled", props["thinking_enabled"])
|
||||||
set(p+".adapter", props["adapter"])
|
set(p+".adapter", props["adapter"])
|
||||||
set(p+".adapter_path", props["adapter_path"])
|
set(p+".adapter_path", props["adapter_path"])
|
||||||
}
|
}
|
||||||
@ -345,9 +349,11 @@ func (r *ConfigRegistry) ToConfig() *types.Config {
|
|||||||
cfg.LLM.Provider = read("core.llm.provider", cfg.LLM.Provider)
|
cfg.LLM.Provider = read("core.llm.provider", cfg.LLM.Provider)
|
||||||
cfg.LLM.Model = read("core.llm.model", cfg.LLM.Model)
|
cfg.LLM.Model = read("core.llm.model", cfg.LLM.Model)
|
||||||
cfg.LLM.BaseURL = read("core.llm.base_url", cfg.LLM.BaseURL)
|
cfg.LLM.BaseURL = read("core.llm.base_url", cfg.LLM.BaseURL)
|
||||||
|
cfg.LLM.APIKey = read("core.llm.api_key", cfg.LLM.APIKey)
|
||||||
cfg.LLM.Adapter = read("core.llm.adapter", cfg.LLM.Adapter)
|
cfg.LLM.Adapter = read("core.llm.adapter", cfg.LLM.Adapter)
|
||||||
cfg.LLM.Temperature = float64(readInt("core.llm.temperature", int(cfg.LLM.Temperature*100))) / 100
|
cfg.LLM.Temperature = float64(readInt("core.llm.temperature", int(cfg.LLM.Temperature*100))) / 100
|
||||||
cfg.LLM.MaxTokens = readInt("core.llm.max_tokens", cfg.LLM.MaxTokens)
|
cfg.LLM.MaxTokens = readInt("core.llm.max_tokens", cfg.LLM.MaxTokens)
|
||||||
|
cfg.LLM.ThinkingEnabled = readBool("core.llm.thinking_enabled", cfg.LLM.ThinkingEnabled)
|
||||||
|
|
||||||
// 重建 sources —— 从 DB 中按前缀扫描,按名称排序保证确定性
|
// 重建 sources —— 从 DB 中按前缀扫描,按名称排序保证确定性
|
||||||
sourceNames := make([]string, 0)
|
sourceNames := make([]string, 0)
|
||||||
@ -362,11 +368,13 @@ func (r *ConfigRegistry) ToConfig() *types.Config {
|
|||||||
for _, name := range sourceNames {
|
for _, name := range sourceNames {
|
||||||
p := "core.llm.sources." + name
|
p := "core.llm.sources." + name
|
||||||
cfg.LLM.Sources = append(cfg.LLM.Sources, types.LLMSource{
|
cfg.LLM.Sources = append(cfg.LLM.Sources, types.LLMSource{
|
||||||
Name: name,
|
Name: name,
|
||||||
BaseURL: read(p+".base_url", ""),
|
BaseURL: read(p+".base_url", ""),
|
||||||
Model: read(p+".model", ""),
|
Model: read(p+".model", ""),
|
||||||
Adapter: read(p+".adapter", ""),
|
APIKey: read(p+".api_key", ""),
|
||||||
AdapterPath: read(p+".adapter_path", ""),
|
Adapter: read(p+".adapter", ""),
|
||||||
|
AdapterPath: read(p+".adapter_path", ""),
|
||||||
|
ThinkingEnabled: readBool(p+".thinking_enabled", false),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -38,8 +38,10 @@ func NewBus() *Bus {
|
|||||||
|
|
||||||
func (b *Bus) Publish(evt *Event) {
|
func (b *Bus) Publish(evt *Event) {
|
||||||
b.mu.RLock()
|
b.mu.RLock()
|
||||||
allHandlers := b.subs[EventAll]
|
allHandlers := make([]Handler, len(b.subs[EventAll]))
|
||||||
typeHandlers := b.subs[evt.Type]
|
copy(allHandlers, b.subs[EventAll])
|
||||||
|
typeHandlers := make([]Handler, len(b.subs[evt.Type]))
|
||||||
|
copy(typeHandlers, b.subs[evt.Type])
|
||||||
b.mu.RUnlock()
|
b.mu.RUnlock()
|
||||||
|
|
||||||
for _, h := range allHandlers {
|
for _, h := range allHandlers {
|
||||||
|
|||||||
@ -5,12 +5,11 @@ adapter.version = "2.0.0"
|
|||||||
adapter.endpoint = "/chat/completions"
|
adapter.endpoint = "/chat/completions"
|
||||||
adapter.headers = {}
|
adapter.headers = {}
|
||||||
|
|
||||||
-- DeepSeek 格式与 OpenAI 兼容,只需要强制 temperature=0(禁用 thinking)
|
-- DeepSeek 格式与 OpenAI 兼容,thinking 模式由 Go 端 ExtraBody 控制
|
||||||
function adapter.transform_request(raw_body)
|
function adapter.transform_request(raw_body)
|
||||||
local ok, req = pcall(json.decode, raw_body)
|
local ok, req = pcall(json.decode, raw_body)
|
||||||
if not ok then return raw_body end
|
if not ok then return raw_body end
|
||||||
req.model = req.model or "deepseek-chat"
|
req.model = req.model or "deepseek-chat"
|
||||||
req.temperature = 0.0
|
|
||||||
req.stream = req.stream or false
|
req.stream = req.stream or false
|
||||||
return json.encode(req)
|
return json.encode(req)
|
||||||
end
|
end
|
||||||
|
|||||||
@ -272,11 +272,11 @@ func (g *GraphDB) Recall(keywords []string, seedEntities []string, depth int, se
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var e Entity
|
var e Entity
|
||||||
if err := rows.Scan(&e.ID, &e.Name, &e.Type, &e.MentionCount, &e.CreatedAt, &e.UpdatedAt); err != nil {
|
if err := rows.Scan(&e.ID, &e.Name, &e.Type, &e.MentionCount, &e.CreatedAt, &e.UpdatedAt); err != nil {
|
||||||
|
rows.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !entityIDs[e.ID] {
|
if !entityIDs[e.ID] {
|
||||||
@ -284,6 +284,7 @@ func (g *GraphDB) Recall(keywords []string, seedEntities []string, depth int, se
|
|||||||
result.Entities = append(result.Entities, e)
|
result.Entities = append(result.Entities, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
rows.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, se := range seedEntities {
|
for _, se := range seedEntities {
|
||||||
@ -336,7 +337,6 @@ func (g *GraphDB) Recall(keywords []string, seedEntities []string, depth int, se
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer relRows.Close()
|
|
||||||
|
|
||||||
newIDs := make(map[int64]bool)
|
newIDs := make(map[int64]bool)
|
||||||
for relRows.Next() {
|
for relRows.Next() {
|
||||||
@ -345,6 +345,7 @@ func (g *GraphDB) Recall(keywords []string, seedEntities []string, depth int, se
|
|||||||
&rel.SourceName, &rel.TargetName, &rel.RelationType,
|
&rel.SourceName, &rel.TargetName, &rel.RelationType,
|
||||||
&rel.Confidence, &rel.Status, &rel.SessionID,
|
&rel.Confidence, &rel.Status, &rel.SessionID,
|
||||||
&rel.TurnID, &rel.CreatedAt, &rel.DateBucket); err != nil {
|
&rel.TurnID, &rel.CreatedAt, &rel.DateBucket); err != nil {
|
||||||
|
relRows.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result.Relations = append(result.Relations, rel)
|
result.Relations = append(result.Relations, rel)
|
||||||
@ -356,6 +357,7 @@ func (g *GraphDB) Recall(keywords []string, seedEntities []string, depth int, se
|
|||||||
newIDs[rel.TargetID] = true
|
newIDs[rel.TargetID] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
relRows.Close()
|
||||||
|
|
||||||
if len(newIDs) == 0 {
|
if len(newIDs) == 0 {
|
||||||
break
|
break
|
||||||
@ -375,11 +377,11 @@ func (g *GraphDB) Recall(keywords []string, seedEntities []string, depth int, se
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer eRows.Close()
|
|
||||||
|
|
||||||
for eRows.Next() {
|
for eRows.Next() {
|
||||||
var e Entity
|
var e Entity
|
||||||
if err := eRows.Scan(&e.ID, &e.Name, &e.Type, &e.MentionCount, &e.CreatedAt, &e.UpdatedAt); err != nil {
|
if err := eRows.Scan(&e.ID, &e.Name, &e.Type, &e.MentionCount, &e.CreatedAt, &e.UpdatedAt); err != nil {
|
||||||
|
eRows.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !entityIDs[e.ID] {
|
if !entityIDs[e.ID] {
|
||||||
@ -387,6 +389,7 @@ func (g *GraphDB) Recall(keywords []string, seedEntities []string, depth int, se
|
|||||||
result.Entities = append(result.Entities, e)
|
result.Entities = append(result.Entities, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
eRows.Close()
|
||||||
|
|
||||||
for id := range newIDs {
|
for id := range newIDs {
|
||||||
entityIDs[id] = true
|
entityIDs[id] = true
|
||||||
@ -408,13 +411,13 @@ func (g *GraphDB) Purge(criteria map[string]string, mode string) (int, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
var ids []interface{}
|
var ids []interface{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var id int64
|
var id int64
|
||||||
rows.Scan(&id)
|
rows.Scan(&id)
|
||||||
ids = append(ids, id)
|
ids = append(ids, id)
|
||||||
}
|
}
|
||||||
|
rows.Close()
|
||||||
if len(ids) > 0 {
|
if len(ids) > 0 {
|
||||||
conds = append(conds, fmt.Sprintf("source_id IN (%s)", placeholders(len(ids))))
|
conds = append(conds, fmt.Sprintf("source_id IN (%s)", placeholders(len(ids))))
|
||||||
args = append(args, ids...)
|
args = append(args, ids...)
|
||||||
@ -426,13 +429,13 @@ func (g *GraphDB) Purge(criteria map[string]string, mode string) (int, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
var ids []interface{}
|
var ids []interface{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var id int64
|
var id int64
|
||||||
rows.Scan(&id)
|
rows.Scan(&id)
|
||||||
ids = append(ids, id)
|
ids = append(ids, id)
|
||||||
}
|
}
|
||||||
|
rows.Close()
|
||||||
if len(ids) > 0 {
|
if len(ids) > 0 {
|
||||||
conds = append(conds, fmt.Sprintf("target_id IN (%s)", placeholders(len(ids))))
|
conds = append(conds, fmt.Sprintf("target_id IN (%s)", placeholders(len(ids))))
|
||||||
args = append(args, ids...)
|
args = append(args, ids...)
|
||||||
|
|||||||
@ -22,7 +22,11 @@ func init() {
|
|||||||
plugin.RegisterFactory("cli", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
plugin.RegisterFactory("cli", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
sock := DefaultSocket
|
sock := DefaultSocket
|
||||||
if sock == "" {
|
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
|
return New(name, sock), nil
|
||||||
})
|
})
|
||||||
|
|||||||
@ -17,7 +17,11 @@ func init() {
|
|||||||
plugin.RegisterFactory("openclaw", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
plugin.RegisterFactory("openclaw", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
dir := SkillsDir
|
dir := SkillsDir
|
||||||
if dir == "" {
|
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
|
return New(name, dir), nil
|
||||||
})
|
})
|
||||||
|
|||||||
@ -17,9 +17,10 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Plugin struct {
|
type Plugin struct {
|
||||||
name string
|
name string
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
stopCh chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type timerTask struct {
|
type timerTask struct {
|
||||||
@ -31,7 +32,7 @@ type timerTask struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(name string) *Plugin {
|
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 }
|
func (p *Plugin) Name() string { return p.name }
|
||||||
@ -75,9 +76,13 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer p.wg.Done()
|
defer p.wg.Done()
|
||||||
time.Sleep(dur)
|
select {
|
||||||
log.Printf("[timer] firing: %s (%s later)", message, dur)
|
case <-time.After(dur):
|
||||||
s.InjectInterruptText("timer", "timer", fmt.Sprintf("timer: %s", message))
|
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)
|
doneAt := time.Now().Add(dur)
|
||||||
@ -93,6 +98,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) Stop() error {
|
func (p *Plugin) Stop() error {
|
||||||
|
close(p.stopCh)
|
||||||
p.wg.Wait()
|
p.wg.Wait()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -558,10 +558,22 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
ticker := time.NewTicker(15 * time.Second)
|
ticker := time.NewTicker(15 * time.Second)
|
||||||
defer ticker.Stop()
|
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) {
|
unsub := h.eventBus.Subscribe(events.EventAll, func(evt *events.Event) {
|
||||||
data, _ := json.Marshal(evt)
|
data, _ := json.Marshal(evt)
|
||||||
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", evt.Type, data)
|
select {
|
||||||
flusher.Flush()
|
case writeCh <- fmt.Sprintf("event: %s\ndata: %s", evt.Type, string(data)):
|
||||||
|
default:
|
||||||
|
}
|
||||||
})
|
})
|
||||||
defer unsub()
|
defer unsub()
|
||||||
|
|
||||||
@ -570,8 +582,10 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
case <-done:
|
case <-done:
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
fmt.Fprintf(w, ": heartbeat\n\n")
|
select {
|
||||||
flusher.Flush()
|
case writeCh <- ": heartbeat":
|
||||||
|
default:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -693,8 +707,8 @@ func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request
|
|||||||
},
|
},
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]interface{}{
|
||||||
"prompt_tokens": len(lastMsg.Content) / 2,
|
"prompt_tokens": len(lastMsg.Content) / 2,
|
||||||
"completion_tokens": len(response.Payload["content"].(string)) / 2,
|
"completion_tokens": len(fmt.Sprint(response.Payload["content"])) / 2,
|
||||||
"total_tokens": (len(lastMsg.Content) + len(response.Payload["content"].(string))) / 2,
|
"total_tokens": (len(lastMsg.Content) + len(fmt.Sprint(response.Payload["content"]))) / 2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -94,23 +94,25 @@ type OperationLog struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LLMSource struct {
|
type LLMSource struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
BaseURL string `json:"base_url"`
|
BaseURL string `json:"base_url"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
APIKey string `json:"api_key,omitempty"`
|
APIKey string `json:"api_key,omitempty"`
|
||||||
Adapter string `json:"adapter"`
|
Adapter string `json:"adapter"`
|
||||||
AdapterPath string `json:"adapter_path,omitempty"`
|
AdapterPath string `json:"adapter_path,omitempty"`
|
||||||
|
ThinkingEnabled bool `json:"thinking_enabled,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LLMConfig struct {
|
type LLMConfig struct {
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
BaseURL string `json:"base_url"`
|
BaseURL string `json:"base_url"`
|
||||||
APIKey string `json:"api_key"`
|
APIKey string `json:"api_key"`
|
||||||
Adapter string `json:"adapter"`
|
Adapter string `json:"adapter"`
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature float64 `json:"temperature"`
|
||||||
MaxTokens int `json:"max_tokens"`
|
MaxTokens int `json:"max_tokens"`
|
||||||
Sources []LLMSource `json:"sources,omitempty"`
|
ThinkingEnabled bool `json:"thinking_enabled"`
|
||||||
|
Sources []LLMSource `json:"sources,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user