dsh-prompt-enhance 0.1.4 → 0.1.6

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
@@ -28,7 +28,7 @@ Good agent prompts state *who the model should be*, *what to deliver*, *in what
28
28
  | ⚙️ **Live settings** | Every knob (model override, temperature, budgets, system prompt, shortcut) hot-applies from Settings → 插件配置 |
29
29
  | 🛡️ **Draft safety** | Empty, over-length, images-only, and command-chip inputs are rejected locally; upstream failures are mapped to readable messages; the draft is never mutated on failure |
30
30
 
31
- ![The preview panel running in dsh web: original and enhanced prompt side by side with model info, fill-back and copy actions](docs/evidence-prompt-enhance-panel.png)
31
+ ![The preview panel running in dsh web: original and enhanced prompt side by side with model info, fill-back and copy actions](https://raw.githubusercontent.com/rongxingda/dsh-prompt-enhance/main/docs/evidence-prompt-enhance-panel.png)
32
32
 
33
33
  ## How it works
34
34
 
@@ -99,7 +99,7 @@ dsh plugin --profile web remove dsh-prompt-enhance
99
99
  1. Local guards run first: empty drafts, over-length drafts (never auto-truncated — that would change your meaning), images-only drafts, and drafts containing command or file-reference chips are refused with a clear message. Chips are rejected because filling back would destroy them.
100
100
  2. The preview panel opens with a cancellable spinner. Your draft stays untouched — the hint says so.
101
101
  3. The result phase shows both texts side by side. **Apply** fills the enhanced text back and raises the undo bar; **Copy** puts it on the clipboard; **Cancel** (or `Esc`, or clicking the overlay) discards everything.
102
- 4. The undo bar sits above the composer: one click restores the original. If you keep typing after applying, the bar quietly retires itself so stale text can never overwrite newer edits.
102
+ 4. The undo bar sits above the composer: one click restores the original. If you keep typing after applying, the bar quietly retires itself so stale text can never overwrite newer edits. Undo entries live **only in the current page's memory** (up to 3 per session) — a page reload, a web restart, or a session switch clears them.
103
103
 
104
104
  **`/enhance <text>`** — rewrite any text from the slash menu. The result renders in the command plane (copyable) and never enters the conversation history or the model's context. To enhance the *composer draft itself*, use the button or shortcut — the draft lives in the browser. Cancelling a running `/enhance` follows the harness command plane; if the client offers no cancel affordance, the call simply runs to completion or times out.
105
105
 
@@ -117,10 +117,11 @@ Everything lives in the `prompt-enhance` settings namespace, edited from the web
117
117
  | `maxOutputTokens` | `2048` | Output token budget of one enhancement call |
118
118
  | `maxInputChars` | `12000` | Input character cap (counted in Unicode code points — an emoji is one character); over-limit drafts are **rejected, never truncated** |
119
119
  | `timeoutMs` | `60000` | End-to-end deadline of one call |
120
- | `systemPrompt` | built-in strategy | Replace the whole enhancement strategy if you prefer your own |
120
+ | `systemPrompt` | built-in strategy | Custom strategy text; how it combines with the built-in strategy is set by `strategyMode` |
121
+ | `strategyMode` | `replace-default` | How a custom strategy combines with the built-in one: `replace-default` **swaps it out entirely** (backward compatible, but the built-in hard rules — preserve intent, never fabricate, body-only output, language mirroring — are not retained and must be carried into your own text); `extend-default` **appends your text after the built-in strategy**, keeping those rules in force |
121
122
  | `shortcut` | `ctrl+alt+e` | Global shortcut spec (at least one modifier + one alphanumeric/function key — bare keys are ignored so normal typing can never be swallowed); empty disables it |
122
- | `maxConcurrent` | `2` | Host-side concurrency cap; extra requests answer `429` |
123
- | `rateLimitPerMinute` | `10` | Sliding-window rate cap per minute; extra requests answer `429` |
123
+ | `maxConcurrent` | `2` | Concurrency cap **within a single host process**; extra requests answer `429` (`concurrency-limit`). The browser UI admits exactly one in-flight request (a single preview panel), so this cap mainly protects the `/enhance` command plane and multi-client callers |
124
+ | `rateLimitPerMinute` | `10` | Sliding-window rate cap per minute, **within a single host process**, counting **successful calls only** — failures (timeout / upstream error / cancellation) never consume the window, so a run of failures cannot rate-limit you out; extra requests answer `429` (`rate-limit`) with a `Retry-After` in seconds |
124
125
  | `provider` + `model` values | — | Match the harness settings: each key under `llm-pi-ai.providers` (e.g. `zhipu`, `muyuu`) is a provider and each `models[].id` under it (e.g. `glm-5.3-flash`) is a model. Example pair: `provider: zhipu` + `model: glm-5.3-flash` |
125
126
 
126
127
  **Model routing precedence:** explicit settings pair → the route recorded in the current session's request header → the harness-wide default model (`agent-default-model`). If none of them names a route (e.g. a fresh session with no default model), the plugin fails with an actionable message instead of guessing.
@@ -138,7 +139,7 @@ The default system prompt instructs the model to be a prompt-rewriting expert an
138
139
 
139
140
  Hard rules: preserve intent exactly (never remove, alter, or contradict user information); never fabricate — insert an explicit placeholder like `(待补充:…)` / `(TBD: …)` for unknown details; keep the scope unchanged; output **only** the rewritten body (no explanations, fences, or pleasantries); mirror the input language; stay within roughly 1–3× the original length; lightly polish already-well-formed prompts instead of inflating them.
140
141
 
141
- Set `systemPrompt` in the settings to replace all of this with your own strategy.
142
+ Set `systemPrompt` in the settings to use your own strategy; `strategyMode` decides how it combines. The default `replace-default` is a **complete replacement** — the built-in safety constraints (preserve intent, never fabricate, body-only output, mirror the input language, treat the framed draft as data) are not retained automatically, so your own strategy must include them. Switching to `extend-default` appends your text after the built-in strategy instead, keeping those rules in force.
142
143
 
143
144
  ## Error handling & edge cases
144
145
 
@@ -149,6 +150,8 @@ Set `systemPrompt` in the settings to replace all of this with your own strategy
149
150
  | Images attached but no text | Refused: text-only feature |
150
151
  | Command or file-reference chips in the draft | Refused: fill-back would destroy the chips |
151
152
  | Submitting / busy phase or a request already in flight | Refused with "try again in a moment" |
153
+ | Per-minute cap exceeded | `429` (`rate-limit`) with a `Retry-After` in seconds; the host message names the exact wait. The window counts successful calls only — failures do not consume it |
154
+ | Concurrency cap full | `429` (`concurrency-limit`), no `Retry-After` — a slot frees whenever an in-flight call settles |
152
155
  | Upstream model failure | Stable codes mapped to readable hints: `AUTH` → check API key, `RATE_LIMIT` → retry later, `QUOTA_EXCEEDED` → check balance, `CONTEXT_WINDOW_EXCEEDED` → shorten input, `NO_ADAPTER`/unconfigured → configure a model |
153
156
  | Output reaches `maxOutputTokens` | Refused with a hint to raise the cap or shorten the draft |
154
157
  | Model returns empty / fence-wrapped / tool-call output | Normalized (fences stripped) or refused; retryable |
@@ -158,14 +161,49 @@ Set `systemPrompt` in the settings to replace all of this with your own strategy
158
161
 
159
162
  Every failure surfaces inside the plugin's own panel; the composer draft is never modified by a failed call, so manual input continues undisturbed.
160
163
 
164
+ ## Error codes & localization
165
+
166
+ The host route answers structured errors of the shape `{ code, message?, params? }`: the browser renders its localized primary line from `code` + `params` using the current language dictionary, and `message` — when present — is an optional diagnostic detail (the provider's raw failure text, a config error) shown verbatim beneath the primary line. The `/enhance` command plane has no locale dictionary; a host-side renderer of the same errors produces the Chinese text directly.
167
+
168
+ | Error code | HTTP status | Browser primary copy (dictionary key) | Params |
169
+ |---|---|---|---|
170
+ | `rejected` | 403 / 413 / 415 / 422 | generic `error.rejected`; with `{ count, max }` it reuses `error.tooLong` | over-length input: `{ count, max }` |
171
+ | `rate-limit` | 429 | `error.rateLimit` | `{ limit, retryAfterSeconds }` |
172
+ | `concurrency-limit` | 429 | `error.concurrencyLimit` | `{ max }` |
173
+ | `timeout` | 504 | `error.timeout` | `{ seconds }` |
174
+ | `unconfigured` | 409 | `error.unconfigured` | — |
175
+ | `upstream` | 502 | generic `error.upstream`; with `reason` it uses `error.upstream.{reason}` (`auth` / `invalidCredential` / `rateLimit` / `quota` / `empty` / `contextWindow` / `toolCall` / `maxTokens`) | `{ reason }` |
176
+ | `internal` | 500 / 502 | `error.internal` | — |
177
+
178
+ ## Troubleshooting
179
+
180
+ **Button missing / shortcut dead**
181
+ Settings → 插件配置 → `prompt-enhance` section: is `enabled` true? Is the plugin installed (`dsh plugin --profile web list`) and `dsh web` restarted? Any apply error in the browser console?
182
+
183
+ **"No model resolved for the enhancement"**
184
+ Routing follows settings pair → session model → harness default; if all three are empty there is nothing to call. Pair `provider`/`model` in the settings, or send a message in the current session first so it carries a model route. Check the `agent-default-model` settings section.
185
+
186
+ **Authentication failures**
187
+ The `message` detail line carries the concrete cause (e.g. 401). Check the provider's API key in the harness credential store; quota/balance problems surface as the `quota` hint.
188
+
189
+ **Rate-limited (429) right after a run of failures**
190
+ Should not happen — the window counts successes only. If it still does, check whether several profiles/processes are mounted (each counts independently and stacks up) or whether `rateLimitPerMinute` is set too low.
191
+
192
+ **Poor rewrites (fabrication, dropped requirements, broken formatting)**
193
+ With `strategyMode` set to `replace-default`, a custom `systemPrompt` replaces the built-in strategy entirely — its hard rules (never fabricate, body-only output, mirror the input language) are not retained automatically; carry them into your own text or switch to `extend-default`.
194
+
195
+ **Behavior changed after upgrading**
196
+ Since 0.1.6 the host no longer sends Chinese primary-line copy: the browser localizes errors by code (`message` is now only a diagnostic detail), and the `rate-limit` window counts successful calls only. No configuration change needed.
197
+
161
198
  ## Security model
162
199
 
163
200
  The enhance route is served by your own dsh host and reachable **only from this machine**:
164
201
 
165
202
  - **Socket fence** — requests from non-loopback addresses are refused (`127.0.0.1` / `::1` only). Note this means *any local process* can call the route; it carries no user authentication.
166
203
  - **Host allowlist** — the route also validates the `Host` header against `localhost` / `127.0.0.1` / `[::1]`, which defeats DNS-rebinding (a rebound attacker domain keeps the loopback socket address but carries the attacker's hostname and is refused). Responses are `cache-control: no-store`.
167
- - **Abuse caps** — an `Origin` gate refuses browser calls from non-local pages, and the route enforces a concurrency cap (`maxConcurrent`, default 2) and a per-minute rate limit (`rateLimitPerMinute`, default 10), answering `429` beyond either.
204
+ - **Abuse caps** — an `Origin` gate refuses browser calls from non-local pages, and the route enforces a concurrency cap (`maxConcurrent`, default 2) and a per-minute rate limit (`rateLimitPerMinute`, default 10), answering `429` beyond either. Both counters live **in a single host process's memory**: multiple processes, a cluster, or several profiles mounting the plugin each count independently, so the global cap would be exceeded — this plugin supports single-process deployments only and ships no shared rate-limit store.
168
205
  - **Proxy rejection** — requests carrying `X-Forwarded-For` / `Forwarded` headers are refused outright: those headers only exist when a proxy is in the path, which the trust model does not cover.
206
+ - **Request-level timeouts are the host's and Node's job** — the plugin bounds the body size (including a Content-Length fast reject) and the per-call `timeoutMs`; connection-level timeouts (headers / request timeout / keep-alive) are server-level settings on the shared `http.Server` that a prefix route must not touch, so they fall to the host and Node's defaults (60 s headers / 300 s request).
169
207
  - **Not for reverse-proxy exposure** — if you put dsh web behind a proxy that listens on the LAN, external callers appear as loopback to the route and the fence is moot. Do not expose a proxied host without adding your own authentication at the proxy.
170
208
  - **Prompt-injection boundary** — the draft is framed between `<raw_prompt>` tags, literal closing tags inside the draft are neutralized, and the strategy prompt treats the framed text as pure data. This lowers the risk of simple tag-escape; prompt-based boundaries are best-effort, not a guarantee. Enhancements run with your own credentials and the result is only ever shown back to you.
171
209
 
@@ -177,7 +215,7 @@ src/
177
215
  ├── config.ts schemastery schema + resolution (paired route validation)
178
216
  ├── prompts.ts built-in strategy system prompt + <raw_prompt> framing
179
217
  ├── enhancer.ts the ctx.llm auxiliary call (route resolution, deadline
180
- │ racing, finish validation, code → message mapping)
218
+ │ racing, finish validation, structured errors + host render)
181
219
  ├── enhance-routes.ts POST /prompt-enhance/enhance (loopback fence, body cap)
182
220
  ├── enhance-command.ts /enhance slash command (host command registry)
183
221
  ├── loopback.ts 127.0.0.1/::1 fence for the route
@@ -191,7 +229,7 @@ src/
191
229
  ├── UndoBar conversation.input.dock entry: restore affordance
192
230
  ├── ui-state.ts external store shared by components (panel, undo, sessions)
193
231
  ├── enhance-client fetch client with abort + typed errors
194
- ├── undo-stack.ts per-session LIFO (depth 3)
232
+ ├── undo-stack.ts per-session LIFO (depth 3, global cap 60, LRU eviction)
195
233
  ├── shortcut.ts pure combo parsing / matching
196
234
  ├── settings.ts client mirror of the settings namespace
197
235
  ├── locales.ts zh + en dictionaries (harness locale namespace)
package/README.zh-CN.md CHANGED
@@ -28,7 +28,7 @@
28
28
  | ⚙️ **配置热生效** | 模型覆盖、温度、预算、系统提示词、快捷键,全部在 设置 → 插件配置 即改即用 |
29
29
  | 🛡️ **草稿安全** | 空输入、超长、仅图片、含命令块在本地拦截;上游失败映射为可读提示;失败绝不改动草稿 |
30
30
 
31
- ![dsh web 中实机运行的预览面板:原文与增强结果并排,含模型信息与回填/复制操作](docs/evidence-prompt-enhance-panel.png)
31
+ ![dsh web 中实机运行的预览面板:原文与增强结果并排,含模型信息与回填/复制操作](https://raw.githubusercontent.com/rongxingda/dsh-prompt-enhance/main/docs/evidence-prompt-enhance-panel.png)
32
32
 
33
33
  ## 工作原理
34
34
 
@@ -99,7 +99,7 @@ dsh plugin --profile web remove dsh-prompt-enhance
99
99
  1. 先跑本地守卫:空草稿、超长草稿(**不自动截断**——截断会改变原意)、仅图片草稿、含命令或文件引用块的草稿,都会被拒绝并给出明确提示。引用块被拒是因为回填会破坏它们。
100
100
  2. 预览面板打开,显示可取消的加载动画。此时草稿原封不动——面板上写明了这一点。
101
101
  3. 结果阶段左右并排展示两份文本。**回填**把增强文本写回输入框并升起撤销条;**复制**进剪贴板;**取消**(`Esc` 或点击遮罩)丢弃一切。
102
- 4. 撤销条停在输入框上方:一键恢复原文。回填后你继续输入,撤销条会安静地自我退位——过期的原文永远不会覆盖你更新的编辑。
102
+ 4. 撤销条停在输入框上方:一键恢复原文。回填后你继续输入,撤销条会安静地自我退位——过期的原文永远不会覆盖你更新的编辑。撤销记录**只存在于当前页面内存**(每个会话最多 3 条),刷新页面、重启 Web 或切换会话后即失效。
103
103
 
104
104
  **`/enhance <文本>`** —— 从斜杠菜单改写任意文本。结果在命令面板渲染(可复制),不进入会话历史与模型上下文。要增强**输入框里的草稿**请用按钮或快捷键——草稿在浏览器里。正在运行的 `/enhance` 取消跟随 harness 命令面板能力;客户端未提供取消入口时,调用会执行到完成或超时。
105
105
 
@@ -115,12 +115,13 @@ dsh plugin --profile web remove dsh-prompt-enhance
115
115
  | `provider` + `model` | 空 | 显式路由覆盖;必须**成对**填写(或都留空以跟随当前会话模型) |
116
116
  | `temperature` | `0.3` | 低温使改写更忠实于原意 |
117
117
  | `maxOutputTokens` | `2048` | 单次增强调用的输出 token 预算 |
118
- | `maxInputChars` | `12000` | 输入字数上限(按 Unicode 码点统计,一个 emoji 算一个字);超限**拒绝而不截断** |
118
+ | `maxInputChars` | `12000` | 输入字符数上限(按 Unicode 字符统计,不是 token 数;一个 emoji 算一个字符);超限**拒绝而不截断** |
119
119
  | `timeoutMs` | `60000` | 单次调用的端到端超时 |
120
- | `systemPrompt` | 内置策略 | 想用自己的增强策略时整体替换 |
120
+ | `systemPrompt` | 内置策略 | 自定义增强策略文本;与内置策略如何组合见 `strategyMode` |
121
+ | `strategyMode` | `replace-default` | 自定义策略的组合方式:`replace-default` **整体替换**内置策略(向后兼容,但「不改变原意、不编造、只输出正文、镜像输入语言」等硬性约束不会自动保留,需自行写入);`extend-default` 把自定义文本**追加在内置策略之后**,硬性约束继续生效 |
121
122
  | `shortcut` | `ctrl+alt+e` | 快捷键规格(至少一个修饰键 + 单个字母/数字/功能键——纯裸键会被忽略,绝不会吞掉正常打字);留空禁用 |
122
- | `maxConcurrent` | `2` | 宿主侧并发上限;超出的请求返回 `429` |
123
- | `rateLimitPerMinute` | `10` | 每分钟滑动窗口限流;超出的请求返回 `429` |
123
+ | `maxConcurrent` | `2` | **单个 Host 进程内**的并发上限;超出的请求返回 `429`(`concurrency-limit`)。浏览器 UI 因单预览面板天然只允许 1 个在途请求,此上限主要保护 `/enhance` 命令入口与多客户端调用 |
124
+ | `rateLimitPerMinute` | `10` | **单个 Host 进程内**的每分钟滑动窗口限流,只统计**成功完成**的增强——失败(超时 / 上游错误 / 取消)不消耗窗口,连续失败不会把自己限流;超出的请求返回 `429`(`rate-limit`),响应附 `Retry-After` 秒数 |
124
125
  | `provider` + `model` 取值 | — | 与 harness 设置一致:设置文件 `llm-pi-ai.providers` 下的键就是 provider(如 `zhipu`、`muyuu`),其 `models[].id` 就是 model(如 `glm-5.3-flash`)。示例:`provider: zhipu` + `model: glm-5.3-flash` |
125
126
 
126
127
  **模型路由优先级:** 设置成对覆盖 → 当前会话请求头中记录的路由 → 全局默认模型(`agent-default-model`)。三者都指不出路由时(例如全新会话且无默认模型),插件给出可操作的报错而不是乱猜。
@@ -138,17 +139,19 @@ dsh plugin --profile web remove dsh-prompt-enhance
138
139
 
139
140
  硬性规则:精确保留原意(不删除、不篡改、不与用户信息冲突);绝不编造——未知细节插入显式占位符如 `(待补充:…)` / `(TBD: …)`;保持任务范围不变;**只输出**改写后的正文(无解释、无围栏、无客套);镜像输入语言;长度约为原文 1–3 倍;已经成形的提示词只做轻度润色而不注水。
140
141
 
141
- 在设置中填写 `systemPrompt` 即可整体替换为你的策略。
142
+ 在设置中填写 `systemPrompt` 即可自定义策略,组合方式由 `strategyMode` 决定:默认 `replace-default` 是**完全替换**——内置策略中的安全约束(不改变原意、不编造、只输出正文、镜像输入语言、原文视为纯数据)不会自动保留,自定义策略需要自行包含这些约束;改为 `extend-default` 则把你的文本追加在内置策略之后,硬性约束继续生效。
142
143
 
143
144
  ## 异常与边界
144
145
 
145
146
  | 情形 | 行为 |
146
147
  |---|---|
147
148
  | 空 / 纯空白 / 零宽字符草稿 | 本地拒绝:「输入框为空」 |
148
- | 草稿超过 `maxInputChars` | 本地与路由双重拒绝并给出精确字数;**不自动截断** |
149
+ | 草稿超过 `maxInputChars` | 本地与路由双重拒绝并给出精确字符数;**不自动截断** |
149
150
  | 只附加了图片没有文本 | 拒绝:仅支持文本 |
150
151
  | 草稿含命令 / 文件引用块 | 拒绝:回填会破坏引用块 |
151
152
  | 提交中 / 占用阶段 / 已有请求在途 | 拒绝并提示「稍后再试」 |
153
+ | 每分钟次数超限 | `429`(`rate-limit`),响应携带 `Retry-After` 秒数,宿主消息给出精确等待时长;窗口只统计成功完成的调用——失败不消耗窗口 |
154
+ | 并发已满 | `429`(`concurrency-limit`),无 `Retry-After`——空出的时机不可预测,等一个在途调用结束即可 |
152
155
  | 上游模型失败 | 稳定错误码映射为可读提示:`AUTH` → 检查 API Key、`RATE_LIMIT` → 稍后重试、`QUOTA_EXCEEDED` → 检查余额、`CONTEXT_WINDOW_EXCEEDED` → 精简输入、`NO_ADAPTER`/未配置 → 先配置模型 |
153
156
  | 输出达到 `maxOutputTokens` | 拒绝并提示调大上限或精简原文 |
154
157
  | 模型返回空 / 围栏包裹 / 工具调用 | 规范化(剥围栏)或拒绝;可重试 |
@@ -158,14 +161,49 @@ dsh plugin --profile web remove dsh-prompt-enhance
158
161
 
159
162
  所有失败只出现在插件自己的面板里;失败的调用绝不修改输入框草稿,手动输入不受任何干扰。
160
163
 
164
+ ## 错误码与本地化
165
+
166
+ 宿主路由返回的结构化错误形如 `{ code, message?, params? }`:主文案由浏览器按 `code` + `params` 从当前语言字典渲染,`message` 只是可选的诊断细节(如模型服务商的原始报错、配置错误原文),原样展示在主文案下方。`/enhance` 命令平面没有 locale 字典,由宿主侧的同源渲染函数直接产出中文文本。
167
+
168
+ | 错误码 | HTTP 状态 | 浏览器主文案(字典键) | 参数 |
169
+ |---|---|---|---|
170
+ | `rejected` | 403 / 413 / 415 / 422 | `error.rejected` 通用;携带 `{ count, max }` 时复用 `error.tooLong` | 超长输入: `{ count, max }` |
171
+ | `rate-limit` | 429 | `error.rateLimit` | `{ limit, retryAfterSeconds }` |
172
+ | `concurrency-limit` | 429 | `error.concurrencyLimit` | `{ max }` |
173
+ | `timeout` | 504 | `error.timeout` | `{ seconds }` |
174
+ | `unconfigured` | 409 | `error.unconfigured` | — |
175
+ | `upstream` | 502 | `error.upstream` 通用;携带 `reason` 时用 `error.upstream.{reason}`(如 `auth` / `quota` / `rateLimit` / `empty` / `contextWindow` / `toolCall` / `maxTokens` / `invalidCredential`) | `{ reason }` |
176
+ | `internal` | 500 / 502 | `error.internal` | — |
177
+
178
+ ## 故障排查
179
+
180
+ **按钮不出现 / 快捷键无响应**
181
+ 设置 → 插件配置 → `prompt-enhance` 节的 `enabled` 是否为 `true`;插件是否成功安装(`dsh plugin --profile web list`)并重启了 `dsh web`;浏览器控制台是否有插件应用报错。
182
+
183
+ **「尚未确定增强用的模型」**
184
+ 插件遵循设置成对覆盖 → 当前会话模型 → 全局默认模型的路由优先级,三者都为空时无法调用。在设置中成对填写 `provider`/`model`,或先在当前会话发一条消息让会话带上模型路由。检查 `agent-default-model` 设置节是否配置。
185
+
186
+ **「鉴权失败」**
187
+ `message` 细节行会带出具体原因(如 401)。检查对应 provider 在 harness 凭据存储中的 API Key;配额/余额问题对应 `quota` 提示。
188
+
189
+ **连续失败后立刻被限流(429)**
190
+ 不应发生——限流窗口只统计成功调用,失败不消耗窗口。若仍遇到,确认同时挂载了多个 profile / 多进程(各自独立计数会叠加),或 `rateLimitPerMinute` 配置过低。
191
+
192
+ **结果不理想(编造、丢要求、格式乱)**
193
+ `strategyMode` 为 `replace-default` 时自定义 `systemPrompt` 会整体替换内置策略——内置的「不编造、只输出正文、镜像输入语言」等硬性约束不会自动保留,请自行写入;或改用 `extend-default`。
194
+
195
+ **升级后行为变了**
196
+ 0.1.6 起宿主错误不再携带中文主文案,浏览器端按错误码本地化渲染(非中文界面不再混入中文);`rate-limit` 窗口改为只统计成功调用。升级后无需改配置。
197
+
161
198
  ## 安全模型
162
199
 
163
200
  增强路由由你自己的 dsh 宿主提供服务,**仅限本机访问**:
164
201
 
165
202
  - **套接字栅栏** —— 非回环地址(`127.0.0.1` / `::1` 之外)的请求一律拒绝。注意这意味着*本机任意进程*都可以调用该路由;路由本身不带用户鉴权。
166
203
  - **Host 白名单** —— 路由同时校验 `Host` 头是否为 `localhost` / `127.0.0.1` / `[::1]`,可防御 DNS 重绑定(重绑定的攻击者域名虽然落在回环套接字上,但携带的是攻击者主机名,会被拒绝)。响应均为 `cache-control: no-store`。
167
- - **滥用上限** —— `Origin` 门拒绝来自非本地页面的浏览器调用;路由还执行并发上限(`maxConcurrent`,默认 2)与每分钟限流(`rateLimitPerMinute`,默认 10),超限返回 `429`。
204
+ - **滥用上限** —— `Origin` 门拒绝来自非本地页面的浏览器调用;路由还执行并发上限(`maxConcurrent`,默认 2)与每分钟限流(`rateLimitPerMinute`,默认 10),超限返回 `429`。这两项计数保存在**单个宿主进程的内存中**——多进程、cluster 或多个 profile 同时挂载时各自独立计数,全局上限会被突破;本插件只支持单进程部署,也不提供共享限流存储。
168
205
  - **代理请求拒绝** —— 携带 `X-Forwarded-For` / `Forwarded` 头的请求直接拒绝:这些头只会在链路上存在代理时出现,而代理场景不在本路由的信任模型内。
206
+ - **请求级超时由宿主与 Node 兜底** —— 插件层只限制 body 字节数(含 Content-Length 快速拒绝)与单次模型调用的 `timeoutMs`;连接级超时(headers timeout / request timeout / keep-alive)是共享 `http.Server` 的 server 级配置,插件不覆盖,由宿主与 Node 默认值(headers 60 秒 / request 300 秒)兜底。
169
207
  - **不要经反向代理暴露** —— 若把 dsh web 放在监听局域网的代理后面,外部调用者在路由看来就是回环地址,栅栏形同虚设。除非在代理层自行加鉴权,否则不要暴露。
170
208
  - **提示注入边界** —— 草稿被框在 `<raw_prompt>` 标签之间,草稿内的字面闭合标签会被中和,策略提示词把框内文本视为纯数据——降低简单标签逃逸的风险;基于提示词的边界是尽力而为,并非完整防护。增强只使用你自己的凭据,结果也只回显给你本人。
171
209
 
@@ -177,7 +215,7 @@ src/
177
215
  ├── config.ts schemastery 模式 + 解析校验(成对路由)
178
216
  ├── prompts.ts 内置策略系统提示词 + <raw_prompt> 框架
179
217
  ├── enhancer.ts ctx.llm 辅助调用(路由解析、超时竞速、finish 校验、
180
- │ 错误码 → 文案映射)
218
+ │ 结构化错误 code+params + 宿主侧渲染)
181
219
  ├── enhance-routes.ts POST /prompt-enhance/enhance(回环栅栏、限长)
182
220
  ├── enhance-command.ts /enhance 斜杠命令(宿主命令注册表)
183
221
  ├── loopback.ts 路由的 127.0.0.1/::1 栅栏
@@ -190,7 +228,7 @@ src/
190
228
  ├── UndoBar conversation.input.dock 条目:恢复入口
191
229
  ├── ui-state.ts 组件共享的外部 store(面板、撤销、会话注册表)
192
230
  ├── enhance-client fetch 客户端(可中止 + 类型化错误)
193
- ├── undo-stack.ts 按会话的 LIFO 栈(深度 3)
231
+ ├── undo-stack.ts 按会话的 LIFO 栈(每会话深度 3,全局 60 条,LRU 淘汰)
194
232
  ├── shortcut.ts 纯函数的组合键解析 / 匹配
195
233
  ├── settings.ts 设置命名空间的客户端镜像
196
234
  ├── locales.ts 中英文字典(harness locale 命名空间)
@@ -230,7 +268,7 @@ CI 在每次 push/PR 上运行类型检查 + 测试 + 构建,并在 `lib/` 与
230
268
  回填通过 `inputActions.setDraft` 写纯文本,会破坏引用块。先移除它们,增强后再插回。
231
269
 
232
270
  **为什么不自动截断超长输入?**
233
- 截断会无声地改变你的原意——插件宁可拒绝并给出精确字数。
271
+ 截断会无声地改变你的原意——插件宁可拒绝并给出精确字符数。
234
272
 
235
273
  **能固定用某个模型吗?**
236
274
  在设置中**成对**填写 `provider` 与 `model`(例如 harness 已配置的 provider 路由)。都留空则跟随当前会话模型。
package/lib/client.js CHANGED
@@ -39,12 +39,15 @@ var import_react_dom = require("react-dom");
39
39
 
40
40
  // src/shared/validate.ts
41
41
  var INVISIBLE_CHARS = /[\u200B-\u200D\uFEFF\u202A-\u202E\u2066-\u2069]/g;
42
+ function countText(text) {
43
+ return [...text].length;
44
+ }
42
45
  function checkInputText(text, maxChars) {
43
46
  const stripped = text.replace(INVISIBLE_CHARS, "");
44
47
  if (stripped.trim().length === 0) {
45
48
  return { ok: false, code: "empty" };
46
49
  }
47
- const codePoints = [...text].length;
50
+ const codePoints = countText(text);
48
51
  if (codePoints > maxChars) {
49
52
  return { ok: false, code: "too-long", count: codePoints, max: maxChars };
50
53
  }
@@ -61,12 +64,22 @@ var EnhanceClientError = class extends Error {
61
64
  this.detail = detail;
62
65
  }
63
66
  };
64
- var KNOWN_ERROR_CODES = /* @__PURE__ */ new Set(["rejected", "rate", "timeout", "upstream", "unconfigured", "internal"]);
67
+ var KNOWN_ERROR_CODES = /* @__PURE__ */ new Set([
68
+ "rejected",
69
+ "rate-limit",
70
+ "concurrency-limit",
71
+ "timeout",
72
+ "upstream",
73
+ "unconfigured",
74
+ "internal"
75
+ ]);
65
76
  function parseError(value) {
66
77
  const record = value;
67
- if (record !== null && typeof record === "object" && typeof record.message === "string" && record.message !== "") {
78
+ if (record !== null && typeof record === "object") {
68
79
  const code = typeof record.code === "string" && KNOWN_ERROR_CODES.has(record.code) ? record.code : "internal";
69
- return { code, message: record.message };
80
+ const message = typeof record.message === "string" && record.message !== "" ? record.message : void 0;
81
+ const params = record.params !== null && typeof record.params === "object" ? record.params : void 0;
82
+ return { code, ...message !== void 0 ? { message } : {}, ...params !== void 0 ? { params } : {} };
70
83
  }
71
84
  return { code: "internal", message: "\u5BBF\u4E3B\u670D\u52A1\u8FD4\u56DE\u5F02\u5E38\u3002" };
72
85
  }
@@ -114,16 +127,117 @@ async function requestEnhance(body, signal) {
114
127
 
115
128
  // src/client/ResultPanel.tsx
116
129
  var import_react = require("react");
130
+
131
+ // src/client/locales.ts
132
+ var zh = {
133
+ "button.title": "\u63D0\u793A\u8BCD\u589E\u5F3A\uFF08\u91CD\u5199\u4E3A\u7ED3\u6784\u5316\u63D0\u793A\u8BCD\uFF09",
134
+ "button.busy": "\u6B63\u5728\u589E\u5F3A\u2026",
135
+ "panel.title": "\u63D0\u793A\u8BCD\u589E\u5F3A",
136
+ "panel.loading": "\u6B63\u5728\u589E\u5F3A\uFF0C\u7A0D\u5019\u2026",
137
+ "panel.loading.hint": "\u539F\u6587\u4FDD\u7559\u5728\u8F93\u5165\u6846\u4E2D\uFF0C\u4E0D\u4F1A\u88AB\u52A8\u4FEE\u6539\u3002",
138
+ "panel.cancel": "\u53D6\u6D88",
139
+ "panel.original": "\u539F\u59CB\u63D0\u793A\u8BCD",
140
+ "panel.enhanced": "\u589E\u5F3A\u7ED3\u679C",
141
+ "panel.apply": "\u56DE\u586B\u5230\u8F93\u5165\u6846",
142
+ "panel.copy": "\u590D\u5236\u7ED3\u679C",
143
+ "panel.copied": "\u5DF2\u590D\u5236",
144
+ "panel.copyFailed": "\u590D\u5236\u5931\u8D25",
145
+ "panel.close": "\u5173\u95ED",
146
+ "panel.retry": "\u91CD\u8BD5",
147
+ "panel.stale.warn": "\u8349\u7A3F\u5728\u589E\u5F3A\u671F\u95F4\u6709\u6539\u52A8\u2014\u2014\u589E\u5F3A\u7ED3\u679C\u57FA\u4E8E\u589E\u5F3A\u524D\u7684\u6587\u672C\u3002\u56DE\u586B\u5C06\u8986\u76D6\u4F60\u7684\u6700\u65B0\u7F16\u8F91(\u64A4\u9500\u53EF\u6062\u590D\u56DE\u586B\u524D\u7684\u8349\u7A3F)\u3002",
148
+ "panel.model": "\u6A21\u578B\uFF1A{provider} / {model}",
149
+ "panel.elapsed": "\u8017\u65F6 {ms}",
150
+ "undo.applied": "\u5DF2\u7528\u589E\u5F3A\u7ED3\u679C\u66FF\u6362\u539F\u63D0\u793A\u8BCD",
151
+ "undo.undo": "\u64A4\u9500",
152
+ "undo.dismiss": "\u5173\u95ED\u63D0\u793A",
153
+ "error.empty": "\u8F93\u5165\u6846\u4E3A\u7A7A\uFF0C\u8BF7\u5148\u8F93\u5165\u8981\u589E\u5F3A\u7684\u63D0\u793A\u8BCD\u3002",
154
+ "error.tooLong": "\u5185\u5BB9\u5171 {count} \u4E2A\u5B57\u7B26\uFF0C\u8D85\u8FC7 {max} \u4E2A\u5B57\u7B26\u4E0A\u9650\uFF08\u6309 Unicode \u5B57\u7B26\u6570\u7EDF\u8BA1\uFF0C\u4E0D\u662F token \u6570\uFF09\u3002\u4E3A\u907F\u514D\u6539\u53D8\u539F\u610F\u4E0D\u4F1A\u81EA\u52A8\u622A\u65AD\uFF0C\u8BF7\u7CBE\u7B80\u540E\u518D\u8BD5\u3002",
155
+ "error.imagesOnly": "\u5F53\u524D\u53EA\u9644\u52A0\u4E86\u56FE\u7247\uFF0C\u4EC5\u652F\u6301\u589E\u5F3A\u6587\u672C\u5185\u5BB9\u3002",
156
+ "error.occurrences": "\u8F93\u5165\u5185\u5BB9\u5305\u542B\u547D\u4EE4\u6216\u6587\u4EF6\u5F15\u7528\uFF0C\u6682\u4E0D\u652F\u6301\u589E\u5F3A\uFF1B\u8BF7\u5148\u79FB\u9664\u540E\u518D\u8BD5\u3002",
157
+ "error.phase": "\u5F53\u524D\u8F93\u5165\u6B63\u88AB\u5360\u7528\uFF08\u63D0\u4EA4\u4E2D\uFF09\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002",
158
+ "error.disabled": "\u63D0\u793A\u8BCD\u589E\u5F3A\u5DF2\u5728\u63D2\u4EF6\u8BBE\u7F6E\u4E2D\u5173\u95ED\u3002",
159
+ "error.rejected": "\u589E\u5F3A\u8BF7\u6C42\u88AB\u62D2\u7EDD\u3002",
160
+ "error.timeout": "\u589E\u5F3A\u8D85\u65F6\uFF08{seconds} \u79D2\uFF09\uFF0C\u8BF7\u91CD\u8BD5\uFF1B\u539F\u8F93\u5165\u672A\u6539\u52A8\u3002",
161
+ "error.unconfigured": "\u5C1A\u672A\u786E\u5B9A\u589E\u5F3A\u7528\u7684\u6A21\u578B\uFF1A\u8BF7\u5728\u63D2\u4EF6\u8BBE\u7F6E\u4E2D\u6210\u5BF9\u586B\u5199 provider/model\uFF0C\u6216\u5148\u5728\u5F53\u524D\u4F1A\u8BDD\u53D1\u9001\u4E00\u6761\u6D88\u606F\uFF08\u5C06\u8DDF\u968F\u4F1A\u8BDD\u6A21\u578B\uFF09\u3002",
162
+ "error.upstream": "\u6A21\u578B\u670D\u52A1\u8FD4\u56DE\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5\uFF1B\u539F\u8F93\u5165\u672A\u6539\u52A8\u3002",
163
+ "error.internal": "\u589E\u5F3A\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\uFF1B\u539F\u8F93\u5165\u672A\u6539\u52A8\u3002",
164
+ "error.rateLimit": "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF1A\u6BCF\u5206\u949F\u6700\u591A {limit} \u6B21\u589E\u5F3A\uFF0C\u8BF7\u5728 {retryAfterSeconds} \u79D2\u540E\u518D\u8BD5\u3002",
165
+ "error.concurrencyLimit": "\u5DF2\u6709 {max} \u4E2A\u589E\u5F3A\u6B63\u5728\u8FDB\u884C\uFF0C\u8BF7\u7B49\u5176\u4E2D\u4E00\u4E2A\u5B8C\u6210\u540E\u518D\u8BD5\u3002",
166
+ "error.upstream.auth": "\u9274\u6743\u5931\u8D25\uFF1A\u8BF7\u68C0\u67E5\u8BE5 provider \u7684 API Key \u914D\u7F6E\u3002",
167
+ "error.upstream.invalidCredential": "\u9274\u6743\u5931\u8D25\uFF1A\u5B58\u50A8\u7684 API Key \u4E0D\u53EF\u7528\uFF0C\u8BF7\u4FEE\u6B63\u540E\u91CD\u8BD5\u3002",
168
+ "error.upstream.rateLimit": "\u6A21\u578B\u670D\u52A1\u9650\u6D41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
169
+ "error.upstream.quota": "\u6A21\u578B\u670D\u52A1\u914D\u989D/\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u68C0\u67E5\u8D26\u6237\u3002",
170
+ "error.upstream.empty": "\u6A21\u578B\u8FD4\u56DE\u4E86\u7A7A\u54CD\u5E94\uFF0C\u8BF7\u91CD\u8BD5\u3002",
171
+ "error.upstream.contextWindow": "\u8F93\u5165\u8D85\u51FA\u6A21\u578B\u4E0A\u4E0B\u6587\u7A97\u53E3\uFF0C\u8BF7\u7CBE\u7B80\u539F\u6587\u6216\u66F4\u6362\u6A21\u578B\u3002",
172
+ "error.upstream.toolCall": "\u6A21\u578B\u8BF7\u6C42\u4E86\u5DE5\u5177\u8C03\u7528\uFF0C\u63D0\u793A\u8BCD\u589E\u5F3A\u53EA\u9700\u8981\u7EAF\u6587\u672C\uFF1B\u8BF7\u66F4\u6362\u6A21\u578B\u540E\u91CD\u8BD5\u3002",
173
+ "error.upstream.maxTokens": "\u91CD\u5199\u7ED3\u679C\u8FBE\u5230\u8F93\u51FA\u4E0A\u9650\uFF08maxOutputTokens\uFF09\uFF0C\u8BF7\u5728\u8BBE\u7F6E\u4E2D\u8C03\u5927\u4E0A\u9650\u6216\u7CBE\u7B80\u539F\u6587\u540E\u91CD\u8BD5\u3002"
174
+ };
175
+ var en = {
176
+ "button.title": "Enhance prompt (rewrite into a structured prompt)",
177
+ "button.busy": "Enhancing\u2026",
178
+ "panel.title": "Prompt Enhance",
179
+ "panel.loading": "Enhancing, please wait\u2026",
180
+ "panel.loading.hint": "Your draft stays untouched until you apply the result.",
181
+ "panel.cancel": "Cancel",
182
+ "panel.original": "Original prompt",
183
+ "panel.enhanced": "Enhanced result",
184
+ "panel.apply": "Fill into input box",
185
+ "panel.copy": "Copy result",
186
+ "panel.copied": "Copied",
187
+ "panel.copyFailed": "Copy failed",
188
+ "panel.close": "Close",
189
+ "panel.retry": "Retry",
190
+ "panel.stale.warn": "The draft changed while enhancing \u2014 the result is based on the pre-enhance text. Applying will overwrite your latest edits (undo restores the draft as it was before applying).",
191
+ "panel.model": "Model: {provider} / {model}",
192
+ "panel.elapsed": "{ms} elapsed",
193
+ "undo.applied": "Original prompt replaced by the enhanced version",
194
+ "undo.undo": "Undo",
195
+ "undo.dismiss": "Dismiss",
196
+ "error.empty": "The input box is empty \u2014 type a prompt to enhance first.",
197
+ "error.tooLong": "The draft is {count} characters, above the {max} cap (Unicode characters, not tokens). It is never auto-truncated (that would change your meaning) \u2014 please shorten it.",
198
+ "error.imagesOnly": "Only images are attached; prompt enhance supports text only.",
199
+ "error.occurrences": "The draft contains commands or file references, which are not supported yet \u2014 remove them first.",
200
+ "error.phase": "The input box is busy (submitting) \u2014 try again in a moment.",
201
+ "error.disabled": "Prompt enhance is disabled in the plugin settings.",
202
+ "error.rejected": "The enhance request was rejected.",
203
+ "error.timeout": "The enhancement timed out ({seconds}s). Retry; your draft is untouched.",
204
+ "error.unconfigured": "No model resolved for the enhancement: pair provider/model in the plugin settings, or send a message in the current session first (the enhancement will follow the session model).",
205
+ "error.upstream": "The model provider returned an error. Retry; your draft is untouched.",
206
+ "error.internal": "Enhancement failed. Retry; your draft is untouched.",
207
+ "error.rateLimit": "Too many requests \u2014 at most {limit} enhancements per minute; retry in {retryAfterSeconds} seconds.",
208
+ "error.concurrencyLimit": "{max} enhancements are already running \u2014 wait for one to finish, then retry.",
209
+ "error.upstream.auth": "Authentication failed \u2014 check the API key configured for this provider.",
210
+ "error.upstream.invalidCredential": "Authentication failed \u2014 the stored API key is invalid; fix it and retry.",
211
+ "error.upstream.rateLimit": "The model provider is rate-limiting; retry later.",
212
+ "error.upstream.quota": "The model provider reports a quota/balance issue \u2014 check your account.",
213
+ "error.upstream.empty": "The model returned an empty response; retry.",
214
+ "error.upstream.contextWindow": "The input exceeds the model context window \u2014 shorten it or switch models.",
215
+ "error.upstream.toolCall": "The model requested tool calls, but prompt enhance needs plain text \u2014 switch models and retry.",
216
+ "error.upstream.maxTokens": "The rewrite hit the output cap (maxOutputTokens) \u2014 raise it in settings or shorten the draft."
217
+ };
218
+ var dictionaries = { zh, en };
219
+ var NS = "prompt-enhance";
220
+
221
+ // src/client/ResultPanel.tsx
117
222
  var import_jsx_runtime = require("react/jsx-runtime");
118
223
  var retryable = (code) => code === "upstream" || code === "timeout" || code === "internal";
119
- function localizedErrorMessage(t, code) {
224
+ function localizedErrorMessage(t, code, params) {
225
+ if (code === "rejected" && typeof params?.count === "number" && typeof params?.max === "number") {
226
+ return t("error.tooLong", { count: params.count, max: params.max });
227
+ }
228
+ if (code === "upstream" && typeof params?.reason === "string") {
229
+ const specific = `error.upstream.${params.reason}`;
230
+ if (specific in zh) return t(specific);
231
+ }
120
232
  switch (code) {
121
233
  case "rejected":
122
234
  return t("error.rejected");
123
- case "rate":
124
- return t("error.rate");
235
+ case "rate-limit":
236
+ return t("error.rateLimit", params);
237
+ case "concurrency-limit":
238
+ return t("error.concurrencyLimit", params);
125
239
  case "timeout":
126
- return t("error.timeout");
240
+ return t("error.timeout", params);
127
241
  case "unconfigured":
128
242
  return t("error.unconfigured");
129
243
  case "upstream":
@@ -223,10 +337,13 @@ function ResultPanel(props) {
223
337
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: t("panel.loading") }),
224
338
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pe-hint", children: t("panel.loading.hint") })
225
339
  ] }),
226
- state.phase === "error" && state.error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-pe-error", children: [
227
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: state.error.localized ?? localizedErrorMessage(t, state.error.code) }),
228
- state.error.localized !== state.error.message && state.error.message !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pe-error-detail", children: state.error.message })
229
- ] }),
340
+ state.phase === "error" && state.error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pe-error", children: state.error.localized !== void 0 ? (
341
+ // Client-side guard failures carry their own pre-localized copy.
342
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: state.error.localized })
343
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
344
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: localizedErrorMessage(t, state.error.code, state.error.params) }),
345
+ state.error.message !== void 0 && state.error.message !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pe-error-detail", children: state.error.message })
346
+ ] }) }),
230
347
  state.phase === "result" && state.result !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-pe-body", children: [
231
348
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-pe-col", children: [
232
349
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pe-col-title", children: t("panel.original") }),
@@ -256,14 +373,25 @@ function ResultPanel(props) {
256
373
  }
257
374
 
258
375
  // src/client/undo-stack.ts
259
- function createUndoStack(maxDepth = 3) {
376
+ function totalOf(stacks) {
377
+ let n = 0;
378
+ for (const stack of stacks.values()) n += stack.length;
379
+ return n;
380
+ }
381
+ function createUndoStack(maxDepth = 3, maxTotalEntries = 60) {
260
382
  const stacks = /* @__PURE__ */ new Map();
261
383
  return {
262
384
  push(sessionId, entry) {
263
385
  const stack = stacks.get(sessionId) ?? [];
264
386
  stack.push(entry);
265
387
  while (stack.length > maxDepth) stack.shift();
388
+ stacks.delete(sessionId);
266
389
  stacks.set(sessionId, stack);
390
+ while (totalOf(stacks) > maxTotalEntries) {
391
+ const oldest = stacks.keys().next().value;
392
+ if (oldest === void 0) break;
393
+ stacks.delete(oldest);
394
+ }
267
395
  },
268
396
  peek(sessionId) {
269
397
  const stack = stacks.get(sessionId);
@@ -543,78 +671,6 @@ function UndoBar(props) {
543
671
  ] });
544
672
  }
545
673
 
546
- // src/client/locales.ts
547
- var zh = {
548
- "button.title": "\u63D0\u793A\u8BCD\u589E\u5F3A\uFF08\u91CD\u5199\u4E3A\u7ED3\u6784\u5316\u63D0\u793A\u8BCD\uFF09",
549
- "button.busy": "\u6B63\u5728\u589E\u5F3A\u2026",
550
- "panel.title": "\u63D0\u793A\u8BCD\u589E\u5F3A",
551
- "panel.loading": "\u6B63\u5728\u589E\u5F3A\uFF0C\u7A0D\u5019\u2026",
552
- "panel.loading.hint": "\u539F\u6587\u4FDD\u7559\u5728\u8F93\u5165\u6846\u4E2D\uFF0C\u4E0D\u4F1A\u88AB\u52A8\u4FEE\u6539\u3002",
553
- "panel.cancel": "\u53D6\u6D88",
554
- "panel.original": "\u539F\u59CB\u63D0\u793A\u8BCD",
555
- "panel.enhanced": "\u589E\u5F3A\u7ED3\u679C",
556
- "panel.apply": "\u56DE\u586B\u5230\u8F93\u5165\u6846",
557
- "panel.copy": "\u590D\u5236\u7ED3\u679C",
558
- "panel.copied": "\u5DF2\u590D\u5236",
559
- "panel.copyFailed": "\u590D\u5236\u5931\u8D25",
560
- "panel.close": "\u5173\u95ED",
561
- "panel.retry": "\u91CD\u8BD5",
562
- "panel.stale.warn": "\u8349\u7A3F\u5728\u589E\u5F3A\u671F\u95F4\u6709\u6539\u52A8\u2014\u2014\u589E\u5F3A\u7ED3\u679C\u57FA\u4E8E\u589E\u5F3A\u524D\u7684\u6587\u672C\u3002\u56DE\u586B\u5C06\u8986\u76D6\u4F60\u7684\u6700\u65B0\u7F16\u8F91(\u64A4\u9500\u53EF\u6062\u590D\u56DE\u586B\u524D\u7684\u8349\u7A3F)\u3002",
563
- "panel.model": "\u6A21\u578B\uFF1A{provider} / {model}",
564
- "panel.elapsed": "\u8017\u65F6 {ms}",
565
- "undo.applied": "\u5DF2\u7528\u589E\u5F3A\u7ED3\u679C\u66FF\u6362\u539F\u63D0\u793A\u8BCD",
566
- "undo.undo": "\u64A4\u9500",
567
- "undo.dismiss": "\u5173\u95ED\u63D0\u793A",
568
- "error.empty": "\u8F93\u5165\u6846\u4E3A\u7A7A\uFF0C\u8BF7\u5148\u8F93\u5165\u8981\u589E\u5F3A\u7684\u63D0\u793A\u8BCD\u3002",
569
- "error.tooLong": "\u5185\u5BB9\u5171 {count} \u5B57\uFF0C\u8D85\u8FC7 {max} \u5B57\u4E0A\u9650\u3002\u4E3A\u907F\u514D\u6539\u53D8\u539F\u610F\u4E0D\u4F1A\u81EA\u52A8\u622A\u65AD\uFF0C\u8BF7\u7CBE\u7B80\u540E\u518D\u8BD5\u3002",
570
- "error.imagesOnly": "\u5F53\u524D\u53EA\u9644\u52A0\u4E86\u56FE\u7247\uFF0C\u4EC5\u652F\u6301\u589E\u5F3A\u6587\u672C\u5185\u5BB9\u3002",
571
- "error.occurrences": "\u8F93\u5165\u5185\u5BB9\u5305\u542B\u547D\u4EE4\u6216\u6587\u4EF6\u5F15\u7528\uFF0C\u6682\u4E0D\u652F\u6301\u589E\u5F3A\uFF1B\u8BF7\u5148\u79FB\u9664\u540E\u518D\u8BD5\u3002",
572
- "error.phase": "\u5F53\u524D\u8F93\u5165\u6B63\u88AB\u5360\u7528\uFF08\u63D0\u4EA4\u4E2D\uFF09\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002",
573
- "error.disabled": "\u63D0\u793A\u8BCD\u589E\u5F3A\u5DF2\u5728\u63D2\u4EF6\u8BBE\u7F6E\u4E2D\u5173\u95ED\u3002",
574
- "error.rejected": "\u589E\u5F3A\u8BF7\u6C42\u88AB\u62D2\u7EDD\u3002",
575
- "error.timeout": "\u589E\u5F3A\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5\uFF1B\u539F\u8F93\u5165\u672A\u6539\u52A8\u3002",
576
- "error.unconfigured": "\u5C1A\u672A\u786E\u5B9A\u589E\u5F3A\u7528\u7684\u6A21\u578B\uFF1A\u8BF7\u5728\u63D2\u4EF6\u8BBE\u7F6E\u4E2D\u6210\u5BF9\u586B\u5199 provider/model\u3002",
577
- "error.upstream": "\u6A21\u578B\u670D\u52A1\u8FD4\u56DE\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5\uFF1B\u539F\u8F93\u5165\u672A\u6539\u52A8\u3002",
578
- "error.internal": "\u589E\u5F3A\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\uFF1B\u539F\u8F93\u5165\u672A\u6539\u52A8\u3002",
579
- "error.rate": "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\u6216\u5E76\u53D1\u5DF2\u6EE1\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002"
580
- };
581
- var en = {
582
- "button.title": "Enhance prompt (rewrite into a structured prompt)",
583
- "button.busy": "Enhancing\u2026",
584
- "panel.title": "Prompt Enhance",
585
- "panel.loading": "Enhancing, please wait\u2026",
586
- "panel.loading.hint": "Your draft stays untouched until you apply the result.",
587
- "panel.cancel": "Cancel",
588
- "panel.original": "Original prompt",
589
- "panel.enhanced": "Enhanced result",
590
- "panel.apply": "Fill into input box",
591
- "panel.copy": "Copy result",
592
- "panel.copied": "Copied",
593
- "panel.copyFailed": "Copy failed",
594
- "panel.close": "Close",
595
- "panel.retry": "Retry",
596
- "panel.stale.warn": "The draft changed while enhancing \u2014 the result is based on the pre-enhance text. Applying will overwrite your latest edits (undo restores the draft as it was before applying).",
597
- "panel.model": "Model: {provider} / {model}",
598
- "panel.elapsed": "{ms} elapsed",
599
- "undo.applied": "Original prompt replaced by the enhanced version",
600
- "undo.undo": "Undo",
601
- "undo.dismiss": "Dismiss",
602
- "error.empty": "The input box is empty \u2014 type a prompt to enhance first.",
603
- "error.tooLong": "The draft is {count} characters, above the {max} cap. It is never auto-truncated (that would change your meaning) \u2014 please shorten it.",
604
- "error.imagesOnly": "Only images are attached; prompt enhance supports text only.",
605
- "error.occurrences": "The draft contains commands or file references, which are not supported yet \u2014 remove them first.",
606
- "error.phase": "The input box is busy (submitting) \u2014 try again in a moment.",
607
- "error.disabled": "Prompt enhance is disabled in the plugin settings.",
608
- "error.rejected": "The enhance request was rejected.",
609
- "error.timeout": "The enhancement timed out. Retry; your draft is untouched.",
610
- "error.unconfigured": "No model resolved for the enhancement: pair provider/model in the plugin settings.",
611
- "error.upstream": "The model provider returned an error. Retry; your draft is untouched.",
612
- "error.internal": "Enhancement failed. Retry; your draft is untouched.",
613
- "error.rate": "Too many requests or the concurrency cap is full \u2014 retry in a moment."
614
- };
615
- var dictionaries = { zh, en };
616
- var NS = "prompt-enhance";
617
-
618
674
  // src/client/styles.ts
619
675
  var STYLE_ID = "dsh-prompt-enhance-styles";
620
676
  var CSS = `