liangdabiao/dsh-weather-plugin ↗★ 2
@demo/weather-plugin
查询城市实时天气,并以带动效的天气卡片展示(晴天/多云/阴/雾/雨/雪/雷暴动画)。
安装
$
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:liangdabiao/dsh-weather-plugin说明文档
阅读完整 README ↗第 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 = 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 要校验你的
Configschema,必须看到它自己的实例; - 浏览器端 format: 'cjs' + inlineDynamicImports:产出一个 CJS 文件;
- banner/footer:把产物包进
window.__ModuleLoader__.load({ id, factory }),这是 dsh 网页端加载插件浏览器端的方式。
从旧 API 迁移最易漏的一环:忘写 ModuleLoader 包装,前端会报
__ModuleLoader__ is not a function。