diff --git a/package/secubox/luci-app-openclaw/Makefile b/package/secubox/luci-app-openclaw/Makefile new file mode 100644 index 00000000..37ad4548 --- /dev/null +++ b/package/secubox/luci-app-openclaw/Makefile @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: MIT +# +# LuCI interface for OpenClaw Personal AI Assistant +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI OpenClaw AI Assistant +LUCI_DESCRIPTION:=Web interface for OpenClaw personal AI with Claude/OpenAI/Ollama support +LUCI_DEPENDS:=+luci-base +secubox-app-openclaw +LUCI_PKGARCH:=all + +PKG_NAME:=luci-app-openclaw +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_LICENSE:=MIT +PKG_MAINTAINER:=CyberMind Studio + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-openclaw/install + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-openclaw.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-openclaw.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.openclaw $(1)/usr/libexec/rpcd/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/openclaw + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/openclaw/*.js $(1)/www/luci-static/resources/view/openclaw/ +endef + +$(eval $(call BuildPackage,luci-app-openclaw)) diff --git a/package/secubox/luci-app-openclaw/htdocs/luci-static/resources/view/openclaw/chat.js b/package/secubox/luci-app-openclaw/htdocs/luci-static/resources/view/openclaw/chat.js new file mode 100644 index 00000000..9ee04a19 --- /dev/null +++ b/package/secubox/luci-app-openclaw/htdocs/luci-static/resources/view/openclaw/chat.js @@ -0,0 +1,238 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require rpc'; +'require ui'; + +var callOpenClawStatus = rpc.declare({ + object: 'luci.openclaw', + method: 'status', + expect: { } +}); + +var callOpenClawChat = rpc.declare({ + object: 'luci.openclaw', + method: 'chat', + params: ['message'], + expect: { } +}); + +var callOpenClawHistory = rpc.declare({ + object: 'luci.openclaw', + method: 'get_history', + expect: { } +}); + +var callOpenClawClearHistory = rpc.declare({ + object: 'luci.openclaw', + method: 'clear_history', + expect: { } +}); + +return view.extend({ + chatContainer: null, + messageInput: null, + + load: function() { + return Promise.all([ + callOpenClawStatus(), + callOpenClawHistory() + ]); + }, + + addMessage: function(content, isUser) { + var msgDiv = E('div', { + 'class': 'openclaw-message ' + (isUser ? 'user-message' : 'ai-message') + }); + + if (isUser) { + msgDiv.appendChild(E('div', { 'class': 'message-avatar user-avatar' }, 'You')); + } else { + msgDiv.appendChild(E('div', { 'class': 'message-avatar ai-avatar' }, 'AI')); + } + + var contentDiv = E('div', { 'class': 'message-content' }); + contentDiv.innerHTML = this.formatMessage(content); + msgDiv.appendChild(contentDiv); + + this.chatContainer.appendChild(msgDiv); + this.chatContainer.scrollTop = this.chatContainer.scrollHeight; + }, + + formatMessage: function(text) { + // Basic markdown-like formatting + text = text.replace(/```([\s\S]*?)```/g, '
$1
'); + text = text.replace(/`([^`]+)`/g, '$1'); + text = text.replace(/\*\*([^*]+)\*\*/g, '$1'); + text = text.replace(/\n/g, '
'); + return text; + }, + + sendMessage: function() { + var self = this; + var message = this.messageInput.value.trim(); + + if (!message) + return; + + this.messageInput.value = ''; + this.addMessage(message, true); + + // Show typing indicator + var typingDiv = E('div', { 'class': 'openclaw-typing' }, 'AI is thinking...'); + this.chatContainer.appendChild(typingDiv); + + callOpenClawChat(message).then(function(response) { + typingDiv.remove(); + + if (response.error) { + self.addMessage('Error: ' + response.error, false); + return; + } + + // Parse response based on provider + var aiResponse = ''; + if (response.content && response.content[0]) { + // Anthropic format + aiResponse = response.content[0].text || ''; + } else if (response.choices && response.choices[0]) { + // OpenAI format + aiResponse = response.choices[0].message.content || ''; + } else if (response.message && response.message.content) { + // Ollama format + aiResponse = response.message.content || ''; + } else { + aiResponse = JSON.stringify(response); + } + + self.addMessage(aiResponse, false); + }).catch(function(err) { + typingDiv.remove(); + self.addMessage('Error: ' + err.message, false); + }); + }, + + render: function(data) { + var self = this; + var status = data[0] || {}; + var history = data[1] || {}; + + var styleEl = E('style', {}, [ + '.openclaw-container { max-width: 900px; margin: 0 auto; }', + '.openclaw-status { padding: 10px; border-radius: 8px; margin-bottom: 15px; }', + '.openclaw-status.online { background: #d4edda; border: 1px solid #28a745; }', + '.openclaw-status.offline { background: #f8d7da; border: 1px solid #dc3545; }', + '.openclaw-chat { border: 1px solid #ddd; border-radius: 8px; height: 500px; overflow-y: auto; padding: 15px; background: #fafafa; margin-bottom: 15px; }', + '.openclaw-message { display: flex; margin-bottom: 15px; }', + '.user-message { flex-direction: row-reverse; }', + '.message-avatar { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 12px; flex-shrink: 0; }', + '.user-avatar { background: #007bff; color: white; margin-left: 10px; }', + '.ai-avatar { background: #6c757d; color: white; margin-right: 10px; }', + '.message-content { max-width: 70%; padding: 12px 16px; border-radius: 18px; line-height: 1.5; }', + '.user-message .message-content { background: #007bff; color: white; border-bottom-right-radius: 4px; }', + '.ai-message .message-content { background: #e9ecef; color: #333; border-bottom-left-radius: 4px; }', + '.message-content pre { background: #2d2d2d; color: #f8f8f2; padding: 10px; border-radius: 4px; overflow-x: auto; margin: 8px 0; }', + '.message-content code { background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px; font-family: monospace; }', + '.openclaw-input { display: flex; gap: 10px; }', + '.openclaw-input textarea { flex: 1; min-height: 60px; padding: 12px; border: 1px solid #ddd; border-radius: 8px; resize: vertical; font-size: 14px; }', + '.openclaw-input button { padding: 12px 24px; background: #007bff; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; }', + '.openclaw-input button:hover { background: #0056b3; }', + '.openclaw-typing { padding: 12px 16px; background: #e9ecef; border-radius: 18px; color: #666; font-style: italic; display: inline-block; margin-left: 50px; }', + '.openclaw-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }', + '.openclaw-header h2 { margin: 0; }', + '.btn-clear { background: #dc3545; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; }', + '.btn-clear:hover { background: #c82333; }' + ]); + + var statusClass = (status.running === '1') ? 'online' : 'offline'; + var statusText = (status.running === '1') ? 'Connected' : 'Offline'; + + this.chatContainer = E('div', { 'class': 'openclaw-chat' }); + this.messageInput = E('textarea', { + 'placeholder': 'Type your message here...', + 'id': 'openclaw-message-input' + }); + + // Add keypress handler for Enter key + this.messageInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + self.sendMessage(); + } + }); + + var sendButton = E('button', { + 'click': function() { self.sendMessage(); } + }, 'Send'); + + var clearButton = E('button', { + 'class': 'btn-clear', + 'click': function() { + ui.showModal('Clear History', [ + E('p', {}, 'Are you sure you want to clear all chat history?'), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, 'Cancel'), + ' ', + E('button', { + 'class': 'btn cbi-button-negative', + 'click': function() { + callOpenClawClearHistory().then(function() { + self.chatContainer.innerHTML = ''; + ui.hideModal(); + }); + } + }, 'Clear') + ]) + ]); + } + }, 'Clear History'); + + // Load existing history + if (history.history && history.history.length > 0) { + history.history.forEach(function(item) { + if (item.user) { + self.addMessage(item.user, true); + } + if (item.response) { + var aiResponse = ''; + if (item.response.content && item.response.content[0]) { + aiResponse = item.response.content[0].text || ''; + } else if (item.response.choices && item.response.choices[0]) { + aiResponse = item.response.choices[0].message.content || ''; + } else if (item.response.message) { + aiResponse = item.response.message.content || ''; + } + if (aiResponse) { + self.addMessage(aiResponse, false); + } + } + }); + } + + return E('div', { 'class': 'openclaw-container' }, [ + styleEl, + E('div', { 'class': 'openclaw-header' }, [ + E('h2', {}, 'OpenClaw AI Chat'), + clearButton + ]), + E('div', { 'class': 'openclaw-status ' + statusClass }, [ + E('strong', {}, 'Status: '), + statusText, + (status.running === '1') ? ' (Port ' + (status.port || '3333') + ')' : '' + ]), + this.chatContainer, + E('div', { 'class': 'openclaw-input' }, [ + this.messageInput, + sendButton + ]) + ]); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-openclaw/htdocs/luci-static/resources/view/openclaw/integrations.js b/package/secubox/luci-app-openclaw/htdocs/luci-static/resources/view/openclaw/integrations.js new file mode 100644 index 00000000..b3e4d930 --- /dev/null +++ b/package/secubox/luci-app-openclaw/htdocs/luci-static/resources/view/openclaw/integrations.js @@ -0,0 +1,135 @@ +'use strict'; +'require view'; +'require form'; +'require uci'; + +return view.extend({ + load: function() { + return uci.load('openclaw'); + }, + + render: function() { + var m, s, o; + + m = new form.Map('openclaw', 'OpenClaw Integrations', + 'Configure external integrations for your AI assistant. ' + + 'These integrations allow OpenClaw to interact with messaging platforms, email, and calendars.'); + + // Telegram Integration + s = m.section(form.TypedSection, 'integration', 'Telegram Bot'); + s.anonymous = true; + s.filter = function(section_id) { + return section_id === 'telegram'; + }; + + o = s.option(form.Flag, 'enabled', 'Enable'); + o.rmempty = false; + o.default = '0'; + + o = s.option(form.Value, 'bot_token', 'Bot Token'); + o.password = true; + o.placeholder = 'Enter Telegram bot token from @BotFather'; + o.depends('enabled', '1'); + + // Discord Integration + s = m.section(form.TypedSection, 'integration', 'Discord Bot'); + s.anonymous = true; + s.filter = function(section_id) { + return section_id === 'discord'; + }; + + o = s.option(form.Flag, 'enabled', 'Enable'); + o.rmempty = false; + o.default = '0'; + + o = s.option(form.Value, 'bot_token', 'Bot Token'); + o.password = true; + o.placeholder = 'Enter Discord bot token'; + o.depends('enabled', '1'); + + // Slack Integration + s = m.section(form.TypedSection, 'integration', 'Slack Bot'); + s.anonymous = true; + s.filter = function(section_id) { + return section_id === 'slack'; + }; + + o = s.option(form.Flag, 'enabled', 'Enable'); + o.rmempty = false; + o.default = '0'; + + o = s.option(form.Value, 'bot_token', 'Bot Token'); + o.password = true; + o.placeholder = 'xoxb-...'; + o.depends('enabled', '1'); + + o = s.option(form.Value, 'app_token', 'App Token'); + o.password = true; + o.placeholder = 'xapp-...'; + o.depends('enabled', '1'); + + // Email Integration + s = m.section(form.TypedSection, 'integration', 'Email'); + s.anonymous = true; + s.filter = function(section_id) { + return section_id === 'email'; + }; + + o = s.option(form.Flag, 'enabled', 'Enable'); + o.rmempty = false; + o.default = '0'; + + o = s.option(form.Value, 'imap_host', 'IMAP Server'); + o.placeholder = 'imap.example.com'; + o.depends('enabled', '1'); + + o = s.option(form.Value, 'imap_port', 'IMAP Port'); + o.placeholder = '993'; + o.datatype = 'port'; + o.default = '993'; + o.depends('enabled', '1'); + + o = s.option(form.Value, 'smtp_host', 'SMTP Server'); + o.placeholder = 'smtp.example.com'; + o.depends('enabled', '1'); + + o = s.option(form.Value, 'smtp_port', 'SMTP Port'); + o.placeholder = '587'; + o.datatype = 'port'; + o.default = '587'; + o.depends('enabled', '1'); + + o = s.option(form.Value, 'email', 'Email Address'); + o.placeholder = 'your@email.com'; + o.datatype = 'email'; + o.depends('enabled', '1'); + + o = s.option(form.Value, 'password', 'Password'); + o.password = true; + o.depends('enabled', '1'); + + // Calendar Integration + s = m.section(form.TypedSection, 'integration', 'Calendar (CalDAV)'); + s.anonymous = true; + s.filter = function(section_id) { + return section_id === 'calendar'; + }; + + o = s.option(form.Flag, 'enabled', 'Enable'); + o.rmempty = false; + o.default = '0'; + + o = s.option(form.Value, 'caldav_url', 'CalDAV URL'); + o.placeholder = 'https://calendar.example.com/caldav'; + o.depends('enabled', '1'); + + o = s.option(form.Value, 'username', 'Username'); + o.depends('enabled', '1'); + + o = s.option(form.Value, 'password', 'Password'); + o.password = true; + o.depends('enabled', '1'); + + return m.render(); + } +}); diff --git a/package/secubox/luci-app-openclaw/htdocs/luci-static/resources/view/openclaw/settings.js b/package/secubox/luci-app-openclaw/htdocs/luci-static/resources/view/openclaw/settings.js new file mode 100644 index 00000000..54a9bd12 --- /dev/null +++ b/package/secubox/luci-app-openclaw/htdocs/luci-static/resources/view/openclaw/settings.js @@ -0,0 +1,196 @@ +'use strict'; +'require view'; +'require dom'; +'require form'; +'require rpc'; +'require ui'; + +var callOpenClawConfig = rpc.declare({ + object: 'luci.openclaw', + method: 'get_config', + expect: { } +}); + +var callOpenClawSetConfig = rpc.declare({ + object: 'luci.openclaw', + method: 'set_config', + params: ['provider', 'model', 'api_key', 'ollama_url'], + expect: { } +}); + +var callOpenClawModels = rpc.declare({ + object: 'luci.openclaw', + method: 'list_models', + expect: { } +}); + +var callOpenClawTestApi = rpc.declare({ + object: 'luci.openclaw', + method: 'test_api', + expect: { } +}); + +return view.extend({ + load: function() { + return Promise.all([ + callOpenClawConfig(), + callOpenClawModels() + ]); + }, + + render: function(data) { + var config = data[0] || {}; + var modelsData = data[1] || {}; + var models = modelsData.models || {}; + + var styleEl = E('style', {}, [ + '.openclaw-settings { max-width: 700px; }', + '.setting-group { background: #f9f9f9; border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 20px; }', + '.setting-group h3 { margin-top: 0; border-bottom: 1px solid #ddd; padding-bottom: 10px; }', + '.setting-row { margin-bottom: 15px; }', + '.setting-row label { display: block; font-weight: bold; margin-bottom: 5px; }', + '.setting-row input, .setting-row select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }', + '.setting-row .hint { font-size: 12px; color: #666; margin-top: 5px; }', + '.btn-group { display: flex; gap: 10px; margin-top: 20px; }', + '.btn { padding: 12px 24px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }', + '.btn-primary { background: #007bff; color: white; }', + '.btn-primary:hover { background: #0056b3; }', + '.btn-success { background: #28a745; color: white; }', + '.btn-success:hover { background: #1e7e34; }', + '.api-status { padding: 10px; border-radius: 4px; margin-top: 10px; }', + '.api-status.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }', + '.api-status.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }', + '.provider-info { background: #e7f3ff; border: 1px solid #b6d4fe; padding: 10px; border-radius: 4px; margin-bottom: 15px; }', + '.provider-info a { color: #0056b3; }' + ]); + + var providerSelect = E('select', { 'id': 'provider-select' }, [ + E('option', { 'value': 'anthropic', 'selected': config.provider === 'anthropic' }, 'Anthropic (Claude)'), + E('option', { 'value': 'openai', 'selected': config.provider === 'openai' }, 'OpenAI (GPT)'), + E('option', { 'value': 'ollama', 'selected': config.provider === 'ollama' }, 'Ollama (Local)') + ]); + + var modelSelect = E('select', { 'id': 'model-select' }); + var updateModels = function() { + var provider = providerSelect.value; + var providerModels = models[provider] || []; + modelSelect.innerHTML = ''; + providerModels.forEach(function(m) { + var opt = E('option', { 'value': m }, m); + if (m === config.model) opt.selected = true; + modelSelect.appendChild(opt); + }); + }; + providerSelect.addEventListener('change', updateModels); + updateModels(); + + var apiKeyInput = E('input', { + 'type': 'password', + 'id': 'api-key-input', + 'placeholder': config.api_key_set === '1' ? '(Key is set: ' + config.api_key_masked + ')' : 'Enter your API key...' + }); + + var ollamaUrlInput = E('input', { + 'type': 'text', + 'id': 'ollama-url-input', + 'value': config.ollama_url || 'http://127.0.0.1:11434', + 'placeholder': 'http://127.0.0.1:11434' + }); + + var apiStatusDiv = E('div', { 'id': 'api-status', 'style': 'display:none;' }); + + var testButton = E('button', { + 'class': 'btn btn-success', + 'click': function() { + apiStatusDiv.style.display = 'none'; + apiStatusDiv.className = 'api-status'; + apiStatusDiv.textContent = 'Testing...'; + apiStatusDiv.style.display = 'block'; + + callOpenClawTestApi().then(function(result) { + if (result.success === '1') { + apiStatusDiv.className = 'api-status success'; + apiStatusDiv.textContent = 'Connection successful! Provider: ' + result.provider + ', Model: ' + result.model; + } else { + apiStatusDiv.className = 'api-status error'; + apiStatusDiv.textContent = 'Connection failed: ' + (result.error || 'Unknown error'); + } + }).catch(function(err) { + apiStatusDiv.className = 'api-status error'; + apiStatusDiv.textContent = 'Error: ' + err.message; + }); + } + }, 'Test Connection'); + + var saveButton = E('button', { + 'class': 'btn btn-primary', + 'click': function() { + var provider = providerSelect.value; + var model = modelSelect.value; + var apiKey = apiKeyInput.value || null; + var ollamaUrl = ollamaUrlInput.value || null; + + callOpenClawSetConfig(provider, model, apiKey, ollamaUrl).then(function(result) { + if (result.status === 'ok') { + ui.addNotification(null, E('p', {}, 'Settings saved successfully!'), 'success'); + } else { + ui.addNotification(null, E('p', {}, 'Failed to save settings'), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'error'); + }); + } + }, 'Save Settings'); + + return E('div', { 'class': 'openclaw-settings' }, [ + styleEl, + E('h2', {}, 'OpenClaw Settings'), + + E('div', { 'class': 'setting-group' }, [ + E('h3', {}, 'AI Provider'), + E('div', { 'class': 'provider-info' }, [ + E('strong', {}, 'Get your API keys:'), + E('br'), + E('a', { 'href': 'https://console.anthropic.com/', 'target': '_blank' }, 'Anthropic Console'), + ' | ', + E('a', { 'href': 'https://platform.openai.com/', 'target': '_blank' }, 'OpenAI Platform'), + ' | ', + E('a', { 'href': 'https://ollama.ai/', 'target': '_blank' }, 'Ollama (Free/Local)') + ]), + E('div', { 'class': 'setting-row' }, [ + E('label', { 'for': 'provider-select' }, 'Provider'), + providerSelect + ]), + E('div', { 'class': 'setting-row' }, [ + E('label', { 'for': 'model-select' }, 'Model'), + modelSelect + ]) + ]), + + E('div', { 'class': 'setting-group' }, [ + E('h3', {}, 'Authentication'), + E('div', { 'class': 'setting-row' }, [ + E('label', { 'for': 'api-key-input' }, 'API Key'), + apiKeyInput, + E('div', { 'class': 'hint' }, 'Required for Anthropic and OpenAI. Leave empty for Ollama.') + ]), + E('div', { 'class': 'setting-row' }, [ + E('label', { 'for': 'ollama-url-input' }, 'Ollama URL'), + ollamaUrlInput, + E('div', { 'class': 'hint' }, 'Only used when provider is set to Ollama.') + ]) + ]), + + apiStatusDiv, + + E('div', { 'class': 'btn-group' }, [ + saveButton, + testButton + ]) + ]); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-openclaw/root/usr/libexec/rpcd/luci.openclaw b/package/secubox/luci-app-openclaw/root/usr/libexec/rpcd/luci.openclaw new file mode 100644 index 00000000..1dc3c4c8 --- /dev/null +++ b/package/secubox/luci-app-openclaw/root/usr/libexec/rpcd/luci.openclaw @@ -0,0 +1,361 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +# RPCD backend for OpenClaw AI Assistant LuCI app + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +OPENCLAW_CONFIG="openclaw" +OPENCLAW_DATA="/srv/openclaw" +HISTORY_FILE="$OPENCLAW_DATA/chat_history.json" + +log_msg() { + logger -t "luci.openclaw" "$1" +} + +# Get service status +get_status() { + local enabled port running pid + + enabled=$(uci -q get $OPENCLAW_CONFIG.main.enabled) + port=$(uci -q get $OPENCLAW_CONFIG.main.port) + + # Check if service is running + if pgrep -f "openclaw" >/dev/null 2>&1; then + running="1" + pid=$(pgrep -f "openclaw" | head -1) + else + running="0" + pid="" + fi + + json_init + json_add_string "enabled" "${enabled:-0}" + json_add_string "running" "$running" + json_add_string "pid" "$pid" + json_add_string "port" "${port:-3333}" + json_dump +} + +# Get current configuration +get_config() { + local provider model api_key ollama_url + + provider=$(uci -q get $OPENCLAW_CONFIG.llm.type) + model=$(uci -q get $OPENCLAW_CONFIG.llm.model) + api_key=$(uci -q get $OPENCLAW_CONFIG.llm.api_key) + ollama_url=$(uci -q get $OPENCLAW_CONFIG.llm.ollama_url) + + # Mask API key for security + local masked_key="" + if [ -n "$api_key" ]; then + local key_len=${#api_key} + if [ $key_len -gt 8 ]; then + masked_key="${api_key:0:4}****${api_key: -4}" + else + masked_key="****" + fi + fi + + json_init + json_add_string "provider" "${provider:-anthropic}" + json_add_string "model" "${model:-claude-sonnet-4-20250514}" + json_add_string "api_key_set" "$([ -n "$api_key" ] && echo '1' || echo '0')" + json_add_string "api_key_masked" "$masked_key" + json_add_string "ollama_url" "${ollama_url:-http://127.0.0.1:11434}" + json_dump +} + +# Set configuration +set_config() { + local provider="$1" model="$2" api_key="$3" ollama_url="$4" + + [ -n "$provider" ] && uci set $OPENCLAW_CONFIG.llm.type="$provider" + [ -n "$model" ] && uci set $OPENCLAW_CONFIG.llm.model="$model" + [ -n "$api_key" ] && uci set $OPENCLAW_CONFIG.llm.api_key="$api_key" + [ -n "$ollama_url" ] && uci set $OPENCLAW_CONFIG.llm.ollama_url="$ollama_url" + + uci commit $OPENCLAW_CONFIG + + json_init + json_add_string "status" "ok" + json_dump +} + +# List available models per provider +list_models() { + json_init + json_add_object "models" + + json_add_array "anthropic" + json_add_string "" "claude-opus-4-20250514" + json_add_string "" "claude-sonnet-4-20250514" + json_add_string "" "claude-3-5-haiku-20241022" + json_close_array + + json_add_array "openai" + json_add_string "" "gpt-4o" + json_add_string "" "gpt-4-turbo" + json_add_string "" "gpt-4" + json_add_string "" "gpt-3.5-turbo" + json_close_array + + json_add_array "ollama" + json_add_string "" "mistral" + json_add_string "" "llama2" + json_add_string "" "llama3" + json_add_string "" "tinyllama" + json_add_string "" "codellama" + json_close_array + + json_close_object + json_dump +} + +# Chat with AI +do_chat() { + local message="$1" + local provider model api_key response + + provider=$(uci -q get $OPENCLAW_CONFIG.llm.type) + model=$(uci -q get $OPENCLAW_CONFIG.llm.model) + api_key=$(uci -q get $OPENCLAW_CONFIG.llm.api_key) + + if [ -z "$api_key" ] && [ "$provider" != "ollama" ]; then + json_init + json_add_string "error" "API key not configured" + json_dump + return + fi + + # Ensure data directory exists + mkdir -p "$OPENCLAW_DATA" + + case "$provider" in + anthropic) + response=$(wget -q -O - \ + --header="Content-Type: application/json" \ + --header="x-api-key: $api_key" \ + --header="anthropic-version: 2023-06-01" \ + --post-data="{\"model\":\"$model\",\"max_tokens\":4096,\"messages\":[{\"role\":\"user\",\"content\":\"$message\"}]}" \ + "https://api.anthropic.com/v1/messages" 2>/dev/null) + ;; + openai) + response=$(wget -q -O - \ + --header="Content-Type: application/json" \ + --header="Authorization: Bearer $api_key" \ + --post-data="{\"model\":\"$model\",\"messages\":[{\"role\":\"user\",\"content\":\"$message\"}]}" \ + "https://api.openai.com/v1/chat/completions" 2>/dev/null) + ;; + ollama) + local ollama_url=$(uci -q get $OPENCLAW_CONFIG.llm.ollama_url) + ollama_url="${ollama_url:-http://127.0.0.1:11434}" + response=$(wget -q -O - \ + --header="Content-Type: application/json" \ + --post-data="{\"model\":\"$model\",\"messages\":[{\"role\":\"user\",\"content\":\"$message\"}],\"stream\":false}" \ + "${ollama_url}/api/chat" 2>/dev/null) + ;; + esac + + if [ -n "$response" ]; then + # Save to history + local timestamp=$(date +%s) + echo "{\"ts\":$timestamp,\"user\":\"$message\",\"response\":$response}" >> "$HISTORY_FILE" + + # Return response + echo "$response" + else + json_init + json_add_string "error" "Failed to get response from AI provider" + json_dump + fi +} + +# Test API connection +test_api() { + local provider model api_key + + provider=$(uci -q get $OPENCLAW_CONFIG.llm.type) + model=$(uci -q get $OPENCLAW_CONFIG.llm.model) + api_key=$(uci -q get $OPENCLAW_CONFIG.llm.api_key) + + if [ -z "$api_key" ] && [ "$provider" != "ollama" ]; then + json_init + json_add_string "success" "0" + json_add_string "error" "API key not configured" + json_dump + return + fi + + local test_result + case "$provider" in + anthropic) + test_result=$(wget -q -O - \ + --header="Content-Type: application/json" \ + --header="x-api-key: $api_key" \ + --header="anthropic-version: 2023-06-01" \ + --post-data="{\"model\":\"$model\",\"max_tokens\":10,\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}]}" \ + "https://api.anthropic.com/v1/messages" 2>&1) + ;; + openai) + test_result=$(wget -q -O - \ + --header="Content-Type: application/json" \ + --header="Authorization: Bearer $api_key" \ + --post-data="{\"model\":\"$model\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":10}" \ + "https://api.openai.com/v1/chat/completions" 2>&1) + ;; + ollama) + local ollama_url=$(uci -q get $OPENCLAW_CONFIG.llm.ollama_url) + ollama_url="${ollama_url:-http://127.0.0.1:11434}" + test_result=$(wget -q -O - "${ollama_url}/api/tags" 2>&1) + ;; + esac + + if echo "$test_result" | grep -qE '(error|Error|ERROR)'; then + json_init + json_add_string "success" "0" + json_add_string "error" "API test failed" + json_dump + else + json_init + json_add_string "success" "1" + json_add_string "provider" "$provider" + json_add_string "model" "$model" + json_dump + fi +} + +# Get chat history +get_history() { + if [ -f "$HISTORY_FILE" ]; then + # Return last 50 entries + tail -50 "$HISTORY_FILE" | { + echo '{"history":[' + local first=1 + while read line; do + [ $first -eq 0 ] && echo "," + echo "$line" + first=0 + done + echo ']}' + } + else + echo '{"history":[]}' + fi +} + +# Clear chat history +clear_history() { + rm -f "$HISTORY_FILE" + json_init + json_add_string "status" "ok" + json_dump +} + +# Install/setup OpenClaw +do_install() { + mkdir -p "$OPENCLAW_DATA" + + # Enable and start service + uci set $OPENCLAW_CONFIG.main.enabled='1' + uci commit $OPENCLAW_CONFIG + + /etc/init.d/openclaw enable 2>/dev/null + /etc/init.d/openclaw start 2>/dev/null + + json_init + json_add_string "status" "ok" + json_dump +} + +# Update OpenClaw +do_update() { + # Restart service + /etc/init.d/openclaw restart 2>/dev/null + + json_init + json_add_string "status" "ok" + json_dump +} + +# Main RPC handler +case "$1" in + list) + json_init + json_add_object "status" + json_close_object + json_add_object "get_config" + json_close_object + json_add_object "set_config" + json_add_string "provider" "string" + json_add_string "model" "string" + json_add_string "api_key" "string" + json_add_string "ollama_url" "string" + json_close_object + json_add_object "list_models" + json_close_object + json_add_object "chat" + json_add_string "message" "string" + json_close_object + json_add_object "test_api" + json_close_object + json_add_object "get_history" + json_close_object + json_add_object "clear_history" + json_close_object + json_add_object "install" + json_close_object + json_add_object "update" + json_close_object + json_dump + ;; + call) + case "$2" in + status) + get_status + ;; + get_config) + get_config + ;; + set_config) + read input + json_load "$input" + json_get_var provider provider + json_get_var model model + json_get_var api_key api_key + json_get_var ollama_url ollama_url + set_config "$provider" "$model" "$api_key" "$ollama_url" + ;; + list_models) + list_models + ;; + chat) + read input + json_load "$input" + json_get_var message message + do_chat "$message" + ;; + test_api) + test_api + ;; + get_history) + get_history + ;; + clear_history) + clear_history + ;; + install) + do_install + ;; + update) + do_update + ;; + *) + echo '{"error":"Unknown method"}' + ;; + esac + ;; + *) + echo '{"error":"Invalid action"}' + ;; +esac diff --git a/package/secubox/luci-app-openclaw/root/usr/share/luci/menu.d/luci-app-openclaw.json b/package/secubox/luci-app-openclaw/root/usr/share/luci/menu.d/luci-app-openclaw.json new file mode 100644 index 00000000..a7c32017 --- /dev/null +++ b/package/secubox/luci-app-openclaw/root/usr/share/luci/menu.d/luci-app-openclaw.json @@ -0,0 +1,36 @@ +{ + "admin/services/openclaw": { + "title": "OpenClaw AI", + "order": 75, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": ["luci-app-openclaw"] + } + }, + "admin/services/openclaw/chat": { + "title": "AI Chat", + "order": 10, + "action": { + "type": "view", + "path": "openclaw/chat" + } + }, + "admin/services/openclaw/settings": { + "title": "Settings", + "order": 20, + "action": { + "type": "view", + "path": "openclaw/settings" + } + }, + "admin/services/openclaw/integrations": { + "title": "Integrations", + "order": 30, + "action": { + "type": "view", + "path": "openclaw/integrations" + } + } +} diff --git a/package/secubox/luci-app-openclaw/root/usr/share/rpcd/acl.d/luci-app-openclaw.json b/package/secubox/luci-app-openclaw/root/usr/share/rpcd/acl.d/luci-app-openclaw.json new file mode 100644 index 00000000..92c5ad67 --- /dev/null +++ b/package/secubox/luci-app-openclaw/root/usr/share/rpcd/acl.d/luci-app-openclaw.json @@ -0,0 +1,17 @@ +{ + "luci-app-openclaw": { + "description": "OpenClaw AI Assistant", + "read": { + "ubus": { + "luci.openclaw": ["status", "get_config", "list_models", "get_history"] + }, + "uci": ["openclaw"] + }, + "write": { + "ubus": { + "luci.openclaw": ["chat", "set_config", "install", "update", "test_api", "clear_history"] + }, + "uci": ["openclaw"] + } + } +}