dsh-mini 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
@@ -177,20 +177,29 @@ cd app
177
177
  在**任何程序**里用鼠标选中一段文字,松开后光标右下方会弹出蓝色小药丸「问大肥鱼」;点它,
178
178
  小窗自动弹到最前,选中的内容以引用块(每行 `> ` 前缀)插进输入框,直接接着提问即可。
179
179
 
180
- * 实现在 `app/SelectionWatcher.cs`:全局鼠标钩子(90ms 轮询)→ 松开时若拖动距离 ≥6 逻辑像素、
180
+ * 实现在 `app/SelectionWatcher.cs`:全局鼠标轮询(90ms)→ 松开时若拖动距离 ≥6 逻辑像素、
181
181
  且不在小窗/药丸自己身上 → 后台线程等 130ms(双击选词等 320ms)→ 校验仍在前台 → 取选区。
182
- * Windows 没有「给我当前选区」的 API,所以取选区是:备份剪贴板 → 发一次 `Ctrl+C` →
183
- 轮询 `GetClipboardSequenceNumber()` 变化(最多 900ms)→ 读文本 → **还原剪贴板**。
184
- 用序列号而不是「文本变了没」,重复选同一段也能识别。
185
- * 三处保护:终端窗口(console/cascadia/mintty/putty)**不发** `Ctrl+C`(会变 SIGINT);
186
- 资源管理器/桌面/任务栏(Progman/WorkerW/CabinetWClass…)直接跳过(否则会真的复制文件、
187
- 污染 `Win+V` 剪贴板历史);用户已切窗就不发。
182
+ * **取选区三级阶梯**(`SelectionReader.ReadHit`),从最无侵入的一档开始:
183
+ 1. **UI Automation**(`TextPattern`)—— 不碰键盘、不碰剪贴板,还能拿到选区最后一行/一个字的矩形,
184
+ 药丸因此能精确贴在它右下角。Notepad、Word、WPS、VS Code、WinForms/WPF、DSH 自己都走这一档。
185
+ 2. **`WM_COPY`** —— 发给焦点控件,不发按键。部分老式编辑框只认这条。
186
+ 3. **合成一次 `Ctrl+C`** —— 只有前两档都拿不到时才用。**浏览器正文(Chromium)、部分 Electron 应用、
187
+ 某些游戏/自绘控件根本没有 `TextPattern` 也没有 `WM_COPY`,这是唯一可行的手段**。
188
+ * **绝不做「备份剪贴板 → 复制 → 还原」**。之前那套是"选中后复制不上"的元凶:还原那一步会把你刚按的
189
+ `Ctrl+C` 覆盖回旧内容。现在第三档会**把选区留在剪贴板里**(你在浏览器里划一下就发现 `Ctrl+V` 有内容了),
190
+ 代价是<u>占用</u>剪贴板,而不是<u>破坏</u>它 —— 这是读取 Chromium 选区的唯一代价,取舍写在明处。
191
+ * 设置页的「选区读取:辅助功能读不到时改用复制」**默认打开**;关掉后浏览器正文里就不会再弹按钮,
192
+ 但剪贴板绝对不会被碰(适合只用 Office/记事本、或对剪贴板敏感的场景)。
193
+ * 保护:按住 Ctrl/Alt/Shift 时的拖动直接跳过(那是复制/扩选,不是重新选词);终端
194
+ (console/cascadia/mintty/putty)不发按键;资源管理器/桌面/任务栏(Progman/WorkerW/CabinetWClass…)
195
+ 一律跳过(否则会真的复制文件、污染 `Win+V` 历史);用户按着修饰键期间完全不介入。
188
196
  * 药丸 9 秒无操作自动消失;托盘菜单「划词问大肥鱼」可随时关掉,状态存在
189
197
  `%LOCALAPPDATA%\dsh-hud\ask.txt`。
190
198
  * 因此**标题条 ✕ 只是收进托盘**(不是退出)——watcher 活在这个进程里,退出了就没法划词。
191
199
  彻底退出用托盘菜单「退出」。
192
- * 诊断:`hud.log` 里 `[pill]` 开头的行会写每次 `kick / skip / read N chars`;
193
- 往 `%LOCALAPPDATA%\dsh-hud\seltest.txt` 写任意内容可触发一次「只读不弹」的自检。
200
+ * 诊断:`hud.log` 里 `[pill]` 开头的行会写每次 `kick / skip / read N chars`,以及实际走了哪一档:
201
+ `copy: wm_copy N chars`、`copy: key_chord N chars seq A->B`(`0 chars` 且序号没动,说明那儿没有真选中
202
+ 或目标程序忽略了合成按键);往 `%LOCALAPPDATA%\dsh-hud\seltest.txt` 写任意内容可触发一次「只读不弹」的自检。
194
203
 
195
204
  ## 常驻划词服务、全局快捷键与设置页
196
205
 
@@ -222,6 +231,8 @@ DSH小窗.exe --service --url <tokenized> # Mutex Local\DshHudService,单
222
231
  主窗「设置 → DSH 小窗」:
223
232
 
224
233
  * 截图自动复制到剪贴板(默认开,整屏/框选都复制);
234
+ * 兼容模式:读不到选区时模拟 `Ctrl+C`(**默认关**。关着时只用系统辅助功能读选区,
235
+ 完全不碰剪贴板、不发按键;打开后老式控件才读得到,代价是可能和自己按的 `Ctrl+C` 抢剪贴板);
225
236
  * 全屏截图快捷键、框选截图快捷键(失焦即保存)。
226
237
 
227
238
  配置落在 `%LOCALAPPDATA%\dsh-hud\config.ini`(`key=value`,C# 壳、宿主插件、客户端三边都读),
@@ -256,9 +267,12 @@ cd ..; npm pack # → dsh-mini-0.1.0.tgz(约 600 KB)
256
267
  * 截图只有「整屏」和「框选/吸附窗口」两种,没有滚动长截图;框选吸附依赖窗口标题非空,
257
268
  无标题窗口(部分全屏游戏、自绘界面)会退化成「该点所在的整块屏幕」。
258
269
  (滚动长截图评估过:要拼接滚动帧 + 去重 + 处理懒加载,工作量明显大于本轮其它几项,先没做。)
259
- * 划词优先走 UI Automation `TextPattern.GetSelection()`(无侵入、能拿到选区矩形),
260
- 只有 UIA 读不到时才回退「发 Ctrl+C 读剪贴板」;终端(console/cascadia/mintty/putty)
261
- 和桌面/资源管理器窗口直接跳过,避免 Ctrl+C 变成 SIGINT 或真的复制文件。
270
+ * 划词按三级阶梯取选区:UIA `TextPattern`(无侵入、带选区矩形)→ `WM_COPY`(发消息、不发按键)→
271
+ 合成一次 `Ctrl+C`。**Chromium 正文没有 `TextPattern`、也不响应 `WM_COPY`**(我实测过 UIA 与
272
+ IAccessible2 两条路都拿不到选区),所以浏览器里必然走到第三档,会占用剪贴板;不想被碰可在设置页关掉
273
+ 「选区读取:辅助功能读不到时改用复制」。终端(console/cascadia/mintty/putty)和桌面/资源管理器窗口
274
+ 直接跳过,避免 Ctrl+C 变成 SIGINT 或真的复制文件。**任何情况都不再还原旧剪贴板内容**——那正是以前
275
+ 「选中后 Ctrl+C 复制不上」的根因。
262
276
  * 主题色与小窗边框:导航完成后同步两次,另外每 6 秒复查一次页面背景色;
263
277
  跨显示器拖到不同 DPI 的屏会收到 `WM_DPICHANGED` 并重算标题条/图标/最小尺寸。
264
278
  * 标题条图标是 `Segoe MDL2 Assets` 字体,字号偏小(E8BB 关闭键取 8.5f 才和其它图标一样大)。
package/app/HudWindow.cs CHANGED
@@ -228,6 +228,7 @@ namespace DshHud
228
228
 
229
229
  config = HudConfig.Load();
230
230
  configStamp = HudConfig.Stamp();
231
+ SelectionReader.ClipboardFallback = config.TextFallback;
231
232
  }
232
233
 
233
234
  private Size Logical(int w, int h)
@@ -567,7 +568,9 @@ namespace DshHud
567
568
  {
568
569
  configStamp = stamp;
569
570
  config = HudConfig.Load();
570
- Log("config reloaded copyShot=" + config.CopyShot);
571
+ SelectionReader.ClipboardFallback = config.TextFallback;
572
+ Log("config reloaded copyShot=" + config.CopyShot +
573
+ " textFallback=" + (config.TextFallback ? "1" : "0"));
571
574
  }
572
575
 
573
576
  string quote = HudConfig.QuotePath;
@@ -52,15 +52,92 @@ namespace DshHud
52
52
 
53
53
  internal const uint GA_ROOT = 2;
54
54
  internal const int VK_LBUTTON = 0x01;
55
- internal const byte VK_CONTROL = 0x11;
55
+ internal const int VK_SHIFT = 0x10;
56
+ internal const int VK_CONTROL = 0x11;
57
+ internal const int VK_MENU = 0x12;
56
58
  internal const byte VK_C = 0x43;
57
59
  internal const uint KEYEVENTF_KEYUP = 0x0002;
58
60
 
61
+ [StructLayout(LayoutKind.Sequential)]
62
+ internal struct GUITHREADINFO
63
+ {
64
+ public int cbSize;
65
+ public uint flags;
66
+ public IntPtr hwndActive;
67
+ public IntPtr hwndFocus;
68
+ public IntPtr hwndCapture;
69
+ public IntPtr hwndMenuOwner;
70
+ public IntPtr hwndMoveSize;
71
+ public IntPtr hwndCaret;
72
+ public RECT rcCaret;
73
+ }
74
+
75
+ [StructLayout(LayoutKind.Sequential)]
76
+ internal struct RECT
77
+ {
78
+ public int left;
79
+ public int top;
80
+ public int right;
81
+ public int bottom;
82
+ }
83
+
84
+ [DllImport("user32.dll")]
85
+ internal static extern bool GetGUIThreadInfo(uint idThread, ref GUITHREADINFO info);
86
+
87
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
88
+ internal static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint msg, IntPtr wParam,
89
+ IntPtr lParam, uint flags, uint timeout, out IntPtr result);
90
+
91
+ internal const uint WM_COPY = 0x0301;
92
+ internal const uint SMTO_ABORTIFHUNG = 0x0002;
93
+
59
94
  internal static bool IsMouseDown()
60
95
  {
61
96
  return (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0;
62
97
  }
63
98
 
99
+ internal static bool IsKeyDown(int vk)
100
+ {
101
+ return (GetAsyncKeyState(vk) & 0x8000) != 0;
102
+ }
103
+
104
+ /// <summary>
105
+ /// True while the user holds Ctrl/Alt/Shift himself. Every probe is skipped then:
106
+ /// injecting our own Ctrl+C on top of a real one is what made the user's copies
107
+ /// land empty, and it can also cancel a drag-copy in Explorer.
108
+ /// </summary>
109
+ internal static bool IsModifierDown()
110
+ {
111
+ return IsKeyDown(VK_CONTROL) || IsKeyDown(VK_MENU) || IsKeyDown(VK_SHIFT);
112
+ }
113
+
114
+ /// <summary>
115
+ /// The control that currently has the keyboard focus. WM_COPY has to be delivered to
116
+ /// the edit control, not the top-level window.
117
+ /// </summary>
118
+ internal static IntPtr FocusedControl()
119
+ {
120
+ try
121
+ {
122
+ IntPtr fg = GetForegroundWindow();
123
+ if (fg == IntPtr.Zero) return IntPtr.Zero;
124
+ uint pid;
125
+ uint tid = GetWindowThreadProcessId(fg, out pid);
126
+ GUITHREADINFO gti = new GUITHREADINFO();
127
+ gti.cbSize = Marshal.SizeOf(typeof(GUITHREADINFO));
128
+ if (GetGUIThreadInfo(tid, ref gti))
129
+ {
130
+ if (gti.hwndFocus != IntPtr.Zero) return gti.hwndFocus;
131
+ if (gti.hwndActive != IntPtr.Zero) return gti.hwndActive;
132
+ }
133
+ return fg;
134
+ }
135
+ catch
136
+ {
137
+ return IntPtr.Zero;
138
+ }
139
+ }
140
+
64
141
  /// <summary>
65
142
  /// Terminals map Ctrl+C to an interrupt rather than to "copy", so the clipboard
66
143
  /// fallback must never fire over one of them.
@@ -187,6 +264,28 @@ namespace DshHud
187
264
  return false;
188
265
  }
189
266
 
267
+ /// <summary>
268
+ /// Ask the focused control to copy through WM_COPY. This is the polite fallback:
269
+ /// no keystrokes are synthesised, so a real Ctrl+C pressed by the user is never
270
+ /// raced or cancelled, and nothing is written back over the clipboard.
271
+ /// </summary>
272
+ internal static bool SendCopyMessage()
273
+ {
274
+ try
275
+ {
276
+ IntPtr target = FocusedControl();
277
+ if (target == IntPtr.Zero) return false;
278
+ IntPtr result;
279
+ SendMessageTimeout(target, WM_COPY, IntPtr.Zero, IntPtr.Zero,
280
+ SMTO_ABORTIFHUNG, 300, out result);
281
+ return true;
282
+ }
283
+ catch
284
+ {
285
+ return false;
286
+ }
287
+ }
288
+
190
289
  internal static void SendCopy()
191
290
  {
192
291
  keybd_event(VK_CONTROL, 0, 0, UIntPtr.Zero);
@@ -218,6 +317,19 @@ namespace DshHud
218
317
 
219
318
  internal static class SelectionReader
220
319
  {
320
+ /// <summary>
321
+ /// On by default, but only ever consulted after UIA came back empty — so modern text
322
+ /// surfaces (Notepad, WinForms, WPF, Office, the browser's own chrome) never touch the
323
+ /// clipboard. Chromium content and legacy controls expose no TextPattern at all, and for
324
+ /// those a copy is the only way to read the selection.
325
+ ///
326
+ /// Crucially the copy is *not* undone: the old implementation restored the previous
327
+ /// clipboard contents afterwards, and that restore raced the user's own Ctrl+C and
328
+ /// overwrote what they had just copied. Losing the previous clipboard contents is the
329
+ /// price of reading a selection here; breaking Ctrl+C is not.
330
+ /// </summary>
331
+ internal static bool ClipboardFallback = true;
332
+
221
333
  /// <summary>
222
334
  /// Read the current selection. UIA first: it is passive (no keystrokes, no clipboard
223
335
  /// churn) and it also tells us where the selection ends, which is what the pill
@@ -275,7 +387,7 @@ namespace DshHud
275
387
  {
276
388
  }
277
389
 
278
- hit.Text = Read();
390
+ if (ClipboardFallback) hit.Text = Read();
279
391
  return hit;
280
392
  }
281
393
 
@@ -338,16 +450,30 @@ namespace DshHud
338
450
 
339
451
  private static string ReadOnStaThread()
340
452
  {
341
- IDataObject backup = null;
342
- try
343
- {
344
- backup = Clipboard.GetDataObject();
345
- }
346
- catch
453
+ // No clipboard backup/restore any more. Restoring was the actual damage: while
454
+ // we waited for our own copy to land, the user's real Ctrl+C could complete
455
+ // first, and our restore then threw that copy away ("复制不上"). Now the worst
456
+ // case is that the selection text stays on the clipboard — which is exactly
457
+ // what the user just asked for anyway.
458
+ if (ShellWin.IsModifierDown()) return "";
459
+
460
+ uint seq = ShellWin.GetClipboardSequenceNumber();
461
+ // WM_COPY is best effort only: Win32 edits answer it, Chromium and most
462
+ // modern renderers ignore it, and SendMessageTimeout also returns false
463
+ // when the target window is busy. Never treat "no answer" as "no selection".
464
+ ShellWin.SendCopyMessage();
465
+
466
+ string text = WaitForClipboardText(seq, 320);
467
+ if (!string.IsNullOrEmpty(text))
347
468
  {
469
+ SelectionWatcher.Log("copy: wm_copy " + text.Length + " chars");
470
+ return Finish(text);
348
471
  }
349
472
 
350
- uint seq = ShellWin.GetClipboardSequenceNumber();
473
+ // A real key chord is the only thing that works for Chromium content and
474
+ // legacy controls. Only when the user has opted in, and never while they
475
+ // are holding a modifier themselves.
476
+ if (!ClipboardFallback || ShellWin.IsModifierDown()) return "";
351
477
  try
352
478
  {
353
479
  ShellWin.SendCopy();
@@ -356,32 +482,35 @@ namespace DshHud
356
482
  {
357
483
  return "";
358
484
  }
485
+ text = WaitForClipboardText(seq, 700);
486
+ SelectionWatcher.Log("copy: key_chord " + (text == null ? 0 : text.Length) +
487
+ " chars seq " + seq + "->" + ShellWin.GetClipboardSequenceNumber());
488
+ return string.IsNullOrEmpty(text) ? "" : Finish(text);
489
+ }
359
490
 
360
- string text = null;
491
+ /// <summary>Wait until the clipboard sequence number moves, then read its text.</summary>
492
+ private static string WaitForClipboardText(uint seq, int budgetMs)
493
+ {
361
494
  int waited = 0;
362
- while (waited < 900)
495
+ while (waited < budgetMs)
363
496
  {
364
- Thread.Sleep(30);
365
- waited += 30;
497
+ Thread.Sleep(25);
498
+ waited += 25;
366
499
  if (ShellWin.GetClipboardSequenceNumber() == seq) continue;
367
500
  try
368
501
  {
369
- if (Clipboard.ContainsText()) text = Clipboard.GetText();
502
+ if (Clipboard.ContainsText()) return Clipboard.GetText();
370
503
  }
371
504
  catch
372
505
  {
373
506
  }
374
- break;
375
- }
376
-
377
- try
378
- {
379
- if (backup != null) Clipboard.SetDataObject(backup, false);
380
- }
381
- catch
382
- {
507
+ return "";
383
508
  }
509
+ return "";
510
+ }
384
511
 
512
+ private static string Finish(string text)
513
+ {
385
514
  if (string.IsNullOrEmpty(text)) return "";
386
515
  text = text.Replace("\r\n", "\n").Replace('\r', '\n').Trim();
387
516
  if (text.Length == 0) return "";
@@ -748,6 +877,9 @@ namespace DshHud
748
877
  private void Kick(Point pt, int delay)
749
878
  {
750
879
  if (!enabled) { Log("skip: disabled"); return; }
880
+ // A drag with Ctrl/Shift/Alt held is a copy or an extension, not a fresh
881
+ // selection: probing it races the user's own shortcut.
882
+ if (ShellWin.IsModifierDown()) { Log("skip: modifier down"); return; }
751
883
 
752
884
  if (ShellWin.IsShellSurfaceAt(pt)) { Log("skip: shell surface"); return; }
753
885
  int id = ++probeId;
package/app/Service.cs CHANGED
@@ -1,4 +1,4 @@
1
- using System;
1
+ using System;
2
2
  using System.Diagnostics;
3
3
  using System.Drawing;
4
4
  using System.IO;
@@ -19,6 +19,7 @@ namespace DshHud
19
19
  internal bool Ask;
20
20
  internal string HotkeyFull;
21
21
  internal string HotkeyRegion;
22
+ internal bool TextFallback;
22
23
 
23
24
  internal HudConfig()
24
25
  {
@@ -26,6 +27,7 @@ namespace DshHud
26
27
  Ask = true;
27
28
  HotkeyFull = "Ctrl+Shift+1";
28
29
  HotkeyRegion = "Ctrl+Shift+2";
30
+ TextFallback = true;
29
31
  }
30
32
 
31
33
  internal static string Dir
@@ -61,6 +63,7 @@ namespace DshHud
61
63
  else if (key == "ask") cfg.Ask = val != "0";
62
64
  else if (key == "hotkeyFull") cfg.HotkeyFull = val;
63
65
  else if (key == "hotkeyRegion") cfg.HotkeyRegion = val;
66
+ else if (key == "textFallback") cfg.TextFallback = val != "0";
64
67
  }
65
68
  }
66
69
  catch
@@ -79,6 +82,7 @@ namespace DshHud
79
82
  sb.AppendLine("ask=" + (Ask ? "1" : "0"));
80
83
  sb.AppendLine("hotkeyFull=" + HotkeyFull);
81
84
  sb.AppendLine("hotkeyRegion=" + HotkeyRegion);
85
+ sb.AppendLine("textFallback=" + (TextFallback ? "1" : "0"));
82
86
  File.WriteAllText(ConfigPath, sb.ToString(), Encoding.UTF8);
83
87
  }
84
88
  catch
@@ -250,6 +254,7 @@ namespace DshHud
250
254
  watcher = new SelectionWatcher(host);
251
255
  watcher.QuoteRequested += DeliverQuote;
252
256
  watcher.Enabled = config.Ask;
257
+ SelectionReader.ClipboardFallback = config.TextFallback;
253
258
 
254
259
  tray = new NotifyIcon();
255
260
  tray.Icon = SystemIcons.Application;
@@ -313,9 +318,11 @@ namespace DshHud
313
318
  config = HudConfig.Load();
314
319
  watcher.Enabled = config.Ask;
315
320
  askItem.Checked = config.Ask;
321
+ SelectionReader.ClipboardFallback = config.TextFallback;
316
322
  host.Bind(config);
317
323
  SelectionWatcher.Log("config reloaded: hotkeyFull=" + config.HotkeyFull +
318
- " hotkeyRegion=" + config.HotkeyRegion);
324
+ " hotkeyRegion=" + config.HotkeyRegion +
325
+ " textFallback=" + (config.TextFallback ? "1" : "0"));
319
326
  }
320
327
 
321
328
  private void OnHotkey(string kind)
package/lib/client.js CHANGED
@@ -551,6 +551,8 @@ window.__ModuleLoader__.load({
551
551
  react.createElement('h3', { style: { fontSize: '14px', margin: '0 0 4px' } }, '小窗与截图'),
552
552
  react.createElement('p', { style: hintStyle }, '设置对原生小窗和后台划词服务同时生效,保存后约 3 秒内应用。'),
553
553
  checkbox('copyShot', '截图自动复制到剪贴板', '全屏、框选两种截图都会复制'),
554
+ checkbox('textFallback', '选区读取:辅助功能读不到时改用复制',
555
+ '默认打开。先走系统辅助功能(不发按键、不碰剪贴板);浏览器正文、老控件没有这个接口,只能复制一次来读选区 —— 会占用剪贴板,但不会再还原,所以不会覆盖你刚按的 Ctrl+C。关掉后这些地方就不出按钮。'),
554
556
  hotkey('hotkeyFull', '全屏截图快捷键'),
555
557
  hotkey('hotkeyRegion', '框选截图快捷键'),
556
558
  react.createElement('p', { style: hintStyle }, '写法:Ctrl+Shift+1、Ctrl+Shift+S、Alt+PrintScreen、F8 等。组合里必须带 Ctrl/Alt/Shift/Win 之一。'),
package/lib/index.js CHANGED
@@ -202,7 +202,7 @@ function advertise(ctx) {
202
202
 
203
203
  const CONFIG_PATH = join(WORK_DIR, 'config.ini')
204
204
 
205
- const CONFIG_DEFAULTS = { copyShot: true, ask: true, hotkeyFull: 'Ctrl+Shift+1', hotkeyRegion: 'Ctrl+Shift+2' }
205
+ const CONFIG_DEFAULTS = { copyShot: true, ask: true, textFallback: true, hotkeyFull: 'Ctrl+Shift+1', hotkeyRegion: 'Ctrl+Shift+2' }
206
206
 
207
207
  /**
208
208
  * Settings live in a flat `key=value` file because the window shell (C# 5) and this
@@ -219,7 +219,7 @@ function readConfig() {
219
219
  if (eq <= 0) continue
220
220
  const key = line.slice(0, eq).trim()
221
221
  const value = line.slice(eq + 1).trim()
222
- if (key === 'copyShot' || key === 'ask') config[key] = value !== '0'
222
+ if (key === 'copyShot' || key === 'ask' || key === 'textFallback') config[key] = value !== '0'
223
223
  else if (key === 'hotkeyFull' || key === 'hotkeyRegion') config[key] = value
224
224
  }
225
225
  } catch {
@@ -235,6 +235,7 @@ function writeConfig(config) {
235
235
  `copyShot=${config.copyShot ? '1' : '0'}`,
236
236
  `ask=${config.ask ? '1' : '0'}`,
237
237
  `hotkeyFull=${config.hotkeyFull}`,
238
+ `textFallback=${config.textFallback ? '1' : '0'}`,
238
239
  `hotkeyRegion=${config.hotkeyRegion}`,
239
240
  ]
240
241
  writeFileSync(CONFIG_PATH, lines.join('\n') + '\n', 'utf8')
@@ -326,12 +327,13 @@ export function apply(ctx, config = {}) {
326
327
  let body
327
328
  try { body = await readJson(req) } catch (error) { sendJson(res, 400, { error: error.message }); return }
328
329
  const next = readConfig()
329
- for (const key of ['copyShot', 'ask', 'hotkeyFull', 'hotkeyRegion']) {
330
+ for (const key of ['copyShot', 'ask', 'textFallback', 'hotkeyFull', 'hotkeyRegion']) {
330
331
  if (Object.prototype.hasOwnProperty.call(body, key)) next[key] = body[key]
331
332
  }
332
333
  // Normalize before writing: the shell matches these strings exactly when it
333
334
  // registers the global hotkeys.
334
335
  next.copyShot = Boolean(next.copyShot)
336
+ next.textFallback = Boolean(next.textFallback)
335
337
  next.ask = Boolean(next.ask)
336
338
  next.hotkeyFull = String(next.hotkeyFull ?? '').trim() || CONFIG_DEFAULTS.hotkeyFull
337
339
  next.hotkeyRegion = String(next.hotkeyRegion ?? '').trim() || CONFIG_DEFAULTS.hotkeyRegion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mini",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "DSH 常驻小窗插件:把当前会话一键弹出为独立置顶小窗(WebView2 原生窗口),含整屏/框选截图、划词「问大肥鱼」、全局快捷键与截图自动复制",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",