feat(openclaw): Add LuCI web interface for OpenClaw AI assistant

Complete LuCI app with:
- Chat view with real-time AI conversation
- Settings view for provider/model/API key configuration
- Integrations view for Telegram/Discord/Slack/Email/Calendar
- RPCD backend handling all ubus calls
- Support for Anthropic, OpenAI, and Ollama providers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-27 09:47:35 +01:00
parent a8dc5f58fe
commit 56f5d8c61f
7 changed files with 1018 additions and 0 deletions

View File

@ -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 <contact@cybermind.fr>
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))

View File

@ -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, '<pre><code>$1</code></pre>');
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
text = text.replace(/\n/g, '<br>');
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
});

View File

@ -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();
}
});

View File

@ -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
});

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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"]
}
}
}