feat(skills): P3 config_schema + P4 Versionierung mit Rollback
P3 — Skill-Configuration
- aria-brain/skills.py: SKILL_CONFIGS_FILE (/shared/config/skill_configs.json)
als zentrale Werte-Persistenz. _normalize_config_schema validiert die
Schema-Felder (name/type/label/secret/description/default), CFG_<UPPER_NAME>
ENV beim run_skill. create_skill + update_skill akzeptieren config_schema.
- agent.py: skill_set_config Brain-Tool fuer ARIA. skill_create/update um
config_schema-Property erweitert.
- main.py: GET/POST /skills/{name}/config — secret-Werte in Antwort gemaskt.
P4 — Versionierung mit Rollback
- aria-brain/skills.py: archive_current_version archiviert nach
versions/v_<ts>/ (ohne venv/logs). update_skill ruft das automatisch auf
bevor strukturelle Aenderungen passieren. list_skill_versions,
rollback_skill (mit Safety-Snapshot + automatischem venv-Rebuild),
delete_skill_version.
- agent.py: skill_list_versions, skill_rollback Brain-Tools.
- main.py: GET /skills/{name}/versions, POST /skills/{name}/rollback,
DELETE /skills/{name}/versions/{version_id}.
UI
- diagnostic/index.html: Skill-Detail um Config-Form (typ-spezifisch,
Secrets als password-Input mit ***SET***-Hinweis) und Versions-Liste
mit Rollback-/Delete-Button.
- android SkillBrowser: SkillDetailModal laedt config_schema + versions
on-mount. Config-Form (TextInput + Switch fuer boolean), Versionen mit
Rollback-Confirm. brainApi um SkillConfigField/SkillVersion +
getSkillConfig/setSkillConfig/listSkillVersions/rollbackSkill/
deleteSkillVersion erweitert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3514,6 +3514,8 @@
|
||||
<button class="btn secondary" onclick="exportSkill('${escapeHtml(s.name)}')" style="padding:2px 10px;font-size:11px;color:#0096FF;border-color:#0096FF;">⬇ Export</button>
|
||||
<button class="btn secondary" onclick="deleteSkill('${escapeHtml(s.name)}')" style="padding:2px 10px;font-size:11px;color:#FF6B6B;border-color:#FF6B6B;">🗑 Löschen</button>
|
||||
</div>
|
||||
<div id="skill-config-${escapeHtml(s.name)}" style="margin-bottom:10px;"></div>
|
||||
<div id="skill-versions-${escapeHtml(s.name)}" style="margin-bottom:10px;"></div>
|
||||
<div style="color:#0096FF;font-size:11px;font-weight:bold;margin:6px 0 4px;">Logs (letzte 20)</div>
|
||||
<div id="skill-logs-${escapeHtml(s.name)}" style="font-size:11px;color:#8888AA;">(Logs lädt...)</div>
|
||||
</div>
|
||||
@@ -3547,6 +3549,8 @@
|
||||
const el = document.getElementById('skill-readme-' + name);
|
||||
if (el && d.readme) el.innerHTML = '<pre style="margin:0;font-family:inherit;white-space:pre-wrap;">' + escapeHtml(d.readme) + '</pre>';
|
||||
} catch {}
|
||||
loadSkillConfigSection(name);
|
||||
loadSkillVersionsSection(name);
|
||||
try {
|
||||
const r2 = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/logs');
|
||||
const d2 = await r2.json();
|
||||
@@ -3565,6 +3569,155 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Skill-Configs (P3) ─────────────────────────────────
|
||||
async function loadSkillConfigSection(name) {
|
||||
const el = document.getElementById('skill-config-' + name);
|
||||
if (!el) return;
|
||||
try {
|
||||
const r = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/config');
|
||||
if (!r.ok) { el.innerHTML = ''; return; }
|
||||
const d = await r.json();
|
||||
const schema = d.schema || [];
|
||||
if (!schema.length) { el.innerHTML = ''; return; }
|
||||
const values = d.values || {};
|
||||
const inputs = schema.map(f => {
|
||||
const fname = f.name;
|
||||
const label = f.label || fname;
|
||||
const desc = f.description ? `<div style="color:#555570;font-size:10px;">${escapeHtml(f.description)}</div>` : '';
|
||||
const isSecret = f.secret || f.type === 'password';
|
||||
const cur = values[fname];
|
||||
const placeholder = isSecret && cur === '***SET***' ? '••• gesetzt (leer lassen = unverändert) •••'
|
||||
: (f.default !== undefined && f.default !== null ? `Default: ${f.default}` : '');
|
||||
let inputEl;
|
||||
if (f.type === 'boolean') {
|
||||
const checked = (cur === true || cur === 'true') ? 'checked' : '';
|
||||
inputEl = `<input type="checkbox" data-cfg="${escapeHtml(fname)}" data-type="boolean" ${checked} style="margin-right:6px;">`;
|
||||
} else {
|
||||
const type = isSecret ? 'password' : (f.type === 'number' ? 'number' : 'text');
|
||||
const val = (isSecret) ? '' : (cur !== undefined && cur !== null && cur !== '***SET***' ? escapeHtml(String(cur)) : '');
|
||||
inputEl = `<input type="${type}" data-cfg="${escapeHtml(fname)}" data-type="${f.type || 'string'}" value="${val}" placeholder="${escapeHtml(placeholder)}" style="flex:1;padding:3px 6px;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;border-radius:3px;font-size:11px;">`;
|
||||
}
|
||||
return `<div style="margin-bottom:6px;">
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
<label style="min-width:120px;color:#8888AA;font-size:11px;">${escapeHtml(label)}${isSecret ? ' 🔒' : ''}</label>
|
||||
${inputEl}
|
||||
</div>
|
||||
${desc}
|
||||
</div>`;
|
||||
}).join('');
|
||||
el.innerHTML = `
|
||||
<div style="background:#0D0D1A;border:1px solid #1E1E2E;border-radius:4px;padding:8px;">
|
||||
<div style="color:#FFD60A;font-size:11px;font-weight:bold;margin-bottom:6px;">⚙ Konfiguration</div>
|
||||
${inputs}
|
||||
<button class="btn secondary" onclick="saveSkillConfig('${escapeHtml(name)}')" style="padding:3px 12px;font-size:11px;color:#3FFF3F;border-color:#3FFF3F;margin-top:4px;">💾 Speichern</button>
|
||||
<span id="skill-cfg-status-${escapeHtml(name)}" style="color:#8888AA;font-size:11px;margin-left:8px;"></span>
|
||||
</div>`;
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div style="color:#FF6B6B;font-size:11px;">Config-Load: ${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSkillConfig(name) {
|
||||
const el = document.getElementById('skill-config-' + name);
|
||||
if (!el) return;
|
||||
const inputs = el.querySelectorAll('[data-cfg]');
|
||||
// Erst aktuelle gespeicherte Werte holen — secret-Felder die leer sind sollen unverändert bleiben
|
||||
let existing = {};
|
||||
try {
|
||||
const r = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/config');
|
||||
const d = await r.json();
|
||||
existing = d.values || {};
|
||||
} catch {}
|
||||
const values = { ...existing };
|
||||
inputs.forEach(inp => {
|
||||
const fname = inp.getAttribute('data-cfg');
|
||||
const type = inp.getAttribute('data-type');
|
||||
let v;
|
||||
if (type === 'boolean') v = inp.checked;
|
||||
else if (type === 'number') v = inp.value === '' ? null : Number(inp.value);
|
||||
else v = inp.value;
|
||||
const isPassword = inp.type === 'password';
|
||||
if (isPassword && v === '') return; // leer bei secret = unverändert
|
||||
if (v === '' || v === null) { delete values[fname]; return; }
|
||||
if (v === '***SET***') return;
|
||||
values[fname] = v;
|
||||
});
|
||||
try {
|
||||
const r = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/config', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ values }),
|
||||
});
|
||||
const stat = document.getElementById('skill-cfg-status-' + name);
|
||||
if (r.ok) {
|
||||
if (stat) { stat.textContent = '✓ gespeichert'; stat.style.color = '#3FFF3F'; }
|
||||
loadSkillConfigSection(name);
|
||||
} else {
|
||||
if (stat) { stat.textContent = 'Fehler ' + r.status; stat.style.color = '#FF6B6B'; }
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Speichern fehlgeschlagen: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Skill-Versions (P4) ─────────────────────────────────
|
||||
async function loadSkillVersionsSection(name) {
|
||||
const el = document.getElementById('skill-versions-' + name);
|
||||
if (!el) return;
|
||||
try {
|
||||
const r = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/versions');
|
||||
if (!r.ok) { el.innerHTML = ''; return; }
|
||||
const d = await r.json();
|
||||
const versions = d.versions || [];
|
||||
if (!versions.length) { el.innerHTML = ''; return; }
|
||||
const fmtDate = (iso) => iso ? new Date(iso).toLocaleString('de-DE') : '?';
|
||||
const rows = versions.map(v => `
|
||||
<div style="display:flex;align-items:center;gap:6px;padding:3px 0;border-bottom:1px solid #1E1E2E;">
|
||||
<span style="flex:1;font-family:monospace;font-size:10px;color:#E0E0F0;">${escapeHtml(v.version_id)}</span>
|
||||
<span style="font-size:10px;color:#8888AA;">${fmtDate(v.archived_at)}</span>
|
||||
<span style="flex:2;font-size:10px;color:#8888AA;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(v.summary || '')}</span>
|
||||
<button class="btn secondary" onclick="rollbackSkillVersion('${escapeHtml(name)}','${escapeHtml(v.version_id)}')" style="padding:1px 8px;font-size:10px;color:#FFD60A;border-color:#FFD60A;">↺ Rollback</button>
|
||||
<button class="btn secondary" onclick="deleteSkillVersion('${escapeHtml(name)}','${escapeHtml(v.version_id)}')" style="padding:1px 8px;font-size:10px;color:#FF6B6B;border-color:#FF6B6B;">🗑</button>
|
||||
</div>
|
||||
`).join('');
|
||||
el.innerHTML = `
|
||||
<div style="background:#0D0D1A;border:1px solid #1E1E2E;border-radius:4px;padding:8px;">
|
||||
<div style="color:#FFD60A;font-size:11px;font-weight:bold;margin-bottom:6px;">📦 Versionen (${versions.length})</div>
|
||||
${rows}
|
||||
</div>`;
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div style="color:#FF6B6B;font-size:11px;">Versions-Load: ${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function rollbackSkillVersion(name, versionId) {
|
||||
if (!confirm(`Skill "${name}" auf Version ${versionId} zurückrollen?\n\nDer aktuelle Stand wird vorher automatisch gesichert (safety-snapshot).`)) return;
|
||||
try {
|
||||
const r = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/rollback', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ version_id: versionId }),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (r.ok) {
|
||||
alert(`✓ Rollback OK\nSicherheits-Snapshot: ${d.safety_snapshot}`);
|
||||
loadSkillVersionsSection(name);
|
||||
loadSkills();
|
||||
} else {
|
||||
alert('Rollback fehlgeschlagen: ' + (d.detail || JSON.stringify(d)));
|
||||
}
|
||||
} catch (e) { alert('Rollback-Fehler: ' + e.message); }
|
||||
}
|
||||
|
||||
async function deleteSkillVersion(name, versionId) {
|
||||
if (!confirm(`Version ${versionId} von "${name}" wirklich löschen?\n\nNicht rückholbar.`)) return;
|
||||
try {
|
||||
const r = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/versions/' + encodeURIComponent(versionId), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (r.ok) loadSkillVersionsSection(name);
|
||||
else { const d = await r.json().catch(()=>({})); alert('Löschen fehlgeschlagen: ' + (d.detail || r.status)); }
|
||||
} catch (e) { alert('Fehler: ' + e.message); }
|
||||
}
|
||||
|
||||
async function toggleSkillActive(name, newActive) {
|
||||
try {
|
||||
await fetch('/api/brain/skills/' + encodeURIComponent(name), {
|
||||
|
||||
Reference in New Issue
Block a user