@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,629 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
4
|
+
import { templatesDir } from '@/lib/rev4a-paths';
|
|
5
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
type JsonObject = Record<string, unknown>;
|
|
10
|
+
type TokenStatus = 'masked' | 'present' | 'missing';
|
|
11
|
+
|
|
12
|
+
interface OpenClawConfig {
|
|
13
|
+
agents?: {
|
|
14
|
+
list?: JsonObject[];
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
channels?: {
|
|
18
|
+
telegram?: {
|
|
19
|
+
accounts?: Record<string, JsonObject>;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
};
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
};
|
|
24
|
+
bindings?: JsonObject[];
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface AgentUpdateInput extends JsonObject {
|
|
29
|
+
currentId?: string;
|
|
30
|
+
id?: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface TelegramAccountUpdateInput extends JsonObject {
|
|
35
|
+
currentAccountId?: string;
|
|
36
|
+
accountId?: string;
|
|
37
|
+
tokenReplacement?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface BindingMatchInput extends JsonObject {
|
|
41
|
+
channel?: string;
|
|
42
|
+
accountId?: string;
|
|
43
|
+
from?: string;
|
|
44
|
+
to?: string;
|
|
45
|
+
peer?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface BindingUpdateInput extends JsonObject {
|
|
49
|
+
currentIndex?: number;
|
|
50
|
+
bindingKey?: string;
|
|
51
|
+
type?: string;
|
|
52
|
+
agentId?: string;
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
allowFrom?: string[];
|
|
55
|
+
defaultTo?: string | string[];
|
|
56
|
+
dmPolicy?: string;
|
|
57
|
+
match?: BindingMatchInput;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface UpdatePayload {
|
|
61
|
+
agents?: AgentUpdateInput[];
|
|
62
|
+
telegramAccounts?: TelegramAccountUpdateInput[];
|
|
63
|
+
bindingScopeAgentId?: string;
|
|
64
|
+
bindings?: BindingUpdateInput[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const OPENCLAW_CONFIG_PATH = '/data/.openclaw/openclaw.json';
|
|
68
|
+
|
|
69
|
+
function readConfig(): OpenClawConfig {
|
|
70
|
+
const raw = fs.readFileSync(OPENCLAW_CONFIG_PATH, 'utf8');
|
|
71
|
+
return JSON.parse(raw) as OpenClawConfig;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatBackupTimestamp(date: Date): string {
|
|
75
|
+
const pad = (value: number) => String(value).padStart(2, '0');
|
|
76
|
+
return [
|
|
77
|
+
date.getFullYear(),
|
|
78
|
+
pad(date.getMonth() + 1),
|
|
79
|
+
pad(date.getDate()),
|
|
80
|
+
pad(date.getHours()),
|
|
81
|
+
pad(date.getMinutes()),
|
|
82
|
+
pad(date.getSeconds()),
|
|
83
|
+
].join('');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeConfig(config: OpenClawConfig): void {
|
|
87
|
+
const configPath = OPENCLAW_CONFIG_PATH;
|
|
88
|
+
const configDir = path.dirname(configPath);
|
|
89
|
+
const configBase = path.basename(configPath);
|
|
90
|
+
const backupPath = path.join(configDir, `${configBase}.bak-${formatBackupTimestamp(new Date())}`);
|
|
91
|
+
const tempPath = path.join(configDir, `${configBase}.tmp-${process.pid}-${Date.now()}`);
|
|
92
|
+
const serializedConfig = `${JSON.stringify(config, null, 2)}\n`;
|
|
93
|
+
|
|
94
|
+
fs.copyFileSync(configPath, backupPath);
|
|
95
|
+
|
|
96
|
+
let tempFd: number | undefined;
|
|
97
|
+
let dirFd: number | undefined;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
tempFd = fs.openSync(tempPath, 'w', 0o600);
|
|
101
|
+
fs.writeFileSync(tempFd, serializedConfig, 'utf8');
|
|
102
|
+
fs.fsyncSync(tempFd);
|
|
103
|
+
fs.closeSync(tempFd);
|
|
104
|
+
tempFd = undefined;
|
|
105
|
+
|
|
106
|
+
fs.renameSync(tempPath, configPath);
|
|
107
|
+
|
|
108
|
+
dirFd = fs.openSync(configDir, 'r');
|
|
109
|
+
fs.fsyncSync(dirFd);
|
|
110
|
+
fs.closeSync(dirFd);
|
|
111
|
+
dirFd = undefined;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (tempFd !== undefined) fs.closeSync(tempFd);
|
|
114
|
+
if (dirFd !== undefined) fs.closeSync(dirFd);
|
|
115
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function pickOptional(source: JsonObject, keys: string[]): JsonObject {
|
|
121
|
+
const out: JsonObject = {};
|
|
122
|
+
for (const key of keys) {
|
|
123
|
+
if (source[key] !== undefined) out[key] = source[key];
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getTokenStatus(value: unknown): TokenStatus {
|
|
129
|
+
if (typeof value !== 'string' || !value.trim()) return 'missing';
|
|
130
|
+
return 'masked';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sanitizeAgent(agent: JsonObject): JsonObject {
|
|
134
|
+
return {
|
|
135
|
+
id: agent.id,
|
|
136
|
+
...pickOptional(agent, ['name', 'label', 'workspace', 'agentDir', 'model', 'defaultModel', 'default_model', 'identity']),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sanitizeTelegramAccount(accountId: string, account: JsonObject): JsonObject {
|
|
141
|
+
return {
|
|
142
|
+
accountId,
|
|
143
|
+
...pickOptional(account, ['name', 'enabled', 'allowFrom', 'defaultTo', 'dmPolicy']),
|
|
144
|
+
tokenStatus: getTokenStatus(account.botToken ?? account.token),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sanitizeBinding(binding: JsonObject, index: number): JsonObject {
|
|
149
|
+
const match = typeof binding.match === 'object' && binding.match !== null ? (binding.match as JsonObject) : null;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
bindingKey: String(index),
|
|
153
|
+
currentIndex: index,
|
|
154
|
+
...pickOptional(binding, ['type', 'agentId', 'enabled', 'allowFrom', 'defaultTo', 'dmPolicy']),
|
|
155
|
+
...(match
|
|
156
|
+
? {
|
|
157
|
+
match: pickOptional(match, ['channel', 'accountId', 'from', 'to', 'peer']),
|
|
158
|
+
}
|
|
159
|
+
: {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeString(value: unknown): string {
|
|
164
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function cloneJsonObject(value: unknown): JsonObject {
|
|
168
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? { ...(value as JsonObject) } : {};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function ensureStringArray(value: unknown, field: string): string[] | undefined {
|
|
172
|
+
if (value === undefined) return undefined;
|
|
173
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
174
|
+
throw new Error(`${field} must be an array of strings`);
|
|
175
|
+
}
|
|
176
|
+
return [...value];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function ensureStringOrStringArray(value: unknown, field: string): string | string[] | undefined {
|
|
180
|
+
if (value === undefined) return undefined;
|
|
181
|
+
if (typeof value === 'string') return value;
|
|
182
|
+
if (Array.isArray(value) && value.every((item) => typeof item === 'string')) return [...value];
|
|
183
|
+
throw new Error(`${field} must be a string or array of strings`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function ensureBoolean(value: unknown, field: string): boolean | undefined {
|
|
187
|
+
if (value === undefined) return undefined;
|
|
188
|
+
if (typeof value !== 'boolean') throw new Error(`${field} must be a boolean`);
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeAgentUpdate(input: AgentUpdateInput): JsonObject {
|
|
193
|
+
const id = normalizeString(input.id);
|
|
194
|
+
const name = normalizeString(input.name);
|
|
195
|
+
if (!id) throw new Error('agent id is required');
|
|
196
|
+
if (!name) throw new Error('agent name is required');
|
|
197
|
+
|
|
198
|
+
const next: JsonObject = {
|
|
199
|
+
id,
|
|
200
|
+
name,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
for (const key of ['label', 'workspace', 'agentDir', 'model', 'defaultModel', 'default_model']) {
|
|
204
|
+
if (input[key] !== undefined) next[key] = input[key];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (input.identity !== undefined) {
|
|
208
|
+
if (input.identity && typeof input.identity === 'object' && !Array.isArray(input.identity)) {
|
|
209
|
+
next.identity = { ...(input.identity as JsonObject) };
|
|
210
|
+
} else {
|
|
211
|
+
throw new Error('identity must be an object');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return next;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function normalizeTelegramAccountUpdate(input: TelegramAccountUpdateInput): { accountId: string; patch: JsonObject; tokenReplacement?: string } {
|
|
219
|
+
const accountId = normalizeString(input.accountId);
|
|
220
|
+
if (!accountId) throw new Error('telegram account id is required');
|
|
221
|
+
|
|
222
|
+
const patch: JsonObject = { accountId };
|
|
223
|
+
if (input.name !== undefined) patch.name = input.name;
|
|
224
|
+
if (input.enabled !== undefined) patch.enabled = ensureBoolean(input.enabled, 'telegram enabled');
|
|
225
|
+
if (input.allowFrom !== undefined) patch.allowFrom = ensureStringArray(input.allowFrom, 'telegram allowFrom');
|
|
226
|
+
if (input.defaultTo !== undefined) patch.defaultTo = ensureStringOrStringArray(input.defaultTo, 'telegram defaultTo');
|
|
227
|
+
if (input.dmPolicy !== undefined) patch.dmPolicy = input.dmPolicy;
|
|
228
|
+
|
|
229
|
+
let tokenReplacement: string | undefined;
|
|
230
|
+
if (input.tokenReplacement !== undefined) {
|
|
231
|
+
if (typeof input.tokenReplacement !== 'string') throw new Error('tokenReplacement must be a string');
|
|
232
|
+
tokenReplacement = input.tokenReplacement.trim();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { accountId, patch, tokenReplacement };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizeBindingUpdate(input: BindingUpdateInput): { currentIndex?: number; value: JsonObject } {
|
|
239
|
+
const type = normalizeString(input.type) || 'telegram';
|
|
240
|
+
const agentId = normalizeString(input.agentId);
|
|
241
|
+
if (!agentId) throw new Error('binding agentId is required');
|
|
242
|
+
|
|
243
|
+
const matchSource = cloneJsonObject(input.match);
|
|
244
|
+
const channel = normalizeString(matchSource.channel) || 'telegram';
|
|
245
|
+
const accountId = normalizeString(matchSource.accountId);
|
|
246
|
+
if (!accountId) throw new Error('binding match.accountId is required');
|
|
247
|
+
if (channel !== 'telegram') throw new Error('binding match.channel must be telegram');
|
|
248
|
+
|
|
249
|
+
const match: JsonObject = {
|
|
250
|
+
...matchSource,
|
|
251
|
+
channel,
|
|
252
|
+
accountId,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const from = normalizeString(matchSource.from);
|
|
256
|
+
const to = normalizeString(matchSource.to);
|
|
257
|
+
const peer = normalizeString(matchSource.peer);
|
|
258
|
+
if (from) match.from = from;
|
|
259
|
+
else delete match.from;
|
|
260
|
+
if (to) match.to = to;
|
|
261
|
+
else delete match.to;
|
|
262
|
+
if (peer) match.peer = peer;
|
|
263
|
+
else delete match.peer;
|
|
264
|
+
|
|
265
|
+
const base = cloneJsonObject(input);
|
|
266
|
+
delete base.currentIndex;
|
|
267
|
+
delete base.bindingKey;
|
|
268
|
+
delete base.match;
|
|
269
|
+
|
|
270
|
+
const value: JsonObject = {
|
|
271
|
+
...base,
|
|
272
|
+
type,
|
|
273
|
+
agentId,
|
|
274
|
+
match,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (input.enabled !== undefined) value.enabled = ensureBoolean(input.enabled, 'binding enabled');
|
|
278
|
+
if (input.allowFrom !== undefined) value.allowFrom = ensureStringArray(input.allowFrom, 'binding allowFrom');
|
|
279
|
+
if (input.defaultTo !== undefined) value.defaultTo = ensureStringOrStringArray(input.defaultTo, 'binding defaultTo');
|
|
280
|
+
if (input.dmPolicy !== undefined) value.dmPolicy = input.dmPolicy;
|
|
281
|
+
|
|
282
|
+
const currentIndex = typeof input.currentIndex === 'number' && Number.isInteger(input.currentIndex) ? input.currentIndex : undefined;
|
|
283
|
+
return { currentIndex, value };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildBindingConflictKey(binding: JsonObject): string {
|
|
287
|
+
const match = cloneJsonObject(binding.match);
|
|
288
|
+
const enabled = binding.enabled !== false;
|
|
289
|
+
if (!enabled) return '';
|
|
290
|
+
const accountId = normalizeString(match.accountId);
|
|
291
|
+
const channel = normalizeString(match.channel) || 'telegram';
|
|
292
|
+
const from = normalizeString(match.from) || '*';
|
|
293
|
+
const to = normalizeString(match.to) || '*';
|
|
294
|
+
const peer = normalizeString(match.peer) || '*';
|
|
295
|
+
const defaultTo = binding.defaultTo;
|
|
296
|
+
const defaultToKey = Array.isArray(defaultTo)
|
|
297
|
+
? defaultTo.map((item) => normalizeString(item)).filter(Boolean).sort().join('|')
|
|
298
|
+
: normalizeString(defaultTo) || '*';
|
|
299
|
+
return [channel, accountId, from, to, peer, defaultToKey].join('::');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function validateTelegramBindings(bindings: JsonObject[], agentIds: Set<string>, accountIds: Set<string>): void {
|
|
303
|
+
const seen = new Map<string, number>();
|
|
304
|
+
|
|
305
|
+
bindings.forEach((binding, index) => {
|
|
306
|
+
const agentId = normalizeString(binding.agentId);
|
|
307
|
+
if (!agentId) throw new Error(`binding ${index + 1}: agentId is required`);
|
|
308
|
+
if (!agentIds.has(agentId)) throw new Error(`binding ${index + 1}: unknown agentId ${agentId}`);
|
|
309
|
+
|
|
310
|
+
const match = cloneJsonObject(binding.match);
|
|
311
|
+
const accountId = normalizeString(match.accountId);
|
|
312
|
+
if (!accountId) throw new Error(`binding ${index + 1}: accountId is required`);
|
|
313
|
+
if (!accountIds.has(accountId)) throw new Error(`binding ${index + 1}: unknown accountId ${accountId}`);
|
|
314
|
+
|
|
315
|
+
const channel = normalizeString(match.channel);
|
|
316
|
+
if (channel !== 'telegram') throw new Error(`binding ${index + 1}: match.channel must be telegram`);
|
|
317
|
+
|
|
318
|
+
const conflictKey = buildBindingConflictKey(binding);
|
|
319
|
+
if (!conflictKey) return;
|
|
320
|
+
const existing = seen.get(conflictKey);
|
|
321
|
+
if (existing !== undefined) {
|
|
322
|
+
throw new Error(`binding ${index + 1}: conflicts with binding ${existing + 1}`);
|
|
323
|
+
}
|
|
324
|
+
seen.set(conflictKey, index);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function buildConfigPayload(config: OpenClawConfig) {
|
|
329
|
+
const agents = Array.isArray(config.agents?.list) ? config.agents.list : [];
|
|
330
|
+
const telegramAccounts = config.channels?.telegram?.accounts ?? {};
|
|
331
|
+
const bindings = Array.isArray(config.bindings) ? config.bindings : [];
|
|
332
|
+
|
|
333
|
+
return agents
|
|
334
|
+
.filter((agent): agent is JsonObject => !!agent && typeof agent.id === 'string')
|
|
335
|
+
.map((agent) => {
|
|
336
|
+
const agentId = String(agent.id);
|
|
337
|
+
const agentBindings = bindings
|
|
338
|
+
.map((binding, index) => ({ binding, index }))
|
|
339
|
+
.filter(({ binding }) => !!binding && typeof binding.agentId === 'string' && binding.agentId === agentId);
|
|
340
|
+
const telegramAccountIds = Array.from(
|
|
341
|
+
new Set(
|
|
342
|
+
agentBindings
|
|
343
|
+
.map(({ binding }) => {
|
|
344
|
+
const match = typeof binding.match === 'object' && binding.match !== null ? (binding.match as JsonObject) : null;
|
|
345
|
+
if (!match || match.channel !== 'telegram' || typeof match.accountId !== 'string') return null;
|
|
346
|
+
return match.accountId;
|
|
347
|
+
})
|
|
348
|
+
.filter((value): value is string => typeof value === 'string'),
|
|
349
|
+
),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
agentId,
|
|
354
|
+
config: sanitizeAgent(agent),
|
|
355
|
+
telegram: {
|
|
356
|
+
accounts: telegramAccountIds.map((accountId) => sanitizeTelegramAccount(accountId, telegramAccounts[accountId] ?? {})),
|
|
357
|
+
bindings: agentBindings
|
|
358
|
+
.filter(({ binding }) => {
|
|
359
|
+
const match = typeof binding.match === 'object' && binding.match !== null ? (binding.match as JsonObject) : null;
|
|
360
|
+
return match?.channel === 'telegram';
|
|
361
|
+
})
|
|
362
|
+
.map(({ binding, index }) => sanitizeBinding(binding, index)),
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
interface CreateAgentPayload {
|
|
369
|
+
id?: string;
|
|
370
|
+
name?: string;
|
|
371
|
+
label?: string;
|
|
372
|
+
workspace?: string;
|
|
373
|
+
agentDir?: string;
|
|
374
|
+
model?: string;
|
|
375
|
+
defaultModel?: string;
|
|
376
|
+
identity?: { name?: string; emoji?: string };
|
|
377
|
+
templateId?: string;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const AGENT_TEMPLATES_DIR = templatesDir(process.cwd());
|
|
381
|
+
|
|
382
|
+
function copyTemplateFiles(templateId: string, workspace: string): string[] {
|
|
383
|
+
const src = path.join(AGENT_TEMPLATES_DIR, templateId);
|
|
384
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) return [];
|
|
385
|
+
const created: string[] = [];
|
|
386
|
+
for (const entry of fs.readdirSync(src, { recursive: true, withFileTypes: true })) {
|
|
387
|
+
if (!entry.isFile()) continue;
|
|
388
|
+
const rel = path.relative(src, path.join(entry.parentPath ?? src, entry.name));
|
|
389
|
+
const dest = path.join(workspace, rel);
|
|
390
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
391
|
+
fs.copyFileSync(path.join(entry.parentPath ?? src, entry.name), dest);
|
|
392
|
+
created.push(rel);
|
|
393
|
+
}
|
|
394
|
+
return created;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function POST(request: NextRequest): Promise<NextResponse> {
|
|
398
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
const body = (await request.json()) as CreateAgentPayload;
|
|
402
|
+
const id = normalizeString(body.id);
|
|
403
|
+
if (!id) throw new Error('agent id is required');
|
|
404
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) throw new Error('agent id must be lowercase alphanumeric with hyphens');
|
|
405
|
+
|
|
406
|
+
const config = readConfig();
|
|
407
|
+
const currentAgents = Array.isArray(config.agents?.list) ? config.agents.list : [];
|
|
408
|
+
|
|
409
|
+
if (currentAgents.some((a) => normalizeString(cloneJsonObject(a).id) === id)) {
|
|
410
|
+
throw new Error(`agent id already in use: ${id}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const newAgent: JsonObject = { id };
|
|
414
|
+
if (body.name) newAgent.name = normalizeString(body.name);
|
|
415
|
+
if (body.label) newAgent.label = normalizeString(body.label);
|
|
416
|
+
if (body.workspace) newAgent.workspace = normalizeString(body.workspace);
|
|
417
|
+
if (body.agentDir) newAgent.agentDir = normalizeString(body.agentDir);
|
|
418
|
+
if (body.model) newAgent.model = normalizeString(body.model);
|
|
419
|
+
if (body.defaultModel) newAgent.defaultModel = normalizeString(body.defaultModel);
|
|
420
|
+
if (body.identity && typeof body.identity === 'object') {
|
|
421
|
+
const ident: JsonObject = {};
|
|
422
|
+
if (body.identity.name) ident.name = body.identity.name;
|
|
423
|
+
if (body.identity.emoji) ident.emoji = body.identity.emoji;
|
|
424
|
+
if (Object.keys(ident).length) newAgent.identity = ident;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const nextConfig: OpenClawConfig = {
|
|
428
|
+
...config,
|
|
429
|
+
agents: {
|
|
430
|
+
...(config.agents ?? {}),
|
|
431
|
+
list: [...currentAgents.map((a) => cloneJsonObject(a)), newAgent],
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
writeConfig(nextConfig);
|
|
436
|
+
|
|
437
|
+
// Copy workspace template files if templateId is provided
|
|
438
|
+
const templateId = normalizeString(body.templateId);
|
|
439
|
+
const workspace = normalizeString(newAgent.workspace ?? '');
|
|
440
|
+
let templateFiles: string[] = [];
|
|
441
|
+
if (templateId && workspace) {
|
|
442
|
+
try {
|
|
443
|
+
templateFiles = copyTemplateFiles(templateId, workspace);
|
|
444
|
+
} catch (e) {
|
|
445
|
+
// Non-blocking: agent is created even if template copy fails
|
|
446
|
+
console.error(`Template copy failed for ${templateId} → ${workspace}:`, (e as Error).message);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Create USER.md symlink if workspace exists
|
|
451
|
+
if (workspace && fs.existsSync(workspace)) {
|
|
452
|
+
const userMdPath = path.join(workspace, 'USER.md');
|
|
453
|
+
// Check if USER.md already exists (as file, symlink, or broken symlink)
|
|
454
|
+
try {
|
|
455
|
+
fs.accessSync(userMdPath, fs.constants.F_OK);
|
|
456
|
+
} catch {
|
|
457
|
+
// USER.md doesn't exist — create symlink
|
|
458
|
+
const sharedUserMd = '/data/.openclaw/shared-context/USER.md';
|
|
459
|
+
try {
|
|
460
|
+
fs.symlinkSync(sharedUserMd, userMdPath);
|
|
461
|
+
templateFiles.push('USER.md (symlink)');
|
|
462
|
+
console.log(`USER.md symlink created for ${id}`);
|
|
463
|
+
} catch (e) {
|
|
464
|
+
console.error(`USER.md symlink failed for ${id}:`, (e as Error).message);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return NextResponse.json({ success: true, data: buildConfigPayload(nextConfig), templateFiles });
|
|
470
|
+
} catch (error: unknown) {
|
|
471
|
+
return NextResponse.json({ error: (error as Error).message }, { status: 400 });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
476
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
return NextResponse.json(buildConfigPayload(readConfig()));
|
|
480
|
+
} catch (error: unknown) {
|
|
481
|
+
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export async function PUT(request: NextRequest): Promise<NextResponse> {
|
|
486
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
const body = (await request.json()) as UpdatePayload;
|
|
490
|
+
const config = readConfig();
|
|
491
|
+
const currentAgents = Array.isArray(config.agents?.list) ? config.agents.list : [];
|
|
492
|
+
const currentAccounts = config.channels?.telegram?.accounts ?? {};
|
|
493
|
+
const currentBindings = Array.isArray(config.bindings) ? config.bindings : [];
|
|
494
|
+
|
|
495
|
+
let nextAgents = currentAgents.map((agent) => cloneJsonObject(agent));
|
|
496
|
+
let nextAccounts = Object.fromEntries(
|
|
497
|
+
Object.entries(currentAccounts).map(([accountId, account]) => [accountId, cloneJsonObject(account)]),
|
|
498
|
+
) as Record<string, JsonObject>;
|
|
499
|
+
let nextBindings = currentBindings.map((binding) => cloneJsonObject(binding));
|
|
500
|
+
|
|
501
|
+
if (body.agents !== undefined) {
|
|
502
|
+
if (!Array.isArray(body.agents)) throw new Error('agents must be an array');
|
|
503
|
+
const updates = new Map<string, JsonObject>();
|
|
504
|
+
for (const input of body.agents) {
|
|
505
|
+
const currentId = normalizeString(input.currentId) || normalizeString(input.id);
|
|
506
|
+
const existingMatch = currentAgents.find((agent) => cloneJsonObject(agent).id === currentId);
|
|
507
|
+
if (!existingMatch) throw new Error(`unknown agent: ${currentId}`);
|
|
508
|
+
const normalized = normalizeAgentUpdate(input);
|
|
509
|
+
const merged = { ...cloneJsonObject(existingMatch), ...normalized };
|
|
510
|
+
updates.set(currentId, merged);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
nextAgents = currentAgents.map((agent) => {
|
|
514
|
+
const currentId = normalizeString(cloneJsonObject(agent).id);
|
|
515
|
+
return updates.get(currentId) ?? cloneJsonObject(agent);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const finalIds = new Set<string>();
|
|
519
|
+
for (const agent of nextAgents) {
|
|
520
|
+
const agentId = normalizeString(agent.id);
|
|
521
|
+
if (!agentId) throw new Error('agent id is required');
|
|
522
|
+
if (!normalizeString(agent.name)) throw new Error(`agent name is required for ${agentId}`);
|
|
523
|
+
if (finalIds.has(agentId)) throw new Error(`duplicate agent id: ${agentId}`);
|
|
524
|
+
finalIds.add(agentId);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (body.telegramAccounts !== undefined) {
|
|
529
|
+
if (!Array.isArray(body.telegramAccounts)) throw new Error('telegramAccounts must be an array');
|
|
530
|
+
const updates = new Map<string, { accountId: string; value: JsonObject }>();
|
|
531
|
+
for (const input of body.telegramAccounts) {
|
|
532
|
+
const currentAccountId = normalizeString(input.currentAccountId) || normalizeString(input.accountId);
|
|
533
|
+
const currentValue = currentAccounts[currentAccountId];
|
|
534
|
+
if (!currentValue || typeof currentValue !== 'object') throw new Error(`unknown telegram account: ${currentAccountId}`);
|
|
535
|
+
const existing = cloneJsonObject(currentValue);
|
|
536
|
+
const { accountId, patch, tokenReplacement } = normalizeTelegramAccountUpdate(input);
|
|
537
|
+
const merged = { ...existing, ...patch };
|
|
538
|
+
delete merged.accountId;
|
|
539
|
+
if (tokenReplacement) {
|
|
540
|
+
if (typeof existing.botToken === 'string') merged.botToken = tokenReplacement;
|
|
541
|
+
else if (typeof existing.token === 'string') merged.token = tokenReplacement;
|
|
542
|
+
else merged.botToken = tokenReplacement;
|
|
543
|
+
}
|
|
544
|
+
updates.set(currentAccountId, { accountId, value: merged });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const rebuiltAccounts: Record<string, JsonObject> = {};
|
|
548
|
+
for (const [existingId, account] of Object.entries(currentAccounts)) {
|
|
549
|
+
const updated = updates.get(existingId);
|
|
550
|
+
const targetId = updated?.accountId ?? existingId;
|
|
551
|
+
if (rebuiltAccounts[targetId]) throw new Error(`duplicate telegram account id: ${targetId}`);
|
|
552
|
+
rebuiltAccounts[targetId] = updated?.value ?? cloneJsonObject(account);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const finalIds = new Set<string>();
|
|
556
|
+
for (const accountId of Object.keys(rebuiltAccounts)) {
|
|
557
|
+
const normalizedId = normalizeString(accountId);
|
|
558
|
+
if (!normalizedId) throw new Error('telegram account id is required');
|
|
559
|
+
if (finalIds.has(normalizedId)) throw new Error(`duplicate telegram account id: ${normalizedId}`);
|
|
560
|
+
finalIds.add(normalizedId);
|
|
561
|
+
}
|
|
562
|
+
nextAccounts = rebuiltAccounts;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (body.bindings !== undefined) {
|
|
566
|
+
if (!Array.isArray(body.bindings)) throw new Error('bindings must be an array');
|
|
567
|
+
const scopeAgentId = normalizeString(body.bindingScopeAgentId);
|
|
568
|
+
if (!scopeAgentId) throw new Error('bindingScopeAgentId is required when updating bindings');
|
|
569
|
+
|
|
570
|
+
const scopedExisting = currentBindings
|
|
571
|
+
.map((binding, index) => ({ binding: cloneJsonObject(binding), index }))
|
|
572
|
+
.filter(({ binding }) => normalizeString(binding.agentId) === scopeAgentId && normalizeString(cloneJsonObject(binding.match).channel) === 'telegram');
|
|
573
|
+
const scopedByIndex = new Map(scopedExisting.map(({ binding, index }) => [index, binding]));
|
|
574
|
+
|
|
575
|
+
const normalizedBindings = body.bindings.map((input) => {
|
|
576
|
+
const { currentIndex, value } = normalizeBindingUpdate(input);
|
|
577
|
+
const preserved = currentIndex !== undefined ? cloneJsonObject(scopedByIndex.get(currentIndex)) : {};
|
|
578
|
+
const preservedMatch = cloneJsonObject(preserved.match);
|
|
579
|
+
return {
|
|
580
|
+
...preserved,
|
|
581
|
+
...value,
|
|
582
|
+
match: {
|
|
583
|
+
...preservedMatch,
|
|
584
|
+
...cloneJsonObject(value.match),
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const nextAgentIds = new Set(nextAgents.map((agent) => normalizeString(agent.id)).filter(Boolean));
|
|
590
|
+
const nextAccountIds = new Set(Object.keys(nextAccounts).map((accountId) => normalizeString(accountId)).filter(Boolean));
|
|
591
|
+
const preservedOtherBindings = currentBindings.filter((binding) => {
|
|
592
|
+
const jsonBinding = cloneJsonObject(binding);
|
|
593
|
+
return !(normalizeString(jsonBinding.agentId) === scopeAgentId && normalizeString(cloneJsonObject(jsonBinding.match).channel) === 'telegram');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
validateTelegramBindings(
|
|
597
|
+
[
|
|
598
|
+
...preservedOtherBindings.filter((binding) => normalizeString(cloneJsonObject(binding.match).channel) === 'telegram'),
|
|
599
|
+
...normalizedBindings,
|
|
600
|
+
],
|
|
601
|
+
nextAgentIds,
|
|
602
|
+
nextAccountIds,
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
nextBindings = [...preservedOtherBindings.map((binding) => cloneJsonObject(binding)), ...normalizedBindings.map((binding) => cloneJsonObject(binding))];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const nextConfig: OpenClawConfig = {
|
|
609
|
+
...config,
|
|
610
|
+
agents: {
|
|
611
|
+
...(config.agents ?? {}),
|
|
612
|
+
list: nextAgents,
|
|
613
|
+
},
|
|
614
|
+
channels: {
|
|
615
|
+
...(config.channels ?? {}),
|
|
616
|
+
telegram: {
|
|
617
|
+
...(config.channels?.telegram ?? {}),
|
|
618
|
+
accounts: nextAccounts,
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
bindings: nextBindings,
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
writeConfig(nextConfig);
|
|
625
|
+
return NextResponse.json({ ok: true, restartRequired: true, data: buildConfigPayload(nextConfig) });
|
|
626
|
+
} catch (error: unknown) {
|
|
627
|
+
return NextResponse.json({ error: (error as Error).message }, { status: 400 });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { maybeSendAlert } from '@/lib/alerts';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
function smokeAllowed(): boolean {
|
|
8
|
+
return process.env.NODE_ENV !== 'production' || process.env.REV4A_ALERT_SMOKE === 'true';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
12
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
13
|
+
if (!smokeAllowed()) return NextResponse.json({ error: 'disabled' }, { status: 403 });
|
|
14
|
+
|
|
15
|
+
const stale = new URL(request.url).searchParams.get('stale') !== '0';
|
|
16
|
+
const result = await maybeSendAlert({
|
|
17
|
+
key: 'smoke.telegram-alert',
|
|
18
|
+
kind: 'db-freshness',
|
|
19
|
+
title: 'Rev4a alert smoke',
|
|
20
|
+
message: 'Smoke test alert message.',
|
|
21
|
+
resolvedMessage: 'Smoke test resolved.',
|
|
22
|
+
stale,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return NextResponse.json({ ok: true, stale, result, dryRun: !process.env.REV4A_TELEGRAM_BOT_TOKEN || !process.env.REV4A_TELEGRAM_CHAT_ID });
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function POST(request: NextRequest): Promise<NextResponse> {
|
|
8
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
9
|
+
try {
|
|
10
|
+
const body = await request.json();
|
|
11
|
+
const alias = body.alias;
|
|
12
|
+
const model = body.model;
|
|
13
|
+
|
|
14
|
+
if (!alias || !model) {
|
|
15
|
+
return NextResponse.json({ error: 'Missing alias or model' }, { status: 400 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const stdout = execSync(
|
|
19
|
+
`openclaw models aliases add "${alias.replace(/"/g, '\\"')}" "${model.replace(/"/g, '\\"')}"`,
|
|
20
|
+
{ timeout: 15000, maxBuffer: 1024 * 1024 },
|
|
21
|
+
).toString();
|
|
22
|
+
|
|
23
|
+
return NextResponse.json({ ok: true, output: stdout.trim() });
|
|
24
|
+
} catch (e: unknown) {
|
|
25
|
+
const message = (e as Error).message;
|
|
26
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
27
|
+
}
|
|
28
|
+
}
|