@mvriu5/payload-ai 0.5.6 → 0.5.9
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 +52 -5
- package/dist/ai/providerOptions.d.ts +1 -14
- package/dist/ai/providerOptions.js +0 -23
- package/dist/ai/providerRuntime.d.ts +3 -2
- package/dist/ai/providerRuntime.js +6 -15
- package/dist/components/AIActionProposalList.d.ts +2 -1
- package/dist/components/AIActionProposalList.js +74 -6
- package/dist/components/AIActionProposalList.module.css +49 -5
- package/dist/components/AIApiKeyField.js +0 -8
- package/dist/components/AIInput.d.ts +1 -1
- package/dist/components/AIInput.js +156 -23
- package/dist/components/AIInput.module.css +185 -147
- package/dist/components/CollectionMentionPopover.d.ts +1 -1
- package/dist/components/DiffDialog.d.ts +12 -0
- package/dist/components/DiffDialog.js +310 -0
- package/dist/components/DiffDialog.module.css +249 -0
- package/dist/components/Icons.d.ts +9 -0
- package/dist/components/Icons.js +266 -0
- package/dist/components/RecentChangesList.d.ts +18 -0
- package/dist/components/RecentChangesList.js +66 -0
- package/dist/components/RecentChangesList.module.css +135 -0
- package/dist/components/hooks/useAISettings.d.ts +1 -1
- package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +3 -1
- package/dist/endpoints/aiApplyActionEndpointHandler.js +209 -8
- package/dist/endpoints/aiChatEndpointHandler.d.ts +5 -1
- package/dist/endpoints/aiChatEndpointHandler.js +106 -21
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +2 -1
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +7 -7
- package/dist/endpoints/aiProposalDiffEndpointHandler.d.ts +7 -0
- package/dist/endpoints/aiProposalDiffEndpointHandler.js +138 -0
- package/dist/endpoints/aiRecentChangesEndpointHandler.d.ts +6 -0
- package/dist/endpoints/aiRecentChangesEndpointHandler.js +35 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +140 -18
- package/dist/payload/collectionPermissions.d.ts +22 -0
- package/dist/payload/collectionPermissions.js +68 -0
- package/dist/payload/schemaContext.d.ts +46 -2
- package/dist/payload/schemaContext.js +105 -14
- package/package.json +113 -121
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { verifyAIActionProposal } from "../ai/proposals.js";
|
|
2
|
-
import { containsSensitiveData } from "../ai/sensitiveData.js";
|
|
2
|
+
import { containsSensitiveData, redactSensitiveData } from "../ai/sensitiveData.js";
|
|
3
3
|
import { getCollectionFields, isAuthCollection, normalizeAuthData, normalizeDataForFields } from "../payload/normalizeData.js";
|
|
4
|
+
import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
|
|
4
5
|
const getProposalMeta = (proposal)=>{
|
|
5
6
|
if (!proposal) return {};
|
|
6
7
|
return {
|
|
@@ -15,12 +16,13 @@ const getAppliedDocReference = (doc)=>{
|
|
|
15
16
|
id: doc.id
|
|
16
17
|
};
|
|
17
18
|
};
|
|
18
|
-
const
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
const isAllowedCollection = (req, collection, collections, action = "read")=>{
|
|
20
|
+
return isCollectionActionAllowed({
|
|
21
|
+
action,
|
|
22
|
+
permissions: collections,
|
|
23
|
+
req,
|
|
24
|
+
slug: collection
|
|
25
|
+
});
|
|
24
26
|
};
|
|
25
27
|
const isKnownGlobal = (req, slug)=>{
|
|
26
28
|
return req.payload.config.globals?.some((item)=>item.slug === slug) || false;
|
|
@@ -28,6 +30,151 @@ const isKnownGlobal = (req, slug)=>{
|
|
|
28
30
|
const isRecord = (value)=>{
|
|
29
31
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
30
32
|
};
|
|
33
|
+
const mergeData = (current, next)=>{
|
|
34
|
+
return Object.fromEntries(Object.entries({
|
|
35
|
+
...current,
|
|
36
|
+
...next
|
|
37
|
+
}).map(([key, value])=>{
|
|
38
|
+
const currentValue = current[key];
|
|
39
|
+
if (isRecord(currentValue) && isRecord(value)) {
|
|
40
|
+
return [
|
|
41
|
+
key,
|
|
42
|
+
mergeData(currentValue, value)
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
return [
|
|
46
|
+
key,
|
|
47
|
+
value
|
|
48
|
+
];
|
|
49
|
+
}));
|
|
50
|
+
};
|
|
51
|
+
const getJSONLineKey = (line)=>{
|
|
52
|
+
const match = /^(\s*)"([^"]+)":/.exec(line);
|
|
53
|
+
return match ? `${match[1]}${match[2]}` : null;
|
|
54
|
+
};
|
|
55
|
+
const countDiffChanges = ({ after, before })=>{
|
|
56
|
+
const beforeLines = JSON.stringify(before, null, 2).split("\n");
|
|
57
|
+
const afterLines = JSON.stringify(after, null, 2).split("\n");
|
|
58
|
+
const dp = Array.from({
|
|
59
|
+
length: beforeLines.length + 1
|
|
60
|
+
}, ()=>Array(afterLines.length + 1).fill(0));
|
|
61
|
+
let additions = 0;
|
|
62
|
+
let removals = 0;
|
|
63
|
+
for(let i = beforeLines.length - 1; i >= 0; i -= 1){
|
|
64
|
+
for(let j = afterLines.length - 1; j >= 0; j -= 1){
|
|
65
|
+
dp[i][j] = beforeLines[i] === afterLines[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let beforeIndex = 0;
|
|
69
|
+
let afterIndex = 0;
|
|
70
|
+
while(beforeIndex < beforeLines.length && afterIndex < afterLines.length){
|
|
71
|
+
if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
|
|
72
|
+
beforeIndex += 1;
|
|
73
|
+
afterIndex += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const beforeKey = getJSONLineKey(beforeLines[beforeIndex]);
|
|
77
|
+
const afterKey = getJSONLineKey(afterLines[afterIndex]);
|
|
78
|
+
if (beforeKey && beforeKey === afterKey) {
|
|
79
|
+
removals += 1;
|
|
80
|
+
additions += 1;
|
|
81
|
+
beforeIndex += 1;
|
|
82
|
+
afterIndex += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (dp[beforeIndex + 1][afterIndex] >= dp[beforeIndex][afterIndex + 1]) {
|
|
86
|
+
removals += 1;
|
|
87
|
+
beforeIndex += 1;
|
|
88
|
+
} else {
|
|
89
|
+
additions += 1;
|
|
90
|
+
afterIndex += 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
removals += beforeLines.length - beforeIndex;
|
|
94
|
+
additions += afterLines.length - afterIndex;
|
|
95
|
+
return {
|
|
96
|
+
additions,
|
|
97
|
+
removals
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
const getTargetURL = ({ documentID, proposal, req })=>{
|
|
101
|
+
const adminRoute = req.payload.config.routes.admin || "/admin";
|
|
102
|
+
if (proposal.action === "updateGlobal") {
|
|
103
|
+
return `${adminRoute}/globals/${proposal.slug}`;
|
|
104
|
+
}
|
|
105
|
+
if (proposal.action === "delete") {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const id = proposal.action === "create" ? documentID : proposal.id;
|
|
109
|
+
if (!id) return null;
|
|
110
|
+
return `${adminRoute}/collections/${proposal.collection}/${id}`;
|
|
111
|
+
};
|
|
112
|
+
const getUserID = (req)=>{
|
|
113
|
+
const user = req.user;
|
|
114
|
+
return user?.id === undefined ? undefined : String(user.id);
|
|
115
|
+
};
|
|
116
|
+
const logAIChange = async ({ changeLogCollection, target, proposal, req })=>{
|
|
117
|
+
if (!changeLogCollection) return null;
|
|
118
|
+
const before = redactSensitiveData(target.before);
|
|
119
|
+
const after = redactSensitiveData(target.after);
|
|
120
|
+
const { additions, removals } = countDiffChanges({
|
|
121
|
+
after,
|
|
122
|
+
before
|
|
123
|
+
});
|
|
124
|
+
const proposalForLog = {
|
|
125
|
+
...proposal
|
|
126
|
+
};
|
|
127
|
+
delete proposalForLog._aiSignature;
|
|
128
|
+
try {
|
|
129
|
+
await req.payload.create({
|
|
130
|
+
collection: changeLogCollection,
|
|
131
|
+
data: {
|
|
132
|
+
action: proposal.action,
|
|
133
|
+
additions,
|
|
134
|
+
after,
|
|
135
|
+
before,
|
|
136
|
+
collection: "collection" in proposal ? proposal.collection : undefined,
|
|
137
|
+
documentID: target.documentID === undefined ? undefined : String(target.documentID),
|
|
138
|
+
proposal: redactSensitiveData(proposalForLog),
|
|
139
|
+
removals,
|
|
140
|
+
slug: "slug" in proposal ? proposal.slug : undefined,
|
|
141
|
+
targetType: proposal.action === "updateGlobal" ? "global" : "collection",
|
|
142
|
+
targetURL: getTargetURL({
|
|
143
|
+
documentID: target.documentID,
|
|
144
|
+
proposal,
|
|
145
|
+
req
|
|
146
|
+
}),
|
|
147
|
+
title: proposal.label,
|
|
148
|
+
userID: getUserID(req)
|
|
149
|
+
},
|
|
150
|
+
overrideAccess: true,
|
|
151
|
+
req
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
action: proposal.action,
|
|
155
|
+
additions,
|
|
156
|
+
after,
|
|
157
|
+
before,
|
|
158
|
+
collection: "collection" in proposal ? proposal.collection : null,
|
|
159
|
+
documentID: target.documentID === undefined ? null : String(target.documentID),
|
|
160
|
+
removals,
|
|
161
|
+
slug: "slug" in proposal ? proposal.slug : null,
|
|
162
|
+
title: proposal.label,
|
|
163
|
+
url: getTargetURL({
|
|
164
|
+
documentID: target.documentID,
|
|
165
|
+
proposal,
|
|
166
|
+
req
|
|
167
|
+
})
|
|
168
|
+
};
|
|
169
|
+
} catch (err) {
|
|
170
|
+
req.payload.logger.error({
|
|
171
|
+
err,
|
|
172
|
+
msg: "AI change log entry could not be written",
|
|
173
|
+
proposal: getProposalMeta(proposal)
|
|
174
|
+
});
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
31
178
|
const isActionProposal = (proposal)=>{
|
|
32
179
|
if (!isRecord(proposal) || typeof proposal.label !== "string") return false;
|
|
33
180
|
if (proposal.action === "create") {
|
|
@@ -82,18 +229,33 @@ export const createAIApplyActionEndpointHandler = (options = {})=>async (req)=>{
|
|
|
82
229
|
});
|
|
83
230
|
const globalConfig = req.payload.config.globals?.find((global)=>global.slug === proposal.slug);
|
|
84
231
|
normalized = normalizeDataForFields(globalConfig?.fields || [], proposal.data);
|
|
232
|
+
const before = await req.payload.findGlobal({
|
|
233
|
+
depth: 2,
|
|
234
|
+
req,
|
|
235
|
+
slug: proposal.slug
|
|
236
|
+
});
|
|
85
237
|
const doc = await req.payload.updateGlobal({
|
|
86
238
|
data: normalized.data,
|
|
87
239
|
overrideAccess: false,
|
|
88
240
|
req,
|
|
89
241
|
slug: proposal.slug
|
|
90
242
|
});
|
|
243
|
+
const change = await logAIChange({
|
|
244
|
+
changeLogCollection: options.changeLogCollection,
|
|
245
|
+
proposal,
|
|
246
|
+
req,
|
|
247
|
+
target: {
|
|
248
|
+
after: mergeData(before, normalized.data),
|
|
249
|
+
before
|
|
250
|
+
}
|
|
251
|
+
});
|
|
91
252
|
return Response.json({
|
|
253
|
+
change,
|
|
92
254
|
doc: getAppliedDocReference(doc),
|
|
93
255
|
status: "applied"
|
|
94
256
|
});
|
|
95
257
|
}
|
|
96
|
-
if (!isAllowedCollection(req, proposal.collection, options.collections)) return Response.json({
|
|
258
|
+
if (!isAllowedCollection(req, proposal.collection, options.collections, proposal.action)) return Response.json({
|
|
97
259
|
error: "Unknown collection"
|
|
98
260
|
}, {
|
|
99
261
|
status: 400
|
|
@@ -105,7 +267,18 @@ export const createAIApplyActionEndpointHandler = (options = {})=>async (req)=>{
|
|
|
105
267
|
overrideAccess: false,
|
|
106
268
|
req
|
|
107
269
|
});
|
|
270
|
+
const change = await logAIChange({
|
|
271
|
+
changeLogCollection: options.changeLogCollection,
|
|
272
|
+
proposal,
|
|
273
|
+
req,
|
|
274
|
+
target: {
|
|
275
|
+
after: {},
|
|
276
|
+
before: doc,
|
|
277
|
+
documentID: proposal.id
|
|
278
|
+
}
|
|
279
|
+
});
|
|
108
280
|
return Response.json({
|
|
281
|
+
change,
|
|
109
282
|
doc: getAppliedDocReference(doc),
|
|
110
283
|
status: "applied"
|
|
111
284
|
});
|
|
@@ -133,11 +306,28 @@ export const createAIApplyActionEndpointHandler = (options = {})=>async (req)=>{
|
|
|
133
306
|
overrideAccess: false,
|
|
134
307
|
req
|
|
135
308
|
});
|
|
309
|
+
const change = await logAIChange({
|
|
310
|
+
changeLogCollection: options.changeLogCollection,
|
|
311
|
+
proposal,
|
|
312
|
+
req,
|
|
313
|
+
target: {
|
|
314
|
+
after: doc,
|
|
315
|
+
before: {},
|
|
316
|
+
documentID: doc.id
|
|
317
|
+
}
|
|
318
|
+
});
|
|
136
319
|
return Response.json({
|
|
320
|
+
change,
|
|
137
321
|
doc: getAppliedDocReference(doc),
|
|
138
322
|
status: "applied"
|
|
139
323
|
});
|
|
140
324
|
}
|
|
325
|
+
const before = await req.payload.findByID({
|
|
326
|
+
collection: proposal.collection,
|
|
327
|
+
depth: 2,
|
|
328
|
+
id: proposal.id,
|
|
329
|
+
req
|
|
330
|
+
});
|
|
141
331
|
const doc = await req.payload.update({
|
|
142
332
|
collection: proposal.collection,
|
|
143
333
|
data: normalized.data,
|
|
@@ -145,7 +335,18 @@ export const createAIApplyActionEndpointHandler = (options = {})=>async (req)=>{
|
|
|
145
335
|
overrideAccess: false,
|
|
146
336
|
req
|
|
147
337
|
});
|
|
338
|
+
const change = await logAIChange({
|
|
339
|
+
changeLogCollection: options.changeLogCollection,
|
|
340
|
+
proposal,
|
|
341
|
+
req,
|
|
342
|
+
target: {
|
|
343
|
+
after: mergeData(before, normalized.data),
|
|
344
|
+
before,
|
|
345
|
+
documentID: proposal.id
|
|
346
|
+
}
|
|
347
|
+
});
|
|
148
348
|
return Response.json({
|
|
349
|
+
change,
|
|
149
350
|
doc: getAppliedDocReference(doc),
|
|
150
351
|
status: "applied"
|
|
151
352
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { PayloadHandler } from "payload";
|
|
2
2
|
import { type AIActionSignature } from "../ai/proposals.js";
|
|
3
|
+
import { type AIModelConfig } from "../ai/providerOptions.js";
|
|
4
|
+
import { type ResolvedAICollectionPermissionMap } from "../payload/collectionPermissions.js";
|
|
3
5
|
export type AIActionProposal = ({
|
|
4
6
|
action: "create";
|
|
5
7
|
collection: string;
|
|
@@ -25,7 +27,9 @@ export type AIActionProposal = ({
|
|
|
25
27
|
_aiSignature?: AIActionSignature;
|
|
26
28
|
};
|
|
27
29
|
type AIChatEndpointOptions = {
|
|
28
|
-
|
|
30
|
+
allowUserApiKeys?: boolean;
|
|
31
|
+
collections?: ResolvedAICollectionPermissionMap;
|
|
32
|
+
models?: AIModelConfig;
|
|
29
33
|
};
|
|
30
34
|
export declare const createAIChatEndpointHandler: (options?: AIChatEndpointOptions) => PayloadHandler;
|
|
31
35
|
export {};
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { streamText, stepCountIs } from "ai";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { signAIActionProposal } from "../ai/proposals.js";
|
|
4
4
|
import { getModel, getProviderConfig } from "../ai/providerRuntime.js";
|
|
5
5
|
import { isAIProvider } from "../ai/providerOptions.js";
|
|
6
6
|
import { containsSensitiveData } from "../ai/sensitiveData.js";
|
|
7
|
-
import { buildPromptWithMentionContext, collectBlocks,
|
|
7
|
+
import { buildPromptWithMentionContext, collectBlocks, describeCollectionLikeConfig, getAllowedCollectionSlugs, getMentionContext } from "../payload/schemaContext.js";
|
|
8
|
+
import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
|
|
9
|
+
const maxProposalLabelLength = 90;
|
|
10
|
+
const getSafeProposalLabel = (label)=>{
|
|
11
|
+
const firstLine = label.replace(/[*_`>#-]/g, "").split("\n").map((line)=>line.trim()).find(Boolean);
|
|
12
|
+
if (!firstLine) return "Review proposed CMS change";
|
|
13
|
+
if (firstLine.length <= maxProposalLabelLength) return firstLine;
|
|
14
|
+
return `${firstLine.slice(0, maxProposalLabelLength - 3).trim()}...`;
|
|
15
|
+
};
|
|
8
16
|
export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
|
|
9
17
|
if (!req.user) return Response.json({
|
|
10
18
|
error: "Unauthorized"
|
|
@@ -26,8 +34,10 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
|
|
|
26
34
|
status: 400
|
|
27
35
|
});
|
|
28
36
|
const provider = requestedProvider;
|
|
37
|
+
const userApiKey = options.allowUserApiKeys === false ? null : user.aiApiKey;
|
|
29
38
|
const providerConfig = getProviderConfig({
|
|
30
|
-
apiKey:
|
|
39
|
+
apiKey: userApiKey,
|
|
40
|
+
defaultModels: options.models?.defaults,
|
|
31
41
|
model: body?.model,
|
|
32
42
|
provider
|
|
33
43
|
});
|
|
@@ -48,7 +58,7 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
|
|
|
48
58
|
};
|
|
49
59
|
if (!providerConfig.apiKey) {
|
|
50
60
|
return Response.json({
|
|
51
|
-
error: `Add a ${provider} API key to your account settings first.`
|
|
61
|
+
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.`
|
|
52
62
|
}, {
|
|
53
63
|
status: 400
|
|
54
64
|
});
|
|
@@ -88,11 +98,23 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
|
|
|
88
98
|
const mentionContext = await getMentionContext({
|
|
89
99
|
blockContexts,
|
|
90
100
|
collectionSlugs,
|
|
101
|
+
collections: options.collections,
|
|
91
102
|
globalSlugs,
|
|
92
103
|
mentions: body?.mentions,
|
|
93
104
|
req
|
|
94
105
|
});
|
|
95
106
|
const collectionSlugSchema = z.enum(collectionSlugs);
|
|
107
|
+
const getDisallowedCollectionActionError = (collection, action)=>{
|
|
108
|
+
if (isCollectionActionAllowed({
|
|
109
|
+
action,
|
|
110
|
+
permissions: options.collections,
|
|
111
|
+
req,
|
|
112
|
+
slug: collection
|
|
113
|
+
})) return null;
|
|
114
|
+
return {
|
|
115
|
+
error: `${action} is not enabled for collection: ${collection}`
|
|
116
|
+
};
|
|
117
|
+
};
|
|
96
118
|
const tools = {
|
|
97
119
|
getDoc: {
|
|
98
120
|
description: "Read one document by collection slug and document id.",
|
|
@@ -114,10 +136,10 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
|
|
|
114
136
|
description: "List all Payload CMS collections available in this app.",
|
|
115
137
|
inputSchema: z.object({}),
|
|
116
138
|
execute: async ()=>{
|
|
117
|
-
return allowedCollections.map((collection)=>({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
139
|
+
return allowedCollections.map((collection)=>describeCollectionLikeConfig({
|
|
140
|
+
config: collection,
|
|
141
|
+
permissions: options.collections,
|
|
142
|
+
type: "collection"
|
|
121
143
|
}));
|
|
122
144
|
}
|
|
123
145
|
},
|
|
@@ -143,10 +165,9 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
|
|
|
143
165
|
description: "List all Payload CMS globals available in this app.",
|
|
144
166
|
inputSchema: z.object({}),
|
|
145
167
|
execute: async ()=>{
|
|
146
|
-
return req.payload.config.globals?.map((global)=>({
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
slug: global.slug
|
|
168
|
+
return req.payload.config.globals?.map((global)=>describeCollectionLikeConfig({
|
|
169
|
+
config: global,
|
|
170
|
+
type: "global"
|
|
150
171
|
})) || [];
|
|
151
172
|
}
|
|
152
173
|
},
|
|
@@ -158,11 +179,13 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
|
|
|
158
179
|
label: z.string().min(1)
|
|
159
180
|
}),
|
|
160
181
|
execute: async ({ collection, data, label })=>{
|
|
182
|
+
const permissionError = getDisallowedCollectionActionError(collection, "create");
|
|
183
|
+
if (permissionError) return permissionError;
|
|
161
184
|
const proposal = {
|
|
162
185
|
action: "create",
|
|
163
186
|
collection,
|
|
164
187
|
data,
|
|
165
|
-
label
|
|
188
|
+
label: getSafeProposalLabel(label)
|
|
166
189
|
};
|
|
167
190
|
return addSignedProposal(proposal);
|
|
168
191
|
}
|
|
@@ -175,11 +198,13 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
|
|
|
175
198
|
label: z.string().min(1)
|
|
176
199
|
}),
|
|
177
200
|
execute: async ({ collection, id, label })=>{
|
|
201
|
+
const permissionError = getDisallowedCollectionActionError(collection, "delete");
|
|
202
|
+
if (permissionError) return permissionError;
|
|
178
203
|
const proposal = {
|
|
179
204
|
action: "delete",
|
|
180
205
|
collection,
|
|
181
206
|
id,
|
|
182
|
-
label
|
|
207
|
+
label: getSafeProposalLabel(label)
|
|
183
208
|
};
|
|
184
209
|
return addSignedProposal(proposal);
|
|
185
210
|
}
|
|
@@ -193,12 +218,14 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
|
|
|
193
218
|
label: z.string().min(1)
|
|
194
219
|
}),
|
|
195
220
|
execute: async ({ collection, data, id, label })=>{
|
|
221
|
+
const permissionError = getDisallowedCollectionActionError(collection, "update");
|
|
222
|
+
if (permissionError) return permissionError;
|
|
196
223
|
const proposal = {
|
|
197
224
|
action: "update",
|
|
198
225
|
collection,
|
|
199
226
|
data,
|
|
200
227
|
id,
|
|
201
|
-
label
|
|
228
|
+
label: getSafeProposalLabel(label)
|
|
202
229
|
};
|
|
203
230
|
return addSignedProposal(proposal);
|
|
204
231
|
}
|
|
@@ -218,7 +245,7 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
|
|
|
218
245
|
const proposal = {
|
|
219
246
|
action: "updateGlobal",
|
|
220
247
|
data,
|
|
221
|
-
label,
|
|
248
|
+
label: getSafeProposalLabel(label),
|
|
222
249
|
slug
|
|
223
250
|
};
|
|
224
251
|
return addSignedProposal(proposal);
|
|
@@ -256,7 +283,11 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
|
|
|
256
283
|
}
|
|
257
284
|
}
|
|
258
285
|
};
|
|
259
|
-
const
|
|
286
|
+
const encoder = new TextEncoder();
|
|
287
|
+
const sendEvent = (controller, event, data)=>{
|
|
288
|
+
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
289
|
+
};
|
|
290
|
+
const result = streamText({
|
|
260
291
|
maxOutputTokens: 700,
|
|
261
292
|
model: getModel({
|
|
262
293
|
apiKey: providerConfig.apiKey,
|
|
@@ -274,13 +305,67 @@ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
|
|
|
274
305
|
"When mention context is provided, use it as the selected CMS scope and prefer it over guessing collection names.",
|
|
275
306
|
"Never claim that a write has been applied unless the user confirms an action proposal in the UI.",
|
|
276
307
|
"For create, update, and delete requests, call the proposal tools instead of directly changing data.",
|
|
277
|
-
"Keep the visible response under
|
|
308
|
+
"Keep the visible response under 40 words and describe only what kind of change was proposed.",
|
|
309
|
+
"Write visible responses as plain text only. Do not use Markdown formatting, bold markers (**), headings, bullets, tables, or code fences.",
|
|
310
|
+
"Do not include proposed field values, full replacement text, quoted content, markdown sections, code blocks, or headings like 'New content'/'Neuer Inhalt' in the visible response.",
|
|
311
|
+
"Put all concrete field values and replacement content only in proposal tool data. Proposal labels must be short action summaries and must not contain proposed content."
|
|
278
312
|
].join("\n"),
|
|
279
313
|
tools
|
|
280
314
|
});
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
315
|
+
const stream = new ReadableStream({
|
|
316
|
+
start: async (controller)=>{
|
|
317
|
+
let didSendTerminalEvent = false;
|
|
318
|
+
try {
|
|
319
|
+
for await (const part of result.fullStream){
|
|
320
|
+
if (part.type === "text-delta") {
|
|
321
|
+
sendEvent(controller, "text", {
|
|
322
|
+
delta: part.text
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (part.type === "error") {
|
|
327
|
+
didSendTerminalEvent = true;
|
|
328
|
+
sendEvent(controller, "error", {
|
|
329
|
+
error: "AI request failed."
|
|
330
|
+
});
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
if (part.type === "finish") {
|
|
334
|
+
sendEvent(controller, "proposals", {
|
|
335
|
+
proposals
|
|
336
|
+
});
|
|
337
|
+
sendEvent(controller, "done", {});
|
|
338
|
+
didSendTerminalEvent = true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (!didSendTerminalEvent) {
|
|
342
|
+
sendEvent(controller, "proposals", {
|
|
343
|
+
proposals
|
|
344
|
+
});
|
|
345
|
+
sendEvent(controller, "done", {});
|
|
346
|
+
}
|
|
347
|
+
} catch (err) {
|
|
348
|
+
req.payload.logger.error({
|
|
349
|
+
debug,
|
|
350
|
+
err,
|
|
351
|
+
msg: "AI chat stream failed"
|
|
352
|
+
});
|
|
353
|
+
if (!didSendTerminalEvent) {
|
|
354
|
+
sendEvent(controller, "error", {
|
|
355
|
+
error: "AI request failed."
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
} finally{
|
|
359
|
+
controller.close();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
return new Response(stream, {
|
|
364
|
+
headers: {
|
|
365
|
+
"Cache-Control": "no-cache, no-transform",
|
|
366
|
+
Connection: "keep-alive",
|
|
367
|
+
"Content-Type": "text/event-stream; charset=utf-8"
|
|
368
|
+
}
|
|
284
369
|
});
|
|
285
370
|
} catch (err) {
|
|
286
371
|
req.payload.logger.error({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PayloadHandler } from "payload";
|
|
2
|
+
import { type ResolvedAICollectionPermissionMap } from "../payload/collectionPermissions.js";
|
|
2
3
|
type AIMentionSuggestionsEndpointOptions = {
|
|
3
|
-
collections?:
|
|
4
|
+
collections?: ResolvedAICollectionPermissionMap;
|
|
4
5
|
};
|
|
5
6
|
export declare const createAIMentionSuggestionsEndpointHandler: (options?: AIMentionSuggestionsEndpointOptions) => PayloadHandler;
|
|
6
7
|
export {};
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const isAllowedCollectionSlug = (slug, collections)=>{
|
|
3
|
-
if (isInternalCollection(slug)) return false;
|
|
4
|
-
if (!collections) return true;
|
|
5
|
-
return collections.includes(slug);
|
|
6
|
-
};
|
|
1
|
+
import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
|
|
7
2
|
const getDocLabel = (doc, useAsTitle)=>{
|
|
8
3
|
const titleField = useAsTitle && typeof doc[useAsTitle] === "string" ? doc[useAsTitle] : null;
|
|
9
4
|
return (titleField || doc.title || doc.name || doc.email || doc.id || "Untitled").toString();
|
|
@@ -22,7 +17,12 @@ export const createAIMentionSuggestionsEndpointHandler = (options = {})=>async (
|
|
|
22
17
|
});
|
|
23
18
|
const suggestions = [];
|
|
24
19
|
const collections = req.payload.config.collections.filter((collection)=>{
|
|
25
|
-
if (!
|
|
20
|
+
if (!isCollectionActionAllowed({
|
|
21
|
+
action: "read",
|
|
22
|
+
permissions: options.collections,
|
|
23
|
+
req,
|
|
24
|
+
slug: collection.slug
|
|
25
|
+
})) return false;
|
|
26
26
|
if (collectionSlug) return collection.slug === collectionSlug;
|
|
27
27
|
return true;
|
|
28
28
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PayloadHandler } from "payload";
|
|
2
|
+
import { type ResolvedAICollectionPermissionMap } from "../payload/collectionPermissions.js";
|
|
3
|
+
type AIProposalDiffEndpointOptions = {
|
|
4
|
+
collections?: ResolvedAICollectionPermissionMap;
|
|
5
|
+
};
|
|
6
|
+
export declare const createAIProposalDiffEndpointHandler: (options?: AIProposalDiffEndpointOptions) => PayloadHandler;
|
|
7
|
+
export {};
|