@walkhi/code-relax 0.1.0-beta.50 → 0.1.0-beta.51
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 +105 -105
- package/dist/bin/cleanup-accounts.mjs +63 -63
- package/dist/bin/codex-remote.mjs +403 -403
- package/dist/bin/publish-android-release.mjs +63 -63
- package/dist/bin/release-upload-server.mjs +190 -190
- package/dist/build-platform.json +1 -1
- package/dist/get-shared-service-url.ps1 +14 -14
- package/dist/shared/code-browser.cjs +159 -159
- package/dist/shared/file-changes.cjs +31 -31
- package/dist/shared/file-citations.cjs +117 -117
- package/dist/shared/file-mentions.cjs +92 -92
- package/dist/shared/message-delivery.cjs +115 -115
- package/dist/shared/p2p-file-channel.cjs +87 -87
- package/dist/shared/server-requests.cjs +311 -311
- package/dist/shared/timeline-sync.cjs +173 -173
- package/dist/skills/code-relax/SKILL.md +22 -22
- package/dist/src/bounded-process-log.mjs +63 -63
- package/dist/src/celsius-client.mjs +107 -107
- package/dist/src/dots-approval-timeline.mjs +99 -99
- package/dist/src/dots-avatar.mjs +42 -42
- package/dist/src/dots-chat-projection.mjs +50 -50
- package/dist/src/dots-chat-server.mjs +483 -483
- package/dist/src/dots-message-images.mjs +319 -319
- package/dist/src/dots-profile.mjs +77 -77
- package/dist/src/dots-room-activity.mjs +172 -172
- package/dist/src/dots-room-client.mjs +135 -135
- package/dist/src/dots-room-projection.mjs +94 -94
- package/dist/src/dots-schedules.mjs +66 -66
- package/dist/src/dots-summary-tasks.mjs +32 -32
- package/dist/src/dots-task-catalog.mjs +430 -430
- package/dist/src/file-transfer-range.mjs +22 -22
- package/dist/src/file-upload-session.mjs +130 -130
- package/dist/src/hosted-history-app-server.mjs +103 -103
- package/dist/src/hosted-history-cache.mjs +113 -113
- package/dist/src/lan-socket-server.mjs +266 -266
- package/dist/src/managed-app-server.mjs +373 -373
- package/dist/src/managed-endpoint.mjs +25 -25
- package/dist/src/message-upload-files.mjs +109 -109
- package/dist/src/onboarding.mjs +12 -12
- package/dist/src/platform/README.md +1 -1
- package/dist/src/platform/codex-protocol.mjs +46 -46
- package/dist/src/platform/desktop-project-store.mjs +208 -208
- package/dist/src/platform/linux/desktop-host.mjs +87 -87
- package/dist/src/platform/linux/process.mjs +53 -53
- package/dist/src/platform/linux/system.mjs +50 -50
- package/dist/src/platform/linux/user-environment.mjs +9 -9
- package/dist/src/platform/macos/codex-runtime.mjs +125 -125
- package/dist/src/platform/macos/desktop-host.mjs +86 -86
- package/dist/src/platform/macos/user-environment.mjs +9 -9
- package/dist/src/platform/native-package.mjs +58 -58
- package/dist/src/platform/windows/activate-desktop.ps1 +49 -49
- package/dist/src/platform/windows/codex-runtime.mjs +215 -215
- package/dist/src/platform/windows/credential-store.mjs +27 -27
- package/dist/src/platform/windows/credential-store.ps1 +49 -49
- package/dist/src/platform/windows/desktop-host.mjs +65 -65
- package/dist/src/platform/windows/desktop-info.ps1 +38 -38
- package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +14 -14
- package/dist/src/platform/windows/dots-profile.mjs +49 -49
- package/dist/src/platform/windows/launch-worker.mjs +41 -41
- package/dist/src/platform/windows/project-locations.mjs +16 -16
- package/dist/src/platform/windows/service-host.mjs +35 -35
- package/dist/src/platform/windows/user-environment.mjs +52 -52
- package/dist/src/project-management.mjs +58 -58
- package/dist/src/runtime-discovery.mjs +47 -47
- package/dist/src/self-relay/account-cleanup.mjs +30 -30
- package/dist/src/self-relay/admin-page.mjs +80 -80
- package/dist/src/self-relay/admin-style.mjs +14 -14
- package/dist/src/self-relay/connector.mjs +439 -439
- package/dist/src/self-relay/demo.mjs +31 -31
- package/dist/src/self-relay/diagnostics.mjs +54 -54
- package/dist/src/self-relay/email-accounts.mjs +183 -183
- package/dist/src/self-relay/file-wire.mjs +13 -13
- package/dist/src/self-relay/p2p-probe.mjs +168 -168
- package/dist/src/self-relay/privacy-page.mjs +26 -26
- package/dist/src/self-relay/public-releases.mjs +67 -67
- package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
- package/dist/src/self-relay/usage-meter.mjs +141 -141
- package/dist/src/server.mjs +5 -4
- package/dist/src/service-doctor.mjs +148 -148
- package/dist/src/service-install.mjs +97 -97
- package/dist/src/service-lifecycle.mjs +139 -139
- package/dist/src/shared-app-server.mjs +666 -666
- package/dist/src/shared-catalog.mjs +132 -132
- package/dist/src/shared-thread-stream.mjs +165 -165
- package/dist/src/usage-reset.mjs +17 -17
- package/dist/src/workstation-downloads.mjs +120 -120
- package/package.json +4 -4
- package/tools/postinstall.mjs +186 -186
|
@@ -1,311 +1,311 @@
|
|
|
1
|
-
(function (root, factory) {
|
|
2
|
-
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
3
|
-
else root.CodexServerRequests = factory();
|
|
4
|
-
})(globalThis, function () {
|
|
5
|
-
const command = 'item/commandExecution/requestApproval';
|
|
6
|
-
const file = 'item/fileChange/requestApproval';
|
|
7
|
-
const input = 'item/tool/requestUserInput';
|
|
8
|
-
const elicitation = 'mcpServer/elicitation/request';
|
|
9
|
-
const permissions = 'item/permissions/requestApproval';
|
|
10
|
-
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
11
|
-
const approvalId = request => 'approval:' + (request.params.turnId || '') + ':' + typeof request.id + ':' + request.id;
|
|
12
|
-
function confirmation(request) {
|
|
13
|
-
const p = request.params || {}, value = p._meta?.['openai/confirmation'], schema = p.requestedSchema;
|
|
14
|
-
if (request.method !== elicitation || p.mode !== 'form' || value?.version !== 1
|
|
15
|
-
|| typeof value.field !== 'string' || schema?.type !== 'object'
|
|
16
|
-
|| Object.keys(schema.properties || {}).length !== 1 || schema.properties[value.field]?.type !== 'boolean'
|
|
17
|
-
|| schema.required?.length !== 1 || schema.required[0] !== value.field) return null;
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
function confirmationResult(request, decision) {
|
|
21
|
-
return { action: decision, content: decision === 'accept' ? { [confirmation(request).field]: true } : null, _meta: null };
|
|
22
|
-
}
|
|
23
|
-
function browserAccess(request) {
|
|
24
|
-
const p = request.params || {}, meta = p._meta || {};
|
|
25
|
-
if (request.method !== elicitation || p.mode !== 'form' || meta.tool_name !== 'access_browser_origin'
|
|
26
|
-
|| meta.codex_approval_kind !== 'mcp_tool_call' || meta.codex_request_type !== 'approval_request'
|
|
27
|
-
|| meta.codex_sensitive_action !== true || p.requestedSchema?.type !== 'object'
|
|
28
|
-
|| Object.keys(p.requestedSchema.properties || {}).length) return null;
|
|
29
|
-
try {
|
|
30
|
-
const origin = new URL(meta.origin);
|
|
31
|
-
if (!['http:', 'https:'].includes(origin.protocol) || origin.origin !== meta.origin) return null;
|
|
32
|
-
return { origin: meta.origin, persistent: meta.persist === 'always' };
|
|
33
|
-
} catch { return null; }
|
|
34
|
-
}
|
|
35
|
-
function browserAction(request) {
|
|
36
|
-
const p = request.params || {}, meta = p._meta || {};
|
|
37
|
-
if (browserAccess(request) || request.method !== elicitation || p.mode !== 'form'
|
|
38
|
-
|| p.requestedSchema?.type !== 'object' || Object.keys(p.requestedSchema.properties || {}).length
|
|
39
|
-
|| p.requestedSchema.required?.length || typeof p.message !== 'string' || !p.message.trim()) return null;
|
|
40
|
-
const tool = meta.codex_approval_kind === 'mcp_tool_call' && meta.connector_id === 'browser-use';
|
|
41
|
-
if (!tool && !['browser', 'browser-use'].includes(p.serverName)) return null;
|
|
42
|
-
if (meta.codex_approval_kind && meta.codex_approval_kind !== 'mcp_tool_call') return null;
|
|
43
|
-
const modes = meta.persist == null ? [] : Array.isArray(meta.persist) ? meta.persist : [meta.persist];
|
|
44
|
-
if (modes.some(mode => !['session', 'always'].includes(mode))) return null;
|
|
45
|
-
if (!tool && !modes.length) return null;
|
|
46
|
-
try {
|
|
47
|
-
const origin = new URL(meta.origin);
|
|
48
|
-
if (!['http:', 'https:'].includes(origin.protocol) || origin.origin !== meta.origin) return null;
|
|
49
|
-
return { origin: meta.origin, session: modes.includes('session'), persistent: modes.includes('always') };
|
|
50
|
-
} catch { return null; }
|
|
51
|
-
}
|
|
52
|
-
function browserActionResult(decision) {
|
|
53
|
-
return { action: decision === 'cancel' ? 'cancel' : 'accept', content: decision === 'cancel' ? null : {},
|
|
54
|
-
_meta: decision === 'allowAlways' ? { persist: 'always' }
|
|
55
|
-
: decision === 'allowSession' ? { persist: 'session' } : null };
|
|
56
|
-
}
|
|
57
|
-
function computerAppAccess(request) {
|
|
58
|
-
const p = request.params || {}, meta = p._meta || {};
|
|
59
|
-
if (request.method !== elicitation || p.mode !== 'form' || meta.codex_approval_kind !== 'mcp_tool_call'
|
|
60
|
-
|| (meta.codex_request_type != null && meta.codex_request_type !== 'approval_request')
|
|
61
|
-
|| typeof meta.connector_id !== 'string' || p.requestedSchema?.type !== 'object'
|
|
62
|
-
|| Object.keys(p.requestedSchema.properties || {}).length || p.requestedSchema.required?.length) return null;
|
|
63
|
-
const connector = meta.connector_id.trim().toLowerCase().split(/[^a-z0-9]+/g).filter(Boolean).join('-');
|
|
64
|
-
if (connector !== 'computer-use' && !connector.startsWith('computer-use-')) return null;
|
|
65
|
-
const modes = Array.isArray(meta.persist) ? meta.persist : [meta.persist];
|
|
66
|
-
if (!modes.length || modes.some(mode => !['session', 'always'].includes(mode))) return null;
|
|
67
|
-
const display = Array.isArray(meta.tool_params_display) ? meta.tool_params_display.find(entry => entry?.name === 'app')?.value : null;
|
|
68
|
-
const app = typeof display === 'string' && display.trim() ? display : meta.tool_params?.app;
|
|
69
|
-
if (typeof app !== 'string' || !app.trim()) return null;
|
|
70
|
-
return { app: app.trim(), session: modes.includes('session'), persistent: modes.includes('always'),
|
|
71
|
-
subtitle: typeof meta.subtitle === 'string' ? meta.subtitle : '', highRisk: meta.riskLevel === 'high' };
|
|
72
|
-
}
|
|
73
|
-
function computerAppResult(request, decision) {
|
|
74
|
-
const app = computerAppAccess(request);
|
|
75
|
-
return { action: decision === 'decline' ? 'decline' : 'accept', content: decision === 'decline' ? null : {},
|
|
76
|
-
_meta: decision === 'allowAlways' ? { persist: 'always' }
|
|
77
|
-
: decision === 'allowConversation' && app?.session ? { persist: 'session' } : null };
|
|
78
|
-
}
|
|
79
|
-
function mcpToolApproval(request) {
|
|
80
|
-
const p = request.params || {}, meta = p._meta || {};
|
|
81
|
-
if (request.method !== elicitation || p.mode !== 'form' || meta.codex_approval_kind !== 'mcp_tool_call'
|
|
82
|
-
|| (meta.codex_request_type != null && meta.codex_request_type !== 'approval_request')
|
|
83
|
-
|| p.requestedSchema?.type !== 'object' || Object.keys(p.requestedSchema.properties || {}).length
|
|
84
|
-
|| p.requestedSchema.required?.length || typeof p.message !== 'string' || !p.message.trim()
|
|
85
|
-
|| typeof meta.connector_id !== 'string' || !meta.connector_id.trim()
|
|
86
|
-
|| !meta.tool_params || typeof meta.tool_params !== 'object' || Array.isArray(meta.tool_params)) return null;
|
|
87
|
-
const connector = meta.connector_id.trim().toLowerCase().split(/[^a-z0-9]+/g).filter(Boolean).join('-');
|
|
88
|
-
// These requests have dedicated approval semantics in Desktop.
|
|
89
|
-
if (connector === 'browser-use' || connector === 'computer-use' || connector.startsWith('computer-use-')
|
|
90
|
-
|| meta.tool_name === 'access_browser_origin' || meta.tool_name === 'access_browser_origin_with_raw_cdp'
|
|
91
|
-
|| meta.connector_id === 'connector_20205bf7d4e99a89d7154bb849718324') return null;
|
|
92
|
-
const modes = meta.persist === undefined ? [] : Array.isArray(meta.persist) ? meta.persist : [meta.persist];
|
|
93
|
-
if ((Array.isArray(meta.persist) && (!modes.length || modes.length > 2))
|
|
94
|
-
|| modes.some(mode => !['session', 'always'].includes(mode))) return null;
|
|
95
|
-
return { session: modes.includes('session'), persistent: modes.includes('always') };
|
|
96
|
-
}
|
|
97
|
-
function mcpToolResult(decision) {
|
|
98
|
-
return { action: decision === 'decline' ? 'decline' : 'accept', content: decision === 'decline' ? null : {},
|
|
99
|
-
_meta: decision === 'allowAlways' ? { persist: 'always' }
|
|
100
|
-
: decision === 'allowSession' ? { persist: 'session' } : null };
|
|
101
|
-
}
|
|
102
|
-
function externalAction(request) {
|
|
103
|
-
const p = request.params || {};
|
|
104
|
-
if (request.method !== elicitation || p.mode !== 'url') return null;
|
|
105
|
-
try {
|
|
106
|
-
const url = new URL(p.url);
|
|
107
|
-
if (url.protocol !== 'https:' || url.username || url.password) return null;
|
|
108
|
-
if (p.serverName !== 'codex_apps') return { url: url.href, connector: null };
|
|
109
|
-
const connector = p._meta?._codex_apps?.connector_auth_failure;
|
|
110
|
-
const trusted = host => ['chatgpt.com', 'chatgpt-staging.com'].some(domain => host === domain || host.endsWith('.' + domain));
|
|
111
|
-
if (!trusted(url.hostname) || connector?.is_auth_failure !== true
|
|
112
|
-
|| typeof connector.connector_id !== 'string' || typeof connector.connector_name !== 'string'
|
|
113
|
-
|| typeof connector.install_url !== 'string' || connector.connection_consent != null) return null;
|
|
114
|
-
return { url: url.href, connector };
|
|
115
|
-
} catch { return null; }
|
|
116
|
-
}
|
|
117
|
-
function elicitationResult(action, content = null) { return { action, content: action === 'accept' ? content : null, _meta: null }; }
|
|
118
|
-
function fieldOptions(field) {
|
|
119
|
-
const schema = field.type === 'array' ? field.items : field;
|
|
120
|
-
if (Array.isArray(schema?.enum)) return schema.enum.map((value, index) => ({ value, label: schema.enumNames?.[index] || value }));
|
|
121
|
-
const options = schema?.oneOf || schema?.anyOf;
|
|
122
|
-
return Array.isArray(options) ? options.map(option => ({ value: option.const, label: option.title || option.const })) : null;
|
|
123
|
-
}
|
|
124
|
-
function formSchema(request) {
|
|
125
|
-
const p = request.params || {}, meta = p._meta || {}, schema = p.requestedSchema;
|
|
126
|
-
// Specialized approvals, credentials and OpenAI forms have their own response contracts.
|
|
127
|
-
if (request.method !== elicitation || p.mode !== 'form' || meta.codex_approval_kind
|
|
128
|
-
|| meta['openai/elicitation'] || meta['openai/confirmation'] || schema?.type !== 'object'
|
|
129
|
-
|| ['browser', 'browser-use'].includes(p.serverName)
|
|
130
|
-
|| !schema.properties || typeof schema.properties !== 'object' || Array.isArray(schema.properties)
|
|
131
|
-
|| Object.keys(schema).some(key => !['$schema', 'type', 'properties', 'required'].includes(key))) return null;
|
|
132
|
-
const known = ['type', 'title', 'description', 'default', 'minLength', 'maxLength', 'format', 'minimum', 'maximum', 'enum', 'enumNames', 'oneOf', 'items', 'minItems', 'maxItems'];
|
|
133
|
-
for (const field of Object.values(schema.properties)) {
|
|
134
|
-
if (!field || Object.keys(field).some(key => !known.includes(key))
|
|
135
|
-
|| !['string', 'number', 'integer', 'boolean', 'array'].includes(field.type)) return null;
|
|
136
|
-
const options = fieldOptions(field);
|
|
137
|
-
if ((field.type === 'array' || field.enum != null || field.oneOf != null) && !options
|
|
138
|
-
|| options && (!options.length || options.some(option => typeof option.value !== 'string' || typeof option.label !== 'string'))) return null;
|
|
139
|
-
if (field.format && !['email', 'uri', 'date', 'date-time'].includes(field.format)) return null;
|
|
140
|
-
}
|
|
141
|
-
if (schema.required != null && (!Array.isArray(schema.required) || schema.required.some(key => !Object.hasOwn(schema.properties, key)))) return null;
|
|
142
|
-
return schema;
|
|
143
|
-
}
|
|
144
|
-
function validateForm(schema, values) {
|
|
145
|
-
if (!values || typeof values !== 'object' || Array.isArray(values)
|
|
146
|
-
|| Object.keys(values).some(key => !Object.hasOwn(schema.properties, key))) throw new Error('表单内容无效');
|
|
147
|
-
for (const [key, field] of Object.entries(schema.properties)) {
|
|
148
|
-
const value = values[key], label = field.title || key;
|
|
149
|
-
if (value === undefined) { if (schema.required?.includes(key)) throw new Error(`请填写${label}`); continue; }
|
|
150
|
-
let valid = field.type === 'array' ? Array.isArray(value) : typeof value === (field.type === 'integer' ? 'number' : field.type);
|
|
151
|
-
if (typeof value === 'number') valid &&= Number.isFinite(value) && (field.type !== 'integer' || Number.isInteger(value))
|
|
152
|
-
&& (field.minimum == null || value >= field.minimum) && (field.maximum == null || value <= field.maximum);
|
|
153
|
-
if (typeof value === 'string') {
|
|
154
|
-
valid &&= (field.minLength == null || [...value].length >= field.minLength) && (field.maxLength == null || [...value].length <= field.maxLength);
|
|
155
|
-
if (field.format === 'email') valid &&= /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
156
|
-
if (field.format === 'uri') { try { new URL(value); } catch { valid = false; } }
|
|
157
|
-
if (field.format === 'date') valid &&= /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(value));
|
|
158
|
-
if (field.format === 'date-time') valid &&= /^\d{4}-\d{2}-\d{2}T.+(?:Z|[+-]\d{2}:\d{2})$/.test(value) && !Number.isNaN(Date.parse(value));
|
|
159
|
-
}
|
|
160
|
-
const options = fieldOptions(field);
|
|
161
|
-
if (Array.isArray(value)) valid &&= new Set(value).size === value.length && (field.minItems == null || value.length >= field.minItems)
|
|
162
|
-
&& (field.maxItems == null || value.length <= field.maxItems) && value.every(item => options?.some(option => option.value === item));
|
|
163
|
-
else if (options) valid &&= options.some(option => option.value === value);
|
|
164
|
-
if (!valid) throw new Error(`${label}的内容不符合请求要求`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
function permissionResult(request, decision) {
|
|
168
|
-
return { permissions: decision === 'decline' ? {} : request.params.permissions, scope: decision === 'acceptForSession' ? 'session' : 'turn' };
|
|
169
|
-
}
|
|
170
|
-
function blocksComposer(request) {
|
|
171
|
-
if (externalAction(request)) return false;
|
|
172
|
-
return request.method === input || Boolean(formSchema(request))
|
|
173
|
-
|| request.method !== elicitation && decisions(request).length > 0
|
|
174
|
-
|| request.method === elicitation && decisions(request).some(value => value !== 'cancel');
|
|
175
|
-
}
|
|
176
|
-
function decisions(request) {
|
|
177
|
-
if (confirmation(request)) return ['decline', 'accept'];
|
|
178
|
-
if (request.method === command) return request.params.availableDecisions ?? ['accept', 'acceptForSession', 'decline', 'cancel'];
|
|
179
|
-
if (request.method === file) return ['accept', 'acceptForSession', 'decline', 'cancel'];
|
|
180
|
-
if (request.method === permissions && request.params.permissions) return ['accept', 'acceptForSession', 'decline'];
|
|
181
|
-
if (externalAction(request)) return ['decline', 'accept'];
|
|
182
|
-
const action = browserAction(request);
|
|
183
|
-
if (action) return ['allowOnce', ...(action.session ? ['allowSession'] : []), ...(action.persistent ? ['allowAlways'] : []), 'cancel'];
|
|
184
|
-
const app = computerAppAccess(request);
|
|
185
|
-
if (app) return ['decline', 'allowConversation', ...(app.persistent ? ['allowAlways'] : [])];
|
|
186
|
-
const browser = browserAccess(request);
|
|
187
|
-
if (browser) return browser.persistent ? ['allowOnce', 'allowSite', 'allowAllSites', 'decline'] : ['allowOnce', 'decline'];
|
|
188
|
-
const tool = mcpToolApproval(request);
|
|
189
|
-
if (tool) return ['decline', 'allowOnce', ...(tool.session ? ['allowSession'] : []), ...(tool.persistent ? ['allowAlways'] : [])];
|
|
190
|
-
return request.method === elicitation ? ['cancel'] : [];
|
|
191
|
-
}
|
|
192
|
-
function validate(request, result) {
|
|
193
|
-
if (request.method === elicitation && equal(result, elicitationResult('cancel'))) return;
|
|
194
|
-
if (externalAction(request)) {
|
|
195
|
-
if (!['accept', 'decline'].some(action => equal(result, elicitationResult(action)))) throw new Error('外部操作结果无效');
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
const schema = formSchema(request);
|
|
199
|
-
if (schema) {
|
|
200
|
-
if (equal(result, elicitationResult('decline'))) return;
|
|
201
|
-
if (result?.action !== 'accept' || result._meta !== null || Object.keys(result).length !== 3) throw new Error('表单响应格式无效');
|
|
202
|
-
validateForm(schema, result.content); return;
|
|
203
|
-
}
|
|
204
|
-
if (request.method === permissions) {
|
|
205
|
-
if (!decisions(request).some(decision => equal(result, permissionResult(request, decision)))) throw new Error('权限决定不属于当前请求');
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
if (confirmation(request)) {
|
|
209
|
-
if (!decisions(request).some(decision => equal(confirmationResult(request, decision), result))) throw new Error('确认结果不属于当前请求');
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if ([command, file].includes(request.method)) {
|
|
213
|
-
if (!result || Object.keys(result).length !== 1 || !decisions(request).some(value => equal(value, result.decision))) throw new Error('审批选项不属于当前请求');
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (request.method === input) {
|
|
217
|
-
const questions = request.params.questions || [];
|
|
218
|
-
if (!result?.answers || Object.keys(result).length !== 1 || Object.keys(result.answers).length !== questions.length
|
|
219
|
-
|| questions.some(q => !Array.isArray(result.answers[q.id]?.answers) || result.answers[q.id].answers.some(a => typeof a !== 'string'))) throw new Error('回答格式无效');
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
if (browserAction(request)) {
|
|
223
|
-
if (!decisions(request).some(decision => equal(browserActionResult(decision), result))) throw new Error('浏览器操作决定不属于当前请求');
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
if (computerAppAccess(request)) {
|
|
227
|
-
if (!decisions(request).some(decision => equal(computerAppResult(request, decision), result))) throw new Error('应用访问决定不属于当前请求');
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
const browser = browserAccess(request);
|
|
231
|
-
if (browser) {
|
|
232
|
-
const allowed = [
|
|
233
|
-
{ action: 'accept', content: {}, _meta: null },
|
|
234
|
-
{ action: 'decline', content: null, _meta: null },
|
|
235
|
-
];
|
|
236
|
-
if (browser.persistent) allowed.push(
|
|
237
|
-
{ action: 'accept', content: {}, _meta: { persist: 'always' } },
|
|
238
|
-
{ action: 'accept', content: {}, _meta: { persist: 'always', browser_use_persistent_approval_scope: 'all-sites' } },
|
|
239
|
-
);
|
|
240
|
-
if (!allowed.some(choice => equal(choice, result))) throw new Error('网站访问决定不属于当前请求');
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (mcpToolApproval(request)) {
|
|
244
|
-
if (!decisions(request).some(decision => equal(mcpToolResult(decision), result))) throw new Error('工具授权决定不属于当前请求');
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
throw new Error('此类请求需在工作站处理');
|
|
248
|
-
}
|
|
249
|
-
class Store {
|
|
250
|
-
constructor(epoch, changed = () => {}) {
|
|
251
|
-
this.epoch = epoch; this.changed = changed; this.requests = new Map(); this.approvalHistory = new Map();
|
|
252
|
-
}
|
|
253
|
-
list(threadId) { return [...this.requests.values()].filter(r => r.params.threadId === threadId).map(r => ({ ...r })); }
|
|
254
|
-
history(threadId) { return [...this.approvalHistory.values()].filter(r => r.params.threadId === threadId).map(r => ({ ...r })); }
|
|
255
|
-
complete(request, status) {
|
|
256
|
-
const approval = this.approvalHistory.get(approvalId(request));
|
|
257
|
-
if (approval) { approval.status = status; approval.completedAtMs = Date.now(); }
|
|
258
|
-
}
|
|
259
|
-
receive(message) {
|
|
260
|
-
if (!['string', 'number'].includes(typeof message.id) || !message.params?.threadId || this.requests.has(message.id)) return;
|
|
261
|
-
const request = { id: message.id, method: message.method, params: message.params, epoch: this.epoch, status: 'pending' };
|
|
262
|
-
if (request.method !== input) this.approvalHistory.set(approvalId(request), {
|
|
263
|
-
type: 'approval', id: approvalId(request), requestId: request.id, method: request.method,
|
|
264
|
-
params: request.params, turnId: request.params.turnId, status: 'pending', receivedAtMs: Date.now(),
|
|
265
|
-
});
|
|
266
|
-
this.requests.set(message.id, request); this.changed(request.params.threadId);
|
|
267
|
-
}
|
|
268
|
-
notification(method, params) {
|
|
269
|
-
if (method === 'serverRequest/resolved') {
|
|
270
|
-
const request = this.requests.get(params.requestId);
|
|
271
|
-
if (request?.params.threadId === params.threadId) {
|
|
272
|
-
this.complete(request, 'resolved'); this.requests.delete(params.requestId); this.changed(params.threadId);
|
|
273
|
-
}
|
|
274
|
-
} else if (method === 'turn/completed' || method === 'thread/closed') {
|
|
275
|
-
for (const [id, r] of this.requests) if (r.params.threadId === params.threadId && (method === 'thread/closed' || r.params.turnId === params.turn?.id)) {
|
|
276
|
-
this.complete(r, 'expired'); this.requests.delete(id);
|
|
277
|
-
}
|
|
278
|
-
this.changed(params.threadId);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
forget(threadId) {
|
|
282
|
-
for (const [id, request] of this.requests) if (request.params.threadId === threadId) this.requests.delete(id);
|
|
283
|
-
for (const [id, approval] of this.approvalHistory) if (approval.params.threadId === threadId) this.approvalHistory.delete(id);
|
|
284
|
-
this.changed(threadId);
|
|
285
|
-
}
|
|
286
|
-
reset(epoch) {
|
|
287
|
-
const threads = new Set([...this.requests.values()].map(r => r.params.threadId));
|
|
288
|
-
for (const request of this.requests.values()) this.complete(request, 'expired');
|
|
289
|
-
this.requests.clear(); this.epoch = epoch; for (const id of threads) this.changed(id);
|
|
290
|
-
this.approvalHistory.clear();
|
|
291
|
-
}
|
|
292
|
-
async respond(threadId, { id, epoch, result }, send) {
|
|
293
|
-
const request = this.requests.get(id);
|
|
294
|
-
if (!request || epoch !== this.epoch || request.params.threadId !== threadId) throw new Error('审批已处理或连接已变化,请等待同步');
|
|
295
|
-
if (request.status !== 'pending') throw new Error('决定已提交,正在等待工作站确认,请勿重复提交');
|
|
296
|
-
validate(request, result);
|
|
297
|
-
const approval = this.approvalHistory.get(approvalId(request));
|
|
298
|
-
if (approval) { approval.status = 'submitted'; approval.result = result; }
|
|
299
|
-
request.status = 'submitted'; this.changed(threadId);
|
|
300
|
-
try { await send({ id: request.id, result }); }
|
|
301
|
-
catch (error) {
|
|
302
|
-
if (this.requests.get(id) === request) {
|
|
303
|
-
request.status = 'unknown'; if (approval) approval.status = 'unknown'; this.changed(threadId);
|
|
304
|
-
}
|
|
305
|
-
throw new Error('审批发送结果未确认,请检查工作站;不会自动重发。' + error.message);
|
|
306
|
-
}
|
|
307
|
-
return { submitted: true };
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return { Store, decisions, validate, command, file, input, elicitation, permissions, externalAction, elicitationResult, formSchema, fieldOptions, validateForm, permissionResult, blocksComposer, browserAccess, browserAction, browserActionResult, computerAppAccess, computerAppResult, mcpToolApproval, mcpToolResult, confirmation, confirmationResult };
|
|
311
|
-
});
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
3
|
+
else root.CodexServerRequests = factory();
|
|
4
|
+
})(globalThis, function () {
|
|
5
|
+
const command = 'item/commandExecution/requestApproval';
|
|
6
|
+
const file = 'item/fileChange/requestApproval';
|
|
7
|
+
const input = 'item/tool/requestUserInput';
|
|
8
|
+
const elicitation = 'mcpServer/elicitation/request';
|
|
9
|
+
const permissions = 'item/permissions/requestApproval';
|
|
10
|
+
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
11
|
+
const approvalId = request => 'approval:' + (request.params.turnId || '') + ':' + typeof request.id + ':' + request.id;
|
|
12
|
+
function confirmation(request) {
|
|
13
|
+
const p = request.params || {}, value = p._meta?.['openai/confirmation'], schema = p.requestedSchema;
|
|
14
|
+
if (request.method !== elicitation || p.mode !== 'form' || value?.version !== 1
|
|
15
|
+
|| typeof value.field !== 'string' || schema?.type !== 'object'
|
|
16
|
+
|| Object.keys(schema.properties || {}).length !== 1 || schema.properties[value.field]?.type !== 'boolean'
|
|
17
|
+
|| schema.required?.length !== 1 || schema.required[0] !== value.field) return null;
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function confirmationResult(request, decision) {
|
|
21
|
+
return { action: decision, content: decision === 'accept' ? { [confirmation(request).field]: true } : null, _meta: null };
|
|
22
|
+
}
|
|
23
|
+
function browserAccess(request) {
|
|
24
|
+
const p = request.params || {}, meta = p._meta || {};
|
|
25
|
+
if (request.method !== elicitation || p.mode !== 'form' || meta.tool_name !== 'access_browser_origin'
|
|
26
|
+
|| meta.codex_approval_kind !== 'mcp_tool_call' || meta.codex_request_type !== 'approval_request'
|
|
27
|
+
|| meta.codex_sensitive_action !== true || p.requestedSchema?.type !== 'object'
|
|
28
|
+
|| Object.keys(p.requestedSchema.properties || {}).length) return null;
|
|
29
|
+
try {
|
|
30
|
+
const origin = new URL(meta.origin);
|
|
31
|
+
if (!['http:', 'https:'].includes(origin.protocol) || origin.origin !== meta.origin) return null;
|
|
32
|
+
return { origin: meta.origin, persistent: meta.persist === 'always' };
|
|
33
|
+
} catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
function browserAction(request) {
|
|
36
|
+
const p = request.params || {}, meta = p._meta || {};
|
|
37
|
+
if (browserAccess(request) || request.method !== elicitation || p.mode !== 'form'
|
|
38
|
+
|| p.requestedSchema?.type !== 'object' || Object.keys(p.requestedSchema.properties || {}).length
|
|
39
|
+
|| p.requestedSchema.required?.length || typeof p.message !== 'string' || !p.message.trim()) return null;
|
|
40
|
+
const tool = meta.codex_approval_kind === 'mcp_tool_call' && meta.connector_id === 'browser-use';
|
|
41
|
+
if (!tool && !['browser', 'browser-use'].includes(p.serverName)) return null;
|
|
42
|
+
if (meta.codex_approval_kind && meta.codex_approval_kind !== 'mcp_tool_call') return null;
|
|
43
|
+
const modes = meta.persist == null ? [] : Array.isArray(meta.persist) ? meta.persist : [meta.persist];
|
|
44
|
+
if (modes.some(mode => !['session', 'always'].includes(mode))) return null;
|
|
45
|
+
if (!tool && !modes.length) return null;
|
|
46
|
+
try {
|
|
47
|
+
const origin = new URL(meta.origin);
|
|
48
|
+
if (!['http:', 'https:'].includes(origin.protocol) || origin.origin !== meta.origin) return null;
|
|
49
|
+
return { origin: meta.origin, session: modes.includes('session'), persistent: modes.includes('always') };
|
|
50
|
+
} catch { return null; }
|
|
51
|
+
}
|
|
52
|
+
function browserActionResult(decision) {
|
|
53
|
+
return { action: decision === 'cancel' ? 'cancel' : 'accept', content: decision === 'cancel' ? null : {},
|
|
54
|
+
_meta: decision === 'allowAlways' ? { persist: 'always' }
|
|
55
|
+
: decision === 'allowSession' ? { persist: 'session' } : null };
|
|
56
|
+
}
|
|
57
|
+
function computerAppAccess(request) {
|
|
58
|
+
const p = request.params || {}, meta = p._meta || {};
|
|
59
|
+
if (request.method !== elicitation || p.mode !== 'form' || meta.codex_approval_kind !== 'mcp_tool_call'
|
|
60
|
+
|| (meta.codex_request_type != null && meta.codex_request_type !== 'approval_request')
|
|
61
|
+
|| typeof meta.connector_id !== 'string' || p.requestedSchema?.type !== 'object'
|
|
62
|
+
|| Object.keys(p.requestedSchema.properties || {}).length || p.requestedSchema.required?.length) return null;
|
|
63
|
+
const connector = meta.connector_id.trim().toLowerCase().split(/[^a-z0-9]+/g).filter(Boolean).join('-');
|
|
64
|
+
if (connector !== 'computer-use' && !connector.startsWith('computer-use-')) return null;
|
|
65
|
+
const modes = Array.isArray(meta.persist) ? meta.persist : [meta.persist];
|
|
66
|
+
if (!modes.length || modes.some(mode => !['session', 'always'].includes(mode))) return null;
|
|
67
|
+
const display = Array.isArray(meta.tool_params_display) ? meta.tool_params_display.find(entry => entry?.name === 'app')?.value : null;
|
|
68
|
+
const app = typeof display === 'string' && display.trim() ? display : meta.tool_params?.app;
|
|
69
|
+
if (typeof app !== 'string' || !app.trim()) return null;
|
|
70
|
+
return { app: app.trim(), session: modes.includes('session'), persistent: modes.includes('always'),
|
|
71
|
+
subtitle: typeof meta.subtitle === 'string' ? meta.subtitle : '', highRisk: meta.riskLevel === 'high' };
|
|
72
|
+
}
|
|
73
|
+
function computerAppResult(request, decision) {
|
|
74
|
+
const app = computerAppAccess(request);
|
|
75
|
+
return { action: decision === 'decline' ? 'decline' : 'accept', content: decision === 'decline' ? null : {},
|
|
76
|
+
_meta: decision === 'allowAlways' ? { persist: 'always' }
|
|
77
|
+
: decision === 'allowConversation' && app?.session ? { persist: 'session' } : null };
|
|
78
|
+
}
|
|
79
|
+
function mcpToolApproval(request) {
|
|
80
|
+
const p = request.params || {}, meta = p._meta || {};
|
|
81
|
+
if (request.method !== elicitation || p.mode !== 'form' || meta.codex_approval_kind !== 'mcp_tool_call'
|
|
82
|
+
|| (meta.codex_request_type != null && meta.codex_request_type !== 'approval_request')
|
|
83
|
+
|| p.requestedSchema?.type !== 'object' || Object.keys(p.requestedSchema.properties || {}).length
|
|
84
|
+
|| p.requestedSchema.required?.length || typeof p.message !== 'string' || !p.message.trim()
|
|
85
|
+
|| typeof meta.connector_id !== 'string' || !meta.connector_id.trim()
|
|
86
|
+
|| !meta.tool_params || typeof meta.tool_params !== 'object' || Array.isArray(meta.tool_params)) return null;
|
|
87
|
+
const connector = meta.connector_id.trim().toLowerCase().split(/[^a-z0-9]+/g).filter(Boolean).join('-');
|
|
88
|
+
// These requests have dedicated approval semantics in Desktop.
|
|
89
|
+
if (connector === 'browser-use' || connector === 'computer-use' || connector.startsWith('computer-use-')
|
|
90
|
+
|| meta.tool_name === 'access_browser_origin' || meta.tool_name === 'access_browser_origin_with_raw_cdp'
|
|
91
|
+
|| meta.connector_id === 'connector_20205bf7d4e99a89d7154bb849718324') return null;
|
|
92
|
+
const modes = meta.persist === undefined ? [] : Array.isArray(meta.persist) ? meta.persist : [meta.persist];
|
|
93
|
+
if ((Array.isArray(meta.persist) && (!modes.length || modes.length > 2))
|
|
94
|
+
|| modes.some(mode => !['session', 'always'].includes(mode))) return null;
|
|
95
|
+
return { session: modes.includes('session'), persistent: modes.includes('always') };
|
|
96
|
+
}
|
|
97
|
+
function mcpToolResult(decision) {
|
|
98
|
+
return { action: decision === 'decline' ? 'decline' : 'accept', content: decision === 'decline' ? null : {},
|
|
99
|
+
_meta: decision === 'allowAlways' ? { persist: 'always' }
|
|
100
|
+
: decision === 'allowSession' ? { persist: 'session' } : null };
|
|
101
|
+
}
|
|
102
|
+
function externalAction(request) {
|
|
103
|
+
const p = request.params || {};
|
|
104
|
+
if (request.method !== elicitation || p.mode !== 'url') return null;
|
|
105
|
+
try {
|
|
106
|
+
const url = new URL(p.url);
|
|
107
|
+
if (url.protocol !== 'https:' || url.username || url.password) return null;
|
|
108
|
+
if (p.serverName !== 'codex_apps') return { url: url.href, connector: null };
|
|
109
|
+
const connector = p._meta?._codex_apps?.connector_auth_failure;
|
|
110
|
+
const trusted = host => ['chatgpt.com', 'chatgpt-staging.com'].some(domain => host === domain || host.endsWith('.' + domain));
|
|
111
|
+
if (!trusted(url.hostname) || connector?.is_auth_failure !== true
|
|
112
|
+
|| typeof connector.connector_id !== 'string' || typeof connector.connector_name !== 'string'
|
|
113
|
+
|| typeof connector.install_url !== 'string' || connector.connection_consent != null) return null;
|
|
114
|
+
return { url: url.href, connector };
|
|
115
|
+
} catch { return null; }
|
|
116
|
+
}
|
|
117
|
+
function elicitationResult(action, content = null) { return { action, content: action === 'accept' ? content : null, _meta: null }; }
|
|
118
|
+
function fieldOptions(field) {
|
|
119
|
+
const schema = field.type === 'array' ? field.items : field;
|
|
120
|
+
if (Array.isArray(schema?.enum)) return schema.enum.map((value, index) => ({ value, label: schema.enumNames?.[index] || value }));
|
|
121
|
+
const options = schema?.oneOf || schema?.anyOf;
|
|
122
|
+
return Array.isArray(options) ? options.map(option => ({ value: option.const, label: option.title || option.const })) : null;
|
|
123
|
+
}
|
|
124
|
+
function formSchema(request) {
|
|
125
|
+
const p = request.params || {}, meta = p._meta || {}, schema = p.requestedSchema;
|
|
126
|
+
// Specialized approvals, credentials and OpenAI forms have their own response contracts.
|
|
127
|
+
if (request.method !== elicitation || p.mode !== 'form' || meta.codex_approval_kind
|
|
128
|
+
|| meta['openai/elicitation'] || meta['openai/confirmation'] || schema?.type !== 'object'
|
|
129
|
+
|| ['browser', 'browser-use'].includes(p.serverName)
|
|
130
|
+
|| !schema.properties || typeof schema.properties !== 'object' || Array.isArray(schema.properties)
|
|
131
|
+
|| Object.keys(schema).some(key => !['$schema', 'type', 'properties', 'required'].includes(key))) return null;
|
|
132
|
+
const known = ['type', 'title', 'description', 'default', 'minLength', 'maxLength', 'format', 'minimum', 'maximum', 'enum', 'enumNames', 'oneOf', 'items', 'minItems', 'maxItems'];
|
|
133
|
+
for (const field of Object.values(schema.properties)) {
|
|
134
|
+
if (!field || Object.keys(field).some(key => !known.includes(key))
|
|
135
|
+
|| !['string', 'number', 'integer', 'boolean', 'array'].includes(field.type)) return null;
|
|
136
|
+
const options = fieldOptions(field);
|
|
137
|
+
if ((field.type === 'array' || field.enum != null || field.oneOf != null) && !options
|
|
138
|
+
|| options && (!options.length || options.some(option => typeof option.value !== 'string' || typeof option.label !== 'string'))) return null;
|
|
139
|
+
if (field.format && !['email', 'uri', 'date', 'date-time'].includes(field.format)) return null;
|
|
140
|
+
}
|
|
141
|
+
if (schema.required != null && (!Array.isArray(schema.required) || schema.required.some(key => !Object.hasOwn(schema.properties, key)))) return null;
|
|
142
|
+
return schema;
|
|
143
|
+
}
|
|
144
|
+
function validateForm(schema, values) {
|
|
145
|
+
if (!values || typeof values !== 'object' || Array.isArray(values)
|
|
146
|
+
|| Object.keys(values).some(key => !Object.hasOwn(schema.properties, key))) throw new Error('表单内容无效');
|
|
147
|
+
for (const [key, field] of Object.entries(schema.properties)) {
|
|
148
|
+
const value = values[key], label = field.title || key;
|
|
149
|
+
if (value === undefined) { if (schema.required?.includes(key)) throw new Error(`请填写${label}`); continue; }
|
|
150
|
+
let valid = field.type === 'array' ? Array.isArray(value) : typeof value === (field.type === 'integer' ? 'number' : field.type);
|
|
151
|
+
if (typeof value === 'number') valid &&= Number.isFinite(value) && (field.type !== 'integer' || Number.isInteger(value))
|
|
152
|
+
&& (field.minimum == null || value >= field.minimum) && (field.maximum == null || value <= field.maximum);
|
|
153
|
+
if (typeof value === 'string') {
|
|
154
|
+
valid &&= (field.minLength == null || [...value].length >= field.minLength) && (field.maxLength == null || [...value].length <= field.maxLength);
|
|
155
|
+
if (field.format === 'email') valid &&= /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
156
|
+
if (field.format === 'uri') { try { new URL(value); } catch { valid = false; } }
|
|
157
|
+
if (field.format === 'date') valid &&= /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(value));
|
|
158
|
+
if (field.format === 'date-time') valid &&= /^\d{4}-\d{2}-\d{2}T.+(?:Z|[+-]\d{2}:\d{2})$/.test(value) && !Number.isNaN(Date.parse(value));
|
|
159
|
+
}
|
|
160
|
+
const options = fieldOptions(field);
|
|
161
|
+
if (Array.isArray(value)) valid &&= new Set(value).size === value.length && (field.minItems == null || value.length >= field.minItems)
|
|
162
|
+
&& (field.maxItems == null || value.length <= field.maxItems) && value.every(item => options?.some(option => option.value === item));
|
|
163
|
+
else if (options) valid &&= options.some(option => option.value === value);
|
|
164
|
+
if (!valid) throw new Error(`${label}的内容不符合请求要求`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function permissionResult(request, decision) {
|
|
168
|
+
return { permissions: decision === 'decline' ? {} : request.params.permissions, scope: decision === 'acceptForSession' ? 'session' : 'turn' };
|
|
169
|
+
}
|
|
170
|
+
function blocksComposer(request) {
|
|
171
|
+
if (externalAction(request)) return false;
|
|
172
|
+
return request.method === input || Boolean(formSchema(request))
|
|
173
|
+
|| request.method !== elicitation && decisions(request).length > 0
|
|
174
|
+
|| request.method === elicitation && decisions(request).some(value => value !== 'cancel');
|
|
175
|
+
}
|
|
176
|
+
function decisions(request) {
|
|
177
|
+
if (confirmation(request)) return ['decline', 'accept'];
|
|
178
|
+
if (request.method === command) return request.params.availableDecisions ?? ['accept', 'acceptForSession', 'decline', 'cancel'];
|
|
179
|
+
if (request.method === file) return ['accept', 'acceptForSession', 'decline', 'cancel'];
|
|
180
|
+
if (request.method === permissions && request.params.permissions) return ['accept', 'acceptForSession', 'decline'];
|
|
181
|
+
if (externalAction(request)) return ['decline', 'accept'];
|
|
182
|
+
const action = browserAction(request);
|
|
183
|
+
if (action) return ['allowOnce', ...(action.session ? ['allowSession'] : []), ...(action.persistent ? ['allowAlways'] : []), 'cancel'];
|
|
184
|
+
const app = computerAppAccess(request);
|
|
185
|
+
if (app) return ['decline', 'allowConversation', ...(app.persistent ? ['allowAlways'] : [])];
|
|
186
|
+
const browser = browserAccess(request);
|
|
187
|
+
if (browser) return browser.persistent ? ['allowOnce', 'allowSite', 'allowAllSites', 'decline'] : ['allowOnce', 'decline'];
|
|
188
|
+
const tool = mcpToolApproval(request);
|
|
189
|
+
if (tool) return ['decline', 'allowOnce', ...(tool.session ? ['allowSession'] : []), ...(tool.persistent ? ['allowAlways'] : [])];
|
|
190
|
+
return request.method === elicitation ? ['cancel'] : [];
|
|
191
|
+
}
|
|
192
|
+
function validate(request, result) {
|
|
193
|
+
if (request.method === elicitation && equal(result, elicitationResult('cancel'))) return;
|
|
194
|
+
if (externalAction(request)) {
|
|
195
|
+
if (!['accept', 'decline'].some(action => equal(result, elicitationResult(action)))) throw new Error('外部操作结果无效');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const schema = formSchema(request);
|
|
199
|
+
if (schema) {
|
|
200
|
+
if (equal(result, elicitationResult('decline'))) return;
|
|
201
|
+
if (result?.action !== 'accept' || result._meta !== null || Object.keys(result).length !== 3) throw new Error('表单响应格式无效');
|
|
202
|
+
validateForm(schema, result.content); return;
|
|
203
|
+
}
|
|
204
|
+
if (request.method === permissions) {
|
|
205
|
+
if (!decisions(request).some(decision => equal(result, permissionResult(request, decision)))) throw new Error('权限决定不属于当前请求');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (confirmation(request)) {
|
|
209
|
+
if (!decisions(request).some(decision => equal(confirmationResult(request, decision), result))) throw new Error('确认结果不属于当前请求');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if ([command, file].includes(request.method)) {
|
|
213
|
+
if (!result || Object.keys(result).length !== 1 || !decisions(request).some(value => equal(value, result.decision))) throw new Error('审批选项不属于当前请求');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (request.method === input) {
|
|
217
|
+
const questions = request.params.questions || [];
|
|
218
|
+
if (!result?.answers || Object.keys(result).length !== 1 || Object.keys(result.answers).length !== questions.length
|
|
219
|
+
|| questions.some(q => !Array.isArray(result.answers[q.id]?.answers) || result.answers[q.id].answers.some(a => typeof a !== 'string'))) throw new Error('回答格式无效');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (browserAction(request)) {
|
|
223
|
+
if (!decisions(request).some(decision => equal(browserActionResult(decision), result))) throw new Error('浏览器操作决定不属于当前请求');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (computerAppAccess(request)) {
|
|
227
|
+
if (!decisions(request).some(decision => equal(computerAppResult(request, decision), result))) throw new Error('应用访问决定不属于当前请求');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const browser = browserAccess(request);
|
|
231
|
+
if (browser) {
|
|
232
|
+
const allowed = [
|
|
233
|
+
{ action: 'accept', content: {}, _meta: null },
|
|
234
|
+
{ action: 'decline', content: null, _meta: null },
|
|
235
|
+
];
|
|
236
|
+
if (browser.persistent) allowed.push(
|
|
237
|
+
{ action: 'accept', content: {}, _meta: { persist: 'always' } },
|
|
238
|
+
{ action: 'accept', content: {}, _meta: { persist: 'always', browser_use_persistent_approval_scope: 'all-sites' } },
|
|
239
|
+
);
|
|
240
|
+
if (!allowed.some(choice => equal(choice, result))) throw new Error('网站访问决定不属于当前请求');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (mcpToolApproval(request)) {
|
|
244
|
+
if (!decisions(request).some(decision => equal(mcpToolResult(decision), result))) throw new Error('工具授权决定不属于当前请求');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
throw new Error('此类请求需在工作站处理');
|
|
248
|
+
}
|
|
249
|
+
class Store {
|
|
250
|
+
constructor(epoch, changed = () => {}) {
|
|
251
|
+
this.epoch = epoch; this.changed = changed; this.requests = new Map(); this.approvalHistory = new Map();
|
|
252
|
+
}
|
|
253
|
+
list(threadId) { return [...this.requests.values()].filter(r => r.params.threadId === threadId).map(r => ({ ...r })); }
|
|
254
|
+
history(threadId) { return [...this.approvalHistory.values()].filter(r => r.params.threadId === threadId).map(r => ({ ...r })); }
|
|
255
|
+
complete(request, status) {
|
|
256
|
+
const approval = this.approvalHistory.get(approvalId(request));
|
|
257
|
+
if (approval) { approval.status = status; approval.completedAtMs = Date.now(); }
|
|
258
|
+
}
|
|
259
|
+
receive(message) {
|
|
260
|
+
if (!['string', 'number'].includes(typeof message.id) || !message.params?.threadId || this.requests.has(message.id)) return;
|
|
261
|
+
const request = { id: message.id, method: message.method, params: message.params, epoch: this.epoch, status: 'pending' };
|
|
262
|
+
if (request.method !== input) this.approvalHistory.set(approvalId(request), {
|
|
263
|
+
type: 'approval', id: approvalId(request), requestId: request.id, method: request.method,
|
|
264
|
+
params: request.params, turnId: request.params.turnId, status: 'pending', receivedAtMs: Date.now(),
|
|
265
|
+
});
|
|
266
|
+
this.requests.set(message.id, request); this.changed(request.params.threadId);
|
|
267
|
+
}
|
|
268
|
+
notification(method, params) {
|
|
269
|
+
if (method === 'serverRequest/resolved') {
|
|
270
|
+
const request = this.requests.get(params.requestId);
|
|
271
|
+
if (request?.params.threadId === params.threadId) {
|
|
272
|
+
this.complete(request, 'resolved'); this.requests.delete(params.requestId); this.changed(params.threadId);
|
|
273
|
+
}
|
|
274
|
+
} else if (method === 'turn/completed' || method === 'thread/closed') {
|
|
275
|
+
for (const [id, r] of this.requests) if (r.params.threadId === params.threadId && (method === 'thread/closed' || r.params.turnId === params.turn?.id)) {
|
|
276
|
+
this.complete(r, 'expired'); this.requests.delete(id);
|
|
277
|
+
}
|
|
278
|
+
this.changed(params.threadId);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
forget(threadId) {
|
|
282
|
+
for (const [id, request] of this.requests) if (request.params.threadId === threadId) this.requests.delete(id);
|
|
283
|
+
for (const [id, approval] of this.approvalHistory) if (approval.params.threadId === threadId) this.approvalHistory.delete(id);
|
|
284
|
+
this.changed(threadId);
|
|
285
|
+
}
|
|
286
|
+
reset(epoch) {
|
|
287
|
+
const threads = new Set([...this.requests.values()].map(r => r.params.threadId));
|
|
288
|
+
for (const request of this.requests.values()) this.complete(request, 'expired');
|
|
289
|
+
this.requests.clear(); this.epoch = epoch; for (const id of threads) this.changed(id);
|
|
290
|
+
this.approvalHistory.clear();
|
|
291
|
+
}
|
|
292
|
+
async respond(threadId, { id, epoch, result }, send) {
|
|
293
|
+
const request = this.requests.get(id);
|
|
294
|
+
if (!request || epoch !== this.epoch || request.params.threadId !== threadId) throw new Error('审批已处理或连接已变化,请等待同步');
|
|
295
|
+
if (request.status !== 'pending') throw new Error('决定已提交,正在等待工作站确认,请勿重复提交');
|
|
296
|
+
validate(request, result);
|
|
297
|
+
const approval = this.approvalHistory.get(approvalId(request));
|
|
298
|
+
if (approval) { approval.status = 'submitted'; approval.result = result; }
|
|
299
|
+
request.status = 'submitted'; this.changed(threadId);
|
|
300
|
+
try { await send({ id: request.id, result }); }
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (this.requests.get(id) === request) {
|
|
303
|
+
request.status = 'unknown'; if (approval) approval.status = 'unknown'; this.changed(threadId);
|
|
304
|
+
}
|
|
305
|
+
throw new Error('审批发送结果未确认,请检查工作站;不会自动重发。' + error.message);
|
|
306
|
+
}
|
|
307
|
+
return { submitted: true };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return { Store, decisions, validate, command, file, input, elicitation, permissions, externalAction, elicitationResult, formSchema, fieldOptions, validateForm, permissionResult, blocksComposer, browserAccess, browserAction, browserActionResult, computerAppAccess, computerAppResult, mcpToolApproval, mcpToolResult, confirmation, confirmationResult };
|
|
311
|
+
});
|