初始化提交
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
This commit is contained in:
504
crates/openfang-api/static/js/pages/hands.js
Normal file
504
crates/openfang-api/static/js/pages/hands.js
Normal file
@@ -0,0 +1,504 @@
|
||||
// OpenFang Hands Page — curated autonomous capability packages
|
||||
'use strict';
|
||||
|
||||
function handsPage() {
|
||||
return {
|
||||
tab: 'available',
|
||||
hands: [],
|
||||
instances: [],
|
||||
loading: true,
|
||||
activeLoading: false,
|
||||
loadError: '',
|
||||
activatingId: null,
|
||||
activateResult: null,
|
||||
detailHand: null,
|
||||
settingsValues: {},
|
||||
_toastTimer: null,
|
||||
browserViewer: null,
|
||||
browserViewerOpen: false,
|
||||
_browserPollTimer: null,
|
||||
|
||||
// ── Setup Wizard State ──────────────────────────────────────────────
|
||||
setupWizard: null,
|
||||
setupStep: 1,
|
||||
setupLoading: false,
|
||||
setupChecking: false,
|
||||
clipboardMsg: null,
|
||||
_clipboardTimer: null,
|
||||
detectedPlatform: 'linux',
|
||||
installPlatforms: {},
|
||||
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
this.loadError = '';
|
||||
try {
|
||||
var data = await OpenFangAPI.get('/api/hands');
|
||||
this.hands = data.hands || [];
|
||||
} catch(e) {
|
||||
this.hands = [];
|
||||
this.loadError = e.message || 'Could not load hands.';
|
||||
}
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
async loadActive() {
|
||||
this.activeLoading = true;
|
||||
try {
|
||||
var data = await OpenFangAPI.get('/api/hands/active');
|
||||
this.instances = (data.instances || []).map(function(i) {
|
||||
i._stats = null;
|
||||
return i;
|
||||
});
|
||||
} catch(e) {
|
||||
this.instances = [];
|
||||
}
|
||||
this.activeLoading = false;
|
||||
},
|
||||
|
||||
getHandIcon(handId) {
|
||||
for (var i = 0; i < this.hands.length; i++) {
|
||||
if (this.hands[i].id === handId) return this.hands[i].icon;
|
||||
}
|
||||
return '\u{1F91A}';
|
||||
},
|
||||
|
||||
async showDetail(handId) {
|
||||
try {
|
||||
var data = await OpenFangAPI.get('/api/hands/' + handId);
|
||||
this.detailHand = data;
|
||||
} catch(e) {
|
||||
for (var i = 0; i < this.hands.length; i++) {
|
||||
if (this.hands[i].id === handId) {
|
||||
this.detailHand = this.hands[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ── Setup Wizard ────────────────────────────────────────────────────
|
||||
|
||||
async activate(handId) {
|
||||
this.openSetupWizard(handId);
|
||||
},
|
||||
|
||||
async openSetupWizard(handId) {
|
||||
this.setupLoading = true;
|
||||
this.setupWizard = null;
|
||||
try {
|
||||
var data = await OpenFangAPI.get('/api/hands/' + handId);
|
||||
// Pre-populate settings defaults
|
||||
this.settingsValues = {};
|
||||
if (data.settings && data.settings.length > 0) {
|
||||
for (var i = 0; i < data.settings.length; i++) {
|
||||
var s = data.settings[i];
|
||||
this.settingsValues[s.key] = s.default || '';
|
||||
}
|
||||
}
|
||||
// Detect platform from server response, fallback to client-side
|
||||
if (data.server_platform) {
|
||||
this.detectedPlatform = data.server_platform;
|
||||
} else {
|
||||
this._detectClientPlatform();
|
||||
}
|
||||
// Initialize per-requirement platform selections
|
||||
this.installPlatforms = {};
|
||||
if (data.requirements) {
|
||||
for (var j = 0; j < data.requirements.length; j++) {
|
||||
this.installPlatforms[data.requirements[j].key] = this.detectedPlatform;
|
||||
}
|
||||
}
|
||||
this.setupWizard = data;
|
||||
// Skip deps step if no requirements
|
||||
var hasReqs = data.requirements && data.requirements.length > 0;
|
||||
this.setupStep = hasReqs ? 1 : 2;
|
||||
} catch(e) {
|
||||
this.showToast('Could not load hand details: ' + (e.message || 'unknown error'));
|
||||
}
|
||||
this.setupLoading = false;
|
||||
},
|
||||
|
||||
_detectClientPlatform() {
|
||||
var ua = (navigator.userAgent || '').toLowerCase();
|
||||
if (ua.indexOf('mac') !== -1) {
|
||||
this.detectedPlatform = 'macos';
|
||||
} else if (ua.indexOf('win') !== -1) {
|
||||
this.detectedPlatform = 'windows';
|
||||
} else {
|
||||
this.detectedPlatform = 'linux';
|
||||
}
|
||||
},
|
||||
|
||||
// ── Auto-Install Dependencies ───────────────────────────────────
|
||||
installProgress: null, // null = idle, object = { status, current, total, results, error }
|
||||
|
||||
async installDeps() {
|
||||
if (!this.setupWizard) return;
|
||||
var handId = this.setupWizard.id;
|
||||
var missing = (this.setupWizard.requirements || []).filter(function(r) { return !r.satisfied; });
|
||||
if (missing.length === 0) {
|
||||
this.showToast('All dependencies already installed!');
|
||||
return;
|
||||
}
|
||||
|
||||
this.installProgress = {
|
||||
status: 'installing',
|
||||
current: 0,
|
||||
total: missing.length,
|
||||
currentLabel: missing[0] ? missing[0].label : '',
|
||||
results: [],
|
||||
error: null
|
||||
};
|
||||
|
||||
try {
|
||||
var data = await OpenFangAPI.post('/api/hands/' + handId + '/install-deps', {});
|
||||
var results = data.results || [];
|
||||
this.installProgress.results = results;
|
||||
this.installProgress.current = results.length;
|
||||
this.installProgress.status = 'done';
|
||||
|
||||
// Update requirements from server response
|
||||
if (data.requirements && this.setupWizard.requirements) {
|
||||
for (var i = 0; i < this.setupWizard.requirements.length; i++) {
|
||||
var existing = this.setupWizard.requirements[i];
|
||||
for (var j = 0; j < data.requirements.length; j++) {
|
||||
if (data.requirements[j].key === existing.key) {
|
||||
existing.satisfied = data.requirements[j].satisfied;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setupWizard.requirements_met = data.requirements_met;
|
||||
}
|
||||
|
||||
var installed = results.filter(function(r) { return r.status === 'installed' || r.status === 'already_installed'; }).length;
|
||||
var failed = results.filter(function(r) { return r.status === 'error' || r.status === 'timeout'; }).length;
|
||||
|
||||
if (data.requirements_met) {
|
||||
this.showToast('All dependencies installed successfully!');
|
||||
// Auto-advance to step 2 after a short delay
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
self.installProgress = null;
|
||||
self.setupNextStep();
|
||||
}, 1500);
|
||||
} else if (failed > 0) {
|
||||
this.installProgress.error = failed + ' dependency(ies) failed to install. Check the details below.';
|
||||
}
|
||||
} catch(e) {
|
||||
this.installProgress = {
|
||||
status: 'error',
|
||||
current: 0,
|
||||
total: missing.length,
|
||||
currentLabel: '',
|
||||
results: [],
|
||||
error: e.message || 'Installation request failed'
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
getInstallResultIcon(status) {
|
||||
if (status === 'installed' || status === 'already_installed') return '\u2713';
|
||||
if (status === 'error' || status === 'timeout') return '\u2717';
|
||||
return '\u2022';
|
||||
},
|
||||
|
||||
getInstallResultClass(status) {
|
||||
if (status === 'installed' || status === 'already_installed') return 'dep-met';
|
||||
if (status === 'error' || status === 'timeout') return 'dep-missing';
|
||||
return '';
|
||||
},
|
||||
|
||||
async recheckDeps() {
|
||||
if (!this.setupWizard) return;
|
||||
this.setupChecking = true;
|
||||
try {
|
||||
var data = await OpenFangAPI.post('/api/hands/' + this.setupWizard.id + '/check-deps', {});
|
||||
if (data.requirements && this.setupWizard.requirements) {
|
||||
for (var i = 0; i < this.setupWizard.requirements.length; i++) {
|
||||
var existing = this.setupWizard.requirements[i];
|
||||
for (var j = 0; j < data.requirements.length; j++) {
|
||||
if (data.requirements[j].key === existing.key) {
|
||||
existing.satisfied = data.requirements[j].satisfied;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setupWizard.requirements_met = data.requirements_met;
|
||||
}
|
||||
if (data.requirements_met) {
|
||||
this.showToast('All dependencies satisfied!');
|
||||
}
|
||||
} catch(e) {
|
||||
this.showToast('Check failed: ' + (e.message || 'unknown'));
|
||||
}
|
||||
this.setupChecking = false;
|
||||
},
|
||||
|
||||
getInstallCmd(req) {
|
||||
if (!req || !req.install) return null;
|
||||
var inst = req.install;
|
||||
var plat = this.installPlatforms[req.key] || this.detectedPlatform;
|
||||
if (plat === 'macos' && inst.macos) return inst.macos;
|
||||
if (plat === 'windows' && inst.windows) return inst.windows;
|
||||
if (plat === 'linux') {
|
||||
return inst.linux_apt || inst.linux_dnf || inst.linux_pacman || inst.pip || null;
|
||||
}
|
||||
return inst.pip || inst.macos || inst.windows || inst.linux_apt || null;
|
||||
},
|
||||
|
||||
getLinuxVariant(req) {
|
||||
if (!req || !req.install) return null;
|
||||
var inst = req.install;
|
||||
var plat = this.installPlatforms[req.key] || this.detectedPlatform;
|
||||
if (plat !== 'linux') return null;
|
||||
// Return all available Linux variants
|
||||
var variants = [];
|
||||
if (inst.linux_apt) variants.push({ label: 'apt', cmd: inst.linux_apt });
|
||||
if (inst.linux_dnf) variants.push({ label: 'dnf', cmd: inst.linux_dnf });
|
||||
if (inst.linux_pacman) variants.push({ label: 'pacman', cmd: inst.linux_pacman });
|
||||
if (inst.pip) variants.push({ label: 'pip', cmd: inst.pip });
|
||||
return variants.length > 1 ? variants : null;
|
||||
},
|
||||
|
||||
copyToClipboard(text) {
|
||||
var self = this;
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
self.clipboardMsg = text;
|
||||
if (self._clipboardTimer) clearTimeout(self._clipboardTimer);
|
||||
self._clipboardTimer = setTimeout(function() { self.clipboardMsg = null; }, 2000);
|
||||
});
|
||||
},
|
||||
|
||||
get setupReqsMet() {
|
||||
if (!this.setupWizard || !this.setupWizard.requirements) return 0;
|
||||
var count = 0;
|
||||
for (var i = 0; i < this.setupWizard.requirements.length; i++) {
|
||||
if (this.setupWizard.requirements[i].satisfied) count++;
|
||||
}
|
||||
return count;
|
||||
},
|
||||
|
||||
get setupReqsTotal() {
|
||||
if (!this.setupWizard || !this.setupWizard.requirements) return 0;
|
||||
return this.setupWizard.requirements.length;
|
||||
},
|
||||
|
||||
get setupAllReqsMet() {
|
||||
return this.setupReqsTotal > 0 && this.setupReqsMet === this.setupReqsTotal;
|
||||
},
|
||||
|
||||
get setupHasReqs() {
|
||||
return this.setupReqsTotal > 0;
|
||||
},
|
||||
|
||||
get setupHasSettings() {
|
||||
return this.setupWizard && this.setupWizard.settings && this.setupWizard.settings.length > 0;
|
||||
},
|
||||
|
||||
setupNextStep() {
|
||||
if (this.setupStep === 1 && this.setupHasSettings) {
|
||||
this.setupStep = 2;
|
||||
} else if (this.setupStep === 1) {
|
||||
this.setupStep = 3;
|
||||
} else if (this.setupStep === 2) {
|
||||
this.setupStep = 3;
|
||||
}
|
||||
},
|
||||
|
||||
setupPrevStep() {
|
||||
if (this.setupStep === 3 && this.setupHasSettings) {
|
||||
this.setupStep = 2;
|
||||
} else if (this.setupStep === 3) {
|
||||
this.setupStep = this.setupHasReqs ? 1 : 2;
|
||||
} else if (this.setupStep === 2 && this.setupHasReqs) {
|
||||
this.setupStep = 1;
|
||||
}
|
||||
},
|
||||
|
||||
closeSetupWizard() {
|
||||
this.setupWizard = null;
|
||||
this.setupStep = 1;
|
||||
this.setupLoading = false;
|
||||
this.setupChecking = false;
|
||||
this.clipboardMsg = null;
|
||||
this.installPlatforms = {};
|
||||
},
|
||||
|
||||
async launchHand() {
|
||||
if (!this.setupWizard) return;
|
||||
var handId = this.setupWizard.id;
|
||||
var config = {};
|
||||
for (var key in this.settingsValues) {
|
||||
config[key] = this.settingsValues[key];
|
||||
}
|
||||
this.activatingId = handId;
|
||||
try {
|
||||
var data = await OpenFangAPI.post('/api/hands/' + handId + '/activate', { config: config });
|
||||
this.showToast('Hand "' + handId + '" activated as ' + (data.agent_name || data.instance_id));
|
||||
this.closeSetupWizard();
|
||||
await this.loadActive();
|
||||
this.tab = 'active';
|
||||
} catch(e) {
|
||||
this.showToast('Activation failed: ' + (e.message || 'unknown error'));
|
||||
}
|
||||
this.activatingId = null;
|
||||
},
|
||||
|
||||
selectOption(settingKey, value) {
|
||||
this.settingsValues[settingKey] = value;
|
||||
},
|
||||
|
||||
getSettingDisplayValue(setting) {
|
||||
var val = this.settingsValues[setting.key] || setting.default || '';
|
||||
if (setting.setting_type === 'toggle') {
|
||||
return val === 'true' ? 'Enabled' : 'Disabled';
|
||||
}
|
||||
if (setting.setting_type === 'select' && setting.options) {
|
||||
for (var i = 0; i < setting.options.length; i++) {
|
||||
if (setting.options[i].value === val) return setting.options[i].label;
|
||||
}
|
||||
}
|
||||
return val || '-';
|
||||
},
|
||||
|
||||
// ── Existing methods ────────────────────────────────────────────────
|
||||
|
||||
async pauseHand(inst) {
|
||||
try {
|
||||
await OpenFangAPI.post('/api/hands/instances/' + inst.instance_id + '/pause', {});
|
||||
inst.status = 'Paused';
|
||||
} catch(e) {
|
||||
this.showToast('Pause failed: ' + (e.message || 'unknown error'));
|
||||
}
|
||||
},
|
||||
|
||||
async resumeHand(inst) {
|
||||
try {
|
||||
await OpenFangAPI.post('/api/hands/instances/' + inst.instance_id + '/resume', {});
|
||||
inst.status = 'Active';
|
||||
} catch(e) {
|
||||
this.showToast('Resume failed: ' + (e.message || 'unknown error'));
|
||||
}
|
||||
},
|
||||
|
||||
async deactivate(inst) {
|
||||
var self = this;
|
||||
var handName = inst.agent_name || inst.hand_id;
|
||||
OpenFangToast.confirm('Deactivate Hand', 'Deactivate hand "' + handName + '"? This will kill its agent.', async function() {
|
||||
try {
|
||||
await OpenFangAPI.delete('/api/hands/instances/' + inst.instance_id);
|
||||
self.instances = self.instances.filter(function(i) { return i.instance_id !== inst.instance_id; });
|
||||
OpenFangToast.success('Hand deactivated.');
|
||||
} catch(e) {
|
||||
OpenFangToast.error('Deactivation failed: ' + (e.message || 'unknown error'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async loadStats(inst) {
|
||||
try {
|
||||
var data = await OpenFangAPI.get('/api/hands/instances/' + inst.instance_id + '/stats');
|
||||
inst._stats = data.metrics || {};
|
||||
} catch(e) {
|
||||
inst._stats = { 'Error': { value: e.message || 'Could not load stats', format: 'text' } };
|
||||
}
|
||||
},
|
||||
|
||||
formatMetric(m) {
|
||||
if (!m || m.value === null || m.value === undefined) return '-';
|
||||
if (m.format === 'duration') {
|
||||
var secs = parseInt(m.value, 10);
|
||||
if (isNaN(secs)) return String(m.value);
|
||||
var h = Math.floor(secs / 3600);
|
||||
var min = Math.floor((secs % 3600) / 60);
|
||||
var s = secs % 60;
|
||||
if (h > 0) return h + 'h ' + min + 'm';
|
||||
if (min > 0) return min + 'm ' + s + 's';
|
||||
return s + 's';
|
||||
}
|
||||
if (m.format === 'number') {
|
||||
var n = parseFloat(m.value);
|
||||
if (isNaN(n)) return String(m.value);
|
||||
return n.toLocaleString();
|
||||
}
|
||||
return String(m.value);
|
||||
},
|
||||
|
||||
showToast(msg) {
|
||||
var self = this;
|
||||
this.activateResult = msg;
|
||||
if (this._toastTimer) clearTimeout(this._toastTimer);
|
||||
this._toastTimer = setTimeout(function() { self.activateResult = null; }, 4000);
|
||||
},
|
||||
|
||||
// ── Browser Viewer ───────────────────────────────────────────────────
|
||||
|
||||
isBrowserHand(inst) {
|
||||
return inst.hand_id === 'browser';
|
||||
},
|
||||
|
||||
async openBrowserViewer(inst) {
|
||||
this.browserViewer = {
|
||||
instance_id: inst.instance_id,
|
||||
hand_id: inst.hand_id,
|
||||
agent_name: inst.agent_name,
|
||||
url: '',
|
||||
title: '',
|
||||
screenshot: '',
|
||||
content: '',
|
||||
loading: true,
|
||||
error: ''
|
||||
};
|
||||
this.browserViewerOpen = true;
|
||||
await this.refreshBrowserView();
|
||||
this.startBrowserPolling();
|
||||
},
|
||||
|
||||
async refreshBrowserView() {
|
||||
if (!this.browserViewer) return;
|
||||
var id = this.browserViewer.instance_id;
|
||||
try {
|
||||
var data = await OpenFangAPI.get('/api/hands/instances/' + id + '/browser');
|
||||
if (data.active) {
|
||||
this.browserViewer.url = data.url || '';
|
||||
this.browserViewer.title = data.title || '';
|
||||
this.browserViewer.screenshot = data.screenshot_base64 || '';
|
||||
this.browserViewer.content = data.content || '';
|
||||
this.browserViewer.error = '';
|
||||
} else {
|
||||
this.browserViewer.error = 'No active browser session';
|
||||
this.browserViewer.screenshot = '';
|
||||
}
|
||||
} catch(e) {
|
||||
this.browserViewer.error = e.message || 'Could not load browser state';
|
||||
}
|
||||
this.browserViewer.loading = false;
|
||||
},
|
||||
|
||||
startBrowserPolling() {
|
||||
var self = this;
|
||||
this.stopBrowserPolling();
|
||||
this._browserPollTimer = setInterval(function() {
|
||||
if (self.browserViewerOpen) {
|
||||
self.refreshBrowserView();
|
||||
} else {
|
||||
self.stopBrowserPolling();
|
||||
}
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
stopBrowserPolling() {
|
||||
if (this._browserPollTimer) {
|
||||
clearInterval(this._browserPollTimer);
|
||||
this._browserPollTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
closeBrowserViewer() {
|
||||
this.stopBrowserPolling();
|
||||
this.browserViewerOpen = false;
|
||||
this.browserViewer = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user