@ai-workbench/network 0.4.3 → 0.4.4
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/NOTICE.md +3 -0
- package/README.md +10 -3
- package/helpers/darwin-arm64/NOTICE.md +3 -0
- package/helpers/darwin-arm64/README.md +1 -1
- package/helpers/darwin-arm64/THIRD_PARTY_NOTICES.txt +68 -0
- package/helpers/darwin-arm64/bin/aw-net +0 -0
- package/helpers/darwin-arm64/helper-manifest.json +1 -1
- package/helpers/darwin-arm64/licenses-manifest.json +67 -32
- package/helpers/darwin-arm64/package.json +1 -1
- package/helpers/darwin-x64/NOTICE.md +3 -0
- package/helpers/darwin-x64/README.md +1 -1
- package/helpers/darwin-x64/THIRD_PARTY_NOTICES.txt +68 -0
- package/helpers/darwin-x64/bin/aw-net +0 -0
- package/helpers/darwin-x64/helper-manifest.json +1 -1
- package/helpers/darwin-x64/licenses-manifest.json +67 -32
- package/helpers/darwin-x64/package.json +1 -1
- package/lib/activation.d.ts +3 -1
- package/lib/activation.js +19 -2
- package/lib/api.d.ts +39 -0
- package/lib/control.d.ts +1 -1
- package/lib/control.js +9 -2
- package/lib/employee-endpoint.d.ts +102 -0
- package/lib/employee-endpoint.js +230 -0
- package/lib/index.js +6 -3
- package/lib/network.d.ts +2 -1
- package/lib/network.js +56 -1
- package/package.json +1 -1
package/lib/activation.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createNetwork } from './network.ts';
|
|
2
2
|
import type { Executable } from './network.ts';
|
|
3
3
|
import type { NetworkApi, NetworkState, PluginSettings } from './api.ts';
|
|
4
|
-
|
|
4
|
+
import type { EmployeeConnectSource } from './employee-endpoint.ts';
|
|
5
|
+
export declare function createActivationService(initial: PluginSettings, executable: Executable, emit?: (s: NetworkState) => void, factory?: typeof createNetwork, getConnect?: () => EmployeeConnectSource | undefined): NetworkApi & {
|
|
5
6
|
start(): Promise<void>;
|
|
7
|
+
refreshEmployeeSource(): void;
|
|
6
8
|
};
|
package/lib/activation.js
CHANGED
|
@@ -3,6 +3,7 @@ import { isAbsolute } from 'node:path';
|
|
|
3
3
|
import { createNetwork } from "./network.js";
|
|
4
4
|
import { fail, ok } from "./api.js";
|
|
5
5
|
import { settingsFields as fields, validHelperPolicy, mergedSettings } from "./settings.js";
|
|
6
|
+
import { createEmployeeEndpointController } from "./employee-endpoint.js";
|
|
6
7
|
function normalized(s) {
|
|
7
8
|
if (!s || typeof s !== 'object' || Object.keys(s).some(k => !fields.includes(k)))
|
|
8
9
|
return;
|
|
@@ -18,12 +19,13 @@ function normalized(s) {
|
|
|
18
19
|
return Object.fromEntries(fields.filter(k => s[k] !== undefined).map(k => [k, s[k]]));
|
|
19
20
|
}
|
|
20
21
|
// One stable Cordis service; callers never own, restart or borrow helper processes.
|
|
21
|
-
export function createActivationService(initial, executable, emit = () => { }, factory = createNetwork) {
|
|
22
|
+
export function createActivationService(initial, executable, emit = () => { }, factory = createNetwork, getConnect = () => undefined) {
|
|
22
23
|
let network = { phase: 'blocked', error: { code: 'SETTINGS_REQUIRED', message: '我需要导入连接资料。', needs: '在设置 → 远程主机连接 导入连接资料', actor: 'network' } };
|
|
23
24
|
let activation = { instanceId: randomUUID(), revision: 0, phase: 'unconfigured', receipt: null, error: network.error };
|
|
24
25
|
let runtime, selected, disposed = false, disposing, handoff, generation = 0, handedOff = false, consumerOnline = false;
|
|
25
26
|
const listeners = new Set(), edges = new Set(), updates = new Set();
|
|
26
27
|
const snapshot = () => structuredClone(activation);
|
|
28
|
+
const employee = createEmployeeEndpointController({ getConnect, getRuntime: () => activation.phase === 'active' ? runtime : undefined, getState: () => network, connect: (target, signal) => api.connect(target, signal) });
|
|
27
29
|
function publish(phase, error, receipt = activation.receipt) {
|
|
28
30
|
activation = { ...activation, revision: activation.revision + 1, phase, error, receipt };
|
|
29
31
|
for (const listener of listeners)
|
|
@@ -82,6 +84,8 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
82
84
|
return;
|
|
83
85
|
const was = consumerOnline;
|
|
84
86
|
consumerOnline = ready && next.phase === 'online';
|
|
87
|
+
if (was && !consumerOnline)
|
|
88
|
+
employee.networkLost();
|
|
85
89
|
if (was !== consumerOnline)
|
|
86
90
|
for (const listener of edges)
|
|
87
91
|
try {
|
|
@@ -105,6 +109,7 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
105
109
|
observed({ phase: 'blocked', error: { code: 'HELPER_START_FAILED', message: '网络组件未能启动,根因未查明。', needs: '管理员检查私有日志', actor: 'network' } }); });
|
|
106
110
|
}
|
|
107
111
|
const api = {
|
|
112
|
+
prepareEmployeeEndpoint: employee.prepare, refreshEmployeeSource: employee.refreshSource,
|
|
108
113
|
async start() { if (disposed)
|
|
109
114
|
return; if (initial.configFile || initial.helperLogFile)
|
|
110
115
|
await api.activate(initial, new AbortController().signal);
|
|
@@ -147,7 +152,19 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
147
152
|
async download(source, signal) { if (disposed)
|
|
148
153
|
return fail('DISPOSED', '网络服务已停止。', '重新加载插件'); return activation.phase === 'active' && runtime ? runtime.download(source, signal) : { ok: false, error: network.error ?? { code: 'NETWORK_NOT_READY', message: '连接尚未就绪。', needs: '等待N身份交接完成', actor: 'network' } }; },
|
|
149
154
|
dispose() { if (disposing)
|
|
150
|
-
return disposing; disposed = true; return disposing = Promise.resolve().then(async () => { publish('stopped', activation.error);
|
|
155
|
+
return disposing; disposed = true; return disposing = Promise.resolve().then(async () => { publish('stopped', activation.error); try {
|
|
156
|
+
await employee.dispose();
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
await runtime?.dispose();
|
|
160
|
+
await handoff;
|
|
161
|
+
network = runtime?.getState() ?? { phase: 'stopped' };
|
|
162
|
+
emit(structuredClone(network));
|
|
163
|
+
publish('stopped', network.error ?? null);
|
|
164
|
+
listeners.clear();
|
|
165
|
+
edges.clear();
|
|
166
|
+
updates.clear();
|
|
167
|
+
} }); },
|
|
151
168
|
};
|
|
152
169
|
return api;
|
|
153
170
|
}
|
package/lib/api.d.ts
CHANGED
|
@@ -69,10 +69,49 @@ export interface ActivationState {
|
|
|
69
69
|
error: Need | null;
|
|
70
70
|
}
|
|
71
71
|
export interface NetworkApi extends NetworkChannels {
|
|
72
|
+
prepareEmployeeEndpoint(request: EmployeeEndpointRequest, signal: AbortSignal): Promise<Result<EmployeeEndpointTarget>>;
|
|
72
73
|
activate(settings: PluginSettings, signal: AbortSignal): Promise<Result<ActivationState>>;
|
|
73
74
|
getActivationState(): ActivationState;
|
|
74
75
|
onActivationState(listener: (state: ActivationState) => void): () => void;
|
|
75
76
|
}
|
|
77
|
+
export interface EmployeeEndpointRequest {
|
|
78
|
+
instanceId: string;
|
|
79
|
+
epoch: number;
|
|
80
|
+
workspaceId: string;
|
|
81
|
+
expectedBindingId: string;
|
|
82
|
+
}
|
|
83
|
+
export interface EmployeeEndpointTarget {
|
|
84
|
+
version: 1;
|
|
85
|
+
local: {
|
|
86
|
+
instanceId: string;
|
|
87
|
+
epoch: number;
|
|
88
|
+
workspaceId: string;
|
|
89
|
+
bindingId: string;
|
|
90
|
+
directory: {
|
|
91
|
+
path: string;
|
|
92
|
+
device: number;
|
|
93
|
+
inode: number;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
endpoint: {
|
|
97
|
+
deviceId: string;
|
|
98
|
+
endpointInstanceId: string;
|
|
99
|
+
executionContextId: string;
|
|
100
|
+
remoteUser: string;
|
|
101
|
+
remoteUid: number;
|
|
102
|
+
remoteHome: string;
|
|
103
|
+
endpointCwd: string;
|
|
104
|
+
endpointShell: string;
|
|
105
|
+
};
|
|
106
|
+
bridge: {
|
|
107
|
+
protocolVersion: 1;
|
|
108
|
+
command: '/usr/local/aw-host/bin/aw-employee-bridge';
|
|
109
|
+
settingsPath: string;
|
|
110
|
+
bridgeInstanceId: string;
|
|
111
|
+
generation: number;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export type EmployeeEndpointReady = Pick<EmployeeEndpointTarget, 'endpoint' | 'bridge'>;
|
|
76
115
|
export interface NetworkChannels {
|
|
77
116
|
getState(): NetworkState;
|
|
78
117
|
connect(target: EnrollmentTarget, signal: AbortSignal): Promise<Result<{
|
package/lib/control.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'node:http';
|
|
2
|
-
export declare function control(socket: string, method: string, path: string, signal: AbortSignal, seconds: number, maxHeaderBytes: number): Promise<IncomingMessage>;
|
|
2
|
+
export declare function control(socket: string, method: string, path: string, signal: AbortSignal, seconds: number, maxHeaderBytes: number, body?: unknown): Promise<IncomingMessage>;
|
|
3
3
|
export declare function jsonResponse(response: IncomingMessage, maxBytes: number): Promise<unknown>;
|
|
4
4
|
export declare function firstFrame(response: IncomingMessage, maxBytes: number): Promise<unknown>;
|
package/lib/control.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { privateSocket } from "./private.js";
|
|
3
3
|
import { ObservationExpired } from "./observation.js";
|
|
4
|
-
export async function control(socket, method, path, signal, seconds, maxHeaderBytes) {
|
|
4
|
+
export async function control(socket, method, path, signal, seconds, maxHeaderBytes, body) {
|
|
5
5
|
await privateSocket(socket);
|
|
6
6
|
signal.throwIfAborted();
|
|
7
7
|
return new Promise((resolve, reject) => {
|
|
@@ -9,7 +9,14 @@ export async function control(socket, method, path, signal, seconds, maxHeaderBy
|
|
|
9
9
|
const started = performance.now();
|
|
10
10
|
req.setTimeout(seconds * 1000, () => req.destroy(new ObservationExpired('CONTROL_OBSERVATION_EXPIRED', 'dialTimeoutSeconds', seconds, started, path === '/v1/status' ? '网络组件状态响应头' : '本机控制响应头', '检查自有组件私有日志、控制socket和当前网络状态;不要重交开通材料')));
|
|
11
11
|
req.on('error', reject);
|
|
12
|
-
|
|
12
|
+
if (body === undefined)
|
|
13
|
+
req.end();
|
|
14
|
+
else {
|
|
15
|
+
const data = JSON.stringify(body);
|
|
16
|
+
req.setHeader('Content-Type', 'application/json');
|
|
17
|
+
req.setHeader('Content-Length', Buffer.byteLength(data));
|
|
18
|
+
req.end(data);
|
|
19
|
+
}
|
|
13
20
|
});
|
|
14
21
|
}
|
|
15
22
|
export async function jsonResponse(response, maxBytes) {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { EmployeeEndpointRequest, EmployeeEndpointTarget, EmployeeEndpointReady, EnrollmentTarget, NetworkState, Result } from './api.ts';
|
|
3
|
+
export interface EmployeeProcess {
|
|
4
|
+
pid: number;
|
|
5
|
+
ppid: number;
|
|
6
|
+
platform: string;
|
|
7
|
+
arch: string;
|
|
8
|
+
executable: string;
|
|
9
|
+
realUid: number;
|
|
10
|
+
effectiveUid: number;
|
|
11
|
+
realGid: number;
|
|
12
|
+
effectiveGid: number;
|
|
13
|
+
account: {
|
|
14
|
+
name: string;
|
|
15
|
+
home: string;
|
|
16
|
+
shell: string | null;
|
|
17
|
+
};
|
|
18
|
+
environment: {
|
|
19
|
+
HOME: string | null;
|
|
20
|
+
PATH: string | null;
|
|
21
|
+
SHELL: string | null;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
interface LocalContext {
|
|
25
|
+
instanceId: string;
|
|
26
|
+
epoch: number;
|
|
27
|
+
workspaceId: string;
|
|
28
|
+
version: 1;
|
|
29
|
+
bindingId: string;
|
|
30
|
+
source: 'local';
|
|
31
|
+
provenance: 'official-workspace-registry';
|
|
32
|
+
directory: {
|
|
33
|
+
path: string;
|
|
34
|
+
device: number;
|
|
35
|
+
inode: number;
|
|
36
|
+
};
|
|
37
|
+
process: EmployeeProcess;
|
|
38
|
+
}
|
|
39
|
+
interface LocationState {
|
|
40
|
+
instanceId: string;
|
|
41
|
+
epoch: number;
|
|
42
|
+
phase: string;
|
|
43
|
+
items: {
|
|
44
|
+
workspaceId: string;
|
|
45
|
+
bindingId: string | null;
|
|
46
|
+
source: string | null;
|
|
47
|
+
status: string;
|
|
48
|
+
}[];
|
|
49
|
+
}
|
|
50
|
+
interface Connection {
|
|
51
|
+
instanceId: string;
|
|
52
|
+
epoch: number;
|
|
53
|
+
phase: string;
|
|
54
|
+
}
|
|
55
|
+
interface SshTarget extends EnrollmentTarget {
|
|
56
|
+
session: {
|
|
57
|
+
settingsPath: string;
|
|
58
|
+
protocolVersion: 1;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
type ForeignResult<T> = {
|
|
62
|
+
ok: true;
|
|
63
|
+
value: T;
|
|
64
|
+
} | {
|
|
65
|
+
ok: false;
|
|
66
|
+
error: unknown;
|
|
67
|
+
};
|
|
68
|
+
export interface EmployeeConnectSource {
|
|
69
|
+
getContractInfo(): {
|
|
70
|
+
localWorkspaceContext?: number;
|
|
71
|
+
};
|
|
72
|
+
getLocalWorkspaceContext?(request: EmployeeEndpointRequest): Promise<ForeignResult<LocalContext>>;
|
|
73
|
+
getWorkspaceLocationState?(): LocationState;
|
|
74
|
+
onWorkspaceLocationState?(listener: (state: LocationState) => void): () => void;
|
|
75
|
+
getConnectionState(): Connection;
|
|
76
|
+
onConnectionState(listener: (state: Connection) => void): () => void;
|
|
77
|
+
getSshTarget(): Promise<ForeignResult<SshTarget>>;
|
|
78
|
+
}
|
|
79
|
+
export interface EmployeeRuntime {
|
|
80
|
+
prepareEmployeeEndpoint(settingsPath: string, signal: AbortSignal): Promise<Result<EmployeeEndpointReady>>;
|
|
81
|
+
closeEmployeeEndpoint(): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
interface Dependencies {
|
|
84
|
+
getConnect(): EmployeeConnectSource | undefined;
|
|
85
|
+
getRuntime(): EmployeeRuntime | undefined;
|
|
86
|
+
getState(): NetworkState;
|
|
87
|
+
connect(target: EnrollmentTarget, signal: AbortSignal): Promise<Result<{
|
|
88
|
+
proxyCommand: string;
|
|
89
|
+
}>>;
|
|
90
|
+
}
|
|
91
|
+
export declare function readEmployeeProcess(): Result<EmployeeProcess>;
|
|
92
|
+
export declare function employeeReady(value: unknown, p: EmployeeProcess, settingsPath: string): Result<EmployeeEndpointReady>;
|
|
93
|
+
export declare function bindEmployeeSourceLifecycle(ctx: Context, controller: {
|
|
94
|
+
refreshSource(): void;
|
|
95
|
+
}): void;
|
|
96
|
+
export declare function createEmployeeEndpointController(d: Dependencies): {
|
|
97
|
+
prepare: (r: EmployeeEndpointRequest, signal: AbortSignal) => Promise<Result<EmployeeEndpointTarget>>;
|
|
98
|
+
refreshSource: () => void;
|
|
99
|
+
networkLost: () => void;
|
|
100
|
+
dispose(): Promise<void>;
|
|
101
|
+
};
|
|
102
|
+
export {};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { userInfo } from 'node:os';
|
|
2
|
+
import { isAbsolute, normalize } from 'node:path';
|
|
3
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
+
import { fail, ok } from "./api.js";
|
|
6
|
+
const absolute = (v) => typeof v === 'string' && isAbsolute(v) && normalize(v) === v && !/[\0\r\n]/.test(v);
|
|
7
|
+
const nonempty = (v) => typeof v === 'string' && v.length > 0 && !/[\0\r\n]/.test(v);
|
|
8
|
+
const uuid = (v) => typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(v);
|
|
9
|
+
const same = isDeepStrictEqual;
|
|
10
|
+
function shape(v) {
|
|
11
|
+
const s = v;
|
|
12
|
+
return !!s && ['getContractInfo', 'getLocalWorkspaceContext', 'getWorkspaceLocationState', 'onWorkspaceLocationState', 'getConnectionState', 'onConnectionState', 'getSshTarget'].every(k => typeof s[k] === 'function') && s.getContractInfo()?.localWorkspaceContext === 1;
|
|
13
|
+
}
|
|
14
|
+
export function readEmployeeProcess() {
|
|
15
|
+
try {
|
|
16
|
+
if (process.platform !== 'darwin' || !process.getuid || !process.geteuid || !process.getgid || !process.getegid)
|
|
17
|
+
return fail('HELPER_PLATFORM_UNSUPPORTED', '当前系统不能提供员工端点。', '使用受支持的Mac客户端');
|
|
18
|
+
const account = userInfo(), uid = process.getuid(), euid = process.geteuid(), gid = process.getgid(), egid = process.getegid();
|
|
19
|
+
if (uid < 1 || uid > 4294967294 || uid !== euid || gid !== egid || account.uid !== euid || !nonempty(account.username) || !absolute(account.homedir) || !absolute(account.shell))
|
|
20
|
+
return fail('EMPLOYEE_ACCOUNT_UNAVAILABLE', '我需要当前员工的真实系统账户与shell。', '核对官方客户端的同用户进程环境');
|
|
21
|
+
return ok({ pid: process.pid, ppid: process.ppid, platform: process.platform, arch: process.arch, executable: process.execPath, realUid: uid, effectiveUid: euid, realGid: gid, effectiveGid: egid, account: { name: account.username, home: account.homedir, shell: account.shell }, environment: { HOME: process.env.HOME ?? null, PATH: process.env.PATH ?? null, SHELL: process.env.SHELL ?? null } });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return fail('EMPLOYEE_ACCOUNT_UNAVAILABLE', '员工账户信息暂不可用。', '核对官方客户端进程环境');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function requestValid(r) {
|
|
28
|
+
return !!r && Object.keys(r).length === 4 && ['instanceId', 'workspaceId', 'expectedBindingId'].every(k => nonempty(r[k])) && Number.isSafeInteger(r.epoch) && r.epoch >= 0;
|
|
29
|
+
}
|
|
30
|
+
export function employeeReady(value, p, settingsPath) {
|
|
31
|
+
const v = value, e = v?.endpoint, b = v?.bridge;
|
|
32
|
+
if (!e || !b || Object.keys(v).sort().join() !== 'bridge,endpoint' || Object.keys(e).sort().join() !== 'deviceId,endpointCwd,endpointInstanceId,endpointShell,executionContextId,remoteHome,remoteUid,remoteUser' || Object.keys(b).sort().join() !== 'bridgeInstanceId,command,generation,protocolVersion,settingsPath' || !/^[0-9a-f]{64}$/.test(e.deviceId) || !uuid(e.endpointInstanceId) || !uuid(e.executionContextId) || e.remoteUid !== p.effectiveUid || e.remoteUser !== p.account.name || e.remoteHome !== p.account.home || e.endpointCwd !== p.account.home || e.endpointShell !== p.account.shell || b.protocolVersion !== 1 || b.command !== '/usr/local/aw-host/bin/aw-employee-bridge' || b.settingsPath !== settingsPath || !uuid(b.bridgeInstanceId) || !Number.isSafeInteger(b.generation) || b.generation < 1)
|
|
33
|
+
return fail('EMPLOYEE_BRIDGE_PROTOCOL_INVALID', '员工端点返回的身份或代际不符。', '检查同一员工端点与主机桥合同');
|
|
34
|
+
return ok(structuredClone(v));
|
|
35
|
+
}
|
|
36
|
+
const cancelled = () => fail('EMPLOYEE_ENDPOINT_PREPARE_CANCELLED', '员工端点准备等待已取消。', '再次派发前重新核验当前来源;不要重放旧操作');
|
|
37
|
+
// L can withdraw an optional getter while its ordinary service object remains.
|
|
38
|
+
// Observe the public Cordis service lifecycle, including dependency disposal in
|
|
39
|
+
// the same turn; no polling timer, dependency on L startup, or command retry.
|
|
40
|
+
export function bindEmployeeSourceLifecycle(ctx, controller) {
|
|
41
|
+
let disposed = false, scheduled;
|
|
42
|
+
ctx.on('internal/service', () => {
|
|
43
|
+
controller.refreshSource();
|
|
44
|
+
scheduled ??= setImmediate(() => { scheduled = undefined; if (!disposed)
|
|
45
|
+
controller.refreshSource(); });
|
|
46
|
+
});
|
|
47
|
+
ctx.effect(() => () => { disposed = true; if (scheduled)
|
|
48
|
+
clearImmediate(scheduled); });
|
|
49
|
+
controller.refreshSource();
|
|
50
|
+
}
|
|
51
|
+
export function createEmployeeEndpointController(d) {
|
|
52
|
+
let source, disposed = false, revision = 0, held;
|
|
53
|
+
let unsubscribers = [], lifetime = new AbortController(), closing = Promise.resolve(), closeFailed = false;
|
|
54
|
+
let pending;
|
|
55
|
+
const detached = () => { for (const off of unsubscribers)
|
|
56
|
+
off(); unsubscribers = []; };
|
|
57
|
+
function invalidate() {
|
|
58
|
+
revision++;
|
|
59
|
+
lifetime.abort();
|
|
60
|
+
lifetime = new AbortController();
|
|
61
|
+
pending = undefined;
|
|
62
|
+
const old = held;
|
|
63
|
+
held = undefined;
|
|
64
|
+
if (old)
|
|
65
|
+
closing = closing.then(() => old.closeEmployeeEndpoint()).catch(() => { closeFailed = true; console.error('EMPLOYEE_ENDPOINT_CLOSE_FAILED'); });
|
|
66
|
+
}
|
|
67
|
+
function localRemains() {
|
|
68
|
+
const s = source?.getWorkspaceLocationState?.();
|
|
69
|
+
return !!s && s.phase === 'ready' && s.items.some(i => i.source === 'local' && i.status === 'selected' && nonempty(i.bindingId));
|
|
70
|
+
}
|
|
71
|
+
function refreshSource() {
|
|
72
|
+
if (disposed)
|
|
73
|
+
return;
|
|
74
|
+
let current;
|
|
75
|
+
try {
|
|
76
|
+
const value = d.getConnect();
|
|
77
|
+
if (shape(value))
|
|
78
|
+
current = value;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
current = undefined;
|
|
82
|
+
}
|
|
83
|
+
if (current === source) {
|
|
84
|
+
try {
|
|
85
|
+
if (source && !localRemains())
|
|
86
|
+
invalidate();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
invalidate();
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
detached();
|
|
94
|
+
invalidate();
|
|
95
|
+
source = current;
|
|
96
|
+
if (!source)
|
|
97
|
+
return;
|
|
98
|
+
try {
|
|
99
|
+
const own = source;
|
|
100
|
+
let connection = own.getConnectionState();
|
|
101
|
+
unsubscribers.push(own.onWorkspaceLocationState(() => { if (source === own) {
|
|
102
|
+
try {
|
|
103
|
+
if (!localRemains())
|
|
104
|
+
invalidate();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
invalidate();
|
|
108
|
+
}
|
|
109
|
+
} }));
|
|
110
|
+
unsubscribers.push(own.onConnectionState(next => { if (source !== own)
|
|
111
|
+
return; const changed = next.phase !== 'connected' || next.instanceId !== connection.instanceId || next.epoch !== connection.epoch; connection = next; if (changed)
|
|
112
|
+
invalidate(); }));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
detached();
|
|
116
|
+
invalidate();
|
|
117
|
+
source = undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function read(r, s) {
|
|
121
|
+
try {
|
|
122
|
+
if (d.getConnect() !== s || !shape(s))
|
|
123
|
+
return fail('EMPLOYEE_CONTEXT_UNAVAILABLE', '本地工作区上下文已撤回。', '等待当前连接组件提供上下文');
|
|
124
|
+
const connection = s.getConnectionState(), locations = s.getWorkspaceLocationState();
|
|
125
|
+
if (connection.instanceId !== r.instanceId || connection.epoch !== r.epoch || connection.phase !== 'connected' || locations.instanceId !== r.instanceId || locations.epoch !== r.epoch || locations.phase !== 'ready' || !locations.items.some(i => i.workspaceId === r.workspaceId && i.bindingId === r.expectedBindingId && i.source === 'local' && i.status === 'selected'))
|
|
126
|
+
return fail('EMPLOYEE_CONTEXT_CHANGED', '工作区来源或绑定已变化。', '使用当前工作区重新派发');
|
|
127
|
+
const result = await s.getLocalWorkspaceContext(r);
|
|
128
|
+
if (!result.ok)
|
|
129
|
+
return fail('EMPLOYEE_CONTEXT_UNAVAILABLE', '本地工作区上下文暂不可用。', '检查当前工作区来源与目录状态');
|
|
130
|
+
const c = result.value, actual = readEmployeeProcess();
|
|
131
|
+
if (!actual.ok)
|
|
132
|
+
return actual;
|
|
133
|
+
if (c?.version !== 1 || c.instanceId !== r.instanceId || c.epoch !== r.epoch || c.workspaceId !== r.workspaceId || c.bindingId !== r.expectedBindingId || c.source !== 'local' || c.provenance !== 'official-workspace-registry' || !c.directory || !absolute(c.directory.path) || !Number.isSafeInteger(c.directory.device) || !Number.isSafeInteger(c.directory.inode) || !same(c.process, actual.value))
|
|
134
|
+
return fail('EMPLOYEE_CONTEXT_INVALID', '工作区上下文与当前客户端进程不一致。', '重新核对同一客户端的来源与账户');
|
|
135
|
+
const canonical = await realpath(c.directory.path), st = await stat(canonical);
|
|
136
|
+
if (canonical !== c.directory.path || !st.isDirectory() || st.dev !== c.directory.device || st.ino !== c.directory.inode)
|
|
137
|
+
return fail('EMPLOYEE_CONTEXT_CHANGED', '工作区目录身份已变化。', '重新选择当前真实工作区');
|
|
138
|
+
if (d.getConnect() !== s || !shape(s))
|
|
139
|
+
return fail('EMPLOYEE_CONTEXT_UNAVAILABLE', '工作区上下文已撤回。', '等待当前连接组件');
|
|
140
|
+
const after = s.getConnectionState(), ls = s.getWorkspaceLocationState();
|
|
141
|
+
if (after.instanceId !== r.instanceId || after.epoch !== r.epoch || after.phase !== 'connected' || ls.instanceId !== r.instanceId || ls.epoch !== r.epoch || ls.phase !== 'ready' || !ls.items.some(i => i.workspaceId === r.workspaceId && i.bindingId === r.expectedBindingId && i.source === 'local' && i.status === 'selected'))
|
|
142
|
+
return fail('EMPLOYEE_CONTEXT_CHANGED', '工作区在读取期间已变化。', '重新核验当前来源后派发');
|
|
143
|
+
return ok(structuredClone(c));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return fail('EMPLOYEE_CONTEXT_INVALID', '本地工作区信息无法核验。', '检查当前目录与连接组件');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function prepare(r, signal) {
|
|
150
|
+
if (signal.aborted)
|
|
151
|
+
return cancelled();
|
|
152
|
+
if (disposed)
|
|
153
|
+
return fail('DISPOSED', '网络服务已停止。', '重新加载插件');
|
|
154
|
+
if (!requestValid(r))
|
|
155
|
+
return fail('EMPLOYEE_CONTEXT_INVALID', '工作区上下文请求无效。', '由当前工作区提供完整绑定');
|
|
156
|
+
refreshSource();
|
|
157
|
+
const s = source;
|
|
158
|
+
if (!s)
|
|
159
|
+
return fail('EMPLOYEE_CONTEXT_UNAVAILABLE', '我需要当前本地工作区上下文。', '等待连接组件提供本地来源');
|
|
160
|
+
const before = await read(r, s);
|
|
161
|
+
if (!before.ok)
|
|
162
|
+
return before;
|
|
163
|
+
if (signal.aborted)
|
|
164
|
+
return cancelled();
|
|
165
|
+
const token = revision, owner = lifetime.signal;
|
|
166
|
+
const runtime = d.getRuntime();
|
|
167
|
+
if (!runtime || d.getState().phase !== 'online')
|
|
168
|
+
return fail('NETWORK_NOT_READY', '远程主机尚未连接。', '等待原网络连接就绪');
|
|
169
|
+
let remote;
|
|
170
|
+
try {
|
|
171
|
+
remote = await s.getSshTarget();
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return fail('EMPLOYEE_CONTEXT_UNAVAILABLE', '主机连接上下文暂不可用。', '检查连接组件状态');
|
|
175
|
+
}
|
|
176
|
+
if (!remote.ok)
|
|
177
|
+
return fail('EMPLOYEE_CONTEXT_UNAVAILABLE', '主机连接目标尚未就绪。', '等待当前主机连接');
|
|
178
|
+
const target = remote.value;
|
|
179
|
+
if (!target?.session || target.session.protocolVersion !== 1 || !absolute(target.session.settingsPath))
|
|
180
|
+
return fail('EMPLOYEE_CONTEXT_INVALID', '主机桥设置来源无效。', '检查已开通主机设置');
|
|
181
|
+
if (signal.aborted)
|
|
182
|
+
return cancelled();
|
|
183
|
+
const trusted = await d.connect(target, owner);
|
|
184
|
+
if (!trusted.ok)
|
|
185
|
+
return trusted;
|
|
186
|
+
if (signal.aborted)
|
|
187
|
+
return cancelled();
|
|
188
|
+
if (disposed || owner.aborted || revision !== token || d.getConnect() !== s)
|
|
189
|
+
return fail('EMPLOYEE_CONTEXT_CHANGED', '工作区连接已变化。', '重新核验后派发');
|
|
190
|
+
await closing;
|
|
191
|
+
if (closeFailed)
|
|
192
|
+
return fail('EMPLOYEE_ENDPOINT_UNAVAILABLE', '旧员工端点关闭未能确认。', '检查组件私有日志,正常停止旧实例后再继续;不要重放旧操作');
|
|
193
|
+
if (disposed || owner.aborted || revision !== token || d.getRuntime() !== runtime)
|
|
194
|
+
return fail('EMPLOYEE_CONTEXT_CHANGED', '员工端点上下文已变化。', '重新核验后派发');
|
|
195
|
+
// One shared preparation per actual device context, never per workspace. Each caller revalidates its own L CAS.
|
|
196
|
+
let work = pending;
|
|
197
|
+
if (!work || work.runtime !== runtime || work.settingsPath !== target.session.settingsPath || work.revision !== token) {
|
|
198
|
+
held = runtime;
|
|
199
|
+
const promise = runtime.prepareEmployeeEndpoint(target.session.settingsPath, owner).catch(() => fail('EMPLOYEE_ENDPOINT_UNAVAILABLE', '员工端点准备失败,根因未查明。', '检查组件私有诊断'));
|
|
200
|
+
work = { runtime, settingsPath: target.session.settingsPath, revision: token, promise };
|
|
201
|
+
pending = work;
|
|
202
|
+
void promise.finally(() => { if (pending === work)
|
|
203
|
+
pending = undefined; });
|
|
204
|
+
}
|
|
205
|
+
const ready = await new Promise(resolve => {
|
|
206
|
+
const abort = () => { signal.removeEventListener('abort', abort); owner.removeEventListener('abort', withdraw); resolve(cancelled()); };
|
|
207
|
+
const withdraw = () => { signal.removeEventListener('abort', abort); owner.removeEventListener('abort', withdraw); resolve(fail('EMPLOYEE_CONTEXT_CHANGED', '员工端点来源已撤回。', '重新核验后派发')); };
|
|
208
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
209
|
+
owner.addEventListener('abort', withdraw, { once: true });
|
|
210
|
+
work.promise.then(v => { signal.removeEventListener('abort', abort); owner.removeEventListener('abort', withdraw); resolve(v); });
|
|
211
|
+
if (signal.aborted)
|
|
212
|
+
abort();
|
|
213
|
+
else if (owner.aborted)
|
|
214
|
+
withdraw();
|
|
215
|
+
});
|
|
216
|
+
if (!ready.ok)
|
|
217
|
+
return ready;
|
|
218
|
+
const verified = employeeReady(ready.value, before.value.process, target.session.settingsPath);
|
|
219
|
+
if (!verified.ok)
|
|
220
|
+
return verified;
|
|
221
|
+
const after = await read(r, s);
|
|
222
|
+
if (disposed || owner.aborted || revision !== token || d.getRuntime() !== runtime || !after.ok || !same(before.value, after.value))
|
|
223
|
+
return fail('EMPLOYEE_CONTEXT_CHANGED', '工作区在端点准备期间已变化。', '重新核验当前来源后派发,不重放旧操作');
|
|
224
|
+
if (signal.aborted)
|
|
225
|
+
return cancelled();
|
|
226
|
+
return ok({ version: 1, local: { instanceId: r.instanceId, epoch: r.epoch, workspaceId: r.workspaceId, bindingId: r.expectedBindingId, directory: structuredClone(after.value.directory) }, ...verified.value });
|
|
227
|
+
}
|
|
228
|
+
return { prepare, refreshSource, networkLost: invalidate, async dispose() { if (disposed)
|
|
229
|
+
return; disposed = true; detached(); invalidate(); await closing; } };
|
|
230
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import z from '@deepseek-ai/schemastery';
|
|
2
2
|
import { createActivationService } from "./activation.js";
|
|
3
|
+
import { bindEmployeeSourceLifecycle } from "./employee-endpoint.js";
|
|
3
4
|
import { resolvePackagedHelper } from "./packaged-helper.js";
|
|
4
5
|
export const name = 'ai-workbench-network';
|
|
5
6
|
const positive = () => z.number().min(0.001).max(2147483.647);
|
|
@@ -9,7 +10,7 @@ export const Config = z.object({
|
|
|
9
10
|
helperLogFile: z.string().description('远程主机连接诊断日志文件(无默认值)'),
|
|
10
11
|
startupObservationSeconds: positive().description('启动观察 startupObservationSeconds(秒):默认未设,沿开通值(现值35);等待组件ready仅为可观测性;增大等更久,减小可能提前报未知;无关闭值,超时给等待事项、已等时长和排查步骤'),
|
|
11
12
|
shutdownObservationSeconds: positive().description('退出观察 shutdownObservationSeconds(秒):默认未设,沿开通值(现值10);等待自有组件正常退出,仅为可观测性;增大清理等更久,减小可能强制结束并报告;无关闭值,不终止未知进程'),
|
|
12
|
-
controlResponseMaxBytes: bytes().description('控制响应上限 controlResponseMaxBytes(字节):默认未设,沿开通值(现值65536);控制JSON、ready
|
|
13
|
+
controlResponseMaxBytes: bytes().description('控制响应上限 controlResponseMaxBytes(字节):默认未设,沿开通值(现值65536);控制JSON、ready及代理首帧,也约束员工端点私有控制请求/响应和主机桥首帧;增大增加内存,减小可能拒绝合法帧;无关闭或无限值,不限制下载或SFTP正文'),
|
|
13
14
|
downloadMaxRedirects: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).description('下载重定向上限 downloadMaxRedirects:默认未设,沿开通值(现值3);增大允许更多HTTPS跳转,减小可能拒绝下载,0关闭跳转;不自动换源'),
|
|
14
15
|
statusRefreshIntervalSeconds: positive().description('状态刷新间隔 statusRefreshIntervalSeconds(秒):默认沿已开通配置(现值2);影响组件状态查询及插件轮询;增大提示更迟,减小增加查询开销;无关闭值,不代表失败重试已修复'),
|
|
15
16
|
dialTimeoutSeconds: positive().description('拨号观察 dialTimeoutSeconds(秒):默认沿已开通配置(现值20);远端TCP/SSH、控制响应头、下载CONNECT/响应头;Node请求采用阶段空闲观察,仅为可观测性,增大等更久,减小可能提前中止;无关闭值,超时说明等待事项、已等时长、下一步检查,不是修复'),
|
|
@@ -27,8 +28,10 @@ export function apply(ctx, config = {}) {
|
|
|
27
28
|
if (!executable.ok) {
|
|
28
29
|
const error = executable;
|
|
29
30
|
const state = { phase: 'blocked', ...!error.ok ? { error: error.error } : {} };
|
|
30
|
-
register(ctx, { async activate() { return error; }, getActivationState: () => ({ instanceId: 'unpackaged', revision: 0, phase: 'blocked', receipt: null, error: !error.ok ? error.error : null }), onActivationState: () => () => { }, async start() { ctx.emit('ai-workbench/network-state', state); }, getState: () => structuredClone(state), async connect() { return error; }, onState: () => () => { }, async openLoginProxy() { return error; }, async download() { return error; }, async dispose() { } });
|
|
31
|
+
register(ctx, { async prepareEmployeeEndpoint() { return error; }, async activate() { return error; }, getActivationState: () => ({ instanceId: 'unpackaged', revision: 0, phase: 'blocked', receipt: null, error: !error.ok ? error.error : null }), onActivationState: () => () => { }, async start() { ctx.emit('ai-workbench/network-state', state); }, getState: () => structuredClone(state), async connect() { return error; }, onState: () => () => { }, async openLoginProxy() { return error; }, async download() { return error; }, async dispose() { } });
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
+
const api = createActivationService(config, executable.value, state => ctx.emit('ai-workbench/network-state', state), undefined, () => ctx.get('aiWorkbenchConnect', false));
|
|
35
|
+
register(ctx, api);
|
|
36
|
+
bindEmployeeSourceLifecycle(ctx, { refreshSource: api.refreshEmployeeSource });
|
|
34
37
|
}
|
package/lib/network.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { control } from './control.ts';
|
|
2
2
|
import type { PluginSettings, NetworkChannels, NetworkState } from './api.ts';
|
|
3
|
+
import type { EmployeeRuntime } from './employee-endpoint.ts';
|
|
3
4
|
export interface Executable {
|
|
4
5
|
path: string;
|
|
5
6
|
sha256: string;
|
|
6
7
|
platform: string;
|
|
7
8
|
arch: string;
|
|
8
9
|
}
|
|
9
|
-
export declare function createNetwork(settings: PluginSettings, executable: Executable, emit?: (state: NetworkState) => void, controlIO?: typeof control): NetworkChannels & {
|
|
10
|
+
export declare function createNetwork(settings: PluginSettings, executable: Executable, emit?: (state: NetworkState) => void, controlIO?: typeof control): NetworkChannels & EmployeeRuntime & {
|
|
10
11
|
start(): Promise<void>;
|
|
11
12
|
};
|
package/lib/network.js
CHANGED
|
@@ -7,6 +7,7 @@ import { fail, ok } from "./api.js";
|
|
|
7
7
|
import { downloadThroughProxy } from "./download.js";
|
|
8
8
|
import { effectiveConfig, helperPolicyArgs, validHelperPolicy } from "./settings.js";
|
|
9
9
|
import { ObservationExpired, observationNeed } from "./observation.js";
|
|
10
|
+
import { readEmployeeProcess, employeeReady } from "./employee-endpoint.js";
|
|
10
11
|
const seconds = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0.001 && v * 1000 <= 2147483647;
|
|
11
12
|
const integer = (v) => Number.isSafeInteger(v) && Number(v) > 0;
|
|
12
13
|
const quote = (s) => "'" + s.replaceAll("'", "'\\''") + "'";
|
|
@@ -45,6 +46,7 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
45
46
|
const childGone = new AbortController();
|
|
46
47
|
const lifetime = new AbortController(), listeners = new Set(), updates = new Set();
|
|
47
48
|
const leases = new Set();
|
|
49
|
+
let employeeParent, employeeUmask;
|
|
48
50
|
const snapshot = () => structuredClone(state);
|
|
49
51
|
const publish = (next) => {
|
|
50
52
|
state = next;
|
|
@@ -185,8 +187,20 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
185
187
|
await log.close();
|
|
186
188
|
return;
|
|
187
189
|
}
|
|
190
|
+
const parent = readEmployeeProcess();
|
|
191
|
+
employeeParent = parent.ok ? parent.value : undefined;
|
|
192
|
+
const environment = { ...process.env };
|
|
193
|
+
delete environment.AW_NET_PARENT_PID;
|
|
194
|
+
delete environment.AW_NET_PARENT_SHELL;
|
|
195
|
+
delete environment.AW_NET_PARENT_UMASK;
|
|
196
|
+
if (employeeParent) {
|
|
197
|
+
employeeUmask = process.umask();
|
|
198
|
+
environment.AW_NET_PARENT_PID = String(process.pid);
|
|
199
|
+
environment.AW_NET_PARENT_SHELL = employeeParent.account.shell;
|
|
200
|
+
environment.AW_NET_PARENT_UMASK = String(employeeUmask);
|
|
201
|
+
}
|
|
188
202
|
try {
|
|
189
|
-
child = spawn(executable.path, ['serve', '--config', settings.configFile, ...helperPolicyArgs(settings)], { stdio: ['ignore', 'pipe', log.fd], env:
|
|
203
|
+
child = spawn(executable.path, ['serve', '--config', settings.configFile, ...helperPolicyArgs(settings), '--controlResponseMaxBytes', String(settings.controlResponseMaxBytes)], { stdio: ['ignore', 'pipe', log.fd], env: environment });
|
|
190
204
|
}
|
|
191
205
|
catch (error) {
|
|
192
206
|
await log.close();
|
|
@@ -350,6 +364,46 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
350
364
|
},
|
|
351
365
|
async download(source, signal) { const lease = await api.openLoginProxy(signal); if (!lease.ok)
|
|
352
366
|
return lease; return downloadThroughProxy(source, lease.value, signal, settings.downloadMaxRedirects, cfg.dialTimeoutSeconds, cfg.httpMaxResponseHeaderBytes); },
|
|
367
|
+
async prepareEmployeeEndpoint(settingsPath, signal) {
|
|
368
|
+
if (disposed)
|
|
369
|
+
return fail('DISPOSED', '网络服务已停止。', '重新加载插件');
|
|
370
|
+
if (signal.aborted)
|
|
371
|
+
return fail('EMPLOYEE_ENDPOINT_PREPARE_CANCELLED', '员工端点准备等待已取消。', '派发前重新核验来源');
|
|
372
|
+
if (!cfg || state.phase !== 'online' || !alive())
|
|
373
|
+
return fail('NETWORK_NOT_READY', '远程主机尚未连接。', '等待原网络连接');
|
|
374
|
+
const actual = readEmployeeProcess();
|
|
375
|
+
if (!actual.ok)
|
|
376
|
+
return actual;
|
|
377
|
+
if (!employeeParent || employeeUmask !== process.umask() || JSON.stringify(employeeParent) !== JSON.stringify(actual.value))
|
|
378
|
+
return fail('EMPLOYEE_ENVIRONMENT_UNAVAILABLE', '客户端环境在组件启动后已变化。', '正常重载插件后重新核验,不替换账户或环境');
|
|
379
|
+
try {
|
|
380
|
+
const response = await controlIO(socket(), 'POST', '/v1/employee-endpoint', AbortSignal.any([signal, childGone.signal, lifetime.signal]), cfg.dialTimeoutSeconds, cfg.httpMaxHeaderBytes, { version: 1, settingsPath });
|
|
381
|
+
const value = await jsonResponse(response, settings.controlResponseMaxBytes);
|
|
382
|
+
if (response.statusCode !== 200) {
|
|
383
|
+
const error = value?.error;
|
|
384
|
+
if (value?.apiVersion !== 1 || !error || !employeeNeedCodes.has(error.code) || !['message', 'needs', 'actor'].every(k => typeof error[k] === 'string') || (error.cause !== null && error.cause !== undefined && typeof error.cause !== 'string'))
|
|
385
|
+
return fail('EMPLOYEE_BRIDGE_PROTOCOL_INVALID', '员工端点控制响应无效。', '核对网络组件版本与合同');
|
|
386
|
+
return { ok: false, error: { code: error.code, message: error.message, needs: error.needs, actor: 'network', cause: error.cause ?? null } };
|
|
387
|
+
}
|
|
388
|
+
if (value?.apiVersion !== 1 || Object.keys(value).sort().join() !== 'apiVersion,bridge,endpoint')
|
|
389
|
+
return fail('EMPLOYEE_BRIDGE_PROTOCOL_INVALID', '员工端点控制帧无效。', '核对网络组件版本');
|
|
390
|
+
return employeeReady({ endpoint: value.endpoint, bridge: value.bridge }, actual.value, settingsPath);
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
if (signal.aborted)
|
|
394
|
+
return fail('EMPLOYEE_ENDPOINT_PREPARE_CANCELLED', '员工端点准备等待已取消。', '重新核验后派发,不重放旧操作');
|
|
395
|
+
const observed = observationNeed(error);
|
|
396
|
+
return observed ? { ok: false, error: observed } : fail('EMPLOYEE_ENDPOINT_UNAVAILABLE', '员工端点控制连接失败,根因未查明。', '检查组件私有日志;普通网络状态不代表端点就绪');
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
async closeEmployeeEndpoint() {
|
|
400
|
+
if (!cfg || !alive())
|
|
401
|
+
return;
|
|
402
|
+
const response = await request('DELETE', '/v1/employee-endpoint', lifetime.signal);
|
|
403
|
+
const value = await jsonResponse(response, settings.controlResponseMaxBytes);
|
|
404
|
+
if (response.statusCode !== 200 || value?.apiVersion !== 1 || value?.closed !== true)
|
|
405
|
+
throw Error('EMPLOYEE_ENDPOINT_UNAVAILABLE');
|
|
406
|
+
},
|
|
353
407
|
dispose() {
|
|
354
408
|
return disposing ??= (async () => {
|
|
355
409
|
disposed = true;
|
|
@@ -369,3 +423,4 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
369
423
|
};
|
|
370
424
|
return api;
|
|
371
425
|
}
|
|
426
|
+
const employeeNeedCodes = new Set(['EMPLOYEE_CONTEXT_UNAVAILABLE', 'EMPLOYEE_CONTEXT_INVALID', 'EMPLOYEE_CONTEXT_CHANGED', 'EMPLOYEE_ACCOUNT_UNAVAILABLE', 'EMPLOYEE_ENVIRONMENT_UNAVAILABLE', 'EMPLOYEE_IDENTITY_STATE_INVALID', 'EMPLOYEE_IDENTITY_ACCOUNT_MISMATCH', 'EMPLOYEE_IDENTITY_BUSY_OR_UNSAFE', 'EMPLOYEE_IDENTITY_CREATE_FAILED', 'EMPLOYEE_IDENTITY_STORAGE_FAILED', 'EMPLOYEE_BRIDGE_UNAVAILABLE', 'EMPLOYEE_BRIDGE_PROTOCOL_INVALID', 'EMPLOYEE_BRIDGE_FAILED', 'EMPLOYEE_ENDPOINT_UNAVAILABLE', 'EMPLOYEE_ENDPOINT_PREPARE_CANCELLED', 'HELPER_PLATFORM_UNSUPPORTED', 'HOST_TRUST_REQUIRED', 'HOST_KEY_MISMATCH', 'ENROLLMENT_MISMATCH', 'DIAL_OBSERVATION_EXPIRED', 'LOCAL_PERMISSION_DENIED', 'NETWORK_INTERRUPTED', 'SSH_FAILED', 'NETWORK_NOT_READY', 'DISPOSED']);
|