dsh-remote-bridge 2.2.3 → 2.3.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 +27 -10
- package/README.md +32 -18
- package/docs/api/asyncapi.yaml +1 -1
- package/docs/api/openapi.yaml +2 -2
- package/kotlin-sdk/build.gradle.kts +1 -1
- package/lib/client.cjs +960 -280
- package/lib/index.d.ts +1078 -940
- package/lib/index.js +511 -57
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -6,6 +6,17 @@ interface RootView {
|
|
|
6
6
|
id: string;
|
|
7
7
|
label: string;
|
|
8
8
|
createdAt: number;
|
|
9
|
+
/**
|
|
10
|
+
* 这是不是插件自动登记的「DSH 工作区」根。
|
|
11
|
+
*
|
|
12
|
+
* 用户要求:默认把当前 DSH 的工作目录作为一个授权根显示出来,**不可删除**,
|
|
13
|
+
* 但可以正常访问(手机端照常浏览)。标记只影响**删除**这一件事 ——
|
|
14
|
+
* 它在数据库里就是一条普通的根,`files.*` 授权、配对、写盘全走同一套。
|
|
15
|
+
*
|
|
16
|
+
* 不写成 `readonly`,是为了让 REST 契约保持「服务端给什么就是什么」:
|
|
17
|
+
* 老客户端不认识这个字段会忽略它,行为不变。
|
|
18
|
+
*/
|
|
19
|
+
builtin?: boolean;
|
|
9
20
|
}
|
|
10
21
|
interface DeviceView {
|
|
11
22
|
id: string;
|
|
@@ -69,11 +80,49 @@ declare class WorkspaceDatabase {
|
|
|
69
80
|
private readonly db;
|
|
70
81
|
constructor(dataDir: string);
|
|
71
82
|
close(): void;
|
|
83
|
+
/**
|
|
84
|
+
* 导出 `roots` 的建表语句(只读,诊断与迁移测试用)。
|
|
85
|
+
*
|
|
86
|
+
* 存在的理由:迁移要判断「UNIQUE 还在不在」,单看 `PRAGMA table_info` 看不出来 ——
|
|
87
|
+
* 表结构读出来一模一样,区别在约束上。这个方法让「重建真的发生了」可以被断言,
|
|
88
|
+
* 而不是靠迁移没报错来推测。
|
|
89
|
+
*/
|
|
90
|
+
dumpSchema(): string;
|
|
72
91
|
getSetting(key: string): string | undefined;
|
|
73
92
|
setSetting(key: string, value: string): void;
|
|
74
|
-
addRoot(inputPath: string, label?: string
|
|
93
|
+
addRoot(inputPath: string, label?: string, options?: {
|
|
94
|
+
allowDuplicate?: boolean;
|
|
95
|
+
}): Promise<StoredRoot>;
|
|
75
96
|
listRoots(): StoredRoot[];
|
|
76
97
|
getRoot(id: string): StoredRoot | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* 登记「DSH 工作区」根 —— **幂等**。
|
|
100
|
+
*
|
|
101
|
+
* 用户要求:当前 DSH 的工作目录要显示成一个授权根,**不可删除、但可以正常访问**。
|
|
102
|
+
* 它不是另起一张表,而是**一条普通的根**加一个 `builtin` 标记:配对、`files.*`
|
|
103
|
+
* 授权、回收站、写盘全走现成逻辑,少一套需要同步维护的规则。
|
|
104
|
+
*
|
|
105
|
+
* ## 同目录已有一条普通根时,另起一条而不是复用它
|
|
106
|
+
*
|
|
107
|
+
* 这是实测踩到的现实情况:用户早于这个功能**自己**加过同一条目录(本机那条是
|
|
108
|
+
* 10-08 手工加的),路径和 DSH 工作区一模一样。
|
|
109
|
+
*
|
|
110
|
+
* 初版在这里「尊重用户选择」直接返回,结果就是**内置项永远登记不上** —— 因为它
|
|
111
|
+
* 每次都撞上那条已有的普通根然后什么都不做。界面上那个「不可删除」也就永远出不来。
|
|
112
|
+
* 用户的选择和功能的正确性在这里是冲突的:用户要的是「这条要不可删」,不是
|
|
113
|
+
* 「这条不能变」。
|
|
114
|
+
*
|
|
115
|
+
* 所以改成**另起一条**:用户原来那条保持可删(那是他的目录,他有权删),内置那条
|
|
116
|
+
* 独立存在。代价是列表里同一个路径出现两行 —— 这是「两条记录、两种权限」的必然
|
|
117
|
+
* 结果,比让功能失效或擅自改他的配置都更可接受。
|
|
118
|
+
*
|
|
119
|
+
* 幂等不再靠 UNIQUE(它已经被去掉了),而是靠「已存在 `builtin = 1` 的同路径就
|
|
120
|
+
* 直接返回」。插件**每次启动都可以无脑调一次**。
|
|
121
|
+
*
|
|
122
|
+
* 只有传入路径确实存在才登记:工作区可能被用户删掉,这时留一条指向虚空的授权根,
|
|
123
|
+
* 手机浏览时会一路报错,比不显示更糟。
|
|
124
|
+
*/
|
|
125
|
+
ensureBuiltinRoot(realPath: string, label: string): Promise<StoredRoot | undefined>;
|
|
77
126
|
removeRoot(id: string): void;
|
|
78
127
|
createPairing(rootIds: string[], scopes: DeviceScope[], ttlMs?: number): {
|
|
79
128
|
code: string;
|
|
@@ -89,7 +138,39 @@ declare class WorkspaceDatabase {
|
|
|
89
138
|
listDevices(): DeviceView[];
|
|
90
139
|
isDeviceActive(id: string): boolean;
|
|
91
140
|
updateDevice(id: string, scopes: DeviceScope[], rootIds: string[]): DeviceView;
|
|
141
|
+
/**
|
|
142
|
+
* 撤销设备 = **直接删除这一行**(用户要求:已撤销的不显示,并自动从数据库中删除)。
|
|
143
|
+
*
|
|
144
|
+
* 原先是打 `revoked_at` 标记保留行,两处都不好:
|
|
145
|
+
* - 界面上会出现一长串灰掉的「已撤销」,本机实测堆到了 32 条,全是历史测试与旧设备;
|
|
146
|
+
* - 行留着就等于凭据的哈希还在库里,是个长期负担 —— 令牌已经没用了,留着没有意义。
|
|
147
|
+
*
|
|
148
|
+
* 删除是安全的,因为:
|
|
149
|
+
* - `device_roots` 是 `ON DELETE CASCADE`,授权行自动清掉;
|
|
150
|
+
* - `audit.actor_id` 是**普通字符串不是外键**,审计留痕(谁在什么时候撤了谁)一条不少;
|
|
151
|
+
* - `authenticate` 仍保留 `revoked_at IS NULL` 作为兜底,万一有别处留下的标记行,
|
|
152
|
+
* 它们依然登不进来 —— 两层保险不冲突。
|
|
153
|
+
*/
|
|
92
154
|
revokeDevice(id: string): void;
|
|
155
|
+
/**
|
|
156
|
+
* 一次性清理历史遗留的「标记式」撤销设备(`revoked_at IS NOT NULL` 的行)。
|
|
157
|
+
*
|
|
158
|
+
* 撤销改成直接删除之前,每次撤销都只是打个标记,于是库越用越臃肿 —— 本机实测堆到
|
|
159
|
+
* **32 条**,全是历史测试探针和早已弃用的旧手机,界面上就是一长串灰字。
|
|
160
|
+
*
|
|
161
|
+
* 幂等:没有这种行时返回 0,不报错。插件每次启动调一次即可。
|
|
162
|
+
* 走 `revokeDevice` 的删除语义,但**不能**借它写 `device.revoke` 审计 ——
|
|
163
|
+
* 那会把「系统清理」记成管理员逐个撤销,审计会失真。清理本身只写一条汇总。
|
|
164
|
+
*/
|
|
165
|
+
/**
|
|
166
|
+
* 仅供测试:造一行「撤销改成删除之前」的标记式撤销数据。
|
|
167
|
+
*
|
|
168
|
+
* 真实场景来自历史库(每次撤销只打 `revoked_at` 标记,于是越积越多)。测试需要个
|
|
169
|
+
* 干净的办法制造这种行,而它不该走生产代码路径 —— 所以显式开一个测试专用入口,
|
|
170
|
+
* 并起一个自见其明的名字,免得有人把它当成正常 API。
|
|
171
|
+
*/
|
|
172
|
+
markRevokedForTest(id: string): void;
|
|
173
|
+
purgeRevokedDevices(): number;
|
|
93
174
|
insertTrash(record: TrashRecord): void;
|
|
94
175
|
updateTrashStatus(id: string, status: string): void;
|
|
95
176
|
getTrash(id: string): TrashRecord | undefined;
|
|
@@ -103,1109 +184,1151 @@ declare class WorkspaceDatabase {
|
|
|
103
184
|
} | undefined;
|
|
104
185
|
saveIdempotent(deviceId: string, key: string, status: number, body: unknown): void;
|
|
105
186
|
private migrate;
|
|
187
|
+
/** `roots.real_path` 上是否还带 UNIQUE 约束。 */
|
|
188
|
+
private rootsPathIsUnique;
|
|
189
|
+
/**
|
|
190
|
+
* 重建 `roots` 去掉 UNIQUE。
|
|
191
|
+
*
|
|
192
|
+
* 这是一次**有真实数据风险**的操作,所以每一步都写清楚理由:
|
|
193
|
+
* - `PRAGMA foreign_keys=OFF`:`device_roots.root_id` 引用 `roots(id)`,而重建要
|
|
194
|
+
* DROP 掉被引用的那张表。开着外键会直接失败。
|
|
195
|
+
* - 事务:`new_roots` 建好、数据搬完、**最后**才 DROP 老表并改名。中途失败会整体
|
|
196
|
+
* 回滚,原表原封不动。
|
|
197
|
+
* - 搬数据用显式列名而不是 `SELECT *`:老表可能还没有 `builtin` 列(两种迁移顺序
|
|
198
|
+
* 都可能出现),按列名搬才不会在某一台机器上错位。
|
|
199
|
+
*/
|
|
200
|
+
private dropRootsUniqueConstraint;
|
|
201
|
+
/** `roots` 表是否已有指定列。 */
|
|
202
|
+
private hasRootColumn;
|
|
106
203
|
}
|
|
107
204
|
//#endregion
|
|
108
|
-
//#region src/host/
|
|
109
|
-
interface
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
code: string;
|
|
117
|
-
message: string;
|
|
118
|
-
details?: unknown;
|
|
119
|
-
};
|
|
120
|
-
};
|
|
205
|
+
//#region src/host/settings-adapter.d.ts
|
|
206
|
+
interface SettingsNamespaceView {
|
|
207
|
+
ns: string;
|
|
208
|
+
schema?: unknown;
|
|
209
|
+
value: unknown;
|
|
210
|
+
base?: unknown;
|
|
211
|
+
user?: unknown;
|
|
212
|
+
revision: number;
|
|
121
213
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
type: 'text';
|
|
134
|
-
text: string;
|
|
135
|
-
} | {
|
|
136
|
-
type: 'image';
|
|
137
|
-
mediaType: string;
|
|
138
|
-
data: string;
|
|
139
|
-
name?: string;
|
|
140
|
-
} | {
|
|
141
|
-
type: 'file';
|
|
142
|
-
receiptId: string;
|
|
143
|
-
};
|
|
144
|
-
interface DshApiProxy {
|
|
145
|
-
respond(message: {
|
|
146
|
-
type: 'client-response';
|
|
147
|
-
rpcId: string;
|
|
148
|
-
result: {
|
|
149
|
-
ok: true;
|
|
150
|
-
value: unknown;
|
|
151
|
-
};
|
|
152
|
-
}): Promise<{
|
|
153
|
-
accepted: true;
|
|
154
|
-
} | {
|
|
155
|
-
accepted: false;
|
|
156
|
-
reason: 'not-pending' | 'bad-response';
|
|
157
|
-
}>;
|
|
158
|
-
sessions: {
|
|
159
|
-
list(request: {
|
|
160
|
-
rpcId: string;
|
|
161
|
-
payload: {
|
|
162
|
-
cursor?: string;
|
|
163
|
-
};
|
|
164
|
-
}): Promise<RpcResponse<{
|
|
165
|
-
items: DshSessionSummary[];
|
|
166
|
-
}>>;
|
|
167
|
-
create(request: {
|
|
168
|
-
rpcId: string;
|
|
169
|
-
payload: {
|
|
170
|
-
cwd?: string;
|
|
171
|
-
workspaceId?: string;
|
|
172
|
-
sessionId?: string;
|
|
173
|
-
agentPreset?: string;
|
|
174
|
-
};
|
|
175
|
-
}): Promise<RpcResponse<{
|
|
176
|
-
sessionId: string;
|
|
177
|
-
agentPreset?: string;
|
|
178
|
-
}>>;
|
|
179
|
-
history(request: {
|
|
214
|
+
interface ConfigurableProvider {
|
|
215
|
+
provider: string;
|
|
216
|
+
displayName: string;
|
|
217
|
+
settingsNs: string;
|
|
218
|
+
settingsPath: string[];
|
|
219
|
+
active: boolean;
|
|
220
|
+
declared?: boolean;
|
|
221
|
+
}
|
|
222
|
+
interface SettingsApiProxy extends DshApiProxy {
|
|
223
|
+
settings: {
|
|
224
|
+
describe(request: {
|
|
180
225
|
rpcId: string;
|
|
181
|
-
payload:
|
|
182
|
-
sessionId: string;
|
|
183
|
-
beforeSeq?: number;
|
|
184
|
-
maxMessages?: number;
|
|
185
|
-
};
|
|
226
|
+
payload: Record<string, never>;
|
|
186
227
|
}): Promise<RpcResponse<{
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
228
|
+
writable: boolean;
|
|
229
|
+
hasDocument: boolean;
|
|
230
|
+
namespaces: SettingsNamespaceView[];
|
|
190
231
|
}>>;
|
|
191
|
-
|
|
232
|
+
mutate(request: {
|
|
192
233
|
rpcId: string;
|
|
193
234
|
payload: {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
235
|
+
ns: string;
|
|
236
|
+
ops: Array<{
|
|
237
|
+
op: 'set';
|
|
238
|
+
path: string[];
|
|
239
|
+
value: unknown;
|
|
240
|
+
} | {
|
|
241
|
+
op: 'unset';
|
|
242
|
+
path: string[];
|
|
243
|
+
}>;
|
|
244
|
+
expectedRevision?: number;
|
|
198
245
|
};
|
|
199
|
-
}): Promise<RpcResponse<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* 上传一个文件,换取 prompt 可引用的凭证(`receiptId`)。
|
|
205
|
-
* `data` 是文件字节的**标准 base64**。
|
|
206
|
-
*/
|
|
207
|
-
uploadAttachment(request: {
|
|
246
|
+
}): Promise<RpcResponse<SettingsNamespaceView>>;
|
|
247
|
+
};
|
|
248
|
+
credentials: {
|
|
249
|
+
describe(request: {
|
|
208
250
|
rpcId: string;
|
|
209
251
|
payload: {
|
|
210
|
-
|
|
211
|
-
data: string;
|
|
212
|
-
name?: string;
|
|
252
|
+
refs: string[];
|
|
213
253
|
};
|
|
214
254
|
}): Promise<RpcResponse<{
|
|
215
|
-
|
|
216
|
-
|
|
255
|
+
credentials: Record<string, {
|
|
256
|
+
configured: boolean;
|
|
257
|
+
source?: string;
|
|
258
|
+
writable: boolean;
|
|
259
|
+
}>;
|
|
217
260
|
}>>;
|
|
218
|
-
|
|
219
|
-
* 按 id 取回附件字节。
|
|
220
|
-
*
|
|
221
|
-
* `attachmentId` 形如 `sha256:<hex>`(**含冒号**,客户端必须 URL 编码)。
|
|
222
|
-
* 返回 `Uint8Array` 而不是 base64:图片动辄上百 KB,多一次编解码纯属浪费。
|
|
223
|
-
*
|
|
224
|
-
* **必须带 `sessionId`**:内核要靠它读会话事件、取出这张图的真实 ref,
|
|
225
|
-
* 再据此做完整性校验(详见 chat-adapter 的 readAttachment 注释)。
|
|
226
|
-
*/
|
|
227
|
-
readAttachment(request: {
|
|
261
|
+
set(request: {
|
|
228
262
|
rpcId: string;
|
|
229
263
|
payload: {
|
|
230
|
-
|
|
231
|
-
|
|
264
|
+
ref: string;
|
|
265
|
+
value: string;
|
|
232
266
|
};
|
|
233
|
-
}): Promise<RpcResponse<
|
|
234
|
-
|
|
235
|
-
mediaType: string;
|
|
236
|
-
}>>;
|
|
237
|
-
/**
|
|
238
|
-
* 取回一张**交接图片**的字节(纯文本模型发图时,图片被落成工作区文件)。
|
|
239
|
-
*
|
|
240
|
-
* 与 [readAttachment] 的区别:那一个取内核附件存储里的图(走
|
|
241
|
-
* `sessionController.attachment`),这一个取插件自己写进 `.dsh-images/`
|
|
242
|
-
* 的交接件。手机端用它把图片**显示**给人看;消息里的路径文本照旧留给模型。
|
|
243
|
-
*/
|
|
244
|
-
readHandoffImage(request: {
|
|
267
|
+
}): Promise<RpcResponse<Record<string, never>>>;
|
|
268
|
+
unset(request: {
|
|
245
269
|
rpcId: string;
|
|
246
270
|
payload: {
|
|
247
|
-
|
|
248
|
-
name: string;
|
|
271
|
+
ref: string;
|
|
249
272
|
};
|
|
250
|
-
}): Promise<RpcResponse<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
cancel(request: {
|
|
273
|
+
}): Promise<RpcResponse<Record<string, never>>>;
|
|
274
|
+
};
|
|
275
|
+
llm: {
|
|
276
|
+
providers(request: {
|
|
255
277
|
rpcId: string;
|
|
256
|
-
payload:
|
|
257
|
-
sessionId: string;
|
|
258
|
-
};
|
|
278
|
+
payload: Record<string, never>;
|
|
259
279
|
}): Promise<RpcResponse<{
|
|
260
|
-
|
|
280
|
+
providers: ConfigurableProvider[];
|
|
261
281
|
}>>;
|
|
262
|
-
|
|
282
|
+
models(request: {
|
|
263
283
|
rpcId: string;
|
|
264
|
-
payload:
|
|
265
|
-
sessionId: string;
|
|
266
|
-
title: string;
|
|
267
|
-
};
|
|
284
|
+
payload: Record<string, never>;
|
|
268
285
|
}): Promise<RpcResponse<{
|
|
269
|
-
|
|
270
|
-
|
|
286
|
+
groups: ModelProviderGroupView[];
|
|
287
|
+
failures: {
|
|
288
|
+
provider: string;
|
|
289
|
+
message: string;
|
|
290
|
+
}[];
|
|
271
291
|
}>>;
|
|
272
|
-
|
|
292
|
+
discoverModels(request: {
|
|
273
293
|
rpcId: string;
|
|
274
294
|
payload: {
|
|
275
|
-
|
|
276
|
-
|
|
295
|
+
settingsNs: string;
|
|
296
|
+
provider?: string;
|
|
297
|
+
baseURL?: string;
|
|
298
|
+
api?: string;
|
|
299
|
+
apiKey?: string;
|
|
277
300
|
};
|
|
278
301
|
}): Promise<RpcResponse<{
|
|
279
|
-
|
|
302
|
+
models: ProviderModelView[];
|
|
280
303
|
}>>;
|
|
281
|
-
models(request: {
|
|
282
|
-
rpcId: string;
|
|
283
|
-
payload: {
|
|
284
|
-
sessionId: string;
|
|
285
|
-
};
|
|
286
|
-
}): Promise<RpcResponse<SessionModelsView>>;
|
|
287
|
-
selectModel(request: {
|
|
288
|
-
rpcId: string;
|
|
289
|
-
payload: {
|
|
290
|
-
sessionId: string;
|
|
291
|
-
provider: string;
|
|
292
|
-
model: string;
|
|
293
|
-
reasoningEffort?: string;
|
|
294
|
-
};
|
|
295
|
-
}): Promise<RpcResponse<{
|
|
296
|
-
selected: ModelSelectionView;
|
|
297
|
-
}>>;
|
|
298
|
-
};
|
|
299
|
-
workspace: {
|
|
300
|
-
list(request: {
|
|
301
|
-
rpcId: string;
|
|
302
|
-
payload: Record<string, never>;
|
|
303
|
-
}): Promise<RpcResponse<{
|
|
304
|
-
items: DshWorkspaceSummary[];
|
|
305
|
-
archivedSessionIds: string[];
|
|
306
|
-
}>>;
|
|
307
|
-
create(request: {
|
|
308
|
-
rpcId: string;
|
|
309
|
-
payload: {
|
|
310
|
-
path: string;
|
|
311
|
-
};
|
|
312
|
-
}): Promise<RpcResponse<{
|
|
313
|
-
workspace: DshWorkspaceSummary;
|
|
314
|
-
created: boolean;
|
|
315
|
-
}>>;
|
|
316
|
-
rename(request: {
|
|
317
|
-
rpcId: string;
|
|
318
|
-
payload: {
|
|
319
|
-
workspaceId: string;
|
|
320
|
-
title: string;
|
|
321
|
-
};
|
|
322
|
-
}): Promise<RpcResponse<{
|
|
323
|
-
workspace: DshWorkspaceSummary;
|
|
324
|
-
}>>;
|
|
325
|
-
delete(request: {
|
|
326
|
-
rpcId: string;
|
|
327
|
-
payload: {
|
|
328
|
-
workspaceId: string;
|
|
329
|
-
};
|
|
330
|
-
}): Promise<RpcResponse<{
|
|
331
|
-
deleted: true;
|
|
332
|
-
}>>;
|
|
333
|
-
archiveSession(request: {
|
|
334
|
-
rpcId: string;
|
|
335
|
-
payload: {
|
|
336
|
-
sessionId: string;
|
|
337
|
-
};
|
|
338
|
-
}): Promise<RpcResponse<{
|
|
339
|
-
archivedSessionIds: string[];
|
|
340
|
-
}>>;
|
|
341
|
-
};
|
|
342
|
-
agentPresets: {
|
|
343
|
-
list(request: {
|
|
344
|
-
rpcId: string;
|
|
345
|
-
payload: Record<string, never>;
|
|
346
|
-
}): Promise<RpcResponse<{
|
|
347
|
-
presets: DshAgentPresetSummary[];
|
|
348
|
-
authorable: boolean;
|
|
349
|
-
hasDocument: boolean;
|
|
350
|
-
}>>;
|
|
351
|
-
select(request: {
|
|
352
|
-
rpcId: string;
|
|
353
|
-
payload: {
|
|
354
|
-
sessionId: string;
|
|
355
|
-
agentPreset: string;
|
|
356
|
-
};
|
|
357
|
-
}): Promise<RpcResponse<{
|
|
358
|
-
agentPreset: string;
|
|
359
|
-
}>>;
|
|
360
|
-
};
|
|
361
|
-
/**
|
|
362
|
-
* 用户问题(内核 `ctx.userQuestions`,即 ask_user_question 工具)。
|
|
363
|
-
*
|
|
364
|
-
* 为什么单开一个命名空间而不是塞进 `sessions`:它由**另一个内核服务**提供
|
|
365
|
-
* (`@deepseek-ai/dsh-user-questions`),在一条没装它的 profile 上是**可选能力** ——
|
|
366
|
-
* 分开之后,「这个能力不在」和「这个会话有问题」在类型上就分得开。
|
|
367
|
-
*/
|
|
368
|
-
userQuestions: {
|
|
369
|
-
/**
|
|
370
|
-
* 列出还能回答的问题(内核 `continued(agent)`)。
|
|
371
|
-
* 没有 live agent、或没有该服务时返回空列表 —— 冷会话是常态,不该报错。
|
|
372
|
-
*/
|
|
373
|
-
list(request: {
|
|
374
|
-
rpcId: string;
|
|
375
|
-
payload: {
|
|
376
|
-
sessionId: string;
|
|
377
|
-
};
|
|
378
|
-
}): Promise<RpcResponse<{
|
|
379
|
-
items: UserQuestionView[];
|
|
380
|
-
}>>;
|
|
381
|
-
/** 提交答案;失败原因见 [UserQuestionAnswerOutcome]。 */
|
|
382
|
-
answer(request: {
|
|
383
|
-
rpcId: string;
|
|
384
|
-
payload: {
|
|
385
|
-
sessionId: string;
|
|
386
|
-
callId: string;
|
|
387
|
-
answer: UserQuestionAnswer;
|
|
388
|
-
};
|
|
389
|
-
}): Promise<RpcResponse<UserQuestionAnswerOutcome>>;
|
|
390
|
-
};
|
|
391
|
-
events: {
|
|
392
|
-
mux(request: {
|
|
393
|
-
rpcId: string;
|
|
394
|
-
payload: Record<string, never>;
|
|
395
|
-
}, signal: AbortSignal): AsyncIterable<{
|
|
396
|
-
rpcId: string;
|
|
397
|
-
payload: Record<string, unknown>;
|
|
398
|
-
}>;
|
|
399
|
-
host(request: {
|
|
400
|
-
rpcId: string;
|
|
401
|
-
payload: Record<string, never>;
|
|
402
|
-
}, signal: AbortSignal): AsyncIterable<{
|
|
403
|
-
rpcId: string;
|
|
404
|
-
payload: Record<string, unknown>;
|
|
405
|
-
}>;
|
|
406
304
|
};
|
|
407
305
|
}
|
|
408
|
-
interface
|
|
409
|
-
provider: string;
|
|
410
|
-
model: string;
|
|
411
|
-
reasoningEffort?: string;
|
|
412
|
-
}
|
|
413
|
-
interface ModelReasoningView {
|
|
414
|
-
efforts: {
|
|
415
|
-
id: string;
|
|
416
|
-
name: string;
|
|
417
|
-
description?: string;
|
|
418
|
-
}[];
|
|
419
|
-
defaultEffort?: string;
|
|
420
|
-
}
|
|
421
|
-
interface ModelProviderGroupView {
|
|
306
|
+
interface ProviderModelView {
|
|
422
307
|
id: string;
|
|
423
|
-
name
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
name: string;
|
|
427
|
-
description?: string;
|
|
428
|
-
contextWindow?: number;
|
|
429
|
-
maxTokens?: number;
|
|
430
|
-
reasoning?: ModelReasoningView;
|
|
431
|
-
}[];
|
|
432
|
-
}
|
|
433
|
-
interface SessionModelsView {
|
|
434
|
-
current: ModelSelectionView;
|
|
435
|
-
routable: boolean;
|
|
436
|
-
groups: ModelProviderGroupView[];
|
|
437
|
-
failures: {
|
|
438
|
-
provider: string;
|
|
439
|
-
message: string;
|
|
440
|
-
}[];
|
|
441
|
-
}
|
|
442
|
-
interface CommandDescriptorView {
|
|
443
|
-
name: string;
|
|
444
|
-
description: string;
|
|
445
|
-
input?: {
|
|
446
|
-
hint: string;
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
interface CommandExecutionView {
|
|
450
|
-
commandId: string;
|
|
451
|
-
result: {
|
|
452
|
-
kind: 'success' | 'error';
|
|
453
|
-
text?: string;
|
|
454
|
-
sourceEventSeq?: number;
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
interface DshCommandRuntime {
|
|
458
|
-
list(agent: unknown): readonly CommandDescriptorView[];
|
|
459
|
-
execute(agent: unknown, line: string, signal: AbortSignal): Promise<CommandExecutionView | undefined>;
|
|
460
|
-
}
|
|
461
|
-
interface DshAgentRegistry {
|
|
462
|
-
get(sessionId: string): unknown | undefined;
|
|
308
|
+
name?: string;
|
|
309
|
+
contextWindow?: number;
|
|
310
|
+
maxTokens?: number;
|
|
463
311
|
}
|
|
464
|
-
interface
|
|
465
|
-
|
|
466
|
-
|
|
312
|
+
interface ProviderConfigView {
|
|
313
|
+
baseURL?: string;
|
|
314
|
+
api?: string;
|
|
315
|
+
displayName?: string;
|
|
316
|
+
thinking?: string;
|
|
317
|
+
reasoningEffort?: string;
|
|
318
|
+
models: ProviderModelView[];
|
|
319
|
+
modelsInherited?: boolean;
|
|
467
320
|
}
|
|
468
|
-
interface
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
321
|
+
interface ProviderView {
|
|
322
|
+
id: string;
|
|
323
|
+
displayName: string;
|
|
324
|
+
active: boolean;
|
|
325
|
+
declared?: boolean;
|
|
326
|
+
configurable: boolean;
|
|
327
|
+
configured: boolean;
|
|
328
|
+
removable: boolean;
|
|
329
|
+
credential: {
|
|
330
|
+
ref: string;
|
|
331
|
+
configured: boolean;
|
|
332
|
+
source?: string;
|
|
333
|
+
writable: boolean;
|
|
480
334
|
};
|
|
335
|
+
config: ProviderConfigView;
|
|
481
336
|
}
|
|
482
|
-
interface
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
337
|
+
interface ProviderPatch {
|
|
338
|
+
displayName?: string | null;
|
|
339
|
+
baseURL?: string | null;
|
|
340
|
+
api?: string | null;
|
|
341
|
+
apiKey?: string | null;
|
|
342
|
+
thinking?: string | null;
|
|
343
|
+
reasoningEffort?: string | null;
|
|
344
|
+
models?: ProviderModelView[];
|
|
345
|
+
expectedRevision?: number;
|
|
489
346
|
}
|
|
490
|
-
interface
|
|
347
|
+
interface CustomProviderCreate {
|
|
491
348
|
id: string;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
349
|
+
displayName?: string;
|
|
350
|
+
baseURL: string;
|
|
351
|
+
api: string;
|
|
352
|
+
apiKey?: string;
|
|
353
|
+
models: ProviderModelView[];
|
|
354
|
+
expectedRevision?: number;
|
|
497
355
|
}
|
|
498
|
-
interface
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
updatedAt: number;
|
|
503
|
-
running: boolean;
|
|
504
|
-
blank: boolean;
|
|
505
|
-
title?: string;
|
|
506
|
-
workspaceId?: string;
|
|
507
|
-
workspaceTitle?: string;
|
|
508
|
-
agentPreset?: string;
|
|
509
|
-
parentSessionId?: string;
|
|
510
|
-
origin?: 'subagent';
|
|
511
|
-
pendingInteraction?: 'approval';
|
|
356
|
+
interface CustomProviderCapability {
|
|
357
|
+
available: boolean;
|
|
358
|
+
protocols: string[];
|
|
359
|
+
revision?: number;
|
|
512
360
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
361
|
+
declare class DshSettingsAdapter {
|
|
362
|
+
private readonly api;
|
|
363
|
+
constructor(api: SettingsApiProxy);
|
|
364
|
+
listProviders(): Promise<{
|
|
365
|
+
writable: boolean;
|
|
366
|
+
revisionByNamespace: Record<string, number>;
|
|
367
|
+
customProvider: CustomProviderCapability;
|
|
368
|
+
providers: ProviderView[];
|
|
369
|
+
}>;
|
|
370
|
+
catalog(): Promise<{
|
|
371
|
+
groups: ModelProviderGroupView[];
|
|
372
|
+
failures: {
|
|
373
|
+
provider: string;
|
|
374
|
+
message: string;
|
|
375
|
+
}[];
|
|
376
|
+
}>;
|
|
377
|
+
updateProvider(providerId: string, patch: ProviderPatch): Promise<void>;
|
|
378
|
+
createProvider(input: CustomProviderCreate): Promise<void>;
|
|
379
|
+
removeProvider(providerId: string, expectedRevision?: number): Promise<void>;
|
|
380
|
+
discover(providerId: string, draft: {
|
|
381
|
+
baseURL?: string;
|
|
382
|
+
api?: string;
|
|
383
|
+
apiKey?: string;
|
|
384
|
+
}): Promise<ProviderModelView[]>;
|
|
385
|
+
private providerContext;
|
|
521
386
|
}
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region src/host/dsh-0.2-bridge.d.ts
|
|
522
389
|
/**
|
|
523
|
-
*
|
|
390
|
+
* Prompt content a 0.2.x Host accepts — mirrors its `PromptContentPart` union.
|
|
524
391
|
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
* 不该等插件跟着发版。所以这里只钉住三个**契约字段**,其余原样透传。
|
|
392
|
+
* 权威定义在 `chat-adapter.ts`(协议层),这里只是复用它,避免两处各写一遍
|
|
393
|
+
* 然后悄悄漂移。
|
|
528
394
|
*/
|
|
529
|
-
|
|
530
|
-
callId: string;
|
|
531
|
-
/** 内核 `UserQuestionState`:'open' 或 'continued';本端点只返回 continued。 */
|
|
532
|
-
state: string;
|
|
533
|
-
/** 问题项**原样**透传(id / question / detail? / header? / options? / multiSelect? / intent?)。 */
|
|
534
|
-
questions: unknown[];
|
|
535
|
-
/** 内核以后加的字段照样透传,不在这里裁剪。 */
|
|
536
|
-
[key: string]: unknown;
|
|
537
|
-
}
|
|
538
|
-
/** 一条问题的答案(内核 `AskUserQuestionAnswerItem`)。 */
|
|
539
|
-
interface UserQuestionAnswerItem {
|
|
540
|
-
/** 被回答的问题 id,必须与请求里的问题 id 对上。 */
|
|
541
|
-
id: string;
|
|
542
|
-
/** 选中的选项 label;多选时可能多项。 */
|
|
543
|
-
selected: string[];
|
|
544
|
-
/** 可选的「其他」自由文本。 */
|
|
545
|
-
custom?: string;
|
|
546
|
-
}
|
|
547
|
-
/** 一次提交的完整答案批次(内核 `AskUserQuestionAnswer`):一次调用里的每个问题各占一项。 */
|
|
548
|
-
interface UserQuestionAnswer {
|
|
549
|
-
answers: UserQuestionAnswerItem[];
|
|
550
|
-
}
|
|
395
|
+
type PromptContentPartLike = PromptContentPart;
|
|
551
396
|
/**
|
|
552
|
-
*
|
|
397
|
+
* `ctx.fileUploads`:内核的文件上传服务。
|
|
553
398
|
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
* 409 / 400 / 503。做成**值**之后由 adapter 决定 HTTP 状态,
|
|
557
|
-
* 和 approvals 的 `respond()` 返回 `{ accepted: false, reason }` 是同一个套路。
|
|
399
|
+
* 由内核的 `@deepseek-ai/dsh-client-file-upload` 以 cordis 服务的形式暴露,
|
|
400
|
+
* 因此插件**不需要**把字节转发到内核的 HTTP 路由(那条路由只绑 loopback,手机也到不了)。
|
|
558
401
|
*/
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
accepted: false;
|
|
574
|
-
reason: UserQuestionAnswerFailure;
|
|
575
|
-
};
|
|
576
|
-
interface ChatWorkspaceView {
|
|
577
|
-
id: string;
|
|
578
|
-
title: string;
|
|
579
|
-
rootId: string;
|
|
580
|
-
path: string;
|
|
581
|
-
createdAt: string;
|
|
582
|
-
updatedAt: string;
|
|
402
|
+
interface FileUploadsLike {
|
|
403
|
+
upload(agent: unknown, request: {
|
|
404
|
+
data: string;
|
|
405
|
+
name?: string;
|
|
406
|
+
}, signal: AbortSignal): Promise<{
|
|
407
|
+
receiptId: string;
|
|
408
|
+
}>;
|
|
409
|
+
/**
|
|
410
|
+
* 把凭证绑定到某次 prompt。返回值不 commit 的话凭证会被回收 ——
|
|
411
|
+
* 所以调用方必须在 prompt **成功之后** commit()。
|
|
412
|
+
*/
|
|
413
|
+
bindPrompt(agent: unknown, receiptIds: readonly string[], requestId: string): {
|
|
414
|
+
commit(): void;
|
|
415
|
+
};
|
|
583
416
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
417
|
+
/**
|
|
418
|
+
* `ctx.attachments`:内核的附件存储。
|
|
419
|
+
*
|
|
420
|
+
* 为什么会话里的图片必须经由它:消息**发送时**图片是 base64 内联的,但**存进历史后**
|
|
421
|
+
* 会变成 `{type:'image', attachment:{attachmentId:'sha256:…'}}` 这样的引用 ——
|
|
422
|
+
* 字节在附件存储里,不在事件里。客户端只拿到 id,所以必须有一步「按 id 取字节」,
|
|
423
|
+
* 否则历史里的图片一律显示不出来。
|
|
424
|
+
*/
|
|
425
|
+
interface AttachmentStoreLike {
|
|
426
|
+
readImage(
|
|
427
|
+
/**
|
|
428
|
+
* 附件引用。
|
|
429
|
+
*
|
|
430
|
+
* `attachmentId` 是唯一的**定位**字段(内核用 `ID_PATTERN.exec()` 取出 sha256
|
|
431
|
+
* 再拼 `objects/<前两位>/<sha>`);而 `mediaType` / `bytes` / `width` / `height`
|
|
432
|
+
* 参与读后**完整性校验** —— 缺任何一项都会抛 `ATTACHMENT_CORRUPT`,
|
|
433
|
+
* 哪怕字节已经正确读出来了。
|
|
434
|
+
*/
|
|
435
|
+
ref: {
|
|
436
|
+
attachmentId: string;
|
|
437
|
+
mediaType?: string;
|
|
438
|
+
bytes?: number;
|
|
439
|
+
width?: number;
|
|
440
|
+
height?: number;
|
|
441
|
+
}, signal?: AbortSignal): Promise<{
|
|
442
|
+
ref: {
|
|
443
|
+
mediaType: string;
|
|
444
|
+
};
|
|
445
|
+
data: Uint8Array;
|
|
446
|
+
}>;
|
|
591
447
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
private readonly sessionRootLookups;
|
|
598
|
-
private readonly pendingApprovals;
|
|
599
|
-
constructor(api: DshApiProxy, database: WorkspaceDatabase, commandServices?: DshCommandServices | undefined);
|
|
600
|
-
listSessions(principal: Principal): Promise<ChatSessionView[]>;
|
|
601
|
-
listWorkspaces(principal: Principal): Promise<ChatWorkspaceView[]>;
|
|
602
|
-
createWorkspace(principal: Principal, rootId: string, relativePath: string): Promise<ChatWorkspaceView>;
|
|
603
|
-
renameWorkspace(principal: Principal, workspaceId: string, title: string): Promise<ChatWorkspaceView>;
|
|
604
|
-
deleteWorkspace(principal: Principal, workspaceId: string): Promise<void>;
|
|
605
|
-
listAgentPresets(): Promise<AgentPresetView[]>;
|
|
606
|
-
selectAgentPreset(principal: Principal, sessionId: string, agentPreset: string): Promise<string>;
|
|
607
|
-
createSessionInWorkspace(principal: Principal, workspaceId: string, options: {
|
|
608
|
-
sessionId?: string;
|
|
609
|
-
agentPreset?: string;
|
|
610
|
-
}): Promise<{
|
|
611
|
-
id: string;
|
|
612
|
-
agentPreset?: string;
|
|
448
|
+
interface SessionControllerLike {
|
|
449
|
+
list(request: {
|
|
450
|
+
cursor?: string;
|
|
451
|
+
}, signal: AbortSignal): Promise<{
|
|
452
|
+
items: readonly unknown[];
|
|
613
453
|
}>;
|
|
614
|
-
|
|
454
|
+
create(request: {
|
|
455
|
+
workspaceId?: string;
|
|
456
|
+
cwd?: string;
|
|
615
457
|
sessionId?: string;
|
|
616
458
|
agentPreset?: string;
|
|
617
459
|
}): Promise<{
|
|
618
|
-
|
|
460
|
+
sessionId: string;
|
|
619
461
|
agentPreset?: string;
|
|
620
462
|
}>;
|
|
621
|
-
|
|
622
|
-
|
|
463
|
+
/**
|
|
464
|
+
* Live follow stream. Its first frame is always a `snapshot` carrying the
|
|
465
|
+
* opening window (`cursor`, `records`, `hasMore`), and that is the only
|
|
466
|
+
* reliable way to read history: `page()` demands a `throughSeq` "obtained from
|
|
467
|
+
* the corresponding follow opening frame", so guessing one (e.g. `-1`) returns
|
|
468
|
+
* a well-formed but *empty* page — a silent failure, not an error.
|
|
469
|
+
*/
|
|
470
|
+
follow(request: {
|
|
471
|
+
address: {
|
|
472
|
+
kind: 'session';
|
|
473
|
+
sessionId: string;
|
|
474
|
+
};
|
|
623
475
|
maxMessages?: number;
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
476
|
+
/**
|
|
477
|
+
* Assistant presentation frames are opt-in: without `assistantStream: true`
|
|
478
|
+
* the host emits durable events only, so a live stream would carry no
|
|
479
|
+
* incremental text at all (the transcript then appears only after commit).
|
|
480
|
+
*/
|
|
481
|
+
assistantStream?: true;
|
|
482
|
+
}, signal: AbortSignal): AsyncIterable<{
|
|
483
|
+
type?: string;
|
|
484
|
+
cursor?: number;
|
|
485
|
+
records?: readonly unknown[];
|
|
486
|
+
hasMore?: boolean;
|
|
487
|
+
}>;
|
|
488
|
+
prompt(request: {
|
|
489
|
+
requestId: string;
|
|
490
|
+
sessionId: string;
|
|
491
|
+
mode: 'queue' | 'steer';
|
|
492
|
+
content: readonly PromptContentPartLike[];
|
|
493
|
+
clientTimeZone?: string;
|
|
494
|
+
}, signal: AbortSignal): Promise<{
|
|
495
|
+
accepted: true;
|
|
628
496
|
}>;
|
|
629
|
-
|
|
497
|
+
cancel(request: {
|
|
498
|
+
sessionId: string;
|
|
499
|
+
}): {
|
|
630
500
|
accepted: true;
|
|
631
|
-
|
|
501
|
+
};
|
|
502
|
+
rename(request: {
|
|
503
|
+
sessionId: string;
|
|
504
|
+
title: string;
|
|
505
|
+
}): Promise<{
|
|
506
|
+
title: string;
|
|
507
|
+
seq: number;
|
|
508
|
+
}>;
|
|
509
|
+
fork(request: {
|
|
510
|
+
sessionId: string;
|
|
511
|
+
atSeq?: number;
|
|
512
|
+
}): Promise<{
|
|
513
|
+
sessionId: string;
|
|
632
514
|
}>;
|
|
633
515
|
/**
|
|
634
|
-
*
|
|
516
|
+
* 取一张**被这个会话引用过**的图片字节(内核主机端方法,名字就叫 `attachment`)。
|
|
635
517
|
*
|
|
636
|
-
*
|
|
637
|
-
* `
|
|
518
|
+
* 这是取历史图片的**正路**,它内部一次做完三件事:校验会话确实引用了这张图、
|
|
519
|
+
* 调 `ctx.attachments.readImage`、用**会话事件里那条真实 ref**做完整性校验
|
|
520
|
+
*(`mediaType` / `bytes` / `width` / `height` 四项逐项比对)。
|
|
521
|
+
*
|
|
522
|
+
* 为什么必须走它、不能自己调 `ctx.attachments`:实测本机这条 profile 里
|
|
523
|
+
* `ctx.attachments` 是 **MISSING**(诊断日志
|
|
524
|
+
* `ctx.attachments MISSING(所以改走内核 sessionController.readAttachment)`),
|
|
525
|
+
* 自己拼 ref 那条路根本走不通;而且绕开内核就得把它内部三件事全复刻一遍,
|
|
526
|
+
* 早先正是只传了 id、缺四项元数据,导致 `ATTACHMENT_CORRUPT`。
|
|
527
|
+
*
|
|
528
|
+
* 注意方法名:**主机端叫 `attachment`**,客户端契约里才叫 `readAttachment`
|
|
529
|
+
*(`dsh-api-session-controller/lib/types/client/contract/session.d.ts`)。
|
|
530
|
+
* `data` 是 **base64 字符串**(主机端实现里 `Buffer.from(stored.data).toString("base64")`),
|
|
531
|
+
* 不是 Uint8Array —— 调用方要自己解码。
|
|
638
532
|
*/
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
533
|
+
attachment(request: {
|
|
534
|
+
sessionId: string;
|
|
535
|
+
attachmentId: string;
|
|
536
|
+
}): Promise<{
|
|
537
|
+
attachment: {
|
|
538
|
+
mediaType: string;
|
|
539
|
+
};
|
|
540
|
+
data: string;
|
|
541
|
+
}>;
|
|
542
|
+
selectModel(request: {
|
|
543
|
+
sessionId: string;
|
|
544
|
+
provider: string;
|
|
545
|
+
model: string;
|
|
546
|
+
reasoningEffort?: string;
|
|
547
|
+
}): Promise<{
|
|
548
|
+
selected: unknown;
|
|
642
549
|
}>;
|
|
643
550
|
/**
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
* `
|
|
648
|
-
* 1. 实测本机 profile 里 `ctx.attachments` 是 **MISSING**(拿不到注入属性)——
|
|
649
|
-
* 自己拼 ref 那条路根本走不通;
|
|
650
|
-
* 2. 内核那个方法内部一次做完三件事:校验该会话确实引用了这张图、
|
|
651
|
-
* 读出字节、用**会话事件里那条真实 ref** 做完整性校验
|
|
652
|
-
* (`mediaType` / `bytes` / `width` / `height` 四项逐项比对)。
|
|
653
|
-
* 绕开它就得把这三件事全复刻 —— 先前正是漏了第三件,于是每张图
|
|
654
|
-
* `ATTACHMENT_CORRUPT`:「字节读到了,却被校验拒之门外」。
|
|
655
|
-
*
|
|
656
|
-
* 因此 **sessionId 是必需参数**(内核要拿它读会话事件、找到那张图的真实 ref);
|
|
657
|
-
* 早先设计的「客户端把四项元数据带上来」是多余的 —— 内核手里本来就有。
|
|
551
|
+
* Host model catalog. 0.2.x reports `default`, the routable route list, and
|
|
552
|
+
* per-route failures; the plugin's `SessionModelsView` instead wants a boolean
|
|
553
|
+
* `routable` plus a per-session `current`, so the bridge folds them (see the
|
|
554
|
+
* `models` method) rather than passing this through.
|
|
658
555
|
*/
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
556
|
+
modelCatalog(): Promise<{
|
|
557
|
+
default?: unknown;
|
|
558
|
+
routableProviders?: readonly string[];
|
|
559
|
+
groups?: readonly unknown[];
|
|
560
|
+
failures?: readonly {
|
|
561
|
+
id?: unknown;
|
|
562
|
+
name?: unknown;
|
|
563
|
+
message?: unknown;
|
|
564
|
+
}[];
|
|
662
565
|
}>;
|
|
663
566
|
/**
|
|
664
|
-
*
|
|
665
|
-
*
|
|
567
|
+
* Projection baseline for one session. Unlike the catalog this is per-session,
|
|
568
|
+
* which is what makes it the source of the session's *current* model selection
|
|
569
|
+
* (`values.modelSelection`).
|
|
666
570
|
*/
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
571
|
+
projections(request: {
|
|
572
|
+
sessionId: string;
|
|
573
|
+
}, signal: AbortSignal): Promise<{
|
|
574
|
+
asOfSeq?: number;
|
|
575
|
+
values?: Record<string, unknown>;
|
|
576
|
+
} | null>;
|
|
577
|
+
resolveAgent(sessionId: string): Promise<unknown>;
|
|
578
|
+
}
|
|
579
|
+
interface WorkspaceControllerLike {
|
|
580
|
+
create(request: {
|
|
581
|
+
path: string;
|
|
582
|
+
}): Promise<{
|
|
583
|
+
workspace: unknown;
|
|
584
|
+
created: boolean;
|
|
670
585
|
}>;
|
|
671
|
-
|
|
672
|
-
|
|
586
|
+
rename(request: {
|
|
587
|
+
workspaceId: string;
|
|
673
588
|
title: string;
|
|
674
|
-
|
|
589
|
+
}): Promise<unknown>;
|
|
590
|
+
delete(request: {
|
|
591
|
+
workspaceId: string;
|
|
592
|
+
}): Promise<{
|
|
593
|
+
deleted: boolean;
|
|
675
594
|
}>;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
595
|
+
archiveSession(request: {
|
|
596
|
+
sessionId: string;
|
|
597
|
+
}): Promise<{
|
|
598
|
+
archivedSessionIds: readonly string[];
|
|
599
|
+
}>;
|
|
600
|
+
}
|
|
601
|
+
interface WorkspaceRegistryLike {
|
|
602
|
+
list(): readonly {
|
|
603
|
+
id: string;
|
|
604
|
+
path: string;
|
|
605
|
+
title: string;
|
|
606
|
+
createdAt: string;
|
|
607
|
+
updatedAt: string;
|
|
608
|
+
sessionIds?: readonly string[];
|
|
609
|
+
}[];
|
|
610
|
+
}
|
|
611
|
+
interface SettingsControllerLike {
|
|
612
|
+
describe(): {
|
|
613
|
+
writable: boolean;
|
|
614
|
+
hasDocument: boolean;
|
|
615
|
+
namespaces: readonly unknown[];
|
|
616
|
+
};
|
|
617
|
+
mutate(ns: string, ops: readonly unknown[], expectedRevision: number | undefined): Promise<unknown>;
|
|
618
|
+
}
|
|
619
|
+
interface CredentialsControllerLike {
|
|
620
|
+
describe(refs: readonly string[]): Promise<Record<string, unknown>>;
|
|
621
|
+
set(ref: string, value: string): Promise<void>;
|
|
622
|
+
unset(ref: string): Promise<void>;
|
|
623
|
+
}
|
|
624
|
+
interface AgentPresetsLike {
|
|
625
|
+
list(): Promise<readonly unknown[]>;
|
|
626
|
+
remoteExportList(): Promise<unknown>;
|
|
627
|
+
resolveAgent?: never;
|
|
628
|
+
select(agent: unknown, agentPreset: string): Promise<string>;
|
|
629
|
+
}
|
|
630
|
+
interface LlmLike {
|
|
631
|
+
/** Live routes, as `{ id, name }` rows. */
|
|
632
|
+
listProviders(): readonly {
|
|
633
|
+
id: string;
|
|
634
|
+
}[];
|
|
635
|
+
/**
|
|
636
|
+
* Every route an adapter plugin could activate through configuration, live or
|
|
637
|
+
* dormant, carrying the settings namespace and path that configures it.
|
|
638
|
+
* Configuration surfaces merge this with `listProviders()` for liveness.
|
|
639
|
+
*/
|
|
640
|
+
listConfigurableProviders(): readonly {
|
|
641
|
+
provider: string;
|
|
642
|
+
displayName: string;
|
|
643
|
+
settingsNs: string;
|
|
644
|
+
settingsPath: readonly string[];
|
|
645
|
+
declared?: boolean;
|
|
646
|
+
error?: string;
|
|
647
|
+
}[];
|
|
648
|
+
discoverModels(settingsNs: string, request: unknown, signal?: AbortSignal): Promise<readonly unknown[]>;
|
|
680
649
|
/**
|
|
681
|
-
*
|
|
650
|
+
* 一个路由 + 模型 id 的**真实元数据**。
|
|
682
651
|
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
652
|
+
* 为什么必须用它、而不是自己猜模型能不能看图:内核拒绝「给纯文本模型发图片」时
|
|
653
|
+
* 用的就是**这个方法**(`SessionController` 的 prompt 准入里
|
|
654
|
+
* `this.ctx.llm.resolveModelInfo(...)` 再查 `inputModalities`)。入口要与内核
|
|
655
|
+
* 得出同一个结论,只能查同一份元数据 —— 名字、前缀、家族表都是启发式,会漂。
|
|
685
656
|
*/
|
|
686
|
-
|
|
657
|
+
resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<{
|
|
658
|
+
inputModalities?: readonly string[];
|
|
659
|
+
} | undefined>;
|
|
660
|
+
}
|
|
661
|
+
/** Host services the bridge consumes; assembled by `src/index.ts` from `ctx`. */
|
|
662
|
+
interface Dsh02HostServices {
|
|
663
|
+
sessionController: SessionControllerLike;
|
|
664
|
+
workspaceController: WorkspaceControllerLike;
|
|
665
|
+
workspaceRegistry: WorkspaceRegistryLike;
|
|
666
|
+
settingsController: SettingsControllerLike;
|
|
667
|
+
credentialsController: CredentialsControllerLike;
|
|
668
|
+
agentPresets: AgentPresetsLike;
|
|
669
|
+
llm: LlmLike;
|
|
687
670
|
/**
|
|
688
|
-
*
|
|
671
|
+
* 内核附件存储(`ctx.attachments`);会话历史里的图片要经它取字节。
|
|
689
672
|
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
673
|
+
* 必须是**已经取到的服务对象**,不能是 `ctx` 本身:cordis 的注入属性是有生命周期的
|
|
674
|
+
* 惰性访问器,只在 `apply` 执行期间可读。把它存进 `host` 里、等到 HTTP 请求回调
|
|
675
|
+
* (`readAttachment`)里再读,就会撞上
|
|
676
|
+
* `cannot get property "attachments" without inject` —— 请求链路本身没问题,
|
|
677
|
+
* 挂在服务端、而且只在真的去取图时才炸,所以很难在本地测出来。
|
|
694
678
|
*/
|
|
695
|
-
|
|
696
|
-
models(principal: Principal, sessionId: string): Promise<SessionModelsView>;
|
|
697
|
-
selectModel(principal: Principal, sessionId: string, selection: ModelSelectionView): Promise<ModelSelectionView>;
|
|
698
|
-
listCommands(principal: Principal, sessionId: string): Promise<CommandDescriptorView[]>;
|
|
699
|
-
executeCommand(principal: Principal, sessionId: string, line: string): Promise<CommandExecutionView>;
|
|
700
|
-
isSessionAuthorized(principal: Principal, sessionId: string): Promise<boolean>;
|
|
701
|
-
startEvents(signal: AbortSignal, emit: (event: WorkspaceEvent) => void): void;
|
|
702
|
-
private captureApproval;
|
|
703
|
-
private hasPendingApproval;
|
|
679
|
+
attachments: AttachmentStoreLike;
|
|
704
680
|
/**
|
|
705
|
-
*
|
|
681
|
+
* Kernel file-upload service (`ctx.fileUploads`).
|
|
706
682
|
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
* property of the session rather than a failure of this call: the caller
|
|
710
|
-
* decides whether it means "no commands" (listing) or an error (execution).
|
|
683
|
+
* 为什么用它而不是内核那条 `/api/session/uploadFileBinary` HTTP 路由:那条路由
|
|
684
|
+
* 只绑 loopback(手机根本到不了),而这个 cordis 服务就在同一个进程里,插件可直接调。
|
|
711
685
|
*/
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
686
|
+
fileUploads: FileUploadsLike;
|
|
687
|
+
/**
|
|
688
|
+
* Cordis event bus of the host context. Used for the two facts the old
|
|
689
|
+
* `apiProxy` event streams carried that no controller returns: session
|
|
690
|
+
* registry changes and the approval answerer waterfall.
|
|
691
|
+
*/
|
|
692
|
+
on(event: string, listener: (...args: any[]) => unknown): () => void;
|
|
719
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* 需要交接时,把每个 `image` 部件换成**两行文本**;否则原样返回。
|
|
696
|
+
*
|
|
697
|
+
* 同一行路径要同时伺候两类读者:
|
|
698
|
+
* - **给人看**(第一行):图片序号、原始文件名、工作目录里的短路径;
|
|
699
|
+
* - **给机器看**(第二行 `[Image: source: <绝对路径>]`):这是读图技能明确
|
|
700
|
+
* 识别的占位形状之一(带图片扩展名的绝对路径同样触发),任何按路径读图的
|
|
701
|
+
* 工具都能接手,不出现任何具体插件名。
|
|
702
|
+
*
|
|
703
|
+
* 任何一步抛错都退回原内容:这个功能的失败**不该**把「发消息」本身弄坏。
|
|
704
|
+
*/
|
|
705
|
+
/**
|
|
706
|
+
* 交接文案用哪种语言。
|
|
707
|
+
*
|
|
708
|
+
* 判据是**发消息那个客户端的界面语言**,由 HTTP `Accept-Language` 头带进来
|
|
709
|
+
* (router → chat-adapter → 这里)。拿不到就退回中文 —— 那是这个功能既有的默认口径,
|
|
710
|
+
* 判断不出来的时候不该突然换语言。
|
|
711
|
+
*/
|
|
712
|
+
type HandoffLocale = 'zh' | 'en';
|
|
720
713
|
//#endregion
|
|
721
|
-
//#region src/host/
|
|
722
|
-
interface
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
settingsPath: string[];
|
|
735
|
-
active: boolean;
|
|
736
|
-
declared?: boolean;
|
|
714
|
+
//#region src/host/chat-adapter.d.ts
|
|
715
|
+
interface RpcResponse<T> {
|
|
716
|
+
result: {
|
|
717
|
+
ok: true;
|
|
718
|
+
value: T;
|
|
719
|
+
} | {
|
|
720
|
+
ok: false;
|
|
721
|
+
error: {
|
|
722
|
+
code: string;
|
|
723
|
+
message: string;
|
|
724
|
+
details?: unknown;
|
|
725
|
+
};
|
|
726
|
+
};
|
|
737
727
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
728
|
+
/**
|
|
729
|
+
* 一条 prompt 的内容片段 —— 与 DSH 0.2.x 的 `PromptContentPart` 一一对应。
|
|
730
|
+
*
|
|
731
|
+
* 三种形态的代价差别很大,值得写清楚:
|
|
732
|
+
* - `text` 直接发送;
|
|
733
|
+
* - `image` 字节**内联**在 payload 里(base64),一次调用完成,但请求体膨胀约 33%,
|
|
734
|
+
* 所以客户端必须先限制原图大小,别等 413;
|
|
735
|
+
* - `file` 的 `receiptId` 必须来自**先前的**上传调用,并在同一次 prompt 里绑定,
|
|
736
|
+
* 否则凭证会被回收。
|
|
737
|
+
*/
|
|
738
|
+
type PromptContentPart = {
|
|
739
|
+
type: 'text';
|
|
740
|
+
text: string;
|
|
741
|
+
} | {
|
|
742
|
+
type: 'image';
|
|
743
|
+
mediaType: string;
|
|
744
|
+
data: string;
|
|
745
|
+
name?: string;
|
|
746
|
+
} | {
|
|
747
|
+
type: 'file';
|
|
748
|
+
receiptId: string;
|
|
749
|
+
};
|
|
750
|
+
interface DshApiProxy {
|
|
751
|
+
respond(message: {
|
|
752
|
+
type: 'client-response';
|
|
753
|
+
rpcId: string;
|
|
754
|
+
result: {
|
|
755
|
+
ok: true;
|
|
756
|
+
value: unknown;
|
|
757
|
+
};
|
|
758
|
+
}): Promise<{
|
|
759
|
+
accepted: true;
|
|
760
|
+
} | {
|
|
761
|
+
accepted: false;
|
|
762
|
+
reason: 'not-pending' | 'bad-response';
|
|
763
|
+
}>;
|
|
764
|
+
sessions: {
|
|
765
|
+
list(request: {
|
|
766
|
+
rpcId: string;
|
|
767
|
+
payload: {
|
|
768
|
+
cursor?: string;
|
|
769
|
+
};
|
|
770
|
+
}): Promise<RpcResponse<{
|
|
771
|
+
items: DshSessionSummary[];
|
|
772
|
+
}>>;
|
|
773
|
+
create(request: {
|
|
774
|
+
rpcId: string;
|
|
775
|
+
payload: {
|
|
776
|
+
cwd?: string;
|
|
777
|
+
workspaceId?: string;
|
|
778
|
+
sessionId?: string;
|
|
779
|
+
agentPreset?: string;
|
|
780
|
+
};
|
|
781
|
+
}): Promise<RpcResponse<{
|
|
782
|
+
sessionId: string;
|
|
783
|
+
agentPreset?: string;
|
|
784
|
+
}>>;
|
|
785
|
+
history(request: {
|
|
786
|
+
rpcId: string;
|
|
787
|
+
payload: {
|
|
788
|
+
sessionId: string;
|
|
789
|
+
beforeSeq?: number;
|
|
790
|
+
maxMessages?: number;
|
|
791
|
+
};
|
|
792
|
+
}): Promise<RpcResponse<{
|
|
793
|
+
events: unknown[];
|
|
794
|
+
hasMore: boolean;
|
|
795
|
+
projections?: unknown;
|
|
796
|
+
}>>;
|
|
797
|
+
/**
|
|
798
|
+
* 提交一条 prompt。
|
|
799
|
+
*
|
|
800
|
+
* `locale` 不是内核字段,是**我们**加的:告诉图片交接「发消息的客户端界面是什么语言」,
|
|
801
|
+
* 好让交接说明说用户看得懂的话。它会一直传到 `dsh-0.2-bridge` 并在调内核前被摘掉。
|
|
802
|
+
*/
|
|
803
|
+
prompt(request: {
|
|
804
|
+
rpcId: string;
|
|
805
|
+
payload: {
|
|
806
|
+
sessionId: string;
|
|
807
|
+
mode: 'queue' | 'steer';
|
|
808
|
+
content: PromptContentPart[];
|
|
809
|
+
clientTimeZone?: string;
|
|
810
|
+
locale?: HandoffLocale;
|
|
811
|
+
};
|
|
812
|
+
}): Promise<RpcResponse<{
|
|
813
|
+
accepted: true;
|
|
814
|
+
command?: unknown;
|
|
815
|
+
}>>;
|
|
816
|
+
/**
|
|
817
|
+
* 上传一个文件,换取 prompt 可引用的凭证(`receiptId`)。
|
|
818
|
+
* `data` 是文件字节的**标准 base64**。
|
|
819
|
+
*/
|
|
820
|
+
uploadAttachment(request: {
|
|
821
|
+
rpcId: string;
|
|
822
|
+
payload: {
|
|
823
|
+
sessionId: string;
|
|
824
|
+
data: string;
|
|
825
|
+
name?: string;
|
|
826
|
+
};
|
|
827
|
+
}): Promise<RpcResponse<{
|
|
828
|
+
receiptId: string;
|
|
829
|
+
name: string | null;
|
|
830
|
+
}>>;
|
|
831
|
+
/**
|
|
832
|
+
* 按 id 取回附件字节。
|
|
833
|
+
*
|
|
834
|
+
* `attachmentId` 形如 `sha256:<hex>`(**含冒号**,客户端必须 URL 编码)。
|
|
835
|
+
* 返回 `Uint8Array` 而不是 base64:图片动辄上百 KB,多一次编解码纯属浪费。
|
|
836
|
+
*
|
|
837
|
+
* **必须带 `sessionId`**:内核要靠它读会话事件、取出这张图的真实 ref,
|
|
838
|
+
* 再据此做完整性校验(详见 chat-adapter 的 readAttachment 注释)。
|
|
839
|
+
*/
|
|
840
|
+
readAttachment(request: {
|
|
841
|
+
rpcId: string;
|
|
842
|
+
payload: {
|
|
843
|
+
sessionId: string;
|
|
844
|
+
attachmentId: string;
|
|
845
|
+
};
|
|
846
|
+
}): Promise<RpcResponse<{
|
|
847
|
+
bytes: Uint8Array;
|
|
848
|
+
mediaType: string;
|
|
849
|
+
}>>;
|
|
850
|
+
/**
|
|
851
|
+
* 取回一张**交接图片**的字节(纯文本模型发图时,图片被落成工作区文件)。
|
|
852
|
+
*
|
|
853
|
+
* 与 [readAttachment] 的区别:那一个取内核附件存储里的图(走
|
|
854
|
+
* `sessionController.attachment`),这一个取插件自己写进 `.dsh-images/`
|
|
855
|
+
* 的交接件。手机端用它把图片**显示**给人看;消息里的路径文本照旧留给模型。
|
|
856
|
+
*/
|
|
857
|
+
readHandoffImage(request: {
|
|
858
|
+
rpcId: string;
|
|
859
|
+
payload: {
|
|
860
|
+
sessionId: string;
|
|
861
|
+
name: string;
|
|
862
|
+
};
|
|
863
|
+
}): Promise<RpcResponse<{
|
|
864
|
+
bytes: Uint8Array;
|
|
865
|
+
mediaType: string;
|
|
866
|
+
}>>;
|
|
867
|
+
cancel(request: {
|
|
868
|
+
rpcId: string;
|
|
869
|
+
payload: {
|
|
870
|
+
sessionId: string;
|
|
871
|
+
};
|
|
872
|
+
}): Promise<RpcResponse<{
|
|
873
|
+
accepted: true;
|
|
874
|
+
}>>;
|
|
875
|
+
rename(request: {
|
|
741
876
|
rpcId: string;
|
|
742
|
-
payload:
|
|
877
|
+
payload: {
|
|
878
|
+
sessionId: string;
|
|
879
|
+
title: string;
|
|
880
|
+
};
|
|
743
881
|
}): Promise<RpcResponse<{
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
namespaces: SettingsNamespaceView[];
|
|
882
|
+
title: string;
|
|
883
|
+
seq: number;
|
|
747
884
|
}>>;
|
|
748
|
-
|
|
885
|
+
fork(request: {
|
|
749
886
|
rpcId: string;
|
|
750
887
|
payload: {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
op: 'set';
|
|
754
|
-
path: string[];
|
|
755
|
-
value: unknown;
|
|
756
|
-
} | {
|
|
757
|
-
op: 'unset';
|
|
758
|
-
path: string[];
|
|
759
|
-
}>;
|
|
760
|
-
expectedRevision?: number;
|
|
888
|
+
sessionId: string;
|
|
889
|
+
atSeq?: number;
|
|
761
890
|
};
|
|
762
|
-
}): Promise<RpcResponse<
|
|
891
|
+
}): Promise<RpcResponse<{
|
|
892
|
+
sessionId: string;
|
|
893
|
+
}>>;
|
|
894
|
+
models(request: {
|
|
895
|
+
rpcId: string;
|
|
896
|
+
payload: {
|
|
897
|
+
sessionId: string;
|
|
898
|
+
};
|
|
899
|
+
}): Promise<RpcResponse<SessionModelsView>>;
|
|
900
|
+
selectModel(request: {
|
|
901
|
+
rpcId: string;
|
|
902
|
+
payload: {
|
|
903
|
+
sessionId: string;
|
|
904
|
+
provider: string;
|
|
905
|
+
model: string;
|
|
906
|
+
reasoningEffort?: string;
|
|
907
|
+
};
|
|
908
|
+
}): Promise<RpcResponse<{
|
|
909
|
+
selected: ModelSelectionView;
|
|
910
|
+
}>>;
|
|
763
911
|
};
|
|
764
|
-
|
|
765
|
-
|
|
912
|
+
workspace: {
|
|
913
|
+
list(request: {
|
|
914
|
+
rpcId: string;
|
|
915
|
+
payload: Record<string, never>;
|
|
916
|
+
}): Promise<RpcResponse<{
|
|
917
|
+
items: DshWorkspaceSummary[];
|
|
918
|
+
archivedSessionIds: string[];
|
|
919
|
+
}>>;
|
|
920
|
+
create(request: {
|
|
766
921
|
rpcId: string;
|
|
767
922
|
payload: {
|
|
768
|
-
|
|
923
|
+
path: string;
|
|
769
924
|
};
|
|
770
925
|
}): Promise<RpcResponse<{
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
source?: string;
|
|
774
|
-
writable: boolean;
|
|
775
|
-
}>;
|
|
926
|
+
workspace: DshWorkspaceSummary;
|
|
927
|
+
created: boolean;
|
|
776
928
|
}>>;
|
|
777
|
-
|
|
929
|
+
rename(request: {
|
|
778
930
|
rpcId: string;
|
|
779
931
|
payload: {
|
|
780
|
-
|
|
781
|
-
|
|
932
|
+
workspaceId: string;
|
|
933
|
+
title: string;
|
|
782
934
|
};
|
|
783
|
-
}): Promise<RpcResponse<
|
|
784
|
-
|
|
935
|
+
}): Promise<RpcResponse<{
|
|
936
|
+
workspace: DshWorkspaceSummary;
|
|
937
|
+
}>>;
|
|
938
|
+
delete(request: {
|
|
785
939
|
rpcId: string;
|
|
786
940
|
payload: {
|
|
787
|
-
|
|
941
|
+
workspaceId: string;
|
|
788
942
|
};
|
|
789
|
-
}): Promise<RpcResponse<
|
|
943
|
+
}): Promise<RpcResponse<{
|
|
944
|
+
deleted: true;
|
|
945
|
+
}>>;
|
|
946
|
+
archiveSession(request: {
|
|
947
|
+
rpcId: string;
|
|
948
|
+
payload: {
|
|
949
|
+
sessionId: string;
|
|
950
|
+
};
|
|
951
|
+
}): Promise<RpcResponse<{
|
|
952
|
+
archivedSessionIds: string[];
|
|
953
|
+
}>>;
|
|
790
954
|
};
|
|
791
|
-
|
|
792
|
-
|
|
955
|
+
agentPresets: {
|
|
956
|
+
list(request: {
|
|
793
957
|
rpcId: string;
|
|
794
958
|
payload: Record<string, never>;
|
|
795
959
|
}): Promise<RpcResponse<{
|
|
796
|
-
|
|
960
|
+
presets: DshAgentPresetSummary[];
|
|
961
|
+
authorable: boolean;
|
|
962
|
+
hasDocument: boolean;
|
|
797
963
|
}>>;
|
|
798
|
-
|
|
964
|
+
select(request: {
|
|
799
965
|
rpcId: string;
|
|
800
|
-
payload:
|
|
966
|
+
payload: {
|
|
967
|
+
sessionId: string;
|
|
968
|
+
agentPreset: string;
|
|
969
|
+
};
|
|
801
970
|
}): Promise<RpcResponse<{
|
|
802
|
-
|
|
803
|
-
failures: {
|
|
804
|
-
provider: string;
|
|
805
|
-
message: string;
|
|
806
|
-
}[];
|
|
971
|
+
agentPreset: string;
|
|
807
972
|
}>>;
|
|
808
|
-
|
|
973
|
+
};
|
|
974
|
+
/**
|
|
975
|
+
* 用户问题(内核 `ctx.userQuestions`,即 ask_user_question 工具)。
|
|
976
|
+
*
|
|
977
|
+
* 为什么单开一个命名空间而不是塞进 `sessions`:它由**另一个内核服务**提供
|
|
978
|
+
* (`@deepseek-ai/dsh-user-questions`),在一条没装它的 profile 上是**可选能力** ——
|
|
979
|
+
* 分开之后,「这个能力不在」和「这个会话有问题」在类型上就分得开。
|
|
980
|
+
*/
|
|
981
|
+
userQuestions: {
|
|
982
|
+
/**
|
|
983
|
+
* 列出还能回答的问题(内核 `continued(agent)`)。
|
|
984
|
+
* 没有 live agent、或没有该服务时返回空列表 —— 冷会话是常态,不该报错。
|
|
985
|
+
*/
|
|
986
|
+
list(request: {
|
|
809
987
|
rpcId: string;
|
|
810
988
|
payload: {
|
|
811
|
-
|
|
812
|
-
provider?: string;
|
|
813
|
-
baseURL?: string;
|
|
814
|
-
api?: string;
|
|
815
|
-
apiKey?: string;
|
|
989
|
+
sessionId: string;
|
|
816
990
|
};
|
|
817
991
|
}): Promise<RpcResponse<{
|
|
818
|
-
|
|
992
|
+
items: UserQuestionView[];
|
|
819
993
|
}>>;
|
|
994
|
+
/** 提交答案;失败原因见 [UserQuestionAnswerOutcome]。 */
|
|
995
|
+
answer(request: {
|
|
996
|
+
rpcId: string;
|
|
997
|
+
payload: {
|
|
998
|
+
sessionId: string;
|
|
999
|
+
callId: string;
|
|
1000
|
+
answer: UserQuestionAnswer;
|
|
1001
|
+
};
|
|
1002
|
+
}): Promise<RpcResponse<UserQuestionAnswerOutcome>>;
|
|
1003
|
+
};
|
|
1004
|
+
events: {
|
|
1005
|
+
mux(request: {
|
|
1006
|
+
rpcId: string;
|
|
1007
|
+
payload: Record<string, never>;
|
|
1008
|
+
}, signal: AbortSignal): AsyncIterable<{
|
|
1009
|
+
rpcId: string;
|
|
1010
|
+
payload: Record<string, unknown>;
|
|
1011
|
+
}>;
|
|
1012
|
+
host(request: {
|
|
1013
|
+
rpcId: string;
|
|
1014
|
+
payload: Record<string, never>;
|
|
1015
|
+
}, signal: AbortSignal): AsyncIterable<{
|
|
1016
|
+
rpcId: string;
|
|
1017
|
+
payload: Record<string, unknown>;
|
|
1018
|
+
}>;
|
|
820
1019
|
};
|
|
821
1020
|
}
|
|
822
|
-
interface
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
contextWindow?: number;
|
|
826
|
-
maxTokens?: number;
|
|
827
|
-
}
|
|
828
|
-
interface ProviderConfigView {
|
|
829
|
-
baseURL?: string;
|
|
830
|
-
api?: string;
|
|
831
|
-
displayName?: string;
|
|
832
|
-
thinking?: string;
|
|
1021
|
+
interface ModelSelectionView {
|
|
1022
|
+
provider: string;
|
|
1023
|
+
model: string;
|
|
833
1024
|
reasoningEffort?: string;
|
|
834
|
-
models: ProviderModelView[];
|
|
835
|
-
modelsInherited?: boolean;
|
|
836
1025
|
}
|
|
837
|
-
interface
|
|
1026
|
+
interface ModelReasoningView {
|
|
1027
|
+
efforts: {
|
|
1028
|
+
id: string;
|
|
1029
|
+
name: string;
|
|
1030
|
+
description?: string;
|
|
1031
|
+
}[];
|
|
1032
|
+
defaultEffort?: string;
|
|
1033
|
+
}
|
|
1034
|
+
interface ModelProviderGroupView {
|
|
838
1035
|
id: string;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1036
|
+
name: string;
|
|
1037
|
+
models: {
|
|
1038
|
+
id: string;
|
|
1039
|
+
name: string;
|
|
1040
|
+
description?: string;
|
|
1041
|
+
contextWindow?: number;
|
|
1042
|
+
maxTokens?: number;
|
|
1043
|
+
reasoning?: ModelReasoningView;
|
|
1044
|
+
}[];
|
|
1045
|
+
}
|
|
1046
|
+
interface SessionModelsView {
|
|
1047
|
+
current: ModelSelectionView;
|
|
1048
|
+
routable: boolean;
|
|
1049
|
+
groups: ModelProviderGroupView[];
|
|
1050
|
+
failures: {
|
|
1051
|
+
provider: string;
|
|
1052
|
+
message: string;
|
|
1053
|
+
}[];
|
|
1054
|
+
}
|
|
1055
|
+
interface CommandDescriptorView {
|
|
1056
|
+
name: string;
|
|
1057
|
+
description: string;
|
|
1058
|
+
input?: {
|
|
1059
|
+
hint: string;
|
|
850
1060
|
};
|
|
851
|
-
config: ProviderConfigView;
|
|
852
1061
|
}
|
|
853
|
-
interface
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1062
|
+
interface CommandExecutionView {
|
|
1063
|
+
commandId: string;
|
|
1064
|
+
result: {
|
|
1065
|
+
kind: 'success' | 'error';
|
|
1066
|
+
text?: string;
|
|
1067
|
+
sourceEventSeq?: number;
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
interface DshCommandRuntime {
|
|
1071
|
+
list(agent: unknown): readonly CommandDescriptorView[];
|
|
1072
|
+
execute(agent: unknown, line: string, signal: AbortSignal): Promise<CommandExecutionView | undefined>;
|
|
1073
|
+
}
|
|
1074
|
+
interface DshAgentRegistry {
|
|
1075
|
+
get(sessionId: string): unknown | undefined;
|
|
1076
|
+
}
|
|
1077
|
+
interface DshCommandServices {
|
|
1078
|
+
commands: DshCommandRuntime;
|
|
1079
|
+
agents: DshAgentRegistry;
|
|
1080
|
+
}
|
|
1081
|
+
interface DshSessionSummary {
|
|
1082
|
+
sessionId: string;
|
|
1083
|
+
updatedAt: number;
|
|
1084
|
+
running: boolean;
|
|
1085
|
+
blank: boolean;
|
|
1086
|
+
cwd?: string;
|
|
1087
|
+
agentPreset?: string;
|
|
1088
|
+
parentSessionId?: string;
|
|
1089
|
+
origin?: 'subagent';
|
|
1090
|
+
projections?: {
|
|
1091
|
+
asOfSeq: number;
|
|
1092
|
+
values: Record<string, unknown>;
|
|
1093
|
+
};
|
|
862
1094
|
}
|
|
863
|
-
interface
|
|
1095
|
+
interface DshWorkspaceSummary {
|
|
1096
|
+
workspaceId: string;
|
|
1097
|
+
path: string;
|
|
1098
|
+
title: string;
|
|
1099
|
+
sessionIds: string[];
|
|
1100
|
+
createdAt: string;
|
|
1101
|
+
updatedAt: string;
|
|
1102
|
+
}
|
|
1103
|
+
interface DshAgentPresetSummary {
|
|
864
1104
|
id: string;
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
expectedRevision?: number;
|
|
1105
|
+
trust: 'system' | 'user';
|
|
1106
|
+
isDefault: boolean;
|
|
1107
|
+
name?: string;
|
|
1108
|
+
description?: string;
|
|
1109
|
+
broken?: string;
|
|
871
1110
|
}
|
|
872
|
-
interface
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1111
|
+
interface ChatSessionView {
|
|
1112
|
+
id: string;
|
|
1113
|
+
rootId: string;
|
|
1114
|
+
cwd: string;
|
|
1115
|
+
updatedAt: number;
|
|
1116
|
+
running: boolean;
|
|
1117
|
+
blank: boolean;
|
|
1118
|
+
title?: string;
|
|
1119
|
+
workspaceId?: string;
|
|
1120
|
+
workspaceTitle?: string;
|
|
1121
|
+
agentPreset?: string;
|
|
1122
|
+
parentSessionId?: string;
|
|
1123
|
+
origin?: 'subagent';
|
|
1124
|
+
pendingInteraction?: 'approval';
|
|
876
1125
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}>;
|
|
886
|
-
catalog(): Promise<{
|
|
887
|
-
groups: ModelProviderGroupView[];
|
|
888
|
-
failures: {
|
|
889
|
-
provider: string;
|
|
890
|
-
message: string;
|
|
891
|
-
}[];
|
|
892
|
-
}>;
|
|
893
|
-
updateProvider(providerId: string, patch: ProviderPatch): Promise<void>;
|
|
894
|
-
createProvider(input: CustomProviderCreate): Promise<void>;
|
|
895
|
-
removeProvider(providerId: string, expectedRevision?: number): Promise<void>;
|
|
896
|
-
discover(providerId: string, draft: {
|
|
897
|
-
baseURL?: string;
|
|
898
|
-
api?: string;
|
|
899
|
-
apiKey?: string;
|
|
900
|
-
}): Promise<ProviderModelView[]>;
|
|
901
|
-
private providerContext;
|
|
1126
|
+
interface PendingApprovalView {
|
|
1127
|
+
id: string;
|
|
1128
|
+
sessionId: string;
|
|
1129
|
+
toolName: string;
|
|
1130
|
+
reason?: string;
|
|
1131
|
+
detail?: string;
|
|
1132
|
+
risk: 'standard' | 'full-access';
|
|
1133
|
+
requestedAt: number;
|
|
902
1134
|
}
|
|
903
|
-
//#endregion
|
|
904
|
-
//#region src/host/dsh-0.2-bridge.d.ts
|
|
905
|
-
/**
|
|
906
|
-
* Prompt content a 0.2.x Host accepts — mirrors its `PromptContentPart` union.
|
|
907
|
-
*
|
|
908
|
-
* 权威定义在 `chat-adapter.ts`(协议层),这里只是复用它,避免两处各写一遍
|
|
909
|
-
* 然后悄悄漂移。
|
|
910
|
-
*/
|
|
911
|
-
type PromptContentPartLike = PromptContentPart;
|
|
912
1135
|
/**
|
|
913
|
-
* `
|
|
1136
|
+
* 一条「还能回答」的用户问题 —— 内核 `userQuestions.continued()` 的一行。
|
|
914
1137
|
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
1138
|
+
* 为什么类型是宽松的、而不是照抄内核的 `AskUserQuestionItem`:内核往 item 或
|
|
1139
|
+
* question 上加过字段(`intent` 就是一例),手机端该自己决定要不要用,
|
|
1140
|
+
* 不该等插件跟着发版。所以这里只钉住三个**契约字段**,其余原样透传。
|
|
917
1141
|
*/
|
|
918
|
-
interface
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1142
|
+
interface UserQuestionView {
|
|
1143
|
+
callId: string;
|
|
1144
|
+
/** 内核 `UserQuestionState`:'open' 或 'continued';本端点只返回 continued。 */
|
|
1145
|
+
state: string;
|
|
1146
|
+
/** 问题项**原样**透传(id / question / detail? / header? / options? / multiSelect? / intent?)。 */
|
|
1147
|
+
questions: unknown[];
|
|
1148
|
+
/** 内核以后加的字段照样透传,不在这里裁剪。 */
|
|
1149
|
+
[key: string]: unknown;
|
|
1150
|
+
}
|
|
1151
|
+
/** 一条问题的答案(内核 `AskUserQuestionAnswerItem`)。 */
|
|
1152
|
+
interface UserQuestionAnswerItem {
|
|
1153
|
+
/** 被回答的问题 id,必须与请求里的问题 id 对上。 */
|
|
1154
|
+
id: string;
|
|
1155
|
+
/** 选中的选项 label;多选时可能多项。 */
|
|
1156
|
+
selected: string[];
|
|
1157
|
+
/** 可选的「其他」自由文本。 */
|
|
1158
|
+
custom?: string;
|
|
1159
|
+
}
|
|
1160
|
+
/** 一次提交的完整答案批次(内核 `AskUserQuestionAnswer`):一次调用里的每个问题各占一项。 */
|
|
1161
|
+
interface UserQuestionAnswer {
|
|
1162
|
+
answers: UserQuestionAnswerItem[];
|
|
932
1163
|
}
|
|
933
1164
|
/**
|
|
934
|
-
*
|
|
1165
|
+
* 「把答案送进内核」的结果。
|
|
935
1166
|
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
1167
|
+
* 刻意**不走 RpcResponse 的 error 通道**:`unwrapDsh` 只认 `not-found` /
|
|
1168
|
+
* `busy` / `locked` 三种 code 去映射 HTTP 状态,其余一律 400 —— 而这里需要区分
|
|
1169
|
+
* 409 / 400 / 503。做成**值**之后由 adapter 决定 HTTP 状态,
|
|
1170
|
+
* 和 approvals 的 `respond()` 返回 `{ accepted: false, reason }` 是同一个套路。
|
|
940
1171
|
*/
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1172
|
+
type UserQuestionAnswerFailure =
|
|
1173
|
+
/** 内核里没有这个 callId 的悬空问题(`answer()` 返回了 false)。 */
|
|
1174
|
+
'not-pending' |
|
|
1175
|
+
/** 已经有一条回复在排队(内核 `REPLY_QUEUED`)。重复提交不是服务器故障。 */
|
|
1176
|
+
'reply-queued' |
|
|
1177
|
+
/** 答案批次没有「一问一答」地对齐(内核 `BAD_ANSWER`)。 */
|
|
1178
|
+
'answer-invalid' |
|
|
1179
|
+
/** 这个 agent 现在不接受人工回答(非 live root / 被别的 agent 拥有),或会话没有 live agent。 */
|
|
1180
|
+
'not-answerable' |
|
|
1181
|
+
/** 这套 DSH 构建里压根没有 `ctx.userQuestions` 服务。 */
|
|
1182
|
+
'unavailable';
|
|
1183
|
+
type UserQuestionAnswerOutcome = {
|
|
1184
|
+
accepted: true;
|
|
1185
|
+
} | {
|
|
1186
|
+
accepted: false;
|
|
1187
|
+
reason: UserQuestionAnswerFailure;
|
|
1188
|
+
};
|
|
1189
|
+
interface ChatWorkspaceView {
|
|
1190
|
+
id: string;
|
|
1191
|
+
title: string;
|
|
1192
|
+
rootId: string;
|
|
1193
|
+
path: string;
|
|
1194
|
+
createdAt: string;
|
|
1195
|
+
updatedAt: string;
|
|
963
1196
|
}
|
|
964
|
-
interface
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1197
|
+
interface AgentPresetView {
|
|
1198
|
+
id: string;
|
|
1199
|
+
name: string;
|
|
1200
|
+
description?: string;
|
|
1201
|
+
trust: 'system' | 'user';
|
|
1202
|
+
isDefault: boolean;
|
|
1203
|
+
available: boolean;
|
|
1204
|
+
}
|
|
1205
|
+
declare class DshChatAdapter {
|
|
1206
|
+
private readonly api;
|
|
1207
|
+
private readonly database;
|
|
1208
|
+
private readonly commandServices?;
|
|
1209
|
+
private readonly sessionRootIds;
|
|
1210
|
+
private readonly sessionRootLookups;
|
|
1211
|
+
private readonly pendingApprovals;
|
|
1212
|
+
constructor(api: DshApiProxy, database: WorkspaceDatabase, commandServices?: DshCommandServices | undefined);
|
|
1213
|
+
listSessions(principal: Principal): Promise<ChatSessionView[]>;
|
|
1214
|
+
listWorkspaces(principal: Principal): Promise<ChatWorkspaceView[]>;
|
|
1215
|
+
createWorkspace(principal: Principal, rootId: string, relativePath: string): Promise<ChatWorkspaceView>;
|
|
1216
|
+
renameWorkspace(principal: Principal, workspaceId: string, title: string): Promise<ChatWorkspaceView>;
|
|
1217
|
+
deleteWorkspace(principal: Principal, workspaceId: string): Promise<void>;
|
|
1218
|
+
listAgentPresets(): Promise<AgentPresetView[]>;
|
|
1219
|
+
selectAgentPreset(principal: Principal, sessionId: string, agentPreset: string): Promise<string>;
|
|
1220
|
+
createSessionInWorkspace(principal: Principal, workspaceId: string, options: {
|
|
973
1221
|
sessionId?: string;
|
|
974
1222
|
agentPreset?: string;
|
|
975
1223
|
}): Promise<{
|
|
976
|
-
|
|
977
|
-
agentPreset?: string;
|
|
978
|
-
}>;
|
|
979
|
-
/**
|
|
980
|
-
* Live follow stream. Its first frame is always a `snapshot` carrying the
|
|
981
|
-
* opening window (`cursor`, `records`, `hasMore`), and that is the only
|
|
982
|
-
* reliable way to read history: `page()` demands a `throughSeq` "obtained from
|
|
983
|
-
* the corresponding follow opening frame", so guessing one (e.g. `-1`) returns
|
|
984
|
-
* a well-formed but *empty* page — a silent failure, not an error.
|
|
985
|
-
*/
|
|
986
|
-
follow(request: {
|
|
987
|
-
address: {
|
|
988
|
-
kind: 'session';
|
|
989
|
-
sessionId: string;
|
|
990
|
-
};
|
|
991
|
-
maxMessages?: number;
|
|
992
|
-
/**
|
|
993
|
-
* Assistant presentation frames are opt-in: without `assistantStream: true`
|
|
994
|
-
* the host emits durable events only, so a live stream would carry no
|
|
995
|
-
* incremental text at all (the transcript then appears only after commit).
|
|
996
|
-
*/
|
|
997
|
-
assistantStream?: true;
|
|
998
|
-
}, signal: AbortSignal): AsyncIterable<{
|
|
999
|
-
type?: string;
|
|
1000
|
-
cursor?: number;
|
|
1001
|
-
records?: readonly unknown[];
|
|
1002
|
-
hasMore?: boolean;
|
|
1003
|
-
}>;
|
|
1004
|
-
prompt(request: {
|
|
1005
|
-
requestId: string;
|
|
1006
|
-
sessionId: string;
|
|
1007
|
-
mode: 'queue' | 'steer';
|
|
1008
|
-
content: readonly PromptContentPartLike[];
|
|
1009
|
-
clientTimeZone?: string;
|
|
1010
|
-
}, signal: AbortSignal): Promise<{
|
|
1011
|
-
accepted: true;
|
|
1224
|
+
id: string;
|
|
1225
|
+
agentPreset?: string;
|
|
1012
1226
|
}>;
|
|
1013
|
-
|
|
1014
|
-
sessionId
|
|
1015
|
-
|
|
1016
|
-
accepted: true;
|
|
1017
|
-
};
|
|
1018
|
-
rename(request: {
|
|
1019
|
-
sessionId: string;
|
|
1020
|
-
title: string;
|
|
1227
|
+
createSession(principal: Principal, rootId: string, relativePath: string, options: {
|
|
1228
|
+
sessionId?: string;
|
|
1229
|
+
agentPreset?: string;
|
|
1021
1230
|
}): Promise<{
|
|
1022
|
-
|
|
1023
|
-
|
|
1231
|
+
id: string;
|
|
1232
|
+
agentPreset?: string;
|
|
1024
1233
|
}>;
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1234
|
+
history(principal: Principal, sessionId: string, options: {
|
|
1235
|
+
beforeSeq?: number;
|
|
1236
|
+
maxMessages?: number;
|
|
1028
1237
|
}): Promise<{
|
|
1029
|
-
|
|
1238
|
+
events: unknown[];
|
|
1239
|
+
hasMore: boolean;
|
|
1240
|
+
projections?: unknown;
|
|
1241
|
+
}>;
|
|
1242
|
+
prompt(principal: Principal, sessionId: string, parts: PromptContentPart[], mode: 'queue' | 'steer', clientTimeZone?: string, locale?: HandoffLocale): Promise<{
|
|
1243
|
+
accepted: true;
|
|
1244
|
+
command?: unknown;
|
|
1030
1245
|
}>;
|
|
1031
1246
|
/**
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
* 这是取历史图片的**正路**,它内部一次做完三件事:校验会话确实引用了这张图、
|
|
1035
|
-
* 调 `ctx.attachments.readImage`、用**会话事件里那条真实 ref**做完整性校验
|
|
1036
|
-
*(`mediaType` / `bytes` / `width` / `height` 四项逐项比对)。
|
|
1037
|
-
*
|
|
1038
|
-
* 为什么必须走它、不能自己调 `ctx.attachments`:实测本机这条 profile 里
|
|
1039
|
-
* `ctx.attachments` 是 **MISSING**(诊断日志
|
|
1040
|
-
* `ctx.attachments MISSING(所以改走内核 sessionController.readAttachment)`),
|
|
1041
|
-
* 自己拼 ref 那条路根本走不通;而且绕开内核就得把它内部三件事全复刻一遍,
|
|
1042
|
-
* 早先正是只传了 id、缺四项元数据,导致 `ATTACHMENT_CORRUPT`。
|
|
1247
|
+
* 上传一个附件,返回 prompt 可以引用的凭证。
|
|
1043
1248
|
*
|
|
1044
|
-
*
|
|
1045
|
-
|
|
1046
|
-
* `data` 是 **base64 字符串**(主机端实现里 `Buffer.from(stored.data).toString("base64")`),
|
|
1047
|
-
* 不是 Uint8Array —— 调用方要自己解码。
|
|
1249
|
+
* 为什么需要这一步而不是让客户端直接把字节塞进消息:内核对**文件**类型要求的是
|
|
1250
|
+
* `receiptId`(由上传换取),只有**图片**才允许内联 base64。
|
|
1048
1251
|
*/
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
}): Promise<{
|
|
1053
|
-
attachment: {
|
|
1054
|
-
mediaType: string;
|
|
1055
|
-
};
|
|
1056
|
-
data: string;
|
|
1057
|
-
}>;
|
|
1058
|
-
selectModel(request: {
|
|
1059
|
-
sessionId: string;
|
|
1060
|
-
provider: string;
|
|
1061
|
-
model: string;
|
|
1062
|
-
reasoningEffort?: string;
|
|
1063
|
-
}): Promise<{
|
|
1064
|
-
selected: unknown;
|
|
1252
|
+
uploadAttachment(principal: Principal, sessionId: string, data: string, name?: string): Promise<{
|
|
1253
|
+
receiptId: string;
|
|
1254
|
+
name: string | null;
|
|
1065
1255
|
}>;
|
|
1066
1256
|
/**
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
* `
|
|
1257
|
+
* 取回会话历史里某个图片附件的字节。
|
|
1258
|
+
*
|
|
1259
|
+
* **为什么会走内核的 `sessionController.attachment()`,而不自己调
|
|
1260
|
+
* `ctx.attachments.readImage()`**:
|
|
1261
|
+
* 1. 实测本机 profile 里 `ctx.attachments` 是 **MISSING**(拿不到注入属性)——
|
|
1262
|
+
* 自己拼 ref 那条路根本走不通;
|
|
1263
|
+
* 2. 内核那个方法内部一次做完三件事:校验该会话确实引用了这张图、
|
|
1264
|
+
* 读出字节、用**会话事件里那条真实 ref** 做完整性校验
|
|
1265
|
+
* (`mediaType` / `bytes` / `width` / `height` 四项逐项比对)。
|
|
1266
|
+
* 绕开它就得把这三件事全复刻 —— 先前正是漏了第三件,于是每张图
|
|
1267
|
+
* `ATTACHMENT_CORRUPT`:「字节读到了,却被校验拒之门外」。
|
|
1268
|
+
*
|
|
1269
|
+
* 因此 **sessionId 是必需参数**(内核要拿它读会话事件、找到那张图的真实 ref);
|
|
1270
|
+
* 早先设计的「客户端把四项元数据带上来」是多余的 —— 内核手里本来就有。
|
|
1071
1271
|
*/
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
groups?: readonly unknown[];
|
|
1076
|
-
failures?: readonly {
|
|
1077
|
-
id?: unknown;
|
|
1078
|
-
name?: unknown;
|
|
1079
|
-
message?: unknown;
|
|
1080
|
-
}[];
|
|
1272
|
+
readAttachment(principal: Principal, sessionId: string, attachmentId: string): Promise<{
|
|
1273
|
+
bytes: Uint8Array;
|
|
1274
|
+
mediaType: string;
|
|
1081
1275
|
}>;
|
|
1082
1276
|
/**
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
* (`values.modelSelection`).
|
|
1277
|
+
* 取回一张**交接图片**(纯文本模型发图时落在工作区 `.dsh-images/` 的那份)。
|
|
1278
|
+
* 与 [readAttachment] 同样要求会话归属;文件名在桥里还有一道白名单校验。
|
|
1086
1279
|
*/
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
asOfSeq?: number;
|
|
1091
|
-
values?: Record<string, unknown>;
|
|
1092
|
-
} | null>;
|
|
1093
|
-
resolveAgent(sessionId: string): Promise<unknown>;
|
|
1094
|
-
}
|
|
1095
|
-
interface WorkspaceControllerLike {
|
|
1096
|
-
create(request: {
|
|
1097
|
-
path: string;
|
|
1098
|
-
}): Promise<{
|
|
1099
|
-
workspace: unknown;
|
|
1100
|
-
created: boolean;
|
|
1280
|
+
readHandoffImage(principal: Principal, sessionId: string, name: string): Promise<{
|
|
1281
|
+
bytes: Uint8Array;
|
|
1282
|
+
mediaType: string;
|
|
1101
1283
|
}>;
|
|
1102
|
-
|
|
1103
|
-
|
|
1284
|
+
cancel(principal: Principal, sessionId: string): Promise<void>;
|
|
1285
|
+
renameSession(principal: Principal, sessionId: string, title: string): Promise<{
|
|
1104
1286
|
title: string;
|
|
1105
|
-
|
|
1106
|
-
delete(request: {
|
|
1107
|
-
workspaceId: string;
|
|
1108
|
-
}): Promise<{
|
|
1109
|
-
deleted: boolean;
|
|
1110
|
-
}>;
|
|
1111
|
-
archiveSession(request: {
|
|
1112
|
-
sessionId: string;
|
|
1113
|
-
}): Promise<{
|
|
1114
|
-
archivedSessionIds: readonly string[];
|
|
1287
|
+
seq: number;
|
|
1115
1288
|
}>;
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
path: string;
|
|
1121
|
-
title: string;
|
|
1122
|
-
createdAt: string;
|
|
1123
|
-
updatedAt: string;
|
|
1124
|
-
sessionIds?: readonly string[];
|
|
1125
|
-
}[];
|
|
1126
|
-
}
|
|
1127
|
-
interface SettingsControllerLike {
|
|
1128
|
-
describe(): {
|
|
1129
|
-
writable: boolean;
|
|
1130
|
-
hasDocument: boolean;
|
|
1131
|
-
namespaces: readonly unknown[];
|
|
1132
|
-
};
|
|
1133
|
-
mutate(ns: string, ops: readonly unknown[], expectedRevision: number | undefined): Promise<unknown>;
|
|
1134
|
-
}
|
|
1135
|
-
interface CredentialsControllerLike {
|
|
1136
|
-
describe(refs: readonly string[]): Promise<Record<string, unknown>>;
|
|
1137
|
-
set(ref: string, value: string): Promise<void>;
|
|
1138
|
-
unset(ref: string): Promise<void>;
|
|
1139
|
-
}
|
|
1140
|
-
interface AgentPresetsLike {
|
|
1141
|
-
list(): Promise<readonly unknown[]>;
|
|
1142
|
-
remoteExportList(): Promise<unknown>;
|
|
1143
|
-
resolveAgent?: never;
|
|
1144
|
-
select(agent: unknown, agentPreset: string): Promise<string>;
|
|
1145
|
-
}
|
|
1146
|
-
interface LlmLike {
|
|
1147
|
-
/** Live routes, as `{ id, name }` rows. */
|
|
1148
|
-
listProviders(): readonly {
|
|
1149
|
-
id: string;
|
|
1150
|
-
}[];
|
|
1151
|
-
/**
|
|
1152
|
-
* Every route an adapter plugin could activate through configuration, live or
|
|
1153
|
-
* dormant, carrying the settings namespace and path that configures it.
|
|
1154
|
-
* Configuration surfaces merge this with `listProviders()` for liveness.
|
|
1155
|
-
*/
|
|
1156
|
-
listConfigurableProviders(): readonly {
|
|
1157
|
-
provider: string;
|
|
1158
|
-
displayName: string;
|
|
1159
|
-
settingsNs: string;
|
|
1160
|
-
settingsPath: readonly string[];
|
|
1161
|
-
declared?: boolean;
|
|
1162
|
-
error?: string;
|
|
1163
|
-
}[];
|
|
1164
|
-
discoverModels(settingsNs: string, request: unknown, signal?: AbortSignal): Promise<readonly unknown[]>;
|
|
1289
|
+
forkSession(principal: Principal, sessionId: string, atSeq?: number): Promise<string>;
|
|
1290
|
+
archiveSession(principal: Principal, sessionId: string): Promise<string[]>;
|
|
1291
|
+
listPendingApprovals(principal: Principal, sessionId: string): Promise<PendingApprovalView[]>;
|
|
1292
|
+
decideApproval(principal: Principal, sessionId: string, approvalId: string, outcome: 'allowed-once' | 'rejected'): Promise<PendingApprovalView>;
|
|
1165
1293
|
/**
|
|
1166
|
-
*
|
|
1294
|
+
* 列出「agent 正在等、且还能回答」的用户问题。
|
|
1167
1295
|
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1170
|
-
* `this.ctx.llm.resolveModelInfo(...)` 再查 `inputModalities`)。入口要与内核
|
|
1171
|
-
* 得出同一个结论,只能查同一份元数据 —— 名字、前缀、家族表都是启发式,会漂。
|
|
1296
|
+
* 口径与 [listPendingApprovals] 一致:先 requireSession(会话对本设备不可见 → 404),
|
|
1297
|
+
* 再问内核;字段一个都不裁 —— 客户端照着渲染,内核加字段不用改插件。
|
|
1172
1298
|
*/
|
|
1173
|
-
|
|
1174
|
-
inputModalities?: readonly string[];
|
|
1175
|
-
} | undefined>;
|
|
1176
|
-
}
|
|
1177
|
-
/** Host services the bridge consumes; assembled by `src/index.ts` from `ctx`. */
|
|
1178
|
-
interface Dsh02HostServices {
|
|
1179
|
-
sessionController: SessionControllerLike;
|
|
1180
|
-
workspaceController: WorkspaceControllerLike;
|
|
1181
|
-
workspaceRegistry: WorkspaceRegistryLike;
|
|
1182
|
-
settingsController: SettingsControllerLike;
|
|
1183
|
-
credentialsController: CredentialsControllerLike;
|
|
1184
|
-
agentPresets: AgentPresetsLike;
|
|
1185
|
-
llm: LlmLike;
|
|
1299
|
+
listUserQuestions(principal: Principal, sessionId: string): Promise<UserQuestionView[]>;
|
|
1186
1300
|
/**
|
|
1187
|
-
*
|
|
1301
|
+
* 把一次答案提交给内核。
|
|
1188
1302
|
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
* 挂在服务端、而且只在真的去取图时才炸,所以很难在本地测出来。
|
|
1303
|
+
* 失败原因全部由 `api.userQuestions.answer` 以**值**给出(见 [UserQuestionAnswerOutcome]),
|
|
1304
|
+
* 这里翻成 HTTP 语义:503 能力缺失 / 409 状态冲突 / 400 批次不合法。
|
|
1305
|
+
* 为什么不在 bridge 里直接抛:`unwrapDsh` 只把 `not-found` / `busy` / `locked`
|
|
1306
|
+
* 映成 404/409,其余一律 400 —— 表达不了这几种区别。
|
|
1194
1307
|
*/
|
|
1195
|
-
|
|
1308
|
+
answerUserQuestion(principal: Principal, sessionId: string, callId: string, answer: UserQuestionAnswer): Promise<void>;
|
|
1309
|
+
models(principal: Principal, sessionId: string): Promise<SessionModelsView>;
|
|
1310
|
+
selectModel(principal: Principal, sessionId: string, selection: ModelSelectionView): Promise<ModelSelectionView>;
|
|
1311
|
+
listCommands(principal: Principal, sessionId: string): Promise<CommandDescriptorView[]>;
|
|
1312
|
+
executeCommand(principal: Principal, sessionId: string, line: string): Promise<CommandExecutionView>;
|
|
1313
|
+
isSessionAuthorized(principal: Principal, sessionId: string): Promise<boolean>;
|
|
1314
|
+
startEvents(signal: AbortSignal, emit: (event: WorkspaceEvent) => void): void;
|
|
1315
|
+
private captureApproval;
|
|
1316
|
+
private hasPendingApproval;
|
|
1196
1317
|
/**
|
|
1197
|
-
*
|
|
1318
|
+
* Resolve the Agent backing a session for command work.
|
|
1198
1319
|
*
|
|
1199
|
-
*
|
|
1200
|
-
*
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
/**
|
|
1204
|
-
* Cordis event bus of the host context. Used for the two facts the old
|
|
1205
|
-
* `apiProxy` event streams carried that no controller returns: session
|
|
1206
|
-
* registry changes and the approval answerer waterfall.
|
|
1320
|
+
* Returns `undefined` when the session exists but cannot be activated. 0.2.x
|
|
1321
|
+
* rejects activation for a Session owned by a subagent, so that outcome is a
|
|
1322
|
+
* property of the session rather than a failure of this call: the caller
|
|
1323
|
+
* decides whether it means "no commands" (listing) or an error (execution).
|
|
1207
1324
|
*/
|
|
1208
|
-
|
|
1325
|
+
private commandAgent;
|
|
1326
|
+
private requireSession;
|
|
1327
|
+
private requireWorkspace;
|
|
1328
|
+
private visibleWorkspaces;
|
|
1329
|
+
private requireAuthorizedRoot;
|
|
1330
|
+
private authorizedRoots;
|
|
1331
|
+
private sessionRootId;
|
|
1209
1332
|
}
|
|
1210
1333
|
//#endregion
|
|
1211
1334
|
//#region src/host/rate-limit.d.ts
|
|
@@ -1322,6 +1445,13 @@ interface RouterOptions {
|
|
|
1322
1445
|
settings: DshSettingsAdapter;
|
|
1323
1446
|
remote: RemoteControl;
|
|
1324
1447
|
maxUploadBytes: number;
|
|
1448
|
+
/**
|
|
1449
|
+
* 解析不出会话工作目录时的图片兜底目录(设置面板里只读展示)。
|
|
1450
|
+
*
|
|
1451
|
+
* 可选:默认按 `database.dataDir` 推出来,测试因此不必每条都补一个字段。
|
|
1452
|
+
* 生产路径由 `index.ts` 传入与交接实际使用**同一个**目录,避免「界面显示 A、落盘在 B」。
|
|
1453
|
+
*/
|
|
1454
|
+
imageFallbackDir?: string;
|
|
1325
1455
|
}
|
|
1326
1456
|
declare class ApiRouter {
|
|
1327
1457
|
private readonly options;
|
|
@@ -1333,6 +1463,8 @@ declare class ApiRouter {
|
|
|
1333
1463
|
private dispatchContent;
|
|
1334
1464
|
private requireRoot;
|
|
1335
1465
|
private authorizedRoots;
|
|
1466
|
+
/** 兜底交接目录:没配就用状态目录下的 `image-handoff`(与 `index.ts` 的默认一致)。 */
|
|
1467
|
+
private imageFallbackDir;
|
|
1336
1468
|
private audit;
|
|
1337
1469
|
private readIdempotent;
|
|
1338
1470
|
private saveIdempotent;
|
|
@@ -1388,13 +1520,19 @@ declare class RemoteApiServer implements RemoteControl {
|
|
|
1388
1520
|
private eventAllowed;
|
|
1389
1521
|
private trimOperations;
|
|
1390
1522
|
}
|
|
1523
|
+
/** 一条挂在 DSH webserver 上的插件自有路由。 */
|
|
1524
|
+
interface EmbeddedRoute {
|
|
1525
|
+
kind: 'exact' | 'prefix';
|
|
1526
|
+
path: string;
|
|
1527
|
+
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
|
1528
|
+
}
|
|
1391
1529
|
declare function attachEmbeddedRoutes(webServer: {
|
|
1392
1530
|
register(route: {
|
|
1393
1531
|
kind: 'exact' | 'prefix';
|
|
1394
1532
|
path: string;
|
|
1395
1533
|
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
|
1396
1534
|
}): () => void;
|
|
1397
|
-
}, router: ApiRouter, remote: RemoteApiServer): () => void;
|
|
1535
|
+
}, router: ApiRouter, remote: RemoteApiServer, extraRoutes?: readonly EmbeddedRoute[]): () => void;
|
|
1398
1536
|
//#endregion
|
|
1399
1537
|
//#region src/index.d.ts
|
|
1400
1538
|
/**
|