配置导入导出
标签分类与导入导出 回答的是「代码源清单怎么搬」,这一页回答的是 「这台机器是怎么配的,怎么整体搬到另一台机器上」:设置项(AI 审核 + GitLab 私有仓库令牌) 与全部代码源装进同一个 JSON 文件。
分界线在这里:代码源是可再生的输入——重填一遍也就几分钟;设置项里装的是接口地址与凭据, 重填要靠回忆,而回忆出来的往往是错的那个模型名。把两者放进同一个文件,是因为「换机器」 从来不会只缺其中一样。
一个文件里有什么
GET /api/settings/export 导出 kind=code-association.config、version=1 的结构:
{
"kind": "code-association.config",
"version": 1,
"exportedAt": "2026-10-30T03:20:00Z",
"secretsIncluded": false,
"settings": {
"ai": { "enabled": true, "baseUrl": "https://api.deepseek.com/v1", "model": "deepseek-chat",
"maxImpacts": 12, "timeoutSeconds": 60, "defaultForRuns": true,
"comments": true, "maxComments": 12, "reflection": true },
"gitlab": {}
},
"sources": [
{ "name": "alpha", "type": "LOCAL_DIR", "location": "/work/alpha",
"defaultRef": "main", "credentialEnv": null, "tags": ["core"] }
]
}
| 字段 | 含义 |
|---|---|
kind |
固定 code-association.config;导入时靠它区分「整份配置」与「只有代码源」 |
version |
结构版本,当前为 1;比它新的文件会被拒绝,因为旧版本不认识新字段 |
exportedAt |
导出时刻(UTC,ISO 8601),给人看,也方便排查「这份文件是哪天导的」 |
secretsIncluded |
这份文件里有没有密钥,与导出时有没有勾选「包含密钥」同值 |
settings.ai |
AI 审核设置:enabled / baseUrl / model / maxImpacts / timeoutSeconds / defaultForRuns / comments / maxComments / reflection;含密钥时另有 apiKey |
settings.gitlab |
GitLab 令牌:不勾「包含密钥」或本机没保存过令牌时是空对象 {};含密钥时为 {"token": "..."} |
sources[] |
代码源,与 标签分类与导入导出 的导出结构完全同构 |
sources 同样不导出 id / status / message / createdAt / lastSyncedAt:
这些只对产生它们的那台机器有意义,带过去只会覆盖导入方自己的健康记录。
密钥策略
「包含密钥」勾选框默认不勾。这条默认值是有代价的:勾上才能一次搬完,不勾就得在新机器上 把 API Key 与令牌重填两次。
代价换的是另一件事——备份文件的实际旅程是聊天工具、工单附件、U 盘、同步盘。默认把 apiKey 与
token 写进文件,等于让每一次「随手备份」都变成一次凭据外泄,而且外泄之后不会有任何提示:
文件在别人手里,本机看起来一切正常。
| 场景 | 文件里 | 导入后的本机 |
|---|---|---|
| 不勾「包含密钥」导出 | 无 apiKey / token,secretsIncluded=false |
本机已有密钥保持不变,不会被清空;gitlab: {} 也不会被当成「清空令牌」(见「导入语义」) |
| 勾选「包含密钥」导出 | 含 apiKey / token,secretsIncluded=true |
文件里有值就写入本机,响应里 secretsImported=true |
| 导入一份代码源导出文件 | 没有 settings 块 |
本机密钥完全不动 |
「导入不删密钥」是同一件事的另一面:一份不含密钥的文件只能说明「这份备份没打算带密钥」, 不能说明「我要把令牌清掉」。要清空令牌仍然只有设置页的「清除」一个入口。
导出响应带 Cache-Control: no-store:这份响应可能装着密钥,不该留在浏览器或中间代理的缓存里。
导入语义
POST /api/settings/import,请求体就是解析后的那份 JSON——不是 multipart,也不需要先上传文件。
导进来之后设置项立即生效,不用重启:它走的是与设置页保存同一条路径。
设置项:按分组独立应用
设置项按 ai / gitlab 两个分组分别校验、分别应用:
- 一组校验通过 → 立即生效,写进
settings.applied; - 一组校验失败 → 记进
settings.errors(section+ 中文reason),其余分组照常应用; - 一组是空对象 → 视为「没有可应用内容」,不应用、不写本机快照、也不进
applied; - 文件里没写的字段 → 保持本机现值。
第三条正是「不勾包含密钥」时的常态:那份文件里 settings.gitlab 是 {},于是导入结果通常
就是 applied: ["ai"],只有文件里真的带了 token 才会多出 gitlab。
为什么空对象不能「照常保存一遍」:保存 GitLab 令牌就是写出 {data-dir}/gitlab-settings.json,
而这份快照会覆盖 application.yml 的基线。让一份不含密钥的共享文件在导入方机器上凭空写出一份
空快照,等于把对方原本从环境变量读到的令牌静默清掉——文件里明明什么都没写,配置却变了。
「空」只能表示「这份文件没带」,不能表示「把它清空」;清空仍然只有设置页的「清除」一个入口。
部分成功是这里的核心语义,不是妥协。 整份文件回滚听起来更安全,实际效果是把一次填错的 接口地址变成「代码源也没导进来」——用户会重试,而重试用的还是同一个文件,于是永远导不进去。 相反,能用的先落地、坏的那组明确报出来,用户改一处再导一次就够了。
「文件里没写的字段保持现值」同理:一份在「逐条评审意见」上线之前导出的文件没有
comments / maxComments / reflection 三个字段,导回来不该把它们重置成默认值——
那等于用一份旧备份悄悄改掉了本机的配置。
代码源:沿用既有的合并规则
两个入口走的是同一套实现,所以判定与合并规则一字不差:
| 情况 | 行为 |
|---|---|
| 已存在(type + location 相同) | 更新:标签取并集,defaultRef / credentialEnv 仅在传入值非空时覆盖 |
| 不存在 | 新建,name 为空时按位置推导 |
| 单条非法 | 记入 sources.errors(index / name / reason),继续处理其余条目 |
| 本地目录不存在 | 仍然导入,置 status = MISSING,列表显示「目录缺失」 |
每条规则背后的理由(为什么标签是并集、为什么空值不覆盖、为什么坏条目只记录不中断)见 标签分类与导入导出。
导入结果与错误报告
{
"kind": "code-association.config",
"version": 1,
"settings": {
"applied": ["ai"],
"errors": [],
"secretsImported": false
},
"sources": { "total": 3, "created": 1, "updated": 2, "skipped": 0,
"errors": [{ "index": 2, "name": "broken", "reason": "不支持的 type:NOT_A_TYPE" }] }
}
| 情况 | settings |
sources |
HTTP |
|---|---|---|---|
| 全部成功 | applied 含全部有内容且校验通过的分组(不含密钥的默认文件里只有 ai),errors 为空 |
计数正常,errors 为空 |
200 |
| 某个设置分组校验失败 | 该组不在 applied 里,errors 里一条 section |
照常导入 | 200 |
某个分组是空对象(如 gitlab: {}) |
该组不进 applied,本机的令牌与 gitlab-settings.json 都不动 |
照常导入 | 200 |
| 某条代码源非法 | 照常应用 | 该条进 errors,其余照常 |
200 |
文件是代码源导出(kind=code-association.sources) |
applied 为空 |
只处理代码源 | 200 |
kind 不认识 / version 比 1 新 |
不导入 | 不导入 | 400 + 中文原因 |
某个分组校验失败时,errors 里那一行长这样——reason 是给人看的中文原因,直接抄自校验器:
{ "section": "ai", "reason": "接口地址需以 http:// 或 https:// 开头" }
最后一行是唯一「什么都没发生」的情况,也是唯一回 400 的情况:连「这是什么文件」都还没确定时, 猜着导入比拒绝更危险——一份来自更新版本的文件可能带着旧版本会误解的字段。
两处 errors 的形状不同,因为要指到能改的地方:settings.errors 用 section 定位到分组,
sources.errors 用 index + name 定位到具体那一行。只说「有 1 条失败」等于没说。
界面按 settings.applied / settings.errors / sources.* 三块分别渲染,而不是一句
「导入成功」或「导入失败」:部分成功是常态,用户需要知道哪些生效了、哪些没有。
幂等
导入一份刚从本机导出的文件,结果必须是:每个有内容的设置分组以同样的值重写一遍
(gitlab: {} 直接跳过),代码源全部计为 updated,标签、默认分支与凭据变量名都不变。
这条比「看起来没变」严格,因为它同时验证了两件事:导出的字段名与导入的字段名一一对上, 代码源的身份判定没有漂移。字段名写错时通常不会报错——导入会把那个字段当成「文件里没写」, 保持现值,表面上一切正常,直到某天在新机器上发现配置少了一半。所以这条值得单独验一次, 而不是只看导入返回了 200。
API 参考
| 接口 | 说明 |
|---|---|
GET /api/settings/export |
下载整份配置;文件名 code-association-config-<yyyyMMdd-HHmmss>.json,响应头 Cache-Control: no-store |
GET /api/settings/export?secrets=1 |
同上,但 ai 里多 apiKey、gitlab 里多 token,secretsIncluded=true |
POST /api/settings/import |
请求体为解析后的 JSON;返回 settings 与 sources 两块结果 |
# 导出(不含密钥)
curl -OJ http://127.0.0.1:9996/api/settings/export
# 导出(含密钥,注意这份文件的落盘位置)
curl -OJ 'http://127.0.0.1:9996/api/settings/export?secrets=1'
# 导入
curl -X POST http://127.0.0.1:9996/api/settings/import \
-H 'Content-Type: application/json' \
--data-binary @code-association-config-20261030-112000.json
与代码源导入导出的区别
| 代码源导入导出 | 配置导入导出(本页) | |
|---|---|---|
| 内容 | 只有代码源注册表 | 设置项 + 全部代码源 |
| 典型用途 | 团队共享一份清单、给同事补几个源 | 换机器、整机迁移、完整备份 |
| 导入方预期 | 「补充」:标签取并集,没写的不动 | 「复原」:这台机器的配置原样搬过去 |
| 密钥 | 从不包含(凭据本身不入库,只导出环境变量名) | 默认不含,勾选「包含密钥」才含 |
文件 kind |
code-association.sources |
code-association.config |
两者不冲突:配置导入接口接受代码源文件(kind=code-association.sources,或只带一个
sources 数组的对象),所以手里的代码源导出文件可以直接从新入口导入,只是 settings.applied
为空、只处理代码源。反方向不在契约内——设置项只从 /api/settings/import 进来。