@mevdragon/vidfarm-devcli 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +9 -0
- package/GETTING_STARTED.developers.md +1 -30
- package/README.md +22 -5
- package/SKILL.developer.md +464 -12
- package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +475 -0
- package/auto-create-templates/extractor-system-prompt.md +113 -0
- package/auto-create-templates/production-graph.schema.json +276 -0
- package/auto-create-templates/runbook.md +140 -0
- package/auto-create-templates/template-plan.schema.json +230 -0
- package/auto-create-templates/template-planner-prompt.md +75 -0
- package/dist/src/account-pages-legacy.js +9064 -0
- package/dist/src/account-pages.js +41 -620
- package/dist/src/app.js +7173 -494
- package/dist/src/cli.js +525 -29
- package/dist/src/config.js +14 -7
- package/dist/src/context.js +51 -35
- package/dist/src/db.js +1049 -55
- package/dist/src/dev-app-legacy.js +693 -0
- package/dist/src/dev-app.js +4 -904
- package/dist/src/domain.js +1 -1
- package/dist/src/editor-chat-history.js +72 -0
- package/dist/src/editor-chat.js +202 -0
- package/dist/src/frontend/flockposter-cache-store.js +116 -0
- package/dist/src/frontend/homepage-client.js +114 -0
- package/dist/src/frontend/homepage-shared.js +3 -0
- package/dist/src/frontend/homepage-store.js +27 -0
- package/dist/src/frontend/homepage-view.js +147 -0
- package/dist/src/frontend/page-runtime-client.js +100 -0
- package/dist/src/frontend/page-runtime-store.js +8 -0
- package/dist/src/frontend/template-editor-chat.js +2261 -0
- package/dist/src/homepage.js +695 -339
- package/dist/src/lib/template-paths.js +10 -4
- package/dist/src/lib/template-style-options.js +95 -15
- package/dist/src/page-runtime.js +1 -0
- package/dist/src/page-shell.js +941 -0
- package/dist/src/primitive-context.js +163 -0
- package/dist/src/primitive-registry.js +340 -0
- package/dist/src/primitive-sdk.js +3 -0
- package/dist/src/primitives/remotion/html-image.js +28 -0
- package/dist/src/react-page-shell.js +34 -0
- package/dist/src/ready-post-schedule-component.js +1514 -0
- package/dist/src/registry.js +44 -18
- package/dist/src/runtime.js +6 -1
- package/dist/src/services/api-call-history.js +245 -0
- package/dist/src/services/auth.js +27 -9
- package/dist/src/services/billing.js +248 -9
- package/dist/src/services/chat-threads.js +88 -0
- package/dist/src/services/job-logs.js +10 -3
- package/dist/src/services/jobs.js +13 -2
- package/dist/src/services/providers.js +944 -55
- package/dist/src/services/rate-limits.js +236 -0
- package/dist/src/services/remotion.js +143 -16
- package/dist/src/services/storage.js +23 -8
- package/dist/src/services/template-certification.js +26 -3
- package/dist/src/services/template-loader.js +19 -1
- package/dist/src/services/template-sources.js +316 -7
- package/dist/src/template-editor-pages.js +2309 -0
- package/dist/src/template-editor-shell.js +1507 -0
- package/dist/src/worker.js +120 -22
- package/package.json +30 -6
- package/public/assets/homepage-app.js +54 -0
- package/public/assets/homepage-client-app.js +54 -0
- package/public/assets/page-runtime-client-app.js +68 -0
- package/dist/templates/template_0000/src/lib/images.js +0 -202
- package/dist/templates/template_0000/src/remotion/Root.js +0 -33
- package/dist/templates/template_0000/src/remotion/index.js +0 -3
- package/dist/templates/template_0000/src/sdk.js +0 -3
- package/dist/templates/template_0000/src/style-options.js +0 -51
- package/dist/templates/template_0000/src/template-dna.js +0 -9
- package/dist/templates/template_0000/src/template.js +0 -1441
- package/templates/template_0000/README.md +0 -77
- package/templates/template_0000/SKILL.md +0 -225
- package/templates/template_0000/assets/Abel-Regular.ttf +0 -0
- package/templates/template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
- package/templates/template_0000/assets/Montserrat[wght].ttf +0 -0
- package/templates/template_0000/assets/SourceCodePro[wght].ttf +0 -0
- package/templates/template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
- package/templates/template_0000/assets/Yesteryear-Regular.ttf +0 -0
- package/templates/template_0000/composition.json +0 -11
- package/templates/template_0000/package.json +0 -28
- package/templates/template_0000/research/preview/.gitkeep +0 -1
- package/templates/template_0000/research/source_notes.md +0 -7
- package/templates/template_0000/src/lib/images.js +0 -202
- package/templates/template_0000/src/lib/images.ts +0 -241
- package/templates/template_0000/src/remotion/Root.js +0 -33
- package/templates/template_0000/src/remotion/Root.tsx +0 -75
- package/templates/template_0000/src/remotion/index.js +0 -3
- package/templates/template_0000/src/remotion/index.tsx +0 -4
- package/templates/template_0000/src/sdk.js +0 -3
- package/templates/template_0000/src/sdk.ts +0 -122
- package/templates/template_0000/src/style-options.js +0 -51
- package/templates/template_0000/src/style-options.ts +0 -60
- package/templates/template_0000/src/template-dna.js +0 -9
- package/templates/template_0000/src/template-dna.ts +0 -15
- package/templates/template_0000/src/template.js +0 -1441
- package/templates/template_0000/src/template.ts +0 -2042
- package/templates/template_0000/template.config.json +0 -21
- package/templates/template_0000/tsconfig.json +0 -19
|
@@ -1,16 +1,255 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { database } from "../db.js";
|
|
2
3
|
import { createId } from "../lib/ids.js";
|
|
4
|
+
import { config } from "../config.js";
|
|
5
|
+
const BILLING_COST_EVENTS_PATH = "/v1/cost-events";
|
|
6
|
+
const BILLING_WALLET_FUNDING_INTENTS_PATH = "/v1/wallet-funding-intents";
|
|
7
|
+
const BILLING_WALLET_FUNDING_CONFIRMED_PATH = "/v1/wallet-funding-confirmed";
|
|
8
|
+
const PAID_SIGNUP_BONUS_GRANT_TYPE = "paid_signup_bonus";
|
|
9
|
+
const PAID_SIGNUP_BONUS_USD = 5;
|
|
3
10
|
export class BillingService {
|
|
11
|
+
async getWalletSummary(customerId) {
|
|
12
|
+
if (!config.VIDFARM_BILLING_URL || !config.VIDFARM_BILLING_SECRET) {
|
|
13
|
+
throw new Error("Billing service is not configured.");
|
|
14
|
+
}
|
|
15
|
+
const response = await fetch(new URL(`/v1/wallets/${encodeURIComponent(customerId)}`, config.VIDFARM_BILLING_URL), {
|
|
16
|
+
headers: {
|
|
17
|
+
"x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
const body = await response.text().catch(() => "");
|
|
22
|
+
throw new Error(`Billing wallet lookup failed (${response.status}): ${body || response.statusText}`);
|
|
23
|
+
}
|
|
24
|
+
const payload = await response.json();
|
|
25
|
+
return {
|
|
26
|
+
currency: typeof payload.currency === "string" ? payload.currency : "USD",
|
|
27
|
+
balanceUsd: Number(payload.balance_usd ?? 0),
|
|
28
|
+
balanceMicrosUsd: Number(payload.balance_micros_usd ?? 0)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async getJobBillingSummary(input) {
|
|
32
|
+
if (!config.VIDFARM_BILLING_URL || !config.VIDFARM_BILLING_SECRET) {
|
|
33
|
+
return {
|
|
34
|
+
currency: "USD",
|
|
35
|
+
eventCount: 0,
|
|
36
|
+
totalBaseCostUsd: 0,
|
|
37
|
+
totalChargeUsd: 0,
|
|
38
|
+
unavailableReason: "Billing service is not configured."
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const url = new URL(`/v1/jobs/${encodeURIComponent(input.jobId)}/billing`, config.VIDFARM_BILLING_URL);
|
|
42
|
+
url.searchParams.set("customer_id", input.customerId);
|
|
43
|
+
if (input.templateId) {
|
|
44
|
+
url.searchParams.set("template_id", input.templateId);
|
|
45
|
+
}
|
|
46
|
+
const response = await fetch(url, {
|
|
47
|
+
headers: {
|
|
48
|
+
"x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const body = await response.text().catch(() => "");
|
|
53
|
+
throw new Error(`Billing job summary lookup failed (${response.status}): ${body || response.statusText}`);
|
|
54
|
+
}
|
|
55
|
+
const payload = await response.json();
|
|
56
|
+
return {
|
|
57
|
+
currency: typeof payload.currency === "string" ? payload.currency : "USD",
|
|
58
|
+
eventCount: Number(payload.event_count ?? 0),
|
|
59
|
+
totalBaseCostUsd: Number(payload.total_base_cost_usd ?? 0),
|
|
60
|
+
totalChargeUsd: Number(payload.total_charge_usd ?? 0),
|
|
61
|
+
unavailableReason: null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
4
64
|
async record(input) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
65
|
+
const normalizedCostCenterSlug = resolveCostCenterSlug(input);
|
|
66
|
+
const normalizedIdempotencyKey = resolveIdempotencyKey(input, normalizedCostCenterSlug);
|
|
67
|
+
const remoteEventCostCenterSlug = normalizedCostCenterSlug ?? null;
|
|
68
|
+
if (!remoteEventCostCenterSlug) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!config.VIDFARM_BILLING_URL || !config.VIDFARM_BILLING_SECRET) {
|
|
72
|
+
throw new Error(`Billing URL/secret missing for cost center ${remoteEventCostCenterSlug}.`);
|
|
73
|
+
}
|
|
74
|
+
const response = await fetch(new URL(BILLING_COST_EVENTS_PATH, config.VIDFARM_BILLING_URL), {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: {
|
|
77
|
+
"content-type": "application/json",
|
|
78
|
+
"x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
idempotency_key: normalizedIdempotencyKey ?? `${remoteEventCostCenterSlug}:${input.jobId}:${input.templateId}:${input.type}`,
|
|
82
|
+
customer_id: input.customerId,
|
|
83
|
+
job_id: input.jobId,
|
|
84
|
+
tracer: input.tracer,
|
|
85
|
+
cost_center_slug: remoteEventCostCenterSlug,
|
|
86
|
+
base_cost_usd: input.costUsd,
|
|
87
|
+
metadata: {
|
|
88
|
+
...input.metadata,
|
|
89
|
+
template_id: input.templateId,
|
|
90
|
+
local_charge_usd: input.chargeUsd
|
|
91
|
+
},
|
|
92
|
+
occurred_at_ms: input.occurredAtMs ?? Date.now()
|
|
93
|
+
})
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const body = await response.text().catch(() => "");
|
|
97
|
+
throw new Error(`Billing service rejected cost event (${response.status}): ${body || response.statusText}`);
|
|
98
|
+
}
|
|
99
|
+
await response.json().catch(() => ({}));
|
|
100
|
+
}
|
|
101
|
+
async grantPaidSignupBonus(customerId, metadata) {
|
|
102
|
+
const customer = database.getCustomerById(customerId);
|
|
103
|
+
if (!customer) {
|
|
104
|
+
throw new Error("Customer not found.");
|
|
105
|
+
}
|
|
106
|
+
if (!customer.isPaidPlan) {
|
|
107
|
+
return { granted: false, reason: "customer_not_paid" };
|
|
108
|
+
}
|
|
109
|
+
if (database.hasCustomerCreditGrant(customerId, PAID_SIGNUP_BONUS_GRANT_TYPE)) {
|
|
110
|
+
return { granted: false, reason: "already_granted" };
|
|
111
|
+
}
|
|
112
|
+
if (!config.VIDFARM_BILLING_URL || !config.VIDFARM_BILLING_SECRET) {
|
|
113
|
+
throw new Error("Billing service is not configured.");
|
|
114
|
+
}
|
|
115
|
+
const clientReferenceId = buildPaidSignupBonusClientReferenceId(customerId);
|
|
116
|
+
const stripeEventId = `${PAID_SIGNUP_BONUS_GRANT_TYPE}:${customerId}`;
|
|
117
|
+
const intentResponse = await fetch(new URL(BILLING_WALLET_FUNDING_INTENTS_PATH, config.VIDFARM_BILLING_URL), {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: {
|
|
120
|
+
"content-type": "application/json",
|
|
121
|
+
"x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
customer_id: customerId,
|
|
125
|
+
client_reference_id: clientReferenceId,
|
|
126
|
+
occurred_at_ms: Date.now(),
|
|
127
|
+
metadata: {
|
|
128
|
+
...(metadata ?? {}),
|
|
129
|
+
grant_type: PAID_SIGNUP_BONUS_GRANT_TYPE
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
});
|
|
133
|
+
if (!intentResponse.ok) {
|
|
134
|
+
const body = await intentResponse.text().catch(() => "");
|
|
135
|
+
if (!/TransactionCanceledException|ConditionalCheckFailed|duplicate/i.test(body)) {
|
|
136
|
+
throw new Error(`Billing service rejected wallet funding intent (${intentResponse.status}): ${body || intentResponse.statusText}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const response = await fetch(new URL(BILLING_WALLET_FUNDING_CONFIRMED_PATH, config.VIDFARM_BILLING_URL), {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
"content-type": "application/json",
|
|
143
|
+
"x-vidfarm-billing-secret": config.VIDFARM_BILLING_SECRET
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
client_reference_id: clientReferenceId,
|
|
147
|
+
amount_usd: PAID_SIGNUP_BONUS_USD,
|
|
148
|
+
stripe_event_id: stripeEventId,
|
|
149
|
+
occurred_at_ms: Date.now(),
|
|
150
|
+
metadata: {
|
|
151
|
+
...(metadata ?? {}),
|
|
152
|
+
grant_type: PAID_SIGNUP_BONUS_GRANT_TYPE
|
|
153
|
+
}
|
|
154
|
+
})
|
|
14
155
|
});
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
const body = await response.text().catch(() => "");
|
|
158
|
+
throw new Error(`Billing service rejected signup wallet funding (${response.status}): ${body || response.statusText}`);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
database.insertCustomerCreditGrant({
|
|
162
|
+
id: createId("grant"),
|
|
163
|
+
customerId,
|
|
164
|
+
grantType: PAID_SIGNUP_BONUS_GRANT_TYPE,
|
|
165
|
+
amountUsd: PAID_SIGNUP_BONUS_USD,
|
|
166
|
+
metadata: metadata ?? {}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (!isUniqueConstraintError(error)) {
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
granted: true,
|
|
176
|
+
reason: "granted",
|
|
177
|
+
amountUsd: PAID_SIGNUP_BONUS_USD,
|
|
178
|
+
grantType: PAID_SIGNUP_BONUS_GRANT_TYPE
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function resolveCostCenterSlug(input) {
|
|
183
|
+
if (input.costCenterSlug?.trim()) {
|
|
184
|
+
const explicitSlug = input.costCenterSlug.trim();
|
|
185
|
+
return explicitSlug === "remotion_lambda" ? explicitSlug : null;
|
|
186
|
+
}
|
|
187
|
+
if (input.type === "render" && input.metadata?.mode === "lambda") {
|
|
188
|
+
return "remotion_lambda";
|
|
15
189
|
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
function resolveIdempotencyKey(input, costCenterSlug) {
|
|
193
|
+
if (input.idempotencyKey?.trim()) {
|
|
194
|
+
return input.idempotencyKey.trim();
|
|
195
|
+
}
|
|
196
|
+
const renderId = typeof input.metadata?.render_id === "string"
|
|
197
|
+
? input.metadata.render_id
|
|
198
|
+
: typeof input.metadata?.renderId === "string"
|
|
199
|
+
? input.metadata.renderId
|
|
200
|
+
: null;
|
|
201
|
+
if (costCenterSlug === "remotion_lambda" && renderId) {
|
|
202
|
+
return `remotion_lambda:${renderId}`;
|
|
203
|
+
}
|
|
204
|
+
return buildBillingEventFingerprint(input, costCenterSlug);
|
|
205
|
+
}
|
|
206
|
+
function buildBillingEventFingerprint(input, costCenterSlug) {
|
|
207
|
+
const fingerprintPayload = {
|
|
208
|
+
customerId: input.customerId,
|
|
209
|
+
jobId: input.jobId,
|
|
210
|
+
templateId: input.templateId,
|
|
211
|
+
tracer: input.tracer ?? null,
|
|
212
|
+
type: input.type,
|
|
213
|
+
costCenterSlug,
|
|
214
|
+
costUsd: roundUsd(input.costUsd),
|
|
215
|
+
chargeUsd: input.chargeUsd == null ? null : roundUsd(input.chargeUsd),
|
|
216
|
+
occurredAtMs: input.occurredAtMs ?? null,
|
|
217
|
+
metadata: sortJsonValue(input.metadata ?? {})
|
|
218
|
+
};
|
|
219
|
+
const digest = createHash("sha256")
|
|
220
|
+
.update(JSON.stringify(fingerprintPayload))
|
|
221
|
+
.digest("hex")
|
|
222
|
+
.slice(0, 24);
|
|
223
|
+
return `${costCenterSlug ?? input.type}:${input.jobId}:${digest}`;
|
|
224
|
+
}
|
|
225
|
+
function sortJsonValue(value) {
|
|
226
|
+
if (Array.isArray(value)) {
|
|
227
|
+
return value.map(sortJsonValue);
|
|
228
|
+
}
|
|
229
|
+
if (value && typeof value === "object") {
|
|
230
|
+
return Object.entries(value)
|
|
231
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
232
|
+
.reduce((acc, [key, nestedValue]) => {
|
|
233
|
+
acc[key] = sortJsonValue(nestedValue);
|
|
234
|
+
return acc;
|
|
235
|
+
}, {});
|
|
236
|
+
}
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
function roundUsd(value) {
|
|
240
|
+
return Number(value.toFixed(6));
|
|
241
|
+
}
|
|
242
|
+
function isUniqueConstraintError(error) {
|
|
243
|
+
return Boolean(error instanceof Error && /unique constraint/i.test(error.message));
|
|
244
|
+
}
|
|
245
|
+
function buildPaidSignupBonusClientReferenceId(customerId) {
|
|
246
|
+
const digest = createHash("sha256").update(`${PAID_SIGNUP_BONUS_GRANT_TYPE}:${customerId}`).digest("hex");
|
|
247
|
+
const part1 = digest.slice(0, 8);
|
|
248
|
+
const part2 = digest.slice(8, 12);
|
|
249
|
+
const part3 = `4${digest.slice(13, 16)}`;
|
|
250
|
+
const variantNibble = (8 + (Number.parseInt(digest.slice(16, 17), 16) % 4)).toString(16);
|
|
251
|
+
const part4 = `${variantNibble}${digest.slice(17, 20)}`;
|
|
252
|
+
const part5 = digest.slice(20, 32);
|
|
253
|
+
return `vidfarm_wallet_topup_${part1}-${part2}-${part3}-${part4}-${part5}`;
|
|
16
254
|
}
|
|
255
|
+
export { PAID_SIGNUP_BONUS_GRANT_TYPE, PAID_SIGNUP_BONUS_USD, resolveCostCenterSlug, resolveIdempotencyKey };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createId } from "../lib/ids.js";
|
|
2
|
+
import { nowIso } from "../lib/time.js";
|
|
3
|
+
import { database } from "../db.js";
|
|
4
|
+
import { StorageService } from "./storage.js";
|
|
5
|
+
const storage = new StorageService();
|
|
6
|
+
function normalizeTracerList(values) {
|
|
7
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
8
|
+
}
|
|
9
|
+
function buildThreadStoragePrefix(customerId, templateId, threadId) {
|
|
10
|
+
return storage.developerScopedKey(customerId, "chat-threads", templateId, threadId);
|
|
11
|
+
}
|
|
12
|
+
function buildThreadChunkKey(prefix, createdAt) {
|
|
13
|
+
const sortableTime = createdAt.replace(/[^0-9]/g, "");
|
|
14
|
+
return `${prefix}/chunks/${sortableTime}-${createId("chunk")}.json`;
|
|
15
|
+
}
|
|
16
|
+
async function readChunkMessages(key) {
|
|
17
|
+
const raw = await storage.readText(key);
|
|
18
|
+
if (!raw) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
return Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
23
|
+
}
|
|
24
|
+
export const chatThreads = {
|
|
25
|
+
async listThreads(input) {
|
|
26
|
+
const threads = database.listChatThreadMetas(input);
|
|
27
|
+
const withMessages = await Promise.all(threads.map(async (thread) => ({
|
|
28
|
+
...thread,
|
|
29
|
+
messages: await this.loadThreadMessages(thread)
|
|
30
|
+
})));
|
|
31
|
+
return withMessages;
|
|
32
|
+
},
|
|
33
|
+
async getThread(input) {
|
|
34
|
+
const thread = database.getChatThreadMeta(input.customerId, input.threadId);
|
|
35
|
+
if (!thread) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
...thread,
|
|
40
|
+
messages: await this.loadThreadMessages(thread)
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
async appendExchange(input) {
|
|
44
|
+
const createdAt = nowIso();
|
|
45
|
+
const storagePrefix = buildThreadStoragePrefix(input.customerId, input.templateId, input.threadId);
|
|
46
|
+
const chunk = {
|
|
47
|
+
version: 1,
|
|
48
|
+
threadId: input.threadId,
|
|
49
|
+
customerId: input.customerId,
|
|
50
|
+
templateId: input.templateId,
|
|
51
|
+
createdAt,
|
|
52
|
+
messages: input.messages
|
|
53
|
+
};
|
|
54
|
+
await storage.putJson(buildThreadChunkKey(storagePrefix, createdAt), chunk);
|
|
55
|
+
const updated = database.appendChatThreadMessages({
|
|
56
|
+
threadId: input.threadId,
|
|
57
|
+
customerId: input.customerId,
|
|
58
|
+
title: input.title || "New chat",
|
|
59
|
+
templateId: input.templateId,
|
|
60
|
+
storagePrefix,
|
|
61
|
+
tracers: normalizeTracerList(input.tracers),
|
|
62
|
+
messageCountDelta: input.messages.length,
|
|
63
|
+
lastMessageAt: createdAt
|
|
64
|
+
});
|
|
65
|
+
return updated ? this.getThread({ customerId: input.customerId, threadId: updated.id }) : null;
|
|
66
|
+
},
|
|
67
|
+
async updateThread(input) {
|
|
68
|
+
const updated = database.updateChatThreadState({
|
|
69
|
+
...input,
|
|
70
|
+
tracers: input.tracers ? normalizeTracerList(input.tracers) : undefined
|
|
71
|
+
});
|
|
72
|
+
return updated ? this.getThread({ customerId: input.customerId, threadId: updated.id }) : null;
|
|
73
|
+
},
|
|
74
|
+
async deleteThread(input) {
|
|
75
|
+
const existing = database.deleteChatThread(input.customerId, input.threadId);
|
|
76
|
+
if (!existing) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const keys = await storage.listKeys(`${existing.storagePrefix}/`);
|
|
80
|
+
await Promise.all(keys.map((key) => storage.deleteObject(key)));
|
|
81
|
+
return existing;
|
|
82
|
+
},
|
|
83
|
+
async loadThreadMessages(thread) {
|
|
84
|
+
const keys = (await storage.listKeys(`${thread.storagePrefix}/chunks/`)).sort();
|
|
85
|
+
const messageGroups = await Promise.all(keys.map((key) => readChunkMessages(key)));
|
|
86
|
+
return messageGroups.flat();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
@@ -32,11 +32,18 @@ export class JobLogStore {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
async listEvents(input) {
|
|
35
|
-
const limit = Math.max(1, Math.min(input.limit ?? 100, 500));
|
|
36
35
|
const persisted = await this.readPersistedEvents(input);
|
|
37
36
|
const buffered = this.readBufferedEvents(input);
|
|
38
|
-
const merged = dedupeAndSort([...persisted, ...buffered]);
|
|
39
|
-
|
|
37
|
+
const merged = dedupeAndSort([...persisted, ...buffered]).reverse();
|
|
38
|
+
const filtered = input.cursor
|
|
39
|
+
? merged.filter((event) => (Date.parse(event.createdAt) < Date.parse(input.cursor.createdAt)
|
|
40
|
+
|| (event.createdAt === input.cursor.createdAt && event.id < input.cursor.id)))
|
|
41
|
+
: merged;
|
|
42
|
+
if (input.limit === undefined) {
|
|
43
|
+
return filtered;
|
|
44
|
+
}
|
|
45
|
+
const limit = Math.max(1, Math.min(input.limit, 501));
|
|
46
|
+
return filtered.slice(0, limit);
|
|
40
47
|
}
|
|
41
48
|
async shutdown() {
|
|
42
49
|
clearInterval(this.flushTimer);
|
|
@@ -4,6 +4,13 @@ import { createId } from "../lib/ids.js";
|
|
|
4
4
|
import { addSeconds, nowIso } from "../lib/time.js";
|
|
5
5
|
import { jobLogStore } from "./job-logs.js";
|
|
6
6
|
const PENDING_JOB_STATUSES = ["queued", "running", "waiting_for_child", "waiting_for_human"];
|
|
7
|
+
function normalizeWebhookUrl(value) {
|
|
8
|
+
if (typeof value !== "string") {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
return trimmed ? trimmed : null;
|
|
13
|
+
}
|
|
7
14
|
export class JobsService {
|
|
8
15
|
assertQueueCapacity(customerId) {
|
|
9
16
|
if (config.MAX_PENDING_JOBS_GLOBAL <= 0 && config.MAX_PENDING_JOBS_PER_CUSTOMER <= 0) {
|
|
@@ -36,7 +43,7 @@ export class JobsService {
|
|
|
36
43
|
_providerHint: input.providerHint ?? null
|
|
37
44
|
},
|
|
38
45
|
progress: 0,
|
|
39
|
-
webhookUrl: input.webhookUrl ?? input.customer.defaultWebhookUrl
|
|
46
|
+
webhookUrl: normalizeWebhookUrl(input.webhookUrl) ?? normalizeWebhookUrl(input.customer.defaultWebhookUrl),
|
|
40
47
|
parentJobId: null,
|
|
41
48
|
priority: 100,
|
|
42
49
|
attemptCount: 0,
|
|
@@ -73,6 +80,9 @@ export class JobsService {
|
|
|
73
80
|
listJobs(input) {
|
|
74
81
|
return database.listJobsForCustomer(input);
|
|
75
82
|
}
|
|
83
|
+
renameTracer(input) {
|
|
84
|
+
return database.renameJobsTracer(input);
|
|
85
|
+
}
|
|
76
86
|
async listLogs(input) {
|
|
77
87
|
return jobLogStore.listEvents(input);
|
|
78
88
|
}
|
|
@@ -93,6 +103,7 @@ export class JobsService {
|
|
|
93
103
|
status: "succeeded",
|
|
94
104
|
progress,
|
|
95
105
|
result,
|
|
106
|
+
error: null,
|
|
96
107
|
completedAt: nowIso()
|
|
97
108
|
});
|
|
98
109
|
}
|
|
@@ -109,7 +120,7 @@ export class JobsService {
|
|
|
109
120
|
database.updateJobStatus({
|
|
110
121
|
id: job.id,
|
|
111
122
|
status: nextStatus,
|
|
112
|
-
error,
|
|
123
|
+
error: nextStatus === "failed" ? error : null,
|
|
113
124
|
runAfter: addSeconds(new Date(), delaySeconds),
|
|
114
125
|
completedAt: nextStatus === "failed" ? nowIso() : null
|
|
115
126
|
});
|