@jingyi0605/codingns4dsh 0.1.2 → 0.1.7-rc.3
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 +4 -4
- package/README.md +14 -5
- package/data/build/dist/client/account-bar.js +57 -3
- package/data/build/dist/client/bundle.js +36246 -268
- package/data/build/dist/client/cli-slots.js +60 -18
- package/data/build/dist/client/dsh-h5-bootstrap.d.ts +2 -0
- package/data/build/dist/client/dsh-h5-bootstrap.js +79 -13
- package/data/build/dist/client/features/file-management-panel.d.ts +5 -0
- package/data/build/dist/client/features/file-management-panel.js +48 -0
- package/data/build/dist/client/features/file-management.d.ts +4 -0
- package/data/build/dist/client/features/file-management.js +50 -0
- package/data/build/dist/client/features/index.d.ts +10 -1
- package/data/build/dist/client/features/index.js +12 -1
- package/data/build/dist/client/features/login-protection-panel.js +1 -1
- package/data/build/dist/client/features/login-protection-session.d.ts +2 -0
- package/data/build/dist/client/features/login-protection-session.js +18 -0
- package/data/build/dist/client/features/peer-host.d.ts +4 -0
- package/data/build/dist/client/features/peer-host.js +89 -0
- package/data/build/dist/client/features/reverse-proxy-panel.js +23 -7
- package/data/build/dist/client/features/reverse-proxy.d.ts +1 -2
- package/data/build/dist/client/features/reverse-proxy.js +75 -5
- package/data/build/dist/client/features/types.d.ts +9 -0
- package/data/build/dist/client/features/workspace-session-enhancement-panel.js +10 -0
- package/data/build/dist/client/features/workspace-session-enhancement.js +18 -1
- package/data/build/dist/client/file-management-dom.d.ts +12 -0
- package/data/build/dist/client/file-management-dom.js +527 -0
- package/data/build/dist/client/git-management.js +24 -5
- package/data/build/dist/client/host-navigation.d.ts +29 -0
- package/data/build/dist/client/host-navigation.js +29 -0
- package/data/build/dist/client/host-router.d.ts +3 -0
- package/data/build/dist/client/host-router.js +2 -0
- package/data/build/dist/client/index.d.ts +6 -1
- package/data/build/dist/client/index.js +30 -10
- package/data/build/dist/client/locale.js +36 -7
- package/data/build/dist/client/peer-host-connection-button.d.ts +13 -0
- package/data/build/dist/client/peer-host-connection-button.js +69 -0
- package/data/build/dist/client/peer-host-management-api.d.ts +40 -0
- package/data/build/dist/client/peer-host-management-api.js +33 -0
- package/data/build/dist/client/peer-host-management-panel.d.ts +13 -0
- package/data/build/dist/client/peer-host-management-panel.js +181 -0
- package/data/build/dist/client/peer-host-native-session-ui.d.ts +60 -0
- package/data/build/dist/client/peer-host-native-session-ui.js +372 -0
- package/data/build/dist/client/peer-host-scoped-client.d.ts +67 -0
- package/data/build/dist/client/peer-host-scoped-client.js +316 -0
- package/data/build/dist/client/peer-host-session-controller.d.ts +30 -0
- package/data/build/dist/client/peer-host-session-controller.js +95 -0
- package/data/build/dist/client/quick-phrase-slot.js +14 -0
- package/data/build/dist/client/remote-web-context.js +26 -4
- package/data/build/dist/client/session-changed-files-view.d.ts +15 -0
- package/data/build/dist/client/session-changed-files-view.js +337 -0
- package/data/build/dist/client/settings-bridge.d.ts +14 -4
- package/data/build/dist/client/settings-bridge.js +59 -12
- package/data/build/dist/client/settings-section.js +17 -4
- package/data/build/dist/client/subscription-slot.js +21 -8
- package/data/build/dist/client/workspace-session-rightbar-dom.d.ts +28 -0
- package/data/build/dist/client/workspace-session-rightbar-dom.js +291 -0
- package/data/build/dist/dsh-capabilities/client/config-forms-adapter.js +25 -33
- package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.d.ts +7 -1
- package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.js +34 -11
- package/data/build/dist/dsh-capabilities/matrix.js +9 -0
- package/data/build/dist/dsh-capabilities/routes.js +35 -0
- package/data/build/dist/dsh-capabilities/settings-store.d.ts +12 -0
- package/data/build/dist/dsh-capabilities/settings-store.js +34 -0
- package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
- package/data/build/dist/features/host-router.d.ts +26 -0
- package/data/build/dist/features/host-router.js +110 -0
- package/data/build/dist/features/registry.js +3 -0
- package/data/build/dist/host/cli-adapters/codex-driver.d.ts +13 -1
- package/data/build/dist/host/cli-adapters/codex-driver.js +528 -72
- package/data/build/dist/host/cli-adapters/command-code-driver.d.ts +21 -2
- package/data/build/dist/host/cli-adapters/command-code-driver.js +408 -105
- package/data/build/dist/host/cli-adapters/driver.d.ts +2 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts +2 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.js +47 -28
- package/data/build/dist/host/cli-adapters/dsh-tool-history.js +37 -0
- package/data/build/dist/host/cli-adapters/feature.js +94 -10
- package/data/build/dist/host/cli-adapters/json-rpc-process.d.ts +6 -0
- package/data/build/dist/host/cli-adapters/json-rpc-process.js +14 -3
- package/data/build/dist/host/cli-adapters/kimi-driver.js +3 -3
- package/data/build/dist/host/cli-adapters/model-catalog.d.ts +6 -0
- package/data/build/dist/host/cli-adapters/model-catalog.js +8 -0
- package/data/build/dist/host/cli-adapters/opencode-driver.js +14 -2
- package/data/build/dist/host/cli-adapters/pi-driver.js +2 -2
- package/data/build/dist/host/cli-adapters/process-utils.d.ts +8 -0
- package/data/build/dist/host/cli-adapters/process-utils.js +48 -0
- package/data/build/dist/host/cli-adapters/registry.d.ts +6 -1
- package/data/build/dist/host/cli-adapters/registry.js +41 -7
- package/data/build/dist/host/cli-adapters/rpc-driver-utils.js +23 -4
- package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +3 -0
- package/data/build/dist/host/cli-adapters/standard-stream-driver.js +36 -15
- package/data/build/dist/host/dsh-device-runtime.d.ts +2 -0
- package/data/build/dist/host/dsh-device-runtime.js +55 -29
- package/data/build/dist/host/dsh-runtime-version.d.ts +3 -2
- package/data/build/dist/host/dsh-runtime-version.js +71 -2
- package/data/build/dist/host/features/auth.js +10 -2
- package/data/build/dist/host/features/file-management.d.ts +5 -0
- package/data/build/dist/host/features/file-management.js +267 -0
- package/data/build/dist/host/features/git-management.js +24 -2
- package/data/build/dist/host/features/index.d.ts +2 -0
- package/data/build/dist/host/features/index.js +6 -0
- package/data/build/dist/host/features/peer-host.d.ts +22 -0
- package/data/build/dist/host/features/peer-host.js +322 -0
- package/data/build/dist/host/features/types.d.ts +4 -0
- package/data/build/dist/host/host-router.d.ts +3 -0
- package/data/build/dist/host/host-router.js +2 -0
- package/data/build/dist/host/index-injection.d.ts +17 -0
- package/data/build/dist/host/index-injection.js +24 -0
- package/data/build/dist/host/index.d.ts +4 -1
- package/data/build/dist/host/index.js +41 -7
- package/data/build/dist/host/lan-access-dsh.d.ts +34 -1
- package/data/build/dist/host/lan-access-dsh.js +145 -26
- package/data/build/dist/host/modules/peer-host/host-api-proxy-service.d.ts +52 -0
- package/data/build/dist/host/modules/peer-host/host-api-proxy-service.js +166 -0
- package/data/build/dist/host/modules/peer-host/host-ws-connector.d.ts +31 -0
- package/data/build/dist/host/modules/peer-host/host-ws-connector.js +108 -0
- package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.d.ts +26 -0
- package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.js +159 -0
- package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.d.ts +50 -0
- package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.js +97 -0
- package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.d.ts +19 -0
- package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.js +54 -0
- package/data/build/dist/host/modules/peer-host/peer-host-handshake.d.ts +44 -0
- package/data/build/dist/host/modules/peer-host/peer-host-handshake.js +133 -0
- package/data/build/dist/host/modules/peer-host/peer-host-relay.d.ts +65 -0
- package/data/build/dist/host/modules/peer-host/peer-host-relay.js +196 -0
- package/data/build/dist/host/modules/peer-host/peer-host-session.d.ts +42 -0
- package/data/build/dist/host/modules/peer-host/peer-host-session.js +152 -0
- package/data/build/dist/host/modules/peer-host/peer-host-store.d.ts +92 -0
- package/data/build/dist/host/modules/peer-host/peer-host-store.js +323 -0
- package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.d.ts +34 -0
- package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.js +156 -0
- package/data/build/dist/host/native-session-bridge.d.ts +18 -2
- package/data/build/dist/host/native-session-bridge.js +236 -8
- package/data/build/dist/host/remote-web-runtime.js +26 -1
- package/data/build/dist/host/rpc.js +8 -3
- package/data/build/dist/host/session-changes.d.ts +5 -0
- package/data/build/dist/host/session-changes.js +163 -0
- package/data/build/dist/host/settings.js +6 -0
- package/data/build/dist/index.d.ts +1 -1
- package/data/build/dist/index.js +1 -1
- package/data/build/dist/shared/contracts/cli-adapter.d.ts +23 -2
- package/data/build/dist/shared/contracts/config.d.ts +15 -0
- package/data/build/dist/shared/contracts/config.js +10 -0
- package/data/build/dist/shared/contracts/dsh-device.d.ts +2 -0
- package/data/build/dist/shared/contracts/errors.d.ts +14 -0
- package/data/build/dist/shared/contracts/errors.js +15 -0
- package/data/build/dist/shared/contracts/feature.d.ts +2 -0
- package/data/build/dist/shared/contracts/file-management.d.ts +6 -0
- package/data/build/dist/shared/contracts/file-management.js +3 -0
- package/data/build/dist/shared/contracts/peer-host.d.ts +116 -15
- package/data/build/dist/shared/contracts/peer-host.js +19 -0
- package/data/build/dist/shared/contracts/version.d.ts +1 -1
- package/data/build/dist/shared/contracts/version.js +1 -1
- package/data/build/dist/shared/index.d.ts +4 -3
- package/data/build/dist/shared/index.js +2 -2
- package/data/build/dist/transport/debug.d.ts +5 -0
- package/data/build/dist/transport/debug.js +23 -1
- package/data/build/dist/transport/dsh-transport.d.ts +2 -0
- package/data/build/dist/transport/dsh-transport.js +11 -0
- package/data/build/dist/transport/webrtc-client.d.ts +2 -0
- package/data/build/dist/transport/webrtc-client.js +2 -1
- package/data/build/dist/transport/webrtc-host.d.ts +2 -0
- package/data/build/dist/transport/webrtc-host.js +6 -3
- package/data/build/dist/typert.host.d.ts +1 -1
- package/data/build/dist/typert.host.js +2 -1
- package/dsh.bundle.patch +10 -0
- package/package.json +19 -11
- package/profile/README.md +2 -2
- package/profile/package.json +2 -2
- package/profile/version.json +1 -1
- package/version.json +1 -1
|
@@ -21,6 +21,8 @@ function installComposerStyles() {
|
|
|
21
21
|
'[data-composer-card] > div:has([data-slot="conversation.input.right"]) [data-slot="conversation.input.right"] > .codingns4dsh-model-root{min-width:0;max-width:min(360px,45cqw);flex:1 1 min(360px,45cqw)}',
|
|
22
22
|
'[data-composer-card] > div:has([data-slot="conversation.input.right"]) [data-slot="conversation.input.model"] > select{width:100%;min-width:0;max-width:min(150px,45cqw);flex:1 1 min(150px,45cqw);overflow:hidden;white-space:nowrap}',
|
|
23
23
|
'[data-composer-card] > div:has([data-slot="conversation.input.right"]) [data-slot="conversation.input.model"] > button{width:100%;min-width:0;max-width:min(360px,45cqw);overflow:hidden;white-space:nowrap}',
|
|
24
|
+
// 移动端工具栏空间有限,适配器按钮沿用模型选择器的紧凑图标形态。
|
|
25
|
+
'@media (max-width: 768px){.codingns4dsh-agent-trigger > .codingns4dsh-agent-label{display:none}.codingns4dsh-agent-trigger > svg{display:none!important}.codingns4dsh-agent-trigger{padding-left:4px;padding-right:4px;gap:0}[data-composer-card] > div:has([data-slot="conversation.input.right"]) > div:has(> [data-slot="conversation.input.right"]),[data-composer-card] [data-slot="conversation.input.right"],.uV2eYG_standardControls,.uV2eYG_trailing{gap:0!important;column-gap:0!important}}',
|
|
24
26
|
// ContextMeter 在 pressure 尚未合并时会暂时返回 null;dock 保留同样的行高,数值回来时只更新内容。
|
|
25
27
|
'[data-composer-card] + div{box-sizing:border-box;min-height:26px;align-items:center}',
|
|
26
28
|
'[data-composer-card] + div svg[viewBox="0 0 14 14"] circle:last-child{transition:stroke-dasharray .18s ease,stroke .18s ease}',
|
|
@@ -38,6 +40,11 @@ function installComposerStyles() {
|
|
|
38
40
|
const DEFAULT_SELECTION = { adapterId: 'dsh' };
|
|
39
41
|
const selections = new Map();
|
|
40
42
|
const selectionListeners = new Map();
|
|
43
|
+
/** 两个 Slot 共用同一条初始化读取,避免响应顺序造成状态回退。 */
|
|
44
|
+
const selectionLoads = new Map();
|
|
45
|
+
/** 记录每个会话最新的写入,旧响应不能覆盖用户较新的选择。 */
|
|
46
|
+
const selectionUpdates = new Map();
|
|
47
|
+
const selectionRevisions = new Map();
|
|
41
48
|
/** 在 Agent 和模型两个 Slot 之间共享当前会话选择。 */
|
|
42
49
|
function useSelection(sessionId, rpc) {
|
|
43
50
|
const [selection, setSelection] = useState(() => sessionId ? selections.get(sessionId) ?? DEFAULT_SELECTION : DEFAULT_SELECTION);
|
|
@@ -45,7 +52,9 @@ function useSelection(sessionId, rpc) {
|
|
|
45
52
|
if (sessionId === undefined || sessionId.trim() === '')
|
|
46
53
|
return;
|
|
47
54
|
let active = true;
|
|
48
|
-
|
|
55
|
+
const loaded = selectionLoads.get(sessionId) ?? callCliRpc(rpc, 'session/get', { sessionId });
|
|
56
|
+
selectionLoads.set(sessionId, loaded);
|
|
57
|
+
void loaded
|
|
49
58
|
.then((value) => {
|
|
50
59
|
if (!active)
|
|
51
60
|
return;
|
|
@@ -54,7 +63,11 @@ function useSelection(sessionId, rpc) {
|
|
|
54
63
|
return;
|
|
55
64
|
publishSelection(sessionId, value);
|
|
56
65
|
})
|
|
57
|
-
.catch(() => undefined)
|
|
66
|
+
.catch(() => undefined)
|
|
67
|
+
.finally(() => {
|
|
68
|
+
if (selectionLoads.get(sessionId) === loaded)
|
|
69
|
+
selectionLoads.delete(sessionId);
|
|
70
|
+
});
|
|
58
71
|
const listeners = selectionListeners.get(sessionId) ?? new Set();
|
|
59
72
|
selectionListeners.set(sessionId, listeners);
|
|
60
73
|
const listener = () => setSelection(selections.get(sessionId) ?? DEFAULT_SELECTION);
|
|
@@ -72,9 +85,20 @@ function useSelection(sessionId, rpc) {
|
|
|
72
85
|
if (sessionId === undefined || sessionId.trim() === '')
|
|
73
86
|
return;
|
|
74
87
|
publishSelection(sessionId, next);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
const revision = (selectionRevisions.get(sessionId) ?? 0) + 1;
|
|
89
|
+
selectionRevisions.set(sessionId, revision);
|
|
90
|
+
const promise = callCliRpc(rpc, 'session/set', { sessionId, ...next });
|
|
91
|
+
selectionUpdates.set(sessionId, { revision, promise });
|
|
92
|
+
void promise
|
|
93
|
+
.then((normalized) => {
|
|
94
|
+
if (selectionUpdates.get(sessionId)?.revision === revision)
|
|
95
|
+
publishSelection(sessionId, normalized);
|
|
96
|
+
})
|
|
97
|
+
.catch(() => undefined)
|
|
98
|
+
.finally(() => {
|
|
99
|
+
if (selectionUpdates.get(sessionId)?.revision === revision)
|
|
100
|
+
selectionUpdates.delete(sessionId);
|
|
101
|
+
});
|
|
78
102
|
};
|
|
79
103
|
return [selection, update];
|
|
80
104
|
}
|
|
@@ -119,7 +143,7 @@ function AgentSlot(props) {
|
|
|
119
143
|
const [selection, update] = useSelection(sessionId, props.rpc);
|
|
120
144
|
const [agents, setAgents] = useState([{ id: 'dsh', name: 'DeepSeek Harness', installed: true, enabled: true, version: null, command: null }]);
|
|
121
145
|
const [open, setOpen] = useState(false);
|
|
122
|
-
const locked =
|
|
146
|
+
const locked = isSessionLocked(session);
|
|
123
147
|
useEffect(() => {
|
|
124
148
|
if (typeof document === 'undefined')
|
|
125
149
|
return;
|
|
@@ -152,7 +176,7 @@ function AgentSlot(props) {
|
|
|
152
176
|
const currentIcon = providerIconUrl(current.id);
|
|
153
177
|
return createElement('div', { style: agentRootStyle }, createElement('button', { type: 'button', className: 'codingns4dsh-agent-trigger', disabled: locked, onClick: () => setOpen((value) => !value), 'aria-label': t('cli.currentAgent', { name: current.name, locked: locked ? t('cli.locked') : '' }), 'aria-haspopup': 'menu', 'aria-expanded': open, style: { ...agentTriggerStyle, cursor: locked ? 'default' : 'pointer', opacity: locked ? 0.7 : 1 } }, currentIcon === undefined
|
|
154
178
|
? createElement(ProviderIconFallback, { name: current.name, size: 20 })
|
|
155
|
-
: createElement('img', { src: currentIcon, alt: '', 'aria-hidden': true, style: applyProviderIconShape(current.id, agentTriggerIconStyle) }), createElement('span', { style: agentTriggerLabelStyle }, current.name), createElement(NativeDropdownChevron, { open, locked })), open && !locked && createElement('div', { role: 'menu', 'aria-label': t('cli.selectAgent'), style: agentMenuStyle }, ...agents.map((agent) => {
|
|
179
|
+
: createElement('img', { src: currentIcon, alt: '', 'aria-hidden': true, style: applyProviderIconShape(current.id, agentTriggerIconStyle) }), createElement('span', { className: 'codingns4dsh-agent-label', style: agentTriggerLabelStyle }, current.name), createElement(NativeDropdownChevron, { open, locked })), open && !locked && createElement('div', { role: 'menu', 'aria-label': t('cli.selectAgent'), style: agentMenuStyle }, ...agents.map((agent) => {
|
|
156
180
|
const selected = agent.id === selection.adapterId;
|
|
157
181
|
const available = agent.installed && agent.enabled;
|
|
158
182
|
const icon = providerIconUrl(agent.id);
|
|
@@ -263,17 +287,30 @@ function ModelSlot(props) {
|
|
|
263
287
|
if (!active)
|
|
264
288
|
return;
|
|
265
289
|
setCatalogState({ adapterId, value });
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
290
|
+
const normalize = () => {
|
|
291
|
+
if (!active)
|
|
292
|
+
return;
|
|
293
|
+
// session/set 可能正在回填适配器级记忆值;必须等它完成后再补默认值,
|
|
294
|
+
// 否则目录第一项会先写入 Host,随后覆盖真正的记忆选择。
|
|
295
|
+
const currentSelection = sessionId === undefined
|
|
296
|
+
? selection
|
|
297
|
+
: selections.get(sessionId) ?? selection;
|
|
298
|
+
if (currentSelection.adapterId !== adapterId)
|
|
299
|
+
return;
|
|
300
|
+
const model = findModel(value, currentSelection.modelId) ?? firstModel(value);
|
|
301
|
+
if (model === undefined)
|
|
302
|
+
return;
|
|
303
|
+
const effort = model.efforts.includes(currentSelection.effortId ?? '') ? currentSelection.effortId : defaultEffort(model.efforts);
|
|
304
|
+
if (model.id !== currentSelection.modelId || effort !== currentSelection.effortId)
|
|
305
|
+
update({ adapterId, modelId: model.id, ...(effort ? { effortId: effort } : {}) });
|
|
306
|
+
};
|
|
307
|
+
const pending = sessionId === undefined ? undefined : selectionUpdates.get(sessionId);
|
|
308
|
+
if (pending === undefined) {
|
|
309
|
+
normalize();
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
void pending.promise.then(normalize).catch(normalize);
|
|
313
|
+
}
|
|
277
314
|
})
|
|
278
315
|
.catch(() => { if (active)
|
|
279
316
|
setCatalogState({ adapterId, value: { groups: [], currentModel: null, currentEffort: null } }); })
|
|
@@ -343,5 +380,10 @@ function defaultEffort(efforts) {
|
|
|
343
380
|
return undefined;
|
|
344
381
|
return efforts.length > 2 ? efforts[efforts.length - 2] : efforts[efforts.length - 1];
|
|
345
382
|
}
|
|
383
|
+
/** 会话离开空白新建态后,适配器选择器不可再修改。 */
|
|
384
|
+
function isSessionLocked(session) {
|
|
385
|
+
return session !== undefined
|
|
386
|
+
&& (!session.blank || Boolean(session.promptAttempted) || Boolean(session.running) || (session.queue?.length ?? 0) > 0);
|
|
387
|
+
}
|
|
346
388
|
export { AgentSlot, ModelSlot };
|
|
347
389
|
//# sourceMappingURL=cli-slots.js.map
|
|
@@ -11,6 +11,8 @@ export interface DshH5BootstrapOptions {
|
|
|
11
11
|
readonly dshDeviceId?: string;
|
|
12
12
|
/** 登录保护开启中继范围时使用的短期 Host 签名票据。 */
|
|
13
13
|
readonly loginProtectionToken?: string;
|
|
14
|
+
/** 重连时动态读取最新登录保护票据,避免使用已续签前的旧值。 */
|
|
15
|
+
readonly getLoginProtectionToken?: () => string | undefined;
|
|
14
16
|
readonly signal?: AbortSignal;
|
|
15
17
|
readonly boot?: () => void | Promise<void>;
|
|
16
18
|
}
|
|
@@ -17,27 +17,87 @@ export async function startDshH5Bootstrap(options) {
|
|
|
17
17
|
const debug = createDshTransportDebugLogger({ side: 'h5', component: 'h5-bootstrap' });
|
|
18
18
|
const devices = await callRpc(options.rpc, 'auth/dsh/device/list', {}, signal);
|
|
19
19
|
const device = chooseDshDevice(devices, options.dshDeviceId);
|
|
20
|
-
const
|
|
20
|
+
const loginProtectionToken = () => options.getLoginProtectionToken?.() ?? options.loginProtectionToken;
|
|
21
|
+
const firstLoginProtectionToken = loginProtectionToken();
|
|
22
|
+
const firstTicket = await callRpc(options.rpc, 'auth/dsh/relayTicket', {
|
|
21
23
|
dshDeviceId: device.dshDeviceId,
|
|
22
|
-
...(
|
|
24
|
+
...(firstLoginProtectionToken === undefined ? {} : { loginProtectionToken: firstLoginProtectionToken }),
|
|
23
25
|
}, signal);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const hostScope = resolveDshHostScope(ticket);
|
|
32
|
-
const session = new DshSession({ carrier: connection.carrier, role: 'client', generation: String(generation), hostScope, dshVersion, debug });
|
|
26
|
+
let generation = 1;
|
|
27
|
+
let connection = await connectWebRtcClient(createWebRtcClientOptions(firstTicket, debug));
|
|
28
|
+
let session = new DshSession({ carrier: connection.carrier, role: 'client', generation: String(generation), hostScope: resolveDshHostScope(firstTicket), dshVersion, debug });
|
|
29
|
+
let reconnecting = false;
|
|
30
|
+
let stopped = false;
|
|
31
|
+
let reconnectRef;
|
|
32
|
+
let unsubscribeConnectionClosed;
|
|
33
33
|
const transport = new DshCodingNsTransport({
|
|
34
34
|
carrier: connection.carrier,
|
|
35
35
|
generation: { id: generation, host: { home: '/' } },
|
|
36
|
-
hostScope,
|
|
36
|
+
hostScope: resolveDshHostScope(firstTicket),
|
|
37
37
|
session,
|
|
38
38
|
requireSessionReady: true,
|
|
39
|
+
reconnect: (reconnectSignal) => reconnectRef?.(reconnectSignal) ?? Promise.reject(new Error('DSH 重连尚未就绪')),
|
|
39
40
|
debug,
|
|
40
41
|
});
|
|
42
|
+
const reconnect = async (reconnectSignal) => {
|
|
43
|
+
if (stopped || reconnecting)
|
|
44
|
+
return;
|
|
45
|
+
reconnecting = true;
|
|
46
|
+
transport.invalidateConnection(new Error('WebRTC connection closed'));
|
|
47
|
+
session.close('旧 WebRTC generation 已失效');
|
|
48
|
+
try {
|
|
49
|
+
for (let attempt = 0; !stopped; attempt += 1) {
|
|
50
|
+
let nextConnection;
|
|
51
|
+
let nextSession;
|
|
52
|
+
try {
|
|
53
|
+
const waitMs = Math.min(10_000, 500 * (attempt + 1));
|
|
54
|
+
if (attempt > 0)
|
|
55
|
+
await delay(waitMs, reconnectSignal ?? signal);
|
|
56
|
+
const currentLoginProtectionToken = loginProtectionToken();
|
|
57
|
+
const ticket = await callRpc(options.rpc, 'auth/dsh/relayTicket', {
|
|
58
|
+
dshDeviceId: device.dshDeviceId,
|
|
59
|
+
...(currentLoginProtectionToken === undefined ? {} : { loginProtectionToken: currentLoginProtectionToken }),
|
|
60
|
+
}, reconnectSignal ?? signal);
|
|
61
|
+
nextConnection = await connectWebRtcClient(createWebRtcClientOptions(ticket, debug));
|
|
62
|
+
nextSession = new DshSession({ carrier: nextConnection.carrier, role: 'client', generation: String(generation + 1), hostScope: resolveDshHostScope(ticket), dshVersion, debug });
|
|
63
|
+
nextSession.start();
|
|
64
|
+
await waitForSessionReady(nextSession, reconnectSignal ?? signal, 15_000);
|
|
65
|
+
const previous = connection;
|
|
66
|
+
connection = nextConnection;
|
|
67
|
+
session = nextSession;
|
|
68
|
+
generation += 1;
|
|
69
|
+
attachConnectionClose(nextConnection);
|
|
70
|
+
transport.replaceConnection(nextConnection.carrier, nextSession, { id: generation, host: { home: '/' } });
|
|
71
|
+
await previous.close();
|
|
72
|
+
nextConnection = undefined;
|
|
73
|
+
nextSession = undefined;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
await nextSession?.close();
|
|
78
|
+
await nextConnection?.close();
|
|
79
|
+
debug.log('bootstrap.reconnect.error', { generation, error: error instanceof Error ? error.message : String(error) });
|
|
80
|
+
if (stopped || signal?.aborted || reconnectSignal?.aborted)
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
reconnecting = false;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
reconnectRef = reconnect;
|
|
90
|
+
const attachConnectionClose = (current) => {
|
|
91
|
+
unsubscribeConnectionClosed?.();
|
|
92
|
+
unsubscribeConnectionClosed = current.onClosed((error) => {
|
|
93
|
+
if (stopped || current !== connection)
|
|
94
|
+
return;
|
|
95
|
+
const reason = error ?? new Error('WebRTC connection closed');
|
|
96
|
+
transport.invalidateConnection(reason);
|
|
97
|
+
session.close(reason.message);
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
attachConnectionClose(connection);
|
|
41
101
|
let registration;
|
|
42
102
|
try {
|
|
43
103
|
session.start();
|
|
@@ -46,14 +106,20 @@ export async function startDshH5Bootstrap(options) {
|
|
|
46
106
|
if (options.boot)
|
|
47
107
|
await options.boot();
|
|
48
108
|
const dispose = async () => {
|
|
109
|
+
stopped = true;
|
|
110
|
+
unsubscribeConnectionClosed?.();
|
|
111
|
+
unsubscribeConnectionClosed = undefined;
|
|
49
112
|
registration?.dispose();
|
|
50
113
|
session.close();
|
|
51
114
|
await transport.close();
|
|
52
115
|
await connection.close();
|
|
53
116
|
};
|
|
54
|
-
return { dshDeviceId: device.dshDeviceId, relayMode:
|
|
117
|
+
return { dshDeviceId: device.dshDeviceId, relayMode: firstTicket.iceTransportPolicy === 'relay' ? 'relay' : 'direct', registration, dispose };
|
|
55
118
|
}
|
|
56
119
|
catch (error) {
|
|
120
|
+
stopped = true;
|
|
121
|
+
unsubscribeConnectionClosed?.();
|
|
122
|
+
unsubscribeConnectionClosed = undefined;
|
|
57
123
|
registration?.dispose();
|
|
58
124
|
session.close();
|
|
59
125
|
await transport.close();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import type { FeaturePanelProps } from './types.js';
|
|
3
|
+
/** 文件管理增强的独立能力开关面板。 */
|
|
4
|
+
export declare function FileManagementPanel({ services, enabled, snapshot, notify }: FeaturePanelProps): ReactElement;
|
|
5
|
+
//# sourceMappingURL=file-management-panel.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
import { CODINGNS_FILE_MANAGEMENT_FIELD, DEFAULT_FILE_MANAGEMENT_SETTINGS, } from '../../shared/contracts/config.js';
|
|
3
|
+
import { dshFormRootStyle, dshSettingsHelpStyle, dshSettingsListRowStyle, dshThemeColor, } from '../theme.js';
|
|
4
|
+
import { useCodingNsTranslator } from '../locale.js';
|
|
5
|
+
/** 文件管理增强的独立能力开关面板。 */
|
|
6
|
+
export function FileManagementPanel({ services, enabled, snapshot, notify }) {
|
|
7
|
+
const t = useCodingNsTranslator(services.locale);
|
|
8
|
+
const value = { ...DEFAULT_FILE_MANAGEMENT_SETTINGS, ...snapshot.value?.fileManagement };
|
|
9
|
+
const disabled = !enabled || snapshot.status === 'loading' || !snapshot.writable;
|
|
10
|
+
const updateSetting = (field, nextValue) => {
|
|
11
|
+
void services.settings.mutate([{
|
|
12
|
+
op: 'set',
|
|
13
|
+
path: [CODINGNS_FILE_MANAGEMENT_FIELD, field],
|
|
14
|
+
value: nextValue,
|
|
15
|
+
}]).then((accepted) => {
|
|
16
|
+
if (!accepted) {
|
|
17
|
+
notify({ kind: 'error', message: t('settings.moduleWriteRejected') });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
notify({ kind: 'success', message: t('fileManagement.saved') });
|
|
21
|
+
}).catch((cause) => {
|
|
22
|
+
notify({ kind: 'error', message: cause instanceof Error ? cause.message : String(cause) });
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
return createElement('div', {
|
|
26
|
+
'aria-disabled': disabled,
|
|
27
|
+
style: {
|
|
28
|
+
...dshFormRootStyle,
|
|
29
|
+
display: 'flex',
|
|
30
|
+
flexDirection: 'column',
|
|
31
|
+
gap: 12,
|
|
32
|
+
opacity: disabled ? 0.5 : 1,
|
|
33
|
+
pointerEvents: disabled ? 'none' : 'auto',
|
|
34
|
+
},
|
|
35
|
+
}, createSwitchRow(t('fileManagement.menuEnhancement'), t('fileManagement.menuEnhancementDescription'), value.menuEnhancement, disabled, (next) => updateSetting('menuEnhancement', next)), createSwitchRow(t('fileManagement.fileEditor'), t('fileManagement.fileEditorDescription'), value.fileEditor, disabled, (next) => updateSetting('fileEditor', next)), createSwitchRow(t('fileManagement.sessionChangedFiles'), t('fileManagement.sessionChangedFilesDescription'), value.sessionChangedFiles, disabled, (next) => updateSetting('sessionChangedFiles', next)));
|
|
36
|
+
}
|
|
37
|
+
function createSwitchRow(label, description, checked, disabled, onChange) {
|
|
38
|
+
return createElement('label', { style: dshSettingsListRowStyle }, createElement('span', { style: { minWidth: 0 } }, createElement('strong', { style: { display: 'block', fontSize: 13, lineHeight: 1.4 } }, label), createElement('span', { style: { display: 'block', marginTop: 3, ...dshSettingsHelpStyle } }, description)), createElement('input', {
|
|
39
|
+
type: 'checkbox',
|
|
40
|
+
role: 'switch',
|
|
41
|
+
'aria-label': label,
|
|
42
|
+
checked,
|
|
43
|
+
disabled,
|
|
44
|
+
onChange: (event) => onChange(event.currentTarget.checked),
|
|
45
|
+
style: { flex: '0 0 auto', accentColor: dshThemeColor.accent },
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=file-management-panel.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { DEFAULT_FILE_MANAGEMENT_SETTINGS } from '../../shared/contracts/config.js';
|
|
2
|
+
import { startFileManagementDom } from '../file-management-dom.js';
|
|
3
|
+
import { FileManagementPanel } from './file-management-panel.js';
|
|
4
|
+
import { registerSessionChangedFilesView } from '../session-changed-files-view.js';
|
|
5
|
+
/** 文件管理增强:只在模块启用期间挂载右键菜单和文本编辑器。 */
|
|
6
|
+
export const fileManagementFeature = {
|
|
7
|
+
descriptor: {
|
|
8
|
+
name: 'fileManagement',
|
|
9
|
+
version: '0.1.0',
|
|
10
|
+
enabledByDefault: false,
|
|
11
|
+
dependencies: [],
|
|
12
|
+
runtime: 'client',
|
|
13
|
+
ui: {
|
|
14
|
+
label: '文件管理增强',
|
|
15
|
+
description: '为 DSH 文件侧栏增加操作菜单,并支持编辑和保存常用文本文件。',
|
|
16
|
+
labelKey: 'feature.fileManagement.label',
|
|
17
|
+
descriptionKey: 'feature.fileManagement.description',
|
|
18
|
+
order: 40,
|
|
19
|
+
defaultOpen: true,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
start(context) {
|
|
23
|
+
const readOptions = () => {
|
|
24
|
+
const value = { ...DEFAULT_FILE_MANAGEMENT_SETTINGS, ...context.services.settings.getSnapshot().value?.fileManagement };
|
|
25
|
+
return { menuEnhancement: value.menuEnhancement, fileEditor: value.fileEditor };
|
|
26
|
+
};
|
|
27
|
+
const dom = startFileManagementDom(context.services.rpc, readOptions());
|
|
28
|
+
let disposeSessionView;
|
|
29
|
+
const syncSessionView = () => {
|
|
30
|
+
const value = { ...DEFAULT_FILE_MANAGEMENT_SETTINGS, ...context.services.settings.getSnapshot().value?.fileManagement };
|
|
31
|
+
if (value.sessionChangedFiles && disposeSessionView === undefined) {
|
|
32
|
+
disposeSessionView = registerSessionChangedFilesView(context.services.uiContext, context.services.rpc, context.services.remote);
|
|
33
|
+
}
|
|
34
|
+
else if (!value.sessionChangedFiles && disposeSessionView !== undefined) {
|
|
35
|
+
disposeSessionView();
|
|
36
|
+
disposeSessionView = undefined;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
syncSessionView();
|
|
40
|
+
const unsubscribe = context.services.settings.subscribe(() => {
|
|
41
|
+
dom.setOptions(readOptions());
|
|
42
|
+
syncSessionView();
|
|
43
|
+
});
|
|
44
|
+
context.resources.add(unsubscribe);
|
|
45
|
+
context.resources.add(dom.dispose);
|
|
46
|
+
context.resources.add(() => disposeSessionView?.());
|
|
47
|
+
},
|
|
48
|
+
settingsPanel: FileManagementPanel,
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=file-management.js.map
|
|
@@ -8,6 +8,8 @@ import { terminalEnhancementFeature } from './terminal-enhancement.js';
|
|
|
8
8
|
import { workspaceSessionEnhancementFeature } from './workspace-session-enhancement.js';
|
|
9
9
|
import { debugFeature } from './debug.js';
|
|
10
10
|
import { gitManagementFeature } from '../git-management.js';
|
|
11
|
+
import { fileManagementFeature } from './file-management.js';
|
|
12
|
+
import { peerHostFeature } from './peer-host.js';
|
|
11
13
|
import type { CodingNsClientFeatureModule, CodingNsClientServices } from './types.js';
|
|
12
14
|
/** Client 侧功能模块清单:新增模块在这里登记一行,不需要改动设置页和入口。 */
|
|
13
15
|
export declare const CLIENT_FEATURES: readonly CodingNsClientFeatureModule[];
|
|
@@ -22,9 +24,16 @@ export interface CodingNsSettingsModule {
|
|
|
22
24
|
* 归属 Host 的模块由 Host 侧配置承载,这里跳过;没有 ui 描述的模块不显示。
|
|
23
25
|
*/
|
|
24
26
|
export declare function settingsModules(registry: FeatureRegistry<CodingNsClientServices, CodingNsClientFeatureModule>): readonly CodingNsSettingsModule[];
|
|
25
|
-
export { lanAccessFeature, loginProtectionFeature, reverseProxyFeature, cliAdaptersFeature, workspaceSessionEnhancementFeature, terminalEnhancementFeature, debugFeature, gitManagementFeature };
|
|
27
|
+
export { lanAccessFeature, loginProtectionFeature, reverseProxyFeature, cliAdaptersFeature, workspaceSessionEnhancementFeature, terminalEnhancementFeature, debugFeature, gitManagementFeature, fileManagementFeature, peerHostFeature };
|
|
26
28
|
export { startBrowserRelayConnection } from './reverse-proxy.js';
|
|
27
29
|
export { LanAccessPanel } from './lan-access-panel.js';
|
|
30
|
+
export { startPeerHostConnectionButton, PEER_HOST_BUTTON_ATTRIBUTE, PEER_HOST_OPEN_EVENT } from '../peer-host-connection-button.js';
|
|
31
|
+
export { startPeerHostManagementPanel } from '../peer-host-management-panel.js';
|
|
32
|
+
export { createPeerHostManagementApi } from '../peer-host-management-api.js';
|
|
33
|
+
export { createPeerHostScopedClient } from '../peer-host-scoped-client.js';
|
|
34
|
+
export { createPeerHostWebSocketFactory } from '../peer-host-scoped-client.js';
|
|
35
|
+
export { startPeerHostNativeNavigation, startPeerHostNativeSession } from '../peer-host-native-session-ui.js';
|
|
36
|
+
export { PeerHostSessionController } from '../peer-host-session-controller.js';
|
|
28
37
|
export { LoginProtectionPanel } from './login-protection-panel.js';
|
|
29
38
|
export { ReverseProxyPanel } from './reverse-proxy-panel.js';
|
|
30
39
|
export { CliAdaptersPanel } from './cli-adapters.js';
|
|
@@ -6,6 +6,8 @@ import { terminalEnhancementFeature } from './terminal-enhancement.js';
|
|
|
6
6
|
import { workspaceSessionEnhancementFeature } from './workspace-session-enhancement.js';
|
|
7
7
|
import { debugFeature } from './debug.js';
|
|
8
8
|
import { gitManagementFeature } from '../git-management.js';
|
|
9
|
+
import { fileManagementFeature } from './file-management.js';
|
|
10
|
+
import { peerHostFeature } from './peer-host.js';
|
|
9
11
|
/** Client 侧功能模块清单:新增模块在这里登记一行,不需要改动设置页和入口。 */
|
|
10
12
|
export const CLIENT_FEATURES = [
|
|
11
13
|
lanAccessFeature,
|
|
@@ -16,6 +18,8 @@ export const CLIENT_FEATURES = [
|
|
|
16
18
|
terminalEnhancementFeature,
|
|
17
19
|
debugFeature,
|
|
18
20
|
gitManagementFeature,
|
|
21
|
+
fileManagementFeature,
|
|
22
|
+
peerHostFeature,
|
|
19
23
|
];
|
|
20
24
|
/**
|
|
21
25
|
* 汇总需要在设置页显示的模块,按 ui.order 升序。
|
|
@@ -35,9 +39,16 @@ export function settingsModules(registry) {
|
|
|
35
39
|
entries.sort((left, right) => (left.ui.order ?? 0) - (right.ui.order ?? 0));
|
|
36
40
|
return entries;
|
|
37
41
|
}
|
|
38
|
-
export { lanAccessFeature, loginProtectionFeature, reverseProxyFeature, cliAdaptersFeature, workspaceSessionEnhancementFeature, terminalEnhancementFeature, debugFeature, gitManagementFeature };
|
|
42
|
+
export { lanAccessFeature, loginProtectionFeature, reverseProxyFeature, cliAdaptersFeature, workspaceSessionEnhancementFeature, terminalEnhancementFeature, debugFeature, gitManagementFeature, fileManagementFeature, peerHostFeature };
|
|
39
43
|
export { startBrowserRelayConnection } from './reverse-proxy.js';
|
|
40
44
|
export { LanAccessPanel } from './lan-access-panel.js';
|
|
45
|
+
export { startPeerHostConnectionButton, PEER_HOST_BUTTON_ATTRIBUTE, PEER_HOST_OPEN_EVENT } from '../peer-host-connection-button.js';
|
|
46
|
+
export { startPeerHostManagementPanel } from '../peer-host-management-panel.js';
|
|
47
|
+
export { createPeerHostManagementApi } from '../peer-host-management-api.js';
|
|
48
|
+
export { createPeerHostScopedClient } from '../peer-host-scoped-client.js';
|
|
49
|
+
export { createPeerHostWebSocketFactory } from '../peer-host-scoped-client.js';
|
|
50
|
+
export { startPeerHostNativeNavigation, startPeerHostNativeSession } from '../peer-host-native-session-ui.js';
|
|
51
|
+
export { PeerHostSessionController } from '../peer-host-session-controller.js';
|
|
41
52
|
export { LoginProtectionPanel } from './login-protection-panel.js';
|
|
42
53
|
export { ReverseProxyPanel } from './reverse-proxy-panel.js';
|
|
43
54
|
export { CliAdaptersPanel } from './cli-adapters.js';
|
|
@@ -66,7 +66,7 @@ export function LoginProtectionPanel({ services, enabled, snapshot, notify }) {
|
|
|
66
66
|
return createElement('div', {
|
|
67
67
|
style: { ...dshFormRootStyle, display: 'flex', flexDirection: 'column', gap: 14, opacity: controlsDisabled ? 0.5 : 1 },
|
|
68
68
|
'aria-disabled': controlsDisabled,
|
|
69
|
-
}, createElement('p', { style: { margin: 0, color: dshThemeColor.labelSecondary, fontSize: 13, lineHeight: 1.5 } }, '为局域网和中继访问统一增加本地账号验证。本机 127.0.0.1 / ::1 永远不受此保护。密码只在 Host 侧验证,不会进入浏览器设置或 URL
|
|
69
|
+
}, createElement('p', { style: { margin: 0, color: dshThemeColor.labelSecondary, fontSize: 13, lineHeight: 1.5 } }, '为局域网和中继访问统一增加本地账号验证。本机 127.0.0.1 / ::1 永远不受此保护。密码只在 Host 侧验证,不会进入浏览器设置或 URL;页面保持打开时,会在会话临近超时时自动续期。'), createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 8, color: dshThemeColor.labelSecondary, fontSize: 13 } }, createElement('input', { type: 'checkbox', checked: active, disabled: controlsDisabled || busy, onChange: (event) => setActive(event.currentTarget.checked), style: { accentColor: dshThemeColor.accent } }), createElement('span', undefined, '启用登录保护')), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, '本地用户名'), createElement('input', { value: username, disabled: controlsDisabled || busy || !active, autoComplete: 'username', onChange: (event) => setUsername(event.currentTarget.value), style: field })), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, saved.passwordConfigured ? '新密码(留空保持不变)' : '密码(至少 8 位)'), createElement('input', { type: 'password', value: password, disabled: controlsDisabled || busy || !active, autoComplete: 'new-password', onChange: (event) => setPassword(event.currentTarget.value), style: field })), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, '会话超时(秒)'), createElement('input', { type: 'number', min: 60, max: 604800, value: timeoutSeconds, disabled: controlsDisabled || busy || !active, onChange: (event) => setTimeoutSeconds(event.currentTarget.value), style: field })), createElement('fieldset', { disabled: controlsDisabled || busy || !active, style: { display: 'flex', flexDirection: 'column', gap: 8, margin: 0, padding: 12, border: `1px solid ${dshThemeColor.border}`, borderRadius: 6 } }, createElement('legend', { style: dshSettingsFieldLabelStyle }, '应用范围'), ...[
|
|
70
70
|
['lan', '局域网访问', '局域网网卡入口'],
|
|
71
71
|
['relay', '中继访问', 'Codingns4DSH 中继'],
|
|
72
72
|
].map(([key, label, hint]) => createElement('label', { key, style: { display: 'flex', alignItems: 'center', gap: 8, color: dshThemeColor.labelSecondary, fontSize: 13 } }, createElement('input', { type: 'checkbox', checked: scopes[key], onChange: () => toggleScope(key), style: { accentColor: dshThemeColor.accent } }), createElement('span', undefined, `${label}(${hint})`)))), createElement('button', { type: 'button', disabled: controlsDisabled || busy, onClick: () => void save(), style: dshSettingsPrimaryButtonStyle }, busy ? '保存中…' : '保存登录保护'));
|
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
export declare const LOGIN_PROTECTION_SESSION_KEY = "codingns4dsh.login-protection.relay-token";
|
|
3
3
|
export declare const LOGIN_PROTECTION_SESSION_EVENT = "codingns4dsh-login-protection-session";
|
|
4
4
|
export declare function readLoginProtectionSession(): string | undefined;
|
|
5
|
+
/** 读取当前中继票据的过期时间;票据格式无效时返回 null。 */
|
|
6
|
+
export declare function readLoginProtectionSessionExpiresAt(): number | null;
|
|
5
7
|
export declare function writeLoginProtectionSession(token: string | undefined): void;
|
|
6
8
|
//# sourceMappingURL=login-protection-session.d.ts.map
|
|
@@ -11,6 +11,24 @@ export function readLoginProtectionSession() {
|
|
|
11
11
|
return memoryToken;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
/** 读取当前中继票据的过期时间;票据格式无效时返回 null。 */
|
|
15
|
+
export function readLoginProtectionSessionExpiresAt() {
|
|
16
|
+
const token = readLoginProtectionSession();
|
|
17
|
+
if (token === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
const encoded = token.split('.', 1)[0];
|
|
21
|
+
if (encoded === undefined)
|
|
22
|
+
return null;
|
|
23
|
+
const padded = encoded.replace(/-/gu, '+').replace(/_/gu, '/');
|
|
24
|
+
const binary = atob(padded.padEnd(Math.ceil(padded.length / 4) * 4, '='));
|
|
25
|
+
const payload = JSON.parse(new TextDecoder().decode(Uint8Array.from(binary, (character) => character.charCodeAt(0))));
|
|
26
|
+
return typeof payload.expiresAt === 'number' && Number.isFinite(payload.expiresAt) ? payload.expiresAt : null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
14
32
|
export function writeLoginProtectionSession(token) {
|
|
15
33
|
memoryToken = token === undefined || token === '' ? undefined : token;
|
|
16
34
|
try {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { startPeerHostConnectionButton } from '../peer-host-connection-button.js';
|
|
2
|
+
import { startPeerHostManagementPanel } from '../peer-host-management-panel.js';
|
|
3
|
+
import { startPeerHostNativeNavigation, startPeerHostNativeSession } from '../peer-host-native-session-ui.js';
|
|
4
|
+
import { createPeerHostManagementApi } from '../peer-host-management-api.js';
|
|
5
|
+
import { createPeerHostWebSocketFactory } from '../peer-host-scoped-client.js';
|
|
6
|
+
/** PeerHost Client 模块的边界声明;实际连接资源在后续任务中装配。 */
|
|
7
|
+
export const peerHostFeature = {
|
|
8
|
+
descriptor: {
|
|
9
|
+
name: 'peerHost',
|
|
10
|
+
version: '0.1.0',
|
|
11
|
+
enabledByDefault: false,
|
|
12
|
+
// PeerHost 暂停启用,待 Host-to-Host/relay 在真实 DSH 环境完成验收后再开放。
|
|
13
|
+
disabled: true,
|
|
14
|
+
dependencies: [],
|
|
15
|
+
runtime: 'client',
|
|
16
|
+
requires: [
|
|
17
|
+
{ capability: 'peer-host.native-navigation', required: false, fallback: 'degrade' },
|
|
18
|
+
{ capability: 'peer-host.remote-web-context-fallback', required: false, fallback: 'degrade' },
|
|
19
|
+
],
|
|
20
|
+
ui: {
|
|
21
|
+
label: '管理其他 DSH Host',
|
|
22
|
+
description: 'PeerHost 暂时停用,待 Host-to-Host 能力完成验收后再启用。',
|
|
23
|
+
order: 50,
|
|
24
|
+
defaultOpen: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
start: async (context) => {
|
|
28
|
+
const controller = startPeerHostConnectionButton();
|
|
29
|
+
context.resources.add(() => controller.dispose());
|
|
30
|
+
const panel = startPeerHostManagementPanel({ rpc: context.services.rpc });
|
|
31
|
+
context.resources.add(() => panel.dispose());
|
|
32
|
+
const management = createPeerHostManagementApi(context.services.rpc);
|
|
33
|
+
let endpoint = null;
|
|
34
|
+
let results = null;
|
|
35
|
+
let aggregateError;
|
|
36
|
+
try {
|
|
37
|
+
;
|
|
38
|
+
[endpoint, results] = await Promise.all([management.webSocketEndpoint(), management.aggregate()]);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
aggregateError = error;
|
|
42
|
+
try {
|
|
43
|
+
endpoint = await management.webSocketEndpoint();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
endpoint = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let socketFactory;
|
|
50
|
+
if (endpoint !== null) {
|
|
51
|
+
try {
|
|
52
|
+
socketFactory = createPeerHostWebSocketFactory(endpoint);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
endpoint = null;
|
|
56
|
+
aggregateError = new Error('PeerHost 实时通道端点不可用');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const session = startPeerHostNativeSession({
|
|
60
|
+
controller: context.services.peerHostSession,
|
|
61
|
+
client: context.services.peerHost,
|
|
62
|
+
...(socketFactory === undefined ? {} : { socketFactory }),
|
|
63
|
+
});
|
|
64
|
+
context.resources.add(() => session.close());
|
|
65
|
+
const navigation = startPeerHostNativeNavigation({
|
|
66
|
+
controller: context.services.peerHostSession,
|
|
67
|
+
onSelect: async (scope) => {
|
|
68
|
+
const selected = await context.services.peerHostSession.select({
|
|
69
|
+
hostId: scope.hostId,
|
|
70
|
+
targetHostId: scope.targetHostId,
|
|
71
|
+
workspaceId: scope.workspaceId,
|
|
72
|
+
sessionId: scope.sessionId,
|
|
73
|
+
});
|
|
74
|
+
await session.open(selected);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
context.resources.add(() => navigation.dispose());
|
|
78
|
+
if (results !== null)
|
|
79
|
+
navigation.refresh(results);
|
|
80
|
+
else
|
|
81
|
+
navigation.setStatus({
|
|
82
|
+
status: 'degraded',
|
|
83
|
+
reason: aggregateError instanceof Error && aggregateError.message === 'PeerHost 实时通道端点不可用'
|
|
84
|
+
? aggregateError.message
|
|
85
|
+
: 'PeerHost 聚合摘要暂不可用',
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=peer-host.js.map
|