@swarmclawai/swarmclaw 0.3.1 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -13
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +10 -0
- package/package.json +4 -1
- package/src/app/api/agents/[id]/route.ts +20 -18
- package/src/app/api/agents/[id]/thread/route.ts +4 -3
- package/src/app/api/agents/route.ts +8 -3
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +14 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
- package/src/app/api/connectors/route.ts +12 -4
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +5 -3
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +5 -3
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/directory/route.ts +26 -0
- package/src/app/api/openclaw/discover/route.ts +61 -0
- package/src/app/api/openclaw/sync/route.ts +30 -0
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -12
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +7 -3
- package/src/app/api/schedules/[id]/route.ts +16 -15
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +8 -3
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +5 -3
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/messages/route.ts +2 -1
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +2 -1
- package/src/app/api/sessions/route.ts +11 -4
- package/src/app/api/settings/route.ts +3 -1
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/app/api/setup/openclaw-device/route.ts +3 -1
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +5 -3
- package/src/app/api/tasks/[id]/approve/route.ts +74 -0
- package/src/app/api/tasks/[id]/route.ts +9 -5
- package/src/app/api/tasks/route.ts +5 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/usage/route.ts +3 -1
- package/src/app/api/version/route.ts +3 -1
- package/src/app/api/webhooks/[id]/route.ts +31 -32
- package/src/app/api/webhooks/route.ts +5 -3
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +11 -26
- package/src/cli/index.js +28 -9
- package/src/cli/index.ts +45 -2
- package/src/cli/spec.js +2 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +166 -81
- package/src/components/chat/chat-area.tsx +71 -34
- package/src/components/chat/chat-header.tsx +141 -29
- package/src/components/chat/chat-tool-toggles.tsx +12 -53
- package/src/components/chat/message-bubble.tsx +110 -42
- package/src/components/chat/tool-call-bubble.tsx +50 -6
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +9 -10
- package/src/components/connectors/connector-sheet.tsx +55 -36
- package/src/components/input/chat-input.tsx +72 -56
- package/src/components/knowledge/knowledge-list.tsx +27 -31
- package/src/components/layout/app-layout.tsx +133 -90
- package/src/components/layout/daemon-indicator.tsx +3 -5
- package/src/components/logs/log-list.tsx +5 -9
- package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
- package/src/components/memory/memory-detail.tsx +1 -1
- package/src/components/plugins/plugin-list.tsx +227 -27
- package/src/components/projects/project-list.tsx +122 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/providers/provider-list.tsx +46 -13
- package/src/components/providers/provider-sheet.tsx +0 -45
- package/src/components/runs/run-list.tsx +6 -15
- package/src/components/schedules/schedule-card.tsx +54 -4
- package/src/components/schedules/schedule-list.tsx +9 -4
- package/src/components/schedules/schedule-sheet.tsx +0 -47
- package/src/components/secrets/secrets-list.tsx +20 -2
- package/src/components/sessions/new-session-sheet.tsx +14 -15
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/shared/connector-platform-icon.tsx +26 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +8 -40
- package/src/components/shared/settings/section-orchestrator.tsx +9 -11
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +73 -0
- package/src/components/skills/skill-list.tsx +262 -35
- package/src/components/skills/skill-sheet.tsx +0 -45
- package/src/components/tasks/task-board.tsx +3 -6
- package/src/components/tasks/task-card.tsx +43 -1
- package/src/components/tasks/task-list.tsx +8 -7
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-continuous-speech.ts +144 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/id.ts +6 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +15 -2
- package/src/lib/providers/index.ts +8 -0
- package/src/lib/providers/ollama.ts +10 -2
- package/src/lib/providers/openai.ts +42 -13
- package/src/lib/providers/openclaw.ts +11 -0
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +57 -8
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
- package/src/lib/server/connectors/bluebubbles.ts +357 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +46 -7
- package/src/lib/server/connectors/manager.ts +401 -6
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +64 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/context-manager.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -0
- package/src/lib/server/data-dir.ts +1 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +67 -8
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-sync.ts +496 -0
- package/src/lib/server/orchestrator-lg.ts +422 -20
- package/src/lib/server/orchestrator.ts +29 -9
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +39 -13
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +8 -3
- package/src/lib/server/session-tools/connector.ts +51 -4
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +5 -5
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +4 -0
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +197 -0
- package/src/lib/server/session-tools/search-providers.ts +270 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/web.ts +47 -66
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +36 -7
- package/src/lib/server/stream-agent-chat.ts +106 -22
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/task-validation.test.ts +23 -0
- package/src/lib/server/task-validation.ts +5 -3
- package/src/lib/server/ws-hub.ts +85 -0
- package/src/lib/tool-definitions.ts +44 -0
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/view-routes.ts +28 -0
- package/src/lib/ws-client.ts +124 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +28 -1
- package/src/stores/use-chat-store.ts +42 -14
- package/src/types/index.ts +34 -2
- package/src/app/api/agents/generate/route.ts +0 -42
- package/src/app/api/generate/info/route.ts +0 -12
- package/src/app/api/generate/route.ts +0 -106
- package/src/app/favicon.ico +0 -0
- package/src/components/shared/ai-gen-block.tsx +0 -77
package/src/lib/server/queue.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import { loadTasks, saveTasks, loadQueue, saveQueue, loadAgents, loadSchedules, saveSchedules, loadSessions, saveSessions, loadSettings } from './storage'
|
|
3
|
+
import { notify } from './ws-hub'
|
|
4
|
+
import { WORKSPACE_DIR } from './data-dir'
|
|
3
5
|
import { createOrchestratorSession, executeOrchestrator } from './orchestrator'
|
|
4
6
|
import { formatValidationFailure, validateTaskCompletion } from './task-validation'
|
|
5
7
|
import { ensureTaskCompletionReport } from './task-reports'
|
|
6
8
|
import { pushMainLoopEventToMainSessions } from './main-agent-loop'
|
|
7
9
|
import { executeSessionChatTurn } from './chat-execution'
|
|
8
10
|
import { extractTaskResult, formatResultBody } from './task-result'
|
|
11
|
+
import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
9
12
|
import type { Agent, BoardTask, Message } from '@/types'
|
|
10
13
|
|
|
11
14
|
let processing = false
|
|
@@ -119,7 +122,7 @@ async function executeTaskRun(
|
|
|
119
122
|
): Promise<string> {
|
|
120
123
|
const prompt = task.description || task.title
|
|
121
124
|
if (agent?.isOrchestrator) {
|
|
122
|
-
return executeOrchestrator(agent, prompt, sessionId)
|
|
125
|
+
return executeOrchestrator(agent, prompt, sessionId, task.id)
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
const run = await executeSessionChatTurn({
|
|
@@ -152,7 +155,11 @@ function notifyMainChatScheduleResult(task: BoardTask): void {
|
|
|
152
155
|
const fallbackText = runSession ? latestAssistantText(runSession) : ''
|
|
153
156
|
|
|
154
157
|
// Zod-validated structured extraction: one pass to get summary + all artifacts
|
|
155
|
-
const taskResult = extractTaskResult(
|
|
158
|
+
const taskResult = extractTaskResult(
|
|
159
|
+
runSession,
|
|
160
|
+
task.result || fallbackText || null,
|
|
161
|
+
{ sinceTime: typeof task.startedAt === 'number' ? task.startedAt : null },
|
|
162
|
+
)
|
|
156
163
|
const resultBody = formatResultBody(taskResult)
|
|
157
164
|
|
|
158
165
|
const statusLabel = task.status === 'completed' ? 'completed' : 'failed'
|
|
@@ -221,7 +228,11 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
221
228
|
const runSessionId = typeof task.sessionId === 'string' ? task.sessionId : ''
|
|
222
229
|
const runSession = runSessionId ? sessions[runSessionId] : null
|
|
223
230
|
const fallbackText = runSession ? latestAssistantText(runSession) : ''
|
|
224
|
-
const taskResult = extractTaskResult(
|
|
231
|
+
const taskResult = extractTaskResult(
|
|
232
|
+
runSession,
|
|
233
|
+
task.result || fallbackText || null,
|
|
234
|
+
{ sinceTime: typeof task.startedAt === 'number' ? task.startedAt : null },
|
|
235
|
+
)
|
|
225
236
|
const resultBody = formatResultBody(taskResult)
|
|
226
237
|
|
|
227
238
|
const statusLabel = task.status === 'completed' ? 'completed' : 'failed'
|
|
@@ -371,7 +382,7 @@ export function validateCompletedTasksQueue() {
|
|
|
371
382
|
task.updatedAt = now
|
|
372
383
|
if (!task.comments) task.comments = []
|
|
373
384
|
task.comments.push({
|
|
374
|
-
id:
|
|
385
|
+
id: genId(),
|
|
375
386
|
author: 'System',
|
|
376
387
|
text: `Task auto-failed completed-queue validation.\n\n${validation.reasons.map((r) => `- ${r}`).join('\n')}`,
|
|
377
388
|
createdAt: now,
|
|
@@ -389,7 +400,7 @@ export function validateCompletedTasksQueue() {
|
|
|
389
400
|
}
|
|
390
401
|
}
|
|
391
402
|
|
|
392
|
-
if (tasksDirty) saveTasks(tasks)
|
|
403
|
+
if (tasksDirty) { saveTasks(tasks); notify('tasks') }
|
|
393
404
|
if (sessionsDirty) saveSessions(sessions)
|
|
394
405
|
if (demoted > 0) {
|
|
395
406
|
console.warn(`[queue] Demoted ${demoted} invalid completed task(s) to failed after validation audit`)
|
|
@@ -410,7 +421,7 @@ function scheduleRetryOrDeadLetter(task: BoardTask, reason: string): 'retry' | '
|
|
|
410
421
|
task.error = `Retry scheduled after failure: ${reason}`.slice(0, 500)
|
|
411
422
|
if (!task.comments) task.comments = []
|
|
412
423
|
task.comments.push({
|
|
413
|
-
id:
|
|
424
|
+
id: genId(),
|
|
414
425
|
author: 'System',
|
|
415
426
|
text: `Attempt ${task.attempts}/${task.maxAttempts} failed. Retrying in ${delaySec}s.\n\nReason: ${reason}`,
|
|
416
427
|
createdAt: now,
|
|
@@ -425,7 +436,7 @@ function scheduleRetryOrDeadLetter(task: BoardTask, reason: string): 'retry' | '
|
|
|
425
436
|
task.error = `Dead-lettered after ${task.attempts}/${task.maxAttempts} attempts: ${reason}`.slice(0, 500)
|
|
426
437
|
if (!task.comments) task.comments = []
|
|
427
438
|
task.comments.push({
|
|
428
|
-
id:
|
|
439
|
+
id: genId(),
|
|
429
440
|
author: 'System',
|
|
430
441
|
text: `Task moved to dead-letter after ${task.attempts}/${task.maxAttempts} attempts.\n\nReason: ${reason}`,
|
|
431
442
|
createdAt: now,
|
|
@@ -510,9 +521,12 @@ export async function processNext() {
|
|
|
510
521
|
task.startedAt = Date.now()
|
|
511
522
|
task.retryScheduledAt = null
|
|
512
523
|
task.deadLetteredAt = null
|
|
524
|
+
// Clear transient failure fields so validation/error state reflects only this attempt.
|
|
525
|
+
task.error = null
|
|
526
|
+
task.validation = null
|
|
513
527
|
task.updatedAt = Date.now()
|
|
514
528
|
|
|
515
|
-
const taskCwd = task.cwd ||
|
|
529
|
+
const taskCwd = task.cwd || WORKSPACE_DIR
|
|
516
530
|
let sessionId = ''
|
|
517
531
|
const scheduleTask = task as ScheduleTaskMeta
|
|
518
532
|
const isScheduleTask = scheduleTask.sourceType === 'schedule'
|
|
@@ -604,7 +618,11 @@ export async function processNext() {
|
|
|
604
618
|
applyTaskPolicyDefaults(t2[taskId])
|
|
605
619
|
// Structured extraction: Zod-validated result with typed artifacts
|
|
606
620
|
const runSessions = loadSessions()
|
|
607
|
-
const taskResult = extractTaskResult(
|
|
621
|
+
const taskResult = extractTaskResult(
|
|
622
|
+
runSessions[sessionId],
|
|
623
|
+
result || null,
|
|
624
|
+
{ sinceTime: typeof t2[taskId].startedAt === 'number' ? t2[taskId].startedAt : null },
|
|
625
|
+
)
|
|
608
626
|
const enrichedResult = formatResultBody(taskResult)
|
|
609
627
|
t2[taskId].result = enrichedResult.slice(0, 4000) || null
|
|
610
628
|
t2[taskId].updatedAt = Date.now()
|
|
@@ -630,7 +648,7 @@ export async function processNext() {
|
|
|
630
648
|
updatedAt: now,
|
|
631
649
|
}
|
|
632
650
|
t2[taskId].comments!.push({
|
|
633
|
-
id:
|
|
651
|
+
id: genId(),
|
|
634
652
|
author: agent.name,
|
|
635
653
|
agentId: agent.id,
|
|
636
654
|
text: `Task completed.\n\n${result?.slice(0, 1000) || 'No summary provided.'}`,
|
|
@@ -641,7 +659,7 @@ export async function processNext() {
|
|
|
641
659
|
const retryState = scheduleRetryOrDeadLetter(t2[taskId], failureReason)
|
|
642
660
|
t2[taskId].completedAt = retryState === 'dead_lettered' ? null : t2[taskId].completedAt
|
|
643
661
|
t2[taskId].comments!.push({
|
|
644
|
-
id:
|
|
662
|
+
id: genId(),
|
|
645
663
|
author: agent.name,
|
|
646
664
|
agentId: agent.id,
|
|
647
665
|
text: `Task failed validation and was not marked completed.\n\n${validation.reasons.map((r) => `- ${r}`).join('\n')}`,
|
|
@@ -688,6 +706,8 @@ export async function processNext() {
|
|
|
688
706
|
}
|
|
689
707
|
|
|
690
708
|
saveTasks(t2)
|
|
709
|
+
notify('tasks')
|
|
710
|
+
notify('runs')
|
|
691
711
|
disableSessionHeartbeat(t2[taskId].sessionId)
|
|
692
712
|
}
|
|
693
713
|
const doneTask = t2[taskId]
|
|
@@ -698,6 +718,10 @@ export async function processNext() {
|
|
|
698
718
|
})
|
|
699
719
|
notifyMainChatScheduleResult(doneTask)
|
|
700
720
|
notifyAgentThreadTaskResult(doneTask)
|
|
721
|
+
// Clean up LangGraph checkpoints for completed tasks
|
|
722
|
+
getCheckpointSaver().deleteThread(taskId).catch((e) =>
|
|
723
|
+
console.warn(`[queue] Failed to clean up checkpoints for task ${taskId}:`, e)
|
|
724
|
+
)
|
|
701
725
|
console.log(`[queue] Task "${task.title}" completed`)
|
|
702
726
|
} else {
|
|
703
727
|
if (doneTask?.status === 'queued') {
|
|
@@ -727,7 +751,7 @@ export async function processNext() {
|
|
|
727
751
|
const isRepeatError = lastComment?.agentId === agent.id && lastComment?.text.startsWith('Task failed')
|
|
728
752
|
if (!isRepeatError) {
|
|
729
753
|
t2[taskId].comments!.push({
|
|
730
|
-
id:
|
|
754
|
+
id: genId(),
|
|
731
755
|
author: agent.name,
|
|
732
756
|
agentId: agent.id,
|
|
733
757
|
text: 'Task failed — see error details above.',
|
|
@@ -735,6 +759,8 @@ export async function processNext() {
|
|
|
735
759
|
})
|
|
736
760
|
}
|
|
737
761
|
saveTasks(t2)
|
|
762
|
+
notify('tasks')
|
|
763
|
+
notify('runs')
|
|
738
764
|
disableSessionHeartbeat(t2[taskId].sessionId)
|
|
739
765
|
if (retryState === 'retry') {
|
|
740
766
|
const qRetry = loadQueue()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import { loadSchedules, saveSchedules, loadAgents, loadTasks, saveTasks } from './storage'
|
|
3
3
|
import { enqueueTask } from './queue'
|
|
4
4
|
import { CronExpressionParser } from 'cron-parser'
|
|
@@ -157,7 +157,7 @@ async function tick() {
|
|
|
157
157
|
prev.runNumber = schedule.runNumber
|
|
158
158
|
} else {
|
|
159
159
|
// Create a new linked task (first run or previous task still in-flight)
|
|
160
|
-
taskId =
|
|
160
|
+
taskId = genId()
|
|
161
161
|
tasks[taskId] = {
|
|
162
162
|
id: taskId,
|
|
163
163
|
title: `[Sched] ${schedule.name} (run #${schedule.runNumber})`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import { loadSessions, saveSessions } from './storage'
|
|
3
3
|
|
|
4
4
|
export type MailboxStatus = 'new' | 'ack'
|
|
@@ -58,7 +58,7 @@ export function sendMailboxEnvelope(input: {
|
|
|
58
58
|
? Math.max(0, Math.min(7 * 24 * 3600, Math.trunc(input.ttlSec)))
|
|
59
59
|
: null
|
|
60
60
|
const envelope: MailboxEnvelope = {
|
|
61
|
-
id:
|
|
61
|
+
id: genId(6),
|
|
62
62
|
type: (input.type || 'message').trim() || 'message',
|
|
63
63
|
payload: String(input.payload || ''),
|
|
64
64
|
fromSessionId: input.fromSessionId || null,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import type { SSEEvent } from '@/types'
|
|
3
3
|
import { active, loadSessions } from './storage'
|
|
4
4
|
import { executeSessionChatTurn, type ExecuteChatTurnResult } from './chat-execution'
|
|
@@ -31,6 +31,7 @@ interface QueueEntry {
|
|
|
31
31
|
message: string
|
|
32
32
|
imagePath?: string
|
|
33
33
|
imageUrl?: string
|
|
34
|
+
attachedFiles?: string[]
|
|
34
35
|
onEvents: Array<(event: SSEEvent) => void>
|
|
35
36
|
signalController: AbortController
|
|
36
37
|
maxRuntimeMs?: number
|
|
@@ -236,6 +237,7 @@ async function drainExecution(executionKey: string): Promise<void> {
|
|
|
236
237
|
message: next.message,
|
|
237
238
|
imagePath: next.imagePath,
|
|
238
239
|
imageUrl: next.imageUrl,
|
|
240
|
+
attachedFiles: next.attachedFiles,
|
|
239
241
|
internal: next.run.internal,
|
|
240
242
|
source: next.run.source,
|
|
241
243
|
runId: next.run.id,
|
|
@@ -333,6 +335,7 @@ export interface EnqueueSessionRunInput {
|
|
|
333
335
|
message: string
|
|
334
336
|
imagePath?: string
|
|
335
337
|
imageUrl?: string
|
|
338
|
+
attachedFiles?: string[]
|
|
336
339
|
internal?: boolean
|
|
337
340
|
source?: string
|
|
338
341
|
mode?: SessionQueueMode
|
|
@@ -384,7 +387,7 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
384
387
|
|
|
385
388
|
const running = state.runningByExecution.get(executionKey)
|
|
386
389
|
const q = queueForExecution(executionKey)
|
|
387
|
-
if (mode === 'collect' && !input.imagePath && !input.imageUrl) {
|
|
390
|
+
if (mode === 'collect' && !input.imagePath && !input.imageUrl && !input.attachedFiles?.length) {
|
|
388
391
|
const nowMs = now()
|
|
389
392
|
const candidate = q.at(-1)
|
|
390
393
|
const canCoalesce = !!candidate
|
|
@@ -393,6 +396,7 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
393
396
|
&& candidate.run.source === source
|
|
394
397
|
&& !candidate.imagePath
|
|
395
398
|
&& !candidate.imageUrl
|
|
399
|
+
&& !candidate.attachedFiles?.length
|
|
396
400
|
&& (nowMs - candidate.run.queuedAt) <= COLLECT_COALESCE_WINDOW_MS
|
|
397
401
|
|
|
398
402
|
if (candidate && canCoalesce) {
|
|
@@ -416,7 +420,7 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
416
420
|
}
|
|
417
421
|
}
|
|
418
422
|
|
|
419
|
-
const runId =
|
|
423
|
+
const runId = genId(8)
|
|
420
424
|
const run: SessionRunRecord = {
|
|
421
425
|
id: runId,
|
|
422
426
|
sessionId: input.sessionId,
|
|
@@ -444,6 +448,7 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
444
448
|
message: input.message,
|
|
445
449
|
imagePath: input.imagePath,
|
|
446
450
|
imageUrl: input.imageUrl,
|
|
451
|
+
attachedFiles: input.attachedFiles,
|
|
447
452
|
onEvents: input.onEvent ? [input.onEvent] : [],
|
|
448
453
|
signalController: new AbortController(),
|
|
449
454
|
maxRuntimeMs: effectiveMaxRuntimeMs > 0 ? effectiveMaxRuntimeMs : undefined,
|
|
@@ -58,6 +58,10 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
58
58
|
const outbound = connector.config?.outboundJid?.trim()
|
|
59
59
|
if (outbound) channelId = outbound
|
|
60
60
|
}
|
|
61
|
+
if (!channelId) {
|
|
62
|
+
const outbound = connector.config?.outboundTarget?.trim()
|
|
63
|
+
if (outbound) channelId = outbound
|
|
64
|
+
}
|
|
61
65
|
if (!channelId) {
|
|
62
66
|
const recentChannelId = getConnectorRecentChannelId(selected.id)
|
|
63
67
|
if (recentChannelId) channelId = recentChannelId
|
|
@@ -67,7 +71,11 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
67
71
|
if (allowed.length) channelId = allowed[0]
|
|
68
72
|
}
|
|
69
73
|
if (!channelId) {
|
|
70
|
-
|
|
74
|
+
const allowed = connector.config?.allowFrom?.split(',').map((s: string) => s.trim()).filter(Boolean) || []
|
|
75
|
+
if (allowed.length) channelId = allowed[0]
|
|
76
|
+
}
|
|
77
|
+
if (!channelId) {
|
|
78
|
+
return `Error: no target recipient configured. Provide "to", or set connector config "outboundJid"/"allowedJids"/"outboundTarget"/"allowFrom".`
|
|
71
79
|
}
|
|
72
80
|
if (connector.platform === 'whatsapp') {
|
|
73
81
|
channelId = normalizeWhatsAppTarget(channelId)
|
|
@@ -93,6 +101,45 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
93
101
|
})
|
|
94
102
|
}
|
|
95
103
|
|
|
104
|
+
if (action === 'message_react' || action === 'message_edit' || action === 'message_pin' || action === 'message_delete') {
|
|
105
|
+
if (!connectorId) return 'Error: connectorId is required for rich messaging actions.'
|
|
106
|
+
const { getRunningInstance } = await import('../connectors/manager')
|
|
107
|
+
const inst = getRunningInstance(connectorId)
|
|
108
|
+
if (!inst) return `Error: connector "${connectorId}" is not running.`
|
|
109
|
+
|
|
110
|
+
const targetChannel = to?.trim() || ''
|
|
111
|
+
const targetMessageId = message?.trim() || ''
|
|
112
|
+
if (!targetMessageId) return 'Error: message parameter (used as messageId) is required for rich messaging actions.'
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
if (action === 'message_react') {
|
|
116
|
+
if (!inst.sendReaction) return 'Error: this connector does not support reactions.'
|
|
117
|
+
const emoji = caption?.trim() || '👍'
|
|
118
|
+
await inst.sendReaction(targetChannel, targetMessageId, emoji)
|
|
119
|
+
return JSON.stringify({ status: 'reacted', connectorId, messageId: targetMessageId, emoji })
|
|
120
|
+
}
|
|
121
|
+
if (action === 'message_edit') {
|
|
122
|
+
if (!inst.editMessage) return 'Error: this connector does not support message editing.'
|
|
123
|
+
const newText = caption?.trim() || ''
|
|
124
|
+
if (!newText) return 'Error: caption (new text) is required for message_edit.'
|
|
125
|
+
await inst.editMessage(targetChannel, targetMessageId, newText)
|
|
126
|
+
return JSON.stringify({ status: 'edited', connectorId, messageId: targetMessageId })
|
|
127
|
+
}
|
|
128
|
+
if (action === 'message_delete') {
|
|
129
|
+
if (!inst.deleteMessage) return 'Error: this connector does not support message deletion.'
|
|
130
|
+
await inst.deleteMessage(targetChannel, targetMessageId)
|
|
131
|
+
return JSON.stringify({ status: 'deleted', connectorId, messageId: targetMessageId })
|
|
132
|
+
}
|
|
133
|
+
if (action === 'message_pin') {
|
|
134
|
+
if (!inst.pinMessage) return 'Error: this connector does not support message pinning.'
|
|
135
|
+
await inst.pinMessage(targetChannel, targetMessageId)
|
|
136
|
+
return JSON.stringify({ status: 'pinned', connectorId, messageId: targetMessageId })
|
|
137
|
+
}
|
|
138
|
+
} catch (err: any) {
|
|
139
|
+
return `Error: ${err.message || String(err)}`
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
96
143
|
return 'Unknown action. Use list_running, list_targets, or send.'
|
|
97
144
|
} catch (err: any) {
|
|
98
145
|
return `Error: ${err.message || String(err)}`
|
|
@@ -100,11 +147,11 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
100
147
|
},
|
|
101
148
|
{
|
|
102
149
|
name: 'connector_message_tool',
|
|
103
|
-
description: 'Send proactive outbound messages
|
|
150
|
+
description: 'Send proactive outbound messages and perform rich messaging actions through running connectors. Supports listing running connectors/targets, sending text/media, and rich messaging (react, edit, delete, pin). For rich actions: connectorId + message (as messageId) required; caption carries emoji for react or new text for edit.',
|
|
104
151
|
schema: z.object({
|
|
105
|
-
action: z.enum(['list_running', 'list_targets', 'send']).describe('connector messaging action'),
|
|
152
|
+
action: z.enum(['list_running', 'list_targets', 'send', 'message_react', 'message_edit', 'message_delete', 'message_pin']).describe('connector messaging action'),
|
|
106
153
|
connectorId: z.string().optional().describe('Optional connector id. Defaults to the first running connector (or first for selected platform).'),
|
|
107
|
-
platform: z.string().optional().describe('Optional platform filter (whatsapp, telegram, slack, discord).'),
|
|
154
|
+
platform: z.string().optional().describe('Optional platform filter (whatsapp, telegram, slack, discord, bluebubbles, etc.).'),
|
|
108
155
|
to: z.string().optional().describe('Target channel id / recipient. For WhatsApp, phone number or full JID.'),
|
|
109
156
|
message: z.string().optional().describe('Message text to send (required for send action).'),
|
|
110
157
|
imageUrl: z.string().optional().describe('Optional public image URL to attach/send where platform supports media.'),
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod'
|
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
3
|
import fs from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
|
-
import
|
|
5
|
+
import { genId } from '@/lib/id'
|
|
6
6
|
import { spawnSync } from 'child_process'
|
|
7
7
|
import * as cheerio from 'cheerio'
|
|
8
8
|
import {
|
|
@@ -342,7 +342,7 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
342
342
|
})
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
|
-
const newId =
|
|
345
|
+
const newId = genId()
|
|
346
346
|
const entry = {
|
|
347
347
|
id: newId,
|
|
348
348
|
...parsed,
|
|
@@ -565,7 +565,7 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
565
565
|
const content = trimDocumentContent(extracted.text)
|
|
566
566
|
if (!content) return 'Error: extracted document text is empty.'
|
|
567
567
|
|
|
568
|
-
const docId =
|
|
568
|
+
const docId = genId(6)
|
|
569
569
|
const now = Date.now()
|
|
570
570
|
const parsedMetadata = metadata && typeof metadata === 'string'
|
|
571
571
|
? (() => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
-
import
|
|
3
|
+
import { genId } from '@/lib/id'
|
|
4
4
|
import { spawn, spawnSync } from 'child_process'
|
|
5
5
|
import { loadAgents, loadTasks, upsertTask } from '../storage'
|
|
6
6
|
import { log } from '../logger'
|
|
@@ -619,8 +619,8 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
619
619
|
}
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
-
// delegate_to_agent: requires
|
|
623
|
-
if (
|
|
622
|
+
// delegate_to_agent: requires "Assign to Other Agents" (platformAssignScope: 'all')
|
|
623
|
+
if (ctx?.platformAssignScope === 'all' && ctx?.agentId) {
|
|
624
624
|
tools.push(
|
|
625
625
|
tool(
|
|
626
626
|
async ({ agentId: targetAgentId, task: taskPrompt, description: taskDesc, startImmediately }) => {
|
|
@@ -640,7 +640,7 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
640
640
|
}
|
|
641
641
|
if (!target) return `Error: Agent "${targetAgentId}" not found. Use the agent directory in your system prompt to find valid agent IDs.`
|
|
642
642
|
|
|
643
|
-
const taskId =
|
|
643
|
+
const taskId = genId()
|
|
644
644
|
const now = Date.now()
|
|
645
645
|
const newTask = {
|
|
646
646
|
id: taskId,
|
|
@@ -653,7 +653,7 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
653
653
|
createdAt: now,
|
|
654
654
|
updatedAt: now,
|
|
655
655
|
comments: [{
|
|
656
|
-
id:
|
|
656
|
+
id: genId(),
|
|
657
657
|
author: agents[ctx.agentId!]?.name || 'Agent',
|
|
658
658
|
agentId: ctx.agentId!,
|
|
659
659
|
text: `Delegated from ${agents[ctx.agentId!]?.name || ctx.agentId}`,
|
|
@@ -44,10 +44,15 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
44
44
|
if (canWriteFiles) {
|
|
45
45
|
tools.push(
|
|
46
46
|
tool(
|
|
47
|
-
async ({ filePath, content }) => {
|
|
47
|
+
async ({ filePath, content, encoding }) => {
|
|
48
48
|
try {
|
|
49
49
|
const resolved = safePath(bctx.cwd, filePath)
|
|
50
50
|
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
51
|
+
if (encoding === 'base64') {
|
|
52
|
+
const buf = Buffer.from(content, 'base64')
|
|
53
|
+
fs.writeFileSync(resolved, buf)
|
|
54
|
+
return `File written: ${filePath} (${buf.length} bytes, binary)`
|
|
55
|
+
}
|
|
51
56
|
fs.writeFileSync(resolved, content, 'utf-8')
|
|
52
57
|
return `File written: ${filePath} (${content.length} bytes)`
|
|
53
58
|
} catch (err: any) {
|
|
@@ -56,10 +61,11 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
56
61
|
},
|
|
57
62
|
{
|
|
58
63
|
name: 'write_file',
|
|
59
|
-
description: 'Write content to a file in the session working directory. Creates directories if needed.',
|
|
64
|
+
description: 'Write content to a file in the session working directory. Creates directories if needed. For PDFs and styled reports, use the create_document tool instead. For other binary files (Excel, images, zip, etc.), set encoding to "base64" and pass base64-encoded content.',
|
|
60
65
|
schema: z.object({
|
|
61
66
|
filePath: z.string().describe('Relative path to the file'),
|
|
62
|
-
content: z.string().describe('The content to write'),
|
|
67
|
+
content: z.string().describe('The content to write. For binary files, this must be a base64-encoded string.'),
|
|
68
|
+
encoding: z.enum(['utf-8', 'base64']).optional().describe('Encoding of the content. Use "base64" for binary files like PDF, Excel, images, zip archives. Defaults to "utf-8" for plain text.'),
|
|
63
69
|
}),
|
|
64
70
|
},
|
|
65
71
|
),
|
|
@@ -229,6 +235,173 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
229
235
|
)
|
|
230
236
|
}
|
|
231
237
|
|
|
238
|
+
if (canSendFiles || canWriteFiles) {
|
|
239
|
+
// create_document: markdown → pdf / html / png / jpg
|
|
240
|
+
tools.push(
|
|
241
|
+
tool(
|
|
242
|
+
async ({ content, title, filename, format }) => {
|
|
243
|
+
try {
|
|
244
|
+
const fmt = format || 'pdf'
|
|
245
|
+
const { marked } = await import('marked')
|
|
246
|
+
const html = await marked.parse(content)
|
|
247
|
+
const safeTitle = (title || 'Document').replace(/</g, '<')
|
|
248
|
+
const fullHtml = `<!DOCTYPE html>
|
|
249
|
+
<html><head><meta charset="utf-8"><title>${safeTitle}</title>
|
|
250
|
+
<style>
|
|
251
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:800px;margin:40px auto;padding:0 20px;color:#1a1a1a;line-height:1.6}
|
|
252
|
+
h1{font-size:28px;border-bottom:2px solid #e5e7eb;padding-bottom:8px}
|
|
253
|
+
h2{font-size:22px;margin-top:32px}
|
|
254
|
+
h3{font-size:18px;margin-top:24px}
|
|
255
|
+
pre{background:#f3f4f6;padding:16px;border-radius:8px;overflow-x:auto;font-size:13px}
|
|
256
|
+
code{background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:13px}
|
|
257
|
+
pre code{background:none;padding:0}
|
|
258
|
+
table{border-collapse:collapse;width:100%}
|
|
259
|
+
th,td{border:1px solid #d1d5db;padding:8px 12px;text-align:left}
|
|
260
|
+
th{background:#f9fafb;font-weight:600}
|
|
261
|
+
blockquote{border-left:4px solid #d1d5db;margin:16px 0;padding:8px 16px;color:#4b5563}
|
|
262
|
+
img{max-width:100%}
|
|
263
|
+
</style></head><body>${html}</body></html>`
|
|
264
|
+
|
|
265
|
+
const defaultBase = (title || 'document').replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
266
|
+
|
|
267
|
+
if (fmt === 'html') {
|
|
268
|
+
const outName = filename || `${defaultBase}.html`
|
|
269
|
+
const resolved = safePath(bctx.cwd, outName)
|
|
270
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
271
|
+
fs.writeFileSync(resolved, fullHtml, 'utf-8')
|
|
272
|
+
return `HTML document created: ${outName} (${fullHtml.length} bytes)`
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const { chromium } = await import('playwright')
|
|
276
|
+
const browser = await chromium.launch({ headless: true })
|
|
277
|
+
try {
|
|
278
|
+
const page = await browser.newPage()
|
|
279
|
+
await page.setContent(fullHtml, { waitUntil: 'networkidle' })
|
|
280
|
+
|
|
281
|
+
if (fmt === 'pdf') {
|
|
282
|
+
const outName = filename || `${defaultBase}.pdf`
|
|
283
|
+
const resolved = safePath(bctx.cwd, outName)
|
|
284
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
285
|
+
await page.pdf({ path: resolved, format: 'A4', margin: { top: '40px', bottom: '40px', left: '40px', right: '40px' }, printBackground: true })
|
|
286
|
+
return `PDF created: ${outName}`
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// png or jpg screenshot
|
|
290
|
+
const ext = fmt === 'jpg' ? 'jpeg' : 'png'
|
|
291
|
+
const outName = filename || `${defaultBase}.${fmt}`
|
|
292
|
+
const resolved = safePath(bctx.cwd, outName)
|
|
293
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
294
|
+
await page.screenshot({ path: resolved, type: ext, fullPage: true })
|
|
295
|
+
const size = fs.statSync(resolved).size
|
|
296
|
+
return `Image created: ${outName} (${(size / 1024).toFixed(1)} KB)`
|
|
297
|
+
} finally {
|
|
298
|
+
await browser.close()
|
|
299
|
+
}
|
|
300
|
+
} catch (err: any) {
|
|
301
|
+
return `Error creating document: ${err.message}`
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: 'create_document',
|
|
306
|
+
description: 'Create a document from markdown content. Renders markdown with professional styling and outputs as PDF, HTML, or image. Use this instead of write_file for PDFs, reports, styled pages, or document screenshots. After creating, use send_file to deliver it to the user.',
|
|
307
|
+
schema: z.object({
|
|
308
|
+
content: z.string().describe('Markdown content for the document'),
|
|
309
|
+
title: z.string().optional().describe('Document title (shown in header and used for default filename)'),
|
|
310
|
+
filename: z.string().optional().describe('Output filename (defaults to title-based name with appropriate extension)'),
|
|
311
|
+
format: z.enum(['pdf', 'html', 'png', 'jpg']).optional().describe('Output format. "pdf" (default) for print-ready documents, "html" for web pages, "png"/"jpg" for images.'),
|
|
312
|
+
}),
|
|
313
|
+
},
|
|
314
|
+
),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
// create_spreadsheet: JSON data → xlsx or csv
|
|
318
|
+
tools.push(
|
|
319
|
+
tool(
|
|
320
|
+
async ({ data, headers, sheetName, filename, format }) => {
|
|
321
|
+
try {
|
|
322
|
+
const fmt = format || 'xlsx'
|
|
323
|
+
let rows: Record<string, unknown>[]
|
|
324
|
+
try {
|
|
325
|
+
rows = JSON.parse(data)
|
|
326
|
+
if (!Array.isArray(rows)) return 'Error: data must be a JSON array of objects'
|
|
327
|
+
} catch {
|
|
328
|
+
return 'Error: data is not valid JSON. Pass a JSON array of objects, e.g. [{"name":"Alice","age":30}]'
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!rows.length) return 'Error: data array is empty'
|
|
332
|
+
|
|
333
|
+
// Resolve column headers: explicit headers, or keys from first row
|
|
334
|
+
const cols = headers?.length
|
|
335
|
+
? headers
|
|
336
|
+
: Object.keys(rows[0] && typeof rows[0] === 'object' ? rows[0] : {})
|
|
337
|
+
if (!cols.length) return 'Error: could not determine column headers. Pass headers or use objects with keys.'
|
|
338
|
+
|
|
339
|
+
const defaultBase = (sheetName || 'spreadsheet').replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
340
|
+
|
|
341
|
+
if (fmt === 'csv') {
|
|
342
|
+
const escapeCsv = (val: unknown): string => {
|
|
343
|
+
const s = val == null ? '' : String(val)
|
|
344
|
+
return s.includes(',') || s.includes('"') || s.includes('\n')
|
|
345
|
+
? `"${s.replace(/"/g, '""')}"`
|
|
346
|
+
: s
|
|
347
|
+
}
|
|
348
|
+
const lines = [cols.map(escapeCsv).join(',')]
|
|
349
|
+
for (const row of rows) {
|
|
350
|
+
const r = Array.isArray(row) ? row : cols.map((c) => (row as Record<string, unknown>)[c])
|
|
351
|
+
lines.push(r.map(escapeCsv).join(','))
|
|
352
|
+
}
|
|
353
|
+
const outName = filename || `${defaultBase}.csv`
|
|
354
|
+
const resolved = safePath(bctx.cwd, outName)
|
|
355
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
356
|
+
fs.writeFileSync(resolved, lines.join('\n'), 'utf-8')
|
|
357
|
+
return `CSV created: ${outName} (${rows.length} rows, ${cols.length} columns)`
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// xlsx via exceljs
|
|
361
|
+
const ExcelJS = await import('exceljs')
|
|
362
|
+
const workbook = new ExcelJS.default.Workbook()
|
|
363
|
+
const sheet = workbook.addWorksheet(sheetName || 'Sheet1')
|
|
364
|
+
|
|
365
|
+
sheet.columns = cols.map((c) => ({ header: c, key: c, width: Math.max(12, c.length + 4) }))
|
|
366
|
+
// Style header row
|
|
367
|
+
sheet.getRow(1).font = { bold: true }
|
|
368
|
+
sheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF3F4F6' } }
|
|
369
|
+
|
|
370
|
+
for (const row of rows) {
|
|
371
|
+
if (Array.isArray(row)) {
|
|
372
|
+
const obj: Record<string, unknown> = {}
|
|
373
|
+
cols.forEach((c, i) => { obj[c] = row[i] })
|
|
374
|
+
sheet.addRow(obj)
|
|
375
|
+
} else {
|
|
376
|
+
sheet.addRow(row)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const outName = filename || `${defaultBase}.xlsx`
|
|
381
|
+
const resolved = safePath(bctx.cwd, outName)
|
|
382
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
383
|
+
await workbook.xlsx.writeFile(resolved)
|
|
384
|
+
const size = fs.statSync(resolved).size
|
|
385
|
+
return `Excel spreadsheet created: ${outName} (${rows.length} rows, ${cols.length} columns, ${(size / 1024).toFixed(1)} KB)`
|
|
386
|
+
} catch (err: any) {
|
|
387
|
+
return `Error creating spreadsheet: ${err.message}`
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: 'create_spreadsheet',
|
|
392
|
+
description: 'Create an Excel (.xlsx) or CSV file from structured data. Pass data as a JSON array of objects. Use this for tables, reports, data exports, and any tabular data the user requests. After creating, use send_file to deliver it to the user.',
|
|
393
|
+
schema: z.object({
|
|
394
|
+
data: z.string().describe('JSON array of objects, e.g. [{"name":"Alice","score":95},{"name":"Bob","score":87}]'),
|
|
395
|
+
headers: z.array(z.string()).optional().describe('Column headers in display order. If omitted, keys from the first object are used.'),
|
|
396
|
+
sheetName: z.string().optional().describe('Worksheet name (default "Sheet1")'),
|
|
397
|
+
filename: z.string().optional().describe('Output filename (defaults to sheetName-based name with extension)'),
|
|
398
|
+
format: z.enum(['xlsx', 'csv']).optional().describe('Output format: "xlsx" (default) for Excel, "csv" for plain CSV.'),
|
|
399
|
+
}),
|
|
400
|
+
},
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
|
|
232
405
|
if (bctx.hasTool('edit_file')) {
|
|
233
406
|
tools.push(
|
|
234
407
|
tool(
|
|
@@ -14,6 +14,8 @@ import { buildCrudTools } from './crud'
|
|
|
14
14
|
import { buildSessionInfoTools } from './session-info'
|
|
15
15
|
import { buildConnectorTools } from './connector'
|
|
16
16
|
import { buildContextTools } from './context-mgmt'
|
|
17
|
+
import { buildSandboxTools } from './sandbox'
|
|
18
|
+
import { buildOpenClawNodeTools } from './openclaw-nodes'
|
|
17
19
|
|
|
18
20
|
export type { ToolContext, SessionToolsResult }
|
|
19
21
|
export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
|
|
@@ -93,6 +95,8 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
|
|
|
93
95
|
...buildSessionInfoTools(bctx),
|
|
94
96
|
...buildConnectorTools(bctx),
|
|
95
97
|
...buildContextTools(bctx),
|
|
98
|
+
...buildSandboxTools(bctx),
|
|
99
|
+
...buildOpenClawNodeTools(bctx),
|
|
96
100
|
)
|
|
97
101
|
|
|
98
102
|
// ---------------------------------------------------------------------------
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
3
|
import fs from 'fs'
|
|
4
|
-
import
|
|
4
|
+
import { genId } from '@/lib/id'
|
|
5
5
|
import { getMemoryDb, getMemoryLookupLimits, storeMemoryImageAsset } from '../memory-db'
|
|
6
6
|
import { loadSettings } from '../storage'
|
|
7
7
|
import type { ToolBuildContext } from './context'
|
|
@@ -81,7 +81,7 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
|
|
|
81
81
|
return `Error: image file not found: ${imagePath}`
|
|
82
82
|
}
|
|
83
83
|
try {
|
|
84
|
-
storedImage = await storeMemoryImageAsset(imagePath,
|
|
84
|
+
storedImage = await storeMemoryImageAsset(imagePath, genId(6))
|
|
85
85
|
} catch {
|
|
86
86
|
return `Error: failed to process image at ${imagePath}`
|
|
87
87
|
}
|