@okrlinkhub/agent-factory 2.0.0 → 2.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 +88 -54
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +90 -37
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +157 -8
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/identity.d.ts +4 -4
- package/dist/component/pushing.d.ts +27 -27
- package/dist/component/queue.d.ts +145 -50
- package/dist/component/queue.d.ts.map +1 -1
- package/dist/component/queue.js +231 -16
- package/dist/component/queue.js.map +1 -1
- package/dist/component/scheduler.d.ts +10 -10
- package/dist/component/scheduler.js +4 -0
- package/dist/component/scheduler.js.map +1 -1
- package/dist/component/schema.d.ts +164 -80
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +29 -0
- package/dist/component/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +171 -42
- package/src/component/_generated/component.ts +189 -14
- package/src/component/lib.test.ts +132 -0
- package/src/component/queue.ts +356 -22
- package/src/component/scheduler.ts +4 -0
- package/src/component/schema.ts +46 -0
package/dist/component/queue.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { internal } from "./_generated/api.js";
|
|
3
|
-
import { internalMutation, internalQuery, mutation, query, } from "./_generated/server.js";
|
|
2
|
+
import { api, internal } from "./_generated/api.js";
|
|
3
|
+
import { action, internalMutation, internalQuery, mutation, query, } from "./_generated/server.js";
|
|
4
4
|
import { computeRetryDelayMs, DEFAULT_CONFIG, providerConfigValidator } from "./config.js";
|
|
5
5
|
import { canTransitionWorkerStatus, isWorkerClaimable, isWorkerRunning, isWorkerTerminal, workerStatusValidator, } from "./workerLifecycle.js";
|
|
6
6
|
const queueStatusValidator = v.union(v.literal("queued"), v.literal("processing"), v.literal("done"), v.literal("failed"), v.literal("dead_letter"));
|
|
7
|
+
const telegramAttachmentKindValidator = v.union(v.literal("photo"), v.literal("video"), v.literal("audio"), v.literal("voice"), v.literal("document"));
|
|
8
|
+
const telegramAttachmentStatusValidator = v.union(v.literal("ready"), v.literal("expired"));
|
|
9
|
+
const telegramAttachmentValidator = v.object({
|
|
10
|
+
kind: telegramAttachmentKindValidator,
|
|
11
|
+
status: telegramAttachmentStatusValidator,
|
|
12
|
+
storageId: v.id("_storage"),
|
|
13
|
+
telegramFileId: v.string(),
|
|
14
|
+
fileName: v.optional(v.string()),
|
|
15
|
+
mimeType: v.optional(v.string()),
|
|
16
|
+
sizeBytes: v.optional(v.number()),
|
|
17
|
+
expiresAt: v.number(),
|
|
18
|
+
});
|
|
19
|
+
const telegramAttachmentCandidateValidator = v.object({
|
|
20
|
+
kind: telegramAttachmentKindValidator,
|
|
21
|
+
telegramFileId: v.string(),
|
|
22
|
+
fileName: v.optional(v.string()),
|
|
23
|
+
mimeType: v.optional(v.string()),
|
|
24
|
+
sizeBytes: v.optional(v.number()),
|
|
25
|
+
});
|
|
7
26
|
const queuePayloadValidator = v.object({
|
|
8
27
|
provider: v.string(),
|
|
9
28
|
providerUserId: v.string(),
|
|
@@ -11,6 +30,7 @@ const queuePayloadValidator = v.object({
|
|
|
11
30
|
externalMessageId: v.optional(v.string()),
|
|
12
31
|
rawUpdateJson: v.optional(v.string()),
|
|
13
32
|
metadata: v.optional(v.record(v.string(), v.string())),
|
|
33
|
+
attachments: v.optional(v.array(telegramAttachmentValidator)),
|
|
14
34
|
});
|
|
15
35
|
const snapshotReasonValidator = v.union(v.literal("drain"), v.literal("signal"), v.literal("manual"));
|
|
16
36
|
const DATA_SNAPSHOT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -102,6 +122,7 @@ const workerSpawnOpenClawEnvValidator = v.object({
|
|
|
102
122
|
});
|
|
103
123
|
const messageRuntimeConfigValidator = v.object({
|
|
104
124
|
systemPrompt: v.optional(v.string()),
|
|
125
|
+
telegramAttachmentRetentionMs: v.optional(v.number()),
|
|
105
126
|
});
|
|
106
127
|
const globalSkillStatusValidator = v.union(v.literal("active"), v.literal("disabled"));
|
|
107
128
|
const globalSkillReleaseChannelValidator = v.union(v.literal("stable"), v.literal("canary"));
|
|
@@ -133,6 +154,7 @@ const RUNTIME_CONFIG_KEYS = {
|
|
|
133
154
|
provider: "provider",
|
|
134
155
|
message: "message",
|
|
135
156
|
};
|
|
157
|
+
const DEFAULT_TELEGRAM_ATTACHMENT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
136
158
|
export const enqueueMessage = mutation({
|
|
137
159
|
args: {
|
|
138
160
|
conversationId: v.string(),
|
|
@@ -435,6 +457,47 @@ export const getMessageRuntimeConfig = internalQuery({
|
|
|
435
457
|
return row.messageConfig;
|
|
436
458
|
},
|
|
437
459
|
});
|
|
460
|
+
export const getTelegramIngressRuntimeConfig = internalQuery({
|
|
461
|
+
args: {
|
|
462
|
+
agentKey: v.string(),
|
|
463
|
+
},
|
|
464
|
+
returns: v.object({
|
|
465
|
+
botToken: v.union(v.null(), v.string()),
|
|
466
|
+
attachmentRetentionMs: v.number(),
|
|
467
|
+
}),
|
|
468
|
+
handler: async (ctx, args) => {
|
|
469
|
+
const profile = await ctx.db
|
|
470
|
+
.query("agentProfiles")
|
|
471
|
+
.withIndex("by_agentKey", (q) => q.eq("agentKey", args.agentKey))
|
|
472
|
+
.unique();
|
|
473
|
+
const botToken = profile ? await resolveActiveTelegramBotToken(ctx, profile.secretsRef) : null;
|
|
474
|
+
const row = await ctx.db
|
|
475
|
+
.query("runtimeConfig")
|
|
476
|
+
.withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.message))
|
|
477
|
+
.unique();
|
|
478
|
+
return {
|
|
479
|
+
botToken,
|
|
480
|
+
attachmentRetentionMs: resolveTelegramAttachmentRetentionMs(row?.messageConfig?.telegramAttachmentRetentionMs),
|
|
481
|
+
};
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
export const prepareTelegramAttachmentsForEnqueue = action({
|
|
485
|
+
args: {
|
|
486
|
+
agentKey: v.string(),
|
|
487
|
+
attachments: v.array(telegramAttachmentCandidateValidator),
|
|
488
|
+
},
|
|
489
|
+
returns: v.array(telegramAttachmentValidator),
|
|
490
|
+
handler: async (ctx, args) => {
|
|
491
|
+
const ingressConfig = await ctx.runQuery(internal.queue.getTelegramIngressRuntimeConfig, {
|
|
492
|
+
agentKey: args.agentKey,
|
|
493
|
+
});
|
|
494
|
+
if (!ingressConfig.botToken) {
|
|
495
|
+
throw new Error(`missing active telegram bot token for agent '${args.agentKey}'`);
|
|
496
|
+
}
|
|
497
|
+
const expiresAt = Date.now() + ingressConfig.attachmentRetentionMs;
|
|
498
|
+
return await Promise.all(args.attachments.map((attachment) => persistTelegramAttachmentFromCandidate(ctx, ingressConfig.botToken, attachment, expiresAt)));
|
|
499
|
+
},
|
|
500
|
+
});
|
|
438
501
|
export const upsertMessageRuntimeConfig = internalMutation({
|
|
439
502
|
args: {
|
|
440
503
|
messageConfig: messageRuntimeConfigValidator,
|
|
@@ -1407,18 +1470,7 @@ export const getHydrationBundleForClaimedJob = query({
|
|
|
1407
1470
|
.query("conversationHydrationCache")
|
|
1408
1471
|
.withIndex("by_conversationId", (q) => q.eq("conversationId", message.conversationId))
|
|
1409
1472
|
.first();
|
|
1410
|
-
|
|
1411
|
-
const telegramSecretRefs = profile.secretsRef.filter((ref) => ref === "telegram.botToken" || ref.startsWith("telegram.botToken."));
|
|
1412
|
-
for (const telegramSecretRef of telegramSecretRefs) {
|
|
1413
|
-
const activeSecret = await ctx.db
|
|
1414
|
-
.query("secrets")
|
|
1415
|
-
.withIndex("by_secretRef_and_active", (q) => q.eq("secretRef", telegramSecretRef).eq("active", true))
|
|
1416
|
-
.unique();
|
|
1417
|
-
if (activeSecret) {
|
|
1418
|
-
telegramBotToken = decryptSecretValue(activeSecret.encryptedValue, activeSecret.algorithm);
|
|
1419
|
-
break;
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1473
|
+
const telegramBotToken = await resolveActiveTelegramBotToken(ctx, profile.secretsRef);
|
|
1422
1474
|
const contextHistory = conversationCache && conversationCache.snapshotKey === snapshotKey
|
|
1423
1475
|
? conversationCache.deltaContext
|
|
1424
1476
|
: conversation.contextHistory;
|
|
@@ -2050,6 +2102,42 @@ export const expireOldDataSnapshots = internalMutation({
|
|
|
2050
2102
|
return rows.length;
|
|
2051
2103
|
},
|
|
2052
2104
|
});
|
|
2105
|
+
export const expireOldTelegramAttachments = internalMutation({
|
|
2106
|
+
args: {
|
|
2107
|
+
nowMs: v.optional(v.number()),
|
|
2108
|
+
limit: v.optional(v.number()),
|
|
2109
|
+
},
|
|
2110
|
+
returns: v.number(),
|
|
2111
|
+
handler: async (ctx, args) => {
|
|
2112
|
+
const nowMs = args.nowMs ?? Date.now();
|
|
2113
|
+
const limit = args.limit ?? 100;
|
|
2114
|
+
const rows = await ctx.db
|
|
2115
|
+
.query("messageAttachments")
|
|
2116
|
+
.withIndex("by_status_and_expiresAt", (q) => q.eq("status", "ready").lte("expiresAt", nowMs))
|
|
2117
|
+
.take(limit);
|
|
2118
|
+
for (const row of rows) {
|
|
2119
|
+
await ctx.db.patch(row._id, {
|
|
2120
|
+
status: "expired",
|
|
2121
|
+
});
|
|
2122
|
+
const message = await ctx.db.get(row.messageId);
|
|
2123
|
+
if (message?.payload.attachments?.length) {
|
|
2124
|
+
await ctx.db.patch(message._id, {
|
|
2125
|
+
payload: {
|
|
2126
|
+
...message.payload,
|
|
2127
|
+
attachments: message.payload.attachments.map((attachment) => attachment.storageId === row.storageId
|
|
2128
|
+
? {
|
|
2129
|
+
...attachment,
|
|
2130
|
+
status: "expired",
|
|
2131
|
+
}
|
|
2132
|
+
: attachment),
|
|
2133
|
+
},
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
await ctx.storage.delete?.(row.storageId);
|
|
2137
|
+
}
|
|
2138
|
+
return rows.length;
|
|
2139
|
+
},
|
|
2140
|
+
});
|
|
2053
2141
|
export const getWorkerStats = query({
|
|
2054
2142
|
args: {},
|
|
2055
2143
|
returns: v.object({
|
|
@@ -2218,6 +2306,23 @@ async function enqueueMessageRecord(ctx, args) {
|
|
|
2218
2306
|
attempts: 0,
|
|
2219
2307
|
maxAttempts: args.maxAttempts ?? DEFAULT_CONFIG.retry.maxAttempts,
|
|
2220
2308
|
});
|
|
2309
|
+
for (const attachment of payload.attachments ?? []) {
|
|
2310
|
+
await ctx.db.insert("messageAttachments", {
|
|
2311
|
+
messageId,
|
|
2312
|
+
conversationId: args.conversationId,
|
|
2313
|
+
agentKey: args.agentKey,
|
|
2314
|
+
provider: payload.provider,
|
|
2315
|
+
kind: attachment.kind,
|
|
2316
|
+
status: attachment.status,
|
|
2317
|
+
storageId: attachment.storageId,
|
|
2318
|
+
telegramFileId: attachment.telegramFileId,
|
|
2319
|
+
fileName: attachment.fileName,
|
|
2320
|
+
mimeType: attachment.mimeType,
|
|
2321
|
+
sizeBytes: attachment.sizeBytes,
|
|
2322
|
+
createdAt: nowMs,
|
|
2323
|
+
expiresAt: attachment.expiresAt,
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2221
2326
|
try {
|
|
2222
2327
|
await ctx.scheduler.runAfter(0, internal.scheduler.reconcileWorkerPoolFromEnqueue, {
|
|
2223
2328
|
workspaceId: "default",
|
|
@@ -2354,10 +2459,16 @@ function dedupeMessagesById(messages) {
|
|
|
2354
2459
|
}
|
|
2355
2460
|
function normalizeMessageRuntimeConfig(messageConfig) {
|
|
2356
2461
|
const systemPrompt = normalizeSystemPrompt(messageConfig?.systemPrompt);
|
|
2357
|
-
|
|
2462
|
+
const telegramAttachmentRetentionMs = normalizeTelegramAttachmentRetentionMs(messageConfig?.telegramAttachmentRetentionMs);
|
|
2463
|
+
if (systemPrompt === null && telegramAttachmentRetentionMs === undefined) {
|
|
2358
2464
|
return null;
|
|
2359
2465
|
}
|
|
2360
|
-
return {
|
|
2466
|
+
return {
|
|
2467
|
+
...(systemPrompt === null ? {} : { systemPrompt }),
|
|
2468
|
+
...(telegramAttachmentRetentionMs === undefined
|
|
2469
|
+
? {}
|
|
2470
|
+
: { telegramAttachmentRetentionMs }),
|
|
2471
|
+
};
|
|
2361
2472
|
}
|
|
2362
2473
|
function normalizeSystemPrompt(systemPrompt) {
|
|
2363
2474
|
if (typeof systemPrompt !== "string") {
|
|
@@ -2366,6 +2477,110 @@ function normalizeSystemPrompt(systemPrompt) {
|
|
|
2366
2477
|
const normalizedSystemPrompt = systemPrompt.trim();
|
|
2367
2478
|
return normalizedSystemPrompt.length > 0 ? normalizedSystemPrompt : null;
|
|
2368
2479
|
}
|
|
2480
|
+
function normalizeTelegramAttachmentRetentionMs(retentionMs) {
|
|
2481
|
+
if (typeof retentionMs !== "number" || !Number.isFinite(retentionMs)) {
|
|
2482
|
+
return undefined;
|
|
2483
|
+
}
|
|
2484
|
+
const normalizedRetentionMs = Math.floor(retentionMs);
|
|
2485
|
+
if (normalizedRetentionMs <= 0) {
|
|
2486
|
+
return undefined;
|
|
2487
|
+
}
|
|
2488
|
+
return normalizedRetentionMs;
|
|
2489
|
+
}
|
|
2490
|
+
function resolveTelegramAttachmentRetentionMs(retentionMs) {
|
|
2491
|
+
return (normalizeTelegramAttachmentRetentionMs(retentionMs) ??
|
|
2492
|
+
DEFAULT_TELEGRAM_ATTACHMENT_RETENTION_MS);
|
|
2493
|
+
}
|
|
2494
|
+
async function resolveActiveTelegramBotToken(ctx, secretRefs) {
|
|
2495
|
+
const telegramSecretRefs = secretRefs.filter((ref) => ref === "telegram.botToken" || ref.startsWith("telegram.botToken."));
|
|
2496
|
+
for (const telegramSecretRef of telegramSecretRefs) {
|
|
2497
|
+
const activeSecret = await ctx.db
|
|
2498
|
+
.query("secrets")
|
|
2499
|
+
.withIndex("by_secretRef_and_active", (q) => q.eq("secretRef", telegramSecretRef).eq("active", true))
|
|
2500
|
+
.unique();
|
|
2501
|
+
if (activeSecret) {
|
|
2502
|
+
return decryptSecretValue(activeSecret.encryptedValue, activeSecret.algorithm);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
return null;
|
|
2506
|
+
}
|
|
2507
|
+
async function persistTelegramAttachmentFromCandidate(ctx, telegramBotToken, attachment, expiresAt) {
|
|
2508
|
+
const filePath = await fetchTelegramFilePath(telegramBotToken, attachment.telegramFileId);
|
|
2509
|
+
const downloaded = await downloadTelegramFile(telegramBotToken, filePath);
|
|
2510
|
+
const uploadTarget = await ctx.runMutation(api.queue.generateMediaUploadUrl, {});
|
|
2511
|
+
const uploadResponse = await fetch(uploadTarget.uploadUrl, {
|
|
2512
|
+
method: "POST",
|
|
2513
|
+
headers: downloaded.mimeType.length > 0
|
|
2514
|
+
? {
|
|
2515
|
+
"Content-Type": downloaded.mimeType,
|
|
2516
|
+
}
|
|
2517
|
+
: undefined,
|
|
2518
|
+
body: downloaded.blob,
|
|
2519
|
+
});
|
|
2520
|
+
const uploadPayload = (await uploadResponse.json().catch(() => ({})));
|
|
2521
|
+
if (!uploadResponse.ok || !uploadPayload.storageId) {
|
|
2522
|
+
throw new Error(`Convex storage upload failed for Telegram ${attachment.kind} attachment`);
|
|
2523
|
+
}
|
|
2524
|
+
return {
|
|
2525
|
+
kind: attachment.kind,
|
|
2526
|
+
status: "ready",
|
|
2527
|
+
storageId: uploadPayload.storageId,
|
|
2528
|
+
telegramFileId: attachment.telegramFileId,
|
|
2529
|
+
fileName: attachment.fileName,
|
|
2530
|
+
mimeType: downloaded.mimeType || attachment.mimeType,
|
|
2531
|
+
sizeBytes: attachment.sizeBytes ?? downloaded.blob.size,
|
|
2532
|
+
expiresAt,
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
async function fetchTelegramFilePath(telegramBotToken, telegramFileId) {
|
|
2536
|
+
const telegramApiBaseUrl = `https://api.telegram.org/bot${encodeURIComponent(telegramBotToken)}`;
|
|
2537
|
+
const response = await fetch(`${telegramApiBaseUrl}/getFile?file_id=${encodeURIComponent(telegramFileId)}`);
|
|
2538
|
+
const payload = (await response.json().catch(() => ({})));
|
|
2539
|
+
if (!response.ok || payload.ok !== true || typeof payload.result?.file_path !== "string") {
|
|
2540
|
+
throw new Error(`Telegram getFile failed: ${typeof payload.description === "string" ? payload.description : "missing file_path"}`);
|
|
2541
|
+
}
|
|
2542
|
+
return payload.result.file_path;
|
|
2543
|
+
}
|
|
2544
|
+
async function downloadTelegramFile(telegramBotToken, filePath) {
|
|
2545
|
+
const response = await fetch(`https://api.telegram.org/file/bot${encodeURIComponent(telegramBotToken)}/${filePath}`);
|
|
2546
|
+
if (!response.ok) {
|
|
2547
|
+
throw new Error(`Telegram file download failed with status ${response.status}`);
|
|
2548
|
+
}
|
|
2549
|
+
const blob = await response.blob();
|
|
2550
|
+
const mimeType = response.headers.get("Content-Type") ??
|
|
2551
|
+
blob.type ??
|
|
2552
|
+
inferMimeTypeFromFilePath(filePath) ??
|
|
2553
|
+
"application/octet-stream";
|
|
2554
|
+
return {
|
|
2555
|
+
blob: mimeType === blob.type ? blob : new Blob([await blob.arrayBuffer()], { type: mimeType }),
|
|
2556
|
+
mimeType,
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
function inferMimeTypeFromFilePath(filePath) {
|
|
2560
|
+
const normalizedPath = filePath.toLowerCase();
|
|
2561
|
+
if (normalizedPath.endsWith(".jpg") || normalizedPath.endsWith(".jpeg")) {
|
|
2562
|
+
return "image/jpeg";
|
|
2563
|
+
}
|
|
2564
|
+
if (normalizedPath.endsWith(".png")) {
|
|
2565
|
+
return "image/png";
|
|
2566
|
+
}
|
|
2567
|
+
if (normalizedPath.endsWith(".webp")) {
|
|
2568
|
+
return "image/webp";
|
|
2569
|
+
}
|
|
2570
|
+
if (normalizedPath.endsWith(".mp4")) {
|
|
2571
|
+
return "video/mp4";
|
|
2572
|
+
}
|
|
2573
|
+
if (normalizedPath.endsWith(".mp3")) {
|
|
2574
|
+
return "audio/mpeg";
|
|
2575
|
+
}
|
|
2576
|
+
if (normalizedPath.endsWith(".ogg")) {
|
|
2577
|
+
return "audio/ogg";
|
|
2578
|
+
}
|
|
2579
|
+
if (normalizedPath.endsWith(".pdf")) {
|
|
2580
|
+
return "application/pdf";
|
|
2581
|
+
}
|
|
2582
|
+
return null;
|
|
2583
|
+
}
|
|
2369
2584
|
async function buildGlobalSkillMaterialization(skill) {
|
|
2370
2585
|
const skillDirName = normalizeGlobalSkillDirName(skill.slug);
|
|
2371
2586
|
const scriptExt = skill.moduleFormat === "cjs" ? "cjs" : "mjs";
|