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 +116 -108
- package/README.zh.md +190 -0
- package/client.js +242 -71
- package/docs/images/01-hero-en.gif +0 -0
- package/docs/images/01-hero.gif +0 -0
- package/docs/images/03-settings-en.png +0 -0
- package/docs/images/04-context-modes-en.png +0 -0
- package/docs/images/05-button-states-en.gif +0 -0
- package/docs/images/05-button-states.gif +0 -0
- package/package.json +4 -2
- package/screenshots.json +6 -3
- package/docs/images/01-composer-button.png +0 -0
- package/docs/images/02-before-after.png +0 -0
- package/docs/images/05-button-states.png +0 -0
package/README.md
CHANGED
|
@@ -1,181 +1,189 @@
|
|
|
1
1
|
# dsh-prompt-rewriter
|
|
2
2
|
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
3
5
|
[](https://www.npmjs.com/package/dsh-prompt-rewriter)
|
|
4
6
|
[](LICENSE)
|
|
5
7
|
|
|
6
|
-
|
|
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
|
-
|
|
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
|
+

|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
### The four button states
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+

|
|
44
50
|
|
|
45
|
-
|
|
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
|
-
|
|
58
|
+
## Two things worth not confusing
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
dsh plugin --profile desktop add dsh-prompt-rewriter
|
|
51
|
-
```
|
|
60
|
+
### 1. Whether it reads the conversation
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+

|
|
54
63
|
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
### 2. Whether it reads images
|
|
64
72
|
|
|
65
|
-
|
|
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
|
-
|
|
75
|
+
## Settings
|
|
72
76
|
|
|
73
|
-
|
|
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
|
-
|
|
79
|
+

|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 #
|
|
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 #
|
|
132
|
-
timeoutMs: 90000 #
|
|
133
|
-
historyTokenBudget: 6000 #
|
|
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` |
|
|
143
|
-
| Client | `client.js` |
|
|
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
|
-
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
183
|
+
| Asset | License | Inside the MIT grant? |
|
|
176
184
|
|---|---|---|
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
| DeepSeek
|
|
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
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/dsh-prompt-rewriter)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
把输入框里那句没头没尾的草稿,原地改成能直接执行的任务说明。
|
|
9
|
+
**只改内容,不发送、不运行。** 不满意就 `Ctrl+Z` 回去。
|
|
10
|
+
|
|
11
|
+
> 非官方社区项目,与 DeepSeek 无隶属或授权关系。内嵌素材的归属见 [NOTICE.md](NOTICE.md)。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+

|
|
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
|
+

|
|
51
|
+
|
|
52
|
+
| 状态 | 你看到的 | 意味着 |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| 空闲 | 静止的鲸鱼尾巴 | 可以点 |
|
|
55
|
+
| 改写中 | 鱼尾旋成螺旋 | 在跑;再点一下是取消 |
|
|
56
|
+
| 完成 | 变成回退箭头 | 一键还原,或用 `Ctrl+Z` |
|
|
57
|
+
| 失败 | 红色重试图标 + 原因 | 原因就在按钮旁边,不用翻日志 |
|
|
58
|
+
|
|
59
|
+
## 两个容易搞混的地方
|
|
60
|
+
|
|
61
|
+
### 一、它读不读前文
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
65
|
+
按钮右下角**有没有那个小圆点**,就是全部区别:
|
|
66
|
+
|
|
67
|
+
- **空会话**(无圆点):只看你当前这句草稿。
|
|
68
|
+
- **已有对话**(有圆点):还会读前面聊过的内容,知道你已经确认过什么、排除过什么,改写更贴你这次真正想要的东西。
|
|
69
|
+
|
|
70
|
+
读前文是要花 token 的。长对话里觉得慢或觉得贵,去设置里关掉「参考之前的对话」,按钮上的圆点随之消失。
|
|
71
|
+
|
|
72
|
+
### 二、它读不读图
|
|
73
|
+
|
|
74
|
+
只读**你现在放在输入框里的**图片,**历史消息里的图片永远不读**。这条边界是刻意的:旧截图既慢又容易看偏,不纳入范围。
|
|
75
|
+
|
|
76
|
+
## 设置
|
|
77
|
+
|
|
78
|
+
`设置 → 提示词改写`。三个开关一个下拉,**只影响这个按钮,不影响正常发送**。
|
|
79
|
+
|
|
80
|
+

|
|
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
|
-
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
const
|
|
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
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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 (
|
|
414
|
-
|
|
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
|
-
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
|
|
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
|
-
|
|
561
|
+
taskStore.patch(sessionId, { failure: error instanceof Error ? error.message : t('state.failed') });
|
|
443
562
|
}
|
|
444
563
|
} finally {
|
|
445
|
-
|
|
446
|
-
if (
|
|
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,
|
|
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
|
-
|
|
625
|
+
taskStore.patch(sessionId, { restore: { text: restore.text, produced: draft, pending: false } });
|
|
457
626
|
return;
|
|
458
627
|
}
|
|
459
|
-
if (restore.produced !== draft)
|
|
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
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-prompt-rewriter",
|
|
3
|
-
"version": "1.0
|
|
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-
|
|
4
|
-
"docs/images/
|
|
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/
|
|
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
|