@promptbook/cli 0.112.0-102 → 0.112.0-104
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/README.md +0 -6
- package/apps/agents-server/src/app/AddAgentButton.tsx +0 -5
- package/apps/agents-server/src/app/actions.ts +50 -0
- package/apps/agents-server/src/app/admin/image-generator-test/ImageAttachmentsEditor.tsx +11 -6
- package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +13 -15
- package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +13 -14
- package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +3 -4
- package/apps/agents-server/src/app/api/health/route.ts +18 -0
- package/apps/agents-server/src/app/api/upload/route.ts +110 -383
- package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +1 -4
- package/apps/agents-server/src/components/Header/Header.tsx +0 -11
- package/apps/agents-server/src/components/Header/useHeaderAgentMenus.tsx +0 -5
- package/apps/agents-server/src/components/NewAgentDialog/useNewAgentDialog.tsx +39 -16
- package/apps/agents-server/src/constants/defaultAgentAvatarVisual.ts +1 -1
- package/apps/agents-server/src/database/migrations/2026-06-0200-default-agent-avatar-visual-octopus3d3.sql +16 -0
- package/apps/agents-server/src/middleware.ts +2 -1
- package/apps/agents-server/src/tools/$provideCdnForServer.ts +87 -49
- package/apps/agents-server/src/utils/agentRouting/resolveAgentRouteTarget.ts +27 -4
- package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +17 -49
- package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +5 -2
- package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +5 -0
- package/apps/agents-server/src/utils/defaultAgents/defaultAgents.ts +168 -0
- package/apps/agents-server/src/utils/defaultAgents/installDefaultAgents.ts +139 -0
- package/apps/agents-server/src/utils/shareTargetPayloads.ts +15 -63
- package/apps/agents-server/src/utils/upload/createBookEditorUploadHandler.ts +23 -150
- package/apps/agents-server/src/utils/upload/uploadFileToServer.ts +113 -0
- package/esm/index.es.js +711 -41
- package/esm/index.es.js.map +1 -1
- package/esm/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
- package/esm/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
- package/esm/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
- package/esm/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
- package/esm/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
- package/esm/src/avatars/visuals/octopus3d3AvatarVisual.d.ts +7 -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/octopus3d3AvatarVisual.ts +903 -0
- package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +1 -3
- package/src/other/templates/getTemplatesPipelineCollection.ts +799 -809
- package/src/utils/agents/resolveAgentAvatarImageUrl.ts +1 -1
- package/src/version.ts +2 -2
- package/src/versions.txt +1 -0
- package/umd/index.umd.js +711 -41
- package/umd/index.umd.js.map +1 -1
- package/umd/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
- package/umd/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
- package/umd/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
- package/umd/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
- package/umd/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
- package/umd/src/avatars/visuals/octopus3d3AvatarVisual.d.ts +7 -0
- package/umd/src/version.d.ts +1 -1
- package/apps/agents-server/src/utils/cdn/resolveCdnStorageProvider.ts +0 -40
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { readdir, readFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import type { AgentBasicInformation, AgentCollection, string_book } from '@promptbook-local/types';
|
|
4
|
+
import { parseAgentSource } from '../../../../../src/book-2.0/agent-source/parseAgentSource';
|
|
5
|
+
import { DEFAULT_AGENT_VISIBILITY } from '../agentVisibility';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Installation status for one default agent book.
|
|
9
|
+
*
|
|
10
|
+
* @private shared utility for Agents Server default-agent installation
|
|
11
|
+
*/
|
|
12
|
+
export type DefaultAgentInstallStatus = 'installed' | 'skipped';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Result for one processed default agent book.
|
|
16
|
+
*
|
|
17
|
+
* @private shared utility for Agents Server default-agent installation
|
|
18
|
+
*/
|
|
19
|
+
export type DefaultAgentInstallRecord = {
|
|
20
|
+
/**
|
|
21
|
+
* Source book filename.
|
|
22
|
+
*/
|
|
23
|
+
readonly fileName: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parsed canonical agent name.
|
|
27
|
+
*/
|
|
28
|
+
readonly agentName: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether the agent was created or skipped because an active agent with the same name already exists.
|
|
32
|
+
*/
|
|
33
|
+
readonly status: DefaultAgentInstallStatus;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Permanent id of the newly created agent, if created.
|
|
37
|
+
*/
|
|
38
|
+
readonly permanentId?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Aggregate default-agent installation result.
|
|
43
|
+
*
|
|
44
|
+
* @private shared utility for Agents Server default-agent installation
|
|
45
|
+
*/
|
|
46
|
+
export type DefaultAgentInstallResult = {
|
|
47
|
+
/**
|
|
48
|
+
* Number of newly created agents.
|
|
49
|
+
*/
|
|
50
|
+
readonly installedCount: number;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Number of already-present agents skipped by name.
|
|
54
|
+
*/
|
|
55
|
+
readonly skippedCount: number;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Per-book processing records.
|
|
59
|
+
*/
|
|
60
|
+
readonly records: ReadonlyArray<DefaultAgentInstallRecord>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Options for installing default agents from a directory.
|
|
65
|
+
*
|
|
66
|
+
* @private shared utility for Agents Server default-agent installation
|
|
67
|
+
*/
|
|
68
|
+
export type InstallDefaultAgentsFromDirectoryOptions = {
|
|
69
|
+
/**
|
|
70
|
+
* Agent collection used for persistence.
|
|
71
|
+
*/
|
|
72
|
+
readonly collection: AgentCollection;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Directory containing the repository default `*.book` files.
|
|
76
|
+
*/
|
|
77
|
+
readonly defaultAgentsDirectoryPath: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Optional logger for install-time progress.
|
|
81
|
+
*/
|
|
82
|
+
readonly logger?: Pick<Console, 'info'>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Lists repository default book filenames in stable install order.
|
|
87
|
+
*
|
|
88
|
+
* @param defaultAgentsDirectoryPath - Directory containing default `*.book` files.
|
|
89
|
+
* @returns Sorted book filenames.
|
|
90
|
+
*
|
|
91
|
+
* @private shared utility for Agents Server default-agent installation
|
|
92
|
+
*/
|
|
93
|
+
export async function listDefaultAgentBookFileNames(defaultAgentsDirectoryPath: string): Promise<ReadonlyArray<string>> {
|
|
94
|
+
const directoryEntries = await readdir(defaultAgentsDirectoryPath, { withFileTypes: true });
|
|
95
|
+
|
|
96
|
+
return directoryEntries
|
|
97
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.book'))
|
|
98
|
+
.map((entry) => entry.name)
|
|
99
|
+
.sort((left, right) => left.localeCompare(right));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates default agents from repository `*.book` files, skipping already-active agents with matching names.
|
|
104
|
+
*
|
|
105
|
+
* @param options - Installation options.
|
|
106
|
+
* @returns Aggregate installation result.
|
|
107
|
+
*
|
|
108
|
+
* @private shared utility for Agents Server default-agent installation
|
|
109
|
+
*/
|
|
110
|
+
export async function installDefaultAgentsFromDirectory(
|
|
111
|
+
options: InstallDefaultAgentsFromDirectoryOptions,
|
|
112
|
+
): Promise<DefaultAgentInstallResult> {
|
|
113
|
+
const fileNames = await listDefaultAgentBookFileNames(options.defaultAgentsDirectoryPath);
|
|
114
|
+
const existingAgentNames = new Set((await options.collection.listAgents()).map((agent) => agent.agentName));
|
|
115
|
+
const records: Array<DefaultAgentInstallRecord> = [];
|
|
116
|
+
|
|
117
|
+
for (const [index, fileName] of fileNames.entries()) {
|
|
118
|
+
const agentSource = (await readFile(join(options.defaultAgentsDirectoryPath, fileName), 'utf-8')) as string_book;
|
|
119
|
+
const parsedAgentProfile = parseAgentSource(agentSource);
|
|
120
|
+
const agentName = parsedAgentProfile.agentName;
|
|
121
|
+
|
|
122
|
+
if (existingAgentNames.has(agentName)) {
|
|
123
|
+
options.logger?.info(`[default-agents] Skipping existing agent "${agentName}" from ${fileName}.`);
|
|
124
|
+
records.push({
|
|
125
|
+
fileName,
|
|
126
|
+
agentName,
|
|
127
|
+
status: 'skipped',
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const createdAgent = await options.collection.createAgent(agentSource, {
|
|
133
|
+
sortOrder: index + 1,
|
|
134
|
+
visibility: DEFAULT_AGENT_VISIBILITY,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
existingAgentNames.add(createdAgent.agentName);
|
|
138
|
+
options.logger?.info(`[default-agents] Installed "${createdAgent.agentName}" from ${fileName}.`);
|
|
139
|
+
records.push(createInstalledDefaultAgentRecord(fileName, createdAgent));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
installedCount: records.filter((record) => record.status === 'installed').length,
|
|
144
|
+
skippedCount: records.filter((record) => record.status === 'skipped').length,
|
|
145
|
+
records,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Creates a typed result record for a newly installed default agent.
|
|
151
|
+
*
|
|
152
|
+
* @param fileName - Source book filename.
|
|
153
|
+
* @param createdAgent - Created agent profile returned by the collection.
|
|
154
|
+
* @returns Installation record.
|
|
155
|
+
*
|
|
156
|
+
* @private utility of `installDefaultAgentsFromDirectory`
|
|
157
|
+
*/
|
|
158
|
+
function createInstalledDefaultAgentRecord(
|
|
159
|
+
fileName: string,
|
|
160
|
+
createdAgent: AgentBasicInformation & Required<Pick<AgentBasicInformation, 'permanentId'>>,
|
|
161
|
+
): DefaultAgentInstallRecord {
|
|
162
|
+
return {
|
|
163
|
+
fileName,
|
|
164
|
+
agentName: createdAgent.agentName,
|
|
165
|
+
status: 'installed',
|
|
166
|
+
permanentId: createdAgent.permanentId,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as dotenv from 'dotenv';
|
|
2
|
+
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
import { AgentCollectionInSupabase } from '../../../../../src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase';
|
|
4
|
+
import type { AgentCollection } from '../../../../../src/collection/agent-collection/AgentCollection';
|
|
5
|
+
import { isAgentsServerSqliteMode } from '../../database/agentsServerDatabaseMode';
|
|
6
|
+
import { $provideLocalSqliteSupabase } from '../../database/sqlite/$provideLocalSqliteSupabase';
|
|
7
|
+
import { installDefaultAgentsFromDirectory } from './defaultAgents';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Environment variable pointing to the installed Agents Server `.env` file.
|
|
11
|
+
*
|
|
12
|
+
* @private install-time default-agent utility
|
|
13
|
+
*/
|
|
14
|
+
const AGENTS_SERVER_ENV_FILE_ENV_NAME = 'PTBK_AGENTS_SERVER_ENV_FILE';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Environment variable pointing to the repository `agents/default` directory.
|
|
18
|
+
*
|
|
19
|
+
* @private install-time default-agent utility
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_AGENTS_DIRECTORY_ENV_NAME = 'PTBK_DEFAULT_AGENTS_DIR';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Environment variable containing the current server table prefix.
|
|
25
|
+
*
|
|
26
|
+
* @private install-time default-agent utility
|
|
27
|
+
*/
|
|
28
|
+
const SUPABASE_TABLE_PREFIX_ENV_NAME = 'SUPABASE_TABLE_PREFIX';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Public Supabase URL environment variable.
|
|
32
|
+
*
|
|
33
|
+
* @private install-time default-agent utility
|
|
34
|
+
*/
|
|
35
|
+
const NEXT_PUBLIC_SUPABASE_URL_ENV_NAME = 'NEXT_PUBLIC_SUPABASE_URL';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Supabase service role key environment variable.
|
|
39
|
+
*
|
|
40
|
+
* @private install-time default-agent utility
|
|
41
|
+
*/
|
|
42
|
+
const SUPABASE_SERVICE_ROLE_KEY_ENV_NAME = 'SUPABASE_SERVICE_ROLE_KEY';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Public Supabase anon key environment variable used as a fallback when service role is unavailable.
|
|
46
|
+
*
|
|
47
|
+
* @private install-time default-agent utility
|
|
48
|
+
*/
|
|
49
|
+
const NEXT_PUBLIC_SUPABASE_ANON_KEY_ENV_NAME = 'NEXT_PUBLIC_SUPABASE_ANON_KEY';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Loads the installed Agents Server environment for the detached installer script.
|
|
53
|
+
*
|
|
54
|
+
* @private install-time default-agent utility
|
|
55
|
+
*/
|
|
56
|
+
function loadDefaultAgentInstallEnvironment(): void {
|
|
57
|
+
const explicitEnvFilePath = process.env[AGENTS_SERVER_ENV_FILE_ENV_NAME]?.trim();
|
|
58
|
+
if (explicitEnvFilePath) {
|
|
59
|
+
const explicitLoadResult = dotenv.config({ path: explicitEnvFilePath });
|
|
60
|
+
if (!explicitLoadResult.error) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dotenv.config();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Creates the install-time agent collection using the same database backend as the server.
|
|
70
|
+
*
|
|
71
|
+
* @returns Agent collection bound to the configured table prefix.
|
|
72
|
+
*
|
|
73
|
+
* @private install-time default-agent utility
|
|
74
|
+
*/
|
|
75
|
+
function createDefaultAgentInstallCollection(): AgentCollection {
|
|
76
|
+
return new AgentCollectionInSupabase(createDefaultAgentInstallSupabaseClient(), {
|
|
77
|
+
tablePrefix: process.env[SUPABASE_TABLE_PREFIX_ENV_NAME] || '',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates the Supabase-shaped client used by the install-time agent collection.
|
|
83
|
+
*
|
|
84
|
+
* @returns Supabase or local SQLite client.
|
|
85
|
+
*
|
|
86
|
+
* @private install-time default-agent utility
|
|
87
|
+
*/
|
|
88
|
+
function createDefaultAgentInstallSupabaseClient(): SupabaseClient {
|
|
89
|
+
if (isAgentsServerSqliteMode()) {
|
|
90
|
+
return $provideLocalSqliteSupabase();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const supabaseUrl = process.env[NEXT_PUBLIC_SUPABASE_URL_ENV_NAME];
|
|
94
|
+
const supabaseKey =
|
|
95
|
+
process.env[SUPABASE_SERVICE_ROLE_KEY_ENV_NAME] || process.env[NEXT_PUBLIC_SUPABASE_ANON_KEY_ENV_NAME];
|
|
96
|
+
|
|
97
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Missing \`${NEXT_PUBLIC_SUPABASE_URL_ENV_NAME}\` and \`${SUPABASE_SERVICE_ROLE_KEY_ENV_NAME}\` for Supabase default-agent installation.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return createClient(supabaseUrl, supabaseKey, {
|
|
104
|
+
auth: {
|
|
105
|
+
autoRefreshToken: false,
|
|
106
|
+
persistSession: false,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Runs the default-agent install command.
|
|
113
|
+
*
|
|
114
|
+
* @private install-time default-agent utility
|
|
115
|
+
*/
|
|
116
|
+
async function installDefaultAgents(): Promise<void> {
|
|
117
|
+
loadDefaultAgentInstallEnvironment();
|
|
118
|
+
|
|
119
|
+
const defaultAgentsDirectoryPath = process.env[DEFAULT_AGENTS_DIRECTORY_ENV_NAME]?.trim();
|
|
120
|
+
if (!defaultAgentsDirectoryPath) {
|
|
121
|
+
throw new Error(`Missing \`${DEFAULT_AGENTS_DIRECTORY_ENV_NAME}\` environment variable.`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const result = await installDefaultAgentsFromDirectory({
|
|
125
|
+
collection: createDefaultAgentInstallCollection(),
|
|
126
|
+
defaultAgentsDirectoryPath,
|
|
127
|
+
logger: console,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
console.info(
|
|
131
|
+
`[default-agents] Finished. Installed ${result.installedCount}, skipped ${result.skippedCount}.`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
installDefaultAgents().catch((error) => {
|
|
136
|
+
console.error('[default-agents] Failed to install default agents.');
|
|
137
|
+
console.error(error instanceof Error ? error.message : error);
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
});
|
|
@@ -2,7 +2,7 @@ import { $getTableName } from '@/src/database/$getTableName';
|
|
|
2
2
|
import { $provideSupabaseForServer } from '@/src/database/$provideSupabaseForServer';
|
|
3
3
|
import type { Json } from '@/src/database/schema';
|
|
4
4
|
import { FILE_SECURITY_CHECKERS } from '@/src/file-security-checkers';
|
|
5
|
-
import { $
|
|
5
|
+
import { $provideCdnForServer } from '@/src/tools/$provideCdnForServer';
|
|
6
6
|
import { $provideServer } from '@/src/tools/$provideServer';
|
|
7
7
|
import { getUserFileCdnKey } from '@/src/utils/cdn/utils/getUserFileCdnKey';
|
|
8
8
|
import { validateMimeType } from '@/src/utils/validators/validateMimeType';
|
|
@@ -231,12 +231,14 @@ async function createShareTargetAttachment(file: File, maxFileUploadBytes: numbe
|
|
|
231
231
|
|
|
232
232
|
const mimeType = resolveShareTargetMimeType(file.type);
|
|
233
233
|
const blobPath = getUserFileCdnKey(buffer, normalizedFilename);
|
|
234
|
-
const cdn = $
|
|
234
|
+
const cdn = $provideCdnForServer();
|
|
235
|
+
const storageUrl = cdn.getItemUrl(blobPath).href;
|
|
236
|
+
|
|
235
237
|
await cdn.setItem(blobPath, {
|
|
236
|
-
data: buffer,
|
|
237
238
|
type: mimeType,
|
|
238
|
-
|
|
239
|
+
data: buffer,
|
|
239
240
|
purpose: SHARE_TARGET_FILE_PURPOSE,
|
|
241
|
+
fileSize: buffer.byteLength,
|
|
240
242
|
}).catch((error) => {
|
|
241
243
|
throw new DatabaseError(
|
|
242
244
|
spaceTrim(`
|
|
@@ -246,24 +248,14 @@ async function createShareTargetAttachment(file: File, maxFileUploadBytes: numbe
|
|
|
246
248
|
`),
|
|
247
249
|
);
|
|
248
250
|
});
|
|
249
|
-
const storageUrl = cdn.getItemUrl(blobPath).href;
|
|
250
|
-
const fileRecordId = await insertShareTargetFileRecord({
|
|
251
|
-
fileName: normalizedFilename,
|
|
252
|
-
fileSize: buffer.byteLength,
|
|
253
|
-
fileType: mimeType,
|
|
254
|
-
storageUrl,
|
|
255
|
-
});
|
|
256
251
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}),
|
|
265
|
-
);
|
|
266
|
-
}
|
|
252
|
+
after(() =>
|
|
253
|
+
populateShareTargetFileSecurityResult({
|
|
254
|
+
storageUrl,
|
|
255
|
+
}).catch((error) => {
|
|
256
|
+
console.error('[share-target] Failed to finalize file security result', error);
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
267
259
|
|
|
268
260
|
return {
|
|
269
261
|
name: normalizedFilename,
|
|
@@ -272,50 +264,10 @@ async function createShareTargetAttachment(file: File, maxFileUploadBytes: numbe
|
|
|
272
264
|
};
|
|
273
265
|
}
|
|
274
266
|
|
|
275
|
-
/**
|
|
276
|
-
* Inserts one admin-visible file row for a shared attachment.
|
|
277
|
-
*/
|
|
278
|
-
async function insertShareTargetFileRecord(options: {
|
|
279
|
-
fileName: string;
|
|
280
|
-
fileSize: number;
|
|
281
|
-
fileType: string;
|
|
282
|
-
storageUrl: string;
|
|
283
|
-
}): Promise<number | null> {
|
|
284
|
-
try {
|
|
285
|
-
const supabase = $provideSupabaseForServer() as TODO_any;
|
|
286
|
-
const fileTableName = await $getTableName('File');
|
|
287
|
-
const { data, error } = await supabase
|
|
288
|
-
.from(fileTableName)
|
|
289
|
-
.insert({
|
|
290
|
-
fileName: options.fileName,
|
|
291
|
-
fileSize: options.fileSize,
|
|
292
|
-
fileType: options.fileType,
|
|
293
|
-
storageUrl: options.storageUrl,
|
|
294
|
-
shortUrl: null,
|
|
295
|
-
purpose: SHARE_TARGET_FILE_PURPOSE,
|
|
296
|
-
status: 'COMPLETED',
|
|
297
|
-
agentId: null,
|
|
298
|
-
securityResult: null,
|
|
299
|
-
})
|
|
300
|
-
.select('id')
|
|
301
|
-
.maybeSingle();
|
|
302
|
-
|
|
303
|
-
if (error) {
|
|
304
|
-
console.error('[share-target] Failed to store uploaded file metadata', error);
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return typeof data?.id === 'number' ? data.id : null;
|
|
309
|
-
} catch (error) {
|
|
310
|
-
console.error('[share-target] Failed to insert shared file metadata', error);
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
267
|
/**
|
|
316
268
|
* Populates best-effort file-security results after the share redirect has already returned.
|
|
317
269
|
*/
|
|
318
|
-
async function populateShareTargetFileSecurityResult(options: {
|
|
270
|
+
async function populateShareTargetFileSecurityResult(options: { storageUrl: string }): Promise<void> {
|
|
319
271
|
const securityResult: Record<string, unknown> = {};
|
|
320
272
|
|
|
321
273
|
for (const checkerId of Object.keys(FILE_SECURITY_CHECKERS)) {
|
|
@@ -343,7 +295,7 @@ async function populateShareTargetFileSecurityResult(options: { fileId: number;
|
|
|
343
295
|
.update({
|
|
344
296
|
securityResult,
|
|
345
297
|
})
|
|
346
|
-
.eq('
|
|
298
|
+
.eq('storageUrl', options.storageUrl);
|
|
347
299
|
|
|
348
300
|
if (error) {
|
|
349
301
|
throw new DatabaseError(
|
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import type { number_percent, string_knowledge_source_content } from '@promptbook-local/types';
|
|
5
|
-
import { isServerRoutedCdnUploadProvider, resolveCdnStorageProvider } from '../cdn/resolveCdnStorageProvider';
|
|
3
|
+
import type { string_knowledge_source_content } from '@promptbook-local/types';
|
|
6
4
|
import { getSafeCdnPath } from '../cdn/utils/getSafeCdnPath';
|
|
7
5
|
import { normalizeUploadFilename } from '../normalization/normalizeUploadFilename';
|
|
6
|
+
import {
|
|
7
|
+
buildDefaultUserFileUploadPath,
|
|
8
|
+
uploadFileToServer,
|
|
9
|
+
type FileUploadProgressCallback,
|
|
10
|
+
} from './uploadFileToServer';
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Progress callback invoked during file uploads.
|
|
11
14
|
*
|
|
12
15
|
* @private used by chat and book editors.
|
|
13
16
|
*/
|
|
14
|
-
export type FileUploadProgressCallback
|
|
15
|
-
progress: number_percent,
|
|
16
|
-
stats?: {
|
|
17
|
-
loadedBytes: number;
|
|
18
|
-
totalBytes: number;
|
|
19
|
-
},
|
|
20
|
-
) => void;
|
|
17
|
+
export type { FileUploadProgressCallback };
|
|
21
18
|
|
|
22
19
|
/**
|
|
23
20
|
* Optional upload configuration such as progress reporting or cancellation.
|
|
@@ -56,8 +53,8 @@ type UploadPathBuilder = (normalizedFilename: string, pathPrefix: string) => str
|
|
|
56
53
|
/**
|
|
57
54
|
* Constant for build default user file path.
|
|
58
55
|
*/
|
|
59
|
-
const buildDefaultUserFilePath: UploadPathBuilder = (normalizedFilename
|
|
60
|
-
|
|
56
|
+
const buildDefaultUserFilePath: UploadPathBuilder = (normalizedFilename) =>
|
|
57
|
+
buildDefaultUserFileUploadPath(normalizedFilename);
|
|
61
58
|
|
|
62
59
|
/**
|
|
63
60
|
* Configuration of the shared upload handler.
|
|
@@ -76,109 +73,6 @@ type SharedUploadHandlerConfig = {
|
|
|
76
73
|
*/
|
|
77
74
|
const DEFAULT_SHORT_URL_PREFIX = 'https://ptbk.io/k/';
|
|
78
75
|
|
|
79
|
-
/**
|
|
80
|
-
* Response returned by the server-routed upload API.
|
|
81
|
-
*
|
|
82
|
-
* @private used by chat and book editors.
|
|
83
|
-
*/
|
|
84
|
-
type ServerRoutedUploadResponse = {
|
|
85
|
-
url: string;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Uploads a file through `/api/upload` for S3-compatible storage providers.
|
|
90
|
-
*
|
|
91
|
-
* @private used by chat and book editors.
|
|
92
|
-
*/
|
|
93
|
-
function uploadFileThroughServer(options: {
|
|
94
|
-
safeUploadPath: string;
|
|
95
|
-
file: File;
|
|
96
|
-
purpose: string;
|
|
97
|
-
abortSignal?: AbortSignal;
|
|
98
|
-
onProgress?: FileUploadProgressCallback;
|
|
99
|
-
}): Promise<ServerRoutedUploadResponse> {
|
|
100
|
-
const { safeUploadPath, file, purpose, abortSignal, onProgress } = options;
|
|
101
|
-
const formData = new FormData();
|
|
102
|
-
formData.set('file', file);
|
|
103
|
-
formData.set('pathname', safeUploadPath);
|
|
104
|
-
formData.set('purpose', purpose);
|
|
105
|
-
formData.set('contentType', file.type || 'application/octet-stream');
|
|
106
|
-
|
|
107
|
-
return new Promise((resolve, reject) => {
|
|
108
|
-
const request = new XMLHttpRequest();
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Removes abort listeners once the upload finishes.
|
|
112
|
-
*/
|
|
113
|
-
const cleanup = () => {
|
|
114
|
-
abortSignal?.removeEventListener('abort', handleAbort);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Aborts the active XHR upload when the caller aborts.
|
|
119
|
-
*/
|
|
120
|
-
const handleAbort = () => {
|
|
121
|
-
request.abort();
|
|
122
|
-
cleanup();
|
|
123
|
-
reject(new DOMException('Upload aborted.', 'AbortError'));
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
request.upload.addEventListener('progress', (event) => {
|
|
127
|
-
if (!event.lengthComputable) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
onProgress?.((event.loaded / event.total) as number_percent, {
|
|
132
|
-
loadedBytes: event.loaded,
|
|
133
|
-
totalBytes: event.total,
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
request.addEventListener('load', () => {
|
|
138
|
-
cleanup();
|
|
139
|
-
|
|
140
|
-
if (request.status < 200 || request.status >= 300) {
|
|
141
|
-
reject(new Error(`Upload failed with HTTP ${request.status}: ${request.responseText}`));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
const parsedResponse = JSON.parse(request.responseText) as Partial<ServerRoutedUploadResponse>;
|
|
147
|
-
if (typeof parsedResponse.url !== 'string' || parsedResponse.url.trim() === '') {
|
|
148
|
-
reject(new Error('Upload response did not contain a file URL.'));
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
onProgress?.(1 as number_percent, {
|
|
153
|
-
loadedBytes: file.size,
|
|
154
|
-
totalBytes: file.size,
|
|
155
|
-
});
|
|
156
|
-
resolve({ url: parsedResponse.url });
|
|
157
|
-
} catch (error) {
|
|
158
|
-
reject(error);
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
request.addEventListener('error', () => {
|
|
163
|
-
cleanup();
|
|
164
|
-
reject(new Error('Upload failed before the server responded.'));
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
request.addEventListener('abort', () => {
|
|
168
|
-
cleanup();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
if (abortSignal?.aborted) {
|
|
172
|
-
handleAbort();
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
abortSignal?.addEventListener('abort', handleAbort);
|
|
177
|
-
request.open('POST', '/api/upload');
|
|
178
|
-
request.send(formData);
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
76
|
/**
|
|
183
77
|
* Upload handler that normalizes the filename, uploads via `/api/upload`, and optionally returns a short URL.
|
|
184
78
|
*
|
|
@@ -196,50 +90,29 @@ export function createFileUploadHandler<ReturnType extends string = string>(
|
|
|
196
90
|
|
|
197
91
|
return async (file, optionsOrOnProgress) => {
|
|
198
92
|
const { onProgress, abortSignal } = normalizeUploadOptions(optionsOrOnProgress);
|
|
199
|
-
const provider = resolveCdnStorageProvider();
|
|
200
|
-
const isServerRoutedUploadEnabled = isServerRoutedCdnUploadProvider(provider);
|
|
201
93
|
const pathPrefix = process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '';
|
|
202
94
|
const normalizedFilename = normalizeUploadFilename(file.name);
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
95
|
+
const uploadPath = pathBuilder(normalizedFilename, pathPrefix);
|
|
96
|
+
const safeUploadPath = getSafeCdnPath({ pathname: uploadPath, pathPrefix });
|
|
97
|
+
|
|
98
|
+
const uploadResult = await uploadFileToServer({
|
|
99
|
+
file,
|
|
100
|
+
pathname: safeUploadPath,
|
|
101
|
+
purpose,
|
|
102
|
+
contentType: file.type || 'application/octet-stream',
|
|
103
|
+
abortSignal,
|
|
104
|
+
onProgress,
|
|
208
105
|
});
|
|
209
106
|
|
|
210
|
-
const blob = isServerRoutedUploadEnabled
|
|
211
|
-
? await uploadFileThroughServer({
|
|
212
|
-
safeUploadPath,
|
|
213
|
-
file,
|
|
214
|
-
purpose,
|
|
215
|
-
abortSignal,
|
|
216
|
-
onProgress,
|
|
217
|
-
})
|
|
218
|
-
: await upload(safeUploadPath, file, {
|
|
219
|
-
access: 'public',
|
|
220
|
-
handleUploadUrl: '/api/upload',
|
|
221
|
-
clientPayload: JSON.stringify({
|
|
222
|
-
purpose,
|
|
223
|
-
contentType: file.type || 'application/octet-stream',
|
|
224
|
-
}),
|
|
225
|
-
abortSignal,
|
|
226
|
-
onUploadProgress: (progressEvent) => {
|
|
227
|
-
onProgress?.(progressEvent.percentage / 100, {
|
|
228
|
-
loadedBytes: progressEvent.loaded,
|
|
229
|
-
totalBytes: progressEvent.total,
|
|
230
|
-
});
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
|
|
234
107
|
if (returnShortUrl && process.env.NEXT_PUBLIC_CDN_PUBLIC_URL) {
|
|
235
|
-
const slashIndex =
|
|
236
|
-
const directoryPath = slashIndex === -1 ? '' : `${
|
|
108
|
+
const slashIndex = uploadPath.lastIndexOf('/');
|
|
109
|
+
const directoryPath = slashIndex === -1 ? '' : `${uploadPath.slice(0, slashIndex + 1)}`;
|
|
237
110
|
const longUrlPrefix = `${process.env.NEXT_PUBLIC_CDN_PUBLIC_URL}/${directoryPath}`;
|
|
238
|
-
const shortUrl =
|
|
111
|
+
const shortUrl = uploadResult.url.split(longUrlPrefix).join(shortUrlPrefix);
|
|
239
112
|
return shortUrl as ReturnType;
|
|
240
113
|
}
|
|
241
114
|
|
|
242
|
-
return
|
|
115
|
+
return uploadResult.url as ReturnType;
|
|
243
116
|
};
|
|
244
117
|
}
|
|
245
118
|
|