orbit-codex-bridge 0.1.2 → 0.1.6
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 +11 -2
- package/lib/desktop.mjs +55 -8
- package/lib/resume-manager.mjs +177 -0
- package/lib/rpc.mjs +28 -2
- package/package.json +2 -2
- package/public/app.js +304 -28
- package/public/goal.css +7 -0
- package/public/index.html +4 -1
- package/public/style.css +30 -0
- package/relay-agent.mjs +4 -2
- package/relay-server.mjs +15 -4
- package/server.mjs +380 -25
package/README.md
CHANGED
|
@@ -30,9 +30,18 @@ orbit login
|
|
|
30
30
|
1. 命令会打开电脑授权页面,登录或注册 Orbit 账号,确认添加这台电脑。
|
|
31
31
|
2. 手机打开 [orbit.cubesugar.tech](https://orbit.cubesugar.tech),登录同一账号。
|
|
32
32
|
3. 登录后选择这台电脑;账号会自动完成安全配对,无需扫码或输入本机密钥。
|
|
33
|
-
4.
|
|
33
|
+
4. 选择任意普通原有会话即可连接。Orbit 优先接入 Codex 桌面当前执行者;桌面未加载时尝试由 Codex App Server 按原会话 ID 接续,不会新建对话。若该会话仍被其他执行者占用,会明确提示并拒绝重复写入。
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
授权信息保存在电脑本地,下次启动会自动复用。连接器默认在当前用户的后台运行,可以关闭安装终端;断网后自动重连,工作进程异常退出后自动恢复。电脑需要保持开机、联网。桌面热接管需 Codex 桌面运行;若会话未由桌面持有,则尝试通过 Codex App Server 按原 ID 接续。电脑重启后运行 `orbit start` 恢复连接。
|
|
36
|
+
|
|
37
|
+
## 会话输入
|
|
38
|
+
|
|
39
|
+
- `普通`:按原会话的 Codex 模型、权限和设置继续。
|
|
40
|
+
- `计划`:通过 Codex App Server `turn/start.collaborationMode` 或桌面 IPC 使用原生 Plan 协作模式;不会要求切回电脑,也不会改动正在运行的轮次。
|
|
41
|
+
- `目标`:通过 Codex 原生 `thread/goal/*` API 为原会话设置持久 Goal,支持预算、暂停、继续和清除;桌面 owner 模式可查看目标状态,但当前 IPC 不公开 Goal 写操作,因此需在 Codex 桌面管理。
|
|
42
|
+
- `添加文件`:每次最多 5 个、每个最多 8 MiB。文件经账号设备中继传到本机,并暂存在项目内 `.orbit-attachments`;Codex 读取原路径。待发送文件 30 分钟清理,已发送文件保留最多 24 小时,任务完成后会提前删除。
|
|
43
|
+
- `电脑文件`:浏览当前会话项目并下载文件;限制在项目根目录内,排除 `.git`、`.codex`、密钥/环境文件和符号链接,单文件最大 8 MiB。
|
|
44
|
+
- 文件附件仅支持已登录账号、在线的自有电脑,以及已加载并已自动热接管的 Codex 桌面原会话。归档会话、子代理和独立续接不支持。
|
|
36
45
|
|
|
37
46
|
## 常用命令
|
|
38
47
|
|
package/lib/desktop.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import net from 'node:net';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
3
5
|
import { EventEmitter } from 'node:events';
|
|
4
6
|
import { messageFromItem } from './store.mjs';
|
|
5
7
|
|
|
@@ -231,7 +233,16 @@ export class DesktopRpc extends EventEmitter {
|
|
|
231
233
|
}
|
|
232
234
|
if (this.attaching.has(id)) return this.attaching.get(id);
|
|
233
235
|
const operation = (async () => {
|
|
234
|
-
|
|
236
|
+
let owner=null;
|
|
237
|
+
for(let attempt=0;attempt<2;attempt++){
|
|
238
|
+
try{
|
|
239
|
+
owner=await this.request('thread-owner-discovery',{conversationId:id,hostId:'local'});
|
|
240
|
+
}catch(error){
|
|
241
|
+
if(error.code!=='DESKTOP_THREAD_UNAVAILABLE'||attempt===1)throw error;
|
|
242
|
+
}
|
|
243
|
+
if(owner?.ownerClientId)break;
|
|
244
|
+
if(attempt===0)await new Promise(resolve=>setTimeout(resolve,180));
|
|
245
|
+
}
|
|
235
246
|
if (!owner.ownerClientId) throw new DesktopUnavailableError('这条原会话尚未在 Codex 桌面加载。请在电脑上打开它,再点击“重新连接”。');
|
|
236
247
|
const result = await this.request('thread-follower-read-model-settings', { conversationId: id }, owner.ownerClientId);
|
|
237
248
|
if (result.settings?.resumeState !== 'resumed') throw new Error('桌面会话尚未就绪,请先在 Codex 中打开。');
|
|
@@ -261,6 +272,13 @@ export class DesktopRpc extends EventEmitter {
|
|
|
261
272
|
permissionMode: state?.currentPermissions?.sandboxPolicy?.type || state?.latestThreadSettings?.sandboxPolicy?.type || 'desktop-inherited',
|
|
262
273
|
model: state?.latestThreadSettings?.model || state?.latestModel || null,
|
|
263
274
|
effort: state?.latestThreadSettings?.effort ?? state?.latestReasoningEffort ?? null,
|
|
275
|
+
collaborationMode:state?.latestCollaborationMode?.mode||'default',
|
|
276
|
+
goal:state?.threadGoal?{
|
|
277
|
+
objective:state.threadGoal.objective,
|
|
278
|
+
status:state.threadGoal.status,
|
|
279
|
+
tokenBudget:state.threadGoal.tokenBudget??null,
|
|
280
|
+
tokensUsed:state.threadGoal.tokensUsed??0
|
|
281
|
+
}:null,
|
|
264
282
|
context: desktopContext(state),
|
|
265
283
|
messages: state ? desktopMessages(state) : [],
|
|
266
284
|
approvals: [...this.approvals.values()].filter(request => request.params.threadId === id).map(request => ({ ...request.params, requestId: String(request.id), method: request.method }))
|
|
@@ -297,8 +315,8 @@ export class DesktopRpc extends EventEmitter {
|
|
|
297
315
|
return thread;
|
|
298
316
|
}
|
|
299
317
|
|
|
300
|
-
async startTurn(id, text) { return this.writeMessage(id, text, false); }
|
|
301
|
-
async steer(id, text, expectedTurnId) { return this.writeMessage(id, text, true, expectedTurnId); }
|
|
318
|
+
async startTurn(id, text, options = {}) { return this.writeMessage(id, text, false, null, options); }
|
|
319
|
+
async steer(id, text, expectedTurnId, options = {}) { return this.writeMessage(id, text, true, expectedTurnId, options); }
|
|
302
320
|
|
|
303
321
|
async updateThreadSettings(id, model, effort, expectedModel, expectedEffort) {
|
|
304
322
|
if (this.starting.has(id) || this.updatingSettings.has(id)) throw new Error('会话正在处理另一项操作,请稍后重试。');
|
|
@@ -322,21 +340,50 @@ export class DesktopRpc extends EventEmitter {
|
|
|
322
340
|
} finally { this.updatingSettings.delete(id); }
|
|
323
341
|
}
|
|
324
342
|
|
|
325
|
-
async writeMessage(id, text, steering, expectedTurnId) {
|
|
343
|
+
async writeMessage(id, text, steering, expectedTurnId, options = {}) {
|
|
326
344
|
if (this.starting.has(id) || this.updatingSettings.has(id)) throw new Error('此会话正在处理其他操作,请勿并发提交。');
|
|
327
345
|
this.starting.add(id);
|
|
328
346
|
try {
|
|
329
347
|
const thread = await this.assertOwner(id);
|
|
330
348
|
if (steering ? !expectedTurnId || thread.turnId !== expectedTurnId : Boolean(thread.turnId)) throw new Error('任务状态已变化,请刷新后选择续聊或追加指令。');
|
|
349
|
+
const mode=options.mode||'default';
|
|
350
|
+
if(!['default','plan','goal'].includes(mode))throw new Error('Codex 轮次模式无效。');
|
|
351
|
+
const attachments=Array.isArray(options.attachments)?options.attachments:[];
|
|
352
|
+
if(attachments.length>5||attachments.some(file=>{
|
|
353
|
+
if(!file||typeof file.label!=='string'||typeof file.path!=='string'||!path.isAbsolute(file.path)||file.fsPath!==file.path||!thread.state.cwd)return true;
|
|
354
|
+
try{
|
|
355
|
+
const workspace=fs.realpathSync(thread.state.cwd);
|
|
356
|
+
const filename=path.resolve(file.path);
|
|
357
|
+
const relative=path.relative(workspace,filename);
|
|
358
|
+
const attachmentRoot=path.join(workspace,'.orbit-attachments');
|
|
359
|
+
const resolved=fs.realpathSync(filename);
|
|
360
|
+
const stat=fs.lstatSync(filename);
|
|
361
|
+
return !relative||relative==='..'||relative.startsWith(`..${path.sep}`)||path.isAbsolute(relative)
|
|
362
|
+
||path.dirname(filename)!==attachmentRoot||resolved!==filename||stat.isSymbolicLink()||!stat.isFile()||stat.size>8*1024*1024;
|
|
363
|
+
}catch{return true}
|
|
364
|
+
}))throw new Error('文件附件无效、位于工作区外或数量超限。');
|
|
365
|
+
const baseMode=thread.state.latestCollaborationMode||{mode:'default',settings:{}};
|
|
366
|
+
const selectedMode=mode==='plan'?'plan':baseMode.mode==='plan'?'default':baseMode.mode||'default';
|
|
367
|
+
const developerInstructions=baseMode.settings?.developer_instructions??null;
|
|
368
|
+
const collaborationMode={
|
|
369
|
+
mode:selectedMode,
|
|
370
|
+
settings:{
|
|
371
|
+
...baseMode.settings,
|
|
372
|
+
model:baseMode.settings?.model||thread.state.latestThreadSettings?.model||thread.state.latestModel||'',
|
|
373
|
+
reasoning_effort:baseMode.settings?.reasoning_effort??thread.state.latestThreadSettings?.effort??thread.state.latestReasoningEffort??null,
|
|
374
|
+
developer_instructions:developerInstructions
|
|
375
|
+
}
|
|
376
|
+
};
|
|
331
377
|
const messageId = crypto.randomUUID();
|
|
332
378
|
const input = [{ type: 'text', text, text_elements: [] }];
|
|
333
|
-
const
|
|
379
|
+
const fileAttachments=attachments.map(({label,path:filename})=>({label,path:filename,fsPath:filename}));
|
|
380
|
+
const restoreMessage = { id: messageId, text, cwd: thread.state.cwd, createdAt: Date.now(), context: { prompt: text, addedFiles: [], fileAttachments, uploadedFileAttachments:[], imageAttachments: [], ideContext: null, workspaceRoots: [thread.state.cwd] } };
|
|
334
381
|
let result;
|
|
335
|
-
if (steering) result = await this.request('thread-follower-steer-turn', { conversationId: id, input, restoreMessage, clientUserMessageId: messageId, attachments:
|
|
336
|
-
else result = await this.request('thread-follower-start-turn', { conversationId: id, turnStart: { request: { threadId: id, input, clientUserMessageId: messageId, turnTrigger: 'composer' }, context: { restoreMessage } } }, thread.ownerClientId);
|
|
382
|
+
if (steering) result = await this.request('thread-follower-steer-turn', { conversationId: id, input, restoreMessage, clientUserMessageId: messageId, attachments:fileAttachments }, thread.ownerClientId);
|
|
383
|
+
else result = await this.request('thread-follower-start-turn', { conversationId: id, turnStart: { request: { threadId: id, input, clientUserMessageId: messageId, turnTrigger: 'composer',collaborationMode }, context: { restoreMessage,attachments:fileAttachments,collaborationMode } } }, thread.ownerClientId);
|
|
337
384
|
const response = result.result || result;
|
|
338
385
|
const turnId = response.turnId || response.turn?.id || this.threads.get(id)?.turnId || null;
|
|
339
|
-
return { threadId:
|
|
386
|
+
return { threadId:id,turnId,completed:typeof response.completed==='boolean'?response.completed:Boolean(!steering&&!turnId),steering,sharedExecution:true,mode,attachments:attachments.map(file=>file.label) };
|
|
340
387
|
} finally { this.starting.delete(id); }
|
|
341
388
|
}
|
|
342
389
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { CodexRpc } from './rpc.mjs';
|
|
3
|
+
|
|
4
|
+
export class ResumeManager extends EventEmitter {
|
|
5
|
+
constructor({ home, createRpc = () => new CodexRpc({ home }) } = {}) {
|
|
6
|
+
super();
|
|
7
|
+
this.home = home;
|
|
8
|
+
this.createRpc = createRpc;
|
|
9
|
+
this.sessions = new Map();
|
|
10
|
+
this.pending = new Map();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
has(id) {
|
|
14
|
+
return this.sessions.has(id);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
ids() {
|
|
18
|
+
return [...this.sessions.keys()];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
rpcFor(id) {
|
|
22
|
+
return this.sessions.get(id)?.rpc || null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
rpcForApproval(requestId) {
|
|
26
|
+
for (const { rpc } of this.sessions.values()) if (rpc.approvals.has(String(requestId))) return rpc;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
runtime(id) {
|
|
31
|
+
const session = this.sessions.get(id);
|
|
32
|
+
const thread = session?.rpc.threads.get(id);
|
|
33
|
+
if (!session || !thread) return null;
|
|
34
|
+
return {
|
|
35
|
+
threadId:id, attached:Boolean(session.rpc.connected), mode:'same-id-resume',
|
|
36
|
+
ownerClientId:null, connectionId:null, turnId:thread.turnId || null, revision:null,
|
|
37
|
+
permissionMode:thread.writable ? 'workspace-write' : 'read-only',
|
|
38
|
+
model:session.model || null, effort:null, collaborationMode:'default',
|
|
39
|
+
goal:session.goal || null, context:null, messages:[],
|
|
40
|
+
approvals:[...session.rpc.approvals.values()]
|
|
41
|
+
.filter(request => request.params?.threadId === id)
|
|
42
|
+
.map(request => ({ ...request.params, requestId:String(request.id), method:request.method }))
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async resume(id, { model = null } = {}) {
|
|
47
|
+
const current = this.sessions.get(id);
|
|
48
|
+
if (current) {
|
|
49
|
+
if (!current.closing) {
|
|
50
|
+
current.releaseWhenIdle = false;
|
|
51
|
+
return this.runtime(id);
|
|
52
|
+
}
|
|
53
|
+
await current.closing;
|
|
54
|
+
}
|
|
55
|
+
const pending = this.pending.get(id);
|
|
56
|
+
if (pending) return pending;
|
|
57
|
+
|
|
58
|
+
const operation = (async () => {
|
|
59
|
+
const rpc = this.createRpc({ home:this.home, threadId:id });
|
|
60
|
+
try {
|
|
61
|
+
await rpc.resume(id, true);
|
|
62
|
+
const goalResult = typeof rpc.request === 'function'
|
|
63
|
+
? await rpc.request('thread/goal/get', { threadId:id }).catch(() => ({ goal:null }))
|
|
64
|
+
: { goal:null };
|
|
65
|
+
const session = { id, rpc, model, goal:goalResult.goal || null, releaseWhenIdle:false, closing:null, eventListener:null };
|
|
66
|
+
session.eventListener = event => this.onEvent(session, event);
|
|
67
|
+
rpc.on('event', session.eventListener);
|
|
68
|
+
this.sessions.set(id, session);
|
|
69
|
+
this.emit('event', { method:'orbit/engine', params:{
|
|
70
|
+
connected:true, mode:'desktop', desktopConnected:false, resumedConnected:true
|
|
71
|
+
} });
|
|
72
|
+
return this.runtime(id);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
await rpc.close();
|
|
75
|
+
if (/already has an active writer/i.test(error.message)) {
|
|
76
|
+
throw Object.assign(new Error('这条原会话仍由 Codex 桌面或其他执行器占用;未启动第二个执行器。请稍后重试。'), { code:'DESKTOP_THREAD_BUSY' });
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
this.pending.set(id, operation);
|
|
82
|
+
try { return await operation; }
|
|
83
|
+
finally { if (this.pending.get(id) === operation) this.pending.delete(id); }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onEvent(session, event) {
|
|
87
|
+
if (event.method === 'thread/goal/updated' && event.params?.threadId === session.id) {
|
|
88
|
+
session.goal = event.params.goal || null;
|
|
89
|
+
this.emit('event', event);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (event.method === 'thread/goal/cleared' && event.params?.threadId === session.id) {
|
|
93
|
+
session.goal = null;
|
|
94
|
+
this.emit('event', event);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (event.method === 'orbit/engine') {
|
|
98
|
+
if (!event.params?.connected && !session.closing) {
|
|
99
|
+
this.sessions.delete(session.id);
|
|
100
|
+
this.emit('released', session.id);
|
|
101
|
+
this.emit('lost', session.id, '本机 Codex App Server 已断开;请重新连接这条原会话。', 'APP_SERVER_DISCONNECTED');
|
|
102
|
+
this.emit('event', { method:'orbit/desktop', params:{
|
|
103
|
+
threadId:session.id, mode:'same-id-resume', attached:false, turnId:null,
|
|
104
|
+
reason:'本机 Codex App Server 已断开;请重新连接这条原会话。',
|
|
105
|
+
reasonCode:'APP_SERVER_DISCONNECTED'
|
|
106
|
+
} });
|
|
107
|
+
this.emit('event', { method:'orbit/engine', params:{
|
|
108
|
+
connected:this.sessions.size>0, mode:'desktop', desktopConnected:false,
|
|
109
|
+
resumedConnected:this.sessions.size>0
|
|
110
|
+
} });
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.emit('event', event);
|
|
115
|
+
if (event.method === 'turn/completed' && event.params?.threadId === session.id
|
|
116
|
+
&& session.releaseWhenIdle && !session.rpc.threads.get(session.id)?.turnId) {
|
|
117
|
+
void this.closeSession(session);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async setGoal(id, { objective, tokenBudget = null }) {
|
|
122
|
+
const session = this.sessions.get(id);
|
|
123
|
+
if (!session || session.closing) throw new Error('原会话 App Server 接续已失效,请重新连接。');
|
|
124
|
+
const result = await session.rpc.request('thread/goal/set', {
|
|
125
|
+
threadId:id, origin:'user', objective, status:'active', tokenBudget
|
|
126
|
+
});
|
|
127
|
+
session.goal = result.goal;
|
|
128
|
+
return result.goal;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async setGoalStatus(id, status) {
|
|
132
|
+
const session = this.sessions.get(id);
|
|
133
|
+
if (!session || session.closing) throw new Error('原会话 App Server 接续已失效,请重新连接。');
|
|
134
|
+
const result = await session.rpc.request('thread/goal/set', { threadId:id, origin:'user', status });
|
|
135
|
+
session.goal = result.goal;
|
|
136
|
+
return result.goal;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async clearGoal(id) {
|
|
140
|
+
const session = this.sessions.get(id);
|
|
141
|
+
if (!session || session.closing) throw new Error('原会话 App Server 接续已失效,请重新连接。');
|
|
142
|
+
const result = await session.rpc.request('thread/goal/clear', { threadId:id, origin:'user' });
|
|
143
|
+
if (result.cleared) session.goal = null;
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async release(id) {
|
|
148
|
+
const session = this.sessions.get(id);
|
|
149
|
+
if (!session) return;
|
|
150
|
+
if (session.rpc.threads.get(id)?.turnId) {
|
|
151
|
+
session.releaseWhenIdle = true;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
await this.closeSession(session);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async closeSession(session) {
|
|
158
|
+
if (session.closing) return session.closing;
|
|
159
|
+
session.closing = (async () => {
|
|
160
|
+
await session.rpc.close();
|
|
161
|
+
session.rpc.off('event', session.eventListener);
|
|
162
|
+
if (this.sessions.get(session.id) === session) this.sessions.delete(session.id);
|
|
163
|
+
this.emit('released', session.id);
|
|
164
|
+
this.emit('event', { method:'orbit/engine', params:{
|
|
165
|
+
connected:this.sessions.size>0, mode:'desktop', desktopConnected:false,
|
|
166
|
+
resumedConnected:this.sessions.size>0
|
|
167
|
+
} });
|
|
168
|
+
})();
|
|
169
|
+
return session.closing;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async close() {
|
|
173
|
+
const sessions = [...this.sessions.values()];
|
|
174
|
+
await Promise.all(sessions.map(session => this.closeSession(session)));
|
|
175
|
+
this.sessions.clear();
|
|
176
|
+
}
|
|
177
|
+
}
|
package/lib/rpc.mjs
CHANGED
|
@@ -130,23 +130,44 @@ export class CodexRpc extends EventEmitter {
|
|
|
130
130
|
if (this.threads.has(id)) return;
|
|
131
131
|
const result = await this.request('thread/resume', { threadId: id, excludeTurns: true, sandbox: writable ? 'workspace-write' : 'read-only', approvalPolicy: 'on-request' });
|
|
132
132
|
if (result.thread.id !== id) throw new Error('返回的会话 ID 不一致,已拒绝发送消息。');
|
|
133
|
-
this.threads.set(id, {
|
|
133
|
+
this.threads.set(id, {
|
|
134
|
+
turnId:null,lastCompletedTurnId:null,writable:Boolean(writable),
|
|
135
|
+
model:result.model||result.thread.model||null,
|
|
136
|
+
effort:result.reasoningEffort??result.thread.reasoningEffort??null,
|
|
137
|
+
collaborationMode:result.collaborationMode||{mode:'default',settings:{}}
|
|
138
|
+
});
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
async startTurn(id, text, writable, settings = null) {
|
|
141
|
+
async startTurn(id, text, writable, settings = null, options = {}) {
|
|
137
142
|
if (this.starting.has(id)) throw new Error('此会话正在发送,请勿并发提交。');
|
|
138
143
|
this.starting.add(id);
|
|
139
144
|
try {
|
|
140
145
|
await this.resume(id, writable);
|
|
141
146
|
const thread = this.threads.get(id);
|
|
142
147
|
if (thread.turnId) throw new Error('此会话仍在执行,请等待完成或停止后再发送。');
|
|
148
|
+
const baseMode=thread.collaborationMode||{mode:'default',settings:{}};
|
|
149
|
+
const collaborationMode={
|
|
150
|
+
mode:options.mode==='plan'?'plan':baseMode.mode==='plan'?'default':baseMode.mode||'default',
|
|
151
|
+
settings:{
|
|
152
|
+
...baseMode.settings,
|
|
153
|
+
model:settings?.model||baseMode.settings?.model||thread.model||'',
|
|
154
|
+
reasoning_effort:settings?.effort??baseMode.settings?.reasoning_effort??thread.effort??null,
|
|
155
|
+
developer_instructions:options.mode==='plan'||baseMode.mode==='plan'
|
|
156
|
+
? null
|
|
157
|
+
: baseMode.settings?.developer_instructions??null
|
|
158
|
+
}
|
|
159
|
+
};
|
|
143
160
|
const result = await this.request('turn/start', {
|
|
144
161
|
threadId: id, input: [{ type: 'text', text }], approvalPolicy: 'on-request',
|
|
145
162
|
sandboxPolicy: writable ? { type: 'workspaceWrite', networkAccess: false, excludeTmpdirEnvVar: true, excludeSlashTmp: true } : { type: 'readOnly' },
|
|
163
|
+
collaborationMode,
|
|
146
164
|
...(settings ? { model: settings.model, effort: settings.effort } : {})
|
|
147
165
|
});
|
|
148
166
|
const completed = thread.lastCompletedTurnId === result.turn.id;
|
|
149
167
|
thread.turnId = completed ? null : result.turn.id;
|
|
168
|
+
thread.collaborationMode=collaborationMode;
|
|
169
|
+
if(settings?.model)thread.model=settings.model;
|
|
170
|
+
if(settings&&Object.hasOwn(settings,'effort'))thread.effort=settings.effort;
|
|
150
171
|
return { threadId: id, turnId: result.turn.id, completed };
|
|
151
172
|
} finally { this.starting.delete(id); }
|
|
152
173
|
}
|
|
@@ -198,6 +219,11 @@ export class CodexRpc extends EventEmitter {
|
|
|
198
219
|
}
|
|
199
220
|
|
|
200
221
|
close() {
|
|
222
|
+
const child = this.child;
|
|
223
|
+
const exited = !child || child.exitCode !== null || child.signalCode !== null
|
|
224
|
+
? Promise.resolve()
|
|
225
|
+
: new Promise(resolve => child.once('exit', resolve));
|
|
201
226
|
this.disconnect(new Error('服务已停止。'));
|
|
227
|
+
return exited;
|
|
202
228
|
}
|
|
203
229
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orbit-codex-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Connect your original Codex desktop conversations to Orbit on the web.",
|
|
6
6
|
"engines": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"test": "node --test test/*.test.mjs",
|
|
28
28
|
"test:browser": "node --test test/browser/*.test.mjs",
|
|
29
29
|
"test:native": "node test/native-smoke.mjs",
|
|
30
|
-
"check": "node --check cli.mjs && node --check server.mjs && node --check lib/store.mjs && node --check lib/rpc.mjs && node --check lib/desktop.mjs && node --check lib/device-auth.mjs && node --check lib/connector-service.mjs && node --check public/app.js && node --check public/connect.js && node --check relay-server.mjs && node --check relay-store.mjs && node --check relay-agent.mjs && node --check public/admin.js"
|
|
30
|
+
"check": "node --check cli.mjs && node --check server.mjs && node --check lib/store.mjs && node --check lib/rpc.mjs && node --check lib/desktop.mjs && node --check lib/resume-manager.mjs && node --check lib/device-auth.mjs && node --check lib/connector-service.mjs && node --check public/app.js && node --check public/connect.js && node --check relay-server.mjs && node --check relay-store.mjs && node --check relay-agent.mjs && node --check public/admin.js"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"dompurify": "^3.3.0",
|