@huicai/cli 2026.1019.0 → 2026.1022.59980

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.
@@ -1 +1 @@
1
- import{getPlatformConfig as e}from"./api.js";import{loadConfig as t,saveConfig as r}from"./config.js";export const PLAN_RESOURCES=Object.freeze({skills:Object.freeze({label:"技能",gate:""}),mcp_servers:Object.freeze({label:"MCP服务",gate:""}),scheduled_tasks:Object.freeze({label:"自动化任务",gate:"agent_tasks"}),agents:Object.freeze({label:"专家",gate:""}),squads:Object.freeze({label:"专家团",gate:""}),projects:Object.freeze({label:"项目",gate:"project"})});export const PLAN_FEATURES=Object.freeze({agent_tasks:"Agent自动化任务",knowledge_base:"知识库",project:"项目"});export function upgradeUrl(e){const t=String(e||"").trim().replace(/\/+$/,"");return t?`${t}/console/membership`:""}const n=["免费版","Plus会员","Pro会员","Max会员","团队版","企业版"],a=3e4;let i={key:"",at:0,plan:null,fetched:!1,failAt:0},l=null,o=null;const u=e=>`${String(e?.baseUrl||"")}\n${String(e?.apiKey||"")}`,s=e=>e&&"object"==typeof e&&!Array.isArray(e)?e:null;export function planFromUser(e){if(!e||"object"!=typeof e)return null;const t=s(e.limits),r=Array.isArray(e.entitlements)?e.entitlements.map(String):null;if(!t&&!r)return null;const n=e.tier_level??e.tierLevel,a=null==n||""===n?NaN:Number(n),i=Number(e.context_limit??e.contextLimit);return{code:String(e.plan_code??e.planCode??"").trim().toLowerCase(),name:String(e.plan_name??e.planName??"").trim(),tier:Number.isInteger(a)?a:null,limits:t,entitlements:r,featureNames:s(e.feature_names??e.featureNames),gates:s(e.limit_gates??e.limitGates),contextLimit:Number.isFinite(i)&&i>=0?Math.floor(i):null}}export function contextLimitOf(e=currentPlan()){const t=Number(e?.contextLimit);return Number.isFinite(t)&&t>0?t:0}export function currentPlan(e=t()){return i.fetched&&i.key===u(e)?i.plan:planFromUser(e?.user)}export async function refreshPlan(r=t(),{force:n=!1}={}){if(!r?.baseUrl||!r?.apiKey)return currentPlan(r);const s=u(r),c=Date.now();if(!n&&i.key===s){if(i.fetched&&c-i.at<a)return i.plan;if(i.failAt&&c-i.failAt<a)return currentPlan(r)}if(l&&l.key===s)return l.promise;const f=(async()=>{let t={};try{t=await e(r.baseUrl,r.apiKey,{timeoutMs:6e3})}catch{t={}}return t&&"object"==typeof t&&t.user&&"object"==typeof t.user?(i={key:s,at:Date.now(),plan:planFromUser(t.user),fetched:!0,failAt:0},o=null,i.plan):(i=i.key===s?{...i,failAt:Date.now()}:{key:s,at:0,plan:null,fetched:!1,failAt:Date.now()},currentPlan(r))})().finally(()=>{l&&l.promise===f&&(l=null)});return l={key:s,promise:f},f}export async function syncPlanUser(n=t()){if(!n?.baseUrl||!n?.apiKey)return currentPlan(n);let a={};try{a=await e(n.baseUrl,n.apiKey,{timeoutMs:6e3})}catch{a={}}const l=a&&"object"==typeof a.user?a.user:null;if(!l)return currentPlan(n);primePlan(n,l);try{JSON.stringify(n.user??null)!==JSON.stringify(l)&&(r({user:l}),n.user=l)}catch{}return i.plan}export function primePlan(e,t){e?.baseUrl&&e?.apiKey&&t&&"object"==typeof t&&(i={key:u(e),at:Date.now(),plan:planFromUser(t),fetched:!0,failAt:0},o=null)}export function noteContextLimit(e,t){const r=Math.floor(Number(t));r>0&&e&&(o={key:u(e),limit:r})}export function contextLimitHint(e){return o&&e&&o.key===u(e)?o.limit:0}export function resetPlanCache(){i={key:"",at:0,plan:null,fetched:!1,failAt:0},l=null,o=null}export async function planVerdict(e,r=t()){await refreshPlan(r);const n=i.fetched&&i.key===u(r)&&Date.now()-i.at<a?i.plan?.entitlements:null;return Array.isArray(n)?n.includes(e)?"granted":"denied":"unknown"}export function featureName(e,t=currentPlan()){const r=t?.featureNames?.[e];return"string"==typeof r&&r.trim()||PLAN_FEATURES[e]||e}export function featureMessage(e,t=currentPlan()){return`当前会员等级未开通「${featureName(e,t)}」,请升级会员后使用`}export function automationSuspendedMessage(e=currentPlan()){return`当前会员等级未开通「${featureName("agent_tasks",e)}」,任务已自动暂停;升级会员后可重新启用`}export function gateOf(e,t=currentPlan()){return t?.gates?String(t.gates[e]||""):PLAN_RESOURCES[e]?.gate||""}export function limitOf(e,t=currentPlan()){const r=t?.limits?.[e];if(null==r||""===r)return null;const n=Number(r);return Number.isFinite(n)?n<0?-1:Math.floor(n):null}export function remainingOf(e,t,r=currentPlan()){const n=limitOf(e,r);return null===n||n<0?1/0:Math.max(0,n-Math.max(0,Number(t)||0))}export function checkLimit(e,t,r=currentPlan()){const a=PLAN_RESOURCES[e],i=a?limitOf(e,r):null;if(null===i||i<0)return null;const l=Math.max(0,Number(t)||0);if(l<i)return null;const o=gateOf(e,r),u=i<=0&&!!o&&Array.isArray(r?.entitlements)&&!r.entitlements.includes(o);let s;return s=u?featureMessage(o,r):i<=0?`当前会员等级暂不支持创建${a.label},请升级会员后使用`:`${function(e){return e?.name?["free","team","enterprise"].includes(e.code)?e.name:`${e.name}会员`:Number.isInteger(e?.tier)&&n[e.tier]?n[e.tier]:"当前会员等级"}(r)}最多可创建 ${i} 个${a.label},如需更多请升级会员`,{code:u?"SUBSCRIPTION_REQUIRED":"RESOURCE_LIMIT_EXCEEDED",resource:e,limit:i,count:l,message:s}}export function checkFeature(e,t=currentPlan()){return!Array.isArray(t?.entitlements)||t.entitlements.includes(e)?null:{code:"SUBSCRIPTION_REQUIRED",feature:e,message:featureMessage(e,t)}}export class PlanLimitError extends Error{constructor(e){super(e.message),this.name="PlanLimitError",this.code=e.code,this.resource=e.resource||"",this.feature=e.feature||"",this.limit=e.limit??null,this.count=e.count??null}}export function isPlanLimitError(e){return!!e&&(e instanceof PlanLimitError||"PlanLimitError"===e.name)}export function assertLimit(e,t,r=currentPlan()){const n=checkLimit(e,t,r);if(n)throw new PlanLimitError(n)}export function assertFeature(e,t=currentPlan()){const r=checkFeature(e,t);if(r)throw new PlanLimitError(r)}export function planDeniedForModel(e){return`${String(e?.message||"").replace(/[。.]$/,"")}。这是当前账号会员等级的限制,本端无法绕过——请如实告诉用户,不要改用写文件、命令行等其它方式变相创建;需要更多额度请到平台控制台「会员订阅」升级。`}
1
+ import{getPlatformConfig as e}from"./api.js";import{loadConfig as t,saveConfig as r}from"./config.js";export const PLAN_RESOURCES=Object.freeze({skills:Object.freeze({label:"技能",gate:""}),mcp_servers:Object.freeze({label:"MCP服务",gate:""}),scheduled_tasks:Object.freeze({label:"自动化任务",gate:"agent_tasks"}),agents:Object.freeze({label:"专家",gate:""}),squads:Object.freeze({label:"专家团",gate:""}),projects:Object.freeze({label:"项目",gate:"project"})});export const PLAN_FEATURES=Object.freeze({agent_tasks:"Agent自动化任务",knowledge_base:"知识库",project:"项目"});export function upgradeUrl(e){const t=String(e||"").trim().replace(/\/+$/,"");return t?`${t}/console/membership`:""}const n=["免费版","Plus会员","Pro会员","Max会员","团队版","企业版"],a=3e4;let i={key:"",at:0,plan:null,fetched:!1,failAt:0},l=null,u=null;const o=e=>`${String(e?.baseUrl||"")}\n${String(e?.apiKey||"")}`,s=e=>e&&"object"==typeof e&&!Array.isArray(e)?e:null;export function planFromUser(e){if(!e||"object"!=typeof e)return null;const t=s(e.limits),r=Array.isArray(e.entitlements)?e.entitlements.map(String):null;if(!t&&!r)return null;const n=e.tier_level??e.tierLevel,a=null==n||""===n?NaN:Number(n),i=Number(e.context_limit??e.contextLimit);return{code:String(e.plan_code??e.planCode??"").trim().toLowerCase(),name:String(e.plan_name??e.planName??"").trim(),tier:Number.isInteger(a)?a:null,limits:t,entitlements:r,featureNames:s(e.feature_names??e.featureNames),gates:s(e.limit_gates??e.limitGates),contextLimit:Number.isFinite(i)&&i>=0?Math.floor(i):null}}export function contextLimitOf(e=currentPlan()){const t=Number(e?.contextLimit);return Number.isFinite(t)&&t>0?t:0}export function currentPlan(e=t()){return i.fetched&&i.key===o(e)?i.plan:planFromUser(e?.user)}export async function refreshPlan(r=t(),{force:n=!1}={}){if(!r?.baseUrl||!r?.apiKey)return currentPlan(r);const s=o(r),c=Date.now();if(!n&&i.key===s){if(i.fetched&&c-i.at<a)return i.plan;if(i.failAt&&c-i.failAt<a)return currentPlan(r)}if(l&&l.key===s)return l.promise;const f=(async()=>{let t={};try{t=await e(r.baseUrl,r.apiKey,{timeoutMs:6e3})}catch{t={}}return t&&"object"==typeof t&&t.user&&"object"==typeof t.user?(i={key:s,at:Date.now(),plan:planFromUser(t.user),fetched:!0,failAt:0},u=null,i.plan):(i=i.key===s?{...i,failAt:Date.now()}:{key:s,at:0,plan:null,fetched:!1,failAt:Date.now()},currentPlan(r))})().finally(()=>{l&&l.promise===f&&(l=null)});return l={key:s,promise:f},f}export async function syncPlanUser(n=t()){if(!n?.baseUrl||!n?.apiKey)return currentPlan(n);let a={};try{a=await e(n.baseUrl,n.apiKey,{timeoutMs:6e3})}catch{a={}}const l=t();if(l.apiKey!==n.apiKey||l.baseUrl!==n.baseUrl)return currentPlan(l);const u=a&&"object"==typeof a.user?a.user:null;if(!u)return currentPlan(n);primePlan(n,u);try{JSON.stringify(n.user??null)!==JSON.stringify(u)&&(r({user:u}),n.user=u)}catch{}return i.plan}export function primePlan(e,t){e?.baseUrl&&e?.apiKey&&t&&"object"==typeof t&&(i={key:o(e),at:Date.now(),plan:planFromUser(t),fetched:!0,failAt:0},u=null)}export function noteContextLimit(e,t){const r=Math.floor(Number(t));r>0&&e&&(u={key:o(e),limit:r})}export function contextLimitHint(e){return u&&e&&u.key===o(e)?u.limit:0}export function resetPlanCache(){i={key:"",at:0,plan:null,fetched:!1,failAt:0},l=null,u=null}export async function planVerdict(e,r=t()){await refreshPlan(r);const n=i.fetched&&i.key===o(r)&&Date.now()-i.at<a?i.plan?.entitlements:null;return Array.isArray(n)?n.includes(e)?"granted":"denied":"unknown"}export function featureName(e,t=currentPlan()){const r=t?.featureNames?.[e];return"string"==typeof r&&r.trim()||PLAN_FEATURES[e]||e}export function featureMessage(e,t=currentPlan()){return`当前会员等级未开通「${featureName(e,t)}」,请升级会员后使用`}export function automationSuspendedMessage(e=currentPlan()){return`当前会员等级未开通「${featureName("agent_tasks",e)}」,任务已自动暂停;升级会员后可重新启用`}export function gateOf(e,t=currentPlan()){return t?.gates?String(t.gates[e]||""):PLAN_RESOURCES[e]?.gate||""}export function limitOf(e,t=currentPlan()){const r=t?.limits?.[e];if(null==r||""===r)return null;const n=Number(r);return Number.isFinite(n)?n<0?-1:Math.floor(n):null}export function remainingOf(e,t,r=currentPlan()){const n=limitOf(e,r);return null===n||n<0?1/0:Math.max(0,n-Math.max(0,Number(t)||0))}export function checkLimit(e,t,r=currentPlan()){const a=PLAN_RESOURCES[e],i=a?limitOf(e,r):null;if(null===i||i<0)return null;const l=Math.max(0,Number(t)||0);if(l<i)return null;const u=gateOf(e,r),o=i<=0&&!!u&&Array.isArray(r?.entitlements)&&!r.entitlements.includes(u);let s;return s=o?featureMessage(u,r):i<=0?`当前会员等级暂不支持创建${a.label},请升级会员后使用`:`${function(e){return e?.name?["free","team","enterprise"].includes(e.code)?e.name:`${e.name}会员`:Number.isInteger(e?.tier)&&n[e.tier]?n[e.tier]:"当前会员等级"}(r)}最多可创建 ${i} 个${a.label},如需更多请升级会员`,{code:o?"SUBSCRIPTION_REQUIRED":"RESOURCE_LIMIT_EXCEEDED",resource:e,limit:i,count:l,message:s}}export function checkFeature(e,t=currentPlan()){return!Array.isArray(t?.entitlements)||t.entitlements.includes(e)?null:{code:"SUBSCRIPTION_REQUIRED",feature:e,message:featureMessage(e,t)}}export class PlanLimitError extends Error{constructor(e){super(e.message),this.name="PlanLimitError",this.code=e.code,this.resource=e.resource||"",this.feature=e.feature||"",this.limit=e.limit??null,this.count=e.count??null}}export function isPlanLimitError(e){return!!e&&(e instanceof PlanLimitError||"PlanLimitError"===e.name)}export function assertLimit(e,t,r=currentPlan()){const n=checkLimit(e,t,r);if(n)throw new PlanLimitError(n)}export function assertFeature(e,t=currentPlan()){const r=checkFeature(e,t);if(r)throw new PlanLimitError(r)}export function planDeniedForModel(e){return`${String(e?.message||"").replace(/[。.]$/,"")}。这是当前账号会员等级的限制,本端无法绕过——请如实告诉用户,不要改用写文件、命令行等其它方式变相创建;需要更多额度请到平台控制台「会员订阅」升级。`}
package/src/privatedir.js CHANGED
@@ -1 +1 @@
1
- import t from"node:fs";import r from"node:path";import n from"node:crypto";import{CONFIG_DIR as e}from"./config.js";export function bucketOf(t){const r=String(t||"anonymous");return n.createHash("sha256").update(r).digest("hex").slice(0,16)}export function safeSegment(t,r="item",n=40){const e=String(t||"").replace(/[^A-Za-z0-9_.-]+/g,"_").slice(0,n);return!e||/^\.+$/.test(e)?r:e}export function areaDir(t){return r.join(e,t)}export function sessionDir(t,n){return r.join(areaDir(t),bucketOf(n))}export function createPrivateFile(e,o,c,i=""){try{const s=sessionDir(e,o);t.mkdirSync(s,{recursive:!0,mode:448});const a=Date.now().toString(36),u=n.randomBytes(4).toString("hex"),f=r.join(s,`${a}-${u}-${safeSegment(c)}.txt`),y=t.openSync(f,"wx",384);try{i&&t.writeFileSync(y,String(i),"utf8")}finally{t.closeSync(y)}return{path:f}}catch{return null}}export function dropSessionDir(r,n){try{return t.rmSync(sessionDir(r,n),{recursive:!0,force:!0}),!0}catch{return!1}}export function pruneArea(n,e){const o=Date.now(),c=areaDir(n);try{const n=t.readdirSync(c).slice(0,200);for(const i of n){const n=r.join(c,i);let s;try{s=t.readdirSync(n)}catch{continue}let a=s.length;for(const c of s){const i=r.join(n,c);try{o-t.statSync(i).mtimeMs>e&&(t.unlinkSync(i),a--)}catch{}}if(a<=0)try{t.rmdirSync(n)}catch{}}}catch{}}export function makeThrottle(t){let r=0;return()=>{const n=Date.now();return!(n-r<t)&&(r=n,!0)}}
1
+ import t from"node:fs";import r from"node:path";import n from"node:crypto";import{accountDataDir as e}from"./account-scope.js";export function bucketOf(t){const r=String(t||"anonymous");return n.createHash("sha256").update(r).digest("hex").slice(0,16)}export function safeSegment(t,r="item",n=40){const e=String(t||"").replace(/[^A-Za-z0-9_.-]+/g,"_").slice(0,n);return!e||/^\.+$/.test(e)?r:e}export function areaDir(t){return r.join(e(),t)}export function sessionDir(t,n){return r.join(areaDir(t),bucketOf(n))}export function createPrivateFile(e,o,c,i=""){try{const s=sessionDir(e,o);t.mkdirSync(s,{recursive:!0,mode:448});const a=Date.now().toString(36),u=n.randomBytes(4).toString("hex"),f=r.join(s,`${a}-${u}-${safeSegment(c)}.txt`),p=t.openSync(f,"wx",384);try{i&&t.writeFileSync(p,String(i),"utf8")}finally{t.closeSync(p)}return{path:f}}catch{return null}}export function dropSessionDir(r,n){try{return t.rmSync(sessionDir(r,n),{recursive:!0,force:!0}),!0}catch{return!1}}export function pruneArea(n,e){const o=Date.now(),c=areaDir(n);try{const n=t.readdirSync(c).slice(0,200);for(const i of n){const n=r.join(c,i);let s;try{s=t.readdirSync(n)}catch{continue}let a=s.length;for(const c of s){const i=r.join(n,c);try{o-t.statSync(i).mtimeMs>e&&(t.unlinkSync(i),a--)}catch{}}if(a<=0)try{t.rmdirSync(n)}catch{}}}catch{}}export function makeThrottle(t){let r=0;return()=>{const n=Date.now();return!(n-r<t)&&(r=n,!0)}}
package/src/repl.js CHANGED
@@ -1 +1 @@
1
- import{terminalSelect as s}from"./terminal-select.js";import{questionSelection as o}from"./selection.js";import{parseAskAnswer as e}from"./ask-ui.js";import t from"node:readline";import{syncPlanUser as r}from"./plan-limits.js";import{c as n}from"./colors.js";import{createPermissions as i}from"./permissions.js";import{runTurn as c}from"./agent.js";import{createSession as a,saveSession as l,turnCount as u}from"./session.js";import{syncSessionLog as p,rewindSessionLog as m}from"./sessionlog.js";import{ensureSessionTitle as d}from"./title.js";import{createCheckpoints as f}from"./checkpoints.js";import{buildSystemPrompt as g}from"./context.js";import{runVerifyLoop as w}from"./autofix.js";import{runAutoFlow as h}from"./autorun.js";import{loadMcpTools as j,closeMcp as y,makeMcpConnectTools as b}from"./mcp.js";import{makeJobTools as k,onSettled as x,describe as I}from"./jobs.js";import{makeTerminalTools as T,closeAllTerminals as S,dropSessionTerminals as v}from"./terminal.js";import{makeGoalTools as $,runGoalRounds as q,setRoundKind as O,getGoal as P,viewGoal as U}from"./goalround.js";import{makeCodeModeTool as _}from"./codemode.js";import{makeWorkflowTool as C}from"./workflow.js";import{makeLspTool as M,setProvider as A}from"./lsp.js";import{ensureVerified as E}from"./sandbox.js";import{makeAutomationTools as R}from"./automation-tools.js";import{makeNotifyTool as L,notifyTurnDone as N}from"./notify.js";import{makePresenceTools as W,acquireWakeLock as B,releaseWakeLock as G}from"./presence.js";import{makeComputerTools as H}from"./computer.js";import{makeDiagramTools as V}from"./diagram.js";import{makePptxTools as D}from"./pptx-tools.js";import{makeDocxTools as Y}from"./docx-tools.js";import{makeReportTools as J}from"./report-tools.js";import{makeXlsxTools as K}from"./xlsx-tools.js";import{makeCodeTools as Q}from"./code-tools.js";import{makeTestTools as z}from"./test-runner.js";import{makeWebTools as F}from"./web-inspect.js";import{browserAvailable as X}from"./web-cdp.js";import{makeUiTools as Z}from"./ui-spec.js";import{makeAskUiTool as ss}from"./ask-ui.js";import{makeSurfaceTools as os}from"./surface-state.js";import{makeVoiceFormTool as es}from"./voice-form.js";import{makeDepAuditTool as ts}from"./depaudit.js";import{makeScopeCheckTool as rs}from"./scopecreep.js";import{makeArchaeologyTool as ns}from"./archaeology.js";import{streamChat as is}from"./api.js";import{execSync as cs}from"node:child_process";import{createVisualBridge as as}from"./visual.js";import{autoSyncPlatformSkills as ls}from"./skills.js";import{makeSubagentTool as us,makeDispatchAgentsTool as ps,makeContinuableAgentTools as ms}from"./subagent.js";import{makeDispatchMemberTool as ds}from"./squad.js";import{makeConsultTool as fs}from"./consult.js";import{makeExpertTools as gs}from"./experts.js";import{listWorkspaceFiles as ws}from"./executors.js";import{makeAdvisorTool as hs}from"./advisor.js";import{makeRecallTool as js,readMemoryRaw as ys}from"./memory.js";import{detectScanners as bs,makeScanTool as ks}from"./scan.js";import{runSlash as xs,listCommands as Is}from"./commands.js";import{handleAccountError as Ts,accountErrorKind as Ss}from"./account.js";import{expandAtMentions as vs,buildUserContent as $s,runBangCommand as qs,loadHistory as Os,appendHistory as Ps}from"./input.js";import{makeSkillTools as Us}from"./skills.js";import{makeSessionEventTools as _s}from"./session-events.js";import{makeExternalAgentTools as Cs}from"./external-agents.js";import{makeWebSearchTools as Ms}from"./websearch.js";import{renderWelcome as As}from"./banner.js";import{createCrewBudget as Es,withBudget as Rs,makePlanBudgetTool as Ls,makeBudgetStatusTool as Ns}from"./crew.js";import{makeEvidenceTools as Ws}from"./evidence.js";import{makeTapdTools as Bs}from"./tapd.js";import{makeWeeklyTools as Gs}from"./weekly.js";export async function buildExtraTools(s,o,e={}){const t="function"==typeof e.onSub?e.onSub:null;let r={tools:[],clients:[],registry:{servers:{}}};try{r=await j(o)}catch(s){process.stdout.write(n.yellow("MCP初始化失败(忽略):"+s.message)+"\n")}const i=[...Us(o),...r.tools],c=s.crewBudget?Es(s.crewBudgetPlan||{}):null,a=(s,o,e)=>c?Rs(s,c,o,e):s,l=us({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t});l&&i.push(a(l,"dispatch"));const u=ps({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t});u&&i.push(a(u,"dispatch",s=>(s?.tasks||[]).length||1)),i.push(...ms({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t,queueInject:"function"==typeof e.queueInject?e.queueInject:null}));const p=ds({config:s,cwd:o,extraTools:r.tools,onSub:t});p&&i.push(p),i.push(...Ms({config:s})),i.push(...Cs({config:s,cwd:o,onSub:t}));const m=fs({config:s});m&&i.push(m);try{i.push(...await gs({config:s}))}catch{}const d=hs({config:s});if(d?i.push(a(d,"advisor")):c&&c.degrade("顾问","未配置advisorModel"),c&&(i.push(Ls(c,e.onBudgetDeclared||null)),i.push(Ns(c))),!1!==s.memoryEnabled&&ys(o)&&i.push(js({cwd:o})),bs(o).length&&i.push(ks({cwd:o})),i.push(...k({sessionId:()=>s.sessionId||""})),"function"==typeof e.queueInject){x(o=>{if(o.sessionId!==String(s.sessionId||""))return;const t="agent"===o.kind?`它已转为空闲,可用send_message继续对话;想看这一段的细节用job_output("${o.id}")。`:`用job_output("${o.id}") 读它的输出(默认只回新增)。`;e.queueInject(`【后台作业完成】${I(o)}\n${t}`)})}i.push(C({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t})),i.push(_()),"function"==typeof e.sessionOf&&i.push(...$({sessionOf:e.sessionOf})),e.lspProvider&&A(e.lspProvider),i.push(M({cwd:o})),i.push(...await T({sessionId:()=>s.sessionId||"",cwd:o,config:s})),i.push(...b({registry:r.registry,clients:r.clients,cwd:o,register:s=>i.push(...s)})),i.push(...R({cwd:o,selfSchedule:!1!==e.selfSchedule,runnerHint:e.runnerHint})),i.push(...V({cwd:o})),i.push(...D({cwd:o,config:s})),i.push(...Y({cwd:o,config:s})),i.push(...J({cwd:o,config:s})),i.push(...K({cwd:o,config:s})),i.push(...Q({cwd:o,config:s})),i.push(...z({cwd:o,config:s})),i.push(...Ws({cwd:o})),i.push(...Bs({cwd:o})),i.push(...Gs({cwd:o})),i.push(...Z({cwd:o})),i.push(ss({ask:e.ask,askUi:e.askUi})),e.sessionId&&i.push(...os({sessionId:e.sessionId,emit:e.onSurface})),i.push(...ts({cwd:o})),i.push(..._s({cwd:o,config:s})),i.push(...ns({cwd:o})),i.push(...rs({cwd:o,runGit:(s,o)=>{try{return cs(`git ${o.join(" ")}`,{cwd:s,encoding:"utf8",stdio:["ignore","pipe","ignore"],maxBuffer:33554432})}catch{return null}},goalOf:()=>{try{const s="function"==typeof e.sessionOf?P(e.sessionOf()):null;return s&&"active"===s.phase?s.objective:""}catch{return""}}})),i.push(...es(async(o,e)=>{let t="";try{await is({baseUrl:s.baseUrl,apiKey:s.apiKey,model:s.model,messages:[{role:"system",content:o},{role:"user",content:e}],temperature:0,client:s.client,extraBody:{enable_thinking:!1},aux:!0},{onContent:s=>{t+=s}})}catch{return""}return t}));const f=L({enabled:!1!==s.notifications});f&&i.push(f),i.push(...W({signalRef:e.signalRef}));const g="function"==typeof e.injectVisual?e.injectVisual:"function"==typeof e.queueInject?as({config:s,queue:e.queueInject,currentModel:e.currentModel}):null;return i.push(...H({config:s,injectVisual:g,cwd:o,omit:e.omitComputer||[],enabled:e.computerUse})),X()&&i.push(...F({cwd:o,config:s,injectVisual:g})),ls(s,{onInfo:o=>{s.debug&&process.stdout.write(n.gray(" · "+o)+"\n")}}).catch(()=>{}),{extraTools:i,clients:r.clients,mcpRegistry:r.registry,crew:c}}export async function startRepl({config:u,cwd:j,yolo:b,initialPrompt:k,session:x,version:I,...T}){E().catch(()=>{}),r(u).catch(()=>{});const $=t.createInterface({input:process.stdin,output:process.stdout,history:Os(j).reverse(),historySize:200,completer:s=>{if(/^\/\S*$/.test(s)){const o=Is(j).flatMap(s=>[s.name,...s.aliases||[]]);return[o.map(s=>"/"+s).filter(o=>o.startsWith(s)),s]}const o=/(?:^|\s)@([^\s@]*)$/.exec(s);if(!o)return[[],s];const e="@"+o[1],t=e.toLowerCase();return[ws(j).map(s=>"@"+s).filter(s=>s.toLowerCase().startsWith(t)).slice(0,40),e]}});let U,_=!1;const C=s=>new Promise((o,e)=>{if($.closed)return e(new Error("终端已关闭"));const t=new AbortController,r=()=>{U=null,e(new Error("终端已关闭"))},n=s=>{$.off("close",r),U=null,o(s)};U=()=>{t.abort(),n("")},$.once("close",r),$.question(s,{signal:t.signal},n)});let M=Promise.resolve();const A=process.stdin.isTTY&&process.stdout.isTTY?o=>{const e=Y?.signal,t=M.then(async()=>{_=!0;try{return await s(o,{rl:$,signal:e,onInterrupt:()=>Y?.abort()})}finally{_=!1}});return M=t.catch(()=>null),t}:void 0,R=T.askUi||(A?async s=>{const t=await A(o(s));return"date_range"===s.kind&&t?.value?e(s,t.value):t||{value:null}}:void 0);let L=!1,W=i({yolo:b,ask:C,select:A,cwd:j});const H={prompt:0,completion:0},V={list:[]},D=f();let Y=null,J=null;const K={config:u,cwd:j,permissions:W,usage:H,session:x,version:I,crewOf:()=>Z};$.on("SIGINT",()=>{_?(Y?.abort(),U?.(),process.stdout.write("\n")):Y?Y.abort():process.stdout.write("\n"+n.gray("输入 /exit退出。")+"\n")}),function(s,o,e,t){process.stdout.write(As({model:s.model,cwd:o,version:t,platform:/^(agent|workflow|datahuman|squad)-/.test(String(s.model||"")),yolo:e.isYolo(),consult:s.consult?s.consultLabel||s.consult:null})+"\n")}(u,j,W,I),Vs(x);const Q=[],z={current:null};let{extraTools:F,clients:X,crew:Z}=await buildExtraTools(u,j,{sessionOf:()=>K.session,queueInject:s=>Q.push(s),signalRef:z,ask:C,askUi:R,sessionId:K.session?.id,onSurface:T.onSurface,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});const ss=()=>{d(K.session,u).then(s=>{s&&l(K.session)}).catch(()=>{})},os=async s=>{const{text:o,mentions:e,images:t}=await vs(s,j,u,{onDocStart:(s,o,e)=>process.stdout.write(n.gray(` · 正在解析文档 @${s}${e>1?`(${o}/${e})`:""}…`)+"\n")});e.length&&process.stdout.write(n.gray(" · 已展开引用:"+e.join("、")+(t.length?`(含 ${t.length} 张图片)`:""))+"\n");const r=D.snapshot({session:K.session,label:s});K.session.messages.push({role:"user",content:$s(o,t)}),ss(),Y=new AbortController,z.current=Y.signal,K.session.compaction||(K.session.compaction={}),u.sessionId=K.session.id,u.goal=P(K.session)?.objective||"";const i="auto"===W.getMode?.()&&!1!==u.wakeLockInAuto?B({reason:"自动模式任务执行中"}):null,a=Date.now();let m;try{m=await c({config:u,messages:K.session.messages,permissions:W,cwd:j,signal:Y.signal,extraTools:F,todoState:V,compaction:K.session.compaction,fileJournal:r,pullQueued:()=>Q.shift()||null})}finally{i?.ok&&G()}return m?.usage&&(H.prompt+=m.usage.prompt,H.completion+=m.usage.completion),m?.lastPromptTokens&&(K.session.lastPromptTokens=m.lastPromptTokens),m?.lastUsage&&(K.session.lastUsage=m.lastUsage),Y=null,z.current=null,m?.aborted||m?.error||N({durationMs:Date.now()-a,cwd:j,summary:String(s).replace(/\s+/g," ").slice(0,80),enabled:!1!==u.notifications}).catch(()=>{}),p(K.session),l(K.session),ss(),m?.error&&"auth"===Ts(m,u)&&(L=!0),m},es=async s=>{O(K.session.id,"human");const o=await os(s);return o?.aborted||o?.error||L?o:P(K.session)?(await q({session:K.session,doTurn:os,stats:()=>({usedTokens:H.prompt+H.completion,budgetTokens:Number(u.budgetTokens)||0}),onInfo:s=>process.stdout.write(n.gray(" ◆ "+s)+"\n"),aborted:()=>L}),l(K.session),o):o},ts=()=>({...K,signal:Y?.signal,select:A,resumeSession:s=>{J&&(clearInterval(J),J=null),v(K.session.id),K.session=s,u.sessionId=s.id,s.model&&(u.model=s.model);const o=W.getMode();W=i({yolo:!1,ask:C,select:A,cwd:j}),W.setMode(o),K.permissions=W,Q.length=0,V.list=[],D.list.length=0,Vs(s)},session:K.session,extraTools:F,checkpoints:D,onRewind:s=>m(K.session,s),print:s=>process.stdout.write(String(s).endsWith("\n")?String(s):String(s)+"\n")});for(k&&(process.stdout.write(n.cyan("\nhuicai> ")+k+"\n"),await es(k));!L;){let s;try{s=(await C(n.cyan("\nhuicai> "))).trim()}catch{break}if(""!==s){if(Ps(j,s),"/exit"===s||"/quit"===s)break;if(s.startsWith("!")&&s.length>1){const o=s.slice(1).trim(),{output:e,record:t}=await qs(o,j);process.stdout.write(n.gray(e.split("\n").map(s=>" "+s).join("\n"))+"\n"),K.session.messages.push({role:"user",content:t}),l(K.session);continue}if(s.startsWith("/")){let o;Y=new AbortController;try{o=await xs(s,ts())}catch(s){process.stdout.write((Y.signal.aborted?"操作已取消":"命令失败:"+s.message)+"\n");continue}finally{Y=null}if($.closed)break;if("exit"===o.action)break;if("clear"===o.action){J&&(clearInterval(J),J=null);const s=W.getMode();W=i({yolo:!1,ask:C,select:A,cwd:j}),W.setMode(s),K.permissions=W,l(K.session),v(K.session.id),K.session=a({cwd:j,model:u.model,messages:[K.session.messages[0]]}),u.sessionId=K.session.id,Q.length=0,V.list=[],D.list.length=0,process.stdout.write(n.gray("已开启新会话(旧会话已保存,可用huicai --resume恢复)。\n"))}"refresh"===o.action&&Vs(K.session),"cd"===o.action&&(j=o.cwd,K.cwd=j,W=i({yolo:W.isYolo(),ask:C,select:A,cwd:j}),K.permissions=W,y(X),({extraTools:F,clients:X,crew:Z}=await buildExtraTools(u,j,{ask:C,askUi:R,sessionOf:()=>K.session,queueInject:s=>Q.push(s),signalRef:z,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"})),K.session=a({cwd:j,model:u.model,messages:[{role:"system",content:g(j,{memory:!1!==u.memoryEnabled,config:u})}]}),V.list=[],D.list.length=0,u.sessionId=K.session.id,process.stdout.write(n.gray("已切换到 "+j+"(新会话,旧会话已保存)。\n"))),"autofix"===o.action&&await w({command:o.command,maxIters:o.maxIters,cwd:j,runTurnOnce:s=>os(s),emit:s=>process.stdout.write(n.gray(s)+"\n")}),"autorun"===o.action&&await h({goal:o.goal,config:u,permissions:W,cwd:j,runTurnOnce:s=>os(s),emit:s=>process.stdout.write(n.gray(" · "+s)+"\n")}),"loop"===o.action&&(J&&(clearInterval(J),J=null),o.stop?process.stdout.write(n.gray("已停止循环。\n")):(process.stdout.write(n.gray(`已开始循环:每 ${o.seconds}s运行「${o.prompt}」(/loop off停止)。\n`)),J=setInterval(()=>{Y||os(o.prompt)},1e3*o.seconds))),o.prompt&&await es(o.prompt);continue}await es(s)}}return J&&clearInterval(J),$.close(),y(X),S(),L?{relogin:!0}:(process.stdout.write(n.gray("\n再见。\n")),{})}function Hs(s){return"string"==typeof s?s:Array.isArray(s)?s.map(s=>"string"==typeof s?s:s&&"text"===s.type&&s.text||"").join(""):""}function Vs(s){const o=(s.messages||[]).filter(s=>"user"===s.role||"assistant"===s.role),e=u(s.messages);if(0===e)return;process.stdout.write(n.gray(` 已恢复会话「${s.title||""}」(${e} 轮)——历史对话如下:`)+"\n");const t=o.length>40?o.length-40:0;t&&process.stdout.write(n.gray(` (较早 ${t} 条已省略,完整记录在 ~/.huicai/sessions)`)+"\n");for(const s of o.slice(-40)){const o=Hs(s.content).trim();if(!o)continue;const e=o.length>800?o.slice(0,800)+"…":o,t="user"===s.role?n.bold(" 你 › "):n.gray(" 助手 › ");process.stdout.write(t+e.replace(/\n/g,"\n ")+"\n")}process.stdout.write(n.gray(" ——以上为历史,可继续对话——")+"\n")}export async function runOnce({config:s,cwd:o,yolo:e,prompt:t,session:r,persist:a,outputFormat:m,maxTurns:d}){t||(process.stderr.write('用法: huicai -p "你的指令"\n'),process.exit(1));const f="json"===m,g=i({yolo:e,ask:null,cwd:o}),w=[],h={current:null},j=new AbortController;h.current=j.signal;let b=!1;const k=()=>{b&&process.exit(130),b=!0,process.stderr.write(n.gray("\n已请求中断(再按一次Ctrl-C强制退出)…\n")),j.abort()};process.on("SIGINT",k);const{extraTools:x,clients:I}=await buildExtraTools(s,o,{sessionOf:()=>r,queueInject:s=>w.push(s),signalRef:h,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});let T=t;if(t.trim().startsWith("/")){const{runSlash:e}=await import("./commands.js"),n=[],i=await e(t.trim(),{cwd:o,config:s,session:r,permissions:g,print:s=>n.push(String(s))}).catch(s=>({handled:!1,error:s}));if(n.length&&process.stdout.write(n.join("\n")+"\n"),i&&i.prompt)T=i.prompt;else if(i&&i.handled)return y(I),process.off("SIGINT",k),{text:n.join("\n")}}const{text:v,images:$}=await vs(T,o,s);r.messages.push({role:"user",content:$s(v,$)}),r.compaction||(r.compaction={}),s.sessionId=r.id;const q=e&&!1!==s.wakeLockInAuto?B({reason:"无人值守任务执行中"}):null;let O;try{O=await c({config:s,messages:r.messages,permissions:g,cwd:o,extraTools:x,signal:j.signal,maxIters:d,compaction:r.compaction,io:f?{write:()=>{}}:void 0,pullQueued:()=>w.shift()||null})}finally{q?.ok&&G()}O?.lastPromptTokens&&(r.lastPromptTokens=O.lastPromptTokens),O?.lastUsage&&(r.lastUsage=O.lastUsage),a&&(l(r),p(r)),process.removeListener("SIGINT",k),y(I),S();const P=O?.error?Ss(O):"";if(P&&(Ts(O,s,s=>process.stderr.write(s)),"auth"===P&&process.stderr.write(n.gray("请重新运行 `huicai login` 登录后重试。")+"\n")),f){const o=[...r.messages].reverse().find(s=>"assistant"===s.role&&s.content);process.stdout.write(JSON.stringify({type:"result",is_error:!!O?.error,...O?.error?{error:O.error}:{},...P?{error_kind:P}:{},result:o?String(o.content):"",usage:{input_tokens:O?.usage?.prompt??0,output_tokens:O?.usage?.completion??0},session_id:r.id,model:s.model,num_turns:u(r.messages)})+"\n")}O?.error&&process.exit(1)}
1
+ import{terminalSelect as s}from"./terminal-select.js";import{questionSelection as o}from"./selection.js";import{parseAskAnswer as e}from"./ask-ui.js";import t from"node:readline";import{syncPlanUser as r}from"./plan-limits.js";import{c as n}from"./colors.js";import{createPermissions as i}from"./permissions.js";import{runTurn as a}from"./agent.js";import{createSession as c,saveSession as l,turnCount as u}from"./session.js";import{syncSessionLog as p,rewindSessionLog as m}from"./sessionlog.js";import{ensureSessionTitle as d}from"./title.js";import{createCheckpoints as f}from"./checkpoints.js";import{buildSystemPrompt as g}from"./context.js";import{runVerifyLoop as w}from"./autofix.js";import{runAutoFlow as h}from"./autorun.js";import{loadMcpTools as j,closeMcp as y,makeMcpConnectTools as b}from"./mcp.js";import{makeJobTools as k,onSettled as x,describe as I}from"./jobs.js";import{makeTerminalTools as T,closeAllTerminals as v,dropSessionTerminals as S}from"./terminal.js";import{makeGoalTools as $,runGoalRounds as q,setRoundKind as O,getGoal as C,viewGoal as P}from"./goalround.js";import{makeCodeModeTool as U}from"./codemode.js";import{makeWorkflowTool as E}from"./workflow.js";import{makeLspTool as _,setProvider as M}from"./lsp.js";import{ensureVerified as A}from"./sandbox.js";import{makeAutomationTools as R}from"./automation-tools.js";import{makeNotifyTool as L,notifyTurnDone as H}from"./notify.js";import{makePresenceTools as W,acquireWakeLock as B,releaseWakeLock as N}from"./presence.js";import{makeComputerTools as V}from"./computer.js";import{makeDiagramTools as D}from"./diagram.js";import{makePptxTools as G}from"./pptx-tools.js";import{makeDocxTools as Y}from"./docx-tools.js";import{makeReportTools as J}from"./report-tools.js";import{makeXlsxTools as K}from"./xlsx-tools.js";import{makeCodeTools as Q}from"./code-tools.js";import{makeTestTools as z}from"./test-runner.js";import{makeEngineeringTools as F}from"./engineering.js";import{makeWebTools as X}from"./web-inspect.js";import{browserAvailable as Z}from"./web-cdp.js";import{makeUiTools as ss}from"./ui-spec.js";import{makeAskUiTool as os}from"./ask-ui.js";import{makeSurfaceTools as es}from"./surface-state.js";import{makeVoiceFormTool as ts}from"./voice-form.js";import{makeDepAuditTool as rs}from"./depaudit.js";import{makeScopeCheckTool as ns}from"./scopecreep.js";import{makeArchaeologyTool as is}from"./archaeology.js";import{streamChat as as}from"./api.js";import{execSync as cs}from"node:child_process";import{createVisualBridge as ls}from"./visual.js";import{autoSyncPlatformSkills as us}from"./skills.js";import{makeSubagentTool as ps,makeDispatchAgentsTool as ms,makeContinuableAgentTools as ds}from"./subagent.js";import{makeDispatchMemberTool as fs}from"./squad.js";import{makeConsultTool as gs}from"./consult.js";import{makeExpertTools as ws}from"./experts.js";import{listWorkspaceFiles as hs}from"./executors.js";import{makeAdvisorTool as js}from"./advisor.js";import{makeRecallTool as ys,readMemoryRaw as bs}from"./memory.js";import{detectScanners as ks,makeScanTool as xs}from"./scan.js";import{runSlash as Is,listCommands as Ts}from"./commands.js";import{handleAccountError as vs,accountErrorKind as Ss}from"./account.js";import{expandAtMentions as $s,buildUserContent as qs,runBangCommand as Os,loadHistory as Cs,appendHistory as Ps}from"./input.js";import{makeSkillTools as Us}from"./skills.js";import{makeSessionEventTools as Es}from"./session-events.js";import{makeExternalAgentTools as _s}from"./external-agents.js";import{makeWebSearchTools as Ms}from"./websearch.js";import{renderWelcome as As}from"./banner.js";import{createCrewBudget as Rs,withBudget as Ls,makePlanBudgetTool as Hs,makeBudgetStatusTool as Ws}from"./crew.js";import{makeEvidenceTools as Bs}from"./evidence.js";import{makeTapdTools as Ns}from"./tapd.js";import{makeWeeklyTools as Vs}from"./weekly.js";export async function buildExtraTools(s,o,e={}){const t="function"==typeof e.onSub?e.onSub:null;let r={tools:[],clients:[],registry:{servers:{}}};try{r=await j(o)}catch(s){process.stdout.write(n.yellow("MCP初始化失败(忽略):"+s.message)+"\n")}const i=[...Us(o),...r.tools],a=s.crewBudget?Rs(s.crewBudgetPlan||{}):null,c=(s,o,e)=>a?Ls(s,a,o,e):s,l=ps({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t});l&&i.push(c(l,"dispatch"));const u=ms({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t});u&&i.push(c(u,"dispatch",s=>(s?.tasks||[]).length||1)),i.push(...ds({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t,queueInject:"function"==typeof e.queueInject?e.queueInject:null}));const p=fs({config:s,cwd:o,extraTools:r.tools,onSub:t});p&&i.push(p),i.push(...Ms({config:s})),i.push(..._s({config:s,cwd:o,onSub:t}));const m=gs({config:s});m&&i.push(m);try{i.push(...await ws({config:s}))}catch{}const d=js({config:s});if(d?i.push(c(d,"advisor")):a&&a.degrade("顾问","未配置advisorModel"),a&&(i.push(Hs(a,e.onBudgetDeclared||null)),i.push(Ws(a))),!1!==s.memoryEnabled&&bs(o)&&i.push(ys({cwd:o})),ks(o).length&&i.push(xs({cwd:o})),i.push(...k({sessionId:()=>s.sessionId||""})),"function"==typeof e.queueInject){x(o=>{if(o.sessionId!==String(s.sessionId||""))return;const t="agent"===o.kind?`它已转为空闲,可用send_message继续对话;想看这一段的细节用job_output("${o.id}")。`:`用job_output("${o.id}") 读它的输出(默认只回新增)。`;e.queueInject(`【后台作业完成】${I(o)}\n${t}`)})}i.push(E({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t})),i.push(U()),"function"==typeof e.sessionOf&&i.push(...$({sessionOf:e.sessionOf})),e.lspProvider&&M(e.lspProvider),i.push(_({cwd:o})),i.push(...await T({sessionId:()=>s.sessionId||"",cwd:o,config:s})),i.push(...b({registry:r.registry,clients:r.clients,cwd:o,register:s=>i.push(...s)})),i.push(...R({cwd:o,selfSchedule:!1!==e.selfSchedule,runnerHint:e.runnerHint})),i.push(...D({cwd:o})),i.push(...G({cwd:o,config:s})),i.push(...Y({cwd:o,config:s})),i.push(...J({cwd:o,config:s})),i.push(...K({cwd:o,config:s})),i.push(...Q({cwd:o,config:s})),i.push(...z({cwd:o,config:s})),i.push(...F()),i.push(...Bs({cwd:o})),i.push(...Ns({cwd:o})),i.push(...Vs({cwd:o})),i.push(...ss({cwd:o})),i.push(os({ask:e.ask,askUi:e.askUi})),e.sessionId&&i.push(...es({sessionId:e.sessionId,emit:e.onSurface})),i.push(...rs({cwd:o})),i.push(...Es({cwd:o,config:s})),i.push(...is({cwd:o})),i.push(...ns({cwd:o,runGit:(s,o)=>{try{return cs(`git ${o.join(" ")}`,{cwd:s,encoding:"utf8",stdio:["ignore","pipe","ignore"],maxBuffer:33554432})}catch{return null}},goalOf:()=>{try{const s="function"==typeof e.sessionOf?C(e.sessionOf()):null;return s&&"active"===s.phase?s.objective:""}catch{return""}}})),i.push(...ts(async(o,e)=>{let t="";try{await as({baseUrl:s.baseUrl,apiKey:s.apiKey,model:s.model,messages:[{role:"system",content:o},{role:"user",content:e}],temperature:0,client:s.client,extraBody:{enable_thinking:!1},aux:!0},{onContent:s=>{t+=s}})}catch{return""}return t}));const f=L({enabled:!1!==s.notifications});f&&i.push(f),i.push(...W({signalRef:e.signalRef}));const g="function"==typeof e.injectVisual?e.injectVisual:"function"==typeof e.queueInject?ls({config:s,queue:e.queueInject,currentModel:e.currentModel}):null;return i.push(...V({config:s,injectVisual:g,cwd:o,omit:e.omitComputer||[],enabled:e.computerUse})),Z()&&i.push(...X({cwd:o,config:s,injectVisual:g})),us(s,{onInfo:o=>{s.debug&&process.stdout.write(n.gray(" · "+o)+"\n")}}).catch(()=>{}),{extraTools:i,clients:r.clients,mcpRegistry:r.registry,crew:a}}export async function startRepl({config:u,cwd:j,yolo:b,initialPrompt:k,session:x,version:I,...T}){A().catch(()=>{}),r(u).catch(()=>{});const $=t.createInterface({input:process.stdin,output:process.stdout,history:Cs(j).reverse(),historySize:200,completer:s=>{if(/^\/\S*$/.test(s)){const o=Ts(j).flatMap(s=>[s.name,...s.aliases||[]]);return[o.map(s=>"/"+s).filter(o=>o.startsWith(s)),s]}const o=/(?:^|\s)@([^\s@]*)$/.exec(s);if(!o)return[[],s];const e="@"+o[1],t=e.toLowerCase();return[hs(j).map(s=>"@"+s).filter(s=>s.toLowerCase().startsWith(t)).slice(0,40),e]}});let P,U=null,E=!1,_=null,M=!1;T.updater?.check.then(s=>{U=s,s&&M&&!$.line&&P?.()});let R=!1;const L=s=>new Promise((o,e)=>{if($.closed)return e(new Error("终端已关闭"));const t=new AbortController,r=()=>{P=null,e(new Error("终端已关闭"))},n=s=>{$.off("close",r),P=null,o(s)};P=()=>{t.abort(),n("")},$.once("close",r),$.question(s,{signal:t.signal},n)});let W=Promise.resolve();const V=process.stdin.isTTY&&process.stdout.isTTY?o=>{const e=z?.signal,t=W.then(async()=>{R=!0;try{return await s(o,{rl:$,signal:e,onInterrupt:()=>z?.abort()})}finally{R=!1}});return W=t.catch(()=>null),t}:void 0,D=T.askUi||(V?async s=>{const t=await V(o(s));return"date_range"===s.kind&&t?.value?e(s,t.value):t||{value:null}}:void 0);let G=!1,Y=i({yolo:b,ask:L,select:V,cwd:j});const J={prompt:0,completion:0},K={list:[]},Q=f();let z=null,F=null;const X={config:u,cwd:j,permissions:Y,usage:J,session:x,version:I,crewOf:()=>ts};$.on("SIGINT",()=>{R?(z?.abort(),P?.(),process.stdout.write("\n")):z?z.abort():process.stdout.write("\n"+n.gray("输入 /exit退出。")+"\n")}),function(s,o,e,t){process.stdout.write(As({model:s.model,cwd:o,version:t,platform:/^(agent|workflow|datahuman|squad)-/.test(String(s.model||"")),yolo:e.isYolo(),consult:s.consult?s.consultLabel||s.consult:null})+"\n")}(u,j,Y,I),Gs(x);const Z=[],ss={current:null};let{extraTools:os,clients:es,crew:ts}=await buildExtraTools(u,j,{sessionOf:()=>X.session,queueInject:s=>Z.push(s),signalRef:ss,ask:L,askUi:D,sessionId:X.session?.id,onSurface:T.onSurface,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});const rs=()=>{d(X.session,u).then(s=>{s&&l(X.session)}).catch(()=>{})},ns=async s=>{const{text:o,mentions:e,images:t}=await $s(s,j,u,{onDocStart:(s,o,e)=>process.stdout.write(n.gray(` · 正在解析文档 @${s}${e>1?`(${o}/${e})`:""}…`)+"\n")});e.length&&process.stdout.write(n.gray(" · 已展开引用:"+e.join("、")+(t.length?`(含 ${t.length} 张图片)`:""))+"\n");const r=Q.snapshot({session:X.session,label:s});X.session.messages.push({role:"user",content:qs(o,t)}),rs(),z=new AbortController,ss.current=z.signal,X.session.compaction||(X.session.compaction={}),u.sessionId=X.session.id,u.goal=C(X.session)?.objective||"";const i="auto"===Y.getMode?.()&&!1!==u.wakeLockInAuto?B({reason:"自动模式任务执行中"}):null,c=Date.now();let m;try{m=await a({config:u,messages:X.session.messages,permissions:Y,cwd:j,signal:z.signal,extraTools:os,todoState:K,compaction:X.session.compaction,fileJournal:r,pullQueued:()=>Z.shift()||null})}finally{i?.ok&&N()}return m?.usage&&(J.prompt+=m.usage.prompt,J.completion+=m.usage.completion),m?.lastPromptTokens&&(X.session.lastPromptTokens=m.lastPromptTokens),m?.lastUsage&&(X.session.lastUsage=m.lastUsage),z=null,ss.current=null,m?.aborted||m?.error||H({durationMs:Date.now()-c,cwd:j,summary:String(s).replace(/\s+/g," ").slice(0,80),enabled:!1!==u.notifications}).catch(()=>{}),p(X.session),l(X.session),rs(),m?.error&&"auth"===vs(m,u)&&(G=!0),m},is=async s=>{O(X.session.id,"human");const o=await ns(s);return o?.aborted||o?.error||G?o:C(X.session)?(await q({session:X.session,doTurn:ns,stats:()=>({usedTokens:J.prompt+J.completion,budgetTokens:Number(u.budgetTokens)||0}),onInfo:s=>process.stdout.write(n.gray(" ◆ "+s)+"\n"),aborted:()=>G}),l(X.session),o):o},as=()=>({...X,signal:z?.signal,select:V,resumeSession:s=>{F&&(clearInterval(F),F=null),S(X.session.id),X.session=s,u.sessionId=s.id,s.model&&(u.model=s.model);const o=Y.getMode();Y=i({yolo:!1,ask:L,select:V,cwd:j}),Y.setMode(o),X.permissions=Y,Z.length=0,K.list=[],Q.list.length=0,Gs(s)},session:X.session,extraTools:os,checkpoints:Q,onRewind:s=>m(X.session,s),print:s=>process.stdout.write(String(s).endsWith("\n")?String(s):String(s)+"\n")});for(k&&(process.stdout.write(n.cyan("\nhuicai> ")+k+"\n"),await is(k));!G;){if(U&&!E&&V){E=!0;if("update"===await V(T.updater.menu(U))){process.stdout.write("正在更新 HuiCai AI,请稍候…\n"),z=new AbortController;try{_=await T.updater.install(U,{signal:z.signal});break}catch(s){process.stdout.write(s.message+" 输入 /update 可重试。\n")}finally{z=null}}}let s;try{M=!0,s=(await L(n.cyan("\nhuicai> "))).trim()}catch{break}finally{M=!1}if(""!==s){if(Ps(j,s),"/exit"===s||"/quit"===s)break;if("/update"===s&&U&&T.updater)E=!1;else{if(s.startsWith("!")&&s.length>1){const o=s.slice(1).trim(),{output:e,record:t}=await Os(o,j);process.stdout.write(n.gray(e.split("\n").map(s=>" "+s).join("\n"))+"\n"),X.session.messages.push({role:"user",content:t}),l(X.session);continue}if(s.startsWith("/")){let o;z=new AbortController;try{o=await Is(s,as())}catch(s){process.stdout.write((z.signal.aborted?"操作已取消":"命令失败:"+s.message)+"\n");continue}finally{z=null}if($.closed)break;if("exit"===o.action)break;if("clear"===o.action){F&&(clearInterval(F),F=null);const s=Y.getMode();Y=i({yolo:!1,ask:L,select:V,cwd:j}),Y.setMode(s),X.permissions=Y,l(X.session),S(X.session.id),X.session=c({cwd:j,model:u.model,messages:[X.session.messages[0]]}),u.sessionId=X.session.id,Z.length=0,K.list=[],Q.list.length=0,process.stdout.write(n.gray("已开启新会话(旧会话已保存,可用huicai --resume恢复)。\n"))}"refresh"===o.action&&Gs(X.session),"cd"===o.action&&(j=o.cwd,X.cwd=j,Y=i({yolo:Y.isYolo(),ask:L,select:V,cwd:j}),X.permissions=Y,y(es),({extraTools:os,clients:es,crew:ts}=await buildExtraTools(u,j,{ask:L,askUi:D,sessionOf:()=>X.session,queueInject:s=>Z.push(s),signalRef:ss,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"})),X.session=c({cwd:j,model:u.model,messages:[{role:"system",content:g(j,{memory:!1!==u.memoryEnabled,config:u})}]}),K.list=[],Q.list.length=0,u.sessionId=X.session.id,process.stdout.write(n.gray("已切换到 "+j+"(新会话,旧会话已保存)。\n"))),"autofix"===o.action&&await w({command:o.command,maxIters:o.maxIters,cwd:j,runTurnOnce:s=>ns(s),emit:s=>process.stdout.write(n.gray(s)+"\n")}),"autorun"===o.action&&await h({goal:o.goal,config:u,permissions:Y,cwd:j,runTurnOnce:s=>ns(s),emit:s=>process.stdout.write(n.gray(" · "+s)+"\n")}),"loop"===o.action&&(F&&(clearInterval(F),F=null),o.stop?process.stdout.write(n.gray("已停止循环。\n")):(process.stdout.write(n.gray(`已开始循环:每 ${o.seconds}s运行「${o.prompt}」(/loop off停止)。\n`)),F=setInterval(()=>{z||ns(o.prompt)},1e3*o.seconds))),o.prompt&&await is(o.prompt);continue}await is(s)}}}return F&&clearInterval(F),$.close(),y(es),v(),T.updater?.dispose(),_?{updated:_}:G?{relogin:!0}:(process.stdout.write(n.gray("\n再见。\n")),{})}function Ds(s){return"string"==typeof s?s:Array.isArray(s)?s.map(s=>"string"==typeof s?s:s&&"text"===s.type&&s.text||"").join(""):""}function Gs(s){const o=(s.messages||[]).filter(s=>"user"===s.role||"assistant"===s.role),e=u(s.messages);if(0===e)return;process.stdout.write(n.gray(` 已恢复会话「${s.title||""}」(${e} 轮)——历史对话如下:`)+"\n");const t=o.length>40?o.length-40:0;t&&process.stdout.write(n.gray(` (较早 ${t} 条已省略,完整记录在 ~/.huicai/sessions)`)+"\n");for(const s of o.slice(-40)){const o=Ds(s.content).trim();if(!o)continue;const e=o.length>800?o.slice(0,800)+"…":o,t="user"===s.role?n.bold(" 你 › "):n.gray(" 助手 › ");process.stdout.write(t+e.replace(/\n/g,"\n ")+"\n")}process.stdout.write(n.gray(" ——以上为历史,可继续对话——")+"\n")}export async function runOnce({config:s,cwd:o,yolo:e,prompt:t,session:r,persist:c,outputFormat:m,maxTurns:d,signal:f,throwOnError:g=!1}){t||(process.stderr.write('用法: huicai -p "你的指令"\n'),process.exit(1));const w="json"===m,h=i({yolo:e,ask:null,cwd:o}),j=[],b={current:null},k=new AbortController;b.current=k.signal;let x=!1;const I=()=>{x&&process.exit(130),x=!0,process.stderr.write(n.gray("\n已请求中断(再按一次Ctrl-C强制退出)…\n")),k.abort()};process.on("SIGINT",I);const T=()=>{k.abort(f?.reason),v()};f?.addEventListener("abort",T,{once:!0}),f?.aborted&&T();let S=[];try{const i=await buildExtraTools(s,o,{sessionOf:()=>r,queueInject:s=>j.push(s),signalRef:b,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});S=i.clients;const m=i.extraTools;let f=t;if(t.trim().startsWith("/")){const{runSlash:e}=await import("./commands.js"),n=[],i=await e(t.trim(),{cwd:o,config:s,session:r,permissions:h,print:s=>n.push(String(s))}).catch(s=>({handled:!1,error:s}));if(n.length&&process.stdout.write(n.join("\n")+"\n"),i&&i.prompt)f=i.prompt;else if(i&&i.handled)return{ok:!i.error,text:n.join("\n")}}const{text:y,images:x}=await $s(f,o,s);r.messages.push({role:"user",content:qs(y,x)}),r.compaction||(r.compaction={}),s.sessionId=r.id;const I=e&&!1!==s.wakeLockInAuto?B({reason:"无人值守任务执行中"}):null;let T;try{T=await a({config:s,messages:r.messages,permissions:h,cwd:o,extraTools:m,signal:k.signal,maxIters:d,compaction:r.compaction,io:w?{write:()=>{}}:void 0,pullQueued:()=>j.shift()||null})}finally{I?.ok&&N()}T?.lastPromptTokens&&(r.lastPromptTokens=T.lastPromptTokens),T?.lastUsage&&(r.lastUsage=T.lastUsage),c&&(l(r),p(r));const v=T?.error?Ss(T):"";if(v&&(vs(T,s,s=>process.stderr.write(s)),"auth"===v&&process.stderr.write(n.gray("请重新运行 `huicai login` 登录后重试。")+"\n")),w){const o=[...r.messages].reverse().find(s=>"assistant"===s.role&&s.content);process.stdout.write(JSON.stringify({type:"result",is_error:!!T?.error,...T?.error?{error:T.error}:{},...v?{error_kind:v}:{},result:o?String(o.content):"",usage:{input_tokens:T?.usage?.prompt??0,output_tokens:T?.usage?.completion??0},session_id:r.id,model:s.model,num_turns:u(r.messages)})+"\n")}if(g&&(T?.error||T?.aborted))throw new Error(String(T.error||"任务已取消"));return T?.error&&(process.exitCode=1),{ok:!T?.error&&!T?.aborted,error:T?.error?String(T.error):T?.aborted?"任务已取消":"",text:String([...r.messages].reverse().find(s=>"assistant"===s.role&&s.content)?.content||"")}}finally{f?.removeEventListener("abort",T),process.off("SIGINT",I),y(S),v()}}
package/src/schedule.js CHANGED
@@ -1 +1 @@
1
- import e from"node:fs";import n from"node:path";import t from"node:os";import{freqToCron as r}from"./schedule-freq.js";import{assertFeature as o,assertLimit as i}from"./plan-limits.js";export const SCHEDULE_FILE=n.join(t.homedir(),".huicai","schedule.json");function d(){try{const n=JSON.parse(e.readFileSync(SCHEDULE_FILE,"utf8"));return Array.isArray(n)?n:[]}catch{return[]}}function c(t){e.mkdirSync(n.dirname(SCHEDULE_FILE),{recursive:!0}),e.writeFileSync(SCHEDULE_FILE,JSON.stringify(t,null,2)+"\n",{mode:384})}function a(e){const n={...e};if(n.freq&&"object"==typeof n.freq){const e=r(n.freq);e&&(n.cron=e)}return void 0===n.enabled&&(n.enabled=!0),null==n.permission&&(n.permission="default"),null==n.model&&(n.model="auto"),"watch"===n.kind?(n.watch={source:"deps",deliver:"dry",...n.watch||{}},["dry","notify","webhook","channel"].includes(n.watch.deliver)||(n.watch.deliver="dry"),"webhook"!==n.watch.deliver||String(n.watch.webhookUrl||"").trim()||(n.watch.deliver="dry"),"channel"!==n.watch.deliver||n.watch.channelId||(n.watch.deliver="dry")):null==n.kind&&(n.kind="action"),n}export function listTasks(){return d()}export function countActiveTasks(e=d()){return e.filter(e=>e&&!(e.freq&&"once"===e.freq.kind&&e.lastRunAt)).length}export function getTask(e){return d().find(n=>n.id===e)||null}export function addTask(e){const n=d(),t=e&&e.id||Date.now().toString(36)+Math.random().toString(36).slice(2,4),r=Date.now(),o=a({...e,id:t,created:e&&e.created||r,updated:r});if(!o.cron&&!o.freq)throw new Error("缺少执行频率(freq)或cron表达式");return i("scheduled_tasks",countActiveTasks(n)),n.push(o),c(n),{id:t}}export function updateTask(e,n){const t=d(),r=t.findIndex(n=>n.id===e);return!(r<0)&&(function(e){return!e||Object.keys(e).every(n=>"enabled"===n&&!0!==e.enabled)}(n)||o("agent_tasks"),t[r]=a({...t[r],...n,id:e,updated:Date.now()}),c(t),!0)}export function removeTask(e){const n=d(),t=n.filter(n=>n.id!==e);return c(t),t.length<n.length}export function setEnabled(e,n){return updateTask(e,{enabled:!!n})}export function touchRun(e,n=Date.now()){const t=d(),r=t.findIndex(n=>n.id===e);return!(r<0)&&(t[r].lastRunAt=n,c(t),!0)}export function crontabLine(e,r="huicai"){return`${e.cron} cd ${JSON.stringify(e.cwd)} && ${r} -p ${JSON.stringify(e.prompt)} --yolo --plain >> ${JSON.stringify(n.join(t.homedir(),".huicai","schedule-"+e.id+".log"))} 2>&1`}
1
+ import e from"node:fs";import r from"node:path";import t from"node:os";import n from"node:crypto";import{accountDataDir as o,requireAccount as c,accountScope as i,assertAccountPath as s}from"./account-scope.js";import{CONFIG_DIR as d,loadConfig as a}from"./config.js";import{freqToCron as u}from"./schedule-freq.js";import{assertFeature as l,assertLimit as f}from"./plan-limits.js";export const SCHEDULE_FILE=r.join(d,"schedule.json");export const scheduleFile=()=>r.join(o(),"automations.json");export function readScheduleState(r=scheduleFile()){try{const t=JSON.parse(e.readFileSync(r,"utf8"));if(!Array.isArray(t.tasks))throw new Error("自动化任务存档格式错误");return t}catch(e){if("ENOENT"===e.code)return{tasks:[],revision:0,generation:0,dirty:!1};throw e}}export function editScheduleState(t,o=scheduleFile()){e.mkdirSync(r.dirname(o),{recursive:!0,mode:448});const c=o+".lock";let i;try{i=e.openSync(c,"wx",384)}catch(r){if("EEXIST"!==r.code)throw r;try{const r=Number(e.readFileSync(c,"utf8"));if(Number.isInteger(r)&&r>0)try{process.kill(r,0)}catch(r){"ESRCH"===r.code&&e.unlinkSync(c)}}catch{}throw new Error("自动化任务正在被其它进程更新,请重试")}e.writeFileSync(i,String(process.pid));const s=o+"."+n.randomUUID()+".tmp";try{const r=readScheduleState(o),n=t(r);return e.writeFileSync(s,JSON.stringify(r,null,2)+"\n",{mode:384}),e.renameSync(s,o),n}finally{e.closeSync(i),e.rmSync(c,{force:!0}),e.rmSync(s,{force:!0})}}function h(e){const r=c();return editScheduleState(t=>{const n=e(t.tasks,r);return t.generation++,t.dirty=!0,n})}function p(e){const r={...e};if(r.cwd&&s(r.cwd),r.freq&&"object"==typeof r.freq){const e=u(r.freq);e&&(r.cron=e)}return void 0===r.enabled&&(r.enabled=!0),null==r.permission&&(r.permission="default"),null==r.model&&(r.model="auto"),"watch"===r.kind?(r.watch={source:"deps",deliver:"dry",...r.watch||{}},["dry","notify","webhook","channel"].includes(r.watch.deliver)||(r.watch.deliver="dry"),"webhook"!==r.watch.deliver||String(r.watch.webhookUrl||"").trim()||(r.watch.deliver="dry"),"channel"!==r.watch.deliver||r.watch.channelId||(r.watch.deliver="dry")):null==r.kind&&(r.kind="action"),r}export function listTasks(){return i()?readScheduleState().tasks:[]}export function countActiveTasks(e=listTasks()){return e.filter(e=>e&&!(e.freq&&"once"===e.freq.kind&&e.lastRunAt)).length}export function getTask(e){return listTasks().find(r=>r.id===e)||null}export function addTask(e){return h((r,t)=>{const o=n.randomUUID(),c=Date.now(),i=p({...e,id:o,owner:t,user:String(a().user.id??a().user.userId??a().user.username),created:c,updated:c});if(!i.cron&&!i.freq)throw new Error("缺少执行频率(freq)或cron表达式");return f("scheduled_tasks",countActiveTasks(r)),r.push(i),{id:o}})}export function updateTask(e,r){return h(t=>{const n=t.findIndex(r=>r.id===e);if(n<0)return!1;(function(e){return!e||Object.keys(e).every(r=>"enabled"===r&&!0!==e.enabled)})(r)||l("agent_tasks");const{owner:o,user:c,created:i}=t[n];return t[n]=p({...t[n],...r,id:e,owner:o,user:c,created:i,updated:Date.now()}),!0})}export function removeTask(e){return h(r=>{const t=r.findIndex(r=>r.id===e);return!(t<0)&&(r.splice(t,1),!0)})}export function setEnabled(e,r){return updateTask(e,{enabled:!!r})}export function touchRun(e,r=Date.now()){return h(t=>{const n=t.find(r=>r.id===e);return!!n&&(n.lastRunAt=r,!0)})}export function crontabLine(e,r="huicai"){const t=getTask(e.id);if(!t||t.owner!==c())throw new Error("任务不属于当前登录用户");const n=e=>"'"+String(e).replace(/'/g,"'\"'\"'")+"'";return`${t.cron} ${n(r)} run-scheduled ${n(t.id)} ${n(t.owner)}`}
package/src/session.js CHANGED
@@ -1 +1 @@
1
- import t from"node:fs";import e from"node:path";import{CONFIG_DIR as n}from"./config.js";import{dropSessionSpill as r}from"./spill.js";import{dropSessionJobs as o}from"./jobs.js";import{dropSessionObservations as i}from"./fs-observe.js";import{dropSessionTerminals as s}from"./terminal.js";import{dropSessionLog as c}from"./sessionlog.js";export const SESSIONS_DIR=e.join(n,"sessions");function l(){t.mkdirSync(SESSIONS_DIR,{recursive:!0})}function a(t){return e.join(SESSIONS_DIR,t+".json")}export function userKeyOf(t){const e=t?.user;if(!e||"object"!=typeof e)return"";const n=e.id??e.userId??e.username??"";return null==n?"":String(n)}export function turnCount(t){return(t||[]).filter(t=>"user"===t.role).length}export function createSession({cwd:t,model:e,messages:n,parentSession:r,origin:o,delegationDepth:i,label:s,user:c}){const l=Date.now(),a={id:Date.now().toString(36)+"-"+Math.random().toString(36).slice(2,6),cwd:t,model:e,createdAt:l,updatedAt:l,title:"",messages:n||[],compaction:{}};return r&&(a.parentSession=String(r)),o&&(a.origin=String(o)),Number.isFinite(i)&&(a.delegationDepth=Math.max(0,Math.floor(i))),s&&(a.label=String(s).replace(/\s+/g," ").slice(0,120)),c&&(a.user=String(c)),a}export function saveSession(e){if(e&&e.id)try{l(),e.updatedAt=Date.now(),t.writeFileSync(a(e.id),JSON.stringify(e),{mode:384})}catch(t){process.stderr.write(`(会话保存失败,不影响对话:${t.message})\n`)}}export function loadSession(e){if(!e)return null;try{const n=JSON.parse(t.readFileSync(a(e),"utf8"));return n&&Array.isArray(n.messages)?(n.id||(n.id=e),n.compaction&&"object"==typeof n.compaction||(n.compaction={}),n):null}catch{return null}}export function listSessions(n){let r;try{l(),r=t.readdirSync(SESSIONS_DIR).filter(t=>t.endsWith(".json"))}catch{return[]}const o=[];for(const i of r)try{const r=JSON.parse(t.readFileSync(e.join(SESSIONS_DIR,i),"utf8"));if(n&&r.cwd!==n)continue;o.push({id:r.id||i.replace(/\.json$/,""),title:r.title||"(未命名会话)",cwd:r.cwd,model:r.model,updatedAt:r.updatedAt||0,turns:turnCount(r.messages),user:r.user?String(r.user):"",origin:r.origin||"",parentSession:r.parentSession||"",delegationDepth:Number.isFinite(r.delegationDepth)?r.delegationDepth:0,label:r.label||""})}catch{}return o.sort((t,e)=>e.updatedAt-t.updatedAt),o}export function mostRecent(t){const e=listSessions(t);return e.length?e[0].id:null}export function deleteSession(e,n){const l=n||new Set;if(l.has(e))return!1;l.add(e);for(const t of listChildSessions(e))deleteSession(t.id,l);let u=!1;try{t.unlinkSync(a(e)),u=!0}catch{u=!1}return r(e),o(e),s(e),c(e),i(e),u}export function listChildSessions(t){return t?listSessions(null).filter(e=>e.parentSession===String(t)):[]}export function relTime(t){if(!t)return"未知时间";const e=Math.max(0,Math.floor((Date.now()-t)/1e3));if(e<60)return e+" 秒前";const n=Math.floor(e/60);if(n<60)return n+" 分钟前";const r=Math.floor(n/60);if(r<24)return r+" 小时前";return Math.floor(r/24)+" 天前"}
1
+ import t from"node:fs";import e from"node:path";import{randomUUID as n}from"node:crypto";import{accountScope as r}from"./account-scope.js";import{CONFIG_DIR as o}from"./config.js";import{dropSessionSpill as i}from"./spill.js";import{dropSessionJobs as s}from"./jobs.js";import{dropSessionObservations as c}from"./fs-observe.js";import{dropSessionTerminals as l}from"./terminal.js";import{dropSessionLog as a}from"./sessionlog.js";export const SESSIONS_DIR=e.join(o,"sessions");function u(){t.mkdirSync(SESSIONS_DIR,{recursive:!0})}function f(t){if(!/^[\w-]{1,100}$/.test(String(t||"")))throw new Error("非法会话编号");return e.join(SESSIONS_DIR,t+".json")}export function userKeyOf(t){const e=t?.user;if(!e||"object"!=typeof e)return"";const n=e.id??e.userId??e.username??"";return null==n?"":String(n)}export function turnCount(t){return(t||[]).filter(t=>"user"===t.role).length}export function createSession({cwd:t,model:e,messages:n,parentSession:o,origin:i,delegationDepth:s,label:c,user:l}){const a=Date.now(),u={id:Date.now().toString(36)+"-"+Math.random().toString(36).slice(2,6),owner:r(),cwd:t,model:e,createdAt:a,updatedAt:a,title:"",messages:n||[],compaction:{}};return o&&(u.parentSession=String(o)),i&&(u.origin=String(i)),Number.isFinite(s)&&(u.delegationDepth=Math.max(0,Math.floor(s))),c&&(u.label=String(c).replace(/\s+/g," ").slice(0,120)),l&&(u.user=String(l)),u}export function saveSession(e){if(!e||!e.id)return;if((e.owner||"")!==r())return;let o;try{u(),e.updatedAt=Date.now();const r=JSON.stringify(e),i=f(e.id);o=i+"."+n()+".tmp",t.writeFileSync(o,r,{mode:384,flag:"wx"}),t.renameSync(o,i)}catch(t){process.stderr.write(`(会话保存失败,不影响对话:${t.message})\n`)}finally{if(o)try{t.unlinkSync(o)}catch{}}}export function loadSession(e){if(!e)return null;try{const n=JSON.parse(t.readFileSync(f(e),"utf8"));return n&&Array.isArray(n.messages)&&(n.owner||"")===r()?(n.id||(n.id=e),n.compaction&&"object"==typeof n.compaction||(n.compaction={}),n):null}catch{return null}}export function listSessions(n){let o;try{u(),o=t.readdirSync(SESSIONS_DIR).filter(t=>t.endsWith(".json"))}catch{return[]}const i=[];for(const s of o)try{const o=JSON.parse(t.readFileSync(e.join(SESSIONS_DIR,s),"utf8"));if((o.owner||"")!==r())continue;if(n&&o.cwd!==n)continue;i.push({id:o.id||s.replace(/\.json$/,""),title:o.title||"(未命名会话)",cwd:o.cwd,model:o.model,updatedAt:o.updatedAt||0,turns:turnCount(o.messages),user:o.user?String(o.user):"",owner:o.owner||"",origin:o.origin||"",parentSession:o.parentSession||"",delegationDepth:Number.isFinite(o.delegationDepth)?o.delegationDepth:0,label:o.label||""})}catch{}return i.sort((t,e)=>e.updatedAt-t.updatedAt),i}export function mostRecent(t){const e=listSessions(t);return e.length?e[0].id:null}export function deleteSession(e,n){let r;try{r=f(e)}catch{return!1}if(t.existsSync(r)&&!loadSession(e))return!1;const o=n||new Set;if(o.has(e))return!1;o.add(e);for(const t of listChildSessions(e))deleteSession(t.id,o);let u=!1;try{t.unlinkSync(f(e)),u=!0}catch{u=!1}return i(e),s(e),l(e),a(e),c(e),u}export function listChildSessions(t){return t?listSessions(null).filter(e=>e.parentSession===String(t)):[]}export function relTime(t){if(!t)return"未知时间";const e=Math.max(0,Math.floor((Date.now()-t)/1e3));if(e<60)return e+" 秒前";const n=Math.floor(e/60);if(n<60)return n+" 分钟前";const r=Math.floor(n/60);if(r<24)return r+" 小时前";return Math.floor(r/24)+" 天前"}
@@ -1 +1 @@
1
- import e from"node:fs";import t from"node:path";import n from"node:zlib";import{CONFIG_DIR as o}from"./config.js";import{SESSIONS_DIR as r,userKeyOf as s}from"./session.js";import{tokenize as i}from"./codeindex.js";const c=t.join(o,"cache","sessions-index.json.gz");let l=null;function u(e){if(!e||"system"===e.role)return"";const t=(n=e.content,"string"==typeof n?n:Array.isArray(n)?n.filter(e=>"text"===e?.type).map(e=>e.text||"").join("\n"):"").trim();var n;return t?t.slice(0,"tool"===e.role?1e3:4e3):""}function a(e){const t=new Map;for(const n of i(e))t.set(n,(t.get(n)||0)+1);return t}function f(n){let o;try{o=JSON.parse(e.readFileSync(n,"utf8"))}catch{return null}if(!o||!Array.isArray(o.messages))return null;const r=o.messages.slice(-300),s=o.messages.length-r.length,i=[];for(let e=0;e<r.length;e++){const t=u(r[e]);if(!t)continue;const n=a(t);i.push({i:s+e,role:r[e].role,text:t,tf:[...n],len:t.length})}return i.length?{meta:{id:o.id||t.basename(n,".json"),title:o.title||"",cwd:o.cwd||"",model:o.model||"",updatedAt:o.updatedAt||0,user:o.user?String(o.user):"",origin:o.origin||""},docs:i}:null}export function buildIndex(){const o=function(){try{const t=JSON.parse(n.gunzipSync(e.readFileSync(c)).toString("utf8"));return t&&1===t.v&&t.files&&"object"==typeof t.files?t.files:null}catch{return null}}()||Object.create(null),s=Object.create(null),i=Date.now();let l=[];try{l=e.readdirSync(r).filter(e=>e.endsWith(".json")).map(n=>{const o=t.join(r,n);try{const t=e.statSync(o);return{full:o,key:n,mtimeMs:t.mtimeMs,size:t.size}}catch{return null}}).filter(Boolean).sort((e,t)=>t.mtimeMs-e.mtimeMs).slice(0,800)}catch{l=[]}for(const e of l){const t=o[e.key];if(t&&t.mtimeMs===e.mtimeMs&&t.size===e.size&&Array.isArray(t.docs)){s[e.key]=t;continue}if(Date.now()-i>3e3){t&&(s[e.key]=t);continue}const n=f(e.full);n&&(s[e.key]={mtimeMs:e.mtimeMs,size:e.size,meta:n.meta,docs:n.docs})}!function(o){try{e.mkdirSync(t.dirname(c),{recursive:!0});const r=c+"."+process.pid+".tmp";e.writeFileSync(r,n.gzipSync(JSON.stringify({v:1,at:Date.now(),files:o})),{mode:384}),e.renameSync(r,c)}catch{}}(s);const u=[],a=new Map;let d=0;for(const e of Object.keys(s)){const t=s[e];for(const e of t.docs){const n=new Map(e.tf);u.push({meta:t.meta,i:e.i,role:e.role,text:e.text,tf:n,len:e.len}),d+=e.len;for(const e of n.keys())a.set(e,(a.get(e)||0)+1)}}return{entries:u,df:a,N:u.length,avgLen:u.length?d/u.length:1}}export function getIndex({force:t=!1}={}){const n=function(){try{return String(e.statSync(r).mtimeMs)}catch{return"none"}}();if(!t&&l&&l.stamp===n&&Date.now()-l.builtAt<3e5)return l.index;const o=buildIndex();return l={index:o,builtAt:Date.now(),stamp:n},o}export function clearIndex(){l=null;try{e.rmSync(c,{force:!0})}catch{}}function d(e,t){const n=e.toLowerCase();let o=-1;for(const e of t){const t=n.indexOf(e);t>=0&&(o<0||t<o)&&(o=t)}o<0&&(o=0);const r=Math.max(0,o-90),s=Math.min(e.length,o+90);return(r>0?"…":"")+e.slice(r,s).replace(/\s+/g," ").trim()+(s<e.length?"…":"")}export function searchSessions(e,t={}){const n=[...new Set(i(e||""))];if(!n.length)return[];const o=t.index||getIndex();if(!o.N)return[];const{df:r,N:s,avgLen:c}=o,l=new Map;for(const e of n){const t=r.get(e)||1;l.set(e,Math.log(1+(s-t+.5)/(t+.5)))}const u=void 0===t.userKey?null:String(t.userKey||""),a=!!t.includeUnowned,f=!!t.includeSubagents,m=new Map,p=new Set;for(const e of o.entries){if(null!==u){const t=e.meta.user||"";if(t!==u&&(!a||""!==t)){""===t&&u&&p.add(e.sessionId||e.meta.id||e.id);continue}}if(!f&&"subagent"===e.meta.origin)continue;if(t.cwd&&e.meta.cwd!==t.cwd)continue;let o=0;for(const t of n){const n=e.tf.get(t);n&&(o+=l.get(t)*(2.5*n/(n+1.5*(.25+e.len/c*.75))))}if(o<=0)continue;const r=new Set(i(e.meta.title));for(const e of n)r.has(e)&&(o+=.8*l.get(e));const s=m.get(e.meta.id);(!s||o>s.score)&&m.set(e.meta.id,{id:e.meta.id,title:e.meta.title,cwd:e.meta.cwd,model:e.meta.model,updatedAt:e.meta.updatedAt,role:e.role,i:e.i,score:o,snippet:d(e.text,n)})}const h=Number.isFinite(t.k)&&t.k>0?Math.min(t.k,50):10,g=[...m.values()].sort((e,t)=>t.score-e.score).slice(0,h);return Object.defineProperty(g,"unownedFiltered",{value:p.size,enumerable:!1}),g}function m(e){if(!e)return"未知时间";const t=Math.max(0,Math.floor((Date.now()-e)/1e3));if(t<60)return t+" 秒前";const n=Math.floor(t/60);if(n<60)return n+" 分钟前";const o=Math.floor(n/60);return o<24?o+" 小时前":Math.floor(o/24)+" 天前"}export function formatResults(e,t){if(!e.length){const n=e.unownedFiltered||0;return`历史会话中没有匹配「${t}」的内容。`+(n?`\n(另有 ${n} 次**登录之前留下的**旧会话命中了,因不带用户标记而未列出。用 \`/history `+t+" --legacy` 可一并查看。)":"")}const n=[`历史会话中匹配「${t}」的 ${e.length} 次对话(按相关性):`,""];for(const t of e)n.push(`· ${t.title||"(未命名会话)"} [${t.id}] ${m(t.updatedAt)}${t.cwd?" "+t.cwd:""}`),n.push(` ${"user"===t.role?"用户":"tool"===t.role?"工具":"助手"}:${t.snippet}`);return n.push("","用 `huicai --resume <会话id>` 可继续该对话。"),n.join("\n")}export async function searchHistory(e,t,n){const o=String(t?.query||"").trim();if(!o)return"缺少query(要在历史对话里找什么)";const r=searchSessions(o,{k:Number.isFinite(t?.k)&&t.k>0?Math.min(t.k,20):8,userKey:s(n||{}),cwd:t?.this_project?e:void 0,includeUnowned:!!t?.include_unowned}),i=formatResults(r,o);return r.length||!r.unownedFiltered||t?.include_unowned?i:i+`\n(你可以带include_unowned=true重新调用search_history把这 ${r.unownedFiltered} 次旧会话一并检索。)`}
1
+ import e from"node:fs";import{accountScope as t}from"./account-scope.js";import n from"node:path";import o from"node:zlib";import{CONFIG_DIR as r}from"./config.js";import{SESSIONS_DIR as i,userKeyOf as s}from"./session.js";import{tokenize as c}from"./codeindex.js";const l=n.join(r,"cache","sessions-index.json.gz");let u=null;function a(e){if(!e||"system"===e.role)return"";const t=(n=e.content,"string"==typeof n?n:Array.isArray(n)?n.filter(e=>"text"===e?.type).map(e=>e.text||"").join("\n"):"").trim();var n;return t?t.slice(0,"tool"===e.role?1e3:4e3):""}function f(e){const t=new Map;for(const n of c(e))t.set(n,(t.get(n)||0)+1);return t}function d(t){let o;try{o=JSON.parse(e.readFileSync(t,"utf8"))}catch{return null}if(!o||!Array.isArray(o.messages))return null;const r=o.messages.slice(-300),i=o.messages.length-r.length,s=[];for(let e=0;e<r.length;e++){const t=a(r[e]);if(!t)continue;const n=f(t);s.push({i:i+e,role:r[e].role,text:t,tf:[...n],len:t.length})}return s.length?{meta:{id:o.id||n.basename(t,".json"),title:o.title||"",cwd:o.cwd||"",model:o.model||"",updatedAt:o.updatedAt||0,user:o.user?String(o.user):"",owner:o.owner||"",origin:o.origin||""},docs:s}:null}export function buildIndex(){const t=function(){try{const t=JSON.parse(o.gunzipSync(e.readFileSync(l)).toString("utf8"));return t&&2===t.v&&t.files&&"object"==typeof t.files?t.files:null}catch{return null}}()||Object.create(null),r=Object.create(null),s=Date.now();let c=[];try{c=e.readdirSync(i).filter(e=>e.endsWith(".json")).map(t=>{const o=n.join(i,t);try{const n=e.statSync(o);return{full:o,key:t,mtimeMs:n.mtimeMs,size:n.size}}catch{return null}}).filter(Boolean).sort((e,t)=>t.mtimeMs-e.mtimeMs).slice(0,800)}catch{c=[]}for(const e of c){const n=t[e.key];if(n&&n.mtimeMs===e.mtimeMs&&n.size===e.size&&Array.isArray(n.docs)){r[e.key]=n;continue}if(Date.now()-s>3e3){n&&(r[e.key]=n);continue}const o=d(e.full);o&&(r[e.key]={mtimeMs:e.mtimeMs,size:e.size,meta:o.meta,docs:o.docs})}!function(t){try{e.mkdirSync(n.dirname(l),{recursive:!0});const r=l+"."+process.pid+".tmp";e.writeFileSync(r,o.gzipSync(JSON.stringify({v:2,at:Date.now(),files:t})),{mode:384}),e.renameSync(r,l)}catch{}}(r);const u=[],a=new Map;let f=0;for(const e of Object.keys(r)){const t=r[e];for(const e of t.docs){const n=new Map(e.tf);u.push({meta:t.meta,i:e.i,role:e.role,text:e.text,tf:n,len:e.len}),f+=e.len;for(const e of n.keys())a.set(e,(a.get(e)||0)+1)}}return{entries:u,df:a,N:u.length,avgLen:u.length?f/u.length:1}}export function getIndex({force:t=!1}={}){const n=function(){try{return String(e.statSync(i).mtimeMs)}catch{return"none"}}();if(!t&&u&&u.stamp===n&&Date.now()-u.builtAt<3e5)return u.index;const o=buildIndex();return u={index:o,builtAt:Date.now(),stamp:n},o}export function clearIndex(){u=null;try{e.rmSync(l,{force:!0})}catch{}}function m(e,t){const n=e.toLowerCase();let o=-1;for(const e of t){const t=n.indexOf(e);t>=0&&(o<0||t<o)&&(o=t)}o<0&&(o=0);const r=Math.max(0,o-90),i=Math.min(e.length,o+90);return(r>0?"…":"")+e.slice(r,i).replace(/\s+/g," ").trim()+(i<e.length?"…":"")}export function searchSessions(e,n={}){const o=[...new Set(c(e||""))];if(!o.length)return[];const r=n.index||getIndex();if(!r.N)return[];const{df:i,N:s,avgLen:l}=r,u=new Map;for(const e of o){const t=i.get(e)||1;u.set(e,Math.log(1+(s-t+.5)/(t+.5)))}const a=void 0===n.userKey?null:String(n.userKey||""),f=!!n.includeUnowned,d=!!n.includeSubagents,p=new Map,h=new Set;for(const e of r.entries){if((e.meta.owner||"")!==t())continue;if(null!==a){const t=e.meta.user||"";if(t!==a&&(!f||""!==t)){""===t&&a&&h.add(e.sessionId||e.meta.id||e.id);continue}}if(!d&&"subagent"===e.meta.origin)continue;if(n.cwd&&e.meta.cwd!==n.cwd)continue;let r=0;for(const t of o){const n=e.tf.get(t);n&&(r+=u.get(t)*(2.5*n/(n+1.5*(.25+e.len/l*.75))))}if(r<=0)continue;const i=new Set(c(e.meta.title));for(const e of o)i.has(e)&&(r+=.8*u.get(e));const s=p.get(e.meta.id);(!s||r>s.score)&&p.set(e.meta.id,{id:e.meta.id,title:e.meta.title,cwd:e.meta.cwd,model:e.meta.model,updatedAt:e.meta.updatedAt,role:e.role,i:e.i,score:r,snippet:m(e.text,o)})}const g=Number.isFinite(n.k)&&n.k>0?Math.min(n.k,50):10,y=[...p.values()].sort((e,t)=>t.score-e.score).slice(0,g);return Object.defineProperty(y,"unownedFiltered",{value:h.size,enumerable:!1}),y}function p(e){if(!e)return"未知时间";const t=Math.max(0,Math.floor((Date.now()-e)/1e3));if(t<60)return t+" 秒前";const n=Math.floor(t/60);if(n<60)return n+" 分钟前";const o=Math.floor(n/60);return o<24?o+" 小时前":Math.floor(o/24)+" 天前"}export function formatResults(e,t){if(!e.length){const n=e.unownedFiltered||0;return`历史会话中没有匹配「${t}」的内容。`+(n?`\n(另有 ${n} 次**登录之前留下的**旧会话命中了,因不带用户标记而未列出。用 \`/history `+t+" --legacy` 可一并查看。)":"")}const n=[`历史会话中匹配「${t}」的 ${e.length} 次对话(按相关性):`,""];for(const t of e)n.push(`· ${t.title||"(未命名会话)"} [${t.id}] ${p(t.updatedAt)}${t.cwd?" "+t.cwd:""}`),n.push(` ${"user"===t.role?"用户":"tool"===t.role?"工具":"助手"}:${t.snippet}`);return n.push("","用 `huicai --resume <会话id>` 可继续该对话。"),n.join("\n")}export async function searchHistory(e,t,n){const o=String(t?.query||"").trim();if(!o)return"缺少query(要在历史对话里找什么)";const r=searchSessions(o,{k:Number.isFinite(t?.k)&&t.k>0?Math.min(t.k,20):8,userKey:s(n||{}),cwd:t?.this_project?e:void 0,includeUnowned:!!t?.include_unowned}),i=formatResults(r,o);return r.length||!r.unownedFiltered||t?.include_unowned?i:i+`\n(你可以带include_unowned=true重新调用search_history把这 ${r.unownedFiltered} 次旧会话一并检索。)`}
package/src/skills.js CHANGED
@@ -1 +1 @@
1
- import e from"node:fs";import n from"node:path";import i from"node:os";import{fileURLToPath as t}from"node:url";import{scanSkillPackage as r}from"./skill-scan.js";import{assertLimit as o,isPlanLimitError as l,refreshPlan as s}from"./plan-limits.js";export const BUNDLED_SKILLS_DIR=n.join(n.dirname(t(import.meta.url)),"..","skills");const c=new Set([".md",".markdown",".txt",".json",".yaml",".yml",".py",".js",".mjs",".cjs",".ts",".sh",".sql",".csv",".html",".css",".xml",".toml"]),a=262144;export const SKILLS_STATE_PATH=n.join(i.homedir(),".huicai","skills-state.json");export function loadDisabledSet(){try{const n=JSON.parse(e.readFileSync(SKILLS_STATE_PATH,"utf8"));return new Set(Array.isArray(n?.disabled)?n.disabled.map(String):[])}catch{return new Set}}function d(i){e.mkdirSync(n.dirname(SKILLS_STATE_PATH),{recursive:!0}),e.writeFileSync(SKILLS_STATE_PATH,JSON.stringify({disabled:[...i].sort()},null,2)+"\n",{mode:384})}export function setSkillEnabled(e,n){const i=String(e||"").trim();if(!i)throw new Error("技能名为空");const t=loadDisabledSet();return n?t.delete(i):t.add(i),d(t),[...t]}export const DEFAULT_INVOCATION=Object.freeze({modelInvocable:!0,userInvocable:!0});function u(e){const n=String(e).trim().toLowerCase().replace(/^["']|["']$/g,"");return!!["true","yes","on","1"].includes(n)||!["false","no","off","0"].includes(n)&&void 0}export function parseFrontmatter(e){const n={name:"",description:"",version:"",body:(e||"").trim(),invocation:{...DEFAULT_INVOCATION},invocationError:""},i=/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(n.body);if(!i)return n;n.body=n.body.slice(i[0].length).trim();for(const e of i[1].split(/\r?\n/)){const i=e.trim(),t=/^(name|description|version)\s*:\s*(.*)$/.exec(i);if(t){n[t[1]]=t[2].trim().replace(/^["']|["']$/g,"");continue}const r=/^(disable-model-invocation|user-invocable|disableModelInvocation|userInvocable)\s*:\s*(.*)$/.exec(i);if(!r)continue;const[,o,l]=r;if("disableModelInvocation"===o||"userInvocable"===o){n.invocationError=`调用面字段必须用连字符写法(${"userInvocable"===o?"user-invocable":"disable-model-invocation"}),不认camelCase`;continue}const s=u(l);void 0!==s?"disable-model-invocation"===o?n.invocation.modelInvocable=!s:n.invocation.userInvocable=s:n.invocationError=`${o} 的值不是布尔:${l.trim()}`}return n}export function skillDirs(e){const t=[];return e&&t.push(n.join(e,".huicai","skills")),t.push(n.join(i.homedir(),".huicai","skills")),t}const f=new Set(["trigger-cases.json","evals.json"]);function m(i,t="",r=0,o=[]){if(r>3||o.length>=50)return o;let l;try{l=e.readdirSync(i,{withFileTypes:!0})}catch{return o}for(const e of l){if(o.length>=50)break;if(e.name.startsWith("."))continue;const l=t?`${t}/${e.name}`:e.name;e.isDirectory()?m(n.join(i,e.name),l,r+1,o):e.isFile()&&"SKILL.md"!==l&&!f.has(l)&&c.has(n.extname(e.name).toLowerCase())&&o.push(l)}return o}let p=[];export function skillLoadWarnings(){return p.slice()}export function loadLocalSkills(t){const r=new Map,o=[],l=loadDisabledSet(),s=[...t?[{dir:n.join(t,".huicai","skills"),scope:"project"}]:[],{dir:n.join(i.homedir(),".huicai","skills"),scope:"global"},{dir:BUNDLED_SKILLS_DIR,scope:"builtin"}];for(const{dir:i,scope:t}of s){let s;try{s=e.readdirSync(i,{withFileTypes:!0})}catch{continue}for(const c of s){if(!c.isDirectory())continue;const s=n.join(i,c.name);let a;try{a=e.readFileSync(n.join(s,"SKILL.md"),"utf8")}catch{continue}const d=parseFrontmatter(a),u=(d.name||c.name).trim();u&&!r.has(u)&&(d.invocationError?o.push(`技能 ${u}(${s})调用面元数据非法,已整条跳过:${d.invocationError}`):r.set(u,{name:u,description:d.description||"",version:d.version||"",dir:s,scope:t,files:m(s),enabled:!l.has(u),invocation:d.invocation}))}}return p=o,[...r.values()]}export function isModelInvocable(e){return!1!==e?.enabled&&(e?.invocation?.modelInvocable??!0)}export function isUserInvocable(e){return!1!==e?.enabled&&(e?.invocation?.userInvocable??!0)}export function readSkillBody(i,t){const r=loadLocalSkills(i).find(e=>e.name===t);if(!r)return null;try{const i=e.readFileSync(n.join(r.dir,"SKILL.md"),"utf8");return{skill:r,raw:i.length>65536?i.slice(0,65536)+"\n…(正文已截断)":i}}catch{return null}}export function readSkillFileContent(i,t,r){const o=loadLocalSkills(i).find(e=>e.name===t);if(!o)throw new Error(`本地技能不存在:${t}`);const l=String(r||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,""),s=n.resolve(o.dir,l);if(!s.startsWith(n.resolve(o.dir)+n.sep))throw new Error(`路径越界:${r}`);if(e.statSync(s).size>a)throw new Error(`附件过大(>256KB):${l}`);return e.readFileSync(s,"utf8")}export const CATALOG_DESC_CAP=120;export function compactDesc(e,n){let i=String(n||"(无描述)").replace(/\s+/g," ").trim();const t=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");if(i=i.replace(new RegExp("^"+t+"\\s*[::—–-]\\s*"),""),i.length<=120)return i;const r=i.slice(0,120),o=Math.max(r.lastIndexOf("。"),r.lastIndexOf(";"),r.lastIndexOf(","),r.lastIndexOf(". "));return(o>72?r.slice(0,o+1):r)+"…"}export function renderSkillContent(e,n){let i=`<skill_content name="${t=e.name,String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}">\n`;var t;return i+="<skill_resources>\n",e.files&&e.files.length?i+=`本技能的资源基址:${e.dir}\n其中 ${e.files.length} 个可读资源文件,需要时用read_skill_file(skill="${e.name}", path=...) 按相对路径读取;正文里提到的相对路径都按该基址解析。用到再读,不要预先全部读入。\n`:i+="本技能不带附加资源文件。\n",i+="</skill_resources>\n\n<skill_instructions>\n",i+=String(n||"").trim()+"\n",i+="</skill_instructions>\n</skill_content>",i}export function makeSkillTools(e){const n=loadLocalSkills(e).filter(isModelInvocable);if(!n.length)return[];const i=[{readOnly:!0,spec:{type:"function",function:{name:"load_skill",description:"载入一个本地技能的操作指引(系统提示词「可用技能」目录中的名称)。载入后严格遵循其流程执行。",parameters:{type:"object",properties:{name:{type:"string",description:"技能名(见「可用技能」目录)"}},required:["name"]}}},run:async n=>{const i=String(n?.name||"").trim(),t=readSkillBody(e,i);if(!t){return`本地技能不存在:${i}(可用:${loadLocalSkills(e).filter(isModelInvocable).map(e=>e.name).join(", ")||"无"})`}return!1===t.skill.enabled?`技能「${i}」已被禁用,无法载入(可在「技能·MCP」中重新启用)。`:isModelInvocable(t.skill)?renderSkillContent(t.skill,t.raw):`技能「${i}」标记为不可由模型自行载入(disable-model-invocation)。如确需它,请让用户在输入里直接敲 /${i}。`}}];return n.some(e=>e.files.length)&&i.push({readOnly:!0,spec:{type:"function",function:{name:"read_skill_file",description:"读取本地技能附带的资源文件(先load_skill查看其文件清单,再按path读取)。",parameters:{type:"object",properties:{skill:{type:"string",description:"技能名"},path:{type:"string",description:"资源文件相对路径(技能载入结果中列出)"}},required:["skill","path"]}}},run:async n=>{try{const i=readSkillFileContent(e,String(n?.skill||"").trim(),n?.path);return`文件 ${n?.path}:\n${i}`}catch(e){return`读取失败:${e.message}`}}}),i}export function writeSkillToDir(i,t){const r=String(t?.name||"").trim();if(!r||!t?.content)throw new Error("技能数据不完整(缺name或content)");const o=n.join(i,r.replace(/[^a-zA-Z0-9_-]/g,"-"));e.existsSync(n.join(o,"SKILL.md"))||h(i),e.mkdirSync(o,{recursive:!0}),e.writeFileSync(n.join(o,"SKILL.md"),t.content,"utf8");for(const i of t.files||[]){const t=String(i?.path||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,"");if(!t||t.includes(".."))continue;const r=n.join(o,t);e.mkdirSync(n.dirname(r),{recursive:!0}),e.writeFileSync(r,i.content??"","utf8")}return o}export function isValidSkillName(e){return/^[a-zA-Z0-9_-]{1,48}$/.test(String(e||""))}const S=()=>n.join(i.homedir(),".huicai","skills");function k(i){let t=0;try{for(const r of e.readdirSync(i,{withFileTypes:!0}))r.isDirectory()&&e.existsSync(n.join(i,r.name,"SKILL.md"))&&t++}catch{}return t}export function countUserSkills(e=null){return k(S())+(e?k(n.join(e,".huicai","skills")):0)}function h(e){const i=n.resolve(S()),t=n.resolve(e);o("skills",k(i)+(t===i?0:k(t)))}function y(e,t){if("project"===e){if(!t)throw new Error("项目级技能需要在会话/项目内创建");return n.join(t,".huicai","skills")}return n.join(i.homedir(),".huicai","skills")}export function scaffoldSkillMarkdown(e,n,i){return`---\nname: ${e}\ndescription: ${String(n||"").replace(/\r?\n/g," ").trim()||"(补充:这个技能做什么、何时使用)"}\nversion: 1.0.0\n---\n\n`+(null!=i&&String(i).trim()?String(i).trim()+"\n":`# ${e}\n\n## 何时使用\n\n(描述触发场景,让模型据此判断是否load_skill)\n\n## 流程\n\n1. \n2. \n3. \n\n## 注意事项\n\n- \n`)}export function createSkillPackage({name:i,description:t="",scope:r="global",cwd:o=null,body:l=""}={}){const s=String(i||"").trim();if(!isValidSkillName(s))throw new Error("技能名非法:仅限字母/数字/下划线/连字符,长度 1–48");if("project"!==r&&"global"!==r)throw new Error("只能创建到项目级或全局级");if(loadLocalSkills(o).some(e=>e.name===s))throw new Error(`技能已存在:${s}`);const c=y(r,o);h(c);const a=n.join(c,s);return e.mkdirSync(a,{recursive:!0}),e.writeFileSync(n.join(a,"SKILL.md"),scaffoldSkillMarkdown(s,t,l),"utf8"),{dir:a,name:s,scope:r}}export function installSkillPackage({name:i,content:t,files:o=[],scope:l="global",cwd:s=null,conflict:c="skip",ack:a=!1}={}){const d=String(i||"").trim();if(!isValidSkillName(d))throw new Error("技能名非法:仅限字母/数字/下划线/连字符,长度 1–48(来源SKILL.md的frontmatter name)");if(!String(t||"").trim())throw new Error("SKILL.md正文为空");if("project"!==l&&"global"!==l)throw new Error("只能导入到项目级或全局级");const u=(Array.isArray(o)?o:[]).filter(e=>e&&"string"==typeof e.path),f=loadLocalSkills(s).find(e=>e.name===d&&"builtin"!==e.scope);f||h(y(l,s));const m=r({name:d,content:t,files:u});if("blocked"===m.verdict&&!0!==a)return{name:d,status:"blocked",scan:m};if(f){if("overwrite"!==c)return{name:d,status:"skipped",dir:f.dir,scan:m};e.writeFileSync(n.join(f.dir,"SKILL.md"),String(t),"utf8");for(const i of u){const t=String(i.path).replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,"");if(!t||t.includes(".."))continue;const r=n.join(f.dir,t);e.mkdirSync(n.dirname(r),{recursive:!0}),e.writeFileSync(r,i.content??"","utf8")}return{name:d,status:"overwritten",dir:f.dir,scan:m}}const p=writeSkillToDir(y(l,s),{name:d,content:t,files:u});return{name:d,status:"imported",dir:p,scan:m}}export function overwriteSkillBody(i,t,r){const o=loadLocalSkills(i).find(e=>e.name===String(t||"").trim());if(!o)throw new Error("本地技能不存在:"+t);if("builtin"===o.scope)throw new Error("内置技能只读,请先「复制到全局」再编辑");if(!String(r||"").trim())throw new Error("SKILL.md正文不能为空");return e.writeFileSync(n.join(o.dir,"SKILL.md"),String(r),"utf8"),{dir:o.dir,name:o.name}}export function removeSkillPackage(i,t){const r=loadLocalSkills(i).find(e=>e.name===String(t||"").trim());if(!r)throw new Error("本地技能不存在:"+t);if("builtin"===r.scope)throw new Error("内置技能不可卸载(可在「技能·MCP」中禁用)");const o=n.resolve(y(r.scope,i)),l=n.resolve(r.dir);if(l!==o&&!l.startsWith(o+n.sep))throw new Error("拒绝删除技能库之外的目录");e.rmSync(l,{recursive:!0,force:!0});const s=loadDisabledSet();return s.delete(r.name)&&d(s),{name:r.name,scope:r.scope}}export function forkSkillToWritable(e,n,i="global"){const t=loadLocalSkills(e).find(e=>e.name===String(n||"").trim());if(!t)throw new Error("本地技能不存在:"+n);if("project"!==i&&"global"!==i)throw new Error("只能复制到项目级或全局级");const r=new Set(loadLocalSkills(e).map(e=>e.name));let o=t.name;for(;r.has(o);)o=(o+"-copy").slice(0,48);const l=readSkillDirForPush(t.dir),s=l.content.replace(/^(---[\s\S]*?\nname:\s*).*$/m,`$1${o}`);return{dir:writeSkillToDir(y(i,e),{name:o,content:s,files:l.files}),name:o,scope:i}}export const SKILLS_SYNC_STAMP=n.join(i.homedir(),".huicai","cache","skills-sync.json");function w(){try{return JSON.parse(e.readFileSync(SKILLS_SYNC_STAMP,"utf8"))||{}}catch{return{}}}export async function autoSyncPlatformSkills(i,t={}){if(!i?.baseUrl||!i?.apiKey)return{synced:0,skipped:0,skipped_reason:"未登录"};if(!1===i.autoSkillSync)return{synced:0,skipped:0,skipped_reason:"已关闭自动同步"};const r=w();if(!t.force&&r.at&&Date.now()-r.at<216e5)return{synced:0,skipped:0,skipped_reason:"距上次同步未满 6 小时"};const o=t.api||await import("./api.js");let c;try{c=await o.listPlatformSkills(i.baseUrl,i.apiKey)}catch(e){return{synced:0,skipped:0,skipped_reason:e.message}}!function(i){try{e.mkdirSync(n.dirname(SKILLS_SYNC_STAMP),{recursive:!0}),e.writeFileSync(SKILLS_SYNC_STAMP,JSON.stringify({...w(),...i})+"\n",{mode:384})}catch{}}({at:Date.now()}),await s(i);const a=new Set(loadLocalSkills(null).map(e=>e.name));let d=0,u=0;const f=[];let m="",p=0;for(const e of c){const n=String(e?.name||"").trim();if(n&&isValidSkillName(n))if(a.has(n))u++;else if(m)p++,u++;else try{const t=await o.getPlatformSkill(i.baseUrl,i.apiKey,e.id),r=installSkillPackage({name:n,content:t?.content,files:t?.files||[],scope:"global",conflict:"skip"});"blocked"===r.status?(f.push(n),u++):"skipped"===r.status?u++:d++}catch(e){l(e)&&(m=e.message,p++),u++}else u++}return d&&t.onInfo&&t.onInfo(`已从平台自动同步 ${d} 个技能到本地技能库`),m&&t.onInfo&&t.onInfo(`${m}(另有 ${p} 个平台技能因此未同步到本地技能库)`),f.length&&t.onInfo&&t.onInfo(`有 ${f.length} 个平台技能因安全扫描发现严重风险未被自动安装:${f.join("、")}(如确需使用,请在「技能·MCP」页手动导入并查看扫描详情)`),{synced:d,skipped:u,blocked:f,...m?{limited:m,unsynced:p}:{}}}export function readSkillDirForPush(i){const t=n.join(i,"SKILL.md"),r=e.readFileSync(t,"utf8"),o=[];for(const t of m(i))try{const r=n.join(i,t);if(e.statSync(r).size>a)continue;o.push({path:t,content:e.readFileSync(r,"utf8")})}catch{}return{content:r,filename:n.basename(i),files:o}}
1
+ import e from"node:fs";import{accountDataDir as n,accountSnapshot as t}from"./account-scope.js";import r from"node:path";import{fileURLToPath as i}from"node:url";import{scanSkillPackage as o}from"./skill-scan.js";import{assertLimit as s,isPlanLimitError as l,refreshPlan as c}from"./plan-limits.js";export const BUNDLED_SKILLS_DIR=r.join(r.dirname(i(import.meta.url)),"..","skills");const a=new Set([".md",".markdown",".txt",".json",".yaml",".yml",".py",".js",".mjs",".cjs",".ts",".sh",".sql",".csv",".html",".css",".xml",".toml"]),d=262144;export const skillsStatePath=()=>r.join(n(),"skills-state.json");export const SKILLS_STATE_PATH=skillsStatePath();export function loadDisabledSet(){try{const n=JSON.parse(e.readFileSync(skillsStatePath(),"utf8"));return new Set(Array.isArray(n?.disabled)?n.disabled.map(String):[])}catch{return new Set}}function p(n){e.mkdirSync(r.dirname(skillsStatePath()),{recursive:!0}),e.writeFileSync(skillsStatePath(),JSON.stringify({disabled:[...n].sort()},null,2)+"\n",{mode:384})}export function setSkillEnabled(e,n){const t=String(e||"").trim();if(!t)throw new Error("技能名为空");const r=loadDisabledSet();return n?r.delete(t):r.add(t),p(r),[...r]}export const DEFAULT_INVOCATION=Object.freeze({modelInvocable:!0,userInvocable:!0});function u(e){const n=String(e).trim().toLowerCase().replace(/^["']|["']$/g,"");return!!["true","yes","on","1"].includes(n)||!["false","no","off","0"].includes(n)&&void 0}export function parseFrontmatter(e){const n={name:"",description:"",version:"",body:(e||"").trim(),invocation:{...DEFAULT_INVOCATION},invocationError:""},t=/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(n.body);if(!t)return n;n.body=n.body.slice(t[0].length).trim();for(const e of t[1].split(/\r?\n/)){const t=e.trim(),r=/^(name|description|version)\s*:\s*(.*)$/.exec(t);if(r){n[r[1]]=r[2].trim().replace(/^["']|["']$/g,"");continue}const i=/^(disable-model-invocation|user-invocable|disableModelInvocation|userInvocable)\s*:\s*(.*)$/.exec(t);if(!i)continue;const[,o,s]=i;if("disableModelInvocation"===o||"userInvocable"===o){n.invocationError=`调用面字段必须用连字符写法(${"userInvocable"===o?"user-invocable":"disable-model-invocation"}),不认camelCase`;continue}const l=u(s);void 0!==l?"disable-model-invocation"===o?n.invocation.modelInvocable=!l:n.invocation.userInvocable=l:n.invocationError=`${o} 的值不是布尔:${s.trim()}`}return n}export function skillDirs(e){const t=[];return e&&t.push(r.join(e,".huicai","skills")),t.push(r.join(n(),"skills")),t}const f=new Set(["trigger-cases.json","evals.json"]);function m(n,t="",i=0,o=[]){if(i>3||o.length>=50)return o;let s;try{s=e.readdirSync(n,{withFileTypes:!0})}catch{return o}for(const e of s){if(o.length>=50)break;if(e.name.startsWith("."))continue;const s=t?`${t}/${e.name}`:e.name;e.isDirectory()?m(r.join(n,e.name),s,i+1,o):e.isFile()&&"SKILL.md"!==s&&!f.has(s)&&a.has(r.extname(e.name).toLowerCase())&&o.push(s)}return o}let S=[];export function skillLoadWarnings(){return S.slice()}export function loadLocalSkills(t){const i=new Map,o=[],s=loadDisabledSet(),l=[...t?[{dir:r.join(t,".huicai","skills"),scope:"project"}]:[],{dir:r.join(n(),"skills"),scope:"global"},{dir:BUNDLED_SKILLS_DIR,scope:"builtin"}];for(const{dir:n,scope:t}of l){let l;try{l=e.readdirSync(n,{withFileTypes:!0})}catch{continue}for(const c of l){if(!c.isDirectory())continue;const l=r.join(n,c.name);let a;try{a=e.readFileSync(r.join(l,"SKILL.md"),"utf8")}catch{continue}const d=parseFrontmatter(a),p=(d.name||c.name).trim();p&&!i.has(p)&&(d.invocationError?o.push(`技能 ${p}(${l})调用面元数据非法,已整条跳过:${d.invocationError}`):i.set(p,{name:p,description:d.description||"",version:d.version||"",dir:l,scope:t,files:m(l),enabled:!s.has(p),invocation:d.invocation}))}}return S=o,[...i.values()]}export function isModelInvocable(e){return!1!==e?.enabled&&(e?.invocation?.modelInvocable??!0)}export function isUserInvocable(e){return!1!==e?.enabled&&(e?.invocation?.userInvocable??!0)}export function readSkillBody(n,t){const i=loadLocalSkills(n).find(e=>e.name===t);if(!i)return null;try{const n=e.readFileSync(r.join(i.dir,"SKILL.md"),"utf8");return{skill:i,raw:n.length>65536?n.slice(0,65536)+"\n…(正文已截断)":n}}catch{return null}}export function readSkillFileContent(n,t,i){const o=loadLocalSkills(n).find(e=>e.name===t);if(!o)throw new Error(`本地技能不存在:${t}`);const s=String(i||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,""),l=r.resolve(o.dir,s);if(!l.startsWith(r.resolve(o.dir)+r.sep))throw new Error(`路径越界:${i}`);if(e.statSync(l).size>d)throw new Error(`附件过大(>256KB):${s}`);return e.readFileSync(l,"utf8")}export const CATALOG_DESC_CAP=120;export function compactDesc(e,n){let t=String(n||"(无描述)").replace(/\s+/g," ").trim();const r=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");if(t=t.replace(new RegExp("^"+r+"\\s*[::—–-]\\s*"),""),t.length<=120)return t;const i=t.slice(0,120),o=Math.max(i.lastIndexOf("。"),i.lastIndexOf(";"),i.lastIndexOf(","),i.lastIndexOf(". "));return(o>72?i.slice(0,o+1):i)+"…"}export function renderSkillContent(e,n){let t=`<skill_content name="${r=e.name,String(r).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}">\n`;var r;return t+="<skill_resources>\n",e.files&&e.files.length?t+=`本技能的资源基址:${e.dir}\n其中 ${e.files.length} 个可读资源文件,需要时用read_skill_file(skill="${e.name}", path=...) 按相对路径读取;正文里提到的相对路径都按该基址解析。用到再读,不要预先全部读入。\n`:t+="本技能不带附加资源文件。\n",t+="</skill_resources>\n\n<skill_instructions>\n",t+=String(n||"").trim()+"\n",t+="</skill_instructions>\n</skill_content>",t}export function makeSkillTools(e){const n=loadLocalSkills(e).filter(isModelInvocable);if(!n.length)return[];const t=[{readOnly:!0,spec:{type:"function",function:{name:"load_skill",description:"载入一个本地技能的操作指引(系统提示词「可用技能」目录中的名称)。载入后严格遵循其流程执行。",parameters:{type:"object",properties:{name:{type:"string",description:"技能名(见「可用技能」目录)"}},required:["name"]}}},run:async n=>{const t=String(n?.name||"").trim(),r=readSkillBody(e,t);if(!r){return`本地技能不存在:${t}(可用:${loadLocalSkills(e).filter(isModelInvocable).map(e=>e.name).join(", ")||"无"})`}return!1===r.skill.enabled?`技能「${t}」已被禁用,无法载入(可在「技能·MCP」中重新启用)。`:isModelInvocable(r.skill)?renderSkillContent(r.skill,r.raw):`技能「${t}」标记为不可由模型自行载入(disable-model-invocation)。如确需它,请让用户在输入里直接敲 /${t}。`}}];return n.some(e=>e.files.length)&&t.push({readOnly:!0,spec:{type:"function",function:{name:"read_skill_file",description:"读取本地技能附带的资源文件(先load_skill查看其文件清单,再按path读取)。",parameters:{type:"object",properties:{skill:{type:"string",description:"技能名"},path:{type:"string",description:"资源文件相对路径(技能载入结果中列出)"}},required:["skill","path"]}}},run:async n=>{try{const t=readSkillFileContent(e,String(n?.skill||"").trim(),n?.path);return`文件 ${n?.path}:\n${t}`}catch(e){return`读取失败:${e.message}`}}}),t}export function writeSkillToDir(n,t){const i=String(t?.name||"").trim();if(!i||!t?.content)throw new Error("技能数据不完整(缺name或content)");const o=r.join(n,i.replace(/[^a-zA-Z0-9_-]/g,"-"));e.existsSync(r.join(o,"SKILL.md"))||h(n),e.mkdirSync(o,{recursive:!0}),e.writeFileSync(r.join(o,"SKILL.md"),t.content,"utf8");for(const n of t.files||[]){const t=String(n?.path||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,"");if(!t||t.includes(".."))continue;const i=r.join(o,t);e.mkdirSync(r.dirname(i),{recursive:!0}),e.writeFileSync(i,n.content??"","utf8")}return o}export function isValidSkillName(e){return/^[a-zA-Z0-9_-]{1,48}$/.test(String(e||""))}const k=()=>r.join(n(),"skills");function y(n){let t=0;try{for(const i of e.readdirSync(n,{withFileTypes:!0}))i.isDirectory()&&e.existsSync(r.join(n,i.name,"SKILL.md"))&&t++}catch{}return t}export function countUserSkills(e=null){return y(k())+(e?y(r.join(e,".huicai","skills")):0)}function h(e){const n=r.resolve(k()),t=r.resolve(e);s("skills",y(n)+(t===n?0:y(t)))}function w(e,t){if("project"===e){if(!t)throw new Error("项目级技能需要在会话/项目内创建");return r.join(t,".huicai","skills")}return r.join(n(),"skills")}export function scaffoldSkillMarkdown(e,n,t){return`---\nname: ${e}\ndescription: ${String(n||"").replace(/\r?\n/g," ").trim()||"(补充:这个技能做什么、何时使用)"}\nversion: 1.0.0\n---\n\n`+(null!=t&&String(t).trim()?String(t).trim()+"\n":`# ${e}\n\n## 何时使用\n\n(描述触发场景,让模型据此判断是否load_skill)\n\n## 流程\n\n1. \n2. \n3. \n\n## 注意事项\n\n- \n`)}export function createSkillPackage({name:n,description:t="",scope:i="global",cwd:o=null,body:s=""}={}){const l=String(n||"").trim();if(!isValidSkillName(l))throw new Error("技能名非法:仅限字母/数字/下划线/连字符,长度 1–48");if("project"!==i&&"global"!==i)throw new Error("只能创建到项目级或全局级");if(loadLocalSkills(o).some(e=>e.name===l))throw new Error(`技能已存在:${l}`);const c=w(i,o);h(c);const a=r.join(c,l);return e.mkdirSync(a,{recursive:!0}),e.writeFileSync(r.join(a,"SKILL.md"),scaffoldSkillMarkdown(l,t,s),"utf8"),{dir:a,name:l,scope:i}}export function installSkillPackage({name:n,content:t,files:i=[],scope:s="global",cwd:l=null,conflict:c="skip",ack:a=!1}={}){const d=String(n||"").trim();if(!isValidSkillName(d))throw new Error("技能名非法:仅限字母/数字/下划线/连字符,长度 1–48(来源SKILL.md的frontmatter name)");if(!String(t||"").trim())throw new Error("SKILL.md正文为空");if("project"!==s&&"global"!==s)throw new Error("只能导入到项目级或全局级");const p=(Array.isArray(i)?i:[]).filter(e=>e&&"string"==typeof e.path),u=loadLocalSkills(l).find(e=>e.name===d&&"builtin"!==e.scope);u||h(w(s,l));const f=o({name:d,content:t,files:p});if("blocked"===f.verdict&&!0!==a)return{name:d,status:"blocked",scan:f};if(u){if("overwrite"!==c)return{name:d,status:"skipped",dir:u.dir,scan:f};e.writeFileSync(r.join(u.dir,"SKILL.md"),String(t),"utf8");for(const n of p){const t=String(n.path).replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,"");if(!t||t.includes(".."))continue;const i=r.join(u.dir,t);e.mkdirSync(r.dirname(i),{recursive:!0}),e.writeFileSync(i,n.content??"","utf8")}return{name:d,status:"overwritten",dir:u.dir,scan:f}}const m=writeSkillToDir(w(s,l),{name:d,content:t,files:p});return{name:d,status:"imported",dir:m,scan:f}}export function overwriteSkillBody(n,t,i){const o=loadLocalSkills(n).find(e=>e.name===String(t||"").trim());if(!o)throw new Error("本地技能不存在:"+t);if("builtin"===o.scope)throw new Error("内置技能只读,请先「复制到全局」再编辑");if(!String(i||"").trim())throw new Error("SKILL.md正文不能为空");return e.writeFileSync(r.join(o.dir,"SKILL.md"),String(i),"utf8"),{dir:o.dir,name:o.name}}export function removeSkillPackage(n,t){const i=loadLocalSkills(n).find(e=>e.name===String(t||"").trim());if(!i)throw new Error("本地技能不存在:"+t);if("builtin"===i.scope)throw new Error("内置技能不可卸载(可在「技能·MCP」中禁用)");const o=r.resolve(w(i.scope,n)),s=r.resolve(i.dir);if(s!==o&&!s.startsWith(o+r.sep))throw new Error("拒绝删除技能库之外的目录");e.rmSync(s,{recursive:!0,force:!0});const l=loadDisabledSet();return l.delete(i.name)&&p(l),{name:i.name,scope:i.scope}}export function forkSkillToWritable(e,n,t="global"){const r=loadLocalSkills(e).find(e=>e.name===String(n||"").trim());if(!r)throw new Error("本地技能不存在:"+n);if("project"!==t&&"global"!==t)throw new Error("只能复制到项目级或全局级");const i=new Set(loadLocalSkills(e).map(e=>e.name));let o=r.name;for(;i.has(o);)o=(o+"-copy").slice(0,48);const s=readSkillDirForPush(r.dir),l=s.content.replace(/^(---[\s\S]*?\nname:\s*).*$/m,`$1${o}`);return{dir:writeSkillToDir(w(t,e),{name:o,content:l,files:s.files}),name:o,scope:t}}export const skillsSyncStamp=()=>r.join(n(),"cache","skills-sync.json");export const SKILLS_SYNC_STAMP=skillsSyncStamp();function g(){try{return JSON.parse(e.readFileSync(skillsSyncStamp(),"utf8"))||{}}catch{return{}}}export async function autoSyncPlatformSkills(n,i={}){if(!n?.baseUrl||!n?.apiKey)return{synced:0,skipped:0,skipped_reason:"未登录"};if(!1===n.autoSkillSync)return{synced:0,skipped:0,skipped_reason:"已关闭自动同步"};const o=t(),s=g();if(!i.force&&s.at&&Date.now()-s.at<216e5)return{synced:0,skipped:0,skipped_reason:"距上次同步未满 6 小时"};const a=i.api||await import("./api.js");let d;try{d=await a.listPlatformSkills(n.baseUrl,n.apiKey)}catch(e){return{synced:0,skipped:0,skipped_reason:e.message}}if(!o.current())return{synced:0,skipped:0,skipped_reason:"登录状态已改变"};if(function(n){try{e.mkdirSync(r.dirname(skillsSyncStamp()),{recursive:!0}),e.writeFileSync(skillsSyncStamp(),JSON.stringify({...g(),...n})+"\n",{mode:384})}catch{}}({at:Date.now()}),await c(n),!o.current())return{synced:0,skipped:0,skipped_reason:"登录状态已改变"};const p=new Set(loadLocalSkills(null).map(e=>e.name));let u=0,f=0;const m=[];let S="",k=0;for(const e of d){const t=String(e?.name||"").trim();if(t&&isValidSkillName(t))if(p.has(t))f++;else if(S)k++,f++;else try{const r=await a.getPlatformSkill(n.baseUrl,n.apiKey,e.id);if(!o.current())break;const i=installSkillPackage({name:t,content:r?.content,files:r?.files||[],scope:"global",conflict:"skip"});"blocked"===i.status?(m.push(t),f++):"skipped"===i.status?f++:u++}catch(e){l(e)&&(S=e.message,k++),f++}else f++}return u&&i.onInfo&&i.onInfo(`已从平台自动同步 ${u} 个技能到本地技能库`),S&&i.onInfo&&i.onInfo(`${S}(另有 ${k} 个平台技能因此未同步到本地技能库)`),m.length&&i.onInfo&&i.onInfo(`有 ${m.length} 个平台技能因安全扫描发现严重风险未被自动安装:${m.join("、")}(如确需使用,请在「技能·MCP」页手动导入并查看扫描详情)`),{synced:u,skipped:f,blocked:m,...S?{limited:S,unsynced:k}:{}}}export function readSkillDirForPush(n){const t=r.join(n,"SKILL.md"),i=e.readFileSync(t,"utf8"),o=[];for(const t of m(n))try{const i=r.join(n,t);if(e.statSync(i).size>d)continue;o.push({path:t,content:e.readFileSync(i,"utf8")})}catch{}return{content:i,filename:r.basename(n),files:o}}
package/src/squad.js CHANGED
@@ -1 +1 @@
1
- import e from"node:fs";import t from"node:path";import n from"node:os";import{fileURLToPath as r}from"node:url";import{runTurn as s}from"./agent.js";import{createPermissions as o}from"./permissions.js";import{loadAgents as i,makeSubEvents as a,withSubagentSlot as c}from"./subagent.js";import{streamChat as m}from"./api.js";import{discoverExperts as l}from"./experts.js";export const BUNDLED_SQUADS_DIR=t.join(t.dirname(r(import.meta.url)),"..","squads");export function squadDirs(e){const r=[];return e&&r.push(t.join(e,".huicai","squads")),r.push(t.join(n.homedir(),".huicai","squads")),r}export function countUserSquads(t=null){let n=0;for(const r of squadDirs(t))try{n+=e.readdirSync(r).filter(e=>e.endsWith(".md")).length}catch{}return n}export function parseSquadDoc(e,t=""){const{attrs:n,body:r}=function(e){const t={attrs:{},body:String(e||"").trim()},n=/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(t.body);if(!n)return t;t.body=t.body.slice(n[0].length).trim();for(const e of n[1].split(/\r?\n/)){const n=/^([A-Za-z][\w-]*)\s*:\s*(.*)$/.exec(e.trim());n&&(t.attrs[n[1]]=n[2].trim().replace(/^["']|["']$/g,""))}return t}(e),s={name:n.name||t,description:n.description||"",leader:n.leader||"主理人",mode:["dispatch","review","ensemble","adversarial"].includes(n.mode)?n.mode:"dispatch",when:n.when||"",mission:"",members:[],precheck:[],workflows:[]},o=new Map,i=r.split(/^##\s+/m);s.mission=(i[0]||"").trim();for(const e of i.slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1);if(/^成员人设/.test(n))for(const e of r.split(/^###\s+/m).slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1).trim();n&&r&&o.set(n,r)}else if(/^成员/.test(n))for(const e of r.split(/\r?\n/)){const t=/^\s*[-*]\s+(.+)$/.exec(e);if(!t)continue;const n=t[1].split("|").map(e=>e.trim());n[0]&&s.members.push({id:n[0],callName:n[1]||n[0],duty:n[2]||"",executor:n[3]||"",side:["proponent","opponent","judge"].includes(n[4])?n[4]:""})}else if(/^预检/.test(n))for(const e of r.split(/\r?\n/)){const t=/^\s*[-*]\s+(.+)$/.exec(e);t&&s.precheck.push(t[1].trim())}else if(/^工作流/.test(n))for(const e of r.split(/^###\s+/m).slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1),o=/^(.*?)(?:(触发[::](.*?)))?$/.exec(n)||[],i={name:(o[1]||n).trim(),trigger:(o[2]||"").trim(),stages:[],deliverable:""};for(const e of r.split(/\r?\n/)){const t=/^\s*\d+[.、)]\s*(.+)$/.exec(e);if(t){i.stages.push(t[1].trim());continue}const n=/^\s*交付[::]\s*(.+)$/.exec(e);n&&(i.deliverable=n[1].trim())}i.name&&s.workflows.push(i)}}for(const[e,t]of o){const n=s.members.find(t=>t.id===e);n&&(n.persona=t)}return s}export function loadSquads(n){const r=new Map,s=[...squadDirs(n).map((e,t)=>({dir:e,scope:n&&0===t?"project":"global"})),{dir:BUNDLED_SQUADS_DIR,scope:"builtin"}];for(const{dir:n,scope:o}of s){let s;try{s=e.readdirSync(n).filter(e=>e.endsWith(".md"))}catch{continue}for(const i of s){const s=i.replace(/\.md$/,"").trim();if(s)try{const a=t.join(n,i),c=parseSquadDoc(e.readFileSync(a,"utf8"),s);c.scope=o,c.file=a,r.has(c.name)||r.set(c.name,c)}catch{}}}return[...r.values()]}export function ensureTeamDir(n,r){const s=(new Date).toISOString().slice(0,16).replace(/[-:]/g,"").replace("T","-"),o=t.join(n,".huicai","teams",`${r}-${s}`);try{e.mkdirSync(o,{recursive:!0})}catch{return null}return o}const p={proponent:"主张方",opponent:"反驳方",judge:"裁判"};export function buildLeaderPrompt(e,t){const n=[];n.push("**Role Override**:以下专家团主理人定义优先于此前任何身份设定,是你在本任务中唯一、权威的角色。"),n.push(""),n.push(`# 专家团「${e.name}」· 主理人${e.leader}`),n.push(`你是专家团「${e.name}」的主理人${e.leader},负责把用户任务拆解、派发给团队成员并汇总交付。`),e.mission&&n.push("",e.mission),n.push("","## 关键规则:只调度,不代工","- 成员职责范围内的专业产出必须经dispatch_member由对应成员完成后采信,你不代写;","- 简单澄清/寒暄/对既有产出的说明可直接回答,无须派发。");const r="adversarial"===e.mode;if(r){n.push("","## 团队架构(调度依据)","| 成员ID | 称呼 | 职责 | 执行者 | 阵营 |","|---|---|---|---|---|");for(const t of e.members)n.push(`| ${t.id} | ${t.callName} | ${t.duty} | ${t.executor||"(当前模型)"} | ${p[t.side]||"(未指定)"} |`)}else{n.push("","## 团队架构(调度依据)","| 成员ID | 称呼 | 职责 | 执行者 |","|---|---|---|---|");for(const t of e.members)n.push(`| ${t.id} | ${t.callName} | ${t.duty} | ${t.executor||"(当前模型)"} |`)}if(n.push("","## 任务分配预检(每次派发前必做)","成员不会说「这个我不会」——超能力范围的任务结果无保证。所以:","1. 解析用户意图(研究/分析/生成/编辑/写作/审校…);","2. 对照职责与速查表做能力匹配;","3. 匹配 → 派发;不匹配 → 告知用户团队能力限制与替代建议,禁止硬派;","4. 意图模糊 → 先向用户确认。"),e.precheck.length){n.push("### 速查表(常见误派提醒)");for(const t of e.precheck)n.push(`- ${t}`)}if(e.workflows.length){n.push("","## 预设工作流(识别到触发条件时按阶段执行,不自由发挥)");for(const t of e.workflows)n.push(`### ${t.name}${t.trigger?`(触发:${t.trigger})`:""}`),t.stages.forEach((e,t)=>n.push(`${t+1}. ${e}`)),t.deliverable&&n.push(`交付:${t.deliverable}`)}return n.push("","## 团队协作规范","1. 成员在隔离上下文执行,看不到本对话与其他成员产出——任务说明必须独立自足;","2. 跨成员信息流经你中转:把上一阶段产出的必要部分放进下一位成员的context;","3. 互相独立的任务同一轮并行派发(一次输出多个dispatch_member调用),减少串行等待;","4. 成员结论为准:如实汇编;产出不符时修正任务说明重派,而非替他改写。"),"review"===e.mode?n.push("### 生成-审查模式(本团约束)","每个关键产出完成后,必须把「产出全文+原始要求」派给职责匹配的审查成员评审,按意见让原成员修订至通过;未经审查的产出不得作为最终交付。"):"ensemble"===e.mode?n.push("### 多候选聚合模式(本团约束)","对开放性核心任务,把同一任务并行派给多位匹配成员(或不同侧重多份),对比候选择优或综合,并向用户说明取舍。"):r&&n.push(...function(e){const t=t=>e.members.filter(e=>e.side===t).map(e=>e.callName).join("、"),n=t("proponent")||"(未指定主张方)",r=t("opponent")||"(未指定反驳方)",s=t("judge"),o=["### 对抗推演模式(本团约束)",`本团分三个阵营:主张方 ${n} · 反驳方 ${r}${s?` · 裁判 ${s}`:""}。`,"**编排固定为四步,不自由发挥**:","1. **主张**:把用户的方案/材料派给主张方,要它给出完整论证(含依据);","2. **反驳**:把主张方的产出**全文**连同原始材料派给反驳方,要它逐条找漏洞、风险与反例;","3. **回应**:把反驳意见派回主张方,要它逐条回应——能修的修,不能修的如实承认;",`4. **裁判**:${s?"派给裁判成员":"由你(主理人)"}做独立的一轮裁定。`,"最多 2 轮(一轮 = 主张+反驳+回应)。第 2 轮结束必须进入裁判,","不许因为「还能再挑出点什么」而继续加轮——对抗的边际收益递减得很快。"];return o.push("**反驳方的硬约束**(写进派给它的任务说明里,每次都写):","- 只许基于已给出的材料反驳,**不许编造事实、数据或案例**;","- 每条反驳必须指明它针对原文的哪一句/哪一条,让人能回去核对;","- 找不到实质问题就如实说「这一部分没有实质问题」,**不要为了凑数而挑措辞**。"),o.push("**裁判的硬约束**:","- 不许给「甲方更有说服力」这类整体印象分;","- 必须逐条裁定:哪几条反驳成立、哪几条不成立、哪几条主张因此需要修改;","- 每条裁定给一句依据,且依据只能来自本轮的三份产出,不得引入新信息。"),o.push("**交付**:最终答复要包含——① 经过修正的方案;② 被有效反驳因而改动的清单;③ 未能解决、需要用户自己拍板的分歧。第三项不许省略,它往往才是最有价值的部分。"),o}(e)),n.push("","## 异常处理(失败三级升级)","- 一级:修正任务说明(更自足、更具体、补context)后重派原成员;同一任务累计失败满 2 次系统会终止该任务线;","- 二级:换人或换执行方式——改派职责相近的其他成员;平台资源不可用时由本机成员降级承接(系统会自动降级并注明);","- 三级:降级交付——完成可完成的部分,如实说明缺口与原因,并给出用户可选的替代方案。"),t&&n.push("","## 团队工作区",`本次协作的产物统一放在 ${t}(你可用文件工具写入;成员只回传文本结论,由你负责归档与整合)。`),n.push("","## 交付检查清单(最终答复前逐项自查)","- 用户目标的每一部分是否都有对应成员产出支撑?","- 是否汇总了全部关键产出、无散落遗漏?","- 是否标注了各部分由哪位成员产出?","- 未完成的部分是否如实说明?"),n.join("\n")}const u=/^(agent|workflow|datahuman|squad)-/;async function d({squad:e,member:t,ref:n,task:r,context:s,expectation:o,config:i,signal:a}){const c=[`【专家团「${e.name}」成员任务】${r}`,s?`\n【上游产出/背景】\n${s}`:"",o?`\n【交付要求】\n${o}`:""].join("");return(await m({baseUrl:i.baseUrl,apiKey:i.apiKey,model:n,messages:[{role:"user",content:c}],client:i.client,sessionId:i.sessionId},{},a)).content||"(平台无返回)"}async function f({squad:e,member:t,task:n,context:r,expectation:m,config:p,cwd:f,readOnlyExtras:h,onSub:$,label:g,signal:b}){b?.throwIfAborted();const w=String(t.executor||"").trim();let x="";if(w.startsWith("platform:auto:")){const s=await async function(e,t){const n=/^platform:auto:(agent|workflow|datahuman|squad)(?::(.+))?$/.exec(e);if(!n)return null;const[,r,s]=n;try{const e=await l(t),n=(s||"").trim().toLowerCase();return e.find(e=>e.type===r&&(!n||(String(e.name)+" "+String(e.description)).toLowerCase().includes(n)))||null}catch{return null}}(w,p);if(s){const o=await d({squad:e,member:t,ref:s.id,task:n,context:r,expectation:m,config:p,signal:b});return`(由平台资源「${s.name}」执行)\n${o}`}x=`(未发现可用的平台${w.split(":")[2]||""}资源,已由本机成员降级执行)\n`}else if(w.startsWith("platform:")){const s=w.slice(9).trim();return u.test(s)?d({squad:e,member:t,ref:s,task:n,context:r,expectation:m,config:p,signal:b}):`成员「${t.callName}」的执行者 ${s} 不是有效的平台伪模型引用`}const y=["**Role Override**:以下角色定义在本次任务中拥有最高优先级。",""];t.persona&&y.push(t.persona,""),y.push(`你是专家团「${e.name}」的成员「${t.callName}」${t.duty?`(职责:${t.duty})`:""},正在执行主理人下发的一项独立任务。`,"- 只完成本任务,直接输出可被主理人直接采用的成果本身,不要寒暄、不要复述任务;","- 无法反问,信息不全时按合理假设执行并注明假设。"),r&&y.push("","## 主理人提供的上游产出/背景",String(r)),m&&y.push("","## 交付要求",String(m));let S=y.join("\n"),q=p;if(w.startsWith("agent:")){const e=w.slice(6).trim(),t=i(f).find(t=>t.name===e);t&&(S=t.prompt.trim()+"\n\n"+S)}else if(w.startsWith("model:")){const e=w.slice(6).trim();e&&(q={...p,model:e})}S+="\n工作目录:"+f;const j=[{role:"system",content:S},{role:"user",content:n}],k=await c(p,0,()=>s({config:{...q,aux:!0},messages:j,permissions:o({yolo:!1,ask:null}),cwd:f,extraTools:h,signal:b,depth:1,maxIters:50,io:{write:()=>{}},events:a($,g||`${t.callName}·${String(n).slice(0,40)}`,"member")}),b);if(b?.throwIfAborted(),k?.error)return`成员执行失败:${k.error}`;const D=[...j].reverse().find(e=>"assistant"===e.role&&e.content);return D?x+D.content:"(成员未产出结论)"}export function makeDispatchMemberTool({config:e,cwd:t,extraTools:n=[],onSub:r=null}){const s=loadSquads(t);if(!s.length)return null;const o=n.filter(e=>e.readOnly),i=new Map,a={type:"function",function:{name:"dispatch_member",description:"把一项独立、自足的任务派发给某个专家团的一位成员:成员在隔离上下文中执行并只回传成果。复杂/跨角色任务优先按团开工:对照下方各团「何时使用」自主选团,按其预设工作流分阶段派发(互相独立的任务同一轮多次调用并行);成员产出由你汇总并用本机工具落地,落地后验证。派发前先做任务分配预检(对照成员职责做能力匹配)。\n可用专家团:\n"+s.map(e=>`- ${e.name}:${e.when||e.description||""}(成员 ${e.members.map(e=>e.id).join("/")})`).join("\n"),parameters:{type:"object",properties:{squad:{type:"string",enum:s.map(e=>e.name),description:"专家团名称"},member:{type:"string",description:"成员ID(推荐)或称呼,须来自该团的团队架构表"},task:{type:"string",description:"下发的任务说明——必须独立自足(成员看不到本对话与其他成员产出)"},context:{type:"string",description:"可选:上游产出/背景(跨成员信息流经主理人中转,把必要部分放这里)"},expectation:{type:"string",description:"可选:交付要求/验收标准"}},required:["squad","member","task"]}}},c=async(n,a,c=r)=>{a?.throwIfAborted();const m=s.find(e=>e.name===String(n?.squad||"").trim());if(!m)return`未找到专家团「${n?.squad}」(可用:${s.map(e=>e.name).join("、")})`;const l=function(e,t){const n=String(t||"").trim();return e.members.find(e=>e.id===n)||e.members.find(e=>e.callName===n)||null}(m,n?.member);if(!l)return`专家团「${m.name}」中没有成员「${n?.member}」(可用:${m.members.map(e=>`${e.id}(${e.callName})`).join("、")})`;const p=String(n?.task||"").trim();if(!p)return"缺少task(任务说明)";const u=`${m.name}|${l.id}|${p.slice(0,40)}`,d=i.get(u)||0;if(d>=2)return`该任务线已按异常协议终止(成员「${l.callName}」同一任务累计失败满 2 次)。请向用户说明原因并给出替代方案。`;try{const r=await f({squad:m,member:l,task:p,context:n?.context,expectation:n?.expectation,config:e,cwd:t,readOnlyExtras:o,signal:a,onSub:c,label:`${l.callName}·${p.replace(/\s+/g," ").slice(0,40)}`});return/^(成员执行失败|平台调用失败)/.test(r)?(i.set(u,d+1),`${r}(累计失败 ${d+1}/2,可修正任务说明后重试或改派他人)`):`成员「${l.callName}」对任务【${p.replace(/\s+/g," ").slice(0,60)}】的产出:\n${r}`}catch(e){if(e&&"AbortError"===e.name)throw e;return i.set(u,d+1),`成员「${l.callName}」执行失败:${e.message}(累计失败 ${d+1}/2)`}};return{readOnly:!0,spec:a,run:c,runWithEnv:(e,t,n)=>c(e,n,e=>r?.({...e,callId:t.toolCallId}))}}export function squadCommandPrompt(e,t,n){return buildLeaderPrompt(e,n)+"\n\n---\n\n用户任务:"+t+`\n\n请以主理人${e.leader}的身份完成:先做任务分配预检(含预设工作流匹配),然后经dispatch_member`+`(squad="${e.name}")派发执行(独立任务同轮并行),跨成员信息流经你中转,最后按交付检查清单汇总交付。`}export const SQUAD_TEMPLATE="---\nname: {NAME}\ndescription: 内容创作专家团(示例:改成你的团队)\nleader: 司远\nmode: dispatch\nwhen: 需要成套内容创作/深度文章且涉及调研-写作-审校多角色协作时\n---\n\n擅长把用户的创作需求拆解为具体执行方案,并调度合适的成员高效交付。\n\n## 成员\n- researcher | 调研员 | 资料收集、事实核查、竞品调研 | agent:researcher\n- writer | 笔澜 | 公众号长文、营销文案、脚本 |\n- reviewer | 审校 | 审查产出、给出修订意见 |\n- fetcher | 源泉 | (示例)平台数字人自动绑定查数 | platform:auto:datahuman\n\n## 成员人设\n### writer\n角色:资深中文内容主笔。\n工作方法:先定读者与核心论点,再列结构,后成文;每段只讲一件事;用事实与例子代替空话。\n交付标准:结构清晰可扫读、观点有依据、结尾给行动建议;附 3 条备选标题。\n\n## 预检\n- 写长文/文案 → writer,不要派给researcher\n- 资料/数据收集 → researcher,产出交给writer前先经你提炼要点\n\n## 工作流\n### 深度长文(触发:用户要深度文章/长文)\n1. researcher:收集资料与事实\n2. writer:按资料成稿\n3. reviewer:审查并给修订意见(mode=review时必经)\n交付:终稿Markdown + 三条标题备选\n";
1
+ import e from"node:fs";import{accountDataDir as t}from"./account-scope.js";import n from"node:path";import{fileURLToPath as r}from"node:url";import{runTurn as s}from"./agent.js";import{createPermissions as o}from"./permissions.js";import{loadAgents as i,makeSubEvents as a,withSubagentSlot as c}from"./subagent.js";import{streamChat as m}from"./api.js";import{discoverExperts as l}from"./experts.js";export const BUNDLED_SQUADS_DIR=n.join(n.dirname(r(import.meta.url)),"..","squads");export function squadDirs(e){const r=[];return e&&r.push(n.join(e,".huicai","squads")),r.push(n.join(t(),"squads")),r}export function countUserSquads(t=null){let n=0;for(const r of squadDirs(t))try{n+=e.readdirSync(r).filter(e=>e.endsWith(".md")).length}catch{}return n}export function parseSquadDoc(e,t=""){const{attrs:n,body:r}=function(e){const t={attrs:{},body:String(e||"").trim()},n=/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(t.body);if(!n)return t;t.body=t.body.slice(n[0].length).trim();for(const e of n[1].split(/\r?\n/)){const n=/^([A-Za-z][\w-]*)\s*:\s*(.*)$/.exec(e.trim());n&&(t.attrs[n[1]]=n[2].trim().replace(/^["']|["']$/g,""))}return t}(e),s={name:n.name||t,description:n.description||"",leader:n.leader||"主理人",mode:["dispatch","review","ensemble","adversarial"].includes(n.mode)?n.mode:"dispatch",when:n.when||"",mission:"",members:[],precheck:[],workflows:[]},o=new Map,i=r.split(/^##\s+/m);s.mission=(i[0]||"").trim();for(const e of i.slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1);if(/^成员人设/.test(n))for(const e of r.split(/^###\s+/m).slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1).trim();n&&r&&o.set(n,r)}else if(/^成员/.test(n))for(const e of r.split(/\r?\n/)){const t=/^\s*[-*]\s+(.+)$/.exec(e);if(!t)continue;const n=t[1].split("|").map(e=>e.trim());n[0]&&s.members.push({id:n[0],callName:n[1]||n[0],duty:n[2]||"",executor:n[3]||"",side:["proponent","opponent","judge"].includes(n[4])?n[4]:""})}else if(/^预检/.test(n))for(const e of r.split(/\r?\n/)){const t=/^\s*[-*]\s+(.+)$/.exec(e);t&&s.precheck.push(t[1].trim())}else if(/^工作流/.test(n))for(const e of r.split(/^###\s+/m).slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1),o=/^(.*?)(?:(触发[::](.*?)))?$/.exec(n)||[],i={name:(o[1]||n).trim(),trigger:(o[2]||"").trim(),stages:[],deliverable:""};for(const e of r.split(/\r?\n/)){const t=/^\s*\d+[.、)]\s*(.+)$/.exec(e);if(t){i.stages.push(t[1].trim());continue}const n=/^\s*交付[::]\s*(.+)$/.exec(e);n&&(i.deliverable=n[1].trim())}i.name&&s.workflows.push(i)}}for(const[e,t]of o){const n=s.members.find(t=>t.id===e);n&&(n.persona=t)}return s}export function loadSquads(t){const r=new Map,s=[...squadDirs(t).map((e,n)=>({dir:e,scope:t&&0===n?"project":"global"})),{dir:BUNDLED_SQUADS_DIR,scope:"builtin"}];for(const{dir:t,scope:o}of s){let s;try{s=e.readdirSync(t).filter(e=>e.endsWith(".md"))}catch{continue}for(const i of s){const s=i.replace(/\.md$/,"").trim();if(s)try{const a=n.join(t,i),c=parseSquadDoc(e.readFileSync(a,"utf8"),s);c.scope=o,c.file=a,r.has(c.name)||r.set(c.name,c)}catch{}}}return[...r.values()]}export function ensureTeamDir(t,r){const s=(new Date).toISOString().slice(0,16).replace(/[-:]/g,"").replace("T","-"),o=n.join(t,".huicai","teams",`${r}-${s}`);try{e.mkdirSync(o,{recursive:!0})}catch{return null}return o}const p={proponent:"主张方",opponent:"反驳方",judge:"裁判"};export function buildLeaderPrompt(e,t){const n=[];n.push("**Role Override**:以下专家团主理人定义优先于此前任何身份设定,是你在本任务中唯一、权威的角色。"),n.push(""),n.push(`# 专家团「${e.name}」· 主理人${e.leader}`),n.push(`你是专家团「${e.name}」的主理人${e.leader},负责把用户任务拆解、派发给团队成员并汇总交付。`),e.mission&&n.push("",e.mission),n.push("","## 关键规则:只调度,不代工","- 成员职责范围内的专业产出必须经dispatch_member由对应成员完成后采信,你不代写;","- 简单澄清/寒暄/对既有产出的说明可直接回答,无须派发。");const r="adversarial"===e.mode;if(r){n.push("","## 团队架构(调度依据)","| 成员ID | 称呼 | 职责 | 执行者 | 阵营 |","|---|---|---|---|---|");for(const t of e.members)n.push(`| ${t.id} | ${t.callName} | ${t.duty} | ${t.executor||"(当前模型)"} | ${p[t.side]||"(未指定)"} |`)}else{n.push("","## 团队架构(调度依据)","| 成员ID | 称呼 | 职责 | 执行者 |","|---|---|---|---|");for(const t of e.members)n.push(`| ${t.id} | ${t.callName} | ${t.duty} | ${t.executor||"(当前模型)"} |`)}if(n.push("","## 任务分配预检(每次派发前必做)","成员不会说「这个我不会」——超能力范围的任务结果无保证。所以:","1. 解析用户意图(研究/分析/生成/编辑/写作/审校…);","2. 对照职责与速查表做能力匹配;","3. 匹配 → 派发;不匹配 → 告知用户团队能力限制与替代建议,禁止硬派;","4. 意图模糊 → 先向用户确认。"),e.precheck.length){n.push("### 速查表(常见误派提醒)");for(const t of e.precheck)n.push(`- ${t}`)}if(e.workflows.length){n.push("","## 预设工作流(识别到触发条件时按阶段执行,不自由发挥)");for(const t of e.workflows)n.push(`### ${t.name}${t.trigger?`(触发:${t.trigger})`:""}`),t.stages.forEach((e,t)=>n.push(`${t+1}. ${e}`)),t.deliverable&&n.push(`交付:${t.deliverable}`)}return n.push("","## 团队协作规范","1. 成员在隔离上下文执行,看不到本对话与其他成员产出——任务说明必须独立自足;","2. 跨成员信息流经你中转:把上一阶段产出的必要部分放进下一位成员的context;","3. 互相独立的任务同一轮并行派发(一次输出多个dispatch_member调用),减少串行等待;","4. 成员结论为准:如实汇编;产出不符时修正任务说明重派,而非替他改写。"),"review"===e.mode?n.push("### 生成-审查模式(本团约束)","每个关键产出完成后,必须把「产出全文+原始要求」派给职责匹配的审查成员评审,按意见让原成员修订至通过;未经审查的产出不得作为最终交付。"):"ensemble"===e.mode?n.push("### 多候选聚合模式(本团约束)","对开放性核心任务,把同一任务并行派给多位匹配成员(或不同侧重多份),对比候选择优或综合,并向用户说明取舍。"):r&&n.push(...function(e){const t=t=>e.members.filter(e=>e.side===t).map(e=>e.callName).join("、"),n=t("proponent")||"(未指定主张方)",r=t("opponent")||"(未指定反驳方)",s=t("judge"),o=["### 对抗推演模式(本团约束)",`本团分三个阵营:主张方 ${n} · 反驳方 ${r}${s?` · 裁判 ${s}`:""}。`,"**编排固定为四步,不自由发挥**:","1. **主张**:把用户的方案/材料派给主张方,要它给出完整论证(含依据);","2. **反驳**:把主张方的产出**全文**连同原始材料派给反驳方,要它逐条找漏洞、风险与反例;","3. **回应**:把反驳意见派回主张方,要它逐条回应——能修的修,不能修的如实承认;",`4. **裁判**:${s?"派给裁判成员":"由你(主理人)"}做独立的一轮裁定。`,"最多 2 轮(一轮 = 主张+反驳+回应)。第 2 轮结束必须进入裁判,","不许因为「还能再挑出点什么」而继续加轮——对抗的边际收益递减得很快。"];return o.push("**反驳方的硬约束**(写进派给它的任务说明里,每次都写):","- 只许基于已给出的材料反驳,**不许编造事实、数据或案例**;","- 每条反驳必须指明它针对原文的哪一句/哪一条,让人能回去核对;","- 找不到实质问题就如实说「这一部分没有实质问题」,**不要为了凑数而挑措辞**。"),o.push("**裁判的硬约束**:","- 不许给「甲方更有说服力」这类整体印象分;","- 必须逐条裁定:哪几条反驳成立、哪几条不成立、哪几条主张因此需要修改;","- 每条裁定给一句依据,且依据只能来自本轮的三份产出,不得引入新信息。"),o.push("**交付**:最终答复要包含——① 经过修正的方案;② 被有效反驳因而改动的清单;③ 未能解决、需要用户自己拍板的分歧。第三项不许省略,它往往才是最有价值的部分。"),o}(e)),n.push("","## 异常处理(失败三级升级)","- 一级:修正任务说明(更自足、更具体、补context)后重派原成员;同一任务累计失败满 2 次系统会终止该任务线;","- 二级:换人或换执行方式——改派职责相近的其他成员;平台资源不可用时由本机成员降级承接(系统会自动降级并注明);","- 三级:降级交付——完成可完成的部分,如实说明缺口与原因,并给出用户可选的替代方案。"),t&&n.push("","## 团队工作区",`本次协作的产物统一放在 ${t}(你可用文件工具写入;成员只回传文本结论,由你负责归档与整合)。`),n.push("","## 交付检查清单(最终答复前逐项自查)","- 用户目标的每一部分是否都有对应成员产出支撑?","- 是否汇总了全部关键产出、无散落遗漏?","- 是否标注了各部分由哪位成员产出?","- 未完成的部分是否如实说明?"),n.join("\n")}const u=/^(agent|workflow|datahuman|squad)-/;async function d({squad:e,member:t,ref:n,task:r,context:s,expectation:o,config:i,signal:a}){const c=[`【专家团「${e.name}」成员任务】${r}`,s?`\n【上游产出/背景】\n${s}`:"",o?`\n【交付要求】\n${o}`:""].join("");return(await m({baseUrl:i.baseUrl,apiKey:i.apiKey,model:n,messages:[{role:"user",content:c}],client:i.client,sessionId:i.sessionId},{},a)).content||"(平台无返回)"}async function f({squad:e,member:t,task:n,context:r,expectation:m,config:p,cwd:f,readOnlyExtras:h,onSub:$,label:g,signal:b}){b?.throwIfAborted();const w=String(t.executor||"").trim();let x="";if(w.startsWith("platform:auto:")){const s=await async function(e,t){const n=/^platform:auto:(agent|workflow|datahuman|squad)(?::(.+))?$/.exec(e);if(!n)return null;const[,r,s]=n;try{const e=await l(t),n=(s||"").trim().toLowerCase();return e.find(e=>e.type===r&&(!n||(String(e.name)+" "+String(e.description)).toLowerCase().includes(n)))||null}catch{return null}}(w,p);if(s){const o=await d({squad:e,member:t,ref:s.id,task:n,context:r,expectation:m,config:p,signal:b});return`(由平台资源「${s.name}」执行)\n${o}`}x=`(未发现可用的平台${w.split(":")[2]||""}资源,已由本机成员降级执行)\n`}else if(w.startsWith("platform:")){const s=w.slice(9).trim();return u.test(s)?d({squad:e,member:t,ref:s,task:n,context:r,expectation:m,config:p,signal:b}):`成员「${t.callName}」的执行者 ${s} 不是有效的平台伪模型引用`}const y=["**Role Override**:以下角色定义在本次任务中拥有最高优先级。",""];t.persona&&y.push(t.persona,""),y.push(`你是专家团「${e.name}」的成员「${t.callName}」${t.duty?`(职责:${t.duty})`:""},正在执行主理人下发的一项独立任务。`,"- 只完成本任务,直接输出可被主理人直接采用的成果本身,不要寒暄、不要复述任务;","- 无法反问,信息不全时按合理假设执行并注明假设。"),r&&y.push("","## 主理人提供的上游产出/背景",String(r)),m&&y.push("","## 交付要求",String(m));let S=y.join("\n"),q=p;if(w.startsWith("agent:")){const e=w.slice(6).trim(),t=i(f).find(t=>t.name===e);t&&(S=t.prompt.trim()+"\n\n"+S)}else if(w.startsWith("model:")){const e=w.slice(6).trim();e&&(q={...p,model:e})}S+="\n工作目录:"+f;const j=[{role:"system",content:S},{role:"user",content:n}],k=await c(p,0,()=>s({config:{...q,aux:!0},messages:j,permissions:o({yolo:!1,ask:null}),cwd:f,extraTools:h,signal:b,depth:1,maxIters:50,io:{write:()=>{}},events:a($,g||`${t.callName}·${String(n).slice(0,40)}`,"member")}),b);if(b?.throwIfAborted(),k?.error)return`成员执行失败:${k.error}`;const D=[...j].reverse().find(e=>"assistant"===e.role&&e.content);return D?x+D.content:"(成员未产出结论)"}export function makeDispatchMemberTool({config:e,cwd:t,extraTools:n=[],onSub:r=null}){const s=loadSquads(t);if(!s.length)return null;const o=n.filter(e=>e.readOnly),i=new Map,a={type:"function",function:{name:"dispatch_member",description:"把一项独立、自足的任务派发给某个专家团的一位成员:成员在隔离上下文中执行并只回传成果。复杂/跨角色任务优先按团开工:对照下方各团「何时使用」自主选团,按其预设工作流分阶段派发(互相独立的任务同一轮多次调用并行);成员产出由你汇总并用本机工具落地,落地后验证。派发前先做任务分配预检(对照成员职责做能力匹配)。\n可用专家团:\n"+s.map(e=>`- ${e.name}:${e.when||e.description||""}(成员 ${e.members.map(e=>e.id).join("/")})`).join("\n"),parameters:{type:"object",properties:{squad:{type:"string",enum:s.map(e=>e.name),description:"专家团名称"},member:{type:"string",description:"成员ID(推荐)或称呼,须来自该团的团队架构表"},task:{type:"string",description:"下发的任务说明——必须独立自足(成员看不到本对话与其他成员产出)"},context:{type:"string",description:"可选:上游产出/背景(跨成员信息流经主理人中转,把必要部分放这里)"},expectation:{type:"string",description:"可选:交付要求/验收标准"}},required:["squad","member","task"]}}},c=async(n,a,c=r)=>{a?.throwIfAborted();const m=s.find(e=>e.name===String(n?.squad||"").trim());if(!m)return`未找到专家团「${n?.squad}」(可用:${s.map(e=>e.name).join("、")})`;const l=function(e,t){const n=String(t||"").trim();return e.members.find(e=>e.id===n)||e.members.find(e=>e.callName===n)||null}(m,n?.member);if(!l)return`专家团「${m.name}」中没有成员「${n?.member}」(可用:${m.members.map(e=>`${e.id}(${e.callName})`).join("、")})`;const p=String(n?.task||"").trim();if(!p)return"缺少task(任务说明)";const u=`${m.name}|${l.id}|${p.slice(0,40)}`,d=i.get(u)||0;if(d>=2)return`该任务线已按异常协议终止(成员「${l.callName}」同一任务累计失败满 2 次)。请向用户说明原因并给出替代方案。`;try{const r=await f({squad:m,member:l,task:p,context:n?.context,expectation:n?.expectation,config:e,cwd:t,readOnlyExtras:o,signal:a,onSub:c,label:`${l.callName}·${p.replace(/\s+/g," ").slice(0,40)}`});return/^(成员执行失败|平台调用失败)/.test(r)?(i.set(u,d+1),`${r}(累计失败 ${d+1}/2,可修正任务说明后重试或改派他人)`):`成员「${l.callName}」对任务【${p.replace(/\s+/g," ").slice(0,60)}】的产出:\n${r}`}catch(e){if(e&&"AbortError"===e.name)throw e;return i.set(u,d+1),`成员「${l.callName}」执行失败:${e.message}(累计失败 ${d+1}/2)`}};return{readOnly:!0,spec:a,run:c,runWithEnv:(e,t,n)=>c(e,n,e=>r?.({...e,callId:t.toolCallId}))}}export function squadCommandPrompt(e,t,n){return buildLeaderPrompt(e,n)+"\n\n---\n\n用户任务:"+t+`\n\n请以主理人${e.leader}的身份完成:先做任务分配预检(含预设工作流匹配),然后经dispatch_member`+`(squad="${e.name}")派发执行(独立任务同轮并行),跨成员信息流经你中转,最后按交付检查清单汇总交付。`}export const SQUAD_TEMPLATE="---\nname: {NAME}\ndescription: 内容创作专家团(示例:改成你的团队)\nleader: 司远\nmode: dispatch\nwhen: 需要成套内容创作/深度文章且涉及调研-写作-审校多角色协作时\n---\n\n擅长把用户的创作需求拆解为具体执行方案,并调度合适的成员高效交付。\n\n## 成员\n- researcher | 调研员 | 资料收集、事实核查、竞品调研 | agent:researcher\n- writer | 笔澜 | 公众号长文、营销文案、脚本 |\n- reviewer | 审校 | 审查产出、给出修订意见 |\n- fetcher | 源泉 | (示例)平台数字人自动绑定查数 | platform:auto:datahuman\n\n## 成员人设\n### writer\n角色:资深中文内容主笔。\n工作方法:先定读者与核心论点,再列结构,后成文;每段只讲一件事;用事实与例子代替空话。\n交付标准:结构清晰可扫读、观点有依据、结尾给行动建议;附 3 条备选标题。\n\n## 预检\n- 写长文/文案 → writer,不要派给researcher\n- 资料/数据收集 → researcher,产出交给writer前先经你提炼要点\n\n## 工作流\n### 深度长文(触发:用户要深度文章/长文)\n1. researcher:收集资料与事实\n2. writer:按资料成稿\n3. reviewer:审查并给修订意见(mode=review时必经)\n交付:终稿Markdown + 三条标题备选\n";
package/src/subagent.js CHANGED
@@ -1 +1 @@
1
- import t from"node:fs";import e from"node:path";import n from"node:os";import{runTurn as r}from"./agent.js";import{createSession as o,saveSession as s,userKeyOf as i}from"./session.js";import*as a from"./jobs.js";import{createPermissions as c}from"./permissions.js";import{parseFrontmatter as l}from"./skills.js";import{getIndex as u}from"./codeindex.js";import{validateAgainstSchema as d}from"./workflow.js";import{VERIFY_DISCIPLINE as g}from"./crew.js";let p=0;const m=[];export async function withSubagentSlot(t,e,n,r){if(r?.throwIfAborted(),e>0)return n();p>=function(t){const e=Number(t?.subagentConcurrency);return Number.isFinite(e)&&e>0?Math.max(1,Math.floor(e)):8}(t)?await new Promise((t,e)=>{const n=()=>{r?.removeEventListener("abort",o),t()},o=()=>{const t=m.indexOf(n);t>=0&&m.splice(t,1),e(r.reason||new DOMException("已取消","AbortError"))};m.push(n),r?.addEventListener("abort",o,{once:!0})}):p++;try{return r?.throwIfAborted(),await n()}finally{const t=m.shift();t?t():p--}}function f(t){try{u(t)}catch{}}export function loadAgents(r){const o=[r?e.join(r,".huicai","agents"):null,e.join(n.homedir(),".huicai","agents")].filter(Boolean),s=new Map;for(const n of o){let r;try{r=t.readdirSync(n).filter(t=>t.endsWith(".md"))}catch{continue}for(const o of r){const r=o.replace(/\.md$/,"").trim();if(r&&!s.has(r))try{const i=t.readFileSync(e.join(n,o),"utf8"),a=l(i);s.set(r,{name:(a.name||r).trim(),description:a.description||"",prompt:a.body||i})}catch{}}}return[...s.values()]}function b(t,e){return t+(e&&e.path?" "+e.path:e&&e.command?": "+String(e.command).split("\n")[0].slice(0,80):e&&e.objective?": "+String(e.objective).slice(0,60):e&&e.query?" "+String(e.query).slice(0,60):e&&e.pattern?" "+e.pattern:"")}export function makeSubEvents(t,e,n){if("function"!=typeof t)return;const r=(r,o)=>{try{t({scope:e,kind:n,type:r,...o})}catch{}};return{onToolStart:(t,e)=>r("tool",{name:t,text:b(t,e)}),onToolResult:(t,e)=>r("toolresult",{name:t,text:String(e).split("\n")[0].slice(0,160)}),onInfo:t=>r("info",{text:String(t).slice(0,200)}),onAssistantText:t=>r("text",{text:String(t).slice(0,400)})}}async function h({config:t,cwd:e,readOnlyExtras:n,depth:o,objective:s,context:i,agentDef:a,onSub:l,label:u,structured:p,signal:m}){if(!s)return"未提供子任务目标objective";const f={...t,aux:!0};t.subagentModel&&t.subagentModel!==t.model&&(f.model=t.subagentModel);const b=a?a.prompt.trim()+"\n\n工作目录:"+e+(i&&String(i).trim()?"\n\n## 背景\n"+i:""):function(t,e){let n="你是一个专注的子智能体,只负责完成主智能体委派的单一子任务。要求:聚焦目标;用可用的只读工具(读文件 / 搜索 / 列目录等)收集信息;直接输出结构清晰、可被主智能体直接复用的最终结论,不要寒暄,也不要复述任务本身。\n工作目录:"+t+"\n";n+="\n"+g+"\n",e&&String(e).trim()&&(n+="\n## 主智能体提供的背景\n"+e+"\n");return n}(e,i),h=p?{value:void 0}:null,y=p?function(t,e){return{readOnly:!0,spec:{type:"function",function:{name:"structured_output",description:"提交本次子任务的结构化结果。这是你的产出方式,调用它之后即可结束。",parameters:t}},run:async n=>{const r=d(n,t);return r.length?"结果不符合要求,请修正后重新调用:\n"+r.join("\n"):(e.value=n,"已记录结构化结果。现在可以结束了。")}}}(p,h):null,S=[{role:"system",content:b},{role:"user",content:p?s+"\n\n完成后必须调用structured_output工具提交结构化结果(这是你唯一的产出方式,不要只把JSON写在正文里)。":s}],x=c({yolo:!1,ask:null}),v=makeSubagentTool({config:f,cwd:e,extraTools:n,depth:o+1,onSub:l}),j=[...n,...v?[v]:[],...y?[y]:[]],w=u||String(s).slice(0,60),$=makeSubEvents(l,w,"agent"),A=(t,e)=>{if("function"==typeof l)try{l({scope:w,kind:"agent",type:t,...e})}catch{}},k=Date.now();A("start",{text:String(s).replace(/\s+/g," ").slice(0,200),agent:a?.name||""});const I=await withSubagentSlot(t,o,()=>r({config:f,messages:S,permissions:x,cwd:e,extraTools:j,depth:o+1,maxIters:t.subagentMaxIters||40,io:{write:()=>{}},events:$,signal:m}),m);m?.throwIfAborted();const _=(t,e)=>A("end",{ok:t,ms:Date.now()-k,text:String(e||"").replace(/\s+/g," ").slice(0,200)});if(I?.error)return _(!1,I.error),p?{ok:!1,error:`子智能体执行失败:${I.error}`}:`子智能体执行失败:${I.error}`;if(p)return void 0===h.value?(_(!1,"未提交结构化结果"),{ok:!1,error:"子智能体没有提交结构化结果(未调用structured_output)。"}):(_(!0,JSON.stringify(h.value)),{ok:!0,value:h.value});const T=[...S].reverse().find(t=>"assistant"===t.role&&t.content),q=T?T.content:"(子智能体未产出结论)";return _(!!T,q),q}export function makeWorkflowDispatcher({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null}){const s=loadAgents(e),i=n.filter(t=>t.readOnly);return async n=>{const a=n.agentType?s.find(t=>t.name===n.agentType):null;if(n.agentType&&!a)return{ok:!1,usage:!0,error:`agent() 指定的agentType「${n.agentType}」不存在。可用:${s.map(t=>t.name).join("、")||"(本项目未定义任何子智能体人格)"}`};f(e);const c=await h({config:t,cwd:e,readOnlyExtras:i,depth:r,objective:n.prompt,context:n.context,agentDef:a,onSub:o,label:n.label,structured:n.schema});if(n.schema)return c.ok?{ok:!0,result:c.value}:{ok:!1,error:c.error};const l=String(c);return l.startsWith("子智能体执行失败:")?{ok:!1,error:l}:{ok:!0,result:l}}}export function makeSubagentTool({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null}){if(r>=2)return null;const s=loadAgents(e),i={type:"function",function:{name:"dispatch_agent",description:"把一个明确、独立、自足的子任务委派给子智能体:它在隔离上下文中自主使用(只读)工具完成后只回传结论。适合可并行的调研/检索/分析类子任务,让主线程上下文保持精简。可多次调用以分解复杂任务。",parameters:{type:"object",properties:{objective:{type:"string",description:"要子智能体完成的单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需的背景信息"},...s.length?{agent:{type:"string",enum:s.map(t=>t.name),description:"可选:使用某个已定义的子智能体人格 — "+s.map(t=>t.name+(t.description?`(${t.description.slice(0,40)})`:"")).join(";")}}:{}},required:["objective"]}}},a=n.filter(t=>t.readOnly),c=async(n,i,c=o)=>{i?.throwIfAborted(),f(e);const l=n?.agent?s.find(t=>t.name===n.agent):null,u=String(n?.objective||"").trim();return h({config:t,cwd:e,readOnlyExtras:a,depth:r,objective:u,context:n?.context,agentDef:l,onSub:c,signal:i,label:u.slice(0,60)})};return{readOnly:!0,spec:i,run:c,runWithEnv:(t,e,n)=>c(t,n,t=>o?.({...t,callId:e.toolCallId}))}}export function makeDispatchAgentsTool({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null}){if(r>=2)return null;const s=loadAgents(e),i=n.filter(t=>t.readOnly),a={objective:{type:"string",description:"单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需背景"},...s.length?{agent:{type:"string",enum:s.map(t=>t.name),description:"可选:指定子智能体人格"}}:{}},c=async(n,a,c=o)=>{a?.throwIfAborted();const l=Array.isArray(n?.tasks)?n.tasks:[],u=l.slice(0,12);if(!u.length)return"未提供tasks(需要一个子任务数组)";const d=l.length-u.length;f(e);const g=String(n?.shared_context||"").trim();if("function"==typeof c)try{c({kind:"agent",type:"batch",text:`并行 ${u.length} 个子任务`,tasks:u.map((t,e)=>({scope:`#${e+1} ${String(t?.objective||"").slice(0,50)}`,objective:String(t?.objective||"").replace(/\s+/g," ").slice(0,200),agent:t?.agent||""}))})}catch{}const p=(await async function(t,e,n){const r=new Array(t.length);let o=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;o<t.length;){const e=o++;r[e]=await n(t[e],e)}})),r}(u,u.length,async(n,o)=>{const l=n?.agent?s.find(t=>t.name===n.agent):null,u=String(n?.objective||"").trim(),d=String(n?.context||"").trim(),p=g&&d?`${g}\n\n${d}`:g||d;try{const s=await h({config:t,cwd:e,readOnlyExtras:i,depth:r,objective:u,context:p,agentDef:l,onSub:c,signal:a,label:`#${o+1} ${u.slice(0,50)}`});return{objective:u.replace(/\s+/g," ").slice(0,80),agent:n?.agent,out:s}}catch(t){if(t&&"AbortError"===t.name)throw t;return{objective:u.replace(/\s+/g," ").slice(0,80),agent:n?.agent,out:`子任务执行异常:${t.message}`}}})).map((t,e)=>`【子任务 ${e+1}${t.agent?" · "+t.agent:""}】${t.objective}\n${t.out}`).join("\n\n---\n\n");return d>0?`${p}\n\n---\n\n(注意:本次提交了 ${l.length} 个子任务,超出上限 12,末尾 ${d} 个未执行,需要的话再发一批。)`:p};return{readOnly:!0,spec:{type:"function",function:{name:"dispatch_agents",description:"一次性把多个**独立、可并行**的子任务同时委派给多个子智能体(隔离上下文、只读工具),并行执行后聚合各自结论。适合把一个较大目标拆成 3-8 个可并行的调研/检索/分析子任务同时推进,显著减少串行等待(上限 12 个)。每个子任务需自足;多个子任务共用的背景放shared_context,不要在每个context里重复抄一遍。复杂任务优先用它一次性铺开,而不是反复单发dispatch_agent串行等待。",parameters:{type:"object",properties:{tasks:{type:"array",description:"并行子任务数组(建议 3-8 个,上限 12)",items:{type:"object",properties:a,required:["objective"]}},shared_context:{type:"string",description:"可选:所有子任务共用的背景(会附加到每个子任务,省去逐条重复)"}},required:["tasks"]}}},run:c,runWithEnv:(t,e,n)=>c(t,n,t=>o?.({...t,callId:e.toolCallId}))}}const y=new Map;function S(t){return{id:t.childId,label:t.label,parentSession:t.parentSessionId,depth:t.depth,status:t.running?"running":t.queue.length?"queued":"idle",job:t.job?.id||"",queued:t.queue.length,turns:t.turns,startedAt:t.startedAt,lastActiveAt:t.lastActiveAt}}function x(t,e){const n=y.get(String(t||""));return n?n.parentSessionId!==String(e||"")?{err:`无权操作子智能体 ${t}:它不是由当前会话派生的`}:{act:n}:{err:`未找到子智能体 ${t}(它可能已结束并被清理,或从未存在)`}}function v(t,e,n){t.queue.push({text:String(e),settle:n}),t.running||async function(t){if(t.running)return;t.running=!0,t.job=a.start({kind:"agent",label:`子智能体 ${t.childId}:${t.label}`,sessionId:t.parentSessionId,cwd:t.cwd,kill:()=>{try{t.abort?.abort()}catch{}},meta:{subagentId:t.childId}});let e=!1,n="",o=0;try{for(;t.queue.length;){const i=t.queue.shift();let a;t.session.messages.push({role:"user",content:i.text}),t.abort=new AbortController,t.turns++,t.lastActiveAt=Date.now();try{a=await withSubagentSlot(t.config,t.depth,()=>r({config:t.config,messages:t.session.messages,permissions:t.permissions,cwd:t.cwd,extraTools:t.extras,depth:t.depth,maxIters:t.config.subagentMaxIters||40,io:{write:()=>{}},events:t.events,signal:t.abort.signal,compaction:t.session.compaction}))}catch(t){a=t&&"AbortError"===t.name?{aborted:!0}:{error:t.message}}t.abort=null,t.lastActiveAt=Date.now();const c=[...t.session.messages].reverse().find(t=>"assistant"===t.role&&t.content);t.lastResult=a?.error?`执行失败:${a.error}`:c?.content||"(未产出结论)";try{t.job?.append(`\n【回合 ${t.turns} 结论】\n${t.lastResult}\n`)}catch{}try{s(t.session)}catch{}if(i.settle&&i.settle(a),o++,a?.error&&(n=a.error),a?.aborted){e=!0;break}}}finally{t.running=!1;const r=t.job;if(t.job=null,r){const s=t.lastResult?String(t.lastResult).replace(/\s+/g," ").slice(0,160):"";e?r.settle("killed",`被打断(${t.queue.length} 条消息仍在排队)`):n?r.settle("failed",n.slice(0,120)):r.settle("done",`${o} 个回合;${s}`)}}}(t).catch(()=>{})}function j({config:t,cwd:e,readOnlyExtras:n,depth:r,objective:a,context:l,agentDef:u,onSub:d,parentSession:g,deliverToParent:p}){const m=String(g?.id||""),f=r+1,h={...t,aux:!0};t.subagentModel&&t.subagentModel!==t.model&&(h.model=t.subagentModel);const x=String(a).replace(/\s+/g," ").slice(0,80),j=u?u.prompt.trim()+"\n\n工作目录:"+e+(l&&String(l).trim()?"\n\n## 背景\n"+l:""):function(t,e){let n="你是一个可持续对话的子智能体:派你来的智能体可能会在你工作过程中追加信息或新指令,你们共享同一个工作目录。要求:聚焦当前收到的指令;用可用的只读工具收集信息;发现会改变对方决策的结论时用report立刻回报,不要攒到最后;输出结构清晰、可直接复用的结论,不寒暄、不复述任务本身。\n工作目录:"+t+"\n";e&&String(e).trim()&&(n+="\n## 背景\n"+e+"\n");return n}(e,l),$=o({cwd:e,model:h.model,messages:[{role:"system",content:j}],parentSession:m,origin:"subagent",delegationDepth:f,label:x,user:g?.user||i(t)||""});$.title="(子智能体) "+x;try{s($)}catch{}h.sessionId=$.id;const A={childId:$.id,parentSessionId:m,label:x,depth:f,cwd:e,config:h,session:$,permissions:c({yolo:!1,ask:null}),queue:[],running:!1,abort:null,turns:0,startedAt:Date.now(),lastActiveAt:Date.now(),lastResult:"",baseEvents:makeSubEvents(d,x,"agent"),job:null,extras:[]},k=makeSubagentTool({config:h,cwd:e,extraTools:n,depth:f,onSub:d});return A.extras=[...n,...k?[k]:[],w(A,p)],A.events=function(t){const e=e=>{try{t.job?.append(e)}catch{}},n=t.baseEvents;return{onToolStart:(t,r)=>{e(`· ${b(t,r)}\n`),n?.onToolStart?.(t,r)},onToolResult:(t,r)=>{e(` → ${String(r).split("\n")[0].slice(0,200)}\n`),n?.onToolResult?.(t,r)},onInfo:t=>{e(` [信息] ${String(t).slice(0,200)}\n`),n?.onInfo?.(t)},onAssistantText:t=>{e(`\n${String(t)}\n`),n?.onAssistantText?.(t)}}}(A),y.set(A.childId,A),v(A,a),{childId:A.childId,view:S(A)}}function w(t,e){return{readOnly:!0,spec:{type:"function",function:{name:"report",description:"把内容回报给派你来的那个智能体。在你**完成前**就可以调用:发现了会改变对方下一步决策的结论时立刻报,不要攒到最后。对方与你共享工作目录,但看不到你的对话过程、工具输出和推理——所以「你把活干完了」本身不等于对方拿到了结果。回报不会结束你的回合,你可以继续做事。",parameters:{type:"object",properties:{output:{type:"string",description:"给对方的自足内容:写清结论,并指出相关的共享路径"}},required:["output"]}}},run:async n=>{const r=String(n?.output||"").trim();return r?"function"!=typeof e?"当前环境不支持向上回报(父智能体不在本进程)。":(e(`【子智能体回报 · ${t.label}(${t.childId})】\n${r}`),"已回报给派你来的智能体。你可以继续做事。"):"未提供output(要回报什么)。"}}}export function makeContinuableAgentTools({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null,session:s=null,queueInject:i=null}){if(r>=2)return[];const a=loadAgents(e),c=n.filter(t=>t.readOnly),l=()=>String(s?.id||t.sessionId||""),u="function"==typeof i?t=>i(t):null,d=(t,e,n={},r=[])=>({type:"function",function:{name:t,description:e,parameters:{type:"object",properties:n,required:r}}});return[{readOnly:!0,spec:d("start_agent","启动一个**可持续对话**的子智能体:它在隔离上下文里自主工作,你可以随后用send_message追加信息或新指令,它也会用report主动向你回报阶段性结论。适合边界会变化、需要来回沟通的长任务。一次性、边界明确的子任务用dispatch_agent更省事(派出去只等一个结论)。本调用立即返回子智能体id,不等它做完——它有进展会主动告诉你,不要为了等它而空转。",{objective:{type:"string",description:"交给它的初始任务(明确、自足)"},context:{type:"string",description:"可选:完成该任务所需的背景信息"},...a.length?{agent:{type:"string",enum:a.map(t=>t.name),description:"可选:使用某个已定义的子智能体人格 — "+a.map(t=>t.name+(t.description?`(${t.description.slice(0,40)})`:"")).join(";")}}:{}},["objective"]),run:async n=>{const i=String(n?.objective||"").trim();if(!i)return"未提供objective(要它做什么)。";if(!l())return"当前没有可归属的会话,无法启动可续子智能体。";f(e);const d=n?.agent?a.find(t=>t.name===n.agent):null,{childId:g,view:p}=j({config:t,cwd:e,readOnlyExtras:c,depth:r,objective:i,context:n?.context,agentDef:d,onSub:o,parentSession:s||{id:l()},deliverToParent:u});return`已启动可续子智能体 ${g}(${p.label})。\n它正在后台工作:有进展会用report主动回报给你,每跑完一段也会有完成通知——不要为了等它而空转。\n用send_message("${g}", …) 追加信息,list_agents查看状态(含它当前作业的id,可用job_output看细节),interrupt_agent打断当前回合。`}},{readOnly:!0,spec:d("list_agents","列出你启动的可续子智能体及其状态(运行中 / 排队中 / 空闲)、已跑回合数与最近一次结论摘要。"),run:async()=>{const t=[...y.values()].filter(t=>t.parentSessionId===l());return t.length?"你的可续子智能体:\n"+t.map(t=>{const e=S(t),n={running:"运行中",queued:"有排队消息",idle:"空闲(可继续对话)"}[e.status],r=t.lastResult?"\n 最近结论:"+String(t.lastResult).replace(/\s+/g," ").slice(0,120):"",o=e.job?`\n 进行中的作业 ${e.job}(job_output可看它当前在做什么)`:"";return`· ${e.id} [${n}] 已跑 ${e.turns} 回合${e.queued?`,${e.queued} 条待处理`:""} ${e.label}${o}${r}`}).join("\n"):"你还没有启动过可续子智能体(用start_agent启动)。"}},{readOnly:!1,spec:d("send_message","给一个可续子智能体追加消息,继续同一段对话。它会成为该子智能体的下一个回合:如果它正在忙,消息会排队等当前回合结束——**因此这不能改变它正在做的事**。本调用只确认送达,不返回它的答复;它的结论会经report回到你这里。",{subagent_id:{type:"string",description:"start_agent返回的子智能体id"},message:{type:"string",description:"要交给它的新信息或新指令"}},["subagent_id","message"]),run:async t=>{const e=String(t?.subagent_id||"").trim(),n=String(t?.message||"").trim();if(!e||!n)return"需要subagent_id与message。";const{act:r,err:o}=x(e,l());return o||(v(r,n),r.running?`已送达 ${e}(它正在忙,这条会在当前回合结束后处理;排队 ${r.queue.length} 条)。`:`已送达 ${e},它已开始处理。`)}},{readOnly:!1,spec:d("interrupt_agent","打断一个可续子智能体**当前正在跑的那个回合**。已排队但还没开始的消息会原地保留,它本身仍然可用——之后再send_message就会继续。用于「方向错了,先停下」。",{subagent_id:{type:"string",description:"要打断的子智能体id"}},["subagent_id"]),run:async t=>{const e=String(t?.subagent_id||"").trim();if(!e)return"需要subagent_id。";const{act:n,err:r}=x(e,l());return r||(n.running&&n.abort?(n.abort.abort(),`已请求打断 ${e} 的当前回合(排队中的 ${n.queue.length} 条消息保留,它仍可继续对话)。`):`子智能体 ${e} 当前没有在跑的回合,无需打断。`)}}]}export function resetActivations(){for(const t of y.values())try{t.abort?.abort()}catch{}y.clear()}export function listActivations(t){const e=[...y.values()];return(void 0===t?e:e.filter(e=>e.parentSessionId===String(t||""))).map(S)}
1
+ import t from"node:fs";import{accountDataDir as e}from"./account-scope.js";import n from"node:path";import{runTurn as r}from"./agent.js";import{createSession as o,saveSession as s,userKeyOf as i}from"./session.js";import*as a from"./jobs.js";import{createPermissions as c}from"./permissions.js";import{parseFrontmatter as l}from"./skills.js";import{getIndex as u}from"./codeindex.js";import{validateAgainstSchema as d}from"./workflow.js";import{VERIFY_DISCIPLINE as g}from"./crew.js";let p=0;const f=[];export async function withSubagentSlot(t,e,n,r){if(r?.throwIfAborted(),e>0)return n();p>=function(t){const e=Number(t?.subagentConcurrency);return Number.isFinite(e)&&e>0?Math.max(1,Math.floor(e)):8}(t)?await new Promise((t,e)=>{const n=()=>{r?.removeEventListener("abort",o),t()},o=()=>{const t=f.indexOf(n);t>=0&&f.splice(t,1),e(r.reason||new DOMException("已取消","AbortError"))};f.push(n),r?.addEventListener("abort",o,{once:!0})}):p++;try{return r?.throwIfAborted(),await n()}finally{const t=f.shift();t?t():p--}}function m(t){try{u(t)}catch{}}export function loadAgents(r){const o=[r?n.join(r,".huicai","agents"):null,n.join(e(),"agents")].filter(Boolean),s=new Map;for(const e of o){let r;try{r=t.readdirSync(e).filter(t=>t.endsWith(".md"))}catch{continue}for(const o of r){const r=o.replace(/\.md$/,"").trim();if(r&&!s.has(r))try{const i=t.readFileSync(n.join(e,o),"utf8"),a=l(i);s.set(r,{name:(a.name||r).trim(),description:a.description||"",prompt:a.body||i})}catch{}}}return[...s.values()]}function b(t,e){return t+(e&&e.path?" "+e.path:e&&e.command?": "+String(e.command).split("\n")[0].slice(0,80):e&&e.objective?": "+String(e.objective).slice(0,60):e&&e.query?" "+String(e.query).slice(0,60):e&&e.pattern?" "+e.pattern:"")}export function makeSubEvents(t,e,n){if("function"!=typeof t)return;const r=(r,o)=>{try{t({scope:e,kind:n,type:r,...o})}catch{}};return{onToolStart:(t,e)=>r("tool",{name:t,text:b(t,e)}),onToolResult:(t,e)=>r("toolresult",{name:t,text:String(e).split("\n")[0].slice(0,160)}),onInfo:t=>r("info",{text:String(t).slice(0,200)}),onAssistantText:t=>r("text",{text:String(t).slice(0,400)})}}async function y({config:t,cwd:e,readOnlyExtras:n,depth:o,objective:s,context:i,agentDef:a,onSub:l,label:u,structured:p,signal:f}){if(!s)return"未提供子任务目标objective";const m={...t,aux:!0};t.subagentModel&&t.subagentModel!==t.model&&(m.model=t.subagentModel);const b=a?a.prompt.trim()+"\n\n工作目录:"+e+(i&&String(i).trim()?"\n\n## 背景\n"+i:""):function(t,e){let n="你是一个专注的子智能体,只负责完成主智能体委派的单一子任务。要求:聚焦目标;用可用的只读工具(读文件 / 搜索 / 列目录等)收集信息;直接输出结构清晰、可被主智能体直接复用的最终结论,不要寒暄,也不要复述任务本身。\n工作目录:"+t+"\n";n+="\n"+g+"\n",e&&String(e).trim()&&(n+="\n## 主智能体提供的背景\n"+e+"\n");return n}(e,i),y=p?{value:void 0}:null,h=p?function(t,e){return{readOnly:!0,spec:{type:"function",function:{name:"structured_output",description:"提交本次子任务的结构化结果。这是你的产出方式,调用它之后即可结束。",parameters:t}},run:async n=>{const r=d(n,t);return r.length?"结果不符合要求,请修正后重新调用:\n"+r.join("\n"):(e.value=n,"已记录结构化结果。现在可以结束了。")}}}(p,y):null,S=[{role:"system",content:b},{role:"user",content:p?s+"\n\n完成后必须调用structured_output工具提交结构化结果(这是你唯一的产出方式,不要只把JSON写在正文里)。":s}],x=c({yolo:!1,ask:null}),v=makeSubagentTool({config:m,cwd:e,extraTools:n,depth:o+1,onSub:l}),j=[...n,...v?[v]:[],...h?[h]:[]],w=u||String(s).slice(0,60),$=makeSubEvents(l,w,"agent"),A=(t,e)=>{if("function"==typeof l)try{l({scope:w,kind:"agent",type:t,...e})}catch{}},k=Date.now();A("start",{text:String(s).replace(/\s+/g," ").slice(0,200),agent:a?.name||""});const I=await withSubagentSlot(t,o,()=>r({config:m,messages:S,permissions:x,cwd:e,extraTools:j,depth:o+1,maxIters:t.subagentMaxIters||40,io:{write:()=>{}},events:$,signal:f}),f);f?.throwIfAborted();const _=(t,e)=>A("end",{ok:t,ms:Date.now()-k,text:String(e||"").replace(/\s+/g," ").slice(0,200)});if(I?.error)return _(!1,I.error),p?{ok:!1,error:`子智能体执行失败:${I.error}`}:`子智能体执行失败:${I.error}`;if(p)return void 0===y.value?(_(!1,"未提交结构化结果"),{ok:!1,error:"子智能体没有提交结构化结果(未调用structured_output)。"}):(_(!0,JSON.stringify(y.value)),{ok:!0,value:y.value});const T=[...S].reverse().find(t=>"assistant"===t.role&&t.content),q=T?T.content:"(子智能体未产出结论)";return _(!!T,q),q}export function makeWorkflowDispatcher({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null}){const s=loadAgents(e),i=n.filter(t=>t.readOnly);return async n=>{const a=n.agentType?s.find(t=>t.name===n.agentType):null;if(n.agentType&&!a)return{ok:!1,usage:!0,error:`agent() 指定的agentType「${n.agentType}」不存在。可用:${s.map(t=>t.name).join("、")||"(本项目未定义任何子智能体人格)"}`};m(e);const c=await y({config:t,cwd:e,readOnlyExtras:i,depth:r,objective:n.prompt,context:n.context,agentDef:a,onSub:o,label:n.label,structured:n.schema});if(n.schema)return c.ok?{ok:!0,result:c.value}:{ok:!1,error:c.error};const l=String(c);return l.startsWith("子智能体执行失败:")?{ok:!1,error:l}:{ok:!0,result:l}}}export function makeSubagentTool({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null}){if(r>=2)return null;const s=loadAgents(e),i={type:"function",function:{name:"dispatch_agent",description:"把一个明确、独立、自足的子任务委派给子智能体:它在隔离上下文中自主使用(只读)工具完成后只回传结论。适合可并行的调研/检索/分析类子任务,让主线程上下文保持精简。可多次调用以分解复杂任务。",parameters:{type:"object",properties:{objective:{type:"string",description:"要子智能体完成的单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需的背景信息"},...s.length?{agent:{type:"string",enum:s.map(t=>t.name),description:"可选:使用某个已定义的子智能体人格 — "+s.map(t=>t.name+(t.description?`(${t.description.slice(0,40)})`:"")).join(";")}}:{}},required:["objective"]}}},a=n.filter(t=>t.readOnly),c=async(n,i,c=o)=>{i?.throwIfAborted(),m(e);const l=n?.agent?s.find(t=>t.name===n.agent):null,u=String(n?.objective||"").trim();return y({config:t,cwd:e,readOnlyExtras:a,depth:r,objective:u,context:n?.context,agentDef:l,onSub:c,signal:i,label:u.slice(0,60)})};return{readOnly:!0,spec:i,run:c,runWithEnv:(t,e,n)=>c(t,n,t=>o?.({...t,callId:e.toolCallId}))}}export function makeDispatchAgentsTool({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null}){if(r>=2)return null;const s=loadAgents(e),i=n.filter(t=>t.readOnly),a={objective:{type:"string",description:"单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需背景"},...s.length?{agent:{type:"string",enum:s.map(t=>t.name),description:"可选:指定子智能体人格"}}:{}},c=async(n,a,c=o)=>{a?.throwIfAborted();const l=Array.isArray(n?.tasks)?n.tasks:[],u=l.slice(0,12);if(!u.length)return"未提供tasks(需要一个子任务数组)";const d=l.length-u.length;m(e);const g=String(n?.shared_context||"").trim();if("function"==typeof c)try{c({kind:"agent",type:"batch",text:`并行 ${u.length} 个子任务`,tasks:u.map((t,e)=>({scope:`#${e+1} ${String(t?.objective||"").slice(0,50)}`,objective:String(t?.objective||"").replace(/\s+/g," ").slice(0,200),agent:t?.agent||""}))})}catch{}const p=(await async function(t,e,n){const r=new Array(t.length);let o=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;o<t.length;){const e=o++;r[e]=await n(t[e],e)}})),r}(u,u.length,async(n,o)=>{const l=n?.agent?s.find(t=>t.name===n.agent):null,u=String(n?.objective||"").trim(),d=String(n?.context||"").trim(),p=g&&d?`${g}\n\n${d}`:g||d;try{const s=await y({config:t,cwd:e,readOnlyExtras:i,depth:r,objective:u,context:p,agentDef:l,onSub:c,signal:a,label:`#${o+1} ${u.slice(0,50)}`});return{objective:u.replace(/\s+/g," ").slice(0,80),agent:n?.agent,out:s}}catch(t){if(t&&"AbortError"===t.name)throw t;return{objective:u.replace(/\s+/g," ").slice(0,80),agent:n?.agent,out:`子任务执行异常:${t.message}`}}})).map((t,e)=>`【子任务 ${e+1}${t.agent?" · "+t.agent:""}】${t.objective}\n${t.out}`).join("\n\n---\n\n");return d>0?`${p}\n\n---\n\n(注意:本次提交了 ${l.length} 个子任务,超出上限 12,末尾 ${d} 个未执行,需要的话再发一批。)`:p};return{readOnly:!0,spec:{type:"function",function:{name:"dispatch_agents",description:"一次性把多个**独立、可并行**的子任务同时委派给多个子智能体(隔离上下文、只读工具),并行执行后聚合各自结论。适合把一个较大目标拆成 3-8 个可并行的调研/检索/分析子任务同时推进,显著减少串行等待(上限 12 个)。每个子任务需自足;多个子任务共用的背景放shared_context,不要在每个context里重复抄一遍。复杂任务优先用它一次性铺开,而不是反复单发dispatch_agent串行等待。",parameters:{type:"object",properties:{tasks:{type:"array",description:"并行子任务数组(建议 3-8 个,上限 12)",items:{type:"object",properties:a,required:["objective"]}},shared_context:{type:"string",description:"可选:所有子任务共用的背景(会附加到每个子任务,省去逐条重复)"}},required:["tasks"]}}},run:c,runWithEnv:(t,e,n)=>c(t,n,t=>o?.({...t,callId:e.toolCallId}))}}const h=new Map;function S(t){return{id:t.childId,label:t.label,parentSession:t.parentSessionId,depth:t.depth,status:t.running?"running":t.queue.length?"queued":"idle",job:t.job?.id||"",queued:t.queue.length,turns:t.turns,startedAt:t.startedAt,lastActiveAt:t.lastActiveAt}}function x(t,e){const n=h.get(String(t||""));return n?n.parentSessionId!==String(e||"")?{err:`无权操作子智能体 ${t}:它不是由当前会话派生的`}:{act:n}:{err:`未找到子智能体 ${t}(它可能已结束并被清理,或从未存在)`}}function v(t,e,n){t.queue.push({text:String(e),settle:n}),t.running||async function(t){if(t.running)return;t.running=!0,t.job=a.start({kind:"agent",label:`子智能体 ${t.childId}:${t.label}`,sessionId:t.parentSessionId,cwd:t.cwd,kill:()=>{try{t.abort?.abort()}catch{}},meta:{subagentId:t.childId}});let e=!1,n="",o=0;try{for(;t.queue.length;){const i=t.queue.shift();let a;t.session.messages.push({role:"user",content:i.text}),t.abort=new AbortController,t.turns++,t.lastActiveAt=Date.now();try{a=await withSubagentSlot(t.config,t.depth,()=>r({config:t.config,messages:t.session.messages,permissions:t.permissions,cwd:t.cwd,extraTools:t.extras,depth:t.depth,maxIters:t.config.subagentMaxIters||40,io:{write:()=>{}},events:t.events,signal:t.abort.signal,compaction:t.session.compaction}))}catch(t){a=t&&"AbortError"===t.name?{aborted:!0}:{error:t.message}}t.abort=null,t.lastActiveAt=Date.now();const c=[...t.session.messages].reverse().find(t=>"assistant"===t.role&&t.content);t.lastResult=a?.error?`执行失败:${a.error}`:c?.content||"(未产出结论)";try{t.job?.append(`\n【回合 ${t.turns} 结论】\n${t.lastResult}\n`)}catch{}try{s(t.session)}catch{}if(i.settle&&i.settle(a),o++,a?.error&&(n=a.error),a?.aborted){e=!0;break}}}finally{t.running=!1;const r=t.job;if(t.job=null,r){const s=t.lastResult?String(t.lastResult).replace(/\s+/g," ").slice(0,160):"";e?r.settle("killed",`被打断(${t.queue.length} 条消息仍在排队)`):n?r.settle("failed",n.slice(0,120)):r.settle("done",`${o} 个回合;${s}`)}}}(t).catch(()=>{})}function j({config:t,cwd:e,readOnlyExtras:n,depth:r,objective:a,context:l,agentDef:u,onSub:d,parentSession:g,deliverToParent:p}){const f=String(g?.id||""),m=r+1,y={...t,aux:!0};t.subagentModel&&t.subagentModel!==t.model&&(y.model=t.subagentModel);const x=String(a).replace(/\s+/g," ").slice(0,80),j=u?u.prompt.trim()+"\n\n工作目录:"+e+(l&&String(l).trim()?"\n\n## 背景\n"+l:""):function(t,e){let n="你是一个可持续对话的子智能体:派你来的智能体可能会在你工作过程中追加信息或新指令,你们共享同一个工作目录。要求:聚焦当前收到的指令;用可用的只读工具收集信息;发现会改变对方决策的结论时用report立刻回报,不要攒到最后;输出结构清晰、可直接复用的结论,不寒暄、不复述任务本身。\n工作目录:"+t+"\n";e&&String(e).trim()&&(n+="\n## 背景\n"+e+"\n");return n}(e,l),$=o({cwd:e,model:y.model,messages:[{role:"system",content:j}],parentSession:f,origin:"subagent",delegationDepth:m,label:x,user:g?.user||i(t)||""});$.title="(子智能体) "+x;try{s($)}catch{}y.sessionId=$.id;const A={childId:$.id,parentSessionId:f,label:x,depth:m,cwd:e,config:y,session:$,permissions:c({yolo:!1,ask:null}),queue:[],running:!1,abort:null,turns:0,startedAt:Date.now(),lastActiveAt:Date.now(),lastResult:"",baseEvents:makeSubEvents(d,x,"agent"),job:null,extras:[]},k=makeSubagentTool({config:y,cwd:e,extraTools:n,depth:m,onSub:d});return A.extras=[...n,...k?[k]:[],w(A,p)],A.events=function(t){const e=e=>{try{t.job?.append(e)}catch{}},n=t.baseEvents;return{onToolStart:(t,r)=>{e(`· ${b(t,r)}\n`),n?.onToolStart?.(t,r)},onToolResult:(t,r)=>{e(` → ${String(r).split("\n")[0].slice(0,200)}\n`),n?.onToolResult?.(t,r)},onInfo:t=>{e(` [信息] ${String(t).slice(0,200)}\n`),n?.onInfo?.(t)},onAssistantText:t=>{e(`\n${String(t)}\n`),n?.onAssistantText?.(t)}}}(A),h.set(A.childId,A),v(A,a),{childId:A.childId,view:S(A)}}function w(t,e){return{readOnly:!0,spec:{type:"function",function:{name:"report",description:"把内容回报给派你来的那个智能体。在你**完成前**就可以调用:发现了会改变对方下一步决策的结论时立刻报,不要攒到最后。对方与你共享工作目录,但看不到你的对话过程、工具输出和推理——所以「你把活干完了」本身不等于对方拿到了结果。回报不会结束你的回合,你可以继续做事。",parameters:{type:"object",properties:{output:{type:"string",description:"给对方的自足内容:写清结论,并指出相关的共享路径"}},required:["output"]}}},run:async n=>{const r=String(n?.output||"").trim();return r?"function"!=typeof e?"当前环境不支持向上回报(父智能体不在本进程)。":(e(`【子智能体回报 · ${t.label}(${t.childId})】\n${r}`),"已回报给派你来的智能体。你可以继续做事。"):"未提供output(要回报什么)。"}}}export function makeContinuableAgentTools({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null,session:s=null,queueInject:i=null}){if(r>=2)return[];const a=loadAgents(e),c=n.filter(t=>t.readOnly),l=()=>String(s?.id||t.sessionId||""),u="function"==typeof i?t=>i(t):null,d=(t,e,n={},r=[])=>({type:"function",function:{name:t,description:e,parameters:{type:"object",properties:n,required:r}}});return[{readOnly:!0,spec:d("start_agent","启动一个**可持续对话**的子智能体:它在隔离上下文里自主工作,你可以随后用send_message追加信息或新指令,它也会用report主动向你回报阶段性结论。适合边界会变化、需要来回沟通的长任务。一次性、边界明确的子任务用dispatch_agent更省事(派出去只等一个结论)。本调用立即返回子智能体id,不等它做完——它有进展会主动告诉你,不要为了等它而空转。",{objective:{type:"string",description:"交给它的初始任务(明确、自足)"},context:{type:"string",description:"可选:完成该任务所需的背景信息"},...a.length?{agent:{type:"string",enum:a.map(t=>t.name),description:"可选:使用某个已定义的子智能体人格 — "+a.map(t=>t.name+(t.description?`(${t.description.slice(0,40)})`:"")).join(";")}}:{}},["objective"]),run:async n=>{const i=String(n?.objective||"").trim();if(!i)return"未提供objective(要它做什么)。";if(!l())return"当前没有可归属的会话,无法启动可续子智能体。";m(e);const d=n?.agent?a.find(t=>t.name===n.agent):null,{childId:g,view:p}=j({config:t,cwd:e,readOnlyExtras:c,depth:r,objective:i,context:n?.context,agentDef:d,onSub:o,parentSession:s||{id:l()},deliverToParent:u});return`已启动可续子智能体 ${g}(${p.label})。\n它正在后台工作:有进展会用report主动回报给你,每跑完一段也会有完成通知——不要为了等它而空转。\n用send_message("${g}", …) 追加信息,list_agents查看状态(含它当前作业的id,可用job_output看细节),interrupt_agent打断当前回合。`}},{readOnly:!0,spec:d("list_agents","列出你启动的可续子智能体及其状态(运行中 / 排队中 / 空闲)、已跑回合数与最近一次结论摘要。"),run:async()=>{const t=[...h.values()].filter(t=>t.parentSessionId===l());return t.length?"你的可续子智能体:\n"+t.map(t=>{const e=S(t),n={running:"运行中",queued:"有排队消息",idle:"空闲(可继续对话)"}[e.status],r=t.lastResult?"\n 最近结论:"+String(t.lastResult).replace(/\s+/g," ").slice(0,120):"",o=e.job?`\n 进行中的作业 ${e.job}(job_output可看它当前在做什么)`:"";return`· ${e.id} [${n}] 已跑 ${e.turns} 回合${e.queued?`,${e.queued} 条待处理`:""} ${e.label}${o}${r}`}).join("\n"):"你还没有启动过可续子智能体(用start_agent启动)。"}},{readOnly:!1,spec:d("send_message","给一个可续子智能体追加消息,继续同一段对话。它会成为该子智能体的下一个回合:如果它正在忙,消息会排队等当前回合结束——**因此这不能改变它正在做的事**。本调用只确认送达,不返回它的答复;它的结论会经report回到你这里。",{subagent_id:{type:"string",description:"start_agent返回的子智能体id"},message:{type:"string",description:"要交给它的新信息或新指令"}},["subagent_id","message"]),run:async t=>{const e=String(t?.subagent_id||"").trim(),n=String(t?.message||"").trim();if(!e||!n)return"需要subagent_id与message。";const{act:r,err:o}=x(e,l());return o||(v(r,n),r.running?`已送达 ${e}(它正在忙,这条会在当前回合结束后处理;排队 ${r.queue.length} 条)。`:`已送达 ${e},它已开始处理。`)}},{readOnly:!1,spec:d("interrupt_agent","打断一个可续子智能体**当前正在跑的那个回合**。已排队但还没开始的消息会原地保留,它本身仍然可用——之后再send_message就会继续。用于「方向错了,先停下」。",{subagent_id:{type:"string",description:"要打断的子智能体id"}},["subagent_id"]),run:async t=>{const e=String(t?.subagent_id||"").trim();if(!e)return"需要subagent_id。";const{act:n,err:r}=x(e,l());return r||(n.running&&n.abort?(n.abort.abort(),`已请求打断 ${e} 的当前回合(排队中的 ${n.queue.length} 条消息保留,它仍可继续对话)。`):`子智能体 ${e} 当前没有在跑的回合,无需打断。`)}}]}export function resetActivations(){for(const t of h.values())try{t.abort?.abort()}catch{}h.clear()}export function listActivations(t){const e=[...h.values()];return(void 0===t?e:e.filter(e=>e.parentSessionId===String(t||""))).map(S)}