@flame0510/project-aether 1.1.9
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 +150 -0
- package/agent-templates/README.md +37 -0
- package/agent-templates/argus/AGENTS.md +52 -0
- package/agent-templates/argus/IDENTITY.md +7 -0
- package/agent-templates/argus/MEMORY.md +9 -0
- package/agent-templates/argus/SOUL.md +24 -0
- package/agent-templates/argus/TOOLS.md +8 -0
- package/agent-templates/atlas/AGENTS.md +47 -0
- package/agent-templates/atlas/HEARTBEAT.md +10 -0
- package/agent-templates/atlas/IDENTITY.md +6 -0
- package/agent-templates/atlas/MEMORY.md +9 -0
- package/agent-templates/atlas/SOUL.md +21 -0
- package/agent-templates/atlas/TOOLS.md +4 -0
- package/agent-templates/base-image/.trigger +0 -0
- package/agent-templates/base-image/Dockerfile +90 -0
- package/agent-templates/base-image/entrypoint.sh +37 -0
- package/agent-templates/custom/AGENTS.md +29 -0
- package/agent-templates/custom/BOOTSTRAP.md +56 -0
- package/agent-templates/custom/MEMORY.md +8 -0
- package/agent-templates/prometheus/AGENTS.md +37 -0
- package/agent-templates/prometheus/IDENTITY.md +6 -0
- package/agent-templates/prometheus/MEMORY.md +9 -0
- package/agent-templates/prometheus/SOUL.md +21 -0
- package/agent-templates/prometheus/TOOLS.md +4 -0
- package/app/agents/ChannelManager.tsx +465 -0
- package/app/agents/ImageDownloadBanner.tsx +159 -0
- package/app/agents/PageClient.tsx +1285 -0
- package/app/agents/create/PageClient.tsx +437 -0
- package/app/agents/create/page.tsx +126 -0
- package/app/agents/loading.tsx +14 -0
- package/app/agents/page.tsx +5 -0
- package/app/api/agents/[id]/backup/route.ts +169 -0
- package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
- package/app/api/agents/[id]/channels/route.ts +14 -0
- package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
- package/app/api/agents/[id]/lifecycle/route.ts +94 -0
- package/app/api/agents/[id]/recreate/route.ts +199 -0
- package/app/api/agents/[id]/restart/route.ts +33 -0
- package/app/api/agents/[id]/restore/route.ts +81 -0
- package/app/api/agents/[id]/route.ts +280 -0
- package/app/api/agents/channels-summary/route.ts +39 -0
- package/app/api/agents/config-json/route.ts +58 -0
- package/app/api/agents/create/route.ts +488 -0
- package/app/api/agents/download-image/route.ts +32 -0
- package/app/api/agents/image-status/route.ts +126 -0
- package/app/api/agents/route.ts +213 -0
- package/app/api/agents/token/route.ts +94 -0
- package/app/api/agents-active/route.ts +190 -0
- package/app/api/agents-config/route.ts +629 -0
- package/app/api/alerts/smoke/route.ts +26 -0
- package/app/api/aliases/add/route.ts +28 -0
- package/app/api/aliases/remove/route.ts +27 -0
- package/app/api/assistant/route.ts +204 -0
- package/app/api/auth/check/route.ts +38 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/logout/route.ts +7 -0
- package/app/api/auth/status/route.ts +33 -0
- package/app/api/config/env/route.ts +132 -0
- package/app/api/config/restart/route.ts +26 -0
- package/app/api/containers/route.ts +75 -0
- package/app/api/cost-override/route.ts +46 -0
- package/app/api/costs/route.ts +84 -0
- package/app/api/credentials/[id]/reveal/route.ts +21 -0
- package/app/api/credentials/[id]/route.ts +52 -0
- package/app/api/credentials/[id]/sync/route.ts +58 -0
- package/app/api/credentials/detect/route.ts +246 -0
- package/app/api/credentials/route.ts +33 -0
- package/app/api/crons/[id]/route.ts +69 -0
- package/app/api/crons/route.ts +12 -0
- package/app/api/crons/runs/route.ts +31 -0
- package/app/api/debug/route.ts +11 -0
- package/app/api/envcheck/route.ts +18 -0
- package/app/api/events/route.ts +24 -0
- package/app/api/gateway/agent/route.ts +154 -0
- package/app/api/gateway/provider/balance/route.ts +38 -0
- package/app/api/gateway/provider/keys.ts +38 -0
- package/app/api/gateway/provider/oauth/route.ts +304 -0
- package/app/api/gateway/provider/route.ts +209 -0
- package/app/api/gateway/route.ts +223 -0
- package/app/api/gateway/sync.ts +189 -0
- package/app/api/lineage/route.ts +54 -0
- package/app/api/memory-context/route.ts +15 -0
- package/app/api/metrics/route.ts +35 -0
- package/app/api/models/route.ts +77 -0
- package/app/api/plugins/route.js +37 -0
- package/app/api/provider/upstream.ts +142 -0
- package/app/api/provider/v1/chat/completions/route.ts +146 -0
- package/app/api/provider/v1/models/route.ts +167 -0
- package/app/api/provider-usage/route.ts +629 -0
- package/app/api/session/route.ts +39 -0
- package/app/api/sessions/[id]/route.ts +38 -0
- package/app/api/sessions/route.ts +23 -0
- package/app/api/setup/agent-image/route.ts +102 -0
- package/app/api/setup/password/route.ts +111 -0
- package/app/api/setup/restart/route.ts +76 -0
- package/app/api/skills/delete/route.js +105 -0
- package/app/api/skills/promote/route.js +129 -0
- package/app/api/skills/route.js +203 -0
- package/app/api/skills/save/route.js +142 -0
- package/app/api/stats/route.ts +34 -0
- package/app/api/stats-since/route.ts +20 -0
- package/app/api/stream/route.ts +70 -0
- package/app/api/system-health/route.ts +233 -0
- package/app/api/tool-calls/route.ts +24 -0
- package/app/api/tools-config/route.ts +163 -0
- package/app/api/update-check/route.ts +127 -0
- package/app/api/version/route.ts +25 -0
- package/app/api/wizard/complete/route.ts +32 -0
- package/app/api/wizard/reset/route.ts +28 -0
- package/app/api/wizard/status/route.ts +28 -0
- package/app/api/workspace/route.ts +392 -0
- package/app/api/workspace/stream/route.ts +136 -0
- package/app/components/AppShell.tsx +33 -0
- package/app/components/AuthGuard.tsx +91 -0
- package/app/components/CostBreakdown.tsx +37 -0
- package/app/components/DashboardHeader.tsx +73 -0
- package/app/components/DashboardLayout.tsx +76 -0
- package/app/components/DashboardToolbar.tsx +61 -0
- package/app/components/FloatingActions.tsx +33 -0
- package/app/components/Icons.tsx +78 -0
- package/app/components/LineageGraphPage.tsx +142 -0
- package/app/components/LiveFeed.tsx +48 -0
- package/app/components/MobileBottomNav.tsx +84 -0
- package/app/components/ModelPickerModal.tsx +94 -0
- package/app/components/PasswordInput.tsx +86 -0
- package/app/components/PulseChat.tsx +362 -0
- package/app/components/Rev4aLoader.tsx +48 -0
- package/app/components/SessionDrawer.tsx +263 -0
- package/app/components/SessionTopology.tsx +289 -0
- package/app/components/Sidebar.tsx +294 -0
- package/app/components/Skeleton.tsx +133 -0
- package/app/components/SystemCockpit.tsx +169 -0
- package/app/components/VersionBanner.tsx +105 -0
- package/app/components/WizardButton.tsx +30 -0
- package/app/components/ui/Badge.tsx +30 -0
- package/app/components/ui/Button.tsx +59 -0
- package/app/components/ui/ConfirmModal.tsx +46 -0
- package/app/components/ui/FilterBar.tsx +33 -0
- package/app/components/ui/Input.tsx +25 -0
- package/app/components/ui/ItemList.tsx +83 -0
- package/app/components/ui/LoadingSpinner.tsx +30 -0
- package/app/components/ui/Metric.tsx +22 -0
- package/app/components/ui/Modal.tsx +42 -0
- package/app/components/ui/Page.tsx +18 -0
- package/app/components/ui/Pill.tsx +6 -0
- package/app/components/ui/PropertyList.tsx +24 -0
- package/app/components/ui/Select.tsx +26 -0
- package/app/components/ui/StatusCard.tsx +32 -0
- package/app/components/ui/Surface.tsx +14 -0
- package/app/components/ui/Tabs.tsx +55 -0
- package/app/components/ui/TemplateOption.tsx +59 -0
- package/app/components/ui/Toast.tsx +44 -0
- package/app/components/ui/index.ts +19 -0
- package/app/components/ui/tokens.ts +18 -0
- package/app/config/PageClient.tsx +293 -0
- package/app/config/loading.tsx +10 -0
- package/app/config/page.tsx +7 -0
- package/app/containers/ContainersClient.tsx +129 -0
- package/app/containers/loading.tsx +21 -0
- package/app/containers/page.tsx +5 -0
- package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
- package/app/containers/terminal/[id]/page.tsx +6 -0
- package/app/credentials/PageClient.tsx +598 -0
- package/app/credentials/loading.tsx +10 -0
- package/app/credentials/page.tsx +7 -0
- package/app/crons/PageClient.tsx +538 -0
- package/app/crons/loading.tsx +10 -0
- package/app/crons/page.tsx +5 -0
- package/app/design-system.ts +57 -0
- package/app/gateway/PageClient.tsx +1048 -0
- package/app/gateway/loading.tsx +24 -0
- package/app/gateway/page.tsx +5 -0
- package/app/globals.css +1474 -0
- package/app/layout.tsx +45 -0
- package/app/lib/models-context.tsx +41 -0
- package/app/lineage/loading.tsx +10 -0
- package/app/lineage/page.tsx +13 -0
- package/app/loading.tsx +16 -0
- package/app/login/page.tsx +160 -0
- package/app/memory/MemoryContextPageClient.tsx +196 -0
- package/app/memory/loading.tsx +10 -0
- package/app/memory/page.tsx +8 -0
- package/app/page.tsx +31 -0
- package/app/plugins/PageClient.tsx +296 -0
- package/app/plugins/loading.tsx +10 -0
- package/app/plugins/page.tsx +5 -0
- package/app/setup/PageClient.tsx +227 -0
- package/app/setup/loading.tsx +21 -0
- package/app/setup/page.tsx +7 -0
- package/app/skills/PageClient.tsx +499 -0
- package/app/skills/loading.tsx +10 -0
- package/app/skills/page.tsx +5 -0
- package/app/tools/ToolsPageClient.tsx +308 -0
- package/app/tools/loading.tsx +10 -0
- package/app/tools/page.tsx +62 -0
- package/app/tools/tools-catalog.ts +141 -0
- package/app/wizard/PageClient.tsx +485 -0
- package/app/wizard/icons.tsx +89 -0
- package/app/wizard/loading.tsx +24 -0
- package/app/wizard/page.tsx +7 -0
- package/app/wizard/storage.ts +33 -0
- package/app/wizard/useWizard.ts +101 -0
- package/app/workspace/WorkspaceClient.tsx +633 -0
- package/app/workspace/loading.tsx +36 -0
- package/app/workspace/page.tsx +8 -0
- package/bin/docker-entrypoint.sh +21 -0
- package/bin/postinstall.js +20 -0
- package/bin/rev4a.js +696 -0
- package/daemon.js +783 -0
- package/docs/ARCHITECTURE.md +557 -0
- package/docs/CONTAINER-TERMINAL.md +280 -0
- package/docs/DESIGN-SYSTEM.md +99 -0
- package/docs/FRONTEND-ARCHITECTURE.md +119 -0
- package/docs/REV4A.md +504 -0
- package/docs/dev/API-REFERENCE.md +1835 -0
- package/docs/dev/DATABASE.md +224 -0
- package/docs/dev/GATEWAY.md +300 -0
- package/docs/dev/PROVIDERS.md +229 -0
- package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
- package/docs/dev/WORKSPACE.md +199 -0
- package/docs/rag/DATA-FRESHNESS.md +109 -0
- package/docs/rag/GLOSSARY.md +125 -0
- package/docs/rag/REV4A-OVERVIEW.md +135 -0
- package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
- package/lib/agent-setup.ts +130 -0
- package/lib/alerts.ts +115 -0
- package/lib/apiFetch.ts +7 -0
- package/lib/auth.ts +28 -0
- package/lib/billing.ts +100 -0
- package/lib/buildAgentImage.ts +346 -0
- package/lib/channelManager.ts +386 -0
- package/lib/container.ts +60 -0
- package/lib/credentials/db.ts +86 -0
- package/lib/credentials/delivery.ts +448 -0
- package/lib/credentials/detect.ts +149 -0
- package/lib/credentials/hash.ts +9 -0
- package/lib/credentials/providers.ts +110 -0
- package/lib/credentials/vault.ts +195 -0
- package/lib/db-bootstrap.d.ts +3 -0
- package/lib/db-bootstrap.mjs +144 -0
- package/lib/db.ts +25 -0
- package/lib/docker-utils.ts +60 -0
- package/lib/hooks/useDashboard.ts +264 -0
- package/lib/hooks/useRev4aTimezone.ts +28 -0
- package/lib/hooks/useScreenshot.ts +30 -0
- package/lib/memory-context.ts +385 -0
- package/lib/model-catalogue.ts +131 -0
- package/lib/model-pricing.ts +63 -0
- package/lib/openclaw-cron.ts +350 -0
- package/lib/patterns/ApiAdapter.ts +73 -0
- package/lib/patterns/ApiClient.ts +65 -0
- package/lib/patterns/EventBus.ts +113 -0
- package/lib/patterns/FilterStrategy.ts +79 -0
- package/lib/patterns/SessionFactory.ts +117 -0
- package/lib/patterns/sessionPresentation.ts +117 -0
- package/lib/provider-balance.ts +163 -0
- package/lib/requireAuth.tsx +38 -0
- package/lib/rev4a-auth.d.ts +3 -0
- package/lib/rev4a-auth.js +36 -0
- package/lib/rev4a-paths.ts +145 -0
- package/lib/timezone.ts +46 -0
- package/lib/types/index.ts +96 -0
- package/lib/utils/format.ts +82 -0
- package/lineage.js +56 -0
- package/model-pricing.json +482 -0
- package/models.config.json +844 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +7 -0
- package/package.json +82 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-64.png +0 -0
- package/public/favicon.svg +58 -0
- package/public/icon-192-maskable.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512-maskable.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +38 -0
- package/public/rev4a-logo.png +0 -0
- package/public/rev4a-logo.svg +58 -0
- package/rev4a-rules/AGENTS.md +37 -0
- package/scripts/add-auth.cjs +185 -0
- package/scripts/backup.sh +66 -0
- package/scripts/check-providers.mjs +143 -0
- package/scripts/docker/traefik/docker-compose.yml +32 -0
- package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
- package/scripts/docker/traefik/traefik.yml +37 -0
- package/scripts/remove-auth-profile.js +56 -0
- package/scripts/restore.sh +82 -0
- package/terminal-ws-server.js +139 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import * as http from 'http';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { detectPublicHost } from '@/lib/docker-utils';
|
|
6
|
+
import { AGENTS_TOKEN_FILE } from '@/lib/rev4a-paths';
|
|
7
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
8
|
+
|
|
9
|
+
const DOCKER_SOCKET = '/var/run/docker.sock';
|
|
10
|
+
|
|
11
|
+
function dockerFetch(method: string, path: string): Promise<any> {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const options = {
|
|
14
|
+
socketPath: DOCKER_SOCKET,
|
|
15
|
+
path,
|
|
16
|
+
method,
|
|
17
|
+
headers: { 'Host': 'localhost' } as any,
|
|
18
|
+
};
|
|
19
|
+
const req = http.request(options, (res: any) => {
|
|
20
|
+
let data = '';
|
|
21
|
+
res.on('data', (chunk: string) => (data += chunk));
|
|
22
|
+
res.on('end', () => {
|
|
23
|
+
try {
|
|
24
|
+
resolve(JSON.parse(data));
|
|
25
|
+
} catch {
|
|
26
|
+
reject(new Error('Invalid JSON from Docker API'));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
req.on('error', reject);
|
|
31
|
+
req.end();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const dynamic = 'force-dynamic';
|
|
36
|
+
|
|
37
|
+
function dockerAvailable(): boolean {
|
|
38
|
+
try {
|
|
39
|
+
fs.accessSync(DOCKER_SOCKET, fs.constants.R_OK | fs.constants.W_OK);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
47
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
48
|
+
// If Docker is not available (e.g. testing environment), return empty list
|
|
49
|
+
if (!dockerAvailable()) {
|
|
50
|
+
return NextResponse.json([]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// List all containers (including stopped) with AGENT_ID label via Docker API
|
|
55
|
+
const containers: any[] = await dockerFetch(
|
|
56
|
+
'GET',
|
|
57
|
+
'/containers/json?all=true&filters={"label":["AGENT_ID"]}',
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Current image ID — used to detect outdated containers
|
|
61
|
+
let currentImageId = '';
|
|
62
|
+
try {
|
|
63
|
+
const imageInspect = await dockerFetch('GET', '/images/openclaw-agent-base:latest/json');
|
|
64
|
+
currentImageId = imageInspect.Id || '';
|
|
65
|
+
} catch {
|
|
66
|
+
// Image doesn't exist yet — all containers are fine
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const agents = await Promise.all(
|
|
70
|
+
containers.map(async (c: any) => {
|
|
71
|
+
const agentId = c.Labels?.AGENT_ID || 'unknown';
|
|
72
|
+
let ip: string | null = null;
|
|
73
|
+
let created: string | null = null;
|
|
74
|
+
let env: string[] = [];
|
|
75
|
+
let authToken: string | null = null;
|
|
76
|
+
let traefikUrl: string | null = null;
|
|
77
|
+
let portBindings: Record<string, Array<{ HostIp: string; HostPort: string }>> | null = null;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const inspect: any = await dockerFetch(
|
|
81
|
+
'GET',
|
|
82
|
+
`/containers/${c.Id}/json`,
|
|
83
|
+
);
|
|
84
|
+
const networks: Record<string, any> =
|
|
85
|
+
inspect.NetworkSettings?.Networks || {};
|
|
86
|
+
ip =
|
|
87
|
+
Object.values(networks).find(
|
|
88
|
+
(n: any) => n.IPAddress,
|
|
89
|
+
)?.IPAddress || null;
|
|
90
|
+
created = inspect.Created || null;
|
|
91
|
+
env = (inspect.Config?.Env || []).filter((e: string) =>
|
|
92
|
+
e.startsWith('AGENT_') || e.startsWith('MODEL_')
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Ports from inspect (HostConfig) — works even when container is stopped
|
|
96
|
+
portBindings = inspect.HostConfig?.PortBindings || null;
|
|
97
|
+
|
|
98
|
+
// Read auth token — always prefer the shared agents-token.json (user-managed)
|
|
99
|
+
const cName = (c.Names?.[0] || '').replace(/^\//, '');
|
|
100
|
+
try {
|
|
101
|
+
const tokenRaw = fs.readFileSync(
|
|
102
|
+
AGENTS_TOKEN_FILE,
|
|
103
|
+
'utf-8'
|
|
104
|
+
);
|
|
105
|
+
const tokenData = JSON.parse(tokenRaw);
|
|
106
|
+
if (tokenData.token) {
|
|
107
|
+
authToken = tokenData.token;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// agents-token.json unavailable
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Fallback only if shared token is missing: try the container's own gateway token
|
|
114
|
+
if (!authToken) {
|
|
115
|
+
try {
|
|
116
|
+
const { execSync } = require('child_process');
|
|
117
|
+
let token = execSync(
|
|
118
|
+
`docker exec ${cName} sh -c 'grep ^OPENCLAW_GATEWAY_TOKEN= /root/….env 2>/dev/null | cut -d= -f2- | tr -d "\"\""' 2>/dev/null || echo ''`,
|
|
119
|
+
{ timeout: 5000, encoding: 'utf-8' }
|
|
120
|
+
).toString().trim();
|
|
121
|
+
if (token && token !== 'undefined') authToken = token;
|
|
122
|
+
} catch {
|
|
123
|
+
// token unavailable for this container
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Build the agent URL from the host's public IP + mapped port.
|
|
128
|
+
// Every agent has -p <port>:3000, so we look for the public port
|
|
129
|
+
// that maps to the internal port 3000.
|
|
130
|
+
// Use portBindings from inspect (works stopped) with fallback to c.Ports (running only).
|
|
131
|
+
const port3000Binding = portBindings?.['3000/tcp'];
|
|
132
|
+
const publicPort = port3000Binding?.[0]?.HostPort
|
|
133
|
+
|| (c.Ports || []).find((p: any) => p.PublicPort && p.PrivatePort === 3000)?.PublicPort
|
|
134
|
+
|| null;
|
|
135
|
+
if (publicPort) {
|
|
136
|
+
const host = detectPublicHost();
|
|
137
|
+
let baseUrl = `http://${host}:${publicPort}`;
|
|
138
|
+
if (authToken) {
|
|
139
|
+
baseUrl += `#token=${encodeURIComponent(authToken)}`;
|
|
140
|
+
}
|
|
141
|
+
traefikUrl = baseUrl;
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// inspect non-critical, continue with partial data
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Determine template from AGENT_TEMPLATE env var.
|
|
148
|
+
// Falls back to 'custom' if not set (agent created before this feature).
|
|
149
|
+
const image: string = c.Image || '';
|
|
150
|
+
let template: string | null = null;
|
|
151
|
+
if (image.startsWith('openclaw-agent-base')) {
|
|
152
|
+
const templateLine = env.find((e: string) => e.startsWith('AGENT_TEMPLATE='));
|
|
153
|
+
template = templateLine ? templateLine.split('=').slice(1).join('=') : 'custom';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Build ports string from portBindings (works stopped) with fallback to c.Ports (running only)
|
|
157
|
+
let ports = '';
|
|
158
|
+
if (portBindings && Object.keys(portBindings).length > 0) {
|
|
159
|
+
ports = Object.entries(portBindings)
|
|
160
|
+
.flatMap(([containerPort, hostPorts]) =>
|
|
161
|
+
(hostPorts as any[]).map((h: any) => `${h.HostPort}:${containerPort.replace('/tcp', '')}${containerPort.includes('/udp') ? '/udp' : ''}`)
|
|
162
|
+
)
|
|
163
|
+
.join(', ');
|
|
164
|
+
} else {
|
|
165
|
+
ports = (c.Ports || [])
|
|
166
|
+
.filter((p: any) => p.PublicPort)
|
|
167
|
+
.map(
|
|
168
|
+
(p: any) =>
|
|
169
|
+
`${p.PublicPort}:${p.PrivatePort}${p.Type === 'udp' ? '/udp' : ''}`,
|
|
170
|
+
)
|
|
171
|
+
.join(', ');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
id: (c.Id || '').slice(0, 12),
|
|
176
|
+
agentId,
|
|
177
|
+
name: (c.Names?.[0] || '').replace(/^\//, ''),
|
|
178
|
+
displayName: (env.find((e: string) => e.startsWith('AGENT_NAME=')) || '').split('=').slice(1).join('=') || (c.Names?.[0] || '').replace(/^\//, ''),
|
|
179
|
+
image,
|
|
180
|
+
imageTag: image.includes(':') ? image.split(':')[1] || 'latest' : (image.includes('@') ? 'digest' : 'latest'),
|
|
181
|
+
template,
|
|
182
|
+
status: c.Status || 'unknown',
|
|
183
|
+
state: c.State || 'unknown',
|
|
184
|
+
ports,
|
|
185
|
+
ip,
|
|
186
|
+
created,
|
|
187
|
+
env,
|
|
188
|
+
traefikUrl,
|
|
189
|
+
authToken,
|
|
190
|
+
// Flag as outdated when the container's image differs from the current base image.
|
|
191
|
+
// We compare raw ImageID digests — no fragile name-based filtering.
|
|
192
|
+
imageOutdated: currentImageId ? c.ImageID !== currentImageId : false,
|
|
193
|
+
};
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Deduplicate by agentId > use the running one when multiple
|
|
198
|
+
const seen = new Map<string, typeof agents[0]>();
|
|
199
|
+
for (const agent of agents) {
|
|
200
|
+
const existing = seen.get(agent.agentId);
|
|
201
|
+
if (!existing || (agent.state === 'running' && existing.state !== 'running')) {
|
|
202
|
+
seen.set(agent.agentId, agent);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return NextResponse.json(Array.from(seen.values()));
|
|
207
|
+
} catch (e: unknown) {
|
|
208
|
+
return NextResponse.json(
|
|
209
|
+
{ error: (e as Error).message },
|
|
210
|
+
{ status: 500 },
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
4
|
+
import { AGENTS_TOKEN_FILE } from '@/lib/rev4a-paths';
|
|
5
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
interface TokenData {
|
|
10
|
+
token: string;
|
|
11
|
+
updated_at: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readTokenFile(): TokenData {
|
|
15
|
+
try {
|
|
16
|
+
const raw = fs.readFileSync(AGENTS_TOKEN_FILE, 'utf-8');
|
|
17
|
+
const data = JSON.parse(raw);
|
|
18
|
+
return {
|
|
19
|
+
token: typeof data.token === 'string' ? data.token : '',
|
|
20
|
+
updated_at: typeof data.updated_at === 'number' ? data.updated_at : 0,
|
|
21
|
+
};
|
|
22
|
+
} catch {
|
|
23
|
+
return { token: '', updated_at: 0 };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shellQuote(value: string): string {
|
|
28
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
32
|
+
return NextResponse.json(readTokenFile());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function PUT(request: NextRequest): Promise<NextResponse> {
|
|
36
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
37
|
+
try {
|
|
38
|
+
const body = await request.json();
|
|
39
|
+
const token = typeof body?.token === 'string' ? body.token : null;
|
|
40
|
+
|
|
41
|
+
if (token === null) {
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{ success: false, error: 'Invalid token payload' },
|
|
44
|
+
{ status: 400 },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const payload: TokenData = {
|
|
49
|
+
token,
|
|
50
|
+
updated_at: Date.now(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
fs.mkdirSync(require('path').dirname(AGENTS_TOKEN_FILE), { recursive: true });
|
|
54
|
+
fs.writeFileSync(AGENTS_TOKEN_FILE, JSON.stringify(payload, null, 2));
|
|
55
|
+
|
|
56
|
+
let containersUpdated = 0;
|
|
57
|
+
let containerNames: string[] = [];
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const raw = execSync(
|
|
61
|
+
`docker ps --filter "label=AGENT_ID" --format "{{.Names}}"`,
|
|
62
|
+
{ encoding: 'utf-8', timeout: 5000 },
|
|
63
|
+
).trim();
|
|
64
|
+
containerNames = raw ? raw.split('\n').map((name) => name.trim()).filter(Boolean) : [];
|
|
65
|
+
} catch {
|
|
66
|
+
containerNames = [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const containerName of containerNames) {
|
|
70
|
+
try {
|
|
71
|
+
// Write token to /root/.agent-token (entrypoint reads this, overrides env var)
|
|
72
|
+
execSync(
|
|
73
|
+
`docker exec ${shellQuote(containerName)} sh -c 'echo ${shellQuote(token)} > /root/.agent-token'`,
|
|
74
|
+
{ encoding: 'utf-8', timeout: 10000 },
|
|
75
|
+
);
|
|
76
|
+
// Restart to pick up new token
|
|
77
|
+
execSync(`docker restart ${shellQuote(containerName)}`, {
|
|
78
|
+
encoding: 'utf-8',
|
|
79
|
+
timeout: 20000,
|
|
80
|
+
});
|
|
81
|
+
containersUpdated += 1;
|
|
82
|
+
} catch {
|
|
83
|
+
// Skip failed containers and continue syncing the rest.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return NextResponse.json({ success: true, containersUpdated });
|
|
88
|
+
} catch (e: unknown) {
|
|
89
|
+
return NextResponse.json(
|
|
90
|
+
{ success: false, error: (e as Error).message || 'Unknown error' },
|
|
91
|
+
{ status: 500 },
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
5
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
interface WorkspaceFile {
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
rel_path: string;
|
|
13
|
+
type: 'markdown' | 'json' | 'html' | 'pdf' | 'script' | 'typescript' | 'stylesheet' | 'yaml' | 'env' | 'text' | 'folder';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ConfiguredAgent {
|
|
17
|
+
id: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
model?: string;
|
|
20
|
+
defaultModel?: string;
|
|
21
|
+
default_model?: string;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ALLOWED_EXT = new Set(['.md', '.json', '.txt', '.html', '.py', '.css', '.js', '.ts', '.tsx', '.yaml', '.yml', '.env', '.sh', '.pdf']);
|
|
26
|
+
|
|
27
|
+
function fileTypeFromExt(ext: string): WorkspaceFile['type'] {
|
|
28
|
+
if (ext === '.md') return 'markdown';
|
|
29
|
+
if (ext === '.json') return 'json';
|
|
30
|
+
if (ext === '.html') return 'html';
|
|
31
|
+
if (ext === '.pdf') return 'pdf';
|
|
32
|
+
if (ext === '.py' || ext === '.sh') return 'script';
|
|
33
|
+
if (ext === '.js' || ext === '.ts' || ext === '.tsx') return 'typescript';
|
|
34
|
+
if (ext === '.css') return 'stylesheet';
|
|
35
|
+
if (ext === '.yaml' || ext === '.yml') return 'yaml';
|
|
36
|
+
if (ext === '.env') return 'env';
|
|
37
|
+
return 'text';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const MAX_WORKSPACE_TREE_DEPTH = 32;
|
|
41
|
+
const MAX_WORKSPACE_TREE_ITEMS = 5000;
|
|
42
|
+
|
|
43
|
+
function listWorkspaceFiles(workspacePath: string): WorkspaceFile[] {
|
|
44
|
+
const out: WorkspaceFile[] = [];
|
|
45
|
+
|
|
46
|
+
function walk(dir: string, depth: number, prefix = '') {
|
|
47
|
+
if (depth > MAX_WORKSPACE_TREE_DEPTH) return;
|
|
48
|
+
let entries: fs.Dirent[] = [];
|
|
49
|
+
try {
|
|
50
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
51
|
+
.sort((a, b) => {
|
|
52
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
53
|
+
return a.name.localeCompare(b.name);
|
|
54
|
+
});
|
|
55
|
+
} catch {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (entry.name.startsWith('.')) continue;
|
|
60
|
+
const absPath = path.join(dir, entry.name);
|
|
61
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
if (entry.name === 'node_modules' || entry.name === '.trash') continue;
|
|
64
|
+
out.push({ name: entry.name, path: absPath, rel_path: relPath, type: 'folder' });
|
|
65
|
+
walk(absPath, depth + 1, relPath);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
69
|
+
if (!ALLOWED_EXT.has(ext)) continue;
|
|
70
|
+
out.push({ name: entry.name, path: absPath, rel_path: relPath, type: fileTypeFromExt(ext) });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
walk(workspacePath, 0);
|
|
75
|
+
const rootFiles = out.filter((f) => f.type !== 'folder' && !f.rel_path.includes('/'));
|
|
76
|
+
const rootFolders = out.filter((f) => f.type === 'folder' && !f.rel_path.includes('/'));
|
|
77
|
+
const rest = out.filter((f) => f.rel_path.includes('/'));
|
|
78
|
+
|
|
79
|
+
rootFiles.sort((a, b) => a.rel_path.localeCompare(b.rel_path));
|
|
80
|
+
rootFolders.sort((a, b) => a.rel_path.localeCompare(b.rel_path));
|
|
81
|
+
rest.sort((a, b) => a.rel_path.localeCompare(b.rel_path));
|
|
82
|
+
|
|
83
|
+
return [...rootFiles, ...rootFolders, ...rest].slice(0, MAX_WORKSPACE_TREE_ITEMS);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readConfiguredAgents(): ConfiguredAgent[] {
|
|
87
|
+
try {
|
|
88
|
+
const raw = fs.readFileSync('/data/.openclaw/openclaw.json', 'utf8');
|
|
89
|
+
const parsed = JSON.parse(raw) as { agents?: { list?: unknown[] } };
|
|
90
|
+
const list = parsed?.agents?.list;
|
|
91
|
+
if (!Array.isArray(list)) return [];
|
|
92
|
+
return list.filter(
|
|
93
|
+
(a): a is ConfiguredAgent => !!a && typeof (a as ConfiguredAgent).id === 'string',
|
|
94
|
+
);
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readDockerAgents(): { id: string; agentId: string; name: string; image: string; status: string; ports: string }[] {
|
|
101
|
+
try {
|
|
102
|
+
const output = execSync(
|
|
103
|
+
'docker ps --filter "label=AGENT_ID" --format "{{.ID}}|{{.Label \"AGENT_ID\"}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}"',
|
|
104
|
+
{ timeout: 5000, encoding: 'utf-8' },
|
|
105
|
+
).trim();
|
|
106
|
+
if (!output) return [];
|
|
107
|
+
return output.split('\n').map((line) => {
|
|
108
|
+
const [id, agentId, name, image, status, ports] = line.split('|');
|
|
109
|
+
return { id, agentId, name, image, status, ports };
|
|
110
|
+
});
|
|
111
|
+
} catch {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
117
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
118
|
+
try {
|
|
119
|
+
const configuredAgents = readConfiguredAgents();
|
|
120
|
+
const dockerAgents = readDockerAgents();
|
|
121
|
+
const dockerAgentIds = new Set(dockerAgents.map((d) => d.agentId));
|
|
122
|
+
|
|
123
|
+
// Map workspace paths per agent
|
|
124
|
+
const agents = configuredAgents.map((cfg) => {
|
|
125
|
+
const agent_id = cfg.id;
|
|
126
|
+
const dockerInfo = dockerAgents.find((d) => d.agentId === agent_id);
|
|
127
|
+
const workspace_path = agent_id === 'ops' || agent_id === 'core'
|
|
128
|
+
? '/data/.openclaw/workspace-ops/'
|
|
129
|
+
: `/data/.openclaw/workspace-${agent_id}/`;
|
|
130
|
+
|
|
131
|
+
// Only read workspace for agents on the host (not running as separate containers)
|
|
132
|
+
const files = dockerInfo ? [] : (fs.existsSync(workspace_path) ? listWorkspaceFiles(workspace_path) : []);
|
|
133
|
+
|
|
134
|
+
const config_model = formatModel(cfg.model ?? cfg.defaultModel ?? cfg.default_model);
|
|
135
|
+
const status = dockerInfo
|
|
136
|
+
? (dockerInfo.status.startsWith('Up') ? 'running' : dockerInfo.status.toLowerCase())
|
|
137
|
+
: 'inactive';
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
agent_id,
|
|
141
|
+
label: cfg.label ?? agent_id,
|
|
142
|
+
config_model,
|
|
143
|
+
workspace_path,
|
|
144
|
+
files,
|
|
145
|
+
docker: dockerInfo ?? null,
|
|
146
|
+
status,
|
|
147
|
+
config: cfg,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Add any Docker-only agents not in config
|
|
152
|
+
for (const d of dockerAgents) {
|
|
153
|
+
if (!configuredAgents.find((c) => c.id === d.agentId)) {
|
|
154
|
+
// Read AGENT_NAME env for display name
|
|
155
|
+
let label = d.agentId;
|
|
156
|
+
try {
|
|
157
|
+
const displayName = execSync(
|
|
158
|
+
`docker exec ${d.name} sh -c 'printenv AGENT_NAME'`,
|
|
159
|
+
{ timeout: 3000, encoding: 'utf-8' },
|
|
160
|
+
).trim();
|
|
161
|
+
if (displayName) label = displayName;
|
|
162
|
+
} catch { /* use fallback */ }
|
|
163
|
+
agents.push({
|
|
164
|
+
agent_id: d.agentId,
|
|
165
|
+
label,
|
|
166
|
+
config_model: null,
|
|
167
|
+
workspace_path: '',
|
|
168
|
+
files: [],
|
|
169
|
+
docker: d as any,
|
|
170
|
+
status: d.status.startsWith('Up') ? 'running' : d.status.toLowerCase(),
|
|
171
|
+
config: {} as ConfiguredAgent,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return NextResponse.json(agents);
|
|
177
|
+
} catch (e: unknown) {
|
|
178
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatModel(value: unknown): string | null {
|
|
183
|
+
if (typeof value === 'string') return value;
|
|
184
|
+
if (value && typeof value === 'object') {
|
|
185
|
+
const model = value as { primary?: unknown; model?: unknown; provider?: unknown };
|
|
186
|
+
if (typeof model.primary === 'string') return model.primary;
|
|
187
|
+
if (typeof model.model === 'string') return model.provider ? `${String(model.provider)}/${model.model}` : model.model;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|