diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fb2525..f379521 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,3 +35,24 @@ jobs: - name: Render test run: python scripts/render_test.py ./_ci_themes/ci-smoke --output-dir ./_ci_output if: matrix.engine == 'jinja2' + + check-lib-sync: + name: Check lib/ is in sync with src/ + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: "20" + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Compile TypeScript + run: npx tsc -p tsconfig.json + + - name: Verify lib/ matches committed output + run: git diff --exit-code lib/ diff --git a/.gitignore b/.gitignore index 785a992..8a788d3 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,7 @@ themes/ # 本地示例/草稿(如需提交 examples/,请从 .gitignore 中移除对应路径) scratch/ tmp/ + +# Node.js +node_modules/ +npm-debug.log* diff --git a/CHANGELOG.md b/CHANGELOG.md index 9220246..8b271be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ ## [Unreleased] +### Added + +**2026-08-20 · DSH (DeepSeek Harness) 插件支持** + +- 新增 `src/index.ts`:基于官方 `skill-badge` 模式,通过 `ctx.skills.registerProvider()` 注册 `gridea-theme-builder` skill provider,`resourceBase` 指向 bundle 根目录,模型可解析 `references/`、`scripts/`、`assets/` 的相对路径。 +- `description` 运行时从 `SKILL.md` frontmatter 自动提取,无需在代码中维护两份。 +- 新增 `package.json`、`tsconfig.json`、`cordis.patch.yml`、`overlay.yml` 等插件配置文件。 +- 新增 `src/README.md`:DSH 插件安装、测试、卸载的完整文档。 +- CI 新增 `check-lib-sync` job:检查 `lib/` 编译产物与 `src/` 源码是否同步。 +- 原有 Claude Skill 用法不受影响,所有 Skill 内容文件未改动。 + ### Fixed / Changed **2026-08-20 · 开源协议由 GPL-3.0 改为 MIT** diff --git a/README.md b/README.md index 6ecec50..766c171 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,10 @@ CustomConfig 用 index 访问,跑通 validate 和 render 测试。 > `CLAUDE.md` 是 Claude Code 专属的元指令文件,其他 Agent 与人类用户可忽略。 +## 作为 DSH 插件使用 + +本 Skill 也可作为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件运行,安装方式和说明详见 [src/README.md](src/README.md)。 + ## 开发环境 ```bash diff --git a/cordis.patch.yml b/cordis.patch.yml new file mode 100644 index 0000000..0a00d15 --- /dev/null +++ b/cordis.patch.yml @@ -0,0 +1,11 @@ +# Register the gridea-theme-builder skill provider in the DSH profile. +# +# Install (from any DSH workspace): +# dsh plugin --profile web add "github:Gridea-Pro/theme-builder-skill" +# +# The plugin's apply() registers a skill provider on ctx.skills; +# tool-skill then publishes the skill to the model-facing catalog. + +- insert: + - id: gridea-theme-builder + name: '@gridea-pro/dsh-skill-theme-builder' diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..2a61224 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,112 @@ +/** + * Gridea Pro theme builder skill provider for DeepSeek Harness. + * + * Registers the `gridea-theme-builder` skill from the bundled SKILL.md. + * The resourceBase points to the plugin bundle directory so the model can + * resolve references/, scripts/, and assets/ relative paths mentioned in + * the skill body. + * + * Pattern follows the official @deepseek-ai/dsh-skill-badge plugin: + * - registerProvider() with a static SkillProvider + * - list() and get() both read SKILL.md at call time, so edits to the + * frontmatter description and to the body are picked up without a rebuild + * + * @module @gridea-pro/dsh-skill-theme-builder + */ +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { BUNDLED_SKILL_RANK, } from '@deepseek-ai/dsh-skill'; +/** Absolute URL to the bundled SKILL.md body file. */ +const SKILL_BODY_URL = new URL('../SKILL.md', import.meta.url); +/** + * Directory base for relative resource resolution. + * The model receives this path in the block and resolves + * references/scripts/assets paths against it. + */ +const RESOURCE_BASE = { + kind: 'directory', + path: fileURLToPath(new URL('../', import.meta.url)), +}; +/** Skill is available on both model and user invocation surfaces. */ +const INVOCATION = { modelInvocable: true, userInvocable: true }; +/** Used only when SKILL.md is unreadable or carries no description. */ +const FALLBACK_DESCRIPTION = 'Gridea Pro 博客主题开发专家'; +/** + * Split a Markdown file into its frontmatter `description` and its body. + * + * Handles both YAML forms the description may take: + * - block scalar (`>` or `|`) followed by indented lines, folded into one line + * - plain single-line value + * + * The block-scalar branch must not depend on anything following it: in this + * skill `description` is the last frontmatter field, and the closing `---` is + * already consumed by the outer match. + */ +function parseFrontmatter(raw) { + const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); + if (!match) + return { description: FALLBACK_DESCRIPTION, body: raw }; + const frontmatter = match[1]; + const body = match[2]; + const block = frontmatter.match(/^description:[ \t]*[>|][-+]?[ \t]*\r?\n((?:[ \t]+.*(?:\r?\n|$))+)/m); + if (block) { + const folded = block[1].split(/\r?\n/).map((line) => line.trim()).filter(Boolean).join(' '); + if (folded) + return { description: folded, body }; + } + const plain = frontmatter.match(/^description:[ \t]*(\S.*?)[ \t]*$/m); + return { description: plain ? plain[1] : FALLBACK_DESCRIPTION, body }; +} +/** Candidate shape shared by list() and get(); `description` is filled in from SKILL.md. */ +const CANDIDATE = { + name: 'gridea-theme-builder', + invocation: INVOCATION, + provider: 'gridea-theme-builder', + source: 'bundled', + resourceBase: RESOURCE_BASE, + rank: BUNDLED_SKILL_RANK, + locator: SKILL_BODY_URL, +}; +const provider = { + name: 'gridea-theme-builder', + /** + * The catalog description is the model's only routing signal — `get()` runs + * only after the model has already chosen this skill — so the full + * frontmatter description (trigger conditions and keywords included) has to + * be resolved here, not deferred to load time. + * + * An unreadable SKILL.md degrades to the fallback description instead of + * throwing, so one broken bundle cannot empty the whole catalog. + */ + async list() { + let description = FALLBACK_DESCRIPTION; + try { + description = parseFrontmatter(await readFile(SKILL_BODY_URL, 'utf8')).description; + } + catch { + // 保底:读不到就用兜底描述,不让整个 skill 目录塌掉 + } + return [{ ...CANDIDATE, description }]; + }, + async get() { + const raw = await readFile(SKILL_BODY_URL, 'utf8'); + const { description, body } = parseFrontmatter(raw); + return { + name: CANDIDATE.name, + description, + invocation: CANDIDATE.invocation, + provider: CANDIDATE.provider, + source: CANDIDATE.source, + resourceBase: RESOURCE_BASE, + content: body, + }; + }, +}; +/** Cordis plugin name. */ +export const name = 'gridea-theme-builder'; +/** Required capability seam: the skills registry. */ +export const inject = ['skills']; +/** Register the bundled gridea-theme-builder skill provider on ctx.skills. */ +export function apply(ctx) { + ctx.skills.registerProvider(() => provider); +} diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts new file mode 100644 index 0000000..f0f7af2 --- /dev/null +++ b/lib/types/index.d.ts @@ -0,0 +1,22 @@ +/** + * Gridea Pro theme builder skill provider for DeepSeek Harness. + * + * Registers the `gridea-theme-builder` skill from the bundled SKILL.md. + * The resourceBase points to the plugin bundle directory so the model can + * resolve references/, scripts/, and assets/ relative paths mentioned in + * the skill body. + * + * Pattern follows the official @deepseek-ai/dsh-skill-badge plugin: + * - registerProvider() with a static SkillProvider + * - list() and get() both read SKILL.md at call time, so edits to the + * frontmatter description and to the body are picked up without a rebuild + * + * @module @gridea-pro/dsh-skill-theme-builder + */ +import type { Context } from '@deepseek-ai/cordis'; +/** Cordis plugin name. */ +export declare const name = "gridea-theme-builder"; +/** Required capability seam: the skills registry. */ +export declare const inject: string[]; +/** Register the bundled gridea-theme-builder skill provider on ctx.skills. */ +export declare function apply(ctx: Context): void; diff --git a/overlay.yml b/overlay.yml new file mode 100644 index 0000000..72e8feb --- /dev/null +++ b/overlay.yml @@ -0,0 +1,20 @@ +# 本地开发 overlay —— 用 --patch 把插件源码直接插入 Web UI +# +# 用法: +# 1. 复制本文件为 overlay.yml(不要改原文件,或改了也行) +# 2. 把下面 name 改成你机器上 src/index.ts 的绝对路径 +# 3. 运行: +# +# Windows: npx @deepseek-ai/dsh web --patch D:/path/to/overlay.yml +# macOS: npx @deepseek-ai/dsh web --patch /Users/you/path/to/overlay.yml +# Linux: npx @deepseek-ai/dsh web --patch /home/you/path/to/overlay.yml +# +# name 格式: +# Windows: file:///D:/theme-builder-skill/src/index.ts (需要 file:// 前缀) +# macOS: /Users/you/theme-builder-skill/src/index.ts (裸路径即可) +# Linux: /home/you/theme-builder-skill/src/index.ts (裸路径即可) +# +# 首次使用前需在项目目录运行 npm install 安装依赖。 +- insert: + - id: gridea-theme-builder + name: 'REPLACE_WITH_ABSOLUTE_PATH_TO_SRC/index.ts' diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ace8983 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,202 @@ +{ + "name": "@gridea-pro/dsh-skill-theme-builder", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@gridea-pro/dsh-skill-theme-builder", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@deepseek-ai/cordis": ">=0.0.1-rc.0", + "@deepseek-ai/dsh-skill": ">=0.0.1-rc.0", + "@types/node": "^20.14.0", + "typescript": "^5.5.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": ">=0.0.1-rc.0", + "@deepseek-ai/dsh-skill": ">=0.0.1-rc.0" + } + }, + "node_modules/@deepseek-ai/cordis": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@deepseek-ai/cordis/-/cordis-4.0.1.tgz", + "integrity": "sha512-YBdskTU2Po1kru3GgcUWUbkTsPMA9LkSQDAY8rBkFJeajdgcQad3QPJZE26JyK99Xb6HaASvoXg2DSUTeN/0Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@deepseek-ai/cosmokit": "^1.8.2", + "@standard-schema/spec": "^1.1.0" + }, + "bin": { + "cordis": "bin.js" + }, + "peerDependencies": { + "@deepseek-ai/cordis-plugin-include": "^1.0.6", + "@deepseek-ai/cordis-plugin-loader": "^1.0.2" + }, + "peerDependenciesMeta": { + "@deepseek-ai/cordis-plugin-include": { + "optional": true + }, + "@deepseek-ai/cordis-plugin-loader": { + "optional": true + } + } + }, + "node_modules/@deepseek-ai/cosmokit": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@deepseek-ai/cosmokit/-/cosmokit-1.8.2.tgz", + "integrity": "sha512-muBOKtSrUKU5m/xpq8ZXWL6hQ/jgd4PhU2PqH97bcxIiLEJfNwZOGQEx4t/aS/GgxRAR+ra9pMHPMtTHU4sqqA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@deepseek-ai/dsh-attachment": { + "version": "0.0.1-rc.1", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-attachment/-/dsh-attachment-0.0.1-rc.1.tgz", + "integrity": "sha512-zBBw6h+YKOf7U8uEZezbFUjLsA8VU1GfCIUonAZHG2hWDWhWbrGaoO9fL8KyncEDSWMLMoIoGo+ZxDEPVJZYOA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1-rc.1", + "@deepseek-ai/dsh-brand": "^0.0.1-rc.1", + "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1" + } + }, + "node_modules/@deepseek-ai/dsh-brand": { + "version": "0.0.1-rc.1", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-brand/-/dsh-brand-0.0.1-rc.1.tgz", + "integrity": "sha512-XhwNAugG/baCVA+BMMvjihYi9XMLcDUew3G63i+ATWIZCvpx8CghbgcV93DEeMH2sqhNuQAssmHgXkpRld3gyQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1-rc.1", + "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1" + } + }, + "node_modules/@deepseek-ai/dsh-invariants": { + "version": "0.0.1-rc.1", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-invariants/-/dsh-invariants-0.0.1-rc.1.tgz", + "integrity": "sha512-ZoGCGColviu3Tkdnv9hMkgBE4eg0hAz5GXR/knqRf7mJUeBlTCya2dIckYlqekYmp5L+1AeWxf9j+Lv1YRcvXw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1-rc.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1-rc.1" + } + }, + "node_modules/@deepseek-ai/dsh-llm": { + "version": "0.0.1-rc.1", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-llm/-/dsh-llm-0.0.1-rc.1.tgz", + "integrity": "sha512-mRJj07IQNfRReGrhpMpL8AONiYKqfwzxvepg/ut9Wkj8oH2BuoftPS4cMnnXFFCUlYql04/sezwDLriYks/bmw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1-rc.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1-rc.1", + "@deepseek-ai/dsh-attachment": "^0.0.1-rc.1", + "@deepseek-ai/dsh-brand": "^0.0.1-rc.1", + "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1", + "@deepseek-ai/dsh-timeout": "^0.0.1-rc.1" + } + }, + "node_modules/@deepseek-ai/dsh-scope": { + "version": "0.0.1-rc.1", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-scope/-/dsh-scope-0.0.1-rc.1.tgz", + "integrity": "sha512-XJa74NACc/285kF7Tak2nA+dlNpSTN77cV9SMhl75Bzu1y7p9+VgSMJfd6ydBsYVtDcAwzbon4wSS1u9AiI5VQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1-rc.1", + "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1" + } + }, + "node_modules/@deepseek-ai/dsh-skill": { + "version": "0.0.1-rc.1", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-skill/-/dsh-skill-0.0.1-rc.1.tgz", + "integrity": "sha512-YFBnqfZqBbid9iZpopgtXhbPzWOroj2s/DldaJ1xTWpQVyVVT2UOTDFRgxVAHkuZJMy79iv0a8br3Hu+3Gcl0Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1-rc.1" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1-rc.1", + "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1", + "@deepseek-ai/dsh-llm": "^0.0.1-rc.1", + "@deepseek-ai/dsh-scope": "^0.0.1-rc.1" + } + }, + "node_modules/@deepseek-ai/dsh-timeout": { + "version": "0.0.1-rc.1", + "resolved": "https://registry.npmjs.org/@deepseek-ai/dsh-timeout/-/dsh-timeout-0.0.1-rc.1.tgz", + "integrity": "sha512-lIMhY/JqA+eLpXXUN33xA7mlAYeHTDTWfcJ3CmEtplvGnvpDAHQ6oPeFs9JtWcfKbuNDZFMNutyhD0qoRkFQeg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1-rc.1", + "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1" + } + }, + "node_modules/@deepseek-ai/schemastery": { + "version": "3.18.1", + "resolved": "https://registry.npmjs.org/@deepseek-ai/schemastery/-/schemastery-3.18.1.tgz", + "integrity": "sha512-Qn0FCSwCQnpnj6SB31I6i2sIKgKWnkbJM8O0EU91Gv2UsYVvtZTl6IA0sCwk2e2MZf5S8w5hpq9QkeVvK9qwxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@deepseek-ai/cosmokit": "^1.8.2", + "@standard-schema/spec": "^1.1.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..650a14b --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "@gridea-pro/dsh-skill-theme-builder", + "description": "Gridea Pro theme builder skill for DeepSeek Harness", + "version": "0.1.0", + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/types/index.d.ts", + "SKILL.md", + "references", + "scripts", + "assets", + "requirements.txt", + "cordis.patch.yml" + ], + "license": "MIT", + "dsh": { + "bundle": { + "patch": "./cordis.patch.yml" + } + }, + "scripts": { + "build": "tsc -p tsconfig.json" + }, + "peerDependencies": { + "@deepseek-ai/dsh-skill": ">=0.0.1-rc.0", + "@deepseek-ai/cordis": ">=0.0.1-rc.0" + }, + "devDependencies": { + "typescript": "^5.5.0", + "@types/node": "^20.14.0", + "@deepseek-ai/dsh-skill": ">=0.0.1-rc.0", + "@deepseek-ai/cordis": ">=0.0.1-rc.0" + } +} diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..e70bb8f --- /dev/null +++ b/src/README.md @@ -0,0 +1,169 @@ +# DSH Plugin — Gridea Theme Builder + +本目录包含将 `theme-builder-skill` 作为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件运行所需的全部文件。 + +## 文件说明 + +| 文件 | 作用 | +|---|---| +| `src/index.ts` | 插件入口:注册 `gridea-theme-builder` skill provider | +| `src/README.md` | 本文档 | +| `package.json` | 插件清单,声明 `dsh.bundle` 和依赖 | +| `cordis.patch.yml` | bundle 模式的 patch 层(`dsh plugin add` 用) | +| `tsconfig.json` | TypeScript 编译配置 | +| `overlay.yml` | 本地开发 overlay 模板(`--patch` 用,需改路径) | + +--- + +## 本地测试(开发调试) + +适合开发阶段快速验证,无需编译,改代码即生效。 + +### 1. 安装依赖 + +```bash +cd theme-builder-skill +npm install +``` + +### 2. 配置 overlay.yml + +复制 `overlay.yml`,将 `name` 改为你机器上 `src/index.ts` 的绝对路径: + +**Windows**(需要 `file://` 前缀): +```yaml +- insert: + - id: gridea-theme-builder + name: 'file:///D:/theme-builder-skill/src/index.ts' +``` + +**macOS / Linux**(裸路径即可): +```yaml +- insert: + - id: gridea-theme-builder + name: '/Users/你/theme-builder-skill/src/index.ts' +``` + +### 3. 启动 + +```bash +# Windows +npx @deepseek-ai/dsh web --patch D:/theme-builder-skill/overlay.yml + +# macOS / Linux +npx @deepseek-ai/dsh web --patch /path/to/theme-builder-skill/overlay.yml +``` + +打开 `http://127.0.0.1:3080`,发送消息测试 skill 是否被模型加载。 + +### 4. 卸载 + +不带 `--patch` 重启即可,无需额外操作: + +```bash +npx @deepseek-ai/dsh web +``` + +--- + +## 从 GitHub 安装(给其他用户用) + +其他用户不需要 clone 仓库,通过 `dsh plugin add` 直接从 GitHub 拉取安装。 + +### 安装 + +```bash +npx @deepseek-ai/dsh plugin --profile web add "github:Gridea-Pro/theme-builder-skill" +``` + +> `package.json` 没有 `prepare` 脚本,pnpm 不会触发构建授权,安装一步到位。编译产物 `lib/` 已提交在仓库中。 + +### 启动 + +```bash +npx @deepseek-ai/dsh web +``` + +### 卸载 + +```bash +npx @deepseek-ai/dsh plugin --profile web remove @gridea-pro/dsh-skill-theme-builder +``` + +### 更新 + +```bash +npx @deepseek-ai/dsh plugin --profile web update @gridea-pro/dsh-skill-theme-builder +``` + +--- + +## 两种模式对比 + +| 维度 | 本地测试 (`--patch`) | GitHub 安装 (`plugin add`) | +|---|---|---| +| 适用场景 | 开发调试 | 分发给用户 | +| 需要本地克隆 | 是 | 否 | +| 需要编译 | 否(tsx 直接跑 .ts) | 否(`lib/` 已提交) | +| 需要构建授权 | 否 | 否 | +| 路径硬编码 | 是(每台机器不同) | 否 | +| 改代码后生效 | 重启即生效 | 需 `plugin update` | +| 卸载方式 | 不带 `--patch` 重启 | `plugin remove` | + +--- + +## 平台路径速查 + +| 平台 | overlay.yml 中 `name` 格式 | `--patch` 路径分隔符 | +|---|---|---| +| Windows | `file:///D:/path/to/src/index.ts` | 正斜杠 `/`(推荐)或双反斜杠 `\\` | +| macOS | `/Users/you/path/to/src/index.ts` | 正斜杠 `/` | +| Linux | `/home/you/path/to/src/index.ts` | 正斜杠 `/` | + +> Windows 必须用 `file:///` 前缀,否则 Node ESM 加载器会把 `D:` 误认为 URL scheme。Unix 系统裸路径可直接使用。 + +--- + +## 常见问题 + +### `ERR_MODULE_NOT_FOUND: Cannot find package '@gridea-pro/dsh-skill-theme-builder'` + +**原因**:用 `cordis.patch.yml`(包名引用)喂 `--patch`(期望文件路径)。 + +**解决**:改用 `overlay.yml`(文件路径引用),不要用 `cordis.patch.yml` 做 `--patch`。 + +### `ERR_UNSUPPORTED_ESM_URL_SCHEME: Received protocol 'd:'` + +**原因**:Windows 上 overlay.yml 中 `name` 用了裸路径 `D:/...`。 + +**解决**:加 `file:///` 前缀 → `file:///D:/...`。 + +### `Cannot find module 'node:fs/promises'` + +**原因**:缺少 `@types/node`。 + +**解决**:`npm install` 确保安装了 `@types/node`(已在 `devDependencies` 中声明)。 + +--- + +## 开发者须知 + +### 修改 `src/index.ts` 后同步 `lib/` + +本项目没有 `prepare` 脚本(去掉它是为了让 GitHub 安装不需要 pnpm 构建授权)。因此修改 `src/index.ts` 后必须手动编译并提交 `lib/`: + +```bash +npm run build +git add lib/ src/index.ts +git commit -m "feat: update plugin code" +``` + +CI 会检查 `lib/` 与 `src/` 是否同步(`check-lib-sync` job)。如果忘了编译,CI 会红灯。 + +### 包管理器说明 + +本项目使用 **npm** 管理依赖(`package-lock.json` + `npm ci`)。DSH 的 `plugin add` 底层使用 pnpm 从 GitHub 拉取,但 pnpm 读的是 `package.json`,不关心仓库的 lock 文件格式——`package-lock.json` 对 pnpm 透明,会被忽略。因此两者不冲突,用户也不需要安装 pnpm。 + +### `description` 自动从 SKILL.md 提取 + +`src/index.ts` 的 `get()` 方法会运行时从 `SKILL.md` frontmatter 解析 `description` 字段,不需要在代码中维护两份。`list()` 使用一个简短的 fallback 描述,仅用于初始目录展示;模型实际获取 skill 时调用 `get()`,拿到的是 SKILL.md 中的完整描述。 diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1ba0859 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,129 @@ +/** + * Gridea Pro theme builder skill provider for DeepSeek Harness. + * + * Registers the `gridea-theme-builder` skill from the bundled SKILL.md. + * The resourceBase points to the plugin bundle directory so the model can + * resolve references/, scripts/, and assets/ relative paths mentioned in + * the skill body. + * + * Pattern follows the official @deepseek-ai/dsh-skill-badge plugin: + * - registerProvider() with a static SkillProvider + * - list() and get() both read SKILL.md at call time, so edits to the + * frontmatter description and to the body are picked up without a rebuild + * + * @module @gridea-pro/dsh-skill-theme-builder + */ + +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import type { Context } from '@deepseek-ai/cordis' +import { + BUNDLED_SKILL_RANK, + type SkillCandidate, + type SkillDefinition, + type SkillProvider, +} from '@deepseek-ai/dsh-skill' + +/** Absolute URL to the bundled SKILL.md body file. */ +const SKILL_BODY_URL = new URL('../SKILL.md', import.meta.url) + +/** + * Directory base for relative resource resolution. + * The model receives this path in the block and resolves + * references/scripts/assets paths against it. + */ +const RESOURCE_BASE = { + kind: 'directory' as const, + path: fileURLToPath(new URL('../', import.meta.url)), +} + +/** Skill is available on both model and user invocation surfaces. */ +const INVOCATION = { modelInvocable: true, userInvocable: true } as const + +/** Used only when SKILL.md is unreadable or carries no description. */ +const FALLBACK_DESCRIPTION = 'Gridea Pro 博客主题开发专家' + +/** + * Split a Markdown file into its frontmatter `description` and its body. + * + * Handles both YAML forms the description may take: + * - block scalar (`>` or `|`) followed by indented lines, folded into one line + * - plain single-line value + * + * The block-scalar branch must not depend on anything following it: in this + * skill `description` is the last frontmatter field, and the closing `---` is + * already consumed by the outer match. + */ +function parseFrontmatter(raw: string): { description: string; body: string } { + const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/) + if (!match) return { description: FALLBACK_DESCRIPTION, body: raw } + + const frontmatter = match[1] + const body = match[2] + + const block = frontmatter.match(/^description:[ \t]*[>|][-+]?[ \t]*\r?\n((?:[ \t]+.*(?:\r?\n|$))+)/m) + if (block) { + const folded = block[1].split(/\r?\n/).map((line) => line.trim()).filter(Boolean).join(' ') + if (folded) return { description: folded, body } + } + + const plain = frontmatter.match(/^description:[ \t]*(\S.*?)[ \t]*$/m) + return { description: plain ? plain[1] : FALLBACK_DESCRIPTION, body } +} + +/** Candidate shape shared by list() and get(); `description` is filled in from SKILL.md. */ +const CANDIDATE: Omit = { + name: 'gridea-theme-builder', + invocation: INVOCATION, + provider: 'gridea-theme-builder', + source: 'bundled', + resourceBase: RESOURCE_BASE, + rank: BUNDLED_SKILL_RANK, + locator: SKILL_BODY_URL, +} + +const provider: SkillProvider = { + name: 'gridea-theme-builder', + /** + * The catalog description is the model's only routing signal — `get()` runs + * only after the model has already chosen this skill — so the full + * frontmatter description (trigger conditions and keywords included) has to + * be resolved here, not deferred to load time. + * + * An unreadable SKILL.md degrades to the fallback description instead of + * throwing, so one broken bundle cannot empty the whole catalog. + */ + async list(): Promise { + let description = FALLBACK_DESCRIPTION + try { + description = parseFrontmatter(await readFile(SKILL_BODY_URL, 'utf8')).description + } catch { + // 保底:读不到就用兜底描述,不让整个 skill 目录塌掉 + } + return [{ ...CANDIDATE, description }] + }, + async get(): Promise { + const raw = await readFile(SKILL_BODY_URL, 'utf8') + const { description, body } = parseFrontmatter(raw) + return { + name: CANDIDATE.name, + description, + invocation: CANDIDATE.invocation, + provider: CANDIDATE.provider, + source: CANDIDATE.source, + resourceBase: RESOURCE_BASE, + content: body, + } + }, +} + +/** Cordis plugin name. */ +export const name = 'gridea-theme-builder' + +/** Required capability seam: the skills registry. */ +export const inject = ['skills'] + +/** Register the bundled gridea-theme-builder skill provider on ctx.skills. */ +export function apply(ctx: Context): void { + ctx.skills.registerProvider(() => provider) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..35d76f7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "types": ["node"], + "declaration": true, + "declarationDir": "./lib/types", + "outDir": "./lib", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"], + "exclude": ["node_modules", "lib", "tests"] +}