@dsh-plugin/dsh-force-read-skill 0.0.0-stage → 0.1.0-dev.42788551751
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 +43 -2
- package/README.zh_CN.md +44 -0
- package/cordis.patch.yml +3 -0
- package/lib/history.d.ts +49 -0
- package/lib/history.js +60 -0
- package/lib/index.d.ts +43 -0
- package/lib/index.js +68 -0
- package/package.json +48 -4
package/README.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# DSH Force Read Skill
|
|
4
|
+
|
|
5
|
+
**Require a DeepSeek Harness model to read a skill before using other tools.**
|
|
6
|
+
|
|
7
|
+
English | [简体中文](README.zh_CN.md)
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
[](https://github.com/dsh-plugins/dsh-force-read-skill/actions/workflows/npm-publish.yml)
|
|
12
|
+
[](https://www.npmjs.com/package/@dsh-plugin/dsh-force-read-skill)
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Install dsh-loader 2.0 and the plugin in the same Web profile.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
dsh plugin --profile web add "@dsh-plugin/dsh-loader@next"
|
|
20
|
+
dsh plugin --profile web add "@dsh-plugin/dsh-force-read-skill"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Edit and save the configuration on the plugin details page. Saved changes take effect immediately.
|
|
24
|
+
|
|
25
|
+
| Setting | Default | Behavior |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| Require skill reading (`enabled`) | Enabled | Reject other tool calls until the model reads an eligible skill. |
|
|
28
|
+
| Required skills (`requiredSkills`) | Empty list | An empty list accepts any skill. Otherwise, the model must read one skill from the list. |
|
|
29
|
+
|
|
30
|
+
A successful read applies to the current session, including resumed sessions, until the next successful context compaction. A `/skill-name` invocation that injects eligible skill instructions also satisfies the requirement.
|
|
31
|
+
|
|
32
|
+
The `skill` tool is exempt. Programmatic Tool Calling (PTC) presets can read through `run_code` and receive the instructions in the next model request. Other tool calls in the same batch still undergo the check.
|
|
33
|
+
|
|
34
|
+
Rejected calls return **You must read those skills that you should read.**
|
|
35
|
+
|
|
36
|
+
## Build and test
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm ci
|
|
40
|
+
npm run typecheck
|
|
41
|
+
npm run build
|
|
42
|
+
npm test
|
|
43
|
+
npm pack --dry-run
|
|
44
|
+
```
|
package/README.zh_CN.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# DSH Force Read Skill
|
|
4
|
+
|
|
5
|
+
**要求 DeepSeek Harness 模型先读取 SKILL,再使用其他工具。**
|
|
6
|
+
|
|
7
|
+
[English](README.md) | 简体中文
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
[](https://github.com/dsh-plugins/dsh-force-read-skill/actions/workflows/npm-publish.yml)
|
|
12
|
+
[](https://www.npmjs.com/package/@dsh-plugin/dsh-force-read-skill)
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
在同一个 Web 配置档案中安装 dsh-loader 2.0 和本插件。
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
dsh plugin --profile web add "@dsh-plugin/dsh-loader@next"
|
|
20
|
+
dsh plugin --profile web add "@dsh-plugin/dsh-force-read-skill"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
在插件详情页修改配置,保存后立即生效。
|
|
24
|
+
|
|
25
|
+
| 配置项 | 默认值 | 行为 |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| 强制阅读 SKILL(`enabled`) | 开启 | 模型成功读取符合要求的 SKILL 后,才可调用其他工具。 |
|
|
28
|
+
| 指定 SKILL(`requiredSkills`) | 空列表 | 留空时可读取任意 SKILL。填写后,读取列表中的任意一个即可。 |
|
|
29
|
+
|
|
30
|
+
成功读取后,当前会话放行至下一次成功的上下文压缩。恢复会话时沿用该会话的记录。通过 `/技能名` 注入符合要求的技能正文也可满足要求。
|
|
31
|
+
|
|
32
|
+
插件豁免 `skill`。程序化工具调用(Programmatic Tool Calling,PTC)预设可通过 `run_code` 读取 SKILL,正文会随下一步请求发送给模型。同一批次内的其他工具调用仍需通过检查。
|
|
33
|
+
|
|
34
|
+
未满足要求时,工具返回 **You must read those skills that you should read.**
|
|
35
|
+
|
|
36
|
+
## 构建与测试
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm ci
|
|
40
|
+
npm run typecheck
|
|
41
|
+
npm run build
|
|
42
|
+
npm test
|
|
43
|
+
npm pack --dry-run
|
|
44
|
+
```
|
package/cordis.patch.yml
ADDED
package/lib/history.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface ContentBlock {
|
|
2
|
+
readonly type: string;
|
|
3
|
+
readonly text?: string;
|
|
4
|
+
}
|
|
5
|
+
interface ToolResultBlock extends ContentBlock {
|
|
6
|
+
readonly isError?: boolean;
|
|
7
|
+
}
|
|
8
|
+
type SessionEvent = {
|
|
9
|
+
readonly type: 'step/end';
|
|
10
|
+
} | {
|
|
11
|
+
readonly type: 'compaction/end';
|
|
12
|
+
readonly data: {
|
|
13
|
+
readonly error?: string;
|
|
14
|
+
};
|
|
15
|
+
} | {
|
|
16
|
+
readonly type: 'tool/call';
|
|
17
|
+
readonly data: {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly arguments: string;
|
|
20
|
+
};
|
|
21
|
+
} | {
|
|
22
|
+
readonly type: 'tool/result';
|
|
23
|
+
readonly sourceEventSeqs?: readonly number[];
|
|
24
|
+
readonly data: {
|
|
25
|
+
readonly message: {
|
|
26
|
+
readonly role: string;
|
|
27
|
+
readonly isError?: boolean;
|
|
28
|
+
readonly content: readonly ToolResultBlock[];
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
} | {
|
|
32
|
+
readonly type: 'user/message';
|
|
33
|
+
readonly data: {
|
|
34
|
+
readonly source: {
|
|
35
|
+
readonly kind: string;
|
|
36
|
+
readonly form?: string;
|
|
37
|
+
};
|
|
38
|
+
readonly content: readonly ContentBlock[];
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export interface SessionHistory {
|
|
42
|
+
readonly events?: readonly SessionEvent[];
|
|
43
|
+
snapshotEvents?(): readonly SessionEvent[];
|
|
44
|
+
}
|
|
45
|
+
/** 从完整的技能正文提取宿主渲染的名称。 */
|
|
46
|
+
export declare function readSkillName(content: readonly ContentBlock[]): string | undefined;
|
|
47
|
+
/** 查找最近一次成功压缩后已交付给模型的技能读取结果。 */
|
|
48
|
+
export declare function hasReadSkill(session: SessionHistory, requiredSkills: readonly string[]): boolean;
|
|
49
|
+
export {};
|
package/lib/history.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const skillNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2
|
+
/** 从完整的技能正文提取宿主渲染的名称。 */
|
|
3
|
+
export function readSkillName(content) {
|
|
4
|
+
for (const block of content) {
|
|
5
|
+
if (block.type !== 'text' || !block.text?.trimEnd().endsWith('</skill_content>')) {
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
const name = /^<skill_content name="([^"]+)">\r?\n/.exec(block.text)?.[1];
|
|
9
|
+
if (name !== undefined && skillNamePattern.test(name)) {
|
|
10
|
+
return name;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
/** 查找最近一次成功压缩后已交付给模型的技能读取结果。 */
|
|
16
|
+
export function hasReadSkill(session, requiredSkills) {
|
|
17
|
+
const events = session.snapshotEvents?.() ?? session.events ?? [];
|
|
18
|
+
let previousStep = false;
|
|
19
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
20
|
+
const event = events[index];
|
|
21
|
+
if (event.type === 'compaction/end' && event.data.error === undefined) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (event.type === 'step/end') {
|
|
25
|
+
previousStep = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
let name;
|
|
29
|
+
if (event.type === 'user/message' && event.data.source.kind === 'skill-invocation'
|
|
30
|
+
&& event.data.source.form === 'instructions') {
|
|
31
|
+
name = readSkillName(event.data.content);
|
|
32
|
+
}
|
|
33
|
+
else if (previousStep && event.type === 'tool/result') {
|
|
34
|
+
const callSeq = event.sourceEventSeqs?.[0];
|
|
35
|
+
const call = callSeq === undefined ? undefined : events[callSeq];
|
|
36
|
+
if (call?.type !== 'tool/call' || call.data.name !== 'skill') {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const message = event.data.message;
|
|
40
|
+
const result = message.role === 'tool' ? message : message.content[0];
|
|
41
|
+
if (result?.isError !== false) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// DSH 可能将长正文写入溢出文件。成功结果确认读取,原始调用保留技能名称。
|
|
45
|
+
try {
|
|
46
|
+
const args = JSON.parse(call.data.arguments);
|
|
47
|
+
if (typeof args?.name === 'string' && skillNamePattern.test(args.name)) {
|
|
48
|
+
name = args.name;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (name !== undefined && (requiredSkills.length === 0 || requiredSkills.includes(name))) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { CordisContext, HostAPI } from '@dsh-plugin/dsh-loader';
|
|
2
|
+
import { type ContentBlock, type SessionHistory } from './history.js';
|
|
3
|
+
export declare const name = "@dsh-plugin/dsh-force-read-skill";
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
interface Execution {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly agent?: {
|
|
8
|
+
readonly session: SessionHistory;
|
|
9
|
+
};
|
|
10
|
+
readonly parent?: object;
|
|
11
|
+
}
|
|
12
|
+
interface Result {
|
|
13
|
+
readonly isError: boolean;
|
|
14
|
+
readonly content: readonly ContentBlock[];
|
|
15
|
+
}
|
|
16
|
+
type PreDecision = {
|
|
17
|
+
readonly kind: 'allow' | 'cancel';
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: 'ask';
|
|
20
|
+
readonly reason?: string;
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: 'deny';
|
|
23
|
+
readonly reason: string;
|
|
24
|
+
};
|
|
25
|
+
type PostDecision = {
|
|
26
|
+
readonly kind: 'accept';
|
|
27
|
+
readonly content?: readonly ContentBlock[];
|
|
28
|
+
readonly value?: unknown;
|
|
29
|
+
readonly additionalContexts?: readonly unknown[];
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: 'block';
|
|
32
|
+
readonly feedback: readonly ContentBlock[];
|
|
33
|
+
};
|
|
34
|
+
interface PluginContext extends CordisContext {
|
|
35
|
+
readonly dshLoader: HostAPI;
|
|
36
|
+
on(event: 'tools/pre-execute', listener: (exec: Execution, next: () => Promise<PreDecision>) => Promise<PreDecision>, options?: {
|
|
37
|
+
prepend?: boolean;
|
|
38
|
+
}): () => void;
|
|
39
|
+
on(event: 'tools/post-execute', listener: (exec: Execution, result: Result, next: () => Promise<PostDecision>) => Promise<PostDecision>): () => void;
|
|
40
|
+
}
|
|
41
|
+
/** 注册阅读要求,并在工具请求进入权限检查前执行校验。 */
|
|
42
|
+
export declare function apply(ctx: PluginContext): Promise<void>;
|
|
43
|
+
export {};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { defineConfig } from '@dsh-plugin/dsh-loader/config';
|
|
2
|
+
import { hasReadSkill, readSkillName } from './history.js';
|
|
3
|
+
export const name = '@dsh-plugin/dsh-force-read-skill';
|
|
4
|
+
export const inject = ['dshLoader', 'tools'];
|
|
5
|
+
const id = 'dsh-force-read-skill';
|
|
6
|
+
const denialReason = 'You must read those skills that you should read.';
|
|
7
|
+
/** 注册阅读要求,并在工具请求进入权限检查前执行校验。 */
|
|
8
|
+
export async function apply(ctx) {
|
|
9
|
+
const loader = ctx.dshLoader;
|
|
10
|
+
const preferences = await loader.config.register(ctx, defineConfig({
|
|
11
|
+
id,
|
|
12
|
+
packageName: name,
|
|
13
|
+
title: '强制阅读 SKILL',
|
|
14
|
+
fields: [
|
|
15
|
+
{ type: 'boolean', key: 'enabled', label: '强制阅读 SKILL', default: true },
|
|
16
|
+
{
|
|
17
|
+
type: 'array',
|
|
18
|
+
key: 'requiredSkills',
|
|
19
|
+
label: '指定 SKILL',
|
|
20
|
+
description: '读取其中任意一个即可。留空时可读取任意 SKILL。',
|
|
21
|
+
default: [],
|
|
22
|
+
item: { type: 'text', label: 'SKILL 名称', pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$' },
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
}));
|
|
26
|
+
const check = (exec) => {
|
|
27
|
+
const { enabled, requiredSkills } = preferences.get().value;
|
|
28
|
+
if (!enabled || !exec.agent || exec.name === 'skill' || exec.name === 'run_code') {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
if (hasReadSkill(exec.agent.session, requiredSkills)) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return denialReason;
|
|
35
|
+
};
|
|
36
|
+
// 先于权限询问拒绝调用,并在宿主的最终检查中再次执行同一规则。
|
|
37
|
+
ctx.effect(() => ctx.on('tools/pre-execute', async (exec, next) => {
|
|
38
|
+
const reason = check(exec);
|
|
39
|
+
return reason === undefined ? next() : { kind: 'deny', reason };
|
|
40
|
+
}, { prepend: true }), `${id}:check`);
|
|
41
|
+
const tools = loader.services.get('tools');
|
|
42
|
+
ctx.effect(() => tools.guard(check), `${id}:guard`);
|
|
43
|
+
ctx.effect(() => ctx.on('tools/post-execute', async (exec, result, next) => {
|
|
44
|
+
if (!exec.agent || !exec.parent || exec.name !== 'skill' || result.isError) {
|
|
45
|
+
return next();
|
|
46
|
+
}
|
|
47
|
+
const { enabled, requiredSkills } = preferences.get().value;
|
|
48
|
+
if (!enabled) {
|
|
49
|
+
return next();
|
|
50
|
+
}
|
|
51
|
+
const decision = await next();
|
|
52
|
+
if (decision.kind !== 'accept' || Object.hasOwn(decision, 'value')) {
|
|
53
|
+
return decision;
|
|
54
|
+
}
|
|
55
|
+
const content = decision.content ?? result.content;
|
|
56
|
+
const skill = readSkillName(content);
|
|
57
|
+
if (skill === undefined || (requiredSkills.length !== 0 && !requiredSkills.includes(skill))
|
|
58
|
+
|| hasReadSkill(exec.agent.session, requiredSkills)) {
|
|
59
|
+
return decision;
|
|
60
|
+
}
|
|
61
|
+
// PTC 的子调用结果只交给程序。附加正文后,模型可在下一步直接读取。
|
|
62
|
+
const instructions = loader.llm.createUserMessage({
|
|
63
|
+
source: { kind: 'skill-invocation', name: skill, form: 'instructions' },
|
|
64
|
+
content,
|
|
65
|
+
});
|
|
66
|
+
return { ...decision, additionalContexts: [...decision.additionalContexts ?? [], instructions] };
|
|
67
|
+
}), `${id}:deliver-ptc-skill`);
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plugin/dsh-force-read-skill",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-dev.42788551751",
|
|
4
|
+
"description": "要求 DeepSeek Harness 模型先读取 SKILL,再执行其他工具。",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/dsh-plugins/dsh-force-read-skill.git"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./lib/index.js",
|
|
14
|
+
"types": "./lib/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./lib/index.d.ts",
|
|
18
|
+
"default": "./lib/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"lib",
|
|
25
|
+
"cordis.patch.yml",
|
|
26
|
+
"README.zh_CN.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.json",
|
|
30
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
31
|
+
"test": "npm run build && node --test",
|
|
32
|
+
"prepack": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=24.9.0"
|
|
36
|
+
},
|
|
37
|
+
"dsh": {
|
|
38
|
+
"bundle": {
|
|
39
|
+
"patch": "./cordis.patch.yml"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@dsh-plugin/dsh-loader": "^2.0.0-dev.42783642741"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@dsh-plugin/dsh-loader": "2.0.0-dev.42783642741",
|
|
47
|
+
"@types/node": "^24.0.0",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
}
|
|
50
|
+
}
|