Files
MeowOS-For-R3P2/patch_v2/meowpanel/src/mp_store.c
T
AskaEth 2826c1e905 feat: M6.40批 — 阅读器UI改版(书架list·grid+空态骨架+界面设置+App设置页+header·footer+背景黑默认) + 系统级音量翻页映射 + P1 MD3(select面板28/对话框按钮胶囊) + MeowPanel写端点护栏 + SDK公开仓·文档同步
- reader: app.c/shelf.c/reader.c 全套UI; 新增 reader_conf(reader.conf)/settings(App设置页)/reader_ipc(音量翻页IPC响应); app.json加volume_page
- launcher: keys.c meow_volpage_*门 + util.c app_id→fd映射(HELLO记fd/断开清) + app_launcher.c 前台三处接线(进入sink/按键路径volpage_key/离开clear) → 前台声明volume_page的App音量键→MSG_KEY_PAGEUP/PAGEDOWN翻页(非阅读器前台音量照旧)
- sdk: meow_select.c 底部面板28 + meow_ui_dialog.c 对话框按钮胶囊(h/2) + test_meow_ui_select.c修采样
- meowpanel: /api/net/policy·/api/power/policy 加防自锁+policy默认关两条护栏(panel.policy_enabled默认0)
- 文档: MEOW_APP_SDK.md §12.6 + meowpanel README端点表

0warning;宿主测试全过;二进制留批量批次未入库
2026-09-04 18:13:18 +08:00

606 lines
26 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* mp_store.c — 设置镜像(settings.conf)+ 主题 + /api/settings*,/api/theme*。权威来源:meow_settings.c g_defs(逐键一致)。 */
#include "mp.h"
const char *g_settings_file = SETTINGS_FILE;
static const setdef_t g_sets[] = {
/* 显示 */
{ "display.brightness", ST_INT, "101", 1, 100, NULL, NULL, "显示" },
{ "display.brightness_min", ST_INT, "1", 0, 255, NULL, NULL, "显示" },
{ "display.screen_off_mode", ST_ENUM, "panel",0,0, "panel,black", "面板下电,背光全黑", "显示" },
{ "display.screen_off_timeout", ST_INT, "30", 0, 86400,NULL, NULL, "显示" },
{ "display.refresh_rate", ST_ENUM, "60", 0,0, "60,30,15", "60Hz,30Hz,15Hz", "显示" },
{ "display.refresh_always_max", ST_BOOL, "0", 0,0, NULL, NULL, "显示" },
{ "display.theme", ST_ENUM, "dark", 0,0, "dark,light", "深色,浅色", "显示" },
{ "display.theme_mode", ST_ENUM, "manual",0,0, "manual,auto", "手动,时段自动", "显示" },
{ "display.theme_dark_start", ST_INT, "0", 0, 1439, NULL, NULL, "显示" },
{ "display.theme_dark_end", ST_INT, "0", 0, 1439, NULL, NULL, "显示" },
{ "display.background", ST_STR, "black",0,0, NULL, NULL, "显示" },
{ "display.multi_font", ST_INT, "0", 0, 1, NULL, NULL, "显示" },
/* 电源 */
{ "power.longpress_menu_ms", ST_INT, "3000", 0,0, NULL, NULL, "电源" },
{ "power.forceoff_ms", ST_INT, "8000", 0,0, NULL, NULL, "电源" },
{ "power.defeat_hotkey", ST_STR, "power_voldown",0,0, NULL, NULL, "电源" },
/* 开机 */
{ "boot.default", ST_ENUM, "meow", 0,0, "meow,hiby", "MeowOS,HiByOS", "开机" },
{ "boot.menu_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "开机" },
{ "boot.timeout_ms", ST_INT, "5000", 0,0, NULL, NULL, "开机" },
/* 输入 */
{ "input.hotkey_seq", ST_STR, "volup,volup,volup",0,0, NULL, NULL, "输入" },
{ "input.hotkey_window_ms", ST_INT, "2500", 0,0, NULL, NULL, "输入" },
{ "input.touch_arm_delay_ms", ST_INT, "700", 0,0, NULL, NULL, "输入" },
{ "input.swipe_invert", ST_BOOL, "0", 0,0, NULL, NULL, "输入" },
/* 字体/主题 */
{ "font.file", ST_STR, "MeowOS/fonts/default/regular/font.png",0,0, NULL, NULL, "字体主题" },
{ "font.class_cjk", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
{ "font.class_cjk_sym", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
{ "font.class_num", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
{ "font.class_latin", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
{ "font.class_latin_sym", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
{ "font.class_other", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
{ "font.load_full", ST_INT, "0", 0,1, NULL, NULL, "字体主题" },
{ "font.size.title", ST_INT, "28", 0,0, NULL, NULL, "字体主题" },
{ "font.size.body", ST_INT, "20", 0,0, NULL, NULL, "字体主题" },
{ "ui.style", ST_ENUM, "md3", 0,0, "md3,miuix", "Material You,MIUIx", "字体主题" },
/* 通知 */
{ "notify.center_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "通知" },
{ "notify.app_quota", ST_INT, "16", 0,0, NULL, NULL, "通知" },
{ "notify.categories", ST_INT, "4", 1,4, NULL, NULL, "通知" },
{ "notify.category_quota", ST_INT, "4", 1,16, NULL, NULL, "通知" },
{ "notify.media_quota", ST_INT, "1", 1,1, NULL, NULL, "通知" },
{ "notify.dnd", ST_BOOL, "0", 0,0, NULL, NULL, "通知" },
/* 日志 */
{ "log.level", ST_ENUM, "info", 0,0, "error,warn,info,debug", "错误,警告,信息,调试", "日志" },
{ "log.to_card", ST_BOOL, "1", 0,0, NULL, NULL, "日志" },
/* 音频 */
{ "audio.volume", ST_INT, "60", 0,100, NULL, NULL, "音频" },
{ "audio.volume_max", ST_INT, "100", 0,100, NULL, NULL, "音频" },
{ "audio.volume_step", ST_INT, "1", 1,5, NULL, NULL, "音频" },
{ "audio.output", ST_STR, "hw:0,0",0,0, NULL, NULL, "音频" },
/* 网络 */
{ "network.wifi_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "网络" },
{ "network.bluetooth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "网络" },
/* 系统 */
{ "system.hostname", ST_STR, "meowos",0,0, NULL, NULL, "系统" },
/* 面板(MeowPanel 自身,访问级 PIN 鉴权)
* panel.pin = 4 位数字 PIN 的 sha256+salt 哈希(`<salt_hex>:<sha256_hex(salt||pin)>`,绝不存明文)。
* 与未来 MeowOS 锁屏 PIN 同源:锁屏键 `system.pin` 预留——本后端只读面板键 panel.pin;将来锁屏启用时
* 以 system.pin 为准并做同源迁移(panel.pin 视为其缓存/别名),此处仅留兼容注记,不实现。
* panel.auth_enabled/panel.auth_pass 为旧密码机制,保留兼容迁移(见鉴权段注释),新代码以 PIN 为准。 */
{ "panel.pin", ST_STR, "", 0,0, NULL, NULL, "面板" },
{ "panel.auth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "面板" },
{ "panel.auth_pass", ST_STR, "", 0,0, NULL, NULL, "面板" },
/* panel.policy_enabled — 策略写「默认关」门禁(guard③):POST /api/net/policy 与 /api/power/policy
* 默认关闭(缺省 0),置 1 才放行。落 settings.conf(「面板」组,前端设置页可开启),仅作面板侧门禁记录,
* 不影响 powermgr 的 `enacted`(那是服务端「策略是否已应用」,二者独立)。 */
{ "panel.policy_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "面板" },
};
#define SETS_COUNT ((int)(sizeof g_sets / sizeof g_sets[0]))
const setdef_t *setdef_find(const char *key) {
for (int i = 0; i < SETS_COUNT; i++)
if (!strcmp(g_sets[i].key, key)) return &g_sets[i];
return NULL;
}
void settings_mkdir_p(void)
{
/* 逐级创建 settings.conf 所在目录(mkdir -p 语义,容忍已存在)。 */
char tmp[1024];
snprintf(tmp, sizeof tmp, "%s", g_settings_file);
char *slash = strrchr(tmp, '/');
if (!slash || slash == tmp) return; /* 无目录或根部 */
*slash = 0;
if (!tmp[0]) return;
char buf[1024]; buf[0] = 0;
size_t i = 0;
if (tmp[0] == '/') buf[i++] = '/';
const char *s = tmp + (tmp[0] == '/' ? 1 : 0);
while (*s) {
const char *tom = strchr(s, '/');
size_t l = tom ? (size_t)(tom - s) : strlen(s);
if (l) {
if (i && buf[i - 1] != '/') buf[i++] = '/';
if (i + l >= sizeof buf) break;
memcpy(buf + i, s, l);
i += l;
buf[i] = 0;
mkdir(buf, 0777);
}
if (!tom) break;
s = tom + 1;
}
}
char *settings_get_value(const char *key)
{
FILE *f = fopen(g_settings_file, "r");
if (!f) return NULL;
size_t kl = strlen(key);
char line[256];
char *found = NULL;
while (fgets(line, sizeof line, f)) {
char *eq = strchr(line, '=');
if (!eq) continue;
char *k = line;
while (*k == ' ' || *k == '\t') k++;
long n = (long)(eq - k);
while (n > 0 && (k[n - 1] == ' ' || k[n - 1] == '\t')) n--;
if ((size_t)n != kl || strncmp(k, key, kl) != 0) continue;
found = eq + 1;
break;
}
fclose(f);
if (!found) return NULL;
while (*found == ' ' || *found == '\t') found++;
char *e = found + strlen(found);
while (e > found && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ' || e[-1] == '\t')) *--e = 0;
return strdup(found);
}
int settings_get_int(const char *key, int defv)
{
char *v = settings_get_value(key);
if (!v) return defv;
int r = atoi(v);
free(v);
return r;
}
int panel_policy_write_enabled(void)
{
return settings_get_int("panel.policy_enabled", 0) ? 1 : 0;
}
int brightness_clamp(int v)
{
int mn = settings_get_int("display.brightness_min", 1);
if (mn < 1) mn = 1;
if (mn > 100) mn = 100;
if (v < mn) v = mn;
if (v > 100) v = 100;
return v;
}
int settings_write_one(const char *key, const char *val)
{
settings_mkdir_p(); /* 目录缺失自动建(与 meow_settings_save 同;失败由 fopen 兜底) */
FILE *f = fopen(g_settings_file, "r");
char tmp[2048];
size_t n = 0;
int c;
if (f) {
while (n < sizeof tmp - 1 && (c = fgetc(f)) != EOF) tmp[n++] = (char)c;
fclose(f);
}
tmp[n] = 0;
FILE *nf = fopen(g_settings_file, "w");
if (!nf) return -1;
size_t klen = strlen(key);
int found = 0;
if (n == 0) fprintf(nf, "%s\n", SETTINGS_HDR);
/* 逐行重写:头照抄;命中 key 行替换;其余照抄。匹配容忍 `key = value` 两侧空格(与 settings_get_value 同口径)。 */
char *save = NULL;
for (char *ln = strtok_r(tmp, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) {
char *p = ln;
while (*p == ' ' || *p == '\t') p++;
char *eq = strchr(p, '=');
if (eq) {
char *k = p;
size_t km = (size_t)(eq - k);
while (km > 0 && (k[km - 1] == ' ' || k[km - 1] == '\t')) km--;
if (km == klen && strncmp(k, key, klen) == 0) {
fprintf(nf, "%s = %s\n", key, val);
found = 1;
continue;
}
}
fprintf(nf, "%s\n", ln);
}
if (!found) fprintf(nf, "%s = %s\n", key, val);
fclose(nf);
return 0;
}
int settings_coerce(const setdef_t *d, const char *input, char *out, size_t outcap)
{
switch (d->type) {
case ST_INT: {
int v = atoi(input);
if (!strcmp(d->key, "display.brightness")) v = brightness_clamp(v);
else {
if (d->max > 0) { if (v < d->min) v = d->min; if (v > d->max) v = d->max; }
else if (v < d->min) v = d->min;
}
snprintf(out, outcap, "%d", v);
return 0;
}
case ST_BOOL: {
char *q = (char *)input;
while (*q == ' ' || *q == '\t') q++;
int b = 0;
if (!strcasecmp(q, "true") || !strcasecmp(q, "yes") || !strcasecmp(q, "on")) b = 1;
else { long v = strtol(q, NULL, 10); if (q[0] && v != 0) b = 1; }
snprintf(out, outcap, "%d", b ? 1 : 0);
return 0;
}
case ST_ENUM: {
const char *names = d->enum_names;
if (!names || !names[0]) { snprintf(out, outcap, "%s", input); return 0; }
int cnt = csv_count(names);
char tok[128];
/* 接受名字或索引(向后兼容) */
int idx = -1;
for (int i = 0; i < cnt; i++) {
csv_at(names, i, tok, sizeof tok);
if (!strcmp(tok, input)) { idx = i; break; }
}
if (idx < 0) {
int n = atoi(input);
if (n >= 0 && n < cnt) idx = n;
}
if (idx < 0) return -1; /* 非法枚举 */
csv_at(names, idx, tok, sizeof tok);
snprintf(out, outcap, "%s", tok);
return 0;
}
case ST_STR:
strncpy(out, input, outcap - 1);
out[outcap - 1] = 0;
return 0;
}
return -1;
}
int settings_reset_all(void)
{
settings_mkdir_p();
FILE *nf = fopen(g_settings_file, "w");
if (!nf) return -1;
fprintf(nf, "%s\n", SETTINGS_HDR);
for (int i = 0; i < SETS_COUNT; i++)
fprintf(nf, "%s = %s\n", g_sets[i].key, g_sets[i].def);
fclose(nf);
return 0;
}
void enum_options_json(sbuf_t *s, const char *names, const char *labels)
{
sA(s, "[");
int cnt = csv_count(names);
for (int i = 0; i < cnt; i++) {
char nb[128], lb[128];
csv_at(names, i, nb, sizeof nb);
csv_at(labels, i, lb, sizeof lb);
if (i) sA(s, ",");
sA(s, "{\"name\":");
sA_str(s, nb);
sA(s, ",\"label\":");
sA_str(s, lb[0] ? lb : nb);
sA(s, "}");
}
sA(s, "]");
}
void settings_item_json(sbuf_t *s, const setdef_t *d, const char *val)
{
const char *tn = "str";
if (d->type == ST_INT) tn = "int";
else if (d->type == ST_BOOL) tn = "bool";
else if (d->type == ST_ENUM) tn = "enum";
sA(s, "{\"key\":");
sA_str(s, d->key);
sA(s, ",\"type\":\"%s\",\"value\":", tn);
if (d->type == ST_INT) {
int v = atoi(val);
if (!strcmp(d->key, "display.brightness")) v = brightness_clamp(v);
else if (d->max > 0) { if (v < d->min) v = d->min; if (v > d->max) v = d->max; }
else if (v < d->min) v = d->min;
sA(s, "%d", v);
sA(s, ",\"min\":%d,\"max\":%d", d->min, int_meta_max(d));
} else if (d->type == ST_BOOL) {
sA(s, "%s", str_truthy(val) ? "true" : "false");
} else {
if (!strcmp(d->key, "panel.auth_pass") || !strcmp(d->key, "panel.pin")) {
/* 密码/PIN 哈希绝不出现在响应里:value 恒空串,附 set 标志(是否已设) */
sA(s, "\"\",\"set\":%s", val[0] ? "true" : "false");
} else {
sA_str(s, val);
}
}
sA(s, ",\"def\":");
sA_str(s, d->def);
if (d->type == ST_ENUM) {
sA(s, ",\"options\":");
enum_options_json(s, d->enum_names, d->enum_labels);
sA(s, ",\"default_name\":");
sA_str(s, d->def);
}
sA(s, "}");
}
char *settings_current_value(const setdef_t *d)
{
char *v = settings_get_value(d->key);
if (v) return v;
return strdup(d->def);
}
int api_settings_get_any(int sock, const char *key)
{
const setdef_t *d = setdef_find(key);
if (!d) return send_err(sock, 404, "NOT_FOUND", "未知设置项");
char *val = settings_current_value(d);
sbuf_t s = {0};
settings_item_json(&s, d, val);
int rc = send_json(sock, 200, s.b ? s.b : "");
free(val);
sbuf_free(&s);
return rc;
}
int api_settings_get_all(int sock)
{
sbuf_t s = {0};
sA(&s, "{\"items\":[");
char groups[16][32];
int ngroups = 0;
for (int i = 0; i < SETS_COUNT; i++) {
const setdef_t *d = &g_sets[i];
char *val = settings_current_value(d);
if (i) sA(&s, ",");
settings_item_json(&s, d, val);
free(val);
if (d->group && d->group[0]) {
int found = 0;
for (int g = 0; g < ngroups; g++) if (!strcmp(groups[g], d->group)) { found = 1; break; }
if (!found && ngroups < 16) snprintf(groups[ngroups++], 32, "%s", d->group);
}
}
sA(&s, "],\"count\":%d,\"groups\":[", SETS_COUNT);
for (int g = 0; g < ngroups; g++) {
if (g) sA(&s, ",");
sA_str(&s, groups[g]);
}
sA(&s, "]}");
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
int api_settings_put(int sock, const char *key, const req_t *r)
{
const setdef_t *d = setdef_find(key);
if (!d) return send_err(sock, 404, "NOT_FOUND", "未知设置项");
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
jv_t *root = jv_parse(r->body, (int)r->body_len);
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
jv_t *val = jv_obj_get(root, "value");
if (!val) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺少 value 字段"); }
char raw[256];
switch (val->type) {
case J_NUM: snprintf(raw, sizeof raw, "%lld", val->inum); break;
case J_STR: snprintf(raw, sizeof raw, "%s", val->str); break;
case J_BOOL: snprintf(raw, sizeof raw, "%d", val->bval ? 1 : 0); break;
default: jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "value 类型不支持");
}
jv_free(root);
char hashbuf[200];
/* panel.pin:4 位数字 PIN,只落带盐哈希;空串=清除(回未设状态);非法(非 4 位数字)→ 422 */
if (!strcmp(key, "panel.pin")) {
if (raw[0]) {
if (!pin_validate(raw)) return send_err(sock, 422, "UNSUPPORTED", "PIN 必须为 4 位数字");
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0)
return send_err(sock, 500, "INTERNAL", "生成 PIN 哈希失败");
} else {
hashbuf[0] = 0; /* 清空 → 回未设状态 */
}
if (settings_write_one(key, hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
pin_lock_reset();
sbuf_t s = {0};
settings_item_json(&s, d, hashbuf);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
/* panel.auth_pass:只落带盐哈希,绝不存明文;空串=清除密码 */
if (!strcmp(key, "panel.auth_pass")) {
if (raw[0]) {
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0)
return send_err(sock, 500, "INTERNAL", "生成密码哈希失败");
} else {
hashbuf[0] = 0; /* 清空 → 禁用密码 */
}
if (settings_write_one(key, hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
sbuf_t s = {0};
settings_item_json(&s, d, hashbuf);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
/* panel.auth_enabled(旧机制,兼容迁移):置 1 前须已有访问屏障(PIN 或旧密码) */
if (!strcmp(key, "panel.auth_enabled")) {
char coerced[256];
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0)
return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法");
if (!strcmp(coerced, "1") && !pin_set() && !auth_pass_set())
return send_err(sock, 422, "UNSUPPORTED", "请先设置面板 PIN 或密码");
if (settings_write_one(key, coerced) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
sbuf_t s = {0};
settings_item_json(&s, d, coerced);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
char coerced[256];
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) {
return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法");
}
if (settings_write_one(key, coerced) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
/* boot.default 同步权威文件(同 meow_settings_save) */
if (!strcmp(key, "boot.default")) {
int v = (!strcmp(coerced, "hiby")) ? 1 : 0;
FILE *bf = fopen(BOOT_DEFAULT_FILE, "w");
if (bf) { fprintf(bf, "%d\n", v); fclose(bf); }
}
sbuf_t s = {0};
settings_item_json(&s, d, coerced);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
int api_settings_batch(int sock, const req_t *r)
{
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
jv_t *root = jv_parse(r->body, (int)r->body_len);
if (!root || root->type != J_OBJ) { if (root) jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON 对象"); }
sbuf_t ok = {0}, fail = {0};
int nok = 0, nfail = 0;
for (const jv_t *p = root->child; p; p = p->next) {
const setdef_t *d = setdef_find(p->str);
if (!d) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"NOT_FOUND\",\"message\":\"未知设置项\"}"); nfail++; continue; }
jv_t *val = p->child;
char raw[256];
switch (val->type) {
case J_NUM: snprintf(raw, sizeof raw, "%lld", val->inum); break;
case J_STR: snprintf(raw, sizeof raw, "%s", val->str); break;
case J_BOOL: snprintf(raw, sizeof raw, "%d", val->bval ? 1 : 0); break;
default: if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"BAD_REQUEST\",\"message\":\"非法值类型\"}"); nfail++; continue;
}
/* panel.pin:只落带盐哈希(空=清除);非法(非 4 位数字)→ 422 */
if (!strcmp(p->str, "panel.pin")) {
char hashbuf[200];
if (raw[0]) {
if (!pin_validate(raw)) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"PIN 必须为 4 位数字\"}"); nfail++; continue; }
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"生成 PIN 哈希失败\"}"); nfail++; continue; }
} else hashbuf[0] = 0;
if (settings_write_one(p->str, hashbuf) == 0) { if (nok) sA(&ok, ","); sA_str(&ok, p->str); nok++; }
else { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}"); nfail++; }
continue;
}
/* panel.auth_pass:只落带盐哈希(空=清除);panel.auth_enabled:启用前须已有密码 */
if (!strcmp(p->str, "panel.auth_pass")) {
char hashbuf[200];
if (raw[0]) { if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"生成密码哈希失败\"}"); nfail++; continue; } }
else hashbuf[0] = 0;
if (settings_write_one(p->str, hashbuf) == 0) { if (nok) sA(&ok, ","); sA_str(&ok, p->str); nok++; }
else { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}"); nfail++; }
continue;
}
char coerced[256];
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) {
if (nfail) sA(&fail, ",");
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"值域/枚举非法\"}");
nfail++;
continue;
}
/* panel.auth_enabled(迁移):启用前须已有访问屏障(PIN 或旧密码) */
if (!strcmp(p->str, "panel.auth_enabled") && !strcmp(coerced, "1") && !pin_set() && !auth_pass_set()) {
if (nfail) sA(&fail, ",");
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"请先设置面板 PIN 或密码\"}");
nfail++;
continue;
}
if (settings_write_one(p->str, coerced) == 0) {
if (nok) sA(&ok, ",");
sA_str(&ok, p->str);
nok++;
} else {
if (nfail) sA(&fail, ",");
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}");
nfail++;
}
}
jv_free(root);
sbuf_t s = {0};
sA(&s, "{\"updated\":[");
if (ok.b) sA(&s, "%s", ok.b);
sA(&s, "],\"failed\":[");
if (fail.b) sA(&s, "%s", fail.b);
sA(&s, "]}");
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s); sbuf_free(&ok); sbuf_free(&fail);
return rc;
}
int api_settings_restore(int sock)
{
if (settings_reset_all() < 0) return send_err(sock, 500, "INTERNAL", "恢复失败");
return send_json(sock, 200, "{\"reset\":true}");
}
void theme_json(sbuf_t *s)
{
const setdef_t *dst = setdef_find("ui.style");
const setdef_t *dth = setdef_find("display.theme");
const setdef_t *dbg = setdef_find("display.background");
char *style = settings_current_value(dst);
char *theme = settings_current_value(dth);
char *bg = settings_current_value(dbg);
/* 当前值 → 中文标签(csv 里同下标查 label) */
char slabel[128] = "", tlabel[128] = "";
for (int i = 0; i < csv_count(dst->enum_names); i++) {
char nb[128], lb[128];
csv_at(dst->enum_names, i, nb, sizeof nb);
csv_at(dst->enum_labels, i, lb, sizeof lb);
if (!strcmp(nb, style)) { snprintf(slabel, sizeof slabel, "%s", lb[0] ? lb : nb); break; }
}
for (int i = 0; i < csv_count(dth->enum_names); i++) {
char nb[128], lb[128];
csv_at(dth->enum_names, i, nb, sizeof nb);
csv_at(dth->enum_labels, i, lb, sizeof lb);
if (!strcmp(nb, theme)) { snprintf(tlabel, sizeof tlabel, "%s", lb[0] ? lb : nb); break; }
}
sA(s, "{\"style\":");
sA_str(s, style);
sA(s, ",\"theme\":");
sA_str(s, theme);
sA(s, ",\"background\":");
sA_str(s, bg);
sA(s, ",\"style_label\":");
sA_str(s, slabel);
sA(s, ",\"theme_label\":");
sA_str(s, tlabel);
sA(s, ",\"style_options\":");
enum_options_json(s, dst->enum_names, dst->enum_labels);
sA(s, ",\"theme_options\":");
enum_options_json(s, dth->enum_names, dth->enum_labels);
sA(s, ",\"background_options\":[{\"name\":\"black\",\"label\":\"黑\"},{\"name\":\"blue\",\"label\":\"深蓝\"},{\"name\":\"green\",\"label\":\"墨绿\"}]}");
free(style); free(theme); free(bg);
}
int api_theme_get(int sock)
{
sbuf_t s = {0};
theme_json(&s);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
int api_theme_put(int sock, const req_t *r)
{
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
jv_t *root = jv_parse(r->body, (int)r->body_len);
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
/* 允许 style/theme/background 任意子集 */
const char *keys[3] = { "style", "theme", "background" };
const char *setkeys[3] = { "ui.style", "display.theme", "display.background" };
for (int i = 0; i < 3; i++) {
jv_t *v = jv_obj_get(root, keys[i]);
if (!v) continue;
const setdef_t *d = setdef_find(setkeys[i]);
if (!d) continue;
char raw[256];
if (v->type == J_STR) snprintf(raw, sizeof raw, "%s", v->str);
else if (v->type == J_NUM) snprintf(raw, sizeof raw, "%lld", v->inum);
else continue;
char coerced[256];
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "非法值"); }
settings_write_one(setkeys[i], coerced);
}
jv_free(root);
return api_theme_get(sock);
}