@agent-native/core 0.92.5 → 0.92.7
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +19 -0
- package/corpus/core/docs/content/template-assets.mdx +139 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +96 -6
- package/corpus/core/src/cli/sync-builder-starter-manifest.ts +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +127 -15
- package/corpus/core/src/client/chat/tool-call-display.tsx +9 -3
- package/corpus/core/src/client/org/TeamPage.tsx +274 -177
- package/corpus/core/src/client/settings/DemoModeSection.tsx +6 -6
- package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
- package/corpus/core/src/client/settings/SettingsTabsPage.tsx +49 -1
- package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
- package/corpus/core/src/demo/config.ts +3 -3
- package/corpus/core/src/demo/redact.ts +720 -0
- package/corpus/core/src/extensions/url-safety.ts +13 -1
- package/corpus/core/src/localization/default-messages.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/de-DE.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/en-US.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/es-ES.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +7 -0
- package/corpus/templates/analytics/AGENTS.md +6 -3
- package/corpus/templates/analytics/changelog/2026-07-09-uptime-monitors-use-scheduled-workers-in-production-serverless.md +6 -0
- package/corpus/templates/analytics/docs/uptime-monitoring.md +5 -0
- package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +14 -2
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +35 -0
- package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +19 -0
- package/corpus/templates/assets/AGENTS.md +13 -0
- package/corpus/templates/assets/actions/_generation-preset-settings.ts +73 -0
- package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +63 -0
- package/corpus/templates/assets/actions/create-generation-preset.ts +22 -8
- package/corpus/templates/assets/actions/duplicate-library.ts +49 -1
- package/corpus/templates/assets/actions/generate-image-batch.ts +14 -1
- package/corpus/templates/assets/actions/generate-image.ts +195 -26
- package/corpus/templates/assets/actions/import-asset-from-url.ts +323 -0
- package/corpus/templates/assets/actions/list-generation-presets.ts +1 -1
- package/corpus/templates/assets/actions/rerun-generation-run.ts +37 -0
- package/corpus/templates/assets/actions/update-generation-preset.ts +19 -1
- package/corpus/templates/assets/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/assets/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/assets/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/assets/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/assets/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/assets/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/assets/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/assets/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/assets/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +41 -0
- package/corpus/templates/assets/app/i18n-data.ts +93 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +66 -11
- package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +959 -24
- package/corpus/templates/assets/changelog/2026-07-07-preset-skeleton-controls-fit-correctly-on-narrower-edit-page.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-09-import-reference-images-from-url.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-09-preset-reference-board.md +6 -0
- package/corpus/templates/assets/server/handlers/assets.ts +7 -27
- package/corpus/templates/assets/server/lib/generation.ts +83 -2
- package/corpus/templates/assets/server/lib/preset-chat-context.ts +17 -4
- package/corpus/templates/assets/server/lib/preset-references.ts +182 -0
- package/corpus/templates/assets/server/lib/upload-validation.ts +38 -0
- package/corpus/templates/assets/shared/api.ts +20 -0
- package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +6 -0
- package/corpus/templates/calendar/actions/create-event.ts +8 -2
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +22 -7
- package/corpus/templates/calendar/changelog/2026-07-09-calendar-events-with-guests-now-automatically-get-a-google-m.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-09-new-events-now-mark-the-creator-rsvp-as-yes.md +6 -0
- package/corpus/templates/calendar/server/handlers/events.ts +9 -2
- package/corpus/templates/calendar/server/lib/event-video-conferencing.ts +58 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +1 -0
- package/corpus/templates/clips/changelog/2026-07-09-meeting-reminder-popovers-can-now-be-dismissed-with-a-top-le.md +6 -0
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +3 -1
- package/corpus/templates/clips/desktop/src/styles.css +13 -9
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +5 -2
- package/dist/agent/production-agent.d.ts +34 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +58 -5
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/sync-builder-starter-manifest.js +1 -1
- package/dist/cli/sync-builder-starter-manifest.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +3 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +96 -15
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +5 -1
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/org/TeamPage.d.ts.map +1 -1
- package/dist/client/org/TeamPage.js +32 -13
- package/dist/client/org/TeamPage.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts +3 -3
- package/dist/client/settings/DemoModeSection.js +4 -4
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
- package/dist/client/settings/SettingsTabsPage.js +38 -2
- package/dist/client/settings/SettingsTabsPage.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/demo/actions/toggle-demo-mode.js +2 -2
- package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
- package/dist/demo/config.js +3 -3
- package/dist/demo/config.js.map +1 -1
- package/dist/demo/redact.d.ts +1 -1
- package/dist/demo/redact.d.ts.map +1 -1
- package/dist/demo/redact.js +0 -0
- package/dist/demo/redact.js.map +1 -1
- package/dist/extensions/url-safety.d.ts +5 -0
- package/dist/extensions/url-safety.d.ts.map +1 -1
- package/dist/extensions/url-safety.js +10 -0
- package/dist/extensions/url-safety.js.map +1 -1
- package/dist/localization/default-messages.d.ts +7 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +7 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/templates/default/app/i18n/ar-SA.ts +7 -0
- package/dist/templates/default/app/i18n/de-DE.ts +7 -0
- package/dist/templates/default/app/i18n/en-US.ts +7 -0
- package/dist/templates/default/app/i18n/es-ES.ts +7 -0
- package/dist/templates/default/app/i18n/fr-FR.ts +7 -0
- package/dist/templates/default/app/i18n/hi-IN.ts +7 -0
- package/dist/templates/default/app/i18n/ja-JP.ts +7 -0
- package/dist/templates/default/app/i18n/ko-KR.ts +7 -0
- package/dist/templates/default/app/i18n/pt-BR.ts +7 -0
- package/dist/templates/default/app/i18n/zh-CN.ts +7 -0
- package/dist/templates/default/app/i18n/zh-TW.ts +7 -0
- package/docs/content/template-assets.mdx +139 -0
- package/package.json +1 -1
- package/src/templates/default/app/i18n/ar-SA.ts +7 -0
- package/src/templates/default/app/i18n/de-DE.ts +7 -0
- package/src/templates/default/app/i18n/en-US.ts +7 -0
- package/src/templates/default/app/i18n/es-ES.ts +7 -0
- package/src/templates/default/app/i18n/fr-FR.ts +7 -0
- package/src/templates/default/app/i18n/hi-IN.ts +7 -0
- package/src/templates/default/app/i18n/ja-JP.ts +7 -0
- package/src/templates/default/app/i18n/ko-KR.ts +7 -0
- package/src/templates/default/app/i18n/pt-BR.ts +7 -0
- package/src/templates/default/app/i18n/zh-CN.ts +7 -0
- package/src/templates/default/app/i18n/zh-TW.ts +7 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { ssrfSafeFetch } from "@agent-native/core/extensions/url-safety";
|
|
3
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
4
|
+
import { and, eq } from "drizzle-orm";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
8
|
+
import { createAssetFromBuffer } from "../server/lib/assets.js";
|
|
9
|
+
import { getObject } from "../server/lib/storage.js";
|
|
10
|
+
import {
|
|
11
|
+
filterDuplicateAssetUploads,
|
|
12
|
+
hashAssetBuffer,
|
|
13
|
+
} from "../server/lib/upload-dedupe.js";
|
|
14
|
+
import {
|
|
15
|
+
hasAllowedSignature,
|
|
16
|
+
IMAGE_MIME_TYPES,
|
|
17
|
+
MAX_IMAGE_UPLOAD_BYTES,
|
|
18
|
+
} from "../server/lib/upload-validation.js";
|
|
19
|
+
import { IMAGE_CATEGORIES } from "../shared/api.js";
|
|
20
|
+
import type { ImageCategory } from "../shared/api.js";
|
|
21
|
+
import { serializeAsset } from "./_helpers.js";
|
|
22
|
+
|
|
23
|
+
const IMPORTABLE_REFERENCE_ROLES = [
|
|
24
|
+
"style_reference",
|
|
25
|
+
"subject_reference",
|
|
26
|
+
"product_reference",
|
|
27
|
+
"background_reference",
|
|
28
|
+
"logo_reference",
|
|
29
|
+
"diagram_reference",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
33
|
+
const MAX_REDIRECTS = 3;
|
|
34
|
+
|
|
35
|
+
// Mirrors the upload route's category↔role mapping so imported references
|
|
36
|
+
// appear in the same category-filtered views as uploaded equivalents.
|
|
37
|
+
const DEFAULT_CATEGORY_BY_ROLE: Record<
|
|
38
|
+
(typeof IMPORTABLE_REFERENCE_ROLES)[number],
|
|
39
|
+
ImageCategory
|
|
40
|
+
> = {
|
|
41
|
+
style_reference: "style-only",
|
|
42
|
+
subject_reference: "other",
|
|
43
|
+
product_reference: "product",
|
|
44
|
+
background_reference: "other",
|
|
45
|
+
logo_reference: "logo",
|
|
46
|
+
diagram_reference: "diagram",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function normalizedImageMimeType(contentType: string | null): string {
|
|
50
|
+
return contentType?.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function assertCollectionBelongsToLibrary(
|
|
54
|
+
collectionId: string,
|
|
55
|
+
libraryId: string,
|
|
56
|
+
) {
|
|
57
|
+
const [collection] = await getDb()
|
|
58
|
+
.select({
|
|
59
|
+
id: schema.assetCollections.id,
|
|
60
|
+
libraryId: schema.assetCollections.libraryId,
|
|
61
|
+
})
|
|
62
|
+
.from(schema.assetCollections)
|
|
63
|
+
.where(eq(schema.assetCollections.id, collectionId))
|
|
64
|
+
.limit(1);
|
|
65
|
+
if (!collection || collection.libraryId !== libraryId) {
|
|
66
|
+
throw new Error("collectionId does not belong to this library.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function assertFolderBelongsToLibrary(
|
|
71
|
+
folderId: string,
|
|
72
|
+
libraryId: string,
|
|
73
|
+
) {
|
|
74
|
+
const [folder] = await getDb()
|
|
75
|
+
.select({
|
|
76
|
+
id: schema.assetFolders.id,
|
|
77
|
+
libraryId: schema.assetFolders.libraryId,
|
|
78
|
+
})
|
|
79
|
+
.from(schema.assetFolders)
|
|
80
|
+
.where(eq(schema.assetFolders.id, folderId))
|
|
81
|
+
.limit(1);
|
|
82
|
+
if (!folder || folder.libraryId !== libraryId) {
|
|
83
|
+
throw new Error("folderId does not belong to this library.");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function validateHttpsUrl(url: string) {
|
|
88
|
+
const parsed = new URL(url);
|
|
89
|
+
if (parsed.protocol !== "https:") {
|
|
90
|
+
throw new Error("Only HTTPS image URLs can be imported.");
|
|
91
|
+
}
|
|
92
|
+
if (parsed.username || parsed.password) {
|
|
93
|
+
throw new Error("URLs with embedded credentials cannot be imported.");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Query params that carry bearer credentials (S3/GCS presigning, Azure SAS,
|
|
98
|
+
// generic tokens). Provenance drops the query when one is present so signed
|
|
99
|
+
// URLs do not become durable asset metadata; the fetch still uses the full URL.
|
|
100
|
+
const CREDENTIAL_QUERY_PARAM_RE =
|
|
101
|
+
/^(x-amz-|x-goog-)|^(sig|signature|token|access[-_]?token|auth|authorization|expires|policy|credential|apikey|api[-_]?key|key|secret|session|sv|se|sp|st|spr|sr|skoid)$/i;
|
|
102
|
+
|
|
103
|
+
function sanitizeProvenanceUrl(url: string): string {
|
|
104
|
+
const parsed = new URL(url);
|
|
105
|
+
parsed.username = "";
|
|
106
|
+
parsed.password = "";
|
|
107
|
+
parsed.hash = "";
|
|
108
|
+
for (const param of parsed.searchParams.keys()) {
|
|
109
|
+
if (CREDENTIAL_QUERY_PARAM_RE.test(param)) {
|
|
110
|
+
parsed.search = "";
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return parsed.toString();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Release an unread response body so its connection is not held until GC. */
|
|
118
|
+
async function discardResponseBody(response: Response) {
|
|
119
|
+
await response.body?.cancel().catch(() => {});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function readResponseBytes(response: Response): Promise<Buffer> {
|
|
123
|
+
const contentLength = response.headers.get("content-length");
|
|
124
|
+
if (contentLength && Number(contentLength) > MAX_IMAGE_UPLOAD_BYTES) {
|
|
125
|
+
await discardResponseBody(response);
|
|
126
|
+
throw new Error("Image too large (max 25 MB).");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const reader = response.body?.getReader?.();
|
|
130
|
+
if (!reader) {
|
|
131
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
132
|
+
if (buffer.byteLength > MAX_IMAGE_UPLOAD_BYTES) {
|
|
133
|
+
throw new Error("Image too large (max 25 MB).");
|
|
134
|
+
}
|
|
135
|
+
return buffer;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const chunks: Uint8Array[] = [];
|
|
139
|
+
let total = 0;
|
|
140
|
+
while (true) {
|
|
141
|
+
const { done, value } = await reader.read();
|
|
142
|
+
if (done) break;
|
|
143
|
+
if (!value) continue;
|
|
144
|
+
total += value.byteLength;
|
|
145
|
+
if (total > MAX_IMAGE_UPLOAD_BYTES) {
|
|
146
|
+
await reader.cancel().catch(() => {});
|
|
147
|
+
throw new Error("Image too large (max 25 MB).");
|
|
148
|
+
}
|
|
149
|
+
chunks.push(value);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return Buffer.concat(
|
|
153
|
+
chunks.map((chunk) => Buffer.from(chunk)),
|
|
154
|
+
total,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function fetchImageBytes(url: string): Promise<{
|
|
159
|
+
buffer: Buffer;
|
|
160
|
+
mimeType: string;
|
|
161
|
+
}> {
|
|
162
|
+
let response: Response;
|
|
163
|
+
try {
|
|
164
|
+
response = await ssrfSafeFetch(
|
|
165
|
+
url,
|
|
166
|
+
{ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) },
|
|
167
|
+
{ maxRedirects: MAX_REDIRECTS, httpsOnly: true },
|
|
168
|
+
);
|
|
169
|
+
} catch {
|
|
170
|
+
throw new Error("Could not fetch that URL.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
await discardResponseBody(response);
|
|
175
|
+
throw new Error(`Could not fetch that URL (${response.status}).`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const mimeType = normalizedImageMimeType(
|
|
179
|
+
response.headers.get("content-type"),
|
|
180
|
+
);
|
|
181
|
+
if (!IMAGE_MIME_TYPES.has(mimeType)) {
|
|
182
|
+
await discardResponseBody(response);
|
|
183
|
+
throw new Error("Only PNG, JPEG, WebP, and AVIF images are supported.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const buffer = await readResponseBytes(response);
|
|
187
|
+
if (!hasAllowedSignature(mimeType, buffer)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
"The fetched bytes do not match the advertised image type.",
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { buffer, mimeType };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Same dedupe scope as the upload route: reference assets in this library
|
|
198
|
+
* with the same role. Returns the existing asset when the fetched bytes are
|
|
199
|
+
* already stored, so repeat imports are idempotent instead of duplicating
|
|
200
|
+
* the asset row and blob.
|
|
201
|
+
*/
|
|
202
|
+
async function findDuplicateReferenceAsset(input: {
|
|
203
|
+
libraryId: string;
|
|
204
|
+
role: (typeof IMPORTABLE_REFERENCE_ROLES)[number];
|
|
205
|
+
buffer: Buffer;
|
|
206
|
+
mimeType: string;
|
|
207
|
+
contentHash: string;
|
|
208
|
+
filename: string | null;
|
|
209
|
+
}): Promise<typeof schema.assets.$inferSelect | null> {
|
|
210
|
+
const db = getDb();
|
|
211
|
+
const existingReferenceAssets = await db
|
|
212
|
+
.select({
|
|
213
|
+
id: schema.assets.id,
|
|
214
|
+
title: schema.assets.title,
|
|
215
|
+
mediaType: schema.assets.mediaType,
|
|
216
|
+
mimeType: schema.assets.mimeType,
|
|
217
|
+
sizeBytes: schema.assets.sizeBytes,
|
|
218
|
+
metadata: schema.assets.metadata,
|
|
219
|
+
objectKey: schema.assets.objectKey,
|
|
220
|
+
})
|
|
221
|
+
.from(schema.assets)
|
|
222
|
+
.where(
|
|
223
|
+
and(
|
|
224
|
+
eq(schema.assets.libraryId, input.libraryId),
|
|
225
|
+
eq(schema.assets.status, "reference"),
|
|
226
|
+
eq(schema.assets.role, input.role),
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
const { skippedDuplicates } = await filterDuplicateAssetUploads({
|
|
230
|
+
files: [
|
|
231
|
+
{
|
|
232
|
+
altText: null,
|
|
233
|
+
buffer: input.buffer,
|
|
234
|
+
contentHash: input.contentHash,
|
|
235
|
+
filename: input.filename,
|
|
236
|
+
mediaType: "image",
|
|
237
|
+
metadata: {},
|
|
238
|
+
mimeType: input.mimeType,
|
|
239
|
+
title: "",
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
existingAssets: existingReferenceAssets,
|
|
243
|
+
readExistingAssetBuffer: (asset) => getObject(asset.objectKey),
|
|
244
|
+
});
|
|
245
|
+
const duplicate = skippedDuplicates.find(
|
|
246
|
+
(skip) => skip.reason === "existing-asset" && skip.assetId,
|
|
247
|
+
);
|
|
248
|
+
if (!duplicate?.assetId) return null;
|
|
249
|
+
const [asset] = await db
|
|
250
|
+
.select()
|
|
251
|
+
.from(schema.assets)
|
|
252
|
+
.where(eq(schema.assets.id, duplicate.assetId))
|
|
253
|
+
.limit(1);
|
|
254
|
+
return asset ?? null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export default defineAction({
|
|
258
|
+
description:
|
|
259
|
+
"Import an external image URL into a library as a reference asset (style, subject, product, background, logo, or diagram reference). Use for ingesting brand imagery found on the web — e.g. a blog hero or logo — so it can be pinned to preset reference boards or set as the canonical logo.",
|
|
260
|
+
schema: z.object({
|
|
261
|
+
libraryId: z.string(),
|
|
262
|
+
url: z.string().url(),
|
|
263
|
+
role: z.enum(IMPORTABLE_REFERENCE_ROLES).default("style_reference"),
|
|
264
|
+
category: z
|
|
265
|
+
.enum(IMAGE_CATEGORIES)
|
|
266
|
+
.optional()
|
|
267
|
+
.describe(
|
|
268
|
+
"Deliverable category for filtered views (e.g. hero, campaign). Defaults to the category matching the chosen role.",
|
|
269
|
+
),
|
|
270
|
+
collectionId: z.string().nullable().optional(),
|
|
271
|
+
folderId: z.string().nullable().optional(),
|
|
272
|
+
title: z.string().max(200).optional(),
|
|
273
|
+
description: z.string().max(1000).optional(),
|
|
274
|
+
}),
|
|
275
|
+
run: async (args) => {
|
|
276
|
+
const { libraryId, url, role, category, title, description } = args;
|
|
277
|
+
// An empty-string id means "unassigned", never a real row — normalize to
|
|
278
|
+
// null so it can't skip membership validation yet still land in the row.
|
|
279
|
+
const collectionId = args.collectionId || null;
|
|
280
|
+
const folderId = args.folderId || null;
|
|
281
|
+
await assertAccess("asset-library", libraryId, "editor");
|
|
282
|
+
validateHttpsUrl(url);
|
|
283
|
+
if (collectionId) {
|
|
284
|
+
await assertCollectionBelongsToLibrary(collectionId, libraryId);
|
|
285
|
+
}
|
|
286
|
+
if (folderId) {
|
|
287
|
+
await assertFolderBelongsToLibrary(folderId, libraryId);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const { buffer, mimeType } = await fetchImageBytes(url);
|
|
291
|
+
const contentHash = hashAssetBuffer(buffer);
|
|
292
|
+
const duplicate = await findDuplicateReferenceAsset({
|
|
293
|
+
libraryId,
|
|
294
|
+
role,
|
|
295
|
+
buffer,
|
|
296
|
+
mimeType,
|
|
297
|
+
contentHash,
|
|
298
|
+
filename: new URL(url).pathname.split("/").pop() || null,
|
|
299
|
+
});
|
|
300
|
+
if (duplicate) {
|
|
301
|
+
return { ...serializeAsset(duplicate), deduplicated: true };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const provenanceUrl = sanitizeProvenanceUrl(url);
|
|
305
|
+
const asset = await createAssetFromBuffer({
|
|
306
|
+
libraryId,
|
|
307
|
+
collectionId,
|
|
308
|
+
folderId,
|
|
309
|
+
buffer,
|
|
310
|
+
mimeType,
|
|
311
|
+
mediaType: "image",
|
|
312
|
+
role,
|
|
313
|
+
category: category ?? DEFAULT_CATEGORY_BY_ROLE[role],
|
|
314
|
+
status: "reference",
|
|
315
|
+
title: title ?? null,
|
|
316
|
+
description: description ?? null,
|
|
317
|
+
sourceUrl: provenanceUrl,
|
|
318
|
+
metadata: { contentHash, importedFrom: provenanceUrl },
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return serializeAsset(asset);
|
|
322
|
+
},
|
|
323
|
+
});
|
|
@@ -7,7 +7,7 @@ import { requireLibrary, serializeGenerationPreset } from "./_helpers.js";
|
|
|
7
7
|
|
|
8
8
|
export default defineAction({
|
|
9
9
|
description:
|
|
10
|
-
"List reusable generation presets for a library, such as social images, blog heroes, and diagrams.",
|
|
10
|
+
"List reusable generation presets for a library, such as social images, blog heroes, and diagrams. This is the required first step for ad-hoc generation requests: if a preset matches, generate with its presetId instead of presetless settings. Results include settings.presetReferences, the reference board used to match named people, products, and backdrops in the request.",
|
|
11
11
|
schema: z.object({
|
|
12
12
|
libraryId: z.string(),
|
|
13
13
|
collectionId: z.string().optional(),
|
|
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
|
|
7
7
|
import { getDb, schema } from "../server/db/index.js";
|
|
8
8
|
import { parseJson } from "../server/lib/json.js";
|
|
9
|
+
import { normalizePresetReferences } from "../server/lib/preset-references.js";
|
|
9
10
|
import { requireGenerationSessionInLibrary } from "./_helpers.js";
|
|
10
11
|
import generateImage from "./generate-image.js";
|
|
11
12
|
|
|
@@ -58,6 +59,7 @@ export default defineAction({
|
|
|
58
59
|
subjectAssetId?: string;
|
|
59
60
|
embeddedText?: string | null;
|
|
60
61
|
textPlacement?: string | null;
|
|
62
|
+
boardAssignments?: Record<string, string[]>;
|
|
61
63
|
};
|
|
62
64
|
includeLogo?: boolean;
|
|
63
65
|
categories?: string[];
|
|
@@ -71,6 +73,40 @@ export default defineAction({
|
|
|
71
73
|
}>(run.metadata, {});
|
|
72
74
|
const categories =
|
|
73
75
|
metadata.settingsUsed?.categories ?? metadata.categories ?? undefined;
|
|
76
|
+
let presetReferenceFills:
|
|
77
|
+
| Array<{ referenceId: string; assetIds: string[] }>
|
|
78
|
+
| undefined;
|
|
79
|
+
// Reruns treat the CURRENT preset as authoritative by design: saved
|
|
80
|
+
// boardAssignments replay only onto entries that still exist and are
|
|
81
|
+
// still variable. Entries the designer has since removed, renamed, or
|
|
82
|
+
// converted to fixed re-resolve from today's preset instead of
|
|
83
|
+
// resurrecting the original run's images — a rerun must never bypass
|
|
84
|
+
// the designer's current board definition.
|
|
85
|
+
const boardAssignments = metadata.settingsUsed?.boardAssignments;
|
|
86
|
+
if (run.presetId && boardAssignments) {
|
|
87
|
+
const [preset] = await db
|
|
88
|
+
.select()
|
|
89
|
+
.from(schema.assetGenerationPresets)
|
|
90
|
+
.where(eq(schema.assetGenerationPresets.id, run.presetId))
|
|
91
|
+
.limit(1);
|
|
92
|
+
const presetSettings = parseJson<{ presetReferences?: unknown }>(
|
|
93
|
+
preset?.settings,
|
|
94
|
+
{},
|
|
95
|
+
);
|
|
96
|
+
presetReferenceFills = normalizePresetReferences(
|
|
97
|
+
presetSettings.presetReferences,
|
|
98
|
+
)
|
|
99
|
+
.filter((entry) => entry.variable)
|
|
100
|
+
.map((entry) => ({
|
|
101
|
+
referenceId: entry.id,
|
|
102
|
+
assetIds: Array.isArray(boardAssignments[entry.id])
|
|
103
|
+
? boardAssignments[entry.id].filter(
|
|
104
|
+
(assetId): assetId is string => typeof assetId === "string",
|
|
105
|
+
)
|
|
106
|
+
: [],
|
|
107
|
+
}))
|
|
108
|
+
.filter((fill) => fill.assetIds.length > 0);
|
|
109
|
+
}
|
|
74
110
|
|
|
75
111
|
return generateImage.run(
|
|
76
112
|
{
|
|
@@ -100,6 +136,7 @@ export default defineAction({
|
|
|
100
136
|
metadata.styleStrength ??
|
|
101
137
|
"balanced") as any,
|
|
102
138
|
categories: categories as any,
|
|
139
|
+
presetReferenceFills,
|
|
103
140
|
includeLogo: Boolean(
|
|
104
141
|
metadata.settingsUsed?.includeLogo ?? metadata.includeLogo,
|
|
105
142
|
),
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
} from "../shared/api.js";
|
|
15
15
|
import { generationPresetSettingsSchema } from "./_generation-preset-settings.js";
|
|
16
16
|
import { serializeGenerationPreset } from "./_helpers.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
assertPresetReferenceAssetsValid,
|
|
19
|
+
assertPresetReferenceModelCompatible,
|
|
20
|
+
assertPresetSkeletonAssetsValid,
|
|
21
|
+
} from "./_preset-skeleton-validation.js";
|
|
18
22
|
|
|
19
23
|
export default defineAction({
|
|
20
24
|
description:
|
|
@@ -88,7 +92,21 @@ export default defineAction({
|
|
|
88
92
|
libraryId: preset.libraryId,
|
|
89
93
|
settings: nextSettings,
|
|
90
94
|
});
|
|
95
|
+
await assertPresetReferenceAssetsValid({
|
|
96
|
+
db,
|
|
97
|
+
libraryId: preset.libraryId,
|
|
98
|
+
settings: nextSettings,
|
|
99
|
+
});
|
|
100
|
+
assertPresetReferenceModelCompatible({
|
|
101
|
+
model: (args.model ?? preset.model) as string,
|
|
102
|
+
settings: nextSettings,
|
|
103
|
+
});
|
|
91
104
|
updates.settings = stringifyJson(nextSettings);
|
|
105
|
+
} else if (args.model !== undefined) {
|
|
106
|
+
assertPresetReferenceModelCompatible({
|
|
107
|
+
model: args.model,
|
|
108
|
+
settings: parseJson<Record<string, unknown>>(preset.settings, {}),
|
|
109
|
+
});
|
|
92
110
|
}
|
|
93
111
|
await db
|
|
94
112
|
.update(schema.assetGenerationPresets)
|
|
@@ -423,6 +423,47 @@ const messages = {
|
|
|
423
423
|
lowerLeft: "左下",
|
|
424
424
|
skeletonEnabled: "已啟用",
|
|
425
425
|
skeletonOff: "關閉",
|
|
426
|
+
// TODO(l10n): translate after QA.
|
|
427
|
+
addReference: "Add reference",
|
|
428
|
+
referenceBoard: "Reference board",
|
|
429
|
+
referenceBoardEmptyDescription:
|
|
430
|
+
'Add named visual slots like "Steve" so every run can use the right people, products, or style references.',
|
|
431
|
+
referenceDefaultLabel: "Reference",
|
|
432
|
+
referenceImageCount: "{{count}}/{{max}} images",
|
|
433
|
+
referenceSubjectImageCount: "{{count}}/{{max}} subject images",
|
|
434
|
+
moreImagesCount: "+{{count}}",
|
|
435
|
+
referenceLabel: "Label",
|
|
436
|
+
referenceRoleLabel: "Role",
|
|
437
|
+
referenceRole_subject: "Subject",
|
|
438
|
+
referenceRole_style: "Style",
|
|
439
|
+
referenceRole_product: "Product",
|
|
440
|
+
referenceRole_background: "Background",
|
|
441
|
+
referenceRole_composition: "Composition",
|
|
442
|
+
referenceDescription: "Description",
|
|
443
|
+
referenceDescriptionPlaceholder:
|
|
444
|
+
"e.g. This is Steve, our usual host. Render him faithfully.",
|
|
445
|
+
referenceMode: "Reference mode",
|
|
446
|
+
fixedReference: "Fixed",
|
|
447
|
+
swappableReference: "Swappable",
|
|
448
|
+
requiredPerRun: "Required",
|
|
449
|
+
required: "Required",
|
|
450
|
+
filledPerGeneration: "Filled per generation",
|
|
451
|
+
noReferenceImages: "No reference images",
|
|
452
|
+
chooseReferenceImage: "Choose from library",
|
|
453
|
+
searchLibraryImages: "Search library images",
|
|
454
|
+
editReference: "Edit reference",
|
|
455
|
+
uploadReferenceImage: "Upload image",
|
|
456
|
+
uploadingReferenceImage: "Uploading...",
|
|
457
|
+
referenceImageUploaded: "Reference image uploaded.",
|
|
458
|
+
couldNotUploadReferenceImage: "Could not upload reference image.",
|
|
459
|
+
removeReference: "Remove reference",
|
|
460
|
+
removeReferenceImage: "Remove reference image",
|
|
461
|
+
referenceBoardTotalLimitError:
|
|
462
|
+
"The reference board may attach at most 8 images total.",
|
|
463
|
+
referenceBoardSubjectLimitError:
|
|
464
|
+
"Subject reference entries may attach at most 4 images total.",
|
|
465
|
+
subjectReferenceModelTip:
|
|
466
|
+
"Tip: gemini-3-pro-image holds up to 5 people consistent — best for photo references.",
|
|
426
467
|
openChat: "開啟聊天",
|
|
427
468
|
defaultTextPolicy: "盡量不要嵌入文字。如需文字,請保持簡短且易讀。",
|
|
428
469
|
generationPresetCreated: "生成預設已建立。",
|