@walkhi/code-relax 0.1.0-beta.10 → 0.1.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -54
- package/dist/bin/codex-remote.mjs +34 -60
- package/dist/bin/migrate-relay-db.mjs +104 -0
- package/dist/bin/self-relay-server.mjs +17 -10
- package/dist/shared/app-server-events.cjs +15 -3
- package/dist/shared/file-mentions.cjs +35 -0
- package/dist/shared/server-requests.cjs +86 -58
- package/dist/skills/code-relax/SKILL.md +1 -1
- package/dist/src/desktop-launcher.mjs +6 -11
- package/dist/src/file-upload-session.mjs +124 -0
- package/dist/src/lan-socket-server.mjs +51 -8
- package/dist/src/managed-app-server.mjs +21 -10
- package/dist/src/onboarding.mjs +3 -3
- package/dist/src/platform/windows/codex-runtime.mjs +138 -4
- package/dist/src/platform/windows/desktop-host.mjs +1 -2
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -0
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +6 -13
- package/dist/src/platform/windows/resident-start.js +8 -0
- package/dist/src/platform/windows/resident-startup.ps1 +6 -3
- package/dist/src/platform/windows/service-host.mjs +0 -9
- package/dist/src/self-relay/admin-page.mjs +1 -1
- package/dist/src/self-relay/admin-state.mjs +8 -6
- package/dist/src/self-relay/connector.mjs +84 -12
- package/dist/src/self-relay/email-accounts.mjs +125 -0
- package/dist/src/self-relay/lifecycle.mjs +52 -100
- package/dist/src/self-relay/mysql-state.mjs +104 -0
- package/dist/src/self-relay/p2p-probe.mjs +1 -1
- package/dist/src/self-relay/server.mjs +155 -38
- package/dist/src/server.mjs +736 -610
- package/dist/src/service-doctor.mjs +1 -1
- package/dist/src/service-install.mjs +1 -4
- package/dist/src/shared-app-server.mjs +274 -272
- package/dist/src/shared-catalog.mjs +1 -11
- package/dist/src/shared-recovery.mjs +5 -5
- package/dist/src/skill-install.mjs +0 -13
- package/package.json +2 -1
- package/dist/src/debug-ui.mjs +0 -131
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
- package/dist/web/activity-view.js +0 -283
- package/dist/web/approval-ui.js +0 -188
- package/dist/web/capabilities.js +0 -50
- package/dist/web/chat-transport.js +0 -196
- package/dist/web/chat.css +0 -778
- package/dist/web/chat.js +0 -3815
- package/dist/web/client-platform.js +0 -11
- package/dist/web/community.css +0 -82
- package/dist/web/community.html +0 -36
- package/dist/web/composer-controller.js +0 -97
- package/dist/web/conversation-controller.js +0 -99
- package/dist/web/disclosure-state-controller.js +0 -95
- package/dist/web/draft-controller.js +0 -103
- package/dist/web/harmony-platform.js +0 -52
- package/dist/web/highlight-powershell.min.js +0 -39
- package/dist/web/highlight.min.js +0 -1262
- package/dist/web/history-cache.js +0 -240
- package/dist/web/history-controller.js +0 -167
- package/dist/web/index.html +0 -288
- package/dist/web/lan-session.js +0 -122
- package/dist/web/link-action-controller.js +0 -212
- package/dist/web/message-send-controller.js +0 -177
- package/dist/web/message-view.js +0 -98
- package/dist/web/p2p-data-channel.js +0 -50
- package/dist/web/p2p-diagnostics.js +0 -28
- package/dist/web/p2p-probe.js +0 -123
- package/dist/web/page-resume.js +0 -28
- package/dist/web/pending-message-store.js +0 -108
- package/dist/web/queue-controller.js +0 -82
- package/dist/web/resources.json +0 -1
- package/dist/web/self-relay-session.js +0 -129
- package/dist/web/server-requests.js +0 -67
- package/dist/web/station-connection-controller.js +0 -75
- package/dist/web/task-list-view.js +0 -289
- package/dist/web/thread-attention-controller.js +0 -124
- package/dist/web/thread-context-controller.js +0 -30
- package/dist/web/thread-list-controller.js +0 -61
- package/dist/web/thread-list-sync.js +0 -86
- package/dist/web/thread-title-controller.js +0 -57
- package/dist/web/timeline-formatters.js +0 -249
- package/dist/web/timeline-reducer.js +0 -77
- package/dist/web/timeline-renderer.js +0 -161
- package/dist/web/timeline-scroll-controller.js +0 -50
- package/dist/web/timeline-sync.js +0 -67
- package/dist/web/usage-controller.js +0 -258
- package/dist/web/vendor/README.md +0 -14
- package/dist/web/vendor/highlight-powershell.min.js +0 -39
- package/dist/web/vendor/highlight.LICENSE.txt +0 -29
- package/dist/web/vendor/highlight.min.js +0 -1262
- package/dist/web/vendor/lucide.LICENSE.txt +0 -17
package/dist/web/approval-ui.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
(function (root) {
|
|
2
|
-
root.createApprovalPanel = function (host, respond, pendingChanged = () => {}) {
|
|
3
|
-
const cards = new Map();
|
|
4
|
-
let connected = false;
|
|
5
|
-
const titles = { accept: '允许本次', acceptForSession: '本会话允许', decline: '拒绝', cancel: '拒绝并停止' };
|
|
6
|
-
function content(text, label, collapsed = false) {
|
|
7
|
-
const full = document.createElement('pre'); full.textContent = text;
|
|
8
|
-
if (!text) full.hidden = true;
|
|
9
|
-
if (!collapsed && text.length <= 180 && text.split('\n').length <= 3) return full;
|
|
10
|
-
const details = document.createElement('details'); details.className = 'approval-details';
|
|
11
|
-
const summary = document.createElement('summary');
|
|
12
|
-
const title = document.createElement('span'); title.className = 'approval-details-title'; title.textContent = label;
|
|
13
|
-
const preview = document.createElement('span'); preview.className = 'approval-preview';
|
|
14
|
-
preview.textContent = text.split('\n').slice(0, 3).join('\n').slice(0, 180) + '…';
|
|
15
|
-
summary.append(title);
|
|
16
|
-
if (!collapsed) summary.append(preview);
|
|
17
|
-
details.append(summary, full);
|
|
18
|
-
return details;
|
|
19
|
-
}
|
|
20
|
-
function paragraph(text, className = '') {
|
|
21
|
-
const p = document.createElement('p'); p.textContent = text; p.className = className; return p;
|
|
22
|
-
}
|
|
23
|
-
function shellPresentation(command) {
|
|
24
|
-
if (typeof command !== 'string') return null;
|
|
25
|
-
const match = command.trim().match(/^(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+([\s\S]*))?$/);
|
|
26
|
-
if (!match) return null;
|
|
27
|
-
const executable = match[1] || match[2] || match[3];
|
|
28
|
-
const name = executable.replaceAll('\\', '/').split('/').at(-1)?.toLowerCase() || '';
|
|
29
|
-
const rest = match[4] || '';
|
|
30
|
-
const shells = [
|
|
31
|
-
{ names: ['powershell', 'powershell.exe', 'pwsh', 'pwsh.exe'], label: 'PowerShell', marker: /(?:^|\s)-(?:command|c)\s+/i },
|
|
32
|
-
{ names: ['cmd', 'cmd.exe'], label: '命令提示符', marker: /(?:^|\s)\/(?:c|k)\s+/i },
|
|
33
|
-
{ names: ['bash', 'bash.exe', 'sh', 'sh.exe'], label: 'Shell', marker: /(?:^|\s)-(?:c|lc)\s+/i }
|
|
34
|
-
];
|
|
35
|
-
const shell = shells.find(candidate => candidate.names.includes(name));
|
|
36
|
-
if (!shell) return null;
|
|
37
|
-
const marker = shell.marker.exec(rest);
|
|
38
|
-
const text = marker ? rest.slice(marker.index + marker[0].length).trim() : rest.trim();
|
|
39
|
-
return { label: shell.label, text };
|
|
40
|
-
}
|
|
41
|
-
function requestDetails(request) {
|
|
42
|
-
const p = request.params, body = document.createElement('div'); body.className = 'approval-body';
|
|
43
|
-
const network = p.networkApprovalContext;
|
|
44
|
-
if (network) body.append(paragraph(`目标:${network.protocol}://${network.host}`));
|
|
45
|
-
if (p.reason && network) body.append(paragraph(p.reason));
|
|
46
|
-
for (const action of p.commandActions || []) {
|
|
47
|
-
if (action.type === 'read') body.append(paragraph(`读取文件:${action.path || action.name}`));
|
|
48
|
-
else if (action.type === 'listFiles') body.append(paragraph(`列出文件:${action.path || p.cwd || '当前目录'}`));
|
|
49
|
-
else if (action.type === 'search') body.append(paragraph(`搜索${action.query ? `“${action.query}”` : '内容'}${action.path ? `:${action.path}` : ''}`));
|
|
50
|
-
}
|
|
51
|
-
const shell = shellPresentation(p.command);
|
|
52
|
-
if (shell) {
|
|
53
|
-
body.append(paragraph(`执行方式:${shell.label}`, 'approval-meta'));
|
|
54
|
-
if (shell.text) body.append(content(shell.text, '要执行的内容'));
|
|
55
|
-
body.append(content(p.command, '完整原始命令', true));
|
|
56
|
-
} else if (p.command) body.append(content(p.command, '完整命令'));
|
|
57
|
-
if (p.cwd) body.append(paragraph(`工作目录:${p.cwd}`, 'approval-meta'));
|
|
58
|
-
if (p.grantRoot) body.append(paragraph(`申请本会话写入目录:${p.grantRoot}`));
|
|
59
|
-
for (const change of request.fileChanges || []) {
|
|
60
|
-
const kind = change.kind?.type || change.kind;
|
|
61
|
-
const move = change.kind?.movePath;
|
|
62
|
-
const label = `${{ add: '新增', delete: '删除', update: '修改' }[kind] || '文件'}:${change.path}${move ? ` → ${move}` : ''}`;
|
|
63
|
-
body.append(change.diff ? content(change.diff, label, true) : paragraph(label));
|
|
64
|
-
}
|
|
65
|
-
if (request.method === CodexServerRequests.file && !request.fileChanges?.length) body.append(paragraph('尚未收到文件修改明细,请先在工作站查看。'));
|
|
66
|
-
const permissions = p.additionalPermissions || p.permissions;
|
|
67
|
-
if (permissions) {
|
|
68
|
-
if (permissions.network?.enabled === true) body.append(paragraph('额外权限:连接互联网'));
|
|
69
|
-
const files = permissions.fileSystem;
|
|
70
|
-
for (const path of files?.read || []) body.append(paragraph(`允许读取:${path}`));
|
|
71
|
-
for (const path of files?.write || []) body.append(paragraph(`允许写入:${path}`));
|
|
72
|
-
for (const entry of files?.entries || []) {
|
|
73
|
-
const path = entry.path?.path || entry.path?.pattern;
|
|
74
|
-
const action = { read: '允许读取', write: '允许写入', deny: '禁止访问' }[entry.access];
|
|
75
|
-
if (path && action) body.append(paragraph(`${action}:${path}`));
|
|
76
|
-
}
|
|
77
|
-
body.append(content(JSON.stringify(permissions, null, 2), '完整权限范围', true));
|
|
78
|
-
}
|
|
79
|
-
return body;
|
|
80
|
-
}
|
|
81
|
-
function decisionDescription(request, decision) {
|
|
82
|
-
if (decision === 'acceptForSession') return request.method === CodexServerRequests.file
|
|
83
|
-
? '本次及本会话内对相同文件的后续修改无需再次询问。' : '本次及会话内匹配的后续操作无需再次询问,不是只允许这一次。';
|
|
84
|
-
if (decision?.applyNetworkPolicyAmendment) {
|
|
85
|
-
const rule = decision.applyNetworkPolicyAmendment.network_policy_amendment;
|
|
86
|
-
return `${rule.action === 'allow' ? '允许' : '禁止'}访问 ${rule.host},并保存为后续网络规则。`;
|
|
87
|
-
}
|
|
88
|
-
return JSON.stringify(decision, null, 2);
|
|
89
|
-
}
|
|
90
|
-
const refresh = () => {
|
|
91
|
-
host.hidden = cards.size === 0;
|
|
92
|
-
for (const card of cards.values()) {
|
|
93
|
-
const status = card.request.status;
|
|
94
|
-
card.status.textContent = !connected ? '连接未恢复,暂不能操作' : card.error || (status === 'submitted' ? '决定已提交,等待工作站确认…' : status === 'unknown' ? '提交结果未确认,请检查工作站' : '等待你处理');
|
|
95
|
-
for (const control of card.form.querySelectorAll('button, input, textarea')) control.disabled = !connected || status !== 'pending' || card.busy;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
const notifyPending = threadId => {
|
|
99
|
-
if (!threadId) return;
|
|
100
|
-
const pending = [...cards.values()].some(card => card.request.params.threadId === threadId
|
|
101
|
-
&& card.request.status === 'pending');
|
|
102
|
-
pendingChanged(threadId, pending);
|
|
103
|
-
};
|
|
104
|
-
function create(request) {
|
|
105
|
-
const element = document.createElement('div'); element.className = 'approval-card';
|
|
106
|
-
const heading = document.createElement('strong');
|
|
107
|
-
const p = request.params;
|
|
108
|
-
heading.textContent = request.method === CodexServerRequests.input ? '需要你的回答'
|
|
109
|
-
: p.networkApprovalContext ? `允许连接 ${p.networkApprovalContext.host}?`
|
|
110
|
-
: p.reason?.trim() || (request.method === CodexServerRequests.file ? '允许修改以下文件?'
|
|
111
|
-
: request.method === CodexServerRequests.command
|
|
112
|
-
? p.kind === 'writeStdin' ? '允许向正在运行的终端发送输入?'
|
|
113
|
-
: '允许执行此命令?'
|
|
114
|
-
: '需要你的批准');
|
|
115
|
-
const status = document.createElement('p'); status.setAttribute('role', 'status');
|
|
116
|
-
const detail = requestDetails(request);
|
|
117
|
-
const form = document.createElement('form');
|
|
118
|
-
const card = { element, status, detail, form, request, fileChangesKey: JSON.stringify(request.fileChanges), busy: false, error: '' };
|
|
119
|
-
const submit = async result => {
|
|
120
|
-
if (!connected || card.busy || card.request.status !== 'pending') return;
|
|
121
|
-
card.busy = true; card.error = '';
|
|
122
|
-
card.request = { ...card.request, status: 'submitted' };
|
|
123
|
-
notifyPending(card.request.params.threadId); refresh();
|
|
124
|
-
try { await respond(card.request, result); }
|
|
125
|
-
catch (error) {
|
|
126
|
-
card.request = { ...card.request, status: 'unknown' };
|
|
127
|
-
card.error = error.message;
|
|
128
|
-
} finally { notifyPending(card.request.params.threadId); refresh(); }
|
|
129
|
-
};
|
|
130
|
-
form.onsubmit = event => event.preventDefault();
|
|
131
|
-
if (request.method === CodexServerRequests.input) {
|
|
132
|
-
const inputs = (p.questions || []).map(q => {
|
|
133
|
-
const label = document.createElement('label'); label.textContent = q.question;
|
|
134
|
-
const input = document.createElement('input'); input.type = q.isSecret ? 'password' : 'text'; input.autocomplete = 'off';
|
|
135
|
-
label.append(input); form.append(label);
|
|
136
|
-
for (const option of q.options || []) {
|
|
137
|
-
const button = document.createElement('button'); button.type = 'button'; button.textContent = option.label;
|
|
138
|
-
button.title = option.description || ''; button.onclick = () => { input.value = option.label; }; form.append(button);
|
|
139
|
-
}
|
|
140
|
-
return { id: q.id, input };
|
|
141
|
-
});
|
|
142
|
-
const button = document.createElement('button'); button.textContent = '提交回答'; form.append(button);
|
|
143
|
-
form.onsubmit = event => { event.preventDefault(); submit({ answers: Object.fromEntries(inputs.map(({ id, input }) => [id, { answers: [input.value] }])) }); };
|
|
144
|
-
} else {
|
|
145
|
-
const choices = CodexServerRequests.decisions(request);
|
|
146
|
-
const actions = document.createElement('div'); actions.className = 'approval-actions';
|
|
147
|
-
for (const decision of choices) {
|
|
148
|
-
const button = document.createElement('button'); button.type = 'button';
|
|
149
|
-
button.textContent = typeof decision === 'string' ? titles[decision] || decision : decision.acceptWithExecpolicyAmendment ? '允许类似命令' : decision.applyNetworkPolicyAmendment ? '保存网络规则' : '应用所示决定';
|
|
150
|
-
button.onclick = () => submit({ decision });
|
|
151
|
-
if (decision === 'acceptForSession' || (typeof decision === 'object' && !decision.acceptWithExecpolicyAmendment)) {
|
|
152
|
-
const option = document.createElement('div'); option.className = 'approval-scope-option';
|
|
153
|
-
option.append(paragraph(decisionDescription(request, decision))); form.append(option);
|
|
154
|
-
}
|
|
155
|
-
actions.append(button);
|
|
156
|
-
}
|
|
157
|
-
if (choices.length) form.append(actions);
|
|
158
|
-
if (!choices.length) {
|
|
159
|
-
const unsupported = document.createElement('p'); unsupported.textContent = '此类请求暂不支持手机处理,请到工作站查看。'; form.append(unsupported);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const type = request.method === CodexServerRequests.input ? '问题'
|
|
163
|
-
: p.networkApprovalContext ? '互联网访问' : request.method === CodexServerRequests.file ? '编辑文件'
|
|
164
|
-
: request.method === CodexServerRequests.command ? '终端' : '请求权限';
|
|
165
|
-
element.append(paragraph(type, 'approval-kind'), heading, status, detail, form); return card;
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
update(requests) {
|
|
169
|
-
const threadId = requests[0]?.params?.threadId || [...cards.values()][0]?.request.params.threadId;
|
|
170
|
-
const active = new Set();
|
|
171
|
-
for (const request of requests) {
|
|
172
|
-
const key = JSON.stringify([request.epoch, request.id]); active.add(key);
|
|
173
|
-
let card = cards.get(key);
|
|
174
|
-
if (!card) { card = create(request); cards.set(key, card); host.append(card.element); }
|
|
175
|
-
const fileChangesKey = JSON.stringify(request.fileChanges);
|
|
176
|
-
if (card.fileChangesKey !== fileChangesKey) {
|
|
177
|
-
card.detail.replaceChildren(...requestDetails(request).children); card.fileChangesKey = fileChangesKey;
|
|
178
|
-
}
|
|
179
|
-
card.request = request;
|
|
180
|
-
}
|
|
181
|
-
for (const [key, card] of cards) if (!active.has(key)) { card.element.remove(); cards.delete(key); }
|
|
182
|
-
notifyPending(threadId); refresh();
|
|
183
|
-
},
|
|
184
|
-
setConnected(value) { connected = value; refresh(); },
|
|
185
|
-
clear() { cards.clear(); host.replaceChildren(); refresh(); }
|
|
186
|
-
};
|
|
187
|
-
};
|
|
188
|
-
})(globalThis);
|
package/dist/web/capabilities.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
(function (root, factory) {
|
|
2
|
-
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
3
|
-
else root.CodexCapabilities = factory();
|
|
4
|
-
})(globalThis, function () {
|
|
5
|
-
// Execution semantics belong to a connection/task, never to a client OS.
|
|
6
|
-
const profiles = {
|
|
7
|
-
'lan-shared': { input: 'inline-image', history: 'paged', settings: 'immediate', updates: 'events',
|
|
8
|
-
stop: 'primary', queueOwner: 'codex', archive: true, approvals: true, syncBeforeWrite: true, sharedRecovery: true },
|
|
9
|
-
relay: { input: 'inline-image', history: 'paged', settings: 'immediate', updates: 'events',
|
|
10
|
-
stop: 'primary', queueOwner: 'codex', archive: true, approvals: true, syncBeforeWrite: true, historyCache: true, remoteImages: true, remoteFiles: true, managedConnection: true },
|
|
11
|
-
};
|
|
12
|
-
const unknown = Object.freeze({ input: 'unknown', history: 'unknown', settings: 'unknown', updates: 'unknown',
|
|
13
|
-
stop: 'none', queueOwner: 'none' });
|
|
14
|
-
for (const profile of Object.values(profiles)) Object.freeze(profile);
|
|
15
|
-
Object.freeze(profiles);
|
|
16
|
-
|
|
17
|
-
function describe(mode) { return profiles[mode] || unknown; }
|
|
18
|
-
function has(mode, feature) {
|
|
19
|
-
const p = describe(mode);
|
|
20
|
-
switch (feature) {
|
|
21
|
-
case 'queue': return p.queueOwner !== 'none';
|
|
22
|
-
case 'nativeInput': return ['local-image', 'inline-image'].includes(p.input);
|
|
23
|
-
case 'primaryStop': return p.stop === 'primary';
|
|
24
|
-
case 'immediateSettings': return p.settings === 'immediate';
|
|
25
|
-
case 'pagedHistory': return p.history === 'paged';
|
|
26
|
-
case 'archive': return p.archive === true;
|
|
27
|
-
case 'incrementalEvents': return p.updates === 'events';
|
|
28
|
-
default: return p[feature] === true;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Support is stable; availability is recalculated for the current task. */
|
|
33
|
-
function actionState(mode, action, context = {}) {
|
|
34
|
-
const p = describe(mode);
|
|
35
|
-
const supported = action === 'interrupt' ? p.stop !== 'none'
|
|
36
|
-
: action === 'queue' ? has(mode, 'queue')
|
|
37
|
-
: action === 'settings' ? has(mode, 'immediateSettings')
|
|
38
|
-
: action === 'send' && p.input !== 'unknown';
|
|
39
|
-
if (!supported) return { visible: false, enabled: false, reason: '当前连接不支持此操作' };
|
|
40
|
-
if (context.connected === false) return { visible: true, enabled: false, reason: '连接尚未恢复' };
|
|
41
|
-
if (context.writable === false) return { visible: true, enabled: false, reason: context.readOnlyReason || '当前对话为只读' };
|
|
42
|
-
if (context.busy) return { visible: true, enabled: false, reason: '操作正在提交' };
|
|
43
|
-
if ((action === 'interrupt' || action === 'settings') && context.syncing) {
|
|
44
|
-
return { visible: true, enabled: false, reason: '请等待对话同步完成' };
|
|
45
|
-
}
|
|
46
|
-
if (action === 'interrupt' && !context.active) return { visible: true, enabled: false, reason: '当前没有运行中的轮次' };
|
|
47
|
-
return { visible: true, enabled: true, reason: '' };
|
|
48
|
-
}
|
|
49
|
-
return { describe, has, actionState };
|
|
50
|
-
});
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
(function (root) {
|
|
2
|
-
const capabilities = root.CodexCapabilities;
|
|
3
|
-
const selfRelay = root.codexSelfRelayConfig;
|
|
4
|
-
const lanOrigin = selfRelay ? new URL(selfRelay.url).origin.replace(/^wss:/, 'https:').replace(/^ws:/, 'http:') : root.codexLanConfig?.address || root.location?.origin;
|
|
5
|
-
const stationScope = selfRelay
|
|
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.
|
|
8
|
-
: root.codexLanConfig?.workstationScope || '';
|
|
9
|
-
|
|
10
|
-
class LanTransport extends (selfRelay ? root.CodexSelfRelaySession : root.CodexLanSession) {
|
|
11
|
-
constructor(token) { super(token); this.mode = root.codexLanConfig?.transportMode || 'unavailable'; }
|
|
12
|
-
connectionState() { return this.ready ? 'connected' : 'disconnected'; }
|
|
13
|
-
async api(path, options = {}) {
|
|
14
|
-
const body = await this.request(path, options);
|
|
15
|
-
if (path.startsWith('/api/health')) {
|
|
16
|
-
this.mode = body.transportMode || 'unavailable';
|
|
17
|
-
this.recoveryAvailable = body.sharedRecoveryAvailable === true;
|
|
18
|
-
this.execution = body.execution;
|
|
19
|
-
this.managedAppServer = body.managedAppServer;
|
|
20
|
-
this.desktop = body.desktop;
|
|
21
|
-
this.desktopRestartRecommended = body.desktopRestartRecommended === true;
|
|
22
|
-
}
|
|
23
|
-
return body;
|
|
24
|
-
}
|
|
25
|
-
events(thread, sync, known) {
|
|
26
|
-
const query = new URLSearchParams();
|
|
27
|
-
if (thread.hostId) query.set('hostId', thread.hostId);
|
|
28
|
-
if (sync?.id && sync.epoch && Number.isSafeInteger(sync.seq)) {
|
|
29
|
-
query.set('syncId', sync.id); query.set('syncEpoch', sync.epoch); query.set('afterSeq', String(sync.seq));
|
|
30
|
-
}
|
|
31
|
-
const knownUpdatedAt = Number(known?.thread?.updatedAt) || 0;
|
|
32
|
-
const knownTurn = known?.turns?.at(-1);
|
|
33
|
-
if (knownUpdatedAt > 0 && knownTurn?.id) {
|
|
34
|
-
query.set('knownUpdatedAt', knownUpdatedAt.toString());
|
|
35
|
-
query.set('knownTurnId', knownTurn.id);
|
|
36
|
-
if (knownTurn.status) query.set('knownTurnStatus', knownTurn.status);
|
|
37
|
-
}
|
|
38
|
-
return this.subscribe(`/api/threads/${encodeURIComponent(thread.id)}/events?${query}`);
|
|
39
|
-
}
|
|
40
|
-
async cacheScope() {
|
|
41
|
-
if (stationScope) return `workstation-v1:${stationScope}`;
|
|
42
|
-
if (!selfRelay) return root.codexLanConfig?.cacheScope ? `${root.codexLanConfig.cacheScope}:${this.mode}` : null;
|
|
43
|
-
const identity = [selfRelay.url, selfRelay.room, selfRelay.clientId, selfRelay.revision, selfRelay.credential].join('\0');
|
|
44
|
-
const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(identity));
|
|
45
|
-
return `self-relay-v1:${[...new Uint8Array(bytes)].map(byte => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
46
|
-
}
|
|
47
|
-
async cacheHash(value) {
|
|
48
|
-
if (!selfRelay) return [...crypto.getRandomValues(new Uint8Array(24))].map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
49
|
-
const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
|
|
50
|
-
return [...new Uint8Array(bytes)].map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
51
|
-
}
|
|
52
|
-
async readCached(id, thread) {
|
|
53
|
-
const scope = await this.cacheScope();
|
|
54
|
-
return scope ? root.codexHistoryCache?.get(scope, (stationScope || selfRelay) ? id : JSON.stringify([thread?.hostId || 'local', id])) : null;
|
|
55
|
-
}
|
|
56
|
-
async readLastCached() {
|
|
57
|
-
const scope = await this.cacheScope();
|
|
58
|
-
return scope ? root.codexHistoryCache?.latest(scope) : null;
|
|
59
|
-
}
|
|
60
|
-
async saveCached(data) {
|
|
61
|
-
const scope = await this.cacheScope();
|
|
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]));
|
|
63
|
-
}
|
|
64
|
-
async addImage(blob) {
|
|
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 图片');
|
|
66
|
-
const bytes = new Uint8Array(await blob.arrayBuffer()); let binary = '';
|
|
67
|
-
for (let i = 0; i < bytes.length; i += 16384) binary += String.fromCharCode(...bytes.subarray(i, i + 16384));
|
|
68
|
-
const url = `data:${blob.type};base64,${btoa(binary)}`;
|
|
69
|
-
return { id: url, url, thumbnailUrl: url };
|
|
70
|
-
}
|
|
71
|
-
assetUrl(value) {
|
|
72
|
-
if (!value) return '';
|
|
73
|
-
if (/^data:image\/(?:png|jpeg|webp|gif|bmp);base64,/i.test(value)) return value;
|
|
74
|
-
if (selfRelay) return '';
|
|
75
|
-
const url = new URL(value, lanOrigin);
|
|
76
|
-
if (url.origin !== new URL(lanOrigin).origin) return '';
|
|
77
|
-
if (this.token) url.searchParams.set('token', this.token);
|
|
78
|
-
return url.href;
|
|
79
|
-
}
|
|
80
|
-
async readImage(threadId, id, variant = 'original', recoveryToken = '', recoveryTurnId = '') {
|
|
81
|
-
if (!/^[a-f0-9]{64}$/.test(id)) throw new Error('无效的图片标识');
|
|
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;
|
|
88
|
-
}
|
|
89
|
-
async readFile(_threadId, id) {
|
|
90
|
-
if (!/^[a-f0-9]{64}$/.test(id)) throw new Error('无效的文件标识');
|
|
91
|
-
return this.api(`/api/files/${id}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Relay owns login, device pairing, reconnection and phone cache.
|
|
96
|
-
// The LAN adapter owns the authenticated WebSocket; both expose the same UI operations.
|
|
97
|
-
root.createChatTransport = function ({ token, currentThread, relay = root.codexTransport }) {
|
|
98
|
-
const adapter = relay || new LanTransport(token);
|
|
99
|
-
const modeFor = thread => {
|
|
100
|
-
if (relay) return 'relay';
|
|
101
|
-
return adapter.mode;
|
|
102
|
-
};
|
|
103
|
-
const mode = () => modeFor(currentThread());
|
|
104
|
-
const serviceAdvisory = () => {
|
|
105
|
-
if (relay) return null;
|
|
106
|
-
const managed = adapter.managedAppServer;
|
|
107
|
-
if (managed?.state === 'restarting' || managed?.restart?.status === 'running') return {
|
|
108
|
-
kind: 'restarting', title: 'Codex 内核正在重启',
|
|
109
|
-
detail: '工作站连接保持在线;任务操作暂不可用,恢复后不会自动重发。'
|
|
110
|
-
};
|
|
111
|
-
if (managed?.ready && managed.updatePending) return {
|
|
112
|
-
kind: 'update-pending', title: 'Codex 内核有更新',
|
|
113
|
-
detail: '当前任务不受影响;点击查看状态,并选择何时重启更新。'
|
|
114
|
-
};
|
|
115
|
-
if (adapter.execution && (!adapter.execution.ready || adapter.execution.state === 'recovering')) return {
|
|
116
|
-
kind: 'execution-unavailable',
|
|
117
|
-
title: adapter.execution.state === 'recovering' ? 'Codex 内核正在恢复' : 'Codex 内核未就绪',
|
|
118
|
-
detail: '工作站连接正常;点击查看当前状况、影响和可执行操作。'
|
|
119
|
-
};
|
|
120
|
-
return null;
|
|
121
|
-
};
|
|
122
|
-
return {
|
|
123
|
-
mode,
|
|
124
|
-
transportMode: () => relay ? 'relay' : adapter.mode,
|
|
125
|
-
recoverySupported: () => !relay && adapter.recoveryAvailable,
|
|
126
|
-
recoveryEvents: () => {
|
|
127
|
-
if (relay) throw new Error('请通过工作站管理连接查看切换进度');
|
|
128
|
-
return adapter.subscribe('/api/shared-recovery/events');
|
|
129
|
-
},
|
|
130
|
-
recoveryStatus: () => {
|
|
131
|
-
if (relay) throw new Error('OpenAI 官方中继不支持管理工作站 Codex 内核');
|
|
132
|
-
return adapter.api('/api/shared-recovery');
|
|
133
|
-
},
|
|
134
|
-
recoverShared: operationId => {
|
|
135
|
-
if (relay) throw new Error('请通过局域网或Code Relax 中继切换工作站执行模式');
|
|
136
|
-
if (!adapter.recoveryAvailable) throw new Error('工作站端尚未支持恢复功能,请更新 Bridge');
|
|
137
|
-
return adapter.api('/api/shared-recovery', { method: 'POST', body: { confirmed: true, operationId } });
|
|
138
|
-
},
|
|
139
|
-
sharedServiceNotice: serviceAdvisory,
|
|
140
|
-
connectionLabel: (compact = false) => {
|
|
141
|
-
if (relay) return compact ? 'OpenAI中继' : 'OpenAI 中继';
|
|
142
|
-
const network = selfRelay ? compact ? 'CodeRelax中继' : 'Code Relax 中继' : '局域网';
|
|
143
|
-
return network;
|
|
144
|
-
},
|
|
145
|
-
draftScope: () => relay ? adapter.draftScope() : stationScope || (selfRelay ? `${lanOrigin}/${selfRelay.room}` : lanOrigin),
|
|
146
|
-
capabilities: () => capabilities.describe(mode()),
|
|
147
|
-
has: name => (name === 'historyCache' && Boolean(selfRelay || root.codexLanConfig?.cacheScope)) || capabilities.has(mode(), name),
|
|
148
|
-
supports: (name, thread) => capabilities.has(modeFor(thread), name),
|
|
149
|
-
actionState: (action, context) => capabilities.actionState(mode(), action, {
|
|
150
|
-
...context, writable: context?.writable !== false && (relay || adapter.execution?.canWrite !== false)
|
|
151
|
-
}),
|
|
152
|
-
api(path, options) {
|
|
153
|
-
if (!relay && /^\/api\/(threads\/|models)/.test(path)) {
|
|
154
|
-
const hostId = currentThread()?.hostId;
|
|
155
|
-
if (hostId) {
|
|
156
|
-
const url = new URL(path, lanOrigin);
|
|
157
|
-
if (!url.searchParams.has('hostId')) url.searchParams.set('hostId', hostId);
|
|
158
|
-
path = url.pathname + url.search;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return adapter.api(path, options);
|
|
162
|
-
},
|
|
163
|
-
events: (thread, sync, known) => relay ? adapter.events(thread.id) : adapter.events(thread, sync, known),
|
|
164
|
-
respondApproval: (thread, request, result) => {
|
|
165
|
-
const body = { id: request.id, epoch: request.epoch, result };
|
|
166
|
-
if (relay) return adapter.respondApproval(thread.id, body);
|
|
167
|
-
const query = new URLSearchParams({ hostId: thread.hostId || 'local' });
|
|
168
|
-
return adapter.api(`/api/threads/${encodeURIComponent(thread.id)}/approvals?${query}`, { method: 'POST', body });
|
|
169
|
-
},
|
|
170
|
-
addImage: blob => adapter.addImage(blob),
|
|
171
|
-
removeImage: id => adapter.removeImage?.(id),
|
|
172
|
-
clearImages: () => adapter.clearImages?.(),
|
|
173
|
-
readImage: (id, path, variant, recoveryToken, recoveryTurnId) => adapter.readImage(id, path, variant, recoveryToken, recoveryTurnId),
|
|
174
|
-
readFile: (id, path) => adapter.readFile(id, path),
|
|
175
|
-
readCached: async (id, thread) => adapter.readCached?.(id, thread),
|
|
176
|
-
readLastCached: async () => adapter.readLastCached?.(),
|
|
177
|
-
saveCached: async timeline => adapter.saveCached?.(timeline),
|
|
178
|
-
diagnostics: () => adapter.diagnostics?.(),
|
|
179
|
-
pause: async () => adapter.pause?.(),
|
|
180
|
-
resume: async () => adapter.resume?.(),
|
|
181
|
-
checkConnection: () => relay ? adapter.checkConnection() : adapter.api('/api/health', { timeoutMs: 5000 }),
|
|
182
|
-
connectionState: () => adapter.connectionState?.(),
|
|
183
|
-
onConnectionChange: callback => { if (!relay) adapter.onConnectionChange = callback; },
|
|
184
|
-
usageEvents: () => {
|
|
185
|
-
if (relay) return adapter.usageEvents();
|
|
186
|
-
if (adapter.mode !== 'lan-shared') return null;
|
|
187
|
-
return adapter.subscribe('/api/usage-events');
|
|
188
|
-
},
|
|
189
|
-
assetUrl(value) {
|
|
190
|
-
if (!relay) return adapter.assetUrl(value);
|
|
191
|
-
if (/^data:image\/(?:png|jpeg|webp|gif);base64,/i.test(value) || /^blob:/i.test(value)) return value;
|
|
192
|
-
const url = new URL(value, location.origin); return url.protocol === 'https:' ? url.href : '';
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
};
|
|
196
|
-
})(globalThis);
|