Skip to the content.

配置导入导出

标签分类与导入导出 回答的是「代码源清单怎么搬」,这一页回答的是 「这台机器是怎么配的,怎么整体搬到另一台机器上」:设置项(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.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 进来。