Add complete French (fr) and Chinese (zh) translations for all documentation: - Root files: README, CHANGELOG, SECURITY, BETA-RELEASE - docs/: All 16 core documentation files - DOCS/: All 19 deep-dive documents including embedded/ and archive/ - package/secubox/: All 123+ package READMEs - Misc: secubox-tools/, scripts/, EXAMPLES/, config-backups/, streamlit-apps/ Total: 346 translation files created Each file includes language switcher links for easy navigation between English, French, and Chinese versions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1194 lines
29 KiB
Markdown
1194 lines
29 KiB
Markdown
# LuCI 开发参考指南
|
||
|
||
> **语言:** [English](../docs/luci-development-reference.md) | [Francais](../docs-fr/luci-development-reference.md) | 中文
|
||
|
||
**版本:** 1.0.0
|
||
**最后更新:** 2025-12-28
|
||
**状态:** 活跃
|
||
**基于:** luci-app-secubox 和 luci-app-system-hub 实现
|
||
**目标受众:** Claude.ai 和从事 OpenWrt LuCI 应用开发的开发人员
|
||
|
||
---
|
||
|
||
## 另请参阅
|
||
|
||
- **设计与标准:** [DEVELOPMENT-GUIDELINES.md](development-guidelines.md)
|
||
- **快速命令:** [QUICK-START.md](quick-start.md)
|
||
- **自动化简报:** [CODEX.md](codex.md)
|
||
- **代码模板:** [CODE-TEMPLATES.md](code-templates.md)
|
||
|
||
本文档记录了在开发 SecuBox LuCI 应用程序过程中发现的关键模式、最佳实践和常见陷阱。请将此作为所有未来 LuCI 应用开发的验证参考。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [ubus 和 RPC 基础](#ubus-和-rpc-基础)
|
||
2. [RPCD 后端模式](#rpcd-后端模式)
|
||
3. [LuCI API 模块模式](#luci-api-模块模式)
|
||
4. [LuCI 视图导入模式](#luci-视图导入模式)
|
||
5. [ACL 权限结构](#acl-权限结构)
|
||
6. [数据结构约定](#数据结构约定)
|
||
7. [常见错误和解决方案](#常见错误和解决方案)
|
||
8. [验证清单](#验证清单)
|
||
9. [测试和部署](#测试和部署)
|
||
|
||
---
|
||
|
||
## ubus 和 RPC 基础
|
||
|
||
### 什么是 ubus?
|
||
|
||
**ubus**(OpenWrt 微总线架构)是 OpenWrt 的进程间通信(IPC)系统。它支持:
|
||
- 进程间的 RPC(远程过程调用)
|
||
- Web 界面(LuCI)与后端服务的通信
|
||
- 通过 `ubus call` 进行命令行交互
|
||
|
||
### ubus 对象命名约定
|
||
|
||
**关键规则**:所有 LuCI 应用程序的 ubus 对象必须使用 `luci.` 前缀。
|
||
|
||
```javascript
|
||
// ✅ 正确
|
||
object: 'luci.system-hub'
|
||
object: 'luci.cdn-cache'
|
||
object: 'luci.wireguard-dashboard'
|
||
|
||
// ❌ 错误
|
||
object: 'system-hub'
|
||
object: 'systemhub'
|
||
object: 'cdn-cache'
|
||
```
|
||
|
||
**为什么?** LuCI 期望 Web 应用程序的对象在 `luci.*` 命名空间下。如果没有此前缀:
|
||
- ACL 权限将不匹配
|
||
- RPCD 将无法正确路由调用
|
||
- 浏览器控制台显示:`RPC call to system-hub/status failed with error -32000: Object not found`
|
||
|
||
### RPCD 脚本名称必须与 ubus 对象匹配
|
||
|
||
RPCD 脚本文件名必须与 ubus 对象名称完全匹配:
|
||
|
||
```bash
|
||
# 如果 JavaScript 声明:
|
||
# object: 'luci.system-hub'
|
||
|
||
# 那么 RPCD 脚本必须命名为:
|
||
/usr/libexec/rpcd/luci.system-hub
|
||
|
||
# 而不是:
|
||
/usr/libexec/rpcd/system-hub
|
||
/usr/libexec/rpcd/luci-system-hub
|
||
```
|
||
|
||
**验证命令**:
|
||
```bash
|
||
# 检查 JavaScript 文件中的 ubus 对象名称
|
||
grep -r "object:" luci-app-*/htdocs --include="*.js"
|
||
|
||
# 验证 RPCD 脚本是否存在且名称匹配
|
||
ls luci-app-*/root/usr/libexec/rpcd/
|
||
```
|
||
|
||
### ubus 调用类型
|
||
|
||
**读操作**(类似 GET):
|
||
- `status` - 获取当前状态
|
||
- `get_*` - 检索数据(例如 `get_health`、`get_settings`)
|
||
- `list_*` - 枚举项目(例如 `list_services`)
|
||
|
||
**写操作**(类似 POST):
|
||
- `save_*` - 持久化配置(例如 `save_settings`)
|
||
- `*_action` - 执行操作(例如 `service_action`)
|
||
- `backup`、`restore`、`reboot` - 系统修改
|
||
|
||
**ACL 映射**:
|
||
- 读操作 → ACL 中的 `"read"` 部分
|
||
- 写操作 → ACL 中的 `"write"` 部分
|
||
|
||
---
|
||
|
||
## RPCD 后端模式
|
||
|
||
### Shell 脚本结构
|
||
|
||
RPCD 后端是可执行的 shell 脚本,它们:
|
||
1. 解析 `$1` 获取操作(`list` 或 `call`)
|
||
2. 解析 `$2` 获取方法名称(如果是 `call`)
|
||
3. 从 stdin 读取 JSON 输入(用于带参数的方法)
|
||
4. 将 JSON 输出到 stdout
|
||
5. 成功时退出状态为 0,失败时为非零
|
||
|
||
### 标准模板
|
||
|
||
```bash
|
||
#!/bin/sh
|
||
# RPCD 后端:luci.system-hub
|
||
# 版本:0.1.0
|
||
|
||
# 加载 JSON shell 助手
|
||
. /usr/share/libubox/jshn.sh
|
||
|
||
case "$1" in
|
||
list)
|
||
# 列出所有可用方法及其参数
|
||
echo '{
|
||
"status": {},
|
||
"get_health": {},
|
||
"service_action": { "service": "string", "action": "string" },
|
||
"save_settings": {
|
||
"auto_refresh": 0,
|
||
"health_check": 0,
|
||
"refresh_interval": 0
|
||
}
|
||
}'
|
||
;;
|
||
call)
|
||
case "$2" in
|
||
status)
|
||
status
|
||
;;
|
||
get_health)
|
||
get_health
|
||
;;
|
||
service_action)
|
||
# 从 stdin 读取 JSON 输入
|
||
read -r input
|
||
json_load "$input"
|
||
json_get_var service service
|
||
json_get_var action action
|
||
service_action "$service" "$action"
|
||
;;
|
||
save_settings)
|
||
read -r input
|
||
json_load "$input"
|
||
json_get_var auto_refresh auto_refresh
|
||
json_get_var health_check health_check
|
||
json_get_var refresh_interval refresh_interval
|
||
save_settings "$auto_refresh" "$health_check" "$refresh_interval"
|
||
;;
|
||
*)
|
||
echo '{"error": "Method not found"}'
|
||
exit 1
|
||
;;
|
||
esac
|
||
;;
|
||
esac
|
||
```
|
||
|
||
### 使用 jshn.sh 输出 JSON
|
||
|
||
**jshn.sh** 提供了用于 JSON 操作的 shell 函数:
|
||
|
||
```bash
|
||
# 初始化 JSON 对象
|
||
json_init
|
||
|
||
# 添加简单值
|
||
json_add_string "hostname" "openwrt"
|
||
json_add_int "uptime" 86400
|
||
json_add_boolean "running" 1
|
||
|
||
# 添加嵌套对象
|
||
json_add_object "cpu"
|
||
json_add_int "usage" 25
|
||
json_add_string "status" "ok"
|
||
json_close_object
|
||
|
||
# 添加数组
|
||
json_add_array "services"
|
||
json_add_string "" "network"
|
||
json_add_string "" "firewall"
|
||
json_close_array
|
||
|
||
# 将 JSON 输出到 stdout
|
||
json_dump
|
||
```
|
||
|
||
**常用函数**:
|
||
- `json_init` - 开始新的 JSON 对象
|
||
- `json_add_string "key" "value"` - 添加字符串
|
||
- `json_add_int "key" 123` - 添加整数
|
||
- `json_add_boolean "key" 1` - 添加布尔值(0 或 1)
|
||
- `json_add_object "key"` - 开始嵌套对象
|
||
- `json_close_object` - 结束嵌套对象
|
||
- `json_add_array "key"` - 开始数组
|
||
- `json_close_array` - 结束数组
|
||
- `json_dump` - 将 JSON 输出到 stdout
|
||
|
||
### 错误处理
|
||
|
||
始终验证输入并返回有意义的错误:
|
||
|
||
```bash
|
||
service_action() {
|
||
local service="$1"
|
||
local action="$2"
|
||
|
||
# 验证服务名称
|
||
if [ -z "$service" ]; then
|
||
json_init
|
||
json_add_boolean "success" 0
|
||
json_add_string "error" "Service name is required"
|
||
json_dump
|
||
return 1
|
||
fi
|
||
|
||
# 验证操作
|
||
case "$action" in
|
||
start|stop|restart|enable|disable)
|
||
;;
|
||
*)
|
||
json_init
|
||
json_add_boolean "success" 0
|
||
json_add_string "error" "Invalid action: $action"
|
||
json_dump
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
# 执行操作
|
||
/etc/init.d/"$service" "$action" >/dev/null 2>&1
|
||
|
||
if [ $? -eq 0 ]; then
|
||
json_init
|
||
json_add_boolean "success" 1
|
||
json_add_string "message" "Service $service $action successful"
|
||
json_dump
|
||
else
|
||
json_init
|
||
json_add_boolean "success" 0
|
||
json_add_string "error" "Service $service $action failed"
|
||
json_dump
|
||
return 1
|
||
fi
|
||
}
|
||
```
|
||
|
||
### UCI 集成
|
||
|
||
对于持久化配置,使用 UCI(统一配置接口):
|
||
|
||
```bash
|
||
save_settings() {
|
||
local auto_refresh="$1"
|
||
local health_check="$2"
|
||
local refresh_interval="$3"
|
||
|
||
# 创建/更新 UCI 配置
|
||
uci set system-hub.general=general
|
||
uci set system-hub.general.auto_refresh="$auto_refresh"
|
||
uci set system-hub.general.health_check="$health_check"
|
||
uci set system-hub.general.refresh_interval="$refresh_interval"
|
||
uci commit system-hub
|
||
|
||
json_init
|
||
json_add_boolean "success" 1
|
||
json_add_string "message" "Settings saved successfully"
|
||
json_dump
|
||
}
|
||
|
||
get_settings() {
|
||
# 加载 UCI 配置
|
||
if [ -f "/etc/config/system-hub" ]; then
|
||
. /lib/functions.sh
|
||
config_load system-hub
|
||
fi
|
||
|
||
json_init
|
||
json_add_object "general"
|
||
|
||
# 获取值或使用默认值
|
||
config_get auto_refresh general auto_refresh "1"
|
||
json_add_boolean "auto_refresh" "${auto_refresh:-1}"
|
||
|
||
config_get refresh_interval general refresh_interval "30"
|
||
json_add_int "refresh_interval" "${refresh_interval:-30}"
|
||
|
||
json_close_object
|
||
json_dump
|
||
}
|
||
```
|
||
|
||
### 性能提示
|
||
|
||
1. **缓存昂贵的操作**:不要多次重复读取 `/proc` 文件
|
||
2. **高效使用命令替换**:
|
||
```bash
|
||
# 好
|
||
uptime=$(cat /proc/uptime | cut -d' ' -f1)
|
||
|
||
# 更好
|
||
read uptime _ < /proc/uptime
|
||
uptime=${uptime%.*}
|
||
```
|
||
3. **尽可能避免外部命令**:
|
||
```bash
|
||
# 慢
|
||
count=$(ls /etc/init.d | wc -l)
|
||
|
||
# 快
|
||
count=0
|
||
for file in /etc/init.d/*; do
|
||
[ -f "$file" ] && count=$((count + 1))
|
||
done
|
||
```
|
||
|
||
---
|
||
|
||
## LuCI API 模块模式
|
||
|
||
### 关键:使用 baseclass.extend()
|
||
|
||
**规则**:LuCI API 模块必须使用 `baseclass.extend()` 模式。
|
||
|
||
```javascript
|
||
'use strict';
|
||
'require baseclass';
|
||
'require rpc';
|
||
|
||
// 声明 RPC 方法
|
||
var callStatus = rpc.declare({
|
||
object: 'luci.system-hub',
|
||
method: 'status',
|
||
expect: {}
|
||
});
|
||
|
||
var callGetHealth = rpc.declare({
|
||
object: 'luci.system-hub',
|
||
method: 'get_health',
|
||
expect: {}
|
||
});
|
||
|
||
var callSaveSettings = rpc.declare({
|
||
object: 'luci.system-hub',
|
||
method: 'save_settings',
|
||
params: ['auto_refresh', 'health_check', 'refresh_interval'],
|
||
expect: {}
|
||
});
|
||
|
||
// ✅ 正确:使用 baseclass.extend()
|
||
return baseclass.extend({
|
||
getStatus: callStatus,
|
||
getHealth: callGetHealth,
|
||
saveSettings: callSaveSettings
|
||
});
|
||
|
||
// ❌ 错误:不要使用这些模式
|
||
return baseclass.singleton({...}); // 会破坏一切!
|
||
return {...}; // 普通对象不起作用
|
||
```
|
||
|
||
**为什么使用 baseclass.extend()?**
|
||
- LuCI 的模块系统期望基于类的模块
|
||
- 视图使用 `'require module/api as API'` 导入,会自动实例化
|
||
- `baseclass.extend()` 创建正确的类构造函数
|
||
- `baseclass.singleton()` 会破坏实例化机制
|
||
- 普通对象不支持 LuCI 的模块生命周期
|
||
|
||
### rpc.declare() 参数
|
||
|
||
```javascript
|
||
var callMethodName = rpc.declare({
|
||
object: 'luci.module-name', // ubus 对象名称(必须以 luci. 开头)
|
||
method: 'method_name', // RPCD 方法名称
|
||
params: ['param1', 'param2'], // 可选:参数名称(顺序很重要!)
|
||
expect: {} // 期望的返回结构(或 { key: [] } 用于数组)
|
||
});
|
||
```
|
||
|
||
**参数顺序很重要**:
|
||
```javascript
|
||
// RPCD 期望参数按此确切顺序
|
||
var callSaveSettings = rpc.declare({
|
||
object: 'luci.system-hub',
|
||
method: 'save_settings',
|
||
params: ['auto_refresh', 'health_check', 'debug_mode', 'refresh_interval'],
|
||
expect: {}
|
||
});
|
||
|
||
// JavaScript 调用必须按相同顺序传递参数
|
||
API.saveSettings(1, 1, 0, 30); // auto_refresh=1, health_check=1, debug_mode=0, refresh_interval=30
|
||
```
|
||
|
||
### expect 参数模式
|
||
|
||
```javascript
|
||
// 方法返回单个对象
|
||
expect: {}
|
||
|
||
// 方法在顶层返回数组
|
||
expect: { services: [] }
|
||
|
||
// 方法返回特定结构
|
||
expect: {
|
||
services: [],
|
||
count: 0
|
||
}
|
||
```
|
||
|
||
### API 模块中的错误处理
|
||
|
||
API 方法返回 Promise。在视图中处理错误:
|
||
|
||
```javascript
|
||
return API.getHealth().then(function(data) {
|
||
if (!data || typeof data !== 'object') {
|
||
console.error('Invalid health data:', data);
|
||
return null;
|
||
}
|
||
return data;
|
||
}).catch(function(err) {
|
||
console.error('Failed to load health data:', err);
|
||
ui.addNotification(null, E('p', {}, 'Failed to load health data'), 'error');
|
||
return null;
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## LuCI 视图导入模式
|
||
|
||
### 关键:为 API 使用 'require ... as VAR'
|
||
|
||
**规则**:导入 API 模块时,在文件顶部使用 `'require ... as VAR'` 模式。
|
||
|
||
```javascript
|
||
// ✅ 正确:自动实例化类
|
||
'require system-hub/api as API';
|
||
|
||
return L.view.extend({
|
||
load: function() {
|
||
return API.getHealth(); // API 已经实例化
|
||
}
|
||
});
|
||
|
||
// ❌ 错误:返回类构造函数,而不是实例
|
||
var api = L.require('system-hub.api');
|
||
api.getHealth(); // 错误:api.getHealth is not a function
|
||
```
|
||
|
||
**为什么?**
|
||
- `'require module/path as VAR'`(使用斜杠)自动实例化类
|
||
- `L.require('module.path')`(使用点)返回原始类构造函数
|
||
- API 模块扩展 `baseclass`,需要实例化
|
||
- 使用 `as VAR` 模式时,LuCI 的模块加载器处理实例化
|
||
|
||
### 标准视图结构
|
||
|
||
```javascript
|
||
'use strict';
|
||
'require view';
|
||
'require form';
|
||
'require ui';
|
||
'require system-hub/api as API';
|
||
|
||
return L.view.extend({
|
||
load: function() {
|
||
// 加载渲染所需的数据
|
||
return Promise.all([
|
||
API.getHealth(),
|
||
API.getStatus()
|
||
]);
|
||
},
|
||
|
||
render: function(data) {
|
||
var health = data[0];
|
||
var status = data[1];
|
||
|
||
// 创建 UI 元素
|
||
var container = E('div', { 'class': 'cbi-map' }, [
|
||
E('h2', {}, 'Dashboard'),
|
||
// ... 更多元素
|
||
]);
|
||
|
||
return container;
|
||
},
|
||
|
||
handleSave: null, // 禁用保存按钮
|
||
handleSaveApply: null, // 禁用保存并应用按钮
|
||
handleReset: null // 禁用重置按钮
|
||
});
|
||
```
|
||
|
||
### 导入模式总结
|
||
|
||
```javascript
|
||
// 核心 LuCI 模块(始终使用引号)
|
||
'require view';
|
||
'require form';
|
||
'require ui';
|
||
'require rpc';
|
||
'require baseclass';
|
||
|
||
// 自定义 API 模块(使用 'as VAR' 进行自动实例化)
|
||
'require system-hub/api as API';
|
||
'require cdn-cache/api as CdnAPI';
|
||
|
||
// 访问全局 L 对象(无需 require)
|
||
L.resolveDefault(...)
|
||
L.Poll.add(...)
|
||
L.ui.addNotification(...)
|
||
```
|
||
|
||
---
|
||
|
||
## ACL 权限结构
|
||
|
||
### 文件位置
|
||
|
||
ACL 文件位于:
|
||
```
|
||
/usr/share/rpcd/acl.d/luci-app-<模块名称>.json
|
||
```
|
||
|
||
在源代码树中:
|
||
```
|
||
luci-app-<模块名称>/root/usr/share/rpcd/acl.d/luci-app-<模块名称>.json
|
||
```
|
||
|
||
### 标准 ACL 模板
|
||
|
||
```json
|
||
{
|
||
"luci-app-module-name": {
|
||
"description": "Module Name - Description",
|
||
"read": {
|
||
"ubus": {
|
||
"luci.module-name": [
|
||
"status",
|
||
"get_system_info",
|
||
"get_health",
|
||
"list_services",
|
||
"get_logs",
|
||
"get_storage",
|
||
"get_settings"
|
||
]
|
||
}
|
||
},
|
||
"write": {
|
||
"ubus": {
|
||
"luci.module-name": [
|
||
"service_action",
|
||
"backup_config",
|
||
"restore_config",
|
||
"reboot",
|
||
"save_settings"
|
||
]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 读取与写入分类
|
||
|
||
**读操作**(不修改系统):
|
||
- `status` - 获取当前状态
|
||
- `get_*` - 检索数据(系统信息、健康状况、设置、日志、存储)
|
||
- `list_*` - 枚举项目(服务、接口等)
|
||
|
||
**写操作**(修改系统状态):
|
||
- `*_action` - 执行操作(启动/停止服务等)
|
||
- `save_*` - 持久化配置更改
|
||
- `backup`、`restore` - 系统备份/恢复
|
||
- `reboot`、`shutdown` - 系统控制
|
||
|
||
### 常见 ACL 错误
|
||
|
||
**错误**:`Access denied` 或 RPC 错误 `-32002`
|
||
|
||
**原因**:方法未在 ACL 文件中列出,或列在错误的部分(读取与写入)
|
||
|
||
**解决方案**:
|
||
1. 确定方法是读操作还是写操作
|
||
2. 将方法名称添加到 ACL 的正确部分
|
||
3. 重启 RPCD:`/etc/init.d/rpcd restart`
|
||
|
||
**验证**:
|
||
```bash
|
||
# 检查 ACL 文件是否为有效 JSON
|
||
jsonlint /usr/share/rpcd/acl.d/luci-app-system-hub.json
|
||
|
||
# 列出所有 ubus 对象和方法
|
||
ubus list luci.system-hub
|
||
|
||
# 使用 ubus call 测试方法
|
||
ubus call luci.system-hub get_health
|
||
```
|
||
|
||
---
|
||
|
||
## 数据结构约定
|
||
|
||
### 健康指标结构(system-hub v0.1.0)
|
||
|
||
基于大量迭代,此结构提供清晰性和一致性:
|
||
|
||
```json
|
||
{
|
||
"cpu": {
|
||
"usage": 25,
|
||
"status": "ok",
|
||
"load_1m": "0.25",
|
||
"load_5m": "0.30",
|
||
"load_15m": "0.28",
|
||
"cores": 4
|
||
},
|
||
"memory": {
|
||
"total_kb": 4096000,
|
||
"free_kb": 2048000,
|
||
"available_kb": 3072000,
|
||
"used_kb": 1024000,
|
||
"buffers_kb": 512000,
|
||
"cached_kb": 1536000,
|
||
"usage": 25,
|
||
"status": "ok"
|
||
},
|
||
"disk": {
|
||
"total_kb": 30408704,
|
||
"used_kb": 5447680,
|
||
"free_kb": 24961024,
|
||
"usage": 19,
|
||
"status": "ok"
|
||
},
|
||
"temperature": {
|
||
"value": 45,
|
||
"status": "ok"
|
||
},
|
||
"network": {
|
||
"wan_up": true,
|
||
"status": "ok"
|
||
},
|
||
"services": {
|
||
"running": 35,
|
||
"failed": 2
|
||
},
|
||
"score": 92,
|
||
"timestamp": "2025-12-26 10:30:00",
|
||
"recommendations": [
|
||
"2 service(s) enabled but not running. Check service status."
|
||
]
|
||
}
|
||
```
|
||
|
||
**关键原则**:
|
||
1. **嵌套对象**用于相关指标(cpu、memory、disk 等)
|
||
2. **一致的结构**:每个指标都有 `usage`(百分比)和 `status`(ok/warning/critical)
|
||
3. **原始值 + 计算值**:同时提供(例如 `used_kb` 和 `usage` 百分比)
|
||
4. **状态阈值**:ok(< warning)、warning(warning-critical)、critical(>= critical)
|
||
5. **总体评分**:仪表板的单一 0-100 健康评分
|
||
6. **动态建议**:基于阈值的可操作警报数组
|
||
|
||
### 状态值
|
||
|
||
对所有指标使用一致的状态字符串:
|
||
- `"ok"` - 正常运行(绿色)
|
||
- `"warning"` - 接近阈值(橙色)
|
||
- `"critical"` - 超过阈值(红色)
|
||
- `"error"` - 无法检索指标
|
||
- `"unknown"` - 指标不可用
|
||
|
||
### 时间戳格式
|
||
|
||
使用 ISO 8601 或一致的本地格式:
|
||
```bash
|
||
timestamp="$(date '+%Y-%m-%d %H:%M:%S')" # 2025-12-26 10:30:00
|
||
```
|
||
|
||
### JSON 中的布尔值
|
||
|
||
在使用 jshn.sh 的 shell 脚本中:
|
||
```bash
|
||
json_add_boolean "wan_up" 1 # true
|
||
json_add_boolean "wan_up" 0 # false
|
||
```
|
||
|
||
在 JavaScript 中:
|
||
```javascript
|
||
if (health.network.wan_up) {
|
||
// WAN 已连接
|
||
}
|
||
```
|
||
|
||
### 数组与单值
|
||
|
||
**对以下情况使用数组**:
|
||
- 相同类型的多个项目(服务、接口、挂载点)
|
||
- 可变长度数据
|
||
|
||
**对以下情况使用单值**:
|
||
- 系统范围的指标(CPU、内存、磁盘)
|
||
- 主要/聚合值(总体温度、总运行时间)
|
||
|
||
**示例 - 存储**:
|
||
```json
|
||
// 多个挂载点 - 使用数组
|
||
"storage": [
|
||
{
|
||
"mount": "/",
|
||
"total_kb": 30408704,
|
||
"used_kb": 5447680,
|
||
"usage": 19
|
||
},
|
||
{
|
||
"mount": "/mnt/usb",
|
||
"total_kb": 128000000,
|
||
"used_kb": 64000000,
|
||
"usage": 50
|
||
}
|
||
]
|
||
|
||
// 仅根文件系统 - 使用对象
|
||
"disk": {
|
||
"total_kb": 30408704,
|
||
"used_kb": 5447680,
|
||
"usage": 19,
|
||
"status": "ok"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 常见错误和解决方案
|
||
|
||
### 1. RPC 错误:"Object not found" (-32000)
|
||
|
||
**错误消息**:
|
||
```
|
||
RPC call to system-hub/status failed with error -32000: Object not found
|
||
```
|
||
|
||
**原因**:RPCD 脚本名称与 JavaScript 中的 ubus 对象名称不匹配
|
||
|
||
**解决方案**:
|
||
1. 检查 JavaScript 中的对象名称:
|
||
```bash
|
||
grep -r "object:" luci-app-system-hub/htdocs --include="*.js"
|
||
```
|
||
输出:`object: 'luci.system-hub'`
|
||
|
||
2. 重命名 RPCD 脚本以完全匹配:
|
||
```bash
|
||
mv root/usr/libexec/rpcd/system-hub root/usr/libexec/rpcd/luci.system-hub
|
||
```
|
||
|
||
3. 确保脚本可执行:
|
||
```bash
|
||
chmod +x root/usr/libexec/rpcd/luci.system-hub
|
||
```
|
||
|
||
4. 重启 RPCD:
|
||
```bash
|
||
/etc/init.d/rpcd restart
|
||
```
|
||
|
||
### 2. JavaScript 错误:"api.methodName is not a function"
|
||
|
||
**错误消息**:
|
||
```
|
||
Uncaught TypeError: api.getHealth is not a function
|
||
at view.load (health.js:12)
|
||
```
|
||
|
||
**原因**:错误的导入模式 - 导入了类构造函数而不是实例
|
||
|
||
**解决方案**:
|
||
从:
|
||
```javascript
|
||
var api = L.require('system-hub.api'); // ❌ 错误
|
||
```
|
||
|
||
改为:
|
||
```javascript
|
||
'require system-hub/api as API'; // ✅ 正确
|
||
```
|
||
|
||
**为什么**:`L.require('module.path')` 返回原始类,`'require module/path as VAR'` 自动实例化。
|
||
|
||
### 3. RPC 错误:"Access denied" (-32002)
|
||
|
||
**错误消息**:
|
||
```
|
||
RPC call to luci.system-hub/get_settings failed with error -32002: Access denied
|
||
```
|
||
|
||
**原因**:方法未在 ACL 文件中列出,或在错误的部分(读取与写入)
|
||
|
||
**解决方案**:
|
||
1. 打开 ACL 文件:`root/usr/share/rpcd/acl.d/luci-app-system-hub.json`
|
||
|
||
2. 将方法添加到适当的部分:
|
||
```json
|
||
"read": {
|
||
"ubus": {
|
||
"luci.system-hub": [
|
||
"get_settings"
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
3. 部署并重启 RPCD:
|
||
```bash
|
||
scp luci-app-system-hub/root/usr/share/rpcd/acl.d/*.json router:/usr/share/rpcd/acl.d/
|
||
ssh router "/etc/init.d/rpcd restart"
|
||
```
|
||
|
||
### 4. 显示错误:"NaN%" 或未定义值
|
||
|
||
**错误**:仪表板显示 "NaN%"、"undefined" 或空值
|
||
|
||
**原因**:前端使用了错误的数据结构键(后端更改后已过时)
|
||
|
||
**解决方案**:
|
||
1. 检查后端输出:
|
||
```bash
|
||
ubus call luci.system-hub get_health
|
||
```
|
||
|
||
2. 更新前端以匹配结构:
|
||
```javascript
|
||
// ❌ 旧结构
|
||
var cpuPercent = health.load / health.cores * 100;
|
||
var memPercent = health.memory.percent;
|
||
|
||
// ✅ 新结构
|
||
var cpuPercent = health.cpu ? health.cpu.usage : 0;
|
||
var memPercent = health.memory ? health.memory.usage : 0;
|
||
```
|
||
|
||
3. 添加 null/undefined 检查:
|
||
```javascript
|
||
var temp = health.temperature?.value || 0;
|
||
var loadAvg = health.cpu?.load_1m || '0.00';
|
||
```
|
||
|
||
### 5. HTTP 404:找不到视图文件
|
||
|
||
**错误消息**:
|
||
```
|
||
HTTP error 404 while loading class file '/luci-static/resources/view/netifyd/overview.js'
|
||
```
|
||
|
||
**原因**:菜单路径与实际视图文件位置不匹配
|
||
|
||
**解决方案**:
|
||
1. 检查菜单 JSON:
|
||
```bash
|
||
cat root/usr/share/luci/menu.d/luci-app-netifyd-dashboard.json
|
||
```
|
||
查找:`"path": "netifyd/overview"`
|
||
|
||
2. 检查实际文件位置:
|
||
```bash
|
||
ls htdocs/luci-static/resources/view/
|
||
```
|
||
文件位于:`view/netifyd-dashboard/overview.js`
|
||
|
||
3. 修复菜单路径或文件位置:
|
||
```json
|
||
// 选项 1:更新菜单路径以匹配文件
|
||
"path": "netifyd-dashboard/overview"
|
||
|
||
// 选项 2:移动文件以匹配菜单
|
||
mv view/netifyd-dashboard/ view/netifyd/
|
||
```
|
||
|
||
### 6. 构建错误:"factory yields invalid constructor"
|
||
|
||
**错误消息**:
|
||
```
|
||
/luci-static/resources/system-hub/api.js: factory yields invalid constructor
|
||
```
|
||
|
||
**原因**:在 API 模块中使用了错误的模式(singleton、普通对象等)
|
||
|
||
**解决方案**:
|
||
始终使用 `baseclass.extend()`:
|
||
```javascript
|
||
return baseclass.extend({
|
||
getStatus: callStatus,
|
||
getHealth: callGetHealth,
|
||
// ... 更多方法
|
||
});
|
||
```
|
||
|
||
不要使用:
|
||
- `baseclass.singleton({...})`
|
||
- 普通对象:`return {...}`
|
||
- `baseclass.prototype`
|
||
|
||
### 7. 更改后 RPCD 无响应
|
||
|
||
**症状**:对 RPCD 脚本的更改不生效
|
||
|
||
**解决方案**:
|
||
1. 验证脚本已部署:
|
||
```bash
|
||
ssh router "ls -la /usr/libexec/rpcd/"
|
||
```
|
||
|
||
2. 检查脚本是否可执行:
|
||
```bash
|
||
ssh router "chmod +x /usr/libexec/rpcd/luci.system-hub"
|
||
```
|
||
|
||
3. 重启 RPCD:
|
||
```bash
|
||
ssh router "/etc/init.d/rpcd restart"
|
||
```
|
||
|
||
4. 清除浏览器缓存(Ctrl+Shift+R)
|
||
|
||
5. 检查 RPCD 日志:
|
||
```bash
|
||
ssh router "logread | grep rpcd"
|
||
```
|
||
|
||
---
|
||
|
||
## 验证清单
|
||
|
||
在部署前使用此清单:
|
||
|
||
### 文件结构
|
||
- [ ] RPCD 脚本存在:`/usr/libexec/rpcd/luci.<模块名称>`
|
||
- [ ] RPCD 脚本可执行:`chmod +x`
|
||
- [ ] 菜单 JSON 存在:`/usr/share/luci/menu.d/luci-app-<模块>.json`
|
||
- [ ] ACL JSON 存在:`/usr/share/rpcd/acl.d/luci-app-<模块>.json`
|
||
- [ ] API 模块存在:`htdocs/luci-static/resources/<模块>/api.js`
|
||
- [ ] 视图存在:`htdocs/luci-static/resources/view/<模块>/*.js`
|
||
|
||
### 命名约定
|
||
- [ ] RPCD 脚本名称与 JavaScript 中的 ubus 对象匹配(包括 `luci.` 前缀)
|
||
- [ ] 菜单路径与视图文件目录结构匹配
|
||
- [ ] 所有 ubus 对象以 `luci.` 开头
|
||
- [ ] ACL 键与包名称匹配:`"luci-app-<模块>"`
|
||
|
||
### 代码验证
|
||
- [ ] API 模块使用 `baseclass.extend()` 模式
|
||
- [ ] 视图使用 `'require <模块>/api as API'` 模式导入 API
|
||
- [ ] 所有 rpc.declare() 调用包含正确的 `object`、`method`、`params`、`expect`
|
||
- [ ] RPCD 脚本输出有效 JSON(使用 `ubus call` 测试)
|
||
- [ ] 菜单 JSON 有效(使用 `jsonlint` 测试)
|
||
- [ ] ACL JSON 有效(使用 `jsonlint` 测试)
|
||
|
||
### 权限
|
||
- [ ] 所有读方法在 ACL `"read"` 部分
|
||
- [ ] 所有写方法在 ACL `"write"` 部分
|
||
- [ ] ACL 中的方法与 RPCD 脚本方法名称完全匹配
|
||
|
||
### 测试
|
||
- [ ] 运行验证脚本:`./secubox-tools/validate-modules.sh`
|
||
- [ ] 通过 ubus 测试每个方法:`ubus call luci.<模块> <方法>`
|
||
- [ ] 在浏览器中测试前端(检查控制台错误)
|
||
- [ ] 部署后清除浏览器缓存
|
||
- [ ] 验证 RPCD 重启:`/etc/init.d/rpcd restart`
|
||
|
||
### 自动化验证命令
|
||
|
||
```bash
|
||
# 运行全面验证
|
||
./secubox-tools/validate-modules.sh
|
||
|
||
# 验证特定模块
|
||
./secubox-tools/validate-module-generation.sh luci-app-system-hub
|
||
|
||
# 检查 JSON 语法
|
||
find luci-app-system-hub -name "*.json" -exec jsonlint {} \;
|
||
|
||
# 检查 shell 脚本
|
||
shellcheck luci-app-system-hub/root/usr/libexec/rpcd/*
|
||
```
|
||
|
||
---
|
||
|
||
## 测试和部署
|
||
|
||
### 使用 ubus 进行本地测试
|
||
|
||
在部署到路由器之前,本地测试 RPCD 脚本:
|
||
|
||
```bash
|
||
# 将 RPCD 脚本复制到本地 /tmp
|
||
cp luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub /tmp/
|
||
|
||
# 设置可执行权限
|
||
chmod +x /tmp/luci.system-hub
|
||
|
||
# 测试 'list' 操作
|
||
/tmp/luci.system-hub list
|
||
|
||
# 使用方法测试 'call' 操作
|
||
/tmp/luci.system-hub call status
|
||
|
||
# 测试带参数的方法
|
||
echo '{"service":"network","action":"restart"}' | /tmp/luci.system-hub call service_action
|
||
```
|
||
|
||
### 部署脚本
|
||
|
||
使用部署脚本进行快速迭代:
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# deploy-system-hub.sh
|
||
|
||
ROUTER="root@192.168.8.191"
|
||
|
||
echo "正在将 system-hub 部署到 $ROUTER"
|
||
|
||
# 部署 API 模块
|
||
scp luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js \
|
||
"$ROUTER:/www/luci-static/resources/system-hub/"
|
||
|
||
# 部署视图
|
||
scp luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/*.js \
|
||
"$ROUTER:/www/luci-static/resources/view/system-hub/"
|
||
|
||
# 部署 RPCD 后端
|
||
scp luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub \
|
||
"$ROUTER:/usr/libexec/rpcd/"
|
||
|
||
# 部署 ACL
|
||
scp luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json \
|
||
"$ROUTER:/usr/share/rpcd/acl.d/"
|
||
|
||
# 设置权限并重启
|
||
ssh "$ROUTER" "chmod +x /usr/libexec/rpcd/luci.system-hub && /etc/init.d/rpcd restart"
|
||
|
||
echo "部署完成!请清除浏览器缓存(Ctrl+Shift+R)"
|
||
```
|
||
|
||
### 浏览器测试
|
||
|
||
1. 打开浏览器控制台(F12)
|
||
2. 导航到模块页面
|
||
3. 检查错误:
|
||
- RPC 错误(object not found、method not found、access denied)
|
||
- JavaScript 错误(api.method is not a function)
|
||
- 404 错误(找不到视图文件)
|
||
4. 测试功能:
|
||
- 数据加载正确显示
|
||
- 操作有效(启动/停止服务、保存设置)
|
||
- 没有 "NaN"、"undefined" 或空值
|
||
|
||
### 远程 ubus 测试
|
||
|
||
在路由器上测试 RPCD 方法:
|
||
|
||
```bash
|
||
# 列出所有方法
|
||
ssh router "ubus list luci.system-hub"
|
||
|
||
# 调用无参数方法
|
||
ssh router "ubus call luci.system-hub status"
|
||
|
||
# 调用带参数方法
|
||
ssh router "ubus call luci.system-hub service_action '{\"service\":\"network\",\"action\":\"restart\"}'"
|
||
|
||
# 格式化 JSON 输出
|
||
ssh router "ubus call luci.system-hub get_health | jsonlint"
|
||
```
|
||
|
||
### 调试技巧
|
||
|
||
**启用 RPCD 调试日志**:
|
||
```bash
|
||
# 编辑 /etc/init.d/rpcd
|
||
# 在 procd_set_param command 中添加 -v 标志
|
||
procd_set_param command "$PROG" -v
|
||
|
||
# 重启 RPCD
|
||
/etc/init.d/rpcd restart
|
||
|
||
# 监视日志
|
||
logread -f | grep rpcd
|
||
```
|
||
|
||
**启用 JavaScript 控制台日志**:
|
||
```javascript
|
||
// 添加到 api.js
|
||
console.log('API v0.1.0 已加载于', new Date().toISOString());
|
||
|
||
// 添加到视图
|
||
console.log('正在加载健康数据...');
|
||
API.getHealth().then(function(data) {
|
||
console.log('健康数据:', data);
|
||
});
|
||
```
|
||
|
||
**测试 JSON 输出**:
|
||
```bash
|
||
# 在路由器上
|
||
/usr/libexec/rpcd/luci.system-hub call get_health | jsonlint
|
||
|
||
# 检查常见错误
|
||
# - 缺少逗号
|
||
# - 尾随逗号
|
||
# - 未加引号的键
|
||
# - 无效的转义序列
|
||
```
|
||
|
||
---
|
||
|
||
## 最佳实践总结
|
||
|
||
### 应该做的:
|
||
- 对所有 ubus 对象使用 `luci.` 前缀
|
||
- RPCD 脚本命名与 ubus 对象完全匹配
|
||
- 对 API 模块使用 `baseclass.extend()`
|
||
- 使用 `'require module/api as API'` 模式导入 API
|
||
- 在前端添加 null/undefined 检查:`health.cpu?.usage || 0`
|
||
- 部署前使用 `jsonlint` 验证 JSON
|
||
- 浏览器测试前先使用 `ubus call` 测试
|
||
- 后端更改后重启 RPCD
|
||
- 前端更改后清除浏览器缓存
|
||
- 提交前运行 `./secubox-tools/validate-modules.sh`
|
||
|
||
### 不应该做的:
|
||
- 使用没有 `luci.` 前缀的 ubus 对象名称
|
||
- 对 API 模块使用 `baseclass.singleton()` 或普通对象
|
||
- 使用 `L.require('module.path')` 导入 API(返回类而不是实例)
|
||
- 忘记将方法添加到 ACL 文件
|
||
- 在 ACL 部分混淆读/写方法
|
||
- 从 RPCD 脚本输出非 JSON
|
||
- 后端和前端之间使用不一致的数据结构
|
||
- 不先本地测试就部署
|
||
- 假设数据存在 - 始终检查 null/undefined
|
||
- 忘记使 RPCD 脚本可执行(`chmod +x`)
|
||
|
||
---
|
||
|
||
## 版本历史
|
||
|
||
**v1.0**(2025-12-26)
|
||
- 初始参考指南
|
||
- 基于 luci-app-secubox v1.0.0 和 luci-app-system-hub v0.1.0
|
||
- 记录了所有关键模式和常见错误
|
||
- 针对实际实现挑战进行了验证
|
||
|
||
---
|
||
|
||
## 参考资料
|
||
|
||
- **OpenWrt 文档**:https://openwrt.org/docs/guide-developer/start
|
||
- **LuCI 文档**:https://github.com/openwrt/luci/wiki
|
||
- **ubus 文档**:https://openwrt.org/docs/techref/ubus
|
||
- **UCI 文档**:https://openwrt.org/docs/guide-user/base-system/uci
|
||
- **jshn.sh 库**:OpenWrt 上的 `/usr/share/libubox/jshn.sh`
|
||
|
||
---
|
||
|
||
## 联系方式
|
||
|
||
如有问题或想为本参考指南做贡献:
|
||
- **作者**:CyberMind <contact@cybermind.fr>
|
||
- **项目**:SecuBox OpenWrt
|
||
- **仓库**:https://github.com/cybermind-fr/secubox-openwrt
|
||
|
||
---
|
||
|
||
**参考指南结束**
|