dsh-xrxs-lingting 0.4.0-dev.50 → 0.4.0-dev.51

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
@@ -110,3 +110,5 @@ npm run smoke:client -- /path/to/cinlyn-desktop
110
110
  该检查不代替安装后对授权、录音、转写和回放的人工验证。
111
111
 
112
112
  布局回归验证:`node scripts/smoke-layout-browser.mjs <playwright/index.mjs 路径>`,使用 Chrome 无头浏览器运行真实插件产物,验证页面边界、弹窗居中、转写面板层级、窗口缩放与卸载后聊天恢复。使用合成录音状态,不访问业务数据、不采集声音。
113
+
114
+ 设备切换:支持的桌面原生模块会在默认输出变化时重新连接系统采集,插件重新采样并替换默认麦克风输入,保留同一录音与转写连接。操作系统切换设备期间可能存在短暂音频缺口;持续无法恢复才报错暂停。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-xrxs-lingting",
3
- "version": "0.4.0-dev.50",
3
+ "version": "0.4.0-dev.51",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite --host 127.0.0.1",
@@ -1606,6 +1606,117 @@ function toSegment(segment) {
1606
1606
  return { id: segment.id, speaker: speakerLabel(segment.speakerId), text: segment.text, final: segment.final };
1607
1607
  }
1608
1608
 
1609
+ // src/audio/microphone-switch.ts
1610
+ function followDefaultMicrophone(options) {
1611
+ let current = options.initial;
1612
+ let stopped = false;
1613
+ let running = false;
1614
+ let queued = false;
1615
+ let cancelAcquire;
1616
+ let cancelDelay;
1617
+ const delay = () => new Promise((resolve) => {
1618
+ const timer = setTimeout(() => {
1619
+ cancelDelay = void 0;
1620
+ resolve();
1621
+ }, 250);
1622
+ cancelDelay = () => {
1623
+ clearTimeout(timer);
1624
+ cancelDelay = void 0;
1625
+ resolve();
1626
+ };
1627
+ });
1628
+ const watch = (stream) => {
1629
+ stream.getAudioTracks().forEach((track) => {
1630
+ track.onended = () => {
1631
+ void reconnect();
1632
+ };
1633
+ });
1634
+ };
1635
+ const reconnect = async () => {
1636
+ if (stopped) return;
1637
+ if (running) {
1638
+ queued = true;
1639
+ return;
1640
+ }
1641
+ running = true;
1642
+ const began = Date.now();
1643
+ try {
1644
+ do {
1645
+ queued = false;
1646
+ try {
1647
+ const next = await new Promise((resolve, reject) => {
1648
+ let settled = false;
1649
+ const finishError = (error) => {
1650
+ if (settled) return;
1651
+ settled = true;
1652
+ clearTimeout(timer);
1653
+ cancelAcquire = void 0;
1654
+ reject(error);
1655
+ };
1656
+ const timer = setTimeout(() => finishError(new Error("Microphone recovery timed out")), Math.max(0, 8e3 - (Date.now() - began)));
1657
+ cancelAcquire = () => finishError(new Error("Recording stopped"));
1658
+ Promise.resolve().then(options.acquire).then((stream) => {
1659
+ if (settled || stopped) {
1660
+ stream.getTracks().forEach((track) => track.stop());
1661
+ return;
1662
+ }
1663
+ settled = true;
1664
+ clearTimeout(timer);
1665
+ cancelAcquire = void 0;
1666
+ resolve(stream);
1667
+ }, finishError);
1668
+ });
1669
+ if (stopped) {
1670
+ next.getTracks().forEach((track) => track.stop());
1671
+ return;
1672
+ }
1673
+ if (!next.getAudioTracks().some((track) => track.readyState === "live")) {
1674
+ next.getTracks().forEach((track) => track.stop());
1675
+ throw new Error("Microphone is not live");
1676
+ }
1677
+ const previous = current;
1678
+ try {
1679
+ options.replace(next, previous);
1680
+ } catch (error) {
1681
+ next.getTracks().forEach((track) => track.stop());
1682
+ throw error;
1683
+ }
1684
+ current = next;
1685
+ previous.getTracks().forEach((track) => {
1686
+ track.onended = null;
1687
+ track.stop();
1688
+ });
1689
+ watch(current);
1690
+ } catch {
1691
+ if (stopped) return;
1692
+ if (Date.now() - began >= 8e3) {
1693
+ if (!current.getAudioTracks().some((track) => track.readyState === "live")) options.failed();
1694
+ return;
1695
+ }
1696
+ await delay();
1697
+ queued = true;
1698
+ }
1699
+ } while (queued && !stopped);
1700
+ } finally {
1701
+ running = false;
1702
+ }
1703
+ };
1704
+ const changed = () => {
1705
+ void reconnect();
1706
+ };
1707
+ watch(current);
1708
+ options.devices.addEventListener?.("devicechange", changed);
1709
+ return () => {
1710
+ stopped = true;
1711
+ cancelDelay?.();
1712
+ cancelAcquire?.();
1713
+ options.devices.removeEventListener?.("devicechange", changed);
1714
+ current.getTracks().forEach((track) => {
1715
+ track.onended = null;
1716
+ });
1717
+ };
1718
+ }
1719
+
1609
1720
  // src/audio/worklet-source.ts
1610
1721
  var captureWorkletSource = `
1611
1722
  class LingtingCaptureProcessor extends AudioWorkletProcessor {
@@ -1691,7 +1802,7 @@ class NativeSystemSource extends AudioWorkletProcessor {
1691
1802
  accept(data) {
1692
1803
  if (this.failed) return;
1693
1804
  if (!data || !(data.samples instanceof Float32Array) || !Number.isFinite(data.sampleRate) || data.sampleRate < 8000 || data.sampleRate > 192000) return;
1694
- if (this.rate && this.rate !== data.sampleRate) { this.fail('\u7CFB\u7EDF\u58F0\u97F3\u91C7\u6837\u7387\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB\u5F55\u97F3\u3002'); return; }
1805
+ if (this.rate && this.rate !== data.sampleRate) { this.read = 0; this.write = 0; this.started = false; this.buffer.fill(0); }
1695
1806
  this.rate = data.sampleRate;
1696
1807
  // Bound latency, not just memory. After a render stall, retain the newest
1697
1808
  // 100 ms instead of spending minutes playing stale audio or killing capture.
@@ -1784,6 +1895,7 @@ var MeetingCapture = class {
1784
1895
  emitting = false;
1785
1896
  stopSystem = null;
1786
1897
  systemNode = null;
1898
+ stopMicrophoneSwitch = null;
1787
1899
  async start(onChunk, onError) {
1788
1900
  if (this.context || this.starting) throw new Error("\u91C7\u96C6\u5DF2\u5728\u8FDB\u884C\u4E2D\u3002");
1789
1901
  this.starting = true;
@@ -1822,7 +1934,6 @@ var MeetingCapture = class {
1822
1934
  for (const stream of this.streams) {
1823
1935
  for (const track of stream.getAudioTracks()) {
1824
1936
  if (track.readyState !== "live") throw new Error("\u97F3\u9891\u8BBE\u5907\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB\u5F55\u97F3\u3002");
1825
- track.onended = () => fail(stream === microphone ? "\u9EA6\u514B\u98CE\u5DF2\u65AD\u5F00\uFF0C\u5F55\u97F3\u5DF2\u6682\u505C\uFF0C\u8BF7\u68C0\u67E5\u8BBE\u5907\u540E\u7EE7\u7EED\u3002" : "\u7CFB\u7EDF\u58F0\u97F3\u91C7\u96C6\u5DF2\u505C\u6B62\uFF0C\u5F55\u97F3\u5DF2\u6682\u505C\uFF0C\u8BF7\u91CD\u65B0\u6388\u6743\u540E\u7EE7\u7EED\u3002");
1826
1937
  }
1827
1938
  }
1828
1939
  const context = new AudioContext();
@@ -1836,7 +1947,7 @@ var MeetingCapture = class {
1836
1947
  return;
1837
1948
  }
1838
1949
  stalledSince ??= Date.now();
1839
- if (Date.now() - stalledSince >= 5e3) {
1950
+ if (Date.now() - stalledSince >= 8e3) {
1840
1951
  fail("\u97F3\u9891\u5904\u7406\u6062\u590D\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB\u5F55\u97F3\u3002");
1841
1952
  return;
1842
1953
  }
@@ -1846,7 +1957,8 @@ var MeetingCapture = class {
1846
1957
  return;
1847
1958
  }
1848
1959
  recovering = true;
1849
- void context.resume().catch(() => fail("\u97F3\u9891\u5904\u7406\u65E0\u6CD5\u6062\u590D\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB\u5F55\u97F3\u3002")).finally(() => {
1960
+ void context.resume().catch(() => {
1961
+ }).finally(() => {
1850
1962
  recovering = false;
1851
1963
  });
1852
1964
  };
@@ -1924,6 +2036,24 @@ var MeetingCapture = class {
1924
2036
  this.nodes.push(source, analyser, gain);
1925
2037
  analysers.push(analyser);
1926
2038
  }
2039
+ this.stopMicrophoneSwitch = followDefaultMicrophone({
2040
+ devices,
2041
+ initial: microphone,
2042
+ acquire: () => devices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true, autoGainControl: true } }),
2043
+ replace: (next, previous) => {
2044
+ if (generation !== this.generation) throw new Error("\u5F55\u97F3\u5DF2\u53D6\u6D88\u3002");
2045
+ const replacement = context.createMediaStreamSource(next);
2046
+ replacement.connect(analysers[0]);
2047
+ const oldSource = sources[0];
2048
+ oldSource.disconnect();
2049
+ sources[0] = replacement;
2050
+ this.nodes = this.nodes.filter((item) => item !== oldSource);
2051
+ this.nodes.push(replacement);
2052
+ this.streams = this.streams.filter((item) => item !== previous);
2053
+ this.streams.push(next);
2054
+ },
2055
+ failed: () => fail("\u9EA6\u514B\u98CE\u5207\u6362\u540E\u672A\u80FD\u6062\u590D\uFF0C\u5F55\u97F3\u5DF2\u6682\u505C\uFF0C\u8BF7\u68C0\u67E5\u8BBE\u5907\u8FDE\u63A5\u3002")
2056
+ });
1927
2057
  const mute = context.createGain();
1928
2058
  mute.gain.value = 0;
1929
2059
  node.connect(mute);
@@ -1970,6 +2100,8 @@ var MeetingCapture = class {
1970
2100
  }
1971
2101
  async stop() {
1972
2102
  ++this.generation;
2103
+ this.stopMicrophoneSwitch?.();
2104
+ this.stopMicrophoneSwitch = null;
1973
2105
  this.emitting = false;
1974
2106
  if (this.timer) clearInterval(this.timer);
1975
2107
  this.timer = null;
@@ -5450,7 +5582,7 @@ function mountBusinessSurface(doc) {
5450
5582
  else center.style.removeProperty("position");
5451
5583
  } };
5452
5584
  }
5453
- function BusinessSurface({ children }) {
5585
+ function BusinessSurface({ children, onClose }) {
5454
5586
  const [target, setTarget] = (0, import_react22.useState)(null);
5455
5587
  (0, import_react22.useLayoutEffect)(() => {
5456
5588
  let mounted = null;
@@ -5461,9 +5593,18 @@ function BusinessSurface({ children }) {
5461
5593
  setTarget(mounted?.target ?? null);
5462
5594
  };
5463
5595
  attach();
5596
+ const onSidebarClick = (event) => {
5597
+ const element = event.target instanceof Element ? event.target : null;
5598
+ const button = element?.closest("button");
5599
+ if (!button?.closest('[data-slot="sidebar"]')) return;
5600
+ const label = button.getAttribute("aria-label")?.trim();
5601
+ if (label && /^(新会话|新建会话|New session)$/i.test(label)) onClose();
5602
+ };
5603
+ document.addEventListener("click", onSidebarClick, true);
5464
5604
  const observer = new MutationObserver(attach);
5465
5605
  observer.observe(document.body, { childList: true, subtree: true });
5466
5606
  return () => {
5607
+ document.removeEventListener("click", onSidebarClick, true);
5467
5608
  observer.disconnect();
5468
5609
  mounted?.dispose();
5469
5610
  };
@@ -5537,7 +5678,7 @@ async function apply(ctx) {
5537
5678
  function Overlay() {
5538
5679
  const page = (0, import_react23.useSyncExternalStore)(business.subscribe, business.getSnapshot, business.getSnapshot);
5539
5680
  return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
5540
- !hostBusiness && page.activeId === ID && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(BusinessSurface, { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Page, { activeId: page.activeId }) }),
5681
+ !hostBusiness && page.activeId === ID && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(BusinessSurface, { onClose: () => business.close(), children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Page, { activeId: page.activeId }) }),
5541
5682
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PanelBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(XinHostNotice, { store: hostState }) }),
5542
5683
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PanelBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(LingtingSidebar, { service, controller, onOpenPage: () => business.open(ID) }) }),
5543
5684
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PanelBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(MeetingPrompt, { service }) })