Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions jay0130-a/deepseek_search/.difyignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.venv/
__pycache__/
**/__pycache__/
*.pyc
.pytest_cache/
tests/
.ruff_cache/
24 changes: 24 additions & 0 deletions jay0130-a/deepseek_search/PRIVACY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Privacy Policy

Last updated: 2026-10-18

## What this plugin collects

This plugin does **not** collect, store, or log any user data on its own. It has no database, no persistent storage, and no analytics.

## What is sent to third parties

The plugin forwards the following inputs to the DeepSeek Tools API (`https://api.deepseek.com`), using the API key configured by the user in Dify:

- **`deepseek_web_search`**: the search `query`, the optional `ori_question`, `count`, and `need_content` flag.
- **`deepseek_web_fetch`**: the `url` to retrieve.

The retrieval, extraction, and ranking of results are performed by DeepSeek's infrastructure. DeepSeek's handling of this data is governed by [DeepSeek's Privacy Policy](https://www.deepseek.com/en/privacy).

## Credentials

The DeepSeek API key is stored by the Dify platform as a secret credential and is only used in the `Authorization` header of requests to `api.deepseek.com`. The key is never logged, cached, or transmitted anywhere else by this plugin.

## Data retention

None. The plugin is stateless; responses are returned to the Dify workflow and are not persisted by the plugin.
53 changes: 53 additions & 0 deletions jay0130-a/deepseek_search/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# DeepSeek Search

[Dify](https://dify.ai/) tool plugin for the [DeepSeek Tools API](https://api-docs.deepseek.com/): AI-native web search and Markdown web page extraction, using the same API key as the DeepSeek model platform.

## Tools

### `deepseek_web_search`

Web search backed by DeepSeek's search infrastructure. Each result includes `title`, `url`, `snippet`, `summary`, `site_name`, `date`, `rerank_score` (0-3; ≥1.5 is high quality), and `authority` (0-4; ≥3 is highly authoritative).

Supported search operators (passed through as-is): `site:`, `-site:`, `-` (exclusion), `-"excluded phrase"`, `"exact phrase"`, `lang:`, `after:`, `before:`.

Parameters:

| Name | Required | Description |
|------|----------|-------------|
| `query` | yes | Search query, 1-128 characters. |
| `ori_question` | no | The user's original question (1-128 characters) that triggered the search. |
| `count` | no | Max results, 1-50 (default 10). |
| `need_content` | no | Include full extracted page content per result (default `false`). Increases payload size and latency. |

### `deepseek_web_fetch`

Fetch a single page through DeepSeek's fetch infrastructure and get `title` plus the body as Markdown. This plugin forwards only the URL; retrieval and extraction happen on DeepSeek's side.

| Name | Required | Description |
|------|----------|-------------|
| `url` | yes | HTTP/HTTPS URL, max 8192 characters, must include a host, no username/password. |

## Setup

1. Get an API key from [platform.deepseek.com](https://platform.deepseek.com/). Search and Fetch use the same key and the same account-level rate budget as the model API.
2. In Dify, install this plugin and enter the key in the plugin credentials.
3. Add the `deepseek_web_search` / `deepseek_web_fetch` tools to an Agent, Chatflow, or Workflow.

## Rate limits

- Search: 5 requests/second per account (independent quota).
- Fetch: 5 requests/second per account, plus per-target-domain limits.
- These quotas are account-level, independent of which API key is used, and independent of model concurrency limits.

## Source repository

<https://github.com/Jay0130-a/dify-plugins> (plugin source under `Jay0130-a/deepseek_search`).

## Contact

- GitHub: [@Jay0130-a](https://github.com/Jay0130-a)
- Email: 309071492+Jay0130-a@users.noreply.github.com (via GitHub)

## License

MIT
6 changes: 6 additions & 0 deletions jay0130-a/deepseek_search/_assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
6 changes: 6 additions & 0 deletions jay0130-a/deepseek_search/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from dify_plugin import DifyPluginEnv, Plugin

plugin = Plugin(DifyPluginEnv(MAX_REQUEST_TIMEOUT=120))

if __name__ == "__main__":
plugin.run()
35 changes: 35 additions & 0 deletions jay0130-a/deepseek_search/manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: 0.0.1
type: plugin
author: jay0130-a
name: deepseek_search
label:
en_US: DeepSeek Search
zh_Hans: DeepSeek 搜索
description:
en_US: DeepSeek Search & Fetch tools — AI-native web search with ranked, scored results, and clean Markdown extraction of any web page. Bring your own DeepSeek API key.
zh_Hans: DeepSeek 搜索与抓取工具 —— AI 原生网页搜索(带相关性评分)与任意网页的 Markdown 正文提取。需要您自己的 DeepSeek API Key。
created_at: "2026-10-18T00:00:00Z"
icon: icon.svg
resource:
memory: 268435456
permission:
tool:
enabled: true
model:
enabled: false
plugins:
tools:
- provider/deepseek_search.yaml
privacy: PRIVACY.md
tags:
- search
- productivity
meta:
version: 0.0.1
arch:
- amd64
- arm64
runner:
language: python
version: "3.12"
entrypoint: main
Empty file.
36 changes: 36 additions & 0 deletions jay0130-a/deepseek_search/provider/deepseek_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

from typing import Any

from dify_plugin import ToolProvider
from dify_plugin.errors.tool import ToolProviderCredentialValidationError

from tools._client import DeepSeekToolsError, build_client
from tools._errors import friendly_error_message


class DeepSeekSearchProvider(ToolProvider):
"""Validate the DeepSeek API key with a single minimal web search.

The credential value is never written to logs, error messages, or test
output. Only the HTTP status and a developer-facing message surface
upward.
"""

def _validate_credentials(self, credentials: dict[str, Any]) -> None:
api_key = credentials.get("deepseek_api_key")
if not api_key or not str(api_key).strip():
raise ToolProviderCredentialValidationError(
"DeepSeek API Key is missing. Set it in the provider credentials."
)
try:
with build_client(api_key=str(api_key)) as client:
client.search(query="deepseek", count=1)
except DeepSeekToolsError as exc:
raise ToolProviderCredentialValidationError(
friendly_error_message(exc, fallback="DeepSeek credential validation failed.")
) from None
except Exception as exc: # network or unexpected failure
raise ToolProviderCredentialValidationError(
friendly_error_message(exc, fallback="DeepSeek credential validation failed.")
) from None
33 changes: 33 additions & 0 deletions jay0130-a/deepseek_search/provider/deepseek_search.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
identity:
name: deepseek_search
author: jay0130-a
label:
en_US: DeepSeek Search
zh_Hans: DeepSeek 搜索
description:
en_US: DeepSeek Search & Fetch tools — AI-native web search and web page extraction, powered by the DeepSeek Tools API. Bring your own DeepSeek API key.
zh_Hans: DeepSeek 搜索与抓取工具 —— 由 DeepSeek Tools API 提供的 AI 原生网页搜索与网页正文提取。需要您自己的 DeepSeek API Key。
icon: icon.svg
tags:
- search
- productivity
credentials_for_provider:
deepseek_api_key:
type: secret-input
required: true
label:
en_US: DeepSeek API Key
zh_Hans: DeepSeek API Key
placeholder:
en_US: Paste your DeepSeek API key (starts with sk-...)
zh_Hans: 粘贴你的 DeepSeek API Key(以 sk- 开头)
help:
en_US: Use the same API key as the DeepSeek model platform. Each user must provide their own key; this plugin never ships with a default.
zh_Hans: 使用与 DeepSeek 模型平台相同的 API Key。每位用户需要使用自己的 Key,本插件不附带默认 Key。
url: https://platform.deepseek.com/
extra:
python:
source: provider/deepseek_search.py
tools:
- tools/deepseek_web_search.yaml
- tools/deepseek_web_fetch.yaml
17 changes: 17 additions & 0 deletions jay0130-a/deepseek_search/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[project]
name = "deepseek_search"
version = "0.0.1"
description = "Dify Marketplace tool plugin for the DeepSeek Tools API (web search, web fetch)."
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }

dependencies = [
"dify_plugin>=0.9.0",
"httpx>=0.27,<1.0",
]

[dependency-groups]
dev = [
"pytest>=8.0",
]
2 changes: 2 additions & 0 deletions jay0130-a/deepseek_search/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dify_plugin>=0.9.0
httpx>=0.27,<1.0
Empty file.
136 changes: 136 additions & 0 deletions jay0130-a/deepseek_search/tools/_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Shared HTTP client for the DeepSeek Tools API (search & fetch).

Centralizes:

* Base URL and Bearer authentication (same key as the DeepSeek model platform).
* Request timeouts, so a slow upstream cannot hang a workflow node.
* A single error type (:class:`DeepSeekToolsError`) that carries the HTTP
status code for user-safe mapping in :mod:`tools._errors`.

The API key is passed in by argument and is never logged here.
"""

from __future__ import annotations

from typing import Any

import httpx

BASE_URL = "https://api.deepseek.com"
SEARCH_ENDPOINT = "/v1/tools/search"
FETCH_ENDPOINT = "/v1/tools/fetch"

# Search: 5 QPS per account; Fetch: 5 QPS per account plus per-target-domain
# limits. Keep the client timeout well below Dify's node timeout so retries
# remain possible.
DEFAULT_TIMEOUT_SECONDS = 60.0

MAX_QUERY_CHARS = 128
MAX_COUNT = 50
MAX_URL_CHARS = 8192


class DeepSeekToolsError(Exception):
"""An error response from ( or a failure to reach ) the DeepSeek Tools API."""

def __init__(self, message: str, *, status_code: int | None = None) -> None:
super().__init__(message)
self.status_code = status_code


class DeepSeekToolsClient:
"""Minimal synchronous client for the two DeepSeek Tools endpoints."""

def __init__(self, *, api_key: str, timeout: float = DEFAULT_TIMEOUT_SECONDS) -> None:
self._headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Accept": "application/json",
}
self._timeout = timeout
self._client: httpx.Client | None = None

def __enter__(self) -> DeepSeekToolsClient:
self._client = httpx.Client(
base_url=BASE_URL,
headers=self._headers,
timeout=self._timeout,
)
return self

def __exit__(self, *exc_info: object) -> None:
if self._client is not None:
self._client.close()
self._client = None

def search(
self,
*,
query: str,
count: int | None = None,
ori_question: str | None = None,
need_content: bool = False,
) -> dict[str, Any]:
"""Run a web search; returns the parsed ``results`` payload."""
body: dict[str, Any] = {"query": query, "need_content": need_content}
if count is not None:
body["count"] = count
if ori_question:
body["ori_question"] = ori_question
return self._post(SEARCH_ENDPOINT, body)

def fetch(self, *, url: str) -> dict[str, Any]:
"""Fetch a single page; returns ``{"title": ..., "content": ...}``."""
return self._post(FETCH_ENDPOINT, {"url": url})

def _post(self, endpoint: str, body: dict[str, Any]) -> dict[str, Any]:
if self._client is None:
raise DeepSeekToolsError(
"Client is not started. Use 'with build_client(...) as client:'."
)
try:
response = self._client.post(endpoint, json=body)
except httpx.TimeoutException as exc:
raise DeepSeekToolsError(
f"DeepSeek request timed out after {self._timeout:g}s."
) from exc
except httpx.HTTPError as exc:
raise DeepSeekToolsError(
"Network error reaching api.deepseek.com. Verify outbound connectivity."
) from exc

if response.status_code >= 400:
raise DeepSeekToolsError(
_extract_error_message(response),
status_code=response.status_code,
)
try:
data = response.json()
except ValueError as exc:
raise DeepSeekToolsError(
"DeepSeek returned a non-JSON response.",
status_code=response.status_code,
) from exc
return data


def _extract_error_message(response: httpx.Response) -> str:
detail = ""
try:
payload = response.json()
if isinstance(payload, dict):
error = payload.get("error")
if isinstance(error, dict):
detail = str(error.get("message") or "")
elif error is not None:
detail = str(error)
elif payload.get("message"):
detail = str(payload["message"])
except ValueError:
pass
return f"DeepSeek API error (HTTP {response.status_code}): {detail or response.text[:200]}"


def build_client(*, api_key: str, timeout: float = DEFAULT_TIMEOUT_SECONDS) -> DeepSeekToolsClient:
"""Construct a client for the DeepSeek Tools API (context manager)."""
return DeepSeekToolsClient(api_key=api_key, timeout=timeout)
Loading
Loading