@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.
Files changed (78) hide show
  1. package/README.md +44 -54
  2. package/dist/bin/codex-remote.mjs +22 -58
  3. package/dist/shared/app-server-events.cjs +15 -3
  4. package/dist/shared/file-mentions.cjs +35 -0
  5. package/dist/skills/code-relax/SKILL.md +1 -1
  6. package/dist/src/desktop-launcher.mjs +6 -11
  7. package/dist/src/file-upload-session.mjs +124 -0
  8. package/dist/src/lan-socket-server.mjs +51 -8
  9. package/dist/src/managed-app-server.mjs +9 -7
  10. package/dist/src/onboarding.mjs +3 -3
  11. package/dist/src/platform/windows/desktop-host.mjs +1 -2
  12. package/dist/src/platform/windows/desktop-shortcuts.ps1 +6 -13
  13. package/dist/src/platform/windows/service-host.mjs +0 -9
  14. package/dist/src/self-relay/admin-page.mjs +1 -1
  15. package/dist/src/self-relay/connector.mjs +84 -12
  16. package/dist/src/self-relay/lifecycle.mjs +8 -24
  17. package/dist/src/self-relay/p2p-probe.mjs +1 -1
  18. package/dist/src/self-relay/server.mjs +67 -12
  19. package/dist/src/server.mjs +141 -34
  20. package/dist/src/service-doctor.mjs +1 -1
  21. package/dist/src/service-install.mjs +1 -4
  22. package/dist/src/shared-app-server.mjs +30 -28
  23. package/dist/src/shared-catalog.mjs +1 -11
  24. package/dist/src/shared-recovery.mjs +5 -5
  25. package/dist/src/skill-install.mjs +0 -13
  26. package/package.json +1 -1
  27. package/dist/src/debug-ui.mjs +0 -131
  28. package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
  29. package/dist/web/activity-view.js +0 -283
  30. package/dist/web/approval-ui.js +0 -188
  31. package/dist/web/capabilities.js +0 -50
  32. package/dist/web/chat-transport.js +0 -196
  33. package/dist/web/chat.css +0 -778
  34. package/dist/web/chat.js +0 -3815
  35. package/dist/web/client-platform.js +0 -11
  36. package/dist/web/community.css +0 -82
  37. package/dist/web/community.html +0 -36
  38. package/dist/web/composer-controller.js +0 -97
  39. package/dist/web/conversation-controller.js +0 -99
  40. package/dist/web/disclosure-state-controller.js +0 -95
  41. package/dist/web/draft-controller.js +0 -103
  42. package/dist/web/harmony-platform.js +0 -52
  43. package/dist/web/highlight-powershell.min.js +0 -39
  44. package/dist/web/highlight.min.js +0 -1262
  45. package/dist/web/history-cache.js +0 -240
  46. package/dist/web/history-controller.js +0 -167
  47. package/dist/web/index.html +0 -288
  48. package/dist/web/lan-session.js +0 -122
  49. package/dist/web/link-action-controller.js +0 -212
  50. package/dist/web/message-send-controller.js +0 -177
  51. package/dist/web/message-view.js +0 -98
  52. package/dist/web/p2p-data-channel.js +0 -50
  53. package/dist/web/p2p-diagnostics.js +0 -28
  54. package/dist/web/p2p-probe.js +0 -123
  55. package/dist/web/page-resume.js +0 -28
  56. package/dist/web/pending-message-store.js +0 -108
  57. package/dist/web/queue-controller.js +0 -82
  58. package/dist/web/resources.json +0 -1
  59. package/dist/web/self-relay-session.js +0 -129
  60. package/dist/web/server-requests.js +0 -67
  61. package/dist/web/station-connection-controller.js +0 -75
  62. package/dist/web/task-list-view.js +0 -289
  63. package/dist/web/thread-attention-controller.js +0 -124
  64. package/dist/web/thread-context-controller.js +0 -30
  65. package/dist/web/thread-list-controller.js +0 -61
  66. package/dist/web/thread-list-sync.js +0 -86
  67. package/dist/web/thread-title-controller.js +0 -57
  68. package/dist/web/timeline-formatters.js +0 -249
  69. package/dist/web/timeline-reducer.js +0 -77
  70. package/dist/web/timeline-renderer.js +0 -161
  71. package/dist/web/timeline-scroll-controller.js +0 -50
  72. package/dist/web/timeline-sync.js +0 -67
  73. package/dist/web/usage-controller.js +0 -258
  74. package/dist/web/vendor/README.md +0 -14
  75. package/dist/web/vendor/highlight-powershell.min.js +0 -39
  76. package/dist/web/vendor/highlight.LICENSE.txt +0 -29
  77. package/dist/web/vendor/highlight.min.js +0 -1262
  78. package/dist/web/vendor/lucide.LICENSE.txt +0 -17
@@ -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
- if (serveDebugResource(request, response, stateRoot, options.port)) return;
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, normalizeSharedTimeline, requestedSync, known);
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
- authorizeDevice: (clientId, credential) => authorizeLanDevice(stateRoot, clientId, credential),
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
- return { ...normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId: 'local', transport: 'shared-app-server' } }),
707
- historyView: 'grouped-lazy-skeleton-v11',
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
- title: item.title || '执行过程',
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 => !allowedUploadsRoots.some(root => path.dirname(file).toLowerCase() === root.toLowerCase())
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 requestMarker = text.lastIndexOf('## My request:');
1217
- if (requestMarker >= 0) {
1218
- text = text.slice(requestMarker + '## My request:'.length).trim();
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', '手机连通性未验证。首次启动请允许记录的 Node 在受信任的专用网络通信;若曾拒绝,可在 Windows 安全中心的防火墙“允许应用通过防火墙”中检查该 Node。不要关闭整个防火墙。');
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 groupId = `g${groups.size + 1}`;
347
- const outline = operationOutline(visibleItems, false);
348
- groups.set(groupId, visibleItems);
349
- skeleton.push({ type: 'lazyOperationGroup', id: `${turn.id}:${groupId}`,
350
- turnId: turn.id, groupId, title: outline.title, count: visibleItems.length });
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 operationOutline(items, hasMore) {
504
- const types = new Set(items.map(item => item?.type));
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
- if (thread.projectId) return thread.projectId;
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 内核。手机连接和 Codex 内核会保持运行;旧 Desktop 实例中的任务可能中断,未发送内容请先保存。切换不会清空项目、对话历史、设置或设备授权。';
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')) || readRecord(path.join(stateRoot, 'shared-desktop.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 || (options => launchDesktop('shared', options));
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkhi/code-relax",
3
- "version": "0.1.0-beta.10",
3
+ "version": "0.1.0-beta.11",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "scripts": {