556 lines
20 KiB
C
556 lines
20 KiB
C
/* config.c - 配置文件 / 环境变量加载 */
|
|
#include "config.h"
|
|
#include "http.h"
|
|
#include "i18n.h"
|
|
#include "json.h"
|
|
#include "util.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdarg.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <limits.h>
|
|
#include <sys/stat.h>
|
|
|
|
#define DEFAULT_BASE_URL "https://api.deepseek.com/v1"
|
|
#define DEFAULT_MODEL "deepseek-flash"
|
|
|
|
void config_defaults(LigentConfig *c)
|
|
{
|
|
memset(c, 0, sizeof *c);
|
|
c->base_url = xstrdup(DEFAULT_BASE_URL);
|
|
c->api_key = xstrdup("");
|
|
c->api_key_env = NULL; /* 密钥来源, 供诊断展示 (见 config.h) */
|
|
c->model = xstrdup(DEFAULT_MODEL);
|
|
c->system = NULL;
|
|
c->temperature = -1; /* 不发送, 用服务端默认 */
|
|
c->max_tokens = 0; /* 不发送 */
|
|
c->thinking = THINKING_AUTO; /* 不发送 thinking, 由服务端决定 */
|
|
c->reasoning_effort = NULL; /* 不发送 */
|
|
c->timeout = 300;
|
|
c->stream = 1;
|
|
/* max_steps <= 0 = 不限制单轮往返次数 (默认). 这是**默认**值: 模型陷进
|
|
* "调工具 → 看结果 → 再调" 的循环时不会再被我们自己掐断, 由用户 Ctrl-C
|
|
* (或模型自己收尾)来结束. 要恢复上限就设个正数. */
|
|
c->max_steps = 0;
|
|
c->use_tools = 1;
|
|
c->retries = 2;
|
|
c->confirm = 1;
|
|
c->auto_save = 1;
|
|
c->probe = 1;
|
|
c->config_path = NULL;
|
|
}
|
|
|
|
static void set_str(char **dst, const char *val)
|
|
{
|
|
free(*dst);
|
|
*dst = val ? xstrdup(val) : NULL;
|
|
}
|
|
|
|
/* 服务端认识的思考级别. DeepSeek 会把它们映射到 low/high/max 三档
|
|
* (minimal/low→low, medium/high/xhigh→high, max/ultra→max),
|
|
* OpenAI 那边则用 minimal/low/medium/high. 这里只做白名单校验,
|
|
* 不替用户改写 —— 原样发过去, 由服务端决定怎么映射. */
|
|
static const char *const EFFORTS[] = {
|
|
"minimal", "low", "medium", "high", "xhigh", "max", "ultra", NULL
|
|
};
|
|
|
|
int config_effort_valid(const char *s)
|
|
{
|
|
if (!s || !*s) return 0;
|
|
for (size_t i = 0; EFFORTS[i]; i++)
|
|
if (strcmp(s, EFFORTS[i]) == 0) return 1;
|
|
return 0;
|
|
}
|
|
|
|
const char *config_effort_list(void)
|
|
{
|
|
static char buf[128];
|
|
if (!buf[0]) {
|
|
size_t n = 0;
|
|
for (size_t i = 0; EFFORTS[i]; i++)
|
|
n += (size_t)snprintf(buf + n, sizeof buf - n, "%s%s",
|
|
i ? "/" : "", EFFORTS[i]);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
const char *config_thinking_name(int thinking)
|
|
{
|
|
if (thinking == THINKING_ON) return t("cfg.thinking.on");
|
|
if (thinking == THINKING_OFF) return t("cfg.thinking.off");
|
|
return t("cfg.thinking.auto");
|
|
}
|
|
|
|
int config_parse_thinking(const char *s, int *out)
|
|
{
|
|
if (!s) return -1;
|
|
if (!*s || strcmp(s, "auto") == 0) { *out = THINKING_AUTO; return 0; }
|
|
if (strcmp(s, "on") == 0 || strcmp(s, "true") == 0 ||
|
|
strcmp(s, "enabled") == 0) { *out = THINKING_ON; return 0; }
|
|
if (strcmp(s, "off") == 0 || strcmp(s, "false") == 0 ||
|
|
strcmp(s, "disabled") == 0) { *out = THINKING_OFF; return 0; }
|
|
return -1;
|
|
}
|
|
|
|
/* 配置文件里 thinking 既可以是布尔也可以是字符串:
|
|
* `"thinking": true` 比 `"thinking": "on"` 自然, 两种都收. */
|
|
static int load_thinking(LigentConfig *c, const JsonValue *v,
|
|
const char *path, char *errbuf, size_t errlen)
|
|
{
|
|
if (v->type == JSON_BOOL) {
|
|
c->thinking = v->u.boolean ? THINKING_ON : THINKING_OFF;
|
|
return 0;
|
|
}
|
|
const char *s = json_string_value(v);
|
|
if (s && config_parse_thinking(s, &c->thinking) == 0) return 0;
|
|
snprintf(errbuf, errlen, "%s", tf("cfg.err.thinking_type", "path", path, NULL));
|
|
return -1;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 配置文件 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* ---- 定位二进制所在目录 ----
|
|
*
|
|
* 关键点: 路径要"跟着用户看到的那个文件走". 如果 ligent 是个软链接,
|
|
* 用户是把它当 ./bin/ligent 用的, 配置就该在 ./bin/ 旁边, 而不是被
|
|
* 悄悄挪到源文件所在的目录去.
|
|
*
|
|
* 所以这里优先用 argv[0] (不解析最后一段的软链接), 只在拿不到时
|
|
* 才退回 /proc/self/exe —— 那个会解析软链接. */
|
|
|
|
/* 确认 cand 确实指向我们自己的可执行文件.
|
|
* 用 inode 比对, 避免 $PATH 里恰好有个同名的别的东西把目录带偏.
|
|
* 读不到 /proc/self/exe 时不做检查 (非 Linux 平台). */
|
|
static int is_self(const char *cand)
|
|
{
|
|
struct stat a, b;
|
|
if (!cand || stat(cand, &a) != 0) return 0; /* stat 会跟随软链接 */
|
|
if (stat("/proc/self/exe", &b) != 0) return 1; /* 无法比对, 放行 */
|
|
return a.st_dev == b.st_dev && a.st_ino == b.st_ino;
|
|
}
|
|
|
|
/* 在 $PATH 里查找可执行且确实是我们自己的文件, 返回完整路径 (malloc)或 NULL */
|
|
static char *search_path(const char *name)
|
|
{
|
|
const char *path = getenv("PATH");
|
|
if (!path || !*path) return NULL;
|
|
|
|
char *copy = xstrdup(path);
|
|
char *save = NULL;
|
|
char *dir = strtok_r(copy, ":", &save);
|
|
char *found = NULL;
|
|
while (dir) {
|
|
char *cand = xasprintf("%s/%s", *dir ? dir : ".", name);
|
|
if (access(cand, X_OK) == 0 && is_self(cand)) { found = cand; break; }
|
|
free(cand);
|
|
dir = strtok_r(NULL, ":", &save);
|
|
}
|
|
free(copy);
|
|
return found;
|
|
}
|
|
|
|
/* 取路径中最后一段之前的部分; 没有 '/' 时返回 NULL */
|
|
static char *dir_of(const char *path)
|
|
{
|
|
const char *slash = path ? strrchr(path, '/') : NULL;
|
|
if (!slash) return NULL;
|
|
if (slash == path) return xstrdup("/");
|
|
return xstrndup(path, (size_t)(slash - path));
|
|
}
|
|
|
|
/* 取所在目录并规范化 (解析目录部分里的软链接、. 和 ..),
|
|
* 但刻意不解析最后一段 —— 那正是"软链接所在目录"的关键 */
|
|
static char *dir_of_canonical(const char *path)
|
|
{
|
|
char *dir = dir_of(path);
|
|
if (!dir) return NULL;
|
|
|
|
char *canon = realpath(dir, NULL); /* 成功时由 malloc 分配 */
|
|
if (canon) { free(dir); return canon; }
|
|
|
|
/* 目录不存在等情况下退回未规范化的结果, 但要保证是绝对路径 */
|
|
if (dir[0] == '/') return dir;
|
|
char cwd[PATH_MAX];
|
|
if (getcwd(cwd, sizeof cwd)) {
|
|
char *full = xasprintf("%s/%s", cwd, dir);
|
|
free(dir);
|
|
return full;
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
char *config_binary_dir(const char *argv0)
|
|
{
|
|
/* 优先 argv[0]: 它反映用户实际调用的那个路径 */
|
|
if (argv0 && *argv0) {
|
|
if (strchr(argv0, '/')) {
|
|
if (is_self(argv0)) {
|
|
char *d = dir_of_canonical(argv0);
|
|
if (d) return d;
|
|
}
|
|
} else {
|
|
/* 只是个名字: 去 $PATH 里找 (返回的仍是软链接本身) */
|
|
char *full = search_path(argv0);
|
|
if (full) {
|
|
char *d = dir_of_canonical(full);
|
|
free(full);
|
|
if (d) return d;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 兜底: 问内核可执行文件在哪. 注意这会解析软链接,
|
|
* 只在 argv[0] 给不出答案时使用 */
|
|
char buf[PATH_MAX];
|
|
ssize_t n = readlink("/proc/self/exe", buf, sizeof buf - 1);
|
|
if (n > 0 && (size_t)n < sizeof buf) {
|
|
buf[n] = '\0';
|
|
char *d = dir_of_canonical(buf);
|
|
if (d) return d;
|
|
}
|
|
|
|
/* 都失败就认当前目录, 至少不会崩 */
|
|
return xstrdup(".");
|
|
}
|
|
|
|
char *config_default_path(const char *argv0)
|
|
{
|
|
char *dir = config_binary_dir(argv0);
|
|
char *path = xasprintf("%s/ligent.json", dir ? dir : ".");
|
|
free(dir);
|
|
return path;
|
|
}
|
|
|
|
/* ---- 查找要加载的配置文件 ---- */
|
|
|
|
/* 只认两个来源: 命令行的显式指定, 以及二进制目录下的 ligent.json.
|
|
* 不做用户级 / 系统级 / 当前工作目录的查找. */
|
|
static char *find_config_file(const char *explicit_path, const char *argv0)
|
|
{
|
|
if (explicit_path && *explicit_path) {
|
|
/* 显式指定的路径不做存在性判断, 读不到会明确报错 */
|
|
return xstrdup(explicit_path);
|
|
}
|
|
|
|
const char *env = getenv("LIGENT_CONFIG");
|
|
if (env && *env) return xstrdup(env);
|
|
|
|
char *path = config_default_path(argv0);
|
|
if (access(path, R_OK) == 0) return path;
|
|
free(path); /* 不存在就表示"用内置默认值" */
|
|
return NULL;
|
|
}
|
|
|
|
int config_load_file(LigentConfig *c, const char *path,
|
|
char *errbuf, size_t errlen)
|
|
{
|
|
size_t len = 0;
|
|
char *text = read_entire_file(path, &len);
|
|
if (!text) {
|
|
snprintf(errbuf, errlen, "%s",
|
|
tf("cfg.err.read_failed", "path", path, NULL));
|
|
return -1;
|
|
}
|
|
char jerr[256];
|
|
JsonValue *root = json_parse_ui(text, jerr, sizeof jerr);
|
|
free(text);
|
|
if (!root) {
|
|
snprintf(errbuf, errlen, "%s",
|
|
tf("cfg.err.bad_json", "path", path, "error", jerr, NULL));
|
|
return -1;
|
|
}
|
|
if (root->type != JSON_OBJECT) {
|
|
json_free(root);
|
|
snprintf(errbuf, errlen, "%s",
|
|
tf("cfg.err.not_object", "path", path, NULL));
|
|
return -1;
|
|
}
|
|
|
|
const char *s;
|
|
if ((s = json_get_string(root, "base_url"))) set_str(&c->base_url, s);
|
|
if ((s = json_get_string(root, "api_key"))) {
|
|
char *expanded = expand_env(s);
|
|
if (*expanded) c->api_key_from_file = 1;
|
|
set_str(&c->api_key, expanded);
|
|
free(expanded);
|
|
}
|
|
if ((s = json_get_string(root, "model"))) set_str(&c->model, s);
|
|
/* 空字符串视为"没写": 内置提示词里带着工具使用规范,
|
|
* 静默丢掉会让 agent 行为明显变差; 而且下发一条空的 system 消息
|
|
* 既浪费又没有意义. */
|
|
if ((s = json_get_string(root, "system"))) {
|
|
if (*s) set_str(&c->system, s);
|
|
}
|
|
|
|
JsonValue *v;
|
|
if ((v = json_object_get(root, "temperature")) && v->type == JSON_NUMBER)
|
|
c->temperature = v->u.number;
|
|
if ((v = json_object_get(root, "max_tokens")) && v->type == JSON_NUMBER)
|
|
c->max_tokens = (long)v->u.number;
|
|
if ((v = json_object_get(root, "thinking"))) {
|
|
if (load_thinking(c, v, path, errbuf, errlen) != 0) {
|
|
json_free(root);
|
|
return -1;
|
|
}
|
|
}
|
|
if ((s = json_get_string(root, "reasoning_effort")) && *s) {
|
|
if (!config_effort_valid(s)) {
|
|
snprintf(errbuf, errlen, "%s",
|
|
tf("cfg.err.bad_effort", "path", path,
|
|
"list", config_effort_list(), NULL));
|
|
json_free(root);
|
|
return -1;
|
|
}
|
|
set_str(&c->reasoning_effort, s);
|
|
}
|
|
if ((v = json_object_get(root, "timeout")) && v->type == JSON_NUMBER)
|
|
c->timeout = (long)v->u.number;
|
|
if ((v = json_object_get(root, "stream")))
|
|
c->stream = json_bool_value(v, c->stream);
|
|
if ((v = json_object_get(root, "max_steps")) && v->type == JSON_NUMBER)
|
|
c->max_steps = (int)v->u.number;
|
|
if ((v = json_object_get(root, "tools")))
|
|
c->use_tools = json_bool_value(v, c->use_tools);
|
|
if ((v = json_object_get(root, "retries")) && v->type == JSON_NUMBER)
|
|
c->retries = (int)v->u.number;
|
|
if ((v = json_object_get(root, "confirm")))
|
|
c->confirm = json_bool_value(v, c->confirm);
|
|
if ((v = json_object_get(root, "auto_save")))
|
|
c->auto_save = json_bool_value(v, c->auto_save);
|
|
if ((v = json_object_get(root, "probe")))
|
|
c->probe = json_bool_value(v, c->probe);
|
|
|
|
json_free(root);
|
|
free(c->config_path);
|
|
c->config_path = xstrdup(path);
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 环境变量 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void env_str(char **dst, const char *name)
|
|
{
|
|
const char *v = getenv(name);
|
|
if (v && *v) set_str(dst, v);
|
|
}
|
|
|
|
void config_apply_env(LigentConfig *c)
|
|
{
|
|
env_str(&c->base_url, "LIGENT_BASE_URL");
|
|
env_str(&c->model, "LIGENT_MODEL");
|
|
{
|
|
const char *v = getenv("LIGENT_SYSTEM");
|
|
if (v && *v) set_str(&c->system, v);
|
|
}
|
|
|
|
/* 厂商专属的环境变量只在"确实在用这家"时才读.
|
|
*
|
|
* 否则会出这种事: 配置文件里写的是 OpenRouter 的 key,
|
|
* 而 shell 里残留的 DEEPSEEK_API_KEY 把它悄悄顶掉,
|
|
* 结果 A 家的密钥被发给了 B 家 —— 对方只会回一个含糊的 401.
|
|
* 用户明明配对了却怎么都调不通, 排查方向还会被带偏. */
|
|
const char *url = c->base_url ? c->base_url : "";
|
|
int use_deepseek = strstr(url, "deepseek") != NULL;
|
|
int use_openai = strstr(url, "api.openai.com") != NULL;
|
|
|
|
/* api_key 优先级:
|
|
* LIGENT_API_KEY (显式覆盖) > 配置文件 > 对应厂商的环境变量兜底 */
|
|
const char *key = getenv("LIGENT_API_KEY");
|
|
if (key && *key) {
|
|
set_str(&c->api_key, key);
|
|
c->api_key_env = "LIGENT_API_KEY";
|
|
} else if (!c->api_key || !c->api_key[0]) {
|
|
const char *k = NULL;
|
|
const char *src = NULL;
|
|
if (use_deepseek) { k = getenv("DEEPSEEK_API_KEY"); src = "DEEPSEEK_API_KEY"; }
|
|
else if (use_openai) { k = getenv("OPENAI_API_KEY"); src = "OPENAI_API_KEY"; }
|
|
if (k && *k) { set_str(&c->api_key, k); c->api_key_env = src; }
|
|
}
|
|
|
|
/* OPENAI_BASE_URL 同理, 只在 base_url 还是内置默认值 (用户没显式配)时兜底 */
|
|
if (use_openai || strcmp(url, DEFAULT_BASE_URL) == 0) {
|
|
const char *bu = getenv("OPENAI_BASE_URL");
|
|
if (bu && *bu) set_str(&c->base_url, bu);
|
|
}
|
|
|
|
const char *v;
|
|
if ((v = getenv("LIGENT_TEMPERATURE")) && *v) c->temperature = atof(v);
|
|
if ((v = getenv("LIGENT_MAX_TOKENS")) && *v) c->max_tokens = atol(v);
|
|
if ((v = getenv("LIGENT_TIMEOUT")) && *v) c->timeout = atol(v);
|
|
if ((v = getenv("LIGENT_MAX_STEPS")) && *v) c->max_steps = atoi(v);
|
|
if ((v = getenv("LIGENT_STREAM")) && *v)
|
|
c->stream = !(strcmp(v, "0") == 0 || strcmp(v, "false") == 0);
|
|
if ((v = getenv("LIGENT_TOOLS")) && *v)
|
|
c->use_tools = !(strcmp(v, "0") == 0 || strcmp(v, "false") == 0);
|
|
if ((v = getenv("LIGENT_RETRIES")) && *v) c->retries = atoi(v);
|
|
if ((v = getenv("LIGENT_CONFIRM")) && *v)
|
|
c->confirm = !(strcmp(v, "0") == 0 || strcmp(v, "false") == 0);
|
|
if ((v = getenv("LIGENT_AUTO_SAVE")) && *v)
|
|
c->auto_save = !(strcmp(v, "0") == 0 || strcmp(v, "false") == 0);
|
|
if ((v = getenv("LIGENT_PROBE")) && *v)
|
|
c->probe = !(strcmp(v, "0") == 0 || strcmp(v, "false") == 0);
|
|
/* 环境变量里写错了不报错、直接忽略: 环境变量是临时开关,
|
|
* 为它把整个程序拦下来太粗暴. 配置文件和命令行则严格校验. */
|
|
if ((v = getenv("LIGENT_THINKING")) && *v) {
|
|
int t;
|
|
if (config_parse_thinking(v, &t) == 0) c->thinking = t;
|
|
}
|
|
if ((v = getenv("LIGENT_REASONING_EFFORT")) && *v) {
|
|
if (config_effort_valid(v)) set_str(&c->reasoning_effort, v);
|
|
}
|
|
}
|
|
|
|
int config_init(LigentConfig *c, const char *explicit_path, const char *argv0,
|
|
char *errbuf, size_t errlen)
|
|
{
|
|
config_defaults(c);
|
|
if (errbuf && errlen) errbuf[0] = '\0';
|
|
|
|
/* 记下二进制目录: 配置和以后任何数据都只放这里 */
|
|
free(c->config_dir);
|
|
c->config_dir = config_binary_dir(argv0);
|
|
|
|
char *path = find_config_file(explicit_path, argv0);
|
|
if (path) {
|
|
if (config_load_file(c, path, errbuf, errlen) != 0) {
|
|
if (explicit_path && *explicit_path) { free(path); return -1; }
|
|
/* 约定位置的配置坏了, 也不该直接拦住用户 */
|
|
if (errbuf && errlen)
|
|
fprintf(stderr, "%s\n",
|
|
tf("cfg.err.parse_ignored", "error", errbuf, NULL));
|
|
if (errbuf && errlen) errbuf[0] = '\0';
|
|
}
|
|
free(path);
|
|
}
|
|
|
|
config_apply_env(c);
|
|
return 0;
|
|
}
|
|
|
|
void config_free(LigentConfig *c)
|
|
{
|
|
if (!c) return;
|
|
free(c->base_url);
|
|
free(c->api_key);
|
|
free(c->model);
|
|
free(c->system);
|
|
free(c->reasoning_effort);
|
|
free(c->config_dir);
|
|
free(c->config_path);
|
|
memset(c, 0, sizeof *c);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 展示 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void mask_key(const char *key, char *out, size_t outlen)
|
|
{
|
|
if (!key || !*key) { snprintf(out, outlen, "%s", t("cfg.val.unset")); return; }
|
|
size_t n = strlen(key);
|
|
if (n <= 8) { snprintf(out, outlen, "%s", t("cfg.val.empty_key")); return; }
|
|
char num[32];
|
|
snprintf(num, sizeof num, "%zu", n);
|
|
snprintf(out, outlen, "%.4s...%.4s %s", key, key + n - 4,
|
|
tf("cfg.val.chars", "n", num, NULL));
|
|
}
|
|
|
|
/* 字段名按显示宽度补齐. 中文标签一个字占 2 列, 用 %-13s 会整体顶歪,
|
|
* 而英文标签没有这个问题 —— 所以宽度得算, 不能写死空格 (同 doctor 的 label). */
|
|
static void dump_field(const char *lbl, const char *fmt, ...)
|
|
{
|
|
size_t w = display_width(lbl, strlen(lbl));
|
|
fputs(lbl, stdout);
|
|
while (w < 13) { putchar(' '); w++; }
|
|
fputs(": ", stdout);
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vprintf(fmt, ap);
|
|
va_end(ap);
|
|
putchar('\n');
|
|
}
|
|
|
|
void config_dump(const LigentConfig *c)
|
|
{
|
|
char masked[128], num[32];
|
|
mask_key(c->api_key, masked, sizeof masked);
|
|
|
|
dump_field("base_url", "%s", c->base_url ? c->base_url : "");
|
|
dump_field("model", "%s", c->model ? c->model : "");
|
|
dump_field("api_key", "%s", masked);
|
|
|
|
if (c->temperature < 0) {
|
|
dump_field("temperature", "%s", t("cfg.val.server_default"));
|
|
} else {
|
|
snprintf(num, sizeof num, "%.2f", c->temperature);
|
|
dump_field("temperature", "%s", num);
|
|
}
|
|
|
|
if (c->max_tokens > 0) {
|
|
snprintf(num, sizeof num, "%ld", c->max_tokens);
|
|
dump_field("max_tokens", "%s", num);
|
|
} else {
|
|
dump_field("max_tokens", "%s", t("cfg.val.server_default"));
|
|
}
|
|
|
|
dump_field("thinking", "%s", config_thinking_name(c->thinking));
|
|
dump_field(t("cfg.dump.label.effort"), "%s",
|
|
(c->reasoning_effort && *c->reasoning_effort)
|
|
? c->reasoning_effort : t("cfg.val.server_default"));
|
|
|
|
snprintf(num, sizeof num, "%ld", c->timeout);
|
|
dump_field("timeout", "%s", tf("cfg.val.seconds", "n", num, NULL));
|
|
dump_field("stream", "%s", t(c->stream ? "cfg.val.yes" : "cfg.val.no"));
|
|
if (c->max_steps > 0) {
|
|
snprintf(num, sizeof num, "%d", c->max_steps);
|
|
dump_field("max_steps", "%s", num);
|
|
} else {
|
|
dump_field("max_steps", "%s", t("cfg.val.unlimited"));
|
|
}
|
|
dump_field("tools", "%s",
|
|
t(c->use_tools ? "cfg.val.enabled" : "cfg.val.disabled"));
|
|
|
|
snprintf(num, sizeof num, "%d", c->retries);
|
|
dump_field("retries", "%s", tf("cfg.val.times", "n", num, NULL));
|
|
dump_field("confirm", "%s",
|
|
t(c->confirm ? "cfg.val.confirm_on" : "cfg.val.disabled"));
|
|
dump_field("auto_save", "%s",
|
|
t(c->auto_save ? "cfg.val.auto_save_on" : "cfg.val.disabled"));
|
|
dump_field("probe", "%s",
|
|
t(c->probe ? "cfg.val.probe_on" : "cfg.val.disabled"));
|
|
|
|
snprintf(num, sizeof num, "%zu", http_log_size());
|
|
dump_field(t("cfg.dump.label.log"), "%s", tf("cfg.val.log", "n", num, NULL));
|
|
dump_field("system", "%s",
|
|
c->system ? c->system : t("cfg.val.builtin_system"));
|
|
dump_field("language", "%s", i18n_lang_name());
|
|
|
|
if (c->config_path) {
|
|
dump_field("config_file", "%s", c->config_path);
|
|
} else if (c->config_dir) {
|
|
char *p = xasprintf("%s/ligent.json", c->config_dir);
|
|
dump_field("config_file", "%s",
|
|
tf("cfg.dump.config_file_missing", "path", p, NULL));
|
|
free(p);
|
|
} else {
|
|
dump_field("config_file", "%s", t("cfg.dump.config_file_none"));
|
|
}
|
|
}
|
|
|
|
/* 密钥来源的可读标签 (诊断用). 诊断代码不该去 strstr() 中文文案
|
|
* 判断来源 —— 界面语言一变那个判断就悄悄失效了. */
|
|
const char *config_api_key_source(const LigentConfig *c)
|
|
{
|
|
if (c->api_key_env) return tf("cfg.src.env", "name", c->api_key_env, NULL);
|
|
if (c->api_key_from_cli) return t("cfg.src.cli");
|
|
if (c->api_key_from_file) return t("cfg.src.file");
|
|
return t("cfg.val.unknown");
|
|
}
|
|
|