orbit-codex-bridge 0.1.0 → 0.1.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.md +55 -150
- package/cli.mjs +105 -34
- package/lib/connector-service.mjs +259 -0
- package/package.json +6 -3
- package/public/admin.html +1 -1
- package/public/admin.js +1 -1
- package/public/app.js +117 -46
- package/public/connect.html +2 -2
- package/public/index.html +22 -3
- package/relay-agent.mjs +120 -43
- package/relay-server.mjs +13 -5
- package/relay-store.mjs +1 -1
- package/server.mjs +10 -4
package/relay-agent.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import https from 'node:https';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import WebSocket from 'ws';
|
|
6
7
|
|
|
7
8
|
const maxBody = 12 * 1024 * 1024;
|
|
8
9
|
const isLoopback = url => ['localhost','127.0.0.1','[::1]'].includes(url.hostname);
|
|
10
|
+
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
9
11
|
|
|
10
12
|
export function createRelayAgent(options = {}) {
|
|
11
13
|
const relay = new URL(options.relayUrl || process.env.ORBIT_RELAY_URL || '');
|
|
@@ -14,6 +16,11 @@ export function createRelayAgent(options = {}) {
|
|
|
14
16
|
const deviceId = options.deviceId || process.env.ORBIT_RELAY_DEVICE_ID || '';
|
|
15
17
|
const onReady = options.onReady || (() => {});
|
|
16
18
|
const onError = options.onError || (() => {});
|
|
19
|
+
const onState = options.onState || (() => {});
|
|
20
|
+
const heartbeatInterval = options.heartbeatInterval ?? 15000;
|
|
21
|
+
const heartbeatTimeout = options.heartbeatTimeout ?? 45000;
|
|
22
|
+
const reconnectDelay = options.reconnectDelay ?? 1000;
|
|
23
|
+
if (heartbeatInterval <= 0 || heartbeatTimeout <= heartbeatInterval || reconnectDelay < 0) throw new Error('连接心跳参数无效。');
|
|
17
24
|
if (relay.username || relay.password || relay.search || relay.hash || relay.pathname !== '/agent'
|
|
18
25
|
|| !(relay.protocol === 'wss:' || relay.protocol === 'ws:' && isLoopback(relay))) {
|
|
19
26
|
throw new Error('ORBIT_RELAY_URL 必须为 wss://域名/agent;仅本地测试允许 ws。');
|
|
@@ -27,6 +34,7 @@ export function createRelayAgent(options = {}) {
|
|
|
27
34
|
let stopped = true;
|
|
28
35
|
let reconnectTimer;
|
|
29
36
|
let readyTimer;
|
|
37
|
+
let heartbeatTimer;
|
|
30
38
|
let retry = 0;
|
|
31
39
|
const bridges = new Map();
|
|
32
40
|
const requests = new Set();
|
|
@@ -42,51 +50,90 @@ export function createRelayAgent(options = {}) {
|
|
|
42
50
|
requests.clear();
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
function
|
|
53
|
+
function requestLocal(method, targetPath, headers, body, cloudSocket) {
|
|
54
|
+
return new Promise((resolve,reject) => {
|
|
55
|
+
if (socket !== cloudSocket || cloudSocket.readyState !== WebSocket.OPEN) return reject(new Error('云端连接已断开。'));
|
|
56
|
+
if (requests.size >= 100) return reject(new Error('电脑端请求过多,请稍后重试。'));
|
|
57
|
+
const target = new URL(targetPath,local);
|
|
58
|
+
const transport = target.protocol === 'https:' ? https : http;
|
|
59
|
+
const request = transport.request(target,{method,headers:{...headers,host:local.host,origin:local.origin},timeout:25000},response => {
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let size = 0;
|
|
62
|
+
response.on('data',chunk => {
|
|
63
|
+
size += chunk.length;
|
|
64
|
+
if (size > maxBody) request.destroy(new Error('本机响应过大。'));
|
|
65
|
+
else chunks.push(chunk);
|
|
66
|
+
});
|
|
67
|
+
response.on('error',error => request.destroy(error));
|
|
68
|
+
response.on('end',() => {
|
|
69
|
+
requests.delete(request);
|
|
70
|
+
resolve({statusCode:response.statusCode || 502,headers:{
|
|
71
|
+
'content-type':response.headers['content-type'] || 'application/json',
|
|
72
|
+
...(response.headers['set-cookie'] ? {'set-cookie':response.headers['set-cookie']} : {})
|
|
73
|
+
},body:Buffer.concat(chunks)});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
requests.add(request);
|
|
77
|
+
request.on('timeout',() => request.destroy(new Error('本机 Orbit 响应超时。')));
|
|
78
|
+
request.on('error',error => {
|
|
79
|
+
requests.delete(request);
|
|
80
|
+
reject(error);
|
|
81
|
+
});
|
|
82
|
+
request.end(body);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function respondLocal(message, cloudSocket, result) {
|
|
87
|
+
if (socket === cloudSocket && cloudSocket.readyState === WebSocket.OPEN) {
|
|
88
|
+
cloudSocket.send(JSON.stringify({type:'http-response',requestId:message.requestId,...result,body:result.body.toString('base64')}));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function localRequest(message, cloudSocket) {
|
|
46
93
|
if (typeof message.path !== 'string' || !message.path.startsWith('/api/') || message.path.startsWith('//')
|
|
47
94
|
|| !['GET','POST'].includes(message.method)) return cloudSocket.close(1008,'Invalid local target');
|
|
48
95
|
const target = new URL(message.path,local);
|
|
49
96
|
if (target.origin !== local.origin) return cloudSocket.close(1008,'Invalid local target');
|
|
50
97
|
const body = Buffer.from(message.body || '', 'base64');
|
|
51
|
-
if (body.length > maxBody
|
|
52
|
-
const transport = target.protocol === 'https:' ? https : http;
|
|
53
|
-
let finished = false;
|
|
54
|
-
const respond = (statusCode,headers,responseBody) => {
|
|
55
|
-
if (finished) return;
|
|
56
|
-
finished = true;
|
|
57
|
-
if (socket === cloudSocket && cloudSocket.readyState === WebSocket.OPEN) {
|
|
58
|
-
cloudSocket.send(JSON.stringify({type:'http-response',requestId:message.requestId,statusCode,headers,body:responseBody.toString('base64')}));
|
|
59
|
-
}
|
|
60
|
-
};
|
|
98
|
+
if (body.length > maxBody) return;
|
|
61
99
|
const headers = {};
|
|
62
|
-
for (const name of ['accept','content-type','cookie','
|
|
100
|
+
for (const name of ['accept','content-type','cookie','user-agent']) {
|
|
63
101
|
const value = message.headers?.[name];
|
|
64
102
|
if (typeof value === 'string') headers[name] = value;
|
|
65
103
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
try {
|
|
105
|
+
respondLocal(message,cloudSocket,await requestLocal(message.method,message.path,headers,body,cloudSocket));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
respondLocal(message,cloudSocket,{statusCode:502,headers:{},body:Buffer.from(JSON.stringify({error:error.message}))});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function connectAccount(message, cloudSocket) {
|
|
112
|
+
try {
|
|
113
|
+
const cookie = message.headers?.cookie || '';
|
|
114
|
+
if (cookie) {
|
|
115
|
+
const current = await requestLocal('GET','/api/status',{cookie},undefined,cloudSocket);
|
|
116
|
+
if (current.statusCode === 200) {
|
|
117
|
+
respondLocal(message,cloudSocket,{statusCode:200,headers:{},body:Buffer.from('{"ok":true}')});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (current.statusCode !== 401) throw new Error('本机连接状态异常。');
|
|
121
|
+
}
|
|
122
|
+
const filename = options.pairingKeyFile || process.env.ORBIT_PAIRING_KEY_FILE || path.join(root,'.runtime','pairing-key');
|
|
123
|
+
const pairingKey = String(options.pairingKey || await fs.readFile(filename,'utf8')).trim();
|
|
124
|
+
if (!/^[a-zA-Z0-9_-]{20,100}$/.test(pairingKey)) throw new Error('本机凭证不可用。');
|
|
125
|
+
const paired = await requestLocal('POST','/api/pair',{'content-type':'application/json'},
|
|
126
|
+
Buffer.from(JSON.stringify({code:pairingKey})),cloudSocket);
|
|
127
|
+
if (paired.statusCode !== 200 || !paired.headers['set-cookie']?.some(value => /^orbit_session=[a-zA-Z0-9_-]{20,100};/.test(value))) {
|
|
128
|
+
throw new Error('本机鉴权未完成。');
|
|
129
|
+
}
|
|
130
|
+
respondLocal(message,cloudSocket,{statusCode:200,headers:paired.headers,body:Buffer.from('{"ok":true}')});
|
|
131
|
+
} catch {
|
|
132
|
+
respondLocal(message,cloudSocket,{
|
|
133
|
+
statusCode:503,headers:{},
|
|
134
|
+
body:Buffer.from(JSON.stringify({error:'电脑连接器未能完成账号连接,请在电脑上运行 orbit restart,或更新连接器后重试。',code:'local_account_connect_failed'}))
|
|
81
135
|
});
|
|
82
|
-
}
|
|
83
|
-
requests.add(request);
|
|
84
|
-
request.on('timeout',() => request.destroy(new Error('本机 Orbit 响应超时。')));
|
|
85
|
-
request.on('error',error => {
|
|
86
|
-
requests.delete(request);
|
|
87
|
-
respond(502,{'content-type':'application/json'},Buffer.from(JSON.stringify({error:error.message})));
|
|
88
|
-
});
|
|
89
|
-
request.end(body);
|
|
136
|
+
}
|
|
90
137
|
}
|
|
91
138
|
|
|
92
139
|
function openLocalSocket(message, cloudSocket) {
|
|
@@ -95,8 +142,8 @@ export function createRelayAgent(options = {}) {
|
|
|
95
142
|
target.protocol = target.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
96
143
|
const localSocket = new WebSocket(target,{
|
|
97
144
|
headers:{
|
|
98
|
-
Cookie:message.headers?.cookie || '', Origin:
|
|
99
|
-
Host:
|
|
145
|
+
Cookie:message.headers?.cookie || '', Origin:local.origin,
|
|
146
|
+
Host:local.host
|
|
100
147
|
},
|
|
101
148
|
maxPayload:maxBody,handshakeTimeout:8000
|
|
102
149
|
});
|
|
@@ -122,8 +169,29 @@ export function createRelayAgent(options = {}) {
|
|
|
122
169
|
if (stopped) return;
|
|
123
170
|
const cloudSocket = new WebSocket(relay,{maxPayload:18*1024*1024,handshakeTimeout:10000});
|
|
124
171
|
socket = cloudSocket;
|
|
172
|
+
onState({status:'connecting'});
|
|
125
173
|
readyTimer = setTimeout(() => cloudSocket.terminate(),15000);
|
|
126
|
-
|
|
174
|
+
let pendingPing = null;
|
|
175
|
+
let pingSentAt = 0;
|
|
176
|
+
cloudSocket.on('open',() => {
|
|
177
|
+
cloudSocket.send(JSON.stringify({type:'agent-auth',token,deviceId,capabilities:['account-connect']}));
|
|
178
|
+
heartbeatTimer = setInterval(() => {
|
|
179
|
+
if (socket !== cloudSocket || cloudSocket.readyState !== WebSocket.OPEN) return;
|
|
180
|
+
if (pendingPing !== null) {
|
|
181
|
+
if (Date.now() - pingSentAt >= heartbeatTimeout) {
|
|
182
|
+
onError(new Error('云端心跳超时,正在自动重连。'));
|
|
183
|
+
cloudSocket.terminate();
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
pendingPing = `${Date.now()}`;
|
|
188
|
+
pingSentAt = Date.now();
|
|
189
|
+
cloudSocket.ping(pendingPing);
|
|
190
|
+
},heartbeatInterval);
|
|
191
|
+
});
|
|
192
|
+
cloudSocket.on('pong',data => {
|
|
193
|
+
if (data.toString() === pendingPing) pendingPing = null;
|
|
194
|
+
});
|
|
127
195
|
cloudSocket.on('error',error => { if (!stopped) onError(error); });
|
|
128
196
|
cloudSocket.on('message',raw => {
|
|
129
197
|
if (socket !== cloudSocket) return;
|
|
@@ -131,8 +199,9 @@ export function createRelayAgent(options = {}) {
|
|
|
131
199
|
try { message = JSON.parse(raw.toString()); } catch { cloudSocket.close(1003,'Invalid relay message'); return; }
|
|
132
200
|
if (!message || typeof message !== 'object') return;
|
|
133
201
|
if (message.type === 'agent-ready') {
|
|
134
|
-
clearTimeout(readyTimer);retry = 0;onReady(message);return;
|
|
202
|
+
clearTimeout(readyTimer);retry = 0;onState({status:'online'});onReady(message);return;
|
|
135
203
|
}
|
|
204
|
+
if (message.type === 'account-connect') return void connectAccount(message,cloudSocket);
|
|
136
205
|
if (message.type === 'http-request') return localRequest(message,cloudSocket);
|
|
137
206
|
if (message.type === 'ws-open') return openLocalSocket(message,cloudSocket);
|
|
138
207
|
const bridge = bridges.get(message.bridgeId);
|
|
@@ -151,11 +220,19 @@ export function createRelayAgent(options = {}) {
|
|
|
151
220
|
else bridge.socket.terminate();
|
|
152
221
|
}
|
|
153
222
|
});
|
|
154
|
-
cloudSocket.on('close',() => {
|
|
223
|
+
cloudSocket.on('close',(code,reason) => {
|
|
155
224
|
if (socket !== cloudSocket) return;
|
|
156
|
-
socket = null;clearTimeout(readyTimer);clearLocalConnections();
|
|
225
|
+
socket = null;clearTimeout(readyTimer);clearInterval(heartbeatTimer);clearLocalConnections();
|
|
157
226
|
if (!stopped) {
|
|
158
|
-
|
|
227
|
+
if (code === 1008 || code === 1012 && reason.toString() === 'Replaced by a newer agent') {
|
|
228
|
+
stopped = true;
|
|
229
|
+
const detail = code === 1008 ? '电脑凭证被拒绝,请检查账号或重新登录。' : '连接已由另一个 Orbit 实例接管。';
|
|
230
|
+
onState({status:'blocked',reason:detail});
|
|
231
|
+
onError(new Error(detail));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const delay = Math.min(30000,reconnectDelay * 2 ** Math.min(5,retry++));
|
|
235
|
+
onState({status:'reconnecting',retryIn:delay,reason:reason.toString() || `连接断开 (${code})`});
|
|
159
236
|
reconnectTimer = setTimeout(connect,delay);
|
|
160
237
|
}
|
|
161
238
|
});
|
|
@@ -164,7 +241,7 @@ export function createRelayAgent(options = {}) {
|
|
|
164
241
|
return {
|
|
165
242
|
start() { if (!stopped) return; stopped = false;connect(); },
|
|
166
243
|
stop() {
|
|
167
|
-
stopped = true;clearTimeout(reconnectTimer);clearTimeout(readyTimer);
|
|
244
|
+
stopped = true;clearTimeout(reconnectTimer);clearTimeout(readyTimer);clearInterval(heartbeatTimer);
|
|
168
245
|
const previous = socket;socket = null;previous?.terminate();clearLocalConnections();
|
|
169
246
|
}
|
|
170
247
|
};
|
package/relay-server.mjs
CHANGED
|
@@ -254,12 +254,15 @@ export function createRelayServer(options = {}) {
|
|
|
254
254
|
return devices[0];
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
async function proxyHttp(request,response,authenticated) {
|
|
257
|
+
async function proxyHttp(request,response,authenticated,accountConnect = false) {
|
|
258
258
|
let device;
|
|
259
259
|
try{device=chooseDevice(request,authenticated.user)}
|
|
260
260
|
catch(error){return json(response,error.status||409,{error:safeError(error),code:error.code||'device_unavailable'})}
|
|
261
261
|
const agent=agents.get(device.id);
|
|
262
262
|
if(!agent||agent.socket.readyState!==WebSocket.OPEN)return json(response,503,{error:`电脑“${device.name}”当前不在线。`});
|
|
263
|
+
if(accountConnect&&!agent.accountConnect)return json(response,409,{
|
|
264
|
+
error:'请在电脑上更新 Orbit 连接器到 0.1.2 或更高版本后重试。',code:'connector_upgrade_required'
|
|
265
|
+
});
|
|
263
266
|
const body=await readBody(request);
|
|
264
267
|
if (!store.hasSession(authenticated.tokenHash,authenticated.user.id)) return json(response,401,{error:'请先登录云端工作台。',code:'cloud_login_required'});
|
|
265
268
|
chooseDevice(request, store.getUser(authenticated.user.id));
|
|
@@ -273,9 +276,12 @@ export function createRelayServer(options = {}) {
|
|
|
273
276
|
pendingHttp.set(requestId,{resolve,reject,timer,deviceId:device.id,agent,userId:authenticated.user.id,sessionHash:authenticated.tokenHash});
|
|
274
277
|
});
|
|
275
278
|
try{
|
|
276
|
-
sendAgent(agent,
|
|
279
|
+
sendAgent(agent,accountConnect
|
|
280
|
+
? {type:'account-connect',requestId,headers:{cookie:headers.cookie}}
|
|
281
|
+
: {type:'http-request',requestId,method:request.method,path:request.url,headers,body:body.toString('base64')});
|
|
277
282
|
const result=await promise;
|
|
278
283
|
if (!store.hasSession(authenticated.tokenHash,authenticated.user.id)) return json(response,401,{error:'请先登录云端工作台。',code:'cloud_login_required'});
|
|
284
|
+
chooseDevice(request,store.getUser(authenticated.user.id));
|
|
279
285
|
response.writeHead(result.statusCode,responseHeaders(result.headers,device.id,secureCookie));
|
|
280
286
|
response.end(Buffer.from(result.body||'','base64'));
|
|
281
287
|
}catch(error){if(!response.destroyed)json(response,502,{error:safeError(error)})}
|
|
@@ -530,12 +536,13 @@ export function createRelayServer(options = {}) {
|
|
|
530
536
|
if(!device)return json(response,403,{error:'设备不属于当前账号。'});
|
|
531
537
|
return json(response,200,{device:publicDevice(device)});
|
|
532
538
|
}
|
|
533
|
-
|
|
539
|
+
const accountConnect=url.pathname==='/api/cloud/connect'&&request.method==='POST';
|
|
540
|
+
if(accountConnect||isLocalApi(url.pathname,request.method)){
|
|
534
541
|
if(request.method!=='GET'&&!sameOrigin(request,true))return json(response,403,{error:'来源校验失败。'});
|
|
535
542
|
const current=userSession(request);if(!current)return json(response,401,{error:'请先登录云端工作台。',code:'cloud_login_required'});
|
|
536
543
|
const devices=store.devicesForUser(current.user.id);
|
|
537
544
|
if(devices.length>Number(current.user.maxDevices))return json(response,403,{error:'设备数量超出套餐额度。'});
|
|
538
|
-
return await proxyHttp(request,response,current);
|
|
545
|
+
return await proxyHttp(request,response,current,accountConnect);
|
|
539
546
|
}
|
|
540
547
|
const file=staticTarget(url.pathname);
|
|
541
548
|
if(file&&['GET','HEAD'].includes(request.method)){
|
|
@@ -565,7 +572,8 @@ export function createRelayServer(options = {}) {
|
|
|
565
572
|
agent=store.authenticateDevice(message.token,message.deviceId);
|
|
566
573
|
if(!agent){clearTimeout(authTimer);return websocket.close(1008,'Agent authentication failed')}
|
|
567
574
|
clearTimeout(authTimer);
|
|
568
|
-
const entry={socket:websocket,deviceId:agent.id,userId:agent.userId,name:agent.name,isAlive:true
|
|
575
|
+
const entry={socket:websocket,deviceId:agent.id,userId:agent.userId,name:agent.name,isAlive:true,
|
|
576
|
+
accountConnect:Array.isArray(message.capabilities)&&message.capabilities.includes('account-connect')};
|
|
569
577
|
const previous=agents.get(agent.id);
|
|
570
578
|
if(previous&&previous.socket!==websocket)closeDeviceConnections(agent.id,1012,'Replaced by a newer agent');
|
|
571
579
|
agents.set(agent.id,entry);
|
package/relay-store.mjs
CHANGED
|
@@ -21,7 +21,7 @@ function safeString(value, label, max = 120) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function validPassword(value) {
|
|
24
|
-
if(typeof value!=='string'||value.length<
|
|
24
|
+
if(typeof value!=='string'||value.length<6||value.length>256||/[\u0000-\u001f\u007f]/.test(value))throw new Error('密码需要 6–256 个字符,不能包含控制字符。');
|
|
25
25
|
return value;
|
|
26
26
|
}
|
|
27
27
|
|
package/server.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { CodexRpc } from './lib/rpc.mjs';
|
|
|
11
11
|
import { DesktopRpc } from './lib/desktop.mjs';
|
|
12
12
|
|
|
13
13
|
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const packageVersion = JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8')).version;
|
|
14
15
|
const sessionLifetime = 12 * 60 * 60 * 1000;
|
|
15
16
|
const safeError = error => String(error.message || error).replace(/Bearer\s+\S+/gi, 'Bearer [REDACTED]').replace(/(?:sk-|sk_)[\w-]{12,}/g, '[REDACTED]').slice(0, 700);
|
|
16
17
|
|
|
@@ -118,7 +119,7 @@ export function createOrbitServer(options = {}) {
|
|
|
118
119
|
const threads = store.list();
|
|
119
120
|
for (const id of session.enabled.keys()) controlFor(session, id);
|
|
120
121
|
return {
|
|
121
|
-
product: 'Orbit', version:
|
|
122
|
+
product: 'Orbit', version: packageVersion, device: os.hostname(), platform: process.platform, home,
|
|
122
123
|
threadCount: threads.length, projectCount: new Set(threads.map(thread => thread.projectId)).size,
|
|
123
124
|
archivedCount: threads.filter(thread => thread.archived).length, lockedCount: threads.filter(thread => thread.locked).length,
|
|
124
125
|
bridge: 'connected', transport: 'websocket', readOnlyMirror: true,
|
|
@@ -308,7 +309,8 @@ export function createOrbitServer(options = {}) {
|
|
|
308
309
|
const body = message.params || {};
|
|
309
310
|
const id = body.threadId;
|
|
310
311
|
if (message.method === 'thread.subscribe') {
|
|
311
|
-
|
|
312
|
+
const row = id ? store.row(id) : null;
|
|
313
|
+
if (id && !row) throw new Error('会话不存在。');
|
|
312
314
|
if (socket.desktopSubscription && socket.desktopSubscription !== (id || null)) unwatchDesktop(socket);
|
|
313
315
|
socket.subscription = id || null;
|
|
314
316
|
let runtime = null;
|
|
@@ -327,6 +329,10 @@ export function createOrbitServer(options = {}) {
|
|
|
327
329
|
};
|
|
328
330
|
}
|
|
329
331
|
}
|
|
332
|
+
if (executionMode === 'desktop' && runtime?.attached && body.autoControl === true
|
|
333
|
+
&& !row.archived && !String(row.source).startsWith('{')) {
|
|
334
|
+
session.enabled.set(id, {sharedExecution:true,desktop:true,writable:false,expires:Date.now() + 1800000});
|
|
335
|
+
}
|
|
330
336
|
const control = controlFor(session, id);
|
|
331
337
|
const approvals = control ? (executionMode === 'desktop' ? runtime?.approvals || [] : [...rpc.approvals.values()].filter(request => request.params.threadId === id).map(request => ({ ...request.params, requestId: String(request.id), method: request.method }))) : [];
|
|
332
338
|
return { subscribed: id || null, control, runtime, settings: control ? session.preferences.get(id) || null : null, turnId: runtime?.turnId || rpc.threads.get(id)?.turnId || null, approvals };
|
|
@@ -476,10 +482,10 @@ export function createOrbitServer(options = {}) {
|
|
|
476
482
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
477
483
|
const app = createOrbitServer();
|
|
478
484
|
const info = await app.listen();
|
|
479
|
-
console.log(`Orbit
|
|
485
|
+
console.log(`Orbit ${packageVersion} · ${info.url}`);
|
|
480
486
|
console.log(`Native conversations: ${app.store.list().length} · default read-only`);
|
|
481
487
|
console.log(`Pairing key file: ${info.credentialPath}`);
|
|
482
|
-
console.log(`Execution channel: ${app.rpc.mode || 'cold-resume'} · desktop
|
|
488
|
+
console.log(`Execution channel: ${app.rpc.mode || 'cold-resume'} · desktop chats support automatic hot attach.`);
|
|
483
489
|
const shutdown = async () => { await app.close(); process.exit(0); };
|
|
484
490
|
process.once('SIGINT', shutdown); process.once('SIGTERM', shutdown);
|
|
485
491
|
}
|