hiwork-core 0.0.24 → 0.0.30

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/lib/client.js CHANGED
@@ -112,6 +112,235 @@ var interactionCss = [
112
112
  // 品牌蓝按钮(登录门禁)hover:提亮而非换底色,保留渐变。
113
113
  `[data-hiwork-core-brand]:hover:not(:disabled) { filter: brightness(1.08) !important; }`
114
114
  ].join("\n");
115
+ var CORE_STYLE_OWNER = "hiwork-core";
116
+ function ownStyleTag(style, cssTagId) {
117
+ style.setAttribute("data-plugin", CORE_STYLE_OWNER);
118
+ style.setAttribute("data-plugin-css", cssTagId);
119
+ }
120
+ var voiceCss = [
121
+ /* ---- 工具栏话筒按钮 ---- */
122
+ `.hiwv-micBtn {`,
123
+ ` display: inline-flex;`,
124
+ ` align-items: center;`,
125
+ ` justify-content: center;`,
126
+ ` width: 28px;`,
127
+ ` height: 28px;`,
128
+ ` padding: 0;`,
129
+ ` border: none;`,
130
+ ` border-radius: 8px;`,
131
+ ` background: transparent;`,
132
+ ` color: inherit;`,
133
+ ` cursor: pointer;`,
134
+ ` opacity: 0.85;`,
135
+ ` flex: none;`,
136
+ `}`,
137
+ `.hiwv-micBtn:hover {`,
138
+ ` opacity: 1;`,
139
+ ` background: rgba(127, 127, 127, 0.14);`,
140
+ `}`,
141
+ `.hiwv-micBtn:active {`,
142
+ ` background: rgba(127, 127, 127, 0.22);`,
143
+ `}`,
144
+ /* ---- 录音浮层 ---- */
145
+ `.hiwv-backdrop {`,
146
+ ` position: fixed;`,
147
+ ` inset: 0;`,
148
+ ` z-index: 2147483000;`,
149
+ ` display: flex;`,
150
+ ` align-items: center;`,
151
+ ` justify-content: center;`,
152
+ ` background: rgba(0, 0, 0, 0.45);`,
153
+ ` animation: hiwv-fade-in 140ms ease-out;`,
154
+ `}`,
155
+ `@keyframes hiwv-fade-in {`,
156
+ ` from { opacity: 0; }`,
157
+ ` to { opacity: 1; }`,
158
+ `}`,
159
+ `.hiwv-panel {`,
160
+ ` display: flex;`,
161
+ ` flex-direction: column;`,
162
+ ` width: min(560px, calc(100vw - 64px));`,
163
+ ` min-width: 420px;`,
164
+ ` border-radius: 16px;`,
165
+ ` padding: 24px 28px 22px;`,
166
+ ` background: #ffffff;`,
167
+ ` color: #1f2329;`,
168
+ ` box-shadow: 0 18px 60px rgba(0, 0, 0, 0.28);`,
169
+ ` animation: hiwv-pop-in 160ms cubic-bezier(0.2, 0.9, 0.3, 1.2);`,
170
+ `}`,
171
+ `@keyframes hiwv-pop-in {`,
172
+ ` from { transform: scale(0.96); opacity: 0; }`,
173
+ ` to { transform: scale(1); opacity: 1; }`,
174
+ `}`,
175
+ `.hiwv-head {`,
176
+ ` display: flex;`,
177
+ ` align-items: baseline;`,
178
+ ` justify-content: space-between;`,
179
+ ` gap: 12px;`,
180
+ `}`,
181
+ `.hiwv-title {`,
182
+ ` font-size: 17px;`,
183
+ ` font-weight: 600;`,
184
+ `}`,
185
+ `.hiwv-hint {`,
186
+ ` font-size: 12px;`,
187
+ ` color: #8a8f99;`,
188
+ ` font-variant-numeric: tabular-nums;`,
189
+ `}`,
190
+ `.hiwv-stage {`,
191
+ ` display: flex;`,
192
+ ` flex-direction: column;`,
193
+ ` align-items: center;`,
194
+ ` justify-content: center;`,
195
+ ` gap: 20px;`,
196
+ ` min-height: 216px;`,
197
+ ` padding: 30px 8px 22px;`,
198
+ `}`,
199
+ `.hiwv-stage.hiwv-stageIdle {`,
200
+ ` gap: 14px;`,
201
+ `}`,
202
+ `.hiwv-orb {`,
203
+ ` width: 76px;`,
204
+ ` height: 76px;`,
205
+ ` border-radius: 50%;`,
206
+ ` display: flex;`,
207
+ ` align-items: center;`,
208
+ ` justify-content: center;`,
209
+ ` color: #ffffff;`,
210
+ ` background: #3370ff;`,
211
+ ` animation: hiwv-orb-pulse 1.6s ease-out infinite;`,
212
+ ` flex: none;`,
213
+ `}`,
214
+ `.hiwv-orb.hiwv-orbBusy {`,
215
+ ` animation: hiwv-orb-breathe 1.2s ease-in-out infinite;`,
216
+ `}`,
217
+ `@keyframes hiwv-orb-pulse {`,
218
+ ` 0% { box-shadow: 0 0 0 0 rgba(51, 112, 255, 0.35); }`,
219
+ ` 70% { box-shadow: 0 0 0 22px rgba(51, 112, 255, 0); }`,
220
+ ` 100% { box-shadow: 0 0 0 0 rgba(51, 112, 255, 0); }`,
221
+ `}`,
222
+ `@keyframes hiwv-orb-breathe {`,
223
+ ` 0%, 100% { opacity: 1; }`,
224
+ ` 50% { opacity: 0.65; }`,
225
+ `}`,
226
+ `.hiwv-bars {`,
227
+ ` display: flex;`,
228
+ ` align-items: center;`,
229
+ ` gap: 3px;`,
230
+ ` height: 56px;`,
231
+ `}`,
232
+ `.hiwv-bars i {`,
233
+ ` display: block;`,
234
+ ` width: 4px;`,
235
+ ` min-height: 8px;`,
236
+ ` border-radius: 2px;`,
237
+ ` background: #3370ff;`,
238
+ ` opacity: 0.85;`,
239
+ ` transition: height 80ms linear;`,
240
+ `}`,
241
+ `.hiwv-bars i.hiwv-barIdle {`,
242
+ ` background: #b9c0cc;`,
243
+ ` opacity: 0.5;`,
244
+ `}`,
245
+ `.hiwv-status {`,
246
+ ` font-size: 13px;`,
247
+ ` color: #5f6672;`,
248
+ `}`,
249
+ /* ---- 错误 / 结果态 ---- */
250
+ `.hiwv-errorBox {`,
251
+ ` display: flex;`,
252
+ ` flex-direction: column;`,
253
+ ` align-items: center;`,
254
+ ` gap: 14px;`,
255
+ ` width: 100%;`,
256
+ `}`,
257
+ `.hiwv-errorMsg {`,
258
+ ` font-size: 14px;`,
259
+ ` line-height: 1.6;`,
260
+ ` color: #b3261e;`,
261
+ ` text-align: center;`,
262
+ ` max-width: 420px;`,
263
+ `}`,
264
+ `.hiwv-recognized {`,
265
+ ` display: flex;`,
266
+ ` flex-direction: column;`,
267
+ ` gap: 10px;`,
268
+ ` width: 100%;`,
269
+ ` max-width: 460px;`,
270
+ ` padding: 14px;`,
271
+ ` border-radius: 10px;`,
272
+ ` background: rgba(127, 127, 127, 0.08);`,
273
+ `}`,
274
+ `.hiwv-recognizedText {`,
275
+ ` font-size: 14px;`,
276
+ ` line-height: 1.6;`,
277
+ ` max-height: 96px;`,
278
+ ` overflow: auto;`,
279
+ ` white-space: pre-wrap;`,
280
+ ` word-break: break-word;`,
281
+ `}`,
282
+ /* ---- 操作区 ---- */
283
+ `.hiwv-actions {`,
284
+ ` display: flex;`,
285
+ ` justify-content: flex-end;`,
286
+ ` gap: 10px;`,
287
+ ` padding-top: 4px;`,
288
+ `}`,
289
+ `.hiwv-btn {`,
290
+ ` appearance: none;`,
291
+ ` border-radius: 8px;`,
292
+ ` padding: 7px 20px;`,
293
+ ` font-size: 13px;`,
294
+ ` line-height: 20px;`,
295
+ ` cursor: pointer;`,
296
+ ` border: 1px solid transparent;`,
297
+ `}`,
298
+ `.hiwv-btn:disabled {`,
299
+ ` cursor: default;`,
300
+ ` opacity: 0.55;`,
301
+ `}`,
302
+ `.hiwv-btnPrimary {`,
303
+ ` background: #3370ff;`,
304
+ ` color: #ffffff;`,
305
+ `}`,
306
+ `.hiwv-btnPrimary:hover:not(:disabled) {`,
307
+ ` background: #2b5fd9;`,
308
+ `}`,
309
+ `.hiwv-btnSecondary {`,
310
+ ` background: transparent;`,
311
+ ` color: inherit;`,
312
+ ` border-color: rgba(127, 127, 127, 0.35);`,
313
+ `}`,
314
+ `.hiwv-btnSecondary:hover:not(:disabled) {`,
315
+ ` background: rgba(127, 127, 127, 0.1);`,
316
+ `}`,
317
+ /* ---- 深色主题 ---- */
318
+ `@media (prefers-color-scheme: dark) {`,
319
+ ` .hiwv-panel {`,
320
+ ` background: #24262b;`,
321
+ ` color: #e8eaed;`,
322
+ ` }`,
323
+ ` .hiwv-hint,`,
324
+ ` .hiwv-status {`,
325
+ ` color: #9aa0ab;`,
326
+ ` }`,
327
+ ` .hiwv-bars i.hiwv-barIdle {`,
328
+ ` background: #565b66;`,
329
+ ` }`,
330
+ ` .hiwv-btnSecondary {`,
331
+ ` border-color: rgba(255, 255, 255, 0.22);`,
332
+ ` }`,
333
+ ` .hiwv-btnSecondary:hover:not(:disabled) {`,
334
+ ` background: rgba(255, 255, 255, 0.08);`,
335
+ ` }`,
336
+ ` .hiwv-recognized {`,
337
+ ` background: rgba(255, 255, 255, 0.07);`,
338
+ ` }`,
339
+ ` .hiwv-errorMsg {`,
340
+ ` color: #ff8a80;`,
341
+ ` }`,
342
+ `}`
343
+ ].join("\n");
115
344
 
116
345
  // src/client/controller.ts
117
346
  var import_dsh_client_store = require("@deepseek-ai/dsh-client-store");
@@ -1285,6 +1514,7 @@ function ensureHeroStyle(doc = document) {
1285
1514
  if (doc.head === null) return;
1286
1515
  const style = doc.createElement("style");
1287
1516
  style.id = HERO_STYLE_ID;
1517
+ ownStyleTag(style, "hiwork-core/hero-greeting.css");
1288
1518
  style.textContent = HERO_STYLE;
1289
1519
  doc.head.append(style);
1290
1520
  }
@@ -1337,6 +1567,7 @@ function ensureManualStyle(doc) {
1337
1567
  if (doc.head === null) return;
1338
1568
  const style = doc.createElement("style");
1339
1569
  style.id = MANUAL_STYLE_ID;
1570
+ ownStyleTag(style, "hiwork-core/manual-link.css");
1340
1571
  style.textContent = MANUAL_STYLE;
1341
1572
  doc.head.append(style);
1342
1573
  }
@@ -1425,6 +1656,7 @@ function ensureDailyCardStyle(doc) {
1425
1656
  if (doc.head === null) return;
1426
1657
  const style = doc.createElement("style");
1427
1658
  style.id = DAILY_CARD_STYLE_ID;
1659
+ ownStyleTag(style, "hiwork-core/daily-card.css");
1428
1660
  style.textContent = DAILY_CARD_STYLE;
1429
1661
  doc.head.append(style);
1430
1662
  }
@@ -2856,6 +3088,7 @@ function mountFeatureCenter(options) {
2856
3088
  const style = document.createElement("style");
2857
3089
  style.id = STYLE_ELEMENT_ID;
2858
3090
  style.setAttribute(OWNER_ATTRIBUTE, owner);
3091
+ ownStyleTag(style, "hiwork-core/feature-center.css");
2859
3092
  style.textContent = featureCenterCss;
2860
3093
  document.head.appendChild(style);
2861
3094
  let menuElement;
@@ -4171,6 +4404,734 @@ var PmsTodoStore = class {
4171
4404
  }
4172
4405
  };
4173
4406
 
4407
+ // src/client/voice/MicButton.tsx
4408
+ var import_react8 = require("react");
4409
+ var import_jsx_runtime8 = require("react/jsx-runtime");
4410
+ function MicIcon({ size }) {
4411
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
4412
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: "9", y: "2.75", width: "6", height: "11.5", rx: "3", stroke: "currentColor", strokeWidth: "1.6" }),
4413
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M5.5 11.5a6.5 6.5 0 0 0 13 0", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }),
4414
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M12 18v3.25", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" })
4415
+ ] });
4416
+ }
4417
+ function MicButton({ open }) {
4418
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4419
+ "button",
4420
+ {
4421
+ type: "button",
4422
+ className: "hiwv-micBtn",
4423
+ title: "\u8BED\u97F3\u8F93\u5165",
4424
+ "aria-label": "\u8BED\u97F3\u8F93\u5165",
4425
+ "aria-haspopup": "dialog",
4426
+ onClick: () => open(),
4427
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(MicIcon, { size: 18 })
4428
+ }
4429
+ );
4430
+ }
4431
+
4432
+ // src/client/voice/entry.ts
4433
+ var import_react10 = require("react");
4434
+ var import_client3 = require("react-dom/client");
4435
+
4436
+ // src/client/voice/composer.ts
4437
+ var WRITABLE_PHASES = /* @__PURE__ */ new Set(["plain", "claimed"]);
4438
+ function detectTextLength(state) {
4439
+ const occurrences = state.occurrences;
4440
+ if (occurrences === void 0) return state.draft.length;
4441
+ let folded = 0;
4442
+ for (const occurrence of occurrences) {
4443
+ if (occurrence.length > 1) folded += occurrence.length - 1;
4444
+ }
4445
+ return Math.max(0, state.draft.length - folded);
4446
+ }
4447
+ function insertDraftText(sessions, text6, sessionId) {
4448
+ if (sessions === void 0) return { ok: false, reason: "no-target" };
4449
+ let target;
4450
+ try {
4451
+ target = sessionId ?? sessions.list?.getSnapshot().current;
4452
+ } catch {
4453
+ return { ok: false, reason: "no-target" };
4454
+ }
4455
+ if (target === void 0) return { ok: false, reason: "no-target" };
4456
+ let actx;
4457
+ try {
4458
+ actx = sessions.scope?.(target) ?? sessions.binding?.(target)?.ctx;
4459
+ } catch {
4460
+ return { ok: false, reason: "no-target" };
4461
+ }
4462
+ if (actx === void 0 || actx === null) return { ok: false, reason: "no-target" };
4463
+ let surface;
4464
+ try {
4465
+ const conversation = actx.get?.("conversation");
4466
+ surface = conversation?.input.for(actx);
4467
+ } catch {
4468
+ return { ok: false, reason: "no-target" };
4469
+ }
4470
+ if (surface === void 0) return { ok: false, reason: "no-target" };
4471
+ if (typeof surface.insertText !== "function") return { ok: false, reason: "no-target" };
4472
+ let snapshot;
4473
+ try {
4474
+ snapshot = surface.state.getSnapshot();
4475
+ } catch {
4476
+ return { ok: false, reason: "no-target" };
4477
+ }
4478
+ if (snapshot.phase !== void 0 && !WRITABLE_PHASES.has(snapshot.phase)) return { ok: false, reason: "busy" };
4479
+ const end = detectTextLength(snapshot);
4480
+ try {
4481
+ const inserted = surface.insertText(text6, { start: end, end, draftRev: snapshot.draftRev });
4482
+ return inserted ? { ok: true } : { ok: false, reason: "rejected" };
4483
+ } catch {
4484
+ return { ok: false, reason: "rejected" };
4485
+ }
4486
+ }
4487
+
4488
+ // src/client/voice/VoiceOverlay.tsx
4489
+ var import_react9 = require("react");
4490
+
4491
+ // src/client/voice/recording.ts
4492
+ var RecordingError = class extends Error {
4493
+ constructor(kind) {
4494
+ super(kind);
4495
+ this.kind = kind;
4496
+ this.name = "RecordingError";
4497
+ }
4498
+ };
4499
+ function encodeWave(samples) {
4500
+ const bytes = new Uint8Array(44 + samples.length * 2);
4501
+ const view = new DataView(bytes.buffer);
4502
+ const text6 = (at, value) => {
4503
+ for (let i = 0; i < value.length; i++) bytes[at + i] = value.charCodeAt(i);
4504
+ };
4505
+ text6(0, "RIFF");
4506
+ view.setUint32(4, bytes.length - 8, true);
4507
+ text6(8, "WAVE");
4508
+ text6(12, "fmt ");
4509
+ view.setUint32(16, 16, true);
4510
+ view.setUint16(20, 1, true);
4511
+ view.setUint16(22, 1, true);
4512
+ view.setUint32(24, 16e3, true);
4513
+ view.setUint32(28, 32e3, true);
4514
+ view.setUint16(32, 2, true);
4515
+ view.setUint16(34, 16, true);
4516
+ text6(36, "data");
4517
+ view.setUint32(40, samples.length * 2, true);
4518
+ for (const [i, sample] of samples.entries()) {
4519
+ const value = Math.max(-1, Math.min(1, sample));
4520
+ view.setInt16(44 + i * 2, Math.round(value * (value < 0 ? 32768 : 32767)), true);
4521
+ }
4522
+ return bytes;
4523
+ }
4524
+ function audioBase64(bytes) {
4525
+ let text6 = "";
4526
+ for (let i = 0; i < bytes.length; i += 8192) text6 += String.fromCharCode(...bytes.subarray(i, i + 8192));
4527
+ return btoa(text6);
4528
+ }
4529
+ function isSilentWav(bytes, threshold = 100) {
4530
+ for (let i = 44; i + 1 < bytes.length; i += 2) {
4531
+ const sample = (bytes[i] ?? 0) | (bytes[i + 1] ?? 0) << 8;
4532
+ if (Math.abs(sample) > threshold) return false;
4533
+ }
4534
+ return true;
4535
+ }
4536
+ var Recording = class {
4537
+ constructor(onDispose) {
4538
+ this.onDispose = onDispose;
4539
+ }
4540
+ stream;
4541
+ recorder;
4542
+ context;
4543
+ analyser;
4544
+ samples = new Float32Array(256);
4545
+ chunks = [];
4546
+ lifetime = new AbortController();
4547
+ disposal;
4548
+ /**
4549
+ * 申请麦克风并开始录音。
4550
+ * @param onError - 采集途中的失败回调(在异步资源释放完成前触发)。
4551
+ */
4552
+ async start(onError) {
4553
+ const devices = navigator.mediaDevices;
4554
+ if (!devices || typeof MediaRecorder === "undefined") throw new RecordingError("unavailable");
4555
+ let stream;
4556
+ try {
4557
+ stream = await devices.getUserMedia({
4558
+ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
4559
+ video: false
4560
+ });
4561
+ } catch (error) {
4562
+ if (error instanceof DOMException && error.name === "NotAllowedError") throw new RecordingError("permission");
4563
+ throw error;
4564
+ }
4565
+ if (this.lifetime.signal.aborted) {
4566
+ stream.getTracks().forEach((track) => {
4567
+ track.stop();
4568
+ });
4569
+ throw new RecordingError("cancelled");
4570
+ }
4571
+ this.stream = stream;
4572
+ try {
4573
+ this.context = new AudioContext();
4574
+ if (this.context.state === "suspended") {
4575
+ await this.context.resume().catch(() => void 0);
4576
+ }
4577
+ this.analyser = this.context.createAnalyser();
4578
+ this.analyser.fftSize = this.samples.length;
4579
+ this.context.createMediaStreamSource(stream).connect(this.analyser);
4580
+ this.recorder = new MediaRecorder(stream);
4581
+ this.recorder.ondataavailable = (event) => {
4582
+ if (!this.lifetime.signal.aborted && event.data.size > 0) this.chunks.push(event.data);
4583
+ };
4584
+ this.recorder.onerror = () => {
4585
+ if (this.lifetime.signal.aborted) return;
4586
+ void this.dispose().catch(() => void 0);
4587
+ try {
4588
+ onError?.(new RecordingError("interrupted"));
4589
+ } catch (error) {
4590
+ console.error("Voice recording error handler failed", error);
4591
+ }
4592
+ };
4593
+ this.recorder.start();
4594
+ } catch (error) {
4595
+ await this.dispose();
4596
+ throw error;
4597
+ }
4598
+ }
4599
+ /** 读实时麦克风电平(RMS,0~1 粗略值);不在采集态时恒为 0。 */
4600
+ amplitude() {
4601
+ if (!this.analyser) return 0;
4602
+ this.analyser.getFloatTimeDomainData(this.samples);
4603
+ let sum2 = 0;
4604
+ for (const sample of this.samples) sum2 += sample * sample;
4605
+ return Math.sqrt(sum2 / this.samples.length);
4606
+ }
4607
+ /**
4608
+ * 结束录音并重采样成 WAV。
4609
+ * @param maxDurationSeconds - 截掉超出上限的部分(定时器漂移保护)。
4610
+ */
4611
+ async stop(maxDurationSeconds) {
4612
+ const recorder = this.recorder;
4613
+ const context = this.context;
4614
+ if (!recorder || !context || recorder.state !== "recording") {
4615
+ await this.dispose();
4616
+ throw new RecordingError("empty");
4617
+ }
4618
+ try {
4619
+ await new Promise((resolve, reject) => {
4620
+ recorder.onstop = () => {
4621
+ resolve();
4622
+ };
4623
+ recorder.onerror = () => {
4624
+ reject(new RecordingError("empty"));
4625
+ };
4626
+ recorder.stop();
4627
+ });
4628
+ this.stream?.getTracks().forEach((track) => {
4629
+ track.stop();
4630
+ });
4631
+ this.lifetime.signal.throwIfAborted();
4632
+ const blob = new Blob(this.chunks, { type: recorder.mimeType });
4633
+ if (blob.size === 0) throw new RecordingError("empty");
4634
+ const decoded = await context.decodeAudioData(await blob.arrayBuffer());
4635
+ this.lifetime.signal.throwIfAborted();
4636
+ const offline = new OfflineAudioContext(
4637
+ 1,
4638
+ Math.max(1, Math.floor(Math.min(decoded.duration, maxDurationSeconds) * 16e3)),
4639
+ 16e3
4640
+ );
4641
+ const source = offline.createBufferSource();
4642
+ source.buffer = decoded;
4643
+ source.connect(offline.destination);
4644
+ source.start();
4645
+ const resampled = await offline.startRendering();
4646
+ this.lifetime.signal.throwIfAborted();
4647
+ return encodeWave(resampled.getChannelData(0));
4648
+ } finally {
4649
+ await this.dispose();
4650
+ }
4651
+ }
4652
+ /** 释放采集资源并作废未完成的授权竞态;重复调用安全。 */
4653
+ dispose() {
4654
+ if (!this.disposal) {
4655
+ let resolve;
4656
+ let reject;
4657
+ const closing = new Promise((res, rej) => {
4658
+ resolve = res;
4659
+ reject = rej;
4660
+ });
4661
+ this.disposal = closing;
4662
+ void this.release().then(resolve, reject);
4663
+ }
4664
+ return this.disposal;
4665
+ }
4666
+ async release() {
4667
+ this.lifetime.abort(new RecordingError("cancelled"));
4668
+ if (this.recorder?.state === "recording") this.recorder.stop();
4669
+ this.stream?.getTracks().forEach((track) => {
4670
+ track.stop();
4671
+ });
4672
+ this.stream = void 0;
4673
+ const context = this.context;
4674
+ this.context = void 0;
4675
+ this.analyser = void 0;
4676
+ this.chunks = [];
4677
+ try {
4678
+ if (context && context.state !== "closed") await context.close();
4679
+ } finally {
4680
+ this.onDispose();
4681
+ }
4682
+ }
4683
+ };
4684
+
4685
+ // src/client/voice/voice-data.ts
4686
+ var VOICE_CHANNEL = "/hiwork-voice";
4687
+ var VOICE_DEFAULT_MAX_DURATION_SECONDS = 60;
4688
+ var VOICE_ERROR_TEXT_SESSION_EXPIRED = "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u518D\u8BD5";
4689
+ var VOICE_ERROR_TEXT_LIMIT_REACHED = "\u4ECA\u5929\u7684\u8BED\u97F3\u8BC6\u522B\u6B21\u6570\u5DF2\u7528\u5B8C\uFF0C\u660E\u5929\u518D\u6765";
4690
+ var VOICE_ERROR_TEXT_AUDIO_TOO_LARGE = "\u8FD9\u6BB5\u5F55\u97F3\u592A\u957F\u4E86\uFF0C\u8BF7\u5206\u6BB5\u8BF4\u5B8C";
4691
+ var VOICE_ERROR_TEXT_UPSTREAM = "\u8BED\u97F3\u8BC6\u522B\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
4692
+ var VOICE_ERROR_TEXT_UNREACHABLE = "\u8FDE\u63A5\u8BED\u97F3\u670D\u52A1\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
4693
+ var VOICE_INVALID_RESPONSE_TEXT = "\u8BED\u97F3\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
4694
+ var VOICE_UNRETRYABLE_CODES = /* @__PURE__ */ new Set([
4695
+ "SESSION_EXPIRED",
4696
+ "VOICE_DISABLED",
4697
+ "VOICE_NOT_CONFIGURED",
4698
+ "VOICE_LIMIT_REACHED",
4699
+ "VOICE_AUDIO_TOO_LARGE",
4700
+ "VOICE_BAD_REQUEST",
4701
+ "bad-request",
4702
+ "cancelled"
4703
+ ]);
4704
+ var VoiceRpcError = class extends Error {
4705
+ constructor(code, message) {
4706
+ super(message);
4707
+ this.code = code;
4708
+ this.name = "VoiceRpcError";
4709
+ }
4710
+ };
4711
+ function unwrapVoiceResult(value) {
4712
+ if (typeof value !== "object" || value === null || !("ok" in value)) {
4713
+ throw new VoiceRpcError("internal", VOICE_INVALID_RESPONSE_TEXT);
4714
+ }
4715
+ const result = value;
4716
+ if (result.ok === true) return result.value;
4717
+ if (result.ok === false) {
4718
+ const code = typeof result.error?.code === "string" && result.error.code !== "" ? result.error.code : "internal";
4719
+ const message = typeof result.error?.message === "string" && result.error.message.trim() !== "" ? result.error.message : VOICE_ERROR_TEXT_UNREACHABLE;
4720
+ throw new VoiceRpcError(code, message);
4721
+ }
4722
+ throw new VoiceRpcError("internal", VOICE_INVALID_RESPONSE_TEXT);
4723
+ }
4724
+ function parseVoiceStatus(value) {
4725
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
4726
+ throw new VoiceRpcError("internal", VOICE_INVALID_RESPONSE_TEXT);
4727
+ }
4728
+ const record = value;
4729
+ const seconds = record["maxDurationSeconds"];
4730
+ return {
4731
+ enabled: record["enabled"] === true,
4732
+ maxDurationSeconds: typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0 ? seconds : VOICE_DEFAULT_MAX_DURATION_SECONDS
4733
+ };
4734
+ }
4735
+ function parseVoiceTranscript(value) {
4736
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
4737
+ throw new VoiceRpcError("internal", VOICE_INVALID_RESPONSE_TEXT);
4738
+ }
4739
+ const text6 = value["text"];
4740
+ if (typeof text6 !== "string") throw new VoiceRpcError("internal", VOICE_INVALID_RESPONSE_TEXT);
4741
+ return { text: text6 };
4742
+ }
4743
+ function voiceErrorMessageOf(error) {
4744
+ const code = error instanceof VoiceRpcError ? error.code : "";
4745
+ switch (code) {
4746
+ case "SESSION_EXPIRED":
4747
+ return VOICE_ERROR_TEXT_SESSION_EXPIRED;
4748
+ case "VOICE_LIMIT_REACHED":
4749
+ return VOICE_ERROR_TEXT_LIMIT_REACHED;
4750
+ case "VOICE_AUDIO_TOO_LARGE":
4751
+ return VOICE_ERROR_TEXT_AUDIO_TOO_LARGE;
4752
+ case "VOICE_UPSTREAM":
4753
+ return VOICE_ERROR_TEXT_UPSTREAM;
4754
+ default:
4755
+ return VOICE_ERROR_TEXT_UNREACHABLE;
4756
+ }
4757
+ }
4758
+ function voiceErrorRetryable(error) {
4759
+ const code = error instanceof VoiceRpcError ? error.code : "";
4760
+ return !VOICE_UNRETRYABLE_CODES.has(code);
4761
+ }
4762
+ function createVoiceRuntime(rpc) {
4763
+ return {
4764
+ status: async () => parseVoiceStatus(unwrapVoiceResult(await rpc.call(VOICE_CHANNEL, "status", {}))),
4765
+ transcribe: async (audioBase642, signal) => parseVoiceTranscript(
4766
+ unwrapVoiceResult(await rpc.call(VOICE_CHANNEL, "transcribe", { audioBase64: audioBase642 }, signal))
4767
+ )
4768
+ };
4769
+ }
4770
+
4771
+ // src/client/voice/VoiceOverlay.tsx
4772
+ var import_jsx_runtime9 = require("react/jsx-runtime");
4773
+ var BAR_COUNT = 28;
4774
+ var LEVEL_INTERVAL_MS = 80;
4775
+ var TICK_INTERVAL_MS = 200;
4776
+ function formatClock(seconds) {
4777
+ const whole = Math.max(0, Math.floor(seconds));
4778
+ const m = Math.floor(whole / 60);
4779
+ const s = whole % 60;
4780
+ return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
4781
+ }
4782
+ function rmsToLevel(rms) {
4783
+ return Math.min(1, Math.max(0, rms) * 6);
4784
+ }
4785
+ function MicIcon2({ size }) {
4786
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
4787
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("rect", { x: "9", y: "2.75", width: "6", height: "11.5", rx: "3", stroke: "currentColor", strokeWidth: "1.6" }),
4788
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M5.5 11.5a6.5 6.5 0 0 0 13 0", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }),
4789
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M12 18v3.25", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" })
4790
+ ] });
4791
+ }
4792
+ function VoiceOverlay(props) {
4793
+ const { runtime, onClose, insertDraft } = props;
4794
+ const [phase, setPhase] = (0, import_react9.useState)("checking");
4795
+ const [error, setError] = (0, import_react9.useState)(void 0);
4796
+ const [levels, setLevels] = (0, import_react9.useState)(() => Array.from({ length: BAR_COUNT }, () => 0.06));
4797
+ const [elapsed, setElapsed] = (0, import_react9.useState)(0);
4798
+ const [maxDuration, setMaxDuration] = (0, import_react9.useState)(60);
4799
+ const [copied, setCopied] = (0, import_react9.useState)(false);
4800
+ const recordingRef = (0, import_react9.useRef)(void 0);
4801
+ const wavRef = (0, import_react9.useRef)(void 0);
4802
+ const startedAtRef = (0, import_react9.useRef)(0);
4803
+ const abortRef = (0, import_react9.useRef)(void 0);
4804
+ const disposedRef = (0, import_react9.useRef)(false);
4805
+ const failWith = (message, opts = {}) => {
4806
+ if (disposedRef.current) return;
4807
+ setPhase("error");
4808
+ setError({
4809
+ message,
4810
+ holdWav: opts.holdWav ?? false,
4811
+ canRestart: opts.canRestart ?? true,
4812
+ ...opts.recognizedText === void 0 ? {} : { recognizedText: opts.recognizedText }
4813
+ });
4814
+ };
4815
+ const beginRecording = () => {
4816
+ setError(void 0);
4817
+ setPhase("requesting");
4818
+ setElapsed(0);
4819
+ setLevels(Array.from({ length: BAR_COUNT }, () => 0.06));
4820
+ const recording2 = new Recording(() => {
4821
+ if (recordingRef.current === recording2) recordingRef.current = void 0;
4822
+ });
4823
+ recordingRef.current = recording2;
4824
+ recording2.start().then(() => {
4825
+ if (disposedRef.current) return;
4826
+ startedAtRef.current = Date.now();
4827
+ setPhase("recording");
4828
+ }).catch((err) => {
4829
+ if (disposedRef.current) return;
4830
+ if (err instanceof RecordingError) {
4831
+ if (err.kind === "permission") {
4832
+ failWith("\u6CA1\u6709\u9EA6\u514B\u98CE\u6743\u9650\uFF1A\u8BF7\u5728\u7CFB\u7EDF\u8BBE\u7F6E\u4E2D\u5141\u8BB8 HiWork \u4F7F\u7528\u9EA6\u514B\u98CE\uFF0C\u7136\u540E\u91CD\u8BD5\u3002", { canRestart: true });
4833
+ } else if (err.kind === "unavailable") {
4834
+ failWith("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u5F55\u97F3\uFF08\u627E\u4E0D\u5230\u53EF\u7528\u7684\u9EA6\u514B\u98CE\uFF09\u3002", { canRestart: false });
4835
+ } else if (err.kind === "cancelled") {
4836
+ onClose();
4837
+ } else {
4838
+ failWith("\u9EA6\u514B\u98CE\u542F\u52A8\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002", { canRestart: true });
4839
+ }
4840
+ return;
4841
+ }
4842
+ failWith(err instanceof Error ? err.message : "\u9EA6\u514B\u98CE\u542F\u52A8\u5931\u8D25\u3002", { canRestart: true });
4843
+ });
4844
+ };
4845
+ const transcribeWav = () => {
4846
+ if (phase === "transcribing") return;
4847
+ const wav = wavRef.current;
4848
+ if (wav === void 0) return;
4849
+ setPhase("transcribing");
4850
+ setError(void 0);
4851
+ const abort = new AbortController();
4852
+ abortRef.current = abort;
4853
+ runtime.transcribe(audioBase64(wav), abort.signal).then(({ text: text6 }) => {
4854
+ if (disposedRef.current) return;
4855
+ if (text6.trim() === "") {
4856
+ failWith("\u6CA1\u6709\u8BC6\u522B\u5230\u8BF4\u8BDD\u5185\u5BB9\uFF0C\u8BF7\u9760\u8FD1\u9EA6\u514B\u98CE\u518D\u8BD5\u4E00\u6B21\u3002", { holdWav: true, canRestart: true });
4857
+ return;
4858
+ }
4859
+ const result = insertDraft(text6);
4860
+ if (result.ok) {
4861
+ onClose();
4862
+ return;
4863
+ }
4864
+ const reason = result.reason === "no-target" ? "\u6CA1\u6709\u627E\u5230\u6253\u5F00\u7684\u4F1A\u8BDD\u8F93\u5165\u6846\u3002" : result.reason === "busy" ? "\u8F93\u5165\u6846\u6B63\u5728\u63D0\u4EA4\u4E2D\uFF0C\u6682\u65F6\u4E0D\u80FD\u5199\u5165\u3002" : "\u5199\u5165\u8F93\u5165\u6846\u88AB\u62D2\u7EDD\u3002";
4865
+ failWith(`${reason}\u8BC6\u522B\u6587\u672C\u672A\u4E22\u5931\uFF0C\u53EF\u590D\u5236\u540E\u624B\u52A8\u7C98\u8D34\u3002`, {
4866
+ holdWav: false,
4867
+ canRestart: true,
4868
+ recognizedText: text6
4869
+ });
4870
+ }).catch((err) => {
4871
+ if (disposedRef.current || abort.signal.aborted) return;
4872
+ const retryable = voiceErrorRetryable(err);
4873
+ failWith(voiceErrorMessageOf(err), { holdWav: retryable, canRestart: true });
4874
+ });
4875
+ };
4876
+ const finish = () => {
4877
+ if (phase !== "recording") return;
4878
+ const recording2 = recordingRef.current;
4879
+ if (recording2 === void 0) return;
4880
+ setPhase("transcribing");
4881
+ recording2.stop(maxDuration).then((wav) => {
4882
+ if (disposedRef.current) return;
4883
+ if (isSilentWav(wav)) {
4884
+ failWith(
4885
+ "\u5F55\u97F3\u6CA1\u6709\u91C7\u5230\u58F0\u97F3\uFF1A\u591A\u534A\u662F\u7CFB\u7EDF\u62D2\u7EDD\u4E86\u9EA6\u514B\u98CE\u6743\u9650\uFF08macOS\uFF1A\u7CFB\u7EDF\u8BBE\u7F6E \u2192 \u9690\u79C1\u4E0E\u5B89\u5168\u6027 \u2192 \u9EA6\u514B\u98CE \u2192 \u52FE\u9009 HiWork\uFF09\uFF0C\u6253\u5F00\u540E\u518D\u5F55\u4E00\u6B21\u3002",
4886
+ { holdWav: false }
4887
+ );
4888
+ return;
4889
+ }
4890
+ wavRef.current = wav;
4891
+ transcribeWav();
4892
+ }).catch((err) => {
4893
+ if (disposedRef.current) return;
4894
+ if (err instanceof RecordingError) {
4895
+ failWith(err.kind === "cancelled" ? "\u5F55\u97F3\u5DF2\u53D6\u6D88\u3002" : "\u8FD9\u6BB5\u5F55\u97F3\u662F\u7A7A\u7684\uFF0C\u8BF7\u518D\u8BD5\u4E00\u6B21\u3002", { holdWav: false });
4896
+ return;
4897
+ }
4898
+ failWith(err instanceof Error ? err.message : "\u5F55\u97F3\u5904\u7406\u5931\u8D25\u3002", { holdWav: false });
4899
+ });
4900
+ };
4901
+ (0, import_react9.useEffect)(() => {
4902
+ disposedRef.current = false;
4903
+ let cancelled = false;
4904
+ runtime.status().then((status) => {
4905
+ if (cancelled) return;
4906
+ setMaxDuration(status.maxDurationSeconds);
4907
+ if (status.enabled !== true) {
4908
+ failWith(VOICE_ERROR_TEXT_UNREACHABLE, { canRestart: false });
4909
+ return;
4910
+ }
4911
+ beginRecording();
4912
+ }).catch(() => {
4913
+ if (cancelled) return;
4914
+ failWith(VOICE_ERROR_TEXT_UNREACHABLE, { canRestart: false });
4915
+ });
4916
+ return () => {
4917
+ cancelled = true;
4918
+ disposedRef.current = true;
4919
+ abortRef.current?.abort();
4920
+ void recordingRef.current?.dispose().catch(() => void 0);
4921
+ recordingRef.current = void 0;
4922
+ };
4923
+ }, []);
4924
+ (0, import_react9.useEffect)(() => {
4925
+ if (phase !== "recording") return;
4926
+ const levelTimer = window.setInterval(() => {
4927
+ const rms = recordingRef.current?.amplitude() ?? 0;
4928
+ setLevels((prev) => [...prev.slice(1), Math.max(0.06, rmsToLevel(rms))]);
4929
+ }, LEVEL_INTERVAL_MS);
4930
+ const clockTimer = window.setInterval(() => {
4931
+ setElapsed((Date.now() - startedAtRef.current) / 1e3);
4932
+ }, TICK_INTERVAL_MS);
4933
+ return () => {
4934
+ window.clearInterval(levelTimer);
4935
+ window.clearInterval(clockTimer);
4936
+ };
4937
+ }, [phase]);
4938
+ (0, import_react9.useEffect)(() => {
4939
+ if (phase !== "recording") return;
4940
+ if (elapsed < maxDuration) return;
4941
+ finish();
4942
+ }, [phase, elapsed, maxDuration]);
4943
+ const handleClose = () => {
4944
+ abortRef.current?.abort();
4945
+ onClose();
4946
+ };
4947
+ (0, import_react9.useEffect)(() => {
4948
+ const onKey = (event) => {
4949
+ if (event.key !== "Escape") return;
4950
+ event.preventDefault();
4951
+ handleClose();
4952
+ };
4953
+ window.addEventListener("keydown", onKey);
4954
+ return () => window.removeEventListener("keydown", onKey);
4955
+ }, []);
4956
+ const copyRecognized = () => {
4957
+ const text6 = error?.recognizedText;
4958
+ if (text6 === void 0) return;
4959
+ void navigator.clipboard.writeText(text6).then(() => setCopied(true)).catch(() => setCopied(false));
4960
+ };
4961
+ const recording = phase === "recording";
4962
+ const idle = phase === "checking" || phase === "requesting" || phase === "transcribing";
4963
+ const canRetryTranscribe = error?.holdWav === true && error.recognizedText === void 0;
4964
+ const canRestart = error?.canRestart === true;
4965
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4966
+ "div",
4967
+ {
4968
+ className: "hiwv-backdrop",
4969
+ onMouseDown: (event) => {
4970
+ if (event.target === event.currentTarget && !idle) handleClose();
4971
+ },
4972
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "hiwv-panel", role: "dialog", "aria-modal": "true", "aria-label": "\u8BED\u97F3\u8F93\u5165", children: [
4973
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "hiwv-head", children: [
4974
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "hiwv-title", children: "\u8BED\u97F3\u8F93\u5165" }),
4975
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "hiwv-hint", children: recording ? `\u5DF2\u5F55 ${formatClock(elapsed)} / \u4E0A\u9650 ${formatClock(maxDuration)}` : `\u5355\u6B21\u6700\u957F ${formatClock(maxDuration)}` })
4976
+ ] }),
4977
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: `hiwv-stage${recording ? "" : " hiwv-stageIdle"}`, children: recording || phase === "transcribing" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
4978
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: `hiwv-orb${phase === "transcribing" ? " hiwv-orbBusy" : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(MicIcon2, { size: 30 }) }),
4979
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "hiwv-bars", "aria-hidden": "true", children: levels.map((level, index) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4980
+ "i",
4981
+ {
4982
+ style: { height: `${Math.round(8 + level * 46)}px` },
4983
+ className: phase === "transcribing" ? "hiwv-barIdle" : void 0
4984
+ },
4985
+ index
4986
+ )) }),
4987
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "hiwv-status", children: phase === "transcribing" ? "\u6B63\u5728\u8BC6\u522B\u2026" : "\u6B63\u5728\u8046\u542C\uFF0C\u8BB2\u5B8C\u540E\u70B9\u300C\u5B8C\u6210\u300D" })
4988
+ ] }) : phase === "checking" || phase === "requesting" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "hiwv-status", children: "\u6B63\u5728\u51C6\u5907\u9EA6\u514B\u98CE\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "hiwv-errorBox", children: [
4989
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "hiwv-errorMsg", children: error?.message ?? "\u51FA\u4E86\u70B9\u95EE\u9898\u3002" }),
4990
+ error?.recognizedText !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "hiwv-recognized", children: [
4991
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "hiwv-recognizedText", children: error.recognizedText }),
4992
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: "hiwv-btn hiwv-btnSecondary", onClick: copyRecognized, children: copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236\u6587\u672C" })
4993
+ ] })
4994
+ ] }) }),
4995
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "hiwv-actions", children: [
4996
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: "hiwv-btn hiwv-btnSecondary", onClick: handleClose, children: "\u53D6\u6D88" }),
4997
+ recording && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: "hiwv-btn hiwv-btnPrimary", onClick: finish, children: "\u5B8C\u6210" }),
4998
+ phase === "error" && canRetryTranscribe && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: "hiwv-btn hiwv-btnPrimary", onClick: transcribeWav, children: "\u91CD\u8BD5\u8BC6\u522B" }),
4999
+ phase === "error" && canRestart && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
5000
+ "button",
5001
+ {
5002
+ type: "button",
5003
+ className: `hiwv-btn ${canRetryTranscribe ? "hiwv-btnSecondary" : "hiwv-btnPrimary"}`,
5004
+ onClick: beginRecording,
5005
+ children: "\u518D\u5F55\u4E00\u6B21"
5006
+ }
5007
+ )
5008
+ ] })
5009
+ ] })
5010
+ }
5011
+ );
5012
+ }
5013
+
5014
+ // src/client/voice/voice-styles.ts
5015
+ var VOICE_STYLE_ID = "hiwork-core-voice-styles";
5016
+ var VOICE_STYLE_TAG_ID = "hiwork-core/voice.css";
5017
+ function ensureVoiceStyle(doc) {
5018
+ const existing = doc.getElementById(VOICE_STYLE_ID);
5019
+ if (existing !== null) {
5020
+ if (!existing.hasAttribute("data-plugin")) ownStyleTag(existing, VOICE_STYLE_TAG_ID);
5021
+ return;
5022
+ }
5023
+ const style = doc.createElement("style");
5024
+ style.id = VOICE_STYLE_ID;
5025
+ ownStyleTag(style, VOICE_STYLE_TAG_ID);
5026
+ style.textContent = voiceCss;
5027
+ doc.head.appendChild(style);
5028
+ }
5029
+
5030
+ // src/client/voice/entry.ts
5031
+ var VOICE_INPUT_SLOT = "conversation.input.right";
5032
+ var VOICE_INPUT_SLOT_ID = "hiwork-core-voice";
5033
+ var VOICE_INPUT_SLOT_ORDER = 10;
5034
+ var VOICE_ROOT_CLASS = "hiwv-root";
5035
+ var VOICE_GATE_RETRY_DELAYS_MS = [3e3, 1e4, 3e4, 6e4, 18e4];
5036
+ var defaultGateTimer = {
5037
+ after: (ms, fn) => {
5038
+ const id = globalThis.setTimeout(fn, ms);
5039
+ return () => {
5040
+ globalThis.clearTimeout(id);
5041
+ };
5042
+ }
5043
+ };
5044
+ function createOverlayController(deps) {
5045
+ let container;
5046
+ let root;
5047
+ const close = () => {
5048
+ const mounted = root;
5049
+ root = void 0;
5050
+ mounted?.unmount();
5051
+ container?.remove();
5052
+ container = void 0;
5053
+ };
5054
+ const open = (sessionId) => {
5055
+ if (root !== void 0) return;
5056
+ const doc = deps.doc;
5057
+ if (doc === void 0) return;
5058
+ container = doc.createElement("div");
5059
+ container.className = VOICE_ROOT_CLASS;
5060
+ doc.body.appendChild(container);
5061
+ root = (0, import_client3.createRoot)(container);
5062
+ root.render(
5063
+ (0, import_react10.createElement)(VoiceOverlay, {
5064
+ runtime: deps.runtime,
5065
+ onClose: close,
5066
+ // 会话 id 缺省(拿不到槽位注入)时由 insertDraftText 退到当前打开会话;
5067
+ // 服务本身**在插入那一刻现取**(装配期它可能还没上线,见 VoiceEntryDeps.resolveSessions)。
5068
+ insertDraft: (text6) => insertDraftText(deps.resolveSessions(), text6, sessionId)
5069
+ })
5070
+ );
5071
+ };
5072
+ return { open, close };
5073
+ }
5074
+ async function installVoiceInput(deps) {
5075
+ const doc = deps.document ?? (typeof document === "undefined" ? void 0 : document);
5076
+ const runtime = createVoiceRuntime(deps.rpc);
5077
+ const overlay = createOverlayController({ runtime, resolveSessions: deps.resolveSessions, doc });
5078
+ const timer = deps.timer ?? defaultGateTimer;
5079
+ if (doc !== void 0) ensureVoiceStyle(doc);
5080
+ let enabled = false;
5081
+ let disposed = false;
5082
+ let step = 0;
5083
+ let cancelPending;
5084
+ let probing = false;
5085
+ const clearPending = () => {
5086
+ const cancel = cancelPending;
5087
+ cancelPending = void 0;
5088
+ cancel?.();
5089
+ };
5090
+ const dispose = () => {
5091
+ disposed = true;
5092
+ clearPending();
5093
+ overlay.close();
5094
+ };
5095
+ if (deps.signal !== void 0) {
5096
+ if (deps.signal.aborted) disposed = true;
5097
+ else deps.signal.addEventListener("abort", dispose, { once: true });
5098
+ }
5099
+ const probe = async () => {
5100
+ if (disposed || enabled || probing) return;
5101
+ probing = true;
5102
+ const status = await runtime.status().catch(() => void 0);
5103
+ probing = false;
5104
+ if (disposed || enabled) return;
5105
+ if (status?.enabled === true) {
5106
+ enabled = true;
5107
+ clearPending();
5108
+ deps.registerMic({ open: (sessionId) => overlay.open(sessionId) });
5109
+ return;
5110
+ }
5111
+ const delay = VOICE_GATE_RETRY_DELAYS_MS[step];
5112
+ if (delay === void 0) return;
5113
+ step += 1;
5114
+ cancelPending = timer.after(delay, () => {
5115
+ cancelPending = void 0;
5116
+ void probe().catch(() => void 0);
5117
+ });
5118
+ };
5119
+ const retryNow = () => {
5120
+ if (disposed || enabled) return;
5121
+ clearPending();
5122
+ step = 0;
5123
+ void probe().catch(() => void 0);
5124
+ };
5125
+ await probe();
5126
+ return {
5127
+ get enabled() {
5128
+ return enabled;
5129
+ },
5130
+ retryNow,
5131
+ dispose
5132
+ };
5133
+ }
5134
+
4174
5135
  // src/client/index.ts
4175
5136
  var inject = ["slots", "locale", "remote", "remote.credentials", "settingsScope", "connection"];
4176
5137
  var INTERACTION_STYLE_ID = "hiwork-core-interaction-styles";
@@ -4179,6 +5140,7 @@ function injectInteractionStyles() {
4179
5140
  if (document.getElementById(INTERACTION_STYLE_ID) !== null) return;
4180
5141
  const style = document.createElement("style");
4181
5142
  style.id = INTERACTION_STYLE_ID;
5143
+ ownStyleTag(style, "hiwork-core/interaction-styles.css");
4182
5144
  style.textContent = [interactionCss, brandCss, frameCss, hostHideCss, composerCss].join("\n");
4183
5145
  document.head.appendChild(style);
4184
5146
  }
@@ -4227,6 +5189,42 @@ function apply(ctx) {
4227
5189
  order: 20,
4228
5190
  inject: () => ({ store: sessionCost })
4229
5191
  }, SessionCost));
5192
+ let voice;
5193
+ ctx.effect(() => {
5194
+ const controller2 = new AbortController();
5195
+ void installVoiceInput({
5196
+ rpc: connection.rpc,
5197
+ // sessions 不在 inject 列表里(那是可选能力),只能按结构读取;**必须现取**:
5198
+ // 它比本插件晚上线,装配期 `ctx.get('sessions')` 是 undefined(真机实测)。装配期
5199
+ // 快照会把 undefined 冻进浮层控制器,每次录音都报「没有找到打开的会话输入框」。
5200
+ // 拿不到时仍按原样降级:草稿插入判定为「没有打开的会话」,浮层照常给出复制出口。
5201
+ resolveSessions: () => ctx.get("sessions"),
5202
+ registerMic: (inject2) => {
5203
+ ctx.slots.inject(VOICE_INPUT_SLOT, () => ctx.slots.register({
5204
+ name: VOICE_INPUT_SLOT,
5205
+ id: VOICE_INPUT_SLOT_ID,
5206
+ order: VOICE_INPUT_SLOT_ORDER,
5207
+ // session 作用域 list 槽:inject 工厂的会话 id 由框架下发,绑进 open 里。
5208
+ inject: (sessionId) => ({ open: () => inject2.open(sessionId) })
5209
+ }, MicButton));
5210
+ },
5211
+ signal: controller2.signal
5212
+ }).then((installed) => {
5213
+ if (controller2.signal.aborted) {
5214
+ installed.dispose();
5215
+ return;
5216
+ }
5217
+ voice = installed;
5218
+ }).catch(() => void 0);
5219
+ return () => {
5220
+ controller2.abort();
5221
+ voice?.dispose();
5222
+ voice = void 0;
5223
+ };
5224
+ }, "hiwork-core: voice input");
5225
+ ctx.effect(() => observeAccountChange(controller.inject().hooks.aiRouterCard, () => {
5226
+ voice?.retryNow();
5227
+ }), "hiwork-core: \u767B\u5F55\u540E\u590D\u95EE\u8BED\u97F3\u95E8\u7981");
4230
5228
  ctx.effect(() => observeAccountChange(controller.inject().hooks.aiRouterCard, () => {
4231
5229
  usage.clear();
4232
5230
  sessionCost.clear();
@@ -4257,7 +5255,7 @@ function apply(ctx) {
4257
5255
  function installFeatureCenter(ctx, managedMcp, managedSkills, managedWorkbench, pmsTodo) {
4258
5256
  const featureRegistry = createFeatureRegistry();
4259
5257
  let center;
4260
- const sessions = ctx.get("sessions");
5258
+ const resolveSessions = () => ctx.get("sessions");
4261
5259
  const locale = ctx.get("locale");
4262
5260
  const face = {
4263
5261
  featureRegistry,
@@ -4265,7 +5263,7 @@ function installFeatureCenter(ctx, managedMcp, managedSkills, managedWorkbench,
4265
5263
  // 顺序固定:先退出 takeover 恢复原生会话区,再交给宿主导航。
4266
5264
  openNativeSession: (sessionId) => {
4267
5265
  featureRegistry.openNative();
4268
- sessions?.open(sessionId);
5266
+ resolveSessions()?.open(sessionId);
4269
5267
  },
4270
5268
  openNativeHome: () => {
4271
5269
  if (center !== void 0) {
@@ -4332,7 +5330,7 @@ function installFeatureCenter(ctx, managedMcp, managedSkills, managedWorkbench,
4332
5330
  center = mountFeatureCenter({
4333
5331
  registry: featureRegistry,
4334
5332
  openNativeSession: (sessionId) => {
4335
- sessions?.open(sessionId);
5333
+ resolveSessions()?.open(sessionId);
4336
5334
  },
4337
5335
  ...locale === void 0 ? {} : { subscribeLocale: (listener) => locale.subscribe(listener) }
4338
5336
  });