@bittersmilezzz/dsh-asr-voice 0.3.3 → 0.4.1

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/cordis.patch.yml CHANGED
@@ -13,15 +13,16 @@
13
13
  # host 实时通道(realtime.provider 选 'builtin' 假 provider 或真云端预置)。
14
14
  # - 提示词优化:本地启发式(zh/en 清洗)或可选 LLM 重写(optimize.mode 默认
15
15
  # llm,走 DSH 当前所选模型的官方 LLM 通道,host 代理)。
16
- # - 设置页卡片:「设置 → 插件 → 配置」→「语音输入」(settings.plugin.item,
17
- # key: asr-voice;host 注册同名 settings namespace 让配置 tab 枚举到卡片)。
16
+ # - 设置页卡片:「设置 → 插件 → 配置」→「语音输入」(plugins.bundle.config,
17
+ # key: asr-voice;配置 schema 是入口模块顶层导出的 Config,DSH 0.1.7 起
18
+ # profile-backed forms,entry id 即 namespace)。
18
19
  # - API key 只进 **DSH 凭据服务**(预置引用名与官方 LLM 凭据同名),settings 与浏览器
19
20
  # 都不持有密钥;音频与转写请求经 /api/asr-voice/* 私有 JSON 代理。纯 Node HTTP 实现,
20
21
  # macOS / Windows 双平台。
21
22
  #
22
- # host inject 只列硬依赖(webServer/llm);settings 走 scoped inject(可选,
23
- # 缺失时仅云端 ASR 无 key、不影响挂载),agentDefaultModel 在优化路由内
24
- # ctx.get 可选读取。client 侧 settingsScope / locale / slots 是硬依赖(设置卡与
23
+ # host inject 只列硬依赖(webServer/llm);配置经函数插件第二参注入(DSH 0.1.7
24
+ # Config 契约),无需 inject settings;agentDefaultModel 在优化路由内
25
+ # ctx.get 可选读取。client 侧 configForms / locale / slots 是硬依赖(设置卡与
25
26
  # 按钮座位必需),connection / sessions / remote.credentials 是可选 ctx.inject
26
27
  # (缺失只让对应能力退化:密钥读写 / 回合打断,不影响挂载)。
27
28
  # 注意:inject 项是 Cordis 服务名(非 entry id)。
package/lib/client.js CHANGED
@@ -818,6 +818,9 @@ window.__ModuleLoader__.load({
818
818
  }
819
819
  .dshav-card:hover { border-color: var(--dsw-alias-label-dimmed); }
820
820
  .dshav-card.dshav-card-open { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-label-dimmed);}
821
+ /* 插件详情页(view: 'page'):官方页面已画标题/外壳,卡片容器透明化、body 无分隔线。 */
822
+ .dshav-card.dshav-card-page { border: 0; background: none; }
823
+ .dshav-card.dshav-card-page:hover { border-color: transparent; }
821
824
  .dshav-header {
822
825
  width: 100%;
823
826
  appearance: none;
@@ -846,6 +849,7 @@ window.__ModuleLoader__.load({
846
849
  .dshav-chevron { flex: none; color: var(--dsw-alias-label-tertiary); transition: transform .16s;}
847
850
  .dshav-chevron.dshav-open { transform: rotate(180deg); }
848
851
  .dshav-body { border-top: 1px solid var(--dsw-alias-border-l2); margin: 0 16px; padding-bottom: 8px;}
852
+ .dshav-body.dshav-body-page { border-top: 0; margin: 0; padding-top: 0; }
849
853
  .dshav-group { display: flex; flex-direction: column; }
850
854
  /* 分组之间用与字段一致的分割线(官方 border-l2)。 */
851
855
  .dshav-group + .dshav-group { border-top: 1px solid var(--dsw-alias-border-l2); }
@@ -1108,7 +1112,7 @@ window.__ModuleLoader__.load({
1108
1112
  label: "内置模拟(开发)",
1109
1113
  wssUrl: "",
1110
1114
  defaultModel: "",
1111
- keyPreset: "openai",
1115
+ keyPreset: "",
1112
1116
  hint: "I3/I4 开发态:host 用能量 VAD 假 provider 驱动整条管道,不花配额"
1113
1117
  }, {
1114
1118
  id: "dashscope-realtime",
@@ -1215,8 +1219,23 @@ window.__ModuleLoader__.load({
1215
1219
  listeners.delete(fn);
1216
1220
  };
1217
1221
  }
1218
- /** settings namespace(host schema 注册与 client 绑定共用同一个名字)。 */
1219
- const ASR_VOICE_NS = "asr-voice";
1222
+ /**
1223
+ * settings namespace = host 半区 `cordis.patch.yml` 的 entry `id`(DSH 0.1.7 起
1224
+ * `SettingsForms.update/describe` 与 `ConfigForms.get` 都用这个字符串定位 entry,
1225
+ * 官方实现是 `entries().find(row => row.options.id === ns)`)。
1226
+ *
1227
+ * ⚠ 它**不是** npm 包名(本插件 npm 包名是 `@bittersmilezzz/dsh-asr-voice`),也不
1228
+ * 是文件里其它地方的插件短名。拿错字符串不会抛错:host 侧 update 抛
1229
+ * `No configurable plugin entry`(migrateLegacyKeys 只落一句 warn),client 侧
1230
+ * configForms.get 拿到 unavailable 快照 → 设置卡能渲染但**所有读写静默失效**。
1231
+ * 一致性由 test/entry-id-parity.test.mjs 钉住(读 cordis.patch.yml 的 id,并与
1232
+ * host 侧 `src/settings.ts` 的 `ASR_VOICE_SETTINGS_NAMESPACE` 三方比对)。
1233
+ *
1234
+ * 这里刻意**不再 import** host 侧的常量:client bundle 由 tsdown 打包,把
1235
+ * `src/settings.ts`(整棵 schemastery schema)拖进浏览器产物没有意义;用一个字面量
1236
+ * 加一条三方比对测试守住等价性,比共享导入更轻。
1237
+ */
1238
+ const ASR_VOICE_NS = "dsh-asr-voice";
1220
1239
  /** 把旧版凭据通道适配成新版形状(alpha.3 前的老运行时仍暴露旧面时兜底)。 */
1221
1240
  function adaptLegacyCredentials(legacy) {
1222
1241
  if (legacy === void 0) return void 0;
@@ -1315,10 +1334,13 @@ window.__ModuleLoader__.load({
1315
1334
  }
1316
1335
  return false;
1317
1336
  }
1318
- /** 绑定 host settings scope 并订阅:首次读取当前值,之后 scope 变化回写本地快照并广播。 @param binder - settingsScope 服务的 binder(SettingsScopeBinder)。 @returns 订阅 disposer(随 fiber 清理)。 */
1319
- /** 绑定 host settings scope 并订阅:首次读取当前值,之后 scope 变化回写本地快照并广播。 @param binder - settingsScope 服务(官方 SettingsScopeBinder,随 fiber 注入)。 @returns 订阅 disposer(随 fiber 清理)。 */
1320
- function bindConfigScope(binder) {
1321
- const scope = binder.bind({ namespace: ASR_VOICE_NS });
1337
+ /**
1338
+ * 绑定 host Config form 并订阅:首次读取当前值,之后 form 变化回写本地快照并广播。
1339
+ * @param forms - configForms 服务(DSH 0.1.7 官方 ConfigForms,随 fiber 注入)。
1340
+ * @returns 订阅 disposer(随 fiber 清理)。
1341
+ */
1342
+ function bindConfigScope(forms) {
1343
+ const scope = forms.get(ASR_VOICE_NS);
1322
1344
  voiceScope = scope;
1323
1345
  const applySnapshot = () => {
1324
1346
  const value = scope.getSnapshot().value;
@@ -1372,7 +1394,7 @@ window.__ModuleLoader__.load({
1372
1394
  tts,
1373
1395
  ttsVoice,
1374
1396
  hotkey,
1375
- bargeIn,
1397
+ bargeIn: engine === "segmented" && bargeIn,
1376
1398
  maxSessionMs,
1377
1399
  language: source.language,
1378
1400
  settleMs: turn.settleMs,
@@ -1502,6 +1524,10 @@ window.__ModuleLoader__.load({
1502
1524
  /** 保存草稿:只把真正改过的顶层段写回 host,然后**读回校验**。
1503
1525
  * `SettingsScope.set` 会把失败吞掉并重载宿主状态(promise 成功不代表落盘),所以
1504
1526
  * 判定标准只能是写完之后宿主那边到底剩什么——这也正是官方设置卡的做法。
1527
+ *
1528
+ * 两道防线:① `set` 返回 `false` = 宿主拒写/跳过,直接计入失败集合并提前返回段名,
1529
+ * 不等读回——因为拒写时宿主刚触发过一次状态重载,重载值恰好等于草稿时读回会算出
1530
+ * 「零变更」→ 静默报成功;② 读回校验兜住「set 说成功但宿主没留住」这一类。
1505
1531
  * @returns 未落盘的段名,undefined 表示全部落定。
1506
1532
  */
1507
1533
  async function writeDraft(draft) {
@@ -1519,7 +1545,17 @@ window.__ModuleLoader__.load({
1519
1545
  announce();
1520
1546
  return;
1521
1547
  }
1522
- for (const key of changed) await scope.set(key, draft[key]);
1548
+ const refused = [];
1549
+ for (const key of changed) {
1550
+ let accepted = false;
1551
+ try {
1552
+ accepted = await scope.set(key, draft[key]);
1553
+ } catch {
1554
+ accepted = false;
1555
+ }
1556
+ if (!accepted) refused.push(key);
1557
+ }
1558
+ if (refused.length > 0) return refused[0];
1523
1559
  mergeHostValue(draft);
1524
1560
  announce();
1525
1561
  const resolved = scope.getSnapshot().value;
@@ -2108,8 +2144,8 @@ window.__ModuleLoader__.load({
2108
2144
  behavior: "groupBehavior",
2109
2145
  realtime: "groupRealtime"
2110
2146
  };
2111
- /** 设置卡片:外层折叠与其他插件卡一致(header + chevron + 条件 body)。 */
2112
- function VoiceSettingsCard({ t }) {
2147
+ /** 设置卡片:插件详情页按 view 渲染(summary 一行 / page 表单),兜底路径自绘折叠外壳。 */
2148
+ function VoiceSettingsCard({ t, view }) {
2113
2149
  const version = useConfigVersion();
2114
2150
  const [open, setOpen] = react.useState(false);
2115
2151
  const [showAdvanced, setShowAdvanced] = react.useState(false);
@@ -2283,9 +2319,11 @@ window.__ModuleLoader__.load({
2283
2319
  ];
2284
2320
  const needKey = keyState !== null && !keyState.configured && keyState.failure === null;
2285
2321
  const keyNameMissing = presetById(provider.preset) === void 0 && provider.name.trim() === "";
2322
+ const pageView = view === "page";
2323
+ if (view === "summary") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: t("cardCopy") });
2286
2324
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2287
- className: "dshav-card" + (open ? " dshav-card-open" : ""),
2288
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2325
+ className: "dshav-card" + (open || pageView ? " dshav-card-open" : "") + (pageView ? " dshav-card-page" : ""),
2326
+ children: [pageView ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2289
2327
  type: "button",
2290
2328
  className: "dshav-header",
2291
2329
  "aria-expanded": open,
@@ -2314,8 +2352,8 @@ window.__ModuleLoader__.load({
2314
2352
  strokeLinejoin: "round"
2315
2353
  })
2316
2354
  })]
2317
- }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2318
- className: "dshav-body",
2355
+ }), open || pageView ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2356
+ className: "dshav-body" + (pageView ? " dshav-body-page" : ""),
2319
2357
  children: [
2320
2358
  !writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2321
2359
  className: "dshav-field-hint",
@@ -2399,7 +2437,7 @@ window.__ModuleLoader__.load({
2399
2437
  placeholder: keyState?.configured === true ? t("keyKeepPlaceholder") : t("keyPastePlaceholder"),
2400
2438
  spellCheck: false,
2401
2439
  autoComplete: "off",
2402
- disabled: !writable || keyState?.writable === false,
2440
+ disabled: !writable || keyState === null || keyState?.writable === false,
2403
2441
  onChange: (e) => setKeyInput(e.target.value)
2404
2442
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2405
2443
  type: "button",
@@ -4894,6 +4932,7 @@ window.__ModuleLoader__.load({
4894
4932
  headers: { accept: "text/event-stream" },
4895
4933
  signal: controller.signal
4896
4934
  });
4935
+ if (res.status === 409) return;
4897
4936
  if (!res.ok || res.body === null) {
4898
4937
  if (!disposed) onEvent({
4899
4938
  type: "error",
@@ -6707,7 +6746,8 @@ window.__ModuleLoader__.load({
6707
6746
  const sys = systemDict();
6708
6747
  const title = chat.active ? chat.title : busy ? state === "recording" ? sys.recordingTitle : state === "transcribing" ? sys.transcribingTitle : sys.optimizingTitle : chat.enabled ? sys.micTitleHoldChat : sys.micTitle;
6709
6748
  const ariaLabel = chat.active ? chat.ariaLabel : busy ? state === "recording" ? t("recordingTitle") : state === "transcribing" ? t("transcribingTitle") : t("optimizingTitle") : chat.enabled ? t("micTitleHoldChat") : t("micTitle");
6710
- const shortcut = chat.enabled && config.realtime.hotkey !== "" ? config.realtime.hotkey.replace(/\bCtrl\b/gi, "Control").replace(/\bCmd\b/gi, "Meta") : void 0;
6749
+ const ariaKeyName = (spec) => spec.replace(/\bCtrl\b/gi, "Control").replace(/\bCmd\b/gi, "Meta").replace(/\bAlt\b/gi, "Alt");
6750
+ const shortcut = chat.enabled && config.realtime.hotkey !== "" ? ariaKeyName(config.realtime.hotkey) : config.behavior.hotkey !== "" ? ariaKeyName(config.behavior.hotkey) : void 0;
6711
6751
  const chatStatus = chat.active || !busy && error === null && notice === null ? chat.status : null;
6712
6752
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6713
6753
  className: "dshav-mic-wrap",
@@ -6848,6 +6888,20 @@ window.__ModuleLoader__.load({
6848
6888
  if (e.key === "Escape") {
6849
6889
  e.stopPropagation();
6850
6890
  closePreview();
6891
+ return;
6892
+ }
6893
+ if (e.key !== "Tab") return;
6894
+ const focusables = e.currentTarget.querySelectorAll("button");
6895
+ if (focusables.length === 0) return;
6896
+ const first = focusables[0];
6897
+ const last = focusables[focusables.length - 1];
6898
+ const active = document.activeElement;
6899
+ if (e.shiftKey && (active === first || !e.currentTarget.contains(active))) {
6900
+ e.preventDefault();
6901
+ last.focus();
6902
+ } else if (!e.shiftKey && (active === last || !e.currentTarget.contains(active))) {
6903
+ e.preventDefault();
6904
+ first.focus();
6851
6905
  }
6852
6906
  },
6853
6907
  children: [
@@ -6903,12 +6957,15 @@ window.__ModuleLoader__.load({
6903
6957
  //#endregion
6904
6958
  //#region src/client/index.ts
6905
6959
  const NS = "asr-voice";
6906
- /** 硬依赖:设置卡/配置读写必须的顶层服务(与兄弟插件同款:settingsScope 必须硬依赖,
6907
- * 否则卡片包在 scoped inject 里会因某服务不可注入而永不注册——这正是此前设置卡消失的根因)。 */
6960
+ /** 包名(`plugins.bundle.config` 的 key:bundle 的 package name)。 */
6961
+ const PACKAGE_NAME = "@bittersmilezzz/dsh-asr-voice";
6962
+ /** 硬依赖:设置卡/配置读写必须的顶层服务(DSH 0.1.7 起 configForms 取代
6963
+ * settingsScope;必须硬依赖,否则卡片包在 scoped inject 里会因服务不可注入而
6964
+ * 永不注册——这正是此前设置卡消失的根因)。 */
6908
6965
  const inject = [
6909
6966
  "slots",
6910
6967
  "locale",
6911
- "settingsScope"
6968
+ "configForms"
6912
6969
  ];
6913
6970
  /** 快捷键处理(按住说话 / 点击切换 / 实时对话进出),随 fiber 生命周期注册。 */
6914
6971
  function applyHotkey() {
@@ -7022,19 +7079,21 @@ window.__ModuleLoader__.load({
7022
7079
  const sessions = raw.sessions;
7023
7080
  cancelTurn = (sessionId) => {
7024
7081
  try {
7025
- ((sessions?.scope(sessionId))?.get("conversation"))?.cancel?.()?.catch?.(() => {});
7082
+ ((sessions?.scope(sessionId))?.get("conversation"))?.cancel?.().catch(() => {});
7026
7083
  } catch {}
7027
7084
  };
7028
7085
  });
7029
7086
  ctx.effect(applyHotkey, "asr-voice: hotkey");
7030
- ctx.effect(() => bindConfigScope(ctx.settingsScope), "asr-voice: settings scope sync");
7031
- ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
7032
- name: "settings.plugin.item",
7033
- key: "asr-voice",
7087
+ ctx.effect(() => bindConfigScope(ctx.configForms), "asr-voice: config form sync");
7088
+ ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
7089
+ name: "plugins.bundle.config",
7090
+ key: PACKAGE_NAME,
7034
7091
  locale: NS
7035
- }, () => react_jsx_runtime.jsx(VoiceSettingsCard, { t })));
7092
+ }, (props) => react_jsx_runtime.jsx(VoiceSettingsCard, {
7093
+ t,
7094
+ view: props?.view
7095
+ })));
7036
7096
  ctx.inject([
7037
- "settingsScope",
7038
7097
  "connection",
7039
7098
  "remote",
7040
7099
  "remote.credentials"