@mvriu5/payload-ai 1.2.0 → 1.4.0
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/README.md +118 -13
- package/dist/ai/providerOptions.d.ts +24 -1
- package/dist/ai/providerOptions.js +81 -0
- package/dist/ai/providerRuntime.d.ts +2 -1
- package/dist/ai/providerRuntime.js +5 -2
- package/dist/ai/tokenUsage.d.ts +38 -0
- package/dist/ai/tokenUsage.js +106 -0
- package/dist/components/Icons.d.ts +1 -8
- package/dist/components/Icons.js +10 -207
- package/dist/components/{ActionToast.d.ts → action-toast/ActionToast.d.ts} +5 -2
- package/dist/components/{ActionToast.js → action-toast/ActionToast.js} +81 -54
- package/dist/components/{ActionToast.module.css → action-toast/ActionToast.module.css} +0 -79
- package/dist/components/ai-input/AIInput.d.ts +5 -0
- package/dist/components/ai-input/AIInput.js +548 -0
- package/dist/components/{AIInput.module.css → ai-input/AIInput.module.css} +150 -44
- package/dist/components/ai-input/badge.d.ts +14 -0
- package/dist/components/ai-input/badge.js +85 -0
- package/dist/components/{AuditLogList.d.ts → audit-log-list/AuditLogList.d.ts} +4 -8
- package/dist/components/{AuditLogList.js → audit-log-list/AuditLogList.js} +56 -21
- package/dist/components/{AuditLogList.module.css → audit-log-list/AuditLogList.module.css} +11 -65
- package/dist/components/dashboard/Dashboard.d.ts +2 -0
- package/dist/components/dashboard/Dashboard.js +17 -0
- package/dist/components/dashboard/Dashboard.module.css +13 -0
- package/dist/components/{DiffDialog.d.ts → diff-dialog/DiffDialog.d.ts} +2 -2
- package/dist/components/{DiffDialog.js → diff-dialog/DiffDialog.js} +200 -189
- package/dist/components/{DiffDialog.module.css → diff-dialog/DiffDialog.module.css} +17 -35
- package/dist/components/hooks/useAIChatStream.d.ts +50 -0
- package/dist/components/hooks/useAIChatStream.js +226 -0
- package/dist/components/hooks/useAISettings.d.ts +6 -4
- package/dist/components/hooks/useAISettings.js +68 -27
- package/dist/components/hooks/useAuditLog.d.ts +11 -0
- package/dist/components/hooks/useAuditLog.js +41 -0
- package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +1 -1
- package/dist/components/hooks/useDocumentMentionSuggestions.js +31 -27
- package/dist/components/hooks/useMentions.d.ts +58 -0
- package/dist/components/hooks/useMentions.js +276 -0
- package/dist/components/hooks/usePluginConfig.d.ts +40 -0
- package/dist/components/hooks/usePluginConfig.js +27 -0
- package/dist/components/{MentionPopover.d.ts → mention-popover/MentionPopover.d.ts} +2 -4
- package/dist/components/{MentionPopover.js → mention-popover/MentionPopover.js} +1 -8
- package/dist/components/{MentionPopover.module.css → mention-popover/MentionPopover.module.css} +1 -1
- package/dist/components/text-shimmer/TextShimmer.d.ts +9 -0
- package/dist/components/text-shimmer/TextShimmer.js +34 -0
- package/dist/components/text-shimmer/TextShimmer.module.css +19 -0
- package/dist/exports/client.d.ts +4 -2
- package/dist/exports/client.js +4 -2
- package/dist/handlers/applyActionHandler.js +27 -7
- package/dist/handlers/chatHandler.d.ts +4 -1
- package/dist/handlers/chatHandler.js +499 -74
- package/dist/handlers/mediaUploadHandler.d.ts +7 -0
- package/dist/handlers/mediaUploadHandler.js +99 -0
- package/dist/handlers/mentionSuggestionHandler.js +16 -14
- package/dist/handlers/proposalDiffHandler.js +41 -29
- package/dist/index.d.ts +12 -1
- package/dist/index.js +131 -12
- package/dist/payload/collectionPermissions.js +3 -1
- package/dist/payload/normalizeData.d.ts +0 -6
- package/dist/payload/normalizeData.js +25 -13
- package/dist/payload/proposalData.js +4 -1
- package/dist/payload/schemaContext.js +98 -43
- package/package.json +22 -20
- package/dist/components/AIInput.d.ts +0 -2
- package/dist/components/AIInput.js +0 -896
|
@@ -4,6 +4,7 @@ import { signAIActionProposal } from "../ai/proposalSigning.js";
|
|
|
4
4
|
import { isAIProvider } from "../ai/providerOptions.js";
|
|
5
5
|
import { getModel, getProviderConfig } from "../ai/providerRuntime.js";
|
|
6
6
|
import { containsSensitiveData } from "../ai/sensitiveData.js";
|
|
7
|
+
import { getExceededTokenUsageLimit, recordTokenUsage } from "../ai/tokenUsage.js";
|
|
7
8
|
import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
|
|
8
9
|
import { prepareProposalWriteData } from "../payload/proposalData.js";
|
|
9
10
|
import { buildPromptWithMentionContext, collectBlocks, describeCollectionLikeConfig, describeCollectionLikeSummary, getAllowedCollectionSlugs, getMentionContext } from "../payload/schemaContext.js";
|
|
@@ -46,7 +47,7 @@ const createDebugPayload = ({ activeLocale, debug, proposalCount, selectedLocale
|
|
|
46
47
|
});
|
|
47
48
|
const createE2EChatResponse = ({ prompt, selectedLocales })=>{
|
|
48
49
|
const normalizedPrompt = prompt.toLowerCase();
|
|
49
|
-
const wantsCreatePost = normalizedPrompt.includes("post") && (normalizedPrompt.includes("create") || normalizedPrompt.includes("erstell"));
|
|
50
|
+
const wantsCreatePost = normalizedPrompt.includes("post") && (normalizedPrompt.includes("create") || normalizedPrompt.includes("erstell") || normalizedPrompt.includes("apply flow") || normalizedPrompt.includes("locale review") || normalizedPrompt.includes("proposal review"));
|
|
50
51
|
const mentionsMars = normalizedPrompt.includes("mars");
|
|
51
52
|
const multipleLocales = selectedLocales.length > 1;
|
|
52
53
|
const activeLocale = selectedLocales.at(-1);
|
|
@@ -241,14 +242,23 @@ const getMissingCreateFields = ({ data, localizedData, requiredFields })=>{
|
|
|
241
242
|
];
|
|
242
243
|
}
|
|
243
244
|
const missing = [];
|
|
245
|
+
const localizedRequiredFields = [];
|
|
246
|
+
const sharedRequiredFields = [];
|
|
247
|
+
for (const field of requiredFields){
|
|
248
|
+
if (field.localized) {
|
|
249
|
+
localizedRequiredFields.push(field);
|
|
250
|
+
} else {
|
|
251
|
+
sharedRequiredFields.push(field);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
244
254
|
for (const [locale, localeData] of locales){
|
|
245
|
-
for (const field of
|
|
255
|
+
for (const field of localizedRequiredFields){
|
|
246
256
|
if (!hasValueAtPath(localeData, field.path)) {
|
|
247
257
|
missing.push(`${locale}:${field.path}`);
|
|
248
258
|
}
|
|
249
259
|
}
|
|
250
260
|
}
|
|
251
|
-
for (const field of
|
|
261
|
+
for (const field of sharedRequiredFields){
|
|
252
262
|
if (!hasValueAtPath(firstLocale[1], field.path)) {
|
|
253
263
|
missing.push(`${firstLocale[0]}:${field.path}`);
|
|
254
264
|
}
|
|
@@ -258,7 +268,9 @@ const getMissingCreateFields = ({ data, localizedData, requiredFields })=>{
|
|
|
258
268
|
if (!data) return [
|
|
259
269
|
"data is required"
|
|
260
270
|
];
|
|
261
|
-
return requiredFields.
|
|
271
|
+
return requiredFields.flatMap((field)=>hasValueAtPath(data, field.path) ? [] : [
|
|
272
|
+
field.path
|
|
273
|
+
]);
|
|
262
274
|
};
|
|
263
275
|
const getProposalSummary = (proposal)=>({
|
|
264
276
|
action: proposal.action,
|
|
@@ -293,19 +305,28 @@ const getCollectionBlockTypes = (fields)=>{
|
|
|
293
305
|
...blockTypes
|
|
294
306
|
];
|
|
295
307
|
};
|
|
308
|
+
const regexSpecialCharactersPattern = /[.*+?^${}()|[\]\\]/g;
|
|
296
309
|
const getRequestedBlockTypes = ({ availableBlockTypes, mentions, prompt })=>{
|
|
297
310
|
const requestedBlockTypes = new Set();
|
|
298
311
|
const normalizedPrompt = prompt.toLowerCase();
|
|
312
|
+
const availableBlockTypeSet = new Set(availableBlockTypes);
|
|
313
|
+
const blockTypesByNormalizedSlug = new Map(availableBlockTypes.map((blockType)=>[
|
|
314
|
+
blockType.toLowerCase(),
|
|
315
|
+
blockType
|
|
316
|
+
]));
|
|
317
|
+
const escapedBlockTypes = availableBlockTypes.map((blockType)=>blockType.replace(regexSpecialCharactersPattern, "\\$&")).join("|");
|
|
318
|
+
const blockPattern = escapedBlockTypes ? new RegExp(`\\b(${escapedBlockTypes})\\b(?:\\s+block)?`, "gi") : null;
|
|
299
319
|
for (const mention of mentions || []){
|
|
300
|
-
if (mention.type === "block" && mention.slug &&
|
|
320
|
+
if (mention.type === "block" && mention.slug && availableBlockTypeSet.has(mention.slug)) {
|
|
301
321
|
requestedBlockTypes.add(mention.slug);
|
|
302
322
|
}
|
|
303
323
|
}
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
324
|
+
if (blockPattern) {
|
|
325
|
+
for (const match of normalizedPrompt.matchAll(blockPattern)){
|
|
326
|
+
const blockType = match[1] ? blockTypesByNormalizedSlug.get(match[1].toLowerCase()) : null;
|
|
327
|
+
if (blockType) {
|
|
328
|
+
requestedBlockTypes.add(blockType);
|
|
329
|
+
}
|
|
309
330
|
}
|
|
310
331
|
}
|
|
311
332
|
return [
|
|
@@ -320,6 +341,10 @@ const collectProposalBlockTypes = ({ data, fields })=>{
|
|
|
320
341
|
const fieldValue = value[field.name];
|
|
321
342
|
if (fieldValue === undefined || fieldValue === null) continue;
|
|
322
343
|
if (field.type === "blocks" && Array.isArray(fieldValue)) {
|
|
344
|
+
const blocksBySlug = new Map(field.blocks?.map((block)=>[
|
|
345
|
+
block.slug,
|
|
346
|
+
block
|
|
347
|
+
]));
|
|
323
348
|
for (const blockItem of fieldValue){
|
|
324
349
|
if (!isRecord(blockItem)) continue;
|
|
325
350
|
const blockType = typeof blockItem.blockType === "string" ? blockItem.blockType : typeof blockItem.type === "string" ? blockItem.type : typeof blockItem.slug === "string" ? blockItem.slug : null;
|
|
@@ -327,7 +352,7 @@ const collectProposalBlockTypes = ({ data, fields })=>{
|
|
|
327
352
|
foundBlockTypes.add(blockType);
|
|
328
353
|
}
|
|
329
354
|
if (blockType) {
|
|
330
|
-
const blockConfig =
|
|
355
|
+
const blockConfig = blocksBySlug.get(blockType);
|
|
331
356
|
if (blockConfig?.fields?.length) {
|
|
332
357
|
visitFields(blockConfig.fields, blockItem);
|
|
333
358
|
}
|
|
@@ -410,11 +435,15 @@ const collectRelationshipTargets = ({ data, fields, path = "" })=>{
|
|
|
410
435
|
continue;
|
|
411
436
|
}
|
|
412
437
|
if (field.type === "blocks" && Array.isArray(fieldValue) && field.blocks?.length) {
|
|
438
|
+
const blocksBySlug = new Map(field.blocks.map((block)=>[
|
|
439
|
+
block.slug,
|
|
440
|
+
block
|
|
441
|
+
]));
|
|
413
442
|
fieldValue.forEach((item, index)=>{
|
|
414
443
|
if (!isRecord(item)) return;
|
|
415
444
|
const blockType = typeof item.blockType === "string" ? item.blockType : typeof item.type === "string" ? item.type : typeof item.slug === "string" ? item.slug : null;
|
|
416
445
|
if (!blockType) return;
|
|
417
|
-
const blockConfig =
|
|
446
|
+
const blockConfig = blocksBySlug.get(blockType);
|
|
418
447
|
if (!blockConfig?.fields?.length) return;
|
|
419
448
|
targets.push(...collectRelationshipTargets({
|
|
420
449
|
data: item,
|
|
@@ -426,13 +455,91 @@ const collectRelationshipTargets = ({ data, fields, path = "" })=>{
|
|
|
426
455
|
}
|
|
427
456
|
return targets;
|
|
428
457
|
};
|
|
458
|
+
const collectUploadTargets = ({ data, fields, path = "" })=>{
|
|
459
|
+
const targets = [];
|
|
460
|
+
for (const field of fields){
|
|
461
|
+
if (!field.name) continue;
|
|
462
|
+
const fieldPath = path ? `${path}.${field.name}` : field.name;
|
|
463
|
+
const fieldValue = data[field.name];
|
|
464
|
+
if (fieldValue === undefined || fieldValue === null) continue;
|
|
465
|
+
if (field.type === "upload") {
|
|
466
|
+
const relationTargets = Array.isArray(field.relationTo) ? field.relationTo.filter((item)=>typeof item === "string") : typeof field.relationTo === "string" ? [
|
|
467
|
+
field.relationTo
|
|
468
|
+
] : [];
|
|
469
|
+
const collectSingle = (value, itemPath)=>{
|
|
470
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
471
|
+
if (relationTargets.length === 1) {
|
|
472
|
+
targets.push({
|
|
473
|
+
collection: relationTargets[0],
|
|
474
|
+
id: value,
|
|
475
|
+
path: itemPath
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (!isRecord(value)) return;
|
|
481
|
+
const relationTo = typeof value.relationTo === "string" ? value.relationTo : relationTargets[0];
|
|
482
|
+
const id = typeof value.id === "string" || typeof value.id === "number" ? value.id : typeof value.value === "string" || typeof value.value === "number" ? value.value : undefined;
|
|
483
|
+
if (!relationTo || id === undefined) return;
|
|
484
|
+
targets.push({
|
|
485
|
+
collection: relationTo,
|
|
486
|
+
id,
|
|
487
|
+
path: itemPath
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
if (field.hasMany && Array.isArray(fieldValue)) {
|
|
491
|
+
fieldValue.forEach((item, index)=>collectSingle(item, `${fieldPath}.${index}`));
|
|
492
|
+
} else {
|
|
493
|
+
collectSingle(fieldValue, fieldPath);
|
|
494
|
+
}
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (field.type === "group" && isRecord(fieldValue) && field.fields?.length) {
|
|
498
|
+
targets.push(...collectUploadTargets({
|
|
499
|
+
data: fieldValue,
|
|
500
|
+
fields: field.fields,
|
|
501
|
+
path: fieldPath
|
|
502
|
+
}));
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (field.type === "array" && Array.isArray(fieldValue) && field.fields?.length) {
|
|
506
|
+
fieldValue.forEach((item, index)=>{
|
|
507
|
+
if (!isRecord(item)) return;
|
|
508
|
+
targets.push(...collectUploadTargets({
|
|
509
|
+
data: item,
|
|
510
|
+
fields: field.fields,
|
|
511
|
+
path: `${fieldPath}.${index}`
|
|
512
|
+
}));
|
|
513
|
+
});
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (field.type === "blocks" && Array.isArray(fieldValue) && field.blocks?.length) {
|
|
517
|
+
const blocksBySlug = new Map(field.blocks.map((block)=>[
|
|
518
|
+
block.slug,
|
|
519
|
+
block
|
|
520
|
+
]));
|
|
521
|
+
fieldValue.forEach((item, index)=>{
|
|
522
|
+
if (!isRecord(item)) return;
|
|
523
|
+
const blockType = typeof item.blockType === "string" ? item.blockType : typeof item.type === "string" ? item.type : typeof item.slug === "string" ? item.slug : null;
|
|
524
|
+
if (!blockType) return;
|
|
525
|
+
const blockConfig = blocksBySlug.get(blockType);
|
|
526
|
+
if (!blockConfig?.fields?.length) return;
|
|
527
|
+
targets.push(...collectUploadTargets({
|
|
528
|
+
data: item,
|
|
529
|
+
fields: blockConfig.fields,
|
|
530
|
+
path: `${fieldPath}.${index}`
|
|
531
|
+
}));
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return targets;
|
|
536
|
+
};
|
|
429
537
|
const validateRelationshipTargetsExist = async ({ data, fields, req })=>{
|
|
430
538
|
const targets = collectRelationshipTargets({
|
|
431
539
|
data,
|
|
432
540
|
fields
|
|
433
541
|
});
|
|
434
|
-
const
|
|
435
|
-
for (const target of targets){
|
|
542
|
+
const targetResults = await Promise.all(targets.map(async (target)=>{
|
|
436
543
|
try {
|
|
437
544
|
await req.payload.findByID({
|
|
438
545
|
collection: target.collection,
|
|
@@ -441,11 +548,19 @@ const validateRelationshipTargetsExist = async ({ data, fields, req })=>{
|
|
|
441
548
|
overrideAccess: false,
|
|
442
549
|
req
|
|
443
550
|
});
|
|
551
|
+
return null;
|
|
444
552
|
} catch {
|
|
445
|
-
|
|
553
|
+
return target;
|
|
446
554
|
}
|
|
447
|
-
}
|
|
448
|
-
return
|
|
555
|
+
}));
|
|
556
|
+
return targetResults.filter((target)=>Boolean(target));
|
|
557
|
+
};
|
|
558
|
+
const getUploadTargetsOutsideAttachments = ({ allowedAttachmentKeys, data, fields })=>{
|
|
559
|
+
if (allowedAttachmentKeys.size === 0) return [];
|
|
560
|
+
return collectUploadTargets({
|
|
561
|
+
data,
|
|
562
|
+
fields
|
|
563
|
+
}).filter((target)=>!allowedAttachmentKeys.has(`${target.collection}:${String(target.id)}`));
|
|
449
564
|
};
|
|
450
565
|
const getMentionSummary = (mentions)=>mentions?.map((mention)=>({
|
|
451
566
|
collection: "collection" in mention ? mention.collection : undefined,
|
|
@@ -453,6 +568,39 @@ const getMentionSummary = (mentions)=>mentions?.map((mention)=>({
|
|
|
453
568
|
slug: mention.slug,
|
|
454
569
|
type: mention.type
|
|
455
570
|
})) || [];
|
|
571
|
+
const getMediaAttachmentContext = async ({ allowedCollectionsBySlug, attachments, collections, req })=>{
|
|
572
|
+
if (!attachments?.length) return [];
|
|
573
|
+
const contexts = [];
|
|
574
|
+
const seen = new Set();
|
|
575
|
+
for (const attachment of attachments.slice(0, 8)){
|
|
576
|
+
if (attachment.type !== "media" || !attachment.collection || !attachment.id) continue;
|
|
577
|
+
const key = `${attachment.collection}:${attachment.id}`;
|
|
578
|
+
if (seen.has(key)) continue;
|
|
579
|
+
const collectionConfig = allowedCollectionsBySlug.get(attachment.collection);
|
|
580
|
+
if (!collectionConfig?.upload) continue;
|
|
581
|
+
const doc = await req.payload.findByID({
|
|
582
|
+
collection: attachment.collection,
|
|
583
|
+
depth: 1,
|
|
584
|
+
id: attachment.id,
|
|
585
|
+
overrideAccess: false,
|
|
586
|
+
req
|
|
587
|
+
}).catch(()=>null);
|
|
588
|
+
if (!doc) continue;
|
|
589
|
+
seen.add(key);
|
|
590
|
+
contexts.push({
|
|
591
|
+
attachment,
|
|
592
|
+
collection: attachment.collection,
|
|
593
|
+
doc,
|
|
594
|
+
schema: describeCollectionLikeConfig({
|
|
595
|
+
config: collectionConfig,
|
|
596
|
+
permissions: collections,
|
|
597
|
+
type: "collection"
|
|
598
|
+
}),
|
|
599
|
+
type: "mediaAttachment"
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
return contexts;
|
|
603
|
+
};
|
|
456
604
|
const formatProposalIssuesForRetry = (issues)=>{
|
|
457
605
|
return issues.slice(0, 6).map((issue)=>{
|
|
458
606
|
switch(issue.code){
|
|
@@ -500,9 +648,19 @@ const createCollectionAliasMap = (collections)=>{
|
|
|
500
648
|
};
|
|
501
649
|
const getLikelyCollectionMatches = ({ aliasMap, prompt })=>{
|
|
502
650
|
const normalizedPrompt = prompt.toLowerCase();
|
|
503
|
-
const matches =
|
|
651
|
+
const matches = new Set();
|
|
652
|
+
const aliases = Object.keys(aliasMap).sort((a, b)=>b.length - a.length);
|
|
653
|
+
const aliasPattern = aliases.length > 0 ? new RegExp(aliases.map((alias)=>alias.replace(regexSpecialCharactersPattern, "\\$&")).join("|"), "g") : null;
|
|
654
|
+
if (!aliasPattern) return [];
|
|
655
|
+
for (const match of normalizedPrompt.matchAll(aliasPattern)){
|
|
656
|
+
const alias = match[0];
|
|
657
|
+
const slug = aliasMap[alias];
|
|
658
|
+
if (slug) {
|
|
659
|
+
matches.add(slug);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
504
662
|
return [
|
|
505
|
-
...
|
|
663
|
+
...matches
|
|
506
664
|
];
|
|
507
665
|
};
|
|
508
666
|
const hasWriteIntent = (prompt)=>{
|
|
@@ -517,15 +675,17 @@ const getIntentToolChoice = (prompt)=>{
|
|
|
517
675
|
type: "tool"
|
|
518
676
|
};
|
|
519
677
|
}
|
|
520
|
-
|
|
678
|
+
// Update existing document (add block, modify content, etc.)
|
|
679
|
+
if (/\b(update|edit|change|modify|einfügen|hinzufügen|addieren)\b/.test(normalizedPrompt)) {
|
|
521
680
|
return {
|
|
522
|
-
toolName: "
|
|
681
|
+
toolName: "proposeUpdateDoc",
|
|
523
682
|
type: "tool"
|
|
524
683
|
};
|
|
525
684
|
}
|
|
526
|
-
|
|
685
|
+
// Create a new document
|
|
686
|
+
if (/\b(create|build|generate|write|draft|make|add|füge|einfügen|hinzufügen|addieren)\b/.test(normalizedPrompt)) {
|
|
527
687
|
return {
|
|
528
|
-
toolName: "
|
|
688
|
+
toolName: "proposeCreateDoc",
|
|
529
689
|
type: "tool"
|
|
530
690
|
};
|
|
531
691
|
}
|
|
@@ -544,7 +704,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
544
704
|
}, {
|
|
545
705
|
status: 400
|
|
546
706
|
});
|
|
547
|
-
const selectedLocales =
|
|
707
|
+
const selectedLocales = [];
|
|
708
|
+
const selectedLocaleSet = new Set();
|
|
709
|
+
for (const mention of body?.mentions || []){
|
|
710
|
+
if (mention.type !== "locale" || !mention.slug || selectedLocaleSet.has(mention.slug)) continue;
|
|
711
|
+
selectedLocaleSet.add(mention.slug);
|
|
712
|
+
selectedLocales.push(mention.slug);
|
|
713
|
+
}
|
|
548
714
|
const activeLocale = selectedLocales.at(-1);
|
|
549
715
|
const mentionSummary = getMentionSummary(body?.mentions);
|
|
550
716
|
if (e2eModeEnabled()) {
|
|
@@ -554,23 +720,67 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
554
720
|
});
|
|
555
721
|
}
|
|
556
722
|
const user = req.user;
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
status: 400
|
|
723
|
+
const exceededTokenUsageLimit = await getExceededTokenUsageLimit({
|
|
724
|
+
maxTokenUsage: options.maxTokenUsage,
|
|
725
|
+
req,
|
|
726
|
+
userID: user.id
|
|
562
727
|
});
|
|
563
|
-
|
|
564
|
-
|
|
728
|
+
if (exceededTokenUsageLimit) {
|
|
729
|
+
const scope = options.maxTokenUsage?.type === "site" ? "site" : "user";
|
|
730
|
+
const periodLabel = exceededTokenUsageLimit.period === "day" ? "Daily" : "Weekly";
|
|
731
|
+
logHandlerEvent(req, "warn", {
|
|
732
|
+
limit: exceededTokenUsageLimit.limit,
|
|
733
|
+
msg: "AI chat blocked: token usage limit reached",
|
|
734
|
+
period: exceededTokenUsageLimit.period,
|
|
735
|
+
scope,
|
|
736
|
+
used: exceededTokenUsageLimit.used,
|
|
737
|
+
userID: String(user.id)
|
|
738
|
+
});
|
|
739
|
+
return Response.json({
|
|
740
|
+
error: `${periodLabel} AI token limit reached for this ${scope}.`,
|
|
741
|
+
limit: exceededTokenUsageLimit.limit,
|
|
742
|
+
period: exceededTokenUsageLimit.period,
|
|
743
|
+
used: exceededTokenUsageLimit.used
|
|
744
|
+
}, {
|
|
745
|
+
status: 429
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
const managedProviders = options.providers?.length ? options.providers : null;
|
|
749
|
+
const requestedProvider = body?.provider || (managedProviders ? managedProviders[0].id : user.aiProvider || "openai");
|
|
750
|
+
const managedProvider = managedProviders?.find((providerConfig)=>providerConfig.id === requestedProvider);
|
|
751
|
+
if (managedProviders && !managedProvider) {
|
|
752
|
+
return Response.json({
|
|
753
|
+
error: `Unsupported AI provider: ${requestedProvider}`
|
|
754
|
+
}, {
|
|
755
|
+
status: 400
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
if (!managedProvider && !isAIProvider(requestedProvider)) {
|
|
759
|
+
return Response.json({
|
|
760
|
+
error: `Unsupported AI provider: ${requestedProvider}`
|
|
761
|
+
}, {
|
|
762
|
+
status: 400
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
const provider = managedProvider?.provider || requestedProvider;
|
|
766
|
+
const requestedModel = body?.model || managedProvider?.defaultModel;
|
|
767
|
+
if (managedProvider && requestedModel && !managedProvider.models.some((model)=>model.value === requestedModel)) {
|
|
768
|
+
return Response.json({
|
|
769
|
+
error: `Unsupported model "${requestedModel}" for AI provider "${managedProvider.id}".`
|
|
770
|
+
}, {
|
|
771
|
+
status: 400
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
const userApiKey = managedProvider ? managedProvider.apiKey : options.allowUserApiKeys === false ? null : user.aiApiKey;
|
|
565
775
|
const providerConfig = getProviderConfig({
|
|
566
776
|
apiKey: userApiKey,
|
|
567
777
|
defaultModels: options.models?.defaults,
|
|
568
|
-
model:
|
|
778
|
+
model: requestedModel,
|
|
569
779
|
provider
|
|
570
780
|
});
|
|
571
781
|
const debug = {
|
|
572
782
|
model: providerConfig.modelID,
|
|
573
|
-
provider,
|
|
783
|
+
provider: managedProvider?.id || provider,
|
|
574
784
|
tools: [
|
|
575
785
|
"getDoc",
|
|
576
786
|
"getGlobal",
|
|
@@ -601,7 +811,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
601
811
|
selectedLocales
|
|
602
812
|
});
|
|
603
813
|
return Response.json({
|
|
604
|
-
error: options.allowUserApiKeys === false ? `Configure a ${provider} API key in the server environment first.` : `Add a ${provider} API key to your account settings or configure it in the server environment first.`
|
|
814
|
+
error: managedProvider ? `Configure a ${managedProvider?.id || provider} API key in the plugin config or server environment first.` : options.allowUserApiKeys === false ? `Configure a ${provider} API key in the server environment first.` : `Add a ${provider} API key to your account settings or configure it in the server environment first.`
|
|
605
815
|
}, {
|
|
606
816
|
status: 400
|
|
607
817
|
});
|
|
@@ -654,10 +864,43 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
654
864
|
});
|
|
655
865
|
return signedProposal;
|
|
656
866
|
};
|
|
657
|
-
const
|
|
658
|
-
const
|
|
659
|
-
const
|
|
660
|
-
|
|
867
|
+
const requestedDocumentScope = body?.documentScope;
|
|
868
|
+
const configuredCollectionSlugs = getAllowedCollectionSlugs(req, options.collections);
|
|
869
|
+
const configuredCollectionSlugSet = new Set(configuredCollectionSlugs);
|
|
870
|
+
const allGlobalConfigs = req.payload.config.globals || [];
|
|
871
|
+
const requestedCollectionSlug = requestedDocumentScope?.type === "collection" && typeof requestedDocumentScope.collection === "string" ? requestedDocumentScope.collection.trim() : undefined;
|
|
872
|
+
const requestedGlobalSlug = requestedDocumentScope?.type === "global" && typeof requestedDocumentScope.slug === "string" ? requestedDocumentScope.slug.trim() : undefined;
|
|
873
|
+
const requestedDocumentID = requestedDocumentScope?.type === "collection" && typeof requestedDocumentScope.id === "string" ? requestedDocumentScope.id.trim() : undefined;
|
|
874
|
+
if (requestedDocumentScope?.type === "collection" && (!requestedCollectionSlug || !configuredCollectionSlugSet.has(requestedCollectionSlug))) {
|
|
875
|
+
return Response.json({
|
|
876
|
+
error: "The current collection is not available to the AI assistant."
|
|
877
|
+
}, {
|
|
878
|
+
status: 400
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
if (requestedDocumentScope?.type === "global" && (!requestedGlobalSlug || !allGlobalConfigs.some((global)=>global.slug === requestedGlobalSlug))) {
|
|
882
|
+
return Response.json({
|
|
883
|
+
error: "The current global is not available to the AI assistant."
|
|
884
|
+
}, {
|
|
885
|
+
status: 400
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
const collectionSlugs = requestedDocumentScope ? requestedCollectionSlug ? [
|
|
889
|
+
requestedCollectionSlug
|
|
890
|
+
] : [] : configuredCollectionSlugs;
|
|
891
|
+
const collectionSlugSet = new Set(collectionSlugs);
|
|
892
|
+
const globalConfigs = requestedDocumentScope ? requestedGlobalSlug ? allGlobalConfigs.filter((global)=>global.slug === requestedGlobalSlug) : [] : allGlobalConfigs;
|
|
893
|
+
const globalSlugs = globalConfigs.map((global)=>global.slug);
|
|
894
|
+
const globalConfigsBySlug = new Map(globalConfigs.map((global)=>[
|
|
895
|
+
global.slug,
|
|
896
|
+
global
|
|
897
|
+
]));
|
|
898
|
+
const allowedCollections = req.payload.config.collections.filter((collection)=>collectionSlugSet.has(collection.slug));
|
|
899
|
+
const allowedCollectionsBySlug = new Map(allowedCollections.map((collection)=>[
|
|
900
|
+
collection.slug,
|
|
901
|
+
collection
|
|
902
|
+
]));
|
|
903
|
+
if (collectionSlugs.length === 0 && globalConfigs.length === 0) {
|
|
661
904
|
logHandlerEvent(req, "warn", {
|
|
662
905
|
debug,
|
|
663
906
|
msg: "AI chat blocked: no AI-enabled collections configured"
|
|
@@ -673,10 +916,10 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
673
916
|
fields: collection.fields,
|
|
674
917
|
parent: collection.slug
|
|
675
918
|
})),
|
|
676
|
-
...
|
|
919
|
+
...globalConfigs.flatMap((global)=>collectBlocks({
|
|
677
920
|
fields: global.fields,
|
|
678
921
|
parent: global.slug
|
|
679
|
-
}))
|
|
922
|
+
}))
|
|
680
923
|
];
|
|
681
924
|
const mentionContext = await getMentionContext({
|
|
682
925
|
blockContexts,
|
|
@@ -686,15 +929,60 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
686
929
|
mentions: body?.mentions,
|
|
687
930
|
req
|
|
688
931
|
});
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
932
|
+
if (requestedCollectionSlug && requestedDocumentID) {
|
|
933
|
+
const currentDocument = await req.payload.findByID({
|
|
934
|
+
collection: requestedCollectionSlug,
|
|
935
|
+
depth: 2,
|
|
936
|
+
id: requestedDocumentID,
|
|
937
|
+
...activeLocale ? {
|
|
938
|
+
locale: activeLocale
|
|
939
|
+
} : {},
|
|
940
|
+
overrideAccess: false,
|
|
941
|
+
req
|
|
942
|
+
});
|
|
943
|
+
mentionContext.push({
|
|
944
|
+
collection: requestedCollectionSlug,
|
|
945
|
+
document: currentDocument,
|
|
946
|
+
id: requestedDocumentID,
|
|
947
|
+
type: "currentDocument"
|
|
948
|
+
});
|
|
949
|
+
} else if (requestedGlobalSlug) {
|
|
950
|
+
const currentGlobal = await req.payload.findGlobal({
|
|
951
|
+
depth: 2,
|
|
952
|
+
...activeLocale ? {
|
|
953
|
+
locale: activeLocale
|
|
954
|
+
} : {},
|
|
955
|
+
overrideAccess: false,
|
|
956
|
+
req,
|
|
957
|
+
slug: requestedGlobalSlug
|
|
958
|
+
});
|
|
959
|
+
mentionContext.push({
|
|
960
|
+
global: currentGlobal,
|
|
961
|
+
slug: requestedGlobalSlug,
|
|
962
|
+
type: "currentGlobal"
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
const mediaAttachmentContext = await getMediaAttachmentContext({
|
|
966
|
+
allowedCollectionsBySlug,
|
|
967
|
+
attachments: body?.attachments,
|
|
968
|
+
collections: options.collections,
|
|
969
|
+
req
|
|
970
|
+
});
|
|
971
|
+
const allowedAttachmentKeys = new Set(mediaAttachmentContext.flatMap((context)=>{
|
|
972
|
+
const attachment = context.attachment;
|
|
973
|
+
return isRecord(attachment) && typeof attachment.collection === "string" && typeof attachment.id === "string" ? [
|
|
974
|
+
`${attachment.collection}:${attachment.id}`
|
|
975
|
+
] : [];
|
|
976
|
+
}));
|
|
977
|
+
mentionContext.push(...mediaAttachmentContext);
|
|
978
|
+
const mentionedCollectionSlugs = [];
|
|
979
|
+
const mentionedCollectionSlugSet = new Set();
|
|
980
|
+
for (const mention of body?.mentions || []){
|
|
981
|
+
const slug = mention.type === "collection" && mention.slug ? mention.slug : mention.type === "doc" && mention.collection ? mention.collection : null;
|
|
982
|
+
if (!slug || !collectionSlugSet.has(slug) || mentionedCollectionSlugSet.has(slug)) continue;
|
|
983
|
+
mentionedCollectionSlugSet.add(slug);
|
|
984
|
+
mentionedCollectionSlugs.push(slug);
|
|
985
|
+
}
|
|
698
986
|
const createRequiredFieldsByCollection = Object.fromEntries(allowedCollections.map((collection)=>[
|
|
699
987
|
collection.slug,
|
|
700
988
|
getRequiredFieldInfos(collection.fields, collection.admin?.useAsTitle)
|
|
@@ -721,8 +1009,8 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
721
1009
|
prompt
|
|
722
1010
|
});
|
|
723
1011
|
const writeIntent = hasWriteIntent(prompt);
|
|
724
|
-
const inferredCollectionSlug = mentionedCollectionSlugs.length === 1 ? mentionedCollectionSlugs[0] : mentionedCollectionSlugs.length === 0 && likelyCollectionMatches.length === 1 ? likelyCollectionMatches[0] : undefined;
|
|
725
|
-
const inferredCollectionConfig = inferredCollectionSlug ?
|
|
1012
|
+
const inferredCollectionSlug = requestedCollectionSlug || (mentionedCollectionSlugs.length === 1 ? mentionedCollectionSlugs[0] : mentionedCollectionSlugs.length === 0 && likelyCollectionMatches.length === 1 ? likelyCollectionMatches[0] : undefined);
|
|
1013
|
+
const inferredCollectionConfig = inferredCollectionSlug ? allowedCollectionsBySlug.get(inferredCollectionSlug) : undefined;
|
|
726
1014
|
if (inferredCollectionConfig && !mentionContext.some((item)=>item.type === "collection" && item.slug === inferredCollectionConfig.slug)) {
|
|
727
1015
|
mentionContext.push({
|
|
728
1016
|
...describeCollectionLikeConfig({
|
|
@@ -733,7 +1021,16 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
733
1021
|
inferredFromPrompt: true
|
|
734
1022
|
});
|
|
735
1023
|
}
|
|
736
|
-
|
|
1024
|
+
if (requestedGlobalSlug) {
|
|
1025
|
+
const currentGlobalConfig = globalConfigsBySlug.get(requestedGlobalSlug);
|
|
1026
|
+
if (currentGlobalConfig) {
|
|
1027
|
+
mentionContext.push(describeCollectionLikeConfig({
|
|
1028
|
+
config: currentGlobalConfig,
|
|
1029
|
+
type: "global"
|
|
1030
|
+
}));
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
let intentToolChoice = inferredCollectionConfig ? getIntentToolChoice(prompt) : undefined;
|
|
737
1034
|
logHandlerEvent(req, "info", {
|
|
738
1035
|
activeLocale,
|
|
739
1036
|
allowedCollectionCount: allowedCollections.length,
|
|
@@ -747,7 +1044,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
747
1044
|
selectedLocales,
|
|
748
1045
|
writeIntent
|
|
749
1046
|
});
|
|
750
|
-
const collectionSlugSchema = z.enum(collectionSlugs);
|
|
1047
|
+
const collectionSlugSchema = collectionSlugs.length > 0 ? z.enum(collectionSlugs) : z.string().refine(()=>false, "No collection is in scope.");
|
|
751
1048
|
const getDisallowedCollectionActionError = (collection, action)=>{
|
|
752
1049
|
if (isCollectionActionAllowed({
|
|
753
1050
|
action,
|
|
@@ -761,14 +1058,21 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
761
1058
|
tool: "collectionPermissionCheck"
|
|
762
1059
|
});
|
|
763
1060
|
};
|
|
764
|
-
const
|
|
1061
|
+
const allTools = {
|
|
765
1062
|
getDoc: {
|
|
766
|
-
description: "Read
|
|
1063
|
+
description: "Read a document by collection and id.",
|
|
767
1064
|
inputSchema: z.object({
|
|
768
1065
|
collection: collectionSlugSchema,
|
|
769
1066
|
id: z.string().min(1)
|
|
770
1067
|
}),
|
|
771
1068
|
execute: async ({ collection, id })=>{
|
|
1069
|
+
if (requestedDocumentScope && (collection !== requestedCollectionSlug || id !== requestedDocumentID)) {
|
|
1070
|
+
return createToolError({
|
|
1071
|
+
collection,
|
|
1072
|
+
message: "Only the current document can be read in this context.",
|
|
1073
|
+
tool: "getDoc"
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
772
1076
|
return req.payload.findByID({
|
|
773
1077
|
collection: collection,
|
|
774
1078
|
depth: 2,
|
|
@@ -782,13 +1086,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
782
1086
|
}
|
|
783
1087
|
},
|
|
784
1088
|
listCollections: {
|
|
785
|
-
description: "List
|
|
1089
|
+
description: "List collections; pass slug for one full schema.",
|
|
786
1090
|
inputSchema: z.object({
|
|
787
1091
|
slug: collectionSlugSchema.optional()
|
|
788
1092
|
}),
|
|
789
1093
|
execute: async ({ slug })=>{
|
|
790
1094
|
if (slug) {
|
|
791
|
-
const collection =
|
|
1095
|
+
const collection = allowedCollectionsBySlug.get(slug);
|
|
792
1096
|
if (!collection) {
|
|
793
1097
|
return createToolError({
|
|
794
1098
|
message: `Unknown collection: ${slug}`,
|
|
@@ -810,12 +1114,19 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
810
1114
|
}
|
|
811
1115
|
},
|
|
812
1116
|
getGlobal: {
|
|
813
|
-
description: "Read
|
|
1117
|
+
description: "Read a global by slug.",
|
|
814
1118
|
inputSchema: z.object({
|
|
815
1119
|
slug: z.string().min(1)
|
|
816
1120
|
}),
|
|
817
1121
|
execute: async ({ slug })=>{
|
|
818
|
-
|
|
1122
|
+
if (requestedDocumentScope && slug !== requestedGlobalSlug) {
|
|
1123
|
+
return createToolError({
|
|
1124
|
+
message: "Only the current global can be read in this context.",
|
|
1125
|
+
slug,
|
|
1126
|
+
tool: "getGlobal"
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
const globalConfig = globalConfigsBySlug.get(slug);
|
|
819
1130
|
if (!globalConfig) {
|
|
820
1131
|
return createToolError({
|
|
821
1132
|
message: `Unknown global: ${slug}`,
|
|
@@ -835,14 +1146,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
835
1146
|
}
|
|
836
1147
|
},
|
|
837
1148
|
listGlobals: {
|
|
838
|
-
description: "List
|
|
1149
|
+
description: "List globals; pass slug for one full schema.",
|
|
839
1150
|
inputSchema: z.object({
|
|
840
1151
|
slug: z.string().optional()
|
|
841
1152
|
}),
|
|
842
1153
|
execute: async ({ slug })=>{
|
|
843
|
-
const globals = req.payload.config.globals || [];
|
|
844
1154
|
if (slug) {
|
|
845
|
-
const global =
|
|
1155
|
+
const global = globalConfigsBySlug.get(slug);
|
|
846
1156
|
if (!global) {
|
|
847
1157
|
return createToolError({
|
|
848
1158
|
message: `Unknown global: ${slug}`,
|
|
@@ -855,14 +1165,14 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
855
1165
|
type: "global"
|
|
856
1166
|
});
|
|
857
1167
|
}
|
|
858
|
-
return
|
|
1168
|
+
return globalConfigs.map((global)=>describeCollectionLikeSummary({
|
|
859
1169
|
config: global,
|
|
860
1170
|
type: "global"
|
|
861
1171
|
}));
|
|
862
1172
|
}
|
|
863
1173
|
},
|
|
864
1174
|
proposeCreateDoc: {
|
|
865
|
-
description: "
|
|
1175
|
+
description: "Propose document creation. Use exact schema fields; include required fields. Use localizedData for multi-locale writes.",
|
|
866
1176
|
inputSchema: z.object({
|
|
867
1177
|
collection: collectionSlugSchema,
|
|
868
1178
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
@@ -874,7 +1184,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
874
1184
|
execute: async ({ collection, data, label, localizedData })=>{
|
|
875
1185
|
const permissionError = getDisallowedCollectionActionError(collection, "create");
|
|
876
1186
|
if (permissionError) return permissionError;
|
|
877
|
-
const collectionConfig =
|
|
1187
|
+
const collectionConfig = allowedCollectionsBySlug.get(collection);
|
|
878
1188
|
const collectionFields = collectionConfig?.fields || [];
|
|
879
1189
|
const preparedData = prepareProposalWriteData({
|
|
880
1190
|
collectionConfig: collectionConfig,
|
|
@@ -935,6 +1245,31 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
935
1245
|
});
|
|
936
1246
|
}
|
|
937
1247
|
}
|
|
1248
|
+
const uploadTargetsOutsideAttachments = [
|
|
1249
|
+
...preparedData.data ? getUploadTargetsOutsideAttachments({
|
|
1250
|
+
allowedAttachmentKeys,
|
|
1251
|
+
data: preparedData.data,
|
|
1252
|
+
fields: collectionFields
|
|
1253
|
+
}) : [],
|
|
1254
|
+
...preparedData.localizedData ? Object.values(preparedData.localizedData).flatMap((localeData)=>getUploadTargetsOutsideAttachments({
|
|
1255
|
+
allowedAttachmentKeys,
|
|
1256
|
+
data: localeData,
|
|
1257
|
+
fields: collectionFields
|
|
1258
|
+
})) : []
|
|
1259
|
+
];
|
|
1260
|
+
if (uploadTargetsOutsideAttachments.length > 0) {
|
|
1261
|
+
return createToolError({
|
|
1262
|
+
collection,
|
|
1263
|
+
details: {
|
|
1264
|
+
allowedAttachments: [
|
|
1265
|
+
...allowedAttachmentKeys
|
|
1266
|
+
],
|
|
1267
|
+
uploadTargetsOutsideAttachments
|
|
1268
|
+
},
|
|
1269
|
+
message: `Create proposal for ${collection} uses upload references that are not in the uploaded attachments: ${uploadTargetsOutsideAttachments.map((target)=>`${target.path} -> ${target.collection}:${target.id}`).join(", ")}. Use only uploaded media attachment IDs for upload fields.`,
|
|
1270
|
+
tool: "proposeCreateDoc"
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
938
1273
|
const invalidRelationshipTargets = [
|
|
939
1274
|
...preparedData.data ? await validateRelationshipTargetsExist({
|
|
940
1275
|
data: preparedData.data,
|
|
@@ -978,13 +1313,20 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
978
1313
|
}
|
|
979
1314
|
},
|
|
980
1315
|
proposeDeleteDoc: {
|
|
981
|
-
description: "
|
|
1316
|
+
description: "Propose document deletion.",
|
|
982
1317
|
inputSchema: z.object({
|
|
983
1318
|
collection: collectionSlugSchema,
|
|
984
1319
|
id: z.string().min(1),
|
|
985
1320
|
label: z.string().min(1)
|
|
986
1321
|
}),
|
|
987
1322
|
execute: async ({ collection, id, label })=>{
|
|
1323
|
+
if (requestedDocumentScope && (collection !== requestedCollectionSlug || id !== requestedDocumentID)) {
|
|
1324
|
+
return createToolError({
|
|
1325
|
+
collection,
|
|
1326
|
+
message: "Only the current document can be deleted in this context.",
|
|
1327
|
+
tool: "proposeDeleteDoc"
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
988
1330
|
const permissionError = getDisallowedCollectionActionError(collection, "delete");
|
|
989
1331
|
if (permissionError) return permissionError;
|
|
990
1332
|
const proposal = {
|
|
@@ -1000,7 +1342,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1000
1342
|
}
|
|
1001
1343
|
},
|
|
1002
1344
|
proposeUpdateDoc: {
|
|
1003
|
-
description: "
|
|
1345
|
+
description: "Propose document update. Use exact schema fields. Use localizedData for multi-locale writes.",
|
|
1004
1346
|
inputSchema: z.object({
|
|
1005
1347
|
collection: collectionSlugSchema,
|
|
1006
1348
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
@@ -1011,9 +1353,16 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1011
1353
|
message: "Either data or localizedData is required."
|
|
1012
1354
|
}),
|
|
1013
1355
|
execute: async ({ collection, data, id, label, localizedData })=>{
|
|
1356
|
+
if (requestedDocumentScope && (collection !== requestedCollectionSlug || id !== requestedDocumentID)) {
|
|
1357
|
+
return createToolError({
|
|
1358
|
+
collection,
|
|
1359
|
+
message: "Only the current document can be updated in this context.",
|
|
1360
|
+
tool: "proposeUpdateDoc"
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1014
1363
|
const permissionError = getDisallowedCollectionActionError(collection, "update");
|
|
1015
1364
|
if (permissionError) return permissionError;
|
|
1016
|
-
const collectionConfig =
|
|
1365
|
+
const collectionConfig = allowedCollectionsBySlug.get(collection);
|
|
1017
1366
|
const collectionFields = collectionConfig?.fields || [];
|
|
1018
1367
|
const preparedData = prepareProposalWriteData({
|
|
1019
1368
|
collectionConfig: collectionConfig,
|
|
@@ -1055,6 +1404,31 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1055
1404
|
tool: "proposeUpdateDoc"
|
|
1056
1405
|
});
|
|
1057
1406
|
}
|
|
1407
|
+
const uploadTargetsOutsideAttachments = [
|
|
1408
|
+
...preparedData.data ? getUploadTargetsOutsideAttachments({
|
|
1409
|
+
allowedAttachmentKeys,
|
|
1410
|
+
data: preparedData.data,
|
|
1411
|
+
fields: collectionFields
|
|
1412
|
+
}) : [],
|
|
1413
|
+
...preparedData.localizedData ? Object.values(preparedData.localizedData).flatMap((localeData)=>getUploadTargetsOutsideAttachments({
|
|
1414
|
+
allowedAttachmentKeys,
|
|
1415
|
+
data: localeData,
|
|
1416
|
+
fields: collectionFields
|
|
1417
|
+
})) : []
|
|
1418
|
+
];
|
|
1419
|
+
if (uploadTargetsOutsideAttachments.length > 0) {
|
|
1420
|
+
return createToolError({
|
|
1421
|
+
collection,
|
|
1422
|
+
details: {
|
|
1423
|
+
allowedAttachments: [
|
|
1424
|
+
...allowedAttachmentKeys
|
|
1425
|
+
],
|
|
1426
|
+
uploadTargetsOutsideAttachments
|
|
1427
|
+
},
|
|
1428
|
+
message: `Update proposal for ${collection} uses upload references that are not in the uploaded attachments: ${uploadTargetsOutsideAttachments.map((target)=>`${target.path} -> ${target.collection}:${target.id}`).join(", ")}. Use only uploaded media attachment IDs for upload fields.`,
|
|
1429
|
+
tool: "proposeUpdateDoc"
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1058
1432
|
const proposal = {
|
|
1059
1433
|
action: "update",
|
|
1060
1434
|
collection,
|
|
@@ -1073,7 +1447,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1073
1447
|
}
|
|
1074
1448
|
},
|
|
1075
1449
|
proposeUpdateGlobal: {
|
|
1076
|
-
description: "
|
|
1450
|
+
description: "Propose global update. Use localizedData for multi-locale writes.",
|
|
1077
1451
|
inputSchema: z.object({
|
|
1078
1452
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
1079
1453
|
label: z.string().min(1),
|
|
@@ -1083,7 +1457,14 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1083
1457
|
message: "Either data or localizedData is required."
|
|
1084
1458
|
}),
|
|
1085
1459
|
execute: async ({ data, label, localizedData, slug })=>{
|
|
1086
|
-
|
|
1460
|
+
if (requestedDocumentScope && slug !== requestedGlobalSlug) {
|
|
1461
|
+
return createToolError({
|
|
1462
|
+
message: "Only the current global can be updated in this context.",
|
|
1463
|
+
slug,
|
|
1464
|
+
tool: "proposeUpdateGlobal"
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
const globalConfig = globalConfigsBySlug.get(slug);
|
|
1087
1468
|
if (!globalConfig) {
|
|
1088
1469
|
return createToolError({
|
|
1089
1470
|
message: `Unknown global: ${slug}`,
|
|
@@ -1129,19 +1510,24 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1129
1510
|
}
|
|
1130
1511
|
},
|
|
1131
1512
|
searchDocs: {
|
|
1132
|
-
description: "Search documents in one collection.
|
|
1513
|
+
description: "Search documents in one collection.",
|
|
1133
1514
|
inputSchema: z.object({
|
|
1134
1515
|
collection: collectionSlugSchema,
|
|
1135
1516
|
limit: z.number().int().min(1).max(10).default(5),
|
|
1136
1517
|
query: z.string().optional()
|
|
1137
1518
|
}),
|
|
1138
1519
|
execute: async ({ collection, limit, query })=>{
|
|
1139
|
-
const collectionConfig =
|
|
1140
|
-
const searchableFields = collectionConfig?.fields.
|
|
1520
|
+
const collectionConfig = allowedCollectionsBySlug.get(collection);
|
|
1521
|
+
const searchableFields = collectionConfig?.fields.flatMap((field)=>{
|
|
1522
|
+
if (!("name" in field) || ![
|
|
1141
1523
|
"email",
|
|
1142
1524
|
"text",
|
|
1143
1525
|
"textarea"
|
|
1144
|
-
].includes(field.type)
|
|
1526
|
+
].includes(field.type) || !field.name) return [];
|
|
1527
|
+
return [
|
|
1528
|
+
field.name
|
|
1529
|
+
];
|
|
1530
|
+
}) || [];
|
|
1145
1531
|
const where = query && searchableFields.length > 0 ? {
|
|
1146
1532
|
or: searchableFields.map((field)=>({
|
|
1147
1533
|
[field]: {
|
|
@@ -1163,12 +1549,31 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1163
1549
|
}
|
|
1164
1550
|
}
|
|
1165
1551
|
};
|
|
1552
|
+
const scopedToolNames = requestedCollectionSlug ? requestedDocumentID ? new Set([
|
|
1553
|
+
"getDoc",
|
|
1554
|
+
"listCollections",
|
|
1555
|
+
"proposeUpdateDoc"
|
|
1556
|
+
]) : new Set([
|
|
1557
|
+
"listCollections",
|
|
1558
|
+
"proposeCreateDoc"
|
|
1559
|
+
]) : requestedGlobalSlug ? new Set([
|
|
1560
|
+
"getGlobal",
|
|
1561
|
+
"listGlobals",
|
|
1562
|
+
"proposeUpdateGlobal"
|
|
1563
|
+
]) : null;
|
|
1564
|
+
const tools = scopedToolNames ? Object.fromEntries(Object.entries(allTools).filter(([name])=>scopedToolNames.has(name))) : allTools;
|
|
1565
|
+
if (intentToolChoice && scopedToolNames && !scopedToolNames.has(intentToolChoice.toolName)) {
|
|
1566
|
+
intentToolChoice = undefined;
|
|
1567
|
+
}
|
|
1166
1568
|
const encoder = new TextEncoder();
|
|
1167
1569
|
const sendEvent = (controller, event, data)=>{
|
|
1168
1570
|
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
1169
1571
|
};
|
|
1170
1572
|
const model = await getModel({
|
|
1171
1573
|
apiKey: providerConfig.apiKey,
|
|
1574
|
+
...managedProvider?.baseURL ? {
|
|
1575
|
+
baseURL: managedProvider.baseURL
|
|
1576
|
+
} : {},
|
|
1172
1577
|
model: providerConfig.modelID,
|
|
1173
1578
|
provider
|
|
1174
1579
|
});
|
|
@@ -1184,6 +1589,9 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1184
1589
|
"You are a Payload CMS assistant. Inspect schema/content with tools before proposing writes.",
|
|
1185
1590
|
"Mentions define the active CMS scope. Locale mentions define active locale; multiple locales require localizedData keyed by locale.",
|
|
1186
1591
|
"Writes are proposals only. Never claim changes were applied before user confirmation.",
|
|
1592
|
+
"Uploaded media attachments appear in context as mediaAttachment entries. Use their exact IDs for upload fields.",
|
|
1593
|
+
"If an uploaded media document has editable descriptive fields, propose an update to that media document with suitable values.",
|
|
1594
|
+
"If a collection has a required `slug` field and the user does not provide it, generate a URL‑friendly slug from the title or from the first meaningful word of the prompt.",
|
|
1187
1595
|
"For create/update/delete requests, you must use proposal tools. Do not end with plain text if the user asked for a content change.",
|
|
1188
1596
|
"If the user asks to create, update, refine, translate, remove, or delete content, produce at least one proposal tool call unless blocked by missing schema information or permissions.",
|
|
1189
1597
|
"Put concrete field values only in tool data, not visible text.",
|
|
@@ -1224,6 +1632,23 @@ export const createChatHandler = (options = {})=>async (req)=>{
|
|
|
1224
1632
|
if (part.type === "finish") {
|
|
1225
1633
|
const finishPart = part;
|
|
1226
1634
|
usage = finishPart.totalUsage || finishPart.usage || null;
|
|
1635
|
+
if (usage && options.maxTokenUsage) {
|
|
1636
|
+
try {
|
|
1637
|
+
await recordTokenUsage({
|
|
1638
|
+
model: providerConfig.modelID,
|
|
1639
|
+
provider: managedProvider?.id || provider,
|
|
1640
|
+
req,
|
|
1641
|
+
usage,
|
|
1642
|
+
userID: user.id
|
|
1643
|
+
});
|
|
1644
|
+
} catch (err) {
|
|
1645
|
+
req.payload.logger.error({
|
|
1646
|
+
err,
|
|
1647
|
+
msg: "AI token usage could not be recorded",
|
|
1648
|
+
userID: String(user.id)
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1227
1652
|
const reason = getChatCompletionReason({
|
|
1228
1653
|
proposalCount: proposals.length,
|
|
1229
1654
|
toolFailures,
|