@promptbook/cli 0.112.0-133 → 0.112.0-135
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/apps/agents-server/src/app/admin/chat-feedback/page.tsx +2 -4
- package/apps/agents-server/src/app/admin/chat-history/page.tsx +2 -4
- package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +2 -0
- package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +33 -9
- package/apps/agents-server/src/app/api/chat/route.ts +29 -7
- package/apps/agents-server/src/app/api/chat-feedback/export/route.ts +2 -2
- package/apps/agents-server/src/app/api/chat-feedback/route.ts +3 -3
- package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +2 -2
- package/apps/agents-server/src/app/api/chat-history/export/route.ts +2 -2
- package/apps/agents-server/src/app/api/chat-history/route.ts +3 -3
- package/apps/agents-server/src/app/api/chat-streaming/route.ts +29 -0
- package/apps/agents-server/src/app/api/elevenlabs/tts/route.ts +60 -2
- package/apps/agents-server/src/app/api/images/[filename]/route.ts +102 -0
- package/apps/agents-server/src/app/api/openai/v1/audio/transcriptions/route.ts +111 -2
- package/apps/agents-server/src/app/api/page-preview/check/route.ts +18 -0
- package/apps/agents-server/src/app/api/page-preview/screenshot/route.ts +15 -8
- package/apps/agents-server/src/app/api/team-agent-profile/route.ts +20 -0
- package/apps/agents-server/src/app/api/v1/agents/[agentId]/route.ts +4 -0
- package/apps/agents-server/src/app/api/v1/agents/route.ts +2 -0
- package/apps/agents-server/src/app/recycle-bin/actions.ts +3 -0
- package/apps/agents-server/src/components/AgentContextMenu/useAgentContextMenuItems.ts +27 -19
- package/apps/agents-server/src/constants/defaultAgentAvatarVisual.ts +1 -1
- package/apps/agents-server/src/database/migrations/2026-06-2600-agent-directory-performance-indexes.sql +14 -0
- package/apps/agents-server/src/database/migrations/2026-06-2700-default-agent-avatar-visual-octopus3d4.sql +16 -0
- package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +92 -1
- package/apps/agents-server/src/utils/agentOwnership.ts +54 -5
- package/apps/agents-server/src/utils/findAgentForCallerWriteAccess.ts +36 -0
- package/apps/agents-server/src/utils/paidApiRequestGuard.ts +241 -0
- package/apps/agents-server/src/utils/userChat/createRunUserChatJobExecutionContext.ts +36 -8
- package/apps/agents-server/src/utils/userChat/listUserChats.ts +5 -1
- package/apps/agents-server/src/utils/userChat/persistUserChatJobProgressCard.ts +11 -2
- package/apps/agents-server/src/utils/userChat/resolveUserChatProgressToolHighlights.ts +100 -0
- package/apps/agents-server/src/utils/userChat/runUserChatJob.ts +4 -3
- package/apps/agents-server/src/utils/userChat/userChatProgressCard.ts +272 -27
- package/apps/agents-server/src/utils/validateApiKey.ts +7 -3
- package/esm/index.es.js +795 -39
- package/esm/index.es.js.map +1 -1
- package/esm/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
- package/esm/src/avatars/visuals/octopus3d4AvatarVisual.d.ts +7 -0
- package/esm/src/utils/validators/url/isValidAgentUrl.d.ts +5 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/avatars/types/AvatarVisualDefinition.ts +1 -0
- package/src/avatars/visuals/avatarVisualRegistry.ts +2 -0
- package/src/avatars/visuals/octopus3d4AvatarVisual.ts +1295 -0
- package/src/other/templates/getTemplatesPipelineCollection.ts +759 -679
- package/src/utils/agents/resolveAgentAvatarImageUrl.ts +1 -1
- package/src/utils/validators/url/isValidAgentUrl.ts +5 -7
- package/src/version.ts +2 -2
- package/src/versions.txt +1 -0
- package/umd/index.umd.js +795 -39
- package/umd/index.umd.js.map +1 -1
- package/umd/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
- package/umd/src/avatars/visuals/octopus3d4AvatarVisual.d.ts +7 -0
- package/umd/src/utils/validators/url/isValidAgentUrl.d.ts +5 -0
- package/umd/src/version.d.ts +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import OpenAI from 'openai';
|
|
3
|
+
import { spaceTrim } from 'spacetrim';
|
|
3
4
|
import { getMetadata } from '@/src/database/getMetadata';
|
|
5
|
+
import { guardPaidApiRequest } from '@/src/utils/paidApiRequestGuard';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Fallback file name used when the uploaded blob does not carry one.
|
|
@@ -12,6 +14,38 @@ const DEFAULT_AUDIO_TRANSCRIPTION_FILE_NAME = 'speech-recording.webm';
|
|
|
12
14
|
*/
|
|
13
15
|
const DEFAULT_AUDIO_TRANSCRIPTION_MODEL_PRIORITY = ['gpt-4o-transcribe', 'gpt-4o-mini-transcribe', 'whisper-1'] as const;
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Allowlist of model identifiers accepted from the caller.
|
|
19
|
+
*
|
|
20
|
+
* Caller-supplied model names are forwarded to the paid OpenAI API, so we
|
|
21
|
+
* validate them against a fixed allowlist to prevent attackers from forcing
|
|
22
|
+
* the server to call expensive or experimental models that are not part of
|
|
23
|
+
* the supported transcription stack.
|
|
24
|
+
*/
|
|
25
|
+
const ALLOWED_AUDIO_TRANSCRIPTION_MODELS = new Set<string>(DEFAULT_AUDIO_TRANSCRIPTION_MODEL_PRIORITY);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Maximum upload size that will be forwarded to OpenAI transcription.
|
|
29
|
+
*
|
|
30
|
+
* OpenAI's own limit is 25 MB; mirroring it here ensures we fail fast with a
|
|
31
|
+
* clear error before paying for an upload that the provider would reject.
|
|
32
|
+
*/
|
|
33
|
+
const MAX_AUDIO_TRANSCRIPTION_FILE_SIZE_BYTES = 25 * 1024 * 1024;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Pattern matching ISO-639-1 language codes that OpenAI transcription accepts.
|
|
37
|
+
*/
|
|
38
|
+
const ISO_639_1_LANGUAGE_CODE_PATTERN = /^[a-z]{2}$/u;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Maximum prompt length forwarded as transcription context.
|
|
42
|
+
*
|
|
43
|
+
* OpenAI rejects prompts longer than 224 tokens; we keep a conservative
|
|
44
|
+
* character cap that comfortably stays under that limit while preventing
|
|
45
|
+
* attackers from inflating request size.
|
|
46
|
+
*/
|
|
47
|
+
const MAX_AUDIO_TRANSCRIPTION_PROMPT_LENGTH = 800;
|
|
48
|
+
|
|
15
49
|
/**
|
|
16
50
|
* Proxy endpoint for OpenAI audio transcription
|
|
17
51
|
*
|
|
@@ -33,17 +67,55 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
|
|
33
67
|
);
|
|
34
68
|
}
|
|
35
69
|
|
|
70
|
+
const guard = await guardPaidApiRequest(request, 'AUDIO_TRANSCRIPTION');
|
|
71
|
+
if (!guard.ok) {
|
|
72
|
+
return guard.response;
|
|
73
|
+
}
|
|
74
|
+
|
|
36
75
|
const formData = await request.formData();
|
|
37
76
|
const file = formData.get('file') as File;
|
|
38
77
|
const requestedModel = normalizeFormDataStringValue(formData.get('model'));
|
|
39
|
-
const language =
|
|
40
|
-
const prompt =
|
|
78
|
+
const language = resolveAudioTranscriptionLanguage(formData.get('language'));
|
|
79
|
+
const prompt = resolveAudioTranscriptionPrompt(formData.get('prompt'));
|
|
41
80
|
const temperature = resolveAudioTranscriptionTemperature(formData.get('temperature'));
|
|
42
81
|
|
|
43
82
|
if (!file) {
|
|
44
83
|
return NextResponse.json({ error: { message: 'No file provided' } }, { status: 400 });
|
|
45
84
|
}
|
|
46
85
|
|
|
86
|
+
if (file.size > MAX_AUDIO_TRANSCRIPTION_FILE_SIZE_BYTES) {
|
|
87
|
+
return NextResponse.json(
|
|
88
|
+
{
|
|
89
|
+
error: {
|
|
90
|
+
message: spaceTrim(`
|
|
91
|
+
Uploaded audio file is too large.
|
|
92
|
+
|
|
93
|
+
**Maximum allowed size:** ${Math.round(
|
|
94
|
+
MAX_AUDIO_TRANSCRIPTION_FILE_SIZE_BYTES / (1024 * 1024),
|
|
95
|
+
)} MB
|
|
96
|
+
**Received:** ${(file.size / (1024 * 1024)).toFixed(2)} MB
|
|
97
|
+
`),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{ status: 413 },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (requestedModel !== undefined && !ALLOWED_AUDIO_TRANSCRIPTION_MODELS.has(requestedModel)) {
|
|
105
|
+
return NextResponse.json(
|
|
106
|
+
{
|
|
107
|
+
error: {
|
|
108
|
+
message: spaceTrim(`
|
|
109
|
+
Requested transcription model is not allowed.
|
|
110
|
+
|
|
111
|
+
**Allowed models:** ${Array.from(ALLOWED_AUDIO_TRANSCRIPTION_MODELS).join(', ')}
|
|
112
|
+
`),
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{ status: 400 },
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
47
119
|
const openai = new OpenAI({
|
|
48
120
|
apiKey: openAiApiKey,
|
|
49
121
|
});
|
|
@@ -108,6 +180,43 @@ function normalizeFormDataStringValue(value: FormDataEntryValue | null): string
|
|
|
108
180
|
return normalizedValue;
|
|
109
181
|
}
|
|
110
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Validates the caller-supplied language hint against ISO-639-1.
|
|
185
|
+
*
|
|
186
|
+
* Forwarding arbitrary `language` strings to OpenAI is harmless cost-wise but
|
|
187
|
+
* widens the attack surface — invalid values can trigger model retries that
|
|
188
|
+
* silently inflate the bill. We mirror the documented constraint and drop
|
|
189
|
+
* anything that does not match.
|
|
190
|
+
*/
|
|
191
|
+
function resolveAudioTranscriptionLanguage(value: FormDataEntryValue | null): string | undefined {
|
|
192
|
+
const normalizedValue = normalizeFormDataStringValue(value);
|
|
193
|
+
if (!normalizedValue) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!ISO_639_1_LANGUAGE_CODE_PATTERN.test(normalizedValue)) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return normalizedValue.toLowerCase();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Validates and truncates the optional transcription prompt before sending it to OpenAI.
|
|
206
|
+
*/
|
|
207
|
+
function resolveAudioTranscriptionPrompt(value: FormDataEntryValue | null): string | undefined {
|
|
208
|
+
const normalizedValue = normalizeFormDataStringValue(value);
|
|
209
|
+
if (!normalizedValue) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (normalizedValue.length <= MAX_AUDIO_TRANSCRIPTION_PROMPT_LENGTH) {
|
|
214
|
+
return normalizedValue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return normalizedValue.slice(0, MAX_AUDIO_TRANSCRIPTION_PROMPT_LENGTH);
|
|
218
|
+
}
|
|
219
|
+
|
|
111
220
|
/**
|
|
112
221
|
* Parses optional transcription temperature from multipart form data.
|
|
113
222
|
*/
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { NextRequest } from 'next/server';
|
|
2
2
|
import { NextResponse } from 'next/server';
|
|
3
3
|
import { assertsError } from '../../../../../../../src/errors/assertsError';
|
|
4
|
+
import { assertSafeUrl } from '../../../../utils/assertSafeUrl';
|
|
5
|
+
import { getCurrentUser } from '../../../../utils/getCurrentUser';
|
|
4
6
|
import { checkIfUrlCanBeEmbedded } from '../../../../utils/iframe/checkIfUrlCanBeEmbedded';
|
|
5
7
|
|
|
6
8
|
/**
|
|
@@ -11,14 +13,30 @@ import { checkIfUrlCanBeEmbedded } from '../../../../utils/iframe/checkIfUrlCanB
|
|
|
11
13
|
* - `url` — the fully-qualified HTTP(S) URL to check
|
|
12
14
|
*
|
|
13
15
|
* Returns `{ canEmbed: boolean }`.
|
|
16
|
+
*
|
|
17
|
+
* Requires authentication to prevent unauthenticated SSRF abuse.
|
|
18
|
+
* The destination URL is validated against private/internal IP ranges before fetching.
|
|
14
19
|
*/
|
|
15
20
|
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
21
|
+
const currentUser = await getCurrentUser();
|
|
22
|
+
if (!currentUser) {
|
|
23
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
const url = request.nextUrl.searchParams.get('url');
|
|
17
27
|
|
|
18
28
|
if (!url) {
|
|
19
29
|
return NextResponse.json({ error: 'Missing required query parameter: url' }, { status: 400 });
|
|
20
30
|
}
|
|
21
31
|
|
|
32
|
+
// Guard against SSRF: reject private/internal IPs and non-HTTP(S) schemes
|
|
33
|
+
try {
|
|
34
|
+
assertSafeUrl(url);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
assertsError(error);
|
|
37
|
+
return NextResponse.json({ error: error.message }, { status: 400 });
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
try {
|
|
23
41
|
const canEmbed = await checkIfUrlCanBeEmbedded(url);
|
|
24
42
|
return NextResponse.json({ canEmbed });
|
|
@@ -3,6 +3,8 @@ import { serializeError } from '@promptbook-local/utils';
|
|
|
3
3
|
import type { NextRequest } from 'next/server';
|
|
4
4
|
import { NextResponse } from 'next/server';
|
|
5
5
|
import { assertsError } from '../../../../../../../src/errors/assertsError';
|
|
6
|
+
import { assertSafeUrl } from '../../../../utils/assertSafeUrl';
|
|
7
|
+
import { getCurrentUser } from '../../../../utils/getCurrentUser';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Takes a screenshot of the given URL using a headless browser.
|
|
@@ -11,23 +13,28 @@ import { assertsError } from '../../../../../../../src/errors/assertsError';
|
|
|
11
13
|
* - `url` — the fully-qualified HTTP(S) URL to screenshot
|
|
12
14
|
*
|
|
13
15
|
* Returns a PNG image.
|
|
16
|
+
*
|
|
17
|
+
* Requires authentication to prevent unauthenticated SSRF abuse.
|
|
18
|
+
* The destination URL is validated against private/internal IP ranges before fetching.
|
|
14
19
|
*/
|
|
15
20
|
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
21
|
+
const currentUser = await getCurrentUser();
|
|
22
|
+
if (!currentUser) {
|
|
23
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
const url = request.nextUrl.searchParams.get('url');
|
|
17
27
|
|
|
18
28
|
if (!url) {
|
|
19
29
|
return NextResponse.json({ error: 'Missing required query parameter: url' }, { status: 400 });
|
|
20
30
|
}
|
|
21
31
|
|
|
22
|
-
|
|
32
|
+
// Guard against SSRF: reject private/internal IPs and non-HTTP(S) schemes
|
|
23
33
|
try {
|
|
24
|
-
|
|
25
|
-
} catch {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
30
|
-
return NextResponse.json({ error: 'Only http and https URLs are supported' }, { status: 400 });
|
|
34
|
+
assertSafeUrl(url);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
assertsError(error);
|
|
37
|
+
return NextResponse.json({ error: error.message }, { status: 400 });
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
try {
|
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
resolvePlaceholderImageUrl,
|
|
9
9
|
resolveProfileImageUrl,
|
|
10
10
|
} from '../../../../../../src/book-components/Chat/utils/loadAgentProfile';
|
|
11
|
+
import { assertsError } from '../../../../../../src/errors/assertsError';
|
|
11
12
|
import { isValidAgentUrl } from '../../../../../../src/utils/validators/url/isValidAgentUrl';
|
|
13
|
+
import { assertSafeUrl } from '../../../utils/assertSafeUrl';
|
|
14
|
+
import { getCurrentUser } from '../../../utils/getCurrentUser';
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Response for team agent profile.
|
|
@@ -88,8 +91,17 @@ async function fetchAgentProfile(agentUrl: string, localServerUrl: string): Prom
|
|
|
88
91
|
*
|
|
89
92
|
* The client uses this to render tool call chips without relying on direct cross-origin requests
|
|
90
93
|
* to the teammate agent.
|
|
94
|
+
*
|
|
95
|
+
* Requires authentication to prevent unauthenticated SSRF abuse and exfiltration of the
|
|
96
|
+
* internal team access token. The destination URL is validated against private/internal
|
|
97
|
+
* IP ranges before any outbound request is made.
|
|
91
98
|
*/
|
|
92
99
|
export async function GET(request: Request) {
|
|
100
|
+
const currentUser = await getCurrentUser();
|
|
101
|
+
if (!currentUser) {
|
|
102
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
const requestUrl = new URL(request.url);
|
|
94
106
|
const agentUrl = requestUrl.searchParams.get('url');
|
|
95
107
|
|
|
@@ -97,6 +109,14 @@ export async function GET(request: Request) {
|
|
|
97
109
|
return NextResponse.json({ error: 'Invalid agent URL' }, { status: 400 });
|
|
98
110
|
}
|
|
99
111
|
|
|
112
|
+
// Guard against SSRF: reject private/internal IPs and non-HTTP(S) schemes
|
|
113
|
+
try {
|
|
114
|
+
assertSafeUrl(agentUrl);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
assertsError(error);
|
|
117
|
+
return NextResponse.json({ error: error.message }, { status: 400 });
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
const normalizedUrl = agentUrl.replace(/\/$/, '');
|
|
101
121
|
const profile = await fetchAgentProfile(normalizedUrl, requestUrl.origin);
|
|
102
122
|
|
|
@@ -6,6 +6,7 @@ import { $getTableName } from '@/src/database/$getTableName';
|
|
|
6
6
|
import { $provideSupabaseForServer } from '@/src/database/$provideSupabaseForServer';
|
|
7
7
|
import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
|
|
8
8
|
import { $provideServer } from '@/src/tools/$provideServer';
|
|
9
|
+
import { invalidateCachedActiveOrganizationSnapshots } from '@/src/utils/agentOrganization/loadAgentOrganizationState';
|
|
9
10
|
import { findOwnedAgentByIdentifier, findOwnedFolderById, type OwnedAgentRow } from '@/src/utils/agentOwnership';
|
|
10
11
|
import {
|
|
11
12
|
getNextOwnedAgentSortOrder,
|
|
@@ -155,6 +156,8 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|
|
155
156
|
return metadataUpdateResponse;
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
invalidateCachedActiveOrganizationSnapshots();
|
|
160
|
+
|
|
158
161
|
return createUpdatedOwnedAgentResponse(request, identityResult.identity.userId, existingAgent);
|
|
159
162
|
} catch (error) {
|
|
160
163
|
return mapOwnedAgentLookupErrorToResponse(request, error);
|
|
@@ -192,6 +195,7 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
|
|
|
192
195
|
|
|
193
196
|
const collection = await $provideAgentCollectionForServer();
|
|
194
197
|
await collection.deleteAgent(existingAgent.permanentId || existingAgent.agentName);
|
|
198
|
+
invalidateCachedActiveOrganizationSnapshots();
|
|
195
199
|
|
|
196
200
|
return createManagementApiJsonResponse(request, { success: true });
|
|
197
201
|
} catch (error) {
|
|
@@ -4,6 +4,7 @@ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentColle
|
|
|
4
4
|
import { $provideServer } from '@/src/tools/$provideServer';
|
|
5
5
|
import { findOwnedFolderById, findOwnedAgentByIdentifier } from '@/src/utils/agentOwnership';
|
|
6
6
|
import { createAgentWithDefaultVisibility } from '@/src/utils/createAgentWithDefaultVisibility';
|
|
7
|
+
import { invalidateCachedActiveOrganizationSnapshots } from '@/src/utils/agentOrganization/loadAgentOrganizationState';
|
|
7
8
|
import {
|
|
8
9
|
searchOwnedAgents,
|
|
9
10
|
getNextOwnedAgentSortOrder,
|
|
@@ -141,6 +142,7 @@ export async function POST(request: NextRequest) {
|
|
|
141
142
|
sortOrder,
|
|
142
143
|
userId: identityResult.identity.userId,
|
|
143
144
|
});
|
|
145
|
+
invalidateCachedActiveOrganizationSnapshots();
|
|
144
146
|
const persistedRow = await findOwnedAgentByIdentifier(identityResult.identity.userId, createdAgent.permanentId);
|
|
145
147
|
|
|
146
148
|
if (!persistedRow) {
|
|
@@ -4,6 +4,7 @@ import { restoreAgentAndFolders } from '@/src/utils/agentOrganization/restoreAge
|
|
|
4
4
|
import { revalidatePath } from 'next/cache';
|
|
5
5
|
import { isUserAdmin } from '../../utils/isUserAdmin';
|
|
6
6
|
import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
|
|
7
|
+
import { invalidateCachedActiveOrganizationSnapshots } from '@/src/utils/agentOrganization/loadAgentOrganizationState';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Restores a deleted agent from the recycle bin.
|
|
@@ -16,6 +17,7 @@ export async function restoreDeletedAgent(agentName: string) {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
await restoreAgentAndFolders(agentName);
|
|
20
|
+
invalidateCachedActiveOrganizationSnapshots();
|
|
19
21
|
|
|
20
22
|
revalidatePath('/recycle-bin');
|
|
21
23
|
revalidatePath(`/agents/${agentName}`);
|
|
@@ -35,6 +37,7 @@ export async function deleteAgent(agentName: string) {
|
|
|
35
37
|
const collection = await $provideAgentCollectionForServer();
|
|
36
38
|
|
|
37
39
|
await collection.deleteAgent(agentName);
|
|
40
|
+
invalidateCachedActiveOrganizationSnapshots();
|
|
38
41
|
|
|
39
42
|
revalidatePath('/recycle-bin');
|
|
40
43
|
revalidatePath(`/agents/${agentName}`);
|
|
@@ -300,6 +300,7 @@ function createManagementMenuItems(
|
|
|
300
300
|
* @param formatText - Agent-aware text formatter.
|
|
301
301
|
* @param handleRequestVisibilityUpdate - Visibility dialog action.
|
|
302
302
|
* @param integrationLink - Generated integration link metadata.
|
|
303
|
+
* @param isAdmin - Whether the current user is an administrator (gates admin-only links).
|
|
303
304
|
* @param isAuthenticated - Whether the current user is logged in.
|
|
304
305
|
* @param shouldShowVisibilityAction - Whether visibility editing is available.
|
|
305
306
|
* @param usageAnalyticsHref - Usage analytics URL for the current agent.
|
|
@@ -311,6 +312,7 @@ function createAdminMenuItems(
|
|
|
311
312
|
formatText: FormatAgentContextMenuText,
|
|
312
313
|
handleRequestVisibilityUpdate: () => Promise<void>,
|
|
313
314
|
integrationLink: AgentContextMenuNavigationLink,
|
|
315
|
+
isAdmin: boolean,
|
|
314
316
|
isAuthenticated: boolean,
|
|
315
317
|
shouldShowVisibilityAction: boolean,
|
|
316
318
|
usageAnalyticsHref: string,
|
|
@@ -333,26 +335,31 @@ function createAdminMenuItems(
|
|
|
333
335
|
);
|
|
334
336
|
}
|
|
335
337
|
|
|
338
|
+
if (isAdmin) {
|
|
339
|
+
menuItems.push(
|
|
340
|
+
{
|
|
341
|
+
type: 'link',
|
|
342
|
+
href: `/admin/chat-history?agentName=${encodeURIComponent(agentName)}`,
|
|
343
|
+
icon: MessageSquareIcon,
|
|
344
|
+
label: formatText('Chat History'),
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
type: 'link',
|
|
348
|
+
href: usageAnalyticsHref,
|
|
349
|
+
icon: BarChart3Icon,
|
|
350
|
+
label: formatText('Usage Analytics'),
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
type: 'link',
|
|
354
|
+
href: `/admin/chat-feedback?agentName=${encodeURIComponent(agentName)}`,
|
|
355
|
+
icon: MessageCircleQuestionIcon,
|
|
356
|
+
label: formatText('Chat Feedback'),
|
|
357
|
+
},
|
|
358
|
+
createDividerItem(),
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
336
362
|
menuItems.push(
|
|
337
|
-
{
|
|
338
|
-
type: 'link',
|
|
339
|
-
href: `/admin/chat-history?agentName=${encodeURIComponent(agentName)}`,
|
|
340
|
-
icon: MessageSquareIcon,
|
|
341
|
-
label: formatText('Chat History'),
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
type: 'link',
|
|
345
|
-
href: usageAnalyticsHref,
|
|
346
|
-
icon: BarChart3Icon,
|
|
347
|
-
label: formatText('Usage Analytics'),
|
|
348
|
-
},
|
|
349
|
-
{
|
|
350
|
-
type: 'link',
|
|
351
|
-
href: `/admin/chat-feedback?agentName=${encodeURIComponent(agentName)}`,
|
|
352
|
-
icon: MessageCircleQuestionIcon,
|
|
353
|
-
label: formatText('Chat Feedback'),
|
|
354
|
-
},
|
|
355
|
-
createDividerItem(),
|
|
356
363
|
{
|
|
357
364
|
type: 'link',
|
|
358
365
|
href: integrationLink.href,
|
|
@@ -437,6 +444,7 @@ export function useAgentContextMenuItems(props: AgentContextMenuBaseProps): Cont
|
|
|
437
444
|
formatText,
|
|
438
445
|
handleRequestVisibilityUpdate,
|
|
439
446
|
integrationLink,
|
|
447
|
+
isAdmin,
|
|
440
448
|
isAuthenticated,
|
|
441
449
|
shouldShowVisibilityAction,
|
|
442
450
|
usageAnalyticsHref,
|
|
@@ -53,7 +53,7 @@ export const DEFAULT_AGENT_AVATAR_VISUAL_METADATA_VALUES = DEFAULT_AGENT_AVATAR_
|
|
|
53
53
|
export const DEFAULT_AGENT_AVATAR_VISUAL_METADATA_VALUE =
|
|
54
54
|
DEFAULT_AGENT_AVATAR_VISUAL_METADATA_OPTIONS.find(
|
|
55
55
|
({ visualId }) => visualId === SHARED_DEFAULT_AGENT_AVATAR_VISUAL_ID,
|
|
56
|
-
)?.metadataValue || '
|
|
56
|
+
)?.metadataValue || 'OCTOPUS3D4';
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Resolves one raw metadata value to a supported built-in avatar visual id.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
CREATE INDEX IF NOT EXISTS "prefix_Agent_deletedAt_sortOrder_agentName_idx"
|
|
2
|
+
ON "prefix_Agent" ("deletedAt", "sortOrder", "agentName");
|
|
3
|
+
|
|
4
|
+
CREATE INDEX IF NOT EXISTS "prefix_Agent_visibility_deletedAt_sortOrder_agentName_idx"
|
|
5
|
+
ON "prefix_Agent" ("visibility", "deletedAt", "sortOrder", "agentName");
|
|
6
|
+
|
|
7
|
+
CREATE INDEX IF NOT EXISTS "prefix_AgentFolder_deletedAt_parentId_sortOrder_name_idx"
|
|
8
|
+
ON "prefix_AgentFolder" ("deletedAt", "parentId", "sortOrder", "name");
|
|
9
|
+
|
|
10
|
+
CREATE INDEX IF NOT EXISTS "prefix_UserChat_userId_agentPermanentId_source_createdAt_idx"
|
|
11
|
+
ON "prefix_UserChat" ("userId", "agentPermanentId", "source", "createdAt" DESC);
|
|
12
|
+
|
|
13
|
+
CREATE INDEX IF NOT EXISTS "prefix_UserChat_agentPermanentId_source_userId_createdAt_idx"
|
|
14
|
+
ON "prefix_UserChat" ("agentPermanentId", "source", "userId", "createdAt" DESC);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
INSERT INTO "prefix_Metadata" ("key", "value", "note", "createdAt", "updatedAt")
|
|
2
|
+
SELECT 'DEFAULT_AGENT_AVATAR_VISUAL',
|
|
3
|
+
'OCTOPUS3D4',
|
|
4
|
+
'Default built-in avatar visual used for agents without `META IMAGE` or `META AVATAR`. Allowed values: PIXEL_ART, OCTOPUS, OCTOPUS2, OCTOPUS3, OCTOPUS3D, OCTOPUS3D2, OCTOPUS3D3, OCTOPUS3D4, ASCII_OCTOPUS, MINECRAFT, MINECRAFT2, FRACTAL, ORB.',
|
|
5
|
+
NOW(),
|
|
6
|
+
NOW()
|
|
7
|
+
WHERE NOT EXISTS (SELECT 1 FROM "prefix_Metadata" WHERE "key" = 'DEFAULT_AGENT_AVATAR_VISUAL');
|
|
8
|
+
|
|
9
|
+
UPDATE "prefix_Metadata"
|
|
10
|
+
SET "value" = CASE
|
|
11
|
+
WHEN UPPER(COALESCE("value", '')) = 'OCTOPUS3D3' THEN 'OCTOPUS3D4'
|
|
12
|
+
ELSE "value"
|
|
13
|
+
END,
|
|
14
|
+
"note" = 'Default built-in avatar visual used for agents without `META IMAGE` or `META AVATAR`. Allowed values: PIXEL_ART, OCTOPUS, OCTOPUS2, OCTOPUS3, OCTOPUS3D, OCTOPUS3D2, OCTOPUS3D3, OCTOPUS3D4, ASCII_OCTOPUS, MINECRAFT, MINECRAFT2, FRACTAL, ORB.',
|
|
15
|
+
"updatedAt" = NOW()
|
|
16
|
+
WHERE "key" = 'DEFAULT_AGENT_AVATAR_VISUAL';
|
|
@@ -65,6 +65,14 @@ type LocalSqliteUpsertOptions = {
|
|
|
65
65
|
readonly onConflict?: string;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Read index definition for hot SQLite queries.
|
|
70
|
+
*/
|
|
71
|
+
type LocalSqliteReadIndex = {
|
|
72
|
+
readonly name: string;
|
|
73
|
+
readonly columns: ReadonlyArray<string>;
|
|
74
|
+
};
|
|
75
|
+
|
|
68
76
|
/**
|
|
69
77
|
* Columns whose values are persisted as JSON text in local SQLite.
|
|
70
78
|
*/
|
|
@@ -141,6 +149,51 @@ const UNIQUE_INDEX_COLUMNS_BY_TABLE = new Map<string, ReadonlyArray<ReadonlyArra
|
|
|
141
149
|
['UserPushSubscription', [['endpoint']]],
|
|
142
150
|
]);
|
|
143
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Non-unique indexes for frequent standalone VPS reads.
|
|
154
|
+
*/
|
|
155
|
+
const READ_INDEXES_BY_TABLE = new Map<string, ReadonlyArray<LocalSqliteReadIndex>>([
|
|
156
|
+
[
|
|
157
|
+
'Agent',
|
|
158
|
+
[
|
|
159
|
+
{ name: 'agentName_lookup', columns: ['agentName'] },
|
|
160
|
+
{ name: 'active_directory', columns: ['deletedAt', 'sortOrder', 'agentName'] },
|
|
161
|
+
{ name: 'public_active_directory', columns: ['visibility', 'deletedAt', 'sortOrder', 'agentName'] },
|
|
162
|
+
],
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
'AgentFolder',
|
|
166
|
+
[{ name: 'active_directory', columns: ['deletedAt', 'parentId', 'sortOrder', 'name'] }],
|
|
167
|
+
],
|
|
168
|
+
[
|
|
169
|
+
'UserChat',
|
|
170
|
+
[
|
|
171
|
+
{ name: 'user_agent_source_createdAt', columns: ['userId', 'agentPermanentId', 'source', 'createdAt'] },
|
|
172
|
+
{ name: 'agent_source_user_createdAt', columns: ['agentPermanentId', 'source', 'userId', 'createdAt'] },
|
|
173
|
+
],
|
|
174
|
+
],
|
|
175
|
+
[
|
|
176
|
+
'UserChatJob',
|
|
177
|
+
[
|
|
178
|
+
{ name: 'ready_queue', columns: ['status', 'cancelRequestedAt', 'queuedAt', 'createdAt'] },
|
|
179
|
+
{ name: 'active_chat_scope', columns: ['chatId', 'userId', 'agentPermanentId', 'status', 'createdAt'] },
|
|
180
|
+
{ name: 'agent_chat_status', columns: ['agentPermanentId', 'chatId', 'status'] },
|
|
181
|
+
{ name: 'running_lease', columns: ['status', 'leaseExpiresAt'] },
|
|
182
|
+
],
|
|
183
|
+
],
|
|
184
|
+
[
|
|
185
|
+
'UserChatTimeout',
|
|
186
|
+
[
|
|
187
|
+
{ name: 'ready_due', columns: ['status', 'cancelRequestedAt', 'pausedAt', 'dueAt', 'createdAt'] },
|
|
188
|
+
{
|
|
189
|
+
name: 'active_chat_scope',
|
|
190
|
+
columns: ['chatId', 'userId', 'agentPermanentId', 'status', 'pausedAt', 'dueAt', 'createdAt'],
|
|
191
|
+
},
|
|
192
|
+
{ name: 'running_lease', columns: ['status', 'leaseExpiresAt'] },
|
|
193
|
+
],
|
|
194
|
+
],
|
|
195
|
+
]);
|
|
196
|
+
|
|
144
197
|
/**
|
|
145
198
|
* Known unique conflict columns used when `.upsert` omits `onConflict`.
|
|
146
199
|
*/
|
|
@@ -179,6 +232,20 @@ export function $resetLocalSqliteSupabaseForTests(): void {
|
|
|
179
232
|
localSqliteSupabase = null;
|
|
180
233
|
}
|
|
181
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Ensures read indexes for a table that is queried through direct SQLite SQL.
|
|
237
|
+
*
|
|
238
|
+
* @param tableName - Actual table name, including any server prefix.
|
|
239
|
+
*
|
|
240
|
+
* @private internal SQLite utility of Agents Server
|
|
241
|
+
*/
|
|
242
|
+
export function ensureLocalSqliteTableReadIndexes(tableName: string): void {
|
|
243
|
+
const database = $provideAgentsServerSqliteDatabase();
|
|
244
|
+
const tableBaseName = resolveTableBaseName(tableName);
|
|
245
|
+
|
|
246
|
+
ensureTable(database, tableName, resolveReadIndexColumns(tableBaseName));
|
|
247
|
+
}
|
|
248
|
+
|
|
182
249
|
/**
|
|
183
250
|
* Supabase-shaped client with only the table query surface used by Agents Server.
|
|
184
251
|
*/
|
|
@@ -836,8 +903,9 @@ function ensureTable(
|
|
|
836
903
|
const columnsToEnsure = uniqueStrings([...requiredColumns, ...resolveUniqueIndexColumns(tableBaseName)]).filter(
|
|
837
904
|
(column) => column !== '*' && column !== 'id',
|
|
838
905
|
);
|
|
906
|
+
const columnsToEnsureWithIndexes = uniqueStrings([...columnsToEnsure, ...resolveReadIndexColumns(tableBaseName)]);
|
|
839
907
|
|
|
840
|
-
for (const column of
|
|
908
|
+
for (const column of columnsToEnsureWithIndexes) {
|
|
841
909
|
if (existingColumns.has(column)) {
|
|
842
910
|
continue;
|
|
843
911
|
}
|
|
@@ -852,6 +920,7 @@ function ensureTable(
|
|
|
852
920
|
}
|
|
853
921
|
|
|
854
922
|
ensureUniqueIndexes(database, tableName, tableBaseName);
|
|
923
|
+
ensureReadIndexes(database, tableName, tableBaseName);
|
|
855
924
|
}
|
|
856
925
|
|
|
857
926
|
/**
|
|
@@ -871,6 +940,21 @@ function ensureUniqueIndexes(database: AgentsServerSqliteDatabase, tableName: st
|
|
|
871
940
|
}
|
|
872
941
|
}
|
|
873
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Creates known read indexes after required columns exist.
|
|
945
|
+
*/
|
|
946
|
+
function ensureReadIndexes(database: AgentsServerSqliteDatabase, tableName: string, tableBaseName: string): void {
|
|
947
|
+
const readIndexes = READ_INDEXES_BY_TABLE.get(tableBaseName) || [];
|
|
948
|
+
|
|
949
|
+
for (const readIndex of readIndexes) {
|
|
950
|
+
const indexName = `index_${sanitizeSqlIdentifier(tableName)}_${sanitizeSqlIdentifier(readIndex.name)}`;
|
|
951
|
+
const columnSql = readIndex.columns.map(quoteIdentifier).join(', ');
|
|
952
|
+
database.exec(
|
|
953
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} (${columnSql})`,
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
874
958
|
/**
|
|
875
959
|
* Inserts one row into the table.
|
|
876
960
|
*/
|
|
@@ -1441,6 +1525,13 @@ function resolveUniqueIndexColumns(tableBaseName: string): Array<string> {
|
|
|
1441
1525
|
return (UNIQUE_INDEX_COLUMNS_BY_TABLE.get(tableBaseName) || []).flatMap((columns) => [...columns]);
|
|
1442
1526
|
}
|
|
1443
1527
|
|
|
1528
|
+
/**
|
|
1529
|
+
* Resolves columns participating in known read indexes.
|
|
1530
|
+
*/
|
|
1531
|
+
function resolveReadIndexColumns(tableBaseName: string): Array<string> {
|
|
1532
|
+
return (READ_INDEXES_BY_TABLE.get(tableBaseName) || []).flatMap((readIndex) => [...readIndex.columns]);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1444
1535
|
/**
|
|
1445
1536
|
* Resolves upsert conflict columns.
|
|
1446
1537
|
*/
|