@promptbook/cli 0.112.0-117 → 0.112.0-119
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/agents/[agentName]/chat/AgentChatSidebarDefault.tsx +5 -6
- package/apps/agents-server/src/app/api/page-preview/check/route.ts +31 -0
- package/apps/agents-server/src/app/api/page-preview/screenshot/route.ts +57 -0
- package/apps/agents-server/src/app/api/upload/route.ts +10 -1
- package/apps/agents-server/src/app/s3/[first]/[second]/[hash]/[filename]/route.ts +52 -0
- package/apps/agents-server/src/database/$provideClientSql.ts +37 -0
- package/apps/agents-server/src/database/$provideSupabaseForServer.ts +41 -0
- package/apps/agents-server/src/tools/$provideCdnForServer.ts +24 -0
- package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +30 -2
- package/apps/agents-server/src/utils/cdn/utils/getUserFileCdnKey.ts +10 -3
- package/apps/agents-server/src/utils/externalChatRunner/processExternalUserChatJob.ts +17 -7
- package/apps/agents-server/src/utils/iframe/checkIfUrlCanBeEmbedded.ts +68 -0
- package/apps/agents-server/src/utils/localChatRunner/processLocalUserChatJob.ts +17 -7
- package/apps/agents-server/src/utils/userChat/createImmediateUserChatAnswerModelRequirements.ts +11 -0
- package/apps/agents-server/src/utils/userChat/listUserChats.ts +5 -7
- package/esm/index.es.js +442 -66
- package/esm/index.es.js.map +1 -1
- package/esm/scripts/run-codex-prompts/common/parseDuration.d.ts +19 -0
- package/esm/src/_packages/node.index.d.ts +10 -0
- package/esm/src/book-3.0/BookNodeAgentSource.d.ts +1 -1
- package/esm/src/book-3.0/CliAgent.d.ts +7 -2
- package/esm/src/book-3.0/cliAgentEnv.d.ts +33 -0
- package/esm/src/book-components/BookEditor/BookEditor.d.ts +1 -1
- package/esm/src/book-components/BookEditor/BookEditorForClient.d.ts +1 -1
- package/esm/src/book-components/Chat/Chat/CitationIframePreview.d.ts +20 -0
- package/esm/src/book-components/_common/Dropdown/Dropdown.d.ts +1 -1
- package/esm/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +1 -1
- package/esm/src/book-components/_common/Modal/Modal.d.ts +1 -1
- package/esm/src/book-components/icons/AboutIcon.d.ts +1 -1
- package/esm/src/book-components/icons/DownloadIcon.d.ts +1 -1
- package/esm/src/book-components/icons/ExitFullscreenIcon.d.ts +1 -1
- package/esm/src/book-components/icons/FullscreenIcon.d.ts +1 -1
- package/esm/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +2 -18
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/_packages/node.index.ts +10 -0
- package/src/avatars/avatarAnimationScheduler.ts +33 -2
- package/src/avatars/visuals/fractalAvatarVisual.ts +5 -4
- package/src/avatars/visuals/minecraft2AvatarVisual.ts +16 -11
- package/src/avatars/visuals/minecraftAvatarVisual.ts +21 -7
- package/src/avatars/visuals/octopus3d2AvatarVisual.ts +69 -17
- package/src/avatars/visuals/octopus3d3AvatarVisual.ts +81 -18
- package/src/avatars/visuals/octopus3dAvatarVisual.ts +69 -17
- package/src/book-3.0/Book.ts +3 -1
- package/src/book-3.0/BookNodeAgentSource.ts +2 -2
- package/src/book-3.0/CliAgent.ts +84 -6
- package/src/book-3.0/LiteAgent.ts +1 -1
- package/src/book-3.0/cliAgentEnv.ts +46 -0
- package/src/book-components/BookEditor/BookEditor.tsx +6 -6
- package/src/book-components/BookEditor/BookEditorForClient.tsx +1 -1
- package/src/book-components/Chat/Chat/Chat.module.css +45 -0
- package/src/book-components/Chat/Chat/ChatCitationModal.tsx +2 -2
- package/src/book-components/Chat/Chat/CitationIframePreview.tsx +83 -0
- package/src/book-components/_common/Dropdown/Dropdown.tsx +1 -1
- package/src/book-components/_common/MenuHoisting/MenuHoistingContext.tsx +1 -1
- package/src/book-components/_common/Modal/Modal.tsx +1 -1
- package/src/book-components/icons/AboutIcon.tsx +1 -1
- package/src/book-components/icons/DownloadIcon.tsx +1 -1
- package/src/book-components/icons/ExitFullscreenIcon.tsx +1 -1
- package/src/book-components/icons/FullscreenIcon.tsx +1 -1
- package/src/cli/cli-commands/agents-server/buildAgentsServer.ts +31 -1
- package/src/cli/cli-commands/coder/run.ts +28 -3
- package/src/cli/cli-commands/common/promptRunnerCliOptions.ts +9 -29
- package/src/execution/createPipelineExecutor/getKnowledgeForTask.ts +1 -1
- package/src/llm-providers/openai/OpenAiAgentKitExecutionToolsToolBuilder.ts +1 -1
- package/src/llm-providers/openai/OpenAiAssistantExecutionToolsToolRunner.ts +1 -1
- package/src/llm-providers/openai/OpenAiVectorStoreKnowledgeSourcePreparer.ts +1 -1
- package/src/other/templates/getTemplatesPipelineCollection.ts +734 -711
- package/src/scripting/javascript/JavascriptEvalExecutionTools.ts +1 -1
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/index.umd.js +442 -66
- package/umd/index.umd.js.map +1 -1
- package/umd/scripts/run-codex-prompts/common/parseDuration.d.ts +19 -0
- package/umd/src/_packages/node.index.d.ts +10 -0
- package/umd/src/book-3.0/BookNodeAgentSource.d.ts +1 -1
- package/umd/src/book-3.0/CliAgent.d.ts +7 -2
- package/umd/src/book-3.0/cliAgentEnv.d.ts +33 -0
- package/umd/src/book-components/BookEditor/BookEditor.d.ts +1 -1
- package/umd/src/book-components/BookEditor/BookEditorForClient.d.ts +1 -1
- package/umd/src/book-components/Chat/Chat/CitationIframePreview.d.ts +20 -0
- package/umd/src/book-components/_common/Dropdown/Dropdown.d.ts +1 -1
- package/umd/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +1 -1
- package/umd/src/book-components/_common/Modal/Modal.d.ts +1 -1
- package/umd/src/book-components/icons/AboutIcon.d.ts +1 -1
- package/umd/src/book-components/icons/DownloadIcon.d.ts +1 -1
- package/umd/src/book-components/icons/ExitFullscreenIcon.d.ts +1 -1
- package/umd/src/book-components/icons/FullscreenIcon.d.ts +1 -1
- package/umd/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +2 -18
- package/umd/src/version.d.ts +1 -1
|
@@ -168,10 +168,7 @@ function resolveAgentChatSidebarExpandedStatusClassName(activityKind: AgentChatS
|
|
|
168
168
|
*
|
|
169
169
|
* @private function of AgentChatSidebar
|
|
170
170
|
*/
|
|
171
|
-
function AgentChatSidebarDefaultCollapsedRow({
|
|
172
|
-
item,
|
|
173
|
-
onChatSelect,
|
|
174
|
-
}: AgentChatSidebarDefaultCollapsedRowProps) {
|
|
171
|
+
function AgentChatSidebarDefaultCollapsedRow({ item, onChatSelect }: AgentChatSidebarDefaultCollapsedRowProps) {
|
|
175
172
|
const statusClassName = resolveAgentChatSidebarCollapsedStatusClassName(
|
|
176
173
|
item.content.activityIndicator.kind,
|
|
177
174
|
item.isActive,
|
|
@@ -444,7 +441,9 @@ function AgentChatSidebarDefaultCollapsedSection({
|
|
|
444
441
|
) : (
|
|
445
442
|
<div className="flex min-h-0 w-full flex-1 flex-col gap-2 overflow-y-auto scrollbar-hidden">
|
|
446
443
|
{sidebarItems.length === 0 ? (
|
|
447
|
-
<p className="px-1 text-center text-[11px] text-slate-500 dark:text-slate-400">
|
|
444
|
+
<p className="px-1 text-center text-[11px] text-slate-500 dark:text-slate-400">
|
|
445
|
+
{emptyStateText}
|
|
446
|
+
</p>
|
|
448
447
|
) : (
|
|
449
448
|
sidebarItems.map((item) => (
|
|
450
449
|
<AgentChatSidebarDefaultCollapsedRow
|
|
@@ -600,7 +599,7 @@ export function AgentChatSidebarDefault({
|
|
|
600
599
|
</div>
|
|
601
600
|
</div>
|
|
602
601
|
|
|
603
|
-
<div className="agent-chat-sidebar-toggle-slot pointer-events-none absolute inset-y-0 right-0 z-10 hidden translate-x-1
|
|
602
|
+
<div className="agent-chat-sidebar-toggle-slot pointer-events-none absolute inset-y-0 right-0 z-10 hidden translate-x-1 items-center justify-center md:flex">
|
|
604
603
|
<SolidArrowButton
|
|
605
604
|
direction={isCollapsed ? 'right' : 'left'}
|
|
606
605
|
onClick={onToggleCollapse}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { NextRequest } from 'next/server';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { assertsError } from '../../../../../../../src/errors/assertsError';
|
|
4
|
+
import { checkIfUrlCanBeEmbedded } from '../../../../utils/iframe/checkIfUrlCanBeEmbedded';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Checks whether a given URL can be embedded in an iframe by inspecting
|
|
8
|
+
* `X-Frame-Options` and `Content-Security-Policy` `frame-ancestors` headers.
|
|
9
|
+
*
|
|
10
|
+
* Query parameters:
|
|
11
|
+
* - `url` — the fully-qualified HTTP(S) URL to check
|
|
12
|
+
*
|
|
13
|
+
* Returns `{ canEmbed: boolean }`.
|
|
14
|
+
*/
|
|
15
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
16
|
+
const url = request.nextUrl.searchParams.get('url');
|
|
17
|
+
|
|
18
|
+
if (!url) {
|
|
19
|
+
return NextResponse.json({ error: 'Missing required query parameter: url' }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const canEmbed = await checkIfUrlCanBeEmbedded(url);
|
|
24
|
+
return NextResponse.json({ canEmbed });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
assertsError(error);
|
|
27
|
+
console.warn('Failed to check if URL can be embedded:', error.message);
|
|
28
|
+
// When the check fails, allow the iframe to try — the browser will handle it
|
|
29
|
+
return NextResponse.json({ canEmbed: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { $provideBrowserForServer } from '@/src/tools/$provideBrowserForServer';
|
|
2
|
+
import { serializeError } from '@promptbook-local/utils';
|
|
3
|
+
import type { NextRequest } from 'next/server';
|
|
4
|
+
import { NextResponse } from 'next/server';
|
|
5
|
+
import { assertsError } from '../../../../../../../src/errors/assertsError';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Takes a screenshot of the given URL using a headless browser.
|
|
9
|
+
*
|
|
10
|
+
* Query parameters:
|
|
11
|
+
* - `url` — the fully-qualified HTTP(S) URL to screenshot
|
|
12
|
+
*
|
|
13
|
+
* Returns a PNG image.
|
|
14
|
+
*/
|
|
15
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
16
|
+
const url = request.nextUrl.searchParams.get('url');
|
|
17
|
+
|
|
18
|
+
if (!url) {
|
|
19
|
+
return NextResponse.json({ error: 'Missing required query parameter: url' }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let parsedUrl: URL;
|
|
23
|
+
try {
|
|
24
|
+
parsedUrl = new URL(url);
|
|
25
|
+
} catch {
|
|
26
|
+
return NextResponse.json({ error: 'Invalid URL' }, { status: 400 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
30
|
+
return NextResponse.json({ error: 'Only http and https URLs are supported' }, { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const browserContext = await $provideBrowserForServer();
|
|
35
|
+
const page = await browserContext.newPage();
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await page.setViewportSize({ width: 1280, height: 800 });
|
|
39
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
40
|
+
const screenshotBuffer = await page.screenshot({ type: 'png' });
|
|
41
|
+
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
return new NextResponse(new Blob([screenshotBuffer as any]), {
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'image/png',
|
|
46
|
+
'Cache-Control': 'public, max-age=300',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
} finally {
|
|
50
|
+
await page.close().catch(() => {});
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
assertsError(error);
|
|
54
|
+
console.error('Error taking page screenshot:', error);
|
|
55
|
+
return NextResponse.json({ error: serializeError(error) }, { status: 500 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from '../../../tools/$provideCdnForServer';
|
|
16
16
|
import { $provideServer } from '../../../tools/$provideServer';
|
|
17
17
|
import { getSafeCdnPath } from '../../../utils/cdn/utils/getSafeCdnPath';
|
|
18
|
+
import { getUserFileCdnKey } from '../../../utils/cdn/utils/getUserFileCdnKey';
|
|
18
19
|
import { FILE_SECURITY_CHECKERS } from '../../../file-security-checkers';
|
|
19
20
|
import { getUserIdFromRequest } from '../../../utils/getUserIdFromRequest';
|
|
20
21
|
import { getMaxFileUploadSizeBytes } from '../../../utils/serverLimits';
|
|
@@ -223,7 +224,7 @@ export async function POST(request: NextRequest) {
|
|
|
223
224
|
const providedServer = await $provideServer();
|
|
224
225
|
assertFileUploadAvailable(providedServer);
|
|
225
226
|
|
|
226
|
-
const { file,
|
|
227
|
+
const { file, purpose, contentType } = await parseUploadRequest(request);
|
|
227
228
|
const fileBuffer = Buffer.from(await file.arrayBuffer());
|
|
228
229
|
const maxFileSize = await getMaxFileUploadSizeBytes();
|
|
229
230
|
|
|
@@ -237,6 +238,14 @@ export async function POST(request: NextRequest) {
|
|
|
237
238
|
);
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
// [✨🏣] Compute a content-addressed CDN key so the public URL contains
|
|
242
|
+
// the file hash and does not expose the internal S3 bucket or path prefix.
|
|
243
|
+
const rawKey = getUserFileCdnKey(fileBuffer, file.name);
|
|
244
|
+
const pathname = getSafeCdnPath({
|
|
245
|
+
pathname: rawKey,
|
|
246
|
+
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX,
|
|
247
|
+
});
|
|
248
|
+
|
|
240
249
|
const cdn = $provideCdnForServer({
|
|
241
250
|
cdnPublicUrl: resolveCdnPublicUrlForServer(providedServer.publicUrl),
|
|
242
251
|
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import {
|
|
3
|
+
$provideCdnForServer,
|
|
4
|
+
resolveCdnPublicUrlForServer,
|
|
5
|
+
} from '../../../../../../tools/$provideCdnForServer';
|
|
6
|
+
import { $provideServer } from '../../../../../../tools/$provideServer';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Serves files stored under the hash-based CDN key format:
|
|
10
|
+
* `{hash[0]}/{hash[1]}/{sha256-hash}/{filename}`
|
|
11
|
+
*
|
|
12
|
+
* This route is reached when nginx routes a request matching
|
|
13
|
+
* `^/s3/[0-9a-f]/[0-9a-f]/[0-9a-f]{64}/` to Next.js instead of VersityGW,
|
|
14
|
+
* which keeps the internal S3 bucket and path prefix hidden from the public URL.
|
|
15
|
+
*
|
|
16
|
+
* [✨🏣] Companion route for `getUserFileCdnKey` and the `publicCdnBaseUrl`
|
|
17
|
+
* configuration in `$provideCdnForServer`.
|
|
18
|
+
*/
|
|
19
|
+
export async function GET(
|
|
20
|
+
_request: NextRequest,
|
|
21
|
+
{
|
|
22
|
+
params,
|
|
23
|
+
}: {
|
|
24
|
+
params: Promise<{
|
|
25
|
+
first: string;
|
|
26
|
+
second: string;
|
|
27
|
+
hash: string;
|
|
28
|
+
filename: string;
|
|
29
|
+
}>;
|
|
30
|
+
},
|
|
31
|
+
) {
|
|
32
|
+
const { first, second, hash, filename } = await params;
|
|
33
|
+
const key = `${first}/${second}/${hash}/${filename}`;
|
|
34
|
+
|
|
35
|
+
const providedServer = await $provideServer();
|
|
36
|
+
const cdn = $provideCdnForServer({
|
|
37
|
+
cdnPublicUrl: resolveCdnPublicUrlForServer(providedServer.publicUrl),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const file = await cdn.getItem(key);
|
|
41
|
+
|
|
42
|
+
if (!file) {
|
|
43
|
+
return new NextResponse(null, { status: 404 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return new NextResponse(file.data, {
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': file.type,
|
|
49
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -31,6 +31,33 @@ export type ClientSqlExecutor = ClientSql & {
|
|
|
31
31
|
readonly raw: ClientSqlRaw;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Maximum number of PostgreSQL connections in the shared pool.
|
|
36
|
+
*
|
|
37
|
+
* Configurable via `DATABASE_POOL_MAX` environment variable.
|
|
38
|
+
* Defaults to 20 which comfortably handles concurrent chat-stream polling loads.
|
|
39
|
+
*
|
|
40
|
+
* @private internal constant of Agents Server database layer
|
|
41
|
+
*/
|
|
42
|
+
const DATABASE_POOL_MAX = Math.max(1, parseInt(process.env.DATABASE_POOL_MAX || '20', 10));
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Milliseconds to wait for a free pool connection before failing.
|
|
46
|
+
*
|
|
47
|
+
* Without a timeout the default `pg` behaviour is to queue requests indefinitely,
|
|
48
|
+
* which causes the server to become unresponsive under pool exhaustion.
|
|
49
|
+
*
|
|
50
|
+
* @private internal constant of Agents Server database layer
|
|
51
|
+
*/
|
|
52
|
+
const POOL_CONNECTION_TIMEOUT_MS = 15_000;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Milliseconds an idle client stays in the pool before being closed.
|
|
56
|
+
*
|
|
57
|
+
* @private internal constant of Agents Server database layer
|
|
58
|
+
*/
|
|
59
|
+
const POOL_IDLE_TIMEOUT_MS = 30_000;
|
|
60
|
+
|
|
34
61
|
/**
|
|
35
62
|
* Shared PostgreSQL pool reused across all requests in the server process.
|
|
36
63
|
*
|
|
@@ -52,6 +79,16 @@ export async function $provideClientSql(): Promise<ClientSqlExecutor> {
|
|
|
52
79
|
clientPool = new Pool({
|
|
53
80
|
connectionString: resolvePostgresConnectionString(),
|
|
54
81
|
ssl: { rejectUnauthorized: false },
|
|
82
|
+
max: DATABASE_POOL_MAX,
|
|
83
|
+
idleTimeoutMillis: POOL_IDLE_TIMEOUT_MS,
|
|
84
|
+
connectionTimeoutMillis: POOL_CONNECTION_TIMEOUT_MS,
|
|
85
|
+
allowExitOnIdle: true,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Prevent unhandled errors from crashing the pool or the process.
|
|
89
|
+
// Individual query errors are still thrown at the call site.
|
|
90
|
+
clientPool.on('error', (error) => {
|
|
91
|
+
console.error('[database] Unexpected error on idle PostgreSQL client:', error);
|
|
55
92
|
});
|
|
56
93
|
}
|
|
57
94
|
|
|
@@ -4,6 +4,44 @@ import { isAgentsServerSqliteMode } from './agentsServerDatabaseMode';
|
|
|
4
4
|
import { $provideLocalSqliteSupabase } from './sqlite/$provideLocalSqliteSupabase';
|
|
5
5
|
import { AgentsServerDatabase } from './schema';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Hard timeout for every Supabase HTTP request.
|
|
9
|
+
*
|
|
10
|
+
* Without this, a slow or unreachable PostgREST endpoint causes requests to
|
|
11
|
+
* hang indefinitely, eventually making the server appear unresponsive.
|
|
12
|
+
*
|
|
13
|
+
* @private internal constant of Agents Server database layer
|
|
14
|
+
*/
|
|
15
|
+
const SUPABASE_REQUEST_TIMEOUT_MS = 30_000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wraps the native `fetch` with a per-request timeout so Supabase queries
|
|
19
|
+
* always resolve (with an error) within `SUPABASE_REQUEST_TIMEOUT_MS`.
|
|
20
|
+
*
|
|
21
|
+
* When the caller already passes an AbortSignal the request is aborted when
|
|
22
|
+
* either that signal fires or the timeout fires — whichever comes first.
|
|
23
|
+
*
|
|
24
|
+
* Uses manual composition instead of `AbortSignal.any` for Node.js 18 compatibility
|
|
25
|
+
* (`AbortSignal.any` is only available in Node.js 20.3+).
|
|
26
|
+
*
|
|
27
|
+
* @private internal helper of Agents Server database layer
|
|
28
|
+
*/
|
|
29
|
+
function fetchWithSupabaseTimeout(url: RequestInfo | URL, options?: RequestInit): Promise<Response> {
|
|
30
|
+
const existingSignal = options?.signal instanceof AbortSignal ? options.signal : null;
|
|
31
|
+
|
|
32
|
+
if (!existingSignal) {
|
|
33
|
+
return fetch(url, { ...options, signal: AbortSignal.timeout(SUPABASE_REQUEST_TIMEOUT_MS) });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Compose the caller signal with the timeout: abort when either fires.
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const abort = () => controller.abort();
|
|
39
|
+
existingSignal.addEventListener('abort', abort, { once: true });
|
|
40
|
+
AbortSignal.timeout(SUPABASE_REQUEST_TIMEOUT_MS).addEventListener('abort', abort, { once: true });
|
|
41
|
+
|
|
42
|
+
return fetch(url, { ...options, signal: controller.signal });
|
|
43
|
+
}
|
|
44
|
+
|
|
7
45
|
/**
|
|
8
46
|
* Internal cache for `$provideSupabaseForServer`
|
|
9
47
|
*
|
|
@@ -42,6 +80,9 @@ export function $provideSupabaseForServer(): SupabaseClient<AgentsServerDatabase
|
|
|
42
80
|
autoRefreshToken: false,
|
|
43
81
|
persistSession: false,
|
|
44
82
|
},
|
|
83
|
+
global: {
|
|
84
|
+
fetch: fetchWithSupabaseTimeout,
|
|
85
|
+
},
|
|
45
86
|
},
|
|
46
87
|
);
|
|
47
88
|
}
|
|
@@ -74,6 +74,13 @@ function createCdnStorageForServer(cdnPublicUrl: URL): IIFilesStorageWithCdn {
|
|
|
74
74
|
gzip: true,
|
|
75
75
|
forcePathStyle: process.env.CDN_FORCE_PATH_STYLE === 'true',
|
|
76
76
|
region: resolveS3CompatibleStorageRegion(),
|
|
77
|
+
// [✨🏣] For self-contained S3: hash-based CDN keys get a public URL that
|
|
78
|
+
// omits the internal bucket and pathPrefix so users can't infer the storage
|
|
79
|
+
// structure. The `/s3/` segment is preserved so nginx routes those requests
|
|
80
|
+
// to the Next.js hash-file route instead of directly to VersityGW.
|
|
81
|
+
publicCdnBaseUrl: isSelfContainedS3StorageSelected()
|
|
82
|
+
? resolveHashBasedPublicCdnBaseUrl(cdnPublicUrl)
|
|
83
|
+
: undefined,
|
|
77
84
|
});
|
|
78
85
|
}
|
|
79
86
|
|
|
@@ -84,6 +91,23 @@ function createCdnStorageForServer(cdnPublicUrl: URL): IIFilesStorageWithCdn {
|
|
|
84
91
|
});
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Derives the public base URL used for hash-based file URLs on self-contained S3.
|
|
96
|
+
*
|
|
97
|
+
* Strips the bucket segment from the configured CDN URL so that the public URL
|
|
98
|
+
* exposes only the `/s3/` prefix — the internal bucket name and path prefix
|
|
99
|
+
* remain hidden from users.
|
|
100
|
+
*
|
|
101
|
+
* Example: `https://s24.ptbk.io/s3/promptbook-files/` → `https://s24.ptbk.io/s3/`
|
|
102
|
+
*
|
|
103
|
+
* [✨🏣] Companion to `isHashBasedCdnKey` in `DigitalOceanSpaces`.
|
|
104
|
+
*
|
|
105
|
+
* @private helper of `createCdnStorageForServer`
|
|
106
|
+
*/
|
|
107
|
+
function resolveHashBasedPublicCdnBaseUrl(cdnPublicUrl: URL): URL {
|
|
108
|
+
return new URL('/s3/', cdnPublicUrl);
|
|
109
|
+
}
|
|
110
|
+
|
|
87
111
|
/**
|
|
88
112
|
* Resolves the CDN public URL from environment configuration.
|
|
89
113
|
*
|
|
@@ -19,6 +19,16 @@ type IDigitalOceanSpacesConfig = {
|
|
|
19
19
|
readonly forcePathStyle?: boolean;
|
|
20
20
|
readonly region?: string;
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* When set, `getItemUrl` generates `new URL(key, publicCdnBaseUrl)` for
|
|
24
|
+
* hash-based CDN keys instead of the default formula that prepends the
|
|
25
|
+
* `pathPrefix`. This allows self-contained S3 to expose an unguessable URL
|
|
26
|
+
* that hides the internal bucket name and path prefix.
|
|
27
|
+
*
|
|
28
|
+
* [✨🏣] Used by self-contained S3 to serve files via the Next.js hash route.
|
|
29
|
+
*/
|
|
30
|
+
readonly publicCdnBaseUrl?: URL;
|
|
31
|
+
|
|
22
32
|
// TODO: [⛳️] Probbably prefix should be in this config not on the consumer side
|
|
23
33
|
};
|
|
24
34
|
|
|
@@ -27,7 +37,7 @@ type IDigitalOceanSpacesConfig = {
|
|
|
27
37
|
*/
|
|
28
38
|
export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
29
39
|
public get cdnPublicUrl() {
|
|
30
|
-
return this.config.cdnPublicUrl;
|
|
40
|
+
return this.config.publicCdnBaseUrl ?? this.config.cdnPublicUrl;
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
private s3: S3Client;
|
|
@@ -45,7 +55,13 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
public getItemUrl(key: string): URL {
|
|
48
|
-
|
|
58
|
+
// [✨🏣] For hash-based keys, omit the pathPrefix from the public URL so
|
|
59
|
+
// the internal S3 structure (bucket, prefix, upload directory) is hidden.
|
|
60
|
+
if (this.config.publicCdnBaseUrl && isHashBasedCdnKey(key)) {
|
|
61
|
+
return new URL(key, this.config.publicCdnBaseUrl);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return new URL(this.config.pathPrefix + '/' + key, this.config.cdnPublicUrl);
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
public async getItem(key: string): Promise<IFile | null> {
|
|
@@ -122,6 +138,18 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Returns `true` when `key` matches the hash-based CDN key format produced by
|
|
143
|
+
* `getUserFileCdnKey`: `{hex}/{hex}/{sha256-64-chars}/{filename}`.
|
|
144
|
+
*
|
|
145
|
+
* [✨🏣] Used by `getItemUrl` to decide whether to strip the internal path prefix.
|
|
146
|
+
*
|
|
147
|
+
* @private helper of `DigitalOceanSpaces`
|
|
148
|
+
*/
|
|
149
|
+
function isHashBasedCdnKey(key: string): boolean {
|
|
150
|
+
return /^[0-9a-f]\/[0-9a-f]\/[0-9a-f]{64}\//.test(key);
|
|
151
|
+
}
|
|
152
|
+
|
|
125
153
|
/**
|
|
126
154
|
* Normalizes endpoint values from legacy host-only configuration and full S3 URLs.
|
|
127
155
|
*
|
|
@@ -2,10 +2,15 @@ import hexEncoder from 'crypto-js/enc-hex';
|
|
|
2
2
|
import sha256 from 'crypto-js/sha256';
|
|
3
3
|
import type { string_uri } from '../../../../../../src/types/typeAliases';
|
|
4
4
|
import { titleToName } from '../../../../../../src/utils/normalization/titleToName';
|
|
5
|
-
import { nameToSubfolderPath } from './nameToSubfolderPath';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
|
-
* Generates a path for
|
|
7
|
+
* Generates a content-addressed path for user-uploaded files.
|
|
8
|
+
*
|
|
9
|
+
* The returned key uses the SHA-256 hash of the file buffer so the URL is
|
|
10
|
+
* unguessable and does not expose internal storage structure (bucket, prefix,
|
|
11
|
+
* or upload directory). The first two hex characters are used as single-level
|
|
12
|
+
* shard directories, matching the same convention as the public URL served by
|
|
13
|
+
* the hash-based file route.
|
|
9
14
|
*/
|
|
10
15
|
export function getUserFileCdnKey(file: Buffer, originalFilename: string): string_uri {
|
|
11
16
|
const hash = sha256(hexEncoder.parse(file.toString('hex'))).toString(/* hex */);
|
|
@@ -18,7 +23,9 @@ export function getUserFileCdnKey(file: Buffer, originalFilename: string): strin
|
|
|
18
23
|
const filename = name + '.' + extension;
|
|
19
24
|
// <- Note: [⛳️] Preserving original file name
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
// [✨🏣] Hash-based key: {hash[0]}/{hash[1]}/{fullHash}/{filename}
|
|
27
|
+
// The single-char shard dirs and full hash hide the internal S3 structure.
|
|
28
|
+
return `${hash[0]}/${hash[1]}/${hash}/${filename}`;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
// TODO: [🌍] Unite this logic in one place
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Json } from '@/src/database/schema';
|
|
2
|
+
import { parseAgentSource } from '../../../../../src/book-2.0/agent-source/parseAgentSource';
|
|
2
3
|
import { createUserChatJobFailureDetails } from '../userChat/createUserChatJobFailureDetails';
|
|
3
4
|
import { claimNextQueuedUserChatJob } from '../userChat/claimNextQueuedUserChatJob';
|
|
4
5
|
import { finalizeUserChatJob } from '../userChat/finalizeUserChatJob';
|
|
@@ -148,13 +149,22 @@ async function enqueueExternalUserChatJob(job: UserChatJobRecord): Promise<Proce
|
|
|
148
149
|
return { didMutate: false, outcome: 'waiting' };
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
const
|
|
152
|
-
.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
152
|
+
const agentInitialMessage = previousThreadMessages.length === 0
|
|
153
|
+
? parseAgentSource(agentSourceSnapshot.agentSource).initialMessage
|
|
154
|
+
: null;
|
|
155
|
+
const initialAgentThreadMessages = agentInitialMessage
|
|
156
|
+
? [{ sender: 'AGENT' as const, content: agentInitialMessage }]
|
|
157
|
+
: [];
|
|
158
|
+
|
|
159
|
+
const threadMessages = [
|
|
160
|
+
...initialAgentThreadMessages,
|
|
161
|
+
...[...previousThreadMessages, userMessage]
|
|
162
|
+
.filter((message) => message.isComplete !== false)
|
|
163
|
+
.filter((message) => message.sender === 'USER' || message.sender === 'AGENT'),
|
|
164
|
+
].map((message) => ({
|
|
165
|
+
sender: String(message.sender),
|
|
166
|
+
content: message.content,
|
|
167
|
+
}));
|
|
158
168
|
|
|
159
169
|
const repository = await ensureExternalAgentRepository(agentSourceSnapshot);
|
|
160
170
|
const queuedAt = new Date().toISOString();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses the X-Frame-Options header value and returns whether embedding is allowed.
|
|
3
|
+
*
|
|
4
|
+
* Some servers set multiple values (e.g. "DENY, SAMEORIGIN") which results in a
|
|
5
|
+
* comma-separated string. Any DENY or SAMEORIGIN token blocks embedding.
|
|
6
|
+
*/
|
|
7
|
+
function canEmbedByXFrameOptions(headerValue: string): boolean {
|
|
8
|
+
const values = headerValue
|
|
9
|
+
.split(',')
|
|
10
|
+
.map((v) => v.trim().toUpperCase());
|
|
11
|
+
return !values.some((v) => v === 'DENY' || v === 'SAMEORIGIN');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parses the Content-Security-Policy header and checks the `frame-ancestors` directive.
|
|
16
|
+
*
|
|
17
|
+
* Returns false when `frame-ancestors` is present and does not include a wildcard or
|
|
18
|
+
* protocol-level allow (`*`, `https:`, `http:`).
|
|
19
|
+
*/
|
|
20
|
+
function canEmbedByCsp(cspHeader: string): boolean {
|
|
21
|
+
const match = cspHeader.match(/frame-ancestors\s+([^;]+)/i);
|
|
22
|
+
if (!match) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const directive = (match[1] ?? '').trim();
|
|
27
|
+
const tokens = directive.split(/\s+/);
|
|
28
|
+
|
|
29
|
+
for (const token of tokens) {
|
|
30
|
+
if (token === '*' || token === 'https:' || token === 'http:') {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Checks whether a given HTTP(S) URL allows being embedded in an iframe by
|
|
40
|
+
* inspecting `X-Frame-Options` and `Content-Security-Policy` `frame-ancestors`.
|
|
41
|
+
*
|
|
42
|
+
* Returns `true` when the page is embeddable, `false` when it is blocked.
|
|
43
|
+
* Throws when the URL is not HTTP(S) or the network request fails.
|
|
44
|
+
*/
|
|
45
|
+
export async function checkIfUrlCanBeEmbedded(url: string): Promise<boolean> {
|
|
46
|
+
const parsedUrl = new URL(url);
|
|
47
|
+
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
48
|
+
throw new Error(`Only http and https URLs are supported, got: ${parsedUrl.protocol}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const response = await fetch(url, {
|
|
52
|
+
method: 'HEAD',
|
|
53
|
+
signal: AbortSignal.timeout(10_000),
|
|
54
|
+
redirect: 'follow',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const xFrameOptions = response.headers.get('X-Frame-Options');
|
|
58
|
+
if (xFrameOptions !== null && !canEmbedByXFrameOptions(xFrameOptions)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const csp = response.headers.get('Content-Security-Policy');
|
|
63
|
+
if (csp !== null && !canEmbedByCsp(csp)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Json } from '@/src/database/schema';
|
|
2
2
|
import { mkdir, readFile, rm, writeFile } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
+
import { parseAgentSource } from '../../../../../src/book-2.0/agent-source/parseAgentSource';
|
|
4
5
|
import { createUserChatJobFailureDetails } from '../userChat/createUserChatJobFailureDetails';
|
|
5
6
|
import { claimNextQueuedUserChatJob } from '../userChat/claimNextQueuedUserChatJob';
|
|
6
7
|
import { finalizeUserChatJob } from '../userChat/finalizeUserChatJob';
|
|
@@ -145,13 +146,22 @@ async function enqueueLocalUserChatJob(job: UserChatJobRecord): Promise<ProcessL
|
|
|
145
146
|
return { didMutate: false, outcome: 'waiting' };
|
|
146
147
|
}
|
|
147
148
|
|
|
148
|
-
const
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
const agentInitialMessage = previousThreadMessages.length === 0
|
|
150
|
+
? parseAgentSource(agentSourceSnapshot.agentSource).initialMessage
|
|
151
|
+
: null;
|
|
152
|
+
const initialAgentThreadMessages = agentInitialMessage
|
|
153
|
+
? [{ sender: 'AGENT' as const, content: agentInitialMessage }]
|
|
154
|
+
: [];
|
|
155
|
+
|
|
156
|
+
const threadMessages = [
|
|
157
|
+
...initialAgentThreadMessages,
|
|
158
|
+
...[...previousThreadMessages, userMessage]
|
|
159
|
+
.filter((message) => message.isComplete !== false)
|
|
160
|
+
.filter((message) => message.sender === 'USER' || message.sender === 'AGENT'),
|
|
161
|
+
].map((message) => ({
|
|
162
|
+
sender: String(message.sender),
|
|
163
|
+
content: message.content,
|
|
164
|
+
}));
|
|
155
165
|
|
|
156
166
|
const agentFolder = await ensureLocalAgentFolder(agentSourceSnapshot);
|
|
157
167
|
const queuedAt = new Date().toISOString();
|
package/apps/agents-server/src/utils/userChat/createImmediateUserChatAnswerModelRequirements.ts
CHANGED
|
@@ -29,7 +29,9 @@ const IMMEDIATE_USER_CHAT_ANSWER_INSTRUCTION_COMMITMENTS = new Set<string>([
|
|
|
29
29
|
* Prefix added to the immediate pre-answer system message.
|
|
30
30
|
*/
|
|
31
31
|
const IMMEDIATE_USER_CHAT_ANSWER_SYSTEM_PREAMBLE = spaceTrim(`
|
|
32
|
+
|
|
32
33
|
You are preparing a short in-progress confirmation for the user while a slower full agent run continues separately.
|
|
34
|
+
|
|
33
35
|
This response is not the final answer. It is only a confirmation that the task is being handled.
|
|
34
36
|
These immediate-answer rules override any agent instruction below that would make the answer sound final or complete.
|
|
35
37
|
|
|
@@ -37,6 +39,7 @@ const IMMEDIATE_USER_CHAT_ANSWER_SYSTEM_PREAMBLE = spaceTrim(`
|
|
|
37
39
|
- You understood what the user wants.
|
|
38
40
|
- You are working on it now or the job has already started.
|
|
39
41
|
- The final answer will arrive after the background processing finishes.
|
|
42
|
+
- You can use Markdown formatting in the messages like **bold** or *italic*
|
|
40
43
|
|
|
41
44
|
Keep the whole response short, preferably one or two sentences.
|
|
42
45
|
Do not provide any part of the final answer yet.
|
|
@@ -46,6 +49,14 @@ const IMMEDIATE_USER_CHAT_ANSWER_SYSTEM_PREAMBLE = spaceTrim(`
|
|
|
46
49
|
Do not use or claim to have used external tools, memory, knowledge bases, web browsing, search, calendar, email, projects, or teammate agents.
|
|
47
50
|
Never present this message as complete, definitive, or ready to use.
|
|
48
51
|
If the user asks for something that clearly requires unavailable capabilities, simply say the checked final answer is still being prepared.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
Example of a good immediate pre-answer response:
|
|
55
|
+
|
|
56
|
+
\`\`\`markdown
|
|
57
|
+
xxx
|
|
58
|
+
\`\`\`
|
|
59
|
+
|
|
49
60
|
`);
|
|
50
61
|
|
|
51
62
|
/**
|
|
@@ -40,7 +40,7 @@ const CLIENT_SQL_MISSING_CONNECTION_MESSAGE_FRAGMENT =
|
|
|
40
40
|
const SQLITE_CHAT_MESSAGES_JSON_EXPRESSION = `CASE WHEN json_valid(chat."messages") THEN chat."messages" ELSE '[]' END`;
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Lists all user chats for one agent ordered by
|
|
43
|
+
* Lists all user chats for one agent ordered by creation time (newest first).
|
|
44
44
|
*/
|
|
45
45
|
export async function listUserChats(options: ListUserChatsOptions): Promise<Array<UserChatRecord>> {
|
|
46
46
|
const { userId, viewerIsAdmin, agentPermanentId, includeExternalChats = false } = options;
|
|
@@ -53,9 +53,7 @@ export async function listUserChats(options: ListUserChatsOptions): Promise<Arra
|
|
|
53
53
|
.eq('userId', userId)
|
|
54
54
|
.eq('agentPermanentId', agentPermanentId)
|
|
55
55
|
.eq('source', USER_CHAT_SOURCES.WEB_UI);
|
|
56
|
-
const { data, error } = await query
|
|
57
|
-
.order('lastMessageAt', { ascending: false, nullsFirst: false })
|
|
58
|
-
.order('updatedAt', { ascending: false });
|
|
56
|
+
const { data, error } = await query.order('createdAt', { ascending: false });
|
|
59
57
|
|
|
60
58
|
if (error) {
|
|
61
59
|
throw new Error(`Failed to list user chats: ${error.message}`);
|
|
@@ -73,7 +71,7 @@ export async function listUserChats(options: ListUserChatsOptions): Promise<Arra
|
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
/**
|
|
76
|
-
* Lists lightweight chat-summary seeds without loading full `messages` JSON payloads.
|
|
74
|
+
* Lists lightweight chat-summary seeds without loading full `messages` JSON payloads, ordered by creation time (newest first).
|
|
77
75
|
*
|
|
78
76
|
* @private function of `userChat`
|
|
79
77
|
*/
|
|
@@ -149,7 +147,7 @@ export async function listUserChatSummarySeeds(options: ListUserChatsOptions): P
|
|
|
149
147
|
) AS "pendingAssistantMessageCount"
|
|
150
148
|
FROM ${userChatTableName}
|
|
151
149
|
WHERE ${whereClause}
|
|
152
|
-
ORDER BY "
|
|
150
|
+
ORDER BY "createdAt" DESC
|
|
153
151
|
`,
|
|
154
152
|
queryValues,
|
|
155
153
|
);
|
|
@@ -235,7 +233,7 @@ async function listUserChatSummarySeedsViaSqlite(options: ListUserChatsOptions):
|
|
|
235
233
|
) AS "pendingAssistantMessageCount"
|
|
236
234
|
FROM ${userChatTableName} AS chat
|
|
237
235
|
WHERE ${whereClause}
|
|
238
|
-
ORDER BY chat."
|
|
236
|
+
ORDER BY chat."createdAt" DESC
|
|
239
237
|
`,
|
|
240
238
|
)
|
|
241
239
|
.all(...queryValues) as Array<UserChatSummarySeedSqlRow>;
|