@roll-agent/core 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/agent-start.js +1 -1
- package/dist/cli/commands/config.js +1 -1
- package/dist/config/helpers.js +1 -1
- package/dist/config/key-codec.d.ts +15 -0
- package/dist/config/key-codec.js +1 -0
- package/dist/config/loader.js +1 -1
- package/dist/config/migration.d.ts +6 -1
- package/dist/config/migration.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{defineCommand as t}from"citty";import{
|
|
1
|
+
import{defineCommand as t}from"citty";import{getAgentEnv as e}from"../../config/helpers.js";import{loadAgentsConfig as r,loadConfig as a}from"../../config/loader.js";import{getAgentLogPath as n,getAgentPid as i,probeAgentEndpoint as s,startAgent as o,stopAgentGracefully as m,waitForAgentReady as p}from"../../registry/process-manager.js";import{AgentStore as l}from"../../registry/store.js";import{log as d}from"../utils/output.js";export default t({meta:{description:"启动 Agent(core-managed HTTP 可由 Roll 托管)"},args:{name:{type:"positional",description:"Agent 名称",required:!0}},async run({args:t}){const{agentsConfig:c}=r(),u=new l(c.dataDir),g=u.findByName(t.name);if(!g)return d.error(`Agent "${t.name}" 未找到`),void(process.exitCode=1);switch(g.runtime.ownership){case"on-demand":return void d.success(`Agent "${t.name}" 为按需模式,无需手动启动。`);case"external-managed":return d.info(`Agent "${t.name}" 由外部服务管理,Roll 不负责启动。`),void("streamable-http"===g.transport.type&&d.info(`端点: ${g.transport.endpoint}`))}const f=i(c.dataDir,g.skill.name);if(void 0!==f)try{return await s(g,{timeoutMs:3e3}),u.updateStatus(g.skill.name,"online"),void d.success(`Agent "${t.name}" 已在运行 (PID: ${String(f)})\n 端点: ${"streamable-http"===g.transport.type?g.transport.endpoint:"n/a"}`)}catch(e){return u.updateStatus(g.skill.name,"error"),d.error(`Agent "${t.name}" 进程存在但不可连接:${e instanceof Error?e.message:String(e)}`),d.info(`日志: ${n(c.dataDir,g.skill.name)}`),void(process.exitCode=1)}let $;u.updateStatus(g.skill.name,"starting");try{const r=e(a().config,g.skill.name);$=o(g,c.dataDir,r),await p(g,{startupTimeoutMs:15e3,probeTimeoutMs:2e3}),u.updateStatus(g.skill.name,"online"),d.success(`Agent "${t.name}" 已启动 (PID: ${String($)})\n 端点: ${"streamable-http"===g.transport.type?g.transport.endpoint:"n/a"}\n 日志: ${n(c.dataDir,g.skill.name)}`)}catch(e){void 0!==$&&await m(c.dataDir,g.skill.name).catch(()=>{}),u.updateStatus(g.skill.name,"error"),d.error(`Agent "${t.name}" 启动失败:${e instanceof Error?e.message:String(e)}`),d.info(`日志: ${n(c.dataDir,g.skill.name)}`),process.exitCode=1}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{defineCommand as o}from"citty";import{readFileSync as t,writeFileSync as r,existsSync as e}from"node:fs";import{resolve as n}from"node:path";import{createInterface as i}from"node:readline/promises";import{stringify as s}from"yaml";import{inspectConfigFile as c,loadConfig as a,parseConfigDocument as l,validateConfigText as
|
|
1
|
+
import{defineCommand as o}from"citty";import{readFileSync as t,writeFileSync as r,existsSync as e}from"node:fs";import{resolve as n}from"node:path";import{createInterface as i}from"node:readline/promises";import{stringify as s}from"yaml";import{inspectConfigFile as c,loadConfig as a,parseConfigDocument as l,validateConfigText as d}from"../../config/loader.js";import{encodePathToYaml as f,normalizeUserPath as u}from"../../config/key-codec.js";import{applyKnownConfigMigrations as g}from"../../config/migration.js";export default o({meta:{description:"管理全局配置"},args:{action:{type:"positional",description:"操作(init/get/set/migrate)",required:!0},key:{type:"positional",description:"配置键(get/set 时使用,点号分隔)",required:!1},value:{type:"positional",description:"配置值(set 时使用)",required:!1}},async run({args:o}){try{if("init"===o.action)return void await v();if("get"===o.action)return void w(o.key);if("set"===o.action)return void S(o.key,o.value);if("migrate"===o.action)return void $();console.error(`✗ 未知操作: ${o.action}。可用: init, get, set, migrate`),process.exitCode=1}catch(o){const t=o instanceof Error?o.message:String(o);console.error(`✗ ${t}`),process.exitCode=1}}});function p(o,t){const r=o?.trim();return r||t}function m({provider:o,model:t,apiKeyEnv:r}){return`llm:\n default-provider: ${o}\n default-model: ${t}\n providers:\n ${o}:\n api-key: \${${r}}\n\nask:\n confirm-threshold: 0.5\n\nagents:\n data-dir: ~/.roll-agent/agents\n`}async function h(){if(!process.stdin.isTTY){const[o,r,e]=t(0,"utf-8").split(/\r?\n/u);return{provider:p(o,"anthropic"),model:p(r,"claude-sonnet-4-20250514"),apiKeyEnv:p(e,"ANTHROPIC_API_KEY")}}const o=i({input:process.stdin,output:process.stderr}),r=p(await o.question("默认 LLM provider (anthropic/openai/qwen) [anthropic]: "),"anthropic"),e=p(await o.question("默认 model [claude-sonnet-4-20250514]: "),"claude-sonnet-4-20250514"),n=p(await o.question("API Key 环境变量名 [ANTHROPIC_API_KEY]: "),"ANTHROPIC_API_KEY");return o.close(),{provider:r,model:e,apiKeyEnv:n}}async function v(){const o=n(process.cwd(),"roll.config.yaml");if(e(o)){const t=c({configPath:o});switch(t.status){case"needs-migration":console.error(`⚠ 现有配置文件需要迁移: ${o}`),console.error(" 建议先运行 `roll config migrate`,再决定是否重新初始化。");break;case"invalid":console.error(`⚠ 现有配置文件存在问题:\n${t.error.message}`)}if(console.error(`⚠ 配置文件已存在: ${o}`),!process.stdin.isTTY)throw new Error("非交互模式下不会覆盖现有配置文件,请手动删除后重试。");const r=i({input:process.stdin,output:process.stderr}),e=await r.question("是否覆盖?(y/N) ");if(r.close(),"y"!==e.toLowerCase())return void console.error("已取消。")}const t=m(await h());d(t,o),r(o,t,"utf-8"),console.log(`✓ 配置文件已创建: ${o}`)}function y(o){const t=new Date;return`${o}.bak.${[t.getFullYear().toString().padStart(4,"0"),(t.getMonth()+1).toString().padStart(2,"0"),t.getDate().toString().padStart(2,"0"),"-",t.getHours().toString().padStart(2,"0"),t.getMinutes().toString().padStart(2,"0"),t.getSeconds().toString().padStart(2,"0")].join("")}`}function $(){const o=c();if("not-found"===o.status)throw new Error("未找到配置文件。请先运行 roll config init");if("valid"===o.status)return void console.log(`✓ 配置文件已是最新格式,无需迁移: ${o.configPath}`);if("invalid"===o.status)throw o.error;const t=l(o.raw,o.configPath),e=g(t);if(!e.ok){const o=e.issues.map(o=>` - ${o.message}`).join("\n");throw new Error(`配置无法自动迁移:\n${o}`)}if(!e.changed)return void console.log(`✓ 配置文件已是最新格式,无需迁移: ${o.configPath}`);const n=y(o.configPath);r(n,o.raw,"utf-8");const i=s(e.document,{lineWidth:0});d(i,o.configPath),r(o.configPath,i,"utf-8"),console.log(`✓ 配置文件已迁移: ${o.configPath}`),console.log(`✓ 已备份原文件: ${n}`);for(const o of e.summary)console.log(` - ${o}`)}function w(o){const{config:t,configPath:r}=a();if(!o)return console.log(JSON.stringify(t,null,2)),void(r&&console.error(`(来源: ${r})`));const e=u(o.split("."));let n=t;for(const t of e){if("object"!=typeof n||null===n)return console.error(`✗ 配置键 "${o}" 不存在`),void(process.exitCode=1);n=n[t]}if(void 0===n)return console.error(`✗ 配置键 "${o}" 不存在`),void(process.exitCode=1);console.log("object"==typeof n?JSON.stringify(n,null,2):String(n))}function S(o,e){if(!o||void 0===e)return console.error("✗ 用法: roll config set <key> <value>"),console.error(" 示例: roll config set ask.confirmThreshold 0.5"),void(process.exitCode=1);const{configPath:n}=a();if(!n)return console.error("✗ 未找到配置文件。请先运行 roll config init"),void(process.exitCode=1);const i=t(n,"utf-8"),c=l(i,n),u=f(o.split(".")),g=u[u.length-1];if(void 0===g)return console.error("✗ 配置键不能为空"),void(process.exitCode=1);let p=c;for(let o=0;o<u.length-1;o++){const t=u[o],r=p[t];("object"!=typeof r||null===r||Array.isArray(r))&&(p[t]={}),p=p[t]}let m=e;"true"===e?m=!0:"false"===e?m=!1:/^\d+(\.\d+)?$/.test(e)&&(m=Number(e)),p[g]=m;const h=s(c,{lineWidth:0});d(h,n),r(n,h,"utf-8"),console.log(`✓ ${o} = ${String(m)}`),console.error(` (已写入: ${n})`)}
|
package/dist/config/helpers.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=/\$\{[^}]+\}/;function
|
|
1
|
+
const e=/\$\{[^}]+\}/;function n(e,n){return e?.[n]}export function getAgentEnv(e,r){return t(n(e.agents.env,r))}export function inspectAgentEnvRequirements(e,i,s,u=process.env){if(!i)return;const o=t(n(s,e)),c=[...r(i.required,!0,o,u),...r(i.optional,!1,o,u)];return 0!==c.length?{items:c,missingRequired:c.filter(e=>e.required&&"missing"===e.source),processEnvOnlyRequired:c.filter(e=>e.required&&"process.env"===e.source)}:void 0}function r(e,n,r,t){return e?e.map(e=>{const i=r?.[e.name];if(void 0!==i&&i.length>0)return{...e,required:n,source:"agents.env"};const s=t[e.name];return"string"==typeof s&&s.length>0?{...e,required:n,source:"process.env"}:e.default?{...e,required:n,source:"default"}:{...e,required:n,source:"missing"}}):[]}function t(e){if(!e)return;const n=Object.entries(e).filter(([,e])=>i(e));return 0!==n.length?Object.fromEntries(n):void 0}function i(n){return"string"==typeof n&&n.length>0&&!e.test(n)}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type KeyCodecNode = {
|
|
2
|
+
readonly kind: "object";
|
|
3
|
+
readonly fields: Readonly<Record<string, KeyCodecNode>>;
|
|
4
|
+
} | {
|
|
5
|
+
readonly kind: "record";
|
|
6
|
+
readonly value: KeyCodecNode;
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: "leaf";
|
|
9
|
+
};
|
|
10
|
+
export declare const CONFIG_KEY_CODEC: KeyCodecNode;
|
|
11
|
+
export declare function kebabToCamel(key: string): string;
|
|
12
|
+
export declare function camelToKebab(key: string): string;
|
|
13
|
+
export declare function decodeFromYaml(value: unknown, node?: KeyCodecNode): unknown;
|
|
14
|
+
export declare function encodePathToYaml(parts: readonly string[]): string[];
|
|
15
|
+
export declare function normalizeUserPath(parts: readonly string[]): string[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e={kind:"leaf"},r={kind:"object",fields:{apiKey:e,baseUrl:e}};export const CONFIG_KEY_CODEC={kind:"object",fields:{llm:{kind:"object",fields:{defaultProvider:e,defaultModel:e,providers:{kind:"record",value:r}}},ask:{kind:"object",fields:{llmModel:e,confirmThreshold:e}},agents:{kind:"object",fields:{dataDir:e,env:{kind:"record",value:{kind:"record",value:e}}}}}};export function kebabToCamel(e){return e.replace(/-([a-z])/g,(e,r)=>r.toUpperCase())}export function camelToKebab(e){return e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}function o(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}export function decodeFromYaml(r,n=CONFIG_KEY_CODEC){if(Array.isArray(r))return r.map(e=>decodeFromYaml(e,n));if(!o(r))return r;if("object"===n.kind){const o={};for(const[t,a]of Object.entries(r)){const r=kebabToCamel(t),c=n.fields[r]??e;o[r]=decodeFromYaml(a,c)}return o}if("record"===n.kind){const e={};for(const[o,t]of Object.entries(r))e[o]=decodeFromYaml(t,n.value);return e}return r}function n(r,o){const n=[];let t=CONFIG_KEY_CODEC;for(const a of r)if("object"===t.kind){const r=kebabToCamel(a),c=t.fields[r];c?(n.push("camel"===o?r:camelToKebab(r)),t=c):(n.push(a),t=e)}else"record"===t.kind?(n.push(a),t=t.value):n.push(a);return n}export function encodePathToYaml(e){return n(e,"kebab")}export function normalizeUserPath(e){return n(e,"camel")}
|
package/dist/config/loader.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as n,existsSync as r}from"node:fs";import{resolve as t}from"node:path";import{parse as o}from"yaml";import{agentsConfigSchema as e,rollConfigSchema as i}from"./schema.js";import{DEFAULT_CONFIG as a,CONFIG_FILE_NAMES as s}from"./defaults.js";import{
|
|
1
|
+
import{readFileSync as n,existsSync as r}from"node:fs";import{resolve as t}from"node:path";import{parse as o}from"yaml";import{agentsConfigSchema as e,rollConfigSchema as i}from"./schema.js";import{DEFAULT_CONFIG as a,CONFIG_FILE_NAMES as s}from"./defaults.js";import{decodeFromYaml as f}from"./key-codec.js";import{detectKnownConfigMigrations as c,formatConfigMigrationError as u}from"./migration.js";function g(n){if("string"==typeof n)return n.replace(/\$\{([^}]+)\}/g,(n,r)=>process.env[r]??n);if(Array.isArray(n))return n.map(g);if(l(n)){const r={};for(const[t,o]of Object.entries(n))r[t]=g(o);return r}return n}function l(n){return"object"==typeof n&&null!==n&&!Array.isArray(n)}function d(n){return l(n)&&"number"==typeof n.line&&"number"==typeof n.col}function p(n){return n instanceof Error&&"linePos"in n&&Array.isArray(n.linePos)&&n.linePos.length>0&&n.linePos.every(d)}function h(n,r){const t=`Invalid YAML syntax in config file: ${n}`;if(!(r instanceof Error))return`${t}\n${String(r)}`;if(p(r)){const[n]=r.linePos;if(n)return`${t} at line ${n.line}, column ${n.col}\n${r.message}`}return`${t}\n${r.message}`}function m(n){let o=t(n);const e=t("/");for(;o!==e;){for(const n of s){const e=t(o,n);if(r(e))return e}const n=t(o,"..");if(n===o)break;o=n}}function v(n){if(n.startsWith("~/")){const r=process.env.HOME??process.env.USERPROFILE??"";return t(r,n.slice(2))}return n}function w(n){return{...n,agents:{...n.agents,dataDir:v(n.agents.dataDir)}}}const $={notFound:"not-found",valid:"valid",needsMigration:"needs-migration",invalid:"invalid"};export function parseConfigDocument(n,r){let t;try{t=o(n)}catch(n){throw new Error(h(r,n),{cause:n instanceof Error?n:void 0})}if(!l(t))throw new Error(`Invalid config file: ${r} (expected YAML object)`);return t}function y(n,r,t={}){const o=parseConfigDocument(n,r),e=c(o,t);if(e.needsMigration)throw new Error(u(r,e));return o}export function validateConfigText(n,r){const t=y(n,r),o=g(f(t));if(!l(o))throw new Error(`Invalid config file: ${r} (expected YAML object)`);const e=C(a,o),s=i.safeParse(e);if(!s.success){const n=s.error.issues.map(n=>` - ${n.path.join(".")}: ${n.message}`).join("\n");throw new Error(`Config validation failed (${r}):\n${n}`)}return w(s.data)}function P(n,r){const t=y(n,r,{scope:"agents"}),o=g(f(t));if(!l(o))throw new Error(`Invalid config file: ${r} (expected YAML object)`);const i=l(o.agents)?o.agents:{},s=C(a.agents,i),c=e.safeParse(s);if(!c.success){const n=c.error.issues.map(n=>` - agents.${n.path.join(".")}: ${n.message}`).join("\n");throw new Error(`Config validation failed (${r}):\n${n}`)}return{...c.data,dataDir:v(c.data.dataDir)}}export function loadConfig(t={}){const o=resolveConfigPath(t);if(!o)return{config:w(a),configPath:void 0};if(!r(o))throw new Error(`Config file not found: ${o}`);return{config:validateConfigText(n(o,"utf-8"),o),configPath:o}}export function loadAgentsConfig(t={}){const o=resolveConfigPath(t);if(!o)return{agentsConfig:{...a.agents,dataDir:v(a.agents.dataDir)},configPath:void 0};if(!r(o))throw new Error(`Config file not found: ${o}`);return{agentsConfig:P(n(o,"utf-8"),o),configPath:o}}export function resolveConfigPath(n={}){const{configPath:r,cwd:t=process.cwd()}=n;return r??m(t)}export function inspectConfigFile(t={}){const o=resolveConfigPath(t);if(!o)return{status:$.notFound,configPath:void 0};if(!r(o))return{status:$.invalid,configPath:o,raw:"",error:new Error(`Config file not found: ${o}`)};const e=n(o,"utf-8");let i;try{i=parseConfigDocument(e,o)}catch(n){return{status:$.invalid,configPath:o,raw:e,error:n instanceof Error?n:new Error(String(n))}}const a=c(i);if(a.needsMigration)return{status:$.needsMigration,configPath:o,raw:e,report:a};try{return{status:$.valid,configPath:o,config:validateConfigText(e,o)}}catch(n){return{status:$.invalid,configPath:o,raw:e,error:n instanceof Error?n:new Error(String(n))}}}function C(n,r){const t={...n};for(const[o,e]of Object.entries(r)){const r=n[o];"object"!=typeof e||null===e||Array.isArray(e)||"object"!=typeof r||null===r||Array.isArray(r)?t[o]=e:t[o]=C(r,e)}return t}
|
|
@@ -5,6 +5,8 @@ declare const CONFIG_MIGRATION_ISSUE_CODES: {
|
|
|
5
5
|
readonly routerAskConflict: "router-ask-conflict";
|
|
6
6
|
readonly duplicateEquivalentKeys: "duplicate-equivalent-keys";
|
|
7
7
|
readonly unknownRouterKeys: "unknown-router-keys";
|
|
8
|
+
readonly legacyCamelCaseAgentEnvKey: "legacy-camelcase-agent-env-key";
|
|
9
|
+
readonly legacyAgentEnvKeyConflict: "legacy-agent-env-key-conflict";
|
|
8
10
|
};
|
|
9
11
|
type ConfigMigrationIssueCode = (typeof CONFIG_MIGRATION_ISSUE_CODES)[keyof typeof CONFIG_MIGRATION_ISSUE_CODES];
|
|
10
12
|
export interface ConfigMigrationIssue {
|
|
@@ -27,7 +29,10 @@ export type ApplyKnownConfigMigrationsResult = {
|
|
|
27
29
|
readonly changed: false;
|
|
28
30
|
readonly issues: readonly ConfigMigrationIssue[];
|
|
29
31
|
};
|
|
30
|
-
export
|
|
32
|
+
export type ConfigMigrationScope = "llm" | "ask" | "agents";
|
|
33
|
+
export declare function detectKnownConfigMigrations(document: Record<string, unknown>, options?: {
|
|
34
|
+
readonly scope?: ConfigMigrationScope;
|
|
35
|
+
}): ConfigMigrationReport;
|
|
31
36
|
export declare function applyKnownConfigMigrations(document: Record<string, unknown>): ApplyKnownConfigMigrationsResult;
|
|
32
37
|
export declare function formatConfigMigrationError(configPath: string, report: ConfigMigrationReport): string;
|
|
33
38
|
export {};
|
package/dist/config/migration.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
import{camelToKebab as e}from"./key-codec.js";const t={deprecatedRouterSection:"deprecated-router-section",invalidAskSection:"invalid-ask-section",invalidRouterSection:"invalid-router-section",routerAskConflict:"router-ask-conflict",duplicateEquivalentKeys:"duplicate-equivalent-keys",unknownRouterKeys:"unknown-router-keys",legacyCamelCaseAgentEnvKey:"legacy-camelcase-agent-env-key",legacyAgentEnvKeyConflict:"legacy-agent-env-key-conflict"},s={llmModel:{routerKeys:["llm-model","llmModel"],askKeys:["llm-model","llmModel"],targetAskKey:"llm-model"},confirmThreshold:{routerKeys:["confirm-threshold","confirmThreshold"],askKeys:["confirm-threshold","confirmThreshold"],targetAskKey:"confirm-threshold"}},n=[...s.llmModel.routerKeys,...s.confirmThreshold.routerKeys,"mode"],o=new Set([t.invalidAskSection,t.invalidRouterSection,t.routerAskConflict,t.unknownRouterKeys,t.legacyAgentEnvKeyConflict]);function r(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function u(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function i(e,t){return{code:e,message:t}}function c(e,t){return t.filter(t=>u(e,t))}function a(e,s,n){const o=c(s,n);if(0===o.length)return{ok:!0,key:void 0,value:void 0};const[r]=o,u=r?s[r]:void 0;return o.some(e=>s[e]!==u)?{ok:!1,issue:i(t.duplicateEquivalentKeys,`\`${e}\` 同时包含等价键 ${o.join(", ")} 且值不一致,请手动处理。`)}:{ok:!0,key:r,value:u}}function l(e){if(!u(e,"router"))return{matches:!1,canAutoMigrate:!1,issues:[]};const c=[i(t.deprecatedRouterSection,"`router` 配置段已废弃,请改用 `ask`。"),i(t.deprecatedRouterSection,"将 `router.llm-model` 迁移为 `ask.llm-model`。"),i(t.deprecatedRouterSection,"将 `router.confirm-threshold` 迁移为 `ask.confirm-threshold`。"),i(t.deprecatedRouterSection,"删除 `router.mode`;命令本身已决定策略(`run` / `ask` / `chat`)。")],l=e.router;if(!r(l))return c.push(i(t.invalidRouterSection,"`router` 配置段不是对象,无法自动迁移,请手动修复。")),{matches:!0,canAutoMigrate:!1,issues:c};const d=Object.keys(l).filter(e=>!n.includes(e));d.length>0&&c.push(i(t.unknownRouterKeys,`\`router\` 包含无法自动迁移的未知键:${d.join(", ")}。`));const g=e.ask;if(void 0===g||r(g)||c.push(i(t.invalidAskSection,"`ask` 配置段不是对象,无法自动迁移,请手动修复。")),r(g))for(const e of Object.keys(s)){const n=s[e],o=a("router",l,n.routerKeys),r=a("ask",g,n.askKeys);o.ok?r.ok?o.key&&r.key&&r.value!==o.value&&c.push(i(t.routerAskConflict,`\`router.${n.targetAskKey}\` 与 \`ask.${n.targetAskKey}\` 同时存在且值冲突,请手动处理。`)):c.push(r.issue):c.push(o.issue)}return{matches:!0,canAutoMigrate:!c.some(e=>o.has(e.code)),issues:c}}function d(e){const t=l(e);if(!t.matches)return{ok:!0,changed:!1,document:structuredClone(e),issues:[],summary:[]};if(!t.canAutoMigrate)return{ok:!1,changed:!1,issues:t.issues};const n=structuredClone(e),o=n.router;if(!r(o))return{ok:!1,changed:!1,issues:t.issues};const i=n.ask,c=r(i)?i:{};let d=!1;const g=[];for(const e of Object.keys(s)){const t=s[e],n=a("router",o,t.routerKeys);if(!n.ok)return{ok:!1,changed:!1,issues:[n.issue]};if(!n.key)continue;const r=a("ask",c,t.askKeys);if(!r.ok)return{ok:!1,changed:!1,issues:[r.issue]};r.key?g.push(`移除已废弃的 \`router.${t.targetAskKey}\``):(c[t.targetAskKey]=n.value,d=!0,g.push(`将 \`router.${t.targetAskKey}\` 迁移到 \`ask.${t.targetAskKey}\``));for(const e of t.routerKeys)u(o,e)&&delete o[e]}return u(o,"mode")&&(delete o.mode,g.push("删除已废弃的 `router.mode`")),d&&(n.ask=c),0===Object.keys(o).length&&(delete n.router,g.push("删除空的 `router` 配置段")),{ok:!0,changed:g.length>0,document:n,issues:t.issues,summary:g}}const g=/^[a-z0-9]+(-[a-z0-9]+)*$/;function f(e){return!g.test(e)}function k(t){const s=e(t).toLowerCase();return g.test(s)?s:void 0}function y(e){const s=e.agents;if(!r(s))return{matches:!1,canAutoMigrate:!1,issues:[]};const n=s.env;if(!r(n))return{matches:!1,canAutoMigrate:!1,issues:[]};const o=Object.keys(n).filter(f);if(0===o.length)return{matches:!1,canAutoMigrate:!1,issues:[]};const c=[];let a=!1;for(const e of o){const s=k(e);void 0!==s?u(n,s)?(a=!0,c.push(i(t.legacyAgentEnvKeyConflict,`\`agents.env\` 下同时存在 \`${e}\` 与 \`${s}\`,无法自动合并,请手动处理。`))):c.push(i(t.legacyCamelCaseAgentEnvKey,`\`agents.env.${e}\` 应使用 kebab-case(\`${s}\`)。`)):(a=!0,c.push(i(t.legacyAgentEnvKeyConflict,`\`agents.env.${e}\` 命名不符合 kebab-case 规范,无法自动迁移,请手动重命名。`)))}return{matches:!0,canAutoMigrate:!a,issues:c}}function h(e){const t=y(e);if(!t.matches)return{ok:!0,changed:!1,document:structuredClone(e),issues:[],summary:[]};if(!t.canAutoMigrate)return{ok:!1,changed:!1,issues:t.issues};const s=structuredClone(e),n=s.agents;if(!r(n))return{ok:!1,changed:!1,issues:t.issues};const o=n.env;if(!r(o))return{ok:!1,changed:!1,issues:t.issues};const u=[];for(const e of Object.keys(o).filter(f)){const t=k(e);void 0!==t&&(o[t]=o[e],delete o[e],u.push(`将 \`agents.env.${e}\` 重命名为 \`agents.env.${t}\``))}return{ok:!0,changed:u.length>0,document:s,issues:t.issues,summary:u}}const m=[{id:"router-to-ask",scopes:new Set(["ask"]),inspect:l,apply:d},{id:"legacy-agent-env-keys",scopes:new Set(["agents"]),inspect:y,apply:h}];export function detectKnownConfigMigrations(e,t={}){const{scope:s}=t,n=(void 0!==s?m.filter(e=>e.scopes.has(s)):m).map(t=>t.inspect(e)).filter(e=>e.matches);return 0===n.length?{needsMigration:!1,canAutoMigrate:!1,issues:[]}:{needsMigration:!0,canAutoMigrate:n.every(e=>e.canAutoMigrate),issues:n.flatMap(e=>e.issues)}}export function applyKnownConfigMigrations(e){let t=structuredClone(e);const s=[],n=[];let o=!1;for(const e of m){const r=e.apply(t);if(!r.ok)return{ok:!1,changed:!1,issues:r.issues};t=r.document,r.changed&&(o=!0,s.push(...r.summary)),n.push(...r.issues)}return{ok:!0,changed:o,document:t,summary:s,issues:n}}export function formatConfigMigrationError(e,t){const s=[`Config validation failed (${e}):`];for(const e of t.issues)s.push(` - ${e.message}`);return t.canAutoMigrate?s.push(" - 可运行 `roll config migrate` 自动迁移当前配置。"):t.needsMigration&&s.push(" - 检测到配置迁移冲突,请手动处理后再重试。"),s.join("\n")}
|