@purposeinplay/payload-ai-translate 0.1.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/LICENSE +21 -0
- package/README.md +714 -0
- package/dist/alerts-collection.d.ts +21 -0
- package/dist/alerts-collection.js +159 -0
- package/dist/api.d.ts +4 -0
- package/dist/api.js +918 -0
- package/dist/bulk-translate-batches-collection.d.ts +29 -0
- package/dist/bulk-translate-batches-collection.js +404 -0
- package/dist/bulk-translate-units-collection.d.ts +35 -0
- package/dist/bulk-translate-units-collection.js +310 -0
- package/dist/client/estimated-cost-cell.d.ts +6 -0
- package/dist/client/estimated-cost-cell.js +12 -0
- package/dist/client/excluded-fields-field.d.ts +45 -0
- package/dist/client/excluded-fields-field.js +553 -0
- package/dist/client/field-translate-button.d.ts +6 -0
- package/dist/client/field-translate-button.js +199 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +6 -0
- package/dist/client/lib/use-global-kill-switches.d.ts +20 -0
- package/dist/client/lib/use-global-kill-switches.js +58 -0
- package/dist/client/translate-button.d.ts +2 -0
- package/dist/client/translate-button.js +228 -0
- package/dist/client/translate-modal.d.ts +16 -0
- package/dist/client/translate-modal.js +549 -0
- package/dist/client/translation-progress.d.ts +10 -0
- package/dist/client/translation-progress.js +297 -0
- package/dist/components/TranslationNavGroup.d.ts +45 -0
- package/dist/components/TranslationNavGroup.js +104 -0
- package/dist/defaults.d.ts +11 -0
- package/dist/defaults.js +16 -0
- package/dist/endpoints/client-config.d.ts +44 -0
- package/dist/endpoints/client-config.js +145 -0
- package/dist/endpoints/estimate.d.ts +5 -0
- package/dist/endpoints/estimate.js +237 -0
- package/dist/endpoints/progress.d.ts +2 -0
- package/dist/endpoints/progress.js +314 -0
- package/dist/endpoints/translate.d.ts +11 -0
- package/dist/endpoints/translate.js +376 -0
- package/dist/endpoints/translation-hub/_helpers.d.ts +140 -0
- package/dist/endpoints/translation-hub/_helpers.js +297 -0
- package/dist/endpoints/translation-hub/active.d.ts +21 -0
- package/dist/endpoints/translation-hub/active.js +220 -0
- package/dist/endpoints/translation-hub/cancel.d.ts +22 -0
- package/dist/endpoints/translation-hub/cancel.js +233 -0
- package/dist/endpoints/translation-hub/enqueue.d.ts +70 -0
- package/dist/endpoints/translation-hub/enqueue.js +529 -0
- package/dist/endpoints/translation-hub/failures.d.ts +12 -0
- package/dist/endpoints/translation-hub/failures.js +67 -0
- package/dist/endpoints/translation-hub/force-reset.d.ts +20 -0
- package/dist/endpoints/translation-hub/force-reset.js +144 -0
- package/dist/endpoints/translation-hub/index.d.ts +21 -0
- package/dist/endpoints/translation-hub/index.js +20 -0
- package/dist/endpoints/translation-hub/list.d.ts +40 -0
- package/dist/endpoints/translation-hub/list.js +182 -0
- package/dist/endpoints/translation-hub/preflight.d.ts +19 -0
- package/dist/endpoints/translation-hub/preflight.js +141 -0
- package/dist/endpoints/translation-hub/retry-failed.d.ts +38 -0
- package/dist/endpoints/translation-hub/retry-failed.js +235 -0
- package/dist/endpoints/translation-hub/revert.d.ts +88 -0
- package/dist/endpoints/translation-hub/revert.js +405 -0
- package/dist/endpoints/translation-hub/status.d.ts +45 -0
- package/dist/endpoints/translation-hub/status.js +391 -0
- package/dist/endpoints/translation-hub/usage-summary.d.ts +114 -0
- package/dist/endpoints/translation-hub/usage-summary.js +481 -0
- package/dist/exports/client.d.ts +6 -0
- package/dist/exports/client.js +6 -0
- package/dist/exports/components.d.ts +6 -0
- package/dist/exports/components.js +5 -0
- package/dist/exports/index.d.ts +8 -0
- package/dist/exports/index.js +7 -0
- package/dist/exports/providers.d.ts +9 -0
- package/dist/exports/providers.js +5 -0
- package/dist/exports/views-client.d.ts +23 -0
- package/dist/exports/views-client.js +22 -0
- package/dist/exports/views.d.ts +30 -0
- package/dist/exports/views.js +29 -0
- package/dist/hooks/after-change-global.d.ts +4 -0
- package/dist/hooks/after-change-global.js +109 -0
- package/dist/hooks/after-change.d.ts +16 -0
- package/dist/hooks/after-change.js +205 -0
- package/dist/hooks/after-delete.d.ts +30 -0
- package/dist/hooks/after-delete.js +95 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/jobs-collection.d.ts +17 -0
- package/dist/jobs-collection.js +139 -0
- package/dist/lexical/classifier.d.ts +3 -0
- package/dist/lexical/classifier.js +108 -0
- package/dist/lexical/deserializer.d.ts +4 -0
- package/dist/lexical/deserializer.js +263 -0
- package/dist/lexical/placeholder-integrity.d.ts +6 -0
- package/dist/lexical/placeholder-integrity.js +21 -0
- package/dist/lexical/placeholders.d.ts +21 -0
- package/dist/lexical/placeholders.js +117 -0
- package/dist/lexical/serializer.d.ts +21 -0
- package/dist/lexical/serializer.js +233 -0
- package/dist/lexical/types.d.ts +32 -0
- package/dist/lexical/types.js +1 -0
- package/dist/lib/auth-diagnostics.d.ts +14 -0
- package/dist/lib/auth-diagnostics.js +19 -0
- package/dist/lib/batch-counts.d.ts +58 -0
- package/dist/lib/batch-counts.js +105 -0
- package/dist/lib/bulk-translate-migrations.d.ts +92 -0
- package/dist/lib/bulk-translate-migrations.js +153 -0
- package/dist/lib/coalescing-queue.d.ts +38 -0
- package/dist/lib/coalescing-queue.js +69 -0
- package/dist/lib/content-extractor.d.ts +16 -0
- package/dist/lib/content-extractor.js +410 -0
- package/dist/lib/content-hash.d.ts +1 -0
- package/dist/lib/content-hash.js +19 -0
- package/dist/lib/content-patcher.d.ts +15 -0
- package/dist/lib/content-patcher.js +293 -0
- package/dist/lib/cost-guards.d.ts +2 -0
- package/dist/lib/cost-guards.js +18 -0
- package/dist/lib/daily-spend-cap.d.ts +58 -0
- package/dist/lib/daily-spend-cap.js +233 -0
- package/dist/lib/effective-locales.d.ts +181 -0
- package/dist/lib/effective-locales.js +302 -0
- package/dist/lib/error-messages.d.ts +245 -0
- package/dist/lib/error-messages.js +626 -0
- package/dist/lib/events.d.ts +39 -0
- package/dist/lib/events.js +146 -0
- package/dist/lib/exclude-fields.d.ts +3 -0
- package/dist/lib/exclude-fields.js +64 -0
- package/dist/lib/field-breadcrumb.d.ts +31 -0
- package/dist/lib/field-breadcrumb.js +227 -0
- package/dist/lib/field-diff.d.ts +1 -0
- package/dist/lib/field-diff.js +25 -0
- package/dist/lib/field-empty.d.ts +2 -0
- package/dist/lib/field-empty.js +68 -0
- package/dist/lib/field-resolver.d.ts +3 -0
- package/dist/lib/field-resolver.js +164 -0
- package/dist/lib/group-soft-skips.d.ts +39 -0
- package/dist/lib/group-soft-skips.js +45 -0
- package/dist/lib/locale-merge.d.ts +44 -0
- package/dist/lib/locale-merge.js +357 -0
- package/dist/lib/locale-row-check.d.ts +30 -0
- package/dist/lib/locale-row-check.js +64 -0
- package/dist/lib/logger.d.ts +74 -0
- package/dist/lib/logger.js +97 -0
- package/dist/lib/manual-edit-guard.d.ts +128 -0
- package/dist/lib/manual-edit-guard.js +393 -0
- package/dist/lib/output-validation.d.ts +48 -0
- package/dist/lib/output-validation.js +148 -0
- package/dist/lib/payload-read.d.ts +16 -0
- package/dist/lib/payload-read.js +51 -0
- package/dist/lib/per-doc-claim.d.ts +90 -0
- package/dist/lib/per-doc-claim.js +140 -0
- package/dist/lib/per-doc-lock.d.ts +94 -0
- package/dist/lib/per-doc-lock.js +119 -0
- package/dist/lib/persist-usage.d.ts +91 -0
- package/dist/lib/persist-usage.js +116 -0
- package/dist/lib/progress-store.d.ts +103 -0
- package/dist/lib/progress-store.js +314 -0
- package/dist/lib/rate-limiter.d.ts +3 -0
- package/dist/lib/rate-limiter.js +53 -0
- package/dist/lib/snapshot-select.d.ts +43 -0
- package/dist/lib/snapshot-select.js +108 -0
- package/dist/lib/translate-prompt.d.ts +31 -0
- package/dist/lib/translate-prompt.js +66 -0
- package/dist/lib/translation-token-bucket.d.ts +57 -0
- package/dist/lib/translation-token-bucket.js +365 -0
- package/dist/lib/truncate-source-value.d.ts +1 -0
- package/dist/lib/truncate-source-value.js +27 -0
- package/dist/manual-edit-collection.d.ts +22 -0
- package/dist/manual-edit-collection.js +124 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +934 -0
- package/dist/providers/ai-sdk-adapter.d.ts +35 -0
- package/dist/providers/ai-sdk-adapter.js +100 -0
- package/dist/providers/anthropic.d.ts +31 -0
- package/dist/providers/anthropic.js +66 -0
- package/dist/providers/custom.d.ts +36 -0
- package/dist/providers/custom.js +24 -0
- package/dist/providers/gemini.d.ts +20 -0
- package/dist/providers/gemini.js +48 -0
- package/dist/providers/mock.d.ts +2 -0
- package/dist/providers/mock.js +29 -0
- package/dist/providers/openai.d.ts +28 -0
- package/dist/providers/openai.js +69 -0
- package/dist/settings-global.d.ts +74 -0
- package/dist/settings-global.js +216 -0
- package/dist/tasks/bulk-translate-coordinator.d.ts +115 -0
- package/dist/tasks/bulk-translate-coordinator.js +708 -0
- package/dist/tasks/bulk-translate-doc-task.d.ts +142 -0
- package/dist/tasks/bulk-translate-doc-task.js +1000 -0
- package/dist/tasks/bulk-translate-janitor.d.ts +87 -0
- package/dist/tasks/bulk-translate-janitor.js +311 -0
- package/dist/tasks/translate-job-task.d.ts +51 -0
- package/dist/tasks/translate-job-task.js +154 -0
- package/dist/translate.d.ts +113 -0
- package/dist/translate.js +911 -0
- package/dist/translation-daily-spend-collection.d.ts +24 -0
- package/dist/translation-daily-spend-collection.js +133 -0
- package/dist/translation-rate-limits-collection.d.ts +30 -0
- package/dist/translation-rate-limits-collection.js +144 -0
- package/dist/types.d.ts +672 -0
- package/dist/types.js +1 -0
- package/dist/usage-collection.d.ts +14 -0
- package/dist/usage-collection.js +377 -0
- package/dist/views/BulkRunsHub/BatchRow.d.ts +32 -0
- package/dist/views/BulkRunsHub/BatchRow.js +1222 -0
- package/dist/views/BulkRunsHub/BucketRow.d.ts +62 -0
- package/dist/views/BulkRunsHub/BucketRow.js +982 -0
- package/dist/views/BulkRunsHub/BulkRunsHub.client.d.ts +18 -0
- package/dist/views/BulkRunsHub/BulkRunsHub.client.js +331 -0
- package/dist/views/BulkRunsHub/EmptyState.d.ts +6 -0
- package/dist/views/BulkRunsHub/EmptyState.js +64 -0
- package/dist/views/BulkRunsHub/FilterBar.d.ts +16 -0
- package/dist/views/BulkRunsHub/FilterBar.js +284 -0
- package/dist/views/BulkRunsHub/InFlightBanner.d.ts +14 -0
- package/dist/views/BulkRunsHub/InFlightBanner.js +59 -0
- package/dist/views/BulkRunsHub/StatusBadge.d.ts +64 -0
- package/dist/views/BulkRunsHub/StatusBadge.js +248 -0
- package/dist/views/BulkRunsHub/SummaryStrip.d.ts +22 -0
- package/dist/views/BulkRunsHub/SummaryStrip.js +249 -0
- package/dist/views/BulkRunsHub/bucket-grouping.d.ts +200 -0
- package/dist/views/BulkRunsHub/bucket-grouping.js +344 -0
- package/dist/views/BulkRunsHub/bucketFailureSummary.d.ts +9 -0
- package/dist/views/BulkRunsHub/bucketFailureSummary.js +36 -0
- package/dist/views/BulkRunsHub/dedupedStatusFetch.d.ts +5 -0
- package/dist/views/BulkRunsHub/dedupedStatusFetch.js +45 -0
- package/dist/views/BulkRunsHub/index.d.ts +17 -0
- package/dist/views/BulkRunsHub/index.js +80 -0
- package/dist/views/BulkRunsHub/urlFilters.d.ts +14 -0
- package/dist/views/BulkRunsHub/urlFilters.js +50 -0
- package/dist/views/BulkRunsHub/useBulkRunsList.d.ts +26 -0
- package/dist/views/BulkRunsHub/useBulkRunsList.js +204 -0
- package/dist/views/BulkRunsHub/useUrlFilters.d.ts +10 -0
- package/dist/views/BulkRunsHub/useUrlFilters.js +88 -0
- package/dist/views/TranslationHub/ActiveJobs.d.ts +6 -0
- package/dist/views/TranslationHub/ActiveJobs.js +320 -0
- package/dist/views/TranslationHub/AdvancedPanel.d.ts +17 -0
- package/dist/views/TranslationHub/AdvancedPanel.js +996 -0
- package/dist/views/TranslationHub/AlertBanner.d.ts +6 -0
- package/dist/views/TranslationHub/AlertBanner.js +568 -0
- package/dist/views/TranslationHub/AuditPanel.d.ts +6 -0
- package/dist/views/TranslationHub/AuditPanel.helpers.d.ts +44 -0
- package/dist/views/TranslationHub/AuditPanel.helpers.js +71 -0
- package/dist/views/TranslationHub/AuditPanel.js +1367 -0
- package/dist/views/TranslationHub/BulkTranslate.types.d.ts +242 -0
- package/dist/views/TranslationHub/BulkTranslate.types.js +36 -0
- package/dist/views/TranslationHub/BulkTranslateFailureDrawer.d.ts +19 -0
- package/dist/views/TranslationHub/BulkTranslateFailureDrawer.js +332 -0
- package/dist/views/TranslationHub/BulkTranslateMonitor.d.ts +28 -0
- package/dist/views/TranslationHub/BulkTranslateMonitor.js +305 -0
- package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.d.ts +3 -0
- package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.js +42 -0
- package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.d.ts +26 -0
- package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.js +95 -0
- package/dist/views/TranslationHub/BulkTranslatePreflightModal.d.ts +22 -0
- package/dist/views/TranslationHub/BulkTranslatePreflightModal.js +879 -0
- package/dist/views/TranslationHub/BulkTranslateTerminalCard.d.ts +29 -0
- package/dist/views/TranslationHub/BulkTranslateTerminalCard.js +445 -0
- package/dist/views/TranslationHub/BulkTranslateTrigger.d.ts +66 -0
- package/dist/views/TranslationHub/BulkTranslateTrigger.js +161 -0
- package/dist/views/TranslationHub/EditorRecentRunsPanel.d.ts +33 -0
- package/dist/views/TranslationHub/EditorRecentRunsPanel.js +290 -0
- package/dist/views/TranslationHub/Hub.client.d.ts +74 -0
- package/dist/views/TranslationHub/Hub.client.js +357 -0
- package/dist/views/TranslationHub/ModelCombobox.d.ts +14 -0
- package/dist/views/TranslationHub/ModelCombobox.js +415 -0
- package/dist/views/TranslationHub/PerCollectionConfig.d.ts +10 -0
- package/dist/views/TranslationHub/PerCollectionConfig.helpers.d.ts +16 -0
- package/dist/views/TranslationHub/PerCollectionConfig.helpers.js +19 -0
- package/dist/views/TranslationHub/PerCollectionConfig.js +759 -0
- package/dist/views/TranslationHub/SettingsRail.d.ts +11 -0
- package/dist/views/TranslationHub/SettingsRail.js +382 -0
- package/dist/views/TranslationHub/StatusStrip.d.ts +6 -0
- package/dist/views/TranslationHub/StatusStrip.js +451 -0
- package/dist/views/TranslationHub/UsageTable.d.ts +6 -0
- package/dist/views/TranslationHub/UsageTable.helpers.d.ts +69 -0
- package/dist/views/TranslationHub/UsageTable.helpers.js +49 -0
- package/dist/views/TranslationHub/UsageTable.js +1240 -0
- package/dist/views/TranslationHub/alertGrouping.d.ts +70 -0
- package/dist/views/TranslationHub/alertGrouping.js +99 -0
- package/dist/views/TranslationHub/index.d.ts +20 -0
- package/dist/views/TranslationHub/index.js +109 -0
- package/dist/views/TranslationHub/tabNavigation.d.ts +53 -0
- package/dist/views/TranslationHub/tabNavigation.js +74 -0
- package/dist/views/TranslationHub/terminalBannerVisibility.d.ts +33 -0
- package/dist/views/TranslationHub/terminalBannerVisibility.js +124 -0
- package/dist/views/TranslationHub/useBulkTranslateActive.d.ts +49 -0
- package/dist/views/TranslationHub/useBulkTranslateActive.js +251 -0
- package/dist/views/TranslationHub/useFocusTrap.d.ts +6 -0
- package/dist/views/TranslationHub/useFocusTrap.js +81 -0
- package/dist/views/TranslationHub/useTranslationHubUsageSummary.d.ts +77 -0
- package/dist/views/TranslationHub/useTranslationHubUsageSummary.js +267 -0
- package/dist/views/shared/EditorError.d.ts +97 -0
- package/dist/views/shared/EditorError.js +205 -0
- package/dist/views/shared/ModelCell.d.ts +18 -0
- package/dist/views/shared/ModelCell.js +31 -0
- package/dist/views/shared/docHref.d.ts +16 -0
- package/dist/views/shared/docHref.js +26 -0
- package/dist/views/shared/fetch-error-body.d.ts +25 -0
- package/dist/views/shared/fetch-error-body.js +42 -0
- package/dist/views/shared/filterPillStyle.d.ts +35 -0
- package/dist/views/shared/filterPillStyle.js +40 -0
- package/dist/views/shared/format.d.ts +75 -0
- package/dist/views/shared/format.js +131 -0
- package/package.json +141 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Payload, TaskConfig } from 'payload';
|
|
2
|
+
import type { BulkTranslateConfig } from '../types.js';
|
|
3
|
+
export declare const BULK_TRANSLATE_JANITOR_SLUG = "bulk-translate-janitor";
|
|
4
|
+
/**
|
|
5
|
+
* Janitor task — sweeps stale `running` units back into `pending` so
|
|
6
|
+
* the cron picks them up again. Cron-driven (recommended cadence
|
|
7
|
+
* `*\/5 * * * *`), but the task is also idempotent for ad-hoc admin
|
|
8
|
+
* triggering.
|
|
9
|
+
*
|
|
10
|
+
* Threshold: `max(2 × p99-of-recent-LLM-latency, 10 min)` per Decision
|
|
11
|
+
* #27 + F-DA-JANITOR-P99. P99 is computed from the
|
|
12
|
+
* `translation-usage.durationMs` column over the last 1000 rows.
|
|
13
|
+
* Self-tuning — if real translations start taking 4 minutes the
|
|
14
|
+
* threshold drifts up automatically.
|
|
15
|
+
*
|
|
16
|
+
* The P99 read is cached for 5 minutes per process. Janitor's
|
|
17
|
+
* threshold can lag actual P99 by up to 5 min; acceptable for a
|
|
18
|
+
* stale-row reset task.
|
|
19
|
+
*/
|
|
20
|
+
export interface BulkJanitorOptions {
|
|
21
|
+
/** Slug override for the units collection. */
|
|
22
|
+
unitsCollectionSlug?: string;
|
|
23
|
+
/** Slug override for the batches collection. */
|
|
24
|
+
batchesCollectionSlug?: string;
|
|
25
|
+
/** Slug override for the usage collection used to compute P99. */
|
|
26
|
+
usageCollectionSlug?: string;
|
|
27
|
+
/** Batch lifecycle callbacks, forwarded to `maybeTransitionBatch`. */
|
|
28
|
+
callbacks?: Pick<BulkTranslateConfig, 'onBatchComplete' | 'onBatchFailed'>;
|
|
29
|
+
/**
|
|
30
|
+
* Floor for the staleness threshold. Even if recent P99 is fast,
|
|
31
|
+
* never reset rows newer than this. Default 10 * 60_000 (10 min).
|
|
32
|
+
*/
|
|
33
|
+
minThresholdMs?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Multiplier applied to recent P99 to compute the threshold.
|
|
36
|
+
* Default 2 — gives slow translations a full 2× headroom over
|
|
37
|
+
* historical P99 before the janitor steps in.
|
|
38
|
+
*/
|
|
39
|
+
p99Multiplier?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Maximum attempts before a stale `running` row is marked
|
|
42
|
+
* permanently failed (rather than re-queued). Default 3.
|
|
43
|
+
*/
|
|
44
|
+
maxAttempts?: number;
|
|
45
|
+
/**
|
|
46
|
+
* P99 cache TTL. Default 5 minutes. Per-process cache; multi-process
|
|
47
|
+
* deployments compute independently which is fine — the threshold
|
|
48
|
+
* change is gradual.
|
|
49
|
+
*/
|
|
50
|
+
p99CacheTtlMs?: number;
|
|
51
|
+
}
|
|
52
|
+
type JanitorTaskInput = Record<string, never>;
|
|
53
|
+
export declare function buildBulkTranslateJanitor(options?: BulkJanitorOptions): TaskConfig<{
|
|
54
|
+
input: JanitorTaskInput;
|
|
55
|
+
output: {
|
|
56
|
+
ok: true;
|
|
57
|
+
reset: number;
|
|
58
|
+
failed: number;
|
|
59
|
+
requeued: number;
|
|
60
|
+
thresholdMs: number;
|
|
61
|
+
};
|
|
62
|
+
}>;
|
|
63
|
+
export type JanitorSweepParams = {
|
|
64
|
+
payload: Payload;
|
|
65
|
+
unitsSlug: string;
|
|
66
|
+
usageSlug: string;
|
|
67
|
+
/** Slug of the batches collection — used to unstick finished batches. */
|
|
68
|
+
batchesSlug?: string;
|
|
69
|
+
minThresholdMs: number;
|
|
70
|
+
p99Multiplier: number;
|
|
71
|
+
maxAttempts: number;
|
|
72
|
+
p99CacheTtlMs: number;
|
|
73
|
+
/** Batch lifecycle callbacks, forwarded to `maybeTransitionBatch`. */
|
|
74
|
+
callbacks?: Pick<BulkTranslateConfig, 'onBatchComplete' | 'onBatchFailed'>;
|
|
75
|
+
/** Test override for the current time. */
|
|
76
|
+
now?: () => number;
|
|
77
|
+
};
|
|
78
|
+
export type JanitorSweepResult = {
|
|
79
|
+
reset: number;
|
|
80
|
+
failed: number;
|
|
81
|
+
requeued: number;
|
|
82
|
+
thresholdMs: number;
|
|
83
|
+
};
|
|
84
|
+
/** Reset for tests. */
|
|
85
|
+
export declare function resetJanitorP99Cache(payload?: Payload): void;
|
|
86
|
+
export declare function runJanitorSweep(params: JanitorSweepParams): Promise<JanitorSweepResult>;
|
|
87
|
+
export {};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG } from '../bulk-translate-batches-collection.js';
|
|
2
|
+
import { DEFAULT_BULK_TRANSLATE_UNITS_COLLECTION_SLUG } from '../bulk-translate-units-collection.js';
|
|
3
|
+
import { DEFAULT_USAGE_COLLECTION_SLUG } from '../defaults.js';
|
|
4
|
+
import { BULK_TRANSLATE_DOC_TASK_SLUG, maybeTransitionBatch } from './bulk-translate-doc-task.js';
|
|
5
|
+
export const BULK_TRANSLATE_JANITOR_SLUG = 'bulk-translate-janitor';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Task config
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
const taskInputSchema = [];
|
|
10
|
+
export function buildBulkTranslateJanitor(options = {}) {
|
|
11
|
+
const unitsSlug = options.unitsCollectionSlug ?? DEFAULT_BULK_TRANSLATE_UNITS_COLLECTION_SLUG;
|
|
12
|
+
const batchesSlug = options.batchesCollectionSlug ?? DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG;
|
|
13
|
+
const usageSlug = options.usageCollectionSlug ?? DEFAULT_USAGE_COLLECTION_SLUG;
|
|
14
|
+
const minThresholdMs = options.minThresholdMs ?? 10 * 60_000;
|
|
15
|
+
const p99Multiplier = options.p99Multiplier ?? 2;
|
|
16
|
+
const maxAttempts = options.maxAttempts ?? 3;
|
|
17
|
+
const p99CacheTtlMs = options.p99CacheTtlMs ?? 5 * 60_000;
|
|
18
|
+
return {
|
|
19
|
+
slug: BULK_TRANSLATE_JANITOR_SLUG,
|
|
20
|
+
label: 'Bulk translate — janitor',
|
|
21
|
+
inputSchema: taskInputSchema,
|
|
22
|
+
retries: 0,
|
|
23
|
+
handler: async ({ req })=>{
|
|
24
|
+
const result = await runJanitorSweep({
|
|
25
|
+
payload: req.payload,
|
|
26
|
+
unitsSlug,
|
|
27
|
+
batchesSlug,
|
|
28
|
+
usageSlug,
|
|
29
|
+
minThresholdMs,
|
|
30
|
+
p99Multiplier,
|
|
31
|
+
maxAttempts,
|
|
32
|
+
p99CacheTtlMs,
|
|
33
|
+
callbacks: options.callbacks
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
output: {
|
|
37
|
+
ok: true,
|
|
38
|
+
...result
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Module-level P99 cache. WeakMap keyed by the Payload instance so a
|
|
46
|
+
* test that creates two payload mocks doesn't see cache pollution.
|
|
47
|
+
*/ const P99_CACHE = new WeakMap();
|
|
48
|
+
/** Reset for tests. */ export function resetJanitorP99Cache(payload) {
|
|
49
|
+
if (payload) {
|
|
50
|
+
P99_CACHE.delete(payload);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// WeakMap has no .clear() — tests can pass a payload reference to
|
|
54
|
+
// evict its entry. Skipping payload deletes nothing; callers wanting
|
|
55
|
+
// a fresh cache for a brand-new mock just rebuild the mock.
|
|
56
|
+
}
|
|
57
|
+
export async function runJanitorSweep(params) {
|
|
58
|
+
const { payload, unitsSlug, usageSlug, minThresholdMs, p99Multiplier, maxAttempts, p99CacheTtlMs } = params;
|
|
59
|
+
const now = params.now ?? (()=>Date.now());
|
|
60
|
+
const p99Ms = await readP99Cached(payload, usageSlug, now, p99CacheTtlMs);
|
|
61
|
+
const thresholdMs = Math.max(minThresholdMs, p99Multiplier * p99Ms);
|
|
62
|
+
const cutoffIso = new Date(now() - thresholdMs).toISOString();
|
|
63
|
+
const result = await payload.find({
|
|
64
|
+
collection: unitsSlug,
|
|
65
|
+
where: {
|
|
66
|
+
and: [
|
|
67
|
+
{
|
|
68
|
+
status: {
|
|
69
|
+
equals: 'running'
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
startedAt: {
|
|
74
|
+
less_than: cutoffIso
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
// Bound the per-tick work. 200 stale rows per sweep is plenty —
|
|
80
|
+
// the next cron tick picks up anything left.
|
|
81
|
+
limit: 200,
|
|
82
|
+
depth: 0,
|
|
83
|
+
overrideAccess: true
|
|
84
|
+
});
|
|
85
|
+
const stale = result.docs;
|
|
86
|
+
const batchesSlug = params.batchesSlug ?? DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG;
|
|
87
|
+
const affectedBatchIds = new Set();
|
|
88
|
+
const noteBatch = (u)=>{
|
|
89
|
+
const raw = typeof u.batchId === 'object' && u.batchId !== null ? u.batchId.id : u.batchId;
|
|
90
|
+
if (raw !== undefined && raw !== null && String(raw).length > 0) {
|
|
91
|
+
affectedBatchIds.add(String(raw));
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
// A unit's worker job is queued exactly once (at enumeration, or by the
|
|
95
|
+
// worker's own deferral re-queue). The worker task has `retries: 0`, and
|
|
96
|
+
// the coordinator never re-runs after enumeration finishes — so a reset
|
|
97
|
+
// WITHOUT a replacement job leaves the unit `pending` forever and its
|
|
98
|
+
// batch stuck in `running`/`cancelling` for eternity. Every reset must
|
|
99
|
+
// therefore queue a fresh job. Cancelled batches are safe to re-queue
|
|
100
|
+
// into: the worker's cancel gate marks their units `skipped` on entry.
|
|
101
|
+
const requeueUnit = async (unitId)=>{
|
|
102
|
+
await payload.jobs.queue({
|
|
103
|
+
task: BULK_TRANSLATE_DOC_TASK_SLUG,
|
|
104
|
+
input: {
|
|
105
|
+
unitId: String(unitId)
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
let reset = 0;
|
|
110
|
+
let failed = 0;
|
|
111
|
+
for (const u of stale){
|
|
112
|
+
const attempts = u.attempts ?? 0;
|
|
113
|
+
try {
|
|
114
|
+
if (attempts < maxAttempts) {
|
|
115
|
+
await payload.update({
|
|
116
|
+
collection: unitsSlug,
|
|
117
|
+
id: u.id,
|
|
118
|
+
data: {
|
|
119
|
+
status: 'pending',
|
|
120
|
+
failureCode: 'transient.crashed',
|
|
121
|
+
failureMessage: `janitor reset: running > ${Math.round(thresholdMs / 1000)}s`,
|
|
122
|
+
// Cleared so the orphan-recovery pass below can tell "fresh
|
|
123
|
+
// pending awaiting its queued job" (startedAt null) apart from
|
|
124
|
+
// "previously claimed, job chain dead" (startedAt set).
|
|
125
|
+
startedAt: null
|
|
126
|
+
},
|
|
127
|
+
overrideAccess: true
|
|
128
|
+
});
|
|
129
|
+
await requeueUnit(u.id);
|
|
130
|
+
reset += 1;
|
|
131
|
+
} else {
|
|
132
|
+
await payload.update({
|
|
133
|
+
collection: unitsSlug,
|
|
134
|
+
id: u.id,
|
|
135
|
+
data: {
|
|
136
|
+
status: 'failed',
|
|
137
|
+
failureCode: 'transient.crashed',
|
|
138
|
+
failureMessage: `janitor reset: exceeded ${maxAttempts} attempts`,
|
|
139
|
+
completedAt: new Date(now()).toISOString()
|
|
140
|
+
},
|
|
141
|
+
overrideAccess: true
|
|
142
|
+
});
|
|
143
|
+
failed += 1;
|
|
144
|
+
}
|
|
145
|
+
noteBatch(u);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
payload.logger?.warn?.(`[ai-translate] janitor: failed to update unit ${u.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// ----- Orphan-recovery pass -----------------------------------------
|
|
151
|
+
// Units whose job chain is dead and that nothing will ever pick up:
|
|
152
|
+
// (a) `pending` with a stale `startedAt` (janitor-reset rows that
|
|
153
|
+
// kept their last claim's timestamp), and
|
|
154
|
+
// (b) `pending` with NO `startedAt` but a stale `updatedAt` — a
|
|
155
|
+
// previous recovery cleared the marker and queued a job that
|
|
156
|
+
// died before claiming (or the queue write itself failed). The
|
|
157
|
+
// original one-shot predicate matched only (a), so a single
|
|
158
|
+
// failed recovery made the unit permanently invisible
|
|
159
|
+
// (observed: 4 units stuck `pending` >25 min on blog-wild prod,
|
|
160
|
+
// 2026-06-11). `updatedAt` refreshes on every touch, so retries
|
|
161
|
+
// self-pace at one attempt per threshold window.
|
|
162
|
+
// Re-queue FIRST, then clear the marker — if the queue write throws,
|
|
163
|
+
// the unit stays matchable by branch (a) instead of burning its
|
|
164
|
+
// marker. Duplicate jobs are harmless either way — the worker's
|
|
165
|
+
// atomic claim turns a second fire into a no-op.
|
|
166
|
+
let requeued = 0;
|
|
167
|
+
try {
|
|
168
|
+
const orphans = await payload.find({
|
|
169
|
+
collection: unitsSlug,
|
|
170
|
+
where: {
|
|
171
|
+
and: [
|
|
172
|
+
{
|
|
173
|
+
status: {
|
|
174
|
+
equals: 'pending'
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
or: [
|
|
179
|
+
{
|
|
180
|
+
startedAt: {
|
|
181
|
+
less_than: cutoffIso
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
and: [
|
|
186
|
+
{
|
|
187
|
+
startedAt: {
|
|
188
|
+
exists: false
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
updatedAt: {
|
|
193
|
+
less_than: cutoffIso
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
},
|
|
202
|
+
limit: 200,
|
|
203
|
+
depth: 0,
|
|
204
|
+
overrideAccess: true
|
|
205
|
+
});
|
|
206
|
+
for (const u of orphans.docs){
|
|
207
|
+
try {
|
|
208
|
+
await requeueUnit(u.id);
|
|
209
|
+
await payload.update({
|
|
210
|
+
collection: unitsSlug,
|
|
211
|
+
id: u.id,
|
|
212
|
+
data: {
|
|
213
|
+
startedAt: null
|
|
214
|
+
},
|
|
215
|
+
overrideAccess: true
|
|
216
|
+
});
|
|
217
|
+
requeued += 1;
|
|
218
|
+
noteBatch(u);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
payload.logger?.warn?.(`[ai-translate] janitor: orphan re-queue failed for unit ${u.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
payload.logger?.warn?.(`[ai-translate] janitor: orphan-recovery pass failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
225
|
+
}
|
|
226
|
+
// ----- Stuck-batch unsticker -----------------------------------------
|
|
227
|
+
// `maybeTransitionBatch` only ever ran from a finishing worker, so a
|
|
228
|
+
// batch whose last open unit was resolved by anything else (cancel
|
|
229
|
+
// sweep races, janitor `failed` transitions above, crashed workers)
|
|
230
|
+
// stayed `running`/`cancelling` forever. Re-evaluate every batch this
|
|
231
|
+
// sweep touched — plus every non-terminal batch, so batches that were
|
|
232
|
+
// already stuck before this sweep (all units terminal, transition never
|
|
233
|
+
// fired) get unstuck even when the unit passes above found nothing.
|
|
234
|
+
try {
|
|
235
|
+
const open = await payload.find({
|
|
236
|
+
collection: batchesSlug,
|
|
237
|
+
where: {
|
|
238
|
+
status: {
|
|
239
|
+
in: [
|
|
240
|
+
'running',
|
|
241
|
+
'cancelling'
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
limit: 20,
|
|
246
|
+
depth: 0,
|
|
247
|
+
overrideAccess: true
|
|
248
|
+
});
|
|
249
|
+
for (const b of open.docs){
|
|
250
|
+
affectedBatchIds.add(String(b.id));
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
payload.logger?.warn?.(`[ai-translate] janitor: open-batch scan failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
254
|
+
}
|
|
255
|
+
for (const batchId of affectedBatchIds){
|
|
256
|
+
try {
|
|
257
|
+
await maybeTransitionBatch(payload, batchesSlug, unitsSlug, batchId, params.callbacks);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
payload.logger?.warn?.(`[ai-translate] janitor: batch transition check failed for ${batchId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
reset,
|
|
264
|
+
failed,
|
|
265
|
+
requeued,
|
|
266
|
+
thresholdMs
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// P99 read
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
async function readP99Cached(payload, usageSlug, now, ttlMs) {
|
|
273
|
+
const cached = P99_CACHE.get(payload);
|
|
274
|
+
if (cached && now() - cached.computedAt < ttlMs) {
|
|
275
|
+
return cached.p99Ms;
|
|
276
|
+
}
|
|
277
|
+
const p99Ms = await computeRecentP99(payload, usageSlug);
|
|
278
|
+
P99_CACHE.set(payload, {
|
|
279
|
+
p99Ms,
|
|
280
|
+
computedAt: now()
|
|
281
|
+
});
|
|
282
|
+
return p99Ms;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Read the most recent 1000 `translation-usage` rows and return the
|
|
286
|
+
* P99 of their `durationMs` values. Falls back to 0 (which lets the
|
|
287
|
+
* threshold floor dominate) when no data is available — fresh
|
|
288
|
+
* installs and test fixtures.
|
|
289
|
+
*/ async function computeRecentP99(payload, usageSlug) {
|
|
290
|
+
try {
|
|
291
|
+
const result = await payload.find({
|
|
292
|
+
collection: usageSlug,
|
|
293
|
+
where: {
|
|
294
|
+
durationMs: {
|
|
295
|
+
exists: true
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
limit: 1000,
|
|
299
|
+
sort: '-createdAt',
|
|
300
|
+
depth: 0,
|
|
301
|
+
overrideAccess: true
|
|
302
|
+
});
|
|
303
|
+
const rows = result.docs;
|
|
304
|
+
const durations = rows.map((r)=>r.durationMs ?? 0).filter((n)=>typeof n === 'number' && n > 0).sort((a, b)=>a - b);
|
|
305
|
+
if (durations.length === 0) return 0;
|
|
306
|
+
const idx = Math.min(durations.length - 1, Math.floor(durations.length * 0.99));
|
|
307
|
+
return durations[idx];
|
|
308
|
+
} catch {
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { TaskConfig } from 'payload';
|
|
2
|
+
import type { TargetPolicy } from '../types.js';
|
|
3
|
+
export declare const TRANSLATE_DOCUMENT_TASK_SLUG = "ai-translate-document";
|
|
4
|
+
export declare const TRANSLATE_GLOBAL_TASK_SLUG = "ai-translate-global";
|
|
5
|
+
type TranslateDocumentTaskInput = {
|
|
6
|
+
collection: string;
|
|
7
|
+
documentId: string;
|
|
8
|
+
jobId?: string;
|
|
9
|
+
targetLocales?: string[] | null;
|
|
10
|
+
previousDoc?: Record<string, unknown> | null;
|
|
11
|
+
draft?: boolean;
|
|
12
|
+
targetPolicy?: TargetPolicy;
|
|
13
|
+
};
|
|
14
|
+
type TranslateGlobalTaskInput = {
|
|
15
|
+
global: string;
|
|
16
|
+
jobId?: string;
|
|
17
|
+
targetLocales?: string[] | null;
|
|
18
|
+
previousDoc?: Record<string, unknown> | null;
|
|
19
|
+
targetPolicy?: TargetPolicy;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Builds the Payload task config for translating a collection
|
|
23
|
+
* document. Consumers don't see this — the plugin auto-registers it
|
|
24
|
+
* when `persistJobs: true`.
|
|
25
|
+
*
|
|
26
|
+
* `allowedCollections` is the plugin's configured `collections` list.
|
|
27
|
+
* Task inputs are queryable via Payload's REST API, so without this
|
|
28
|
+
* allowlist an attacker with write access to `payload-jobs` could
|
|
29
|
+
* enqueue a task targeting any collection (e.g. `users`) and the
|
|
30
|
+
* worker would execute it with `overrideAccess: true`. Empty list
|
|
31
|
+
* disables the check for back-compat with consumers wiring tasks
|
|
32
|
+
* manually.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildTranslateDocumentTask(allowedCollections?: readonly string[]): TaskConfig<{
|
|
35
|
+
input: TranslateDocumentTaskInput;
|
|
36
|
+
output: {
|
|
37
|
+
ok: true;
|
|
38
|
+
};
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* Sibling task config for translating a global. Same persistence
|
|
42
|
+
* contract — survives server restart. Same allowlist rationale as
|
|
43
|
+
* `buildTranslateDocumentTask`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildTranslateGlobalTask(allowedGlobals?: readonly string[]): TaskConfig<{
|
|
46
|
+
input: TranslateGlobalTaskInput;
|
|
47
|
+
output: {
|
|
48
|
+
ok: true;
|
|
49
|
+
};
|
|
50
|
+
}>;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { translateDocument, translateGlobal } from '../api.js';
|
|
2
|
+
export const TRANSLATE_DOCUMENT_TASK_SLUG = 'ai-translate-document';
|
|
3
|
+
export const TRANSLATE_GLOBAL_TASK_SLUG = 'ai-translate-global';
|
|
4
|
+
/**
|
|
5
|
+
* Payload jobs task that runs `translateDocument` from a durable queue
|
|
6
|
+
* entry rather than the in-memory coalescing closure. Surviving a
|
|
7
|
+
* server restart works because the task's input lives in the
|
|
8
|
+
* `payload-jobs` collection — when the process restarts, Payload's
|
|
9
|
+
* cron picks up un-completed jobs and re-runs the handler.
|
|
10
|
+
*
|
|
11
|
+
* The original after-change closure captured `req`, `doc`, and
|
|
12
|
+
* `previousDoc` directly. We can't serialize those across a restart,
|
|
13
|
+
* so the task input carries (collection, documentId, previousDoc-as-
|
|
14
|
+
* JSON, targetLocales, jobId, draft). The handler reads the live doc
|
|
15
|
+
* via the plugin's existing `findByIdNoFallback` inside
|
|
16
|
+
* `translateDocument`, so it always operates on current state.
|
|
17
|
+
*/ const taskInputSchema = [
|
|
18
|
+
{
|
|
19
|
+
name: 'collection',
|
|
20
|
+
type: 'text',
|
|
21
|
+
required: true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'documentId',
|
|
25
|
+
type: 'text',
|
|
26
|
+
required: true
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'jobId',
|
|
30
|
+
type: 'text'
|
|
31
|
+
},
|
|
32
|
+
/**
|
|
33
|
+
* Serialized as JSON so it round-trips through the jobs collection
|
|
34
|
+
* cleanly. May be `null` when the after-change hook detected a
|
|
35
|
+
* publish-transition and asked for a full re-translate.
|
|
36
|
+
*/ {
|
|
37
|
+
name: 'targetLocales',
|
|
38
|
+
type: 'json'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'previousDoc',
|
|
42
|
+
type: 'json'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'draft',
|
|
46
|
+
type: 'checkbox',
|
|
47
|
+
defaultValue: true
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'targetPolicy',
|
|
51
|
+
type: 'text'
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
const taskInputSchemaGlobal = [
|
|
55
|
+
{
|
|
56
|
+
name: 'global',
|
|
57
|
+
type: 'text',
|
|
58
|
+
required: true
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'jobId',
|
|
62
|
+
type: 'text'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'targetLocales',
|
|
66
|
+
type: 'json'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'previousDoc',
|
|
70
|
+
type: 'json'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'targetPolicy',
|
|
74
|
+
type: 'text'
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
/**
|
|
78
|
+
* Builds the Payload task config for translating a collection
|
|
79
|
+
* document. Consumers don't see this — the plugin auto-registers it
|
|
80
|
+
* when `persistJobs: true`.
|
|
81
|
+
*
|
|
82
|
+
* `allowedCollections` is the plugin's configured `collections` list.
|
|
83
|
+
* Task inputs are queryable via Payload's REST API, so without this
|
|
84
|
+
* allowlist an attacker with write access to `payload-jobs` could
|
|
85
|
+
* enqueue a task targeting any collection (e.g. `users`) and the
|
|
86
|
+
* worker would execute it with `overrideAccess: true`. Empty list
|
|
87
|
+
* disables the check for back-compat with consumers wiring tasks
|
|
88
|
+
* manually.
|
|
89
|
+
*/ export function buildTranslateDocumentTask(allowedCollections = []) {
|
|
90
|
+
const allowed = new Set(allowedCollections);
|
|
91
|
+
return {
|
|
92
|
+
slug: TRANSLATE_DOCUMENT_TASK_SLUG,
|
|
93
|
+
label: 'AI translate — collection document',
|
|
94
|
+
inputSchema: taskInputSchema,
|
|
95
|
+
// No retries: the per-locale work inside translateDocument already
|
|
96
|
+
// applies the configured `retry` config to provider calls. A second
|
|
97
|
+
// outer attempt would double-bill on transient errors that already
|
|
98
|
+
// got their inner retries.
|
|
99
|
+
retries: 0,
|
|
100
|
+
handler: async ({ input, req })=>{
|
|
101
|
+
const typed = input;
|
|
102
|
+
if (allowed.size > 0 && !allowed.has(typed.collection)) {
|
|
103
|
+
throw new Error(`[ai-translate] Task input rejected: collection '${typed.collection}' is not in the plugin's allowed collections list.`);
|
|
104
|
+
}
|
|
105
|
+
await translateDocument(req.payload, {
|
|
106
|
+
collection: typed.collection,
|
|
107
|
+
id: typed.documentId,
|
|
108
|
+
jobId: typed.jobId,
|
|
109
|
+
targetLocales: typed.targetLocales ?? undefined,
|
|
110
|
+
previousDoc: typed.previousDoc ?? undefined,
|
|
111
|
+
draft: typed.draft ?? true,
|
|
112
|
+
targetPolicy: typed.targetPolicy,
|
|
113
|
+
req
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
output: {
|
|
117
|
+
ok: true
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Sibling task config for translating a global. Same persistence
|
|
125
|
+
* contract — survives server restart. Same allowlist rationale as
|
|
126
|
+
* `buildTranslateDocumentTask`.
|
|
127
|
+
*/ export function buildTranslateGlobalTask(allowedGlobals = []) {
|
|
128
|
+
const allowed = new Set(allowedGlobals);
|
|
129
|
+
return {
|
|
130
|
+
slug: TRANSLATE_GLOBAL_TASK_SLUG,
|
|
131
|
+
label: 'AI translate — global',
|
|
132
|
+
inputSchema: taskInputSchemaGlobal,
|
|
133
|
+
retries: 0,
|
|
134
|
+
handler: async ({ input, req })=>{
|
|
135
|
+
const typed = input;
|
|
136
|
+
if (allowed.size > 0 && !allowed.has(typed.global)) {
|
|
137
|
+
throw new Error(`[ai-translate] Task input rejected: global '${typed.global}' is not in the plugin's allowed globals list.`);
|
|
138
|
+
}
|
|
139
|
+
await translateGlobal(req.payload, {
|
|
140
|
+
global: typed.global,
|
|
141
|
+
jobId: typed.jobId,
|
|
142
|
+
targetLocales: typed.targetLocales ?? undefined,
|
|
143
|
+
previousDoc: typed.previousDoc ?? undefined,
|
|
144
|
+
targetPolicy: typed.targetPolicy,
|
|
145
|
+
req
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
output: {
|
|
149
|
+
ok: true
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|