dsh-skill-importer 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/index.js +14 -17
- package/package.json +1 -1
- package/src/server.ts +5 -7
- package/tests/batch-import.test.mjs +7 -1
package/README.md
CHANGED
|
@@ -171,7 +171,7 @@ If `dsh` is not installed globally, replace the last line with `npx @deepseek-ai
|
|
|
171
171
|
## Notes
|
|
172
172
|
|
|
173
173
|
- A single imported file may be up to 256 KB.
|
|
174
|
-
- Batch import accepts `.claude/skills`, `.codex/skills`,
|
|
174
|
+
- Batch import accepts any agent directory named `skills` (for example `.claude/skills`, `.codex/skills`, or a custom `agent/skills`) or one skill directory directly below it. A scan accepts up to 200 skills; each skill may contain up to 2,000 files and 10 MB of resources. Symbolic links are refused.
|
|
175
175
|
- URL import is HTTPS-only and refuses loopback, private, link-local, and reserved addresses; every redirect is validated again. `.md` sources are preserved, while HTML pages use lightweight text extraction, so direct Markdown URLs work best.
|
|
176
176
|
- The installed list polls briefly after import (every 2 seconds for up to 20 seconds). **Refresh** syncs external changes immediately.
|
|
177
177
|
|
package/README.zh.md
CHANGED
|
@@ -171,7 +171,7 @@ dsh plugin --profile web add /path/to/dsh-skill-importer
|
|
|
171
171
|
## 注意事项
|
|
172
172
|
|
|
173
173
|
- 单个导入文件最大 256 KB。
|
|
174
|
-
-
|
|
174
|
+
- 批量导入接受任意 Agent 中名为 `skills` 的目录(例如 `.claude/skills`、`.codex/skills`、项目自定义的 `agent/skills`)或其中的单个技能目录;每次最多扫描 200 个技能,单个技能最多包含 2,000 个文件和 10 MB 资源,不接受符号链接。
|
|
175
175
|
- URL 导入仅支持 HTTPS,并拒绝本机、私有网络、链路本地和保留地址;每次重定向都会重新校验。`.md` 会原样保留,HTML 页面仅做轻量正文提取,因此推荐使用 Markdown 直链。
|
|
176
176
|
- 导入后列表会短暂轮询(每 2 秒一次,最多 20 秒);外部改动可点击 **刷新** 立即同步。
|
|
177
177
|
|
package/lib/index.js
CHANGED
|
@@ -99,12 +99,6 @@ const MAX_BATCH_FILES_PER_SKILL = 2e3;
|
|
|
99
99
|
const MAX_BATCH_SKILL_BYTES = 10485760;
|
|
100
100
|
const BATCH_SCAN_TTL_MS = 6e5;
|
|
101
101
|
const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
102
|
-
const AGENT_SKILL_PARENTS = /* @__PURE__ */ new Set([
|
|
103
|
-
".agents",
|
|
104
|
-
".claude",
|
|
105
|
-
".codex",
|
|
106
|
-
".dsh"
|
|
107
|
-
]);
|
|
108
102
|
const MAX_URL_REDIRECTS = 5;
|
|
109
103
|
/** The harness home (`$DSH_HOME`, defaulting to `~/.dsh`). */
|
|
110
104
|
function dshHomeDir() {
|
|
@@ -311,12 +305,9 @@ function scanBatch(request) {
|
|
|
311
305
|
if (!isAbsolute(request.sourcePath)) throw new Error("批量导入目录必须是绝对路径");
|
|
312
306
|
if (!existsSync(request.sourcePath) || !statSync(request.sourcePath).isDirectory()) throw new Error("批量导入目录不存在或不可读");
|
|
313
307
|
const sourcePath = realpathSync(request.sourcePath);
|
|
314
|
-
const leaf = basename(sourcePath);
|
|
315
|
-
const parent = basename(dirname(sourcePath));
|
|
316
|
-
|
|
317
|
-
const isSkillsRoot = leaf === "skills" && AGENT_SKILL_PARENTS.has(parent);
|
|
318
|
-
const isSkillDirectory = parent === "skills" && AGENT_SKILL_PARENTS.has(grandparent);
|
|
319
|
-
if (!isSkillsRoot && !isSkillDirectory) throw new Error("批量导入仅支持 .claude、.codex、.agents 或 .dsh 下的 skills 目录");
|
|
308
|
+
const leaf = basename(sourcePath).toLowerCase();
|
|
309
|
+
const parent = basename(dirname(sourcePath)).toLowerCase();
|
|
310
|
+
if (!(leaf === "skills") && !(parent === "skills")) throw new Error("批量导入仅支持名为 skills 的目录或其中的单个技能目录");
|
|
320
311
|
if (request.target !== "user" && request.workspacePath === void 0) throw new Error("项目目标需要 workspacePath(当前工作区路径)");
|
|
321
312
|
const sources = batchSources(sourcePath);
|
|
322
313
|
if (sources.length === 0) throw new Error("所选目录中没有找到可导入的技能");
|
|
@@ -761,11 +752,17 @@ function apply(ctx) {
|
|
|
761
752
|
}
|
|
762
753
|
const workspacePath = requireWorkspace(body.target, body.workspacePath);
|
|
763
754
|
pruneBatchScans();
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
755
|
+
let session;
|
|
756
|
+
try {
|
|
757
|
+
session = scanBatch({
|
|
758
|
+
sourcePath: body.sourcePath,
|
|
759
|
+
target: body.target,
|
|
760
|
+
workspacePath
|
|
761
|
+
});
|
|
762
|
+
} catch (error) {
|
|
763
|
+
sendError(res, 400, error instanceof Error ? error.message : String(error));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
769
766
|
batchScans.set(session.scanId, session);
|
|
770
767
|
sendJson(res, 200, {
|
|
771
768
|
ok: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-importer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Web UI plugin for DeepSeek Harness: import skills from local Markdown files or URLs into the skill roots the harness discovers automatically",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/src/server.ts
CHANGED
|
@@ -34,7 +34,6 @@ export const MAX_BATCH_SKILL_BYTES = 10 * 1024 * 1024
|
|
|
34
34
|
export const BATCH_SCAN_TTL_MS = 10 * 60 * 1000
|
|
35
35
|
|
|
36
36
|
const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
37
|
-
const AGENT_SKILL_PARENTS = new Set(['.agents', '.claude', '.codex', '.dsh'])
|
|
38
37
|
const MAX_URL_REDIRECTS = 5
|
|
39
38
|
|
|
40
39
|
/** The harness home (`$DSH_HOME`, defaulting to `~/.dsh`). */
|
|
@@ -284,13 +283,12 @@ export function scanBatch(request: BatchScanRequest): BatchScanSession {
|
|
|
284
283
|
if (!isAbsolute(request.sourcePath)) throw new Error('批量导入目录必须是绝对路径')
|
|
285
284
|
if (!existsSync(request.sourcePath) || !statSync(request.sourcePath).isDirectory()) throw new Error('批量导入目录不存在或不可读')
|
|
286
285
|
const sourcePath = realpathSync(request.sourcePath)
|
|
287
|
-
const leaf = basename(sourcePath)
|
|
288
|
-
const parent = basename(dirname(sourcePath))
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
const isSkillDirectory = parent === 'skills' && AGENT_SKILL_PARENTS.has(grandparent)
|
|
286
|
+
const leaf = basename(sourcePath).toLowerCase()
|
|
287
|
+
const parent = basename(dirname(sourcePath)).toLowerCase()
|
|
288
|
+
const isSkillsRoot = leaf === 'skills'
|
|
289
|
+
const isSkillDirectory = parent === 'skills'
|
|
292
290
|
if (!isSkillsRoot && !isSkillDirectory) {
|
|
293
|
-
throw new Error('
|
|
291
|
+
throw new Error('批量导入仅支持名为 skills 的目录或其中的单个技能目录')
|
|
294
292
|
}
|
|
295
293
|
if (request.target !== 'user' && request.workspacePath === undefined) throw new Error('项目目标需要 workspacePath(当前工作区路径)')
|
|
296
294
|
const sources = batchSources(sourcePath)
|
|
@@ -70,13 +70,19 @@ try {
|
|
|
70
70
|
assert.match(changedResults[0]?.message ?? '', /发生变化/)
|
|
71
71
|
assert.equal(existsSync(join(process.env.DSH_HOME, 'skills', 'beta')), false)
|
|
72
72
|
|
|
73
|
+
// Any agent may use its own parent directory; the selected root name is the boundary.
|
|
74
|
+
const customAgentSource = join(sandbox, 've_hcs_agent', 'skills')
|
|
75
|
+
mkdirSync(join(customAgentSource, 'gamma'), { recursive: true })
|
|
76
|
+
writeFileSync(join(customAgentSource, 'gamma', 'SKILL.md'), skill('gamma'))
|
|
77
|
+
assert.equal(scanBatch({ sourcePath: customAgentSource, target: 'user' }).entries[0]?.name, 'gamma')
|
|
78
|
+
|
|
73
79
|
const empty = join(sandbox, '.agents', 'skills')
|
|
74
80
|
mkdirSync(empty, { recursive: true })
|
|
75
81
|
assert.throws(() => scanBatch({ sourcePath: empty, target: 'user' }), /没有找到/)
|
|
76
82
|
|
|
77
83
|
const arbitrary = join(sandbox, 'arbitrary')
|
|
78
84
|
mkdirSync(arbitrary)
|
|
79
|
-
assert.throws(() => scanBatch({ sourcePath: arbitrary, target: 'user' }),
|
|
85
|
+
assert.throws(() => scanBatch({ sourcePath: arbitrary, target: 'user' }), /名为 skills/)
|
|
80
86
|
|
|
81
87
|
assert.equal(originAllowed({ headers: {} }), false)
|
|
82
88
|
assert.equal(originAllowed({ headers: { origin: 'https://evil.example' } }), false)
|