dsh-prompt-refine 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 願
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 ADDED
@@ -0,0 +1,93 @@
1
+ # dsh-prompt-refine
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
+
5
+ DSH 提示词优化插件 —— 在发送前点一下,让模型结合上下文告诉你这条 prompt 缺什么。
6
+
7
+ ![插件预览:点 ✨ 后弹窗列出 3 条建议,勾选后实时预览合并结果](docs/overview.png)
8
+
9
+ ## 它做什么
10
+
11
+ 在输入框发送按钮**左侧**加一个 ✨ 按钮。点击后:
12
+
13
+ 1. 读当前草稿 + 最近几条对话历史
14
+ 2. 调你已配置好的模型(自动读 `~/.dsh/settings.yaml` 的 `agent-default-model`)
15
+ 3. 弹窗列出 **3 条「问题 + 补丁」** 形式的建议
16
+ 4. 勾选想要的 → 实时预览合并后的 prompt
17
+ 5. 点「采纳 N 条并填入」→ 新 prompt 写回输入框
18
+
19
+ **不抢你的发送权**:填好后你自己按发送。Esc / 点遮罩 / 「跳过」都能随时退出。
20
+
21
+ ## 为什么是「补丁」而不是「重写」
22
+
23
+ 建议保留你原话,只补缺口:
24
+
25
+ ```
26
+ 原文 写个排序
27
+
28
+ scope 未说明排序对象 → 对一组整数数组进行升序排序。
29
+ format 未指定语言签名 → 请用 Python 实现,函数签名 sort_list(nums)。
30
+ constraint 缺边界约束 → 要求 O(n log n),处理空数组与重复元素。
31
+ ```
32
+
33
+ 三条可以同时采纳,不用三选一。
34
+
35
+ ## 安装
36
+
37
+ ### 方式一:npm 安装(推荐)
38
+
39
+ ```bash
40
+ cd ~/.dsh/profiles/web # 换成你要装进去的 profile
41
+ pnpm add dsh-prompt-refine
42
+ ```
43
+
44
+ 包内自带 `dsh.bundle.patch` 声明,安装后把 `dsh-prompt-refine` 加进该 profile `package.json` 的 `dsh.profile.bundles` 数组即可激活,然后重启 DSH。
45
+
46
+ ### 方式二:源码安装(开发用)
47
+
48
+ ```bash
49
+ pnpm install
50
+ pnpm build
51
+
52
+ # 装进 web profile
53
+ node scripts/install.mjs --profile web
54
+ rm -rf ~/.dsh/profiles/web/node_modules/dsh-prompt-refine
55
+ (cd ~/.dsh/profiles/web && pnpm install --force)
56
+ ```
57
+
58
+ ## 运行(开发模式)
59
+
60
+ ```bash
61
+ # 在 deepseek-harness 仓库根目录
62
+ pnpm dsh web --patch ./dsh-prompt-refine/cordis.yml --no-open --port 0
63
+ ```
64
+
65
+ 或者用附带的脚本(自动杀旧实例 + 重启 + 打印新 URL):
66
+
67
+ ```bash
68
+ bash scripts/dev-restart.sh
69
+ ```
70
+
71
+ ## 模型配置
72
+
73
+ **v0.1 无需配置** —— 自动读用户 settings.yaml 里的 `agent-default-model`,
74
+ 跟随你平时用的模型。改模型只改那里。
75
+
76
+ v0.2 计划:插件自己的设置页,可单独指定"优化用的模型"(比如用便宜的快模型)。
77
+
78
+ ## 文件结构
79
+
80
+ ```
81
+ src/
82
+ dsh-prompt-refine.ts host 入口
83
+ host.ts HTTP 端点 + ctx.llm 调用 + 降级
84
+ shared/types.ts 请求/响应形状
85
+ shared/prompt.ts system prompt + JSON 解析
86
+ client.ts ✨ 按钮 + Portal 弹窗
87
+ scripts/
88
+ install.mjs 装进 profile
89
+ ```
90
+
91
+ ## License
92
+
93
+ MIT
@@ -0,0 +1,8 @@
1
+ # dsh-prompt-refine 的 bundle patch —— 安装到 profile 后自动进配置层
2
+ # name 用包名(不是 file:// 源码路径):loader 从 profile 的 node_modules
3
+ # 解析到编译好的 lib/index.mjs。指向 TS 源码只在仓库内 dev 模式(pnpm dsh,
4
+ # 带 tsx)下能跑;全局 npm 安装的 dsh 是纯 node ESM,会报
5
+ # ERR_MODULE_NOT_FOUND: Cannot find module '...\src\host'。
6
+ - insert:
7
+ - id: dsh-prompt-refine
8
+ name: 'dsh-prompt-refine'
@@ -0,0 +1,518 @@
1
+ (function() {
2
+ //#region src/client.ts
3
+ function makeFactory() {
4
+ return (require) => {
5
+ const React = require("react");
6
+ const h = React.createElement;
7
+ const { createPortal } = require("react-dom");
8
+ const name = "dsh-prompt-refine";
9
+ const inject = ["slots"];
10
+ let portalHost = null;
11
+ function getPortalHost() {
12
+ if (portalHost === null || !portalHost.isConnected) {
13
+ portalHost = document.createElement("div");
14
+ portalHost.id = "dsh-prompt-refine-portal";
15
+ document.body.appendChild(portalHost);
16
+ }
17
+ return portalHost;
18
+ }
19
+ const S = {
20
+ btn: {
21
+ padding: 0,
22
+ width: "32px",
23
+ height: "32px",
24
+ borderRadius: "8px",
25
+ border: "1px solid var(--dsw-alias-border-l2)",
26
+ background: "transparent",
27
+ color: "var(--dsw-alias-label-secondary)",
28
+ cursor: "pointer",
29
+ display: "inline-flex",
30
+ alignItems: "center",
31
+ justifyContent: "center",
32
+ fontSize: "15px",
33
+ lineHeight: 1
34
+ },
35
+ overlay: {
36
+ position: "fixed",
37
+ inset: 0,
38
+ background: "rgba(0,0,0,0.55)",
39
+ display: "flex",
40
+ alignItems: "center",
41
+ justifyContent: "center",
42
+ zIndex: 9999,
43
+ padding: "20px"
44
+ },
45
+ modal: {
46
+ background: "var(--dsw-alias-bg-layer-1)",
47
+ borderRadius: "14px",
48
+ width: "100%",
49
+ maxWidth: "560px",
50
+ maxHeight: "86vh",
51
+ overflowY: "auto",
52
+ boxShadow: "0 20px 60px rgba(0,0,0,0.28)",
53
+ border: "1px solid var(--dsw-alias-border-l2)",
54
+ color: "var(--dsw-alias-label-primary)"
55
+ },
56
+ header: {
57
+ padding: "18px 22px 14px",
58
+ borderBottom: "1px solid var(--dsw-alias-border-l2)"
59
+ },
60
+ title: {
61
+ fontSize: "16px",
62
+ fontWeight: 600,
63
+ margin: "0 0 4px"
64
+ },
65
+ subtitle: {
66
+ fontSize: "13px",
67
+ color: "var(--dsw-alias-label-secondary)",
68
+ margin: 0
69
+ },
70
+ original: {
71
+ margin: "14px 22px 0",
72
+ padding: "10px 12px",
73
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.04))",
74
+ borderRadius: "8px",
75
+ fontSize: "13px",
76
+ color: "var(--dsw-alias-label-secondary)",
77
+ borderLeft: "3px solid var(--dsw-alias-label-tertiary)"
78
+ },
79
+ originalLabel: {
80
+ fontSize: "10px",
81
+ color: "var(--dsw-alias-label-tertiary)",
82
+ textTransform: "uppercase",
83
+ letterSpacing: "0.5px",
84
+ marginBottom: "4px"
85
+ },
86
+ list: { padding: "14px 22px" },
87
+ row: {
88
+ display: "flex",
89
+ gap: "12px",
90
+ padding: "12px",
91
+ border: "1px solid var(--dsw-alias-border-l2)",
92
+ borderRadius: "10px",
93
+ marginBottom: "8px",
94
+ cursor: "pointer"
95
+ },
96
+ rowOn: {
97
+ borderColor: "var(--dsw-alias-state-business-primary)",
98
+ background: "var(--dsw-alias-interactive-bg-active, rgba(37,99,235,0.08))"
99
+ },
100
+ rowBody: {
101
+ flex: 1,
102
+ minWidth: 0
103
+ },
104
+ tag: {
105
+ display: "inline-block",
106
+ fontSize: "11px",
107
+ padding: "2px 7px",
108
+ borderRadius: "4px",
109
+ fontWeight: 500,
110
+ marginBottom: "6px"
111
+ },
112
+ tagScope: {
113
+ background: "var(--dsw-alias-state-error-secondary)",
114
+ color: "var(--dsw-alias-state-error-primary)"
115
+ },
116
+ tagFormat: {
117
+ background: "rgba(217,119,6,0.14)",
118
+ color: "#d97706"
119
+ },
120
+ tagConstraint: {
121
+ background: "rgba(37,99,235,0.14)",
122
+ color: "var(--dsw-alias-state-business-primary)"
123
+ },
124
+ tagClarity: {
125
+ background: "rgba(5,150,105,0.16)",
126
+ color: "var(--dsw-alias-state-success-primary)"
127
+ },
128
+ tagOther: {
129
+ background: "rgba(128,128,128,0.16)",
130
+ color: "var(--dsw-alias-label-secondary)"
131
+ },
132
+ issue: {
133
+ fontSize: "13.5px",
134
+ margin: "0 0 8px",
135
+ lineHeight: 1.45
136
+ },
137
+ patch: {
138
+ fontSize: "12.5px",
139
+ color: "var(--dsw-alias-label-secondary)",
140
+ background: "rgba(128,128,128,0.08)",
141
+ padding: "7px 10px",
142
+ borderRadius: "6px",
143
+ margin: 0,
144
+ fontFamily: "ui-monospace, \"SF Mono\", Consolas, monospace",
145
+ border: "1px dashed var(--dsw-alias-border-l2)"
146
+ },
147
+ preview: {
148
+ margin: "0 22px 14px",
149
+ padding: "10px 12px",
150
+ background: "rgba(128,128,128,0.08)",
151
+ borderRadius: "8px",
152
+ fontSize: "13px"
153
+ },
154
+ previewLabel: {
155
+ fontSize: "10px",
156
+ color: "var(--dsw-alias-label-tertiary)",
157
+ textTransform: "uppercase",
158
+ letterSpacing: "0.5px",
159
+ marginBottom: "6px",
160
+ display: "flex",
161
+ justifyContent: "space-between"
162
+ },
163
+ previewCount: {
164
+ color: "var(--dsw-alias-state-business-primary)",
165
+ fontWeight: 600
166
+ },
167
+ previewText: { lineHeight: 1.6 },
168
+ added: {
169
+ background: "rgba(5,150,105,0.16)",
170
+ color: "var(--dsw-alias-state-success-primary)",
171
+ padding: "1px 5px",
172
+ borderRadius: "3px",
173
+ margin: "0 2px",
174
+ fontWeight: 500
175
+ },
176
+ footer: {
177
+ padding: "14px 22px",
178
+ borderTop: "1px solid var(--dsw-alias-border-l2)",
179
+ display: "flex",
180
+ justifyContent: "space-between",
181
+ alignItems: "center",
182
+ gap: "12px"
183
+ },
184
+ hint: {
185
+ fontSize: "12px",
186
+ color: "var(--dsw-alias-label-tertiary)"
187
+ },
188
+ btnSec: {
189
+ padding: "8px 14px",
190
+ borderRadius: "8px",
191
+ border: "1px solid var(--dsw-alias-border-l2)",
192
+ background: "transparent",
193
+ color: "var(--dsw-alias-label-primary)",
194
+ cursor: "pointer",
195
+ fontSize: "14px"
196
+ },
197
+ btnPri: {
198
+ padding: "8px 14px",
199
+ borderRadius: "8px",
200
+ border: "none",
201
+ background: "var(--dsw-alias-state-business-primary)",
202
+ color: "var(--dsw-alias-button-info-fill, #fff)",
203
+ cursor: "pointer",
204
+ fontSize: "14px",
205
+ fontWeight: 600
206
+ },
207
+ skel: {
208
+ height: "58px",
209
+ background: "rgba(128,128,128,0.14)",
210
+ borderRadius: "10px",
211
+ marginBottom: "8px"
212
+ },
213
+ tip: {
214
+ textAlign: "center",
215
+ fontSize: "13px",
216
+ color: "var(--dsw-alias-label-secondary)",
217
+ padding: "8px 0"
218
+ },
219
+ err: {
220
+ padding: "16px 22px",
221
+ fontSize: "13px",
222
+ color: "var(--dsw-alias-state-error-primary)"
223
+ },
224
+ warn: {
225
+ padding: "0 22px 10px",
226
+ fontSize: "12px",
227
+ color: "var(--dsw-alias-state-warn-primary, #d97706)"
228
+ }
229
+ };
230
+ function tagStyle(tag) {
231
+ switch (tag) {
232
+ case "scope": return S.tagScope;
233
+ case "format": return S.tagFormat;
234
+ case "constraint": return S.tagConstraint;
235
+ case "clarity": return S.tagClarity;
236
+ default: return S.tagOther;
237
+ }
238
+ }
239
+ function PromptRefineControl(props) {
240
+ const useInput = props.useInput;
241
+ const useChat = props.useChat;
242
+ const inputActions = props.inputActions;
243
+ const draftNow = (useInput ? useInput((s) => s.draft) : "") ?? "";
244
+ const nodes = (useChat ? useChat((s) => s.legacy.nodes) : []) ?? [];
245
+ const [host] = React.useState(() => getPortalHost());
246
+ const [open, setOpen] = React.useState(false);
247
+ const [phase, setPhase] = React.useState("loading");
248
+ const [reqDraft, setReqDraft] = React.useState("");
249
+ const [items, setItems] = React.useState([]);
250
+ const [checked, setChecked] = React.useState([]);
251
+ const [errMsg, setErrMsg] = React.useState("");
252
+ const [usedModel, setUsedModel] = React.useState("");
253
+ const collectHistory = () => {
254
+ const out = [];
255
+ for (let i = nodes.length - 1; i >= 0 && out.length < 6; i--) {
256
+ const n = nodes[i];
257
+ if (!n) continue;
258
+ if (n.kind === "user") {
259
+ const text = (n.content ?? []).filter((b) => b && b.type === "text").map((b) => b.text ?? "").join("").trim();
260
+ if (text) out.unshift({
261
+ role: "user",
262
+ content: text.slice(0, 500)
263
+ });
264
+ } else if (n.kind === "assistant") {
265
+ const text = (n.blocks ?? []).filter((b) => b && b.kind === "text").map((b) => b.text ?? "").join("").trim();
266
+ if (text) out.unshift({
267
+ role: "assistant",
268
+ content: text.slice(0, 500)
269
+ });
270
+ }
271
+ }
272
+ return out;
273
+ };
274
+ const request = () => {
275
+ const d = draftNow.trim();
276
+ if (!d) {
277
+ setReqDraft("");
278
+ setPhase("empty");
279
+ setOpen(true);
280
+ return;
281
+ }
282
+ setReqDraft(d);
283
+ setItems([]);
284
+ setChecked([]);
285
+ setErrMsg("");
286
+ setUsedModel("");
287
+ setPhase("loading");
288
+ setOpen(true);
289
+ const history = collectHistory();
290
+ fetch("/dsh-prompt-refine/suggest", {
291
+ method: "POST",
292
+ headers: { "Content-Type": "application/json" },
293
+ body: JSON.stringify({
294
+ draft: d,
295
+ history
296
+ })
297
+ }).then((r) => r.json()).then((data) => {
298
+ const list = Array.isArray(data?.suggestions) ? data.suggestions.slice(0, 3) : [];
299
+ if (data?.ok && list.length > 0) {
300
+ setItems(list);
301
+ setChecked(list.map((_, i) => i < 2));
302
+ setUsedModel(data.usedModel ?? "");
303
+ setErrMsg(data.error ?? "");
304
+ setPhase("result");
305
+ } else {
306
+ setErrMsg(data?.error ?? "未知错误");
307
+ setPhase("error");
308
+ }
309
+ }).catch((e) => {
310
+ setErrMsg("网络错误: " + (e instanceof Error ? e.message : String(e)));
311
+ setPhase("error");
312
+ });
313
+ };
314
+ React.useEffect(() => {
315
+ if (!open) return;
316
+ const onKey = (e) => {
317
+ if (e.key === "Escape") setOpen(false);
318
+ };
319
+ window.addEventListener("keydown", onKey);
320
+ return () => {
321
+ window.removeEventListener("keydown", onKey);
322
+ };
323
+ }, [open]);
324
+ const close = () => setOpen(false);
325
+ const apply = () => {
326
+ const patches = [];
327
+ for (let i = 0; i < items.length; i++) if (checked[i] && items[i]?.patch) patches.push(items[i].patch);
328
+ const next = reqDraft + patches.join("");
329
+ if (inputActions?.setDraft) inputActions.setDraft(next);
330
+ setOpen(false);
331
+ };
332
+ const button = h("button", {
333
+ type: "button",
334
+ title: "优化提示词",
335
+ "aria-label": "优化提示词",
336
+ style: S.btn,
337
+ onClick: (e) => {
338
+ e.stopPropagation();
339
+ e.preventDefault();
340
+ request();
341
+ },
342
+ onMouseDown: (e) => e.preventDefault()
343
+ }, "✨");
344
+ if (!open || !host) return button;
345
+ const selected = [];
346
+ for (let i = 0; i < items.length; i++) if (checked[i] && items[i]?.patch) selected.push(items[i].patch);
347
+ const selectedCount = checked.filter(Boolean).length;
348
+ let body;
349
+ if (phase === "empty") body = h("div", {
350
+ style: S.err,
351
+ children: "请先在输入框写点内容,再点 ✨ 优化。"
352
+ });
353
+ else if (phase === "loading") body = h("div", {
354
+ style: { padding: "18px 22px" },
355
+ children: [
356
+ h("div", { style: S.skel }),
357
+ h("div", { style: S.skel }),
358
+ h("div", { style: S.skel }),
359
+ h("div", {
360
+ style: S.tip,
361
+ children: "正在结合上下文分析…"
362
+ })
363
+ ]
364
+ });
365
+ else if (phase === "error") body = h("div", {
366
+ style: S.err,
367
+ children: "⚠ " + (errMsg || "出错了")
368
+ });
369
+ else {
370
+ const rows = items.map((s, i) => h("label", {
371
+ key: i,
372
+ style: {
373
+ ...S.row,
374
+ ...checked[i] ? S.rowOn : {}
375
+ },
376
+ children: [h("input", {
377
+ type: "checkbox",
378
+ checked: !!checked[i],
379
+ onChange: (e) => {
380
+ setChecked((prev) => {
381
+ const n = [...prev];
382
+ n[i] = e.target.checked;
383
+ return n;
384
+ });
385
+ },
386
+ style: {
387
+ marginTop: "2px",
388
+ width: "16px",
389
+ height: "16px",
390
+ accentColor: "var(--dsw-alias-state-business-primary)",
391
+ cursor: "pointer"
392
+ }
393
+ }), h("div", {
394
+ style: S.rowBody,
395
+ children: [
396
+ h("span", {
397
+ style: tagStyle(s.tag),
398
+ children: s.tag
399
+ }),
400
+ h("p", {
401
+ style: S.issue,
402
+ children: s.issue
403
+ }),
404
+ s.patch ? h("p", {
405
+ style: S.patch,
406
+ children: s.patch
407
+ }) : null
408
+ ]
409
+ })]
410
+ }));
411
+ const previewChildren = [h("span", {
412
+ key: "draft",
413
+ children: reqDraft
414
+ })];
415
+ selected.forEach((p, i) => {
416
+ previewChildren.push(h("span", {
417
+ key: "p" + i,
418
+ style: S.added,
419
+ children: p
420
+ }));
421
+ });
422
+ body = h(React.Fragment, null, [
423
+ h("div", {
424
+ style: S.original,
425
+ children: [h("div", {
426
+ style: S.originalLabel,
427
+ children: "原文"
428
+ }), h("div", { children: reqDraft })]
429
+ }),
430
+ h("div", {
431
+ style: S.list,
432
+ children: rows
433
+ }),
434
+ h("div", {
435
+ style: S.preview,
436
+ children: [h("div", {
437
+ style: S.previewLabel,
438
+ children: [h("span", { children: "采纳后预览" }), h("span", {
439
+ style: S.previewCount,
440
+ children: `已选 ${selectedCount} / ${items.length}`
441
+ })]
442
+ }), h("div", {
443
+ style: S.previewText,
444
+ children: previewChildren
445
+ })]
446
+ }),
447
+ errMsg ? h("div", {
448
+ style: S.warn,
449
+ children: "提示:" + errMsg
450
+ }) : null,
451
+ h("div", {
452
+ style: S.footer,
453
+ children: [h("span", {
454
+ style: S.hint,
455
+ children: (usedModel ? usedModel + " · " : "") + "Esc 关闭"
456
+ }), h("div", {
457
+ style: {
458
+ display: "flex",
459
+ gap: "8px"
460
+ },
461
+ children: [h("button", {
462
+ type: "button",
463
+ style: S.btnSec,
464
+ onClick: close,
465
+ children: "跳过"
466
+ }), h("button", {
467
+ type: "button",
468
+ style: S.btnPri,
469
+ onClick: apply,
470
+ children: selectedCount > 0 ? `采纳 ${selectedCount} 条并填入` : "填入原文"
471
+ })]
472
+ })]
473
+ })
474
+ ]);
475
+ }
476
+ const overlay = h("div", {
477
+ style: S.overlay,
478
+ onClick: (e) => {
479
+ if (e.target === e.currentTarget) close();
480
+ },
481
+ children: h("div", {
482
+ style: S.modal,
483
+ onClick: (e) => e.stopPropagation(),
484
+ children: [h("div", {
485
+ style: S.header,
486
+ children: [h("div", {
487
+ style: S.title,
488
+ children: "✨ 提示词优化建议"
489
+ }), h("p", {
490
+ style: S.subtitle,
491
+ children: phase === "result" ? `发现 ${items.length} 处可以更精确` : "正在结合上下文生成建议"
492
+ })]
493
+ }), body]
494
+ })
495
+ });
496
+ return h(React.Fragment, null, [button, createPortal(overlay, host)]);
497
+ }
498
+ function apply(ctx) {
499
+ const c = ctx;
500
+ c.slots.inject("conversation.input.right", () => c.slots.register({
501
+ name: "conversation.input.right",
502
+ id: "dsh-prompt-refine-button",
503
+ order: 10
504
+ }, PromptRefineControl));
505
+ }
506
+ return {
507
+ name,
508
+ inject,
509
+ apply
510
+ };
511
+ };
512
+ }
513
+ window.__ModuleLoader__.load({
514
+ id: "dsh-prompt-refine",
515
+ factory: makeFactory()
516
+ });
517
+ //#endregion
518
+ })();