dsh-plugin-book-deconstruct 0.0.0-stage → 1.3.0
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/LICENSE +21 -0
- package/README.md +57 -2
- package/cordis.patch.yml +4 -0
- package/icon.svg +10 -0
- package/index.js +512 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +47 -4
- package//347/254/254/344/270/200/347/247/215/344/275/277/347/224/250/346/226/271/345/274/217.png +0 -0
- package//347/254/254/344/272/214/347/247/215/344/275/277/347/224/250/346/226/271/345/274/217.png +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,58 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-plugin-book-deconstruct
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> GitHub:`zmz123612/dsh-plugin-book-deconstruct`
|
|
4
|
+
|
|
5
|
+
DeepSeek Harness **Host 插件**:装进 profile 后会出现在插件列表,**任意会话**都可用:
|
|
6
|
+
|
|
7
|
+
# 好用的书籍拆解工具
|
|
8
|
+
|
|
9
|
+
- 使用方式: `帮我解读书籍 xxx 作者xxx` 或者 `/dsh-plugin-book-deconstruct 书名 | 作者`
|
|
10
|
+
- 工具:`dsh-plugin-book-deconstruct`(智能体在对话里调用)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## 使用方式截图
|
|
14
|
+
|
|
15
|
+
### 第一种使用方式
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
### 第二种使用方式
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## 输出两段:
|
|
25
|
+
|
|
26
|
+
1. **故事情节** — 讲清整本书(不是一句话短评,也不灌水)
|
|
27
|
+
2. **一段有深度的思考** — 只写一整段
|
|
28
|
+
|
|
29
|
+
## 安装
|
|
30
|
+
|
|
31
|
+
从 npm(推荐):
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
dsh plugin --profile desktop add dsh-plugin-book-deconstruct
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
从 GitHub:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
dsh plugin --profile desktop add github:OWNER/dsh-plugin-book-deconstruct
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
装完后**完全退出并重新打开** DeepSeek Harness。
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## 配置(可选)
|
|
49
|
+
|
|
50
|
+
在 profile 的 `cordis.patch.yml` 按行 id `dsh-plugin-book-deconstruct` 覆盖:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
- id: dsh-plugin-book-deconstruct
|
|
54
|
+
name: dsh-plugin-book-deconstruct
|
|
55
|
+
config:
|
|
56
|
+
provider: '' # 与 model 成对;空则用默认模型
|
|
57
|
+
model: ''
|
|
58
|
+
```
|
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Book deconstruct preset">
|
|
2
|
+
<rect x="8" y="10" width="40" height="44" rx="8" fill="#eef4ff" stroke="#2f6fed" stroke-width="3"/>
|
|
3
|
+
<path d="M28 15 V49" stroke="#8fb6f7" stroke-width="2.5" stroke-linecap="round"/>
|
|
4
|
+
<path d="M15 21 H23" stroke="#2f6fed" stroke-width="3" stroke-linecap="round"/>
|
|
5
|
+
<path d="M15 28 H23" stroke="#8fb6f7" stroke-width="3" stroke-linecap="round"/>
|
|
6
|
+
<path d="M15 35 H21" stroke="#8fb6f7" stroke-width="3" stroke-linecap="round"/>
|
|
7
|
+
<path d="M33 21 H41" stroke="#2f6fed" stroke-width="3" stroke-linecap="round"/>
|
|
8
|
+
<path d="M33 28 H41" stroke="#8fb6f7" stroke-width="3" stroke-linecap="round"/>
|
|
9
|
+
<path d="M46 36 L47.8 41.2 L53 43 L47.8 44.8 L46 50 L44.2 44.8 L39 43 L44.2 41.2 Z" fill="#f2b622" stroke="#d19a12" stroke-width="1.5" stroke-linejoin="round"/>
|
|
10
|
+
</svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* book-deconstruct — 输入书名 + 作者,直接输出成品级书籍解读。
|
|
3
|
+
*
|
|
4
|
+
* 输出只保留两部分:
|
|
5
|
+
* 1. 书籍的故事情节(讲清整本书,不要一句话短评,也不要灌水)
|
|
6
|
+
* 2. 一段有深度的思考(最后只写一段,不列条目)
|
|
7
|
+
*
|
|
8
|
+
* 约束:
|
|
9
|
+
* 1. 保留用户原问题(不替换、不改写原文)
|
|
10
|
+
* 2. 同会话每次提问都重新调 dsh-plugin-book-deconstruct 工具;书名/作者重复也一样
|
|
11
|
+
*
|
|
12
|
+
* 入口:
|
|
13
|
+
* - `/dsh-plugin-book-deconstruct …`:立刻回执;把原问题投入会话;强制调工具
|
|
14
|
+
* - 自然语言(解读/帮我拆解…):保留原话,追加强制调工具提示
|
|
15
|
+
* - `dsh-plugin-book-deconstruct` 工具:真正执行解读
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { randomUUID } from 'node:crypto';
|
|
19
|
+
|
|
20
|
+
export const inject = ['llm', 'commands', 'tools', 'agents'];
|
|
21
|
+
|
|
22
|
+
const DEFAULT_TEMPERATURE = 0.5;
|
|
23
|
+
const DEFAULT_MAX_TOKENS = 8000;
|
|
24
|
+
const TOOL_NAME = 'dsh-plugin-book-deconstruct';
|
|
25
|
+
const STEER_PREFIX = `必须调用 ${TOOL_NAME} 工具`;
|
|
26
|
+
|
|
27
|
+
const USAGE = [
|
|
28
|
+
`用法:/${TOOL_NAME} 书名 | 作者`,
|
|
29
|
+
`示例:/${TOOL_NAME} 思考,快与慢 | 丹尼尔·卡尼曼`,
|
|
30
|
+
`也支持:/${TOOL_NAME} 《思考,快与慢》丹尼尔·卡尼曼`,
|
|
31
|
+
`或:/${TOOL_NAME} 帮我解读《思考,快与慢》,作者丹尼尔·卡尼曼`,
|
|
32
|
+
'也可直接说:帮我解读《思考,快与慢》,作者丹尼尔·卡尼曼',
|
|
33
|
+
].join('\n');
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// 配置
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function clampNumber(value, min, max, fallback) {
|
|
40
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
41
|
+
if (!Number.isFinite(n)) return fallback;
|
|
42
|
+
return Math.min(max, Math.max(min, n));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clampInt(value, min, max, fallback) {
|
|
46
|
+
return Math.round(clampNumber(value, min, max, fallback));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeConfig(raw) {
|
|
50
|
+
const source = raw !== null && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
51
|
+
let provider = typeof source.provider === 'string' ? source.provider.trim() : '';
|
|
52
|
+
let model = typeof source.model === 'string' ? source.model.trim() : '';
|
|
53
|
+
if ((provider === '') !== (model === '')) {
|
|
54
|
+
provider = '';
|
|
55
|
+
model = '';
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
provider,
|
|
59
|
+
model,
|
|
60
|
+
temperature: clampNumber(source.temperature, 0, 2, DEFAULT_TEMPERATURE),
|
|
61
|
+
maxTokens: clampInt(source.maxTokens, 256, 32000, DEFAULT_MAX_TOKENS),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// 输入解析
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/** 去掉首尾空白、首尾书名号/引号,并压平换行。 */
|
|
70
|
+
function cleanField(value, maxLength) {
|
|
71
|
+
if (typeof value !== 'string') return '';
|
|
72
|
+
const flat = value.replace(/\s+/g, ' ').trim();
|
|
73
|
+
const stripped = flat
|
|
74
|
+
.replace(/^[《「『"'“”]+/u, '')
|
|
75
|
+
.replace(/[》」』"'“”]+$/u, '')
|
|
76
|
+
.trim();
|
|
77
|
+
return stripped.slice(0, maxLength);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 去掉作者字段前的分隔符与「作者/by」前缀。 */
|
|
81
|
+
function cleanAuthorTail(value) {
|
|
82
|
+
let author = String(value ?? '').trim();
|
|
83
|
+
author = author.replace(/^[\s||/—\-–—,,、:;:]+/u, '').trim();
|
|
84
|
+
author = author.replace(/^(?:作者(?:是)?|by)\s*[:::]?\s*/iu, '').trim();
|
|
85
|
+
return author;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const INTENT_PREFIX =
|
|
89
|
+
/^(?:请?帮?我?)?(?:解读|拆解|总结|分析|讲讲|讲一下)(?:一下)?\s*/iu;
|
|
90
|
+
|
|
91
|
+
/** 斜杠 followup 防去重标记;解析前剥离,不污染书名/作者。 */
|
|
92
|
+
const REQUEST_MARK_RE = /\n?[请求:[^]]+]\s*$/u;
|
|
93
|
+
|
|
94
|
+
function hasBookIntent(text) {
|
|
95
|
+
return /(?:解读|拆解|总结|分析|讲讲|讲一下)/u.test(text);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function stripRequestMark(text) {
|
|
99
|
+
return text.replace(REQUEST_MARK_RE, '').trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 解析「书名 + 作者」。支持:
|
|
104
|
+
* 《书名》作者 / 书名 | 作者 / 书名|作者 / 书名 / 作者
|
|
105
|
+
* 自然语言:帮我解读《活着》,作者余华
|
|
106
|
+
* 意图 + 空格:解读 平凡的世界 路遥 / 帮我拆解 活着 余华
|
|
107
|
+
* @returns {{ title: string, author: string } | null}
|
|
108
|
+
*/
|
|
109
|
+
export function parseBookInput(raw) {
|
|
110
|
+
let text = String(raw === null || raw === undefined ? '' : raw)
|
|
111
|
+
.replace(/\r\n?/gu, '\n')
|
|
112
|
+
.trim();
|
|
113
|
+
if (text === '') return null;
|
|
114
|
+
|
|
115
|
+
text = stripRequestMark(text);
|
|
116
|
+
if (text === '') return null;
|
|
117
|
+
|
|
118
|
+
text = text.replace(new RegExp(`^/${TOOL_NAME}\\b`, 'i'), '').trim();
|
|
119
|
+
if (text === '') return null;
|
|
120
|
+
|
|
121
|
+
const intentMatch = text.match(INTENT_PREFIX);
|
|
122
|
+
const hadIntent = intentMatch !== null;
|
|
123
|
+
if (hadIntent) {
|
|
124
|
+
text = text.slice(intentMatch[0].length).trim();
|
|
125
|
+
if (text === '') return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const angle = text.match(/《([^》\n]{1,200})》(.*)$/su);
|
|
129
|
+
if (angle !== null) {
|
|
130
|
+
const title = angle[1].trim();
|
|
131
|
+
const author = cleanAuthorTail(angle[2]);
|
|
132
|
+
if (title !== '' && author !== '') return { title, author };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const named = text.match(
|
|
136
|
+
/[《「"']?([^》」"'\n||,,]{1,200}?)[》」"']?\s*[,,、]?\s*(?:作者(?:是)?|by)\s*[:::]?\s*(.+)$/iu,
|
|
137
|
+
);
|
|
138
|
+
if (named !== null) {
|
|
139
|
+
const title = cleanField(named[1], 200);
|
|
140
|
+
const author = cleanAuthorTail(named[2]);
|
|
141
|
+
if (title !== '' && author !== '') return { title, author };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const pipe = text
|
|
145
|
+
.split(/[||]/u)
|
|
146
|
+
.map((part) => part.trim())
|
|
147
|
+
.filter((part) => part !== '');
|
|
148
|
+
if (pipe.length >= 2) {
|
|
149
|
+
const title = cleanField(pipe[0], 200);
|
|
150
|
+
const author = cleanAuthorTail(pipe.slice(1).join(' '));
|
|
151
|
+
if (title !== '' && author !== '') return { title, author };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const slash = text
|
|
155
|
+
.split(/\s*\/\s*/u)
|
|
156
|
+
.map((part) => part.trim())
|
|
157
|
+
.filter((part) => part !== '');
|
|
158
|
+
if (slash.length >= 2) {
|
|
159
|
+
const title = cleanField(slash[0], 200);
|
|
160
|
+
const author = cleanAuthorTail(slash.slice(1).join(' '));
|
|
161
|
+
if (title !== '' && author !== '') return { title, author };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const lines = text
|
|
165
|
+
.split('\n')
|
|
166
|
+
.map((line) => line.trim())
|
|
167
|
+
.filter((line) => line !== '');
|
|
168
|
+
if (lines.length >= 2) {
|
|
169
|
+
const title = cleanField(lines[0], 200);
|
|
170
|
+
const author = cleanAuthorTail(lines.slice(1).join(' '));
|
|
171
|
+
if (title !== '' && author !== '') return { title, author };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 仅在有解读意图时:末段当作者,前面当书名(避免误伤普通两词输入)
|
|
175
|
+
if (hadIntent) {
|
|
176
|
+
const parts = text
|
|
177
|
+
.split(/\s+/u)
|
|
178
|
+
.map((part) => part.trim())
|
|
179
|
+
.filter((part) => part !== '');
|
|
180
|
+
if (parts.length >= 2) {
|
|
181
|
+
const title = cleanField(parts.slice(0, -1).join(' '), 200);
|
|
182
|
+
const author = cleanAuthorTail(parts[parts.length - 1]);
|
|
183
|
+
if (title !== '' && author !== '') return { title, author };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function newRequestId() {
|
|
191
|
+
return `${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 强制调工具;每次带唯一 requestId,避免同书重复请求被当成重复消息。 */
|
|
195
|
+
function buildToolSteerPrompt(parsed, raw, requestId) {
|
|
196
|
+
const target =
|
|
197
|
+
parsed !== null
|
|
198
|
+
? `解读《${parsed.title}》,作者${parsed.author}`
|
|
199
|
+
: `处理:${raw}`;
|
|
200
|
+
return (
|
|
201
|
+
`${STEER_PREFIX}(${requestId})${target}。` +
|
|
202
|
+
'这是一次全新请求:即使用户重复同一本书,或本会话已解读过其他书,也必须再次调用工具;' +
|
|
203
|
+
'禁止复用旧结果,禁止自己写短评代替。调用后把工具返回的全文原样展示给用户。'
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function messageText(message) {
|
|
208
|
+
if (message === null || message === undefined || !Array.isArray(message.content)) return '';
|
|
209
|
+
return message.content
|
|
210
|
+
.filter((block) => block !== null && typeof block === 'object' && block.type === 'text')
|
|
211
|
+
.map((block) => String(block.text ?? ''))
|
|
212
|
+
.join('\n')
|
|
213
|
+
.trim();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 是否需要追加强制调工具提示。
|
|
218
|
+
* 斜杠 followup 只投入「书名 | 作者」原文时,也要能识别并强制调工具。
|
|
219
|
+
*/
|
|
220
|
+
function shouldHandleFreeform(text) {
|
|
221
|
+
if (text === '') return false;
|
|
222
|
+
if (text.includes(STEER_PREFIX)) return false;
|
|
223
|
+
|
|
224
|
+
const parsed = parseBookInput(text);
|
|
225
|
+
if (parsed !== null) {
|
|
226
|
+
if (hasBookIntent(text)) return true;
|
|
227
|
+
// 斜杠命令 followup 的原文:平凡的世界 | 路遥 / 《书》作者
|
|
228
|
+
if (/[||]/u.test(text) || /作者|by/iu.test(text) || /《.+》/u.test(text)) return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!hasBookIntent(text)) return false;
|
|
232
|
+
return /作者|by|《.|》/u.test(text) || /[||]/u.test(text);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** 不依赖 @deepseek-ai/dsh-llm(本地 file: 插件 realpath 解析不到宿主包)。 */
|
|
236
|
+
function makeUserMessage(text) {
|
|
237
|
+
return Object.freeze({
|
|
238
|
+
id: randomUUID(),
|
|
239
|
+
role: 'user',
|
|
240
|
+
content: Object.freeze([{ type: 'text', text }]),
|
|
241
|
+
source: Object.freeze({ kind: 'user' }),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function makePluginMessage(text) {
|
|
246
|
+
return Object.freeze({
|
|
247
|
+
id: randomUUID(),
|
|
248
|
+
role: 'user',
|
|
249
|
+
content: Object.freeze([{ type: 'text', text }]),
|
|
250
|
+
source: Object.freeze({
|
|
251
|
+
kind: `plugin:${TOOL_NAME}`,
|
|
252
|
+
plugin: TOOL_NAME,
|
|
253
|
+
}),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function isPluginSource(source) {
|
|
258
|
+
if (source !== null && source !== undefined) {
|
|
259
|
+
const kind = source.kind;
|
|
260
|
+
if (kind === 'plugin' || (typeof kind === 'string' && kind.startsWith('plugin:'))) return true;
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function enqueueAgentMessage(agent, message) {
|
|
266
|
+
if (agent !== null && agent !== undefined && typeof agent.followup === 'function') {
|
|
267
|
+
agent.followup(message);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
if (agent !== null && agent !== undefined && typeof agent.steer === 'function') {
|
|
271
|
+
agent.steer(message);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// 提示词:只产出成品,不产出过程
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
const SYSTEM_PROMPT = [
|
|
282
|
+
'你是一位有洞察力的书籍解读者。只交付成品,不展示分析过程。',
|
|
283
|
+
'',
|
|
284
|
+
'输出有且仅有两部分:',
|
|
285
|
+
'1. 书籍的故事情节:把整本书讲清楚(小说讲清人物与情节推进;非虚构讲清主线内容与关键论点)。不要一句话短评,也不要灌水啰嗦。',
|
|
286
|
+
'2. 一段有深度的思考:全文最后只写一整段,不列条目、不加小标题。',
|
|
287
|
+
'',
|
|
288
|
+
'铁律:',
|
|
289
|
+
'1. 只输出成品。不要写分析步骤、框架说明、知识边界、置信度、自我审校或元评论。',
|
|
290
|
+
'2. 不编造。不确定的事实、引文、页码、奖项、时间线不要写。',
|
|
291
|
+
'3. 你的推论须标明语气(如「由此可以推出」),勿冒充作者原话。',
|
|
292
|
+
'4. 禁止鸡汤短评、书腰简介、朋友圈文案;也禁止为凑字数而重复、注水。',
|
|
293
|
+
'5. 用中文 Markdown。若无法可靠解读该书,只写「我无法可靠地解读这本书」。',
|
|
294
|
+
].join('\n');
|
|
295
|
+
|
|
296
|
+
function buildAnalysisPrompt(title, author) {
|
|
297
|
+
return `书名:《${title}》
|
|
298
|
+
作者:${author}
|
|
299
|
+
|
|
300
|
+
请直接输出成品,只写下面两部分,标题必须原样使用。不要写分析过程或元信息。
|
|
301
|
+
|
|
302
|
+
## 故事情节
|
|
303
|
+
把整本书讲清楚即可:
|
|
304
|
+
- 小说:主要人物、起因—发展—转折—结局,关键情节线;
|
|
305
|
+
- 非虚构:主线问题、各部分分别讲什么、关键论点/案例如何推进到结论。
|
|
306
|
+
要求:读完能掌握全书在讲什么;禁止一句话短评;禁止灌水、重复、百科词条或目录清单。篇幅以讲清为准,通常大约 800–1500 字,够用就停。
|
|
307
|
+
|
|
308
|
+
## 一段有深度的思考
|
|
309
|
+
紧接着写**一整段**(约 200–350 字,不要分条、不要小标题):从全书提炼一个真正有深度的思考——它是什么、为何成立(扣住书中内容)、对读者意味着什么。除此之外不要再写别的部分。`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// LLM 调用(工具路径使用;斜杠无 agent 时降级)
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
function resolveRoute(ctx, config) {
|
|
317
|
+
if (config.provider !== '' && config.model !== '') {
|
|
318
|
+
return { provider: config.provider, model: config.model };
|
|
319
|
+
}
|
|
320
|
+
const defaultModel = ctx.get('agentDefaultModel');
|
|
321
|
+
const selection =
|
|
322
|
+
defaultModel !== undefined && typeof defaultModel.currentSelection === 'function'
|
|
323
|
+
? defaultModel.currentSelection()
|
|
324
|
+
: undefined;
|
|
325
|
+
if (selection !== undefined && selection.provider && selection.model) {
|
|
326
|
+
return { provider: selection.provider, model: selection.model };
|
|
327
|
+
}
|
|
328
|
+
throw new Error('没有可用的模型:请在插件配置里设置 provider 和 model,或在设置里选择默认模型。');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function generate(ctx, config, options) {
|
|
332
|
+
const route = resolveRoute(ctx, config);
|
|
333
|
+
const stream = ctx.llm.stream({
|
|
334
|
+
provider: route.provider,
|
|
335
|
+
model: route.model,
|
|
336
|
+
system: options.system,
|
|
337
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: options.user }] }],
|
|
338
|
+
temperature: options.temperature,
|
|
339
|
+
maxTokens: options.maxTokens,
|
|
340
|
+
signal: options.signal,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
let text = '';
|
|
344
|
+
let sawDelta = false;
|
|
345
|
+
const blockTexts = [];
|
|
346
|
+
let finish;
|
|
347
|
+
|
|
348
|
+
for await (const chunk of stream) {
|
|
349
|
+
if (chunk.type === 'text-delta') {
|
|
350
|
+
sawDelta = true;
|
|
351
|
+
text += chunk.text;
|
|
352
|
+
} else if (chunk.type === 'block-end') {
|
|
353
|
+
if (chunk.block !== null && typeof chunk.block === 'object' && chunk.block.type === 'text') {
|
|
354
|
+
blockTexts.push(chunk.block.text);
|
|
355
|
+
}
|
|
356
|
+
} else if (chunk.type === 'finish') {
|
|
357
|
+
finish = chunk.reason;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!sawDelta && blockTexts.length > 0) text = blockTexts.join('');
|
|
362
|
+
|
|
363
|
+
if (finish !== undefined && finish.kind === 'error') {
|
|
364
|
+
const message =
|
|
365
|
+
finish.failure !== undefined && finish.failure !== null && finish.failure.message
|
|
366
|
+
? finish.failure.message
|
|
367
|
+
: '未知错误';
|
|
368
|
+
throw new Error('模型调用失败:' + message);
|
|
369
|
+
}
|
|
370
|
+
if (finish !== undefined && finish.kind === 'aborted') {
|
|
371
|
+
throw new Error('模型调用被取消');
|
|
372
|
+
}
|
|
373
|
+
return { text: text.trim(), finish };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function deconstruct(ctx, config, rawTitle, rawAuthor, options = {}) {
|
|
377
|
+
const title = cleanField(rawTitle, 200);
|
|
378
|
+
const author = cleanField(rawAuthor, 200);
|
|
379
|
+
if (title === '') throw new Error('书名不能为空');
|
|
380
|
+
if (author === '') throw new Error('作者不能为空');
|
|
381
|
+
|
|
382
|
+
const result = await generate(ctx, config, {
|
|
383
|
+
system: SYSTEM_PROMPT,
|
|
384
|
+
user: buildAnalysisPrompt(title, author),
|
|
385
|
+
temperature: config.temperature,
|
|
386
|
+
maxTokens: config.maxTokens,
|
|
387
|
+
signal: options.signal,
|
|
388
|
+
});
|
|
389
|
+
if (result.text === '') throw new Error('模型没有返回内容,请稍后重试。');
|
|
390
|
+
|
|
391
|
+
let output = result.text;
|
|
392
|
+
if (result.finish !== undefined && result.finish.kind === 'max-tokens') {
|
|
393
|
+
output += '\n\n> (输出达到长度上限,可能有截断)';
|
|
394
|
+
}
|
|
395
|
+
return output;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// 插件入口
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
export function apply(ctx, rawConfig) {
|
|
403
|
+
const config = normalizeConfig(rawConfig);
|
|
404
|
+
const disposers = [];
|
|
405
|
+
|
|
406
|
+
// 保留用户原话,只追加「必须调工具」提示;正文只由工具产出。
|
|
407
|
+
disposers.push(
|
|
408
|
+
ctx.on('agent/pre-step', async (_payload, next) => {
|
|
409
|
+
const decision = await next();
|
|
410
|
+
if (decision.kind !== 'enter') return decision;
|
|
411
|
+
|
|
412
|
+
let changed = false;
|
|
413
|
+
const rewritten = [];
|
|
414
|
+
for (const message of decision.messages) {
|
|
415
|
+
if (isPluginSource(message.source)) {
|
|
416
|
+
rewritten.push(message);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const raw = messageText(message);
|
|
420
|
+
if (!shouldHandleFreeform(raw)) {
|
|
421
|
+
rewritten.push(message);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
changed = true;
|
|
426
|
+
rewritten.push(message);
|
|
427
|
+
rewritten.push(
|
|
428
|
+
makePluginMessage(buildToolSteerPrompt(parseBookInput(raw), raw, newRequestId())),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!changed) return decision;
|
|
433
|
+
return { ...decision, messages: rewritten };
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
disposers.push(
|
|
438
|
+
ctx.commands.register({
|
|
439
|
+
name: TOOL_NAME,
|
|
440
|
+
description: '书籍拆解:深度解读一本书。用法:/dsh-plugin-book-deconstruct 书名 | 作者',
|
|
441
|
+
input: { hint: '书名 | 作者' },
|
|
442
|
+
handler: async (invocation) => {
|
|
443
|
+
const raw = String(invocation.rawInput ?? '').trim();
|
|
444
|
+
if (raw === '') return { kind: 'error', text: USAGE };
|
|
445
|
+
|
|
446
|
+
const parsed = parseBookInput(raw);
|
|
447
|
+
if (parsed === null) return { kind: 'error', text: USAGE };
|
|
448
|
+
|
|
449
|
+
// 会话里只显示用户输入的书名/作者原文,不带 /命令 和请求号。
|
|
450
|
+
// 强制调工具由 pre-step 追加(唯一 requestId 只在插件提示里)。
|
|
451
|
+
const agent = invocation.agent;
|
|
452
|
+
|
|
453
|
+
// 立刻回执,避免桌面端「点了发送没反应」。
|
|
454
|
+
if (enqueueAgentMessage(agent, makeUserMessage(raw))) {
|
|
455
|
+
return { kind: 'success', text: `已收到:${raw}` };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
const text = await deconstruct(ctx, config, parsed.title, parsed.author, {
|
|
460
|
+
signal: invocation.signal,
|
|
461
|
+
});
|
|
462
|
+
return { kind: 'success', text: `问题:${raw}\n\n${text}` };
|
|
463
|
+
} catch (error) {
|
|
464
|
+
const message = error !== null && error !== undefined && error.message ? error.message : String(error);
|
|
465
|
+
return { kind: 'error', text: '解读失败:' + message };
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
disposers.push(
|
|
472
|
+
ctx.tools.register({
|
|
473
|
+
name: TOOL_NAME,
|
|
474
|
+
description:
|
|
475
|
+
'深度解读一本书:先写「故事情节」(讲清整本书,勿一句话短评、勿灌水),再写「一段有深度的思考」。' +
|
|
476
|
+
'用户要求解读/拆解某书时必须调用;即使用户重复同一本、或本会话已解读过其他书,也必须再次调用并重新生成;' +
|
|
477
|
+
'禁止复用旧结果,禁止自己写短评代替。',
|
|
478
|
+
parameters: {
|
|
479
|
+
type: 'object',
|
|
480
|
+
properties: {
|
|
481
|
+
title: { type: 'string', description: '书名,例如《思考,快与慢》或“思考,快与慢”' },
|
|
482
|
+
author: { type: 'string', description: '作者,例如“丹尼尔·卡尼曼”' },
|
|
483
|
+
},
|
|
484
|
+
required: ['title', 'author'],
|
|
485
|
+
additionalProperties: false,
|
|
486
|
+
},
|
|
487
|
+
output: {
|
|
488
|
+
schema: { type: 'string' },
|
|
489
|
+
render(_args, value) {
|
|
490
|
+
return [{ type: 'text', text: String(value) }];
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
async execute(args, exec) {
|
|
494
|
+
const payload = args !== null && typeof args === 'object' ? args : {};
|
|
495
|
+
const title = cleanField(payload.title, 200);
|
|
496
|
+
const author = cleanField(payload.author, 200);
|
|
497
|
+
if (title === '' || author === '') throw new Error('title 和 author 必填');
|
|
498
|
+
return deconstruct(ctx, config, title, author, { signal: exec.signal });
|
|
499
|
+
},
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
return () => {
|
|
504
|
+
for (const dispose of disposers.reverse()) {
|
|
505
|
+
try {
|
|
506
|
+
dispose();
|
|
507
|
+
} catch {
|
|
508
|
+
/* ignore disposer errors */
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-book-deconstruct",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "dsh-plugin — 书籍拆解:/dsh-plugin-book-deconstruct slash + dsh-plugin-book-deconstruct tool for full book walkthroughs.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"dsh-plugin",
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"book",
|
|
10
|
+
"reading"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Kang",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./index.js",
|
|
18
|
+
"./locale/*.json": "./locale/*.json",
|
|
19
|
+
"./icon": "./icon.svg",
|
|
20
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"index.js",
|
|
24
|
+
"cordis.patch.yml",
|
|
25
|
+
"locale/en.json",
|
|
26
|
+
"locale/zh.json",
|
|
27
|
+
"icon.svg",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"第一种使用方式.png",
|
|
31
|
+
"第二种使用方式.png"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"dsh": ">=0.2.0"
|
|
35
|
+
},
|
|
36
|
+
"dsh": {
|
|
37
|
+
"bundle": {
|
|
38
|
+
"patch": "./cordis.patch.yml"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/zmz123612/dsh-plugin-book-deconstruct.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/zmz123612/dsh-plugin-book-deconstruct/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/zmz123612/dsh-plugin-book-deconstruct#readme"
|
|
49
|
+
}
|