@roll-agent/core 0.18.0 → 0.20.0
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/THIRD_PARTY_NOTICES.txt +112 -0
- package/dist/cli/chat/banner.d.ts +13 -0
- package/dist/cli/chat/banner.js +1 -1
- package/dist/cli/chat/ink/app.d.ts +4 -1
- package/dist/cli/chat/ink/app.js +1 -1
- package/dist/cli/chat/ink/banner-view.d.ts +15 -0
- package/dist/cli/chat/ink/banner-view.js +1 -0
- package/dist/cli/chat/ink/commands.d.ts +4 -12
- package/dist/cli/chat/ink/commands.js +1 -1
- package/dist/cli/chat/ink/history-from-messages.js +1 -1
- package/dist/cli/chat/ink/history-item.js +1 -1
- package/dist/cli/chat/ink/input-history.d.ts +2 -0
- package/dist/cli/chat/ink/input-history.js +1 -0
- package/dist/cli/chat/ink/line-buffer.d.ts +2 -0
- package/dist/cli/chat/ink/line-buffer.js +1 -1
- package/dist/cli/chat/ink/live-region.d.ts +2 -0
- package/dist/cli/chat/ink/live-region.js +1 -1
- package/dist/cli/chat/ink/reasoning-block.d.ts +4 -0
- package/dist/cli/chat/ink/reasoning-block.js +1 -0
- package/dist/cli/chat/ink/run-ink-repl.js +1 -1
- package/dist/cli/chat/ink/spinner.d.ts +3 -1
- package/dist/cli/chat/ink/spinner.js +1 -1
- package/dist/cli/chat/ink/state.d.ts +26 -3
- package/dist/cli/chat/ink/state.js +1 -1
- package/dist/cli/chat/ink/text-prompt.d.ts +3 -1
- package/dist/cli/chat/ink/text-prompt.js +1 -1
- package/dist/cli/chat/ink/turn-activity.d.ts +19 -0
- package/dist/cli/chat/ink/turn-activity.js +1 -0
- package/dist/cli/chat/ink/turn-status-line.d.ts +14 -0
- package/dist/cli/chat/ink/turn-status-line.js +1 -0
- package/dist/cli/chat/ink/use-session.js +1 -1
- package/dist/cli/commands/chat.d.ts +28 -0
- package/dist/cli/commands/chat.js +1 -1
- package/dist/cli/commands/config-explain.js +1 -1
- package/dist/cli/commands/config-setup.js +1 -1
- package/dist/cli/commands/config.js +1 -1
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/ui.d.ts +30 -0
- package/dist/cli/commands/ui.js +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/utils/chat-renderer.js +1 -1
- package/dist/cli/utils/current-cli-shim.d.ts +14 -0
- package/dist/cli/utils/current-cli-shim.js +1 -0
- package/dist/cli/utils/debug-format.d.ts +4 -0
- package/dist/cli/utils/debug-format.js +1 -0
- package/dist/config/application-service.d.ts +90 -0
- package/dist/config/application-service.js +1 -0
- package/dist/config/catalog.d.ts +76 -0
- package/dist/config/catalog.js +1 -0
- package/dist/config/defaults.d.ts +4 -3
- package/dist/config/defaults.js +1 -1
- package/dist/config/document-store.d.ts +70 -0
- package/dist/config/document-store.js +1 -0
- package/dist/config/guidance.d.ts +571 -0
- package/dist/config/guidance.js +1 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.js +1 -1
- package/dist/config/key-codec.d.ts +24 -3
- package/dist/config/key-codec.js +1 -1
- package/dist/config/migration.js +1 -1
- package/dist/config/schema.d.ts +52 -7
- package/dist/config/schema.js +1 -1
- package/dist/config/secret-policy.d.ts +20 -0
- package/dist/config/secret-policy.js +1 -0
- package/dist/llm/providers.d.ts +8 -4
- package/dist/llm/providers.js +1 -1
- package/dist/registry/agent-lifecycle.d.ts +97 -0
- package/dist/registry/agent-lifecycle.js +1 -0
- package/dist/registry/discovery.js +1 -1
- package/dist/registry/install.js +1 -1
- package/dist/registry/process-identity.d.ts +14 -0
- package/dist/registry/process-identity.js +1 -0
- package/dist/registry/process-manager.d.ts +28 -7
- package/dist/registry/process-manager.js +1 -1
- package/dist/registry/store.js +1 -1
- package/dist/skills/invocation.d.ts +27 -0
- package/dist/skills/invocation.js +1 -0
- package/dist/types/agent.d.ts +6 -0
- package/dist/types/agent.js +1 -1
- package/dist/types/chat.d.ts +3 -0
- package/dist/ui/config-controller.d.ts +15 -0
- package/dist/ui/config-controller.js +1 -0
- package/dist/ui/contracts.d.ts +49 -0
- package/dist/ui/contracts.js +1 -0
- package/dist/ui/index.d.ts +8 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/runtime-controller.d.ts +38 -0
- package/dist/ui/runtime-controller.js +1 -0
- package/dist/ui/server.d.ts +21 -0
- package/dist/ui/server.js +1 -0
- package/dist/ui/static-assets.d.ts +2 -0
- package/dist/ui/static-assets.js +1 -0
- package/dist/ui-assets/assets/app.css +1 -0
- package/dist/ui-assets/assets/app.js +53 -0
- package/dist/ui-assets/assets/roll-mark-wXIkY5y0.svg +5 -0
- package/dist/ui-assets/index.html +17 -0
- package/package.json +16 -8
- package/dist/cli/commands/config-guidance.d.ts +0 -148
- package/dist/cli/commands/config-guidance.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createElement as t,useEffect as e,useRef as n,useState as r}from"react";import{Box as o,Text as a,useStdout as i}from"ink";import{displayWidth as l}from"./display-width.js";import{Spinner as s}from"./spinner.js";import{TURN_ACTIVITY_KINDS as u}from"./turn-activity.js";const m=250,c=2,p=2,d=2,h=4,f=1e3,g="…",y=new Intl.Segmenter(void 0,{granularity:"grapheme"}),T={[u.waitingModel]:"gray",[u.reasoning]:"magenta",[u.replying]:"green",[u.tool]:"cyan",[u.compacting]:"yellow",[u.waitingUser]:"yellow",[u.cancelling]:"yellow"};function v(t,e){if(e<=0)return"";if(l(t)<=e)return t;const n=e-l(g);let r="",o=0;for(const{segment:e}of y.segment(t)){const t=l(e);if(o+t>n)break;r+=e,o+=t}return`${r}${g}`}export function formatElapsed(t){const e=Math.max(0,Math.floor(t/1e3)),n=Math.floor(e/60),r=e%60;return n>0?`${String(n)}m${String(r)}s`:`${String(r)}s`}export function composeTurnStatusLayout(t,e,n,r){if(r<=0)return{label:"",phaseTime:void 0,spacer:"",turnTime:""};const o=`本轮 ${formatElapsed(n)}`,a=l(o);if(a>=r)return{label:"",phaseTime:void 0,spacer:"",turnTime:v(o,r)};const i=Math.max(0,r-a-2);let s=t.showPhaseElapsed&&e>=f?formatElapsed(e):void 0;let u=i-(void 0===s?0:l(` ${s}`));void 0!==s&&u<4&&(s=void 0,u=i);const m=v(t.label,Math.max(0,u)),c=void 0===s?"":` ${s}`;return{label:m,phaseTime:s,spacer:" ".repeat(Math.max(1,r-l(m)-l(c)-a)),turnTime:o}}export function composeTurnStatusLine(t,e,n,r){const o=composeTurnStatusLayout(t,e,n,r),a=void 0===o.phaseTime?"":` ${o.phaseTime}`;return`${o.label}${a}${o.spacer}${o.turnTime}`}export function TurnStatusLine({activity:l}){const{stdout:u}=i(),c=u.columns||80,p=Date.now(),d=n(p),h=n({key:l.key,startedAt:p});h.current.key!==l.key&&(h.current={key:l.key,startedAt:p});const[f,g]=r(p);e(()=>{const t=setInterval(()=>{g(Date.now())},m);return()=>{clearInterval(t)}},[]);const y=Math.max(f,p),v=composeTurnStatusLayout(l,y-h.current.startedAt,y-d.current,Math.max(0,c-2-2));return t(o,{width:c,paddingX:1},l.animated?t(s,{color:T[l.kind]}):t(a,{color:T[l.kind]},"◆"),c>3?t(a,null," "):null,v.label.length>0?t(a,{color:T[l.kind]},v.label):null,void 0===v.phaseTime?null:t(a,{dimColor:!0},` ${v.phaseTime}`),v.spacer.length>0?t(a,null,v.spacer):null,v.turnTime.length>0?t(a,{dimColor:!0},v.turnTime):null)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as e}from"node:crypto";import{useCallback as t,useReducer as n,useRef as i}from"react";import{chatReducer as r,createInitialState as o}from"./state.js";import{log as
|
|
1
|
+
import{randomUUID as e}from"node:crypto";import{useCallback as t,useReducer as n,useRef as i}from"react";import{chatReducer as r,createInitialState as o}from"./state.js";import{log as a}from"../../utils/output.js";import{formatDebugEvent as s}from"../../utils/debug-format.js";const c=32;function l(e){return e instanceof Error?e.message:String(e)}export function useSession(d,p){const[u,y]=n(r,o(p.model,p.contextWindow,{...p.initialHistory?{history:p.initialHistory}:{},...p.initialThinkingLevel?{thinkingLevel:p.initialThinkingLevel}:{}})),m=p.onThinkingChange,v=i(null),g=i(!1),f=i(!1),h=t(async t=>{let n,i;const r=()=>{if(void 0!==i&&(clearTimeout(i),i=void 0),void 0!==n){const t=n;n=void 0,y({type:"session-event",id:e(),event:t})}},o=e=>{void 0===n?n=e:"text-delta"===n.type&&"text-delta"===e.type?n={type:"text-delta",delta:n.delta+e.delta}:"reasoning-delta"===n.type&&"reasoning-delta"===e.type&&n.reasoningId===e.reasoningId?n={type:"reasoning-delta",reasoningId:e.reasoningId,delta:n.delta+e.delta}:(r(),n=e),void 0===i&&(i=setTimeout(r,c))};try{for await(const n of t)if("debug"!==n.type)if("text-delta"!==n.type&&"reasoning-delta"!==n.type){if(r(),"confirmation-required"===n.type){if(g.current){d.approve(n.approvalId);continue}const t=new Promise(e=>{v.current=e});y({type:"session-event",id:e(),event:n});const i=await t;v.current=null,i?d.approve(n.approvalId):d.reject(n.approvalId,"用户取消"),y({type:"confirm-resolved"});continue}y({type:"session-event",id:e(),event:n})}else o(n);else a.debug(s(n))}catch(t){r(),y({type:"session-event",id:e(),event:{type:"error",stage:"execute",message:l(t)}})}finally{r(),y({type:"turn-end"}),f.current=!1}},[d]),x=t(t=>{f.current||(f.current=!0,y({type:"submit-user",id:e(),text:t}),h(d.send(t)).catch(()=>{}))},[h,d]),I=t(()=>{f.current||(f.current=!0,y({type:"start-compaction"}),h(d.compact("manual")).catch(()=>{}))},[h,d]),k=t(()=>{f.current&&d.cancel()&&y({type:"cancel-requested"})},[d]),T=t(e=>{v.current?.(e)},[]),b=t(e=>{y({type:"set-draft",value:e})},[]),j=t(e=>{y({type:"set-thinking",level:e}),m?.(e)},[m]),w=t(e=>{g.current=e,y({type:"set-auto",value:e}),e&&v.current?.(!0)},[]),H=t(()=>{w(!g.current)},[w]),L=t(e=>{y({type:"commit-history",item:e})},[]);return{state:u,submit:x,compact:I,cancel:k,resolveConfirm:T,setDraft:b,setThinking:j,setAutoMode:w,toggleAutoMode:H,commitHistory:L}}
|
|
@@ -5,12 +5,40 @@ import type { RollConfig } from "../../config/schema.ts";
|
|
|
5
5
|
import type { LlmConfigReadiness } from "../../config/helpers.ts";
|
|
6
6
|
type RuntimeModule = typeof import("@roll-agent/runtime");
|
|
7
7
|
type ThreadStoreInstance = InstanceType<RuntimeModule["ThreadStore"]>;
|
|
8
|
+
type ChatEngineOptions = ConstructorParameters<RuntimeModule["ConversationEngine"]>[0];
|
|
9
|
+
export declare const CHAT_ENGINE_SURFACES: {
|
|
10
|
+
readonly ink: "ink";
|
|
11
|
+
readonly basicRepl: "basic-repl";
|
|
12
|
+
readonly oneShot: "one-shot";
|
|
13
|
+
readonly json: "json";
|
|
14
|
+
readonly server: "server";
|
|
15
|
+
};
|
|
16
|
+
export type ChatEngineSurface = (typeof CHAT_ENGINE_SURFACES)[keyof typeof CHAT_ENGINE_SURFACES];
|
|
17
|
+
export declare function chatHostModeForSurface(surface: ChatEngineSurface): NonNullable<ChatEngineOptions["hostMode"]>;
|
|
18
|
+
interface CreateChatEngineInput {
|
|
19
|
+
readonly runtime: RuntimeModule;
|
|
20
|
+
readonly config: RollConfig;
|
|
21
|
+
readonly model: NonNullable<ChatEngineOptions["model"]>;
|
|
22
|
+
readonly store: ThreadStoreInstance;
|
|
23
|
+
readonly surface: ChatEngineSurface;
|
|
24
|
+
readonly providerOptions?: NonNullable<ChatEngineOptions["providerOptions"]>;
|
|
25
|
+
readonly structuredOutputProviderOptions?: NonNullable<ChatEngineOptions["structuredOutputProviderOptions"]>;
|
|
26
|
+
readonly structuredOutputReasoning?: NonNullable<ChatEngineOptions["structuredOutputReasoning"]>;
|
|
27
|
+
readonly shellEnv?: NodeJS.ProcessEnv;
|
|
28
|
+
}
|
|
8
29
|
interface ReplIo {
|
|
9
30
|
readonly input: NodeJS.ReadableStream;
|
|
10
31
|
readonly output: NodeJS.WritableStream;
|
|
11
32
|
readonly confirm?: ChatConfirm;
|
|
12
33
|
}
|
|
34
|
+
export declare function createChatEngine(input: CreateChatEngineInput): import("@roll-agent/runtime").ConversationEngine;
|
|
13
35
|
export declare function resolveChatLlmReadiness(config: RollConfig): LlmConfigReadiness;
|
|
36
|
+
export declare function resolveChatLlmCalls(provider: string, modelName: string, apiKey: string, baseUrl: string | undefined, thinkingLevel: RollConfig["runtime"]["thinkingLevel"], compactionThinkingLevel?: RollConfig["runtime"]["compaction"]["thinkingLevel"], compactionUsesStructuredOutput?: boolean): {
|
|
37
|
+
readonly model: NonNullable<ChatEngineOptions["model"]>;
|
|
38
|
+
readonly providerOptions?: NonNullable<ChatEngineOptions["providerOptions"]>;
|
|
39
|
+
readonly structuredOutputProviderOptions?: NonNullable<ChatEngineOptions["structuredOutputProviderOptions"]>;
|
|
40
|
+
readonly structuredOutputReasoning?: NonNullable<ChatEngineOptions["structuredOutputReasoning"]>;
|
|
41
|
+
};
|
|
14
42
|
export declare function runJsonTurn(session: AgentSession, message: string): Promise<ChatCommandResult>;
|
|
15
43
|
export declare function runRepl(session: AgentSession, store: ThreadStoreInstance, isNewSession: boolean, io?: ReplIo): Promise<void>;
|
|
16
44
|
declare const _default: import("citty").CommandDef<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=this&&this.__rewriteRelativeImportExtension||function(e,t){return"string"==typeof e&&/^\.\.?\//.test(e)?e.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i,function(e,o,s,n,i){return o?t?".jsx":".js":!s||n&&i?s+n+"."+i.toLowerCase()+"js":e}):e};import{defineCommand as t}from"citty";import{inspectLlmConfigReadiness as o}from"../../config/helpers.js";import{loadConfig as s}from"../../config/loader.js";import{resolveLLMCall as n,thinkingProviderOptions as i}from"../../llm/providers.js";import{createInterface as r}from"node:readline/promises";import a from"chalk";import l from"cli-table3";import{isDebugLogEnabled as c,log as d}from"../utils/output.js";import{ChatRenderer as u,clackConfirm as p}from"../utils/chat-renderer.js";import{titleFromMessage as m}from"../chat/title.js";import{buildBannerLines as g,renderBannerText as f}from"../chat/banner.js";import{getCurrentVersion as h}from"../utils/update-checker.js";import{buildSkillInvocationPrompt as v,formatSkillList as w,parseSkillInvocation as y}from"../chat/ink/commands.js";const b=import.meta.url.endsWith(".ts")?"ts":"js";function k(e,t){return new e.ConfigurableToolPolicy({defaultMode:t.runtime.approval.default,overrides:t.runtime.approval.overrides})}function C(e){console.log(JSON.stringify(e,null,2))}function x(e){d.warn(`Agent "${e.agentName}" 启动失败:${e.message}`)}function S(e){d.warn(`skill 目录加载警告:${e}`)}function T(e,t){const o=y(t,e.getSkillSummaries());return o&&o.prompt.length>0?v(o):t}async function I(e,t,o){d.debug(`chat.repl input waiting · ${o}`),e.resume();try{const s=await e.question(t);return d.debug(`chat.repl input received · ${o} · chars=${String(s.length)}`),s}catch(e){const t=e instanceof Error?e.message:String(e);return void d.debug(`chat.repl input closed · ${o} · ${t}`)}finally{e.pause()}}async function j(){return import("@roll-agent/runtime")}async function $(t,o){const s=new URL(`./setup.${b}`,import.meta.url).href;return(await import(e(s))).runChatOnboarding({},{provider:t,model:o})}export function resolveChatLlmReadiness(e){return o(e,{provider:e.runtime.provider??e.llm.defaultProvider,model:e.runtime.model??e.llm.defaultModel})}async function N(e){const t=resolveChatLlmReadiness(e);if(!t.configured||!t.providerConfig)return d.error(t.message),void(process.exitCode=1);const o=t.providerConfig,s=t.provider,i=t.model,r=await j(),{ConversationEngine:a,ThreadStore:l,RuntimeServer:u,createStdioConnection:p}=r,{model:m,providerOptions:g}=n(s,i,o.apiKey,"chat",o.baseUrl,e.runtime.thinkingLevel),f=new l(e.runtime.threadsDir),h=new a({config:e,model:m,store:f,policy:k(r,e),maxSteps:e.runtime.maxSteps,...g?{providerOptions:g}:{},debugEvents:c(),onAgentBootstrapIssue:x,onSkillLibraryIssue:S}),v=p(process.stdin,process.stdout),w=new u(h,v);v.onClose(()=>{w.abortAll().then(()=>h.dispose()).catch(()=>{}).finally(()=>{f.close(),process.exit(0)})}),d.info("roll runtime-server 已启动(stdio JSON-RPC,等待客户端连接)")}async function R(e,t){const{ThreadStore:o}=await j(),s=new o(e.runtime.threadsDir);try{const e=s.listThreads();if(t){const t=e.map(e=>({...e,messageCount:s.countMessages(e.id)}));return void console.log(JSON.stringify(t,null,2))}if(0===e.length)return void console.log('暂无会话。用 `roll chat "<message>"` 开始一个。');const o=new l({head:["Session ID","标题","消息","更新时间"],style:{head:["cyan"]}});for(const t of e)o.push([t.id,t.title??"-",String(s.countMessages(t.id)),t.updatedAt]);console.log(o.toString())}finally{s.close()}}export async function runJsonTurn(e,t){const o=[],s=[],n=[],i=[];let r,a,l,c,d="";for await(const u of e.send(t))switch(u.type){case"text-delta":d+=u.delta;break;case"tool-call":o.push({summary:`${u.agentName}.${u.toolName}`,agentName:u.agentName,toolName:u.toolName});break;case"confirmation-required":i.push({summary:`${u.agentName}.${u.toolName}`,agentName:u.agentName,toolName:u.toolName}),e.reject(u.approvalId,"json 模式不支持交互确认");break;case"step-finish":s.push({finishReason:u.finishReason,...u.usage?{usage:u.usage}:{}});break;case"message-finish":a=u.totalUsage,l=u.sessionUsage,c=u.contextInputTokens;break;case"context-compacted":n.push({reason:u.reason,strategy:u.strategy,removed:u.removed,kept:u.kept,...void 0!==u.truncatedTools?{truncatedTools:u.truncatedTools}:{},...void 0!==u.beforeInputTokens?{beforeInputTokens:u.beforeInputTokens}:{}});break;case"turn-cancelled":case"error":r=u.message}const u=e.getContextWindow();return void 0!==r?{status:"failed",stage:"execute",message:r,sessionId:e.id}:i.length>0?{status:"needs_confirmation",sessionId:e.id,message:"存在需要确认的工具调用,请在交互模式下执行或显式批准",pendingActions:i}:{status:"completed",sessionId:e.id,output:d,steps:o,...s.length>0?{stepUsages:s}:{},...a?{totalUsage:a}:{},...l?{sessionUsage:l}:{},...void 0!==u?{contextWindow:u}:{},...void 0!==c?{contextInputTokens:c}:{},...n.length>0?{compactions:n}:{}}}export async function runRepl(e,t,o,s={input:process.stdin,output:process.stdout}){const n=r({input:s.input,output:s.output});n.on("SIGINT",()=>n.close());const i=s.confirm??(async e=>{d.debug("chat.repl input waiting · confirm"),n.pause();const t=await p(e);return d.debug(`chat.repl input received · confirm · approved=${String(t)}`),t}),l=new u(i,e.getContextWindow()),c=e.getSkillSummaries();d.info("进入多轮对话(输入 exit / quit 或 Ctrl-C 退出,/compact 手动压缩上下文)");let g=!o,f=!1;try{for(;;){const o=await I(n,a.green("› "),"prompt");if(void 0===o)break;const s=o.trim();if(0===s.length)continue;if("exit"===s||"quit"===s)break;if("/compact"===s){d.debug("chat.repl manual compact requested");for await(const t of e.compact("manual"))await l.handle(t,e);d.debug("chat.repl manual compact completed");continue}if("/skills"===s){d.info(w(c,(process.stdout.columns||96)-2));continue}const i=y(s,c);if(i&&0===i.prompt.length){d.info("用法: /<skill-name> [/<skill-name> ...] 你的请求");continue}const r=i?v(i):s;g||(t.updateTitle(e.id,m(s)),g=!0),f=!0,d.debug(`chat.repl send start · chars=${String(s.length)}`);for await(const t of e.send(r))await l.handle(t,e);d.debug("chat.repl send completed")}}finally{n.close(),o&&!f&&0===t.countMessages(e.id)&&t.deleteThread(e.id)}}export default t({meta:{description:"会话式 AI 助手(多轮对话 + 自动工具调用)"},args:{message:{type:"positional",description:"起始消息",required:!1},session:{type:"string",description:"继续已有会话的 session ID"},last:{type:"boolean",description:"继续最近一个会话",default:!1},list:{type:"boolean",description:"列出已有会话",default:!1},json:{type:"boolean",description:"JSON 格式输出",default:!1},server:{type:"boolean",description:"以 JSON-RPC daemon 模式运行(stdio,供 GUI/前端接入)",default:!1}},async run({args:e}){let{config:t}=s();if(e.server)return void await N(t);if(e.list)return void await R(t,e.json);let o=resolveChatLlmReadiness(t);if(!o.configured){!0===process.stdin.isTTY&&!0===process.stderr.isTTY&&!e.json&&await $(o.provider,o.model)&&(t=s().config,o=resolveChatLlmReadiness(t))}if(!o.configured||!o.providerConfig)return d.error(o.message),void(process.exitCode=1);const r=o.providerConfig,a=o.provider,l=o.model;if(e.json&&!e.message)return d.error('--json 模式需要消息:roll chat "<message>" --json'),void(process.exitCode=1);const v=await j(),{ConversationEngine:w,ThreadStore:y}=v,{model:I,providerOptions:L}=n(a,l,r.apiKey,"chat",r.baseUrl,t.runtime.thinkingLevel),O=new y(t.runtime.threadsDir),U=new w({config:t,model:I,store:O,policy:k(v,t),maxSteps:t.runtime.maxSteps,...L?{providerOptions:L}:{},debugEvents:c(),onAgentBootstrapIssue:x,onSkillLibraryIssue:S,sessionExecEnabled:void 0===e.message});let E;try{let o;if(e.session)o=await U.resumeSession(e.session);else if(e.last){const e=O.listThreads()[0];if(!e)return d.error('暂无可继续的会话,先用 `roll chat "<message>"` 开始一个'),void(process.exitCode=1);o=await U.resumeSession(e.id)}else o=await U.createSession(e.message?{title:m(e.message)}:{});if(E=o,e.json&&e.message){const t=await runJsonTurn(o,T(o,e.message));return C(t),void("completed"!==t.status&&(process.exitCode=1))}if(d.info(`会话 ${o.id}`),t.runtime.compaction.enabled&&void 0===o.getContextWindow()&&d.warn(`未知模型 "${l}" 的 context window,阈值自动压缩不可用。可在 roll.config.yaml 设置 runtime.context-window`),e.message){const t=new u(p,o.getContextWindow());for await(const s of o.send(T(o,e.message)))await t.handle(s,o)}else{const s=!e.session&&!e.last,n=Boolean(process.stdout.isTTY&&process.stdin.isTTY&&"function"==typeof process.stdin.setRawMode),r=await U.getContextSummary(),c={version:h(),model:l,agentCount:r.agentCount,skillCount:r.skillCount};let u=!1;if(n)try{const e=new URL(`../chat/ink/run-ink-repl.${b}`,import.meta.url).href,{runInkRepl:n}=await import(e);await n(o,O,s,{model:l,banner:c,initialThinkingLevel:t.runtime.thinkingLevel,onThinkingChange:e=>o.setProviderOptions(i(a,l,e))}),u=!0}catch(e){d.warn(`Ink TUI 不可用,回退到基础多轮模式:${e instanceof Error?e.message:String(e)}`)}u||(process.stderr.write(`${f(g(c,process.stdout.columns||80))}\n`),await runRepl(o,O,s))}}catch(e){d.error(e instanceof Error?e.message:String(e)),process.exitCode=1}finally{await(E?.close()),await U.dispose(),O.close()}}});
|
|
1
|
+
var e=this&&this.__rewriteRelativeImportExtension||function(e,t){return"string"==typeof e&&/^\.\.?\//.test(e)?e.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i,function(e,o,n,s,r){return o?t?".jsx":".js":!n||s&&r?n+s+"."+r.toLowerCase()+"js":e}):e};import{defineCommand as t}from"citty";import{inspectLlmConfigReadiness as o}from"../../config/helpers.js";import{loadConfig as n}from"../../config/loader.js";import{resolveLLMCall as s,thinkingProviderOptions as r}from"../../llm/providers.js";import{createInterface as i}from"node:readline/promises";import a from"chalk";import c from"cli-table3";import{isDebugLogEnabled as u,log as l}from"../utils/output.js";import{ChatRenderer as d,clackConfirm as p}from"../utils/chat-renderer.js";import{titleFromMessage as m}from"../chat/title.js";import{buildBannerLines as g,renderBannerText as f}from"../chat/banner.js";import{getCurrentVersion as h}from"../utils/update-checker.js";import{formatSkillList as v,parseSkillInvocation as C}from"../chat/ink/commands.js";import{installCurrentCliShim as S}from"../utils/current-cli-shim.js";const w=import.meta.url.endsWith(".ts")?"ts":"js";function y(e,t){return new e.ConfigurableToolPolicy({defaultMode:t.runtime.approval.default,overrides:t.runtime.approval.overrides})}export const CHAT_ENGINE_SURFACES={ink:"ink",basicRepl:"basic-repl",oneShot:"one-shot",json:"json",server:"server"};const k={[CHAT_ENGINE_SURFACES.ink]:"interactive",[CHAT_ENGINE_SURFACES.basicRepl]:"interactive",[CHAT_ENGINE_SURFACES.oneShot]:"one-shot",[CHAT_ENGINE_SURFACES.json]:"one-shot",[CHAT_ENGINE_SURFACES.server]:"server"};export function chatHostModeForSurface(e){return k[e]}function E(e){console.log(JSON.stringify(e,null,2))}function O(e){l.warn(`Agent "${e.agentName}" 启动失败:${e.message}`)}function T(e){l.warn(`skill 目录加载警告:${e}`)}function N(){const e={...process.env};try{const t=S({env:e});return{env:e,dispose:()=>t.dispose()}}catch(t){return l.warn(`无法锁定当前 Roll CLI,子任务将继续按 PATH 查找 roll:${t instanceof Error?t.message:String(t)}`),{env:e,dispose:()=>{}}}}export function createChatEngine(e){return new e.runtime.ConversationEngine({config:e.config,model:e.model,store:e.store,hostMode:chatHostModeForSurface(e.surface),policy:y(e.runtime,e.config),maxSteps:e.config.runtime.maxSteps,...e.providerOptions?{providerOptions:e.providerOptions}:{},...e.structuredOutputProviderOptions?{structuredOutputProviderOptions:e.structuredOutputProviderOptions}:{},...e.structuredOutputReasoning?{structuredOutputReasoning:e.structuredOutputReasoning}:{},debugEvents:u(),onAgentBootstrapIssue:O,onSkillLibraryIssue:T,...e.shellEnv?{shellEnv:e.shellEnv}:{}})}async function b(e,t,o){l.debug(`chat.repl input waiting · ${o}`),e.resume();try{const n=await e.question(t);return l.debug(`chat.repl input received · ${o} · chars=${String(n.length)}`),n}catch(e){const t=e instanceof Error?e.message:String(e);return void l.debug(`chat.repl input closed · ${o} · ${t}`)}finally{e.pause()}}async function R(){return import("@roll-agent/runtime")}async function I(t,o){const n=new URL(`./setup.${w}`,import.meta.url).href;return(await import(e(n))).runChatOnboarding({},{provider:t,model:o})}export function resolveChatLlmReadiness(e){return o(e,{provider:e.runtime.provider??e.llm.defaultProvider,model:e.runtime.model??e.llm.defaultModel})}export function resolveChatLlmCalls(e,t,o,n,r,i=void 0,a=!0){const c=s(e,t,o,"chat",n,r),u=a?s(e,t,o,"structured-output",n,i??r):void 0;return{model:c.model,...c.providerOptions?{providerOptions:c.providerOptions}:{},...u?.providerOptions?{structuredOutputProviderOptions:u.providerOptions}:{},...u?.reasoning?{structuredOutputReasoning:u.reasoning}:{}}}async function x(e){const t=resolveChatLlmReadiness(e);if(!t.configured||!t.providerConfig)return l.error(t.message),void(process.exitCode=1);const o=t.providerConfig,n=t.provider,s=t.model,r=await R(),{ThreadStore:i,RuntimeServer:a,createStdioConnection:c}=r,{model:u,providerOptions:d,structuredOutputProviderOptions:p,structuredOutputReasoning:m}=resolveChatLlmCalls(n,s,o.apiKey,o.baseUrl,e.runtime.thinkingLevel,e.runtime.compaction.thinkingLevel,"summarize"===e.runtime.compaction.strategy),g=new i(e.runtime.threadsDir),f=N();let h;try{h=createChatEngine({runtime:r,config:e,model:u,store:g,surface:CHAT_ENGINE_SURFACES.server,shellEnv:f.env,...d?{providerOptions:d}:{},...p?{structuredOutputProviderOptions:p}:{},...m?{structuredOutputReasoning:m}:{}});const t=h,o=c(process.stdin,process.stdout),n=new a(t,o);o.onClose(()=>{n.abortAll().then(()=>t.dispose()).catch(()=>{}).finally(()=>{f.dispose(),g.close(),process.exit(0)})})}catch(e){throw await(h?.dispose().catch(()=>{})),f.dispose(),g.close(),e}l.info("roll runtime-server 已启动(stdio JSON-RPC,等待客户端连接)")}async function A(e,t){const{ThreadStore:o}=await R(),n=new o(e.runtime.threadsDir);try{const e=n.listThreads();if(t){const t=e.map(e=>({...e,messageCount:n.countMessages(e.id)}));return void console.log(JSON.stringify(t,null,2))}if(0===e.length)return void console.log('暂无会话。用 `roll chat "<message>"` 开始一个。');const o=new c({head:["Session ID","标题","消息","更新时间"],style:{head:["cyan"]}});for(const t of e)o.push([t.id,t.title??"-",String(n.countMessages(t.id)),t.updatedAt]);console.log(o.toString())}finally{n.close()}}export async function runJsonTurn(e,t){const o=[],n=[],s=[],r=[];let i,a,c,u,l="";for await(const d of e.send(t))switch(d.type){case"text-delta":l+=d.delta;break;case"tool-call":o.push({summary:`${d.agentName}.${d.toolName}`,agentName:d.agentName,toolName:d.toolName});break;case"confirmation-required":r.push({summary:`${d.agentName}.${d.toolName}`,agentName:d.agentName,toolName:d.toolName}),e.reject(d.approvalId,"json 模式不支持交互确认");break;case"step-finish":n.push({finishReason:d.finishReason,...d.usage?{usage:d.usage}:{}});break;case"message-finish":a=d.totalUsage,c=d.sessionUsage,u=d.contextInputTokens;break;case"context-compacted":s.push({reason:d.reason,strategy:d.strategy,removed:d.removed,kept:d.kept,...void 0!==d.truncatedTools?{truncatedTools:d.truncatedTools}:{},...void 0!==d.beforeInputTokens?{beforeInputTokens:d.beforeInputTokens}:{},...void 0!==d.checkpointId?{checkpointId:d.checkpointId}:{},...void 0!==d.checkpointGeneration?{checkpointGeneration:d.checkpointGeneration}:{},...void 0!==d.checkpointSummaryStatus?{checkpointSummaryStatus:d.checkpointSummaryStatus}:{}});break;case"turn-cancelled":case"error":i=d.message}const d=e.getContextWindow();return void 0!==i?{status:"failed",stage:"execute",message:i,sessionId:e.id}:r.length>0?{status:"needs_confirmation",sessionId:e.id,message:"存在需要确认的工具调用,请在交互模式下执行或显式批准",pendingActions:r}:{status:"completed",sessionId:e.id,output:l,steps:o,...n.length>0?{stepUsages:n}:{},...a?{totalUsage:a}:{},...c?{sessionUsage:c}:{},...void 0!==d?{contextWindow:d}:{},...void 0!==u?{contextInputTokens:u}:{},...s.length>0?{compactions:s}:{}}}export async function runRepl(e,t,o,n={input:process.stdin,output:process.stdout}){const s=i({input:n.input,output:n.output});s.on("SIGINT",()=>s.close());const r=n.confirm??(async e=>{l.debug("chat.repl input waiting · confirm"),s.pause();const t=await p(e);return l.debug(`chat.repl input received · confirm · approved=${String(t)}`),t}),c=new d(r,e.getContextWindow()),u=e.getSkillSummaries();l.info("进入多轮对话(输入 exit / quit 或 Ctrl-C 退出,/compact 手动压缩上下文)");let g=!o,f=!1;try{for(;;){const o=await b(s,a.green("› "),"prompt");if(void 0===o)break;const n=o.trim();if(0===n.length)continue;if("exit"===n||"quit"===n)break;if("/compact"===n){l.debug("chat.repl manual compact requested");for await(const t of e.compact("manual"))await c.handle(t,e);l.debug("chat.repl manual compact completed");continue}if("/skills"===n){l.info(v(u,(process.stdout.columns||96)-2));continue}const r=C(n,u);if(r&&0===r.prompt.length)l.info("用法: /<skill-name> [/<skill-name> ...] 你的请求");else{g||(t.updateTitle(e.id,m(n)),g=!0),f=!0,l.debug(`chat.repl send start · chars=${String(n.length)}`);for await(const t of e.send(n))await c.handle(t,e);l.debug("chat.repl send completed")}}}finally{s.close(),o&&!f&&0===t.countMessages(e.id)&&t.deleteThread(e.id)}}export default t({meta:{description:"会话式 AI 助手(多轮对话 + 自动工具调用)"},args:{message:{type:"positional",description:"起始消息",required:!1},session:{type:"string",description:"继续已有会话的 session ID"},last:{type:"boolean",description:"继续最近一个会话",default:!1},list:{type:"boolean",description:"列出已有会话",default:!1},json:{type:"boolean",description:"JSON 格式输出",default:!1},server:{type:"boolean",description:"以 JSON-RPC daemon 模式运行(stdio,供 GUI/前端接入)",default:!1}},async run({args:e}){let{config:t}=n();if(e.server)return void await x(t);if(e.list)return void await A(t,e.json);let o=resolveChatLlmReadiness(t);if(!o.configured){!0===process.stdin.isTTY&&!0===process.stderr.isTTY&&!e.json&&await I(o.provider,o.model)&&(t=n().config,o=resolveChatLlmReadiness(t))}if(!o.configured||!o.providerConfig)return l.error(o.message),void(process.exitCode=1);const s=o.providerConfig,i=o.provider,a=o.model;if(e.json&&!e.message)return l.error('--json 模式需要消息:roll chat "<message>" --json'),void(process.exitCode=1);const c=await R(),{ThreadStore:u}=c,{model:v,providerOptions:C,structuredOutputProviderOptions:S,structuredOutputReasoning:y}=resolveChatLlmCalls(i,a,s.apiKey,s.baseUrl,t.runtime.thinkingLevel,t.runtime.compaction.thinkingLevel,"summarize"===t.runtime.compaction.strategy),k=new u(t.runtime.threadsDir),O=Boolean(process.stdout.isTTY&&process.stdin.isTTY&&"function"==typeof process.stdin.setRawMode),T=e.message?e.json?CHAT_ENGINE_SURFACES.json:CHAT_ENGINE_SURFACES.oneShot:O?CHAT_ENGINE_SURFACES.ink:CHAT_ENGINE_SURFACES.basicRepl,b=N();let j,_;try{let o;if(j=createChatEngine({runtime:c,config:t,model:v,store:k,surface:T,shellEnv:b.env,...C?{providerOptions:C}:{},...S?{structuredOutputProviderOptions:S}:{},...y?{structuredOutputReasoning:y}:{}}),e.session)o=await j.resumeSession(e.session);else if(e.last){const e=k.listThreads()[0];if(!e)return l.error('暂无可继续的会话,先用 `roll chat "<message>"` 开始一个'),void(process.exitCode=1);o=await j.resumeSession(e.id)}else o=await j.createSession(e.message?{title:m(e.message)}:{});if(_=o,e.json&&e.message){const t=await runJsonTurn(o,e.message);return E(t),void("completed"!==t.status&&(process.exitCode=1))}if(l.info(`会话 ${o.id}`),t.runtime.compaction.enabled&&void 0===o.getContextWindow()&&l.warn(`未知模型 "${a}" 的 context window,阈值自动压缩不可用。可在 roll.config.yaml 设置 runtime.context-window`),e.message){const t=new d(p,o.getContextWindow());for await(const n of o.send(e.message))await t.handle(n,o)}else{const n=!e.session&&!e.last,s=await j.getContextSummary(),c={version:h(),model:a,agentCount:s.agentCount,skillCount:s.skillCount};let u=!1;if(O)try{const e=new URL(`../chat/ink/run-ink-repl.${w}`,import.meta.url).href,{runInkRepl:s}=await import(e);await s(o,k,n,{model:a,banner:c,initialThinkingLevel:t.runtime.thinkingLevel,onThinkingChange:e=>o.setProviderOptions(r(i,a,e))}),u=!0}catch(e){l.warn(`Ink TUI 不可用,回退到基础多轮模式:${e instanceof Error?e.message:String(e)}`)}u||(process.stderr.write(`${f(g(c,process.stdout.columns||80))}\n`),await runRepl(o,k,n))}}catch(e){l.error(e instanceof Error?e.message:String(e)),process.exitCode=1}finally{await(_?.close()),await(j?.dispose()),k.close(),b.dispose()}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getAgentEnvFromAgentsConfig as e,inspectAgentEnvRequirements as n}from"../../config/helpers.js";import{loadAgentsConfig as o}from"../../config/loader.js";import{normalizeUserPath as t}from"../../config/key-codec.js";import{AgentStore as l}from"../../registry/store.js";import{findConfigGuidance as s,flattenAgentEnvDeclarations as r,listConfigGuidanceEntries as i}from"
|
|
1
|
+
import{getAgentEnvFromAgentsConfig as e,inspectAgentEnvRequirements as n}from"../../config/helpers.js";import{loadAgentsConfig as o}from"../../config/loader.js";import{normalizeUserPath as t}from"../../config/key-codec.js";import{AgentStore as l}from"../../registry/store.js";import{findConfigGuidance as s,flattenAgentEnvDeclarations as r,listConfigGuidanceEntries as i}from"../../config/guidance.js";export function explainConfig(e){if(void 0===e){console.log("可解释的配置路径:\n");for(const e of i())console.log(`- ${e.path}: ${e.title}`);return console.log("\n示例:"),console.log(" roll config explain install.registry"),void console.log(" roll config explain agents.env.browser-use-agent.REPLY_AUTHORITY_URL")}const n=g(e);if(n)return void console.log(n);const o=s(e);if(!o)return console.error(`✗ 未找到配置说明: ${e}`),console.error(" 运行 `roll config explain` 查看可用路径。"),void(process.exitCode=1);console.log(`${o.title}\n`),console.log(`路径: ${o.path}`),console.log(`用途: ${o.purpose}`),o.defaultBehavior&&console.log(`默认行为: ${o.defaultBehavior}`),o.example&&console.log(`示例:\n${o.example}`),o.setupCommand&&console.log(`配置向导: ${o.setupCommand}`)}function g(s){const i=t(s.split("."));if("agents"!==i[0]||"env"!==i[1])return;const g=i[2];if(void 0===g)return;const c=i[3],{agentsConfig:a}=o(),f=new l(a.dataDir).findByName(g);if(!f)return`Agent 环境变量\n\nAgent "${g}" 未注册。\n配置向导: roll config setup agent ${g}`;const u=r(f.skill.env);if(0===u.length)return`Agent 环境变量\n\nAgent "${g}" 未声明环境变量需求。`;if(void 0===c){const e=n(f.skill.name,f.skill.env,a.env),o=["Agent 环境变量","",`路径: agents.env.${g}`,`用途: 注入到 Agent "${g}" 进程的环境变量。`,`配置向导: roll config setup agent ${g}`,"","变量:"];for(const n of e?.items??u)o.push(`- ${n.name}: ${n.required?"必填":"可选"}${"source"in n?`,来源 ${n.source}`:""}`);return o.join("\n")}const p=u.find(e=>e.name===c);if(!p)return`Agent 环境变量\n\nAgent "${g}" 未声明环境变量 ${c}。\n配置向导: roll config setup agent ${g}`;const $=n(f.skill.name,f.skill.env,a.env),m=$?.items.find(e=>e.name===c),d=e(a,f.skill.name),v=[`Agent 环境变量: ${c}`,"",`路径: agents.env.${g}.${c}`,`Agent: ${g}`,"类型: "+(p.required?"必填":"可选")];return p.purpose&&v.push(`用途: ${p.purpose}`),p.default&&v.push(`默认值: ${p.default}`),p.example&&v.push(`示例: ${p.example}`),m&&v.push(`当前来源: ${m.source}`),d?.[c]&&v.push("当前 YAML: 已配置"),v.push(`配置向导: roll config setup agent ${g}`),v.join("\n")}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync as e,readFileSync as t,writeFileSync as r}from"node:fs";import{homedir as n}from"node:os";import{resolve as a}from"node:path";import{stringify as i}from"yaml";import{DEFAULT_CONFIG as o,DEFAULT_LLM_MODELS as l,DEFAULT_LLM_PROVIDER as s,LLM_PROVIDER_OPTIONS as c}from"../../config/defaults.js";import{inspectAgentEnvRequirements as u}from"../../config/helpers.js";import{loadAgentsConfig as m,parseConfigDocument as f,resolveConfigPath as d,validateConfigText as g}from"../../config/loader.js";import{encodePathToYaml as p}from"../../config/key-codec.js";import{detectKnownConfigMigrations as h,formatConfigMigrationError as v}from"../../config/migration.js";import{AgentStore as w}from"../../registry/store.js";import{flattenAgentEnvDeclarations as y,isSecretEnvName as k}from"./config-guidance.js";import{ConfigSetupCancelledError as $,clackPromptAdapter as b}from"./config-prompts.js";const A=/\$\{[^}]+\}/u;export async function runConfigSetup(e,t,r=b){try{r.intro("Roll 配置向导"),q();switch(await x(e,r)){case"llm":r.outro(await setupLlm(r));break;case"install":r.outro(await setupInstall(r));break;case"agent":r.outro(await setupAgentEnv(t,r));break;case"shell":r.outro(await setupShell(r))}}catch(e){if(e instanceof $)return void(process.exitCode=1);throw e}}async function x(e,t){if(void 0===e)return t.select({message:"要配置哪一类内容?",options:[{value:"llm",label:"LLM",hint:"默认模型和 provider key"},{value:"install",label:"Install network",hint:"npm registry、重试和超时"},{value:"agent",label:"Agent env",hint:"配置某个 Agent 需要的环境变量"},{value:"shell",label:"Chat shell 工具",hint:"roll chat 内建本机命令执行能力(默认关闭)"}]});if("llm"===e||"install"===e||"agent"===e||"shell"===e)return e;if("bash"===e)return"shell";throw new Error(`未知 setup 模块: ${e}。可用: llm, install, agent, shell`)}export async function setupLlm(e){q();const t=await e.select({message:"选择默认 LLM provider",options:c.map(e=>({value:e,label:e,hint:l[e]})),initialValue:s}),r=await e.text({message:"默认 model",defaultValue:l[t],required:!0}),n=await e.password({message:`${t} API key(可直接输入真实 key,也可输入 \${ENV_VAR})`,required:!0}),a=await e.text({message:"自定义 base-url(可选,留空则不写)",required:!1}),i=q();return N(i.document,["llm","defaultProvider"],t),N(i.document,["llm","defaultModel"],r),N(i.document,["llm","providers",t,"apiKey"],n),a.trim().length>0?N(i.document,["llm","providers",t,"baseUrl"],a.trim()):O(i.document,["llm","providers",t,"baseUrl"]),R(i),C(e,`${t} API key`,n),`已配置 LLM: ${t}/${r}(写入 ${i.configPath})`}export async function setupInstall(e){q();const t=await e.select({message:"当前安装/更新网络更接近哪种场景?",options:[{value:"default-network",label:"默认网络 / 海外网络",hint:"不设置 registry"},{value:"china-dev",label:"国内开发机",hint:"使用 https://registry.npmmirror.com"},{value:"private-registry",label:"企业内网 / 私有 registry"},{value:"advanced",label:"高级自定义"}]}),r=await S(t,e),n=q();switch(r.scenario){case"default-network":P(n.document),O(n.document,["install","registry"]);break;case"china-dev":P(n.document),N(n.document,["install","registry"],"https://registry.npmmirror.com");break;case"private-registry":P(n.document),N(n.document,["install","registry"],r.registry);break;case"advanced":{const{registry:e,fetchRetries:t,preferOffline:a,networkTimeoutMs:i}=r.values;N(n.document,["install","fetchRetries"],t),N(n.document,["install","preferOffline"],a),N(n.document,["install","networkTimeoutMs"],i),e.length>0?N(n.document,["install","registry"],e):O(n.document,["install","registry"]);break}}return R(n),`已配置 install 网络参数(写入 ${n.configPath})`}export async function setupShell(e,t=process.platform){q();const r=await e.confirm({message:"启用 roll chat 内建 shell 工具(允许模型在本机执行命令;默认确认策略受 runtime.approval 及显式 override 控制)?",initialValue:!1});let n=!0,a=!1;const i="win32"===t;r&&i&&e.warn("Windows PowerShell 命令当前全部按 unknown 处理,默认每条命令都需确认;显式 approval override(roll.powershell: auto、roll.exec_command: auto)可覆盖对应工具的默认策略。"),r&&!i&&(n=await e.confirm({message:"安全只读命令(ls/grep 等)自动放行,无需逐条确认?",initialValue:!0})),r&&(a=await e.confirm({message:"启用长跑命令会话(session exec,供 dev server 等常驻进程使用)?",initialValue:!1}));const o=q();N(o.document,["runtime","shell","enabled"],r),r&&!i&&N(o.document,["runtime","shell","autoApproveSafe"],n),r&&N(o.document,["runtime","shell","session","enabled"],a),R(o);return`已${r?"启用":"禁用"} chat shell 工具${r&&i?";Windows PowerShell 默认逐条确认(显式 approval override 可覆盖)":""}(写入 ${o.configPath})`}export async function setupBash(e){return setupShell(e)}async function S(e,t){switch(e){case"default-network":case"china-dev":return{scenario:e};case"private-registry":return{scenario:e,registry:(await t.text({message:"私有 registry URL",placeholder:"https://registry.example.com",required:!0})).trim()};case"advanced":return{scenario:e,values:await V(t)}}}async function V(e){const t=await e.text({message:"npm registry(留空则不写)",placeholder:"https://registry.npmmirror.com"}),r=await e.text({message:"fetch-retries",defaultValue:"3",required:!0,validate:T(0,10)}),n=await e.confirm({message:"是否启用 prefer-offline?",initialValue:!1}),a=await e.text({message:"network-timeout-ms",defaultValue:"120000",required:!0,validate:T(1e4,Number.MAX_SAFE_INTEGER)});return{registry:t.trim(),fetchRetries:Number.parseInt(r,10),preferOffline:n,networkTimeoutMs:Number.parseInt(a,10)}}export async function setupAgentEnv(e,t){q();const{agentsConfig:r}=m(),n=new w(r.dataDir),a=await E(n.list(),e,t),i=u(a.skill.name,a.skill.env,r.env);if(!i)throw new Error(`Agent "${a.skill.name}" 未声明环境变量需求。`);const o={...r.env?.[a.skill.name]??{}},l=y(a.skill.env),s=new Map(i.items.map(e=>[e.name,e]));for(const e of l.filter(e=>e.required)){const r=s.get(e.name),n=await j({declaration:e,yamlValue:o[e.name],source:r?.source,processEnvValue:process.env[e.name]},t);void 0!==n&&(o[e.name]=n)}if(await t.confirm({message:"是否配置可选环境变量?",initialValue:!1}))for(const e of l.filter(e=>!e.required)){const r=s.get(e.name),n=await j({declaration:e,yamlValue:o[e.name],source:r?.source,processEnvValue:process.env[e.name]},t);void 0!==n&&(o[e.name]=n)}const c=q();N(c.document,["agents","env",a.skill.name],o),R(c);for(const[e,r]of Object.entries(o))k(e)&&C(t,e,r);return D(t,a),`已配置 Agent 环境变量: ${a.skill.name}(写入 ${c.configPath})`}async function E(e,t,r){if(void 0!==t){const r=e.find(e=>e.skill.name===t);if(!r)throw new Error(`Agent "${t}" 未找到`);if(!M(r))throw new Error(`Agent "${t}" 未声明环境变量需求。`);return r}const n=e.filter(M);if(0===n.length)throw new Error("没有已注册且声明环境变量需求的 Agent。");const a=await r.select({message:"选择要配置的 Agent",options:n.map(e=>({value:e.skill.name,label:e.skill.name,hint:e.skill.description}))}),i=n.find(e=>e.skill.name===a);if(!i)throw new Error(`Agent "${a}" 未找到`);return i}function M(e){return y(e.skill.env).length>0}async function j(e,t){const{declaration:r,processEnvValue:n,source:a,yamlValue:i}=e,o="agents.env"===a&&_(i),l=[r.required?"必填":"可选",o?"回车保留当前值":void 0].filter(e=>void 0!==e).join(","),s=`${r.name}(${l})`,c=[r.purpose?`用途: ${r.purpose}`:void 0,r.example?`示例: ${r.example}`:void 0,r.default?`默认: ${r.default}`:void 0,o?"当前 YAML 已配置,回车保留当前值。":void 0,void 0===i||o?void 0:"当前 YAML 值为空或占位符未解析,会被视为缺失。","process.env"===a?"当前来源: 当前 shell 临时环境变量,尚未持久写入 YAML。":void 0].filter(e=>void 0!==e).join("\n");if(c.length>0&&t.info(c),"process.env"===a&&"string"==typeof n&&n.length>0){if(await t.confirm({message:`是否将 ${r.name} 的当前 shell 值写入 roll.config.yaml?`,initialValue:!0}))return n}const u=r.required&&!o&&void 0===r.default&&"process.env"!==a,m={message:s,...o&&void 0!==i?{defaultValue:i}:{},...void 0!==r.example?{placeholder:r.example}:{},required:u},f=k(r.name)?await t.password({message:s,required:u}):await t.text(m);return 0===f.trim().length?o?i:void 0:f}function P(e){N(e,["install","fetchRetries"],o.install.fetchRetries),N(e,["install","preferOffline"],o.install.preferOffline),N(e,["install","networkTimeoutMs"],o.install.networkTimeoutMs)}function q(){const r=d(),i=r??a(n(),"roll.config.yaml");if(void 0===r||!e(i))return{configPath:i,existed:!1,document:L()};const o=t(i,"utf-8"),l=f(o,i),s=h(l);if(s.needsMigration)throw new Error(v(i,s));return{configPath:i,existed:!0,raw:o,document:l}}function L(){return{llm:{"default-provider":o.llm.defaultProvider,"default-model":o.llm.defaultModel,providers:{}},ask:{},agents:{"data-dir":o.agents.dataDir}}}function R(e){const t=i(e.document,{lineWidth:0});g(t,e.configPath),e.existed&&void 0!==e.raw&&r(I(e.configPath),e.raw,"utf-8"),r(e.configPath,t,"utf-8")}function I(e){const t=new Date;return`${e}.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 N(e,t,r){const n=p(t),a=n[n.length-1];if(void 0===a)throw new Error("配置路径不能为空");let i=e;for(const e of n.slice(0,-1)){const t=i[e];("object"!=typeof t||null===t||Array.isArray(t))&&(i[e]={}),i=i[e]}i[a]=r}function O(e,t){const r=p(t),n=r[r.length-1];if(void 0===n)return;let a=e;for(const e of r.slice(0,-1)){if("object"!=typeof a||null===a||Array.isArray(a))return;a=a[e]}"object"!=typeof a||null===a||Array.isArray(a)||delete a[n]}function T(e,t){return r=>{if(!/^\d+$/u.test(r))return"请输入整数";const n=Number.parseInt(r,10);return n<e||n>t?`请输入 ${String(e)} 到 ${String(t)} 之间的整数`:void 0}}function _(e){return"string"==typeof e&&e.length>0&&!A.test(e)}function C(e,t,r){0===r.trim().length||A.test(r)||e.warn(`${t} 将以明文写入 roll.config.yaml,建议改用 \${ENV_VAR} 引用并避免把配置文件提交到版本库。`)}function D(e,t){const r=t.skill.name;switch(t.runtime.ownership){case"core-managed":e.info(`需重启后生效:roll agent stop ${r} && roll agent start ${r}`);break;case"external-managed":e.info(`请重启你自行管理的 "${r}" 进程以加载新环境变量。`);break;case"on-demand":e.info(`下次 roll run / roll ask 调用 "${r}" 时自动生效。`)}}
|
|
1
|
+
import{isDeepStrictEqual as e}from"node:util";import{ConfigApplicationService as t,createConfigPatches as r}from"../../config/application-service.js";import{ConfigRevisionConflictError as n}from"../../config/document-store.js";import{DEFAULT_CONFIG as i,DEFAULT_LLM_MODELS as s,DEFAULT_LLM_PROVIDER as o,LLM_PROVIDER_OPTIONS as a}from"../../config/defaults.js";import{inspectAgentEnvRequirements as l}from"../../config/helpers.js";import{decodeFromYaml as c}from"../../config/key-codec.js";import{loadAgentsConfig as u}from"../../config/loader.js";import{AgentStore as f}from"../../registry/store.js";import{flattenAgentEnvDeclarations as m,isSecretEnvDeclaration as d}from"../../config/guidance.js";import{ConfigSetupCancelledError as p,clackPromptAdapter as g}from"./config-prompts.js";const v=/\$\{[^}]+\}/u;export async function runConfigSetup(e,t,r=g){try{r.intro("Roll 配置向导"),x();switch(await h(e,r)){case"llm":r.outro(await setupLlm(r));break;case"install":r.outro(await setupInstall(r));break;case"agent":r.outro(await setupAgentEnv(t,r));break;case"shell":r.outro(await setupShell(r))}}catch(e){if(e instanceof p)return void(process.exitCode=1);throw e}}async function h(e,t){if(void 0===e)return t.select({message:"要配置哪一类内容?",options:[{value:"llm",label:"LLM",hint:"默认模型和 provider key"},{value:"install",label:"Install network",hint:"npm registry、重试和超时"},{value:"agent",label:"Agent env",hint:"配置某个 Agent 需要的环境变量"},{value:"shell",label:"Chat shell 工具",hint:"roll chat 内建本机命令执行能力(默认关闭)"}]});if("llm"===e||"install"===e||"agent"===e||"shell"===e)return e;if("bash"===e)return"shell";throw new Error(`未知 setup 模块: ${e}。可用: llm, install, agent, shell`)}export async function setupLlm(e){const t=x(),r=await e.select({message:"选择默认 LLM provider",options:a.map(e=>({value:e,label:e,hint:s[e]})),initialValue:o}),n=await e.text({message:"默认 model",defaultValue:s[r],required:!0}),i=await e.password({message:`${r} API key(可直接输入真实 key,也可输入 \${ENV_VAR})`,required:!0}),l=await e.text({message:"自定义 base-url(可选,留空则不写)",required:!1});return M(t.document,["llm","defaultProvider"],r),M(t.document,["llm","defaultModel"],n),M(t.document,["llm","providers",r,"apiKey"],i),l.trim().length>0?M(t.document,["llm","providers",r,"baseUrl"],l.trim()):q(t.document,["llm","providers",r,"baseUrl"]),j(t),T(e,`${r} API key`,i),`已配置 LLM: ${r}/${n}(写入 ${t.configPath})`}export async function setupInstall(e){const t=x(),r=await e.select({message:"当前安装/更新网络更接近哪种场景?",options:[{value:"default-network",label:"默认网络 / 海外网络",hint:"不设置 registry"},{value:"china-dev",label:"国内开发机",hint:"使用 https://registry.npmmirror.com"},{value:"private-registry",label:"企业内网 / 私有 registry"},{value:"advanced",label:"高级自定义"}]}),n=await w(r,e);switch(n.scenario){case"default-network":A(t.document),q(t.document,["install","registry"]);break;case"china-dev":A(t.document),M(t.document,["install","registry"],"https://registry.npmmirror.com");break;case"private-registry":A(t.document),M(t.document,["install","registry"],n.registry);break;case"advanced":{const{registry:e,fetchRetries:r,preferOffline:i,networkTimeoutMs:s}=n.values;M(t.document,["install","fetchRetries"],r),M(t.document,["install","preferOffline"],i),M(t.document,["install","networkTimeoutMs"],s),e.length>0?M(t.document,["install","registry"],e):q(t.document,["install","registry"]);break}}return j(t),`已配置 install 网络参数(写入 ${t.configPath})`}export async function setupShell(e,t=process.platform){const r=x(),n=await e.confirm({message:"启用 roll chat 内建 shell 工具(允许模型在本机执行命令;默认确认策略受 runtime.approval 及显式 override 控制)?",initialValue:!1});let i=!0,s=!1;const o="win32"===t;n&&o&&e.warn("Windows PowerShell 命令当前全部按 unknown 处理,默认每条命令都需确认;显式 approval override(roll.powershell: auto、roll.exec_command: auto)可覆盖对应工具的默认策略。"),n&&!o&&(i=await e.confirm({message:"安全只读命令(ls/grep 等)自动放行,无需逐条确认?",initialValue:!0})),n&&(s=await e.confirm({message:"启用长跑命令会话(session exec,供 dev server 等常驻进程使用)?",initialValue:!1})),M(r.document,["runtime","shell","enabled"],n),n&&!o&&M(r.document,["runtime","shell","autoApproveSafe"],i),n&&M(r.document,["runtime","shell","session","enabled"],s),j(r);return`已${n?"启用":"禁用"} chat shell 工具${n&&o?";Windows PowerShell 默认逐条确认(显式 approval override 可覆盖)":""}(写入 ${r.configPath})`}export async function setupBash(e){return setupShell(e)}async function w(e,t){switch(e){case"default-network":case"china-dev":return{scenario:e};case"private-registry":return{scenario:e,registry:(await t.text({message:"私有 registry URL",placeholder:"https://registry.example.com",required:!0})).trim()};case"advanced":return{scenario:e,values:await y(t)}}}async function y(e){const t=await e.text({message:"npm registry(留空则不写)",placeholder:"https://registry.npmmirror.com"}),r=await e.text({message:"fetch-retries",defaultValue:"3",required:!0,validate:I(0,10)}),n=await e.confirm({message:"是否启用 prefer-offline?",initialValue:!1}),i=await e.text({message:"network-timeout-ms",defaultValue:"120000",required:!0,validate:I(1e4,Number.MAX_SAFE_INTEGER)});return{registry:t.trim(),fetchRetries:Number.parseInt(r,10),preferOffline:n,networkTimeoutMs:Number.parseInt(i,10)}}export async function setupAgentEnv(e,t){const r=x(),{agentsConfig:n}=u(),i=new f(n.dataDir),s=await b(i.list(),e,t),o=l(s.skill.name,s.skill.env,n.env);if(!o)throw new Error(`Agent "${s.skill.name}" 未声明环境变量需求。`);const a={...L(r.document,s.skill.name)},c=m(s.skill.env),p=new Map(o.items.map(e=>[e.name,e]));for(const e of c.filter(e=>e.required)){const r=p.get(e.name),n=await $({declaration:e,yamlValue:a[e.name],source:r?.source,processEnvValue:process.env[e.name]},t);void 0!==n&&(a[e.name]=n)}if(await t.confirm({message:"是否配置可选环境变量?",initialValue:!1}))for(const e of c.filter(e=>!e.required)){const r=p.get(e.name),n=await $({declaration:e,yamlValue:a[e.name],source:r?.source,processEnvValue:process.env[e.name]},t);void 0!==n&&(a[e.name]=n)}M(r.document,["agents","env",s.skill.name],a),j(r);for(const[e,r]of Object.entries(a)){const n=c.find(t=>t.name===e);void 0!==n&&d(n)&&T(t,e,r)}return _(t,s),`已配置 Agent 环境变量: ${s.skill.name}(写入 ${r.configPath})`}async function b(e,t,r){if(void 0!==t){const r=e.find(e=>e.skill.name===t);if(!r)throw new Error(`Agent "${t}" 未找到`);if(!k(r))throw new Error(`Agent "${t}" 未声明环境变量需求。`);return r}const n=e.filter(k);if(0===n.length)throw new Error("没有已注册且声明环境变量需求的 Agent。");const i=await r.select({message:"选择要配置的 Agent",options:n.map(e=>({value:e.skill.name,label:e.skill.name,hint:e.skill.description}))}),s=n.find(e=>e.skill.name===i);if(!s)throw new Error(`Agent "${i}" 未找到`);return s}function k(e){return m(e.skill.env).length>0}async function $(e,t){const{declaration:r,processEnvValue:n,source:i,yamlValue:s}=e,o=N(s),a=o&&v.test(s),l=[r.required?"必填":"可选",o?"回车保留当前值":void 0].filter(e=>void 0!==e).join(","),c=`${r.name}(${l})`,u=[r.purpose?`用途: ${r.purpose}`:void 0,r.example?`示例: ${r.example}`:void 0,r.default?`默认: ${r.default}`:void 0,o?a?"当前 YAML 使用环境变量占位符,回车会原样保留。":"当前 YAML 已配置,回车保留当前值。":void 0,a&&"agents.env"!==i?"当前占位符未解析,运行时仍会把该值视为缺失。":void 0,"process.env"===i?"当前来源: 当前 shell 临时环境变量,尚未持久写入 YAML。":void 0].filter(e=>void 0!==e).join("\n");if(u.length>0&&t.info(u),"process.env"===i&&"string"==typeof n&&n.length>0){if(await t.confirm({message:`是否将 ${r.name} 的当前 shell 值写入 roll.config.yaml?`,initialValue:!0}))return n}const f=r.required&&!o&&void 0===r.default&&"process.env"!==i,m={message:c,...o&&void 0!==s?{defaultValue:s}:{},...void 0!==r.example?{placeholder:r.example}:{},required:f},p=d(r)?await t.password({message:c,required:f}):await t.text(m);return 0===p.trim().length?o?s:void 0:p}function A(e){M(e,["install","fetchRetries"],i.install.fetchRetries),M(e,["install","preferOffline"],i.install.preferOffline),M(e,["install","networkTimeoutMs"],i.install.networkTimeoutMs)}function x(){const e=new t,r=e.readForRepair(),i=V(e);if(r.revision!==i.revision)throw new n(r.revision,i.revision);return{configPath:r.configPath,existed:r.existed,document:R(r.persisted),basePersisted:i.persisted,snapshot:r,service:e}}function j(e){const t=r(e.snapshot.persisted,e.document),n=V(e.service);E(e.basePersisted,n.persisted,t,e.snapshot.revision,n.revision),e.service.savePatches(t,n.revision)}function V(e){const t=e.store.read(),r=c(t.persisted);if(!S(r))throw new Error(`配置内容必须是 object: ${t.configPath}`);return{revision:t.revision,persisted:r}}function E(t,r,i,s,o){for(const a of i){const i=P(t,a.path),l=P(r,a.path);if(i.exists!==l.exists||!e(i.value,l.value))throw new n(s,o)}}function P(e,t){let r=e;for(const e of t)if("number"!=typeof e){if(!S(r)||!(e in r))return{exists:!1,value:void 0};r=r[e]}else{if(!Array.isArray(r)||!(e in r))return{exists:!1,value:void 0};r=r[e]}return{exists:!0,value:r}}function M(e,t,r){const n=t[t.length-1];if(void 0===n)throw new Error("配置路径不能为空");let i=e;for(const e of t.slice(0,-1)){const t=i[e];("object"!=typeof t||null===t||Array.isArray(t))&&(i[e]={}),i=i[e]}i[n]=r}function q(e,t){const r=t[t.length-1];if(void 0===r)return;let n=e;for(const e of t.slice(0,-1)){if("object"!=typeof n||null===n||Array.isArray(n))return;n=n[e]}"object"!=typeof n||null===n||Array.isArray(n)||delete n[r]}function L(e,t){const r=e.agents,n=S(r)?r.env:void 0,i=S(n)?n[t]:void 0;return S(i)?Object.fromEntries(Object.entries(i).filter(e=>"string"==typeof e[1])):{}}function R(e){return Object.fromEntries(Object.entries(e).map(([e,t])=>[e,O(t)]))}function O(e){return Array.isArray(e)?e.map(O):S(e)?R(e):e}function S(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function I(e,t){return r=>{if(!/^\d+$/u.test(r))return"请输入整数";const n=Number.parseInt(r,10);return n<e||n>t?`请输入 ${String(e)} 到 ${String(t)} 之间的整数`:void 0}}function N(e){return"string"==typeof e&&e.length>0}function T(e,t,r){0===r.trim().length||v.test(r)||e.warn(`${t} 将以明文写入 roll.config.yaml,建议改用 \${ENV_VAR} 引用并避免把配置文件提交到版本库。`)}function _(e,t){const r=t.skill.name;switch(t.runtime.ownership){case"core-managed":e.info(`需重启后生效:roll agent stop ${r} && roll agent start ${r}`);break;case"external-managed":e.info(`请重启你自行管理的 "${r}" 进程以加载新环境变量。`);break;case"on-demand":e.info(`下次 roll run / roll ask 调用 "${r}" 时自动生效。`)}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{defineCommand as o}from"citty";import{
|
|
1
|
+
import{defineCommand as o}from"citty";import{existsSync as e,readFileSync as r}from"node:fs";import{homedir as t}from"node:os";import{resolve as n}from"node:path";import{createInterface as i}from"node:readline/promises";import{ConfigApplicationService as s}from"../../config/application-service.js";import{DEFAULT_CONFIG as c,DEFAULT_LLM_MODELS as a,LLM_PROVIDER_OPTIONS as l}from"../../config/defaults.js";import{inspectConfigFile as f,loadConfig as d,parseConfigDocument as u,resolveConfigPath as p}from"../../config/loader.js";import{createConfigRevision as g}from"../../config/document-store.js";import{decodeFromYaml as m,normalizeUserPath as v}from"../../config/key-codec.js";import{applyKnownConfigMigrations as h}from"../../config/migration.js";import{explainConfig as y}from"./config-explain.js";import{runConfigSetup as $}from"./config-setup.js";export default o({meta:{description:"管理全局配置"},args:{action:{type:"positional",description:"操作(init/get/set/migrate/setup/explain)",required:!0},key:{type:"positional",description:"配置键(get/set 时使用,用英文句点 `.` 分隔,如 ask.confirm-threshold)",required:!1},value:{type:"positional",description:"配置值(set 时使用)",required:!1}},async run({args:o}){try{if("init"===o.action)return void await Y();if("get"===o.action)return void A(o.key);if("set"===o.action)return void I(o.key,o.value);if("migrate"===o.action)return void migrateConfig();if("setup"===o.action)return process.stdin.isTTY?void await $(o.key,o.value):(console.error("✗ `roll config setup` 需要交互式终端。"),console.error(" 非交互环境请改用 `roll config set <key> <value>` 或直接编辑 roll.config.yaml。"),console.error(" 查看配置说明:`roll config explain <key>`。"),void(process.exitCode=1));if("explain"===o.action)return void y(o.key);console.error(`✗ 未知操作: ${o.action}。可用: init, get, set, migrate, setup, explain`),process.exitCode=1}catch(o){const e=o instanceof Error?o.message:String(o);console.error(`✗ ${e}`),process.exitCode=1}}});const P={anthropic:"ANTHROPIC_API_KEY",openai:"OPENAI_API_KEY",qwen:"DASHSCOPE_API_KEY",deepseek:"DEEPSEEK_API_KEY",xai:"XAI_API_KEY"};function w(o,e){const r=o?.trim();return r||e}function k(o){return l.some(e=>e===o)}function E(o){return k(o)?a[o]:c.llm.defaultModel}function x(o){return k(o)?P[o]:P.anthropic}function C({provider:o,model:e,apiKeyEnv:r}){return`llm:\n default-provider: ${o}\n default-model: ${e}\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 j(){if(!process.stdin.isTTY){const[o,e,t]=r(0,"utf-8").split(/\r?\n/u),n=w(o,c.llm.defaultProvider);return{provider:n,model:w(e,E(n)),apiKeyEnv:w(t,x(n))}}const o=i({input:process.stdin,output:process.stderr}),e=w(await o.question(`默认 LLM provider (${l.join("/")}) [${c.llm.defaultProvider}]: `),c.llm.defaultProvider),t=E(e),n=w(await o.question(`默认 model [${t}]: `),t),s=x(e),a=w(await o.question(`API Key 环境变量名 [${s}]: `),s);return o.close(),{provider:e,model:n,apiKeyEnv:a}}async function Y(){const o=p()??n(t(),"roll.config.yaml");let c,a=!1;if(e(o)){c=g(r(o,"utf-8"));const e=f({configPath:o});switch(e.status){case"needs-migration":console.error(`⚠ 现有配置文件需要迁移: ${o}`),console.error(" 建议先运行 `roll config migrate`,再决定是否重新初始化。");break;case"invalid":a=!0,console.error(`⚠ 现有配置文件存在问题:\n${e.error.message}`)}if(console.error(`⚠ 配置文件已存在: ${o}`),!process.stdin.isTTY)throw new Error("非交互模式下不会覆盖现有配置文件,请手动删除后重试。");const t=i({input:process.stdin,output:process.stderr}),n=await t.question("是否覆盖?(y/N) ");if(t.close(),"y"!==n.toLowerCase())return void console.error("已取消。")}const l=C(await j()),d=new s({configPath:o}),u=void 0===c?d.saveYaml(l):a?d.replaceYamlForInit(l,c):d.saveYaml(l,c);console.log(`✓ 配置文件已创建: ${o}`),void 0!==u.backupPath&&console.log(`✓ 已备份原文件: ${u.backupPath}`)}export function migrateConfig(){const o=f();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 e=u(o.raw,o.configPath),r=h(e);if(!r.ok){const o=r.issues.map(o=>` - ${o.message}`).join("\n");throw new Error(`配置无法自动迁移:\n${o}`)}if(!r.changed)return void console.log(`✓ 配置文件已是最新格式,无需迁移: ${o.configPath}`);const t=new s({configPath:o.configPath}).saveStructured(m(r.document),g(o.raw));console.log(`✓ 配置文件已迁移: ${o.configPath}`),void 0!==t.backupPath&&console.log(`✓ 已备份原文件: ${t.backupPath}`);for(const o of r.summary)console.log(` - ${o}`)}function A(o){const{config:e,configPath:r}=d();if(!o)return console.log(JSON.stringify(e,null,2)),void(r&&console.error(`(来源: ${r})`));const t=v(o.split("."));let n=e;for(const e of t){if("object"!=typeof n||null===n)return console.error(`✗ 配置键 "${o}" 不存在`),void(process.exitCode=1);n=n[e]}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 I(o,e){if(!o||void 0===e)return console.error("✗ 用法: roll config set <key> <value>"),console.error(" 示例: roll config set ask.confirm-threshold 0.5"),void(process.exitCode=1);const{configPath:r}=d();if(!r)return console.error("✗ 未找到配置文件。请先运行 roll config init"),void(process.exitCode=1);const t=v(o.split("."));if(0===t.length)return console.error("✗ 配置键不能为空"),void(process.exitCode=1);let n=e;"true"===e?n=!0:"false"===e?n=!1:/^\d+(\.\d+)?$/.test(e)&&(n=Number(e));const i=new s({configPath:r}),c=i.read();i.savePatches([{op:"set",path:t,value:n}],c.revision),console.log(`✓ ${o} = ${String(n)}`),console.error(` (已写入: ${r})`)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{defineCommand as e}from"citty";import{existsSync as s,mkdirSync as t,writeFileSync as n}from"node:fs";import{stringify as a}from"yaml";import{getAgentEnv as o,getAgentEnvFromAgentsConfig as r,inspectAgentEnvRequirements as i}from"../../config/helpers.js";import{collectBrowserConfigWarnings as l}from"../../config/browser-inspection.js";import{inspectConfigFile as c,loadAgentsConfig as u,loadConfig as g,parseConfigDocument as m,validateConfigText as f}from"../../config/loader.js";import{applyKnownConfigMigrations as p}from"../../config/migration.js";import{inspectAgentRuntimeEnvRequirements as d,summarizeAgentRuntimeEnvReport as h}from"../../config/runtime-env.js";import{inspectAgentRuntimeEnv as w}from"../../mcp/agent-diagnostics.js";import{cleanupOrphanAgentRuntimeMetadata as b,inspectManagedAgentRuntime as x}from"../../registry/process-manager.js";import{AgentStore as k}from"../../registry/store.js";const $={ok:"✓",warn:"⚠",fail:"✗"},y={applied:"✓",skipped:"•",failed:"✗"},S={major:22,minor:6,patch:0};function j(e){const s="Browser security (browser-use-agent)";if("unverified"===e.status)return{name:s,status:"warn",message:`未校验: ${e.message}`,fix:"启动 browser-use-agent 后重新运行 `roll doctor`"};const t=e.payload.security;if(void 0===t)return{name:s,status:"warn",message:"browser_status.security 未返回,无法确认浏览器安全策略",fix:"升级并重启 browser-use-agent"};const n=e.payload.policyWarnings??[];return{name:s,status:n.length>0?"warn":"ok",message:I(t,e.payload.toolPolicy,n),...n.length>0?{fix:"检查 BROWSER_SECURITY_JSON 与 BROWSER_USE_POLICY_JSON 的组合;Boss 日常编排建议 browser actionPolicy=log"}:{}}}function v(e,s){const t=l(e,s);if(0!==t.length)return{name:"Browser config (browser.instances)",status:"warn",message:t.join(";"),fix:"在 roll.config.yaml 中配置 browser.default-instance,并移除 agents.env.browser-use-agent 下已废弃的 BROWSER_CDP_* / BROWSER_USER_DATA_DIR 等实例身份 env",details:{type:"browser-config",warnings:t}}}function P(e,s){const t="Browser runtime (browser-use-agent)",n=Object.keys(e.instances);if(0===n.length)return{name:t,status:"ok",message:"跳过(未配置 browser.instances,使用 legacy 单实例运行时)"};if("unverified"===s.status)return{name:t,status:"warn",message:`未校验: ${s.message}`,fix:"启动 browser-use-agent 后重新运行 `roll doctor`"};const a=s.payload.instances??[];if(0===a.length)return{name:t,status:"warn",message:"browser_status.instances 未返回,无法确认浏览器实例运行态",fix:"升级并重启 browser-use-agent"};const o=new Set(a.map(e=>e.id)),r=n.filter(e=>!o.has(e)),i=a.filter(A),l=a.filter(e=>"missing"===e.tracking.source),c={type:"browser-runtime",declaredInstanceIds:n,...void 0!==e.defaultInstance?{defaultInstanceId:e.defaultInstance}:{},runtimeInstances:a,missingInstanceIds:r,unhealthyInstanceIds:i.map(e=>e.id),missingTrackingInstanceIds:l.map(e=>e.id)},u=r.length>0||i.length>0||l.length>0?"warn":"ok";return{name:t,status:u,details:c,message:[`declared=${n.join(",")}`,`runtime=${a.map(D).join(";")}`,...r.length>0?[`missing=${r.join(",")}`]:[],...l.length>0?[`trackingMissing=${l.map(e=>e.id).join(",")}`]:[]].join(" "),..."warn"===u?{fix:"检查 browser.instances、BROWSER_INSTANCES_JSON、CDP 端口和 profile 目录;必要时重启 browser-use-agent"}:{}}}function A(e){return!(e.cdp.versionReachable&&e.cdp.listReachable&&e.profile.exists&&e.profile.writable)}function D(e){return`${e.id}:cdp=${e.cdp.versionReachable&&e.cdp.listReachable?"ok":"warn"},profile=${e.profile.exists&&e.profile.writable?"ok":"warn"},tracking=${e.tracking.source}`}function I(e,s,t){const n=e.domainAllowlist.length>0?e.domainAllowlist.join(", "):"none",a=O(s),o=t.length>0?`; warnings=${t.map(e=>e.code).join(", ")}`:"";return`actionPolicy=${e.actionPolicy}; foregroundPolicy=${e.foregroundPolicy}; domainAllowlist=${n}; maxPageContentBytes=${String(e.maxPageContentBytes)}; maxSnapshotNodes=${String(e.maxSnapshotNodes)}; toolPolicy=${a}`+o}function O(e){if(void 0===e)return"unavailable";const s=Object.entries(e.tools),t=s.length>0?s.map(([e,s])=>`${e}:${s.policy}`).join(","):"none";return`approvalTtlMs=${String(e.approvalTtlMs)},tools=${t}`}export function isNodeVersionSupported(e){const s=/^(\d+)\.(\d+)\.(\d+)/.exec(e);if(!s)return!1;const t=Number(s[1]),n=Number(s[2]),a=Number(s[3]);return t>S.major||!(t<S.major)&&(n>S.minor||!(n<S.minor)&&a>=S.patch)}export default e({meta:{description:"诊断 Roll 配置、Agent 注册表和运行时状态"},args:{json:{type:"boolean",description:"JSON 格式输出",default:!1},"fix-plan":{type:"boolean",description:"输出可执行修复建议(不自动修改)",default:!1},fix:{type:"boolean",description:"执行安全自动修复:配置自动迁移、创建 dataDir、清理孤儿 runtime 元数据",default:!1}},async run({args:e}){const t=[],n=[],a=!0===e.fix;let l;const m=process.versions.node;t.push(isNodeVersionSupported(m)?{name:"Node.js 版本",status:"ok",message:`v${m}`}:{name:"Node.js 版本",status:"fail",message:`v${m} (需要 ≥22.6.0)`,fix:"升级 Node.js 到 22.6.0 或更高版本"});let f,p=c();if("needs-migration"===p.status&&a){const e=R(p);n.push(e),"applied"===e.status&&(p=c())}switch(p.status){case"not-found":t.push({name:"配置文件",status:"warn",message:"未找到,使用默认配置",fix:"运行 `roll config init` 生成显式配置文件"}),l=g().config;break;case"valid":t.push({name:"配置文件",status:"ok",message:p.configPath}),l=p.config;break;case"needs-migration":t.push({name:"配置文件",status:"warn",message:`${p.configPath} (需要迁移,运行 roll config migrate)`,fix:"运行 `roll config migrate`,确认备份文件后提交配置变更"});break;case"invalid":t.push({name:"配置文件",status:"fail",message:p.error.message,fix:"修正配置文件语法或运行 `roll config migrate` 处理 breaking schema change"})}try{f=u().agentsConfig}catch(e){t.push({name:"Agent 配置",status:"fail",message:e instanceof Error?e.message:String(e),fix:"检查 `roll.config.yaml` 的 `agents` 段;如配置版本过旧,运行 `roll config migrate`"})}if(l){const e=Object.keys(l.llm.providers);if(e.length>0){const s=e.filter(e=>{const s=l.llm.providers[e]?.apiKey??"";return s.startsWith("${")||0===s.length});s.length>0?t.push({name:"LLM Providers",status:"warn",message:`${e.join(", ")} (${s.join(", ")} API key 未设置)`,fix:`配置 ${s.map(e=>`llm.providers.${e}.apiKey`).join(", ")} 或对应环境变量`}):t.push({name:"LLM Providers",status:"ok",message:e.join(", ")})}else t.push({name:"LLM Providers",status:"warn",message:"未配置任何 provider",fix:"运行 `roll config init` 或手动配置 `llm.providers`"})}else"needs-migration"===p.status&&t.push({name:"LLM Providers",status:"warn",message:"配置需要迁移,跳过完整 LLM 配置校验",fix:"先运行 `roll config migrate`,再重新执行 `roll doctor`"});if(f){const e=f.dataDir;let c=s(e);if(!c&&a){const t=N(e);n.push(t),c=s(e)}t.push(c?{name:"Agent 数据目录",status:"ok",message:e}:{name:"Agent 数据目录",status:"warn",message:`${e} (不存在,首次 add 时创建)`,fix:"运行 `roll doctor --fix` 创建目录,或手动创建该目录"});const u=new k(f.dataDir).list();if(t.push({name:"已注册 Agent",status:u.length>0?"ok":"warn",message:u.length>0?`${String(u.length)} 个 (${u.map(e=>e.skill.name).join(", ")})`:"无",...0===u.length?{fix:"运行 `roll agent add <path|git-url>` 注册 Agent"}:{}}),l&&Object.keys(l.browser.instances).length>0&&!u.some(e=>"browser-use-agent"===e.skill.name)&&t.push({name:"Browser runtime (browser-use-agent)",status:"fail",message:"已配置 browser.instances,但 browser-use-agent 未注册",fix:"运行 `roll agent add agents/browser-use` 或安装并注册 browser-use-agent",details:{type:"browser-runtime",declaredInstanceIds:Object.keys(l.browser.instances),missingAgent:"browser-use-agent"}}),l&&Object.keys(l.browser.instances).length>0){const e=v(l.browser,o(l,"browser-use-agent"));e&&t.push(e)}for(const e of u){let s;const c=async()=>l?(s??=await w(e,{config:l}),s):{status:"unverified",reason:"connection-failed",message:"无法校验运行态: 配置未加载"};if("core-managed"===e.runtime.ownership){let s=x(e,f.dataDir);if(a&&s.issues.some(e=>"orphan-sidecar"===e.code)){const t=C(f.dataDir,e.skill.name);n.push(t),"applied"===t.status&&(s=x(e,f.dataDir))}(void 0!==s.pid||s.issues.length>0)&&t.push({name:`Agent runtime (${e.skill.name})`,status:s.issues.length>0?"warn":"ok",message:s.issues.length>0?s.issues.map(e=>e.message).join(";"):`PID ${String(s.pid)},runtime sidecar 与当前配置一致`,...s.issues.length>0?{fix:E(s.issues.map(e=>e.fix)).join(";")}:{}})}const u=i(e.skill.name,e.skill.env,f.env);if(!u){"browser-use-agent"===e.skill.name&&(s=await c(),l&&t.push(P(l.browser,s)),t.push(j(s)));continue}s=await c();const g=void 0!==l?o(l,e.skill.name):r(f,e.skill.name),m=d(u,g,s),p=h(m);t.push({name:`Agent 环境变量 (${e.skill.name})`,status:p.status,message:p.message,..."ok"===p.status?{}:{fix:`在 \`roll.config.yaml\` 的 \`agents.env.${e.skill.name}\` 或 shell 环境中配置缺失变量`}}),"browser-use-agent"===e.skill.name&&(l&&t.push(P(l.browser,s)),t.push(j(s)))}}const b=t.some(e=>"fail"===e.status),$=n.some(e=>"failed"===e.status),y=t.some(e=>"warn"===e.status),S=!0===e["fix-plan"]||a;if(e.json){const e=a?formatDoctorJsonOutput(t,{fixPlan:S,fixes:n}):formatDoctorChecksForJsonOutput(t,{fixPlan:S});return console.log(JSON.stringify(e,null,2)),void((b||$)&&(process.exitCode=1))}console.log("Roll Agent 系统诊断\n");for(const e of t)for(const s of formatDoctorCheckLines(e,{fixPlan:S}))console.log(s);if(a)if(console.log("\nFix 执行结果\n"),0===n.length)console.log(" ✓ 无可自动修复项");else for(const e of n)for(const s of formatDoctorFixLines(e))console.log(s);console.log(b||$?"\n存在问题,请修复后重试。":y?"\n存在警告,可按 fix plan 处理。":"\n系统状态正常。"),(b||$)&&(process.exitCode=1)}});export function formatDoctorCheckLines(e,s){const t=[` ${$[e.status]} ${e.name}: ${e.message}`];return s.fixPlan&&e.fix&&t.push(` fix: ${e.fix}`),t}export function formatDoctorChecksForJsonOutput(e,s){return s.fixPlan?e:e.map(({fix:e,...s})=>s)}export function formatDoctorJsonOutput(e,s){return{checks:formatDoctorChecksForJsonOutput(e,{fixPlan:s.fixPlan}),fixes:s.fixes}}export function formatDoctorFixLines(e){return[` ${y[e.status]} ${e.name}: ${e.message}`]}function R(e){if(!e.report.canAutoMigrate)return{name:"配置迁移",status:"skipped",message:"检测到需要人工处理的配置迁移项,未自动修改"};try{const s=m(e.raw,e.configPath),t=p(s);if(!t.ok)return{name:"配置迁移",status:"skipped",message:`无法自动迁移:${_(t.issues)}`};if(!t.changed)return{name:"配置迁移",status:"skipped",message:"配置文件已是最新格式"};const o=a(t.document,{lineWidth:0});f(o,e.configPath);const r=L(e.configPath);return n(r,e.raw,"utf-8"),n(e.configPath,o,"utf-8"),{name:"配置迁移",status:"applied",message:`已迁移 ${e.configPath},备份 ${r}`}}catch(e){return{name:"配置迁移",status:"failed",message:e instanceof Error?e.message:String(e)}}}function N(e){try{return t(e,{recursive:!0}),{name:"Agent 数据目录",status:"applied",message:`已创建 ${e}`}}catch(e){return{name:"Agent 数据目录",status:"failed",message:e instanceof Error?e.message:String(e)}}}function C(e,s){try{return b(e,s)?{name:`Agent runtime (${s})`,status:"applied",message:"已清理孤儿 runtime 元数据"}:{name:`Agent runtime (${s})`,status:"skipped",message:"检测到活动 PID,跳过 runtime 元数据清理"}}catch(e){return{name:`Agent runtime (${s})`,status:"failed",message:e instanceof Error?e.message:String(e)}}}function L(e){const s=new Date;return`${e}.bak.${[s.getFullYear().toString().padStart(4,"0"),(s.getMonth()+1).toString().padStart(2,"0"),s.getDate().toString().padStart(2,"0"),"-",s.getHours().toString().padStart(2,"0"),s.getMinutes().toString().padStart(2,"0"),s.getSeconds().toString().padStart(2,"0")].join("")}`}function _(e){return e.map(e=>e.message).join(";")}function E(e){return[...new Set(e)]}
|
|
1
|
+
import{defineCommand as e}from"citty";import{existsSync as s,mkdirSync as t}from"node:fs";import{ConfigApplicationService as n}from"../../config/application-service.js";import{createConfigRevision as o}from"../../config/document-store.js";import{getAgentEnv as a,getAgentEnvFromAgentsConfig as r,inspectAgentEnvRequirements as i}from"../../config/helpers.js";import{collectBrowserConfigWarnings as l}from"../../config/browser-inspection.js";import{inspectConfigFile as c,loadAgentsConfig as u,loadConfig as m,parseConfigDocument as g}from"../../config/loader.js";import{decodeFromYaml as f}from"../../config/key-codec.js";import{applyKnownConfigMigrations as p}from"../../config/migration.js";import{inspectAgentRuntimeEnvRequirements as d,summarizeAgentRuntimeEnvReport as h}from"../../config/runtime-env.js";import{inspectAgentRuntimeEnv as w}from"../../mcp/agent-diagnostics.js";import{cleanupOrphanAgentRuntimeMetadata as b,inspectManagedAgentRuntime as x}from"../../registry/process-manager.js";import{AgentStore as k}from"../../registry/store.js";const $={ok:"✓",warn:"⚠",fail:"✗"},y={applied:"✓",skipped:"•",failed:"✗"},j={major:22,minor:6,patch:0};function v(e){const s="Browser security (browser-use-agent)";if("unverified"===e.status)return{name:s,status:"warn",message:`未校验: ${e.message}`,fix:"启动 browser-use-agent 后重新运行 `roll doctor`"};const t=e.payload.security;if(void 0===t)return{name:s,status:"warn",message:"browser_status.security 未返回,无法确认浏览器安全策略",fix:"升级并重启 browser-use-agent"};const n=e.payload.policyWarnings??[];return{name:s,status:n.length>0?"warn":"ok",message:O(t,e.payload.toolPolicy,n),...n.length>0?{fix:"检查 BROWSER_SECURITY_JSON 与 BROWSER_USE_POLICY_JSON 的组合;Boss 日常编排建议 browser actionPolicy=log"}:{}}}function P(e,s){const t=l(e,s);if(0!==t.length)return{name:"Browser config (browser.instances)",status:"warn",message:t.join(";"),fix:"在 roll.config.yaml 中配置 browser.default-instance,并移除 agents.env.browser-use-agent 下已废弃的 BROWSER_CDP_* / BROWSER_USER_DATA_DIR 等实例身份 env",details:{type:"browser-config",warnings:t}}}function S(e,s){const t="Browser runtime (browser-use-agent)",n=Object.keys(e.instances);if(0===n.length)return{name:t,status:"ok",message:"跳过(未配置 browser.instances,使用 legacy 单实例运行时)"};if("unverified"===s.status)return{name:t,status:"warn",message:`未校验: ${s.message}`,fix:"启动 browser-use-agent 后重新运行 `roll doctor`"};const o=s.payload.instances??[];if(0===o.length)return{name:t,status:"warn",message:"browser_status.instances 未返回,无法确认浏览器实例运行态",fix:"升级并重启 browser-use-agent"};const a=new Set(o.map(e=>e.id)),r=n.filter(e=>!a.has(e)),i=o.filter(A),l=o.filter(e=>"missing"===e.tracking.source),c={type:"browser-runtime",declaredInstanceIds:n,...void 0!==e.defaultInstance?{defaultInstanceId:e.defaultInstance}:{},runtimeInstances:o,missingInstanceIds:r,unhealthyInstanceIds:i.map(e=>e.id),missingTrackingInstanceIds:l.map(e=>e.id)},u=r.length>0||i.length>0||l.length>0?"warn":"ok";return{name:t,status:u,details:c,message:[`declared=${n.join(",")}`,`runtime=${o.map(I).join(";")}`,...r.length>0?[`missing=${r.join(",")}`]:[],...l.length>0?[`trackingMissing=${l.map(e=>e.id).join(",")}`]:[]].join(" "),..."warn"===u?{fix:"检查 browser.instances、BROWSER_INSTANCES_JSON、CDP 端口和 profile 目录;必要时重启 browser-use-agent"}:{}}}function A(e){return!(e.cdp.versionReachable&&e.cdp.listReachable&&e.profile.exists&&e.profile.writable)}function I(e){return`${e.id}:cdp=${e.cdp.versionReachable&&e.cdp.listReachable?"ok":"warn"},profile=${e.profile.exists&&e.profile.writable?"ok":"warn"},tracking=${e.tracking.source}`}function O(e,s,t){const n=e.domainAllowlist.length>0?e.domainAllowlist.join(", "):"none",o=D(s),a=t.length>0?`; warnings=${t.map(e=>e.code).join(", ")}`:"";return`actionPolicy=${e.actionPolicy}; foregroundPolicy=${e.foregroundPolicy}; domainAllowlist=${n}; maxPageContentBytes=${String(e.maxPageContentBytes)}; maxSnapshotNodes=${String(e.maxSnapshotNodes)}; toolPolicy=${o}`+a}function D(e){if(void 0===e)return"unavailable";const s=Object.entries(e.tools),t=s.length>0?s.map(([e,s])=>`${e}:${s.policy}`).join(","):"none";return`approvalTtlMs=${String(e.approvalTtlMs)},tools=${t}`}export function isNodeVersionSupported(e){const s=/^(\d+)\.(\d+)\.(\d+)/.exec(e);if(!s)return!1;const t=Number(s[1]),n=Number(s[2]),o=Number(s[3]);return t>j.major||!(t<j.major)&&(n>j.minor||!(n<j.minor)&&o>=j.patch)}export default e({meta:{description:"诊断 Roll 配置、Agent 注册表和运行时状态"},args:{json:{type:"boolean",description:"JSON 格式输出",default:!1},"fix-plan":{type:"boolean",description:"输出可执行修复建议(不自动修改)",default:!1},fix:{type:"boolean",description:"执行安全自动修复:配置自动迁移、创建 dataDir、清理孤儿 runtime 元数据",default:!1}},async run({args:e}){const t=[],n=[],o=!0===e.fix;let l;const g=process.versions.node;t.push(isNodeVersionSupported(g)?{name:"Node.js 版本",status:"ok",message:`v${g}`}:{name:"Node.js 版本",status:"fail",message:`v${g} (需要 ≥22.6.0)`,fix:"升级 Node.js 到 22.6.0 或更高版本"});let f,p=c();if("needs-migration"===p.status&&o){const e=R(p);n.push(e),"applied"===e.status&&(p=c())}switch(p.status){case"not-found":t.push({name:"配置文件",status:"warn",message:"未找到,使用默认配置",fix:"运行 `roll config init` 生成显式配置文件"}),l=m().config;break;case"valid":t.push({name:"配置文件",status:"ok",message:p.configPath}),l=p.config;break;case"needs-migration":t.push({name:"配置文件",status:"warn",message:`${p.configPath} (需要迁移,运行 roll config migrate)`,fix:"运行 `roll config migrate`,确认备份文件后提交配置变更"});break;case"invalid":t.push({name:"配置文件",status:"fail",message:p.error.message,fix:"修正配置文件语法或运行 `roll config migrate` 处理 breaking schema change"})}try{f=u().agentsConfig}catch(e){t.push({name:"Agent 配置",status:"fail",message:e instanceof Error?e.message:String(e),fix:"检查 `roll.config.yaml` 的 `agents` 段;如配置版本过旧,运行 `roll config migrate`"})}if(l){const e=Object.keys(l.llm.providers);if(e.length>0){const s=e.filter(e=>{const s=l.llm.providers[e]?.apiKey??"";return s.startsWith("${")||0===s.length});s.length>0?t.push({name:"LLM Providers",status:"warn",message:`${e.join(", ")} (${s.join(", ")} API key 未设置)`,fix:`配置 ${s.map(e=>`llm.providers.${e}.apiKey`).join(", ")} 或对应环境变量`}):t.push({name:"LLM Providers",status:"ok",message:e.join(", ")})}else t.push({name:"LLM Providers",status:"warn",message:"未配置任何 provider",fix:"运行 `roll config init` 或手动配置 `llm.providers`"})}else"needs-migration"===p.status&&t.push({name:"LLM Providers",status:"warn",message:"配置需要迁移,跳过完整 LLM 配置校验",fix:"先运行 `roll config migrate`,再重新执行 `roll doctor`"});if(f){const e=f.dataDir;let c=s(e);if(!c&&o){const t=N(e);n.push(t),c=s(e)}t.push(c?{name:"Agent 数据目录",status:"ok",message:e}:{name:"Agent 数据目录",status:"warn",message:`${e} (不存在,首次 add 时创建)`,fix:"运行 `roll doctor --fix` 创建目录,或手动创建该目录"});const u=new k(f.dataDir).list();if(t.push({name:"已注册 Agent",status:u.length>0?"ok":"warn",message:u.length>0?`${String(u.length)} 个 (${u.map(e=>e.skill.name).join(", ")})`:"无",...0===u.length?{fix:"运行 `roll agent add <path|git-url>` 注册 Agent"}:{}}),l&&Object.keys(l.browser.instances).length>0&&!u.some(e=>"browser-use-agent"===e.skill.name)&&t.push({name:"Browser runtime (browser-use-agent)",status:"fail",message:"已配置 browser.instances,但 browser-use-agent 未注册",fix:"运行 `roll agent add agents/browser-use` 或安装并注册 browser-use-agent",details:{type:"browser-runtime",declaredInstanceIds:Object.keys(l.browser.instances),missingAgent:"browser-use-agent"}}),l&&Object.keys(l.browser.instances).length>0){const e=P(l.browser,a(l,"browser-use-agent"));e&&t.push(e)}for(const e of u){let s;const c=async()=>l?(s??=await w(e,{config:l}),s):{status:"unverified",reason:"connection-failed",message:"无法校验运行态: 配置未加载"};if("core-managed"===e.runtime.ownership){let s=x(e,f.dataDir);if(o&&s.issues.some(e=>"orphan-sidecar"===e.code)){const t=C(f.dataDir,e.skill.name);n.push(t),"applied"===t.status&&(s=x(e,f.dataDir))}(void 0!==s.pid||s.issues.length>0)&&t.push({name:`Agent runtime (${e.skill.name})`,status:s.issues.length>0?"warn":"ok",message:s.issues.length>0?s.issues.map(e=>e.message).join(";"):`PID ${String(s.pid)},runtime sidecar 与当前配置一致`,...s.issues.length>0?{fix:_(s.issues.map(e=>e.fix)).join(";")}:{}})}const u=i(e.skill.name,e.skill.env,f.env);if(!u){"browser-use-agent"===e.skill.name&&(s=await c(),l&&t.push(S(l.browser,s)),t.push(v(s)));continue}s=await c();const m=void 0!==l?a(l,e.skill.name):r(f,e.skill.name),g=d(u,m,s),p=h(g);t.push({name:`Agent 环境变量 (${e.skill.name})`,status:p.status,message:p.message,..."ok"===p.status?{}:{fix:`在 \`roll.config.yaml\` 的 \`agents.env.${e.skill.name}\` 或 shell 环境中配置缺失变量`}}),"browser-use-agent"===e.skill.name&&(l&&t.push(S(l.browser,s)),t.push(v(s)))}}const b=t.some(e=>"fail"===e.status),$=n.some(e=>"failed"===e.status),y=t.some(e=>"warn"===e.status),j=!0===e["fix-plan"]||o;if(e.json){const e=o?formatDoctorJsonOutput(t,{fixPlan:j,fixes:n}):formatDoctorChecksForJsonOutput(t,{fixPlan:j});return console.log(JSON.stringify(e,null,2)),void((b||$)&&(process.exitCode=1))}console.log("Roll Agent 系统诊断\n");for(const e of t)for(const s of formatDoctorCheckLines(e,{fixPlan:j}))console.log(s);if(o)if(console.log("\nFix 执行结果\n"),0===n.length)console.log(" ✓ 无可自动修复项");else for(const e of n)for(const s of formatDoctorFixLines(e))console.log(s);console.log(b||$?"\n存在问题,请修复后重试。":y?"\n存在警告,可按 fix plan 处理。":"\n系统状态正常。"),(b||$)&&(process.exitCode=1)}});export function formatDoctorCheckLines(e,s){const t=[` ${$[e.status]} ${e.name}: ${e.message}`];return s.fixPlan&&e.fix&&t.push(` fix: ${e.fix}`),t}export function formatDoctorChecksForJsonOutput(e,s){return s.fixPlan?e:e.map(({fix:e,...s})=>s)}export function formatDoctorJsonOutput(e,s){return{checks:formatDoctorChecksForJsonOutput(e,{fixPlan:s.fixPlan}),fixes:s.fixes}}export function formatDoctorFixLines(e){return[` ${y[e.status]} ${e.name}: ${e.message}`]}function R(e){if(!e.report.canAutoMigrate)return{name:"配置迁移",status:"skipped",message:"检测到需要人工处理的配置迁移项,未自动修改"};try{const s=g(e.raw,e.configPath),t=p(s);if(!t.ok)return{name:"配置迁移",status:"skipped",message:`无法自动迁移:${L(t.issues)}`};if(!t.changed)return{name:"配置迁移",status:"skipped",message:"配置文件已是最新格式"};const a=new n({configPath:e.configPath}).saveStructured(f(t.document),o(e.raw));return{name:"配置迁移",status:"applied",message:void 0===a.backupPath?`已迁移 ${e.configPath}`:`已迁移 ${e.configPath},备份 ${a.backupPath}`}}catch(e){return{name:"配置迁移",status:"failed",message:e instanceof Error?e.message:String(e)}}}function N(e){try{return t(e,{recursive:!0}),{name:"Agent 数据目录",status:"applied",message:`已创建 ${e}`}}catch(e){return{name:"Agent 数据目录",status:"failed",message:e instanceof Error?e.message:String(e)}}}function C(e,s){try{return b(e,s)?{name:`Agent runtime (${s})`,status:"applied",message:"已清理孤儿 runtime 元数据"}:{name:`Agent runtime (${s})`,status:"skipped",message:"检测到活动 PID,跳过 runtime 元数据清理"}}catch(e){return{name:`Agent runtime (${s})`,status:"failed",message:e instanceof Error?e.message:String(e)}}}function L(e){return e.map(e=>e.message).join(";")}function _(e){return[...new Set(e)]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
interface RunRollUiOptions {
|
|
2
|
+
readonly configPath?: string;
|
|
3
|
+
readonly open: boolean;
|
|
4
|
+
}
|
|
5
|
+
interface RunRollUiDependencies {
|
|
6
|
+
readonly assetsDirectory?: string;
|
|
7
|
+
readonly openExternalUrl?: (url: string) => Promise<void>;
|
|
8
|
+
readonly waitForShutdown?: () => Promise<NodeJS.Signals>;
|
|
9
|
+
}
|
|
10
|
+
interface ExternalOpenCommand {
|
|
11
|
+
readonly command: string;
|
|
12
|
+
readonly args: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
declare const _default: import("citty").CommandDef<{
|
|
15
|
+
config: {
|
|
16
|
+
type: "string";
|
|
17
|
+
description: string;
|
|
18
|
+
required: false;
|
|
19
|
+
};
|
|
20
|
+
open: {
|
|
21
|
+
type: "boolean";
|
|
22
|
+
description: string;
|
|
23
|
+
default: true;
|
|
24
|
+
};
|
|
25
|
+
}>;
|
|
26
|
+
export default _default;
|
|
27
|
+
export declare function runRollUi(options: RunRollUiOptions, dependencies?: RunRollUiDependencies): Promise<void>;
|
|
28
|
+
export declare function resolveRollUiConfigPath(explicitPath?: string): string;
|
|
29
|
+
export declare function resolveRollUiAssetsDirectory(moduleUrl?: string): string;
|
|
30
|
+
export declare function getExternalOpenCommand(url: string, platform?: NodeJS.Platform): ExternalOpenCommand;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawn as o}from"node:child_process";import{existsSync as r}from"node:fs";import{homedir as n}from"node:os";import{dirname as e,resolve as t}from"node:path";import{fileURLToPath as i}from"node:url";import{defineCommand as s}from"citty";import{resolveConfigPath as c}from"../../config/loader.js";import{createFileSystemStaticAssetProvider as a,createRollUiRuntimeController as l,startRollUiServer as f}from"../../ui/index.js";import{log as m}from"../utils/output.js";export default s({meta:{description:"启动本地 Web 配置台"},args:{config:{type:"string",description:"配置文件路径(默认按 roll.config.yaml 发现链查找)",required:!1},open:{type:"boolean",description:"自动在默认浏览器打开(可用 --no-open 跳过)",default:!0}},async run({args:o}){try{await runRollUi({...void 0!==o.config?{configPath:o.config}:{},open:o.open})}catch(o){m.error(o instanceof Error?o.message:String(o)),process.exitCode=1}}});export async function runRollUi(o,r={}){const n=resolveRollUiConfigPath(o.configPath),e=r.assetsDirectory??resolveRollUiAssetsDirectory();d(e);const t=await f({controller:l({configPath:n}),staticAssets:a(e),onError:o=>{m.error(`配置台请求失败:${o instanceof Error?o.message:String(o)}`)}});try{m.info(`配置文件:${n}`),await p(t,o.open,r.openExternalUrl??u),m.info("配置台仅监听 127.0.0.1;按 Ctrl+C 停止。"),await(r.waitForShutdown??g)()}finally{await t.close(),m.success("Roll 配置台已停止。")}}export function resolveRollUiConfigPath(o){const r=c({...void 0!==o?{configPath:o}:{}});return t(r??t(n(),"roll.config.yaml"))}export function resolveRollUiAssetsDirectory(o=import.meta.url){const r=e(i(o));return o.endsWith(".ts")?t(r,"../../../dist/ui-assets"):t(r,"../../ui-assets")}export function getExternalOpenCommand(o,r=process.platform){return"darwin"===r?{command:"open",args:[o]}:"win32"===r?{command:"cmd.exe",args:["/d","/s","/c","start","",o]}:{command:"xdg-open",args:[o]}}async function p(o,r,n){if(r)try{return await n(o.url),void m.success(`配置台已在默认浏览器打开:${o.origin}`)}catch(o){m.warn(`无法自动打开浏览器:${o instanceof Error?o.message:String(o)}`)}console.log(o.url),m.info("请打开上方一次性链接;认证完成后 URL 中的 token 会立即移除并失效。")}function d(o){if(!r(t(o,"index.html")))throw new Error(`Roll UI 静态资源不存在:${o}。请先运行 \`pnpm --filter @roll-agent/core build\`。`)}function u(r){const n=getExternalOpenCommand(r);return new Promise((r,e)=>{const t=o(n.command,[...n.args],{stdio:"ignore",windowsHide:!0});t.once("error",e),t.once("close",o=>{0===o?r():e(new Error(`${n.command} 退出码:${String(o)}`))})})}function g(){return new Promise(o=>{const r=n=>{process.off("SIGINT",r),process.off("SIGTERM",r),o(n)};process.once("SIGINT",r),process.once("SIGTERM",r)})}
|
package/dist/cli/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var t=this&&this.__rewriteRelativeImportExtension||function(t,e){return"string"==typeof t&&/^\.\.?\//.test(t)?t.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i,function(t,r,o,s,n){return r?e?".jsx":".js":!o||s&&n?o+s+"."+n.toLowerCase()+"js":t}):t};import{defineCommand as e,runMain as r}from"citty";import o from"chalk";import{checkForUpdate as s,getCurrentVersion as n}from"./utils/update-checker.js";import{resolveLogLevelFromArgv as a,setLogLevel as i}from"./utils/output.js";const c=n(),l=import.meta.url.endsWith(".ts")?"ts":"js";function u(e){const r=new URL(`./commands/${e}.${l}`,import.meta.url).href;return import(t(r)).then(t=>t.default)}const p=e({meta:{name:"roll",version:c,description:"花卷 Agent — 轻量级 Agent 编排系统"},args:{verbose:{type:"boolean",alias:"v",description:"输出调试日志",default:!1}},subCommands:{agent:()=>u("agent"),run:()=>u("run"),ask:()=>u("ask"),chat:()=>u("chat"),config:()=>u("config"),setup:()=>u("setup"),skills:()=>u("skills"),browser:()=>u("browser"),doctor:()=>u("doctor"),update:()=>u("update")}}),m=s({allowNetwork:!1}).catch(()=>{});i(a(process.argv.slice(2))),r(p).then(()=>{m.then(t=>{t?.hasUpdate&&console.error(`\n${o.yellow("⬆")} roll ${o.green(`v${t.latest}`)} available (current: v${t.current}). Run ${o.cyan("roll update")} to upgrade.`)}).catch(()=>{})});
|
|
1
|
+
var t=this&&this.__rewriteRelativeImportExtension||function(t,e){return"string"==typeof t&&/^\.\.?\//.test(t)?t.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i,function(t,r,o,s,n){return r?e?".jsx":".js":!o||s&&n?o+s+"."+n.toLowerCase()+"js":t}):t};import{defineCommand as e,runMain as r}from"citty";import o from"chalk";import{checkForUpdate as s,getCurrentVersion as n}from"./utils/update-checker.js";import{resolveLogLevelFromArgv as a,setLogLevel as i}from"./utils/output.js";const c=n(),l=import.meta.url.endsWith(".ts")?"ts":"js";function u(e){const r=new URL(`./commands/${e}.${l}`,import.meta.url).href;return import(t(r)).then(t=>t.default)}const p=e({meta:{name:"roll",version:c,description:"花卷 Agent — 轻量级 Agent 编排系统"},args:{verbose:{type:"boolean",alias:"v",description:"输出调试日志",default:!1}},subCommands:{agent:()=>u("agent"),run:()=>u("run"),ask:()=>u("ask"),chat:()=>u("chat"),config:()=>u("config"),setup:()=>u("setup"),skills:()=>u("skills"),browser:()=>u("browser"),ui:()=>u("ui"),doctor:()=>u("doctor"),update:()=>u("update")}}),m=s({allowNetwork:!1}).catch(()=>{});i(a(process.argv.slice(2))),r(p).then(()=>{m.then(t=>{t?.hasUpdate&&console.error(`\n${o.yellow("⬆")} roll ${o.green(`v${t.latest}`)} available (current: v${t.current}). Run ${o.cyan("roll update")} to upgrade.`)}).catch(()=>{})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import e from"chalk";import{isCancel as t,select as s}from"@clack/prompts";import{createSpinner as o,log as n}from"./output.js";import{GLYPHS as a}from"./glyphs.js";import{computeUsageParts as i,formatUsageLine as r}from"./token-format.js";import{formatToolInput as p,formatApprovalDetails as l}from"./tool-format.js";import{formatDebugEvent as c}from"./debug-format.js";export const clackConfirm=async e=>{const o=await s({message:e,options:[{value:"yes",label:"Yes"},{value:"no",label:"No"}],initialValue:"no"});return!t(o)&&"yes"===o};export class ChatRenderer{confirm;contextWindow;spinners=new Map;toolLabels=new Map;compactionSpinner;messageSpinner;streaming=!1;constructor(e,t){this.confirm=e,this.contextWindow=t}async handle(t,s){switch(t.type){case"debug":n.debug(c(t));break;case"message-start":this.startMessageSpinner();break;case"text-delta":this.stopMessageSpinner(),process.stdout.write(t.delta),this.streaming=!0;break;case"tool-call":{this.stopMessageSpinner(),this.flushLine();const s=`${t.agentName}.${t.toolName}`,n=o(`${e.cyan(s)} ${e.gray(p(t.input))}`);n.start(),this.spinners.set(t.toolCallId,n),this.toolLabels.set(t.toolCallId,s);break}case"tool-output-delta":{const s=this.spinners.get(t.toolCallId),o=this.toolLabels.get(t.toolCallId);if(s&&o){const n=t.delta.split("\n").map(e=>e.trim()).filter(e=>e.length>0).at(-1);if(n){const t=n.length>80?`${n.slice(0,79)}…`:n;s.text=`${e.cyan(o)} ${e.gray(t)}`}}break}case"tool-result":{const e=this.spinners.get(t.toolCallId);e&&(t.isError?e.fail():e.succeed(),this.spinners.delete(t.toolCallId)),this.toolLabels.delete(t.toolCallId);break}case"confirmation-required":{this.stopMessageSpinner(),this.flushLine();const e=t.reason?`(${t.reason})`:"",o=l(t.input),n=`执行 ${t.agentName}.${t.toolName}${e}?`;await this.confirm(o?`${n}\n${o}`:n)?s.approve(t.approvalId):s.reject(t.approvalId,"用户取消");break}case"compaction-start":this.stopMessageSpinner(),this.flushLine(),this.compactionSpinner=o(e.gray("压缩上下文中…")),this.compactionSpinner.start();break;case"context-compacted":{this.stopCompactionSpinner(),this.flushLine();const s="auto"===t.reason?"自动压缩":"手动压缩",o=t.truncatedTools?`,精简 ${String(t.truncatedTools)} 个工具结果`:"",n=0!==t.removed||t.truncatedTools?`${a.compact} ${s}(${t.strategy}):移除 ${String(t.removed)} 条 → 保留 ${String(t.kept)} 条${o}`:`${a.compact} ${s}:无需压缩`;process.stderr.write(`${e.gray(n)}\n`);break}case"turn-cancelled":this.stopMessageSpinner(),this.stopCompactionSpinner(),this.flushLine();for(const t of this.spinners.values())t.warn(`${t.text} ${e.yellow("已中断")}`);this.spinners.clear(),this.toolLabels.clear(),"user"===t.reason?process.stderr.write(`${e.gray(`■ ${t.message}`)}\n`):"timeout"===t.reason?n.warn(t.message):n.error(t.message);break;case"error":this.stopMessageSpinner(),this.stopCompactionSpinner(),this.flushLine(),n.error(t.message);break;case"message-finish":{this.stopMessageSpinner(),this.flushLine(),0===t.text.length&&(t.totalUsage?.outputTokens??0)>0&&n.warn("模型本轮只返回了 thinking/reasoning,没有生成可见回复"),t.stoppedAtStepLimit&&n.warn("已达单轮最大工具步数,任务可能未完成 — 继续追问即可接着做,或调高 runtime.max-steps");const s=r(i(t.totalUsage,t.sessionUsage,this.contextWindow,t.contextInputTokens));s&&process.stderr.write(`${e.gray(s)}\n`);break}}}stopCompactionSpinner(){this.compactionSpinner&&(this.compactionSpinner.stop(),this.compactionSpinner=void 0)}startMessageSpinner(){this.stopMessageSpinner(),this.messageSpinner=o(e.gray("思考中…")),this.messageSpinner.start()}stopMessageSpinner(){this.messageSpinner&&(this.messageSpinner.stop(),this.messageSpinner=void 0)}flushLine(){this.streaming&&(process.stdout.write("\n"),this.streaming=!1)}}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CurrentCliShim {
|
|
2
|
+
readonly path: string;
|
|
3
|
+
dispose(): void;
|
|
4
|
+
}
|
|
5
|
+
export interface InstallCurrentCliShimOptions {
|
|
6
|
+
readonly env: NodeJS.ProcessEnv;
|
|
7
|
+
readonly executable?: string;
|
|
8
|
+
readonly execArgv?: readonly string[];
|
|
9
|
+
readonly entryPath?: string;
|
|
10
|
+
readonly launchCwd?: string;
|
|
11
|
+
readonly platform?: NodeJS.Platform;
|
|
12
|
+
readonly tempRoot?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function installCurrentCliShim(options: InstallCurrentCliShimOptions): CurrentCliShim;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{chmodSync as e,mkdtempSync as t,rmSync as n,writeFileSync as r}from"node:fs";import{tmpdir as o}from"node:os";import{join as i,resolve as s}from"node:path";const c="ROLL_CURRENT_CLI",a=/^--inspect/u,l=["--require","-r","--import","--loader","--experimental-loader"],p=new WeakMap;function u(e){return`'${e.replaceAll("'","'\\''")}'`}function h(e){return`"${e.replaceAll("%","%%").replaceAll('"','\\"')}"`}function f(e,t){return/^\.{1,2}[\\/]/u.test(e)?s(t,e):e}function d(e,t){const n=[];for(let r=0;r<e.length;r+=1){const o=e[r];if(void 0===o||a.test(o))continue;if(void 0!==l.find(e=>e===o)){n.push(o);const i=e[r+1];void 0!==i&&(n.push(f(i,t)),r+=1);continue}const i=l.find(e=>o.startsWith(`${e}=`));if(void 0!==i){const e=o.slice(i.length+1);n.push(`${i}=${f(e,t)}`);continue}n.push(o)}return n}function v(e,t){return"win32"!==t?"PATH":Object.keys(e).find(e=>"path"===e.toLowerCase())??"Path"}function m(e){return`#!/bin/sh\nexec ${e.map(u).join(" ")} "$@"\n`}function C(e){return["@echo off",`${e.map(h).join(" ")} %*`,"exit /b %ERRORLEVEL%",""].join("\r\n")}export function installCurrentCliShim(s){const a=s.executable??process.execPath,l=s.entryPath??process.argv[1];if(!l)throw new Error("无法确定当前 Roll CLI 入口");const u=s.platform??process.platform,h=s.env,f="win32"===u?";":":";let w,$;try{w=t(i(s.tempRoot??o(),"roll-current-cli-")),e(w,448),$=i(w,"win32"===u?"roll.cmd":"roll");const n=[a,...d(s.execArgv??process.execArgv,s.launchCwd??process.cwd()),l],c="win32"===u?C(n):m(n);r($,c,{encoding:"utf8",mode:448})}catch(e){throw void 0!==w&&n(w,{recursive:!0,force:!0}),e}const b=v(h,u),x=p.get(h),y=x??{pathKey:b,basePath:h[b],baseCurrentCli:h[c],entries:[]};void 0===x&&p.set(h,y);const g={directory:w,path:$,active:!0};y.entries.push(g);const L=h[b];h[b]=L?`${w}${f}${L}`:w,h[c]=$;const R=()=>{n(w,{recursive:!0,force:!0})};process.once("exit",R);let P=!1;return{path:$,dispose(){if(P)return;P=!0,process.removeListener("exit",R),g.active=!1;const e=y.entries.filter(e=>e.active);if(0===e.length)void 0===y.basePath?delete h[y.pathKey]:h[y.pathKey]=y.basePath,void 0===y.baseCurrentCli?delete h[c]:h[c]=y.baseCurrentCli,p.delete(h);else{const t=h[y.pathKey];void 0!==t&&(h[y.pathKey]=t.split(f).filter(e=>e!==w).join(f)),y.entries.some(e=>e.path===h[c])&&(h[c]=e.at(-1)?.path)}R()}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function formatDebugEvent(s){const t=[`chat.${s.stage}`,s.message];return void 0!==s.elapsedMs&&t.push(`${String(s.elapsedMs)}ms`),void 0!==s.data&&t.push(JSON.stringify(s.data)),t.join(" · ")}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { YamlConfigDocumentStore, type ConfigPatch, type ConfigPath, type ConfigRevision } from "./document-store.ts";
|
|
2
|
+
import { type LoadConfigOptions } from "./loader.ts";
|
|
3
|
+
export declare const CONFIG_UI_SECRET_SENTINEL: "__ROLL_UI_KEEP_EXISTING_SECRET__";
|
|
4
|
+
export declare const CONFIG_ACTIVATION_KINDS: readonly ["next-command", "next-chat", "restart-agent", "manual"];
|
|
5
|
+
export type ConfigActivationKind = (typeof CONFIG_ACTIVATION_KINDS)[number];
|
|
6
|
+
export interface ConfigActivationEffect {
|
|
7
|
+
readonly kind: ConfigActivationKind;
|
|
8
|
+
readonly paths: readonly ConfigPath[];
|
|
9
|
+
readonly title: string;
|
|
10
|
+
readonly description: string;
|
|
11
|
+
readonly agentName?: string;
|
|
12
|
+
readonly requiresConfirmation: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ConfigValidationIssue {
|
|
15
|
+
readonly path: string;
|
|
16
|
+
readonly message: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ConfigDiffLine {
|
|
19
|
+
readonly kind: "context" | "add" | "remove";
|
|
20
|
+
readonly text: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ConfigApplicationSnapshot {
|
|
23
|
+
readonly configPath: string;
|
|
24
|
+
readonly existed: boolean;
|
|
25
|
+
readonly revision: ConfigRevision;
|
|
26
|
+
readonly persisted: Readonly<Record<string, unknown>>;
|
|
27
|
+
readonly yaml: string;
|
|
28
|
+
readonly configuredSecretPaths: readonly ConfigPath[];
|
|
29
|
+
}
|
|
30
|
+
export interface ConfigApplicationPreview {
|
|
31
|
+
readonly snapshot: ConfigApplicationSnapshot;
|
|
32
|
+
readonly changed: boolean;
|
|
33
|
+
readonly changedPaths: readonly ConfigPath[];
|
|
34
|
+
readonly effects: readonly ConfigActivationEffect[];
|
|
35
|
+
readonly diff: readonly ConfigDiffLine[];
|
|
36
|
+
}
|
|
37
|
+
export interface ConfigApplicationSaveResult extends ConfigApplicationPreview {
|
|
38
|
+
readonly backupPath?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ConfigApplicationRecoverySaveResult {
|
|
41
|
+
readonly snapshot: ConfigApplicationSnapshot;
|
|
42
|
+
readonly changed: boolean;
|
|
43
|
+
readonly backupPath?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ConfigApplicationServiceOptions extends LoadConfigOptions {
|
|
46
|
+
/** Extra Agent env names declared as secret in their env.yaml contract. */
|
|
47
|
+
readonly secretEnvNames?: readonly string[];
|
|
48
|
+
/** Exact Agent env visibility derived from registered env.yaml declarations. */
|
|
49
|
+
readonly agentEnvFields?: readonly ConfigAgentEnvFieldPolicy[];
|
|
50
|
+
/** Fail closed for undeclared Agent env fields, including newly added candidate values. */
|
|
51
|
+
readonly redactUnknownAgentEnv?: boolean;
|
|
52
|
+
}
|
|
53
|
+
export interface ConfigAgentEnvFieldPolicy {
|
|
54
|
+
readonly agentName: string;
|
|
55
|
+
readonly name: string;
|
|
56
|
+
readonly secret: boolean;
|
|
57
|
+
}
|
|
58
|
+
export declare class ConfigApplicationValidationError extends Error {
|
|
59
|
+
readonly code: "config_validation_failed";
|
|
60
|
+
readonly issues: readonly ConfigValidationIssue[];
|
|
61
|
+
constructor(error: unknown);
|
|
62
|
+
}
|
|
63
|
+
export declare class ConfigApplicationService {
|
|
64
|
+
readonly configPath: string;
|
|
65
|
+
readonly store: YamlConfigDocumentStore;
|
|
66
|
+
private readonly isSecretPath;
|
|
67
|
+
constructor(options?: ConfigApplicationServiceOptions);
|
|
68
|
+
read(): ConfigApplicationSnapshot;
|
|
69
|
+
/** Load syntactically valid persisted data so a focused CLI setup can repair schema errors. */
|
|
70
|
+
readForRepair(): ConfigApplicationSnapshot;
|
|
71
|
+
previewPatches(patches: readonly ConfigPatch[], expectedRevision?: ConfigRevision): ConfigApplicationPreview;
|
|
72
|
+
savePatches(patches: readonly ConfigPatch[], expectedRevision?: ConfigRevision): ConfigApplicationSaveResult;
|
|
73
|
+
previewStructured(persisted: unknown, expectedRevision?: ConfigRevision): ConfigApplicationPreview;
|
|
74
|
+
saveStructured(persisted: unknown, expectedRevision?: ConfigRevision): ConfigApplicationSaveResult;
|
|
75
|
+
previewYaml(yaml: string, expectedRevision?: ConfigRevision): ConfigApplicationPreview;
|
|
76
|
+
saveYaml(yaml: string, expectedRevision?: ConfigRevision): ConfigApplicationSaveResult;
|
|
77
|
+
/**
|
|
78
|
+
* Recover an invalid existing file during an explicitly confirmed `roll config init` overwrite.
|
|
79
|
+
* The generated candidate is fully validated, but the old document is never parsed or returned
|
|
80
|
+
* to a browser. UI and ordinary CLI edits must continue to use preview/save methods above.
|
|
81
|
+
*/
|
|
82
|
+
replaceYamlForInit(yaml: string, expectedRevision: ConfigRevision): ConfigApplicationRecoverySaveResult;
|
|
83
|
+
private preparePatches;
|
|
84
|
+
private prepareStructured;
|
|
85
|
+
private prepareYaml;
|
|
86
|
+
private prepareDocumentPreview;
|
|
87
|
+
private commitPrepared;
|
|
88
|
+
}
|
|
89
|
+
export declare function planConfigActivation(changedPaths: readonly ConfigPath[]): readonly ConfigActivationEffect[];
|
|
90
|
+
export declare function createConfigPatches(before: unknown, after: unknown, path?: ConfigPath): readonly ConfigPatch[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{homedir as t}from"node:os";import{resolve as e}from"node:path";import{stringify as r}from"yaml";import{DEFAULT_CONFIG as n}from"./defaults.js";import{ConfigRevisionConflictError as i,YamlConfigDocumentStore as o,applyPatchesToYamlText as s}from"./document-store.js";import{CONFIG_KEY_CODEC as a,decodeFromYaml as c,encodePathToYaml as p,encodeToYaml as h,kebabToCamel as u}from"./key-codec.js";import{parseConfigDocument as f,resolveConfigPath as l,validateConfigText as d}from"./loader.js";import{detectKnownConfigMigrations as g,formatConfigMigrationError as m}from"./migration.js";import{isRollConfigSecretPath as w,isSecretConfigValue as v}from"./secret-policy.js";export const CONFIG_UI_SECRET_SENTINEL="__ROLL_UI_KEEP_EXISTING_SECRET__";export const CONFIG_ACTIVATION_KINDS=["next-command","next-chat","restart-agent","manual"];export class ConfigApplicationValidationError extends Error{code="config_validation_failed";issues;constructor(t){const e=t instanceof Error?t:new Error(String(t));super(e.message,{cause:e}),this.name="ConfigApplicationValidationError",this.issues=D(e.message)}}export class ConfigApplicationService{configPath;store;isSecretPath;constructor(r={}){this.configPath=l(r)??e(t(),"roll.config.yaml"),this.store=new o(this.configPath,Y());const n=new Set(r.secretEnvNames??[]),i=new Map((r.agentEnvFields??[]).map(t=>[F(t.agentName,t.name),t.secret]));this.isSecretPath=t=>V(t,n,i,r.redactUnknownAgentEnv??!1)}read(){const t=this.store.read();return y(t.raw,t.configPath),P(t,this.isSecretPath)}readForRepair(){const t=this.store.read(),e=f(t.raw,t.configPath),r=g(e);if(r.needsMigration)throw new ConfigApplicationValidationError(new Error(m(t.configPath,r)));return P(t,this.isSecretPath)}previewPatches(t,e){return this.preparePatches(t,e).publicPreview}savePatches(t,e){return this.commitPrepared(this.preparePatches(t,e))}previewStructured(t,e){return this.prepareStructured(t,e).publicPreview}saveStructured(t,e){return this.commitPrepared(this.prepareStructured(t,e))}previewYaml(t,e){return this.prepareYaml(t,e).publicPreview}saveYaml(t,e){return this.commitPrepared(this.prepareYaml(t,e))}replaceYamlForInit(t,e){y(t,this.configPath);const r=this.store.replaceRawForRecovery(t,e);return{snapshot:P(r,this.isSecretPath),changed:r.changed,...void 0!==r.backupPath?{backupPath:r.backupPath}:{}}}preparePatches(t,e){const r=t.map(S),n=this.store.previewPatches(r,e);return this.prepareDocumentPreview(n)}prepareStructured(t,e){if(!M(t))throw new ConfigApplicationValidationError(new Error("配置内容必须是一个 JSON/YAML object。"));const r=this.store.read();if(void 0!==e&&e!==r.revision)throw new i(e,r.revision);const n=O(t,A(r.persisted),[],this.isSecretPath),o=h(n);if(!M(o))throw new ConfigApplicationValidationError(new Error("编码后的配置必须是 object。"));const s=this.store.previewObject(o,r.revision);return this.prepareDocumentPreview(s)}prepareYaml(t,e){const r=this.store.read();if(void 0!==e&&e!==r.revision)throw new i(e,r.revision);let n,o=t;try{n=f(o,this.configPath)}catch(t){throw new ConfigApplicationValidationError(t)}const c=[];let p;j(n,A(r.persisted),[],[],c,this.isSecretPath,a),c.length>0&&(o=s(o,this.configPath,c));try{p=this.store.previewRaw(o,r.revision)}catch(t){throw new ConfigApplicationValidationError(t)}return this.prepareDocumentPreview(p)}prepareDocumentPreview(t){y(t.raw,this.configPath);const e=_(A(f(t.previousRaw,this.configPath)),A(t.persisted)),r=P(t,this.isSecretPath),n=f(t.previousRaw,this.configPath),i=P({configPath:t.configPath,existed:t.existed,raw:t.previousRaw,revision:t.previousRevision,persisted:n},this.isSecretPath);return{documentPreview:t,publicPreview:{snapshot:r,changed:t.changed,changedPaths:e,effects:planConfigActivation(e),diff:U(i.yaml,r.yaml)}}}commitPrepared(t){const e=this.store.commit(t.documentPreview,{backup:!0}),r=P(e,this.isSecretPath);return{...t.publicPreview,snapshot:r,...void 0!==e.backupPath?{backupPath:e.backupPath}:{}}}}export function planConfigActivation(t){const e=new Map;for(const r of t){const t=E(r),n=`${t.kind}:${t.agentName??""}:${t.title}`,i=e.get(n);void 0!==i?i.paths.push(r):e.set(n,{...t,paths:[r]})}return[...e.values()].map(t=>({...t,paths:t.paths}))}export function createConfigPatches(t,e,r=[]){if(M(t)&&M(e)){const n=[],i=new Set([...Object.keys(t),...Object.keys(e)]);for(const o of i)o in e?o in t?n.push(...createConfigPatches(t[o],e[o],[...r,o])):n.push({op:"set",path:[...r,o],value:e[o]}):n.push({op:"delete",path:[...r,o]});return n}if(q(t,e))return[];if(0===r.length)throw new ConfigApplicationValidationError(new Error("配置根节点必须保持为 object。"));return[{op:"set",path:r,value:e}]}function E(t){const[e,r,n]=t;return"agents"===e&&"dataDir"===r?{kind:"manual",title:"Agent 数据目录需要人工迁移",description:"保存不会搬迁旧 PID、日志或注册数据;请先停止 Agent,再人工迁移目录。",requiresConfirmation:!0}:"agents"===e&&"env"===r&&"string"==typeof n?{kind:"restart-agent",title:`重启 ${n}`,description:"运行中的 core-managed Agent 需要重启;已停止的 Agent 保持停止。",agentName:n,requiresConfirmation:!0}:"browser"===e?{kind:"restart-agent",title:"重启 browser-use-agent",description:"浏览器实例声明在 Agent 启动时注入;重启 Agent 后 Chrome 仍会在首次工具调用时懒启动。",agentName:"browser-use-agent",requiresConfirmation:!0}:"runtime"===e||"skills"===e?{kind:"next-chat",title:"新会话生效",description:"当前 roll chat 会话保持原配置,新会话会重新加载。",requiresConfirmation:!1}:{kind:"next-command",title:"后续命令生效",description:"后续 Roll 命令会重新加载该配置。",requiresConfirmation:!1}}function P(t,e){const r=A(t.persisted),n=N(t.persisted,[],[],e,a),i=n.map(t=>t.path),o=new Set(i.map(L)),c=I(r,[],o);if(!M(c))throw new ConfigApplicationValidationError(new Error("脱敏后的配置必须是 object。"));const p=n.filter(t=>{const e=G(r,t.path);return!("string"==typeof e&&$(e))}).map(t=>({op:"set",path:t.persistedPath,value:CONFIG_UI_SECRET_SENTINEL})),h=0===p.length?t.raw:s(t.raw,t.configPath,p);return{configPath:t.configPath,existed:t.existed,revision:t.revision,persisted:c,yaml:h,configuredSecretPaths:i}}function y(t,e){try{d(t,e)}catch(t){throw new ConfigApplicationValidationError(t)}}function A(t){const e=c(t);if(!M(e))throw new ConfigApplicationValidationError(new Error("持久化配置必须是 object。"));return e}function S(t){const e=C(t.path);return"set"===t.op?{op:"set",path:e,value:k(t.value,t.path)}:{op:"delete",path:e}}function C(t){return p(t.map(String)).map((e,r)=>"number"==typeof t[r]?t[r]:e)}function k(t,e){if(!M(t)&&!Array.isArray(t))return t;const r=b({},e,t),n=h(r);return M(n)?G(n,C(e)):t}function b(t,e,r){let n=t;for(const[t,i]of e.entries()){const o=String(i);if(t===e.length-1){n[o]=r;break}const s={};n[o]=s,n=s}return t}function _(t,e,r=[]){const n=[],i=new Set([...Object.keys(t),...Object.keys(e)]);for(const o of i){const i=t[o],s=e[o],a=[...r,o];M(i)||M(s)?n.push(..._(M(i)?i:{},M(s)?s:{},a)):q(i,s)||n.push(a)}return n}function N(t,e,r,n,i){const o=[];if(Array.isArray(t)){const s="array"===i?.kind?i.item:i;return t.forEach((t,i)=>{const a=[...e,i],c=[...r,i],p=R(t,s);v(a,p,n)?o.push({path:a,persistedPath:c}):(Array.isArray(t)||M(t))&&o.push(...N(t,a,c,n,s))}),o}if(!M(t))return o;for(const[s,a]of Object.entries(t)){const t=x(s,a,i),c=[...e,t.key],p=[...r,s];v(c,t.value,n)?o.push({path:c,persistedPath:p}):(Array.isArray(a)||M(a))&&o.push(...N(a,c,p,n,t.node))}return o}function I(t,e,r){return r.has(L(e))?"string"==typeof t&&$(t)?t:CONFIG_UI_SECRET_SENTINEL:"string"==typeof t?t:Array.isArray(t)?t.map((t,n)=>I(t,[...e,n],r)):M(t)?Object.fromEntries(Object.entries(t).map(([t,n])=>[t,I(n,[...e,t],r)])):t}function O(t,e,r,n){if(t===CONFIG_UI_SECRET_SENTINEL)return T(e,r,n),e;if(Array.isArray(t)){const i=Array.isArray(e)?e:[];return t.map((t,e)=>O(t,i[e],[...r,e],n))}if(!M(t))return t;const i=M(e)?e:{};return Object.fromEntries(Object.entries(t).map(([t,e])=>[t,O(e,i[t],[...r,t],n)]))}function j(t,e,r,n,i,o,s){if(Array.isArray(t)){const a="array"===s?.kind?s.item:s;return void t.forEach((t,s)=>{const c=[...r,s],p=[...n,s],h=G(e,c);if(t===CONFIG_UI_SECRET_SENTINEL)return T(h,c,o),void i.push({op:"set",path:p,value:h});j(t,e,c,p,i,o,a)})}if(M(t))for(const[a,c]of Object.entries(t)){const t=x(a,c,s),p=[...r,t.key],h=[...n,a],u=G(e,p);c!==CONFIG_UI_SECRET_SENTINEL?j(c,e,p,h,i,o,t.node):(T(u,p,o),i.push({op:"set",path:h,value:u}))}}function x(t,e,r){if("object"===r?.kind){const n=u(t),i=r.fields[n];return{key:n,value:R(e,i),node:i}}return"record"===r?.kind?{key:t,value:R(e,r.value),node:r.value}:{key:t,value:e,node:void 0}}function R(t,e){return void 0===e||"leaf"===e.kind?t:c(t,e)}function T(t,e,r){if(void 0===t||t===CONFIG_UI_SECRET_SENTINEL||!v(e,t,r))throw new ConfigApplicationValidationError(new Error(`无法在原路径恢复敏感配置: ${K(e)}`))}function V(t,e=new Set,r=new Map,n=!1){if(w(t))return!0;if(4===t.length&&"agents"===t[0]&&"env"===t[1]){const i=t[2],o=t[3];if("string"!=typeof i||"string"!=typeof o)return!1;const s=F(i,o);return r.has(s)?!0===r.get(s):!(!e.has(o)&&!n)||(/(?:^|_)(?:TOKEN|API_KEY|SECRET|PASSWORD|PRIVATE_KEY)(?:_|$)/u.test(o)||o.endsWith("_WEBHOOK"))}return!1}function F(t,e){return`${t}\0${e}`}function $(t){return/^\$\{[^{}]+\}$/u.test(t)}function L(t){return JSON.stringify(t)}function G(t,e){let r=t;for(const t of e)if(Array.isArray(r)&&"number"==typeof t)r=r[t];else{if(!M(r))return;r=r[String(t)]}return r}function U(t,e){if(t===e)return t.split("\n").map(t=>({kind:"context",text:t}));const r=t.split("\n"),n=e.split("\n");let i=0;for(;i<r.length&&i<n.length&&r[i]===n[i];)i+=1;let o=0;for(;o<r.length-i&&o<n.length-i&&r[r.length-o-1]===n[n.length-o-1];)o+=1;return[...r.slice(0,i).map(t=>({kind:"context",text:t})),...r.slice(i,r.length-o).map(t=>({kind:"remove",text:t})),...n.slice(i,n.length-o).map(t=>({kind:"add",text:t})),...r.slice(r.length-o).map(t=>({kind:"context",text:t}))]}function Y(){const t={llm:n.llm,ask:n.ask,agents:n.agents};return r(h(t),{lineWidth:0})}function D(t){const e=t.split("\n").map(t=>t.match(/^\s*-\s+([^:]+):\s*(.+)$/u)).filter(t=>null!==t).map(e=>({path:e[1]??"",message:e[2]??t}));return e.length>0?e:[{path:"",message:t}]}function K(t){return t.map(String).join(" / ")}function q(t,e){if(Object.is(t,e))return!0;if(Array.isArray(t)&&Array.isArray(e))return t.length===e.length&&t.every((t,r)=>q(t,e[r]));if(!M(t)||!M(e))return!1;const r=Object.keys(t),n=Object.keys(e);return r.length===n.length&&r.every(r=>r in e&&q(t[r],e[r]))}function M(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}
|