@flame0510/project-aether 1.4.1 → 1.4.2
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.
|
@@ -770,6 +770,14 @@ function currentPortRange(ports: string): string {
|
|
|
770
770
|
return min === max ? `${min}` : `${min}-${max}`;
|
|
771
771
|
}
|
|
772
772
|
|
|
773
|
+
/** The agent's own published host ports, from the `3700:3000, 3701:3001` string. */
|
|
774
|
+
function ownHostPorts(ports: string): number[] {
|
|
775
|
+
return (ports || '')
|
|
776
|
+
.split(',')
|
|
777
|
+
.map((p) => parseInt(p.trim().split(':')[0] ?? '', 10))
|
|
778
|
+
.filter((n) => Number.isInteger(n));
|
|
779
|
+
}
|
|
780
|
+
|
|
773
781
|
function EditAgentModal({ agent, onClose, onSaved, onStarted }: {
|
|
774
782
|
agent: Agent;
|
|
775
783
|
onClose: () => void;
|
|
@@ -780,20 +788,37 @@ function EditAgentModal({ agent, onClose, onSaved, onStarted }: {
|
|
|
780
788
|
}) {
|
|
781
789
|
const initialName = agent.displayName || agent.name;
|
|
782
790
|
const initialPorts = currentPortRange(agent.ports);
|
|
791
|
+
const ownPorts = useMemo(() => ownHostPorts(agent.ports), [agent.ports]);
|
|
783
792
|
const [displayName, setDisplayName] = useState(initialName);
|
|
784
793
|
const [portRange, setPortRange] = useState(initialPorts);
|
|
785
794
|
const [saving, setSaving] = useState(false);
|
|
786
795
|
const [error, setError] = useState('');
|
|
796
|
+
// The ports other containers hold, fetched when the panel opens: the same set the
|
|
797
|
+
// create form gets as a page prop, from the same lookup (`GET /api/agents/ports`).
|
|
798
|
+
const [usedPorts, setUsedPorts] = useState<Set<number> | null>(null);
|
|
799
|
+
|
|
800
|
+
useEffect(() => {
|
|
801
|
+
const controller = new AbortController();
|
|
802
|
+
fetch('/api/agents/ports', { signal: controller.signal })
|
|
803
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
804
|
+
.then((data: { ports?: number[] } | null) => {
|
|
805
|
+
if (data && Array.isArray(data.ports)) setUsedPorts(new Set(data.ports));
|
|
806
|
+
})
|
|
807
|
+
.catch((e: unknown) => { if ((e as Error)?.name !== 'AbortError') setUsedPorts(null); });
|
|
808
|
+
return () => controller.abort();
|
|
809
|
+
}, []);
|
|
787
810
|
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
//
|
|
811
|
+
// The same rules and messages as agent creation (lib/agent-ports.ts). This agent's own
|
|
812
|
+
// block is excluded, so keeping or shifting it is not a conflict with itself; every
|
|
813
|
+
// other container counts, running or stopped.
|
|
791
814
|
const nameError = displayName.trim() ? '' : 'Display name must not be empty';
|
|
792
815
|
const portError = useMemo(() => {
|
|
793
816
|
if (!portRange.trim()) return '';
|
|
794
|
-
const
|
|
817
|
+
const others = new Set(usedPorts ?? []);
|
|
818
|
+
for (const p of ownPorts) others.delete(p);
|
|
819
|
+
const r = validatePortInput(portRange, others);
|
|
795
820
|
return r.valid ? '' : r.error;
|
|
796
|
-
}, [portRange]);
|
|
821
|
+
}, [portRange, usedPorts, ownPorts]);
|
|
797
822
|
|
|
798
823
|
const nameChanged = displayName.trim() !== initialName;
|
|
799
824
|
const portsChanged = portRange.trim() !== initialPorts;
|
|
@@ -882,7 +907,7 @@ function EditAgentModal({ agent, onClose, onSaved, onStarted }: {
|
|
|
882
907
|
{error && <div style={{ fontSize: 11, color: '#ef4444' }}>{error}</div>}
|
|
883
908
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
|
884
909
|
<Button variant="secondary" size="sm" onClick={onClose} disabled={saving}>Cancel</Button>
|
|
885
|
-
<Button variant="primary" size="sm" onClick={handleSave} disabled={saving}>
|
|
910
|
+
<Button variant="primary" size="sm" onClick={handleSave} disabled={saving || !!nameError || !!portError}>
|
|
886
911
|
{saving ? 'Saving...' : 'Save & Recreate'}
|
|
887
912
|
</Button>
|
|
888
913
|
</div>
|
|
@@ -60,6 +60,20 @@ export default function CreatePageClient({ templates, models, usedNames, usedPor
|
|
|
60
60
|
const [selectedTemplate, setSelectedTemplate] = useState<string>('');
|
|
61
61
|
const [agentName, setAgentName] = useState('');
|
|
62
62
|
const [portRange, setPortRange] = useState<string>('');
|
|
63
|
+
// The server render seeds this; the same endpoint the Edit panel uses refreshes it, so
|
|
64
|
+
// both flows validate against the same numbers (every container, running or stopped).
|
|
65
|
+
const [usedPorts, setUsedPorts] = useState<number[]>(initialUsedPorts);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
fetch('/api/agents/ports', { signal: controller.signal })
|
|
70
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
71
|
+
.then((data: { ports?: number[] } | null) => {
|
|
72
|
+
if (data && Array.isArray(data.ports)) setUsedPorts(data.ports);
|
|
73
|
+
})
|
|
74
|
+
.catch(() => { /* the server still validates on submit */ });
|
|
75
|
+
return () => controller.abort();
|
|
76
|
+
}, []);
|
|
63
77
|
const [selectedModel, setSelectedModel] = useState<string>(
|
|
64
78
|
models.length > 0 ? models[0].id : ''
|
|
65
79
|
);
|
|
@@ -73,11 +87,11 @@ export default function CreatePageClient({ templates, models, usedNames, usedPor
|
|
|
73
87
|
setSelectedTemplate(id);
|
|
74
88
|
setAgentName('');
|
|
75
89
|
// Auto-assign port block: find first free 10-port block
|
|
76
|
-
if (
|
|
90
|
+
if (usedPorts.length === 0) {
|
|
77
91
|
setPortRange('3700-3709');
|
|
78
92
|
} else {
|
|
79
|
-
//
|
|
80
|
-
const block =
|
|
93
|
+
// Same block finder as the server: the first free 10-port block.
|
|
94
|
+
const block = findAvailablePortBlock(new Set(usedPorts));
|
|
81
95
|
setPortRange(block ? `${block}-${block + 9}` : '');
|
|
82
96
|
}
|
|
83
97
|
};
|
|
@@ -91,9 +105,9 @@ export default function CreatePageClient({ templates, models, usedNames, usedPor
|
|
|
91
105
|
|
|
92
106
|
const portError = useMemo(() => {
|
|
93
107
|
if (!portRange.trim()) return '';
|
|
94
|
-
const res = validatePortInput(portRange, new Set(
|
|
108
|
+
const res = validatePortInput(portRange, new Set(usedPorts));
|
|
95
109
|
return res.valid ? '' : res.error;
|
|
96
|
-
}, [portRange,
|
|
110
|
+
}, [portRange, usedPorts]);
|
|
97
111
|
|
|
98
112
|
const canProceed = (): boolean => {
|
|
99
113
|
if (step === 'template') return selectedTemplate !== '';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
3
|
+
import { getUsedHostPorts } from '@/lib/agent-ports-server';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /api/agents/ports
|
|
9
|
+
*
|
|
10
|
+
* The host ports held by any container, running or stopped — the set the port validators
|
|
11
|
+
* in lib/agent-ports.ts take. The create form receives it as a page prop; the Edit panel
|
|
12
|
+
* fetches it when it opens, so both flows validate a range against the same numbers and
|
|
13
|
+
* the same rules, and give the same message.
|
|
14
|
+
*/
|
|
15
|
+
export async function GET(request: Request): Promise<NextResponse> {
|
|
16
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
17
|
+
const ports = await getUsedHostPorts();
|
|
18
|
+
return NextResponse.json({ ports: [...ports].sort((a, b) => a - b) });
|
|
19
|
+
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -189,6 +189,10 @@ The central container, running the Next.js dashboard + orchestration API.
|
|
|
189
189
|
`recreateAgentContainer()` refuses such a clash **before** removing anything, naming the
|
|
190
190
|
container that holds each port, so the agent stays up and the operator changes the range
|
|
191
191
|
from the Edit panel; a failed `docker run` no longer leaves a container behind.
|
|
192
|
+
The two forms validate a range **as it is typed** against that same set — the create
|
|
193
|
+
wizard through its page prop plus `GET /api/agents/ports`, the Edit panel through the
|
|
194
|
+
endpoint when it opens — so both show the same message and disable their action while
|
|
195
|
+
the range is refused, instead of letting the server refuse it on submit.
|
|
192
196
|
- **Agent edits** are `lib/agent-edit.ts`, with their state in the `agent_edits` table
|
|
193
197
|
(`lib/agent-edit-state.ts`): the display name (`AGENT_NAME`) and/or the port range are
|
|
194
198
|
applied by rebuilding the container on the image it already runs — **no backup**, the
|
|
@@ -48,7 +48,7 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
|
|
|
48
48
|
| `ImageDownloadBanner` | `app/agents/ImageDownloadBanner.tsx` | Agent image banner on the Agents page. Polls `/api/agents/image-status` every 2 s; offers **Download Image** when no supported version is downloaded, **Download <version>** when the registry publishes a newer one, and shows the download in progress and its completion. Downloading changes no agent. |
|
|
49
49
|
| `BrowserAccessSection` / `OpenControlUiButton` | `app/agents/BrowserAccessSection.tsx` | Browser access to one agent's Control UI, in its detail panel: requests waiting for approval (Approve / Reject) and approved browsers (Rename / Revoke), refreshed every 5 s while mounted. A successful approve, reject, rename or revoke updates the list at once, since the refresh behind it runs the OpenClaw CLI and takes seconds; a read started before the mutation is discarded. On agents that require approval, "Invite link" fetches `/api/agents/[id]/invite-link` and shows the link in a read-only field with Copy, which uses the Clipboard API in a secure context and the field's selection over plain HTTP, plus a warning when the link uses localhost. `OpenControlUiButton` opens `/api/agents/[id]/open-control-ui` in a new tab inside the click; that route redirects to a one-time link that pairs the browser with no approval, or to the plain token link when none can be issued. Used on the agent cards and in the panel. |
|
|
50
50
|
| `ChannelManager` / `ChannelSection` | `app/agents/ChannelManager.tsx`, `ChannelSection` inline in `app/agents/PageClient.tsx` | Telegram, in the agent detail panel (`ChannelSection` is the card that opens the modal; the modal title is the agent's display name). Reads `GET /channels`; lists pending pairing requests with **Approve** (`POST /channels/pairing`) and approved senders with **Revoke** after a confirm (`DELETE /channels/pairing?senderId=`). Pending comes from `openclaw pairing list`, approved from OpenClaw's pairing store (`lib/channelManager.ts`). Polls pairings every 5 s while open; one keyed busy state per action. |
|
|
51
|
-
| `EditAgentModal` / `EditBanner` | `app/agents/PageClient.tsx` | Rename/ports editing. The modal has **Display Name** and **Host Port** (same input and validation messages as the create wizard, from `lib/agent-ports.ts`), sends `PATCH /api/agents/[id]` and closes on `202`; `EditBanner` (top of the agent detail) polls `GET /api/agents/[id]` every 2 s while an edit is `rebuilding`, resumes on mount, and refetches the agent once it finishes, so a reload or a navigation shows the running edit instead of allowing a second (the server answers 409). No backup is taken: the volume is untouched. |
|
|
51
|
+
| `EditAgentModal` / `EditBanner` | `app/agents/PageClient.tsx` | Rename/ports editing. The modal has **Display Name** and **Host Port** (same input and validation messages as the create wizard, from `lib/agent-ports.ts`), sends `PATCH /api/agents/[id]` and closes on `202`; `EditBanner` (top of the agent detail) polls `GET /api/agents/[id]` every 2 s while an edit is `rebuilding`, resumes on mount, and refetches the agent once it finishes, so a reload or a navigation shows the running edit instead of allowing a second (the server answers 409). No backup is taken: the volume is untouched. The port range is validated **as it is typed**: the modal fetches `GET /api/agents/ports` when it opens (the same set the create form uses, every container, running or stopped) and excludes the agent's own block, so an occupied range shows the API's message inline and the Save button stays disabled — the create wizard does the same with the same endpoint. |
|
|
52
52
|
| `ModelSection` | `app/agents/ModelSection.tsx` | Primary model and fallbacks for one agent, in its detail panel. Explicit save, no restart. The model is a property of the agent, not of the gateway. Tags models the catalogue marks `deprecated`. |
|
|
53
53
|
| `ModelsProvider` / `useModels` | `app/lib/models-context.tsx` | The client's single model list, from `/api/models`. Whatever changes what is offered calls `refresh()`: the Gateway page after a toggle, a key save or removal, or a sync, and the first-run wizard after saving keys. Everything else only reads, PulseChat included. |
|
|
54
54
|
| `WizardPageClient` + step components | `app/wizard/PageClient.tsx` | Multi-step first-run wizard plus its frame shell, with mobile-first CSS. |
|
|
@@ -704,6 +704,27 @@ List running agent containers (Docker containers with `AGENT_ID` label) with ful
|
|
|
704
704
|
|
|
705
705
|
---
|
|
706
706
|
|
|
707
|
+
### `GET /api/agents/ports`
|
|
708
|
+
The host ports held by any container, running or stopped: the set the validators in
|
|
709
|
+
`lib/agent-ports.ts` take, sorted ascending. Backed by the shared lookup
|
|
710
|
+
(`getUsedHostPorts()`, `lib/agent-ports-server.ts`), which reads every container's
|
|
711
|
+
`HostConfig.PortBindings` — a stopped container keeps its block while `docker ps -a` and
|
|
712
|
+
the API's container list report nothing for it.
|
|
713
|
+
|
|
714
|
+
**Auth:** `rev4a_token` cookie or `Authorization: Bearer <REV4A_TOKEN>`
|
|
715
|
+
|
|
716
|
+
**Response:**
|
|
717
|
+
```json
|
|
718
|
+
{ "ports": [3700, 3701, 3710, 3711, 3741, 5555] }
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
The Edit panel fetches it when it opens and the create form refreshes it on mount (the
|
|
722
|
+
page also receives it as a prop), so both validate a range as the operator types, against
|
|
723
|
+
the same numbers and with the same messages, and both disable their action while the range
|
|
724
|
+
is refused.
|
|
725
|
+
|
|
726
|
+
---
|
|
727
|
+
|
|
707
728
|
### `GET /api/agents/models-summary`
|
|
708
729
|
One row per agent container, running or not: the model it runs and whether that
|
|
709
730
|
model is still in good standing. Feeds the model chip on the agent cards. Running
|