dsh-prompt-rewriter 1.0.2 → 1.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/README.md CHANGED
@@ -1,181 +1,189 @@
1
1
  # dsh-prompt-rewriter
2
2
 
3
+ English | [中文](README.zh.md)
4
+
3
5
  [![npm](https://img.shields.io/npm/v/dsh-prompt-rewriter.svg)](https://www.npmjs.com/package/dsh-prompt-rewriter)
4
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
7
 
6
- 一键把聊天输入框里的草稿改写成更清晰、更完整的提示词。**原位替换,不发送消息、不运行 Agent。**
8
+ Turns the half-formed draft in your composer into a task instruction you can actually run.
9
+ **It rewrites the box, it never sends.** Don't like it? `Ctrl+Z` and you're back.
7
10
 
8
- 一个非官方社区项目,与 DeepSeek 无隶属或授权关系。素材归属见 [NOTICE.md](NOTICE.md)。
11
+ > An unofficial community project, not affiliated with or endorsed by DeepSeek. Artwork attribution is in [NOTICE.md](NOTICE.md).
9
12
 
10
13
  ---
11
14
 
12
- ## 它解决什么问题
15
+ ![One click in the composer turns a draft into a complete prompt](https://raw.githubusercontent.com/huaguoxingkong/dsh-prompt-rewriter/main/docs/images/01-hero-en.gif)
13
16
 
14
- 一句话的草稿往往缺目标、缺约束、缺输出格式,模型只能猜。这个插件把草稿改写成可执行的提示词,再放回输入框——你可以先看一眼、改两笔,再决定发不发。
17
+ ## What it fills in for you
15
18
 
16
- **按钮长在输入框里,不用另开页面。** 位置在输入框右下角、模型名左侧(图中蓝框那一个),输入框有内容时出现,为空时自动隐藏。
19
+ A one-line draft is missing **goal, format, and boundaries**. The model can only guess — guess wrong and you spend another round rewording. This button puts those three things back into the sentence you already wrote:
17
20
 
18
- ![按钮就在输入框里,紧挨模型名](https://raw.githubusercontent.com/huaguoxingkong/dsh-prompt-rewriter/main/docs/images/01-composer-button.png)
21
+ | | What you typed | What goes back in the box |
22
+ |---|---|---|
23
+ | You wrote | *help me write a scraper for Douban movies* | the same sentence |
24
+ | Goal added | | scrape **Top 250** titles, **ratings and links** |
25
+ | Format added | | output a **CSV file** |
26
+ | Boundaries added | | respect robots.txt, **wait at least 2s between requests**, back off on rate limits **instead of circumventing them** |
19
27
 
20
- ## 功能
28
+ The third column is not something you wrote. It read your sentence and filled it in. **And it stays editable** — it lands in the composer, not somewhere else.
21
29
 
22
- - **一键改写**:输入框有内容时出现按钮,无内容时隐藏。
23
- - **原位替换**:结果直接写回输入框,是一个可撤销的编辑步,`Ctrl+Z` 可回退。
24
- - **随时可停**:改写中再点一下即取消,草稿保持原样,不报错。
25
- - **失败可见**:出错时按钮变成重试图标,原因直接显示在按钮旁。
26
- - **上下文感知**:会话已有对话时参考前文的目标、已确认要求与已排除方案;空会话只处理当前草稿。
27
- - **参考输入框图片**:可选让输入框里的图片一起参与改写;历史消息里的图片**始终不读**。
30
+ ## How to use it
28
31
 
29
- ### 改写前后
32
+ 1. Type any draft in the composer — shorter is fine.
33
+ 2. Click the **whale-tail icon just left of the model name**. It isn't there when the box is empty.
34
+ 3. A second later the draft is replaced with the rewritten prompt.
35
+ 4. Not what you wanted? `Ctrl+Z`, or click the undo arrow the button turned into.
30
36
 
31
- ![改写前后对比](https://raw.githubusercontent.com/huaguoxingkong/dsh-prompt-rewriter/main/docs/images/02-before-after.png)
37
+ Changed your mind mid-flight? Click again to cancel. The draft stays exactly as it was — no half-finished text left behind.
32
38
 
33
- ### 两种模式
39
+ ### You can navigate away mid-rewrite
34
40
 
35
- 空会话只处理当前草稿,图标无标记;会话已有对话时参考前文,图标右下角出现小圆点。
41
+ Once you've clicked, you don't have to sit and wait. **Switch to another session and the task keeps running.**
36
42
 
37
- ![两种模式的区别](https://raw.githubusercontent.com/huaguoxingkong/dsh-prompt-rewriter/main/docs/images/04-context-modes.png)
43
+ Come back and the result is already in the composer; if it hasn't finished, the button is still the spinning tail. Two sessions can each run their own rewrite without interfering.
38
44
 
39
- ### 按钮状态
45
+ That wasn't true in 1.0.x: switching sessions aborted the task, and coming back showed nothing.
40
46
 
41
- ![按钮状态](https://raw.githubusercontent.com/huaguoxingkong/dsh-prompt-rewriter/main/docs/images/05-button-states.png)
47
+ ### The four button states
42
48
 
43
- ## 安装
49
+ ![Button states: idle, rewriting, done and undoable, failed and retryable](https://raw.githubusercontent.com/huaguoxingkong/dsh-prompt-rewriter/main/docs/images/05-button-states-en.gif)
44
50
 
45
- 需要 DeepSeek Harness 桌面版,`node >= 22`。
51
+ | State | What you see | What it means |
52
+ |---|---|---|
53
+ | Idle | static whale tail | ready to click |
54
+ | Rewriting | the tail spins into a spiral | running; click again to cancel |
55
+ | Done | turns into an undo arrow | one click back, or `Ctrl+Z` |
56
+ | Failed | red retry icon + reason | the reason sits right next to it — no log hunting |
46
57
 
47
- ### 从 npm 安装(推荐)
58
+ ## Two things worth not confusing
48
59
 
49
- ```sh
50
- dsh plugin --profile desktop add dsh-prompt-rewriter
51
- ```
60
+ ### 1. Whether it reads the conversation
52
61
 
53
- ### 从 GitHub 源码安装
62
+ ![Empty session vs. existing conversation: the small dot in the corner is the marker](https://raw.githubusercontent.com/huaguoxingkong/dsh-prompt-rewriter/main/docs/images/04-context-modes-en.png)
54
63
 
55
- ```sh
56
- dsh plugin --profile desktop add github:huaguoxingkong/dsh-prompt-rewriter
57
- ```
64
+ The **small dot in the button's lower-right corner** is the whole difference:
58
65
 
59
- 源码安装会拉取未构建的仓库,首次安装可能提示允许运行该包的安装脚本——只对你信任的包授予此权限。
66
+ - **Empty session** (no dot): sees only your current draft.
67
+ - **Existing conversation** (dot): also reads earlier turns, so it knows what you already confirmed and already ruled out. The rewrite lands closer to what you actually want this time.
60
68
 
61
- ### 从 Release 安装
69
+ Conversation context costs tokens. If long chats feel slow or expensive, turn off *Reference the conversation* in settings and the dot disappears.
62
70
 
63
- 到 [Releases](https://github.com/huaguoxingkong/dsh-prompt-rewriter/releases) 下载 `dsh-prompt-rewriter-1.0.0.zip`,解压后:
71
+ ### 2. Whether it reads images
64
72
 
65
- ```sh
66
- cd dsh-prompt-rewriter-1.0.0
67
- npm install
68
- dsh plugin --profile desktop add .
69
- ```
73
+ Only images **currently sitting in the composer** — **never images from earlier messages**. That line is deliberate: old pasted screenshots are slow to process and easy to misread, so they're out of scope.
70
74
 
71
- ### 本地 checkout 安装
75
+ ## Settings
72
76
 
73
- ```sh
74
- git clone https://github.com/huaguoxingkong/dsh-prompt-rewriter.git
75
- cd dsh-prompt-rewriter
76
- npm install
77
- dsh plugin --profile desktop add .
78
- ```
77
+ `Settings → Prompt rewriter`. Three switches and one dropdown, affecting **only this button, never normal sending**.
79
78
 
80
- `npm install` 是必需的:插件用 `@deepseek-ai/schemastery` 声明设置页的 `Config`,它是唯一外部依赖。
79
+ ![Settings page](https://raw.githubusercontent.com/huaguoxingkong/dsh-prompt-rewriter/main/docs/images/03-settings-en.png)
81
80
 
82
- ### 安装后
81
+ | Option | Default | What turning it on does |
82
+ |---|---|---|
83
+ | Use a fixed model | Off | Off: follow the model of the current conversation. On: always use the model picked below. |
84
+ | Reference the conversation | **On** | Read earlier turns. More accurate, but extra tokens and slower on long chats. |
85
+ | Reference composer images | Off | Images in the composer take part. Earlier messages' images are never read. |
83
86
 
84
- **重启 DeepSeek Harness。** 本插件没有 HMR,bundle 在启动时读取快照,不重启不会生效。
87
+ - The model dropdown lists **the providers and models you actually have** — you can't type in one that doesn't exist.
88
+ - Turning the model switch off does not clear your selection; flipping it back on shows the same choice.
89
+ - Images still uploading are skipped, and the status area tells you how many were missed. A rewrite never fails because of an image.
85
90
 
86
- ## 使用
91
+ ## Install
87
92
 
88
- 1. 在输入框里写下任意草稿。
89
- 2. 点模型名左侧的增强按钮(大肥鱼图标)。
90
- 3. 等待改写完成,草稿被替换成新提示词。
91
- 4. 不满意可按 `Ctrl+Z` 或点回退图标还原。
93
+ DeepSeek Harness desktop, `node >= 22`. **Restart Harness after installing** — this plugin has no HMR.
92
94
 
93
- 改写用的是**当前会话选中的模型**,除非你在设置里固定了别的模型。
95
+ ### From npm (recommended)
94
96
 
95
- ## 设置
97
+ ```sh
98
+ dsh plugin --profile desktop add dsh-prompt-rewriter
99
+ ```
96
100
 
97
- `设置 → 提示词增强` 提供三个开关与一个模型选择器,只影响这个按钮,不影响正常发送。
101
+ ### From GitHub source
98
102
 
99
- ![设置页](https://raw.githubusercontent.com/huaguoxingkong/dsh-prompt-rewriter/main/docs/images/03-settings.png)
103
+ ```sh
104
+ dsh plugin --profile desktop add github:huaguoxingkong/dsh-prompt-rewriter
105
+ ```
100
106
 
101
- | 选项 | 默认 | 说明 |
102
- |---|---|---|
103
- | **用哪个模型改写** | 关 | 关:跟随当前对话正在用的模型。开:始终使用下拉中选定的模型。 |
104
- | **参考之前的对话** | 开 | 改写时参考本会话前文,更贴合真实意图。会多耗 token,长对话也更慢。 |
105
- | **参考输入框里的图片** | 关 | 输入框里已放好的图片一并参与改写。**历史消息里的图片始终不读。** |
107
+ Installing from source runs a build step and may ask you to approve it. Grant that only to packages you trust.
106
108
 
107
- 行为细节:
109
+ ### From a release zip
108
110
 
109
- - **模型选择会持久化**:选择器列出你实际拥有的渠道与模型(与输入框的模型选择器同一份数据),无法手填不存在的模型。关闭开关不会清空已选模型,重新打开直接回显上次选择。
110
- - **图片仅在完成上传后可读**:上传中的图片会被跳过,并在状态区提示“有 N 张图片未能读取”,不会让改写失败。
111
- - 三个开关各自独立生效,互不影响。
111
+ Download the zip from [Releases](https://github.com/huaguoxingkong/dsh-prompt-rewriter/releases), unzip, then `npm install && dsh plugin --profile desktop add .`.
112
112
 
113
- ## 配置
113
+ ## FAQ
114
114
 
115
- profile 插件行的 `config` 字段全部可选。前五项由设置页托管,后七项为运维上限:
115
+ **Clicked it and nothing happened.** The composer is empty, or *Use a fixed model* is on with no model picked.
116
+
117
+ **"No credentials configured for the current model."** That model has no key set. Switch models, or pin one that works in settings.
118
+
119
+ **Can I undo the whole rewrite?** One `Ctrl+Z`. It's an ordinary edit step in the composer.
120
+
121
+ **Does it change messages I already sent?** No. It only edits the draft, and it never sends.
122
+
123
+ ## Configuration
124
+
125
+ Every `config` key on the profile plugin line is optional. The first five are managed by the settings page; the last seven are operational ceilings and are not exposed in the UI.
116
126
 
117
127
  ```yaml
118
128
  - id: dsh-prompt-rewriter
119
129
  name: dsh-prompt-rewriter
120
130
  config:
121
- # 设置页托管
122
- useCustomModel: false # 是否固定使用 enhanceProvider/enhanceModel
123
- enhanceProvider: "" # 空 = 跟随会话模型
131
+ # managed by the settings page
132
+ useCustomModel: false # pin enhanceProvider/enhanceModel
133
+ enhanceProvider: "" # empty = follow the session model
124
134
  enhanceModel: ""
125
- referenceImages: false # 是否参考输入框图片
126
- referenceContext: true # 是否参考前文
127
-
128
- # 运维上限(不暴露在设置页)
129
- maxDraftChars: 8000 # 草稿字符上限
130
- maxOutputChars: 12000 # 输出字符上限
131
- maxOutputTokens: 2000 # 单次请求输出 token 上限
132
- timeoutMs: 90000 # 端到端超时
133
- historyTokenBudget: 6000 # 历史注入的 token 预算
134
- historyMaxMessages: 24 # 历史最多取多少条
135
- historyMessageChars: 4000 # 单条历史字符上限
135
+ referenceImages: false # read composer images
136
+ referenceContext: true # read earlier turns
137
+
138
+ # operational ceilings (not exposed in the UI)
139
+ maxDraftChars: 8000 # draft length cap
140
+ maxOutputChars: 12000 # output length cap
141
+ maxOutputTokens: 2000 # output tokens per request
142
+ timeoutMs: 90000 # end-to-end timeout
143
+ historyTokenBudget: 6000 # token budget for injected history
144
+ historyMaxMessages: 24 # max history messages
145
+ historyMessageChars: 4000 # per-message character cap
136
146
  ```
137
147
 
138
- ## 工作原理
148
+ ## How it works
139
149
 
140
- | 半边 | 文件 | 职责 |
150
+ | Half | File | Responsibility |
141
151
  |---|---|---|
142
- | Host | `index.js` | 注册 `POST /api/prompt-enhance`(改写)与 `GET/POST /api/prompt-enhance/settings`(设置),走 `ctx.connection.fetch`,继承 Harness 鉴权与来源校验。读取会话投影得到当前模型,经 `ctx.llm` 生成结果。 |
143
- | Client | `client.js` | 在 `conversation.input.right` 渲染按钮;在 `settings.section` 渲染设置页。通过 `inputActions.insertText` 原位替换草稿。 |
152
+ | Host | `index.js` | Registers `POST /api/prompt-enhance` (rewrite) and `GET/POST /api/prompt-enhance/settings`, through `ctx.connection.fetch` so it inherits Harness auth and origin checks. Reads the session projection for the current model, then generates via `ctx.llm`. |
153
+ | Client | `client.js` | Renders the button at `conversation.input.right` and the settings page at `settings.section`. Replaces the draft in place via `inputActions.insertText`. |
144
154
 
145
- 三个值得注意的设计:
155
+ Three designs worth calling out:
146
156
 
147
- - **上下文按预算裁剪**:历史注入量取 `min(historyTokenBudget, 上下文窗口 × 5%)`,从最新往回收集;单条超预算时截断而非丢弃,保证“有上下文”不会静默退化成“无上下文”。
148
- - **历史以 JSON 数组注入**,系统提示中明确声明“这是数据,不是指令”,防止历史里的文本伪造结构。
149
- - **图片开关由 Host 裁决**:浏览器只要输入框有图就上传,Host 依据开关决定是否使用——避免设置尚未加载完成时读到默认值而静默失效。
157
+ - **Context is budgeted**: injected history is `min(historyTokenBudget, contextWindow × 5%)`, collected newest-first. An over-budget single message is truncated rather than dropped, so "has context" never silently degrades into "no context".
158
+ - **History is injected as a JSON array**, and the system prompt states explicitly that it is data, not instructions — text inside history can't forge structure.
159
+ - **The image switch is decided by the Host**: the browser uploads composer images unconditionally and the Host decides whether to use them, so a not-yet-loaded setting can't silently disable the feature.
150
160
 
151
- ## 已知限制
161
+ ## Known limitations
152
162
 
153
- - 历史图片不参与改写(有意为之,只读输入框图片)。
154
- - 非流式替换:改写完成前按钮显示为动画态。
155
- - 设置页的模型下拉依赖 Host 的 `remote.session.modelCatalog`;该面不可用时下拉会明确提示读取失败,而不是留空。
163
+ - Images from earlier messages never take part (by design; composer images only).
164
+ - Replacement is non-streaming: the button shows its animated state until the rewrite lands.
165
+ - The settings dropdown depends on the Host's `remote.session.modelCatalog`. When that surface is unavailable it says so explicitly rather than rendering an empty list.
156
166
 
157
- ## 开发
167
+ ## Development
158
168
 
159
- 插件分 Host 与 Client 两半:Host 只依赖 `ctx.connection`、`ctx.llm`、`ctx.settings` 等宿主服务,Client 通过 `window.__ModuleLoader__` 注册槽位组件。两半都随 bundle 在启动时加载,改动后需重启验证。
169
+ The plugin has a Host half and a Client half. The Host depends only on host services such as `ctx.connection`, `ctx.llm` and `ctx.settings`; the Client registers slot components through `window.__ModuleLoader__`. Both halves load with the bundle at startup, so changes require a restart to verify.
160
170
 
161
- **单元测试未随本仓库发布。** 开发期测试依赖宿主私有的 `@deepseek-ai/schemastery`、宿主 React 副本与本机 profile 目录结构,直接开源会让使用者拿到跑不起来的文件。
171
+ **Unit tests are not published with this repository.** Development-time tests depend on the host's private `@deepseek-ai/schemastery`, a copy of the host's React, and this machine's profile layout — publishing them would hand users files that cannot run.
162
172
 
163
173
  ```sh
164
- node --check index.js # 语法
174
+ node --check index.js # syntax
165
175
  node --check client.js
166
- npm pack --dry-run # 检查发布内容
176
+ npm pack --dry-run # inspect what would ship
167
177
  ```
168
178
 
169
- ## 许可
170
-
171
- 代码采用 MIT,详见 [LICENSE](LICENSE)。
179
+ ## License
172
180
 
173
- 内嵌素材分两类:
181
+ Code is MIT — see [LICENSE](LICENSE). Embedded artwork falls into two classes:
174
182
 
175
- | 素材 | 许可 | 是否在 MIT 范围内 |
183
+ | Asset | License | Inside the MIT grant? |
176
184
  |---|---|---|
177
- | 插件代码 | MIT | 是 |
178
- | 鲸鱼尾巴 36 帧轮廓(来自 `dsh-status-rotator` v0.38.0,作者 01Virex) | **MIT**,已在上游核实(36 帧逐条比对一致) | 是,保留其版权声明 |
179
- | DeepSeek「大肥鱼」品牌标(来源 `dsh-web-frontend`) | 源包内**未找到许可证** | **否**,已在 LICENSE 中显式排除 |
185
+ | Plugin code | MIT | yes |
186
+ | Whale-tail 36-frame outline (from `dsh-status-rotator` v0.38.0 by 01Virex) | **MIT**, verified upstream (all 36 frames compared) | yes, copyright notice retained |
187
+ | DeepSeek "big fat fish" brand mark (from `dsh-web-frontend`) | **no license found** in the source package | **no**, explicitly excluded in LICENSE |
180
188
 
181
- 本项目为非官方社区项目,与 DeepSeek 无隶属或授权关系。完整归属与核实过程见 [NOTICE.md](NOTICE.md)。
189
+ Unofficial community project, not affiliated with or endorsed by DeepSeek. Full attribution and verification notes are in [NOTICE.md](NOTICE.md).
package/README.zh.md ADDED
@@ -0,0 +1,190 @@
1
+ # dsh-prompt-rewriter
2
+
3
+ [English](README.md) | 中文
4
+
5
+ [![npm](https://img.shields.io/npm/v/dsh-prompt-rewriter.svg)](https://www.npmjs.com/package/dsh-prompt-rewriter)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
8
+ 把输入框里那句没头没尾的草稿,原地改成能直接执行的任务说明。
9
+ **只改内容,不发送、不运行。** 不满意就 `Ctrl+Z` 回去。
10
+
11
+ > 非官方社区项目,与 DeepSeek 无隶属或授权关系。内嵌素材的归属见 [NOTICE.md](NOTICE.md)。
12
+
13
+ ---
14
+
15
+ ![在输入框里点一下,草稿原地变成完整提示词](https://raw.githubusercontent.com/huaguoxingkong/dsh-prompt-rewriter/main/docs/images/01-hero.gif)
16
+
17
+ ## 它替你补上了什么
18
+
19
+ 一句话的草稿缺的是**目标、格式、边界**。模型只能猜——猜错了你得换个说法再问一轮。
20
+ 这个按钮把这三样补进你原本那句话里:
21
+
22
+ | | 你写的 | 放回输入框的 |
23
+ |---|---|---|
24
+ | 你写的 | *帮我写个爬虫抓豆瓣电影* | 同一句话 |
25
+ | 补上目标 | | 抓取电影 **Top250** 的**名称、评分和链接** |
26
+ | 补上格式 | | 输出为 **CSV 文件** |
27
+ | 补上边界 | | 遵守 robots 协议、**请求间隔不少于 2 秒**、遇到反爬**降低频率而不是绕过限制** |
28
+
29
+ 第三列不是你打上去的,是它读了你那句话之后补的。**补出来的东西你随时能改**——它落在输入框里,不在别的地方。
30
+
31
+ ## 怎么用
32
+
33
+ 1. 在输入框里写下草稿,多短都行。
34
+ 2. 点**模型名左边**那个鲸鱼尾巴图标。输入框为空时它不出现。
35
+ 3. 一秒多之后,草稿被换成改写后的提示词。
36
+ 4. 不满意?`Ctrl+Z`,或点按钮变成的回退箭头。
37
+
38
+ 中途想放弃就再点一下,草稿保持原样,不会给你留下半截文字。
39
+
40
+ ### 增强过程中可以切走
41
+
42
+ 点了按钮之后不必守着它。**切到别的会话,任务会继续跑完。**
43
+
44
+ 回来的时候,结果已经写进输入框了;如果还没跑完,按钮仍然是那个旋转的鱼尾。两个会话可以同时各跑一个,互不影响。
45
+
46
+ 这一点在 1.0.x 里不是这样:那时切换会话会直接中断任务,回来什么也没有。
47
+
48
+ ### 按钮的四种状态
49
+
50
+ ![按钮状态:空闲、改写中、完成可回退、失败可重试](https://raw.githubusercontent.com/huaguoxingkong/dsh-prompt-rewriter/main/docs/images/05-button-states.gif)
51
+
52
+ | 状态 | 你看到的 | 意味着 |
53
+ |---|---|---|
54
+ | 空闲 | 静止的鲸鱼尾巴 | 可以点 |
55
+ | 改写中 | 鱼尾旋成螺旋 | 在跑;再点一下是取消 |
56
+ | 完成 | 变成回退箭头 | 一键还原,或用 `Ctrl+Z` |
57
+ | 失败 | 红色重试图标 + 原因 | 原因就在按钮旁边,不用翻日志 |
58
+
59
+ ## 两个容易搞混的地方
60
+
61
+ ### 一、它读不读前文
62
+
63
+ ![空会话 vs 已有对话:图标右下角的小圆点是标记](https://raw.githubusercontent.com/huaguoxingkong/dsh-prompt-rewriter/main/docs/images/04-context-modes.png)
64
+
65
+ 按钮右下角**有没有那个小圆点**,就是全部区别:
66
+
67
+ - **空会话**(无圆点):只看你当前这句草稿。
68
+ - **已有对话**(有圆点):还会读前面聊过的内容,知道你已经确认过什么、排除过什么,改写更贴你这次真正想要的东西。
69
+
70
+ 读前文是要花 token 的。长对话里觉得慢或觉得贵,去设置里关掉「参考之前的对话」,按钮上的圆点随之消失。
71
+
72
+ ### 二、它读不读图
73
+
74
+ 只读**你现在放在输入框里的**图片,**历史消息里的图片永远不读**。这条边界是刻意的:旧截图既慢又容易看偏,不纳入范围。
75
+
76
+ ## 设置
77
+
78
+ `设置 → 提示词改写`。三个开关一个下拉,**只影响这个按钮,不影响正常发送**。
79
+
80
+ ![设置页](https://raw.githubusercontent.com/huaguoxingkong/dsh-prompt-rewriter/main/docs/images/03-settings.png)
81
+
82
+ | 选项 | 默认 | 打开会怎样 |
83
+ |---|---|---|
84
+ | 用哪个模型改写 | 关 | 关:跟随当前对话的模型。开:固定用下拉选定的模型。 |
85
+ | 参考之前的对话 | **开** | 读前面的内容。更准,但多耗 token、长对话更慢。 |
86
+ | 参考输入框里的图片 | 关 | 输入框里的图一起参与。历史消息里的图片始终不读。 |
87
+
88
+ - 模型下拉列出你**实际拥有**的渠道与模型,填不进不存在的模型。
89
+ - 关掉模型开关不会清空你的选择,重新打开还是同一个。
90
+ - 上传中的图片会被跳过,状态区会告诉你少读了几张。不会因为一张图让整次改写失败。
91
+
92
+ ## 安装
93
+
94
+ 需要 DeepSeek Harness 桌面版,`node >= 22`。**装完必须重启 Harness**——本插件没有 HMR。
95
+
96
+ ### 从 npm(推荐)
97
+
98
+ ```sh
99
+ dsh plugin --profile desktop add dsh-prompt-rewriter
100
+ ```
101
+
102
+ ### 从 GitHub 源码
103
+
104
+ ```sh
105
+ dsh plugin --profile desktop add github:huaguoxingkong/dsh-prompt-rewriter
106
+ ```
107
+
108
+ 源码安装会跑构建步骤,首次可能要求你授权。只对你信任的包给这个权限。
109
+
110
+ ### 从 Release 的 zip
111
+
112
+ 到 [Releases](https://github.com/huaguoxingkong/dsh-prompt-rewriter/releases) 下载 zip,解压后 `npm install && dsh plugin --profile desktop add .`。
113
+
114
+ ## 常见问题
115
+
116
+ **点了没反应?** 输入框是空的,或者「用哪个模型改写」打开了但没选模型。
117
+
118
+ **报「当前模型尚未配置可用的凭据」?** 这个模型没配 key。换个模型,或在设置里固定一个能用的。
119
+
120
+ **改写完想全撤?** 一次 `Ctrl+Z` 就回到原文,它只是输入框里一个普通编辑步。
121
+
122
+ **会把我已经发出去的消息也改了吗?** 不会。它只改草稿,从不发送。
123
+
124
+ ## 配置
125
+
126
+ profile 插件行的 `config` 全部可选。前五项由设置页托管,后七项是运维上限,不暴露在界面上:
127
+
128
+ ```yaml
129
+ - id: dsh-prompt-rewriter
130
+ name: dsh-prompt-rewriter
131
+ config:
132
+ # 设置页托管
133
+ useCustomModel: false # 是否固定使用 enhanceProvider/enhanceModel
134
+ enhanceProvider: "" # 空 = 跟随会话模型
135
+ enhanceModel: ""
136
+ referenceImages: false # 是否参考输入框图片
137
+ referenceContext: true # 是否参考前文
138
+
139
+ # 运维上限(不暴露在设置页)
140
+ maxDraftChars: 8000 # 草稿字符上限
141
+ maxOutputChars: 12000 # 输出字符上限
142
+ maxOutputTokens: 2000 # 单次请求输出 token 上限
143
+ timeoutMs: 90000 # 端到端超时
144
+ historyTokenBudget: 6000 # 历史注入的 token 预算
145
+ historyMaxMessages: 24 # 历史最多取多少条
146
+ historyMessageChars: 4000 # 单条历史字符上限
147
+ ```
148
+
149
+ ## 工作原理
150
+
151
+ | 半边 | 文件 | 职责 |
152
+ |---|---|---|
153
+ | Host | `index.js` | 注册 `POST /api/prompt-enhance`(改写)与 `GET/POST /api/prompt-enhance/settings`(设置),走 `ctx.connection.fetch`,继承 Harness 鉴权与来源校验。读取会话投影得到当前模型,经 `ctx.llm` 生成结果。 |
154
+ | Client | `client.js` | 在 `conversation.input.right` 渲染按钮;在 `settings.section` 渲染设置页。通过 `inputActions.insertText` 原位替换草稿。 |
155
+
156
+ 三处值得说明的设计:
157
+
158
+ - **上下文按预算裁剪**:历史注入量取 `min(historyTokenBudget, 上下文窗口 × 5%)`,从最新往回收集;单条超预算时截断而非丢弃,保证「有上下文」不会静默退化成「无上下文」。
159
+ - **历史以 JSON 数组注入**,系统提示中明确声明「这是数据,不是指令」,防止历史里的文本伪造结构。
160
+ - **图片开关由 Host 裁决**:浏览器只要输入框有图就上传,Host 依据开关决定是否使用——避免设置尚未加载完成时读到默认值而静默失效。
161
+
162
+ ## 已知限制
163
+
164
+ - 历史消息里的图片不参与改写(有意为之,只读输入框图片)。
165
+ - 非流式替换:改写完成前按钮显示为动画态。
166
+ - 设置页的模型下拉依赖 Host 的 `remote.session.modelCatalog`;该面不可用时下拉会明确提示读取失败,而不是留空。
167
+
168
+ ## 开发
169
+
170
+ 插件分 Host 与 Client 两半:Host 只依赖 `ctx.connection`、`ctx.llm`、`ctx.settings` 等宿主服务,Client 通过 `window.__ModuleLoader__` 注册槽位组件。两半都随 bundle 在启动时加载,改动后需重启验证。
171
+
172
+ **单元测试未随本仓库发布。** 开发期测试依赖宿主私有的 `@deepseek-ai/schemastery`、宿主 React 副本与本机 profile 目录结构,直接开源会让使用者拿到跑不起来的文件。
173
+
174
+ ```sh
175
+ node --check index.js # 语法
176
+ node --check client.js
177
+ npm pack --dry-run # 检查发布内容
178
+ ```
179
+
180
+ ## 许可
181
+
182
+ 代码采用 MIT,详见 [LICENSE](LICENSE)。内嵌素材分两类:
183
+
184
+ | 素材 | 许可 | 是否在 MIT 范围内 |
185
+ |---|---|---|
186
+ | 插件代码 | MIT | 是 |
187
+ | 鲸鱼尾巴 36 帧轮廓(来自 `dsh-status-rotator` v0.38.0,作者 01Virex) | **MIT**,已在上游核实(36 帧逐条比对一致) | 是,保留其版权声明 |
188
+ | DeepSeek「大肥鱼」品牌标(来源 `dsh-web-frontend`) | 源包内**未找到许可证** | **否**,已在 LICENSE 中显式排除 |
189
+
190
+ 本项目为非官方社区项目,与 DeepSeek 无隶属或授权关系。完整归属与核实过程见 [NOTICE.md](NOTICE.md)。
package/client.js CHANGED
@@ -248,6 +248,103 @@ window.__ModuleLoader__.load({
248
248
  );
249
249
  }
250
250
 
251
+ /**
252
+ * One enhancement task per session, owned OUTSIDE the component tree.
253
+ *
254
+ * The composer control is mounted per session and unmounted the moment the
255
+ * user switches away. Keeping the in-flight request (and its abort
256
+ * controller) inside the component therefore tied the task's lifetime to
257
+ * whichever session happened to be on screen: unmounting aborted the fetch
258
+ * and the cleanup flagged the component dead, so the reply was discarded
259
+ * even if it arrived. Switching sessions looked like "the enhancement was
260
+ * cancelled", when in fact the UI had simply outlived it.
261
+ *
262
+ * This registry is the decoupling point. A task belongs to a sessionId, not
263
+ * to a component instance, so it survives unmount, keeps running, and is
264
+ * delivered to whichever composer mounts that session later. Two sessions
265
+ * get independent entries and cannot interfere.
266
+ *
267
+ * @returns a small imperative store with subscribe/getSnapshot per session.
268
+ */
269
+ function createTaskStore() {
270
+ /** sessionId -> task record. Entries are removed once settled and consumed. */
271
+ const tasks = new Map();
272
+ /** sessionId -> listener set, so a remount can re-render on progress. */
273
+ const listeners = new Map();
274
+ /** Bumped whenever any task changes, so snapshots are stable to compare. */
275
+ let version = 0;
276
+ /** Cache of published snapshots, keyed by sessionId, invalidated by version. */
277
+ const snapshots = new Map();
278
+
279
+ const notify = (sessionId) => {
280
+ version += 1;
281
+ snapshots.clear();
282
+ const set = listeners.get(sessionId);
283
+ if (set === undefined) return;
284
+ for (const listener of set) listener();
285
+ };
286
+
287
+ /**
288
+ * The public view of a session's task. `null` means "nothing known", which
289
+ * is what the control renders when the session has no work.
290
+ */
291
+ const snapshot = (sessionId) => {
292
+ const cached = snapshots.get(sessionId);
293
+ if (cached !== undefined) return cached;
294
+ const task = tasks.get(sessionId);
295
+ const next = task === undefined
296
+ ? null
297
+ : Object.freeze({ phase: task.phase, failure: task.failure, announce: task.announce, contextAware: task.contextAware, restore: task.restore, result: task.result });
298
+ snapshots.set(sessionId, next);
299
+ return next;
300
+ };
301
+
302
+ const subscribe = (sessionId, listener) => {
303
+ let set = listeners.get(sessionId);
304
+ if (set === undefined) {
305
+ set = new Set();
306
+ listeners.set(sessionId, set);
307
+ }
308
+ set.add(listener);
309
+ return () => {
310
+ set.delete(listener);
311
+ if (set.size === 0) listeners.delete(sessionId);
312
+ };
313
+ };
314
+
315
+ /** Read-or-create the record for a session. */
316
+ const ensure = (sessionId) => {
317
+ let task = tasks.get(sessionId);
318
+ if (task === undefined) {
319
+ task = { phase: 'idle', failure: null, announce: null, contextAware: false, restore: null, controller: null };
320
+ tasks.set(sessionId, task);
321
+ }
322
+ return task;
323
+ };
324
+
325
+ /** Apply a patch to a session's record and publish it. */
326
+ const patch = (sessionId, changes) => {
327
+ const task = ensure(sessionId);
328
+ Object.assign(task, changes);
329
+ notify(sessionId);
330
+ };
331
+
332
+ /** Drop a session's record entirely (task finished and was consumed). */
333
+ const clear = (sessionId) => {
334
+ if (tasks.delete(sessionId)) notify(sessionId);
335
+ };
336
+
337
+ return { snapshot, subscribe, ensure, patch, clear, get tasks() { return tasks; } };
338
+ }
339
+
340
+ /**
341
+ * Sessions whose enhancement finished while their composer was unmounted and
342
+ * nobody has looked at them yet. On remount the control picks these up and
343
+ * adopts the result, so the user sees the completed task on return instead of
344
+ * a silent loss.
345
+ */
346
+ const taskStore = createTaskStore();
347
+
251
348
  /**
252
349
  * Composer-side enhancement control: sends the current draft to this
253
350
  * package's Host half, then replaces the draft in place — never submits.
@@ -263,34 +360,33 @@ window.__ModuleLoader__.load({
263
360
  // The composer's own attachment list. Only these images may be referenced;
264
361
  // images from earlier turns live in the session log and are never read here.
265
362
  const attachmentIds = useInput((state) => state.attachmentIds);
266
- const [busy, setBusy] = useState(false);
267
- // Failure text only; success is represented by the undo offer itself.
268
- const [failure, setFailure] = useState(null);
269
- // Status text for the live region only. A user-initiated pause lands here
270
- // instead of `failure`, so it is announced without changing the icon.
271
- const [announce, setAnnounce] = useState(null);
272
- // True when the last successful run consulted the conversation history.
273
- const [contextAware, setContextAware] = useState(false);
274
- // `restore` is the undo offer: `text` is the pre-enhancement prompt and
275
- // `produced` is the exact draft the swap left behind. The offer dies as
276
- // soon as the live draft stops matching `produced`.
277
- const [restore, setRestore] = useState(null);
278
- const flight = useRef(null);
279
- const alive = useRef(false);
280
- const ticket = useRef(0);
363
+ // Task state lives in the module-level registry, keyed by session, so it
364
+ // outlives this component instance. Switching sessions only changes which
365
+ // entry is read; it never cancels work.
366
+ //
367
+ // A version counter forces a re-render when the task changes while this
368
+ // component is mounted (progress, failure, completion). The counter is
369
+ // local state only — it is not the task, so losing it on unmount costs
370
+ // nothing and cannot cancel work.
371
+ const [, bumpTaskVersion] = useState(0);
372
+ useEffect(() => taskStore.subscribe(sessionId, () => bumpTaskVersion((v) => v + 1)), [sessionId]);
373
+ // The snapshot is read straight from the registry on every render. The version
374
+ // counter above is what triggers those renders; the value itself never lives
375
+ // in component state, so unmounting cannot lose or cancel a task.
376
+ const task = taskStore.snapshot(sessionId);
377
+ const busy = task?.phase === 'running';
378
+ const failure = task?.failure ?? null;
379
+ const announce = task?.announce ?? null;
380
+ const contextAware = task?.contextAware === true;
381
+ const restore = task?.restore ?? null;
281
382
  const latestRev = useRef(draftRev);
282
383
  latestRev.current = draftRev;
283
384
 
284
- // Owns the lifetime of in-flight work: the composer unmounts far more
285
- // often than the page reloads, so the abort must follow the component.
286
- useEffect(() => {
287
- alive.current = true;
288
- return () => {
289
- alive.current = false;
290
- flight.current?.abort();
291
- flight.current = null;
292
- };
293
- }, []);
385
+ // NO abort on unmount. The component unmounts whenever the user switches
386
+ // away from this session, and aborting there is exactly what made the
387
+ // enhancement stop; the request now belongs to the registry and keeps
388
+ // running until it settles on its own.
389
+ useEffect(() => () => {}, []);
294
390
 
295
391
  const replaceDraft = useCallback(
296
392
  (text, before, rev) => inputActions?.insertText(text, { start: 0, end: before.length, draftRev: rev }) === true,
@@ -301,16 +397,15 @@ window.__ModuleLoader__.load({
301
397
  const undo = useCallback(() => {
302
398
  if (restore === null) return;
303
399
  if (draft !== restore.produced) {
304
- setRestore(null);
400
+ taskStore.patch(sessionId, { restore: null });
305
401
  return;
306
402
  }
307
403
  // Select over the draft the editor actually holds, so any normalization
308
404
  // the composer applied to the model output cannot leave a residue.
309
405
  if (replaceDraft(restore.text, draft, draftRev)) {
310
- setRestore(null);
311
- setFailure(null);
406
+ taskStore.patch(sessionId, { restore: null, failure: null });
312
407
  }
313
- }, [draft, draftRev, replaceDraft, restore]);
408
+ }, [draft, draftRev, replaceDraft, restore, sessionId]);
314
409
 
315
410
  /** Encode bytes as canonical base64 for the JSON request body. */
316
411
  function bytesToBase64(bytes) {
@@ -365,25 +460,42 @@ window.__ModuleLoader__.load({
365
460
  return { images, skipped };
366
461
  }
367
462
 
368
- const run = useCallback(() => {
369
- if (flight.current !== null) {
370
- flight.current.abort();
463
+ /**
464
+ * Start or pause this session's enhancement.
465
+ *
466
+ * The request runs against the REGISTRY entry, not this closure, so neither
467
+ * unmounting (session switch) nor a re-render can cancel it. `phase` in the
468
+ * record is the single source of truth for busy/idle, which also makes the
469
+ * button state correct when the user returns mid-flight.
470
+ */
471
+ const run = useCallback(() => {
472
+ const task = taskStore.ensure(sessionId);
473
+ // Second click means "pause": abort the request this task owns. Because the
474
+ // controller is stored on the record, it is still reachable no matter which
475
+ // component instance asked.
476
+ if (task.phase === 'running' && task.controller !== null) {
477
+ task.controller.abort();
371
478
  return;
372
479
  }
373
480
  const before = draft;
374
481
  if (before.trim().length === 0) {
375
- setFailure(null);
376
- setRestore(null);
377
- setAnnounce(null);
482
+ taskStore.patch(sessionId, { failure: null, restore: null, announce: null });
378
483
  return;
379
484
  }
380
485
  const rev = draftRev;
381
486
  const controller = new AbortController();
382
- flight.current = controller;
383
- const mine = ++ticket.current;
384
- setBusy(true);
385
- setFailure(null);
386
- setAnnounce(null);
487
+ taskStore.patch(sessionId, {
488
+ phase: 'running',
489
+ failure: null,
490
+ announce: null,
491
+ controller,
492
+ // The draft this run rewrites; captured so the reply is validated
493
+ // against the text that was actually sent.
494
+ origin: before,
495
+ originRev: rev,
496
+ });
497
+ // Deliberately a floating async task owned by the store: nothing awaits it,
498
+ // and no component cleanup can reach its abort controller.
387
499
  (async () => {
388
500
  try {
389
501
  // Read the composer's images whenever the draft has any. The Host owns
@@ -407,57 +519,114 @@ window.__ModuleLoader__.load({
407
519
  });
408
520
  if (!response.ok) throw new Error(t('state.transport', { status: response.status }));
409
521
  const payload = await response.json();
410
- if (!alive.current || mine !== ticket.current) return;
411
522
  if (payload?.ok !== true) throw new Error(payload?.message || t('state.failed'));
412
523
  const text = typeof payload.text === 'string' ? payload.text : '';
413
- if (latestRev.current !== rev || text.trim().length === 0) {
414
- setRestore(null);
415
- return;
416
- }
417
- if (!replaceDraft(text, before, rev)) {
418
- setFailure(t('state.refused'));
524
+ if (text.trim().length === 0) {
525
+ taskStore.patch(sessionId, { restore: null });
419
526
  return;
420
527
  }
421
- // Record whether this run was context-aware so the seat can show it.
422
- setContextAware(payload?.mode === 'context');
423
- // Say so when a picture could not be read, rather than silently
424
- // rewriting without it.
425
528
  const skippedImages = Number(payload?.imagesSkipped) || 0;
426
- setAnnounce(skippedImages > 0 ? t('state.imagesSkipped', { count: skippedImages }) : null);
427
- // Success: the same seat now offers the way back. The produced text
428
- // is read from the next published draft, so the offer compares
429
- // against what the composer really stored.
430
- setRestore({ text: before, produced: null, pending: true });
529
+ // `applyResult` is the live delivery path, supplied by whichever composer
530
+ // is currently mounted for this session. When nobody is mounted it is
531
+ // undefined and the parked result waits for the remount.
532
+ const apply = taskStore.ensure(sessionId).applyResult;
533
+ const outcome = typeof apply === 'function' ? apply(text, before, rev) : 'parked';
534
+ taskStore.patch(sessionId, {
535
+ contextAware: payload?.mode === 'context',
536
+ announce: skippedImages > 0 ? t('state.imagesSkipped', { count: skippedImages }) : null,
537
+ });
538
+ if (outcome === 'applied') {
539
+ // The undo offer is only meaningful once the text actually landed in the
540
+ // editor; offering it after a refused or superseded write would claim a
541
+ // swap that never happened.
542
+ taskStore.patch(sessionId, { restore: { text: before, produced: null, pending: true } });
543
+ } else if (outcome === 'refused') {
544
+ taskStore.patch(sessionId, { restore: null, failure: t('state.refused') });
545
+ } else if (outcome === 'parked') {
546
+ // Nobody is mounted for this session: keep the result for the next mount.
547
+ taskStore.patch(sessionId, { result: { text, originRev: rev, origin: before }, restore: null });
548
+ } else {
549
+ // The draft moved on while the user was away; report it instead of
550
+ // overwriting text they have since edited.
551
+ taskStore.patch(sessionId, { restore: null, announce: t('state.stale') });
552
+ }
431
553
  } catch (error) {
432
- if (!alive.current || mine !== ticket.current) return;
433
554
  if (controller.signal.aborted) {
434
- // The user paused the run themselves. That is not a failure, so
435
- // it must not raise the retry mark: fall back to the resting
436
- // fish and keep the draft untouched. Screen readers still hear
437
- // the cancellation through the live region.
438
- setFailure(null);
439
- setRestore(null);
440
- setAnnounce(t('state.cancelled'));
555
+ // The user paused the run themselves. That is not a failure, so it
556
+ // must not raise the retry mark: fall back to the resting fish and
557
+ // keep the draft untouched. Screen readers still hear the
558
+ // cancellation through the live region.
559
+ taskStore.patch(sessionId, { failure: null, restore: null, announce: t('state.cancelled') });
441
560
  } else {
442
- setFailure(error instanceof Error ? error.message : t('state.failed'));
561
+ taskStore.patch(sessionId, { failure: error instanceof Error ? error.message : t('state.failed') });
443
562
  }
444
563
  } finally {
445
- if (flight.current === controller) flight.current = null;
446
- if (alive.current && mine === ticket.current) setBusy(false);
564
+ const current = taskStore.ensure(sessionId);
565
+ if (current.controller === controller) taskStore.patch(sessionId, { phase: 'idle', controller: null });
447
566
  }
448
567
  })();
449
- }, [draft, draftRev, replaceDraft, sessionId, t]);
568
+ }, [attachmentIds, draft, draftRev, sessionId, sessions, t]);
569
+
570
+ /**
571
+ * Publish this composer as the live delivery target for its session.
572
+ *
573
+ * The task runs outside the component tree, so when it finishes it needs a
574
+ * way to reach whatever editor is on screen RIGHT NOW. Registering the
575
+ * applier here is that bridge: mounted → applied immediately; unmounted →
576
+ * the parked result waits for the next mount, which re-registers and takes it.
577
+ * This replaces the old "effect only runs on mount" delivery, which could not
578
+ * see a result that landed after the first render.
579
+ */
580
+ useEffect(() => {
581
+ const apply = (text, before, rev) => {
582
+ // Distinguish the two reasons a result may not land, because the user needs
583
+ // to know which happened: the editor refused the write, or the draft moved
584
+ // on while the request was in flight.
585
+ if (latestRev.current !== rev) return 'stale';
586
+ return replaceDraft(text, before, rev) === true ? 'applied' : 'refused';
587
+ };
588
+ taskStore.patch(sessionId, { applyResult: apply });
589
+ return () => {
590
+ // Only clear if we are still the registered applier; a remount may have
591
+ // taken over already.
592
+ const current = taskStore.ensure(sessionId);
593
+ if (current.applyResult === apply) taskStore.patch(sessionId, { applyResult: undefined });
594
+ };
595
+ }, [replaceDraft, sessionId]);
596
+
597
+ // Deliver a parked result: the run finished while this session had no mounted
598
+ // composer (the user was looking at another conversation). Applying it here
599
+ // is what makes "switch away and come back" show a completed rewrite instead
600
+ // of a lost one, and it runs once per result because the record is cleared
601
+ // after consumption.
602
+ useEffect(() => {
603
+ const pendingResult = taskStore.ensure(sessionId).result;
604
+ if (pendingResult === undefined) return;
605
+ // Only the composer that owns this session's live editor may write to it.
606
+ if (inputActions === undefined || typeof inputActions.insertText !== 'function') return;
607
+ const { text, originRev } = pendingResult;
608
+ const swapped = latestRev.current === originRev
609
+ ? replaceDraft(text, pendingResult.origin, originRev)
610
+ : false;
611
+ // Consume regardless: a stale result must not be re-applied on every mount.
612
+ taskStore.patch(sessionId, { result: undefined });
613
+ if (!swapped) {
614
+ // The draft moved on while the user was away; keep the outcome visible as
615
+ // a notice instead of overwriting text they have since edited.
616
+ taskStore.patch(sessionId, { restore: null, announce: t('state.stale') });
617
+ }
618
+ }, [inputActions, replaceDraft, sessionId, t]);
450
619
 
451
620
  // Adopt the produced draft on the first render after a successful swap,
452
621
  // then retire the offer once the user edits that text.
453
622
  useEffect(() => {
454
623
  if (restore === null) return;
455
624
  if (restore.pending) {
456
- setRestore({ text: restore.text, produced: draft, pending: false });
625
+ taskStore.patch(sessionId, { restore: { text: restore.text, produced: draft, pending: false } });
457
626
  return;
458
627
  }
459
- if (restore.produced !== draft) setRestore(null);
460
- }, [draft, restore]);
628
+ if (restore.produced !== draft) taskStore.patch(sessionId, { restore: null });
629
+ }, [draft, restore, sessionId]);
461
630
 
462
631
  if (inputActions === undefined || typeof inputActions.insertText !== 'function') return null;
463
632
 
@@ -845,6 +1014,7 @@ window.__ModuleLoader__.load({
845
1014
  'state.failed': '增强失败,草稿保持不变。',
846
1015
  'state.failedWith': '增强失败:{reason}。点击可重试。',
847
1016
  'state.cancelled': '已停止增强,草稿保持不变。',
1017
+ 'state.stale': '增强已完成,但草稿在你离开期间被改动,结果未写入。',
848
1018
  'state.transport': '增强接口不可用(HTTP {status}),请确认插件已启用。',
849
1019
  'state.imagesSkipped': '有 {count} 张图片未能读取,本次增强未参考它们。',
850
1020
  // Settings page
@@ -875,6 +1045,7 @@ window.__ModuleLoader__.load({
875
1045
  'state.failed': 'Enhancement failed; the draft is unchanged.',
876
1046
  'state.failedWith': 'Enhancement failed: {reason}. Click to retry.',
877
1047
  'state.cancelled': 'Enhancement stopped; the draft is unchanged.',
1048
+ 'state.stale': 'The rewrite finished, but the draft changed while you were away, so it was not applied.',
878
1049
  'state.transport': 'The enhancement endpoint is unavailable (HTTP {status}); check that the plugin is enabled.',
879
1050
  'state.imagesSkipped': '{count} image(s) could not be read; this enhancement did not use them.',
880
1051
  // Settings page
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-prompt-rewriter",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "One-click prompt enhancement beside the DeepSeek Harness composer: rewrites a draft in place without sending it.",
5
5
  "type": "module",
6
6
  "icon": "icon.svg",
@@ -17,8 +17,10 @@
17
17
  "locale/*.json",
18
18
  "icon.svg",
19
19
  "docs/images/*.png",
20
+ "docs/images/*.gif",
20
21
  "screenshots.json",
21
22
  "README.md",
23
+ "README.zh.md",
22
24
  "LICENSE",
23
25
  "NOTICE.md"
24
26
  ],
@@ -78,4 +80,4 @@
78
80
  "scripts": {
79
81
  "prepublishOnly": "node --check index.js && node --check client.js"
80
82
  }
81
- }
83
+ }
package/screenshots.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "screenshots": [
3
- "docs/images/01-composer-button.png",
4
- "docs/images/02-before-after.png",
3
+ "docs/images/01-hero.gif",
4
+ "docs/images/05-button-states.gif",
5
5
  "docs/images/03-settings.png",
6
6
  "docs/images/04-context-modes.png",
7
- "docs/images/05-button-states.png"
7
+ "docs/images/01-hero-en.gif",
8
+ "docs/images/05-button-states-en.gif",
9
+ "docs/images/03-settings-en.png",
10
+ "docs/images/04-context-modes-en.png"
8
11
  ]
9
12
  }
Binary file
Binary file
Binary file