@huicai/cli 2026.721.0 → 2026.814.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -6
- package/bin/huicai.js +24 -4
- package/package.json +2 -2
- package/src/account.js +1 -0
- package/src/advisor.js +1 -1
- package/src/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/assets-import.js +1 -0
- package/src/autofix.js +1 -1
- package/src/automation-tools.js +1 -0
- package/src/automation.js +1 -1
- package/src/commands.js +1 -1
- package/src/compaction.js +1 -1
- package/src/computer.js +1 -0
- package/src/config.js +1 -1
- package/src/context-report.js +1 -1
- package/src/context.js +1 -1
- package/src/daemon.js +1 -1
- package/src/decisions.js +1 -0
- package/src/errkind.js +1 -0
- package/src/executors.js +1 -1
- package/src/expert-catalog.js +1 -0
- package/src/experts-store.js +1 -0
- package/src/gitflow.js +1 -1
- package/src/mcp.js +1 -1
- package/src/notify.js +1 -0
- package/src/presence.js +1 -0
- package/src/pyruntime.js +1 -1
- package/src/repl.js +1 -1
- package/src/schedule-freq.js +1 -0
- package/src/schedule.js +1 -1
- package/src/session.js +1 -1
- package/src/skills.js +1 -1
- package/src/squad.js +1 -1
- package/src/subagent.js +1 -1
- package/src/title.js +1 -0
- package/src/tools.js +1 -1
- package/src/transcript.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/vision.js +1 -0
- package/src/visual.js +1 -0
package/src/executors.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import t from"node:path";import{StringDecoder as n}from"node:string_decoder";import{spawn as r,execFileSync as i}from"node:child_process";import*as o from"./ide.js";import{extractDocument as s}from"./api.js";import{pythonEnvForBash as l}from"./pyruntime.js";const c=16777216,a=536870912,u=2e3,f=2e3,p=3e4,h=new Set([".git","node_modules","dist","build","target","out",".next",".nuxt",".venv","venv","__pycache__",".idea",".gradle",".mvn","coverage",".cache","vendor","bower_components","Pods","DerivedData",".turbo",".parcel-cache",".terraform",".pytest_cache",".mypy_cache",".tox",".angular",".svelte-kit",".output",".serverless",".svn",".hg"]);function g(e,n,r){if(!e||"string"!=typeof e)throw new Error("工作目录未设置:请在项目目录中运行(VS Code 中需先「打开文件夹」再对话)");if(!n||"string"!=typeof n)throw new Error("缺少路径 path(工具参数里必须提供 path 字段)");const i=t.resolve(e,n);if(r){const r=t.relative(e,i);if(""===r||r.startsWith("..")||t.isAbsolute(r))throw new Error(`路径超出项目目录(写操作仅限当前项目内):${n}`)}return i}function m(e,n){const r=t.relative(e,n);return""===r?".":r}function d(e){const t=Math.min(e.length,4096);for(let n=0;n<t;n++)if(0===e[n])return!0;return!1}function*$(n,r=0,i=14){let o;try{o=e.readdirSync(n,{withFileTypes:!0})}catch{return}for(const e of o){if(h.has(e.name))continue;const o=t.join(n,e.name);e.isDirectory()?r<i&&(yield*$(o,r+1,i)):e.isFile()&&(yield o)}}function y(e){let t="";for(let n=0;n<e.length;n++){const r=e[n];"*"===r?"*"===e[n+1]?(t+=".*",n++,"/"===e[n+1]&&n++):t+="[^/]*":"?"===r?t+="[^/]":"\\^$+.()|{}[]".includes(r)?t+="\\"+r:t+=r}return new RegExp("^"+t+"$")}export function readFile(t,r){const i=g(t,r.path,!1),o=e.statSync(i);if(o.isDirectory())throw new Error(`是目录而非文件:${r.path}`);if(o.size>4194304)return function(t,r,i,o){const s=Buffer.allocUnsafe(4096),l=e.openSync(r,"r"),c=e.readSync(l,s,0,4096,0);if(e.closeSync(l),d(s.subarray(0,c)))return`(二进制文件,${o.size} 字节,未展示内容)`;const p=Math.max(0,(Number.isFinite(i.offset)?i.offset:1)-1),h=Number.isFinite(i.limit)&&i.limit>0?i.limit:u,{lines:g,lastLine:m,hitEof:$,truncatedScan:y}=function(t,r,i,o,s){const l=r+i,c=e=>e.length>o?e.slice(0,o)+"…(行过长已截断)":e,a=e.openSync(t,"r");try{const t=65536,i=Buffer.allocUnsafe(t),o=new n("utf8"),u=[];let f="",p=0,h=0,g=!1,m=!1,d=!1;for(;!d;){const n=e.readSync(a,i,0,t,null);if(0===n){g=!0;break}h+=n;let $,y=f+o.write(i.subarray(0,n)),w=0;for(;-1!==($=y.indexOf("\n",w));)if(p>=r&&p<l&&u.push(c(y.slice(w,$))),p++,w=$+1,p>=l){d=!0;break}if(f=d?"":y.slice(w),!d&&h>=s){m=!0;break}}return g&&(f+=o.end(),f.length&&p>=r&&p<l?(u.push(c(f)),p++):f.length&&p++),{lines:u,lastLine:p,hitEof:g,truncatedScan:m}}finally{e.closeSync(a)}}(r,p,h,f,a),w=(o.size/1024/1024).toFixed(1);if(!g.length)return y?`(大文件 ${w}MB:起始行 ${p+1} 超出已扫描的前 ${Math.round(512)}MB;请改用 grep 精确定位)`:`(起始行 ${p+1} 超出文件总行数 ${m})`;const b=p+g.length,S=String(b).length,x=g.map((e,t)=>`${String(p+t+1).padStart(S)}→${e}`).join("\n");return x+`\n…(大文件 ${w}MB,${$?`共 ${m} 行`:`${m}+ 行(大文件未全扫)`},本次第 ${p+1}-${b} 行;用 offset/limit 翻页,或用 grep 精确定位)`}(0,i,r,o);const s=e.readFileSync(i);if(d(s))return`(二进制文件,${o.size} 字节,未展示内容)`;const l=s.toString("utf8");if(""===l)return"(空文件)";const c=l.split("\n");c.length>1&&""===c[c.length-1]&&c.pop();const p=c.length,h=Math.max(0,(Number.isFinite(r.offset)?r.offset:1)-1),m=Number.isFinite(r.limit)&&r.limit>0?r.limit:u,$=Math.min(p,h+m);if(h>=p)return`(起始行 ${h+1} 超出文件总行数 ${p})`;const y=String($).length,w=[];for(let e=h;e<$;e++){let t=c[e];t.length>f&&(t=t.slice(0,f)+"…(行过长已截断)"),w.push(`${String(e+1).padStart(y)}→${t}`)}let b=w.join("\n");return(h>0||$<p)&&(b+=`\n…(文件共 ${p} 行,本次显示第 ${h+1}-${$} 行,可用 offset/limit 继续读取)`),b}const w=20971520;export async function readDocument(n,r,i){const o=g(n,r.path,!1),l=e.statSync(o);if(l.isDirectory())throw new Error(`是目录而非文件:${r.path}`);if(l.size>w)throw new Error(`文档超过 ${Math.round(20)}MB 解析上限`);if(!i||!i.baseUrl||!i.apiKey)throw new Error("解析文档需登录平台(先执行 huicai login)");const c=Number.isFinite(Number(r.offset))&&Number(r.offset)>0?Math.floor(Number(r.offset)):0,a=Number.isFinite(Number(r.max_chars))&&Number(r.max_chars)>0?Math.min(18e3,Math.max(1e3,Math.floor(Number(r.max_chars)))):16e3,u=await s(i.baseUrl,i.apiKey,{filename:t.basename(o),data:e.readFileSync(o).toString("base64"),maxChars:a,offset:c}),f=u.chars??0,p=u.text||"";if(!p)return`(第 ${c} 字符起已无内容;全文共 ${f} 字符)`;const h=c+p.length,d=u.truncated?`\n…(全文共 ${f} 字符,本段为第 ${c}–${h} 字符;继续调用 read_document 并传 offset=${h} 读取后续)`:`\n(全文共 ${f} 字符,本段为第 ${c}–${h} 字符,已到结尾)`,$=u.pages?`${u.pages}页·`:"";return`【文档 ${m(n,o)}(${$}共 ${f} 字符)】\n${p}${d}`}export async function writeFile(n,r){const i=g(n,r.path,!0),s=null==r.content?"":String(r.content);if(!0===r.append){let r="";try{r=e.readFileSync(i,"utf8")}catch{}const l=r+s;if(Buffer.byteLength(l)>c)throw new Error(`追加后文件将超过 ${Math.round(16)}MB 上限,请拆分为多个文件`);if(o.isActive()){const e=await o.proposeWrite(i,l,n);if(e.decided)return e.applied?`已追加到 ${m(n,i)}(经 IDE 审阅应用,本段 ${Buffer.byteLength(s)} 字节)`:`用户在 IDE 中拒绝了对 ${m(n,i)} 的追加${e.reason?":"+e.reason:""}`}return e.mkdirSync(t.dirname(i),{recursive:!0}),e.writeFileSync(i,l,"utf8"),`已追加到 ${m(n,i)}(本段 ${Buffer.byteLength(s)} 字节,文件现 ${Buffer.byteLength(l)} 字节)`+(o.isActive()?"(IDE 未响应,已直接写入)":"")}if(o.isActive()){const e=await o.proposeWrite(i,s,n);if(e.decided)return e.applied?`已写入 ${m(n,i)}(经 IDE 审阅应用)`:`用户在 IDE 中拒绝了对 ${m(n,i)} 的修改${e.reason?":"+e.reason:""}`}return e.mkdirSync(t.dirname(i),{recursive:!0}),e.writeFileSync(i,s,"utf8"),`已写入 ${m(n,i)}(${Buffer.byteLength(s)} 字节)`+(o.isActive()?"(IDE 未响应,已直接写入)":"")}export async function editFile(t,n){const r=g(t,n.path,!0),i=null==n.old_string?"":String(n.old_string),s=null==n.new_string?"":String(n.new_string);if(""===i)throw new Error("edit_file 需要非空 old_string");const l=e.readFileSync(r,"utf8"),c=l.split(i).length-1;if(0===c)throw new Error("未找到要替换的内容(old_string 未出现在文件中)。注意:old_string 必须是文件原文,不能包含 read_file 显示的「行号→」前缀");if(c>1&&!n.replace_all)throw new Error(`old_string 在文件中出现 ${c} 处,不唯一;请补充上下文使其唯一,或设 replace_all=true`);const a=n.replace_all?l.split(i).join(s):l.replace(i,s),u=n.replace_all?c:1;if(o.isActive()){const e=await o.proposeWrite(r,a,t);if(e.decided)return e.applied?`已编辑 ${m(t,r)}(经 IDE 审阅应用,替换 ${u} 处)`:`用户在 IDE 中拒绝了对 ${m(t,r)} 的修改${e.reason?":"+e.reason:""}`}return e.writeFileSync(r,a,"utf8"),`已编辑 ${m(t,r)},替换 ${u} 处`+(o.isActive()?"(IDE 未响应,已直接写入)":"")}export function listFiles(n,r){const i=g(n,r.path||".",!1),o=[];let s=0;const l=(n,r)=>{if(s>=400)return;let c;try{c=e.readdirSync(n,{withFileTypes:!0})}catch{return}c.sort((e,t)=>e.name.localeCompare(t.name));for(const e of c){if(s>=400)break;if(h.has(e.name))continue;const c=t.join(n,e.name),a=t.relative(i,c);e.isDirectory()?(o.push(a+"/"),s++,r<6&&l(c,r+1)):(o.push(a),s++)}};return l(i,0),0===o.length?"(空目录)":o.join("\n")+(s>=400?"\n…(条目已截断)":"")}export function glob(e,n){if(!n.pattern)throw new Error("缺少 pattern");const r=y(n.pattern),i=[];for(const n of $(e)){const o=t.relative(e,n).split(t.sep).join("/");if(r.test(o)&&(i.push(o),i.length>=300))break}return 0===i.length?"(无匹配文件)":i.join("\n")}let b=null;export function grep(e,t){if(!t.pattern)throw new Error("缺少 pattern");const n=function(){if(null!==b)return b;try{i("rg",["--version"],{stdio:"ignore"}),b=!0}catch{b=!1}return b}()?function(e,t){const n=Number.isFinite(t.max_results)&&t.max_results>0?Math.min(t.max_results,500):200,r=t.path?String(t.path):".",o=[...h].flatMap(e=>["-g",`!**/${e}/**`,"-g",`!${e}/**`]),s=["--color=never","--no-messages","--max-columns","2000"];t.ignore_case&&s.push("-i");t.glob&&s.push("-g",String(t.glob));s.push(...o),t.files_only?s.push("-l"):s.push("-n","--no-heading");let l;s.push("-e",String(t.pattern),"--",r);try{l=i("rg",s,{cwd:e,encoding:"utf8",maxBuffer:32<<20,stdio:["ignore","pipe","pipe"]})}catch(e){return e&&1===e.status?"(无匹配)":null}const c=e=>e.replace(/\\/g,"/").replace(/^\.\//,""),a=l.split("\n").filter(Boolean);if(t.files_only)return a.length?a.slice(0,n).map(c).join("\n"):"(无匹配)";const u=[];for(const e of a){const t=/^(.*?):(\d+):(.*)$/.exec(e);if(t&&(u.push(`${c(t[1])}:${t[2]}: ${t[3].slice(0,240)}`),u.length>=n))break}return u.length?u.join("\n")+(u.length>=n?"\n…(结果已截断)":""):"(无匹配)"}(e,t):null;return null!==n?n:grepJs(e,t)}export function grepJs(n,r){if(!r.pattern)throw new Error("缺少 pattern");let i;try{i=new RegExp(r.pattern,r.ignore_case?"i":void 0)}catch(e){throw new Error(`无效的正则:${e.message}`)}const o=r.glob?y(r.glob):null,s=g(n,r.path||".",!1),l=Number.isFinite(r.max_results)&&r.max_results>0?Math.min(r.max_results,500):200,c=[],a=[];for(const u of $(s)){if(c.length>=l)break;const s=t.relative(n,u).split(t.sep).join("/");if(o&&!o.test(s))continue;let f;try{f=e.statSync(u)}catch{continue}let p=!1;if(f.size<=262144){let t;try{t=e.readFileSync(u)}catch{continue}if(d(t))continue;const n=t.toString("utf8").split("\n");for(let e=0;e<n.length;e++)if(i.test(n[e])){if(p=!0,r.files_only)break;if(c.push(`${s}:${e+1}: ${n[e].slice(0,240)}`),c.length>=l)break}}else p=S(u,i,s,!!r.files_only,c,l);if(p&&(a.push(s),r.files_only&&a.length>=l))break}return r.files_only?a.length?a.join("\n"):"(无匹配)":0===c.length?"(无匹配)":c.join("\n")+(c.length>=l?"\n…(结果已截断)":"")}function S(t,r,i,o,s,l){let c;try{c=e.openSync(t,"r")}catch{return!1}try{const t=65536,a=Buffer.allocUnsafe(t),u=new n("utf8");let f="",p=0,h=0,g=!1,m=!0,$=!1;for(;;){const n=e.readSync(c,a,0,t,null);if(0===n)break;if(m){if(d(a.subarray(0,n)))return!1;m=!1}h+=n;const y=f+u.write(a.subarray(0,n));let w,b=0;for(;-1!==(w=y.indexOf("\n",b));){p++;const e=y.slice(b,w);if(r.test(e)){if(g=!0,o)return!0;if(s.push(`${i}:${p}: ${e.slice(0,240)}`),s.length>=l)return!0}b=w+1}if(f=y.slice(b),h>=16777216){$=!0;break}}return $||(f+=u.end(),f.length&&(p++,r.test(f)&&(g=!0,!o&&s.length<l&&s.push(`${i}:${p}: ${f.slice(0,240)}`)))),g}finally{e.closeSync(c)}}export function runBash(e,t){const n=null==t.command?"":String(t.command);if(""===n.trim())return Promise.resolve("(未提供命令)");const i=Number.isFinite(t.timeout)?Math.min(t.timeout,6e5):12e4,o="win32"===process.platform;return new Promise(t=>{let s="",c=!1;const a={...process.env,...l()},u=o?r(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",n],{cwd:e,env:a,windowsVerbatimArguments:!0}):r(process.env.SHELL||"/bin/sh",["-c",n],{cwd:e,env:a,detached:!0}),f=setTimeout(()=>{c=!0,(()=>{try{!o&&u.pid?process.kill(-u.pid,"SIGKILL"):u.kill("SIGKILL")}catch{try{u.kill("SIGKILL")}catch{}}})()},i),h=e=>{s.length<p&&(s+=e.toString())};u.stdout.on("data",h),u.stderr.on("data",h),u.on("error",e=>{clearTimeout(f),t(`命令启动失败:${e.message}`)}),u.on("close",e=>{clearTimeout(f);let n=s.length>p?s.slice(0,p)+"\n…(输出已截断)":s;""===n.trim()&&(n="(无输出)");t(`${c?"【超时被终止】":`退出码 ${e}`}\n${n}`)})})}export async function fetchUrl(e,t){const n=String(t.url||"").trim();if(!/^https?:\/\//i.test(n))throw new Error("仅支持 http/https URL");let r;try{r=await fetch(n,{redirect:"follow",signal:AbortSignal.timeout(Number.isFinite(t.timeout)?Math.min(t.timeout,12e4):3e4),headers:{"User-Agent":"huicai-cli",Accept:"text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.5"}})}catch(e){throw new Error(`请求失败:${e?.cause?.message||e.message}`)}const i=await async function(e,t){if(!e.body)return Buffer.alloc(0);const n=e.body.getReader(),r=[];let i=0;for(;;){const{done:e,value:o}=await n.read();if(e)break;if(r.push(o),i+=o.length,i>=t){n.cancel().catch(()=>{});break}}return Buffer.concat(r.map(e=>Buffer.from(e)),Math.min(i,t))}(r,2097152),o=(r.headers.get("content-type")||"").toLowerCase();if(d(i)&&!/text|json|xml|html/.test(o))return`HTTP ${r.status} · ${o||"未知类型"} · ${i.length} 字节(二进制内容未展示)`;let s=function(e,t){let n=(/charset=([\w-]+)/i.exec(t||"")||[])[1];if(!n){const t=e.slice(0,4096).toString("latin1");n=(/<meta[^>]+charset\s*=\s*["']?([\w-]+)/i.exec(t)||[])[1]}const r=String(n||"utf-8").toLowerCase();if("utf-8"===r||"utf8"===r)return e.toString("utf8");try{return new TextDecoder(r).decode(e)}catch{return e.toString("utf8")}}(i,o),l="",c="";if(o.includes("html")||/^\s*<(!doctype|html)/i.test(s)){if(l=((/<title[^>]*>([\s\S]*?)<\/title>/i.exec(s)||[])[1]||"").replace(/\s+/g," ").trim().slice(0,120),t.links){const e=function(e,t,n=60){const r=[],i=new Set,o=/<a\b[^>]*href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>([\s\S]*?)<\/a>/gi;let s;for(;(s=o.exec(e))&&r.length<n;){const e=(s[1]??s[2]??s[3]??"").trim();if(!e||/^(javascript:|mailto:|tel:|#)/i.test(e))continue;let n;try{n=new URL(e,t).href}catch{continue}if(!/^https?:/i.test(n)||i.has(n))continue;i.add(n);const o=s[4].replace(/<[^>]+>/g,"").replace(/\s+/g," ").trim().slice(0,80);r.push(`- ${o||"(无文本)"} → ${n}`)}return r}(s,r.url||n);e.length&&(c=`\n\n—— 页面链接(${e.length} 条)——\n`+e.join("\n"))}s=function(e){let t=String(e);return t=t.replace(/<(script|style|noscript|svg|head)[\s\S]*?<\/\1>/gi,""),t=t.replace(/<!--[\s\S]*?-->/g,""),t=t.replace(/<br\s*\/?>/gi,"\n"),t=t.replace(/<\/(p|div|li|tr|h[1-6]|section|article|blockquote|pre|ul|ol|table)>/gi,"\n"),t=t.replace(/<li[^>]*>/gi,"- "),t=t.replace(/<[^>]+>/g,""),t=t.replace(/ /g," ").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'|'/g,"'").replace(/&/g,"&"),t=t.replace(/[ \t]+\n/g,"\n").replace(/\n{3,}/g,"\n\n"),t}(s)}s=s.trim(),s.length>8e4&&(s=s.slice(0,8e4)+"\n…(内容过长已截断)");return`${`HTTP ${r.status}${l?` · ${l}`:""}${r.url&&r.url!==n?` · 重定向至 ${r.url}`:""}`}\n${s||"(无内容)"}${c}`}
|
|
1
|
+
import e from"node:fs";import t from"node:path";import{StringDecoder as n}from"node:string_decoder";import{spawn as r,execFileSync as i}from"node:child_process";import*as o from"./ide.js";import{extractDocument as s}from"./api.js";import{pythonEnvForBash as l}from"./pyruntime.js";const c=16777216,a=536870912,f=2e3,u=2e3,p=3e4,h=new Set([".git","node_modules","dist","build","target","out",".next",".nuxt",".venv","venv","__pycache__",".idea",".gradle",".mvn","coverage",".cache","vendor","bower_components","Pods","DerivedData",".turbo",".parcel-cache",".terraform",".pytest_cache",".mypy_cache",".tox",".angular",".svelte-kit",".output",".serverless",".svn",".hg"]);function g(e,n,r){if(!e||"string"!=typeof e)throw new Error("工作目录未设置:请在项目目录中运行(VS Code 中需先「打开文件夹」再对话)");if(!n||"string"!=typeof n)throw new Error("缺少路径 path(工具参数里必须提供 path 字段)");const i=t.resolve(e,n);if(r){const r=t.relative(e,i);if(""===r||r.startsWith("..")||t.isAbsolute(r))throw new Error(`路径超出项目目录(写操作仅限当前项目内):${n}`)}return i}function m(e,n){const r=t.relative(e,n);return""===r?".":r}function d(e){const t=Math.min(e.length,4096);for(let n=0;n<t;n++)if(0===e[n])return!0;return!1}function*$(n,r=0,i=14){let o;try{o=e.readdirSync(n,{withFileTypes:!0})}catch{return}for(const e of o){if(h.has(e.name))continue;const o=t.join(n,e.name);e.isDirectory()?r<i&&(yield*$(o,r+1,i)):e.isFile()&&(yield o)}}function y(e){let t="";for(let n=0;n<e.length;n++){const r=e[n];"*"===r?"*"===e[n+1]?(t+=".*",n++,"/"===e[n+1]&&n++):t+="[^/]*":"?"===r?t+="[^/]":"\\^$+.()|{}[]".includes(r)?t+="\\"+r:t+=r}return new RegExp("^"+t+"$")}export function readFile(t,r){const i=g(t,r.path,!1),o=e.statSync(i);if(o.isDirectory())throw new Error(`是目录而非文件:${r.path}`);if(o.size>4194304)return function(t,r,i,o){const s=Buffer.allocUnsafe(4096),l=e.openSync(r,"r"),c=e.readSync(l,s,0,4096,0);if(e.closeSync(l),d(s.subarray(0,c)))return`(二进制文件,${o.size} 字节,未展示内容)`;const p=Math.max(0,(Number.isFinite(i.offset)?i.offset:1)-1),h=Number.isFinite(i.limit)&&i.limit>0?i.limit:f,{lines:g,lastLine:m,hitEof:$,truncatedScan:y}=function(t,r,i,o,s){const l=r+i,c=e=>e.length>o?e.slice(0,o)+"…(行过长已截断)":e,a=e.openSync(t,"r");try{const t=65536,i=Buffer.allocUnsafe(t),o=new n("utf8"),f=[];let u="",p=0,h=0,g=!1,m=!1,d=!1;for(;!d;){const n=e.readSync(a,i,0,t,null);if(0===n){g=!0;break}h+=n;let $,y=u+o.write(i.subarray(0,n)),w=0;for(;-1!==($=y.indexOf("\n",w));)if(p>=r&&p<l&&f.push(c(y.slice(w,$))),p++,w=$+1,p>=l){d=!0;break}if(u=d?"":y.slice(w),!d&&h>=s){m=!0;break}}return g&&(u+=o.end(),u.length&&p>=r&&p<l?(f.push(c(u)),p++):u.length&&p++),{lines:f,lastLine:p,hitEof:g,truncatedScan:m}}finally{e.closeSync(a)}}(r,p,h,u,a),w=(o.size/1024/1024).toFixed(1);if(!g.length)return y?`(大文件 ${w}MB:起始行 ${p+1} 超出已扫描的前 ${Math.round(512)}MB;请改用 grep 精确定位)`:`(起始行 ${p+1} 超出文件总行数 ${m})`;const b=p+g.length,S=String(b).length,x=g.map((e,t)=>`${String(p+t+1).padStart(S)}→${e}`).join("\n");return x+`\n…(大文件 ${w}MB,${$?`共 ${m} 行`:`${m}+ 行(大文件未全扫)`},本次第 ${p+1}-${b} 行;用 offset/limit 翻页,或用 grep 精确定位)`}(0,i,r,o);const s=e.readFileSync(i);if(d(s))return`(二进制文件,${o.size} 字节,未展示内容)`;const l=s.toString("utf8");if(""===l)return"(空文件)";const c=l.split("\n");c.length>1&&""===c[c.length-1]&&c.pop();const p=c.length,h=Math.max(0,(Number.isFinite(r.offset)?r.offset:1)-1),m=Number.isFinite(r.limit)&&r.limit>0?r.limit:f,$=Math.min(p,h+m);if(h>=p)return`(起始行 ${h+1} 超出文件总行数 ${p})`;const y=String($).length,w=[];for(let e=h;e<$;e++){let t=c[e];t.length>u&&(t=t.slice(0,u)+"…(行过长已截断)"),w.push(`${String(e+1).padStart(y)}→${t}`)}let b=w.join("\n");return(h>0||$<p)&&(b+=`\n…(文件共 ${p} 行,本次显示第 ${h+1}-${$} 行,可用 offset/limit 继续读取)`),b}const w=20971520;export async function readDocument(n,r,i){const o=g(n,r.path,!1),l=e.statSync(o);if(l.isDirectory())throw new Error(`是目录而非文件:${r.path}`);if(l.size>w)throw new Error(`文档超过 ${Math.round(20)}MB 解析上限`);if(!i||!i.baseUrl||!i.apiKey)throw new Error("解析文档需登录平台(先执行 huicai login)");const c=Number.isFinite(Number(r.offset))&&Number(r.offset)>0?Math.floor(Number(r.offset)):0,a=Number.isFinite(Number(r.max_chars))&&Number(r.max_chars)>0?Math.min(18e3,Math.max(1e3,Math.floor(Number(r.max_chars)))):16e3,f=await s(i.baseUrl,i.apiKey,{filename:t.basename(o),data:e.readFileSync(o).toString("base64"),maxChars:a,offset:c}),u=f.chars??0,p=f.text||"";if(!p)return`(第 ${c} 字符起已无内容;全文共 ${u} 字符)`;const h=c+p.length,d=f.truncated?`\n…(全文共 ${u} 字符,本段为第 ${c}–${h} 字符;继续调用 read_document 并传 offset=${h} 读取后续)`:`\n(全文共 ${u} 字符,本段为第 ${c}–${h} 字符,已到结尾)`,$=f.pages?`${f.pages}页·`:"";return`【文档 ${m(n,o)}(${$}共 ${u} 字符)】\n${p}${d}`}export async function writeFile(n,r){const i=g(n,r.path,!0),s=null==r.content?"":String(r.content);if(!0===r.append){let r="";try{r=e.readFileSync(i,"utf8")}catch{}const l=r+s;if(Buffer.byteLength(l)>c)throw new Error(`追加后文件将超过 ${Math.round(16)}MB 上限,请拆分为多个文件`);if(o.isActive()){const e=await o.proposeWrite(i,l,n);if(e.decided)return e.applied?`已追加到 ${m(n,i)}(经 IDE 审阅应用,本段 ${Buffer.byteLength(s)} 字节)`:`用户在 IDE 中拒绝了对 ${m(n,i)} 的追加${e.reason?":"+e.reason:""}`}return e.mkdirSync(t.dirname(i),{recursive:!0}),e.writeFileSync(i,l,"utf8"),`已追加到 ${m(n,i)}(本段 ${Buffer.byteLength(s)} 字节,文件现 ${Buffer.byteLength(l)} 字节)`+(o.isActive()?"(IDE 未响应,已直接写入)":"")}if(o.isActive()){const e=await o.proposeWrite(i,s,n);if(e.decided)return e.applied?`已写入 ${m(n,i)}(经 IDE 审阅应用)`:`用户在 IDE 中拒绝了对 ${m(n,i)} 的修改${e.reason?":"+e.reason:""}`}return e.mkdirSync(t.dirname(i),{recursive:!0}),e.writeFileSync(i,s,"utf8"),`已写入 ${m(n,i)}(${Buffer.byteLength(s)} 字节)`+(o.isActive()?"(IDE 未响应,已直接写入)":"")}export async function editFile(t,n){const r=g(t,n.path,!0),i=null==n.old_string?"":String(n.old_string),s=null==n.new_string?"":String(n.new_string);if(""===i)throw new Error("edit_file 需要非空 old_string");if(!e.existsSync(r))throw new Error(`目标文件不存在:${n.path}。请先用 read_file / list_files 确认路径是否正确(可能工作目录不对,或文件已被移动/删除);不要改用 write_file 新建一个同名空文件替代——如确需新建,请先向用户说明再创建。`);const l=e.readFileSync(r,"utf8"),c=l.split(i).length-1;if(0===c)throw new Error("未找到要替换的内容(old_string 未出现在文件中)。注意:old_string 必须是文件原文,不能包含 read_file 显示的「行号→」前缀");if(c>1&&!n.replace_all)throw new Error(`old_string 在文件中出现 ${c} 处,不唯一;请补充上下文使其唯一,或设 replace_all=true`);const a=n.replace_all?l.split(i).join(s):l.replace(i,s),f=n.replace_all?c:1;if(o.isActive()){const e=await o.proposeWrite(r,a,t);if(e.decided)return e.applied?`已编辑 ${m(t,r)}(经 IDE 审阅应用,替换 ${f} 处)`:`用户在 IDE 中拒绝了对 ${m(t,r)} 的修改${e.reason?":"+e.reason:""}`}return e.writeFileSync(r,a,"utf8"),`已编辑 ${m(t,r)},替换 ${f} 处`+(o.isActive()?"(IDE 未响应,已直接写入)":"")}export function listFiles(n,r){const i=g(n,r.path||".",!1),o=[];let s=0;const l=(n,r)=>{if(s>=400)return;let c;try{c=e.readdirSync(n,{withFileTypes:!0})}catch{return}c.sort((e,t)=>e.name.localeCompare(t.name));for(const e of c){if(s>=400)break;if(h.has(e.name))continue;const c=t.join(n,e.name),a=t.relative(i,c);e.isDirectory()?(o.push(a+"/"),s++,r<6&&l(c,r+1)):(o.push(a),s++)}};return l(i,0),0===o.length?"(空目录)":o.join("\n")+(s>=400?"\n…(条目已截断)":"")}export function listWorkspaceFiles(e,n=2e3){const r=[];for(const i of $(e))if(r.push(t.relative(e,i).split(t.sep).join("/")),r.length>=n)break;return r}export function glob(e,n){if(!n.pattern)throw new Error("缺少 pattern");const r=y(n.pattern),i=[];for(const n of $(e)){const o=t.relative(e,n).split(t.sep).join("/");if(r.test(o)&&(i.push(o),i.length>=300))break}return 0===i.length?"(无匹配文件)":i.join("\n")}let b=null;export function grep(e,t){if(!t.pattern)throw new Error("缺少 pattern");const n=function(){if(null!==b)return b;try{i("rg",["--version"],{stdio:"ignore"}),b=!0}catch{b=!1}return b}()?function(e,t){const n=Number.isFinite(t.max_results)&&t.max_results>0?Math.min(t.max_results,500):200,r=t.path?String(t.path):".",o=[...h].flatMap(e=>["-g",`!**/${e}/**`,"-g",`!${e}/**`]),s=["--color=never","--no-messages","--max-columns","2000"];t.ignore_case&&s.push("-i");t.glob&&s.push("-g",String(t.glob));s.push(...o),t.files_only?s.push("-l"):s.push("-n","--no-heading");let l;s.push("-e",String(t.pattern),"--",r);try{l=i("rg",s,{cwd:e,encoding:"utf8",maxBuffer:32<<20,stdio:["ignore","pipe","pipe"]})}catch(e){return e&&1===e.status?"(无匹配)":null}const c=e=>e.replace(/\\/g,"/").replace(/^\.\//,""),a=l.split("\n").filter(Boolean);if(t.files_only)return a.length?a.slice(0,n).map(c).join("\n"):"(无匹配)";const f=[];for(const e of a){const t=/^(.*?):(\d+):(.*)$/.exec(e);if(t&&(f.push(`${c(t[1])}:${t[2]}: ${t[3].slice(0,240)}`),f.length>=n))break}return f.length?f.join("\n")+(f.length>=n?"\n…(结果已截断)":""):"(无匹配)"}(e,t):null;return null!==n?n:grepJs(e,t)}export function grepJs(n,r){if(!r.pattern)throw new Error("缺少 pattern");let i;try{i=new RegExp(r.pattern,r.ignore_case?"i":void 0)}catch(e){throw new Error(`无效的正则:${e.message}`)}const o=r.glob?y(r.glob):null,s=g(n,r.path||".",!1),l=Number.isFinite(r.max_results)&&r.max_results>0?Math.min(r.max_results,500):200,c=[],a=[];for(const f of $(s)){if(c.length>=l)break;const s=t.relative(n,f).split(t.sep).join("/");if(o&&!o.test(s))continue;let u;try{u=e.statSync(f)}catch{continue}let p=!1;if(u.size<=262144){let t;try{t=e.readFileSync(f)}catch{continue}if(d(t))continue;const n=t.toString("utf8").split("\n");for(let e=0;e<n.length;e++)if(i.test(n[e])){if(p=!0,r.files_only)break;if(c.push(`${s}:${e+1}: ${n[e].slice(0,240)}`),c.length>=l)break}}else p=S(f,i,s,!!r.files_only,c,l);if(p&&(a.push(s),r.files_only&&a.length>=l))break}return r.files_only?a.length?a.join("\n"):"(无匹配)":0===c.length?"(无匹配)":c.join("\n")+(c.length>=l?"\n…(结果已截断)":"")}function S(t,r,i,o,s,l){let c;try{c=e.openSync(t,"r")}catch{return!1}try{const t=65536,a=Buffer.allocUnsafe(t),f=new n("utf8");let u="",p=0,h=0,g=!1,m=!0,$=!1;for(;;){const n=e.readSync(c,a,0,t,null);if(0===n)break;if(m){if(d(a.subarray(0,n)))return!1;m=!1}h+=n;const y=u+f.write(a.subarray(0,n));let w,b=0;for(;-1!==(w=y.indexOf("\n",b));){p++;const e=y.slice(b,w);if(r.test(e)){if(g=!0,o)return!0;if(s.push(`${i}:${p}: ${e.slice(0,240)}`),s.length>=l)return!0}b=w+1}if(u=y.slice(b),h>=16777216){$=!0;break}}return $||(u+=f.end(),u.length&&(p++,r.test(u)&&(g=!0,!o&&s.length<l&&s.push(`${i}:${p}: ${u.slice(0,240)}`)))),g}finally{e.closeSync(c)}}export function runBash(e,t,i,o){const s=null==t.command?"":String(t.command);if(""===s.trim())return Promise.resolve("(未提供命令)");const c=Number.isFinite(t.timeout)?Math.min(t.timeout,6e5):12e4,a="win32"===process.platform;return new Promise(t=>{let i="",f=!1,u=!1;const h={...process.env,...l()},g=a?r(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",s],{cwd:e,env:h,windowsVerbatimArguments:!0}):r(process.env.SHELL||"/bin/sh",["-c",s],{cwd:e,env:h,detached:!0}),m=()=>{try{!a&&g.pid?process.kill(-g.pid,"SIGKILL"):g.kill("SIGKILL")}catch{try{g.kill("SIGKILL")}catch{}}},d=setTimeout(()=>{f=!0,m()},c),$=()=>{u=!0,f=!0,m()};o&&(o.aborted?$():o.addEventListener("abort",$,{once:!0}));const y=()=>{clearTimeout(d),o&&o.removeEventListener("abort",$)},w=new n("utf8"),b=new n("utf8"),S=e=>{i.length<p&&(i+=e)};g.stdout.on("data",e=>S(w.write(e))),g.stderr.on("data",e=>S(b.write(e))),g.on("error",e=>{y(),t(`命令启动失败:${e.message}`)}),g.on("close",e=>{y(),S(w.end()),S(b.end());let n=i.length>p?i.slice(0,p)+"\n…(输出已截断)":i;""===n.trim()&&(n="(无输出)");t(`${u?"【已中断】":f?"【超时被终止】":`退出码 ${e}`}\n${n}`)})})}export async function fetchUrl(e,t){const n=String(t.url||"").trim();if(!/^https?:\/\//i.test(n))throw new Error("仅支持 http/https URL");let r;try{r=await fetch(n,{redirect:"follow",signal:AbortSignal.timeout(Number.isFinite(t.timeout)?Math.min(t.timeout,12e4):3e4),headers:{"User-Agent":"huicai-cli",Accept:"text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.5"}})}catch(e){throw new Error(`请求失败:${e?.cause?.message||e.message}`)}const i=await async function(e,t){if(!e.body)return Buffer.alloc(0);const n=e.body.getReader(),r=[];let i=0;for(;;){const{done:e,value:o}=await n.read();if(e)break;if(r.push(o),i+=o.length,i>=t){n.cancel().catch(()=>{});break}}return Buffer.concat(r.map(e=>Buffer.from(e)),Math.min(i,t))}(r,2097152),o=(r.headers.get("content-type")||"").toLowerCase();if(d(i)&&!/text|json|xml|html/.test(o))return`HTTP ${r.status} · ${o||"未知类型"} · ${i.length} 字节(二进制内容未展示)`;let s=function(e,t){let n=(/charset=([\w-]+)/i.exec(t||"")||[])[1];if(!n){const t=e.slice(0,4096).toString("latin1");n=(/<meta[^>]+charset\s*=\s*["']?([\w-]+)/i.exec(t)||[])[1]}const r=String(n||"utf-8").toLowerCase();if("utf-8"===r||"utf8"===r)return e.toString("utf8");try{return new TextDecoder(r).decode(e)}catch{return e.toString("utf8")}}(i,o),l="",c="";if(o.includes("html")||/^\s*<(!doctype|html)/i.test(s)){if(l=((/<title[^>]*>([\s\S]*?)<\/title>/i.exec(s)||[])[1]||"").replace(/\s+/g," ").trim().slice(0,120),t.links){const e=function(e,t,n=60){const r=[],i=new Set,o=/<a\b[^>]*href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>([\s\S]*?)<\/a>/gi;let s;for(;(s=o.exec(e))&&r.length<n;){const e=(s[1]??s[2]??s[3]??"").trim();if(!e||/^(javascript:|mailto:|tel:|#)/i.test(e))continue;let n;try{n=new URL(e,t).href}catch{continue}if(!/^https?:/i.test(n)||i.has(n))continue;i.add(n);const o=s[4].replace(/<[^>]+>/g,"").replace(/\s+/g," ").trim().slice(0,80);r.push(`- ${o||"(无文本)"} → ${n}`)}return r}(s,r.url||n);e.length&&(c=`\n\n—— 页面链接(${e.length} 条)——\n`+e.join("\n"))}s=function(e){let t=String(e);return t=t.replace(/<(script|style|noscript|svg|head)[\s\S]*?<\/\1>/gi,""),t=t.replace(/<!--[\s\S]*?-->/g,""),t=t.replace(/<br\s*\/?>/gi,"\n"),t=t.replace(/<\/(p|div|li|tr|h[1-6]|section|article|blockquote|pre|ul|ol|table)>/gi,"\n"),t=t.replace(/<li[^>]*>/gi,"- "),t=t.replace(/<[^>]+>/g,""),t=t.replace(/ /g," ").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'|'/g,"'").replace(/&/g,"&"),t=t.replace(/[ \t]+\n/g,"\n").replace(/\n{3,}/g,"\n\n"),t}(s)}s=s.trim(),s.length>8e4&&(s=s.slice(0,8e4)+"\n…(内容过长已截断)");return`${`HTTP ${r.status}${l?` · ${l}`:""}${r.url&&r.url!==n?` · 重定向至 ${r.url}`:""}`}\n${s||"(无内容)"}${c}`}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CATEGORIES=["办公自动化","数据智能","文档写作","内容创作","金融投资","产品设计","技术工程","营销增长","法律财税","教育科研","运营人力"];export const SORTS=[{key:"smart",label:"综合"},{key:"hot",label:"最热"},{key:"new",label:"最新"}];export const EXPERTS=[{id:"excel-pro",name:"Excel 数据处理专家",by:"表哥",emoji:"📊",cat:"办公自动化",hot:3200,featured:!0,desc:"把杂乱表格整理成干净数据,做透视、公式、图表与自动化报表。",tags:["数据清洗","透视分析","自动报表"],tasks:["帮我清洗这份 Excel 并做一张透视表和趋势图","把多个 Sheet 合并成一张标准明细表"],prompt:["你是资深 Excel / 表格数据处理专家「表哥」,服务于办公自动化场景。","","工作准则:","- 面对真实表格文件,一律用 run_python(pandas/openpyxl)精确读取处理,绝不凭文本摘要臆测数据;公式、多 Sheet、合并单元格、样式都要照顾到。","- 先摸清数据(列含义、量级、缺失、异常、口径),与用户对齐目标后再动手;每个结论都能回答「怎么算出来的」。","- 交付可复用的结果:清洗后的干净表 + 透视/汇总 + 图表,落盘到工作区,必要时给一段可重复执行的脚本。","- 表头脏、类型混乱、日期格式不一等常见坑主动识别与修复;处理前对原始文件做保留,不覆盖用户原件。","","交付标准:结论先行,随后「数据事实 / 推断」分开陈述,附口径说明与可执行文件。"].join("\n")},{id:"ppt-pro",name:"PPT 演示设计专家",by:"片神",emoji:"📽️",cat:"办公自动化",hot:2800,featured:!0,desc:"把要点梳理成有叙事的演示大纲,生成结构清晰、观点鲜明的 PPT。",tags:["演示叙事","结构排版","一键成稿"],tasks:["把这份材料做成 15 页的汇报 PPT","给这个方案做一版结论先行的路演 PPT"],prompt:["你是演示设计专家「片神」,擅长把零散材料变成有说服力的演示。","","工作准则:","- 先立叙事主线(受众是谁、要他们记住什么、期望的行动),再定每页的「一页一观点」,最后才谈视觉。","- 用可落地的方式生成:优先 HTML 演示 / python-pptx 产出可编辑成品,落盘到工作区并可预览导出。","- 每页标题写成「观点句」而非「名词短语」;图表为结论服务,克制装饰。","- 控制信息密度:一页不超过一个核心信息,复杂内容拆页或用图示。","","交付标准:演示大纲(页题=观点)+ 可编辑成品文件 + 讲者备注要点。"].join("\n")},{id:"word-pro",name:"长文档写作专家",by:"文胆",emoji:"📝",cat:"文档写作",hot:2600,featured:!0,desc:"把提纲、访谈、旧稿整理成结构清晰的长文档,支持章节续写与成稿前检查。",tags:["长文写作","结构编排","成稿收口"],tasks:["把这些素材整理成一份结构清晰的方案文档","给这篇长文做成稿前的一致性与错漏检查"],prompt:["你是专业长文档写作专家「文胆」,面向报告、方案、白皮书、说明文档等正式长文。","","工作准则:","- 先搭骨架(受众/目的/结论/大纲)再填肉;章节之间逻辑连贯,避免堆砌。","- 限定范围改稿:只在用户框定的章节内改,不擅自重写全文;保留可追溯的修改说明。","- 成稿前做收口检查:术语一致、编号连续、引用可核、结论与正文不打架。","- 长文一律落盘为 Markdown/Docx 交付,必要时用 run_python 生成 Word。","","交付标准:可直接使用的文档 + 一页修订说明(改了什么、为什么)。"].join("\n")},{id:"data-analyst",name:"数据分析专家",by:"数衡",emoji:"📈",cat:"数据智能",hot:3400,featured:!0,desc:"一份 Excel / CSV 进,趋势、结构、异常三视角分析,交付网页 / Excel / PPT 报告。",tags:["数据分析","归因诊断","多格式报告"],tasks:["分析这份销售数据的趋势、结构与异常,出一份报告","这批数据里有什么值得关注的问题?"],prompt:["你是数据分析专家「数衡」,把一份数据变成决策者能读懂的洞察。","","工作准则:","- 用 run_python(pandas)实打实地算,绝不编造数字;先描述(量级/分布/趋势)后解释(对比/拆解/归因)。","- 三视角扫描:趋势(随时间怎么变)、结构(构成/占比/分层)、异常(离群/突变/缺口),每个发现标注是「数据事实」还是「推断」。","- 归因给证据链与置信度;相关不当因果;样本不足就如实说数据不支持结论。","- 交付形式随任务定:网页仪表盘 / Excel 三格式报表 / PPT,落盘工作区可导出。","","交付标准:结论先行的分析报告 + 关键图表 + 数据口径附录。"].join("\n")},{id:"data-viz",name:"数据可视化专家",by:"绘象",emoji:"🎨",cat:"数据智能",hot:2100,desc:"为结论选对图表,产出可运行的绘图代码与可交互的网页看板。",tags:["图表选型","可交互看板","绘图代码"],tasks:["把这份数据做成一个可交互的网页看板","给这些指标选合适的图表并生成绘图代码"],prompt:["你是数据可视化专家「绘象」,让数据「一眼看懂」。","","工作准则:","- 每张图先写「这张图想让读者看出什么」,再选型:趋势用线、构成用堆叠、对比用条形、分布用箱形/直方,拒绝装饰性图表。","- 产出本机可运行的绘图代码(matplotlib/plotly)或前端图表配置;需要交互时做成单页 HTML 看板,落盘可预览。","- 配色克制、标注清晰、坐标不误导(零基线、单位、图例齐全)。","","交付标准:图表清单(图 → 要传达的结论 → 代码/配置)+ 可运行文件。"].join("\n")},{id:"report-writer",name:"报告撰写专家",by:"陈章",emoji:"📄",cat:"文档写作",hot:1500,desc:"把分析、数据与素材组织成决策者可读的专业报告。",tags:["结论先行","证据链","决策报告"],tasks:["把这些分析结果写成一份给管理层的报告"],prompt:["你是报告撰写专家「陈章」,专攻把复杂信息组织成好读的决策报告。","","工作准则:结论先行(一段话说清最重要的发现)→ 按「发现 → 证据 → 建议」组织 → 每条建议标注依据。","让决策者 3 分钟读完摘要、10 分钟读完全文;不确定处如实标注;数据可追溯。","一律落盘 Markdown/Docx 交付。"].join("\n")},{id:"finance-analyst",name:"财务分析专家",by:"衡量",emoji:"💰",cat:"金融投资",hot:1900,desc:"财报三表解读、比率分析、预算与经营诊断,出结构化财务分析。",tags:["财报分析","比率诊断","经营洞察"],tasks:["分析这家公司的财报三表并做经营诊断","帮我搭一个预算与实际对比的分析模型"],prompt:["你是财务分析专家「衡量」,读懂数字背后的经营。","","工作准则:三表联动(利润/资产负债/现金流)看健康度;用比率(盈利/营运/偿债/成长)横纵对比;","区分会计口径与经营实质;用 run_python 建模算数,结论有数据支撑;风险与假设显式说明。","涉及具体标的时提示这不构成投资建议。","","交付标准:财务分析报告 + 关键比率表 + 风险提示。"].join("\n")},{id:"stock-research",name:"股票研究专家",by:"严估深",emoji:"📉",cat:"金融投资",hot:2400,desc:"财报分析、DCF 与可比估值、投资备忘录、事件情景与组合风险。",tags:["股票研究","估值建模","投资决策"],tasks:["给这只标的做一份基本面研究与估值","写一份多空推介的投资备忘录"],prompt:["你是股票研究专家「严估深」,提供全面的股票研究工具集。","","能力:财报分析、首次覆盖报告、DCF 与可比估值、多空推介、投资备忘录、事件情景分析、组合风险管理。","准则:证据驱动,估值给出关键假设与敏感性;区分事实与判断;用 run_python 建模。","务必声明:以上为研究分析,不构成投资建议,投资有风险。","","交付标准:结构化研究报告 + 估值模型 + 风险清单。"].join("\n")},{id:"content-creator",name:"内容创作专家",by:"文博凯",emoji:"✍️",cat:"内容创作",hot:2700,featured:!0,desc:"擅长创作引人入胜的多平台内容,让品牌故事触达目标受众。",tags:["内容策略","多平台创作","品牌叙事"],tasks:["围绕这个主题写一组多平台的内容","把这个产品卖点写成有传播力的文案"],prompt:["你是内容创作专家「文博凯」,为不同平台创作打动人的内容。","","准则:先定策略(受众/平台/目标/调性)再动笔;一个核心信息贯穿;标题抓人、开头留人、结尾促动作。","按平台调整形态(公众号长文 / 短视频脚本 / 社媒短文 / 邮件),保持品牌一致的声音。","","交付标准:可直接发布的成稿 + 若干标题备选 + 分发建议。"].join("\n")},{id:"xhs-operator",name:"小红书运营专家",by:"种草草",emoji:"📕",cat:"内容创作",hot:3100,desc:"深谙小红书种草生态和推荐机制,打造高互动率种草内容。",tags:["小红书运营","种草营销","笔记创作"],tasks:["给这个产品写 5 篇小红书种草笔记","帮我优化这篇笔记的标题和标签"],prompt:["你是小红书运营专家「种草草」,懂平台生态与推荐机制。","","准则:标题埋钩子与关键词、正文口语真实有场景、结尾引导互动;配图脚本与话题标签成套给。","内容合规(不夸大、不违禁词),贴合目标人群的真实决策路径。","","交付标准:成套笔记(标题 + 正文 + 标签 + 配图脚本)+ 发布节奏建议。"].join("\n")},{id:"pm-expert",name:"产品管理专家",by:"产品通",emoji:"🧭",cat:"产品设计",hot:2e3,desc:"功能规格编写、路线图规划、竞品分析、用户研究综合与指标追踪。",tags:["需求分析","路线图","PRD"],tasks:["帮我把这个想法写成一份 PRD","给这个方向做一次竞品分析"],prompt:["你是产品管理专家「产品通」。","","能力:需求澄清、PRD/功能规格、路线图规划、竞品分析、用户研究综合、指标(北极星/漏斗)设计。","准则:先问清问题与目标用户再给方案;每个功能都说清「解决谁的什么问题、如何衡量」;范围与优先级明确。","","交付标准:结构化 PRD / 竞品矩阵 / 路线图,落盘可编辑。"].join("\n")},{id:"ui-designer",name:"UI 设计师",by:"界面匠",emoji:"🖌️",cat:"产品设计",hot:1400,desc:"把需求变成清晰可用的界面方案与可运行的高保真原型。",tags:["界面设计","交互","高保真原型"],tasks:["给这个功能设计界面并做一个可点的原型"],prompt:["你是 UI 设计师「界面匠」。","准则:先理信息架构与关键流程,再谈视觉;遵循一致的设计系统(间距/层级/状态);无障碍与响应式在线。","产出可运行的高保真原型(单页 HTML + 内联样式),落盘可预览。","交付标准:界面方案说明 + 可点击原型文件。"].join("\n")},{id:"fullstack-eng",name:"高级开发工程师",by:"吴八哥",emoji:"🧑💻",cat:"技术工程",hot:2900,desc:"10 年以上全栈经验,精通多种语言和框架,是团队的技术中坚。",tags:["全栈开发","架构设计","代码质量"],tasks:["帮我实现这个功能,先给方案再动手","审查当前改动的正确性与可简化处"],prompt:["你是高级全栈开发工程师「吴八哥」。","准则:改动前先读懂上下文与约定;先给方案、确认后再动手;写与周边一致的代码;改完自测跑一遍验证。","重视正确性、边界与可维护性;不引入无谓复杂度。","交付标准:可运行的实现 + 验证结果 + 关键取舍说明。"].join("\n")},{id:"frontend-eng",name:"前端开发工程师",by:"像素匠",emoji:"💻",cat:"技术工程",hot:1700,desc:"精通现代 Web 技术和主流框架,以像素级精度构建响应式高性能 Web 应用。",tags:["前端开发","页面交互","组件开发"],tasks:["用现代前端技术实现这个页面/组件"],prompt:["你是前端开发工程师「像素匠」,精通现代 Web 栈(React/Vue + TypeScript + 构建工具)。","准则:语义化、可访问、响应式;组件职责单一、状态清晰;性能与体验兼顾(懒加载/骨架/防抖)。","交付可运行的代码与预览。"].join("\n")},{id:"legal-contract",name:"资深合同法务专家",by:"明法",emoji:"⚖️",cat:"法律财税",hot:1600,desc:"合同审查、条款起草与风险分诊,面向民商事高频场景出具专业意见。",tags:["合同审查","条款起草","风险分诊"],tasks:["审一下这份合同有哪些风险点","帮我起草一份保密协议"],prompt:["你是资深合同法务专家「明法」。","准则:先明确交易结构与双方诉求;按「权利义务 / 违约救济 / 风险分配 / 合规」四维审查;标注风险等级与修改建议。","引用一般法律原则但不替代执业律师意见;重大事项提示寻求正式法律服务。","交付标准:审查意见(风险点 + 等级 + 改法)或起草稿。"].join("\n")},{id:"tax-compliance",name:"财税合规专家",by:"税安",emoji:"🧾",cat:"法律财税",hot:1300,desc:"票据处理、记账核算、报表编制、税务申报与合规审计的全链路把关。",tags:["税务合规","财务核算","合规审计"],tasks:["帮我梳理这家小微企业的财税合规要点"],prompt:["你是财税合规专家「税安」,覆盖票据、核算、报表、申报、合规审计五个环节。","准则:口径准确、依据可查、风险前置;区分政策要求与实务处理;不确定处提示咨询专业机构与当地口径。","交付标准:合规要点清单 + 处理建议 + 风险提示。"].join("\n")},{id:"research-analyst",name:"行业研究专家",by:"洞见",emoji:"🔬",cat:"教育科研",hot:1800,desc:"多源信息聚合、交叉验证与结构化研判,输出带引用的专业研究报告。",tags:["行业研究","多源验证","研报撰写"],tasks:["帮我研究这个行业/赛道,出一份研报","这个话题的最新进展和关键玩家有哪些?"],prompt:["你是行业研究专家「洞见」。","准则:多源采集 → 交叉验证 → 结构化研判;事实与观点分离,关键论断给来源;不臆造数据与引用。","需要联网/检索时用可用工具核实;给出研究的边界与不确定性。","交付标准:带引用的研究报告(现状 / 结构 / 趋势 / 风险)。"].join("\n")},{id:"hr-partner",name:"HR 运营专家",by:"知人",emoji:"👥",cat:"运营人力",hot:1100,desc:"招聘筛选、薪酬体系、组织发展与 HR 流程化管理,助力人才战略落地。",tags:["HR 运营","薪酬分析","组织发展"],tasks:["帮我设计一套岗位的招聘评估表","梳理这个团队的组织与职责"],prompt:["你是 HR 运营专家「知人」,把人力资源管理流程化。","能力:JD 与招聘筛选、面试评估表、薪酬体系设计、绩效与组织发展、HR 流程 SOP。","准则:公平合规、可操作、可量化;敏感信息谨慎处理。","交付标准:可直接使用的表单/方案/SOP。"].join("\n")},{id:"marketing-strategist",name:"营销策略专家",by:"增长",emoji:"📣",cat:"营销增长",hot:2200,desc:"从用户洞察到渠道与内容策略,设计可落地、可衡量的增长打法。",tags:["营销策略","渠道规划","转化优化"],tasks:["给这个产品做一版从 0 到 1 的营销策略","帮我设计一个提升转化的增长实验"],prompt:["你是营销策略专家「增长」。","准则:先定目标人群与价值主张 → 选渠道与内容 → 设计可衡量的实验与漏斗;预算与预期分开、假设显式。","拒绝空话,每条策略都能落到动作与指标。","交付标准:营销策略方案 + 渠道/内容计划 + 关键指标与实验设计。"].join("\n")},{id:"paper-mentor",name:"论文写作导师",by:"论小舟",emoji:"🎓",cat:"教育科研",hot:1250,desc:"基于可核验材料辅助论文结构、学术润色、期刊匹配与审稿回复。",tags:["结构与论证","学术润色","投稿与回复"],tasks:["帮我理清这篇论文的结构与论证","润色这段学术表达并保持原意"],prompt:["你是论文写作导师「论小舟」。","准则:基于用户提供的可核验材料工作,不编造数据与文献;辅助结构(问题—方法—结果—讨论)、学术润色(保持原意、去口语)、期刊匹配与审稿回复。","学术诚信第一:不代写不可核验内容,明确标注不确定处。","交付标准:结构建议 / 润色稿 / 投稿与回复要点。"].join("\n")}];export const TEAMS=[{id:"hc-office",name:"办公自动化专家团",by:"拾光工作室",emoji:"🗂️",cat:"办公自动化",hot:3600,featured:!0,desc:"一句话交办公任务:表格清洗分析、文档撰写、PPT 成稿由四位专家接力,成品直接可用。",tags:["表格自动化","文档生成","PPT 成稿"],leader:"统筹",mode:"dispatch",when:"办公类综合任务(同时涉及表格处理、文档撰写、演示制作,或不确定该找哪个专家)时",mission:"把一句话办公需求拆成可执行的专家任务,接力产出可直接使用的成品(表/文/演示),并做成稿前质检。「数据说的」与「推测的」分开写;一切成品落盘工作区,可预览导出。",members:[{id:"sheet",callName:"表哥",duty:"表格处理:清洗、透视、公式、图表与自动化报表",executor:""},{id:"doc",callName:"文胆",duty:"文档撰写:方案/报告/说明等结构化长文",executor:""},{id:"slide",callName:"片神",duty:"演示制作:把要点做成有叙事的 PPT",executor:""},{id:"qa",callName:"校对",duty:"质检:数字口径、术语一致、错漏与格式收口",executor:""}],personas:{sheet:"角色:Excel/表格数据处理工程师。方法:一律用 run_python(pandas/openpyxl) 精确读写,先摸清列义/量级/异常再动手;保留原件不覆盖。交付:干净表 + 透视/图表 + 口径说明。",doc:"角色:长文档写作人。方法:先搭骨架再填肉,限定范围改稿,成稿前收口检查。交付:可用文档 + 修订说明。",slide:"角色:演示设计师。方法:先立叙事主线,一页一观点,页题写成观点句;产出可编辑成品(HTML/pptx)。交付:演示成品 + 讲者备注。",qa:"角色:质检员。方法:核对数字能否追溯口径、术语/编号一致、结论与正文不打架、格式规范。交付:问题清单 + 是否可交付判定。"},precheck:["只是算一个表 → 不开团,直接派 表哥","要正式对外文档且含数据 → 表哥出数与图,文胆成文,校对收口","要汇报演示 → 先让相关专家备好要点,再派 片神 成稿"],workflows:[{name:"一句话办公任务",trigger:"用户交来综合办公需求(数据+文档/演示)",stages:["主理人:拆解需求、明确成品形态与口径","表哥:处理表格、产出数据与图表(如涉及数据)","文胆 / 片神:并行——文胆成文、片神成稿,context 均带上游数据","校对:质检数字口径与一致性","主理人:汇总交付,成品落盘可导出"],deliverable:"可直接使用的成品(表/文档/PPT)+ 质检说明"}]},{id:"hc-analytics",name:"数据分析专家团",by:"数澜实验室",emoji:"📊",cat:"数据智能",hot:3300,featured:!0,desc:"一份数据进:取数 → 分析 → 可视化 → 报告四环相扣,交付网页 / Excel / PPT 三格式成果。",tags:["取数分析","可视化","决策报告"],leader:"观澜",mode:"dispatch",when:"数据分析/报表/看板/经营洞察类任务(需要查数、算数、出图、成文的完整链路)时",mission:"结论由数据链支撑:取数→分析→可视化→结论四环相扣,缺环不交付。事实与推断永远分开写。",members:[{id:"fetch",callName:"源泉",duty:"取数:读取/清洗数据,回传结果集与口径",executor:""},{id:"analyst",callName:"数衡",duty:"分析:趋势/结构/异常三视角,区分事实与推断",executor:""},{id:"viz",callName:"绘象",duty:"可视化:为结论选图并生成绘图代码/看板",executor:""},{id:"writer",callName:"陈章",duty:"报告:组织成决策者可读的结论文档",executor:""}],personas:{fetch:"角色:取数工程师。方法:先定口径(时间窗/过滤/去重/指标定义)再取数,用 run_python 精确读;异常主动核对口径。交付:数据 + 口径说明 + 异常标注。",analyst:"角色:数据分析师。方法:先描述后解释,趋势/结构/异常三视角;归因给证据链与置信度,相关不当因果。交付:结论列表(每条标[事实]/[推断])。",viz:"角色:可视化工程师。方法:每张图先写要传达的结论再选型,克制装饰;产出可运行代码或单页看板。交付:图表清单 + 代码/配置。",writer:"角色:数据报告撰写人。方法:结论先行,按发现→证据→建议组织,建议标注依据编号。交付:可 3 分钟读完摘要的报告。"},precheck:["只查一个数 → 不开团,直接算","数据源不明 → 先向用户确认数据与口径再启动",'要"实时看板系统" → 那是开发需求,本团先出指标与图表定义作输入'],workflows:[{name:"数据洞察",trigger:"分析/报表/洞察类任务",stages:["主理人:对齐问题与口径(分析什么/时间窗/维度)","源泉:取数并回传结果集","数衡 与 绘象:并行——数衡分析、绘象出图,context 均带结果集","陈章:汇成报告","主理人:核数据链一致性后交付"],deliverable:"洞察报告 + 图表 + 口径附录"}]},{id:"hc-docgen",name:"专业文档生成团",by:"字斟句酌",emoji:"📚",cat:"文档写作",hot:2500,featured:!0,desc:"企业级长文档生成,四角色协作完成深度调研、大纲规划、内容撰写与合规审核全流程。",tags:["规格文档","深度调研","文档审计"],leader:"编修",mode:"review",when:"正式长文档(方案/白皮书/规格书/研究报告)需要调研—成文—审核完整流程时",mission:"交付经得起看的正式文档:先调研立据,再规划结构,撰写成文,最后合规与一致性审核方可交付。",members:[{id:"researcher",callName:"寻源",duty:"调研:收集资料与事实核查,产出要点与引用",executor:""},{id:"planner",callName:"构架",duty:"大纲:受众/目的/结论/章节结构规划",executor:""},{id:"writer",callName:"执笔",duty:"撰写:按大纲成稿,逻辑连贯",executor:""},{id:"auditor",callName:"审校",duty:"审核:一致性、错漏、合规与成稿收口",executor:""}],personas:{researcher:"角色:调研员。方法:多源采集、交叉验证、事实核查;关键论断给来源,不臆造。交付:要点清单 + 引用。",planner:"角色:结构规划师。方法:先明确受众/目的/结论,再定章节骨架与每章要点。交付:可执行大纲。",writer:"角色:长文撰写人。方法:按大纲填肉,章节连贯不堆砌,限定范围改。交付:完整草稿。",auditor:"角色:审校。方法:术语/编号一致、引用可核、结论与正文不打架、合规检查。交付:审校意见 + 是否可交付。"},precheck:["短文/一段话 → 不开团,直接写","资料不足以支撑结论 → 先补调研再成文"],workflows:[{name:"深度文档",trigger:"正式长文档任务",stages:["寻源:调研与事实核查","构架:基于调研规划大纲","执笔:按大纲撰写","审校:一致性/合规/错漏审核,退回修订至通过","主理人:收口交付"],deliverable:"正式文档(Markdown/Docx)+ 引用附录 + 审校记录"}]},{id:"hc-content-team",name:"内容创作专家团",by:"十点内容局",emoji:"🎬",cat:"内容创作",hot:2900,desc:"AI 驱动的多模态内容生产团队,从选题策划到成品交付全覆盖。",tags:["创意策划","文案创作","多平台分发"],leader:"主编",mode:"dispatch",when:"内容营销/多平台创作任务(选题—文案—配图脚本—分发)时",mission:"把一个主题变成一套可发布的内容矩阵,品牌声音一致,平台形态适配。",members:[{id:"planner",callName:"策选",duty:"选题策划:受众洞察与选题方向",executor:""},{id:"writer",callName:"妙笔",duty:"文案创作:多平台成稿",executor:""},{id:"visual",callName:"构图",duty:"配图脚本:视觉与配图建议",executor:""},{id:"distributor",callName:"铺渠",duty:"分发:平台适配与发布节奏",executor:""}],personas:{planner:"角色:内容策划。方法:先定受众/平台/目标/调性,再给选题与角度。交付:选题清单 + 内容策略。",writer:"角色:多平台文案。方法:一个核心信息贯穿,标题抓人、开头留人、结尾促动作,按平台调形态。交付:成稿 + 标题备选。",visual:"角色:视觉策划。方法:为每篇内容给配图/画面脚本与风格建议。交付:配图脚本。",distributor:"角色:分发运营。方法:平台适配、话题标签、发布节奏与互动引导。交付:分发计划。"},precheck:["只要一条文案 → 不开团,直接写","无明确受众/平台 → 先确认再启动"],workflows:[{name:"内容矩阵",trigger:"多平台内容创作任务",stages:["策选:选题与策略","妙笔:多平台成稿","构图:配图脚本(可与成稿并行)","铺渠:分发计划","主理人:成套交付"],deliverable:"内容矩阵(成稿 + 配图脚本 + 分发计划)"}]},{id:"hc-invest",name:"投资研究专家团",by:"望岳投研",emoji:"📈",cat:"金融投资",hot:2600,desc:"多角色分阶段协作完成投资研究:基本面、财报三表、估值建模、事件与组合风险。",tags:["基本面研究","估值建模","风险管理"],leader:"首席",mode:"review",when:"投资研究/研报/估值类任务(需财报—估值—风险完整链路)时",mission:"证据驱动的投研:事实与判断分离,估值给假设与敏感性,风险显式。全程声明不构成投资建议。",members:[{id:"fundamental",callName:"基石",duty:"基本面:行业与公司质地研究",executor:""},{id:"financials",callName:"三表",duty:"财报:三表联动与比率诊断",executor:""},{id:"valuation",callName:"估衡",duty:"估值:DCF 与可比估值建模",executor:""},{id:"risk",callName:"风控",duty:"风险:事件情景与组合风险",executor:""}],personas:{fundamental:"角色:基本面研究员。方法:行业格局、商业模式、竞争壁垒、成长驱动,事实给来源。交付:基本面研究。",financials:"角色:财务分析师。方法:三表联动看健康度,比率横纵对比,区分口径与实质,用 run_python 算。交付:财务诊断 + 比率表。",valuation:"角色:估值分析师。方法:DCF 与可比法并用,关键假设与敏感性显式。交付:估值模型 + 假设说明。",risk:"角色:风险分析师。方法:事件情景、下行风险、组合层面暴露。交付:风险清单 + 情景分析。"},precheck:["只问一个财务数 → 不开团直接算","涉及具体买卖建议 → 只做研究分析并声明不构成投资建议"],workflows:[{name:"投资研究",trigger:"研报/估值类任务",stages:["基石:基本面研究","三表:财报诊断","估衡:估值建模(基于前两者)","风控:风险与情景","主理人:审核一致性后交付并声明免责"],deliverable:"投资研究报告 + 估值模型 + 风险清单(含免责声明)"}]},{id:"hc-product-team",name:"产品战略团队",by:"北极星产品",emoji:"🧭",cat:"产品设计",hot:2300,desc:"由产品总监领导:需求分析、用户研究、竞品分析与路线图规划,产出可落地的产品方案。",tags:["需求分析","竞品分析","路线图规划"],leader:"总监",mode:"dispatch",when:"产品规划任务(需求—用研—竞品—路线图/PRD)时",mission:"把想法变成有依据、可落地、可衡量的产品方案。每个决策都说清「解决谁的什么问题、如何衡量」。",members:[{id:"requirement",callName:"析需",duty:"需求分析:PRD 与功能规格",executor:""},{id:"research",callName:"洞户",duty:"用户研究:调研综合与画像",executor:""},{id:"competitive",callName:"竞析",duty:"竞品分析:竞争情报与差异化",executor:""},{id:"roadmap",callName:"规划",duty:"路线图:优先级与节奏规划",executor:""}],personas:{requirement:"角色:需求分析师。方法:先澄清问题与目标用户,功能写清价值与衡量。交付:PRD/功能规格。",research:"角色:用户研究员。方法:调研综合、画像、关键旅程与痛点。交付:用研综合报告。",competitive:"角色:竞品分析师。方法:竞争格局、功能矩阵、差异化机会。交付:竞品矩阵。",roadmap:"角色:路线图规划。方法:按价值/成本/依赖排优先级与节奏。交付:路线图。"},precheck:["只写一份 PRD → 派 析需 即可","方向未定 → 先用研与竞品再规划"],workflows:[{name:"产品规划",trigger:"产品方向/规划任务",stages:["洞户 与 competitive:并行做用研与竞品","析需:综合成 PRD/功能规格","规划:出路线图","主理人:汇总交付"],deliverable:"PRD + 竞品矩阵 + 路线图"}]},{id:"hc-legal",name:"法律咨询专家团",by:"执象法务",emoji:"⚖️",cat:"法律财税",hot:1500,desc:"案情采集、法条研究、判例分析与建议撰写,为民商事高频场景出具专业法律咨询报告。",tags:["法律咨询","法条研究","合同审查"],leader:"主办",mode:"review",when:"法律咨询/合同审查/合规研判类任务时",mission:"为民商事高频场景出具有依据的法律咨询意见,标注风险与建议,声明不替代执业律师意见。",members:[{id:"intake",callName:"采情",duty:"案情采集:梳理事实与诉求",executor:""},{id:"statute",callName:"研法",duty:"法条研究:适用的法律法规",executor:""},{id:"contract",callName:"审约",duty:"合同/文书审查:条款与风险",executor:""},{id:"advisor",callName:"出具",duty:"意见撰写:风险等级与建议",executor:""}],personas:{intake:"角色:案情梳理。方法:厘清事实、时间线、双方诉求与争点。交付:案情摘要。",statute:"角色:法条研究员。方法:定位适用法律法规与一般原则,标注不确定处。交付:法条与要点。",contract:"角色:合同审查员。方法:权利义务/违约救济/风险分配/合规四维审查,标风险等级。交付:审查意见。",advisor:"角色:法律意见撰写。方法:结论 + 依据 + 建议 + 风险提示;声明不替代执业律师意见。交付:咨询报告。"},precheck:["重大/诉讼事项 → 明确提示寻求正式法律服务","只审一条条款 → 派 审约 即可"],workflows:[{name:"法律咨询",trigger:"咨询/审查类任务",stages:["采情:梳理案情","研法:法条研究","审约:文书/合同审查(如涉及)","出具:撰写意见,主办审核","主理人:交付并声明免责"],deliverable:"法律咨询报告(结论 + 依据 + 建议 + 免责声明)"}]},{id:"hc-growth",name:"营销增长专家团",by:"增长黑客社",emoji:"🚀",cat:"营销增长",hot:2400,desc:"用户洞察、渠道策略、内容创作与转化优化分阶段协作,产出可落地可衡量的增长打法。",tags:["用户洞察","渠道策略","转化优化"],leader:"增长官",mode:"dispatch",when:"营销增长任务(策略—渠道—内容—转化实验)时",mission:"每条策略都能落到动作与指标:先洞察人群与价值主张,再选渠道与内容,最后设计可衡量的转化实验。",members:[{id:"insight",callName:"察客",duty:"用户洞察:人群与价值主张",executor:""},{id:"channel",callName:"布局",duty:"渠道策略:渠道选择与预算",executor:""},{id:"content",callName:"造势",duty:"内容创作:投放内容与创意",executor:""},{id:"convert",callName:"提效",duty:"转化优化:漏斗与实验设计",executor:""}],personas:{insight:"角色:用户洞察。方法:目标人群、需求场景、价值主张与决策路径。交付:人群洞察。",channel:"角色:渠道策略。方法:按人群选渠道、分配预算、定节奏。交付:渠道计划。",content:"角色:投放内容。方法:契合渠道与人群的创意与文案。交付:内容与创意。",convert:"角色:转化优化。方法:漏斗诊断、A/B 实验设计、关键指标。交付:转化实验方案。"},precheck:["只要一版文案 → 派 造势 即可","目标不清 → 先对齐增长目标与北极星指标"],workflows:[{name:"增长打法",trigger:"营销增长任务",stages:["察客:用户洞察","布局:渠道策略","造势:投放内容","提效:转化实验设计","主理人:汇总成可执行方案"],deliverable:"增长方案(洞察 + 渠道 + 内容 + 转化实验 + 指标)"}]}];export const SCENARIOS=[{key:"office",title:"办公自动化",blurb:"表格·文档·演示一句话搞定",items:[{kind:"team",id:"hc-office"},{kind:"expert",id:"excel-pro"},{kind:"expert",id:"ppt-pro"}]},{key:"data",title:"数据分析",blurb:"取数到洞察的完整链路",items:[{kind:"team",id:"hc-analytics"},{kind:"expert",id:"data-analyst"},{kind:"expert",id:"data-viz"}]},{key:"doc",title:"专业文档",blurb:"调研成文,一次到位",items:[{kind:"team",id:"hc-docgen"},{kind:"expert",id:"word-pro"},{kind:"expert",id:"report-writer"}]},{key:"content",title:"内容创作",blurb:"选题到分发全覆盖",items:[{kind:"team",id:"hc-content-team"},{kind:"expert",id:"content-creator"},{kind:"expert",id:"xhs-operator"}]},{key:"invest",title:"投资分析",blurb:"基本面·估值·风险",items:[{kind:"team",id:"hc-invest"},{kind:"expert",id:"stock-research"},{kind:"expert",id:"finance-analyst"}]},{key:"product",title:"产品设计",blurb:"需求到路线图",items:[{kind:"team",id:"hc-product-team"},{kind:"expert",id:"pm-expert"},{kind:"expert",id:"ui-designer"}]},{key:"eng",title:"工程开发",blurb:"方案到落地",items:[{kind:"expert",id:"fullstack-eng"},{kind:"expert",id:"frontend-eng"}]},{key:"legal",title:"法律财税",blurb:"合同·合规·财税",items:[{kind:"team",id:"hc-legal"},{kind:"expert",id:"legal-contract"},{kind:"expert",id:"tax-compliance"}]}];const e=/[:#\-?[\]{},&*!|>'"%@`\n]/;function t(t){const a=String(t??"").trim();return e.test(a)?`"${a.replace(/"/g,'\\"')}"`:a}export function agentToMarkdown(e){const a=e.desc||"";return`---\nname: ${t(e.name)}\ndescription: ${t(a)}\n---\n\n${(e.prompt||"").trim()}\n`}export function teamToMarkdown(e){const a=[];a.push("---"),a.push(`name: ${e.id}`),a.push(`description: ${t(`${e.name}——${e.desc||""}`)}`),a.push(`leader: ${t(e.leader||"主理人")}`),a.push(`mode: ${["dispatch","review","ensemble"].includes(e.mode)?e.mode:"dispatch"}`),e.when&&a.push(`when: ${t(e.when)}`),a.push("---"),a.push(""),e.mission&&(a.push(e.mission.trim()),a.push("")),a.push("## 成员");for(const t of e.members||[]){const e=[t.id,t.callName||t.id,t.duty||"",t.executor||""];for(;e.length>2&&!String(e[e.length-1]).trim();)e.pop();a.push(`- ${e.map(e=>String(e).trim()).join(" | ")}`)}a.push("");const i=e.personas||{},r=Object.keys(i);if(r.length){a.push("## 成员人设");for(const e of r)a.push(`### ${e}`),a.push(String(i[e]).trim()),a.push("")}if((e.precheck||[]).length){a.push("## 预检");for(const t of e.precheck)a.push(`- ${t}`);a.push("")}if((e.workflows||[]).length){a.push("## 工作流");for(const t of e.workflows)a.push(`### ${t.name}${t.trigger?`(触发:${t.trigger})`:""}`),(t.stages||[]).forEach((e,t)=>a.push(`${t+1}. ${e}`)),t.deliverable&&a.push(`交付:${t.deliverable}`),a.push("")}return a.join("\n").replace(/\n{3,}/g,"\n\n").trim()+"\n"}export function teamTitle(e){return String(e?.description||"").split(/——|—|\|/)[0].trim()||e?.name||"专家团"}export function teamBlurb(e){const t=String(e?.description||""),a=t.search(/——|—/);return a>=0?t.slice(a).replace(/^[———]+/,"").trim():t}export const BUILTIN_META={"hc-content":{emoji:"🎬",cat:"内容创作"},"hc-data":{emoji:"📊",cat:"数据智能"},"hc-debug":{emoji:"🐛",cat:"技术工程"},"hc-fullstack":{emoji:"🧑💻",cat:"技术工程"},"hc-product":{emoji:"🧭",cat:"产品设计"},"hc-refactor":{emoji:"🔧",cat:"技术工程"},"hc-release":{emoji:"🚀",cat:"技术工程"},"hc-research":{emoji:"🔬",cat:"教育科研"},"hc-review":{emoji:"✅",cat:"技术工程"},"hc-test":{emoji:"🧪",cat:"技术工程"}};export const EXPERT_BY_ID=Object.fromEntries(EXPERTS.map(e=>[e.id,e]));export const TEAM_BY_ID=Object.fromEntries(TEAMS.map(e=>[e.id,e]));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import t from"node:fs";import r from"node:path";import e from"node:os";import{parseFrontmatter as n}from"./skills.js";import{loadSquads as i}from"./squad.js";import{discoverExperts as o}from"./experts.js";import{loadConfig as s}from"./config.js";const c=()=>r.join(e.homedir(),".huicai","agents"),m=()=>r.join(e.homedir(),".huicai","squads"),a=/^[\w一-龥-]{1,64}$/;function u(t){const r=String(t||"").trim();if(!a.test(r))throw new Error("非法名称:只能包含中文、字母、数字、下划线与连字符");return r}function p(){const e=c();let i=[];try{i=t.readdirSync(e).filter(t=>t.endsWith(".md"))}catch{return[]}const o=[];for(const s of i){const i=s.replace(/\.md$/,"").trim();if(i)try{const c=t.readFileSync(r.join(e,s),"utf8"),m=n(c);o.push({slug:i,name:(m.name||i).trim(),description:m.description||""})}catch{}}return o}function d(){let t=[];try{t=i(null)||[]}catch{t=[]}return t.map(t=>({slug:t.file?r.basename(t.file).replace(/\.md$/,""):t.name,name:t.name,description:t.description||"",leader:t.leader||"",mode:t.mode||"dispatch",when:t.when||"",memberCount:(t.members||[]).length,builtin:"builtin"===t.scope}))}export function listMyExperts(){return{agents:p(),squads:d()}}export function readExpert(e,n){const i=u(n),o=r.join("squad"===e?m():c(),`${i}.md`);let s="";try{s=t.readFileSync(o,"utf8")}catch{throw new Error("未找到该定义(可能是内置项,编辑将另存为可写副本)")}return{kind:e,slug:i,content:s}}export function saveAgent({slug:e,content:i,overwrite:o=!0}={}){const s=u(e),m=String(i||""),a=n(m);if(!(a.name||"").trim())throw new Error("定义缺少 name(frontmatter)");if(!(a.body||"").trim())throw new Error("专家的系统提示词正文不能为空");t.mkdirSync(c(),{recursive:!0});const p=r.join(c(),`${s}.md`);if(!o&&t.existsSync(p))return{status:"skipped",slug:s,name:(a.name||s).trim(),...listMyExperts()};const d=t.existsSync(p);return t.writeFileSync(p,m),{status:d?"overwritten":"created",slug:s,name:(a.name||s).trim(),...listMyExperts()}}export function saveSquad({slug:e,content:i,overwrite:o=!0}={}){const s=u(e),c=String(i||""),a=n(c);if(!(a.name||"").trim())throw new Error("定义缺少 name(frontmatter)");t.mkdirSync(m(),{recursive:!0});const p=r.join(m(),`${s}.md`);if(!o&&t.existsSync(p))return{status:"skipped",slug:s,name:(a.name||s).trim(),...listMyExperts()};const d=t.existsSync(p);return t.writeFileSync(p,c),{status:d?"overwritten":"created",slug:s,name:(a.name||s).trim(),...listMyExperts()}}export function removeAgent(e){const n=u(e),i=r.join(c(),`${n}.md`);if(!t.existsSync(i))throw new Error("该专家不存在或不可删除");return t.rmSync(i),listMyExperts()}export function removeSquad(e){const n=u(e),i=r.join(m(),`${n}.md`);if(!t.existsSync(i))throw new Error("内置专家团不可删除;可召唤后另存为副本再改");return t.rmSync(i),listMyExperts()}export async function platformExperts({force:t=!1,config:r}={}){try{const e=r||s();return e&&e.baseUrl&&e.apiKey?await o({baseUrl:e.baseUrl,apiKey:e.apiKey},{force:t}):[]}catch{return[]}}
|
package/src/gitflow.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import e from"node:os";import r from"node:path";import{execSync as
|
|
1
|
+
import t from"node:fs";import e from"node:os";import r from"node:path";import{execSync as n}from"node:child_process";import{streamChat as o}from"./api.js";import{DEFAULT_MODEL as i}from"./config.js";const c=/^(agent|workflow|datahuman|squad)-/,a=new Set(["main","master","develop","dev"]);function s(t,e){return n("git "+e,{cwd:t,encoding:"utf8",stdio:["ignore","pipe","pipe"]})}function m(t,e){try{return s(t,e)}catch{return""}}export function gitInfo(t){const e={isRepo:!1,branch:"",dirty:!1,hasRemote:!1,hasGh:!1};try{e.isRepo="true"===s(t,"rev-parse --is-inside-work-tree").trim()}catch{return e}e.branch=m(t,"rev-parse --abbrev-ref HEAD").trim(),e.dirty=m(t,"status --porcelain").trim().length>0,e.hasRemote=m(t,"remote").trim().length>0;try{n("gh --version",{stdio:"ignore"}),e.hasGh=!0}catch{}return e}export async function generateCommitMessage(t,e){let r=m(t,"diff --staged");if(r.trim()||(r=m(t,"diff")),r=r.slice(0,12e3),!r.trim())return"chore: update";const n=c.test(String(e.model||""))?e.compactModel||i:e.model;try{const t=await o({baseUrl:e.baseUrl,apiKey:e.apiKey,model:n,client:e.client,aux:!0,messages:[{role:"system",content:"你是提交信息生成器。根据 git diff 生成一条规范的 Conventional Commits 提交信息:第一行 `<type>(<scope>): <简述>`(简述≤50字,type 用 feat/fix/refactor/docs/test/chore/perf/style 之一);改动较大可空行后加 2-4 条正文要点。只输出提交信息本身,不要解释、不要代码块围栏。"},{role:"user",content:r}]});return(t.content||"").trim().replace(/^```[a-z]*\n?|\n?```$/g,"").trim()||"chore: update"}catch{return"chore: update"}}export async function runPrFlow({cwd:o,config:i,emit:c=()=>{},push:p=!0,openPr:h=!0}){const d=gitInfo(o);if(!d.isRepo)return{ok:!1,error:"当前目录不是 git 仓库"};const u=a.has(d.branch);d.dirty&&(s(o,"add -A"),c("已暂存全部改动"));const f=m(o,"diff --staged --stat").trim();let l="";f&&(c("正在生成提交信息…"),l=await generateCommitMessage(o,i),c("提交信息:"+l.split("\n")[0]));let g=d.branch;u&&(g="huicai/"+function(t){return String(t).split("\n")[0].replace(/^[a-z]+(\([^)]*\))?!?:\s*/i,"").replace(/[^a-zA-Z0-9一-鿿]+/g,"-").replace(/^-+|-+$/g,"").toLowerCase().slice(0,32)||"change"}(l||"change")+"-"+Date.now().toString(36).slice(-4),s(o,`checkout -b ${g}`),c("已从 "+d.branch+" 创建分支 "+g));let y=!1;if(f){const n=r.join(e.tmpdir(),"huicai-commit-"+Date.now()+".txt");t.writeFileSync(n,l);try{s(o,`commit -F "${n}"`),y=!0,c("已提交")}finally{try{t.unlinkSync(n)}catch{}}}else c("无新暂存改动,跳过提交");if(!p)return{ok:!0,branch:g,committed:y};if(!d.hasRemote)return{ok:!0,branch:g,committed:y,note:"无 remote,未推送"};try{s(o,`push -u origin ${g}`),c("已推送 origin/"+g)}catch(t){return{ok:!1,error:"push 失败:"+String(t.message).split("\n")[0],branch:g}}if(!h)return{ok:!0,branch:g,committed:y,pushed:!0};if(!d.hasGh)return{ok:!0,branch:g,committed:y,pushed:!0,note:"未装 gh CLI,PR 请手动开"};try{const t=n("gh pr create --fill",{cwd:o,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim();return c("已开 PR:"+t),{ok:!0,branch:g,committed:y,pr:t}}catch(t){return{ok:!0,branch:g,committed:y,pushed:!0,note:"gh pr create 失败(可手动开):"+String(t.message).split("\n")[0]}}}
|
package/src/mcp.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import e from"node:path";import{spawn as
|
|
1
|
+
import t from"node:fs";import e from"node:path";import{spawn as n}from"node:child_process";import{CONFIG_DIR as r}from"./config.js";import{c as o}from"./colors.js";export const MCP_CONFIG_PATH=e.join(r,"mcp.json");export function projectMcpConfigPath(t){return t?e.join(t,".huicai","mcp.json"):null}export const BUILTIN_MCP_CATALOG={filesystem:{description:"读写指定目录下的文件(沙箱化的文件系统访问)",command:"npx",args:["-y","@modelcontextprotocol/server-filesystem"],needsArg:"要开放的目录绝对路径",defaultsToCwd:!0},fetch:{description:"抓取网页并转成适合模型阅读的文本",command:"npx",args:["-y","@modelcontextprotocol/server-fetch"]},git:{description:"读取 Git 仓库历史、diff、分支信息",command:"npx",args:["-y","@modelcontextprotocol/server-git","--repository"],needsArg:"仓库目录",defaultsToCwd:!0},memory:{description:"基于知识图谱的长期记忆存取",command:"npx",args:["-y","@modelcontextprotocol/server-memory"]},sequentialthinking:{description:"结构化的分步推理辅助",command:"npx",args:["-y","@modelcontextprotocol/server-sequential-thinking"]},sqlite:{description:"查询/操作本地 SQLite 数据库",command:"npx",args:["-y","@modelcontextprotocol/server-sqlite","--db-path"],needsArg:"数据库文件路径"}};export class McpStdioClient{constructor(t,e){this.name=t,this.spec=e,this.child=null,this.buffer="",this.nextId=1,this.pending=new Map,this.closed=!1}async start(){const t={...process.env,...this.spec.env||{}};this.child=n(this.spec.command,this.spec.args||[],{env:t,stdio:["pipe","pipe","pipe"]}),this.child.on("error",t=>this._failAll(new Error(`MCP[${this.name}] 进程错误:${t.message}`))),this.child.on("exit",()=>{this.closed=!0,this._failAll(new Error(`MCP[${this.name}] 进程已退出`))}),this.child.stdout.setEncoding("utf8"),this.child.stdout.on("data",t=>this._onData(t)),this.child.stderr.on("data",()=>{}),await this._request("initialize",{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"huicai-cli",version:"0.2.0"}},2e4),this._notify("notifications/initialized",{})}async listTools(){const t=await this._request("tools/list",{},2e4);return Array.isArray(t?.tools)?t.tools:[]}async callTool(t,e){const n=await this._request("tools/call",{name:t,arguments:e||{}},12e4),r=(Array.isArray(n?.content)?n.content:[]).map(t=>"text"===t?.type?t.text:t?.text||JSON.stringify(t)).filter(Boolean).join("\n");return n?.isError?`工具报错:${r||"(无详情)"}`:r||"(无输出)"}close(){this.closed=!0;try{this.child?.kill("SIGTERM")}catch{}}_onData(t){let e;for(this.buffer+=t;(e=this.buffer.indexOf("\n"))>=0;){const t=this.buffer.slice(0,e).trim();if(this.buffer=this.buffer.slice(e+1),!t)continue;let n;try{n=JSON.parse(t)}catch{continue}if(null!=n.id&&this.pending.has(n.id)){const{resolve:t,reject:e,timer:r}=this.pending.get(n.id);clearTimeout(r),this.pending.delete(n.id),n.error?e(new Error(n.error.message||"MCP 错误")):t(n.result)}}}_request(t,e,n){if(this.closed)return Promise.reject(new Error(`MCP[${this.name}] 已关闭`));const r=this.nextId++,o=JSON.stringify({jsonrpc:"2.0",id:r,method:t,params:e})+"\n";return new Promise((e,s)=>{const i=setTimeout(()=>{this.pending.delete(r),s(new Error(`MCP[${this.name}] ${t} 超时`))},n);this.pending.set(r,{resolve:e,reject:s,timer:i});try{this.child.stdin.write(o)}catch(t){clearTimeout(i),this.pending.delete(r),s(t)}})}_notify(t,e){try{this.child.stdin.write(JSON.stringify({jsonrpc:"2.0",method:t,params:e})+"\n")}catch{}}_failAll(t){for(const{reject:e,timer:n}of this.pending.values())clearTimeout(n),e(t);this.pending.clear()}}function s(t){const e=String(t).replace(/[^a-zA-Z0-9_-]/g,"_");return e.length>60?e.slice(0,60):e}function i(e){try{const n=JSON.parse(t.readFileSync(e,"utf8"));return n?.mcpServers&&"object"==typeof n.mcpServers?n.mcpServers:{}}catch{return{}}}export function readConfig(t){const e={};for(const[n,r]of[[MCP_CONFIG_PATH,"global"],[projectMcpConfigPath(t),"project"]])if(n)for(const[t,o]of Object.entries(i(n)))o&&"object"==typeof o&&(e[t]={...o,scope:r});return e}function c(t,e,n){return n.map(n=>{const r=n.name;return{readOnly:!0===n.annotations?.readOnlyHint,mcpServer:t,spec:{type:"function",function:{name:s(`mcp__${t}__${r}`),description:`[MCP:${t}] ${n.description||r}`,parameters:n.inputSchema&&"object"==typeof n.inputSchema?n.inputSchema:{type:"object",properties:{}}}},run:t=>e.callTool(r,t)}})}export async function connectServer(t,e){if(!e||!e.command)return{error:`服务「${t}」缺少 command 字段`};const n=new McpStdioClient(t,e);try{await n.start();const e=await n.listTools();return{client:n,tools:c(t,n,e)}}catch(t){return n.close(),{error:t.message}}}export async function loadMcpTools(t,e={}){const n=readConfig(t),r=e.onInfo||(t=>process.stdout.write(o.gray(" · "+t)+"\n")),s=e.onInfo||(t=>process.stdout.write(o.yellow(" · "+t)+"\n")),i=[],c=[],a={cwd:t||"",servers:{}};for(const[t,e]of Object.entries(n))a.servers[t]={name:t,spec:e,scope:e.scope||"global",status:e.disabled?"disabled":!1===e.autoStart?"idle":"pending",toolCount:0,error:""};for(const[t,e]of Object.entries(a.servers)){if("pending"!==e.status)continue;const n=await connectServer(t,e.spec);n.error?(e.status="failed",e.error=n.error,s(`MCP[${t}] 加载失败,已跳过:${n.error}`)):(e.status="connected",e.toolCount=n.tools.length,e.client=n.client,c.push(n.client),i.push(...n.tools),r(`MCP[${t}] 已连接,工具 ${n.tools.length} 个`))}return{tools:i,clients:c,registry:a}}const a=(t,e,n={},r=[])=>({type:"function",function:{name:t,description:e,parameters:{type:"object",properties:n,required:r}}});export function mcpCatalogText(t){const e=Object.values(t?.servers||{}),n=[];for(const t of e){const e="connected"===t.status?`已连接(${t.toolCount} 个工具)`:"idle"===t.status?"已登记,未连接(需要时 connect_mcp 连上)":"failed"===t.status?`连接失败:${String(t.error||"").slice(0,60)}`:"disabled"===t.status?"已禁用":"未连接";n.push(`- ${t.name}(${"project"===t.scope?"项目级":"全局"}):${e}`)}const r=Object.entries(BUILTIN_MCP_CATALOG).filter(([e])=>!t?.servers?.[e]).map(([t,e])=>`- ${t}:${e.description}(未安装,可 connect_mcp 一键装上)`);return r.length&&n.push(...r),n.join("\n")}export function makeMcpConnectTools({registry:t,register:e,clients:n=[],cwd:r=""}){return t&&"function"==typeof e?[{readOnly:!0,spec:a("list_mcp_servers","列出本机可用的 MCP 服务(外部能力连接器):哪些已连上、哪些登记了没连、哪些是内置目录里可一键装的。当任务需要当前工具覆盖不到的能力(访问某个系统、某类数据源)时先看这里。"),run:async()=>{const e=mcpCatalogText(t);return e?"MCP 服务:\n"+e+"\n\n用 connect_mcp(name) 连接后,该服务的工具会以 mcp__<服务名>__<工具名> 出现,下一轮即可调用。":"本机没有配置任何 MCP 服务,也没有内置可装的服务。"}},{readOnly:!1,spec:a("connect_mcp","连接一个 MCP 服务并把它的工具挂进当前会话(下一轮就能调用)。用于会话进行中发现「这活儿需要某个外部能力」时按需接入,不必重开会话。已登记但未自动连接的服务直接给名字即可;内置目录里的服务会用 npx 拉起(首次会下载,需用户同意)。",{name:{type:"string",description:"服务名(见 list_mcp_servers)"},argument:{type:"string",description:"(可选)该服务需要的参数,如 filesystem 的目录、sqlite 的库文件路径"}},["name"]),run:async o=>{const s=String(o?.name||"").trim();if(!s)return"缺少服务名。";const i=t.servers[s];if("connected"===i?.status)return`MCP[${s}] 已经连接(${i.toolCount} 个工具),可直接调用 mcp__${s}__* 工具。`;if("disabled"===i?.status)return`MCP[${s}] 已被禁用(在 mcp.json 里去掉 "disabled": true 后可用)。`;let c=i?.spec;if(!c){const e=BUILTIN_MCP_CATALOG[s];if(!e)return`未找到 MCP 服务「${s}」。可用的见 list_mcp_servers;要接入新服务请在 ~/.huicai/mcp.json 或 <项目>/.huicai/mcp.json 的 mcpServers 里登记。`;const n=String(o?.argument||"").trim()||(e.defaultsToCwd?r:"");if(e.needsArg&&!n)return`服务「${s}」需要参数:${e.needsArg}。请带 argument 再调一次。`;c={command:e.command,args:n?[...e.args,n]:[...e.args]},t.servers[s]={name:s,spec:c,scope:"builtin",status:"pending",toolCount:0,error:""}}const a=await connectServer(s,c),l=t.servers[s];if(a.error)return l.status="failed",l.error=a.error,`连接 MCP[${s}] 失败:${a.error}。(若是内置服务,可能是网络受限导致 npx 拉包失败;可改用本机已有工具完成任务。)`;l.status="connected",l.toolCount=a.tools.length,l.client=a.client,n.push(a.client),e(a.tools);const p=a.tools.map(t=>t.spec.function.name).slice(0,12).join("、");return`✅ 已连接 MCP[${s}],新增 ${a.tools.length} 个工具:${p}`+(a.tools.length>12?" …":"")+"\n下一轮起即可直接调用它们。"}}]:[]}export function closeMcp(t){for(const e of t||[])e.close()}
|
package/src/notify.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{execFile as e,execFileSync as t}from"node:child_process";import n from"node:os";let i;function o(t,n,{timeout:i=8e3}={}){return new Promise(o=>{let r;try{r=e(t,n,{timeout:i,windowsHide:!0},(e,t,n)=>o({ok:!e,out:String(t||""),err:e?String(n||"")||e.message:""}))}catch(e){return void o({ok:!1,out:"",err:e.message})}r.on("error",e=>o({ok:!1,out:"",err:e.message}))})}function r(e){try{return t("win32"===process.platform?"where":"which",[e],{stdio:["ignore","ignore","ignore"],timeout:3e3,windowsHide:!0}),!0}catch{return!1}}export function detectBackend(e=process.platform){return void 0!==i||(i="darwin"===e?r("terminal-notifier")?"terminal-notifier":"osascript":"win32"===e?"powershell":r("notify-send")?"notify-send":null),i}export function resetBackendCache(){i=void 0}export function escapeAppleScript(e){return String(null==e?"":e).replace(/\\/g,"\\\\").replace(/"/g,'\\"')}export function escapePowerShell(e){return String(null==e?"":e).replace(/'/g,"''")}export function normalizeNotification({title:e,message:t,subtitle:n}={}){const i=(e,t)=>String(null==e?"":e).replace(/\s+/g," ").trim().slice(0,t);return{title:i(e,80)||"huicai",message:i(t,240),subtitle:i(n,80)}}export function buildPowerShellScript({title:e,message:t}){return["Add-Type -AssemblyName System.Windows.Forms;","$n = New-Object System.Windows.Forms.NotifyIcon;","$n.Icon = [System.Drawing.SystemIcons]::Information;","$n.BalloonTipTitle = '"+escapePowerShell(e)+"';","$n.BalloonTipText = '"+escapePowerShell(t)+"';","$n.Visible = $true;","$n.ShowBalloonTip(8000);","Start-Sleep -Milliseconds 3000;","$n.Dispose();"].join(" ")}export async function notify(e={}){const{title:t,message:n,subtitle:i}=normalizeNotification(e),r=!1!==e.sound,s=detectBackend();try{if("terminal-notifier"===s){const e=["-title",t,"-message",n||t];i&&e.push("-subtitle",i),r&&e.push("-sound","default");const a=await o("terminal-notifier",e);return a.ok?{ok:!0,backend:s}:{ok:!1,backend:s,reason:a.err}}if("osascript"===s){let e=`display notification "${escapeAppleScript(n||t)}" with title "${escapeAppleScript(t)}"`;i&&(e+=` subtitle "${escapeAppleScript(i)}"`),r&&(e+=' sound name "Glass"');const a=await o("osascript",["-e",e]);return a.ok?{ok:!0,backend:s}:{ok:!1,backend:s,reason:a.err}}if("powershell"===s){const e=buildPowerShellScript({title:t,message:n||t}),i=await o("powershell",["-NoProfile","-NonInteractive","-Command",e],{timeout:12e3});return i.ok?{ok:!0,backend:s}:{ok:!1,backend:s,reason:i.err}}if("notify-send"===s){const i=["-a","huicai",t,n||t];"critical"===e.urgency&&i.unshift("-u","critical");const r=await o("notify-send",i);return r.ok?{ok:!0,backend:s}:{ok:!1,backend:s,reason:r.err}}return{ok:!1,backend:null,reason:"本机没有可用的通知后端(Linux 需安装 libnotify 的 notify-send)"}}catch(e){return{ok:!1,backend:s,reason:e.message}}}export function bell(e){try{(e&&e.write?e.write:e=>process.stdout.write(e))("")}catch{}}export async function notifyTurnDone({durationMs:e,summary:t="",cwd:n="",minMs:i=6e4,force:o=!1,enabled:r=!0}={}){if(!1===r)return{ok:!1,backend:null,reason:"通知已关闭"};if(!(o||e>=i))return{ok:!1,backend:null,reason:"任务时长未达提醒阈值"};const s=n?n.split(/[\\/]/).filter(Boolean).pop():"",a=Math.round(e/6e4);return notify({title:"huicai · 任务完成"+(s?"("+s+")":""),subtitle:a>=1?`耗时约 ${a} 分钟`:"",message:t||"会话已产出结果,回到终端查看。"})}export function makeNotifyTool({enabled:e=!0,appName:t="huicai"}={}){return!1===e?null:{readOnly:!0,spec:{type:"function",function:{name:"send_notification",description:"在用户电脑上弹一条系统通知(通知中心/托盘),用于把用户「叫回来」:长任务完成、遇到必须由人决定的岔路、无人值守自动化产出了结果、监控发现异常等。不要为每个小步骤刷屏——只在真正需要用户注意时用。",parameters:{type:"object",properties:{title:{type:"string",description:"通知标题(简短,如「数据核对完成」)"},message:{type:"string",description:"通知正文(一句话说清发生了什么、需要用户做什么)"},urgency:{type:"string",description:"可选:normal(默认)| critical(仅 Linux 生效,会常驻)"}},required:["title","message"]}}},run:async e=>{const i=await notify({title:`${t} · ${e?.title||"提醒"}`,message:String(e?.message||""),urgency:e?.urgency});return i.ok?`已发送系统通知(${i.backend}):${e?.title}`:(bell(),`系统通知未能发送(${i.reason||"未知原因"}),已改用终端响铃提醒。`+("linux"===n.platform()?"在 Linux 上可安装 libnotify(notify-send)后恢复。":""))}}}
|
package/src/presence.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{execFile as e,spawn as n}from"node:child_process";function o(n,o,t=4e3){return new Promise(r=>{let s;try{s=e(n,o,{timeout:t,windowsHide:!0,maxBuffer:4194304},(e,n)=>r({ok:!e,out:String(n||"")}))}catch(e){return void r({ok:!1,out:"",err:e.message})}s.on("error",()=>r({ok:!1,out:""}))})}export function parseMacLocked(e){const n=/"CGSSessionScreenIsLocked"\s*=\s*(Yes|No|true|false|1|0)/i.exec(String(e||""));return n?/^(yes|true|1)$/i.test(n[1]):null}export function parseMacIdleSeconds(e){const n=/"HIDIdleTime"\s*=\s*(\d+)/.exec(String(e||""));return n?Math.floor(Number(n[1])/1e9):null}export function parseLoginctlLocked(e){const n=/LockedHint\s*=\s*(yes|no|true|false)/i.exec(String(e||""));return n?/^(yes|true)$/i.test(n[1]):null}export function parseWindowsLocked(e){const n=String(e||"");return!/No tasks are running/i.test(n)&&(!!/LogonUI\.exe/i.test(n)||null)}export async function presence(e=process.platform){let n=null,t=null;if("darwin"===e){const[e,r]=await Promise.all([o("ioreg",["-n","Root","-d1"]),o("ioreg",["-c","IOHIDSystem"])]);n=parseMacLocked(e.out),null===n&&e.ok&&(n=!1),t=parseMacIdleSeconds(r.out)}else if("win32"===e){const e=await o("tasklist",["/FI","IMAGENAME eq LogonUI.exe","/NH"]);n=e.ok?parseWindowsLocked(e.out):null}else{const e=await o("loginctl",["show-session","self","-p","LockedHint"]);n=e.ok?parseLoginctlLocked(e.out):null,null===n&&(process.env.DISPLAY||process.env.WAYLAND_DISPLAY||(n=!0))}return{platform:e,locked:n,idleSeconds:t,guiAvailable:null===n?null:!n}}export function describePresence(e){const n=[];return!0===e.locked?n.push("屏幕已锁定"):!1===e.locked?n.push("屏幕未锁定"):n.push("锁屏状态未知"),null!=e.idleSeconds&&n.push(e.idleSeconds>=60?`已空闲约 ${Math.round(e.idleSeconds/60)} 分钟`:`刚刚有操作(空闲 ${e.idleSeconds}s)`),n.join(",")}let t=null;export function wakeLockCommand(e=process.platform,{keepDisplayOn:n=!1}={}){if("darwin"===e)return{bin:"caffeinate",args:n?["-dimsu"]:["-ims"]};if("linux"===e)return{bin:"systemd-inhibit",args:["--what=idle:sleep"+(n?":handle-lid-switch":""),"--who=huicai","--why=自动化任务运行中","--mode=block","sleep","infinity"]};if("win32"===e){return{bin:"powershell",args:["-NoProfile","-NonInteractive","-Command","Add-Type -Name P -Namespace W -MemberDefinition '[DllImport(\"kernel32.dll\")] public static extern uint SetThreadExecutionState(uint e);'; [W.P]::SetThreadExecutionState("+(n?"0x80000003":"0x80000001")+") | Out-Null; while ($true) { Start-Sleep -Seconds 30 }"]}}return null}export function acquireWakeLock({reason:e="自动化任务运行中",keepDisplayOn:o=!1,spawnImpl:r}={}){if(t)return t.refs++,{ok:!0,held:!0,reason:t.reason,release:()=>releaseWakeLock()};const s=wakeLockCommand(process.platform,{keepDisplayOn:o});if(!s)return{ok:!1,held:!1,reason:"当前平台没有内置的防休眠手段",release:()=>{}};const i=r||((e,o)=>n(e,o,{stdio:"ignore",detached:!1,windowsHide:!0}));let a;try{a=i(s.bin,s.args)}catch(e){return{ok:!1,held:!1,reason:`${s.bin} 启动失败:${e.message}`,release:()=>{}}}let l=null;if(a.on?.("error",e=>{l=e.message,t=null}),a.on?.("exit",()=>{t&&t.child===a&&(t=null)}),t={child:a,refs:1,reason:e,keepDisplayOn:o,since:Date.now()},!acquireWakeLock._hooked){acquireWakeLock._hooked=!0;for(const e of["exit","SIGINT","SIGTERM"])try{process.once(e,()=>{try{t?.child?.kill?.()}catch{}})}catch{}}return{ok:!l,held:!l,reason:l||e,release:()=>releaseWakeLock()}}export function releaseWakeLock({force:e=!1}={}){if(!t)return{held:!1};if(t.refs-=1,t.refs>0&&!e)return{held:!0,refs:t.refs};try{t.child?.kill?.()}catch{}return t=null,{held:!1}}export function wakeLockStatus(){return t?{held:!0,reason:t.reason,keepDisplayOn:!!t.keepDisplayOn,sinceMs:Date.now()-t.since}:{held:!1}}export function _resetWakeLock(){t=null}export async function waitForUnlock({timeoutMs:e=18e5,pollMs:n=5e3,signal:o}={}){const t=Date.now();for(;;){if(o?.aborted)return{unlocked:!1,waitedMs:Date.now()-t,reason:"已中断"};const r=await presence();if(!1===r.locked)return{unlocked:!0,waitedMs:Date.now()-t};if(null===r.locked)return{unlocked:!1,waitedMs:Date.now()-t,reason:"无法探测锁屏状态(该环境可能没有图形会话)"};if(Date.now()-t>=e)return{unlocked:!1,waitedMs:Date.now()-t,reason:`等待超时(${Math.round(e/6e4)} 分钟内未解锁)`};await new Promise(e=>setTimeout(e,n))}}const r=(e,n,o={},t=[])=>({type:"function",function:{name:e,description:n,parameters:{type:"object",properties:o,required:t}}});export function makePresenceTools({signalRef:e}={}){return[{readOnly:!0,spec:r("machine_presence","查看用户电脑当前的在场状态:是否锁屏、空闲多久、是否已持有防休眠锁。在准备做界面自动化(computer_* / 打开应用操作)之前先查一次:锁屏时界面操作必然失败,应改为先做不需要界面的部分(命令/Python/文件/联网),或用 wait_for_unlock 等用户回来。"),run:async()=>{const e=await presence(),n=wakeLockStatus();return`在场状态:${describePresence(e)}。\n界面自动化:${!0===e.guiAvailable?"可用":!1===e.guiAvailable?"不可用(锁屏中)":"未知"}。\n防休眠锁:${n.held?`已持有(${n.reason})`:"未持有"}。`+(e.locked?"\n提示:锁屏期间命令/Python/文件/联网类工作照常可跑,只有屏幕点击键入不行。":"")}},{readOnly:!1,spec:r("set_wake_lock","开启/关闭「防休眠锁」:开启后系统在任务期间不会睡眠,用户合盖或离开时长任务也能跑完。适合在开始一个预计要跑很久的任务(大批量数据处理、长构建、无人值守自动化)前调用,任务结束后关掉。",{on:{type:"boolean",description:"true=获取防休眠锁;false=释放"},reason:{type:"string",description:"(可选)为什么需要,写进状态里便于用户理解"},keep_display_on:{type:"boolean",description:"(可选)连屏幕一起保持常亮(默认否——只防系统睡眠,屏幕仍可自动关闭/锁屏)"}},["on"]),run:async e=>{if(!1===e?.on){return releaseWakeLock({force:!0}).held?"仍有其他持有者,未释放。":"✅ 已释放防休眠锁,系统恢复正常休眠策略。"}const n=acquireWakeLock({reason:String(e?.reason||"自动化任务运行中"),keepDisplayOn:!!e?.keep_display_on});return n.ok?`✅ 已获取防休眠锁(${n.reason})。系统在任务期间不会休眠;`+(e?.keep_display_on?"屏幕也会保持常亮。":"屏幕仍可能自动关闭或锁屏——界面自动化需先 machine_presence 确认。")+"任务结束后请调用 set_wake_lock(on=false) 释放。":"未能获取防休眠锁:"+(n.reason||"未知原因")}},{readOnly:!0,spec:r("wait_for_unlock","挂起等待用户解锁电脑(默认最多 30 分钟)。只在「剩下的步骤必须操作图形界面、且当前锁屏」时用;能改成命令/脚本完成的事不要等人。",{timeout_minutes:{type:"number",description:"(可选)最长等待分钟数,默认 30,上限 240"}}),run:async n=>{const o=Math.min(240,Math.max(1,Number(n?.timeout_minutes)||30)),t=await waitForUnlock({timeoutMs:6e4*o,signal:e?.current});return t.unlocked?`用户已解锁(等待 ${Math.round(t.waitedMs/1e3)}s),现在可以继续界面自动化。`:`未等到解锁:${t.reason||"超时"}。请把剩余工作改为不依赖图形界面的方式完成,或用 send_notification 提醒用户回来,并把当前进度说清楚。`}}]}
|
package/src/pyruntime.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import e from"node:path";import n from"node:os";import r from"node:crypto";import{spawn as o}from"node:child_process";export const PYTHON_VERSION="3.12";export const CORE_PACKAGES=["pandas","openpyxl","xlrd","tabulate"];export const EXTRA_PACKAGES=["matplotlib","python-docx","python-pptx","pypdf"];export const BASELINE_PACKAGES=[...CORE_PACKAGES,...EXTRA_PACKAGES];export const BUNDLE_MANIFEST="hc-python-manifest.json";const i=27e5,s=3e4;export function runtimeDir(){return e.join(n.homedir(),".huicai","python")}export function uvBin(){return e.join(runtimeDir(),"win32"===process.platform?"uv.exe":"uv")}function c(){return e.join(runtimeDir(),"venv")}export function venvPython(){if("win32"===process.platform)return e.join(c(),"Scripts","python.exe");const n=e.join(c(),"bin","python3");return t.existsSync(n)?n:e.join(c(),"bin","python")}export function pythonEnvForBash(){try{const e=venvPython();if(t.existsSync(e))return{HUICAI_PYTHON:e}}catch{}return{}}export function mirrors(t={}){return{bundle:(process.env.HUICAI_PYRT_BUNDLE||t.pyBundleUrl||"").trim().replace(/\/+$/,""),uv:(process.env.HUICAI_UV_MIRROR||t.pyUvMirror||"").trim(),uvVersion:(process.env.HUICAI_UV_VERSION||t.pyUvVersion||"").trim(),python:(process.env.HUICAI_PYTHON_MIRROR||t.pyPythonMirror||"").trim(),pypi:(process.env.HUICAI_PYPI_MIRROR||t.pyPypiMirror||"").trim()}}export function uvTarget({platform:t=process.platform,arch:e=process.arch}={}){const n={x64:"x86_64",arm64:"aarch64"}[e];if(!n)throw new Error(`不支持的 CPU 架构:${e}(托管 Python 仅支持 x64/arm64)`);if("darwin"===t)return{file:`uv-${n}-apple-darwin.tar.gz`};if("linux"===t)return{file:`uv-${n}-unknown-linux-gnu.tar.gz`};if("win32"===t)return{file:`uv-${n}-pc-windows-msvc.zip`};throw new Error(`不支持的平台:${t}`)}export function uvUrl({mirror:t="",version:e=""}={},n=uvTarget()){return t?t.replace(/\/+$/,"")+"/"+n.file:e?`https://github.com/astral-sh/uv/releases/download/${e}/${n.file}`:`https://github.com/astral-sh/uv/releases/latest/download/${n.file}`}function a(){return e.join(runtimeDir(),"state.json")}function u(){try{return JSON.parse(t.readFileSync(a(),"utf8"))}catch{return{}}}function l(e){const n={...u(),...e};return t.mkdirSync(runtimeDir(),{recursive:!0}),t.writeFileSync(a(),JSON.stringify(n,null,2)+"\n"),n}function y(){const t=u(),e=Array.isArray(t.baseline?.packages)?t.baseline.packages:[],n=Array.isArray(t.extras)?t.extras:[];return new Set([...e,...n])}function p(){const t=y();return BASELINE_PACKAGES.filter(e=>!t.has(e))}function h(){const t=y();return CORE_PACKAGES.filter(e=>!t.has(e))}function f(){const t=y();return EXTRA_PACKAGES.filter(e=>!t.has(e))}function m(){try{return t.readdirSync(e.join(runtimeDir(),"cpython")).length>0}catch{return!1}}export function runtimeStatus(e={}){const n=u(),r=t.existsSync(venvPython()),o=p(),i=t.existsSync(uvBin())&&m()&&r;return{dir:runtimeDir(),uv:t.existsSync(uvBin()),uvVersion:n.uv?.version||"",python:m(),pythonVersion:n.python?.version||"3.12",venv:r,venvPython:r?venvPython():"",baselineMissing:o,coreMissing:h(),extrasMissing:f(),packages:[...y()].sort(),usable:i&&0===h().length,ready:i&&0===o.length,mirrors:mirrors(e)}}export function resetRuntime(){t.rmSync(runtimeDir(),{recursive:!0,force:!0})}export function execFileCapture(t,e,{cwd:n,timeoutMs:r=12e4,env:i}={}){const c="win32"===process.platform;return new Promise(a=>{let u,l="",y=!1;try{u=o(t,e,{cwd:n,env:i?{...process.env,...i}:process.env,...c?{}:{detached:!0}})}catch(t){return void a({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}const p=setTimeout(()=>{y=!0,(()=>{try{!c&&u.pid?process.kill(-u.pid,"SIGKILL"):u.kill("SIGKILL")}catch{try{u.kill("SIGKILL")}catch{}}})()},r),h=t=>{l.length<s&&(l+=t.toString())};u.stdout.on("data",h),u.stderr.on("data",h),u.on("error",t=>{clearTimeout(p),a({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}),u.on("close",t=>{clearTimeout(p);const e=l.length>s?l.slice(0,s)+"\n…(输出已截断)":l;a({code:t,killed:y,output:e})})})}async function w(n,r){let o;try{const t=await fetch(n,{redirect:"follow",signal:AbortSignal.timeout(6e5),headers:{"User-Agent":"huicai-cli"}});if(!t.ok)throw new Error(`HTTP ${t.status}`);o=Buffer.from(await t.arrayBuffer())}catch(t){throw new Error(`下载失败 ${n}:${t?.cause?.message||t.message}\n(网络受限时可配置镜像:HUICAI_UV_MIRROR 指向内网/加速基址,详见 docs/21)`)}return t.mkdirSync(e.dirname(r),{recursive:!0}),t.writeFileSync(r,o),o.length}async function d(e,n,r){t.mkdirSync(n,{recursive:!0});const o=await r("tar",["-xf",e,"-C",n],{timeoutMs:12e4});if(0!==o.code)throw new Error(`解压失败(退出码 ${o.code}):${o.output.slice(0,400)}`)}function v(n,r,o=0){if(o>3)return null;let i;try{i=t.readdirSync(n,{withFileTypes:!0})}catch{return null}for(const t of i){const i=e.join(n,t.name);if(t.isFile()&&t.name===r)return i;if(t.isDirectory()){const t=v(i,r,o+1);if(t)return t}}return null}function S(n,r,o=0){if(o>3)return null;let i;try{i=t.readdirSync(n,{withFileTypes:!0})}catch{return null}for(const t of i){if(!t.isDirectory())continue;const i=e.join(n,t.name);if(t.name===r)return i;const s=S(i,r,o+1);if(s)return s}return null}const g=t=>String(t||"").split("\n")[0],A=t=>Math.round(t/1024/1024);async function x(e,n){if(!n)return!0;const o=r.createHash("sha256");return await new Promise((n,r)=>{t.createReadStream(e).on("data",t=>o.update(t)).on("end",n).on("error",r)}),o.digest("hex").toLowerCase()===String(n).toLowerCase()}export async function downloadResumable(n,r,{sha256:o="",size:i=0,log:s=()=>{},inactivityMs:c=6e4}={}){if(t.mkdirSync(e.dirname(r),{recursive:!0}),t.existsSync(r)){if(await x(r,o))return;t.rmSync(r,{force:!0})}const a=r+".part";let u=0;try{u=t.statSync(a).size}catch{}const l=new AbortController;let y=null;const p=()=>{clearTimeout(y),y=setTimeout(()=>l.abort(),c)};let h;p();try{h=await fetch(n,{redirect:"follow",signal:l.signal,headers:{"User-Agent":"huicai-cli",...u>0?{Range:`bytes=${u}-`}:{}}})}catch(t){throw clearTimeout(y),new Error(`下载失败 ${n}:${t?.cause?.message||t.message}`)}if(u>0&&206!==h.status&&(u=0),!h.ok&&206!==h.status)throw clearTimeout(y),new Error(`下载失败 ${n}:HTTP ${h.status}`);const f=i||u+Number(h.headers.get("content-length")||0)||0;u>0&&s(`断点续传:本地已有 ${A(u)}MB,继续…`);const m=t.createWriteStream(a,{flags:u>0?"a":"w"});let w=u,d=-10;try{const t=h.body.getReader();for(;;){const{done:e,value:n}=await t.read();if(e)break;if(p(),w+=n.length,m.write(Buffer.from(n))||await new Promise(t=>m.once("drain",t)),f>0){const t=Math.floor(w/f*100);t>=d+10&&(d=t,s(`下载 ${t}%(${A(w)}/${A(f)}MB)`))}}await new Promise((t,e)=>m.end(n=>n?e(n):t()))}catch(t){clearTimeout(y);try{m.destroy()}catch{}throw new Error(`下载中断(已存 ${A(w)}MB,重试将从断点续传):${t?.cause?.message||t.message}`)}if(clearTimeout(y),!await x(a,o))throw t.rmSync(a,{force:!0}),new Error("整包校验失败(sha256 不符),已删除缓存;请重试或检查镜像内容");t.renameSync(a,r)}function $(t){return{UV_PYTHON_INSTALL_DIR:e.join(runtimeDir(),"cpython"),...t.python?{UV_PYTHON_INSTALL_MIRROR:t.python}:{}}}async function E(e,{log:n,run:r}){if(t.existsSync(venvPython()))return;n("创建默认虚拟环境…");const o=await r(uvBin(),["venv",c(),"--python","3.12"],{timeoutMs:12e4,env:$(e)});if(0!==o.code)throw new Error(`虚拟环境创建失败(退出码 ${o.code}):${o.output.slice(-600)}`);l({venv:{at:(new Date).toISOString()}}),n("✓ 虚拟环境就绪")}async function P(t,e,{log:n,run:r},{offline:o=!1}={}){const s=["pip","install","--python",venvPython(),...!o&&e.pypi?["--index-url",e.pypi]:[],...t],c=await r(uvBin(),s,{timeoutMs:i,env:$(e)});if(0!==c.code)throw new Error(`包安装失败(退出码 ${c.code}):${c.output.slice(-600)}`+(o?"":"\n(网络受限时可配置 HUICAI_PYPI_MIRROR,如 https://pypi.tuna.tsinghua.edu.cn/simple)"));n(`✓ 已安装:${t.filter(t=>!t.startsWith("-")).join(" ")||"(按 requirements)"}`)}function I(t){const e=y();for(const n of t)e.add(n);l({baseline:{packages:BASELINE_PACKAGES.filter(t=>e.has(t)),at:(new Date).toISOString()}})}async function _(n,r){const o=!t.existsSync(uvBin()),i=!m();if(!o&&!i&&t.existsSync(venvPython())&&0===p().length)return;r.log(`整包直下:读取清单 ${n.bundle}/${BUNDLE_MANIFEST} …`);const s=await async function(t){try{const e=await fetch(t,{redirect:"follow",signal:AbortSignal.timeout(6e4),headers:{"User-Agent":"huicai-cli"}});if(!e.ok)throw new Error(`HTTP ${e.status}`);return await e.json()}catch(e){throw new Error(`获取 ${t} 失败:${e?.cause?.message||e.message}`)}}(`${n.bundle}/${BUNDLE_MANIFEST}`),c=`${process.platform}-${process.arch}`,a=s&&s[c];if(!a||!a.file)throw new Error(`整包清单无本平台条目(${c})`);const u=e.join(runtimeDir(),"tmp"),y=e.join(u,a.file);await downloadResumable(`${n.bundle}/${a.file}`,y,{sha256:a.sha256,size:a.size,log:r.log});const h=e.join(u,"bundle-extract");t.rmSync(h,{recursive:!0,force:!0}),r.log("解压整包…"),await d(y,h,r.run);const f=(new Date).toISOString();if(o){const e=v(h,"win32"===process.platform?"uv.exe":"uv");if(!e)throw new Error("整包内未找到 uv");t.renameSync(e,uvBin()),"win32"!==process.platform&&t.chmodSync(uvBin(),493),l({uv:{version:"bundle",at:f}})}if(i){const n=S(h,"cpython");if(!n)throw new Error("整包内未找到 cpython 目录");t.renameSync(n,e.join(runtimeDir(),"cpython")),l({python:{version:a.python||"3.12",at:f}})}await E(n,r);const w=S(h,"wheels"),g=v(h,"requirements.txt");p().length>0&&w&&g&&(r.log("离线安装基础包(本地 wheels,不走网络)…"),await P(["--no-index","--find-links",w,"-r",g],n,r,{offline:!0}),I(BASELINE_PACKAGES)),t.rmSync(h,{recursive:!0,force:!0}),t.rmSync(y,{force:!0}),r.log("✓ 整包就绪")}export async function ensureRuntime({log:n=()=>{},config:r={},run:o=execFileCapture,fetchFile:i=w,retryExtrasNow:s=!1}={}){const c=mirrors(r),a=()=>t.existsSync(uvBin())&&m()&&t.existsSync(venvPython())&&0===p().length;return a()?{python:venvPython(),firstInstall:!1,extrasMissing:[]}:async function(n,r){const o=e.join(runtimeDir(),".lock");t.mkdirSync(runtimeDir(),{recursive:!0});const i=Date.now();let s=!1;for(;;)try{t.mkdirSync(o);break}catch(e){if("EEXIST"!==e.code)throw e;let n=null;try{n=t.statSync(o)}catch{continue}if(n&&Date.now()-n.mtimeMs>18e5){try{t.rmdirSync(o)}catch{}continue}if(Date.now()-i>9e5)throw new Error("等待其他会话完成 Python 运行时安装超时(15 分钟);稍后重试,或 huicai python reset --yes 后重装");s||(s=!0,r("另一会话正在安装运行时,等待其完成…")),await new Promise(t=>setTimeout(t,2e3))}try{return await n()}finally{try{t.rmdirSync(o)}catch{}}}(async()=>{if(a())return{python:venvPython(),firstInstall:!1,extrasMissing:[]};const r=Date.now(),y={log:n,run:o,fetchFile:i},p=t.existsSync(uvBin())&&m()&&t.existsSync(venvPython())&&0===h().length;if(!a()&&c.bundle)try{await _(c,y)}catch(t){n(`整包直下未成(${g(t.message)}),回退组装路径…`)}await async function(n,{log:r,run:o,fetchFile:i}){if(t.existsSync(uvBin()))return;const s=uvTarget(),c=uvUrl({mirror:n.uv,version:n.uvVersion},s),a=e.join(runtimeDir(),"tmp"),u=e.join(a,s.file);r(`下载 uv(${c})…`);const y=await i(c,u),p=e.join(a,"uv-extract");t.rmSync(p,{recursive:!0,force:!0}),await d(u,p,o);const h="win32"===process.platform?"uv.exe":"uv",f=v(p,h);if(!f)throw new Error(`解压后未找到 ${h}(工件结构异常:${s.file})`);t.renameSync(f,uvBin()),"win32"!==process.platform&&t.chmodSync(uvBin(),493),t.rmSync(p,{recursive:!0,force:!0}),t.rmSync(u,{force:!0}),l({uv:{version:n.uvVersion||"latest",at:(new Date).toISOString()}}),r(`✓ uv 就绪(${Math.round(y/1024/1024)}MB)`)}(c,y),await async function(t,{log:e,run:n}){if(m())return;e("安装 CPython 3.12(约 40MB,python-build-standalone)…");const r=await n(uvBin(),["python","install","3.12"],{timeoutMs:18e5,env:$(t)});if(0!==r.code)throw new Error(`CPython 安装失败(退出码 ${r.code}):${r.output.slice(-600)}\n(网络受限时可配置 HUICAI_PYTHON_MIRROR 指向 python-build-standalone 镜像)`);l({python:{version:"3.12",at:(new Date).toISOString()}}),e("✓ CPython 3.12 就绪")}(c,y),await E(c,y),await async function(t,e){const n=h();0!==n.length&&(e.log(`安装核心包(${n.join(" ")},约 30MB,Excel 分析优先可用)…`),await P(n,t,e),I(n))}(c,y),await async function(t,e,{throttle:n=!0}={}){const r=f();if(0===r.length)return;const o=u();if(n&&o.extrasTryAt&&Date.now()-o.extrasTryAt<6e5)e.log(`扩展包暂缺(${r.join(" ")}),稍后自动重试;huicai python setup 可立即补装`);else{l({extrasTryAt:Date.now()});try{e.log(`安装扩展包(${r.join(" ")})…`),await P(r,t,e),I(r),l({extrasTryAt:0})}catch(t){e.log(`⚠ 扩展包安装未完成:${g(t.message)}(核心已可用;图表/Word/PPT 用到时自动补装)`)}}}(c,y,{throttle:!s});const w=f();return n(`运行时${w.length?"核心":""}就绪(共 ${Math.round((Date.now()-r)/1e3)}s):${venvPython()}`),{python:venvPython(),firstInstall:!p,extrasMissing:w}},n)}export async function packRuntime({outDir:o,config:s={},log:c=()=>{},run:a=execFileCapture}={}){if(!runtimeStatus(s).ready)throw new Error("本机运行时未全量就绪:先执行 huicai python setup 再打包");const u=mirrors(s),l=e.resolve(o||".");t.mkdirSync(l,{recursive:!0});const y=t.mkdtempSync(e.join(n.tmpdir(),"hc-pyrt-pack-")),p=e.join(y,"hc-python");t.mkdirSync(p,{recursive:!0});try{c("导出依赖锁定清单(uv pip freeze)…");const n=await a(uvBin(),["pip","freeze","--python",venvPython()],{timeoutMs:6e4,env:$(u)});if(0!==n.code)throw new Error(`freeze 失败:${n.output.slice(-300)}`);const o=n.output.split("\n").map(t=>t.trim()).filter(t=>/^[A-Za-z0-9][A-Za-z0-9._-]*==/.test(t)).join("\n")+"\n",s=e.join(p,"requirements.txt");t.writeFileSync(s,o),c("准备 pip(wheels 下载器)…"),await P(["pip"],u,{log:c,run:a}),c("下载本平台 wheels(按锁定版本)…");const h=e.join(p,"wheels");t.mkdirSync(h,{recursive:!0});const f=await a(venvPython(),["-m","pip","download","--dest",h,"-r",s,"--only-binary",":all:",...u.pypi?["--index-url",u.pypi]:[]],{timeoutMs:i});if(0!==f.code)throw new Error(`wheels 下载失败:${f.output.slice(-400)}`);c("复制 uv 与 CPython …");const m=e.basename(uvBin());t.copyFileSync(uvBin(),e.join(p,m)),"win32"!==process.platform&&t.chmodSync(e.join(p,m),493),t.cpSync(e.join(runtimeDir(),"cpython"),e.join(p,"cpython"),{recursive:!0});const w=`hc-python-3.12-${process.platform}-${process.arch}.tar.gz`;c(`打包 ${w} …`);const d=await a("tar",["-czf",e.join(l,w),"-C",y,"hc-python"],{timeoutMs:9e5});if(0!==d.code)throw new Error(`打包失败:${d.output.slice(-300)}`);const v=e.join(l,w),S=t.statSync(v).size,g=r.createHash("sha256");await new Promise((e,n)=>{t.createReadStream(v).on("data",t=>g.update(t)).on("end",e).on("error",n)});const A=e.join(l,BUNDLE_MANIFEST);let x={};try{x=JSON.parse(t.readFileSync(A,"utf8"))}catch{}return x[`${process.platform}-${process.arch}`]={file:w,size:S,sha256:g.digest("hex"),python:"3.12",packages:BASELINE_PACKAGES,builtAt:(new Date).toISOString()},t.writeFileSync(A,JSON.stringify(x,null,2)+"\n"),{file:v,manifest:A,size:S}}finally{t.rmSync(y,{recursive:!0,force:!0})}}export function validPyPackage(t){return"string"==typeof t&&""!==t&&!t.startsWith("-")&&/^[A-Za-z0-9][A-Za-z0-9._-]*(\[[A-Za-z0-9._,-]+\])?([=<>!~]=?[A-Za-z0-9.*+!_-]+)?$/.test(t)}export async function ensurePackages(t,{log:e=()=>{},config:n={},run:r=execFileCapture}={}){const o=y(),i=t.map(t=>t.replace(/[\[=<>!~].*$/,"")),s=t.filter((t,e)=>!o.has(i[e]));if(0===s.length)return;const c=mirrors(n);e(`按需安装:${s.join(" ")} …`),await P(s,c,{log:e,run:r});const a=u(),p=new Set(Array.isArray(a.extras)?a.extras:[]);for(const t of i)p.add(t);l({extras:[...p].sort()})}export async function runPython(n,r={},o={}){if("1"===process.env.HUICAI_PYTHON_DISABLED)return"Python 运行时已被禁用(HUICAI_PYTHON_DISABLED=1,企业策略或用户设置)。";const i=null==r.code?"":String(r.code),s=null==r.script?"":String(r.script);if(!i.trim()&&!s.trim())throw new Error("run_python 需要 code(内联代码)或 script(脚本路径)之一");if(i.trim()&&s.trim())throw new Error("code 与 script 只能二选一");const a=Array.isArray(r.packages)?r.packages.map(String):[];for(const t of a)if(!validPyPackage(t))throw new Error(`非法的包名:${t}`);const u=[],l=t=>u.push(t);let y;await ensureRuntime({log:l,config:o}),a.length&&await ensurePackages(a,{log:l,config:o});let p=null;if(s){if(y=e.resolve(n||".",s),!t.existsSync(y))throw new Error(`脚本不存在:${s}`)}else{const n=e.join(runtimeDir(),"tmp");t.mkdirSync(n,{recursive:!0}),y=e.join(n,`run-${Date.now()}-${Math.random().toString(36).slice(2,8)}.py`),t.writeFileSync(y,i,"utf8"),p=()=>{try{t.unlinkSync(y)}catch{}}}const h=Array.isArray(r.args)?r.args.map(String):[],f=Number.isFinite(r.timeout)?Math.min(r.timeout,6e5):12e4,m=venvPython();try{const t=await execFileCapture(m,["-X","utf8",y,...h],{cwd:n,timeoutMs:f,env:{PYTHONIOENCODING:"utf-8",MPLBACKEND:"Agg",VIRTUAL_ENV:c(),PATH:e.dirname(m)+e.delimiter+(process.env.PATH||"")}});let r=t.output;""===r.trim()&&(r="(无输出)");const o=u.length?`【运行时准备】\n${u.join("\n")}\n\n`:"";return`${o}${t.killed?"【超时被终止】":`退出码 ${t.code}`}\n${r}`}finally{p&&p()}}
|
|
1
|
+
import t from"node:fs";import e from"node:path";import n from"node:os";import r from"node:crypto";import{spawn as o}from"node:child_process";import{StringDecoder as i}from"node:string_decoder";export const PYTHON_VERSION="3.12";export const CORE_PACKAGES=["pandas","openpyxl","xlrd","tabulate"];export const EXTRA_PACKAGES=["matplotlib","python-docx","python-pptx","pypdf","requests"];export const BASELINE_PACKAGES=[...CORE_PACKAGES,...EXTRA_PACKAGES];export const BUNDLE_MANIFEST="hc-python-manifest.json";const s=27e5,c=3e4;export function runtimeDir(){return e.join(n.homedir(),".huicai","python")}export function uvBin(){return e.join(runtimeDir(),"win32"===process.platform?"uv.exe":"uv")}function a(){return e.join(runtimeDir(),"venv")}export function venvPython(){if("win32"===process.platform)return e.join(a(),"Scripts","python.exe");const n=e.join(a(),"bin","python3");return t.existsSync(n)?n:e.join(a(),"bin","python")}export function pythonEnvForBash(){try{const e=venvPython();if(t.existsSync(e))return{HUICAI_PYTHON:e}}catch{}return{}}export function mirrors(t={}){return{bundle:(process.env.HUICAI_PYRT_BUNDLE||t.pyBundleUrl||"").trim().replace(/\/+$/,""),uv:(process.env.HUICAI_UV_MIRROR||t.pyUvMirror||"").trim(),uvVersion:(process.env.HUICAI_UV_VERSION||t.pyUvVersion||"").trim(),python:(process.env.HUICAI_PYTHON_MIRROR||t.pyPythonMirror||"").trim(),pypi:(process.env.HUICAI_PYPI_MIRROR||t.pyPypiMirror||"").trim()}}export function uvTarget({platform:t=process.platform,arch:e=process.arch}={}){const n={x64:"x86_64",arm64:"aarch64"}[e];if(!n)throw new Error(`不支持的 CPU 架构:${e}(托管 Python 仅支持 x64/arm64)`);if("darwin"===t)return{file:`uv-${n}-apple-darwin.tar.gz`};if("linux"===t)return{file:`uv-${n}-unknown-linux-gnu.tar.gz`};if("win32"===t)return{file:`uv-${n}-pc-windows-msvc.zip`};throw new Error(`不支持的平台:${t}`)}export function uvUrl({mirror:t="",version:e=""}={},n=uvTarget()){return t?t.replace(/\/+$/,"")+"/"+n.file:e?`https://github.com/astral-sh/uv/releases/download/${e}/${n.file}`:`https://github.com/astral-sh/uv/releases/latest/download/${n.file}`}function u(){return e.join(runtimeDir(),"state.json")}function l(){try{return JSON.parse(t.readFileSync(u(),"utf8"))}catch{return{}}}function y(e){const n={...l(),...e};return t.mkdirSync(runtimeDir(),{recursive:!0}),t.writeFileSync(u(),JSON.stringify(n,null,2)+"\n"),n}function p(){const t=l(),e=Array.isArray(t.baseline?.packages)?t.baseline.packages:[],n=Array.isArray(t.extras)?t.extras:[];return new Set([...e,...n])}function h(){const t=p();return BASELINE_PACKAGES.filter(e=>!t.has(e))}function f(){const t=p();return CORE_PACKAGES.filter(e=>!t.has(e))}function m(){const t=p();return EXTRA_PACKAGES.filter(e=>!t.has(e))}function w(){try{return t.readdirSync(e.join(runtimeDir(),"cpython")).length>0}catch{return!1}}export function runtimeStatus(e={}){const n=l(),r=t.existsSync(venvPython()),o=h(),i=t.existsSync(uvBin())&&w()&&r;return{dir:runtimeDir(),uv:t.existsSync(uvBin()),uvVersion:n.uv?.version||"",python:w(),pythonVersion:n.python?.version||"3.12",venv:r,venvPython:r?venvPython():"",baselineMissing:o,coreMissing:f(),extrasMissing:m(),packages:[...p()].sort(),usable:i&&0===f().length,ready:i&&0===o.length,mirrors:mirrors(e)}}export function resetRuntime(){t.rmSync(runtimeDir(),{recursive:!0,force:!0})}export function execFileCapture(t,e,{cwd:n,timeoutMs:r=12e4,env:s,signal:a}={}){const u="win32"===process.platform;return new Promise(l=>{let y,p="",h=!1,f=!1;try{y=o(t,e,{cwd:n,env:s?{...process.env,...s}:process.env,...u?{}:{detached:!0}})}catch(t){return void l({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}const m=()=>{try{!u&&y.pid?process.kill(-y.pid,"SIGKILL"):y.kill("SIGKILL")}catch{try{y.kill("SIGKILL")}catch{}}},w=setTimeout(()=>{h=!0,m()},r),d=()=>{f=!0,h=!0,m()};a&&(a.aborted?d():a.addEventListener("abort",d,{once:!0}));const v=()=>{clearTimeout(w),a&&a.removeEventListener("abort",d)},S=new i("utf8"),g=new i("utf8"),A=t=>{p.length<c&&(p+=t)};y.stdout.on("data",t=>A(S.write(t))),y.stderr.on("data",t=>A(g.write(t))),y.on("error",t=>{v(),l({code:-1,killed:!1,aborted:f,output:`命令启动失败:${t.message}`})}),y.on("close",t=>{v(),A(S.end()),A(g.end());const e=p.length>c?p.slice(0,c)+"\n…(输出已截断)":p;l({code:t,killed:h,aborted:f,output:e})})})}async function d(n,r){let o;try{const t=await fetch(n,{redirect:"follow",signal:AbortSignal.timeout(6e5),headers:{"User-Agent":"huicai-cli"}});if(!t.ok)throw new Error(`HTTP ${t.status}`);o=Buffer.from(await t.arrayBuffer())}catch(t){throw new Error(`下载失败 ${n}:${t?.cause?.message||t.message}\n(网络受限时可配置镜像:HUICAI_UV_MIRROR 指向内网/加速基址,详见 docs/21)`)}return t.mkdirSync(e.dirname(r),{recursive:!0}),t.writeFileSync(r,o),o.length}async function v(e,n,r){t.mkdirSync(n,{recursive:!0});const o=await r("tar",["-xf",e,"-C",n],{timeoutMs:12e4});if(0!==o.code)throw new Error(`解压失败(退出码 ${o.code}):${o.output.slice(0,400)}`)}function S(n,r,o=0){if(o>3)return null;let i;try{i=t.readdirSync(n,{withFileTypes:!0})}catch{return null}for(const t of i){const i=e.join(n,t.name);if(t.isFile()&&t.name===r)return i;if(t.isDirectory()){const t=S(i,r,o+1);if(t)return t}}return null}function g(n,r,o=0){if(o>3)return null;let i;try{i=t.readdirSync(n,{withFileTypes:!0})}catch{return null}for(const t of i){if(!t.isDirectory())continue;const i=e.join(n,t.name);if(t.name===r)return i;const s=g(i,r,o+1);if(s)return s}return null}const A=t=>String(t||"").split("\n")[0],x=t=>Math.round(t/1024/1024);async function $(e,n){if(!n)return!0;const o=r.createHash("sha256");return await new Promise((n,r)=>{t.createReadStream(e).on("data",t=>o.update(t)).on("end",n).on("error",r)}),o.digest("hex").toLowerCase()===String(n).toLowerCase()}export async function downloadResumable(n,r,{sha256:o="",size:i=0,log:s=()=>{},inactivityMs:c=6e4}={}){if(t.mkdirSync(e.dirname(r),{recursive:!0}),t.existsSync(r)){if(await $(r,o))return;t.rmSync(r,{force:!0})}const a=r+".part";let u=0;try{u=t.statSync(a).size}catch{}const l=new AbortController;let y=null;const p=()=>{clearTimeout(y),y=setTimeout(()=>l.abort(),c)};let h;p();try{h=await fetch(n,{redirect:"follow",signal:l.signal,headers:{"User-Agent":"huicai-cli",...u>0?{Range:`bytes=${u}-`}:{}}})}catch(t){throw clearTimeout(y),new Error(`下载失败 ${n}:${t?.cause?.message||t.message}`)}if(u>0&&206!==h.status&&(u=0),!h.ok&&206!==h.status)throw clearTimeout(y),new Error(`下载失败 ${n}:HTTP ${h.status}`);const f=i||u+Number(h.headers.get("content-length")||0)||0;u>0&&s(`断点续传:本地已有 ${x(u)}MB,继续…`);const m=t.createWriteStream(a,{flags:u>0?"a":"w"});let w=u,d=-10;try{const t=h.body.getReader();for(;;){const{done:e,value:n}=await t.read();if(e)break;if(p(),w+=n.length,m.write(Buffer.from(n))||await new Promise(t=>m.once("drain",t)),f>0){const t=Math.floor(w/f*100);t>=d+10&&(d=t,s(`下载 ${t}%(${x(w)}/${x(f)}MB)`))}}await new Promise((t,e)=>m.end(n=>n?e(n):t()))}catch(t){clearTimeout(y);try{m.destroy()}catch{}throw new Error(`下载中断(已存 ${x(w)}MB,重试将从断点续传):${t?.cause?.message||t.message}`)}if(clearTimeout(y),!await $(a,o))throw t.rmSync(a,{force:!0}),new Error("整包校验失败(sha256 不符),已删除缓存;请重试或检查镜像内容");t.renameSync(a,r)}function E(t){return{UV_PYTHON_INSTALL_DIR:e.join(runtimeDir(),"cpython"),...t.python?{UV_PYTHON_INSTALL_MIRROR:t.python}:{}}}async function P(e,{log:n,run:r}){if(t.existsSync(venvPython()))return;n("创建默认虚拟环境…");const o=await r(uvBin(),["venv",a(),"--python","3.12"],{timeoutMs:12e4,env:E(e)});if(0!==o.code)throw new Error(`虚拟环境创建失败(退出码 ${o.code}):${o.output.slice(-600)}`);y({venv:{at:(new Date).toISOString()}}),n("✓ 虚拟环境就绪")}async function I(t,e,{log:n,run:r},{offline:o=!1}={}){const i=["pip","install","--python",venvPython(),...!o&&e.pypi?["--index-url",e.pypi]:[],...t],c=await r(uvBin(),i,{timeoutMs:s,env:E(e)});if(0!==c.code)throw new Error(`包安装失败(退出码 ${c.code}):${c.output.slice(-600)}`+(o?"":"\n(网络受限时可配置 HUICAI_PYPI_MIRROR,如 https://pypi.tuna.tsinghua.edu.cn/simple)"));n(`✓ 已安装:${t.filter(t=>!t.startsWith("-")).join(" ")||"(按 requirements)"}`)}function _(t){const e=p();for(const n of t)e.add(n);y({baseline:{packages:BASELINE_PACKAGES.filter(t=>e.has(t)),at:(new Date).toISOString()}})}async function T(n,r){const o=!t.existsSync(uvBin()),i=!w();if(!o&&!i&&t.existsSync(venvPython())&&0===h().length)return;r.log(`整包直下:读取清单 ${n.bundle}/${BUNDLE_MANIFEST} …`);const s=await async function(t){try{const e=await fetch(t,{redirect:"follow",signal:AbortSignal.timeout(6e4),headers:{"User-Agent":"huicai-cli"}});if(!e.ok)throw new Error(`HTTP ${e.status}`);return await e.json()}catch(e){throw new Error(`获取 ${t} 失败:${e?.cause?.message||e.message}`)}}(`${n.bundle}/${BUNDLE_MANIFEST}`),c=`${process.platform}-${process.arch}`,a=s&&s[c];if(!a||!a.file)throw new Error(`整包清单无本平台条目(${c})`);const u=e.join(runtimeDir(),"tmp"),l=e.join(u,a.file);await downloadResumable(`${n.bundle}/${a.file}`,l,{sha256:a.sha256,size:a.size,log:r.log});const p=e.join(u,"bundle-extract");t.rmSync(p,{recursive:!0,force:!0}),r.log("解压整包…"),await v(l,p,r.run);const f=(new Date).toISOString();if(o){const e=S(p,"win32"===process.platform?"uv.exe":"uv");if(!e)throw new Error("整包内未找到 uv");t.renameSync(e,uvBin()),"win32"!==process.platform&&t.chmodSync(uvBin(),493),y({uv:{version:"bundle",at:f}})}if(i){const n=g(p,"cpython");if(!n)throw new Error("整包内未找到 cpython 目录");t.renameSync(n,e.join(runtimeDir(),"cpython")),y({python:{version:a.python||"3.12",at:f}})}await P(n,r);const m=g(p,"wheels"),d=S(p,"requirements.txt");h().length>0&&m&&d&&(r.log("离线安装基础包(本地 wheels,不走网络)…"),await I(["--no-index","--find-links",m,"-r",d],n,r,{offline:!0}),_(BASELINE_PACKAGES)),t.rmSync(p,{recursive:!0,force:!0}),t.rmSync(l,{force:!0}),r.log("✓ 整包就绪")}export async function ensureRuntime({log:n=()=>{},config:r={},run:o=execFileCapture,fetchFile:i=d,retryExtrasNow:s=!1}={}){const c=mirrors(r),a=()=>t.existsSync(uvBin())&&w()&&t.existsSync(venvPython())&&0===h().length;return a()?{python:venvPython(),firstInstall:!1,extrasMissing:[]}:async function(n,r){const o=e.join(runtimeDir(),".lock");t.mkdirSync(runtimeDir(),{recursive:!0});const i=Date.now();let s=!1;for(;;)try{t.mkdirSync(o);break}catch(e){if("EEXIST"!==e.code)throw e;let n=null;try{n=t.statSync(o)}catch{continue}if(n&&Date.now()-n.mtimeMs>18e5){try{t.rmdirSync(o)}catch{}continue}if(Date.now()-i>9e5)throw new Error("等待其他会话完成 Python 运行时安装超时(15 分钟);稍后重试,或 huicai python reset --yes 后重装");s||(s=!0,r("另一会话正在安装运行时,等待其完成…")),await new Promise(t=>setTimeout(t,2e3))}try{return await n()}finally{try{t.rmdirSync(o)}catch{}}}(async()=>{if(a())return{python:venvPython(),firstInstall:!1,extrasMissing:[]};const r=Date.now(),u={log:n,run:o,fetchFile:i},p=t.existsSync(uvBin())&&w()&&t.existsSync(venvPython())&&0===f().length;if(!a()&&c.bundle)try{await T(c,u)}catch(t){n(`整包直下未成(${A(t.message)}),回退组装路径…`)}await async function(n,{log:r,run:o,fetchFile:i}){if(t.existsSync(uvBin()))return;const s=uvTarget(),c=uvUrl({mirror:n.uv,version:n.uvVersion},s),a=e.join(runtimeDir(),"tmp"),u=e.join(a,s.file);r(`下载 uv(${c})…`);const l=await i(c,u),p=e.join(a,"uv-extract");t.rmSync(p,{recursive:!0,force:!0}),await v(u,p,o);const h="win32"===process.platform?"uv.exe":"uv",f=S(p,h);if(!f)throw new Error(`解压后未找到 ${h}(工件结构异常:${s.file})`);t.renameSync(f,uvBin()),"win32"!==process.platform&&t.chmodSync(uvBin(),493),t.rmSync(p,{recursive:!0,force:!0}),t.rmSync(u,{force:!0}),y({uv:{version:n.uvVersion||"latest",at:(new Date).toISOString()}}),r(`✓ uv 就绪(${Math.round(l/1024/1024)}MB)`)}(c,u),await async function(t,{log:e,run:n}){if(w())return;e("安装 CPython 3.12(约 40MB,python-build-standalone)…");const r=await n(uvBin(),["python","install","3.12"],{timeoutMs:18e5,env:E(t)});if(0!==r.code)throw new Error(`CPython 安装失败(退出码 ${r.code}):${r.output.slice(-600)}\n(网络受限时可配置 HUICAI_PYTHON_MIRROR 指向 python-build-standalone 镜像)`);y({python:{version:"3.12",at:(new Date).toISOString()}}),e("✓ CPython 3.12 就绪")}(c,u),await P(c,u),await async function(t,e){const n=f();0!==n.length&&(e.log(`安装核心包(${n.join(" ")},约 30MB,Excel 分析优先可用)…`),await I(n,t,e),_(n))}(c,u),await async function(t,e,{throttle:n=!0}={}){const r=m();if(0===r.length)return;const o=l();if(n&&o.extrasTryAt&&Date.now()-o.extrasTryAt<6e5)e.log(`扩展包暂缺(${r.join(" ")}),稍后自动重试;huicai python setup 可立即补装`);else{y({extrasTryAt:Date.now()});try{e.log(`安装扩展包(${r.join(" ")})…`),await I(r,t,e),_(r),y({extrasTryAt:0})}catch(t){e.log(`⚠ 扩展包安装未完成:${A(t.message)}(核心已可用;图表/Word/PPT 用到时自动补装)`)}}}(c,u,{throttle:!s});const h=m();return n(`运行时${h.length?"核心":""}就绪(共 ${Math.round((Date.now()-r)/1e3)}s):${venvPython()}`),{python:venvPython(),firstInstall:!p,extrasMissing:h}},n)}export async function packRuntime({outDir:o,config:i={},log:c=()=>{},run:a=execFileCapture}={}){if(!runtimeStatus(i).ready)throw new Error("本机运行时未全量就绪:先执行 huicai python setup 再打包");const u=mirrors(i),l=e.resolve(o||".");t.mkdirSync(l,{recursive:!0});const y=t.mkdtempSync(e.join(n.tmpdir(),"hc-pyrt-pack-")),p=e.join(y,"hc-python");t.mkdirSync(p,{recursive:!0});try{c("导出依赖锁定清单(uv pip freeze)…");const n=await a(uvBin(),["pip","freeze","--python",venvPython()],{timeoutMs:6e4,env:E(u)});if(0!==n.code)throw new Error(`freeze 失败:${n.output.slice(-300)}`);const o=n.output.split("\n").map(t=>t.trim()).filter(t=>/^[A-Za-z0-9][A-Za-z0-9._-]*==/.test(t)).join("\n")+"\n",i=e.join(p,"requirements.txt");t.writeFileSync(i,o),c("准备 pip(wheels 下载器)…"),await I(["pip"],u,{log:c,run:a}),c("下载本平台 wheels(按锁定版本)…");const h=e.join(p,"wheels");t.mkdirSync(h,{recursive:!0});const f=await a(venvPython(),["-m","pip","download","--dest",h,"-r",i,"--only-binary",":all:",...u.pypi?["--index-url",u.pypi]:[]],{timeoutMs:s});if(0!==f.code)throw new Error(`wheels 下载失败:${f.output.slice(-400)}`);c("复制 uv 与 CPython …");const m=e.basename(uvBin());t.copyFileSync(uvBin(),e.join(p,m)),"win32"!==process.platform&&t.chmodSync(e.join(p,m),493),t.cpSync(e.join(runtimeDir(),"cpython"),e.join(p,"cpython"),{recursive:!0});const w=`hc-python-3.12-${process.platform}-${process.arch}.tar.gz`;c(`打包 ${w} …`);const d=await a("tar",["-czf",e.join(l,w),"-C",y,"hc-python"],{timeoutMs:9e5});if(0!==d.code)throw new Error(`打包失败:${d.output.slice(-300)}`);const v=e.join(l,w),S=t.statSync(v).size,g=r.createHash("sha256");await new Promise((e,n)=>{t.createReadStream(v).on("data",t=>g.update(t)).on("end",e).on("error",n)});const A=e.join(l,BUNDLE_MANIFEST);let x={};try{x=JSON.parse(t.readFileSync(A,"utf8"))}catch{}return x[`${process.platform}-${process.arch}`]={file:w,size:S,sha256:g.digest("hex"),python:"3.12",packages:BASELINE_PACKAGES,builtAt:(new Date).toISOString()},t.writeFileSync(A,JSON.stringify(x,null,2)+"\n"),{file:v,manifest:A,size:S}}finally{t.rmSync(y,{recursive:!0,force:!0})}}export function validPyPackage(t){return"string"==typeof t&&""!==t&&!t.startsWith("-")&&/^[A-Za-z0-9][A-Za-z0-9._-]*(\[[A-Za-z0-9._,-]+\])?([=<>!~]=?[A-Za-z0-9.*+!_-]+)?$/.test(t)}export async function ensurePackages(t,{log:e=()=>{},config:n={},run:r=execFileCapture}={}){const o=p(),i=t.map(t=>t.replace(/[\[=<>!~].*$/,"")),s=t.filter((t,e)=>!o.has(i[e]));if(0===s.length)return;const c=mirrors(n);e(`按需安装:${s.join(" ")} …`),await I(s,c,{log:e,run:r});const a=l(),u=new Set(Array.isArray(a.extras)?a.extras:[]);for(const t of i)u.add(t);y({extras:[...u].sort()})}export async function runPython(n,r={},o={},i){if("1"===process.env.HUICAI_PYTHON_DISABLED)return"Python 运行时已被禁用(HUICAI_PYTHON_DISABLED=1,企业策略或用户设置)。";const s=null==r.code?"":String(r.code),c=null==r.script?"":String(r.script);if(!s.trim()&&!c.trim())throw new Error("run_python 需要 code(内联代码)或 script(脚本路径)之一");if(s.trim()&&c.trim())throw new Error("code 与 script 只能二选一");const u=Array.isArray(r.packages)?r.packages.map(String):[];for(const t of u)if(!validPyPackage(t))throw new Error(`非法的包名:${t}`);const l=[],y=t=>l.push(t);let p;await ensureRuntime({log:y,config:o}),u.length&&await ensurePackages(u,{log:y,config:o});let h=null;if(c){if(p=e.resolve(n||".",c),!t.existsSync(p))throw new Error(`脚本不存在:${c}`)}else{const n=e.join(runtimeDir(),"tmp");t.mkdirSync(n,{recursive:!0}),p=e.join(n,`run-${Date.now()}-${Math.random().toString(36).slice(2,8)}.py`),t.writeFileSync(p,s,"utf8"),h=()=>{try{t.unlinkSync(p)}catch{}}}const f=Array.isArray(r.args)?r.args.map(String):[],m=Number.isFinite(r.timeout)?Math.min(r.timeout,6e5):12e4,w=venvPython();try{const t=await execFileCapture(w,["-X","utf8",p,...f],{cwd:n,timeoutMs:m,signal:i,env:{PYTHONIOENCODING:"utf-8",MPLBACKEND:"Agg",VIRTUAL_ENV:a(),PATH:e.dirname(w)+e.delimiter+(process.env.PATH||"")}});let r=t.output;""===r.trim()&&(r="(无输出)");const o=l.length?`【运行时准备】\n${l.join("\n")}\n\n`:"";return`${o}${t.killed?"【超时被终止】":`退出码 ${t.code}`}\n${r}`}finally{h&&h()}}
|
package/src/repl.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import s from"node:readline";import{c as o}from"./colors.js";import{createPermissions as t}from"./permissions.js";import{runTurn as e}from"./agent.js";import{createSession as
|
|
1
|
+
import s from"node:readline";import{c as o}from"./colors.js";import{createPermissions as t}from"./permissions.js";import{runTurn as e}from"./agent.js";import{createSession as r,saveSession as n,turnCount as i}from"./session.js";import{ensureSessionTitle as c}from"./title.js";import{createCheckpoints as a}from"./checkpoints.js";import{buildSystemPrompt as l}from"./context.js";import{runVerifyLoop as u}from"./autofix.js";import{runAutoFlow as p}from"./autorun.js";import{loadMcpTools as m,closeMcp as d,makeMcpConnectTools as g}from"./mcp.js";import{makeAutomationTools as f}from"./automation-tools.js";import{makeNotifyTool as w,notifyTurnDone as h}from"./notify.js";import{makePresenceTools as y,acquireWakeLock as j,releaseWakeLock as x}from"./presence.js";import{makeComputerTools as b}from"./computer.js";import{createVisualBridge as k}from"./visual.js";import{autoSyncPlatformSkills as T}from"./skills.js";import{makeSubagentTool as I,makeDispatchAgentsTool as S}from"./subagent.js";import{makeDispatchMemberTool as v}from"./squad.js";import{makeConsultTool as $}from"./consult.js";import{makeExpertTools as q}from"./experts.js";import{listWorkspaceFiles as P}from"./executors.js";import{makeAdvisorTool as E}from"./advisor.js";import{makeRecallTool as R,readMemoryRaw as _}from"./memory.js";import{detectScanners as A,makeScanTool as C}from"./scan.js";import{runSlash as L}from"./commands.js";import{handleAccountError as M,accountErrorKind as O}from"./account.js";import{expandAtMentions as W,buildUserContent as D,runBangCommand as H,loadHistory as V,appendHistory as J}from"./input.js";import{makeSkillTools as N}from"./skills.js";import{renderWelcome as Q}from"./banner.js";export async function buildExtraTools(s,t,e={}){const r="function"==typeof e.onSub?e.onSub:null;let n={tools:[],clients:[],registry:{servers:{}}};try{n=await m(t)}catch(s){process.stdout.write(o.yellow("MCP 初始化失败(忽略):"+s.message)+"\n")}const i=[...N(t),...n.tools],c=I({config:s,cwd:t,extraTools:n.tools,depth:0,onSub:r});c&&i.push(c);const a=S({config:s,cwd:t,extraTools:n.tools,depth:0,onSub:r});a&&i.push(a);const l=v({config:s,cwd:t,extraTools:n.tools,onSub:r});l&&i.push(l);const u=$({config:s});u&&i.push(u);try{i.push(...await q({config:s}))}catch{}const p=E({config:s});p&&i.push(p),!1!==s.memoryEnabled&&_(t)&&i.push(R({cwd:t})),A(t).length&&i.push(C({cwd:t})),i.push(...g({registry:n.registry,clients:n.clients,cwd:t,register:s=>i.push(...s)})),i.push(...f({cwd:t,selfSchedule:!1!==e.selfSchedule,runnerHint:e.runnerHint}));const d=w({enabled:!1!==s.notifications});d&&i.push(d),i.push(...y({signalRef:e.signalRef}));const h="function"==typeof e.injectVisual?e.injectVisual:"function"==typeof e.queueInject?k({config:s,queue:e.queueInject,currentModel:e.currentModel}):null;return i.push(...b({config:s,injectVisual:h,omit:e.omitComputer||[],enabled:e.computerUse})),T(s,{onInfo:t=>{s.debug&&process.stdout.write(o.gray(" · "+t)+"\n")}}).catch(()=>{}),{extraTools:i,clients:n.clients,mcpRegistry:n.registry}}export async function startRepl({config:i,cwd:m,yolo:g,initialPrompt:f,session:w,version:y}){const b=s.createInterface({input:process.stdin,output:process.stdout,history:V(m).reverse(),historySize:200,completer:s=>{const o=/(?:^|\s)@([^\s@]*)$/.exec(s);if(!o)return[[],s];const t="@"+o[1],e=t.toLowerCase();return[P(m).map(s=>"@"+s).filter(s=>s.toLowerCase().startsWith(e)).slice(0,40),t]}}),k=s=>new Promise(o=>b.question(s,o));let T=!1,I=t({yolo:g,ask:k,cwd:m});const S={prompt:0,completion:0},v={list:[]},$=a();let q=null,E=null;const R={config:i,cwd:m,permissions:I,usage:S,session:w,version:y};b.on("SIGINT",()=>{q?q.abort():process.stdout.write("\n"+o.gray("输入 /exit 退出。")+"\n")}),function(s,o,t,e){process.stdout.write(Q({model:s.model,cwd:o,version:e,platform:/^(agent|workflow|datahuman|squad)-/.test(String(s.model||"")),yolo:t.isYolo(),consult:s.consult?s.consultLabel||s.consult:null})+"\n")}(i,m,I,y),z(w);const _=[],A={current:null};let{extraTools:C,clients:O}=await buildExtraTools(i,m,{queueInject:s=>_.push(s),signalRef:A,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});const N=()=>{c(R.session,i).then(s=>{s&&n(R.session)}).catch(()=>{})},Y=async s=>{const{text:t,mentions:r,images:c}=await W(s,m,i,{onDocStart:(s,t,e)=>process.stdout.write(o.gray(` · 正在解析文档 @${s}${e>1?`(${t}/${e})`:""}…`)+"\n")});r.length&&process.stdout.write(o.gray(" · 已展开引用:"+r.join("、")+(c.length?`(含 ${c.length} 张图片)`:""))+"\n");const a=$.snapshot({session:R.session,label:s});R.session.messages.push({role:"user",content:D(t,c)}),N(),q=new AbortController,A.current=q.signal,R.session.compaction||(R.session.compaction={}),i.sessionId=R.session.id;const l="auto"===I.getMode?.()&&!1!==i.wakeLockInAuto?j({reason:"自动模式任务执行中"}):null,u=Date.now();let p;try{p=await e({config:i,messages:R.session.messages,permissions:I,cwd:m,signal:q.signal,extraTools:C,todoState:v,compaction:R.session.compaction,fileJournal:a,pullQueued:()=>_.shift()||null})}finally{l?.ok&&x()}return p?.usage&&(S.prompt+=p.usage.prompt,S.completion+=p.usage.completion),p?.lastPromptTokens&&(R.session.lastPromptTokens=p.lastPromptTokens),q=null,A.current=null,p?.aborted||p?.error||h({durationMs:Date.now()-u,cwd:m,summary:String(s).replace(/\s+/g," ").slice(0,80),enabled:!1!==i.notifications}).catch(()=>{}),n(R.session),N(),p?.error&&"auth"===M(p,i)&&(T=!0),p},F=()=>({...R,session:R.session,extraTools:C,checkpoints:$,print:s=>process.stdout.write(String(s).endsWith("\n")?String(s):String(s)+"\n")});for(f&&(process.stdout.write(o.cyan("\nhuicai> ")+f+"\n"),await Y(f));!T;){let s;try{s=(await k(o.cyan("\nhuicai> "))).trim()}catch{break}if(""!==s){if(J(m,s),"/exit"===s||"/quit"===s)break;if(s.startsWith("!")&&s.length>1){const t=s.slice(1).trim(),{output:e,record:r}=await H(t,m);process.stdout.write(o.gray(e.split("\n").map(s=>" "+s).join("\n"))+"\n"),R.session.messages.push({role:"user",content:r}),n(R.session);continue}if(s.startsWith("/")){const e=await L(s,F());if("exit"===e.action)break;"clear"===e.action&&(R.session=r({cwd:m,model:i.model,messages:[R.session.messages[0]]}),v.list=[],$.list.length=0,process.stdout.write(o.gray("已开启新会话(旧会话已保存,可用 huicai --resume 恢复)。\n"))),"refresh"===e.action&&z(R.session),"cd"===e.action&&(m=e.cwd,R.cwd=m,I=t({yolo:I.isYolo(),ask:k,cwd:m}),R.permissions=I,d(O),({extraTools:C,clients:O}=await buildExtraTools(i,m,{queueInject:s=>_.push(s),signalRef:A})),R.session=r({cwd:m,model:i.model,messages:[{role:"system",content:l(m,{memory:!1!==i.memoryEnabled,config:i})}]}),v.list=[],$.list.length=0,i.sessionId=R.session.id,process.stdout.write(o.gray("已切换到 "+m+"(新会话,旧会话已保存)。\n"))),"autofix"===e.action&&await u({command:e.command,maxIters:e.maxIters,cwd:m,runTurnOnce:s=>Y(s),emit:s=>process.stdout.write(o.gray(s)+"\n")}),"autorun"===e.action&&await p({goal:e.goal,config:i,permissions:I,cwd:m,runTurnOnce:s=>Y(s),emit:s=>process.stdout.write(o.gray(" · "+s)+"\n")}),"loop"===e.action&&(E&&(clearInterval(E),E=null),e.stop?process.stdout.write(o.gray("已停止循环。\n")):(process.stdout.write(o.gray(`已开始循环:每 ${e.seconds}s 运行「${e.prompt}」(/loop off 停止)。\n`)),E=setInterval(()=>{q||Y(e.prompt)},1e3*e.seconds))),e.prompt&&await Y(e.prompt);continue}await Y(s)}}return E&&clearInterval(E),b.close(),d(O),T?{relogin:!0}:(process.stdout.write(o.gray("\n再见。\n")),{})}function Y(s){return"string"==typeof s?s:Array.isArray(s)?s.map(s=>"string"==typeof s?s:s&&"text"===s.type&&s.text||"").join(""):""}function z(s){const t=(s.messages||[]).filter(s=>"user"===s.role||"assistant"===s.role),e=i(s.messages);if(0===e)return;process.stdout.write(o.gray(` 已恢复会话「${s.title||""}」(${e} 轮)——历史对话如下:`)+"\n");const r=t.length>40?t.length-40:0;r&&process.stdout.write(o.gray(` (较早 ${r} 条已省略,完整记录在 ~/.huicai/sessions)`)+"\n");for(const s of t.slice(-40)){const t=Y(s.content).trim();if(!t)continue;const e=t.length>800?t.slice(0,800)+"…":t,r="user"===s.role?o.bold(" 你 › "):o.gray(" 助手 › ");process.stdout.write(r+e.replace(/\n/g,"\n ")+"\n")}process.stdout.write(o.gray(" ——以上为历史,可继续对话——")+"\n")}export async function runOnce({config:s,cwd:r,yolo:c,prompt:a,session:l,persist:u,outputFormat:p,maxTurns:m}){a||(process.stderr.write('用法: huicai -p "你的指令"\n'),process.exit(1));const g="json"===p,f=t({yolo:c,ask:null,cwd:r}),w=[],{extraTools:h,clients:y}=await buildExtraTools(s,r,{queueInject:s=>w.push(s)}),{text:b,images:k}=await W(a,r,s);l.messages.push({role:"user",content:D(b,k)}),l.compaction||(l.compaction={}),s.sessionId=l.id;const T=c&&!1!==s.wakeLockInAuto?j({reason:"无人值守任务执行中"}):null;let I;try{I=await e({config:s,messages:l.messages,permissions:f,cwd:r,extraTools:h,maxIters:m,compaction:l.compaction,io:g?{write:()=>{}}:void 0,pullQueued:()=>w.shift()||null})}finally{T?.ok&&x()}I?.lastPromptTokens&&(l.lastPromptTokens=I.lastPromptTokens),u&&n(l),d(y);const S=I?.error?O(I):"";if(S&&(M(I,s,s=>process.stderr.write(s)),"auth"===S&&process.stderr.write(o.gray("请重新运行 `huicai login` 登录后重试。")+"\n")),g){const o=[...l.messages].reverse().find(s=>"assistant"===s.role&&s.content);process.stdout.write(JSON.stringify({type:"result",is_error:!!I?.error,...I?.error?{error:I.error}:{},...S?{error_kind:S}:{},result:o?String(o.content):"",usage:{input_tokens:I?.usage?.prompt??0,output_tokens:I?.usage?.completion??0},session_id:l.id,model:s.model,num_turns:i(l.messages)})+"\n")}I?.error&&process.exit(1)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cronMatches as e}from"./cron.js";const t=["周日","周一","周二","周三","周四","周五","周六"];function r(e,t,r,n){const i=Math.floor(Number(e));return Number.isNaN(i)?n:Math.min(r,Math.max(t,i))}function n(e){return String(e).padStart(2,"0")}export function parseHM(e){const t=/^(\d{1,2}):(\d{2})$/.exec(String(e||"").trim());if(!t)return null;const r=Number(t[1]),n=Number(t[2]);return r>23||n>59?null:{h:r,mi:n}}function i(e,t="09:00"){const r=parseHM(e);return r?`${n(r.h)}:${n(r.mi)}`:t}export function parseYMD(e){const t=/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})$/.exec(String(e||"").trim());if(!t)return null;const r=Number(t[1]),n=Number(t[2]),i=Number(t[3]);return n<1||n>12||i<1||i>31?null:{y:r,m:n,d:i}}export function freqToCron(e){if(!e||"object"!=typeof e)return null;const t=parseHM(e.time)||{h:9,mi:0},{h:n,mi:i}=t;if("periodic"===e.kind)switch(e.period){case"daily":default:return`${i} ${n} * * *`;case"weekly":case"biweekly":return`${i} ${n} * * ${r(e.weekday,0,6,1)}`;case"monthly":return`${i} ${n} ${r(e.day,1,31,1)} * *`;case"yearly":return`${i} ${n} ${r(e.day,1,31,1)} ${r(e.month,1,12,1)} *`}if("interval"===e.kind){const t=Array.isArray(e.weekdays)&&e.weekdays.length&&e.weekdays.length<7?[...new Set(e.weekdays.map(e=>r(e,0,6,0)))].sort((e,t)=>e-t).join(","):"*";if("hour"===e.unit){return`0 */${r(e.every,1,23,1)} * * ${t}`}return`*/${r(e.every,1,59,5)} * * * ${t}`}if("once"===e.kind){const t=parseYMD(e.date);return t?`${i} ${n} ${t.d} ${t.m} *`:null}return null}function o(e){const t=new Date(e.getFullYear(),e.getMonth(),e.getDate());return t.setDate(t.getDate()-(t.getDay()+6)%7),t}export function isDue(t,r=new Date){if(!t)return!1;const n=t.cron||freqToCron(t.freq);if(!n||!e(n,r))return!1;if(!function(e,t){const r=parseYMD(e.from),n=parseYMD(e.to);return!(r&&t<new Date(r.y,r.m-1,r.d,0,0,0,0)||n&&t>new Date(n.y,n.m-1,n.d,23,59,59,999))}(t,r))return!1;const i=t.freq;if(i&&"once"===i.kind){const e=parseYMD(i.date);if(!e)return!1;if(r.getFullYear()!==e.y||r.getMonth()+1!==e.m||r.getDate()!==e.d)return!1}return!(i&&"periodic"===i.kind&&"biweekly"===i.period&&!function(e,t){const r=o(new Date(Number(e.created)||Date.now())),n=o(t);return(Math.round((n-r)/6048e5)%2+2)%2==0}(t,r))}export function nextRunAt(e,t=new Date,r=366){if(!e)return null;if(!(e.cron||freqToCron(e.freq)))return null;if(e.freq&&"once"===e.freq.kind){const r=parseYMD(e.freq.date);if(!r)return null;const n=parseHM(e.freq.time)||{h:0,mi:0},i=new Date(r.y,r.m-1,r.d,n.h,n.mi,0,0).getTime();return i>t.getTime()?i:null}const n=new Date(t.getTime());n.setSeconds(0,0),n.setMinutes(n.getMinutes()+1);const i=n.getTime()+864e5*r;for(let t=n.getTime();t<=i;t+=6e4)if(isDue(e,new Date(t)))return t;return null}const u={"0 9 * * *":"每天 09:00","0 9 * * 1-5":"工作日 09:00","0 * * * *":"每小时整点","0 9 * * 1":"每周一 09:00"};export function describeSchedule(e){const o=e&&e.freq;if(o&&"object"==typeof o){const e=i(o.time);if("periodic"===o.kind)switch(o.period){case"daily":default:return`每天 ${e}`;case"weekly":return`每${t[r(o.weekday,0,6,1)]} ${e}`;case"biweekly":return`每两周·${t[r(o.weekday,0,6,1)]} ${e}`;case"monthly":return`每月${r(o.day,1,31,1)}号 ${e}`;case"yearly":return`每年${r(o.month,1,12,1)}月${r(o.day,1,31,1)}日 ${e}`}if("interval"===o.kind){let e=`每 ${r(o.every,1,999,5)} ${"hour"===o.unit?"小时":"分钟"}`;return Array.isArray(o.weekdays)&&o.weekdays.length&&o.weekdays.length<7&&(e+=` · ${[...new Set(o.weekdays)].sort((e,t)=>e-t).map(e=>t[r(e,0,6,0)]).join("")}`),e}if("once"===o.kind){const e=parseYMD(o.date);return e?`${e.y}/${n(e.m)}/${n(e.d)} ${i(o.time)}(单次)`:`单次 ${i(o.time)}`}}const a=String(e&&e.cron||"").trim();return u[a]||(a?`cron:${a}`:"")}
|
package/src/schedule.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import n from"node:fs";import e from"node:path";import t from"node:os";import{freqToCron as r}from"./schedule-freq.js";export const SCHEDULE_FILE=e.join(t.homedir(),".huicai","schedule.json");function o(){try{const e=JSON.parse(n.readFileSync(SCHEDULE_FILE,"utf8"));return Array.isArray(e)?e:[]}catch{return[]}}function i(t){n.mkdirSync(e.dirname(SCHEDULE_FILE),{recursive:!0}),n.writeFileSync(SCHEDULE_FILE,JSON.stringify(t,null,2)+"\n",{mode:384})}function d(n){const e={...n};if(e.freq&&"object"==typeof e.freq){const n=r(e.freq);n&&(e.cron=n)}return void 0===e.enabled&&(e.enabled=!0),null==e.permission&&(e.permission="default"),null==e.model&&(e.model="auto"),e}export function listTasks(){return o()}export function getTask(n){return o().find(e=>e.id===n)||null}export function addTask(n){const e=o(),t=n&&n.id||Date.now().toString(36)+Math.random().toString(36).slice(2,4),r=Date.now(),u=d({...n,id:t,created:n&&n.created||r,updated:r});if(!u.cron&&!u.freq)throw new Error("缺少执行频率(freq)或 cron 表达式");return e.push(u),i(e),{id:t}}export function updateTask(n,e){const t=o(),r=t.findIndex(e=>e.id===n);return!(r<0)&&(t[r]=d({...t[r],...e,id:n,updated:Date.now()}),i(t),!0)}export function removeTask(n){const e=o(),t=e.filter(e=>e.id!==n);return i(t),t.length<e.length}export function setEnabled(n,e){return updateTask(n,{enabled:!!e})}export function touchRun(n,e=Date.now()){const t=o(),r=t.findIndex(e=>e.id===n);return!(r<0)&&(t[r].lastRunAt=e,i(t),!0)}export function crontabLine(n,r="huicai"){return`${n.cron} cd ${JSON.stringify(n.cwd)} && ${r} -p ${JSON.stringify(n.prompt)} --yolo --plain >> ${JSON.stringify(e.join(t.homedir(),".huicai","schedule-"+n.id+".log"))} 2>&1`}
|
package/src/session.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import e from"node:path";import{CONFIG_DIR as n}from"./config.js";export const SESSIONS_DIR=e.join(n,"sessions");function r(){t.mkdirSync(SESSIONS_DIR,{recursive:!0})}function o(t){return e.join(SESSIONS_DIR,t+".json")}
|
|
1
|
+
import t from"node:fs";import e from"node:path";import{CONFIG_DIR as n}from"./config.js";export const SESSIONS_DIR=e.join(n,"sessions");function r(){t.mkdirSync(SESSIONS_DIR,{recursive:!0})}function o(t){return e.join(SESSIONS_DIR,t+".json")}export function turnCount(t){return(t||[]).filter(t=>"user"===t.role).length}export function createSession({cwd:t,model:e,messages:n}){const r=Date.now();return{id:Date.now().toString(36)+"-"+Math.random().toString(36).slice(2,6),cwd:t,model:e,createdAt:r,updatedAt:r,title:"",messages:n||[],compaction:{}}}export function saveSession(e){if(e&&e.id)try{r(),e.updatedAt=Date.now(),t.writeFileSync(o(e.id),JSON.stringify(e),{mode:384})}catch(t){process.stderr.write(`(会话保存失败,不影响对话:${t.message})\n`)}}export function loadSession(e){if(!e)return null;try{const n=JSON.parse(t.readFileSync(o(e),"utf8"));return n&&Array.isArray(n.messages)?(n.id||(n.id=e),n.compaction&&"object"==typeof n.compaction||(n.compaction={}),n):null}catch{return null}}export function listSessions(n){let o;try{r(),o=t.readdirSync(SESSIONS_DIR).filter(t=>t.endsWith(".json"))}catch{return[]}const i=[];for(const r of o)try{const o=JSON.parse(t.readFileSync(e.join(SESSIONS_DIR,r),"utf8"));if(n&&o.cwd!==n)continue;i.push({id:o.id||r.replace(/\.json$/,""),title:o.title||"(未命名会话)",cwd:o.cwd,model:o.model,updatedAt:o.updatedAt||0,turns:turnCount(o.messages),user:o.user?String(o.user):""})}catch{}return i.sort((t,e)=>e.updatedAt-t.updatedAt),i}export function mostRecent(t){const e=listSessions(t);return e.length?e[0].id:null}export function deleteSession(e){try{return t.unlinkSync(o(e)),!0}catch{return!1}}export function relTime(t){if(!t)return"未知时间";const e=Math.max(0,Math.floor((Date.now()-t)/1e3));if(e<60)return e+" 秒前";const n=Math.floor(e/60);if(n<60)return n+" 分钟前";const r=Math.floor(n/60);if(r<24)return r+" 小时前";return Math.floor(r/24)+" 天前"}
|
package/src/skills.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import n from"node:path";import t from"node:os";import{fileURLToPath as r}from"node:url";export const BUNDLED_SKILLS_DIR=n.join(n.dirname(r(import.meta.url)),"..","skills");const i=new Set([".md",".markdown",".txt",".json",".yaml",".yml",".py",".js",".mjs",".cjs",".ts",".sh",".sql",".csv",".html",".css",".xml",".toml"]),o=262144;export function parseFrontmatter(e){const n={name:"",description:"",version:"",body:(e||"").trim()},t=/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(n.body);if(!t)return n;n.body=n.body.slice(t[0].length).trim();for(const e of t[1].split(/\r?\n/)){const t=/^(name|description|version)\s*:\s*(.*)$/.exec(e.trim());t&&(n[t[1]]=t[2].trim().replace(/^["']|["']$/g,""))}return n}export function skillDirs(e){const r=[];return e&&r.push(n.join(e,".huicai","skills")),r.push(n.join(t.homedir(),".huicai","skills")),r}function s(t,r="",o=0,l=[]){if(o>3||l.length>=50)return l;let c;try{c=e.readdirSync(t,{withFileTypes:!0})}catch{return l}for(const e of c){if(l.length>=50)break;if(e.name.startsWith("."))continue;const c=r?`${r}/${e.name}`:e.name;e.isDirectory()?s(n.join(t,e.name),c,o+1,l):e.isFile()&&"SKILL.md"!==c&&i.has(n.extname(e.name).toLowerCase())&&l.push(c)}return l}export function loadLocalSkills(r){const i=new Map,o=[...r?[{dir:n.join(r,".huicai","skills"),scope:"project"}]:[],{dir:n.join(t.homedir(),".huicai","skills"),scope:"global"},{dir:BUNDLED_SKILLS_DIR,scope:"builtin"}];for(const{dir:t,scope:r}of o){let o;try{o=e.readdirSync(t,{withFileTypes:!0})}catch{continue}for(const l of o){if(!l.isDirectory())continue;const o=n.join(t,l.name);let c;try{c=e.readFileSync(n.join(o,"SKILL.md"),"utf8")}catch{continue}const a=parseFrontmatter(c),p=(a.name||l.name).trim();p&&!i.has(p)&&i.set(p,{name:p,description:a.description||"",version:a.version||"",dir:o,scope:r,files:s(o)})}}return[...i.values()]}export function readSkillBody(t,r){const i=loadLocalSkills(t).find(e=>e.name===r);if(!i)return null;try{const t=e.readFileSync(n.join(i.dir,"SKILL.md"),"utf8");return{skill:i,raw:t.length>65536?t.slice(0,65536)+"\n…(正文已截断)":t}}catch{return null}}export function readSkillFileContent(t,r,i){const s=loadLocalSkills(t).find(e=>e.name===r);if(!s)throw new Error(`本地技能不存在:${r}`);const l=String(i||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,""),c=n.resolve(s.dir,l);if(!c.startsWith(n.resolve(s.dir)+n.sep))throw new Error(`路径越界:${i}`);if(e.statSync(c).size>o)throw new Error(`附件过大(>256KB):${l}`);return e.readFileSync(c,"utf8")}export function skillCatalog(e){const n=loadLocalSkills(e);if(!n.length)return"";return"以下本地技能可用 load_skill(name) 载入其操作指引后遵循执行(描述含「何时使用」,据此判断是否载入;无关任务不要载入):\n"+n.map(e=>{const n=(e.description||"(无描述)").replace(/\s+/g," ").slice(0,300);return`- ${e.name}:${n}`}).join("\n")}export function makeSkillTools(e){const n=loadLocalSkills(e);if(!n.length)return[];const t=[{readOnly:!0,spec:{type:"function",function:{name:"load_skill",description:"载入一个本地技能的操作指引(系统提示词「可用技能」目录中的名称)。载入后严格遵循其流程执行。",parameters:{type:"object",properties:{name:{type:"string",description:"技能名(见「可用技能」目录)"}},required:["name"]}}},run:async n=>{const t=String(n?.name||"").trim(),r=readSkillBody(e,t);if(!r){return`本地技能不存在:${t}(可用:${loadLocalSkills(e).map(e=>e.name).join(", ")||"无"})`}let i=r.raw;return r.skill.files.length&&(i+=`\n\n---\n本技能附带 ${r.skill.files.length} 个资源文件,需要时用 read_skill_file(skill="${t}", path=...) 读取:\n`+r.skill.files.map(e=>`- ${e}`).join("\n")),i}}];return n.some(e=>e.files.length)&&t.push({readOnly:!0,spec:{type:"function",function:{name:"read_skill_file",description:"读取本地技能附带的资源文件(先 load_skill 查看其文件清单,再按 path 读取)。",parameters:{type:"object",properties:{skill:{type:"string",description:"技能名"},path:{type:"string",description:"资源文件相对路径(技能载入结果中列出)"}},required:["skill","path"]}}},run:async n=>{try{const t=readSkillFileContent(e,String(n?.skill||"").trim(),n?.path);return`文件 ${n?.path}:\n${t}`}catch(e){return`读取失败:${e.message}`}}}),t}export function writeSkillToDir(t,r){const i=String(r?.name||"").trim();if(!i||!r?.content)throw new Error("技能数据不完整(缺 name 或 content)");const o=n.join(t,i.replace(/[^a-zA-Z0-9_-]/g,"-"));e.mkdirSync(o,{recursive:!0}),e.writeFileSync(n.join(o,"SKILL.md"),r.content,"utf8");for(const t of r.files||[]){const r=String(t?.path||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,"");if(!r||r.includes(".."))continue;const i=n.join(o,r);e.mkdirSync(n.dirname(i),{recursive:!0}),e.writeFileSync(i,t.content??"","utf8")}return o}export function readSkillDirForPush(t){const r=n.join(t,"SKILL.md"),i=e.readFileSync(r,"utf8"),l=[];for(const r of s(t))try{const i=n.join(t,r);if(e.statSync(i).size>o)continue;l.push({path:r,content:e.readFileSync(i,"utf8")})}catch{}return{content:i,filename:n.basename(t),files:l}}
|
|
1
|
+
import e from"node:fs";import n from"node:path";import r from"node:os";import{fileURLToPath as t}from"node:url";export const BUNDLED_SKILLS_DIR=n.join(n.dirname(t(import.meta.url)),"..","skills");const i=new Set([".md",".markdown",".txt",".json",".yaml",".yml",".py",".js",".mjs",".cjs",".ts",".sh",".sql",".csv",".html",".css",".xml",".toml"]),o=262144;export const SKILLS_STATE_PATH=n.join(r.homedir(),".huicai","skills-state.json");export function loadDisabledSet(){try{const n=JSON.parse(e.readFileSync(SKILLS_STATE_PATH,"utf8"));return new Set(Array.isArray(n?.disabled)?n.disabled.map(String):[])}catch{return new Set}}function l(r){e.mkdirSync(n.dirname(SKILLS_STATE_PATH),{recursive:!0}),e.writeFileSync(SKILLS_STATE_PATH,JSON.stringify({disabled:[...r].sort()},null,2)+"\n",{mode:384})}export function setSkillEnabled(e,n){const r=String(e||"").trim();if(!r)throw new Error("技能名为空");const t=loadDisabledSet();return n?t.delete(r):t.add(r),l(t),[...t]}export function parseFrontmatter(e){const n={name:"",description:"",version:"",body:(e||"").trim()},r=/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(n.body);if(!r)return n;n.body=n.body.slice(r[0].length).trim();for(const e of r[1].split(/\r?\n/)){const r=/^(name|description|version)\s*:\s*(.*)$/.exec(e.trim());r&&(n[r[1]]=r[2].trim().replace(/^["']|["']$/g,""))}return n}export function skillDirs(e){const t=[];return e&&t.push(n.join(e,".huicai","skills")),t.push(n.join(r.homedir(),".huicai","skills")),t}function s(r,t="",o=0,l=[]){if(o>3||l.length>=50)return l;let a;try{a=e.readdirSync(r,{withFileTypes:!0})}catch{return l}for(const e of a){if(l.length>=50)break;if(e.name.startsWith("."))continue;const a=t?`${t}/${e.name}`:e.name;e.isDirectory()?s(n.join(r,e.name),a,o+1,l):e.isFile()&&"SKILL.md"!==a&&i.has(n.extname(e.name).toLowerCase())&&l.push(a)}return l}export function loadLocalSkills(t){const i=new Map,o=loadDisabledSet(),l=[...t?[{dir:n.join(t,".huicai","skills"),scope:"project"}]:[],{dir:n.join(r.homedir(),".huicai","skills"),scope:"global"},{dir:BUNDLED_SKILLS_DIR,scope:"builtin"}];for(const{dir:r,scope:t}of l){let l;try{l=e.readdirSync(r,{withFileTypes:!0})}catch{continue}for(const a of l){if(!a.isDirectory())continue;const l=n.join(r,a.name);let c;try{c=e.readFileSync(n.join(l,"SKILL.md"),"utf8")}catch{continue}const d=parseFrontmatter(c),p=(d.name||a.name).trim();p&&!i.has(p)&&i.set(p,{name:p,description:d.description||"",version:d.version||"",dir:l,scope:t,files:s(l),enabled:!o.has(p)})}}return[...i.values()]}export function readSkillBody(r,t){const i=loadLocalSkills(r).find(e=>e.name===t);if(!i)return null;try{const r=e.readFileSync(n.join(i.dir,"SKILL.md"),"utf8");return{skill:i,raw:r.length>65536?r.slice(0,65536)+"\n…(正文已截断)":r}}catch{return null}}export function readSkillFileContent(r,t,i){const l=loadLocalSkills(r).find(e=>e.name===t);if(!l)throw new Error(`本地技能不存在:${t}`);const s=String(i||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,""),a=n.resolve(l.dir,s);if(!a.startsWith(n.resolve(l.dir)+n.sep))throw new Error(`路径越界:${i}`);if(e.statSync(a).size>o)throw new Error(`附件过大(>256KB):${s}`);return e.readFileSync(a,"utf8")}export function skillCatalog(e){const n=loadLocalSkills(e).filter(e=>!1!==e.enabled);if(!n.length)return"";return"以下本地技能可用 load_skill(name) 载入其操作指引后遵循执行(描述含「何时使用」,据此判断是否载入;无关任务不要载入):\n"+n.map(e=>{const n=(e.description||"(无描述)").replace(/\s+/g," ").slice(0,300);return`- ${e.name}:${n}`}).join("\n")}export function makeSkillTools(e){const n=loadLocalSkills(e).filter(e=>!1!==e.enabled);if(!n.length)return[];const r=[{readOnly:!0,spec:{type:"function",function:{name:"load_skill",description:"载入一个本地技能的操作指引(系统提示词「可用技能」目录中的名称)。载入后严格遵循其流程执行。",parameters:{type:"object",properties:{name:{type:"string",description:"技能名(见「可用技能」目录)"}},required:["name"]}}},run:async n=>{const r=String(n?.name||"").trim(),t=readSkillBody(e,r);if(!t){return`本地技能不存在:${r}(可用:${loadLocalSkills(e).filter(e=>!1!==e.enabled).map(e=>e.name).join(", ")||"无"})`}if(!1===t.skill.enabled)return`技能「${r}」已被禁用,无法载入(可在「技能·MCP」中重新启用)。`;let i=t.raw;return t.skill.files.length&&(i+=`\n\n---\n本技能附带 ${t.skill.files.length} 个资源文件,需要时用 read_skill_file(skill="${r}", path=...) 读取:\n`+t.skill.files.map(e=>`- ${e}`).join("\n")),i}}];return n.some(e=>e.files.length)&&r.push({readOnly:!0,spec:{type:"function",function:{name:"read_skill_file",description:"读取本地技能附带的资源文件(先 load_skill 查看其文件清单,再按 path 读取)。",parameters:{type:"object",properties:{skill:{type:"string",description:"技能名"},path:{type:"string",description:"资源文件相对路径(技能载入结果中列出)"}},required:["skill","path"]}}},run:async n=>{try{const r=readSkillFileContent(e,String(n?.skill||"").trim(),n?.path);return`文件 ${n?.path}:\n${r}`}catch(e){return`读取失败:${e.message}`}}}),r}export function writeSkillToDir(r,t){const i=String(t?.name||"").trim();if(!i||!t?.content)throw new Error("技能数据不完整(缺 name 或 content)");const o=n.join(r,i.replace(/[^a-zA-Z0-9_-]/g,"-"));e.mkdirSync(o,{recursive:!0}),e.writeFileSync(n.join(o,"SKILL.md"),t.content,"utf8");for(const r of t.files||[]){const t=String(r?.path||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,"");if(!t||t.includes(".."))continue;const i=n.join(o,t);e.mkdirSync(n.dirname(i),{recursive:!0}),e.writeFileSync(i,r.content??"","utf8")}return o}export function isValidSkillName(e){return/^[a-zA-Z0-9_-]{1,48}$/.test(String(e||""))}function a(e,t){if("project"===e){if(!t)throw new Error("项目级技能需要在会话/项目内创建");return n.join(t,".huicai","skills")}return n.join(r.homedir(),".huicai","skills")}export function scaffoldSkillMarkdown(e,n,r){return`---\nname: ${e}\ndescription: ${String(n||"").replace(/\r?\n/g," ").trim()||"(补充:这个技能做什么、何时使用)"}\nversion: 1.0.0\n---\n\n`+(null!=r&&String(r).trim()?String(r).trim()+"\n":`# ${e}\n\n## 何时使用\n\n(描述触发场景,让模型据此判断是否 load_skill)\n\n## 流程\n\n1. \n2. \n3. \n\n## 注意事项\n\n- \n`)}export function createSkillPackage({name:r,description:t="",scope:i="global",cwd:o=null,body:l=""}={}){const s=String(r||"").trim();if(!isValidSkillName(s))throw new Error("技能名非法:仅限字母/数字/下划线/连字符,长度 1–48");if("project"!==i&&"global"!==i)throw new Error("只能创建到项目级或全局级");if(loadLocalSkills(o).some(e=>e.name===s))throw new Error(`技能已存在:${s}`);const c=a(i,o),d=n.join(c,s);return e.mkdirSync(d,{recursive:!0}),e.writeFileSync(n.join(d,"SKILL.md"),scaffoldSkillMarkdown(s,t,l),"utf8"),{dir:d,name:s,scope:i}}export function installSkillPackage({name:r,content:t,files:i=[],scope:o="global",cwd:l=null,conflict:s="skip"}={}){const c=String(r||"").trim();if(!isValidSkillName(c))throw new Error("技能名非法:仅限字母/数字/下划线/连字符,长度 1–48(来源 SKILL.md 的 frontmatter name)");if(!String(t||"").trim())throw new Error("SKILL.md 正文为空");if("project"!==o&&"global"!==o)throw new Error("只能导入到项目级或全局级");const d=(Array.isArray(i)?i:[]).filter(e=>e&&"string"==typeof e.path),p=loadLocalSkills(l).find(e=>e.name===c&&"builtin"!==e.scope);if(p){if("overwrite"!==s)return{name:c,status:"skipped",dir:p.dir};e.writeFileSync(n.join(p.dir,"SKILL.md"),String(t),"utf8");for(const r of d){const t=String(r.path).replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,"");if(!t||t.includes(".."))continue;const i=n.join(p.dir,t);e.mkdirSync(n.dirname(i),{recursive:!0}),e.writeFileSync(i,r.content??"","utf8")}return{name:c,status:"overwritten",dir:p.dir}}const f=writeSkillToDir(a(o,l),{name:c,content:t,files:d});return{name:c,status:"imported",dir:f}}export function overwriteSkillBody(r,t,i){const o=loadLocalSkills(r).find(e=>e.name===String(t||"").trim());if(!o)throw new Error("本地技能不存在:"+t);if("builtin"===o.scope)throw new Error("内置技能只读,请先「复制到全局」再编辑");if(!String(i||"").trim())throw new Error("SKILL.md 正文不能为空");return e.writeFileSync(n.join(o.dir,"SKILL.md"),String(i),"utf8"),{dir:o.dir,name:o.name}}export function removeSkillPackage(r,t){const i=loadLocalSkills(r).find(e=>e.name===String(t||"").trim());if(!i)throw new Error("本地技能不存在:"+t);if("builtin"===i.scope)throw new Error("内置技能不可卸载(可在「技能·MCP」中禁用)");const o=n.resolve(a(i.scope,r)),s=n.resolve(i.dir);if(s!==o&&!s.startsWith(o+n.sep))throw new Error("拒绝删除技能库之外的目录");e.rmSync(s,{recursive:!0,force:!0});const c=loadDisabledSet();return c.delete(i.name)&&l(c),{name:i.name,scope:i.scope}}export function forkSkillToWritable(e,n,r="global"){const t=loadLocalSkills(e).find(e=>e.name===String(n||"").trim());if(!t)throw new Error("本地技能不存在:"+n);if("project"!==r&&"global"!==r)throw new Error("只能复制到项目级或全局级");const i=new Set(loadLocalSkills(e).map(e=>e.name));let o=t.name;for(;i.has(o);)o=(o+"-copy").slice(0,48);const l=readSkillDirForPush(t.dir),s=l.content.replace(/^(---[\s\S]*?\nname:\s*).*$/m,`$1${o}`);return{dir:writeSkillToDir(a(r,e),{name:o,content:s,files:l.files}),name:o,scope:r}}export const SKILLS_SYNC_STAMP=n.join(r.homedir(),".huicai","cache","skills-sync.json");function c(){try{return JSON.parse(e.readFileSync(SKILLS_SYNC_STAMP,"utf8"))||{}}catch{return{}}}export async function autoSyncPlatformSkills(r,t={}){if(!r?.baseUrl||!r?.apiKey)return{synced:0,skipped:0,skipped_reason:"未登录"};if(!1===r.autoSkillSync)return{synced:0,skipped:0,skipped_reason:"已关闭自动同步"};const i=c();if(!t.force&&i.at&&Date.now()-i.at<216e5)return{synced:0,skipped:0,skipped_reason:"距上次同步未满 6 小时"};const o=t.api||await import("./api.js");let l;try{l=await o.listPlatformSkills(r.baseUrl,r.apiKey)}catch(e){return{synced:0,skipped:0,skipped_reason:e.message}}!function(r){try{e.mkdirSync(n.dirname(SKILLS_SYNC_STAMP),{recursive:!0}),e.writeFileSync(SKILLS_SYNC_STAMP,JSON.stringify({...c(),...r})+"\n",{mode:384})}catch{}}({at:Date.now()});const s=new Set(loadLocalSkills(null).map(e=>e.name));let a=0,d=0;for(const e of l){const n=String(e?.name||"").trim();if(n&&isValidSkillName(n))if(s.has(n))d++;else try{const t=await o.getPlatformSkill(r.baseUrl,r.apiKey,e.id);"skipped"===installSkillPackage({name:n,content:t?.content,files:t?.files||[],scope:"global",conflict:"skip"}).status?d++:a++}catch{d++}else d++}return a&&t.onInfo&&t.onInfo(`已从平台自动同步 ${a} 个技能到本地技能库`),{synced:a,skipped:d}}export function readSkillDirForPush(r){const t=n.join(r,"SKILL.md"),i=e.readFileSync(t,"utf8"),l=[];for(const t of s(r))try{const i=n.join(r,t);if(e.statSync(i).size>o)continue;l.push({path:t,content:e.readFileSync(i,"utf8")})}catch{}return{content:i,filename:n.basename(r),files:l}}
|