dsh-sidecard-ask 1.5.1 → 1.6.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/CHANGELOG.md +30 -0
- package/README.md +24 -6
- package/client.js +228 -13
- package/index.js +74 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
**主题:最新版 DSH(0.2.1-alpha.2)适配 + 新旧包名共载时的单例护栏。**
|
|
6
|
+
|
|
7
|
+
十版本真机矩阵(隔离 DSH_HOME、真实安装启动、HTTP/探针双证据)发现并修复:
|
|
8
|
+
|
|
9
|
+
- **0.2.1-alpha.2 侧边作答引擎失效(真问题)**:该版本把 `subagents.start(provider, request)`
|
|
10
|
+
重构为受管激活 API `subagents.startActivation({provider, label, request, signal, delivery})`
|
|
11
|
+
(返回值由 `{id, localAgent, result, dispose}` 变为 `{childId, result, dispose}`)。
|
|
12
|
+
旧代码的能力门只认 `start()`,导致 provider 明明注册成功(spawn/fork)却自检显示
|
|
13
|
+
「侧边作答引擎:不可用」,侧答走降级。现按运行时方法面自动选择两代 API:新 API 用
|
|
14
|
+
`delivery: 'caller'` 保持"答案不进主对话"的旧语义,流式帧改由子会话 id 归因(新运行时不再
|
|
15
|
+
提供 live Agent 句柄)。0.1.5-rc.3~0.2.1-alpha.1 的调用路径保持不变。
|
|
16
|
+
- **新旧包名同时启用时双重触发(真问题)**:`dsh-selection-followup`(≤1.0.1 旧名)与
|
|
17
|
+
`dsh-sidecard-ask` 被同时装入一个 profile 时,选区旁叠出两个悬浮按钮,一次 Alt+Q 弹两个
|
|
18
|
+
提问框。现新增**页面内家族注册表**(`window.__dshSidecardFamily`):协作的副本按版本号选举
|
|
19
|
+
唯一领导者(同版本先加载者胜),落选副本保留设置页(显示停用说明横幅)但不挂载任何交互面、
|
|
20
|
+
不响应快捷键;无法协作的 1.0.x 旧版通过结构特征识别(无 owner 标记的 `dsa-layer`),
|
|
21
|
+
领导者注入一条样式规则隐藏其整个交互层,并提示卸载旧包。
|
|
22
|
+
- **data-slot 锚点自检结果不稳定(真问题)**:旧逻辑用"当前选区是否落在锚点链内"反推
|
|
23
|
+
"本版本是否支持锚点",选区落在直接挂 body 的节点(引导弹窗、第三方裸 DOM 面板)时会把
|
|
24
|
+
全局判定翻成"缺失",导致同一次自检两次结果不一致。改为构建级探测(全文档是否存在
|
|
25
|
+
`[data-slot]`),单个无锚点选区不再影响全局判定。
|
|
26
|
+
|
|
27
|
+
客户端自检上报新增 `family` 字段(leader/legacy/peers),宿主侧白名单做同样的长度裁剪。
|
|
28
|
+
|
|
29
|
+
本机实测:verify 53 / contract 323 / float-placement 38 / smoke 163 = **577 项全部通过**;
|
|
30
|
+
另在 0.2.0-rc.2 与 0.2.1-alpha.2 真机(headless)复核:两版引擎均可用、共载单实例、
|
|
31
|
+
锚点判定稳定、better-sidebar 0.25.0 承载面识别正常。
|
|
32
|
+
|
|
3
33
|
## 1.5.1
|
|
4
34
|
|
|
5
35
|
**主题:1.5.0 的发布收尾——把发布闸门补全,并把校验记录进版本号。**
|
package/README.md
CHANGED
|
@@ -392,11 +392,27 @@ node tools/compat-probe.mjs --json tools/.cache/matrix.json
|
|
|
392
392
|
| 没有 `data-slot` 锚点 | 检测到选区但槽位路径为空 → 判定"区域锚点不可用",**停用区域过滤**(否则 `captureZones:chat` 会静默失效)并在设置页自检里标明 | `contract-test`:`shouldOffer(..., {anchors:false})` 断言放宽且返回 `zoneFiltering:'unavailable'` |
|
|
393
393
|
| 路由 kind 不被接受 | `prefix` 注册失败 → 退化为**逐方法精确路由**(`state/ask/cancel/config/reset`),处理器不变 | 前缀路由由 smoke 全流程覆盖;退化分支为纯 fallback(未在真机触发过) |
|
|
394
394
|
|
|
395
|
-
### 7.3
|
|
395
|
+
### 7.3 真机验证记录(1.6.0,2026-10-11)
|
|
396
|
+
|
|
397
|
+
1.6.0 把 §7.1/§7.2 的产物层证据升级为**真机证据**:最近 10 个发布版(0.1.5-rc.3 → 0.2.1-alpha.2,
|
|
398
|
+
按 npm 发布时间)全部在隔离 `DSH_HOME` 中真实安装、启动并读取运行时能力面;
|
|
399
|
+
0.2.0-rc.2(= npm `latest`)与 0.2.1-alpha.2(= npm `next`)另做无头浏览器真机验证。
|
|
400
|
+
|
|
401
|
+
- 0.1.5-rc.3 … 0.2.1-alpha.1:引擎可用,provider=spawn/fork,0 启动告警。
|
|
402
|
+
- **0.2.1-alpha.2:1.5.1 真机不可用 → 1.6.0 已修复**。该版本把子代理 API 从 `subagents.start(provider, request)`
|
|
403
|
+
重构为 `subagents.startActivation({provider, label, request, signal, delivery})`,1.5.1 的能力门只认旧方法
|
|
404
|
+
(provider 其实注册成功,自检却显示"不可用")。1.6.0 按运行时方法面自动选择两代 API,新 API 用
|
|
405
|
+
`delivery:'caller'` 保持"答案不进主对话",流式帧按子会话 id 归因。
|
|
406
|
+
- 更早的 0.1.2-rc.1 … 0.1.4 仍只有产物层证据(§7.1/§7.2 的降级分支仍保留,双 API 调度不改动旧路径)。
|
|
407
|
+
- 生态侧:25 个侧边卡片/侧边栏类插件做了产物扫描 + 4 组共载真机,localStorage 键 / 路由前缀 / tab kind /
|
|
408
|
+
patch id / 配置目录 / Alt+Q 零硬冲突;旧名包 `dsh-selection-followup@1.0.1` 共载时的双重触发由
|
|
409
|
+
1.6.0 的页面内家族单例选举解决。完整矩阵、复现脚本与截图见 `.compat-report/`。
|
|
410
|
+
|
|
411
|
+
### 7.4 更早版本仍未做真机验证的部分(如实标注)
|
|
396
412
|
|
|
397
413
|
- 上表所有 ✅/❌ 都是**包内容层面的证据**(字符串/签名存在于该版本的发布产物),不等于"插件在该版本上跑起来过"。
|
|
398
|
-
|
|
399
|
-
- 在 0.1.2-rc.1 … 0.1.
|
|
414
|
+
真机覆盖为 0.1.5-rc.3 → 0.2.1-alpha.2(1.6.0 实测)与 0.1.7-rc.2(§10.1 的历史实测)。
|
|
415
|
+
- 在 0.1.2-rc.1 … 0.1.4 这几个版本上,只验证了"所需 API 是否存在 + 插件有为缺失准备的分支",
|
|
400
416
|
**没有**在那些版本上安装并启动过插件。
|
|
401
417
|
- `inputActions` 作为会话槽位标准 prop:13 个版本的 `dsh-client-ui-conversation` 产物里都有该名字,
|
|
402
418
|
0.1.7-rc.2 的槽位目录也把它列为 `conversation.input.right` 的 standardProps;**更早版本是否真的把它下发给该槽位条目未验证**,
|
|
@@ -494,8 +510,8 @@ node test/smoke-test.mjs # 端到端:流式/截断/取消/持久化/失
|
|
|
494
510
|
```
|
|
495
511
|
|
|
496
512
|
四个脚本都以 `process.exitCode` 反映结果,失败会列出具体条目;测试会把 `DSH_HOME` 指向临时目录,不会污染真实配置。
|
|
497
|
-
当前规模(1.
|
|
498
|
-
smoke **
|
|
513
|
+
当前规模(1.6.0 本机实测,四项均 exit 0):verify **53** 项 + contract **323** 项 + float-placement **38** 项 +
|
|
514
|
+
smoke **163** 项 = **577 项全部通过**。
|
|
499
515
|
|
|
500
516
|
> 1.5.0 之前这里还放过 `tools/balance-check.mjs` 与 `tools/i18n-check.mjs` 两个「替代检查」。
|
|
501
517
|
> 它们已被**删除**:前者用「正则字面量 vs 除号」的启发式判断,把 8 个源文件全部误报为残损;
|
|
@@ -635,7 +651,9 @@ npm deprecate "<旧包名>@*" "Renamed to <新包名> - <原因>" # 旧名
|
|
|
635
651
|
1. **区域归类是启发式的**:`data-slot` 名称来自 harness 自己;其它插件若在自己的面板里不再经由槽位渲染,
|
|
636
652
|
该区域会被归为"其它区域"——用 `captureZones: auto` + `showInUnclassified: true`(默认)仍然可用,或改用 `captureZones` 限制。
|
|
637
653
|
本机实测能识别:`conversation.*`(聊天)、`rightbar` / `sidebar.right.*` / 名字含 task·todo·schedule·team·job·plan 的面板(任务)。
|
|
638
|
-
2.
|
|
654
|
+
2. **浏览器内的视觉与交互**:1.6.0 起用无头 Edge(Playwright,系统浏览器 `channel:'msedge'`)做真机验证,
|
|
655
|
+
脚本见 `.compat-report/scripts/`,截图见 `.compat-report/shots/`——选区浮按钮、Alt+Q 弹层、共载单实例、
|
|
656
|
+
引擎自检均已在 0.2.0-rc.2 与 0.2.1-alpha.2 上自动核对。桌面 Electron 的视觉观感仍只有 2026-09-30 的人工实测(§5.3)。
|
|
639
657
|
已做的保障是:只使用 `--dsw-alias-*` 主题令牌、只在槽位内渲染(`shell.overlay` / `settings.section` / `conversation.input.right`)、不写 `document.body`、不 import harness 客户端包。
|
|
640
658
|
2b. **1.5.0 的浮层改动:四个测试脚本已在本机实跑通过(各 exit 0),但页面里的观感仍未验证**:
|
|
641
659
|
写这个版本的前半段,本工作区的 shell 工具整体不可用(任何命令都返回
|
package/client.js
CHANGED
|
@@ -47,7 +47,7 @@ window.__ModuleLoader__.load({
|
|
|
47
47
|
* the Host and the module's `api.version` both read it, so a report can
|
|
48
48
|
* never claim a generation the browser is not actually running.
|
|
49
49
|
*/
|
|
50
|
-
const CLIENT_VERSION = '1.
|
|
50
|
+
const CLIENT_VERSION = '1.6.0'
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Tab kind served by the DSH native right rail (also its implementation id).
|
|
@@ -294,6 +294,10 @@ window.__ModuleLoader__.load({
|
|
|
294
294
|
none: '无',
|
|
295
295
|
unsupportedTrigger: '无法识别该快捷键,请用形如 Alt+Q / Ctrl+Shift+K 的写法',
|
|
296
296
|
surfaceUnavailable: '所选侧边承载面不可用',
|
|
297
|
+
familySuppressed: '检测到同系列更新版本({version})已在本页运行,本实例已自动停用划词触发与快捷键,界面由更新版本接管。建议在插件页卸载本旧版本。',
|
|
298
|
+
familyLegacyToast: '检测到旧版 dsh-selection-followup 同时启用,已由本版本接管;建议卸载旧包',
|
|
299
|
+
familyLegacyDiag: '旧版共载',
|
|
300
|
+
familyPeers: '同页实例',
|
|
297
301
|
},
|
|
298
302
|
en: {
|
|
299
303
|
triggerLabel: 'Ask about selection',
|
|
@@ -430,6 +434,10 @@ window.__ModuleLoader__.load({
|
|
|
430
434
|
none: 'none',
|
|
431
435
|
unsupportedTrigger: 'Unrecognized shortcut — use a form like Alt+Q / Ctrl+Shift+K',
|
|
432
436
|
surfaceUnavailable: 'The chosen side surface is unavailable',
|
|
437
|
+
familySuppressed: 'A newer family build ({version}) is already running on this page; this copy has disabled its trigger and shortcut so only one UI answers. Uninstall this older package.',
|
|
438
|
+
familyLegacyToast: 'Legacy dsh-selection-followup detected alongside this plugin; this build took over. Uninstall the old package.',
|
|
439
|
+
familyLegacyDiag: 'Legacy co-load',
|
|
440
|
+
familyPeers: 'Copies on page',
|
|
433
441
|
},
|
|
434
442
|
}
|
|
435
443
|
|
|
@@ -659,6 +667,12 @@ window.__ModuleLoader__.load({
|
|
|
659
667
|
zoneAnchors: null,
|
|
660
668
|
/** Which ladder rung each registration actually landed on. */
|
|
661
669
|
slots: {},
|
|
670
|
+
/** §4.5 family election: this copy owns the interaction surface. */
|
|
671
|
+
familyLeader: true,
|
|
672
|
+
/** Other cooperating copies currently live in this page. */
|
|
673
|
+
familyPeers: [],
|
|
674
|
+
/** A non-cooperating legacy build (≤1.0.x) is present and suppressed. */
|
|
675
|
+
familyLegacy: false,
|
|
662
676
|
revision: 0,
|
|
663
677
|
},
|
|
664
678
|
listeners: new Set(),
|
|
@@ -686,6 +700,94 @@ window.__ModuleLoader__.load({
|
|
|
686
700
|
return store.state
|
|
687
701
|
}
|
|
688
702
|
|
|
703
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
704
|
+
// §4.5 family singleton
|
|
705
|
+
//
|
|
706
|
+
// The plugin shipped under two npm names (`dsh-selection-followup` ≤1.0.1
|
|
707
|
+
// and `dsh-sidecard-ask` ≥1.1.0), and a profile can accidentally enable
|
|
708
|
+
// BOTH bundles — or the same bundle twice. Two live copies used to mean
|
|
709
|
+
// two trigger buttons stacked on the selection and two Alt+Q popovers.
|
|
710
|
+
//
|
|
711
|
+
// Cooperating copies (any build with this section) share one registry on
|
|
712
|
+
// `window` and elect a single leader by version (ties → first loaded);
|
|
713
|
+
// non-leaders keep their settings page but mount NO interaction surface.
|
|
714
|
+
//
|
|
715
|
+
// The old 1.0.x build cannot cooperate: it is detected structurally (its
|
|
716
|
+
// overlay carries the same `dsa-layer` class without an owner marker), the
|
|
717
|
+
// leader hides its UI with one stylesheet rule, and the user is told to
|
|
718
|
+
// uninstall the old package.
|
|
719
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
720
|
+
|
|
721
|
+
const FAMILY_WINDOW_KEY = '__dshSidecardFamily'
|
|
722
|
+
const INSTANCE_ID = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
|
723
|
+
const OWNER_TOKEN = `${PLUGIN_ID}@${CLIENT_VERSION}#${INSTANCE_ID}`
|
|
724
|
+
|
|
725
|
+
/** Compare dotted numeric versions; non-numeric parts sort first. */
|
|
726
|
+
function compareVersions(a, b) {
|
|
727
|
+
const parts = v => String(v).split(/[.+-]/).map(part => (/^\d+$/.test(part) ? Number(part) : -1))
|
|
728
|
+
const pa = parts(a)
|
|
729
|
+
const pb = parts(b)
|
|
730
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
|
|
731
|
+
const x = pa[i] ?? -1
|
|
732
|
+
const y = pb[i] ?? -1
|
|
733
|
+
if (x !== y) return x < y ? -1 : 1
|
|
734
|
+
}
|
|
735
|
+
return 0
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Elect the leader: highest version wins; a tie goes to the member that
|
|
740
|
+
* joined first (Map insertion order). Pure so the contract test can pin it.
|
|
741
|
+
* @returns {string} the winning member id.
|
|
742
|
+
*/
|
|
743
|
+
function electFamilyLeader(members, selfId) {
|
|
744
|
+
let winner = null
|
|
745
|
+
for (const member of members) {
|
|
746
|
+
// Strictly greater version replaces; an equal version keeps the member
|
|
747
|
+
// that joined earlier (Map insertion order = load order).
|
|
748
|
+
if (winner === null || compareVersions(member.version, winner.version) > 0) {
|
|
749
|
+
winner = member
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return winner?.id ?? selfId
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** Fetch-or-create the cross-copy registry (survives HMR on `window`). */
|
|
756
|
+
function familyTable(win) {
|
|
757
|
+
const existing = win[FAMILY_WINDOW_KEY]
|
|
758
|
+
if (existing !== undefined && existing !== null && typeof existing.subscribe === 'function') {
|
|
759
|
+
return existing
|
|
760
|
+
}
|
|
761
|
+
const members = new Map()
|
|
762
|
+
const listeners = new Set()
|
|
763
|
+
const emit = () => { for (const fn of [...listeners]) { try { fn() } catch { /* peer listener */ } } }
|
|
764
|
+
const table = {
|
|
765
|
+
join(member) { members.set(member.id, member); emit() },
|
|
766
|
+
leave(id) { members.delete(id); emit() },
|
|
767
|
+
list: () => [...members.values()],
|
|
768
|
+
subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn) },
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
Object.defineProperty(win, FAMILY_WINDOW_KEY, { value: table, configurable: true, writable: true })
|
|
772
|
+
} catch {
|
|
773
|
+
win[FAMILY_WINDOW_KEY] = table
|
|
774
|
+
}
|
|
775
|
+
return table
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Find legacy (≤1.0.x, non-cooperating) family UI: a `dsa-layer` carrying
|
|
780
|
+
* a trigger button but no owner marker. Every layer this build renders
|
|
781
|
+
* stamps `data-dsa-owner`, so an unmarked layer can only be the old build.
|
|
782
|
+
*/
|
|
783
|
+
function findLegacyLayers(root) {
|
|
784
|
+
if (typeof root.querySelectorAll !== 'function') return []
|
|
785
|
+
try {
|
|
786
|
+
return [...root.querySelectorAll('.dsa-layer:not([data-dsa-owner])')]
|
|
787
|
+
.filter(node => node.querySelector?.('.dsa-trigger, .dsa-pop') !== null)
|
|
788
|
+
} catch { return [] }
|
|
789
|
+
}
|
|
790
|
+
|
|
689
791
|
/**
|
|
690
792
|
* In-session override of the configured floating-layer shape. Deliberately
|
|
691
793
|
* NOT persisted: the settings page owns the durable default, and a reload
|
|
@@ -1210,6 +1312,12 @@ window.__ModuleLoader__.load({
|
|
|
1210
1312
|
...(surfaces.better.reason === null ? {} : { reason: String(surfaces.better.reason) }),
|
|
1211
1313
|
features: surfaces.better.features,
|
|
1212
1314
|
},
|
|
1315
|
+
// §4.5 family singleton verdict (host-side diagnostics only).
|
|
1316
|
+
family: {
|
|
1317
|
+
leader: state.familyLeader !== false,
|
|
1318
|
+
peers: Array.isArray(state.familyPeers) ? state.familyPeers : [],
|
|
1319
|
+
legacy: state.familyLegacy === true,
|
|
1320
|
+
},
|
|
1213
1321
|
}
|
|
1214
1322
|
}
|
|
1215
1323
|
|
|
@@ -1368,6 +1476,15 @@ window.__ModuleLoader__.load({
|
|
|
1368
1476
|
.dsa-check{display:flex;align-items:center;gap:6px;font-size:12px}
|
|
1369
1477
|
.dsa-diag{display:flex;flex-direction:column;gap:2px;font-size:12px;
|
|
1370
1478
|
color:var(--dsw-alias-label-secondary, #555)}
|
|
1479
|
+
/* §4.5 legacy coexistence: an unmarked dsa-layer can only be the old
|
|
1480
|
+
dsh-selection-followup ≤1.0.x build. When this build is the elected
|
|
1481
|
+
leader it hides the old build's whole interaction surface so exactly
|
|
1482
|
+
one trigger button / popover answers the selection. Our own layer always
|
|
1483
|
+
carries data-dsa-owner, so this never matches current-generation UI. */
|
|
1484
|
+
.dsa-layer:not([data-dsa-owner]){display:none!important}
|
|
1485
|
+
.dsa-family-notice{padding:8px 10px;border-radius:8px;font-size:12px;
|
|
1486
|
+
border:1px solid var(--dsw-alias-state-warn-primary, #b8860b);
|
|
1487
|
+
color:var(--dsw-alias-state-warn-primary, #b8860b)}
|
|
1371
1488
|
`
|
|
1372
1489
|
|
|
1373
1490
|
/** Render the stylesheet as a React element (removed with its component). */
|
|
@@ -1977,10 +2094,19 @@ window.__ModuleLoader__.load({
|
|
|
1977
2094
|
/** The `shell.overlay` entry: styles + trigger + popover + cards + toast. */
|
|
1978
2095
|
function OverlayRoot({ actions }) {
|
|
1979
2096
|
const state = useStoreState()
|
|
2097
|
+
// §4.5 a non-leader copy mounts no interaction surface at all (its
|
|
2098
|
+
// settings page stays available to explain the situation).
|
|
2099
|
+
if (state.familyLeader === false) return null
|
|
1980
2100
|
const config = state.config
|
|
1981
2101
|
const candidate = state.trigger
|
|
1982
2102
|
const popover = state.popover
|
|
1983
|
-
return h('div', {
|
|
2103
|
+
return h('div', {
|
|
2104
|
+
className: 'dsa-layer',
|
|
2105
|
+
'data-dsa-root': '',
|
|
2106
|
+
// §4.5 stamps ownership so the legacy-suppression stylesheet can
|
|
2107
|
+
// distinguish this build's layer from an unmarked ≤1.0.x layer.
|
|
2108
|
+
'data-dsa-owner': OWNER_TOKEN,
|
|
2109
|
+
},
|
|
1984
2110
|
h(Styles, null),
|
|
1985
2111
|
candidate !== null && popover === null && config.trigger !== 'shortcut'
|
|
1986
2112
|
? h(TriggerButton, { candidate, onOpen: () => actions.openPopover(candidate) })
|
|
@@ -2060,11 +2186,20 @@ window.__ModuleLoader__.load({
|
|
|
2060
2186
|
}
|
|
2061
2187
|
}
|
|
2062
2188
|
|
|
2189
|
+
// §4.5 a non-leader copy explains why its interaction surface is gone.
|
|
2190
|
+
const familyBanner = state.familyLeader === false
|
|
2191
|
+
? h('div', { className: 'dsa-family-notice', role: 'status' },
|
|
2192
|
+
t('familySuppressed', { version: (state.familyPeers[0]?.version) ?? '?' }))
|
|
2193
|
+
: (state.familyLegacy === true
|
|
2194
|
+
? h('div', { className: 'dsa-family-notice', role: 'status' }, t('familyLegacyToast'))
|
|
2195
|
+
: null)
|
|
2196
|
+
|
|
2063
2197
|
return h('div', { className: 'dsa-settings', 'data-dsa-root': '' },
|
|
2064
2198
|
h(Styles, null),
|
|
2065
2199
|
h('div', null,
|
|
2066
2200
|
h('div', { className: 'dsa-title' }, t('settingsTitle')),
|
|
2067
2201
|
h('div', { className: 'dsa-muted' }, t('settingsDesc'))),
|
|
2202
|
+
familyBanner,
|
|
2068
2203
|
h('div', { className: 'dsa-grid' },
|
|
2069
2204
|
field(t('setTrigger'), select('trigger', [
|
|
2070
2205
|
['selection', t('setTriggerSelection')],
|
|
@@ -2671,20 +2806,30 @@ window.__ModuleLoader__.load({
|
|
|
2671
2806
|
// ── selection listeners ──────────────────────────────────────────
|
|
2672
2807
|
const evaluate = () => {
|
|
2673
2808
|
pendingTimer = null
|
|
2809
|
+
// §4.5 a non-leader copy must not surface its trigger, even if a
|
|
2810
|
+
// stale selection event arrives.
|
|
2811
|
+
if (store.state.familyLeader === false) return
|
|
2674
2812
|
const config = store.state.config
|
|
2675
2813
|
const candidate = readSelection()
|
|
2676
2814
|
// Zone anchors: the harness stamps every slot outlet with
|
|
2677
|
-
// `data-slot="<key>"`.
|
|
2678
|
-
// selection
|
|
2679
|
-
//
|
|
2680
|
-
//
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2815
|
+
// `data-slot="<key>"`. This is a property of the BUILD, not of the
|
|
2816
|
+
// current selection — a selection on a body-mounted node (an
|
|
2817
|
+
// onboarding modal, another plugin's raw-DOM panel) has an empty
|
|
2818
|
+
// slot path even though the frame is full of anchors. Probe the
|
|
2819
|
+
// whole document instead, or one such selection would flip the
|
|
2820
|
+
// global verdict to "missing" and silently disable zone filtering.
|
|
2821
|
+
if (candidate !== null && store.state.zoneAnchors !== true) {
|
|
2822
|
+
const buildHasAnchors = candidate.slotPath.length > 0
|
|
2823
|
+
|| (typeof document !== 'undefined' && document.querySelector('[data-slot]') !== null)
|
|
2824
|
+
if (buildHasAnchors) {
|
|
2825
|
+
if (store.state.zoneAnchors !== true) scheduleReport(0)
|
|
2826
|
+
store.set({ zoneAnchors: true })
|
|
2827
|
+
} else if (store.state.zoneAnchors === null && candidate.slotPath.length === 0) {
|
|
2828
|
+
// Only the build-wide miss (and never a single unanchored
|
|
2829
|
+
// selection) flips the verdict to unavailable.
|
|
2830
|
+
store.set({ zoneAnchors: false })
|
|
2831
|
+
scheduleReport(0)
|
|
2832
|
+
console.info(`[${PLUGIN_ID}] 该版本没有 data-slot 锚点,区域过滤不可用,已按"全部区域"工作`)
|
|
2688
2833
|
}
|
|
2689
2834
|
}
|
|
2690
2835
|
const decision = shouldOffer(candidate, config, { anchors: store.state.zoneAnchors !== false })
|
|
@@ -2715,6 +2860,9 @@ window.__ModuleLoader__.load({
|
|
|
2715
2860
|
}
|
|
2716
2861
|
|
|
2717
2862
|
const onKeyDown = (event) => {
|
|
2863
|
+
// §4.5 only the leader answers the shortcut; otherwise two copies
|
|
2864
|
+
// would each preventDefault and each open their own popover.
|
|
2865
|
+
if (store.state.familyLeader === false) return
|
|
2718
2866
|
const config = store.state.config
|
|
2719
2867
|
const facts = parseShortcut(config.shortcut)
|
|
2720
2868
|
if (matchesShortcut(event, facts)) {
|
|
@@ -3153,6 +3301,61 @@ window.__ModuleLoader__.load({
|
|
|
3153
3301
|
// Corrupt-but-parseable history must never block startup.
|
|
3154
3302
|
console.warn(`[${PLUGIN_ID}] card history restore failed:`, error)
|
|
3155
3303
|
}
|
|
3304
|
+
|
|
3305
|
+
// §4.5 family singleton: register this copy, (re)elect on every join
|
|
3306
|
+
// or leave, and gate the interaction surface on the verdict.
|
|
3307
|
+
const family = familyTable(window)
|
|
3308
|
+
family.join({ id: INSTANCE_ID, version: CLIENT_VERSION, at: Date.now() })
|
|
3309
|
+
const reelect = () => {
|
|
3310
|
+
const members = family.list()
|
|
3311
|
+
const leaderId = electFamilyLeader(members, INSTANCE_ID)
|
|
3312
|
+
const leader = leaderId === INSTANCE_ID
|
|
3313
|
+
const peers = members
|
|
3314
|
+
.filter(member => member.id !== INSTANCE_ID)
|
|
3315
|
+
.map(member => ({ id: member.id, version: member.version }))
|
|
3316
|
+
store.set({ familyLeader: leader, familyPeers: peers })
|
|
3317
|
+
scheduleReport(0)
|
|
3318
|
+
if (leader) detectLegacy()
|
|
3319
|
+
}
|
|
3320
|
+
const unsubscribeFamily = family.subscribe(reelect)
|
|
3321
|
+
|
|
3322
|
+
// Legacy (≤1.0.x) detection: it never joins the registry, so watch
|
|
3323
|
+
// the overlay tree for an unmarked dsa-layer carrying family UI. The
|
|
3324
|
+
// suppression stylesheet then hides it; we only need detection to
|
|
3325
|
+
// raise the toast/report. Repeated matches are a no-op.
|
|
3326
|
+
let legacyAnnounced = false
|
|
3327
|
+
let legacyObserver = null
|
|
3328
|
+
const detectLegacy = () => {
|
|
3329
|
+
if (store.state.familyLeader !== true || legacyAnnounced === true) return
|
|
3330
|
+
const root = typeof document !== 'undefined' ? document : null
|
|
3331
|
+
if (root === null) return
|
|
3332
|
+
const found = findLegacyLayers(root).length > 0
|
|
3333
|
+
if (found === false) return
|
|
3334
|
+
legacyAnnounced = true
|
|
3335
|
+
// Inject the suppression rule directly: the React stylesheet only
|
|
3336
|
+
// exists while our overlay is mounted, but the legacy layer must
|
|
3337
|
+
// stay hidden even during its own re-renders.
|
|
3338
|
+
try {
|
|
3339
|
+
if (root.getElementById('dsa-family-legacy-style') === null) {
|
|
3340
|
+
const style = root.createElement('style')
|
|
3341
|
+
style.id = 'dsa-family-legacy-style'
|
|
3342
|
+
style.textContent = '.dsa-layer:not([data-dsa-owner]){display:none!important}'
|
|
3343
|
+
root.head?.appendChild(style)
|
|
3344
|
+
}
|
|
3345
|
+
} catch { /* React stylesheet already carries the same rule */ }
|
|
3346
|
+
store.set({ familyLegacy: true })
|
|
3347
|
+
toast(t('familyLegacyToast'))
|
|
3348
|
+
scheduleReport(0)
|
|
3349
|
+
}
|
|
3350
|
+
const legacyTimers = [300, 1200, 3000].map(delay => setTimeout(detectLegacy, delay))
|
|
3351
|
+
if (typeof document !== 'undefined' && typeof MutationObserver === 'function') {
|
|
3352
|
+
legacyObserver = new MutationObserver(() => detectLegacy())
|
|
3353
|
+
try {
|
|
3354
|
+
legacyObserver.observe(document.documentElement ?? document, { childList: true, subtree: true })
|
|
3355
|
+
} catch { /* document without a root yet — timers still cover it */ }
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
reelect()
|
|
3156
3359
|
void refreshState().catch(() => { /* the settings page shows the failure */ })
|
|
3157
3360
|
// One report after the slot ladders have had time to settle, so the
|
|
3158
3361
|
// Host ends up holding the CLIENT's real registration state.
|
|
@@ -3170,6 +3373,15 @@ window.__ModuleLoader__.load({
|
|
|
3170
3373
|
pendingProofs.clear()
|
|
3171
3374
|
if (toastTimer !== null) clearTimeout(toastTimer)
|
|
3172
3375
|
if (reportTimer !== null) clearTimeout(reportTimer)
|
|
3376
|
+
for (const timer of legacyTimers) clearTimeout(timer)
|
|
3377
|
+
try {
|
|
3378
|
+
if (typeof document !== 'undefined' && document.getElementById) {
|
|
3379
|
+
document.getElementById('dsa-family-legacy-style')?.remove()
|
|
3380
|
+
}
|
|
3381
|
+
} catch { /* torn down */ }
|
|
3382
|
+
try { legacyObserver?.disconnect() } catch { /* already gone */ }
|
|
3383
|
+
try { unsubscribeFamily() } catch { /* registry torn down */ }
|
|
3384
|
+
try { family.leave(INSTANCE_ID) } catch { /* registry torn down */ }
|
|
3173
3385
|
store.set({
|
|
3174
3386
|
trigger: null,
|
|
3175
3387
|
popover: null,
|
|
@@ -3222,6 +3434,9 @@ window.__ModuleLoader__.load({
|
|
|
3222
3434
|
renderRichText,
|
|
3223
3435
|
pickSurface,
|
|
3224
3436
|
floatPlacement,
|
|
3437
|
+
compareVersions,
|
|
3438
|
+
electFamilyLeader,
|
|
3439
|
+
findLegacyLayers,
|
|
3225
3440
|
localizeError,
|
|
3226
3441
|
errorCodes: ERROR_CODES,
|
|
3227
3442
|
resolveLanguage,
|
package/index.js
CHANGED
|
@@ -42,7 +42,7 @@ export const name = 'dsh-sidecard-ask'
|
|
|
42
42
|
export const inject = ['webServer']
|
|
43
43
|
|
|
44
44
|
/** Version of this plugin (kept in step with package.json by test/verify.mjs). */
|
|
45
|
-
export const PLUGIN_VERSION = '1.
|
|
45
|
+
export const PLUGIN_VERSION = '1.6.0'
|
|
46
46
|
|
|
47
47
|
/** Route prefix of the plugin's own API. */
|
|
48
48
|
export const ROUTE_PREFIX = '/sidecard-ask/api'
|
|
@@ -542,10 +542,64 @@ function createSideEngine(ctx, configOf) {
|
|
|
542
542
|
}
|
|
543
543
|
}), 'sidecard-ask: durable chunk bridge')
|
|
544
544
|
|
|
545
|
-
/**
|
|
545
|
+
/**
|
|
546
|
+
* The optional subagents service, or undefined.
|
|
547
|
+
*
|
|
548
|
+
* 0.2.1-alpha.2 replaced `subagents.start(provider, request)` with the
|
|
549
|
+
* managed-activations API `subagents.startActivation(spec)`; a build carries
|
|
550
|
+
* exactly one of the two launch methods, so either one means the service is
|
|
551
|
+
* present and launchable.
|
|
552
|
+
*/
|
|
546
553
|
function subagentsService() {
|
|
547
554
|
const service = ctx.get('subagents')
|
|
548
|
-
|
|
555
|
+
if (service === undefined) return undefined
|
|
556
|
+
const launchable = typeof service.start === 'function' || typeof service.startActivation === 'function'
|
|
557
|
+
return launchable ? service : undefined
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Whether this runtime speaks the managed-activations API. */
|
|
561
|
+
function usesActivationApi(service) {
|
|
562
|
+
return typeof service?.startActivation === 'function' && typeof service?.start !== 'function'
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Start one child answer, normalizing both subagents API generations to the
|
|
567
|
+
* shape the stream bridge consumes (`{id, localAgent?, result, dispose}`).
|
|
568
|
+
*
|
|
569
|
+
* Old API (≤0.2.1-alpha.1): `start(provider, request)` returns a SubagentRun.
|
|
570
|
+
* New API (0.2.1-alpha.2+): `startActivation({provider, label, request,
|
|
571
|
+
* signal, delivery: 'caller'})` returns `{childId, result, dispose}`; the
|
|
572
|
+
* caller-delivery policy keeps the child out of the parent's visible
|
|
573
|
+
* conversation — the same independent-answer semantics the old run had.
|
|
574
|
+
*
|
|
575
|
+
* @returns {Promise<{id: string, localAgent?: object, result: Promise<object>,
|
|
576
|
+
* dispose: Function, api: 'start'|'activation'}>}
|
|
577
|
+
*/
|
|
578
|
+
async function startSubagent(service, provider, fields) {
|
|
579
|
+
if (typeof service.start === 'function') {
|
|
580
|
+
const run = await service.start(provider, fields)
|
|
581
|
+
return { id: run.id, localAgent: run.localAgent, result: run.result, dispose: run.dispose, api: 'start' }
|
|
582
|
+
}
|
|
583
|
+
const activation = await service.startActivation({
|
|
584
|
+
provider,
|
|
585
|
+
label: fields.label,
|
|
586
|
+
delivery: 'caller',
|
|
587
|
+
signal: fields.signal,
|
|
588
|
+
request: {
|
|
589
|
+
prompt: fields.prompt,
|
|
590
|
+
parent: fields.parent,
|
|
591
|
+
...(fields.persona === undefined ? {} : { persona: fields.persona }),
|
|
592
|
+
...(fields.toolFilter === undefined ? {} : { toolFilter: fields.toolFilter }),
|
|
593
|
+
},
|
|
594
|
+
})
|
|
595
|
+
return {
|
|
596
|
+
id: activation.childId,
|
|
597
|
+
// No live Agent handle on the activation face; frame attribution falls
|
|
598
|
+
// back to the child session id (onFrame already matches that path).
|
|
599
|
+
result: activation.result,
|
|
600
|
+
dispose: () => activation.dispose(),
|
|
601
|
+
api: 'activation',
|
|
602
|
+
}
|
|
549
603
|
}
|
|
550
604
|
|
|
551
605
|
/** Registered subagent provider names (empty when the service is absent). */
|
|
@@ -825,7 +879,7 @@ function createSideEngine(ctx, configOf) {
|
|
|
825
879
|
const personaSupported = caps?.persona === true
|
|
826
880
|
const toolFilterSupported = caps?.toolFilter === true
|
|
827
881
|
const promptText = personaSupported ? prompt : `${SIDE_PERSONA}\n\n${prompt}`
|
|
828
|
-
const run = await service
|
|
882
|
+
const run = await startSubagent(service, provider, {
|
|
829
883
|
label: `划词追问:${question.slice(0, 40)}`,
|
|
830
884
|
prompt: [{ type: 'text', text: promptText }],
|
|
831
885
|
parent: parent.agent,
|
|
@@ -1133,6 +1187,22 @@ export function apply(ctx, patchConfig) {
|
|
|
1133
1187
|
.map(feature => feature.slice(0, 40))
|
|
1134
1188
|
}
|
|
1135
1189
|
}
|
|
1190
|
+
// §4.5 family singleton verdict (bounded, same hardening as slots).
|
|
1191
|
+
if (raw.family !== null && typeof raw.family === 'object' && !Array.isArray(raw.family)) {
|
|
1192
|
+
out.family = {
|
|
1193
|
+
leader: flag(raw.family.leader) ?? true,
|
|
1194
|
+
legacy: flag(raw.family.legacy) ?? false,
|
|
1195
|
+
peers: Array.isArray(raw.family.peers)
|
|
1196
|
+
? raw.family.peers
|
|
1197
|
+
.filter(peer => peer !== null && typeof peer === 'object')
|
|
1198
|
+
.slice(0, 8)
|
|
1199
|
+
.map(peer => ({
|
|
1200
|
+
id: short(peer.id, 64) ?? 'peer',
|
|
1201
|
+
...(short(peer.version, 24) !== undefined ? { version: short(peer.version, 24) } : {}),
|
|
1202
|
+
}))
|
|
1203
|
+
: [],
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1136
1206
|
return out
|
|
1137
1207
|
}
|
|
1138
1208
|
|