liangdabiao/dsh-weather-plugin2

@demo/weather-plugin

查询城市实时天气,并以带动效的天气卡片展示(晴天/多云/阴/雾/雨/雪/雷暴动画)。

包名
@demo/weather-plugin
版本
0.1.0
最近更新
2026年9月1日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:liangdabiao/dsh-weather-plugin

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

5.1 一切的起点:index.ts

每个插件都有一个入口文件 src/index.ts,导出 nameinjectConfigapply 函数。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 = 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 插件规范写法。


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