@jingyi0605/codingns4dsh 0.1.2 → 0.1.7-rc.2
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 +232 -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
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { CODINGNS_RPC_CHANNEL } from '../shared/contracts/transport.js';
|
|
2
|
+
/** 根据当前 Host 自有端点生成作用域 WebSocket 工厂;不接受目标 Host URL。 */
|
|
3
|
+
export function createPeerHostWebSocketFactory(endpoint, options = {}) {
|
|
4
|
+
if (!Number.isSafeInteger(endpoint.port) || endpoint.port < 0 || endpoint.port > 65_535)
|
|
5
|
+
throw new TypeError('PeerHost WebSocket 端口无效');
|
|
6
|
+
const path = endpoint.path.startsWith('/api/') && !endpoint.path.includes('..') ? endpoint.path : (() => { throw new TypeError('PeerHost WebSocket 路径无效'); })();
|
|
7
|
+
return async (scope) => {
|
|
8
|
+
assertPeerScope(scope);
|
|
9
|
+
const host = options.host?.trim() || wildcardHost(endpoint.host) || currentLocationHost();
|
|
10
|
+
if (host === '')
|
|
11
|
+
throw new Error('无法解析当前 Host WebSocket 地址');
|
|
12
|
+
const protocol = options.protocol ?? currentLocationProtocol();
|
|
13
|
+
const url = new URL(`${protocol}://${host}:${String(endpoint.port)}${path}`);
|
|
14
|
+
url.searchParams.set('hostId', scope.hostId);
|
|
15
|
+
url.searchParams.set('targetHostId', scope.targetHostId);
|
|
16
|
+
url.searchParams.set('workspaceId', scope.workspaceId);
|
|
17
|
+
if (scope.sessionId !== null)
|
|
18
|
+
url.searchParams.set('sessionId', scope.sessionId);
|
|
19
|
+
url.searchParams.set('scopeGeneration', String(scope.scopeGeneration));
|
|
20
|
+
const WebSocketCtor = globalThis.WebSocket;
|
|
21
|
+
if (typeof WebSocketCtor !== 'function')
|
|
22
|
+
throw new Error('当前运行时没有 WebSocket');
|
|
23
|
+
return adaptBrowserWebSocket(new WebSocketCtor(url));
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** 将浏览器标准 WebSocket 事件适配为 Client 事件流使用的轻量接口。 */
|
|
27
|
+
function adaptBrowserWebSocket(socket) {
|
|
28
|
+
return {
|
|
29
|
+
get readyState() { return socket.readyState; },
|
|
30
|
+
send(data) { socket.send(data); },
|
|
31
|
+
close(code, reason) { socket.close(code, reason); },
|
|
32
|
+
on(event, listener) {
|
|
33
|
+
if (event === 'message') {
|
|
34
|
+
socket.addEventListener('message', (message) => listener(message.data, typeof message.data !== 'string'));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
socket.addEventListener(event, (...args) => listener(...args));
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Client 侧远端资源适配器;每一次请求都从 HostScope 生成,不保存目标凭据。 */
|
|
42
|
+
export function createPeerHostScopedClient(rpc) {
|
|
43
|
+
const request = async (scope, path, options = {}) => {
|
|
44
|
+
assertPeerScope(scope);
|
|
45
|
+
if (!path.startsWith('/api/') || path.includes('://'))
|
|
46
|
+
throw new TypeError('PeerHost 代理路径必须是固定 API 路径');
|
|
47
|
+
const payload = {
|
|
48
|
+
peerHostId: scope.targetHostId,
|
|
49
|
+
scope,
|
|
50
|
+
path,
|
|
51
|
+
...(options.method === undefined ? {} : { method: options.method }),
|
|
52
|
+
...(options.body === undefined ? {} : { body: options.body }),
|
|
53
|
+
};
|
|
54
|
+
let result;
|
|
55
|
+
try {
|
|
56
|
+
result = await rpc.call(CODINGNS_RPC_CHANNEL, 'peerHost/request', payload);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
60
|
+
if (!/HTTP (?:404|405)\b/u.test(message))
|
|
61
|
+
throw error;
|
|
62
|
+
result = await rpc.call('/api', 'codingns/peerHost/request', payload);
|
|
63
|
+
}
|
|
64
|
+
if (!result.ok)
|
|
65
|
+
throw new Error(result.error.message);
|
|
66
|
+
return result.value;
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
request,
|
|
70
|
+
loadSessionHistory: (scope, cursor) => request(requireSession(scope), `/api/sessions/${encodeURIComponent(scope.sessionId)}/history${cursor === undefined ? '' : `?cursor=${encodeURIComponent(cursor)}`}`),
|
|
71
|
+
sendMessage: (scope, body) => request(requireSession(scope), `/api/sessions/${encodeURIComponent(scope.sessionId)}/messages`, { method: 'POST', body }),
|
|
72
|
+
stopSession: (scope) => request(requireSession(scope), `/api/sessions/${encodeURIComponent(scope.sessionId)}/stop`, { method: 'POST', body: '{}' }),
|
|
73
|
+
replyPermission: (scope, body) => request(requireSession(scope), `/api/sessions/${encodeURIComponent(scope.sessionId)}/permission`, { method: 'POST', body }),
|
|
74
|
+
answerQuestion: (scope, body) => request(requireSession(scope), `/api/sessions/${encodeURIComponent(scope.sessionId)}/answer`, { method: 'POST', body }),
|
|
75
|
+
readFile: (scope, path) => request(scope, `/api/files?path=${encodeURIComponent(path)}`),
|
|
76
|
+
writeFile: (scope, body) => request(scope, '/api/files', { method: 'PUT', body }),
|
|
77
|
+
gitStatus: (scope) => request(scope, '/api/git/status'),
|
|
78
|
+
terminal: (scope, action, body) => request(scope, `/api/terminal/${action}`, { method: 'POST', ...(body === undefined ? {} : { body }) }),
|
|
79
|
+
rightTool: (scope, action, body) => request(scope, `/api/right-tools/${action}`, { method: 'POST', ...(body === undefined ? {} : { body }) }),
|
|
80
|
+
openEventStream: (scope, socketFactory, listener, options) => openEventStream(scope, socketFactory, listener, options),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const OPEN = 1;
|
|
84
|
+
const CLOSED = 3;
|
|
85
|
+
const SOCKET_OPEN_TIMEOUT_MS = 15_000;
|
|
86
|
+
const PEER_HOST_EVENT_TYPES = new Set([
|
|
87
|
+
'peerHost.error',
|
|
88
|
+
'system.connected', 'workbench.snapshot', 'workbench.delta', 'fileTree.snapshot',
|
|
89
|
+
'git.snapshot', 'session.subscribed', 'session.backfill', 'session.delta',
|
|
90
|
+
'session.runtime_message', 'session.runtime_status', 'session.activity',
|
|
91
|
+
'session.permission_request', 'session.error', 'terminal.output', 'terminal.status',
|
|
92
|
+
'terminal.exit', 'terminal.error', 'rightTool.snapshot', 'rightTool.delta',
|
|
93
|
+
]);
|
|
94
|
+
async function openEventStream(scope, socketFactory, listener, options = {}) {
|
|
95
|
+
assertPeerScope(scope);
|
|
96
|
+
const maxReconnectAttempts = normalizeReconnectAttempts(options.maxReconnectAttempts);
|
|
97
|
+
const reconnectDelaysMs = normalizeReconnectDelays(options.reconnectDelaysMs);
|
|
98
|
+
const initialSocket = await socketFactory(scope);
|
|
99
|
+
try {
|
|
100
|
+
await waitForSocketOpen(initialSocket);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
closeSocket(initialSocket, 1011, 'PeerHost WebSocket 打开失败');
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
let socket = initialSocket;
|
|
107
|
+
let closed = false;
|
|
108
|
+
let reconnectTimer;
|
|
109
|
+
let reconnectAttempt = 0;
|
|
110
|
+
let connecting = false;
|
|
111
|
+
let pendingSocket;
|
|
112
|
+
const replayableSubscriptions = new Map();
|
|
113
|
+
const close = () => {
|
|
114
|
+
if (closed)
|
|
115
|
+
return;
|
|
116
|
+
closed = true;
|
|
117
|
+
if (reconnectTimer !== undefined)
|
|
118
|
+
clearTimeout(reconnectTimer);
|
|
119
|
+
reconnectTimer = undefined;
|
|
120
|
+
closeSocket(socket, 1000, 'PeerHost 作用域已清理');
|
|
121
|
+
if (pendingSocket !== undefined && pendingSocket !== socket)
|
|
122
|
+
closeSocket(pendingSocket, 1000, 'PeerHost 作用域已清理');
|
|
123
|
+
};
|
|
124
|
+
const send = (type, payload = {}) => {
|
|
125
|
+
if (!PEER_HOST_CLIENT_MESSAGE_TYPES.has(type))
|
|
126
|
+
throw new Error('PeerHost WebSocket 消息未加入白名单');
|
|
127
|
+
assertMessagePayload(payload);
|
|
128
|
+
if (closed || socket.readyState !== OPEN)
|
|
129
|
+
throw new Error('PeerHost WebSocket 尚未连接');
|
|
130
|
+
socket.send(JSON.stringify({ type, ...scope, ...payload }));
|
|
131
|
+
if (PEER_HOST_REPLAYABLE_MESSAGE_TYPES.has(type))
|
|
132
|
+
replayableSubscriptions.set(type, { ...payload });
|
|
133
|
+
};
|
|
134
|
+
const terminalInput = (payload) => { send('terminal.input', payload); };
|
|
135
|
+
const terminalResize = (payload) => { send('terminal.resize', payload); };
|
|
136
|
+
const terminalClose = (payload = {}) => { send('terminal.close', payload); };
|
|
137
|
+
const rightToolSubscribe = (payload) => { send('rightTool.subscribe', payload); };
|
|
138
|
+
const rightToolRefresh = (payload = {}) => { send('rightTool.refresh', payload); };
|
|
139
|
+
const rightToolClose = (payload = {}) => { send('rightTool.close', payload); };
|
|
140
|
+
const scheduleReconnect = () => {
|
|
141
|
+
if (closed || reconnectTimer !== undefined || reconnectAttempt >= maxReconnectAttempts)
|
|
142
|
+
return;
|
|
143
|
+
const delay = reconnectDelaysMs[Math.min(reconnectAttempt, reconnectDelaysMs.length - 1)];
|
|
144
|
+
reconnectAttempt += 1;
|
|
145
|
+
reconnectTimer = setTimeout(() => {
|
|
146
|
+
reconnectTimer = undefined;
|
|
147
|
+
void reconnect();
|
|
148
|
+
}, delay);
|
|
149
|
+
};
|
|
150
|
+
const attach = (next) => {
|
|
151
|
+
socket = next;
|
|
152
|
+
const current = next;
|
|
153
|
+
current.on('message', (data, isBinary) => {
|
|
154
|
+
if (closed || isBinary === true || typeof data !== 'string')
|
|
155
|
+
return;
|
|
156
|
+
const event = parseScopedEvent(data, scope);
|
|
157
|
+
if (event !== null)
|
|
158
|
+
listener(event);
|
|
159
|
+
});
|
|
160
|
+
current.on('close', () => {
|
|
161
|
+
if (!closed && socket === current)
|
|
162
|
+
scheduleReconnect();
|
|
163
|
+
});
|
|
164
|
+
current.on('error', () => {
|
|
165
|
+
if (closed || socket !== current)
|
|
166
|
+
return;
|
|
167
|
+
if (current.readyState === OPEN)
|
|
168
|
+
current.close(1011, 'PeerHost WebSocket 连接错误');
|
|
169
|
+
else
|
|
170
|
+
scheduleReconnect();
|
|
171
|
+
});
|
|
172
|
+
replaySubscriptions(current);
|
|
173
|
+
};
|
|
174
|
+
const reconnect = async () => {
|
|
175
|
+
if (closed || connecting)
|
|
176
|
+
return;
|
|
177
|
+
connecting = true;
|
|
178
|
+
try {
|
|
179
|
+
const next = await socketFactory(scope);
|
|
180
|
+
pendingSocket = next;
|
|
181
|
+
await waitForSocketOpen(next);
|
|
182
|
+
pendingSocket = undefined;
|
|
183
|
+
if (closed) {
|
|
184
|
+
closeSocket(next, 1000, 'PeerHost 作用域已清理');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
reconnectAttempt = 0;
|
|
188
|
+
attach(next);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
pendingSocket = undefined;
|
|
192
|
+
scheduleReconnect();
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
connecting = false;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
attach(socket);
|
|
199
|
+
return { close, send, terminalInput, terminalResize, terminalClose, rightToolSubscribe, rightToolRefresh, rightToolClose };
|
|
200
|
+
function replaySubscriptions(current) {
|
|
201
|
+
if (current.readyState !== OPEN)
|
|
202
|
+
return;
|
|
203
|
+
for (const [type, payload] of replayableSubscriptions) {
|
|
204
|
+
current.send(JSON.stringify({ type, ...scope, ...payload }));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function waitForSocketOpen(socket) {
|
|
209
|
+
if (socket.readyState === OPEN)
|
|
210
|
+
return;
|
|
211
|
+
if (socket.readyState === CLOSED)
|
|
212
|
+
throw new Error('PeerHost WebSocket 在打开前关闭');
|
|
213
|
+
await new Promise((resolve, reject) => {
|
|
214
|
+
let settled = false;
|
|
215
|
+
const finish = (error) => {
|
|
216
|
+
if (settled)
|
|
217
|
+
return;
|
|
218
|
+
settled = true;
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
if (error === undefined)
|
|
221
|
+
resolve();
|
|
222
|
+
else
|
|
223
|
+
reject(error);
|
|
224
|
+
};
|
|
225
|
+
const timer = setTimeout(() => finish(new Error('PeerHost WebSocket 打开超时')), SOCKET_OPEN_TIMEOUT_MS);
|
|
226
|
+
socket.on('open', () => finish());
|
|
227
|
+
socket.on('close', () => finish(new Error('PeerHost WebSocket 在打开前关闭')));
|
|
228
|
+
socket.on('error', () => finish(new Error('PeerHost WebSocket 打开失败')));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function closeSocket(socket, code, reason) {
|
|
232
|
+
if (socket.readyState !== CLOSED)
|
|
233
|
+
socket.close(code, reason);
|
|
234
|
+
}
|
|
235
|
+
const PEER_HOST_CLIENT_MESSAGE_TYPES = new Set([
|
|
236
|
+
'workbench.subscribe', 'workbench.refresh', 'fileTree.subscribe', 'fileTree.refresh',
|
|
237
|
+
'git.subscribe', 'git.refresh', 'session.subscribe', 'session.load_older',
|
|
238
|
+
'session.send', 'session.stop', 'session.permission_reply', 'session.answer',
|
|
239
|
+
'terminal.subscribe', 'terminal.input', 'terminal.resize', 'terminal.close',
|
|
240
|
+
'rightTool.subscribe', 'rightTool.refresh', 'rightTool.close',
|
|
241
|
+
]);
|
|
242
|
+
/** 这些消息描述可重建的订阅,重连后可以安全重放;命令和输入绝不能重放。 */
|
|
243
|
+
const PEER_HOST_REPLAYABLE_MESSAGE_TYPES = new Set([
|
|
244
|
+
'workbench.subscribe', 'fileTree.subscribe', 'git.subscribe', 'session.subscribe', 'terminal.subscribe', 'rightTool.subscribe',
|
|
245
|
+
]);
|
|
246
|
+
function parseScopedEvent(raw, expected) {
|
|
247
|
+
let value;
|
|
248
|
+
try {
|
|
249
|
+
value = JSON.parse(raw);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
255
|
+
return null;
|
|
256
|
+
const event = value;
|
|
257
|
+
if (typeof event.type !== 'string' || !PEER_HOST_EVENT_TYPES.has(event.type))
|
|
258
|
+
return null;
|
|
259
|
+
if (requiresSession(event.type) && (typeof event.sessionId !== 'string' || event.sessionId.trim() === ''))
|
|
260
|
+
return null;
|
|
261
|
+
if (event.hostId !== expected.hostId || event.targetHostId !== expected.targetHostId || event.workspaceId !== expected.workspaceId || event.scopeGeneration !== expected.scopeGeneration)
|
|
262
|
+
return null;
|
|
263
|
+
const eventSessionId = typeof event.sessionId === 'string' && event.sessionId.trim() !== '' ? event.sessionId : null;
|
|
264
|
+
if (eventSessionId !== expected.sessionId)
|
|
265
|
+
return null;
|
|
266
|
+
return event;
|
|
267
|
+
}
|
|
268
|
+
function requiresSession(type) {
|
|
269
|
+
return type.startsWith('session.');
|
|
270
|
+
}
|
|
271
|
+
function assertMessagePayload(payload) {
|
|
272
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload))
|
|
273
|
+
throw new TypeError('PeerHost WebSocket 消息参数必须是对象');
|
|
274
|
+
for (const key of ['type', 'hostId', 'targetHostId', 'workspaceId', 'sessionId', 'scopeGeneration']) {
|
|
275
|
+
if (Object.prototype.hasOwnProperty.call(payload, key))
|
|
276
|
+
throw new TypeError('PeerHost WebSocket 消息不得覆盖作用域字段');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function normalizeReconnectAttempts(value) {
|
|
280
|
+
if (value === undefined)
|
|
281
|
+
return 4;
|
|
282
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 8)
|
|
283
|
+
throw new TypeError('PeerHost WebSocket 重连次数无效');
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
function normalizeReconnectDelays(value) {
|
|
287
|
+
const delays = value === undefined ? [100, 250, 500, 1000] : [...value];
|
|
288
|
+
if (delays.length === 0 || delays.some((item) => !Number.isSafeInteger(item) || item < 0 || item > 30_000))
|
|
289
|
+
throw new TypeError('PeerHost WebSocket 重连等待时间无效');
|
|
290
|
+
return delays;
|
|
291
|
+
}
|
|
292
|
+
function requireSession(scope) {
|
|
293
|
+
assertPeerScope(scope);
|
|
294
|
+
if (scope.sessionId === null || scope.sessionId.trim() === '')
|
|
295
|
+
throw new TypeError('会话操作必须包含 sessionId');
|
|
296
|
+
return scope;
|
|
297
|
+
}
|
|
298
|
+
function assertPeerScope(scope) {
|
|
299
|
+
if (scope.targetHostId === null || scope.targetHostId.trim() === '')
|
|
300
|
+
throw new TypeError('PeerHost 作用域必须包含 targetHostId');
|
|
301
|
+
if (scope.hostId.trim() === '' || scope.workspaceId.trim() === '' || !Number.isSafeInteger(scope.scopeGeneration) || scope.scopeGeneration < 0)
|
|
302
|
+
throw new TypeError('PeerHost 作用域无效');
|
|
303
|
+
}
|
|
304
|
+
function wildcardHost(value) {
|
|
305
|
+
const host = value.trim();
|
|
306
|
+
return host === '0.0.0.0' || host === '::' || host === '[::]' ? '' : host;
|
|
307
|
+
}
|
|
308
|
+
function currentLocationHost() {
|
|
309
|
+
const location = globalThis.location;
|
|
310
|
+
return location?.hostname?.trim() ?? '';
|
|
311
|
+
}
|
|
312
|
+
function currentLocationProtocol() {
|
|
313
|
+
const location = globalThis.location;
|
|
314
|
+
return location?.protocol === 'https:' ? 'wss' : 'ws';
|
|
315
|
+
}
|
|
316
|
+
//# sourceMappingURL=peer-host-scoped-client.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HostScope } from '../shared/contracts/peer-host.js';
|
|
2
|
+
import { HostRouter, type HostRouterInput } from './host-router.js';
|
|
3
|
+
import type { PeerHostEventListener, PeerHostEventSocketFactory, PeerHostEventSubscription, PeerHostEventStreamOptions, PeerHostProxyResponse, PeerHostScopedClient } from './peer-host-scoped-client.js';
|
|
4
|
+
/**
|
|
5
|
+
* 远端会话的唯一 Client 入口。
|
|
6
|
+
*
|
|
7
|
+
* 控制器不保存目标凭据,只保存当前 HostScope;所有异步结果在提交前再次
|
|
8
|
+
* 校验 generation,避免切换工作区或会话后旧请求回写新页面。
|
|
9
|
+
*/
|
|
10
|
+
export declare class PeerHostSessionController {
|
|
11
|
+
private readonly router;
|
|
12
|
+
private readonly client;
|
|
13
|
+
constructor(router: HostRouter, client: PeerHostScopedClient);
|
|
14
|
+
select(input: HostRouterInput): Promise<HostScope>;
|
|
15
|
+
current(): HostScope | null;
|
|
16
|
+
/**
|
|
17
|
+
* Host 侧重连成功后重建同一逻辑作用域。
|
|
18
|
+
* `HostRouter.rebuild` 会先清理旧订阅,再递增 generation;摘要刷新回调只能使用新作用域。
|
|
19
|
+
*/
|
|
20
|
+
rebuildAfterReconnect(scope: HostScope, refreshSummary?: (nextScope: HostScope) => Promise<void> | void): Promise<HostScope>;
|
|
21
|
+
loadHistory(scope: HostScope, cursor?: string): Promise<PeerHostProxyResponse>;
|
|
22
|
+
sendMessage(scope: HostScope, body: string): Promise<PeerHostProxyResponse>;
|
|
23
|
+
stop(scope: HostScope): Promise<PeerHostProxyResponse>;
|
|
24
|
+
replyPermission(scope: HostScope, body: string): Promise<PeerHostProxyResponse>;
|
|
25
|
+
answerQuestion(scope: HostScope, body: string): Promise<PeerHostProxyResponse>;
|
|
26
|
+
subscribe(scope: HostScope, socketFactory: PeerHostEventSocketFactory, listener: PeerHostEventListener, options?: PeerHostEventStreamOptions): Promise<PeerHostEventSubscription>;
|
|
27
|
+
private run;
|
|
28
|
+
private assertCurrent;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=peer-host-session-controller.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { HostRouter } from './host-router.js';
|
|
2
|
+
/**
|
|
3
|
+
* 远端会话的唯一 Client 入口。
|
|
4
|
+
*
|
|
5
|
+
* 控制器不保存目标凭据,只保存当前 HostScope;所有异步结果在提交前再次
|
|
6
|
+
* 校验 generation,避免切换工作区或会话后旧请求回写新页面。
|
|
7
|
+
*/
|
|
8
|
+
export class PeerHostSessionController {
|
|
9
|
+
router;
|
|
10
|
+
client;
|
|
11
|
+
constructor(router, client) {
|
|
12
|
+
this.router = router;
|
|
13
|
+
this.client = client;
|
|
14
|
+
}
|
|
15
|
+
async select(input) {
|
|
16
|
+
return this.router.switchTo(input);
|
|
17
|
+
}
|
|
18
|
+
current() {
|
|
19
|
+
return this.router.getCurrent();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Host 侧重连成功后重建同一逻辑作用域。
|
|
23
|
+
* `HostRouter.rebuild` 会先清理旧订阅,再递增 generation;摘要刷新回调只能使用新作用域。
|
|
24
|
+
*/
|
|
25
|
+
async rebuildAfterReconnect(scope, refreshSummary) {
|
|
26
|
+
this.assertCurrent(scope);
|
|
27
|
+
const nextScope = await this.router.rebuild({
|
|
28
|
+
hostId: scope.hostId,
|
|
29
|
+
targetHostId: scope.targetHostId,
|
|
30
|
+
workspaceId: scope.workspaceId,
|
|
31
|
+
sessionId: scope.sessionId,
|
|
32
|
+
});
|
|
33
|
+
if (refreshSummary !== undefined) {
|
|
34
|
+
this.assertCurrent(nextScope);
|
|
35
|
+
await refreshSummary(nextScope);
|
|
36
|
+
this.assertCurrent(nextScope);
|
|
37
|
+
}
|
|
38
|
+
return nextScope;
|
|
39
|
+
}
|
|
40
|
+
async loadHistory(scope, cursor) {
|
|
41
|
+
this.assertCurrent(scope);
|
|
42
|
+
const response = await this.client.loadSessionHistory(scope, cursor);
|
|
43
|
+
this.assertCurrent(scope);
|
|
44
|
+
return response;
|
|
45
|
+
}
|
|
46
|
+
async sendMessage(scope, body) {
|
|
47
|
+
return this.run(scope, () => this.client.sendMessage(scope, body));
|
|
48
|
+
}
|
|
49
|
+
async stop(scope) {
|
|
50
|
+
return this.run(scope, () => this.client.stopSession(scope));
|
|
51
|
+
}
|
|
52
|
+
async replyPermission(scope, body) {
|
|
53
|
+
return this.run(scope, () => this.client.replyPermission(scope, body));
|
|
54
|
+
}
|
|
55
|
+
async answerQuestion(scope, body) {
|
|
56
|
+
return this.run(scope, () => this.client.answerQuestion(scope, body));
|
|
57
|
+
}
|
|
58
|
+
async subscribe(scope, socketFactory, listener, options) {
|
|
59
|
+
this.assertCurrent(scope);
|
|
60
|
+
const subscription = await this.client.openEventStream(scope, socketFactory, listener, options);
|
|
61
|
+
if (!this.router.isCurrent(scope)) {
|
|
62
|
+
subscription.close();
|
|
63
|
+
throw new Error('PeerHost 作用域已失效');
|
|
64
|
+
}
|
|
65
|
+
const remove = this.router.addDisposer(subscription.close);
|
|
66
|
+
let disposed = false;
|
|
67
|
+
const close = () => {
|
|
68
|
+
if (disposed)
|
|
69
|
+
return;
|
|
70
|
+
disposed = true;
|
|
71
|
+
subscription.close();
|
|
72
|
+
remove();
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
close,
|
|
76
|
+
send: subscription.send,
|
|
77
|
+
terminalInput: subscription.terminalInput,
|
|
78
|
+
terminalResize: subscription.terminalResize,
|
|
79
|
+
terminalClose: subscription.terminalClose,
|
|
80
|
+
rightToolSubscribe: subscription.rightToolSubscribe,
|
|
81
|
+
rightToolRefresh: subscription.rightToolRefresh,
|
|
82
|
+
rightToolClose: subscription.rightToolClose,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async run(scope, operation) {
|
|
86
|
+
this.assertCurrent(scope);
|
|
87
|
+
const response = await operation();
|
|
88
|
+
this.assertCurrent(scope);
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
assertCurrent(scope) {
|
|
92
|
+
this.router.assertCurrent(scope);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=peer-host-session-controller.js.map
|
|
@@ -5,8 +5,20 @@ import { applyQuickPhraseOrder, QUICK_PHRASE_FALLBACK_MARGIN, QUICK_PHRASE_TRIGG
|
|
|
5
5
|
import { dshPopupSurfaceStyle, dshThemeColor } from './theme.js';
|
|
6
6
|
const QUICK_PHRASE_FALLBACK_SIZE = 28;
|
|
7
7
|
const QUICK_PHRASE_FALLBACK_ICON_SIZE = 14;
|
|
8
|
+
const QUICK_PHRASE_STYLE_ID = 'codingns4dsh-quick-phrase-responsive-style';
|
|
9
|
+
/** 移动端弹层脱离对话窗口边界,使用整个视口宽度。 */
|
|
10
|
+
function installQuickPhraseStyles() {
|
|
11
|
+
if (typeof document === 'undefined' || document.querySelector(`style[data-plugin-css="${QUICK_PHRASE_STYLE_ID}"]`) !== null)
|
|
12
|
+
return;
|
|
13
|
+
const style = document.createElement('style');
|
|
14
|
+
style.dataset.plugin = 'codingns4dsh';
|
|
15
|
+
style.dataset.pluginCss = QUICK_PHRASE_STYLE_ID;
|
|
16
|
+
style.textContent = '@media (max-width: 768px){.codingns4dsh-quick-phrase-overlay{left:0!important;right:0!important;width:100vw!important}}';
|
|
17
|
+
document.head.appendChild(style);
|
|
18
|
+
}
|
|
8
19
|
/** 在添加文件按钮右侧的输入工具区挂载插件自己的快捷会话入口。 */
|
|
9
20
|
export function registerQuickPhraseSlot(slots, settings, locale) {
|
|
21
|
+
installQuickPhraseStyles();
|
|
10
22
|
const t = locale.bind('codingns');
|
|
11
23
|
return slots.inject('conversation.input.left', () => slots.register({
|
|
12
24
|
name: 'conversation.input.left',
|
|
@@ -172,6 +184,7 @@ function QuickPhraseSlot(props) {
|
|
|
172
184
|
strokeWidth: 2,
|
|
173
185
|
'aria-hidden': true,
|
|
174
186
|
}, createElement('path', { d: 'M7 8h10' }), createElement('path', { d: 'M7 12h8' }), createElement('path', { d: 'M7 16h5' }), createElement('path', { d: 'M5 5h14v14H9l-4 4V5z' }))), open && createElement('div', {
|
|
187
|
+
className: 'codingns4dsh-quick-phrase-overlay',
|
|
175
188
|
role: 'dialog',
|
|
176
189
|
'aria-label': t('workspace.quickPhrasesDialogLabel'),
|
|
177
190
|
'aria-modal': true,
|
|
@@ -224,6 +237,7 @@ function QuickPhraseSlot(props) {
|
|
|
224
237
|
onClick: () => removePhrase(phrase.id),
|
|
225
238
|
style: quickPhraseDeleteStyle,
|
|
226
239
|
}, '×')))))), addOpen && createElement('div', {
|
|
240
|
+
className: 'codingns4dsh-quick-phrase-overlay',
|
|
227
241
|
role: 'dialog',
|
|
228
242
|
'aria-label': t('workspace.quickPhrasesAddDialogLabel'),
|
|
229
243
|
'aria-modal': true,
|
|
@@ -802,18 +802,40 @@ function createBridgeScript() {
|
|
|
802
802
|
const parentTransport = (() => {
|
|
803
803
|
try { return parent.__CODINGNS4DSH_REMOTE_TRANSPORT__; } catch { return undefined; }
|
|
804
804
|
})();
|
|
805
|
-
|
|
805
|
+
const bridgedTransport = {
|
|
806
806
|
fetch: window.fetch.bind(window),
|
|
807
807
|
openStream: openRemoteStream,
|
|
808
|
-
//
|
|
809
|
-
|
|
810
|
-
//
|
|
808
|
+
// 仅作为 iframe 内 RemoteWebSocket 的虚拟基址,实际请求仍回到父窗口。
|
|
809
|
+
streamBaseUrl: resourceBase(),
|
|
810
|
+
// 声明 Host 所有权,保持原生设置存储可用。
|
|
811
811
|
ownsHost: true,
|
|
812
812
|
generation: parentTransport?.getGeneration?.bind(parentTransport),
|
|
813
813
|
onGenerationChange: parentTransport?.onGenerationChange?.bind(parentTransport),
|
|
814
814
|
reconnect: parentTransport?.reconnect?.bind(parentTransport),
|
|
815
815
|
close: parentTransport?.close?.bind(parentTransport),
|
|
816
816
|
};
|
|
817
|
+
// 远程 DSH Web 的启动页自带 Host 注入的 __DSH_TRANSPORT__ 行,它在本脚本之后
|
|
818
|
+
// 执行:直接赋值会覆盖桥接 Transport,丢掉 openStream 和 streamBaseUrl,让
|
|
819
|
+
// /api/remote.mux 退回 iframe 自己的 WebSocket(CSP connect-src 为 none),
|
|
820
|
+
// 会话列表和原生设置页面会一起加载失败。这里改为合并写入,后到的赋值只能补充
|
|
821
|
+
// 字段,桥接能力始终保留。
|
|
822
|
+
const mergeTransport = (next) => Object.assign(
|
|
823
|
+
{},
|
|
824
|
+
next !== null && typeof next === 'object' ? next : {},
|
|
825
|
+
bridgedTransport,
|
|
826
|
+
);
|
|
827
|
+
let transportValue = mergeTransport(globalThis.__DSH_TRANSPORT__);
|
|
828
|
+
try {
|
|
829
|
+
Object.defineProperty(globalThis, '__DSH_TRANSPORT__', {
|
|
830
|
+
configurable: true,
|
|
831
|
+
enumerable: true,
|
|
832
|
+
get: () => transportValue,
|
|
833
|
+
set: (next) => { transportValue = mergeTransport(next); },
|
|
834
|
+
});
|
|
835
|
+
} catch {
|
|
836
|
+
// 同名属性不可配置时退化为一次合并赋值,同样保留桥接字段。
|
|
837
|
+
globalThis.__DSH_TRANSPORT__ = mergeTransport(globalThis.__DSH_TRANSPORT__);
|
|
838
|
+
}
|
|
817
839
|
globalThis.__CODINGNS4DSH_DEBUG__ = (event, fields = {}) => {
|
|
818
840
|
bridgeLog('client.' + String(event), fields);
|
|
819
841
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import type { CodingNsRpcClient } from './features/types.js';
|
|
3
|
+
export declare const SESSION_CHANGED_FILES_VIEW_ID = "codingns4dsh/session-changed-files";
|
|
4
|
+
interface SessionChangedFilesViewProps {
|
|
5
|
+
readonly sessionId: string;
|
|
6
|
+
readonly rpc: CodingNsRpcClient;
|
|
7
|
+
readonly remote?: unknown;
|
|
8
|
+
readonly reportCount?: (sessionId: string, count: number) => void;
|
|
9
|
+
}
|
|
10
|
+
/** 会话“修改文件”视图;数据只通过插件 RPC 和现有 Git RPC 读取。 */
|
|
11
|
+
export declare function SessionChangedFilesView(props: SessionChangedFilesViewProps): ReactElement;
|
|
12
|
+
/** 注册 DSH 原生 conversation.view Slot;标签由 DSH 根据 Slot 的 id/label 自动投影。 */
|
|
13
|
+
export declare function registerSessionChangedFilesView(ctx: unknown, rpc: CodingNsRpcClient, remote?: unknown): (() => void) | undefined;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=session-changed-files-view.d.ts.map
|