dsh-remote-bridge 2.2.4 → 2.3.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 +30 -13
- package/README.md +34 -22
- 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 +133 -2
- package/lib/index.js +498 -45
- 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,6 +184,22 @@ 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
205
|
//#region src/host/settings-adapter.d.ts
|
|
@@ -415,6 +512,25 @@ interface SessionControllerLike {
|
|
|
415
512
|
}): Promise<{
|
|
416
513
|
sessionId: string;
|
|
417
514
|
}>;
|
|
515
|
+
/**
|
|
516
|
+
* Read one message-aligned history page without activating an Agent.
|
|
517
|
+
*
|
|
518
|
+
* `throughSeq` must be obtained from the corresponding follow opening frame's
|
|
519
|
+
* `cursor`; hard-coding one returns a well-formed but empty page.
|
|
520
|
+
* `beforeSeq` is optional and acts as an earlier boundary.
|
|
521
|
+
*/
|
|
522
|
+
page(request: {
|
|
523
|
+
address: {
|
|
524
|
+
kind: 'session';
|
|
525
|
+
sessionId: string;
|
|
526
|
+
};
|
|
527
|
+
throughSeq: number;
|
|
528
|
+
beforeSeq?: number;
|
|
529
|
+
maxMessages?: number;
|
|
530
|
+
}, signal: AbortSignal): Promise<{
|
|
531
|
+
records: readonly unknown[];
|
|
532
|
+
hasMore: boolean;
|
|
533
|
+
}>;
|
|
418
534
|
/**
|
|
419
535
|
* 取一张**被这个会话引用过**的图片字节(内核主机端方法,名字就叫 `attachment`)。
|
|
420
536
|
*
|
|
@@ -1348,6 +1464,13 @@ interface RouterOptions {
|
|
|
1348
1464
|
settings: DshSettingsAdapter;
|
|
1349
1465
|
remote: RemoteControl;
|
|
1350
1466
|
maxUploadBytes: number;
|
|
1467
|
+
/**
|
|
1468
|
+
* 解析不出会话工作目录时的图片兜底目录(设置面板里只读展示)。
|
|
1469
|
+
*
|
|
1470
|
+
* 可选:默认按 `database.dataDir` 推出来,测试因此不必每条都补一个字段。
|
|
1471
|
+
* 生产路径由 `index.ts` 传入与交接实际使用**同一个**目录,避免「界面显示 A、落盘在 B」。
|
|
1472
|
+
*/
|
|
1473
|
+
imageFallbackDir?: string;
|
|
1351
1474
|
}
|
|
1352
1475
|
declare class ApiRouter {
|
|
1353
1476
|
private readonly options;
|
|
@@ -1359,6 +1482,8 @@ declare class ApiRouter {
|
|
|
1359
1482
|
private dispatchContent;
|
|
1360
1483
|
private requireRoot;
|
|
1361
1484
|
private authorizedRoots;
|
|
1485
|
+
/** 兜底交接目录:没配就用状态目录下的 `image-handoff`(与 `index.ts` 的默认一致)。 */
|
|
1486
|
+
private imageFallbackDir;
|
|
1362
1487
|
private audit;
|
|
1363
1488
|
private readIdempotent;
|
|
1364
1489
|
private saveIdempotent;
|
|
@@ -1414,13 +1539,19 @@ declare class RemoteApiServer implements RemoteControl {
|
|
|
1414
1539
|
private eventAllowed;
|
|
1415
1540
|
private trimOperations;
|
|
1416
1541
|
}
|
|
1542
|
+
/** 一条挂在 DSH webserver 上的插件自有路由。 */
|
|
1543
|
+
interface EmbeddedRoute {
|
|
1544
|
+
kind: 'exact' | 'prefix';
|
|
1545
|
+
path: string;
|
|
1546
|
+
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
|
1547
|
+
}
|
|
1417
1548
|
declare function attachEmbeddedRoutes(webServer: {
|
|
1418
1549
|
register(route: {
|
|
1419
1550
|
kind: 'exact' | 'prefix';
|
|
1420
1551
|
path: string;
|
|
1421
1552
|
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
|
1422
1553
|
}): () => void;
|
|
1423
|
-
}, router: ApiRouter, remote: RemoteApiServer): () => void;
|
|
1554
|
+
}, router: ApiRouter, remote: RemoteApiServer, extraRoutes?: readonly EmbeddedRoute[]): () => void;
|
|
1424
1555
|
//#endregion
|
|
1425
1556
|
//#region src/index.d.ts
|
|
1426
1557
|
/**
|