@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
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { PEER_HOST_ERROR_CODES } from '../../../shared/contracts/peer-host.js';
|
|
5
|
+
export class PeerHostStoreError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'PeerHostStoreError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** 当前 Host 的 PeerHost 配置服务;所有 mutation 都在 Host 内完成校验。 */
|
|
14
|
+
export class PeerHostStore {
|
|
15
|
+
ownerUserId;
|
|
16
|
+
recordStore;
|
|
17
|
+
credentials;
|
|
18
|
+
now;
|
|
19
|
+
createId;
|
|
20
|
+
records = null;
|
|
21
|
+
operation = Promise.resolve();
|
|
22
|
+
constructor(ownerUserId, recordStore, credentials, now = Date.now, createId = () => `peer-${randomBytes(12).toString('hex')}`) {
|
|
23
|
+
this.ownerUserId = ownerUserId;
|
|
24
|
+
this.recordStore = recordStore;
|
|
25
|
+
this.credentials = credentials;
|
|
26
|
+
this.now = now;
|
|
27
|
+
this.createId = createId;
|
|
28
|
+
if (!ownerUserId.trim())
|
|
29
|
+
throw new TypeError('PeerHost ownerUserId 不能为空');
|
|
30
|
+
}
|
|
31
|
+
async list() {
|
|
32
|
+
await this.load();
|
|
33
|
+
return this.records.map(cloneRecord);
|
|
34
|
+
}
|
|
35
|
+
async get(peerHostId) {
|
|
36
|
+
const record = (await this.list()).find((item) => item.id === peerHostId);
|
|
37
|
+
return record ?? null;
|
|
38
|
+
}
|
|
39
|
+
async create(input) {
|
|
40
|
+
return this.enqueue(async () => {
|
|
41
|
+
await this.load();
|
|
42
|
+
const displayName = requireDisplayName(input.displayName);
|
|
43
|
+
const route = normalizePeerHostRoute(input.route);
|
|
44
|
+
this.assertUnique(route);
|
|
45
|
+
const timestamp = this.now();
|
|
46
|
+
const record = {
|
|
47
|
+
id: this.createId(),
|
|
48
|
+
ownerUserId: this.ownerUserId,
|
|
49
|
+
displayName,
|
|
50
|
+
route,
|
|
51
|
+
status: 'configured',
|
|
52
|
+
pluginId: null,
|
|
53
|
+
pluginVersion: null,
|
|
54
|
+
dshVersion: null,
|
|
55
|
+
apiCompatibility: null,
|
|
56
|
+
fingerprint: null,
|
|
57
|
+
lastCheckedAt: null,
|
|
58
|
+
lastErrorCode: null,
|
|
59
|
+
createdAt: timestamp,
|
|
60
|
+
updatedAt: timestamp,
|
|
61
|
+
};
|
|
62
|
+
this.records.push(record);
|
|
63
|
+
await this.persist();
|
|
64
|
+
return cloneRecord(record);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async update(peerHostId, input) {
|
|
68
|
+
return this.enqueue(async () => {
|
|
69
|
+
await this.load();
|
|
70
|
+
const index = this.findIndex(peerHostId);
|
|
71
|
+
const previous = this.records[index];
|
|
72
|
+
const displayName = input.displayName === undefined ? previous.displayName : requireDisplayName(input.displayName);
|
|
73
|
+
const route = input.route === undefined ? previous.route : normalizePeerHostRoute(input.route);
|
|
74
|
+
const routeChanged = routeKey(previous.route) !== routeKey(route);
|
|
75
|
+
if (routeChanged)
|
|
76
|
+
this.assertUnique(route, peerHostId);
|
|
77
|
+
const next = {
|
|
78
|
+
...previous,
|
|
79
|
+
displayName,
|
|
80
|
+
route,
|
|
81
|
+
...(routeChanged ? resetHandshake(previous) : {}),
|
|
82
|
+
updatedAt: this.now(),
|
|
83
|
+
};
|
|
84
|
+
this.records[index] = next;
|
|
85
|
+
if (routeChanged)
|
|
86
|
+
await this.credentials.clear(peerHostId);
|
|
87
|
+
await this.persist();
|
|
88
|
+
return cloneRecord(next);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async remove(peerHostId) {
|
|
92
|
+
return this.enqueue(async () => {
|
|
93
|
+
await this.load();
|
|
94
|
+
const index = this.findIndex(peerHostId);
|
|
95
|
+
this.records.splice(index, 1);
|
|
96
|
+
await this.credentials.clear(peerHostId);
|
|
97
|
+
await this.persist();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async updateHandshake(peerHostId, input) {
|
|
101
|
+
return this.enqueue(async () => {
|
|
102
|
+
await this.load();
|
|
103
|
+
const index = this.findIndex(peerHostId);
|
|
104
|
+
const next = { ...this.records[index], ...input, updatedAt: this.now() };
|
|
105
|
+
this.records[index] = next;
|
|
106
|
+
await this.persist();
|
|
107
|
+
return cloneRecord(next);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async updateStatus(peerHostId, status, lastErrorCode) {
|
|
111
|
+
return this.enqueue(async () => {
|
|
112
|
+
await this.load();
|
|
113
|
+
const index = this.findIndex(peerHostId);
|
|
114
|
+
const next = { ...this.records[index], status, lastErrorCode, updatedAt: this.now() };
|
|
115
|
+
this.records[index] = next;
|
|
116
|
+
await this.persist();
|
|
117
|
+
return cloneRecord(next);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async load() {
|
|
121
|
+
if (this.records !== null)
|
|
122
|
+
return;
|
|
123
|
+
const records = await this.recordStore.read();
|
|
124
|
+
this.records = records
|
|
125
|
+
.filter((record) => record.ownerUserId === this.ownerUserId)
|
|
126
|
+
.map((record) => ({ ...record, route: normalizePeerHostRoute(record.route) }));
|
|
127
|
+
}
|
|
128
|
+
async persist() {
|
|
129
|
+
await this.recordStore.write(this.records.map(cloneRecord));
|
|
130
|
+
}
|
|
131
|
+
findIndex(peerHostId) {
|
|
132
|
+
const index = this.records.findIndex((record) => record.id === peerHostId);
|
|
133
|
+
if (index < 0)
|
|
134
|
+
throw new PeerHostStoreError(PEER_HOST_ERROR_CODES.NOT_FOUND, 'PeerHost 不存在');
|
|
135
|
+
return index;
|
|
136
|
+
}
|
|
137
|
+
assertUnique(route, exceptId) {
|
|
138
|
+
if (this.records.some((record) => record.id !== exceptId && routeKey(record.route) === routeKey(route))) {
|
|
139
|
+
throw new PeerHostStoreError(PEER_HOST_ERROR_CODES.DUPLICATE, 'PeerHost 路由已存在');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
enqueue(operation) {
|
|
143
|
+
const next = this.operation.then(operation, operation);
|
|
144
|
+
this.operation = next.then(() => undefined, () => undefined);
|
|
145
|
+
return next;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export class InMemoryPeerHostRecordStore {
|
|
149
|
+
records;
|
|
150
|
+
constructor(records = []) {
|
|
151
|
+
this.records = records;
|
|
152
|
+
}
|
|
153
|
+
async read() {
|
|
154
|
+
return this.records.map(cloneRecord);
|
|
155
|
+
}
|
|
156
|
+
async write(records) {
|
|
157
|
+
this.records = records.map(cloneRecord);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export class InMemoryPeerHostCredentialStore {
|
|
161
|
+
records = new Map();
|
|
162
|
+
async read(peerHostId) {
|
|
163
|
+
const value = this.records.get(peerHostId);
|
|
164
|
+
return value === undefined ? null : { ...value };
|
|
165
|
+
}
|
|
166
|
+
async write(peerHostId, credential) {
|
|
167
|
+
this.records.set(peerHostId, { ...credential });
|
|
168
|
+
}
|
|
169
|
+
async clear(peerHostId) {
|
|
170
|
+
this.records.delete(peerHostId);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** 非敏感记录的原子文件存储;token 不允许进入此文件。 */
|
|
174
|
+
export class FilePeerHostRecordStore {
|
|
175
|
+
filePath;
|
|
176
|
+
constructor(filePath) {
|
|
177
|
+
this.filePath = filePath;
|
|
178
|
+
if (!filePath.trim())
|
|
179
|
+
throw new TypeError('PeerHost record 文件路径不能为空');
|
|
180
|
+
}
|
|
181
|
+
async read() {
|
|
182
|
+
try {
|
|
183
|
+
const value = JSON.parse(await readFile(this.filePath, 'utf8'));
|
|
184
|
+
if (!Array.isArray(value))
|
|
185
|
+
throw new Error('PeerHost record 文件格式无效');
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (isNodeError(error, 'ENOENT'))
|
|
190
|
+
return [];
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async write(records) {
|
|
195
|
+
await mkdir(dirname(this.filePath), { recursive: true, mode: 0o700 });
|
|
196
|
+
const temporary = `${this.filePath}.${process.pid}.tmp`;
|
|
197
|
+
await writeFile(temporary, `${JSON.stringify(records)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
198
|
+
await rename(temporary, this.filePath);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** AES-256-GCM 文件存储,密钥必须由 Host 启动边界注入。 */
|
|
202
|
+
export class EncryptedFilePeerHostCredentialStore {
|
|
203
|
+
filePath;
|
|
204
|
+
constructor(filePath, key) {
|
|
205
|
+
this.filePath = filePath;
|
|
206
|
+
if (!filePath.trim())
|
|
207
|
+
throw new TypeError('PeerHost credential 文件路径不能为空');
|
|
208
|
+
if (key.byteLength !== 32)
|
|
209
|
+
throw new TypeError('PeerHost credential 密钥必须为 32 字节');
|
|
210
|
+
this.key = Buffer.from(key);
|
|
211
|
+
}
|
|
212
|
+
key;
|
|
213
|
+
async read(peerHostId) {
|
|
214
|
+
const records = await this.readAll();
|
|
215
|
+
const record = records[peerHostId];
|
|
216
|
+
return record === undefined ? null : { ...record };
|
|
217
|
+
}
|
|
218
|
+
async write(peerHostId, credential) {
|
|
219
|
+
const records = await this.readAll();
|
|
220
|
+
records[peerHostId] = { ...credential };
|
|
221
|
+
await this.writeAll(records);
|
|
222
|
+
}
|
|
223
|
+
async clear(peerHostId) {
|
|
224
|
+
const records = await this.readAll();
|
|
225
|
+
delete records[peerHostId];
|
|
226
|
+
await this.writeAll(records);
|
|
227
|
+
}
|
|
228
|
+
async readAll() {
|
|
229
|
+
try {
|
|
230
|
+
const envelope = JSON.parse(await readFile(this.filePath, 'utf8'));
|
|
231
|
+
if (!envelope.iv || !envelope.tag || !envelope.data)
|
|
232
|
+
throw new Error('PeerHost credential 文件格式无效');
|
|
233
|
+
const decipher = createDecipheriv('aes-256-gcm', this.key, Buffer.from(envelope.iv, 'base64'));
|
|
234
|
+
decipher.setAuthTag(Buffer.from(envelope.tag, 'base64'));
|
|
235
|
+
const plaintext = Buffer.concat([decipher.update(Buffer.from(envelope.data, 'base64')), decipher.final()]).toString('utf8');
|
|
236
|
+
const value = JSON.parse(plaintext);
|
|
237
|
+
if (!isCredentialMap(value))
|
|
238
|
+
throw new Error('PeerHost credential 内容无效');
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (isNodeError(error, 'ENOENT'))
|
|
243
|
+
return {};
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async writeAll(records) {
|
|
248
|
+
const iv = randomBytes(12);
|
|
249
|
+
const cipher = createCipheriv('aes-256-gcm', this.key, iv);
|
|
250
|
+
const encrypted = Buffer.concat([cipher.update(JSON.stringify(records), 'utf8'), cipher.final()]);
|
|
251
|
+
const envelope = {
|
|
252
|
+
iv: iv.toString('base64'),
|
|
253
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
254
|
+
data: encrypted.toString('base64'),
|
|
255
|
+
};
|
|
256
|
+
await mkdir(dirname(this.filePath), { recursive: true, mode: 0o700 });
|
|
257
|
+
const temporary = `${this.filePath}.${process.pid}.tmp`;
|
|
258
|
+
await writeFile(temporary, `${JSON.stringify(envelope)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
259
|
+
await rename(temporary, this.filePath);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export function normalizePeerHostRoute(route) {
|
|
263
|
+
if (route.kind === 'relay') {
|
|
264
|
+
const deviceId = requireText(route.deviceId, 'deviceId');
|
|
265
|
+
const relayEntryId = requireText(route.relayEntryId, 'relayEntryId');
|
|
266
|
+
return { kind: 'relay', deviceId, relayEntryId, transportVersion: requireText(route.transportVersion, 'transportVersion') };
|
|
267
|
+
}
|
|
268
|
+
if (route.kind !== 'lan')
|
|
269
|
+
throw new PeerHostStoreError(PEER_HOST_ERROR_CODES.INVALID_ROUTE, 'PeerHost 路由类型无效');
|
|
270
|
+
const raw = requireText(route.baseUrl, 'baseUrl');
|
|
271
|
+
let parsed;
|
|
272
|
+
try {
|
|
273
|
+
parsed = new URL(raw);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
throw new PeerHostStoreError(PEER_HOST_ERROR_CODES.INVALID_ROUTE, 'PeerHost 局域网地址无效');
|
|
277
|
+
}
|
|
278
|
+
if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') || parsed.username || parsed.password || parsed.search || parsed.hash || (parsed.pathname !== '/' && parsed.pathname !== '')) {
|
|
279
|
+
throw new PeerHostStoreError(PEER_HOST_ERROR_CODES.INVALID_ROUTE, 'PeerHost 局域网地址必须是无凭据的 HTTP(S) Origin');
|
|
280
|
+
}
|
|
281
|
+
const normalizedOrigin = parsed.origin.toLowerCase();
|
|
282
|
+
return { kind: 'lan', baseUrl: normalizedOrigin, normalizedOrigin };
|
|
283
|
+
}
|
|
284
|
+
function routeKey(route) {
|
|
285
|
+
return route.kind === 'lan'
|
|
286
|
+
? `lan:${route.normalizedOrigin}`
|
|
287
|
+
: `relay:${route.deviceId}:${route.relayEntryId}`;
|
|
288
|
+
}
|
|
289
|
+
function resetHandshake(record) {
|
|
290
|
+
return {
|
|
291
|
+
status: 'configured',
|
|
292
|
+
pluginId: null,
|
|
293
|
+
pluginVersion: null,
|
|
294
|
+
dshVersion: null,
|
|
295
|
+
apiCompatibility: null,
|
|
296
|
+
fingerprint: null,
|
|
297
|
+
lastCheckedAt: null,
|
|
298
|
+
lastErrorCode: null,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function requireDisplayName(value) {
|
|
302
|
+
const name = requireText(value, 'displayName');
|
|
303
|
+
if (name.length > 128)
|
|
304
|
+
throw new TypeError('PeerHost 名称不能超过 128 个字符');
|
|
305
|
+
return name;
|
|
306
|
+
}
|
|
307
|
+
function requireText(value, field) {
|
|
308
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
309
|
+
throw new PeerHostStoreError(PEER_HOST_ERROR_CODES.INVALID_ROUTE, `PeerHost ${field} 不能为空`);
|
|
310
|
+
return value.trim();
|
|
311
|
+
}
|
|
312
|
+
function cloneRecord(record) {
|
|
313
|
+
return { ...record, route: record.route.kind === 'lan' ? { ...record.route } : { ...record.route } };
|
|
314
|
+
}
|
|
315
|
+
function isCredentialMap(value) {
|
|
316
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
317
|
+
return false;
|
|
318
|
+
return Object.values(value).every((item) => typeof item === 'object' && item !== null && typeof item.accessToken === 'string' && typeof item.refreshToken === 'string' && typeof item.expiresAt === 'number');
|
|
319
|
+
}
|
|
320
|
+
function isNodeError(error, code) {
|
|
321
|
+
return error instanceof Error && 'code' in error && error.code === code;
|
|
322
|
+
}
|
|
323
|
+
//# sourceMappingURL=peer-host-store.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type IncomingMessage } from 'node:http';
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
export declare const PEER_HOST_WS_PATH = "/api/codingns/peer-host/ws";
|
|
4
|
+
export interface PeerHostWsGatewayEndpoint {
|
|
5
|
+
readonly host: string;
|
|
6
|
+
readonly port: number;
|
|
7
|
+
readonly path: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PeerHostWsGatewayOptions {
|
|
10
|
+
readonly listenHost?: string;
|
|
11
|
+
readonly listenPort?: number;
|
|
12
|
+
readonly path?: string;
|
|
13
|
+
readonly authorizeUpgrade?: (request: IncomingMessage) => boolean | Promise<boolean>;
|
|
14
|
+
readonly onConnection: (socket: WebSocket, request: IncomingMessage) => void | Promise<void | (() => void)>;
|
|
15
|
+
}
|
|
16
|
+
/** 插件自有的 WebSocket upgrade 入口,不修改 DSH WebServer。 */
|
|
17
|
+
export declare class PeerHostWebSocketGateway {
|
|
18
|
+
private readonly options;
|
|
19
|
+
private readonly server;
|
|
20
|
+
private readonly websocketServer;
|
|
21
|
+
private readonly path;
|
|
22
|
+
private readonly listenHost;
|
|
23
|
+
private readonly listenPort;
|
|
24
|
+
private readonly cleanups;
|
|
25
|
+
private endpointValue;
|
|
26
|
+
private closed;
|
|
27
|
+
constructor(options: PeerHostWsGatewayOptions);
|
|
28
|
+
start(): Promise<PeerHostWsGatewayEndpoint>;
|
|
29
|
+
endpoint(): PeerHostWsGatewayEndpoint | null;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
private handleUpgrade;
|
|
32
|
+
private attach;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=peer-host-ws-gateway.d.ts.map
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
3
|
+
export const PEER_HOST_WS_PATH = '/api/codingns/peer-host/ws';
|
|
4
|
+
/** 插件自有的 WebSocket upgrade 入口,不修改 DSH WebServer。 */
|
|
5
|
+
export class PeerHostWebSocketGateway {
|
|
6
|
+
options;
|
|
7
|
+
server;
|
|
8
|
+
websocketServer;
|
|
9
|
+
path;
|
|
10
|
+
listenHost;
|
|
11
|
+
listenPort;
|
|
12
|
+
cleanups = new Map();
|
|
13
|
+
endpointValue = null;
|
|
14
|
+
closed = false;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.path = normalizePath(options.path ?? PEER_HOST_WS_PATH);
|
|
18
|
+
this.listenHost = normalizeHost(options.listenHost ?? '127.0.0.1');
|
|
19
|
+
this.listenPort = normalizePort(options.listenPort ?? 0);
|
|
20
|
+
this.server = createServer((_request, response) => {
|
|
21
|
+
response.statusCode = 404;
|
|
22
|
+
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
23
|
+
response.end(JSON.stringify({ error: 'PeerHost WebSocket 入口只接受 upgrade 请求' }));
|
|
24
|
+
});
|
|
25
|
+
this.websocketServer = new WebSocketServer({ noServer: true, clientTracking: false, maxPayload: 256 * 1024 });
|
|
26
|
+
this.server.on('upgrade', (request, socket, head) => { void this.handleUpgrade(request, socket, head); });
|
|
27
|
+
}
|
|
28
|
+
async start() {
|
|
29
|
+
if (this.endpointValue !== null)
|
|
30
|
+
return this.endpointValue;
|
|
31
|
+
await new Promise((resolve, reject) => {
|
|
32
|
+
const onError = (error) => { this.server.removeListener('listening', onListening); reject(error); };
|
|
33
|
+
const onListening = () => { this.server.removeListener('error', onError); resolve(); };
|
|
34
|
+
this.server.once('error', onError);
|
|
35
|
+
this.server.once('listening', onListening);
|
|
36
|
+
this.server.listen({ host: this.listenHost, port: this.listenPort });
|
|
37
|
+
});
|
|
38
|
+
const address = this.server.address();
|
|
39
|
+
if (address === null || typeof address === 'string')
|
|
40
|
+
throw new Error('PeerHost WebSocket 监听器未返回有效端口');
|
|
41
|
+
this.endpointValue = { host: this.listenHost, port: address.port, path: this.path };
|
|
42
|
+
return this.endpointValue;
|
|
43
|
+
}
|
|
44
|
+
endpoint() { return this.endpointValue; }
|
|
45
|
+
async close() {
|
|
46
|
+
if (this.closed)
|
|
47
|
+
return;
|
|
48
|
+
this.closed = true;
|
|
49
|
+
const closing = [];
|
|
50
|
+
for (const [socket, cleanup] of this.cleanups) {
|
|
51
|
+
cleanup();
|
|
52
|
+
if (socket.readyState === WebSocket.CLOSED)
|
|
53
|
+
continue;
|
|
54
|
+
closing.push(new Promise((resolve) => {
|
|
55
|
+
const timeout = setTimeout(() => socket.terminate(), 1000);
|
|
56
|
+
socket.once('close', () => {
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
resolve();
|
|
59
|
+
});
|
|
60
|
+
socket.close(1001, 'PeerHost WebSocket 网关已关闭');
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
this.cleanups.clear();
|
|
64
|
+
this.websocketServer.close();
|
|
65
|
+
await Promise.all(closing);
|
|
66
|
+
// 让远端 ws 客户端先处理 TCP FIN,再向调用方返回已关闭状态。
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
68
|
+
await new Promise((resolve) => {
|
|
69
|
+
if (!this.server.listening) {
|
|
70
|
+
resolve();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.server.close(() => resolve());
|
|
74
|
+
});
|
|
75
|
+
this.endpointValue = null;
|
|
76
|
+
}
|
|
77
|
+
async handleUpgrade(request, socket, head) {
|
|
78
|
+
if (this.closed || request.method !== 'GET' || readPath(request.url) !== this.path) {
|
|
79
|
+
rejectUpgrade(socket, 404, 'PeerHost WebSocket 路径不存在');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
if (this.options.authorizeUpgrade !== undefined && !(await this.options.authorizeUpgrade(request))) {
|
|
84
|
+
rejectUpgrade(socket, 401, 'PeerHost WebSocket 未授权');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
rejectUpgrade(socket, 401, 'PeerHost WebSocket 未授权');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.websocketServer.handleUpgrade(request, socket, head, (websocket) => {
|
|
93
|
+
this.websocketServer.emit('connection', websocket, request);
|
|
94
|
+
void this.attach(websocket, request);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async attach(socket, request) {
|
|
98
|
+
let cleanup;
|
|
99
|
+
try {
|
|
100
|
+
const result = this.options.onConnection(socket, request);
|
|
101
|
+
if (result instanceof Promise) {
|
|
102
|
+
const resolved = await result;
|
|
103
|
+
cleanup = typeof resolved === 'function' ? resolved : undefined;
|
|
104
|
+
}
|
|
105
|
+
else if (typeof result === 'function') {
|
|
106
|
+
cleanup = result;
|
|
107
|
+
}
|
|
108
|
+
this.cleanups.set(socket, cleanup ?? (() => undefined));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
socket.close(1011, 'PeerHost WebSocket 连接失败');
|
|
112
|
+
cleanup?.();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
socket.once('close', () => {
|
|
116
|
+
const current = this.cleanups.get(socket);
|
|
117
|
+
if (current !== undefined)
|
|
118
|
+
current();
|
|
119
|
+
this.cleanups.delete(socket);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function normalizePath(value) {
|
|
124
|
+
const path = value.trim();
|
|
125
|
+
if (!/^\/api\/[A-Za-z0-9_./-]+$/u.test(path) || path.includes('..'))
|
|
126
|
+
throw new TypeError('PeerHost WebSocket 路径无效');
|
|
127
|
+
return path;
|
|
128
|
+
}
|
|
129
|
+
function normalizeHost(value) {
|
|
130
|
+
const host = value.trim();
|
|
131
|
+
if (host === '')
|
|
132
|
+
throw new TypeError('PeerHost WebSocket 监听地址不能为空');
|
|
133
|
+
return host;
|
|
134
|
+
}
|
|
135
|
+
function normalizePort(value) {
|
|
136
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 65_535)
|
|
137
|
+
throw new TypeError('PeerHost WebSocket 监听端口无效');
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
function readPath(rawUrl) {
|
|
141
|
+
if (rawUrl === undefined)
|
|
142
|
+
return null;
|
|
143
|
+
try {
|
|
144
|
+
return new URL(rawUrl, 'http://peer-host.invalid').pathname;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function rejectUpgrade(socket, status, message) {
|
|
151
|
+
if (socket.destroyed)
|
|
152
|
+
return;
|
|
153
|
+
const statusText = status === 401 ? 'Unauthorized' : 'Not Found';
|
|
154
|
+
socket.end(`HTTP/1.1 ${status} ${statusText}\r\nConnection: close\r\nContent-Length: ${Buffer.byteLength(message)}\r\n\r\n${message}`);
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=peer-host-ws-gateway.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
-
import type { CodingNsAgentQuestion, CodingNsAgentQuestionResponse } from '../shared/contracts/cli-adapter.js';
|
|
2
|
+
import type { CodingNsAgentEvent, CodingNsAgentQuestion, CodingNsAgentQuestionResponse } from '../shared/contracts/cli-adapter.js';
|
|
3
3
|
/**
|
|
4
4
|
* DSH Host 原生会话服务的最小运行时面。
|
|
5
5
|
*
|
|
@@ -66,13 +66,23 @@ export interface CodingNsNativeRequestContext {
|
|
|
66
66
|
readonly provider: string;
|
|
67
67
|
readonly model: string;
|
|
68
68
|
readonly contextWindow?: number;
|
|
69
|
+
/** Provider usage 明确确认的容量;路由占位上下文不应覆盖它。 */
|
|
70
|
+
readonly confirmed?: boolean;
|
|
71
|
+
/** 容量来源;catalog 只用于首个 usage 前的已知模型提示,不锁定后续 Provider usage。 */
|
|
72
|
+
readonly source?: 'provider' | 'catalog';
|
|
69
73
|
}
|
|
70
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* 外部 Provider 已报告的用量采样;写入 assistant/attempt,不加入模型可见 surface。
|
|
76
|
+
*
|
|
77
|
+
* 记录进 DSH 会话后必须使用 DSH 的互斥桶口径:`inputTokens` 只含未缓存输入,
|
|
78
|
+
* 计费输入 = inputTokens + 缓存读写。调用方(公共投影层)负责从 Provider 口径折算。
|
|
79
|
+
*/
|
|
71
80
|
export interface CodingNsNativeUsageSample {
|
|
72
81
|
readonly inputTokens: number;
|
|
73
82
|
readonly outputTokens: number;
|
|
74
83
|
readonly cacheReadTokens?: number;
|
|
75
84
|
readonly cacheWriteTokens?: number;
|
|
85
|
+
/** 未缓存输入的显式副本;与互斥桶口径的 inputTokens 取值相同,仅作兼容保留。 */
|
|
76
86
|
readonly uncachedInputTokens?: number;
|
|
77
87
|
readonly totalTokens?: number;
|
|
78
88
|
readonly cacheHitRate?: number;
|
|
@@ -80,6 +90,10 @@ export interface CodingNsNativeUsageSample {
|
|
|
80
90
|
readonly contextTokens?: number;
|
|
81
91
|
readonly contextUsageRatio?: number;
|
|
82
92
|
}
|
|
93
|
+
/** 公共消息投影层传递的 Provider 上下文压缩活动。 */
|
|
94
|
+
export type CodingNsNativeCompactionEvent = Extract<CodingNsAgentEvent, {
|
|
95
|
+
type: 'context-compaction';
|
|
96
|
+
}>;
|
|
83
97
|
export interface CodingNsNativeSessionController {
|
|
84
98
|
create?(request: {
|
|
85
99
|
readonly sessionId?: string;
|
|
@@ -130,6 +144,8 @@ export interface CodingNsNativeSessionBridge {
|
|
|
130
144
|
appendRequestContext?(sessionId: string, context: CodingNsNativeRequestContext): boolean;
|
|
131
145
|
/** 在当前步骤即时记录外部 Provider 用量;该事件不进入模型可见 surface。 */
|
|
132
146
|
appendUsageSample?(sessionId: string, usage: CodingNsNativeUsageSample): boolean;
|
|
147
|
+
/** 将 Provider 压缩生命周期写入 DSH 原生 compaction 事件;不可用时安静降级。 */
|
|
148
|
+
appendCompactionEvent?(sessionId: string, event: CodingNsNativeCompactionEvent): boolean;
|
|
133
149
|
/** 判断指定会话是否真的具备下一步注入能力。 */
|
|
134
150
|
canInjectNextStep?(sessionId: string): boolean;
|
|
135
151
|
/** 在当前 Agent turn 的下一个合法 step 注入插件上下文,不唤醒空闲 Agent。 */
|