@walkhi/code-relax 0.1.0-beta.30 → 0.1.0-beta.32
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 +95 -89
- package/dist/bin/cleanup-accounts.mjs +63 -63
- package/dist/bin/codex-remote.mjs +374 -367
- package/dist/bin/manage-relay-admin.mjs +20 -20
- package/dist/bin/migrate-relay-db.mjs +109 -109
- package/dist/bin/publish-android-release.mjs +63 -58
- package/dist/bin/publish-harmony-release.mjs +46 -0
- package/dist/bin/release-upload-server.mjs +174 -0
- package/dist/bin/self-relay-admin.mjs +8 -8
- package/dist/bin/self-relay-demo.mjs +33 -33
- package/dist/bin/self-relay-server.mjs +66 -66
- package/dist/build-platform.json +1 -1
- package/dist/get-lan-networks.ps1 +40 -40
- package/dist/get-shared-service-url.ps1 +6 -6
- package/dist/shared/app-server-events.cjs +422 -412
- package/dist/shared/code-browser.cjs +159 -159
- package/dist/shared/file-changes.cjs +31 -31
- package/dist/shared/file-citations.cjs +117 -117
- package/dist/shared/file-mentions.cjs +92 -92
- package/dist/shared/message-delivery.cjs +54 -54
- package/dist/shared/p2p-data-channel.cjs +72 -72
- package/dist/shared/p2p-diagnostics.cjs +28 -28
- package/dist/shared/p2p-file-channel.cjs +78 -74
- package/dist/shared/plan-pricing.cjs +32 -32
- package/dist/shared/question-attention.cjs +69 -0
- package/dist/shared/server-requests.cjs +219 -219
- package/dist/shared/timeline-sync.cjs +173 -173
- package/dist/skills/code-relax/SKILL.md +22 -22
- package/dist/src/archived-threads.mjs +22 -0
- package/dist/src/background-process.mjs +18 -18
- package/dist/src/bounded-process-log.mjs +63 -63
- package/dist/src/celsius-client.mjs +107 -107
- package/dist/src/code-browser.mjs +100 -100
- package/dist/src/desktop-launcher.mjs +46 -46
- package/dist/src/dots-approval-timeline.mjs +99 -99
- package/dist/src/dots-avatar.mjs +42 -42
- package/dist/src/dots-chat-projection.mjs +50 -50
- package/dist/src/dots-chat-server.mjs +461 -461
- package/dist/src/dots-message-images.mjs +319 -319
- package/dist/src/dots-profile.mjs +77 -77
- package/dist/src/dots-room-activity.mjs +94 -94
- package/dist/src/dots-room-client.mjs +135 -135
- package/dist/src/dots-room-projection.mjs +94 -94
- package/dist/src/dots-schedules.mjs +66 -66
- package/dist/src/dots-summary-tasks.mjs +32 -32
- package/dist/src/dots-task-catalog.mjs +430 -430
- package/dist/src/dots-task-files.mjs +52 -52
- package/dist/src/execution-health.mjs +26 -26
- package/dist/src/file-transfer-range.mjs +22 -22
- package/dist/src/file-upload-session.mjs +130 -130
- package/dist/src/hosted-history-app-server.mjs +103 -103
- package/dist/src/hosted-history-cache.mjs +113 -113
- package/dist/src/lan-socket-server.mjs +265 -263
- package/dist/src/managed-app-server.mjs +244 -244
- package/dist/src/message-images.mjs +9 -9
- package/dist/src/message-upload-files.mjs +74 -74
- package/dist/src/onboarding.mjs +12 -12
- package/dist/src/plan-pricing.mjs +47 -47
- package/dist/src/platform/README.md +12 -12
- package/dist/src/platform/codex-protocol.mjs +46 -46
- package/dist/src/platform/codex-runtime.mjs +3 -3
- package/dist/src/platform/credential-store.mjs +3 -3
- package/dist/src/platform/desktop-host.mjs +3 -3
- package/dist/src/platform/desktop-project-registration.mjs +3 -3
- package/dist/src/platform/desktop-project-store.mjs +208 -149
- package/dist/src/platform/file-opener.mjs +9 -0
- package/dist/src/platform/linux/README.md +3 -3
- package/dist/src/platform/linux/codex-runtime.mjs +56 -0
- package/dist/src/platform/linux/credential-store.mjs +28 -0
- package/dist/src/platform/linux/desktop-host.mjs +83 -0
- package/dist/src/platform/linux/desktop-installation.mjs +36 -0
- package/dist/src/platform/linux/desktop-project-registration.mjs +11 -0
- package/dist/src/platform/linux/network-info.mjs +11 -0
- package/dist/src/platform/linux/process.mjs +42 -0
- package/dist/src/platform/linux/project-locations.mjs +17 -0
- package/dist/src/platform/linux/resident-startup.mjs +52 -0
- package/dist/src/platform/linux/retained-runtime.mjs +76 -0
- package/dist/src/platform/linux/service-host.mjs +18 -0
- package/dist/src/platform/linux/system.mjs +49 -0
- package/dist/src/platform/linux/user-environment.mjs +7 -0
- package/dist/src/platform/macos/README.md +3 -3
- package/dist/src/platform/macos/codex-runtime.mjs +125 -125
- package/dist/src/platform/macos/credential-store.mjs +10 -10
- package/dist/src/platform/macos/desktop-host.mjs +82 -82
- package/dist/src/platform/macos/desktop-project-registration.mjs +11 -10
- package/dist/src/platform/macos/network-info.mjs +8 -28
- package/dist/src/platform/macos/project-locations.mjs +8 -8
- package/dist/src/platform/macos/resident-startup.mjs +84 -84
- package/dist/src/platform/macos/retained-runtime.mjs +127 -127
- package/dist/src/platform/macos/service-host.mjs +18 -18
- package/dist/src/platform/macos/system.mjs +29 -29
- package/dist/src/platform/macos/user-environment.mjs +7 -7
- package/dist/src/platform/native-package.mjs +58 -46
- package/dist/src/platform/network-info.mjs +3 -3
- package/dist/src/platform/path-identity.mjs +5 -5
- package/dist/src/platform/posix/network-candidates.mjs +23 -0
- package/dist/src/platform/posix/runtime-paths.mjs +25 -25
- package/dist/src/platform/project-locations.mjs +3 -3
- package/dist/src/platform/runtime-paths.mjs +8 -8
- package/dist/src/platform/select.mjs +5 -5
- package/dist/src/platform/service-host.mjs +3 -3
- package/dist/src/platform/user-environment.mjs +3 -3
- package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
- package/dist/src/platform/windows/activate-desktop.ps1 +48 -48
- package/dist/src/platform/windows/background-process.mjs +30 -30
- package/dist/src/platform/windows/codex-runtime.mjs +215 -215
- package/dist/src/platform/windows/credential-store.mjs +27 -27
- package/dist/src/platform/windows/credential-store.ps1 +49 -49
- package/dist/src/platform/windows/desktop-helper.mjs +179 -179
- package/dist/src/platform/windows/desktop-host.mjs +58 -58
- package/dist/src/platform/windows/desktop-info.ps1 +14 -14
- package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +21 -21
- package/dist/src/platform/windows/dots-profile.mjs +49 -49
- package/dist/src/platform/windows/launch-worker.mjs +41 -41
- package/dist/src/platform/windows/network-info.mjs +34 -34
- package/dist/src/platform/windows/project-locations.mjs +16 -16
- package/dist/src/platform/windows/resident-start.js +20 -20
- package/dist/src/platform/windows/resident-start.ps1 +7 -7
- package/dist/src/platform/windows/resident-startup.ps1 +83 -83
- package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
- package/dist/src/platform/windows/runtime-paths.mjs +20 -20
- package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
- package/dist/src/platform/windows/service-host.mjs +31 -31
- package/dist/src/platform/windows/start-hidden-console.ps1 +49 -49
- package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
- package/dist/src/platform/windows/user-environment.mjs +50 -50
- package/dist/src/platform/windows/user-environment.ps1 +41 -41
- package/dist/src/project-archive.mjs +30 -0
- package/dist/src/project-create.mjs +121 -119
- package/dist/src/project-management.mjs +59 -69
- package/dist/src/project-remove.mjs +55 -0
- package/dist/src/project-update.mjs +56 -0
- package/dist/src/runtime-discovery.mjs +47 -47
- package/dist/src/self-relay/account-cleanup.mjs +30 -30
- package/dist/src/self-relay/account-connections.mjs +52 -52
- package/dist/src/self-relay/admin-cloud.mjs +101 -101
- package/dist/src/self-relay/admin-page.mjs +80 -80
- package/dist/src/self-relay/admin-server.mjs +74 -74
- package/dist/src/self-relay/admin-state.mjs +127 -127
- package/dist/src/self-relay/admin-style.mjs +14 -14
- package/dist/src/self-relay/android-releases.mjs +61 -61
- package/dist/src/self-relay/client.mjs +61 -61
- package/dist/src/self-relay/connector.mjs +439 -438
- package/dist/src/self-relay/debug-allowlist.mjs +33 -33
- package/dist/src/self-relay/demo.mjs +37 -37
- package/dist/src/self-relay/device-info.mjs +27 -0
- package/dist/src/self-relay/device-store.mjs +92 -84
- package/dist/src/self-relay/diagnostics.mjs +54 -54
- package/dist/src/self-relay/email-accounts.mjs +183 -183
- package/dist/src/self-relay/file-wire.mjs +13 -13
- package/dist/src/self-relay/lan-authorizations.mjs +71 -42
- package/dist/src/self-relay/lifecycle.mjs +264 -250
- package/dist/src/self-relay/long-pairing.mjs +40 -40
- package/dist/src/self-relay/mysql-state.mjs +118 -105
- package/dist/src/self-relay/p2p-probe.mjs +159 -159
- package/dist/src/self-relay/privacy-page.mjs +26 -26
- package/dist/src/self-relay/public-releases.mjs +52 -0
- package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
- package/dist/src/self-relay/server.mjs +1006 -932
- package/dist/src/self-relay/usage-daily.mjs +55 -55
- package/dist/src/self-relay/usage-meter.mjs +141 -141
- package/dist/src/self-relay/windows-route-helper.mjs +75 -75
- package/dist/src/self-relay/wire.mjs +36 -36
- package/dist/src/server.mjs +1861 -1778
- package/dist/src/service-control.mjs +82 -82
- package/dist/src/service-doctor.mjs +110 -110
- package/dist/src/service-health.mjs +33 -33
- package/dist/src/service-install.mjs +91 -91
- package/dist/src/service-lifecycle.mjs +138 -138
- package/dist/src/service-restart.mjs +81 -81
- package/dist/src/shared-app-server.mjs +615 -611
- package/dist/src/shared-catalog.mjs +132 -132
- package/dist/src/shared-recovery.mjs +146 -146
- package/dist/src/shared-thread-stream.mjs +164 -164
- package/dist/src/skill-install.mjs +51 -51
- package/dist/src/thread-title-generation.mjs +150 -150
- package/dist/src/turn-model-history.mjs +47 -47
- package/dist/src/usage-reset.mjs +17 -17
- package/dist/src/workstation-downloads.mjs +120 -120
- package/dist/tools/rtc-route-helper.ps1 +99 -99
- package/dist/tools/start-rtc-route-helper.ps1 +15 -15
- package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
- package/dist/web/vendor/markdown-it.umd.min.js +13 -13
- package/package.json +7 -6
- package/tools/postinstall.mjs +127 -127
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
import events from '../shared/app-server-events.cjs';
|
|
2
|
-
import { projectSummary } from './project-management.mjs';
|
|
3
|
-
|
|
4
|
-
export function sharedThreadProjectId(thread, projects) {
|
|
5
|
-
return events.catalogThreadProjectId(thread, projects);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function dotsThreadProjectId(thread, projects) {
|
|
9
|
-
if (thread.projectId && projects.some(project => project.id === thread.projectId)) return thread.projectId;
|
|
10
|
-
if (thread.environments?.length) {
|
|
11
|
-
for (const environment of thread.environments) {
|
|
12
|
-
const roots = Array.isArray(environment.runtimeWorkspaceRoots) ? environment.runtimeWorkspaceRoots : [];
|
|
13
|
-
for (const root of roots) {
|
|
14
|
-
if (!root) continue;
|
|
15
|
-
const projectId = events.threadProjectId({ cwd: root }, projects);
|
|
16
|
-
if (projectId) return projectId;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
// Display grouping may use the execution directory when workspace roots
|
|
20
|
-
// do not map to a known project. This never changes the thread's workspace
|
|
21
|
-
// permissions or its native project assignment.
|
|
22
|
-
for (const environment of thread.environments) {
|
|
23
|
-
if (!environment.cwd) continue;
|
|
24
|
-
const projectId = events.threadProjectId({ cwd: environment.cwd }, projects);
|
|
25
|
-
if (projectId) return projectId;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return events.threadProjectId({ cwd: thread.cwd }, projects);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Mobile catalog order comes entirely from app-server authority, independent of Desktop UI state.
|
|
32
|
-
export class SharedCatalog {
|
|
33
|
-
constructor(server, dotsServer, dotsChat) {
|
|
34
|
-
this.server = server; this.dotsServer = dotsServer; this.dotsChat = dotsChat;
|
|
35
|
-
this.states = new Map();
|
|
36
|
-
dotsServer?.on('disconnected', () => this.states.clear());
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
threadState(thread, refresh = false) {
|
|
40
|
-
const client = thread.threadSource === 'aeon' ? this.dotsChat : this.dotsServer;
|
|
41
|
-
const observed = client?.threads.get(thread.id);
|
|
42
|
-
if (client?.ready && observed && !observed.needsReload && !observed.hydrating && !observed.refresh) {
|
|
43
|
-
const current = observed.thread;
|
|
44
|
-
return { chatRevision: current.chatRevision,
|
|
45
|
-
...(thread.threadSource === 'aeon' ? { chatMessageRevision: current.chatMessageRevision, updatedAt: current.updatedAt }
|
|
46
|
-
: { lastTurnStatus: current.turns.at(-1)?.status || null }) };
|
|
47
|
-
}
|
|
48
|
-
const identity = this.dotsServer.profileIdentity;
|
|
49
|
-
let entry = this.states.get(thread.id);
|
|
50
|
-
const version = JSON.stringify([thread.updatedAt, thread.status]);
|
|
51
|
-
if (!entry) { entry = { value: {} }; this.states.set(thread.id, entry); }
|
|
52
|
-
if (!entry.work && (refresh || entry.version !== version || Date.now() - (entry.at || 0) >= 30000)) {
|
|
53
|
-
const work = (async () => {
|
|
54
|
-
try {
|
|
55
|
-
const value = thread.threadSource === 'aeon'
|
|
56
|
-
? await this.dotsChat.latestChatState(thread.id)
|
|
57
|
-
: await this.dotsServer.latestTaskState(thread.id, refresh || entry.version !== version);
|
|
58
|
-
if (identity !== this.dotsServer.profileIdentity || this.states.get(thread.id) !== entry) return;
|
|
59
|
-
const changed = JSON.stringify(entry.value) !== JSON.stringify(value);
|
|
60
|
-
entry.value = value; entry.version = version; entry.at = Date.now();
|
|
61
|
-
if (changed) this.dotsServer.emit('notification', {
|
|
62
|
-
method: 'thread/status/changed', params: { threadId: thread.id, ...value },
|
|
63
|
-
});
|
|
64
|
-
} catch (error) {
|
|
65
|
-
entry.version = version; entry.at = Date.now();
|
|
66
|
-
console.error('云端目录修订读取失败:', thread.id, error.message);
|
|
67
|
-
}
|
|
68
|
-
})().finally(() => { if (entry.work === work) entry.work = null; });
|
|
69
|
-
entry.work = work;
|
|
70
|
-
}
|
|
71
|
-
// The live chat updates its revision before notifying stream listeners.
|
|
72
|
-
// A directory refresh can still be in flight with an older cached value;
|
|
73
|
-
// never return that value after the rendered timeline has advanced.
|
|
74
|
-
if (thread.threadSource === 'aeon') {
|
|
75
|
-
const current = this.dotsChat.threads.get(thread.id)?.thread;
|
|
76
|
-
return current?.chatRevision === undefined ? {} : {
|
|
77
|
-
chatRevision: current.chatRevision, chatMessageRevision: current.chatMessageRevision, updatedAt: current.updatedAt,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
return entry.value;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async projects() {
|
|
84
|
-
const native = await this.server.projects();
|
|
85
|
-
const projects = native.map(projectSummary);
|
|
86
|
-
return { projects, native };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async threads(limit, archived = false, scope = 'all', refresh = false) {
|
|
90
|
-
const { projects, native } = await this.projects();
|
|
91
|
-
const threads = []; let cursor;
|
|
92
|
-
if (scope !== 'durable') do {
|
|
93
|
-
const page = await this.server.request('thread/list', { limit: Math.min(100, limit - threads.length),
|
|
94
|
-
sortKey: 'recency_at', sortDirection: 'desc', archived, ...(cursor ? { cursor } : {}) });
|
|
95
|
-
threads.push(...page.data); cursor = page.nextCursor;
|
|
96
|
-
} while (cursor && threads.length < limit);
|
|
97
|
-
const summaries = threads.map(thread => ({ id: thread.id, kind: 'codex', hostId: 'local',
|
|
98
|
-
...(thread.model ? { model: thread.model, reasoningEffort: thread.reasoningEffort } : {}),
|
|
99
|
-
title: thread.name || thread.preview || '', cwd: thread.cwd,
|
|
100
|
-
projectId: sharedThreadProjectId(thread, native), updatedAt: thread.updatedAt,
|
|
101
|
-
recencyAt: thread.recencyAt ?? thread.updatedAt,
|
|
102
|
-
status: typeof thread.status === 'string' ? thread.status : thread.status?.type || 'unknown',
|
|
103
|
-
activeFlags: threadActiveFlags(thread.status) }));
|
|
104
|
-
let catalogError;
|
|
105
|
-
if (scope !== 'local' && this.dotsServer) {
|
|
106
|
-
try {
|
|
107
|
-
const dots = await this.dotsServer.catalog(archived, refresh);
|
|
108
|
-
summaries.push(...dots.map(thread => ({ id: thread.id, kind: thread.threadSource === 'aeon' ? 'dots' : 'codex', hostId: 'durable',
|
|
109
|
-
threadSource: thread.threadSource,
|
|
110
|
-
...(thread.threadSource === 'aeon_child' && thread.model ? { model: thread.model, reasoningEffort: thread.reasoningEffort } : {}),
|
|
111
|
-
...this.threadState(thread, refresh),
|
|
112
|
-
...(thread.threadSource === 'aeon' ? { avatar: thread.avatar || '', color: thread.color || '' } : {}),
|
|
113
|
-
title: thread.name || (thread.threadSource === 'aeon' ? 'Dots' : thread.preview || ''), cwd: thread.cwd,
|
|
114
|
-
projectId: thread.threadSource === 'aeon' ? null : dotsThreadProjectId(thread, native),
|
|
115
|
-
updatedAt: thread.threadSource === 'aeon' ? this.dotsChat.threads.get(thread.id)?.thread.updatedAt || 0 : thread.updatedAt,
|
|
116
|
-
recencyAt: thread.recencyAt ?? thread.updatedAt,
|
|
117
|
-
status: thread.threadSource === 'aeon' ? this.dotsChat?.threads.get(thread.id)?.thread.status || 'unknown'
|
|
118
|
-
: typeof thread.status === 'string' ? thread.status : thread.status?.type || 'unknown',
|
|
119
|
-
activeFlags: thread.threadSource === 'aeon' ? [] : threadActiveFlags(thread.status) })));
|
|
120
|
-
} catch (error) { catalogError = `Dots 对话读取失败:${error.message}`; }
|
|
121
|
-
}
|
|
122
|
-
summaries.sort((a, b) => b.recencyAt - a.recencyAt);
|
|
123
|
-
return { catalogScope: scope, projects, pinnedThreads: [], threads: [...summaries.filter(thread => thread.kind === 'dots'),
|
|
124
|
-
...summaries.filter(thread => thread.kind !== 'dots').slice(0, limit)], ...(catalogError ? { catalogError } : {}) };
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function threadActiveFlags(status) {
|
|
129
|
-
return status && typeof status === 'object' && Array.isArray(status.activeFlags)
|
|
130
|
-
? status.activeFlags.filter(flag => typeof flag === 'string')
|
|
131
|
-
: [];
|
|
132
|
-
}
|
|
1
|
+
import events from '../shared/app-server-events.cjs';
|
|
2
|
+
import { projectSummary } from './project-management.mjs';
|
|
3
|
+
|
|
4
|
+
export function sharedThreadProjectId(thread, projects) {
|
|
5
|
+
return events.catalogThreadProjectId(thread, projects);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function dotsThreadProjectId(thread, projects) {
|
|
9
|
+
if (thread.projectId && projects.some(project => project.id === thread.projectId)) return thread.projectId;
|
|
10
|
+
if (thread.environments?.length) {
|
|
11
|
+
for (const environment of thread.environments) {
|
|
12
|
+
const roots = Array.isArray(environment.runtimeWorkspaceRoots) ? environment.runtimeWorkspaceRoots : [];
|
|
13
|
+
for (const root of roots) {
|
|
14
|
+
if (!root) continue;
|
|
15
|
+
const projectId = events.threadProjectId({ cwd: root }, projects);
|
|
16
|
+
if (projectId) return projectId;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Display grouping may use the execution directory when workspace roots
|
|
20
|
+
// do not map to a known project. This never changes the thread's workspace
|
|
21
|
+
// permissions or its native project assignment.
|
|
22
|
+
for (const environment of thread.environments) {
|
|
23
|
+
if (!environment.cwd) continue;
|
|
24
|
+
const projectId = events.threadProjectId({ cwd: environment.cwd }, projects);
|
|
25
|
+
if (projectId) return projectId;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return events.threadProjectId({ cwd: thread.cwd }, projects);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Mobile catalog order comes entirely from app-server authority, independent of Desktop UI state.
|
|
32
|
+
export class SharedCatalog {
|
|
33
|
+
constructor(server, dotsServer, dotsChat) {
|
|
34
|
+
this.server = server; this.dotsServer = dotsServer; this.dotsChat = dotsChat;
|
|
35
|
+
this.states = new Map();
|
|
36
|
+
dotsServer?.on('disconnected', () => this.states.clear());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
threadState(thread, refresh = false) {
|
|
40
|
+
const client = thread.threadSource === 'aeon' ? this.dotsChat : this.dotsServer;
|
|
41
|
+
const observed = client?.threads.get(thread.id);
|
|
42
|
+
if (client?.ready && observed && !observed.needsReload && !observed.hydrating && !observed.refresh) {
|
|
43
|
+
const current = observed.thread;
|
|
44
|
+
return { chatRevision: current.chatRevision,
|
|
45
|
+
...(thread.threadSource === 'aeon' ? { chatMessageRevision: current.chatMessageRevision, updatedAt: current.updatedAt }
|
|
46
|
+
: { lastTurnStatus: current.turns.at(-1)?.status || null }) };
|
|
47
|
+
}
|
|
48
|
+
const identity = this.dotsServer.profileIdentity;
|
|
49
|
+
let entry = this.states.get(thread.id);
|
|
50
|
+
const version = JSON.stringify([thread.updatedAt, thread.status]);
|
|
51
|
+
if (!entry) { entry = { value: {} }; this.states.set(thread.id, entry); }
|
|
52
|
+
if (!entry.work && (refresh || entry.version !== version || Date.now() - (entry.at || 0) >= 30000)) {
|
|
53
|
+
const work = (async () => {
|
|
54
|
+
try {
|
|
55
|
+
const value = thread.threadSource === 'aeon'
|
|
56
|
+
? await this.dotsChat.latestChatState(thread.id)
|
|
57
|
+
: await this.dotsServer.latestTaskState(thread.id, refresh || entry.version !== version);
|
|
58
|
+
if (identity !== this.dotsServer.profileIdentity || this.states.get(thread.id) !== entry) return;
|
|
59
|
+
const changed = JSON.stringify(entry.value) !== JSON.stringify(value);
|
|
60
|
+
entry.value = value; entry.version = version; entry.at = Date.now();
|
|
61
|
+
if (changed) this.dotsServer.emit('notification', {
|
|
62
|
+
method: 'thread/status/changed', params: { threadId: thread.id, ...value },
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
entry.version = version; entry.at = Date.now();
|
|
66
|
+
console.error('云端目录修订读取失败:', thread.id, error.message);
|
|
67
|
+
}
|
|
68
|
+
})().finally(() => { if (entry.work === work) entry.work = null; });
|
|
69
|
+
entry.work = work;
|
|
70
|
+
}
|
|
71
|
+
// The live chat updates its revision before notifying stream listeners.
|
|
72
|
+
// A directory refresh can still be in flight with an older cached value;
|
|
73
|
+
// never return that value after the rendered timeline has advanced.
|
|
74
|
+
if (thread.threadSource === 'aeon') {
|
|
75
|
+
const current = this.dotsChat.threads.get(thread.id)?.thread;
|
|
76
|
+
return current?.chatRevision === undefined ? {} : {
|
|
77
|
+
chatRevision: current.chatRevision, chatMessageRevision: current.chatMessageRevision, updatedAt: current.updatedAt,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return entry.value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async projects() {
|
|
84
|
+
const native = await this.server.projects();
|
|
85
|
+
const projects = native.map(projectSummary);
|
|
86
|
+
return { projects, native };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async threads(limit, archived = false, scope = 'all', refresh = false) {
|
|
90
|
+
const { projects, native } = await this.projects();
|
|
91
|
+
const threads = []; let cursor;
|
|
92
|
+
if (scope !== 'durable') do {
|
|
93
|
+
const page = await this.server.request('thread/list', { limit: Math.min(100, limit - threads.length),
|
|
94
|
+
sortKey: 'recency_at', sortDirection: 'desc', archived, ...(cursor ? { cursor } : {}) });
|
|
95
|
+
threads.push(...page.data); cursor = page.nextCursor;
|
|
96
|
+
} while (cursor && threads.length < limit);
|
|
97
|
+
const summaries = threads.map(thread => ({ id: thread.id, kind: 'codex', hostId: 'local',
|
|
98
|
+
...(thread.model ? { model: thread.model, reasoningEffort: thread.reasoningEffort } : {}),
|
|
99
|
+
title: thread.name || thread.preview || '', cwd: thread.cwd,
|
|
100
|
+
projectId: sharedThreadProjectId(thread, native), updatedAt: thread.updatedAt,
|
|
101
|
+
recencyAt: thread.recencyAt ?? thread.updatedAt,
|
|
102
|
+
status: typeof thread.status === 'string' ? thread.status : thread.status?.type || 'unknown',
|
|
103
|
+
activeFlags: threadActiveFlags(thread.status) }));
|
|
104
|
+
let catalogError;
|
|
105
|
+
if (scope !== 'local' && this.dotsServer) {
|
|
106
|
+
try {
|
|
107
|
+
const dots = await this.dotsServer.catalog(archived, refresh);
|
|
108
|
+
summaries.push(...dots.map(thread => ({ id: thread.id, kind: thread.threadSource === 'aeon' ? 'dots' : 'codex', hostId: 'durable',
|
|
109
|
+
threadSource: thread.threadSource,
|
|
110
|
+
...(thread.threadSource === 'aeon_child' && thread.model ? { model: thread.model, reasoningEffort: thread.reasoningEffort } : {}),
|
|
111
|
+
...this.threadState(thread, refresh),
|
|
112
|
+
...(thread.threadSource === 'aeon' ? { avatar: thread.avatar || '', color: thread.color || '' } : {}),
|
|
113
|
+
title: thread.name || (thread.threadSource === 'aeon' ? 'Dots' : thread.preview || ''), cwd: thread.cwd,
|
|
114
|
+
projectId: thread.threadSource === 'aeon' ? null : dotsThreadProjectId(thread, native),
|
|
115
|
+
updatedAt: thread.threadSource === 'aeon' ? this.dotsChat.threads.get(thread.id)?.thread.updatedAt || 0 : thread.updatedAt,
|
|
116
|
+
recencyAt: thread.recencyAt ?? thread.updatedAt,
|
|
117
|
+
status: thread.threadSource === 'aeon' ? this.dotsChat?.threads.get(thread.id)?.thread.status || 'unknown'
|
|
118
|
+
: typeof thread.status === 'string' ? thread.status : thread.status?.type || 'unknown',
|
|
119
|
+
activeFlags: thread.threadSource === 'aeon' ? [] : threadActiveFlags(thread.status) })));
|
|
120
|
+
} catch (error) { catalogError = `Dots 对话读取失败:${error.message}`; }
|
|
121
|
+
}
|
|
122
|
+
summaries.sort((a, b) => b.recencyAt - a.recencyAt);
|
|
123
|
+
return { catalogScope: scope, projects, pinnedThreads: [], threads: [...summaries.filter(thread => thread.kind === 'dots'),
|
|
124
|
+
...summaries.filter(thread => thread.kind !== 'dots').slice(0, limit)], ...(catalogError ? { catalogError } : {}) };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function threadActiveFlags(status) {
|
|
129
|
+
return status && typeof status === 'object' && Array.isArray(status.activeFlags)
|
|
130
|
+
? status.activeFlags.filter(flag => typeof flag === 'string')
|
|
131
|
+
: [];
|
|
132
|
+
}
|
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { readRecord, writeRecord, lifecycle } from './service-lifecycle.mjs';
|
|
7
|
-
import { startBackground } from './background-process.mjs';
|
|
8
|
-
import { desktopHost } from './platform/desktop-host.mjs';
|
|
9
|
-
import * as serviceHost from './platform/service-host.mjs';
|
|
10
|
-
import { launchDesktop, portAvailable } from './desktop-launcher.mjs';
|
|
11
|
-
import { configureManagedEnvironment } from './platform/user-environment.mjs';
|
|
12
|
-
|
|
13
|
-
const script = fileURLToPath(import.meta.url);
|
|
14
|
-
const recordPath = root => path.join(root, 'shared-recovery.json');
|
|
15
|
-
const lockPath = root => path.join(root, 'shared-recovery.lock');
|
|
16
|
-
const sameProcess = (a, b) => a && b && a.pid === b.pid && a.created === b.created && a.executable === b.executable;
|
|
17
|
-
const active = value => value && ['scheduled', 'running'].includes(value.status);
|
|
18
|
-
const recoverySteps = () => [
|
|
19
|
-
['checking', '检查启动条件'], ['stopping-desktop', '关闭当前 Codex'],
|
|
20
|
-
['starting-app-server', '确认 Codex 内核'], ['starting-desktop', '重新打开 Codex'],
|
|
21
|
-
['checking-execution', '确认移动端控制就绪'],
|
|
22
|
-
].map(([id, label]) => ({ id, label, status: 'pending' }));
|
|
23
|
-
export const recoveryWarning = 'Codex Desktop 需要重新打开,才能接入同一个 Codex 内核。移动端连接和 Codex 内核会保持运行;旧 Desktop 实例中的任务可能中断,未发送内容请先保存。切换不会清空项目、对话历史、设置或设备授权。';
|
|
24
|
-
|
|
25
|
-
export async function recoveryStatus(stateRoot, host = desktopHost) {
|
|
26
|
-
if (host.desktopIntegration === false) return { status: 'unsupported', message: '此平台尚不支持 Desktop 自动接入。' };
|
|
27
|
-
const record = readRecord(recordPath(stateRoot));
|
|
28
|
-
if (!record) return { status: 'none', message: '尚未执行 Desktop 接入流程。' };
|
|
29
|
-
const lostWorker = active(record) && (record.worker
|
|
30
|
-
? !sameProcess(record.worker, await host.process(record.worker.pid))
|
|
31
|
-
: Date.now() - Date.parse(record.startedAt) > 30000);
|
|
32
|
-
if (lostWorker) {
|
|
33
|
-
record.status = 'failed'; record.error = '独立恢复进程已退出,恢复未完成。';
|
|
34
|
-
record.steps = record.steps?.map(step => ({ ...step, status: step.status === 'running' ? 'failed' : step.status }));
|
|
35
|
-
writeRecord(recordPath(stateRoot), record);
|
|
36
|
-
if (readRecord(lockPath(stateRoot))?.operationId === record.operationId) fs.rmSync(lockPath(stateRoot));
|
|
37
|
-
}
|
|
38
|
-
const { worker, oldService, desktop, ...publicRecord } = record;
|
|
39
|
-
return publicRecord;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function planRecovery(stateRoot, host, available = portAvailable) {
|
|
43
|
-
const info = await host.inspect();
|
|
44
|
-
const action = await host.sharedServiceUrl() ? 'restart' : 'switch';
|
|
45
|
-
if (!info.desktop) throw new Error('未找到 Codex Desktop;CLI 用户无需执行 Desktop 接入。');
|
|
46
|
-
const record = readRecord(path.join(stateRoot, 'managed-app-server.json'));
|
|
47
|
-
const actual = record ? await host.process(record.pid) : null;
|
|
48
|
-
if (actual && (!sameProcess(record, actual) || !actual.command?.includes('app-server') || !actual.command.includes('ws://127.0.0.1:45839'))) {
|
|
49
|
-
throw new Error('Codex 内核进程身份与记录不符,未结束任何进程。');
|
|
50
|
-
}
|
|
51
|
-
if (!actual && !await available()) throw new Error('45839 被未识别进程占用,未关闭 Desktop 或结束任何进程。');
|
|
52
|
-
return { desktop: info.desktop, action };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function requestRecovery(stateRoot, { confirmed, operationId = randomUUID() } = {}, host = desktopHost) {
|
|
56
|
-
if (host.desktopIntegration === false) throw new Error('此平台尚不支持 Desktop 自动接入。');
|
|
57
|
-
if (confirmed !== true) throw new Error(recoveryWarning + ' 请明确确认后再执行。');
|
|
58
|
-
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(operationId)) throw new Error('无效的恢复操作编号。');
|
|
59
|
-
fs.mkdirSync(stateRoot, { recursive: true });
|
|
60
|
-
const previous = await recoveryStatus(stateRoot, host);
|
|
61
|
-
if (previous.operationId === operationId) return previous;
|
|
62
|
-
if (active(previous)) throw new Error('已有恢复操作正在执行,请查看恢复状态。');
|
|
63
|
-
const planned = await planRecovery(stateRoot, host);
|
|
64
|
-
let lock;
|
|
65
|
-
try { lock = fs.openSync(lockPath(stateRoot), 'wx'); }
|
|
66
|
-
catch (error) { if (error.code === 'EEXIST') throw new Error('已有恢复操作,请先查看恢复状态。'); throw error; }
|
|
67
|
-
fs.writeFileSync(lock, JSON.stringify({ operationId })); fs.closeSync(lock);
|
|
68
|
-
const record = { operationId, status: 'scheduled', stage: 'prepared', startedAt: new Date().toISOString(),
|
|
69
|
-
message: 'Codex 连接请求已接受,电脑管理连接保持运行。', steps: recoverySteps(), ...planned };
|
|
70
|
-
try {
|
|
71
|
-
writeRecord(recordPath(stateRoot), record);
|
|
72
|
-
const log = path.join(stateRoot, 'shared-recovery.log');
|
|
73
|
-
const pid = await startBackground(process.execPath, [script, '--worker', stateRoot, operationId], process.env, [log, log]);
|
|
74
|
-
const worker = await host.process(pid);
|
|
75
|
-
if (!worker) throw new Error('独立恢复进程未启动,请查看 shared-recovery.log。');
|
|
76
|
-
// Publish the process identity before allowing the worker to begin disruption.
|
|
77
|
-
writeRecord(recordPath(stateRoot), { ...record, worker });
|
|
78
|
-
return { operationId, status: record.status, message: record.message };
|
|
79
|
-
} catch (error) {
|
|
80
|
-
writeRecord(recordPath(stateRoot), { ...record, status: 'failed', error: error.message });
|
|
81
|
-
fs.rmSync(lockPath(stateRoot), { force: true }); throw error;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function runRecovery(stateRoot, operationId, dependencies = {}) {
|
|
86
|
-
const host = dependencies.host || desktopHost;
|
|
87
|
-
const service = dependencies.service || lifecycle({ stateRoot, host: serviceHost });
|
|
88
|
-
const available = dependencies.available || portAvailable;
|
|
89
|
-
const start = dependencies.launch || launchDesktop;
|
|
90
|
-
let record = readRecord(recordPath(stateRoot));
|
|
91
|
-
if (record?.operationId !== operationId || readRecord(lockPath(stateRoot))?.operationId !== operationId) throw new Error('恢复实例已失效。');
|
|
92
|
-
record.steps ||= recoverySteps();
|
|
93
|
-
const stage = name => {
|
|
94
|
-
record = { ...record, status: 'running', stage: name, steps: record.steps.map(step => ({ ...step,
|
|
95
|
-
status: step.id === name ? 'running' : step.status === 'running' ? 'completed' : step.status })) };
|
|
96
|
-
writeRecord(recordPath(stateRoot), record);
|
|
97
|
-
};
|
|
98
|
-
const waitFor = async (check, message) => {
|
|
99
|
-
const deadline = Date.now() + (dependencies.timeout ?? 10000);
|
|
100
|
-
while (!await check()) { if (Date.now() >= deadline) throw new Error(message); await delay(100); }
|
|
101
|
-
};
|
|
102
|
-
try {
|
|
103
|
-
stage('checking');
|
|
104
|
-
const planned = await planRecovery(stateRoot, host, available);
|
|
105
|
-
await configureManagedEnvironment(stateRoot);
|
|
106
|
-
record = { ...record, ...planned };
|
|
107
|
-
if (fs.existsSync(path.join(stateRoot, 'desktop-launch.lock'))) throw new Error('Desktop 正在启动,未与启动操作并行恢复。');
|
|
108
|
-
stage('stopping-desktop'); await host.closeDesktop(record.desktop);
|
|
109
|
-
await waitFor(async () => !(await host.inspect()).running, '当前 Codex Desktop 未退出,未继续接入 Codex 内核。');
|
|
110
|
-
stage('starting-app-server'); await start({ stateRoot, host, recoveryId: operationId, progress: stage });
|
|
111
|
-
stage('checking-execution');
|
|
112
|
-
await service.start(); // Refresh the execution adapter; resident Bridge retains its PID and sockets.
|
|
113
|
-
await waitFor(async () => {
|
|
114
|
-
const health = await service.status();
|
|
115
|
-
return health.running && health.mode === 'lan-shared' && health.execution?.ready === true;
|
|
116
|
-
}, 'Codex 内核未就绪;桌面服务和中继连接仍保留。');
|
|
117
|
-
record = { ...record, status: 'completed', stage: 'ready', steps: record.steps.map(step => ({ ...step, status: 'completed' })),
|
|
118
|
-
message: 'Codex Desktop 已接入 Codex 内核;移动端控制保持就绪,原任务未自动重发。' };
|
|
119
|
-
} catch (error) { record = { ...record, status: 'failed', error: error.message,
|
|
120
|
-
steps: record.steps.map(step => ({ ...step, status: step.status === 'running' ? 'failed' : step.status })),
|
|
121
|
-
message: 'Codex Desktop 未完成接入,已停止后续步骤。' }; }
|
|
122
|
-
finally {
|
|
123
|
-
record.finishedAt = new Date().toISOString(); writeRecord(recordPath(stateRoot), record);
|
|
124
|
-
if (readRecord(lockPath(stateRoot))?.operationId === operationId) fs.rmSync(lockPath(stateRoot));
|
|
125
|
-
}
|
|
126
|
-
return record;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (process.argv[1] && path.resolve(process.argv[1]) === script && process.argv[2] === '--worker') {
|
|
130
|
-
const [stateRoot, operationId] = process.argv.slice(3);
|
|
131
|
-
(async () => {
|
|
132
|
-
const deadline = Date.now() + 15000;
|
|
133
|
-
while (true) {
|
|
134
|
-
const record = readRecord(recordPath(stateRoot));
|
|
135
|
-
if (record?.operationId !== operationId || record.status !== 'scheduled') throw new Error('恢复调度已失效。');
|
|
136
|
-
if (record.worker?.pid === process.pid) break;
|
|
137
|
-
if (Date.now() >= deadline) throw new Error('恢复启动交接超时。');
|
|
138
|
-
await delay(100);
|
|
139
|
-
}
|
|
140
|
-
// A deliberate handoff window lets the caller display the accepted operation ID.
|
|
141
|
-
await delay(3000);
|
|
142
|
-
const result = await runRecovery(stateRoot, operationId);
|
|
143
|
-
console.log(JSON.stringify({ operationId, status: result.status, stage: result.stage, error: result.error }));
|
|
144
|
-
process.exit(result.status === 'completed' ? 0 : 1);
|
|
145
|
-
})().catch(error => { console.error(error.message); process.exit(1); });
|
|
146
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { readRecord, writeRecord, lifecycle } from './service-lifecycle.mjs';
|
|
7
|
+
import { startBackground } from './background-process.mjs';
|
|
8
|
+
import { desktopHost } from './platform/desktop-host.mjs';
|
|
9
|
+
import * as serviceHost from './platform/service-host.mjs';
|
|
10
|
+
import { launchDesktop, portAvailable } from './desktop-launcher.mjs';
|
|
11
|
+
import { configureManagedEnvironment } from './platform/user-environment.mjs';
|
|
12
|
+
|
|
13
|
+
const script = fileURLToPath(import.meta.url);
|
|
14
|
+
const recordPath = root => path.join(root, 'shared-recovery.json');
|
|
15
|
+
const lockPath = root => path.join(root, 'shared-recovery.lock');
|
|
16
|
+
const sameProcess = (a, b) => a && b && a.pid === b.pid && a.created === b.created && a.executable === b.executable;
|
|
17
|
+
const active = value => value && ['scheduled', 'running'].includes(value.status);
|
|
18
|
+
const recoverySteps = () => [
|
|
19
|
+
['checking', '检查启动条件'], ['stopping-desktop', '关闭当前 Codex'],
|
|
20
|
+
['starting-app-server', '确认 Codex 内核'], ['starting-desktop', '重新打开 Codex'],
|
|
21
|
+
['checking-execution', '确认移动端控制就绪'],
|
|
22
|
+
].map(([id, label]) => ({ id, label, status: 'pending' }));
|
|
23
|
+
export const recoveryWarning = 'Codex Desktop 需要重新打开,才能接入同一个 Codex 内核。移动端连接和 Codex 内核会保持运行;旧 Desktop 实例中的任务可能中断,未发送内容请先保存。切换不会清空项目、对话历史、设置或设备授权。';
|
|
24
|
+
|
|
25
|
+
export async function recoveryStatus(stateRoot, host = desktopHost) {
|
|
26
|
+
if (host.desktopIntegration === false) return { status: 'unsupported', message: '此平台尚不支持 Desktop 自动接入。' };
|
|
27
|
+
const record = readRecord(recordPath(stateRoot));
|
|
28
|
+
if (!record) return { status: 'none', message: '尚未执行 Desktop 接入流程。' };
|
|
29
|
+
const lostWorker = active(record) && (record.worker
|
|
30
|
+
? !sameProcess(record.worker, await host.process(record.worker.pid))
|
|
31
|
+
: Date.now() - Date.parse(record.startedAt) > 30000);
|
|
32
|
+
if (lostWorker) {
|
|
33
|
+
record.status = 'failed'; record.error = '独立恢复进程已退出,恢复未完成。';
|
|
34
|
+
record.steps = record.steps?.map(step => ({ ...step, status: step.status === 'running' ? 'failed' : step.status }));
|
|
35
|
+
writeRecord(recordPath(stateRoot), record);
|
|
36
|
+
if (readRecord(lockPath(stateRoot))?.operationId === record.operationId) fs.rmSync(lockPath(stateRoot));
|
|
37
|
+
}
|
|
38
|
+
const { worker, oldService, desktop, ...publicRecord } = record;
|
|
39
|
+
return publicRecord;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function planRecovery(stateRoot, host, available = portAvailable) {
|
|
43
|
+
const info = await host.inspect();
|
|
44
|
+
const action = await host.sharedServiceUrl() ? 'restart' : 'switch';
|
|
45
|
+
if (!info.desktop) throw new Error('未找到 Codex Desktop;CLI 用户无需执行 Desktop 接入。');
|
|
46
|
+
const record = readRecord(path.join(stateRoot, 'managed-app-server.json'));
|
|
47
|
+
const actual = record ? await host.process(record.pid) : null;
|
|
48
|
+
if (actual && (!sameProcess(record, actual) || !actual.command?.includes('app-server') || !actual.command.includes('ws://127.0.0.1:45839'))) {
|
|
49
|
+
throw new Error('Codex 内核进程身份与记录不符,未结束任何进程。');
|
|
50
|
+
}
|
|
51
|
+
if (!actual && !await available()) throw new Error('45839 被未识别进程占用,未关闭 Desktop 或结束任何进程。');
|
|
52
|
+
return { desktop: info.desktop, action };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function requestRecovery(stateRoot, { confirmed, operationId = randomUUID() } = {}, host = desktopHost) {
|
|
56
|
+
if (host.desktopIntegration === false) throw new Error('此平台尚不支持 Desktop 自动接入。');
|
|
57
|
+
if (confirmed !== true) throw new Error(recoveryWarning + ' 请明确确认后再执行。');
|
|
58
|
+
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(operationId)) throw new Error('无效的恢复操作编号。');
|
|
59
|
+
fs.mkdirSync(stateRoot, { recursive: true });
|
|
60
|
+
const previous = await recoveryStatus(stateRoot, host);
|
|
61
|
+
if (previous.operationId === operationId) return previous;
|
|
62
|
+
if (active(previous)) throw new Error('已有恢复操作正在执行,请查看恢复状态。');
|
|
63
|
+
const planned = await planRecovery(stateRoot, host);
|
|
64
|
+
let lock;
|
|
65
|
+
try { lock = fs.openSync(lockPath(stateRoot), 'wx'); }
|
|
66
|
+
catch (error) { if (error.code === 'EEXIST') throw new Error('已有恢复操作,请先查看恢复状态。'); throw error; }
|
|
67
|
+
fs.writeFileSync(lock, JSON.stringify({ operationId })); fs.closeSync(lock);
|
|
68
|
+
const record = { operationId, status: 'scheduled', stage: 'prepared', startedAt: new Date().toISOString(),
|
|
69
|
+
message: 'Codex 连接请求已接受,电脑管理连接保持运行。', steps: recoverySteps(), ...planned };
|
|
70
|
+
try {
|
|
71
|
+
writeRecord(recordPath(stateRoot), record);
|
|
72
|
+
const log = path.join(stateRoot, 'shared-recovery.log');
|
|
73
|
+
const pid = await startBackground(process.execPath, [script, '--worker', stateRoot, operationId], process.env, [log, log]);
|
|
74
|
+
const worker = await host.process(pid);
|
|
75
|
+
if (!worker) throw new Error('独立恢复进程未启动,请查看 shared-recovery.log。');
|
|
76
|
+
// Publish the process identity before allowing the worker to begin disruption.
|
|
77
|
+
writeRecord(recordPath(stateRoot), { ...record, worker });
|
|
78
|
+
return { operationId, status: record.status, message: record.message };
|
|
79
|
+
} catch (error) {
|
|
80
|
+
writeRecord(recordPath(stateRoot), { ...record, status: 'failed', error: error.message });
|
|
81
|
+
fs.rmSync(lockPath(stateRoot), { force: true }); throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function runRecovery(stateRoot, operationId, dependencies = {}) {
|
|
86
|
+
const host = dependencies.host || desktopHost;
|
|
87
|
+
const service = dependencies.service || lifecycle({ stateRoot, host: serviceHost });
|
|
88
|
+
const available = dependencies.available || portAvailable;
|
|
89
|
+
const start = dependencies.launch || launchDesktop;
|
|
90
|
+
let record = readRecord(recordPath(stateRoot));
|
|
91
|
+
if (record?.operationId !== operationId || readRecord(lockPath(stateRoot))?.operationId !== operationId) throw new Error('恢复实例已失效。');
|
|
92
|
+
record.steps ||= recoverySteps();
|
|
93
|
+
const stage = name => {
|
|
94
|
+
record = { ...record, status: 'running', stage: name, steps: record.steps.map(step => ({ ...step,
|
|
95
|
+
status: step.id === name ? 'running' : step.status === 'running' ? 'completed' : step.status })) };
|
|
96
|
+
writeRecord(recordPath(stateRoot), record);
|
|
97
|
+
};
|
|
98
|
+
const waitFor = async (check, message) => {
|
|
99
|
+
const deadline = Date.now() + (dependencies.timeout ?? 10000);
|
|
100
|
+
while (!await check()) { if (Date.now() >= deadline) throw new Error(message); await delay(100); }
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
stage('checking');
|
|
104
|
+
const planned = await planRecovery(stateRoot, host, available);
|
|
105
|
+
await configureManagedEnvironment(stateRoot);
|
|
106
|
+
record = { ...record, ...planned };
|
|
107
|
+
if (fs.existsSync(path.join(stateRoot, 'desktop-launch.lock'))) throw new Error('Desktop 正在启动,未与启动操作并行恢复。');
|
|
108
|
+
stage('stopping-desktop'); await host.closeDesktop(record.desktop);
|
|
109
|
+
await waitFor(async () => !(await host.inspect()).running, '当前 Codex Desktop 未退出,未继续接入 Codex 内核。');
|
|
110
|
+
stage('starting-app-server'); await start({ stateRoot, host, recoveryId: operationId, progress: stage });
|
|
111
|
+
stage('checking-execution');
|
|
112
|
+
await service.start(); // Refresh the execution adapter; resident Bridge retains its PID and sockets.
|
|
113
|
+
await waitFor(async () => {
|
|
114
|
+
const health = await service.status();
|
|
115
|
+
return health.running && health.mode === 'lan-shared' && health.execution?.ready === true;
|
|
116
|
+
}, 'Codex 内核未就绪;桌面服务和中继连接仍保留。');
|
|
117
|
+
record = { ...record, status: 'completed', stage: 'ready', steps: record.steps.map(step => ({ ...step, status: 'completed' })),
|
|
118
|
+
message: 'Codex Desktop 已接入 Codex 内核;移动端控制保持就绪,原任务未自动重发。' };
|
|
119
|
+
} catch (error) { record = { ...record, status: 'failed', error: error.message,
|
|
120
|
+
steps: record.steps.map(step => ({ ...step, status: step.status === 'running' ? 'failed' : step.status })),
|
|
121
|
+
message: 'Codex Desktop 未完成接入,已停止后续步骤。' }; }
|
|
122
|
+
finally {
|
|
123
|
+
record.finishedAt = new Date().toISOString(); writeRecord(recordPath(stateRoot), record);
|
|
124
|
+
if (readRecord(lockPath(stateRoot))?.operationId === operationId) fs.rmSync(lockPath(stateRoot));
|
|
125
|
+
}
|
|
126
|
+
return record;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === script && process.argv[2] === '--worker') {
|
|
130
|
+
const [stateRoot, operationId] = process.argv.slice(3);
|
|
131
|
+
(async () => {
|
|
132
|
+
const deadline = Date.now() + 15000;
|
|
133
|
+
while (true) {
|
|
134
|
+
const record = readRecord(recordPath(stateRoot));
|
|
135
|
+
if (record?.operationId !== operationId || record.status !== 'scheduled') throw new Error('恢复调度已失效。');
|
|
136
|
+
if (record.worker?.pid === process.pid) break;
|
|
137
|
+
if (Date.now() >= deadline) throw new Error('恢复启动交接超时。');
|
|
138
|
+
await delay(100);
|
|
139
|
+
}
|
|
140
|
+
// A deliberate handoff window lets the caller display the accepted operation ID.
|
|
141
|
+
await delay(3000);
|
|
142
|
+
const result = await runRecovery(stateRoot, operationId);
|
|
143
|
+
console.log(JSON.stringify({ operationId, status: result.status, stage: result.stage, error: result.error }));
|
|
144
|
+
process.exit(result.status === 'completed' ? 0 : 1);
|
|
145
|
+
})().catch(error => { console.error(error.message); process.exit(1); });
|
|
146
|
+
}
|