dsh-remote-bridge 2.8.1 → 2.9.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/README.en.md +21 -2
- package/README.md +16 -2
- package/docs/api/asyncapi.yaml +8 -1
- package/docs/api/openapi.yaml +76 -2
- package/kotlin-sdk/src/main/kotlin/ai/deepseek/dsh/workspace/DshWorkspaceClient.kt +21 -0
- package/lib/index.d.ts +72 -8
- package/lib/index.js +112 -11
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<img alt="Version" src="https://img.shields.io/badge/version-v2.
|
|
12
|
+
<img alt="Version" src="https://img.shields.io/badge/version-v2.9.0-087f8c">
|
|
13
13
|
<img alt="DSH plugin" src="https://img.shields.io/badge/DeepSeek_Harness-plugin-1f2328">
|
|
14
14
|
<img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-586069">
|
|
15
15
|
<img alt="License" src="https://img.shields.io/badge/license-AGPL--3.0-2da44e">
|
|
@@ -402,7 +402,26 @@ inside the zero-change set, and 4 slots retained with 0 removed.
|
|
|
402
402
|
|
|
403
403
|
## Project
|
|
404
404
|
|
|
405
|
-
- Version: `v2.
|
|
405
|
+
- Version: `v2.9.0`
|
|
406
|
+
- **v2.9.0 makes teammates (sub-sessions) readable**: the phone's team panel could list members and
|
|
407
|
+
sub-sessions but could not open a single one, because `sessions.list` filters out every row with
|
|
408
|
+
`origin === 'subagent'` (the bridge's own `dsh-0.2-bridge.ts:2327`, with a comment explaining that
|
|
409
|
+
exposing them "only yields rows that crash on tap"), so any sub-session id hitting `/messages` gets a
|
|
410
|
+
hard 404. Even past that visibility check the kernel's `validateAddress()` demands the
|
|
411
|
+
`{ kind:'subagent', parentSessionId, childSessionId, mode }` discriminated union instead of a plain
|
|
412
|
+
session address. This release adds a **read-only narrow port guarded by membership**:
|
|
413
|
+
`GET /api/v1/chat/sessions/{leadSessionId}/subagents/{subagentId}/messages`. Authorization takes two
|
|
414
|
+
steps — (1) the Lead session must be visible to the device (reusing `requireSession`, not a single rule
|
|
415
|
+
relaxed); (2) membership must be vouched for by a projection the kernel itself publishes
|
|
416
|
+
(`subagentCatalog[].id` or `agentTeam.members[].id`). Only then is history read directly, deliberately
|
|
417
|
+
**without** calling `requireSession(subagentId)` — removing the `origin` filter would put "crash on tap"
|
|
418
|
+
rows back into the list, while loosening `requireSession` would backdoor every session; the narrow port
|
|
419
|
+
has the cleanest boundary. Read-only (non-GET always 405), and `SUBAGENT_NOT_FOUND` deliberately does
|
|
420
|
+
not distinguish "does not exist" from "not your teammate", otherwise id existence becomes a probe
|
|
421
|
+
surface. New capability bit `subagent-history-v1` (it must be separate from `history-v1`, which always
|
|
422
|
+
404s on sub-session ids and therefore cannot advertise this). The same release re-checked DSH
|
|
423
|
+
0.2.1-rc.2 → rc.5: 18 of the 19 packages the bridge depends on are byte-identical, the only diff being
|
|
424
|
+
a single KDoc comment.
|
|
406
425
|
- **v2.7.1 is a defect-fix release**: the kernel's `sessionController.resolveAgent()` returns an
|
|
407
426
|
`{ agent }` / `{ error }` **envelope**, not the Agent itself (`dsh-api-session-controller/lib/index.js:365-368`
|
|
408
427
|
`liveAgent()` and `:221-246` `resolve()` both have that shape). The bridge used to hand the envelope
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<img alt="Version" src="https://img.shields.io/badge/version-v2.
|
|
12
|
+
<img alt="Version" src="https://img.shields.io/badge/version-v2.9.0-087f8c">
|
|
13
13
|
<img alt="DSH plugin" src="https://img.shields.io/badge/DeepSeek_Harness-plugin-1f2328">
|
|
14
14
|
<img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-586069">
|
|
15
15
|
<img alt="License" src="https://img.shields.io/badge/license-AGPL--3.0-2da44e">
|
|
@@ -433,7 +433,21 @@ DSH **0.2.1-rc.3** 隔离实例上**实跑验过**,不是静态推断:插件
|
|
|
433
433
|
|
|
434
434
|
## 项目信息
|
|
435
435
|
|
|
436
|
-
- 当前版本:`v2.
|
|
436
|
+
- 当前版本:`v2.9.0`
|
|
437
|
+
- **v2.9.0 是团队成员(子会话)可读版**:手机端的团队面板此前只能看到成员与子会话的名字,点开读不到
|
|
438
|
+
任何内容 —— 因为 `sessions.list` 会把 `origin === 'subagent'` 的行整批滤掉(桥自己的
|
|
439
|
+
`dsh-0.2-bridge.ts:2327`,注释写明「暴露出来只会得到点开就崩的行」),子会话 id 打 `/messages`
|
|
440
|
+
必然 404;即使绕过可见性,内核 `validateAddress()` 也要求用
|
|
441
|
+
`{ kind:'subagent', parentSessionId, childSessionId, mode }` 判别联合寻址,而非普通会话地址。
|
|
442
|
+
这一版新增**受成员关系校验的只读窄口**
|
|
443
|
+
`GET /api/v1/chat/sessions/{leadSessionId}/subagents/{subagentId}/messages`:授权两步 ——
|
|
444
|
+
① Lead 会话必须对设备可见(沿用 `requireSession`,规则一行不放宽);② 成员关系必须由内核自己发布的
|
|
445
|
+
投影背书(`subagentCatalog[].id` 或 `agentTeam.members[].id`)。两步都过才直读历史,刻意不再调
|
|
446
|
+
`requireSession(subagentId)` —— 拆 `origin` 过滤会把「点开就崩」的行放回列表,放宽 `requireSession`
|
|
447
|
+
等于给所有会话开后门,窄口的边界最清晰。只读(非 GET 一律 405),错误码 `SUBAGENT_NOT_FOUND`
|
|
448
|
+
刻意不区分「不存在」与「不是成员」否则 id 是否存在就成了探测面。新增能力位 `subagent-history-v1`
|
|
449
|
+
(必须独立于 `history-v1`,后者对子会话永远 404 探测不到)。同版复核了 DSH 0.2.1-rc.2 → rc.5:
|
|
450
|
+
桥依赖的 19 个包中 18 个零差异,唯一差异是一行 KDoc 注释。
|
|
437
451
|
- **v2.7.1 是缺陷修复版**:内核 `sessionController.resolveAgent()` 返回的是 `{ agent }` / `{ error }`
|
|
438
452
|
**包装对象**而非 Agent 本体(`dsh-api-session-controller/lib/index.js:365-368` 的 `liveAgent()` 与
|
|
439
453
|
`:221-246` 的 `resolve()` 都是这个形状),桥此前把包装对象直接当 Agent 传给 `fileUploads.upload()`,
|
package/docs/api/asyncapi.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
asyncapi: 3.0.0
|
|
2
2
|
info:
|
|
3
3
|
title: DeepSeek Harness Mobile Events
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.9.0
|
|
5
5
|
description: |
|
|
6
6
|
Live file and authorized DSH chat events. Reconnect clients must refresh
|
|
7
7
|
REST state; unbounded replay is not provided.
|
|
@@ -154,6 +154,13 @@ channels:
|
|
|
154
154
|
> is missing from the set will start echoing a stale `requestId` from
|
|
155
155
|
> whatever request happened to be in flight when it fired.
|
|
156
156
|
|
|
157
|
+
> **子会话历史不走 WS**(2026-10-30):团队成员 / 子会话的历史只能经
|
|
158
|
+
> REST `GET /chat/sessions/{leadSessionId}/subagents/{subagentId}/messages`
|
|
159
|
+
> 读取,本协议**没有**对应帧,也不打算加。原因是普通 `history` 帧对子会话
|
|
160
|
+
> id 必然 404(子会话被 `sessions.list` 刻意滤掉),而那条窄口的授权靠
|
|
161
|
+
> 「Lead 可见 + 内核投影背书成员关系」两步,与 WS 连接级的会话白名单不是
|
|
162
|
+
> 一回事;硬塞进 WS 只会让两套授权语义打架。详见 `docs/project/API.md`。
|
|
163
|
+
|
|
157
164
|
Rules:
|
|
158
165
|
|
|
159
166
|
- Absent or empty string ⇒ **no** `requestId` key in the response at all
|
package/docs/api/openapi.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
openapi: 3.1.0
|
|
2
2
|
info:
|
|
3
3
|
title: DeepSeek Harness Mobile API
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.9.0
|
|
5
5
|
description: Versioned file and chat BFF for mobile and third-party DSH clients. HTTP and HTTPS are both supported.
|
|
6
6
|
servers:
|
|
7
7
|
- url: http://127.0.0.1:3090/api/v1
|
|
@@ -24,7 +24,7 @@ paths:
|
|
|
24
24
|
properties:
|
|
25
25
|
ok: { type: boolean, const: true }
|
|
26
26
|
version: { type: string, const: v1 }
|
|
27
|
-
pluginVersion: { type: string, const: 2.
|
|
27
|
+
pluginVersion: { type: string, const: 2.9.0 }
|
|
28
28
|
/debug/host-services:
|
|
29
29
|
get:
|
|
30
30
|
operationId: hostServices
|
|
@@ -989,6 +989,80 @@ paths:
|
|
|
989
989
|
responses:
|
|
990
990
|
'202': { description: Prompt accepted by DSH. }
|
|
991
991
|
'409': { $ref: '#/components/responses/Error' }
|
|
992
|
+
/chat/sessions/{leadSessionId}/subagents/{subagentId}/messages:
|
|
993
|
+
parameters:
|
|
994
|
+
- name: leadSessionId
|
|
995
|
+
in: path
|
|
996
|
+
required: true
|
|
997
|
+
description: |
|
|
998
|
+
The session that owns the team (the Lead). Its visibility is checked with the
|
|
999
|
+
ordinary session rules -- the narrow endpoint never relaxes them.
|
|
1000
|
+
schema: { type: string }
|
|
1001
|
+
- name: subagentId
|
|
1002
|
+
in: path
|
|
1003
|
+
required: true
|
|
1004
|
+
description: |
|
|
1005
|
+
A subagent / teammate session id. It is *not* required to be listed by
|
|
1006
|
+
`GET /chat/sessions` (subagent sessions are filtered out of that list);
|
|
1007
|
+
membership is verified against the Lead's own `subagentCatalog` /
|
|
1008
|
+
`agentTeam.members` projection instead.
|
|
1009
|
+
schema: { type: string }
|
|
1010
|
+
get:
|
|
1011
|
+
operationId: listSubagentMessages
|
|
1012
|
+
description: |
|
|
1013
|
+
Read the history of a subagent (teammate) session -- a read-only narrow endpoint.
|
|
1014
|
+
|
|
1015
|
+
Why it exists: `GET /chat/sessions/{sessionId}/messages` resolves visibility via
|
|
1016
|
+
the session list, and subagent sessions are deliberately filtered out of that list
|
|
1017
|
+
(the kernel refuses to activate them as ordinary sessions, so exposing them yields
|
|
1018
|
+
rows that crash on open). Every subagent id therefore 404s on the ordinary path.
|
|
1019
|
+
|
|
1020
|
+
Authorization has two steps, both required: (1) the Lead session must be visible to
|
|
1021
|
+
this device under the unchanged rules; (2) membership must be backed by the kernel's
|
|
1022
|
+
own projection for that Lead (`subagentCatalog[].id` or `agentTeam.members[].id`).
|
|
1023
|
+
Guessing a session id does not work -- a miss is a 404.
|
|
1024
|
+
|
|
1025
|
+
Read-only: any non-GET method is `405 METHOD_NOT_ALLOWED`. Writing to a teammate
|
|
1026
|
+
session would blur who is directing whom; the official Web UI likewise only writes
|
|
1027
|
+
through the Lead's model tools.
|
|
1028
|
+
parameters:
|
|
1029
|
+
- name: beforeSeq
|
|
1030
|
+
in: query
|
|
1031
|
+
schema: { type: integer, minimum: 0 }
|
|
1032
|
+
- name: maxMessages
|
|
1033
|
+
in: query
|
|
1034
|
+
schema: { type: integer, minimum: 1 }
|
|
1035
|
+
responses:
|
|
1036
|
+
'200':
|
|
1037
|
+
description: |
|
|
1038
|
+
Identical shape and byte budget as `listMessages`: the oldest events may be
|
|
1039
|
+
dropped (with `hasMore` forced to true) and oversized strings inside one event
|
|
1040
|
+
may be head/tail clamped. `truncated`/`bytes` appear only when something was
|
|
1041
|
+
actually clamped. `historyFormatVersion` is always present.
|
|
1042
|
+
content:
|
|
1043
|
+
application/json:
|
|
1044
|
+
schema:
|
|
1045
|
+
type: object
|
|
1046
|
+
required: [events, hasMore]
|
|
1047
|
+
properties:
|
|
1048
|
+
events:
|
|
1049
|
+
type: array
|
|
1050
|
+
items: { type: object, additionalProperties: true }
|
|
1051
|
+
hasMore: { type: boolean }
|
|
1052
|
+
projections: { type: object, additionalProperties: true }
|
|
1053
|
+
bytes: { type: integer, minimum: 0 }
|
|
1054
|
+
truncated: { type: boolean }
|
|
1055
|
+
historyFormatVersion: { type: integer, minimum: 1 }
|
|
1056
|
+
'404':
|
|
1057
|
+
description: |
|
|
1058
|
+
`SUBAGENT_NOT_FOUND` -- either the subagent is not a member of this team, or
|
|
1059
|
+
the Lead's projections were unavailable (membership unprovable). The two cases
|
|
1060
|
+
are deliberately indistinguishable so the existence of a session id is not
|
|
1061
|
+
turned into a probing surface.
|
|
1062
|
+
content:
|
|
1063
|
+
application/json:
|
|
1064
|
+
schema: { $ref: '#/components/schemas/Error' }
|
|
1065
|
+
'405': { $ref: '#/components/responses/Error' }
|
|
992
1066
|
/chat/sessions/{sessionId}/attachments:
|
|
993
1067
|
parameters:
|
|
994
1068
|
- name: sessionId
|
|
@@ -541,6 +541,27 @@ class DshWorkspaceClient(
|
|
|
541
541
|
|
|
542
542
|
fun listMessages(sessionId: String): JsonElement = executeJsonElement("chat/sessions/$sessionId/messages", "GET", null)
|
|
543
543
|
|
|
544
|
+
/**
|
|
545
|
+
* 读**子会话(团队成员)**的历史 —— 受成员关系校验的只读窄口。
|
|
546
|
+
*
|
|
547
|
+
* 不能复用 [listMessages]:那条路径靠会话列表判定可见性,而子会话被
|
|
548
|
+
* `sessions.list` 刻意滤掉(内核拒绝把它们当普通会话激活),所以任何子会话 id
|
|
549
|
+
* 打进去都是 404。这条窄口改成两步授权:Lead 会话必须可见 + 内核投影
|
|
550
|
+
* (`subagentCatalog` / `agentTeam.members`)必须背书成员关系。只读。
|
|
551
|
+
*/
|
|
552
|
+
fun listSubagentMessages(
|
|
553
|
+
leadSessionId: String,
|
|
554
|
+
subagentId: String,
|
|
555
|
+
beforeSeq: Int? = null,
|
|
556
|
+
maxMessages: Int? = null,
|
|
557
|
+
): JsonElement {
|
|
558
|
+
val query = buildString {
|
|
559
|
+
if (beforeSeq != null) append(if (isEmpty()) "?" else "&").append("beforeSeq=").append(beforeSeq)
|
|
560
|
+
if (maxMessages != null) append(if (isEmpty()) "?" else "&").append("maxMessages=").append(maxMessages)
|
|
561
|
+
}
|
|
562
|
+
return executeJsonElement("chat/sessions/$leadSessionId/subagents/$subagentId/messages$query", "GET", null)
|
|
563
|
+
}
|
|
564
|
+
|
|
544
565
|
fun renameSession(sessionId: String, title: String) {
|
|
545
566
|
val body = json.encodeToString(RenameRequest.serializer(), RenameRequest(title))
|
|
546
567
|
executeUnit("chat/sessions/$sessionId", "PATCH", body)
|
package/lib/index.d.ts
CHANGED
|
@@ -785,6 +785,23 @@ interface AttachmentStoreLike {
|
|
|
785
785
|
data: Uint8Array;
|
|
786
786
|
}>;
|
|
787
787
|
}
|
|
788
|
+
/**
|
|
789
|
+
* 内核的 `SessionAddress` 判别联合(`dsh-api-session-controller` 的
|
|
790
|
+
* types.d.ts:406-414)。普通会话用 `kind:'session'`,**子会话必须**用
|
|
791
|
+
* `kind:'subagent'` 并带上父会话 id —— 内核在 `validateAddress()` 里对
|
|
792
|
+
* `{kind:'session'}` 且 header.origin==='subagent' 直接抛
|
|
793
|
+
* `session/agent-busy: "subagent Sessions require their durable parent address"`。
|
|
794
|
+
* 也就是说:子会话历史一直是可读的,只是必须按内核要的地址形状去问。
|
|
795
|
+
*/
|
|
796
|
+
type SessionAddressLike = {
|
|
797
|
+
kind: 'session';
|
|
798
|
+
sessionId: string;
|
|
799
|
+
} | {
|
|
800
|
+
kind: 'subagent';
|
|
801
|
+
parentSessionId: string;
|
|
802
|
+
childSessionId: string;
|
|
803
|
+
mode: 'one-shot' | 'continuable' | 'unknown';
|
|
804
|
+
};
|
|
788
805
|
interface SessionControllerLike {
|
|
789
806
|
list(request: {
|
|
790
807
|
cursor?: string;
|
|
@@ -808,10 +825,7 @@ interface SessionControllerLike {
|
|
|
808
825
|
* a well-formed but *empty* page — a silent failure, not an error.
|
|
809
826
|
*/
|
|
810
827
|
follow(request: {
|
|
811
|
-
address:
|
|
812
|
-
kind: 'session';
|
|
813
|
-
sessionId: string;
|
|
814
|
-
};
|
|
828
|
+
address: SessionAddressLike;
|
|
815
829
|
maxMessages?: number;
|
|
816
830
|
/**
|
|
817
831
|
* Assistant presentation frames are opt-in: without `assistantStream: true`
|
|
@@ -860,10 +874,7 @@ interface SessionControllerLike {
|
|
|
860
874
|
* `beforeSeq` is optional and acts as an earlier boundary.
|
|
861
875
|
*/
|
|
862
876
|
page(request: {
|
|
863
|
-
address:
|
|
864
|
-
kind: 'session';
|
|
865
|
-
sessionId: string;
|
|
866
|
-
};
|
|
877
|
+
address: SessionAddressLike;
|
|
867
878
|
throughSeq: number;
|
|
868
879
|
beforeSeq?: number;
|
|
869
880
|
maxMessages?: number;
|
|
@@ -1286,12 +1297,20 @@ interface DshApiProxy {
|
|
|
1286
1297
|
sessionId: string;
|
|
1287
1298
|
agentPreset?: string;
|
|
1288
1299
|
}>>;
|
|
1300
|
+
/**
|
|
1301
|
+
* 读历史。`parentSessionId` + `subagentMode` 是本桥的**扩展字段**(内核契约里
|
|
1302
|
+
* 没有):带上它们表示「这是 parentSessionId 名下的子会话」,底层据此把地址
|
|
1303
|
+
* 拼成 `kind:'subagent'`(内核只认这个形状,拿 childSessionId 冒充普通
|
|
1304
|
+
* sessionId 会被 validateAddress() 拒掉)。不带就是普通会话,行为一字不变。
|
|
1305
|
+
*/
|
|
1289
1306
|
history(request: {
|
|
1290
1307
|
rpcId: string;
|
|
1291
1308
|
payload: {
|
|
1292
1309
|
sessionId: string;
|
|
1293
1310
|
beforeSeq?: number;
|
|
1294
1311
|
maxMessages?: number;
|
|
1312
|
+
parentSessionId?: string;
|
|
1313
|
+
subagentMode?: 'one-shot' | 'continuable' | 'unknown';
|
|
1295
1314
|
};
|
|
1296
1315
|
}): Promise<RpcResponse<{
|
|
1297
1316
|
events: unknown[];
|
|
@@ -1928,6 +1947,51 @@ declare class DshChatAdapter {
|
|
|
1928
1947
|
truncated?: boolean;
|
|
1929
1948
|
historyFormatVersion?: number;
|
|
1930
1949
|
}>;
|
|
1950
|
+
/**
|
|
1951
|
+
* 读**子会话**(subagent / 团队成员)的历史 —— 受成员关系校验的只读窄口。
|
|
1952
|
+
*
|
|
1953
|
+
* ## 为什么需要一条单独的路径
|
|
1954
|
+
*
|
|
1955
|
+
* 内核 0.2.x 把子会话也列在 `sessions.list` 里、并标 `origin: 'subagent'`,
|
|
1956
|
+
* 而桥在 `dsh-0.2-bridge.ts` 的 `sessions.list` 把它们**整批滤掉**(内核拒绝
|
|
1957
|
+
* 把子会话当普通会话激活,暴露出来只会得到「点开就崩」的行)。`requireSession()`
|
|
1958
|
+
* 的可见性判定正是建立在 `listSessions()` 上的,于是任何 `{subagentId}` 打进来
|
|
1959
|
+
* 都是 404 —— 手机端「团队面板里点成员/子会话看内容」因此根本走不通。
|
|
1960
|
+
*
|
|
1961
|
+
* 三条路可选:拆掉 `origin` 过滤(把「点开就崩」的行放回列表,回归风险最大)、
|
|
1962
|
+
* 放宽 `requireSession`(等于给所有会话开一条后门,最危险)、或者**单开一条
|
|
1963
|
+
* 自带授权规则的窄口**。本实现选第三条:授权判定不依赖会话列表,改用**内核自己
|
|
1964
|
+
* 发布的投影**来证明成员关系。
|
|
1965
|
+
*
|
|
1966
|
+
* ## 授权是怎么成立的(两步,缺一不可)
|
|
1967
|
+
*
|
|
1968
|
+
* ① **Lead 必须可见**:`requireSession(leadSessionId)` 走的是完全没变的既有规则
|
|
1969
|
+
* (设备 scope + 授权根 + 归档状态)。子会话能读,前提是它的 Lead 本来就能读。
|
|
1970
|
+
* ② **成员关系必须由内核背书**:读 Lead 的会话投影,要求 `subagentId` 出现在
|
|
1971
|
+
* `subagentCatalog[].id`(或 `agentTeam.members[].id`,两者在内核里指向同一批
|
|
1972
|
+
* Session)里。这份目录是内核为这个 Lead 亲手发布的,不是我们维护的白名单,
|
|
1973
|
+
* 所以「猜一个 sessionId 来读别人会话」在这里不成立 —— 猜不中就是 404。
|
|
1974
|
+
*
|
|
1975
|
+
* 通过这两步之后才直读历史。注意 ③ **刻意不再调 `requireSession`**:那正是会
|
|
1976
|
+
* 必然 404 的那一步,授权已在 ①② 完成,③ 纯粹是取数据。
|
|
1977
|
+
*
|
|
1978
|
+
* ## 为什么只读
|
|
1979
|
+
*
|
|
1980
|
+
* 写子会话(给成员发消息、改它的模型/权限)会让「谁在指挥谁」变得含混:官方 Web 端
|
|
1981
|
+
* 同样不允许人类直接写成员会话,一切经由 Lead 的模型工具。这条窄口只开放 `events`
|
|
1982
|
+
* 读取,`POST` 一律 405。
|
|
1983
|
+
*/
|
|
1984
|
+
subagentHistory(principal: Principal, leadSessionId: string, subagentId: string, options: {
|
|
1985
|
+
beforeSeq?: number;
|
|
1986
|
+
maxMessages?: number;
|
|
1987
|
+
}): Promise<{
|
|
1988
|
+
events: unknown[];
|
|
1989
|
+
hasMore: boolean;
|
|
1990
|
+
projections?: unknown;
|
|
1991
|
+
bytes?: number;
|
|
1992
|
+
truncated?: boolean;
|
|
1993
|
+
historyFormatVersion?: number;
|
|
1994
|
+
}>;
|
|
1931
1995
|
prompt(principal: Principal, sessionId: string, parts: PromptContentPart[], mode: 'queue' | 'steer', clientTimeZone?: string, locale?: HandoffLocale): Promise<{
|
|
1932
1996
|
accepted: true;
|
|
1933
1997
|
command?: unknown;
|
package/lib/index.js
CHANGED
|
@@ -2998,22 +2998,30 @@ function createDsh02ApiProxy(host, options = {}) {
|
|
|
2998
2998
|
create: (request) => call("sessions.create", (signal) => host.sessionController.create(request.payload)),
|
|
2999
2999
|
history: (request) => call("sessions.history", async (signal) => {
|
|
3000
3000
|
const { sessionId, maxMessages, beforeSeq } = request.payload;
|
|
3001
|
+
const parentSessionId = request.payload.parentSessionId;
|
|
3002
|
+
const subagentMode = request.payload.subagentMode;
|
|
3003
|
+
const address = typeof parentSessionId === "string" && parentSessionId !== "" ? {
|
|
3004
|
+
kind: "subagent",
|
|
3005
|
+
parentSessionId,
|
|
3006
|
+
childSessionId: sessionId,
|
|
3007
|
+
mode: subagentMode === "one-shot" || subagentMode === "continuable" ? subagentMode : "unknown"
|
|
3008
|
+
} : {
|
|
3009
|
+
kind: "session",
|
|
3010
|
+
sessionId
|
|
3011
|
+
};
|
|
3001
3012
|
const controller = new AbortController();
|
|
3002
3013
|
if (signal.aborted) controller.abort();
|
|
3003
3014
|
else signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
3004
3015
|
try {
|
|
3005
3016
|
const snapshot = (await host.sessionController.follow({
|
|
3006
|
-
address
|
|
3007
|
-
kind: "session",
|
|
3008
|
-
sessionId
|
|
3009
|
-
},
|
|
3017
|
+
address,
|
|
3010
3018
|
...maxMessages === void 0 ? {} : { maxMessages }
|
|
3011
3019
|
}, controller.signal)[Symbol.asyncIterator]().next()).value;
|
|
3012
3020
|
if (snapshot === void 0 || snapshot.type !== "snapshot") return {
|
|
3013
3021
|
events: [],
|
|
3014
3022
|
hasMore: false
|
|
3015
3023
|
};
|
|
3016
|
-
const projections = projectionsOf(snapshot) ?? await readSessionProjections(host, sessionId, controller.signal);
|
|
3024
|
+
const projections = projectionsOf(snapshot) ?? (address.kind === "session" ? await readSessionProjections(host, sessionId, controller.signal) : void 0);
|
|
3017
3025
|
const withProjections = (payload) => projections === void 0 ? payload : {
|
|
3018
3026
|
...payload,
|
|
3019
3027
|
projections
|
|
@@ -3023,10 +3031,7 @@ function createDsh02ApiProxy(host, options = {}) {
|
|
|
3023
3031
|
hasMore: snapshot.hasMore === true
|
|
3024
3032
|
});
|
|
3025
3033
|
const page = await host.sessionController.page({
|
|
3026
|
-
address
|
|
3027
|
-
kind: "session",
|
|
3028
|
-
sessionId
|
|
3029
|
-
},
|
|
3034
|
+
address,
|
|
3030
3035
|
throughSeq: snapshot.cursor,
|
|
3031
3036
|
beforeSeq,
|
|
3032
3037
|
...maxMessages === void 0 ? {} : { maxMessages }
|
|
@@ -3294,7 +3299,7 @@ function createDsh02ApiProxy(host, options = {}) {
|
|
|
3294
3299
|
}
|
|
3295
3300
|
//#endregion
|
|
3296
3301
|
//#region src/shared/version.ts
|
|
3297
|
-
const PLUGIN_VERSION = "2.
|
|
3302
|
+
const PLUGIN_VERSION = "2.9.0";
|
|
3298
3303
|
//#endregion
|
|
3299
3304
|
//#region src/host/capabilities.ts
|
|
3300
3305
|
/**
|
|
@@ -3358,6 +3363,7 @@ const BRIDGE_CAPABILITIES = [
|
|
|
3358
3363
|
"session-archive-v1",
|
|
3359
3364
|
"session-archive-list-v1",
|
|
3360
3365
|
"history-v1",
|
|
3366
|
+
"subagent-history-v1",
|
|
3361
3367
|
"turn-cancel-v1",
|
|
3362
3368
|
"session-config-v1",
|
|
3363
3369
|
"commands-v1",
|
|
@@ -3911,6 +3917,85 @@ var DshChatAdapter = class {
|
|
|
3911
3917
|
historyFormatVersion: 1
|
|
3912
3918
|
};
|
|
3913
3919
|
}
|
|
3920
|
+
/**
|
|
3921
|
+
* 读**子会话**(subagent / 团队成员)的历史 —— 受成员关系校验的只读窄口。
|
|
3922
|
+
*
|
|
3923
|
+
* ## 为什么需要一条单独的路径
|
|
3924
|
+
*
|
|
3925
|
+
* 内核 0.2.x 把子会话也列在 `sessions.list` 里、并标 `origin: 'subagent'`,
|
|
3926
|
+
* 而桥在 `dsh-0.2-bridge.ts` 的 `sessions.list` 把它们**整批滤掉**(内核拒绝
|
|
3927
|
+
* 把子会话当普通会话激活,暴露出来只会得到「点开就崩」的行)。`requireSession()`
|
|
3928
|
+
* 的可见性判定正是建立在 `listSessions()` 上的,于是任何 `{subagentId}` 打进来
|
|
3929
|
+
* 都是 404 —— 手机端「团队面板里点成员/子会话看内容」因此根本走不通。
|
|
3930
|
+
*
|
|
3931
|
+
* 三条路可选:拆掉 `origin` 过滤(把「点开就崩」的行放回列表,回归风险最大)、
|
|
3932
|
+
* 放宽 `requireSession`(等于给所有会话开一条后门,最危险)、或者**单开一条
|
|
3933
|
+
* 自带授权规则的窄口**。本实现选第三条:授权判定不依赖会话列表,改用**内核自己
|
|
3934
|
+
* 发布的投影**来证明成员关系。
|
|
3935
|
+
*
|
|
3936
|
+
* ## 授权是怎么成立的(两步,缺一不可)
|
|
3937
|
+
*
|
|
3938
|
+
* ① **Lead 必须可见**:`requireSession(leadSessionId)` 走的是完全没变的既有规则
|
|
3939
|
+
* (设备 scope + 授权根 + 归档状态)。子会话能读,前提是它的 Lead 本来就能读。
|
|
3940
|
+
* ② **成员关系必须由内核背书**:读 Lead 的会话投影,要求 `subagentId` 出现在
|
|
3941
|
+
* `subagentCatalog[].id`(或 `agentTeam.members[].id`,两者在内核里指向同一批
|
|
3942
|
+
* Session)里。这份目录是内核为这个 Lead 亲手发布的,不是我们维护的白名单,
|
|
3943
|
+
* 所以「猜一个 sessionId 来读别人会话」在这里不成立 —— 猜不中就是 404。
|
|
3944
|
+
*
|
|
3945
|
+
* 通过这两步之后才直读历史。注意 ③ **刻意不再调 `requireSession`**:那正是会
|
|
3946
|
+
* 必然 404 的那一步,授权已在 ①② 完成,③ 纯粹是取数据。
|
|
3947
|
+
*
|
|
3948
|
+
* ## 为什么只读
|
|
3949
|
+
*
|
|
3950
|
+
* 写子会话(给成员发消息、改它的模型/权限)会让「谁在指挥谁」变得含混:官方 Web 端
|
|
3951
|
+
* 同样不允许人类直接写成员会话,一切经由 Lead 的模型工具。这条窄口只开放 `events`
|
|
3952
|
+
* 读取,`POST` 一律 405。
|
|
3953
|
+
*/
|
|
3954
|
+
async subagentHistory(principal, leadSessionId, subagentId, options) {
|
|
3955
|
+
await this.requireSession(principal, leadSessionId);
|
|
3956
|
+
const baseline = unwrapDsh(await this.api.sessions.projections({
|
|
3957
|
+
rpcId: randomUUID(),
|
|
3958
|
+
payload: { sessionId: leadSessionId }
|
|
3959
|
+
}));
|
|
3960
|
+
if (baseline === null || baseline === void 0) throw new ApiError(404, "SUBAGENT_NOT_FOUND", "Session projections are unavailable, cannot verify membership.");
|
|
3961
|
+
const values = baseline.values ?? {};
|
|
3962
|
+
const members = /* @__PURE__ */ new Set();
|
|
3963
|
+
const modes = /* @__PURE__ */ new Map();
|
|
3964
|
+
const catalog = values["subagentCatalog"];
|
|
3965
|
+
if (Array.isArray(catalog)) for (const entry of catalog) {
|
|
3966
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
3967
|
+
const record = entry;
|
|
3968
|
+
const id = typeof record["id"] === "string" ? record["id"] : record["sessionId"];
|
|
3969
|
+
if (typeof id !== "string" || id === "") continue;
|
|
3970
|
+
members.add(id);
|
|
3971
|
+
const mode = record["mode"];
|
|
3972
|
+
if (mode === "one-shot" || mode === "continuable" || mode === "unknown") modes.set(id, mode);
|
|
3973
|
+
}
|
|
3974
|
+
const team = values["agentTeam"];
|
|
3975
|
+
if (typeof team === "object" && team !== null) {
|
|
3976
|
+
const teamMembers = team["members"];
|
|
3977
|
+
if (Array.isArray(teamMembers)) for (const entry of teamMembers) {
|
|
3978
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
3979
|
+
const id = entry["id"];
|
|
3980
|
+
if (typeof id === "string" && id !== "") members.add(id);
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
if (!members.has(subagentId)) throw new ApiError(404, "SUBAGENT_NOT_FOUND", "The session is not a member of this team.");
|
|
3984
|
+
const payload = {
|
|
3985
|
+
sessionId: subagentId,
|
|
3986
|
+
parentSessionId: leadSessionId,
|
|
3987
|
+
subagentMode: modes.get(subagentId) ?? "unknown",
|
|
3988
|
+
...options.beforeSeq === void 0 ? {} : { beforeSeq: options.beforeSeq },
|
|
3989
|
+
...options.maxMessages === void 0 ? {} : { maxMessages: options.maxMessages }
|
|
3990
|
+
};
|
|
3991
|
+
return {
|
|
3992
|
+
...applyHistoryBudget(unwrapDsh(await this.api.sessions.history({
|
|
3993
|
+
rpcId: randomUUID(),
|
|
3994
|
+
payload
|
|
3995
|
+
}))),
|
|
3996
|
+
historyFormatVersion: 1
|
|
3997
|
+
};
|
|
3998
|
+
}
|
|
3914
3999
|
async prompt(principal, sessionId, parts, mode, clientTimeZone, locale) {
|
|
3915
4000
|
await this.requireSession(principal, sessionId);
|
|
3916
4001
|
const hasText = parts.some((part) => part.type === "text" && part.text.trim() !== "");
|
|
@@ -4267,7 +4352,7 @@ var DshChatAdapter = class {
|
|
|
4267
4352
|
const key = approvalKey(payload.sessionId, payload.approvalId);
|
|
4268
4353
|
const previous = this.pendingApprovals.get(key);
|
|
4269
4354
|
this.pendingApprovals.set(key, {
|
|
4270
|
-
rpcId: frame.rpcId,
|
|
4355
|
+
rpcId: typeof payload.rpcId === "string" && payload.rpcId.length > 0 ? payload.rpcId : frame.rpcId,
|
|
4271
4356
|
sessionId: payload.sessionId,
|
|
4272
4357
|
approvalId: payload.approvalId,
|
|
4273
4358
|
toolName: redactApprovalText(payload.toolName, 120),
|
|
@@ -9991,6 +10076,22 @@ var ApiRouter = class {
|
|
|
9991
10076
|
return;
|
|
9992
10077
|
}
|
|
9993
10078
|
}
|
|
10079
|
+
const subagentHistoryRoute = pathname.match(/^\/api\/v1\/chat\/sessions\/([^/]+)\/subagents\/([^/]+)\/messages$/);
|
|
10080
|
+
if (subagentHistoryRoute !== null) {
|
|
10081
|
+
const leadSessionId = decodePathSegment(subagentHistoryRoute[1]);
|
|
10082
|
+
const subagentId = decodePathSegment(subagentHistoryRoute[2]);
|
|
10083
|
+
if (method === "GET") {
|
|
10084
|
+
this.options.auth.requireScope(principal, "chat.read");
|
|
10085
|
+
const beforeSeq = optionalInteger(url.searchParams.get("beforeSeq"));
|
|
10086
|
+
const maxMessages = optionalInteger(url.searchParams.get("maxMessages"));
|
|
10087
|
+
sendJson(res, 200, await this.options.chat.subagentHistory(principal, leadSessionId, subagentId, {
|
|
10088
|
+
...beforeSeq === void 0 ? {} : { beforeSeq },
|
|
10089
|
+
...maxMessages === void 0 ? {} : { maxMessages }
|
|
10090
|
+
}));
|
|
10091
|
+
return;
|
|
10092
|
+
}
|
|
10093
|
+
throw new ApiError(405, "METHOD_NOT_ALLOWED", "Subagent history is read-only.");
|
|
10094
|
+
}
|
|
9994
10095
|
const attachmentRoute = pathname.match(/^\/api\/v1\/attachments\/([^/]+)$/);
|
|
9995
10096
|
if (attachmentRoute !== null && method === "GET") {
|
|
9996
10097
|
this.options.auth.requireScope(principal, "files.read");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-bridge",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Versioned REST bridge for DeepSeek Harness: remote access for trusted clients (chat, files, devices) plus a Phone-settings panel inside the DSH WebUI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.19.0",
|