@consilioweb/payload-support 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +656 -355
- package/dist/components/RichTextEditor/index.d.ts +19 -0
- package/dist/components/TicketConversation/RewriteDropdown.d.ts +6 -0
- package/dist/components/TicketConversation/SkeletonText.d.ts +4 -0
- package/dist/components/TicketConversation/components/AISummaryPanel.d.ts +14 -0
- package/dist/components/TicketConversation/components/ActionPanels.d.ts +47 -0
- package/dist/components/TicketConversation/components/ActivityLog.d.ts +5 -0
- package/dist/components/TicketConversation/components/ClientBar.d.ts +5 -0
- package/dist/components/TicketConversation/components/ClientHistory.d.ts +24 -0
- package/dist/components/TicketConversation/components/CodeBlock.d.ts +26 -0
- package/dist/components/TicketConversation/components/CodeBlockInserter.d.ts +12 -0
- package/dist/components/TicketConversation/components/QuickActions.d.ts +32 -0
- package/dist/components/TicketConversation/components/TicketHeader.d.ts +19 -0
- package/dist/components/TicketConversation/components/TimeTrackingPanel.d.ts +24 -0
- package/dist/components/TicketConversation/config.d.ts +10 -0
- package/dist/components/TicketConversation/config.js +1 -40
- package/dist/components/TicketConversation/constants.d.ts +56 -0
- package/dist/components/TicketConversation/context.d.ts +53 -0
- package/dist/components/TicketConversation/hooks/useAI.d.ts +21 -0
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.d.ts +3 -0
- package/dist/components/TicketConversation/hooks/useFeatures.d.ts +23 -0
- package/dist/components/TicketConversation/hooks/useFeatures.js +61 -0
- package/dist/components/TicketConversation/hooks/useMessageActions.d.ts +20 -0
- package/dist/components/TicketConversation/hooks/useReply.d.ts +27 -0
- package/dist/components/TicketConversation/hooks/useTicketActions.d.ts +58 -0
- package/dist/components/TicketConversation/hooks/useTimeTracking.d.ts +18 -0
- package/dist/components/TicketConversation/hooks/useTranslation.d.ts +33 -0
- package/dist/components/TicketConversation/hooks/useTranslation.js +22 -3
- package/dist/components/TicketConversation/index.d.ts +4 -0
- package/dist/components/TicketConversation/index.js +2 -2
- package/dist/components/TicketConversation/types.d.ts +56 -0
- package/dist/components/TicketConversation/utils.d.ts +2 -0
- package/dist/index.cjs +392 -320
- package/dist/index.d.cts +100 -3
- package/dist/index.d.ts +100 -3
- package/dist/index.js +388 -321
- package/dist/utils/features.d.ts +123 -0
- package/dist/utils/features.js +147 -0
- package/dist/views/BillingView/client.d.ts +2 -0
- package/dist/views/BillingView/index.d.ts +4 -0
- package/dist/views/ChatView/client.d.ts +2 -0
- package/dist/views/ChatView/client.js +9 -9
- package/dist/views/ChatView/index.d.ts +4 -0
- package/dist/views/CrmView/client.d.ts +2 -0
- package/dist/views/CrmView/client.js +6 -9
- package/dist/views/CrmView/index.d.ts +4 -0
- package/dist/views/EmailTrackingView/client.d.ts +2 -0
- package/dist/views/EmailTrackingView/index.d.ts +4 -0
- package/dist/views/ImportConversationView/client.d.ts +2 -0
- package/dist/views/ImportConversationView/index.d.ts +4 -0
- package/dist/views/LogsView/client.d.ts +2 -0
- package/dist/views/LogsView/index.d.ts +4 -0
- package/dist/views/NewTicketView/client.d.ts +2 -0
- package/dist/views/NewTicketView/index.d.ts +4 -0
- package/dist/views/PendingEmailsView/client.d.ts +2 -0
- package/dist/views/PendingEmailsView/index.d.ts +4 -0
- package/dist/views/SupportDashboardView/client.d.ts +2 -0
- package/dist/views/SupportDashboardView/client.js +1 -1
- package/dist/views/SupportDashboardView/index.d.ts +4 -0
- package/dist/views/TicketDetailView/NextActionItem.d.ts +6 -0
- package/dist/views/TicketDetailView/RewriteDropdown.d.ts +7 -0
- package/dist/views/TicketDetailView/client.d.ts +2 -0
- package/dist/views/TicketDetailView/client.js +2 -2
- package/dist/views/TicketDetailView/constants.d.ts +13 -0
- package/dist/views/TicketDetailView/helpers.d.ts +2 -0
- package/dist/views/TicketDetailView/index.d.ts +4 -0
- package/dist/views/TicketDetailView/types.d.ts +40 -0
- package/dist/views/TicketInboxView/client.d.ts +2 -0
- package/dist/views/TicketInboxView/client.js +5 -5
- package/dist/views/TicketInboxView/index.d.ts +4 -0
- package/dist/views/TicketingSettingsView/client.d.ts +2 -0
- package/dist/views/TicketingSettingsView/client.js +6 -5
- package/dist/views/TicketingSettingsView/index.d.ts +4 -0
- package/dist/views/TimeDashboardView/client.d.ts +2 -0
- package/dist/views/TimeDashboardView/index.d.ts +4 -0
- package/dist/views/shared/AdminViewHeader.d.ts +13 -0
- package/dist/views/shared/ErrorBoundary.d.ts +17 -0
- package/dist/views/shared/Skeleton.d.ts +21 -0
- package/dist/views/shared/StatusPill.d.ts +8 -0
- package/dist/views/shared/adminTokens.d.ts +19 -0
- package/dist/views/shared/config.d.ts +8 -0
- package/dist/views/shared/config.js +1 -40
- package/dist/views/shared/dateLocale.d.ts +5 -0
- package/dist/views/shared/index.d.ts +12 -0
- package/dist/views/shared/index.js +2 -1
- package/dist/views/shared/locales/en.json +708 -0
- package/dist/views/shared/locales/fr.json +708 -0
- package/dist/views/shared/sla.d.ts +14 -0
- package/dist/views.d.ts +13 -13
- package/package.json +26 -41
- package/src/collections/SupportClients.ts +29 -0
- package/src/collections/TicketCollaborators.ts +6 -1
- package/src/collections/TicketMessages.ts +46 -24
- package/src/collections/Tickets.ts +117 -79
- package/src/collections/TimeEntries.ts +57 -32
- package/src/components/TicketConversation/config.ts +16 -80
- package/src/components/TicketConversation/hooks/useFeatures.ts +109 -0
- package/src/components/TicketConversation/hooks/useTranslation.ts +38 -4
- package/src/components/TicketConversation/index.tsx +2 -2
- package/src/endpoints/ai.ts +2 -1
- package/src/endpoints/client-intelligence.ts +2 -1
- package/src/endpoints/process-scheduled.ts +13 -8
- package/src/endpoints/round-robin-config.ts +30 -20
- package/src/endpoints/settings.ts +28 -77
- package/src/index.ts +6 -2
- package/src/portal/LiveChat.tsx +33 -22
- package/src/portal/auth/tickets/detail/TicketPolling.tsx +5 -1
- package/src/types/lucide-react.d.ts +10 -2
- package/src/utils/aiAgent.ts +2 -1
- package/src/utils/aiProvider.ts +21 -0
- package/src/utils/features.ts +297 -0
- package/src/utils/fireWebhooks.ts +6 -0
- package/src/utils/generateTicketSynthesis.ts +2 -1
- package/src/utils/index.ts +4 -2
- package/src/utils/readSettings.ts +82 -9
- package/src/utils/webhookDispatcher.ts +19 -7
- package/src/views/ChatView/client.tsx +13 -9
- package/src/views/CrmView/client.tsx +10 -11
- package/src/views/SupportDashboardView/client.tsx +1 -1
- package/src/views/TicketDetailView/client.tsx +2 -2
- package/src/views/TicketInboxView/client.tsx +10 -5
- package/src/views/TicketingSettingsView/client.tsx +9 -7
- package/src/views/shared/config.ts +14 -80
- package/src/views/shared/index.ts +3 -1
- package/src/views/shared/locales/en.json +2 -2
- package/src/views/shared/locales/fr.json +2 -2
- package/dist/components/RichTextEditor/index.cjs +0 -279
- package/dist/components/TicketConversation/RewriteDropdown.cjs +0 -98
- package/dist/components/TicketConversation/SkeletonText.cjs +0 -21
- package/dist/components/TicketConversation/components/AISummaryPanel.cjs +0 -85
- package/dist/components/TicketConversation/components/ActionPanels.cjs +0 -193
- package/dist/components/TicketConversation/components/ActivityLog.cjs +0 -24
- package/dist/components/TicketConversation/components/ClientBar.cjs +0 -43
- package/dist/components/TicketConversation/components/ClientHistory.cjs +0 -138
- package/dist/components/TicketConversation/components/CodeBlock.cjs +0 -171
- package/dist/components/TicketConversation/components/CodeBlockInserter.cjs +0 -156
- package/dist/components/TicketConversation/components/QuickActions.cjs +0 -87
- package/dist/components/TicketConversation/components/TicketHeader.cjs +0 -93
- package/dist/components/TicketConversation/components/TimeTrackingPanel.cjs +0 -134
- package/dist/components/TicketConversation/config.cjs +0 -44
- package/dist/components/TicketConversation/constants.cjs +0 -127
- package/dist/components/TicketConversation/context.cjs +0 -13
- package/dist/components/TicketConversation/hooks/useAI.cjs +0 -196
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +0 -20
- package/dist/components/TicketConversation/hooks/useMessageActions.cjs +0 -136
- package/dist/components/TicketConversation/hooks/useReply.cjs +0 -188
- package/dist/components/TicketConversation/hooks/useTicketActions.cjs +0 -290
- package/dist/components/TicketConversation/hooks/useTimeTracking.cjs +0 -123
- package/dist/components/TicketConversation/hooks/useTranslation.cjs +0 -78
- package/dist/components/TicketConversation/index.cjs +0 -1114
- package/dist/components/TicketConversation/types.cjs +0 -2
- package/dist/components/TicketConversation/utils.cjs +0 -27
- package/dist/views/BillingView/client.cjs +0 -469
- package/dist/views/BillingView/index.cjs +0 -34
- package/dist/views/ChatView/client.cjs +0 -301
- package/dist/views/ChatView/index.cjs +0 -34
- package/dist/views/CrmView/client.cjs +0 -458
- package/dist/views/CrmView/index.cjs +0 -34
- package/dist/views/EmailTrackingView/client.cjs +0 -171
- package/dist/views/EmailTrackingView/index.cjs +0 -34
- package/dist/views/ImportConversationView/client.cjs +0 -238
- package/dist/views/ImportConversationView/index.cjs +0 -34
- package/dist/views/LogsView/client.cjs +0 -148
- package/dist/views/LogsView/index.cjs +0 -32
- package/dist/views/NewTicketView/client.cjs +0 -213
- package/dist/views/NewTicketView/index.cjs +0 -32
- package/dist/views/PendingEmailsView/client.cjs +0 -471
- package/dist/views/PendingEmailsView/index.cjs +0 -34
- package/dist/views/SupportDashboardView/client.cjs +0 -437
- package/dist/views/SupportDashboardView/index.cjs +0 -34
- package/dist/views/TicketDetailView/NextActionItem.cjs +0 -40
- package/dist/views/TicketDetailView/RewriteDropdown.cjs +0 -80
- package/dist/views/TicketDetailView/client.cjs +0 -1412
- package/dist/views/TicketDetailView/constants.cjs +0 -20
- package/dist/views/TicketDetailView/helpers.cjs +0 -23
- package/dist/views/TicketDetailView/index.cjs +0 -34
- package/dist/views/TicketDetailView/types.cjs +0 -2
- package/dist/views/TicketInboxView/client.cjs +0 -359
- package/dist/views/TicketInboxView/index.cjs +0 -32
- package/dist/views/TicketingSettingsView/client.cjs +0 -740
- package/dist/views/TicketingSettingsView/index.cjs +0 -34
- package/dist/views/TimeDashboardView/client.cjs +0 -180
- package/dist/views/TimeDashboardView/index.cjs +0 -34
- package/dist/views/shared/AdminViewHeader.cjs +0 -68
- package/dist/views/shared/ErrorBoundary.cjs +0 -55
- package/dist/views/shared/Skeleton.cjs +0 -77
- package/dist/views/shared/StatusPill.cjs +0 -47
- package/dist/views/shared/adminTokens.cjs +0 -34
- package/dist/views/shared/config.cjs +0 -44
- package/dist/views/shared/dateLocale.cjs +0 -5
- package/dist/views/shared/index.cjs +0 -72
- package/dist/views/shared/sla.cjs +0 -37
- package/dist/views.cjs +0 -86
- package/dist/views.d.cts +0 -13
- package/src/__tests__/aiSummaryRendering.test.ts +0 -21
- package/src/__tests__/authResponses.test.ts +0 -69
- package/src/__tests__/capabilities.test.ts +0 -71
- package/src/__tests__/generateTrackingToken.test.ts +0 -67
- package/src/__tests__/integration/ai-agent.test.ts +0 -46
- package/src/__tests__/integration/auth.test.ts +0 -43
- package/src/__tests__/integration/automation-rules.test.ts +0 -68
- package/src/__tests__/integration/buildTestPayload.ts +0 -52
- package/src/__tests__/integration/channels.test.ts +0 -52
- package/src/__tests__/integration/core-behaviors.test.ts +0 -81
- package/src/__tests__/integration/dashboard-volume.test.ts +0 -31
- package/src/__tests__/integration/digest.test.ts +0 -48
- package/src/__tests__/integration/invoice-pdf.test.ts +0 -30
- package/src/__tests__/integration/isolation.test.ts +0 -81
- package/src/__tests__/integration/mentions-invoice.test.ts +0 -50
- package/src/__tests__/integration/nps.test.ts +0 -37
- package/src/__tests__/integration/per-team-sla.test.ts +0 -44
- package/src/__tests__/integration/push.test.ts +0 -75
- package/src/__tests__/integration/sanitization.test.ts +0 -35
- package/src/__tests__/integration/sla-pause.test.ts +0 -39
- package/src/__tests__/integration/smoke.test.ts +0 -26
- package/src/__tests__/integration/snooze.test.ts +0 -68
- package/src/__tests__/integration/teams.test.ts +0 -38
- package/src/__tests__/rateLimiter.test.ts +0 -83
- package/src/__tests__/sanitizeHtml.test.ts +0 -165
- package/src/__tests__/sla.test.ts +0 -258
- package/src/__tests__/trackOpenEndpoint.test.ts +0 -54
- package/src/__tests__/webhookSecurity.test.ts +0 -58
|
@@ -1,59 +1,21 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
fromName: string
|
|
13
|
-
replyToAddress: string
|
|
14
|
-
}
|
|
15
|
-
ai: {
|
|
16
|
-
provider: 'anthropic' | 'openai' | 'gemini' | 'ollama'
|
|
17
|
-
model: string
|
|
18
|
-
enableSentiment: boolean
|
|
19
|
-
enableSynthesis: boolean
|
|
20
|
-
enableSuggestion: boolean
|
|
21
|
-
enableRewrite: boolean
|
|
22
|
-
}
|
|
23
|
-
sla: {
|
|
24
|
-
firstResponseMinutes: number
|
|
25
|
-
resolutionMinutes: number
|
|
26
|
-
businessHoursOnly: boolean
|
|
27
|
-
escalationEmail: string
|
|
28
|
-
}
|
|
29
|
-
autoClose: {
|
|
30
|
-
enabled: boolean
|
|
31
|
-
daysBeforeClose: number
|
|
32
|
-
reminderDaysBefore: number
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const DEFAULT_SUPPORT_SETTINGS: SupportSettings = {
|
|
37
|
-
email: { fromAddress: '', fromName: 'Support', replyToAddress: '' },
|
|
38
|
-
ai: {
|
|
39
|
-
provider: 'anthropic',
|
|
40
|
-
model: 'claude-haiku-4-5-20251001',
|
|
41
|
-
enableSentiment: true,
|
|
42
|
-
enableSynthesis: true,
|
|
43
|
-
enableSuggestion: true,
|
|
44
|
-
enableRewrite: true,
|
|
45
|
-
},
|
|
46
|
-
sla: {
|
|
47
|
-
firstResponseMinutes: 120,
|
|
48
|
-
resolutionMinutes: 1440,
|
|
49
|
-
businessHoursOnly: true,
|
|
50
|
-
escalationEmail: '',
|
|
51
|
-
},
|
|
52
|
-
autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 },
|
|
53
|
-
}
|
|
4
|
+
import {
|
|
5
|
+
SUPPORT_SETTINGS_PREF_KEY as PREF_KEY,
|
|
6
|
+
invalidateSupportSettingsCache,
|
|
7
|
+
mergeSupportSettings,
|
|
8
|
+
readSupportSettingsState,
|
|
9
|
+
type SupportSettings,
|
|
10
|
+
} from '../utils/readSettings'
|
|
11
|
+
import { stripProjectedFeatures } from '../utils/features'
|
|
54
12
|
|
|
55
13
|
/**
|
|
56
14
|
* GET /api/support/settings — Read support settings
|
|
15
|
+
*
|
|
16
|
+
* `featuresConfigured` tells the admin UI whether the server has ever had
|
|
17
|
+
* feature flags saved; a client still holding pre-2.1 `localStorage` flags uses
|
|
18
|
+
* it to seed the server once instead of losing its configuration.
|
|
57
19
|
*/
|
|
58
20
|
export function createSettingsGetEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
59
21
|
return {
|
|
@@ -65,25 +27,9 @@ export function createSettingsGetEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
65
27
|
|
|
66
28
|
requireAdmin(req, slugs)
|
|
67
29
|
|
|
68
|
-
const
|
|
69
|
-
where: { key: { equals: PREF_KEY } },
|
|
70
|
-
limit: 1,
|
|
71
|
-
depth: 0,
|
|
72
|
-
overrideAccess: true,
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
let settings = { ...DEFAULT_SUPPORT_SETTINGS }
|
|
76
|
-
if (prefs.docs.length > 0) {
|
|
77
|
-
const stored = prefs.docs[0].value as Partial<SupportSettings>
|
|
78
|
-
settings = {
|
|
79
|
-
email: { ...DEFAULT_SUPPORT_SETTINGS.email, ...stored.email },
|
|
80
|
-
ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...stored.ai },
|
|
81
|
-
sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...stored.sla },
|
|
82
|
-
autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...stored.autoClose },
|
|
83
|
-
}
|
|
84
|
-
}
|
|
30
|
+
const { settings, featuresConfigured } = await readSupportSettingsState(payload)
|
|
85
31
|
|
|
86
|
-
return Response.json(settings)
|
|
32
|
+
return Response.json({ ...settings, featuresConfigured })
|
|
87
33
|
} catch (error) {
|
|
88
34
|
const authResponse = handleAuthError(error)
|
|
89
35
|
if (authResponse) return authResponse
|
|
@@ -95,7 +41,11 @@ export function createSettingsGetEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
95
41
|
}
|
|
96
42
|
|
|
97
43
|
/**
|
|
98
|
-
* POST /api/support/settings — Save
|
|
44
|
+
* POST /api/support/settings — Save support settings (admin-only)
|
|
45
|
+
*
|
|
46
|
+
* The body is merged onto the CURRENT settings, not onto the defaults: the
|
|
47
|
+
* feature-flag screen posts `{ features }` alone, and that must not reset the
|
|
48
|
+
* email / AI / SLA blocks.
|
|
99
49
|
*/
|
|
100
50
|
export function createSettingsPostEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
101
51
|
return {
|
|
@@ -109,19 +59,20 @@ export function createSettingsPostEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
109
59
|
|
|
110
60
|
const body = (await req.json!()) as Partial<SupportSettings>
|
|
111
61
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...body.ai },
|
|
115
|
-
sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...body.sla },
|
|
116
|
-
autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...body.autoClose },
|
|
117
|
-
}
|
|
62
|
+
const { settings: current } = await readSupportSettingsState(payload)
|
|
63
|
+
const merged = mergeSupportSettings(body, current)
|
|
118
64
|
|
|
119
65
|
await payload.db.upsert({
|
|
120
66
|
collection: 'payload-preferences',
|
|
121
67
|
data: {
|
|
122
68
|
key: PREF_KEY,
|
|
123
69
|
user: { relationTo: req.user!.collection, value: req.user!.id },
|
|
124
|
-
value:
|
|
70
|
+
value: {
|
|
71
|
+
...merged,
|
|
72
|
+
// Persist the flags without the two projected ones, so `features`
|
|
73
|
+
// and the `autoClose` block can never drift apart.
|
|
74
|
+
features: stripProjectedFeatures(merged.features),
|
|
75
|
+
} as unknown as Record<string, unknown>,
|
|
125
76
|
},
|
|
126
77
|
req: { payload, user: req.user } as any,
|
|
127
78
|
where: {
|
|
@@ -135,7 +86,7 @@ export function createSettingsPostEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
135
86
|
|
|
136
87
|
invalidateSupportSettingsCache()
|
|
137
88
|
|
|
138
|
-
return Response.json(merged)
|
|
89
|
+
return Response.json({ ...merged, featuresConfigured: true })
|
|
139
90
|
} catch (error) {
|
|
140
91
|
const authResponse = handleAuthError(error)
|
|
141
92
|
if (authResponse) return authResponse
|
package/src/index.ts
CHANGED
|
@@ -21,8 +21,12 @@ export { DEFAULT_FEATURES } from './types'
|
|
|
21
21
|
// Utils
|
|
22
22
|
export { resolveSlugs, DEFAULT_SLUGS } from './utils/slugs'
|
|
23
23
|
export type { CollectionSlugs } from './utils/slugs'
|
|
24
|
-
export { readSupportSettings, readUserPrefs, DEFAULT_SETTINGS, DEFAULT_USER_PREFS } from './utils/readSettings'
|
|
25
|
-
export type { SupportSettings, UserPrefs } from './utils/readSettings'
|
|
24
|
+
export { readSupportSettings, readSupportSettingsState, readUserPrefs, DEFAULT_SETTINGS, DEFAULT_USER_PREFS } from './utils/readSettings'
|
|
25
|
+
export type { SupportSettings, SupportSettingsState, UserPrefs } from './utils/readSettings'
|
|
26
|
+
// Runtime feature flags (server-authoritative, see utils/features.ts). Distinct
|
|
27
|
+
// from the build-time `SupportFeatures` passed to `supportPlugin()`.
|
|
28
|
+
export { DEFAULT_TICKETING_FEATURES, normalizeFeatures, projectAutoClose, stripProjectedFeatures } from './utils/features'
|
|
29
|
+
export type { TicketingFeatures } from './utils/features'
|
|
26
30
|
export { MemoryRateLimitStore, PayloadRateLimitStore, RateLimiter } from './utils/rateLimiter'
|
|
27
31
|
export type { RateLimitEntry, RateLimitStore } from './utils/rateLimiter'
|
|
28
32
|
export { DEFAULT_INBOUND_EMAIL_LIMITS, validateInboundEmailPayload, verifySecret } from './utils/webhookSecurity'
|
package/src/portal/LiveChat.tsx
CHANGED
|
@@ -29,6 +29,13 @@ export function LiveChat() {
|
|
|
29
29
|
const lastFetchRef = useRef<string | null>(null)
|
|
30
30
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
31
31
|
const pollIntervalRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
32
|
+
// Mirror of `messages`. The poll decision (and the unread badge) MUST be taken
|
|
33
|
+
// outside the setMessages updater: React skips the updater on the eager-state
|
|
34
|
+
// path, so a flag assigned inside it is only ever written on the very first
|
|
35
|
+
// update — after which `hasNewMessages` stays false, the adaptive backoff climbs
|
|
36
|
+
// to its 30 s ceiling and never comes back down, mid-conversation. Same pattern
|
|
37
|
+
// as ChatWidget.tsx.
|
|
38
|
+
const messagesRef = useRef<ChatMessage[]>([])
|
|
32
39
|
|
|
33
40
|
// Load session from localStorage on mount
|
|
34
41
|
useEffect(() => {
|
|
@@ -42,6 +49,7 @@ export function LiveChat() {
|
|
|
42
49
|
|
|
43
50
|
// Auto-scroll to bottom when messages change
|
|
44
51
|
useEffect(() => {
|
|
52
|
+
messagesRef.current = messages
|
|
45
53
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
46
54
|
}, [messages])
|
|
47
55
|
|
|
@@ -61,31 +69,32 @@ export function LiveChat() {
|
|
|
61
69
|
|
|
62
70
|
const data = await res.json()
|
|
63
71
|
if (data.messages?.length > 0) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
79
|
-
if (agentNewMsgs.length > 0) {
|
|
80
|
-
setUnreadCount((prev) => prev + agentNewMsgs.length)
|
|
81
|
-
}
|
|
72
|
+
const existingIds = new Set(messagesRef.current.map((m) => String(m.id)))
|
|
73
|
+
const newMsgs: ChatMessage[] = data.messages.filter(
|
|
74
|
+
(m: ChatMessage) => !existingIds.has(String(m.id)),
|
|
75
|
+
)
|
|
76
|
+
lastFetchRef.current = data.messages[data.messages.length - 1].createdAt
|
|
77
|
+
if (newMsgs.length === 0) return false
|
|
78
|
+
|
|
79
|
+
// Count agent messages as unread if chat panel is closed. Kept OUT of the
|
|
80
|
+
// updater: nesting a setState there also double-counted the badge under
|
|
81
|
+
// StrictMode's double invocation in dev.
|
|
82
|
+
if (screen === 'closed') {
|
|
83
|
+
const agentNewMsgs = newMsgs.filter((m) => m.senderType === 'agent')
|
|
84
|
+
if (agentNewMsgs.length > 0) {
|
|
85
|
+
setUnreadCount((prev) => prev + agentNewMsgs.length)
|
|
82
86
|
}
|
|
87
|
+
}
|
|
83
88
|
|
|
84
|
-
|
|
89
|
+
setMessages((prev) => {
|
|
90
|
+
// Re-dedupe against the authoritative state: the mirror can lag by one
|
|
91
|
+
// commit if a message was appended optimistically in the meantime.
|
|
92
|
+
const prevIds = new Set(prev.map((m) => String(m.id)))
|
|
93
|
+
const trulyNew = newMsgs.filter((m) => !prevIds.has(String(m.id)))
|
|
94
|
+
if (trulyNew.length === 0) return prev
|
|
95
|
+
return [...prev, ...trulyNew]
|
|
85
96
|
})
|
|
86
|
-
|
|
87
|
-
data.messages[data.messages.length - 1].createdAt
|
|
88
|
-
return hadNewMessages
|
|
97
|
+
return true
|
|
89
98
|
}
|
|
90
99
|
return false
|
|
91
100
|
} catch {
|
|
@@ -127,6 +136,8 @@ export function LiveChat() {
|
|
|
127
136
|
// If we have a session, fetch messages
|
|
128
137
|
if (session.sessionId) {
|
|
129
138
|
lastFetchRef.current = null
|
|
139
|
+
// Synchronous too: pollMessages() runs before the mirroring effect commits.
|
|
140
|
+
messagesRef.current = []
|
|
130
141
|
setMessages([])
|
|
131
142
|
pollMessages()
|
|
132
143
|
}
|
|
@@ -30,8 +30,12 @@ export function TicketPolling({
|
|
|
30
30
|
|
|
31
31
|
const interval = setInterval(async () => {
|
|
32
32
|
try {
|
|
33
|
+
// `/count` returns a single number. The previous limit=0 idiom did NOT return
|
|
34
|
+
// zero documents — Payload disables pagination for limit=0 and derives
|
|
35
|
+
// totalDocs from the rows it materialised, so every open portal tab used to
|
|
36
|
+
// re-download its whole conversation every 30 s to read one counter.
|
|
33
37
|
const res = await fetch(
|
|
34
|
-
`/api/ticket-messages?where[ticket][equals]=${ticketId}
|
|
38
|
+
`/api/ticket-messages/count?where[ticket][equals]=${ticketId}`,
|
|
35
39
|
{ credentials: 'include' },
|
|
36
40
|
)
|
|
37
41
|
if (res.status === 401 || res.status === 403) {
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ambient stub for 'lucide-react'.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* lucide-react is a REQUIRED peer dependency — three unconditionally registered
|
|
5
|
+
* components import it statically, so a host without it fails at build time.
|
|
6
|
+
* It is deliberately not a devDependency here; this stub is what lets the plugin
|
|
7
|
+
* type-check in isolation without pulling the icon set into the repo.
|
|
8
|
+
*
|
|
9
|
+
* Keep the icon list below in sync with the actual imports:
|
|
10
|
+
* src/portal/LiveChat.tsx
|
|
11
|
+
* src/views/PendingEmailsView/client.tsx
|
|
12
|
+
* src/views/TicketingSettingsView/client.tsx
|
|
5
13
|
*/
|
|
6
14
|
declare module 'lucide-react' {
|
|
7
15
|
import type { FC, SVGProps } from 'react'
|
package/src/utils/aiAgent.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Payload } from 'payload'
|
|
|
2
2
|
import type { CollectionSlugs } from './slugs'
|
|
3
3
|
import { readSupportSettings, type SupportSettings } from './readSettings'
|
|
4
4
|
import { dbFind, dbFindByID, dbCreate, dbUpdate } from './db'
|
|
5
|
+
import { resolveOllamaBaseUrl } from './aiProvider'
|
|
5
6
|
|
|
6
7
|
export interface AgentDecision {
|
|
7
8
|
action: 'reply' | 'escalate'
|
|
@@ -61,7 +62,7 @@ export async function applyAgentDecision(
|
|
|
61
62
|
function getClient(aiSettings: SupportSettings['ai']) {
|
|
62
63
|
const Anthropic = require('@anthropic-ai/sdk').default
|
|
63
64
|
if (aiSettings.provider === 'ollama') {
|
|
64
|
-
return new Anthropic({ apiKey: 'ollama', baseURL:
|
|
65
|
+
return new Anthropic({ apiKey: 'ollama', baseURL: resolveOllamaBaseUrl() })
|
|
65
66
|
}
|
|
66
67
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
|
|
67
68
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { APIError } from 'payload'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the base URL of the OpenAI-compatible gateway used by the `ollama`
|
|
5
|
+
* provider.
|
|
6
|
+
*
|
|
7
|
+
* There is deliberately NO default here. An integrator who selects `ollama` does
|
|
8
|
+
* so precisely to keep ticket content off third-party infrastructure; a silent
|
|
9
|
+
* fallback to any hosted endpoint would ship the whole conversation — subject,
|
|
10
|
+
* message bodies, client name and company — to a server the operator never chose,
|
|
11
|
+
* and would do so invisibly because the AI answer still comes back normally.
|
|
12
|
+
*
|
|
13
|
+
* Fail loudly instead: a missing `OLLAMA_API_URL` is a configuration error.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveOllamaBaseUrl(): string {
|
|
16
|
+
const baseURL = process.env.OLLAMA_API_URL
|
|
17
|
+
if (!baseURL) {
|
|
18
|
+
throw new APIError("provider 'ollama' requires OLLAMA_API_URL", 500)
|
|
19
|
+
}
|
|
20
|
+
return baseURL
|
|
21
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticketing feature flags — single source of truth (client + server).
|
|
3
|
+
*
|
|
4
|
+
* Flags are stored SERVER-side, in the `support-settings` row of
|
|
5
|
+
* `payload-preferences` (see `readSettings.ts` and `endpoints/settings.ts`).
|
|
6
|
+
* A flag toggled on one machine therefore applies to every browser and to
|
|
7
|
+
* server-side code (hooks, cron endpoints) alike.
|
|
8
|
+
*
|
|
9
|
+
* `localStorage` is still used, but demoted to a plain CACHE: it lets admin
|
|
10
|
+
* views paint synchronously on first render and keeps them usable when the
|
|
11
|
+
* settings endpoint is unreachable. It is never authoritative.
|
|
12
|
+
*
|
|
13
|
+
* Two flags are *projections* of settings that already lived server-side long
|
|
14
|
+
* before this module, and are deliberately NOT stored twice:
|
|
15
|
+
* - `autoClose` ← `settings.autoClose.enabled`
|
|
16
|
+
* - `autoCloseDays` ← `settings.autoClose.daysBeforeClose`
|
|
17
|
+
* The `autoClose` block stays authoritative; the flags are recomputed on read
|
|
18
|
+
* and dropped on write. See `projectAutoClose` / `stripProjectedFeatures`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Feature flags for the ticketing module.
|
|
23
|
+
* Each feature can be enabled/disabled by the admin.
|
|
24
|
+
* When disabled, the corresponding UI section is hidden entirely.
|
|
25
|
+
*/
|
|
26
|
+
export interface TicketingFeatures {
|
|
27
|
+
/** Time tracking: timer, manual entries, billing */
|
|
28
|
+
timeTracking: boolean
|
|
29
|
+
/** AI features: sentiment, synthesis, suggestion, rewrite */
|
|
30
|
+
ai: boolean
|
|
31
|
+
/** Satisfaction surveys: CSAT rating after resolution */
|
|
32
|
+
satisfaction: boolean
|
|
33
|
+
/** Live chat integration: chat → ticket conversion */
|
|
34
|
+
chat: boolean
|
|
35
|
+
/** Email tracking: pixel tracking, open/sent status per message */
|
|
36
|
+
emailTracking: boolean
|
|
37
|
+
/** Canned responses: quick reply templates */
|
|
38
|
+
canned: boolean
|
|
39
|
+
/** Ticket merge: combine two tickets into one */
|
|
40
|
+
merge: boolean
|
|
41
|
+
/** Snooze: temporarily hide a ticket */
|
|
42
|
+
snooze: boolean
|
|
43
|
+
/** External messages: add messages received outside the system */
|
|
44
|
+
externalMessages: boolean
|
|
45
|
+
/** Client history: past tickets, projects, notes sidebar */
|
|
46
|
+
clientHistory: boolean
|
|
47
|
+
/** Activity log: audit trail of actions on the ticket */
|
|
48
|
+
activityLog: boolean
|
|
49
|
+
/** Split ticket: extract a message into a new ticket */
|
|
50
|
+
splitTicket: boolean
|
|
51
|
+
/** Scheduled replies: send a message at a future date */
|
|
52
|
+
scheduledReplies: boolean
|
|
53
|
+
/** Auto-close: automatically resolve inactive tickets (projection of settings.autoClose.enabled) */
|
|
54
|
+
autoClose: boolean
|
|
55
|
+
/** Auto-close delay in days (projection of settings.autoClose.daysBeforeClose) */
|
|
56
|
+
autoCloseDays: number
|
|
57
|
+
/** Round-robin: distribute new tickets evenly among agents */
|
|
58
|
+
roundRobin: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Default features — all enabled except round-robin */
|
|
62
|
+
export const DEFAULT_TICKETING_FEATURES: TicketingFeatures = {
|
|
63
|
+
timeTracking: true,
|
|
64
|
+
ai: true,
|
|
65
|
+
satisfaction: true,
|
|
66
|
+
chat: true,
|
|
67
|
+
emailTracking: true,
|
|
68
|
+
canned: true,
|
|
69
|
+
merge: true,
|
|
70
|
+
snooze: true,
|
|
71
|
+
externalMessages: true,
|
|
72
|
+
clientHistory: true,
|
|
73
|
+
activityLog: true,
|
|
74
|
+
splitTicket: true,
|
|
75
|
+
scheduledReplies: true,
|
|
76
|
+
autoClose: true,
|
|
77
|
+
autoCloseDays: 7,
|
|
78
|
+
roundRobin: false,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type BooleanFeatureKey = Exclude<keyof TicketingFeatures, 'autoCloseDays'>
|
|
82
|
+
|
|
83
|
+
const BOOLEAN_FEATURE_KEYS: readonly BooleanFeatureKey[] = [
|
|
84
|
+
'timeTracking',
|
|
85
|
+
'ai',
|
|
86
|
+
'satisfaction',
|
|
87
|
+
'chat',
|
|
88
|
+
'emailTracking',
|
|
89
|
+
'canned',
|
|
90
|
+
'merge',
|
|
91
|
+
'snooze',
|
|
92
|
+
'externalMessages',
|
|
93
|
+
'clientHistory',
|
|
94
|
+
'activityLog',
|
|
95
|
+
'splitTicket',
|
|
96
|
+
'scheduledReplies',
|
|
97
|
+
'autoClose',
|
|
98
|
+
'roundRobin',
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
/** Flags derived from another settings block — never persisted inside `features`. */
|
|
102
|
+
const PROJECTED_FEATURE_KEYS = ['autoClose', 'autoCloseDays'] as const
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Coerce arbitrary input (stale localStorage, hand-edited preference row, older
|
|
106
|
+
* plugin version) into a complete, well-typed feature set. Unknown keys are
|
|
107
|
+
* dropped and wrongly-typed values fall back to their default.
|
|
108
|
+
*/
|
|
109
|
+
export function normalizeFeatures(raw: unknown): TicketingFeatures {
|
|
110
|
+
const out = { ...DEFAULT_TICKETING_FEATURES }
|
|
111
|
+
if (!raw || typeof raw !== 'object') return out
|
|
112
|
+
|
|
113
|
+
const src = raw as Record<string, unknown>
|
|
114
|
+
for (const key of BOOLEAN_FEATURE_KEYS) {
|
|
115
|
+
if (typeof src[key] === 'boolean') out[key] = src[key] as boolean
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const days = src.autoCloseDays
|
|
119
|
+
if (typeof days === 'number' && Number.isFinite(days) && days >= 1) {
|
|
120
|
+
out.autoCloseDays = Math.floor(days)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return out
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Shape of the `autoClose` settings block the two projected flags mirror. */
|
|
127
|
+
export interface AutoCloseProjectionSource {
|
|
128
|
+
enabled?: boolean
|
|
129
|
+
daysBeforeClose?: number
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Recompute the projected flags from the authoritative `autoClose` block. */
|
|
133
|
+
export function projectAutoClose(
|
|
134
|
+
features: TicketingFeatures,
|
|
135
|
+
autoClose: AutoCloseProjectionSource | undefined,
|
|
136
|
+
): TicketingFeatures {
|
|
137
|
+
if (!autoClose) return features
|
|
138
|
+
return {
|
|
139
|
+
...features,
|
|
140
|
+
autoClose: typeof autoClose.enabled === 'boolean' ? autoClose.enabled : features.autoClose,
|
|
141
|
+
autoCloseDays:
|
|
142
|
+
typeof autoClose.daysBeforeClose === 'number' && autoClose.daysBeforeClose >= 1
|
|
143
|
+
? Math.floor(autoClose.daysBeforeClose)
|
|
144
|
+
: features.autoCloseDays,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Drop the projected flags before persisting, so `features` and the `autoClose`
|
|
150
|
+
* block can never disagree.
|
|
151
|
+
*/
|
|
152
|
+
export function stripProjectedFeatures(
|
|
153
|
+
features: TicketingFeatures,
|
|
154
|
+
): Omit<TicketingFeatures, (typeof PROJECTED_FEATURE_KEYS)[number]> {
|
|
155
|
+
const out = { ...features } as Record<string, unknown>
|
|
156
|
+
for (const key of PROJECTED_FEATURE_KEYS) delete out[key]
|
|
157
|
+
return out as Omit<TicketingFeatures, (typeof PROJECTED_FEATURE_KEYS)[number]>
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── Client-side cache ───────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/** localStorage key. Was the source of truth before 2.1 — now only a cache. */
|
|
163
|
+
export const FEATURES_CACHE_KEY = 'ticketing_features'
|
|
164
|
+
/** Set once the pre-2.1 localStorage values have been pushed to the server. */
|
|
165
|
+
export const FEATURES_ADOPTED_KEY = 'ticketing_features_adopted'
|
|
166
|
+
/** Admin-only settings endpoint backing the flags. */
|
|
167
|
+
export const FEATURES_ENDPOINT = '/api/support/settings'
|
|
168
|
+
|
|
169
|
+
/** localStorage can throw (Safari private mode, disabled storage) — never let it bubble. */
|
|
170
|
+
function storage(): Storage | null {
|
|
171
|
+
try {
|
|
172
|
+
if (typeof window === 'undefined' || !window.localStorage) return null
|
|
173
|
+
return window.localStorage
|
|
174
|
+
} catch {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Read the cached flags, or `null` when nothing usable is cached. */
|
|
180
|
+
export function readFeaturesCache(): TicketingFeatures | null {
|
|
181
|
+
const store = storage()
|
|
182
|
+
if (!store) return null
|
|
183
|
+
try {
|
|
184
|
+
const stored = store.getItem(FEATURES_CACHE_KEY)
|
|
185
|
+
if (!stored) return null
|
|
186
|
+
return normalizeFeatures(JSON.parse(stored))
|
|
187
|
+
} catch {
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Refresh the cache with the values the server just confirmed. */
|
|
193
|
+
export function writeFeaturesCache(features: TicketingFeatures): void {
|
|
194
|
+
const store = storage()
|
|
195
|
+
if (!store) return
|
|
196
|
+
try {
|
|
197
|
+
store.setItem(FEATURES_CACHE_KEY, JSON.stringify(features))
|
|
198
|
+
} catch {
|
|
199
|
+
/* quota exceeded / storage disabled — the server copy still stands */
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Synchronous read used for first paint. Returns the cache when present, the
|
|
205
|
+
* defaults otherwise. Callers must still refresh from the server — see
|
|
206
|
+
* `useFeatures`.
|
|
207
|
+
*/
|
|
208
|
+
export function getFeatures(): TicketingFeatures {
|
|
209
|
+
return readFeaturesCache() ?? { ...DEFAULT_TICKETING_FEATURES }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── One-shot migration of pre-2.1 localStorage ──────────
|
|
213
|
+
|
|
214
|
+
/** True once this browser's legacy flags have been handed over to the server. */
|
|
215
|
+
export function hasAdoptedLegacyFeatures(): boolean {
|
|
216
|
+
const store = storage()
|
|
217
|
+
if (!store) return true // nothing to migrate without storage
|
|
218
|
+
try {
|
|
219
|
+
return store.getItem(FEATURES_ADOPTED_KEY) === '1'
|
|
220
|
+
} catch {
|
|
221
|
+
return true
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function markLegacyFeaturesAdopted(): void {
|
|
226
|
+
const store = storage()
|
|
227
|
+
if (!store) return
|
|
228
|
+
try {
|
|
229
|
+
store.setItem(FEATURES_ADOPTED_KEY, '1')
|
|
230
|
+
} catch {
|
|
231
|
+
/* ignore */
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Flags this browser configured before the server became authoritative, or
|
|
237
|
+
* `null` when there is nothing to migrate (no local values, or already done).
|
|
238
|
+
*/
|
|
239
|
+
export function readLegacyFeatures(): TicketingFeatures | null {
|
|
240
|
+
if (hasAdoptedLegacyFeatures()) return null
|
|
241
|
+
return readFeaturesCache()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ─── Server round-trips ──────────────────────────────────
|
|
245
|
+
|
|
246
|
+
export interface FetchedFeatures {
|
|
247
|
+
features: TicketingFeatures
|
|
248
|
+
/** False when the server has never had feature flags saved (fresh install). */
|
|
249
|
+
configured: boolean
|
|
250
|
+
/** True when the values come from the cache/defaults because the call failed. */
|
|
251
|
+
offline: boolean
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Read the flags from the server. Falls back to the local cache, then to the
|
|
256
|
+
* defaults: a failed settings call must never leave a ticket screen unusable.
|
|
257
|
+
*/
|
|
258
|
+
export async function fetchFeatures(signal?: AbortSignal): Promise<FetchedFeatures> {
|
|
259
|
+
try {
|
|
260
|
+
const res = await fetch(FEATURES_ENDPOINT, { credentials: 'include', signal })
|
|
261
|
+
if (res.ok) {
|
|
262
|
+
const data = (await res.json()) as {
|
|
263
|
+
features?: unknown
|
|
264
|
+
featuresConfigured?: boolean
|
|
265
|
+
autoClose?: AutoCloseProjectionSource
|
|
266
|
+
}
|
|
267
|
+
const features = projectAutoClose(normalizeFeatures(data.features), data.autoClose)
|
|
268
|
+
writeFeaturesCache(features)
|
|
269
|
+
return { features, configured: data.featuresConfigured === true, offline: false }
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
/* network error / aborted — fall through to the cache */
|
|
273
|
+
}
|
|
274
|
+
return { features: getFeatures(), configured: false, offline: true }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Persist the flags server-side, then refresh the local cache.
|
|
279
|
+
*
|
|
280
|
+
* @returns true when the server accepted the write.
|
|
281
|
+
*/
|
|
282
|
+
export async function saveFeatures(features: TicketingFeatures): Promise<boolean> {
|
|
283
|
+
const normalized = normalizeFeatures(features)
|
|
284
|
+
try {
|
|
285
|
+
const res = await fetch(FEATURES_ENDPOINT, {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
headers: { 'Content-Type': 'application/json' },
|
|
288
|
+
credentials: 'include',
|
|
289
|
+
body: JSON.stringify({ features: normalized }),
|
|
290
|
+
})
|
|
291
|
+
if (!res.ok) return false
|
|
292
|
+
writeFeaturesCache(normalized)
|
|
293
|
+
return true
|
|
294
|
+
} catch {
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -6,6 +6,12 @@ import { dbFind, dbUpdate } from './db'
|
|
|
6
6
|
* Fire all active webhooks matching the given event.
|
|
7
7
|
* Uses fire-and-forget pattern (Promise.allSettled) so hook callers are never blocked.
|
|
8
8
|
* Each webhook request has a 10-second timeout.
|
|
9
|
+
*
|
|
10
|
+
* @deprecated Use `dispatchWebhook` instead. This transport sends the endpoint
|
|
11
|
+
* secret IN CLEAR in the `X-Webhook-Secret` header, which contradicts the
|
|
12
|
+
* description of the `secret` field (HMAC-SHA256 signature) and defeats the point
|
|
13
|
+
* of signing. No hook in the plugin calls it any more — it stays exported only
|
|
14
|
+
* because it is part of the public surface, and will be removed in the next major.
|
|
9
15
|
*/
|
|
10
16
|
export async function fireWebhooks(
|
|
11
17
|
payload: Payload,
|
|
@@ -2,12 +2,13 @@ import type { Payload } from 'payload'
|
|
|
2
2
|
import type { CollectionSlugs } from './slugs'
|
|
3
3
|
import { readSupportSettings, type SupportSettings } from './readSettings'
|
|
4
4
|
import { dbFindByID, dbUpdate, dbFind } from './db'
|
|
5
|
+
import { resolveOllamaBaseUrl } from './aiProvider'
|
|
5
6
|
|
|
6
7
|
function getClient(aiSettings: SupportSettings['ai']) {
|
|
7
8
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
8
9
|
const Anthropic = require('@anthropic-ai/sdk').default
|
|
9
10
|
if (aiSettings.provider === 'ollama') {
|
|
10
|
-
const baseURL =
|
|
11
|
+
const baseURL = resolveOllamaBaseUrl()
|
|
11
12
|
return new Anthropic({ apiKey: 'ollama', baseURL })
|
|
12
13
|
}
|
|
13
14
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
|
package/src/utils/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export { readSupportSettings, DEFAULT_SETTINGS } from './readSettings'
|
|
2
|
-
export type { SupportSettings } from './readSettings'
|
|
1
|
+
export { readSupportSettings, readSupportSettingsState, mergeSupportSettings, invalidateSupportSettingsCache, DEFAULT_SETTINGS } from './readSettings'
|
|
2
|
+
export type { SupportSettings, SupportSettingsState } from './readSettings'
|
|
3
|
+
export { DEFAULT_TICKETING_FEATURES, normalizeFeatures, projectAutoClose, stripProjectedFeatures } from './features'
|
|
4
|
+
export type { TicketingFeatures } from './features'
|
|
3
5
|
export { resolveSlugs, DEFAULT_SLUGS } from './slugs'
|
|
4
6
|
export type { CollectionSlugs } from './slugs'
|
|
5
7
|
export { RateLimiter } from './rateLimiter'
|