Merge origin/master at ecbf75a5e7 into skill catalog hot refresh

This commit is contained in:
Tianyi Cui
2026-07-29 21:42:18 +08:00
422 changed files with 5585 additions and 2063 deletions

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/skill/skill-local/README.md
README.md: d4b6253c6667f4786d53ad6c291f9e96f82546c3
README.zh.md: 7cede0ea64064ca4f170d81303085b40b5751f15
README.zh.md: 0c02080fce49c13c4a128676c39b793f3054fdb8

View File

@@ -4,19 +4,19 @@
`ctx.skills` 注册表的本地文件系统提供方。
该包实现一个 skill 来源。它扫描本地项目、自定义和用户 skill 根,解析 `SKILL.md` 或平铺 Markdown skill 文件,并将提供方注册到 `ctx.skills`。注册表仍位于 `@deepseek-ai/dsh-skill`;持久会话目录和面向模型的加载器工具仍位于 `@deepseek-ai/dsh-tool-skill`
该包package实现一个 skill(技能)来源。它扫描本地项目、自定义和用户 skill 根目录,解析 `SKILL.md` 或平铺 Markdown skill 文件,并将提供方注册到 `ctx.skills`。注册表仍位于 `@deepseek-ai/dsh-skill`;持久会话目录和面向模型的 loader 工具仍位于 `@deepseek-ai/dsh-tool-skill`
## 插件
需要 `ctx.skills` `inject: ['skills']`)。
需要 `ctx.skills``inject: ['skills']`)。
### 配置
| 字段 | 默认值 | 含义 |
|---|---|---|
| `dshHome` | `$DSH_HOME` or `~/.dsh` | 由 [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) 解析的 DeepSeek Harness 配置根;扫描该目录下的 `skills`。 |
| `agentsHome` | `$DSH_AGENTS_HOME` or `~/.agents` | 为兼容 skill 扫描的共享 agent 配置根。 |
| `customSkillDirs` | `[]` | 在项目根之后、用户根之前扫描的其他本地 skill 根。 |
| `dshHome` | `$DSH_HOME` `~/.dsh` | 由 [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) 解析的 DeepSeek Harness 配置根目录;扫描该目录下的 `skills`。 |
| `agentsHome` | `$DSH_AGENTS_HOME` `~/.agents` | 为兼容 skill 扫描的共享 agent(智能体)配置根目录。 |
| `customSkillDirs` | `[]` | 在项目根目录之后、用户根目录之前扫描的其他本地 skill 根目录。 |
| `watch` | `true` | 监视宿主本地根,并在目录成员或 frontmatter 可能发生变化时使本地提供方失效。 |
| `watchUsePolling` | `false` | 对现有 skill 根使用 Chokidar 轮询,而不是原生事件。 |
| `watchStabilityThresholdMs` | `200` | Chokidar `add``change` 事件的稳定写入窗口。 |
@@ -36,7 +36,7 @@
| 400 | `user-dsh` | `<dshHome>/skills` |
| 500 | `user-agents` | `<agentsHome>/skills` |
项目根是包含 `.git` 的最近祖先;如果不存在,则使用当前 cwd。用户 DSH 根会跳过其 `.system`,因此系统所有目录不会被当作普通用户 skill。该提供方提供项目和用户 skill其他提供方可提供内置系统 skill。
项目根目录是包含 `.git` 的最近祖先目录;如果不存在,则使用当前 cwd。用户 DSH 根目录会跳过其 `.system`目录,因此系统所有目录不会被当作普通用户 skill。该提供方提供项目和用户 skill其他提供方可提供内置系统 skill。
`ctx.fs` 可用时,发现通过 `ctx.fs.listDir` 列出根,通过 `ctx.fs.readText` 读取 skill 文件,并通过文件系统服务探测 `.git`。完整 skill 加载会将查找中止信号转发给文件系统元数据和内容读取。如果没有文件系统服务,提供方回退到可中止的 Node 文件系统 I/O使最小本地上下文仍能加载 skill。已确认缺失的路径属于有效空状态格式错误或非文本条目会警告并跳过意外的发现或读取失败会使注册表快照不完整系统不会因此用看似发生删除的结果替换上一份可用模型目录。
@@ -50,21 +50,21 @@
## Skill 格式
Skill 可以是单层目录 bundle`<name>/SKILL.md`),也可以是平铺 Markdown 文件(`<name>.md`。v1 刻意不包含嵌套 `**/SKILL.md` 发现。Frontmatter 使用 `yaml` 包解析为 YAML它要求 `name``description`,而 `whenToUse``disableModelInvocation``metadata` 可选。名称必须使用 kebab-case。
Skill 可以是单层目录 bundle`<name>/SKILL.md`),也可以是平铺 Markdown 文件(`<name>.md`。v1 刻意不支持发现嵌套 `**/SKILL.md`。Frontmatter 使用 `yaml` 包解析为 YAML它要求 `name``description`,而 `whenToUse``disableModelInvocation``metadata` 可选。名称必须使用 kebab-case。
目录与正文具有独立的生命周期。发现阶段解析 frontmatter 以生成概述。每次 `skill(name)` 加载都会重新读取并解析当前文件,因此正文编辑不需要 hash、修订号、缓存失效或主动通知模型。若在发现与加载之间重命名 frontmatter系统会拒绝陈旧名称并使提供方失效下一次目录观察会发布新名称。
## 模型体验
通过 `dsh-tool-skill` 间接影响模型。它将该提供方的可调用名称和有上限描述渲染到初始目录或替换目录中,并将所选当前指令正文与资源基底指引渲染到保留工具历史中;路径、提供方 rank 和已禁用 skill 仍被隐藏。
通过 `dsh-tool-skill` 间接影响模型。它将该提供方的可调用名称和有长度上限描述渲染到初始目录或替换目录中,并将所选当前指令正文与资源基底指引渲染到保留工具历史中;路径、提供方 rank 和已禁用 skill 仍被隐藏。
#### KV 缓存影响
#### KV Cache 影响
watcher 触发的失效可促使指定的消费方在现有请求历史中追加替换目录。仅涉及正文的编辑不会改变目录 digest。
## 已知限制与待完成工作
## 已知限制与暂缓事项
- **发现深度为一层**:只识别 `<root>/<name>/SKILL.md``<root>/<name>.md`;忽略嵌套 skill 树和包 manifest。
- **发现深度为一层**:只识别 `<root>/<name>/SKILL.md``<root>/<name>.md`;忽略嵌套 skill 树和包 manifest(元数据清单)
- **项目范围为最近 `.git` 祖先**:没有该标记的工作区回退到提供的 cwd不支持其他项目根标记或 monorepo 子项目选择。
- **格式错误的条目会随警告消失**:模型目录不会收到每个 skill 的诊断,无法区分缺失的 skill 与无效的 skill意外 I/O 失败则会保留最后一份可用目录。
- **缺失根观察每次轮询一个路径段**:启动时不存在的根会使用 `fs.watchFile``watchPollIntervalMs` 轮询,直至 Chokidar 可以附加;这以有界检测延迟换取跨 IDE、Git 和 shell 工作流的可靠创建检测。

View File

@@ -504,7 +504,7 @@ class SkillWatchManager {
readiness.resolve(undefined)
})
for (const event of ['add', 'addDir', 'change', 'unlink', 'unlinkDir'] as const) {
watcher.on(event, (path) => { this.handleWatchEvent(state, mode, event, path) })
watcher.on(event, (path) => { this.handleWatchEvent(state, event, path) })
}
try {
await readiness.promise
@@ -519,11 +519,10 @@ class SkillWatchManager {
private handleWatchEvent(
state: RootWatchState,
mode: Extract<RootWatchMode, { kind: 'root' }>,
event: SkillWatchEvent,
path: string,
): void {
if (this.closing || !isRelevantWatchEvent(state.root, mode, event, resolve(path))) return
if (this.closing || !isRelevantWatchEvent(state.root, event, resolve(path))) return
this.queueInvalidation()
if (resolve(path) === state.root.path && event === 'unlinkDir') {
state.unhealthy = true
@@ -630,7 +629,6 @@ function sameWatchMode(left: RootWatchMode, right: RootWatchMode): boolean {
function isRelevantWatchEvent(
root: SkillRoot,
mode: Extract<RootWatchMode, { kind: 'root' }>,
event: SkillWatchEvent,
path: string,
): boolean {

View File

@@ -665,13 +665,17 @@ describe('LocalSkillProvider', () => {
})
return provider
})
expect((await provider.list({})).map(skill => skill.name)).toEqual(['disposed-skill'])
const beforeDisposal = await provider.list({})
expect((Array.isArray(beforeDisposal) ? beforeDisposal : beforeDisposal.candidates).map(skill => skill.name))
.toEqual(['disposed-skill'])
await provider.dispose()
await provider.dispose()
provider.observeHostMutation(join(home, '.agents/skills/disposed-skill/SKILL.md'))
expect((await provider.list({})).map(skill => skill.name)).toEqual(['disposed-skill'])
const afterDisposal = await provider.list({})
expect((Array.isArray(afterDisposal) ? afterDisposal : afterDisposal.candidates).map(skill => skill.name))
.toEqual(['disposed-skill'])
disposeProvider()
})