![]() 玄同 765 AI Engineer | ▍深耕领域 AI Agent 架构设计 Agent Harness 工程 Context Engineering 多智能体协同 & RAG |
| 「 让 AI 交互更智能,让技术落地更高效 」 | 欢迎技术探讨 / 项目合作,关注解锁大模型与智能交互的无限可能! | |
【DeepSeek Harness 零基础入门 #09】如何给 AI 装上“豆包画图”超能力?手把手开发原生工具与无水印生图插件
📌 一句话精华摘要:不想配付费 API Key,怎么让 Agent 一句话自动调用豆包网页版生成 2.7K 无水印高清配图?本文以开源插件 dsh-doubao-image 为例,详解原生 AI 工具注册与多模态图片回传实战。
📚 《DeepSeek Harness 零基础入门与插件开发实战》十篇连载导航
- #01:只调大模型 API 就够了?一文看懂 Agent“安全底盘”与积木式插件架构
- #02:改配置总怕升级被覆盖?揭秘三层配置不丢数据与本地插件挂载秘诀
- #03:对话发错指令怎么“时光倒流”?拆解聊天记账本与无损分支重发原理
- #04:长对话越聊越卡还爆窗口?揭秘大模型缓存加速与自动瘦身压缩原理
- #05:怎么防止 AI 乱删代码或卡死?拆解安全护栏、后台任务与多智能体组队
- #06:不改官方源码怎么改界面?一文搞懂前端“插槽接管”与防白屏保护
- #07:10 分钟写出第一个全栈插件!从新建文件夹到设置面板保姆级教程
- #08:AI 连续调工具闷头当哑巴?手把手写“自动提醒汇报进展”的拦截插件
- #09(本文):如何给 AI 装上“豆包画图”超能力?手把手开发原生工具与无水印生图插件
- #10:聊天气泡原位编辑与划词批注怎么做?UI 深度改造与开源上架全流程
想给 Agent 扩展外部能力,做成“原生 AI 工具”比“手动表单面板”好用十倍!本文以开源插件 dsh-doubao-image(豆包网页版 2.7K 无水印生图插件) 为实战蓝本,手把手教你使用 defineTool 注册原生工具、绕过前端风控下载无水印原图并在对话流原位渲染。
文章目录
一、为什么给 Agent 扩展能力必须首选「注册原生 AI 工具」?(原理拆解篇)
1.1 从一个直觉误区说起:给机械臂外挂手动遥控器 vs 为机械臂安装神经接口
很多初次开发 AI 客户端插件的工程师,在接到“给客户端增加 AI 生图或外部检索能力”的需求时,第一反应是在聊天输入框上方挂一个折叠表单:塞满提示词输入框、比例下拉框、风格单选框和一个「点击执行」按钮。
这种传统 GUI 思维在 Agentic Workflow(智能体自主工作流) 中存在三个致命短板:
- 打断多步自主编排(Breaking Autonomous Chains):当用户下达复合指令(如“帮我写一个 React 落地页,自动生成一张 16:9 赛博朋克主视觉图保存到
/Users/dev/workspace/public/hero.png并写入组件代码”)时,手动表单会迫使 Agent 停机等待人类点按钮;而注册为原生工具doubao_generate_image后,Agent 可在单轮对话中自主串联「生图 → 拿路径 → 写代码」全流程。 - 丧失大模型上下文感知 Prompt 扩写能力:人类手填词条往往只有寥寥数字;而大模型在调用
doubao_generate_image前,会自动结合项目上下文将其扩写为包含构图、光影、材质与景深的专业视觉描述。 - 打通多模态视觉自检闭环(Visual Self-Verification):通过
output.render回传{ type: 'image', attachment }内容块后,生成的图片不仅展示给用户,还会直接进入大模型的视觉上下文(如支持视觉输入的云端模型或本地http://127.0.0.1:8000/v1多模态服务),支持 Agent 自主检视成图效果。
1.2 DSH 四种外部能力扩展机制全景对比
| 扩展方案 | 核心挂载接口 | 多模态附件原生回传 (ContentBlock) | 调用卡片定制 (presentCall) | 复用宿主服务 (webServer / systemPrompt) | 典型适用场景与选型建议 |
|---|---|---|---|---|---|
| 1. 纯前端 Slot 表单插件 | ctx.slots.register('conversation.input.dock') | 不支持(仅能改写输入框草稿) | 无工具调用卡片 | 无法在宿主端注册工具 | 仅适合划词引用等纯前端交互增强 |
2. 独立脚本 + bash 调用 | 无(让 Agent 调内置 bash 跑 CLI) | 不支持(仅返回 stdout 纯文本) | 统一显示为普通 Bash 卡片 | 无法访问 Cordis ctx 与配置表单 | 适合临时一次性脚本,无强类型 Schema 约束 |
| 3. 外部 MCP Server | 标准 stdio / SSE MCP 协议 | 依赖协议转换层,大图传输开销高 | 通用 MCP 卡片,定制受限 | 进程隔离,无法直接挂载本机 Web 路由 | 适合跨 IDE 通用的标准化工具集 |
| 4. DSH 原生工具插件(本文方案) | ctx.tools.register(defineTool(...)) | 原生支持(直连 ctx.attachments.saveImage) | 完整支持(定制标题与输入摘要) | 同进程协同(联动 systemPrompt 与 webServer) | 深度集成首选:需多模态回传与本机路由的生产级插件 |
二、基础篇:深度拆解 @deepseek-ai/dsh-tools 的 defineTool 契约与跨包解析
2.1 defineTool 全字段契约解析
在 DSH 宿主端中,ctx.tools.register 接收由 @deepseek-ai/dsh-tools 的 defineTool 构造的工具定义对象,其 7 大核心字段契约如下:
| 字段路径 | 类型签名 | 必填 | 底层运行机制与工程注意事项 |
|---|---|---|---|
name | string | 是 | 工具唯一标识符(推荐 snake_case,如 doubao_generate_image),直接映射为 LLM Function Calling 名称 |
description | string | 是 | 发送给大模型的工具说明书,需明确描述触发条件、参数语义及 status: "login_required" 等状态的处理指引 |
parameters | Record<string, Spec> | JsonSchema | 是 | 入参定义。支持属性内简写 required: true,编译后生成顶层 { type: 'object', properties, required } 标准结构 |
timeoutMs | number | 否 | 单次工具执行最大超时毫秒数。网页生图建议设为 185000(185 秒),避免被默认短超时提前中断 |
presentCall | (args) => CallCardSpec | 否 | 控制聊天流中工具折叠卡片的 UI 展示,返回 { card: 'generic', title: '豆包网页生图', kind: 'other', rawInput } |
execute | async (args, exec) => any | 是 | 异步执行器。exec.signal 为 AbortSignal,当用户点击停止时触发中断,必须透传给子进程与 fetch |
output | { schema, render } | 否 | 多模态回传核心:schema 声明返回值结构,render(args, value) 将结果转为 ContentBlock[] 图文块数组 |
2.2 跨包解析深水区:ProfileRuntimeResolution.routeLinked 与 fallbackDefineTool
为什么在 link: 本地软链接或 ~/.dsh/profiles/desktop 模式下,插件内部执行 import('@deepseek-ai/dsh-tools') 经常报错 ERR_MODULE_NOT_FOUND?
原因在于 @deepseek-ai/dsh-tools 封装在 DSH 宿主的 app.asar 内部。当宿主通过 ProfileRuntimeResolution.routeLinked(specifier, parentURL) 解析软链接插件的依赖时,会调用 readPeerNames(directory) 读取插件 package.json 的 peerDependencies——只有显式声明在 peerDependencies 中的 @deepseek-ai/* 包,才会被允许路由到宿主内部依赖树!同时必须声明 "engines": { "dsh": ">=0.1.0-0" } 以通过 evaluatePluginCompatibility() 兼容性门禁。
此外,为了在脱离 DSH 宿主的 node --test 单元测试环境下也能零依赖运行,我们编写了 importHostPackage + fallbackDefineTool 双保险加载器(注意 ToolRuntime 的 assertSupportedJsonSchema() 严禁 output.schema 使用非标 type: 'json',必须规范化为 { type: 'object', additionalProperties: true }):
// 摘自 dsh-doubao-image/lib/index.js:宿主包动态寻址 + 单元测试无损降级编译器
import { createRequire } from 'node:module';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
async function importHostPackage(specifier) {
try {
return await import(specifier);
} catch {
const anchors = [
process.argv[1],
typeof process.resourcesPath === 'string'
? path.join(process.resourcesPath, 'app.asar', 'dsh', 'package.json')
: undefined,
].filter(Boolean);
for (const anchor of anchors) {
try {
return await import(pathToFileURL(createRequire(anchor).resolve(specifier)).href);
} catch {}
}
return null;
}
}
function fallbackDefineTool(options) {
const rawParams = options?.parameters || {};
const properties = {};
const required = [];
for (const [key, spec] of Object.entries(rawParams)) {
if (!spec || typeof spec !== 'object') continue;
const { required: isReq, ...rest } = spec;
properties[key] = { ...rest };
if (isReq === true) required.push(key);
}
const outSchema =
options?.output?.schema?.type === 'json'
? { type: 'object', additionalProperties: true }
: options?.output?.schema || { type: 'object', additionalProperties: true };
return {
...options,
parameters: { type: 'object', properties, ...(required.length > 0 ? { required } : {}) },
output: { ...options?.output, schema: outSchema },
};
}
const dshToolsMod = await importHostPackage('@deepseek-ai/dsh-tools');
const defineTool = typeof dshToolsMod?.defineTool === 'function' ? dshToolsMod.defineTool : fallbackDefineTool;
2.3 核心代码实战一:工具 Schema 编译器与跨包门禁校验器(Python 完整实现)
下面用 Python 完整实现一套 DSH 工具契约编译器与 package.json 跨包门禁静态审计器(共 56 行,可通过 ast.parse 校验):
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, List, Set, Tuple
@dataclass
class ToolContractCompiler:
"""DSH 原生工具 Schema 编译器与 ProfileRuntimeResolution 门禁校验器。"""
allowed_types: Set[str] = field(
default_factory=lambda: {"object", "array", "string", "number", "integer", "boolean", "null"}
)
required_peers: Set[str] = field(
default_factory=lambda: {"@deepseek-ai/cordis", "@deepseek-ai/dsh-tools", "@deepseek-ai/schemastery"}
)
def compile_shorthand_tool(self, raw_def: Dict[str, Any]) -> Dict[str, Any]:
"""将 defineTool 简写参数字典编译为符合 ToolRuntime 规范的标准 JSON Schema。"""
raw_params = raw_def.get("parameters") or {}
if raw_params.get("type") == "object" and isinstance(raw_params.get("properties"), dict):
compiled_params = dict(raw_params)
else:
props: Dict[str, Dict[str, Any]] = {}
req_list: List[str] = []
for key, spec in raw_params.items():
if not isinstance(spec, dict):
continue
props[key] = {k: v for k, v in spec.items() if k != "required"}
if spec.get("required") is True:
req_list.append(key)
compiled_params = {"type": "object", "properties": props}
if req_list:
compiled_params["required"] = req_list
raw_out = dict(raw_def.get("output") or {})
out_schema = dict(raw_out.get("schema") or {"type": "object", "additionalProperties": True})
if out_schema.get("type") == "json":
out_schema = {"type": "object", "additionalProperties": True}
raw_out["schema"] = out_schema
return {**raw_def, "parameters": compiled_params, "output": raw_out}
def audit_manifest_and_tool(self, pkg_json: Dict[str, Any], tool_def: Dict[str, Any]) -> Tuple[bool, List[str]]:
"""联合校验 package.json 跨包路由声明与编译后的工具 Schema。"""
errors: List[str] = []
if not str((pkg_json.get("engines") or {}).get("dsh", "")).strip():
errors.append("MISSING_ENGINE_DSH: package.json 必须声明 engines.dsh")
peers = set((pkg_json.get("peerDependencies") or {}).keys())
for missing in sorted(self.required_peers - peers):
errors.append(f"MISSING_PEER_DEP: 缺少 {missing},link 模式下 routeLinked 将拒绝解析")
params = tool_def.get("parameters") or {}
if params.get("type") != "object" or not isinstance(params.get("properties"), dict):
errors.append("INVALID_PARAMETERS_ROOT: parameters 根节点必须为 type='object'")
out_type = ((tool_def.get("output") or {}).get("schema") or {}).get("type")
if out_type not in self.allowed_types:
errors.append(f"INVALID_OUTPUT_SCHEMA: output.schema.type={out_type!r} 不受支持")
return (len(errors) == 0, errors)
三、进阶篇:多候选图片 URL 质量评分去重与混合下载引擎
3.1 豆包网页版「一图三轨」URL 结构与 cgen_wm0 无水印优选
当豆包网页版(doubao.com/chat/)生成图片时,DOM 中针对同一张图会挂载共享同一个 /rc_gen_image/<32位哈希> 的三类 URL:
| 候选 URL 轨名称 | URL 后缀特征 | 真实解码尺寸 (naturalWidth × Height) | 页面 CSS 尺寸 (clientWidth) | 水印状态 | 质量评分 (qualityScore) 与策略 |
|---|---|---|---|---|---|
| 渐进模糊缩略图 | -cthumb_lwm3.png | 192 × 108 | 425 × 239 | 带水印且模糊 | 0 分(直接剔除):命中 -cthumb 立即过滤 |
| 标准对话展示图 | -cgen_lwm3.png | 2730 × 1536 | 425 × 239 | 右下角带水印 | 1 分(保底候选):仅在未出现 wm0 时兜底 |
| 画布无水印原图(WebP) | -cgen_wm0.webp | 2730 × 1536 | 422 × 237 或 18 × 18 | 零水印纯净版 | 2 分(高优候选):2.7K 纯净无水印 WebP |
| 画布无水印母版(PNG) | -cgen_wm0.png | 2730 × 1536 | 422 × 237 或 18 × 18 | 零水印纯净版 | 3 分(最高优先级):同哈希下无条件胜出 |
关键工程防坑:在画布缩略栏中,cgen_wm0 节点的 CSS 宽度 clientWidth 可能仅有 18px,但真实像素 naturalWidth 高达 2730px!因此过滤规则必须对含 /rc_gen_image/ 的 URL 豁免 clientWidth < 120 限制,统一按 naturalWidth >= 360 判定。此外,豆包前端 bdms-sdk.js 重写了页面内的 window.fetch(直接在 page.evaluate 内请求 byteimg.com 必报 Failed to fetch),因此我们采用 「浏览器内提取签名 URL + Node.js 宿主携带 Referer: https://www.doubao.com/ 直连下载」 的混合架构。
3.2 核心代码实战二:多候选图片 URL 质量评分去重器(Python 完整实现)
下面给出「基于 /rc_gen_image/<hash> 唯一键的图像候选去重与 wm0 评分优选器」完整 Python 代码(共 54 行,已通过 ast.parse 语法校验):
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Dict, Iterable, List
@dataclass(frozen=True)
class RawImgNode:
"""浏览器页面单次轮询提取的 <img> 节点快照。"""
src: str
natural_width: int
natural_height: int
client_width: int
client_height: int
complete: bool = True
class WatermarkedUrlDeduplicator:
"""基于 /rc_gen_image/<hash> 唯一键对候选图像去重,并按 wm0 > lwm3 质量分优选。"""
HASH_REGEX = re.compile(r"/rc_gen_image/([^.~/?#]+)", re.IGNORECASE)
NOISE_MARKERS = ("-cthumb", "/rc/icon/", "/static/image/", "avatar", "qrcode", "favicon")
@classmethod
def resolve_content_key(cls, src_url: str) -> str:
"""提取豆包生成图的哈希内容键,确保同图的不同水印后缀归入同一主键。"""
cleaned = (src_url or "").strip()
matched = cls.HASH_REGEX.search(cleaned)
return f"rc_gen_image:{matched.group(1).lower()}" if matched else cleaned.split("?", 1)[0]
@classmethod
def score_and_deduplicate(
cls, candidates: Iterable[RawImgNode], baseline_urls: Iterable[str] = ()
) -> List[Dict[str, object]]:
"""过滤历史基准图、模糊缩略图与小图标,同哈希下自动保留最高分 wm0 无水印版本。"""
base_set = {u.strip() for u in baseline_urls if u and u.strip()}
base_keys = {cls.resolve_content_key(u) for u in base_set}
winners: Dict[str, Dict[str, object]] = {}
for node in candidates:
src = (node.src or "").strip()
lower = src.lower()
if not src.startswith(("http://", "https://")) or src in base_set:
continue
if any(m in lower for m in cls.NOISE_MARKERS) or lower.endswith(".svg"):
continue
if "/rc_gen_image/" not in lower and (node.client_width < 120 or node.client_height < 120):
continue
if not node.complete or node.natural_width < 360 or node.natural_height < 360:
continue
key = cls.resolve_content_key(src)
if key in base_keys:
continue
score = 3 if "wm0.png" in lower else (2 if "wm0" in lower else 1)
prev = winners.get(key)
if prev is None or int(prev["quality_score"]) < score:
winners[key] = {
"key": key, "src": src, "width": node.natural_width,
"height": node.natural_height, "watermark_free": score >= 2, "quality_score": score,
}
return list(winners.values())
四、高阶技巧:多模态 ContentBlock 回传与本机 WebServer 路由扩展
4.1 ctx.get('attachments').saveImage 与 ctx.webServer.register 完整实战
在宿主端 apply(ctx, config) 中,为了同时支撑 Agent 原生工具多模态回传与前端设置页画廊预览,我们打通了以下三大机制:
- 多模态附件持久化与渲染:图片下载落盘后,调用
ctx.get('attachments').saveImage({ data: new Uint8Array(buffer), mediaType })获取attachmentRef,并在output.render中返回[{ type: 'text', text: summaryJson }, { type: 'image', attachment: attachmentRef }]。 - Cordis
ctxProxy 安全访问规范:顶层仅声明export const inject = ['tools']时,切勿直接读取ctx.locale或ctx.webServer(会触发 Proxy 异常cannot get property without inject),必须使用ctx.get('locale')或ctx.inject(['webServer'], fn)子 Fiber。 - 本机 HTTP 路由扩展:通过
webCtx.webServer.register({ kind: 'exact', path: '/doubao-image/file', handler })暴露本地原图流式预览接口,并强制校验path.isAbsolute与图片扩展名白名单。
// 摘自 dsh-doubao-image/lib/index.js:多模态渲染 + 原生工具注册 + 本机 Web 路由扩展
export function renderDoubaoToolOutput(value) {
if (!value || typeof value !== 'object') return [{ type: 'text', text: String(value ?? '') }];
const cleanImages = (value.images || []).map(({ attachment, ...rest }) => ({
...rest,
attachedToChat: Boolean(attachment),
}));
const blocks = [{ type: 'text', text: JSON.stringify({ ...value, images: cleanImages }, null, 2) }];
for (const img of value.images || []) {
if (img?.attachment) blocks.push({ type: 'image', attachment: img.attachment });
}
return blocks;
}
export function apply(ctx, config) {
const initial = normalizeConfig(config);
if (!initial.enabled) return;
const history = [];
// 1. 注册原生 AI 工具:doubao_generate_image
const registerTools = (toolCtx) => {
toolCtx.tools.register(
defineTool({
name: 'doubao_generate_image',
description: 'Generate watermark-free 2.7K AI images via Doubao Web in Tabbit.',
parameters: {
prompt: { type: 'string', required: true, description: 'Visual prompt.' },
aspectRatio: { type: 'string', enum: [...SUPPORTED_ASPECT_RATIOS] },
style: { type: 'string', description: 'Style preset id.' },
},
output: {
schema: { type: 'object', additionalProperties: true },
render: (_args, val) => renderDoubaoToolOutput(val),
},
timeoutMs: 185000,
presentCall: (args) => ({
card: 'generic',
title: '豆包网页生图 (Doubao Image)',
kind: 'other',
rawInput: `${args.prompt || ''} [${args.aspectRatio || '16:9'}]`,
}),
async execute(args, exec) {
return await executeDoubaoAction(
{ mode: 'generate', ...args, config },
{ ctx, signal: exec?.signal, history },
);
},
}),
);
};
if (ctx.tools?.register) registerTools(ctx);
else if (typeof ctx.inject === 'function') ctx.inject(['tools'], registerTools);
// 2. 子 Fiber 扩展本机 WebServer 路由(本地原图安全预览)
if (typeof ctx.inject === 'function') {
ctx.inject(['webServer'], (webCtx) => {
webCtx.webServer.register({
kind: 'exact',
path: '/doubao-image/file',
handler: async (req, res) => {
const u = new URL(req.url || '/doubao-image/file', 'http://127.0.0.1');
const fp = u.searchParams.get('path') || '';
const mime = IMAGE_MEDIA_BY_EXT[path.extname(fp).slice(1).toLowerCase()];
if (!fp || !path.isAbsolute(fp) || !mime) return sendJson(res, 400, { ok: false });
const buf = await readFile(fp);
res.writeHead(200, { 'content-type': mime, 'cache-control': 'private, max-age=3600' });
res.end(buf);
},
});
});
}
}
4.2 核心代码实战三:混合下载引擎与多模态附件回传组装器(Python 完整实现)
下面给出「携带 Referer 直连下载、注册附件句柄并组装多模态 ContentBlock 数组」的完整 Python 实现(共 56 行,已通过 ast.parse 语法校验):
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple
from urllib.request import Request, urlopen
class HybridImageDownloaderAndRenderer:
"""宿主端带 Referer 直连下载无水印原图,并组装 DSH 多模态 ContentBlock 列表。"""
MEDIA_TO_EXT: Dict[str, str] = {"image/png": "png", "image/jpeg": "jpg", "image/webp": "webp"}
def __init__(
self,
workspace_dir: Path,
attachment_saver: Optional[Callable[[bytes, str], Dict[str, Any]]] = None,
) -> None:
self.workspace_dir = workspace_dir
self.attachment_saver = attachment_saver
@staticmethod
def sanitize_prefix(raw_prefix: str = "doubao") -> str:
"""清洗文件名前缀,防止路径穿越或非法字符。"""
cleaned = re.sub(r"[^a-zA-Z0-9_-]+", "_", (raw_prefix or "doubao")).strip("_")[:40]
return cleaned or "doubao"
def fetch_with_doubao_referer(self, source_url: str, timeout_sec: float = 30.0) -> Tuple[bytes, str]:
"""在宿主进程携带豆包 Referer 直连拉取 byteimg 签名原图,绕过页面 bdms-sdk 拦截。"""
req = Request(
source_url,
headers={"Referer": "https://www.doubao.com/", "User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/132.0"},
)
with urlopen(req, timeout=timeout_sec) as resp:
mime = (resp.headers.get("Content-Type") or "image/png").split(";")[0].strip().lower()
return resp.read(), mime
def save_and_render_blocks(
self, candidates: List[Dict[str, Any]], prefix: str = "doubao"
) -> List[Dict[str, Any]]:
"""将候选图片下载落盘、注册附件句柄,并生成 DSH [TextBlock, ImageBlock...] 数组。"""
out_dir = self.workspace_dir / "doubao_images"
out_dir.mkdir(parents=True, exist_ok=True)
safe_prefix = self.sanitize_prefix(prefix)
saved_records: List[Dict[str, Any]] = []
image_blocks: List[Dict[str, Any]] = []
for idx, item in enumerate(candidates, start=1):
raw_bytes, mime = self.fetch_with_doubao_referer(str(item["src"]))
file_path = out_dir / f"{safe_prefix}_{idx}.{self.MEDIA_TO_EXT.get(mime, 'png')}"
file_path.write_bytes(raw_bytes)
att_ref = self.attachment_saver(raw_bytes, mime) if self.attachment_saver else None
saved_records.append({"index": idx, "filePath": str(file_path), "attachedToChat": bool(att_ref)})
if att_ref:
image_blocks.append({"type": "image", "attachment": att_ref})
return [{"type": "text", "text": json.dumps({"images": saved_records}, ensure_ascii=False)}, *image_blocks]
五、实战避坑指南:原生 Agent 工具插件开发高频报错与一键解决速查表
| 序号 | 遇到的报错或异常现象 | 背后的真正底层原因 | 一句话解决办法 |
|---|---|---|---|
| 01 | Error: Cannot find package '@deepseek-ai/dsh-tools' | link: 插件未在 package.json 声明 peerDependencies,被 ProfileRuntimeResolution.routeLinked() 拦截 | 在 package.json 声明 "peerDependencies": { "@deepseek-ai/cordis": "*", "@deepseek-ai/dsh-tools": "*", "@deepseek-ai/schemastery": "*" } 与 "engines": { "dsh": ">=0.1.0-0" } |
| 02 | 插件状态为 fiberPhase: "failed",报 cannot get property "locale" without inject | 在仅声明 inject = ['tools'] 的插件里直接访问 ctx.locale 或 ctx.webServer,触发 Cordis Proxy 异常 | 改用 ctx.get('locale')、ctx.get('attachments'),或使用 ctx.inject(['webServer'], fn) 派生子 Fiber |
| 03 | 注册工具时报错 Unsupported JSON Schema | output.schema 误写了非标 { type: 'json' },或属性内 required: true 未提升为顶层 required 数组 | 将 output.schema 设为 { type: 'object', additionalProperties: true },确保 parameters 根节点为 type: 'object' |
| 04 | page.evaluate 内拉取图片必报 TypeError: Failed to fetch (bdms-sdk.js) | 豆包网页前端安全 SDK 重写了页面的 window.fetch,阻断了跨子域编程式请求 | 仅从 DOM 提取签名 img.src,回到 Node.js 宿主进程携带 Referer: https://www.doubao.com/ 直连下载 |
| 05 | 豆包生图单次耗时高达 140 秒甚至超时 | 豆包网页停留在慢速「豆包·工作(云电脑)」模式,每次生图均需启动云端画布容器 | 发送指令前检测 [data-testid="create_office_task_button"],自动点击顶栏 [data-testid="workspace-switch-chip"] 切回极速「豆包」模式 |
| 06 | 画布模式下漏抓 cgen_wm0.png 无水印原图 | 缩略栏中 cgen_wm0 对应 <img> 的 clientWidth 仅为 18px,被 clientWidth < 120 误杀 | 对 URL 含 /rc_gen_image/ 的节点豁免 clientWidth 检查,统一按真实分辨率 naturalWidth >= 360 过滤 |
结语与下一篇收官预告
在本篇(连载第 09 篇)中,我们以开源插件 dsh-doubao-image 为实战载体,完整拆解了 DeepSeek Harness 原生 Agent 工具插件的研发全链路:从 @deepseek-ai/dsh-tools 的 defineTool 契约、ProfileRuntimeResolution.routeLinked 的 peerDependencies 跨包门禁,到 /rc_gen_image/<hash> 无水印 2.7K 原图优选算法,再到 ctx.attachments.saveImage 多模态回传与 ctx.webServer.register 本机路由扩展。
在下一篇收官之作——第 10 篇《【DeepSeek Harness 架构与插件实战 #10】客户端 UI 深度定制与开源发布全链路:从气泡内联交互到官方插件市场 PR 收录》 中,我们将手把手拆解如何接管 conversation.chat.node 实现用户消息原位编辑与分支重发(dsh-message-edit)、如何实现划词引用与行内批注胶囊(dsh-selection-quote),并完整奉上从 node --test 单元测试到 awesome-dsh-plugin 官方市场 PR 收录的 6 步端到端 SOP!
本文以dsh-doubao-image插件为例,详解如何在DeepSeek Harness中注册原生AI工具,实现豆包网页版无水印2.7K高清图片生成与多模态回传。涵盖工具Schema契约定义、跨包依赖门禁校验、多候选URL质量评分去重、混合下载引擎及本机WebServer路由扩展等核心技术,聚焦Agent能力扩展的工程化实践。





929

被折叠的 条评论
为什么被折叠?



