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/public/app.js
CHANGED
|
@@ -25,18 +25,20 @@ const paths = {
|
|
|
25
25
|
const icon = name => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths[name] || paths.inbox}</svg>`;
|
|
26
26
|
function decorate(parent = document) { parent.querySelectorAll('[data-icon]').forEach(element => { element.innerHTML = icon(element.dataset.icon); }); }
|
|
27
27
|
function markdown(text) { return DOMPurify.sanitize(marked.parse(String(text || ''), { breaks: true }), { ALLOWED_TAGS: ['p','br','strong','b','em','i','s','del','ul','ol','li','blockquote','pre','code','h1','h2','h3','h4','h5','h6','hr','a','table','thead','tbody','tr','th','td','sup','sub','kbd','details','summary'], ALLOWED_ATTR: ['href','title','start','colspan','rowspan'], ALLOW_DATA_ATTR: false, ALLOW_ARIA_ATTR: false }); }
|
|
28
|
-
const state = { threads: [], status: null, selected: null, selectionVersion: 0, historyStatus: 'idle', historyError: '', filter: 'all', project: null, search: '', view: 'sessions', socket: null, pending: new Map(), enabled: new Map(), baseMessages: [], olderMessages: [], cursor: null, liveMessages: new Map(), desktopMessages: [], desktopRuntime: null, selectedSettings: null, models: [], contextUsage: null, settingsBusy: false, reopenModelAfterGrant: false, pendingModelChoice: null, cloudMode: false, cloudUser: null, cloudDevices: [], cloudDeviceId: null,
|
|
28
|
+
const state = { threads: [], status: null, selected: null, selectionVersion: 0, historyStatus: 'idle', historyError: '', filter: 'all', project: null, search: '', view: 'sessions', socket: null, pending: new Map(), enabled: new Map(), baseMessages: [], olderMessages: [], cursor: null, liveMessages: new Map(), desktopMessages: [], desktopRuntime: null, selectedSettings: null, models: [], contextUsage: null, settingsBusy: false, reopenModelAfterGrant: false, pendingModelChoice: null, cloudMode: false, cloudUser: null, cloudDevices: [], cloudDeviceId: null, approvals: new Map(), reconnect: 0, loggedOut: false, busy: false, turnId: null };
|
|
29
29
|
let refreshTimer;
|
|
30
30
|
let toastTimer;
|
|
31
31
|
let reconnectTimer;
|
|
32
|
+
let controlRenewTimer;
|
|
32
33
|
let desktopConnectionAttempt = 0;
|
|
33
34
|
let desktopConnecting = false;
|
|
34
35
|
let workspaceGeneration = 0;
|
|
35
36
|
let cloudRegistrationEnabled = false;
|
|
36
37
|
let cloudAuthVersion = 0;
|
|
38
|
+
let cloudConnecting = false;
|
|
37
39
|
const computerInstallCommands = {
|
|
38
|
-
windows:'$minimum = 24; $node = Get-Command node -ErrorAction SilentlyContinue; if (
|
|
39
|
-
unix:'npx --yes --package=orbit-codex-bridge orbit login'
|
|
40
|
+
windows:'$minimum = 24; $node = Get-Command node.exe -ErrorAction SilentlyContinue; $nodeMajor = if ($node) { [int](& $node.Source -p "parseInt(process.versions.node)") } else { 0 }; if ($nodeMajor -lt $minimum) { if (-not (Get-Command winget.exe -ErrorAction SilentlyContinue)) { throw "Node.js 24+ 未安装且找不到 winget;请先从 https://nodejs.org 安装 Node.js 24+。" }; winget.exe install --id OpenJS.NodeJS --exact --source winget --silent --accept-source-agreements --accept-package-agreements; if ($LASTEXITCODE -ne 0) { throw "Node.js 安装失败。" }; $env:Path = "$env:ProgramFiles\\nodejs;$env:Path"; $node = Get-Command node.exe -ErrorAction SilentlyContinue; $nodeMajor = if ($node) { [int](& $node.Source -p "parseInt(process.versions.node)") } else { 0 } }; if (-not $node -or $nodeMajor -lt $minimum) { throw "Node.js 24+ 安装未完成,请关闭并重新打开 PowerShell 后重试。" }; npx.cmd --yes --package=orbit-codex-bridge@latest orbit login',
|
|
41
|
+
unix:'npx --yes --package=orbit-codex-bridge@latest orbit login'
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
function setCloudAuthMode(register = false) {
|
|
@@ -58,7 +60,7 @@ function setCloudAuthMode(register = false) {
|
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
async function loadCloudConfig() {
|
|
61
|
-
const response = await fetch('/api/cloud/config', { cache:'no-store' });
|
|
63
|
+
const response = await fetch('/api/cloud/config', { cache:'no-store', signal:AbortSignal.timeout(15000) });
|
|
62
64
|
if (!response.ok) throw new Error('无法读取账号设置,请刷新页面重试。');
|
|
63
65
|
const config = await response.json();
|
|
64
66
|
cloudRegistrationEnabled = config.registrationEnabled === true;
|
|
@@ -67,10 +69,9 @@ async function loadCloudConfig() {
|
|
|
67
69
|
if (!cloudRegistrationEnabled) setCloudAuthMode(false);
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
function
|
|
72
|
+
function discardCloudPairCode() {
|
|
71
73
|
const fragment=new URLSearchParams(location.hash.slice(1));
|
|
72
74
|
if(!fragment.has('pair'))return;
|
|
73
|
-
state.cloudPairCode=fragment.get('pair');
|
|
74
75
|
history.replaceState(null,'',location.pathname+location.search);
|
|
75
76
|
}
|
|
76
77
|
|
|
@@ -92,23 +93,40 @@ async function api(url, body, signal) {
|
|
|
92
93
|
const generation = workspaceGeneration;
|
|
93
94
|
const headers = {};
|
|
94
95
|
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
95
|
-
if (state.cloudMode && state.cloudDeviceId && !url.startsWith('/api/cloud/')) headers['X-Orbit-Device-Id'] = state.cloudDeviceId;
|
|
96
|
+
if (state.cloudMode && state.cloudDeviceId && (!url.startsWith('/api/cloud/') || url === '/api/cloud/connect')) headers['X-Orbit-Device-Id'] = state.cloudDeviceId;
|
|
96
97
|
const options = body === undefined ? { headers } : { method: 'POST', headers, body: JSON.stringify(body) };
|
|
97
98
|
if (signal) options.signal = signal;
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
let response;
|
|
100
|
+
let result;
|
|
101
|
+
try {
|
|
102
|
+
response = await fetch(url, {...options, signal:signal || AbortSignal.timeout(35000)});
|
|
103
|
+
result = await response.json();
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (generation !== workspaceGeneration) throw Object.assign(new Error('工作台已切换。'), {code:'STALE_WORKSPACE'});
|
|
106
|
+
if (signal?.aborted) throw error;
|
|
107
|
+
throw new Error(error.name === 'TimeoutError' ? '连接超时,请检查电脑端和网络后重试。' : '无法连接 Orbit 服务,请检查网络后重试。');
|
|
108
|
+
}
|
|
100
109
|
if (generation !== workspaceGeneration) throw Object.assign(new Error('工作台已切换。'),{code:'STALE_WORKSPACE'});
|
|
101
|
-
if (response.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
const error = Object.assign(new Error(result.error || '请求失败。'), {status:response.status, code:result.code});
|
|
112
|
+
if (response.status === 401 && state.cloudMode && (result.code === 'cloud_login_required' || url.startsWith('/api/cloud/') && url !== '/api/cloud/login')) {
|
|
113
|
+
showCloudLogin(result.error);
|
|
114
|
+
error.handled = true;
|
|
115
|
+
} else if (response.status === 401 && !state.cloudMode && url !== '/api/pair' && !url.startsWith('/api/cloud/')) {
|
|
116
|
+
showPairing();
|
|
117
|
+
error.handled = true;
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
105
121
|
return result;
|
|
106
122
|
}
|
|
107
|
-
function showPairing() {
|
|
123
|
+
function showPairing(error = '') {
|
|
108
124
|
state.loggedOut = true; clearTimeout(reconnectTimer);
|
|
109
125
|
state.socket?.close();
|
|
110
126
|
select('#workspace').hidden = true; select('#pair-screen').hidden = false;
|
|
111
127
|
select('#cloud-login-screen').hidden = true; select('#cloud-device-screen').hidden = true;
|
|
128
|
+
select('#pair-error').textContent = error;
|
|
129
|
+
select('#pair-screen .pair-form > p').textContent = '输入电脑上的配对密钥。会话内容仅向已配对的设备开放。';
|
|
112
130
|
const fragment = new URLSearchParams(location.hash.slice(1));
|
|
113
131
|
if (fragment.has('pair')) { select('#pair-code').value = fragment.get('pair'); history.replaceState(null, '', location.pathname); }
|
|
114
132
|
}
|
|
@@ -130,21 +148,48 @@ function showCloudDevices(message = '') {
|
|
|
130
148
|
state.cloudDeviceId = null; localStorage.removeItem('orbit_cloud_device_id');
|
|
131
149
|
select('#workspace').hidden = true; select('#pair-screen').hidden = true; select('#cloud-login-screen').hidden = true;
|
|
132
150
|
select('#cloud-device-screen').hidden = false;
|
|
151
|
+
select('#cloud-device-message').setAttribute('role', 'status');
|
|
133
152
|
const quota = state.cloudUser?.maxDevices || 1;
|
|
134
153
|
select('#cloud-devices-title').textContent = state.cloudDevices.length ? '选择要连接的电脑' : '连接你的第一台电脑';
|
|
135
154
|
const greeting = `已登录 ${state.cloudUser?.displayName || state.cloudUser?.username}。`;
|
|
136
|
-
select('#cloud-device-message').textContent = state.
|
|
137
|
-
? `${greeting}选择发起连接的电脑,完成本机配对。${message ? ` ${message}` : ''}`
|
|
138
|
-
: message || greeting + (state.cloudDevices.length ? '选择一台电脑继续。' : '先添加你自己的电脑,连接后即可查看原有会话。');
|
|
155
|
+
select('#cloud-device-message').textContent = message || greeting + (state.cloudDevices.length ? '选择一台电脑继续,无需输入配对密钥。' : '先添加你自己的电脑,连接后即可查看原有会话。');
|
|
139
156
|
select('#cloud-device-quota').textContent = `套餐 ${state.cloudUser?.plan || 'free'} · 已添加 ${state.cloudDevices.length} / ${quota} 台电脑`;
|
|
140
157
|
select('#cloud-add-computer').disabled = state.cloudDevices.length >= quota;
|
|
141
158
|
select('#cloud-add-computer').title = state.cloudDevices.length >= quota ? '已达到设备上限,可移除旧电脑或联系管理员调整额度。' : '';
|
|
142
159
|
select('#cloud-onboarding').hidden = state.cloudDevices.length > 0;
|
|
143
160
|
select('#cloud-devices-list').innerHTML = state.cloudDevices.map(device =>
|
|
144
|
-
`<article class="cloud-computer-card"><button class="device-card cloud-device-option" data-cloud-device="${escapeHtml(device.id)}" ${device.status !== 'active'
|
|
161
|
+
`<article class="cloud-computer-card"><button type="button" class="device-card cloud-device-option" data-cloud-device="${escapeHtml(device.id)}" ${device.status !== 'active' ? 'disabled' : ''}><div class="device-big-icon">${icon('monitor')}</div><h2>${escapeHtml(device.name)}<span class="capability-state ${device.online ? '' : 'pending'}">${device.online ? '在线 · 点击连接' : '电脑离线 · 点击检查'}</span></h2><p>${device.lastSeen ? `最近在线 · ${ago(device.lastSeen)}` : '请在电脑终端运行 orbit login,启动后台连接器。'}</p></button><footer><span>${device.online ? '仅你的账号可连接' : '点击可重新检查上线状态'}</span><button type="button" class="button small" data-cloud-revoke="${escapeHtml(device.id)}">移除电脑</button></footer></article>`
|
|
145
162
|
).join('');
|
|
146
163
|
}
|
|
147
164
|
|
|
165
|
+
async function connectCloudDevice(deviceId) {
|
|
166
|
+
if (cloudConnecting) return;
|
|
167
|
+
cloudConnecting = true;
|
|
168
|
+
const button = select(`[data-cloud-device="${CSS.escape(deviceId)}"]`);
|
|
169
|
+
if (button) {
|
|
170
|
+
button.disabled = true;
|
|
171
|
+
button.setAttribute('aria-busy', 'true');
|
|
172
|
+
button.querySelector('.capability-state').textContent = '正在连接…';
|
|
173
|
+
}
|
|
174
|
+
select('#cloud-device-message').textContent = '正在检查电脑连接,请稍候…';
|
|
175
|
+
try {
|
|
176
|
+
const profile = await api('/api/cloud/me');
|
|
177
|
+
state.cloudUser = profile.user;
|
|
178
|
+
state.cloudDevices = profile.devices || [];
|
|
179
|
+
const device = state.cloudDevices.find(item => item.id === deviceId && item.status === 'active');
|
|
180
|
+
if (!device) { showCloudDevices('这台电脑已被移除或不属于当前账号,请重新选择。'); return; }
|
|
181
|
+
if (!device.online) { showCloudDevices(`电脑“${device.name}”尚未上线。请确认电脑已联网,在电脑终端运行 orbit login,后台启动后再点击连接。`); return; }
|
|
182
|
+
state.cloudDeviceId = deviceId;
|
|
183
|
+
localStorage.setItem('orbit_cloud_device_id', deviceId);
|
|
184
|
+
await boot();
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (!error.handled && error.code !== 'STALE_WORKSPACE') showCloudDevices(error.message);
|
|
187
|
+
} finally {
|
|
188
|
+
cloudConnecting = false;
|
|
189
|
+
if (button?.isConnected) { button.disabled = false; button.removeAttribute('aria-busy'); }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
148
193
|
async function refreshCloudDevices() {
|
|
149
194
|
const workspace = await api('/api/cloud/me');
|
|
150
195
|
state.cloudUser = workspace.user;
|
|
@@ -153,7 +198,7 @@ async function refreshCloudDevices() {
|
|
|
153
198
|
}
|
|
154
199
|
|
|
155
200
|
function resetCloudWorkspace() {
|
|
156
|
-
state.loggedOut = true; clearTimeout(reconnectTimer); clearTimeout(refreshTimer); refreshTimer = null;
|
|
201
|
+
state.loggedOut = true; clearTimeout(reconnectTimer); clearTimeout(refreshTimer); clearTimeout(controlRenewTimer); refreshTimer = null;
|
|
157
202
|
state.selectionVersion++;
|
|
158
203
|
state.socket?.close();
|
|
159
204
|
state.socket = null;
|
|
@@ -173,11 +218,11 @@ function resetCloudWorkspace() {
|
|
|
173
218
|
async function discoverCloudWorkspace() {
|
|
174
219
|
const generation = workspaceGeneration;
|
|
175
220
|
let response;
|
|
176
|
-
try { response = await fetch('/api/cloud/me', { cache:'no-store' }); }
|
|
221
|
+
try { response = await fetch('/api/cloud/me', { cache:'no-store', signal:AbortSignal.timeout(15000) }); }
|
|
177
222
|
catch (error) { throw new Error('无法连接工作台服务,请检查网络。'); }
|
|
178
223
|
if (generation !== workspaceGeneration) return 'stale';
|
|
179
224
|
if (response.status === 404) return 'local';
|
|
180
|
-
|
|
225
|
+
discardCloudPairCode();
|
|
181
226
|
let configError = '';
|
|
182
227
|
try { await loadCloudConfig(); }
|
|
183
228
|
catch (error) { configError = error.message; }
|
|
@@ -193,7 +238,7 @@ async function discoverCloudWorkspace() {
|
|
|
193
238
|
state.cloudUser = workspace.user; state.cloudDevices = workspace.devices || [];
|
|
194
239
|
const saved = localStorage.getItem('orbit_cloud_device_id');
|
|
195
240
|
if (saved && state.cloudDevices.some(device => device.id === saved && device.status === 'active')) state.cloudDeviceId = saved;
|
|
196
|
-
else if (state.cloudDevices.length === 1 && state.cloudDevices[0].status === 'active'
|
|
241
|
+
else if (state.cloudDevices.length === 1 && state.cloudDevices[0].status === 'active') {
|
|
197
242
|
state.cloudDeviceId = state.cloudDevices[0].id;
|
|
198
243
|
localStorage.setItem('orbit_cloud_device_id',state.cloudDeviceId);
|
|
199
244
|
} else {
|
|
@@ -217,6 +262,13 @@ async function boot() {
|
|
|
217
262
|
try {
|
|
218
263
|
const cloudState = await discoverCloudWorkspace();
|
|
219
264
|
if (cloudState !== 'local' && cloudState !== 'ready') return;
|
|
265
|
+
if (cloudState === 'ready') {
|
|
266
|
+
try { await api('/api/cloud/connect',{}); }
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (error.status === 404) throw new Error('云端尚未支持账号直接连接,请先更新云端 Orbit 服务到 0.1.2 或更高版本。');
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
220
272
|
state.status = await api('/api/status');
|
|
221
273
|
if (generation !== workspaceGeneration) return;
|
|
222
274
|
state.loggedOut = false;
|
|
@@ -227,7 +279,7 @@ async function boot() {
|
|
|
227
279
|
state.enabled = new Map(Object.entries(state.status.execution.grants || {}));
|
|
228
280
|
await refreshThreads(); connectSocket();
|
|
229
281
|
} catch (error) {
|
|
230
|
-
if (error.code === 'STALE_WORKSPACE') return;
|
|
282
|
+
if (error.code === 'STALE_WORKSPACE' || error.handled) return;
|
|
231
283
|
if (state.cloudMode) showCloudDevices(error.message);
|
|
232
284
|
else if (!state.loggedOut) { showPairing(); select('#pair-error').textContent = error.message; }
|
|
233
285
|
}
|
|
@@ -334,6 +386,7 @@ function scheduleReconnect() {
|
|
|
334
386
|
clearTimeout(reconnectTimer);
|
|
335
387
|
reconnectTimer = setTimeout(async () => {
|
|
336
388
|
try {
|
|
389
|
+
if (state.cloudMode) await api('/api/cloud/connect',{});
|
|
337
390
|
state.status = await api('/api/status');
|
|
338
391
|
state.enabled = new Map(Object.entries(state.status.execution.grants || {}));
|
|
339
392
|
connectSocket();
|
|
@@ -341,11 +394,20 @@ function scheduleReconnect() {
|
|
|
341
394
|
}, Math.min(15000, 1000 * 2 ** Math.min(4, state.reconnect++)));
|
|
342
395
|
}
|
|
343
396
|
async function subscribeThread(id) {
|
|
397
|
+
clearTimeout(controlRenewTimer);
|
|
344
398
|
const selectionVersion = state.selectionVersion;
|
|
345
399
|
const attempt = ++desktopConnectionAttempt;
|
|
346
400
|
desktopConnecting = true; updateMode();
|
|
347
401
|
try {
|
|
348
|
-
const
|
|
402
|
+
const desktop = state.status?.execution.mode === 'desktop';
|
|
403
|
+
const eligible = desktop && state.selected?.id === id && !state.selected.archived && !String(state.selected.source).startsWith('{');
|
|
404
|
+
let result = await rpc('thread.subscribe', { threadId:id, autoControl:eligible });
|
|
405
|
+
if (id !== state.selected?.id || selectionVersion !== state.selectionVersion || attempt !== desktopConnectionAttempt) return;
|
|
406
|
+
if (eligible && result.runtime?.attached && !result.control) {
|
|
407
|
+
await rpc('thread.enable', {threadId:id,confirm:true});
|
|
408
|
+
if (id !== state.selected?.id || selectionVersion !== state.selectionVersion || attempt !== desktopConnectionAttempt) return;
|
|
409
|
+
result = await rpc('thread.subscribe', {threadId:id});
|
|
410
|
+
}
|
|
349
411
|
if (id !== state.selected?.id || selectionVersion !== state.selectionVersion || attempt !== desktopConnectionAttempt) return;
|
|
350
412
|
if (result.control) state.enabled.set(id, result.control); else state.enabled.delete(id);
|
|
351
413
|
state.desktopRuntime = result.runtime || null;
|
|
@@ -354,6 +416,13 @@ async function subscribeThread(id) {
|
|
|
354
416
|
state.turnId = result.turnId;
|
|
355
417
|
state.busy = Boolean(result.turnId);
|
|
356
418
|
state.approvals = new Map((result.approvals || []).map(approval => [approval.requestId, approval]));
|
|
419
|
+
if (desktop && result.control) {
|
|
420
|
+
controlRenewTimer = setTimeout(() => {
|
|
421
|
+
if (state.selected?.id === id && state.socket?.readyState === WebSocket.OPEN && state.desktopRuntime?.attached) {
|
|
422
|
+
subscribeThread(id).catch(error => toast(error.message));
|
|
423
|
+
}
|
|
424
|
+
},Math.max(1000,result.control.expires - Date.now() - 120000));
|
|
425
|
+
}
|
|
357
426
|
renderMessages(); renderApprovals();
|
|
358
427
|
} catch (error) {
|
|
359
428
|
if (id === state.selected?.id && selectionVersion === state.selectionVersion && attempt === desktopConnectionAttempt) {
|
|
@@ -650,14 +719,14 @@ function updateMode() {
|
|
|
650
719
|
const attached = desktopConnected && state.desktopRuntime?.attached === true;
|
|
651
720
|
const blocked = thread.archived || (desktop ? !attached || desktopConnecting : thread.locked && !control && state.status?.execution.mode !== 'endpoint');
|
|
652
721
|
select('#enable-control').disabled = Boolean(blocked || control);
|
|
653
|
-
select('#enable-control').textContent = control ? desktop ? '
|
|
722
|
+
select('#enable-control').textContent = control ? desktop ? '热接管中' : '接续已授权' : thread.archived ? '归档只读' : desktopConnecting && desktop ? '连接中…' : desktop && !desktopConnected ? '桌面未连接' : desktop && !attached ? '原会话未连接' : blocked ? '原客户端占用' : desktop ? '重新接管' : '接续会话';
|
|
654
723
|
select('#mode-banner').classList.toggle('controlled', Boolean(control));
|
|
655
724
|
const banner = desktop
|
|
656
725
|
? desktopConnecting ? '正在连接这条原会话;历史仍可查看。'
|
|
657
726
|
: !desktopConnected ? 'Codex 桌面连接已断开。请保持桌面应用运行,再点击“重新连接”。'
|
|
658
727
|
: !attached ? state.desktopRuntime?.reason || '这条原会话尚未连接。请先在 Codex 桌面打开它,再点击“重新连接”。'
|
|
659
|
-
: control ? '
|
|
660
|
-
: '这条原会话已连接 ·
|
|
728
|
+
: control ? '热接管中 · 与电脑共用同一 Codex 执行实例;消息、运行状态和审批实时同步。'
|
|
729
|
+
: '这条原会话已连接 · 点击“重新接管”恢复控制。'
|
|
661
730
|
: control ? `原 ID 接续 · ${control.writable ? '工作区可写 / 操作需审批' : '只读执行沙箱'} · ${control.sharedExecution ? '目标服务已加载此会话' : '独立执行实例'}`
|
|
662
731
|
: thread.locked ? '原客户端持有此会话:独立接续不会抢占桌面执行。' : '只读镜像:查看原生历史不会启动执行实例。';
|
|
663
732
|
const modeBanner = select('#mode-banner');
|
|
@@ -672,7 +741,7 @@ function updateMode() {
|
|
|
672
741
|
select('#send-message').disabled = !control || !connected || desktop && (!attached || desktopConnecting) || waitingForTurn || state.settingsBusy || !select('#message-input').value.trim();
|
|
673
742
|
select('#send-message').title = state.busy && desktop ? '追加指令到当前桌面任务' : '沿用原会话发送';
|
|
674
743
|
select('#stop-turn').hidden = !state.busy || !state.turnId || !control;
|
|
675
|
-
select('#message-input').placeholder = state.busy && desktop ? '追加指令给电脑上正在运行的任务…' : control ? '沿着原来的上下文继续…' : '先确认接续方式,再沿着这条会话继续…';
|
|
744
|
+
select('#message-input').placeholder = state.busy && desktop ? '追加指令给电脑上正在运行的任务…' : control ? '沿着原来的上下文继续…' : desktop ? '连接原桌面会话后继续…' : '先确认接续方式,再沿着这条会话继续…';
|
|
676
745
|
select('#composer-note').textContent = control ? desktop ? '消息进入原 Codex 桌面会话;任务不会迁移或重启。断线不会自动重发。' : '原会话 ID 不变;断线不会自动重发消息。' : '只读查看不会唤醒或打断电脑上的执行。';
|
|
677
746
|
renderModelStatus();
|
|
678
747
|
}
|
|
@@ -693,12 +762,18 @@ function renderDevices() {
|
|
|
693
762
|
cloudRow.querySelector('span:last-child').classList.toggle('pending', !state.cloudMode);
|
|
694
763
|
select('#copy-url').onclick = async () => { try { await navigator.clipboard.writeText(url); toast('连接地址已复制。'); } catch { toast('浏览器不允许复制,请手动复制地址。'); } };
|
|
695
764
|
select('#show-qr').onclick = async () => {
|
|
765
|
+
if (state.cloudMode) return;
|
|
696
766
|
try {
|
|
697
767
|
const data = await api('/api/pair/qr', { url });
|
|
698
768
|
select('#qr-container').innerHTML = `<img src="${escapeHtml(data.image)}" alt="敏感的设备配对二维码"><p>${escapeHtml(data.warning)}</p>`;
|
|
699
769
|
select('#qr-container').hidden = false;
|
|
700
770
|
} catch (error) { toast(error.message); }
|
|
701
771
|
};
|
|
772
|
+
if (state.cloudMode) {
|
|
773
|
+
const card = select('#show-qr').closest('section');
|
|
774
|
+
card.querySelector('p').textContent = '手机打开工作台,登录同一 Orbit 账号并选择这台电脑,即可继续原有会话,无需扫码或输入密钥。';
|
|
775
|
+
select('#show-qr').hidden = true;
|
|
776
|
+
}
|
|
702
777
|
}
|
|
703
778
|
|
|
704
779
|
function closeNav() { select('#workspace').classList.remove('nav-open'); select('#scrim').hidden = true; }
|
|
@@ -805,27 +880,13 @@ document.addEventListener('click', async event => {
|
|
|
805
880
|
try {
|
|
806
881
|
await api(`/api/cloud/devices/${encodeURIComponent(device.id)}/revoke`, {});
|
|
807
882
|
await refreshCloudDevices();
|
|
808
|
-
toast('
|
|
883
|
+
toast('电脑已移除。原电脑运行 orbit rebind 可重新绑定。');
|
|
809
884
|
} catch (error) { toast(error.message); revoke.disabled = false; }
|
|
810
885
|
return;
|
|
811
886
|
}
|
|
812
887
|
const cloudDevice=event.target.closest('[data-cloud-device]');
|
|
813
888
|
if(cloudDevice){
|
|
814
|
-
|
|
815
|
-
localStorage.setItem('orbit_cloud_device_id',state.cloudDeviceId);
|
|
816
|
-
if(state.cloudPairCode){
|
|
817
|
-
cloudDevice.disabled=true;
|
|
818
|
-
try{
|
|
819
|
-
await api('/api/pair',{code:state.cloudPairCode});
|
|
820
|
-
state.cloudPairCode=null;
|
|
821
|
-
showCloudDevices();
|
|
822
|
-
await boot();
|
|
823
|
-
}catch(error){
|
|
824
|
-
showCloudDevices(error.message);
|
|
825
|
-
}
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
await boot();return;
|
|
889
|
+
await connectCloudDevice(cloudDevice.dataset.cloudDevice);return;
|
|
829
890
|
}
|
|
830
891
|
if(event.target.closest('#cloud-logout-picker')){
|
|
831
892
|
try{await api('/api/cloud/logout',{});}catch{}
|
|
@@ -838,7 +899,7 @@ document.addEventListener('click', async event => {
|
|
|
838
899
|
if (!id || select('#reconnect-desktop').disabled) return;
|
|
839
900
|
try {
|
|
840
901
|
await subscribeThread(id);
|
|
841
|
-
if (id === state.selected?.id) toast(state.
|
|
902
|
+
if (id === state.selected?.id) toast(state.enabled.has(id) ? '已连接并接管这条原会话。' : state.desktopRuntime?.reason || '原会话尚未连接。');
|
|
842
903
|
} catch (error) { toast(error.message); }
|
|
843
904
|
return;
|
|
844
905
|
}
|
|
@@ -901,8 +962,18 @@ select('#logout').onclick = async () => {
|
|
|
901
962
|
}
|
|
902
963
|
} catch(error){toast(error.message)}
|
|
903
964
|
};
|
|
904
|
-
select('#enable-control').onclick = () => {
|
|
965
|
+
select('#enable-control').onclick = async () => {
|
|
905
966
|
const desktop = state.status.execution.mode === 'desktop';
|
|
967
|
+
if (desktop) {
|
|
968
|
+
try {
|
|
969
|
+
await subscribeThread(state.selected.id);
|
|
970
|
+
if (state.reopenModelAfterGrant && state.enabled.has(state.selected?.id)) {
|
|
971
|
+
state.reopenModelAfterGrant = false;
|
|
972
|
+
await openModelSettings();
|
|
973
|
+
}
|
|
974
|
+
} catch (error) { toast(error.message); }
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
906
977
|
select('#control-form').reset(); select('#control-error').textContent = '';
|
|
907
978
|
select('#control-title').textContent = desktop ? '授权热接管' : '沿用原会话';
|
|
908
979
|
select('#control-description').textContent = desktop ? '不创建新会话、不迁移执行器。Orbit 将订阅并控制 Codex 桌面当前加载的原会话。' : '保持原会话 ID,使用独立执行通道继续。原客户端占用时不会强行抢锁。';
|
package/public/connect.html
CHANGED
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
</form>
|
|
26
26
|
<form id="connect-register-form" hidden>
|
|
27
27
|
<label for="connect-register-username">用户名</label><input id="connect-register-username" autocomplete="username" minlength="3" maxlength="64" autocapitalize="none" spellcheck="false" required>
|
|
28
|
-
<label for="connect-register-password">设置密码</label><input id="connect-register-password" type="password" autocomplete="new-password" minlength="
|
|
29
|
-
<label for="connect-register-confirm">确认密码</label><input id="connect-register-confirm" type="password" autocomplete="new-password" minlength="
|
|
28
|
+
<label for="connect-register-password">设置密码</label><input id="connect-register-password" type="password" autocomplete="new-password" minlength="6" maxlength="256" placeholder="至少 6 个字符" required>
|
|
29
|
+
<label for="connect-register-confirm">确认密码</label><input id="connect-register-confirm" type="password" autocomplete="new-password" minlength="6" maxlength="256" required>
|
|
30
30
|
<button class="primary full" type="submit">创建账号并确认 <span>↗</span></button>
|
|
31
31
|
<p id="connect-register-error" class="error" role="alert"></p>
|
|
32
32
|
</form>
|
package/public/index.html
CHANGED
|
@@ -7,15 +7,34 @@
|
|
|
7
7
|
<body>
|
|
8
8
|
<section id="cloud-login-screen" class="pair-screen" hidden>
|
|
9
9
|
<div class="pair-art"><a class="brand light" href="/"><img src="/icon.svg" alt=""><span>orbit<span class="brand-dot">.</span></span><small>SECURE CLOUD</small></a><div class="pair-copy"><span class="eyebrow">YOUR DEVICES. YOUR SESSION.</span><h1>连接你的<br>个人工作空间。</h1><p>通过账号连接云端工作台。会话仍留在你的电脑上。</p></div><span class="art-caption">DEVICE-BOUND / LOCAL-FIRST</span></div>
|
|
10
|
-
<div class="pair-form-wrap"><div class="pair-form"
|
|
10
|
+
<div class="pair-form-wrap"><div class="pair-form">
|
|
11
|
+
<span class="mini-badge">CLOUD WORKSPACE</span>
|
|
12
|
+
<div id="cloud-auth-tabs" class="cloud-auth-tabs" role="group" aria-label="账号入口" hidden><button id="cloud-show-login" class="active" type="button" aria-pressed="true">登录</button><button id="cloud-show-register" type="button" aria-pressed="false">创建账号</button></div>
|
|
13
|
+
<h2 id="cloud-auth-title">登录 Orbit</h2><p id="cloud-auth-description">登录后,继续连接你的电脑和原有会话。</p>
|
|
14
|
+
<form id="cloud-login-form">
|
|
15
|
+
<label for="cloud-username">用户名</label><input id="cloud-username" autocomplete="username" maxlength="64" autocapitalize="none" spellcheck="false" required>
|
|
16
|
+
<label for="cloud-password">密码</label><input id="cloud-password" type="password" autocomplete="current-password" maxlength="256" required>
|
|
17
|
+
<button class="primary full" type="submit">安全登录 <span>↗</span></button><p id="cloud-login-error" class="error" role="alert"></p>
|
|
18
|
+
</form>
|
|
19
|
+
<form id="cloud-register-form" hidden>
|
|
20
|
+
<label for="register-username">用户名</label><input id="register-username" autocomplete="username" minlength="3" maxlength="64" autocapitalize="none" spellcheck="false" aria-describedby="register-username-help" required>
|
|
21
|
+
<small id="register-username-help" class="field-help">3–64 位,可用英文字母、数字、点、连字符和下划线。</small>
|
|
22
|
+
<label for="register-display-name">显示名称 <span class="optional">选填</span></label><input id="register-display-name" autocomplete="nickname" maxlength="120" placeholder="你希望我们如何称呼你">
|
|
23
|
+
<label for="register-password">设置密码</label><input id="register-password" type="password" autocomplete="new-password" minlength="6" maxlength="256" placeholder="至少 6 个字符" required>
|
|
24
|
+
<label for="register-password-confirm">确认密码</label><input id="register-password-confirm" type="password" autocomplete="new-password" minlength="6" maxlength="256" required>
|
|
25
|
+
<p class="registration-note">免费账号可连接 1 台电脑。注册后即可添加电脑。</p>
|
|
26
|
+
<button class="primary full" type="submit">创建账号并继续 <span>↗</span></button><p id="cloud-register-error" class="error" role="alert"></p>
|
|
27
|
+
</form>
|
|
28
|
+
<p id="cloud-registration-notice" class="field-help" hidden>当前暂停自助注册,请联系管理员开通账号。</p><p class="cloud-admin-link"><a href="/admin">管理员入口</a></p>
|
|
29
|
+
</div></div>
|
|
11
30
|
</section>
|
|
12
31
|
<section id="cloud-device-screen" class="devices-view cloud-device-screen" hidden>
|
|
13
32
|
<header class="cloud-device-header"><div><span class="eyebrow">YOUR CONNECTED COMPUTERS</span><h1 id="cloud-devices-title">选择要连接的电脑</h1><p id="cloud-device-message">只显示绑定到你账号的设备。</p><p id="cloud-device-quota"></p></div><div class="cloud-device-actions"><button id="cloud-refresh-devices" class="button small" type="button">刷新状态</button><button id="cloud-add-computer" class="primary" type="button">添加电脑</button><button id="cloud-logout-picker" class="button small" type="button">退出登录</button></div></header>
|
|
14
|
-
<section id="cloud-onboarding" class="cloud-onboarding" hidden><div><span class="mini-badge">FIRST CONNECTION</span><h2>一条命令,连接你的电脑</h2><p>电脑需要运行 Codex 桌面应用。Windows 用户复制下面命令到 PowerShell 运行。</p><ol><li>命令会检查 Node.js 24+,缺少时通过 winget 安装。</li><li>浏览器打开后登录此 Orbit 账号,确认添加这台电脑。</li><li
|
|
33
|
+
<section id="cloud-onboarding" class="cloud-onboarding" hidden><div><span class="mini-badge">FIRST CONNECTION</span><h2>一条命令,连接你的电脑</h2><p>电脑需要运行 Codex 桌面应用。Windows 用户复制下面命令到 PowerShell 运行。</p><ol><li>命令会检查 Node.js 24+,缺少时通过 winget 安装。</li><li>浏览器打开后登录此 Orbit 账号,确认添加这台电脑。</li><li>手机登录同一账号,选择已上线的电脑即可使用,无需扫码或输入配对密钥。</li></ol><div class="cloud-command-box"><pre class="cloud-install-command"><code id="cloud-install-command"></code></pre><button id="cloud-copy-install" class="button small" type="button">复制命令</button></div><p id="cloud-install-status" class="cloud-install-status" role="status"></p><p class="cloud-release-note">如果电脑没有 winget,请先从 nodejs.org 安装 Node.js 24+。winget 可能会请求系统安装授权。</p><p class="cloud-release-note">macOS / Linux:先安装 Node.js 24+,再运行 <code>npx --yes --package=orbit-codex-bridge@latest orbit login</code>。</p></div></section>
|
|
15
34
|
<div id="cloud-devices-list" class="device-grid cloud-device-list"></div></section>
|
|
16
35
|
<section id="pair-screen" class="pair-screen" hidden>
|
|
17
36
|
<div class="pair-art"><a class="brand light" href="/"><img src="/icon.svg" alt=""><span>orbit<span class="brand-dot">.</span></span></a><div class="orbital-art"><div class="orbit-ring ring-one"></div><div class="orbit-ring ring-two"></div><div class="orbit-core">o</div><span class="orbit-point"></span></div><div class="pair-copy"><span class="eyebrow">YOUR WORK. IN YOUR ORBIT.</span><h1>会话留在电脑,<br>灵感随你出发。</h1><p>不复制上下文,不从头再来。<br>从熟悉的项目,找到原来的对话。</p></div><span class="art-caption">LOCAL FIRST / BUILT FOR CONTINUITY</span></div>
|
|
18
|
-
<div class="pair-form-wrap"><div class="pair-form"><span class="mini-badge">PRIVATE WORKSPACE</span><h2>连接你的工作台</h2><p>输入电脑上的配对密钥。会话内容仅向已配对的设备开放。</p><form id="pair-form"><label for="pair-code">设备配对密钥</label><input id="pair-code" type="password" placeholder="粘贴本机配对密钥" autocomplete="off" required><button class="primary full" type="submit">安全连接 <span>↗</span></button><p id="pair-error" class="error" role="alert"></p></form><div class="pair-help"><span data-icon="shield"></span><div><strong
|
|
37
|
+
<div class="pair-form-wrap"><div class="pair-form"><span class="mini-badge">PRIVATE WORKSPACE</span><h2>连接你的工作台</h2><p>输入电脑上的配对密钥。会话内容仅向已配对的设备开放。</p><form id="pair-form"><label for="pair-code">设备配对密钥</label><input id="pair-code" type="password" placeholder="粘贴本机配对密钥" autocomplete="off" required><button class="primary full" type="submit">安全连接 <span>↗</span></button><p id="pair-error" class="error" role="alert"></p></form><div class="pair-help"><span data-icon="shield"></span><div><strong>原会话始终留在电脑</strong><p>选择原会话后,消息和操作会发送到这台电脑上正在运行的 Codex。</p></div></div><details><summary>在哪里找到密钥?</summary><p>在 Orbit 目录运行 <code>Get-Content .runtime\pairing-key</code>。仅用于可信设备;它不是模型 API Key。</p></details></div><footer>ORBIT <span>原生会话 · 电脑执行</span></footer></div>
|
|
19
38
|
</section>
|
|
20
39
|
<div id="workspace" class="workspace" hidden>
|
|
21
40
|
<button id="scrim" class="scrim" aria-label="收起导航" hidden></button>
|