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
Binary file not shown.
121 changes: 121 additions & 0 deletions tools/tool_deepseek_fetch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# DeepSeek 网页抓取工具

基于 DeepSeek 官方 Fetch API(<https://api-docs.deepseek.com/zh-cn/guides/search>)封装的网页抓取工具,输入 URL,返回页面标题与 Markdown 格式正文。

## 一、功能说明

- **单页抓取**:调用 `https://api.deepseek.com/v1/tools/fetch`,将目标网页转换为 Markdown 正文
- **长度可控**:`max_chars` 参数控制返回正文的字符上限,超出自动截断并标注,避免撑爆模型上下文
- **结构化输出**:同时返回 `title` / `content` / `markdown`(标题 + 来源 + 正文拼装)与截断标记
- **降级友好**:任何失败统一返回 `success: false` 而非抛异常,并附带 `error` 与中文 `hint`
- **与搜索配套**:配合「DeepSeek 联网搜索」工具(`tool_deepseek_search`)可组成"搜索 → 抓正文 → 生成回答"的完整链路

## 二、参数说明

### 2.1 启动参数

| 参数 | 组件类型 | 必填 | 说明 |
| :--- | :--- | :--- | :--- |
| `api_key` | 密码框 | 是 | DeepSeek API Key,在 <https://platform.deepseek.com/api_keys> 获取 |

### 2.2 输入参数

| 参数名 | 数据类型 | 必填 | 来源 | 说明 |
| :--- | :--- | :--- | :--- | :--- |
| `url` | string | 是 | 引用参数 | 目标网页地址,必须为含 host 的 http/https URL,不能带用户名密码,长度不超过 8192 |
| `max_chars` | int | 否 | 自定义参数 | 返回正文的最大字符数,默认 8000,超出部分截断 |

### 2.3 输出参数

| 字段 | 类型 | 说明 |
| :--- | :--- | :--- |
| `success` | bool | 是否成功 |
| `title` | string | 页面标题,缺失时为空串 |
| `url` | string | 实际抓取的地址 |
| `chars` | int | 返回正文长度 |
| `truncated` | bool | 正文是否被截断 |
| `content` | string | Markdown 正文 |
| `markdown` | string | `# 标题 + 来源链接 + 正文` 的拼装结果 |
| `error` / `hint` / `status` | string / string / int | 失败时的错误信息、排查建议与原样状态码 |

## 三、工具内容(Python)

完整源码见本目录下 `deepseek_fetch.py`(同时内嵌于 `1.0.0/DeepSeek网页抓取.tool`)。

```python
def main(api_key, url, max_chars=8000):
# 1. 参数校验:api_key / url 必填
# 2. POST https://api.deepseek.com/v1/tools/fetch {"url": ...}
# headers: Authorization: Bearer <key>, Content-Type: application/json
# 3. 状态码分级:400/401/402/403/404/429 分别给出排查提示
# 4. 正文按 max_chars 截断,拼装 markdown
return {"success": True, "title": ..., "content": ..., "markdown": ...}
```

编写时遵守的三条 MaxKB 规范:

1. 入口函数名必须是 `main`,形参名与工具面板中的参数名逐字一致(MaxKB 以 `main(**keywords)` 调用)
2. `main` 必须位于代码末尾(MaxKB 取最后一个定义的对象作为入口)
3. 返回值需可 JSON 序列化;`print` 会被重定向到 devnull,不要依赖它调试

## 四、调用示例

### 4.1 与搜索工具串联(推荐编排)

```
开始(question)
→ DeepSeek 联网搜索(query={{question}}, count=5)
→ AI 对话(从搜索结果中选出最相关的 1~2 条 url)
→ DeepSeek 网页抓取(url={{AI选出的url}}, max_chars=6000)
→ AI 对话(基于正文生成带引用的回答)
```

### 4.2 直接调用(Python)

```python
from deepseek_fetch import main

r = main(api_key="sk-xxxxxxxx", url="https://example.com/article", max_chars=6000)
print(r["title"])
print(r["markdown"][:500])
```

### 4.3 典型返回

```json
{
"success": true,
"title": "Example article",
"url": "https://example.com/article",
"chars": 5210,
"truncated": false,
"content": "# Example article\n\nArticle content...",
"markdown": "# Example article\n\n来源:https://example.com/article\n\n# Example article..."
}
```

## 五、错误处理

| 现象 | 原因 | 处理 |
| :--- | :--- | :--- |
| `HTTP 400` | URL 不合法(无 host / 带账号密码 / 超 8192) | 检查 url 格式 |
| `HTTP 401` | Key 错误或失效 | 检查启动参数中的 `api_key` |
| `HTTP 402` | 账户余额不足 | 充值后重试 |
| `HTTP 403` | 被 DeepSeek 安全策略拒绝 | 目标站点不允许抓取,更换来源 |
| `HTTP 404` | 无法获取可用网页内容 | URL 失效/被站点拦截/无可提取正文,属正常业务失败 |
| `HTTP 429` | 超过 QPS 限制(账号级 5 QPS + 目标域名单独限速) | 稍后重试 |
| 请求异常 / 超时 | MaxKB 容器无出网权限 | 在容器内执行 `curl -I https://api.deepseek.com` 验证 |
| `ModuleNotFoundError: requests` | 精简镜像缺包 | 代码已内置 urllib 回退,通常无需安装 |

注意:`403`/`404` 多数情况是目标站点策略所致,属预期业务结果,编排中应把 `success=false` 视为"跳过该链接继续",而非流程报错。

## 六、计费与限速

- 计费:Fetch 按成功请求计费,**5 元 / 1000 次**;失败、超时不计费(必须返回网页正文才计费)
- 限速:账号级 **5 QPS**,另外对目标域名有独立 QPS 限制,超限同样返回 429

## 七、版本记录

| 版本 | 日期 | 说明 |
| :--- | :--- | :--- |
| 1.0.0 | 2026-10-19 | 首发:Fetch 接口封装,支持 max_chars 截断、markdown 拼装与分级错误处理 |
5 changes: 5 additions & 0 deletions tools/tool_deepseek_fetch/data.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: DeepSeek 网页抓取
tags:
- 联网搜索
title: 使用 DeepSeek Fetch API 抓取单个网页并转换为 Markdown 正文
description: 基于 DeepSeek 官方 Fetch API(https://api.deepseek.com/v1/tools/fetch)的网页抓取工具。输入 URL,返回页面标题与 Markdown 格式正文,支持控制正文字符上限;抓取失败时返回结构化错误与原因提示,不抛异常。与 DeepSeek 联网搜索工具配套使用,可完成"搜索 → 抓正文 → 生成回答"链路。
106 changes: 106 additions & 0 deletions tools/tool_deepseek_fetch/deepseek_fetch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
# =============================================================================
# MaxKB 脚本工具:DeepSeek 网页正文抓取(Fetch API)
# 接口文档:https://api-docs.deepseek.com/zh-cn/guides/search
# 规范同上:main() 必须放在最后,参数名与工具面板一致,返回 dict。
# =============================================================================
import json

FETCH_URL = "https://api.deepseek.com/v1/tools/fetch"
TIMEOUT = 40
MAX_CHARS = 8000

STATUS_HINT = {
400: "url 不合法(必须为含 host 的 http/https 地址,不带账号密码,长度 ≤ 8192)",
401: "API Key 缺失或无效",
402: "DeepSeek 账户余额不足",
403: "Fetch 被安全策略拒绝(目标站点不允许抓取)",
404: "未能获取可用网页内容(URL 失效/被站点拦截/无可提取正文)",
429: "触发 QPS 限速(Fetch 每账号 5 QPS,且按目标域名单独限速)",
}


def _http_post_json(url, headers, payload, timeout=TIMEOUT):
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
try:
import requests
except Exception:
requests = None

if requests is not None:
resp = requests.post(url, headers=headers, data=body, timeout=timeout)
return resp.status_code, resp.text

import urllib.request
import urllib.error

req = urllib.request.Request(url, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status, r.read().decode("utf-8", "replace")
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", "replace")


def main(api_key, url, max_chars=8000):
api_key = (api_key or "").strip()
url = (url or "").strip()

if not api_key:
return {"success": False, "error": "缺少 api_key", "title": "", "content": ""}
if not url:
return {"success": False, "error": "缺少 url", "title": "", "content": ""}

body = {"url": url[:8192]}
headers = {
"Authorization": "Bearer " + api_key,
"Content-Type": "application/json",
"Accept": "application/json",
}

try:
status, text = _http_post_json(FETCH_URL, headers, body)
except Exception as e:
return {
"success": False,
"error": "请求异常:%s" % e,
"hint": "检查 MaxKB 容器到 api.deepseek.com 的网络连通性",
"title": "",
"content": "",
}

try:
data = json.loads(text) if text else {}
except Exception:
return {"success": False, "status": status, "error": (text or "")[:300], "title": "", "content": ""}

if status != 200:
err = data.get("error") or {}
return {
"success": False,
"status": status,
"error": err.get("message") or ("HTTP %s" % status),
"hint": STATUS_HINT.get(status, "未预期状态码,请对照官方文档排查"),
"title": "",
"content": "",
}

title = data.get("title") or ""
content = data.get("content") or ""
try:
limit = int(float(str(max_chars)))
except Exception:
limit = MAX_CHARS
truncated = len(content) > limit
if truncated:
content = content[:limit] + "\n\n……(正文已截断)"

return {
"success": True,
"title": title,
"url": url,
"chars": len(content),
"truncated": truncated,
"content": content,
"markdown": ("# %s\n\n来源:%s\n\n%s" % (title or url, url, content)).strip(),
}
Binary file added tools/tool_deepseek_fetch/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading