@adminforth/bulk-ai-flow 1.27.4 → 1.27.6
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/build.log +2 -2
- package/custom/VisionAction.vue +4 -6
- package/dist/custom/VisionAction.vue +4 -6
- package/dist/index.d.ts +29 -0
- package/dist/index.js +218 -105
- package/index.ts +225 -95
- package/package.json +1 -1
package/build.log
CHANGED
|
@@ -14,5 +14,5 @@ custom/package.json
|
|
|
14
14
|
custom/pnpm-lock.yaml
|
|
15
15
|
custom/tsconfig.json
|
|
16
16
|
|
|
17
|
-
sent 130,
|
|
18
|
-
total size is 129,
|
|
17
|
+
sent 130,414 bytes received 191 bytes 261,210.00 bytes/sec
|
|
18
|
+
total size is 129,691 speedup is 0.99
|
package/custom/VisionAction.vue
CHANGED
|
@@ -884,7 +884,7 @@ async function processOneRecord(recordId: string, sessionIds: string[]) {
|
|
|
884
884
|
record.label = oldDataResult._label || record.label;
|
|
885
885
|
initializeRecordData(record, oldDataResult);
|
|
886
886
|
if (props.meta.isAttachFiles) {
|
|
887
|
-
await fetchImages(record,
|
|
887
|
+
await fetchImages(record, recordId);
|
|
888
888
|
if (!checkIfDialogOpen()) {
|
|
889
889
|
return;
|
|
890
890
|
}
|
|
@@ -1162,16 +1162,14 @@ function normalizeEnumValue(key: string, value: any) {
|
|
|
1162
1162
|
return match ? value : null;
|
|
1163
1163
|
}
|
|
1164
1164
|
|
|
1165
|
-
async function fetchImages(record: RecordState,
|
|
1165
|
+
async function fetchImages(record: RecordState, recordId: string) {
|
|
1166
1166
|
try {
|
|
1167
1167
|
const res = await callAdminForthApi({
|
|
1168
1168
|
path: `/plugin/${props.meta.pluginInstanceId}/get_images`,
|
|
1169
1169
|
method: 'POST',
|
|
1170
|
-
body: {
|
|
1171
|
-
record: [oldRecord],
|
|
1172
|
-
},
|
|
1170
|
+
body: { recordId },
|
|
1173
1171
|
});
|
|
1174
|
-
record.images = res.images
|
|
1172
|
+
record.images = res.images || [];
|
|
1175
1173
|
touchRecords();
|
|
1176
1174
|
} catch (error) {
|
|
1177
1175
|
console.error('Failed to get images:', error);
|
|
@@ -884,7 +884,7 @@ async function processOneRecord(recordId: string, sessionIds: string[]) {
|
|
|
884
884
|
record.label = oldDataResult._label || record.label;
|
|
885
885
|
initializeRecordData(record, oldDataResult);
|
|
886
886
|
if (props.meta.isAttachFiles) {
|
|
887
|
-
await fetchImages(record,
|
|
887
|
+
await fetchImages(record, recordId);
|
|
888
888
|
if (!checkIfDialogOpen()) {
|
|
889
889
|
return;
|
|
890
890
|
}
|
|
@@ -1162,16 +1162,14 @@ function normalizeEnumValue(key: string, value: any) {
|
|
|
1162
1162
|
return match ? value : null;
|
|
1163
1163
|
}
|
|
1164
1164
|
|
|
1165
|
-
async function fetchImages(record: RecordState,
|
|
1165
|
+
async function fetchImages(record: RecordState, recordId: string) {
|
|
1166
1166
|
try {
|
|
1167
1167
|
const res = await callAdminForthApi({
|
|
1168
1168
|
path: `/plugin/${props.meta.pluginInstanceId}/get_images`,
|
|
1169
1169
|
method: 'POST',
|
|
1170
|
-
body: {
|
|
1171
|
-
record: [oldRecord],
|
|
1172
|
-
},
|
|
1170
|
+
body: { recordId },
|
|
1173
1171
|
});
|
|
1174
|
-
record.images = res.images
|
|
1172
|
+
record.images = res.images || [];
|
|
1175
1173
|
touchRecords();
|
|
1176
1174
|
} catch (error) {
|
|
1177
1175
|
console.error('Failed to get images:', error);
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,15 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
10
10
|
rateLimiters: Record<string, RateLimiter>;
|
|
11
11
|
sessionIds: Set<string>;
|
|
12
12
|
constructor(options: PluginOptions);
|
|
13
|
+
/**
|
|
14
|
+
* Compiles Handlebars templates from `source`.
|
|
15
|
+
*
|
|
16
|
+
* `record` is the raw row and is always what `provideAdditionalContextForRecord` sees, but the
|
|
17
|
+
* templates themselves are rendered against `record` only when they come from `PluginOptions`:
|
|
18
|
+
* those are written by the app author and may reference any column. Templates which arrived in
|
|
19
|
+
* the request body are rendered against the record as the caller is allowed to read it, so a
|
|
20
|
+
* hand-written `{{column}}` cannot pull a backendOnly column out of the row.
|
|
21
|
+
*/
|
|
13
22
|
private compileTemplates;
|
|
14
23
|
private compileOutputFieldsTemplates;
|
|
15
24
|
private compileOutputFieldsTemplatesNoImage;
|
|
@@ -34,5 +43,25 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
34
43
|
oldRecord: any;
|
|
35
44
|
fields: Record<string, any>;
|
|
36
45
|
}): Promise<string | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Reads one record the way the core show route does: the resource-level `show` permission first,
|
|
48
|
+
* then the resource's own `show.beforeDatasourceRequest` hooks so row-level scoping narrows the
|
|
49
|
+
* filters before the query runs.
|
|
50
|
+
* The returned record is the raw row, so callers which send it to the browser must pass it
|
|
51
|
+
* through `scopeRecordForFrontend` first.
|
|
52
|
+
*/
|
|
53
|
+
private getRecordForShow;
|
|
54
|
+
/**
|
|
55
|
+
* The action dialog only ever renders the fields this plugin generates, so only those leave the
|
|
56
|
+
* backend, next to the primary key and the record label. backendOnly columns are dropped the same
|
|
57
|
+
* way the core show route drops them.
|
|
58
|
+
*/
|
|
59
|
+
private scopeRecordForFrontend;
|
|
60
|
+
/**
|
|
61
|
+
* The row as the caller is allowed to read it: columns which are not declared on the resource and
|
|
62
|
+
* columns whose `backendOnly` resolves true for this request are dropped, exactly the way the
|
|
63
|
+
* core show route drops them before a row reaches the browser.
|
|
64
|
+
*/
|
|
65
|
+
private stripBackendOnlyColumns;
|
|
37
66
|
setupEndpoints(server: IHttpServer): void;
|
|
38
67
|
}
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ const getRecordsBodySchema = z.object({
|
|
|
17
17
|
record: z.array(z.any()).nullish(),
|
|
18
18
|
}).strict();
|
|
19
19
|
const getImagesBodySchema = z.object({
|
|
20
|
-
|
|
20
|
+
recordId: z.union([z.string(), z.number()]),
|
|
21
21
|
}).strict();
|
|
22
22
|
const getOldDataBodySchema = z.object({
|
|
23
23
|
recordId: z.union([z.string(), z.number()]).nullish(),
|
|
@@ -28,7 +28,7 @@ const updateFieldsBodySchema = z.object({
|
|
|
28
28
|
saveImages: z.boolean().nullish(),
|
|
29
29
|
}).strict();
|
|
30
30
|
const getImageGenerationPromptsBodySchema = z.object({
|
|
31
|
-
recordId: z.union([z.string(), z.number()])
|
|
31
|
+
recordId: z.union([z.string(), z.number()]),
|
|
32
32
|
customPrompt: z.string().nullish(),
|
|
33
33
|
}).strict();
|
|
34
34
|
const createJobBodySchema = z.object({
|
|
@@ -106,40 +106,49 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
106
106
|
// for rate limiting
|
|
107
107
|
this.sessionIds = new Set();
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Compiles Handlebars templates from `source`.
|
|
111
|
+
*
|
|
112
|
+
* `record` is the raw row and is always what `provideAdditionalContextForRecord` sees, but the
|
|
113
|
+
* templates themselves are rendered against `record` only when they come from `PluginOptions`:
|
|
114
|
+
* those are written by the app author and may reference any column. Templates which arrived in
|
|
115
|
+
* the request body are rendered against the record as the caller is allowed to read it, so a
|
|
116
|
+
* hand-written `{{column}}` cannot pull a backendOnly column out of the row.
|
|
117
|
+
*/
|
|
118
|
+
compileTemplates(source_1, _a, valueSelector_1) {
|
|
119
|
+
return __awaiter(this, arguments, void 0, function* (source, { record, ctx, clientSupplied }, valueSelector) {
|
|
120
|
+
let templateContext = clientSupplied ? yield this.stripBackendOnlyColumns(record, ctx) : record;
|
|
112
121
|
if (this.options.provideAdditionalContextForRecord) {
|
|
113
|
-
const additionalFields = yield this.options.provideAdditionalContextForRecord({ record, adminUser:
|
|
114
|
-
|
|
122
|
+
const additionalFields = yield this.options.provideAdditionalContextForRecord({ record, adminUser: ctx.adminUser, resource: this.resourceConfig });
|
|
123
|
+
templateContext = Object.assign(Object.assign({}, templateContext), additionalFields);
|
|
115
124
|
}
|
|
116
125
|
const compiled = {};
|
|
117
126
|
for (const [key, value] of Object.entries(source)) {
|
|
118
127
|
const templateStr = valueSelector(value);
|
|
119
128
|
try {
|
|
120
129
|
const tpl = Handlebars.compile(templateStr);
|
|
121
|
-
compiled[key] = tpl(
|
|
130
|
+
compiled[key] = tpl(templateContext);
|
|
122
131
|
}
|
|
123
|
-
catch (
|
|
132
|
+
catch (_b) {
|
|
124
133
|
compiled[key] = templateStr;
|
|
125
134
|
}
|
|
126
135
|
}
|
|
127
136
|
return compiled;
|
|
128
137
|
});
|
|
129
138
|
}
|
|
130
|
-
compileOutputFieldsTemplates(
|
|
131
|
-
return __awaiter(this,
|
|
132
|
-
return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillFieldsFromImages, record, v => String(v));
|
|
139
|
+
compileOutputFieldsTemplates(record_1, ctx_1, customPrompt_1) {
|
|
140
|
+
return __awaiter(this, arguments, void 0, function* (record, ctx, customPrompt, clientSupplied = !!customPrompt) {
|
|
141
|
+
return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillFieldsFromImages, { record, ctx, clientSupplied }, v => String(v));
|
|
133
142
|
});
|
|
134
143
|
}
|
|
135
|
-
compileOutputFieldsTemplatesNoImage(
|
|
136
|
-
return __awaiter(this,
|
|
137
|
-
return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillPlainFields, record, v => String(v));
|
|
144
|
+
compileOutputFieldsTemplatesNoImage(record_1, ctx_1, customPrompt_1) {
|
|
145
|
+
return __awaiter(this, arguments, void 0, function* (record, ctx, customPrompt, clientSupplied = !!customPrompt) {
|
|
146
|
+
return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillPlainFields, { record, ctx, clientSupplied }, v => String(v));
|
|
138
147
|
});
|
|
139
148
|
}
|
|
140
|
-
compileGenerationFieldTemplates(
|
|
141
|
-
return __awaiter(this,
|
|
142
|
-
return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.generateImages, record, v => String(customPrompt ? v : v.prompt));
|
|
149
|
+
compileGenerationFieldTemplates(record_1, ctx_1, customPrompt_1) {
|
|
150
|
+
return __awaiter(this, arguments, void 0, function* (record, ctx, customPrompt, clientSupplied = !!customPrompt) {
|
|
151
|
+
return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.generateImages, { record, ctx, clientSupplied }, v => String(customPrompt ? v : v.prompt));
|
|
143
152
|
});
|
|
144
153
|
}
|
|
145
154
|
removeFromPromptFilledFields(compiledOutputFields, record) {
|
|
@@ -188,19 +197,15 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
188
197
|
Do NOT wrap in \`\`\` or \`\`\`json. Do not add any extra text. Do not return prompt in response`;
|
|
189
198
|
return prompt;
|
|
190
199
|
}
|
|
191
|
-
analyze_image(jobId_1,
|
|
192
|
-
return __awaiter(this, arguments, void 0, function* (jobId,
|
|
200
|
+
analyze_image(jobId_1, record_1, ctx_1, headers_1, customPrompt_1) {
|
|
201
|
+
return __awaiter(this, arguments, void 0, function* (jobId, record, ctx, headers, customPrompt, filterFilledFields = true) {
|
|
193
202
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u;
|
|
194
203
|
// TODO: need to do correct check of rate limit
|
|
195
204
|
// if (this.options.rateLimits && this.options.rateLimits.fillFieldsFromImages && await this.checkRateLimit("fillFieldsFromImages" ,this.options.rateLimits.fillFieldsFromImages, headers)) {
|
|
196
205
|
// jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
|
|
197
206
|
// return { error: "Rate limit exceeded" };
|
|
198
207
|
// }
|
|
199
|
-
const selectedId = recordId;
|
|
200
208
|
let isError = false;
|
|
201
|
-
// Fetch the record using the provided ID
|
|
202
|
-
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
203
|
-
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, selectedId)]);
|
|
204
209
|
//recieve image URLs to analyze
|
|
205
210
|
const attachmentFiles = yield this.options.attachFiles({ record: record });
|
|
206
211
|
if (STUB_MODE) {
|
|
@@ -228,7 +233,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
228
233
|
return { ok: false, error: 'One of the image URLs is not valid' };
|
|
229
234
|
}
|
|
230
235
|
//create prompt for OpenAI
|
|
231
|
-
const compiledOutputFields = yield this.compileOutputFieldsTemplates(record, customPrompt);
|
|
236
|
+
const compiledOutputFields = yield this.compileOutputFieldsTemplates(record, ctx, customPrompt);
|
|
232
237
|
const filteredCompiledOutputFields = filterFilledFields ? this.removeFromPromptFilledFields(compiledOutputFields, record) : compiledOutputFields;
|
|
233
238
|
if (Object.keys(filteredCompiledOutputFields).length === 0) {
|
|
234
239
|
jobs.set(jobId, { status: 'completed', result: {} });
|
|
@@ -276,14 +281,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
276
281
|
}
|
|
277
282
|
});
|
|
278
283
|
}
|
|
279
|
-
analyzeNoImages(jobId_1,
|
|
280
|
-
return __awaiter(this, arguments, void 0, function* (jobId,
|
|
284
|
+
analyzeNoImages(jobId_1, record_1, ctx_1, headers_1, customPrompt_1) {
|
|
285
|
+
return __awaiter(this, arguments, void 0, function* (jobId, record, ctx, headers, customPrompt, filterFilledFields = true) {
|
|
281
286
|
// TODO: need to do correct check of rate limit
|
|
282
287
|
// if (this.options.rateLimits && this.options.rateLimits.fillPlainFields && await this.checkRateLimit("fillPlainFields" ,this.options.rateLimits.fillPlainFields, headers)) {
|
|
283
288
|
// jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
|
|
284
289
|
// return { error: "Rate limit exceeded" };
|
|
285
290
|
// }
|
|
286
|
-
const selectedId = recordId;
|
|
287
291
|
let isError = false;
|
|
288
292
|
if (STUB_MODE) {
|
|
289
293
|
const fakeError = Math.random() < 0.005; // 0.05% chance of error
|
|
@@ -298,9 +302,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
298
302
|
}
|
|
299
303
|
}
|
|
300
304
|
else {
|
|
301
|
-
const
|
|
302
|
-
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, selectedId)]);
|
|
303
|
-
const compiledOutputFields = yield this.compileOutputFieldsTemplatesNoImage(record, customPrompt);
|
|
305
|
+
const compiledOutputFields = yield this.compileOutputFieldsTemplatesNoImage(record, ctx, customPrompt);
|
|
304
306
|
const filteredCompiledOutputFields = filterFilledFields ? this.removeFromPromptFilledFields(compiledOutputFields, record) : compiledOutputFields;
|
|
305
307
|
if (Object.keys(filteredCompiledOutputFields).length === 0) {
|
|
306
308
|
jobs.set(jobId, { status: 'completed', result: {} });
|
|
@@ -342,18 +344,16 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
342
344
|
}
|
|
343
345
|
});
|
|
344
346
|
}
|
|
345
|
-
initialImageGenerate(jobId_1,
|
|
346
|
-
return __awaiter(this, arguments, void 0, function* (jobId,
|
|
347
|
-
var _a
|
|
347
|
+
initialImageGenerate(jobId_1, record_1, ctx_1, headers_1, customPrompt_1) {
|
|
348
|
+
return __awaiter(this, arguments, void 0, function* (jobId, record, ctx, headers, customPrompt, filterFilledFields = true) {
|
|
349
|
+
var _a;
|
|
348
350
|
// TODO: need to do correct check of rate limit
|
|
349
351
|
// if (this.options.rateLimits && this.options.rateLimits.generateImages && await this.checkRateLimit("generateImages" ,this.options.rateLimits.generateImages, headers)) {
|
|
350
352
|
// jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
|
|
351
353
|
// return { error: "Rate limit exceeded" };
|
|
352
354
|
// }
|
|
353
|
-
const selectedId = recordId;
|
|
354
355
|
let isError = false;
|
|
355
356
|
const start = +new Date();
|
|
356
|
-
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ((_a = this.resourceConfig.columns.find(c => c.primaryKey)) === null || _a === void 0 ? void 0 : _a.name, selectedId)]);
|
|
357
357
|
let attachmentFiles;
|
|
358
358
|
if (!this.options.attachFiles) {
|
|
359
359
|
attachmentFiles = [];
|
|
@@ -370,7 +370,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
370
370
|
return { ok: false, error: 'One of the image URLs is not valid' };
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
|
-
const fieldTasks = Object.keys(((
|
|
373
|
+
const fieldTasks = Object.keys(((_a = this.options) === null || _a === void 0 ? void 0 : _a.generateImages) || {}).map((key) => __awaiter(this, void 0, void 0, function* () {
|
|
374
374
|
if (record[key] && filterFilledFields) {
|
|
375
375
|
const plugin = this.adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.resourceConfig.resourceId &&
|
|
376
376
|
p.pluginOptions.pathColumnName === key);
|
|
@@ -384,7 +384,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
384
384
|
}
|
|
385
385
|
};
|
|
386
386
|
}
|
|
387
|
-
const prompt = (yield this.compileGenerationFieldTemplates(record, customPrompt))[key];
|
|
387
|
+
const prompt = (yield this.compileGenerationFieldTemplates(record, ctx, customPrompt))[key];
|
|
388
388
|
let images;
|
|
389
389
|
if (this.options.attachFiles && attachmentFiles.length === 0) {
|
|
390
390
|
isError = true;
|
|
@@ -456,10 +456,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
456
456
|
}
|
|
457
457
|
});
|
|
458
458
|
}
|
|
459
|
-
regenerateImage(jobId,
|
|
459
|
+
regenerateImage(jobId, record, fieldName, prompt, headers) {
|
|
460
460
|
return __awaiter(this, void 0, void 0, function* () {
|
|
461
|
-
var _a;
|
|
462
|
-
const Id = recordId;
|
|
463
461
|
let isError = false;
|
|
464
462
|
// TODO: need to do correct check of rate limit
|
|
465
463
|
// if (await this.checkRateLimit(fieldName, this.options.generateImages[fieldName].rateLimit, headers)) {
|
|
@@ -467,7 +465,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
467
465
|
// return { error: "Rate limit exceeded" };
|
|
468
466
|
// }
|
|
469
467
|
const start = +new Date();
|
|
470
|
-
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ((_a = this.resourceConfig.columns.find(c => c.primaryKey)) === null || _a === void 0 ? void 0 : _a.name, Id)]);
|
|
471
468
|
let attachmentFiles;
|
|
472
469
|
if (!this.options.attachFiles) {
|
|
473
470
|
attachmentFiles = [];
|
|
@@ -525,13 +522,16 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
525
522
|
}
|
|
526
523
|
});
|
|
527
524
|
}
|
|
528
|
-
regenerateCell(jobId, fieldToRegenerate,
|
|
525
|
+
regenerateCell(jobId, fieldToRegenerate, record, ctx, actionType, prompt) {
|
|
529
526
|
return __awaiter(this, void 0, void 0, function* () {
|
|
530
527
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
531
|
-
if (!fieldToRegenerate || !
|
|
528
|
+
if (!fieldToRegenerate || !actionType) {
|
|
532
529
|
jobs.set(jobId, { status: 'failed', error: 'Missing parameters' });
|
|
533
530
|
//return { ok: false, error: "Missing parameters" };
|
|
534
531
|
}
|
|
532
|
+
// a prompt which arrived in the request body is client-authored even though it is wrapped into
|
|
533
|
+
// `promptToPass` below, while the PluginOptions fallback is written by the app author
|
|
534
|
+
const clientSupplied = !!prompt;
|
|
535
535
|
if (!prompt) {
|
|
536
536
|
if (actionType === 'analyze') {
|
|
537
537
|
prompt = this.options.fillFieldsFromImages ? this.options.fillFieldsFromImages[fieldToRegenerate] : null;
|
|
@@ -540,8 +540,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
540
540
|
prompt = this.options.fillPlainFields ? this.options.fillPlainFields[fieldToRegenerate] : null;
|
|
541
541
|
}
|
|
542
542
|
}
|
|
543
|
-
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
544
|
-
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, recordId)]);
|
|
545
543
|
let promptToPass = JSON.stringify({ [fieldToRegenerate]: prompt });
|
|
546
544
|
if (STUB_MODE) {
|
|
547
545
|
yield new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
|
|
@@ -550,7 +548,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
550
548
|
}
|
|
551
549
|
else {
|
|
552
550
|
if (actionType === 'analyze') {
|
|
553
|
-
const compiledPropmt = yield this.compileOutputFieldsTemplates(record, promptToPass);
|
|
551
|
+
const compiledPropmt = yield this.compileOutputFieldsTemplates(record, ctx, promptToPass, clientSupplied);
|
|
554
552
|
const finalPrompt = this.getPromptForImageAnalysis(compiledPropmt);
|
|
555
553
|
const attachmentFiles = yield this.options.attachFiles({ record: record });
|
|
556
554
|
if (attachmentFiles.length === 0) {
|
|
@@ -587,7 +585,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
587
585
|
jobs.set(jobId, { status: 'completed', result: resData });
|
|
588
586
|
}
|
|
589
587
|
else if (actionType === 'analyze_no_images') {
|
|
590
|
-
const compiledPropmt = yield this.compileOutputFieldsTemplatesNoImage(record, promptToPass);
|
|
588
|
+
const compiledPropmt = yield this.compileOutputFieldsTemplatesNoImage(record, ctx, promptToPass, clientSupplied);
|
|
591
589
|
const finalPrompt = this.getPromptForPlainFields(compiledPropmt);
|
|
592
590
|
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
|
|
593
591
|
let resp;
|
|
@@ -907,44 +905,143 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
907
905
|
return null;
|
|
908
906
|
});
|
|
909
907
|
}
|
|
908
|
+
/**
|
|
909
|
+
* Reads one record the way the core show route does: the resource-level `show` permission first,
|
|
910
|
+
* then the resource's own `show.beforeDatasourceRequest` hooks so row-level scoping narrows the
|
|
911
|
+
* filters before the query runs.
|
|
912
|
+
* The returned record is the raw row, so callers which send it to the browser must pass it
|
|
913
|
+
* through `scopeRecordForFrontend` first.
|
|
914
|
+
*/
|
|
915
|
+
getRecordForShow(_a) {
|
|
916
|
+
return __awaiter(this, arguments, void 0, function* ({ adminUser, recordId, extra }) {
|
|
917
|
+
var _b, _c;
|
|
918
|
+
const meta = { requestBody: extra.body, pk: recordId };
|
|
919
|
+
const ctx = {
|
|
920
|
+
adminUser,
|
|
921
|
+
resource: this.resourceConfig,
|
|
922
|
+
meta,
|
|
923
|
+
source: ActionCheckSource.ShowRequest,
|
|
924
|
+
adminforth: this.adminforth,
|
|
925
|
+
};
|
|
926
|
+
const { allowedActions } = yield interpretResource(adminUser, this.resourceConfig, meta, ActionCheckSource.ShowRequest, this.adminforth);
|
|
927
|
+
const showAllowed = allowedActions[AllowedActionsEnum.show];
|
|
928
|
+
if (showAllowed !== true) {
|
|
929
|
+
return { error: typeof showAllowed === 'string' ? showAllowed : 'You are not allowed to view records in this resource' };
|
|
930
|
+
}
|
|
931
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
932
|
+
const hookQuery = {
|
|
933
|
+
resourceId: this.resourceConfig.resourceId,
|
|
934
|
+
source: 'show',
|
|
935
|
+
filters: [Filters.EQ(primaryKeyColumn.name, recordId)],
|
|
936
|
+
limit: 1,
|
|
937
|
+
offset: 0,
|
|
938
|
+
sort: [],
|
|
939
|
+
};
|
|
940
|
+
for (const hook of ((_c = (_b = this.resourceConfig.hooks) === null || _b === void 0 ? void 0 : _b.show) === null || _c === void 0 ? void 0 : _c.beforeDatasourceRequest) || []) {
|
|
941
|
+
const filterTools = filtersTools.get(hookQuery);
|
|
942
|
+
hookQuery.filtersTools = filterTools;
|
|
943
|
+
const resp = yield hook({
|
|
944
|
+
resource: this.resourceConfig,
|
|
945
|
+
query: hookQuery,
|
|
946
|
+
adminUser,
|
|
947
|
+
//@ts-ignore
|
|
948
|
+
filtersTools: filterTools,
|
|
949
|
+
extra,
|
|
950
|
+
adminforth: this.adminforth,
|
|
951
|
+
});
|
|
952
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
953
|
+
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
954
|
+
}
|
|
955
|
+
if (resp.error) {
|
|
956
|
+
return { error: resp.error };
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get(hookQuery.filters);
|
|
960
|
+
if (!record) {
|
|
961
|
+
return { error: 'Record not found' };
|
|
962
|
+
}
|
|
963
|
+
return { record, ctx };
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* The action dialog only ever renders the fields this plugin generates, so only those leave the
|
|
968
|
+
* backend, next to the primary key and the record label. backendOnly columns are dropped the same
|
|
969
|
+
* way the core show route drops them.
|
|
970
|
+
*/
|
|
971
|
+
scopeRecordForFrontend(record, ctx) {
|
|
972
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
973
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
974
|
+
const managedFields = this.pluginManagedFields();
|
|
975
|
+
const scoped = {
|
|
976
|
+
[primaryKeyColumn.name]: record[primaryKeyColumn.name],
|
|
977
|
+
_label: this.resourceConfig.recordLabel(record),
|
|
978
|
+
};
|
|
979
|
+
for (const column of this.resourceConfig.columns.filter((col) => managedFields.has(col.name))) {
|
|
980
|
+
if (yield resolveBoolOrFn(column.backendOnly, ctx)) {
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
scoped[column.name] = record[column.name];
|
|
984
|
+
}
|
|
985
|
+
return scoped;
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* The row as the caller is allowed to read it: columns which are not declared on the resource and
|
|
990
|
+
* columns whose `backendOnly` resolves true for this request are dropped, exactly the way the
|
|
991
|
+
* core show route drops them before a row reaches the browser.
|
|
992
|
+
*/
|
|
993
|
+
stripBackendOnlyColumns(record, ctx) {
|
|
994
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
995
|
+
const visible = {};
|
|
996
|
+
for (const column of this.resourceConfig.columns) {
|
|
997
|
+
if (yield resolveBoolOrFn(column.backendOnly, ctx)) {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
visible[column.name] = record[column.name];
|
|
1001
|
+
}
|
|
1002
|
+
return visible;
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
910
1005
|
setupEndpoints(server) {
|
|
911
1006
|
server.endpoint({
|
|
912
1007
|
method: 'POST',
|
|
913
1008
|
path: `/plugin/${this.pluginInstanceId}/get_old_data`,
|
|
914
1009
|
request_schema: getOldDataBodySchema,
|
|
915
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body,
|
|
1010
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, query, cookies, requestUrl }) {
|
|
916
1011
|
const data = body;
|
|
917
1012
|
const recordId = data.recordId;
|
|
918
1013
|
if (recordId === undefined || recordId === null) {
|
|
919
1014
|
return { ok: false, error: "Missing recordId" };
|
|
920
1015
|
}
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1016
|
+
const { record, ctx, error } = yield this.getRecordForShow({
|
|
1017
|
+
adminUser,
|
|
1018
|
+
recordId,
|
|
1019
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1020
|
+
});
|
|
1021
|
+
if (error) {
|
|
1022
|
+
return { ok: false, error };
|
|
926
1023
|
}
|
|
927
|
-
record
|
|
928
|
-
return { ok: true, record };
|
|
1024
|
+
return { ok: true, record: yield this.scopeRecordForFrontend(record, ctx) };
|
|
929
1025
|
})
|
|
930
1026
|
});
|
|
931
1027
|
server.endpoint({
|
|
932
1028
|
method: 'POST',
|
|
933
1029
|
path: `/plugin/${this.pluginInstanceId}/get_images`,
|
|
934
1030
|
request_schema: getImagesBodySchema,
|
|
935
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body,
|
|
1031
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, query, cookies, requestUrl }) {
|
|
936
1032
|
const data = body;
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1033
|
+
// attachFiles runs with server privileges and usually mints download URLs, so it is only
|
|
1034
|
+
// ever given a record loaded here after the show check, never one supplied by the client
|
|
1035
|
+
const { record, error } = yield this.getRecordForShow({
|
|
1036
|
+
adminUser,
|
|
1037
|
+
recordId: data.recordId,
|
|
1038
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1039
|
+
});
|
|
1040
|
+
if (error) {
|
|
1041
|
+
return { ok: false, error };
|
|
944
1042
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
};
|
|
1043
|
+
const images = this.options.attachFiles ? yield this.options.attachFiles({ record }) : [];
|
|
1044
|
+
return { ok: true, images };
|
|
948
1045
|
})
|
|
949
1046
|
});
|
|
950
1047
|
server.endpoint({
|
|
@@ -1074,13 +1171,18 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1074
1171
|
method: 'POST',
|
|
1075
1172
|
path: `/plugin/${this.pluginInstanceId}/get_image_generation_prompts`,
|
|
1076
1173
|
request_schema: getImageGenerationPromptsBodySchema,
|
|
1077
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, headers,
|
|
1078
|
-
var _b;
|
|
1174
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, query, cookies, requestUrl }) {
|
|
1079
1175
|
const data = body;
|
|
1080
|
-
const Id = data.recordId || [];
|
|
1081
1176
|
const customPrompt = data.customPrompt || null;
|
|
1082
|
-
const record
|
|
1083
|
-
|
|
1177
|
+
const { record, ctx, error } = yield this.getRecordForShow({
|
|
1178
|
+
adminUser,
|
|
1179
|
+
recordId: data.recordId,
|
|
1180
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1181
|
+
});
|
|
1182
|
+
if (error) {
|
|
1183
|
+
return { ok: false, error };
|
|
1184
|
+
}
|
|
1185
|
+
const compiledGenerationOptions = yield this.compileGenerationFieldTemplates(record, ctx, JSON.stringify({ "prompt": customPrompt }));
|
|
1084
1186
|
return compiledGenerationOptions;
|
|
1085
1187
|
})
|
|
1086
1188
|
});
|
|
@@ -1099,7 +1201,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1099
1201
|
method: 'POST',
|
|
1100
1202
|
path: `/plugin/${this.pluginInstanceId}/create-job`,
|
|
1101
1203
|
request_schema: createJobBodySchema,
|
|
1102
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, response }) {
|
|
1204
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, query, cookies, requestUrl, response }) {
|
|
1103
1205
|
const data = body;
|
|
1104
1206
|
const { actionType, recordId, customPrompt, filterFilledFields, sessionIds } = data;
|
|
1105
1207
|
if (this.options.rateLimits) {
|
|
@@ -1115,41 +1217,47 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1115
1217
|
return { ok: false, error: "Missing session ids" };
|
|
1116
1218
|
}
|
|
1117
1219
|
}
|
|
1118
|
-
const jobId = randomUUID();
|
|
1119
|
-
jobs.set(jobId, { status: "in_progress" });
|
|
1120
1220
|
if (!actionType) {
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1221
|
+
return { ok: false, error: "Missing action type" };
|
|
1222
|
+
}
|
|
1223
|
+
if (!recordId && typeof recordId !== 'number') {
|
|
1224
|
+
return { ok: false, error: "Missing record id" };
|
|
1225
|
+
}
|
|
1226
|
+
// every worker below compiles this record into an AI prompt whose answer is handed back
|
|
1227
|
+
// through get-job-status, so the record is read here, behind the same show check and
|
|
1228
|
+
// row-level scoping the other read endpoints use, and the row itself is what gets dispatched
|
|
1229
|
+
const { record, ctx, error } = yield this.getRecordForShow({
|
|
1230
|
+
adminUser,
|
|
1231
|
+
recordId,
|
|
1232
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1233
|
+
});
|
|
1234
|
+
if (error) {
|
|
1235
|
+
return { ok: false, error };
|
|
1127
1236
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
}
|
|
1144
|
-
this.regenerateImage(jobId, recordId, data.fieldName, data.prompt, adminUser, headers);
|
|
1145
|
-
break;
|
|
1146
|
-
case 'regenerate_cell':
|
|
1147
|
-
const fieldToRegenerate = data.fieldToRegenerate;
|
|
1148
|
-
this.regenerateCell(jobId, fieldToRegenerate, recordId, data.action, data.prompt);
|
|
1237
|
+
const jobId = randomUUID();
|
|
1238
|
+
jobs.set(jobId, { status: "in_progress" });
|
|
1239
|
+
switch (actionType) {
|
|
1240
|
+
case 'generate_images':
|
|
1241
|
+
this.initialImageGenerate(jobId, record, ctx, headers, customPrompt, filterFilledFields);
|
|
1242
|
+
break;
|
|
1243
|
+
case 'analyze_no_images':
|
|
1244
|
+
this.analyzeNoImages(jobId, record, ctx, headers, customPrompt, filterFilledFields);
|
|
1245
|
+
break;
|
|
1246
|
+
case 'analyze':
|
|
1247
|
+
this.analyze_image(jobId, record, ctx, headers, customPrompt, filterFilledFields);
|
|
1248
|
+
break;
|
|
1249
|
+
case 'regenerate_images':
|
|
1250
|
+
if (!data.prompt || !data.fieldName) {
|
|
1251
|
+
jobs.set(jobId, { status: "failed", error: "Missing prompt or field name" });
|
|
1149
1252
|
break;
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1253
|
+
}
|
|
1254
|
+
this.regenerateImage(jobId, record, data.fieldName, data.prompt, headers);
|
|
1255
|
+
break;
|
|
1256
|
+
case 'regenerate_cell':
|
|
1257
|
+
this.regenerateCell(jobId, data.fieldToRegenerate, record, ctx, data.action, data.prompt);
|
|
1258
|
+
break;
|
|
1259
|
+
default:
|
|
1260
|
+
jobs.set(jobId, { status: "failed", error: "Unknown action type" });
|
|
1153
1261
|
}
|
|
1154
1262
|
setTimeout(() => jobs.delete(jobId), 1800000);
|
|
1155
1263
|
setTimeout(() => jobs.set(jobId, { status: "failed", error: "Job timed out" }), 180000);
|
|
@@ -1258,6 +1366,11 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1258
1366
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, query, cookies, requestUrl, response }) {
|
|
1259
1367
|
var _b, _c;
|
|
1260
1368
|
const resource = this.resourceConfig;
|
|
1369
|
+
const { allowedActions } = yield interpretResource(adminUser, resource, { requestBody: body }, ActionCheckSource.ListRequest, this.adminforth);
|
|
1370
|
+
const listAllowed = allowedActions[AllowedActionsEnum.list];
|
|
1371
|
+
if (listAllowed !== true) {
|
|
1372
|
+
return { ok: false, error: typeof listAllowed === 'string' ? listAllowed : 'You are not allowed to list records in this resource' };
|
|
1373
|
+
}
|
|
1261
1374
|
for (const hook of ((_c = (_b = resource.hooks) === null || _b === void 0 ? void 0 : _b.list) === null || _c === void 0 ? void 0 : _c.beforeDatasourceRequest) || []) {
|
|
1262
1375
|
const filterTools = filtersTools.get(body);
|
|
1263
1376
|
body.filtersTools = filterTools;
|
package/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ const getRecordsBodySchema = z.object({
|
|
|
12
12
|
}).strict();
|
|
13
13
|
|
|
14
14
|
const getImagesBodySchema = z.object({
|
|
15
|
-
|
|
15
|
+
recordId: z.union([z.string(), z.number()]),
|
|
16
16
|
}).strict();
|
|
17
17
|
|
|
18
18
|
const getOldDataBodySchema = z.object({
|
|
@@ -26,7 +26,7 @@ const updateFieldsBodySchema = z.object({
|
|
|
26
26
|
}).strict();
|
|
27
27
|
|
|
28
28
|
const getImageGenerationPromptsBodySchema = z.object({
|
|
29
|
-
recordId: z.union([z.string(), z.number()])
|
|
29
|
+
recordId: z.union([z.string(), z.number()]),
|
|
30
30
|
customPrompt: z.string().nullish(),
|
|
31
31
|
}).strict();
|
|
32
32
|
|
|
@@ -123,22 +123,31 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
123
123
|
this.sessionIds = new Set();
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Compiles Handlebars templates from `source`.
|
|
128
|
+
*
|
|
129
|
+
* `record` is the raw row and is always what `provideAdditionalContextForRecord` sees, but the
|
|
130
|
+
* templates themselves are rendered against `record` only when they come from `PluginOptions`:
|
|
131
|
+
* those are written by the app author and may reference any column. Templates which arrived in
|
|
132
|
+
* the request body are rendered against the record as the caller is allowed to read it, so a
|
|
133
|
+
* hand-written `{{column}}` cannot pull a backendOnly column out of the row.
|
|
134
|
+
*/
|
|
127
135
|
private async compileTemplates<T extends Record<string, any>>(
|
|
128
136
|
source: T,
|
|
129
|
-
record: any,
|
|
137
|
+
{ record, ctx, clientSupplied }: { record: any, ctx: AccessCheckContext, clientSupplied: boolean },
|
|
130
138
|
valueSelector: (value: T[keyof T]) => string
|
|
131
139
|
): Promise<Record<string, string>> {
|
|
140
|
+
let templateContext = clientSupplied ? await this.stripBackendOnlyColumns(record, ctx) : record;
|
|
132
141
|
if (this.options.provideAdditionalContextForRecord) {
|
|
133
|
-
const additionalFields = await this.options.provideAdditionalContextForRecord({ record, adminUser:
|
|
134
|
-
|
|
142
|
+
const additionalFields = await this.options.provideAdditionalContextForRecord({ record, adminUser: ctx.adminUser, resource: this.resourceConfig });
|
|
143
|
+
templateContext = { ...templateContext, ...additionalFields };
|
|
135
144
|
}
|
|
136
145
|
const compiled: Record<string, string> = {};
|
|
137
146
|
for (const [key, value] of Object.entries(source)) {
|
|
138
147
|
const templateStr = valueSelector(value);
|
|
139
148
|
try {
|
|
140
149
|
const tpl = Handlebars.compile(templateStr);
|
|
141
|
-
compiled[key] = tpl(
|
|
150
|
+
compiled[key] = tpl(templateContext);
|
|
142
151
|
} catch {
|
|
143
152
|
compiled[key] = templateStr;
|
|
144
153
|
}
|
|
@@ -146,16 +155,16 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
146
155
|
return compiled;
|
|
147
156
|
}
|
|
148
157
|
|
|
149
|
-
private async compileOutputFieldsTemplates(record: any, customPrompt? : string) {
|
|
150
|
-
return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) :this.options.fillFieldsFromImages, record, v => String(v));
|
|
158
|
+
private async compileOutputFieldsTemplates(record: any, ctx: AccessCheckContext, customPrompt? : string, clientSupplied: boolean = !!customPrompt) {
|
|
159
|
+
return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) :this.options.fillFieldsFromImages, { record, ctx, clientSupplied }, v => String(v));
|
|
151
160
|
}
|
|
152
161
|
|
|
153
|
-
private async compileOutputFieldsTemplatesNoImage(record: any, customPrompt? : string) {
|
|
154
|
-
return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillPlainFields, record, v => String(v));
|
|
162
|
+
private async compileOutputFieldsTemplatesNoImage(record: any, ctx: AccessCheckContext, customPrompt? : string, clientSupplied: boolean = !!customPrompt) {
|
|
163
|
+
return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillPlainFields, { record, ctx, clientSupplied }, v => String(v));
|
|
155
164
|
}
|
|
156
165
|
|
|
157
|
-
private async compileGenerationFieldTemplates(record: any, customPrompt? : string) {
|
|
158
|
-
return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.generateImages, record, v => String(customPrompt ? v : v.prompt));
|
|
166
|
+
private async compileGenerationFieldTemplates(record: any, ctx: AccessCheckContext, customPrompt? : string, clientSupplied: boolean = !!customPrompt) {
|
|
167
|
+
return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.generateImages, { record, ctx, clientSupplied }, v => String(customPrompt ? v : v.prompt));
|
|
159
168
|
}
|
|
160
169
|
|
|
161
170
|
private removeFromPromptFilledFields(compiledOutputFields: Record<string, string>, record: Record<string, any>): Record<string, string> {
|
|
@@ -206,17 +215,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
206
215
|
return prompt;
|
|
207
216
|
}
|
|
208
217
|
|
|
209
|
-
private async analyze_image(jobId: string,
|
|
218
|
+
private async analyze_image(jobId: string, record: any, ctx: AccessCheckContext, headers: Record<string, string | string[] | undefined>, customPrompt? : string, filterFilledFields: boolean = true) {
|
|
210
219
|
// TODO: need to do correct check of rate limit
|
|
211
220
|
// if (this.options.rateLimits && this.options.rateLimits.fillFieldsFromImages && await this.checkRateLimit("fillFieldsFromImages" ,this.options.rateLimits.fillFieldsFromImages, headers)) {
|
|
212
221
|
// jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
|
|
213
222
|
// return { error: "Rate limit exceeded" };
|
|
214
223
|
// }
|
|
215
|
-
const selectedId = recordId;
|
|
216
224
|
let isError = false;
|
|
217
|
-
// Fetch the record using the provided ID
|
|
218
|
-
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
219
|
-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, selectedId)] );
|
|
220
225
|
|
|
221
226
|
//recieve image URLs to analyze
|
|
222
227
|
const attachmentFiles = await this.options.attachFiles({ record: record });
|
|
@@ -241,7 +246,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
241
246
|
return { ok: false, error: 'One of the image URLs is not valid' };
|
|
242
247
|
}
|
|
243
248
|
//create prompt for OpenAI
|
|
244
|
-
const compiledOutputFields = await this.compileOutputFieldsTemplates(record, customPrompt);
|
|
249
|
+
const compiledOutputFields = await this.compileOutputFieldsTemplates(record, ctx, customPrompt);
|
|
245
250
|
const filteredCompiledOutputFields = filterFilledFields ? this.removeFromPromptFilledFields(compiledOutputFields, record) : compiledOutputFields;
|
|
246
251
|
|
|
247
252
|
if (Object.keys(filteredCompiledOutputFields).length === 0) {
|
|
@@ -291,13 +296,12 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
291
296
|
|
|
292
297
|
}
|
|
293
298
|
|
|
294
|
-
private async analyzeNoImages(jobId: string,
|
|
299
|
+
private async analyzeNoImages(jobId: string, record: any, ctx: AccessCheckContext, headers: Record<string, string | string[] | undefined>, customPrompt? : string, filterFilledFields: boolean = true) {
|
|
295
300
|
// TODO: need to do correct check of rate limit
|
|
296
301
|
// if (this.options.rateLimits && this.options.rateLimits.fillPlainFields && await this.checkRateLimit("fillPlainFields" ,this.options.rateLimits.fillPlainFields, headers)) {
|
|
297
302
|
// jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
|
|
298
303
|
// return { error: "Rate limit exceeded" };
|
|
299
304
|
// }
|
|
300
|
-
const selectedId = recordId;
|
|
301
305
|
let isError = false;
|
|
302
306
|
if (STUB_MODE) {
|
|
303
307
|
const fakeError = Math.random() < 0.005; // 0.05% chance of error
|
|
@@ -310,10 +314,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
310
314
|
return { ok: true };
|
|
311
315
|
}
|
|
312
316
|
} else {
|
|
313
|
-
const
|
|
314
|
-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, selectedId)] );
|
|
315
|
-
|
|
316
|
-
const compiledOutputFields = await this.compileOutputFieldsTemplatesNoImage(record, customPrompt);
|
|
317
|
+
const compiledOutputFields = await this.compileOutputFieldsTemplatesNoImage(record, ctx, customPrompt);
|
|
317
318
|
|
|
318
319
|
const filteredCompiledOutputFields = filterFilledFields ? this.removeFromPromptFilledFields(compiledOutputFields, record) : compiledOutputFields;
|
|
319
320
|
|
|
@@ -355,16 +356,14 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
355
356
|
}
|
|
356
357
|
}
|
|
357
358
|
|
|
358
|
-
private async initialImageGenerate(jobId: string,
|
|
359
|
+
private async initialImageGenerate(jobId: string, record: any, ctx: AccessCheckContext, headers: Record<string, string | string[] | undefined>, customPrompt? : string, filterFilledFields: boolean = true) {
|
|
359
360
|
// TODO: need to do correct check of rate limit
|
|
360
361
|
// if (this.options.rateLimits && this.options.rateLimits.generateImages && await this.checkRateLimit("generateImages" ,this.options.rateLimits.generateImages, headers)) {
|
|
361
362
|
// jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
|
|
362
363
|
// return { error: "Rate limit exceeded" };
|
|
363
364
|
// }
|
|
364
|
-
const selectedId = recordId;
|
|
365
365
|
let isError = false;
|
|
366
366
|
const start = +new Date();
|
|
367
|
-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, selectedId)]);
|
|
368
367
|
let attachmentFiles
|
|
369
368
|
if(!this.options.attachFiles){
|
|
370
369
|
attachmentFiles = [];
|
|
@@ -395,7 +394,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
395
394
|
}
|
|
396
395
|
};
|
|
397
396
|
}
|
|
398
|
-
const prompt = (await this.compileGenerationFieldTemplates(record, customPrompt))[key];
|
|
397
|
+
const prompt = (await this.compileGenerationFieldTemplates(record, ctx, customPrompt))[key];
|
|
399
398
|
let images;
|
|
400
399
|
if (this.options.attachFiles && attachmentFiles.length === 0) {
|
|
401
400
|
isError = true;
|
|
@@ -467,8 +466,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
467
466
|
}
|
|
468
467
|
}
|
|
469
468
|
|
|
470
|
-
private async regenerateImage(jobId: string,
|
|
471
|
-
const Id = recordId;
|
|
469
|
+
private async regenerateImage(jobId: string, record: any, fieldName: string, prompt: string, headers: Record<string, string | string[] | undefined>) {
|
|
472
470
|
let isError = false;
|
|
473
471
|
// TODO: need to do correct check of rate limit
|
|
474
472
|
// if (await this.checkRateLimit(fieldName, this.options.generateImages[fieldName].rateLimit, headers)) {
|
|
@@ -476,7 +474,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
476
474
|
// return { error: "Rate limit exceeded" };
|
|
477
475
|
// }
|
|
478
476
|
const start = +new Date();
|
|
479
|
-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, Id)]);
|
|
480
477
|
let attachmentFiles
|
|
481
478
|
if(!this.options.attachFiles){
|
|
482
479
|
attachmentFiles = [];
|
|
@@ -536,11 +533,14 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
536
533
|
}
|
|
537
534
|
}
|
|
538
535
|
|
|
539
|
-
private async regenerateCell(jobId, fieldToRegenerate,
|
|
540
|
-
if (!fieldToRegenerate || !
|
|
536
|
+
private async regenerateCell(jobId, fieldToRegenerate, record: any, ctx: AccessCheckContext, actionType, prompt) {
|
|
537
|
+
if (!fieldToRegenerate || !actionType ) {
|
|
541
538
|
jobs.set(jobId, { status: 'failed', error: 'Missing parameters' });
|
|
542
539
|
//return { ok: false, error: "Missing parameters" };
|
|
543
540
|
}
|
|
541
|
+
// a prompt which arrived in the request body is client-authored even though it is wrapped into
|
|
542
|
+
// `promptToPass` below, while the PluginOptions fallback is written by the app author
|
|
543
|
+
const clientSupplied = !!prompt;
|
|
544
544
|
if ( !prompt ) {
|
|
545
545
|
if (actionType === 'analyze') {
|
|
546
546
|
prompt = this.options.fillFieldsFromImages ? (this.options.fillFieldsFromImages as any)[fieldToRegenerate] : null;
|
|
@@ -548,9 +548,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
548
548
|
prompt = this.options.fillPlainFields ? (this.options.fillPlainFields as any)[fieldToRegenerate] : null;
|
|
549
549
|
}
|
|
550
550
|
}
|
|
551
|
-
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
552
|
-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, recordId)] );
|
|
553
|
-
|
|
554
551
|
let promptToPass = JSON.stringify({[fieldToRegenerate]: prompt});
|
|
555
552
|
if (STUB_MODE) {
|
|
556
553
|
await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
|
|
@@ -558,7 +555,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
558
555
|
jobs.set(jobId, { status: 'completed', result: {[fieldToRegenerate]: "stub value"} });
|
|
559
556
|
} else {
|
|
560
557
|
if ( actionType === 'analyze') {
|
|
561
|
-
const compiledPropmt = await this.compileOutputFieldsTemplates(record, promptToPass);
|
|
558
|
+
const compiledPropmt = await this.compileOutputFieldsTemplates(record, ctx, promptToPass, clientSupplied);
|
|
562
559
|
const finalPrompt = this.getPromptForImageAnalysis(compiledPropmt);
|
|
563
560
|
const attachmentFiles = await this.options.attachFiles({ record: record });
|
|
564
561
|
if (attachmentFiles.length === 0) {
|
|
@@ -595,7 +592,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
595
592
|
jobs.set(jobId, { status: 'completed', result: resData });
|
|
596
593
|
|
|
597
594
|
} else if ( actionType === 'analyze_no_images') {
|
|
598
|
-
const compiledPropmt = await this.compileOutputFieldsTemplatesNoImage(record, promptToPass);
|
|
595
|
+
const compiledPropmt = await this.compileOutputFieldsTemplatesNoImage(record, ctx, promptToPass, clientSupplied);
|
|
599
596
|
const finalPrompt = this.getPromptForPlainFields(compiledPropmt);
|
|
600
597
|
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
|
|
601
598
|
let resp;
|
|
@@ -949,25 +946,132 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
949
946
|
return null;
|
|
950
947
|
}
|
|
951
948
|
|
|
949
|
+
/**
|
|
950
|
+
* Reads one record the way the core show route does: the resource-level `show` permission first,
|
|
951
|
+
* then the resource's own `show.beforeDatasourceRequest` hooks so row-level scoping narrows the
|
|
952
|
+
* filters before the query runs.
|
|
953
|
+
* The returned record is the raw row, so callers which send it to the browser must pass it
|
|
954
|
+
* through `scopeRecordForFrontend` first.
|
|
955
|
+
*/
|
|
956
|
+
private async getRecordForShow({ adminUser, recordId, extra }: {
|
|
957
|
+
adminUser: AdminUser,
|
|
958
|
+
recordId: any,
|
|
959
|
+
extra: { body: any, query: any, headers: any, cookies: any, requestUrl: string },
|
|
960
|
+
}): Promise<{ record?: any, ctx?: AccessCheckContext, error?: string }> {
|
|
961
|
+
const meta = { requestBody: extra.body, pk: recordId };
|
|
962
|
+
const ctx: AccessCheckContext = {
|
|
963
|
+
adminUser,
|
|
964
|
+
resource: this.resourceConfig,
|
|
965
|
+
meta,
|
|
966
|
+
source: ActionCheckSource.ShowRequest,
|
|
967
|
+
adminforth: this.adminforth,
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
const { allowedActions } = await interpretResource(
|
|
971
|
+
adminUser,
|
|
972
|
+
this.resourceConfig,
|
|
973
|
+
meta,
|
|
974
|
+
ActionCheckSource.ShowRequest,
|
|
975
|
+
this.adminforth,
|
|
976
|
+
);
|
|
977
|
+
const showAllowed = allowedActions[AllowedActionsEnum.show] as boolean | string | undefined;
|
|
978
|
+
if (showAllowed !== true) {
|
|
979
|
+
return { error: typeof showAllowed === 'string' ? showAllowed : 'You are not allowed to view records in this resource' };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
983
|
+
const hookQuery: any = {
|
|
984
|
+
resourceId: this.resourceConfig.resourceId,
|
|
985
|
+
source: 'show',
|
|
986
|
+
filters: [Filters.EQ(primaryKeyColumn.name, recordId)],
|
|
987
|
+
limit: 1,
|
|
988
|
+
offset: 0,
|
|
989
|
+
sort: [],
|
|
990
|
+
};
|
|
991
|
+
for (const hook of this.resourceConfig.hooks?.show?.beforeDatasourceRequest || []) {
|
|
992
|
+
const filterTools = filtersTools.get(hookQuery);
|
|
993
|
+
hookQuery.filtersTools = filterTools;
|
|
994
|
+
const resp = await hook({
|
|
995
|
+
resource: this.resourceConfig,
|
|
996
|
+
query: hookQuery,
|
|
997
|
+
adminUser,
|
|
998
|
+
//@ts-ignore
|
|
999
|
+
filtersTools: filterTools,
|
|
1000
|
+
extra,
|
|
1001
|
+
adminforth: this.adminforth,
|
|
1002
|
+
});
|
|
1003
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
1004
|
+
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
1005
|
+
}
|
|
1006
|
+
if (resp.error) {
|
|
1007
|
+
return { error: resp.error };
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get(hookQuery.filters);
|
|
1012
|
+
if (!record) {
|
|
1013
|
+
return { error: 'Record not found' };
|
|
1014
|
+
}
|
|
1015
|
+
return { record, ctx };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* The action dialog only ever renders the fields this plugin generates, so only those leave the
|
|
1020
|
+
* backend, next to the primary key and the record label. backendOnly columns are dropped the same
|
|
1021
|
+
* way the core show route drops them.
|
|
1022
|
+
*/
|
|
1023
|
+
private async scopeRecordForFrontend(record: any, ctx: AccessCheckContext): Promise<Record<string, any>> {
|
|
1024
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
1025
|
+
const managedFields = this.pluginManagedFields();
|
|
1026
|
+
const scoped: Record<string, any> = {
|
|
1027
|
+
[primaryKeyColumn.name]: record[primaryKeyColumn.name],
|
|
1028
|
+
_label: this.resourceConfig.recordLabel(record),
|
|
1029
|
+
};
|
|
1030
|
+
for (const column of this.resourceConfig.columns.filter((col) => managedFields.has(col.name))) {
|
|
1031
|
+
if (await resolveBoolOrFn(column.backendOnly, ctx)) {
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
scoped[column.name] = record[column.name];
|
|
1035
|
+
}
|
|
1036
|
+
return scoped;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* The row as the caller is allowed to read it: columns which are not declared on the resource and
|
|
1041
|
+
* columns whose `backendOnly` resolves true for this request are dropped, exactly the way the
|
|
1042
|
+
* core show route drops them before a row reaches the browser.
|
|
1043
|
+
*/
|
|
1044
|
+
private async stripBackendOnlyColumns(record: any, ctx: AccessCheckContext): Promise<Record<string, any>> {
|
|
1045
|
+
const visible: Record<string, any> = {};
|
|
1046
|
+
for (const column of this.resourceConfig.columns) {
|
|
1047
|
+
if (await resolveBoolOrFn(column.backendOnly, ctx)) {
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
visible[column.name] = record[column.name];
|
|
1051
|
+
}
|
|
1052
|
+
return visible;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
952
1055
|
setupEndpoints(server: IHttpServer) {
|
|
953
1056
|
server.endpoint({
|
|
954
1057
|
method: 'POST',
|
|
955
1058
|
path: `/plugin/${this.pluginInstanceId}/get_old_data`,
|
|
956
1059
|
request_schema: getOldDataBodySchema,
|
|
957
|
-
handler: async ({ body,
|
|
1060
|
+
handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
|
|
958
1061
|
const data = body as z.infer<typeof getOldDataBodySchema>;
|
|
959
1062
|
const recordId = data.recordId;
|
|
960
1063
|
if (recordId === undefined || recordId === null) {
|
|
961
1064
|
return { ok: false, error: "Missing recordId" };
|
|
962
1065
|
}
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1066
|
+
const { record, ctx, error } = await this.getRecordForShow({
|
|
1067
|
+
adminUser,
|
|
1068
|
+
recordId,
|
|
1069
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1070
|
+
});
|
|
1071
|
+
if (error) {
|
|
1072
|
+
return { ok: false, error };
|
|
968
1073
|
}
|
|
969
|
-
record
|
|
970
|
-
return { ok: true, record };
|
|
1074
|
+
return { ok: true, record: await this.scopeRecordForFrontend(record, ctx) };
|
|
971
1075
|
}
|
|
972
1076
|
});
|
|
973
1077
|
|
|
@@ -976,19 +1080,20 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
976
1080
|
method: 'POST',
|
|
977
1081
|
path: `/plugin/${this.pluginInstanceId}/get_images`,
|
|
978
1082
|
request_schema: getImagesBodySchema,
|
|
979
|
-
handler: async ({ body,
|
|
1083
|
+
handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
|
|
980
1084
|
const data = body as z.infer<typeof getImagesBodySchema>;
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1085
|
+
// attachFiles runs with server privileges and usually mints download URLs, so it is only
|
|
1086
|
+
// ever given a record loaded here after the show check, never one supplied by the client
|
|
1087
|
+
const { record, error } = await this.getRecordForShow({
|
|
1088
|
+
adminUser,
|
|
1089
|
+
recordId: data.recordId,
|
|
1090
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1091
|
+
});
|
|
1092
|
+
if (error) {
|
|
1093
|
+
return { ok: false, error };
|
|
988
1094
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
};
|
|
1095
|
+
const images = this.options.attachFiles ? await this.options.attachFiles({ record }) : [];
|
|
1096
|
+
return { ok: true, images };
|
|
992
1097
|
}
|
|
993
1098
|
});
|
|
994
1099
|
|
|
@@ -1130,12 +1235,18 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1130
1235
|
method: 'POST',
|
|
1131
1236
|
path: `/plugin/${this.pluginInstanceId}/get_image_generation_prompts`,
|
|
1132
1237
|
request_schema: getImageGenerationPromptsBodySchema,
|
|
1133
|
-
handler: async ({ body, headers,
|
|
1238
|
+
handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
|
|
1134
1239
|
const data = body as z.infer<typeof getImageGenerationPromptsBodySchema>;
|
|
1135
|
-
const Id = data.recordId || [];
|
|
1136
1240
|
const customPrompt = data.customPrompt || null;
|
|
1137
|
-
const record = await this.
|
|
1138
|
-
|
|
1241
|
+
const { record, ctx, error } = await this.getRecordForShow({
|
|
1242
|
+
adminUser,
|
|
1243
|
+
recordId: data.recordId,
|
|
1244
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1245
|
+
});
|
|
1246
|
+
if (error) {
|
|
1247
|
+
return { ok: false, error };
|
|
1248
|
+
}
|
|
1249
|
+
const compiledGenerationOptions = await this.compileGenerationFieldTemplates(record, ctx, JSON.stringify({"prompt": customPrompt}));
|
|
1139
1250
|
return compiledGenerationOptions;
|
|
1140
1251
|
}
|
|
1141
1252
|
});
|
|
@@ -1158,7 +1269,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1158
1269
|
method: 'POST',
|
|
1159
1270
|
path: `/plugin/${this.pluginInstanceId}/create-job`,
|
|
1160
1271
|
request_schema: createJobBodySchema,
|
|
1161
|
-
handler: async ({ body, adminUser, headers, response }) => {
|
|
1272
|
+
handler: async ({ body, adminUser, headers, query, cookies, requestUrl, response }) => {
|
|
1162
1273
|
const data = body as z.infer<typeof createJobBodySchema>;
|
|
1163
1274
|
const { actionType, recordId, customPrompt, filterFilledFields, sessionIds } = data;
|
|
1164
1275
|
if (this.options.rateLimits) {
|
|
@@ -1173,40 +1284,47 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1173
1284
|
return { ok: false, error: "Missing session ids" };
|
|
1174
1285
|
}
|
|
1175
1286
|
}
|
|
1176
|
-
const jobId = randomUUID();
|
|
1177
|
-
jobs.set(jobId, { status: "in_progress" });
|
|
1178
1287
|
if (!actionType) {
|
|
1179
|
-
|
|
1180
|
-
//return { error: "Missing action type" };
|
|
1288
|
+
return { ok: false, error: "Missing action type" };
|
|
1181
1289
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1290
|
+
if (!recordId && typeof recordId !== 'number') {
|
|
1291
|
+
return { ok: false, error: "Missing record id" };
|
|
1292
|
+
}
|
|
1293
|
+
// every worker below compiles this record into an AI prompt whose answer is handed back
|
|
1294
|
+
// through get-job-status, so the record is read here, behind the same show check and
|
|
1295
|
+
// row-level scoping the other read endpoints use, and the row itself is what gets dispatched
|
|
1296
|
+
const { record, ctx, error } = await this.getRecordForShow({
|
|
1297
|
+
adminUser,
|
|
1298
|
+
recordId,
|
|
1299
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1300
|
+
});
|
|
1301
|
+
if (error) {
|
|
1302
|
+
return { ok: false, error };
|
|
1303
|
+
}
|
|
1304
|
+
const jobId = randomUUID();
|
|
1305
|
+
jobs.set(jobId, { status: "in_progress" });
|
|
1306
|
+
switch(actionType) {
|
|
1307
|
+
case 'generate_images':
|
|
1308
|
+
this.initialImageGenerate(jobId, record, ctx, headers, customPrompt, filterFilledFields);
|
|
1309
|
+
break;
|
|
1310
|
+
case 'analyze_no_images':
|
|
1311
|
+
this.analyzeNoImages(jobId, record, ctx, headers, customPrompt, filterFilledFields);
|
|
1312
|
+
break;
|
|
1313
|
+
case 'analyze':
|
|
1314
|
+
this.analyze_image(jobId, record, ctx, headers, customPrompt, filterFilledFields);
|
|
1315
|
+
break;
|
|
1316
|
+
case 'regenerate_images':
|
|
1317
|
+
if (!data.prompt || !data.fieldName) {
|
|
1318
|
+
jobs.set(jobId, { status: "failed", error: "Missing prompt or field name" });
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
this.regenerateImage(jobId, record, data.fieldName, data.prompt, headers);
|
|
1322
|
+
break;
|
|
1323
|
+
case 'regenerate_cell':
|
|
1324
|
+
this.regenerateCell(jobId, data.fieldToRegenerate, record, ctx, data.action, data.prompt);
|
|
1325
|
+
break;
|
|
1326
|
+
default:
|
|
1327
|
+
jobs.set(jobId, { status: "failed", error: "Unknown action type" });
|
|
1210
1328
|
}
|
|
1211
1329
|
setTimeout(() => jobs.delete(jobId), 1_800_000);
|
|
1212
1330
|
setTimeout(() => jobs.set(jobId, { status: "failed", error: "Job timed out" }), 180_000);
|
|
@@ -1324,6 +1442,18 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1324
1442
|
handler: async ({ body, adminUser, headers, query, cookies, requestUrl, response }) => {
|
|
1325
1443
|
const resource = this.resourceConfig;
|
|
1326
1444
|
|
|
1445
|
+
const { allowedActions } = await interpretResource(
|
|
1446
|
+
adminUser,
|
|
1447
|
+
resource,
|
|
1448
|
+
{ requestBody: body },
|
|
1449
|
+
ActionCheckSource.ListRequest,
|
|
1450
|
+
this.adminforth,
|
|
1451
|
+
);
|
|
1452
|
+
const listAllowed = allowedActions[AllowedActionsEnum.list] as boolean | string | undefined;
|
|
1453
|
+
if (listAllowed !== true) {
|
|
1454
|
+
return { ok: false, error: typeof listAllowed === 'string' ? listAllowed : 'You are not allowed to list records in this resource' };
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1327
1457
|
for (const hook of resource.hooks?.list?.beforeDatasourceRequest || []) {
|
|
1328
1458
|
const filterTools = filtersTools.get(body);
|
|
1329
1459
|
body.filtersTools = filterTools;
|