DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Weather Plugin — DSH Plugin for DeepSeek Harness
← Plugins
W

@demo/weather-plugin

Weather Plugin

Check real-time weather for cities and display it in animated weather cards (sunny, partly cloudy, overcast, fog, rain, snow, and thunderstorm animations).

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:liangdabiao/dsh-weather-plugin#7fa2f4e2f5424f99891ded14bbde29daf70dbb66
READMECompatibilityVersions

Compatibility and provenance

Weather Plugin is published as @demo/weather-plugin and currently resolves to version 0.1.0. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
web
Release source
github
Registry updated
9/1/2026

Versions

0.1.0stable
9/1/2026

Related plugins

Loading related plugins…

Latest
0.1.0
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
web
License
MIT
Source
github
GitHub
★ 3
Weekly downloads
0
Last push
9/1/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in

Related plugins

More verified plugins in ui-customization.

Web App@deepseek-ai/dsh-web-appThe dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)Experimental Agent Team Web Profile@deepseek-ai/dsh-experimental-agent-team-web-profileExperimental Web profile layer for Agent Teams Remote and UI pluginsRemote Web Ui@linxin666/dsh-remote-web-uiScan-to-pair remote access for the dsh web GUI that shares one official interface: a QR beside the settings button pairs phones and PCs into the same Web GUI (a portrait-touch adaptation layer for phones, full desktop on PCs) through one-time tokens and rClient Ui Task Board@linxin666/dsh-client-ui-task-boardHost-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.

README

从 0 到 1 开发 dsh 插件实战课

以「查天气 + 动效卡片」插件为完整案例,带你走完一个真实 dsh 插件的全部开发流程:需求 → 环境 → 骨架 → 实现(工具/技能/系统提示/浏览器端)→ 双端构建 → 安装测试 → 界面验收 → 发布维护。


写在前面

这门课教什么

DeepSeek Harness(简称 dsh)是一个"万物皆插件"的智能体框架。给 dsh 增加任何新能力——让模型会查天气、会调用你的系统、会画出好看的界面——本质上都是写一个插件。

这门课以一个真实的天气插件(我们已经在 dsh 网页里跑通、显示过天气卡片的项目)为蓝本,手把手带你从零写出、构建、安装并测试一个插件。你最终会得到:

  • 输入"北京今天天气怎么样",模型自动调用 weather 工具;
  • 工具调用 Open-Meteo 天气 API 拿到真实数据(不是编的);
  • 模型按 weather-briefing 技能规范播报:"北京今天晴天 ☀️ 29°C 东风 6km/h……";
  • 在 dsh 网页界面里,渲染出一张会动的天气卡片(☀️ 旋转光芒、🌧 下落雨线、❄️ 飘落雪花……)。

这门课适合谁

  • 完全没接触过 dsh,但会一点前端/Node 的开发者;
  • 想给自己的智能体(Agent)增加自定义工具和界面的产品/技术人员;
  • 想理解"插件化智能体框架"到底怎么回事的人。

怎么学效果最好

  1. 跟着做:每一章的代码和命令都自己敲一遍,别只看。
  2. 看真实报错:这门课记录了我们开发时真实踩过的坑和报错原文,遇到类似问题直接查附录 C。
  3. 遇到卡点先自查:先看是不是 Node 版本、junction 顺序、产物没同步这类"高频三坑"。

第 1 部分 · 认识 dsh

第 1 章 dsh 是什么:一个「万物皆插件」的智能体框架

1.1 传统智能体 vs dsh

传统做法:模型、工具、对话循环、网页界面……全部"焊死"在一个程序里。想加一个能力,就得改源码、重新编译、重新发布。这对第三方开发者非常不友好。

dsh 换了一种哲学——everything-is-a-plugin(万物皆插件):

  • dsh 的核心几乎是一个"空壳"(无特权内核);
  • 几乎所有能力——模型适配器、工具注册表、Agent 循环、网页界面——本身都是插件;
  • 所有插件挂在一个叫 Cordis 的容器框架上,按需组合、替换、扩展。

一句话:dsh = 一台"所有零件都能换"的智能体机器。写插件 = 造一个零件装上去。不需要 fork 源码。

1.2 你的插件挂在哪里

当你写一个天气插件时,你实际上是在往这台机器上加三样东西:

东西类比作用
工具(Tool)给模型"手"模型可以调用的动作:查天气、查数据、算东西
技能(Skill)给模型"说明书"教模型"按什么规范产出":天气怎么播报、文案怎么写
系统提示给模型"入职培训"教模型"什么时候该用哪个工具"

三样配合,模型才能"会查 → 会播报 → 播得好"。

1.3 一个插件通常写"两端"

dsh 网页端的插件往往是双端的:

  • Node 端(后端进程,可信环境):负责"干活"——注册工具、调 API、算数据;
  • 浏览器端(前端沙箱 iframe,受限环境):负责"画画"——把工具结果渲染成卡片、图表、动效。

两端通过 meta 数据衔接:Node 端算好结构化数据放进 meta,浏览器端读取并渲染。

这正是我们天气插件的设计:Node 端调 Open-Meteo 拿温度风力,浏览器端用这些数据画一张会动的天气卡片。


第 2 章 一个 dsh 插件的生命周期(全景图)

写一个插件不是只写代码,而是一条完整流水线。全程共 7 步,本课程后续章节逐一展开:

① 选型 → ② 环境 → ③ 骨架 → ④ 实现 → ⑤ 构建 → ⑥ 安装 → ⑦ 测试

第 ① 步 选型:这是哪一类插件?

动手前先回答:"用户要加的新能力属于哪一类?" dsh 插件分四种(详见第 15 章):

类型解决什么什么时候用
Tool Plugin让模型拥有新动作90% 的需求,比如查天气、查数据库、调 API
Skill 插件教模型按规范产出想让输出风格统一、结构固定
Service Provider换底层驱动换模型网关、换文件沙箱
Event Interceptor关键路径加料审批、审计、限流
Agent Loop重写核心循环高难定制,一般用不到

选型口诀:Provider 是"换驱动",Tool 是"装软件",Interceptor 是"加关卡",AgentLoop 是"换引擎"。 我们的天气插件 = Tool Plugin(工具)+ Skill 插件(技能)+ 系统提示 + 浏览器端渲染,是 dsh 插件最典型的组合。

第 ② 步 环境准备

  • Node.js 必须 22+(dsh 0.1.x 用到 Promise.withResolvers、node:zlib zstd、node:module stripTypeScriptTypes,Node 20 必崩);
  • 能运行 dsh 命令;
  • 定位到 dsh 发布包缓存(构建时要用它里面的宿主依赖,见第 11 章)。

第 ③~④ 步 骨架与实现

用脚手架一键生成项目骨架,然后填充四块核心代码:工具、技能、系统提示、浏览器端。

第 ⑤ 步 双端构建

用 tsdown 一次构建出两个产物:lib/index.js(Node 端)+ lib/client.js(浏览器端单文件)。

第 ⑥ 步 安装进 profile

dsh 用 profile(配置档案)管理插件集合。dsh plugin --profile 名字 add file:路径 即可把插件装进去。

第 ⑦ 步 测试与验收

  • headless 冒烟:命令行问一句,验证"识别意图 → 调工具 → 播报"全链路;
  • web 界面验收(最终标准):浏览器里看到工具调用 + 动效卡片渲染。

重要提醒:命令行跑通 不算完成。用户真正看到的是网页界面。所以最终验收必须在浏览器里做。这也是本课程贯穿始终的标准。


第 2 部分 · 环境与骨架

第 3 章 环境准备:Node 22 是硬门槛

3.1 我们真实踩到的坑

开发天气插件时,第一次启动 dsh 直接崩了,报错类似:

TypeError: Promise.withResolvers is not a function
TypeError: zlib.createZstdDecompress is not a function
TypeError: module.stripTypeScriptTypes is not a function

原因:dsh 0.1.x 必须在 Node 22+ 上运行,而我们系统里的 node 是 v20.20.2。Node 20 缺少这些 API。

3.2 dsh 到底用了哪些 Node 22 新能力

Node 22 新能力dsh 用来干什么
Promise.withResolvers标准库新增的 Promise 构造方式,dsh 内部异步逻辑用到
node:zlib 的 createZstdDecompress解压 zstd 压缩的数据
node:module 的 stripTypeScriptTypes让 dsh 能直接加载/剥离 TS 类型

这些在 Node 20 里都不存在,所以不是配置问题,是版本问题。

3.3 怎么解决

方法一(推荐):升级 Node 到 22+。官网下载 LTS 版(22.x)安装即可。

方法二(本机多版本):如果你系统里同时有多个 node,直接用 Node 22 的 node.exe 跑 dsh,不污染全局:

# 找到 node22 的 node.exe,直接调 dsh 的入口文件
C:\Users\49707\node22\node-v22.23.2-win-x64\node.exe C:\Users\49707\AppData\Local\npm-cache\_npx\<hash>\node_modules\@deepseek-ai\dsh\lib\bin.js --profile web

方法三:把 node22 目录放到 PATH 的最前面,这样 dsh、node、npx 都优先用 Node 22。

验证:node -v 必须输出 v22.x 及以上。

连带坑:构建工具 tsdown 0.22+ 也要求 Node 22。如果只能在 Node 20 环境构建,请用 tsdown@^0.19.0(我们就是这么降级的)。

3.4 一键脚本的完整流程

环境确认后,构建脚本按这个顺序工作(见 scripts/build.ps1):

# ① 安装构建工具(tsdown/typescript)
npm install --legacy-peer-deps
# ② 链宿主依赖(junction,必须放在 npm install 之后!)
python scripts/link_deps.py
# ③ 双端打包
npx tsdown
# ④ 自检产物
# 检查 lib/index.js 与 lib/client.js 是否生成

为什么 link_deps 必须放在 npm install 之后? 因为 npm install 会把 node_modules 里的链接清掉重新装,如果在它之前建链接,会被冲掉。这是开发时反复踩的坑(详见第 11 章)。


第 4 章 认识插件骨架:每个文件是干什么的

4.1 用脚手架一键生成

不用手动搭文件,用 skill 自带的脚手架脚本:

python scripts/scaffold_plugin.py D:/dev/weather-plugin --name @me/weather-plugin --desc "查天气 + 动效卡片"

它会自动复制模板、替换包名和描述占位符,生成一个结构完整、可直接开始改代码的项目。

4.2 项目结构逐文件解读(真实 API 版)

weather-plugin/
├─ package.json          # 插件"身份证":入口/依赖/清单字段
├─ cordis.patch.yml      # 挂载声明:把插件插进 profile 的 layer 栈
├─ tsdown.config.ts      # 双端打包配置
├─ tsconfig.json         # Node 端 TS 配置
├─ tsconfig.client.json  # 浏览器端 TS 配置(lib: DOM)
├─ scripts/
│  ├─ build.ps1          # 一键构建(Windows)
│  ├─ link_deps.py       # junction 链宿主依赖
│  └─ sync_profile.py    # 构建后同步产物到已安装的 profile
└─ src/
   ├─ index.ts           # 插件入口 apply + 配置 Config
   ├─ tool.ts            # 工具定义(defineTool)
   ├─ skill.ts           # 技能定义(SkillProvider)
   ├─ fragment.ts        # 共享契约纯函数(双端共用)
   └─ client/
      └─ index.tsx       # 浏览器端 Toolview 组件

4.3 package.json 里的关键字段(容易看懵)

dsh 插件不用 dsh.plugin.json(那是旧版资料的说法),而是在 package.json 里声明:

{
  "name": "@me/weather-plugin",
  "type": "module",
  "main": "lib/index.js",
  "exports": {
    ".": "./lib/index.js",
    "./client": "./lib/client.js",
    "./cordis.patch.yml": "./cordis.patch.yml"
  },
  "dsh": {
    "bundle": { "patch": "./cordis.patch.yml" },
    "client": { "inject": ["@deepseek-ai/dsh-client-runtime"], "platform": "web" }
  },
  "dshx": {
    "contributes": { "tools": ["weather"], "skills": ["weather-briefing"] }
  },
  "peerDependencies": {
    "@deepseek-ai/cordis": "^4.0.1-rc.1",
    "@deepseek-ai/dsh-tools": "*",
    "@deepseek-ai/dsh-skill": "*",
    "@deepseek-ai/dsh-system-prompt": "*",
    "@deepseek-ai/schemastery": "^3.18.1-rc.1",
    "react": "^18.2.0"
  }
}
字段干什么
dsh.bundle.patch指向 cordis.patch.yml,告诉 dsh 怎么挂载插件
dsh.client.inject / dsh.client.platform浏览器端注入声明(web 平台)
dshx.contributes.tools/skills插件提供的工具/技能清单(发现用)
peerDependencies声明依赖的宿主包(不打包,运行时由 dsh 提供)

4.4 挂载声明 cordis.patch.yml

- insert:
    - id: weather-plugin      # 插件实例 id
      name: '@me/weather-plugin'   # npm 包名

这个文件让 dsh 启动时知道"要加载一个叫 weather-plugin 的插件",并把它挂进当前 profile 的 layer 栈。


第 3 部分 · 开发核心

第 5 章 插件入口 apply 与配置 Config

5.1 一切的起点:index.ts

每个插件都有一个入口文件 src/index.ts,导出 name、inject、Config 和 apply 函数。dsh 启动时加载插件,调用 apply(ctx, config),把容器上下文 ctx 传给你。

import type { Context as CordisContext } from '@deepseek-ai/cordis'
import type SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import type SkillService from '@deepseek-ai/dsh-skill'
import type ToolRegistry from '@deepseek-ai/dsh-tools'
import z from '@deepseek-ai/schemastery'
import { weatherTool } from './tool.js'
import { weatherSkillProvider } from './skill.js'
import type { WeatherClientConfig } from './client.js'

// 声明"这个插件要用哪些服务",并给出合并后的 ctx 类型
type Context = CordisContext & {
  tools: ToolRegistry
  systemPrompt: SystemPrompt
  skills: SkillService
}

export const name = 'weather-plugin'          // 插件唯一 id
export const inject = ['tools', 'systemPrompt', 'skills']  // 依赖的服务

export interface Config extends WeatherClientConfig {}

export const Config: z<Config> = z.object({
  baseUrl: z.string().default('https://api.open-meteo.com/v1/forecast')
    .description('Open-Meteo 天气 API 基地址;可指向自建镜像做离线开发。'),
  geocodingUrl: z.string().default('https://geocoding-api.open-meteo.com/v1/search')
    .description('Open-Meteo 地理编码 API 基地址。'),
  timeoutMs: z.number().step(1).min(1_000).default(10_000)
    .description('单次天气请求超时(毫秒)。'),
})

export function apply(ctx: Context, config: Config): void {
  // 把配置收敛成一个运行时对象(带默认值兜底)
  const resolved: WeatherClientConfig = {
    baseUrl: config.baseUrl ?? 'https://api.open-meteo.com/v1/forecast',
    geocodingUrl: config.geocodingUrl ?? 'https://geocoding-api.open-meteo.com/v1/search',
    timeoutMs: config.timeoutMs ?? 10_000,
  }

  // 三件套都包在 ctx.effect 里注册:插件被移除时自动清理
  ctx.effect(() => ctx.tools.register(weatherTool(resolved)), 'weather-plugin.tool')
  ctx.effect(() => ctx.skills.registerProvider(() => weatherSkillProvider), 'weather-plugin.skill')
  ctx.effect(() => ctx.systemPrompt.section({
    name: 'tool:weather',
    order: 117,
    text: PROMPT_TEXT,
  }), 'weather-plugin.prompt')
}

5.2 逐块拆解

部分说明
export const name插件唯一 id,必须和 cordis.patch.yml 的 id 一致
export const inject声明依赖的服务。ctx 就能访问 .tools / .skills / .systemPrompt
export const Config用 schemastery 定义配置 schema,带默认值 + 中文说明。用户可在 profile/补丁层覆盖,不用改代码
export function apply真正干活的地方:注册工具、注册技能、注入系统提示
ctx.effect(fn, key)把注册动作挂到插件生命周期,卸载自动清理("插头的保险丝")

为什么用 ctx.effect? 如果插件被移除/停用,effect 会自动注销它注册的工具、技能、系统提示,不会留下"幽灵注册"污染容器。这是 dsh 插件规范写法。


第 6 章 写工具:让模型「会查天气」(defineTool)

6.1 工具的本质

工具 = 给模型看的"可执行函数签名" + 你写的 execute 实现。

  • 模型看到的是 description 和 parameters(决定它会不会调、怎么调);
  • 模型调用后,你的 execute 去干活(这里就是查天气 API);
  • 你返回的数据,一部分给模型看(短文本),一部分给浏览器渲染(结构化数据)。

6.2 天气数据源:Open-Meteo(免 key)

我们用的天气 API 是 Open-Meteo,完全免费、无需注册 API key:

  • 地理编码:把城市名转成经纬度
    GET https://geocoding-api.open-meteo.com/v1/search?name=北京&count=1&language=zh
    
  • 实时天气:按经纬度拿天气
    GET https://api.open-meteo.com/v1/forecast?latitude=..&longitude=..&current_weather=true&daily=..&timezone=auto&forecast_days=1
    

6.3 defineTool 四要素

真实 API 用 defineTool(...) 产出工具定义,再 ctx.tools.register(...) 注册:

import { defineTool, type ToolDefinition } from '@deepseek-ai/dsh-tools'
import { normalizeItems, summarize, myMetaFrom } from './fragment.js'

export function weatherTool(config: WeatherClientConfig, fetchImpl?: typeof fetch): ToolDefinition {
  return defineTool({
    name: 'weather',
    description: '查询指定城市的实时天气。当用户询问天气、温度、雨雪、风力时使用。',
    parameters: {
      city: { type: 'string', required: true, description: '城市名,例如:北京、上海、London' },
      unit: { type: 'string', enum: ['celsius', 'fahrenheit'], description: '温度单位,默认摄氏度' },
    },
    output: {
      schema: { /* ... */ },
      render: (_args, value) => [{ type: 'text', text: value.text }],
      presentationMeta: (_args, value) => ({ kind: 'weather', ...value.weather }),
    },
    isConcurrencySafe: () => true,
    async execute(args) { /* 查天气,返回 { text, weather } */ },
    presentCall: () => ({ card: 'generic', title: '查天气', kind: 'other' }),
    presentResult() { /* 结果卡片标题 */ },
  })
}

四要素详解:

要素作用写作要点
name工具名蛇形命名,唯一
description说明模型决定调不调它全靠这句。写清"做什么 + 何时用 + 注意"
parameters参数 Schema手写 JSON Schema;每个参数带 description,否则模型不知道传什么
output返回说明render 定模型看到的文本;presentationMeta 定浏览器渲染的数据

6.4 execute 里做了什么(核心逻辑)

async execute(args) {
  const city = typeof args.city === 'string' ? args.city.trim() : ''
  if (city === '') throw new Error('weather: city 是必填参数')

  // ① 地理编码:城市名 → 经纬度
  const geo = await fetchImpl_(`${config.geocodingUrl}/search?name=${encodeURIComponent(city)}&count=1&language=zh`)
  const geoData = await geo.json()
  const hit = geoData.results?.[0]
  if (!hit) throw new Error(`weather: 找不到城市「${city}」,请换英文名试试`)

  // ② 实时天气:经纬度 → 天气数据
  const unit = args.unit === 'fahrenheit' ? 'fahrenheit' : 'celsius'
  const forecast = await fetchImpl_(`${config.baseUrl}?latitude=${hit.latitude}&longitude=${hit.longitude}&current_weather=true&daily=temperature_2m_max,temperature_2m_min&timezone=auto&forecast_days=1`)
  const data = await forecast.json()

  // ③ 用共享契约把原始数据规整成 WeatherInfo
  const info = parseWeather(hit.name, data, unit)

  // ④ 返回:text 给模型看(短),weather 结构化数据进 meta 给浏览器
  return { text: summarize(info), weather: info }
}

6.5 output 与 presentationMeta 分离(核心套路)

这是 dsh 插件最重要的设计套路:

output: {
  schema: {
    type: 'object', additionalProperties: false,
    properties: {
      text: { type: 'string', required: true },      // 给模型的一句话
      weather: { type: 'object', additionalProperties: true, required: true }, // 结构化数据
    },
  },
  render: (_args, value) => [{ type: 'text', text: value.text }],
  presentationMeta: (_args, value) => ({ kind: 'weather', ...value.weather }),
}
  • text(模型看到的):要短。模型还要把它写进自己的回复,太长挤占上下文。例:北京当前晴天,29°C,20~29°C,6km/h 东风。
  • presentationMeta(浏览器用的):可序列化的结构化数据(城市、天气类型、温度、风力、温度范围……),dsh 把它写进持久化 meta,不占模型上下文;浏览器端从 block.meta 读回渲染卡片,回放也一致。

结果:用户看到的是"模型一句话播报 + 浏览器一张精美动效卡片",而模型上下文里只有一句话,省 token、渲染富、两端职责分离。

6.6 错误处理

两种失败要区别对待:

// 参数错误 → 直接 throw(让模型知道这次调用无效,自行修正)
if (city === '') throw new Error('weather: city 是必填参数')

// 业务失败(找不到城市、外部服务报错)→ 抛出带信息的错误,让模型读得懂并调整策略
if (!hit) throw new Error(`weather: 找不到城市「${city}」,请换英文名试试`)

原则:参数/调用错误抛异常,业务失败返回结构化结果。别把"外部服务挂了"当成"工具坏了"抛给模型。


第 7 章 共享契约 fragment:双端一致的秘密

7.1 问题:同一份数据,两端各算一套?

Node 端把天气数据写进 meta,浏览器端要读它渲染。如果两端各写各的解析逻辑,很容易不一致:Node 端说 WMO 天气码 0 是"晴",浏览器端以为是"阴",卡片就画错了。

解法:把"数据怎么算/怎么解析"抽成纯函数契约模块 src/fragment.ts,Node 端、浏览器端、测试三方 import 同一个函数。

7.2 fragment.ts 里有什么

// 双端共用的纯函数:不 import 任何 @deepseek-ai 包,Node/浏览器都能打包

/** WMO 天气码 → 动效类型 + 中文描述 */
export function weatherTypeFromCode(code: number): { type: WeatherType; zh: string } {
  if (code === 0) return { type: 'sunny', zh: '晴' }
  if (code >= 1 && code <= 3) return { type: 'partly', zh: '多云' }
  if (code >= 45 && code <= 48) return { type: 'fog', zh: '雾' }
  if (code >= 51 && code <= 67) return { type: 'rain', zh: '降雨' }
  if (code >= 71 && code <= 77) return { type: 'snow', zh: '降雪' }
  if (code >= 95) return { type: 'storm', zh: '雷暴' }
  return { type: 'cloudy', zh: '阴' }
}

/** 把 Open-Meteo 原始数据规整成渲染/展示用的 WeatherInfo */
export function parseWeather(city: string, data: unknown, unit: string): WeatherInfo { /* ... */ }

/** 给模型看的一句话摘要 */
export function summarize(info: WeatherInfo): string {
  return `${info.city}当前${info.zh},${info.temp}°${unitChar},${info.low}~${info.high}°${unitChar},${info.windSpeed}km/h ${info.windDir}。`
}

/** 从持久化 tool/result meta 还原结构化数据(Node 写入、浏览器读回) */
export function weatherMetaFrom(meta: unknown): WeatherInfo | undefined { /* ... */ }

7.3 为什么这设计很值

  1. 一致性:同一份映射、同一份解析,双端必然一致;
  2. 可测试:纯函数不碰网络、不碰 DOM,是最便宜、最该多写的测试层;
  3. 省 token:规则说明(如天气码映射)还能被模型直接引用,省得重复写。

经验:把"所有会算数据的逻辑"都往 fragment 里放,是 dsh 插件的最佳实践。


第 8 章 写技能:教模型「怎么播报天气」(SkillProvider)

8.1 技能是什么

技能 = 给 AI 的"写作规范手册"。 工具给 AI"手"(动作),技能给 AI"说明书"(怎么做、按什么规范产出)。

我们的天气技能 weather-briefing 就干一件事:教模型拿到天气数据后,按什么结构、什么语气播报。

8.2 真实 API:SkillProvider(list + get)

当前版本的技能是 SkillProvider { name, list(), get() } 两级加载(旧资料里的 fetchCandidates/load 已经变了):

import {
  BUNDLED_SKILL_RANK,
  type SkillCandidate, type SkillDefinition, type SkillProvider,
} from '@deepseek-ai/dsh-skill'

const PROVIDER_NAME = 'weather-plugin'
const INVOCATION = { modelInvocable: true, userInvocable: true } as const

const CANDIDATES: SkillCandidate[] = [{
  name: 'weather-briefing',
  description: '向用户播报天气时使用:固定结构、语气自然、附一条实用建议。'
    + '在调用 weather 工具拿到实时数据后,按此规范组织播报。',
  invocation: INVOCATION,
  provider: PROVIDER_NAME,
  source: 'bundled',
  rank: BUNDLED_SKILL_RANK,
  locator: 'weather-briefing',
}]

const BODY = `# 天气播报规范
1. 先报城市与天气类型(如:北京当前晴天)。
2. 再报温度与体感,单位与用户要求一致(摄氏度/华氏度)。
3. 补充风力(风速+风向)。
4. 有高低温时附上今日范围。
5. 结合天气给一句实用建议(雨天带伞、降温加衣、雪天慢行),不超过 10 字。
6. 只依据 weather 工具返回的实时数据,不编造预报。`

export const weatherSkillProvider: SkillProvider = {
  name: PROVIDER_NAME,
  list: () => Promise.resolve(CANDIDATES),   // 候选清单:轻量,常驻
  async get(candidate): Promise<SkillDefinition> {
    return {
      name: candidate.name,
      description: candidate.description,
      invocation: candidate.invocation,
      provider: PROVIDER_NAME,
      source: 'bundled',
      rank: candidate.rank,
      content: BODY,   // 完整规范:重,触发才加载
    }
  },
}

8.3 两级加载的设计用意

层内容何时进上下文
list() 候选name + description(轻)常驻,模型据此判断是否触发
get() 正文完整 content(重)仅当模型决定使用该技能时

为什么要这样? 模型上下文是稀缺资源。如果把所有技能的完整规范都常驻,上下文立刻爆炸。所以:候选精炼常驻、正文按需加载。

8.4 技能正文就是"创作契约"

写技能正文 = 写清楚产出物的结构、必含要素、格式、语气、边界。要点:

  1. 结构契约:固定顺序(先城市 → 温度 → 风力 → 范围 → 建议);
  2. 必含要素:列出缺一不可的(单位一致、高低温范围);
  3. 格式契约:字数、语言;
  4. 边界:明确"不做什么"——我们写了"只依据工具实时数据,不编造预报",这是防止模型幻觉的关键。

在 apply 里注册:ctx.effect(() => ctx.skills.registerProvider(() => weatherSkillProvider), 'weather-plugin.skill')


第 9 章 系统提示:引导模型「何时用工具」

9.1 为什么需要系统提示

工具 description 通常写得较长,模型在长对话里不一定记得用它。系统提示是常驻的,加一小段就能持续引导:"遇到天气问题 → 调用 weather 工具 → 按技能播报"。

9.2 真实 API:ctx.systemPrompt.section

注意签名是传一个对象 { name, order, text }(不是三个参数):

const PROMPT_TEXT = `## Query weather (weather)
Use the \`weather\` tool when the user asks about the weather, temperature,
rain/snow, or wind for any city (for example "北京天气怎么样" or
"will it rain in London tomorrow morning?"). Pass the city name in \`city\`.
The tool returns real-time conditions as an animated weather card; report it
following the weather-briefing skill. Only pass \`unit\` when the user asks
for Fahrenheit.`

ctx.effect(() => ctx.systemPrompt.section({
  name: 'tool:weather',   // 段落唯一 id
  order: 117,             // 排序:小的靠前
  text: PROMPT_TEXT,      // 段落正文
}), 'weather-plugin.prompt')

9.3 order 的作用

系统提示由多个插件各自追加,顺序有讲究:通用规则在前、具体约定在后。order 小的靠前,约定类段落给较大 order 放后面(我们用 117),避免干扰模型对通用指令的理解。

9.4 写好工具/系统提示的"三件套"

模型只通过文字理解工具。 写 description 的黄金模板:

  1. 做什么:查询指定城市的实时天气。
  2. 何时用:当用户询问天气、温度、雨雪、风力时使用。
  3. 何时不用/注意:仅当用户要求华氏度时才传 unit 参数。

实战验证:weather 的系统提示 + description 按"三件套"写,浏览器实测模型一次就准确调用了 weather 工具。

9.5 系统提示 vs 技能:怎么分

维度系统提示技能
加载常驻(始终在上下文)按需触发加载
体量宜短可长
用途引导"何时用工具/遵守约定"提供"怎么写/怎么组织"的完整规范

原则:全局的、简短的 → 系统提示;局部的、详细的 → 技能。别把长规范塞进系统提示。


第 10 章 浏览器端:把数据画成动效卡片

10.1 浏览器端在架构里的位置

Node 端负责"干活"(调 API、算数据),浏览器端负责"画画"(渲染界面)。两端通过持久化 meta 衔接:

工具 presentationMeta ──► block.meta(持久化)──► 浏览器端 Toolview ──► 用户看到的动效卡片

浏览器端代码最终被打包成 lib/client.js,由 dsh 以 /plugins/weather-plugin/client.js 提供,在沙箱 iframe 里运行。

10.2 注册方式:slots 注入 keyed Toolview

真实 API 是 ctx.slots.inject('tool.call.toolview', ...) + ctx.slots.register({name, key}, Component)(旧资料的 registerView 已经变了):

import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client'
import { weatherMetaFrom } from '../fragment'

export const name = 'weather-plugin'
export const inject = ['slots']

function WeatherCardView({ callId, block }: ToolCallViewProps) {
  // ① 运行中:block 还没有 kind,显示 loading
  if (!('kind' in block)) {
    return <div className="wea-load">加载天气中…</div>
  }
  // ② 失败
  if (block.isError) {
    return <div>天气查询失败</div>
  }
  // ③ 成功:从持久化 meta 还原数据,渲染动效卡片
  const info = weatherMetaFrom(block.meta)
  if (info === undefined) return <div>无天气数据</div>
  return <WeatherCard info={info} />
}

export function apply(ctx: ClientContext): void {
  ctx.slots.inject('tool.call.toolview', () => ctx.slots.register(
    { name: 'tool.call.toolview', key: 'weather' },  // key = 工具名
    WeatherCardView,
  ))
}

10.3 三种状态判断(重要)

组件拿到的 block 有三种形态:

形态判断方式渲染什么
运行中(pending)!('kind' in block)loading 动画
失败block.isError错误文本
成功有 kind,从 block.meta 还原结果卡片

别漏掉:运行中的 block 没有 kind 字段,直接读 block.meta 会拿不到数据。

10.4 动效卡片:纯 CSS,零外部依赖

天气卡片用纯 CSS keyframes 实现 6 种天气动画,不引任何 CDN(沙箱 CSP 也禁止外部脚本):

天气动效
☀️ 晴太阳核心 + 旋转的虚线光芒
⛅ 多云漂浮移动的云朵
🌫 雾平移的雾带
🌧 降雨周期性下落、淡出的雨线
❄️ 降雪飘落 + 旋转的雪花
⛈ 雷暴闪烁的闪电

卡片还显示:城市、温度、体感、风向风速、今日温度范围、"白天/夜间"、更新时间。

硬约束:浏览器产物必须单文件、无动态 import、无多 chunk,不能 import Node 内置模块。这些由构建自检把关(见第 11 章)。


第 4 部分 · 构建与测试

第 11 章 双端构建:tsdown + junction + ModuleLoader

11.1 构建的目标

一次构建产出两个文件:

  • lib/index.js —— Node 端(工具/技能/系统提示),ESM + 类型声明;
  • lib/client.js —— 浏览器端(Toolview),CJS 单文件,带 ModuleLoader 包装。

11.2 junction:宿主依赖从哪来

插件要 import @deepseek-ai/* 宿主包,但不打包它们(运行时由 dsh 提供)。这些包在 dsh 发布包的 node_modules 里就有。

用 junction(Windows 目录联接) 把它们链进插件的 node_modules,保证和运行的 dsh 完全同版本:

# scripts/link_deps.py(节选)
NODE_DEPS = ["cordis", "cosmokit", "schemastery",
             "dsh-tools", "dsh-skill", "dsh-system-prompt", ...]
CLIENT_DEPS = ["dsh-client-runtime", "dsh-client-ui-tool", "dsh-client-ui-slots", ...]

# 用 cmd 的 mklink /J 建目录联接(不需要管理员权限)
subprocess.run(["cmd", "/c", "mklink", "/J", dst, src], check=True)

为什么不用 npm install 装一份? 会装到 registry 上的另一个版本,和运行的 dsh 不一致,类型和运行时都可能错位。junction 直连 dsh 自带的包最稳。

高频坑:npm install 会把 node_modules 里的链接清掉重装,所以 link_deps.py 必须放在 npm install 之后跑(build.ps1 已按此编排)。

11.3 tsdown 双端配置

// tsdown.config.ts(要点)
export default [
  {
    entry: { index: 'src/index.ts' },
    outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024',
    dts: true, clean: true,
    deps: { neverBundle: ['@deepseek-ai/schemastery', '@deepseek-ai/cordis'] },
  },
  {
    entry: { client: 'src/client/index.tsx' },
    outDir: 'lib', format: 'cjs', platform: 'browser',
    outputOptions: {
      entryFileNames: 'client.js',
      inlineDynamicImports: true,   // 强制单文件
      banner: `window.__ModuleLoader__.load({ id: "weather-plugin", factory: (require) => {`,
      footer: `return module.exports; } });`,
      intro: 'var module = { exports: {} }; var exports = module.exports;',
    },
  },
]

关键点:

  • neverBundle:schemastery 和 cordis 不打包,因为 dsh 的 Loader 要校验你的 Config schema,必须看到它自己的实例;
  • 浏览器端 format: 'cjs' + inlineDynamicImports:产出一个 CJS 文件;
  • banner/footer:把产物包进 window.__ModuleLoader__.load({ id, factory }),这是 dsh 网页端加载插件浏览器端的方式。

从旧 API 迁移最易漏的一环:忘写 ModuleLoader 包装,前端会报 __ModuleLoader__ is not a function。

11.4 产物自检(三大硬约束)

构建后自检 lib/client.js:

  1. require 白名单:只允许 import react、@deepseek-ai/dsh-client-* 等平台模块;
  2. 无动态 import:不允许 import(;
  3. 无多 chunk:必须是单文件。

任一不满足即报错,保证浏览器产物能在 CSP 沙箱安全加载。

11.5 build.ps1 全流程

# ① 装构建工具
npm install --legacy-peer-deps
# ② 链宿主依赖(junction,必须在 install 之后)
python scripts/link_deps.py
# ③ 双端打包
npx tsdown
# ④ 自检
# 检查 lib/index.js 与 lib/client.js 是否生成

第 12 章 安装进 profile 与 headless 冒烟测试

12.1 安装:三行命令

dsh 用 profile(配置档案)管理插件。我们装进两个 profile:web(网页端)和 headless(命令行端)。

dsh plugin --profile web add file:D:/dsh-openmaic-main/weather-plugin
dsh plugin --profile headless add file:D:/dsh-openmaic-main/weather-plugin

这条命令会自动把插件(含 cordis.patch.yml 的 insert)写进 profile 的 package.json → dsh.profile.bundles,dsh 启动该 profile 时按 layer 顺序加载。

12.2 headless 冒烟(最快验证全链路)

不需要浏览器,命令行直接问:

dsh --profile headless "北京今天天气怎么样"

我们真实得到的输出:

北京今天晴天 ☀️
气温:当前 29°C,体感较热,全天在 20~29°C 之间
风力:东风,风速约 6 km/h,风很小

白天太阳很足、午后偏热,出门记得防晒补水;早晚 20°C 左右稍凉,可以带件薄外套。

再测一个城市:

dsh --profile headless "上海今天天气怎么样"
# → 上海当前阴天 ☁️ 29°C 东风 11km/h

这证明了什么:

  1. 模型识别了"查天气"的意图 → 调用了 weather 工具;
  2. 工具真的调了 Open-Meteo 拿到实时数据;
  3. 模型加载了 weather-briefing 技能,按规范播报(结构、语气、实用建议都对)。

12.3 数据真实性校验

为了确认不是模型编的,我们写了一个独立脚本直接调 Open-Meteo,与插件返回逐项比对一致:

Open-Meteo 北京实时:晴天 29°C 6km/h 东风 20~29°C
weather 工具返回:   北京当前晴天 29°C 20~29°C 6km/h 东风

数据一致 → 全链路真实可靠。

说明:weather 工具调用的是 Open-Meteo 免费 API,无需任何 API key。如果你开发的插件需要调用付费服务,key 应放在 ~/.dsh/.credentials.yaml 或 profile 配置里,绝不写进代码。


第 13 章 web 界面验收:在浏览器里看到天气卡片

13.1 为什么这步是"最终验收"

用户真正看到的是网页界面。命令行跑通只能证明"逻辑对",界面验收才能证明"用户看得见、用得上"。

13.2 启动 web 并实测

dsh --profile web
# 浏览器打开 http://127.0.0.1:3080

我们真实实测的过程:

  1. 浏览器打开 http://127.0.0.1:3080,标题显示 DeepSeek Harness,模型已配置为 DeepSeek-V4-Flash;
  2. 输入框输入"北京今天天气怎么样",点击发送;
  3. 会话标题自动变成 "北京今日天气查询";
  4. 界面消息流里依次出现:
    • 上下文注入(system-prompt、skill-catalog);
    • Think:模型判断要用 weather 工具;
    • Skill:weather-briefing 被加载;
    • 动效天气卡片渲染:
      ☀️
      📍 北京
      29°C
      体感 29°C · 东风 6km/h
      20~29°C · 白天
      更新于 16:30
      
    • Think:工具返回"北京当前晴天,29°C,20~29°C,6km/h 东风。";
  5. 模型按技能规范完整播报 + 实用建议;
  6. 统计信息:1 轮 · 2 步 | LLM 4.5s · 工具调用 3.6s | 首 token 平均 1.1s · 156 tok/s | 缓存命中 50% | 输入 31.4K tok · 输出 358 tok。

13.3 这次验收证明了什么

  • 天气工具被真实调用并返回数据;
  • weather-briefing 技能被加载并指导播报;
  • 浏览器端动效卡片成功渲染(前端读取持久化 meta → 画出生动的天气卡);
  • 前端卡片数据与 headless 完全一致 → 回放一致性的体现。

13.4 如果卡片没显示怎么办

排查顺序:

  1. http://127.0.0.1:3080/plugins/weather-plugin/client.js 是否返回 200(浏览器端产物是否被 serve);
  2. block.meta 是否有数据(Node 端 presentationMeta 是否正确投影);
  3. ctx.slots.register 的 key 是否与工具名 weather 一致;
  4. client.js 是否单文件、无动态 import(构建自检)。

第 5 部分 · 交付与进阶

第 14 章 发布与维护:改代码后的正确姿势

14.1 一个容易忽略的坑:file: 依赖是复制

用 file: 方式安装插件,dsh 是把它复制进 profile 的 node_modules,而不是软链。所以:

  • 第一次安装后,你再改插件代码、重新构建,profile 里还是旧产物;
  • 必须手动把新产物同步过去。

正确姿势(改代码后三步):

# ① 重新构建
powershell -ExecutionPolicy Bypass -File scripts/build.ps1
# ② 同步产物到已安装的 profile
python scripts/sync_profile.py
# ③ 重启 dsh 生效

sync_profile.py 会把 lib/、cordis.patch.yml、package.json 拷到 profile 里的插件目录。

14.2 本次开发真实踩过的坑(高频)

#坑现象解法
1Node 版本太旧dsh 启动报 Promise.withResolvers 等升级 Node 22+,或用 node22 的 node.exe 跑
2junction 被 npm install 清掉构建找不到 @deepseek-ai/*link_deps 放 npm install 之后
3改了代码没生效行为还是旧的file: 是复制,跑 sync_profile.py
4浏览器端没渲染前端报 __ModuleLoader__ 错误client.js 缺 ModuleLoader 包装
5中文城市查不到"纽约" geocode 无结果API 语言问题,换英文名或北京/上海
6tsdown 版本Node 20 下 tsdown 0.22 报 Promise.withResolvers用 tsdown@^0.19.0
7PowerShell 中文乱码build.ps1 输出乱码脚本输出用纯英文
8npm peer 冲突npm install 报 peerDependencies加 --legacy-peer-deps

14.3 发布清单

对外发布前逐项确认:

  • lib/index.js 和 lib/client.js 已生成且通过自检;
  • cordis.patch.yml 的 id 与 package.json 的包名一致;
  • headless 冒烟通过(dsh --profile headless "<问题>");
  • web 界面看到渲染效果(最终验收);
  • 密钥/口令不在代码或产物里(放配置文件);
  • 可选:npm publish 后,用户用 dsh plugin add 包名 安装。

第 15 章 进阶:四种插件类型,选型与实现

15.1 四类插件总览

类型一句话实现手段典型场景
Tool Plugin给模型加可执行动作defineTool + ctx.tools.register查天气、查库、调 API
Skill 插件教模型按规范产出SkillProvider {list,get}统一写作风格、播报规范
Service Provider换底层驱动实现服务接口 + ctx.super('key')换模型网关、换文件沙箱
Event Interceptor关键路径加料waterfall 事件 + next() 委托/短路审批、审计、限流
Agent Loop重写核心循环实现 Agent 接口 + AgentFactoryPlan-and-Execute、多智能体

15.2 选型决策

用户想"加新能力" →
├─ 想让模型多一个"能干的动作"?            → Tool Plugin(首选)
├─ 想教模型"怎么写/怎么组织"?            → Skill 插件
├─ 想换掉底层驱动(模型/文件/沙箱)?     → Service Provider
├─ 想在关键路径插审批/审计/限流?         → Event Interceptor
└─ 想重写整个思考-行动循环?              → Agent Loop

选型口诀:Provider 是"换驱动",Tool 是"装软件",Interceptor 是"加关卡",AgentLoop 是"换引擎"。

建议:多数需求用 Tool + Skill + 系统提示 + 浏览器端 组合即可解决。先做最小可用的 Tool,再补技能/系统提示/前端,不要一上来就动 Agent Loop。


第 16 章 沉淀:把经验变成可复用的 Skill

16.1 为什么值得沉淀

你刚走完一个完整插件的全部流程。下次开发一个类似插件(查股票、查物流、查数据库),80% 的步骤是重复的:环境、骨架、构建、安装、测试、避坑。

把这些沉淀成一个 Skill(给 AI 的"作业规范手册"),就能让 AI 照着做、少踩坑、速度快一倍。

16.2 一个 dsh 插件开发 Skill 应该装什么

资源内容
SKILL.md7 步工作流(选型→环境→骨架→实现→构建→安装→测试)
references/深度文档:架构、四种类型、工具、技能、系统提示、浏览器端、构建测试、排错
assets/plugin-skeleton/可直接复制的插件骨架模板
assets/examples/weather-plugin/完整可运行的天气插件案例(就是本课做的这个)
scripts/scaffold_plugin.py一键生成插件项目脚手架

16.3 你从这门课带走的核心资产

  1. 一套方法论:需求 → 选型 → 环境 → 骨架 → 实现 → 构建 → 安装 → 测试 → 验收;
  2. 一个真实 API 认知:defineTool、SkillProvider {list,get}、systemPrompt.section、slots.inject——都是当前版本的真实用法,网上很多旧资料是错的;
  3. 一个可运行的案例:天气插件(查天气 + 动效卡片),可作为任何新插件的起点;
  4. 一堆避坑经验:Node 22、junction 顺序、file: 同步、ModuleLoader、中文城市 geocode……都在附录 C 有对照表。

附录

附录 A · 完整代码清单(weather-plugin)

天气插件的完整源码位于 D:\dsh-openmaic-main\weather-plugin\。核心文件速览:

文件职责关键代码
src/index.ts入口 + 配置export const name / inject / Config(schemastery z.object)/ apply 里三个 ctx.effect 注册
src/tool.tsweather 工具defineTool:name / description / parameters / output(render+presentationMeta)/ execute / presentCall / presentResult
src/skill.tsweather-briefing 技能SkillProvider { name, list(), get() },正文 BODY 是播报规范
src/fragment.ts共享契约weatherTypeFromCode(WMO 码→动效/中文)、parseWeather、summarize、weatherMetaFrom
src/client/index.tsx浏览器端ctx.slots.inject('tool.call.toolview') + keyed WeatherCardView + 6 种纯 CSS 天气动画
tsdown.config.ts双端打包Node ESM + 浏览器 CJS 单文件 + ModuleLoader banner/footer
cordis.patch.yml挂载声明- insert: { id: weather-plugin, name: '@demo/weather-plugin' }
scripts/build.ps1一键构建install → link_deps → tsdown → 自检
scripts/link_deps.pyjunction 链依赖mklink /J 链 @deepseek-ai/*
scripts/sync_profile.py同步产物拷 lib/ + patch + package.json 到 profile

这份源码也完整保存在 dsh-plugin-developer Skill 的 assets/examples/weather-plugin/,可随时对照。

附录 B · 常用命令速查

目的命令
检查 Node 版本(必须 ≥22)node -v
生成插件骨架python scripts/scaffold_plugin.py <目录> --name @scope/name
一键构建powershell -ExecutionPolicy Bypass -File scripts/build.ps1
链宿主依赖python scripts/link_deps.py
双端打包npx tsdown
安装进 profiledsh plugin --profile web add file:<绝对路径>
headless 冒烟dsh --profile headless "北京天气怎么样"
启动 webdsh --profile web(浏览器开 http://127.0.0.1:3080)
同步产物到 profilepython scripts/sync_profile.py
检查浏览器端产物是否被 serve浏览器访问 http://127.0.0.1:3080/plugins/weather-plugin/client.js

附录 C · 常见报错对照表

报错/现象原因解法
Promise.withResolvers is not a functionNode 太旧(需 22+)升级 Node 22+,或用 node22 的 node.exe 跑 dsh
zlib.createZstdDecompress is not a functionNode < 22同上
module.stripTypeScriptTypes is not a functionNode < 22同上
构建找不到 @deepseek-ai/*junction 被 npm install 清掉link_deps 放 npm install 之后重跑
mklink /J 需要管理员权限用了符号链接用 junction(mklink /J),不需要管理员
tsdown 报 Promise.withResolverstsdown 0.22+ 也要 Node 22Node 20 用 tsdown@^0.19.0
npm peerDependencies 冲突dsh 各包 peer 相互引用npm install --legacy-peer-deps
前端报 __ModuleLoader__ is not a functionclient.js 缺 banner/footer配 tsdown outputOptions banner/footer
改了代码行为没变file: 依赖是复制重建后 python scripts/sync_profile.py
工具返回了但前端没渲染meta 缺失 / key 不一致查 presentationMeta、slots.register 的 key
"纽约"查不到天气geocode API 中文问题换英文名或北京/上海
PowerShell 中文乱码PS 5.1 编码脚本输出用纯英文

附录 D · 术语表

术语含义
dshDeepSeek Harness,万物皆插件的智能体框架
Cordis插件容器框架,所有插件挂载于此
插件(Plugin)一个 npm 包,导出 apply(ctx, config),给 dsh 加能力
Profiledsh 的配置档案,管理一组插件(如 web、headless)
工具(Tool)给模型的可执行动作,defineTool + execute
技能(Skill)给模型的写作规范,SkillProvider {list,get} 两级加载
系统提示(System Prompt)常驻指导文字,ctx.systemPrompt.section
Toolview浏览器端把工具结果渲染成界面的组件
metaNode 端写、浏览器端读的持久化结构化数据
presentationMeta工具把结构化数据投影进持久化 meta 的钩子
junctionWindows 目录联接,用于链宿主依赖
tsdownTS 打包器,本课用于双端打包
ModuleLoaderdsh 网页端加载浏览器端产物的机制(window.__ModuleLoader__.load)
Bundleprofile 的插件清单(dsh.profile.bundles)
Open-Meteo免费天气 API(地理编码 + 实时预报,无需 key)

结语

恭喜你走完了从 0 到 1 开发 dsh 插件的完整旅程。你现在掌握了:

  • 认知:dsh 的"万物皆插件"哲学,插件双端架构;
  • 实战:工具、技能、系统提示、浏览器端动效四块核心能力的真实 API 写法;
  • 工程:Node 22 环境、junction 链依赖、tsdown 双端构建、ModuleLoader 包装;
  • 交付:profile 安装、headless 冒烟、web 界面验收、发布维护;
  • 沉淀:把经验变成可复用 Skill。

下一步建议:用脚手架生成一个新骨架,把天气插件换成你的真实需求(查股票、查物流、查数据库……),照本课流程再走一遍。第二次会比第一次快一倍。

祝你做出好插件!