orbit-codex-bridge 0.1.0 → 0.1.1

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/public/index.html CHANGED
@@ -7,11 +7,30 @@
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"><span class="mini-badge">CLOUD WORKSPACE</span><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><h2 id="cloud-auth-title">登录 Orbit</h2><p id="cloud-auth-description">登录后,继续连接你的电脑和原有会话。</p><form id="cloud-login-form"><label for="cloud-username">用户名</label><input id="cloud-username" autocomplete="username" maxlength="64" autocapitalize="none" spellcheck="false" required><label for="cloud-password">密码</label><input id="cloud-password" type="password" autocomplete="current-password" maxlength="256" required><button class="primary full" type="submit">安全登录 <span>↗</span></button><p id="cloud-login-error" class="error" role="alert"></p></form><form id="cloud-register-form" hidden><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><small id="register-username-help" class="field-help">3–64 位,可用英文字母、数字、点、连字符和下划线。</small><label for="register-display-name">显示名称 <span class="optional">选填</span></label><input id="register-display-name" autocomplete="nickname" maxlength="120" placeholder="你希望我们如何称呼你"><label for="register-password">设置密码</label><input id="register-password" type="password" autocomplete="new-password" minlength="12" maxlength="256" placeholder="至少 12 个字符" required><label for="register-password-confirm">确认密码</label><input id="register-password-confirm" type="password" autocomplete="new-password" minlength="12" maxlength="256" required><p class="registration-note">免费账号可连接 1 台电脑。注册后即可添加电脑。</p><button class="primary full" type="submit">创建账号并继续 <span>↗</span></button><p id="cloud-register-error" class="error" role="alert"></p></form><p id="cloud-registration-notice" class="field-help" hidden>当前暂停自助注册,请联系管理员开通账号。</p><p class="cloud-admin-link"><a href="/admin">管理员入口</a></p></div></div>
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>授权完成后,终端会启动 Orbit 并显示手机配对二维码;用手机扫描完成首次连接。</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">电脑端 npm 安装包尚未发布;发布后这条命令即可直接使用。</p><p class="cloud-release-note">macOS / Linux:先安装 Node.js 24+,再运行 <code>npx --yes --package=orbit-codex-bridge orbit login</code>。</p></div></section>
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>授权完成后,终端会启动 Orbit 并显示手机配对二维码;用手机扫描完成首次连接。</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 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>
package/relay-agent.mjs CHANGED
@@ -14,6 +14,11 @@ export function createRelayAgent(options = {}) {
14
14
  const deviceId = options.deviceId || process.env.ORBIT_RELAY_DEVICE_ID || '';
15
15
  const onReady = options.onReady || (() => {});
16
16
  const onError = options.onError || (() => {});
17
+ const onState = options.onState || (() => {});
18
+ const heartbeatInterval = options.heartbeatInterval ?? 15000;
19
+ const heartbeatTimeout = options.heartbeatTimeout ?? 45000;
20
+ const reconnectDelay = options.reconnectDelay ?? 1000;
21
+ if (heartbeatInterval <= 0 || heartbeatTimeout <= heartbeatInterval || reconnectDelay < 0) throw new Error('连接心跳参数无效。');
17
22
  if (relay.username || relay.password || relay.search || relay.hash || relay.pathname !== '/agent'
18
23
  || !(relay.protocol === 'wss:' || relay.protocol === 'ws:' && isLoopback(relay))) {
19
24
  throw new Error('ORBIT_RELAY_URL 必须为 wss://域名/agent;仅本地测试允许 ws。');
@@ -27,6 +32,7 @@ export function createRelayAgent(options = {}) {
27
32
  let stopped = true;
28
33
  let reconnectTimer;
29
34
  let readyTimer;
35
+ let heartbeatTimer;
30
36
  let retry = 0;
31
37
  const bridges = new Map();
32
38
  const requests = new Set();
@@ -59,10 +65,12 @@ export function createRelayAgent(options = {}) {
59
65
  }
60
66
  };
61
67
  const headers = {};
62
- for (const name of ['accept','content-type','cookie','host','origin','user-agent']) {
68
+ for (const name of ['accept','content-type','cookie','user-agent']) {
63
69
  const value = message.headers?.[name];
64
70
  if (typeof value === 'string') headers[name] = value;
65
71
  }
72
+ headers.host = local.host;
73
+ headers.origin = local.origin;
66
74
  const request = transport.request(target,{method:message.method,headers,timeout:25000},response => {
67
75
  const chunks = [];
68
76
  let size = 0;
@@ -95,8 +103,8 @@ export function createRelayAgent(options = {}) {
95
103
  target.protocol = target.protocol === 'https:' ? 'wss:' : 'ws:';
96
104
  const localSocket = new WebSocket(target,{
97
105
  headers:{
98
- Cookie:message.headers?.cookie || '', Origin:message.headers?.origin || '',
99
- Host:message.headers?.host || local.host
106
+ Cookie:message.headers?.cookie || '', Origin:local.origin,
107
+ Host:local.host
100
108
  },
101
109
  maxPayload:maxBody,handshakeTimeout:8000
102
110
  });
@@ -122,8 +130,29 @@ export function createRelayAgent(options = {}) {
122
130
  if (stopped) return;
123
131
  const cloudSocket = new WebSocket(relay,{maxPayload:18*1024*1024,handshakeTimeout:10000});
124
132
  socket = cloudSocket;
133
+ onState({status:'connecting'});
125
134
  readyTimer = setTimeout(() => cloudSocket.terminate(),15000);
126
- cloudSocket.on('open',() => cloudSocket.send(JSON.stringify({type:'agent-auth',token,deviceId})));
135
+ let pendingPing = null;
136
+ let pingSentAt = 0;
137
+ cloudSocket.on('open',() => {
138
+ cloudSocket.send(JSON.stringify({type:'agent-auth',token,deviceId}));
139
+ heartbeatTimer = setInterval(() => {
140
+ if (socket !== cloudSocket || cloudSocket.readyState !== WebSocket.OPEN) return;
141
+ if (pendingPing !== null) {
142
+ if (Date.now() - pingSentAt >= heartbeatTimeout) {
143
+ onError(new Error('云端心跳超时,正在自动重连。'));
144
+ cloudSocket.terminate();
145
+ }
146
+ return;
147
+ }
148
+ pendingPing = `${Date.now()}`;
149
+ pingSentAt = Date.now();
150
+ cloudSocket.ping(pendingPing);
151
+ },heartbeatInterval);
152
+ });
153
+ cloudSocket.on('pong',data => {
154
+ if (data.toString() === pendingPing) pendingPing = null;
155
+ });
127
156
  cloudSocket.on('error',error => { if (!stopped) onError(error); });
128
157
  cloudSocket.on('message',raw => {
129
158
  if (socket !== cloudSocket) return;
@@ -131,7 +160,7 @@ export function createRelayAgent(options = {}) {
131
160
  try { message = JSON.parse(raw.toString()); } catch { cloudSocket.close(1003,'Invalid relay message'); return; }
132
161
  if (!message || typeof message !== 'object') return;
133
162
  if (message.type === 'agent-ready') {
134
- clearTimeout(readyTimer);retry = 0;onReady(message);return;
163
+ clearTimeout(readyTimer);retry = 0;onState({status:'online'});onReady(message);return;
135
164
  }
136
165
  if (message.type === 'http-request') return localRequest(message,cloudSocket);
137
166
  if (message.type === 'ws-open') return openLocalSocket(message,cloudSocket);
@@ -151,11 +180,19 @@ export function createRelayAgent(options = {}) {
151
180
  else bridge.socket.terminate();
152
181
  }
153
182
  });
154
- cloudSocket.on('close',() => {
183
+ cloudSocket.on('close',(code,reason) => {
155
184
  if (socket !== cloudSocket) return;
156
- socket = null;clearTimeout(readyTimer);clearLocalConnections();
185
+ socket = null;clearTimeout(readyTimer);clearInterval(heartbeatTimer);clearLocalConnections();
157
186
  if (!stopped) {
158
- const delay = Math.min(30000,1000 * 2 ** Math.min(5,retry++));
187
+ if (code === 1008 || code === 1012 && reason.toString() === 'Replaced by a newer agent') {
188
+ stopped = true;
189
+ const detail = code === 1008 ? '电脑凭证被拒绝,请检查账号或重新登录。' : '连接已由另一个 Orbit 实例接管。';
190
+ onState({status:'blocked',reason:detail});
191
+ onError(new Error(detail));
192
+ return;
193
+ }
194
+ const delay = Math.min(30000,reconnectDelay * 2 ** Math.min(5,retry++));
195
+ onState({status:'reconnecting',retryIn:delay,reason:reason.toString() || `连接断开 (${code})`});
159
196
  reconnectTimer = setTimeout(connect,delay);
160
197
  }
161
198
  });
@@ -164,7 +201,7 @@ export function createRelayAgent(options = {}) {
164
201
  return {
165
202
  start() { if (!stopped) return; stopped = false;connect(); },
166
203
  stop() {
167
- stopped = true;clearTimeout(reconnectTimer);clearTimeout(readyTimer);
204
+ stopped = true;clearTimeout(reconnectTimer);clearTimeout(readyTimer);clearInterval(heartbeatTimer);
168
205
  const previous = socket;socket = null;previous?.terminate();clearLocalConnections();
169
206
  }
170
207
  };
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<12||value.length>256||/[\u0000-\u001f\u007f]/.test(value))throw new Error('密码需要 12–256 个字符,不能包含控制字符。');
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: '0.1.0', device: os.hostname(), platform: process.platform, home,
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
- if (id && !store.row(id)) throw new Error('会话不存在。');
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 0.1.0 · ${info.url}`);
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 hot-attach is opt-in per conversation.`);
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
  }