@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.
- package/README.md +14 -15
- package/dist/bin/codex-remote.mjs +81 -26
- package/dist/bin/self-relay-admin.mjs +8 -0
- package/dist/bin/self-relay-demo.mjs +1 -1
- package/dist/bin/self-relay-server.mjs +5 -2
- package/dist/shared/app-server-events.cjs +50 -22
- package/dist/shared/p2p-data-channel.cjs +50 -0
- package/dist/skills/code-relax/SKILL.md +1 -1
- package/dist/src/desktop-launcher.mjs +11 -11
- package/dist/src/lan-socket-server.mjs +8 -2
- package/dist/src/managed-app-server.mjs +27 -7
- package/dist/src/message-images.mjs +0 -18
- package/dist/src/onboarding.mjs +3 -5
- package/dist/src/platform/README.md +1 -1
- package/dist/src/platform/windows/IsolatedProcess.cs +7 -3
- package/dist/src/platform/windows/background-process.mjs +7 -2
- package/dist/src/platform/windows/codex-runtime.mjs +117 -0
- package/dist/src/platform/windows/desktop-host.mjs +16 -1
- package/dist/src/platform/windows/desktop-info.ps1 +2 -4
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +1 -1
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +1 -1
- package/dist/src/platform/windows/launch-worker.mjs +42 -0
- package/dist/src/platform/windows/resident-startup.ps1 +58 -18
- package/dist/src/platform/windows/service-host.mjs +4 -7
- package/dist/src/platform/windows/start-hidden-console.ps1 +18 -8
- package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
- package/dist/src/runtime-discovery.mjs +43 -0
- package/dist/src/self-relay/admin-cloud.mjs +101 -0
- package/dist/src/self-relay/admin-page.mjs +71 -0
- package/dist/src/self-relay/admin-server.mjs +61 -0
- package/dist/src/self-relay/admin-state.mjs +122 -0
- package/dist/src/self-relay/client.mjs +2 -0
- package/dist/src/self-relay/connector.mjs +23 -8
- package/dist/src/self-relay/demo.mjs +4 -5
- package/dist/src/self-relay/lifecycle.mjs +53 -49
- package/dist/src/self-relay/p2p-probe.mjs +123 -83
- package/dist/src/self-relay/server.mjs +157 -17
- package/dist/src/server.mjs +484 -1274
- package/dist/src/service-doctor.mjs +13 -25
- package/dist/src/service-lifecycle.mjs +12 -12
- package/dist/src/shared-app-server.mjs +192 -34
- package/dist/src/shared-catalog.mjs +4 -14
- package/dist/src/shared-recovery.mjs +8 -6
- package/dist/src/shared-thread-stream.mjs +1 -1
- package/dist/web/activity-view.js +283 -0
- package/dist/web/capabilities.js +2 -9
- package/dist/web/chat-transport.js +21 -21
- package/dist/web/chat.css +148 -106
- package/dist/web/chat.js +1454 -2788
- package/dist/web/community.css +82 -0
- package/dist/web/community.html +36 -0
- package/dist/web/composer-controller.js +97 -0
- package/dist/web/conversation-controller.js +99 -0
- package/dist/web/disclosure-state-controller.js +95 -0
- package/dist/web/draft-controller.js +103 -0
- package/dist/web/harmony-platform.js +3 -2
- package/dist/web/history-cache.js +112 -18
- package/dist/web/history-controller.js +167 -0
- package/dist/web/index.html +113 -47
- package/dist/web/link-action-controller.js +212 -0
- package/dist/web/message-send-controller.js +177 -0
- package/dist/web/message-view.js +98 -0
- package/dist/web/p2p-data-channel.js +50 -0
- package/dist/web/p2p-probe.js +67 -27
- package/dist/web/page-resume.js +28 -0
- package/dist/web/pending-message-store.js +108 -0
- package/dist/web/queue-controller.js +82 -0
- package/dist/web/resources.json +1 -1
- package/dist/web/self-relay-session.js +28 -18
- package/dist/web/station-connection-controller.js +75 -0
- package/dist/web/task-list-view.js +289 -0
- package/dist/web/thread-attention-controller.js +124 -0
- package/dist/web/thread-context-controller.js +30 -0
- package/dist/web/thread-list-controller.js +61 -0
- package/dist/web/thread-list-sync.js +86 -0
- package/dist/web/thread-title-controller.js +57 -0
- package/dist/web/timeline-formatters.js +249 -0
- package/dist/web/timeline-reducer.js +77 -0
- package/dist/web/timeline-renderer.js +161 -0
- package/dist/web/timeline-scroll-controller.js +50 -0
- package/dist/web/usage-controller.js +258 -0
- package/dist/web/vendor/lucide.LICENSE.txt +17 -0
- package/package.json +16 -18
- package/tools/postinstall.mjs +140 -3
- package/dist/src/app-server-client.mjs +0 -468
- package/dist/src/app-server-tasks.mjs +0 -355
- package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
- package/dist/src/platform/windows/desktop-tools.mjs +0 -48
- package/dist/src/platform/windows/launch-worker.ps1 +0 -13
- package/dist/src/thread-catalog.mjs +0 -42
- 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);
|
package/dist/web/capabilities.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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 || '
|
|
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 || '
|
|
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 (
|
|
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, (
|
|
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), (
|
|
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(
|
|
80
|
+
async readImage(threadId, id, variant = 'original', recoveryToken = '', recoveryTurnId = '') {
|
|
80
81
|
if (!/^[a-f0-9]{64}$/.test(id)) throw new Error('无效的图片标识');
|
|
81
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
143
|
+
return network;
|
|
144
144
|
},
|
|
145
|
-
draftScope: () => relay ? adapter.draftScope() :
|
|
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?.(),
|