@okrlinkhub/agent-factory 3.0.0 → 3.0.2
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/dist/client/index.d.ts +42 -14
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +68 -2
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +114 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/lib.d.ts +2 -1
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +2 -1
- package/dist/component/lib.js.map +1 -1
- package/dist/component/messageTemplates.d.ts +37 -0
- package/dist/component/messageTemplates.d.ts.map +1 -0
- package/dist/component/messageTemplates.js +177 -0
- package/dist/component/messageTemplates.js.map +1 -0
- package/dist/component/pushing.d.ts +14 -14
- package/dist/component/queue.d.ts +33 -0
- package/dist/component/queue.d.ts.map +1 -1
- package/dist/component/queue.js +89 -2
- package/dist/component/queue.js.map +1 -1
- package/dist/component/scheduler.d.ts.map +1 -1
- package/dist/component/scheduler.js +37 -10
- package/dist/component/scheduler.js.map +1 -1
- package/dist/component/schema.d.ts +34 -8
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +14 -0
- package/dist/component/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +69 -2
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +158 -0
- package/src/component/lib.test.ts +216 -1
- package/src/component/lib.ts +8 -0
- package/src/component/messageTemplates.ts +205 -0
- package/src/component/queue.ts +110 -2
- package/src/component/scheduler.ts +77 -14
- package/src/component/schema.ts +15 -0
|
@@ -583,6 +583,20 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
583
583
|
},
|
|
584
584
|
Name
|
|
585
585
|
>;
|
|
586
|
+
createMessageTemplate: FunctionReference<
|
|
587
|
+
"mutation",
|
|
588
|
+
"internal",
|
|
589
|
+
{
|
|
590
|
+
actorUserId: string;
|
|
591
|
+
enabled?: boolean;
|
|
592
|
+
nowMs?: number;
|
|
593
|
+
tags: Array<string>;
|
|
594
|
+
text: string;
|
|
595
|
+
title: string;
|
|
596
|
+
},
|
|
597
|
+
string,
|
|
598
|
+
Name
|
|
599
|
+
>;
|
|
586
600
|
createPairingCode: FunctionReference<
|
|
587
601
|
"mutation",
|
|
588
602
|
"internal",
|
|
@@ -740,6 +754,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
740
754
|
{ message: string; ok: boolean; status: number },
|
|
741
755
|
Name
|
|
742
756
|
>;
|
|
757
|
+
deleteMessageTemplate: FunctionReference<
|
|
758
|
+
"mutation",
|
|
759
|
+
"internal",
|
|
760
|
+
{ templateId: string },
|
|
761
|
+
boolean,
|
|
762
|
+
Name
|
|
763
|
+
>;
|
|
743
764
|
deletePushJob: FunctionReference<
|
|
744
765
|
"mutation",
|
|
745
766
|
"internal",
|
|
@@ -1253,6 +1274,25 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1253
1274
|
{ secretId: string; secretRef: string; version: number },
|
|
1254
1275
|
Name
|
|
1255
1276
|
>;
|
|
1277
|
+
listMessageTemplatesByCompany: FunctionReference<
|
|
1278
|
+
"query",
|
|
1279
|
+
"internal",
|
|
1280
|
+
{ includeDisabled?: boolean; limit?: number },
|
|
1281
|
+
Array<{
|
|
1282
|
+
_id: string;
|
|
1283
|
+
createdAt: number;
|
|
1284
|
+
createdBy: string;
|
|
1285
|
+
enabled: boolean;
|
|
1286
|
+
tags: Array<string>;
|
|
1287
|
+
templateKey: string;
|
|
1288
|
+
text: string;
|
|
1289
|
+
title: string;
|
|
1290
|
+
updatedAt: number;
|
|
1291
|
+
updatedBy: string;
|
|
1292
|
+
usageCount: number;
|
|
1293
|
+
}>,
|
|
1294
|
+
Name
|
|
1295
|
+
>;
|
|
1256
1296
|
listPushDispatchesByJob: FunctionReference<
|
|
1257
1297
|
"query",
|
|
1258
1298
|
"internal",
|
|
@@ -1632,6 +1672,29 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1632
1672
|
},
|
|
1633
1673
|
Name
|
|
1634
1674
|
>;
|
|
1675
|
+
sendMessageTemplateToUserAgent: FunctionReference<
|
|
1676
|
+
"mutation",
|
|
1677
|
+
"internal",
|
|
1678
|
+
{
|
|
1679
|
+
agentKey: string;
|
|
1680
|
+
consumerUserId: string;
|
|
1681
|
+
metadata?: Record<string, string>;
|
|
1682
|
+
nowMs?: number;
|
|
1683
|
+
providerConfig?: {
|
|
1684
|
+
appName: string;
|
|
1685
|
+
image: string;
|
|
1686
|
+
kind: "fly" | "runpod" | "ecs";
|
|
1687
|
+
organizationSlug: string;
|
|
1688
|
+
region: string;
|
|
1689
|
+
volumeName: string;
|
|
1690
|
+
volumePath: string;
|
|
1691
|
+
volumeSizeGb: number;
|
|
1692
|
+
};
|
|
1693
|
+
templateId: string;
|
|
1694
|
+
},
|
|
1695
|
+
{ messageId: string; usageCount: number },
|
|
1696
|
+
Name
|
|
1697
|
+
>;
|
|
1635
1698
|
sendMessageToUserAgent: FunctionReference<
|
|
1636
1699
|
"mutation",
|
|
1637
1700
|
"internal",
|
|
@@ -1736,6 +1799,21 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1736
1799
|
{ enqueuedMessageId: string; runKey: string },
|
|
1737
1800
|
Name
|
|
1738
1801
|
>;
|
|
1802
|
+
updateMessageTemplate: FunctionReference<
|
|
1803
|
+
"mutation",
|
|
1804
|
+
"internal",
|
|
1805
|
+
{
|
|
1806
|
+
actorUserId: string;
|
|
1807
|
+
enabled?: boolean;
|
|
1808
|
+
nowMs?: number;
|
|
1809
|
+
tags?: Array<string>;
|
|
1810
|
+
templateId: string;
|
|
1811
|
+
text?: string;
|
|
1812
|
+
title?: string;
|
|
1813
|
+
},
|
|
1814
|
+
boolean,
|
|
1815
|
+
Name
|
|
1816
|
+
>;
|
|
1739
1817
|
updatePushJob: FunctionReference<
|
|
1740
1818
|
"mutation",
|
|
1741
1819
|
"internal",
|
|
@@ -1817,6 +1895,63 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1817
1895
|
Name
|
|
1818
1896
|
>;
|
|
1819
1897
|
};
|
|
1898
|
+
messageTemplates: {
|
|
1899
|
+
createMessageTemplate: FunctionReference<
|
|
1900
|
+
"mutation",
|
|
1901
|
+
"internal",
|
|
1902
|
+
{
|
|
1903
|
+
actorUserId: string;
|
|
1904
|
+
enabled?: boolean;
|
|
1905
|
+
nowMs?: number;
|
|
1906
|
+
tags: Array<string>;
|
|
1907
|
+
text: string;
|
|
1908
|
+
title: string;
|
|
1909
|
+
},
|
|
1910
|
+
string,
|
|
1911
|
+
Name
|
|
1912
|
+
>;
|
|
1913
|
+
deleteMessageTemplate: FunctionReference<
|
|
1914
|
+
"mutation",
|
|
1915
|
+
"internal",
|
|
1916
|
+
{ templateId: string },
|
|
1917
|
+
boolean,
|
|
1918
|
+
Name
|
|
1919
|
+
>;
|
|
1920
|
+
listMessageTemplatesByCompany: FunctionReference<
|
|
1921
|
+
"query",
|
|
1922
|
+
"internal",
|
|
1923
|
+
{ includeDisabled?: boolean; limit?: number },
|
|
1924
|
+
Array<{
|
|
1925
|
+
_id: string;
|
|
1926
|
+
createdAt: number;
|
|
1927
|
+
createdBy: string;
|
|
1928
|
+
enabled: boolean;
|
|
1929
|
+
tags: Array<string>;
|
|
1930
|
+
templateKey: string;
|
|
1931
|
+
text: string;
|
|
1932
|
+
title: string;
|
|
1933
|
+
updatedAt: number;
|
|
1934
|
+
updatedBy: string;
|
|
1935
|
+
usageCount: number;
|
|
1936
|
+
}>,
|
|
1937
|
+
Name
|
|
1938
|
+
>;
|
|
1939
|
+
updateMessageTemplate: FunctionReference<
|
|
1940
|
+
"mutation",
|
|
1941
|
+
"internal",
|
|
1942
|
+
{
|
|
1943
|
+
actorUserId: string;
|
|
1944
|
+
enabled?: boolean;
|
|
1945
|
+
nowMs?: number;
|
|
1946
|
+
tags?: Array<string>;
|
|
1947
|
+
templateId: string;
|
|
1948
|
+
text?: string;
|
|
1949
|
+
title?: string;
|
|
1950
|
+
},
|
|
1951
|
+
boolean,
|
|
1952
|
+
Name
|
|
1953
|
+
>;
|
|
1954
|
+
};
|
|
1820
1955
|
providers: {
|
|
1821
1956
|
fly: {
|
|
1822
1957
|
deleteFlyVolumeManual: FunctionReference<
|
|
@@ -2949,6 +3084,29 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2949
3084
|
{ requeued: number; unlocked: number },
|
|
2950
3085
|
Name
|
|
2951
3086
|
>;
|
|
3087
|
+
sendMessageTemplateToUserAgent: FunctionReference<
|
|
3088
|
+
"mutation",
|
|
3089
|
+
"internal",
|
|
3090
|
+
{
|
|
3091
|
+
agentKey: string;
|
|
3092
|
+
consumerUserId: string;
|
|
3093
|
+
metadata?: Record<string, string>;
|
|
3094
|
+
nowMs?: number;
|
|
3095
|
+
providerConfig?: {
|
|
3096
|
+
appName: string;
|
|
3097
|
+
image: string;
|
|
3098
|
+
kind: "fly" | "runpod" | "ecs";
|
|
3099
|
+
organizationSlug: string;
|
|
3100
|
+
region: string;
|
|
3101
|
+
volumeName: string;
|
|
3102
|
+
volumePath: string;
|
|
3103
|
+
volumeSizeGb: number;
|
|
3104
|
+
};
|
|
3105
|
+
templateId: string;
|
|
3106
|
+
},
|
|
3107
|
+
{ messageId: string; usageCount: number },
|
|
3108
|
+
Name
|
|
3109
|
+
>;
|
|
2952
3110
|
sendMessageToUserAgent: FunctionReference<
|
|
2953
3111
|
"mutation",
|
|
2954
3112
|
"internal",
|
|
@@ -201,6 +201,131 @@ describe("component lib", () => {
|
|
|
201
201
|
expect(claim?.payload.messageText).toBe("hello");
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
+
test("message templates should normalize tags and auto-generate key", async () => {
|
|
205
|
+
const t = initConvexTest();
|
|
206
|
+
|
|
207
|
+
const templateId = await t.mutation(api.lib.createMessageTemplate, {
|
|
208
|
+
title: " Status Update ",
|
|
209
|
+
text: " Share your latest progress. ",
|
|
210
|
+
tags: [" Urgent ", "follow-up", "urgent", " Team "],
|
|
211
|
+
actorUserId: "user-admin-1",
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(templateId).toBeDefined();
|
|
215
|
+
|
|
216
|
+
const templates = await t.query(api.lib.listMessageTemplatesByCompany, {
|
|
217
|
+
includeDisabled: true,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(templates).toHaveLength(1);
|
|
221
|
+
expect(templates[0]).toMatchObject({
|
|
222
|
+
templateKey: "status-update",
|
|
223
|
+
title: "Status Update",
|
|
224
|
+
text: "Share your latest progress.",
|
|
225
|
+
tags: ["follow-up", "team", "urgent"],
|
|
226
|
+
usageCount: 0,
|
|
227
|
+
enabled: true,
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("message templates should generate a unique key globally", async () => {
|
|
232
|
+
const t = initConvexTest();
|
|
233
|
+
|
|
234
|
+
const firstTemplateId = await t.mutation(api.lib.createMessageTemplate, {
|
|
235
|
+
title: "Daily check-in",
|
|
236
|
+
text: "Share your update.",
|
|
237
|
+
tags: [],
|
|
238
|
+
actorUserId: "user-admin-1",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const secondTemplateId = await t.mutation(api.lib.createMessageTemplate, {
|
|
242
|
+
title: "Daily check-in",
|
|
243
|
+
text: "Duplicate title",
|
|
244
|
+
tags: [],
|
|
245
|
+
actorUserId: "user-admin-2",
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const templates = await t.query(api.lib.listMessageTemplatesByCompany, {
|
|
249
|
+
includeDisabled: true,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(firstTemplateId).toBeDefined();
|
|
253
|
+
expect(secondTemplateId).toBeDefined();
|
|
254
|
+
expect(templates.map((template) => template.templateKey).sort()).toEqual([
|
|
255
|
+
"daily-check-in",
|
|
256
|
+
"daily-check-in-2",
|
|
257
|
+
]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("message templates should track usage and surface most used templates first", async () => {
|
|
261
|
+
const t = initConvexTest();
|
|
262
|
+
await t.mutation(api.queue.upsertAgentProfile, {
|
|
263
|
+
agentKey: "template-agent",
|
|
264
|
+
version: "1.0.0",
|
|
265
|
+
secretsRef: [],
|
|
266
|
+
enabled: true,
|
|
267
|
+
});
|
|
268
|
+
await t.mutation(api.lib.bindUserAgent, {
|
|
269
|
+
consumerUserId: "user-template-1",
|
|
270
|
+
agentKey: "template-agent",
|
|
271
|
+
source: "manual",
|
|
272
|
+
metadata: { companyId: "co-templates" },
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const firstTemplateId = await t.mutation(api.lib.createMessageTemplate, {
|
|
276
|
+
title: "Weekly recap",
|
|
277
|
+
text: "Condividi il recap della settimana.",
|
|
278
|
+
tags: ["weekly", "recap"],
|
|
279
|
+
actorUserId: "user-admin-1",
|
|
280
|
+
});
|
|
281
|
+
const secondTemplateId = await t.mutation(api.lib.createMessageTemplate, {
|
|
282
|
+
title: "Quick nudge",
|
|
283
|
+
text: "Mandami un aggiornamento rapido.",
|
|
284
|
+
tags: ["follow-up"],
|
|
285
|
+
actorUserId: "user-admin-1",
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await t.mutation((api.lib as any).sendMessageTemplateToUserAgent, {
|
|
289
|
+
consumerUserId: "user-template-1",
|
|
290
|
+
agentKey: "template-agent",
|
|
291
|
+
templateId: firstTemplateId,
|
|
292
|
+
});
|
|
293
|
+
await t.mutation((api.lib as any).sendMessageTemplateToUserAgent, {
|
|
294
|
+
consumerUserId: "user-template-1",
|
|
295
|
+
agentKey: "template-agent",
|
|
296
|
+
templateId: firstTemplateId,
|
|
297
|
+
});
|
|
298
|
+
await t.mutation((api.lib as any).sendMessageTemplateToUserAgent, {
|
|
299
|
+
consumerUserId: "user-template-1",
|
|
300
|
+
agentKey: "template-agent",
|
|
301
|
+
templateId: secondTemplateId,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const templates = await t.query(api.lib.listMessageTemplatesByCompany, {
|
|
305
|
+
includeDisabled: true,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(templates.map((template) => ({
|
|
309
|
+
title: template.title,
|
|
310
|
+
usageCount: template.usageCount,
|
|
311
|
+
}))).toEqual([
|
|
312
|
+
{ title: "Weekly recap", usageCount: 2 },
|
|
313
|
+
{ title: "Quick nudge", usageCount: 1 },
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
const queuedItems = await t.query(api.lib.listQueueItemsForUserAgent, {
|
|
317
|
+
consumerUserId: "user-template-1",
|
|
318
|
+
agentKey: "template-agent",
|
|
319
|
+
limit: 10,
|
|
320
|
+
});
|
|
321
|
+
expect(queuedItems).toHaveLength(3);
|
|
322
|
+
expect(queuedItems.map((item) => item.payload.messageText).sort()).toEqual([
|
|
323
|
+
"Condividi il recap della settimana.",
|
|
324
|
+
"Condividi il recap della settimana.",
|
|
325
|
+
"Mandami un aggiornamento rapido.",
|
|
326
|
+
].sort());
|
|
327
|
+
});
|
|
328
|
+
|
|
204
329
|
test("message runtime config should store telegram attachment retention", async () => {
|
|
205
330
|
const t = initConvexTest();
|
|
206
331
|
await t.mutation(api.lib.setMessageRuntimeConfig, {
|
|
@@ -1123,6 +1248,14 @@ describe("component lib", () => {
|
|
|
1123
1248
|
const t = initConvexTest();
|
|
1124
1249
|
const nowMs = Date.UTC(2026, 0, 1, 17, 0, 0);
|
|
1125
1250
|
vi.setSystemTime(nowMs);
|
|
1251
|
+
let machineCreateBody:
|
|
1252
|
+
| {
|
|
1253
|
+
name?: string;
|
|
1254
|
+
config?: {
|
|
1255
|
+
env?: Record<string, string>;
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
| null = null;
|
|
1126
1259
|
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
1127
1260
|
const url = String(input);
|
|
1128
1261
|
const method = init?.method ?? "GET";
|
|
@@ -1148,7 +1281,13 @@ describe("component lib", () => {
|
|
|
1148
1281
|
});
|
|
1149
1282
|
}
|
|
1150
1283
|
if (url.endsWith(`/apps/${TEST_PROVIDER_CONFIG.appName}/machines`) && method === "POST") {
|
|
1151
|
-
const body = JSON.parse(String(init?.body ?? "{}")) as {
|
|
1284
|
+
const body = JSON.parse(String(init?.body ?? "{}")) as {
|
|
1285
|
+
name?: string;
|
|
1286
|
+
config?: {
|
|
1287
|
+
env?: Record<string, string>;
|
|
1288
|
+
};
|
|
1289
|
+
};
|
|
1290
|
+
machineCreateBody = body;
|
|
1152
1291
|
return jsonResponse({
|
|
1153
1292
|
id: "machine-new-worker",
|
|
1154
1293
|
name: body.name,
|
|
@@ -1224,6 +1363,28 @@ describe("component lib", () => {
|
|
|
1224
1363
|
|
|
1225
1364
|
expect(reconcile.spawned).toBe(1);
|
|
1226
1365
|
expect(reconcile.activeWorkers).toBe(2);
|
|
1366
|
+
const machineCreateEnv = (
|
|
1367
|
+
machineCreateBody as
|
|
1368
|
+
| {
|
|
1369
|
+
config?: {
|
|
1370
|
+
env?: Record<string, string>;
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
| null
|
|
1374
|
+
)?.config?.env;
|
|
1375
|
+
expect(machineCreateEnv?.OPENCLAW_CONVERSATION_ID).toBe("telegram:chat:spawn-b");
|
|
1376
|
+
expect(machineCreateEnv?.OPENCLAW_AGENT_KEY).toBe("support-agent");
|
|
1377
|
+
|
|
1378
|
+
const workers = await t.query((internal.queue as any).listWorkersForScheduler, {});
|
|
1379
|
+
const spawnedWorker = workers.find(
|
|
1380
|
+
(row: { workerId: string }) => row.workerId === `afw-${nowMs}-0`,
|
|
1381
|
+
);
|
|
1382
|
+
expect(spawnedWorker?.assignment).toEqual({
|
|
1383
|
+
conversationId: "telegram:chat:spawn-b",
|
|
1384
|
+
agentKey: "support-agent",
|
|
1385
|
+
leaseId: `spawn:afw-${nowMs}-0`,
|
|
1386
|
+
assignedAt: nowMs,
|
|
1387
|
+
});
|
|
1227
1388
|
});
|
|
1228
1389
|
|
|
1229
1390
|
test("scheduler should forward OpenClaw bridge env to spawned machines", async () => {
|
|
@@ -1498,6 +1659,60 @@ describe("component lib", () => {
|
|
|
1498
1659
|
});
|
|
1499
1660
|
});
|
|
1500
1661
|
|
|
1662
|
+
test("preassigned workers should claim only their assigned conversation on first claim", async () => {
|
|
1663
|
+
const t = initConvexTest();
|
|
1664
|
+
const nowMs = Date.UTC(2026, 0, 1, 17, 35, 0);
|
|
1665
|
+
vi.setSystemTime(nowMs);
|
|
1666
|
+
await t.mutation(api.queue.upsertAgentProfile, {
|
|
1667
|
+
agentKey: "support-agent",
|
|
1668
|
+
version: "1.0.0",
|
|
1669
|
+
secretsRef: [],
|
|
1670
|
+
enabled: true,
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
await t.mutation(api.queue.enqueueMessage, {
|
|
1674
|
+
conversationId: "telegram:chat:preassign-a",
|
|
1675
|
+
agentKey: "support-agent",
|
|
1676
|
+
payload: {
|
|
1677
|
+
provider: "telegram",
|
|
1678
|
+
providerUserId: "u-preassign-a",
|
|
1679
|
+
messageText: "first",
|
|
1680
|
+
},
|
|
1681
|
+
nowMs,
|
|
1682
|
+
});
|
|
1683
|
+
const messageB = await t.mutation(api.queue.enqueueMessage, {
|
|
1684
|
+
conversationId: "telegram:chat:preassign-b",
|
|
1685
|
+
agentKey: "support-agent",
|
|
1686
|
+
payload: {
|
|
1687
|
+
provider: "telegram",
|
|
1688
|
+
providerUserId: "u-preassign-b",
|
|
1689
|
+
messageText: "second",
|
|
1690
|
+
},
|
|
1691
|
+
nowMs: nowMs + 1,
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
await t.mutation(internal.queue.upsertWorkerState, {
|
|
1695
|
+
workerId: "worker-preassigned-1",
|
|
1696
|
+
provider: "fly",
|
|
1697
|
+
status: "active",
|
|
1698
|
+
load: 0,
|
|
1699
|
+
nowMs,
|
|
1700
|
+
assignment: {
|
|
1701
|
+
conversationId: "telegram:chat:preassign-b",
|
|
1702
|
+
agentKey: "support-agent",
|
|
1703
|
+
leaseId: "spawn:worker-preassigned-1",
|
|
1704
|
+
assignedAt: nowMs,
|
|
1705
|
+
},
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
const claim = await t.mutation(api.lib.claim, {
|
|
1709
|
+
workerId: "worker-preassigned-1",
|
|
1710
|
+
nowMs: nowMs + 10,
|
|
1711
|
+
});
|
|
1712
|
+
expect(claim?.messageId).toBe(messageB);
|
|
1713
|
+
expect(claim?.conversationId).toBe("telegram:chat:preassign-b");
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1501
1716
|
test("exclusive ownership should block another worker and let the owner reclaim", async () => {
|
|
1502
1717
|
const t = initConvexTest();
|
|
1503
1718
|
const nowMs = Date.UTC(2026, 0, 1, 17, 40, 0);
|
package/src/component/lib.ts
CHANGED
|
@@ -23,6 +23,7 @@ export {
|
|
|
23
23
|
listQueueItemsForUserAgent,
|
|
24
24
|
getConversationViewForUserAgent,
|
|
25
25
|
sendMessageToUserAgent,
|
|
26
|
+
sendMessageTemplateToUserAgent,
|
|
26
27
|
listSnapshotsForConversation,
|
|
27
28
|
listSnapshotsForUserAgent,
|
|
28
29
|
getLatestSnapshotForUserAgent,
|
|
@@ -59,6 +60,13 @@ export {
|
|
|
59
60
|
getWebhookReadiness,
|
|
60
61
|
} from "./identity.js";
|
|
61
62
|
|
|
63
|
+
export {
|
|
64
|
+
createMessageTemplate,
|
|
65
|
+
updateMessageTemplate,
|
|
66
|
+
deleteMessageTemplate,
|
|
67
|
+
listMessageTemplatesByCompany,
|
|
68
|
+
} from "./messageTemplates.js";
|
|
69
|
+
|
|
62
70
|
export {
|
|
63
71
|
createPushTemplate,
|
|
64
72
|
updatePushTemplate,
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "./_generated/server.js";
|
|
3
|
+
import type { MutationCtx } from "./_generated/server.js";
|
|
4
|
+
|
|
5
|
+
const messageTemplateViewValidator = v.object({
|
|
6
|
+
_id: v.id("messageTemplates"),
|
|
7
|
+
templateKey: v.string(),
|
|
8
|
+
title: v.string(),
|
|
9
|
+
text: v.string(),
|
|
10
|
+
tags: v.array(v.string()),
|
|
11
|
+
usageCount: v.number(),
|
|
12
|
+
enabled: v.boolean(),
|
|
13
|
+
createdBy: v.string(),
|
|
14
|
+
updatedBy: v.string(),
|
|
15
|
+
createdAt: v.number(),
|
|
16
|
+
updatedAt: v.number(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function normalizeTemplateKey(value: string) {
|
|
20
|
+
return value.trim().toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildTemplateKeyBase(title: string) {
|
|
24
|
+
const normalized = title
|
|
25
|
+
.trim()
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
28
|
+
.replace(/^-+|-+$/g, "");
|
|
29
|
+
return normalized || "message-template";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeTitle(value: string) {
|
|
33
|
+
return value.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeText(value: string) {
|
|
37
|
+
return value.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeTags(tags: Array<string>) {
|
|
41
|
+
return Array.from(
|
|
42
|
+
new Set(
|
|
43
|
+
tags
|
|
44
|
+
.map((tag) => tag.trim().toLowerCase())
|
|
45
|
+
.filter((tag) => tag.length > 0),
|
|
46
|
+
),
|
|
47
|
+
).sort((left, right) => left.localeCompare(right));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateMessageTemplateFields(input: {
|
|
51
|
+
title?: string;
|
|
52
|
+
text?: string;
|
|
53
|
+
}) {
|
|
54
|
+
if (input.title !== undefined && input.title.length === 0) {
|
|
55
|
+
throw new Error("Template title is required");
|
|
56
|
+
}
|
|
57
|
+
if (input.text !== undefined && input.text.length === 0) {
|
|
58
|
+
throw new Error("Template text is required");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function resolveUniqueTemplateKey(
|
|
63
|
+
ctx: MutationCtx,
|
|
64
|
+
title: string,
|
|
65
|
+
currentTemplateId?: string,
|
|
66
|
+
) {
|
|
67
|
+
const baseKey = buildTemplateKeyBase(title);
|
|
68
|
+
let candidate = baseKey;
|
|
69
|
+
let suffix = 2;
|
|
70
|
+
|
|
71
|
+
while (true) {
|
|
72
|
+
const existing = await ctx.db
|
|
73
|
+
.query("messageTemplates")
|
|
74
|
+
.withIndex("by_templateKey", (q) => q.eq("templateKey", candidate))
|
|
75
|
+
.unique();
|
|
76
|
+
if (!existing || String(existing._id) === currentTemplateId) {
|
|
77
|
+
return candidate;
|
|
78
|
+
}
|
|
79
|
+
candidate = `${baseKey}-${suffix}`;
|
|
80
|
+
suffix += 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const createMessageTemplate = mutation({
|
|
85
|
+
args: {
|
|
86
|
+
title: v.string(),
|
|
87
|
+
text: v.string(),
|
|
88
|
+
tags: v.array(v.string()),
|
|
89
|
+
enabled: v.optional(v.boolean()),
|
|
90
|
+
actorUserId: v.string(),
|
|
91
|
+
nowMs: v.optional(v.number()),
|
|
92
|
+
},
|
|
93
|
+
returns: v.id("messageTemplates"),
|
|
94
|
+
handler: async (ctx, args) => {
|
|
95
|
+
const title = normalizeTitle(args.title);
|
|
96
|
+
const text = normalizeText(args.text);
|
|
97
|
+
const tags = normalizeTags(args.tags);
|
|
98
|
+
validateMessageTemplateFields({ title, text });
|
|
99
|
+
const templateKey = normalizeTemplateKey(await resolveUniqueTemplateKey(ctx, title));
|
|
100
|
+
|
|
101
|
+
const nowMs = args.nowMs ?? Date.now();
|
|
102
|
+
return await ctx.db.insert("messageTemplates", {
|
|
103
|
+
templateKey,
|
|
104
|
+
title,
|
|
105
|
+
text,
|
|
106
|
+
tags,
|
|
107
|
+
usageCount: 0,
|
|
108
|
+
enabled: args.enabled ?? true,
|
|
109
|
+
createdBy: args.actorUserId,
|
|
110
|
+
updatedBy: args.actorUserId,
|
|
111
|
+
createdAt: nowMs,
|
|
112
|
+
updatedAt: nowMs,
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
export const updateMessageTemplate = mutation({
|
|
118
|
+
args: {
|
|
119
|
+
templateId: v.id("messageTemplates"),
|
|
120
|
+
title: v.optional(v.string()),
|
|
121
|
+
text: v.optional(v.string()),
|
|
122
|
+
tags: v.optional(v.array(v.string())),
|
|
123
|
+
enabled: v.optional(v.boolean()),
|
|
124
|
+
actorUserId: v.string(),
|
|
125
|
+
nowMs: v.optional(v.number()),
|
|
126
|
+
},
|
|
127
|
+
returns: v.boolean(),
|
|
128
|
+
handler: async (ctx, args) => {
|
|
129
|
+
const template = await ctx.db.get(args.templateId);
|
|
130
|
+
if (!template) return false;
|
|
131
|
+
|
|
132
|
+
const title = args.title !== undefined ? normalizeTitle(args.title) : template.title;
|
|
133
|
+
const text = args.text !== undefined ? normalizeText(args.text) : template.text;
|
|
134
|
+
const tags = args.tags !== undefined ? normalizeTags(args.tags) : template.tags;
|
|
135
|
+
validateMessageTemplateFields({ title, text });
|
|
136
|
+
const templateKey =
|
|
137
|
+
title !== template.title
|
|
138
|
+
? normalizeTemplateKey(await resolveUniqueTemplateKey(ctx, title, String(template._id)))
|
|
139
|
+
: template.templateKey;
|
|
140
|
+
|
|
141
|
+
const nowMs = args.nowMs ?? Date.now();
|
|
142
|
+
await ctx.db.patch(template._id, {
|
|
143
|
+
templateKey,
|
|
144
|
+
title,
|
|
145
|
+
text,
|
|
146
|
+
tags,
|
|
147
|
+
enabled: args.enabled ?? template.enabled,
|
|
148
|
+
updatedBy: args.actorUserId,
|
|
149
|
+
updatedAt: nowMs,
|
|
150
|
+
});
|
|
151
|
+
return true;
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
export const deleteMessageTemplate = mutation({
|
|
156
|
+
args: {
|
|
157
|
+
templateId: v.id("messageTemplates"),
|
|
158
|
+
},
|
|
159
|
+
returns: v.boolean(),
|
|
160
|
+
handler: async (ctx, args) => {
|
|
161
|
+
const template = await ctx.db.get(args.templateId);
|
|
162
|
+
if (!template) return false;
|
|
163
|
+
await ctx.db.delete(template._id);
|
|
164
|
+
return true;
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
export const listMessageTemplatesByCompany = query({
|
|
169
|
+
args: {
|
|
170
|
+
includeDisabled: v.optional(v.boolean()),
|
|
171
|
+
limit: v.optional(v.number()),
|
|
172
|
+
},
|
|
173
|
+
returns: v.array(messageTemplateViewValidator),
|
|
174
|
+
handler: async (ctx, args) => {
|
|
175
|
+
const includeDisabled = args.includeDisabled ?? false;
|
|
176
|
+
const limit = Math.max(1, Math.min(args.limit ?? 100, 200));
|
|
177
|
+
|
|
178
|
+
const rows = includeDisabled
|
|
179
|
+
? await ctx.db.query("messageTemplates").take(limit)
|
|
180
|
+
: await ctx.db
|
|
181
|
+
.query("messageTemplates")
|
|
182
|
+
.withIndex("by_enabled", (q) => q.eq("enabled", true))
|
|
183
|
+
.take(limit);
|
|
184
|
+
|
|
185
|
+
rows.sort((left, right) => {
|
|
186
|
+
if (right.usageCount !== left.usageCount) {
|
|
187
|
+
return right.usageCount - left.usageCount;
|
|
188
|
+
}
|
|
189
|
+
return right.updatedAt - left.updatedAt;
|
|
190
|
+
});
|
|
191
|
+
return rows.map((row) => ({
|
|
192
|
+
_id: row._id,
|
|
193
|
+
templateKey: row.templateKey,
|
|
194
|
+
title: row.title,
|
|
195
|
+
text: row.text,
|
|
196
|
+
tags: row.tags,
|
|
197
|
+
usageCount: row.usageCount,
|
|
198
|
+
enabled: row.enabled,
|
|
199
|
+
createdBy: row.createdBy,
|
|
200
|
+
updatedBy: row.updatedBy,
|
|
201
|
+
createdAt: row.createdAt,
|
|
202
|
+
updatedAt: row.updatedAt,
|
|
203
|
+
}));
|
|
204
|
+
},
|
|
205
|
+
});
|