@sanqianx/claude-ai-workbench 0.13.2 → 0.13.3
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/LICENSE +201 -201
- package/README.md +156 -156
- package/apps/agent-terminal/app.css +636 -613
- package/apps/agent-terminal/app.js +125 -65
- package/apps/agent-terminal/conversation-composer.js +44 -0
- package/apps/agent-terminal/index.html +171 -170
- package/apps/agent-terminal/panel-host-proof.html +152 -152
- package/apps/agent-terminal/panel-proof-content.html +41 -41
- package/apps/agent-terminal/right-workspace-dock.css +187 -159
- package/apps/agent-terminal/right-workspace-dock.js +623 -519
- package/apps/ai-profile-settings-host/index.html +25 -25
- package/apps/ai-profile-settings-host/page.css +1 -1
- package/apps/ai-profile-settings-host/page.js +113 -113
- package/apps/minimal-test-host/README.md +15 -15
- package/apps/minimal-test-host/host.css +2 -2
- package/apps/minimal-test-host/host.js +35 -35
- package/apps/minimal-test-host/index.html +58 -58
- package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
- package/docs/sdk/FILE_LAYOUT.md +31 -31
- package/docs/sdk/INTEGRATION.md +126 -126
- package/docs/sdk/README.md +21 -21
- package/docs/sdk/SESSION_EVENTS.md +13 -13
- package/docs/sdk/openapi.yaml +8 -1
- package/package.json +1 -1
- package/packages/client/README.md +29 -29
- package/packages/client/index.d.ts +122 -122
- package/packages/client/index.js +260 -260
- package/packages/client/package.json +2 -2
- package/packages/contracts/README.md +9 -9
- package/packages/contracts/index.d.ts +519 -517
- package/packages/contracts/index.js +312 -312
- package/packages/contracts/package.json +1 -1
- package/packages/core/README.md +8 -8
- package/packages/core/index.d.ts +76 -76
- package/packages/core/index.js +222 -222
- package/packages/core/package.json +2 -2
- package/packages/server/README.md +17 -17
- package/packages/server/bin/agent-terminal-server.js +42 -42
- package/packages/server/bin/claude-workbench.js +225 -225
- package/packages/server/index.d.ts +171 -171
- package/packages/server/index.js +230 -230
- package/packages/server/lib/agent-discovery.js +133 -133
- package/packages/server/lib/agent-registry.js +143 -143
- package/packages/server/lib/agent-spawn-env.js +17 -17
- package/packages/server/lib/attachment-store.js +78 -78
- package/packages/server/lib/bridges/app-server-bridge.js +373 -373
- package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
- package/packages/server/lib/capture-settings-store.js +35 -35
- package/packages/server/lib/claude-history-recovery.js +87 -87
- package/packages/server/lib/cli-session-recovery.js +28 -28
- package/packages/server/lib/context-resolver.js +55 -55
- package/packages/server/lib/conversation-capture.js +51 -51
- package/packages/server/lib/delegation-phase-store.js +357 -357
- package/packages/server/lib/delegation-store.js +474 -468
- package/packages/server/lib/diagnostics.js +32 -32
- package/packages/server/lib/drivers/cli-json-runner.js +974 -974
- package/packages/server/lib/drivers/codex-driver.js +1442 -1442
- package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
- package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
- package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
- package/packages/server/lib/event-stream.js +138 -138
- package/packages/server/lib/folder-picker.js +623 -623
- package/packages/server/lib/http-handler.js +339 -339
- package/packages/server/lib/json-file.js +26 -26
- package/packages/server/lib/knowledge-context.js +33 -33
- package/packages/server/lib/model-detection.js +295 -295
- package/packages/server/lib/parent-context.js +44 -0
- package/packages/server/lib/profile-store.js +283 -283
- package/packages/server/lib/project-store.js +102 -102
- package/packages/server/lib/runtime-loader.js +120 -120
- package/packages/server/lib/session-archive-store.js +53 -53
- package/packages/server/lib/session-history.js +612 -612
- package/packages/server/lib/system-doctor.js +125 -125
- package/packages/server/lib/ui-preferences-store.js +48 -48
- package/packages/server/lib/workbench-service.js +1696 -1675
- package/packages/server/package.json +2 -2
- package/packages/ui/README.md +30 -30
- package/packages/ui/ai-profile-settings.d.ts +45 -45
- package/packages/ui/ai-profile-settings.js +283 -283
- package/packages/ui/claude-terminal.d.ts +31 -31
- package/packages/ui/claude-terminal.js +343 -343
- package/packages/ui/index.d.ts +18 -18
- package/packages/ui/index.js +51 -51
- package/packages/ui/package.json +2 -2
- package/packages/ui/theme.css +13 -13
- package/src/assets/claude-prompts.json +21 -21
- package/src/backend/http/ai-profile-routes.js +61 -61
- package/src/backend/http/claude-routes.js +170 -170
- package/src/backend/http/node-helpers.js +45 -45
- package/src/backend/index.js +21 -21
- package/src/backend/lib/ai-adapter.js +144 -144
- package/src/backend/lib/ai-profile-store.js +259 -259
- package/src/backend/lib/ai-vendor-presets.js +83 -83
- package/src/backend/lib/ai-workspace.js +140 -140
- package/src/backend/lib/automation-config.js +253 -253
- package/src/backend/lib/claude-cli-runner.js +2392 -2392
- package/src/backend/lib/data-dir.js +169 -169
- package/src/backend/lib/llm-client.js +176 -176
- package/src/backend/lib/model-context-windows.js +82 -82
- package/src/backend/lib/native-task-control.js +217 -217
- package/src/backend/lib/native-task-history-index.js +475 -475
- package/src/backend/lib/native-task-history-reader.js +526 -526
- package/src/backend/lib/native-task-messages.js +457 -457
- package/src/backend/lib/native-task-registry.js +325 -325
- package/src/backend/lib/native-task-runtime.js +242 -242
- package/src/backend/lib/native-task-trace.js +440 -440
- package/src/backend/lib/native-task-transcript.js +393 -393
- package/src/backend/lib/prompt-registry.js +166 -166
- package/src/backend/lib/runtime-config.js +121 -121
- package/src/backend/lib/sdk-session-stream.js +143 -143
- package/src/backend/lib/sdk-turn-stream.js +16 -16
- package/src/backend/lib/store-scope.js +131 -131
- package/src/backend/lib/task-identity.js +104 -104
- package/src/backend/lib/task-state-projection.js +58 -58
- package/src/index.js +7 -7
- package/src/sdk-wave-a.ts +1 -1
- package/src/sdk-wave-b.ts +1 -1
- package/src/sdk-wave-c.ts +1 -1
- package/src/sdk-wave-d.ts +1 -1
- package/src/sdk-wave-e.ts +1 -1
|
@@ -1,312 +1,312 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const API_VERSION = 'v1';
|
|
4
|
-
const DEFAULT_API_PREFIX = `/api/claude-workbench/${API_VERSION}`;
|
|
5
|
-
const RUNTIME = 'claude-code';
|
|
6
|
-
const DEFAULT_AGENT = 'claude-code';
|
|
7
|
-
const EFFORT_LEVELS = Object.freeze(['low', 'medium', 'high', 'max']);
|
|
8
|
-
const PERMISSION_MODES = Object.freeze(['default', 'acceptEdits', 'plan', 'bypassPermissions']);
|
|
9
|
-
// Delegated-session identity split: a delegation command is a Workbench
|
|
10
|
-
// server command (basis), never an SDK run or a claimed SDK tool causality.
|
|
11
|
-
const DELEGATED_SESSION_KIND = 'delegated-session';
|
|
12
|
-
const DELEGATION_COMMAND_SOURCE = 'workbench-delegation-command';
|
|
13
|
-
const DELEGATION_COMMAND_BASIS = 'server-command';
|
|
14
|
-
const DELEGATION_PHASES = Object.freeze([
|
|
15
|
-
'reserved', 'child-created', 'child-create-failed', 'dispatching', 'dispatched', 'dispatch-unknown',
|
|
16
|
-
]);
|
|
17
|
-
const DELEGATION_OUTCOMES = Object.freeze(['not-started', 'not-dispatched', 'pending', 'dispatched', 'unknown']);
|
|
18
|
-
const IMAGE_MEDIA_TYPES = Object.freeze(['image/jpeg', 'image/png', 'image/gif', 'image/webp']);
|
|
19
|
-
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
20
|
-
const MAX_IMAGES_PER_MESSAGE = 20;
|
|
21
|
-
const MAX_IMAGE_MESSAGE_BYTES = 20 * 1024 * 1024;
|
|
22
|
-
const TEXT_MEDIA_TYPES = Object.freeze(['text/plain', 'text/markdown', 'application/json', 'text/csv']);
|
|
23
|
-
const TEXT_FILE_TYPES = Object.freeze({ '.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json', '.csv': 'text/csv' });
|
|
24
|
-
const MAX_TEXT_BYTES = 512 * 1024;
|
|
25
|
-
const MAX_ATTACHMENTS_PER_MESSAGE = MAX_IMAGES_PER_MESSAGE;
|
|
26
|
-
const MAX_ATTACHMENT_MESSAGE_BYTES = MAX_IMAGE_MESSAGE_BYTES;
|
|
27
|
-
|
|
28
|
-
// Capability matrix for every agent the terminal knows about. Drivers must
|
|
29
|
-
// declare the same shape; the UI hides controls the active agent does not
|
|
30
|
-
// support instead of failing.
|
|
31
|
-
const AGENTS = Object.freeze({
|
|
32
|
-
'claude-code': Object.freeze({
|
|
33
|
-
id: 'claude-code', label: 'Claude Code', transport: 'agent-sdk',
|
|
34
|
-
authModes: Object.freeze(['profile-injected']),
|
|
35
|
-
capabilities: Object.freeze({
|
|
36
|
-
thinking: true, permissions: true, toolStream: true, images: true, backgroundTasks: true,
|
|
37
|
-
resume: true, modelOverride: true, generationParams: Object.freeze(['effort']),
|
|
38
|
-
}),
|
|
39
|
-
}),
|
|
40
|
-
codex: Object.freeze({
|
|
41
|
-
id: 'codex', label: 'Codex', transport: 'cli-json',
|
|
42
|
-
authModes: Object.freeze(['agent-owned', 'profile-injected']),
|
|
43
|
-
capabilities: Object.freeze({
|
|
44
|
-
thinking: false,
|
|
45
|
-
backgroundTasks: false,
|
|
46
|
-
// Permission modes map to exec sandboxes (read-only/workspace-write/danger).
|
|
47
|
-
permissions: true,
|
|
48
|
-
toolStream: true, images: true,
|
|
49
|
-
resume: true, modelOverride: true,
|
|
50
|
-
// The driver maps effort to `model_reasoning_effort` (low/medium/high/xhigh).
|
|
51
|
-
generationParams: Object.freeze(['effort']),
|
|
52
|
-
}),
|
|
53
|
-
}),
|
|
54
|
-
opencode: Object.freeze({
|
|
55
|
-
id: 'opencode', label: 'OpenCode', transport: 'cli',
|
|
56
|
-
authModes: Object.freeze(['agent-owned', 'profile-injected']),
|
|
57
|
-
capabilities: Object.freeze({
|
|
58
|
-
// The CLI streams reasoning parts; the driver maps them to thinking events.
|
|
59
|
-
thinking: true,
|
|
60
|
-
backgroundTasks: false,
|
|
61
|
-
permissions: true, toolStream: true, images: true,
|
|
62
|
-
resume: true, modelOverride: true, generationParams: Object.freeze([]),
|
|
63
|
-
}),
|
|
64
|
-
}),
|
|
65
|
-
zcode: Object.freeze({
|
|
66
|
-
id: 'zcode', label: 'ZCode', transport: 'cli',
|
|
67
|
-
authModes: Object.freeze(['agent-owned', 'profile-injected']),
|
|
68
|
-
capabilities: Object.freeze({
|
|
69
|
-
// The bundled zcode CLI (app-server --stdio) exists, but the terminal has
|
|
70
|
-
// no bridge driver yet; every capability stays off until one ships.
|
|
71
|
-
thinking: false, permissions: false, toolStream: false, images: false,
|
|
72
|
-
backgroundTasks: false,
|
|
73
|
-
resume: false, modelOverride: false, generationParams: Object.freeze([]),
|
|
74
|
-
}),
|
|
75
|
-
}),
|
|
76
|
-
});
|
|
77
|
-
const AGENT_IDS = Object.freeze(Object.keys(AGENTS));
|
|
78
|
-
|
|
79
|
-
const EVENT_TYPES = Object.freeze([
|
|
80
|
-
'workbench/stream-open', 'workbench/stream-reconnecting', 'workbench/resync',
|
|
81
|
-
'claude/init', 'claude/state', 'claude/session-ready', 'claude/user-prompt',
|
|
82
|
-
'claude/system-prompt', 'claude/text-start', 'claude/text-delta', 'claude/result',
|
|
83
|
-
'claude/thinking-start', 'claude/thinking-delta', 'claude/tool-use', 'claude/tool-result',
|
|
84
|
-
'claude/permission-request', 'claude/permission-resolved', 'claude/usage',
|
|
85
|
-
'claude/commands', 'claude/image', 'claude/retry', 'claude/error', 'claude/stderr', 'claude/aborted',
|
|
86
|
-
'claude/selection-changed',
|
|
87
|
-
'claude/turn-end', 'claude/task-started', 'claude/task-progress', 'claude/task-updated', 'claude/task-notification',
|
|
88
|
-
'claude/native-trace', 'claude/native-task-state', 'claude/native-task-transcript', 'claude/task-interrupted',
|
|
89
|
-
'workbench/sessions-snapshot', 'workbench/session-changed',
|
|
90
|
-
]);
|
|
91
|
-
|
|
92
|
-
function cleanId(value, field = 'id') {
|
|
93
|
-
const id = String(value || '').trim();
|
|
94
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/.test(id)) {
|
|
95
|
-
throw Object.assign(new Error(`${field} is invalid`), { status: 400 });
|
|
96
|
-
}
|
|
97
|
-
return id;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function normalizeContext(input = {}) {
|
|
101
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
102
|
-
throw Object.assign(new Error('context must be an object'), { status: 400 });
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
contextId: cleanId(input.contextId, 'contextId'),
|
|
106
|
-
contextType: input.contextType ? String(input.contextType).slice(0, 64) : undefined,
|
|
107
|
-
displayName: input.displayName ? String(input.displayName).slice(0, 256) : undefined,
|
|
108
|
-
workspaceRef: input.workspaceRef ? cleanId(input.workspaceRef, 'workspaceRef') : undefined,
|
|
109
|
-
aiProfileId: input.aiProfileId === null ? null : input.aiProfileId ? cleanId(input.aiProfileId, 'aiProfileId') : undefined,
|
|
110
|
-
model: input.model === null ? null : input.model ? String(input.model).trim().slice(0, 128) : undefined,
|
|
111
|
-
metadata: input.metadata && typeof input.metadata === 'object' && !Array.isArray(input.metadata)
|
|
112
|
-
? input.metadata : undefined,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function normalizeAgentId(value) {
|
|
117
|
-
const id = String(value || DEFAULT_AGENT).trim();
|
|
118
|
-
if (!AGENTS[id]) throw Object.assign(new Error(`unknown agent: ${id}`), { status: 400 });
|
|
119
|
-
return id;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function normalizeEffort(value) {
|
|
123
|
-
if (value == null || value === '') return undefined;
|
|
124
|
-
const effort = String(value).trim();
|
|
125
|
-
if (!EFFORT_LEVELS.includes(effort)) {
|
|
126
|
-
throw Object.assign(new Error(`effort must be one of ${EFFORT_LEVELS.join(', ')}`), { status: 400 });
|
|
127
|
-
}
|
|
128
|
-
return effort;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function isAbsoluteLikePath(value) {
|
|
132
|
-
return /^([a-zA-Z]:[\\/]|\\\\|\/)/.test(String(value || ''));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function normalizeProject(input = {}, idOverride) {
|
|
136
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
137
|
-
throw Object.assign(new Error('project must be an object'), { status: 400 });
|
|
138
|
-
}
|
|
139
|
-
const id = cleanId(idOverride || input.id, 'projectId');
|
|
140
|
-
const name = String(input.name || '').trim().slice(0, 128);
|
|
141
|
-
if (!name) throw Object.assign(new Error('project name is required'), { status: 400 });
|
|
142
|
-
const rootPath = String(input.rootPath || '').trim();
|
|
143
|
-
if (!isAbsoluteLikePath(rootPath)) {
|
|
144
|
-
throw Object.assign(new Error('project rootPath must be an absolute local directory'), { status: 400 });
|
|
145
|
-
}
|
|
146
|
-
return {
|
|
147
|
-
id, name, rootPath,
|
|
148
|
-
...(input.defaultAiProfileId !== undefined ? { defaultAiProfileId: input.defaultAiProfileId === null ? null : cleanId(input.defaultAiProfileId, 'defaultAiProfileId') } : {}),
|
|
149
|
-
...(input.defaultModel !== undefined ? { defaultModel: input.defaultModel === null ? null : String(input.defaultModel).trim().slice(0, 128) || null } : {}),
|
|
150
|
-
...(input.defaultAgentId !== undefined ? { defaultAgentId: normalizeAgentId(input.defaultAgentId) } : {}),
|
|
151
|
-
createdAt: input.createdAt || new Date().toISOString(),
|
|
152
|
-
updatedAt: input.updatedAt || input.createdAt || new Date().toISOString(),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function normalizeModelEntries(input) {
|
|
157
|
-
if (input == null) return [];
|
|
158
|
-
if (!Array.isArray(input)) throw Object.assign(new Error('models.list must be an array'), { status: 400 });
|
|
159
|
-
if (input.length > 100) throw Object.assign(new Error('models.list exceeds 100 entries'), { status: 400 });
|
|
160
|
-
const seen = new Set();
|
|
161
|
-
const entries = [];
|
|
162
|
-
for (const item of input) {
|
|
163
|
-
const raw = item && typeof item === 'object' && !Array.isArray(item) ? item : { id: item };
|
|
164
|
-
const id = String(raw.id || raw.model || '').trim().slice(0, 128);
|
|
165
|
-
if (!id) throw Object.assign(new Error('model entry id is required'), { status: 400 });
|
|
166
|
-
if (seen.has(id)) continue;
|
|
167
|
-
seen.add(id);
|
|
168
|
-
const tags = Array.isArray(raw.tags)
|
|
169
|
-
? raw.tags.map(tag => String(tag).trim().slice(0, 32)).filter(Boolean).slice(0, 8)
|
|
170
|
-
: [];
|
|
171
|
-
// Detected context windows ride along so drivers (opencode limit.context)
|
|
172
|
-
// and the capacity bubble can use them; invalid values are dropped.
|
|
173
|
-
const contextWindow = Number(raw.contextWindow);
|
|
174
|
-
const limits = Number.isFinite(contextWindow) && contextWindow > 0 && contextWindow <= 10_000_000
|
|
175
|
-
? { contextWindow: Math.floor(contextWindow) }
|
|
176
|
-
: {};
|
|
177
|
-
entries.push({ id, openaiId: String(raw.openaiId || id.replace(/\[1m\]$/i, '')).trim().slice(0, 128), label: String(raw.label || id).slice(0, 128), tags, ...limits });
|
|
178
|
-
}
|
|
179
|
-
return entries;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function normalizeProfile(input = {}, idOverride) {
|
|
183
|
-
const id = cleanId(idOverride || input.id, 'profileId');
|
|
184
|
-
const models = input.models && typeof input.models === 'object' ? input.models : {};
|
|
185
|
-
const list = normalizeModelEntries(models.list);
|
|
186
|
-
const fallbackModel = list.length ? list[0].id : '';
|
|
187
|
-
const defaultModel = String(models.default || input.mainModel || input.model || fallbackModel).trim();
|
|
188
|
-
return {
|
|
189
|
-
id,
|
|
190
|
-
name: String(input.name || id).trim().slice(0, 256),
|
|
191
|
-
provider: String(input.provider || '').trim().slice(0, 128),
|
|
192
|
-
protocol: String(input.protocol || 'anthropic-compatible').trim(),
|
|
193
|
-
runtime: RUNTIME,
|
|
194
|
-
baseUrl: String(input.baseUrl || '').trim().replace(/\/+$/, ''),
|
|
195
|
-
openaiBaseUrl: resolveOpenAIBaseUrl(input),
|
|
196
|
-
credentialRef: String(input.credentialRef || input.secretId || `credential:${id}`).trim(),
|
|
197
|
-
models: {
|
|
198
|
-
default: defaultModel,
|
|
199
|
-
fast: String(models.fast || defaultModel).trim(),
|
|
200
|
-
reasoning: String(models.reasoning || defaultModel).trim(),
|
|
201
|
-
coding: String(models.coding || defaultModel).trim(),
|
|
202
|
-
largeContext: String(models.largeContext || defaultModel).trim(),
|
|
203
|
-
list,
|
|
204
|
-
},
|
|
205
|
-
contextWindow: positiveInt(input.contextWindow, 200000),
|
|
206
|
-
timeoutMs: positiveInt(input.timeoutMs, 300000),
|
|
207
|
-
permissionMode: PERMISSION_MODES.includes(input.permissionMode) ? input.permissionMode : 'default',
|
|
208
|
-
systemPrompt: normalizeSystemPrompt(input.systemPrompt),
|
|
209
|
-
enabled: input.enabled !== false,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function positiveInt(value, fallback) {
|
|
214
|
-
const parsed = Number(value);
|
|
215
|
-
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function resolveOpenAIBaseUrl(profile = {}) {
|
|
219
|
-
const explicit = String(profile.openaiBaseUrl || '').trim().replace(/\/+$/, '');
|
|
220
|
-
if (explicit) return explicit;
|
|
221
|
-
const baseUrl = String(profile.baseUrl || '').trim().replace(/\/+$/, '');
|
|
222
|
-
if (profile.protocol === 'openai-compatible') return baseUrl;
|
|
223
|
-
let hostname = '';
|
|
224
|
-
try { hostname = new URL(baseUrl).hostname; } catch {}
|
|
225
|
-
if (hostname === 'api.minimax.io') return 'https://api.minimax.io/v1';
|
|
226
|
-
if (hostname === 'api.minimaxi.com' || /minimax/i.test(String(profile.provider || ''))) return 'https://api.minimaxi.com/v1';
|
|
227
|
-
return '';
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function resolveOpenAIModel(profile = {}, model) {
|
|
231
|
-
const id = String(model || profile.mainModel || profile.models && profile.models.default || '').trim();
|
|
232
|
-
const entries = profile.models && profile.models.list;
|
|
233
|
-
const entry = Array.isArray(entries) && entries.find(item => item.id === id);
|
|
234
|
-
return String(entry && entry.openaiId || id.replace(/\[1m\]$/i, '')).trim();
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function normalizeAttachments(input) {
|
|
238
|
-
if (input == null) return [];
|
|
239
|
-
if (!Array.isArray(input)) throw Object.assign(new Error('attachments must be an array'), { status: 400 });
|
|
240
|
-
if (input.length > MAX_ATTACHMENTS_PER_MESSAGE) throw Object.assign(new Error(`at most ${MAX_ATTACHMENTS_PER_MESSAGE} attachments are allowed`), { status: 413 });
|
|
241
|
-
let totalBytes = 0;
|
|
242
|
-
return input.map((item, index) => {
|
|
243
|
-
if (!item || typeof item !== 'object' || Array.isArray(item)) throw Object.assign(new Error(`attachment ${index + 1} is invalid`), { status: 400 });
|
|
244
|
-
const name = String(item.name || `image-${index + 1}`).replace(/[\u0000-\u001f]/g, '').slice(0, 255);
|
|
245
|
-
let mediaType = String(item.mediaType || item.mimeType || '').toLowerCase().split(';')[0].trim();
|
|
246
|
-
const isImage = IMAGE_MEDIA_TYPES.includes(mediaType);
|
|
247
|
-
if (!isImage) {
|
|
248
|
-
const extension = (name.match(/\.[^.\\/]+$/) || [''])[0].toLowerCase();
|
|
249
|
-
const textType = TEXT_FILE_TYPES[extension];
|
|
250
|
-
if (!textType || !['', 'application/octet-stream', 'text/plain', textType].includes(mediaType)) {
|
|
251
|
-
throw Object.assign(new Error(`attachment ${index + 1} has an unsupported type; use images or .txt, .md, .json, .csv files`), { status: 415 });
|
|
252
|
-
}
|
|
253
|
-
mediaType = textType;
|
|
254
|
-
}
|
|
255
|
-
const data = String(item.data || '').replace(/^data:[^;]+;base64,/, '').replace(/\s+/g, '');
|
|
256
|
-
if (!data || !/^[a-zA-Z0-9+/]+={0,2}$/.test(data) || data.length % 4 === 1 || (data.includes('=') && data.length % 4 !== 0)) throw Object.assign(new Error(`attachment ${index + 1} has invalid base64 data`), { status: 400 });
|
|
257
|
-
const size = Math.floor((data.length * 3) / 4) - (data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0);
|
|
258
|
-
const maxBytes = isImage ? MAX_IMAGE_BYTES : MAX_TEXT_BYTES;
|
|
259
|
-
if (size <= 0 || size > maxBytes) throw Object.assign(new Error(`attachment ${index + 1} exceeds ${maxBytes} bytes`), { status: 413 });
|
|
260
|
-
const bytes = Buffer.from(data, 'base64');
|
|
261
|
-
if (bytes.toString('base64').replace(/=+$/, '') !== data.replace(/=+$/, '')) throw Object.assign(new Error(`attachment ${index + 1} has invalid base64 data`), { status: 400 });
|
|
262
|
-
if (!isImage) {
|
|
263
|
-
try {
|
|
264
|
-
const content = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
265
|
-
if (content.includes('\u0000')) throw new Error('binary content');
|
|
266
|
-
} catch { throw Object.assign(new Error(`attachment ${index + 1} must contain UTF-8 text`), { status: 415 }); }
|
|
267
|
-
}
|
|
268
|
-
totalBytes += size;
|
|
269
|
-
if (totalBytes > MAX_ATTACHMENT_MESSAGE_BYTES) throw Object.assign(new Error(`attachment payload exceeds ${MAX_ATTACHMENT_MESSAGE_BYTES} bytes`), { status: 413 });
|
|
270
|
-
return {
|
|
271
|
-
id: item.id ? cleanId(item.id, 'attachmentId') : undefined,
|
|
272
|
-
name,
|
|
273
|
-
mediaType,
|
|
274
|
-
size,
|
|
275
|
-
width: positiveInt(item.width, 0) || undefined,
|
|
276
|
-
height: positiveInt(item.height, 0) || undefined,
|
|
277
|
-
data,
|
|
278
|
-
};
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function normalizeImageAttachments(input) {
|
|
283
|
-
const attachments = normalizeAttachments(input);
|
|
284
|
-
if (attachments.some(item => !IMAGE_MEDIA_TYPES.includes(item.mediaType))) throw Object.assign(new Error('unsupported image type'), { status: 415 });
|
|
285
|
-
return attachments;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function appendTextAttachments(text, attachments = []) {
|
|
289
|
-
const blocks = attachments.filter(item => TEXT_MEDIA_TYPES.includes(item.mediaType)).map(item => {
|
|
290
|
-
const name = String(item.name).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
291
|
-
return `<attachment name="${name}">\n${Buffer.from(item.data, 'base64').toString('utf8')}\n</attachment>`;
|
|
292
|
-
});
|
|
293
|
-
return [String(text || ''), ...blocks].filter(Boolean).join('\n\n');
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function normalizeSystemPrompt(value) {
|
|
297
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
|
298
|
-
if (typeof value === 'string' && value.trim()) return { type: 'custom', content: value };
|
|
299
|
-
return { type: 'preset', preset: 'claude_code', content: '' };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
module.exports = {
|
|
303
|
-
API_VERSION, DEFAULT_API_PREFIX, RUNTIME, DEFAULT_AGENT, AGENTS, AGENT_IDS, EFFORT_LEVELS,
|
|
304
|
-
PERMISSION_MODES, IMAGE_MEDIA_TYPES, TEXT_MEDIA_TYPES, TEXT_FILE_TYPES,
|
|
305
|
-
MAX_IMAGE_BYTES, MAX_IMAGES_PER_MESSAGE, MAX_IMAGE_MESSAGE_BYTES, EVENT_TYPES,
|
|
306
|
-
MAX_TEXT_BYTES, MAX_ATTACHMENTS_PER_MESSAGE, MAX_ATTACHMENT_MESSAGE_BYTES,
|
|
307
|
-
DELEGATED_SESSION_KIND, DELEGATION_COMMAND_SOURCE, DELEGATION_COMMAND_BASIS,
|
|
308
|
-
DELEGATION_PHASES, DELEGATION_OUTCOMES,
|
|
309
|
-
cleanId, normalizeContext, normalizeProfile, normalizeProject, normalizeAgentId, normalizeEffort,
|
|
310
|
-
normalizeModelEntries, normalizeImageAttachments, normalizeAttachments, appendTextAttachments,
|
|
311
|
-
resolveOpenAIBaseUrl, resolveOpenAIModel, positiveInt,
|
|
312
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const API_VERSION = 'v1';
|
|
4
|
+
const DEFAULT_API_PREFIX = `/api/claude-workbench/${API_VERSION}`;
|
|
5
|
+
const RUNTIME = 'claude-code';
|
|
6
|
+
const DEFAULT_AGENT = 'claude-code';
|
|
7
|
+
const EFFORT_LEVELS = Object.freeze(['low', 'medium', 'high', 'max']);
|
|
8
|
+
const PERMISSION_MODES = Object.freeze(['default', 'acceptEdits', 'plan', 'bypassPermissions']);
|
|
9
|
+
// Delegated-session identity split: a delegation command is a Workbench
|
|
10
|
+
// server command (basis), never an SDK run or a claimed SDK tool causality.
|
|
11
|
+
const DELEGATED_SESSION_KIND = 'delegated-session';
|
|
12
|
+
const DELEGATION_COMMAND_SOURCE = 'workbench-delegation-command';
|
|
13
|
+
const DELEGATION_COMMAND_BASIS = 'server-command';
|
|
14
|
+
const DELEGATION_PHASES = Object.freeze([
|
|
15
|
+
'reserved', 'child-created', 'child-create-failed', 'dispatching', 'dispatched', 'dispatch-unknown',
|
|
16
|
+
]);
|
|
17
|
+
const DELEGATION_OUTCOMES = Object.freeze(['not-started', 'not-dispatched', 'pending', 'dispatched', 'unknown']);
|
|
18
|
+
const IMAGE_MEDIA_TYPES = Object.freeze(['image/jpeg', 'image/png', 'image/gif', 'image/webp']);
|
|
19
|
+
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
20
|
+
const MAX_IMAGES_PER_MESSAGE = 20;
|
|
21
|
+
const MAX_IMAGE_MESSAGE_BYTES = 20 * 1024 * 1024;
|
|
22
|
+
const TEXT_MEDIA_TYPES = Object.freeze(['text/plain', 'text/markdown', 'application/json', 'text/csv']);
|
|
23
|
+
const TEXT_FILE_TYPES = Object.freeze({ '.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json', '.csv': 'text/csv' });
|
|
24
|
+
const MAX_TEXT_BYTES = 512 * 1024;
|
|
25
|
+
const MAX_ATTACHMENTS_PER_MESSAGE = MAX_IMAGES_PER_MESSAGE;
|
|
26
|
+
const MAX_ATTACHMENT_MESSAGE_BYTES = MAX_IMAGE_MESSAGE_BYTES;
|
|
27
|
+
|
|
28
|
+
// Capability matrix for every agent the terminal knows about. Drivers must
|
|
29
|
+
// declare the same shape; the UI hides controls the active agent does not
|
|
30
|
+
// support instead of failing.
|
|
31
|
+
const AGENTS = Object.freeze({
|
|
32
|
+
'claude-code': Object.freeze({
|
|
33
|
+
id: 'claude-code', label: 'Claude Code', transport: 'agent-sdk',
|
|
34
|
+
authModes: Object.freeze(['profile-injected']),
|
|
35
|
+
capabilities: Object.freeze({
|
|
36
|
+
thinking: true, permissions: true, toolStream: true, images: true, backgroundTasks: true,
|
|
37
|
+
resume: true, modelOverride: true, generationParams: Object.freeze(['effort']),
|
|
38
|
+
}),
|
|
39
|
+
}),
|
|
40
|
+
codex: Object.freeze({
|
|
41
|
+
id: 'codex', label: 'Codex', transport: 'cli-json',
|
|
42
|
+
authModes: Object.freeze(['agent-owned', 'profile-injected']),
|
|
43
|
+
capabilities: Object.freeze({
|
|
44
|
+
thinking: false,
|
|
45
|
+
backgroundTasks: false,
|
|
46
|
+
// Permission modes map to exec sandboxes (read-only/workspace-write/danger).
|
|
47
|
+
permissions: true,
|
|
48
|
+
toolStream: true, images: true,
|
|
49
|
+
resume: true, modelOverride: true,
|
|
50
|
+
// The driver maps effort to `model_reasoning_effort` (low/medium/high/xhigh).
|
|
51
|
+
generationParams: Object.freeze(['effort']),
|
|
52
|
+
}),
|
|
53
|
+
}),
|
|
54
|
+
opencode: Object.freeze({
|
|
55
|
+
id: 'opencode', label: 'OpenCode', transport: 'cli',
|
|
56
|
+
authModes: Object.freeze(['agent-owned', 'profile-injected']),
|
|
57
|
+
capabilities: Object.freeze({
|
|
58
|
+
// The CLI streams reasoning parts; the driver maps them to thinking events.
|
|
59
|
+
thinking: true,
|
|
60
|
+
backgroundTasks: false,
|
|
61
|
+
permissions: true, toolStream: true, images: true,
|
|
62
|
+
resume: true, modelOverride: true, generationParams: Object.freeze([]),
|
|
63
|
+
}),
|
|
64
|
+
}),
|
|
65
|
+
zcode: Object.freeze({
|
|
66
|
+
id: 'zcode', label: 'ZCode', transport: 'cli',
|
|
67
|
+
authModes: Object.freeze(['agent-owned', 'profile-injected']),
|
|
68
|
+
capabilities: Object.freeze({
|
|
69
|
+
// The bundled zcode CLI (app-server --stdio) exists, but the terminal has
|
|
70
|
+
// no bridge driver yet; every capability stays off until one ships.
|
|
71
|
+
thinking: false, permissions: false, toolStream: false, images: false,
|
|
72
|
+
backgroundTasks: false,
|
|
73
|
+
resume: false, modelOverride: false, generationParams: Object.freeze([]),
|
|
74
|
+
}),
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
const AGENT_IDS = Object.freeze(Object.keys(AGENTS));
|
|
78
|
+
|
|
79
|
+
const EVENT_TYPES = Object.freeze([
|
|
80
|
+
'workbench/stream-open', 'workbench/stream-reconnecting', 'workbench/resync',
|
|
81
|
+
'claude/init', 'claude/state', 'claude/session-ready', 'claude/user-prompt',
|
|
82
|
+
'claude/system-prompt', 'claude/text-start', 'claude/text-delta', 'claude/result',
|
|
83
|
+
'claude/thinking-start', 'claude/thinking-delta', 'claude/tool-use', 'claude/tool-result',
|
|
84
|
+
'claude/permission-request', 'claude/permission-resolved', 'claude/usage',
|
|
85
|
+
'claude/commands', 'claude/image', 'claude/retry', 'claude/error', 'claude/stderr', 'claude/aborted',
|
|
86
|
+
'claude/selection-changed',
|
|
87
|
+
'claude/turn-end', 'claude/task-started', 'claude/task-progress', 'claude/task-updated', 'claude/task-notification',
|
|
88
|
+
'claude/native-trace', 'claude/native-task-state', 'claude/native-task-transcript', 'claude/task-interrupted',
|
|
89
|
+
'workbench/sessions-snapshot', 'workbench/session-changed',
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
function cleanId(value, field = 'id') {
|
|
93
|
+
const id = String(value || '').trim();
|
|
94
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/.test(id)) {
|
|
95
|
+
throw Object.assign(new Error(`${field} is invalid`), { status: 400 });
|
|
96
|
+
}
|
|
97
|
+
return id;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeContext(input = {}) {
|
|
101
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
102
|
+
throw Object.assign(new Error('context must be an object'), { status: 400 });
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
contextId: cleanId(input.contextId, 'contextId'),
|
|
106
|
+
contextType: input.contextType ? String(input.contextType).slice(0, 64) : undefined,
|
|
107
|
+
displayName: input.displayName ? String(input.displayName).slice(0, 256) : undefined,
|
|
108
|
+
workspaceRef: input.workspaceRef ? cleanId(input.workspaceRef, 'workspaceRef') : undefined,
|
|
109
|
+
aiProfileId: input.aiProfileId === null ? null : input.aiProfileId ? cleanId(input.aiProfileId, 'aiProfileId') : undefined,
|
|
110
|
+
model: input.model === null ? null : input.model ? String(input.model).trim().slice(0, 128) : undefined,
|
|
111
|
+
metadata: input.metadata && typeof input.metadata === 'object' && !Array.isArray(input.metadata)
|
|
112
|
+
? input.metadata : undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeAgentId(value) {
|
|
117
|
+
const id = String(value || DEFAULT_AGENT).trim();
|
|
118
|
+
if (!AGENTS[id]) throw Object.assign(new Error(`unknown agent: ${id}`), { status: 400 });
|
|
119
|
+
return id;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeEffort(value) {
|
|
123
|
+
if (value == null || value === '') return undefined;
|
|
124
|
+
const effort = String(value).trim();
|
|
125
|
+
if (!EFFORT_LEVELS.includes(effort)) {
|
|
126
|
+
throw Object.assign(new Error(`effort must be one of ${EFFORT_LEVELS.join(', ')}`), { status: 400 });
|
|
127
|
+
}
|
|
128
|
+
return effort;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isAbsoluteLikePath(value) {
|
|
132
|
+
return /^([a-zA-Z]:[\\/]|\\\\|\/)/.test(String(value || ''));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeProject(input = {}, idOverride) {
|
|
136
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
137
|
+
throw Object.assign(new Error('project must be an object'), { status: 400 });
|
|
138
|
+
}
|
|
139
|
+
const id = cleanId(idOverride || input.id, 'projectId');
|
|
140
|
+
const name = String(input.name || '').trim().slice(0, 128);
|
|
141
|
+
if (!name) throw Object.assign(new Error('project name is required'), { status: 400 });
|
|
142
|
+
const rootPath = String(input.rootPath || '').trim();
|
|
143
|
+
if (!isAbsoluteLikePath(rootPath)) {
|
|
144
|
+
throw Object.assign(new Error('project rootPath must be an absolute local directory'), { status: 400 });
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
id, name, rootPath,
|
|
148
|
+
...(input.defaultAiProfileId !== undefined ? { defaultAiProfileId: input.defaultAiProfileId === null ? null : cleanId(input.defaultAiProfileId, 'defaultAiProfileId') } : {}),
|
|
149
|
+
...(input.defaultModel !== undefined ? { defaultModel: input.defaultModel === null ? null : String(input.defaultModel).trim().slice(0, 128) || null } : {}),
|
|
150
|
+
...(input.defaultAgentId !== undefined ? { defaultAgentId: normalizeAgentId(input.defaultAgentId) } : {}),
|
|
151
|
+
createdAt: input.createdAt || new Date().toISOString(),
|
|
152
|
+
updatedAt: input.updatedAt || input.createdAt || new Date().toISOString(),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normalizeModelEntries(input) {
|
|
157
|
+
if (input == null) return [];
|
|
158
|
+
if (!Array.isArray(input)) throw Object.assign(new Error('models.list must be an array'), { status: 400 });
|
|
159
|
+
if (input.length > 100) throw Object.assign(new Error('models.list exceeds 100 entries'), { status: 400 });
|
|
160
|
+
const seen = new Set();
|
|
161
|
+
const entries = [];
|
|
162
|
+
for (const item of input) {
|
|
163
|
+
const raw = item && typeof item === 'object' && !Array.isArray(item) ? item : { id: item };
|
|
164
|
+
const id = String(raw.id || raw.model || '').trim().slice(0, 128);
|
|
165
|
+
if (!id) throw Object.assign(new Error('model entry id is required'), { status: 400 });
|
|
166
|
+
if (seen.has(id)) continue;
|
|
167
|
+
seen.add(id);
|
|
168
|
+
const tags = Array.isArray(raw.tags)
|
|
169
|
+
? raw.tags.map(tag => String(tag).trim().slice(0, 32)).filter(Boolean).slice(0, 8)
|
|
170
|
+
: [];
|
|
171
|
+
// Detected context windows ride along so drivers (opencode limit.context)
|
|
172
|
+
// and the capacity bubble can use them; invalid values are dropped.
|
|
173
|
+
const contextWindow = Number(raw.contextWindow);
|
|
174
|
+
const limits = Number.isFinite(contextWindow) && contextWindow > 0 && contextWindow <= 10_000_000
|
|
175
|
+
? { contextWindow: Math.floor(contextWindow) }
|
|
176
|
+
: {};
|
|
177
|
+
entries.push({ id, openaiId: String(raw.openaiId || id.replace(/\[1m\]$/i, '')).trim().slice(0, 128), label: String(raw.label || id).slice(0, 128), tags, ...limits });
|
|
178
|
+
}
|
|
179
|
+
return entries;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeProfile(input = {}, idOverride) {
|
|
183
|
+
const id = cleanId(idOverride || input.id, 'profileId');
|
|
184
|
+
const models = input.models && typeof input.models === 'object' ? input.models : {};
|
|
185
|
+
const list = normalizeModelEntries(models.list);
|
|
186
|
+
const fallbackModel = list.length ? list[0].id : '';
|
|
187
|
+
const defaultModel = String(models.default || input.mainModel || input.model || fallbackModel).trim();
|
|
188
|
+
return {
|
|
189
|
+
id,
|
|
190
|
+
name: String(input.name || id).trim().slice(0, 256),
|
|
191
|
+
provider: String(input.provider || '').trim().slice(0, 128),
|
|
192
|
+
protocol: String(input.protocol || 'anthropic-compatible').trim(),
|
|
193
|
+
runtime: RUNTIME,
|
|
194
|
+
baseUrl: String(input.baseUrl || '').trim().replace(/\/+$/, ''),
|
|
195
|
+
openaiBaseUrl: resolveOpenAIBaseUrl(input),
|
|
196
|
+
credentialRef: String(input.credentialRef || input.secretId || `credential:${id}`).trim(),
|
|
197
|
+
models: {
|
|
198
|
+
default: defaultModel,
|
|
199
|
+
fast: String(models.fast || defaultModel).trim(),
|
|
200
|
+
reasoning: String(models.reasoning || defaultModel).trim(),
|
|
201
|
+
coding: String(models.coding || defaultModel).trim(),
|
|
202
|
+
largeContext: String(models.largeContext || defaultModel).trim(),
|
|
203
|
+
list,
|
|
204
|
+
},
|
|
205
|
+
contextWindow: positiveInt(input.contextWindow, 200000),
|
|
206
|
+
timeoutMs: positiveInt(input.timeoutMs, 300000),
|
|
207
|
+
permissionMode: PERMISSION_MODES.includes(input.permissionMode) ? input.permissionMode : 'default',
|
|
208
|
+
systemPrompt: normalizeSystemPrompt(input.systemPrompt),
|
|
209
|
+
enabled: input.enabled !== false,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function positiveInt(value, fallback) {
|
|
214
|
+
const parsed = Number(value);
|
|
215
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function resolveOpenAIBaseUrl(profile = {}) {
|
|
219
|
+
const explicit = String(profile.openaiBaseUrl || '').trim().replace(/\/+$/, '');
|
|
220
|
+
if (explicit) return explicit;
|
|
221
|
+
const baseUrl = String(profile.baseUrl || '').trim().replace(/\/+$/, '');
|
|
222
|
+
if (profile.protocol === 'openai-compatible') return baseUrl;
|
|
223
|
+
let hostname = '';
|
|
224
|
+
try { hostname = new URL(baseUrl).hostname; } catch {}
|
|
225
|
+
if (hostname === 'api.minimax.io') return 'https://api.minimax.io/v1';
|
|
226
|
+
if (hostname === 'api.minimaxi.com' || /minimax/i.test(String(profile.provider || ''))) return 'https://api.minimaxi.com/v1';
|
|
227
|
+
return '';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveOpenAIModel(profile = {}, model) {
|
|
231
|
+
const id = String(model || profile.mainModel || profile.models && profile.models.default || '').trim();
|
|
232
|
+
const entries = profile.models && profile.models.list;
|
|
233
|
+
const entry = Array.isArray(entries) && entries.find(item => item.id === id);
|
|
234
|
+
return String(entry && entry.openaiId || id.replace(/\[1m\]$/i, '')).trim();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeAttachments(input) {
|
|
238
|
+
if (input == null) return [];
|
|
239
|
+
if (!Array.isArray(input)) throw Object.assign(new Error('attachments must be an array'), { status: 400 });
|
|
240
|
+
if (input.length > MAX_ATTACHMENTS_PER_MESSAGE) throw Object.assign(new Error(`at most ${MAX_ATTACHMENTS_PER_MESSAGE} attachments are allowed`), { status: 413 });
|
|
241
|
+
let totalBytes = 0;
|
|
242
|
+
return input.map((item, index) => {
|
|
243
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) throw Object.assign(new Error(`attachment ${index + 1} is invalid`), { status: 400 });
|
|
244
|
+
const name = String(item.name || `image-${index + 1}`).replace(/[\u0000-\u001f]/g, '').slice(0, 255);
|
|
245
|
+
let mediaType = String(item.mediaType || item.mimeType || '').toLowerCase().split(';')[0].trim();
|
|
246
|
+
const isImage = IMAGE_MEDIA_TYPES.includes(mediaType);
|
|
247
|
+
if (!isImage) {
|
|
248
|
+
const extension = (name.match(/\.[^.\\/]+$/) || [''])[0].toLowerCase();
|
|
249
|
+
const textType = TEXT_FILE_TYPES[extension];
|
|
250
|
+
if (!textType || !['', 'application/octet-stream', 'text/plain', textType].includes(mediaType)) {
|
|
251
|
+
throw Object.assign(new Error(`attachment ${index + 1} has an unsupported type; use images or .txt, .md, .json, .csv files`), { status: 415 });
|
|
252
|
+
}
|
|
253
|
+
mediaType = textType;
|
|
254
|
+
}
|
|
255
|
+
const data = String(item.data || '').replace(/^data:[^;]+;base64,/, '').replace(/\s+/g, '');
|
|
256
|
+
if (!data || !/^[a-zA-Z0-9+/]+={0,2}$/.test(data) || data.length % 4 === 1 || (data.includes('=') && data.length % 4 !== 0)) throw Object.assign(new Error(`attachment ${index + 1} has invalid base64 data`), { status: 400 });
|
|
257
|
+
const size = Math.floor((data.length * 3) / 4) - (data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0);
|
|
258
|
+
const maxBytes = isImage ? MAX_IMAGE_BYTES : MAX_TEXT_BYTES;
|
|
259
|
+
if (size <= 0 || size > maxBytes) throw Object.assign(new Error(`attachment ${index + 1} exceeds ${maxBytes} bytes`), { status: 413 });
|
|
260
|
+
const bytes = Buffer.from(data, 'base64');
|
|
261
|
+
if (bytes.toString('base64').replace(/=+$/, '') !== data.replace(/=+$/, '')) throw Object.assign(new Error(`attachment ${index + 1} has invalid base64 data`), { status: 400 });
|
|
262
|
+
if (!isImage) {
|
|
263
|
+
try {
|
|
264
|
+
const content = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
265
|
+
if (content.includes('\u0000')) throw new Error('binary content');
|
|
266
|
+
} catch { throw Object.assign(new Error(`attachment ${index + 1} must contain UTF-8 text`), { status: 415 }); }
|
|
267
|
+
}
|
|
268
|
+
totalBytes += size;
|
|
269
|
+
if (totalBytes > MAX_ATTACHMENT_MESSAGE_BYTES) throw Object.assign(new Error(`attachment payload exceeds ${MAX_ATTACHMENT_MESSAGE_BYTES} bytes`), { status: 413 });
|
|
270
|
+
return {
|
|
271
|
+
id: item.id ? cleanId(item.id, 'attachmentId') : undefined,
|
|
272
|
+
name,
|
|
273
|
+
mediaType,
|
|
274
|
+
size,
|
|
275
|
+
width: positiveInt(item.width, 0) || undefined,
|
|
276
|
+
height: positiveInt(item.height, 0) || undefined,
|
|
277
|
+
data,
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function normalizeImageAttachments(input) {
|
|
283
|
+
const attachments = normalizeAttachments(input);
|
|
284
|
+
if (attachments.some(item => !IMAGE_MEDIA_TYPES.includes(item.mediaType))) throw Object.assign(new Error('unsupported image type'), { status: 415 });
|
|
285
|
+
return attachments;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function appendTextAttachments(text, attachments = []) {
|
|
289
|
+
const blocks = attachments.filter(item => TEXT_MEDIA_TYPES.includes(item.mediaType)).map(item => {
|
|
290
|
+
const name = String(item.name).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
291
|
+
return `<attachment name="${name}">\n${Buffer.from(item.data, 'base64').toString('utf8')}\n</attachment>`;
|
|
292
|
+
});
|
|
293
|
+
return [String(text || ''), ...blocks].filter(Boolean).join('\n\n');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function normalizeSystemPrompt(value) {
|
|
297
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
|
298
|
+
if (typeof value === 'string' && value.trim()) return { type: 'custom', content: value };
|
|
299
|
+
return { type: 'preset', preset: 'claude_code', content: '' };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = {
|
|
303
|
+
API_VERSION, DEFAULT_API_PREFIX, RUNTIME, DEFAULT_AGENT, AGENTS, AGENT_IDS, EFFORT_LEVELS,
|
|
304
|
+
PERMISSION_MODES, IMAGE_MEDIA_TYPES, TEXT_MEDIA_TYPES, TEXT_FILE_TYPES,
|
|
305
|
+
MAX_IMAGE_BYTES, MAX_IMAGES_PER_MESSAGE, MAX_IMAGE_MESSAGE_BYTES, EVENT_TYPES,
|
|
306
|
+
MAX_TEXT_BYTES, MAX_ATTACHMENTS_PER_MESSAGE, MAX_ATTACHMENT_MESSAGE_BYTES,
|
|
307
|
+
DELEGATED_SESSION_KIND, DELEGATION_COMMAND_SOURCE, DELEGATION_COMMAND_BASIS,
|
|
308
|
+
DELEGATION_PHASES, DELEGATION_OUTCOMES,
|
|
309
|
+
cleanId, normalizeContext, normalizeProfile, normalizeProject, normalizeAgentId, normalizeEffort,
|
|
310
|
+
normalizeModelEntries, normalizeImageAttachments, normalizeAttachments, appendTextAttachments,
|
|
311
|
+
resolveOpenAIBaseUrl, resolveOpenAIModel, positiveInt,
|
|
312
|
+
};
|