6464dbe28c
Stefan-Wunsch: ARIA-Aenderungen an Dateien sollen vom System (nicht
von ARIA selbst) automatisch versioniert werden. Im Datei-Manager:
Versionen auflisten, einzelne downloaden, oder als neue aktive Version
setzen (Restore = non-destructive neuer Commit).
Implementierung (alles im diagnostic-Container, da der eh schon
File-Handling kann):
1. Dockerfile: apk add git
2. server.js — Auto-Commit-Loop:
- Beim Start: /shared/uploads als git-Repo initialisieren (idempotent;
bestehendes .git wird uebernommen)
- setInterval(30s): git status --porcelain → wenn dirty, add+commit
mit "auto: <ISO-Timestamp>"-Message
- Re-Entrancy-Guard fuer langsame git-Ops
3. server.js — drei neue HTTP-Routen:
GET /api/files-versions?path=X
→ [{hash, ts, subject, isCurrent}] aus git log --follow
GET /api/files-version-content?path=X&hash=Y
→ Binary-Stream der Datei aus diesem Commit (Content-Disposition
attachment mit "name@<short-hash>.ext" als Default-Dateiname)
POST /api/files-version-restore body={path, hash}
→ non-destructive: schreibt alten Inhalt als NEUE Version, neuer
Commit "restore: <path> <- <short>". Aktive Version damit
weiterhin rollback-bar.
4. index.html — Datei-Manager:
- Pro Datei zusaetzlich 🕒-Button neben ⬇/🗑
- Klick zeigt Modal mit Version-Liste (timestamp, short-hash,
'AKTIV'-Marker fuer den jeweils letzten)
- Pro Version: ⬇ Download + ⟲ Restore (mit Confirm)
- Restore broadcasted file_version_restored damit Browser refreshen
Path-Safety: alle Pfade muessen relative-to-uploads sein, kein '..',
kein '/', kein '.git/'. Hash muss [0-9a-f]{7,40}.
.gitignore zunaechst keine — uploads/ ist eh nur User-/ARIA-Dateien,
kein Log-Noise erwartet. Falls Disk explodiert: spaeter ergaenzen.
Step-2 (App-Side via RVS-Messages) folgt im naechsten Commit, sobald
das hier in Diagnostic funktioniert.
2765 lines
113 KiB
JavaScript
2765 lines
113 KiB
JavaScript
"use strict";
|
|
|
|
/**
|
|
* ARIA Diagnostic Server
|
|
*
|
|
* Leichtgewichtiges Diagnose-Tool:
|
|
* - Verbindet sich direkt mit OpenClaw Gateway (ws://127.0.0.1:18789)
|
|
* - Fuehrt den vollstaendigen Handshake durch
|
|
* - Sendet Testnachrichten und zeigt Antworten
|
|
* - Zeigt Verbindungsstatus aller Komponenten
|
|
*
|
|
* Laeuft im selben Netzwerk wie aria-core (network_mode: service:aria)
|
|
*/
|
|
|
|
const http = require("http");
|
|
const { WebSocket, WebSocketServer } = require("ws");
|
|
const crypto = require("crypto");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
// ── Konfiguration ───────────────────────────────────────
|
|
const HTTP_PORT = parseInt(process.env.DIAG_PORT || "3001", 10);
|
|
const GATEWAY_URL = process.env.ARIA_CORE_WS || "ws://127.0.0.1:18789";
|
|
const GATEWAY_TOKEN = process.env.ARIA_AUTH_TOKEN || "";
|
|
const RVS_HOST = process.env.RVS_HOST || "";
|
|
const RVS_PORT = process.env.RVS_PORT || "443";
|
|
const RVS_TLS = process.env.RVS_TLS || "true";
|
|
const RVS_TLS_FALLBACK = process.env.RVS_TLS_FALLBACK || "true";
|
|
const RVS_TOKEN = process.env.RVS_TOKEN || "";
|
|
const PROXY_URL = process.env.PROXY_URL || "http://proxy:3456";
|
|
|
|
// ── Persistenz fuer agent_stream-Events ──────────────────
|
|
// Jeder agent_stream-Event wird parallel zum Broadcast in eine .jsonl
|
|
// geschrieben. Live-View laedt beim Tab-Oeffnen die letzten ~200 Zeilen,
|
|
// damit Browser-Reload / Standby den Verlauf nicht wegwerfen. Rotation
|
|
// haendelt logrotate / manual cleanup — wir cappen hier nur weichweich.
|
|
const AGENT_STREAM_LOG = process.env.AGENT_STREAM_LOG || "/shared/logs/agent_stream.jsonl";
|
|
const AGENT_STREAM_MAX_BYTES = 50 * 1024 * 1024; // 50 MB → halten den File handlebar
|
|
function appendAgentStream(payload) {
|
|
if (!payload || typeof payload !== "object") return;
|
|
try {
|
|
const line = JSON.stringify({ ts: Date.now(), ...payload }) + "\n";
|
|
// Soft-Cap: bei >50 MB ein Truncate auf den letzten ~25 MB Inhalt
|
|
try {
|
|
const st = fs.statSync(AGENT_STREAM_LOG);
|
|
if (st.size > AGENT_STREAM_MAX_BYTES) {
|
|
const half = Math.floor(AGENT_STREAM_MAX_BYTES / 2);
|
|
const fd = fs.openSync(AGENT_STREAM_LOG, "r");
|
|
const buf = Buffer.alloc(half);
|
|
fs.readSync(fd, buf, 0, half, st.size - half);
|
|
fs.closeSync(fd);
|
|
// bis zum naechsten Newline springen damit wir keine halbe Zeile haben
|
|
const firstNl = buf.indexOf(0x0a);
|
|
const start = firstNl >= 0 ? firstNl + 1 : 0;
|
|
fs.writeFileSync(AGENT_STREAM_LOG, buf.slice(start));
|
|
}
|
|
} catch {}
|
|
// Verzeichnis sicherstellen
|
|
try { fs.mkdirSync(path.dirname(AGENT_STREAM_LOG), { recursive: true }); } catch {}
|
|
fs.appendFileSync(AGENT_STREAM_LOG, line);
|
|
} catch (e) {
|
|
// Schweigend ignorieren — Persistence darf den Stream nicht blockieren
|
|
}
|
|
}
|
|
|
|
// ── State ───────────────────────────────────────────────
|
|
const state = {
|
|
gateway: { status: "disconnected", lastError: null, handshakeOk: false },
|
|
rvs: { status: "disconnected", lastError: null },
|
|
proxy: { status: "unknown", lastError: null },
|
|
};
|
|
const SESSION_KEY_FILE = "/data/active-session";
|
|
// /data Verzeichnis sicherstellen (Volume Mount)
|
|
try { fs.mkdirSync("/data", { recursive: true }); } catch (e) {
|
|
console.error(`[startup] /data mkdir fehlgeschlagen: ${e.message}`);
|
|
}
|
|
// sessionFromFile zeigt an, ob der aktive Key aus der Datei kam.
|
|
// Wenn true, darf resolveActiveSession NICHT mehr auto-picken (Wahl respektieren).
|
|
let sessionFromFile = false;
|
|
let activeSessionKey = (() => {
|
|
try {
|
|
const saved = fs.readFileSync(SESSION_KEY_FILE, "utf-8").trim();
|
|
if (saved) {
|
|
console.log(`[startup] Gespeicherte Session geladen: '${saved}'`);
|
|
sessionFromFile = true;
|
|
return saved;
|
|
}
|
|
} catch (e) {
|
|
console.error(`[startup] SESSION_KEY_FILE read: ${e.code || e.message}`);
|
|
}
|
|
console.log("[startup] Keine gespeicherte Session — Fallback 'main'");
|
|
return "main";
|
|
})();
|
|
|
|
// ── Auto-Versionierung /shared/uploads/ via git ────────────────
|
|
//
|
|
// Jede Aenderung im uploads/-Verzeichnis (User-Upload, ARIA-Generate,
|
|
// ARIA-Bearbeitung) wird durch eine 30s-Polling-Loop in einen git-Commit
|
|
// gepackt. Idempotent (kein Commit ohne Diff), kein Bloat im Normalbetrieb.
|
|
// Stefan kann via UI eine Version anschauen, herunterladen oder als
|
|
// neue aktive Version setzen (Restore = neuer commit mit altem Inhalt,
|
|
// non-destructive).
|
|
const SHARED_UPLOADS = "/shared/uploads";
|
|
const VERSIONING_INTERVAL_MS = 30 * 1000;
|
|
const { execFile } = require("child_process");
|
|
|
|
function git(args, opts = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = execFile(
|
|
"git",
|
|
["-C", SHARED_UPLOADS, ...args],
|
|
{ maxBuffer: 20 * 1024 * 1024, ...opts },
|
|
(err, stdout, stderr) => {
|
|
if (err && !opts.allowFail) {
|
|
err.stderr = stderr;
|
|
return reject(err);
|
|
}
|
|
resolve({
|
|
stdout: stdout || "",
|
|
stderr: stderr || "",
|
|
code: err ? (err.code || 1) : 0,
|
|
});
|
|
},
|
|
);
|
|
if (opts.input != null) {
|
|
try { child.stdin.write(opts.input); } catch (_) {}
|
|
try { child.stdin.end(); } catch (_) {}
|
|
}
|
|
});
|
|
}
|
|
|
|
async function initSharedVersioning() {
|
|
try {
|
|
fs.mkdirSync(SHARED_UPLOADS, { recursive: true });
|
|
} catch (e) {
|
|
console.error(`[shared-git] mkdir uploads fehlgeschlagen: ${e.message}`);
|
|
return;
|
|
}
|
|
const gitDir = path.join(SHARED_UPLOADS, ".git");
|
|
if (!fs.existsSync(gitDir)) {
|
|
console.log("[shared-git] Initialisiere /shared/uploads als git-Repo");
|
|
try {
|
|
await git(["init", "-q", "-b", "main"]);
|
|
await git(["config", "user.email", "aria@diagnostic"]);
|
|
await git(["config", "user.name", "aria-diagnostic"]);
|
|
// Initial commit (auch wenn leer) damit log/checkout immer funktioniert
|
|
await git(["commit", "-q", "--allow-empty", "-m", "initial snapshot"]);
|
|
// Falls schon Files drin sind: noch ein 'auto'-Commit hinten dran
|
|
const status = await git(["status", "--porcelain"]);
|
|
if (status.stdout.trim()) {
|
|
await git(["add", "-A"]);
|
|
await git(["commit", "-q", "-m", `auto: ${new Date().toISOString()}`]);
|
|
}
|
|
console.log("[shared-git] Init OK");
|
|
} catch (e) {
|
|
console.error(`[shared-git] Init fehlgeschlagen: ${e.message}`);
|
|
return;
|
|
}
|
|
} else {
|
|
console.log("[shared-git] Bestehendes git-Repo erkannt — uebernehme");
|
|
}
|
|
setInterval(autoCommitTick, VERSIONING_INTERVAL_MS);
|
|
console.log(`[shared-git] Auto-Commit-Loop alle ${VERSIONING_INTERVAL_MS}ms aktiv`);
|
|
}
|
|
|
|
let autoCommitBusy = false;
|
|
async function autoCommitTick() {
|
|
if (autoCommitBusy) return; // re-entrancy guard fuer langsame git ops
|
|
autoCommitBusy = true;
|
|
try {
|
|
const status = await git(["status", "--porcelain"]);
|
|
if (!status.stdout.trim()) return;
|
|
await git(["add", "-A"]);
|
|
const ts = new Date().toISOString();
|
|
await git(["commit", "-q", "-m", `auto: ${ts}`]);
|
|
console.log(`[shared-git] auto-commit @ ${ts}`);
|
|
} catch (e) {
|
|
console.error(`[shared-git] auto-commit fehlgeschlagen: ${e.message}`);
|
|
} finally {
|
|
autoCommitBusy = false;
|
|
}
|
|
}
|
|
|
|
// Versions-API helpers — werden weiter unten von den Routen genutzt.
|
|
function isPathSafe(rel) {
|
|
if (!rel || typeof rel !== "string") return false;
|
|
if (rel.includes("..") || rel.startsWith("/") || rel.startsWith(".git")) return false;
|
|
return true;
|
|
}
|
|
async function listVersionsForFile(rel) {
|
|
// git log --follow damit Renames trotzdem die Historie zeigen
|
|
const out = await git(["log", "--follow", "--format=%H%x00%aI%x00%s", "--", rel]);
|
|
const lines = out.stdout.trim().split("\n").filter(Boolean);
|
|
const versions = lines.map(line => {
|
|
const [hash, isoTs, subject] = line.split(" |