@walkhi/code-relax 0.1.0-beta.1 → 0.1.0-beta.10

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 (91) hide show
  1. package/README.md +14 -15
  2. package/dist/bin/codex-remote.mjs +81 -26
  3. package/dist/bin/self-relay-admin.mjs +8 -0
  4. package/dist/bin/self-relay-demo.mjs +1 -1
  5. package/dist/bin/self-relay-server.mjs +5 -2
  6. package/dist/shared/app-server-events.cjs +50 -22
  7. package/dist/shared/p2p-data-channel.cjs +50 -0
  8. package/dist/skills/code-relax/SKILL.md +1 -1
  9. package/dist/src/desktop-launcher.mjs +11 -11
  10. package/dist/src/lan-socket-server.mjs +8 -2
  11. package/dist/src/managed-app-server.mjs +27 -7
  12. package/dist/src/message-images.mjs +0 -18
  13. package/dist/src/onboarding.mjs +3 -5
  14. package/dist/src/platform/README.md +1 -1
  15. package/dist/src/platform/windows/IsolatedProcess.cs +7 -3
  16. package/dist/src/platform/windows/background-process.mjs +7 -2
  17. package/dist/src/platform/windows/codex-runtime.mjs +117 -0
  18. package/dist/src/platform/windows/desktop-host.mjs +16 -1
  19. package/dist/src/platform/windows/desktop-info.ps1 +2 -4
  20. package/dist/src/platform/windows/desktop-shortcut-run.ps1 +1 -1
  21. package/dist/src/platform/windows/desktop-shortcuts.ps1 +1 -1
  22. package/dist/src/platform/windows/launch-worker.mjs +42 -0
  23. package/dist/src/platform/windows/resident-startup.ps1 +58 -18
  24. package/dist/src/platform/windows/service-host.mjs +4 -7
  25. package/dist/src/platform/windows/start-hidden-console.ps1 +18 -8
  26. package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
  27. package/dist/src/runtime-discovery.mjs +43 -0
  28. package/dist/src/self-relay/admin-cloud.mjs +101 -0
  29. package/dist/src/self-relay/admin-page.mjs +71 -0
  30. package/dist/src/self-relay/admin-server.mjs +61 -0
  31. package/dist/src/self-relay/admin-state.mjs +122 -0
  32. package/dist/src/self-relay/client.mjs +2 -0
  33. package/dist/src/self-relay/connector.mjs +23 -8
  34. package/dist/src/self-relay/demo.mjs +4 -5
  35. package/dist/src/self-relay/lifecycle.mjs +53 -49
  36. package/dist/src/self-relay/p2p-probe.mjs +123 -83
  37. package/dist/src/self-relay/server.mjs +157 -17
  38. package/dist/src/server.mjs +484 -1274
  39. package/dist/src/service-doctor.mjs +13 -25
  40. package/dist/src/service-lifecycle.mjs +12 -12
  41. package/dist/src/shared-app-server.mjs +192 -34
  42. package/dist/src/shared-catalog.mjs +4 -14
  43. package/dist/src/shared-recovery.mjs +8 -6
  44. package/dist/src/shared-thread-stream.mjs +1 -1
  45. package/dist/web/activity-view.js +283 -0
  46. package/dist/web/capabilities.js +2 -9
  47. package/dist/web/chat-transport.js +21 -21
  48. package/dist/web/chat.css +148 -106
  49. package/dist/web/chat.js +1454 -2788
  50. package/dist/web/community.css +82 -0
  51. package/dist/web/community.html +36 -0
  52. package/dist/web/composer-controller.js +97 -0
  53. package/dist/web/conversation-controller.js +99 -0
  54. package/dist/web/disclosure-state-controller.js +95 -0
  55. package/dist/web/draft-controller.js +103 -0
  56. package/dist/web/harmony-platform.js +3 -2
  57. package/dist/web/history-cache.js +112 -18
  58. package/dist/web/history-controller.js +167 -0
  59. package/dist/web/index.html +113 -47
  60. package/dist/web/link-action-controller.js +212 -0
  61. package/dist/web/message-send-controller.js +177 -0
  62. package/dist/web/message-view.js +98 -0
  63. package/dist/web/p2p-data-channel.js +50 -0
  64. package/dist/web/p2p-probe.js +67 -27
  65. package/dist/web/page-resume.js +28 -0
  66. package/dist/web/pending-message-store.js +108 -0
  67. package/dist/web/queue-controller.js +82 -0
  68. package/dist/web/resources.json +1 -1
  69. package/dist/web/self-relay-session.js +28 -18
  70. package/dist/web/station-connection-controller.js +75 -0
  71. package/dist/web/task-list-view.js +289 -0
  72. package/dist/web/thread-attention-controller.js +124 -0
  73. package/dist/web/thread-context-controller.js +30 -0
  74. package/dist/web/thread-list-controller.js +61 -0
  75. package/dist/web/thread-list-sync.js +86 -0
  76. package/dist/web/thread-title-controller.js +57 -0
  77. package/dist/web/timeline-formatters.js +249 -0
  78. package/dist/web/timeline-reducer.js +77 -0
  79. package/dist/web/timeline-renderer.js +161 -0
  80. package/dist/web/timeline-scroll-controller.js +50 -0
  81. package/dist/web/usage-controller.js +258 -0
  82. package/dist/web/vendor/lucide.LICENSE.txt +17 -0
  83. package/package.json +16 -18
  84. package/tools/postinstall.mjs +140 -3
  85. package/dist/src/app-server-client.mjs +0 -468
  86. package/dist/src/app-server-tasks.mjs +0 -355
  87. package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
  88. package/dist/src/platform/windows/desktop-tools.mjs +0 -48
  89. package/dist/src/platform/windows/launch-worker.ps1 +0 -13
  90. package/dist/src/thread-catalog.mjs +0 -42
  91. package/dist/tools/find-desktop-pipe.mjs +0 -10
@@ -0,0 +1,283 @@
1
+ (function (root) {
2
+ 'use strict';
3
+
4
+ function createActivityView(options) {
5
+ const {
6
+ document, disclosures, entryDisclosureKey, activityIcon, disclosureChevronElement,
7
+ codeBlockIcon, copyText, showToast, compactActivityElement,
8
+ activityPreview, commandStatusText, diffLineCounts, formatDuration, statusText, toolStatusText,
9
+ } = options;
10
+
11
+ function copyButton(value, label) {
12
+ const button = document.createElement('button');
13
+ button.type = 'button';
14
+ button.className = 'activity-copy';
15
+ button.title = label;
16
+ button.setAttribute('aria-label', label);
17
+ button.append(codeBlockIcon('copy'));
18
+ button.onclick = async event => {
19
+ event.preventDefault();
20
+ event.stopPropagation();
21
+ if (button.dataset.copyState === 'copied') return;
22
+ try {
23
+ await copyText(value);
24
+ button.dataset.copyState = 'copied';
25
+ button.title = '已复制';
26
+ button.setAttribute('aria-label', '已复制');
27
+ button.replaceChildren(codeBlockIcon('check'));
28
+ setTimeout(() => {
29
+ if (!button.isConnected) return;
30
+ delete button.dataset.copyState;
31
+ button.title = label;
32
+ button.setAttribute('aria-label', label);
33
+ button.replaceChildren(codeBlockIcon('copy'));
34
+ }, 1500);
35
+ } catch {
36
+ showToast('复制失败,可长按选择文字。', 3000);
37
+ }
38
+ };
39
+ return button;
40
+ }
41
+
42
+ function command(entry) {
43
+ const entryStatus = statusText(entry.status);
44
+ const commandText = Array.isArray(entry.command) ? entry.command.join(' ') : String(entry.command || '');
45
+ const output = String(entry.output || '');
46
+ const preview = activityPreview(commandText);
47
+ const details = document.createElement('details');
48
+ details.className = 'activity command-activity';
49
+ const summary = document.createElement('summary');
50
+ const label = document.createElement('span');
51
+ label.className = 'activity-title';
52
+ label.textContent = `${commandStatusText(entry.status)}${preview ? ` ${preview}` : ''}`;
53
+ summary.append(activityIcon('command'), label, disclosureChevronElement());
54
+ const shell = document.createElement('div');
55
+ shell.className = 'command-shell';
56
+ if (commandText) {
57
+ const row = document.createElement('div');
58
+ row.className = 'command-shell-command';
59
+ const prompt = document.createElement('span');
60
+ prompt.className = 'command-shell-prompt';
61
+ prompt.textContent = '$';
62
+ const code = document.createElement('code');
63
+ code.textContent = commandText;
64
+ row.append(prompt, code, copyButton(commandText, '复制命令'));
65
+ shell.append(row);
66
+ }
67
+ const outputArea = document.createElement('div');
68
+ outputArea.className = 'command-shell-output';
69
+ const outputText = document.createElement('pre');
70
+ outputText.textContent = output || (entryStatus === 'inProgress' ? '' : '无输出');
71
+ if (!output) outputText.classList.add('empty');
72
+ outputArea.append(outputText);
73
+ if (output) outputArea.append(copyButton(output, '复制输出'));
74
+ shell.append(outputArea);
75
+ const footer = document.createElement('div');
76
+ footer.className = 'command-shell-footer';
77
+ const cwd = document.createElement('span');
78
+ cwd.className = 'command-shell-cwd';
79
+ cwd.textContent = entry.cwd ? `目录:${entry.cwd}` : '';
80
+ cwd.title = entry.cwd || '';
81
+ const status = document.createElement('span');
82
+ status.className = 'command-shell-status';
83
+ const duration = Number(entry.durationMs);
84
+ const durationText = Number.isFinite(duration) && duration >= 0 ? ` · ${formatDuration(duration)}` : '';
85
+ if (entryStatus === 'inProgress') status.textContent = `运行中${durationText}`;
86
+ else if (entryStatus === 'interrupted') status.textContent = `已停止${durationText}`;
87
+ else if (entry.exitCode === 0) status.textContent = `成功${durationText}`;
88
+ else if (entry.exitCode != null) status.textContent = `退出码 ${entry.exitCode}${durationText}`;
89
+ else status.textContent = `${entryStatus === 'failed' ? '失败' : '已完成'}${durationText}`;
90
+ footer.append(cwd, status);
91
+ shell.append(footer);
92
+ details.append(summary, shell);
93
+ return disclosures.bind(details, entryDisclosureKey(entry));
94
+ }
95
+
96
+ const fieldLabels = {
97
+ cmd: '命令', command: '命令', cwd: '目录', workdir: '目录', path: '路径', file: '文件',
98
+ query: '查询', url: '地址', prompt: '内容', message: '消息', target: '目标', pattern: '匹配',
99
+ server: '服务', tool: '工具', status: '状态', language: '语言', timeout: '超时',
100
+ };
101
+
102
+ function fieldLabel(value) {
103
+ const key = String(value || '字段');
104
+ return fieldLabels[key] || key.replaceAll('_', ' ');
105
+ }
106
+
107
+ function parsedStructuredText(value) {
108
+ const text = String(value || '').trim();
109
+ if (!text || !['{', '['].includes(text[0])) return null;
110
+ try { return JSON.parse(text); } catch { return null; }
111
+ }
112
+
113
+ function appendReadableValue(container, value, depth = 0) {
114
+ if (typeof value === 'string') {
115
+ const structured = parsedStructuredText(value);
116
+ if (structured != null) { appendReadableValue(container, structured, depth); return; }
117
+ const node = document.createElement(value.includes('\n') ? 'pre' : 'span');
118
+ node.className = value.includes('\n') ? 'tool-detail-text' : 'tool-detail-value';
119
+ node.textContent = value || '空';
120
+ container.append(node);
121
+ return;
122
+ }
123
+ if (value == null || typeof value !== 'object') {
124
+ const node = document.createElement('span');
125
+ node.className = 'tool-detail-value';
126
+ node.textContent = value == null ? '无' : String(value);
127
+ container.append(node);
128
+ return;
129
+ }
130
+ if (depth >= 3) {
131
+ const node = document.createElement('span');
132
+ node.className = 'tool-detail-value muted';
133
+ node.textContent = Array.isArray(value) ? `${value.length} 项` : `${Object.keys(value).length} 个字段`;
134
+ container.append(node);
135
+ return;
136
+ }
137
+ if (Array.isArray(value)) {
138
+ if (!value.length) {
139
+ const node = document.createElement('span');
140
+ node.className = 'tool-detail-value muted';
141
+ node.textContent = '无';
142
+ container.append(node);
143
+ return;
144
+ }
145
+ const list = document.createElement('ul');
146
+ list.className = 'tool-detail-list';
147
+ for (const item of value.slice(0, 20)) {
148
+ const row = document.createElement('li');
149
+ appendReadableValue(row, item, depth + 1);
150
+ list.append(row);
151
+ }
152
+ if (value.length > 20) {
153
+ const more = document.createElement('li');
154
+ more.className = 'muted';
155
+ more.textContent = `另有 ${value.length - 20} 项`;
156
+ list.append(more);
157
+ }
158
+ container.append(list);
159
+ return;
160
+ }
161
+ const entries = Object.entries(value);
162
+ if (!entries.length) {
163
+ const node = document.createElement('span');
164
+ node.className = 'tool-detail-value muted';
165
+ node.textContent = '无';
166
+ container.append(node);
167
+ return;
168
+ }
169
+ const fields = document.createElement('dl');
170
+ fields.className = 'tool-detail-fields';
171
+ for (const [key, fieldValue] of entries.slice(0, 24)) {
172
+ const term = document.createElement('dt');
173
+ term.textContent = fieldLabel(key);
174
+ const description = document.createElement('dd');
175
+ appendReadableValue(description, fieldValue, depth + 1);
176
+ fields.append(term, description);
177
+ }
178
+ if (entries.length > 24) {
179
+ const description = document.createElement('dd');
180
+ description.className = 'muted';
181
+ description.textContent = `另有 ${entries.length - 24} 个字段`;
182
+ fields.append(description);
183
+ }
184
+ container.append(fields);
185
+ }
186
+
187
+ function detailSection(title, value, className = '') {
188
+ const section = document.createElement('section');
189
+ section.className = `tool-detail-section ${className}`.trim();
190
+ const heading = document.createElement('div');
191
+ heading.className = 'tool-detail-heading';
192
+ heading.textContent = title;
193
+ section.append(heading);
194
+ appendReadableValue(section, value);
195
+ return section;
196
+ }
197
+
198
+ function tool(entry) {
199
+ const source = [entry.server, entry.tool].filter(Boolean).join('/');
200
+ const title = `${toolStatusText(entry.status)}${source ? ` ${source}` : '工具'}`;
201
+ const argumentsValue = entry.arguments && typeof entry.arguments === 'object' ? entry.arguments : {};
202
+ const resources = Array.isArray(entry.resources) ? entry.resources : [];
203
+ const hasDetails = Object.keys(argumentsValue).length > 0 || entry.resultText || entry.errorText || resources.length || entry.mediaCount;
204
+ if (!hasDetails) return compactActivityElement(title, 'tool');
205
+ const details = document.createElement('details');
206
+ details.className = 'activity tool-activity';
207
+ const summary = document.createElement('summary');
208
+ const label = document.createElement('span');
209
+ label.className = 'activity-title';
210
+ label.textContent = title;
211
+ summary.append(activityIcon('tool'), label, disclosureChevronElement());
212
+ const body = document.createElement('div');
213
+ body.className = 'tool-detail';
214
+ if (Object.keys(argumentsValue).length) body.append(detailSection('参数', argumentsValue));
215
+ if (entry.resultText) body.append(detailSection('结果', entry.resultText));
216
+ if (entry.errorText) body.append(detailSection('错误', entry.errorText, 'error'));
217
+ if (resources.length) body.append(detailSection('资源', resources));
218
+ if (entry.mediaCount) body.append(detailSection('媒体', `返回了 ${entry.mediaCount} 个媒体结果`));
219
+ const duration = Number(entry.durationMs);
220
+ if (Number.isFinite(duration) && duration >= 0) {
221
+ const footer = document.createElement('div');
222
+ footer.className = 'tool-detail-footer';
223
+ footer.textContent = formatDuration(duration);
224
+ body.append(footer);
225
+ }
226
+ details.append(summary, body);
227
+ return disclosures.bind(details, entryDisclosureKey(entry));
228
+ }
229
+
230
+ function activity(title, text, status, iconKind = null, disclosureKey = '') {
231
+ const details = document.createElement('details');
232
+ details.className = 'activity';
233
+ const summary = document.createElement('summary');
234
+ if (iconKind) summary.append(activityIcon(iconKind));
235
+ const label = document.createElement('span');
236
+ label.className = 'activity-title';
237
+ if (typeof title === 'string') label.textContent = title;
238
+ else label.append(title);
239
+ summary.append(label, disclosureChevronElement());
240
+ const content = document.createElement('pre');
241
+ content.textContent = text || '无详细信息';
242
+ details.append(summary, content);
243
+ return disclosures.bind(details, disclosureKey);
244
+ }
245
+
246
+ function fileChange(entry) {
247
+ const entryStatus = statusText(entry.status);
248
+ const changes = Array.isArray(entry.changes) ? entry.changes : [];
249
+ if (!changes.length) {
250
+ const title = entryStatus === 'completed' || entryStatus === 'failed' ? '已编辑文件' : '正在编辑文件';
251
+ return activity(title, '无详细信息', entry.status, 'edit');
252
+ }
253
+ const fragment = document.createDocumentFragment();
254
+ for (const [index, change] of changes.entries()) {
255
+ const title = document.createDocumentFragment();
256
+ const label = document.createElement('span');
257
+ const action = entryStatus === 'completed' || entryStatus === 'failed' ? '已编辑' : '正在编辑';
258
+ const filename = String(change.path || '文件').split(/[\\/]/).filter(Boolean).at(-1) || '文件';
259
+ label.textContent = `${action} ${filename}`;
260
+ title.append(label);
261
+ const counts = diffLineCounts(typeof change.diff === 'string' ? change.diff : change.diff?.text);
262
+ if (counts.added || counts.removed) {
263
+ const added = document.createElement('span');
264
+ added.className = 'diff-add';
265
+ added.textContent = `+${counts.added}`;
266
+ const removed = document.createElement('span');
267
+ removed.className = 'diff-remove';
268
+ removed.textContent = `-${counts.removed}`;
269
+ title.append(added, removed);
270
+ }
271
+ const diff = typeof change.diff === 'string' ? change.diff : change.diff?.text || '';
272
+ const detail = [change.path ? `路径:${change.path}` : '', diff].filter(Boolean).join('\n\n');
273
+ fragment.append(activity(title, detail || '已记录文件修改', entry.status, 'edit',
274
+ entryDisclosureKey(entry, `file:${index}:${change.path || ''}`)));
275
+ }
276
+ return fragment;
277
+ }
278
+
279
+ return { activity, command, fileChange, tool };
280
+ }
281
+
282
+ root.createActivityView = createActivityView;
283
+ })(globalThis);
@@ -4,14 +4,10 @@
4
4
  })(globalThis, function () {
5
5
  // Execution semantics belong to a connection/task, never to a client OS.
6
6
  const profiles = {
7
- 'lan-legacy': { input: 'markdown-path', history: 'recent', settings: 'next-message', updates: 'snapshot-poll',
8
- stop: 'none', queueOwner: 'bridge', desktopNavigation: true },
9
7
  'lan-shared': { input: 'inline-image', history: 'paged', settings: 'immediate', updates: 'events',
10
- stop: 'primary', queueOwner: 'codex', archive: true, approvals: true, desktopNavigation: true, syncBeforeWrite: true, sharedRecovery: true },
8
+ stop: 'primary', queueOwner: 'codex', archive: true, approvals: true, syncBeforeWrite: true, sharedRecovery: true },
11
9
  relay: { input: 'inline-image', history: 'paged', settings: 'immediate', updates: 'events',
12
10
  stop: 'primary', queueOwner: 'codex', archive: true, approvals: true, syncBeforeWrite: true, historyCache: true, remoteImages: true, remoteFiles: true, managedConnection: true },
13
- 'app-server': { input: 'inline-image', history: 'recent', settings: 'next-message', updates: 'events',
14
- stop: 'separate', queueOwner: 'bridge' },
15
11
  };
16
12
  const unknown = Object.freeze({ input: 'unknown', history: 'unknown', settings: 'unknown', updates: 'unknown',
17
13
  stop: 'none', queueOwner: 'none' });
@@ -25,13 +21,10 @@
25
21
  case 'queue': return p.queueOwner !== 'none';
26
22
  case 'nativeInput': return ['local-image', 'inline-image'].includes(p.input);
27
23
  case 'primaryStop': return p.stop === 'primary';
28
- case 'separateStop': return p.stop === 'separate';
29
24
  case 'immediateSettings': return p.settings === 'immediate';
30
25
  case 'pagedHistory': return p.history === 'paged';
31
- case 'recentHistory': return p.history === 'recent';
32
26
  case 'archive': return p.archive === true;
33
- // The independent adapter keeps its existing snapshot-based UI contract.
34
- case 'incrementalEvents': return p.updates === 'events' && mode !== 'app-server';
27
+ case 'incrementalEvents': return p.updates === 'events';
35
28
  default: return p[feature] === true;
36
29
  }
37
30
  }
@@ -2,17 +2,18 @@
2
2
  const capabilities = root.CodexCapabilities;
3
3
  const selfRelay = root.codexSelfRelayConfig;
4
4
  const lanOrigin = selfRelay ? new URL(selfRelay.url).origin.replace(/^wss:/, 'https:').replace(/^ws:/, 'http:') : root.codexLanConfig?.address || root.location?.origin;
5
- const workstationScope = selfRelay
5
+ const stationScope = selfRelay
6
6
  ? JSON.stringify([selfRelay.url, selfRelay.room, selfRelay.clientId])
7
+ // workstationScope is the released native/Web wire field; stationScope is the domain name used here.
7
8
  : root.codexLanConfig?.workstationScope || '';
8
9
 
9
10
  class LanTransport extends (selfRelay ? root.CodexSelfRelaySession : root.CodexLanSession) {
10
- constructor(token) { super(token); this.mode = root.codexLanConfig?.transportMode || 'lan-legacy'; }
11
+ constructor(token) { super(token); this.mode = root.codexLanConfig?.transportMode || 'unavailable'; }
11
12
  connectionState() { return this.ready ? 'connected' : 'disconnected'; }
12
13
  async api(path, options = {}) {
13
14
  const body = await this.request(path, options);
14
15
  if (path.startsWith('/api/health')) {
15
- this.mode = body.transportMode || 'lan-legacy';
16
+ this.mode = body.transportMode || 'unavailable';
16
17
  this.recoveryAvailable = body.sharedRecoveryAvailable === true;
17
18
  this.execution = body.execution;
18
19
  this.managedAppServer = body.managedAppServer;
@@ -37,7 +38,7 @@
37
38
  return this.subscribe(`/api/threads/${encodeURIComponent(thread.id)}/events?${query}`);
38
39
  }
39
40
  async cacheScope() {
40
- if (workstationScope) return `workstation-v1:${workstationScope}`;
41
+ if (stationScope) return `workstation-v1:${stationScope}`;
41
42
  if (!selfRelay) return root.codexLanConfig?.cacheScope ? `${root.codexLanConfig.cacheScope}:${this.mode}` : null;
42
43
  const identity = [selfRelay.url, selfRelay.room, selfRelay.clientId, selfRelay.revision, selfRelay.credential].join('\0');
43
44
  const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(identity));
@@ -50,7 +51,7 @@
50
51
  }
51
52
  async readCached(id, thread) {
52
53
  const scope = await this.cacheScope();
53
- return scope ? root.codexHistoryCache?.get(scope, (workstationScope || selfRelay) ? id : JSON.stringify([thread?.hostId || 'local', id])) : null;
54
+ return scope ? root.codexHistoryCache?.get(scope, (stationScope || selfRelay) ? id : JSON.stringify([thread?.hostId || 'local', id])) : null;
54
55
  }
55
56
  async readLastCached() {
56
57
  const scope = await this.cacheScope();
@@ -58,7 +59,7 @@
58
59
  }
59
60
  async saveCached(data) {
60
61
  const scope = await this.cacheScope();
61
- if (scope) return root.codexHistoryCache?.save(scope, data, value => this.cacheHash(value), (workstationScope || selfRelay) ? data.thread.id : JSON.stringify([data.thread.hostId || 'local', data.thread.id]));
62
+ if (scope) return root.codexHistoryCache?.save(scope, data, value => this.cacheHash(value), (stationScope || selfRelay) ? data.thread.id : JSON.stringify([data.thread.hostId || 'local', data.thread.id]));
62
63
  }
63
64
  async addImage(blob) {
64
65
  if (!['image/jpeg', 'image/png', 'image/webp'].includes(blob.type) || !blob.size || blob.size > 8 * 1024 * 1024) throw new Error('请选择不超过 8 MiB 的 JPEG、PNG 或 WebP 图片');
@@ -76,9 +77,14 @@
76
77
  if (this.token) url.searchParams.set('token', this.token);
77
78
  return url.href;
78
79
  }
79
- async readImage(_threadId, id) {
80
+ async readImage(threadId, id, variant = 'original', recoveryToken = '', recoveryTurnId = '') {
80
81
  if (!/^[a-f0-9]{64}$/.test(id)) throw new Error('无效的图片标识');
81
- return (await this.api(`/api/images/${id}`)).url;
82
+ const query = new URLSearchParams();
83
+ if (variant === 'thumbnail') query.set('variant', 'thumbnail');
84
+ if (recoveryToken) query.set('recovery', recoveryToken);
85
+ if (threadId) query.set('threadId', threadId);
86
+ if (recoveryTurnId) query.set('turnId', recoveryTurnId);
87
+ return (await this.api(`/api/images/${id}${query.size ? `?${query}` : ''}`)).url;
82
88
  }
83
89
  async readFile(_threadId, id) {
84
90
  if (!/^[a-f0-9]{64}$/.test(id)) throw new Error('无效的文件标识');
@@ -86,15 +92,13 @@
86
92
  }
87
93
  }
88
94
 
89
- // Relay owns login, device pairing, chunking, reconnection and phone cache.
95
+ // Relay owns login, device pairing, reconnection and phone cache.
90
96
  // The LAN adapter owns the authenticated WebSocket; both expose the same UI operations.
91
97
  root.createChatTransport = function ({ token, currentThread, relay = root.codexTransport }) {
92
98
  const adapter = relay || new LanTransport(token);
93
99
  const modeFor = thread => {
94
100
  if (relay) return 'relay';
95
- if (thread?.transport === 'app-server') return 'app-server';
96
- if (thread?.hostId && thread.hostId !== 'local') return 'lan-legacy';
97
- return adapter.mode;
101
+ return adapter.mode;
98
102
  };
99
103
  const mode = () => modeFor(currentThread());
100
104
  const serviceAdvisory = () => {
@@ -110,13 +114,9 @@
110
114
  };
111
115
  if (adapter.execution && (!adapter.execution.ready || adapter.execution.state === 'recovering')) return {
112
116
  kind: 'execution-unavailable',
113
- title: adapter.execution.state === 'recovering' ? 'Codex 内核正在恢复' : 'Codex 内核暂不可用',
117
+ title: adapter.execution.state === 'recovering' ? 'Codex 内核正在恢复' : 'Codex 内核未就绪',
114
118
  detail: '工作站连接正常;点击查看当前状况、影响和可执行操作。'
115
119
  };
116
- if (adapter.mode === 'lan-legacy') return {
117
- kind: 'basic', title: '当前为基础控制',
118
- detail: '部分实时控制不可用;点击查看影响和恢复完整控制。'
119
- };
120
120
  return null;
121
121
  };
122
122
  return {
@@ -128,7 +128,7 @@
128
128
  return adapter.subscribe('/api/shared-recovery/events');
129
129
  },
130
130
  recoveryStatus: () => {
131
- if (relay) throw new Error('OpenAI 官方中继不支持从手机启用完整控制');
131
+ if (relay) throw new Error('OpenAI 官方中继不支持管理工作站 Codex 内核');
132
132
  return adapter.api('/api/shared-recovery');
133
133
  },
134
134
  recoverShared: operationId => {
@@ -140,9 +140,9 @@
140
140
  connectionLabel: (compact = false) => {
141
141
  if (relay) return compact ? 'OpenAI中继' : 'OpenAI 中继';
142
142
  const network = selfRelay ? compact ? 'CodeRelax中继' : 'Code Relax 中继' : '局域网';
143
- return serviceAdvisory() ? network : `${network} · ${adapter.mode === 'lan-shared' ? '完整控制' : '基础控制'}`;
143
+ return network;
144
144
  },
145
- draftScope: () => relay ? adapter.draftScope() : workstationScope || (selfRelay ? `${lanOrigin}/${selfRelay.room}` : lanOrigin),
145
+ draftScope: () => relay ? adapter.draftScope() : stationScope || (selfRelay ? `${lanOrigin}/${selfRelay.room}` : lanOrigin),
146
146
  capabilities: () => capabilities.describe(mode()),
147
147
  has: name => (name === 'historyCache' && Boolean(selfRelay || root.codexLanConfig?.cacheScope)) || capabilities.has(mode(), name),
148
148
  supports: (name, thread) => capabilities.has(modeFor(thread), name),
@@ -170,7 +170,7 @@
170
170
  addImage: blob => adapter.addImage(blob),
171
171
  removeImage: id => adapter.removeImage?.(id),
172
172
  clearImages: () => adapter.clearImages?.(),
173
- readImage: (id, path) => adapter.readImage(id, path),
173
+ readImage: (id, path, variant, recoveryToken, recoveryTurnId) => adapter.readImage(id, path, variant, recoveryToken, recoveryTurnId),
174
174
  readFile: (id, path) => adapter.readFile(id, path),
175
175
  readCached: async (id, thread) => adapter.readCached?.(id, thread),
176
176
  readLastCached: async () => adapter.readLastCached?.(),