@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.
Files changed (68) hide show
  1. package/README.md +80 -67
  2. package/dist/bin/codex-remote.mjs +303 -239
  3. package/dist/bin/self-relay-server.mjs +2 -0
  4. package/dist/shared/app-server-events.cjs +102 -16
  5. package/dist/shared/code-browser.cjs +159 -0
  6. package/dist/shared/file-changes.cjs +31 -0
  7. package/dist/shared/file-citations.cjs +117 -82
  8. package/dist/shared/file-mentions.cjs +80 -35
  9. package/dist/shared/message-delivery.cjs +38 -0
  10. package/dist/shared/p2p-file-channel.cjs +74 -0
  11. package/dist/shared/server-requests.cjs +149 -41
  12. package/dist/shared/service-endpoints.cjs +11 -11
  13. package/dist/shared/timeline-sync.cjs +173 -156
  14. package/dist/skills/code-relax/SKILL.md +2 -1
  15. package/dist/src/bounded-process-log.mjs +63 -0
  16. package/dist/src/celsius-client.mjs +107 -0
  17. package/dist/src/code-browser.mjs +101 -0
  18. package/dist/src/desktop-launcher.mjs +30 -30
  19. package/dist/src/dots-approval-timeline.mjs +99 -0
  20. package/dist/src/dots-avatar.mjs +42 -0
  21. package/dist/src/dots-chat-projection.mjs +50 -0
  22. package/dist/src/dots-chat-server.mjs +412 -0
  23. package/dist/src/dots-message-images.mjs +319 -0
  24. package/dist/src/dots-profile.mjs +77 -0
  25. package/dist/src/dots-room-activity.mjs +94 -0
  26. package/dist/src/dots-room-client.mjs +135 -0
  27. package/dist/src/dots-room-projection.mjs +94 -0
  28. package/dist/src/dots-schedules.mjs +66 -0
  29. package/dist/src/dots-summary-tasks.mjs +32 -0
  30. package/dist/src/dots-task-catalog.mjs +411 -0
  31. package/dist/src/file-transfer-range.mjs +22 -0
  32. package/dist/src/file-upload-session.mjs +130 -124
  33. package/dist/src/hosted-history-app-server.mjs +103 -0
  34. package/dist/src/hosted-history-cache.mjs +113 -0
  35. package/dist/src/lan-socket-server.mjs +205 -160
  36. package/dist/src/managed-app-server.mjs +241 -238
  37. package/dist/src/message-upload-files.mjs +74 -0
  38. package/dist/src/platform/windows/activate-desktop.ps1 +48 -0
  39. package/dist/src/platform/windows/codex-runtime.mjs +253 -251
  40. package/dist/src/platform/windows/desktop-host.mjs +38 -32
  41. package/dist/src/platform/windows/desktop-info.ps1 +2 -2
  42. package/dist/src/platform/windows/desktop-project-registration.mjs +153 -0
  43. package/dist/src/platform/windows/dots-profile.mjs +49 -0
  44. package/dist/src/platform/windows/launch-worker.mjs +41 -42
  45. package/dist/src/platform/windows/project-locations.mjs +16 -0
  46. package/dist/src/project-create.mjs +119 -0
  47. package/dist/src/project-management.mjs +69 -0
  48. package/dist/src/self-relay/account-connections.mjs +52 -0
  49. package/dist/src/self-relay/admin-page.mjs +80 -80
  50. package/dist/src/self-relay/admin-style.mjs +14 -9
  51. package/dist/src/self-relay/connector.mjs +23 -5
  52. package/dist/src/self-relay/email-accounts.mjs +35 -10
  53. package/dist/src/self-relay/file-wire.mjs +13 -0
  54. package/dist/src/self-relay/lifecycle.mjs +22 -4
  55. package/dist/src/self-relay/long-pairing.mjs +40 -0
  56. package/dist/src/self-relay/p2p-probe.mjs +152 -123
  57. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -0
  58. package/dist/src/self-relay/server.mjs +922 -849
  59. package/dist/src/server.mjs +474 -199
  60. package/dist/src/service-control.mjs +13 -3
  61. package/dist/src/service-doctor.mjs +72 -61
  62. package/dist/src/service-lifecycle.mjs +51 -43
  63. package/dist/src/shared-app-server.mjs +166 -107
  64. package/dist/src/shared-catalog.mjs +132 -40
  65. package/dist/src/shared-thread-stream.mjs +103 -25
  66. package/dist/src/usage-reset.mjs +17 -0
  67. package/dist/src/workstation-downloads.mjs +120 -0
  68. 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 request = '## My request:';
8
-
9
- function parse(value) {
10
- const source = typeof value === 'string' ? value : '';
11
- if (!source.startsWith(heading)) return { text: source, files: [] };
12
- const marker = source.indexOf(`\n${request}`, heading.length);
13
- if (marker < 0) return { text: source, files: [] };
14
- const files = [];
15
- for (const line of source.slice(heading.length, marker).split(/\r?\n/)) {
16
- const match = /^## (.+): ((?:[A-Za-z]:[\\/]|\/)[^\r\n]+)$/.exec(line);
17
- if (!match) continue;
18
- let name = match[1];
19
- if (name.startsWith('"')) {
20
- try { name = JSON.parse(name); } catch { continue; }
21
- }
22
- if (typeof name !== 'string' || !name) continue;
23
- files.push({ type: 'file', name, path: match[2] });
24
- }
25
- if (!files.length) return { text: source, files: [] };
26
- return { text: source.slice(marker + request.length + 1).trim(), files };
27
- }
28
-
29
- function wrap(prompt, files) {
30
- if (!files?.length) return prompt;
31
- return `${heading}\n${files.map(file => `\n## ${JSON.stringify(file.name)}: ${file.path}\n`).join('')}\n${request}\n${prompt || '请查看上述文件。'}`;
32
- }
33
-
34
- return Object.freeze({ parse, wrap });
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
- function browserAccess(request) {
11
- const p = request.params || {}, meta = p._meta || {};
12
- if (request.method !== elicitation || p.mode !== 'form' || meta.tool_name !== 'access_browser_origin'
13
- || meta.codex_approval_kind !== 'mcp_tool_call' || meta.codex_request_type !== 'approval_request'
14
- || meta.codex_sensitive_action !== true || p.requestedSchema?.type !== 'object'
15
- || Object.keys(p.requestedSchema.properties || {}).length) return null;
16
- try {
17
- const origin = new URL(meta.origin);
18
- if (!['http:', 'https:'].includes(origin.protocol) || origin.origin !== meta.origin) return null;
19
- return { origin: meta.origin, persistent: meta.persist === 'always' };
20
- } catch { return null; }
21
- }
22
- function decisions(request) {
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 browser = browserAccess(request);
26
- if (browser) return browser.persistent ? ['allowOnce', 'allowSite', 'allowAllSites', 'decline'] : ['allowOnce', 'decline'];
27
- return [];
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
- const browser = browserAccess(request);
41
- if (browser) {
42
- const allowed = [
43
- { action: 'accept', content: {}, _meta: null },
44
- { action: 'decline', content: null, _meta: null },
45
- ];
46
- if (browser.persistent) allowed.push(
47
- { action: 'accept', content: {}, _meta: { persist: 'always' } },
48
- { action: 'accept', content: {}, _meta: { persist: 'always', browser_use_persistent_approval_scope: 'all-sites' } },
49
- );
50
- if (!allowed.some(choice => equal(choice, result))) throw new Error('网站访问决定不属于当前请求');
51
- return;
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 = () => {}) { this.epoch = epoch; this.changed = changed; this.requests = new Map(); }
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) { this.requests.delete(params.requestId); this.changed(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)) this.requests.delete(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) { request.status = 'unknown'; this.changed(threadId); }
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": "uulife",
5
- "apiOrigin": "https://api.uulife.site",
6
- "webOrigin": "https://api.uulife.site",
7
- "adminOrigin": "https://api.uulife.site",
8
- "relayUrl": "wss://api.uulife.site/relay",
9
- "pairingUrl": "https://api.uulife.site/pair",
10
- "accountBase": "https://api.uulife.site/account",
11
- "announcementUrl": "https://api.uulife.site/announcement",
12
- "installUrl": "https://api.uulife.site/code-relax/install",
13
- "updateManifestUrl": "https://api.uulife.site/code-relax/releases/latest/manifest.json5",
14
- "adminUrl": "https://api.uulife.site/admin"
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;