---
name: dsh-plugin-dev
description: 开发、打包、更新 DeepSeek Harness 云端插件，并提交到 addon.dshdo.com 审核上架。当用户要写 dsh 插件、打 tar.gz、发布/更新目录插件、或审核 pending 队列时使用。
---

# DeepSeek Harness 插件开发与发布

目录：`https://addon.dshdo.com`。浏览器只看说明；安装发生在有 `deepseek-harness` 的机器上。

**不要把 `ADMIN_TOKEN`、cookie、密码写进聊天、skill、仓库或 tarball。** 管理员 token 只在服务器 `/etc/dsh-plugin-catalog/env`。审核页用这个 token 登录，不要向用户回显它。

## 何时用哪条路

- **新 id 开发者投稿**：打包 → `POST /publish`（或打开 `/publish`）。结果是 pending，不会立刻 listed；同一 id 再次投稿需要管理员（见「更新已上架插件」）。
- **管理员审核**：打开 `/admin`，用服务器 env 里的 token 登录，对**确切版本号**通过 / 拒绝；已上架插件可下架。
- **本机安装市场**：`curl -fsSL https://addon.dshdo.com/install-plugin-market.sh | sh`，然后重启 `dsh web`。
- **本机从市场装插件**：`/plugin-market/` 里点安装（只登记）再启用。
- **详情页**：人看 `GET /plugins/<id>`（无 JS）；Agent 看 `GET /plugins/<id>.md` 或 `GET /v1/plugins/<id>`（含 changelog、versions、readme、urls）。包根 `README.md` / `README.zh.md` 在投稿时写入版本记录。

## 插件形态

最小文件：

```
my-plugin/
  package.json     name === 目录 id（或 scoped + dsh-catalog.json）
  index.js         ESM；export name / inject / apply
```

`package.json`：

```json
{
  "name": "my-plugin",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "main": "index.js",
  "description": "一句话"
}
```

`index.js` 跑在 dsh web 进程里。需要官方服务就声明 `export const inject = ['webServer']` 等。不要依赖仓库外的 `../lib`；需要共享代码就打进包。

目录 id：`^[a-z0-9][a-z0-9._-]*$`，无 scope、无冒号。  
版本：`0.1.0` 或 `0.1.0-lan.2`。同一 id+version 不能重复提交（tarball 与版本行不可变），更新必须升版本。`package.json` 里的 `version` 必须与投稿表单里的版本一致，否则 400。

若 `package.json name` 是 scoped（例如 `@deepseek-ai/dsh-host-access-gateway`），同目录放 `dsh-catalog.json`：

```json
{
  "id": "dsh-access-gateway",
  "packageName": "@deepseek-ai/dsh-host-access-gateway",
  "loaderId": "access-gateway"
}
```

禁止：路径穿越、绝对路径、密钥、`.env`、本机 `node_modules`。tarball ≤ 50MiB。

## 从目录拉已上架源码

```sh
ID=hello-catalog
VER=$(curl -fsSL https://addon.dshdo.com/v1/plugins/$ID | python3 -c 'import json,sys; print(json.load(sys.stdin)["value"]["version"])')
mkdir -p /tmp/$ID && curl -fsSL "https://addon.dshdo.com/v1/plugins/$ID/versions/$VER/tarball" | tar -xzf - -C /tmp/$ID
```

未 listed 的 pending 包没有公开 tarball。

## 打包

在插件根目录（能看见 `package.json` 的那一层）：

```sh
tar -czf /tmp/${ID}-${VER}.tar.gz .
```

包里应有 `./package.json`，不要先包一层无意义的父目录。可用 `tar -tzf` 检查。

## 提交审核（开发者）

表单：https://addon.dshdo.com/publish

或：

```sh
curl -fsSL -F "id=$ID" -F "name=$NAME" -F "developer=$DEV" \
  -F "version=$VER" -F "description=$DESC" -F "changelog=$LOG" \
  -F "tarball=@/tmp/${ID}-${VER}.tar.gz" \
  https://addon.dshdo.com/publish
```

成功会说已提交、等待审核。公开 `GET /v1/plugins` 此时还看不到它。

**已存在的 id 不能再走公开 `/publish`**：返回 409，且不会改动已上架信息。更新已上架插件要由管理员走 `/v1/admin/plugins`（见下）。

## 审核与上架（管理员）

1. 在**运行 catalog 的那台机器**读取管理 token：环境文件 `/etc/dsh-plugin-catalog/env` 里的 `ADMIN_TOKEN` 键。不要把值打印到用户可见日志。
2. 打开 https://addon.dshdo.com/admin 粘贴 token 登录（cookie 12 小时，Secure）。
3. 队列按版本列出，每个条目显示确切的 `id@version`、插件当前状态、提交时声明的信息。**通过/拒绝只作用于这个确切版本**——不会顺手把同 id 更新的待审版本一起通过。已上架（当前指针所在）版本不能直接拒绝，先下架。
4. CLI 等价（token 只放环境变量）。**approve/reject 必须带 `--version`，防止误通过最新待审版本**：

```sh
# 在 catalog 服务器或已注入 ADMIN_TOKEN 的 priv 环境
export CATALOG_URL=https://addon.dshdo.com
# ADMIN_TOKEN 已在环境中，不要 echo
bun /opt/dsh-plugin-catalog/scripts/admin.ts approve --id "$ID" --version "$VER"
# 或 reject --id "$ID" --version "$VER"；unlist --id "$ID"
```

管理员也可用 `scripts/admin.ts publish` 走 `/v1/admin/plugins`（仍进 pending，还要 approve）。已存在 id 的更新只能走这条（或审核页登录后的表单），不会改动已上架的 plugins 行与当前版本。不要把这条路径教给普通开发者。

## 更新已上架插件

1. 改代码，**升高** `package.json version`。
2. 重新 `tar -czf`。
3. 由**管理员**在运行 catalog 的机器上用 `scripts/admin.ts publish --file ... --id $ID --version $VER`（等价 `POST /v1/admin/plugins`），新版本进 pending；旧版本保持 listed 与可下载，公开列表不变。
4. 管理员对确切版本 approve 后指针前移，新版本才对外可见；旧版本仍保留为 approved 历史。本机市场对 `origin=catalog` 的目录才能点「更新」。已存在的自研目录不会被覆盖。

## 本机验证

有 harness 时：把目录放到 `~/dsh-plugins/<id>/`，在 `/plugin-market/` 启用。访问网关等特殊插件的设置走市场专用面板，不要把密码写进 `settings.yaml`。

## 链接

- 首页 https://addon.dshdo.com/
- 帮助 https://addon.dshdo.com/help
- 投稿 https://addon.dshdo.com/publish
- 审核 https://addon.dshdo.com/admin
- JSON `GET /v1/plugins`
- 详情 HTML `GET /plugins/<id>` · Markdown `GET /plugins/<id>.md` · JSON `GET /v1/plugins/<id>`
