@walkhi/code-relax 0.1.0-beta.10 → 0.1.0-beta.11
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 +44 -54
- package/dist/bin/codex-remote.mjs +22 -58
- package/dist/shared/app-server-events.cjs +15 -3
- package/dist/shared/file-mentions.cjs +35 -0
- package/dist/skills/code-relax/SKILL.md +1 -1
- package/dist/src/desktop-launcher.mjs +6 -11
- package/dist/src/file-upload-session.mjs +124 -0
- package/dist/src/lan-socket-server.mjs +51 -8
- package/dist/src/managed-app-server.mjs +9 -7
- package/dist/src/onboarding.mjs +3 -3
- package/dist/src/platform/windows/desktop-host.mjs +1 -2
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +6 -13
- package/dist/src/platform/windows/service-host.mjs +0 -9
- package/dist/src/self-relay/admin-page.mjs +1 -1
- package/dist/src/self-relay/connector.mjs +84 -12
- package/dist/src/self-relay/lifecycle.mjs +8 -24
- package/dist/src/self-relay/p2p-probe.mjs +1 -1
- package/dist/src/self-relay/server.mjs +67 -12
- package/dist/src/server.mjs +141 -34
- package/dist/src/service-doctor.mjs +1 -1
- package/dist/src/service-install.mjs +1 -4
- package/dist/src/shared-app-server.mjs +30 -28
- package/dist/src/shared-catalog.mjs +1 -11
- package/dist/src/shared-recovery.mjs +5 -5
- package/dist/src/skill-install.mjs +0 -13
- package/package.json +1 -1
- package/dist/src/debug-ui.mjs +0 -131
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
- package/dist/web/activity-view.js +0 -283
- package/dist/web/approval-ui.js +0 -188
- package/dist/web/capabilities.js +0 -50
- package/dist/web/chat-transport.js +0 -196
- package/dist/web/chat.css +0 -778
- package/dist/web/chat.js +0 -3815
- package/dist/web/client-platform.js +0 -11
- package/dist/web/community.css +0 -82
- package/dist/web/community.html +0 -36
- package/dist/web/composer-controller.js +0 -97
- package/dist/web/conversation-controller.js +0 -99
- package/dist/web/disclosure-state-controller.js +0 -95
- package/dist/web/draft-controller.js +0 -103
- package/dist/web/harmony-platform.js +0 -52
- package/dist/web/highlight-powershell.min.js +0 -39
- package/dist/web/highlight.min.js +0 -1262
- package/dist/web/history-cache.js +0 -240
- package/dist/web/history-controller.js +0 -167
- package/dist/web/index.html +0 -288
- package/dist/web/lan-session.js +0 -122
- package/dist/web/link-action-controller.js +0 -212
- package/dist/web/message-send-controller.js +0 -177
- package/dist/web/message-view.js +0 -98
- package/dist/web/p2p-data-channel.js +0 -50
- package/dist/web/p2p-diagnostics.js +0 -28
- package/dist/web/p2p-probe.js +0 -123
- package/dist/web/page-resume.js +0 -28
- package/dist/web/pending-message-store.js +0 -108
- package/dist/web/queue-controller.js +0 -82
- package/dist/web/resources.json +0 -1
- package/dist/web/self-relay-session.js +0 -129
- package/dist/web/server-requests.js +0 -67
- package/dist/web/station-connection-controller.js +0 -75
- package/dist/web/task-list-view.js +0 -289
- package/dist/web/thread-attention-controller.js +0 -124
- package/dist/web/thread-context-controller.js +0 -30
- package/dist/web/thread-list-controller.js +0 -61
- package/dist/web/thread-list-sync.js +0 -86
- package/dist/web/thread-title-controller.js +0 -57
- package/dist/web/timeline-formatters.js +0 -249
- package/dist/web/timeline-reducer.js +0 -77
- package/dist/web/timeline-renderer.js +0 -161
- package/dist/web/timeline-scroll-controller.js +0 -50
- package/dist/web/timeline-sync.js +0 -67
- package/dist/web/usage-controller.js +0 -258
- package/dist/web/vendor/README.md +0 -14
- package/dist/web/vendor/highlight-powershell.min.js +0 -39
- package/dist/web/vendor/highlight.LICENSE.txt +0 -29
- package/dist/web/vendor/highlight.min.js +0 -1262
- package/dist/web/vendor/lucide.LICENSE.txt +0 -17
package/dist/src/server.mjs
CHANGED
|
@@ -6,7 +6,8 @@ import os from 'node:os';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import vm from 'node:vm';
|
|
8
8
|
import sharp from 'sharp';
|
|
9
|
-
import { attachLanSocket } from './lan-socket-server.mjs';
|
|
9
|
+
import { attachLanSocket } from './lan-socket-server.mjs';
|
|
10
|
+
import fileMentions from '../shared/file-mentions.cjs';
|
|
10
11
|
import { validateInlineImage } from './message-images.mjs';
|
|
11
12
|
import { ThreadWriterConflictError } from './shared-app-server.mjs';
|
|
12
13
|
import { ManagedAppServer, managedAppServerUrl } from './managed-app-server.mjs';
|
|
@@ -17,7 +18,6 @@ import { runtimePaths } from './platform/windows/runtime-paths.mjs';
|
|
|
17
18
|
import { openControl } from './service-control.mjs';
|
|
18
19
|
import { writeRecord, readRecord } from './service-lifecycle.mjs';
|
|
19
20
|
import { ExecutionHealth } from './execution-health.mjs';
|
|
20
|
-
import { activeDebugPort, serveDebugResource } from './debug-ui.mjs';
|
|
21
21
|
import { authorizeLanDevice } from './self-relay/lan-authorizations.mjs';
|
|
22
22
|
import { generateAndSetThreadTitle } from './thread-title-generation.mjs';
|
|
23
23
|
import { desktopHost } from './platform/windows/desktop-host.mjs';
|
|
@@ -33,15 +33,25 @@ const MAX_LIVE_ACTIVITY_TAIL_BYTES = 1024 * 1024;
|
|
|
33
33
|
const SHARED_HISTORY_PAGE_BYTES = 500 * 1024;
|
|
34
34
|
const SHARED_HISTORY_PAGE_TURNS = 8;
|
|
35
35
|
const MAX_SETTINGS_SCAN_BYTES = 64 * 1024 * 1024;
|
|
36
|
-
const ALLOWED_MODELS = new Set([
|
|
37
|
-
'gpt-6-astra',
|
|
36
|
+
const ALLOWED_MODELS = new Set([
|
|
37
|
+
'gpt-6-astra',
|
|
38
|
+
'gpt-6-sol',
|
|
38
39
|
'gpt-5.6-sol',
|
|
39
40
|
'gpt-5.6-terra',
|
|
40
41
|
'gpt-5.6-luna',
|
|
41
42
|
'gpt-5.5',
|
|
42
43
|
'gpt-5.3-codex-spark',
|
|
43
44
|
]);
|
|
44
|
-
const ALLOWED_THINKING = new Set(['low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
|
|
45
|
+
const ALLOWED_THINKING = new Set(['low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
|
|
46
|
+
|
|
47
|
+
class RequestError extends Error {
|
|
48
|
+
constructor(statusCode, message, details) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.statusCode = statusCode;
|
|
51
|
+
this.details = details;
|
|
52
|
+
this.code = details?.code;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
45
55
|
|
|
46
56
|
function readRuntimeVersion() {
|
|
47
57
|
let directory = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -58,7 +68,6 @@ const options = parseOptions(process.argv.slice(2));
|
|
|
58
68
|
const runtimeVersion = readRuntimeVersion();
|
|
59
69
|
|
|
60
70
|
const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'web');
|
|
61
|
-
const repositoryRoot = path.resolve(webRoot, '..', '..');
|
|
62
71
|
const projectRoot = os.homedir();
|
|
63
72
|
const { codexDataRoot, stateRoot, sessionsRoot, uploadsRoot } = runtimePaths();
|
|
64
73
|
const markdownItScript = fs.readFileSync(path.join(webRoot, 'vendor', 'markdown-it.umd.min.js'));
|
|
@@ -70,9 +79,6 @@ const fileCache = new Map();
|
|
|
70
79
|
const fileCapabilitySecret = crypto.randomBytes(32);
|
|
71
80
|
const imageCapabilityKey = crypto.createHash('sha256')
|
|
72
81
|
.update('code-relax-image-capability\0').update(options.token || fileCapabilitySecret).digest();
|
|
73
|
-
const legacyUploadsRoot = path.resolve(webRoot, '..', 'uploads');
|
|
74
|
-
const allowedUploadsRoots = [uploadsRoot, legacyUploadsRoot,
|
|
75
|
-
...JSON.parse(process.env.CODEX_REMOTE_LEGACY_UPLOADS_ROOTS || '[]')];
|
|
76
82
|
const sessionPathCache = new Map();
|
|
77
83
|
const sessionSettingsCache = new Map();
|
|
78
84
|
const liveActivityCache = new Map();
|
|
@@ -119,8 +125,7 @@ async function handleRequest(request, response) {
|
|
|
119
125
|
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
|
|
120
126
|
|
|
121
127
|
if (!request.socketRequest) {
|
|
122
|
-
|
|
123
|
-
sendJson(response, 404, { error: 'Bridge 调试页面未开启,或请求的资源不存在。' });
|
|
128
|
+
sendJson(response, 404, { error: '此端口不提供网页。' });
|
|
124
129
|
return;
|
|
125
130
|
}
|
|
126
131
|
|
|
@@ -167,6 +172,24 @@ async function handleRequest(request, response) {
|
|
|
167
172
|
return;
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
if (request.method === 'GET' && url.pathname === '/api/official-remote/status') {
|
|
176
|
+
if (!managedAppServer.client.ready) throw new RequestError(503, 'Codex 内核未就绪,无法读取官方工作站状态。');
|
|
177
|
+
const result = await managedAppServer.client.request('remoteControl/status/read', {});
|
|
178
|
+
sendJson(response, 200, { status: result.status, environmentId: result.environmentId });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (request.method === 'POST' && url.pathname === '/api/official-remote/pairing') {
|
|
182
|
+
if (!managedAppServer.client.ready) throw new RequestError(503, 'Codex 内核未就绪,无法生成官方配对码。');
|
|
183
|
+
const status = await managedAppServer.client.request('remoteControl/status/read', {});
|
|
184
|
+
if (status.status !== 'connected') throw new RequestError(409, '当前 Codex 内核未连接 OpenAI Remote,无法配对。');
|
|
185
|
+
const result = await managedAppServer.client.request('remoteControl/pairing/start', { manualCode: true });
|
|
186
|
+
if (!result.manualPairingCode || !result.environmentId) throw new RequestError(502, 'Codex 内核未返回有效的官方配对码。');
|
|
187
|
+
// One-time code: return only to the authenticated station controller; never persist or log it.
|
|
188
|
+
sendJson(response, 200, { manualPairingCode: result.manualPairingCode,
|
|
189
|
+
environmentId: result.environmentId, expiresAt: result.expiresAt });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
170
193
|
if (request.method === 'GET' && url.pathname === '/api/health') {
|
|
171
194
|
// Status readers consume the workstation's cached sample. A phone query
|
|
172
195
|
// must not trigger PowerShell, process-identity, or app-server checks.
|
|
@@ -176,7 +199,6 @@ async function handleRequest(request, response) {
|
|
|
176
199
|
mode: executionMode },
|
|
177
200
|
managedAppServer: managedAppServer.snapshot(),
|
|
178
201
|
...runtimeDiscovery.state,
|
|
179
|
-
debugUiPort: await activeDebugPort(stateRoot, options.port),
|
|
180
202
|
sharedAppServerConnected: sharedServer?.ready === true,
|
|
181
203
|
sharedRecoveryAvailable: runtimeDiscovery.state.desktop?.installed === true,
|
|
182
204
|
mode: options.lan ? 'lan' : 'loopback',
|
|
@@ -352,7 +374,8 @@ async function handleRequest(request, response) {
|
|
|
352
374
|
turnId: url.searchParams.get('knownTurnId'),
|
|
353
375
|
turnStatus: url.searchParams.get('knownTurnStatus') || '',
|
|
354
376
|
} : undefined;
|
|
355
|
-
await streamSharedThread(response, threadId, sharedServer,
|
|
377
|
+
await streamSharedThread(response, threadId, sharedServer,
|
|
378
|
+
normalizeSharedTimeline, requestedSync, known);
|
|
356
379
|
return;
|
|
357
380
|
}
|
|
358
381
|
|
|
@@ -470,8 +493,9 @@ async function handleRequest(request, response) {
|
|
|
470
493
|
}
|
|
471
494
|
|
|
472
495
|
const server = http.createServer(handleRequest);
|
|
473
|
-
const lanSocket = attachLanSocket(server, { token: options.token, dispatch: handleRequest,
|
|
474
|
-
|
|
496
|
+
const lanSocket = attachLanSocket(server, { token: options.token, dispatch: handleRequest,
|
|
497
|
+
uploadsRoot,
|
|
498
|
+
authorizeDevice: (clientId, credential) => authorizeLanDevice(stateRoot, clientId, credential),
|
|
475
499
|
});
|
|
476
500
|
|
|
477
501
|
let control;
|
|
@@ -703,8 +727,9 @@ function normalizeTimeline(data) {
|
|
|
703
727
|
}
|
|
704
728
|
|
|
705
729
|
function normalizeSharedTimeline({ thread, nextCursor, sync }) {
|
|
706
|
-
|
|
707
|
-
|
|
730
|
+
const timeline = normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId: 'local', transport: 'shared-app-server' } });
|
|
731
|
+
return { ...timeline,
|
|
732
|
+
historyView: 'grouped-lazy-skeleton-v12',
|
|
708
733
|
...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
|
|
709
734
|
}
|
|
710
735
|
|
|
@@ -728,7 +753,7 @@ function normalizeThread(thread) {
|
|
|
728
753
|
function normalizeTurn(turn) {
|
|
729
754
|
const source = turn && typeof turn === 'object' ? turn : {};
|
|
730
755
|
const turnId = source.id || '';
|
|
731
|
-
const entries = Array.isArray(source.items) ? source.items.map(normalizeItem).filter(Boolean) : [];
|
|
756
|
+
const entries = collapseSubAgentEntries(Array.isArray(source.items) ? source.items.map(normalizeItem).filter(Boolean) : []);
|
|
732
757
|
for (const entry of entries) {
|
|
733
758
|
const images = [...(entry.attachments || []), entry.image, ...Object.values(entry.imageAssets || {})].filter(Boolean);
|
|
734
759
|
for (const image of images) if (image.remotePath && !image.recoveryTurnId) image.recoveryTurnId = turnId;
|
|
@@ -738,7 +763,7 @@ function normalizeTurn(turn) {
|
|
|
738
763
|
status: statusText(source.status),
|
|
739
764
|
startedAt: source.startedAt || null,
|
|
740
765
|
completedAt: source.completedAt || null,
|
|
741
|
-
durationMs: source.durationMs ?? null,
|
|
766
|
+
durationMs: source.durationMs ?? null,
|
|
742
767
|
error: source.error ?? null,
|
|
743
768
|
entries,
|
|
744
769
|
};
|
|
@@ -754,8 +779,7 @@ function normalizeItem(item) {
|
|
|
754
779
|
kind: 'lazyOperationGroup',
|
|
755
780
|
turnId: item.turnId || '',
|
|
756
781
|
groupId: item.groupId || '',
|
|
757
|
-
|
|
758
|
-
count: Number(item.count) || 0,
|
|
782
|
+
operationTypes: Array.isArray(item.operationTypes) ? item.operationTypes : [],
|
|
759
783
|
};
|
|
760
784
|
}
|
|
761
785
|
|
|
@@ -783,9 +807,9 @@ function normalizeItem(item) {
|
|
|
783
807
|
}
|
|
784
808
|
}
|
|
785
809
|
|
|
786
|
-
if (item.type === 'userMessage') {
|
|
787
|
-
const text = extractUserText(item.content);
|
|
788
|
-
const attachments = normalizeAttachments(item.content);
|
|
810
|
+
if (item.type === 'userMessage') {
|
|
811
|
+
const { text, files } = extractUserText(item.content);
|
|
812
|
+
const attachments = [...normalizeAttachments(item.content), ...files];
|
|
789
813
|
if (!text && !attachments.length) return null;
|
|
790
814
|
return {
|
|
791
815
|
clientId: item.clientId,
|
|
@@ -949,7 +973,11 @@ function normalizeItem(item) {
|
|
|
949
973
|
status: statusText(item.status),
|
|
950
974
|
tool: item.tool || '',
|
|
951
975
|
prompt: item.prompt || '',
|
|
976
|
+
senderThreadId: item.senderThreadId || '',
|
|
952
977
|
receiverThreadIds: Array.isArray(item.receiverThreadIds) ? item.receiverThreadIds : [],
|
|
978
|
+
model: item.model || '',
|
|
979
|
+
reasoningEffort: item.reasoningEffort || '',
|
|
980
|
+
agentsStates: item.agentsStates && typeof item.agentsStates === 'object' ? item.agentsStates : {},
|
|
953
981
|
};
|
|
954
982
|
}
|
|
955
983
|
|
|
@@ -963,6 +991,83 @@ function normalizeItem(item) {
|
|
|
963
991
|
};
|
|
964
992
|
}
|
|
965
993
|
|
|
994
|
+
function collapseSubAgentEntries(entries) {
|
|
995
|
+
const agents = new Map();
|
|
996
|
+
let firstIndex = -1;
|
|
997
|
+
const retained = [];
|
|
998
|
+
const ensureAgent = threadId => {
|
|
999
|
+
if (!threadId) return null;
|
|
1000
|
+
let agent = agents.get(threadId);
|
|
1001
|
+
if (!agent) {
|
|
1002
|
+
agent = { threadId, name: '', status: 'pendingInit', objective: '', model: '', reasoningEffort: '' };
|
|
1003
|
+
agents.set(threadId, agent);
|
|
1004
|
+
}
|
|
1005
|
+
return agent;
|
|
1006
|
+
};
|
|
1007
|
+
const setProtocolStatus = (agent, status) => {
|
|
1008
|
+
if (!agent || !status || status === 'unknown') return;
|
|
1009
|
+
const terminal = ['interrupted', 'completed', 'errored', 'shutdown', 'notFound'];
|
|
1010
|
+
if (terminal.includes(agent.status) && ['pendingInit', 'running'].includes(status)) return;
|
|
1011
|
+
if (status === 'shutdown' && agent.status === 'completed') return;
|
|
1012
|
+
agent.status = status;
|
|
1013
|
+
};
|
|
1014
|
+
for (const entry of entries) {
|
|
1015
|
+
if (entry.kind === 'subAgent') {
|
|
1016
|
+
if (firstIndex < 0) firstIndex = retained.length;
|
|
1017
|
+
const agent = ensureAgent(entry.agentThreadId);
|
|
1018
|
+
if (!agent) continue;
|
|
1019
|
+
agent.name = subAgentDisplayName(entry.agentPath) || agent.name;
|
|
1020
|
+
const activityStatus = {
|
|
1021
|
+
started: 'running',
|
|
1022
|
+
interacted: 'running',
|
|
1023
|
+
interrupted: 'interrupted',
|
|
1024
|
+
completed: 'completed',
|
|
1025
|
+
}[entry.status];
|
|
1026
|
+
agent.activity = entry.status;
|
|
1027
|
+
setProtocolStatus(agent, activityStatus);
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (entry.kind === 'collaboration') {
|
|
1031
|
+
const receiverIds = entry.receiverThreadIds || [];
|
|
1032
|
+
const createsAgent = entry.tool === 'spawnAgent';
|
|
1033
|
+
const updatesKnownAgent = receiverIds.some(threadId => agents.has(threadId));
|
|
1034
|
+
if (!createsAgent && !updatesKnownAgent) {
|
|
1035
|
+
retained.push(entry);
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
if (firstIndex < 0) firstIndex = retained.length;
|
|
1039
|
+
for (const threadId of receiverIds) {
|
|
1040
|
+
const agent = ensureAgent(threadId);
|
|
1041
|
+
if (!agent) continue;
|
|
1042
|
+
if (entry.prompt) agent.objective = entry.prompt;
|
|
1043
|
+
if (entry.model) agent.model = entry.model;
|
|
1044
|
+
if (entry.reasoningEffort) agent.reasoningEffort = entry.reasoningEffort;
|
|
1045
|
+
setProtocolStatus(agent, statusText(entry.agentsStates?.[threadId]?.status));
|
|
1046
|
+
if (entry.status === 'failed' && !entry.agentsStates?.[threadId]?.status) agent.status = 'errored';
|
|
1047
|
+
if (entry.status === 'interrupted' && !entry.agentsStates?.[threadId]?.status) agent.status = 'interrupted';
|
|
1048
|
+
}
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
retained.push(entry);
|
|
1052
|
+
}
|
|
1053
|
+
if (!agents.size) return retained;
|
|
1054
|
+
retained.splice(firstIndex < 0 ? retained.length : firstIndex, 0, {
|
|
1055
|
+
type: 'activity',
|
|
1056
|
+
id: `subagents:${[...agents.keys()].join(':')}`,
|
|
1057
|
+
kind: 'subAgentGroup',
|
|
1058
|
+
title: '子 Agent',
|
|
1059
|
+
agents: [...agents.values()],
|
|
1060
|
+
});
|
|
1061
|
+
return retained;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function subAgentDisplayName(agentPath) {
|
|
1065
|
+
const segment = String(agentPath || '').split('/').map(value => value.trim()).filter(value => value && value !== 'root').at(-1);
|
|
1066
|
+
if (!segment) return '';
|
|
1067
|
+
const text = segment.replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
1068
|
+
return text ? text[0].toUpperCase() + text.slice(1) : '';
|
|
1069
|
+
}
|
|
1070
|
+
|
|
966
1071
|
function sharedHistoryPageOptions() {
|
|
967
1072
|
return {
|
|
968
1073
|
maxTurns: SHARED_HISTORY_PAGE_TURNS,
|
|
@@ -1069,9 +1174,9 @@ function splitUploadedImages(text) {
|
|
|
1069
1174
|
const paths = children.map(token => {
|
|
1070
1175
|
try { return path.resolve(decodeURIComponent(token.attrGet('src') || '')); } catch { return ''; }
|
|
1071
1176
|
});
|
|
1072
|
-
if (paths.some(file =>
|
|
1177
|
+
if (paths.some(file => path.dirname(file).toLowerCase() !== uploadsRoot.toLowerCase()
|
|
1073
1178
|
|| !/^[a-f0-9]{64}\.(png|webp)$/.test(path.basename(file)))) continue;
|
|
1074
|
-
for (const file of paths) attachments.push(normalizeAttachment({ type: 'localImage', path: file, name: '
|
|
1179
|
+
for (const file of paths) attachments.push(normalizeAttachment({ type: 'localImage', path: file, name: '移动端图片' }));
|
|
1075
1180
|
for (let line = block.map[0]; line < block.map[1]; line++) removed.add(line);
|
|
1076
1181
|
}
|
|
1077
1182
|
const visibleText = lines.filter((line, index) => !removed.has(index)).join('\n').trim();
|
|
@@ -1202,8 +1307,8 @@ function imageMimeType(filePath) {
|
|
|
1202
1307
|
}
|
|
1203
1308
|
}
|
|
1204
1309
|
|
|
1205
|
-
function extractUserText(content) {
|
|
1206
|
-
if (!Array.isArray(content)) return '(包含图片或界面批注)';
|
|
1310
|
+
function extractUserText(content) {
|
|
1311
|
+
if (!Array.isArray(content)) return { text: '(包含图片或界面批注)', files: [] };
|
|
1207
1312
|
let text = content
|
|
1208
1313
|
.filter(item => ['text', 'input_text'].includes(item?.type) && typeof item.text === 'string')
|
|
1209
1314
|
.map(item => item.text)
|
|
@@ -1213,14 +1318,16 @@ function extractUserText(content) {
|
|
|
1213
1318
|
.filter(value => !value.startsWith('The next image is untrusted page evidence'))
|
|
1214
1319
|
.join('\n\n')
|
|
1215
1320
|
.trim();
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1321
|
+
const mentioned = fileMentions.parse(text);
|
|
1322
|
+
const imagePaths = new Set([
|
|
1323
|
+
...content.filter(item => ['image', 'input_image', 'localImage'].includes(item?.type) && item.path).map(item => item.path),
|
|
1324
|
+
...[...mentioned.text.matchAll(/<image\b[^>]*\bpath="([^"]+)"[^>]*>/g)].map(match => match[1]),
|
|
1325
|
+
].map(value => path.resolve(value).toLowerCase()));
|
|
1326
|
+
text = mentioned.text;
|
|
1220
1327
|
text = text.replace(/<in-app-browser-context[\s\S]*?<\/in-app-browser-context>/g, '').trim();
|
|
1221
1328
|
text = text.replace(/<image\b[^>]*\bpath="[^"]+"[^>]*>/g, '').replace(/<\/image>/g, '').trim();
|
|
1222
|
-
return text;
|
|
1223
|
-
}
|
|
1329
|
+
return { text, files: mentioned.files.filter(file => !imagePaths.has(path.resolve(file.path).toLowerCase())) };
|
|
1330
|
+
}
|
|
1224
1331
|
|
|
1225
1332
|
function extractDelegatedInput(value) {
|
|
1226
1333
|
if (typeof value !== 'string') return '';
|
|
@@ -46,6 +46,6 @@ export async function doctor({ paths, host, service, skillTarget }) {
|
|
|
46
46
|
add('skill', state.managed ? 'ok' : 'warning', state.managed ? '本包管理的 Skill 已安装。'
|
|
47
47
|
: state.installed ? '存在同名自定义 Skill,本包不会覆盖。' : '可执行 relax skill-install 安装 Skill。');
|
|
48
48
|
});
|
|
49
|
-
add('firewall', 'warning', '
|
|
49
|
+
add('firewall', 'warning', '移动端连通性未验证。首次启动请允许记录的 Node 在受信任的专用网络通信;若曾拒绝,可在 Windows 安全中心的防火墙“允许应用通过防火墙”中检查该 Node。不要关闭整个防火墙。');
|
|
50
50
|
return { ready: !checks.some(item => item.status === 'error'), lanAccess: 'unverified', checks };
|
|
51
51
|
}
|
|
@@ -22,8 +22,6 @@ export async function installService(source, target, executable, stateRoot, hook
|
|
|
22
22
|
if (!fs.existsSync(path.join(source, 'dist/bin/codex-remote.mjs'))) throw new Error('缺少发布构建;源码目录请先执行 npm run build。');
|
|
23
23
|
if (!fs.existsSync(path.join(source, 'node_modules'))) throw new Error('缺少包依赖,请重新安装完整 npm 包。');
|
|
24
24
|
const parent = path.dirname(target);
|
|
25
|
-
const legacyUploadsRoots = readRecord(path.join(target, 'installation.json'))?.legacyUploadsRoots || [];
|
|
26
|
-
if (fs.existsSync(path.join(source, 'src/server.mjs'))) legacyUploadsRoots.push(path.join(source, 'uploads'));
|
|
27
25
|
fs.mkdirSync(parent, { recursive: true });
|
|
28
26
|
const stage = path.join(parent, `service-stage-${randomUUID()}`);
|
|
29
27
|
const backup = path.join(parent, `service-backup-${randomUUID()}`);
|
|
@@ -34,8 +32,7 @@ export async function installService(source, target, executable, stateRoot, hook
|
|
|
34
32
|
}
|
|
35
33
|
await exec(executable, [path.join(stage, 'dist/bin/codex-remote.mjs'), '--help'], { windowsHide: true });
|
|
36
34
|
await exec(executable, ['-e', 'const r=require("node:module").createRequire(process.argv[1]); r("sharp"); r("qrcode"); r("ws"); r("node-datachannel");', path.join(stage, 'package.json')], { windowsHide: true });
|
|
37
|
-
writeRecord(path.join(stage, 'installation.json'), { installedAt: new Date().toISOString(), source, nodeExecutable: executable, stateRoot,
|
|
38
|
-
legacyUploadsRoots: [...new Set(legacyUploadsRoots)],
|
|
35
|
+
writeRecord(path.join(stage, 'installation.json'), { installedAt: new Date().toISOString(), source, nodeExecutable: executable, stateRoot,
|
|
39
36
|
version: readRecord(path.join(source, 'package.json')).version });
|
|
40
37
|
const previous = fs.existsSync(target);
|
|
41
38
|
let previousMoved = false, activated = false;
|
|
@@ -33,7 +33,8 @@ export class SharedAppServer extends EventEmitter {
|
|
|
33
33
|
super();
|
|
34
34
|
const address = new URL(url);
|
|
35
35
|
if (address.protocol !== 'ws:' || address.hostname !== '127.0.0.1') throw new Error('Codex 内核必须使用本机回环 WebSocket。');
|
|
36
|
-
this.url = url;
|
|
36
|
+
this.url = url;
|
|
37
|
+
this.serverVersion = '';
|
|
37
38
|
this.epoch = randomUUID();
|
|
38
39
|
this.sequence = 0;
|
|
39
40
|
this.pending = new Map();
|
|
@@ -56,8 +57,9 @@ export class SharedAppServer extends EventEmitter {
|
|
|
56
57
|
|
|
57
58
|
async open() {
|
|
58
59
|
const socket = new WebSocket(this.url, { handshakeTimeout: 5000 });
|
|
59
|
-
this.socket = socket;
|
|
60
|
-
this.ready = false;
|
|
60
|
+
this.socket = socket;
|
|
61
|
+
this.ready = false;
|
|
62
|
+
this.serverVersion = '';
|
|
61
63
|
this.serverRequests.reset(randomUUID());
|
|
62
64
|
socket.on('message', bytes => {
|
|
63
65
|
let message;
|
|
@@ -76,8 +78,9 @@ export class SharedAppServer extends EventEmitter {
|
|
|
76
78
|
}
|
|
77
79
|
else pending.resolve(message.result);
|
|
78
80
|
});
|
|
79
|
-
socket.on('close', () => {
|
|
80
|
-
this.ready = false;
|
|
81
|
+
socket.on('close', () => {
|
|
82
|
+
this.ready = false;
|
|
83
|
+
this.serverVersion = '';
|
|
81
84
|
for (const pending of this.pending.values()) {
|
|
82
85
|
clearTimeout(pending.timer);
|
|
83
86
|
pending.reject(new Error('Codex 内核连接断开;发送结果可能未确认,请检查对话后再操作。'));
|
|
@@ -97,7 +100,10 @@ export class SharedAppServer extends EventEmitter {
|
|
|
97
100
|
socket.on('error', () => {}); // open/request reports the connection error to the caller.
|
|
98
101
|
await new Promise((resolve, reject) => { socket.once('open', resolve); socket.once('error', reject); });
|
|
99
102
|
try {
|
|
100
|
-
await this.request('initialize', { clientInfo: { name: 'codex_remote_shared', version: '1.0.0' }, capabilities: { experimentalApi: true } });
|
|
103
|
+
const initialized = await this.request('initialize', { clientInfo: { name: 'codex_remote_shared', version: '1.0.0' }, capabilities: { experimentalApi: true } });
|
|
104
|
+
// The first user-agent product reports the running server build, not the
|
|
105
|
+
// selected executable that may only take effect after a future restart.
|
|
106
|
+
this.serverVersion = String(initialized?.userAgent || '').match(/^[^\s/]+\/([^\s]+)/)?.[1] || '';
|
|
101
107
|
socket.send(JSON.stringify({ method: 'initialized' }));
|
|
102
108
|
this.ready = true;
|
|
103
109
|
this.usageCache.read(() => this.request('account/rateLimits/read')).catch(error => this.emit('usage-error', error));
|
|
@@ -176,8 +182,8 @@ export class SharedAppServer extends EventEmitter {
|
|
|
176
182
|
items: params.turn.items.filter(item => ['userMessage', 'agentMessage'].includes(item?.type)) } }
|
|
177
183
|
: params;
|
|
178
184
|
const turn = events.applyNotification(record.thread, method, appliedParams);
|
|
179
|
-
if (turn) {
|
|
180
|
-
const sequence = ++record.sequence;
|
|
185
|
+
if (turn) {
|
|
186
|
+
const sequence = ++record.sequence;
|
|
181
187
|
record.changes.set(turn.id, { sequence, turn });
|
|
182
188
|
record.changeBytes += Buffer.byteLength(JSON.stringify({ method, params: appliedParams }));
|
|
183
189
|
if (record.changeBytes > syncBufferMaxBytes) {
|
|
@@ -233,8 +239,8 @@ export class SharedAppServer extends EventEmitter {
|
|
|
233
239
|
cwd: result.cwd, ...events.permissionState(result) });
|
|
234
240
|
await this.extendHistoryRecord(threadId, record, pageOptions);
|
|
235
241
|
}
|
|
236
|
-
}
|
|
237
|
-
const id = randomUUID(), expires = Date.now() + syncTokenTtlMs;
|
|
242
|
+
}
|
|
243
|
+
const id = randomUUID(), expires = Date.now() + syncTokenTtlMs;
|
|
238
244
|
record.tokens.set(id, { sequence: record.sequence, expires });
|
|
239
245
|
this.scheduleCleanup(threadId, slot);
|
|
240
246
|
return { thread: record.thread, nextCursor: record.nextCursor,
|
|
@@ -279,7 +285,7 @@ export class SharedAppServer extends EventEmitter {
|
|
|
279
285
|
}).catch(error => this.emit('subscription-error', error));
|
|
280
286
|
}
|
|
281
287
|
|
|
282
|
-
async acquire(threadId, requested) {
|
|
288
|
+
async acquire(threadId, requested) {
|
|
283
289
|
const slot = this.slot(threadId);
|
|
284
290
|
let record = this.threads.get(threadId), token;
|
|
285
291
|
if (record && requested?.epoch === this.epoch && requested.id && Number.isSafeInteger(requested.after)) {
|
|
@@ -288,7 +294,7 @@ export class SharedAppServer extends EventEmitter {
|
|
|
288
294
|
}
|
|
289
295
|
let snapshot = false, catchup;
|
|
290
296
|
if (!record || !token) {
|
|
291
|
-
const fresh = await this.timeline(threadId);
|
|
297
|
+
const fresh = await this.timeline(threadId);
|
|
292
298
|
record = this.threads.get(threadId); token = record.tokens.get(fresh.sync.id);
|
|
293
299
|
requested = { id: fresh.sync.id, epoch: fresh.sync.epoch, after: fresh.sync.seq };
|
|
294
300
|
snapshot = true;
|
|
@@ -343,11 +349,16 @@ export class SharedAppServer extends EventEmitter {
|
|
|
343
349
|
const visibleItems = operationItems.filter(item => item?.type !== 'reasoning');
|
|
344
350
|
operationItems = [];
|
|
345
351
|
if (!visibleItems.length) return;
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
352
|
+
const subAgentItems = visibleItems.filter(isSubAgentItem);
|
|
353
|
+
const detailItems = visibleItems.filter(item => !isSubAgentItem(item));
|
|
354
|
+
if (detailItems.length) {
|
|
355
|
+
const groupId = `g${groups.size + 1}`;
|
|
356
|
+
groups.set(groupId, detailItems);
|
|
357
|
+
skeleton.push({ type: 'lazyOperationGroup', id: `${turn.id}:${groupId}`,
|
|
358
|
+
turnId: turn.id, groupId,
|
|
359
|
+
operationTypes: [...new Set(detailItems.map(item => item.type).filter(Boolean))] });
|
|
360
|
+
}
|
|
361
|
+
skeleton.push(...subAgentItems);
|
|
351
362
|
};
|
|
352
363
|
for (const item of items) {
|
|
353
364
|
if (['userMessage', 'agentMessage', 'imageGeneration'].includes(item?.type)) {
|
|
@@ -500,15 +511,6 @@ export class SharedAppServer extends EventEmitter {
|
|
|
500
511
|
close() { this.socket?.close(); }
|
|
501
512
|
}
|
|
502
513
|
|
|
503
|
-
function
|
|
504
|
-
|
|
505
|
-
const parts = [];
|
|
506
|
-
if (types.has('fileChange')) parts.push('编辑了文件');
|
|
507
|
-
if (types.has('commandExecution')) parts.push('运行了命令');
|
|
508
|
-
if (types.has('mcpToolCall') || types.has('dynamicToolCall')) parts.push('调用了工具');
|
|
509
|
-
if (types.has('webSearch')) parts.push('搜索了网页');
|
|
510
|
-
if (types.has('imageView')) parts.push('查看了图像');
|
|
511
|
-
if (types.has('collabAgentToolCall') || types.has('subAgentActivity')) parts.push('处理了子任务');
|
|
512
|
-
const available = hasMore || items.some(item => item && !['userMessage', 'agentMessage'].includes(item.type));
|
|
513
|
-
return { available, title: parts.length ? `${parts.join(' · ')}${hasMore ? '等' : ''}` : '执行过程' };
|
|
514
|
+
function isSubAgentItem(item) {
|
|
515
|
+
return item?.type === 'collabAgentToolCall' || item?.type === 'subAgentActivity';
|
|
514
516
|
}
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import events from '../shared/app-server-events.cjs';
|
|
2
2
|
|
|
3
3
|
export function sharedThreadProjectId(thread, projects) {
|
|
4
|
-
|
|
5
|
-
const local = thread.environments?.find(environment => environment.environmentId === 'local');
|
|
6
|
-
if (local && Array.isArray(local.runtimeWorkspaceRoots)) {
|
|
7
|
-
for (const root of local.runtimeWorkspaceRoots) {
|
|
8
|
-
const projectId = events.threadProjectId({ cwd: root }, projects);
|
|
9
|
-
if (projectId) return projectId;
|
|
10
|
-
}
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
// Unloaded Desktop history omits environments and often has no persisted project ID.
|
|
14
|
-
return events.threadProjectId({ cwd: thread.cwd }, projects);
|
|
4
|
+
return events.catalogThreadProjectId(thread, projects);
|
|
15
5
|
}
|
|
16
6
|
|
|
17
7
|
// Mobile catalog order comes entirely from app-server authority, independent of Desktop UI state.
|
|
@@ -18,9 +18,9 @@ const active = value => value && ['scheduled', 'running'].includes(value.status)
|
|
|
18
18
|
const recoverySteps = () => [
|
|
19
19
|
['checking', '检查启动条件'], ['stopping-desktop', '关闭当前 Codex'],
|
|
20
20
|
['starting-app-server', '确认 Codex 内核'], ['starting-desktop', '重新打开 Codex'],
|
|
21
|
-
['checking-execution', '
|
|
21
|
+
['checking-execution', '确认移动端控制就绪'],
|
|
22
22
|
].map(([id, label]) => ({ id, label, status: 'pending' }));
|
|
23
|
-
export const recoveryWarning = 'Codex Desktop 需要重新打开,才能接入同一个 Codex
|
|
23
|
+
export const recoveryWarning = 'Codex Desktop 需要重新打开,才能接入同一个 Codex 内核。移动端连接和 Codex 内核会保持运行;旧 Desktop 实例中的任务可能中断,未发送内容请先保存。切换不会清空项目、对话历史、设置或设备授权。';
|
|
24
24
|
|
|
25
25
|
export async function recoveryStatus(stateRoot, host = desktopHost) {
|
|
26
26
|
const record = readRecord(recordPath(stateRoot));
|
|
@@ -42,7 +42,7 @@ export async function planRecovery(stateRoot, host, available = portAvailable) {
|
|
|
42
42
|
const info = await host.inspect();
|
|
43
43
|
const action = await host.sharedServiceUrl() ? 'restart' : 'switch';
|
|
44
44
|
if (!info.desktop) throw new Error('未找到 Codex Desktop;CLI 用户无需执行 Desktop 接入。');
|
|
45
|
-
const record = readRecord(path.join(stateRoot, 'managed-app-server.json'))
|
|
45
|
+
const record = readRecord(path.join(stateRoot, 'managed-app-server.json'));
|
|
46
46
|
const actual = record ? await host.process(record.pid) : null;
|
|
47
47
|
if (actual && (!sameProcess(record, actual) || !actual.command?.includes('app-server') || !actual.command.includes('ws://127.0.0.1:45839'))) {
|
|
48
48
|
throw new Error('Codex 内核进程身份与记录不符,未结束任何进程。');
|
|
@@ -84,7 +84,7 @@ export async function runRecovery(stateRoot, operationId, dependencies = {}) {
|
|
|
84
84
|
const host = dependencies.host || desktopHost;
|
|
85
85
|
const service = dependencies.service || lifecycle({ stateRoot, host: serviceHost });
|
|
86
86
|
const available = dependencies.available || portAvailable;
|
|
87
|
-
const start = dependencies.launch ||
|
|
87
|
+
const start = dependencies.launch || launchDesktop;
|
|
88
88
|
let record = readRecord(recordPath(stateRoot));
|
|
89
89
|
if (record?.operationId !== operationId || readRecord(lockPath(stateRoot))?.operationId !== operationId) throw new Error('恢复实例已失效。');
|
|
90
90
|
record.steps ||= recoverySteps();
|
|
@@ -113,7 +113,7 @@ export async function runRecovery(stateRoot, operationId, dependencies = {}) {
|
|
|
113
113
|
return health.running && health.mode === 'lan-shared' && health.execution?.ready === true;
|
|
114
114
|
}, 'Codex 内核未就绪;Bridge 和中继管理连接仍保留。');
|
|
115
115
|
record = { ...record, status: 'completed', stage: 'ready', steps: record.steps.map(step => ({ ...step, status: 'completed' })),
|
|
116
|
-
message: 'Codex Desktop 已接入 Codex
|
|
116
|
+
message: 'Codex Desktop 已接入 Codex 内核;移动端控制保持就绪,原任务未自动重发。' };
|
|
117
117
|
} catch (error) { record = { ...record, status: 'failed', error: error.message,
|
|
118
118
|
steps: record.steps.map(step => ({ ...step, status: step.status === 'running' ? 'failed' : step.status })),
|
|
119
119
|
message: 'Codex Desktop 未完成接入,已停止后续步骤。' }; }
|
|
@@ -6,11 +6,8 @@ import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
|
6
6
|
|
|
7
7
|
const owner = '@walkhi/code-relax';
|
|
8
8
|
const markerName = '.code-relax-install.json';
|
|
9
|
-
const legacyOwner = '@closedhi/codex-remote';
|
|
10
|
-
const legacyMarkerName = '.codex-remote-install.json';
|
|
11
9
|
const digest = value => createHash('sha256').update(value).digest('hex');
|
|
12
10
|
export const skillDirectory = (home = os.homedir()) => path.join(home, '.agents/skills/code-relax');
|
|
13
|
-
const legacySkillDirectory = (home = os.homedir()) => path.join(home, '.agents/skills/codex-remote');
|
|
14
11
|
|
|
15
12
|
// Refuse junctions/symlinks along managed paths before writing or removing files.
|
|
16
13
|
export function assertPlainPath(target) {
|
|
@@ -33,15 +30,6 @@ export function inspectSkill(target = skillDirectory()) {
|
|
|
33
30
|
return { installed: true, managed, target };
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
export function removeLegacyManagedSkill(target = legacySkillDirectory()) {
|
|
37
|
-
const marker = readRecord(path.join(target, legacyMarkerName));
|
|
38
|
-
const file = path.join(target, 'SKILL.md');
|
|
39
|
-
if (marker?.owner !== legacyOwner || !fs.existsSync(file) || marker.sha256 !== digest(fs.readFileSync(file))
|
|
40
|
-
|| !fs.readdirSync(target).every(name => ['SKILL.md', legacyMarkerName].includes(name))) return false;
|
|
41
|
-
fs.unlinkSync(file); fs.unlinkSync(path.join(target, legacyMarkerName)); fs.rmdirSync(target);
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
33
|
export function installSkill(source, target = skillDirectory()) {
|
|
46
34
|
const state = inspectSkill(target);
|
|
47
35
|
if (state.installed && !state.managed) throw new Error(`同名 Skill 非本包管理或已被修改,请保留并手动处理:${target}`);
|
|
@@ -49,7 +37,6 @@ export function installSkill(source, target = skillDirectory()) {
|
|
|
49
37
|
fs.mkdirSync(target, { recursive: true });
|
|
50
38
|
fs.writeFileSync(path.join(target, 'SKILL.md'), content);
|
|
51
39
|
writeRecord(path.join(target, markerName), { owner, sha256: digest(content) });
|
|
52
|
-
if (path.resolve(target) === path.resolve(skillDirectory())) removeLegacyManagedSkill();
|
|
53
40
|
return { installed: true, target, message: 'Skill 已安装/更新;若 Codex 尚未显示,请重新打开 Codex。' };
|
|
54
41
|
}
|
|
55
42
|
|