dsh-mic-dictation 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-mic-dictation contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # dsh-mic-dictation
2
+
3
+ DeepSeek Harness 原生客户端插件:在 Web 提问栏的 **Full access 左侧** 插入一个麦克风按钮,点一下直接说话,识别文字写进提问框。
4
+
5
+ - 浏览器原生 `SpeechRecognition`,中文识别(`zh-CN`)
6
+ - 无需外部 API Key
7
+ - 纯客户端插件:host 半是空 `apply`,浏览器半通过 `dsh.client` 清单发货
8
+ - 用 `MutationObserver` 盯住 DSH 的 React DOM,把按钮稳定地放在 Full access(访问模式)按钮左边
9
+
10
+ ## 目录
11
+
12
+ ```text
13
+ dsh-mic-dictation/
14
+ ├── package.json # 插件清单 + dsh.client / dsh.bundle 声明
15
+ ├── cordis.patch.yml # bundle patch:把本插件插入 web profile
16
+ ├── lib/
17
+ │ ├── index.js # host half(空 apply)
18
+ │ ├── index.d.ts
19
+ │ ├── client.js # 浏览器 half(lazy-CJS factory bundle)
20
+ │ └── client.d.ts
21
+ ├── README.md
22
+ └── LICENSE
23
+ ```
24
+
25
+ ## 安装到自己的 DeepSeek Harness
26
+
27
+ 先发布到 npm 或 GitHub,然后用户在自己的终端里执行:
28
+
29
+ ```bash
30
+ # 从 npm 安装
31
+ dsh plugin --profile web add dsh-mic-dictation
32
+
33
+ # 或从 GitHub 安装(pnpm 会直接安装该仓库)
34
+ dsh plugin --profile web add github:你的用户名/dsh-mic-dictation
35
+ ```
36
+
37
+ 重启 Web:
38
+
39
+ ```bash
40
+ dsh web
41
+ ```
42
+
43
+ 刷新页面后,首次点麦克风并允许浏览器使用麦克风即可。
44
+
45
+ > 说明:当前 DSH 版本的「插件市场」是**只读清单**(设置 → 插件列表);安装入口是 `dsh plugin --profile web add ...`。等官方市场 UI 上线后,只要这个包发布在 npm/GitHub,就能被市场检索到。
46
+
47
+ ## 发布到 GitHub / npm
48
+
49
+ ### GitHub
50
+
51
+ 1. 把这个目录推到新仓库。
52
+ 2. 用户安装:`dsh plugin --profile web add github:OWNER/dsh-mic-dictation`。
53
+ 3. 仓库里必须保留 `cordis.patch.yml`、`lib/client.js` 和 `package.json`;它们是发布内容,不是构建产物。
54
+
55
+ ### npm
56
+
57
+ ```bash
58
+ npm login
59
+ npm publish --access public
60
+ ```
61
+
62
+ 之后用户直接 `dsh plugin --profile web add dsh-mic-dictation`。
63
+
64
+ ## 工作原理
65
+
66
+ 1. `cordis.patch.yml` 把 `dsh-mic-dictation` 作为一个 Loader entry 插进 profile。
67
+ 2. host 扫描该 entry,读到 `package.json` 的 `dsh.client` 声明,把 `exports["./client"]`(`lib/client.js`)作为客户端 bundle 提供给浏览器。
68
+ 3. 浏览器用 `window.__ModuleLoader__.load({ id, factory })` 注册这个 lazy-CJS 模块。
69
+ 4. `apply(ctx)` 在页面启动后挂载一个 DOM controller,找到 `[data-composer-card]` 里的 Full access / 访问模式按钮,把麦克风按钮插到它前面。
70
+ 5. 语音识别结果通过原生 value setter + `input` 事件写入 textarea,React 正常接管草稿。
71
+
72
+ ## 限制
73
+
74
+ - 需要 Chrome / Edge / Safari(`SpeechRecognition` / `webkitSpeechRecognition`)。
75
+ - 需要页面运行在 localhost / 127.0.0.1(浏览器的安全上下文要求)。
76
+ - Chrome 的语音识别依赖 Google 语音服务,需要联网。
77
+ - 如果 DSH 前端升级改变了 `data-composer-card` 或访问模式按钮的 `aria-label`,需要同步调整 `lib/client.js` 里的选择器。
@@ -0,0 +1,8 @@
1
+ # dsh-mic-dictation bundle patch.
2
+ # Installed as a profile bundle after dsh-web-app, this inserts the client
3
+ # plugin into the web profile's Loader tree. The host half is an empty apply;
4
+ # the browser half ships through exports["./client"] and is discovered from
5
+ # the "dsh.client" manifest in package.json.
6
+ - insert:
7
+ - id: mic-dictation
8
+ name: dsh-mic-dictation
@@ -0,0 +1,4 @@
1
+ /** Services required by this client plugin (none). */
2
+ export declare const inject: string[];
3
+ /** Mount the microphone dictation controller in the web composer. */
4
+ export declare function apply(ctx: unknown): void;
package/lib/client.js ADDED
@@ -0,0 +1,234 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-mic-dictation",
3
+ factory: function (require) {
4
+ "use strict";
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
+
9
+ var inject = [];
10
+
11
+ function apply(ctx) {
12
+ if (typeof window === "undefined" || typeof document === "undefined") return;
13
+ if (window.__dshMicDictationInstalled) return;
14
+ window.__dshMicDictationInstalled = true;
15
+
16
+ var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
17
+ if (!SR) {
18
+ console.warn("[dsh-mic-dictation] SpeechRecognition is not supported by this browser.");
19
+ return;
20
+ }
21
+
22
+ var style = document.createElement("style");
23
+ style.dataset.plugin = "dsh-mic-dictation";
24
+ style.dataset.pluginCss = "dsh-mic-dictation/client.css";
25
+ style.textContent = [
26
+ ".dsh-mic-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;flex:none;border:none;border-radius:999px;background:transparent;color:var(--dsw-alias-label-secondary,#6b7280);cursor:pointer;padding:0;margin:0;}",
27
+ ".dsh-mic-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,0.06));}",
28
+ ".dsh-mic-btn.listening{color:#ef4444;background:rgba(239,68,68,0.12);}",
29
+ ".dsh-mic-btn.listening svg{animation:dsh-mic-pulse 1.2s ease-in-out infinite;}",
30
+ "@keyframes dsh-mic-pulse{0%,100%{opacity:1;}50%{opacity:0.45;}}"
31
+ ].join("");
32
+ document.head.appendChild(style);
33
+
34
+ var button = document.createElement("button");
35
+ button.type = "button";
36
+ button.id = "dsh-mic-dictation-btn";
37
+ button.className = "dsh-mic-btn";
38
+ button.setAttribute("aria-label", "语音输入");
39
+ button.title = "语音输入";
40
+ button.innerHTML =
41
+ '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">' +
42
+ '<rect x="6" y="1.5" width="4" height="9" rx="2" stroke="currentColor" stroke-width="1.3"/>' +
43
+ '<path d="M3.5 7.5a4.5 4.5 0 0 0 9 0M8 12.5V15M5.5 15h5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>' +
44
+ "</svg>";
45
+
46
+ var recognition = null;
47
+ var listening = false;
48
+ var scheduled = false;
49
+ var syncTimer = 0;
50
+
51
+ function findAccessButton() {
52
+ var cards = document.querySelectorAll("[data-composer-card]");
53
+ for (var i = 0; i < cards.length; i++) {
54
+ var btn = cards[i].querySelector(
55
+ 'button[aria-label^="Access mode"], button[aria-label^="访问模式"]'
56
+ );
57
+ if (btn) return btn;
58
+ }
59
+ return null;
60
+ }
61
+
62
+ function scheduleSync() {
63
+ if (scheduled) return;
64
+ scheduled = true;
65
+ requestAnimationFrame(function () {
66
+ scheduled = false;
67
+ syncPlacement();
68
+ });
69
+ }
70
+
71
+ function syncPlacement() {
72
+ var target = findAccessButton();
73
+ if (!target) {
74
+ if (button.isConnected) button.remove();
75
+ return;
76
+ }
77
+ var parent = target.parentElement;
78
+ if (!parent) return;
79
+ if (button.parentElement !== parent || button.nextElementSibling !== target) {
80
+ target.insertAdjacentElement("beforebegin", button);
81
+ }
82
+ }
83
+
84
+ function setListening(on) {
85
+ button.classList.toggle("listening", on);
86
+ button.title = on ? "正在聆听,点击停止" : "语音输入";
87
+ }
88
+
89
+ function setNativeValue(el, value) {
90
+ var proto = el instanceof HTMLTextAreaElement
91
+ ? HTMLTextAreaElement.prototype
92
+ : HTMLInputElement.prototype;
93
+ var desc = Object.getOwnPropertyDescriptor(proto, "value");
94
+ if (desc && desc.set) desc.set.call(el, value);
95
+ else el.value = value;
96
+ el.dispatchEvent(new Event("input", { bubbles: true }));
97
+ }
98
+
99
+ function appendTranscript(text) {
100
+ var clean = String(text || "").trim();
101
+ if (!clean) return;
102
+ var card = button.closest("[data-composer-card]");
103
+ var textarea = card ? card.querySelector("textarea") : null;
104
+ if (!textarea) return;
105
+
106
+ var start = textarea.selectionStart != null
107
+ ? textarea.selectionStart
108
+ : textarea.value.length;
109
+ var end = textarea.selectionEnd != null ? textarea.selectionEnd : start;
110
+ var next = textarea.value.slice(0, start) + clean + textarea.value.slice(end);
111
+ setNativeValue(textarea, next);
112
+
113
+ var caret = start + clean.length;
114
+ try { textarea.setSelectionRange(caret, caret); } catch (_) {}
115
+ try { textarea.focus({ preventScroll: true }); } catch (_) { textarea.focus(); }
116
+ }
117
+
118
+ function stop() {
119
+ listening = false;
120
+ setListening(false);
121
+ if (recognition) {
122
+ try { recognition.stop(); } catch (_) {}
123
+ }
124
+ }
125
+
126
+ function start() {
127
+ if (listening) return;
128
+ var rec = new SR();
129
+ rec.lang = "zh-CN";
130
+ rec.continuous = true;
131
+ rec.interimResults = true;
132
+ recognition = rec;
133
+ listening = true;
134
+ setListening(true);
135
+
136
+ rec.onresult = function (event) {
137
+ var interim = "";
138
+ for (var i = event.resultIndex; i < event.results.length; i++) {
139
+ var result = event.results[i];
140
+ if (result.isFinal) {
141
+ appendTranscript(result[0].transcript);
142
+ } else {
143
+ interim += result[0].transcript;
144
+ }
145
+ }
146
+ if (interim) button.title = interim;
147
+ };
148
+
149
+ rec.onerror = function (event) {
150
+ console.warn("[dsh-mic-dictation] recognition error:", event.error, event.message || "");
151
+ if (event.error === "not-allowed" || event.error === "service-not-allowed") {
152
+ listening = false;
153
+ setListening(false);
154
+ button.title = "麦克风权限被拒绝,请在浏览器地址栏允许麦克风";
155
+ } else if (event.error === "audio-capture") {
156
+ listening = false;
157
+ setListening(false);
158
+ button.title = "未检测到麦克风设备";
159
+ } else if (event.error === "no-speech") {
160
+ button.title = "没有听到语音,请再试一次";
161
+ } else if (event.error === "network") {
162
+ listening = false;
163
+ setListening(false);
164
+ button.title = "网络错误,语音识别不可用";
165
+ } else if (event.error === "aborted") {
166
+ // 主动停止或页面切换,交给 onend 处理。
167
+ } else {
168
+ listening = false;
169
+ setListening(false);
170
+ button.title = "语音识别错误:" + event.error;
171
+ }
172
+ };
173
+
174
+ rec.onend = function () {
175
+ if (listening) {
176
+ try {
177
+ rec.start();
178
+ } catch (err) {
179
+ listening = false;
180
+ setListening(false);
181
+ button.title = "语音识别已结束";
182
+ }
183
+ } else {
184
+ setListening(false);
185
+ var keep = button.title.indexOf("麦克风权限") === 0
186
+ || button.title.indexOf("网络") === 0
187
+ || button.title.indexOf("未检测到") === 0
188
+ || button.title.indexOf("语音识别错误") === 0
189
+ || button.title.indexOf("没有听到") === 0;
190
+ if (!keep) button.title = "语音输入";
191
+ }
192
+ };
193
+
194
+ try {
195
+ rec.start();
196
+ } catch (err) {
197
+ listening = false;
198
+ setListening(false);
199
+ button.title = "无法启动语音识别:" + err.message;
200
+ }
201
+ }
202
+
203
+ button.addEventListener("click", function (event) {
204
+ event.preventDefault();
205
+ event.stopPropagation();
206
+ if (listening) stop();
207
+ else start();
208
+ });
209
+
210
+ var observer = new MutationObserver(function () { scheduleSync(); });
211
+ observer.observe(document.documentElement, { childList: true, subtree: true });
212
+ syncTimer = window.setInterval(syncPlacement, 1000);
213
+ scheduleSync();
214
+
215
+ ctx.effect(function () {
216
+ return function () {
217
+ observer.disconnect();
218
+ window.clearInterval(syncTimer);
219
+ if (recognition) {
220
+ try { recognition.abort(); } catch (_) {}
221
+ }
222
+ if (button.isConnected) button.remove();
223
+ if (style.isConnected) style.remove();
224
+ window.__dshMicDictationInstalled = false;
225
+ };
226
+ }, "dsh-mic-dictation: DOM controller");
227
+ }
228
+
229
+ exports.apply = apply;
230
+ exports.inject = inject;
231
+ return module.exports;
232
+ }
233
+ });
234
+ //# sourceMappingURL=client.js.map
package/lib/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Host plugin body — no host-side behavior for this client-only plugin. */
2
+ export declare function apply(): void;
package/lib/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * dsh-mic-dictation, host half.
3
+ *
4
+ * The empty apply exists so this package is a real Loader entry on the host
5
+ * and the client half is discovered through the package.json "dsh.client"
6
+ * declaration. All behavior lives in the browser half (./client).
7
+ */
8
+ export function apply() {}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "dsh-mic-dictation",
3
+ "version": "0.1.0",
4
+ "description": "DeepSeek Harness client plugin: a microphone dictation button to the left of the Full access control in the web composer.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./client": {
14
+ "types": "./lib/client.d.ts",
15
+ "default": "./lib/client.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "lib",
21
+ "cordis.patch.yml",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "dsh": {
26
+ "bundle": {
27
+ "patch": "./cordis.patch.yml"
28
+ },
29
+ "client": {
30
+ "inject": [],
31
+ "platform": "web"
32
+ }
33
+ },
34
+ "peerDependencies": {
35
+ "@deepseek-ai/cordis": "^4.0.1"
36
+ },
37
+ "license": "MIT",
38
+ "keywords": [
39
+ "deepseek-harness",
40
+ "dsh",
41
+ "plugin",
42
+ "voice",
43
+ "dictation",
44
+ "speech-recognition"
45
+ ]
46
+ }