@huicai/cli 2026.1021.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.
- package/bin/huicai.js +45 -10
- package/package.json +2 -2
- package/src/account-scope.js +1 -0
- package/src/agent.js +1 -1
- package/src/assets-import.js +1 -1
- package/src/automation-sync.js +1 -0
- package/src/codeindex.js +1 -1
- package/src/commands.js +1 -1
- package/src/config.js +1 -1
- package/src/context.js +1 -1
- package/src/daemon.js +1 -1
- package/src/decisions.js +1 -1
- package/src/doccache.js +1 -1
- package/src/experts-store.js +1 -1
- package/src/experts.js +1 -1
- package/src/input.js +1 -1
- package/src/lsp.js +1 -1
- package/src/mcp.js +1 -1
- package/src/memory.js +1 -1
- package/src/plan-limits.js +1 -1
- package/src/privatedir.js +1 -1
- package/src/repl.js +1 -1
- package/src/schedule.js +1 -1
- package/src/session.js +1 -1
- package/src/sessionindex.js +1 -1
- package/src/skills.js +1 -1
- package/src/squad.js +1 -1
- package/src/subagent.js +1 -1
- package/src/toolpipe.js +1 -1
- package/src/uninstall.js +1 -1
- package/src/workflow.js +1 -1
- package/src/worklog.js +1 -1
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}">\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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}">\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)}
|
package/src/toolpipe.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import r from"node:path";import{runTool as
|
|
1
|
+
import e from"node:fs";import{assertAccountOperation as r}from"./account-scope.js";import t from"node:path";import{runTool as n,toolTimeoutMs as o,toolTimeoutText as s}from"./tools.js";import{runHooks as i}from"./hooks.js";import{isFileWriteTool as a}from"./permissions.js";import{capOrSpill as l,MAX_TOOL_RESULT_CHARS as c}from"./spill.js";import{toolResultCharBudget as u}from"./context-window.js";import{queryOf as f}from"./crush.js";import{guardOverwrite as d,noteAfterCall as m}from"./fs-observe.js";import{engineeringStage as g}from"./engineering.js";export function looksBinary(e){return!!e&&e.subarray(0,8e3).includes(0)}export function isBinaryFile(r){let t;try{t=e.openSync(r,"r");const n=Buffer.alloc(8e3),o=e.readSync(t,n,0,n.length,0);return looksBinary(n.subarray(0,o))}catch{return!1}finally{if(void 0!==t)try{e.closeSync(t)}catch{}}}export async function withToolDeadline(e,r,t,n){if(!(r>0))return e(n);const o=new AbortController,i=()=>o.abort();n&&(n.aborted?o.abort():n.addEventListener("abort",i,{once:!0}));let a=null;const l=new Promise(e=>{a=setTimeout(()=>{o.abort(),e({timedOut:!0})},r)});try{const n=await Promise.race([e(o.signal).then(e=>({value:e}),e=>({err:e})),l]);if(n.timedOut)return s(t,r);if(n.err)throw n.err;return n.value}finally{a&&clearTimeout(a),n?.removeEventListener?.("abort",i)}}const p={name:"validate",before(e){e.argsError&&(e.result=e.argsError,e.settled=!0)}},h={name:"authorize",async before(e,r){const t=await r.permissions.request(e.name,e.args,r.cwd,e.readOnly||e.deferToIde);t.policyRule&&(e.policyRule=t.policyRule),t.allow||(e.denied=t.reason||"未授权,未执行。")}};function y(e,r){return{cwd:r.cwd,toolName:e.name,args:e.args,callId:e.callId||"",sessionId:r.config?.sessionId||"",model:r.config?.model||"",turn:r.turn??""}}const w={name:"hooks",skipReadOnly:!0,before(e,r){if(!r.hooksActive)return;const t=i("PreToolUse",{...y(e,r)},r.hooks);for(const e of t.outputs)r.info("钩子·PreToolUse:"+r.shortLine(e.stdout,120));t.block?e.denied="PreToolUse钩子阻断:"+t.reason:t.modifiedArgs&&(e.args={...e.args,...t.modifiedArgs},r.info("钩子·PreToolUse已修改工具参数"))},after(e,r){if(!r.hooksActive)return;const t=i("PostToolUse",{...y(e,r),result:String(e.result)},r.hooks);if(t.outputs.length){for(const e of t.outputs)r.info("钩子·PostToolUse:"+r.shortLine(e.stdout,120));e.result=String(e.result)+"\n\n[PostToolUse钩子输出]\n"+t.outputs.map(e=>e.stdout).join("\n")}}},b={name:"journal",skipReadOnly:!0,before(r,n){if(!a(r.name)||!r.args?.path)return;const o=String(r.args.path),s=t.resolve(n.cwd,o);let i=null;try{i=e.readFileSync(s)}catch{}const l=looksBinary(i);if(r.meta.before=l?null:i?i.toString("utf8"):"",!n.fileJournal)return;const c=!!i||e.existsSync(s);i&&i.length>2097152?n.fileJournal.push({path:o,tooLarge:!0}):l?n.fileJournal.push({path:o,before:i.toString("base64"),encoding:"base64",existed:c}):n.fileJournal.push({path:o,before:r.meta.before,existed:c})}},S={name:"inbound-scan",async after(e,r){if(!1===r.config?.inboundScan)return;const t=function(e){const r=e.extra;if(r?.mcpServer)return r.mcpOrigin?`${r.mcpServer}@${r.mcpOrigin}`:`MCP:${r.mcpServer}`;if(r?.networkSource)return String(r.networkSource);if(v.has(e.name)){const r=e.args?.url||e.args?.uri||"";let t="";try{t=r?new URL(String(r)).host:""}catch{t=""}return t||e.name}return null}(e);if(!t)return;const n=String(e.result??"");if(!n.trim())return;const{scanUntrustedText:o,fenceUntrusted:s}=await import("./skill-scan.js"),i=o(n,{source:t,label:e.name});if(i.ok){if(e.meta.inboundScan={source:t,verdict:i.verdict,counts:i.counts},i.counts.critical){const o=i.findings.filter(e=>"critical"===e.severity).slice(0,3).map(e=>e.message).join(";");return r.info(`已给来自 ${t} 的结果加不可信围栏:命中 ${i.counts.critical} 处注入话术(${o})`),void(e.result=s(n,t))}i.counts.warn&&r.config?.debug&&r.info(`来自 ${t} 的结果含 ${i.counts.warn} 处可疑表述(未拦截,仅记录)`)}}};const v=new Set(["fetch_url"]),k={name:"observe",before(e,r){if(!1===r.config?.readBeforeWrite)return;const t=d({sessionId:r.config?.sessionId||"",cwd:r.cwd,name:e.name,args:e.args});t&&(e.denied=t)},after(e,r){const t=/^工具执行失败|^读取失败|不存在/.test(String(e.result??""));m({sessionId:r.config?.sessionId||"",cwd:r.cwd,name:e.name,args:e.args,failed:t})}};export const DEFAULT_STAGES=[p,k,h,w,g,b,S];export async function runToolCall(e,t){e.meta=e.meta||{},e.denied=null,e.settled=!1;const s=(t.stages||DEFAULT_STAGES).filter(r=>!(r.skipReadOnly&&e.readOnly)),i=[];for(const r of s){if(r.before){if(await r.before(e,t),null!=e.denied)return e;if(e.settled)return e}i.push(r)}t.onStart?.(e),e.result=await async function(e,t){const{config:s,cwd:i,signal:a}=t;let d;try{r(),d=await withToolDeadline(r=>e.extra?"function"==typeof e.extra.runWithEnv?e.extra.runWithEnv(e.args,{...t,toolCallId:e.callId},r):e.extra.run(e.args):n(e.name,e.args,i,s,r),o(e.name,e.extra),e.name,a)}catch(e){d=`工具执行失败:${e.message}`}return e.nested?String(d):l(String(d),{sessionId:s.sessionId||"",toolName:e.name,callId:e.callId,enabled:!1!==s.spill,maxChars:u(s,c),query:f(e.name,e.args),crush:!1!==s.crush})}(e,t);for(let r=i.length-1;r>=0;r--)i[r].after&&await i[r].after(e,t);return e}export function makeDispatch(e){return async(r,t)=>{const n=e.resolveTool?e.resolveTool(r):null;if(!n)return{ok:!1,denied:`未知工具:${r}`};const o=await runToolCall({name:r,args:t||{},readOnly:!!n.readOnly,extra:n.extra,callId:`nested-${r}-${Date.now().toString(36)}`,nested:!0},e);return null!=o.denied?{ok:!1,denied:o.denied}:{ok:!0,result:String(o.result??"")}}}
|
package/src/uninstall.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import t from"node:path";import{CONFIG_DIR as o}from"./config.js";function s(o){let r=0,n=[];try{n=e.readdirSync(o,{withFileTypes:!0})}catch{return 0}for(const i of n){const n=t.join(o,i.name);try{r+=i.isDirectory()?s(n):e.statSync(n).size}catch{}}return r}export const DATA_LABELS={"config.json":"配置与登录令牌",sessions:"会话历史",spill:"工具超长输出的全文备份",jobs:"后台作业日志",cache:"检索索引等缓存(可再生)",memory:"长期记忆",skills:"本地技能",agents:"子智能体人格",squads:"专家团",commands:"自定义斜杠命令",decisions:"决策图谱",schedule:"定时任务","schedule.json":"定时任务",AutoTasks:"自动化任务",daemon:"调度器运行状态",forks:"后台任务日志",workflows:"工作流运行记录(脚本原文与每个子智能体的产出,供复盘与续跑)","desktop-chats":"桌面端对话","history.json":"命令行输入历史","mcp.json":"MCP服务配置","idea-host":"IDEA插件宿主",".anonymous-user-id":"匿名标识"};export function labelOf(e){return DATA_LABELS[e]||""}export function describeData(){const r=o;if(!e.existsSync(r))return{exists:!1,dir:r,entries:[],total:0};const n=[];let i=0;for(const o of e.readdirSync(r,{withFileTypes:!0})){const c=t.join(r,o.name);let a=0;try{a=o.isDirectory()?s(c):e.statSync(c).size}catch{}i+=a,n.push({name:o.name,kind:o.isDirectory()?"dir":"file",size:a,label:labelOf(o.name)})}return n.sort((e,t)=>e.name.localeCompare(t.name)),{exists:!0,dir:r,entries:n,total:i}}export function purgeData(){const t=o;return e.existsSync(t)?(e.rmSync(t,{recursive:!0,force:!0}),{removed:!0,dir:t}):{removed:!1,dir:t}}export function fmtSize(e){return e<1024?e+"B":e<1048576?(e/1024).toFixed(1)+"KB":(e/1024/1024).toFixed(1)+"MB"}
|
|
1
|
+
import e from"node:fs";import t from"node:path";import{CONFIG_DIR as o}from"./config.js";function s(o){let r=0,n=[];try{n=e.readdirSync(o,{withFileTypes:!0})}catch{return 0}for(const i of n){const n=t.join(o,i.name);try{r+=i.isDirectory()?s(n):e.statSync(n).size}catch{}}return r}export const DATA_LABELS={"config.json":"配置与登录令牌",sessions:"会话历史",accounts:"各账号私密数据(自动化、技能、MCP凭据、记忆、后台作业日志与工具超长输出的全文备份)","automation-device-id":"本机自动化终端标识",spill:"工具超长输出的全文备份",jobs:"后台作业日志",cache:"检索索引等缓存(可再生)",memory:"长期记忆",skills:"本地技能",agents:"子智能体人格",squads:"专家团",commands:"自定义斜杠命令",decisions:"决策图谱",schedule:"定时任务","schedule.json":"定时任务",AutoTasks:"自动化任务",daemon:"调度器运行状态",forks:"后台任务日志",workflows:"工作流运行记录(脚本原文与每个子智能体的产出,供复盘与续跑)","desktop-chats":"桌面端对话","history.json":"命令行输入历史","mcp.json":"MCP服务配置","idea-host":"IDEA插件宿主",".anonymous-user-id":"匿名标识"};export function labelOf(e){return DATA_LABELS[e]||""}export function describeData(){const r=o;if(!e.existsSync(r))return{exists:!1,dir:r,entries:[],total:0};const n=[];let i=0;for(const o of e.readdirSync(r,{withFileTypes:!0})){const c=t.join(r,o.name);let a=0;try{a=o.isDirectory()?s(c):e.statSync(c).size}catch{}i+=a,n.push({name:o.name,kind:o.isDirectory()?"dir":"file",size:a,label:labelOf(o.name)})}return n.sort((e,t)=>e.name.localeCompare(t.name)),{exists:!0,dir:r,entries:n,total:i}}export function purgeData(){const t=o;return e.existsSync(t)?(e.rmSync(t,{recursive:!0,force:!0}),{removed:!0,dir:t}):{removed:!1,dir:t}}export function fmtSize(e){return e<1024?e+"B":e<1048576?(e/1024).toFixed(1)+"KB":(e/1024/1024).toFixed(1)+"MB"}
|
package/src/workflow.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import t from"node:path";import r from"node:crypto";import{Worker as n}from"node:worker_threads";import{CONFIG_DIR as o}from"./config.js";export const MAX_AGENTS_PER_RUN=200;const a=36e5;export class WorkflowUsageError extends Error{constructor(e){super(e),this.name="WorkflowUsageError"}}export const RUNS_DIR=t.join(o,"workflows");export function pruneRuns(r=Date.now()){let n;try{n=e.readdirSync(RUNS_DIR)}catch{return 0}const o=[];for(const r of n)if(r.startsWith("wf-"))try{o.push({n:r,t:e.statSync(t.join(RUNS_DIR,r)).mtimeMs})}catch{}o.sort((e,t)=>t.t-e.t);const a=r-12096e5;let s=0;return o.forEach((r,n)=>{if(!(n<30&&r.t>=a))try{e.rmSync(t.join(RUNS_DIR,r.n),{recursive:!0,force:!0}),s++}catch{}}),s}function s(r,{script:n,meta:o,args:a}){const s=t.join(RUNS_DIR,r);try{!function(t){e.mkdirSync(t,{recursive:!0,mode:448});try{e.chmodSync(t,448)}catch{}}(s),e.writeFileSync(t.join(s,"script.js"),String(n),{mode:384}),e.writeFileSync(t.join(s,"meta.json"),JSON.stringify({runId:r,meta:o,args:a,startedAt:(new Date).toISOString()},null,2),{mode:384})}catch{return{dir:s,append:()=>{},finish:()=>{},ok:!1}}const i=t.join(s,"journal.jsonl");return{dir:s,ok:!0,append(t){try{e.appendFileSync(i,JSON.stringify(t)+"\n",{mode:384})}catch{}},finish(r){try{const n=t.join(s,"meta.json"),o=JSON.parse(e.readFileSync(n,"utf8"));e.writeFileSync(n,JSON.stringify({...o,...r,finishedAt:(new Date).toISOString()},null,2),{mode:384})}catch{}}}}export function loadResume(r){const n=new Map;if(!r)return n;let o;try{o=e.readFileSync(t.join(RUNS_DIR,String(r),"journal.jsonl"),"utf8").split("\n")}catch{return n}for(const e of o){if(!e.trim())continue;let t;try{t=JSON.parse(e)}catch{continue}if("agent"!==t.type||!t.ok||!t.key)continue;const r=n.get(t.key)||[];r.push(t.result),n.set(t.key,r)}return n}export function validateMeta(e){if(!e||"object"!=typeof e||Array.isArray(e))throw new WorkflowUsageError("meta必须是一个对象,至少包含name与description。");const t=String(e.name||"").trim(),r=String(e.description||"").trim();if(!t)throw new WorkflowUsageError("meta.name不能为空(用于进度显示与运行记录)。");if(!r)throw new WorkflowUsageError("meta.description不能为空(一句话说明这个工作流做什么)。");const n=[];if(void 0!==e.phases){if(!Array.isArray(e.phases))throw new WorkflowUsageError("meta.phases必须是数组。");for(const t of e.phases){if(!t||"object"!=typeof t||!String(t.title||"").trim())throw new WorkflowUsageError("meta.phases的每一项都必须是 { title, detail? } 形式且title非空。");n.push({title:String(t.title).trim(),detail:String(t.detail||"")})}}return{name:t,description:r,phases:n}}export function schemaShapeError(e){if(!e||"object"!=typeof e||Array.isArray(e))return"agent() 的schema必须是一个JSON Schema对象。";if("string"==typeof e.type||e.properties||e.items||Array.isArray(e.enum)||e.oneOf||e.anyOf||e.allOf||e.$ref)return"";return`agent() 的schema不是合法的JSON Schema(收到的键:${Object.keys(e).slice(0,5).join("、")||"空对象"})。正确形状形如 { type: "object", properties: { port: { type: "integer" } }, required: ["port"] }。写成 { port: "number" } 这类简写不会报错,但会让结构化校验彻底失效——子智能体报回什么都算通过。`}export function validateAgainstSchema(e,t,r="结果"){const n=[];if(!t||"object"!=typeof t)return n;const o=t.type,a=e=>Array.isArray(e)?"array":null===e?"null":typeof e;if(o&&a(e)!==o&&("integer"!==o||!Number.isInteger(e)))return n.push(`${r}应为${o},实际为${a(e)}`),n;Array.isArray(t.enum)&&!t.enum.includes(e)&&n.push(`${r} 应为 ${JSON.stringify(t.enum)} 之一`);if("object"===o||!o&&(t.properties||t.required)){if(null===e||"object"!=typeof e||Array.isArray(e))return n.push(`${r}应为object,实际为${a(e)}`),n;for(const o of t.required||[])void 0===e[o]&&n.push(`${r} 缺少必填字段${o}`);for(const[o,a]of Object.entries(t.properties||{}))void 0!==e[o]&&n.push(...validateAgainstSchema(e[o],a,`${r}.${o}`))}else"array"===o&&t.items&&e.forEach((e,o)=>n.push(...validateAgainstSchema(e,t.items,`${r}[${o}]`)));return n}export function runWorkflow({script:e,meta:t,args:o,resumeFrom:i,dispatchAgent:l,onEvent:c,signal:p}={}){const u="wf-"+Date.now().toString(36)+"-"+r.randomBytes(3).toString("hex"),m=loadResume(i),g=new Map,d=s(u,{script:e,meta:t,args:o});try{pruneRuns()}catch{}return new Promise(t=>{let s=null,i=!1,h=0,f=0,y=0,k=0,w=0;const S=[],v=[],_=Date.now(),b=e=>{try{c?.(e)}catch{}},j=e=>{if(i)return;i=!0,clearInterval(x),clearTimeout($),p?.removeEventListener?.("abort",D);try{s?.terminate()}catch{}const r={ok:e.ok,kind:e.kind||(e.ok?"done":"error"),agents:h,cached:f,phases:S.slice(),elapsedMs:Date.now()-_,error:e.error||""};d.finish(r),t({...e,runId:u,dir:d.dir,agents:h,cached:f,phases:S.slice(),elapsedMs:r.elapsedMs})},x=setInterval(()=>{Date.now()-_-w-(y>0?Date.now()-k:0)>2e4&&j({ok:!1,kind:"compute-timeout",logs:v.slice(),value:null,error:`脚本自身运算超过 ${Math.round(20)} 秒(不含等待子智能体)。工作流脚本只该做编排,重活交给agent()。`})},250),$=setTimeout(()=>{j({ok:!1,kind:"wall-timeout",logs:v.slice(),value:null,error:`工作流总耗时超过 ${Math.round(60)} 分钟,已中止。`})},a),D=()=>j({ok:!1,kind:"abort",logs:v.slice(),value:null,error:"已中断"});if(p){if(p.aborted)return D();p.addEventListener("abort",D,{once:!0})}const A=(e,t)=>{if(!i)try{s.postMessage({reply:e,...t})}catch{}},M=e=>j({ok:!1,kind:"usage",logs:v.slice(),value:null,error:e}),R=async(e,t)=>{const n="string"==typeof t?.prompt?t.prompt.trim():"";if(!n)return M("agent() 的第一个参数必须是非空字符串(子任务目标)。");const o=new Set(["prompt","label","phase","schema","context","agentType"]),a=Object.keys(t).filter(e=>!o.has(e));if(a.length)return M(`agent() 收到不认识的选项:${a.join("、")}。可用选项为label / phase / schema / context / agentType。`);if(void 0!==t.schema){const e=schemaShapeError(t.schema);if(e)return M(e)}if(h>=200)return M("本次工作流的子智能体数量已达上限 200,已中止。请缩小扇出规模或分几次跑。");const s=function(e){const t=r.createHash("sha256");return t.update(String(e.prompt||"")),t.update("\0"+String(e.label||"")),t.update("\0"+String(e.agentType||"")),t.update("\0"+JSON.stringify(e.schema||null)),t.digest("hex").slice(0,32)}(t),i=m.get(s),c=g.get(s)||0;if(i&&c<i.length)return g.set(s,c+1),h++,f++,d.append({type:"agent",key:s,ok:!0,cached:!0,phase:t.phase||"",label:t.label||"",result:i[c]}),b({type:"agent-end",cached:!0,label:t.label||n.slice(0,40)}),A(e,{ok:!0,result:i[c]});h++;const p=String(t.label||n.split("\n")[0]).slice(0,60);0===y&&(k=Date.now()),y++,b({type:"agent-start",label:p,phase:t.phase||""});const u=Date.now();let S;try{S=await l({...t,prompt:n,label:p})}catch(e){S={ok:!1,error:e.message}}if(y--,0===y&&(w+=Date.now()-k),d.append({type:"agent",key:s,ok:!!S.ok,phase:t.phase||"",label:p,ms:Date.now()-u,result:S.ok?S.result:void 0,error:S.ok?void 0:S.error||""}),b({type:"agent-end",label:p,ok:!!S.ok,ms:Date.now()-u}),!S.ok&&S.usage)return M(S.error||"agent() 用法错误");A(e,{ok:!0,result:S.ok?S.result:null})};try{s=new n("\nconst { parentPort, workerData } = require('node:worker_threads');\nconst vm = require('node:vm');\n\nconst pending = new Map();\nlet nextId = 0;\nfunction host(kind, payload) {\n return new Promise((resolve, reject) => {\n const id = ++nextId;\n pending.set(id, { resolve, reject });\n parentPort.postMessage({ call: id, kind, payload });\n });\n}\nparentPort.on('message', (m) => {\n if (!m || typeof m.reply !== 'number') return;\n const p = pending.get(m.reply);\n if (!p) return; // 未知/重复回包:忽略\n pending.delete(m.reply);\n if (m.ok) p.resolve(m.result);\n else p.reject(new Error(m.error || '调用失败'));\n});\n// 注意:这里只处理「子任务失败」这一种拒绝。钩子误用不会走到这里——\n// 宿主根本不回包,而是直接终止整个worker。理由见主线程handleAgent的注释。\n\n// 全新realm:这里根本不存在require / process / fs / setTimeout —— 「只能协调」由此成立\nconst ctx = vm.createContext({ __host: host, __args: workerData.args, __logs: [] });\n\n// 在**上下文内部**构造钩子:这样agent.constructor指向的是本realm的Function,\n// 而不是宿主realm的——顺手关掉最直白的那条摸回宿主的路。\nvm.runInContext(`\n const __h = __host;\n // 时钟与随机数:脚本不确定就无法复现,续跑也就无从谈起\n const __deny = (what) => () => { throw new Error('工作流脚本里禁用 ' + what + ':脚本必须可复现,否则断点续跑时提示词对不上、缓存永远命不中。需要时间戳/随机数请由调用方经args传入。'); };\n Date.now = __deny('Date.now()');\n Math.random = __deny('Math.random()');\n const __RealDate = Date;\n Date = new Proxy(__RealDate, {\n construct(T, a) { if (!a.length) __deny('new Date()')(); return Reflect.construct(T, a); },\n });\n\n var args = __args;\n var agent = (prompt, opts) => __h('agent', { prompt, ...(opts || {}) });\n var phase = (title) => { __h('phase', { title }); };\n var log = (message) => { __logs.push(String(message)); __h('log', { message: String(message) }); };\n\n // pipeline:每个条目独立穿过所有阶段,**阶段之间没有栅栏**。\n // 这是默认首选:加了栅栏,快的条目就得白等慢的,整条流水的耗时从「最慢的那条链」\n // 劣化成「每一阶段最慢者之和」。某一条目在某阶段抛错 → 该条目变null并跳过其余阶段,\n // 不影响别的条目;但钩子误用(__fatal)必须整体炸掉。\n var pipeline = (items, ...stages) => Promise.all((items || []).map(async (item, i) => {\n let cur = item;\n for (const st of stages) {\n try { cur = await st(cur, item, i); } catch { return null; }\n }\n return cur;\n }));\n\n // parallel:栅栏。仅当确实需要「全部结果一起看」(去重、汇总、早退判断)时才用。\n var parallel = (thunks) => Promise.all((thunks || []).map(async (t) => {\n try { return await t(); } catch { return null; }\n }));\n`, ctx);\n\n(async () => {\n let fn;\n try {\n fn = vm.runInContext('(async function () {' + workerData.code + '\\n})', ctx, { filename: 'workflow-script.js' });\n } catch (e) {\n parentPort.postMessage({ done: true, error: '脚本语法错误:' + e.message, syntax: true });\n return;\n }\n try {\n const value = await fn();\n parentPort.postMessage({ done: true, value: value === undefined ? null : value, logs: ctx.__logs });\n } catch (e) {\n parentPort.postMessage({ done: true, error: (e && e.message) ? e.message : String(e), logs: ctx.__logs });\n }\n})();\n",{eval:!0,workerData:{code:String(e||""),args:void 0===o?null:o},env:{},resourceLimits:{maxOldGenerationSizeMb:256},stdout:!0,stderr:!0})}catch(e){return j({ok:!1,kind:"exception",logs:[],value:null,error:"无法启动工作流运行时:"+e.message})}s.on("message",e=>{if(!i&&e&&"object"==typeof e){if(e.done)return e.error?j({ok:!1,kind:e.syntax?"syntax":"exception",logs:v.slice(),value:null,error:e.error}):j({ok:!0,logs:v.slice(),value:e.value??null});if("number"==typeof e.call)if("agent"!==e.kind){if("phase"===e.kind){const t=String(e.payload?.title||"").trim();return t&&!S.includes(t)&&S.push(t),t&&(d.append({type:"phase",title:t}),b({type:"phase",title:t})),A(e.call,{ok:!0,result:null})}if("log"===e.kind){const t=String(e.payload?.message||"");return v.push(t),d.append({type:"log",message:t}),b({type:"log",message:t}),A(e.call,{ok:!0,result:null})}M(`未知的脚本钩子:${e.kind}`)}else R(e.call,e.payload&&"object"==typeof e.payload?e.payload:{})}}),s.on("error",e=>j({ok:!1,kind:"exception",logs:v.slice(),value:null,error:e.message})),s.on("exit",e=>{i||j({ok:!1,kind:"worker-exit",logs:v.slice(),value:null,error:`工作流运行时意外退出(code=${e}),可能是内存超限。`})})})}export function makeWorkflowTool({config:e,cwd:t,extraTools:r=[],depth:n=0,onSub:o=null}={}){return{readOnly:!0,timeoutMs:0,spec:{type:"function",function:{name:"workflow",description:'交出一段JavaScript脚本来编排**子智能体**:由脚本决定扇出多少、怎么串成流水、什么时候收敛。适合固定形状表达不了的拓扑——扇出→各自多阶段流水→跨条目去重→对抗式验证→综合,或「一直找到连续两轮没有新发现为止」这类收敛循环。\n脚本里可用:\n· agent(prompt, { label?, phase?, schema?, context?, agentType? }) —— 起一个子智能体并拿回结论。只有这五个选项,传别的会**当场终止整个工作流**。schema必须是完整JSON Schema (形如 { type: "object", properties: { port: { type: "integer" } }, required: ["port"] }),写成 { port: "number" } 这种简写会让校验彻底失效。子任务失败时agent() 返回null(不抛异常),用 .filter(Boolean) 过滤即可;pipeline(items, ...stages) 让每个条目独立穿过所有阶段(阶段之间无栅栏,默认首选);parallel(thunks) 并发并等全部完成(栅栏,仅当确实要一起看全部结果时用);phase(title) / log(msg) 报告进度;args是你传入的输入。\n脚本只能协调、不能干活:没有文件系统、网络、定时器,也禁用Date.now与Math.random(脚本必须可复现,否则断点续跑时对不上)。要读写文件或跑命令,交给agent() 里的子智能体,或改用run_program。\n每次运行都会留下记录,中断后可用resume_from续跑:已完成的agent() 直接复用,不重跑。',parameters:{type:"object",properties:{script:{type:"string",description:"脚本体(异步函数体;可用顶层await与return)"},meta:{type:"object",description:"身份块(纯JSON,不从脚本里解析):{ name, description, phases?: [{title, detail?}] }",properties:{name:{type:"string"},description:{type:"string"},phases:{type:"array",items:{type:"object"}}},required:["name","description"]},args:{description:"可选:传给脚本的输入,脚本里以args逐字可见"},resume_from:{type:"string",description:"可选:上次运行的runId,复用其已完成的agent() 结果续跑"}},required:["script","meta"]}}},runWithEnv:async(a,s,i)=>{let l;try{l=validateMeta(a?.meta)}catch(e){return`【工作流未启动】${e.message}`}const c=String(a?.script||"").trim();if(!c)return"【工作流未启动】script不能为空。";const{makeWorkflowDispatcher:p}=await import("./subagent.js"),u=p({config:e,cwd:t,extraTools:r,depth:n,onSub:o}),m=s?.info||(()=>{});m(`工作流「${l.name}」开始:${l.description}`);return formatResult(await runWorkflow({script:c,meta:l,args:a?.args,resumeFrom:a?.resume_from,dispatchAgent:u,signal:i,onEvent:e=>{"phase"===e.type?m(`工作流·阶段:${e.title}`):"log"===e.type?m(`工作流:${e.message}`):"agent-end"===e.type&&e.cached&&m(`工作流·复用上次结果:${e.label}`)}}),l)}}}export function formatResult(e,t){const r=Math.round((e.elapsedMs||0)/1e3),n=`【工作流${e.ok?"完成":"失败"}】${t?.name||""} 子智能体 ${e.agents} 个${e.cached?`(其中 ${e.cached} 个复用上次结果)`:""} 耗时 ${r} 秒 runId=${e.runId}`;if(!e.ok){const t={usage:"这是**脚本用法错误**,不是某个子任务失败——修正脚本后重跑。",syntax:"脚本没能通过语法解析,一个子智能体都没起。","compute-timeout":"脚本自身算得太久:工作流脚本只该做编排。","wall-timeout":"整体超时:缩小扇出规模,或分几次跑。",abort:"已被用户中断。"}[e.kind]||"",r="usage"===e.kind||"syntax"===e.kind?"":`\n已完成的部分可续跑:再次调用时传resume_from="${e.runId}",已跑完的子智能体不会重跑。`;return`${n}\n${e.error}${t?"\n"+t:""}${r}`}const o=[n];return e.phases?.length&&o.push("阶段:"+e.phases.join(" → ")),e.logs?.length&&o.push("过程:\n"+e.logs.join("\n")),null!==e.value&&void 0!==e.value?o.push("结果:\n"+("string"==typeof e.value?e.value:JSON.stringify(e.value,null,2))):e.logs?.length||o.push("(脚本没有return也没有log任何内容——结果不会自动回传,记得把要的东西return出来。)"),o.join("\n")}
|
|
1
|
+
import e from"node:fs";import t from"node:path";import r from"node:crypto";import{Worker as n}from"node:worker_threads";import{accountDataDir as o}from"./account-scope.js";export const MAX_AGENTS_PER_RUN=200;const a=36e5;export class WorkflowUsageError extends Error{constructor(e){super(e),this.name="WorkflowUsageError"}}const s=()=>t.join(o(),"workflows");export const RUNS_DIR=s();export function pruneRuns(r=Date.now()){let n;try{n=e.readdirSync(s())}catch{return 0}const o=[];for(const r of n)if(r.startsWith("wf-"))try{o.push({n:r,t:e.statSync(t.join(s(),r)).mtimeMs})}catch{}o.sort((e,t)=>t.t-e.t);const a=r-12096e5;let i=0;return o.forEach((r,n)=>{if(!(n<30&&r.t>=a))try{e.rmSync(t.join(s(),r.n),{recursive:!0,force:!0}),i++}catch{}}),i}function i(r,{script:n,meta:o,args:a}){const i=t.join(s(),r);try{!function(t){e.mkdirSync(t,{recursive:!0,mode:448});try{e.chmodSync(t,448)}catch{}}(i),e.writeFileSync(t.join(i,"script.js"),String(n),{mode:384}),e.writeFileSync(t.join(i,"meta.json"),JSON.stringify({runId:r,meta:o,args:a,startedAt:(new Date).toISOString()},null,2),{mode:384})}catch{return{dir:i,append:()=>{},finish:()=>{},ok:!1}}const l=t.join(i,"journal.jsonl");return{dir:i,ok:!0,append(t){try{e.appendFileSync(l,JSON.stringify(t)+"\n",{mode:384})}catch{}},finish(r){try{const n=t.join(i,"meta.json"),o=JSON.parse(e.readFileSync(n,"utf8"));e.writeFileSync(n,JSON.stringify({...o,...r,finishedAt:(new Date).toISOString()},null,2),{mode:384})}catch{}}}}export function loadResume(r){const n=new Map;if("string"!=typeof r||!/^[a-zA-Z0-9_-]+$/.test(r))return n;let o;try{o=e.readFileSync(t.join(s(),String(r),"journal.jsonl"),"utf8").split("\n")}catch{return n}for(const e of o){if(!e.trim())continue;let t;try{t=JSON.parse(e)}catch{continue}if("agent"!==t.type||!t.ok||!t.key)continue;const r=n.get(t.key)||[];r.push(t.result),n.set(t.key,r)}return n}export function validateMeta(e){if(!e||"object"!=typeof e||Array.isArray(e))throw new WorkflowUsageError("meta必须是一个对象,至少包含name与description。");const t=String(e.name||"").trim(),r=String(e.description||"").trim();if(!t)throw new WorkflowUsageError("meta.name不能为空(用于进度显示与运行记录)。");if(!r)throw new WorkflowUsageError("meta.description不能为空(一句话说明这个工作流做什么)。");const n=[];if(void 0!==e.phases){if(!Array.isArray(e.phases))throw new WorkflowUsageError("meta.phases必须是数组。");for(const t of e.phases){if(!t||"object"!=typeof t||!String(t.title||"").trim())throw new WorkflowUsageError("meta.phases的每一项都必须是 { title, detail? } 形式且title非空。");n.push({title:String(t.title).trim(),detail:String(t.detail||"")})}}return{name:t,description:r,phases:n}}export function schemaShapeError(e){if(!e||"object"!=typeof e||Array.isArray(e))return"agent() 的schema必须是一个JSON Schema对象。";if("string"==typeof e.type||e.properties||e.items||Array.isArray(e.enum)||e.oneOf||e.anyOf||e.allOf||e.$ref)return"";return`agent() 的schema不是合法的JSON Schema(收到的键:${Object.keys(e).slice(0,5).join("、")||"空对象"})。正确形状形如 { type: "object", properties: { port: { type: "integer" } }, required: ["port"] }。写成 { port: "number" } 这类简写不会报错,但会让结构化校验彻底失效——子智能体报回什么都算通过。`}export function validateAgainstSchema(e,t,r="结果"){const n=[];if(!t||"object"!=typeof t)return n;const o=t.type,a=e=>Array.isArray(e)?"array":null===e?"null":typeof e;if(o&&a(e)!==o&&("integer"!==o||!Number.isInteger(e)))return n.push(`${r}应为${o},实际为${a(e)}`),n;Array.isArray(t.enum)&&!t.enum.includes(e)&&n.push(`${r} 应为 ${JSON.stringify(t.enum)} 之一`);if("object"===o||!o&&(t.properties||t.required)){if(null===e||"object"!=typeof e||Array.isArray(e))return n.push(`${r}应为object,实际为${a(e)}`),n;for(const o of t.required||[])void 0===e[o]&&n.push(`${r} 缺少必填字段${o}`);for(const[o,a]of Object.entries(t.properties||{}))void 0!==e[o]&&n.push(...validateAgainstSchema(e[o],a,`${r}.${o}`))}else"array"===o&&t.items&&e.forEach((e,o)=>n.push(...validateAgainstSchema(e,t.items,`${r}[${o}]`)));return n}export function runWorkflow({script:e,meta:t,args:o,resumeFrom:s,dispatchAgent:l,onEvent:c,signal:p}={}){const u="wf-"+Date.now().toString(36)+"-"+r.randomBytes(3).toString("hex"),m=loadResume(s),g=new Map,d=i(u,{script:e,meta:t,args:o});try{pruneRuns()}catch{}return new Promise(t=>{let s=null,i=!1,h=0,f=0,y=0,k=0,w=0;const S=[],v=[],_=Date.now(),b=e=>{try{c?.(e)}catch{}},j=e=>{if(i)return;i=!0,clearInterval(x),clearTimeout($),p?.removeEventListener?.("abort",D);try{s?.terminate()}catch{}const r={ok:e.ok,kind:e.kind||(e.ok?"done":"error"),agents:h,cached:f,phases:S.slice(),elapsedMs:Date.now()-_,error:e.error||""};d.finish(r),t({...e,runId:u,dir:d.dir,agents:h,cached:f,phases:S.slice(),elapsedMs:r.elapsedMs})},x=setInterval(()=>{Date.now()-_-w-(y>0?Date.now()-k:0)>2e4&&j({ok:!1,kind:"compute-timeout",logs:v.slice(),value:null,error:`脚本自身运算超过 ${Math.round(20)} 秒(不含等待子智能体)。工作流脚本只该做编排,重活交给agent()。`})},250),$=setTimeout(()=>{j({ok:!1,kind:"wall-timeout",logs:v.slice(),value:null,error:`工作流总耗时超过 ${Math.round(60)} 分钟,已中止。`})},a),D=()=>j({ok:!1,kind:"abort",logs:v.slice(),value:null,error:"已中断"});if(p){if(p.aborted)return D();p.addEventListener("abort",D,{once:!0})}const A=(e,t)=>{if(!i)try{s.postMessage({reply:e,...t})}catch{}},M=e=>j({ok:!1,kind:"usage",logs:v.slice(),value:null,error:e}),O=async(e,t)=>{const n="string"==typeof t?.prompt?t.prompt.trim():"";if(!n)return M("agent() 的第一个参数必须是非空字符串(子任务目标)。");const o=new Set(["prompt","label","phase","schema","context","agentType"]),a=Object.keys(t).filter(e=>!o.has(e));if(a.length)return M(`agent() 收到不认识的选项:${a.join("、")}。可用选项为label / phase / schema / context / agentType。`);if(void 0!==t.schema){const e=schemaShapeError(t.schema);if(e)return M(e)}if(h>=200)return M("本次工作流的子智能体数量已达上限 200,已中止。请缩小扇出规模或分几次跑。");const s=function(e){const t=r.createHash("sha256");return t.update(String(e.prompt||"")),t.update("\0"+String(e.label||"")),t.update("\0"+String(e.agentType||"")),t.update("\0"+JSON.stringify(e.schema||null)),t.digest("hex").slice(0,32)}(t),i=m.get(s),c=g.get(s)||0;if(i&&c<i.length)return g.set(s,c+1),h++,f++,d.append({type:"agent",key:s,ok:!0,cached:!0,phase:t.phase||"",label:t.label||"",result:i[c]}),b({type:"agent-end",cached:!0,label:t.label||n.slice(0,40)}),A(e,{ok:!0,result:i[c]});h++;const p=String(t.label||n.split("\n")[0]).slice(0,60);0===y&&(k=Date.now()),y++,b({type:"agent-start",label:p,phase:t.phase||""});const u=Date.now();let S;try{S=await l({...t,prompt:n,label:p})}catch(e){S={ok:!1,error:e.message}}if(y--,0===y&&(w+=Date.now()-k),d.append({type:"agent",key:s,ok:!!S.ok,phase:t.phase||"",label:p,ms:Date.now()-u,result:S.ok?S.result:void 0,error:S.ok?void 0:S.error||""}),b({type:"agent-end",label:p,ok:!!S.ok,ms:Date.now()-u}),!S.ok&&S.usage)return M(S.error||"agent() 用法错误");A(e,{ok:!0,result:S.ok?S.result:null})};try{s=new n("\nconst { parentPort, workerData } = require('node:worker_threads');\nconst vm = require('node:vm');\n\nconst pending = new Map();\nlet nextId = 0;\nfunction host(kind, payload) {\n return new Promise((resolve, reject) => {\n const id = ++nextId;\n pending.set(id, { resolve, reject });\n parentPort.postMessage({ call: id, kind, payload });\n });\n}\nparentPort.on('message', (m) => {\n if (!m || typeof m.reply !== 'number') return;\n const p = pending.get(m.reply);\n if (!p) return; // 未知/重复回包:忽略\n pending.delete(m.reply);\n if (m.ok) p.resolve(m.result);\n else p.reject(new Error(m.error || '调用失败'));\n});\n// 注意:这里只处理「子任务失败」这一种拒绝。钩子误用不会走到这里——\n// 宿主根本不回包,而是直接终止整个worker。理由见主线程handleAgent的注释。\n\n// 全新realm:这里根本不存在require / process / fs / setTimeout —— 「只能协调」由此成立\nconst ctx = vm.createContext({ __host: host, __args: workerData.args, __logs: [] });\n\n// 在**上下文内部**构造钩子:这样agent.constructor指向的是本realm的Function,\n// 而不是宿主realm的——顺手关掉最直白的那条摸回宿主的路。\nvm.runInContext(`\n const __h = __host;\n // 时钟与随机数:脚本不确定就无法复现,续跑也就无从谈起\n const __deny = (what) => () => { throw new Error('工作流脚本里禁用 ' + what + ':脚本必须可复现,否则断点续跑时提示词对不上、缓存永远命不中。需要时间戳/随机数请由调用方经args传入。'); };\n Date.now = __deny('Date.now()');\n Math.random = __deny('Math.random()');\n const __RealDate = Date;\n Date = new Proxy(__RealDate, {\n construct(T, a) { if (!a.length) __deny('new Date()')(); return Reflect.construct(T, a); },\n });\n\n var args = __args;\n var agent = (prompt, opts) => __h('agent', { prompt, ...(opts || {}) });\n var phase = (title) => { __h('phase', { title }); };\n var log = (message) => { __logs.push(String(message)); __h('log', { message: String(message) }); };\n\n // pipeline:每个条目独立穿过所有阶段,**阶段之间没有栅栏**。\n // 这是默认首选:加了栅栏,快的条目就得白等慢的,整条流水的耗时从「最慢的那条链」\n // 劣化成「每一阶段最慢者之和」。某一条目在某阶段抛错 → 该条目变null并跳过其余阶段,\n // 不影响别的条目;但钩子误用(__fatal)必须整体炸掉。\n var pipeline = (items, ...stages) => Promise.all((items || []).map(async (item, i) => {\n let cur = item;\n for (const st of stages) {\n try { cur = await st(cur, item, i); } catch { return null; }\n }\n return cur;\n }));\n\n // parallel:栅栏。仅当确实需要「全部结果一起看」(去重、汇总、早退判断)时才用。\n var parallel = (thunks) => Promise.all((thunks || []).map(async (t) => {\n try { return await t(); } catch { return null; }\n }));\n`, ctx);\n\n(async () => {\n let fn;\n try {\n fn = vm.runInContext('(async function () {' + workerData.code + '\\n})', ctx, { filename: 'workflow-script.js' });\n } catch (e) {\n parentPort.postMessage({ done: true, error: '脚本语法错误:' + e.message, syntax: true });\n return;\n }\n try {\n const value = await fn();\n parentPort.postMessage({ done: true, value: value === undefined ? null : value, logs: ctx.__logs });\n } catch (e) {\n parentPort.postMessage({ done: true, error: (e && e.message) ? e.message : String(e), logs: ctx.__logs });\n }\n})();\n",{eval:!0,workerData:{code:String(e||""),args:void 0===o?null:o},env:{},resourceLimits:{maxOldGenerationSizeMb:256},stdout:!0,stderr:!0})}catch(e){return j({ok:!1,kind:"exception",logs:[],value:null,error:"无法启动工作流运行时:"+e.message})}s.on("message",e=>{if(!i&&e&&"object"==typeof e){if(e.done)return e.error?j({ok:!1,kind:e.syntax?"syntax":"exception",logs:v.slice(),value:null,error:e.error}):j({ok:!0,logs:v.slice(),value:e.value??null});if("number"==typeof e.call)if("agent"!==e.kind){if("phase"===e.kind){const t=String(e.payload?.title||"").trim();return t&&!S.includes(t)&&S.push(t),t&&(d.append({type:"phase",title:t}),b({type:"phase",title:t})),A(e.call,{ok:!0,result:null})}if("log"===e.kind){const t=String(e.payload?.message||"");return v.push(t),d.append({type:"log",message:t}),b({type:"log",message:t}),A(e.call,{ok:!0,result:null})}M(`未知的脚本钩子:${e.kind}`)}else O(e.call,e.payload&&"object"==typeof e.payload?e.payload:{})}}),s.on("error",e=>j({ok:!1,kind:"exception",logs:v.slice(),value:null,error:e.message})),s.on("exit",e=>{i||j({ok:!1,kind:"worker-exit",logs:v.slice(),value:null,error:`工作流运行时意外退出(code=${e}),可能是内存超限。`})})})}export function makeWorkflowTool({config:e,cwd:t,extraTools:r=[],depth:n=0,onSub:o=null}={}){return{readOnly:!0,timeoutMs:0,spec:{type:"function",function:{name:"workflow",description:'交出一段JavaScript脚本来编排**子智能体**:由脚本决定扇出多少、怎么串成流水、什么时候收敛。适合固定形状表达不了的拓扑——扇出→各自多阶段流水→跨条目去重→对抗式验证→综合,或「一直找到连续两轮没有新发现为止」这类收敛循环。\n脚本里可用:\n· agent(prompt, { label?, phase?, schema?, context?, agentType? }) —— 起一个子智能体并拿回结论。只有这五个选项,传别的会**当场终止整个工作流**。schema必须是完整JSON Schema (形如 { type: "object", properties: { port: { type: "integer" } }, required: ["port"] }),写成 { port: "number" } 这种简写会让校验彻底失效。子任务失败时agent() 返回null(不抛异常),用 .filter(Boolean) 过滤即可;pipeline(items, ...stages) 让每个条目独立穿过所有阶段(阶段之间无栅栏,默认首选);parallel(thunks) 并发并等全部完成(栅栏,仅当确实要一起看全部结果时用);phase(title) / log(msg) 报告进度;args是你传入的输入。\n脚本只能协调、不能干活:没有文件系统、网络、定时器,也禁用Date.now与Math.random(脚本必须可复现,否则断点续跑时对不上)。要读写文件或跑命令,交给agent() 里的子智能体,或改用run_program。\n每次运行都会留下记录,中断后可用resume_from续跑:已完成的agent() 直接复用,不重跑。',parameters:{type:"object",properties:{script:{type:"string",description:"脚本体(异步函数体;可用顶层await与return)"},meta:{type:"object",description:"身份块(纯JSON,不从脚本里解析):{ name, description, phases?: [{title, detail?}] }",properties:{name:{type:"string"},description:{type:"string"},phases:{type:"array",items:{type:"object"}}},required:["name","description"]},args:{description:"可选:传给脚本的输入,脚本里以args逐字可见"},resume_from:{type:"string",description:"可选:上次运行的runId,复用其已完成的agent() 结果续跑"}},required:["script","meta"]}}},runWithEnv:async(a,s,i)=>{let l;try{l=validateMeta(a?.meta)}catch(e){return`【工作流未启动】${e.message}`}const c=String(a?.script||"").trim();if(!c)return"【工作流未启动】script不能为空。";const{makeWorkflowDispatcher:p}=await import("./subagent.js"),u=p({config:e,cwd:t,extraTools:r,depth:n,onSub:o}),m=s?.info||(()=>{});m(`工作流「${l.name}」开始:${l.description}`);return formatResult(await runWorkflow({script:c,meta:l,args:a?.args,resumeFrom:a?.resume_from,dispatchAgent:u,signal:i,onEvent:e=>{"phase"===e.type?m(`工作流·阶段:${e.title}`):"log"===e.type?m(`工作流:${e.message}`):"agent-end"===e.type&&e.cached&&m(`工作流·复用上次结果:${e.label}`)}}),l)}}}export function formatResult(e,t){const r=Math.round((e.elapsedMs||0)/1e3),n=`【工作流${e.ok?"完成":"失败"}】${t?.name||""} 子智能体 ${e.agents} 个${e.cached?`(其中 ${e.cached} 个复用上次结果)`:""} 耗时 ${r} 秒 runId=${e.runId}`;if(!e.ok){const t={usage:"这是**脚本用法错误**,不是某个子任务失败——修正脚本后重跑。",syntax:"脚本没能通过语法解析,一个子智能体都没起。","compute-timeout":"脚本自身算得太久:工作流脚本只该做编排。","wall-timeout":"整体超时:缩小扇出规模,或分几次跑。",abort:"已被用户中断。"}[e.kind]||"",r="usage"===e.kind||"syntax"===e.kind?"":`\n已完成的部分可续跑:再次调用时传resume_from="${e.runId}",已跑完的子智能体不会重跑。`;return`${n}\n${e.error}${t?"\n"+t:""}${r}`}const o=[n];return e.phases?.length&&o.push("阶段:"+e.phases.join(" → ")),e.logs?.length&&o.push("过程:\n"+e.logs.join("\n")),null!==e.value&&void 0!==e.value?o.push("结果:\n"+("string"==typeof e.value?e.value:JSON.stringify(e.value,null,2))):e.logs?.length||o.push("(脚本没有return也没有log任何内容——结果不会自动回传,记得把要的东西return出来。)"),o.join("\n")}
|
package/src/worklog.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import e from"node:os";import n from"node:path";import{execFile as s}from"node:child_process";import{promisify as o}from"node:util";import{classifyCommit as i}from"./archaeology.js";const r=o(s);export const GIT_CONCURRENCY=Math.max(2,Math.min(8,e.cpus().length));export async function mapLimit(t,e,n){const s=new Array(t.length);let o=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;;){const e=o++;if(e>=t.length)return;s[e]=await n(t[e],e)}})),s}const c=[["test",/\b(test|spec|e2e)\b|测试|用例|压测|自测/i],["docs",/\b(docs?|readme|changelog|comment)\b|文档|注释|说明书|readme/i],["perf",/\b(perf|optimiz|improve|enhance|speed[\s-]?up)\b|优化|完善|增强|提速|性能|调优/i],["chore",/\b(chore|build|deps|dependency|bump|release|ci)\b|同步|升级|依赖|版本号|构建|打包|配置调整/i],["style",/\b(style|format|lint|prettier)\b|格式化|排版调整|代码风格/i],["feat",/迁入|迁移|接入|对接|打通|开发完成|新建|搭建/]];export function commitLead(t){return(e=t,String(e||"").replace(/[\w.-]*\/[\w./-]*/g," ")).split(/[::]/)[0].slice(0,50);var e}function a(t){const e=i(t);if("other"!==e)return e;const n=commitLead(t);for(const[t,e]of c)if(e.test(n))return t;return"other"}const u=new Set(["node_modules",".git",".svn",".hg","dist","build","out","target","bin","obj","__pycache__",".venv","venv","env",".tox",".mypy_cache",".pytest_cache",".ruff_cache",".gradle",".m2",".cache",".next",".nuxt",".turbo",".parcel-cache","coverage","vendor","Pods","Carthage","DerivedData",".idea",".vscode-test","Library","Applications","System",".Trash",".npm",".pnpm-store",".docker"]),l=/(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|poetry\.lock|Cargo\.lock|composer\.lock|Gemfile\.lock|go\.sum)$|(^|\/)(dist|build|out|target|vendor|node_modules|__generated__|generated)\/|\.min\.(js|css)$|\.(map|lock)$/i,p=[["文档",/\.(docx?|wps|md|markdown|txt|rtf|odt|ofd|pdf|tex)$/i],["表格",/\.(xlsx?|xlsm|csv|et|ods|numbers)$/i],["演示",/\.(pptx?|dps|odp|key)$/i],["图表",/\.(drawio|vsdx?|xmind|mmd|puml)$/i]];export function ymd(t){const e=t=>String(t).padStart(2,"0");return`${t.getFullYear()}-${e(t.getMonth()+1)}-${e(t.getDate())}`}function h(t){const e=String(t).trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);if(!e)throw new Error("日期格式应为YYYY-MM-DD:"+t);const[n,s,o]=[Number(e[1]),Number(e[2]),Number(e[3])],i=new Date(n,s-1,o);if(i.getFullYear()!==n||i.getMonth()!==s-1||i.getDate()!==o)throw new Error(`日期不存在:${t}(不要靠JS的自动进位,请给真实存在的日期)`);return i}function f(t,e){const n=new Date(t.getTime());return n.setDate(n.getDate()+e),n}export function parseRange({since:t,until:e,days:n,week:s}={}){const o=h(ymd(new Date));if(t){const n=h(t),s=e?h(e):o;if(s<n)throw new Error(`区间反了:${ymd(n)} 晚于 ${ymd(s)}`);return{since:n,until:s,label:`${ymd(n)} ~ ${ymd(s)}`,source:"显式区间"}}if("this"===s||"last"===s){const t=(o.getDay()+6)%7,e=f(o,-t-("last"===s?7:0)),n=f(e,6);return{since:e,until:n,label:`${ymd(e)} ~ ${ymd(n)}`,source:"this"===s?"本自然周(周一至周日)":"上一自然周(周一至周日)"}}const i=Number(n),r=Math.min(120,Number.isFinite(i)&&i>0?Math.round(i):7),c=f(o,-(r-1));return{since:c,until:o,label:`${ymd(c)} ~ ${ymd(o)}`,source:`最近 ${r} 天(含今天)`}}export function cnRange(t){const e=t=>`${t.getFullYear()}年${String(t.getMonth()+1).padStart(2,"0")}月${String(t.getDate()).padStart(2,"0")}日`;return`${e(t.since)}-${e(t.until)}`}export function expandPath(t,s=process.cwd()){let o=String(t||"").trim();return o?("~"===o?o=e.homedir():(o.startsWith("~/")||o.startsWith("~\\"))&&(o=n.join(e.homedir(),o.slice(2))),n.resolve(s,o)):""}export function discoverRepos(e,{maxDepth:s=4}={}){const o=[],i=[];let r=0;const c=(e,i)=>{if(o.length>=300||r>=6e4)return;if(r++,function(e){try{return t.existsSync(n.join(e,".git"))}catch{return!1}}(e))return void o.push(e);if(i>=s)return;let a=[];try{a=t.readdirSync(e,{withFileTypes:!0})}catch{return}for(const t of a)(t.isDirectory()||t.isSymbolicLink())&&(t.name.startsWith(".")&&"."!==t.name||u.has(t.name)||t.isSymbolicLink()||c(n.join(e,t.name),i+1))};for(const n of e)t.existsSync(n)?c(n,0):i.push(n);const a=new Map;for(const e of o){let n=e;try{n=t.realpathSync(e)}catch{}a.has(n)||a.set(n,e)}return{repos:[...a.values()].sort(),missingRoots:i,truncated:o.length>=300}}async function d(t,e,n){try{return await async function(t,e,{maxBuffer:n=67108864}={}){const{stdout:s}=await r("git",e,{cwd:t,encoding:"utf8",maxBuffer:n,windowsHide:!0});return s}(t,e,n)}catch{return""}}export async function buildIdentities({configured:t=[],repos:e=[]}={}){const n=new Set;for(const e of t){const t=String(e||"").trim().toLowerCase();t&&n.add(t)}const s=await mapLimit(e,GIT_CONCURRENCY,t=>d(t,["config","--get-regexp","^user\\.(name|email)$"]));for(const t of s)for(const e of String(t).split("\n")){const t=e.indexOf(" ");if(t<0)continue;const s=e.slice(t+1).trim().toLowerCase();s&&n.add(s)}return n}const m=(t,e,n)=>t.has(String(e||"").trim().toLowerCase())||t.has(String(n||"").trim().toLowerCase());export function normalizePath(t){let e=function(t){if(!(t.startsWith('"')&&t.endsWith('"')&&t.length>=2))return t;const e=t.slice(1,-1),n=[];for(let t=0;t<e.length;t++){if("\\"!==e[t]){n.push(e.charCodeAt(t));continue}const s=e.slice(t+1,t+4);if(/^[0-7]{3}$/.test(s)){n.push(parseInt(s,8)),t+=3;continue}const o={n:10,t:9,r:13,'"':34,"\\":92},i=e[t+1];i in o?(n.push(o[i]),t+=1):n.push(e.charCodeAt(t))}try{return Buffer.from(n).toString("utf8")}catch{return t}}(String(t||"").trim());e=e.replace(/\{([^{}]*) => ([^{}]*)\}/g,"$2");const n=e.indexOf(" => ");return n>=0&&(e=e.slice(n+4)),e.replace(/\/{2,}/g,"/").trim()}export async function collectRepoCommits(t,{since:e,until:s,identities:o}){const i=ymd(f(e,-90)),r=await d(t,["-c","core.quotepath=false","log","--all","--no-merges",`--since=${i}`,"-n4000","--date=format:%Y-%m-%d %H:%M","--pretty=format:%x00%H%x1f%h%x1f%ad%x1f%cd%x1f%an%x1f%ae%x1f%s","--numstat"]),c=ymd(e),u=ymd(s),p=[],h=[],g=new Map;for(const t of r.split("\0")){if(!t.trim())continue;const e=t.indexOf("\n"),n=(e<0?t:t.slice(0,e)).split("");if(n.length<7)continue;const[s,i,r,f,d,$,y]=n,k=r.slice(0,10),b=f.slice(0,10);if(!m(o,d,$)){if(k>=c&&k<=u){const t=`${d} <${$}>`;g.set(t,(g.get(t)||0)+1)}continue}const x=k>=c&&k<=u;if(!x&&!(!x&&k<c&&b>=c&&b<=u))continue;let w=0,S=0,j=0,D=0,v=0;const _=[];if(e>=0)for(const n of t.slice(e+1).split("\n")){if(!n.trim())continue;const t=n.split("\t");if(t.length<3)continue;const e=normalizePath(t.slice(2).join("\t"));if(!e)continue;if(_.push(e),"-"===t[0]||"-"===t[1]){j++;continue}const s=Number(t[0])||0,o=Number(t[1])||0;w+=s,S+=o,l.test(e)&&(D+=s,v+=o)}const I={hash:i,date:k,time:r,commitDate:b,subject:y,kind:a(y),ins:w,del:S,binary:j,noiseIns:D,noiseDel:v,files:_,bulk:w+S>=2e3||_.length>=40};(x?p:h).push(I)}return p.sort((t,e)=>t.time<e.time?1:-1),h.sort((t,e)=>t.time<e.time?1:-1),{dir:t,name:n.basename(t),branch:(await d(t,["rev-parse","--abbrev-ref","HEAD"])).trim(),commits:p,landed:h,otherAuthors:[...g.entries()].sort((t,e)=>e[1]-t[1])}}export function summarize(t){const e={n:t.length,ins:0,del:0,binary:0,noiseIns:0,noiseDel:0,bulk:[],kinds:{},files:new Set,dirs:new Map};for(const n of t){e.ins+=n.ins,e.del+=n.del,e.binary+=n.binary,e.noiseIns+=n.noiseIns,e.noiseDel+=n.noiseDel,e.kinds[n.kind]=(e.kinds[n.kind]||0)+1,n.bulk&&e.bulk.push(n);for(const t of n.files){e.files.add(t);const n=t.includes("/")?t.slice(0,t.indexOf("/"))+"/":"(根目录)";e.dirs.set(n,(e.dirs.get(n)||0)+1)}}e.netIns=e.ins-e.noiseIns,e.netDel=e.del-e.noiseDel;const n=new Set(e.bulk);e.exBulkIns=0,e.exBulkDel=0,e.exBulkN=0;for(const s of t)n.has(s)||(e.exBulkN++,e.exBulkIns+=s.ins-s.noiseIns,e.exBulkDel+=s.del-s.noiseDel);return e}const g={feat:"新功能",fix:"修复",refactor:"重构",revert:"回滚",hotfix:"紧急修复",merge:"合并",perf:"优化",chore:"同步/构建",docs:"文档",test:"测试",style:"格式",other:"其它"};function $(t,e){return[...t.entries()].sort((t,e)=>e[1]-t[1]).slice(0,e)}export function formatGitReport(t,e,n){const s=[];s.push(`【本机Git工作记录】${t.label}(${t.source},按**作者提交时间**切窗)`),s.push(`身份:${n.identities.length?n.identities.join("、"):"(未识别到任何身份)"}`),s.push(`扫描根目录:${n.roots.join("、")||"(无)"}`);const o=e.filter(t=>t.commits.length||t.landed.length);if(s.push(`发现仓库 ${n.repoCount} 个,其中 ${o.length} 个在本周期内有你的提交`),n.missingRoots.length&&s.push(`⚠ 这些根目录不存在,已跳过:${n.missingRoots.join("、")}`),n.truncated&&s.push("⚠ 仓库数达到上限 300,可能没扫全——请把roots收窄到真正的项目目录"),s.push(""),!o.length){s.push("本周期内没有找到属于你的提交。三种可能,请逐一排除:"),s.push(" ① 根目录没设对——现在扫的是上面那几个路径;"),s.push(" ② 身份没配全——下面「其它作者」里如果有你自己的马甲,把它加进identities重跑;"),s.push(" ③ 确实没提交(活干在别处,用work_scan或tapd_fetch看)。");const t=new Map;for(const n of e)for(const[e,s]of n.otherAuthors)t.set(e,(t.get(e)||0)+s);return t.size&&s.push(" 本周期内出现过的其它作者:"+$(t,12).map(([t,e])=>`${t}×${e}`).join("、")),s.join("\n")}let i=0,r=0,c=0,a=0,u=0,l=0;const p={};for(const t of o){const e=summarize(t.commits);i+=e.n,r+=e.netIns,c+=e.netDel,a+=e.exBulkIns,u+=e.exBulkDel,l+=e.bulk.length;for(const[t,n]of Object.entries(e.kinds))p[t]=(p[t]||0)+n;if(s.push(`■ ${t.name}${t.branch?`(当前分支 ${t.branch})`:""} ${t.dir}`),e.n){s.push(` 本周期 ${e.n} 次提交 · 净 +${e.netIns}/-${e.netDel} 行 · 触及 ${e.files.size} 个文件`+(e.binary?` · 二进制文件 ${e.binary} 个(行数无从统计)`:"")),e.bulk.length&&s.push(` 其中 ${e.bulk.length} 次是超大提交;**剔除后**只剩 ${e.exBulkN} 次提交 · 净 +${e.exBulkIns}/-${e.exBulkDel} 行——两个口径都在这儿,写进周报的那个要说得清是怎么算的`),e.noiseIns+e.noiseDel>0&&s.push(` (另有 +${e.noiseIns}/-${e.noiseDel} 行来自锁文件/构建产物/生成代码,已从上面的净额里剔除——若本周期的工作本身就是升级依赖,这部分要自己加回去说明)`),s.push(" 类型分布:"+Object.entries(e.kinds).sort((t,e)=>e[1]-t[1]).map(([t,e])=>`${g[t]||t} ${e}`).join(" · ")),s.push(" 主要触及:"+$(e.dirs,6).map(([t,e])=>`${t}(${e})`).join("、"));for(const e of t.commits)s.push(` ${e.date} ${e.hash} [${g[e.kind]||e.kind}] ${e.subject} +${e.ins}/-${e.del},${e.files.length} 文件${e.bulk?" ⚠超大":""}`);e.bulk.length&&(s.push(` ⚠ ${e.bulk.length} 次超大提交(≥2000 行或 ≥40 文件):`+e.bulk.map(t=>`${t.hash}「${t.subject}」`).join("、")),s.push(" 超大提交常是格式化、代码生成或副本同步。写进周报前先确认它代表真实工作量,否则「本周产出N行」会在第一次追问时塌台。"))}t.landed.length&&(s.push(` ↳ 另有 ${t.landed.length} 次提交是**更早写的、本周期才合入/整理**(rebase/cherry-pick):`+t.landed.slice(0,8).map(t=>`${t.hash}「${t.subject}」(写于 ${t.date})`).join("、")),s.push(" 这些算不算本周工作由你判断:合入与冲突处理是本周做的,代码不是。")),s.push("")}s.push(`【合计】${o.length} 个仓库 · ${i} 次提交 · 净 +${r}/-${c} 行 · `+Object.entries(p).sort((t,e)=>e[1]-t[1]).map(([t,e])=>`${g[t]||t} ${e}`).join(" · ")),l&&s.push(` 剔除 ${l} 次超大提交后:${i-l} 次提交 · 净 +${a}/-${u} 行。两个数差得越远,越说明本周期的行数不代表手写工作量——这种时候周报里干脆别写行数,写交付了什么。`);const h=new Map;for(const t of e)for(const[e,n]of t.otherAuthors)h.set(e,(h.get(e)||0)+n);return h.size&&(s.push(`【同期其它作者】${$(h,12).map(([t,e])=>`${t}×${e}`).join("、")}`),s.push(" —— 若其中有你自己的另一套name/email,加进identities重跑,否则这部分工作量会一直漏。")),s.push(""),s.push("【写周报时的口径提醒】提交次数与行数是**过程量**,不是成果。周报要写的是「这些提交合起来交付了什么、达到了什么指标」——把提交主题按项目/功能归并成 3~5 件事,每件事给出可验证的结果,不要罗列commit。"),s.join("\n")}export async function trackedFiles(t){const e=new Set,s=await mapLimit(t,GIT_CONCURRENCY,t=>async function(t){const e=new Set,s=await d(t,["-c","core.quotepath=false","ls-files","-z"],{maxBuffer:134217728});if(!s)return e;for(const o of s.split("\0"))o&&e.add(n.join(t,o));return e}(t));for(const t of s)for(const n of t)e.add(n);return e}function y(t){for(const[e,n]of p)if(n.test(t))return e;return""}export function scanDocs(e,{since:s,until:o,maxDepth:i=5,limit:r=400,tracked:c=null}={}){const a=s.getTime(),l=f(o,1).getTime(),p=[];let h=0,d=!1,m=0;const g=(e,s)=>{if(p.length>=r||h>=6e4)return void(d=!0);h++;let o=[];try{o=t.readdirSync(e,{withFileTypes:!0})}catch{return}for(const h of o){if(p.length>=r)return void(d=!0);const o=n.join(e,h.name);if(h.isSymbolicLink())continue;if(h.isDirectory()){if(u.has(h.name)||h.name.startsWith("."))continue;s<i&&g(o,s+1);continue}if(!h.isFile())continue;if(h.name.startsWith("~$")||h.name.startsWith("."))continue;const f=y(h.name);if(!f)continue;let $;try{$=t.statSync(o)}catch{continue}const k=$.mtimeMs;k<a||k>=l||(c&&c.has(o)?m++:p.push({path:o,kind:f,mtime:new Date(k),size:$.size}))}};for(const n of e)t.existsSync(n)&&g(n,0);return p.sort((t,e)=>e.mtime-t.mtime),{hits:p,truncated:d,skippedTracked:m}}export function formatDocReport(t,e,{hits:s,truncated:o,skippedTracked:i=0},{repoDirs:r=[]}={}){const c=[];if(c.push(`【文档与其它工作痕迹】${t.label}(${t.source})`),c.push("口径:按文件**最后修改时间**筛出的文档/表格/演示稿。"),c.push("⚠ 这是**弱证据**——拷贝、解压、网盘同步、打开后另存都会刷新修改时间,「文件改过」不等于「这是你的工作成果」。逐条自己认一下再写进周报。"),c.push(`扫描根目录:${e.join("、")||"(无)"}`),i&&c.push(`已排除 ${i} 个被git跟踪的文件——那些走git_worklog(有作者有行数,证据强得多);这里只留版本库之外的东西:方案稿、评审材料、台账、汇报稿。`),!s.length)return c.push("本周期内没有找到版本库之外的文档改动。"+(i?"(仓库内的文档改动请看git_worklog。)":"")),c.join("\n");const a=t=>r.some(e=>t===e||t.startsWith(e+n.sep)),u=new Map;for(const t of s){const s=r.find(e=>t.path.startsWith(e+n.sep)),o=e.find(e=>t.path.startsWith(e+n.sep))||n.dirname(t.path),i=s||(()=>{const e=n.relative(o,t.path),s=e.split(n.sep)[0];return e.includes(n.sep)?n.join(o,s):o})();u.has(i)||u.set(i,[]),u.get(i).push(t)}c.push(`共 ${s.length} 个文件${o?"(已达上限,可能没列全)":""},分布在 ${u.size} 处:`),c.push("");for(const[t,e]of[...u.entries()].sort((t,e)=>e[1].length-t[1].length)){const s=e.reduce((t,e)=>(t[e.kind]=(t[e.kind]||0)+1,t),{});c.push(`■ ${t}${a(t)?"(git仓库内——正文改动请以git_worklog为准,这里只看文档件)":""}`),c.push(" "+Object.entries(s).map(([t,e])=>`${t} ${e}`).join(" · "));for(const s of e.slice(0,20)){const e=n.relative(t,s.path)||n.basename(s.path);c.push(` ${ymd(s.mtime)} ${String(s.mtime.getHours()).padStart(2,"0")}:${String(s.mtime.getMinutes()).padStart(2,"0")} [${s.kind}] ${e} ${(s.size/1024).toFixed(0)}KB`)}e.length>20&&c.push(` …… 另有 ${e.length-20} 个未列出`),c.push("")}return c.push("【怎么用】文档名往往就是工作事项本身(《X方案》《Y评审纪要》)。把它们与git提交、TAPD需求对照,能补上「写方案、做评审、对外汇报」这类不留代码痕迹的工作。"),c.join("\n")}export const WEEKLY_SETTINGS_FILE="weekly.json";function k(e){try{const n=JSON.parse(t.readFileSync(e,"utf8"));return n&&"object"==typeof n?n:{}}catch{return{}}}export function weeklySettingsPaths(t=process.cwd()){return[n.join(t,".huicai","weekly.json"),n.join(e.homedir(),".huicai","weekly.json")]}export function weeklySettings(t=process.cwd()){const[e,n]=weeklySettingsPaths(t),s={...k(n),...k(e)},o={...k(n).tapd||{},...k(e).tapd||{}};return Object.keys(o).length&&(s.tapd=o),s}export function weeklyDefaults(t=process.cwd()){const e=weeklySettings(t),s=String(process.env.HUICAI_WEEKLY_ROOTS||"").split(n.delimiter).filter(Boolean);return{roots:(s.length?s:Array.isArray(e.roots)?e.roots:[]).map(e=>expandPath(e,t)).filter(Boolean),identities:Array.isArray(e.identities)?e.identities.map(String):[],reporter:String(e.reporter||"").trim()}}
|
|
1
|
+
import{accountDataDir as t}from"./account-scope.js";import e from"node:fs";import n from"node:os";import s from"node:path";import{execFile as o}from"node:child_process";import{promisify as i}from"node:util";import{classifyCommit as r}from"./archaeology.js";const c=i(o);export const GIT_CONCURRENCY=Math.max(2,Math.min(8,n.cpus().length));export async function mapLimit(t,e,n){const s=new Array(t.length);let o=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;;){const e=o++;if(e>=t.length)return;s[e]=await n(t[e],e)}})),s}const a=[["test",/\b(test|spec|e2e)\b|测试|用例|压测|自测/i],["docs",/\b(docs?|readme|changelog|comment)\b|文档|注释|说明书|readme/i],["perf",/\b(perf|optimiz|improve|enhance|speed[\s-]?up)\b|优化|完善|增强|提速|性能|调优/i],["chore",/\b(chore|build|deps|dependency|bump|release|ci)\b|同步|升级|依赖|版本号|构建|打包|配置调整/i],["style",/\b(style|format|lint|prettier)\b|格式化|排版调整|代码风格/i],["feat",/迁入|迁移|接入|对接|打通|开发完成|新建|搭建/]];export function commitLead(t){return(e=t,String(e||"").replace(/[\w.-]*\/[\w./-]*/g," ")).split(/[::]/)[0].slice(0,50);var e}function u(t){const e=r(t);if("other"!==e)return e;const n=commitLead(t);for(const[t,e]of a)if(e.test(n))return t;return"other"}const l=new Set(["node_modules",".git",".svn",".hg","dist","build","out","target","bin","obj","__pycache__",".venv","venv","env",".tox",".mypy_cache",".pytest_cache",".ruff_cache",".gradle",".m2",".cache",".next",".nuxt",".turbo",".parcel-cache","coverage","vendor","Pods","Carthage","DerivedData",".idea",".vscode-test","Library","Applications","System",".Trash",".npm",".pnpm-store",".docker"]),p=/(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|poetry\.lock|Cargo\.lock|composer\.lock|Gemfile\.lock|go\.sum)$|(^|\/)(dist|build|out|target|vendor|node_modules|__generated__|generated)\/|\.min\.(js|css)$|\.(map|lock)$/i,h=[["文档",/\.(docx?|wps|md|markdown|txt|rtf|odt|ofd|pdf|tex)$/i],["表格",/\.(xlsx?|xlsm|csv|et|ods|numbers)$/i],["演示",/\.(pptx?|dps|odp|key)$/i],["图表",/\.(drawio|vsdx?|xmind|mmd|puml)$/i]];export function ymd(t){const e=t=>String(t).padStart(2,"0");return`${t.getFullYear()}-${e(t.getMonth()+1)}-${e(t.getDate())}`}function f(t){const e=String(t).trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);if(!e)throw new Error("日期格式应为YYYY-MM-DD:"+t);const[n,s,o]=[Number(e[1]),Number(e[2]),Number(e[3])],i=new Date(n,s-1,o);if(i.getFullYear()!==n||i.getMonth()!==s-1||i.getDate()!==o)throw new Error(`日期不存在:${t}(不要靠JS的自动进位,请给真实存在的日期)`);return i}function m(t,e){const n=new Date(t.getTime());return n.setDate(n.getDate()+e),n}export function parseRange({since:t,until:e,days:n,week:s}={}){const o=f(ymd(new Date));if(t){const n=f(t),s=e?f(e):o;if(s<n)throw new Error(`区间反了:${ymd(n)} 晚于 ${ymd(s)}`);return{since:n,until:s,label:`${ymd(n)} ~ ${ymd(s)}`,source:"显式区间"}}if("this"===s||"last"===s){const t=(o.getDay()+6)%7,e=m(o,-t-("last"===s?7:0)),n=m(e,6);return{since:e,until:n,label:`${ymd(e)} ~ ${ymd(n)}`,source:"this"===s?"本自然周(周一至周日)":"上一自然周(周一至周日)"}}const i=Number(n),r=Math.min(120,Number.isFinite(i)&&i>0?Math.round(i):7),c=m(o,-(r-1));return{since:c,until:o,label:`${ymd(c)} ~ ${ymd(o)}`,source:`最近 ${r} 天(含今天)`}}export function cnRange(t){const e=t=>`${t.getFullYear()}年${String(t.getMonth()+1).padStart(2,"0")}月${String(t.getDate()).padStart(2,"0")}日`;return`${e(t.since)}-${e(t.until)}`}export function expandPath(t,e=process.cwd()){let o=String(t||"").trim();return o?("~"===o?o=n.homedir():(o.startsWith("~/")||o.startsWith("~\\"))&&(o=s.join(n.homedir(),o.slice(2))),s.resolve(e,o)):""}export function discoverRepos(t,{maxDepth:n=4}={}){const o=[],i=[];let r=0;const c=(t,i)=>{if(o.length>=300||r>=6e4)return;if(r++,function(t){try{return e.existsSync(s.join(t,".git"))}catch{return!1}}(t))return void o.push(t);if(i>=n)return;let a=[];try{a=e.readdirSync(t,{withFileTypes:!0})}catch{return}for(const e of a)(e.isDirectory()||e.isSymbolicLink())&&(e.name.startsWith(".")&&"."!==e.name||l.has(e.name)||e.isSymbolicLink()||c(s.join(t,e.name),i+1))};for(const n of t)e.existsSync(n)?c(n,0):i.push(n);const a=new Map;for(const t of o){let n=t;try{n=e.realpathSync(t)}catch{}a.has(n)||a.set(n,t)}return{repos:[...a.values()].sort(),missingRoots:i,truncated:o.length>=300}}async function d(t,e,n){try{return await async function(t,e,{maxBuffer:n=67108864}={}){const{stdout:s}=await c("git",e,{cwd:t,encoding:"utf8",maxBuffer:n,windowsHide:!0});return s}(t,e,n)}catch{return""}}export async function buildIdentities({configured:t=[],repos:e=[]}={}){const n=new Set;for(const e of t){const t=String(e||"").trim().toLowerCase();t&&n.add(t)}const s=await mapLimit(e,GIT_CONCURRENCY,t=>d(t,["config","--get-regexp","^user\\.(name|email)$"]));for(const t of s)for(const e of String(t).split("\n")){const t=e.indexOf(" ");if(t<0)continue;const s=e.slice(t+1).trim().toLowerCase();s&&n.add(s)}return n}const g=(t,e,n)=>t.has(String(e||"").trim().toLowerCase())||t.has(String(n||"").trim().toLowerCase());export function normalizePath(t){let e=function(t){if(!(t.startsWith('"')&&t.endsWith('"')&&t.length>=2))return t;const e=t.slice(1,-1),n=[];for(let t=0;t<e.length;t++){if("\\"!==e[t]){n.push(e.charCodeAt(t));continue}const s=e.slice(t+1,t+4);if(/^[0-7]{3}$/.test(s)){n.push(parseInt(s,8)),t+=3;continue}const o={n:10,t:9,r:13,'"':34,"\\":92},i=e[t+1];i in o?(n.push(o[i]),t+=1):n.push(e.charCodeAt(t))}try{return Buffer.from(n).toString("utf8")}catch{return t}}(String(t||"").trim());e=e.replace(/\{([^{}]*) => ([^{}]*)\}/g,"$2");const n=e.indexOf(" => ");return n>=0&&(e=e.slice(n+4)),e.replace(/\/{2,}/g,"/").trim()}export async function collectRepoCommits(t,{since:e,until:n,identities:o}){const i=ymd(m(e,-90)),r=await d(t,["-c","core.quotepath=false","log","--all","--no-merges",`--since=${i}`,"-n4000","--date=format:%Y-%m-%d %H:%M","--pretty=format:%x00%H%x1f%h%x1f%ad%x1f%cd%x1f%an%x1f%ae%x1f%s","--numstat"]),c=ymd(e),a=ymd(n),l=[],h=[],f=new Map;for(const t of r.split("\0")){if(!t.trim())continue;const e=t.indexOf("\n"),n=(e<0?t:t.slice(0,e)).split("");if(n.length<7)continue;const[s,i,r,m,d,$,y]=n,k=r.slice(0,10),b=m.slice(0,10);if(!g(o,d,$)){if(k>=c&&k<=a){const t=`${d} <${$}>`;f.set(t,(f.get(t)||0)+1)}continue}const x=k>=c&&k<=a;if(!x&&!(!x&&k<c&&b>=c&&b<=a))continue;let w=0,S=0,j=0,D=0,v=0;const _=[];if(e>=0)for(const n of t.slice(e+1).split("\n")){if(!n.trim())continue;const t=n.split("\t");if(t.length<3)continue;const e=normalizePath(t.slice(2).join("\t"));if(!e)continue;if(_.push(e),"-"===t[0]||"-"===t[1]){j++;continue}const s=Number(t[0])||0,o=Number(t[1])||0;w+=s,S+=o,p.test(e)&&(D+=s,v+=o)}const I={hash:i,date:k,time:r,commitDate:b,subject:y,kind:u(y),ins:w,del:S,binary:j,noiseIns:D,noiseDel:v,files:_,bulk:w+S>=2e3||_.length>=40};(x?l:h).push(I)}return l.sort((t,e)=>t.time<e.time?1:-1),h.sort((t,e)=>t.time<e.time?1:-1),{dir:t,name:s.basename(t),branch:(await d(t,["rev-parse","--abbrev-ref","HEAD"])).trim(),commits:l,landed:h,otherAuthors:[...f.entries()].sort((t,e)=>e[1]-t[1])}}export function summarize(t){const e={n:t.length,ins:0,del:0,binary:0,noiseIns:0,noiseDel:0,bulk:[],kinds:{},files:new Set,dirs:new Map};for(const n of t){e.ins+=n.ins,e.del+=n.del,e.binary+=n.binary,e.noiseIns+=n.noiseIns,e.noiseDel+=n.noiseDel,e.kinds[n.kind]=(e.kinds[n.kind]||0)+1,n.bulk&&e.bulk.push(n);for(const t of n.files){e.files.add(t);const n=t.includes("/")?t.slice(0,t.indexOf("/"))+"/":"(根目录)";e.dirs.set(n,(e.dirs.get(n)||0)+1)}}e.netIns=e.ins-e.noiseIns,e.netDel=e.del-e.noiseDel;const n=new Set(e.bulk);e.exBulkIns=0,e.exBulkDel=0,e.exBulkN=0;for(const s of t)n.has(s)||(e.exBulkN++,e.exBulkIns+=s.ins-s.noiseIns,e.exBulkDel+=s.del-s.noiseDel);return e}const $={feat:"新功能",fix:"修复",refactor:"重构",revert:"回滚",hotfix:"紧急修复",merge:"合并",perf:"优化",chore:"同步/构建",docs:"文档",test:"测试",style:"格式",other:"其它"};function y(t,e){return[...t.entries()].sort((t,e)=>e[1]-t[1]).slice(0,e)}export function formatGitReport(t,e,n){const s=[];s.push(`【本机Git工作记录】${t.label}(${t.source},按**作者提交时间**切窗)`),s.push(`身份:${n.identities.length?n.identities.join("、"):"(未识别到任何身份)"}`),s.push(`扫描根目录:${n.roots.join("、")||"(无)"}`);const o=e.filter(t=>t.commits.length||t.landed.length);if(s.push(`发现仓库 ${n.repoCount} 个,其中 ${o.length} 个在本周期内有你的提交`),n.missingRoots.length&&s.push(`⚠ 这些根目录不存在,已跳过:${n.missingRoots.join("、")}`),n.truncated&&s.push("⚠ 仓库数达到上限 300,可能没扫全——请把roots收窄到真正的项目目录"),s.push(""),!o.length){s.push("本周期内没有找到属于你的提交。三种可能,请逐一排除:"),s.push(" ① 根目录没设对——现在扫的是上面那几个路径;"),s.push(" ② 身份没配全——下面「其它作者」里如果有你自己的马甲,把它加进identities重跑;"),s.push(" ③ 确实没提交(活干在别处,用work_scan或tapd_fetch看)。");const t=new Map;for(const n of e)for(const[e,s]of n.otherAuthors)t.set(e,(t.get(e)||0)+s);return t.size&&s.push(" 本周期内出现过的其它作者:"+y(t,12).map(([t,e])=>`${t}×${e}`).join("、")),s.join("\n")}let i=0,r=0,c=0,a=0,u=0,l=0;const p={};for(const t of o){const e=summarize(t.commits);i+=e.n,r+=e.netIns,c+=e.netDel,a+=e.exBulkIns,u+=e.exBulkDel,l+=e.bulk.length;for(const[t,n]of Object.entries(e.kinds))p[t]=(p[t]||0)+n;if(s.push(`■ ${t.name}${t.branch?`(当前分支 ${t.branch})`:""} ${t.dir}`),e.n){s.push(` 本周期 ${e.n} 次提交 · 净 +${e.netIns}/-${e.netDel} 行 · 触及 ${e.files.size} 个文件`+(e.binary?` · 二进制文件 ${e.binary} 个(行数无从统计)`:"")),e.bulk.length&&s.push(` 其中 ${e.bulk.length} 次是超大提交;**剔除后**只剩 ${e.exBulkN} 次提交 · 净 +${e.exBulkIns}/-${e.exBulkDel} 行——两个口径都在这儿,写进周报的那个要说得清是怎么算的`),e.noiseIns+e.noiseDel>0&&s.push(` (另有 +${e.noiseIns}/-${e.noiseDel} 行来自锁文件/构建产物/生成代码,已从上面的净额里剔除——若本周期的工作本身就是升级依赖,这部分要自己加回去说明)`),s.push(" 类型分布:"+Object.entries(e.kinds).sort((t,e)=>e[1]-t[1]).map(([t,e])=>`${$[t]||t} ${e}`).join(" · ")),s.push(" 主要触及:"+y(e.dirs,6).map(([t,e])=>`${t}(${e})`).join("、"));for(const e of t.commits)s.push(` ${e.date} ${e.hash} [${$[e.kind]||e.kind}] ${e.subject} +${e.ins}/-${e.del},${e.files.length} 文件${e.bulk?" ⚠超大":""}`);e.bulk.length&&(s.push(` ⚠ ${e.bulk.length} 次超大提交(≥2000 行或 ≥40 文件):`+e.bulk.map(t=>`${t.hash}「${t.subject}」`).join("、")),s.push(" 超大提交常是格式化、代码生成或副本同步。写进周报前先确认它代表真实工作量,否则「本周产出N行」会在第一次追问时塌台。"))}t.landed.length&&(s.push(` ↳ 另有 ${t.landed.length} 次提交是**更早写的、本周期才合入/整理**(rebase/cherry-pick):`+t.landed.slice(0,8).map(t=>`${t.hash}「${t.subject}」(写于 ${t.date})`).join("、")),s.push(" 这些算不算本周工作由你判断:合入与冲突处理是本周做的,代码不是。")),s.push("")}s.push(`【合计】${o.length} 个仓库 · ${i} 次提交 · 净 +${r}/-${c} 行 · `+Object.entries(p).sort((t,e)=>e[1]-t[1]).map(([t,e])=>`${$[t]||t} ${e}`).join(" · ")),l&&s.push(` 剔除 ${l} 次超大提交后:${i-l} 次提交 · 净 +${a}/-${u} 行。两个数差得越远,越说明本周期的行数不代表手写工作量——这种时候周报里干脆别写行数,写交付了什么。`);const h=new Map;for(const t of e)for(const[e,n]of t.otherAuthors)h.set(e,(h.get(e)||0)+n);return h.size&&(s.push(`【同期其它作者】${y(h,12).map(([t,e])=>`${t}×${e}`).join("、")}`),s.push(" —— 若其中有你自己的另一套name/email,加进identities重跑,否则这部分工作量会一直漏。")),s.push(""),s.push("【写周报时的口径提醒】提交次数与行数是**过程量**,不是成果。周报要写的是「这些提交合起来交付了什么、达到了什么指标」——把提交主题按项目/功能归并成 3~5 件事,每件事给出可验证的结果,不要罗列commit。"),s.join("\n")}export async function trackedFiles(t){const e=new Set,n=await mapLimit(t,GIT_CONCURRENCY,t=>async function(t){const e=new Set,n=await d(t,["-c","core.quotepath=false","ls-files","-z"],{maxBuffer:134217728});if(!n)return e;for(const o of n.split("\0"))o&&e.add(s.join(t,o));return e}(t));for(const t of n)for(const n of t)e.add(n);return e}function k(t){for(const[e,n]of h)if(n.test(t))return e;return""}export function scanDocs(t,{since:n,until:o,maxDepth:i=5,limit:r=400,tracked:c=null}={}){const a=n.getTime(),u=m(o,1).getTime(),p=[];let h=0,f=!1,d=0;const g=(t,n)=>{if(p.length>=r||h>=6e4)return void(f=!0);h++;let o=[];try{o=e.readdirSync(t,{withFileTypes:!0})}catch{return}for(const h of o){if(p.length>=r)return void(f=!0);const o=s.join(t,h.name);if(h.isSymbolicLink())continue;if(h.isDirectory()){if(l.has(h.name)||h.name.startsWith("."))continue;n<i&&g(o,n+1);continue}if(!h.isFile())continue;if(h.name.startsWith("~$")||h.name.startsWith("."))continue;const m=k(h.name);if(!m)continue;let $;try{$=e.statSync(o)}catch{continue}const y=$.mtimeMs;y<a||y>=u||(c&&c.has(o)?d++:p.push({path:o,kind:m,mtime:new Date(y),size:$.size}))}};for(const n of t)e.existsSync(n)&&g(n,0);return p.sort((t,e)=>e.mtime-t.mtime),{hits:p,truncated:f,skippedTracked:d}}export function formatDocReport(t,e,{hits:n,truncated:o,skippedTracked:i=0},{repoDirs:r=[]}={}){const c=[];if(c.push(`【文档与其它工作痕迹】${t.label}(${t.source})`),c.push("口径:按文件**最后修改时间**筛出的文档/表格/演示稿。"),c.push("⚠ 这是**弱证据**——拷贝、解压、网盘同步、打开后另存都会刷新修改时间,「文件改过」不等于「这是你的工作成果」。逐条自己认一下再写进周报。"),c.push(`扫描根目录:${e.join("、")||"(无)"}`),i&&c.push(`已排除 ${i} 个被git跟踪的文件——那些走git_worklog(有作者有行数,证据强得多);这里只留版本库之外的东西:方案稿、评审材料、台账、汇报稿。`),!n.length)return c.push("本周期内没有找到版本库之外的文档改动。"+(i?"(仓库内的文档改动请看git_worklog。)":"")),c.join("\n");const a=t=>r.some(e=>t===e||t.startsWith(e+s.sep)),u=new Map;for(const t of n){const n=r.find(e=>t.path.startsWith(e+s.sep)),o=e.find(e=>t.path.startsWith(e+s.sep))||s.dirname(t.path),i=n||(()=>{const e=s.relative(o,t.path),n=e.split(s.sep)[0];return e.includes(s.sep)?s.join(o,n):o})();u.has(i)||u.set(i,[]),u.get(i).push(t)}c.push(`共 ${n.length} 个文件${o?"(已达上限,可能没列全)":""},分布在 ${u.size} 处:`),c.push("");for(const[t,e]of[...u.entries()].sort((t,e)=>e[1].length-t[1].length)){const n=e.reduce((t,e)=>(t[e.kind]=(t[e.kind]||0)+1,t),{});c.push(`■ ${t}${a(t)?"(git仓库内——正文改动请以git_worklog为准,这里只看文档件)":""}`),c.push(" "+Object.entries(n).map(([t,e])=>`${t} ${e}`).join(" · "));for(const n of e.slice(0,20)){const e=s.relative(t,n.path)||s.basename(n.path);c.push(` ${ymd(n.mtime)} ${String(n.mtime.getHours()).padStart(2,"0")}:${String(n.mtime.getMinutes()).padStart(2,"0")} [${n.kind}] ${e} ${(n.size/1024).toFixed(0)}KB`)}e.length>20&&c.push(` …… 另有 ${e.length-20} 个未列出`),c.push("")}return c.push("【怎么用】文档名往往就是工作事项本身(《X方案》《Y评审纪要》)。把它们与git提交、TAPD需求对照,能补上「写方案、做评审、对外汇报」这类不留代码痕迹的工作。"),c.join("\n")}export const WEEKLY_SETTINGS_FILE="weekly.json";function b(t){try{const n=JSON.parse(e.readFileSync(t,"utf8"));return n&&"object"==typeof n?n:{}}catch{return{}}}export function weeklySettingsPaths(e=process.cwd()){return[s.join(e,".huicai","weekly.json"),s.join(t(),"weekly.json")]}export function weeklySettings(t=process.cwd()){const[e,n]=weeklySettingsPaths(t),s={...b(n),...b(e)},o={...b(n).tapd||{},...b(e).tapd||{}};return Object.keys(o).length&&(s.tapd=o),s}export function weeklyDefaults(t=process.cwd()){const e=weeklySettings(t),n=String(process.env.HUICAI_WEEKLY_ROOTS||"").split(s.delimiter).filter(Boolean);return{roots:(n.length?n:Array.isArray(e.roots)?e.roots:[]).map(e=>expandPath(e,t)).filter(Boolean),identities:Array.isArray(e.identities)?e.identities.map(String):[],reporter:String(e.reporter||"").trim()}}
|