dsh-prompt-enhance 0.3.0 → 0.3.1

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 CHANGED
@@ -47,14 +47,14 @@ flowchart LR
47
47
 
48
48
  The plugin is one npm package with two halves, following the dsh plugin conventions:
49
49
 
50
- - **Host half** (`exports "."`, Node): declares the `prompt-enhance` settings schema (schemastery — the built-in plugin config page renders it straight from the loader config, and the plugin **re-reads it on every request** instead of caching the boot snapshot), registers the `POST /prompt-enhance/enhance` route on the shared webserver (loopback-fenced, body-capped), and the `/enhance` slash command. The model call goes through `ctx.llm.stream` with a normalized output pass — the same auxiliary-call discipline the harness applies to session titles: deadline + caller cancellation rechecked during and after the stream, terminal-finish validation, tool-call rejection.
50
+ - **Host half** (`exports "."`, Node): declares the `prompt-enhance` settings schema (schemastery, every field marked `.volatile()` — the host's `describe()` **drops any entry with no volatile field**, so that mark is both what makes the config page appear and the contract for "edits apply to the next request"; the plugin **re-reads it on every request** instead of caching the boot snapshot), registers the `POST /prompt-enhance/enhance` route on the shared webserver (loopback-fenced, body-capped), and the `/enhance` slash command. The model call goes through `ctx.llm.stream` with a normalized output pass — the same auxiliary-call discipline the harness applies to session titles: deadline + caller cancellation rechecked during and after the stream, terminal-finish validation, tool-call rejection.
51
51
  - **Browser half** (`exports "./client"`): registers the enhance button into the `conversation.input.right` slot and the undo bar into `conversation.input.dock`, binds a live mirror of the settings namespace, and installs the global shortcut. All copy is localized (zh/en) through the harness locale system.
52
52
 
53
53
  ## Requirements
54
54
 
55
55
  - `dsh >= 0.1.1-rc.2 < 0.3.0` — the range states what has actually been exercised, not an open-ended promise.
56
56
  - Boot-verified on `0.1.1-rc.2`, `0.1.2-alpha.3`, `0.1.7-rc.2`, `0.2.0-rc.2`, and **`0.2.1-rc.2`** (layer mounts, enhance route answers, client bundle builds; on alpha the plugin takes the newer `ctx.settings.installSection` registration path, and on `0.2.0-rc.2` it also passes the runtime's plugin-compatibility gate). The `0.2.1-rc.2` check is a **real-host end-to-end smoke**: with the local build installed into a temporary profile, the enhance route returned a real model result; setting that profile's `maxInputChars` to `300` made a 400-character draft fail with `422 {count: 400, max: 300}`, and after editing the same profile to `20000` — **without restarting** — the identical draft went through, so a settings edit reaches the *next* request instead of staying at the boot-time snapshot. The `0.2.0-rc.2` checks include the **dsh desktop application** — the environment this plugin is normally used in. On a `0.2.x` runtime expect some *other* plugins to disappear from the UI: the gate skips any bundle whose `peerDependencies` pin an older `@deepseek-ai/dsh-*` range (observed: `dshmarket`, `dsh-find-plugin`, `dsh-better-sidebar`). The harness has broken several of the APIs this plugin touches: `@deepseek-ai/dsh-settings` changed its registration entry between the two early lines, from `0.1.5-rc.3` the settings service became `SettingsForms` (no synchronous `get`), and `0.2.x` **deleted the `installSettingsSection` / `settingsNamespace` exports outright**; `dsh-agent-default-model` moved the harness default model behind its own `ctx.agentDefaultModel.currentSelection()` service; and the browser-side settings mirror moved from `ctx.settingsScope` to `ctx.configForms`. The plugin probes every generation at runtime and adapts, no configuration needed.
57
- - The ceiling covers the whole `0.2.x` line (including its prereleases); a `0.3.x` is untested and therefore outside the range. `dsh.engines` is advisory metadata read by the loader and plugin manager — it states support, it does not enforce it.
57
+ - The ceiling covers the whole `0.2.x` line (including its prereleases); a `0.3.x` is untested and therefore outside the range. `dsh.engines` **is enforced**: the host's boot preflight marks a mismatched entry `disabled = true`, and only an explicit `--accept-risk` gets past the throw (`Incompatible plugins may cause crashes or data loss`). A wrong range means the plugin does not load — not that it loads with a warning.
58
58
  - Node `^22.19.0 || >=24.0.0` (for building from source)
59
59
  - Browser peers `react` / `react-dom` (`^18.2.0`) come from the web shell's module registry. The host-half module `@deepseek-ai/dsh-llm` is a **runtime module supplied by the dsh host and is not installed from npm** (the `lib/` bundles keep every package import external), so it is declared as an *optional* peer: an install that does not provide it produces no warning, and the plugin reports a normal error at enhance time rather than failing to load.
60
60
 
package/README.zh-CN.md CHANGED
@@ -47,14 +47,14 @@ flowchart LR
47
47
 
48
48
  插件是单个 npm 包、双半区结构,完全遵循 dsh 插件规范:
49
49
 
50
- - **宿主半区**(`exports "."`,Node):声明 `prompt-enhance` 设置 schema(schemastery,由内置插件配置页按加载器配置直接渲染,插件在**每次请求时**重新读取,而不是缓存启动快照),注册共享 webserver 上的 `POST /prompt-enhance/enhance` 路由(仅回环、限长),以及 `/enhance` 斜杠命令。模型调用走 `ctx.llm.stream` 并做规范化处理——与 harness 对会话标题相同的辅助调用纪律:超时与调用方取消在流过程中和结束后复查、终结 finish 校验、拒绝工具调用。
50
+ - **宿主半区**(`exports "."`,Node):声明 `prompt-enhance` 设置 schema(schemastery,每个字段带 `.volatile()`——宿主的 `describe()` 会**跳过没有任何 volatile 字段的条目**,这个标记既是"设置页会出现"的前提,也是"改完下次请求就生效"的契约;插件在**每次请求时**重新读取,而不是缓存启动快照),注册共享 webserver 上的 `POST /prompt-enhance/enhance` 路由(仅回环、限长),以及 `/enhance` 斜杠命令。模型调用走 `ctx.llm.stream` 并做规范化处理——与 harness 对会话标题相同的辅助调用纪律:超时与调用方取消在流过程中和结束后复查、终结 finish 校验、拒绝工具调用。
51
51
  - **浏览器半区**(`exports "./client"`):把增强按钮注册进 `conversation.input.right` 插槽、撤销条注册进 `conversation.input.dock` 插槽,绑定设置命名空间的实时镜像,并安装全局快捷键。全部文案经 harness locale 系统提供中英双语。
52
52
 
53
53
  ## 环境要求
54
54
 
55
55
  - `dsh >= 0.1.1-rc.2 < 0.3.0` —— 这个区间表示**实际验证过的范围**,而不是无限期的承诺。
56
56
  - 实测环境:`0.1.1-rc.2`、`0.1.2-alpha.3`、`0.1.7-rc.2`、`0.2.0-rc.2`、**`0.2.1-rc.2`** 均已 boot 验证(插件层挂载、增强路由应答、客户端 bundle 构建通过;alpha 上走新版 `ctx.settings.installSection` 注册路径,`0.2.0-rc.2` 上还通过了运行时的插件兼容性闸门)。`0.2.1-rc.2` 的验证是**真实宿主的端到端冒烟**:在临时 profile 里装入本地构建后,增强路由对着在线模型返回了真实结果;把该 profile 的 `maxInputChars` 设为 `300` 后,400 字草稿被 `422 {count:400,max:300}` 拒绝,随后**不重启**把它改成 `20000`,同一份草稿立即通过——设置改动确实作用到下一次请求,而不是停在启动时的快照。其中 `0.2.0-rc.2` 的验证包含 **dsh 桌面应用**——也就是本插件日常实际运行的环境。在 `0.2.x` 运行时上,可能会发现**别的**插件从界面消失:凡 `peerDependencies` 把 `@deepseek-ai/dsh-*` 锁在旧区间的 bundle 都会被闸门跳过(实测被跳过的是 `dshmarket`、`dsh-find-plugin`、`dsh-better-sidebar`)。宿主在插件用到的几处 API 上做过破坏性变更:`@deepseek-ai/dsh-settings` 的注册入口在两条早期版本线之间变过,`0.1.5-rc.3` 起 settings 服务改为 `SettingsForms`(不再有同步 `get`),而 `0.2.x` 干脆**删掉了 `installSettingsSection` / `settingsNamespace` 导出**;`dsh-agent-default-model` 把全局默认模型收进了自己的 `ctx.agentDefaultModel.currentSelection()` 服务;浏览器侧的设置镜像则从 `ctx.settingsScope` 迁到了 `ctx.configForms`。插件对每一代形态均做运行时探测并自动适配,无需配置。
57
- - 上界覆盖整条 `0.2.x` 线(含其预发布版本);`0.3.x` 尚未验证,因此不在区间内。`dsh.engines` 是加载器与插件管理器读取的**声明性元数据**——它表达支持范围,并不强制拦截。
57
+ - 上界覆盖整条 `0.2.x` 线(含其预发布版本);`0.3.x` 尚未验证,因此不在区间内。`dsh.engines` **是强制的**:宿主启动预检会把不匹配的条目直接 `disabled = true`,要放行必须显式 `--accept-risk`,否则抛 `Incompatible plugins may cause crashes or data loss`。所以这个区间写错的结果是**插件不加载**,而不是"只提示一下"。
58
58
  - Node `^22.19.0 || >=24.0.0`(仅从源码构建时需要)
59
59
  - 浏览器侧 peer `react` / `react-dom`(`^18.2.0`)由 Web 壳的模块注册表提供。宿主半区用到的 `@deepseek-ai/dsh-llm` 是**由 dsh 宿主提供的运行时模块,不从 npm 安装**(`lib/` 产物把包引用全部保持为外部),因此声明为**可选** peer:未提供它的环境不会出现安装警告,增强时按正常错误路径报错,而不是加载失败。
60
60
 
@@ -0,0 +1,50 @@
1
+ # 0.3.1 — 设置页真的会出现,失败流真的会关
2
+
3
+ 0.3.0 把插件搬到了 `0.2.1-rc.2` 的契约上,但有两条最关键的路径**在真实宿主上从未生效**:设置页不存在,失败流不结束。两条都属于"看着在、其实不在",所以这次每条都配了一个**在旧代码上必然失败**的测试。
4
+
5
+ ## 两个致命项
6
+
7
+ | 面 | 0.2.x 的真实行为 | 用户看到什么 | 修法 |
8
+ |---|---|---|---|
9
+ | **设置** | `SettingsForms.describe()` 用内部 `volatileForm()` 过滤每个条目:**没有任何字段带 `meta.volatile` 时整条被丢弃** | 设置页里没有本插件;即使写入也会抛 `Plugin entry "prompt-enhance" has no volatile fields` | 16 个字段全部加 `.volatile()`;schema 库从裸 `schemastery` 换成 `@deepseek-ai/schemastery`(只有它带 `.volatile()` 与配套的位置校验) |
10
+ | **流式失败** | 错误分支写了 `event: error` 帧,**没有 `res.end()`** | 浏览器因为收到帧而正常显示错误;`curl` / `response.text()` / 代理**永久挂起** | 成功与失败共用 `endResponse()` 收尾 |
11
+
12
+ 复现与验证(不是推断):
13
+
14
+ ```
15
+ 宿主 volatileForm(Config) → 修复前: undefined(条目被跳过)
16
+ → 修复后: present, 16 个字段
17
+ 真宿主 dsh web @0.2.1-rc.2 → 插件激活、POST /prompt-enhance/enhance
18
+ 返回 deepseek-official / deepseek-flash 的真实结果
19
+ ```
20
+
21
+ 失败流那条的证伪:把 `endResponse(res)` 注释掉,新用例在 2.07 秒被 abort 判失败——正是"永久挂起"。
22
+
23
+ ## 生命周期与失败路径
24
+
25
+ - **路由不随卸载回收**:`webServer.register` 只返回"从表里删项"的函数,disposer 却存在模块级 map 里、无人调用 ⇒ 卸载后路由仍用旧配置与旧限流窗答卷,重载撞 `duplicate prefix route`。改为 `ctx.inject(['webServer'])` + `ctx.effect`,服务按身份跟随、随光纤回收。
26
+ - **`/enhance` 在无 webServer 的组合里整体消失**:`inject` 里硬写了 `['llm','webServer']`。`webServer` 移出硬依赖、改按需注入后,斜杠命令在纯 CLI 组合里依然可用。
27
+ - **`/enhance` 遇配置异常直接抛**:`readConfig()` 在 `try` 之外,provider/model 半填时抛出的是未捕获异常,而不是路由侧那句中英双语提示。已移入 `try`。
28
+ - **流式端点把"服务端拒绝"当成"宿主不支持流式"**:`response.ok === false` 时丢弃整个错误信封并**原样重发一次**——429 反而打两次、结构化原因(`limit`/`retryAfterSeconds`)全丢。现在先按 `ok` 分流:拒绝即报拒绝,只有"200 但不是流"才回退一次性。
29
+ - **面板打开时快捷键打到别的会话**:结果面板 `createPortal` 挂在 `body` 上并接管焦点,上溯循环走到 `body` 时 `body.contains(每个 composer)` 恒真 ⇒ 返回注册表里的第一个会话。现在遇 `body`/`documentElement` 即停,退回"最近挂载的会话"。证伪:去掉这行判断,用例立刻变成"点了另一个会话"。
30
+ - **设置镜像可能永久停在默认值**:`boundId` 在 `forms.get()` **之前**落值,取不到时后续每次 describe tick 都因 `entryId === boundId` 直接返回 ⇒ 永不重试。改为取到 form 之后才认领 id。
31
+
32
+ ## 可诊断性与小项
33
+
34
+ - 客户端三处空 `catch` 改为 `console.warn`(模块注释早就承诺 "caught and logged",代码里一行都没有)——"按钮不出现"这类报告唯一的线索来源。
35
+ - 对话框 Tab 焦点陷阱的**反向**漏:面板根节点是刻意 `tabIndex={-1}`,既不是 first 也不是 last,`Shift+Tab` 能直接逃出。已把根节点视为"first 之前"。
36
+ - 关闭插件后不再抢键:此前仍 `preventDefault` 后跑一个空动作,等于静默吃掉用户绑给同一组合的宿主快捷键。
37
+ - `scripts/build.mjs` 列在 `tsconfig.include` 里但 `allowJs` 没开 ⇒ 从未被检查。现在开 `checkJs`,并给 esbuild 选项补 JSDoc(否则 `packages: 'external'` 会退化成 `string`)。
38
+ - README 不再称 `dsh.engines` 为"声明性、不强制":宿主启动预检会直接 `disabled = true`,放行必须显式 `--accept-risk`。
39
+
40
+ ## 验证
41
+
42
+ - `npm run typecheck` 干净(`build.mjs` 现在确实在检查范围内,`tsc --listFilesOnly` 命中 1)
43
+ - `npm test` **24 文件 / 242 用例全绿**(0.3.0 为 22 / 231,新增 `client-enhance-client`、`client-shortcut-target` 及配置/路由/镜像若干条)
44
+ - 真实宿主冒烟:临时 profile 装本地包 → 插件激活、无 `pending` / `did not activate` → 增强路由返回真实模型结果 → 临时 profile 与临时目录已删除
45
+
46
+ ## 未做(有意)
47
+
48
+ - **快捷键没有迁移到宿主的 `shortcuts` 服务**(`dsh-client-shortcuts` 支持用户改键与冲突消解)。迁移需要先确认该服务的注册契约与冲突语义,属于独立改动,不在这次"失败路径闭包"的范围内。
49
+ - **`Origin` 只比对 hostname、不比对端口**:任意 `localhost:任意端口` 的页面都被信任。收紧需要把服务实际监听的端口传进栅栏,同样属独立改动。
50
+ - 跨代探测的兼容死分支(`installSection`、旧 settings 模块、`webUiSettings`、`settingsScope`)保留:它们在旧版本线上仍是唯一的路径。
package/lib/client.js CHANGED
@@ -124,6 +124,16 @@ async function readEnvelope(response) {
124
124
  }
125
125
  throw new EnhanceClientError({ code: "internal", message: `\u5BBF\u4E3B\u670D\u52A1\u8FD4\u56DE\u5F02\u5E38\uFF08HTTP ${response.status}\uFF09\u3002` });
126
126
  }
127
+ async function rejectionOf(response) {
128
+ try {
129
+ const parsed = await response.json();
130
+ if (parsed !== null && typeof parsed === "object" && parsed.error !== void 0) {
131
+ return new EnhanceClientError(parseError(parsed.error));
132
+ }
133
+ } catch {
134
+ }
135
+ return new EnhanceClientError({ code: "internal", message: `\u5BBF\u4E3B\u670D\u52A1\u62D2\u7EDD\u4E86\u8BF7\u6C42\uFF08HTTP ${response.status}\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002` });
136
+ }
127
137
  function takeFrames(buffer) {
128
138
  const frames = [];
129
139
  let rest = buffer;
@@ -170,9 +180,9 @@ async function requestEnhanceStream(body, options) {
170
180
  throw new EnhanceClientError({ code: "internal", message: "\u65E0\u6CD5\u8FDE\u63A5\u5BBF\u4E3B\u670D\u52A1\uFF0C\u8BF7\u786E\u8BA4 dsh web \u6B63\u5728\u8FD0\u884C\u540E\u91CD\u8BD5\u3002" });
171
181
  }
172
182
  const contentType = String(response.headers.get("content-type") ?? "").toLowerCase();
183
+ if (!response.ok) throw await rejectionOf(response);
173
184
  const stream = response.body;
174
- const streamable = response.ok && stream !== null && stream !== void 0 && contentType.includes("event-stream");
175
- if (stream === null || stream === void 0 || !streamable) {
185
+ if (stream === null || stream === void 0 || !contentType.includes("event-stream")) {
176
186
  void Promise.resolve().then(() => stream?.cancel()).catch(() => {
177
187
  });
178
188
  return requestEnhance(body, options.signal);
@@ -295,10 +305,12 @@ function ResultPanel(props) {
295
305
  const first = focusables[0];
296
306
  const last = focusables[focusables.length - 1];
297
307
  if (first === void 0 || last === void 0) return;
298
- if (event.shiftKey && document.activeElement === first) {
308
+ const active = document.activeElement;
309
+ const atRoot = active === panel;
310
+ if (event.shiftKey && (active === first || atRoot)) {
299
311
  event.preventDefault();
300
312
  last.focus();
301
- } else if (!event.shiftKey && document.activeElement === last) {
313
+ } else if (!event.shiftKey && active === last) {
302
314
  event.preventDefault();
303
315
  first.focus();
304
316
  }
@@ -562,6 +574,7 @@ function shortcutTarget() {
562
574
  if (active !== null && active instanceof Element) {
563
575
  let node = active;
564
576
  for (let depth = 0; depth < 8 && node !== null; depth++) {
577
+ if (node === document.body || node === document.documentElement) break;
565
578
  for (const entry of sessions.values()) {
566
579
  if (entry.root !== null && (node === entry.root || node.contains(entry.root))) {
567
580
  const found = entry;
@@ -1168,12 +1181,6 @@ function installClientSettingsSource(ctx, options) {
1168
1181
  const bind = () => {
1169
1182
  const entryId = resolveEntryId();
1170
1183
  if (entryId === void 0 || entryId === boundId) return;
1171
- try {
1172
- unsubscribeForm?.();
1173
- } catch {
1174
- }
1175
- unsubscribeForm = void 0;
1176
- boundId = entryId;
1177
1184
  let form;
1178
1185
  try {
1179
1186
  form = forms.get?.(entryId);
@@ -1181,6 +1188,12 @@ function installClientSettingsSource(ctx, options) {
1181
1188
  return;
1182
1189
  }
1183
1190
  if (form === void 0) return;
1191
+ try {
1192
+ unsubscribeForm?.();
1193
+ } catch {
1194
+ }
1195
+ unsubscribeForm = void 0;
1196
+ boundId = entryId;
1184
1197
  adoptSnapshot(RANK.configForms, form);
1185
1198
  try {
1186
1199
  unsubscribeForm = form.subscribe?.(() => adoptSnapshot(RANK.configForms, form));
@@ -1263,15 +1276,22 @@ function installClientSettingsSource(ctx, options) {
1263
1276
 
1264
1277
  // src/client/index.tsx
1265
1278
  var inject = [];
1279
+ function report(step, error) {
1280
+ try {
1281
+ console.warn(`[prompt-enhance] ${step} failed; that feature stays off.`, error);
1282
+ } catch {
1283
+ }
1284
+ return () => {
1285
+ };
1286
+ }
1266
1287
  function apply(ctx) {
1267
1288
  ensureStyles();
1268
1289
  ctx.inject(["locale"], (localeCtx) => {
1269
1290
  ctx.effect(() => {
1270
1291
  try {
1271
1292
  return localeCtx.locale.register(NS, dictionaries);
1272
- } catch {
1273
- return () => {
1274
- };
1293
+ } catch (error) {
1294
+ return report("dictionary registration", error);
1275
1295
  }
1276
1296
  }, "dsh-prompt-enhance: dictionaries");
1277
1297
  });
@@ -1289,9 +1309,8 @@ function apply(ctx) {
1289
1309
  { name: "conversation.input.right", id: "prompt-enhance", order: 60, locale: NS },
1290
1310
  EnhanceButton
1291
1311
  );
1292
- } catch {
1293
- return () => {
1294
- };
1312
+ } catch (error) {
1313
+ return report("composer enhance button slot", error);
1295
1314
  }
1296
1315
  });
1297
1316
  });
@@ -1304,16 +1323,17 @@ function apply(ctx) {
1304
1323
  { name: "conversation.input.dock", id: "prompt-enhance-undo", order: 90, locale: NS },
1305
1324
  UndoBar
1306
1325
  );
1307
- } catch {
1308
- return () => {
1309
- };
1326
+ } catch (error) {
1327
+ return report("undo bar slot", error);
1310
1328
  }
1311
1329
  });
1312
1330
  });
1313
1331
  ctx.effect(() => {
1314
1332
  const onKeyDown = (event) => {
1315
1333
  if (event.defaultPrevented || event.isComposing) return;
1316
- const combo = parseShortcut(getClientSettings().shortcut);
1334
+ const settings = getClientSettings();
1335
+ if (!settings.enabled) return;
1336
+ const combo = parseShortcut(settings.shortcut);
1317
1337
  if (!matchesShortcut(event, combo)) return;
1318
1338
  const target = shortcutTarget();
1319
1339
  if (target === void 0) return;
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/config.ts
2
- import z from "schemastery";
2
+ import z from "@deepseek-ai/schemastery";
3
3
  var PROMPT_ENHANCE_NAMESPACE = "prompt-enhance";
4
4
  var DEFAULT_CONFIG = {
5
5
  enabled: true,
@@ -18,22 +18,22 @@ var DEFAULT_CONFIG = {
18
18
  contextMaxChars: 4e3
19
19
  };
20
20
  var Config = z.object({
21
- enabled: z.boolean().default(DEFAULT_CONFIG.enabled).description("\u603B\u5F00\u5173\uFF1A\u5173\u95ED\u540E\u9690\u85CF\u8F93\u5165\u6846\u589E\u5F3A\u6309\u94AE\u5E76\u505C\u7528\u6240\u6709\u89E6\u53D1\u65B9\u5F0F"),
22
- provider: z.string().description("\u8986\u76D6\u6A21\u578B\u8DEF\u7531\u7684 provider\uFF08\u4E0E\u300C\u6A21\u578B\u300D\u5FC5\u987B\u6210\u5BF9\u586B\u5199\uFF1B\u7559\u7A7A\u8DDF\u968F\u5F53\u524D\u4F1A\u8BDD\u6A21\u578B\uFF09"),
23
- model: z.string().description("\u8986\u76D6\u6A21\u578B\u8DEF\u7531\u7684 model\uFF08\u4E0E\u300Cprovider\u300D\u5FC5\u987B\u6210\u5BF9\u586B\u5199\uFF1B\u7559\u7A7A\u8DDF\u968F\u5F53\u524D\u4F1A\u8BDD\u6A21\u578B\uFF09"),
24
- temperature: z.number().min(0).max(1).step(0.05).default(DEFAULT_CONFIG.temperature).description("\u91C7\u6837\u6E29\u5EA6\uFF1B\u4F4E\u6E29\u6539\u5199\u66F4\u5FE0\u5B9E\u4E8E\u539F\u610F"),
25
- maxOutputTokens: z.number().step(1).min(256).max(32768).default(DEFAULT_CONFIG.maxOutputTokens).description("\u5355\u6B21\u589E\u5F3A\u7684\u8F93\u51FA token \u4E0A\u9650"),
26
- maxInputChars: z.number().step(1).min(200).max(2e5).default(DEFAULT_CONFIG.maxInputChars).description("\u8F93\u5165\u5B57\u6570\u4E0A\u9650\uFF1B\u8D85\u9650\u62D2\u7EDD\u800C\u4E0D\u622A\u65AD\uFF0C\u907F\u514D\u6539\u53D8\u539F\u610F"),
27
- timeoutMs: z.number().step(1).min(5e3).max(6e5).default(DEFAULT_CONFIG.timeoutMs).description("\u5355\u6B21\u589E\u5F3A\u7684\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09"),
28
- systemPrompt: z.string().role("textarea").default(DEFAULT_CONFIG.systemPrompt).description("\u7CFB\u7EDF\u63D0\u793A\u8BCD\uFF1B\u7559\u7A7A\u4F7F\u7528\u5185\u7F6E\u589E\u5F3A\u7B56\u7565"),
29
- strategyMode: z.union(["replace-default", "extend-default"]).default(DEFAULT_CONFIG.strategyMode).description("\u81EA\u5B9A\u4E49\u7CFB\u7EDF\u63D0\u793A\u8BCD\u7684\u7EC4\u5408\u65B9\u5F0F\uFF1A\u6574\u4F53\u66FF\u6362\u5185\u7F6E\u7B56\u7565\uFF08\u9ED8\u8BA4\uFF1B\u5185\u7F6E\u7B56\u7565\u7684\u4E0D\u7F16\u9020\u3001\u53EA\u8F93\u51FA\u6B63\u6587\u7B49\u786C\u6027\u7EA6\u675F\u4E0D\u4F1A\u81EA\u52A8\u4FDD\u7559\uFF09\uFF0C\u6216\u628A\u81EA\u5B9A\u4E49\u6587\u672C\u8FFD\u52A0\u5728\u5185\u7F6E\u7B56\u7565\u4E4B\u540E\uFF08\u786C\u6027\u7EA6\u675F\u7EE7\u7EED\u751F\u6548\uFF09"),
30
- shortcut: z.string().default(DEFAULT_CONFIG.shortcut).description("\u89E6\u53D1\u5FEB\u6377\u952E\uFF08\u5982 ctrl+alt+e\uFF1B\u987B\u5305\u542B ctrl/alt/meta \u4E2D\u81F3\u5C11\u4E00\u4E2A\u4FEE\u9970\u952E\uFF0Cshift \u4EC5\u53EF\u4F5C\u9644\u52A0\uFF0C\u7EAF\u5B57\u6BCD/\u6570\u5B57\u6216 shift+\u5B57\u6BCD\u4F1A\u88AB\u5FFD\u7565\uFF1B\u7559\u7A7A\u7981\u7528\uFF09"),
31
- maxConcurrent: z.number().step(1).min(1).max(16).default(DEFAULT_CONFIG.maxConcurrent).description("\u5BBF\u4E3B\u4FA7\u5E76\u53D1\u4E0A\u9650\uFF1A\u540C\u65F6\u8FDB\u884C\u7684\u589E\u5F3A\u8C03\u7528\u6570\uFF0C\u8D85\u51FA\u7684\u8BF7\u6C42\u8FD4\u56DE 429"),
32
- rateLimitPerMinute: z.number().step(1).min(1).max(600).default(DEFAULT_CONFIG.rateLimitPerMinute).description("\u6BCF\u5206\u949F\u589E\u5F3A\u6B21\u6570\u4E0A\u9650\uFF08\u6ED1\u52A8\u7A97\u53E3\uFF09\uFF0C\u8D85\u51FA\u8FD4\u56DE 429"),
33
- streaming: z.boolean().default(DEFAULT_CONFIG.streaming).description("\u589E\u91CF\u5C55\u793A\uFF1A\u6A21\u578B\u8FB9\u5199\u8FB9\u5728\u9762\u677F\u663E\u793A\uFF08\u4EC5\u5F71\u54CD\u663E\u793A\u8282\u594F\uFF0C\u6700\u7EC8\u7ED3\u679C\u4E0D\u53D8\uFF1B\u5BBF\u4E3B\u6216\u7F51\u7EDC\u4E0D\u652F\u6301\u65F6\u81EA\u52A8\u56DE\u9000\u4E3A\u4E00\u6B21\u6027\u8FD4\u56DE\uFF09"),
34
- contextAware: z.boolean().default(DEFAULT_CONFIG.contextAware).description("\u4E0A\u4E0B\u6587\u611F\u77E5\uFF1A\u8BFB\u53D6\u5F53\u524D\u4F1A\u8BDD\u8FD1\u671F\u5BF9\u8BDD\uFF0C\u7528\u4E8E\u6D88\u89E3\u6307\u4EE3\u3001\u8865\u5168\u7701\u7565\u4E0E\u672F\u8BED\u7EA6\u675F\uFF1B\u65E0\u5386\u53F2\u6216\u5386\u53F2\u4E0D\u8DB3\u65F6\u81EA\u52A8\u56DE\u9000\u4E3A\u5355\u6761\u589E\u5F3A\uFF0C\u4E0D\u81C6\u9020\u4FE1\u606F"),
35
- contextMaxMessages: z.number().step(1).min(0).max(50).default(DEFAULT_CONFIG.contextMaxMessages).description("\u4E0A\u4E0B\u6587\u7A97\u53E3\uFF08\u6761\u6570\uFF09\uFF1A\u6700\u591A\u53C2\u8003\u6700\u8FD1\u591A\u5C11\u6761 user/assistant \u8F6E\u6B21\uFF1B0 \u8868\u793A\u4E0D\u53C2\u8003"),
36
- contextMaxChars: z.number().step(1).min(0).max(1e5).default(DEFAULT_CONFIG.contextMaxChars).description("\u4E0A\u4E0B\u6587\u7A97\u53E3\uFF08\u5B57\u6570\uFF09\uFF1A\u5386\u53F2\u7247\u6BB5\u7684\u5B57\u7B26\u9884\u7B97\uFF0C\u4ECE\u6700\u65B0\u4E00\u6761\u5411\u524D\u88C5\u914D\uFF1B0 \u8868\u793A\u4E0D\u53C2\u8003")
21
+ enabled: z.boolean().default(DEFAULT_CONFIG.enabled).description("\u603B\u5F00\u5173\uFF1A\u5173\u95ED\u540E\u9690\u85CF\u8F93\u5165\u6846\u589E\u5F3A\u6309\u94AE\u5E76\u505C\u7528\u6240\u6709\u89E6\u53D1\u65B9\u5F0F").volatile(),
22
+ provider: z.string().description("\u8986\u76D6\u6A21\u578B\u8DEF\u7531\u7684 provider\uFF08\u4E0E\u300C\u6A21\u578B\u300D\u5FC5\u987B\u6210\u5BF9\u586B\u5199\uFF1B\u7559\u7A7A\u8DDF\u968F\u5F53\u524D\u4F1A\u8BDD\u6A21\u578B\uFF09").volatile(),
23
+ model: z.string().description("\u8986\u76D6\u6A21\u578B\u8DEF\u7531\u7684 model\uFF08\u4E0E\u300Cprovider\u300D\u5FC5\u987B\u6210\u5BF9\u586B\u5199\uFF1B\u7559\u7A7A\u8DDF\u968F\u5F53\u524D\u4F1A\u8BDD\u6A21\u578B\uFF09").volatile(),
24
+ temperature: z.number().min(0).max(1).step(0.05).default(DEFAULT_CONFIG.temperature).description("\u91C7\u6837\u6E29\u5EA6\uFF1B\u4F4E\u6E29\u6539\u5199\u66F4\u5FE0\u5B9E\u4E8E\u539F\u610F").volatile(),
25
+ maxOutputTokens: z.number().step(1).min(256).max(32768).default(DEFAULT_CONFIG.maxOutputTokens).description("\u5355\u6B21\u589E\u5F3A\u7684\u8F93\u51FA token \u4E0A\u9650").volatile(),
26
+ maxInputChars: z.number().step(1).min(200).max(2e5).default(DEFAULT_CONFIG.maxInputChars).description("\u8F93\u5165\u5B57\u6570\u4E0A\u9650\uFF1B\u8D85\u9650\u62D2\u7EDD\u800C\u4E0D\u622A\u65AD\uFF0C\u907F\u514D\u6539\u53D8\u539F\u610F").volatile(),
27
+ timeoutMs: z.number().step(1).min(5e3).max(6e5).default(DEFAULT_CONFIG.timeoutMs).description("\u5355\u6B21\u589E\u5F3A\u7684\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09").volatile(),
28
+ systemPrompt: z.string().role("textarea").default(DEFAULT_CONFIG.systemPrompt).description("\u7CFB\u7EDF\u63D0\u793A\u8BCD\uFF1B\u7559\u7A7A\u4F7F\u7528\u5185\u7F6E\u589E\u5F3A\u7B56\u7565").volatile(),
29
+ strategyMode: z.union(["replace-default", "extend-default"]).default(DEFAULT_CONFIG.strategyMode).description("\u81EA\u5B9A\u4E49\u7CFB\u7EDF\u63D0\u793A\u8BCD\u7684\u7EC4\u5408\u65B9\u5F0F\uFF1A\u6574\u4F53\u66FF\u6362\u5185\u7F6E\u7B56\u7565\uFF08\u9ED8\u8BA4\uFF1B\u5185\u7F6E\u7B56\u7565\u7684\u4E0D\u7F16\u9020\u3001\u53EA\u8F93\u51FA\u6B63\u6587\u7B49\u786C\u6027\u7EA6\u675F\u4E0D\u4F1A\u81EA\u52A8\u4FDD\u7559\uFF09\uFF0C\u6216\u628A\u81EA\u5B9A\u4E49\u6587\u672C\u8FFD\u52A0\u5728\u5185\u7F6E\u7B56\u7565\u4E4B\u540E\uFF08\u786C\u6027\u7EA6\u675F\u7EE7\u7EED\u751F\u6548\uFF09").volatile(),
30
+ shortcut: z.string().default(DEFAULT_CONFIG.shortcut).description("\u89E6\u53D1\u5FEB\u6377\u952E\uFF08\u5982 ctrl+alt+e\uFF1B\u987B\u5305\u542B ctrl/alt/meta \u4E2D\u81F3\u5C11\u4E00\u4E2A\u4FEE\u9970\u952E\uFF0Cshift \u4EC5\u53EF\u4F5C\u9644\u52A0\uFF0C\u7EAF\u5B57\u6BCD/\u6570\u5B57\u6216 shift+\u5B57\u6BCD\u4F1A\u88AB\u5FFD\u7565\uFF1B\u7559\u7A7A\u7981\u7528\uFF09").volatile(),
31
+ maxConcurrent: z.number().step(1).min(1).max(16).default(DEFAULT_CONFIG.maxConcurrent).description("\u5BBF\u4E3B\u4FA7\u5E76\u53D1\u4E0A\u9650\uFF1A\u540C\u65F6\u8FDB\u884C\u7684\u589E\u5F3A\u8C03\u7528\u6570\uFF0C\u8D85\u51FA\u7684\u8BF7\u6C42\u8FD4\u56DE 429").volatile(),
32
+ rateLimitPerMinute: z.number().step(1).min(1).max(600).default(DEFAULT_CONFIG.rateLimitPerMinute).description("\u6BCF\u5206\u949F\u589E\u5F3A\u6B21\u6570\u4E0A\u9650\uFF08\u6ED1\u52A8\u7A97\u53E3\uFF09\uFF0C\u8D85\u51FA\u8FD4\u56DE 429").volatile(),
33
+ streaming: z.boolean().default(DEFAULT_CONFIG.streaming).description("\u589E\u91CF\u5C55\u793A\uFF1A\u6A21\u578B\u8FB9\u5199\u8FB9\u5728\u9762\u677F\u663E\u793A\uFF08\u4EC5\u5F71\u54CD\u663E\u793A\u8282\u594F\uFF0C\u6700\u7EC8\u7ED3\u679C\u4E0D\u53D8\uFF1B\u5BBF\u4E3B\u6216\u7F51\u7EDC\u4E0D\u652F\u6301\u65F6\u81EA\u52A8\u56DE\u9000\u4E3A\u4E00\u6B21\u6027\u8FD4\u56DE\uFF09").volatile(),
34
+ contextAware: z.boolean().default(DEFAULT_CONFIG.contextAware).description("\u4E0A\u4E0B\u6587\u611F\u77E5\uFF1A\u8BFB\u53D6\u5F53\u524D\u4F1A\u8BDD\u8FD1\u671F\u5BF9\u8BDD\uFF0C\u7528\u4E8E\u6D88\u89E3\u6307\u4EE3\u3001\u8865\u5168\u7701\u7565\u4E0E\u672F\u8BED\u7EA6\u675F\uFF1B\u65E0\u5386\u53F2\u6216\u5386\u53F2\u4E0D\u8DB3\u65F6\u81EA\u52A8\u56DE\u9000\u4E3A\u5355\u6761\u589E\u5F3A\uFF0C\u4E0D\u81C6\u9020\u4FE1\u606F").volatile(),
35
+ contextMaxMessages: z.number().step(1).min(0).max(50).default(DEFAULT_CONFIG.contextMaxMessages).description("\u4E0A\u4E0B\u6587\u7A97\u53E3\uFF08\u6761\u6570\uFF09\uFF1A\u6700\u591A\u53C2\u8003\u6700\u8FD1\u591A\u5C11\u6761 user/assistant \u8F6E\u6B21\uFF1B0 \u8868\u793A\u4E0D\u53C2\u8003").volatile(),
36
+ contextMaxChars: z.number().step(1).min(0).max(1e5).default(DEFAULT_CONFIG.contextMaxChars).description("\u4E0A\u4E0B\u6587\u7A97\u53E3\uFF08\u5B57\u6570\uFF09\uFF1A\u5386\u53F2\u7247\u6BB5\u7684\u5B57\u7B26\u9884\u7B97\uFF0C\u4ECE\u6700\u65B0\u4E00\u6761\u5411\u524D\u88C5\u914D\uFF1B0 \u8868\u793A\u4E0D\u53C2\u8003").volatile()
37
37
  });
38
38
  function resolveConfig(config) {
39
39
  if (config === null || typeof config !== "object") throw new Error("prompt-enhance: configuration is required");
@@ -867,6 +867,13 @@ function writeJson(res, status, body) {
867
867
  });
868
868
  res.end(JSON.stringify(body));
869
869
  }
870
+ function endResponse(res) {
871
+ if (res.writableEnded || res.destroyed) return;
872
+ try {
873
+ res.end();
874
+ } catch {
875
+ }
876
+ }
870
877
 
871
878
  // src/enhance-routes.ts
872
879
  var bodyCapOf = (maxInputChars) => maxInputChars * 6 + 4096;
@@ -1005,12 +1012,7 @@ data: ${JSON.stringify({ type: "delta", text: tail })}
1005
1012
  data: ${JSON.stringify({ type: "done", value })}
1006
1013
 
1007
1014
  `);
1008
- if (!res.writableEnded && !res.destroyed) {
1009
- try {
1010
- res.end();
1011
- } catch {
1012
- }
1013
- }
1015
+ endResponse(res);
1014
1016
  } finally {
1015
1017
  sse.dispose();
1016
1018
  }
@@ -1030,10 +1032,16 @@ data: ${JSON.stringify({ type: "done", value })}
1030
1032
  }
1031
1033
  const wire = toEnhanceError(error);
1032
1034
  if (stream && res.headersSent) {
1033
- if (!res.writableEnded) res.write(`event: error
1035
+ if (!res.writableEnded) {
1036
+ try {
1037
+ res.write(`event: error
1034
1038
  data: ${JSON.stringify({ type: "error", error: wire })}
1035
1039
 
1036
1040
  `);
1041
+ } catch {
1042
+ }
1043
+ }
1044
+ endResponse(res);
1037
1045
  } else {
1038
1046
  writeJson(res, wire.code === "timeout" ? 504 : wire.code === "unconfigured" ? 409 : 502, { ok: false, error: wire });
1039
1047
  }
@@ -1044,27 +1052,40 @@ data: ${JSON.stringify({ type: "error", error: wire })}
1044
1052
  }
1045
1053
  var mounts = /* @__PURE__ */ new WeakMap();
1046
1054
  function registerEnhanceRoute(ctx, readConfig) {
1047
- const webserver = ctx.get("webServer");
1048
- if (webserver === void 0) return;
1049
- const existing = mounts.get(ctx);
1050
- if (existing !== void 0 && existing.webserver === webserver) return;
1051
- if (existing !== void 0) {
1052
- try {
1053
- existing.dispose();
1054
- } catch {
1055
- }
1056
- }
1057
- const gate = { stamps: [], active: 0, failures: 0, cooldownUntil: 0 };
1058
- const dispose = webserver.register({
1059
- kind: "prefix",
1060
- path: ENHANCE_PREFIX,
1061
- handler: (req, res) => {
1062
- const pathname = new URL(req.url ?? "/", "http://x").pathname;
1063
- return serveEnhance(ctx, readConfig, gate, pathname, req, res, pathname === ENHANCE_STREAM_ENDPOINT);
1055
+ ctx.inject(["webServer"], (routeCtx) => {
1056
+ const webserver = routeCtx.get("webServer");
1057
+ if (webserver === void 0) return;
1058
+ const existing = mounts.get(ctx);
1059
+ if (existing !== void 0 && existing.webserver === webserver) return;
1060
+ if (existing !== void 0) {
1061
+ try {
1062
+ existing.dispose();
1063
+ } catch {
1064
+ }
1065
+ mounts.delete(ctx);
1064
1066
  }
1067
+ const gate = { stamps: [], active: 0, failures: 0, cooldownUntil: 0 };
1068
+ routeCtx.effect(() => {
1069
+ const dispose = webserver.register({
1070
+ kind: "prefix",
1071
+ path: ENHANCE_PREFIX,
1072
+ handler: (req, res) => {
1073
+ const pathname = new URL(req.url ?? "/", "http://x").pathname;
1074
+ return serveEnhance(ctx, readConfig, gate, pathname, req, res, pathname === ENHANCE_STREAM_ENDPOINT);
1075
+ }
1076
+ });
1077
+ const disposed = typeof dispose === "function" ? dispose : () => {
1078
+ };
1079
+ mounts.set(ctx, { webserver, dispose: disposed });
1080
+ return () => {
1081
+ if (mounts.get(ctx)?.dispose === disposed) mounts.delete(ctx);
1082
+ try {
1083
+ disposed();
1084
+ } catch {
1085
+ }
1086
+ };
1087
+ }, "prompt-enhance: enhance route");
1065
1088
  });
1066
- mounts.set(ctx, { webserver, dispose: typeof dispose === "function" ? dispose : () => {
1067
- } });
1068
1089
  }
1069
1090
 
1070
1091
  // src/enhance-command.ts
@@ -1081,15 +1102,15 @@ function registerEnhanceCommand(ctx, readConfig) {
1081
1102
  if (raw === "") {
1082
1103
  return { kind: "error", text: "\u7528\u6CD5\uFF1A/enhance <\u6587\u672C>\u3002\u8981\u589E\u5F3A\u8F93\u5165\u6846\u91CC\u7684\u8349\u7A3F\uFF0C\u8BF7\u7528\u8F93\u5165\u6846\u53F3\u4FA7\u7684\u300C\u589E\u5F3A\u300D\u6309\u94AE\u6216\u5FEB\u6377\u952E\u3002" };
1083
1104
  }
1084
- const config = readConfig();
1085
- if (!config.enabled) {
1086
- return { kind: "error", text: "\u63D0\u793A\u8BCD\u589E\u5F3A\u5DF2\u5728\u63D2\u4EF6\u8BBE\u7F6E\u4E2D\u5173\u95ED\u3002" };
1087
- }
1088
- const check = checkInputText(raw, config.maxInputChars);
1089
- if (!check.ok) {
1090
- return { kind: "error", text: formatInputCheckZh(check) };
1091
- }
1092
1105
  try {
1106
+ const config = readConfig();
1107
+ if (!config.enabled) {
1108
+ return { kind: "error", text: "\u63D0\u793A\u8BCD\u589E\u5F3A\u5DF2\u5728\u63D2\u4EF6\u8BBE\u7F6E\u4E2D\u5173\u95ED\u3002" };
1109
+ }
1110
+ const check = checkInputText(raw, config.maxInputChars);
1111
+ if (!check.ok) {
1112
+ return { kind: "error", text: formatInputCheckZh(check) };
1113
+ }
1093
1114
  const value = await runEnhance(ctx, config, {
1094
1115
  text: raw,
1095
1116
  sessionRoute: sessionRouteOf(ctx, invocation.agent.session.id),
@@ -1107,7 +1128,7 @@ function registerEnhanceCommand(ctx, readConfig) {
1107
1128
 
1108
1129
  // src/index.ts
1109
1130
  var name = "prompt-enhance";
1110
- var inject = ["llm", "webServer"];
1131
+ var inject = ["llm"];
1111
1132
  function apply(ctx, config = { ...DEFAULT_CONFIG }) {
1112
1133
  const source = installSettingsSource(ctx, {
1113
1134
  namespace: PROMPT_ENHANCE_NAMESPACE,