dsh-sidecard-ask 2.1.8 → 2.1.9
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 +55 -0
- package/README.md +67 -0
- package/client.js +308 -38
- package/index.js +66 -11
- package/locale/en.json +3 -1
- package/locale/zh.json +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.9
|
|
4
|
+
|
|
5
|
+
**主题:承载面适配对齐 dsh-better-sidebar 0.28.2,修掉两个已复现的用户可见缺陷。**
|
|
6
|
+
|
|
7
|
+
触发是一次专家评审(架构 / 安全 / 验收三线独立复核):0.28.x 把 better 的 tab 从它自己的
|
|
8
|
+
workbench 搬进了 **DSH 原生右栏**,而本插件仍按「better 是一个独立面板」建模——落点矩阵、
|
|
9
|
+
`dockRail` 锚点、tab 归属、菜单入口四处同时失真。四个发布闸门当时全绿(1928),
|
|
10
|
+
但绿只证明旧契约面没坏。
|
|
11
|
+
|
|
12
|
+
### 修复(适配)
|
|
13
|
+
|
|
14
|
+
- **A1 跨会话关闭/删除不关 tab**:归属从进程级单槽 `cardId` 改为按 `(kind, session)` 记录
|
|
15
|
+
(`owners` Map)。单槽在跨会话时会误判「别的卡拥有它」,让 A 会话的 tab 永远关不掉。
|
|
16
|
+
会话未知(历史条目、幽灵 id)时仍退回单槽,保住 2.1.5 R2 的归属语义。
|
|
17
|
+
- **A2 重载后的幽灵面板没有出口**:无主 body 现在渲染「关闭此页」;**只有本页真的挂载过**
|
|
18
|
+
该 tab 才自动关闭。宿主会持久化栏布局,别的窗口恢复的同一张 tab 不会被本页抢走。
|
|
19
|
+
- **A3 dockRail 铺到别人的页面上**:`CardHost` 根节点声明 `data-dsa-card-id`,
|
|
20
|
+
`findRail` 拒绝不属于本层卡片的 body;全部候选被占时 `data-dsa-dock="occupied"`,浮层保持浮动。
|
|
21
|
+
- **A4 `+` 菜单里的空页胶囊**:better 描述符加 `hidden: true`——保留 `openTab` 可用,只是不再进菜单。
|
|
22
|
+
原生 guide 那一条缺宿主语义材料,仍会显示,已记入 README §5.6「仍未验证」。
|
|
23
|
+
- **A5 禁用态被误判成故障**:`available()` 读 0.28.x 的 `isTabEnabled`,禁用即 `reason='tab-disabled'`,
|
|
24
|
+
`auto` 直接落浮层,不再先挂 600 ms 再报「承载面未证实」。
|
|
25
|
+
- **A6 非当前会话的卡被证明迁走**:宿主对非当前屏的 `openTab` 是排队而不是拒绝,渲染证明
|
|
26
|
+
现在只对当前屏会话武装。
|
|
27
|
+
- **A7 自检看不清真相**:上报增 `railTabs`(宿主清单里本插件行数)与 `mountedTabId`。
|
|
28
|
+
|
|
29
|
+
### 修复(安全 / 归属)
|
|
30
|
+
|
|
31
|
+
- `/config` 改 SMTP 主机/端口/TLS/账号任一项 → 已存授权码作废(凭据不再跟随新目标外送)。
|
|
32
|
+
- `/config` 的 `sideTools: "inherit"` 需要 `confirmInheritTools: true`(防误触;不是安全边界)。
|
|
33
|
+
- 只读护栏 **fail-closed**:注册表答了却认不出只读工具时拒跑(新错误码 `no-readonly-guard`,
|
|
34
|
+
客户端中英文案 + 错误码分类已补),不再静默继承父会话全部工具。
|
|
35
|
+
- `cardId` 不可预测;better tab 标题不再携带问题原文;`tab.meta.cardId` 校验归属会话。
|
|
36
|
+
- `closeRailRow` 容忍带 `dsh-better-sidebar:` 前缀的 kind;`/ask` 的 start 帧回带
|
|
37
|
+
`parentSessionId` / `requestedSessionId`,跨会话作答可被看见。
|
|
38
|
+
|
|
39
|
+
### 新增(行为)
|
|
40
|
+
|
|
41
|
+
- **答完自动展开卡片**:浮层默认是胶囊(一行状态),答案在那里只看得到「已完成」三个字。
|
|
42
|
+
现在回答结束(`done` / `stopped` / `error`)时自动把浮层切到卡片形态并展开该卡,
|
|
43
|
+
答案内容直接出现在侧边卡片里。设置页新增「答完自动展开卡片」开关(默认开);
|
|
44
|
+
置 `autoExpandOnDone: false` 可恢复旧行为(始终停在胶囊,点一下才展开)。
|
|
45
|
+
### 闸门
|
|
46
|
+
|
|
47
|
+
- `node test/verify.mjs` → 58 / 0
|
|
48
|
+
- `node test/contract-test.mjs` → 1499 / 0
|
|
49
|
+
- `node test/float-placement.mjs` → 100 / 0
|
|
50
|
+
- `node test/smoke-test.mjs` → 291 / 0
|
|
51
|
+
- 合计 **1948**(2.1.8 基线 1928,新增 20 条:hidden、禁用门、SMTP 换目标清密码、
|
|
52
|
+
inherit 确认、`no-readonly-guard` 拒跑等)。
|
|
53
|
+
|
|
54
|
+
### 未执行
|
|
55
|
+
|
|
56
|
+
真实浏览器/Electron 点按与视觉、桌面 profile、macOS/Linux 均未验证;详见 README §5.6。
|
|
57
|
+
|
|
3
58
|
## 2.1.8
|
|
4
59
|
|
|
5
60
|
**主题:交付《非 Node 宿主契约》文档,无代码改动。**
|
package/README.md
CHANGED
|
@@ -167,6 +167,7 @@ plugin_manager(action="install_bundle", target="D:\\Users\\34332\\AI\\dsh-sideca
|
|
|
167
167
|
| `sideSurface` | `auto` \| `native-rightbar` \| `better-sidebar` \| `flow` | `auto` | **窗口模式(侧边承载面)**:自动挑选(先 dsh-better-sidebar,再内置浮层卡片)/ 强制 DSH 原生右侧栏(不参与自动)/ 强制 dsh-better-sidebar / 强制内置浮层卡片 | 立即 |
|
|
168
168
|
| `maxChars` | 200–60000 | `4000` | **最大字符数**:超出部分头尾保留、中间截断并标注 | 立即 |
|
|
169
169
|
| `floatMode` | `capsule` \| `full` | `capsule` | **内置浮层的默认形态**:胶囊(一行,最小遮挡)/ 完整卡片栈。运行时可即时收起为胶囊,但不写回本项——刷新后仍回到这里设的默认。旧配置里存过 `off` 的会在启动时校验失败、回退为 `capsule`,并在 `problems` 里给出提示(`index.js` 的 `sanitizeLayer`),无需手工迁移 | 立即 |
|
|
170
|
+
| `autoExpandOnDone` | 布尔 | `true` | **答完自动展开卡片**:回答结束(`done` / `stopped` / `error`)时把浮层从胶囊切到卡片形态并展开该卡,答案正文直接显示在侧边卡片里。置 `false` 则始终停在胶囊,点一下才展开(2.1.9 起) | 立即 |
|
|
170
171
|
| `floatMaxWidth` | 240–560 | `340` | **内置浮层宽度上限**(px):实际宽度 = `min(本项, 对话区右侧可用带宽)`,可用带宽不足 240px 时才会压入对话区 | 立即 |
|
|
171
172
|
| `dockRail` | 布尔 | `true` | **右侧栏打开时占满它**:答案卡铺满右栏标签条下方的内容区,读起来像栏内的一页,而不是 340px 的小浮层。收起为胶囊、点卡片底部「关闭」、右栏宽度 < 280px 或高度 < 240px 时自动回到常规浮层;实现靠测量右栏 DOM 矩形,**没有**走 DSH 的右栏 API(见 §4.2 与 §6.1) | 立即 |
|
|
172
173
|
| `shortcut` | 形如 `Alt+Q`、`Ctrl+Shift+K` | `Alt+Q` | **快捷键**:唤起提问框(`trigger` 允许时) | 立即 |
|
|
@@ -549,6 +550,72 @@ dsh-better-sidebar 为此专门带了一条防御(`html[data-platform] body>[d
|
|
|
549
550
|
|
|
550
551
|
---
|
|
551
552
|
|
|
553
|
+
### 5.6 2026-11-03 复核(v2.1.9 / dsh-better-sidebar 0.28.2)
|
|
554
|
+
|
|
555
|
+
本轮把适配核对的**对象**从上表的 0.22.1(网页)/ 0.24.1(桌面)更新到本机实际运行的
|
|
556
|
+
**0.28.2**(`SIDEBAR_SERVICE_VERSION = "0.28.2"`,`SIDEBAR_FEATURES` 14 项),并修掉四处
|
|
557
|
+
按旧模型写的假设。证据分三类:**源码级**(读 0.28.2 的 `lib/types/client/service.d.ts`
|
|
558
|
+
与 `src/client/*.ts`)、**替身复现**(`test/` 下的 0.28.2 形状替身)、**真机**(本轮**未做**,见末尾)。
|
|
559
|
+
|
|
560
|
+
#### 0.28.2 的关键事实(相对 0.22.1 的漂移)
|
|
561
|
+
|
|
562
|
+
| # | 事实 | 对适配的影响 |
|
|
563
|
+
|---|---|---|
|
|
564
|
+
| 1 | 组件类 seed 的 `openTab` 实际落到 **DSH 原生右栏**(`service.ts:1120-1145`),不再进 better 自己的 workbench | better 与 native **共用同一个物理容器**;`dockRail` 的锚点会指到别人的页面 |
|
|
565
|
+
| 2 | tab id 由宿主 mint(`tab1…`),seed id 被忽略,`closeTab(seed.id)` 是 no-op | 关闭必须用 `CardHost` 记录的真实 id(2.1.6 S1 已做) |
|
|
566
|
+
| 3 | 同 kind 每会话只有**一个 page 地址**(无 `multiple`) | 同会话再开一张 = 导航,旧卡必须迁回浮层(2.1.6 S2 已做) |
|
|
567
|
+
| 4 | `isTabEnabled(type)` 禁用后 `openTab` **warn + return**,不抛错 | 禁用态会被误判成「承载面未证实」并弹假故障(2.1.9 A5 修) |
|
|
568
|
+
| 5 | 每个注册类型自动进 `+` 菜单,并生成 guide 胶囊 | 点开是一张无 `cardId` 的空页(2.1.9 A4 用 `hidden: true` 修) |
|
|
569
|
+
| 6 | `OpenTabSeed.target` 支持 `right/bottom/side`,另有 `preferNewPane` 与 split pane | 本插件不传 target(默认 right);分栏时 panel 级回退会横跨两栏 |
|
|
570
|
+
| 7 | 非当前会话的 `openTab` 是**排队**,不是拒绝;只有 on-screen 才 mount | 600 ms 渲染证明会把排队中的卡误判为失败(2.1.9 A6 修) |
|
|
571
|
+
| 8 | 镜像注册的 `id` 是 `dsh-better-sidebar:<id>`、`kind` 是描述符 id | 两个适配器同 kind 相撞的旧缺陷**已清除**;但 `closeRailRow` 需容忍带前缀的 kind |
|
|
572
|
+
|
|
573
|
+
> `.d.ts` 里 `openTab` 的注释写「每次 open 落在 bottom workbench」,而 `OpenTabSeed.target`
|
|
574
|
+
> 的注释写默认 `'right'` 是原生右栏——两者矛盾,**只有 `src/client/service.ts` 给出真值**。
|
|
575
|
+
> 下一位维护者不要只看 `.d.ts`。
|
|
576
|
+
|
|
577
|
+
#### 本版修掉的适配缺陷
|
|
578
|
+
|
|
579
|
+
| 编号 | 缺陷 | 修法 |
|
|
580
|
+
|---|---|---|
|
|
581
|
+
| A1 | 跨会话关闭/删除不关 better tab(进程级单槽 `cardId` 误判归属) | 归属改为按 `(kind, session)` 记(`owners` Map);「本会话有别的卡持有才拒绝」,会话未知时退回单槽 |
|
|
582
|
+
| A2 | 刷新后宿主还原的 better tab 无人认领、没有出口 | 空页渲染「关闭此页」;**只有本页真的挂载过**该 tab 才自动关(别的窗口的 tab 不抢) |
|
|
583
|
+
| A3 | `dockRail` 把浮层卡铺到另一张卡自己的页面上 | `CardHost` 根节点声明 `data-dsa-card-id`;`findRail` 拒绝不属于本层的 body,全被拒时 `data-dsa-dock="occupied"` |
|
|
584
|
+
| A4 | `+` 菜单两条「点开即空页」胶囊 | descriptor 加 `hidden: true`(保留可 open,只是不进菜单);原生 guide 那条缺宿主语义材料,仍会显示 |
|
|
585
|
+
| A5 | 禁用的 tab 类型仍被 `auto` 选中并报假故障 | `available()` 读 `isTabEnabled`,禁用即 `reason='tab-disabled'`,直接落浮层 |
|
|
586
|
+
| A6 | 非当前会话的卡被 600 ms 证明迁走 | 证明只对「当前屏会话」武装 |
|
|
587
|
+
| A7 | 自检看不出「到底有没有落栏」 | 上报增 `railTabs`(宿主清单里本插件行数)与 `mountedTabId`(最近一次真实挂载的宿主 tab id) |
|
|
588
|
+
|
|
589
|
+
#### 安全相关(同一轮,详见 §十二)
|
|
590
|
+
|
|
591
|
+
- `/config` 改 `feedbackSmtpHost/Port/Secure/User` 任一项 → **已存授权码作废**。否则「空密码 = 保留旧值」
|
|
592
|
+
等于把凭据交给新主机,而 `AUTH PLAIN` 是明文。
|
|
593
|
+
- `/config` 写 `sideTools: "inherit"` 需要 `confirmInheritTools: true`。**这不是安全边界**
|
|
594
|
+
(同源脚本可以伪造这个字段),只让这次提权必须是有意的;同源信任边界由宿主权限决定。
|
|
595
|
+
- 只读护栏改为 **fail-closed**:工具注册表答了却认不出任何只读工具时,除非另有 `agentPreset`
|
|
596
|
+
收窄,否则**拒跑**(新错误码 `no-readonly-guard`,卡片文案中英齐全)。
|
|
597
|
+
- `cardId` 改为不可预测(`card-<base36 时间>-<序号>-<随机 6 位>`);better tab 标题不再携带问题原文
|
|
598
|
+
(0.28.x 会把 title/meta 随布局持久化);`tab.meta.cardId` 增加「属于本会话」校验。
|
|
599
|
+
|
|
600
|
+
#### 0.28.2 新 API 的准入条件(采用前必须满足)
|
|
601
|
+
|
|
602
|
+
- `updateTab(tabId, { title, path, meta }, sessionId)`:① 只允许 `embeddedTabIds` 里的自持 tabId;
|
|
603
|
+
② `sessionId === card.sessionId`;③ **永不带 `path`**(那等于给出「在后台会话打开任意文件路径」的原语)。
|
|
604
|
+
- `stateSubscription` / `getSnapshot`:只用来读,**禁止**用快照的 `sessionId` 重定向在飞请求。
|
|
605
|
+
- `pluginSettings`:只放 UI 偏好,绝不放卡片正文或选中文本。
|
|
606
|
+
|
|
607
|
+
#### 仍未验证(如实记录)
|
|
608
|
+
|
|
609
|
+
- 真实浏览器 / Electron 页面里的**点按与视觉**:本机无 GUI 自动化。`dockRail` 的 `occupied`、
|
|
610
|
+
`hidden` 之后的 `+` 菜单、跨窗口清扫都**没有被真机执行**。
|
|
611
|
+
- 桌面端:desktop profile 同样是 `dsh-better-sidebar ^0.28.2`,§5.3 的 0.24.1 核对**已过期**;
|
|
612
|
+
本版按同一套源码事实处理,但没跑桌面。
|
|
613
|
+
- macOS / Linux:零用例,仓库没有 CI。
|
|
614
|
+
- `openTabs` 是否跨窗口共享(`app.asar` 内的 `sidebar-right` 读不到):因此「自动清扫」只采用
|
|
615
|
+
「本页挂载过」这一按页成立的判据,没有做 boot 期盲扫。
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
552
619
|
## 六、未知 DSH API:占位接口与替换方式
|
|
553
620
|
|
|
554
621
|
开发时以**运行时能力探测**为主,任何"本机没验证到"的接口都在代码里显式留了占位与替换点。它们集中在三处:
|
package/client.js
CHANGED
|
@@ -117,7 +117,7 @@ window.__ModuleLoader__.load({
|
|
|
117
117
|
* the Host and the module's `api.version` both read it, so a report can
|
|
118
118
|
* never claim a generation the browser is not actually running.
|
|
119
119
|
*/
|
|
120
|
-
const CLIENT_VERSION = '2.1.
|
|
120
|
+
const CLIENT_VERSION = '2.1.9'
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* Tab kind served by the DSH native right rail (also its implementation id).
|
|
@@ -206,6 +206,10 @@ window.__ModuleLoader__.load({
|
|
|
206
206
|
// tool-narrowing channel 0.2.1 honors (see the settings row help text).
|
|
207
207
|
sideAgentPreset: '',
|
|
208
208
|
floatMode: 'capsule',
|
|
209
|
+
// 2.1.9:答完自动把浮层从胶囊展开成卡片。胶囊只渲染一行状态,答案要
|
|
210
|
+
// 点一下 › 才看得到;用户明确要求「提问完成之后直接在侧边卡片显示回答内容」。
|
|
211
|
+
// 置 false 可恢复旧行为(始终停在胶囊)。
|
|
212
|
+
autoExpandOnDone: true,
|
|
209
213
|
floatMaxWidth: 340,
|
|
210
214
|
// Docked presentation (1.8.3): when the right rail is on screen, the
|
|
211
215
|
// answer paints inside the rail's rectangle instead of floating as a
|
|
@@ -463,6 +467,12 @@ window.__ModuleLoader__.load({
|
|
|
463
467
|
// where a page goes. It also works on builds whose rail markers we have
|
|
464
468
|
// never seen — including the leftover "card closed" tab, which still
|
|
465
469
|
// occupies a page body.
|
|
470
|
+
//
|
|
471
|
+
// 0.28.x moved the side-card plugin's tabs INTO this same rail, so this
|
|
472
|
+
// marker now also matches a DIFFERENT card's page. `findRail` refuses a
|
|
473
|
+
// body that declares another card id (`data-dsa-card-id`) instead of
|
|
474
|
+
// painting our card on top of it; unmarked bodies (older hosts, leftover
|
|
475
|
+
// closed-card faces) stay eligible.
|
|
466
476
|
'[data-dsa-rail-host]',
|
|
467
477
|
'[data-sidebar-right-panel][data-sidebar-right-open] [data-dockkit-pane]',
|
|
468
478
|
'[data-sidebar-right-panel][data-sidebar-right-open]',
|
|
@@ -539,17 +549,31 @@ window.__ModuleLoader__.load({
|
|
|
539
549
|
*
|
|
540
550
|
* @returns {{node: Element, box: {left: number, top: number, width: number, height: number}}|null}
|
|
541
551
|
*/
|
|
542
|
-
function findRail(scope) {
|
|
552
|
+
function findRail(scope, ownCardIds) {
|
|
543
553
|
if (scope === null || scope === undefined || typeof scope.querySelector !== 'function') return null
|
|
554
|
+
const owns = ownCardIds instanceof Set ? ownCardIds : null
|
|
555
|
+
let occupied = false
|
|
544
556
|
for (const selector of RAIL_SELECTORS) {
|
|
545
557
|
let node = null
|
|
546
558
|
try { node = scope.querySelector(selector) } catch { node = null }
|
|
547
559
|
if (node === null || typeof node.getBoundingClientRect !== 'function') continue
|
|
560
|
+
// A body that names a card this layer does not own belongs to another
|
|
561
|
+
// side card (or another session's page). Docking into it would cover
|
|
562
|
+
// that card, so it is skipped and remembered as "occupied" — the layer
|
|
563
|
+
// then stays floating and says why through `data-dsa-dock`.
|
|
564
|
+
let cardId = null
|
|
565
|
+
try {
|
|
566
|
+
cardId = typeof node.getAttribute === 'function' ? node.getAttribute('data-dsa-card-id') : null
|
|
567
|
+
} catch { cardId = null }
|
|
568
|
+
if (typeof cardId === 'string' && cardId !== '' && (owns === null || !owns.has(cardId))) {
|
|
569
|
+
occupied = true
|
|
570
|
+
continue
|
|
571
|
+
}
|
|
548
572
|
let box = null
|
|
549
573
|
try { box = dockedBodyRect(node) } catch { box = null }
|
|
550
|
-
if (railOnScreen(box)) return { node, box }
|
|
574
|
+
if (railOnScreen(box)) return { node, box, occupied }
|
|
551
575
|
}
|
|
552
|
-
return null
|
|
576
|
+
return occupied ? { node: null, box: null, occupied: true } : null
|
|
553
577
|
}
|
|
554
578
|
|
|
555
579
|
/**
|
|
@@ -627,6 +651,7 @@ window.__ModuleLoader__.load({
|
|
|
627
651
|
answerNonStreaming: '当前 DSH 版本未提供流式帧,答案一次性返回',
|
|
628
652
|
answerEmpty: '(没有内容)',
|
|
629
653
|
cardClosed: '该卡片已关闭',
|
|
654
|
+
closeThisTab: '关闭此页',
|
|
630
655
|
cardMovedToFlow: '该卡片已移至浮层显示',
|
|
631
656
|
cardDelete: '删除这条追问对话',
|
|
632
657
|
cardDeleteTitle: '删除这条追问对话?',
|
|
@@ -660,6 +685,7 @@ window.__ModuleLoader__.load({
|
|
|
660
685
|
errorBusy: '并发追问已达上限({n}),请稍后再试',
|
|
661
686
|
errorNoSideEngineBody: '这台 DSH 组合没有可用的子代理 provider',
|
|
662
687
|
errorNoParentBody: '当前没有活动的会话代理,无法发起独立作答',
|
|
688
|
+
errorNoReadonlyGuardBody: '这台宿主的工具注册表里认不出只读工具,为保证侧边作答不写工作区,本次已拒绝',
|
|
663
689
|
errorAborted: '作答被取消或超时',
|
|
664
690
|
errorUnreachableBody: '宿主接口不可达',
|
|
665
691
|
errorUnauthorizedBody: '需要先通过 DSH 登录(宿主会话未认证)',
|
|
@@ -689,6 +715,8 @@ window.__ModuleLoader__.load({
|
|
|
689
715
|
setFloatModeCapsule: '胶囊(不遮挡对话,可随时展开)',
|
|
690
716
|
setFloatModeFull: '直接展开成卡片',
|
|
691
717
|
setFloatModeHint: '浮层贴右侧边缘,只在空间不足时才可能压到对话区',
|
|
718
|
+
setAutoExpandOnDone: '答完自动展开卡片',
|
|
719
|
+
setAutoExpandOnDoneHint: '开启后,回答一结束就展开成卡片显示正文;关闭后保持一行胶囊,点一下才展开',
|
|
692
720
|
setFloatMaxWidth: '浮层宽度上限(px)',
|
|
693
721
|
setFloatMaxWidthHint: '空间不足时自动收窄,不会硬顶',
|
|
694
722
|
setDockRail: '右侧栏打开时占满它',
|
|
@@ -902,6 +930,7 @@ window.__ModuleLoader__.load({
|
|
|
902
930
|
answerNonStreaming: 'This DSH build publishes no stream frames; the answer arrived at once',
|
|
903
931
|
answerEmpty: '(empty)',
|
|
904
932
|
cardClosed: 'This card was closed',
|
|
933
|
+
closeThisTab: 'Close this tab',
|
|
905
934
|
cardMovedToFlow: 'This card moved to the floating stack',
|
|
906
935
|
cardDelete: 'Delete this follow-up conversation',
|
|
907
936
|
cardDeleteTitle: 'Delete this follow-up conversation?',
|
|
@@ -935,6 +964,7 @@ window.__ModuleLoader__.load({
|
|
|
935
964
|
errorBusy: 'Too many concurrent answers (limit {n}); try again shortly',
|
|
936
965
|
errorNoSideEngineBody: 'This DSH composition has no usable subagent provider',
|
|
937
966
|
errorNoParentBody: 'No active session agent to answer independently',
|
|
967
|
+
errorNoReadonlyGuardBody: 'This host\'s tool registry has no recognisable read-only tools, so the side answer was refused to keep the workspace untouched',
|
|
938
968
|
errorAborted: 'The answer was cancelled or timed out',
|
|
939
969
|
errorUnreachableBody: 'The host API is unreachable',
|
|
940
970
|
errorUnauthorizedBody: 'Sign in to DSH first (the host session is not authenticated)',
|
|
@@ -964,6 +994,8 @@ window.__ModuleLoader__.load({
|
|
|
964
994
|
setFloatModeCapsule: 'Capsule (never covers the conversation)',
|
|
965
995
|
setFloatModeFull: 'Expanded card stack',
|
|
966
996
|
setFloatModeHint: 'The layer is anchored to the right edge and only overlaps on very narrow frames',
|
|
997
|
+
setAutoExpandOnDone: 'Expand the card when the answer finishes',
|
|
998
|
+
setAutoExpandOnDoneHint: 'On: the answer appears in the card the moment it settles. Off: the one-line capsule stays until you click it',
|
|
967
999
|
setFloatMaxWidth: 'Floating layer max width (px)',
|
|
968
1000
|
setFloatMaxWidthHint: 'Narrows automatically when there is not enough room',
|
|
969
1001
|
setDockRail: 'Fill the right rail while it is open',
|
|
@@ -1215,6 +1247,9 @@ window.__ModuleLoader__.load({
|
|
|
1215
1247
|
busy: { key: 'errorBusy', vars: () => ({ n: store?.state?.config?.maxConcurrentAsks ?? 0 }) },
|
|
1216
1248
|
'no-side-engine': { key: 'errorNoSideEngineBody' },
|
|
1217
1249
|
'no-parent': { key: 'errorNoParentBody' },
|
|
1250
|
+
// 2.1.9: the read-only guard now fails closed. The code must be named
|
|
1251
|
+
// Client-side, or an English UI would show the Host's Chinese line.
|
|
1252
|
+
'no-readonly-guard': { key: 'errorNoReadonlyGuardBody' },
|
|
1218
1253
|
aborted: { key: 'errorAborted' },
|
|
1219
1254
|
unreachable: { key: 'errorUnreachableBody' },
|
|
1220
1255
|
// The Host now gates these routes on the DSH browser session, so both are
|
|
@@ -1606,6 +1641,23 @@ window.__ModuleLoader__.load({
|
|
|
1606
1641
|
store.set({ floatMode: mode })
|
|
1607
1642
|
}
|
|
1608
1643
|
|
|
1644
|
+
/**
|
|
1645
|
+
* Bring a finished card on screen.
|
|
1646
|
+
*
|
|
1647
|
+
* The floating layer defaults to the `capsule` pill (one line of status), so
|
|
1648
|
+
* an answer that finishes while collapsed never shows its text. Only a card
|
|
1649
|
+
* carried by the flow layer needs this: a card in the right rail already has
|
|
1650
|
+
* its own page, and an already-expanded card is left alone.
|
|
1651
|
+
* @param {string} id - the card that just finished.
|
|
1652
|
+
*/
|
|
1653
|
+
function revealFinishedCard(id) {
|
|
1654
|
+
if (store.state.config.autoExpandOnDone === false) return
|
|
1655
|
+
const card = store.state.cards.find(item => item.id === id)
|
|
1656
|
+
if (card === undefined || card.surface !== 'flow') return
|
|
1657
|
+
setFloatMode('full')
|
|
1658
|
+
patchCard(id, { open: true })
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1609
1661
|
/** Replace one card by id (immutably; a no-op when it is gone). */
|
|
1610
1662
|
function patchCard(id, patch) {
|
|
1611
1663
|
const cards = store.state.cards.map(card => (card.id === id ? { ...card, ...patch } : card))
|
|
@@ -2332,21 +2384,34 @@ window.__ModuleLoader__.load({
|
|
|
2332
2384
|
* this id instead of guessing at a `tab.meta` that no longer exists.
|
|
2333
2385
|
*/
|
|
2334
2386
|
cardId: null,
|
|
2387
|
+
/** Per-session tab ownership; see `ownerKeyOf`. */
|
|
2388
|
+
owners: new Map(),
|
|
2335
2389
|
open(card) {
|
|
2336
2390
|
if (!this.available() || typeof this.controller.openTab !== 'function') return false
|
|
2337
2391
|
// 同一个 kind 在原生栏里只有一个地址(见 cardId 的注释):先把栏里
|
|
2338
2392
|
// 其它卡片迁回浮层,否则它们既不在栏里也不在浮层上,流式输出还会
|
|
2339
2393
|
// 继续往一张看不见的卡片里写。
|
|
2394
|
+
const openSession = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
2395
|
+
? card.sessionId
|
|
2396
|
+
: store.state.sessionId
|
|
2397
|
+
const openKey = ownerKeyOf(openSession)
|
|
2340
2398
|
for (const other of store.state.cards) {
|
|
2341
|
-
if (other.id
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2399
|
+
if (other.id === card.id || other.surface !== 'native-rightbar') continue
|
|
2400
|
+
// Only the SAME session's other card loses its address: every
|
|
2401
|
+
// session keeps its own tab, so migrating across sessions would
|
|
2402
|
+
// yank a card out of a rail the user is still looking at.
|
|
2403
|
+
const otherSession = typeof other.sessionId === 'string' && other.sessionId !== ''
|
|
2404
|
+
? other.sessionId
|
|
2405
|
+
: store.state.sessionId
|
|
2406
|
+
if (ownerKeyOf(otherSession) !== openKey) continue
|
|
2407
|
+
// `open: true` is load-bearing: the floating layer only mounts a
|
|
2408
|
+
// card whose `surface === 'flow'` AND `open` is true, so migrating
|
|
2409
|
+
// a card back to the flow while leaving it folded hid it on BOTH
|
|
2410
|
+
// surfaces — not in the rail, not on screen, still streaming.
|
|
2411
|
+
patchCard(other.id, { surface: 'flow', open: true })
|
|
2348
2412
|
}
|
|
2349
2413
|
this.cardId = card.id
|
|
2414
|
+
this.owners.set(openKey, card.id)
|
|
2350
2415
|
try {
|
|
2351
2416
|
this.controller.openTab(CARD_KIND, { params: { cardId: card.id }, revealIfOpened: true })
|
|
2352
2417
|
} catch (error) {
|
|
@@ -2372,12 +2437,20 @@ window.__ModuleLoader__.load({
|
|
|
2372
2437
|
// 却把另一种真实场景关在门外:页面重载后宿主会从布局里恢复出这张
|
|
2373
2438
|
// tab,而 `cardId` 是内存态、必然是 null —— 此时没有卡片认领它,
|
|
2374
2439
|
// 删除历史条目也永远关不掉,就是专家复现的"幽灵面板"。
|
|
2375
|
-
//
|
|
2376
|
-
|
|
2377
|
-
//
|
|
2440
|
+
// 规则改为:**本会话里有别的卡拥有它才拒绝**;本会话无主时按
|
|
2441
|
+
// kind + 会话定位(重载后的幽灵面板由此能关掉)。
|
|
2442
|
+
//
|
|
2443
|
+
// 2.1.9:归属按会话记。进程级单槽在跨会话时会误判「别的卡拥有它」,
|
|
2444
|
+
// 让 A 会话的 tab 永远关不掉(专家复现的跨会话残留)。
|
|
2378
2445
|
const ownerSession = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
2379
2446
|
? card.sessionId
|
|
2380
2447
|
: store.state.sessionId
|
|
2448
|
+
const knownSession = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
2449
|
+
const holderId = this.owners.get(ownerKeyOf(ownerSession))
|
|
2450
|
+
if (holderId !== undefined && holderId !== card.id) return false
|
|
2451
|
+
// 会话未知(历史条目/幽灵 id)时,退回进程级单槽:没有任何会话认领
|
|
2452
|
+
// 它,而栏里那张属于别的卡,就不能替它关。
|
|
2453
|
+
if (holderId === undefined && !knownSession && this.cardId !== null && this.cardId !== card.id) return false
|
|
2381
2454
|
let rows = []
|
|
2382
2455
|
try {
|
|
2383
2456
|
rows = typeof controller.openTabs?.getSnapshot === 'function'
|
|
@@ -2403,6 +2476,7 @@ window.__ModuleLoader__.load({
|
|
|
2403
2476
|
return false
|
|
2404
2477
|
}
|
|
2405
2478
|
if (this.cardId === card.id) this.cardId = null
|
|
2479
|
+
if (this.owners.get(ownerKeyOf(ownerSession)) === card.id) this.owners.delete(ownerKeyOf(ownerSession))
|
|
2406
2480
|
return true
|
|
2407
2481
|
} catch (error) {
|
|
2408
2482
|
console.warn(`[${PLUGIN_ID}] the right rail refused the close:`, error)
|
|
@@ -2425,6 +2499,8 @@ window.__ModuleLoader__.load({
|
|
|
2425
2499
|
* that is not this owner must not close someone else's tab.
|
|
2426
2500
|
*/
|
|
2427
2501
|
cardId: null,
|
|
2502
|
+
/** Per-session tab ownership; see `ownerKeyOf`. */
|
|
2503
|
+
owners: new Map(),
|
|
2428
2504
|
/**
|
|
2429
2505
|
* Why this adapter is (not) usable, in the settings page's own words.
|
|
2430
2506
|
* `null` means "usable"; a string explains the refusal.
|
|
@@ -2445,7 +2521,21 @@ window.__ModuleLoader__.load({
|
|
|
2445
2521
|
*/
|
|
2446
2522
|
available() {
|
|
2447
2523
|
if (this.service === null || this.error !== null) return false
|
|
2448
|
-
|
|
2524
|
+
if (!(Array.isArray(this.features) && this.features.includes('tabMeta'))) return false
|
|
2525
|
+
// 0.28.x: every registered type has its own on/off switch in the
|
|
2526
|
+
// side-card plugin's settings, and `openTab` on a disabled type
|
|
2527
|
+
// warns and returns instead of throwing. Treat it as "not usable"
|
|
2528
|
+
// here so `auto` goes straight to the floating card instead of
|
|
2529
|
+
// spending 600 ms proving a surface that deliberately refused.
|
|
2530
|
+
try {
|
|
2531
|
+
if (typeof this.service.isTabEnabled === 'function'
|
|
2532
|
+
&& this.service.isTabEnabled(CARD_KIND_BETTER) === false) {
|
|
2533
|
+
this.reason = 'tab-disabled'
|
|
2534
|
+
return false
|
|
2535
|
+
}
|
|
2536
|
+
} catch { /* the probe is optional */ }
|
|
2537
|
+
if (this.reason === 'tab-disabled') this.reason = null
|
|
2538
|
+
return true
|
|
2449
2539
|
},
|
|
2450
2540
|
open(card) {
|
|
2451
2541
|
if (!this.available() || typeof this.service.openTab !== 'function') return false
|
|
@@ -2468,21 +2558,25 @@ window.__ModuleLoader__.load({
|
|
|
2468
2558
|
// `dedupeKey` on our descriptor is what collapses repeat opens onto
|
|
2469
2559
|
// the same tab; both are part of the stable consumer contract.
|
|
2470
2560
|
this.service.openTab(
|
|
2471
|
-
{ type: CARD_KIND_BETTER, id: tabId, title:
|
|
2561
|
+
{ type: CARD_KIND_BETTER, id: tabId, title: t('answerTitle'), meta: { cardId: card.id } },
|
|
2472
2562
|
typeof targetSession === 'string' && targetSession !== '' ? { sessionId: targetSession } : undefined,
|
|
2473
2563
|
)
|
|
2474
2564
|
this.cardId = card.id
|
|
2565
|
+
this.owners.set(ownerKeyOf(targetSession), card.id)
|
|
2475
2566
|
return true
|
|
2476
2567
|
},
|
|
2477
2568
|
close(card) {
|
|
2478
2569
|
if (!this.available() || typeof this.service.closeTab !== 'function') return false
|
|
2479
|
-
// 单一 page
|
|
2480
|
-
//
|
|
2481
|
-
//
|
|
2482
|
-
if (this.cardId !== null && this.cardId !== card.id) return false
|
|
2570
|
+
// 单一 page 地址(每个会话一个):只有本会话里最后 open 的那张卡
|
|
2571
|
+
// 拥有栏里的 tab。同 native.close:本会话无主(页面重载后)也要
|
|
2572
|
+
// 放行,否则在历史管理器里删掉一条 better 卡,栏里那张 tab 永远留空。
|
|
2483
2573
|
const sessionId = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
2484
2574
|
? card.sessionId
|
|
2485
2575
|
: (store.state.sessionId ?? undefined)
|
|
2576
|
+
const knownSession = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
2577
|
+
const holderId = this.owners.get(ownerKeyOf(sessionId))
|
|
2578
|
+
if (holderId !== undefined && holderId !== card.id) return false
|
|
2579
|
+
if (holderId === undefined && !knownSession && this.cardId !== null && this.cardId !== card.id) return false
|
|
2486
2580
|
const scope = typeof sessionId === 'string' && sessionId !== '' ? { sessionId } : undefined
|
|
2487
2581
|
// 0.28.x 的原生 tab id 由宿主 mint(tab1…),不是我们传入的 seed id;
|
|
2488
2582
|
// CardHost 挂载时把真实 id 记进 embeddedTabIds。
|
|
@@ -2490,17 +2584,20 @@ window.__ModuleLoader__.load({
|
|
|
2490
2584
|
if (typeof mountedId === 'string' && mountedId !== '') {
|
|
2491
2585
|
this.service.closeTab(mountedId, scope)
|
|
2492
2586
|
this.cardId = null
|
|
2587
|
+
if (this.owners.get(ownerKeyOf(sessionId)) === card.id) this.owners.delete(ownerKeyOf(sessionId))
|
|
2493
2588
|
return true
|
|
2494
2589
|
}
|
|
2495
2590
|
// 卡片还没挂载过(或已从 store 移除):按 kind + 会话在栏的清单里
|
|
2496
2591
|
// 找到唯一那张 tab,经原生控制器关闭。
|
|
2497
2592
|
if (closeRailRow(CARD_KIND_BETTER, sessionId)) {
|
|
2498
2593
|
this.cardId = null
|
|
2594
|
+
if (this.owners.get(ownerKeyOf(sessionId)) === card.id) this.owners.delete(ownerKeyOf(sessionId))
|
|
2499
2595
|
return true
|
|
2500
2596
|
}
|
|
2501
2597
|
// 0.19 之前的 bottom workbench:seed id 就是 tab id。
|
|
2502
2598
|
this.service.closeTab(`${CARD_KIND_BETTER}:${card.id}`, scope)
|
|
2503
2599
|
this.cardId = null
|
|
2600
|
+
if (this.owners.get(ownerKeyOf(sessionId)) === card.id) this.owners.delete(ownerKeyOf(sessionId))
|
|
2504
2601
|
return true
|
|
2505
2602
|
},
|
|
2506
2603
|
},
|
|
@@ -2549,6 +2646,29 @@ window.__ModuleLoader__.load({
|
|
|
2549
2646
|
* @param {object} state - `store.state`.
|
|
2550
2647
|
* @returns {object} the report body (allow-listed again on the Host).
|
|
2551
2648
|
*/
|
|
2649
|
+
/**
|
|
2650
|
+
* How many rail rows currently belong to this plugin.
|
|
2651
|
+
*
|
|
2652
|
+
* 0.28.x's `available` only says the descriptor registered — it cannot say
|
|
2653
|
+
* the tab ever sat in the rail, because a disabled type or a missing guide
|
|
2654
|
+
* entry makes `openTab` return silently. Counting the host's own inventory
|
|
2655
|
+
* is the only first-hand signal, and it is what a bug report needs.
|
|
2656
|
+
* @returns {number}
|
|
2657
|
+
*/
|
|
2658
|
+
function clientRailTabCount() {
|
|
2659
|
+
const controller = surfaces.native === null ? null : surfaces.native.controller
|
|
2660
|
+
try {
|
|
2661
|
+
const rows = typeof controller?.openTabs?.getSnapshot === 'function'
|
|
2662
|
+
? controller.openTabs.getSnapshot()
|
|
2663
|
+
: []
|
|
2664
|
+
if (!Array.isArray(rows)) return 0
|
|
2665
|
+
return rows.filter(row => typeof row?.kind === 'string'
|
|
2666
|
+
&& (row.kind === CARD_KIND
|
|
2667
|
+
|| row.kind === CARD_KIND_BETTER
|
|
2668
|
+
|| row.kind === `dsh-better-sidebar:${CARD_KIND_BETTER}`)).length
|
|
2669
|
+
} catch { return 0 }
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2552
2672
|
function buildClientReport(state) {
|
|
2553
2673
|
return {
|
|
2554
2674
|
// Read from the module's own version constant so the report can never
|
|
@@ -2567,6 +2687,10 @@ window.__ModuleLoader__.load({
|
|
|
2567
2687
|
...(surfaces.better.version === null ? {} : { version: surfaces.better.version }),
|
|
2568
2688
|
...(surfaces.better.reason === null ? {} : { reason: String(surfaces.better.reason) }),
|
|
2569
2689
|
features: surfaces.better.features,
|
|
2690
|
+
// First-hand proof that a page really exists in the rail, plus the
|
|
2691
|
+
// last host tab id a body actually mounted with.
|
|
2692
|
+
railTabs: clientRailTabCount(),
|
|
2693
|
+
mountedTabId: lastMountedHostTabId,
|
|
2570
2694
|
},
|
|
2571
2695
|
// §4.5 family singleton verdict (host-side diagnostics only).
|
|
2572
2696
|
family: {
|
|
@@ -2586,6 +2710,9 @@ window.__ModuleLoader__.load({
|
|
|
2586
2710
|
*/
|
|
2587
2711
|
let renderedCardId = null
|
|
2588
2712
|
|
|
2713
|
+
/** The host tab id the most recent embedded body mounted with. */
|
|
2714
|
+
let lastMountedHostTabId = null
|
|
2715
|
+
|
|
2589
2716
|
/**
|
|
2590
2717
|
* Host tab ids for cards mounted by an external embedder, keyed by card id.
|
|
2591
2718
|
*
|
|
@@ -2595,8 +2722,29 @@ window.__ModuleLoader__.load({
|
|
|
2595
2722
|
* open lands in DSH's native rail, so `closeTab(seed.id)` is a strict no-op.
|
|
2596
2723
|
* The body records the real id here and `surfaces.better.close` uses it.
|
|
2597
2724
|
*/
|
|
2725
|
+
/**
|
|
2726
|
+
* The key an adapter records tab ownership under.
|
|
2727
|
+
*
|
|
2728
|
+
* Ownership is per SESSION, never process-global: each session has its own
|
|
2729
|
+
* page address for a kind, so one slot could only ever describe the last
|
|
2730
|
+
* opened card and would deny every other session its own close.
|
|
2731
|
+
* @param {string|null|undefined} sessionId
|
|
2732
|
+
* @returns {string} '' for "unknown session"
|
|
2733
|
+
*/
|
|
2734
|
+
const ownerKeyOf = sessionId => (typeof sessionId === 'string' && sessionId !== '' ? sessionId : '')
|
|
2735
|
+
|
|
2598
2736
|
const embeddedTabIds = new Map()
|
|
2599
2737
|
|
|
2738
|
+
/**
|
|
2739
|
+
* Card ids this page has actually rendered a body for.
|
|
2740
|
+
*
|
|
2741
|
+
* Mount is the only thing that proves a tab belongs to THIS page: the host
|
|
2742
|
+
* persists the rail layout, so another window can restore the same tab
|
|
2743
|
+
* record while its card lives there. A restored-but-unmounted tab therefore
|
|
2744
|
+
* gets a close button, never an automatic close.
|
|
2745
|
+
*/
|
|
2746
|
+
const everMountedCards = new Set()
|
|
2747
|
+
|
|
2600
2748
|
/**
|
|
2601
2749
|
* Pick the surface for a new card.
|
|
2602
2750
|
* @param {string} wanted - `auto` | adapter kind.
|
|
@@ -4911,8 +5059,10 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
4911
5059
|
} catch { return null }
|
|
4912
5060
|
}
|
|
4913
5061
|
const laneRect = measure(lane)
|
|
4914
|
-
const
|
|
4915
|
-
const
|
|
5062
|
+
const ownCards = new Set(store.state.cards.filter(item => item.surface === 'flow').map(item => item.id))
|
|
5063
|
+
const rail = findRail(document, ownCards)
|
|
5064
|
+
const railBox = rail === null || rail.box === null ? null : rail.box
|
|
5065
|
+
const railOccupied = rail !== null && rail.occupied === true && railBox === null
|
|
4916
5066
|
const laneRight = laneRect === null ? rootWidth : laneRect.right
|
|
4917
5067
|
const railLeft = railBox === null ? null : railBox.left
|
|
4918
5068
|
// 2.1.3: the composer occupies the bottom of the conversation column,
|
|
@@ -4965,7 +5115,9 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
4965
5115
|
? 'dock'
|
|
4966
5116
|
: (mode !== 'full'
|
|
4967
5117
|
? 'mode'
|
|
4968
|
-
: (dockWanted
|
|
5118
|
+
: (dockWanted
|
|
5119
|
+
? (railBox !== null ? 'rail-too-small' : (railOccupied ? 'occupied' : 'no-rail'))
|
|
5120
|
+
: 'off'))
|
|
4969
5121
|
// The rail can appear AFTER this effect ran (opening it mounts new
|
|
4970
5122
|
// nodes), and it can be re-created later: keep the observer pointed at
|
|
4971
5123
|
// whichever node is currently accepted.
|
|
@@ -5722,6 +5874,14 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
5722
5874
|
: notifyPerm === 'denied'
|
|
5723
5875
|
? t('notifyDenied')
|
|
5724
5876
|
: t('setNotifyOnDoneHint')),
|
|
5877
|
+
h('label', { className: 'dsa-check' },
|
|
5878
|
+
h('input', {
|
|
5879
|
+
type: 'checkbox',
|
|
5880
|
+
checked: draft.autoExpandOnDone !== false,
|
|
5881
|
+
onChange: event => setDraft({ ...draft, autoExpandOnDone: event.target.checked }),
|
|
5882
|
+
}),
|
|
5883
|
+
t('setAutoExpandOnDone')),
|
|
5884
|
+
h('span', { className: 'dsa-muted' }, t('setAutoExpandOnDoneHint')),
|
|
5725
5885
|
h('label', { className: 'dsa-check' },
|
|
5726
5886
|
h('input', {
|
|
5727
5887
|
type: 'checkbox',
|
|
@@ -6240,7 +6400,8 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6240
6400
|
: []
|
|
6241
6401
|
} catch { return false }
|
|
6242
6402
|
if (!Array.isArray(rows)) return false
|
|
6243
|
-
const candidates = rows.filter(entry => entry?.kind === kind
|
|
6403
|
+
const candidates = rows.filter(entry => (entry?.kind === kind
|
|
6404
|
+
|| entry?.kind === `dsh-better-sidebar:${kind}`)
|
|
6244
6405
|
&& typeof entry?.tabId === 'string' && entry.tabId !== '')
|
|
6245
6406
|
// 会话已知时按会话匹配;会话未知(旧历史没有 sessionId)时只有
|
|
6246
6407
|
// 全进程恰好一张该 kind 的 tab 才敢关,否则可能误关别的会话。
|
|
@@ -6276,15 +6437,19 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6276
6437
|
const better = surfaces.better
|
|
6277
6438
|
if (better === null || better === undefined) return
|
|
6278
6439
|
if (better.service === null || typeof better.service?.closeTab !== 'function') return
|
|
6279
|
-
// 单一 page
|
|
6280
|
-
//
|
|
6281
|
-
//
|
|
6282
|
-
|
|
6440
|
+
// 单一 page 地址(每个会话一个):只有本会话里最后 open 的那张卡
|
|
6441
|
+
// 拥有栏里的 tab;释放一张不是本会话所有者的历史条目,会把现在
|
|
6442
|
+
// 真正在栏里的卡片连带关掉。本会话无主时也放行(理由见 better.close)。
|
|
6443
|
+
const knownSession = typeof sessionId === 'string' && sessionId !== ''
|
|
6444
|
+
const holderId = better.owners.get(ownerKeyOf(sessionId))
|
|
6445
|
+
if (holderId !== undefined && holderId !== id) return
|
|
6446
|
+
if (holderId === undefined && !knownSession && better.cardId !== null && better.cardId !== id) return
|
|
6283
6447
|
const scope = typeof sessionId === 'string' && sessionId !== '' ? { sessionId } : undefined
|
|
6284
6448
|
const mountedId = embeddedTabIds.get(id)
|
|
6285
6449
|
if (typeof mountedId === 'string' && mountedId !== '') {
|
|
6286
6450
|
try { better.service.closeTab(mountedId, scope) } catch { /* the host may already be gone */ }
|
|
6287
6451
|
better.cardId = null
|
|
6452
|
+
if (better.owners.get(ownerKeyOf(sessionId)) === id) better.owners.delete(ownerKeyOf(sessionId))
|
|
6288
6453
|
return
|
|
6289
6454
|
}
|
|
6290
6455
|
if (closeRailRow(CARD_KIND_BETTER, sessionId)) {
|
|
@@ -6310,8 +6475,9 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6310
6475
|
const native = surfaces.native
|
|
6311
6476
|
if (native === null || native === undefined) return
|
|
6312
6477
|
if (native.controller === null || native.controller === undefined) return
|
|
6313
|
-
//
|
|
6314
|
-
|
|
6478
|
+
// 本会话无主时也放行:理由见 native.close。
|
|
6479
|
+
const holderId = native.owners.get(ownerKeyOf(sessionId))
|
|
6480
|
+
if (holderId !== undefined && holderId !== id) return
|
|
6315
6481
|
try {
|
|
6316
6482
|
native.close({ id, sessionId })
|
|
6317
6483
|
} catch { /* the rail owns its tab's lifetime */ }
|
|
@@ -6349,6 +6515,41 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6349
6515
|
}
|
|
6350
6516
|
|
|
6351
6517
|
const actions = {
|
|
6518
|
+
/**
|
|
6519
|
+
* Close the rail page that is rendering `props`, by its host tab id
|
|
6520
|
+
* when the host gave one, else by kind + session through the rail
|
|
6521
|
+
* inventory. Used by the orphan page's own close button.
|
|
6522
|
+
* @param {object} props - the body props the host handed `CardHost`.
|
|
6523
|
+
* @returns {boolean} whether a close was issued.
|
|
6524
|
+
*/
|
|
6525
|
+
closeRailTab(props) {
|
|
6526
|
+
const tabId = typeof props?.tab?.id === 'string' && props.tab.id !== '' ? props.tab.id : null
|
|
6527
|
+
const scopeId = typeof props?.scope?.sessionId === 'string' && props.scope.sessionId !== ''
|
|
6528
|
+
? props.scope.sessionId
|
|
6529
|
+
: (props?.tab?.sessionId ?? null)
|
|
6530
|
+
const controller = surfaces.native === null ? null : surfaces.native.controller
|
|
6531
|
+
try {
|
|
6532
|
+
if (tabId !== null && controller !== null && controller !== undefined) {
|
|
6533
|
+
if (typeof controller.closeIn === 'function' && typeof scopeId === 'string') {
|
|
6534
|
+
controller.closeIn(scopeId, tabId)
|
|
6535
|
+
return true
|
|
6536
|
+
}
|
|
6537
|
+
if (typeof controller.close === 'function') {
|
|
6538
|
+
controller.close(tabId)
|
|
6539
|
+
return true
|
|
6540
|
+
}
|
|
6541
|
+
}
|
|
6542
|
+
} catch { /* the rail owns its tab's lifetime */ }
|
|
6543
|
+
if (typeof scopeId === 'string' && closeRailRow(CARD_KIND, scopeId)) return true
|
|
6544
|
+
const better = surfaces.better
|
|
6545
|
+
try {
|
|
6546
|
+
if (tabId !== null && better?.service && typeof better.service.closeTab === 'function') {
|
|
6547
|
+
better.service.closeTab(tabId, typeof scopeId === 'string' ? { sessionId: scopeId } : undefined)
|
|
6548
|
+
return true
|
|
6549
|
+
}
|
|
6550
|
+
} catch { /* host may be gone */ }
|
|
6551
|
+
return false
|
|
6552
|
+
},
|
|
6352
6553
|
openPopover(candidate) {
|
|
6353
6554
|
// P2-2: the composer must be remembered BEFORE the popover's
|
|
6354
6555
|
// textarea takes focus, otherwise "insert into composer" has no
|
|
@@ -6503,7 +6704,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6503
6704
|
const config = store.state.config
|
|
6504
6705
|
const cut = truncateSelection(candidate.text, config.maxChars)
|
|
6505
6706
|
const card = {
|
|
6506
|
-
id: `card-${Date.now()}-${nextCardId++}`,
|
|
6707
|
+
id: `card-${Date.now().toString(36)}-${nextCardId++}-${Math.random().toString(36).slice(2, 8)}`,
|
|
6507
6708
|
question: payload.question,
|
|
6508
6709
|
selection: candidate.text,
|
|
6509
6710
|
selected: cut.text,
|
|
@@ -6589,6 +6790,15 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6589
6790
|
// what the "moved to the flow stack" body below covers.
|
|
6590
6791
|
const proof = setTimeout(() => {
|
|
6591
6792
|
pendingProofs.delete(card.id)
|
|
6793
|
+
// A card whose session is not the one on screen is queued by
|
|
6794
|
+
// the host, not refused: the rail only mounts on-screen tabs.
|
|
6795
|
+
// Judging it "unproven" here moved a perfectly live card to
|
|
6796
|
+
// the flow stack and closed the tab it had just opened.
|
|
6797
|
+
const ownSession = typeof card.sessionId === 'string' && card.sessionId !== ''
|
|
6798
|
+
? card.sessionId
|
|
6799
|
+
: null
|
|
6800
|
+
if (ownSession !== null && store.state.sessionId !== null
|
|
6801
|
+
&& ownSession !== store.state.sessionId) return
|
|
6592
6802
|
const live = store.state.cards.find(item => item.id === card.id)
|
|
6593
6803
|
if (live === undefined || live.surface === 'flow') return
|
|
6594
6804
|
if (renderedCardId === card.id) return
|
|
@@ -6874,7 +7084,11 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6874
7084
|
// and managed explicitly in the settings-page history manager.
|
|
6875
7085
|
},
|
|
6876
7086
|
async saveConfig(patch) {
|
|
6877
|
-
|
|
7087
|
+
// 宿主要求 inherit 显式确认(防误触/静默提权);用户点"保存"本身
|
|
7088
|
+
// 就是那个确认动作,恶意同源脚本仍可伪造 —— 它不是安全边界,
|
|
7089
|
+
// 只让这种变更必须是有意的。
|
|
7090
|
+
const body = patch?.sideTools === 'inherit' ? { ...patch, confirmInheritTools: true } : patch
|
|
7091
|
+
const value = await postJson('config', body)
|
|
6878
7092
|
store.set({
|
|
6879
7093
|
config: { ...CLIENT_DEFAULTS, ...(value.config ?? {}) },
|
|
6880
7094
|
problems: value.problems ?? [],
|
|
@@ -7046,6 +7260,8 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7046
7260
|
})
|
|
7047
7261
|
controllers.delete(cardId)
|
|
7048
7262
|
syncPersisted()
|
|
7263
|
+
// 2.1.9:答完就让答案出现在卡片里(胶囊态不再只显示一行状态)。
|
|
7264
|
+
revealFinishedCard(cardId)
|
|
7049
7265
|
// #5: background finish notification (guarded inside).
|
|
7050
7266
|
const finished = store.state.cards.find(item => item.id === cardId)
|
|
7051
7267
|
if (finished !== undefined) notifyCardFinish(finished)
|
|
@@ -7055,6 +7271,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7055
7271
|
flushStreamBuffers()
|
|
7056
7272
|
patchCard(cardId, { status: 'error', error, elapsedMs: Date.now() - startedAt })
|
|
7057
7273
|
controllers.delete(cardId)
|
|
7274
|
+
revealFinishedCard(cardId)
|
|
7058
7275
|
const failed = store.state.cards.find(item => item.id === cardId)
|
|
7059
7276
|
if (failed !== undefined) notifyCardFinish(failed)
|
|
7060
7277
|
},
|
|
@@ -7536,6 +7753,12 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7536
7753
|
title: () => t('answerTitle'),
|
|
7537
7754
|
description: () => t('settingsDesc'),
|
|
7538
7755
|
order: 120,
|
|
7756
|
+
// 0.28.x: a registered type is added to the + menu automatically,
|
|
7757
|
+
// and a guide entry opened with no card behind it renders this
|
|
7758
|
+
// plugin's "card closed" page — two dead entries per session
|
|
7759
|
+
// (native guide + this mirror). `hidden` keeps the type usable
|
|
7760
|
+
// through `openTab` while dropping it from the menu.
|
|
7761
|
+
hidden: true,
|
|
7539
7762
|
dedupeKey: tab => (tab?.meta?.cardId !== undefined ? String(tab.meta.cardId) : undefined),
|
|
7540
7763
|
component: props => h(CardHost, {
|
|
7541
7764
|
props: { tab: props?.tab, sessionId: props?.scope?.sessionId },
|
|
@@ -7587,7 +7810,16 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7587
7810
|
function CardHost({ props, surface }) {
|
|
7588
7811
|
const state = useStoreState()
|
|
7589
7812
|
const cardId = hostCardIdOf(props)
|
|
7590
|
-
|
|
7813
|
+
// A tab's `meta.cardId` is just a string the host persisted; it must
|
|
7814
|
+
// not route this body to a card that lives in ANOTHER session.
|
|
7815
|
+
const scopeId = typeof props?.scope?.sessionId === 'string' && props.scope.sessionId !== ''
|
|
7816
|
+
? props.scope.sessionId
|
|
7817
|
+
: null
|
|
7818
|
+
const card = state.cards.find(item => item.id === cardId
|
|
7819
|
+
&& (scopeId === null
|
|
7820
|
+
|| typeof item.sessionId !== 'string'
|
|
7821
|
+
|| item.sessionId === ''
|
|
7822
|
+
|| item.sessionId === scopeId))
|
|
7591
7823
|
// The embedder's REAL tab id (better-sidebar 0.28.x mints it and
|
|
7592
7824
|
// ignores the seed id we passed), recorded for close/delete.
|
|
7593
7825
|
const hostTabId = typeof props?.tab?.id === 'string' && props.tab.id !== '' ? props.tab.id : null
|
|
@@ -7596,7 +7828,11 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7596
7828
|
React.useEffect(() => {
|
|
7597
7829
|
if (typeof cardId !== 'string' || cardId === '') return undefined
|
|
7598
7830
|
renderedCardId = cardId
|
|
7599
|
-
|
|
7831
|
+
everMountedCards.add(cardId)
|
|
7832
|
+
if (hostTabId !== null) {
|
|
7833
|
+
embeddedTabIds.set(cardId, hostTabId)
|
|
7834
|
+
lastMountedHostTabId = hostTabId
|
|
7835
|
+
}
|
|
7600
7836
|
// Mounting settles the render proof, even when this host mounted
|
|
7601
7837
|
// late (a lazy tab panel slower than the 600ms timer) — the proof
|
|
7602
7838
|
// must never fire after the card is demonstrably on screen.
|
|
@@ -7605,6 +7841,13 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7605
7841
|
clearTimeout(proof)
|
|
7606
7842
|
pendingProofs.delete(cardId)
|
|
7607
7843
|
}
|
|
7844
|
+
// A body whose card is gone, for a tab THIS page mounted before:
|
|
7845
|
+
// release it now. A tab restored by another window was never
|
|
7846
|
+
// mounted here (`everMountedCards`), so it keeps its close button
|
|
7847
|
+
// instead of being stolen from that window.
|
|
7848
|
+
if (card === undefined && everMountedCards.has(cardId)) {
|
|
7849
|
+
try { actions.closeRailTab(props) } catch { /* host may be gone */ }
|
|
7850
|
+
}
|
|
7608
7851
|
return () => {
|
|
7609
7852
|
if (renderedCardId === cardId) renderedCardId = null
|
|
7610
7853
|
if (hostTabId !== null && embeddedTabIds.get(cardId) === hostTabId) {
|
|
@@ -7612,10 +7855,26 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7612
7855
|
}
|
|
7613
7856
|
}
|
|
7614
7857
|
}, [cardId, hostTabId])
|
|
7858
|
+
// No live card claims this body: a tab the host restored from its
|
|
7859
|
+
// persisted layout, or a card deleted before its body mounted. Give
|
|
7860
|
+
// the page an exit — the old build rendered one grey line and left
|
|
7861
|
+
// the user with no way to close it (A1 was fixed for the native
|
|
7862
|
+
// sweep, never for this face).
|
|
7615
7863
|
if (card === undefined) {
|
|
7616
|
-
return h('div', {
|
|
7864
|
+
return h('div', {
|
|
7865
|
+
className: 'dsa-settings',
|
|
7866
|
+
'data-dsa-root': '',
|
|
7867
|
+
'data-dsa-rail-host': '',
|
|
7868
|
+
'data-dsa-card-id': typeof cardId === 'string' ? cardId : '',
|
|
7869
|
+
},
|
|
7617
7870
|
h(Styles, null),
|
|
7618
|
-
h('div', { className: 'dsa-muted' }, t('cardClosed'))
|
|
7871
|
+
h('div', { className: 'dsa-muted' }, t('cardClosed')),
|
|
7872
|
+
h('button', {
|
|
7873
|
+
type: 'button',
|
|
7874
|
+
className: 'dsa-btn',
|
|
7875
|
+
'data-dsa-action': 'close-orphan-tab',
|
|
7876
|
+
onClick: () => { try { actions.closeRailTab(props) } catch { /* host may be gone */ } },
|
|
7877
|
+
}, t('closeThisTab')))
|
|
7619
7878
|
}
|
|
7620
7879
|
if (card.surface === 'flow') {
|
|
7621
7880
|
// The proof already moved this card to the flow stack and this
|
|
@@ -7624,11 +7883,22 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
7624
7883
|
// note for a rail that keeps it.
|
|
7625
7884
|
try { surfaces.native.close(card) } catch { /* the rail owns its lifecycle */ }
|
|
7626
7885
|
try { surfaces.better.close(card) } catch { /* the host may already be gone */ }
|
|
7627
|
-
return h('div', {
|
|
7886
|
+
return h('div', {
|
|
7887
|
+
className: 'dsa-settings',
|
|
7888
|
+
'data-dsa-root': '',
|
|
7889
|
+
'data-dsa-rail-host': '',
|
|
7890
|
+
'data-dsa-card-id': typeof cardId === 'string' ? cardId : '',
|
|
7891
|
+
},
|
|
7628
7892
|
h(Styles, null),
|
|
7629
7893
|
h('div', { className: 'dsa-muted' }, t('cardMovedToFlow')))
|
|
7630
7894
|
}
|
|
7631
|
-
return h('div', {
|
|
7895
|
+
return h('div', {
|
|
7896
|
+
className: 'dsa-settings',
|
|
7897
|
+
'data-dsa-root': '',
|
|
7898
|
+
'data-dsa-rail-host': '',
|
|
7899
|
+
'data-dsa-card-id': typeof cardId === 'string' ? cardId : '',
|
|
7900
|
+
style: { padding: '10px' },
|
|
7901
|
+
},
|
|
7632
7902
|
h(Styles, null),
|
|
7633
7903
|
h(MemoAnswerCard, {
|
|
7634
7904
|
card,
|
package/index.js
CHANGED
|
@@ -45,7 +45,7 @@ export const name = 'dsh-sidecard-ask'
|
|
|
45
45
|
export const inject = ['webServer']
|
|
46
46
|
|
|
47
47
|
/** Version of this plugin (kept in step with package.json by test/verify.mjs). */
|
|
48
|
-
export const PLUGIN_VERSION = '2.1.
|
|
48
|
+
export const PLUGIN_VERSION = '2.1.9'
|
|
49
49
|
|
|
50
50
|
/** Route prefix of the plugin's own API. */
|
|
51
51
|
export const ROUTE_PREFIX = '/sidecard-ask/api'
|
|
@@ -127,6 +127,9 @@ export const DEFAULT_CONFIG = {
|
|
|
127
127
|
* floating layer is used either way.
|
|
128
128
|
*/
|
|
129
129
|
dockRail: true,
|
|
130
|
+
// 2.1.9: expand the floating layer when an answer settles, so the answer is
|
|
131
|
+
// visible in the side card instead of hiding behind the capsule pill.
|
|
132
|
+
autoExpandOnDone: true,
|
|
130
133
|
/**
|
|
131
134
|
* One-click quick actions rendered beside the selection trigger.
|
|
132
135
|
*
|
|
@@ -534,7 +537,7 @@ export function sanitizeLayer(raw, problems = []) {
|
|
|
534
537
|
if (value !== null && (value === '' || looksLikeEmail(value))) out.feedbackSmtpFrom = value
|
|
535
538
|
else problems.push('feedbackSmtpFrom: 必须是合法邮箱或空字符串,已忽略')
|
|
536
539
|
}
|
|
537
|
-
for (const boolKey of ['showInUnclassified', 'notifyOnDone', 'contextMenu', 'followUpSuggestions', 'dockRail']) {
|
|
540
|
+
for (const boolKey of ['showInUnclassified', 'notifyOnDone', 'contextMenu', 'followUpSuggestions', 'dockRail', 'autoExpandOnDone']) {
|
|
538
541
|
if (raw[boolKey] === undefined) continue
|
|
539
542
|
if (typeof raw[boolKey] === 'boolean') out[boolKey] = raw[boolKey]
|
|
540
543
|
else problems.push(`${boolKey}: 必须是布尔值,已忽略`)
|
|
@@ -1346,7 +1349,12 @@ export function readOnlyToolFilter(ctx) {
|
|
|
1346
1349
|
if (!Array.isArray(known)) return undefined
|
|
1347
1350
|
const names = new Set(known.map(schema => schema?.name).filter(name => typeof name === 'string'))
|
|
1348
1351
|
const allow = READONLY_TOOL_NAMES.filter(name => names.has(name))
|
|
1349
|
-
|
|
1352
|
+
// A registry that answered but names NONE of our read-only tools cannot
|
|
1353
|
+
// honour the guard. Returning `undefined` here made the child inherit every
|
|
1354
|
+
// parent tool (fail-open) while the card only said "unsupported" — the guard
|
|
1355
|
+
// disappeared exactly when a host upgrade renamed things.
|
|
1356
|
+
if (allow.length === 0) return { blocked: 'no-readonly-guard' }
|
|
1357
|
+
return { allow }
|
|
1350
1358
|
}
|
|
1351
1359
|
|
|
1352
1360
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -1762,6 +1770,7 @@ function createSideEngine(ctx, configOf) {
|
|
|
1762
1770
|
]
|
|
1763
1771
|
const seen = new Set()
|
|
1764
1772
|
let fallback
|
|
1773
|
+
let live
|
|
1765
1774
|
for (const agent of listed) {
|
|
1766
1775
|
const id = agentSessionId(agent)
|
|
1767
1776
|
if (id === undefined || seen.has(id)) continue
|
|
@@ -1769,11 +1778,16 @@ function createSideEngine(ctx, configOf) {
|
|
|
1769
1778
|
const isArchived = archived.has(id)
|
|
1770
1779
|
candidates.push({ id, archived: isArchived })
|
|
1771
1780
|
if (fallback === undefined) fallback = { agent, id, archived: isArchived }
|
|
1772
|
-
if (!isArchived)
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1781
|
+
if (live === undefined && !isArchived) live = { agent, id, archived: false }
|
|
1782
|
+
}
|
|
1783
|
+
// A caller that NAMED a session which is not live is answered by another
|
|
1784
|
+
// one (the host's historical fallback — older clients depend on it), but the
|
|
1785
|
+
// start frame now says WHICH session answered so the card can show it
|
|
1786
|
+
// instead of silently publishing into an unfamiliar conversation.
|
|
1787
|
+
const chosen = live ?? fallback
|
|
1788
|
+
return chosen === undefined
|
|
1775
1789
|
? { agent: undefined, id: undefined, archived: false, candidates }
|
|
1776
|
-
: { ...
|
|
1790
|
+
: { ...chosen, candidates }
|
|
1777
1791
|
}
|
|
1778
1792
|
|
|
1779
1793
|
/**
|
|
@@ -1967,6 +1981,20 @@ function createSideEngine(ctx, configOf) {
|
|
|
1967
1981
|
const preset = typeof config.sideAgentPreset === 'string' ? config.sideAgentPreset.trim() : ''
|
|
1968
1982
|
const presetSupported = caps?.agentPreset === true
|
|
1969
1983
|
const presetInForce = config.sideTools === 'readonly' && preset !== '' && presetSupported
|
|
1984
|
+
// 2.1.9: the read-only guard must fail CLOSED. Without a preset there is
|
|
1985
|
+
// no other real narrowing channel on this host, so a guard we cannot
|
|
1986
|
+
// build is a refusal, not an inheritance.
|
|
1987
|
+
if (config.sideTools === 'readonly' && !presetInForce
|
|
1988
|
+
&& toolFilter !== undefined && toolFilter.blocked === 'no-readonly-guard') {
|
|
1989
|
+
sink.send('error', {
|
|
1990
|
+
id,
|
|
1991
|
+
code: 'no-readonly-guard',
|
|
1992
|
+
message: '这台宿主的工具注册表里认不出任何只读工具,无法保证侧边作答只读,已拒绝本次作答',
|
|
1993
|
+
retryable: false,
|
|
1994
|
+
fallback: 'main',
|
|
1995
|
+
})
|
|
1996
|
+
return
|
|
1997
|
+
}
|
|
1970
1998
|
// Style and the user's own extra instruction ride the SAME persona
|
|
1971
1999
|
// string, so the "persona vs prompt" gate above covers all three of
|
|
1972
2000
|
// them with one branch — there is no second injection point.
|
|
@@ -1979,7 +2007,7 @@ function createSideEngine(ctx, configOf) {
|
|
|
1979
2007
|
signal: controller.signal,
|
|
1980
2008
|
...(personaSupported ? { persona: personaText } : {}),
|
|
1981
2009
|
...(presetInForce ? { agentPreset: preset } : {}),
|
|
1982
|
-
...(toolFilter === undefined || !toolFilterSupported ? {} : { toolFilter }),
|
|
2010
|
+
...(toolFilter === undefined || toolFilter.allow === undefined || !toolFilterSupported ? {} : { toolFilter }),
|
|
1983
2011
|
})
|
|
1984
2012
|
runRecord.dispose = run.dispose
|
|
1985
2013
|
|
|
@@ -2005,12 +2033,20 @@ function createSideEngine(ctx, configOf) {
|
|
|
2005
2033
|
? 'inherit'
|
|
2006
2034
|
: (presetInForce
|
|
2007
2035
|
? 'preset'
|
|
2008
|
-
: (toolFilter
|
|
2009
|
-
? '
|
|
2010
|
-
: (
|
|
2036
|
+
: (toolFilter !== undefined && toolFilter.blocked !== undefined
|
|
2037
|
+
? 'blocked'
|
|
2038
|
+
: (toolFilter === undefined
|
|
2039
|
+
? 'unsupported'
|
|
2040
|
+
: (toolFilterSupported ? 'applied' : 'unsupported')))),
|
|
2011
2041
|
agentPreset: presetInForce ? preset : null,
|
|
2012
2042
|
persona: personaSupported ? 'section' : 'prompt',
|
|
2013
2043
|
parentArchived: parent.archived === true,
|
|
2044
|
+
// The session that actually owns this child. A mismatch with the
|
|
2045
|
+
// requested session is shown on the card, never silently absorbed.
|
|
2046
|
+
parentSessionId: parent.id ?? null,
|
|
2047
|
+
requestedSessionId: typeof request.sessionId === 'string' && request.sessionId !== ''
|
|
2048
|
+
? request.sessionId
|
|
2049
|
+
: null,
|
|
2014
2050
|
})
|
|
2015
2051
|
|
|
2016
2052
|
const childId = run.id
|
|
@@ -2698,6 +2734,25 @@ export function apply(ctx, patchConfig) {
|
|
|
2698
2734
|
writeError(res, 400, 'invalid-config', problems.join(';'))
|
|
2699
2735
|
return
|
|
2700
2736
|
}
|
|
2737
|
+
// 2.1.9: changing WHERE mail is sent (host/port/tls/user) invalidates the
|
|
2738
|
+
// stored authorization code. Keeping it made "host:" a one-field takeover:
|
|
2739
|
+
// point the host at an attacker and the next /feedback hands them the
|
|
2740
|
+
// password in cleartext AUTH PLAIN. Losing the password is recoverable;
|
|
2741
|
+
// leaking it is not.
|
|
2742
|
+
const endpointChanged = ['feedbackSmtpHost', 'feedbackSmtpPort', 'feedbackSmtpSecure', 'feedbackSmtpUser']
|
|
2743
|
+
.some(key => accepted[key] !== undefined && String(accepted[key]) !== String(state.config[key] ?? ''))
|
|
2744
|
+
if (endpointChanged && accepted.feedbackSmtpPass === undefined) {
|
|
2745
|
+
delete userLayer.feedbackSmtpPass
|
|
2746
|
+
problems.push('SMTP 目标已变更,已保存的授权码随之作废,请重新填写')
|
|
2747
|
+
}
|
|
2748
|
+
// The side answerer inheriting the whole tool set is the one irreversible
|
|
2749
|
+
// kind of change here (a page-level script can ask the host to run with the
|
|
2750
|
+
// session's tools). A written confirmation does not stop same-origin code,
|
|
2751
|
+
// but it stops the accident and makes the intent auditable.
|
|
2752
|
+
if (accepted.sideTools === 'inherit' && body.confirmInheritTools !== true) {
|
|
2753
|
+
writeError(res, 400, 'privileged-key', 'sideTools=inherit 需要显式确认(confirmInheritTools: true)')
|
|
2754
|
+
return
|
|
2755
|
+
}
|
|
2701
2756
|
userLayer = { ...userLayer, ...accepted }
|
|
2702
2757
|
const saved = writePersistedConfig(userLayer, ctx.logger)
|
|
2703
2758
|
if (!saved.ok) {
|
package/locale/en.json
CHANGED
|
@@ -41,5 +41,7 @@
|
|
|
41
41
|
"versionViewing": "Viewing an older version (read-only)",
|
|
42
42
|
"suggestionsLabel": "Suggested follow-ups",
|
|
43
43
|
"setFollowUps": "Suggest follow-ups at the end",
|
|
44
|
-
"setFollowUpsHint": "The model ends each answer with a <followups> block of 2–3 next questions, rendered as clickable chips (no extra request)"
|
|
44
|
+
"setFollowUpsHint": "The model ends each answer with a <followups> block of 2–3 next questions, rendered as clickable chips (no extra request)",
|
|
45
|
+
"setAutoExpandOnDone": "Expand the card when the answer finishes",
|
|
46
|
+
"setAutoExpandOnDoneHint": "On: the answer appears in the card the moment it settles. Off: the one-line capsule stays until you click it"
|
|
45
47
|
}
|
package/locale/zh.json
CHANGED
|
@@ -41,5 +41,7 @@
|
|
|
41
41
|
"versionViewing": "正在查看历史版本(只读)",
|
|
42
42
|
"suggestionsLabel": "建议追问",
|
|
43
43
|
"setFollowUps": "答案末尾给出建议追问",
|
|
44
|
-
"setFollowUpsHint": "开启后模型会用 <followups> 块在答案末尾给出 2–3 个可点的下一步问题,点一下即追问(不额外发请求)"
|
|
44
|
+
"setFollowUpsHint": "开启后模型会用 <followups> 块在答案末尾给出 2–3 个可点的下一步问题,点一下即追问(不额外发请求)",
|
|
45
|
+
"setAutoExpandOnDone": "答完自动展开卡片",
|
|
46
|
+
"setAutoExpandOnDoneHint": "开启后,回答一结束就展开成卡片显示正文;关闭后保持一行胶囊,点一下才展开"
|
|
45
47
|
}
|