@konbakuyomu/smart-search-linux-x64 0.1.28 → 0.1.30-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/package.json +1 -1
  2. package/runtime/smart-search/_internal/base_library.zip +0 -0
  3. package/runtime/smart-search/_internal/certifi/cacert.pem +0 -23
  4. package/runtime/smart-search/_internal/smart_search/assets/i18n/messages.json +164 -0
  5. package/runtime/smart-search/_internal/smart_search/assets/skills/smart-search-cli/references/cli-core.md +1 -1
  6. package/runtime/smart-search/_internal/smart_search/assets/skills/smart-search-cli/references/command-patterns.md +2 -0
  7. package/runtime/smart-search/_internal/smart_search/assets/skills/smart-search-cli/references/provider-routing.md +11 -3
  8. package/runtime/smart-search/_internal/smart_search/assets/skills/smart-search-cli/references/setup-config.md +2 -0
  9. package/runtime/smart-search/_internal/smart_search/assets/ui/index.html +1 -1
  10. package/runtime/smart-search/_internal/smart_search/cli.py +35 -3
  11. package/runtime/smart-search/_internal/smart_search/config.py +20 -1
  12. package/runtime/smart-search/_internal/smart_search/jev_search.py +9 -1
  13. package/runtime/smart-search/_internal/smart_search/provider_descriptions.py +14 -0
  14. package/runtime/smart-search/_internal/smart_search/providers/__init__.py +2 -0
  15. package/runtime/smart-search/_internal/smart_search/providers/deepseek.py +110 -0
  16. package/runtime/smart-search/_internal/smart_search/service.py +84 -10
  17. package/runtime/smart-search/_internal/smart_search/ui_metadata.py +18 -0
  18. package/runtime/smart-search/_internal/smart_search/ui_server.py +44 -3
  19. package/runtime/smart-search/_internal/smart_search-0.1.30b1.dist-info/METADATA +294 -0
  20. package/runtime/smart-search/_internal/{smart_search-0.1.28.dist-info → smart_search-0.1.30b1.dist-info}/RECORD +25 -23
  21. package/runtime/smart-search/_internal/{smart_search-0.1.28.dist-info → smart_search-0.1.30b1.dist-info}/WHEEL +1 -1
  22. package/runtime/smart-search/_internal/wcwidth/_wcwidth_c.abi3.so +0 -0
  23. package/runtime/smart-search/package.json +7 -7
  24. package/runtime/smart-search/smart-search +0 -0
  25. package/runtime/smart-search/_internal/smart_search-0.1.28.dist-info/METADATA +0 -97
  26. /package/runtime/smart-search/_internal/{smart_search-0.1.28.dist-info → smart_search-0.1.30b1.dist-info}/INSTALLER +0 -0
  27. /package/runtime/smart-search/_internal/{smart_search-0.1.28.dist-info → smart_search-0.1.30b1.dist-info}/REQUESTED +0 -0
  28. /package/runtime/smart-search/_internal/{smart_search-0.1.28.dist-info → smart_search-0.1.30b1.dist-info}/direct_url.json +0 -0
  29. /package/runtime/smart-search/_internal/{smart_search-0.1.28.dist-info → smart_search-0.1.30b1.dist-info}/entry_points.txt +0 -0
  30. /package/runtime/smart-search/_internal/{smart_search-0.1.28.dist-info → smart_search-0.1.30b1.dist-info}/licenses/LICENSE +0 -0
  31. /package/runtime/smart-search/_internal/{smart_search-0.1.28.dist-info → smart_search-0.1.30b1.dist-info}/top_level.txt +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konbakuyomu/smart-search-linux-x64",
3
- "version": "0.1.28",
3
+ "version": "0.1.30-beta.1",
4
4
  "description": "Self-contained Smart Search CLI for linux/x64",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -2005,29 +2005,6 @@ RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx
2005
2005
  iN66zB+Afko=
2006
2006
  -----END CERTIFICATE-----
2007
2007
 
2008
- # Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI
2009
- # Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI
2010
- # Label: "emSign ECC Root CA - G3"
2011
- # Serial: 287880440101571086945156
2012
- # MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40
2013
- # SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1
2014
- # SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b
2015
- -----BEGIN CERTIFICATE-----
2016
- MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG
2017
- EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo
2018
- bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g
2019
- RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ
2020
- TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s
2021
- b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw
2022
- djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0
2023
- WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS
2024
- fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB
2025
- zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq
2026
- hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB
2027
- CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD
2028
- +JbNR6iC8hZVdyR+EhCVBCyj
2029
- -----END CERTIFICATE-----
2030
-
2031
2008
  # Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI
2032
2009
  # Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI
2033
2010
  # Label: "emSign Root CA - C1"
@@ -17858,5 +17858,169 @@
17858
17858
  "{0} 另外,联网搜索能力检查未通过:{1}。OpenAI-compatible 不会发送搜索工具;如果该模型只能通过 Responses 的 web_search 工具联网,请把 XAI_API_URL、XAI_API_KEY、XAI_MODEL 指向支持 /v1/responses 的中转站。": [
17859
17859
  "{0} 另外,联网搜索能力检查未通过:{1}。OpenAI-compatible 不会发送搜索工具;如果该模型只能通过 Responses 的 web_search 工具联网,请把 XAI_API_URL、XAI_API_KEY、XAI_MODEL 指向支持 /v1/responses 的中转站。",
17860
17860
  "{0} Also, the web search capability check did not pass: {1}. OpenAI-compatible sends no search tool; if this model can only search through the Responses web_search tool, point XAI_API_URL, XAI_API_KEY and XAI_MODEL at a relay that supports /v1/responses."
17861
+ ],
17862
+ "DeepSeek search response is not a JSON object": [
17863
+ "DeepSeek 搜索响应不是 JSON 对象",
17864
+ "DeepSeek search response is not a JSON object"
17865
+ ],
17866
+ "DeepSeek search response is missing results": [
17867
+ "DeepSeek 搜索响应缺少 results",
17868
+ "DeepSeek search response is missing results"
17869
+ ],
17870
+ "DeepSeek fetch response is not a JSON object": [
17871
+ "DeepSeek 抓取响应不是 JSON 对象",
17872
+ "DeepSeek fetch response is not a JSON object"
17873
+ ],
17874
+ "DeepSeek fetch content is not text": [
17875
+ "DeepSeek 抓取内容不是文本",
17876
+ "DeepSeek fetch content is not text"
17877
+ ],
17878
+ "DeepSeek search: {0}": [
17879
+ "DeepSeek 搜索:{0}",
17880
+ "DeepSeek search: {0}"
17881
+ ],
17882
+ "DeepSeek fetch: {0}": [
17883
+ "DeepSeek 抓取:{0}",
17884
+ "DeepSeek fetch: {0}"
17885
+ ],
17886
+ "DeepSeek fetch returned no content for {0}": [
17887
+ "DeepSeek 抓取 {0} 没有返回正文",
17888
+ "DeepSeek fetch returned no content for {0}"
17889
+ ],
17890
+ "DEEPSEEK_API_KEY is not set. DeepSeek search and fetch are unavailable.": [
17891
+ "DEEPSEEK_API_KEY 未设置,DeepSeek 搜索与抓取不可用",
17892
+ "DEEPSEEK_API_KEY is not set. DeepSeek search and fetch are unavailable."
17893
+ ],
17894
+ "DEEPSEEK_API_KEY 未设置,DeepSeek 搜索与抓取不可用": [
17895
+ "DEEPSEEK_API_KEY 未设置,DeepSeek 搜索与抓取不可用",
17896
+ "DEEPSEEK_API_KEY is not set. DeepSeek search and fetch are unavailable."
17897
+ ],
17898
+ "Both DeepSeek search and fetch APIs are available": [
17899
+ "DeepSeek 搜索与抓取 API 均可用",
17900
+ "Both DeepSeek search and fetch APIs are available"
17901
+ ],
17902
+ "DeepSeek 搜索与抓取 API 均可用": [
17903
+ "DeepSeek 搜索与抓取 API 均可用",
17904
+ "Both DeepSeek search and fetch APIs are available"
17905
+ ],
17906
+ "DeepSeek API request timed out": [
17907
+ "DeepSeek API 请求超时",
17908
+ "DeepSeek API request timed out"
17909
+ ],
17910
+ "DeepSeek API 请求超时": [
17911
+ "DeepSeek API 请求超时",
17912
+ "DeepSeek API request timed out"
17913
+ ],
17914
+ "Save DEEPSEEK_API_KEY.": [
17915
+ "保存 DEEPSEEK_API_KEY。",
17916
+ "Save DEEPSEEK_API_KEY."
17917
+ ],
17918
+ "Save DEEPSEEK_API_URL.": [
17919
+ "保存 DEEPSEEK_API_URL。",
17920
+ "Save DEEPSEEK_API_URL."
17921
+ ],
17922
+ "Save DEEPSEEK_TIMEOUT_SECONDS.": [
17923
+ "保存 DEEPSEEK_TIMEOUT_SECONDS。",
17924
+ "Save DEEPSEEK_TIMEOUT_SECONDS."
17925
+ ],
17926
+ "没有找到 WSL 发行版。": [
17927
+ "没有找到 WSL 发行版。",
17928
+ "No WSL distribution was found."
17929
+ ],
17930
+ "没有在 WSL 中找到 smart-search。请先在 WSL 中安装 CLI,并确认 bash 能直接运行 smart-search。": [
17931
+ "没有在 WSL 中找到 smart-search。请先在 WSL 中安装 CLI,并确认 bash 能直接运行 smart-search。",
17932
+ "smart-search was not found in WSL. Install the CLI in WSL first and make sure bash can run smart-search directly."
17933
+ ],
17934
+ "旧版": [
17935
+ "旧版",
17936
+ "old version"
17937
+ ],
17938
+ "当前连接的是 WSL 中的 CLI,只观察它自己的配置目录。": [
17939
+ "当前连接的是 WSL 中的 CLI,只观察它自己的配置目录。",
17940
+ "Connected to a CLI in WSL; only its own configuration directory is observed."
17941
+ ],
17942
+ "这是 WSL 中的目录。需要更换时,在 WSL 中设置 SMART_SEARCH_CONFIG_DIR。": [
17943
+ "这是 WSL 中的目录。需要更换时,在 WSL 中设置 SMART_SEARCH_CONFIG_DIR。",
17944
+ "This directory is inside WSL. To use another one, set SMART_SEARCH_CONFIG_DIR in WSL."
17945
+ ],
17946
+ "WSL 中的 CLI 使用该发行版自己的配置和 Skills。未运行的发行版不会自动启动。": [
17947
+ "WSL 中的 CLI 使用该发行版自己的配置和 Skills。未运行的发行版不会自动启动。",
17948
+ "A CLI in WSL uses that distribution's own configuration and Skills. Stopped distributions are not started automatically."
17949
+ ],
17950
+ "未运行:{0}。“检测 WSL”会启动这些发行版并查找 CLI。": [
17951
+ "未运行:{0}。“检测 WSL”会启动这些发行版并查找 CLI。",
17952
+ "Not running: {0}. Detect WSL starts them to look for the CLI."
17953
+ ],
17954
+ "检测 WSL": [
17955
+ "检测 WSL",
17956
+ "Detect WSL"
17957
+ ],
17958
+ "{0} · 需要更新": [
17959
+ "{0} · 需要更新",
17960
+ "{0} · update needed"
17961
+ ],
17962
+ "位于 WSL {0},使用该发行版中的配置和 Skills。": [
17963
+ "位于 WSL {0},使用该发行版中的配置和 Skills。",
17964
+ "Installed in WSL {0}; it uses that distribution's configuration and Skills."
17965
+ ],
17966
+ "WSL {0} 中的 CLI 版本过旧,App 无法连接。": [
17967
+ "WSL {0} 中的 CLI 版本过旧,App 无法连接。",
17968
+ "The CLI in WSL {0} is too old for the App to connect."
17969
+ ],
17970
+ "此 CLI 由 mise 管理,请在 WSL 终端更新:mise use -g npm:@konbakuyomu/smart-search@latest": [
17971
+ "此 CLI 由 mise 管理,请在 WSL 终端更新:mise use -g npm:@konbakuyomu/smart-search@latest",
17972
+ "mise manages this CLI. Update it from a WSL terminal: mise use -g npm:@konbakuyomu/smart-search@latest"
17973
+ ],
17974
+ "npm 全局目录需要管理员权限,请在 WSL 终端更新:sudo npm install -g @konbakuyomu/smart-search@latest": [
17975
+ "npm 全局目录需要管理员权限,请在 WSL 终端更新:sudo npm install -g @konbakuyomu/smart-search@latest",
17976
+ "Its npm global directory needs administrator rights. Update it from a WSL terminal: sudo npm install -g @konbakuyomu/smart-search@latest"
17977
+ ],
17978
+ "App 无法确定它的安装方式,请在 WSL 终端用原安装方式更新。": [
17979
+ "App 无法确定它的安装方式,请在 WSL 终端用原安装方式更新。",
17980
+ "The App cannot tell how it was installed. Update it from a WSL terminal with the tool you installed it with."
17981
+ ],
17982
+ "WSL {0} 的 npm 全局目录需要管理员权限,App 不使用 sudo。请在 WSL 终端运行:sudo npm install -g @konbakuyomu/smart-search@latest": [
17983
+ "WSL {0} 的 npm 全局目录需要管理员权限,App 不使用 sudo。请在 WSL 终端运行:sudo npm install -g @konbakuyomu/smart-search@latest",
17984
+ "The npm global directory in WSL {0} needs administrator rights, and the App does not use sudo. In a WSL terminal, run: sudo npm install -g @konbakuyomu/smart-search@latest"
17985
+ ],
17986
+ "WSL {0} 中的 Node.js 没有配套的 npm。请重新安装 Node.js,再点击“检测 WSL”。": [
17987
+ "WSL {0} 中的 Node.js 没有配套的 npm。请重新安装 Node.js,再点击“检测 WSL”。",
17988
+ "Node.js in WSL {0} has no matching npm. Reinstall Node.js, then click Detect WSL."
17989
+ ],
17990
+ "WSL {0} 中没有找到 Node.js 18 或更新版本。请先在 WSL 中安装 Node.js,再点击“检测 WSL”。": [
17991
+ "WSL {0} 中没有找到 Node.js 18 或更新版本。请先在 WSL 中安装 Node.js,再点击“检测 WSL”。",
17992
+ "No Node.js 18 or later was found in WSL {0}. Install Node.js in WSL, then click Detect WSL."
17993
+ ],
17994
+ "{0} 中的 CLI 已安装或更新为 {1}。": [
17995
+ "{0} 中的 CLI 已安装或更新为 {1}。",
17996
+ "The CLI in {0} is now {1}."
17997
+ ],
17998
+ "更新 {0} 中的 CLI": [
17999
+ "更新 {0} 中的 CLI",
18000
+ "Update the CLI in {0}"
18001
+ ],
18002
+ "这些环境还没有 Smart Search CLI。安装使用该环境自己的 npm,不会改动其他环境。": [
18003
+ "这些环境还没有 Smart Search CLI。安装使用该环境自己的 npm,不会改动其他环境。",
18004
+ "These environments have no Smart Search CLI yet. Installing uses that environment's own npm and leaves the others unchanged."
18005
+ ],
18006
+ "安装到 {0}": [
18007
+ "安装到 {0}",
18008
+ "Install in {0}"
18009
+ ],
18010
+ "在 WSL 中安装 Node.js": [
18011
+ "在 WSL 中安装 Node.js",
18012
+ "Install Node.js in WSL"
18013
+ ],
18014
+ "未安装 CLI 的环境": [
18015
+ "未安装 CLI 的环境",
18016
+ "Environments without the CLI"
18017
+ ],
18018
+ "WSL {0} · npm {1} · {2}": [
18019
+ "WSL {0} · npm {1} · {2}",
18020
+ "WSL {0} · npm {1} · {2}"
18021
+ ],
18022
+ "正在卸载 CLI…": [
18023
+ "正在卸载 CLI…",
18024
+ "Removing the CLI…"
17861
18025
  ]
17862
18026
  }
@@ -48,7 +48,7 @@
48
48
  - `smart-search map URL [--instructions TEXT] [--max-depth N] [--max-breadth N] [--limit N] [--timeout SECONDS] [--format json|markdown|content] [--output PATH]`
49
49
  - `smart-search doctor [--format json|markdown|content] [--output PATH]`
50
50
  - `smart-search diagnose openai-compatible [--timeout SECONDS] [--format json|markdown] [--output PATH]`
51
- - `smart-search setup [--lang zh|en] [--advanced] [--non-interactive] [--skip-skills] [--install-skills CSV] [--skills-root PATH] [--xai-api-url URL] [--xai-api-key KEY] [--xai-model ID] [--xai-tools-explicit CSV] [--openai-compatible-api-url URL] [--openai-compatible-api-key KEY] [--openai-compatible-model ID] [--openai-compatible-fallback-models CSV] [--openai-compatible-api-mode chat-completions|responses] [--openai-compatible-stream true|false] [--validation-level fast|balanced|strict] [--fallback-mode auto|off] [--minimum-profile standard|off] [--intent-router jev|local] [--search-timeout SECONDS] [--provider-cooldown SECONDS] [--provider-failure-threshold N] [--exa-key KEY] [--context7-key KEY] [--zhipu-key KEY] [--zhipu-api-url URL] [--zhipu-search-engine ENGINE] [--zhipu-mcp-key KEY] [--zhipu-mcp-search-api-url URL] [--zhipu-mcp-reader-api-url URL] [--zhipu-mcp-zread-api-url URL] [--zhipu-mcp-timeout SECONDS] [--jina-key KEY] [--jina-reader-api-url URL] [--jina-respond-with MODE] [--jina-timeout SECONDS] [--tavily-api-url URL] [--tavily-key KEY] [--firecrawl-api-url URL] [--firecrawl-key KEY] [--tinyfish-key KEY] [--tinyfish-search-api-url URL] [--tinyfish-fetch-api-url URL] [--tinyfish-timeout SECONDS] [--anysearch-api-url URL] [--anysearch-key KEY] [--anysearch-timeout SECONDS] [--sciverse-api-url URL] [--sciverse-token TOKEN] [--sciverse-timeout SECONDS] [--format json|markdown|content] [--output PATH]`
51
+ - `smart-search setup [--lang zh|en] [--advanced] [--non-interactive] [--skip-skills] [--install-skills CSV] [--skills-root PATH] [--xai-api-url URL] [--xai-api-key KEY] [--xai-model ID] [--xai-tools-explicit CSV] [--openai-compatible-api-url URL] [--openai-compatible-api-key KEY] [--openai-compatible-model ID] [--openai-compatible-fallback-models CSV] [--openai-compatible-api-mode chat-completions|responses] [--openai-compatible-stream true|false] [--validation-level fast|balanced|strict] [--fallback-mode auto|off] [--minimum-profile standard|off] [--intent-router jev|local] [--search-timeout SECONDS] [--provider-cooldown SECONDS] [--provider-failure-threshold N] [--exa-key KEY] [--context7-key KEY] [--zhipu-key KEY] [--zhipu-api-url URL] [--zhipu-search-engine ENGINE] [--zhipu-mcp-key KEY] [--zhipu-mcp-search-api-url URL] [--zhipu-mcp-reader-api-url URL] [--zhipu-mcp-zread-api-url URL] [--zhipu-mcp-timeout SECONDS] [--jina-key KEY] [--jina-reader-api-url URL] [--jina-respond-with MODE] [--jina-timeout SECONDS] [--tavily-api-url URL] [--tavily-key KEY] [--firecrawl-api-url URL] [--firecrawl-key KEY] [--tinyfish-key KEY] [--tinyfish-search-api-url URL] [--tinyfish-fetch-api-url URL] [--tinyfish-timeout SECONDS] [--deepseek-key KEY] [--deepseek-api-url URL] [--deepseek-timeout SECONDS] [--anysearch-api-url URL] [--anysearch-key KEY] [--anysearch-timeout SECONDS] [--sciverse-api-url URL] [--sciverse-token TOKEN] [--sciverse-timeout SECONDS] [--format json|markdown|content] [--output PATH]`
52
52
  - `smart-search config path|list|set|unset ... [--format json|markdown|content] [--output PATH]`
53
53
  - `smart-search model set MODEL [--format json|markdown|content] [--output PATH]`
54
54
  - `smart-search model current [--format json|markdown|content] [--output PATH]`
@@ -96,6 +96,8 @@ smart-search config set FIRECRAWL_API_URL "https://api.firecrawl.dev/v2" --forma
96
96
  smart-search config set TINYFISH_API_KEY "key" --format json
97
97
  smart-search config set TINYFISH_SEARCH_API_URL "https://api.search.tinyfish.ai" --format json
98
98
  smart-search config set TINYFISH_FETCH_API_URL "https://api.fetch.tinyfish.ai" --format json
99
+ smart-search config set DEEPSEEK_API_KEY "key" --format json
100
+ smart-search config set DEEPSEEK_API_URL "https://api.deepseek.com" --format json
99
101
  smart-search model current --format json
100
102
  smart-search doctor --format json
101
103
  smart-search doctor --format markdown
@@ -32,11 +32,11 @@ For research, `SMART_SEARCH_JEV_SYNTHESIZE` accepts `true`, `false`, and `auto`.
32
32
  - Jina Reader is `web_fetch` only, not a general search provider. `JINA_API_KEY` is required before Jina satisfies the standard minimum profile; anonymous `r.jina.ai` is explicit/experimental fetch behavior.
33
33
  - Same-capability fallback is allowed; model answers and web search are mutual fallbacks in ordinary search. Context7 is not used for unrelated broad web queries, and page extraction providers are not used as docs search providers.
34
34
  - `TAVILY_ENABLED=false` removes Tavily from registered `web_search` and `web_fetch` routes even when its key is present. Direct Tavily search/extract calls, `map`, `doctor`, and smoke must make no Tavily network request; `map` reports a local configuration error. Firecrawl remains independently configured and all fallback stays within its capability.
35
- - In Jev mode, Jev chooses among allowed capabilities and providers for the question; there is no mandatory main-model call or fixed global provider order. Local routing prioritizes suitable capabilities and configured preferences.
36
- - `main_search` contains xAI Responses and OpenAI-compatible model channels; `web_search` contains Zhipu REST/MCP, Tavily, Firecrawl and TinyFish; `docs_search` contains Context7 and Exa. Failure fallback stays within the selected capability family; model answers and web search are mutual fallbacks in ordinary search.
35
+ - In Jev mode, Jev chooses among allowed capabilities and providers for the question; there is no mandatory main-model call or fixed global provider order. Local routing prioritizes suitable capabilities and configured preferences; a docs question with no docs channel configured uses web search instead of no channel.
36
+ - `main_search` contains xAI Responses and OpenAI-compatible model channels; `web_search` contains Zhipu REST/MCP, Tavily, Firecrawl, TinyFish and DeepSeek; `docs_search` contains Context7 and Exa. Failure fallback stays within the selected capability family; model answers and web search are mutual fallbacks in ordinary search.
37
37
  - Ordinary Context7 uses the first library in Context7's ranked candidates, without a second Jev call. Research may use Jev to select the matching library, using the first candidate if Jev fails or no score reaches its threshold. Empty results may fall through to same-capability Exa; explicit Context7 commands remain unchanged.
38
38
  - `--extra-sources N` controls the selected channel's result count. It does not add channels, split a budget between providers, or make Tavily/Firecrawl a required extra stage. A known-URL search follows the configured Jev/local routing; local routing prefers page reading.
39
- - `fetch` tries Tavily first, then Jina with `JINA_API_KEY`, then Zhipu Coding Plan MCP Reader, then Firecrawl, then TinyFish.
39
+ - `fetch` tries Tavily first, then Jina with `JINA_API_KEY`, then Zhipu Coding Plan MCP Reader, then Firecrawl, then TinyFish, then DeepSeek.
40
40
 
41
41
  TinyFish:
42
42
 
@@ -47,6 +47,14 @@ TinyFish:
47
47
  - Fetch reads `results[0].text`; `errors[]` entries are surfaced as `provider_error` with the upstream message. A payload with neither results nor errors is `provider_error`, and a non-object payload is `parse_error`.
48
48
  - Challenge pages such as `Checking if the site connection is secure` are reported as `quality_error` so same-capability fallback can continue.
49
49
  - `401`/403 map to `auth_error`, `408` to `timeout`, `429` to `rate_limited`, `5xx` to `network_error`, and `400`/422 to `parameter_error`.
50
+ DeepSeek:
51
+
52
+ - `DEEPSEEK_API_KEY` is optional and registers DeepSeek for both `web_search` and `web_fetch`; it is the same key as the DeepSeek model API. It never satisfies `main_search` or `docs_search`. Jev may select it directly; the direct `fetch` fallback order remains as listed above.
53
+ - `DEEPSEEK_API_URL` defaults to `https://api.deepseek.com`; search calls `POST /v1/tools/search` with `{"query", "count"}` and fetch calls `POST /v1/tools/fetch` with `{"url"}`, both with `Authorization: Bearer`. A trailing `/v1` in the base URL is accepted. Never log or echo the key.
54
+ - DeepSeek accepts queries of at most 128 characters, so longer queries are shortened at a word boundary. `site:`, `-site:`, `lang:`, `after:`/`before:` and quoted phrases in the query pass through unchanged.
55
+ - Search results normalize `summary` (falling back to `snippet`) to `description`, `site_name` to `source`, and `date` to `published_date`. Summaries are not page text: fetch a key page before citing it. An empty result set is a successful empty response.
56
+ - Fetch returns the page Markdown. HTTP 403 (URL refused by DeepSeek's safety policy) and 404 (no usable page content, which currently includes PDF links and private network addresses) mean that page has nothing to read: the attempt is `empty`, fallback continues, and the provider is not cooled down. `401` maps to `auth_error`, `402` (insufficient balance) to `provider_error`, `429` to `rate_limited`, `5xx` to `network_error`.
57
+ - DeepSeek bills each successful search or fetch request. `doctor` and `providers test deepseek` send one real request to each.
50
58
  - `map` currently uses Tavily only.
51
59
  - `exa-search` and `exa-similar` use Exa only.
52
60
  - `context7-library` and `context7-docs` use Context7 only.
@@ -98,6 +98,8 @@
98
98
  - Use `smart-search setup --non-interactive --tinyfish-key "key"` to add TinyFish to `web_search` and `web_fetch`. It never satisfies `main_search` or `docs_search`; Jev can select it among the allowed channels.
99
99
  - `TINYFISH_SEARCH_API_URL` defaults to `https://api.search.tinyfish.ai`, and `TINYFISH_FETCH_API_URL` defaults to `https://api.fetch.tinyfish.ai`. Both authenticate with the `X-API-Key` header, not a bearer token.
100
100
  - `TINYFISH_TIMEOUT_SECONDS` defaults to `60` and covers both TinyFish search and fetch reads.
101
+ - Use `smart-search setup --non-interactive --deepseek-key "key"` to add DeepSeek to `web_search` and `web_fetch` with the same key as the DeepSeek model API. It never satisfies `main_search` or `docs_search`; Jev can select it among the allowed channels.
102
+ - `DEEPSEEK_API_URL` defaults to `https://api.deepseek.com`, and `DEEPSEEK_TIMEOUT_SECONDS` defaults to `60` and covers both DeepSeek search and fetch reads. Each successful DeepSeek request is billed.
101
103
 
102
104
  ## Provider Failure Cooldown
103
105
 
@@ -691,7 +691,7 @@ details.more[open] > summary::before { content: "▾ "; }
691
691
  { capability: "docs_search", title: "stepDocs",
692
692
  providers: ["context7", "exa"] },
693
693
  { capability: "web_fetch", title: "stepFetch",
694
- providers: ["tavily", "jina", "zhipu-mcp-reader", "zhipu-mcp", "firecrawl"] }
694
+ providers: ["tavily", "jina", "zhipu-mcp-reader", "zhipu-mcp", "firecrawl", "deepseek"] }
695
695
  ];
696
696
 
697
697
  function viewGettingStarted() {
@@ -627,6 +627,7 @@ def _format_doctor_markdown(data: dict[str, Any]) -> str:
627
627
  ("jina", data.get("jina_connection_test") or {}),
628
628
  ("firecrawl", data.get("firecrawl_connection_test") or {}),
629
629
  ("tinyfish", data.get("tinyfish_connection_test") or {}),
630
+ ("deepseek", data.get("deepseek_connection_test") or {}),
630
631
  ("zhipu", data.get("zhipu_connection_test") or {}),
631
632
  ("zhipu-mcp", data.get("zhipu_mcp_connection_test") or {}),
632
633
  ("context7", data.get("context7_connection_test") or {}),
@@ -1478,6 +1479,7 @@ def _display_provider(provider: str, lang: str) -> str:
1478
1479
  "tavily": "Tavily",
1479
1480
  "firecrawl": "Firecrawl",
1480
1481
  "tinyfish": "TinyFish",
1482
+ "deepseek": "DeepSeek",
1481
1483
  "anysearch": "AnySearch",
1482
1484
  "sciverse": "Sciverse",
1483
1485
  }
@@ -2078,7 +2080,7 @@ def _prompt_web_fetch(values: dict[str, str], current: dict[str, str], lang: str
2078
2080
  "选择 web_fetch provider",
2079
2081
  "Choose web_fetch providers",
2080
2082
  ),
2081
- ["tavily", "jina", "firecrawl", "tinyfish"],
2083
+ ["tavily", "jina", "firecrawl", "tinyfish", "deepseek"],
2082
2084
  default_selected,
2083
2085
  lang,
2084
2086
  )
@@ -2110,6 +2112,13 @@ def _prompt_web_fetch(values: dict[str, str], current: dict[str, str], lang: str
2110
2112
  current.get("TINYFISH_API_KEY", ""),
2111
2113
  lang=lang,
2112
2114
  )
2115
+ if "deepseek" in selected:
2116
+ values["DEEPSEEK_API_KEY"] = _prompt_value(
2117
+ "DEEPSEEK_API_KEY",
2118
+ "DeepSeek API key",
2119
+ current.get("DEEPSEEK_API_KEY", ""),
2120
+ lang=lang,
2121
+ )
2113
2122
 
2114
2123
 
2115
2124
  def _prompt_optional_enhancements(values: dict[str, str], current: dict[str, str], lang: str) -> None:
@@ -2347,6 +2356,9 @@ def _run_advanced_setup_prompts(values: dict[str, str], current: dict[str, str],
2347
2356
  ("TINYFISH_SEARCH_API_URL", "TinyFish Search API URL", True),
2348
2357
  ("TINYFISH_FETCH_API_URL", "TinyFish Fetch API URL", True),
2349
2358
  ("TINYFISH_TIMEOUT_SECONDS", "TinyFish timeout seconds", True),
2359
+ ("DEEPSEEK_API_KEY", "DeepSeek API key", True),
2360
+ ("DEEPSEEK_API_URL", "DeepSeek API URL", True),
2361
+ ("DEEPSEEK_TIMEOUT_SECONDS", "DeepSeek timeout seconds", True),
2350
2362
  ("ANYSEARCH_API_URL", "AnySearch MCP API URL", True),
2351
2363
  ("ANYSEARCH_API_KEY", "AnySearch API key", True),
2352
2364
  ("ANYSEARCH_TIMEOUT_SECONDS", "AnySearch timeout seconds", True),
@@ -2370,7 +2382,7 @@ def _run_advanced_setup_prompts(values: dict[str, str], current: dict[str, str],
2370
2382
  value = _normalize_custom_base_url(value)
2371
2383
  elif key == "SCIVERSE_API_URL":
2372
2384
  value = _normalize_custom_base_url(value)
2373
- elif key in {"TINYFISH_SEARCH_API_URL", "TINYFISH_FETCH_API_URL"}:
2385
+ elif key in {"TINYFISH_SEARCH_API_URL", "TINYFISH_FETCH_API_URL", "DEEPSEEK_API_URL"}:
2374
2386
  value = _normalize_custom_base_url(value)
2375
2387
  values[key] = value
2376
2388
 
@@ -2775,6 +2787,9 @@ def _run_setup(args: argparse.Namespace) -> int:
2775
2787
  "TINYFISH_SEARCH_API_URL": _normalize_custom_base_url(args.tinyfish_search_api_url),
2776
2788
  "TINYFISH_FETCH_API_URL": _normalize_custom_base_url(args.tinyfish_fetch_api_url),
2777
2789
  "TINYFISH_TIMEOUT_SECONDS": args.tinyfish_timeout,
2790
+ "DEEPSEEK_API_KEY": args.deepseek_key,
2791
+ "DEEPSEEK_API_URL": _normalize_custom_base_url(args.deepseek_api_url),
2792
+ "DEEPSEEK_TIMEOUT_SECONDS": args.deepseek_timeout,
2778
2793
  "ANYSEARCH_API_URL": _normalize_custom_base_url(args.anysearch_api_url),
2779
2794
  "ANYSEARCH_API_KEY": args.anysearch_key,
2780
2795
  "ANYSEARCH_TIMEOUT_SECONDS": args.anysearch_timeout,
@@ -2885,6 +2900,7 @@ def _run_regression() -> int:
2885
2900
  "tests/test_jina_provider.py",
2886
2901
  "tests/test_tinyfish_provider.py",
2887
2902
  "tests/test_tinyfish_service.py",
2903
+ "tests/test_deepseek_provider.py",
2888
2904
  "tests/test_zhipu_mcp_provider.py",
2889
2905
  "tests/test_smoke.py",
2890
2906
  "tests/test_intent_router.py",
@@ -3484,6 +3500,9 @@ def build_parser() -> argparse.ArgumentParser:
3484
3500
  setup_parser.add_argument("--tinyfish-search-api-url", default="", help="Save TINYFISH_SEARCH_API_URL.")
3485
3501
  setup_parser.add_argument("--tinyfish-fetch-api-url", default="", help="Save TINYFISH_FETCH_API_URL.")
3486
3502
  setup_parser.add_argument("--tinyfish-timeout", default="", help="Save TINYFISH_TIMEOUT_SECONDS.")
3503
+ setup_parser.add_argument("--deepseek-key", default="", help="Save DEEPSEEK_API_KEY.")
3504
+ setup_parser.add_argument("--deepseek-api-url", default="", help="Save DEEPSEEK_API_URL.")
3505
+ setup_parser.add_argument("--deepseek-timeout", default="", help="Save DEEPSEEK_TIMEOUT_SECONDS.")
3487
3506
  setup_parser.add_argument("--anysearch-api-url", default="", help="Save ANYSEARCH_API_URL.")
3488
3507
  setup_parser.add_argument("--anysearch-key", default="", help="Save ANYSEARCH_API_KEY.")
3489
3508
  setup_parser.add_argument("--anysearch-timeout", default="", help="Save ANYSEARCH_TIMEOUT_SECONDS.")
@@ -3562,6 +3581,18 @@ def _main(argv: list[str] | None = None) -> int:
3562
3581
  return EXIT_RUNTIME_ERROR
3563
3582
 
3564
3583
 
3584
+ def _desktop_launch_command() -> list[str]:
3585
+ """Argv that starts this same CLI without a shell, e.g. inside WSL from the Windows App."""
3586
+ package_root = os.environ.get("SMART_SEARCH_PACKAGE_ROOT", "")
3587
+ node = os.environ.get("SMART_SEARCH_NODE_PATH", "")
3588
+ wrapper = Path(package_root) / "npm" / "bin" / "smart-search.js"
3589
+ if package_root and node and wrapper.is_file():
3590
+ return [node, str(wrapper)]
3591
+ if getattr(sys, "frozen", False):
3592
+ return [sys.executable]
3593
+ return [sys.executable, "-m", "smart_search.cli"]
3594
+
3595
+
3565
3596
  def main(argv: list[str] | None = None) -> int:
3566
3597
  arguments = list(sys.argv[1:] if argv is None else argv)
3567
3598
  if arguments == ["--desktop-backend"]:
@@ -3569,7 +3600,8 @@ def main(argv: list[str] | None = None) -> int:
3569
3600
  return desktop_main()
3570
3601
  if arguments == ["--desktop-capabilities"]:
3571
3602
  print(json.dumps({"product": "smart-search", "version": _get_version(), "activity_protocol_version": 1,
3572
- "desktop_protocol_version": 1, "skills_protocol_version": 1}))
3603
+ "desktop_protocol_version": 1, "skills_protocol_version": 1,
3604
+ "launch_command": _desktop_launch_command()}))
3573
3605
  return EXIT_OK
3574
3606
  try:
3575
3607
  arguments, language, warning = _command_language(arguments)
@@ -58,6 +58,7 @@ class Config:
58
58
  "tavily": "TAVILY_ENABLED",
59
59
  "firecrawl": "FIRECRAWL_ENABLED",
60
60
  "tinyfish": "TINYFISH_ENABLED",
61
+ "deepseek": "DEEPSEEK_ENABLED",
61
62
  "jina": "JINA_ENABLED",
62
63
  "anysearch": "ANYSEARCH_ENABLED",
63
64
  "sciverse": "SCIVERSE_ENABLED",
@@ -122,6 +123,9 @@ class Config:
122
123
  "TINYFISH_SEARCH_API_URL",
123
124
  "TINYFISH_FETCH_API_URL",
124
125
  "TINYFISH_TIMEOUT_SECONDS",
126
+ "DEEPSEEK_API_KEY",
127
+ "DEEPSEEK_API_URL",
128
+ "DEEPSEEK_TIMEOUT_SECONDS",
125
129
  "ANYSEARCH_API_KEY",
126
130
  "ANYSEARCH_API_URL",
127
131
  "ANYSEARCH_TIMEOUT_SECONDS",
@@ -712,7 +716,7 @@ class Config:
712
716
  if key in JEV_DEFAULTS:
713
717
  validate_jev_value(key, value)
714
718
  return
715
- if key in {"SMART_SEARCH_TIMEOUT_SECONDS", "TINYFISH_TIMEOUT_SECONDS"}:
719
+ if key in {"SMART_SEARCH_TIMEOUT_SECONDS", "TINYFISH_TIMEOUT_SECONDS", "DEEPSEEK_TIMEOUT_SECONDS"}:
716
720
  self._parse_positive_float_value(key, value)
717
721
  return
718
722
  if key == "SMART_SEARCH_PROVIDER_COOLDOWN_SECONDS":
@@ -970,6 +974,18 @@ class Config:
970
974
  def tinyfish_timeout(self) -> float:
971
975
  return self._parse_positive_float_value("TINYFISH_TIMEOUT_SECONDS", self._get_config_value("TINYFISH_TIMEOUT_SECONDS", "150") or "150")
972
976
 
977
+ @property
978
+ def deepseek_api_key(self) -> str | None:
979
+ return self._get_config_value("DEEPSEEK_API_KEY")
980
+
981
+ @property
982
+ def deepseek_api_url(self) -> str:
983
+ return self._get_config_value("DEEPSEEK_API_URL", "https://api.deepseek.com") or "https://api.deepseek.com"
984
+
985
+ @property
986
+ def deepseek_timeout(self) -> float:
987
+ return self._parse_positive_float_value("DEEPSEEK_TIMEOUT_SECONDS", self._get_config_value("DEEPSEEK_TIMEOUT_SECONDS", "60") or "60")
988
+
973
989
  @property
974
990
  def anysearch_api_url(self) -> str:
975
991
  return self._get_config_value("ANYSEARCH_API_URL", "https://api.anysearch.com/mcp") or "https://api.anysearch.com/mcp"
@@ -1253,6 +1269,9 @@ class Config:
1253
1269
  "TINYFISH_FETCH_API_URL": self.tinyfish_fetch_api_url,
1254
1270
  "TINYFISH_API_KEY": self._mask_api_key(self.tinyfish_api_key) if self.tinyfish_api_key else tr('未配置'),
1255
1271
  "TINYFISH_TIMEOUT_SECONDS": self.tinyfish_timeout,
1272
+ "DEEPSEEK_API_URL": self.deepseek_api_url,
1273
+ "DEEPSEEK_API_KEY": self._mask_api_key(self.deepseek_api_key) if self.deepseek_api_key else tr('未配置'),
1274
+ "DEEPSEEK_TIMEOUT_SECONDS": self.deepseek_timeout,
1256
1275
  "ANYSEARCH_API_URL": self.anysearch_api_url,
1257
1276
  "ANYSEARCH_API_KEY": self._mask_api_key(self.anysearch_api_key) if self.anysearch_api_key else tr('未配置'),
1258
1277
  "ANYSEARCH_TIMEOUT_SECONDS": self.anysearch_timeout,
@@ -89,9 +89,13 @@ def fallback_channels(svc: Any, query: str, candidates: list[dict], limit: int,
89
89
  if reading:
90
90
  order = svc._research_fetch_order(query, reading[0]["url"])
91
91
  return sorted(reading, key=lambda item: order.index(item["provider"]) if item["provider"] in order else len(order))[:limit]
92
+ allowed = []
92
93
  if rule.docs_intent:
93
94
  allowed = [item for item in candidates if item["operation"] == "search" and item["capability"] == "docs_search"]
94
- else:
95
+ # With no docs channel configured, a docs question still gets general web
96
+ # discovery instead of no channel at all. Failure fallback passes only
97
+ # same-family candidates, so this cannot cross a capability boundary.
98
+ if not allowed:
95
99
  allowed = [item for item in candidates if item["operation"] == "search" and
96
100
  (item["capability"] in {"main_search", "web_search"} or item["provider"] == "exa" or
97
101
  item["capability"] in rule.required_capabilities)]
@@ -162,6 +166,8 @@ class ChannelExecutor:
162
166
  content = _check_data(await svc.zhipu_mcp_reader(url)).get("content")
163
167
  elif provider == "tinyfish":
164
168
  content = _check_data(await svc.call_tinyfish_fetch(url)).get("content")
169
+ elif provider == "deepseek":
170
+ content = await svc.call_deepseek_fetch(url)
165
171
  else:
166
172
  raise ProviderCallError("parameter_error", source_message('Unsupported fetch channel'))
167
173
  results = [{"url": url, "content": content}]
@@ -177,6 +183,8 @@ class ChannelExecutor:
177
183
  results = await svc.call_firecrawl_search(query, self.count) or []
178
184
  elif provider == "tinyfish":
179
185
  results = await svc.call_tinyfish_search(query, self.count) or []
186
+ elif provider == "deepseek":
187
+ results = await svc.call_deepseek_search(query, self.count) or []
180
188
  elif provider == "anysearch":
181
189
  results = _check_data(await svc.anysearch_search(query, max_results=self.count)).get("results", [])
182
190
  elif provider == "context7":
@@ -113,6 +113,20 @@ PROVIDER_DESCRIPTIONS: dict[str, dict[str, dict[str, str]]] = {
113
113
  "guidance_zh": "擅长读取已找到的页面;不适合发现新来源或执行浏览器自动化。",
114
114
  },
115
115
  },
116
+ "deepseek": {
117
+ "search": {
118
+ "en": "Searches the web with DeepSeek's own search engine and returns linked results with long query-focused summaries.",
119
+ "zh": "通过 DeepSeek 自研搜索引擎检索网页,返回链接和与问题相关的长摘要。",
120
+ "guidance_en": "Best for broad web and Chinese-language source discovery; summaries are not full page text, so read key pages before citing them. Queries longer than 128 characters are shortened.",
121
+ "guidance_zh": "擅长广泛发现网页和中文来源;摘要不是网页全文,引用关键结论前应读取原页。超过 128 个字符的查询会被截短。",
122
+ },
123
+ "fetch": {
124
+ "en": "Fetches a known URL as markdown through DeepSeek Fetch.",
125
+ "zh": "通过 DeepSeek Fetch 将已知链接读取为 Markdown。",
126
+ "guidance_en": "Best for reading public web pages already found; unsuitable for source discovery, PDF files, login-only pages or private network addresses.",
127
+ "guidance_zh": "擅长读取已找到的公开网页;不适合发现新来源,也读不了 PDF、需登录的页面或内网地址。",
128
+ },
129
+ },
116
130
  "anysearch": {
117
131
  "search": {
118
132
  "en": "Searches AnySearch's available domains with a plain query.",
@@ -1,6 +1,7 @@
1
1
  from .base import BaseSearchProvider, SearchResult
2
2
  from .anysearch import AnySearchProvider
3
3
  from .context7 import Context7Provider
4
+ from .deepseek import DeepSeekToolsProvider
4
5
  from .openai_compatible import OpenAICompatibleSearchProvider
5
6
  from .xai_responses import XAIResponsesSearchProvider
6
7
  from .exa import ExaSearchProvider
@@ -15,6 +16,7 @@ __all__ = [
15
16
  "SearchResult",
16
17
  "AnySearchProvider",
17
18
  "Context7Provider",
19
+ "DeepSeekToolsProvider",
18
20
  "OpenAICompatibleSearchProvider",
19
21
  "XAIResponsesSearchProvider",
20
22
  "ExaSearchProvider",
@@ -0,0 +1,110 @@
1
+ """DeepSeek web search and page fetch.
2
+
3
+ Both tools live under the DeepSeek API host (`/v1/tools/search` and
4
+ `/v1/tools/fetch`) and use the same Bearer key as the DeepSeek chat API.
5
+ """
6
+ from ..i18n import source_message
7
+
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ from ..provider_errors import ProviderCallError, provider_call_error
13
+
14
+
15
+ MAX_QUERY_CHARS = 128
16
+ MAX_COUNT = 50
17
+ # Fetch answers 403 when its safety policy refuses a URL and 404 when the page
18
+ # has no usable Markdown. Both describe that one page, not the key or the
19
+ # service, so they read as "no content" and leave provider health alone.
20
+ PAGE_UNAVAILABLE_STATUSES = frozenset({403, 404})
21
+
22
+
23
+ def tools_endpoint(api_url: str, tool: str) -> str:
24
+ base = api_url.rstrip("/")
25
+ # The chat API documents both https://api.deepseek.com and .../v1 as its base.
26
+ if base.endswith("/v1"):
27
+ base = base[: -len("/v1")]
28
+ return f"{base}/v1/tools/{tool}"
29
+
30
+
31
+ def search_query(query: str) -> str:
32
+ """Fit a query into the 128-character limit, preferring a word boundary."""
33
+ text = " ".join(query.split())
34
+ if len(text) <= MAX_QUERY_CHARS:
35
+ return text
36
+ cut = text[:MAX_QUERY_CHARS]
37
+ space = cut.rfind(" ")
38
+ return cut[:space] if space >= MAX_QUERY_CHARS // 2 else cut
39
+
40
+
41
+ def _normalize_search_result(item: dict[str, Any]) -> dict[str, Any]:
42
+ out: dict[str, Any] = {
43
+ "title": item.get("title") or "",
44
+ "url": item.get("url") or "",
45
+ # summary is the longer, query-focused excerpt DeepSeek writes for models.
46
+ "description": item.get("summary") or item.get("snippet") or "",
47
+ "provider": "deepseek",
48
+ }
49
+ site_name = item.get("site_name") or ""
50
+ if site_name:
51
+ out["source"] = site_name
52
+ published = item.get("date") or ""
53
+ if published:
54
+ out["published_date"] = published
55
+ return out
56
+
57
+
58
+ class DeepSeekToolsProvider:
59
+ def __init__(self, api_url: str, api_key: str, timeout: float = 60.0):
60
+ self.api_url = api_url
61
+ self.api_key = api_key or ""
62
+ self.timeout = timeout
63
+
64
+ def get_provider_name(self) -> str:
65
+ return "deepseek"
66
+
67
+ async def _post(self, tool: str, body: dict[str, Any]) -> httpx.Response:
68
+ headers = {
69
+ "Authorization": f"Bearer {self.api_key}",
70
+ "Content-Type": "application/json",
71
+ "Accept": "application/json",
72
+ }
73
+ timeout = httpx.Timeout(connect=6.0, read=self.timeout, write=10.0, pool=None)
74
+ async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
75
+ return await client.post(tools_endpoint(self.api_url, tool), headers=headers, json=body)
76
+
77
+ async def search(self, query: str, max_results: int = 5) -> list[dict[str, Any]]:
78
+ """Return normalized results; an empty list means DeepSeek found nothing."""
79
+ body = {"query": search_query(query), "count": max(1, min(int(max_results), MAX_COUNT))}
80
+ try:
81
+ response = await self._post("search", body)
82
+ response.raise_for_status()
83
+ payload = response.json()
84
+ if not isinstance(payload, dict):
85
+ raise ProviderCallError("parse_error", source_message('DeepSeek search response is not a JSON object'))
86
+ results = payload.get("results")
87
+ if not isinstance(results, list):
88
+ raise ProviderCallError("parse_error", source_message('DeepSeek search response is missing results'))
89
+ except Exception as exc:
90
+ raise provider_call_error(exc, additional_secrets=(self.api_key,)) from exc
91
+ return [_normalize_search_result(item) for item in results[:max_results] if isinstance(item, dict)]
92
+
93
+ async def fetch(self, url: str) -> dict[str, str] | None:
94
+ """Return {title, content}, or None when DeepSeek has no usable text for this page."""
95
+ try:
96
+ response = await self._post("fetch", {"url": url})
97
+ if response.status_code in PAGE_UNAVAILABLE_STATUSES:
98
+ return None
99
+ response.raise_for_status()
100
+ payload = response.json()
101
+ if not isinstance(payload, dict):
102
+ raise ProviderCallError("parse_error", source_message('DeepSeek fetch response is not a JSON object'))
103
+ content = payload.get("content")
104
+ if not isinstance(content, str):
105
+ raise ProviderCallError("parse_error", source_message('DeepSeek fetch content is not text'))
106
+ except Exception as exc:
107
+ raise provider_call_error(exc, additional_secrets=(self.api_key,)) from exc
108
+ if not content.strip():
109
+ return None
110
+ return {"title": str(payload.get("title") or ""), "content": content}