From 56693f6ce48381374e39f9f4e9cd5dfd5676bd33 Mon Sep 17 00:00:00 2001 From: gzzwp <38277434+gzzwp@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:23:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20DeepSeek=20=E7=BD=91?= =?UTF-8?q?=E9=A1=B5=E6=8A=93=E5=8F=96=E5=B7=A5=E5=85=B7=EF=BC=88Fetch?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 与 #135 的联网搜索工具配套:Search 负责检索, Fetch 负责抓取单个网页并转换为 Markdown 正文。 - POST https://api.deepseek.com/v1/tools/fetch - max_chars 控制正文上限,超长自动截断并标注 - 403/404 等按业务失败处理(success=false + 中文 hint),不抛异常 - api_key 作为启动参数(密码框),与输入参数分离 --- ...\351\241\265\346\212\223\345\217\226.tool" | Bin 0 -> 4917 bytes tools/tool_deepseek_fetch/README.md | 121 ++++++++++++++++++ tools/tool_deepseek_fetch/data.yaml | 5 + tools/tool_deepseek_fetch/deepseek_fetch.py | 106 +++++++++++++++ tools/tool_deepseek_fetch/logo.png | Bin 0 -> 186 bytes 5 files changed, 232 insertions(+) create mode 100644 "tools/tool_deepseek_fetch/1.0.0/DeepSeek\347\275\221\351\241\265\346\212\223\345\217\226.tool" create mode 100644 tools/tool_deepseek_fetch/README.md create mode 100644 tools/tool_deepseek_fetch/data.yaml create mode 100644 tools/tool_deepseek_fetch/deepseek_fetch.py create mode 100644 tools/tool_deepseek_fetch/logo.png diff --git "a/tools/tool_deepseek_fetch/1.0.0/DeepSeek\347\275\221\351\241\265\346\212\223\345\217\226.tool" "b/tools/tool_deepseek_fetch/1.0.0/DeepSeek\347\275\221\351\241\265\346\212\223\345\217\226.tool" new file mode 100644 index 0000000000000000000000000000000000000000..f93ae43ab62b5ef5fecc8badb891097ec0db172e GIT binary patch literal 4917 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 VcGez&0}{+QuJhWF8rN=*{2STwT9g0) literal 0 HcmV?d00001 diff --git a/tools/tool_deepseek_fetch/README.md b/tools/tool_deepseek_fetch/README.md new file mode 100644 index 0000000..22cb80b --- /dev/null +++ b/tools/tool_deepseek_fetch/README.md @@ -0,0 +1,121 @@ +# DeepSeek 网页抓取工具 + +基于 DeepSeek 官方 Fetch API()封装的网页抓取工具,输入 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,在 获取 | + +### 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 , 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 拼装与分级错误处理 | diff --git a/tools/tool_deepseek_fetch/data.yaml b/tools/tool_deepseek_fetch/data.yaml new file mode 100644 index 0000000..3747583 --- /dev/null +++ b/tools/tool_deepseek_fetch/data.yaml @@ -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 联网搜索工具配套使用,可完成"搜索 → 抓正文 → 生成回答"链路。 diff --git a/tools/tool_deepseek_fetch/deepseek_fetch.py b/tools/tool_deepseek_fetch/deepseek_fetch.py new file mode 100644 index 0000000..85e3d47 --- /dev/null +++ b/tools/tool_deepseek_fetch/deepseek_fetch.py @@ -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(), + } diff --git a/tools/tool_deepseek_fetch/logo.png b/tools/tool_deepseek_fetch/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..982e0ce4fa062bbe35214cdecfe09f9205b17e5a GIT binary patch literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0B~Ajh-%!Ar-gY-Z;p4KtX^tP})cJ zTfM26f}WaIeW`lI*-sq8j+ZBQAD)_Uu!;4x{p-6Q4g_s{*4@bZ+T_}k_2NCQ(TtP( z&o0s}t1PR$6?G;{T40N5kw@XdtoV{EV(go8<$aEAV%Lg1<`WtHE#m3bj^Jy(;-0Ht j{d0NZB`&5DaW|APXOUT^a&a>=&@l|2u6{1-oD!M literal 0 HcmV?d00001