/* 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 哈希(`:`,绝不存明文)。 * 与未来 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); }