dsh-sidecard-ask 2.0.0 → 2.0.2
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 +113 -0
- package/README.md +17 -12
- package/client.js +201 -43
- package/index.js +443 -72
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,118 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.2
|
|
4
|
+
|
|
5
|
+
**主题:跨端(网页端 / 桌面端)排查后的六个修复 —— 插件 API 补上宿主会话鉴权、诊断上报不再互相覆盖、prompt 预算真正守住 60k、SMTP 密码状态自洽、未知接口状态码对称、用户层配置按 profile 分文件。**
|
|
6
|
+
|
|
7
|
+
排查方式:网页端在真实宿主上跑运行态探针(curl 打全部 7 个接口 + 真跑一次 SSE 作答);桌面端读
|
|
8
|
+
`DeepSeek Harness.exe` 里 `app.asar` 的 `lib/main.js`(网络转发、权限、下载拦截、窗口参数)做静态取证。
|
|
9
|
+
|
|
10
|
+
### 安全
|
|
11
|
+
|
|
12
|
+
- **插件 API 之前绕过了宿主的 401 登录门禁**(D1)。`ctx.webServer.register` 注册的 prefix 路由不经过
|
|
13
|
+
`dsh-client-connection` 的 `authorizeIndex`,插件自己又只做「回环 / 可信域 + `Origin` + `Sec-Fetch-Site`」围栏——
|
|
14
|
+
实测**不带任何 cookie 的 `POST /sidecard-ask/api/ask` 真的会 spawn 一个子代理**,`POST /config` 会写配置。
|
|
15
|
+
现在在围栏之后追加一次 `ctx.get('connection').requestRejection(req)`:
|
|
16
|
+
- 返回 `401` → 一律 `{code:"unauthorized"}`「需要先通过 DSH 登录(宿主会话未认证)」;
|
|
17
|
+
- 返回 `403` → `{code:"forbidden"}`「请求来源不受信任」;
|
|
18
|
+
- 宿主**没有**挂载 `connection` 服务(旧组合 / 单测)→ 只记一条 info 日志,退回原来的单层围栏;
|
|
19
|
+
- 校验本身抛错 → fail-closed 按 401 拒绝。
|
|
20
|
+
桌面端不受影响:`forwardWebRequest` 本来就会注入宿主启动时换来的鉴权 cookie(`lib/main.js:8268-8299`)。
|
|
21
|
+
|
|
22
|
+
### 修复
|
|
23
|
+
|
|
24
|
+
- **`/diagnose` 上报是 last-write-wins**(D2)。`clientReport = report` 无条件覆盖,于是「哪个页面最后上报,
|
|
25
|
+
`/state` 里的客户端诊断就只剩它看到的那点东西」——实测发一个 `{}` 就能把整段诊断清空。改为
|
|
26
|
+
`mergeClientReport(previous, next)`:本次没观测到的字段保留旧值(`slots` / `native` / `better` 浅合并),
|
|
27
|
+
`at` 取新值;完全空的上报直接 `400 bad-request`,不再当成一次有效观测。
|
|
28
|
+
- **prompt 预算算不准**(D3)。`truncateSelection` 把省略标记加在预算之外(给 100 字符的预算返回 119 字符),
|
|
29
|
+
`fixedLength` 又比实际 join 少算一个分隔符,实测总量落到 60018–60023(`MAX_PROMPT_CHARS = 60000`),
|
|
30
|
+
极端组合(超长 question + 6 轮历史)能把 selection 压到 1 个字符。现在:
|
|
31
|
+
- 省略标记**计入**预算,任何 `maxChars` 下结果都 ≤ cap(`test/contract-test.mjs` 用 9 档 cap × 900 点的
|
|
32
|
+
emoji 样本逐档断言);
|
|
33
|
+
- 固定块先算长度、selection 吃剩余预算,问题行长按 `MAX_QUESTION_CHARS = 20000` 先截断,
|
|
34
|
+
selection 保留 `SELECTION_FLOOR_CHARS = 2000` 下限,预算不够时按轮丢弃更旧的历史而不是把 selection 压没;
|
|
35
|
+
- 最坏组合(19000 字问题 + 6 轮历史 + 20000 字 selection)总量验证 ≤ 60000;
|
|
36
|
+
- `start` 帧新增 `promptTruncated` / `promptDroppedChars` / `promptHistoryDropped`:prompt 侧的截断
|
|
37
|
+
以前对客户端完全不可见,用户只会看到"答案变短了"。
|
|
38
|
+
- **SMTP 状态自相矛盾**(D4)。`configured` 走 `resolveSmtp()`(环境变量优先),`hasPassword` 却只看设置页里那一格,
|
|
39
|
+
于是「凭据来自环境变量」时报出 `configured: true` + `hasPassword: false`。现在 `hasPassword` 表示
|
|
40
|
+
**手上真的有一份能用的密码**(任一层),并新增 `source`(`config` / `env`)与 `storedPassword`
|
|
41
|
+
(设置页里是否存了一份)区分两层来源。
|
|
42
|
+
- **未知接口的状态码不对称**(D5)。`GET /nope` 回 405(`method-error`)、`POST /nope` 回 404(`not-found`)——
|
|
43
|
+
方法名判定排在动词判定之后。现在先判方法名:任何动词打未知接口都是 `404 not-found`,已知接口用错动词才是 405。
|
|
44
|
+
- **网页端与桌面端共用同一份 `config.json`**(D6)。两边是同一台机器上的两个 profile,共用一个 `DSH_HOME`,
|
|
45
|
+
于是 web 端存的 `sideTools: inherit` 会直接作用于桌面端的运行。用户层文件改为
|
|
46
|
+
`<DSH_HOME>/sidecard-ask/config.<DSH_PROFILE>.json`(profile 名只接受 `[A-Za-z0-9._-]`,否则退回旧名,
|
|
47
|
+
防止路径穿越);`DSH_PROFILE` 未设置时仍是 `config.json`,**旧文件不迁移、不删除**:新 profile 第一次启动
|
|
48
|
+
会继承它一次(`/state` 的 `provenance.persistedInheritedFrom` 会说明),下一次保存在自己的文件里落盘。
|
|
49
|
+
- **客户端 `getState()` 丢掉了宿主的错误码**(D1 的配套)。`ok !== true` 时统一抛 `bad-response`,于是 401 会被
|
|
50
|
+
渲染成"宿主接口不可达"。现在透传 `body.error.code`,并给 `unauthorized` / `forbidden` 各加一条中英文案。
|
|
51
|
+
|
|
52
|
+
### 复审追加(同版本内)
|
|
53
|
+
|
|
54
|
+
- **一个 `%` 就能打断设置页**。`parseSmtpUrl` 用 `decodeURIComponent` 解账号/密码,密码里带未转义的 `%`
|
|
55
|
+
(`pass%word`)会抛 `URIError`;异常经 `describeFeedback → resolveSmtp → readSmtpFromEnv` 一路逃到 `/state`,
|
|
56
|
+
而 `/state` 分支当时在 `routeHandler` 的 `try` 之外,于是整页变成「宿主接口不可达」。现在解析失败返回
|
|
57
|
+
`{ok:false, reason}`,且 `/state` 与其余接口共用同一个 try(失败回可读的 500,而不是未处理的拒绝)。
|
|
58
|
+
- **`history` 里的坏条目会把提问打成 TypeError**。`composeSidePrompt` 直接读 `turn.question`,`history:[null]`
|
|
59
|
+
在 `ask()` 的 try 之前抛出,前端只看到英文 `Cannot read properties of null`。现在非对象条目被丢弃;
|
|
60
|
+
顺带修掉 `historyTurns: 0` 被 `slice(-0)` 解释成「全部历史」的边界。
|
|
61
|
+
- **历史持久化在配额满时是 O(n²)**。按字段上限单卡最坏 ~0.9 MB、默认 10 卡 ~8.8 MB,超过常见 5 MB 配额后
|
|
62
|
+
旧实现逐张丢弃并整份重新 `JSON.stringify`。现在先按 2M UTF-16 单元的总预算裁剪,失败时折半丢弃。
|
|
63
|
+
- **答案流式期间重复解析 reasoning**。`renderRichText(card.reasoning)` 未 memo,答案每 50 ms 刷新都重解析
|
|
64
|
+
整段思考过程;现在与正文一样按文本 memo。
|
|
65
|
+
- **旧版插件探测在启动 6 秒内全文档扫描**。`MutationObserver` 每次 DOM 变更都跑
|
|
66
|
+
`querySelectorAll('.dsa-layer:not([data-dsa-owner])')`,现在合并成 250 ms 一次的尾随扫描,卸载时清定时器。
|
|
67
|
+
- **已存的 SMTP 密码无法清除**(空串=保留)。新增 `feedbackSmtpPassClear: true` 显式清除。
|
|
68
|
+
- `inlineNodes` 的 token 正则从每次调用新建改为模块级常量(用前复位 `lastIndex`)。
|
|
69
|
+
- `openSse.end()` 在流已关闭时提前返回、跳过 `clearInterval`,现在关闭动作统一走 `shutdown()`。
|
|
70
|
+
- README 自测规模更正为实测值,并补上本轮 9 条回归断言(坏 `%` 两处、坏 `history` 条目、`historyTurns: 0`、
|
|
71
|
+
配额下的裁剪/落盘/删除保持)——现为 verify 57 + contract 1377 + float-placement 82 + smoke 262 = **1778 项**;
|
|
72
|
+
同时删掉仓库里未声明、无 lock 的 `node_modules`(js-yaml / argparse)残留,`.acl-recovery/` 加入 `.gitignore`。
|
|
73
|
+
|
|
74
|
+
### 未修:宿主 / 安装包侧(不是本插件能修的)
|
|
75
|
+
|
|
76
|
+
- **桌面端在本机冷启动即崩**:`DeepSeek Harness.exe` 6 秒内退出,唯一确凿栈是
|
|
77
|
+
`Received fatal exception EXCEPTION_BREAKPOINT` → `electron::fuses::IsLoadBrowserProcessSpecificV8SnapshotEnabled`
|
|
78
|
+
→ `v8::ScriptCompiler::CreateCodeCacheForFunction`,崩在 V8 snapshot fuse 初始化阶段、早于任何插件加载。
|
|
79
|
+
安装根还缺 `browser_v8_context_snapshot.bin`。因此桌面端的结论只能来自 `app.asar` 静态取证,没有运行态验证。
|
|
80
|
+
- **桌面壳没有 `app.setAppUserModelId`**(只有 `setAsDefaultProtocolClient("dsh")`,`lib/main.js:13055`):
|
|
81
|
+
Windows 上 toast 通知可能不显示。插件 `notifyOnDone` 走的就是 `new window.Notification`,这一项**待验证**,
|
|
82
|
+
插件侧无法兜底(`Notification` 是桌面壳的默认会话权限,非麦克风权限一律放行)。
|
|
83
|
+
|
|
84
|
+
### 排查中推翻的三个旧判断
|
|
85
|
+
|
|
86
|
+
1. **桌面端下载没有被取消。** `will-download → preventDefault()` 只出现在 `dsh-policy-auth-*` 登录窗
|
|
87
|
+
(`lib/main.js:10113`)和侧边栏 webview 的 `dsh-sidebar-browser-*` 分区(`lib/main.js:10632`,由
|
|
88
|
+
`configureSession` 安装)。主窗口走 `defaultSession`,没有该拦截,`downloadMarkdown()` 在桌面端可用。
|
|
89
|
+
2. **剪贴板不存在"静默无操作"。** `copyText(text, fallbackElement)` 在 `navigator.clipboard` 缺失或失败时
|
|
90
|
+
会 `selectNodeContents` 选中卡片正文并返回 `'selected'`,最后才 `'failed'`。
|
|
91
|
+
3. **`getState()` 没有未处理的 rejection。** 它的两个调用点都收口(`await` + `.catch(() => {})`)。
|
|
92
|
+
|
|
93
|
+
### 测试
|
|
94
|
+
|
|
95
|
+
- `node test/contract-test.mjs` 1377 通过(2.0.1 是 1334);`smoke-test.mjs` 262(原 226);`verify.mjs` 57;
|
|
96
|
+
`float-placement.mjs` 82。合计 **1778 通过,0 失败**。
|
|
97
|
+
- 新增覆盖:`truncateSelection` 的「结果 ≤ cap」不变式(9 档 cap)、最坏组合的 prompt 预算、`start` 帧的
|
|
98
|
+
prompt 侧截断字段、`/diagnose` 的合并与空上报拒绝、宿主会话门禁四种返回(401 / 403 / undefined / 抛错)、
|
|
99
|
+
未知接口在 GET/HEAD/POST 下的 404、profile 分文件与一次性继承、`describeSmtpSettings` 的两层来源;
|
|
100
|
+
复审追加 9 条:坏 `%` 的解析与 `/state` 降级(4)、坏 `history` 条目与 `historyTurns: 0`(2)、
|
|
101
|
+
配额下的历史裁剪 / 落盘 / 删除保持(3)。
|
|
102
|
+
|
|
103
|
+
## 2.0.1
|
|
104
|
+
|
|
105
|
+
**主题:清掉 6 个死 i18n 键。无行为变化,无 API / 配置变化,升级无需任何操作。**
|
|
106
|
+
|
|
107
|
+
- **删除 4 个纯死字符串**:`selectionEmpty`、`selectionTooShort`、`floatDocked`、`resetDone`。它们对应的界面在早期版本
|
|
108
|
+
就已下线,此后没有任何 `t()` 调用点,只是留在字典里被 `locale/*.json` 与打包产物带着走。
|
|
109
|
+
- **删除 2 个重复定义的界面标签**:`familyLegacyDiag`(旧版共载 / Legacy co-load)、`familyPeers`(同页实例 / Copies on page)。
|
|
110
|
+
它们在 zh、en 两个字典里各被**重复定义两次**(后一份静默覆盖前一份),且都没有调用点。同一功能的
|
|
111
|
+
`familyLegacyToast` / `familySuppressed` 仍在使用,保留;**运行时状态字段** `familyPeers`(family 接管详情里的同页实例列表)
|
|
112
|
+
是数据而非标签,同样保留。
|
|
113
|
+
- **字典规模**:`client.js` 每个语言从 272 键降到 266 键,zh / en 键集仍完全一致。
|
|
114
|
+
- **防回流**:`test/contract-test.mjs` 的「退役键必须在两个字典里都是 undefined」断言从 4 个键扩到 10 个。
|
|
115
|
+
|
|
3
116
|
## 2.0.0
|
|
4
117
|
|
|
5
118
|
**主题:追问卡片标题栏的 `×` 从「隐藏整个浮层」改成「删除这条追问对话」——一次 Breaking 的语义重定义,顺带把「隐藏浮层」这套能力整体下线,并移除反馈表单里「用邮件客户端发送」这条出口。**
|
package/README.md
CHANGED
|
@@ -153,7 +153,8 @@ plugin_manager(action="install_bundle", target="D:\\Users\\34332\\AI\\dsh-sideca
|
|
|
153
153
|
|
|
154
154
|
1. **内置默认值**(`index.js` 的 `DEFAULT_CONFIG`)
|
|
155
155
|
2. **bundle 补丁层**:`cordis.patch.yml` 的 `config:`(改这里需要重启)
|
|
156
|
-
3. **用户层**:设置页保存后写入 `<DSH_HOME>/sidecard-ask/config
|
|
156
|
+
3. **用户层**:设置页保存后写入 `<DSH_HOME>/sidecard-ask/config.<profile>.json`,其中 `<profile>` 取自
|
|
157
|
+
`DSH_PROFILE`(web 端与桌面端各一份,互不覆盖);`DSH_PROFILE` 未设置时用旧名 `config.json`
|
|
157
158
|
(`DSH_HOME` 未设置或为空白时回退到 `~/.dsh`;**不会**写进程当前目录)
|
|
158
159
|
|
|
159
160
|
设置页点「重置为 patch 配置」会清空用户层。
|
|
@@ -187,7 +188,8 @@ plugin_manager(action="install_bundle", target="D:\\Users\\34332\\AI\\dsh-sideca
|
|
|
187
188
|
| `feedbackSmtpPort` | 数字,1–65535 | `465` | SMTP 端口。465 配隐式 TLS,587 配 STARTTLS(服务器没声明 STARTTLS 时不会硬升级) | 立即 |
|
|
188
189
|
| `feedbackSmtpSecure` | 布尔 | `true` | 隐式 TLS(465)。关掉则按 STARTTLS 处理 | 立即 |
|
|
189
190
|
| `feedbackSmtpUser` | 字符串,≤254 | `''` | SMTP 登录账号。部分中继给的是裸账号名而非邮箱,故不强制邮箱格式 | 立即 |
|
|
190
|
-
| `feedbackSmtpPass` | 字符串,≤512 | `''` | **密码 / 授权码**(QQ、163 必须用授权码)。**只写不读**:`/state`
|
|
191
|
+
| `feedbackSmtpPass` | 字符串,≤512 | `''` | **密码 / 授权码**(QQ、163 必须用授权码)。**只写不读**:`/state` 会删掉它、页面永远拿到空串;留空保存=保留原值(要删掉已存的密码用下一行的 `feedbackSmtpPassClear`) | 立即 |
|
|
192
|
+
| `feedbackSmtpPassClear` | 布尔 | `false` | **显式清除已存的 SMTP 密码**(典型场景:改回用环境变量)。只在 `/config` 请求里生效,本身不落盘;与 `feedbackSmtpPass` 同时出现时以清除为准 | 立即 |
|
|
191
193
|
| `feedbackSmtpFrom` | 字符串(邮箱)\| `''` | `''` | 发信账号显式指定的 `From:`,留空等于 `feedbackSmtpUser`。必须是 SMTP 服务商允许的地址 | 立即 |
|
|
192
194
|
|
|
193
195
|
非法值会被拒绝(设置页报错、接口返回 `invalid-config`),并把被忽略的项列在 `/state` 的 `problems` 里——不会静默吞掉。
|
|
@@ -283,14 +285,14 @@ bleeds = 对话区右边界 - left > 1 // 只有带宽 < 240px 时才
|
|
|
283
285
|
|
|
284
286
|
**收件邮箱默认可用,也可被环境变量接管。** 打包默认值是维护者邮箱(`b64:` 混淆存储,见上表);
|
|
285
287
|
环境变量 `DSH_SIDECARD_FEEDBACK_TO` 或 `DSH_CRB_FEEDBACK_TO`(与 dsh-conversation-recycle-bin 共用)
|
|
286
|
-
**优先**于它。用户层配置落在 `$DSH_HOME/sidecard-ask/config
|
|
287
|
-
|
|
288
|
-
|
|
288
|
+
**优先**于它。用户层配置落在 `$DSH_HOME/sidecard-ask/config.<profile>.json`
|
|
289
|
+
(`<profile>` = `DSH_PROFILE`;Windows 默认 `C:\Users\<你>\.dsh\sidecard-ask\config.web.json`
|
|
290
|
+
/ `config.desktop.json`,两个壳各一份)——**不在插件目录里,因此不会被提交、不会进 npm 包**。
|
|
289
291
|
|
|
290
292
|
**SMTP 凭证有两种给法**,都落在部署方/维护者这一侧(1.9.1 起设置页不再提供填写框):
|
|
291
293
|
|
|
292
|
-
1. **用户层配置**(`$DSH_HOME/sidecard-ask/config
|
|
293
|
-
`C:\Users\<你>\.dsh\sidecard-ask\config.json`,或 profile 的 `cordis.patch.yml` 里给该行写 `config`):
|
|
294
|
+
1. **用户层配置**(`$DSH_HOME/sidecard-ask/config.<profile>.json`,Windows 默认
|
|
295
|
+
`C:\Users\<你>\.dsh\sidecard-ask\config.web.json`,或 profile 的 `cordis.patch.yml` 里给该行写 `config`):
|
|
294
296
|
`feedbackSmtpHost` / `feedbackSmtpPort` / `feedbackSmtpSecure` / `feedbackSmtpUser` / `feedbackSmtpPass` /
|
|
295
297
|
`feedbackSmtpFrom`。**密码只写不读**:`/state` 删除该键、页面永远拿到空串,留空保存表示保留原值。
|
|
296
298
|
QQ 邮箱请填 `smtp.qq.com` / 465 / `secure: true` / 你的 QQ 邮箱 / **授权码**
|
|
@@ -772,9 +774,9 @@ node test/float-placement.mjs # 浮层定位矩阵
|
|
|
772
774
|
node test/smoke-test.mjs # 冒烟:装载、槽位、渲染、错误路径
|
|
773
775
|
```
|
|
774
776
|
|
|
775
|
-
当前规模(本机实测,四项均 exit 0):verify **57** 项 + contract **
|
|
776
|
-
smoke **
|
|
777
|
-
|
|
777
|
+
当前规模(本机实测,四项均 exit 0):verify **57** 项 + contract **1377** 项 + float-placement **82** 项 +
|
|
778
|
+
smoke **262** 项 = **1778 项全部通过**(较 1.8.4 的 1359 多 419 项;contract 从 989 涨到 1377,新增大多来自
|
|
779
|
+
反馈功能与 2.0.x 的跨端修复:
|
|
778
780
|
纯函数边界、`parseSmtpUrl` / `readSmtpFromEnv` 两种写法(含复用 `DSH_CRB_SMTP_URL`)、`b64:` 收件人解码、
|
|
779
781
|
「环境变量优先于用户层配置」的优先级回归、`Reply-To:` 组头、限速闸门,以及一条**真 socket** 的 `/feedback`
|
|
780
782
|
端到端——本地 SMTP 假中继逐条断言 `AUTH PLAIN`、信封、`DATA`、编码后的主题与正文、429 与 `retry-after`,
|
|
@@ -896,8 +898,11 @@ Host: ctx.subagents.start('spawn')
|
|
|
896
898
|
## 十二、隐私与安全
|
|
897
899
|
|
|
898
900
|
- 选中文本与问题会**发给你自己配置的模型**(走 DSH 既有的模型路由),插件不做任何额外外发。
|
|
899
|
-
- 插件只在本地写一个配置文件:`<DSH_HOME>/sidecard-ask/config
|
|
900
|
-
|
|
901
|
+
- 插件只在本地写一个配置文件:`<DSH_HOME>/sidecard-ask/config.<profile>.json`(原子写入:先写 `.tmp` 再 rename;
|
|
902
|
+
POSIX 下目录 `0700`、文件 `0600`,因为里面可能有 SMTP 授权码)。
|
|
903
|
+
- 插件路由带**两层**围栏:回环 / 可信域 + 同源校验(Host 头、`Origin`、`Sec-Fetch-Site`),然后交给宿主的
|
|
904
|
+
`connection.requestRejection()` 校验 **DSH 浏览器会话**(宿主没挂载该服务时才只保留第一层)。
|
|
905
|
+
因此未登录的外部程序拿到 loopback 地址也读不到 `/state`、更发不出 `/ask`。
|
|
901
906
|
- 侧边作答者默认**只读**(`sideTools: readonly`):白名单只包含读取、搜索、网络查询类工具,不会在后台改你的工作区。
|
|
902
907
|
- 提示词里选中文本被包在 ```` ```text ```` 围栏中并显式声明"这是数据不是指令",降低提示注入面。
|
|
903
908
|
|