# dsh-skill-station

**DeepSeek Harness 的技能库、导入器与公开技能市场。**

[![npm version](https://img.shields.io/npm/v/dsh-skill-station?logo=npm&label=npm)](https://www.npmjs.com/package/dsh-skill-station)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Node.js](https://img.shields.io/badge/Node.js-22.19%20%7C%2024-339933?logo=node.js&logoColor=white)](#环境要求)
[![ci](https://github.com/WilShi/dsh-skill-station/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/WilShi/dsh-skill-station/actions/workflows/ci.yml)

[English](README.md) | 中文

技能站把 Agent 能用的技能收进一个页面：列出 DSH 家目录、共享 agents 目录和每个工作区下的全部技能；从 Claude Code、Codex CLI、Cursor、Gemini CLI 的现有技能库一键导入；也可以通过拖拽文件夹、ZIP、本地路径或公开 Agent Skills 市场安装新技能。公开市场不需要账号或令牌，入口在浏览器界面里，而凭据、网络请求和文件写入始终留在 Host 侧。

## 目录

- [截图](#截图)
- [功能](#功能)
- [环境要求](#环境要求)
- [安装](#安装)
- [使用技能站](#使用技能站)
- [技能市场](#技能市场)
- [配置](#配置)
- [实现方式](#实现方式)
- [安全模型](#安全模型)
- [实测证据](#实测证据)
- [目录结构](#目录结构)
- [开发](#开发)
- [已知限制](#已知限制)
- [许可证](#许可证)
- [致谢](#致谢)

## 截图

### 技能市场

![技能市场页：skills.sh 结果列表，带安装、目标根目录、冲突策略和版本选择](docs/screenshots/market.png)

*浏览 skills.sh 目录或按关键词搜索，选择目标根目录与冲突策略，再用版本选择器一键安装。*

### 安装结果

![安装完成的市场技能，显示本地路径与 SHA-256](docs/screenshots/install.png)

*安装完成后，面板会给出技能的本地目录和规范归档的 SHA-256；同一个摘要会写进技能旁的 `.dsh-skillhub.json`。*

### 技能库

![技能库页：两个已安装技能、启停开关和逐项操作](docs/screenshots/library.png)

*技能库按根目录分组。可以切换模型调用、查看详情、分配场景标签、导出 ZIP，或把技能移入回收站；宿主内置技能只读展示。*

截图来自本地 harness 调用真实 Host API 渲染，市场数据是 skills.sh 的线上数据。界面支持中文和英文，这几张截图使用中文界面。

## 功能

### 技能库

- 按作用域分组列出所有可写根目录下的技能：`$DSH_HOME/skills`（通常是 `~/.dsh/skills`）、`~/.agents/skills`，以及每个工作区的 `<workspace>/.dsh/skills` 和 `<workspace>/.agents/skills`。宿主内置技能单独成组，只读展示。
- 按名称和描述搜索；详情视图展示解析后的 frontmatter、完整的 `SKILL.md` 正文，目录型技能还有文件树。
- 直接编辑文本文件（包括 `SKILL.md`）；保存后返回重新解析的元数据，列表状态保持准确。
- 通过改写 frontmatter 里的调用开关（`disable-model-invocation`、`user-invocable`）启停技能，目录型 bundle 和扁平 `<name>.md` 都支持。
- 把任意技能导出为以技能名命名的 ZIP；重新导入该压缩包即可还原目录结构。
- 运行发现诊断，按宿主加载器的严格规则检查非法 YAML、缺少 `name` 或 `description`、非 kebab 名称、旧式 camelCase 开关键，并支持就地修复不规范的前置元数据。
- 用受校验的向导新建技能，生成合法的 kebab-case `SKILL.md`。
- 用场景标签给技能分组（例如「工程」「设计」）；归属关系保存在 `~/.dsh/skill-station/scenes.json`。
- 删除只是移入可恢复的回收站；可以恢复条目，也可以显式清空回收站。

### 从其他 Agent 导入

- 只读扫描 Claude Code、Codex CLI、Cursor、Gemini CLI 使用的技能库，绝不修改来源目录。
- 默认扫描位置：

  | Agent | 用户级目录 | 工作区目录 |
  | --- | --- | --- |
  | Claude Code | `~/.claude/skills` | `.claude/skills` |
  | Codex CLI | `~/.codex/skills` | `.codex/skills` |
  | Cursor | `~/.cursor/skills` | `.cursor/skills` |
  | Gemini CLI | `~/.gemini/antigravity/skills`、`~/.gemini/skills` | `.gemini/skills` |

- 可以导入到全局根目录，也可以导入到当前选中工作区的项目根目录。
- 同名冲突支持跳过、改名、替换三种策略。改名会同步改写 frontmatter 里的 name，避免导入副本遮蔽原技能；替换会先把旧版本移入回收站。
- 宿主技能 watcher 会发现新文件，导入后无需重启 DSH。

### 随处安装

- 把技能文件夹拖进面板、用文件选择器挑选文件夹、拖入或选择 ZIP，或粘贴 Host 本机上已有文件夹的绝对路径。
- ZIP 在 Host 侧解压；symlink 条目会被拒绝，所有路径在写入前都会校验。
- 浏览器安装会先校验、在目标根目录内暂存，再用原子 rename 落到最终位置。暂存或 rename 失败时会清理暂存目录，并从回收站恢复被替换的技能。
- 本地路径安装是磁盘到磁盘的流式复制，适合带 vendored 依赖树的大技能。

### 公开技能市场

- 默认对接 [skills.sh](https://skills.sh)：通过公开 sitemap 发现目录、通过公开搜索 API 做关键词搜索、从 GitHub 仓库归档安装。
- 市场身份沿用来源自身的唯一性规则，并成为安装后的技能名：skills.sh 用 `owner--repo--skillId`（例如 `anthropics--skills--pdf`，安装为 `anthropics-skills-pdf`）。
- 其他后端只需改配置：[OpenClaw 注册表](https://clawhub.ai)、静态 JSON 目录，或部署方自带的企业 Skill Hub。界面、安装管线、冲突策略和来源记录完全一致。
- 下载类技能支持跳过和替换两种冲突策略；改名会被拒绝并返回 `409`，因为归档自身已经定义了身份。
- 每个市场技能都带来源记录：provider、上游身份、版本、归档 SHA-256 和安装时间。

## 环境要求

- 支持插件机制的 DSH Web profile（0.2.x 系列）。本包 peer 依赖 `@deepseek-ai/cordis ^4.0.5` 与 0.2.1 的客户端 shell 包。
- Node.js 22.19 以上（22.x 系列），或 Node.js 24。
- DSH 支持的操作系统均可。路径遵循 DSH 约定，并识别 `$DSH_HOME`。

## 安装

把 bundle 加进你实际运行的 profile，然后重启该 profile：

```bash
dsh plugin --profile web add dsh-skill-station
# 重启 profile（例如 dsh web，或重新打开 Desktop）
```

需要移除时：

```bash
dsh plugin --profile web remove dsh-skill-station
```

这是一个标准 DSH bundle：`cordis.patch.yml` 会插入 Host 行来挂载 API，并插入 client 行来注册侧边栏按钮和设置分区；profile 会一直保留该插件，直到你主动移除。

DSH Desktop 同样按 profile 管理插件。如果你启动的 profile 里还没有技能站，用 `--profile desktop`（或你的 Desktop 构建实际使用的 profile 名）把同一个 bundle 装进去即可；已经内置该 bundle 的构建不需要额外安装。

安装后，从侧边栏底部的技能站按钮打开面板，或进入「设置 → 技能站」。

## 使用技能站

面板有五个标签页，顶部还有工作区选择器。选择器决定管理哪个工作区的项目根目录；全局根目录始终可用。

| 标签页 | 作用 |
| --- | --- |
| 技能库 | 浏览、搜索、启停、查看、编辑、导出、诊断、打标签；新建技能。 |
| Skill 市场 | 浏览已配置的市场、按关键词搜索、选择目标根目录与版本，安装单个技能。 |
| 导入技能 | 扫描外部 Agent 技能库，勾选候选项，按冲突策略导入。 |
| 安装技能 | 拖入文件夹、选择文件夹、上传 ZIP，或粘贴本地绝对路径。 |
| 回收站 | 恢复已删除的技能，或清空回收站。 |

可写根目录（技能库按此顺序展示）：

| 作用域 | 根目录 |
| --- | --- |
| 全局（用户） | `$DSH_HOME/skills`，默认 `~/.dsh/skills` |
| 共享（用户） | `~/.agents/skills` |
| 工作区 | `<workspace>/.dsh/skills` |
| 工作区（共享） | `<workspace>/.agents/skills` |

技能要么是目录型 bundle（`<name>/SKILL.md` 加附属资源），要么是直接放在根目录下的单个 Markdown 文件（`<name>.md`）。删除技能时，它总是先被移入 `~/.dsh/skill-station/trash`。

## 技能市场

### skills.sh（默认）

[skills.sh](https://skills.sh) 是一个公开的 Agent Skills 目录。它没有提供「一次列出全部技能」的 JSON，所以 Host 使用它公开暴露的三个入口：

| 环节 | 请求 | 说明 |
| --- | --- | --- |
| 发现 | `GET /sitemap.xml` → `GET /sitemap-skills-N.xml` | 最多 4 万个技能页，首次浏览时解析，缓存 10 分钟 |
| 搜索 | `GET /api/search?q=...` | 关键词搜索；结果行带仓库、技能 id 和安装次数 |
| 归档 | `GET https://codeload.github.com/<owner>/<repo>/zip/HEAD` | 技能内容来自仓库自身的归档 |

同一个技能 id 只在单个仓库内唯一，所以 skills.sh 身份保留三段坐标：`owner--repo--skillId`。`SKILL.md` 的 name 必须是 kebab-case，因此身份会被投影成合法名称，安装为 `anthropics-skills-pdf` 而不是 `anthropics--skills--pdf`。安装请求始终指定一个精确身份；Host 会校验下载到的归档确实发布了该技能，对格式合法但并不存在的身份返回 `404 NOT_FOUND`。

### 其他后端

| 配置 | 后端 | 发现 | 下载 | 身份 |
| --- | --- | --- | --- | --- |
| `market.mode: skills-sh`（默认） | skills.sh | sitemap 分片或搜索 | GitHub 仓库归档 | `owner--repo--skillId` |
| `market.mode: registry` | [clawhub.ai](https://clawhub.ai) | 游标目录或搜索 | 注册表 ZIP 端点 | `owner--slug` |
| `market.mode: catalog` | 静态 JSON 文档 | 单个目录文档 | GitHub、npm 或直链归档 | 条目 id |
| `market.provider: htsc` | 部署方提供的企业 Skill Hub | 部署方 API | 不可变版本 ZIP | 部署方技能名 |

静态目录默认读取 `https://awesome-dsh-plugin.com/plugins.json`。切换后端只是配置变化；面板、安装管线和安全检查共用同一套实现。

### 企业 Skill Hub

运行自有 Skill Hub 的部署，可以设置 `market.provider: htsc` 并填写只由 Host 读取的 `skillHub` 配置块，接入按组织授权的目录。本构建不内置任何默认 Hub 地址：base URL、Bearer 令牌和环境都必须由部署方提供。没有有效 Hub 配置时，市场页会给出可操作的不可用状态，技能库、导入和本地安装仍然正常工作。

### 冲突与来源记录

- 跳过：保留已存在的技能不动；替换：把旧技能移入回收站后安装新归档。
- 下载类安装不支持改名（`409`）：市场归档自带身份，改名后的副本将不再对应来源。
- 每次安装都会在技能旁写入 `.dsh-skillhub.json`：schema 版本、provider、市场身份、版本、归档 SHA-256 和安装时间。替换已安装的市场技能需要匹配的来源记录；本地技能或其他来源的技能不会被覆盖。

## 配置

市场默认零配置即可使用。下面所有字段都是可选的，写在 profile 配置中该插件的配置行下：

```yaml
plugins:
  dsh-skill-station:
    # 替换内置的外部 Agent 扫描列表（可选）。
    sources:
      - id: claude
        label: Claude Code
        userDirs: ['~/.claude/skills']
        projectDirs: ['.claude/skills']
    # 浏览器上传请求体上限，单位字节（64 MiB）。
    maxBodyBytes: 67108864
    market:
      # external（默认）或 htsc（部署方提供的企业 Skill Hub）。
      provider: external
      # skills-sh（默认）、registry 或 catalog。
      mode: skills-sh
      baseUrl: https://skills.sh
      catalogUrl: https://awesome-dsh-plugin.com/plugins.json
      npmRegistry: https://registry.npmjs.org
      maxPages: 8
      timeoutMs: 15000
      maxAttempts: 3
      catalogCacheTtlMs: 600000
    # 只由 Host 读取，不会下发到渲染进程；仅 htsc 需要。
    skillHub:
      baseUrl: https://skillhub.example.com/ai_skillhub_service
      apiToken: <部署方令牌>
      userId: <部署方用户身份>
      environment: uat
```

| 配置项 | 默认值 | 含义 |
| --- | --- | --- |
| `maxBodyBytes` | `67108864` | 浏览器请求体大小上限，单位字节。 |
| `sources` | Claude Code、Codex CLI、Cursor、Gemini CLI | 只读扫描的外部技能库。该列表会替换默认值，而不是追加上去。 |
| `market.provider` | `external` | `external` 使用公开市场，`htsc` 使用部署方 Skill Hub。 |
| `market.mode` | `skills-sh` | 公开市场形态：`skills-sh`、`registry` 或 `catalog`。 |
| `market.baseUrl` | 随 mode 变化 | 市场根地址；`skills-sh` 默认 `https://skills.sh`，`registry` 默认 `https://clawhub.ai`。 |
| `market.catalogUrl` | `https://awesome-dsh-plugin.com/plugins.json` | `catalog` 模式读取的目录文档。 |
| `market.npmRegistry` | `https://registry.npmjs.org` | 解析目录里 npm 包条目的 registry。 |
| `market.maxPages` | `8` | 每次浏览 registry 目录时翻取的页数。 |
| `market.timeoutMs` | `15000` | 单次网络请求超时，单位毫秒。 |
| `market.maxAttempts` | `3` | 可重试目录请求的尝试次数。 |
| `market.catalogCacheTtlMs` | `600000` | 目录缓存时长（10 分钟）。 |
| `skillHub.baseUrl` | — | 部署方提供的 Skill Hub 服务根地址。 |
| `skillHub.apiToken` | — | Hub 的 Bearer 令牌，只存在 Host 侧。 |
| `skillHub.userId` | — | 部署方未注入时可显式指定的用户身份。 |
| `skillHub.environment` | — | `dev`、`sit`、`uat` 或 `prd`。 |
| `skillHub.allowInsecureUat` | `false` | 是否允许 UAT Hub 使用明文 HTTP。 |
| `skillHub.timeoutMs` | `10000` | 单次 Hub 请求超时，单位毫秒。 |
| `skillHub.maxAttempts` | `3` | 可重试 Hub 请求的尝试次数。 |
| `skillHub.maxResponseBytes` | `67108864` | 从 Hub 读取的响应大小上限。 |
| `skillHub.backoffBaseMs` | `500` | Hub 请求的初始重试退避。 |

Hub 部署也可以通过环境变量 `SKILLHUB_BASE_URL`、`SKILLHUB_API_TOKEN`、`SKILLHUB_ENV`、`SKILLHUB_USER_ID` 提供这些值，而不写进配置行。配置和环境变量都由 Host 进程读取；渲染进程只会收到一个「市场是否已配置」的状态对象。

## 实现方式

### Host 与 Client 分界

本包由同一个 bundle patch 挂载两半：

- **Host**（`src/`）：Cordis 函数插件，名称 `dsh-skill-station`，inject `webServer`，在 `ctx.effect()` 内注册一条前缀路由 `/skill-station/api`，随插件卸载。它负责解析可写根目录、从磁盘枚举技能、校验每次写入是否落在根目录内，并执行所有网络访问。
- **Client**（`src/client/`）：一个浏览器 bundle，形态是 DSH Web shell 的 `window.__ModuleLoader__.load({ id, factory })`，其中 `factory` 是 CommonJS 闭包，`react`、`react-dom`、`@deepseek-ai/*` 从 shell 的模块表解析。它注册两个 slot：`sidebar.footer.action`（打开抽屉的按钮）和 `settings.section`（同样的面板内嵌到设置页），并注册中英文词典。

渲染进程只通过本地 API 与 Host 通信，永远拿不到市场归档、上游响应体、技能根之外的文件路径或 Skill Hub 令牌。

### HTTP API

所有路由位于 `/skill-station/api` 下并返回 JSON。变更类请求必须同源，POST 请求体必须声明 `application/json`。

| 方法 | 路由 | 用途 |
| --- | --- | --- |
| GET | `/roots` | 工作区与已配置的外部来源 |
| GET | `/skills` | 按根目录分组的技能，以及宿主注册表视图 |
| GET | `/skill` | 单个技能的元数据、正文和文件树 |
| GET | `/file` | 技能内的单个文本文件 |
| GET | `/export` | 下载单个技能的 ZIP |
| GET | `/diagnose` | 会被宿主加载器忽略的技能及原因 |
| GET | `/scenes` | 场景标签归属 |
| GET | `/trash` | 当前回收站条目 |
| GET | `/market/status` | 市场 provider 与配置状态 |
| GET | `/market/skills` | 市场目录或关键词搜索 |
| POST | `/scan` | 扫描外部 Agent 技能库 |
| POST | `/import` | 导入选中的外部技能 |
| POST | `/upload` | 安装拖入或选择的文件集 |
| POST | `/upload-zip` | 安装上传的 ZIP 归档 |
| POST | `/market/install` | 安装单个市场技能 |
| POST | `/toggle` | 启停技能 |
| POST | `/delete` | 把技能移入回收站 |
| POST | `/trash-restore` | 恢复回收站条目 |
| POST | `/trash-empty` | 永久清空回收站 |
| POST | `/scenes/assign`、`/scenes/unassign` | 修改场景标签 |
| POST | `/save-file` | 保存编辑过的文件 |
| POST | `/repair` | 修复不规范的前置元数据 |
| POST | `/scaffold` | 通过向导新建技能 |

### 安装管线

所有安装、导入和市场下载都走同一条路径：

1. **校验。** 解析传入的 `SKILL.md`，要求 kebab-case 的 `name` 和非空 `description`；在严格的宿主加载器看到文件之前，先修复常见的真实世界 frontmatter 损坏（例如 description 里未加引号的冒号）。
2. **确定名称。** 应用冲突策略：跳过、改名（仅导入）或经回收站替换。
3. **暂存。** 把完整文件集写入目标根目录内的隐藏 `.staging-<uuid>` 目录，逐项检查路径包含性、单文件大小和总大小。复制时跳过 symlink，归档中出现 symlink 直接拒绝。
4. **复核。** 重新读取暂存区里的 `SKILL.md`，确认元数据合法后才改动目标位置。
5. **提交。** 用一次原子 rename 把暂存目录移到目标路径。
6. **回滚。** 如果替换操作已把旧技能移入回收站，之后任何一步失败都会恢复回收站条目，并清理暂存目录。

### 归档规范化

不同市场返回的形态不同：GitHub 仓库归档、注册表 ZIP、npm tarball 或直链归档。在归档进入安装器之前，Host 会把它重建为规范形态：

1. 解码 ZIP 或 gzip 压缩的 tarball，丢弃绝对路径、`..` 段、控制字符和 Windows 设备名的条目。
2. 去掉唯一的顶层文件夹，然后按 `skills/<skillId>/SKILL.md`、`<skillId>/SKILL.md`、最浅层 `SKILL.md` 的顺序定位技能目录。skills.sh 模式要求身份匹配，因此发布不出该技能的归档会被拒绝，而不是装进一个无关的模板。
3. 只保留该技能根目录下的文件，重新生成一个以技能目录为根的 ZIP，并在根 `SKILL.md` 的 name 与市场身份不一致时改写它。
4. 限制归档规模（5,000 个条目、单文件 32 MiB、总计 64 MiB），再交给普通安装管线。

## 安全模型

- **仅同源。** 所有变更请求和所有 `/market/*` 请求必须来自同源或非浏览器调用；跨源 `Origin` 会在任何工作开始前被 `403` 拒绝。
- **仅 JSON。** 未声明 `application/json` 的 POST 请求会被 `415` 拒绝，从而挡住简单的跨站表单提交。
- **路径包含。** 每个写入目标都会规范化并检查是否落在对应根目录内。上传路径拒绝绝对路径、`..`、反斜杠、控制字符和 Windows 设备名；文件查看与保存被限制在技能目录内。
- **不跟随 symlink。** 目录复制、导出和归档解码会跳过或拒绝符号链接，技能根目录本身必须是真实目录。
- **有界的工作量。** 默认请求体上限 64 MiB。浏览器 ZIP 解压上限为 10,000 个条目、总计 256 MiB；安装文件单文件上限 64 MiB、总计 256 MiB。市场归档更严格：5,000 个条目、单文件 32 MiB、总计 64 MiB。文件查看在 512 KiB 处截断，就地编辑上限 2 MiB。
- **令牌隔离。** Skill Hub 令牌只从 Host 配置或环境读取，永远不会被序列化进客户端 bundle 或渲染进程响应；市场错误会映射成稳定、脱敏的错误码。
- **身份校验。** 市场归档必须确实发布了请求的技能身份；安装来源记录也会在替换时校验，避免不同 provider 互相覆盖。
- **诚实的信任边界。** 公开市场对所有人开放，上游只有自动审核。记录的 SHA-256 只是对重建归档的本地完整性校验，不是签名，也不是信任判断。启用前请审阅第三方技能说明。DSH 技能本身就是可执行的指令和资源。

## 实测证据

0.2.1 的 Host API 在真实 HTTP 服务器和真实网络（skills.sh 搜索与 codeload.github.com 归档）上跑过，`$HOME` 隔离到临时目录：

| 检查项 | 结果 |
| --- | --- |
| Host API 端到端套件 | **22 / 22 用例通过**，70 条断言，0 失败（Node.js 22.22.3，darwin-arm64） |
| 真实关键词搜索 | `GET /market/skills?keyword=pdf` 1.7 秒返回 6 条；首条 `anthropics--skills--pdf`（测试时 222,304 次安装） |
| 真实安装 | `anthropics/skills` 的 `pdf` 归档（4,317,426 B，543 个条目）8.0 秒安装完成：写入 13 个文件（12 个技能文件加来源记录），`SKILL.md` 8,101 B |
| 内容来源 | 12 个 payload 文件中 11 个与 GitHub 归档逐字节相同；`SKILL.md` 仅 frontmatter 的 `name` 行不同，归一化该行后与上游一致；磁盘上没有多余文件 |
| 生命周期 | 关闭、打开、删除、回收站、恢复全部返回 200；恢复后 `SKILL.md` 的 SHA-256 不变，回收站为空 |
| 反向用例 | 未知技能 `404`、跨源请求 `403`、非 JSON 类型 `415`、非法分页或参数 `400`、未知根目录 `400`、下载改名 `409` |
| 身份校验 | 格式合法但未发布的 skills.sh 身份会被 `404 NOT_FOUND` 拒绝 |
| 隔离性 | 真实 `~/.dsh` 在运行前后逐字节一致 |

测试 harness 从源码重新构建 Host API，用真实 `node:http` 请求驱动，并对照单独下载的上游归档核对落盘内容。网络耗时因机器和线路而异；上面的计数和摘要来自该次运行。

## 目录结构

```text
.
├── src/
│   ├── index.ts             # Host 插件入口：挂载 HTTP 路由
│   ├── api.ts               # 路由分发、请求校验、安装编排
│   ├── external-market.ts   # skills.sh / registry / catalog 客户端与归档规范化
│   ├── skillhub.ts          # 企业 Skill Hub 客户端（令牌只在 Host 侧）
│   ├── roots.ts             # 可写根目录、路径包含、磁盘枚举
│   ├── scanner.ts           # 只读的外部 Agent 技能库扫描
│   ├── importer.ts          # 冲突策略、暂存安装、回滚
│   ├── zip.ts               # 上传 ZIP 解码与大小限制
│   ├── frontmatter.ts       # YAML frontmatter 解析、修复、开关、改名
│   ├── diagnose.ts          # 按宿主加载器规则做发现诊断
│   ├── export.ts            # ZIP 导出
│   ├── trash.ts             # 回收站清单、恢复、清空
│   ├── scenes.ts            # 场景标签持久化
│   └── client/              # 浏览器 bundle：entry、App、MarketTab、api、locales、styles
├── docs/screenshots/        # market.png、install.png、library.png
├── scripts/                 # build-client.mjs、verify-client.mjs、smoke.mjs
├── tests/                   # Vitest 规格
├── cordis.patch.yml         # 激活插件行的 bundle patch
├── MARKET.md                # 市场 provider 参考
├── DESIGN.md                # 架构说明
└── .github/workflows/ci.yml # Node 22/24 构建、测试、冒烟
```

## 开发

```bash
npm install
npm run build     # Host TypeScript、客户端 bundle 与 bundle 形态校验
npm test          # Vitest 单元与组合测试
npm run smoke     # 启动构建产物并逐项调用所有端点
```

仓库不带 lockfile，因此 CI 使用 `npm install` 而不是 `npm ci`。构建产物写入 `lib/`，该目录已被 git 忽略；客户端构建会被包装成 `window.__ModuleLoader__.load`，并由 `scripts/verify-client.mjs` 把关（banner、插件 id、shell 模块是否 external、以及在沙箱里试跑 factory）。`npm run typecheck` 只做类型检查，不产出文件。

CI 使用 Node.js 22/24 矩阵：安装、构建、单元测试、构建产物冒烟测试。

## 已知限制

- 公开市场不提供上游签名。本地摘要能发现归档被改动，但不能证明是谁构建的；启用技能前请审阅第三方说明。
- 下载类技能不能在安装时改名（`409`）；需要不同名称时，先卸载或重命名已有技能。
- 浏览器上传必须经过 JSON 请求体，超大技能建议改用本地路径安装。
- 企业 Skill Hub 需要部署方提供配置；没有配置时市场页会显示不可用，但本地技能库和导入功能不受影响。
- 市场后端是第三方服务，可能调整接口或限流；`429` 会作为错误返回，目录缓存 10 分钟。
- 记录的 SHA-256 覆盖的是重建后的归档。安装时会重建归档，因此这个摘要代表该次安装的完整性记录，而不是可复现的上游摘要。

## 许可证

[MIT](LICENSE)

## 致谢

- [skills.sh](https://skills.sh) —— 默认市场使用的公开 Agent Skills 目录。
- [clawhub.ai](https://clawhub.ai) —— OpenClaw 注册表后端。
- [awesome-dsh-plugin.com](https://awesome-dsh-plugin.com) —— 默认静态目录。
- DSH 团队提供的宿主技能注册表、Web 服务、slot 与本地化 API。

