dsh-remote-bridge 2.8.2 → 2.9.1
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 +39 -32
- package/README.md +59 -29
- package/assets/screenshots/panel-app-download.png +0 -0
- package/assets/screenshots/panel-devices.png +0 -0
- package/assets/screenshots/panel-plugins.png +0 -0
- package/assets/screenshots/panel-remote.png +0 -0
- package/assets/screenshots/panel-roots.png +0 -0
- package/assets/screenshots/panel-tunnel.png +0 -0
- 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/client.cjs +1972 -1595
- package/lib/index.d.ts +72 -8
- package/lib/index.js +111 -10
- package/package.json +1 -1
- package/screenshots.json +2 -0
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.1";
|
|
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() !== "");
|
|
@@ -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.1",
|
|
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",
|
package/screenshots.json
CHANGED