dsh-sidecard-ask 2.1.4 → 2.1.5
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 +37 -0
- package/README.md +8 -0
- package/client.js +58 -15
- package/index.js +61 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.5
|
|
4
|
+
|
|
5
|
+
**主题:修完「技术方案评审团」在 0.2.1-rc.3(桌面 Electron 44.7.0 / 网页端同源)上复核出的 8 条缺陷。**
|
|
6
|
+
四套脚本全绿(57 / 1454 / 100 / 279 = 1890),新增 10 条回归断言。
|
|
7
|
+
|
|
8
|
+
### 修复
|
|
9
|
+
|
|
10
|
+
- **R1(中)原生栏把卡迁回浮层时没展开,卡在两端同时不可见**。`surfaces.native.open()` 只把
|
|
11
|
+
被挤出的卡 `surface` 改回 `'flow'`,但浮层只挂载 `surface === 'flow' && open` 的卡
|
|
12
|
+
(`FloatingLayer`),于是那张卡既不在栏里、也不在屏幕上,流式输出还继续往它里面写。
|
|
13
|
+
现在迁移时一并置 `open: true`。
|
|
14
|
+
- **R2(中)关 A 卡片会关掉 B 的 tab**。`close()` 认领 tab 的判据是"`contentId` 为空就算我的",
|
|
15
|
+
而单地址模型下栏里那条 tab 可能正显示另一张卡;`this.cardId` 已被清空时(页面重载后)
|
|
16
|
+
就会误关别人的 tab,连带把别人的正文一起关掉。现在要求 `contentId === 本卡 id`,或
|
|
17
|
+
宿主没报 `contentId` 且适配器仍持有本卡地址。
|
|
18
|
+
- **R3(中)删历史 / 删卡片不释放原生栏的 tab**。tab 是宿主自己的状态,不随 `store.cards`
|
|
19
|
+
变化;删掉卡片后页面里留着一张渲染"已不存在卡片"的空壳,直到用户手动关。新增
|
|
20
|
+
`releaseNativeTab(id)`,`deleteHistoryCard` / `deleteCard` / `clearHistory`(仅历史态卡)都会调用。
|
|
21
|
+
- **R4(中)设置页把只读开关标成"只读(推荐)",而 0.2.1-rc.3 上它根本不生效**。
|
|
22
|
+
宿主的 `SubagentCapabilities` 没有 `toolFilter`,`readOnlyToolFilter()` 返回的白名单会被丢弃,
|
|
23
|
+
侧边作答实际继承会话全部工具——卡片上的 `toolFilter: 'unsupported'` 是对的,但设置页的
|
|
24
|
+
"推荐"是假的。标签改为「只读(需宿主支持,0.2.1 无效)」。
|
|
25
|
+
- **R5(低)`setSideToolsHint` 谎称"只读靠探针自动拒写"**。代码里没有任何"拒写探针",
|
|
26
|
+
真实机制是 `toolFilter`(宿主不支持即失效)或 `agentPreset`(0.2.1-rc.3 支持)。文案改为
|
|
27
|
+
指向 preset,并写明版本前提。
|
|
28
|
+
- **R6(中)`/cancel` 不校验归属**。`runs` 是进程级全局表,任何已认证页面拿到(或猜到)一个
|
|
29
|
+
run id 就能打断别的会话的追问。run 记录现在带 `sessionId`,`cancel(id, sessionId)` 在双方
|
|
30
|
+
都已知且不一致时按"没找到"处理;不带 `sessionId` 的老客户端保持可用。
|
|
31
|
+
- **R8(低)写盘失败的报错只有 errno 原文**。`toWireError()` 现按 `EACCES` / `EPERM` / `EROFS` /
|
|
32
|
+
`ENOSPC` / `EBUSY` / `ENOENT` 追加中文处置提示,`error.code` 不变(程序分支仍按码判断)。
|
|
33
|
+
|
|
34
|
+
### 评估后不改
|
|
35
|
+
|
|
36
|
+
- **R7 `/state` 回配置文件的绝对路径**:该接口要求宿主登录(页面外是 `401 unauthorized`),
|
|
37
|
+
且这个字段是设置页"到底读的哪个文件"的唯一依据。已写进 README §隐私与安全第 16 条,
|
|
38
|
+
说明暴露面边界与转发时的处置办法。
|
|
39
|
+
|
|
3
40
|
## 2.1.4
|
|
4
41
|
|
|
5
42
|
**主题:把「技术方案评审团」查出的 11 条缺陷一次修完。四套脚本全绿,新增 21 条回归断言。**
|
package/README.md
CHANGED
|
@@ -1088,4 +1088,12 @@ npm deprecate "<旧包名>@*" "Renamed to <新包名> - <原因>" # 旧名
|
|
|
1088
1088
|
15. **部署前缀不带尾斜杠时(2.1.0)**:`API` 基址会先按 `document.baseURI` 猜一次,若得到 404
|
|
1089
1089
|
则回退到根绝对路径重试一次。这是唯一安全的判别方式(401/403 都不能用来改基址),
|
|
1090
1090
|
代价是每次页面加载最多多一次 404 往返——正常部署下不会触发。
|
|
1091
|
+
16. **`/state` 会回配置文件的绝对路径(`provenance.persistedPath`,2.1.5 复核后保留)**:这是设置页
|
|
1092
|
+
用来说明"到底读的是哪个文件"的诊断字段,且该接口要求宿主登录(页面外拿到的是
|
|
1093
|
+
`401 unauthorized`)。它**不是**无鉴权暴露面;如果你把 `/state` 的响应转发到别处,请自行删掉
|
|
1094
|
+
这个字段。同一响应里的 `persistedError` 会带上 errno 的中文提示(`EACCES` / `EPERM` /
|
|
1095
|
+
`ENOSPC` / `EBUSY` 等),原始错误码仍保留在 `error.code` 上供程序分支。
|
|
1096
|
+
17. **`/cancel` 校验归属(2.1.5)**:请求体带 `sessionId` 且与 run 的发起会话不一致时,按"没找到"
|
|
1097
|
+
返回 `cancelled: false`(不回 403,避免把别的会话的 run id 是否存在当成探测面)。不带
|
|
1098
|
+
`sessionId` 的老客户端仍然可以取消——向后兼容优先于收紧。
|
|
1091
1099
|
|
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.5'
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* Tab kind served by the DSH native right rail (also its implementation id).
|
|
@@ -707,9 +707,9 @@ window.__ModuleLoader__.load({
|
|
|
707
707
|
setMinChars: '最小触发字符数',
|
|
708
708
|
setConcurrency: '并发作答上限',
|
|
709
709
|
setSideTools: '侧边作答工具权限',
|
|
710
|
-
setSideToolsReadonly: '
|
|
710
|
+
setSideToolsReadonly: '只读(需宿主支持,0.2.1 无效)',
|
|
711
711
|
setSideToolsInherit: '继承当前会话',
|
|
712
|
-
setSideToolsHint: '
|
|
712
|
+
setSideToolsHint: '这个开关只在宿主支持工具白名单时才生效;0.2.1-rc.3 不支持,侧边作答会继承当前会话的全部工具。要真正收窄,请填写下面的 preset(0.2.1-rc.3 已支持)。',
|
|
713
713
|
setSideAgentPreset: '只读 preset(可选)',
|
|
714
714
|
setSideAgentPresetPlaceholder: '留空则不启用',
|
|
715
715
|
setSideAgentPresetHint: '子代理以该 agent preset 启动,工具集由 preset 决定(0.2.1 唯一可靠的收窄通道);留空时退回探针拒写',
|
|
@@ -982,9 +982,9 @@ window.__ModuleLoader__.load({
|
|
|
982
982
|
setMinChars: 'Minimum selection length',
|
|
983
983
|
setConcurrency: 'Max concurrent answers',
|
|
984
984
|
setSideTools: 'Side answerer tool access',
|
|
985
|
-
setSideToolsReadonly: 'Read-only (
|
|
985
|
+
setSideToolsReadonly: 'Read-only (needs host support; no-op on 0.2.1)',
|
|
986
986
|
setSideToolsInherit: 'Inherit from the session',
|
|
987
|
-
setSideToolsHint: '
|
|
987
|
+
setSideToolsHint: 'This switch only works when the host supports a tool allow-list; 0.2.1-rc.3 does not, so the side answerer inherits every tool of the session. To really narrow it, set the preset below (supported on 0.2.1-rc.3).',
|
|
988
988
|
setSideAgentPreset: 'Read-only preset (optional)',
|
|
989
989
|
setSideAgentPresetPlaceholder: 'Leave empty to disable',
|
|
990
990
|
setSideAgentPresetHint: 'Starts the subagent with this agent preset, so the preset decides the tool set — the only reliable narrowing channel on 0.2.1; when empty the write-refusing probe is the fallback',
|
|
@@ -2322,7 +2322,11 @@ window.__ModuleLoader__.load({
|
|
|
2322
2322
|
// 继续往一张看不见的卡片里写。
|
|
2323
2323
|
for (const other of store.state.cards) {
|
|
2324
2324
|
if (other.id !== card.id && other.surface === 'native-rightbar') {
|
|
2325
|
-
|
|
2325
|
+
// `open: true` is load-bearing: the floating layer only mounts a
|
|
2326
|
+
// card whose `surface === 'flow'` AND `open` is true, so migrating
|
|
2327
|
+
// a card back to the flow while leaving it folded hid it on BOTH
|
|
2328
|
+
// surfaces — not in the rail, not on screen, still streaming.
|
|
2329
|
+
patchCard(other.id, { surface: 'flow', open: true })
|
|
2326
2330
|
}
|
|
2327
2331
|
}
|
|
2328
2332
|
this.cardId = card.id
|
|
@@ -2356,12 +2360,15 @@ window.__ModuleLoader__.load({
|
|
|
2356
2360
|
const row = Array.isArray(rows)
|
|
2357
2361
|
? rows.find(entry => entry?.kind === CARD_KIND
|
|
2358
2362
|
&& typeof entry?.tabId === 'string' && entry.tabId !== ''
|
|
2359
|
-
//
|
|
2360
|
-
//
|
|
2361
|
-
//
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2363
|
+
// Ownership, not just kind. The tab belongs to this card when the
|
|
2364
|
+
// host echoes the id it was opened with (`openTab` carries it in
|
|
2365
|
+
// `params.cardId`, which the rail publishes as `contentId`), or
|
|
2366
|
+
// when the host reported no id at all AND this adapter still holds
|
|
2367
|
+
// that card's address. Claiming a tab merely because `contentId`
|
|
2368
|
+
// was empty made "close A" close B's tab — and with it B's body.
|
|
2369
|
+
&& (entry.contentId === card.id
|
|
2370
|
+
|| ((entry.contentId === undefined || entry.contentId === null || entry.contentId === '')
|
|
2371
|
+
&& this.cardId === card.id))
|
|
2365
2372
|
&& (typeof entry?.sessionId !== 'string'
|
|
2366
2373
|
|| store.state.sessionId === null
|
|
2367
2374
|
|| entry.sessionId === store.state.sessionId))
|
|
@@ -6129,6 +6136,26 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6129
6136
|
: [],
|
|
6130
6137
|
})
|
|
6131
6138
|
|
|
6139
|
+
/**
|
|
6140
|
+
* Close the right-rail tab when it is showing this card.
|
|
6141
|
+
*
|
|
6142
|
+
* The rail's tab is not derived from `store.cards`: deleting a card (or
|
|
6143
|
+
* a history entry) only removed it from the stores, so a card that lived
|
|
6144
|
+
* in the native rail left its tab behind — a body rendering an id that
|
|
6145
|
+
* no longer exists, until the user closed it by hand. Only the tab this
|
|
6146
|
+
* adapter actually opened is touched; `close` re-checks ownership.
|
|
6147
|
+
* @param {string} id - the card id that is going away.
|
|
6148
|
+
*/
|
|
6149
|
+
function releaseNativeTab(id) {
|
|
6150
|
+
const native = surfaces.native
|
|
6151
|
+
if (native === null || native === undefined) return
|
|
6152
|
+
if (native.controller === null || native.controller === undefined) return
|
|
6153
|
+
if (native.cardId !== id) return
|
|
6154
|
+
try {
|
|
6155
|
+
native.close({ id })
|
|
6156
|
+
} catch { /* the rail owns its tab's lifetime */ }
|
|
6157
|
+
}
|
|
6158
|
+
|
|
6132
6159
|
const actions = {
|
|
6133
6160
|
openPopover(candidate) {
|
|
6134
6161
|
// P2-2: the composer must be remembered BEFORE the popover's
|
|
@@ -6167,6 +6194,9 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6167
6194
|
const next = store.state.history
|
|
6168
6195
|
.filter(item => item.id !== id)
|
|
6169
6196
|
.slice(0, historyCap())
|
|
6197
|
+
// A deleted entry must not keep a right-rail tab alive: the tab is
|
|
6198
|
+
// not part of the store, so nothing else would ever close it.
|
|
6199
|
+
releaseNativeTab(id)
|
|
6170
6200
|
writePersisted(next)
|
|
6171
6201
|
store.set({
|
|
6172
6202
|
history: next,
|
|
@@ -6194,7 +6224,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6194
6224
|
const controller = controllers.get(id)
|
|
6195
6225
|
if (controller !== undefined) {
|
|
6196
6226
|
controller.abort()
|
|
6197
|
-
void postJson('cancel', { id }).catch(() => {})
|
|
6227
|
+
void postJson('cancel', { id, sessionId: store.state.sessionId }).catch(() => {})
|
|
6198
6228
|
}
|
|
6199
6229
|
controllers.delete(id)
|
|
6200
6230
|
pendingDeltas.delete(id)
|
|
@@ -6211,6 +6241,10 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6211
6241
|
surfaces.native.close(card)
|
|
6212
6242
|
} catch { /* the right rail owns its lifecycle */ }
|
|
6213
6243
|
}
|
|
6244
|
+
// A card restored from durable history has no live `surface`, so the
|
|
6245
|
+
// branch above cannot see its tab; this checks the adapter's own
|
|
6246
|
+
// address instead and is a no-op when nothing is on the rail.
|
|
6247
|
+
releaseNativeTab(id)
|
|
6214
6248
|
const next = store.state.history
|
|
6215
6249
|
.filter(item => item.id !== id)
|
|
6216
6250
|
.slice(0, historyCap())
|
|
@@ -6237,6 +6271,15 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6237
6271
|
* by definition: it is the one action allowed to drop the old keys.
|
|
6238
6272
|
*/
|
|
6239
6273
|
clearHistory() {
|
|
6274
|
+
// A history-only card that happens to sit on the rail loses its
|
|
6275
|
+
// durable entry here; leaving the tab up would show a body for an
|
|
6276
|
+
// entry the manager no longer has. A card that is still live keeps
|
|
6277
|
+
// its tab — this action does not touch running cards.
|
|
6278
|
+
const railCard = surfaces.native?.cardId
|
|
6279
|
+
if (typeof railCard === 'string' && railCard !== ''
|
|
6280
|
+
&& !store.state.cards.some(card => card.id === railCard)) {
|
|
6281
|
+
releaseNativeTab(railCard)
|
|
6282
|
+
}
|
|
6240
6283
|
try {
|
|
6241
6284
|
for (const key of [PERSIST_KEY, PERSIST_V2_KEY, PERSIST_LEGACY_KEY]) {
|
|
6242
6285
|
window.localStorage?.removeItem(key)
|
|
@@ -6542,7 +6585,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6542
6585
|
// so POST /cancel too. Fire-and-forget on purpose — the card is
|
|
6543
6586
|
// already stopped locally, and a dead host must not turn the
|
|
6544
6587
|
// Stop button into a failure.
|
|
6545
|
-
void postJson('cancel', { id: card.id }).catch(() => {})
|
|
6588
|
+
void postJson('cancel', { id: card.id, sessionId: store.state.sessionId }).catch(() => {})
|
|
6546
6589
|
}
|
|
6547
6590
|
patchCard(card.id, { status: 'stopped' })
|
|
6548
6591
|
},
|
|
@@ -6591,7 +6634,7 @@ body[data-ds-dark-theme] .dsa-seg button[data-on="true"]:hover{
|
|
|
6591
6634
|
controller.abort()
|
|
6592
6635
|
// Same host-side release as the Stop button: closing a card
|
|
6593
6636
|
// mid-stream must not leave the child running server-side.
|
|
6594
|
-
void postJson('cancel', { id: card.id }).catch(() => {})
|
|
6637
|
+
void postJson('cancel', { id: card.id, sessionId: store.state.sessionId }).catch(() => {})
|
|
6595
6638
|
}
|
|
6596
6639
|
controllers.delete(card.id)
|
|
6597
6640
|
// A closed card's buffered-but-unflushed deltas belong to nobody:
|
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.5'
|
|
49
49
|
|
|
50
50
|
/** Route prefix of the plugin's own API. */
|
|
51
51
|
export const ROUTE_PREFIX = '/sidecard-ask/api'
|
|
@@ -1248,6 +1248,21 @@ export async function sendSmtpMessage(endpoint, mail, { timeoutMs = 20_000, now
|
|
|
1248
1248
|
}
|
|
1249
1249
|
}
|
|
1250
1250
|
|
|
1251
|
+
/**
|
|
1252
|
+
* Plain-language hints for the filesystem errnos that actually reach a save on
|
|
1253
|
+
* Windows/macOS/Linux. The raw `EACCES: permission denied, open '…'` line is
|
|
1254
|
+
* accurate but tells a user nothing about what to do; the code stays on the
|
|
1255
|
+
* wire (clients branch on it) and the hint is appended to the message.
|
|
1256
|
+
*/
|
|
1257
|
+
const ERRNO_HINTS = {
|
|
1258
|
+
EACCES: '(没有写权限:检查文件是否被其它程序占用,或改用可写目录)',
|
|
1259
|
+
EPERM: '(系统拒绝写入:文件可能被占用或只读)',
|
|
1260
|
+
EROFS: '(所在磁盘是只读的)',
|
|
1261
|
+
ENOSPC: '(磁盘空间不足,设置未保存)',
|
|
1262
|
+
EBUSY: '(文件正被其它进程占用,请稍后重试)',
|
|
1263
|
+
ENOENT: '(目标目录不存在)',
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1251
1266
|
/**
|
|
1252
1267
|
* Reduce any thrown value to a wire error the Client can present.
|
|
1253
1268
|
* @param {unknown} error - the thrown value.
|
|
@@ -1258,7 +1273,15 @@ export function toWireError(error, fallbackCode = 'internal') {
|
|
|
1258
1273
|
if (error !== null && typeof error === 'object') {
|
|
1259
1274
|
const code = typeof error.code === 'string' && error.code !== '' ? error.code : undefined
|
|
1260
1275
|
const message = typeof error.message === 'string' && error.message !== '' ? error.message : undefined
|
|
1261
|
-
if (message !== undefined)
|
|
1276
|
+
if (message !== undefined) {
|
|
1277
|
+
const hint = code !== undefined && Object.prototype.hasOwnProperty.call(ERRNO_HINTS, code)
|
|
1278
|
+
? ERRNO_HINTS[code]
|
|
1279
|
+
: undefined
|
|
1280
|
+
return {
|
|
1281
|
+
code: code ?? fallbackCode,
|
|
1282
|
+
message: hint === undefined || message.includes(hint) ? message : `${message}${hint}`,
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1262
1285
|
}
|
|
1263
1286
|
return { code: fallbackCode, message: String(error) }
|
|
1264
1287
|
}
|
|
@@ -1901,7 +1924,16 @@ function createSideEngine(ctx, configOf) {
|
|
|
1901
1924
|
|
|
1902
1925
|
/** Detach this run's stream listener (frames are per attempt). */
|
|
1903
1926
|
let detach = () => {}
|
|
1904
|
-
const runRecord = {
|
|
1927
|
+
const runRecord = {
|
|
1928
|
+
abort: () => controller.abort(),
|
|
1929
|
+
dispose: undefined,
|
|
1930
|
+
// Which session asked. `runs` is process-global, so `/cancel` needs an
|
|
1931
|
+
// owner to check against: without it any trusted page could abort any
|
|
1932
|
+
// other session's run by guessing (or sniffing) a run id.
|
|
1933
|
+
sessionId: typeof request.sessionId === 'string' && request.sessionId !== ''
|
|
1934
|
+
? request.sessionId
|
|
1935
|
+
: null,
|
|
1936
|
+
}
|
|
1905
1937
|
runs.set(id, runRecord)
|
|
1906
1938
|
|
|
1907
1939
|
try {
|
|
@@ -2123,10 +2155,28 @@ function createSideEngine(ctx, configOf) {
|
|
|
2123
2155
|
}
|
|
2124
2156
|
}
|
|
2125
2157
|
|
|
2126
|
-
/**
|
|
2127
|
-
|
|
2158
|
+
/**
|
|
2159
|
+
* Abort one in-flight run.
|
|
2160
|
+
*
|
|
2161
|
+
* `runs` is process-global: every session shares it. When both sides know the
|
|
2162
|
+
* owning session and they differ, this is a cross-session cancel and is
|
|
2163
|
+
* refused (it reports `false`, exactly like an unknown id, so it does not
|
|
2164
|
+
* confirm that the id exists elsewhere). A request that cannot name its
|
|
2165
|
+
* session is still honoured — older clients and scripted calls must keep
|
|
2166
|
+
* working.
|
|
2167
|
+
* @param {string} id - the run id.
|
|
2168
|
+
* @param {string|null} [sessionId] - the session asking for the cancel.
|
|
2169
|
+
* @returns {boolean} whether a run was aborted.
|
|
2170
|
+
*/
|
|
2171
|
+
function cancel(id, sessionId = null) {
|
|
2128
2172
|
const record = runs.get(id)
|
|
2129
2173
|
if (record === undefined) return false
|
|
2174
|
+
const owner = record.sessionId
|
|
2175
|
+
if (typeof owner === 'string' && owner !== ''
|
|
2176
|
+
&& typeof sessionId === 'string' && sessionId !== ''
|
|
2177
|
+
&& owner !== sessionId) {
|
|
2178
|
+
return false
|
|
2179
|
+
}
|
|
2130
2180
|
record.abort()
|
|
2131
2181
|
return true
|
|
2132
2182
|
}
|
|
@@ -2603,7 +2653,12 @@ export function apply(ctx, patchConfig) {
|
|
|
2603
2653
|
// 只回本次取消的结果:`runs` 是跨会话的全局表,把它的 id 列表回给
|
|
2604
2654
|
// 任意可信页面等于泄露别的会话正在追问的卡片 id(而 cancel 本身也不
|
|
2605
2655
|
// 校验归属),Client 侧也没有任何地方消费这个字段。
|
|
2606
|
-
|
|
2656
|
+
// 归属校验:请求方必须带上自己的 sessionId,与 run 记录不一致时按"没
|
|
2657
|
+
// 找到"处理(不回 403,避免把别的会话的 id 是否存在当成探测面)。
|
|
2658
|
+
const sessionId = typeof body?.sessionId === 'string' && body.sessionId !== ''
|
|
2659
|
+
? body.sessionId
|
|
2660
|
+
: null
|
|
2661
|
+
writeOk(res, { cancelled: engine.cancel(id, sessionId) })
|
|
2607
2662
|
}
|
|
2608
2663
|
|
|
2609
2664
|
/** `/config` — merge a partial config into the user layer and persist it. */
|