ofiere-openclaw-plugin 4.23.0 → 4.25.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/package.json +1 -1
- package/src/prompt.ts +10 -3
- package/src/tools.ts +208 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.25.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin for Ofiere PM - 13 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, and SOP management",
|
|
6
6
|
"keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
|
package/src/prompt.ts
CHANGED
|
@@ -62,14 +62,20 @@ const TOOL_DOCS: Record<string, string> = {
|
|
|
62
62
|
- Edge handles: condition edges use sourceHandle "condition-true"/"condition-false". Loop edges use "loop_body"/"done"
|
|
63
63
|
- Variables: Use {{prev.nodeId.outputText}} for prior outputs, {{variables.key}} for stored variables`,
|
|
64
64
|
|
|
65
|
-
OFIERE_NOTIFY_OPS: `- **OFIERE_NOTIFY_OPS** — Notifications & Channel Reports (action: "list", "mark_read", "mark_all_read", "delete", "send_report", "schedule_report", "list_schedules", "delete_schedule")
|
|
65
|
+
OFIERE_NOTIFY_OPS: `- **OFIERE_NOTIFY_OPS** — Notifications & Channel Reports (action: "list", "mark_read", "mark_all_read", "delete", "send_report", "get_task_detail", "schedule_report", "list_schedules", "delete_schedule")
|
|
66
66
|
- list: Recent notifications. unread_only=true for unread only
|
|
67
67
|
- mark_read: Mark one notification read by ID
|
|
68
68
|
- mark_all_read: Mark all as read
|
|
69
|
-
- send_report: Send a PM progress report to YOUR connected channels (Telegram, Discord, Slack, etc.)
|
|
69
|
+
- send_report: Send a numbered PM progress report to YOUR connected channels (Telegram, Discord, Slack, etc.)
|
|
70
70
|
- Required: scope_type (space|folder|project|task|all), agent_id (YOUR name, e.g. "thalia")
|
|
71
71
|
- Optional: scope_id, channel_types[] (filter to specific channels), include_completed
|
|
72
72
|
- The report is automatically generated from current PM data and sent through YOUR channel bindings ONLY
|
|
73
|
+
- Tasks are numbered globally (1, 2, 3...) across Active/Pending/Done sections. Recurring tasks that executed recently appear in BOTH Done (today's run) and Pending (next cycle)
|
|
74
|
+
- get_task_detail: Get the FULL result/content of a task by its report number
|
|
75
|
+
- Required: task_number (1-indexed number from the report)
|
|
76
|
+
- Optional: scope_type (default all), scope_id
|
|
77
|
+
- Returns: complete task description (where execution results live), custom_fields, status, tags, and all metadata
|
|
78
|
+
- Use when user asks "details on number X", "what's task 2 result", "show me #3", etc.
|
|
73
79
|
- schedule_report: Create a recurring/scheduled report
|
|
74
80
|
- Required: scope_type, recurrence_type (hourly|daily|weekly|monthly), agent_id (YOUR name)
|
|
75
81
|
- Optional: scope_id, scope_label, channel_types[], recurrence_time (HH:MM UTC, default 09:00), recurrence_interval, recurrence_days_of_week (e.g. "mon,wed,fri"), include_completed
|
|
@@ -202,7 +208,8 @@ ${toolDocs}
|
|
|
202
208
|
- When task instructions or system prompts contain file references like @[filename](file:FILE_ID), use OFIERE_FILE_OPS action:"read_text_file" file_id:"FILE_ID" to read the file content. Do NOT ask the user for the file — retrieve it yourself.
|
|
203
209
|
- Use OFIERE_FILE_OPS to create output files (reports, data, configs) in the Space Files explorer. Prefer create_text_file for text-based outputs.
|
|
204
210
|
- To save task output as a file, call OFIERE_FILE_OPS action:"create_text_file" with the space_id from the task context.
|
|
205
|
-
- CHANNEL REPORTS: When the user asks you to "send a report", "send progress", "update me on Telegram/Discord/Slack/WhatsApp", use OFIERE_NOTIFY_OPS action:"send_report" with agent_id set to YOUR name. ALWAYS include agent_id — without it the report will fail. The report is generated from live PM data and sent through YOUR connected channels ONLY — not other agents' channels.
|
|
211
|
+
- CHANNEL REPORTS: When the user asks you to "send a report", "send progress", "update me on Telegram/Discord/Slack/WhatsApp", use OFIERE_NOTIFY_OPS action:"send_report" with agent_id set to YOUR name. ALWAYS include agent_id — without it the report will fail. The report is generated from live PM data and sent through YOUR connected channels ONLY — not other agents' channels. Tasks are numbered (1, 2, 3...) for easy reference.
|
|
212
|
+
- TASK DRILL-DOWN: When the user asks "details on number X", "what's task X result", "show me #X", or any variation referencing a numbered task from a report, use OFIERE_NOTIFY_OPS action:"get_task_detail" with task_number set to the number they mentioned. The description field contains the actual execution result (e.g. scan findings, analysis output). Present the description content as the primary response — that IS the task result.
|
|
206
213
|
- To set up recurring reports (e.g. "send me a daily report at 9am"), use OFIERE_NOTIFY_OPS action:"schedule_report" with scope_type, recurrence_type, and agent_id set to YOUR name.
|
|
207
214
|
- If a report is too long for the channel's message limit, save the full report as a markdown file using OFIERE_FILE_OPS action:"create_text_file" and send a summary to the channel instead.
|
|
208
215
|
- PLANNING WORKFLOW: Use OFIERE_PLAN_OPS to build complex multi-step execution flows BEFORE creating individual tasks. Build the plan → let the user review in the Planning Tab → execute when approved.
|
package/src/tools.ts
CHANGED
|
@@ -2156,9 +2156,13 @@ function registerNotifyOps(
|
|
|
2156
2156
|
`- "mark_read": Mark one as read. Required: id\n` +
|
|
2157
2157
|
`- "mark_all_read": Mark all as read\n` +
|
|
2158
2158
|
`- "delete": Delete a notification. Required: id\n` +
|
|
2159
|
-
`- "send_report": Send a PM progress report to YOUR connected channels (Telegram, Discord, etc.)\n` +
|
|
2159
|
+
`- "send_report": Send a numbered PM progress report to YOUR connected channels (Telegram, Discord, etc.)\n` +
|
|
2160
2160
|
` Required: scope_type (space|folder|project|task|all), agent_id (YOUR name e.g. "thalia")\n` +
|
|
2161
2161
|
` Optional: scope_id, channel_types[] (filter specific channels), include_completed (default true)\n` +
|
|
2162
|
+
` Tasks are numbered globally (1, 2, 3...) across Active/Pending/Done sections for easy reference\n` +
|
|
2163
|
+
`- "get_task_detail": Get the FULL result/content of a task by its report number. Use when user asks "details on number X"\n` +
|
|
2164
|
+
` Required: task_number (1-indexed from the report). Optional: scope_type, scope_id (defaults to all)\n` +
|
|
2165
|
+
` Returns the complete task description, execution results, status, and metadata\n` +
|
|
2162
2166
|
`- "schedule_report": Create a recurring/scheduled report\n` +
|
|
2163
2167
|
` Required: scope_type, recurrence_type (hourly|daily|weekly|monthly), agent_id (YOUR name)\n` +
|
|
2164
2168
|
` Optional: scope_id, scope_label, channel_types[], recurrence_time (HH:MM UTC, default 09:00),\n` +
|
|
@@ -2169,10 +2173,11 @@ function registerNotifyOps(
|
|
|
2169
2173
|
type: "object",
|
|
2170
2174
|
required: ["action"],
|
|
2171
2175
|
properties: {
|
|
2172
|
-
action: { type: "string", enum: ["list", "mark_read", "mark_all_read", "delete", "send_report", "schedule_report", "list_schedules", "delete_schedule"] },
|
|
2176
|
+
action: { type: "string", enum: ["list", "mark_read", "mark_all_read", "delete", "send_report", "get_task_detail", "schedule_report", "list_schedules", "delete_schedule"] },
|
|
2173
2177
|
agent_id: { type: "string", description: "Your agent name or ID (required for send_report/schedule_report)" },
|
|
2174
2178
|
id: { type: "string", description: "Notification ID" },
|
|
2175
2179
|
schedule_id: { type: "string", description: "Schedule ID (for delete_schedule)" },
|
|
2180
|
+
task_number: { type: "number", description: "1-indexed task number from the report (for get_task_detail)" },
|
|
2176
2181
|
unread_only: { type: "boolean", description: "Only show unread" },
|
|
2177
2182
|
limit: { type: "number", description: "Max results (default 50)" },
|
|
2178
2183
|
scope_type: { type: "string", enum: ["space", "folder", "project", "task", "all"], description: "Report scope" },
|
|
@@ -2257,6 +2262,69 @@ function registerNotifyOps(
|
|
|
2257
2262
|
});
|
|
2258
2263
|
}
|
|
2259
2264
|
|
|
2265
|
+
// ── GET TASK DETAIL — Drill into task result by report number ──
|
|
2266
|
+
case "get_task_detail": {
|
|
2267
|
+
const taskNumber = params.task_number as number;
|
|
2268
|
+
if (!taskNumber || taskNumber < 1) return err("Missing required: task_number (1-indexed number from the report)");
|
|
2269
|
+
const detailScopeType = (params.scope_type as string) || "all";
|
|
2270
|
+
const detailScopeId = params.scope_id as string | undefined;
|
|
2271
|
+
|
|
2272
|
+
// Re-run the same query the report uses to get consistent ordering
|
|
2273
|
+
let dq = supabase.from("tasks").select("id, title, description, status, priority, progress, completed_at, due_date, updated_at, custom_fields, tags, agent_id")
|
|
2274
|
+
.eq("user_id", userId).is("parent_task_id", null);
|
|
2275
|
+
if (detailScopeType === "space" && detailScopeId) dq = dq.eq("space_id", detailScopeId);
|
|
2276
|
+
else if (detailScopeType === "folder" && detailScopeId) dq = dq.eq("folder_id", detailScopeId);
|
|
2277
|
+
else if (detailScopeType === "project" && detailScopeId) dq = dq.eq("project_id", detailScopeId);
|
|
2278
|
+
else if (detailScopeType === "task" && detailScopeId) dq = dq.eq("id", detailScopeId);
|
|
2279
|
+
|
|
2280
|
+
const { data: detailTasks } = await dq.order("priority", { ascending: false }).limit(40);
|
|
2281
|
+
const dtAll = detailTasks || [];
|
|
2282
|
+
|
|
2283
|
+
// Build same ordered list as report: Active → Pending → Done
|
|
2284
|
+
const dtInProgress = dtAll.filter((t: any) => t.status === "IN_PROGRESS" || t.status === "RUNNING");
|
|
2285
|
+
const dtPending = dtAll.filter((t: any) => t.status === "PENDING" || t.status === "NEW");
|
|
2286
|
+
const dtCompleted = dtAll.filter((t: any) => t.status === "DONE");
|
|
2287
|
+
const dtRecentThreshold = 24 * 60 * 60 * 1000;
|
|
2288
|
+
const dtNowMs = Date.now();
|
|
2289
|
+
const dtRecentlyExecuted = dtPending.filter((t: any) =>
|
|
2290
|
+
t.description && t.description.length > 50 &&
|
|
2291
|
+
t.updated_at && (dtNowMs - new Date(t.updated_at).getTime()) < dtRecentThreshold
|
|
2292
|
+
);
|
|
2293
|
+
|
|
2294
|
+
// Same ordering as report formatter
|
|
2295
|
+
const dtOrdered: any[] = [];
|
|
2296
|
+
const dtSeen = new Set<string>();
|
|
2297
|
+
const dtAdd = (t: any) => { if (!dtSeen.has(t.id)) { dtSeen.add(t.id); dtOrdered.push(t); } };
|
|
2298
|
+
for (const t of dtInProgress.slice(0, 8)) dtAdd(t);
|
|
2299
|
+
for (const t of dtPending.slice(0, 6)) dtAdd(t);
|
|
2300
|
+
for (const t of dtRecentlyExecuted) dtAdd(t);
|
|
2301
|
+
for (const t of dtCompleted.slice(0, 5)) dtAdd(t);
|
|
2302
|
+
|
|
2303
|
+
if (taskNumber > dtOrdered.length) {
|
|
2304
|
+
return err(`Task #${taskNumber} not found. The report has ${dtOrdered.length} numbered task(s). Valid range: 1-${dtOrdered.length}.`);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
const target = dtOrdered[taskNumber - 1];
|
|
2308
|
+
const isRecurring = dtRecentlyExecuted.some((r: any) => r.id === target.id);
|
|
2309
|
+
|
|
2310
|
+
return ok({
|
|
2311
|
+
task_number: taskNumber,
|
|
2312
|
+
task_id: target.id,
|
|
2313
|
+
title: target.title,
|
|
2314
|
+
status: target.status,
|
|
2315
|
+
priority: target.priority === 3 ? "CRITICAL" : target.priority === 2 ? "HIGH" : target.priority === 1 ? "MEDIUM" : "LOW",
|
|
2316
|
+
progress: target.progress,
|
|
2317
|
+
is_recurring_today: isRecurring,
|
|
2318
|
+
description: target.description || "(no description)",
|
|
2319
|
+
custom_fields: target.custom_fields || {},
|
|
2320
|
+
tags: target.tags || [],
|
|
2321
|
+
agent_id: target.agent_id,
|
|
2322
|
+
due_date: target.due_date,
|
|
2323
|
+
completed_at: target.completed_at,
|
|
2324
|
+
updated_at: target.updated_at,
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2260
2328
|
// ── SCHEDULE REPORT — Create recurring report ──
|
|
2261
2329
|
case "schedule_report": {
|
|
2262
2330
|
const scopeType = (params.scope_type as string) || "all";
|
|
@@ -2319,7 +2387,7 @@ function registerNotifyOps(
|
|
|
2319
2387
|
}
|
|
2320
2388
|
|
|
2321
2389
|
default:
|
|
2322
|
-
return err(`Unknown action "${action}". Valid: list, mark_read, mark_all_read, delete, send_report, schedule_report, list_schedules, delete_schedule`);
|
|
2390
|
+
return err(`Unknown action "${action}". Valid: list, mark_read, mark_all_read, delete, send_report, get_task_detail, schedule_report, list_schedules, delete_schedule`);
|
|
2323
2391
|
}
|
|
2324
2392
|
},
|
|
2325
2393
|
});
|
|
@@ -2336,7 +2404,7 @@ async function generatePMReportDirect(
|
|
|
2336
2404
|
includeCompleted: boolean,
|
|
2337
2405
|
): Promise<{ report: string; scopeLabel: string; taskCount: number }> {
|
|
2338
2406
|
let scopeLabel = "All Projects";
|
|
2339
|
-
let query = supabase.from("tasks").select("id, title, status, priority, progress, completed_at, due_date")
|
|
2407
|
+
let query = supabase.from("tasks").select("id, title, status, priority, progress, completed_at, due_date, description, updated_at")
|
|
2340
2408
|
.eq("user_id", userId).is("parent_task_id", null);
|
|
2341
2409
|
|
|
2342
2410
|
if (scopeType === "space" && scopeId) {
|
|
@@ -2367,24 +2435,53 @@ async function generatePMReportDirect(
|
|
|
2367
2435
|
.limit(1).single();
|
|
2368
2436
|
if (agentRow) agentName = agentRow.name || agentRow.codename || agentId;
|
|
2369
2437
|
|
|
2438
|
+
// Detect recently-executed recurring tasks: PENDING but updated within 24h with content
|
|
2439
|
+
const recentThreshold = 24 * 60 * 60 * 1000;
|
|
2440
|
+
const nowMs = Date.now();
|
|
2441
|
+
const isRecentlyExecuted = (t: any) =>
|
|
2442
|
+
(t.status === "PENDING" || t.status === "NEW") &&
|
|
2443
|
+
t.description && t.description.length > 50 &&
|
|
2444
|
+
t.updated_at && (nowMs - new Date(t.updated_at).getTime()) < recentThreshold;
|
|
2445
|
+
|
|
2446
|
+
const recentlyExecuted = all.filter(isRecentlyExecuted);
|
|
2370
2447
|
const completed = all.filter((t: any) => t.status === "DONE");
|
|
2371
2448
|
const inProgress = all.filter((t: any) => t.status === "IN_PROGRESS" || t.status === "RUNNING");
|
|
2372
2449
|
const pending = all.filter((t: any) => t.status === "PENDING" || t.status === "NEW");
|
|
2373
2450
|
const failed = all.filter((t: any) => t.status === "FAILED");
|
|
2374
2451
|
|
|
2452
|
+
// Build ordered task list for consistent numbering (same order as report sections)
|
|
2453
|
+
const orderedTaskIds: string[] = [];
|
|
2454
|
+
const taskNumMap = new Map<string, number>();
|
|
2455
|
+
const assignNum = (t: any) => {
|
|
2456
|
+
if (!taskNumMap.has(t.id)) {
|
|
2457
|
+
orderedTaskIds.push(t.id);
|
|
2458
|
+
taskNumMap.set(t.id, orderedTaskIds.length);
|
|
2459
|
+
}
|
|
2460
|
+
return taskNumMap.get(t.id)!;
|
|
2461
|
+
};
|
|
2462
|
+
|
|
2463
|
+
// Pre-assign numbers in display order: Active → Pending → Recently Executed → Done
|
|
2464
|
+
for (const t of inProgress.slice(0, 8)) assignNum(t);
|
|
2465
|
+
for (const t of pending.slice(0, 6)) assignNum(t);
|
|
2466
|
+
for (const t of recentlyExecuted) assignNum(t);
|
|
2467
|
+
for (const t of completed.slice(0, 5)) assignNum(t);
|
|
2468
|
+
|
|
2469
|
+
const doneCount = completed.length + recentlyExecuted.length;
|
|
2470
|
+
|
|
2375
2471
|
const lines: string[] = [];
|
|
2376
2472
|
lines.push(`📊 Project Report — ${scopeLabel}`);
|
|
2377
2473
|
lines.push(`Agent: ${agentName}`);
|
|
2378
2474
|
lines.push("");
|
|
2379
2475
|
lines.push("━━━ Summary ━━━");
|
|
2380
|
-
lines.push(`✅ ${
|
|
2476
|
+
lines.push(`✅ ${doneCount} | 🔄 ${inProgress.length} | ⏳ ${pending.length} | ❌ ${failed.length}`);
|
|
2381
2477
|
|
|
2382
2478
|
if (inProgress.length > 0) {
|
|
2383
2479
|
lines.push("");
|
|
2384
2480
|
lines.push("━━━ Active ━━━");
|
|
2385
2481
|
for (const t of inProgress.slice(0, 8)) {
|
|
2482
|
+
const n = taskNumMap.get(t.id)!;
|
|
2386
2483
|
const pl = t.priority === 3 ? "CRIT" : t.priority === 2 ? "HIGH" : t.priority === 1 ? "MED" : "LOW";
|
|
2387
|
-
lines.push(
|
|
2484
|
+
lines.push(`${n}. 🔄 [${pl}] ${t.title}${t.progress ? ` — ${t.progress}%` : ""}`);
|
|
2388
2485
|
}
|
|
2389
2486
|
if (inProgress.length > 8) lines.push(`... +${inProgress.length - 8} more`);
|
|
2390
2487
|
}
|
|
@@ -2393,26 +2490,41 @@ async function generatePMReportDirect(
|
|
|
2393
2490
|
lines.push("");
|
|
2394
2491
|
lines.push("━━━ Pending ━━━");
|
|
2395
2492
|
for (const t of pending.slice(0, 6)) {
|
|
2493
|
+
const n = taskNumMap.get(t.id)!;
|
|
2396
2494
|
const pl = t.priority === 3 ? "CRIT" : t.priority === 2 ? "HIGH" : t.priority === 1 ? "MED" : "LOW";
|
|
2397
|
-
|
|
2495
|
+
const suffix = isRecentlyExecuted(t) ? " (next cycle)" : "";
|
|
2496
|
+
lines.push(`${n}. ⏳ [${pl}] ${t.title}${suffix}`);
|
|
2398
2497
|
}
|
|
2399
2498
|
if (pending.length > 6) lines.push(`... +${pending.length - 6} more`);
|
|
2400
2499
|
}
|
|
2401
2500
|
|
|
2402
|
-
|
|
2501
|
+
// Done section: recently-executed recurring tasks + regular completed
|
|
2502
|
+
const doneItems: { task: any; isRecurring: boolean }[] = [
|
|
2503
|
+
...recentlyExecuted.map((t: any) => ({ task: t, isRecurring: true })),
|
|
2504
|
+
...(includeCompleted ? completed.filter((t: any) => t.completed_at)
|
|
2505
|
+
.sort((a: any, b: any) => new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime())
|
|
2506
|
+
.slice(0, 5).map((t: any) => ({ task: t, isRecurring: false })) : []),
|
|
2507
|
+
];
|
|
2508
|
+
|
|
2509
|
+
if (doneItems.length > 0) {
|
|
2403
2510
|
lines.push("");
|
|
2404
2511
|
lines.push("━━━ Done ━━━");
|
|
2405
|
-
const
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2512
|
+
for (const { task: t, isRecurring } of doneItems) {
|
|
2513
|
+
const n = assignNum(t);
|
|
2514
|
+
if (isRecurring) {
|
|
2515
|
+
lines.push(`${n}. ✅ ${t.title} (today's run)`);
|
|
2516
|
+
} else {
|
|
2517
|
+
const diff = Date.now() - new Date(t.completed_at).getTime();
|
|
2518
|
+
const mins = Math.floor(diff / 60000);
|
|
2519
|
+
const ago = mins < 60 ? `${mins}m ago` : mins < 1440 ? `${Math.floor(mins / 60)}h ago` : `${Math.floor(mins / 1440)}d ago`;
|
|
2520
|
+
lines.push(`${n}. ✅ ${t.title} (${ago})`);
|
|
2521
|
+
}
|
|
2413
2522
|
}
|
|
2414
2523
|
}
|
|
2415
2524
|
|
|
2525
|
+
lines.push("");
|
|
2526
|
+
lines.push(`💡 Reply "details on #N" for full task result`);
|
|
2527
|
+
|
|
2416
2528
|
const now = new Date();
|
|
2417
2529
|
lines.push("");
|
|
2418
2530
|
lines.push(`Generated: ${now.toISOString().replace("T", " ").slice(0, 16)} UTC`);
|
|
@@ -2447,10 +2559,6 @@ async function dispatchReportDirect(
|
|
|
2447
2559
|
const sent: string[] = [];
|
|
2448
2560
|
const failed: string[] = [];
|
|
2449
2561
|
|
|
2450
|
-
// Resolve dashboard URL + auth for sending
|
|
2451
|
-
const dashboardUrl = process.env.OFIERE_DASHBOARD_URL || "https://ofiere.com";
|
|
2452
|
-
const serviceRoleKey = (supabase as any).supabaseKey || process.env.OFIERE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || "";
|
|
2453
|
-
|
|
2454
2562
|
for (const b of bindings) {
|
|
2455
2563
|
const limit = LIMITS[b.channel_type] || 4000;
|
|
2456
2564
|
let msg = report;
|
|
@@ -2458,32 +2566,89 @@ async function dispatchReportDirect(
|
|
|
2458
2566
|
msg = msg.substring(0, limit - 80) + "\n\n... [Full report on dashboard]";
|
|
2459
2567
|
}
|
|
2460
2568
|
|
|
2461
|
-
// accountId must match the key used during gateway sync (gatewaySync.ts:435),
|
|
2462
|
-
// which defaults to agentId when channel_account_id is null.
|
|
2463
2569
|
const effectiveAccountId = b.channel_account_id || b.agent_id || agentId || "default";
|
|
2464
2570
|
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
const
|
|
2483
|
-
|
|
2571
|
+
if (b.channel_type === "telegram") {
|
|
2572
|
+
// Direct Telegram Bot API — bypass gateway entirely
|
|
2573
|
+
const chatId = b.thread_id;
|
|
2574
|
+
if (!chatId) {
|
|
2575
|
+
failed.push(`telegram: no chat_id (thread_id) for ${effectiveAccountId}`);
|
|
2576
|
+
continue;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
const config = typeof b.channel_config === "string" ? JSON.parse(b.channel_config) : b.channel_config;
|
|
2580
|
+
if (!config?.encryptedToken) {
|
|
2581
|
+
failed.push(`telegram: no encryptedToken for ${effectiveAccountId}`);
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
// Decrypt bot token (mirrors dashboard/lib/encryption.ts)
|
|
2586
|
+
let botToken: string | null = null;
|
|
2587
|
+
try {
|
|
2588
|
+
const crypto = await import("crypto");
|
|
2589
|
+
const keyHex = process.env.OFIERE_ENCRYPTION_KEY ||
|
|
2590
|
+
crypto.createHash("sha256").update("nerv-os-default-key-change-me").digest("hex");
|
|
2591
|
+
const keyBuf = Buffer.from(keyHex, "hex");
|
|
2592
|
+
const data = Buffer.from(config.encryptedToken, "hex");
|
|
2593
|
+
const iv = data.subarray(0, 12);
|
|
2594
|
+
const authTag = data.subarray(12, 28);
|
|
2595
|
+
const ciphertext = data.subarray(28);
|
|
2596
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", keyBuf, iv);
|
|
2597
|
+
decipher.setAuthTag(authTag);
|
|
2598
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
2599
|
+
botToken = decrypted.toString("utf8");
|
|
2600
|
+
} catch (e: any) {
|
|
2601
|
+
failed.push(`telegram: decrypt failed for ${effectiveAccountId}: ${e.message}`);
|
|
2602
|
+
continue;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
if (!botToken) {
|
|
2606
|
+
failed.push(`telegram: null token for ${effectiveAccountId}`);
|
|
2607
|
+
continue;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
try {
|
|
2611
|
+
const res = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
2612
|
+
method: "POST",
|
|
2613
|
+
headers: { "Content-Type": "application/json" },
|
|
2614
|
+
body: JSON.stringify({ chat_id: chatId, text: msg }),
|
|
2615
|
+
});
|
|
2616
|
+
if (res.ok) {
|
|
2617
|
+
sent.push(b.channel_type);
|
|
2618
|
+
} else {
|
|
2619
|
+
const errText = await res.text().catch(() => "");
|
|
2620
|
+
failed.push(`telegram: Bot API ${res.status} ${errText.slice(0, 100)}`);
|
|
2621
|
+
}
|
|
2622
|
+
} catch (e: any) {
|
|
2623
|
+
failed.push(`telegram: ${e.message || "fetch error"}`);
|
|
2624
|
+
}
|
|
2625
|
+
} else {
|
|
2626
|
+
// Non-telegram: route through dashboard gateway-send (best-effort)
|
|
2627
|
+
const dashboardUrl = process.env.OFIERE_DASHBOARD_URL || "https://ofiere.com";
|
|
2628
|
+
const serviceRoleKey = (supabase as any).supabaseKey || process.env.OFIERE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || "";
|
|
2629
|
+
try {
|
|
2630
|
+
const res = await fetch(`${dashboardUrl}/api/channels/gateway-send`, {
|
|
2631
|
+
method: "POST",
|
|
2632
|
+
headers: {
|
|
2633
|
+
"Content-Type": "application/json",
|
|
2634
|
+
"Authorization": `Bearer ${serviceRoleKey}`,
|
|
2635
|
+
},
|
|
2636
|
+
body: JSON.stringify({
|
|
2637
|
+
userId,
|
|
2638
|
+
channelType: b.channel_type,
|
|
2639
|
+
accountId: effectiveAccountId,
|
|
2640
|
+
message: msg,
|
|
2641
|
+
...(b.thread_id ? { threadId: b.thread_id } : {}),
|
|
2642
|
+
}),
|
|
2643
|
+
});
|
|
2644
|
+
if (res.ok) sent.push(b.channel_type);
|
|
2645
|
+
else {
|
|
2646
|
+
const errText = await res.text().catch(() => "");
|
|
2647
|
+
failed.push(`${b.channel_type}: HTTP ${res.status} ${errText.slice(0, 100)}`);
|
|
2648
|
+
}
|
|
2649
|
+
} catch (e: any) {
|
|
2650
|
+
failed.push(`${b.channel_type}: ${e.message || "fetch error"}`);
|
|
2484
2651
|
}
|
|
2485
|
-
} catch (e: any) {
|
|
2486
|
-
failed.push(`${b.channel_type}: ${e.message || "fetch error"}`);
|
|
2487
2652
|
}
|
|
2488
2653
|
}
|
|
2489
2654
|
|