dsh-plugin-standards
Skill5.9k starsChanged 15 days agoMITView on GitHub
编写、修改或评审 DSH 插件时的代码规范:统一插件类型、包清单、目录与文件、命名、类型与接口、配置与状态、测试与发布方式。
Adopted by
Steady: 0 new projects took it in the last 4 weeks, none the 4 weeks before.thin data
Who's talking about PC2005-cloud/dsh-pet
- NousResearch/hermes-agent“Add dsh-pet to the plugin catalog”GitHub · Nov 9, 2026
- hyperion2144/dsh-desktop-plus“桌面横幅通知缺具体内容、点击无法聚焦/跳会话、声音不受「无声」设置控制(参考 dsh-pet 重写通知)”GitHub · Nov 6, 2026
- kexijiang/Piora“feat: 支持独立于 Pi Agent 插件的 DSH/Cordis 桌面插件体系”GitHub · Oct 30, 2026
- baileyh8/hermes-feishu-streaming-card“[Feature] 完成态展示 Agent 虚拟形象动图(opt-in,默认关闭)”GitHub · Oct 29, 2026
- WSK-build/DSHBox“[Feature] 支持插件透明悬浮窗契约,让 DSH 桌宠浮在所有应用之上(游戏时隐藏)”GitHub · Oct 29, 2026
The biggest 5 of 14 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 14
Talked about on GitHub and Bluesky (14 times).
On GitHub · 13
- Nov 9, 2026Add dsh-pet to the plugin catalog — NousResearch/hermes-agent0 reactions
- Nov 6, 2026桌面横幅通知缺具体内容、点击无法聚焦/跳会话、声音不受「无声」设置控制(参考 dsh-pet 重写通知) — hyperion2144/dsh-desktop-plus0 reactions · 2 comments
- Oct 30, 2026feat: 支持独立于 Pi Agent 插件的 DSH/Cordis 桌面插件体系 — kexijiang/Piora0 reactions
- Oct 29, 2026[Feature] 完成态展示 Agent 虚拟形象动图(opt-in,默认关闭) — baileyh8/hermes-feishu-streaming-card0 reactions · 1 comment
- Oct 29, 2026[Feature] 支持插件透明悬浮窗契约,让 DSH 桌宠浮在所有应用之上(游戏时隐藏) — WSK-build/DSHBox0 reactions
8 more on GitHub
- Oct 22, 2026Add kongbaiming5/dsh-pet-button(桌宠开关) — awesome-dsh-plugin/awesome-dsh-plugin0 reactions
- Oct 15, 2026[Bug][Windows]2.0.17 安装包未注册 `dsh://` 自定义协议,导致插件(dsh-pet)唤起主窗口时弹出 Microsoft Store"获取打开此链接的应用" 对话框 — anywhere-labs/dsh-desktop0 reactions
- Oct 3, 2026[Plugin] dsh-pet — AI-Scarlett/DSH-Store0 reactions
- Aug 23, 2026[Audit] dsh-pet-pc2005-cloud — SoberReport-AI/DeepGuard0 reactions · 2 comments
- Aug 22, 2026[Audit] dsh-pet — SoberReport-AI/DeepGuard0 reactions · 2 comments
- Aug 21, 2026catalog: add pc2005-cloud-dsh-pet (community curation, created by @PC2005-cloud) — diegosouzapw/awesome-omni-dsh-plugins0 reactions · 1 comment
- Aug 18, 2026catalog: add DSH Pet — MicroSpotlight/openharness-plugins0 reactions
- Aug 15, 2026[Submit] dsh-pet — 桌面宠物:会待机呼吸、随机小动作、屏幕漫游、点击互动 — 2BingLing/dsh-market0 reactions · 3 comments
On Bluesky · 1
- Oct 20, 2026🎉 Celebrating! 🎉 (500+ new stars) 📦 PC2005-cloud / dsh-pet ⭐ 3,005 (+505) 🗒 TypeScript DSH 桌面宠物:一行命令装好即用的透明动画小桌宠,支持多开、大小位置随心配置;还内置 DIY… — @github-trending-js.bsky.social0 likes and reposts · 1 reply
In this repository4 other files it gives its agents.
- dsh-pet-devSkill
- dsh-plugin-comment-specSkill
- git-commitSkill
- task-guardrailsSkill
The file
Files
DSH 插件代码规范
目标
让 DSH 插件在结构上一致、在接口上可预测、在维护上可拆分。规范只回答一个问题:这段代码该怎么写,才能让别人接手时不骂人。
适用对象:宿主半侧(host)、浏览器半侧(client)、工具、投影、bundle、独立运行与桌面 carrier。不适用于第三方库内部实现。
1. 先定插件类型
写第一行代码前,先确认属于哪一类;类型决定入口、清单字段与生命周期。
| 类型 | 特征 | 入口 |
| A 函数式 host | 只注册服务/监听/路由 | 命名导出 name/inject/Config/apply |
| B 服务式 host | 对外提供一个 Cordis 服务 | class X extends Service + static Config + super(ctx, 'name') |
| C 工具 | 给模型注册工具 | ctx.tools.register(defineTool({...})) |
| D 投影 | 从会话事件折叠状态 | ctx.sessionProjections.register({...}) |
| E 客户端 UI | 有浏览器渲染 | host 侧空 apply() + lib/client.js + dsh.client |
| F bundle | 只组合其他插件 | lib/index.js + cordis.patch.yml + dsh.bundle.patch |
| G 纯再导出 | 只导出类型或转调 | 只 re-export,不写逻辑 |
| H 元数据 | 只有 locale/icon | locale/*.json + icon |
判定顺序:有 UI → E;只组合 → F;给模型用 → C;维护派生状态 → D;对外提供服务 → B;其余 → A。
2. 包清单
package.json 固定字段:
{
"name": "@scope/dsh-<domain>-<role>",
"description": "一句话,市场展示用",
"version": "0.2.1-rc.2", // 与同 release 的 DSH 精确对齐,不用 ^
"publishConfig": { "access": "public" },
"repository": { "type": "git", "url": "…", "directory": "packages/<组>/<包>" },
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": { … },
"files": [ … ],
"license": "MIT"
}exports 五段固定:
".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
"./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" },
"./src/*": "./src/*",
"./package.json": "./package.json",
"./locale/*.json": "./locale/*.json"./client只给 E 类,且必须同时存在lib/client.js与lib/types/client/index.d.ts。./cordis.patch.yml只给 F 类。files只发运行产物与元数据:lib/index.js、lib/types/**/*.d.ts、locale/*.json;E 类加lib/client.js;F 类加cordis.patch.yml、icon.svg。
依赖三分:
| 位置 | 放什么 | 版本 |
dependencies | 运行时真正 import、宿主不提供的包 | 精确 |
peerDependencies | 宿主提供、不能打包的包;通常只有 @deepseek-ai/cordis | 精确或 ~ |
devDependencies | 其余全部:peer 包、类型包、测试库、react | 与 release 精确一致 |
凡写进 dsh.client.inject 的包,必须同时列进 devDependencies,否则拿不到类型。
dsh 字段:
// E 类
"dsh": { "client": { "inject": ["@deepseek-ai/dsh-client-locale"], "platform": "web" } }
// F 类
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
// 需要兼容性声明时
"dsh": { "compatibility": { "dshReleases": { "0.2.1-rc.2": "compatible" } } }dsh.client.inject 列包名,插件 inject 列服务名,两者不是一回事。
3. 目录与文件
src 模板:
src/
├── index.ts # 插件入口:name/inject/Config/apply
├── types.ts # 本包类型唯一归属地
├── client.ts # 可选:客户端类型再导出
├── client/ # E 类浏览器半边
│ ├── index.ts # inject + apply(ClientContext)
│ ├── <Component>.tsx
│ ├── slots.ts # inject face / props
│ └── locales.ts # zh/en 字典 + key 类型
└── <topic>.ts # host 侧按主题拆:storage / request / format文件规模(有效行,去空行与注释):
| 类型 | 软上限 | 硬上限 |
| 普通模块 | 200 | 300 |
| 含 IO/路由聚合 | 300 | 400 |
| React 组件文件 | 200 | 250 |
| 入口/装配 | 120 | 150 |
| 类型文件 | 150 | 200 |
| 测试文件 | 400 | 600 |
- 超限时按变化原因拆,不按行数切:同一资源、同一状态所有者、同一生命周期放一起。
- 行数与变化原因冲突时,变化原因优先:确实没有独立变化原因的就不切,在提交信息里写明“超限但没有独立变化原因,暂不拆”。判断标准是“以后改一个需求,还会不会同时动这两处”,不是“行数有没有回到表内”。
- 一个文件一个主职责,模块头第一行必须说得清。
- 目录深度 ≤4;单目录文件 ≤30;不留空目录。
- 文件命名 kebab-case;组件文件 PascalCase;类型
types.ts;插槽slots.ts;字典locales.ts。 - 禁止
utils.ts、helpers.ts、common.ts、misc.ts;按领域命名(jsonc.ts、http.ts、dom.ts)。 - 相对导入带扩展名:
from './slots.ts'、from './GoalBar.tsx'。 src/index.ts是唯一入口,其他文件都是它的私有依赖;客户端插件入口不得 importsrc/client/**。
README 必须有源码地图:
| 文件 | 职责 |
|---|---|
| `src/index.ts` | 插件入口:Config schema、工具注册 |
| `src/types.ts` | 投影键与载荷类型的唯一归属地 |目录细化
目录按领域分,不按技术类型分。满足以下五条才建子目录:
- 这组文件共享一个变化原因(改 A 常要改 B);
- 对外只需要 1–2 个入口;
- 能用领域名命名(不是
utils/common/components); - 组内 ≤30 个文件(与单目录上限同口径),且不会继续膨胀;
- 跨组引用主要经过出口。
| 单目录文件数(含测试) | 建议 |
| ≤20 | 保持扁平 |
| 20–30 | 先合并同类、删无用文件,再考虑分 |
| >30 | 必须按领域分 |
- 子目录 ≥3 个文件才建。
index.ts出口按需建:跨组引用 ≥2 处、或有稳定的对外面时才建;只有 1–2 处引用时直接相对导入。出口解决的是“散落的深路径”,不是“目录必须长得一样”——没有第二个消费方时,加一层只是多一跳。- 禁止按技术类型分(
components/、hooks/、services/)与杂物目录(misc/、helpers/、common/)。 - 测试文件跟随源文件进子目录,不集中到
tests/。
文件生命周期
拆分或替换旧文件时,旧文件只有三种去向:
| 旧文件身份 | 去向 | 保留多久 |
| 包入口 | 薄壳:只 re-export 与装配,≤50 行 | 永久 |
| 被其他包 import | 兼容层:只 re-export,标 @deprecated 替代路径与删除版本 | 一个 minor |
| 仅本包内部使用 | 删除 | 立即 |
- 禁止
legacy/、old/、archive/、backup/目录;禁止_v2/_new并行实现;禁止注释掉的旧代码。 - 删除前确认:引用清零、类型检查通过、测试绿、README 源码地图已更新。
- 历史交给 git(
git log --follow、git blame -C -C),不靠留文件。
4. 插件骨架
函数式 host:
/**
* <一句话职责>。
*
* <为什么需要这个插件 / 它约束了什么,2–4 句>。
* @module @scope/dsh-<name>
*/
import z from '@deepseek-ai/schemastery';
/** Cordis plugin name. */
export const name = '<name>';
/** 需要的宿主服务。 */
export const inject = ['<service>'];
/** 运行时配置 schema;默认值写在这里。 */
export const Config = z.object({
enabled: z.boolean().default(true),
});
/**
* <注册了什么、依赖什么、清理什么>。
* @param ctx - 挂载上下文。
* @param config - <配置语义>。
*/
export function apply(ctx: Context, config: z.infer<typeof Config>) {
ctx.effect(() => ctx.<service>.<register>({ /* ... */ }), '<name>.<resource>()');
}服务式 host:
export class WebServer extends Service {
static readonly Config = z.object({ host: z.string().required(), port: z.number().required() });
constructor(ctx: Context, config: z.infer<typeof Config>) {
super(ctx, 'webServer');
}
}
declare module '@deepseek-ai/cordis' {
interface Context { webServer: WebServer; }
}客户端 UI:host 侧保留空 apply(),让 Loader 能看到插件;浏览器侧:
import type { Context as ClientContext } from '@deepseek-ai/cordis';
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface LocaleNamespaceMap { <ns>: <Key>; }
}
/** Required services. */
export const inject: string[] = ['slots', 'locale'];
/**
* Client plugin body.
* @param ctx - client root context.
*/
export function apply(ctx: ClientContext): void { /* ctx.slots.register(...) */ }规则:
- 所有注册走
ctx.effect(register, '<模块>.<资源>()'),第二个参数是稳定标签,不能省。 - 配置默认值只写在
Configschema,apply内不再兜底。 - 可选字段用展开合并:
...config.x ? { x: true } : {}。 - bundle 的 patch 用
- insert:+id/name/config,group: true配isolate隔离服务作用域。
5. 导出与 Loader 契约
Loader 的 unwrapExports 只要发现 default 就取 default,命名导出被静默忽略:
| 形态 | 写法 | 后果 |
| 函数式 | 只用命名导出,不要 default | 有 default 则 inject 丢失 |
| 类式 | export { Klass, Klass as default },inject/name 写成静态成员 | 命名 inject 不生效 |
- 导出顺序按字母序:
export { Config, apply, inject, name }。 - 一个文件一个主导出;类型紧邻实现;测试专用导出标
@internal并放文件末尾。 - 单文件导出 ≤15 个;
index.ts只做公开入口,禁止export *。 - 包导出面只包含插件四件套与公开类型;内部模块不承诺兼容。
6. 命名
| 对象 | 规则 | 例 |
| 文件/目录 | kebab-case | request-zone.ts |
| 组件文件 | PascalCase | GoalBar.tsx |
| 变量/函数 | camelCase,动词开头 | deriveTimeZoneContext |
| 常量 | UPPER_SNAKE | MAX_WAIT_MS |
| 类型/接口 | PascalCase,无 I 前缀 | WebRoute |
| 布尔 | is/has/can/should/was | isLoopbackHost |
| 服务名 | camelCase,Context 上的键 | webServer |
| 插件 name | kebab-case | tool-todo |
| 事件名 | <域>/<动作> | turn/start、llm/stream |
| slot 名 | 点分层级 | conversation.input.dock |
| 配置字段 | camelCase,单位入名 | compressionThresholdBytes |
- 单位后缀强制:
_MS、_PX、_BYTES、_PERCENT。 - 禁止 1–2 字符标识符(
i/j循环、ecatch、t翻译函数例外)。 - 禁止
data/info/temp/obj/val/manager/helper/flag作局部变量名。 - 缩写白名单:
id、url、api、http、json、css、ms。 - 同一概念只用一个词;不要
pet/instance/item轮换。
7. 变量
按角色分类,每类有自己的规则:
| 种类 | 形态 | 规则 |
| 模块常量 | UPPER_SNAKE | 只在定义处赋值;单位入名 |
| 共享常量 | 集中在 constants.ts | 一个概念一处定义;跨包用 type-only 引用 |
| 配置值 | config.xxx | 只来自 schema;只读;不再兜底 |
| 环境变量 | DSH_<PKG>_<NAME> | 只在 env.ts 读一次并解析;非法即失败 |
| 可变状态 | camelCase | 只存在 store/闭包;不导出可变对象 |
| 模块单例 | xxxStore/xxxRegistry | 必须可 dispose;测试可重置;初始化顺序写进模块头 |
| 缓存 | cache/byId 后缀 | 必须有失效策略与容量上限 |
| 计数器 | seq/count/epoch | 单调性写清;并发下不靠自增保证唯一 |
| 定时器 | *Timer/*Handle | 必须可清理;unref 策略统一 |
| disposer | dispose*/*Off | 必须被 ctx.effect 收集,禁止丢弃返回值 |
| 资源句柄 | child/stream/fd/server | 单一所有者;关闭顺序写进模块头 |
| 哨兵 | 禁魔法字符串 | 缺失用 undefined,无值用 null,不让一个值兼表两义 |
| 凭据 | *Key/*Token/*Secret | 只驻内存;禁日志;错误只回分类文案 |
| 路径 | *Path/*Dir | 内部用绝对路径;拼接后校验前缀 |
| 标识符串 | 常量导出 | route/service/slot/event 名禁止字面量散落 |
| 时间/随机 | now/rng 参数 | 纯函数不读时钟;随机可注入 |
位置规则:模块常量在 import 之后、类型之前;局部变量距首次使用 ≤10 行;循环不变量提到循环外;同一语义只声明一次。
8. 类型与接口
两种 schema 分工明确:
- 插件
Config用 schemastery:要能 dump JSON Schema 给 Loader 与 patch。 - 持久化 record / wire payload 用 zod:
z.infer推导类型,同一 schema 之后还能投影成 RPC schema。
类型规则:
- 对外字段一律
readonly;数组readonly T[]。 - 失败用判别联合:
{ ok: false; error: { code; message } },不用boolean + 可选错误。 - 跨包契约只用
declare module合并(Context/Events/SlotMap/LocaleNamespaceMap/SessionProjectionMap),不在本地复制一份。 - 配置类型从 schema 推导,不手写第二份。
- 禁
any;外部输入用unknown+ 逐字段校验;断言必须紧邻校验语句。 - 内部字段用
#private,不靠_前缀。 - 判别联合必须穷尽:
default: assertNever(x)。
接口分层与稳定性:
| 层 | 内容 | 承诺 |
| 包导出面 | exports 的 ././client | semver 约束 |
| 插件接口 | name/inject/Config/apply | Loader 契约 |
| 服务接口 | Service 类 + Context 增强 | 跨插件运行时契约 |
| 跨包类型 | declare module 合并 | 编译期契约 |
- 事件声明写清
@mode emit/@mode waterfall/@mode serial,载荷必须是可序列化判别联合。 - 注册类接口一律
register(def) => disposer:id 唯一、内置优先、重复注册抛错、注销只删自己那一次。 - 对外错误必须有稳定
code(kebab-case)+ 人话message;code变更等同 API 变更。 - 废弃用
@deprecated 替代品与起始版本,至少保留一个 minor。
9. 函数、控制流与分层
9.1 规模与复杂度
| 对象 | 上限 |
| 普通函数 | 40 行 |
| 导出 API 函数 | 60 行 |
apply | 60 行 |
| React 组件(展示) | 150 行 |
| React 组件(实时:rAF / 指针 / 状态机) | 面板部分 150 行,驱动必须外置 |
| 自定义 hook | 80 行 |
| 类 | 200 行;方法 30 行;公开方法 10 个 |
| 参数 | 4(超过改对象) |
| 嵌套 | 3 |
| 圈复杂度 | 10 |
- 一个函数只做一层抽象:不混“决策 + IO + 格式化”。
- 布尔参数 ≥2 个、参数 >4 个,改选项对象或拆函数。
- 类只用于“有生命周期/多实例状态”;一次性对象必须在模块头写明“stop 后不可复用”。
- 实时组件按驱动数量判,不按行数:
ref ≥ 8、rAF 循环 ≥ 2、或带指针状态机时,把驱动抽成createXxx(deps)工厂,组件只留「面板 + 装配」。拆分的判据是“这块有没有独立的失败模式”,不是“行数超没超”。 - 拿不准要不要抽时,用一条判据:这块的规则能不能脱离 DOM / React 断言? 能(换算、优先级、状态转移、决策)就抽成纯函数并配测试;不能(帧时序、指针捕获、布局写入)就先收拢状态,别急着换文件。
9.2 控制流
- 卫语句 + 早返回;成功路径保持最左缩进,
else只用于真正对称的分支。 - 嵌套 ≤3;超出就提取函数或改查表。
- 同一概念的分支 >3 个(例如
kind有四种以上)改Record<Kind, Handler>,并用assertNever兜住穷尽性。 - 循环内不做 IO;嵌套循环先建索引;能一次遍历解决就不要两遍。
- 禁止嵌套三元;
if (x) return true; return false;写成return x。 - 复杂条件先命名:
const isStale = ...,不要整段塞进if。
9.3 校验边界
DSH 的写法是“解析一次,之后信任”:
- 外部输入在入口解析一次:
parseX(raw): X返回已校验的值,不返回 boolean。 - 分工:schema 负责类型/必填/枚举;
parseX只补 schema 表达不了的约束(非空、范围、去重、组合)。 - 解析后向下传已校验类型(branded 或解析后对象);内部函数不再校验。
- 配置在
apply入口一次性解析:校验 + 填默认 +deepFreeze,之后只读。 - 只有三种情况允许二次校验:跨进程边界、跨版本读取、从介质读回。
- A 校验后调 B、B 又校验——二者只能留一个:B 是公共 API 就删 A 的校验;B 是内部函数就删 B 的校验。
9.4 分层与依赖方向
按能力缝分四层,不照搬 controller / service / mapper:
| 层 | 形态 | 只做 | 禁止 |
| 入口 carrier | 路由 handler、slot 组件、tool execute、CLI 命令 | 解析输入、调服务、写响应/渲染 | 业务规则、直接读介质 |
| 服务编排 service | Service 子类、registry | 注册、路由、生命周期、事务边界 | 纯计算、文案拼接 |
| 领域 domain | 纯 fold、parse、format、policy | 规则、换算、状态转移 | IO、时钟、随机、DOM |
| 介质适配 adapter | storage backend、LLM adapter、HTTP client、bridge | 序列化、持久化、协议 | 业务判断 |
- 依赖单向:carrier → service → domain;adapter 由 service 调用;domain 不依赖任何层。
- 按需分层:小插件不强行四层,但 domain 不得依赖 carrier。
- 模块头写非目标,官方写法是“owns only X, never Y”。
- 同一结果给多处消费时,结构化数据只构造一次,渲染各自做薄映射。
9.5 副作用与错误位置
- 副作用集中在入口与 adapter;领域函数不读时钟、不写盘、不发请求。
- 时间、随机、IO 通过参数注入,便于测试。
- 领域抛语义错误(带
cause),入口转成结构化结果;不在中间层重复包装。
9.6 调用依赖
- 依赖分两种:import 边与调用边。import 环用工具禁,调用环由装配根拉直。
- 双向通信只能经装配根注入的函数或事件,禁止两个模块互相 import。
- 下层通知上层:注入回调或发事件,不 import 上层。
let+ 延迟引用只允许出现在装配根,业务模块禁止。- 命名区分“请求”与“执行”:
requestRestart()不等于restart()。
9.7 实时驱动
rAF 循环、指针状态机、定时器泵这类实时设施共用一套骨架,按这张表走,不要在同一仓库里各自发明:
| 要素 | 做法 | 为什么 |
| 帧句柄 | 驱动内部持有 frame: number | null | 必须跨渲染存活;放组件里,重渲染后就失去了取消能力 |
| 世代号 | 起跑/停止时 token++,帧首 if (token !== own) return | 迟到的帧自弃,旧循环不会改到新状态 |
| 实例 | 组件里 xxxRef.current ??= createXxx({...}) | 每渲染重建会丢句柄;只建一次,则依赖必须全走 getter |
| 依赖 | 全部 getter(box()/physics()/viewport()),不传快照 | 驱动活过多次渲染,快照会陈旧 |
| 停止 | stop() 取消帧、清句柄、清派生状态;要停谁由调用方决定 | 一条驱动只管自己,不 import 兄弟驱动 |
| 写入 | 帧内同步写 DOM 与实时值,落定才提交框架状态 | 把写入推迟到回调里,重渲染会读到旧值,只在特定渲染节奏下暴露 |
组件侧只留三件事:把 getter 束装配起来、把驱动方法接到视图、在卸载时 stop。
判据:这块有没有独立的失败模式(帧不取消会漏帧、句柄不跨渲染会丢循环、快照会陈旧)——有就抽成驱动;只是“行数多”不构成理由。
9.8 运行时注入的 React
组件库由宿主注入 react 能力(h、useState、useEffect、useRef)而不是直接 import 时,按这个模具写,不要每次重新设计:
/** 注入的 react 能力;类型只取用到的子集。 */
export interface XxxRuntime {
/** 创建元素。 */
h: typeof jsx;
/** 状态。 */
useState: <T>(init: T) => [T, Dispatch<SetStateAction<T>>];
/** 引用。 */
useRef: <T>(initial: T) => ReactNS.MutableRefObject<T>;
}
/**
* 造 hook。
*
* @param rt - 注入的 react 能力。
* @returns 接收视图输入的 hook。
*/
export function createXxx(rt: XxxRuntime) {
return function useXxx(input: XxxInput): Xxx {
// …
};
}- hook 定义在工厂闭包里,组件里无条件调用一次;返回的方法不依赖
this,可以在视图里直接传onClick: xxx.onClick。 - 必须有
useRef才能跨渲染存活的状态,一律用useRef持有;不要用模块级变量(会串到别的实例)。 - 注入类型用「只取用到的子集」,不要整份
typeof React——测试替身也得跟着实现全部方法,成本会翻倍。
10. 配置与状态
配置:
- 一份 schema 是唯一真源:类型、默认值、校验、文档都由它派生。
- 未知字段默认拒绝(
strict),要放行必须显式并写明理由。 - 优先级固定并写进模块头:内置默认 → bundle patch → profile patch → 用户覆盖 → CLI。
- 字段改名/改语义必须提供迁移与版本号;迁移幂等;失败不写半成品。
- 敏感项标
secret:不回显、不落日志、不进 dump。 - 配置变更必须走同一条重载路径;重载失败保留旧值并报错。
状态:
- 持久化状态必须有
stateVersion;升版提供相邻迁移,旧数据可回放。 - 投影
apply必须是纯同步函数:(state, event) => state,禁止 IO/时钟/随机。 - 未关心的事件返回同一引用,让框架用
Object.is短路。 - 持久化状态必须是 plain JSON。
- 携带状态的日志事件必须给完整变更后状态,禁止裸 delta。
- 写链语义:先等持久化成功 → 再改内存 → 再发变更事件;失败则内存不动。
- 持久化行只是“fold 捷径”,版本不符或 seq 超界就丢弃,不当权威。
11. 并发与资源
- 所有等待有超时;超时常量命名并注明依据。
- 异步入口接受
AbortSignal并贯穿到最内层;取消后不再写响应/状态。 - 禁止 floating promise:要么
await,要么void+ 注释说明,要么进队列。 - 同一资源的并发写必须串行化(队列/锁/单飞);读可以并发。
- 流式输出必须有界;队列必须有上限与丢弃策略。
- 清理顺序固定并写进模块头:摘监听 → 停定时器 → 关流/子进程 → 落盘。
- 子进程用
stopAndWait语义;重启退避 + 熔断;禁止新旧进程重叠。 - 定时器统一
unref策略;请求级与长驻级分开。 - 注册返回的 disposer 必须交给
ctx.effect;不允许“注册了但没人清理”。
12. 错误与日志
错误分三类:
| 类型 | 例 | 处理 |
| 用户错误 | 参数非法、配置非法 | 4xx / 明确文案;不记 error |
| 系统错误 | IO、网络、超时 | 5xx / 归一化 code;记 warn/error;可重试 |
| 依赖错误 | 服务缺失、凭证缺失 | 降级或明确不可用;严禁冒充成功 |
- 抛错带
cause:throw new TypeError(msg, { cause })。 - 错误信息写“期望什么、收到什么”,不写“出错了”。
- 安全相关错误只回分类文案,绝不回显凭据、完整 URL、请求体。
- 日志用
ctx.logger,禁裸console.*(客户端可用,但统一前缀)。 - 日志结构化:级别 +
event+ 关键字段 +durationMs;不打印用户内容。 - 每个插件一个日志前缀;请求级日志带 session/trace id。
13. 安全
- 输入:长度上限、类型白名单、枚举白名单;路径拼接后校验前缀;URL 只允许 http/https。
- 凭据:只驻内存;错误脱敏;不写日志、不落盘;读取时校验文件权限。
- 命令:禁止 shell 拼接;参数数组传递;命令固定时也要防路径含引号。
- HTML:外部文本一律转义;富文本走白名单节点树 + 容量上限;CSP 默认拒绝。
- 网络:默认只监听回环;跨源默认拒绝;令牌比较用 timing-safe。
- 供应链:lockfile 入库;依赖构建脚本默认禁用;发布 job 与构建 job 权限分离。
14. 客户端 UI
- 插槽由 owner 声明
kind(single/list/keyed/chain)与scope(root/session-maybe/session),registrant 只消费。 - props 由框架三段合成:
PropsRuntime<slot>+InjectFace<...>+PropsLocale<ns>;组件不自己造t。 - 每个导出组件 props 字段:语义不显然的写一行,坐标/尺寸/名字这类不写(细则见
dsh-plugin-comment-spec);返回类型显式JSX.Element | null。 - locale:
locales.ts里zh是 key 集真源,en键集必须一致;type Key = keyof typeof zh;key 用<域>.<对象>.<角色>。 locale/zh.json只放市场元数据(meta.title/meta.description),不放 UI 文案。- 组件拆分:展示组件单组件 ≤150 行;
useState≤5;useEffect≤6;派生状态抽纯函数。实时组件(rAF / 指针 / 状态机)按 §9.7 把驱动外置,面板本身仍受 150 行约束,useState/useEffect的额度只算面板自己的。 - 样式只用
--dsw-*设计 token,禁止字面色值;优先用 primitives,不重复造按钮/菜单/弹窗。 - 尺寸、间距、圆角走 token 或共享常量;CSS 注入只允许一个
injectStyleOnce(key, css)入口。 - 无障碍:可点元素用原生语义或
role="button" tabIndex={0}+ Enter/Space;图标按钮有aria-label;状态用aria-expanded/aria-live;焦点可见;键盘可达。
15. 注释与文档
注释细则见同目录的 dsh-plugin-comment-spec,这里只列硬约束:
- 文件头按角色分档:入口/装配/驱动/带状态 hook 写 4–8 行(职责、非目标、依赖、不变量),普通模块 1 行,纯函数叶子可省略。
- 每个导出一行摘要;复杂函数写
@param name - 语义、@returns 边界、@throws 条件。 - 只写“为什么”,不写“做了什么”;禁历史叙述、文件路径引用、无主 TODO。
- 覆盖率豁免必须带原因:
/* v8 ignore next N -- 原因 */。 - 注释语言跟随项目,标签与结构不变。
文档:
README.md与README.zh.md成对,顶部互链。- frontmatter 固定
description+kind: "package-reference"。 - 章节固定:概述 → 使用本包 → 理解实现(含源码地图、导出形状)→ 进一步探索 → 模型体验(工具类)→ 已知限制 → 开发备注。
- 章节锚点用
<a id="kebab-case"></a>;长实现说明放<details>。 - 双语 README 用
README.i18n.yaml记录每个 heading 的中英哈希;改一侧必须同步另一侧并重录。 - 设计决策写进
notes/<date>-<topic>.md或 ADR,README 链接过去,不塞进代码注释。
16. 测试
16.1 测试类型
| 类型 | 测什么 | 放哪 | 占比 |
| 单元 | 纯函数:parse / format / 换算 / 状态转移 | 与被测同目录 | ~70% |
| 契约 | 插件注册了什么、清理了什么 | 与被测同目录 | ~20% |
| 集成 | 组件 + slot + locale 的组合行为 | tests/integration/ | 少量 |
| 端到端 | 启动、路由、失败矩阵 | tests/*.expected.e2e.ts | ~10% |
- 组件测试用真实渲染,不用大快照。
- 需要 HTTP 的测试用可控替身:本机 stub、
nock/msw、注入的 fetch 任一即可;断言请求形状、超时、重试与失败归一化,不依赖真实账号与当前时间。 - 默认测试套件不访问公网与真实服务商;需要真实服务商的用例单独放
*.live.test.ts(默认 skip、需显式环境变量、不进 CI)。
16.2 文件与目录
- 一个源文件对应一个测试文件:
foo.ts↔foo.test.ts,同目录。 - 跨模块集成放
tests/integration/;e2e 放tests/*.expected.e2e.ts。 - fixture 放
tests/fixtures/,只读;需要写入的用每个 test 自建的临时目录。 - 测试辅助函数放
tests/support/,不叫utils.ts。 - 测试文件 ≤600 行;超过按行为分组拆,不按“第 1 部分 / 第 2 部分”拆。
16.3 写法
describe('<模块/函数>');test('<行为>:<条件> → <结果>'),中文。- AAA:准备、执行、断言;一个 test 只验证一个行为。
- 断言消息写“应该是什么”,失败时能直接定位。
- 每个 test 自建 ctx / store / 临时目录;测试之间不共享可变状态、不依赖执行顺序。
- 时间、随机、网络、文件系统通过参数注入。
- 契约测试至少断言两件事:注册成功、dispose 后清理干净。
- 网络测试:每个 test 自建 server、随机端口、结束时关闭;断言请求头/路径/超时,不依赖公网。
16.4 禁止
- 新增读源码的正则守卫(如“这个文件里必须出现某个符号”):它测的是文本不是行为,重构一动就红。存量守卫登记进 baseline,只减不增;改到相关文件时把守卫换成断言行为的测试。
- mock 被测模块的内部函数。
- 大对象快照。
sleep等待;用条件轮询或注入时钟。- 测试之间传状态(全局变量、模块级缓存)。
- 断言实现细节(私有字段、调用次数)而非行为。
16.5 覆盖率
- 新增/改动行 ≥80%;纯函数 ≥90%;不追全量数字。
- 未覆盖的存量登记 baseline,只降不升。
- 覆盖率豁免必须写原因。
- 测试行数 : 源码行数 ≈ 0.8–1.2。
17. 构建与发布
- 构建用 tsdown,产物
lib/;客户端 bundle 形态为window.__ModuleLoader__.load({ id, factory }),react/react-dom/@deepseek-ai/*外部化。 - 发布前必须跑:
typecheck → lint → format:check → test → prepack-check。 files白名单只能有一个来源;生成器、打包器、检查脚本都从它派生或对它断言。- 生成物(
lib/types/**、bundle、schema dump、catalog、i18n 哈希)标@generated+ 生成命令;CI diff 校验。 - 版本与 DSH release 同步;打 tag 前校验
package.json.version与 tag 一致。 - CI:PR 跑全量门禁;发布独立 job,
contents: write只给它;构建 job 用只读权限。
18. 性能
- 预算:bundle 大小、启动时间、单次请求内存、首屏时间,各有阈值并进 CI。
- 缓存:只缓存纯计算结果;有 key、有上限、有失效;缓存不改变语义。
- 懒加载:SDK、解析器一类重依赖动态 import,不进启动路径。
- 热路径(rAF、轮询)禁止每帧新建大对象/闭包;派生值缓存。
- IO:批量读、避免重复 stat;文件监听去抖。
19. 禁止清单
- 函数式插件导出
default(会让 Loader 丢inject)。 - 客户端插件缺 host 半边
apply()。 - 只声明
exports["./client"]而漏dsh.client。 dsh.client.inject的包漏出devDependencies。- 手写第二份配置默认值。
- 用字面色值/硬编码 px,不用
--dsw-*。 utils.ts/common.ts杂物袋。- 跨插件复制类型/常量/协议。
- 单文件 >300 行、单函数 >60 行、单组件 >150 行(实时组件的“驱动外置”见 §9.7,面板部分同样受此限)。
- 注册了资源却没有 disposer 归属。
- floating promise、无超时的等待、无法取消的异步。
- 用
console.*取代ctx.logger。 - 用注释写历史、写路径、写无主 TODO。
- 发布清单与构建脚本各自维护。
- 迁移没有版本号,或改格式不 bump
stateVersion。
20. 提交前检查
- 插件类型明确,入口四件套齐全。
exports/files/依赖三分/dsh字段正确。- 文件 ≤300 行、函数 ≤60 行、组件 ≤150 行;超限的先问“有没有独立变化原因”,没有就写明不拆,有才拆。
- 实时组件(rAF / 指针 / 状态机)的驱动已外置,帧句柄与世代号在驱动内部(§9.7)。
- 注入 react 的 hook 走
createXxx(rt)模具,跨渲染状态用useRef而非模块级变量(§9.8)。 - 没有第二份常量/类型/规则/清单。
- 变量按种类放对位置;句柄与定时器有清理归属。
- 配置默认值只在 schema;未知字段被拒绝。
- 持久化状态有
stateVersion与相邻迁移。 - 异步有超时/取消;没有 floating promise。
- 错误分类、带
code/cause、已脱敏。 - 日志结构化,不打印用户内容与凭据。
- 输入校验长度/枚举/路径/URL。
- 客户端只用
--dsw-*token,优先用 primitives。 - locale 的 zh/en 键集一致;键盘可用。
- 测试同目录、测行为、覆盖新增行。
- 生成物标
@generated并被 CI 校验。 - README 有源码地图、双语成对、i18n 哈希已更新。
- 废弃/兼容有声明与迁移说明。
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

