@huicai/cli 2026.1019.0 → 2026.1022.59980
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/huicai.js +55 -12
- package/bin/update.js +97 -0
- package/package.json +2 -2
- package/skills/ai-coding/SKILL.md +2 -0
- package/skills/eng-develop/SKILL.md +9 -1
- package/skills/eng-develop/references/dod-checklist.md +3 -0
- package/skills/eng-develop/references/engineering-workflow.md +37 -0
- package/src/account-scope.js +1 -0
- package/src/agent.js +1 -1
- package/src/api.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/engineering.js +1 -0
- package/src/executors.js +1 -1
- package/src/experts-store.js +1 -1
- package/src/experts.js +1 -1
- package/src/ide.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/test-runner.js +1 -1
- package/src/toolpipe.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/uninstall.js +1 -1
- package/src/vision.js +1 -1
- package/src/workflow.js +1 -1
- package/src/worklog.js +1 -1
package/src/test-runner.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import t from"node:path";import s from"node:os";import{captureCommand as n}from"./autofix.js";const r=[{id:"maven",detect:s=>e.existsSync(t.join(s,"pom.xml")),command:e=>"mvn -q -B test"+(e?` -Dtest=${o(e)}`:""),reports:["**/target/surefire-reports","**/target/failsafe-reports"],format:"junit-xml"},{id:"gradle",detect:s=>e.existsSync(t.join(s,"build.gradle"))||e.existsSync(t.join(s,"build.gradle.kts")),command:e=>("win32"===process.platform?"gradlew.bat":"./gradlew")+" test"+(e?` --tests ${o(e)}`:""),reports:["**/build/test-results/test"],format:"junit-xml"},{id:"pytest",detect:s=>e.existsSync(t.join(s,"pytest.ini"))||e.existsSync(t.join(s,"tox.ini"))||e.existsSync(t.join(s,"conftest.py"))||e.existsSync(t.join(s,"pyproject.toml"))&&/\[tool\.pytest/.test(i(t.join(s,"pyproject.toml")))||a(s,e=>/^test_.*\.py$|_test\.py$/.test(e)),command:(e,t)=>`pytest -q --junit-xml=${o(t)}`+(e?` -k ${o(e)}`:""),format:"junit-xml",ownXml:!0},{id:"node",detect:s=>{const n=function(t){try{return JSON.parse(e.readFileSync(t,"utf8"))}catch{return null}}(t.join(s,"package.json"));return!!(n&&n.scripts&&n.scripts.test)},command:e=>"npm test --silent"+(e?` -- --test-name-pattern=${o(e)}`:""),format:"tap"}];function i(t){try{return e.readFileSync(t,"utf8")}catch{return""}}function a(s,n,r=2){try{for(const i of e.readdirSync(s,{withFileTypes:!0}))if(!i.name.startsWith(".")&&"node_modules"!==i.name){if(i.isFile()&&n(i.name))return!0;if(i.isDirectory()&&r>0&&a(t.join(s,i.name),n,r-1))return!0}}catch{}return!1}function o(e){const t=String(e);return/^[\w./:@=-]+$/.test(t)?t:`'${t.replace(/'/g,"'\\''")}'`}export function detectRunner(e,t){return t?r.find(e=>e.id===t)||null:r.find(t=>{try{return t.detect(e)}catch{return!1}})||null}const c=/<testcase\b([^>]*?)(\/>|>([\s\S]*?)<\/testcase>)/g,l=/(\w[\w:.-]*)\s*=\s*"([^"]*)"/g,p=/<(failure|error)\b([^>]*?)(?:\/>|>([\s\S]*?)<\/\1>)/;function u(e){const t={};let s;for(l.lastIndex=0;s=l.exec(e);)t[s[1]]=d(s[2]);return t}function d(e){return String(e).replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/ /g,"\n").replace(/	/g,"\t").replace(/&/g,"&")}export function parseJUnitXml(e,t){let s;for(c.lastIndex=0;s=c.exec(e);){const e=u(s[1]),n=s[3]||"";if(t.total+=1,t.timeMs+=Math.round(1e3*Number(e.time||0)),/<skipped\b/.test(n)){t.skipped+=1;continue}const r=p.exec(n);if(!r){t.passed+=1;continue}const i=u(r[2]||"");t.failed+=1,t.failures.push({suite:e.classname||e.class||"",name:e.name||"(未命名用例)",kind:"error"===r[1]?"报错":"断言失败",message:(i.message||"").trim(),type:i.type||"",stack:d(r[3]||"").trim()})}return t}function m(s,n=[],r=6){let i=[];try{i=e.readdirSync(s,{withFileTypes:!0})}catch{return n}for(const e of i){const i=t.join(s,e.name);if(e.isDirectory()){if("node_modules"===e.name||e.name.startsWith("."))continue;r>0&&m(i,n,r-1)}else(/^TEST-.*\.xml$/.test(e.name)||/\.xml$/.test(e.name))&&n.push(i)}return n}function f(s,n=4){const r=[],i=(s,n)=>{let a=[];try{a=e.readdirSync(s,{withFileTypes:!0})}catch{return}for(const e of a){if(!e.isDirectory()||e.name.startsWith(".")||"node_modules"===e.name)continue;const a=t.join(s,e.name);"surefire-reports"===e.name||"failsafe-reports"===e.name||"test-results"===e.name?r.push(a):n>0&&i(a,n-1)}};return i(s,n),r}const y=/^[✔✓]\s+(.+?)\s*\(\d[\d.]*\s*ms\)\s*$/,x=/^[✖✗×]\s+(.+?)\s*\(\d[\d.]*\s*ms\)\s*$/;export function parseNodeSpec(e,t){const s=String(e||""),n=s.split("\n"),r=e=>{const t=new RegExp(`^\\s*[ℹi]\\s+${e}\\s+(\\d+)\\s*$`,"m").exec(s);return t?Number(t[1]):null},i=r("pass"),a=r("fail"),o=r("skipped");if(null!==i||null!==a)t.passed+=i||0,t.failed+=a||0,t.skipped+=o||0,t.total+=(i||0)+(a||0)+(o||0);else for(const e of n){const s=e.trim();y.test(s)?(t.total+=1,t.passed+=1):x.test(s)&&(t.total+=1,t.failed+=1)}const c=n.findIndex(e=>/failing tests:/.test(e));if(c<0)return t;let l=null;const p=new Set;for(const e of n.slice(c+1)){const s=e.trim(),n=x.exec(s);if(n){if(p.has(n[1])){l=null;continue}p.add(n[1]),l={suite:"",name:n[1],kind:"断言失败",message:"",type:"",stack:""},t.failures.push(l);continue}if(!l)continue;const r=/^actual:\s*(.+?),?$/.exec(s),i=/^expected:\s*(.+?),?$/.exec(s);r?l.actual=r[1]:i?l.expected=i[1]:/Error|AssertionError/.test(s)&&!l.message?l.message=s:(/^at\s+/.test(s)||/^test at /.test(s))&&(l.stack+=(l.stack?"\n":"")+s)}return t}export function parseTap(e,t){const s=String(e||"").split("\n");let n=null;for(const e of s){const s=e.replace(/^\s*/,""),r=/^ok\s+\d+\s*-?\s*(.*)$/.exec(s),i=/^not ok\s+\d+\s*-?\s*(.*)$/.exec(s);if(r)t.total+=1,/#\s*SKIP/i.test(r[1])?t.skipped+=1:t.passed+=1,n=null;else if(i)t.total+=1,t.failed+=1,n={suite:"",name:i[1].trim(),kind:"断言失败",message:"",type:"",stack:""},t.failures.push(n);else if(n){/^(?:error|expected|actual|operator):\s*(.*)$/i.exec(s)&&!n.message?n.message=s.trim():/^at\s+/.test(s)&&(n.stack+=(n.stack?"\n":"")+s.trim())}const a=/^#\s*(pass|fail|skipped)\s+(\d+)$/.exec(s);a&&0===t.total&&("pass"===a[1]&&(t.passed=Number(a[2])),"fail"===a[1]&&(t.failed=Number(a[2])),"skipped"===a[1]&&(t.skipped=Number(a[2])),t.total=t.passed+t.failed+t.skipped)}return t}const g=new RegExp(["at\\s+(java|javax|jdk|sun|com\\.sun|kotlin)\\.","org\\.(junit|mockito|hamcrest|assertj|testng|springframework|apache\\.maven|gradle)","surefire|failsafe|\\breflect\\b|jdk\\.internal","pytest|_pytest|site-packages|unittest[/\\\\.]","node:internal|node_modules|internal/test_runner","System\\.(Runtime|Threading|Reflection)|xunit|nunit|Microsoft\\.VisualStudio\\.TestPlatform"].join("|"),"i"),$=/([Tt]ests?\.(java|py|cs)|_test\.py|test_\w+\.py|\.test\.[cm]?[jt]s|\.spec\.[cm]?[jt]s)/;export function locateSourceFrame(e){let t=null;for(const s of String(e||"").split("\n")){const e=s.trim();if(!e||g.test(e))continue;let n=/\(([\w./$-]+\.(?:java|kt)):(\d+)\)/.exec(e);if(n||(n=/File\s+"([^"]+\.py)",\s+line\s+(\d+)/.exec(e)),n||(n=/^([\w./$-]+\.py):(\d+):/.exec(e)),n||(n=/\(?([\w./$-]+\.[cm]?[jt]sx?):(\d+)(?::\d+)?\)?$/.exec(e)),!n)continue;const r={file:n[1],line:Number(n[2])};if(!$.test(n[1]))return{...r,isTest:!1};t||(t={...r,isTest:!0})}return t}export function extractAssertion(e){const t=String(e||"").split("\n")[0];let s=/expected:?\s*<?(.+?)>?\s+but\s+(?:was|got):?\s*<?(.+?)>?\s*$/is.exec(t);const n=e=>String(e).trim().replace(/\s+/g," ").replace(/[.,;]$/,"").slice(0,160);return s?{expected:n(s[1]),actual:n(s[2])}:(s=/Expected:\s*(.+?)[\r\n]+\s*(?:Actual|but was):\s*(.+?)$/is.exec(t),s?{expected:n(s[1]),actual:n(s[2])}:(s=/\bassert\s+(.+?)\s*==\s*(.+?)\s*$/is.exec(t),s?{expected:n(s[2]),actual:n(s[1])}:null))}export async function runTests(r,a={}){const o=detectRunner(r,a.framework);if(!o)return{error:"没有探测到测试框架。支持:Maven(pom.xml) / Gradle(build.gradle) / pytest(pytest.ini·conftest.py·test_*.py) / Node(package.json的test脚本)。\n可以用framework参数强制指定,或直接用run_bash跑你自己的命令。"};const c=o.ownXml?t.join(s.tmpdir(),`huicai-tests-${process.pid}-${Date.now()}.xml`):"",l=o.ownXml?[]:f(r),p=new Map;for(const t of l)for(const s of m(t))try{p.set(s,e.statSync(s).mtimeMs)}catch{}const u=o.command(a.filter||"",c),d=Date.now(),{code:y,output:x}=await n(r,u,Number(a.timeoutMs)||6e5),g=Date.now()-d,$={total:0,passed:0,failed:0,skipped:0,timeMs:0,failures:[]};let h="";if("junit-xml"===o.format){const t=o.ownXml?e.existsSync(c)?[c]:[]:f(r).flatMap(e=>m(e)).filter(t=>{try{return!p.has(t)||e.statSync(t).mtimeMs>p.get(t)}catch{return!1}});for(const e of t)parseJUnitXml(i(e),$);if(h=t.length?`${t.length} 份JUnit XML报告`:"",o.ownXml)try{e.unlinkSync(c)}catch{}}else"tap"===o.format&&(parseTap(x,$),0===$.total?(parseNodeSpec(x,$),h=$.total?"node:test输出":""):h="TAP输出");const k=$.total>0;return{runner:o.id,command:u,code:y,wallMs:g,stats:{...$,failures:void 0},failures:$.failures,parsedFrom:h,compiledOk:k,raw:x}}export function renderTestReport(e,s="."){if(e.error)return e.error;const n=e.stats,r=`【测试结果】${e.runner} · ${(e.wallMs/1e3).toFixed(1)}s`;if(!e.compiledOk){const t=String(e.raw||"").split("\n").filter(Boolean).slice(-40).join("\n");return`${r}\n✗ **一个用例都没跑起来**(退出码 ${e.code})——通常是编译失败、依赖缺失或用例收集报错。\n命令:${e.command}\n\n原始输出(末 40 行):\n${t}\n\n先把它跑起来再谈测试结果:编译错误看上面第一个ERROR的文件与行号。`}const i=[`${r} · 解析自${e.parsedFrom}`,`通过 ${n.passed} · 失败 ${n.failed} · 跳过 ${n.skipped} · 共 ${n.total}`];if(0===n.failed)return i.push("✅ 全部通过。"),n.skipped&&i.push(`ℹ 有 ${n.skipped} 个用例被跳过——确认是有意跳过,而不是被skip标记忘了去掉。`),i.join("\n");i.push("");const a=e.failures.slice(0,12);return a.forEach((e,r)=>{const a=e.suite?`${e.suite}.${e.name}`:e.name;i.push(`── 失败 ${r+1}/${n.failed}:${a}`);const o=void 0!==e.expected||void 0!==e.actual?{expected:String(e.expected??"(未给出)"),actual:String(e.actual??"(未给出)")}:extractAssertion(e.message);o?(i.push(` 期望:${o.expected}`),i.push(` 实际:${o.actual}`)):e.message?i.push(` ${e.kind}:${e.message.split("\n")[0].slice(0,200)}`):i.push(` ${e.kind}(无消息)`);const c=locateSourceFrame(e.stack)||locateSourceFrame(e.message);if(c){const e=t.isAbsolute(c.file)?t.relative(s,c.file):c.file;i.push(c.isTest?` → 用例位置:${e}:${c.line}(堆栈里没有业务代码帧)`:` → 被测代码:${e}:${c.line}`)}}),e.failures.length>a.length&&i.push(`…… 另有 ${e.failures.length-a.length} 个失败用例未展开(用filter缩小范围逐个看)。`),i.push(""),i.push("先读「期望vs实际」——两者的差异通常直接指出是业务逻辑错了还是用例的预期写错了;再看「被测代码」那一行(它是堆栈里第一个既非测试、也非框架的位置)。"),i.join("\n")}export function makeTestTools({cwd:s,config:n={}}){return[{readOnly:!1,spec:{type:"function",function:{name:"run_tests",description:"跑单元测试并**把结果解析成结构化摘要**——改完代码验证、排查失败用例时用它,不要用run_bash跑测试。\n两者的差别在于你拿到什么:run_bash给你几千行原始输出(Maven的下载与编译日志、pytest堆栈里十几层框架调用),要你自己从噪声里找哪个用例失败、为什么;本工具直接给出「通过/失败/跳过的数量 + 每个失败用例的期望值与实际值 + **堆栈里第一个非测试非框架的位置**(也就是你的哪一行业务代码出的问题)」。\n自动探测框架:Maven(pom.xml) · Gradle(build.gradle) · pytest(pytest.ini/conftest.py/test_*.py) · Node(package.json的test脚本)。Maven/Gradle/pytest都走JUnit XML报告,结果精确到用例级。\n用filter只跑相关用例(Maven是 -Dtest、Gradle是 --tests、pytest是 -k、Node是用例名模式)——定位问题时比全量跑快得多。\n**一个用例都没跑起来时会如实说明**(编译失败/依赖缺失/收集报错),并给出原始输出末尾,不会伪装成「0 个用例、全部通过」。",parameters:{type:"object",properties:{path:{type:"string",description:"项目目录(相对工作目录),默认当前目录"},filter:{type:"string",description:"只跑匹配的用例。Maven:`OrderServiceTest` 或 `OrderServiceTest#某方法`;Gradle:`*.OrderServiceTest`;pytest:`-k` 表达式如 `settle and not slow`;Node:用例名片段"},framework:{type:"string",description:"强制指定框架(maven / gradle / pytest / node)。不给则自动探测"},timeout:{type:"number",description:"超时毫秒,默认 600000(10 分钟)"}},required:[]}}},run:async n=>{const r=t.resolve(s,String(n?.path||".").trim()||".");if(r!==s&&!r.startsWith(s+t.sep))return"路径越出工作目录:"+n.path;if(!e.existsSync(r))return"路径不存在:"+n.path;try{return renderTestReport(await runTests(r,{filter:n?.filter,framework:n?.framework,timeoutMs:n?.timeout}),r)}catch(e){return"测试未能运行:"+(e.message||String(e))}}}]}
|
|
1
|
+
import e from"node:fs";import t from"node:path";import s from"node:os";import{captureCommand as n}from"./autofix.js";const r=[{id:"maven",detect:s=>e.existsSync(t.join(s,"pom.xml")),command:e=>"mvn -q -B test"+(e?` -Dtest=${o(e)}`:""),reports:["**/target/surefire-reports","**/target/failsafe-reports"],format:"junit-xml"},{id:"gradle",detect:s=>e.existsSync(t.join(s,"build.gradle"))||e.existsSync(t.join(s,"build.gradle.kts")),command:e=>("win32"===process.platform?"gradlew.bat":"./gradlew")+" test"+(e?` --tests ${o(e)}`:""),reports:["**/build/test-results/test"],format:"junit-xml"},{id:"pytest",detect:s=>e.existsSync(t.join(s,"pytest.ini"))||e.existsSync(t.join(s,"tox.ini"))||e.existsSync(t.join(s,"conftest.py"))||e.existsSync(t.join(s,"pyproject.toml"))&&/\[tool\.pytest/.test(i(t.join(s,"pyproject.toml")))||a(s,e=>/^test_.*\.py$|_test\.py$/.test(e)),command:(e,t)=>`pytest -q --junit-xml=${o(t)}`+(e?` -k ${o(e)}`:""),format:"junit-xml",ownXml:!0},{id:"node",detect:s=>{const n=function(t){try{return JSON.parse(e.readFileSync(t,"utf8"))}catch{return null}}(t.join(s,"package.json"));return!!(n&&n.scripts&&n.scripts.test)},command:e=>"npm test --silent"+(e?` -- --test-name-pattern=${o(e)}`:""),format:"tap"}];function i(t){try{return e.readFileSync(t,"utf8")}catch{return""}}function a(s,n,r=2){try{for(const i of e.readdirSync(s,{withFileTypes:!0}))if(!i.name.startsWith(".")&&"node_modules"!==i.name){if(i.isFile()&&n(i.name))return!0;if(i.isDirectory()&&r>0&&a(t.join(s,i.name),n,r-1))return!0}}catch{}return!1}function o(e){const t=String(e);return/^[\w./:@=-]+$/.test(t)?t:`'${t.replace(/'/g,"'\\''")}'`}export function detectRunner(e,t){return t?r.find(e=>e.id===t)||null:r.find(t=>{try{return t.detect(e)}catch{return!1}})||null}const c=/<testcase\b([^>]*?)(\/>|>([\s\S]*?)<\/testcase>)/g,l=/(\w[\w:.-]*)\s*=\s*"([^"]*)"/g,p=/<(failure|error)\b([^>]*?)(?:\/>|>([\s\S]*?)<\/\1>)/;function u(e){const t={};let s;for(l.lastIndex=0;s=l.exec(e);)t[s[1]]=d(s[2]);return t}function d(e){return String(e).replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/ /g,"\n").replace(/	/g,"\t").replace(/&/g,"&")}export function parseJUnitXml(e,t){let s;for(c.lastIndex=0;s=c.exec(e);){const e=u(s[1]),n=s[3]||"";if(t.total+=1,t.timeMs+=Math.round(1e3*Number(e.time||0)),/<skipped\b/.test(n)){t.skipped+=1;continue}const r=p.exec(n);if(!r){t.passed+=1;continue}const i=u(r[2]||"");t.failed+=1,t.failures.push({suite:e.classname||e.class||"",name:e.name||"(未命名用例)",kind:"error"===r[1]?"报错":"断言失败",message:(i.message||"").trim(),type:i.type||"",stack:d(r[3]||"").trim()})}return t}function m(s,n=[],r=6){let i=[];try{i=e.readdirSync(s,{withFileTypes:!0})}catch{return n}for(const e of i){const i=t.join(s,e.name);if(e.isDirectory()){if("node_modules"===e.name||e.name.startsWith("."))continue;r>0&&m(i,n,r-1)}else(/^TEST-.*\.xml$/.test(e.name)||/\.xml$/.test(e.name))&&n.push(i)}return n}function f(s,n=4){const r=[],i=(s,n)=>{let a=[];try{a=e.readdirSync(s,{withFileTypes:!0})}catch{return}for(const e of a){if(!e.isDirectory()||e.name.startsWith(".")||"node_modules"===e.name)continue;const a=t.join(s,e.name);"surefire-reports"===e.name||"failsafe-reports"===e.name||"test-results"===e.name?r.push(a):n>0&&i(a,n-1)}};return i(s,n),r}const y=/^[✔✓]\s+(.+?)\s*\(\d[\d.]*\s*ms\)\s*$/,g=/^[✖✗×]\s+(.+?)\s*\(\d[\d.]*\s*ms\)\s*$/;export function parseNodeSpec(e,t){const s=String(e||""),n=s.split("\n"),r=e=>{const t=new RegExp(`^\\s*[ℹi]\\s+${e}\\s+(\\d+)\\s*$`,"m").exec(s);return t?Number(t[1]):null},i=r("pass"),a=r("fail"),o=r("skipped");if(null!==i||null!==a)t.passed+=i||0,t.failed+=a||0,t.skipped+=o||0,t.total+=(i||0)+(a||0)+(o||0);else for(const e of n){const s=e.trim();y.test(s)?(t.total+=1,t.passed+=1):g.test(s)&&(t.total+=1,t.failed+=1)}const c=n.findIndex(e=>/failing tests:/.test(e));if(c<0)return t;let l=null;const p=new Set;for(const e of n.slice(c+1)){const s=e.trim(),n=g.exec(s);if(n){if(p.has(n[1])){l=null;continue}p.add(n[1]),l={suite:"",name:n[1],kind:"断言失败",message:"",type:"",stack:""},t.failures.push(l);continue}if(!l)continue;const r=/^actual:\s*(.+?),?$/.exec(s),i=/^expected:\s*(.+?),?$/.exec(s);r?l.actual=r[1]:i?l.expected=i[1]:/Error|AssertionError/.test(s)&&!l.message?l.message=s:(/^at\s+/.test(s)||/^test at /.test(s))&&(l.stack+=(l.stack?"\n":"")+s)}return t}export function parseTap(e,t){const s=String(e||"").split("\n");let n=null;for(const e of s){const s=e.replace(/^\s*/,""),r=/^ok\s+\d+\s*-?\s*(.*)$/.exec(s),i=/^not ok\s+\d+\s*-?\s*(.*)$/.exec(s);if(r)t.total+=1,/#\s*SKIP/i.test(r[1])?t.skipped+=1:t.passed+=1,n=null;else if(i)t.total+=1,t.failed+=1,n={suite:"",name:i[1].trim(),kind:"断言失败",message:"",type:"",stack:""},t.failures.push(n);else if(n){/^(?:error|expected|actual|operator):\s*(.*)$/i.exec(s)&&!n.message?n.message=s.trim():/^at\s+/.test(s)&&(n.stack+=(n.stack?"\n":"")+s.trim())}const a=/^#\s*(pass|fail|skipped)\s+(\d+)$/.exec(s);a&&0===t.total&&("pass"===a[1]&&(t.passed=Number(a[2])),"fail"===a[1]&&(t.failed=Number(a[2])),"skipped"===a[1]&&(t.skipped=Number(a[2])),t.total=t.passed+t.failed+t.skipped)}return t}const x=new RegExp(["at\\s+(java|javax|jdk|sun|com\\.sun|kotlin)\\.","org\\.(junit|mockito|hamcrest|assertj|testng|springframework|apache\\.maven|gradle)","surefire|failsafe|\\breflect\\b|jdk\\.internal","pytest|_pytest|site-packages|unittest[/\\\\.]","node:internal|node_modules|internal/test_runner","System\\.(Runtime|Threading|Reflection)|xunit|nunit|Microsoft\\.VisualStudio\\.TestPlatform"].join("|"),"i"),$=/([Tt]ests?\.(java|py|cs)|_test\.py|test_\w+\.py|\.test\.[cm]?[jt]s|\.spec\.[cm]?[jt]s)/;export function locateSourceFrame(e){let t=null;for(const s of String(e||"").split("\n")){const e=s.trim();if(!e||x.test(e))continue;let n=/\(([\w./$-]+\.(?:java|kt)):(\d+)\)/.exec(e);if(n||(n=/File\s+"([^"]+\.py)",\s+line\s+(\d+)/.exec(e)),n||(n=/^([\w./$-]+\.py):(\d+):/.exec(e)),n||(n=/\(?([\w./$-]+\.[cm]?[jt]sx?):(\d+)(?::\d+)?\)?$/.exec(e)),!n)continue;const r={file:n[1],line:Number(n[2])};if(!$.test(n[1]))return{...r,isTest:!1};t||(t={...r,isTest:!0})}return t}export function extractAssertion(e){const t=String(e||"").split("\n")[0];let s=/expected:?\s*<?(.+?)>?\s+but\s+(?:was|got):?\s*<?(.+?)>?\s*$/is.exec(t);const n=e=>String(e).trim().replace(/\s+/g," ").replace(/[.,;]$/,"").slice(0,160);return s?{expected:n(s[1]),actual:n(s[2])}:(s=/Expected:\s*(.+?)[\r\n]+\s*(?:Actual|but was):\s*(.+?)$/is.exec(t),s?{expected:n(s[1]),actual:n(s[2])}:(s=/\bassert\s+(.+?)\s*==\s*(.+?)\s*$/is.exec(t),s?{expected:n(s[2]),actual:n(s[1])}:null))}export async function runTests(r,a={}){const o=detectRunner(r,a.framework);if(!o)return{error:"没有探测到测试框架。支持:Maven(pom.xml) / Gradle(build.gradle) / pytest(pytest.ini·conftest.py·test_*.py) / Node(package.json的test脚本)。\n可以用framework参数强制指定,或直接用run_bash跑你自己的命令。"};const c=o.ownXml?t.join(s.tmpdir(),`huicai-tests-${process.pid}-${Date.now()}.xml`):"",l=o.ownXml?[]:f(r),p=new Map;for(const t of l)for(const s of m(t))try{p.set(s,e.statSync(s).mtimeMs)}catch{}const u=o.command(a.filter||"",c),d=Date.now(),{code:y,output:g}=await n(r,u,Number(a.timeoutMs)||6e5,a.signal),x=Date.now()-d,$={total:0,passed:0,failed:0,skipped:0,timeMs:0,failures:[]};let h="";if("junit-xml"===o.format){const t=o.ownXml?e.existsSync(c)?[c]:[]:f(r).flatMap(e=>m(e)).filter(t=>{try{return!p.has(t)||e.statSync(t).mtimeMs>p.get(t)}catch{return!1}});for(const e of t)parseJUnitXml(i(e),$);if(h=t.length?`${t.length} 份JUnit XML报告`:"",o.ownXml)try{e.unlinkSync(c)}catch{}}else"tap"===o.format&&(parseTap(g,$),0===$.total?(parseNodeSpec(g,$),h=$.total?"node:test输出":""):h="TAP输出");const k=$.total>0;return{runner:o.id,command:u,code:y,wallMs:x,stats:{...$,failures:void 0},failures:$.failures,parsedFrom:h,compiledOk:k,raw:g}}export function renderTestReport(e,s="."){if(e.error)return e.error;const n=e.stats,r=`【测试结果】${e.runner} · ${(e.wallMs/1e3).toFixed(1)}s`;if(!e.compiledOk){const t=String(e.raw||"").split("\n").filter(Boolean).slice(-40).join("\n");return`${r}\n✗ **一个用例都没跑起来**(退出码 ${e.code})——通常是编译失败、依赖缺失或用例收集报错。\n命令:${e.command}\n\n原始输出(末 40 行):\n${t}\n\n先把它跑起来再谈测试结果:编译错误看上面第一个ERROR的文件与行号。`}const i=[`${r} · 解析自${e.parsedFrom}`,`通过 ${n.passed} · 失败 ${n.failed} · 跳过 ${n.skipped} · 共 ${n.total}`];if(0===n.failed&&0===e.code&&n.passed>0)return i.push("✅ 全部通过。"),n.skipped&&i.push(`ℹ 有 ${n.skipped} 个用例被跳过——确认是有意跳过,而不是被skip标记忘了去掉。`),i.join("\n");0===e.code&&0!==n.passed||(i.push(`✗ 验证未通过:退出码 ${e.code},实际通过 ${n.passed} 个用例。`),i.push(String(e.raw||"").split("\n").slice(-20).join("\n"))),i.push("");const a=e.failures.slice(0,12);return a.forEach((e,r)=>{const a=e.suite?`${e.suite}.${e.name}`:e.name;i.push(`── 失败 ${r+1}/${n.failed}:${a}`);const o=void 0!==e.expected||void 0!==e.actual?{expected:String(e.expected??"(未给出)"),actual:String(e.actual??"(未给出)")}:extractAssertion(e.message);o?(i.push(` 期望:${o.expected}`),i.push(` 实际:${o.actual}`)):e.message?i.push(` ${e.kind}:${e.message.split("\n")[0].slice(0,200)}`):i.push(` ${e.kind}(无消息)`);const c=locateSourceFrame(e.stack)||locateSourceFrame(e.message);if(c){const e=t.isAbsolute(c.file)?t.relative(s,c.file):c.file;i.push(c.isTest?` → 用例位置:${e}:${c.line}(堆栈里没有业务代码帧)`:` → 被测代码:${e}:${c.line}`)}}),e.failures.length>a.length&&i.push(`…… 另有 ${e.failures.length-a.length} 个失败用例未展开(用filter缩小范围逐个看)。`),i.push(""),i.push("先读「期望vs实际」——两者的差异通常直接指出是业务逻辑错了还是用例的预期写错了;再看「被测代码」那一行(它是堆栈里第一个既非测试、也非框架的位置)。"),i.join("\n")}export function makeTestTools({cwd:s,config:n={}}){const r=async(n,r,i)=>{const a=t.resolve(s,String(n?.path||".").trim()||".");if(a!==s&&!a.startsWith(s+t.sep))return"路径越出工作目录:"+n.path;if(!e.existsSync(a))return"路径不存在:"+n.path;try{const e=await runTests(a,{filter:n?.filter,framework:n?.framework,timeoutMs:n?.timeout,signal:i});return r?.(e),renderTestReport(e,a)}catch(e){return"测试未能运行:"+(e.message||String(e))}};return[{readOnly:!1,timeoutMs:66e4,spec:{type:"function",function:{name:"run_tests",description:"跑单元测试并**把结果解析成结构化摘要**——改完代码验证、排查失败用例时用它,不要用run_bash跑测试。\n两者的差别在于你拿到什么:run_bash给你几千行原始输出(Maven的下载与编译日志、pytest堆栈里十几层框架调用),要你自己从噪声里找哪个用例失败、为什么;本工具直接给出「通过/失败/跳过的数量 + 每个失败用例的期望值与实际值 + **堆栈里第一个非测试非框架的位置**(也就是你的哪一行业务代码出的问题)」。\n自动探测框架:Maven(pom.xml) · Gradle(build.gradle) · pytest(pytest.ini/conftest.py/test_*.py) · Node(package.json的test脚本)。Maven/Gradle/pytest都走JUnit XML报告,结果精确到用例级。\n用filter只跑相关用例(Maven是 -Dtest、Gradle是 --tests、pytest是 -k、Node是用例名模式)——定位问题时比全量跑快得多。\n**一个用例都没跑起来时会如实说明**(编译失败/依赖缺失/收集报错),并给出原始输出末尾,不会伪装成「0 个用例、全部通过」。",parameters:{type:"object",properties:{path:{type:"string",description:"项目目录(相对工作目录),默认当前目录"},filter:{type:"string",description:"只跑匹配的用例。Maven:`OrderServiceTest` 或 `OrderServiceTest#某方法`;Gradle:`*.OrderServiceTest`;pytest:`-k` 表达式如 `settle and not slow`;Node:用例名片段"},framework:{type:"string",description:"强制指定框架(maven / gradle / pytest / node)。不给则自动探测"},timeout:{type:"number",description:"超时毫秒,默认 600000(10 分钟)"}},required:[]}}},run:e=>r(e),runWithEnv:(e,t,s)=>r(e,t.onTestResult,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/tui/app.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import r,{useState as e,useRef as t,useReducer as n,useEffect as o,useMemo as s}from"react";import{syncPlanUser as c}from"../plan-limits.js";import{exec as u}from"node:child_process";import{render as i,Box as l,Text as a,useInput as m,usePaste as f,useApp as d,useStdout as p,useStdin as g}from"ink";import{runTurn as h}from"../agent.js";import{fetchUsage as x}from"../api.js";import{toolLabel as w}from"../render.js";import{renderMarkdown as v,looksLikeMarkdown as b}from"../markdown.js";import{createSession as k,saveSession as y}from"../session.js";import{ensureSessionTitle as j}from"../title.js";import{createCheckpoints as I}from"../checkpoints.js";import{buildExtraTools as T}from"../repl.js";import{buildSystemPrompt as M}from"../context.js";import{closeMcp as $}from"../mcp.js";import{runSlash as A,listCommands as S}from"../commands.js";import{matchCommands as P}from"../command-ui.js";import{menuOptions as U}from"./command-menu.js";import{questionSelection as E,approvalSelection as R,initialSelection as D,selectionKey as q,insertSelectionText as L}from"../selection.js";import{parseAskAnswer as C}from"../ask-ui.js";import{usageStatus as O,fmtReset as G}from"../usage.js";import{handleAccountError as W}from"../account.js";import{pricingUrl as N}from"../errkind.js";import{expandAtMentions as Y,buildUserContent as F,loadHistory as H,appendHistory as Q}from"../input.js";import{listWorkspaceFiles as z,runBash as K}from"../executors.js";import{loadAllowRules as V,loadDenyRules as _,matchAllowRules as B,suggestRule as J,addProjectAllowRule as X,riskWarning as Z}from"../settings.js";import{PLAN_DENY_REASON as rr,SANDBOX_DENY_REASON as er,sandboxBlocked as tr,isFileWriteTool as nr,AUTOMATION_TOOLS as or,automationActionLabel as sr,automationDetail as cr,PERMISSION_MODES as ur,MODE_ICON as ir,MODE_LABEL as lr}from"../permissions.js";import{runVerifyLoop as ar}from"../autofix.js";import{runAutoFlow as mr}from"../autorun.js";import{acquireWakeLock as fr,releaseWakeLock as dr}from"../presence.js";import{notifyTurnDone as pr}from"../notify.js";import{closeAllTerminals as gr,dropSessionTerminals as hr}from"../terminal.js";import{shutdownLsp as xr}from"../lsp.js";import{ensureVerified as wr}from"../sandbox.js";import{runGoalRounds as vr,getGoal as br,setRoundKind as kr}from"../goalround.js";import{Workbench as yr,moveCursor as jr}from"./workbench.js";import{WelcomeBanner as Ir}from"./welcome.js";import{enableMouseTracking as Tr,mouseReport as Mr}from"./scroll.js";const $r=r.createElement;function Ar(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}export function App({config:r,cwd:c,session:i,extraTools:l,mcp:a,yolo:gr,initialPrompt:xr,version:wr,onExit:Ir,exitState:Sr={},injectQueue:Pr=[],bridge:Ur={}}){const Er=d(),{stdout:Rr}=p(),{stdin:Dr}=g();o(()=>Dr.isTTY?Tr(Rr):void 0,[Dr,Rr]);const[qr,Lr]=e({columns:Rr.columns||80,rows:Rr.rows||24});o(()=>{const r=()=>Lr({columns:Rr.columns||80,rows:Rr.rows||24});return Rr.on("resize",r),()=>Rr.off("resize",r)},[Rr]);const[Cr,Or]=e({offset:0,revision:0}),Gr=r=>{Wr.current.offset=r,Or(e=>({offset:r,revision:e.revision+1}))},Wr=t({max:0,page:10,offset:0}),Nr=r=>Gr(Math.max(0,Math.min(Wr.current.max,Wr.current.offset+r))),Yr=t(null),Fr=t(!1),Hr=t(null),[Qr,zr]=e(null),Kr=t([]),[Vr,_r]=e(""),Br=t(0),[Jr,Xr]=e(c),Zr=t(l),re=t(null);re.current||(re.current=i.messages.filter(r=>["user","assistant"].includes(r.role)&&r.content).map((r,e)=>({id:e,kind:r.role,text:"string"==typeof r.content?r.content:r.content.filter(r=>"text"===r.type).map(r=>r.text).join("\n")})));const ee=t(re.current.length),te=t(""),ne=t(i),oe=t(null),se=t(new Set),ce=t(V(Jr)),ue=t(_(Jr)),ie=t(!1),le=t(!1),ae=t(!1),me=t(I()),fe=t(!!gr),de=t(!1),pe=t("idle"),ge=t([]),he=t({list:[]}),xe=t(H(Jr)),we=t(null),ve=t(""),[,be]=n(r=>r+1,0),[ke,ye]=n(r=>r+1,0),[je,Ie]=e("idle"),[Te,Me]=e({text:"",cursor:0}),$e=t({prompt:0,completion:0}),[Ae,Se]=e({prompt:0,completion:0}),[Pe,Ue]=e(0),[Ee,Re]=e([]),[De,qe]=e("hidden"),[Le,Ce]=e(0),[Oe,Ge]=e(gr?"auto":"manual"),We=()=>ie.current?"plan":le.current?"sandbox":fe.current?"auto":ae.current?"acceptEdits":"manual",[Ne,Ye]=e(""),Fe=t(null),[He,Qe]=e(null),ze=t(null),[Ke,Ve]=e(0),[_e,Be]=e(!1),Je=s(()=>S(Jr),[Jr,je]),[Xe,Ze]=e(0),[rt,et]=e(!1),tt=s(()=>{try{return z(Jr)}catch{return[]}},[Jr,je]),[nt,ot]=e(!1),[st,ct]=e(0),ut=t(0),it=(r,e)=>{re.current.push({id:ee.current++,kind:r,text:e}),ye()},lt=()=>{if(te.current){const e=te.current,t=!1!==r.markdown&&b(e);it("assistant",t?v(e):e),te.current=""}},at=r=>{pe.current=r?"busy":"idle",Ie(pe.current)};Ur.signalRef||={current:null},Ur.sessionOf=()=>ne.current;const mt=(r=null)=>{const e=Hr.current,t=Kr.current.shift()||null;Hr.current=t,zr(t),e?.resolve(r)},ft=r=>new Promise(e=>{if(Yr.current?.signal.aborted||Fr.current)return e(null);const t={...D(r),resolve:e};Hr.current?Kr.current.push(t):(Hr.current=t,zr(t),Gr(0))});Ur.askUi=async r=>{const e=await ft(E(r));return"date_range"===r.kind&&e?.value?C(r,e.value):e||{value:null}},Ur.ask=async r=>(await Ur.askUi({kind:"text",question:r}))?.value||"已取消";const dt=()=>{const r=[Hr.current,...Kr.current.splice(0)];Hr.current=null,zr(null);for(const e of r)e?.resolve(null)},pt=()=>{qe("hidden"),Yr.current?.abort(),oe.current?.abort(),dt()};o(()=>()=>{Yr.current?.abort(),Hr.current?.resolve(null),Hr.current=null;for(const r of Kr.current.splice(0))r.resolve(null);Ur.askUi=null,Ur.ask=null},[]),o(()=>{if("busy"!==je)return;const r=setInterval(be,250);return()=>clearInterval(r)},[je]);const gt=async()=>{if(r.apiKey)try{const e=await x(r.baseUrl,r.apiKey);ze.current=e,Qe(e)}catch{}};o(()=>{gt();const r=setInterval(()=>{gt()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void Ye("");let e=!1,t=null;const n=()=>{t||(t=u(r.statusLine,{cwd:Jr,timeout:3e3,maxBuffer:8192},(r,n)=>{t=null,e||Ye(r?"":n.split("\n")[0].slice(0,80))}))};n();const o=setInterval(n,5e3);return()=>{e=!0,clearInterval(o),t?.kill()}},[r.statusLine,Jr]),o(()=>()=>{Fe.current&&clearInterval(Fe.current)},[]);const ht=t(""),xt={onDelta:r=>{ht.current&&(ht.current=""),te.current+=r,be()},onReasoning:r=>{ht.current=(ht.current+r).slice(-400),be()},onToolStart:(r,e)=>{ht.current="",lt(),_r(w(r,e)),it("tool",w(r,e))},onToolResult:(r,e)=>{_r("整理结果"),it("toolresult",String(e))},onFileDiff:r=>it("diff",r),onSkip:(r,e)=>it("skip",w(r,e)+" — 已跳过"),onInfo:r=>{lt(),it("info",r)},onTodo:r=>{Re(r),Yr.current?.signal.aborted||qe(e=>r.length?"hidden"===e?"expanded":e:"hidden")},onDecision:r=>{lt();const e=(r.alternatives||[]).length?` · ${r.alternatives.length} 备选`:"",t="draft"===r.status?"(待确认草稿)":"";it("info",`◆ 已记录决策:${r.title}${e}${t}`)},onTurnEnd:()=>{qe("hidden"),ht.current="",lt()}},wt={request:(r,e,t,n)=>{if(Yr.current?.signal.aborted)return Promise.resolve({allow:!1,reason:"操作已中断。"});if(ue.current.length&&B(ue.current,r,e))return Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions查看或移除)。"});if(ie.current&&!n)return Promise.resolve({allow:!1,reason:rr});if(le.current&&tr(r))return Promise.resolve({allow:!1,reason:er});if(n||fe.current||se.current.has(r)||B(ce.current,r,e))return Promise.resolve({allow:!0});if(ae.current&&nr(r))return Promise.resolve({allow:!0});const o=or.has(r);return ft(R({name:r,args:e,consent:o,label:o?sr(r,e):w(r,e),detail:o?cr(r,e):"",risk:Z(r,e)})).then(t=>{if(!t||"deny"===t||Yr.current?.signal.aborted)return{allow:!1,reason:"用户未批准此操作。"};if("session"!==t&&"project"!==t||se.current.add(r),"project"===t){const t=J(r,e);try{X(Jr,t),ce.current=V(Jr),it("info",`已写入允许规则:${t} → .huicai/settings.json`)}catch(r){it("info",`规则写入失败(本会话仍生效):${r.message}`)}}return{allow:!0}})},isYolo:()=>fe.current,setYolo:r=>{fe.current=!!r,Ge(We())},setPlanMode:r=>{ie.current=!!r,Ge(We())},isPlanMode:()=>ie.current,setSandbox:r=>{le.current=!!r,Ge(We())},isSandbox:()=>le.current,setAcceptEdits:r=>{ae.current=!!r,Ge(We())},isAcceptEdits:()=>ae.current,setMode:r=>{fe.current="auto"===r,ae.current="acceptEdits"===r,ie.current="plan"===r,le.current="sandbox"===r,Ge(r)},getMode:()=>We(),reloadRules:()=>{ce.current=V(Jr),ue.current=_(Jr)}},vt=()=>{if(Pr.length)return Pr.shift();const r=ge.current.shift();return Ce(ge.current.length),null==r?null:r},bt=()=>{j(ne.current,r).then(r=>{r&&y(ne.current)}).catch(()=>{})},kt=async e=>{qe("hidden");const t=O(ze.current);if(t&&"exhausted"===t.level){const e=N(r.baseUrl);return it("info",`✗ Token用量窗口(${t.label})已用尽,暂无法继续。${G(t.resetAtEpochMs)}`+(e?`\n 升级会员提高上限 → ${e}`:"")),{error:"Token用量已用尽"}}if(Yr.current?.signal.aborted)return{aborted:!0};_r("准备上下文");const{text:n,images:o}=await Y(e,Jr,r);if(Yr.current?.signal.aborted)return{aborted:!0};const s=me.current.snapshot({session:ne.current,label:e});ne.current.messages.push({role:"user",content:F(n,o)}),bt(),at(!0),oe.current=Yr.current||new AbortController,Ur.signalRef.current=oe.current.signal,ne.current.compaction||(ne.current.compaction={});const c="auto"===We()&&!1!==r.wakeLockInAuto?fr({reason:"自动模式任务执行中"}):null,u=Date.now();let i;try{i=await h({config:r,messages:ne.current.messages,permissions:wt,cwd:Jr,extraTools:Zr.current,events:xt,signal:oe.current.signal,todoState:he.current,compaction:ne.current.compaction,pullQueued:vt,fileJournal:s})}finally{qe("hidden"),c?.ok&&dr()}if(i?.aborted||i?.error||pr({durationMs:Date.now()-u,cwd:Jr,summary:String(e).replace(/\s+/g," ").slice(0,80),enabled:!1!==r.notifications}).catch(()=>{}),lt(),i?.usage&&($e.current={prompt:$e.current.prompt+(i.usage.prompt||0),completion:$e.current.completion+(i.usage.completion||0)},Se({...$e.current})),i?.lastPromptTokens&&(Ue(i.lastPromptTokens),ne.current.lastPromptTokens=i.lastPromptTokens),y(ne.current),bt(),(i?.aborted||i?.error)&&ge.current.length&&(ge.current=[],Ce(0),it("info","已清空排队消息")),i?.error){let e="";const t=W(i,r,r=>{e+=r});if(e.trim()&&it("info",e.trim()),"auth"===t)return Sr.relogin=!0,yt(),i}return gt(),i},yt=()=>{Fr.current=!0,pt(),Yr.current||(Ir?.(),Er.exit())},jt=async e=>{kr(ne.current.id,"human");const t=await kt(e);return t?.aborted||t?.error||Fr.current||Yr.current?.signal.aborted||br(ne.current)&&(await vr({session:ne.current,doTurn:kt,stats:()=>({usedTokens:$e.current.prompt+$e.current.completion,budgetTokens:Number(r.budgetTokens)||0}),onInfo:r=>it("info",r),aborted:()=>Fr.current||!!Yr.current?.signal.aborted}),y(ne.current)),t},It=async e=>{const t=e.trim();if(t){if(Yr.current)return/^\/(exit|quit)$/.test(t)?yt():t.startsWith("/")||t.startsWith("!")?void it("info","任务执行中,请先按 Esc 中断,再执行此命令。"):(Me({text:"",cursor:0}),it("user",t),ge.current.push(t),void Ce(ge.current.length));Me({text:"",cursor:0}),Gr(0),we.current=null,Q(Jr,t),xe.current.at(-1)!==t&&xe.current.push(t),Yr.current=new AbortController,oe.current=Yr.current,Ur.signalRef.current=Yr.current.signal,Br.current=Date.now(),qe("hidden"),at(!0);try{for(t.startsWith("/")?await(async e=>{const t=br(ne.current)?.rev,n={version:wr,config:r,cwd:Jr,select:r=>ft({...r,command:e.trim()}),signal:Yr.current?.signal,resumeSession:e=>{Fe.current&&(clearInterval(Fe.current),Fe.current=null),hr(ne.current.id),ne.current=e,r.sessionId=e.id,e.model&&(r.model=e.model),Pr.length=0,se.current.clear(),he.current.list=[],me.current.list.length=0,Re([]),Ue(e.lastPromptTokens||0),re.current=e.messages.filter(r=>["user","assistant"].includes(r.role)&&r.content).map(r=>({id:ee.current++,kind:r.role,text:"string"==typeof r.content?r.content:r.content.filter(r=>"text"===r.type).map(r=>r.text).join("\n")})),te.current="",Gr(0),ye()},usage:$e.current,permissions:wt,extraTools:Zr.current,session:ne.current,checkpoints:me.current,print:r=>it("info",String(r))},o=await A(e,n);if("exit"===o.action)return yt();if("clear"===o.action&&(y(ne.current),Fe.current&&(clearInterval(Fe.current),Fe.current=null),hr(ne.current.id),ne.current=k({cwd:Jr,model:r.model,messages:[ne.current.messages[0]]}),r.sessionId=ne.current.id,Pr.length=0,se.current.clear(),he.current.list=[],me.current.list.length=0,Re([]),Ue(0),re.current=[],te.current="",it("info","已开启新会话(旧会话仍可 --resume恢复)")),"refresh"===o.action&&(he.current.list=[],Re([]),Ue(0)),"autofix"===o.action&&await ar({command:o.command,maxIters:o.maxIters,signal:Yr.current?.signal,cwd:Jr,runTurnOnce:r=>kt(r),emit:r=>it("info",r)}),"autorun"===o.action&&await mr({goal:o.goal,config:r,permissions:wt,cwd:Jr,runTurnOnce:async r=>{const e=await kt(r);if(e?.aborted||e?.error||Yr.current?.signal.aborted)throw new Error("自动执行已停止");return e},emit:r=>it("info",r)}),"cd"===o.action){Fe.current&&(clearInterval(Fe.current),Fe.current=null);const e=await T(r,o.cwd,{queueInject:r=>Pr.push(r),ask:r=>Ur.ask(r),askUi:r=>Ur.askUi(r),sessionOf:()=>ne.current,signalRef:Ur.signalRef});ce.current=V(o.cwd),ue.current=_(o.cwd),hr(ne.current.id),$(a?.clients),Zr.current=e.extraTools,a&&(a.clients=e.clients),ne.current=k({cwd:o.cwd,model:r.model,messages:[{role:"system",content:M(o.cwd,{memory:!1!==r.memoryEnabled,config:r})}]}),r.sessionId=ne.current.id,Pr.length=0,se.current.clear(),xe.current=H(o.cwd),we.current=null,he.current.list=[],me.current.list.length=0,Re([]),Ue(0),re.current=[],te.current="",Xr(o.cwd),it("info","已切换到 "+o.cwd+"(新会话,旧会话已保存)")}if("loop"===o.action&&(Fe.current&&(clearInterval(Fe.current),Fe.current=null),o.stop?it("info","已停止循环。"):(it("info",`已开始循环:每 ${o.seconds}s运行「${o.prompt}」(/loop off停止)`),Fe.current=setInterval(()=>{"busy"!==pe.current&&It(o.prompt)},1e3*o.seconds))),o.prompt)it("user",o.prompt),await jt(o.prompt);else if(/^\/goal\s+/.test(e)&&!/^\/goal\s+(?:status|pause|clear|limit)(?:\s|$)/.test(e)&&"active"===br(ne.current)?.phase&&br(ne.current)?.rev!==t){const r="继续完成用户明确设定的目标:"+br(ne.current).objective;it("user",e),await jt(r)}})(t):t.startsWith("!")&&t.length>1?await(async e=>{it("user","! "+e),_r("$ "+e);const t=await K(Jr,{command:e},r,Yr.current?.signal);it("toolresult",t),ne.current.messages.push({role:"user",content:`我在终端直接执行了命令:$ ${e}\n${t.slice(0,8e3)}`}),y(ne.current)})(t.slice(1).trim()):(it("user",t),await jt(t));ge.current.length&&!Yr.current.signal.aborted&&!Fr.current;){const r=ge.current.shift();Ce(ge.current.length),await jt(r)}}catch(r){lt(),it("info",Yr.current?.signal.aborted?"操作已中断":"执行失败:"+r.message)}finally{dt(),ge.current=[],Ce(0),Yr.current=null,oe.current=null,Ur.signalRef.current=null,at(!1),qe("hidden"),_r(""),Fr.current&&(Ir?.(),Er.exit())}}};o(()=>{!de.current&&xr&&(de.current=!0,It(xr))},[]);const Tt=Qr?null:/^\/(\S*)$/.exec(Te.text),Mt=Tt?Tt[1]:null,$t=null==Mt?[]:P(Je,Mt),At=$t.length>0&&!_e;o(()=>{Ve(0),Be(!1)},[Mt]);const St=Qr||null!=Mt?null:/(?:^|\s)@([^\s@]*)$/.exec(Te.text),Pt=St?St[1]:null,Ut=null==Pt?[]:(()=>{const r=Pt.toLowerCase(),e=[];for(const t of tt){const n=t.toLowerCase(),o=n.split("/").pop();let s=-1;if(r?o.startsWith(r)?s=3:o.includes(r)?s=2:n.includes(r)&&(s=1):s=0,s>=0&&e.push({f:t,rank:s}),!r&&e.length>=50)break}return e.sort((r,e)=>e.rank-r.rank||r.f.length-e.f.length||r.f.localeCompare(e.f)),e.slice(0,50).map(r=>r.f)})(),Et=Ut.length>0&&!rt;o(()=>{Ze(0),et(!1)},[Pt]);const Rt=s(()=>{const r=new Set,e=[];for(let t=xe.current.length-1;t>=0&&e.length<50;t--){const n=xe.current[t];n&&!r.has(n)&&(r.add(n),e.push(n))}return e},[nt]);f(r=>{if(Hr.current){if(Hr.current.editing){const e=L(Hr.current,r);Hr.current=e,zr(e)}return}const e=r.replace(/\r\n?/g,"\n");we.current=null,Me(r=>({text:r.text.slice(0,r.cursor)+e+r.text.slice(r.cursor),cursor:r.cursor+e.length}))}),m((e,t)=>{const n=Mr(e);if(n)if(n.delta&&Hr.current){const r=Hr.current,e={...r,index:Math.max(0,Math.min(U(r).length-1,r.index-Math.sign(n.delta)))};Hr.current=e,zr(e)}else n.delta&&Nr(n.delta);else if(t.ctrl&&"c"===e)Yr.current?(pt(),it("info","已请求中断")):yt();else{if(t.ctrl&&t.end)return Gr(0);if(t.ctrl&&t.home)return Gr(Wr.current.max);if(t.ctrl&&(t.upArrow||t.downArrow))return Nr(t.upArrow?1:-1);if(!t.ctrl||"t"!==e||Hr.current){if(Hr.current){const r=Hr.current;if(r.kind){const n=q(r,e,t);return"result"in n?mt(n.result):(Hr.current=n.menu,void zr(n.menu))}const n=U(r),o=e=>{const t={...r,...e};Hr.current=t,zr(t)};if(t.escape)return mt();if(t.return)return n[r.index]?mt(n[r.index].value):void 0;if(t.upArrow||t.downArrow||t.pageUp||t.pageDown||"segments"===r.layout&&(t.leftArrow||t.rightArrow||t.tab)){const e=t.pageUp||t.pageDown?Math.max(1,Wr.current.menuPage||4):1,s=t.upArrow||t.pageUp||t.leftArrow||t.tab&&t.shift?-e:e;return o({index:Math.max(0,Math.min(n.length-1,r.index+s)),..."segments"===r.layout?{query:""}:{}})}const s=e=>{const t="segments"===r.layout?n.findIndex(r=>[r.label,r.value].join(" ").toLowerCase().includes(e.toLowerCase())):0;o({query:e,index:Math.max(0,t)})};return t.ctrl&&"u"===e?s(""):t.backspace||t.delete?s(r.query.slice(0,jr(r.query,r.query.length,-1))):!e||t.ctrl||t.meta||t.tab?void 0:s(r.query+e.replace(/[\x00-\x1f\x7f]/g,""))}if((t.pageUp||t.pageDown)&&!At&&!Et&&!nt)return Nr((t.pageUp?1:-1)*Math.max(1,Wr.current.page-1));if(t.ctrl&&"p"===e)return Me({text:"/",cursor:1}),void Be(!1);if(nt){if(!Rt.length)return void ot(!1);if(t.upArrow)return ct(r=>(r-1+Rt.length)%Rt.length);if(t.downArrow)return ct(r=>(r+1)%Rt.length);if(t.pageUp)return ct(r=>Math.max(0,r-4));if(t.pageDown)return ct(r=>Math.min(Rt.length-1,r+4));if(t.return){const r=Rt[Math.min(st,Rt.length-1)]||"";return ot(!1),we.current=null,Me({text:r,cursor:r.length})}return t.escape?ot(!1):void 0}if(t.tab&&t.shift&&!Yr.current){const e=ur[(ur.indexOf(wt.getMode())+1)%ur.length];return wt.setMode(o=e),r.planMode="plan"===o,r.sandbox="sandbox"===o,void it("info",`对话模式 → ${ir[o]} ${lr[o]}`+("auto"===o?"(已开放所有权限,任何命令免审批)":""))}var o;if(At){const r=()=>$t[Math.min(Ke,$t.length-1)];if(t.upArrow)return Ve(r=>(r-1+$t.length)%$t.length);if(t.downArrow)return Ve(r=>(r+1)%$t.length);if(t.pageUp)return Ve(r=>Math.max(0,r-4));if(t.pageDown)return Ve(r=>Math.min($t.length-1,r+4));if(t.tab){const e=r().name;return Me({text:"/"+e+" ",cursor:e.length+2})}if(t.return){const e=r();return!e.menu&&e.args?.startsWith("<")?Me({text:"/"+e.name+" ",cursor:e.name.length+2}):void It("/"+e.name)}if(t.escape)return Be(!0)}if(Et){const r=()=>Ut[Math.min(Xe,Ut.length-1)];if(t.upArrow)return Ze(r=>(r-1+Ut.length)%Ut.length);if(t.downArrow)return Ze(r=>(r+1)%Ut.length);if(t.pageUp)return Ze(r=>Math.max(0,r-4));if(t.pageDown)return Ze(r=>Math.min(Ut.length-1,r+4));if(t.tab||t.return){const e=r(),t=Te.text.length-(Pt?Pt.length:0)-1,n=Te.text.slice(0,t+1)+e+" ";return Me({text:n,cursor:n.length})}if(t.escape)return et(!0)}if(t.escape){if("busy"===pe.current&&oe.current)return pt(),void it("info","已请求中断(Esc)");const r=Date.now();return r-ut.current<600&&xe.current.length?(ut.current=0,ct(0),void ot(!0)):void(ut.current=r)}if(t.ctrl&&"c"===e)"busy"===pe.current&&oe.current?(pt(),it("info","已请求中断")):yt();else{if(t.return&&(t.shift||t.meta)||t.ctrl&&"j"===e)return Me(r=>({text:r.text.slice(0,r.cursor)+"\n"+r.text.slice(r.cursor),cursor:r.cursor+1}));if(t.return)It(Te.text);else{if(t.upArrow)return(()=>{const r=xe.current;if(!r.length)return;null==we.current?(ve.current=Te.text,we.current=r.length-1):we.current>0&&we.current--;const e=r[we.current]??"";Me({text:e,cursor:e.length})})();if(t.downArrow)return(()=>{if(null==we.current)return;const r=xe.current;if(we.current<r.length-1){we.current++;const e=r[we.current];Me({text:e,cursor:e.length})}else we.current=null,Me({text:ve.current,cursor:ve.current.length})})();if(t.leftArrow)return Me(r=>({...r,cursor:jr(r.text,r.cursor,-1)}));if(t.rightArrow)return Me(r=>({...r,cursor:jr(r.text,r.cursor,1)}));if(t.ctrl&&"a"===e)return Me(r=>({...r,cursor:0}));if(t.ctrl&&"e"===e)return Me(r=>({...r,cursor:r.text.length}));if(t.ctrl&&"u"===e)return Me(r=>({text:r.text.slice(r.cursor),cursor:0}));if(t.ctrl&&"k"===e)return Me(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(t.ctrl&&"w"===e)return Me(r=>{const e=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:e+r.text.slice(r.cursor),cursor:e.length}});if(t.backspace||t.delete)return we.current=null,Me(r=>{const e=t.backspace?jr(r.text,r.cursor,-1):r.cursor,n=t.backspace?r.cursor:jr(r.text,r.cursor,1);return{text:r.text.slice(0,e)+r.text.slice(n),cursor:e}});if(e&&!t.ctrl&&!t.meta){we.current=null;const r=e.replace(/\r\n?/g,"\n");Me(e=>({text:e.text.slice(0,e.cursor)+r+e.text.slice(e.cursor),cursor:e.cursor+r.length}))}}}}else he.current.list.length&&qe(r=>"expanded"===r?"busy"===pe.current?"collapsed":"hidden":"expanded")}});const Dt=s(()=>re.current.slice(),[ke]),qt=O(He),Lt=qt?"exhausted"===qt.level?`⛔ ${qt.label}用量已用尽${G(qt.resetAtEpochMs)?" · "+G(qt.resetAtEpochMs):""}`:`⚠ ${qt.label}用量 ${Math.round(100*qt.pct)}%(${Ar(qt.used)}/${Ar(qt.limit)})`:"";return $r(yr,{columns:qr.columns,rows:qr.rows,version:wr,cwd:Jr,model:r.model,theme:r.theme,accent:r.accentColor,mode:`${ir[Oe]} ${lr[Oe]}`,logItems:Dt,live:te.current,liveId:ee.current,thinking:ht.current,todos:Ee,taskView:De,busy:"busy"===je,activity:Ne||Vr,elapsed:Math.floor((Date.now()-Br.current)/1e3),pending:null,question:null,menu:Qr,input:Te,candidates:nt?Rt:At?$t.map(r=>"/"+r.name+(r.args?" "+r.args:"")+" ["+r.category+"] "+r.desc):Et?Ut.map(r=>"@"+r):[],selected:nt?st:At?Ke:Xe,candidateTitle:nt?"输入历史":At?"命令与技能":"引用文件",offset:Cr.offset,scrollRevision:Cr.revision,scrollRef:Wr,usage:{context:Ar(Pe),total:Ar(Ae.prompt+Ae.completion)},queueLen:Le,warning:Lt,toolCount:Zr.current.length})}export async function runInk({config:r,cwd:e,session:t,yolo:n,initialPrompt:o,version:s}){wr().catch(()=>{}),c(r).catch(()=>{}),r.sessionId=t.id;const u=[],m={signalRef:{current:null},sessionOf:()=>t},f={clients:[]},d={};let p,g=!1;const h=()=>{g=!0,p?.unmount()};try{p=i($r(l,{flexDirection:"column"},$r(Ir,{columns:process.stdout.columns,rows:process.stdout.rows,version:s}),$r(a,{dimColor:!0},"正在加载技能与工具…")),{alternateScreen:!0,interactive:!(!process.stdout.isTTY||!process.stdin.isTTY),exitOnCtrlC:!1,maxFps:20}),process.once("SIGTERM",h),process.once("SIGHUP",h),process.once("SIGINT",h);const c=await T(r,e,{queueInject:r=>u.push(r),sessionOf:()=>m.sessionOf(),signalRef:m.signalRef,ask:r=>m.ask?m.ask(r):Promise.resolve("终端尚未就绪"),askUi:r=>m.askUi?m.askUi(r):Promise.resolve({value:null}),runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});return f.clients=c.clients,g?d:(process.removeListener("SIGINT",h),p.rerender($r(App,{config:r,cwd:e,session:t,extraTools:c.extraTools,mcp:f,yolo:n,initialPrompt:o,version:s,exitState:d,injectQueue:u,bridge:m})),await p.waitUntilExit(),d)}finally{process.removeListener("SIGINT",h),process.removeListener("SIGTERM",h),process.removeListener("SIGHUP",h),p?.unmount(),$(f.clients),gr(),xr(),dr()}}
|
|
1
|
+
import r,{useState as e,useRef as t,useReducer as n,useEffect as o,useMemo as s}from"react";import{syncPlanUser as c}from"../plan-limits.js";import{exec as u}from"node:child_process";import{render as i,Box as l,Text as a,useInput as m,usePaste as f,useApp as d,useStdout as p,useStdin as g}from"ink";import{runTurn as h}from"../agent.js";import{fetchUsage as x}from"../api.js";import{toolLabel as w}from"../render.js";import{renderMarkdown as v,looksLikeMarkdown as b}from"../markdown.js";import{createSession as k,saveSession as y}from"../session.js";import{ensureSessionTitle as j}from"../title.js";import{createCheckpoints as I}from"../checkpoints.js";import{buildExtraTools as T}from"../repl.js";import{buildSystemPrompt as M}from"../context.js";import{closeMcp as A}from"../mcp.js";import{runSlash as $,listCommands as S}from"../commands.js";import{matchCommands as P}from"../command-ui.js";import{menuOptions as U}from"./command-menu.js";import{questionSelection as E,approvalSelection as R,initialSelection as D,selectionKey as C,insertSelectionText as L}from"../selection.js";import{parseAskAnswer as q}from"../ask-ui.js";import{usageStatus as O,fmtReset as G}from"../usage.js";import{handleAccountError as W}from"../account.js";import{pricingUrl as N}from"../errkind.js";import{expandAtMentions as Y,buildUserContent as H,loadHistory as F,appendHistory as Q}from"../input.js";import{listWorkspaceFiles as z,runBash as K}from"../executors.js";import{loadAllowRules as V,loadDenyRules as _,matchAllowRules as B,suggestRule as J,addProjectAllowRule as X,riskWarning as Z}from"../settings.js";import{PLAN_DENY_REASON as rr,SANDBOX_DENY_REASON as er,sandboxBlocked as tr,isFileWriteTool as nr,AUTOMATION_TOOLS as or,automationActionLabel as sr,automationDetail as cr,PERMISSION_MODES as ur,MODE_ICON as ir,MODE_LABEL as lr}from"../permissions.js";import{runVerifyLoop as ar}from"../autofix.js";import{runAutoFlow as mr}from"../autorun.js";import{acquireWakeLock as fr,releaseWakeLock as dr}from"../presence.js";import{notifyTurnDone as pr}from"../notify.js";import{closeAllTerminals as gr,dropSessionTerminals as hr}from"../terminal.js";import{shutdownLsp as xr}from"../lsp.js";import{ensureVerified as wr}from"../sandbox.js";import{runGoalRounds as vr,getGoal as br,setRoundKind as kr}from"../goalround.js";import{Workbench as yr,moveCursor as jr}from"./workbench.js";import{WelcomeBanner as Ir}from"./welcome.js";import{enableMouseTracking as Tr,mouseReport as Mr}from"./scroll.js";const Ar=r.createElement;function $r(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}export function App({config:r,cwd:c,session:i,extraTools:l,mcp:a,yolo:gr,initialPrompt:xr,version:wr,updater:Ir,onExit:Sr,exitState:Pr={},injectQueue:Ur=[],bridge:Er={}}){const Rr=d(),{stdout:Dr}=p(),{stdin:Cr}=g();o(()=>Cr.isTTY?Tr(Dr):void 0,[Cr,Dr]);const[Lr,qr]=e({columns:Dr.columns||80,rows:Dr.rows||24});o(()=>{const r=()=>qr({columns:Dr.columns||80,rows:Dr.rows||24});return Dr.on("resize",r),()=>Dr.off("resize",r)},[Dr]);const[Or,Gr]=e({offset:0,revision:0}),Wr=r=>{Nr.current.offset=r,Gr(e=>({offset:r,revision:e.revision+1}))},Nr=t({max:0,page:10,offset:0}),Yr=r=>Wr(Math.max(0,Math.min(Nr.current.max,Nr.current.offset+r))),Hr=t(null),Fr=t(!1),Qr=t(null),[zr,Kr]=e(null),Vr=t([]),[_r,Br]=e(""),Jr=t(0),[Xr,Zr]=e(c),re=t(l),ee=t(null);ee.current||(ee.current=i.messages.filter(r=>["user","assistant"].includes(r.role)&&r.content).map((r,e)=>({id:e,kind:r.role,text:"string"==typeof r.content?r.content:r.content.filter(r=>"text"===r.type).map(r=>r.text).join("\n")})));const te=t(ee.current.length),ne=t(""),oe=t(i),se=t(null),ce=t(new Set),ue=t(V(Xr)),ie=t(_(Xr)),le=t(!1),ae=t(!1),me=t(!1),fe=t(I()),de=t(!!gr),pe=t(!1),ge=t("idle"),he=t([]),xe=t({list:[]}),we=t(F(Xr)),ve=t(null),be=t(""),[,ke]=n(r=>r+1,0),[ye,je]=n(r=>r+1,0),[Ie,Te]=e("idle"),[Me,Ae]=e({text:"",cursor:0}),[$e,Se]=e(null),Pe=t(!1);o(()=>{let r=!0;return Ir?.check.then(e=>{r&&Se(e)}),()=>{r=!1,Ir?.dispose()}},[Ir]);const Ue=t({prompt:0,completion:0}),[Ee,Re]=e({prompt:0,completion:0}),[De,Ce]=e(0),[Le,qe]=e([]),[Oe,Ge]=e("hidden"),[We,Ne]=e(0),[Ye,He]=e(gr?"auto":"manual"),Fe=()=>le.current?"plan":ae.current?"sandbox":de.current?"auto":me.current?"acceptEdits":"manual",[Qe,ze]=e(""),Ke=t(null),[Ve,_e]=e(null),Be=t(null),[Je,Xe]=e(0),[Ze,rt]=e(!1),et=s(()=>[...S(Xr),...Ir&&$e?[{name:"update",description:"安装已检测到的 CLI 新版本",menu:!0}]:[]],[Xr,Ie,$e,Ir]),[tt,nt]=e(0),[ot,st]=e(!1),ct=s(()=>{try{return z(Xr)}catch{return[]}},[Xr,Ie]),[ut,it]=e(!1),[lt,at]=e(0),mt=t(0),ft=(r,e)=>{ee.current.push({id:te.current++,kind:r,text:e}),je()},dt=()=>{if(ne.current){const e=ne.current,t=!1!==r.markdown&&b(e);ft("assistant",t?v(e):e),ne.current=""}},pt=r=>{ge.current=r?"busy":"idle",Te(ge.current)};Er.signalRef||={current:null},Er.sessionOf=()=>oe.current;const gt=(r=null)=>{const e=Qr.current,t=Vr.current.shift()||null;Qr.current=t,Kr(t),e?.resolve(r)},ht=r=>new Promise(e=>{if(Hr.current?.signal.aborted||Fr.current)return e(null);const t={...D(r),resolve:e};Qr.current?Vr.current.push(t):(Qr.current=t,Kr(t),Wr(0))});Er.askUi=async r=>{const e=await ht(E(r));return"date_range"===r.kind&&e?.value?q(r,e.value):e||{value:null}},Er.ask=async r=>(await Er.askUi({kind:"text",question:r}))?.value||"已取消";const xt=()=>{const r=[Qr.current,...Vr.current.splice(0)];Qr.current=null,Kr(null);for(const e of r)e?.resolve(null)},wt=()=>{Ge("hidden"),Hr.current?.abort(),se.current?.abort(),xt()};o(()=>()=>{Hr.current?.abort(),Qr.current?.resolve(null),Qr.current=null;for(const r of Vr.current.splice(0))r.resolve(null);Er.askUi=null,Er.ask=null},[]),o(()=>{if("busy"!==Ie)return;const r=setInterval(ke,250);return()=>clearInterval(r)},[Ie]);const vt=async()=>{if(r.apiKey)try{const e=await x(r.baseUrl,r.apiKey);Be.current=e,_e(e)}catch{}};o(()=>{vt();const r=setInterval(()=>{vt()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void ze("");let e=!1,t=null;const n=()=>{t||(t=u(r.statusLine,{cwd:Xr,timeout:3e3,maxBuffer:8192},(r,n)=>{t=null,e||ze(r?"":n.split("\n")[0].slice(0,80))}))};n();const o=setInterval(n,5e3);return()=>{e=!0,clearInterval(o),t?.kill()}},[r.statusLine,Xr]),o(()=>()=>{Ke.current&&clearInterval(Ke.current)},[]);const bt=t(""),kt={onDelta:r=>{bt.current&&(bt.current=""),ne.current+=r,ke()},onReasoning:r=>{bt.current=(bt.current+r).slice(-400),ke()},onToolStart:(r,e)=>{bt.current="",dt(),Br(w(r,e)),ft("tool",w(r,e))},onToolResult:(r,e)=>{Br("整理结果"),ft("toolresult",String(e))},onFileDiff:r=>ft("diff",r),onSkip:(r,e)=>ft("skip",w(r,e)+" — 已跳过"),onInfo:r=>{dt(),ft("info",r)},onTodo:r=>{qe(r),Hr.current?.signal.aborted||Ge(e=>r.length?"hidden"===e?"expanded":e:"hidden")},onDecision:r=>{dt();const e=(r.alternatives||[]).length?` · ${r.alternatives.length} 备选`:"",t="draft"===r.status?"(待确认草稿)":"";ft("info",`◆ 已记录决策:${r.title}${e}${t}`)},onTurnEnd:()=>{Ge("hidden"),bt.current="",dt()}},yt={request:(r,e,t,n)=>{if(Hr.current?.signal.aborted)return Promise.resolve({allow:!1,reason:"操作已中断。"});if(ie.current.length&&B(ie.current,r,e))return Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions查看或移除)。"});if(le.current&&!n)return Promise.resolve({allow:!1,reason:rr});if(ae.current&&tr(r))return Promise.resolve({allow:!1,reason:er});if(n||de.current||ce.current.has(r)||B(ue.current,r,e))return Promise.resolve({allow:!0});if(me.current&&nr(r))return Promise.resolve({allow:!0});const o=or.has(r);return ht(R({name:r,args:e,consent:o,label:o?sr(r,e):w(r,e),detail:o?cr(r,e):"",risk:Z(r,e)})).then(t=>{if(!t||"deny"===t||Hr.current?.signal.aborted)return{allow:!1,reason:"用户未批准此操作。"};if("session"!==t&&"project"!==t||ce.current.add(r),"project"===t){const t=J(r,e);try{X(Xr,t),ue.current=V(Xr),ft("info",`已写入允许规则:${t} → .huicai/settings.json`)}catch(r){ft("info",`规则写入失败(本会话仍生效):${r.message}`)}}return{allow:!0}})},isYolo:()=>de.current,setYolo:r=>{de.current=!!r,He(Fe())},setPlanMode:r=>{le.current=!!r,He(Fe())},isPlanMode:()=>le.current,setSandbox:r=>{ae.current=!!r,He(Fe())},isSandbox:()=>ae.current,setAcceptEdits:r=>{me.current=!!r,He(Fe())},isAcceptEdits:()=>me.current,setMode:r=>{de.current="auto"===r,me.current="acceptEdits"===r,le.current="plan"===r,ae.current="sandbox"===r,He(r)},getMode:()=>Fe(),reloadRules:()=>{ue.current=V(Xr),ie.current=_(Xr)}},jt=()=>{if(Ur.length)return Ur.shift();const r=he.current.shift();return Ne(he.current.length),null==r?null:r},It=()=>{j(oe.current,r).then(r=>{r&&y(oe.current)}).catch(()=>{})},Tt=async e=>{Ge("hidden");const t=O(Be.current);if(t&&"exhausted"===t.level){const e=N(r.baseUrl);return ft("info",`✗ Token用量窗口(${t.label})已用尽,暂无法继续。${G(t.resetAtEpochMs)}`+(e?`\n 升级会员提高上限 → ${e}`:"")),{error:"Token用量已用尽"}}if(Hr.current?.signal.aborted)return{aborted:!0};Br("准备上下文");const{text:n,images:o}=await Y(e,Xr,r);if(Hr.current?.signal.aborted)return{aborted:!0};const s=fe.current.snapshot({session:oe.current,label:e});oe.current.messages.push({role:"user",content:H(n,o)}),It(),pt(!0),se.current=Hr.current||new AbortController,Er.signalRef.current=se.current.signal,oe.current.compaction||(oe.current.compaction={});const c="auto"===Fe()&&!1!==r.wakeLockInAuto?fr({reason:"自动模式任务执行中"}):null,u=Date.now();let i;try{i=await h({config:r,messages:oe.current.messages,permissions:yt,cwd:Xr,extraTools:re.current,events:kt,signal:se.current.signal,todoState:xe.current,compaction:oe.current.compaction,pullQueued:jt,fileJournal:s})}finally{Ge("hidden"),c?.ok&&dr()}if(i?.aborted||i?.error||pr({durationMs:Date.now()-u,cwd:Xr,summary:String(e).replace(/\s+/g," ").slice(0,80),enabled:!1!==r.notifications}).catch(()=>{}),dt(),i?.usage&&(Ue.current={prompt:Ue.current.prompt+(i.usage.prompt||0),completion:Ue.current.completion+(i.usage.completion||0)},Re({...Ue.current})),i?.lastPromptTokens&&(Ce(i.lastPromptTokens),oe.current.lastPromptTokens=i.lastPromptTokens),y(oe.current),It(),(i?.aborted||i?.error)&&he.current.length&&(he.current=[],Ne(0),ft("info","已清空排队消息")),i?.error){let e="";const t=W(i,r,r=>{e+=r});if(e.trim()&&ft("info",e.trim()),"auth"===t)return Pr.relogin=!0,Mt(),i}return vt(),i},Mt=()=>{Fr.current=!0,wt(),Hr.current||(Sr?.(),Rr.exit())},At=async e=>{kr(oe.current.id,"human");const t=await Tt(e);return t?.aborted||t?.error||Fr.current||Hr.current?.signal.aborted||br(oe.current)&&(await vr({session:oe.current,doTurn:Tt,stats:()=>({usedTokens:Ue.current.prompt+Ue.current.completion,budgetTokens:Number(r.budgetTokens)||0}),onInfo:r=>ft("info",r),aborted:()=>Fr.current||!!Hr.current?.signal.aborted}),y(oe.current)),t},$t=async e=>{const t=e.trim();if(t){if(Hr.current)return/^\/(exit|quit)$/.test(t)?Mt():t.startsWith("/")||t.startsWith("!")?void ft("info","任务执行中,请先按 Esc 中断,再执行此命令。"):(Ae({text:"",cursor:0}),ft("user",t),he.current.push(t),void Ne(he.current.length));Ae({text:"",cursor:0}),Wr(0),ve.current=null,Q(Xr,t),we.current.at(-1)!==t&&we.current.push(t),Hr.current=new AbortController,se.current=Hr.current,Er.signalRef.current=Hr.current.signal,Jr.current=Date.now(),Ge("hidden"),pt(!0);try{for("/update"===t&&Ir&&$e?(Br("正在更新 HuiCai AI,请稍候"),Pr.updated=await Ir.install($e,{signal:Hr.current.signal}),y(oe.current),Fr.current=!0):t.startsWith("/")?await(async e=>{const t=br(oe.current)?.rev,n={version:wr,config:r,cwd:Xr,select:r=>ht({...r,command:e.trim()}),signal:Hr.current?.signal,resumeSession:e=>{Ke.current&&(clearInterval(Ke.current),Ke.current=null),hr(oe.current.id),oe.current=e,r.sessionId=e.id,e.model&&(r.model=e.model),Ur.length=0,ce.current.clear(),xe.current.list=[],fe.current.list.length=0,qe([]),Ce(e.lastPromptTokens||0),ee.current=e.messages.filter(r=>["user","assistant"].includes(r.role)&&r.content).map(r=>({id:te.current++,kind:r.role,text:"string"==typeof r.content?r.content:r.content.filter(r=>"text"===r.type).map(r=>r.text).join("\n")})),ne.current="",Wr(0),je()},usage:Ue.current,permissions:yt,extraTools:re.current,session:oe.current,checkpoints:fe.current,print:r=>ft("info",String(r))},o=await $(e,n);if("exit"===o.action)return Mt();if("clear"===o.action&&(y(oe.current),Ke.current&&(clearInterval(Ke.current),Ke.current=null),hr(oe.current.id),oe.current=k({cwd:Xr,model:r.model,messages:[oe.current.messages[0]]}),r.sessionId=oe.current.id,Ur.length=0,ce.current.clear(),xe.current.list=[],fe.current.list.length=0,qe([]),Ce(0),ee.current=[],ne.current="",ft("info","已开启新会话(旧会话仍可 --resume恢复)")),"refresh"===o.action&&(xe.current.list=[],qe([]),Ce(0)),"autofix"===o.action&&await ar({command:o.command,maxIters:o.maxIters,signal:Hr.current?.signal,cwd:Xr,runTurnOnce:r=>Tt(r),emit:r=>ft("info",r)}),"autorun"===o.action&&await mr({goal:o.goal,config:r,permissions:yt,cwd:Xr,runTurnOnce:async r=>{const e=await Tt(r);if(e?.aborted||e?.error||Hr.current?.signal.aborted)throw new Error("自动执行已停止");return e},emit:r=>ft("info",r)}),"cd"===o.action){Ke.current&&(clearInterval(Ke.current),Ke.current=null);const e=await T(r,o.cwd,{queueInject:r=>Ur.push(r),ask:r=>Er.ask(r),askUi:r=>Er.askUi(r),sessionOf:()=>oe.current,signalRef:Er.signalRef});ue.current=V(o.cwd),ie.current=_(o.cwd),hr(oe.current.id),A(a?.clients),re.current=e.extraTools,a&&(a.clients=e.clients),oe.current=k({cwd:o.cwd,model:r.model,messages:[{role:"system",content:M(o.cwd,{memory:!1!==r.memoryEnabled,config:r})}]}),r.sessionId=oe.current.id,Ur.length=0,ce.current.clear(),we.current=F(o.cwd),ve.current=null,xe.current.list=[],fe.current.list.length=0,qe([]),Ce(0),ee.current=[],ne.current="",Zr(o.cwd),ft("info","已切换到 "+o.cwd+"(新会话,旧会话已保存)")}if("loop"===o.action&&(Ke.current&&(clearInterval(Ke.current),Ke.current=null),o.stop?ft("info","已停止循环。"):(ft("info",`已开始循环:每 ${o.seconds}s运行「${o.prompt}」(/loop off停止)`),Ke.current=setInterval(()=>{"busy"!==ge.current&&$t(o.prompt)},1e3*o.seconds))),o.prompt)ft("user",o.prompt),await At(o.prompt);else if(/^\/goal\s+/.test(e)&&!/^\/goal\s+(?:status|pause|clear|limit)(?:\s|$)/.test(e)&&"active"===br(oe.current)?.phase&&br(oe.current)?.rev!==t){const r="继续完成用户明确设定的目标:"+br(oe.current).objective;ft("user",e),await At(r)}})(t):t.startsWith("!")&&t.length>1?await(async e=>{ft("user","! "+e),Br("$ "+e);const t=await K(Xr,{command:e},r,Hr.current?.signal);ft("toolresult",t),oe.current.messages.push({role:"user",content:`我在终端直接执行了命令:$ ${e}\n${t.slice(0,8e3)}`}),y(oe.current)})(t.slice(1).trim()):(ft("user",t),await At(t));he.current.length&&!Hr.current.signal.aborted&&!Fr.current;){const r=he.current.shift();Ne(he.current.length),await At(r)}}catch(r){dt(),ft("info",Hr.current?.signal.aborted?"操作已中断":"执行失败:"+r.message+("/update"===t?" 输入 /update 可重试。":""))}finally{xt(),he.current=[],Ne(0),Hr.current=null,se.current=null,Er.signalRef.current=null,pt(!1),Ge("hidden"),Br(""),Fr.current&&(Sr?.(),Rr.exit())}}};o(()=>{!$e||Pe.current||"idle"!==Ie||Me.text||zr||Fr.current||(Pe.current=!0,ht(Ir.menu($e)).then(r=>{"update"===r&&$t("/update")}))},[$e,Ie,Me.text,zr]),o(()=>{!pe.current&&xr&&(pe.current=!0,$t(xr))},[]);const St=zr?null:/^\/(\S*)$/.exec(Me.text),Pt=St?St[1]:null,Ut=null==Pt?[]:P(et,Pt),Et=Ut.length>0&&!Ze;o(()=>{Xe(0),rt(!1)},[Pt]);const Rt=zr||null!=Pt?null:/(?:^|\s)@([^\s@]*)$/.exec(Me.text),Dt=Rt?Rt[1]:null,Ct=null==Dt?[]:(()=>{const r=Dt.toLowerCase(),e=[];for(const t of ct){const n=t.toLowerCase(),o=n.split("/").pop();let s=-1;if(r?o.startsWith(r)?s=3:o.includes(r)?s=2:n.includes(r)&&(s=1):s=0,s>=0&&e.push({f:t,rank:s}),!r&&e.length>=50)break}return e.sort((r,e)=>e.rank-r.rank||r.f.length-e.f.length||r.f.localeCompare(e.f)),e.slice(0,50).map(r=>r.f)})(),Lt=Ct.length>0&&!ot;o(()=>{nt(0),st(!1)},[Dt]);const qt=s(()=>{const r=new Set,e=[];for(let t=we.current.length-1;t>=0&&e.length<50;t--){const n=we.current[t];n&&!r.has(n)&&(r.add(n),e.push(n))}return e},[ut]);f(r=>{if(Qr.current){if(Qr.current.editing){const e=L(Qr.current,r);Qr.current=e,Kr(e)}return}const e=r.replace(/\r\n?/g,"\n");ve.current=null,Ae(r=>({text:r.text.slice(0,r.cursor)+e+r.text.slice(r.cursor),cursor:r.cursor+e.length}))}),m((e,t)=>{const n=Mr(e);if(n)if(n.delta&&Qr.current){const r=Qr.current,e={...r,index:Math.max(0,Math.min(U(r).length-1,r.index-Math.sign(n.delta)))};Qr.current=e,Kr(e)}else n.delta&&Yr(n.delta);else if(t.ctrl&&"c"===e)Hr.current?(wt(),ft("info","已请求中断")):Mt();else{if(t.ctrl&&t.end)return Wr(0);if(t.ctrl&&t.home)return Wr(Nr.current.max);if(t.ctrl&&(t.upArrow||t.downArrow))return Yr(t.upArrow?1:-1);if(!t.ctrl||"t"!==e||Qr.current){if(Qr.current){const r=Qr.current;if(r.kind){const n=C(r,e,t);return"result"in n?gt(n.result):(Qr.current=n.menu,void Kr(n.menu))}const n=U(r),o=e=>{const t={...r,...e};Qr.current=t,Kr(t)};if(t.escape)return gt();if(t.return)return n[r.index]?gt(n[r.index].value):void 0;if(t.upArrow||t.downArrow||t.pageUp||t.pageDown||"segments"===r.layout&&(t.leftArrow||t.rightArrow||t.tab)){const e=t.pageUp||t.pageDown?Math.max(1,Nr.current.menuPage||4):1,s=t.upArrow||t.pageUp||t.leftArrow||t.tab&&t.shift?-e:e;return o({index:Math.max(0,Math.min(n.length-1,r.index+s)),..."segments"===r.layout?{query:""}:{}})}const s=e=>{const t="segments"===r.layout?n.findIndex(r=>[r.label,r.value].join(" ").toLowerCase().includes(e.toLowerCase())):0;o({query:e,index:Math.max(0,t)})};return t.ctrl&&"u"===e?s(""):t.backspace||t.delete?s(r.query.slice(0,jr(r.query,r.query.length,-1))):!e||t.ctrl||t.meta||t.tab?void 0:s(r.query+e.replace(/[\x00-\x1f\x7f]/g,""))}if((t.pageUp||t.pageDown)&&!Et&&!Lt&&!ut)return Yr((t.pageUp?1:-1)*Math.max(1,Nr.current.page-1));if(t.ctrl&&"p"===e)return Ae({text:"/",cursor:1}),void rt(!1);if(ut){if(!qt.length)return void it(!1);if(t.upArrow)return at(r=>(r-1+qt.length)%qt.length);if(t.downArrow)return at(r=>(r+1)%qt.length);if(t.pageUp)return at(r=>Math.max(0,r-4));if(t.pageDown)return at(r=>Math.min(qt.length-1,r+4));if(t.return){const r=qt[Math.min(lt,qt.length-1)]||"";return it(!1),ve.current=null,Ae({text:r,cursor:r.length})}return t.escape?it(!1):void 0}if(t.tab&&t.shift&&!Hr.current){const e=ur[(ur.indexOf(yt.getMode())+1)%ur.length];return yt.setMode(o=e),r.planMode="plan"===o,r.sandbox="sandbox"===o,void ft("info",`对话模式 → ${ir[o]} ${lr[o]}`+("auto"===o?"(已开放所有权限,任何命令免审批)":""))}var o;if(Et){const r=()=>Ut[Math.min(Je,Ut.length-1)];if(t.upArrow)return Xe(r=>(r-1+Ut.length)%Ut.length);if(t.downArrow)return Xe(r=>(r+1)%Ut.length);if(t.pageUp)return Xe(r=>Math.max(0,r-4));if(t.pageDown)return Xe(r=>Math.min(Ut.length-1,r+4));if(t.tab){const e=r().name;return Ae({text:"/"+e+" ",cursor:e.length+2})}if(t.return){const e=r();return!e.menu&&e.args?.startsWith("<")?Ae({text:"/"+e.name+" ",cursor:e.name.length+2}):void $t("/"+e.name)}if(t.escape)return rt(!0)}if(Lt){const r=()=>Ct[Math.min(tt,Ct.length-1)];if(t.upArrow)return nt(r=>(r-1+Ct.length)%Ct.length);if(t.downArrow)return nt(r=>(r+1)%Ct.length);if(t.pageUp)return nt(r=>Math.max(0,r-4));if(t.pageDown)return nt(r=>Math.min(Ct.length-1,r+4));if(t.tab||t.return){const e=r(),t=Me.text.length-(Dt?Dt.length:0)-1,n=Me.text.slice(0,t+1)+e+" ";return Ae({text:n,cursor:n.length})}if(t.escape)return st(!0)}if(t.escape){if("busy"===ge.current&&se.current)return wt(),void ft("info","已请求中断(Esc)");const r=Date.now();return r-mt.current<600&&we.current.length?(mt.current=0,at(0),void it(!0)):void(mt.current=r)}if(t.ctrl&&"c"===e)"busy"===ge.current&&se.current?(wt(),ft("info","已请求中断")):Mt();else{if(t.return&&(t.shift||t.meta)||t.ctrl&&"j"===e)return Ae(r=>({text:r.text.slice(0,r.cursor)+"\n"+r.text.slice(r.cursor),cursor:r.cursor+1}));if(t.return)$t(Me.text);else{if(t.upArrow)return(()=>{const r=we.current;if(!r.length)return;null==ve.current?(be.current=Me.text,ve.current=r.length-1):ve.current>0&&ve.current--;const e=r[ve.current]??"";Ae({text:e,cursor:e.length})})();if(t.downArrow)return(()=>{if(null==ve.current)return;const r=we.current;if(ve.current<r.length-1){ve.current++;const e=r[ve.current];Ae({text:e,cursor:e.length})}else ve.current=null,Ae({text:be.current,cursor:be.current.length})})();if(t.leftArrow)return Ae(r=>({...r,cursor:jr(r.text,r.cursor,-1)}));if(t.rightArrow)return Ae(r=>({...r,cursor:jr(r.text,r.cursor,1)}));if(t.ctrl&&"a"===e)return Ae(r=>({...r,cursor:0}));if(t.ctrl&&"e"===e)return Ae(r=>({...r,cursor:r.text.length}));if(t.ctrl&&"u"===e)return Ae(r=>({text:r.text.slice(r.cursor),cursor:0}));if(t.ctrl&&"k"===e)return Ae(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(t.ctrl&&"w"===e)return Ae(r=>{const e=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:e+r.text.slice(r.cursor),cursor:e.length}});if(t.backspace||t.delete)return ve.current=null,Ae(r=>{const e=t.backspace?jr(r.text,r.cursor,-1):r.cursor,n=t.backspace?r.cursor:jr(r.text,r.cursor,1);return{text:r.text.slice(0,e)+r.text.slice(n),cursor:e}});if(e&&!t.ctrl&&!t.meta){ve.current=null;const r=e.replace(/\r\n?/g,"\n");Ae(e=>({text:e.text.slice(0,e.cursor)+r+e.text.slice(e.cursor),cursor:e.cursor+r.length}))}}}}else xe.current.list.length&&Ge(r=>"expanded"===r?"busy"===ge.current?"collapsed":"hidden":"expanded")}});const Ot=s(()=>ee.current.slice(),[ye]),Gt=O(Ve),Wt=Gt?"exhausted"===Gt.level?`⛔ ${Gt.label}用量已用尽${G(Gt.resetAtEpochMs)?" · "+G(Gt.resetAtEpochMs):""}`:`⚠ ${Gt.label}用量 ${Math.round(100*Gt.pct)}%(${$r(Gt.used)}/${$r(Gt.limit)})`:"";return Ar(yr,{columns:Lr.columns,rows:Lr.rows,version:wr,cwd:Xr,model:r.model,theme:r.theme,accent:r.accentColor,mode:`${ir[Ye]} ${lr[Ye]}`,logItems:Ot,live:ne.current,liveId:te.current,thinking:bt.current,todos:Le,taskView:Oe,busy:"busy"===Ie,activity:Qe||_r,elapsed:Math.floor((Date.now()-Jr.current)/1e3),pending:null,question:null,menu:zr,input:Me,candidates:ut?qt:Et?Ut.map(r=>"/"+r.name+(r.args?" "+r.args:"")+" ["+r.category+"] "+r.desc):Lt?Ct.map(r=>"@"+r):[],selected:ut?lt:Et?Je:tt,candidateTitle:ut?"输入历史":Et?"命令与技能":"引用文件",offset:Or.offset,scrollRevision:Or.revision,scrollRef:Nr,usage:{context:$r(De),total:$r(Ee.prompt+Ee.completion)},queueLen:We,warning:Wt,toolCount:re.current.length})}export async function runInk({config:r,cwd:e,session:t,yolo:n,initialPrompt:o,version:s,updater:u}){wr().catch(()=>{}),c(r).catch(()=>{}),r.sessionId=t.id;const m=[],f={signalRef:{current:null},sessionOf:()=>t},d={clients:[]},p={};let g,h=!1;const x=()=>{h=!0,g?.unmount()};try{g=i(Ar(l,{flexDirection:"column"},Ar(Ir,{columns:process.stdout.columns,rows:process.stdout.rows,version:s}),Ar(a,{dimColor:!0},"正在加载技能与工具…")),{alternateScreen:!0,interactive:!(!process.stdout.isTTY||!process.stdin.isTTY),exitOnCtrlC:!1,maxFps:20}),process.once("SIGTERM",x),process.once("SIGHUP",x),process.once("SIGINT",x);const c=await T(r,e,{queueInject:r=>m.push(r),sessionOf:()=>f.sessionOf(),signalRef:f.signalRef,ask:r=>f.ask?f.ask(r):Promise.resolve("终端尚未就绪"),askUi:r=>f.askUi?f.askUi(r):Promise.resolve({value:null}),runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});return d.clients=c.clients,h?p:(process.removeListener("SIGINT",x),g.rerender(Ar(App,{config:r,cwd:e,session:t,extraTools:c.extraTools,mcp:d,yolo:n,initialPrompt:o,version:s,updater:u,exitState:p,injectQueue:m,bridge:f})),await g.waitUntilExit(),p)}finally{process.removeListener("SIGINT",x),process.removeListener("SIGTERM",x),process.removeListener("SIGHUP",x),g?.unmount(),A(d.clients),gr(),xr(),dr()}}
|
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/vision.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PSEUDO_MODEL_RE=/^(agent|workflow|datahuman|squad)-/;export const VISION_MODEL_RE=/(vl|vision|gpt-4o|omni|multimodal|\dv\b)/i;export function modelSeesImages(t){const e=String(t||"").trim();return!(!e||"auto"===e)&&(!!PSEUDO_MODEL_RE.test(e)||VISION_MODEL_RE.test(e))}export function hasImageParts(t){return Array.isArray(t)&&t.some(t=>t&&"image_url"===t.type)}export function messagesHaveImages(t){return Array.isArray(t)&&t.some(t=>hasImageParts(t&&t.content))}export function stripImageParts(t,e="(图片已省略:当前模型不支持读取图片)"){if(!hasImageParts(t))return t;const s=[];let r=0;for(const e of t)e&&"image_url"===e.type?r++:s.push(e);r&&s.push({type:"text",text:e});const a=s.filter(t=>t&&"text"===t.type);return a.length===s.length?a.map(t=>t.text||"").join("\n\n"):s}export function sanitizeMessagesForModel(t,e){if(!Array.isArray(t)||
|
|
1
|
+
export const PSEUDO_MODEL_RE=/^(agent|workflow|datahuman|squad)-/;export const VISION_MODEL_RE=/(vl|vision|gpt-4o|omni|multimodal|\dv\b)/i;export function modelSeesImages(t){const e=String(t||"").trim();return!(!e||"auto"===e)&&(!!PSEUDO_MODEL_RE.test(e)||VISION_MODEL_RE.test(e))}export function hasImageParts(t){return Array.isArray(t)&&t.some(t=>t&&"image_url"===t.type)}export function messagesHaveImages(t){return Array.isArray(t)&&t.some(t=>hasImageParts(t&&t.content))}export function stripImageParts(t,e="(图片已省略:当前模型不支持读取图片)"){if(!hasImageParts(t))return t;const s=[];let r=0;for(const e of t)e&&"image_url"===e.type?r++:s.push(e);r&&s.push({type:"text",text:e});const a=s.filter(t=>t&&"text"===t.type);return a.length===s.length?a.map(t=>t.text||"").join("\n\n"):s}export function sanitizeMessagesForModel(t,e,s){const r="boolean"==typeof s?s:modelSeesImages(e);if(!Array.isArray(t)||r||!messagesHaveImages(t))return{messages:t,stripped:0};let a=0;return{messages:t.map(t=>hasImageParts(t&&t.content)?(a++,{...t,content:stripImageParts(t.content)}):t),stripped:a}}
|
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()}}
|