mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-11-11 05:03:50 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7140d14a17 |
No files matched your search
+12
-15
@@ -134,7 +134,7 @@ func (c *ChatAgent) Chat(ctx context.Context, agentKey, sessionID, message strin
|
|||||||
Provider: c.prov,
|
Provider: c.prov,
|
||||||
SystemPrompt: system,
|
SystemPrompt: system,
|
||||||
DynamicBoundary: boundary,
|
DynamicBoundary: boundary,
|
||||||
Tools: tools,
|
Tools: append(tools, deepSeekTools(ws)...),
|
||||||
DeferredTools: def.Deferred,
|
DeferredTools: def.Deferred,
|
||||||
UnlockSet: def.Unlock,
|
UnlockSet: def.Unlock,
|
||||||
PermissionMode: permission.ModeBypass,
|
PermissionMode: permission.ModeBypass,
|
||||||
@@ -143,20 +143,17 @@ func (c *ChatAgent) Chat(ctx context.Context, agentKey, sessionID, message strin
|
|||||||
WebFetchCACert: c.proxyCACert,
|
WebFetchCACert: c.proxyCACert,
|
||||||
// 联网搜索(可选)。ddgs 无需 key;brave-free 需 BraveKey;tavily 需 TavilyKey。
|
// 联网搜索(可选)。ddgs 无需 key;brave-free 需 BraveKey;tavily 需 TavilyKey。
|
||||||
// WebSearchProxy 是独立出口代理(http/https/socks5),与记录流量的 MITM 代理无关;空则直连。
|
// WebSearchProxy 是独立出口代理(http/https/socks5),与记录流量的 MITM 代理无关;空则直连。
|
||||||
EnableWebSearch: ws.Enabled,
|
EnableWebSearch: ws.Enabled && ws.Backend != "deepseek",
|
||||||
WebSearchBackend: ws.Backend,
|
WebSearchBackend: ws.Backend,
|
||||||
BraveSearchAPIKey: ws.BraveKey,
|
BraveSearchAPIKey: ws.BraveKey,
|
||||||
TavilySearchAPIKey: ws.TavilyKey,
|
TavilySearchAPIKey: ws.TavilyKey,
|
||||||
DeepSeekSearchBaseURL: ws.DeepSeekBaseURL,
|
WebSearchProxy: ws.Proxy,
|
||||||
DeepSeekSearchAPIKey: ws.DeepSeekAPIKey,
|
BashEnv: proxyEnv(proxyAddr, c.proxyCACert), // Bash 子命令默认走代理+信任 CA
|
||||||
DeepSeekSearchModel: ws.DeepSeekModel,
|
WorkingDir: sessionWorkDir,
|
||||||
WebSearchProxy: ws.Proxy,
|
MaxTurns: maxTurns,
|
||||||
BashEnv: proxyEnv(proxyAddr, c.proxyCACert), // Bash 子命令默认走代理+信任 CA
|
MaxDuration: maxDuration,
|
||||||
WorkingDir: sessionWorkDir,
|
Compaction: compactionConfig(c.window),
|
||||||
MaxTurns: maxTurns,
|
Todos: actool.NewTodoStore(),
|
||||||
MaxDuration: maxDuration,
|
|
||||||
Compaction: compactionConfig(c.window),
|
|
||||||
Todos: actool.NewTodoStore(),
|
|
||||||
// large tool output spills to cmd-output/ under the session dir.
|
// large tool output spills to cmd-output/ under the session dir.
|
||||||
// 截断上限用 SDK 默认(tool.Capture 的 30000 字符)。
|
// 截断上限用 SDK 默认(tool.Capture 的 30000 字符)。
|
||||||
ToolOutputDir: filepath.Join(sessionWorkDir, "cmd-output"),
|
ToolOutputDir: filepath.Join(sessionWorkDir, "cmd-output"),
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Autumn-27/norma/permission"
|
||||||
|
actool "github.com/Autumn-27/norma/tool"
|
||||||
|
)
|
||||||
|
|
||||||
|
const deepSeekToolsURL = "https://api.deepseek.com/v1/tools/"
|
||||||
|
|
||||||
|
func deepSeekTools(opts WebSearchOpts) []actool.CoreTool {
|
||||||
|
if !opts.Enabled || opts.Backend != "deepseek" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(opts.DeepSeekAPIKey)
|
||||||
|
if key == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
if proxy := strings.TrimSpace(opts.Proxy); proxy != "" {
|
||||||
|
if parsed, err := url.Parse(proxy); err == nil && parsed.Scheme != "" && parsed.Host != "" {
|
||||||
|
client.Transport = &http.Transport{Proxy: http.ProxyURL(parsed)}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []actool.CoreTool{
|
||||||
|
deepSeekTool("deepseek_search", "Search the web with DeepSeek and return ranked titles, URLs, and snippets.", "search", key, client, map[string]any{
|
||||||
|
"query": map[string]any{"type": "string", "description": "Search query"},
|
||||||
|
"count": map[string]any{"type": "integer", "description": "Maximum results (1-10)"},
|
||||||
|
}, "query"),
|
||||||
|
deepSeekTool("deepseek_fetch", "Fetch a web page with DeepSeek and return its extracted content.", "fetch", key, client, map[string]any{
|
||||||
|
"url": map[string]any{"type": "string", "description": "Page URL to fetch"},
|
||||||
|
}, "url"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeepSeekSearchProbe(ctx context.Context, key, proxy, query string, count int) (int, error) {
|
||||||
|
if strings.TrimSpace(key) == "" {
|
||||||
|
return 0, fmt.Errorf("DeepSeek Search API Key is required")
|
||||||
|
}
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
if strings.TrimSpace(proxy) != "" {
|
||||||
|
parsed, err := url.Parse(proxy)
|
||||||
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||||
|
return 0, fmt.Errorf("invalid search proxy URL")
|
||||||
|
}
|
||||||
|
client.Transport = &http.Transport{Proxy: http.ProxyURL(parsed)}
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(map[string]any{"query": query, "count": count})
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, deepSeekToolsURL+"search", strings.NewReader(string(body)))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(key))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var result map[string]json.RawMessage
|
||||||
|
if err := json.NewDecoder(io.LimitReader(resp.Body, 2<<20)).Decode(&result); err != nil {
|
||||||
|
return 0, fmt.Errorf("DeepSeek returned invalid JSON: %w", err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return 0, fmt.Errorf("DeepSeek search returned HTTP %d: %s", resp.StatusCode, string(result["error"]))
|
||||||
|
}
|
||||||
|
for _, field := range []string{"results", "data", "items"} {
|
||||||
|
var items []json.RawMessage
|
||||||
|
if json.Unmarshal(result[field], &items) == nil && len(items) > 0 {
|
||||||
|
return len(items), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("DeepSeek search response did not contain results")
|
||||||
|
}
|
||||||
|
|
||||||
|
func deepSeekTool(name, description, endpoint, key string, client *http.Client, properties map[string]any, required string) actool.CoreTool {
|
||||||
|
return actool.Build(actool.Spec{
|
||||||
|
Name: name, Description: description,
|
||||||
|
Schema: map[string]any{"type": "object", "properties": properties, "required": []any{required}},
|
||||||
|
ReadOnly: func(json.RawMessage) bool { return true }, Concurrent: func(json.RawMessage) bool { return true },
|
||||||
|
Permissions: func(context.Context, json.RawMessage, permission.Context) permission.Decision {
|
||||||
|
return permission.Allowed()
|
||||||
|
},
|
||||||
|
Run: func(ctx context.Context, input json.RawMessage, _ *actool.ToolContext) (actool.Result, error) {
|
||||||
|
var args map[string]any
|
||||||
|
if err := json.Unmarshal(input, &args); err != nil {
|
||||||
|
return actool.Errorf("invalid input"), nil
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(args)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, deepSeekToolsURL+endpoint, strings.NewReader(string(body)))
|
||||||
|
if err != nil {
|
||||||
|
return actool.Errorf(err.Error()), nil
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+key)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return actool.Errorf("DeepSeek " + endpoint + " failed: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var result json.RawMessage
|
||||||
|
if err := json.NewDecoder(io.LimitReader(resp.Body, 2<<20)).Decode(&result); err != nil {
|
||||||
|
return actool.Errorf("DeepSeek returned invalid JSON"), nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return actool.Errorf(fmt.Sprintf("DeepSeek %s returned HTTP %d: %s", endpoint, resp.StatusCode, string(result))), nil
|
||||||
|
}
|
||||||
|
return actool.Text(string(result)), nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type deepSeekRoundTripper func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
func (f deepSeekRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
||||||
|
|
||||||
|
func TestDeepSeekSearchToolUsesConfiguredKeyAndQuery(t *testing.T) {
|
||||||
|
client := &http.Client{Transport: deepSeekRoundTripper(func(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.URL.String() != deepSeekToolsURL+"search" {
|
||||||
|
t.Errorf("request URL = %q", r.URL)
|
||||||
|
}
|
||||||
|
if got := r.Header.Get("Authorization"); got != "Bearer test-key" {
|
||||||
|
t.Errorf("authorization = %q", got)
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
if string(body) != `{"count":3,"query":"ARTEX"}` {
|
||||||
|
t.Errorf("request body = %s", body)
|
||||||
|
}
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"results":[{"title":"ARTEX","url":"https://example.test"}]}`)), Header: make(http.Header)}, nil
|
||||||
|
})}
|
||||||
|
tool := deepSeekTool("deepseek_search", "", "search", "test-key", client, map[string]any{
|
||||||
|
"query": map[string]any{"type": "string"}, "count": map[string]any{"type": "integer"},
|
||||||
|
}, "query")
|
||||||
|
result, err := tool.Call(context.Background(), []byte(`{"query":"ARTEX","count":3}`), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.IsError || !strings.Contains(result.Flatten(), `"results"`) {
|
||||||
|
t.Fatalf("unexpected result: %+v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeepSeekToolsRespectGlobalAndBackendSettings(t *testing.T) {
|
||||||
|
for _, opts := range []WebSearchOpts{
|
||||||
|
{Enabled: false, Backend: "deepseek", DeepSeekAPIKey: "key"},
|
||||||
|
{Enabled: true, Backend: "ddgs", DeepSeekAPIKey: "key"},
|
||||||
|
{Enabled: true, Backend: "deepseek"},
|
||||||
|
} {
|
||||||
|
if got := deepSeekTools(opts); len(got) != 0 {
|
||||||
|
t.Fatalf("deepseek tools enabled for opts %+v", opts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-15
@@ -142,7 +142,7 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, mainSeg int, as *db.
|
|||||||
Provider: m.prov,
|
Provider: m.prov,
|
||||||
SystemPrompt: system,
|
SystemPrompt: system,
|
||||||
DynamicBoundary: boundary,
|
DynamicBoundary: boundary,
|
||||||
Tools: tools,
|
Tools: append(tools, deepSeekTools(m.webSearch)...),
|
||||||
DeferredTools: def.Deferred,
|
DeferredTools: def.Deferred,
|
||||||
UnlockSet: def.Unlock,
|
UnlockSet: def.Unlock,
|
||||||
PermissionMode: permission.ModeBypass,
|
PermissionMode: permission.ModeBypass,
|
||||||
@@ -151,20 +151,17 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, mainSeg int, as *db.
|
|||||||
WebFetchCACert: m.proxyCACert,
|
WebFetchCACert: m.proxyCACert,
|
||||||
// 联网搜索(可选)。ddgs 无需 key;brave-free 需 BraveKey;tavily 需 TavilyKey。
|
// 联网搜索(可选)。ddgs 无需 key;brave-free 需 BraveKey;tavily 需 TavilyKey。
|
||||||
// WebSearchProxy 是独立出口代理(http/https/socks5),与记录流量的 MITM 代理无关;空则直连。
|
// WebSearchProxy 是独立出口代理(http/https/socks5),与记录流量的 MITM 代理无关;空则直连。
|
||||||
EnableWebSearch: m.webSearch.Enabled,
|
EnableWebSearch: m.webSearch.Enabled && m.webSearch.Backend != "deepseek",
|
||||||
WebSearchBackend: m.webSearch.Backend,
|
WebSearchBackend: m.webSearch.Backend,
|
||||||
BraveSearchAPIKey: m.webSearch.BraveKey,
|
BraveSearchAPIKey: m.webSearch.BraveKey,
|
||||||
TavilySearchAPIKey: m.webSearch.TavilyKey,
|
TavilySearchAPIKey: m.webSearch.TavilyKey,
|
||||||
DeepSeekSearchBaseURL: m.webSearch.DeepSeekBaseURL,
|
WebSearchProxy: m.webSearch.Proxy,
|
||||||
DeepSeekSearchAPIKey: m.webSearch.DeepSeekAPIKey,
|
BashEnv: proxyEnv(proxyAddr, m.proxyCACert), // Bash 子命令默认走代理+信任 CA
|
||||||
DeepSeekSearchModel: m.webSearch.DeepSeekModel,
|
WorkingDir: mainDir, // 本任务工作目录 <workDir>/tasks/<taskID>
|
||||||
WebSearchProxy: m.webSearch.Proxy,
|
ToolOutputDir: cmdOutDir(mainDir),
|
||||||
BashEnv: proxyEnv(proxyAddr, m.proxyCACert), // Bash 子命令默认走代理+信任 CA
|
MaxTurns: m.maxTurns, // 0 = unlimited (configurable in agent management)
|
||||||
WorkingDir: mainDir, // 本任务工作目录 <workDir>/tasks/<taskID>
|
Compaction: compactionConfig(m.compactionWindow()), // long chats stay within the window
|
||||||
ToolOutputDir: cmdOutDir(mainDir),
|
Todos: actool.NewTodoStore(), // 会话级临时待办(TodoWrite),纯规划用,退出即丢
|
||||||
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 轮)。
|
// 命中预算(步数)→ SDK 跑收尾:向用户输出一句进展总结。Prompt 与收尾轮数可后台编辑(默认 10 轮)。
|
||||||
Settlement: wrapupSettlement("mainagent", nil),
|
Settlement: wrapupSettlement("mainagent", nil),
|
||||||
NonStreaming: m.nonStreaming(), // 该 profile 选非流式时走 Provider.Complete
|
NonStreaming: m.nonStreaming(), // 该 profile 选非流式时走 Provider.Complete
|
||||||
|
|||||||
+12
-15
@@ -412,7 +412,7 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts
|
|||||||
Provider: p.prov,
|
Provider: p.prov,
|
||||||
SystemPrompt: system,
|
SystemPrompt: system,
|
||||||
DynamicBoundary: boundary,
|
DynamicBoundary: boundary,
|
||||||
Tools: tools,
|
Tools: append(tools, deepSeekTools(p.webSearch)...),
|
||||||
DeferredTools: def.Deferred,
|
DeferredTools: def.Deferred,
|
||||||
UnlockSet: def.Unlock,
|
UnlockSet: def.Unlock,
|
||||||
PermissionMode: permission.ModeBypass,
|
PermissionMode: permission.ModeBypass,
|
||||||
@@ -421,20 +421,17 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts
|
|||||||
WebFetchCACert: p.proxyCACert,
|
WebFetchCACert: p.proxyCACert,
|
||||||
// 联网搜索(可选)。ddgs 无需 key;brave-free 需 BraveKey;tavily 需 TavilyKey。
|
// 联网搜索(可选)。ddgs 无需 key;brave-free 需 BraveKey;tavily 需 TavilyKey。
|
||||||
// WebSearchProxy 是独立出口代理(http/https/socks5),与记录流量的 MITM 代理无关;空则直连。
|
// WebSearchProxy 是独立出口代理(http/https/socks5),与记录流量的 MITM 代理无关;空则直连。
|
||||||
EnableWebSearch: p.webSearch.Enabled,
|
EnableWebSearch: p.webSearch.Enabled && p.webSearch.Backend != "deepseek",
|
||||||
WebSearchBackend: p.webSearch.Backend,
|
WebSearchBackend: p.webSearch.Backend,
|
||||||
BraveSearchAPIKey: p.webSearch.BraveKey,
|
BraveSearchAPIKey: p.webSearch.BraveKey,
|
||||||
TavilySearchAPIKey: p.webSearch.TavilyKey,
|
TavilySearchAPIKey: p.webSearch.TavilyKey,
|
||||||
DeepSeekSearchBaseURL: p.webSearch.DeepSeekBaseURL,
|
WebSearchProxy: p.webSearch.Proxy,
|
||||||
DeepSeekSearchAPIKey: p.webSearch.DeepSeekAPIKey,
|
BashEnv: proxyEnv(proxyAddr, p.proxyCACert), // Bash 子命令默认走代理+信任 CA
|
||||||
DeepSeekSearchModel: p.webSearch.DeepSeekModel,
|
WorkingDir: taskDir, // 本任务工作目录 <workDir>/tasks/<taskID>
|
||||||
WebSearchProxy: p.webSearch.Proxy,
|
ToolOutputDir: cmdOutDir(taskDir),
|
||||||
BashEnv: proxyEnv(proxyAddr, p.proxyCACert), // Bash 子命令默认走代理+信任 CA
|
MaxTurns: p.maxTurns, // 0 = unlimited (configurable in agent management)
|
||||||
WorkingDir: taskDir, // 本任务工作目录 <workDir>/tasks/<taskID>
|
MaxDuration: maxDur, // 0=不限;有 deadline 时=距 deadline 剩余
|
||||||
ToolOutputDir: cmdOutDir(taskDir),
|
Compaction: compactionConfig(p.compactionWindow()),
|
||||||
MaxTurns: p.maxTurns, // 0 = unlimited (configurable in agent management)
|
|
||||||
MaxDuration: maxDur, // 0=不限;有 deadline 时=距 deadline 剩余
|
|
||||||
Compaction: compactionConfig(p.compactionWindow()),
|
|
||||||
// 跨唤醒共享的规划待办:让串行链在多轮之间保留(session 是新的,store 不是)。
|
// 跨唤醒共享的规划待办:让串行链在多轮之间保留(session 是新的,store 不是)。
|
||||||
Todos: p.todoFor(ts.ID()),
|
Todos: p.todoFor(ts.ID()),
|
||||||
// 命中【本轮】步数预算→ SDK 跑收尾:把本轮已想清楚的结论落地(该派的 add_intent、
|
// 命中【本轮】步数预算→ SDK 跑收尾:把本轮已想清楚的结论落地(该派的 add_intent、
|
||||||
|
|||||||
+13
-26
@@ -28,27 +28,17 @@ import (
|
|||||||
// WebSearchOpts is the web-search backend selection the server pushes into each
|
// WebSearchOpts is the web-search backend selection the server pushes into each
|
||||||
// agent (planner/worker/main). Enabled=false leaves the web_search tool off.
|
// agent (planner/worker/main). Enabled=false leaves the web_search tool off.
|
||||||
// Backend is "ddgs" (no key), "brave-free" (BraveKey required), "tavily"
|
// Backend is "ddgs" (no key), "brave-free" (BraveKey required), "tavily"
|
||||||
// (TavilyKey required), or "deepseek" (DeepSeek* required, filled from the
|
// (TavilyKey required), or "deepseek" (uses DeepSeek's independent Search API).
|
||||||
// active LLM profile). It maps directly onto agentcore.Options.
|
|
||||||
// Proxy is a dedicated egress proxy for the search request (http/https/socks5),
|
// 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
|
// independent of the traffic-recording MITM proxy — set it when the search endpoint
|
||||||
// is only reachable via a VPN/SOCKS proxy. Empty = direct.
|
// is only reachable via a VPN/SOCKS proxy. Empty = direct.
|
||||||
//
|
|
||||||
// 注意 deepseek 后端与其它三个的性质不同:DeepSeek 没有可直接调用的搜索接口,
|
|
||||||
// 搜索只存在于其 Anthropic 兼容 messages 接口内部(web_search_20250305 server
|
|
||||||
// tool),因此每次搜索会消耗一次模型调用,且搜索请求由 DeepSeek 服务端发出——
|
|
||||||
// 不经过本机 Proxy,也不会进流量留痕。
|
|
||||||
type WebSearchOpts struct {
|
type WebSearchOpts struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Backend string
|
Backend string
|
||||||
BraveKey string
|
BraveKey string
|
||||||
TavilyKey string
|
TavilyKey string
|
||||||
Proxy string
|
Proxy string
|
||||||
// DeepSeek* 来自当前激活的 LLM 配置(仅 anthropic 格式的 DeepSeek 官方端点),
|
DeepSeekAPIKey string
|
||||||
// 不单独配置,随 LLM 配置切换而变。
|
|
||||||
DeepSeekBaseURL string
|
|
||||||
DeepSeekAPIKey string
|
|
||||||
DeepSeekModel string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Worker struct {
|
type Worker struct {
|
||||||
@@ -437,7 +427,7 @@ func (w *Worker) execute(ctx context.Context, name string, taskID int64, as *db.
|
|||||||
Provider: w.prov,
|
Provider: w.prov,
|
||||||
SystemPrompt: system,
|
SystemPrompt: system,
|
||||||
DynamicBoundary: boundary,
|
DynamicBoundary: boundary,
|
||||||
Tools: tools,
|
Tools: append(tools, deepSeekTools(w.webSearch)...),
|
||||||
DeferredTools: def.Deferred,
|
DeferredTools: def.Deferred,
|
||||||
UnlockSet: def.Unlock,
|
UnlockSet: def.Unlock,
|
||||||
PermissionMode: permission.ModeBypass,
|
PermissionMode: permission.ModeBypass,
|
||||||
@@ -448,14 +438,11 @@ func (w *Worker) execute(ctx context.Context, name string, taskID int64, as *db.
|
|||||||
WebFetchCACert: w.proxyCACert,
|
WebFetchCACert: w.proxyCACert,
|
||||||
// 联网搜索(可选)。ddgs 无需 key;brave-free 需 BraveKey;tavily 需 TavilyKey。
|
// 联网搜索(可选)。ddgs 无需 key;brave-free 需 BraveKey;tavily 需 TavilyKey。
|
||||||
// WebSearchProxy 是独立的出口代理(http/https/socks5),与记录流量的 MITM 代理无关;空则直连。
|
// WebSearchProxy 是独立的出口代理(http/https/socks5),与记录流量的 MITM 代理无关;空则直连。
|
||||||
EnableWebSearch: w.webSearch.Enabled,
|
EnableWebSearch: w.webSearch.Enabled && w.webSearch.Backend != "deepseek",
|
||||||
WebSearchBackend: w.webSearch.Backend,
|
WebSearchBackend: w.webSearch.Backend,
|
||||||
BraveSearchAPIKey: w.webSearch.BraveKey,
|
BraveSearchAPIKey: w.webSearch.BraveKey,
|
||||||
TavilySearchAPIKey: w.webSearch.TavilyKey,
|
TavilySearchAPIKey: w.webSearch.TavilyKey,
|
||||||
DeepSeekSearchBaseURL: w.webSearch.DeepSeekBaseURL,
|
WebSearchProxy: w.webSearch.Proxy,
|
||||||
DeepSeekSearchAPIKey: w.webSearch.DeepSeekAPIKey,
|
|
||||||
DeepSeekSearchModel: w.webSearch.DeepSeekModel,
|
|
||||||
WebSearchProxy: w.webSearch.Proxy,
|
|
||||||
// Bash 子命令的 HTTP 默认走记录代理 + 信任其 CA(工具无需 -x/-k)。
|
// Bash 子命令的 HTTP 默认走记录代理 + 信任其 CA(工具无需 -x/-k)。
|
||||||
BashEnv: proxyEnv(proxyAddr, w.proxyCACert),
|
BashEnv: proxyEnv(proxyAddr, w.proxyCACert),
|
||||||
WorkingDir: runDir,
|
WorkingDir: runDir,
|
||||||
|
|||||||
+13
-16
@@ -304,6 +304,7 @@ const (
|
|||||||
settingWebSearchBackend = "web_search_backend"
|
settingWebSearchBackend = "web_search_backend"
|
||||||
settingBraveKey = "brave_search_api_key"
|
settingBraveKey = "brave_search_api_key"
|
||||||
settingTavilyKey = "tavily_search_api_key"
|
settingTavilyKey = "tavily_search_api_key"
|
||||||
|
settingDeepSeekSearchKey = "deepseek_search_api_key"
|
||||||
settingWebSearchProxy = "web_search_proxy"
|
settingWebSearchProxy = "web_search_proxy"
|
||||||
// settingGlobalProxy is the global egress proxy for all target traffic
|
// settingGlobalProxy is the global egress proxy for all target traffic
|
||||||
// (http/https/socks5). Empty = direct. Distinct from web_search_proxy (which
|
// (http/https/socks5). Empty = direct. Distinct from web_search_proxy (which
|
||||||
@@ -328,8 +329,7 @@ const (
|
|||||||
settingNoaCompaction = "noa_compaction"
|
settingNoaCompaction = "noa_compaction"
|
||||||
// defaultWebSearchBackend is used when web search is on but no backend was picked.
|
// defaultWebSearchBackend is used when web search is on but no backend was picked.
|
||||||
defaultWebSearchBackend = "ddgs"
|
defaultWebSearchBackend = "ddgs"
|
||||||
// deepSeekWebSearchBackend borrows the active LLM profile instead of its own
|
// deepSeekWebSearchBackend calls DeepSeek's independent web-search API.
|
||||||
// key, so it only works on an anthropic-format profile pointed at DeepSeek.
|
|
||||||
deepSeekWebSearchBackend = "deepseek"
|
deepSeekWebSearchBackend = "deepseek"
|
||||||
// defaultWorkers is the concurrent work-agent count when the setting is unset.
|
// defaultWorkers is the concurrent work-agent count when the setting is unset.
|
||||||
defaultWorkers = 3
|
defaultWorkers = 3
|
||||||
@@ -783,26 +783,23 @@ func (m *Manager) WebSearchOpts() agent.WebSearchOpts {
|
|||||||
if on && backend == "tavily" && strings.TrimSpace(tavilyKey) == "" {
|
if on && backend == "tavily" && strings.TrimSpace(tavilyKey) == "" {
|
||||||
on = false
|
on = false
|
||||||
}
|
}
|
||||||
|
if on && backend == deepSeekWebSearchBackend && m.deepSeekSearchKey() == "" {
|
||||||
|
on = false
|
||||||
|
}
|
||||||
o := 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 {
|
if backend == deepSeekWebSearchBackend {
|
||||||
o.DeepSeekBaseURL, o.DeepSeekAPIKey, o.DeepSeekModel = m.deepSeekSearchCreds()
|
o.DeepSeekAPIKey = m.deepSeekSearchKey()
|
||||||
}
|
}
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
// deepSeekSearchCreds resolves the credentials the "deepseek" search backend
|
func (m *Manager) deepSeekSearchKey() string {
|
||||||
// borrows from the active LLM profile (it has no key of its own). Whether that
|
key, _, _ := m.pg.GetSetting(settingDeepSeekSearchKey)
|
||||||
// profile can actually drive server-side search — DeepSeek exposes it only on
|
return strings.TrimSpace(key)
|
||||||
// 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
|
func (m *Manager) setDeepSeekSearchKey(key string) error {
|
||||||
// the same feedback every other backend gives for a bad key.
|
return m.pg.SetSetting(settingDeepSeekSearchKey, strings.TrimSpace(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
|
// SetWebSearch persists and applies the web-search settings. braveKey, tavilyKey, and
|
||||||
|
|||||||
+31
-12
@@ -3668,6 +3668,7 @@ func (s *Server) settingsPayload() map[string]any {
|
|||||||
"web_search_backend": backend,
|
"web_search_backend": backend,
|
||||||
"brave_key_set": strings.TrimSpace(braveKey) != "",
|
"brave_key_set": strings.TrimSpace(braveKey) != "",
|
||||||
"tavily_key_set": strings.TrimSpace(tavilyKey) != "",
|
"tavily_key_set": strings.TrimSpace(tavilyKey) != "",
|
||||||
|
"deepseek_search_key_set": s.m.deepSeekSearchKey() != "",
|
||||||
"web_search_proxy": maskSecrets(proxy), // 独立出口代理(http/https/socks5),空=直连
|
"web_search_proxy": maskSecrets(proxy), // 独立出口代理(http/https/socks5),空=直连
|
||||||
"global_proxy": maskSecrets(s.m.GlobalProxy()), // 全局出口代理(http/https/socks5),所有目标流量走它,空=直连
|
"global_proxy": maskSecrets(s.m.GlobalProxy()), // 全局出口代理(http/https/socks5),所有目标流量走它,空=直连
|
||||||
"proxy_pool": func() []ProxyPoolEntry { e, _, _ := s.m.ProxyPoolSnapshot(); return e }(),
|
"proxy_pool": func() []ProxyPoolEntry { e, _, _ := s.m.ProxyPoolSnapshot(); return e }(),
|
||||||
@@ -3768,6 +3769,7 @@ func (s *Server) putSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
WebSearchBackend *string `json:"web_search_backend"`
|
WebSearchBackend *string `json:"web_search_backend"`
|
||||||
BraveKey *string `json:"brave_search_api_key"`
|
BraveKey *string `json:"brave_search_api_key"`
|
||||||
TavilyKey *string `json:"tavily_search_api_key"`
|
TavilyKey *string `json:"tavily_search_api_key"`
|
||||||
|
DeepSeekSearchKey *string `json:"deepseek_search_api_key"`
|
||||||
WebSearchProxy *string `json:"web_search_proxy"` // 独立出口代理(http/https/socks5);null=不改,""=清空
|
WebSearchProxy *string `json:"web_search_proxy"` // 独立出口代理(http/https/socks5);null=不改,""=清空
|
||||||
GlobalProxy *string `json:"global_proxy"` // 全局出口代理(http/https/socks5);null=不改,""=清空(直连)
|
GlobalProxy *string `json:"global_proxy"` // 全局出口代理(http/https/socks5);null=不改,""=清空(直连)
|
||||||
ProxyRotateTimed *bool `json:"proxy_rotate_timed_enabled"`
|
ProxyRotateTimed *bool `json:"proxy_rotate_timed_enabled"`
|
||||||
@@ -4055,7 +4057,7 @@ func (s *Server) putSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if req.WebSearchEnabled != nil || req.WebSearchBackend != nil || req.BraveKey != nil || req.TavilyKey != nil || req.WebSearchProxy != nil {
|
if req.WebSearchEnabled != nil || req.WebSearchBackend != nil || req.BraveKey != nil || req.TavilyKey != nil || req.DeepSeekSearchKey != nil || req.WebSearchProxy != nil {
|
||||||
// Fill unspecified fields from current state so a partial PUT doesn't reset them.
|
// Fill unspecified fields from current state so a partial PUT doesn't reset them.
|
||||||
on, backend, _, _, _ := s.m.WebSearch()
|
on, backend, _, _, _ := s.m.WebSearch()
|
||||||
if req.WebSearchEnabled != nil {
|
if req.WebSearchEnabled != nil {
|
||||||
@@ -4074,6 +4076,16 @@ func (s *Server) putSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
if req.DeepSeekSearchKey != nil {
|
||||||
|
if strings.Contains(*req.DeepSeekSearchKey, "***") {
|
||||||
|
*req.DeepSeekSearchKey = s.m.deepSeekSearchKey()
|
||||||
|
}
|
||||||
|
if err := s.m.setDeepSeekSearchKey(*req.DeepSeekSearchKey); err != nil {
|
||||||
|
writeErr(w, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
if changed {
|
if changed {
|
||||||
// rebuild agents so the new proxy/tools/prompt/web-search take hold (only if LLM configured).
|
// rebuild agents so the new proxy/tools/prompt/web-search take hold (only if LLM configured).
|
||||||
s.cfgMu.Lock()
|
s.cfgMu.Lock()
|
||||||
@@ -4129,10 +4141,11 @@ func (s *Server) setReporterBrowserScreenshotVisibility(enabled bool) error {
|
|||||||
// not retype them. Always 200 with {ok, error?, count?, backend?}.
|
// not retype them. Always 200 with {ok, error?, count?, backend?}.
|
||||||
func (s *Server) testWebSearch(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) testWebSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Backend string `json:"web_search_backend"`
|
Backend string `json:"web_search_backend"`
|
||||||
Proxy string `json:"web_search_proxy"`
|
Proxy string `json:"web_search_proxy"`
|
||||||
BraveKey string `json:"brave_search_api_key"`
|
BraveKey string `json:"brave_search_api_key"`
|
||||||
TavilyKey string `json:"tavily_search_api_key"`
|
TavilyKey string `json:"tavily_search_api_key"`
|
||||||
|
DeepSeekSearchKey string `json:"deepseek_search_api_key"`
|
||||||
}
|
}
|
||||||
// Empty body is fine — fall back entirely to the saved config below.
|
// Empty body is fine — fall back entirely to the saved config below.
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && err != io.EOF {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && err != io.EOF {
|
||||||
@@ -4155,18 +4168,24 @@ func (s *Server) testWebSearch(w http.ResponseWriter, r *http.Request) {
|
|||||||
if strings.TrimSpace(req.TavilyKey) != "" {
|
if strings.TrimSpace(req.TavilyKey) != "" {
|
||||||
tavilyKey = req.TavilyKey
|
tavilyKey = req.TavilyKey
|
||||||
}
|
}
|
||||||
|
deepSeekKey := s.m.deepSeekSearchKey()
|
||||||
|
if strings.TrimSpace(req.DeepSeekSearchKey) != "" {
|
||||||
|
deepSeekKey = strings.TrimSpace(req.DeepSeekSearchKey)
|
||||||
|
}
|
||||||
cfg := actool.WebSearchConfig{Backend: backend, BraveAPIKey: braveKey, TavilyAPIKey: tavilyKey, Proxy: proxy}
|
cfg := actool.WebSearchConfig{Backend: backend, BraveAPIKey: braveKey, TavilyAPIKey: tavilyKey, Proxy: proxy}
|
||||||
// Hard cap so a slow/blocked proxy can't hang the request.
|
// Hard cap so a slow/blocked proxy can't hang the request.
|
||||||
wall := 30 * time.Second
|
wall := 30 * time.Second
|
||||||
// deepseek 的凭据不在表单里,来自当前激活的 LLM 配置;同时它每次搜索都跑一次
|
|
||||||
// 模型推理,30s 的通用上限偏紧,单独放宽。这里不预判配置能不能用——测这一下
|
|
||||||
// 本来就是给用户自己确认的手段,真跑不通时下面的报错比预判更有信息量。
|
|
||||||
probeQuery := "test"
|
probeQuery := "test"
|
||||||
if strings.TrimSpace(backend) == deepSeekWebSearchBackend {
|
if strings.TrimSpace(backend) == deepSeekWebSearchBackend {
|
||||||
cfg.DeepSeekBaseURL, cfg.DeepSeekAPIKey, cfg.DeepSeekModel = s.m.deepSeekSearchCreds()
|
ctx, cancel := context.WithTimeout(r.Context(), wall)
|
||||||
wall = 120 * time.Second
|
defer cancel()
|
||||||
// 搜索词由 DeepSeek 端的模型自行决定,"test" 太空泛会让它跳过搜索直接作答。
|
count, err := agent.DeepSeekSearchProbe(ctx, deepSeekKey, proxy, "DeepSeek company official website", 3)
|
||||||
probeQuery = "DeepSeek company official website"
|
if err != nil {
|
||||||
|
writeJSON(w, 200, map[string]any{"ok": false, "error": err.Error(), "backend": backend})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, 200, map[string]any{"ok": count > 0, "count": count, "backend": backend})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), wall)
|
ctx, cancel := context.WithTimeout(r.Context(), wall)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ export default function SystemSettingsPage() {
|
|||||||
const [braveKeyInput, setBraveKeyInput] = React.useState("");
|
const [braveKeyInput, setBraveKeyInput] = React.useState("");
|
||||||
const [tavilyKeySet, setTavilyKeySet] = React.useState(false);
|
const [tavilyKeySet, setTavilyKeySet] = React.useState(false);
|
||||||
const [tavilyKeyInput, setTavilyKeyInput] = React.useState("");
|
const [tavilyKeyInput, setTavilyKeyInput] = React.useState("");
|
||||||
|
const [deepSeekKeySet, setDeepSeekKeySet] = React.useState(false);
|
||||||
|
const [deepSeekKeyInput, setDeepSeekKeyInput] = React.useState("");
|
||||||
|
const [savingDeepSeekKey, setSavingDeepSeekKey] = React.useState(false);
|
||||||
const [savingTavilyKey, setSavingTavilyKey] = React.useState(false);
|
const [savingTavilyKey, setSavingTavilyKey] = React.useState(false);
|
||||||
const [proxyInput, setProxyInput] = React.useState("");
|
const [proxyInput, setProxyInput] = React.useState("");
|
||||||
const [savingProxy, setSavingProxy] = React.useState(false);
|
const [savingProxy, setSavingProxy] = React.useState(false);
|
||||||
@@ -124,6 +127,7 @@ export default function SystemSettingsPage() {
|
|||||||
setBackend(s.web_search_backend || "ddgs");
|
setBackend(s.web_search_backend || "ddgs");
|
||||||
setBraveKeySet(!!s.brave_key_set);
|
setBraveKeySet(!!s.brave_key_set);
|
||||||
setTavilyKeySet(!!s.tavily_key_set);
|
setTavilyKeySet(!!s.tavily_key_set);
|
||||||
|
setDeepSeekKeySet(!!s.deepseek_search_key_set);
|
||||||
setProxyInput(s.web_search_proxy ?? "");
|
setProxyInput(s.web_search_proxy ?? "");
|
||||||
const savedProxy = s.global_proxy ?? "";
|
const savedProxy = s.global_proxy ?? "";
|
||||||
setGlobalProxyInput(savedProxy);
|
setGlobalProxyInput(savedProxy);
|
||||||
@@ -437,6 +441,16 @@ export default function SystemSettingsPage() {
|
|||||||
.finally(() => setSavingTavilyKey(false));
|
.finally(() => setSavingTavilyKey(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveDeepSeekKey = () => {
|
||||||
|
setSavingDeepSeekKey(true);
|
||||||
|
api.setSettings({ deepseek_search_api_key: deepSeekKeyInput }).then((s) => {
|
||||||
|
apply(s);
|
||||||
|
setDeepSeekKeyInput("");
|
||||||
|
toast.success("已保存 DeepSeek Search API Key");
|
||||||
|
}).catch((e) => toast.error(`保存失败:${(e as Error).message}`))
|
||||||
|
.finally(() => setSavingDeepSeekKey(false));
|
||||||
|
};
|
||||||
|
|
||||||
const saveProxy = () => {
|
const saveProxy = () => {
|
||||||
setSavingProxy(true);
|
setSavingProxy(true);
|
||||||
api
|
api
|
||||||
@@ -608,6 +622,7 @@ export default function SystemSettingsPage() {
|
|||||||
web_search_proxy: proxyInput.trim(),
|
web_search_proxy: proxyInput.trim(),
|
||||||
brave_search_api_key: braveKeyInput,
|
brave_search_api_key: braveKeyInput,
|
||||||
tavily_search_api_key: tavilyKeyInput,
|
tavily_search_api_key: tavilyKeyInput,
|
||||||
|
deepseek_search_api_key: deepSeekKeyInput,
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
if (r.ok) toast.success(`搜索测试成功 · ${r.backend} 返回 ${r.count} 条结果`);
|
if (r.ok) toast.success(`搜索测试成功 · ${r.backend} 返回 ${r.count} 条结果`);
|
||||||
@@ -1441,7 +1456,7 @@ export default function SystemSettingsPage() {
|
|||||||
记录代理,独立于流量捕获。
|
记录代理,独立于流量捕获。
|
||||||
<br />
|
<br />
|
||||||
来源可选 <b>DuckDuckGo(ddgs)</b>(无需 Key)、<b>Brave(免费版)</b>(需填写 Brave API Key)、{" "}
|
来源可选 <b>DuckDuckGo(ddgs)</b>(无需 Key)、<b>Brave(免费版)</b>(需填写 Brave API Key)、{" "}
|
||||||
<b>Tavily</b>(需填写 Tavily API Key)或 <b>DeepSeek</b>(复用当前 LLM 配置)。总开关关闭时,各 Agent
|
<b>Tavily</b>(需填写 Tavily API Key)或 <b>DeepSeek</b>(需单独配置 Search API Key)。总开关关闭时,各 Agent
|
||||||
的网络搜索开关不可用。
|
的网络搜索开关不可用。
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -1487,21 +1502,31 @@ export default function SystemSettingsPage() {
|
|||||||
|
|
||||||
{webSearch && backend === "deepseek" && (
|
{webSearch && backend === "deepseek" && (
|
||||||
<div className="border-border/60 bg-muted/30 flex flex-col gap-2 rounded-md border p-3">
|
<div className="border-border/60 bg-muted/30 flex flex-col gap-2 rounded-md border p-3">
|
||||||
<p className="text-sm font-medium">DeepSeek 官方联网搜索</p>
|
<p className="text-sm font-medium">DeepSeek 联网搜索</p>
|
||||||
<p className="text-muted-foreground text-xs leading-relaxed">
|
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||||
该来源直接复用<b>当前激活的 LLM 配置</b>。因此它
|
通过 DeepSeek 独立 Search / Fetch API 提供搜索与网页提取,使用当前 Agent 选择的模型生成查询和整理结果。
|
||||||
<b>仅支持 DeepSeek 官方模型</b>,且该配置<b>必须使用 anthropic 协议</b>
|
|
||||||
——DeepSeek 的 OpenAI 协议端点不支持服务端搜索。切换 LLM 配置后此来源可能失效。
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground text-xs leading-relaxed">
|
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||||
与其它来源不同,搜索由 <b>DeepSeek 服务端执行</b>:每次搜索会额外消耗一次模型调用(产生 Token
|
搜索请求由 ARTEX 服务端发往 DeepSeek,可使用上方独立出口代理。API Key 只保存于服务端设置,不会回显。
|
||||||
费用),搜索请求<b>不经过上面的出口代理</b>,也<b>不计入流量留痕</b>;返回结果
|
|
||||||
<b>只有标题和链接</b>
|
|
||||||
(无摘要),需要正文时由 WebFetch 抓取。
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-xs leading-relaxed">
|
|
||||||
是否满足上述条件由你自行确认,系统不做拦截;可用下方「测试搜索」按钮实际跑一次来验证。
|
|
||||||
</p>
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder={deepSeekKeySet ? "已配置(留空则不变)" : "DeepSeek Search API Key"}
|
||||||
|
value={deepSeekKeyInput}
|
||||||
|
disabled={!loaded || savingDeepSeekKey}
|
||||||
|
onChange={(e) => setDeepSeekKeyInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={saveDeepSeekKey}
|
||||||
|
disabled={!loaded || savingDeepSeekKey || deepSeekKeyInput.trim() === ""}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{deepSeekKeySet && <span className="text-xs text-emerald-500">API Key 已配置</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -996,6 +996,7 @@ export const api = {
|
|||||||
web_search_proxy?: string;
|
web_search_proxy?: string;
|
||||||
brave_search_api_key?: string;
|
brave_search_api_key?: string;
|
||||||
tavily_search_api_key?: string;
|
tavily_search_api_key?: string;
|
||||||
|
deepseek_search_api_key?: string;
|
||||||
}) => post<{ ok: boolean; error?: string; count?: number; backend?: string }>(`/settings/web-search/test`, patch),
|
}) => post<{ ok: boolean; error?: string; count?: number; backend?: string }>(`/settings/web-search/test`, patch),
|
||||||
|
|
||||||
// global_proxy 传空 = 测直连;probe_url 可覆盖默认探测地址(隔离网用)。
|
// global_proxy 传空 = 测直连;probe_url 可覆盖默认探测地址(隔离网用)。
|
||||||
|
|||||||
@@ -3348,6 +3348,7 @@ export const settings: Settings = {
|
|||||||
web_search_backend: "ddgs",
|
web_search_backend: "ddgs",
|
||||||
brave_key_set: false,
|
brave_key_set: false,
|
||||||
tavily_key_set: true,
|
tavily_key_set: true,
|
||||||
|
deepseek_search_key_set: false,
|
||||||
web_search_proxy: "",
|
web_search_proxy: "",
|
||||||
global_proxy: "",
|
global_proxy: "",
|
||||||
proxy_pool: [],
|
proxy_pool: [],
|
||||||
|
|||||||
@@ -996,9 +996,11 @@ export interface Settings {
|
|||||||
web_search_backend: string; // "ddgs" | "brave-free" | "tavily" | "deepseek"
|
web_search_backend: string; // "ddgs" | "brave-free" | "tavily" | "deepseek"
|
||||||
brave_key_set: boolean;
|
brave_key_set: boolean;
|
||||||
tavily_key_set: boolean;
|
tavily_key_set: boolean;
|
||||||
|
deepseek_search_key_set: boolean;
|
||||||
// write-only: only sent on PUT to store/clear the key.
|
// write-only: only sent on PUT to store/clear the key.
|
||||||
brave_search_api_key?: string;
|
brave_search_api_key?: string;
|
||||||
tavily_search_api_key?: string;
|
tavily_search_api_key?: string;
|
||||||
|
deepseek_search_api_key?: string;
|
||||||
// 独立出口代理(http/https/socks5),用于访问搜索端点;与记录流量的 MITM 代理无关。空=直连。
|
// 独立出口代理(http/https/socks5),用于访问搜索端点;与记录流量的 MITM 代理无关。空=直连。
|
||||||
web_search_proxy?: string;
|
web_search_proxy?: string;
|
||||||
// 全局出口代理(http/https/socks5,可带 user:pass),所有目标流量走它。开启流量捕获时作为
|
// 全局出口代理(http/https/socks5,可带 user:pass),所有目标流量走它。开启流量捕获时作为
|
||||||
|
|||||||
Reference in new issue
Block a user