@walkhi/code-relax 0.1.0-beta.7 → 0.1.0-beta.9
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 +12 -13
- package/dist/bin/codex-remote.mjs +43 -18
- package/dist/bin/self-relay-demo.mjs +1 -1
- package/dist/shared/app-server-events.cjs +5 -3
- package/dist/src/desktop-launcher.mjs +7 -10
- package/dist/src/managed-app-server.mjs +3 -0
- package/dist/src/message-images.mjs +0 -18
- package/dist/src/onboarding.mjs +3 -4
- package/dist/src/platform/README.md +1 -1
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +1 -1
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +1 -1
- package/dist/src/platform/windows/resident-startup.ps1 +58 -18
- package/dist/src/platform/windows/service-host.mjs +4 -7
- package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
- package/dist/src/self-relay/admin-page.mjs +54 -0
- package/dist/src/self-relay/admin-state.mjs +25 -12
- package/dist/src/self-relay/demo.mjs +2 -3
- package/dist/src/self-relay/lifecycle.mjs +6 -6
- package/dist/src/self-relay/server.mjs +7 -11
- package/dist/src/server.mjs +163 -1034
- package/dist/src/service-doctor.mjs +10 -5
- package/dist/src/service-lifecycle.mjs +3 -8
- package/dist/src/shared-app-server.mjs +13 -11
- package/dist/src/shared-recovery.mjs +5 -5
- package/dist/src/shared-thread-stream.mjs +1 -1
- package/dist/web/capabilities.js +1 -8
- package/dist/web/chat-transport.js +6 -12
- package/dist/web/chat.css +13 -14
- package/dist/web/chat.js +23 -41
- package/dist/web/community.css +15 -10
- package/dist/web/community.html +4 -5
- package/dist/web/composer-controller.js +0 -4
- package/dist/web/index.html +7 -39
- package/dist/web/resources.json +1 -1
- package/dist/web/task-list-view.js +1 -8
- package/dist/web/timeline-reducer.js +0 -4
- package/package.json +16 -18
- package/tools/postinstall.mjs +4 -1
- package/dist/src/app-server-client.mjs +0 -468
- package/dist/src/app-server-tasks.mjs +0 -358
- package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
- package/dist/src/platform/windows/desktop-tools.mjs +0 -48
- package/dist/src/thread-catalog.mjs +0 -47
- package/dist/tools/find-desktop-pipe.mjs +0 -10
- package/dist/web/community-view.js +0 -20
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import { EventEmitter } from 'node:events';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import events from '../shared/app-server-events.cjs';
|
|
6
|
-
|
|
7
|
-
const STORE_SCHEMA_VERSION = 1;
|
|
8
|
-
|
|
9
|
-
export class AppServerTaskStore {
|
|
10
|
-
constructor(filePath) {
|
|
11
|
-
this.filePath = path.resolve(filePath);
|
|
12
|
-
this.records = new Map();
|
|
13
|
-
this.loadError = null;
|
|
14
|
-
this.load();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
load() {
|
|
18
|
-
if (!fs.existsSync(this.filePath)) return;
|
|
19
|
-
try {
|
|
20
|
-
const value = JSON.parse(fs.readFileSync(this.filePath, 'utf8'));
|
|
21
|
-
if (value?.schemaVersion !== STORE_SCHEMA_VERSION || !Array.isArray(value.tasks)) {
|
|
22
|
-
throw new Error('任务归属文件格式不受支持。');
|
|
23
|
-
}
|
|
24
|
-
for (const task of value.tasks) {
|
|
25
|
-
if (!validTaskRecord(task)) continue;
|
|
26
|
-
this.records.set(task.id, { ...task, transport: 'app-server' });
|
|
27
|
-
}
|
|
28
|
-
} catch (error) {
|
|
29
|
-
this.loadError = error instanceof Error ? error : new Error(String(error));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
ensureUsable() {
|
|
34
|
-
if (this.loadError) {
|
|
35
|
-
throw new Error(`无法读取 app-server 任务归属:${this.loadError.message}`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
has(threadId) {
|
|
40
|
-
return this.records.has(threadId);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
get(threadId) {
|
|
44
|
-
return this.records.get(threadId) || null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
list() {
|
|
48
|
-
return [...this.records.values()].sort((left, right) => taskRecency(right) - taskRecency(left));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
upsert(task) {
|
|
52
|
-
this.ensureUsable();
|
|
53
|
-
if (!validTaskRecord(task)) throw new Error('app-server 任务归属记录无效。');
|
|
54
|
-
const record = { ...task, transport: 'app-server' };
|
|
55
|
-
const existing = this.records.get(record.id);
|
|
56
|
-
if (existing && JSON.stringify(existing) === JSON.stringify(record)) return existing;
|
|
57
|
-
this.records.set(record.id, record);
|
|
58
|
-
this.save();
|
|
59
|
-
return record;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
remove(threadId) {
|
|
63
|
-
this.ensureUsable();
|
|
64
|
-
if (!this.records.delete(threadId)) return false;
|
|
65
|
-
this.save();
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
save() {
|
|
70
|
-
this.ensureUsable();
|
|
71
|
-
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
72
|
-
const temporaryPath = `${this.filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
73
|
-
const payload = `${JSON.stringify({
|
|
74
|
-
schemaVersion: STORE_SCHEMA_VERSION,
|
|
75
|
-
tasks: this.list(),
|
|
76
|
-
}, null, 2)}\n`;
|
|
77
|
-
try {
|
|
78
|
-
fs.writeFileSync(temporaryPath, payload, { encoding: 'utf8', flag: 'wx' });
|
|
79
|
-
fs.renameSync(temporaryPath, this.filePath);
|
|
80
|
-
} catch (error) {
|
|
81
|
-
try { fs.unlinkSync(temporaryPath); } catch {}
|
|
82
|
-
throw error;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export class AppServerTaskTransport extends EventEmitter {
|
|
88
|
-
constructor({ client, store }) {
|
|
89
|
-
super();
|
|
90
|
-
this.client = client;
|
|
91
|
-
this.store = store;
|
|
92
|
-
this.hydratedGeneration = 0;
|
|
93
|
-
this.loadedGenerations = new Map();
|
|
94
|
-
this.client.on('notification', record => this.handleNotification(record));
|
|
95
|
-
this.client.on('server-request', request => {
|
|
96
|
-
const threadId = request.params?.threadId || request.params?.conversationId || '';
|
|
97
|
-
if (threadId && this.owns(threadId)) this.emit('thread-event', { threadId, record: request });
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
owns(threadId) {
|
|
102
|
-
return this.store.has(threadId);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
record(threadId) {
|
|
106
|
-
const record = this.store.get(threadId);
|
|
107
|
-
if (!record) throw new Error('该任务不属于独立 app-server 通道。');
|
|
108
|
-
return record;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async createTask({ cwd, projectId, title, input, model, effort }) {
|
|
112
|
-
if (!path.isAbsolute(cwd)) throw new Error('app-server 任务 cwd 必须是绝对路径。');
|
|
113
|
-
if (!Array.isArray(input) || !input.length) throw new Error('app-server 任务输入不能为空。');
|
|
114
|
-
this.store.ensureUsable();
|
|
115
|
-
await this.client.start();
|
|
116
|
-
const startParams = { cwd, ephemeral: false, projectId: projectId || null };
|
|
117
|
-
if (!projectId) startParams.runtimeWorkspaceRoots = [];
|
|
118
|
-
if (model) startParams.model = model;
|
|
119
|
-
const started = await this.client.request('thread/start', startParams, 60_000);
|
|
120
|
-
this.loadedGenerations.set(started.thread.id, this.client.generation);
|
|
121
|
-
const now = Math.floor(Date.now() / 1000);
|
|
122
|
-
const record = this.store.upsert({
|
|
123
|
-
id: started.thread.id,
|
|
124
|
-
title: title || started.thread.name || '新对话',
|
|
125
|
-
cwd,
|
|
126
|
-
projectId: projectId || null,
|
|
127
|
-
createdAt: Number(started.thread.createdAt) || now,
|
|
128
|
-
updatedAt: Number(started.thread.updatedAt) || now,
|
|
129
|
-
recencyAt: Number(started.thread.recencyAt) || Number(started.thread.updatedAt) || now,
|
|
130
|
-
status: threadStatus(started.thread.status),
|
|
131
|
-
activeFlags: threadActiveFlags(started.thread.status),
|
|
132
|
-
model: model || started.model || started.thread.model || '',
|
|
133
|
-
effort: effort || started.reasoningEffort || started.thread.reasoningEffort || '',
|
|
134
|
-
transport: 'app-server',
|
|
135
|
-
});
|
|
136
|
-
if (title) try {
|
|
137
|
-
await this.client.request('thread/name/set', { threadId: record.id, name: title }, 15_000);
|
|
138
|
-
} catch {
|
|
139
|
-
// The persisted local title still keeps the task identifiable.
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const turnParams = { threadId: record.id, input };
|
|
143
|
-
if (model) turnParams.model = model;
|
|
144
|
-
if (effort) turnParams.effort = effort;
|
|
145
|
-
const turn = await this.client.request('turn/start', turnParams, 60_000);
|
|
146
|
-
this.store.upsert({ ...record, status: 'active', activeFlags: [] });
|
|
147
|
-
return { thread: started.thread, turn: turn.turn, record: this.store.get(record.id) };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async listTasks() {
|
|
151
|
-
const records = this.store.list();
|
|
152
|
-
if (!records.length) return [];
|
|
153
|
-
await this.client.start();
|
|
154
|
-
if (this.hydratedGeneration === this.client.generation) {
|
|
155
|
-
return records.map(record => summaryFromRecord(record));
|
|
156
|
-
}
|
|
157
|
-
const results = await Promise.allSettled(records.map(record => this.readTask(record.id, { includeTurns: false })));
|
|
158
|
-
const summaries = results.map((result, index) => {
|
|
159
|
-
const record = records[index];
|
|
160
|
-
if (result.status === 'rejected') return summaryFromRecord(record, 'unknown');
|
|
161
|
-
const thread = result.value;
|
|
162
|
-
const updated = this.store.upsert({
|
|
163
|
-
...record,
|
|
164
|
-
title: thread.name || record.title,
|
|
165
|
-
cwd: thread.cwd || record.cwd,
|
|
166
|
-
updatedAt: Number(thread.updatedAt) || record.updatedAt,
|
|
167
|
-
recencyAt: Number(thread.recencyAt) || record.recencyAt || record.updatedAt,
|
|
168
|
-
status: threadStatus(thread.status),
|
|
169
|
-
activeFlags: threadActiveFlags(thread.status),
|
|
170
|
-
model: thread.model || record.model,
|
|
171
|
-
effort: thread.reasoningEffort || record.effort,
|
|
172
|
-
});
|
|
173
|
-
return summaryFromThread(thread, updated);
|
|
174
|
-
});
|
|
175
|
-
if (results.every(result => result.status === 'fulfilled')) {
|
|
176
|
-
this.hydratedGeneration = this.client.generation;
|
|
177
|
-
}
|
|
178
|
-
return summaries;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async readTask(threadId, { includeTurns = true, turnLimit = 10 } = {}) {
|
|
182
|
-
this.record(threadId);
|
|
183
|
-
await this.client.start();
|
|
184
|
-
if (!includeTurns) return (await this.readInitializedThread({ threadId, includeTurns: false })).thread;
|
|
185
|
-
const [threadResult, turnsResult] = await Promise.all([
|
|
186
|
-
this.readInitializedThread({ threadId, includeTurns: false }),
|
|
187
|
-
this.client.request('thread/turns/list', {
|
|
188
|
-
threadId,
|
|
189
|
-
limit: turnLimit,
|
|
190
|
-
sortDirection: 'desc',
|
|
191
|
-
itemsView: 'full',
|
|
192
|
-
}, 30_000),
|
|
193
|
-
]);
|
|
194
|
-
return { ...threadResult.thread, turns: [...(turnsResult.data || [])].reverse().map(events.compactTurn) };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async readInitializedThread(params) {
|
|
198
|
-
for (let attempt = 0; ; attempt += 1) {
|
|
199
|
-
try {
|
|
200
|
-
return await this.client.request('thread/read', params, 30_000);
|
|
201
|
-
} catch (error) {
|
|
202
|
-
const initializing = error instanceof AppServerRpcError
|
|
203
|
-
&& error.method === 'thread/read'
|
|
204
|
-
&& /rollout at .* is empty/i.test(error.message);
|
|
205
|
-
if (!initializing || attempt >= 3) throw error;
|
|
206
|
-
await delay(100 * (attempt + 1));
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async startTurn(threadId, { input, model, effort }) {
|
|
212
|
-
const record = this.record(threadId);
|
|
213
|
-
await this.client.start();
|
|
214
|
-
await this.ensureResumed(threadId);
|
|
215
|
-
const params = { threadId, input };
|
|
216
|
-
if (model) params.model = model;
|
|
217
|
-
if (effort) params.effort = effort;
|
|
218
|
-
const result = await this.client.request('turn/start', params, 60_000);
|
|
219
|
-
this.store.upsert({
|
|
220
|
-
...record,
|
|
221
|
-
status: 'active',
|
|
222
|
-
activeFlags: [],
|
|
223
|
-
model: model || record.model,
|
|
224
|
-
effort: effort || record.effort,
|
|
225
|
-
});
|
|
226
|
-
return result;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async adoptTask(thread) {
|
|
230
|
-
this.store.ensureUsable();
|
|
231
|
-
await this.client.start();
|
|
232
|
-
let result;
|
|
233
|
-
try {
|
|
234
|
-
result = await this.client.request('thread/resume', { threadId: thread.id }, 60000);
|
|
235
|
-
} catch (error) {
|
|
236
|
-
if (error instanceof AppServerRpcError && /already has an active writer/.test(error.message)) {
|
|
237
|
-
throw new Error('Desktop 仍持有此聊天的写入权限,空闲不代表已释放。当前无法直接接管;此聊天仍由 Desktop 执行。');
|
|
238
|
-
}
|
|
239
|
-
throw error;
|
|
240
|
-
}
|
|
241
|
-
if (result.thread.id !== thread.id) throw new Error('恢复后的聊天 ID 不一致。');
|
|
242
|
-
const record = this.store.upsert({
|
|
243
|
-
id: thread.id, title: thread.title || result.thread.name || thread.id,
|
|
244
|
-
cwd: result.thread.cwd, projectId: thread.projectId || null,
|
|
245
|
-
createdAt: Number(result.thread.createdAt) || Number(thread.createdAt) || 0,
|
|
246
|
-
updatedAt: Number(result.thread.updatedAt) || Number(thread.updatedAt) || 0,
|
|
247
|
-
recencyAt: Number(result.thread.recencyAt) || Number(thread.recencyAt)
|
|
248
|
-
|| Number(result.thread.updatedAt) || Number(thread.updatedAt) || 0,
|
|
249
|
-
status: threadStatus(result.thread.status), activeFlags: threadActiveFlags(result.thread.status),
|
|
250
|
-
origin: 'desktop', transport: 'app-server',
|
|
251
|
-
});
|
|
252
|
-
this.loadedGenerations.set(thread.id, this.client.generation);
|
|
253
|
-
return summaryFromRecord(record);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async ensureResumed(threadId) {
|
|
257
|
-
if (this.loadedGenerations.get(threadId) === this.client.generation) return;
|
|
258
|
-
await this.client.request('thread/resume', { threadId }, 60000);
|
|
259
|
-
this.loadedGenerations.set(threadId, this.client.generation);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async steerTurn(threadId, expectedTurnId, input) {
|
|
263
|
-
this.record(threadId);
|
|
264
|
-
await this.client.start();
|
|
265
|
-
return this.client.request('turn/steer', { threadId, expectedTurnId, input }, 60_000);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async interruptTurn(threadId, turnId) {
|
|
269
|
-
this.record(threadId);
|
|
270
|
-
await this.client.start();
|
|
271
|
-
return this.client.request('turn/interrupt', { threadId, turnId }, 60_000);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async deleteTask(threadId) {
|
|
275
|
-
this.record(threadId);
|
|
276
|
-
if (this.record(threadId).origin === 'desktop') throw new Error('接管的 Desktop 历史不允许通过此接口删除。');
|
|
277
|
-
await this.client.start();
|
|
278
|
-
await this.client.request('thread/delete', { threadId }, 30_000);
|
|
279
|
-
this.store.remove(threadId);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
handleNotification(record) {
|
|
283
|
-
const threadId = notificationThreadId(record);
|
|
284
|
-
if (!threadId || !this.owns(threadId)) return;
|
|
285
|
-
const current = this.store.get(threadId);
|
|
286
|
-
if (current && ['thread/status/changed', 'turn/started', 'turn/completed', 'thread/name/updated'].includes(record.method)) {
|
|
287
|
-
const next = { ...current };
|
|
288
|
-
if (record.method === 'thread/status/changed') {
|
|
289
|
-
next.status = threadStatus(record.params?.status);
|
|
290
|
-
next.activeFlags = threadActiveFlags(record.params?.status);
|
|
291
|
-
}
|
|
292
|
-
if (record.method === 'turn/started') { next.status = 'active'; next.activeFlags = []; }
|
|
293
|
-
if (record.method === 'turn/completed') { next.status = 'idle'; next.activeFlags = []; }
|
|
294
|
-
if (record.method === 'thread/name/updated' && record.params?.threadName) next.title = record.params.threadName;
|
|
295
|
-
this.store.upsert(next);
|
|
296
|
-
this.hydratedGeneration = 0;
|
|
297
|
-
}
|
|
298
|
-
this.emit('thread-event', { threadId, record });
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export function summaryFromRecord(record, status = record.status || 'unknown') {
|
|
303
|
-
return {
|
|
304
|
-
id: record.id,
|
|
305
|
-
kind: 'codex',
|
|
306
|
-
hostId: 'app-server',
|
|
307
|
-
title: record.title,
|
|
308
|
-
cwd: record.cwd,
|
|
309
|
-
projectId: record.projectId,
|
|
310
|
-
updatedAt: record.updatedAt,
|
|
311
|
-
recencyAt: record.recencyAt ?? record.updatedAt,
|
|
312
|
-
status,
|
|
313
|
-
activeFlags: Array.isArray(record.activeFlags) ? record.activeFlags : [],
|
|
314
|
-
transport: 'app-server',
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function summaryFromThread(thread, record) {
|
|
319
|
-
return {
|
|
320
|
-
...summaryFromRecord(record, threadStatus(thread.status)),
|
|
321
|
-
activeFlags: threadActiveFlags(thread.status),
|
|
322
|
-
title: thread.name || record.title || thread.preview || thread.id,
|
|
323
|
-
cwd: thread.cwd || record.cwd,
|
|
324
|
-
updatedAt: Number(thread.updatedAt) || record.updatedAt,
|
|
325
|
-
recencyAt: Number(thread.recencyAt) || record.recencyAt || record.updatedAt,
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function validTaskRecord(task) {
|
|
330
|
-
return task && typeof task.id === 'string' && task.id.length > 10
|
|
331
|
-
&& typeof task.title === 'string' && typeof task.cwd === 'string' && path.isAbsolute(task.cwd)
|
|
332
|
-
&& Number.isFinite(Number(task.createdAt)) && Number.isFinite(Number(task.updatedAt));
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function taskRecency(task) {
|
|
336
|
-
return Number(task?.recencyAt ?? task?.updatedAt) || 0;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function delay(milliseconds) {
|
|
340
|
-
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function threadStatus(value) {
|
|
344
|
-
if (typeof value === 'string') return value;
|
|
345
|
-
if (value && typeof value === 'object') return value.type || value.status || 'unknown';
|
|
346
|
-
return 'unknown';
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function threadActiveFlags(value) {
|
|
350
|
-
return value && typeof value === 'object' && Array.isArray(value.activeFlags)
|
|
351
|
-
? value.activeFlags.filter(flag => typeof flag === 'string')
|
|
352
|
-
: [];
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function notificationThreadId(record) {
|
|
356
|
-
const params = record?.params || {};
|
|
357
|
-
return params.threadId || params.conversationId || params.thread?.id || '';
|
|
358
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { findDesktopPipe } from './desktop-tools.mjs';
|
|
2
|
-
|
|
3
|
-
// Optional Desktop presentation data never blocks the shared execution service.
|
|
4
|
-
export class DesktopMonitor {
|
|
5
|
-
constructor({ discover = () => findDesktopPipe({ allowMissing: true }), read, report = console.error }) {
|
|
6
|
-
this.discover = discover; this.read = read; this.report = report;
|
|
7
|
-
this.pipe = null; this.snapshot = null; this.stopped = true;
|
|
8
|
-
}
|
|
9
|
-
start() { this.stopped = false; void this.tick(); }
|
|
10
|
-
stop() { this.stopped = true; clearTimeout(this.timer); }
|
|
11
|
-
async refresh() {
|
|
12
|
-
try {
|
|
13
|
-
const pipe = await this.discover();
|
|
14
|
-
if (this.stopped) return;
|
|
15
|
-
this.pipe = pipe;
|
|
16
|
-
if (!pipe) { this.snapshot = null; return; }
|
|
17
|
-
const [listing, projects] = await Promise.all([
|
|
18
|
-
this.read('list_threads', { limit: 50 }, pipe), this.read('list_projects', {}, pipe),
|
|
19
|
-
]);
|
|
20
|
-
if (this.stopped) return;
|
|
21
|
-
if (!listing.success || !projects.success) throw new Error('Desktop 列表读取失败');
|
|
22
|
-
this.snapshot = { listing: listing.data, projects: projects.data.projects || [] };
|
|
23
|
-
this.error = null;
|
|
24
|
-
} catch (error) {
|
|
25
|
-
this.pipe = null; this.snapshot = null;
|
|
26
|
-
if (this.error !== error.message) this.report(`Desktop 排序暂不可用:${error.message}`);
|
|
27
|
-
this.error = error.message;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
async tick() {
|
|
31
|
-
await this.refresh();
|
|
32
|
-
if (!this.stopped) { this.timer = setTimeout(() => this.tick(), this.pipe ? 10000 : 2000); this.timer.unref(); }
|
|
33
|
-
}
|
|
34
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import net from 'node:net';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
-
|
|
5
|
-
async function isToolsPipe(pipe, timeout) {
|
|
6
|
-
return new Promise(resolve => {
|
|
7
|
-
const socket = net.createConnection(pipe);
|
|
8
|
-
let data = Buffer.alloc(0);
|
|
9
|
-
const finish = value => { clearTimeout(timer); socket.destroy(); resolve(value); };
|
|
10
|
-
const timer = setTimeout(() => finish(false), timeout);
|
|
11
|
-
socket.on('error', () => finish(false));
|
|
12
|
-
socket.on('end', () => finish(false));
|
|
13
|
-
socket.on('connect', () => {
|
|
14
|
-
const body = Buffer.from(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { threadStartKind: 'all' } }));
|
|
15
|
-
const header = Buffer.alloc(4); header.writeUInt32LE(body.length);
|
|
16
|
-
socket.write(Buffer.concat([header, body]));
|
|
17
|
-
});
|
|
18
|
-
socket.on('data', chunk => {
|
|
19
|
-
data = Buffer.concat([data, chunk]);
|
|
20
|
-
if (data.length < 4) return;
|
|
21
|
-
const length = data.readUInt32LE(0);
|
|
22
|
-
if (length > 8 * 1024 * 1024) return finish(false);
|
|
23
|
-
if (data.length < length + 4) return;
|
|
24
|
-
try {
|
|
25
|
-
const response = JSON.parse(data.subarray(4, length + 4));
|
|
26
|
-
finish(response.id === 1 && response.result?.tools?.some(tool => tool.namespace === 'codex_app' && tool.name === 'send_message_to_thread'));
|
|
27
|
-
} catch { finish(false); }
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// The Desktop WebSocket may connect before its tools router is ready.
|
|
34
|
-
export async function findDesktopPipe({ timeout = 0, allowMissing = false, wait = delay, now = Date.now,
|
|
35
|
-
enumerate = () => fs.readdirSync('\\\\.\\pipe\\').filter(name => name.startsWith('codex-browser-use-')).map(name => `\\\\.\\pipe\\${name}`),
|
|
36
|
-
ready = isToolsPipe } = {}) {
|
|
37
|
-
const deadline = now() + timeout;
|
|
38
|
-
do {
|
|
39
|
-
const matches = (await Promise.all(enumerate().map(async pipe =>
|
|
40
|
-
await ready(pipe, Math.max(1, Math.min(2000, timeout ? deadline - now() : 2000))) ? pipe : null))).filter(Boolean);
|
|
41
|
-
if (matches.length === 1) return matches[0];
|
|
42
|
-
if (matches.length > 1) throw new Error(`检测到 ${matches.length} 个桌面工具管道,无法确定 Bridge 应连接的 Desktop。`);
|
|
43
|
-
if (now() >= deadline) break;
|
|
44
|
-
await wait(Math.min(250, deadline - now()));
|
|
45
|
-
} while (true);
|
|
46
|
-
if (allowMissing) return null;
|
|
47
|
-
throw new Error('Desktop 工具接口尚未就绪。请等待 Desktop 首页加载完成后重试;未启动 Bridge,也未结束 Desktop。');
|
|
48
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
function normalizedPath(value) {
|
|
5
|
-
return String(value || '').replace(/^\\\\\?\\/, '').replaceAll('\\', '/').replace(/\/+$/, '').toLowerCase();
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function supplementThreadCatalog(data, projects, codexRoot, limit) {
|
|
9
|
-
const db = new DatabaseSync(path.join(codexRoot, 'state_5.sqlite'), { readOnly: true });
|
|
10
|
-
try {
|
|
11
|
-
const rows = db.prepare(`SELECT id, name, title, cwd, project_id, archived,
|
|
12
|
-
COALESCE(updated_at_ms, updated_at * 1000) AS updated_ms,
|
|
13
|
-
MAX(recency_at_ms, recency_at * 1000, COALESCE(updated_at_ms, updated_at * 1000)) AS sort_ms
|
|
14
|
-
FROM threads WHERE source IN ('vscode', 'cli') ORDER BY sort_ms DESC`).all();
|
|
15
|
-
const indexed = new Map(rows.map(row => [row.id, row]));
|
|
16
|
-
const localProjects = projects.filter(project => project.hostId === 'local' && project.path)
|
|
17
|
-
.sort((a, b) => normalizedPath(b.path).length - normalizedPath(a.path).length);
|
|
18
|
-
const assignProject = thread => {
|
|
19
|
-
if (thread.projectId || thread.hostId !== 'local') return thread;
|
|
20
|
-
const cwd = normalizedPath(thread.cwd);
|
|
21
|
-
const project = localProjects.find(project => cwd === normalizedPath(project.path) || cwd.startsWith(normalizedPath(project.path) + '/'));
|
|
22
|
-
return { ...thread, projectId: project?.projectId || null };
|
|
23
|
-
};
|
|
24
|
-
const normalizeRecency = thread => {
|
|
25
|
-
const assigned = assignProject(thread);
|
|
26
|
-
const row = assigned.hostId === 'local' ? indexed.get(assigned.id) : null;
|
|
27
|
-
return row ? { ...assigned, updatedAt: row.updated_ms / 1000, recencyAt: row.sort_ms / 1000 }
|
|
28
|
-
: { ...assigned, recencyAt: assigned.recencyAt ?? assigned.updatedAt };
|
|
29
|
-
};
|
|
30
|
-
const visible = thread => thread.kind === 'codex'
|
|
31
|
-
&& (thread.hostId !== 'local' || !indexed.get(thread.id)?.archived);
|
|
32
|
-
const listed = [...(data.pinnedThreads || []), ...(data.threads || [])];
|
|
33
|
-
const threads = new Map(listed.filter(visible).map(thread => [thread.id, normalizeRecency(thread)]));
|
|
34
|
-
for (const row of rows) {
|
|
35
|
-
if (row.archived || threads.has(row.id)) continue;
|
|
36
|
-
threads.set(row.id, normalizeRecency({ id: row.id, kind: 'codex', hostId: 'local',
|
|
37
|
-
title: row.name || row.title, cwd: row.cwd, projectId: row.project_id,
|
|
38
|
-
updatedAt: row.updated_ms / 1000, recencyAt: row.sort_ms / 1000, status: 'unknown' }));
|
|
39
|
-
}
|
|
40
|
-
const recency = thread => thread.hostId === 'local' && indexed.has(thread.id)
|
|
41
|
-
? indexed.get(thread.id).sort_ms : Number(thread.updatedAt || 0) * 1000;
|
|
42
|
-
return { ...data, pinnedThreads: [], threads: [...threads.values()]
|
|
43
|
-
.sort((a, b) => recency(b) - recency(a)).slice(0, limit) };
|
|
44
|
-
} finally {
|
|
45
|
-
db.close();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { findDesktopPipe } from '../src/platform/windows/desktop-tools.mjs';
|
|
2
|
-
|
|
3
|
-
try {
|
|
4
|
-
const args = process.argv.slice(2);
|
|
5
|
-
if (args.length && (args.length !== 1 || args[0] !== '--wait')) throw new Error('Invalid pipe discovery arguments');
|
|
6
|
-
console.log(await findDesktopPipe({ timeout: args.length ? 120000 : 0 }));
|
|
7
|
-
} catch (error) {
|
|
8
|
-
console.error(error.message);
|
|
9
|
-
process.exitCode = 1;
|
|
10
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
(function (root, factory) {
|
|
2
|
-
if (typeof module === 'object' && module.exports) module.exports = factory;
|
|
3
|
-
else root.createCommunityView = factory;
|
|
4
|
-
})(globalThis, function createCommunityView({ button, page, onOpen, onClose }) {
|
|
5
|
-
function open() {
|
|
6
|
-
page.hidden = false;
|
|
7
|
-
button.setAttribute('aria-current', 'page');
|
|
8
|
-
onOpen?.();
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function close() {
|
|
12
|
-
if (page.hidden) return;
|
|
13
|
-
page.hidden = true;
|
|
14
|
-
button.removeAttribute('aria-current');
|
|
15
|
-
onClose?.();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
button.addEventListener('click', open);
|
|
19
|
-
return { open, close, isOpen: () => !page.hidden };
|
|
20
|
-
});
|