@swarmclawai/swarmclaw 0.6.8 → 0.7.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/README.md +70 -45
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +18 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +11 -3
- package/src/app/api/tasks/route.ts +8 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +13 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +86 -29
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +30 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +29 -6
- package/src/components/home/home-view.tsx +20 -14
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +73 -21
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +213 -59
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +19 -7
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +170 -66
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +66 -64
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +223 -62
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +42 -0
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +180 -17
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/orchestrator-lg.ts +4 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +650 -142
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/queue.ts +253 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +11 -1
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +85 -33
- package/src/lib/server/session-tools/index.ts +205 -160
- package/src/lib/server/session-tools/memory.ts +152 -265
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +66 -31
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +179 -349
- package/src/lib/server/storage.ts +24 -0
- package/src/lib/server/stream-agent-chat.ts +301 -244
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +23 -5
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +23 -23
- package/src/lib/validation/schemas.ts +12 -0
- package/src/lib/view-routes.ts +2 -24
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +121 -7
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ The orchestration dashboard for OpenClaw. Manage a swarm of OpenClaws + 14 other
|
|
|
12
12
|
|
|
13
13
|
Inspired by [OpenClaw](https://github.com/openclaw).
|
|
14
14
|
|
|
15
|
-
**[Documentation](https://swarmclaw.ai/docs)** | **[Website](https://swarmclaw.ai)**
|
|
15
|
+
**[Documentation](https://swarmclaw.ai/docs)** | **[Plugin Tutorial](https://swarmclaw.ai/docs/plugin-tutorial)** | **[Website](https://swarmclaw.ai)**
|
|
16
16
|
|
|
17
17
|

|
|
18
18
|

|
|
@@ -85,7 +85,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
88
|
-
To pin a version: `SWARMCLAW_VERSION=v0.
|
|
88
|
+
To pin a version: `SWARMCLAW_VERSION=v0.7.0 curl ... | bash`
|
|
89
89
|
|
|
90
90
|
Or run locally from the repo (friendly for non-technical users):
|
|
91
91
|
|
|
@@ -201,8 +201,9 @@ Notes:
|
|
|
201
201
|
All config lives in `.env.local` (auto-generated):
|
|
202
202
|
|
|
203
203
|
```
|
|
204
|
-
ACCESS_KEY=<your-access-key>
|
|
205
|
-
CREDENTIAL_SECRET=<auto-generated>
|
|
204
|
+
ACCESS_KEY=<your-access-key> # Auth key for the dashboard
|
|
205
|
+
CREDENTIAL_SECRET=<auto-generated> # AES-256 encryption key for stored credentials
|
|
206
|
+
SWARMCLAW_PLUGIN_FAILURE_THRESHOLD=3 # Consecutive failures before auto-disabling a plugin
|
|
206
207
|
```
|
|
207
208
|
|
|
208
209
|
Data is stored in `data/swarmclaw.db` (SQLite with WAL mode), `data/memory.db` (agent memory with FTS5 + vector embeddings), `data/logs.db` (execution audit trail), and `data/langgraph-checkpoints.db` (orchestrator checkpoints). Back the `data/` directory up if you care about your sessions, agents, and credentials. Existing JSON file data is auto-migrated to SQLite on first run.
|
|
@@ -449,54 +450,68 @@ When enabled, new memories get vector embeddings. Search uses both FTS5 keyword
|
|
|
449
450
|
|
|
450
451
|
Agents and sessions can have **fallback credentials**. If the primary API key gets a 401, 429, or 500 error, SwarmClaw automatically retries with the next credential. Configure fallback keys in the agent builder UI.
|
|
451
452
|
|
|
452
|
-
##
|
|
453
|
+
## Plugin System
|
|
453
454
|
|
|
454
|
-
|
|
455
|
+
SwarmClaw features a powerful, modular plugin system designed for both agent enhancement and application extensibility. It is fully compatible with the **OpenClaw** plugin format.
|
|
455
456
|
|
|
456
|
-
|
|
457
|
-
2. **URL** — Install from any HTTPS URL via Settings → Plugins → Install from URL
|
|
458
|
-
3. **Manual** — Drop `.js` files into `data/plugins/`
|
|
457
|
+
Plugins can be managed in **Settings → Plugins** and installed via the Marketplace, URL, or by dropping `.js` files into `data/plugins/`.
|
|
459
458
|
|
|
460
|
-
|
|
459
|
+
Docs:
|
|
460
|
+
- Full docs: https://swarmclaw.ai/docs
|
|
461
|
+
- Plugin tutorial: https://swarmclaw.ai/docs/plugin-tutorial
|
|
462
|
+
|
|
463
|
+
### Extension Points
|
|
464
|
+
|
|
465
|
+
Unlike standard tool systems, SwarmClaw plugins can modify the application itself:
|
|
466
|
+
|
|
467
|
+
- **Agent Tools**: Define custom tools that agents can autonomously discover and use.
|
|
468
|
+
- **Lifecycle Hooks**: Intercept events like `beforeAgentStart`, `afterToolExec`, and `onMessage`.
|
|
469
|
+
- **UI Extensions**:
|
|
470
|
+
- `sidebarItems`: Inject new navigation links into the main sidebar.
|
|
471
|
+
- `headerWidgets`: Add status badges or indicators to the chat header (e.g., Wallet Balance).
|
|
472
|
+
- `chatInputActions`: Add custom action buttons next to the chat input (e.g., "Quick Scan").
|
|
473
|
+
- `plugin-ui` Messages: Render rich, interactive React cards in the chat stream.
|
|
474
|
+
- **Deep Chat Hooks**:
|
|
475
|
+
- `transformInboundMessage`: Modify user messages before they reach the agent.
|
|
476
|
+
- `transformOutboundMessage`: Modify agent responses before they are saved or displayed.
|
|
477
|
+
- **Custom Providers**: Add new LLM backends (e.g., a specialized local model or a new API).
|
|
478
|
+
- **Custom Connectors**: Build new chat platform bridges (e.g., a proprietary internal messenger).
|
|
479
|
+
|
|
480
|
+
### Autonomous Capability Discovery
|
|
481
|
+
|
|
482
|
+
Agents in SwarmClaw are "aware" of the plugin system. If an agent lacks a tool needed for a task, it can:
|
|
483
|
+
1. **Discover**: Scan the system for all installed plugins.
|
|
484
|
+
2. **Search Marketplace**: Autonomously search **ClawHub** and the **SwarmClaw Registry** for new capabilities.
|
|
485
|
+
3. **Request Access**: Prompt the user in-chat to enable a specific installed plugin.
|
|
486
|
+
4. **Install Request**: Suggest installing a new plugin from a marketplace URL to fill a capability gap (requires user approval).
|
|
487
|
+
|
|
488
|
+
### Example Plugin (SwarmClaw Format)
|
|
461
489
|
|
|
462
490
|
```js
|
|
463
491
|
module.exports = {
|
|
464
|
-
name: 'my-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
afterAgentComplete: async ({ session, response }) => { /* ... */ },
|
|
469
|
-
beforeToolExec: async ({ toolName, input }) => { /* return { abort: true } to cancel */ },
|
|
470
|
-
afterToolExec: async ({ toolName, input, output }) => { /* ... */ },
|
|
471
|
-
onMessage: async ({ session, message }) => { /* ... */ },
|
|
472
|
-
onTaskComplete: async ({ taskId, result }) => { /* ... */ },
|
|
473
|
-
onAgentDelegation: async ({ sourceAgentId, targetAgentId, task }) => { /* ... */ },
|
|
492
|
+
name: 'my-custom-extension',
|
|
493
|
+
ui: {
|
|
494
|
+
sidebarItems: [{ id: 'dashboard', label: 'My View', href: '/custom-view' }],
|
|
495
|
+
headerWidgets: [{ id: 'status', label: '🟢 Active' }]
|
|
474
496
|
},
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
]
|
|
484
|
-
}
|
|
497
|
+
tools: [{
|
|
498
|
+
name: 'custom_action',
|
|
499
|
+
description: 'Perform a specialized task',
|
|
500
|
+
parameters: { type: 'object', properties: { input: { type: 'string' } } },
|
|
501
|
+
execute: async (args) => {
|
|
502
|
+
// Logic here
|
|
503
|
+
return { kind: 'plugin-ui', text: 'Rich result card content' };
|
|
504
|
+
}
|
|
505
|
+
}]
|
|
506
|
+
};
|
|
485
507
|
```
|
|
486
508
|
|
|
487
|
-
###
|
|
509
|
+
### Lifecycle Management
|
|
488
510
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
| `onAgentStart` | `beforeAgentStart` |
|
|
494
|
-
| `onAgentComplete` | `afterAgentComplete` |
|
|
495
|
-
| `onToolCall` | `beforeToolExec` |
|
|
496
|
-
| `onToolResult` | `afterToolExec` |
|
|
497
|
-
| `onMessage` | `onMessage` |
|
|
498
|
-
|
|
499
|
-
Plugin API: `GET /api/plugins`, `POST /api/plugins`, `GET /api/plugins/marketplace`, `POST /api/plugins/install`.
|
|
511
|
+
- **Versioning**: All plugins support semantic versioning (e.g., `v1.2.3`).
|
|
512
|
+
- **Updates**: Plugins can be updated individually or in bulk via the Plugins manager.
|
|
513
|
+
- **Hot-Reload**: The system automatically reloads plugin logic when a file is updated or a new plugin is installed.
|
|
514
|
+
- **Stability Guardrails**: Consecutive plugin failures are tracked in `data/plugin-failures.json`; failing plugins are auto-disabled, a warning notification is emitted in-app, and users can re-enable manually from the Plugins manager.
|
|
500
515
|
|
|
501
516
|
## Deploy to a VPS
|
|
502
517
|
|
|
@@ -586,7 +601,7 @@ npm run dev:webpack
|
|
|
586
601
|
### First-Run Helpers
|
|
587
602
|
|
|
588
603
|
```bash
|
|
589
|
-
npm run setup:easy # setup only (does not start server)
|
|
604
|
+
npm run setup:easy # setup only (installs Deno if missing; does not start server)
|
|
590
605
|
npm run quickstart # setup + start dev server
|
|
591
606
|
npm run quickstart:prod # setup + build + start production server
|
|
592
607
|
npm run update:easy # safe update helper for local installs
|
|
@@ -597,8 +612,8 @@ npm run update:easy # safe update helper for local installs
|
|
|
597
612
|
SwarmClaw uses tag-based releases (`vX.Y.Z`) as the stable channel.
|
|
598
613
|
|
|
599
614
|
```bash
|
|
600
|
-
# example
|
|
601
|
-
npm version
|
|
615
|
+
# example minor release (v0.7.0 style)
|
|
616
|
+
npm version minor
|
|
602
617
|
git push origin main --follow-tags
|
|
603
618
|
```
|
|
604
619
|
|
|
@@ -607,6 +622,16 @@ On `v*` tags, GitHub Actions will:
|
|
|
607
622
|
2. Create a GitHub Release
|
|
608
623
|
3. Build and publish Docker images to `ghcr.io/swarmclawai/swarmclaw` (`:vX.Y.Z`, `:latest`, `:sha-*`)
|
|
609
624
|
|
|
625
|
+
#### v0.7.0 Release Readiness Notes
|
|
626
|
+
|
|
627
|
+
Before shipping `v0.7.0`, confirm the following user-facing changes are reflected in docs:
|
|
628
|
+
|
|
629
|
+
1. Plugins UI now shows richer installed plugin details (description fallback, source/status, capability badges, and clearer detail sheet metadata).
|
|
630
|
+
2. Marketplace plugin installs normalize legacy URLs from `swarmclawai/plugins` to `swarmclawai/swarmforge` to avoid 404 installs.
|
|
631
|
+
3. Hydration fixes removed nested `<button>` structures in list-card UIs (Plugins, Providers, Secrets, MCP Servers).
|
|
632
|
+
4. Clipboard actions now use a browser-safe fallback when `navigator.clipboard` is unavailable.
|
|
633
|
+
5. Site docs/release notes are updated in `swarmclaw-site` (especially `content/docs/plugins.md`, `content/docs/release-notes.md`, and `public/registry/plugins.json`).
|
|
634
|
+
|
|
610
635
|
## CLI
|
|
611
636
|
|
|
612
637
|
SwarmClaw ships a built-in CLI for core operational workflows:
|
package/next.config.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { NextConfig } from "next";
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
|
+
import { networkInterfaces } from "os";
|
|
4
|
+
import { DIRECT_NAV_SEGMENTS } from "./view-route-paths";
|
|
3
5
|
|
|
4
6
|
function getGitSha(): string {
|
|
5
7
|
try {
|
|
@@ -9,6 +11,33 @@ function getGitSha(): string {
|
|
|
9
11
|
}
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
function getAllowedDevOrigins(): string[] {
|
|
15
|
+
const allowed = new Set<string>([
|
|
16
|
+
'localhost',
|
|
17
|
+
'127.0.0.1',
|
|
18
|
+
'0.0.0.0',
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
// Include all active local IPv4 interfaces so LAN devices can load /_next assets in dev.
|
|
22
|
+
for (const interfaces of Object.values(networkInterfaces())) {
|
|
23
|
+
for (const iface of interfaces ?? []) {
|
|
24
|
+
if ((iface.family === 'IPv4' || (iface.family as string | number) === 4) && !iface.internal) {
|
|
25
|
+
allowed.add(iface.address)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Optional override for custom origins/hosts, e.g. `NEXT_ALLOWED_DEV_ORIGINS=host1,host2`.
|
|
31
|
+
const extra = (process.env.NEXT_ALLOWED_DEV_ORIGINS ?? '')
|
|
32
|
+
.split(',')
|
|
33
|
+
.map((v) => v.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.map((v) => v.replace(/^https?:\/\//, '').replace(/\/$/, ''))
|
|
36
|
+
for (const host of extra) allowed.add(host)
|
|
37
|
+
|
|
38
|
+
return [...allowed]
|
|
39
|
+
}
|
|
40
|
+
|
|
12
41
|
const nextConfig: NextConfig = {
|
|
13
42
|
output: 'standalone',
|
|
14
43
|
turbopack: {
|
|
@@ -35,13 +64,9 @@ const nextConfig: NextConfig = {
|
|
|
35
64
|
'@whiskeysockets/baileys',
|
|
36
65
|
'qrcode',
|
|
37
66
|
],
|
|
38
|
-
allowedDevOrigins:
|
|
39
|
-
'localhost',
|
|
40
|
-
'127.0.0.1',
|
|
41
|
-
'0.0.0.0',
|
|
42
|
-
],
|
|
67
|
+
allowedDevOrigins: getAllowedDevOrigins(),
|
|
43
68
|
async rewrites() {
|
|
44
|
-
const views = '
|
|
69
|
+
const views = DIRECT_NAV_SEGMENTS.join('|')
|
|
45
70
|
return [
|
|
46
71
|
{
|
|
47
72
|
source: `/:view(${views})`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"lint:baseline:update": "node ./scripts/lint-baseline.mjs update",
|
|
53
53
|
"cli": "node ./bin/swarmclaw.js",
|
|
54
54
|
"test:cli": "node --test src/cli/index.test.js",
|
|
55
|
-
"test:openclaw": "tsx --test src/lib/server/connectors/openclaw.test.ts src/lib/openclaw-endpoint.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/tool-capability-policy.test.ts",
|
|
55
|
+
"test:openclaw": "tsx --test src/lib/server/connectors/openclaw.test.ts src/lib/openclaw-endpoint.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/server/task-validation.test.ts src/lib/server/task-quality-gate.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts",
|
|
56
|
+
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
56
57
|
"postinstall": "node ./scripts/postinstall.mjs"
|
|
57
58
|
},
|
|
58
59
|
"dependencies": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
|
-
import { loadAgents, saveAgents, logActivity } from '@/lib/server/storage'
|
|
3
|
+
import { loadAgents, loadSessions, loadUsage, saveAgents, logActivity } from '@/lib/server/storage'
|
|
4
4
|
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
5
5
|
import { notify } from '@/lib/server/ws-hub'
|
|
6
|
-
import {
|
|
6
|
+
import { getAgentSpendWindows } from '@/lib/server/cost'
|
|
7
7
|
import { AgentCreateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
8
8
|
import { z } from 'zod'
|
|
9
9
|
export const dynamic = 'force-dynamic'
|
|
@@ -11,10 +11,19 @@ export const dynamic = 'force-dynamic'
|
|
|
11
11
|
|
|
12
12
|
export async function GET(req: Request) {
|
|
13
13
|
const agents = loadAgents()
|
|
14
|
-
|
|
14
|
+
const sessions = loadSessions()
|
|
15
|
+
const usage = loadUsage()
|
|
16
|
+
// Enrich agents that have spend limits with current spend windows
|
|
15
17
|
for (const agent of Object.values(agents)) {
|
|
16
|
-
if (
|
|
17
|
-
agent.
|
|
18
|
+
if (
|
|
19
|
+
(typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0)
|
|
20
|
+
|| (typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0)
|
|
21
|
+
|| (typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0)
|
|
22
|
+
) {
|
|
23
|
+
const spend = getAgentSpendWindows(agent.id, Date.now(), { sessions, usage })
|
|
24
|
+
if (typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0) agent.monthlySpend = spend.monthly
|
|
25
|
+
if (typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0) agent.dailySpend = spend.daily
|
|
26
|
+
if (typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0) agent.hourlySpend = spend.hourly
|
|
18
27
|
}
|
|
19
28
|
}
|
|
20
29
|
|
|
@@ -54,6 +63,10 @@ export async function POST(req: Request) {
|
|
|
54
63
|
capabilities: body.capabilities,
|
|
55
64
|
thinkingLevel: body.thinkingLevel || undefined,
|
|
56
65
|
autoRecovery: body.autoRecovery || false,
|
|
66
|
+
monthlyBudget: body.monthlyBudget ?? null,
|
|
67
|
+
dailyBudget: body.dailyBudget ?? null,
|
|
68
|
+
hourlyBudget: body.hourlyBudget ?? null,
|
|
69
|
+
budgetAction: body.budgetAction || 'warn',
|
|
57
70
|
soul: body.soul || undefined,
|
|
58
71
|
createdAt: now,
|
|
59
72
|
updatedAt: now,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { listPendingApprovals, submitDecision } from '@/lib/server/approvals'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
return NextResponse.json(listPendingApprovals())
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function POST(req: Request) {
|
|
11
|
+
try {
|
|
12
|
+
const body = await req.json()
|
|
13
|
+
const { id, approved } = body
|
|
14
|
+
if (!id || typeof approved !== 'boolean') {
|
|
15
|
+
return NextResponse.json({ error: 'id and approved required' }, { status: 400 })
|
|
16
|
+
}
|
|
17
|
+
await submitDecision(id, approved)
|
|
18
|
+
return NextResponse.json({ ok: true })
|
|
19
|
+
} catch (err: unknown) {
|
|
20
|
+
return NextResponse.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 })
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -11,9 +11,9 @@ export async function POST(req: Request) {
|
|
|
11
11
|
if (!content) {
|
|
12
12
|
try {
|
|
13
13
|
content = await fetchSkillContent(url)
|
|
14
|
-
} catch (err:
|
|
14
|
+
} catch (err: unknown) {
|
|
15
15
|
return NextResponse.json(
|
|
16
|
-
{ error: err.message
|
|
16
|
+
{ error: err instanceof Error ? err.message : 'Failed to fetch skill content' },
|
|
17
17
|
{ status: 502 }
|
|
18
18
|
)
|
|
19
19
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadMcpServers } from '@/lib/server/storage'
|
|
3
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
|
+
import { runMcpConformanceCheck } from '@/lib/server/mcp-conformance'
|
|
5
|
+
|
|
6
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
7
|
+
const { id } = await params
|
|
8
|
+
const servers = loadMcpServers()
|
|
9
|
+
const server = servers[id]
|
|
10
|
+
if (!server) return notFound()
|
|
11
|
+
|
|
12
|
+
const body = await req.json().catch(() => ({}))
|
|
13
|
+
const timeoutMs = typeof body?.timeoutMs === 'number' ? body.timeoutMs : undefined
|
|
14
|
+
const smokeToolName = typeof body?.smokeToolName === 'string' ? body.smokeToolName : undefined
|
|
15
|
+
const smokeToolArgs = body?.smokeToolArgs && typeof body.smokeToolArgs === 'object' && !Array.isArray(body.smokeToolArgs)
|
|
16
|
+
? body.smokeToolArgs
|
|
17
|
+
: undefined
|
|
18
|
+
|
|
19
|
+
const result = await runMcpConformanceCheck(server, {
|
|
20
|
+
timeoutMs,
|
|
21
|
+
smokeToolName,
|
|
22
|
+
smokeToolArgs,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return NextResponse.json(result, { status: result.ok ? 200 : 502 })
|
|
26
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadMcpServers } from '@/lib/server/storage'
|
|
3
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
|
+
import { connectMcpServer, disconnectMcpServer } from '@/lib/server/mcp-client'
|
|
5
|
+
|
|
6
|
+
function parseArgs(value: unknown): Record<string, unknown> {
|
|
7
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
8
|
+
return value as Record<string, unknown>
|
|
9
|
+
}
|
|
10
|
+
if (typeof value === 'string' && value.trim()) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(value)
|
|
13
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
14
|
+
return parsed as Record<string, unknown>
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
// Handled by caller via fallback validation error.
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
24
|
+
const { id } = await params
|
|
25
|
+
const servers = loadMcpServers()
|
|
26
|
+
const server = servers[id]
|
|
27
|
+
if (!server) return notFound()
|
|
28
|
+
|
|
29
|
+
const body = await req.json().catch(() => null)
|
|
30
|
+
const toolName = typeof body?.toolName === 'string' ? body.toolName.trim() : ''
|
|
31
|
+
if (!toolName) {
|
|
32
|
+
return NextResponse.json({ error: 'toolName is required' }, { status: 400 })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const argsRaw = body?.args
|
|
36
|
+
if (
|
|
37
|
+
argsRaw !== undefined
|
|
38
|
+
&& typeof argsRaw !== 'string'
|
|
39
|
+
&& (typeof argsRaw !== 'object' || Array.isArray(argsRaw))
|
|
40
|
+
) {
|
|
41
|
+
return NextResponse.json({ error: 'args must be an object or JSON string' }, { status: 400 })
|
|
42
|
+
}
|
|
43
|
+
const args = parseArgs(argsRaw)
|
|
44
|
+
|
|
45
|
+
let client: unknown
|
|
46
|
+
let transport: unknown
|
|
47
|
+
try {
|
|
48
|
+
const conn = await connectMcpServer(server)
|
|
49
|
+
client = conn.client
|
|
50
|
+
transport = conn.transport
|
|
51
|
+
const result = await (client as { callTool: (opts: { name: string; arguments: Record<string, unknown> }) => Promise<Record<string, unknown>> }).callTool({
|
|
52
|
+
name: toolName,
|
|
53
|
+
arguments: args,
|
|
54
|
+
})
|
|
55
|
+
const textParts = Array.isArray(result?.content)
|
|
56
|
+
? (result.content as Array<Record<string, unknown>>)
|
|
57
|
+
.filter((part) => part?.type === 'text' && typeof part?.text === 'string')
|
|
58
|
+
.map((part) => part.text as string)
|
|
59
|
+
: []
|
|
60
|
+
const text = textParts.join('\n').trim() || '(no text output)'
|
|
61
|
+
|
|
62
|
+
return NextResponse.json({
|
|
63
|
+
ok: true,
|
|
64
|
+
toolName,
|
|
65
|
+
args,
|
|
66
|
+
text,
|
|
67
|
+
result,
|
|
68
|
+
isError: result?.isError === true,
|
|
69
|
+
})
|
|
70
|
+
} catch (err: unknown) {
|
|
71
|
+
return NextResponse.json(
|
|
72
|
+
{ ok: false, error: err instanceof Error ? err.message : 'MCP tool invocation failed' },
|
|
73
|
+
{ status: 500 },
|
|
74
|
+
)
|
|
75
|
+
} finally {
|
|
76
|
+
if (client && transport) {
|
|
77
|
+
await disconnectMcpServer(client, transport)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { genId } from '@/lib/id'
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import { NextResponse } from 'next/server'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
filterMemoriesByScope,
|
|
6
|
+
getMemoryDb,
|
|
7
|
+
getMemoryLookupLimits,
|
|
8
|
+
normalizeMemoryScopeMode,
|
|
9
|
+
storeMemoryImageAsset,
|
|
10
|
+
storeMemoryImageFromDataUrl,
|
|
11
|
+
type MemoryRerankMode,
|
|
12
|
+
type MemoryScopeFilter,
|
|
13
|
+
} from '@/lib/server/memory-db'
|
|
5
14
|
import { resolveLookupRequest } from '@/lib/server/memory-graph'
|
|
6
15
|
import type { MemoryReference, FileReference, MemoryImage } from '@/types'
|
|
7
16
|
|
|
@@ -21,10 +30,16 @@ export async function GET(req: Request) {
|
|
|
21
30
|
const { searchParams } = new URL(req.url)
|
|
22
31
|
const q = searchParams.get('q')
|
|
23
32
|
const agentId = searchParams.get('agentId')
|
|
33
|
+
const rawScope = searchParams.get('scope')
|
|
24
34
|
const envelope = searchParams.get('envelope') === 'true'
|
|
25
35
|
const requestedDepth = parseOptionalInt(searchParams.get('depth'))
|
|
26
36
|
const requestedLimit = parseOptionalInt(searchParams.get('limit'))
|
|
27
37
|
const requestedLinkedLimit = parseOptionalInt(searchParams.get('linkedLimit'))
|
|
38
|
+
const scopeMode = normalizeMemoryScopeMode(rawScope ?? (agentId ? 'agent' : 'all'))
|
|
39
|
+
const scopeSessionId = searchParams.get('scopeSessionId')
|
|
40
|
+
const scopeProjectRoot = searchParams.get('projectRoot')
|
|
41
|
+
const rerankRaw = searchParams.get('rerank')
|
|
42
|
+
const rerankMode: MemoryRerankMode = rerankRaw === 'semantic' || rerankRaw === 'lexical' ? rerankRaw : 'balanced'
|
|
28
43
|
|
|
29
44
|
const counts = searchParams.get('counts') === 'true'
|
|
30
45
|
const db = getMemoryDb()
|
|
@@ -39,14 +54,27 @@ export async function GET(req: Request) {
|
|
|
39
54
|
limit: requestedLimit,
|
|
40
55
|
linkedLimit: requestedLinkedLimit,
|
|
41
56
|
})
|
|
57
|
+
const scopeFilter: MemoryScopeFilter = {
|
|
58
|
+
mode: scopeMode,
|
|
59
|
+
agentId: agentId || null,
|
|
60
|
+
sessionId: scopeSessionId || null,
|
|
61
|
+
projectRoot: scopeProjectRoot || null,
|
|
62
|
+
}
|
|
42
63
|
|
|
43
64
|
if (q) {
|
|
44
65
|
if (limits.maxDepth > 0) {
|
|
45
|
-
const result = db.searchWithLinked(
|
|
66
|
+
const result = db.searchWithLinked(
|
|
67
|
+
q,
|
|
68
|
+
agentId || undefined,
|
|
69
|
+
limits.maxDepth,
|
|
70
|
+
limits.maxPerLookup,
|
|
71
|
+
limits.maxLinkedExpansion,
|
|
72
|
+
{ scope: scopeFilter, rerankMode },
|
|
73
|
+
)
|
|
46
74
|
if (envelope) return NextResponse.json(result)
|
|
47
75
|
return NextResponse.json(result.entries)
|
|
48
76
|
}
|
|
49
|
-
const base = db.search(q, agentId || undefined)
|
|
77
|
+
const base = db.search(q, agentId || undefined, { scope: scopeFilter, rerankMode })
|
|
50
78
|
const entries = base.slice(0, limits.maxPerLookup)
|
|
51
79
|
if (envelope) {
|
|
52
80
|
return NextResponse.json({
|
|
@@ -59,11 +87,14 @@ export async function GET(req: Request) {
|
|
|
59
87
|
return NextResponse.json(entries)
|
|
60
88
|
}
|
|
61
89
|
|
|
62
|
-
const
|
|
90
|
+
const scanLimit = Math.max(limits.maxPerLookup, 200)
|
|
91
|
+
const listed = db.list(undefined, scanLimit)
|
|
92
|
+
const filtered = filterMemoriesByScope(listed, scopeFilter)
|
|
93
|
+
const entries = filtered.slice(0, limits.maxPerLookup)
|
|
63
94
|
if (envelope) {
|
|
64
95
|
return NextResponse.json({
|
|
65
96
|
entries,
|
|
66
|
-
truncated:
|
|
97
|
+
truncated: filtered.length > entries.length,
|
|
67
98
|
expandedLinkedCount: 0,
|
|
68
99
|
limits,
|
|
69
100
|
})
|
|
@@ -13,6 +13,9 @@ export async function GET(req: Request) {
|
|
|
13
13
|
const all = loadNotifications()
|
|
14
14
|
let entries = Object.values(all) as AppNotification[]
|
|
15
15
|
|
|
16
|
+
// Approval requests now have a dedicated Approvals view/badge; keep notifications focused on ops/events.
|
|
17
|
+
entries = entries.filter((e) => e.entityType !== 'approval')
|
|
18
|
+
|
|
16
19
|
if (unreadOnly) {
|
|
17
20
|
entries = entries.filter((e) => !e.read)
|
|
18
21
|
}
|
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import path from 'path'
|
|
4
|
+
import { getPluginManager } from '@/lib/server/plugins'
|
|
4
5
|
|
|
5
6
|
const PLUGINS_DIR = path.join(process.cwd(), 'data', 'plugins')
|
|
6
7
|
|
|
8
|
+
function toRawUrl(url: string): string {
|
|
9
|
+
if (url.includes('github.com') && url.includes('/blob/')) {
|
|
10
|
+
return url.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/')
|
|
11
|
+
}
|
|
12
|
+
if (url.includes('gist.github.com')) {
|
|
13
|
+
return url.endsWith('/raw') ? url : `${url}/raw`
|
|
14
|
+
}
|
|
15
|
+
return url
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeMarketplaceUrl(url: string): string {
|
|
19
|
+
const trimmed = typeof url === 'string' ? url.trim() : ''
|
|
20
|
+
if (!trimmed) return trimmed
|
|
21
|
+
|
|
22
|
+
let normalized = trimmed
|
|
23
|
+
.replace('github.com/swarmclawai/plugins/', 'github.com/swarmclawai/swarmforge/')
|
|
24
|
+
.replace('raw.githubusercontent.com/swarmclawai/plugins/', 'raw.githubusercontent.com/swarmclawai/swarmforge/')
|
|
25
|
+
|
|
26
|
+
normalized = toRawUrl(normalized)
|
|
27
|
+
|
|
28
|
+
// Legacy registry entries used master and old repo names.
|
|
29
|
+
normalized = normalized
|
|
30
|
+
.replace('/swarmclawai/swarmforge/master/', '/swarmclawai/swarmforge/main/')
|
|
31
|
+
.replace('/swarmclawai/plugins/master/', '/swarmclawai/swarmforge/main/')
|
|
32
|
+
.replace('/swarmclawai/plugins/main/', '/swarmclawai/swarmforge/main/')
|
|
33
|
+
|
|
34
|
+
return normalized
|
|
35
|
+
}
|
|
36
|
+
|
|
7
37
|
export async function POST(req: Request) {
|
|
8
38
|
const body = await req.json()
|
|
9
39
|
const { url, filename } = body
|
|
40
|
+
const rawUrl = normalizeMarketplaceUrl(url)
|
|
10
41
|
|
|
11
42
|
// Validate URL
|
|
12
43
|
if (!url || typeof url !== 'string' || !url.startsWith('https://')) {
|
|
@@ -34,11 +65,27 @@ export async function POST(req: Request) {
|
|
|
34
65
|
}
|
|
35
66
|
|
|
36
67
|
try {
|
|
37
|
-
const res = await fetch(
|
|
68
|
+
const res = await fetch(rawUrl, { signal: AbortSignal.timeout(15_000) })
|
|
38
69
|
if (!res.ok) {
|
|
39
|
-
|
|
70
|
+
return NextResponse.json(
|
|
71
|
+
{ error: `Download failed (HTTP ${res.status}) from ${rawUrl}` },
|
|
72
|
+
{ status: 502 },
|
|
73
|
+
)
|
|
40
74
|
}
|
|
41
|
-
const
|
|
75
|
+
const contentType = res.headers.get('content-type') || ''
|
|
76
|
+
let code = await res.text()
|
|
77
|
+
|
|
78
|
+
// Reject HTML responses (likely a GitHub page, not raw content)
|
|
79
|
+
if (contentType.includes('text/html') && code.includes('<!DOCTYPE')) {
|
|
80
|
+
return NextResponse.json(
|
|
81
|
+
{ error: 'URL returned an HTML page instead of JavaScript. Use a raw/direct link to the .js file.' },
|
|
82
|
+
{ status: 400 },
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Compatibility fix: Strip node-fetch requires if present, as modern Node has global fetch
|
|
87
|
+
code = code.replace(/const\s+fetch\s*=\s*require\(['"]node-fetch['"]\);?/g, '// node-fetch stripped for compatibility')
|
|
88
|
+
code = code.replace(/import\s+fetch\s+from\s+['"]node-fetch['"];?/g, '// node-fetch stripped for compatibility')
|
|
42
89
|
|
|
43
90
|
// Ensure plugins directory exists
|
|
44
91
|
if (!fs.existsSync(PLUGINS_DIR)) {
|
|
@@ -48,11 +95,16 @@ export async function POST(req: Request) {
|
|
|
48
95
|
const dest = path.join(PLUGINS_DIR, sanitized)
|
|
49
96
|
fs.writeFileSync(dest, code, 'utf8')
|
|
50
97
|
|
|
98
|
+
// Force plugin manager to re-scan so the new plugin appears in listings
|
|
99
|
+
getPluginManager().reload()
|
|
100
|
+
|
|
51
101
|
return NextResponse.json({ ok: true, filename: sanitized })
|
|
52
102
|
} catch (err: unknown) {
|
|
103
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
104
|
+
const isTimeout = msg.includes('abort') || msg.includes('timeout')
|
|
53
105
|
return NextResponse.json(
|
|
54
|
-
{ error: '
|
|
55
|
-
{ status: 500 },
|
|
106
|
+
{ error: isTimeout ? 'Download timed out — the plugin URL may be unreachable' : `Install failed: ${msg}` },
|
|
107
|
+
{ status: isTimeout ? 504 : 500 },
|
|
56
108
|
)
|
|
57
109
|
}
|
|
58
110
|
}
|