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/lib/index.mjs ADDED
@@ -0,0 +1,272 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { createAssistantMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
5
+ //#region src/shared/prompt.ts
6
+ /** 优化器 system prompt —— 关键是要 LLM 返回可被 JSON.parse 的结构 */
7
+ const SYSTEM_PROMPT = `你是一个提示词优化助手。用户给你:
8
+ 1. 一段最近的对话历史(可能为空)
9
+ 2. 用户即将发送的最新草稿
10
+
11
+ 你的任务:分析草稿的不足,给出恰好 3 条改进建议。每条建议 = 一个「问题标签」+「问题描述」+「具体补丁」。
12
+
13
+ 要求:
14
+ - 只输出合法 JSON,不要任何额外文字、不要 markdown 代码块标记
15
+ - patch 必须是可以直接追加到原草稿末尾或合适位置的一句话,长度 5-50 字
16
+ - 优先指出:范围不清、缺少输出格式、缺少约束条件、模糊指代
17
+ - 不要重写整个草稿,只补漏
18
+ - 如果草稿已经足够好,issue 里如实写「无需修改」,patch 留空
19
+
20
+ 输出 schema(严格遵守):
21
+ {"suggestions": [{"tag": "scope|format|constraint|clarity|other", "issue": "一句话问题", "patch": "一句话补丁"}, ...共 3 条]}
22
+ `;
23
+ /** 用户消息模板:把 history + draft 装进 user message */
24
+ function buildUserMessage(history, draft) {
25
+ const lines = [];
26
+ if (history.length > 0) {
27
+ lines.push("【最近对话历史】");
28
+ for (const m of history) lines.push(`${m.role === "user" ? "用户" : "助手"}: ${m.content}`);
29
+ lines.push("");
30
+ }
31
+ lines.push("【用户当前草稿】");
32
+ lines.push(draft);
33
+ lines.push("");
34
+ lines.push("请按 system 要求输出 JSON。");
35
+ return lines.join("\n");
36
+ }
37
+ /**
38
+ * 从 LLM 输出里提取 JSON。
39
+ * 策略:
40
+ * 1. 去掉可能的 markdown ```json``` 包裹
41
+ * 2. 找到第一个 { 到最后一个 } 的子串,parse
42
+ * 3. parse 失败抛错(调用方会回退)
43
+ */
44
+ function parseSuggestions(raw) {
45
+ let s = raw.trim();
46
+ s = s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
47
+ const first = s.indexOf("{");
48
+ const last = s.lastIndexOf("}");
49
+ if (first < 0 || last <= first) throw new Error("LLM 输出不含 JSON 对象");
50
+ const json = s.slice(first, last + 1);
51
+ const arr = JSON.parse(json).suggestions;
52
+ if (!Array.isArray(arr) || arr.length === 0) throw new Error("JSON 里没有 suggestions 数组");
53
+ return arr.slice(0, 3).map((raw, i) => {
54
+ const x = raw && typeof raw === "object" ? raw : {};
55
+ return {
56
+ tag: typeof x.tag === "string" ? x.tag : "other",
57
+ issue: typeof x.issue === "string" ? x.issue : `建议 #${i + 1}`,
58
+ patch: typeof x.patch === "string" ? x.patch : ""
59
+ };
60
+ });
61
+ }
62
+ //#endregion
63
+ //#region src/host.ts
64
+ const ROUTE_PREFIX = "/dsh-prompt-refine";
65
+ /** 读请求体 */
66
+ function readBody(req) {
67
+ return new Promise((resolve, reject) => {
68
+ const chunks = [];
69
+ req.on("data", (c) => chunks.push(c));
70
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
71
+ req.on("error", reject);
72
+ });
73
+ }
74
+ /** 写 JSON 响应 */
75
+ function sendJson(res, status, body) {
76
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
77
+ res.end(JSON.stringify(body));
78
+ }
79
+ /** 拿到 llm 服务(懒注入:即使它晚于 webServer 就绪也能等到) */
80
+ function resolveLlm(ctx) {
81
+ return new Promise((resolve, reject) => {
82
+ try {
83
+ ctx.inject(["llm"], (llmCtx) => {
84
+ const llm = llmCtx.llm;
85
+ if (!llm) {
86
+ reject(/* @__PURE__ */ new Error("ctx.llm 不存在"));
87
+ return;
88
+ }
89
+ resolve(llm);
90
+ });
91
+ } catch (err) {
92
+ reject(err);
93
+ }
94
+ });
95
+ }
96
+ /**
97
+ * 读用户 settings.yaml 里的 agent-default-model,拿到默认 provider/model。
98
+ * 用逐行扫描而不是 YAML 库:结构固定,避免引入依赖。
99
+ */
100
+ async function resolveDefaultModel() {
101
+ const fallback = {
102
+ provider: "deepseek-official",
103
+ model: "deepseek-v4-flash"
104
+ };
105
+ try {
106
+ const home = process.env.DSH_HOME ?? join(homedir(), ".dsh");
107
+ const lines = (await readFile(join(home, "settings.yaml"), "utf8")).split(/\r?\n/);
108
+ let i = lines.findIndex((l) => /^agent-default-model\s*:/.test(l));
109
+ if (i < 0) return fallback;
110
+ let provider = "";
111
+ let model = "";
112
+ for (i += 1; i < lines.length; i++) {
113
+ const line = lines[i];
114
+ if (/^\S/.test(line)) break;
115
+ const pm = line.match(/^\s+provider\s*:\s*(\S+)/);
116
+ const mm = line.match(/^\s+model\s*:\s*(\S+)/);
117
+ if (pm) provider = pm[1];
118
+ if (mm) model = mm[1];
119
+ }
120
+ if (provider && model) return {
121
+ provider,
122
+ model
123
+ };
124
+ return fallback;
125
+ } catch {
126
+ return fallback;
127
+ }
128
+ }
129
+ /**
130
+ * 调一次 LLM,拼出完整文本。
131
+ * Message 用官方工厂函数构造。
132
+ */
133
+ async function callLlm(ctx, req) {
134
+ const llm = await resolveLlm(ctx);
135
+ const historyMessages = req.history.map((m) => m.role === "user" ? createUserMessage({
136
+ content: [{
137
+ type: "text",
138
+ text: m.content
139
+ }],
140
+ source: { kind: "user" }
141
+ }) : createAssistantMessage({
142
+ content: [{
143
+ type: "text",
144
+ text: m.content
145
+ }],
146
+ source: {
147
+ provider: req.provider,
148
+ model: req.model
149
+ }
150
+ }));
151
+ const draftMessage = createUserMessage({
152
+ content: [{
153
+ type: "text",
154
+ text: buildUserMessage([], req.draft)
155
+ }],
156
+ source: { kind: "user" }
157
+ });
158
+ const stream = llm.stream({
159
+ provider: req.provider,
160
+ model: req.model,
161
+ system: SYSTEM_PROMPT,
162
+ messages: [...historyMessages, draftMessage]
163
+ });
164
+ let out = "";
165
+ let lastError = "";
166
+ for await (const chunk of stream) if (chunk.type === "text-delta" && typeof chunk.text === "string") out += chunk.text;
167
+ else if (chunk.type === "finish") {
168
+ const reason = chunk.reason;
169
+ if (reason?.kind === "error") lastError = reason.failure?.message ?? "未知模型错误";
170
+ }
171
+ if (!out && lastError) throw new Error(`模型调用失败: ${lastError}`);
172
+ return out;
173
+ }
174
+ /** 兜底建议 —— LLM 挂掉或解析失败时给用户一个能用的结果 */
175
+ function fallbackSuggestions() {
176
+ return [
177
+ {
178
+ tag: "clarity",
179
+ issue: "请补充更多背景信息(语言、目标、约束)",
180
+ patch: "请补充:编程语言 / 目标读者 / 期望长度"
181
+ },
182
+ {
183
+ tag: "format",
184
+ issue: "请指定输出格式(代码 / 表格 / 列表 / 段落)",
185
+ patch: "请用 Markdown 格式返回"
186
+ },
187
+ {
188
+ tag: "constraint",
189
+ issue: "请说明边界条件或偏好",
190
+ patch: "请不要使用外部依赖"
191
+ }
192
+ ];
193
+ }
194
+ /** 注册 HTTP 路由 */
195
+ function registerHttpRoutes(ctx) {
196
+ const webServer = ctx.webServer;
197
+ if (!webServer) {
198
+ console.log("[dsh-prompt-refine] webServer 服务不可用,HTTP 端点未注册");
199
+ return;
200
+ }
201
+ webServer.register({
202
+ kind: "prefix",
203
+ path: ROUTE_PREFIX,
204
+ handler: async (req, res) => {
205
+ const url = (req.url ?? "/").split("?")[0];
206
+ const method = req.method ?? "GET";
207
+ if (url !== "/dsh-prompt-refine/suggest" || method !== "POST") {
208
+ sendJson(res, 404, {
209
+ ok: false,
210
+ error: `not found: ${method} ${url}`
211
+ });
212
+ return;
213
+ }
214
+ try {
215
+ const body = await readBody(req);
216
+ const parsed = JSON.parse(body);
217
+ const draft = typeof parsed.draft === "string" ? parsed.draft.trim() : "";
218
+ const history = Array.isArray(parsed.history) ? parsed.history.filter((m) => !!m && typeof m.content === "string" && (m.role === "user" || m.role === "assistant")) : [];
219
+ if (!draft) {
220
+ sendJson(res, 400, {
221
+ ok: false,
222
+ error: "draft 不能为空"
223
+ });
224
+ return;
225
+ }
226
+ const resolved = typeof parsed.provider === "string" && typeof parsed.model === "string" && parsed.provider && parsed.model ? {
227
+ provider: parsed.provider,
228
+ model: parsed.model
229
+ } : await resolveDefaultModel();
230
+ console.log(`[dsh-prompt-refine] 建议请求: draft=${draft.length}字, history=${history.length}条, model=${resolved.provider}/${resolved.model}`);
231
+ try {
232
+ sendJson(res, 200, {
233
+ ok: true,
234
+ suggestions: parseSuggestions(await callLlm(ctx, {
235
+ draft,
236
+ history,
237
+ ...resolved
238
+ })),
239
+ usedModel: `${resolved.provider}/${resolved.model}`
240
+ });
241
+ } catch (err) {
242
+ const msg = err instanceof Error ? err.message : String(err);
243
+ console.log(`[dsh-prompt-refine] LLM 调用或解析失败: ${msg}`);
244
+ sendJson(res, 200, {
245
+ ok: true,
246
+ suggestions: fallbackSuggestions(),
247
+ error: "模型调用失败,已用兜底建议: " + msg,
248
+ usedModel: `${resolved.provider}/${resolved.model}`
249
+ });
250
+ }
251
+ } catch (err) {
252
+ sendJson(res, 500, {
253
+ ok: false,
254
+ error: "服务器错误: " + (err instanceof Error ? err.message : String(err))
255
+ });
256
+ }
257
+ }
258
+ });
259
+ console.log("[dsh-prompt-refine] HTTP 端点已注册: POST " + ROUTE_PREFIX + "/suggest");
260
+ }
261
+ //#endregion
262
+ //#region src/dsh-prompt-refine.ts
263
+ const name = "dsh-prompt-refine";
264
+ const inject = ["webServer"];
265
+ function apply(ctx) {
266
+ registerHttpRoutes(ctx);
267
+ ctx.inject(["llm"], (llmCtx) => {
268
+ console.log("[dsh-prompt-refine] llm 服务已就绪,可以处理优化请求");
269
+ });
270
+ }
271
+ //#endregion
272
+ export { apply, inject, name };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "dsh-prompt-refine",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "lib/index.mjs",
6
+ "description": "DSH 提示词优化插件:在发送前拦截用户提示词,结合上下文给出 3 条改进建议",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/sojo-negai/dsh-prompt-refine.git"
11
+ },
12
+ "keywords": [
13
+ "dsh",
14
+ "dsh-plugin",
15
+ "deepseek-harness",
16
+ "prompt",
17
+ "prompt-engineering"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "default": "./lib/index.mjs"
22
+ },
23
+ "./client": {
24
+ "default": "./lib/client.iife.js"
25
+ },
26
+ "./src/*": "./src/*",
27
+ "./cordis.patch.yml": "./cordis.patch.yml",
28
+ "./package.json": "./package.json"
29
+ },
30
+ "files": [
31
+ "lib",
32
+ "src",
33
+ "scripts",
34
+ "cordis.patch.yml",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "scripts": {
39
+ "build": "tsdown && tsdown --config ./tsdown.client.config.ts",
40
+ "prepack": "npm run build"
41
+ },
42
+ "dsh": {
43
+ "bundle": {
44
+ "patch": "./cordis.patch.yml"
45
+ },
46
+ "client": {
47
+ "platform": "web"
48
+ }
49
+ },
50
+ "peerDependencies": {
51
+ "@deepseek-ai/dsh-llm": ">=0.1.0-0 <0.1.1-0 || >=0.1.1-0 <0.1.2-0 || >=0.1.2-0 <0.2.0-0"
52
+ },
53
+ "devDependencies": {
54
+ "tsdown": "^0.23.0",
55
+ "typescript": "^5.9.2"
56
+ }
57
+ }
@@ -0,0 +1,97 @@
1
+ // scripts/install.mjs —— 把 dsh-prompt-refine 装进指定 DSH profile
2
+ //
3
+ // 用法:
4
+ // node scripts/install.mjs --profile web # 装到单个 profile
5
+ // node scripts/install.mjs --all # 装到所有 profile
6
+ // node scripts/install.mjs --profile web --remove # 从 web 卸载
7
+
8
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs'
9
+ import { dirname, join } from 'node:path'
10
+ import { fileURLToPath } from 'node:url'
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url))
13
+ const PLUGIN_PATH = dirname(__dirname)
14
+
15
+ const PROFILE_DIR = process.env.DSH_HOME ?? join(process.env.USERPROFILE ?? process.env.HOME, '.dsh')
16
+ const ALL_PROFILES = ['desktop', 'web', 'open-design']
17
+
18
+ function parseArgs(argv) {
19
+ const args = { profile: null, all: false, remove: false, help: false }
20
+ for (let i = 2; i < argv.length; i++) {
21
+ const a = argv[i]
22
+ if (a === '--profile') args.profile = argv[++i]
23
+ else if (a === '--all') args.all = true
24
+ else if (a === '--remove') args.remove = true
25
+ else if (a === '--help' || a === '-h') args.help = true
26
+ }
27
+ return args
28
+ }
29
+
30
+ function usage() {
31
+ console.log(`用法:
32
+ node scripts/install.mjs --profile <name> 安装到指定 profile
33
+ node scripts/install.mjs --all 安装到所有 profile
34
+ node scripts/install.mjs --profile <name> --remove 从指定 profile 卸载
35
+
36
+ 可用 profile: ${ALL_PROFILES.join(', ')}`)
37
+ }
38
+
39
+ function listProfiles() {
40
+ return ALL_PROFILES.filter((n) => existsSync(join(PROFILE_DIR, 'profiles', n, 'package.json')))
41
+ }
42
+
43
+ function applyProfile(profileName, remove) {
44
+ const pkgPath = join(PROFILE_DIR, 'profiles', profileName, 'package.json')
45
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
46
+ const deps = pkg.dependencies ?? {}
47
+ const dshProfile = pkg.dsh?.profile ?? {}
48
+ const bundles = dshProfile.bundles ?? []
49
+
50
+ if (remove) {
51
+ let changed = false
52
+ if (deps['dsh-prompt-refine']) {
53
+ delete deps['dsh-prompt-refine']
54
+ changed = true
55
+ }
56
+ const idx = bundles.indexOf('dsh-prompt-refine')
57
+ if (idx >= 0) {
58
+ bundles.splice(idx, 1)
59
+ changed = true
60
+ }
61
+ if (changed) {
62
+ pkg.dependencies = deps
63
+ pkg.dsh = pkg.dsh ?? {}
64
+ pkg.dsh.profile = dshProfile
65
+ pkg.dsh.profile.bundles = bundles
66
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8')
67
+ console.log(` ✓ 已从 ${profileName} 卸载`)
68
+ } else {
69
+ console.log(` - ${profileName} 本来就没装`)
70
+ }
71
+ } else {
72
+ deps['dsh-prompt-refine'] = `file:${PLUGIN_PATH}`
73
+ if (!bundles.includes('dsh-prompt-refine')) bundles.push('dsh-prompt-refine')
74
+ pkg.dependencies = deps
75
+ pkg.dsh = pkg.dsh ?? {}
76
+ pkg.dsh.profile = dshProfile
77
+ pkg.dsh.profile.bundles = bundles
78
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8')
79
+ console.log(` ✓ 已装到 ${profileName}`)
80
+ }
81
+ }
82
+
83
+ const args = parseArgs(process.argv)
84
+ if (args.help || (!args.profile && !args.all)) {
85
+ usage()
86
+ process.exit(args.help ? 0 : 1)
87
+ }
88
+
89
+ const targets = args.all ? listProfiles() : [args.profile]
90
+ for (const name of targets) {
91
+ if (!existsSync(join(PROFILE_DIR, 'profiles', name))) {
92
+ console.warn(`! ${name} 不存在,跳过`)
93
+ continue
94
+ }
95
+ applyProfile(name, args.remove)
96
+ }
97
+ console.log('完成。')