@jingyi0605/codingns4dsh 0.1.7-rc.3 → 0.2.0-beta.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/LICENSE +695 -0
- package/README.en.md +23 -9
- package/README.md +23 -9
- package/data/build/dist/bootstrap/dsh-connection-adapter.d.ts +1 -1
- package/data/build/dist/bootstrap/index.d.ts +1 -1
- package/data/build/dist/client/bundle.js +1200 -170
- package/data/build/dist/client/context-breakdown-dom.d.ts +53 -0
- package/data/build/dist/client/context-breakdown-dom.js +171 -0
- package/data/build/dist/client/dsh-h5-bootstrap.js +7 -1
- package/data/build/dist/client/features/cli-adapters.js +11 -0
- package/data/build/dist/client/features/index.d.ts +1 -0
- package/data/build/dist/client/features/index.js +1 -0
- package/data/build/dist/client/features/subscription-usage-panel.d.ts +15 -0
- package/data/build/dist/client/features/subscription-usage-panel.js +117 -0
- package/data/build/dist/client/features/workspace-session-enhancement-panel.js +22 -5
- package/data/build/dist/client/features/workspace-session-enhancement.js +17 -2
- package/data/build/dist/client/git-management.js +80 -10
- package/data/build/dist/client/index.d.ts +1 -0
- package/data/build/dist/client/index.js +16 -49
- package/data/build/dist/client/locale.js +20 -2
- package/data/build/dist/client/remote-web-context.js +26 -2
- package/data/build/dist/client/settings-bridge.d.ts +20 -5
- package/data/build/dist/client/settings-bridge.js +30 -7
- package/data/build/dist/client/subscription-slot.d.ts +3 -1
- package/data/build/dist/client/subscription-slot.js +31 -6
- package/data/build/dist/client/terminal/ui.js +1 -1
- package/data/build/dist/client/terminal/xterm-view.js +1 -1
- package/data/build/dist/dsh-capabilities/client/config-forms-adapter.d.ts +10 -0
- package/data/build/dist/dsh-capabilities/client/config-forms-adapter.js +22 -0
- package/data/build/dist/dsh-capabilities/client/primitives-adapter.d.ts +5 -2
- package/data/build/dist/dsh-capabilities/client/primitives-adapter.js +3 -2
- package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.d.ts +2 -2
- package/data/build/dist/dsh-capabilities/host/config-forms-adapter.d.ts +22 -1
- package/data/build/dist/dsh-capabilities/host/config-forms-adapter.js +29 -3
- package/data/build/dist/dsh-capabilities/host/settings-scope-adapter.d.ts +2 -2
- package/data/build/dist/dsh-capabilities/matrix.js +31 -1
- package/data/build/dist/dsh-capabilities/routes.d.ts +8 -1
- package/data/build/dist/dsh-capabilities/routes.js +173 -5
- package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
- package/data/build/dist/features/registry.d.ts +7 -0
- package/data/build/dist/features/registry.js +20 -2
- package/data/build/dist/host/cli-adapters/feature.js +24 -2
- package/data/build/dist/host/cli-adapters/native-team-proxy.d.ts +36 -0
- package/data/build/dist/host/cli-adapters/native-team-proxy.js +108 -0
- package/data/build/dist/host/cli-adapters/provider-subscription.d.ts +35 -0
- package/data/build/dist/host/cli-adapters/provider-subscription.js +405 -5
- package/data/build/dist/host/cli-adapters/registry.d.ts +9 -3
- package/data/build/dist/host/cli-adapters/registry.js +30 -0
- package/data/build/dist/host/cli-adapters/session-store.d.ts +8 -2
- package/data/build/dist/host/cli-adapters/session-store.js +19 -0
- package/data/build/dist/host/features/git-management.js +41 -18
- package/data/build/dist/host/features/types.d.ts +6 -3
- package/data/build/dist/host/index-injection.d.ts +12 -5
- package/data/build/dist/host/index-injection.js +32 -11
- package/data/build/dist/host/index.js +15 -4
- package/data/build/dist/host/lan-access-dsh.d.ts +2 -2
- package/data/build/dist/host/native-session-bridge.d.ts +4 -0
- package/data/build/dist/host/native-session-bridge.js +51 -9
- package/data/build/dist/host/relay-tunnel-runtime.js +10 -1
- package/data/build/dist/host/remote-web-runtime.js +40 -17
- package/data/build/dist/host/rpc.d.ts +24 -5
- package/data/build/dist/host/rpc.js +38 -9
- package/data/build/dist/host/session-migration-repair.js +14 -0
- package/data/build/dist/host/settings.d.ts +2 -2
- package/data/build/dist/host/settings.js +11 -1
- package/data/build/dist/host/terminal/startup.d.ts +2 -2
- package/data/build/dist/shared/contracts/cli-adapter.d.ts +22 -1
- package/data/build/dist/shared/contracts/config.d.ts +21 -0
- package/data/build/dist/shared/contracts/config.js +17 -0
- package/data/build/dist/shared/contracts/errors.d.ts +2 -2
- package/data/build/dist/shared/contracts/subscription.d.ts +7 -0
- package/data/build/dist/shared/contracts/subscription.js +14 -1
- package/data/build/dist/shared/contracts/version.d.ts +4 -4
- package/data/build/dist/shared/contracts/version.js +3 -3
- package/data/build/dist/shared/index.d.ts +3 -2
- package/data/build/dist/shared/index.js +2 -1
- package/data/build/dist/transport/carrier.d.ts +11 -1
- package/data/build/dist/transport/carrier.js +77 -26
- package/data/build/dist/transport/debug.d.ts +5 -0
- package/data/build/dist/transport/debug.js +17 -2
- package/data/build/dist/transport/dsh-session.d.ts +18 -1
- package/data/build/dist/transport/dsh-session.js +48 -13
- package/data/build/dist/transport/index.d.ts +1 -1
- package/data/build/dist/transport/index.js +1 -1
- package/package.json +23 -19
- package/profile/README.md +2 -2
- package/profile/package.json +4 -3
- package/profile/version.json +3 -3
- package/version.json +3 -3
|
@@ -2,10 +2,20 @@
|
|
|
2
2
|
import { decodeFrame } from './frame.js';
|
|
3
3
|
import { createDshTransportDebugLogger } from './debug.js';
|
|
4
4
|
export const TUNNEL_DATA_CHANNEL_LABEL = 'codingns-tunnel';
|
|
5
|
-
/**
|
|
5
|
+
/** 单个分片的正文上限;整帧为正文加上分片头。 */
|
|
6
6
|
export const DATA_CHANNEL_FRAGMENT_PAYLOAD_BYTES = 64 * 1024;
|
|
7
7
|
export const DATA_CHANNEL_FRAGMENT_HEADER_BYTES = 20;
|
|
8
8
|
export const DATA_CHANNEL_MAX_REASSEMBLY_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
/**
|
|
10
|
+
* Host 侧向浏览器宣告的 SCTP max-message-size(256 KiB)。
|
|
11
|
+
*
|
|
12
|
+
* 浏览器按对端 SDP 里的 max-message-size 限制 `RTCDataChannel.send()`;werift 默认
|
|
13
|
+
* 只宣告 64 KiB,而 Carrier 的整分片是 64 KiB 正文加 20 字节头,因此浏览器侧只要
|
|
14
|
+
* 发送超过 64 KiB 的消息就会抛 `Trying to send message larger than max-message-size`。
|
|
15
|
+
* 中继下的设置写入、附件和大字段都属于这一类,必须把上限抬到浏览器自身
|
|
16
|
+
* (Chrome/Firefox 为 256 KiB)能接受的水平。
|
|
17
|
+
*/
|
|
18
|
+
export const DATA_CHANNEL_MAX_MESSAGE_BYTES = 256 * 1024;
|
|
9
19
|
const DATA_CHANNEL_FRAGMENT_MAGIC = new Uint8Array([0x44, 0x53, 0x46, 0x01]);
|
|
10
20
|
/** Host 侧剥离父仓库 relay-tunnel-wire 的首个 hello,随后只转发 DSH Envelope 二进制。 */
|
|
11
21
|
export function createRelayTunnelHostCarrier(base, debug) {
|
|
@@ -83,6 +93,24 @@ export function createDataChannelCarrier(channel, options = {}) {
|
|
|
83
93
|
closeListeners.clear();
|
|
84
94
|
};
|
|
85
95
|
const onClose = () => { state = 'closed'; clearFragments(); logger.log('data-channel.close', { label: channel.label ?? null }); notifyClosed('DataChannel closed'); listeners.clear(); };
|
|
96
|
+
/**
|
|
97
|
+
* 物理线路已经不可继续使用时统一收敛:分片缺帧、非法帧和背压超时都必须让上层
|
|
98
|
+
* 立刻失败。早期实现只在重组超时时删掉半成品分片,请求因此永远得不到回应,
|
|
99
|
+
* 表现成「设置页一直空白但没有任何报错」。
|
|
100
|
+
*/
|
|
101
|
+
const failCarrier = (error) => {
|
|
102
|
+
if (state === 'closed')
|
|
103
|
+
return;
|
|
104
|
+
logger.log('carrier.failed', { error: error.message });
|
|
105
|
+
state = 'closed';
|
|
106
|
+
clearFragments();
|
|
107
|
+
notifyClosed(error.message);
|
|
108
|
+
listeners.clear();
|
|
109
|
+
try {
|
|
110
|
+
channel.close();
|
|
111
|
+
}
|
|
112
|
+
catch { /* 关闭路径尽力而为 */ }
|
|
113
|
+
};
|
|
86
114
|
const processBytes = (bytes, value) => {
|
|
87
115
|
if (!bytes) {
|
|
88
116
|
logger.log('carrier.receive.invalid', { dataType: Object.prototype.toString.call(value), valueType: typeof value });
|
|
@@ -98,10 +126,7 @@ export function createDataChannelCarrier(channel, options = {}) {
|
|
|
98
126
|
}
|
|
99
127
|
catch (error) {
|
|
100
128
|
logger.log('carrier.fragment.error', { physicalBytes: bytes.byteLength, prefix: bytesToHex(bytes.subarray(0, 8)), error: error instanceof Error ? error.message : String(error) });
|
|
101
|
-
|
|
102
|
-
clearFragments();
|
|
103
|
-
listeners.clear();
|
|
104
|
-
channel.close();
|
|
129
|
+
failCarrier(error instanceof Error ? error : new Error(String(error)));
|
|
105
130
|
}
|
|
106
131
|
};
|
|
107
132
|
const onMessage = (event) => {
|
|
@@ -172,7 +197,10 @@ export function createDataChannelCarrier(channel, options = {}) {
|
|
|
172
197
|
const parsed = decodeFragment(data, maxReassemblyBytes);
|
|
173
198
|
let assembly = fragments.get(parsed.fragmentId);
|
|
174
199
|
if (!assembly) {
|
|
175
|
-
const timer = setTimeout(() =>
|
|
200
|
+
const timer = setTimeout(() => {
|
|
201
|
+
fragments.delete(parsed.fragmentId);
|
|
202
|
+
failCarrier(new Error(`DataChannel 分片重组超时(${String(parsed.chunkCount)} 片,共 ${String(parsed.totalBytes)} 字节)`));
|
|
203
|
+
}, reassemblyTimeoutMs);
|
|
176
204
|
assembly = { totalBytes: parsed.totalBytes, chunkCount: parsed.chunkCount, chunks: new Map(), receivedBytes: 0, timer };
|
|
177
205
|
fragments.set(parsed.fragmentId, assembly);
|
|
178
206
|
logger.log('carrier.fragment.receive', { fragmentId: parsed.fragmentId, chunkCount: parsed.chunkCount, totalBytes: parsed.totalBytes });
|
|
@@ -187,17 +215,7 @@ export function createDataChannelCarrier(channel, options = {}) {
|
|
|
187
215
|
return null;
|
|
188
216
|
clearTimeout(assembly.timer);
|
|
189
217
|
fragments.delete(parsed.fragmentId);
|
|
190
|
-
const result =
|
|
191
|
-
let offset = 0;
|
|
192
|
-
for (let index = 0; index < assembly.chunkCount; index += 1) {
|
|
193
|
-
const chunk = assembly.chunks.get(index);
|
|
194
|
-
if (!chunk)
|
|
195
|
-
throw new Error('DataChannel 分片缺失');
|
|
196
|
-
result.set(chunk, offset);
|
|
197
|
-
offset += chunk.byteLength;
|
|
198
|
-
}
|
|
199
|
-
if (offset !== assembly.totalBytes)
|
|
200
|
-
throw new Error('DataChannel 分片总长度不一致');
|
|
218
|
+
const result = assembleChunks(assembly);
|
|
201
219
|
logger.log('carrier.fragment.complete', { fragmentId: parsed.fragmentId, chunks: assembly.chunkCount, totalBytes: result.byteLength });
|
|
202
220
|
return result;
|
|
203
221
|
};
|
|
@@ -206,8 +224,10 @@ export function createDataChannelCarrier(channel, options = {}) {
|
|
|
206
224
|
send(data) {
|
|
207
225
|
if (!(data instanceof Uint8Array))
|
|
208
226
|
return Promise.reject(new TypeError('Carrier 只接受 Uint8Array'));
|
|
209
|
-
|
|
210
|
-
|
|
227
|
+
const result = chain.then(() => sendLogical(data));
|
|
228
|
+
// 串行链只负责保序:单次发送失败不能让后续发送永久失败。
|
|
229
|
+
chain = result.then(() => undefined, () => undefined);
|
|
230
|
+
return result;
|
|
211
231
|
},
|
|
212
232
|
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
|
|
213
233
|
onClosed(listener) { if (closeNotified) {
|
|
@@ -233,24 +253,55 @@ function encodeFragment(fragmentId, index, chunkCount, totalBytes, body) {
|
|
|
233
253
|
result.set(body, DATA_CHANNEL_FRAGMENT_HEADER_BYTES);
|
|
234
254
|
return result;
|
|
235
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* 按分片头校验单个分片,不假设发送端的固定正文长度。
|
|
258
|
+
*
|
|
259
|
+
* 旧版本(以及线上已部署的 H5 运行时)以 64 KiB 正文分片,只有最后一片更短;
|
|
260
|
+
* 分片长度由发送端决定,接收端必须按「除最后一片外长度一致」来校验,否则任何
|
|
261
|
+
* 分片长度调整都会让新旧两端无法互通。单帧正文仍限制在
|
|
262
|
+
* `DATA_CHANNEL_FRAGMENT_PAYLOAD_BYTES` 以内,避免一次分配过大内存。
|
|
263
|
+
*/
|
|
236
264
|
function decodeFragment(data, maxReassemblyBytes) {
|
|
237
265
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
238
266
|
const fragmentId = view.getUint32(4);
|
|
239
267
|
const index = view.getUint32(8);
|
|
240
268
|
const chunkCount = view.getUint32(12);
|
|
241
269
|
const totalBytes = view.getUint32(16);
|
|
242
|
-
if (chunkCount
|
|
270
|
+
if (chunkCount < 2 || totalBytes < 2 || totalBytes > maxReassemblyBytes || index >= chunkCount)
|
|
243
271
|
throw new Error('DataChannel 分片头无效');
|
|
244
|
-
const expectedCount = Math.ceil(totalBytes / DATA_CHANNEL_FRAGMENT_PAYLOAD_BYTES);
|
|
245
|
-
if (chunkCount !== expectedCount)
|
|
246
|
-
throw new Error('DataChannel 分片数量无效');
|
|
247
|
-
const offset = index * DATA_CHANNEL_FRAGMENT_PAYLOAD_BYTES;
|
|
248
|
-
const expectedBytes = Math.min(DATA_CHANNEL_FRAGMENT_PAYLOAD_BYTES, totalBytes - offset);
|
|
249
272
|
const body = data.subarray(DATA_CHANNEL_FRAGMENT_HEADER_BYTES);
|
|
250
|
-
if (body.byteLength
|
|
273
|
+
if (body.byteLength === 0 || body.byteLength > DATA_CHANNEL_FRAGMENT_PAYLOAD_BYTES)
|
|
251
274
|
throw new Error('DataChannel 分片长度无效');
|
|
252
275
|
return { fragmentId, index, chunkCount, totalBytes, body: body.slice() };
|
|
253
276
|
}
|
|
277
|
+
/** 收齐全部索引后按发送端的分片长度重组,并校验长度自洽。 */
|
|
278
|
+
function assembleChunks(assembly) {
|
|
279
|
+
const first = assembly.chunks.get(0);
|
|
280
|
+
if (first === undefined)
|
|
281
|
+
throw new Error('DataChannel 分片缺失');
|
|
282
|
+
const chunkBytes = first.byteLength;
|
|
283
|
+
if (chunkBytes === 0 || chunkBytes > DATA_CHANNEL_FRAGMENT_PAYLOAD_BYTES)
|
|
284
|
+
throw new Error('DataChannel 分片长度无效');
|
|
285
|
+
if (assembly.chunkCount !== Math.ceil(assembly.totalBytes / chunkBytes))
|
|
286
|
+
throw new Error('DataChannel 分片数量无效');
|
|
287
|
+
const lastBytes = assembly.totalBytes - chunkBytes * (assembly.chunkCount - 1);
|
|
288
|
+
if (lastBytes < 1 || lastBytes > chunkBytes)
|
|
289
|
+
throw new Error('DataChannel 分片长度无效');
|
|
290
|
+
const result = new Uint8Array(assembly.totalBytes);
|
|
291
|
+
let offset = 0;
|
|
292
|
+
for (let index = 0; index < assembly.chunkCount; index += 1) {
|
|
293
|
+
const chunk = assembly.chunks.get(index);
|
|
294
|
+
if (!chunk)
|
|
295
|
+
throw new Error('DataChannel 分片缺失');
|
|
296
|
+
if (chunk.byteLength !== (index === assembly.chunkCount - 1 ? lastBytes : chunkBytes))
|
|
297
|
+
throw new Error('DataChannel 分片长度不一致');
|
|
298
|
+
result.set(chunk, offset);
|
|
299
|
+
offset += chunk.byteLength;
|
|
300
|
+
}
|
|
301
|
+
if (offset !== assembly.totalBytes)
|
|
302
|
+
throw new Error('DataChannel 分片总长度不一致');
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
254
305
|
function toBytes(value) {
|
|
255
306
|
if (value instanceof Uint8Array)
|
|
256
307
|
return new Uint8Array(value);
|
|
@@ -20,6 +20,11 @@ export declare function createDshTransportDebugLogger(options?: DshTransportDebu
|
|
|
20
20
|
/**
|
|
21
21
|
* 调试字段采用正向白名单。Transport 可能同时处理 token、URL、命令和正文,
|
|
22
22
|
* 因此不能依赖调用方自觉脱敏;未知字段和错误正文直接丢弃。
|
|
23
|
+
*
|
|
24
|
+
* `path` / `method` / `status` / `errorCode` 是 DSH Web 请求的路由元数据:
|
|
25
|
+
* 没有它们就无法区分「请求没到达 Host」和「请求到达但返回空结果」,
|
|
26
|
+
* 中继设置页空白这类问题只能靠日志猜。这些字段只能是 HTTP 路径与状态,
|
|
27
|
+
* 禁止把查询串、Cookie、请求/响应正文塞进来。
|
|
23
28
|
*/
|
|
24
29
|
export declare function sanitizeDshTransportDebugFields(fields: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>>;
|
|
25
30
|
/** 解析统一的 Codingns4DSH 调试开关,保留旧隧道变量作为兼容别名。 */
|
|
@@ -26,6 +26,11 @@ export function createDshTransportDebugLogger(options = {}) {
|
|
|
26
26
|
/**
|
|
27
27
|
* 调试字段采用正向白名单。Transport 可能同时处理 token、URL、命令和正文,
|
|
28
28
|
* 因此不能依赖调用方自觉脱敏;未知字段和错误正文直接丢弃。
|
|
29
|
+
*
|
|
30
|
+
* `path` / `method` / `status` / `errorCode` 是 DSH Web 请求的路由元数据:
|
|
31
|
+
* 没有它们就无法区分「请求没到达 Host」和「请求到达但返回空结果」,
|
|
32
|
+
* 中继设置页空白这类问题只能靠日志猜。这些字段只能是 HTTP 路径与状态,
|
|
33
|
+
* 禁止把查询串、Cookie、请求/响应正文塞进来。
|
|
29
34
|
*/
|
|
30
35
|
export function sanitizeDshTransportDebugFields(fields) {
|
|
31
36
|
const allowed = new Set([
|
|
@@ -34,17 +39,27 @@ export function sanitizeDshTransportDebugFields(fields) {
|
|
|
34
39
|
'channel', 'channelLabel', 'operation', 'feature', 'type', 'code', 'state', 'role',
|
|
35
40
|
'streams', 'activeStreams', 'openingStreams', 'maxStreams', 'fragmentId', 'chunkCount',
|
|
36
41
|
'totalBytes', 'payloadBytes', 'bufferedAmount', 'highWaterMark', 'lowWaterMark', 'dataType',
|
|
37
|
-
'valueType', 'trafficRemainingBytes',
|
|
42
|
+
'valueType', 'trafficRemainingBytes', 'path', 'method', 'status', 'errorCode',
|
|
38
43
|
]);
|
|
39
44
|
const result = {};
|
|
40
45
|
for (const [key, value] of Object.entries(fields)) {
|
|
41
46
|
if (!allowed.has(key))
|
|
42
47
|
continue;
|
|
43
|
-
if (typeof value === 'string'
|
|
48
|
+
if (typeof value === 'string') {
|
|
49
|
+
result[key] = key === 'path' ? sanitizeDebugPath(value) : value;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === 'number' || typeof value === 'boolean' || value === null)
|
|
44
53
|
result[key] = value;
|
|
45
54
|
}
|
|
46
55
|
return result;
|
|
47
56
|
}
|
|
57
|
+
/** 路径只保留 pathname,去掉查询串与可能带凭据的片段。 */
|
|
58
|
+
function sanitizeDebugPath(value) {
|
|
59
|
+
const query = value.indexOf('?');
|
|
60
|
+
const trimmed = query === -1 ? value : value.slice(0, query);
|
|
61
|
+
return trimmed.length > 200 ? `${trimmed.slice(0, 200)}…` : trimmed;
|
|
62
|
+
}
|
|
48
63
|
/** 解析统一的 Codingns4DSH 调试开关,保留旧隧道变量作为兼容别名。 */
|
|
49
64
|
export function resolveDshTransportDebugEnabled() {
|
|
50
65
|
return resolveCodingNsDebugEnabled();
|
|
@@ -19,7 +19,7 @@ export interface DshSessionOptions {
|
|
|
19
19
|
acceptInitialGeneration?: boolean;
|
|
20
20
|
debug?: DshTransportDebugLogger;
|
|
21
21
|
}
|
|
22
|
-
/** 负责 DSH hello/ready
|
|
22
|
+
/** 负责 DSH hello/ready、协议与能力协商和心跳,不执行任何业务。 */
|
|
23
23
|
export declare class DshSession {
|
|
24
24
|
private readonly options;
|
|
25
25
|
private stateValue;
|
|
@@ -45,11 +45,28 @@ export declare class DshSession {
|
|
|
45
45
|
close(reason?: string): void;
|
|
46
46
|
private sendHello;
|
|
47
47
|
private sendReady;
|
|
48
|
+
/**
|
|
49
|
+
* 握手只校验隧道协议版本:对端缺省不带协议时按 v1 处理。
|
|
50
|
+
* DSH 应用版本不再参与握手门槛;只有插件升级 DSH_ENVELOPE_PROTOCOL
|
|
51
|
+
* (对应不再兼容的 WebRTC 变更)时,才会在此拒绝旧协议对端。
|
|
52
|
+
*/
|
|
53
|
+
private handshakeProtocolMatches;
|
|
54
|
+
/** 协议不兼容时先回发 session.close 让对端拿到明确原因,再收敛本地会话。 */
|
|
55
|
+
private rejectHandshake;
|
|
48
56
|
private receive;
|
|
49
57
|
private receiveSession;
|
|
50
58
|
private validateScope;
|
|
51
59
|
private createEnvelope;
|
|
52
60
|
private startHeartbeat;
|
|
61
|
+
/**
|
|
62
|
+
* 会话失败必须同时收敛物理线路。
|
|
63
|
+
*
|
|
64
|
+
* 早期实现只把本地状态改成 `degraded`、拒绝 `waitReady`:对端既收不到
|
|
65
|
+
* `session.close`,DataChannel 也不会关闭,于是一次发送失败(例如浏览器宣告的
|
|
66
|
+
* max-message-size 太小、分片发送被拒、背压超时)之后,对端会在一个已经死掉的
|
|
67
|
+
* 线路上等一个永远不回来的响应。中继页面的原生设置 `settings/describe` 正是这样
|
|
68
|
+
* 永久停在 loading:页面空白、没有任何报错。
|
|
69
|
+
*/
|
|
53
70
|
private fail;
|
|
54
71
|
}
|
|
55
72
|
//# sourceMappingURL=dsh-session.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DSH_ENVELOPE_PROTOCOL, decodeDshEnvelope, encodeDshEnvelope, } from './dsh-envelope.js';
|
|
2
|
-
import { DSH_VERSION
|
|
2
|
+
import { DSH_VERSION } from '../shared/contracts/version.js';
|
|
3
3
|
import { createDshTransportDebugLogger } from './debug.js';
|
|
4
|
-
/** 负责 DSH hello/ready
|
|
4
|
+
/** 负责 DSH hello/ready、协议与能力协商和心跳,不执行任何业务。 */
|
|
5
5
|
export class DshSession {
|
|
6
6
|
options;
|
|
7
7
|
stateValue = 'idle';
|
|
@@ -93,15 +93,31 @@ export class DshSession {
|
|
|
93
93
|
capabilities: [...this.options.capabilities ?? []],
|
|
94
94
|
}));
|
|
95
95
|
}
|
|
96
|
-
sendReady(capabilities) {
|
|
96
|
+
sendReady(capabilities, peerDshVersion) {
|
|
97
97
|
this.send(this.createEnvelope('session.ready', 'session', {
|
|
98
98
|
protocol: this.options.protocol ?? DSH_ENVELOPE_PROTOCOL,
|
|
99
|
-
|
|
99
|
+
// 回显对端上报的版本:旧客户端按自身兼容区间校验 ready,回显其自身版本才能放行。
|
|
100
|
+
dshVersion: peerDshVersion ?? this.options.dshVersion ?? DSH_VERSION,
|
|
101
|
+
hostDshVersion: this.options.dshVersion ?? DSH_VERSION,
|
|
100
102
|
capabilities: [...capabilities],
|
|
101
103
|
byteCredit: 64 * 1024,
|
|
102
104
|
messageCredit: 32,
|
|
103
105
|
}));
|
|
104
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* 握手只校验隧道协议版本:对端缺省不带协议时按 v1 处理。
|
|
109
|
+
* DSH 应用版本不再参与握手门槛;只有插件升级 DSH_ENVELOPE_PROTOCOL
|
|
110
|
+
* (对应不再兼容的 WebRTC 变更)时,才会在此拒绝旧协议对端。
|
|
111
|
+
*/
|
|
112
|
+
handshakeProtocolMatches(envelope) {
|
|
113
|
+
const protocol = envelope.meta.protocol ?? DSH_ENVELOPE_PROTOCOL;
|
|
114
|
+
return protocol === (this.options.protocol ?? DSH_ENVELOPE_PROTOCOL);
|
|
115
|
+
}
|
|
116
|
+
/** 协议不兼容时先回发 session.close 让对端拿到明确原因,再收敛本地会话。 */
|
|
117
|
+
rejectHandshake(reason) {
|
|
118
|
+
this.send(this.createEnvelope('session.close', 'session', { reason }));
|
|
119
|
+
this.fail(new Error(reason));
|
|
120
|
+
}
|
|
105
121
|
receive(data) {
|
|
106
122
|
if (this.stateValue === 'closed')
|
|
107
123
|
return;
|
|
@@ -133,18 +149,17 @@ export class DshSession {
|
|
|
133
149
|
this.fail(new Error('非法 session.hello'));
|
|
134
150
|
return;
|
|
135
151
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (protocol !== (this.options.protocol ?? DSH_ENVELOPE_PROTOCOL) || typeof dshVersion !== 'string' || !isDshVersionCompatible(dshVersion)) {
|
|
139
|
-
this.fail(new Error('PROTOCOL_VERSION_UNSUPPORTED'));
|
|
152
|
+
if (!this.handshakeProtocolMatches(envelope)) {
|
|
153
|
+
this.rejectHandshake('PROTOCOL_VERSION_UNSUPPORTED');
|
|
140
154
|
return;
|
|
141
155
|
}
|
|
156
|
+
const peerDshVersion = typeof envelope.meta.dshVersion === 'string' ? envelope.meta.dshVersion : undefined;
|
|
142
157
|
const offered = readCapabilities(envelope.meta.capabilities);
|
|
143
158
|
const allowed = new Set(this.options.capabilities ?? offered);
|
|
144
159
|
this.remoteCapabilities = offered.filter((capability) => allowed.has(capability));
|
|
145
160
|
this.stateValue = 'ready';
|
|
146
161
|
this.debug.log('session.ready', { role: this.options.role, capabilities: this.remoteCapabilities });
|
|
147
|
-
this.sendReady(this.remoteCapabilities);
|
|
162
|
+
this.sendReady(this.remoteCapabilities, peerDshVersion);
|
|
148
163
|
this.readyResolve?.();
|
|
149
164
|
this.options.onReady?.(this);
|
|
150
165
|
return;
|
|
@@ -154,10 +169,8 @@ export class DshSession {
|
|
|
154
169
|
this.fail(new Error('非法 session.ready'));
|
|
155
170
|
return;
|
|
156
171
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (protocol !== (this.options.protocol ?? DSH_ENVELOPE_PROTOCOL) || typeof dshVersion !== 'string' || !isDshVersionCompatible(dshVersion)) {
|
|
160
|
-
this.fail(new Error('PROTOCOL_VERSION_UNSUPPORTED'));
|
|
172
|
+
if (!this.handshakeProtocolMatches(envelope)) {
|
|
173
|
+
this.rejectHandshake('PROTOCOL_VERSION_UNSUPPORTED');
|
|
161
174
|
return;
|
|
162
175
|
}
|
|
163
176
|
this.remoteCapabilities = readCapabilities(envelope.meta.capabilities);
|
|
@@ -228,11 +241,33 @@ export class DshSession {
|
|
|
228
241
|
}
|
|
229
242
|
}, interval);
|
|
230
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* 会话失败必须同时收敛物理线路。
|
|
246
|
+
*
|
|
247
|
+
* 早期实现只把本地状态改成 `degraded`、拒绝 `waitReady`:对端既收不到
|
|
248
|
+
* `session.close`,DataChannel 也不会关闭,于是一次发送失败(例如浏览器宣告的
|
|
249
|
+
* max-message-size 太小、分片发送被拒、背压超时)之后,对端会在一个已经死掉的
|
|
250
|
+
* 线路上等一个永远不回来的响应。中继页面的原生设置 `settings/describe` 正是这样
|
|
251
|
+
* 永久停在 loading:页面空白、没有任何报错。
|
|
252
|
+
*/
|
|
231
253
|
fail(error) {
|
|
254
|
+
if (this.stateValue === 'closed' || this.stateValue === 'degraded')
|
|
255
|
+
return;
|
|
256
|
+
const wasReady = this.stateValue === 'ready';
|
|
232
257
|
this.stateValue = 'degraded';
|
|
233
258
|
this.debug.log('session.error', { error: error.message });
|
|
234
259
|
this.readyReject?.(error);
|
|
235
260
|
this.options.onError?.(error);
|
|
261
|
+
if (wasReady) {
|
|
262
|
+
// 先尽力通知对端失败原因;发送本身失败也已经不影响后续收敛。
|
|
263
|
+
try {
|
|
264
|
+
this.send(this.createEnvelope('session.close', 'session', { reason: error.message }));
|
|
265
|
+
}
|
|
266
|
+
catch { /* 线路不可用时尽力而为 */ }
|
|
267
|
+
}
|
|
268
|
+
const carrier = this.options.carrier;
|
|
269
|
+
// 让已排队的收尾帧先进入发送链,再关闭物理线路。
|
|
270
|
+
setTimeout(() => { void carrier.close(`DSH Session 失败:${error.message}`); }, 0);
|
|
236
271
|
}
|
|
237
272
|
}
|
|
238
273
|
function envelopeDebugFields(envelope) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createDataChannelCarrier, createRelayTunnelHostCarrier, TUNNEL_DATA_CHANNEL_LABEL, DATA_CHANNEL_FRAGMENT_PAYLOAD_BYTES, DATA_CHANNEL_FRAGMENT_HEADER_BYTES, DATA_CHANNEL_MAX_REASSEMBLY_BYTES, type CodingNsCarrier, type DataChannelLike, type DataChannelCarrierOptions } from './carrier.js';
|
|
1
|
+
export { createDataChannelCarrier, createRelayTunnelHostCarrier, TUNNEL_DATA_CHANNEL_LABEL, DATA_CHANNEL_FRAGMENT_PAYLOAD_BYTES, DATA_CHANNEL_FRAGMENT_HEADER_BYTES, DATA_CHANNEL_MAX_REASSEMBLY_BYTES, DATA_CHANNEL_MAX_MESSAGE_BYTES, type CodingNsCarrier, type DataChannelLike, type DataChannelCarrierOptions } from './carrier.js';
|
|
2
2
|
export { encodeFrame, decodeFrame, decodeFrames, createFrameDecoder, decodeTunnelFrame, encodeTunnelFrame, TUNNEL_WIRE_VERSION, TUNNEL_FRAME_HEADER_BYTES, TUNNEL_MAX_META_BYTES, TUNNEL_MAX_FRAME_BODY_BYTES, TUNNEL_FRAME_TYPE_CODES, TUNNEL_PROTOCOL_VERSION, validateTunnelFrame, tunnelFrameByteLength, DEFAULT_MAX_TUNNEL_FRAME_BYTES, type TunnelChannel, type TunnelFrame, type TunnelFrameKind, type TunnelFrameCodecOptions, type TunnelFrameType, type TunnelClientContext, } from './frame.js';
|
|
3
3
|
export { DshTunnelMultiplexer, type DshTunnelMultiplexerOptions, type TunnelFlowControl } from './multiplexer.js';
|
|
4
4
|
export { createDshTransportDebugLogger, resolveDshTransportDebugEnabled, type DshTransportDebugLogger, type DshTransportDebugOptions, type DshTransportDebugSide } from './debug.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createDataChannelCarrier, createRelayTunnelHostCarrier, TUNNEL_DATA_CHANNEL_LABEL, DATA_CHANNEL_FRAGMENT_PAYLOAD_BYTES, DATA_CHANNEL_FRAGMENT_HEADER_BYTES, DATA_CHANNEL_MAX_REASSEMBLY_BYTES } from './carrier.js';
|
|
1
|
+
export { createDataChannelCarrier, createRelayTunnelHostCarrier, TUNNEL_DATA_CHANNEL_LABEL, DATA_CHANNEL_FRAGMENT_PAYLOAD_BYTES, DATA_CHANNEL_FRAGMENT_HEADER_BYTES, DATA_CHANNEL_MAX_REASSEMBLY_BYTES, DATA_CHANNEL_MAX_MESSAGE_BYTES } from './carrier.js';
|
|
2
2
|
export { encodeFrame, decodeFrame, decodeFrames, createFrameDecoder, decodeTunnelFrame, encodeTunnelFrame, TUNNEL_WIRE_VERSION, TUNNEL_FRAME_HEADER_BYTES, TUNNEL_MAX_META_BYTES, TUNNEL_MAX_FRAME_BODY_BYTES, TUNNEL_FRAME_TYPE_CODES, TUNNEL_PROTOCOL_VERSION, validateTunnelFrame, tunnelFrameByteLength, DEFAULT_MAX_TUNNEL_FRAME_BYTES, } from './frame.js';
|
|
3
3
|
export { DshTunnelMultiplexer } from './multiplexer.js';
|
|
4
4
|
export { createDshTransportDebugLogger, resolveDshTransportDebugEnabled } from './debug.js';
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jingyi0605/codingns4dsh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.1",
|
|
4
4
|
"description": "Codingns4DSH 插件骨架与连接契约",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/jingyi0605/Codingns4DSH"
|
|
8
8
|
},
|
|
9
|
+
"license": "GPL-3.0-or-later",
|
|
9
10
|
"type": "module",
|
|
10
11
|
"main": "./data/build/dist/index.js",
|
|
11
12
|
"types": "./data/build/dist/index.d.ts",
|
|
@@ -53,11 +54,11 @@
|
|
|
53
54
|
"./package.json": "./package.json"
|
|
54
55
|
},
|
|
55
56
|
"engines": {
|
|
56
|
-
"dsh": ">=0.
|
|
57
|
+
"dsh": ">=0.2.0-rc.1 <=0.2.0-rc.1",
|
|
57
58
|
"node": ">=22.19.0"
|
|
58
59
|
},
|
|
59
60
|
"peerDependencies": {
|
|
60
|
-
"@deepseek-ai/dsh": ">=0.
|
|
61
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.1 <=0.2.0-rc.1"
|
|
61
62
|
},
|
|
62
63
|
"dsh": {
|
|
63
64
|
"manifestVersion": 1,
|
|
@@ -98,6 +99,9 @@
|
|
|
98
99
|
"bundle:h5": "tsdown --config tsdown.h5.config.ts",
|
|
99
100
|
"dev:link": "node scripts/link-dsh-dev.mjs",
|
|
100
101
|
"dev:watch": "node scripts/dev-watch.mjs",
|
|
102
|
+
"dsh:web": "bash scripts/run-dsh-web.sh",
|
|
103
|
+
"dsh:stage0": "bash scripts/run-dsh-stage0.sh",
|
|
104
|
+
"dsh:stage0:clean-link": "bash scripts/clean-stage0-dev-link.sh",
|
|
101
105
|
"watch:client": "tsdown --watch",
|
|
102
106
|
"test": "pnpm run build && node --test tests/*.spec.ts",
|
|
103
107
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
@@ -114,22 +118,22 @@
|
|
|
114
118
|
"scripts/check-dsh-install.mjs"
|
|
115
119
|
],
|
|
116
120
|
"devDependencies": {
|
|
117
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
118
|
-
"@deepseek-ai/dsh-api-remotes": "0.
|
|
119
|
-
"@deepseek-ai/dsh-client-connection": "0.
|
|
120
|
-
"@deepseek-ai/dsh-client-locale": "0.
|
|
121
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.
|
|
122
|
-
"@deepseek-ai/dsh-client-ui-layout": "0.
|
|
123
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.
|
|
124
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.
|
|
125
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.
|
|
126
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.
|
|
127
|
-
"@deepseek-ai/dsh-client-ui-session": "0.
|
|
128
|
-
"@deepseek-ai/dsh-client-ui-sidebar-right": "0.
|
|
129
|
-
"@deepseek-ai/dsh-client-ui-theme": "0.
|
|
130
|
-
"@deepseek-ai/dsh-settings": "0.
|
|
131
|
-
"@deepseek-ai/dsh-typert-protocol": "0.
|
|
132
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
121
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
122
|
+
"@deepseek-ai/dsh-api-remotes": "0.2.0-rc.1",
|
|
123
|
+
"@deepseek-ai/dsh-client-connection": "0.2.0-rc.1",
|
|
124
|
+
"@deepseek-ai/dsh-client-locale": "0.2.0-rc.1",
|
|
125
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.1",
|
|
126
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.2.0-rc.1",
|
|
127
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.1",
|
|
128
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.1",
|
|
129
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.1",
|
|
130
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.1",
|
|
131
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.1",
|
|
132
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right": "0.2.0-rc.1",
|
|
133
|
+
"@deepseek-ai/dsh-client-ui-theme": "0.2.0-rc.1",
|
|
134
|
+
"@deepseek-ai/dsh-settings": "0.2.0-rc.1",
|
|
135
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.1",
|
|
136
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
133
137
|
"@types/react": "~18.3.1",
|
|
134
138
|
"@types/ws": "^8.18.1",
|
|
135
139
|
"react": "^18.2.0",
|
package/profile/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Codingns4DSH Profile
|
|
2
2
|
|
|
3
|
-
这是独立的 Codingns4DSH Profile,插件版本为 `0.
|
|
3
|
+
这是独立的 Codingns4DSH Profile,插件版本为 `0.2.0-beta.1`,兼容 DSH `>=0.2.0-rc.1 <=0.2.0-rc.1`,当前测试版本为 `0.2.0-rc.1`。
|
|
4
4
|
|
|
5
5
|
Profile 只选择 `codingns4dsh` Bundle。`cordis.patch.yml` 保持 `[]`,因为启动期
|
|
6
6
|
Transport 必须由外部 pre-Cordis 启动胶水在 DSH Client/Cordis 创建前登记,不能由
|
|
@@ -9,7 +9,7 @@ Transport 必须由外部 pre-Cordis 启动胶水在 DSH Client/Cordis 创建前
|
|
|
9
9
|
发布后,在 DSH 的 Profile 中安装精确版本的插件 Bundle:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
dsh plugin --profile codingns4dsh add @jingyi0605/codingns4dsh@0.
|
|
12
|
+
dsh plugin --profile codingns4dsh add @jingyi0605/codingns4dsh@0.2.0-beta.1
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Profile 安装完成后,使用 DSH 官方启动器启动:
|
package/profile/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codingns4dsh-profile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.1",
|
|
4
|
+
"license": "GPL-3.0-or-later",
|
|
4
5
|
"private": true,
|
|
5
6
|
"type": "module",
|
|
6
7
|
"engines": {
|
|
7
|
-
"dsh": ">=0.
|
|
8
|
+
"dsh": ">=0.2.0-rc.1 <=0.2.0-rc.1"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"preinstall": "node scripts/check-dsh-install.mjs"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
|
-
"@jingyi0605/codingns4dsh": "0.
|
|
14
|
+
"@jingyi0605/codingns4dsh": "0.2.0-beta.1"
|
|
14
15
|
},
|
|
15
16
|
"dsh": {
|
|
16
17
|
"profile": {
|
package/profile/version.json
CHANGED
package/version.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"pluginVersion": "0.
|
|
3
|
-
"dshCompatibility": ">=0.
|
|
4
|
-
"dshTestedVersion": "0.
|
|
2
|
+
"pluginVersion": "0.2.0-beta.1",
|
|
3
|
+
"dshCompatibility": ">=0.2.0-rc.1 <=0.2.0-rc.1",
|
|
4
|
+
"dshTestedVersion": "0.2.0-rc.1",
|
|
5
5
|
"dshProtocolVersion": 1
|
|
6
6
|
}
|