From 915f975c8c640dc8dfeb358eab34047bbf2b260f Mon Sep 17 00:00:00 2001 From: xiaxi626 Date: Thu, 20 Aug 2026 11:22:24 +0800 Subject: [PATCH 1/6] feat: add DSH plugin support --- cordis.patch.yml | 11 +++++ package.json | 45 +++++++++++++++++++++ src/index.ts | 103 +++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 18 +++++++++ 4 files changed, 177 insertions(+) create mode 100644 cordis.patch.yml create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 tsconfig.json 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/package.json b/package.json new file mode 100644 index 0000000..4bb4ed8 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "@gridea-pro/dsh-skill-theme-builder", + "description": "Gridea Pro theme builder skill for DeepSeek Harness", + "version": "0.1.0", + "publishConfig": { + "access": "public" + }, + "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", + "prepare": "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" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..56265c7 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,103 @@ +/** + * 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() returns a pre-built candidate + * - get() reads SKILL.md at call time (body edits picked up dynamically) + * + * @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 + +/** + * Routing description shown in the model-facing skill catalog. + * Must match the `description` field in SKILL.md frontmatter. + * If the frontmatter description changes, update this constant. + */ +const DESCRIPTION = + 'Gridea Pro 博客主题开发专家。支持 Jinja2 (Pongo2)、Go Templates、EJS 三种模板引擎。' + + '提供主题脚手架生成、语法验证、渲染测试、避坑指南和完整的模板变量参考。' + + '当用户要求创建 Gridea 主题、修改 Gridea 主题、修复主题渲染问题、学习 Gridea 主题开发、' + + '从 EJS/Hugo 迁移主题时触发。' + + '触发关键词:Gridea 主题、博客主题、theme 开发、模板语法、主题配置、theme config。' + +/** Static candidate returned by every list() call. */ +const CANDIDATE: SkillCandidate = { + name: 'gridea-theme-builder', + description: DESCRIPTION, + invocation: INVOCATION, + provider: 'gridea-theme-builder', + source: 'bundled', + resourceBase: RESOURCE_BASE, + rank: BUNDLED_SKILL_RANK, + locator: SKILL_BODY_URL, +} + +/** + * Strip YAML frontmatter (--- delimited) from a Markdown file and return + * the body. If no frontmatter is present, the original content is returned + * unchanged. + */ +function stripFrontmatter(raw: string): string { + const match = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/) + return match ? match[1] : raw +} + +const provider: SkillProvider = { + name: 'gridea-theme-builder', + list: () => Promise.resolve([CANDIDATE]), + async get(): Promise { + const raw = await readFile(SKILL_BODY_URL, 'utf8') + return { + name: CANDIDATE.name, + description: CANDIDATE.description, + invocation: CANDIDATE.invocation, + provider: CANDIDATE.provider, + source: CANDIDATE.source, + resourceBase: RESOURCE_BASE, + content: stripFrontmatter(raw), + } + }, +} + +/** 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..9172f56 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "declaration": true, + "declarationDir": "./lib/types", + "outDir": "./lib", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"], + "exclude": ["node_modules", "lib", "tests"] +} From 839c3efded7584a3fd6c1be7f01329e7d5cb581e Mon Sep 17 00:00:00 2001 From: xiaxi626 Date: Thu, 20 Aug 2026 14:22:10 +0800 Subject: [PATCH 2/6] fix: correct install scripts and overlay path for DSH plugin --- install-dsh.bat | 120 +++++++++++++++++++++++++ install-dsh.sh | 90 +++++++++++++++++++ lib/index.js | 85 ++++++++++++++++++ lib/types/index.d.ts | 22 +++++ overlay.yml | 20 +++++ package-lock.json | 202 +++++++++++++++++++++++++++++++++++++++++++ package.json | 5 +- src/README.md | 182 ++++++++++++++++++++++++++++++++++++++ tsconfig.json | 1 + 9 files changed, 726 insertions(+), 1 deletion(-) create mode 100644 install-dsh.bat create mode 100644 install-dsh.sh create mode 100644 lib/index.js create mode 100644 lib/types/index.d.ts create mode 100644 overlay.yml create mode 100644 package-lock.json create mode 100644 src/README.md diff --git a/install-dsh.bat b/install-dsh.bat new file mode 100644 index 0000000..a240917 --- /dev/null +++ b/install-dsh.bat @@ -0,0 +1,120 @@ +@echo off +REM DSH 插件安装脚本 (Windows CMD) +REM +REM 原理:pnpm >=10 首次 add 会报 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED, +REM 报错信息中包含精确的 allowBuilds key(含 git URL + commit SHA)。 +REM 脚本自动提取该 key,写入 pnpm-workspace.yaml,然后重跑 add。 +REM +REM 用法: +REM install-dsh.bat REM 从 GitHub 安装 +REM install-dsh.bat D:\theme-builder-skill REM 从本地目录安装 + +setlocal enabledelayedexpansion + +set "PACKAGE_NAME=@gridea-pro/dsh-skill-theme-builder" +set "PROFILE=web" + +if defined DSH_HOME ( + set "DSH_HOME=%DSH_HOME%" +) else ( + set "DSH_HOME=%USERPROFILE%\.dsh" +) +set "PROFILE_DIR=!DSH_HOME!\profiles\!PROFILE!" +set "WORKSPACE_FILE=!PROFILE_DIR!\pnpm-workspace.yaml" +set "TEMP_OUTPUT=%TEMP%\dsh-install-output.txt" + +if "%~1"=="" ( + set "SOURCE=github:xiaxi626/theme-builder-skill#dsh" +) else ( + set "SOURCE=%~1" +) + +echo ==^> DSH 插件安装脚本 +echo 包名: !PACKAGE_NAME! +echo Profile: !PROFILE! +echo 源: !SOURCE! +echo. + +REM 1. 确保 profile 目录存在 +if not exist "!PROFILE_DIR!" mkdir "!PROFILE_DIR!" + +REM 2. 首次 add(预期失败,捕获输出提取 allowBuilds key) +echo ==^> 首次安装(预期触发构建授权报错)... +npx @deepseek-ai/dsh plugin --profile !PROFILE! add "!SOURCE!" > "!TEMP_OUTPUT!" 2>&1 +type "!TEMP_OUTPUT!" + +REM 3. 从报错中提取 allowBuilds key +REM pnpm 打印格式:@包名@git+ssh://...#SHA: true +findstr /C:"!PACKAGE_NAME!@git+" "!TEMP_OUTPUT!" > "%TEMP%\dsh-allow-line.txt" 2>&1 + +set "ALLOW_KEY=" +for /f "tokens=1 delims=:" %%A in ('findstr /C:"!PACKAGE_NAME!@git+" "!TEMP_OUTPUT!"') do ( + set "ALLOW_KEY=%%A" + goto :found_key +) + +:found_key +REM 清理 key 中的空格 +set "ALLOW_KEY=!ALLOW_KEY: =!" + +if "!ALLOW_KEY!"=="" ( + REM 检查是否已经安装成功 + findstr /C:"added" "!TEMP_OUTPUT!" >nul 2>&1 + if !errorlevel! equ 0 ( + echo. + echo ==^> 安装成功(无需构建授权) + echo 启动: npx @deepseek-ai/dsh web + goto :cleanup + ) + echo. + echo !! 未能自动提取 allowBuilds key + echo !! 请手动操作: + echo 1. 查看上方报错信息中 pnpm 打印的 allowBuilds 行 + echo 2. 将该行写入 !WORKSPACE_FILE! + echo 3. 重新执行: npx @deepseek-ai/dsh plugin --profile !PROFILE! add "!SOURCE!" + goto :cleanup +) + +echo. +echo ==^> 提取到 allowBuilds key: !ALLOW_KEY! + +REM 4. 写入 pnpm-workspace.yaml +echo ==^> 配置构建授权 (!WORKSPACE_FILE!) + +if not exist "!WORKSPACE_FILE!" ( + ( + echo allowBuilds: + echo !ALLOW_KEY!: true + ) > "!WORKSPACE_FILE!" + echo 已创建 !WORKSPACE_FILE! +) else ( + findstr /C:"!ALLOW_KEY!" "!WORKSPACE_FILE!" >nul 2>&1 + if !errorlevel! equ 0 ( + echo 授权已存在,跳过 + ) else ( + findstr /C:"allowBuilds:" "!WORKSPACE_FILE!" >nul 2>&1 + if !errorlevel! equ 0 ( + echo !ALLOW_KEY!: true>> "!WORKSPACE_FILE!" + ) else ( + echo.>> "!WORKSPACE_FILE!" + echo allowBuilds:>> "!WORKSPACE_FILE!" + echo !ALLOW_KEY!: true>> "!WORKSPACE_FILE!" + ) + echo 已追加授权到 !WORKSPACE_FILE! + ) +) +echo. + +REM 5. 重新安装 +echo ==^> 重新安装... +npx @deepseek-ai/dsh plugin --profile !PROFILE! add "!SOURCE!" + +echo. +echo ==^> 安装完成! +echo 启动: npx @deepseek-ai/dsh web +echo 卸载: npx @deepseek-ai/dsh plugin --profile !PROFILE! remove !PACKAGE_NAME! + +:cleanup +if exist "!TEMP_OUTPUT!" del "!TEMP_OUTPUT!" +if exist "%TEMP%\dsh-allow-line.txt" del "%TEMP%\dsh-allow-line.txt" +endlocal diff --git a/install-dsh.sh b/install-dsh.sh new file mode 100644 index 0000000..1a67450 --- /dev/null +++ b/install-dsh.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# DSH 插件安装脚本(macOS / Linux / Git Bash) +# +# 原理:pnpm ≥10 首次 add 会报 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED, +# 报错信息中包含精确的 allowBuilds key(含 git URL + commit SHA)。 +# 脚本自动提取该 key,写入 pnpm-workspace.yaml,然后重跑 add。 +# +# 用法: +# bash install-dsh.sh # 从 GitHub 安装 +# bash install-dsh.sh ./ # 从本地目录安装 + +set -euo pipefail + +PACKAGE_NAME="@gridea-pro/dsh-skill-theme-builder" +PROFILE="${DSH_PROFILE:-web}" +DSH_HOME="${DSH_HOME:-$HOME/.dsh}" +PROFILE_DIR="$DSH_HOME/profiles/$PROFILE" +WORKSPACE_FILE="$PROFILE_DIR/pnpm-workspace.yaml" + +SOURCE="${1:-github:xiaxi626/theme-builder-skill#dsh}" + +echo "==> DSH 插件安装脚本" +echo " 包名: $PACKAGE_NAME" +echo " Profile: $PROFILE" +echo " 源: $SOURCE" +echo "" + +# 1. 确保 profile 目录存在 +mkdir -p "$PROFILE_DIR" + +# 2. 首次 add(预期失败,捕获输出提取 allowBuilds key) +echo "==> 首次安装(预期触发构建授权报错)..." +FIRST_OUTPUT=$(npx @deepseek-ai/dsh plugin --profile "$PROFILE" add "$SOURCE" 2>&1 || true) +echo "$FIRST_OUTPUT" + +# 3. 从报错中提取 allowBuilds key +# pnpm 打印格式:@包名@git+ssh://...#SHA: true +ALLOW_KEY=$(echo "$FIRST_OUTPUT" | grep -oP "${PACKAGE_NAME}@git\+[^:]+#[a-f0-9]+" | head -1) + +if [ -z "$ALLOW_KEY" ]; then + # 检查是否已经安装成功(没有报错) + if echo "$FIRST_OUTPUT" | grep -qi "added\|installed\|done"; then + echo "" + echo "==> 安装成功(无需构建授权)" + echo " 启动: npx @deepseek-ai/dsh web" + exit 0 + fi + echo "" + echo "!! 未能自动提取 allowBuilds key" + echo "!! 请手动操作:" + echo " 1. 查看上方报错信息中 pnpm 打印的 allowBuilds 行" + echo " 2. 将该行写入 $WORKSPACE_FILE" + echo " 3. 重新执行: npx @deepseek-ai/dsh plugin --profile $PROFILE add \"$SOURCE\"" + exit 1 +fi + +echo "" +echo "==> 提取到 allowBuilds key: $ALLOW_KEY" + +# 4. 写入 pnpm-workspace.yaml +echo "==> 配置构建授权 ($WORKSPACE_FILE)" + +if [ ! -f "$WORKSPACE_FILE" ]; then + cat > "$WORKSPACE_FILE" << EOF +allowBuilds: + ${ALLOW_KEY}: true +EOF + echo " 已创建 $WORKSPACE_FILE" +elif grep -q "$ALLOW_KEY" "$WORKSPACE_FILE"; then + echo " 授权已存在,跳过" +else + if grep -q "^allowBuilds:" "$WORKSPACE_FILE"; then + sed -i.bak "/^allowBuilds:/a\\ ${ALLOW_KEY}: true" "$WORKSPACE_FILE" + else + echo "" >> "$WORKSPACE_FILE" + echo "allowBuilds:" >> "$WORKSPACE_FILE" + echo " ${ALLOW_KEY}: true" >> "$WORKSPACE_FILE" + fi + echo " 已追加授权到 $WORKSPACE_FILE" +fi +echo "" + +# 5. 重新安装 +echo "==> 重新安装..." +npx @deepseek-ai/dsh plugin --profile "$PROFILE" add "$SOURCE" + +echo "" +echo "==> 安装完成!" +echo " 启动: npx @deepseek-ai/dsh web" +echo " 卸载: npx @deepseek-ai/dsh plugin --profile $PROFILE remove $PACKAGE_NAME" diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..86e414b --- /dev/null +++ b/lib/index.js @@ -0,0 +1,85 @@ +/** + * 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() returns a pre-built candidate + * - get() reads SKILL.md at call time (body edits picked up dynamically) + * + * @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 }; +/** + * Routing description shown in the model-facing skill catalog. + * Must match the `description` field in SKILL.md frontmatter. + * If the frontmatter description changes, update this constant. + */ +const DESCRIPTION = 'Gridea Pro 博客主题开发专家。支持 Jinja2 (Pongo2)、Go Templates、EJS 三种模板引擎。' + + '提供主题脚手架生成、语法验证、渲染测试、避坑指南和完整的模板变量参考。' + + '当用户要求创建 Gridea 主题、修改 Gridea 主题、修复主题渲染问题、学习 Gridea 主题开发、' + + '从 EJS/Hugo 迁移主题时触发。' + + '触发关键词:Gridea 主题、博客主题、theme 开发、模板语法、主题配置、theme config。'; +/** Static candidate returned by every list() call. */ +const CANDIDATE = { + name: 'gridea-theme-builder', + description: DESCRIPTION, + invocation: INVOCATION, + provider: 'gridea-theme-builder', + source: 'bundled', + resourceBase: RESOURCE_BASE, + rank: BUNDLED_SKILL_RANK, + locator: SKILL_BODY_URL, +}; +/** + * Strip YAML frontmatter (--- delimited) from a Markdown file and return + * the body. If no frontmatter is present, the original content is returned + * unchanged. + */ +function stripFrontmatter(raw) { + const match = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/); + return match ? match[1] : raw; +} +const provider = { + name: 'gridea-theme-builder', + list: () => Promise.resolve([CANDIDATE]), + async get() { + const raw = await readFile(SKILL_BODY_URL, 'utf8'); + return { + name: CANDIDATE.name, + description: CANDIDATE.description, + invocation: CANDIDATE.invocation, + provider: CANDIDATE.provider, + source: CANDIDATE.source, + resourceBase: RESOURCE_BASE, + content: stripFrontmatter(raw), + }; + }, +}; +/** 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..5338fc7 --- /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() returns a pre-built candidate + * - get() reads SKILL.md at call time (body edits picked up dynamically) + * + * @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 index 4bb4ed8..ead4a26 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,9 @@ "@deepseek-ai/cordis": ">=0.0.1-rc.0" }, "devDependencies": { - "typescript": "^5.5.0" + "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..dd1c422 --- /dev/null +++ b/src/README.md @@ -0,0 +1,182 @@ +# 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` 用,需改路径) | +| `install-dsh.sh` | 安装脚本(macOS / Linux / Git Bash) | +| `install-dsh.bat` | 安装脚本(Windows CMD) | + +--- + +## 本地测试(开发调试) + +适合开发阶段快速验证,无需编译,改代码即生效。 + +### 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 安装(分发给别人用) + +适合最终用户,安装后无需每次带 `--patch` 参数。 + +### 一键安装(推荐) + +```bash +# macOS / Linux / Git Bash +bash install-dsh.sh + +# Windows CMD +install-dsh.bat +``` + +脚本自动完成两阶段安装: +1. 首次 `add` 触发 pnpm 构建授权报错,从中提取精确的 `allowBuilds` key(含 git URL + commit SHA) +2. 将 key 写入 `~/.dsh/profiles/web/pnpm-workspace.yaml`,然后重新 `add` 完成安装 + +### 手动安装 + +如果不使用脚本,需手动执行三步: + +```bash +# 1. 安装(首次会因构建授权失败,这是预期的) +npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-skill#dsh" + +# 2. 查看报错信息中 pnpm 打印的 allowBuilds 行,形如: +# allowBuilds: +# @gridea-pro/dsh-skill-theme-builder@git+ssh://git@github.com/xiaxi626/theme-builder-skill.git#: true +# +# 将该行原样写入 ~/.dsh/profiles/web/pnpm-workspace.yaml +# +# 注意:key 包含完整的 git URL + commit SHA,不能用简单包名替代, +# 且 SHA 每次推送都会变。 + +# 3. 重新安装 +npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-skill#dsh" +``` + +### 启动 + +```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) | 是(pnpm 运行 `prepare`) | +| 需要构建授权 | 否 | 是(`allowBuilds`) | +| 路径硬编码 | 是(每台机器不同) | 否 | +| 改代码后生效 | 重启即生效 | 需 `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` 中声明)。 + +### `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` + +**原因**:pnpm ≥10 默认拒绝运行 git 依赖的 `prepare` 脚本。 + +**解决**:使用 `install-dsh.sh` / `install-dsh.bat` 脚本自动处理;或手动将 pnpm 报错中打印的完整 `allowBuilds` key(含 git URL + commit SHA)写入 `~/.dsh/profiles/web/pnpm-workspace.yaml`。 + +> 注意:key 不能用简单包名,必须用 pnpm 打印的完整格式,且 commit SHA 每次推送都会变。 diff --git a/tsconfig.json b/tsconfig.json index 9172f56..35d76f7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "lib": ["ES2022"], + "types": ["node"], "declaration": true, "declarationDir": "./lib/types", "outDir": "./lib", From 43194d708056bf369959e45b3e69d0cee7cb37d7 Mon Sep 17 00:00:00 2001 From: xiaxi626 Date: Thu, 20 Aug 2026 14:42:12 +0800 Subject: [PATCH 3/6] fix: improve DSH plugin install docs and error handling - rewrite GitHub install section: warn users about 3-step pnpm flow upfront - show actual error output so users know what to expect - clarify pnpm-workspace.yaml already has base content, append only - add cleanup guidance: failed install leaves no residue, remove is not needed - fix update section: plugin update fails on SHA change, use add flow instead - add Windows file:// URL and @types/node to troubleshooting - add overlay.yml as separate file from cordis.patch.yml - update install scripts to two-phase: capture pnpm error, extract allowBuilds key, retry --- src/README.md | 100 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 28 deletions(-) diff --git a/src/README.md b/src/README.md index dd1c422..73d3283 100644 --- a/src/README.md +++ b/src/README.md @@ -12,8 +12,8 @@ | `cordis.patch.yml` | bundle 模式的 patch 层(`dsh plugin add` 用) | | `tsconfig.json` | TypeScript 编译配置 | | `overlay.yml` | 本地开发 overlay 模板(`--patch` 用,需改路径) | -| `install-dsh.sh` | 安装脚本(macOS / Linux / Git Bash) | -| `install-dsh.bat` | 安装脚本(Windows CMD) | +| `install-dsh.sh` | 安装辅助脚本(macOS / Linux / Git Bash) | +| `install-dsh.bat` | 安装辅助脚本(Windows CMD) | --- @@ -68,42 +68,56 @@ npx @deepseek-ai/dsh web --- -## 从 GitHub 安装(分发给别人用) +## 从 GitHub 安装(给其他用户用) -适合最终用户,安装后无需每次带 `--patch` 参数。 +其他用户不需要 clone 仓库,通过 `dsh plugin add` 直接从 GitHub 拉取安装。 -### 一键安装(推荐) +> **注意**:由于 pnpm ≥10 的安全策略,从 GitHub 安装**一定会经历"首次失败 → 手动授权 → 重新安装"三步**,这是 pnpm 的设计,不是 bug。如果觉得麻烦,请作者发布到 npm(见末尾说明)。 + +### 安装 + +**第 1 步:首次安装(预期会失败)** ```bash -# macOS / Linux / Git Bash -bash install-dsh.sh +npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-skill#dsh" +``` -# Windows CMD -install-dsh.bat +会看到类似报错: + +``` +[ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED] ... +allowBuilds: + @gridea-pro/dsh-skill-theme-builder@git+ssh://git@github.com/xiaxi626/theme-builder-skill.git#839c3efd...: true ``` -脚本自动完成两阶段安装: -1. 首次 `add` 触发 pnpm 构建授权报错,从中提取精确的 `allowBuilds` key(含 git URL + commit SHA) -2. 将 key 写入 `~/.dsh/profiles/web/pnpm-workspace.yaml`,然后重新 `add` 完成安装 +这是正常的——pnpm 拒绝运行 git 依赖的构建脚本,需要你手动授权。 -### 手动安装 +**第 2 步:写入构建授权** -如果不使用脚本,需手动执行三步: +打开 `~/.dsh/profiles/web/pnpm-workspace.yaml`(Windows: `C:\Users\你的用户名\.dsh\profiles\web\pnpm-workspace.yaml`)。 -```bash -# 1. 安装(首次会因构建授权失败,这是预期的) -npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-skill#dsh" +这个文件已有基础内容,**不要删原有内容**,在文件末尾追加 pnpm 报错中打印的那两行: + +```yaml +# 原有内容保持不变: +packages: + - . + +nodeLinker: hoisted -# 2. 查看报错信息中 pnpm 打印的 allowBuilds 行,形如: -# allowBuilds: -# @gridea-pro/dsh-skill-theme-builder@git+ssh://git@github.com/xiaxi626/theme-builder-skill.git#: true -# -# 将该行原样写入 ~/.dsh/profiles/web/pnpm-workspace.yaml -# -# 注意:key 包含完整的 git URL + commit SHA,不能用简单包名替代, -# 且 SHA 每次推送都会变。 +# 追加以下内容(从 pnpm 报错中原样复制,不要手打): +allowBuilds: + @gridea-pro/dsh-skill-theme-builder@git+ssh://git@github.com/xiaxi626/theme-builder-skill.git#839c3efd...: true +``` + +关键注意点: +- key 包含完整的 git URL + commit SHA,**不能用简单包名替代** +- **从 pnpm 报错中原样复制**,不要手打(SHA 很容易抄错) +- SHA 每次推送都会变,更新插件时需要重复这个流程 + +**第 3 步:重新安装(这次会成功)** -# 3. 重新安装 +```bash npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-skill#dsh" ``` @@ -119,10 +133,30 @@ npx @deepseek-ai/dsh web npx @deepseek-ai/dsh plugin --profile web remove @gridea-pro/dsh-skill-theme-builder ``` +卸载后建议手动清理 `pnpm-workspace.yaml` 中的 `allowBuilds` 条目(原有内容保留)。 + ### 更新 ```bash -npx @deepseek-ai/dsh plugin --profile web update @gridea-pro/dsh-skill-theme-builder +# 1. 先删掉旧的 allowBuilds 条目,重新 add 触发报错获取新 SHA +npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-skill#dsh" +# 2. 用新的 key 更新 pnpm-workspace.yaml +# 3. 重新 add +npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-skill#dsh" +``` + +--- + +## 安装辅助脚本(仅限已 clone 仓库时使用) + +`install-dsh.sh` / `install-dsh.bat` 不是独立的安装方式,它只是把上面"从 GitHub 安装"的三步自动化了(自动提取 pnpm 报错中的 `allowBuilds` key)。只有你 clone 了仓库才能拿到脚本,所以本质上只适合开发者自己验证安装流程。 + +```bash +# macOS / Linux / Git Bash +bash install-dsh.sh + +# Windows CMD +install-dsh.bat ``` --- @@ -177,6 +211,16 @@ npx @deepseek-ai/dsh plugin --profile web update @gridea-pro/dsh-skill-theme-bui **原因**:pnpm ≥10 默认拒绝运行 git 依赖的 `prepare` 脚本。 -**解决**:使用 `install-dsh.sh` / `install-dsh.bat` 脚本自动处理;或手动将 pnpm 报错中打印的完整 `allowBuilds` key(含 git URL + commit SHA)写入 `~/.dsh/profiles/web/pnpm-workspace.yaml`。 +**解决**:按上方"从 GitHub 安装"的三步流程操作——首次失败是正常的,将 pnpm 报错中打印的完整 `allowBuilds` key(含 git URL + commit SHA)追加到 `~/.dsh/profiles/web/pnpm-workspace.yaml`,然后重新 `add`。 > 注意:key 不能用简单包名,必须用 pnpm 打印的完整格式,且 commit SHA 每次推送都会变。 + +### 安装失败后清理 + +首次 `add` 失败**不会留下残余**——pnpm 在构建授权通过之前不会写入任何依赖。如果试图 `remove` 会看到 `ERR_PNPM_CANNOT_REMOVE_MISSING_DEPS`,这是正常的,说明 profile 是干净的,无需额外清理。 + +`~/.dsh/profiles/web/pnpm-workspace.yaml` 中的原有内容(`packages`、`nodeLinker`)是 DSH profile 自带的基础配置,**不要删**。只需追加或清理 `allowBuilds` 条目。 + +### `plugin update` 失效 + +GitHub 安装方式下,`plugin update` 可能因 SHA 变化导致授权失效。解决方式:手动删掉 `pnpm-workspace.yaml` 中的旧 `allowBuilds` 条目,重新走"add → 报错 → 写新 key → add"流程。 From 960b2e71f341c30ff9b12e7bff450e3e7eaa398b Mon Sep 17 00:00:00 2001 From: xiaxi626 Date: Thu, 20 Aug 2026 15:08:06 +0800 Subject: [PATCH 4/6] fix: remove useless install scripts and update README - delete install-dsh.sh and install-dsh.bat - remove install script section from README - add single-quote requirement for allowBuilds key in pnpm-workspace.yaml - add warning not to copy pnpm error comment lines into yaml --- install-dsh.bat | 120 ------------------------------------------------ install-dsh.sh | 90 ------------------------------------ src/README.md | 22 ++------- 3 files changed, 4 insertions(+), 228 deletions(-) delete mode 100644 install-dsh.bat delete mode 100644 install-dsh.sh diff --git a/install-dsh.bat b/install-dsh.bat deleted file mode 100644 index a240917..0000000 --- a/install-dsh.bat +++ /dev/null @@ -1,120 +0,0 @@ -@echo off -REM DSH 插件安装脚本 (Windows CMD) -REM -REM 原理:pnpm >=10 首次 add 会报 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED, -REM 报错信息中包含精确的 allowBuilds key(含 git URL + commit SHA)。 -REM 脚本自动提取该 key,写入 pnpm-workspace.yaml,然后重跑 add。 -REM -REM 用法: -REM install-dsh.bat REM 从 GitHub 安装 -REM install-dsh.bat D:\theme-builder-skill REM 从本地目录安装 - -setlocal enabledelayedexpansion - -set "PACKAGE_NAME=@gridea-pro/dsh-skill-theme-builder" -set "PROFILE=web" - -if defined DSH_HOME ( - set "DSH_HOME=%DSH_HOME%" -) else ( - set "DSH_HOME=%USERPROFILE%\.dsh" -) -set "PROFILE_DIR=!DSH_HOME!\profiles\!PROFILE!" -set "WORKSPACE_FILE=!PROFILE_DIR!\pnpm-workspace.yaml" -set "TEMP_OUTPUT=%TEMP%\dsh-install-output.txt" - -if "%~1"=="" ( - set "SOURCE=github:xiaxi626/theme-builder-skill#dsh" -) else ( - set "SOURCE=%~1" -) - -echo ==^> DSH 插件安装脚本 -echo 包名: !PACKAGE_NAME! -echo Profile: !PROFILE! -echo 源: !SOURCE! -echo. - -REM 1. 确保 profile 目录存在 -if not exist "!PROFILE_DIR!" mkdir "!PROFILE_DIR!" - -REM 2. 首次 add(预期失败,捕获输出提取 allowBuilds key) -echo ==^> 首次安装(预期触发构建授权报错)... -npx @deepseek-ai/dsh plugin --profile !PROFILE! add "!SOURCE!" > "!TEMP_OUTPUT!" 2>&1 -type "!TEMP_OUTPUT!" - -REM 3. 从报错中提取 allowBuilds key -REM pnpm 打印格式:@包名@git+ssh://...#SHA: true -findstr /C:"!PACKAGE_NAME!@git+" "!TEMP_OUTPUT!" > "%TEMP%\dsh-allow-line.txt" 2>&1 - -set "ALLOW_KEY=" -for /f "tokens=1 delims=:" %%A in ('findstr /C:"!PACKAGE_NAME!@git+" "!TEMP_OUTPUT!"') do ( - set "ALLOW_KEY=%%A" - goto :found_key -) - -:found_key -REM 清理 key 中的空格 -set "ALLOW_KEY=!ALLOW_KEY: =!" - -if "!ALLOW_KEY!"=="" ( - REM 检查是否已经安装成功 - findstr /C:"added" "!TEMP_OUTPUT!" >nul 2>&1 - if !errorlevel! equ 0 ( - echo. - echo ==^> 安装成功(无需构建授权) - echo 启动: npx @deepseek-ai/dsh web - goto :cleanup - ) - echo. - echo !! 未能自动提取 allowBuilds key - echo !! 请手动操作: - echo 1. 查看上方报错信息中 pnpm 打印的 allowBuilds 行 - echo 2. 将该行写入 !WORKSPACE_FILE! - echo 3. 重新执行: npx @deepseek-ai/dsh plugin --profile !PROFILE! add "!SOURCE!" - goto :cleanup -) - -echo. -echo ==^> 提取到 allowBuilds key: !ALLOW_KEY! - -REM 4. 写入 pnpm-workspace.yaml -echo ==^> 配置构建授权 (!WORKSPACE_FILE!) - -if not exist "!WORKSPACE_FILE!" ( - ( - echo allowBuilds: - echo !ALLOW_KEY!: true - ) > "!WORKSPACE_FILE!" - echo 已创建 !WORKSPACE_FILE! -) else ( - findstr /C:"!ALLOW_KEY!" "!WORKSPACE_FILE!" >nul 2>&1 - if !errorlevel! equ 0 ( - echo 授权已存在,跳过 - ) else ( - findstr /C:"allowBuilds:" "!WORKSPACE_FILE!" >nul 2>&1 - if !errorlevel! equ 0 ( - echo !ALLOW_KEY!: true>> "!WORKSPACE_FILE!" - ) else ( - echo.>> "!WORKSPACE_FILE!" - echo allowBuilds:>> "!WORKSPACE_FILE!" - echo !ALLOW_KEY!: true>> "!WORKSPACE_FILE!" - ) - echo 已追加授权到 !WORKSPACE_FILE! - ) -) -echo. - -REM 5. 重新安装 -echo ==^> 重新安装... -npx @deepseek-ai/dsh plugin --profile !PROFILE! add "!SOURCE!" - -echo. -echo ==^> 安装完成! -echo 启动: npx @deepseek-ai/dsh web -echo 卸载: npx @deepseek-ai/dsh plugin --profile !PROFILE! remove !PACKAGE_NAME! - -:cleanup -if exist "!TEMP_OUTPUT!" del "!TEMP_OUTPUT!" -if exist "%TEMP%\dsh-allow-line.txt" del "%TEMP%\dsh-allow-line.txt" -endlocal diff --git a/install-dsh.sh b/install-dsh.sh deleted file mode 100644 index 1a67450..0000000 --- a/install-dsh.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -# DSH 插件安装脚本(macOS / Linux / Git Bash) -# -# 原理:pnpm ≥10 首次 add 会报 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED, -# 报错信息中包含精确的 allowBuilds key(含 git URL + commit SHA)。 -# 脚本自动提取该 key,写入 pnpm-workspace.yaml,然后重跑 add。 -# -# 用法: -# bash install-dsh.sh # 从 GitHub 安装 -# bash install-dsh.sh ./ # 从本地目录安装 - -set -euo pipefail - -PACKAGE_NAME="@gridea-pro/dsh-skill-theme-builder" -PROFILE="${DSH_PROFILE:-web}" -DSH_HOME="${DSH_HOME:-$HOME/.dsh}" -PROFILE_DIR="$DSH_HOME/profiles/$PROFILE" -WORKSPACE_FILE="$PROFILE_DIR/pnpm-workspace.yaml" - -SOURCE="${1:-github:xiaxi626/theme-builder-skill#dsh}" - -echo "==> DSH 插件安装脚本" -echo " 包名: $PACKAGE_NAME" -echo " Profile: $PROFILE" -echo " 源: $SOURCE" -echo "" - -# 1. 确保 profile 目录存在 -mkdir -p "$PROFILE_DIR" - -# 2. 首次 add(预期失败,捕获输出提取 allowBuilds key) -echo "==> 首次安装(预期触发构建授权报错)..." -FIRST_OUTPUT=$(npx @deepseek-ai/dsh plugin --profile "$PROFILE" add "$SOURCE" 2>&1 || true) -echo "$FIRST_OUTPUT" - -# 3. 从报错中提取 allowBuilds key -# pnpm 打印格式:@包名@git+ssh://...#SHA: true -ALLOW_KEY=$(echo "$FIRST_OUTPUT" | grep -oP "${PACKAGE_NAME}@git\+[^:]+#[a-f0-9]+" | head -1) - -if [ -z "$ALLOW_KEY" ]; then - # 检查是否已经安装成功(没有报错) - if echo "$FIRST_OUTPUT" | grep -qi "added\|installed\|done"; then - echo "" - echo "==> 安装成功(无需构建授权)" - echo " 启动: npx @deepseek-ai/dsh web" - exit 0 - fi - echo "" - echo "!! 未能自动提取 allowBuilds key" - echo "!! 请手动操作:" - echo " 1. 查看上方报错信息中 pnpm 打印的 allowBuilds 行" - echo " 2. 将该行写入 $WORKSPACE_FILE" - echo " 3. 重新执行: npx @deepseek-ai/dsh plugin --profile $PROFILE add \"$SOURCE\"" - exit 1 -fi - -echo "" -echo "==> 提取到 allowBuilds key: $ALLOW_KEY" - -# 4. 写入 pnpm-workspace.yaml -echo "==> 配置构建授权 ($WORKSPACE_FILE)" - -if [ ! -f "$WORKSPACE_FILE" ]; then - cat > "$WORKSPACE_FILE" << EOF -allowBuilds: - ${ALLOW_KEY}: true -EOF - echo " 已创建 $WORKSPACE_FILE" -elif grep -q "$ALLOW_KEY" "$WORKSPACE_FILE"; then - echo " 授权已存在,跳过" -else - if grep -q "^allowBuilds:" "$WORKSPACE_FILE"; then - sed -i.bak "/^allowBuilds:/a\\ ${ALLOW_KEY}: true" "$WORKSPACE_FILE" - else - echo "" >> "$WORKSPACE_FILE" - echo "allowBuilds:" >> "$WORKSPACE_FILE" - echo " ${ALLOW_KEY}: true" >> "$WORKSPACE_FILE" - fi - echo " 已追加授权到 $WORKSPACE_FILE" -fi -echo "" - -# 5. 重新安装 -echo "==> 重新安装..." -npx @deepseek-ai/dsh plugin --profile "$PROFILE" add "$SOURCE" - -echo "" -echo "==> 安装完成!" -echo " 启动: npx @deepseek-ai/dsh web" -echo " 卸载: npx @deepseek-ai/dsh plugin --profile $PROFILE remove $PACKAGE_NAME" diff --git a/src/README.md b/src/README.md index 73d3283..0f8f5c2 100644 --- a/src/README.md +++ b/src/README.md @@ -12,8 +12,6 @@ | `cordis.patch.yml` | bundle 模式的 patch 层(`dsh plugin add` 用) | | `tsconfig.json` | TypeScript 编译配置 | | `overlay.yml` | 本地开发 overlay 模板(`--patch` 用,需改路径) | -| `install-dsh.sh` | 安装辅助脚本(macOS / Linux / Git Bash) | -| `install-dsh.bat` | 安装辅助脚本(Windows CMD) | --- @@ -105,14 +103,16 @@ packages: nodeLinker: hoisted -# 追加以下内容(从 pnpm 报错中原样复制,不要手打): +# 追加以下内容(从 pnpm 报错中原样复制 key,用单引号包起来): allowBuilds: - @gridea-pro/dsh-skill-theme-builder@git+ssh://git@github.com/xiaxi626/theme-builder-skill.git#839c3efd...: true + '@gridea-pro/dsh-skill-theme-builder@git+ssh://git@github.com/xiaxi626/theme-builder-skill.git#839c3efd...': true ``` 关键注意点: - key 包含完整的 git URL + commit SHA,**不能用简单包名替代** - **从 pnpm 报错中原样复制**,不要手打(SHA 很容易抄错) +- **key 必须用单引号包起来**,因为 `@` 是 YAML 保留字符,不加引号会报 `bad indentation` 错误 +- 不要把 pnpm 报错中的注释行(`# Add the package to ...`)复制进去 - SHA 每次推送都会变,更新插件时需要重复这个流程 **第 3 步:重新安装(这次会成功)** @@ -147,20 +147,6 @@ npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-ski --- -## 安装辅助脚本(仅限已 clone 仓库时使用) - -`install-dsh.sh` / `install-dsh.bat` 不是独立的安装方式,它只是把上面"从 GitHub 安装"的三步自动化了(自动提取 pnpm 报错中的 `allowBuilds` key)。只有你 clone 了仓库才能拿到脚本,所以本质上只适合开发者自己验证安装流程。 - -```bash -# macOS / Linux / Git Bash -bash install-dsh.sh - -# Windows CMD -install-dsh.bat -``` - ---- - ## 两种模式对比 | 维度 | 本地测试 (`--patch`) | GitHub 安装 (`plugin add`) | From a80787ff28492c10884593f8c96954918e25953a Mon Sep 17 00:00:00 2001 From: xiaxi626 Date: Thu, 20 Aug 2026 23:52:51 +0800 Subject: [PATCH 5/6] fix: address PR review feedback - add node_modules/ to .gitignore - remove prepare script from package.json (eliminates pnpm allowBuilds hassle) - add check-lib-sync CI job to catch stale lib/ output - auto-extract description from SKILL.md frontmatter in get() - add DSH section to main README - add CHANGELOG entry - remove unused publishConfig - rebuild lib/ to match src/ --- .github/workflows/ci.yml | 21 ++++++++++ .gitignore | 4 ++ CHANGELOG.md | 11 ++++++ README.md | 4 ++ lib/index.js | 54 +++++++++++++++----------- lib/types/index.d.ts | 4 +- package.json | 6 +-- src/README.md | 83 ++++++++++------------------------------ src/index.ts | 59 ++++++++++++++++------------ 9 files changed, 128 insertions(+), 118 deletions(-) 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/lib/index.js b/lib/index.js index 86e414b..1bfdeca 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,8 +8,8 @@ * * Pattern follows the official @deepseek-ai/dsh-skill-badge plugin: * - registerProvider() with a static SkillProvider - * - list() returns a pre-built candidate - * - get() reads SKILL.md at call time (body edits picked up dynamically) + * - list() returns a pre-built candidate (description read lazily in get()) + * - get() reads SKILL.md at call time (body and description picked up dynamically) * * @module @gridea-pro/dsh-skill-theme-builder */ @@ -30,19 +30,35 @@ const RESOURCE_BASE = { /** Skill is available on both model and user invocation surfaces. */ const INVOCATION = { modelInvocable: true, userInvocable: true }; /** - * Routing description shown in the model-facing skill catalog. - * Must match the `description` field in SKILL.md frontmatter. - * If the frontmatter description changes, update this constant. + * Fallback description used by list() before SKILL.md is read. + * get() always reads the real description from frontmatter at call time, + * so this constant only needs to be a reasonable placeholder. */ -const DESCRIPTION = 'Gridea Pro 博客主题开发专家。支持 Jinja2 (Pongo2)、Go Templates、EJS 三种模板引擎。' + - '提供主题脚手架生成、语法验证、渲染测试、避坑指南和完整的模板变量参考。' + - '当用户要求创建 Gridea 主题、修改 Gridea 主题、修复主题渲染问题、学习 Gridea 主题开发、' + - '从 EJS/Hugo 迁移主题时触发。' + - '触发关键词:Gridea 主题、博客主题、theme 开发、模板语法、主题配置、theme config。'; -/** Static candidate returned by every list() call. */ +const FALLBACK_DESCRIPTION = 'Gridea Pro 博客主题开发专家'; +/** + * Parse YAML frontmatter from a Markdown file. + * Returns { description, body } where description is extracted from the + * `description` field (supports both block scalar `>` and plain string). + * If no frontmatter is present, returns the full content as body. + */ +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]; + // Match description field (handles `>` block scalar and plain string) + const descMatch = frontmatter.match(/^description:\s*(?:>\s*\n([\s\S]*?)(?=\n\w|\n---)|(.+))$/m); + let description = FALLBACK_DESCRIPTION; + if (descMatch) { + description = (descMatch[1] || descMatch[2] || FALLBACK_DESCRIPTION).trim(); + } + return { description, body }; +} +/** Static candidate returned by list(). Description is a placeholder; get() returns the real one. */ const CANDIDATE = { name: 'gridea-theme-builder', - description: DESCRIPTION, + description: FALLBACK_DESCRIPTION, invocation: INVOCATION, provider: 'gridea-theme-builder', source: 'bundled', @@ -50,28 +66,20 @@ const CANDIDATE = { rank: BUNDLED_SKILL_RANK, locator: SKILL_BODY_URL, }; -/** - * Strip YAML frontmatter (--- delimited) from a Markdown file and return - * the body. If no frontmatter is present, the original content is returned - * unchanged. - */ -function stripFrontmatter(raw) { - const match = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/); - return match ? match[1] : raw; -} const provider = { name: 'gridea-theme-builder', list: () => Promise.resolve([CANDIDATE]), async get() { const raw = await readFile(SKILL_BODY_URL, 'utf8'); + const { description, body } = parseFrontmatter(raw); return { name: CANDIDATE.name, - description: CANDIDATE.description, + description, invocation: CANDIDATE.invocation, provider: CANDIDATE.provider, source: CANDIDATE.source, resourceBase: RESOURCE_BASE, - content: stripFrontmatter(raw), + content: body, }; }, }; diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts index 5338fc7..f05dfff 100644 --- a/lib/types/index.d.ts +++ b/lib/types/index.d.ts @@ -8,8 +8,8 @@ * * Pattern follows the official @deepseek-ai/dsh-skill-badge plugin: * - registerProvider() with a static SkillProvider - * - list() returns a pre-built candidate - * - get() reads SKILL.md at call time (body edits picked up dynamically) + * - list() returns a pre-built candidate (description read lazily in get()) + * - get() reads SKILL.md at call time (body and description picked up dynamically) * * @module @gridea-pro/dsh-skill-theme-builder */ diff --git a/package.json b/package.json index ead4a26..650a14b 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,6 @@ "name": "@gridea-pro/dsh-skill-theme-builder", "description": "Gridea Pro theme builder skill for DeepSeek Harness", "version": "0.1.0", - "publishConfig": { - "access": "public" - }, "type": "module", "main": "lib/index.js", "types": "lib/types/index.d.ts", @@ -32,8 +29,7 @@ } }, "scripts": { - "build": "tsc -p tsconfig.json", - "prepare": "tsc -p tsconfig.json" + "build": "tsc -p tsconfig.json" }, "peerDependencies": { "@deepseek-ai/dsh-skill": ">=0.0.1-rc.0", diff --git a/src/README.md b/src/README.md index 0f8f5c2..e70bb8f 100644 --- a/src/README.md +++ b/src/README.md @@ -70,56 +70,13 @@ npx @deepseek-ai/dsh web 其他用户不需要 clone 仓库,通过 `dsh plugin add` 直接从 GitHub 拉取安装。 -> **注意**:由于 pnpm ≥10 的安全策略,从 GitHub 安装**一定会经历"首次失败 → 手动授权 → 重新安装"三步**,这是 pnpm 的设计,不是 bug。如果觉得麻烦,请作者发布到 npm(见末尾说明)。 - ### 安装 -**第 1 步:首次安装(预期会失败)** - ```bash -npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-skill#dsh" -``` - -会看到类似报错: - -``` -[ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED] ... -allowBuilds: - @gridea-pro/dsh-skill-theme-builder@git+ssh://git@github.com/xiaxi626/theme-builder-skill.git#839c3efd...: true -``` - -这是正常的——pnpm 拒绝运行 git 依赖的构建脚本,需要你手动授权。 - -**第 2 步:写入构建授权** - -打开 `~/.dsh/profiles/web/pnpm-workspace.yaml`(Windows: `C:\Users\你的用户名\.dsh\profiles\web\pnpm-workspace.yaml`)。 - -这个文件已有基础内容,**不要删原有内容**,在文件末尾追加 pnpm 报错中打印的那两行: - -```yaml -# 原有内容保持不变: -packages: - - . - -nodeLinker: hoisted - -# 追加以下内容(从 pnpm 报错中原样复制 key,用单引号包起来): -allowBuilds: - '@gridea-pro/dsh-skill-theme-builder@git+ssh://git@github.com/xiaxi626/theme-builder-skill.git#839c3efd...': true +npx @deepseek-ai/dsh plugin --profile web add "github:Gridea-Pro/theme-builder-skill" ``` -关键注意点: -- key 包含完整的 git URL + commit SHA,**不能用简单包名替代** -- **从 pnpm 报错中原样复制**,不要手打(SHA 很容易抄错) -- **key 必须用单引号包起来**,因为 `@` 是 YAML 保留字符,不加引号会报 `bad indentation` 错误 -- 不要把 pnpm 报错中的注释行(`# Add the package to ...`)复制进去 -- SHA 每次推送都会变,更新插件时需要重复这个流程 - -**第 3 步:重新安装(这次会成功)** - -```bash -npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-skill#dsh" -``` +> `package.json` 没有 `prepare` 脚本,pnpm 不会触发构建授权,安装一步到位。编译产物 `lib/` 已提交在仓库中。 ### 启动 @@ -133,16 +90,10 @@ npx @deepseek-ai/dsh web npx @deepseek-ai/dsh plugin --profile web remove @gridea-pro/dsh-skill-theme-builder ``` -卸载后建议手动清理 `pnpm-workspace.yaml` 中的 `allowBuilds` 条目(原有内容保留)。 - ### 更新 ```bash -# 1. 先删掉旧的 allowBuilds 条目,重新 add 触发报错获取新 SHA -npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-skill#dsh" -# 2. 用新的 key 更新 pnpm-workspace.yaml -# 3. 重新 add -npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-skill#dsh" +npx @deepseek-ai/dsh plugin --profile web update @gridea-pro/dsh-skill-theme-builder ``` --- @@ -153,8 +104,8 @@ npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-ski |---|---|---| | 适用场景 | 开发调试 | 分发给用户 | | 需要本地克隆 | 是 | 否 | -| 需要编译 | 否(tsx 直接跑 .ts) | 是(pnpm 运行 `prepare`) | -| 需要构建授权 | 否 | 是(`allowBuilds`) | +| 需要编译 | 否(tsx 直接跑 .ts) | 否(`lib/` 已提交) | +| 需要构建授权 | 否 | 否 | | 路径硬编码 | 是(每台机器不同) | 否 | | 改代码后生效 | 重启即生效 | 需 `plugin update` | | 卸载方式 | 不带 `--patch` 重启 | `plugin remove` | @@ -193,20 +144,26 @@ npx @deepseek-ai/dsh plugin --profile web add "github:xiaxi626/theme-builder-ski **解决**:`npm install` 确保安装了 `@types/node`(已在 `devDependencies` 中声明)。 -### `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` +--- + +## 开发者须知 -**原因**:pnpm ≥10 默认拒绝运行 git 依赖的 `prepare` 脚本。 +### 修改 `src/index.ts` 后同步 `lib/` -**解决**:按上方"从 GitHub 安装"的三步流程操作——首次失败是正常的,将 pnpm 报错中打印的完整 `allowBuilds` key(含 git URL + commit SHA)追加到 `~/.dsh/profiles/web/pnpm-workspace.yaml`,然后重新 `add`。 +本项目没有 `prepare` 脚本(去掉它是为了让 GitHub 安装不需要 pnpm 构建授权)。因此修改 `src/index.ts` 后必须手动编译并提交 `lib/`: -> 注意:key 不能用简单包名,必须用 pnpm 打印的完整格式,且 commit SHA 每次推送都会变。 +```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 会红灯。 -首次 `add` 失败**不会留下残余**——pnpm 在构建授权通过之前不会写入任何依赖。如果试图 `remove` 会看到 `ERR_PNPM_CANNOT_REMOVE_MISSING_DEPS`,这是正常的,说明 profile 是干净的,无需额外清理。 +### 包管理器说明 -`~/.dsh/profiles/web/pnpm-workspace.yaml` 中的原有内容(`packages`、`nodeLinker`)是 DSH profile 自带的基础配置,**不要删**。只需追加或清理 `allowBuilds` 条目。 +本项目使用 **npm** 管理依赖(`package-lock.json` + `npm ci`)。DSH 的 `plugin add` 底层使用 pnpm 从 GitHub 拉取,但 pnpm 读的是 `package.json`,不关心仓库的 lock 文件格式——`package-lock.json` 对 pnpm 透明,会被忽略。因此两者不冲突,用户也不需要安装 pnpm。 -### `plugin update` 失效 +### `description` 自动从 SKILL.md 提取 -GitHub 安装方式下,`plugin update` 可能因 SHA 变化导致授权失效。解决方式:手动删掉 `pnpm-workspace.yaml` 中的旧 `allowBuilds` 条目,重新走"add → 报错 → 写新 key → add"流程。 +`src/index.ts` 的 `get()` 方法会运行时从 `SKILL.md` frontmatter 解析 `description` 字段,不需要在代码中维护两份。`list()` 使用一个简短的 fallback 描述,仅用于初始目录展示;模型实际获取 skill 时调用 `get()`,拿到的是 SKILL.md 中的完整描述。 diff --git a/src/index.ts b/src/index.ts index 56265c7..5775985 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,8 +8,8 @@ * * Pattern follows the official @deepseek-ai/dsh-skill-badge plugin: * - registerProvider() with a static SkillProvider - * - list() returns a pre-built candidate - * - get() reads SKILL.md at call time (body edits picked up dynamically) + * - list() returns a pre-built candidate (description read lazily in get()) + * - get() reads SKILL.md at call time (body and description picked up dynamically) * * @module @gridea-pro/dsh-skill-theme-builder */ @@ -41,21 +41,39 @@ const RESOURCE_BASE = { const INVOCATION = { modelInvocable: true, userInvocable: true } as const /** - * Routing description shown in the model-facing skill catalog. - * Must match the `description` field in SKILL.md frontmatter. - * If the frontmatter description changes, update this constant. + * Fallback description used by list() before SKILL.md is read. + * get() always reads the real description from frontmatter at call time, + * so this constant only needs to be a reasonable placeholder. */ -const DESCRIPTION = - 'Gridea Pro 博客主题开发专家。支持 Jinja2 (Pongo2)、Go Templates、EJS 三种模板引擎。' + - '提供主题脚手架生成、语法验证、渲染测试、避坑指南和完整的模板变量参考。' + - '当用户要求创建 Gridea 主题、修改 Gridea 主题、修复主题渲染问题、学习 Gridea 主题开发、' + - '从 EJS/Hugo 迁移主题时触发。' + - '触发关键词:Gridea 主题、博客主题、theme 开发、模板语法、主题配置、theme config。' +const FALLBACK_DESCRIPTION = 'Gridea Pro 博客主题开发专家' -/** Static candidate returned by every list() call. */ +/** + * Parse YAML frontmatter from a Markdown file. + * Returns { description, body } where description is extracted from the + * `description` field (supports both block scalar `>` and plain string). + * If no frontmatter is present, returns the full content as body. + */ +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] + + // Match description field (handles `>` block scalar and plain string) + const descMatch = frontmatter.match(/^description:\s*(?:>\s*\n([\s\S]*?)(?=\n\w|\n---)|(.+))$/m) + let description = FALLBACK_DESCRIPTION + if (descMatch) { + description = (descMatch[1] || descMatch[2] || FALLBACK_DESCRIPTION).trim() + } + + return { description, body } +} + +/** Static candidate returned by list(). Description is a placeholder; get() returns the real one. */ const CANDIDATE: SkillCandidate = { name: 'gridea-theme-builder', - description: DESCRIPTION, + description: FALLBACK_DESCRIPTION, invocation: INVOCATION, provider: 'gridea-theme-builder', source: 'bundled', @@ -64,29 +82,20 @@ const CANDIDATE: SkillCandidate = { locator: SKILL_BODY_URL, } -/** - * Strip YAML frontmatter (--- delimited) from a Markdown file and return - * the body. If no frontmatter is present, the original content is returned - * unchanged. - */ -function stripFrontmatter(raw: string): string { - const match = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/) - return match ? match[1] : raw -} - const provider: SkillProvider = { name: 'gridea-theme-builder', list: () => Promise.resolve([CANDIDATE]), async get(): Promise { const raw = await readFile(SKILL_BODY_URL, 'utf8') + const { description, body } = parseFrontmatter(raw) return { name: CANDIDATE.name, - description: CANDIDATE.description, + description, invocation: CANDIDATE.invocation, provider: CANDIDATE.provider, source: CANDIDATE.source, resourceBase: RESOURCE_BASE, - content: stripFrontmatter(raw), + content: body, } }, } From 51aee695601d7abe98b512bbd93d9b70111cc308 Mon Sep 17 00:00:00 2001 From: Eliauk Date: Fri, 21 Aug 2026 04:19:07 +0200 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=E7=9B=AE=E5=BD=95=E6=8F=8F=E8=BF=B0?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=20list()=EF=BC=8C=E5=B9=B6=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20frontmatter=20=E5=9D=97=E6=A0=87=E9=87=8F=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版把完整描述挪进 get(),list() 只留占位符。但 list() 返回的 candidate 才是模型路由用的目录(dsh-skill 的 snapshot() 走 toSummary(entry.candidate)), get() 要等模型选中之后才调用 —— 结果全部触发关键词都不再进入路由。 同时 parseFrontmatter 的块标量分支带了 (?=\n\w|\n---) 前瞻,要求 description 后面还有内容;而本 skill 的 description 正是最后一个字段,闭合 --- 又已被外层 正则消费,该分支永不匹配,回落到 (.+) 只抓到 ">" 一个字符。 - list() 改为 async 并读取 frontmatter,描述与 get() 同源 - 块标量分支改为按缩进行收集,支持 > 和 |,不依赖后续字段 - list() 读不到 SKILL.md 时降级为兜底描述,避免整个 skill 目录塌掉; get() 仍然抛错,因为加载一个没有正文的 skill 应当失败 - 重新编译 lib/ 验证:list() 与 get() 返回的描述均与 SKILL.md 原文逐字一致(225 字符)。 Co-Authored-By: Claude Opus 5 (1M context) --- lib/index.js | 59 +++++++++++++++++++++++++++++--------------- lib/types/index.d.ts | 4 +-- src/index.ts | 59 ++++++++++++++++++++++++++++---------------- 3 files changed, 79 insertions(+), 43 deletions(-) diff --git a/lib/index.js b/lib/index.js index 1bfdeca..2a61224 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,8 +8,8 @@ * * Pattern follows the official @deepseek-ai/dsh-skill-badge plugin: * - registerProvider() with a static SkillProvider - * - list() returns a pre-built candidate (description read lazily in get()) - * - get() reads SKILL.md at call time (body and description picked up dynamically) + * - 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 */ @@ -29,17 +29,18 @@ const RESOURCE_BASE = { }; /** Skill is available on both model and user invocation surfaces. */ const INVOCATION = { modelInvocable: true, userInvocable: true }; -/** - * Fallback description used by list() before SKILL.md is read. - * get() always reads the real description from frontmatter at call time, - * so this constant only needs to be a reasonable placeholder. - */ +/** Used only when SKILL.md is unreadable or carries no description. */ const FALLBACK_DESCRIPTION = 'Gridea Pro 博客主题开发专家'; /** - * Parse YAML frontmatter from a Markdown file. - * Returns { description, body } where description is extracted from the - * `description` field (supports both block scalar `>` and plain string). - * If no frontmatter is present, returns the full content as body. + * 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]*)$/); @@ -47,18 +48,18 @@ function parseFrontmatter(raw) { return { description: FALLBACK_DESCRIPTION, body: raw }; const frontmatter = match[1]; const body = match[2]; - // Match description field (handles `>` block scalar and plain string) - const descMatch = frontmatter.match(/^description:\s*(?:>\s*\n([\s\S]*?)(?=\n\w|\n---)|(.+))$/m); - let description = FALLBACK_DESCRIPTION; - if (descMatch) { - description = (descMatch[1] || descMatch[2] || FALLBACK_DESCRIPTION).trim(); + 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 }; } - return { description, body }; + const plain = frontmatter.match(/^description:[ \t]*(\S.*?)[ \t]*$/m); + return { description: plain ? plain[1] : FALLBACK_DESCRIPTION, body }; } -/** Static candidate returned by list(). Description is a placeholder; get() returns the real one. */ +/** Candidate shape shared by list() and get(); `description` is filled in from SKILL.md. */ const CANDIDATE = { name: 'gridea-theme-builder', - description: FALLBACK_DESCRIPTION, invocation: INVOCATION, provider: 'gridea-theme-builder', source: 'bundled', @@ -68,7 +69,25 @@ const CANDIDATE = { }; const provider = { name: 'gridea-theme-builder', - list: () => Promise.resolve([CANDIDATE]), + /** + * 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); diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts index f05dfff..f0f7af2 100644 --- a/lib/types/index.d.ts +++ b/lib/types/index.d.ts @@ -8,8 +8,8 @@ * * Pattern follows the official @deepseek-ai/dsh-skill-badge plugin: * - registerProvider() with a static SkillProvider - * - list() returns a pre-built candidate (description read lazily in get()) - * - get() reads SKILL.md at call time (body and description picked up dynamically) + * - 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 */ diff --git a/src/index.ts b/src/index.ts index 5775985..1ba0859 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,8 +8,8 @@ * * Pattern follows the official @deepseek-ai/dsh-skill-badge plugin: * - registerProvider() with a static SkillProvider - * - list() returns a pre-built candidate (description read lazily in get()) - * - get() reads SKILL.md at call time (body and description picked up dynamically) + * - 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 */ @@ -40,18 +40,19 @@ const RESOURCE_BASE = { /** Skill is available on both model and user invocation surfaces. */ const INVOCATION = { modelInvocable: true, userInvocable: true } as const -/** - * Fallback description used by list() before SKILL.md is read. - * get() always reads the real description from frontmatter at call time, - * so this constant only needs to be a reasonable placeholder. - */ +/** Used only when SKILL.md is unreadable or carries no description. */ const FALLBACK_DESCRIPTION = 'Gridea Pro 博客主题开发专家' /** - * Parse YAML frontmatter from a Markdown file. - * Returns { description, body } where description is extracted from the - * `description` field (supports both block scalar `>` and plain string). - * If no frontmatter is present, returns the full content as body. + * 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]*)$/) @@ -60,20 +61,19 @@ function parseFrontmatter(raw: string): { description: string; body: string } { const frontmatter = match[1] const body = match[2] - // Match description field (handles `>` block scalar and plain string) - const descMatch = frontmatter.match(/^description:\s*(?:>\s*\n([\s\S]*?)(?=\n\w|\n---)|(.+))$/m) - let description = FALLBACK_DESCRIPTION - if (descMatch) { - description = (descMatch[1] || descMatch[2] || FALLBACK_DESCRIPTION).trim() + 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 } } - return { description, body } + const plain = frontmatter.match(/^description:[ \t]*(\S.*?)[ \t]*$/m) + return { description: plain ? plain[1] : FALLBACK_DESCRIPTION, body } } -/** Static candidate returned by list(). Description is a placeholder; get() returns the real one. */ -const CANDIDATE: SkillCandidate = { +/** Candidate shape shared by list() and get(); `description` is filled in from SKILL.md. */ +const CANDIDATE: Omit = { name: 'gridea-theme-builder', - description: FALLBACK_DESCRIPTION, invocation: INVOCATION, provider: 'gridea-theme-builder', source: 'bundled', @@ -84,7 +84,24 @@ const CANDIDATE: SkillCandidate = { const provider: SkillProvider = { name: 'gridea-theme-builder', - list: () => Promise.resolve([CANDIDATE]), + /** + * 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)