From e4c744a318a83149d46b27a6074e0f90d895e6dc Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Sat, 12 Sep 2026 07:13:46 -0400 Subject: [PATCH] =?UTF-8?q?feat(search):=20=E7=BD=91=E7=BB=9C=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=96=B0=E5=A2=9E=20DeepSeek=20=E5=AE=98=E6=96=B9?= =?UTF-8?q?=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 不单独配置凭据,直接复用当前激活的 LLM 配置(base_url/api_key/model)。 这个来源与 ddgs/brave/tavily 性质不同:DeepSeek 没有可直接调用的搜索接口, 搜索只存在于其 Anthropic 兼容 messages 接口内部,以 web_search_20250305 服务端工具的形式由 DeepSeek 服务端执行。由此带来几个必须让用户知道的差异, 已在设置页写明: - 仅支持 DeepSeek 官方端点 + anthropic 协议。实测其 OpenAI 协议端点会直接以 "unknown variant, expected `function`" 拒绝一切服务端工具,web_search_options 之类的顶层字段则被静默忽略,没有可用的替代写法。 - 每次搜索额外消耗一次模型调用。 - 搜索请求由 DeepSeek 服务端发出,不经过搜索出口代理,也不计入流量留痕。 - 返回结果只有标题与链接,没有摘要(DeepSeek 只给 encrypted_content),需要正文 时仍由 WebFetch 抓取。 上述条件不做程序校验,仅在界面说明,由用户自行确认——配置不满足时搜索会在实际 调用时报错,与其它来源填错 Key 的反馈路径一致,可用「测试搜索」按钮实跑验证。 依赖 norma v0.3.8。 --- CHANGELOG.md | 6 +++++ agent/chat.go | 25 ++++++++++-------- agent/mainagent.go | 25 ++++++++++-------- agent/planner.go | 25 ++++++++++-------- agent/worker.go | 28 +++++++++++++++------ go.mod | 2 +- go.sum | 2 ++ server/manager.go | 24 +++++++++++++++++- server/server.go | 16 ++++++++++-- web/src/app/(main)/system/settings/page.tsx | 25 ++++++++++++++++-- web/src/lib/types.ts | 2 +- 11 files changed, 133 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cabeaaa..54cc3ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ## [Unreleased] +### 网络 + +#### 新增的功能 + +- **网络搜索新增 DeepSeek 官方来源**:直接复用当前激活的 LLM 配置。它与其余三个来源性质不同——DeepSeek 没有可直接调用的搜索接口,搜索只存在于其 Anthropic 兼容接口内部(`web_search_20250305` 服务端工具),由 DeepSeek 服务端执行。因此它**仅支持 DeepSeek 官方端点 + anthropic 协议**(OpenAI 协议端点会直接拒绝服务端工具),且每次搜索会**额外消耗一次模型调用**、请求**不经过搜索出口代理**、也**不计入流量留痕**,返回结果**只有标题与链接**(无摘要,需正文时由 WebFetch 抓取)。设置页说明上述限制但**不做校验拦截**,是否满足由用户自行确认,可用「测试搜索」按钮实跑一次验证。 + ### Agent #### 修改的功能 diff --git a/agent/chat.go b/agent/chat.go index fbf0832..99b2095 100644 --- a/agent/chat.go +++ b/agent/chat.go @@ -129,17 +129,20 @@ func (c *ChatAgent) Chat(ctx context.Context, agentKey, sessionID, message strin WebFetchCACert: c.proxyCACert, // 联网搜索(可选)。ddgs 无需 key;brave-free 需 BraveKey;tavily 需 TavilyKey。 // WebSearchProxy 是独立出口代理(http/https/socks5),与记录流量的 MITM 代理无关;空则直连。 - EnableWebSearch: ws.Enabled, - WebSearchBackend: ws.Backend, - BraveSearchAPIKey: ws.BraveKey, - TavilySearchAPIKey: ws.TavilyKey, - WebSearchProxy: ws.Proxy, - BashEnv: proxyEnv(c.proxyAddr, c.proxyCACert), // Bash 子命令默认走代理+信任 CA - WorkingDir: sessionWorkDir, - MaxTurns: maxTurns, - MaxDuration: maxDuration, - Compaction: compactionConfig(c.window), - Todos: actool.NewTodoStore(), + EnableWebSearch: ws.Enabled, + WebSearchBackend: ws.Backend, + BraveSearchAPIKey: ws.BraveKey, + TavilySearchAPIKey: ws.TavilyKey, + DeepSeekSearchBaseURL: ws.DeepSeekBaseURL, + DeepSeekSearchAPIKey: ws.DeepSeekAPIKey, + DeepSeekSearchModel: ws.DeepSeekModel, + WebSearchProxy: ws.Proxy, + BashEnv: proxyEnv(c.proxyAddr, c.proxyCACert), // Bash 子命令默认走代理+信任 CA + WorkingDir: sessionWorkDir, + MaxTurns: maxTurns, + MaxDuration: maxDuration, + Compaction: compactionConfig(c.window), + Todos: actool.NewTodoStore(), // large tool output spills to cmd-output/ under the session dir. // 截断上限用 SDK 默认(tool.Capture 的 30000 字符)。 ToolOutputDir: filepath.Join(sessionWorkDir, "cmd-output"), diff --git a/agent/mainagent.go b/agent/mainagent.go index 6e14ce8..513706d 100644 --- a/agent/mainagent.go +++ b/agent/mainagent.go @@ -136,17 +136,20 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, mainSeg int, as *db. WebFetchCACert: m.proxyCACert, // 联网搜索(可选)。ddgs 无需 key;brave-free 需 BraveKey;tavily 需 TavilyKey。 // WebSearchProxy 是独立出口代理(http/https/socks5),与记录流量的 MITM 代理无关;空则直连。 - EnableWebSearch: m.webSearch.Enabled, - WebSearchBackend: m.webSearch.Backend, - BraveSearchAPIKey: m.webSearch.BraveKey, - TavilySearchAPIKey: m.webSearch.TavilyKey, - WebSearchProxy: m.webSearch.Proxy, - BashEnv: proxyEnv(m.proxyAddr, m.proxyCACert), // Bash 子命令默认走代理+信任 CA - WorkingDir: mainDir, // 本任务工作目录 /tasks/ - ToolOutputDir: cmdOutDir(mainDir), - MaxTurns: m.maxTurns, // 0 = unlimited (configurable in agent management) - Compaction: compactionConfig(m.compactionWindow()), // long chats stay within the window - Todos: actool.NewTodoStore(), // 会话级临时待办(TodoWrite),纯规划用,退出即丢 + EnableWebSearch: m.webSearch.Enabled, + WebSearchBackend: m.webSearch.Backend, + BraveSearchAPIKey: m.webSearch.BraveKey, + TavilySearchAPIKey: m.webSearch.TavilyKey, + DeepSeekSearchBaseURL: m.webSearch.DeepSeekBaseURL, + DeepSeekSearchAPIKey: m.webSearch.DeepSeekAPIKey, + DeepSeekSearchModel: m.webSearch.DeepSeekModel, + WebSearchProxy: m.webSearch.Proxy, + BashEnv: proxyEnv(m.proxyAddr, m.proxyCACert), // Bash 子命令默认走代理+信任 CA + WorkingDir: mainDir, // 本任务工作目录 /tasks/ + ToolOutputDir: cmdOutDir(mainDir), + MaxTurns: m.maxTurns, // 0 = unlimited (configurable in agent management) + Compaction: compactionConfig(m.compactionWindow()), // long chats stay within the window + Todos: actool.NewTodoStore(), // 会话级临时待办(TodoWrite),纯规划用,退出即丢 // 命中预算(步数)→ SDK 跑收尾:向用户输出一句进展总结。Prompt 与收尾轮数可后台编辑(默认 10 轮)。 Settlement: wrapupSettlement("mainagent", nil), NonStreaming: m.nonStreaming(), // 该 profile 选非流式时走 Provider.Complete diff --git a/agent/planner.go b/agent/planner.go index ccf1415..7e2651b 100644 --- a/agent/planner.go +++ b/agent/planner.go @@ -394,17 +394,20 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts WebFetchCACert: p.proxyCACert, // 联网搜索(可选)。ddgs 无需 key;brave-free 需 BraveKey;tavily 需 TavilyKey。 // WebSearchProxy 是独立出口代理(http/https/socks5),与记录流量的 MITM 代理无关;空则直连。 - EnableWebSearch: p.webSearch.Enabled, - WebSearchBackend: p.webSearch.Backend, - BraveSearchAPIKey: p.webSearch.BraveKey, - TavilySearchAPIKey: p.webSearch.TavilyKey, - WebSearchProxy: p.webSearch.Proxy, - BashEnv: proxyEnv(p.proxyAddr, p.proxyCACert), // Bash 子命令默认走代理+信任 CA - WorkingDir: taskDir, // 本任务工作目录 /tasks/ - ToolOutputDir: cmdOutDir(taskDir), - MaxTurns: p.maxTurns, // 0 = unlimited (configurable in agent management) - MaxDuration: maxDur, // 0=不限;有 deadline 时=距 deadline 剩余 - Compaction: compactionConfig(p.compactionWindow()), + EnableWebSearch: p.webSearch.Enabled, + WebSearchBackend: p.webSearch.Backend, + BraveSearchAPIKey: p.webSearch.BraveKey, + TavilySearchAPIKey: p.webSearch.TavilyKey, + DeepSeekSearchBaseURL: p.webSearch.DeepSeekBaseURL, + DeepSeekSearchAPIKey: p.webSearch.DeepSeekAPIKey, + DeepSeekSearchModel: p.webSearch.DeepSeekModel, + WebSearchProxy: p.webSearch.Proxy, + BashEnv: proxyEnv(p.proxyAddr, p.proxyCACert), // Bash 子命令默认走代理+信任 CA + WorkingDir: taskDir, // 本任务工作目录 /tasks/ + ToolOutputDir: cmdOutDir(taskDir), + MaxTurns: p.maxTurns, // 0 = unlimited (configurable in agent management) + MaxDuration: maxDur, // 0=不限;有 deadline 时=距 deadline 剩余 + Compaction: compactionConfig(p.compactionWindow()), // 跨唤醒共享的规划待办:让串行链在多轮之间保留(session 是新的,store 不是)。 Todos: p.todoFor(ts.ID()), // 命中【本轮】步数预算→ SDK 跑收尾:把本轮已想清楚的结论落地(该派的 add_intent、 diff --git a/agent/worker.go b/agent/worker.go index 7dd30b0..65b499f 100644 --- a/agent/worker.go +++ b/agent/worker.go @@ -26,17 +26,28 @@ import ( // goal on its own. Multiple workers run concurrently as goroutines. // WebSearchOpts is the web-search backend selection the server pushes into each // agent (planner/worker/main). Enabled=false leaves the web_search tool off. -// Backend is "ddgs" (no key), "brave-free" (BraveKey required), or "tavily" -// (TavilyKey required). It maps directly onto agentcore.Options. +// Backend is "ddgs" (no key), "brave-free" (BraveKey required), "tavily" +// (TavilyKey required), or "deepseek" (DeepSeek* required, filled from the +// active LLM profile). It maps directly onto agentcore.Options. // Proxy is a dedicated egress proxy for the search request (http/https/socks5), // independent of the traffic-recording MITM proxy — set it when the search endpoint // is only reachable via a VPN/SOCKS proxy. Empty = direct. +// +// 注意 deepseek 后端与其它三个的性质不同:DeepSeek 没有可直接调用的搜索接口, +// 搜索只存在于其 Anthropic 兼容 messages 接口内部(web_search_20250305 server +// tool),因此每次搜索会消耗一次模型调用,且搜索请求由 DeepSeek 服务端发出—— +// 不经过本机 Proxy,也不会进流量留痕。 type WebSearchOpts struct { Enabled bool Backend string BraveKey string TavilyKey string Proxy string + // DeepSeek* 来自当前激活的 LLM 配置(仅 anthropic 格式的 DeepSeek 官方端点), + // 不单独配置,随 LLM 配置切换而变。 + DeepSeekBaseURL string + DeepSeekAPIKey string + DeepSeekModel string } type Worker struct { @@ -417,11 +428,14 @@ func (w *Worker) execute(ctx context.Context, name string, taskID int64, as *db. WebFetchCACert: w.proxyCACert, // 联网搜索(可选)。ddgs 无需 key;brave-free 需 BraveKey;tavily 需 TavilyKey。 // WebSearchProxy 是独立的出口代理(http/https/socks5),与记录流量的 MITM 代理无关;空则直连。 - EnableWebSearch: w.webSearch.Enabled, - WebSearchBackend: w.webSearch.Backend, - BraveSearchAPIKey: w.webSearch.BraveKey, - TavilySearchAPIKey: w.webSearch.TavilyKey, - WebSearchProxy: w.webSearch.Proxy, + EnableWebSearch: w.webSearch.Enabled, + WebSearchBackend: w.webSearch.Backend, + BraveSearchAPIKey: w.webSearch.BraveKey, + TavilySearchAPIKey: w.webSearch.TavilyKey, + DeepSeekSearchBaseURL: w.webSearch.DeepSeekBaseURL, + DeepSeekSearchAPIKey: w.webSearch.DeepSeekAPIKey, + DeepSeekSearchModel: w.webSearch.DeepSeekModel, + WebSearchProxy: w.webSearch.Proxy, // Bash 子命令的 HTTP 默认走记录代理 + 信任其 CA(工具无需 -x/-k)。 BashEnv: proxyEnv(w.proxyAddr, w.proxyCACert), WorkingDir: runDir, diff --git a/go.mod b/go.mod index 2d7fd0a..4c1e217 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Autumn-27/artex go 1.26.3 require ( - github.com/Autumn-27/norma v0.3.7 + github.com/Autumn-27/norma v0.3.8 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.10.0 diff --git a/go.sum b/go.sum index d518878..a3312de 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/Autumn-27/norma v0.3.7 h1:BdzVV3LUqN3zzaoL5cqY9aq0l/ucOYonSewQHhoL8Wo= github.com/Autumn-27/norma v0.3.7/go.mod h1:BudWrf7iHl96FgHqFr/HwN3mKYmGjJDt+xL2ZHXfFZw= +github.com/Autumn-27/norma v0.3.8 h1:MOz9bekMyQ29cgt728nZ4gq+HjpeDgQwCTd/7dtBvuc= +github.com/Autumn-27/norma v0.3.8/go.mod h1:BudWrf7iHl96FgHqFr/HwN3mKYmGjJDt+xL2ZHXfFZw= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= diff --git a/server/manager.go b/server/manager.go index cbeac66..5101e6f 100644 --- a/server/manager.go +++ b/server/manager.go @@ -273,6 +273,9 @@ const ( settingConcurrencyLimit = "task_concurrency_limit" // defaultWebSearchBackend is used when web search is on but no backend was picked. defaultWebSearchBackend = "ddgs" + // deepSeekWebSearchBackend borrows the active LLM profile instead of its own + // key, so it only works on an anthropic-format profile pointed at DeepSeek. + deepSeekWebSearchBackend = "deepseek" // defaultWorkers is the concurrent work-agent count when the setting is unset. defaultWorkers = 3 // defaultConcurrencyLimit is the simultaneous-running-task cap when the feature @@ -539,7 +542,26 @@ func (m *Manager) WebSearchOpts() agent.WebSearchOpts { if on && backend == "tavily" && strings.TrimSpace(tavilyKey) == "" { on = false } - return agent.WebSearchOpts{Enabled: on, Backend: backend, BraveKey: braveKey, TavilyKey: tavilyKey, Proxy: proxy} + o := agent.WebSearchOpts{Enabled: on, Backend: backend, BraveKey: braveKey, TavilyKey: tavilyKey, Proxy: proxy} + if backend == deepSeekWebSearchBackend { + o.DeepSeekBaseURL, o.DeepSeekAPIKey, o.DeepSeekModel = m.deepSeekSearchCreds() + } + return o +} + +// deepSeekSearchCreds resolves the credentials the "deepseek" search backend +// borrows from the active LLM profile (it has no key of its own). Whether that +// profile can actually drive server-side search — DeepSeek exposes it only on +// the Anthropic-format endpoint — is deliberately NOT validated here: the UI +// states the requirement and the user decides. A profile that can't serve it +// simply fails at search time (or at the settings page's 测试 button), which is +// the same feedback every other backend gives for a bad key. +func (m *Manager) deepSeekSearchCreds() (baseURL, apiKey, model string) { + p, err := m.pg.ActiveProfile() + if err != nil || p == nil { + return "", "", "" + } + return p.BaseURL, p.APIKey, p.Model } // SetWebSearch persists and applies the web-search settings. braveKey, tavilyKey, and diff --git a/server/server.go b/server/server.go index 7db9486..011d68a 100644 --- a/server/server.go +++ b/server/server.go @@ -3413,10 +3413,22 @@ func (s *Server) testWebSearch(w http.ResponseWriter, r *http.Request) { if strings.TrimSpace(req.TavilyKey) != "" { tavilyKey = req.TavilyKey } + cfg := actool.WebSearchConfig{Backend: backend, BraveAPIKey: braveKey, TavilyAPIKey: tavilyKey, Proxy: proxy} // Hard cap so a slow/blocked proxy can't hang the request. - ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) + wall := 30 * time.Second + // deepseek 的凭据不在表单里,来自当前激活的 LLM 配置;同时它每次搜索都跑一次 + // 模型推理,30s 的通用上限偏紧,单独放宽。这里不预判配置能不能用——测这一下 + // 本来就是给用户自己确认的手段,真跑不通时下面的报错比预判更有信息量。 + probeQuery := "test" + if strings.TrimSpace(backend) == deepSeekWebSearchBackend { + cfg.DeepSeekBaseURL, cfg.DeepSeekAPIKey, cfg.DeepSeekModel = s.m.deepSeekSearchCreds() + wall = 120 * time.Second + // 搜索词由 DeepSeek 端的模型自行决定,"test" 太空泛会让它跳过搜索直接作答。 + probeQuery = "DeepSeek company official website" + } + ctx, cancel := context.WithTimeout(r.Context(), wall) defer cancel() - results, err := actool.WebSearchProbe(ctx, actool.WebSearchConfig{Backend: backend, BraveAPIKey: braveKey, TavilyAPIKey: tavilyKey, Proxy: proxy}, "test", 3) + results, err := actool.WebSearchProbe(ctx, cfg, probeQuery, 3) if err != nil { writeJSON(w, 200, map[string]any{"ok": false, "error": err.Error(), "backend": backend}) return diff --git a/web/src/app/(main)/system/settings/page.tsx b/web/src/app/(main)/system/settings/page.tsx index 71e945f..ab5659a 100644 --- a/web/src/app/(main)/system/settings/page.tsx +++ b/web/src/app/(main)/system/settings/page.tsx @@ -398,8 +398,9 @@ export default function SystemSettingsPage() { web_search(仅返回标题/链接/摘要,不抓取正文;抓取由 WebFetch 负责)。网络搜索不走 记录代理,独立于流量捕获。
- 来源可选 DuckDuckGo(ddgs)(无需 Key)、Brave(免费版)(需填写 Brave API Key)或{" "} - Tavily(需填写 Tavily API Key)。总开关关闭时,各 Agent 的网络搜索开关不可用。 + 来源可选 DuckDuckGo(ddgs)(无需 Key)、Brave(免费版)(需填写 Brave API Key)、{" "} + Tavily(需填写 Tavily API Key)或 DeepSeek(复用当前 LLM 配置)。总开关关闭时,各 + Agent 的网络搜索开关不可用。 @@ -436,11 +437,31 @@ export default function SystemSettingsPage() { DuckDuckGo(ddgs · 免费无 Key) Brave(免费版 · 需 Key) Tavily(需 Key) + DeepSeek(官方) )} + {webSearch && backend === "deepseek" && ( +
+

DeepSeek 官方联网搜索

+

+ 该来源直接复用当前激活的 LLM 配置。因此它 + 仅支持 DeepSeek 官方模型,且该配置必须使用 anthropic 协议 + ——DeepSeek 的 OpenAI 协议端点不支持服务端搜索。切换 LLM 配置后此来源可能失效。 +

+

+ 与其它来源不同,搜索由 DeepSeek 服务端执行:每次搜索会额外消耗一次模型调用(产生 Token + 费用),搜索请求不经过上面的出口代理,也不计入流量留痕;返回结果只有标题和链接 + (无摘要),需要正文时由 WebFetch 抓取。 +

+

+ 是否满足上述条件由你自行确认,系统不做拦截;可用下方「测试搜索」按钮实际跑一次来验证。 +

+
+ )} + {webSearch && backend === "brave-free" && (