xtt-xt/dsh-custom-tool-studio

xtt-xt/dsh-custom-tool-studio

在网页或对话里编写 Python 工具,保存后免重启注册成 DSH 工具;危险工具每次执行都走内置审批,工具可分组成包,还可选「解锁模式」按对话逐步放出。需 PATH 中有 Python 3。

ツールと機能cordiscustom-toolsdsh

DSH 自定义工具工作室(dsh-custom-tool-studio)

在 DeepSeek Harness(DSH)里用 Python 写工具,保存即生效。

两条入口都能用:

  • 网页:左侧导航「自定义工具」→ 新建 / 编辑 / 测试 / 删除 / 启停;
  • 对话:直接让模型调用内置的 custom_tool_manage 工具。

保存后的工具会立刻注册成真正的 DSH 工具(不需要重启 DSH),模型下一轮就能调用。所有数据只存在本机 %DSH_HOME%\custom-tools,不会上传。

目录

  1. 这个插件做什么
  2. 界面入口
  3. Python 工具约定
  4. 危险工具与内置审批 / 自动审查
  5. 工具包(把几个工具放在一起)
  6. AI 侧工具 custom_tool_manage
  7. 数据存放位置
  8. 安装 / 验证 / 卸载
  9. 一键恢复脚本
  10. 故障排查
  11. 文件清单

1. 这个插件做什么

能力 · 说明 创建工具 · 起一个模型可见的工具名,写描述、参数 JSON Schema、Python 代码 即时注册 · 保存/启用/改名/删除后,host 立即对账并注册或注销,无需重启 DSH 试运行 · 页面「测试」或 AI 的 custom_tool_manage → action="test",直接传参数跑一次 危险工具 · 标记后每次执行前都走 DSH 内置审批,和内置的自动审查兼容 工具包 · 把几个工具归到一个有名称、有描述的分组里,一键启停整包;工具仍各自注册 AI 主动解锁 · 开启后每个对话只看到管理/解锁两个工具,AI 解锁工具包后才出现包内工具;支持「始终解锁」与未分组工具单独解锁 统一存储 · 所有工具存在一个 tools.json,可整体备份、迁移、删除 一键恢复 · restore.ps1 把插件从 profile 里干净卸载,可选清空工具数据

不是 Skill、不是 MCP:工具是同进程注册的真正 DSH 工具,执行时由 host 启动本机 Python 进程运行你的脚本。

2. 界面入口

安装并重启 DSH 后,左侧导航栏会出现「自定义工具」(与「插件」「自动化任务」同一排)。点进去是工具列表页:

  • 每行显示工具名(等宽字体)、标题、一句话描述,以及 Python、危险、已禁用 徽标;
  • 列表按工具包分组:每个包一张卡片,卡片头是包名、包描述、N 个工具 · 启用 M,右侧是整包开关和 编辑/删除;没有归包的工具放在最后一张「未分组工具」卡片里;
  • 每行右侧:测试、编辑、删除,以及该工具自己的启用/停用开关;
  • 顶部按钮:设置、刷新、新建工具包、新建工具。新建工具 打开编辑对话框:名称、标题、描述、所属工具包、参数 JSON Schema、Python 代码、危险工具开关、启用开关;
  • 没有工具也没有工具包时显示空状态和 def run(args): ... 约定;
  • 如果 host 探测不到 Python,页面顶部会显示「未检测到 Python,创建的工具无法执行」的警告条,并带上具体原因。

「测试」对话框里填 args JSON(就是传给 run(args) 的对象),点「运行」后显示成功/失败、结果、stdout、stderr 和耗时。测试失败不会弄坏已保存的工具。

3. Python 工具约定

3.1 最小例子

def run(args):
    text = args["text"]
    return {
        "chars": len(text),
        "upper": text.upper(),
    }
  • 脚本必须定义 `def run(args):`(页面和 AI 侧都会校验代码里有没有这个入口);
  • args 是模型按参数 JSON Schema 传进来的参数对象(普通 Python dict);
  • 返回值必须是可 JSON 序列化的值(dict / list / str / 数字 / bool / None)。host 会把它 json.dumps(..., ensure_ascii=False) 后作为工具结果给模型看;
  • 返回值是结果通道,print() 是日志通道,两者互不干扰:
  • print() / sys.stdout 的内容作为 stdout 展示(最多保留末尾 8,000 字符);
  • 脚本写到 sys.stderr 的内容进 stderr;抛出的异常被抓成 error(含 traceback),不会混进 stderr;
  • 结果 JSON 超过 20,000 字符会被截断。
  • 脚本在独立 Python 进程里执行,工作目录是 DSH 进程的当前目录;可以用 Python 标准库,第三方库需要装在那个解释器里;
  • 默认超时 60 秒,超时或取消会终止进程。可用环境变量 DSH_CUSTOM_TOOL_TIMEOUT_MS 调整(毫秒),改完重启 DSH;
  • 每次执行都会在 runs 下建一个临时目录(tool.py、args.json、result.json、stdout.txt、stderr.txt),只保留最新 30 个;
  • 解释器探测顺序:DSH_CUSTOM_TOOL_PYTHON(可指定完整路径)→ python → python3 → py -3。

3.2 参数 JSON Schema 怎么填

参数根节点必须是 JSON 对象。常用写法:

{
  "type": "object",
  "properties": {
    "text": { "type": "string", "description": "要统计的文本" },
    "limit": { "type": "integer", "description": "最多返回几行" }
  },
  "required": ["text"]
}
  • description 写给模型看,说明每个参数的含义;
  • required 里列出必填参数;
  • 没有参数就填 {"type": "object", "properties": {}};
  • 非法 JSON 或非对象会被 host 容忍并退化成「无参数」,但那样模型就传不进参数了——以页面「测试」的结果为准。

3.3 名称与描述

  • 工具名规则:^[a-z][a-z0-9_]{1,47}$(小写字母开头,只含小写字母/数字/下划线,长度 2–48);
  • run_code 是 DSH 保留名,不能使用;
  • 不能和内置工具、其它插件已经注册的工具重名(例如 read_file),冲突时保存会报「已被现有工具占用」;
  • description 是模型选工具的主要依据,写清「做什么 / 什么时候用 / 返回什么」;
  • 标题(title)只给人看,可以写中文;不填时默认等于工具名。

4. 危险工具与内置审批 / 自动审查

  • 编辑对话框里的「危险工具」开关,或 AI 侧 dangerous: true,会把工具标记为危险;
  • 标为危险后,模型每一次调用它都会先产生一个 ask 审批决定,由 DSH 内置审批界面问你「是否运行危险自定义工具 xxx?」,点拒绝就不会执行;AI 通过 custom_tool_manage 的 action="test" 试跑危险工具同样要审批;
  • 页面上的「测试」按钮是你本人直接点的一次运行,直接执行、不弹审批(否则等于自己审批自己);
  • 这与 desktop profile 默认加载的内置自动审查(@deepseek-ai/dsh-experimental-auto-review)兼容:自动审查策略接管 ask 的时候同样按它的判断走,不需要额外配置;
  • 非危险工具不会弹审批;
  • 建议把会写文件、删文件、执行系统命令、访问网络、读取敏感目录的工具都标成危险。

4.1 管理操作审批(顶部「设置」)

除「危险工具执行」之外,还可以让 AI 的管理操作也要你确认:

开关 · 默认 · 作用 创建工具需要审批 · 关 · AI 用 custom_tool_manage 的 create 新建工具时先弹审批 修改工具需要审批 · 关 · AI 用 update 改名称/描述/参数/代码/危险与启用状态时先弹审批 删除工具需要审批 · 开 · AI 用 delete 删除工具时先弹审批(删除不可恢复,所以默认开)

工具包的操作复用同样三个开关:

AI action · 走哪个开关 pack_create · 创建工具需要审批 pack_update(改描述/改成员/改名) · 修改工具需要审批 pack_delete · 删除工具需要审批 pack_set_enabled · 不拦(和 set_enabled 一样,只是批量改启用状态)

  • 三个开关存在 %DSH_HOME%\custom-tools\settings.json,点「保存设置」写入;
  • 只拦截 AI 侧 的 custom_tool_manage;你在页面上点按钮 / 用表单操作不会被自己拦;
  • test(试跑)不在三个开关里:它按「危险工具」规则单独判定;
  • 「危险工具执行审批」不受这里影响,始终生效。
危险开关只是「每次都问」,不是沙箱。脚本仍以当前用户权限运行,写代码时要自己注意边界。

5. 工具包(把几个工具放在一起)

工具包(pack)是分组,不是新的工具类型:

  • 一个包有名称(1–48 字符,可写中文)和描述;
  • 包里的工具仍然是各自独立的 DSH 工具,注册方式、工具名、参数、代码、危险标记全部不变;
  • 包只做两件事:把工具在页面里归到一起、以及一键启停(等价于逐个改它们的启用标记)。

5.1 在页面里用

  1. 点顶部 新建工具包,填名称和描述,在「包里的工具」里勾选要放进去的工具;
  2. 保存后列表里出现这个工具包的卡片,勾选的工具显示在它下面;
  3. 卡片头的开关一键启用/停用整包:全部启用时开、全部停用时空、部分启用时显示「部分启用」并可一键全部打开;
  4. 工具要换包:编辑工具 → 「所属工具包」下拉选另一个包(或选「不放入工具包」);
  5. 删除工具包只是取消分组:包里的工具全部保留,变回「未分组工具」。
包和工具是「一个工具最多属于一个包」的关系(成员关系记在工具记录上),所以改包名不会影响任何工具。

5.2 什么时候用

  • 工具多起来以后按用途分类:文本处理、数据处理、运维脚本……;
  • 调试某批工具时整包停掉,避免它们出现在模型的工具列表里;需要时整包打开。

5.3 AI 主动解锁模式(deferred tool loading)

通用叫法是 deferred tool loading(工具延迟加载)/ progressive tool disclosure(渐进式工具披露);Anthropic 侧叫 Tool Search / defer_loading,OpenAI Responses 侧叫 deferred tools。插件用的是 DSH 内核自带的「会话中途增加工具」通道:解锁只改变当前 agent 的可见性,不重新注册工具。

开启方式:页面顶部「设置」→ 打开「AI 主动解锁模式」。

开启后:

  1. 每个对话初始只看到两个自定义工具:
  • custom_tool_manage(管理工具/工具包)
  • custom_tool_unlock(解锁工具包)
  1. 包里的工具默认不可见、不可调用;AI 调 custom_tool_unlock 的 action="unlock"、pack="包名" 后,包内启用的工具在下一轮请求出现。
  2. 不同对话单独解锁:解锁状态按 session id 记在 unlocks.json;新对话 / 子代理不会继承。DSH 重启或插件重挂载后会按文件恢复。
  3. 始终解锁:编辑工具包 → 打开「始终解锁(所有对话可见)」。这个包在任何对话里都无需解锁,适合常用工具。也可以让 AI 用 custom_tool_manage 的 pack_update 传 alwaysUnlocked=true。
  4. 未分组工具:编辑工具 → 把它放进某个工具包是推荐做法。若确实要保留未分组,可在「设置」里打开「允许单独解锁未分组工具」,然后 AI 用 custom_tool_unlock 的 tool="工具名" 单独解锁;关闭这个开关时,未分组工具不能单独解锁,只能放进工具包。
  5. 关闭解锁模式:所有启用工具立即恢复可见;unlocks.json 里的记录保留,下次开启继续生效。
  6. 工具包清单会注入上下文:插件注册了一段动态运行上下文(ctx.systemPrompt.context),把当前工具包、成员名与未分组工具写进每个模型步骤的提示里,AI 不用先调 action="list" 就知道有什么可解锁。文本每次组装时重建,改包/改开关下一轮即生效;解锁模式关闭时不注入,避免无意义 token。

注意:

  • 这是可见性/上下文管理,不是权限边界。危险工具即使被解锁,每次执行仍然走 DSH 内置审批。
  • 解锁/锁定不会改变工具的启用状态;要停用整包请用工具包开关或 pack_set_enabled。
  • 可见工具集变化会让该对话的提示前缀缓存失效一次,这是解锁的固有成本。
  • 插件重挂载(bundle 禁用→启用)时,旧掩码会先释放、再按 unlocks.json 重建,不会出现「解锁了却仍不可见」的叠加。

6. AI 侧工具 custom_tool_manage

模型可以直接调用 custom_tool_manage 完成全部管理操作,和网页共用同一套逻辑。

6.1 action 列表

action · 作用 · 必填参数 list · 列出所有自定义工具(含启用/危险状态,以及工具包名) · – get · 查看某个工具的完整定义(含 Python 代码、所属工具包) · name create · 新建并注册一个工具 · name、description、code update · 修改已有工具(只改传入的字段) · name + 至少一个字段 delete · 删除工具(同时从 DSH 注销) · name set_enabled · 启用 / 停用 · name、enabled test · 用给定参数试运行一次 · name(args 可选) pack_list · 列出工具包及各自成员 · – pack_get · 查看一个工具包的成员与描述 · pack(或 name) pack_create · 新建工具包,可同时指定成员 · name(description/tools/alwaysUnlocked 可选) pack_update · 改包描述 / 改成员 / 改名 / 改始终解锁 · pack + 至少一项(description/tools/alwaysUnlocked/name) pack_delete · 删除工具包(工具保留,变未分组) · pack pack_set_enabled · 一键启停整包 · pack、enabled

6.2 参数

参数 · 类型 · 说明 action · string · 上面 13 个之一(必填) name · string · 工具名;create 必填,其余工具类 action 用它定位工具。pack_create 时是包名,pack_update 时是包的新名字 title · string · 给人看的标题,可空 description · string · 给模型看的说明;create 必填。工具包类 action 里是包的描述 parameters · object · 参数 JSON Schema 对象;省略则无参数 code · string · Python 源码,必须含 def run(args):;create 必填 dangerous · boolean · 是否危险(每次执行需审批),默认 false enabled · boolean · 是否启用,默认 true;停用的工具不注册 pack · string · 包名或包 id:pack_get/update/delete/set_enabled 用它定位包;create/update 用它把这个工具放进该包,传空字符串表示移出 tools · string[] · 包成员(工具名数组):pack_create 可选;pack_update 给了就整体替换成员(没列出的工具移出该包但保留) alwaysUnlocked · boolean · 工具包是否「始终解锁」:true 时任何对话都无需 custom_tool_unlock 即可见。仅在解锁模式开启时有意义 args · object · 仅 test 使用:传给 run(args) 的参数对象

6.3 对话示例

帮我新建一个自定义工具 python_word_count,统计文本字符数。
参数:text(字符串,必填)。代码用 def run(args): return {"chars": len(args["text"])}

模型会调用 custom_tool_manage:action="create",然后建议 action="test" 验证。

也可以直接说:

列出我现在所有的自定义工具,并测试 python_word_count,参数 text="hello"

工具包相关:

把 python_word_count 和 python_clean_text 放进一个叫「文本处理」的工具包,
描述写「处理文本统计与清洗」。然后把整个包停用。

模型会依次调用 pack_create(name="文本处理", description=..., tools=["python_word_count","python_clean_text"])和 pack_set_enabled(pack="文本处理", enabled=false)。

6.4 解锁工具 custom_tool_unlock

只在开启「AI 主动解锁模式」后有意义。参数:

参数 · 类型 · 说明 action · string · list / unlock / lock(必填) pack · string · 工具包名称或 id;与 tool 二选一 tool · string · 未分组工具名;与 pack 二选一,需要设置开启「允许单独解锁未分组工具」

action 行为:

action · 作用 list · 列出所有工具包、未分组工具及各自状态(未解锁 / 当前对话已解锁 / 始终解锁 / 已停用),并说明当前设置 unlock · 解锁整个包(pack=...)或一个未分组工具(tool=...);下一轮请求生效 lock · 反向收起;alwaysUnlocked 的包不能单独锁定,要去页面关闭「始终解锁」

示例:

我需要用 mcdev-docs 里的工具查文档。

模型会先 custom_tool_unlock action="list",再 action="unlock"、pack="mcdev-docs";包内工具在下一轮出现。属于某个包的工具不能用 tool=... 绕过包,必须整包解锁。

7. 数据存放位置

所有数据都在:

%DSH_HOME%\custom-tools

本机默认是:

C:\Users\acer\.dsh\custom-tools
如果设置过 DSH_HOME,以它为准;插件也支持用 DSH_CUSTOM_TOOLS_DIR 指向别的目录(需重启 DSH)。

路径 · 内容 · 说明 tools.json · 工具记录(唯一真源) · 名称、标题、描述、参数、代码、dangerous、enabled、packId(所属工具包,空=未分组)、时间戳 packs.json · 工具包记录 · id、name、description、alwaysUnlocked(是否始终解锁)、时间戳;成员关系不在这个文件里,而在各工具的 packId 上 settings.json · 全局开关 · unlockMode(AI 主动解锁模式)、allowUngroupedUnlock(允许单独解锁未分组工具)、approveCreate / approveUpdate / approveDelete;文件不存在时用默认值(解锁模式关、删除审批开) unlocks.json · 按对话的解锁状态 · sessions..packs(已解锁的工具包 id)+ .tools(已单独解锁的未分组工具 id);最多保留最近 200 个对话 scripts.py · 每个工具的代码副本 · 方便直接阅读/备份;直接改这里不会生效,执行用的是 tools.json 里的 code runs\r_ · 每次执行的临时目录 · 含 tool.py、args.json、result.json、stdout.txt、stderr.txt;自动只留最新 30 个 runner.py · 自动生成的 Python 执行器 · 不要手改,会被覆盖

备份/迁移:直接复制整个 custom-tools 目录即可(tools.json + packs.json + settings.json + unlocks.json 一起复制才算完整)。恢复时把目录放回原位(或设 DSH_CUSTOM_TOOLS_DIR),重启 DSH。