@walkhi/code-relax 0.1.0-beta.27 → 0.1.0-beta.29
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 +80 -67
- package/dist/bin/codex-remote.mjs +303 -239
- package/dist/bin/self-relay-server.mjs +2 -0
- package/dist/shared/app-server-events.cjs +102 -16
- package/dist/shared/code-browser.cjs +159 -0
- package/dist/shared/file-changes.cjs +31 -0
- package/dist/shared/file-citations.cjs +117 -82
- package/dist/shared/file-mentions.cjs +80 -35
- package/dist/shared/message-delivery.cjs +38 -0
- package/dist/shared/p2p-file-channel.cjs +74 -0
- package/dist/shared/server-requests.cjs +149 -41
- package/dist/shared/service-endpoints.cjs +11 -11
- package/dist/shared/timeline-sync.cjs +173 -156
- package/dist/skills/code-relax/SKILL.md +2 -1
- package/dist/src/bounded-process-log.mjs +63 -0
- package/dist/src/celsius-client.mjs +107 -0
- package/dist/src/code-browser.mjs +101 -0
- package/dist/src/desktop-launcher.mjs +30 -30
- package/dist/src/dots-approval-timeline.mjs +99 -0
- package/dist/src/dots-avatar.mjs +42 -0
- package/dist/src/dots-chat-projection.mjs +50 -0
- package/dist/src/dots-chat-server.mjs +412 -0
- package/dist/src/dots-message-images.mjs +319 -0
- package/dist/src/dots-profile.mjs +77 -0
- package/dist/src/dots-room-activity.mjs +94 -0
- package/dist/src/dots-room-client.mjs +135 -0
- package/dist/src/dots-room-projection.mjs +94 -0
- package/dist/src/dots-schedules.mjs +66 -0
- package/dist/src/dots-summary-tasks.mjs +32 -0
- package/dist/src/dots-task-catalog.mjs +411 -0
- package/dist/src/file-transfer-range.mjs +22 -0
- package/dist/src/file-upload-session.mjs +130 -124
- package/dist/src/hosted-history-app-server.mjs +103 -0
- package/dist/src/hosted-history-cache.mjs +113 -0
- package/dist/src/lan-socket-server.mjs +205 -160
- package/dist/src/managed-app-server.mjs +241 -238
- package/dist/src/message-upload-files.mjs +74 -0
- package/dist/src/platform/windows/activate-desktop.ps1 +48 -0
- package/dist/src/platform/windows/codex-runtime.mjs +253 -251
- package/dist/src/platform/windows/desktop-host.mjs +38 -32
- package/dist/src/platform/windows/desktop-info.ps1 +2 -2
- package/dist/src/platform/windows/desktop-project-registration.mjs +153 -0
- package/dist/src/platform/windows/dots-profile.mjs +49 -0
- package/dist/src/platform/windows/launch-worker.mjs +41 -42
- package/dist/src/platform/windows/project-locations.mjs +16 -0
- package/dist/src/project-create.mjs +119 -0
- package/dist/src/project-management.mjs +69 -0
- package/dist/src/self-relay/account-connections.mjs +52 -0
- package/dist/src/self-relay/admin-page.mjs +80 -80
- package/dist/src/self-relay/admin-style.mjs +14 -9
- package/dist/src/self-relay/connector.mjs +23 -5
- package/dist/src/self-relay/email-accounts.mjs +35 -10
- package/dist/src/self-relay/file-wire.mjs +13 -0
- package/dist/src/self-relay/lifecycle.mjs +22 -4
- package/dist/src/self-relay/long-pairing.mjs +40 -0
- package/dist/src/self-relay/p2p-probe.mjs +152 -123
- package/dist/src/self-relay/rtc-file-bridge.mjs +41 -0
- package/dist/src/self-relay/server.mjs +922 -849
- package/dist/src/server.mjs +474 -199
- package/dist/src/service-control.mjs +13 -3
- package/dist/src/service-doctor.mjs +72 -61
- package/dist/src/service-lifecycle.mjs +51 -43
- package/dist/src/shared-app-server.mjs +166 -107
- package/dist/src/shared-catalog.mjs +132 -40
- package/dist/src/shared-thread-stream.mjs +103 -25
- package/dist/src/usage-reset.mjs +17 -0
- package/dist/src/workstation-downloads.mjs +120 -0
- package/package.json +1 -1
|
@@ -1,35 +1,80 @@
|
|
|
1
|
-
(function (root, factory) {
|
|
2
|
-
const api = factory();
|
|
3
|
-
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
4
|
-
else root.CodexFileMentions = api;
|
|
5
|
-
})(globalThis, function () {
|
|
6
|
-
const heading = '# Files mentioned by the user:';
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
4
|
+
else root.CodexFileMentions = api;
|
|
5
|
+
})(globalThis, function () {
|
|
6
|
+
const heading = '# Files mentioned by the user:';
|
|
7
|
+
const pastedHeading = '# Files pasted by the user:';
|
|
8
|
+
const request = '## My request:';
|
|
9
|
+
|
|
10
|
+
function stripBrowserContext(value) {
|
|
11
|
+
const context = /^\s*<in-app-browser-context\b[^>]*>[\s\S]*?<\/in-app-browser-context>\s*/;
|
|
12
|
+
if (!context.test(value)) return value;
|
|
13
|
+
return value.replace(context, '').replace(/^## My request:[ \t]*(?:\r?\n|$)/, '').trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parse(value) {
|
|
17
|
+
const source = stripBrowserContext(typeof value === 'string' ? value : '');
|
|
18
|
+
const firstHeading = [heading, pastedHeading].find(value => source.startsWith(value));
|
|
19
|
+
if (!firstHeading) return { text: source, files: [] };
|
|
20
|
+
const marker = source.indexOf(`\n${request}`, firstHeading.length);
|
|
21
|
+
if (marker < 0) return { text: source, files: [] };
|
|
22
|
+
const files = [];
|
|
23
|
+
let pasted = firstHeading === pastedHeading;
|
|
24
|
+
for (const line of source.slice(firstHeading.length, marker).split(/\r?\n/)) {
|
|
25
|
+
if (line === heading || line === pastedHeading) { pasted = line === pastedHeading; continue; }
|
|
26
|
+
const media = /^<!-- code-relax-media:(.+) -->$/.exec(line);
|
|
27
|
+
if (media && files.length) {
|
|
28
|
+
try { Object.assign(files.at(-1), mediaMetadata(JSON.parse(media[1]))); } catch {}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const match = /^## (.+): ((?:[A-Za-z]:[\\/]|\/)[^\r\n]+)$/.exec(line);
|
|
32
|
+
if (!match) continue;
|
|
33
|
+
let name = match[1];
|
|
34
|
+
if (name.startsWith('"')) {
|
|
35
|
+
try { name = JSON.parse(name); } catch { continue; }
|
|
36
|
+
}
|
|
37
|
+
if (typeof name !== 'string' || !name) continue;
|
|
38
|
+
files.push({ type: 'file', name: pasted ? match[2].split(/[\\/]/).at(-1) : name, path: match[2] });
|
|
39
|
+
}
|
|
40
|
+
if (!files.length) return { text: source, files: [] };
|
|
41
|
+
return { text: stripBrowserContext(source.slice(marker + request.length + 1).trim()), files };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function wrap(prompt, files) {
|
|
45
|
+
if (!files?.length) return prompt;
|
|
46
|
+
return `${heading}\n${files.map(file => {
|
|
47
|
+
const media = mediaMetadata(file);
|
|
48
|
+
return `\n## ${JSON.stringify(file.name)}: ${file.path}\n${media.mediaKind ? `<!-- code-relax-media:${JSON.stringify(media).replace(/</g, '\\u003c').replace(/>/g, '\\u003e')} -->\n` : ''}`;
|
|
49
|
+
}).join('')}\n${request}\n${prompt || '请查看上述文件。'}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function mediaMetadata(value) {
|
|
53
|
+
if (!value || !['image', 'video'].includes(value.mediaKind)) return {};
|
|
54
|
+
const result = { mediaKind: value.mediaKind };
|
|
55
|
+
for (const field of ['sha256', 'previewSha256']) if (/^[a-f0-9]{64}$/.test(value[field] || '')) result[field] = value[field];
|
|
56
|
+
if (typeof value.previewPath === 'string' && /^(?:[A-Za-z]:[\\/]|\/(?!\/))[^\r\n]+$/.test(value.previewPath)) result.previewPath = value.previewPath;
|
|
57
|
+
if (/^(?:image|video)\/[a-z0-9.+-]+$/i.test(value.mimeType || '')) result.mimeType = value.mimeType;
|
|
58
|
+
for (const field of ['size', 'duration']) if (Number.isFinite(value[field]) && value[field] > 0) result[field] = value[field];
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Formats accepted as native Codex image inputs; other files remain file references.
|
|
63
|
+
function isNativeImage(file) {
|
|
64
|
+
return file?.mediaKind === 'image' && /\.(png|jpe?g|webp|gif)$/i.test(file.name || file.path || '');
|
|
65
|
+
}
|
|
66
|
+
function attachment(file) {
|
|
67
|
+
return isNativeImage(file) ? { type: 'image', name: file.name, remotePath: file.path,
|
|
68
|
+
originalUpload: true, localImageId: file.sha256, localThumbnailId: file.previewSha256 }
|
|
69
|
+
: { ...file, type: 'file' };
|
|
70
|
+
}
|
|
71
|
+
function originalInputs(body) {
|
|
72
|
+
const paths = body.originalImages || [];
|
|
73
|
+
if (!Array.isArray(paths) || paths.length + (body.images || []).length > 4
|
|
74
|
+
|| paths.some(value => typeof value !== 'string' || !/^(?:[A-Za-z]:[\\/]|\/(?!\/))[^\r\n]+\.(png|jpe?g|webp|gif)$/i.test(value))) {
|
|
75
|
+
throw new Error('原图路径无效或每条消息图片超过 4 张');
|
|
76
|
+
}
|
|
77
|
+
return paths.map(path => ({ type: 'localImage', path }));
|
|
78
|
+
}
|
|
79
|
+
return Object.freeze({ parse, wrap, mediaMetadata, isNativeImage, attachment, originalInputs });
|
|
80
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const value = factory();
|
|
3
|
+
if (typeof module === 'object' && module.exports) module.exports = value;
|
|
4
|
+
else root.CodexMessageDelivery = value;
|
|
5
|
+
})(globalThis, function () {
|
|
6
|
+
function rejectedBeforeSubmission(error) {
|
|
7
|
+
return error?.rpcCode === -32602 || error?.rpcCode === -32600
|
|
8
|
+
|| error?.rpcCode != null && /^(no active turn to steer|expected active turn id `)/i.test(error.message || '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class Store {
|
|
12
|
+
constructor() { this.entries = new Map(); }
|
|
13
|
+
key(scope, clientId) { return JSON.stringify([scope, clientId]); }
|
|
14
|
+
status(scope, clientId) {
|
|
15
|
+
const entry = this.entries.get(this.key(scope, clientId));
|
|
16
|
+
return entry ? { state: entry.state, ...(entry.result ? { result: entry.result } : {}),
|
|
17
|
+
...(entry.error ? { error: entry.error.message } : {}) } : { state: 'unknown' };
|
|
18
|
+
}
|
|
19
|
+
submit(scope, clientId, operation) {
|
|
20
|
+
if (!/^[a-f0-9]{32}$/i.test(clientId || '')) return Promise.reject(Object.assign(new Error('消息确认标识无效'), { deliveryUnknown: false }));
|
|
21
|
+
const key = this.key(scope, clientId), previous = this.entries.get(key);
|
|
22
|
+
if (previous?.state === 'accepted') return Promise.resolve(previous.result);
|
|
23
|
+
if (previous?.state === 'sending') return previous.work;
|
|
24
|
+
if (previous?.state === 'unknown') return Promise.reject(Object.assign(new Error(previous.error?.message || '原发送结果仍未确认'), { deliveryUnknown: true }));
|
|
25
|
+
const entry = { state: 'sending' };
|
|
26
|
+
this.entries.set(key, entry);
|
|
27
|
+
entry.work = Promise.resolve().then(operation).then(result => {
|
|
28
|
+
entry.state = 'accepted'; entry.result = result; return result;
|
|
29
|
+
}, error => {
|
|
30
|
+
entry.state = error.deliveryUnknown === false || error.details?.deliveryUnknown === false ? 'not_sent' : 'unknown';
|
|
31
|
+
error.deliveryUnknown = entry.state !== 'not_sent';
|
|
32
|
+
entry.error = error; throw error;
|
|
33
|
+
}).finally(() => { delete entry.work; });
|
|
34
|
+
return entry.work;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return Object.freeze({ Store, rejectedBeforeSubmission });
|
|
38
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
3
|
+
else root.CodeRelaxFileChannel = factory();
|
|
4
|
+
})(globalThis, function () {
|
|
5
|
+
const PART = 16 * 1024, MAX = 256 * 1024, LIMIT = 1024 * 1024;
|
|
6
|
+
// Socket-shaped adapter; one ordered RTC stream owns one file transaction.
|
|
7
|
+
return class FileChannel {
|
|
8
|
+
constructor(io) {
|
|
9
|
+
this.io = io; this.readyState = 0; this.listeners = new Map();
|
|
10
|
+
this.outbound = []; this.queued = 0; this.partial = null; this.timer = null;
|
|
11
|
+
}
|
|
12
|
+
get bufferedAmount() { return this.queued + this.io.buffered(); }
|
|
13
|
+
addEventListener(type, callback, options = {}) {
|
|
14
|
+
const list = this.listeners.get(type) || []; list.push({ callback, once: options.once }); this.listeners.set(type, list);
|
|
15
|
+
}
|
|
16
|
+
removeEventListener(type, callback) { this.listeners.set(type, (this.listeners.get(type) || []).filter(item => item.callback !== callback)); }
|
|
17
|
+
emit(type, details = {}) {
|
|
18
|
+
const event = { type, ...details };
|
|
19
|
+
this['on' + type]?.(event);
|
|
20
|
+
for (const item of [...(this.listeners.get(type) || [])]) {
|
|
21
|
+
if (item.once) this.removeEventListener(type, item.callback);
|
|
22
|
+
item.callback(event);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
opened() { if (this.readyState !== 0) return; this.readyState = 1; this.emit('open'); this.pump(); }
|
|
26
|
+
send(value) {
|
|
27
|
+
if (this.readyState !== 1) throw new Error('文件直连已断开');
|
|
28
|
+
const binary = typeof value !== 'string';
|
|
29
|
+
const data = binary ? (value instanceof Uint8Array ? value : new Uint8Array(value)) : value;
|
|
30
|
+
const size = binary ? data.byteLength : new TextEncoder().encode(data).length;
|
|
31
|
+
if (!size || size > (binary ? MAX : PART) || this.bufferedAmount + size > LIMIT) throw new Error('文件直连分片或队列超限');
|
|
32
|
+
const frames = binary ? [JSON.stringify({ type: 'rtc-file-bytes', length: size })] : [];
|
|
33
|
+
if (binary) for (let i = 0; i < size; i += PART) frames.push(data.slice(i, i + PART));
|
|
34
|
+
else frames.push(data);
|
|
35
|
+
this.outbound.push({ frames, size, index: 0 }); this.queued += size; this.pump();
|
|
36
|
+
}
|
|
37
|
+
pump() {
|
|
38
|
+
if (this.readyState !== 1) return;
|
|
39
|
+
try {
|
|
40
|
+
while (this.outbound.length && this.io.buffered() < MAX) {
|
|
41
|
+
const item = this.outbound[0];
|
|
42
|
+
if (this.io.send(item.frames[item.index]) === false) throw new Error('文件直连发送失败');
|
|
43
|
+
if (++item.index === item.frames.length) { this.outbound.shift(); this.queued -= item.size; }
|
|
44
|
+
}
|
|
45
|
+
} catch { this.close(1011); }
|
|
46
|
+
}
|
|
47
|
+
receive(value) {
|
|
48
|
+
if (this.readyState !== 1) return;
|
|
49
|
+
try {
|
|
50
|
+
if (typeof value === 'string') {
|
|
51
|
+
if (this.partial || new TextEncoder().encode(value).length > PART) throw new Error();
|
|
52
|
+
const frame = JSON.parse(value);
|
|
53
|
+
if (frame.type !== 'rtc-file-bytes') { this.emit('message', { data: value }); return; }
|
|
54
|
+
if (!Number.isSafeInteger(frame.length) || frame.length < 1 || frame.length > MAX) throw new Error();
|
|
55
|
+
this.partial = { data: new Uint8Array(frame.length), offset: 0 };
|
|
56
|
+
this.timer = setTimeout(() => this.close(1008), 30000); return;
|
|
57
|
+
}
|
|
58
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value), part = this.partial;
|
|
59
|
+
if (!part || bytes.length !== Math.min(PART, part.data.length - part.offset)) throw new Error();
|
|
60
|
+
part.data.set(bytes, part.offset); part.offset += bytes.length;
|
|
61
|
+
if (part.offset === part.data.length) {
|
|
62
|
+
this.partial = null; clearTimeout(this.timer); this.timer = null;
|
|
63
|
+
this.emit('message', { data: part.data.buffer });
|
|
64
|
+
}
|
|
65
|
+
} catch { this.close(1008); }
|
|
66
|
+
}
|
|
67
|
+
close(code = 1000) {
|
|
68
|
+
if (this.readyState === 3) return;
|
|
69
|
+
this.readyState = 3; clearTimeout(this.timer); this.timer = null;
|
|
70
|
+
this.partial = null; this.outbound = []; this.queued = 0;
|
|
71
|
+
this.io.close(); this.emit('close', { code });
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
});
|
|
@@ -4,92 +4,200 @@
|
|
|
4
4
|
})(globalThis, function () {
|
|
5
5
|
const command = 'item/commandExecution/requestApproval';
|
|
6
6
|
const file = 'item/fileChange/requestApproval';
|
|
7
|
-
const input = 'item/tool/requestUserInput';
|
|
8
|
-
const elicitation = 'mcpServer/elicitation/request';
|
|
9
|
-
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|| meta.
|
|
15
|
-
||
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
const input = 'item/tool/requestUserInput';
|
|
8
|
+
const elicitation = 'mcpServer/elicitation/request';
|
|
9
|
+
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
10
|
+
const approvalId = request => 'approval:' + (request.params.turnId || '') + ':' + typeof request.id + ':' + request.id;
|
|
11
|
+
function browserAccess(request) {
|
|
12
|
+
const p = request.params || {}, meta = p._meta || {};
|
|
13
|
+
if (request.method !== elicitation || p.mode !== 'form' || meta.tool_name !== 'access_browser_origin'
|
|
14
|
+
|| meta.codex_approval_kind !== 'mcp_tool_call' || meta.codex_request_type !== 'approval_request'
|
|
15
|
+
|| meta.codex_sensitive_action !== true || p.requestedSchema?.type !== 'object'
|
|
16
|
+
|| Object.keys(p.requestedSchema.properties || {}).length) return null;
|
|
17
|
+
try {
|
|
18
|
+
const origin = new URL(meta.origin);
|
|
19
|
+
if (!['http:', 'https:'].includes(origin.protocol) || origin.origin !== meta.origin) return null;
|
|
20
|
+
return { origin: meta.origin, persistent: meta.persist === 'always' };
|
|
21
|
+
} catch { return null; }
|
|
22
|
+
}
|
|
23
|
+
function browserAction(request) {
|
|
24
|
+
const p = request.params || {}, meta = p._meta || {};
|
|
25
|
+
if (browserAccess(request) || request.method !== elicitation || p.mode !== 'form'
|
|
26
|
+
|| p.requestedSchema?.type !== 'object' || Object.keys(p.requestedSchema.properties || {}).length
|
|
27
|
+
|| p.requestedSchema.required?.length || typeof p.message !== 'string' || !p.message.trim()) return null;
|
|
28
|
+
const tool = meta.codex_approval_kind === 'mcp_tool_call' && meta.connector_id === 'browser-use';
|
|
29
|
+
if (!tool && !['browser', 'browser-use'].includes(p.serverName)) return null;
|
|
30
|
+
if (meta.codex_approval_kind && meta.codex_approval_kind !== 'mcp_tool_call') return null;
|
|
31
|
+
const modes = meta.persist == null ? [] : Array.isArray(meta.persist) ? meta.persist : [meta.persist];
|
|
32
|
+
if (modes.some(mode => !['session', 'always'].includes(mode))) return null;
|
|
33
|
+
if (!tool && !modes.length) return null;
|
|
34
|
+
try {
|
|
35
|
+
const origin = new URL(meta.origin);
|
|
36
|
+
if (!['http:', 'https:'].includes(origin.protocol) || origin.origin !== meta.origin) return null;
|
|
37
|
+
return { origin: meta.origin, session: modes.includes('session'), persistent: modes.includes('always') };
|
|
38
|
+
} catch { return null; }
|
|
39
|
+
}
|
|
40
|
+
function browserActionResult(decision) {
|
|
41
|
+
return { action: decision === 'cancel' ? 'cancel' : 'accept', content: decision === 'cancel' ? null : {},
|
|
42
|
+
_meta: decision === 'allowAlways' ? { persist: 'always' }
|
|
43
|
+
: decision === 'allowSession' ? { persist: 'session' } : null };
|
|
44
|
+
}
|
|
45
|
+
function computerAppAccess(request) {
|
|
46
|
+
const p = request.params || {}, meta = p._meta || {};
|
|
47
|
+
if (request.method !== elicitation || p.mode !== 'form' || meta.codex_approval_kind !== 'mcp_tool_call'
|
|
48
|
+
|| (meta.codex_request_type != null && meta.codex_request_type !== 'approval_request')
|
|
49
|
+
|| typeof meta.connector_id !== 'string' || p.requestedSchema?.type !== 'object'
|
|
50
|
+
|| Object.keys(p.requestedSchema.properties || {}).length || p.requestedSchema.required?.length) return null;
|
|
51
|
+
const connector = meta.connector_id.trim().toLowerCase().split(/[^a-z0-9]+/g).filter(Boolean).join('-');
|
|
52
|
+
if (connector !== 'computer-use' && !connector.startsWith('computer-use-')) return null;
|
|
53
|
+
const modes = Array.isArray(meta.persist) ? meta.persist : [meta.persist];
|
|
54
|
+
if (!modes.length || modes.some(mode => !['session', 'always'].includes(mode))) return null;
|
|
55
|
+
const display = Array.isArray(meta.tool_params_display) ? meta.tool_params_display.find(entry => entry?.name === 'app')?.value : null;
|
|
56
|
+
const app = typeof display === 'string' && display.trim() ? display : meta.tool_params?.app;
|
|
57
|
+
if (typeof app !== 'string' || !app.trim()) return null;
|
|
58
|
+
return { app: app.trim(), session: modes.includes('session'), persistent: modes.includes('always'),
|
|
59
|
+
subtitle: typeof meta.subtitle === 'string' ? meta.subtitle : '', highRisk: meta.riskLevel === 'high' };
|
|
60
|
+
}
|
|
61
|
+
function computerAppResult(request, decision) {
|
|
62
|
+
const app = computerAppAccess(request);
|
|
63
|
+
return { action: decision === 'decline' ? 'decline' : 'accept', content: decision === 'decline' ? null : {},
|
|
64
|
+
_meta: decision === 'allowAlways' ? { persist: 'always' }
|
|
65
|
+
: decision === 'allowConversation' && app?.session ? { persist: 'session' } : null };
|
|
66
|
+
}
|
|
67
|
+
function mcpToolApproval(request) {
|
|
68
|
+
const p = request.params || {}, meta = p._meta || {};
|
|
69
|
+
if (request.method !== elicitation || p.mode !== 'form' || meta.codex_approval_kind !== 'mcp_tool_call'
|
|
70
|
+
|| (meta.codex_request_type != null && meta.codex_request_type !== 'approval_request')
|
|
71
|
+
|| p.requestedSchema?.type !== 'object' || Object.keys(p.requestedSchema.properties || {}).length
|
|
72
|
+
|| p.requestedSchema.required?.length || typeof p.message !== 'string' || !p.message.trim()
|
|
73
|
+
|| typeof meta.connector_id !== 'string' || !meta.connector_id.trim()
|
|
74
|
+
|| !meta.tool_params || typeof meta.tool_params !== 'object' || Array.isArray(meta.tool_params)) return null;
|
|
75
|
+
const connector = meta.connector_id.trim().toLowerCase().split(/[^a-z0-9]+/g).filter(Boolean).join('-');
|
|
76
|
+
// These requests have dedicated approval semantics in Desktop.
|
|
77
|
+
if (connector === 'browser-use' || connector === 'computer-use' || connector.startsWith('computer-use-')
|
|
78
|
+
|| meta.tool_name === 'access_browser_origin' || meta.tool_name === 'access_browser_origin_with_raw_cdp'
|
|
79
|
+
|| meta.connector_id === 'connector_20205bf7d4e99a89d7154bb849718324') return null;
|
|
80
|
+
const modes = meta.persist === undefined ? [] : Array.isArray(meta.persist) ? meta.persist : [meta.persist];
|
|
81
|
+
if ((Array.isArray(meta.persist) && (!modes.length || modes.length > 2))
|
|
82
|
+
|| modes.some(mode => !['session', 'always'].includes(mode))) return null;
|
|
83
|
+
return { session: modes.includes('session'), persistent: modes.includes('always') };
|
|
84
|
+
}
|
|
85
|
+
function mcpToolResult(decision) {
|
|
86
|
+
return { action: decision === 'decline' ? 'decline' : 'accept', content: decision === 'decline' ? null : {},
|
|
87
|
+
_meta: decision === 'allowAlways' ? { persist: 'always' }
|
|
88
|
+
: decision === 'allowSession' ? { persist: 'session' } : null };
|
|
89
|
+
}
|
|
90
|
+
function decisions(request) {
|
|
23
91
|
if (request.method === command) return request.params.availableDecisions ?? ['accept', 'acceptForSession', 'decline', 'cancel'];
|
|
24
|
-
if (request.method === file) return ['accept', 'acceptForSession', 'decline', 'cancel'];
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
|
|
92
|
+
if (request.method === file) return ['accept', 'acceptForSession', 'decline', 'cancel'];
|
|
93
|
+
const action = browserAction(request);
|
|
94
|
+
if (action) return ['allowOnce', ...(action.session ? ['allowSession'] : []), ...(action.persistent ? ['allowAlways'] : []), 'cancel'];
|
|
95
|
+
const app = computerAppAccess(request);
|
|
96
|
+
if (app) return ['decline', 'allowConversation', ...(app.persistent ? ['allowAlways'] : [])];
|
|
97
|
+
const browser = browserAccess(request);
|
|
98
|
+
if (browser) return browser.persistent ? ['allowOnce', 'allowSite', 'allowAllSites', 'decline'] : ['allowOnce', 'decline'];
|
|
99
|
+
const tool = mcpToolApproval(request);
|
|
100
|
+
if (tool) return ['decline', 'allowOnce', ...(tool.session ? ['allowSession'] : []), ...(tool.persistent ? ['allowAlways'] : [])];
|
|
101
|
+
return [];
|
|
28
102
|
}
|
|
29
103
|
function validate(request, result) {
|
|
30
104
|
if ([command, file].includes(request.method)) {
|
|
31
105
|
if (!result || Object.keys(result).length !== 1 || !decisions(request).some(value => equal(value, result.decision))) throw new Error('审批选项不属于当前请求');
|
|
32
106
|
return;
|
|
33
107
|
}
|
|
34
|
-
if (request.method === input) {
|
|
108
|
+
if (request.method === input) {
|
|
35
109
|
const questions = request.params.questions || [];
|
|
36
110
|
if (!result?.answers || Object.keys(result).length !== 1 || Object.keys(result.answers).length !== questions.length
|
|
37
111
|
|| questions.some(q => !Array.isArray(result.answers[q.id]?.answers) || result.answers[q.id].answers.some(a => typeof a !== 'string'))) throw new Error('回答格式无效');
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (browserAction(request)) {
|
|
115
|
+
if (!decisions(request).some(decision => equal(browserActionResult(decision), result))) throw new Error('浏览器操作决定不属于当前请求');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (computerAppAccess(request)) {
|
|
119
|
+
if (!decisions(request).some(decision => equal(computerAppResult(request, decision), result))) throw new Error('应用访问决定不属于当前请求');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const browser = browserAccess(request);
|
|
123
|
+
if (browser) {
|
|
124
|
+
const allowed = [
|
|
125
|
+
{ action: 'accept', content: {}, _meta: null },
|
|
126
|
+
{ action: 'decline', content: null, _meta: null },
|
|
127
|
+
];
|
|
128
|
+
if (browser.persistent) allowed.push(
|
|
129
|
+
{ action: 'accept', content: {}, _meta: { persist: 'always' } },
|
|
130
|
+
{ action: 'accept', content: {}, _meta: { persist: 'always', browser_use_persistent_approval_scope: 'all-sites' } },
|
|
131
|
+
);
|
|
132
|
+
if (!allowed.some(choice => equal(choice, result))) throw new Error('网站访问决定不属于当前请求');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (mcpToolApproval(request)) {
|
|
136
|
+
if (!decisions(request).some(decision => equal(mcpToolResult(decision), result))) throw new Error('工具授权决定不属于当前请求');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
53
139
|
throw new Error('此类请求需在工作站处理');
|
|
54
140
|
}
|
|
55
141
|
class Store {
|
|
56
|
-
constructor(epoch, changed = () => {}) {
|
|
142
|
+
constructor(epoch, changed = () => {}) {
|
|
143
|
+
this.epoch = epoch; this.changed = changed; this.requests = new Map(); this.approvalHistory = new Map();
|
|
144
|
+
}
|
|
57
145
|
list(threadId) { return [...this.requests.values()].filter(r => r.params.threadId === threadId).map(r => ({ ...r })); }
|
|
146
|
+
history(threadId) { return [...this.approvalHistory.values()].filter(r => r.params.threadId === threadId).map(r => ({ ...r })); }
|
|
147
|
+
complete(request, status) {
|
|
148
|
+
const approval = this.approvalHistory.get(approvalId(request));
|
|
149
|
+
if (approval) { approval.status = status; approval.completedAtMs = Date.now(); }
|
|
150
|
+
}
|
|
58
151
|
receive(message) {
|
|
59
152
|
if (!['string', 'number'].includes(typeof message.id) || !message.params?.threadId || this.requests.has(message.id)) return;
|
|
60
153
|
const request = { id: message.id, method: message.method, params: message.params, epoch: this.epoch, status: 'pending' };
|
|
154
|
+
if (request.method !== input) this.approvalHistory.set(approvalId(request), {
|
|
155
|
+
type: 'approval', id: approvalId(request), requestId: request.id, method: request.method,
|
|
156
|
+
params: request.params, turnId: request.params.turnId, status: 'pending', receivedAtMs: Date.now(),
|
|
157
|
+
});
|
|
61
158
|
this.requests.set(message.id, request); this.changed(request.params.threadId);
|
|
62
159
|
}
|
|
63
160
|
notification(method, params) {
|
|
64
161
|
if (method === 'serverRequest/resolved') {
|
|
65
162
|
const request = this.requests.get(params.requestId);
|
|
66
|
-
if (request?.params.threadId === params.threadId) {
|
|
163
|
+
if (request?.params.threadId === params.threadId) {
|
|
164
|
+
this.complete(request, 'resolved'); this.requests.delete(params.requestId); this.changed(params.threadId);
|
|
165
|
+
}
|
|
67
166
|
} else if (method === 'turn/completed' || method === 'thread/closed') {
|
|
68
|
-
for (const [id, r] of this.requests) if (r.params.threadId === params.threadId && (method === 'thread/closed' || r.params.turnId === params.turn?.id))
|
|
167
|
+
for (const [id, r] of this.requests) if (r.params.threadId === params.threadId && (method === 'thread/closed' || r.params.turnId === params.turn?.id)) {
|
|
168
|
+
this.complete(r, 'expired'); this.requests.delete(id);
|
|
169
|
+
}
|
|
69
170
|
this.changed(params.threadId);
|
|
70
171
|
}
|
|
71
172
|
}
|
|
72
173
|
forget(threadId) {
|
|
73
174
|
for (const [id, request] of this.requests) if (request.params.threadId === threadId) this.requests.delete(id);
|
|
175
|
+
for (const [id, approval] of this.approvalHistory) if (approval.params.threadId === threadId) this.approvalHistory.delete(id);
|
|
74
176
|
this.changed(threadId);
|
|
75
177
|
}
|
|
76
178
|
reset(epoch) {
|
|
77
179
|
const threads = new Set([...this.requests.values()].map(r => r.params.threadId));
|
|
180
|
+
for (const request of this.requests.values()) this.complete(request, 'expired');
|
|
78
181
|
this.requests.clear(); this.epoch = epoch; for (const id of threads) this.changed(id);
|
|
182
|
+
this.approvalHistory.clear();
|
|
79
183
|
}
|
|
80
184
|
async respond(threadId, { id, epoch, result }, send) {
|
|
81
185
|
const request = this.requests.get(id);
|
|
82
186
|
if (!request || epoch !== this.epoch || request.params.threadId !== threadId) throw new Error('审批已处理或连接已变化,请等待同步');
|
|
83
187
|
if (request.status !== 'pending') throw new Error('决定已提交,正在等待工作站确认,请勿重复提交');
|
|
84
188
|
validate(request, result);
|
|
189
|
+
const approval = this.approvalHistory.get(approvalId(request));
|
|
190
|
+
if (approval) { approval.status = 'submitted'; approval.result = result; }
|
|
85
191
|
request.status = 'submitted'; this.changed(threadId);
|
|
86
192
|
try { await send({ id: request.id, result }); }
|
|
87
193
|
catch (error) {
|
|
88
|
-
if (this.requests.get(id) === request) {
|
|
194
|
+
if (this.requests.get(id) === request) {
|
|
195
|
+
request.status = 'unknown'; if (approval) approval.status = 'unknown'; this.changed(threadId);
|
|
196
|
+
}
|
|
89
197
|
throw new Error('审批发送结果未确认,请检查工作站;不会自动重发。' + error.message);
|
|
90
198
|
}
|
|
91
199
|
return { submitted: true };
|
|
92
200
|
}
|
|
93
201
|
}
|
|
94
|
-
return { Store, decisions, validate, command, file, input, elicitation, browserAccess };
|
|
202
|
+
return { Store, decisions, validate, command, file, input, elicitation, browserAccess, browserAction, browserActionResult, computerAppAccess, computerAppResult, mcpToolApproval, mcpToolResult };
|
|
95
203
|
});
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// Generated from packages/service-domains.json; run node bridge/tools/generate-service-endpoints.mjs.
|
|
2
2
|
(function(root) {
|
|
3
3
|
const endpoints = {
|
|
4
|
-
"profile": "
|
|
5
|
-
"apiOrigin": "https://api.
|
|
6
|
-
"webOrigin": "https://
|
|
7
|
-
"adminOrigin": "https://
|
|
8
|
-
"relayUrl": "wss://api.
|
|
9
|
-
"pairingUrl": "https://
|
|
10
|
-
"accountBase": "https://api.
|
|
11
|
-
"announcementUrl": "https://api.
|
|
12
|
-
"installUrl": "https://
|
|
13
|
-
"updateManifestUrl": "https://
|
|
14
|
-
"adminUrl": "https://
|
|
4
|
+
"profile": "coderelax",
|
|
5
|
+
"apiOrigin": "https://api.coderelax.cn",
|
|
6
|
+
"webOrigin": "https://www.coderelax.cn",
|
|
7
|
+
"adminOrigin": "https://admin.coderelax.cn",
|
|
8
|
+
"relayUrl": "wss://api.coderelax.cn/relay",
|
|
9
|
+
"pairingUrl": "https://www.coderelax.cn/pair",
|
|
10
|
+
"accountBase": "https://api.coderelax.cn/account",
|
|
11
|
+
"announcementUrl": "https://api.coderelax.cn/announcement",
|
|
12
|
+
"installUrl": "https://www.coderelax.cn/code-relax/install",
|
|
13
|
+
"updateManifestUrl": "https://www.coderelax.cn/code-relax/releases/latest/manifest.json5",
|
|
14
|
+
"adminUrl": "https://admin.coderelax.cn/admin"
|
|
15
15
|
};
|
|
16
16
|
endpoints.isOfficialRelayUrl = value => ["wss://api.uulife.site/relay","wss://api.coderelax.cn/relay"].includes(value);
|
|
17
17
|
endpoints.resolveRelayUrl = value => endpoints.isOfficialRelayUrl(value) ? endpoints.relayUrl : value;
|