@aicloud360/360-aidrive 0.8.29 → 0.8.30

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.
Files changed (2) hide show
  1. package/build/cli.js +1 -1
  2. package/package.json +1 -1
package/build/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{Command as e}from"commander";import t from"fs";import n from"path";import{fileURLToPath as r}from"url";import o,{homedir as i}from"os";import s from"qrcode-terminal";import a from"jsqr";import{Jimp as c}from"jimp";import u from"crypto";import l from"pino";import f from"iconv-lite";import{promises as d}from"dns";import{spawn as h,execFileSync as p}from"child_process";import m from"fs/promises";import{createRequire as w}from"module";function _(){try{const e=n.dirname(r(import.meta.url)),o=[n.resolve(e,"../package.json"),n.resolve(e,"../../package.json"),n.resolve(e,"../../../package.json"),n.resolve(process.cwd(),"package.json")];for(const e of o)if(t.existsSync(e)){const n=JSON.parse(t.readFileSync(e,"utf8"));if(n.cli?.name)return n.cli.name}}catch{}return"360disk"}function g(){const e=process.env.AI_CLOUD_DISK_CONFIG_DIR?.trim();if(e)return function(e){const t=e.trim();if("~"===t||t.startsWith("~/")){const e="~"===t?"":t.slice(2);return e?n.join(i(),e):i()}return t}(e);const t=_();return n.join(i(),`.${t}`)}function y(){return n.join(g(),"config.json")}function $(){return n.join(g(),"claw-auto-backup.json")}function v(){const e=g();t.existsSync(e)||t.mkdirSync(e,{recursive:!0,mode:448})}function b(){try{const e=y();if(t.existsSync(e))return JSON.parse(t.readFileSync(e,"utf8"))}catch{}return{}}function E(e){v();const n=y();t.writeFileSync(n,JSON.stringify(e,null,2),{mode:384})}function k(e){v();const n=$();t.writeFileSync(n,JSON.stringify(e,null,2),{mode:384})}function P(e){const t=b();return{apiKey:e.apiKey||process.env.API_KEY||t.api_key||"",ecsEnv:e.env||process.env.ECS_ENV||t.ecs_env||"prod",subChannel:e.subChannel||process.env.SUB_CHANNEL||t.sub_channel||"open",timeout:e.timeout?parseInt(e.timeout):void 0,retries:e.retries?parseInt(e.retries):void 0}}var S;!function(e){e[e.SUCCESS=0]="SUCCESS",e[e.GENERAL=1]="GENERAL",e[e.INVALID_ARGS=2]="INVALID_ARGS",e[e.AUTH_ERROR=3]="AUTH_ERROR",e[e.NOT_FOUND=4]="NOT_FOUND",e[e.PERMISSION_DENIED=5]="PERMISSION_DENIED",e[e.NETWORK_ERROR=6]="NETWORK_ERROR",e[e.CONFLICT=7]="CONFLICT",e[e.SERVER_ERROR=8]="SERVER_ERROR",e[e.QUOTA_EXCEEDED=10]="QUOTA_EXCEEDED",e[e.QR_INIT_FAILED=20]="QR_INIT_FAILED",e[e.QR_RENDER_FAILED=21]="QR_RENDER_FAILED",e[e.QR_POLL_TIMEOUT=22]="QR_POLL_TIMEOUT",e[e.QR_POLL_FAILED=23]="QR_POLL_FAILED",e[e.QR_PARSE_FAILED=24]="QR_PARSE_FAILED",e[e.QR_SETCOOKIE_FAILED=25]="QR_SETCOOKIE_FAILED",e[e.QR_INFO_FAILED=26]="QR_INFO_FAILED",e[e.QR_EXCHANGE_APIKEY_FAILED=27]="QR_EXCHANGE_APIKEY_FAILED"}(S||(S={}));class I extends Error{code;traceId;constructor(e,t=S.GENERAL,n){super(e),this.name="CLIError",this.code=t,this.traceId=n}}function D(e){switch(e){case 1001:case 1002:case 1003:case 3001:case 3002:case 3003:return S.AUTH_ERROR;case 3008:return S.NOT_FOUND;case 3007:case 3013:case 3129:return S.CONFLICT;case 3005:case 3006:return S.PERMISSION_DENIED;case 3010:case 3011:return S.QUOTA_EXCEEDED;default:return e>=5e3?S.SERVER_ERROR:S.GENERAL}}function O(e){const t=(e?.message||e?.toString()||"").toLowerCase();return t.includes("超时")||t.includes("timeout")||t.includes("aborted")||"AbortError"===e?.name||t.includes("econnreset")||t.includes("etimedout")||t.includes("econnrefused")||t.includes("fetch failed")||t.includes("网络")?S.NETWORK_ERROR:t.includes("api key")||t.includes("api_key")||t.includes("未配置")||t.includes("token")||t.includes("auth")||t.includes("登录")||t.includes("401")?S.AUTH_ERROR:t.includes("参数")||t.includes("argument")||t.includes("required")||t.includes("互斥")||t.includes("invalid")?S.INVALID_ARGS:t.includes("不存在")||t.includes("not found")||t.includes("404")||t.includes("no such")?S.NOT_FOUND:t.includes("权限")||t.includes("permission")||t.includes("forbidden")||t.includes("403")?S.PERMISSION_DENIED:t.includes("已存在")||t.includes("conflict")||t.includes("duplicate")||t.includes("409")?S.CONFLICT:t.includes("状态码: 5")||t.includes("500")||t.includes("502")||t.includes("503")||t.includes("服务端")?S.SERVER_ERROR:t.includes("空间不足")||t.includes("quota")||t.includes("limit")||t.includes("429")||t.includes("频率")?S.QUOTA_EXCEEDED:S.GENERAL}class N extends Error{traceId;apiErrno;constructor(e,t){super(e),this.name="ApiError",this.traceId=t?.traceId,this.apiErrno=t?.apiErrno}}function R(e){if(!e||"object"!=typeof e)return;const t=e.trace_id??e.traceId;return"string"==typeof t&&t.length>0?t:void 0}function T(e,t,n,r){if(r?.quiet)return void process.stdout.write(JSON.stringify(e)+"\n");const o={success:!0,result:e,meta:{duration_ms:Date.now()-n,command:t}};process.stdout.write(JSON.stringify(o,null,2)+"\n")}function A(e,t,n,r,o,i){if(i?.quiet)return process.stdout.write(JSON.stringify(e)+"\n"),void(process.exitCode=n||1);const s={success:!1,result:e,error:t,code:n,meta:{duration_ms:Date.now()-o,command:r}};process.stdout.write(JSON.stringify(s,null,2)+"\n"),process.exitCode=n||1}function C(e){process.stdout.write(e+"\n")}function L(e,t,n){const r=e instanceof Error?e.message:e;let o;o=e instanceof I?e.code:e instanceof N&&void 0!==e.apiErrno?D(e.apiErrno):O(e);const i={duration_ms:n?Date.now()-n:0,command:t};(e instanceof I&&e.traceId||e instanceof N&&e.traceId)&&(i.trace_id=e.traceId);process.stderr.write(JSON.stringify({success:!1,error:r,code:o,meta:i},null,2)+"\n"),process.exit(o||1)}function F(e,t,n){if(e&&"number"==typeof e.errno&&0!==e.errno){const r=D(e.errno);L(new I(e.errmsg?`${e.errmsg} (errno: ${e.errno})`:`API 错误 (errno: ${e.errno})`,r,"string"==typeof e.trace_id?e.trace_id:"string"==typeof e.traceId?e.traceId:void 0),t,n)}}function x(e,t=2){if(!e||e<=0)return"-";const n=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,n)).toFixed(t))+" "+["B","KB","MB","GB","TB"][n]}function M(e){if(!e)return"-";const t="string"==typeof e?parseInt(e):e;if(isNaN(t)||t<=0)return"-";const n=new Date(t<1e12?1e3*t:t),r=e=>String(e).padStart(2,"0");return`${n.getFullYear()}-${r(n.getMonth()+1)}-${r(n.getDate())} ${r(n.getHours())}:${r(n.getMinutes())}`}function q(e){let t=0;for(const n of e)t+=n.charCodeAt(0)>127?2:1;return t}function z(e,t){const n=t-q(e);return n>0?e+" ".repeat(n):e}function U(e,t){const n=e.map((e,n)=>{const r=t.reduce((e,t)=>Math.max(e,q(t[n]||"")),0);return Math.max(q(e),r)}),r=[];r.push(e.map((e,t)=>z(e,n[t])).join(" ")),r.push(n.map(e=>"-".repeat(e)).join(" "));for(const e of t)r.push(e.map((e,t)=>z(e||"",n[t])).join(" "));return r.join("\n")}function K(e){const t=e?.data;if(!t)return{nodes:[],total:0};const n=t.list||t.data||t.node_list||[],r=Array.isArray(n)?n:[];return{nodes:r,total:Number(t.total_count??t.total??r.length)||r.length,page:void 0!==t.page_num?Number(t.page_num):void 0}}function j(e){return!!e&&(1===e.is_dir||"1"===e.is_dir||"dir"===e.type||(1===e.type||"1"===e.type))}function B(e){const{nodes:t,total:n,page:r}=K(e);if(0===t.length)return"(空目录)";const o=U(["类型","名称","大小","修改时间","NID"],t.map(e=>{const t=j(e);return[t?"d":"-",e.name||e.fname||"-",t?"-":x(parseInt(e.count_size||e.size||"0")),M(e.modify_time||e.mtime),e.nid||"-"]}));let i=`共 ${n} 项`;return void 0===r||Number.isNaN(r)||(i+=`,第 ${r} 页`),o+"\n"+i}function W(e){const{nodes:t,total:n}=K(e);if(0===t.length)return"无搜索结果";return U(["类型","名称","大小","路径","NID"],t.map(e=>{const t=j(e);return[t?"d":"-",e.name||e.fname||"-",t?"-":x(parseInt(e.count_size||e.size||"0")),e.path||"-",e.nid||"-"]}))+"\n"+`共找到 ${n} 项`}function Y(e,t,n){if(!e||"number"!=typeof e.errno||0!==e.errno)return J(e,n);const r=e.data;if(!r||"object"!=typeof r)return J(e,n);const o=e=>null==e?String(e):"object"==typeof e?JSON.stringify(e,null,2):String(e);if("config:read"===t&&"config"in r)return`${n}\n\n${o(r.config)}`;if("config:get"===t&&"value"in r)return`${n}\n\n${o(r.value)}`;if("config:list"===t&&"keys"in r){const e=r.keys;return`${n}\n\n${Array.isArray(e)?e.join("\n"):o(e)}`}return J(e,n)}function J(e,t){if(!e)return t;if(e.data?.share_url)return`${t}\n分享链接: ${e.data.share_url}`;if(e.data?.downloadUrl||e.downloadUrl){const n=e.data?.downloadUrl||e.downloadUrl,r=e.filename||"",o=e.fileSize||"",i=e.downloadPath||"";let s=t;return r&&(s+=`\n文件名: ${r}`),o&&(s+=`\n大小: ${o}`),i&&(s+=`\n保存到: ${i}`),n&&(s+=`\n链接: ${n}`),s}if(void 0!==e.fileCount){let n=`${t}\n上传: ${e.fileCount}/${e.totalFileCount} 文件,耗时 ${e.totalTime}秒`;if(e.uploadResults?.length)for(const t of e.uploadResults)n+=`\n - ${t.name||t.uploadRes?.name||"未知"}`;return e.duplicateFiles?.length&&(n+=`\n重名文件: ${e.duplicateFiles.map(e=>e.name).join(", ")}`),n}if(e.data?.task_id||e.data?.file_path){let n=t;return e.data?.file_path&&(n+=`\n路径: ${e.data.file_path}`),e.data?.file_size&&(n+=`\n大小: ${x(e.data.file_size)}`),n}return t}function G(e){if(null==e||""===e)return;if("number"==typeof e)return Number.isFinite(e)&&e>=0?e:void 0;const t=parseInt(String(e),10);return Number.isFinite(t)&&t>=0?t:void 0}function Q(e){const t=e?.data;if(!t)return"无法获取用户信息";const n=function(e){const t=String(e.nickname??e.nick??e.name??"").trim(),n=null!=e.qid?String(e.qid):"";let r,o;if(null!=e.space_used){const t=Number(e.space_used);r=Number.isFinite(t)&&t>=0?t:void 0}else r=G(e.used_size);if(null!=e.space_total){const t=Number(e.space_total);o=Number.isFinite(t)&&t>=0?t:void 0}else o=G(e.total_size);let i="";const s=e.vip_type;i=null!=s&&""!==String(s).trim()?String(s).trim():1===e.is_vip||"1"===e.is_vip||!0===e.is_vip?(null!=e.vip_desc?String(e.vip_desc).trim():"")||"VIP用户":"普通用户";return{nickname:t,qid:n,usedBytes:r,totalBytes:o,vipLabel:i}}(t),r=void 0!==n.usedBytes?x(n.usedBytes):"-",o=void 0!==n.totalBytes?x(n.totalBytes):"-";return[`昵称: ${n.nickname||"-"}`,`QID: ${n.qid||"-"}`,`空间: ${r} / ${o}`,`VIP: ${n.vipLabel||"-"}`].join("\n")}const V=Object.freeze({__proto__:null,checkApiResult:F,executeBatch:async function(e,t){const n=[];let r=0,o=0;for(let i=0;i<e.length;i++)try{const o=await t(e[i],i);n.push({index:i,input:e[i],success:!0,result:o}),r++}catch(t){n.push({index:i,input:e[i],success:!1,error:t.message}),o++}return{total:e.length,succeeded:r,failed:o,items:n}},formatBatchResult:function(e){const t=[];t.push(`批量操作完成: 成功 ${e.succeeded}/${e.total},失败 ${e.failed}/${e.total}`);for(const n of e.items){const e=n.success?"":` (${n.error})`;t.push(` ${n.success?"✓":"✗"} [${n.index+1}] ${JSON.stringify(n.input)}${e}`)}return t.join("\n")},formatBytes:x,formatFileList:B,formatMcpConfigTextResult:Y,formatSearchResult:W,formatSimpleResult:J,formatTable:U,formatUserInfo:Q,outputError:L,outputFailureJson:A,outputJson:T,outputText:C}),X="function"==typeof a?a:a.default;async function H(e){let t=e.text;if(e.imageBuffer)try{const n=await c.read(e.imageBuffer),r={data:new Uint8ClampedArray(n.bitmap.data),width:n.bitmap.width,height:n.bitmap.height},o=X(r.data,r.width,r.height);if(!o||!o.data)throw new Error("无法从图片中识别出二维码内容");t=o.data}catch(e){throw new Error(`解析原始二维码图片失败: ${e.message}`)}if(!t)throw new Error("没有提供二维码文本或图片解析失败");return new Promise(e=>{s.generate(t,{small:!0},t=>{process.stdout.write("\n"+t+"\n"),e()})})}const Z={test:"https://qaopen.eyun.360.cn/intf.php",hgtest:"https://hg-openapi.eyun.360.cn/intf.php",prod:"https://openapi.eyun.360.cn/intf.php"};function ee(e){const t=e||process.env.ECS_ENV||"prod";return{request_url:Z[t]||Z.prod,client_env:t}}const te="https://login.360.cn/",ne="pcw_aidrive",re=`${te}?o=sso&m=getLoginQrcode&s=3&src=${ne}&qrcodeType=miniprogram&userCenter=1`,oe="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",ie="image/png";function se(e){const t=e.trim();try{return JSON.parse(t)}catch{}const n=t.match(/^[^(]+\((.+)\)\s*;?\s*$/s);if(!n)throw new I(`无法解析 JSONP 响应: ${t.substring(0,100)}`,S.QR_PARSE_FAILED);try{return JSON.parse(n[1])}catch(e){throw new I(`JSONP 内容 JSON 解析失败: ${e.message}`,S.QR_PARSE_FAILED)}}function ae(e){const t=e.headers.get("set-cookie");return t?t.split(/,(?=\s*\w+=)/):[]}function ce(e){return Object.entries(e).map(([e,t])=>`${e}=${t}`).join("; ")}function ue(e,t){for(const n of t){const t=n.match(/^([^=]+)=([^;]*)/);t&&(e[t[1].trim()]=t[2])}}function le(e){return new Promise(t=>setTimeout(t,e))}function fe(e){const t=Date.now();return`${te}?func=${e}&src=${ne}&from=${ne}&charset=UTF-8&requestScema=https&quc_sdk_version=7.3.7&quc_sdk_name=jssdk&mid=&asc=&mname=&o=sso&m=qrLogin&_=${t}`}async function de(){const e={},t=Math.random().toString().substring(2),n=`${re}&t=0.${t}`;let r;try{r=await fetch(n,{headers:{"User-Agent":oe,Referer:"https://i.360.cn/"}})}catch(e){throw new I(`获取二维码请求失败: ${e.message}`,S.QR_INIT_FAILED)}const o=function(e,t){for(const n of e){const e=n.match(new RegExp(`${t}=([^;]+)`));if(e)return e[1]}}(ae(r),"i360QRKEY");if(!o)throw new I("未能从二维码响应中获取 i360QRKEY",S.QR_INIT_FAILED);e.i360QRKEY=o;const i=Buffer.from(await r.arrayBuffer());if(i.length<100)throw new I("二维码图片数据异常",S.QR_INIT_FAILED);const s=`data:${ie};base64,${i.toString("base64")}`;return{qrKey:o,qrImageBuffer:i,qrImageDataUrl:s,cookies:e}}function he(e,r){const o=n.resolve(e.trim()),i=n.dirname(o);return t.existsSync(i)||t.mkdirSync(i,{recursive:!0,mode:448}),t.writeFileSync(o,r,{mode:384}),o}function pe(){return n.join(o.tmpdir(),`360-wechat-qr-${process.pid}-${Date.now()}.png`)}async function me(e,t){const{cookies:n}=e,r=Date.now()+1e3*t.timeoutSec,o=`jQuery${Date.now()}_${Date.now()}`;let i;for(;Date.now()<r;){await le(t.intervalMs);const e=fe(o);let r;try{const t=await fetch(e,{headers:{"User-Agent":oe,Referer:"https://i.360.cn/",Cookie:ce(n)}});ue(n,ae(t)),r=await t.text()}catch(e){throw new I(`轮询请求失败: ${e.message}`,S.QR_POLL_FAILED)}const s=se(r),a=String(s.errno);if("2"!==a){if("1020204"!==a){if("0"===a&&s.s){i=s.s;break}throw new I(`轮询返回异常 errno=${a}: ${s.errmsg||""}`,S.QR_POLL_FAILED)}process.stderr.write("已扫码,请在手机上确认登录...\n")}}if(!i)throw new I("扫码登录超时,请重试",S.QR_POLL_TIMEOUT);process.stderr.write("正在完成登录...\n");let s,a="";const c=async()=>{try{const e=await async function(e){const{q:t,t:n}={q:(r=e.cookies).Q||r.o,t:r.T||r.i};var r;if(!t||!n)return{apiKey:""};const{request_url:o,client_env:i}=ee(e.env),s=new URL(o);let a,c;s.searchParams.append("method","Oauth.getApiKeyByQT"),s.searchParams.append("client_env",i),s.searchParams.append("client_src","pcw_aidrive"),s.searchParams.append("sub_channel",e.subChannel);try{a=await fetch(s.toString(),{method:"GET",headers:{Accept:"application/json",q:t,t:n}})}catch(e){throw new Error(`换取 API Key 请求失败: ${e.message}`)}if(!a.ok)throw new Error(`换取 API Key 失败,HTTP ${a.status}`);try{c=await a.json()}catch{throw new Error("换取 API Key 失败:响应不是合法 JSON")}if(0!==c.errno)throw new Error(c.errmsg||`换取 API Key 失败 errno=${c.errno}`);return{apiKey:c.data?.api_key||c.data?.apiKey||"",extra:c.data}}({cookies:n,env:t.env,subChannel:t.subChannel});e.apiKey&&(a=e.apiKey)}catch(e){s=e?.message||String(e)}};await c();const u=function(e,t){const n=Date.now(),r=encodeURIComponent(t);return`${te}?func=${e}&src=${ne}&from=${ne}&charset=UTF-8&requestScema=https&quc_sdk_version=7.3.7&quc_sdk_name=jssdk&mid=&asc=&mname=&o=sso&m=setcookie&s=${r}&_=${n}`}(o,i);try{const e=await fetch(u,{headers:{"User-Agent":oe,Referer:"https://i.360.cn/",Cookie:ce(n)}});ue(n,ae(e));const t=se(await e.text());if("0"!==String(t.errno))throw new I(`setcookie 失败: ${t.errmsg||"未知错误"}`,S.QR_SETCOOKIE_FAILED)}catch(e){if(e instanceof I)throw e;throw new I(`setcookie 请求失败: ${e.message}`,S.QR_SETCOOKIE_FAILED)}if(a||await c(),!a)throw new I(s||"换取 API Key 失败:未从登录 Cookie 中取到有效的 Q/T,或 Oauth.getApiKeyByQT 未返回 api_key",S.QR_EXCHANGE_APIKEY_FAILED);const l=function(e){const t=Date.now();return`${te}?callback=${e}&src=${ne}&from=${ne}&charset=UTF-8&requestScema=https&quc_sdk_version=7.3.7&quc_sdk_name=jssdk&mid=&asc=&mname=&o=sso&m=info&show_name_flag=1&head_type=b&relmobile_format=hide&_=${t}`}(o);let f;try{const e=await fetch(l,{headers:{"User-Agent":oe,Referer:"https://i.360.cn/",Cookie:ce(n)}});f=se(await e.text())}catch(e){throw new I(`获取用户信息失败: ${e.message}`,S.QR_INFO_FAILED)}if(!f.qid)throw new I("登录校验失败:未获取到用户 QID",S.QR_INFO_FAILED);return{qid:f.qid,username:f.username||f.userName,nickname:f.nickname,mobile:f.mobile,ssoCookies:n,apiKey:a}}function we(){const r=new e("auth").description("鉴权管理").enablePositionalOptions().passThroughOptions(),o=e=>e.option("--format <type>","输出格式 (json/text)","json").option("--quiet","是否静默输出",!1);return o(r.command("login")).description("登录:保存 API Key 到本地配置").requiredOption("--api-key <api_key>","API 密钥").option("--env <env>","环境 (prod/test)","prod").option("--sub-channel <channel>","子渠道","open").action(e=>{const t=Date.now();try{const n=b();n.api_key=e.apiKey,n.login_mode="api_key",delete n.sso_login_at,delete n.sso_user,delete n.sso_cookies,e.env&&(n.ecs_env=e.env),e.subChannel&&(n.sub_channel=e.subChannel),E(n);const r=e;"text"===r.format?C(`登录成功!配置已保存到 ${y()}`):T({message:"登录成功",configPath:y()},"auth login",t,{quiet:r.quiet})}catch(e){L(e,"auth login",t)}}),o(r.command("login-wechat")).description("使用微信扫码登录").option("--timeout <sec>","轮询总超时秒数","120").option("--interval <ms>","轮询间隔毫秒","1500").option("--qr-mode <mode>","二维码呈现:terminal(终端字符码)或 file(PNG),默认 terminal","terminal").option("--qr-file <path>","与 --qr-mode=file 联用:PNG 输出路径(未指定则写入系统临时目录)").option("--env <env>","环境 (prod/test)","prod").option("--sub-channel <channel>","子渠道","open").action(async e=>{const t=Date.now(),r=e,o=process.argv.includes("--format")&&"json"===r.format;try{const i=String(e.qrMode||"terminal").toLowerCase();if("terminal"!==i&&"file"!==i)return void L(new I("无效 --qr-mode,仅支持 terminal 或 file",S.INVALID_ARGS),"auth login-wechat",t);const s={timeoutSec:parseInt(e.timeout,10),intervalMs:parseInt(e.interval,10),env:e.env,subChannel:e.subChannel,qrPresentMode:"file"===i?"file":"terminal",qrFilePath:e.qrFile};if(!o&&!r.quiet){const o=await async function(e){const t=await de();if("file"===e.qrPresentMode){process.stderr.write("正在获取登录二维码...\n");const r=he(e.qrFilePath?.trim()?n.resolve(e.qrFilePath.trim()):pe(),t.qrImageBuffer);process.stderr.write(`二维码已写入: ${r}\n请使用微信扫描该 PNG 图片完成登录\n`)}else{process.stderr.write("正在获取登录二维码...\n");const e=`https://login.360.cn/?o=sso&m=getLoginQrcode&s=3&src=${ne}&qrcodeType=miniprogram&userCenter=1&qrkey=${t.qrKey}`;try{await H({text:e,imageBuffer:t.qrImageBuffer})}catch(t){try{await H({text:e})}catch(e){throw new I(`二维码渲染失败: ${e.message}`,S.QR_RENDER_FAILED)}}process.stderr.write("请使用微信扫描上方二维码登录\n")}return await me(t,e)}(s),i=b();i.api_key=o.apiKey,i.ecs_env=e.env,i.sub_channel=e.subChannel,i.login_mode="wechat_qr",i.sso_login_at=Date.now(),i.sso_user={qid:o.qid,username:o.username,nickname:o.nickname,mobile:o.mobile},i.sso_cookies=o.ssoCookies,E(i);const a={message:"微信扫码登录成功",login_mode:"wechat_qr",qid:o.qid,username:o.username,nickname:o.nickname,configPath:y()};return void("text"===r.format?C(`微信扫码登录成功\nQID: ${o.qid}\n昵称: ${o.nickname||"-"}\n配置已保存到 ${y()}`):T(a,"auth login-wechat",t,{quiet:r.quiet}))}const a=await de(),c=parseInt(e.timeout,10);let u;if("file"===i){const t="string"==typeof e.qrFile?e.qrFile.trim():"",r=he(t?n.resolve(t):pe(),a.qrImageBuffer);u={type:"qr_file",qr_key:a.qrKey,qr_image_path:r,qr_image_mime:ie,expires_in:c}}else u={type:"qr",qr_key:a.qrKey,qr_image_data_url:a.qrImageDataUrl,qr_image_mime:ie,qr_image_base64:a.qrImageBuffer.toString("base64"),expires_in:c};process.stdout.write(`${JSON.stringify(u)}\n`);const l=await me(a,{timeoutSec:parseInt(e.timeout,10),intervalMs:parseInt(e.interval,10),env:e.env,subChannel:e.subChannel}),f=b();f.api_key=l.apiKey,f.ecs_env=e.env,f.sub_channel=e.subChannel,f.login_mode="wechat_qr",f.sso_login_at=Date.now(),f.sso_user={qid:l.qid,username:l.username,nickname:l.nickname,mobile:l.mobile},f.sso_cookies=l.ssoCookies,E(f);const d={success:!0,result:{message:"微信扫码登录成功",login_mode:"wechat_qr",qid:l.qid,username:l.username,nickname:l.nickname,configPath:y()},meta:{duration_ms:Date.now()-t,command:"auth login-wechat"}};process.stdout.write(`${JSON.stringify(d)}\n`)}catch(e){L(e,"auth login-wechat",t)}}),o(r.command("whoami")).description("查看当前鉴权状态").action(e=>{const t=Date.now();try{const n=b(),r=e,o={logged_in:!!n.api_key,login_mode:n.login_mode||(n.api_key?"api_key":void 0),api_key:n.api_key?n.api_key.substring(0,10)+"***":void 0,ecs_env:n.ecs_env||process.env.ECS_ENV||"prod",sub_channel:n.sub_channel||process.env.SUB_CHANNEL||"open",sso_user:n.sso_user,sso_login_at:n.sso_login_at,config_path:y()};if("text"===r.format)if(o.logged_in){const e=["已登录",`登录方式: ${o.login_mode||"-"}`,`API Key: ${o.api_key}`,`环境: ${o.ecs_env}`,`渠道: ${o.sub_channel}`];o.sso_user?.qid&&e.push(`QID: ${o.sso_user.qid}`),o.sso_user?.nickname&&e.push(`昵称: ${o.sso_user.nickname}`),C(e.join("\n"))}else C("未登录。请使用 auth login --api-key <API_KEY> 或 auth login-wechat 登录");else T(o,"auth whoami",t,{quiet:r.quiet})}catch(e){L(e,"auth whoami",t)}}),o(r.command("logout")).description("退出登录:清除本地配置").action(e=>{const n=Date.now();try{!function(){try{const e=y();t.existsSync(e)&&t.unlinkSync(e)}catch{}}();const r=e;"text"===r.format?C("已退出登录,本地配置已清除"):T({message:"已退出登录"},"auth logout",n,{quiet:r.quiet})}catch(e){L(e,"auth logout",n)}}),r}function _e(e,t="e7b24b112a44fdd9ee93bdf998c6ca0e"){const n=Object.keys(e).sort().map(t=>{const n=function(e){return encodeURIComponent(e).replace(/%20/g,"+").replace(/[!'()*~]/g,e=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`)}(e[t]);return`${t}=${n}`});let r=n.join("&");return r+=t,o=r,u.createHash("md5").update(o,"utf8").digest("hex");var o}const ge="production"===process.env.NODE_ENV,ye="true"===process.env.LOG_TO_FILE,$e=process.env.LOG_FILE_PATH||"/data/logs/ecs-mcp/app.log",ve=(process.env.LOG_TIME_FORMAT||"epoch").toLowerCase();function be(){const e=new Date,t=new Intl.DateTimeFormat("en-CA",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}).formatToParts(e).reduce((e,t)=>("literal"!==t.type&&(e[t.type]=t.value),e),{}),n=String(e.getMilliseconds()).padStart(3,"0");return`,"time":"${`${t.year}-${t.month}-${t.day}T${t.hour}:${t.minute}:${t.second}.${n}+08:00`}"`}let Ee;if(ye)Ee=l.destination({dest:$e,minLength:512,sync:!1});else{const e=process.env.LOG_STDERR,t="string"==typeof e&&e.length>0,n=Array.isArray(process.argv)?process.argv.slice(2):[],r=n.includes("--stdio")||n.includes("--all"),o=t?"true"===e.toLowerCase():r;Ee=l.destination({fd:o?2:1,minLength:256,sync:!0})}const ke=l({level:process.env.LOG_LEVEL||(ge?"info":"debug"),base:{service:process.env.APP_NAME||"ecs-mcp",version:process.env.APP_VERSION||"0.0.0",log_type:"app"},formatters:{level:(e,t)=>({level:e,level_number:t})},redact:{paths:["req.headers.authorization","authInfo.token","authInfo.access_token","req.body.params.arguments.access_token","req.body.params.arguments.token"],censor:"***"},timestamp:"iso"===ve?l.stdTimeFunctions.isoTime:"epoch"===ve?l.stdTimeFunctions.epochTime:"beijing"===ve||"cst"===ve||"asia/shanghai"===ve?be:l.stdTimeFunctions.isoTime},Ee);function Pe(){try{Ee?.flushSync?.()}catch{}try{Ee?.end?.()}catch{}}async function Se(e,t){const n=function(e){let t="",n="",r="",o="",i="";e&&(e.apiKey&&(t=e.apiKey),e.subChannel&&(n=e.subChannel),e.q&&(r=e.q),e.t&&(o=e.t),e.clientSrc&&(i=e.clientSrc)),t||(t=process.env.API_KEY||""),n||(n=process.env.SUB_CHANNEL||"open"),i||(i=process.env.CLIENT_SRC||"default");const{client_env:s}=ee(e?.ecsEnv);return{apiKey:t,clientEnv:s,clientSrc:i,subChannel:n,q:r,t:o}}(t);if(!n.apiKey&&!n.q&&!n.t)throw new Error("未配置API_KEY环境变量");try{const{request_url:r}=ee(t?.ecsEnv),o=n.subChannel,i=new URL(r);let s={};const a=e.extraParams;a&&"file-upload-stdio"===a.toolName?(s.method=a.method,s.client_env=a.clientEnv,s.client_src=a.clientSrc,s.qid=a.qid,s.grant_type=a.grantType,s.sub_channel=o):(s.method="Oauth.getAccessTokenByApiKeyOrQT",s.client_env=n.clientEnv,s.client_src=n.clientSrc,s.grant_type="authorization_code",s.sub_channel=o,n.apiKey&&(s.api_key=n.apiKey)),Object.entries(s).forEach(([e,t])=>{i.searchParams.append(e,t)});const c={Accept:"application/json"};n.apiKey&&(c.api_key=n.apiKey),n.q&&(c.q=n.q),n.t&&(c.t=n.t);const u=await fetch(i.toString(),{method:"GET",headers:c});if(!u.ok)throw new Error(`鉴权请求失败,状态码: ${u.status}`);const l=await u.json(),f=R(l);if(0!==l.errno)throw ke.debug({trace_id:f,errno:l.errno,msg:"oauth response"},"oauth response error"),new N(`鉴权请求返回错误: ${l.errmsg}`,{traceId:f,apiErrno:l.errno});ke.debug({trace_id:f,errno:0},"oauth response");const{access_token:d,qid:h,token:p}=l.data;return{access_token:d,qid:h,token:p,sub_channel:o}}catch(e){throw ke.error({err:e},"获取鉴权信息失败"),e}}async function Ie(e={},t){ke.debug({transportAuthInfo:t?{hasApiKey:!!t.apiKey,apiKey:t.apiKey||"",subChannel:t.subChannel,ecsEnv:t.ecsEnv,q:t.q||"",t:t.t||""}:void 0},"getAuthInfo");const n=await Se(e,t);if(e.extraParams&&"file-upload-stdio"===e.extraParams.toolName)return n.qid=e.extraParams.qid,n;const r=_e(function(e,t,n={}){const r={};if(e.access_token&&(r.access_token=String(e.access_token)),t&&(r.method=String(t)),e.qid&&(r.qid=String(e.qid)),n)for(const[e,t]of Object.entries(n))null!=t&&(r[String(e)]=String(t));return{...r}}({access_token:n.access_token,qid:n.qid},e.method||"",e.extraParams));return n.sign=r,n}process.on("uncaughtException",e=>{ke.error({err:e},"uncaught exception"),Pe(),process.exit(1)}),process.on("unhandledRejection",e=>{ke.error({err:e},"unhandled rejection"),Pe(),process.exit(1)}),process.on("SIGINT",()=>{Pe(),process.exit(0)}),process.on("SIGTERM",()=>{Pe(),process.exit(0)}),process.on("beforeExit",()=>{Pe()});class De{apiKey;ecsEnv;subChannel;timeout;retries;retryDelay;constructor(e){this.apiKey=e.apiKey,this.ecsEnv=e.ecsEnv||"prod",this.subChannel=e.subChannel||"open",this.timeout=e.timeout||3e4,this.retries=e.retries||0,this.retryDelay=e.retryDelay||1e3}getTransportAuthInfo(){return{apiKey:this.apiKey,ecsEnv:this.ecsEnv,subChannel:this.subChannel}}logApiResponse(e,t){if(!e)return;const n=R(t),r=t&&"object"==typeof t&&"errno"in t?t.errno:void 0;ke.debug({apiMethod:e,trace_id:n,errno:"number"==typeof r?r:void 0},"api response")}throwIfApiError(e,t="API请求失败"){if(!e||"object"!=typeof e)return;if("number"!=typeof e.errno||0===e.errno)return;const n=R(e);throw new N(e.errmsg||t,{traceId:n,apiErrno:e.errno})}async getAuth(e={}){const t=this.getTransportAuthInfo(),n=await Ie(e,t);return n.request_url=ee(this.ecsEnv).request_url,n}isRetryable(e){return"AbortError"===e.name||("ECONNRESET"===e.code||"ETIMEDOUT"===e.code||"ECONNREFUSED"===e.code||!!e.message?.includes("状态码: 5"))}async fetchWithRetry(e,t=0){const{url:n,init:r}=e(),o=new AbortController,i=setTimeout(()=>o.abort(),this.timeout),s={...r,signal:o.signal};try{const e=await fetch(n,s);if(clearTimeout(i),!e.ok)throw new Error(`API 请求失败,状态码: ${e.status}`);const t=await e.text();try{return JSON.parse(t)}catch{throw new Error(`无法解析API响应: ${t.substring(0,100)}...`)}}catch(n){if(clearTimeout(i),t<this.retries&&this.isRetryable(n)){const r=this.retryDelay*Math.pow(2,t);return ke.debug({attempt:t+1,delay:r,error:n.message},"请求失败,准备重试"),await new Promise(e=>setTimeout(e,r)),this.fetchWithRetry(e,t+1)}if("AbortError"===n.name)throw new Error(`请求超时 (${this.timeout}ms)`);throw n}}async apiGet(e,t){const n=t.method,r=await this.fetchWithRetry(()=>{const n=new URL(e.request_url||"");return Object.entries(t).forEach(([e,t])=>{n.searchParams.append(e,String(t))}),{url:n.toString(),init:{method:"GET",headers:{"Access-Token":e.access_token||""}}}});return this.logApiResponse(n,r),r}async apiPost(e,t,n={}){const r=t.method,o=await this.fetchWithRetry(()=>{const r=new URL(e.request_url||"");Object.entries(t).forEach(([e,t])=>{r.searchParams.append(e,String(t))});const o=new URLSearchParams;return Object.entries(n).forEach(([e,t])=>{o.append(e,String(t))}),{url:r.toString(),init:{method:"POST",headers:{"Access-Token":e.access_token||"","Content-Type":"application/x-www-form-urlencoded"},body:o}}});return this.logApiResponse(r,o),o}buildBaseParams(e,t){return{method:t,access_token:e.access_token||"",qid:e.qid||"",sign:e.sign||"",sub_channel:e.sub_channel}}get logger(){return ke}}class Oe extends De{async info(){const e=await this.getAuth({}),t=this.buildBaseParams(e,"User.getUserDetail");return t.sign="",await this.apiGet(e,t)}}class Ne extends De{async list(e={}){const{path:t="/",page:n=0,page_size:r=50}=e;let o=t||"/";"/"!==o&&(o.startsWith("/")||(o="/"+o),o.endsWith("/")||(o+="/"));const i={path:o,page:n,page_size:r},s=await this.getAuth({method:"File.getList",extraParams:i}),a={...this.buildBaseParams(s,"File.getList")};for(const[e,t]of Object.entries(i))"access_token"!==e&&(a[e]=String(t));return await this.apiGet(s,a)}async mkdir(e){const t={fname:e},n=await this.getAuth({method:"File.mkdir",extraParams:t}),r=this.buildBaseParams(n,"File.mkdir");return await this.apiPost(n,{},{...r,fname:e})}}const Re="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";function Te(e){let t=String(e||"").trim();return t=t.replace(/^\uFEFF/,""),t.startsWith("-e ")&&(t=t.slice(3).trim(),(t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))&&(t=t.slice(1,-1))),t.replace(/\\r\\n/g,"\r\n").replace(/\\n/g,"\n")}async function Ae(){if(process.stdin.isTTY){if("win32"===process.platform)throw new Error("--stdin 模式需要管道输入。Windows cmd 勿用 echo -e(会把 -e 写入内容且编码易乱码),请用 --content、或 chcp 65001 后 echo、或 type/PowerShell Get-Content 读 UTF-8 文件管道;追加/保存示例:type x.md | 360disk file save --stdin ...");throw new Error('--stdin 模式需要管道输入,例如: echo "content" | 360disk file save --stdin')}const e=[];for await(const t of process.stdin)e.push(Buffer.from(t));return function(e){if(0===e.length)return"";const t=e=>e.replace(/^\uFEFF/,"");if("win32"!==process.platform)return t(e.toString("utf-8"));const n=t(e.toString("utf-8"));if(!/\uFFFD/.test(n))return n;try{const n=t(f.decode(e,"gbk"));if(!/\uFFFD/.test(n))return n}catch{}return n}(Buffer.concat(e))}async function Ce(){const e=await Ae();return Te(e).split(/\r?\n/).map(e=>e.trim()).filter(Boolean)}class Le extends De{async move(e){const{src_name:t,new_name:n}=e,r={src_name:t,new_name:n},o=await this.getAuth({method:"File.move",extraParams:r}),i=this.buildBaseParams(o,"File.move");return await this.apiPost(o,{},{...i,src_name:t,new_name:n})}async rename(e){const{src_name:t,new_name:n}=e,r={src_name:t,new_name:n},o=await this.getAuth({method:"File.rename",extraParams:r}),i=this.buildBaseParams(o,"File.rename");return await this.apiPost(o,{},{...i,src_name:t,new_name:n})}async delete(e){const t=await this.getAuth({method:"File.delete"}),n=this.buildBaseParams(t,"File.delete");return await this.apiPost(t,{},{...n,fname:e})}async transOrCopy(e){const{src_name:t,new_path:n,is_delete:r,is_replace:o=0,src_ks_id:i,new_ks_id:s}=e;if(0!==r&&1!==r)throw new Error("is_delete 仅支持 0 或 1");if(0!==o&&1!==o)throw new Error("is_replace 仅支持 0 或 1");const a={src_name:t,new_path:n,is_delete:r},c=await this.getAuth({method:"File.transOrCopy",extraParams:a}),u={...this.buildBaseParams(c,"File.transOrCopy"),src_name:t,new_path:n,is_delete:String(r),is_replace:String(o)};return i&&(u.src_ks_id=i),s&&(u.new_ks_id=s),await this.apiPost(c,{},u)}async countOriginSize(e){const{path:t}=e;if(!t||!t.startsWith("/"))throw new Error("path 必须以 / 开头");const n={path:t},r=await this.getAuth({method:"File.countFileOriginSize",extraParams:n}),o=this.buildBaseParams(r,"File.countFileOriginSize");return await this.apiPost(r,{},{...o,path:t})}async clearDir(e){const{fname:t}=e;if(!t)throw new Error("fname 不能为空");if(t.includes("|"))throw new Error("File.clearDir 仅支持单个目录路径,勿用英文竖线(|)拼接;竖线会被服务端视为路径非法字符(errno:3022)。请对每个目录分别调用");const n=await this.getAuth({method:"File.clearDir"}),r=this.buildBaseParams(n,"File.clearDir");return await this.apiPost(n,{},{...r,fname:t})}async config(e){const{path:t,command:n,type:r,key:o,value:i,content:s}=e;if(!t||!t.startsWith("/"))throw new Error("path 必须以 / 开头");const a="yml"===r?"yaml":r,c={path:t,command:n,type:a},u=await this.getAuth({method:"MCP.config",extraParams:c}),l={...this.buildBaseParams(u,"MCP.config"),path:t,command:n,type:a};return void 0!==o&&(l.key=o),void 0!==i&&(l.value=i),void 0!==s&&(l.content=s),await this.apiPost(u,{},l)}}const Fe=["http:","https:"],xe=/[\\@]/,Me=[/^127\./,/^10\./,/^11\./,/^172\.(1[6-9]|2\d|3[01])\./,/^192\.168\./,/^169\.254\./,/^224\./,/^0\./,/^255\./,/^::1$/,/^fe80::/i,/^fc00::/i,/^fd00::/i,/^localhost$/i,/\.local$/i,/\.lan$/i,/\.internal$/i,/\.intranet$/i,/\.corp$/i,/\.home$/i],qe=[22,23,25,53,135,139,445,1433,1521,3306,3389,5432,6379,9200,11211,27017];async function ze(e,t=5e3){const n=function(e){if(!e||"string"!=typeof e)return{isValid:!1,error:"URL不能为空"};try{const n=e.trim();if(xe.test(n))return{isValid:!1,error:"URL包含不安全的字符"};const r=new URL(n);if(!Fe.includes(r.protocol))return{isValid:!1,error:"不支持的协议,只支持 http/https"};const o=r.hostname.toLowerCase();if(!o)return{isValid:!1,error:"主机名不能为空"};for(const e of Me)if(e.test(o))return{isValid:!1,error:"禁止访问受限地址"};if(Ue(o)&&!Ke(o))return{isValid:!1,error:"禁止访问受限IP地址"};if(r.port){const e=parseInt(r.port,10);if(qe.includes(e))return{isValid:!1,error:"禁止访问受限端口"}}return t=r.pathname,[/\.\./,/%2e%2e/i,/%252e%252e/i,/\/etc\//i,/\/proc\//i,/\/sys\//i,/\/dev\//i].some(e=>e.test(t))?{isValid:!1,error:"路径包含不安全的内容"}:{isValid:!0,normalizedUrl:r.toString(),originalHostname:o}}catch(e){return{isValid:!1,error:"URL格式无效"}}var t}(e);if(!n.isValid)return n;try{const r=new URL(e).hostname.toLowerCase();if(Ue(r))return{...n,resolvedIP:r,originalHostname:r,needsHostHeader:!1};let o=[];try{const e=d.lookup(r,{all:!0}),n=new Promise((e,n)=>{setTimeout(()=>n(new Error("DNS解析超时")),t)});o=(await Promise.race([e,n])).map(e=>e.address)}catch(e){return{isValid:!1,error:"域名解析失败"}}for(const e of o){for(const t of Me)if(t.test(e))return{isValid:!1,error:"域名解析到受限IP地址"};if(!Ke(e))return{isValid:!1,error:"域名解析到受限IP地址"}}return{isValid:!0,normalizedUrl:n.normalizedUrl,resolvedIP:o[0],resolvedIPs:o,originalHostname:r,needsHostHeader:!1}}catch(e){return{isValid:!1,error:"URL验证过程中发生错误"}}}function Ue(e){if(/^(\d{1,3}\.){3}\d{1,3}$/.test(e)){return e.split(".").every(e=>{const t=parseInt(e,10);return t>=0&&t<=255})}return/^([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}$/i.test(e)}function Ke(e){for(const t of Me)if(t.test(e))return!1;return!0}class je extends De{normalizeUploadResult(e){return{id:e?.id,name:e?.name||e?.uploadRes?.name,path:e?.uploadRes?.name,fid:e?.fid,size:e?.size||e?.uploadRes?.count_size,uploadRes:e?.uploadRes?{nid:e.uploadRes.nid,qid:e.uploadRes.qid,name:e.uploadRes.name,count_size:e.uploadRes.count_size,file_hash:e.uploadRes.file_hash,create_time:e.uploadRes.create_time,modify_time:e.uploadRes.modify_time}:void 0}}normalizeRemoteDir(e){let t=String(e||"").trim();if(!t)throw new Error("远端目录不能为空");return t.startsWith("/")||(t="/"+t),t.endsWith("/")||(t+="/"),t}toPosixRelativePath(e){return e.split(n.sep).join(n.posix.sep)}getMissingUploadPath(e){const t=e?.message||"",r="文件不存在: ";if(t.startsWith(r))return n.resolve(t.slice(7).trim())}async getSysConfig(){const e=await this.getAuth({method:"Oauth.getSysConfig"}),t=this.buildBaseParams(e,"Oauth.getSysConfig"),n=await this.apiGet(e,t);return this.throwIfApiError(n,"获取系统云控配置失败"),n}async search(e){const{key:t="",file_category:n=-1,page:r=1,page_size:o=20}=e;if(!t&&-1===n)throw new Error("必须提供搜索关键词(key)或指定文件类型(file_category)");const i={file_category:n,key:t,page:r,page_size:o},s=await this.getAuth({method:"File.searchList",extraParams:i}),a=this.buildBaseParams(s,"File.searchList"),c={};for(const[e,t]of Object.entries(i))"access_token"!==e&&(c[e]=String(t));return await this.apiPost(s,a,c)}async share(e){const t=await this.getAuth({method:"Share.preShare"}),n=this.buildBaseParams(t,"Share.preShare");return await this.apiPost(t,{},{...n,paths:e})}async getDownloadUrl(e){const{nid:t,fpath:n}=e;if(void 0===t&&!n)throw new Error("必须提供nid或fpath中的一个参数");const r={};void 0!==t?r.nid=t:n&&(r.fpath=n);const o=await this.getAuth({method:"MCP.getDownLoadUrl",extraParams:r}),i={...this.buildBaseParams(o,"MCP.getDownLoadUrl")};return void 0!==t?i.nid=String(t):n&&(i.fpath=n),await this.apiPost(o,{},i)}async getNodeInfoByNid(e){const{nid:t,ks_ext:n=0}=e;if(!t)throw new Error("nid 不能为空");if(0!==n&&1!==n)throw new Error("ks_ext 仅支持 0 或 1");const r={nid:t},o=await this.getAuth({method:"File.getNodeInfoByNid",extraParams:r}),i=this.buildBaseParams(o,"File.getNodeInfoByNid");return await this.apiGet(o,{...i,nid:t,ks_ext:String(n)})}async save(e){const{url:t,content:n,upload_path:r,file_name:o,is_rename:i=1}=e;if(!t&&!n||t&&n)throw new Error("url 与 content 互斥,必须且只能传一个");if(0!==i&&1!==i)throw new Error("is_rename 仅支持 0 或 1");if(t){if(t.includes("|"))throw new Error("MCP.saveFile 仅支持单个下载 URL,请勿使用英文竖线(|)连接多个地址;请对每个 URL 分别执行保存");const e=await ze(t.trim(),5e3);if(!e.isValid)throw new Error(`URL安全验证失败: ${e.error}`)}const s=await this.getAuth({method:"MCP.saveFile"}),a=this.buildBaseParams(s,"MCP.saveFile"),c={};r&&(c.upload_path=r),t?c.url=t:n&&(c.content=n),o&&(c.file_name=o),c.is_rename=String(i);const u=await this.apiPost(s,a,c);if(u&&0===u.errno){const e=Array.isArray(u.data)?u.data:u.data?[u.data]:[];if(0===e.length)throw new Error("保存文件失败: 未获取到任何任务信息");const t=[],n=[];for(const r of e){const e=r.task_id;if(e)try{t.push(await this.pollTaskStatus(s,e))}catch(t){n.push(`任务 ${e} 失败: ${t.message}`)}else n.push(`URL ${r.url||"未知"} 未获取到任务ID`)}if(0===t.length&&n.length>0)throw new Error(`文件保存失败: ${n.join("; ")}`);return 1===t.length?t[0]:{errno:0,errmsg:n.length>0?`部分任务失败: ${n.join("; ")}`:"",data:{tasks:t,is_multi:!0}}}throw new N(u?.errmsg||"API请求失败",{traceId:R(u),apiErrno:"number"==typeof u?.errno?u.errno:void 0})}async appendContent(e){const{path:t,content:n}=e;if(!t||!t.startsWith("/"))throw new Error("path 必须以 / 开头");if("string"!=typeof n||0===n.length)throw new Error("content 不能为空");const r={path:t,content:n},o=await this.getAuth({method:"File.appendContent",extraParams:r}),i=this.buildBaseParams(o,"File.appendContent");return await this.apiPost(o,{},{...i,path:t,content:n})}async detectFileExists(e){const{path:t,files:n}=e;if(!t||!t.startsWith("/"))throw new Error("path 必须以 / 开头");if(!Array.isArray(n)||0===n.length)throw new Error("files 不能为空");for(const e of n){if(!e?.fname||"string"!=typeof e.fname)throw new Error("files[].fname 必须为非空字符串");if(!Number.isFinite(e.fsize)||e.fsize<0)throw new Error("files[].fsize 必须为大于等于 0 的数字")}const r=JSON.stringify(n),o={path:t,data:r},i=await this.getAuth({method:"Sync.detectFileExists",extraParams:o}),s=this.buildBaseParams(i,"Sync.detectFileExists");return await this.apiGet(i,{...s,path:t,data:r})}async queryTaskStatus(e,t){const n={method:"MCP.query",qid:e.qid||"",access_token:e.access_token||"",sign:e.sign||""};return await this.apiPost(e,n,{task_id:t})}async pollTaskStatus(e,t,n=1e3,r=120){let o,i=0;for(;i<r;){if(i++,o=await this.queryTaskStatus(e,t),0!==o.errno)throw new N(o.errmsg||"查询任务状态失败",{traceId:R(o),apiErrno:o.errno});const r=o.data?.status;if(2===r)return o;if(3===r)throw new N(o.data?.error||"文件保存失败",{traceId:R(o),apiErrno:"number"==typeof o.errno?o.errno:void 0});await new Promise(e=>setTimeout(e,n))}return o}async upload(e){const{filePaths:n,uploadPath:r="/",is_rename:o}=e,i=void 0!==o?String(o):void 0;if(!n||0===n.length)throw new Error("filePaths 为必填参数且不能为空");if(void 0!==o&&0!==o&&1!==o)throw new Error("is_rename 仅支持 0 或 1");for(const e of n)if(!t.existsSync(e))throw new Error(`文件不存在: ${e}`);const s=await this.getAuth({});let a;try{const e=await import("@aicloud360/sec-sdk-node");a=e.UploadNode}catch{throw new Error("请先安装 @aicloud360/sec-sdk-node: npm install @aicloud360/sec-sdk-node")}const c={qid:s.qid||"0",token:s.token||"",access_token:s.access_token||"",env:this.ecsEnv,path:r,is_rename:i,preventAutoTokenRefresh:!1,retryForTokenError:!0,tokenRefreshCallback:async()=>{this.logger.info("SDK 触发 token 刷新");const e=await this.getAuth({});return{token:e.token,access_token:e.access_token}}};return new Promise((e,t)=>{const o=Date.now(),s=[],u=[],l=[],f=new a(c,{success:e=>{s.push(this.normalizeUploadResult(e))},progress:()=>{},error:(e,t)=>{const n=t?.errmsg||t?.message||JSON.stringify(t);u.push({fileName:e?.name||"未知文件",error:n})},duplicateList:e=>{l.push(...e.map(e=>({name:e.name,nid:e.nid,size:e.count_size,path:e.path||r})))},complete:()=>{if(u.length>0&&0===s.length){const e=new Error(`所有文件上传失败: ${u.map(e=>`${e.fileName}: ${e.error}`).join("; ")}`);return e.uploadErrors=u,e.primaryUploadError=u[0]?.error,void t(e)}e({uploadResults:s,uploadErrors:u,duplicateFiles:l,totalTime:((Date.now()-o)/1e3).toFixed(2),fileCount:s.length,totalFileCount:n.length})}});void 0!==i&&f.on("filequeue",(e,t)=>{t&&"object"==typeof t&&(t.is_rename=i,t.file&&"object"==typeof t.file&&(t.file.is_rename=i))}),f.addWaitFile(n)})}async uploadTree(e){const{rootDir:r,filePaths:o,uploadRoot:i,is_rename:s}=e,a=Date.now();if(!r)throw new Error("rootDir 为必填参数");if(!o||0===o.length)throw new Error("filePaths 为必填参数且不能为空");const c=n.resolve(r),u=this.normalizeRemoteDir(i),l=new Map;for(const e of o){const t=n.resolve(e),r=this.toPosixRelativePath(n.relative(c,t));if(!r||".."===r||r.startsWith("../")||n.posix.isAbsolute(r))throw new Error(`文件不在 rootDir 下: ${t}`);const o=n.posix.dirname(r),i="."===o?u:this.normalizeRemoteDir(n.posix.join(u,o)),s=l.get(i)||[];s.push({absolutePath:t,relativePath:r}),l.set(i,s)}const f=[],d=[],h=[];for(const[e,r]of l){let o=[...r];for(;o.length>0;){const r=[];for(const n of o)t.existsSync(n.absolutePath)?r.push(n):d.push({absolutePath:n.absolutePath,relativePath:n.relativePath,remoteDir:e,error:`文件不存在: ${n.absolutePath}`});if(o=r,0===o.length)break;let i;try{i=await this.upload({filePaths:o.map(e=>e.absolutePath),uploadPath:e,is_rename:s})}catch(t){const r=this.getMissingUploadPath(t);if(r){const n=o.findIndex(e=>e.absolutePath===r);if(n>=0){const[r]=o.splice(n,1);d.push({absolutePath:r.absolutePath,relativePath:r.relativePath,remoteDir:e,error:t.message});continue}}const i=t;if(Array.isArray(i.uploadErrors)&&i.uploadErrors.length>0)for(const t of i.uploadErrors){const r=o.find(e=>n.basename(e.absolutePath)===t.fileName);d.push({absolutePath:r?.absolutePath||t.fileName,relativePath:r?.relativePath||t.fileName,remoteDir:e,error:t.error})}else for(const n of o)d.push({absolutePath:n.absolutePath,relativePath:n.relativePath,remoteDir:e,error:t?.message||"上传失败"});break}h.push(...i.duplicateFiles);const a=[...o];for(const t of i.uploadResults){const r=n.basename(String(t?.name||t?.uploadRes?.name||"")),o=a.findIndex(e=>n.basename(e.absolutePath)===r),i=o>=0?a.splice(o,1)[0]:a.shift();i&&f.push({absolutePath:i.absolutePath,relativePath:i.relativePath,remoteDir:e,uploadResult:t})}for(const t of i.uploadErrors){const r=o.find(e=>n.basename(e.absolutePath)===t.fileName);d.push({absolutePath:r?.absolutePath||t.fileName,relativePath:r?.relativePath||t.fileName,remoteDir:e,error:t.error})}break}}return{uploadResults:f,uploadErrors:d,duplicateFiles:h,totalTime:((Date.now()-a)/1e3).toFixed(2),fileCount:f.length,totalFileCount:o.length}}async download(e){const{nid:t,auto:r=!0,downloadDir:o,forceForeground:s=!1}=e,a=o||n.join(i(),".mcp-downloads"),c=await this.getDownloadUrl({nid:String(t)});if(!c)throw new N("API请求失败");this.throwIfApiError(c);const u=c.data?.downloadUrl||"";if(!u)throw new N("未能获取到文件下载链接",{traceId:R(c)});const l=n.basename(String(c.data?.fname||"downloaded_file")),f=Number(c.data?.size||0),d=f>0?f/1048576:0,h=d>0?`${d.toFixed(2)} MB`:"未知大小";if(!r)return{downloadUrl:u,filename:l,fileSize:h,downloadPath:""};await m.mkdir(a,{recursive:!0});const p=n.join(a,l);return d>10&&!s?(this.spawnBackgroundDownload(u,p),{downloadUrl:u,filename:l,fileSize:h,downloadPath:p,background:!0}):(await this.downloadWithCurl(u,p),{downloadUrl:u,filename:l,fileSize:h,downloadPath:p})}extractFileInfoFromUrl(e){try{const t=new URL(e);let r="downloaded_file";const o=t.searchParams.get("fname");if(o?r=n.basename(decodeURIComponent(o)):t.pathname&&t.pathname.length>1&&(r=n.basename(t.pathname)),r=r.replace(/[\x00/]/g,"_"),r&&"."!==r||(r="downloaded_file"),r.length>200){const e=n.extname(r);r=n.basename(r,e).substring(0,200-e.length)+e}const i=t.searchParams.get("fsize");return{filename:r,sizeMB:i?parseInt(i,10)/1048576:0}}catch{return{filename:"downloaded_file",sizeMB:0}}}downloadWithCurl(e,t,n=3e5){return new Promise((r,o)=>{const i=h("curl",["-L","-s","-A","yunpan_mcp_server",e,"-o",t]),s=setTimeout(()=>{i.kill(),o(new Error("下载超时"))},n);i.on("close",e=>{clearTimeout(s),0===e?r():o(new Error(`下载失败,curl 退出码: ${e}`))}),i.on("error",e=>{clearTimeout(s),o(e)})})}spawnBackgroundDownload(e,t){h("curl",["-L","-s","-A","yunpan_mcp_server",e,"-o",t],{detached:!0,stdio:"ignore"}).unref()}}const Be="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";function We(e,t,n,r,o){"text"===r.format&&o?C(o(e)):T(e,t,n,{quiet:r.quiet})}async function Ye(e,t){const n=await e.delete(t);if(3008===n?.errno&&!t.endsWith("/")){const n=await e.delete(t+"/");if(0===n?.errno)return n}return n}function Je(e){return"string"!=typeof e?e:e.replace(/\\r\\n/g,"\r\n").replace(/\\n/g,"\n")}function Ge(e){let t;for(const n of function(e){const t=[],n=new Set,r=e=>{const r=String(e).trim();r&&!n.has(r)&&(n.add(r),t.push(r))},o=String(e??"").trim().replace(/^\uFEFF/,"");if(r(o),o.length>=2&&o.startsWith("'")&&o.endsWith("'")){const e=o.slice(1,-1).trim();r(e),r(e.replace(/\\"/g,'"'))}return r(o.replace(/\\"/g,'"')),t}(e))try{t=JSON.parse(n);break}catch{}if(void 0===t)throw new I('files 必须是合法 JSON(示例: [{"fname":"a.txt","fsize":123}])。Windows cmd 不支持单引号包裹参数,请用双引号并对内部 " 转义,或改用 --stdin。',S.INVALID_ARGS);if("string"==typeof t)try{t=JSON.parse(t)}catch{throw new I('files 必须是合法 JSON(示例: [{"fname":"a.txt","fsize":123}])',S.INVALID_ARGS)}if(!Array.isArray(t)||0===t.length)throw new I("files 必须是非空数组",S.INVALID_ARGS);for(const e of t){if(!e||"string"!=typeof e.fname||""===e.fname.trim())throw new I("files[].fname 必须为非空字符串",S.INVALID_ARGS);if(!Number.isFinite(e.fsize)||e.fsize<0)throw new I("files[].fsize 必须为大于等于 0 的数字",S.INVALID_ARGS)}return t}function Qe(){const t=new e("file").description("文件操作");return t.command("mv").description("移动文件或文件夹").argument("<src>","源路径,多个用 | 分隔").argument("<dest>","目标文件夹路径").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=P(o);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file mv",r);const i=new Le(t),s=await async function(e,t,n){const r=await e.move({src_name:t,new_name:n}),o=!t.includes("|");if(3008===r?.errno&&o&&!t.endsWith("/")){const r=await e.move({src_name:t+"/",new_name:n});if(0===r?.errno)return r}return r}(i,e,n);F(s,"file mv",r),We(s,"file mv",r,o,t=>J(t,`已移动: ${e} -> ${n}`))}catch(e){L(e,"file mv",r)}}),t.command("trans-copy").description("转移或复制文件到目标目录").argument("<src>","源路径,多个用 | 分隔").argument("<dest>","目标目录路径").option("--delete <0|1>","是否删除源文件:1=转移,0=复制","1").option("--replace <0|1>","同名文件处理:0=重命名,1=覆盖","0").option("--src-ks-id <id>","源文件所在群组 ks_id(可选)").option("--new-ks-id <id>","目标目录所在群组 ks_id(可选)").action(async(e,n,r)=>{const o=Date.now(),i=t.parent?.opts()||{};try{const t=P(i);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file trans-copy",o);const s=parseInt(r.delete,10),a=parseInt(r.replace,10);if(![0,1].includes(s))return void L(new I("--delete 仅支持 0 或 1",S.INVALID_ARGS),"file trans-copy",o);if(![0,1].includes(a))return void L(new I("--replace 仅支持 0 或 1",S.INVALID_ARGS),"file trans-copy",o);if(!n.startsWith("/"))return void L(new I("dest 必须以 / 开头",S.INVALID_ARGS),"file trans-copy",o);const c=new Le(t),u=await c.transOrCopy({src_name:e,new_path:n,is_delete:s,is_replace:a,src_ks_id:r.srcKsId,new_ks_id:r.newKsId});F(u,"file trans-copy",o),We(u,"file trans-copy",o,i,t=>J(t,`${1===s?"转移":"复制"}完成: ${e} -> ${n}`))}catch(e){L(e,"file trans-copy",o)}}),t.command("rename").description("重命名文件或文件夹").argument("<path>","原文件完整路径").argument("<new_name>","新名称").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=P(o);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file rename",r);const i=function(e,t){const n=String(e||"").trim();let r=String(t||"").trim();const o=n.endsWith("/");if(!n.startsWith("/"))throw new I("path 必须以 / 开头",S.INVALID_ARGS);if(!r)throw new I("new_name 不能为空",S.INVALID_ARGS);if(o){r=r.replace(/^\/+/,"");const e=r.replace(/\/+$/,"").split("/").filter(Boolean);if(0===e.length)throw new I("new_name 不能为空(目录场景需传新目录名,如 bbb_renamed/)",S.INVALID_ARGS);return r=e[e.length-1]+"/",{src_name:n,new_name:r,isDir:!0}}if(r.includes("/"))throw new I("new_name 仅支持文件名(不含路径)。若要改路径请使用 file mv",S.INVALID_ARGS);return{src_name:n,new_name:r,isDir:!1}}(e,n),s=new Le(t),a=await s.rename({src_name:i.src_name,new_name:i.new_name});F(a,"file rename",r),We(a,"file rename",r,o,e=>J(e,`已重命名: ${i.src_name} -> ${i.new_name}`))}catch(e){L(e,"file rename",r)}}),t.command("rm").description("删除文件或文件夹").argument("<path>","文件路径,多个用 | 分隔").option("--batch","批量模式:从 stdin 读取路径列表(每行一个)").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=P(o);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file rm",r);const i=new Le(t);if(n.batch){const t=await Ce(),n=[e,...t].filter(Boolean),{executeBatch:s,formatBatchResult:a}=await Promise.resolve().then(function(){return V});We(await s(n,async e=>{const t=await Ye(i,e);if(t&&"number"==typeof t.errno&&0!==t.errno){throw new N(t.errmsg?`${t.errmsg} (errno: ${t.errno})`:`API 错误 (errno: ${t.errno})`,{traceId:R(t),apiErrno:t.errno})}return t}),"file rm --batch",r,o,e=>a(e))}else{const t=await Ye(i,e);F(t,"file rm",r),We(t,"file rm",r,o,t=>J(t,`已删除: ${e}`))}}catch(e){L(e,"file rm",r)}}),t.command("search").description("按关键词搜索文件").argument("<keyword>","搜索关键词").option("--type <type>","文件类型: -1(全部) 0(其他) 1(图片) 2(文档) 3(音乐) 4(视频)","-1").option("--page <n>","页码","1").option("--size <n>","每页数量","20").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=P(o);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file search",r);const i=new je(t),s=await i.search({key:e,file_category:parseInt(n.type),page:parseInt(n.page),page_size:parseInt(n.size)});F(s,"file search",r),We(s,"file search",r,o,W)}catch(e){L(e,"file search",r)}}),t.command("share").description("生成文件分享链接").argument("<paths>","文件路径,多个用 | 分隔").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=P(r);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file share",n);const o=new je(t),i=await o.share(e);F(i,"file share",n),We(i,"file share",n,r,e=>J(e,"分享链接已生成"))}catch(e){L(e,"file share",n)}}),t.command("url").description("获取文件下载链接").argument("<path>","文件路径").option("--nid <nid>","文件 NID(与路径二选一)").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=P(o);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file url",r);const i=new je(t),s=await i.getDownloadUrl({nid:n.nid,fpath:n.nid?void 0:e});F(s,"file url",r),We(s,"file url",r,o,e=>J(e,"下载链接已获取"))}catch(e){L(e,"file url",r)}}),t.command("node-info").description("根据 nid 获取节点信息(可选返回 ks_info)").argument("<nid>","节点 nid(仅支持文件夹/知识库)").option("--ks-ext <0|1>","是否返回 ks_info:0=不返回,1=返回","0").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=P(o);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file node-info",r);const i=parseInt(n.ksExt,10);if(![0,1].includes(i))return void L(new I("--ks-ext 仅支持 0 或 1",S.INVALID_ARGS),"file node-info",r);const s=new je(t),a=await s.getNodeInfoByNid({nid:e,ks_ext:i});F(a,"file node-info",r),We(a,"file node-info",r,o,t=>J(t,`节点信息获取成功: ${e}`))}catch(e){L(e,"file node-info",r)}}),t.command("origin-size").description("统计目录下文件和文件夹(递归)原始大小").argument("<path>","目录完整路径").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=P(o);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file origin-size",r);if(!e.startsWith("/"))return void L(new I("path 必须以 / 开头",S.INVALID_ARGS),"file origin-size",r);const n=new Le(t),i=await n.countOriginSize({path:e});F(i,"file origin-size",r),We(i,"file origin-size",r,o,t=>J(t,`目录大小统计完成: ${e}`))}catch(e){L(e,"file origin-size",r)}}),t.command("clear-dir").description("清空目录下文件,保留目录本身(每次仅一个目录)").argument("<path>","单个目录路径,必须以 / 开头").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=P(o);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file clear-dir",r);if(!e.startsWith("/"))return void L(new I("目录 path 必须以 / 开头",S.INVALID_ARGS),"file clear-dir",r);if(e.includes("|"))return void L(new I("每次仅支持清空一个目录,不支持用英文竖线(|)连接多个路径(会触发服务端非法字符错误);请分多次执行 file clear-dir",S.INVALID_ARGS),"file clear-dir",r);const n=new Le(t),i=await n.clearDir({fname:e});F(i,"file clear-dir",r),We(i,"file clear-dir",r,o,t=>J(t,`目录清空完成: ${e}`))}catch(e){L(e,"file clear-dir",r)}}),t.command("config").description("读取/写入/列出配置文件(INI/JSON/YAML)").requiredOption("--path <path>","配置文件路径,必须以 / 开头").requiredOption("--command <cmd>","操作命令:config:get|config:set|config:delete|config:list|config:read|config:write").requiredOption("--type <type>","配置类型:ini|json|yaml|yml").option("--key <key>","键路径,如 a.b.c").option("--value <value>","写入值(set 场景)").option("--content <text>","写入完整内容(write 场景,与 --stdin 互斥)").option("--stdin","从标准输入读取 content(write 场景,与 --content 互斥)").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=P(r);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file config",n);if(!e.path.startsWith("/"))return void L(new I("--path 必须以 / 开头",S.INVALID_ARGS),"file config",n);if(!new Set(["config:get","config:set","config:delete","config:list","config:read","config:write"]).has(e.command))return void L(new I("--command 非法",S.INVALID_ARGS),"file config",n);if(!new Set(["ini","json","yaml","yml"]).has(e.type))return void L(new I("--type 非法",S.INVALID_ARGS),"file config",n);if("config:write"===e.command){if(1!==[e.content,e.stdin].filter(Boolean).length)return void L(new I("config:write 场景下 --content 与 --stdin 互斥,必须且只能传一个",S.INVALID_ARGS),"file config",n)}else if(e.content||e.stdin)return void L(new I("--content/--stdin 仅在 config:write 场景使用",S.INVALID_ARGS),"file config",n);let o=Je(e.content);if(e.stdin&&(o=await Ae(),!o.trim()))return void L(new I("stdin 输入内容为空",S.INVALID_ARGS),"file config",n);const i=new Le(t),s=await i.config({path:e.path,command:e.command,type:e.type,key:e.key,value:e.value,content:o});F(s,"file config",n),We(s,"file config",n,r,t=>Y(t,e.command,`配置操作完成: ${e.command}`))}catch(e){L(e,"file config",n)}}),t.command("save").description("通过 URL 或文本内容保存文件到云盘").option("--url <url>","单个文件下载地址(与 --content/--stdin 互斥;勿用 | 连接多个 URL)").option("--content <text>","文件内容(与 --url/--stdin 互斥)").option("--stdin","从标准输入读取内容(与 --url/--content 互斥)").option("--dest <path>","云盘存储路径,必须以 / 开头和结尾").option("--filename <name>","保存的文件名").option("--rename <0|1>","同名文件处理策略:0=直接替换,1=自动重命名","1").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=P(r);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file save",n);if(1!==[e.url,e.content,e.stdin].filter(Boolean).length)return void L(new I("--url、--content、--stdin 三者互斥,必须且只能传一个",S.INVALID_ARGS),"file save",n);const o=parseInt(e.rename,10);if(![0,1].includes(o))return void L(new I("--rename 仅支持 0 或 1",S.INVALID_ARGS),"file save",n);if(e.url&&String(e.url).includes("|"))return void L(new I("`--url` 仅支持单个下载地址,不支持用英文竖线(|)连接多个 URL;请对每个地址分别执行 file save",S.INVALID_ARGS),"file save",n);let i=Je(e.content);if(e.stdin&&(i=Te(await Ae()),!i.trim()))return void L(new I("stdin 输入内容为空",S.INVALID_ARGS),"file save",n);const s=new je(t),a=await s.save({url:e.url,content:i,upload_path:e.dest,file_name:e.filename,is_rename:o});F(a,"file save",n),We(a,"file save",n,r,e=>J(e,"文件保存成功"))}catch(e){L(e,"file save",n)}}),t.command("append").description("向云盘文本文件末尾追加内容").argument("<path>","文件完整路径,必须以 / 开头").option("--content <text>","追加的文本内容(与 --stdin 互斥)").option("--stdin","从标准输入读取追加内容(与 --content 互斥;Windows cmd 用 type 文件 | ... 勿用 cat)").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=P(o);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file append",r);if(!e.startsWith("/"))return void L(new I("path 必须以 / 开头",S.INVALID_ARGS),"file append",r);if(1!==[n.content,n.stdin].filter(Boolean).length)return void L(new I("--content 与 --stdin 互斥,必须且只能传一个",S.INVALID_ARGS),"file append",r);let i=Je(n.content);if(n.stdin&&(i=Te(await Ae()),!i||!i.trim()))return void L(new I("stdin 输入内容为空",S.INVALID_ARGS),"file append",r);const s=new je(t),a=await s.appendContent({path:e,content:i});F(a,"file append",r),We(a,"file append",r,o,t=>J(t,`已追加内容到: ${e}`))}catch(e){L(e,"file append",r)}}),t.command("exists").description("检测目录下是否存在同名文件").option("--path <path>","目标目录,必须以 / 开头").option("--files <json>",'待检测文件数组 JSON,例如 [{"fname":"a.txt","fsize":123}](与 --stdin 互斥;Windows cmd 勿用单引号,见文档)').option("--stdin","从标准输入读取 files JSON(与 --files 互斥)").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=P(r);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file exists",n);if(!e.path||!e.path.startsWith("/"))return void L(new I("--path 必须以 / 开头",S.INVALID_ARGS),"file exists",n);if(1!==[e.files,e.stdin].filter(Boolean).length)return void L(new I("--files 与 --stdin 互斥,必须且只能传一个",S.INVALID_ARGS),"file exists",n);const o=Ge(e.stdin?await Ae():e.files),i=new je(t),s=await i.detectFileExists({path:e.path,files:o});F(s,"file exists",n),We(s,"file exists",n,r,t=>J(t,`检测完成: ${e.path}`))}catch(e){L(e,"file exists",n)}}),t.command("upload").description("上传本地文件到云盘").argument("<files>","本地文件路径,多个用逗号分隔").option("--dest <path>","云盘目标路径","/").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=P(o);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file upload",r);const i=e.split(",").map(e=>e.trim()),s=new je(t);We(await s.upload({filePaths:i,uploadPath:n.dest}),"file upload",r,o,e=>J(e,"上传完成"))}catch(e){L(e,"file upload",r)}}),t.command("download").description("下载云盘文件到本地").argument("<nid>","文件 NID(可通过 file search / dir ls 获取)").option("--dir <path>","本地下载目录").option("--no-auto","仅获取下载链接,不自动下载").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=P(o);if(!t.apiKey)return void L(new I(Be,S.AUTH_ERROR),"file download",r);const i=new je(t);We(await i.download({nid:e,auto:n.auto,downloadDir:n.dir}),"file download",r,o,e=>J(e,"下载完成"))}catch(e){L(e,"file download",r)}}),t}function Ve(e){return`${e} CLI auto-completion`}function Xe(e){return"_"+e.replace(/[^a-zA-Z0-9]/g,"_")}function He(){return g()}function Ze(){const e=process.env.SHELL||"";return e.includes("zsh")?"zsh":e.includes("bash")?"bash":"unknown"}function et(e){const t=Xe(e),n=`~/.${e}`;return`# ${e} bash completion\n# 安装: source ${n}/completion.bash\n# 或添加到 ~/.bashrc: source ${n}/completion.bash\n\n${t}_completions() {\n local cur prev commands\n COMPREPLY=()\n cur="\${COMP_WORDS[COMP_CWORD]}"\n prev="\${COMP_WORDS[COMP_CWORD-1]}"\n\n # 顶级命令\n local top_commands="auth user dir file claw-backup claw-restore claw-auto-backup completion help"\n\n # 子命令映射\n local auth_commands="login whoami logout"\n local user_commands="info"\n local dir_commands="ls mkdir"\n local file_commands="mv rename rm search share url save upload download"\n local completion_commands="install uninstall script"\n\n # 全局选项\n local global_options="--api-key --env --sub-channel --format --quiet --timeout --retries --help --version"\n\n case "\${COMP_WORDS[1]}" in\n auth)\n case "$prev" in\n login) COMPREPLY=( $(compgen -W "--api-key --env --sub-channel --help" -- "$cur") ) ;;\n whoami|logout) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$auth_commands" -- "$cur") ) ;;\n esac\n ;;\n user)\n case "$prev" in\n info) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$user_commands" -- "$cur") ) ;;\n esac\n ;;\n dir)\n case "$prev" in\n ls) COMPREPLY=( $(compgen -W "--page --size --help" -- "$cur") ) ;;\n mkdir) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$dir_commands" -- "$cur") ) ;;\n esac\n ;;\n file)\n case "$prev" in\n search) COMPREPLY=( $(compgen -W "--type --page --size --help" -- "$cur") ) ;;\n url) COMPREPLY=( $(compgen -W "--nid --help" -- "$cur") ) ;;\n save) COMPREPLY=( $(compgen -W "--url --content --stdin --dest --filename --help" -- "$cur") ) ;;\n upload) COMPREPLY=( $(compgen -W "--dest --help" -- "$cur") ) ;;\n download) COMPREPLY=( $(compgen -W "--dir --no-auto --help" -- "$cur") ) ;;\n rm) COMPREPLY=( $(compgen -W "--batch --help" -- "$cur") ) ;;\n mv|rename|share) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$file_commands" -- "$cur") ) ;;\n esac\n ;;\n completion)\n case "$prev" in\n install|uninstall) COMPREPLY=( $(compgen -W "--bash --zsh --help" -- "$cur") ) ;;\n script) COMPREPLY=( $(compgen -W "--bash --zsh --help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$completion_commands" -- "$cur") ) ;;\n esac\n ;;\n claw-backup)\n COMPREPLY=( $(compgen -W "--source --dest --source-dir --claw-name --help" -- "$cur") )\n ;;\n claw-restore)\n COMPREPLY=( $(compgen -W "--remote --target --help" -- "$cur") )\n ;;\n claw-auto-backup)\n case "\${COMP_WORDS[2]}" in\n enable) COMPREPLY=( $(compgen -W "--source-dir --claw-name --event-log --help" -- "$cur") ) ;;\n disable|status) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "enable disable status --help" -- "$cur") ) ;;\n esac\n ;;\n *)\n if [[ "$cur" == -* ]]; then\n COMPREPLY=( $(compgen -W "$global_options" -- "$cur") )\n else\n COMPREPLY=( $(compgen -W "$top_commands" -- "$cur") )\n fi\n ;;\n esac\n}\n\ncomplete -F ${t}_completions ${e}\n`}function tt(e){const t=Xe(e),n=`~/.${e}`;return`#compdef ${e}\n# ${e} zsh completion\n# 安装: source ${n}/completion.zsh\n# 或添加到 ~/.zshrc: source ${n}/completion.zsh\n\n${t}() {\n local -a top_commands\n top_commands=(\n 'auth:鉴权管理'\n 'user:用户信息'\n 'dir:目录操作'\n 'file:文件操作'\n 'claw-backup:上传本地文件或目录到云盘'\n 'claw-restore:递归恢复云盘目录到本地'\n 'claw-auto-backup:监听目录变化并自动备份到云盘'\n 'completion:Shell 自动补全'\n 'help:显示帮助'\n )\n\n local -a auth_commands\n auth_commands=(\n 'login:登录(保存 API Key)'\n 'whoami:查看鉴权状态'\n 'logout:退出登录'\n )\n\n local -a user_commands\n user_commands=(\n 'info:获取用户详细信息'\n )\n\n local -a dir_commands\n dir_commands=(\n 'ls:列出目录内容'\n 'mkdir:创建文件夹'\n )\n\n local -a file_commands\n file_commands=(\n 'mv:移动文件或文件夹'\n 'rename:重命名文件或文件夹'\n 'rm:删除文件或文件夹'\n 'search:搜索文件'\n 'share:生成分享链接'\n 'url:获取下载链接'\n 'save:保存文件到云盘'\n 'upload:上传文件'\n 'download:下载文件'\n )\n\n local -a completion_commands\n completion_commands=(\n 'install:安装补全脚本'\n 'uninstall:卸载补全脚本'\n 'script:输出补全脚本'\n )\n\n _arguments -C \\\n '--api-key[API 密钥]:key:' \\\n '--env[环境]:env:(prod test)' \\\n '--sub-channel[子渠道]:channel:' \\\n '--format[输出格式]:format:(json text)' \\\n '--quiet[仅输出结果]' \\\n '--timeout[超时时间(ms)]:ms:' \\\n '--retries[重试次数]:n:' \\\n '--help[显示帮助]' \\\n '--version[显示版本]' \\\n '1:command:->command' \\\n '2:subcommand:->subcommand' \\\n '*::arg:->args'\n\n case "$state" in\n command)\n _describe 'command' top_commands\n ;;\n subcommand)\n case "$words[2]" in\n auth) _describe 'auth command' auth_commands ;;\n user) _describe 'user command' user_commands ;;\n dir) _describe 'dir command' dir_commands ;;\n file) _describe 'file command' file_commands ;;\n completion) _describe 'completion command' completion_commands ;;\n claw-backup) _arguments '--source[本地源文件或目录]:path:_files' '--dest[云盘目标目录]:path:' '--source-dir[OpenClaw本地根目录]:path:_files -/' '--claw-name[云盘备份目录名]:name:' ;;\n claw-restore) _arguments '--remote[云盘源目录]:path:' '--target[本地目标目录]:path:_files -/' ;;\n claw-auto-backup) _arguments '1:subcommand:(enable disable status)' ;;\n esac\n ;;\n args)\n case "$words[2]" in\n claw-auto-backup)\n case "$words[3]" in\n enable) _arguments '--source-dir[监听的本地根目录]:path:_files -/' '--claw-name[云盘备份目录名]:name:' '--event-log[事件日志文件]:path:' ;;\n esac\n ;;\n file)\n case "$words[3]" in\n search) _arguments '--type[文件类型]:type:(-1 0 1 2 3 4)' '--page[页码]:n:' '--size[每页数量]:n:' ;;\n save) _arguments '--url[文件URL]:url:' '--content[文件内容]:text:' '--stdin[从stdin读取]' '--dest[目标路径]:path:' '--filename[文件名]:name:' ;;\n upload) _arguments '--dest[云盘路径]:path:' ;;\n download) _arguments '--dir[下载目录]:path:_files -/' '--no-auto[不自动下载]' ;;\n url) _arguments '--nid[文件NID]:nid:' ;;\n rm) _arguments '--batch[批量模式]' ;;\n esac\n ;;\n dir)\n case "$words[3]" in\n ls) _arguments '--page[页码]:n:' '--size[每页数量]:n:' ;;\n esac\n ;;\n auth)\n case "$words[3]" in\n login) _arguments '--api-key[API密钥]:key:' '--env[环境]:env:(prod test)' '--sub-channel[渠道]:channel:' ;;\n esac\n ;;\n esac\n ;;\n esac\n}\n\n${t} "$@"\n`}function nt(e){const t=i();return n.join(t,"zsh"===e?".zshrc":".bashrc")}function rt(){const r=new e("completion").description("Shell 自动补全");return r.command("install").description("安装补全脚本到当前 shell").option("--bash","安装 bash 补全").option("--zsh","安装 zsh 补全").action(e=>{try{const r=e.bash?"bash":e.zsh?"zsh":Ze();if("unknown"===r)return void L("无法检测 shell 类型,请使用 --bash 或 --zsh 指定","completion install");const o=_(),i="zsh"===r?tt(o):et(o),s="zsh"===r?"zsh":"bash",a=He(),c=n.join(a,`completion.${s}`);t.existsSync(a)||t.mkdirSync(a,{recursive:!0,mode:448}),t.writeFileSync(c,i,{mode:420});const u=nt(r),l=function(e,t){return`source ~/.${e}/completion.${"zsh"===t?"zsh":"bash"} # ${Ve(e)}`}(o,r);let f="";t.existsSync(u)&&(f=t.readFileSync(u,"utf8")),!function(e,t){return!!e.includes(Ve(t))||!("360disk"!==t||!e.includes("360disk auto-completion"))}(f,o)?(t.appendFileSync(u,"\n"+l+"\n"),C(`补全脚本已安装:\n 脚本: ${c}\n 配置: ${u}\n\n请执行 source ${u} 或重新打开终端生效`)):C(`补全脚本已更新: ${c}\n配置已存在于 ${u},无需重复添加`)}catch(e){L(e,"completion install")}}),r.command("uninstall").description("卸载补全脚本").option("--bash","卸载 bash 补全").option("--zsh","卸载 zsh 补全").action(e=>{try{const r=e.bash?"bash":e.zsh?"zsh":Ze();if("unknown"===r)return void L("无法检测 shell 类型,请使用 --bash 或 --zsh 指定","completion uninstall");const o="zsh"===r?"zsh":"bash",i=n.join(He(),`completion.${o}`);t.existsSync(i)&&t.unlinkSync(i);const s=nt(r);if(t.existsSync(s)){const e=t.readFileSync(s,"utf8"),n=_(),r=Ve(n),o=e.split("\n").filter(e=>!e.includes(r)&&("360disk"!==n||!e.includes("360disk auto-completion")));t.writeFileSync(s,o.join("\n"))}C(`补全脚本已卸载\n请执行 source ${nt(r)} 或重新打开终端生效`)}catch(e){L(e,"completion uninstall")}}),r.command("script").description("输出补全脚本到 stdout(手动安装用)").option("--bash","输出 bash 补全脚本").option("--zsh","输出 zsh 补全脚本").action(e=>{try{const t=e.bash?"bash":e.zsh?"zsh":Ze();if("unknown"===t)return void L("无法检测 shell 类型,请使用 --bash 或 --zsh 指定","completion script");const n=_(),r="zsh"===t?tt(n):et(n);process.stdout.write(r)}catch(e){L(e,"completion script")}}),r}const ot=524288;function it(e){const n=t.openSync(e,"r"),r=Buffer.allocUnsafe(ot),o=[];try{let e=0;for(;;){const i=t.readSync(n,r,0,ot,e);if(0===i)break;const s=u.createHash("sha1").update(r.subarray(0,i)).digest("hex");o.push(s),e+=i}}finally{t.closeSync(n)}return u.createHash("sha1").update(o.join("")).digest("hex")}function st(e){const t=JSON.stringify({apiKey:e.apiKey,ecsEnv:e.ecsEnv||"prod",subChannel:e.subChannel||"open"});return u.createHash("sha256").update(t).digest("hex")}function at(e){const t=Array.isArray(e?.uploadResults)?e.uploadResults[0]:void 0,n=t?.uploadRes;return{nid:void 0!==n?.nid?String(n.nid):void 0,remote_file_hash:void 0!==n?.file_hash?String(n.file_hash):void 0,remote_name:n?.name||t?.name}}class ct{db;constructor(e=function(){return n.join(g(),"claw-backup.db")}()){v();const t=w(import.meta.url)("better-sqlite3");this.db=new t(e),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("busy_timeout = 5000"),this.migrate()}getFileState(e){return this.db.prepare("\n SELECT *\n FROM claw_backup_files\n WHERE cache_scope = ?\n AND source_root = ?\n AND dest_root = ?\n AND relative_path = ?\n ").get(e.cacheScope,e.sourceRoot,e.destRoot,e.relativePath)}upsertFileState(e){this.db.prepare("\n INSERT INTO claw_backup_files (\n cache_scope,\n source_root,\n dest_root,\n relative_path,\n absolute_path,\n size,\n mtime_ms,\n fhash,\n nid,\n remote_file_hash,\n remote_name,\n remote_dir,\n uploaded_at,\n created_at,\n updated_at\n ) VALUES (\n @cache_scope,\n @source_root,\n @dest_root,\n @relative_path,\n @absolute_path,\n @size,\n @mtime_ms,\n @fhash,\n @nid,\n @remote_file_hash,\n @remote_name,\n @remote_dir,\n @uploaded_at,\n datetime('now'),\n datetime('now')\n )\n ON CONFLICT(cache_scope, source_root, dest_root, relative_path)\n DO UPDATE SET\n absolute_path = excluded.absolute_path,\n size = excluded.size,\n mtime_ms = excluded.mtime_ms,\n fhash = excluded.fhash,\n nid = excluded.nid,\n remote_file_hash = excluded.remote_file_hash,\n remote_name = excluded.remote_name,\n remote_dir = excluded.remote_dir,\n uploaded_at = excluded.uploaded_at,\n updated_at = datetime('now')\n ").run({...e,nid:e.nid??null,remote_file_hash:e.remote_file_hash??null,remote_name:e.remote_name??null})}touchUnchangedFileState(e,t){this.db.prepare("\n UPDATE claw_backup_files\n SET absolute_path = @absolute_path,\n size = @size,\n mtime_ms = @mtime_ms,\n fhash = @fhash,\n updated_at = datetime('now')\n WHERE id = @id\n ").run({id:e.id,...t})}close(){this.db.close()}migrate(){this.db.exec("\n CREATE TABLE IF NOT EXISTS claw_backup_files (\n -- 自增主键,仅用于本地数据库内部定位记录\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n -- 缓存作用域:由 apiKey、ECS_ENV、SUB_CHANNEL 计算出的 SHA-256 摘要,避免不同账号/环境共用缓存\n cache_scope TEXT NOT NULL,\n -- 本地备份源根路径;目录备份时为目录路径,单文件备份时为文件路径\n source_root TEXT NOT NULL,\n -- 云盘备份目标根目录,和 source_root 共同区分不同备份任务\n dest_root TEXT NOT NULL,\n -- 文件相对 source_root 的 POSIX 路径,用于保持目录结构和定位同一文件\n relative_path TEXT NOT NULL,\n -- 文件当前本地绝对路径,用于诊断源目录移动或单文件备份记录\n absolute_path TEXT NOT NULL,\n -- 本地文件大小,作为快速跳过未变化文件的第一层判断\n size INTEGER NOT NULL,\n -- 本地文件 mtime 毫秒时间戳,用于记录上传时的本地文件状态\n mtime_ms INTEGER NOT NULL,\n -- 业务文件 hash:按 512KB 分块算 bhash 后再 SHA1 得到的 fhash\n fhash TEXT NOT NULL,\n -- 云盘上传成功后返回的节点 ID,用于后续下载、定位或排查\n nid TEXT,\n -- 云盘服务端返回的 file_hash;正常应与本地 fhash 保持一致\n remote_file_hash TEXT,\n -- 云盘服务端返回的文件名或远端完整文件名,用于排查实际上传结果\n remote_name TEXT,\n -- 本次文件实际上传到的云盘目录\n remote_dir TEXT NOT NULL,\n -- 最近一次成功上传该文件的时间\n uploaded_at TEXT NOT NULL,\n -- 本地数据库记录首次创建时间\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n -- 本地数据库记录最近更新时间\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(cache_scope, source_root, dest_root, relative_path)\n );\n\n CREATE UNIQUE INDEX IF NOT EXISTS idx_claw_backup_files_scope_path\n ON claw_backup_files(cache_scope, source_root, dest_root, relative_path);\n\n -- 用于后续按内容 hash 排查或扩展去重能力\n CREATE INDEX IF NOT EXISTS idx_claw_backup_files_fhash\n ON claw_backup_files(fhash);\n ")}}function ut(e){return e.split(n.sep).join(n.posix.sep)}function lt(e){const t=String(e||"").trim();if(!t||"."===t||".."===t)return;const r=t.split(/[\\/]+/).filter(Boolean).join(n.posix.sep);return r&&"."!==r&&".."!==r&&!r.startsWith("../")?r:void 0}function ft(e){if(!Array.isArray(e))return[];const t=new Set;for(const n of e){const e=lt(n);e&&t.add(e)}return[...t].sort()}function dt(e){const t=e?.data?.AFS;if(!t||"object"!=typeof t)throw new Error("系统云控配置缺少 data.AFS");const n=ft(t.backup_dir),r=ft(t.backup_match_dir),o=ft(t.backup_file);if(0===n.length&&0===r.length&&0===o.length)throw new Error("系统云控配置 data.AFS 为空");return{backupDirs:n,backupMatchDirs:r,backupFiles:o}}function ht(e,t){const r=ut(n.relative(e,t));return Boolean(r)&&".."!==r&&!r.startsWith("../")&&!n.posix.isAbsolute(r)}function pt(e,r){const o=t.realpathSync(e),i=t.realpathSync(r),s=ut(n.relative(o,i));return Boolean(s)&&".."!==s&&!s.startsWith("../")&&!n.posix.isAbsolute(s)}function mt(e){const r=[],o=[];if(!t.existsSync(e))return{files:r,errors:o};const i=[e];for(;i.length>0;){const e=i.pop();let s;try{s=t.readdirSync(e,{withFileTypes:!0})}catch(t){o.push({path:e,error:t?.message||"读取目录失败"});continue}for(const t of s){if(".DS_Store"===t.name)continue;const o=n.join(e,t.name);t.isDirectory()?i.push(o):t.isFile()&&r.push(o)}}return{files:r,errors:o}}function wt(e,r,o,i){const s=n.resolve(e,r);if(!ht(e,s))return;if(!t.existsSync(s))return;let a;try{a=t.lstatSync(s)}catch(e){return void i.push({path:s,error:e?.message||"读取目录状态失败"})}if(a.isSymbolicLink())return;if(!a.isDirectory())return;try{if(!pt(e,s))return void i.push({path:s,error:"配置目录指向 source-dir 外部"})}catch(e){return void i.push({path:s,error:e?.message||"校验目录真实路径失败"})}const c=mt(s);for(const e of c.files)o.add(e);i.push(...c.errors)}function _t(e,r,o,i){const s=n.resolve(e,r);if(ht(e,s)&&t.existsSync(s))try{const n=t.lstatSync(s);if(n.isSymbolicLink())return;if(n.isFile()){if(!pt(e,s))return void i.push({path:s,error:"配置文件指向 source-dir 外部"});o.add(s)}}catch(e){i.push({path:s,error:e?.message||"读取文件状态失败"})}}function gt(e,t){return t.backupMatchDirs.some(t=>e.includes(t))}function yt(e,r){const o=new Set,i=[];let s;try{s=t.readdirSync(e,{withFileTypes:!0})}catch(t){return{files:[],errors:[{path:e,error:t?.message||"读取目录失败"}]}}for(const t of r.backupDirs)wt(e,t,o,i);for(const t of r.backupFiles)_t(e,t,o,i);for(const t of s){const s=n.join(e,t.name);if(t.isDirectory()&&gt(t.name,r)){const e=mt(s);for(const t of e.files)o.add(t);i.push(...e.errors)}}return{files:[...o].sort(),errors:i}}function $t(e,t,r){const o=ut(n.relative(e,n.resolve(t)));if(!o||".."===o||o.startsWith("../")||n.posix.isAbsolute(o))return!1;if(r.backupFiles.includes(o))return!0;if(r.backupDirs.some(e=>o===e||o.startsWith(`${e}/`)))return!0;const[i]=o.split("/");return!!i&&gt(i,r)}const vt="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";function bt(e){return n.resolve(e)}function Et(e){let t=String(e||"").trim();if(!t)throw new I("远端目录不能为空",S.INVALID_ARGS);return t.startsWith("/")||(t="/"+t),t.endsWith("/")||(t+="/"),t}function kt(e){return String(e?.name||e?.fname||"").trim()}function Pt(e,t,r){const o=kt(t);if(!o)return"";let i=o;return i.startsWith("/")||(i=n.posix.join(e,i)),r?Et(i):i}function St(e){return!!e&&(1===e.is_dir||"1"===e.is_dir||"dir"===e.type||(1===e.type||"1"===e.type))}function It(e){const t=e?.data;if(!t)return[];const n=t.list||t.data||t.node_list||[];return Array.isArray(n)?n:[]}function Dt(e,t){const n=e?.data||{},r=Number(n.total_count??n.total??t);return Number.isFinite(r)?r:t}function Ot(e){t.mkdirSync(e,{recursive:!0})}function Nt(e){return".DS_Store"===e}function Rt(e){const r=t.statSync(e);if(r.isDirectory())return function(e){const r=[""],o=[];return function e(i,s){const a=t.readdirSync(i,{withFileTypes:!0});for(const t of a){const a=n.join(i,t.name),c=s?n.posix.join(s,t.name):t.name;if(t.isDirectory())r.push(c),e(a,c);else if(t.isFile()){if(Nt(t.name))continue;o.push(c)}}}(e,""),{dirs:r,files:o}}(e).files;if(r.isFile())return Nt(n.basename(e))?[]:[n.basename(e)];throw new I(`--source 必须是文件或目录: ${e}`,S.INVALID_ARGS)}async function Tt(e,r){const o=Boolean(e.source||e.dest),i=Boolean(e.sourceDir||e.clawName);if(o&&i)throw new I("通用备份参数 --source/--dest 不能和 OpenClaw 参数 --source-dir/--claw-name 混用",S.INVALID_ARGS);if(!o&&!i)throw new I("必须传入 --source/--dest,或传入 --source-dir/--claw-name",S.INVALID_ARGS);if(i){if(!e.sourceDir)throw new I("--source-dir 为必填参数",S.INVALID_ARGS);const o=bt(e.sourceDir),i=function(e){const t=String(e||"").trim();if(!t)throw new I("--claw-name 不能为空",S.INVALID_ARGS);if("."===t||".."===t||t.includes("/")||t.includes("\\"))throw new I("--claw-name 必须是目录名,不能包含路径分隔符",S.INVALID_ARGS);return t}(e.clawName);if(!t.existsSync(o))throw new I(`source-dir 目录不存在: ${o}`,S.INVALID_ARGS);if(!t.statSync(o).isDirectory())throw new I(`source-dir 必须是本地目录: ${o}`,S.INVALID_ARGS);const s=dt(await r.getSysConfig()),a=yt(o,s);return{sourceRoot:o,uploadRoot:Et(i),uploadTreeRoot:o,files:a.files.map(e=>({absoluteFile:e,relativeFile:At(n.relative(o,e))})),initialFailures:a.errors.map(e=>({path:At(n.relative(o,e.path))||e.path,error:e.error,code:O(new Error(e.error))}))}}if(!e.source||!e.dest)throw new I("--source 与 --dest 必须同时传入",S.INVALID_ARGS);const s=bt(e.source);if(!t.existsSync(s))throw new I(`本地源路径不存在: ${s}`,S.INVALID_ARGS);const a=t.statSync(s).isDirectory(),c=Rt(s);return{sourceRoot:s,uploadRoot:Et(e.dest),uploadTreeRoot:a?s:n.dirname(s),files:c.map(e=>({absoluteFile:a?n.join(s,e):s,relativeFile:At(e)})),initialFailures:[]}}function At(e){return e.split(n.sep).join(n.posix.sep)}function Ct(e){return[S.AUTH_ERROR,S.PERMISSION_DENIED,S.QUOTA_EXCEEDED,S.NETWORK_ERROR,S.SERVER_ERROR].includes(e.code)}function Lt(e){if(e instanceof I)return{message:e.message,code:e.code};const t=e;return{message:e instanceof Error?e.message:String(e||"未知错误"),code:O(t?.primaryUploadError||t?.uploadErrors?.[0]?.error||e)}}function Ft(){const n=new e("claw-backup").description("将本地文件或目录上传到云盘目录").option("--source <path>","本地源文件或目录").option("--dest <path>","云盘目标目录").option("--source-dir <path>","OpenClaw 本地根目录,只备份白名单目录和文件").option("--claw-name <name>","云盘中的 OpenClaw 备份目录名").action(async e=>{const r=Date.now(),o=n.parent?.opts()||{};try{const n=P(o);if(!n.apiKey)return void L(new I(vt,S.AUTH_ERROR),"claw-backup",r);const i=new je(n),s=await Tt(e,i),a=s.files,c=[],u=[],l=[...s.initialFailures],f=[];let d,h;const p=st({apiKey:n.apiKey,ecsEnv:n.ecsEnv,subChannel:n.subChannel}),m=new ct;try{for(const e of a){const{absoluteFile:n,relativeFile:r}=e;try{const e=t.statSync(n),o=Math.trunc(e.mtimeMs),i=m.getFileState({cacheScope:p,sourceRoot:s.sourceRoot,destRoot:s.uploadRoot,relativePath:r}),a=i&&i.size!==e.size?void 0:it(n);if(i&&a&&i.fhash===a){m.touchUnchangedFileState(i,{absolute_path:n,size:e.size,mtime_ms:o,fhash:a}),u.push(r);continue}f.push({absoluteFile:n,relativeFile:r,size:e.size,mtimeMs:o,fhash:a||it(n)})}catch(e){const t=Lt(e),n={path:r,error:t.message,code:t.code};if(l.push(n),Ct(n)){d=n,h=n.error;break}}}if(f.length>0&&!d){const e=await i.uploadTree({rootDir:s.uploadTreeRoot,filePaths:f.map(e=>e.absoluteFile),uploadRoot:s.uploadRoot,is_rename:0}),t=new Map(f.map(e=>[e.absoluteFile,e]));for(const n of e.uploadResults){const e=t.get(n.absolutePath);if(!e)continue;const r=at({uploadResults:[n.uploadResult]});m.upsertFileState({cache_scope:p,source_root:s.sourceRoot,dest_root:s.uploadRoot,relative_path:e.relativeFile,absolute_path:e.absoluteFile,size:e.size,mtime_ms:e.mtimeMs,fhash:e.fhash,nid:r.nid,remote_file_hash:r.remote_file_hash||e.fhash,remote_name:r.remote_name,remote_dir:n.remoteDir,uploaded_at:(new Date).toISOString()}),c.push(e.relativeFile)}for(const t of e.uploadErrors){const e=Lt(new Error(t.error)),n={path:t.relativePath,error:t.error,code:e.code};if(l.push(n),Ct(n)){d=n,h=n.error;break}}}}finally{m.close()}const w=a.length+s.initialFailures.length,_=Math.max(w-c.length-l.length-u.length,0),g=u.length+_,y=_>0,$=l.length>0?function(e,t){if(t)return t.code;const n=e.find(e=>e.code!==S.GENERAL)?.code;return n??e[0]?.code??S.GENERAL}(l,d):void 0,v={source:s.sourceRoot,destination:s.uploadRoot,directories_created:0,total_files:w,files_uploaded:c.length,files_failed:l.length,files_skipped:g,files_unchanged:u.length,stopped_early:y,aborted_reason:h,files:c,unchanged_files:u,failed_items:l};if("text"===o.format)C(function(e){const t=[`source: ${e.source}`,`destination: ${e.destination}`,`directories_created: ${e.directories_created}`,`total_files: ${e.total_files}`,`files_uploaded: ${e.files_uploaded}`,`files_failed: ${e.files_failed}`,`files_skipped: ${e.files_skipped}`,`files_unchanged: ${e.files_unchanged}`];if(e.stopped_early&&e.aborted_reason&&t.push(`aborted_reason: ${e.aborted_reason}`),e.failed_items.length>0){t.push("failed_items:");for(const n of e.failed_items)t.push(`- ${n.path}: ${n.error}`)}return t.join("\n")}(v)),void 0!==$&&(process.exitCode=$);else if(l.length>0){A(v,y&&h?`备份提前终止: ${h}`:`部分文件备份失败: ${l.length}/${w}`,$??S.GENERAL,"claw-backup",r,{quiet:o.quiet})}else T(v,"claw-backup",r,{quiet:o.quiet})}catch(e){L(e,"claw-backup",r)}});return n}function xt(){const r=new e("claw-restore").description("将云盘目录递归恢复到本地目录").requiredOption("--remote <path>","云盘源目录").requiredOption("--target <path>","本地目标目录").action(async e=>{const o=Date.now(),i=r.parent?.opts()||{};try{const r=P(i);if(!r.apiKey)return void L(new I(vt,S.AUTH_ERROR),"claw-restore",o);const s=Et(e.remote),a=bt(e.target);Ot(a);const c=new Ne(r),u=new je(r),l=await async function(e,t){const n=[Et(t)],r=[];for(;n.length>0;){const t=n.shift();let o=0;const i=1e3;let s=0,a=0;do{const c=await e.list({path:t,page:o,page_size:i});if(0!==c?.errno)throw new N(c?.errmsg||`列目录失败: ${t}`,{traceId:R(c),apiErrno:"number"==typeof c?.errno?c.errno:void 0});const u=It(c);a=Dt(c,u.length),s+=u.length;for(const e of u){const o=kt(e);if(!o)continue;const i=St(e),s={name:o,path:Pt(t,e,i),nid:e?.nid?String(e.nid):void 0,isDir:i};r.push(s),i&&n.push(s.path)}o+=1}while(s<a)}return r}(c,s),f=[];let d=0;for(const e of l){const r=e.path.slice(s.length).replace(/^\/+/,"");if(!r)continue;const o=n.join(a,r.split("/").join(n.sep));if(e.isDir){Ot(o),d+=1;continue}if(!e.nid)throw new Error(`缺少文件 nid,无法下载: ${e.path}`);Ot(n.dirname(o));const i=await u.download({nid:e.nid,downloadDir:n.dirname(o),auto:!0,forceForeground:!0});i.downloadPath&&i.downloadPath!==o&&t.renameSync(i.downloadPath,o),f.push(r)}const h={remote:s,target:a,directories_restored:d,files_restored:f.length,files:f};"text"===i.format?C(Object.entries({remote:s,target:a,directories_restored:d,files_restored:f.length}).map(([e,t])=>`${e}: ${t}`).join("\n")):T(h,"claw-restore",o,{quiet:i.quiet})}catch(e){L(e,"claw-restore",o)}});return r}function Mt(e,r){const o={...r,ts:r.ts||(new Date).toISOString()};return function(e){const r=n.dirname(e);t.existsSync(r)||t.mkdirSync(r,{recursive:!0,mode:493})}(e),t.appendFileSync(e,`${JSON.stringify(o)}\n`,"utf8"),o}const qt=["**/.DS_Store"];let zt=null;const Ut="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";function Kt(e){if(!Number.isInteger(e)||Number(e)<=0)return!1;try{return process.kill(Number(e),0),!0}catch(e){return"EPERM"===e?.code}}function jt(e){let t=String(e||"").trim();if(!t)throw new I("dest 不能为空",S.INVALID_ARGS);return t.startsWith("/")||(t=`/${t}`),t.endsWith("/")||(t+="/"),t}function Bt(e){if(!e.sourceDir)throw new I("--source-dir 为必填参数",S.INVALID_ARGS);const r=n.resolve(String(e.sourceDir||"").trim());const o=function(e){const t=String(e||"").trim();if(!t)throw new I("--claw-name 不能为空",S.INVALID_ARGS);if("."===t||".."===t||t.includes("/")||t.includes("\\"))throw new I("--claw-name 必须是目录名,不能包含路径分隔符",S.INVALID_ARGS);return t}(e.clawName);if(!t.existsSync(r))throw new I(`source-dir 目录不存在: ${r}`,S.INVALID_ARGS);if(!t.statSync(r).isDirectory())throw new I(`source-dir 必须是本地目录: ${r}`,S.INVALID_ARGS);return[{source_dir:r,claw_name:o,upload_root:jt(o)}]}function Wt(){return function(){try{const e=$();if(t.existsSync(e))return JSON.parse(t.readFileSync(e,"utf8"))}catch{}return null}()||{enabled:!1,watch_pairs:[]}}function Yt(e,t){if(e)try{Mt(e,t)}catch{}}function Jt(e){const t=String(e||"").trim();return t?function(e,t){const r=String(e??t??"").trim();return r?n.resolve(r):"/tmp/360disk-claw-auto-backup-events.ndjson"}(t):void 0}function Gt(e){return{env:e.env,subChannel:e.subChannel,timeout:e.timeout,retries:e.retries,hasApiKey:Boolean(e.apiKey)}}function Qt(e){const t=[];return e.env&&t.push("--env",e.env),e.subChannel&&t.push("--sub-channel",e.subChannel),e.timeout&&t.push("--timeout",e.timeout),e.retries&&t.push("--retries",e.retries),t}function Vt(e){const t=P(e);return{apiKey:t.apiKey,env:t.ecsEnv,subChannel:t.subChannel,timeout:e.timeout,retries:e.retries}}async function Xt(e,t=1e4){if(!e||!Kt(e))return;const n=Date.now();for(;Kt(e);){if(Date.now()-n>=t)throw new I(`等待旧 watcher 退出超时: ${e}`,S.CONFLICT);await new Promise(e=>setTimeout(e,200))}}function Ht(e){if(!e.pid||!e.instance_id||!Kt(e.pid))return!1;const t=function(e){if(!Number.isInteger(e)||Number(e)<=0)return"";try{return"win32"===process.platform?p("powershell.exe",["-NoProfile","-Command",`(Get-CimInstance Win32_Process -Filter "ProcessId = ${String(e)}" | Select-Object -ExpandProperty CommandLine)`],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim():p("ps",["-p",String(e),"-o","command="],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{return""}}(e.pid);return!!t&&(t.includes("claw-auto-backup __watch")&&t.includes(`--instance-id ${e.instance_id}`))}async function Zt(e){return!!e.pid&&(!!Ht(e)&&(process.kill(e.pid,"SIGTERM"),await Xt(e.pid),!0))}async function en(e,t){return new Promise((n,r)=>{let o=!1;const i=()=>{e.off("error",a),e.off("exit",c)},s=async(e,n=S.GENERAL)=>{o||(o=!0,i(),await async function(e,t){const n=Wt();if(n.instance_id===e){if(n.pid&&Kt(n.pid))try{process.kill(n.pid,"SIGTERM"),await Xt(n.pid,2e3)}catch{}tn(e,{enabled:!1,pid:void 0,instance_id:void 0,last_backup_result:{success:!1,message:t}})}}(t,e),r(new I(e,n)))},a=e=>{s(`自动备份监听启动失败: ${e.message}`)},c=(e,t)=>{s(`自动备份监听启动失败,后台进程已退出${t?`,信号: ${t}`:`,退出码: ${e??"unknown"}`}`)};e.once("error",a),e.once("exit",c);const u=Date.now(),l=()=>{if(o)return;const r=Wt();if(r.instance_id===t&&r.enabled&&r.pid===e.pid&&Ht(r))return o=!0,i(),void n(r);Date.now()-u>=6e4?s("等待自动备份 watcher 启动超时",S.CONFLICT):setTimeout(l,100)};l()})}function tn(e,t){const n=Wt();if(n.instance_id!==e)return null;const r={...n,...t};return k(r),r}async function nn(e,r,o,i){const s=await async function(){return zt||(zt=import("chokidar").then(e=>e.default||e)),zt}(),a=new Map,c=[],u=new Set,l=new Set,f=new Map,d=new Map;let h=!1;const p=e=>{tn(o,e)},m=async e=>{const t=e.source_dir,n=Date.now(),o=d.get(t);if(o?.config&&o.expiresAt>n)return o.config;if(o?.loading)return o.loading;const i=(async()=>{try{const e=P(r),n=new je(e),o=dt(await n.getSysConfig());return d.set(t,{config:o,expiresAt:Date.now()+6e4}),o}catch(e){if(o?.config)return o.config;throw e}finally{const e=d.get(t);e?.loading&&d.set(t,{config:e.config,expiresAt:e.expiresAt})}})();return d.set(t,{config:o?.config,expiresAt:o?.expiresAt||0,loading:i}),i},w=async(e,t=!0)=>{if(!h){h=!0;for(const e of a.values())clearTimeout(e);await Promise.all(c.map(e=>e.close())),Yt(i,{type:"watcher_stopped",instance_id:o,success:t,message:e||"watcher stopped"}),tn(o,{enabled:!1,pid:void 0,instance_id:void 0,event_log_path:i,last_backup_result:e?{success:t,message:e}:Wt().last_backup_result}),process.exit(t?0:1)}},_=(e,s={})=>{const{markEvent:c=!0,fsEvent:d,filePath:m}=s,w=e.source_dir;if(c){const t=f.get(w)||[];t.push({fsEvent:d,filePath:m}),f.set(w,t),p({last_event_at:(new Date).toISOString()}),Yt(i,{type:"watch_event",instance_id:o,source:e.source_dir,dest:e.upload_root,fs_event:d,path:m})}a.has(w)&&clearTimeout(a.get(w)),Yt(i,{type:"backup_scheduled",instance_id:o,source:e.source_dir,dest:e.upload_root,fs_event:d,path:m,message:"debounce timer started"});const g=setTimeout(async()=>{if(a.delete(w),f.delete(w),u.has(w))return l.add(w),void Yt(i,{type:"backup_deferred",instance_id:o,source:e.source_dir,dest:e.upload_root,message:"backup already running for source, queued next run"});u.add(w);const s=`${Date.now()}-${Math.random().toString(36).slice(2,8)}`,c=Date.now(),d=async function(e,r){const o=P(r);if(!o.apiKey)throw new I(Ut,S.AUTH_ERROR);const i=new je(o),s=[];let a=0,c=0,u=0;const l=dt(await i.getSysConfig()),f=yt(e.source_dir,l),d=f.files;c+=f.errors.length;const h=st({apiKey:o.apiKey,ecsEnv:o.ecsEnv,subChannel:o.subChannel}),p=new ct;try{for(const r of d){const o=ut(n.relative(e.source_dir,r));try{const n=t.statSync(r);if(!n.isFile())continue;const i=Math.trunc(n.mtimeMs),c=p.getFileState({cacheScope:h,sourceRoot:e.source_dir,destRoot:e.upload_root,relativePath:o}),u=c&&c.size!==n.size?void 0:it(r);if(c&&u&&c.fhash===u){p.touchUnchangedFileState(c,{absolute_path:r,size:n.size,mtime_ms:i,fhash:u}),a+=1;continue}s.push({absoluteFile:r,relativeFile:o,size:n.size,mtimeMs:i,fhash:u||it(r)})}catch{c+=1}}if(s.length>0){const t=await i.uploadTree({rootDir:e.source_dir,filePaths:s.map(e=>e.absoluteFile),uploadRoot:e.upload_root,is_rename:0}),n=new Map(s.map(e=>[e.absoluteFile,e]));for(const r of t.uploadResults){const t=n.get(r.absolutePath);if(!t)continue;const o=at({uploadResults:[r.uploadResult]});p.upsertFileState({cache_scope:h,source_root:e.source_dir,dest_root:e.upload_root,relative_path:t.relativeFile,absolute_path:t.absoluteFile,size:t.size,mtime_ms:t.mtimeMs,fhash:t.fhash,nid:o.nid,remote_file_hash:o.remote_file_hash||t.fhash,remote_name:o.remote_name,remote_dir:r.remoteDir,uploaded_at:(new Date).toISOString()}),u+=1}c+=t.uploadErrors.length}return{totalFiles:d.length,uploadedFiles:u,unchangedFiles:a,failedFiles:c}}finally{p.close()}}(e,r);Yt(i,{type:"backup_started",instance_id:o,run_id:s,source:e.source_dir,dest:e.upload_root,pid:process.pid}),d.then(t=>{const n=Date.now()-c;Yt(i,{type:"backup_finished",instance_id:o,run_id:s,source:e.source_dir,dest:e.upload_root,success:0===t.failedFiles,duration_ms:n,pid:process.pid,exit_code:0,message:`总计 ${t.totalFiles} 个,上传 ${t.uploadedFiles} 个,跳过 ${t.unchangedFiles} 个,失败 ${t.failedFiles} 个`}),h||p({last_backup_at:(new Date).toISOString(),last_backup_result:{success:0===t.failedFiles,message:`已备份: ${e.source_dir} -> ${e.upload_root},总计 ${t.totalFiles} 个,上传 ${t.uploadedFiles} 个,跳过 ${t.unchangedFiles} 个,失败 ${t.failedFiles} 个`}})}).catch(t=>{const n=Date.now()-c;Yt(i,{type:"backup_failed",instance_id:o,run_id:s,source:e.source_dir,dest:e.upload_root,success:!1,duration_ms:n,pid:process.pid,exit_code:t?.code??null,error:t?.message||"备份失败"}),h||p({last_backup_at:(new Date).toISOString(),last_backup_result:{success:!1,message:t?.message||"备份失败"}})}).finally(()=>{u.delete(w),!h&&l.has(w)&&(l.delete(w),f.set(w,[{forceBackup:!0}]),_(e,{markEvent:!1}))})},5e3);a.set(w,g)};for(const t of e){const e=s.watch(t.source_dir,{persistent:!0,ignoreInitial:!0,usePolling:!0,interval:1e4,binaryInterval:1e4,ignored:qt});e.on("all",(e,n)=>{["add","addDir","change","unlink","unlinkDir"].includes(e)&&(async()=>{try{const r=await m(t);if(!$t(t.source_dir,n,r))return;_(t,{fsEvent:e,filePath:n})}catch(r){p({last_event_at:(new Date).toISOString(),last_backup_result:{success:!1,message:r?.message||"读取自动备份云控配置失败"}}),Yt(i,{type:"backup_failed",instance_id:o,source:t.source_dir,dest:t.upload_root,fs_event:e,path:n,success:!1,error:r?.message||"读取自动备份云控配置失败"})}})()}),e.on("error",e=>{w(`监听失败: ${e.message}`,!1)}),await new Promise((t,n)=>{const r=()=>{e.off("error",o),t()},o=t=>{e.off("ready",r),n(t)};e.once("ready",r),e.once("error",o)}),c.push(e)}p({enabled:!0,pid:process.pid,instance_id:o,started_at:(new Date).toISOString(),event_log_path:i,runtime_options:Gt(r),watch_pairs:e}),Yt(i,{type:"watcher_started",instance_id:o,source:e.map(e=>e.source_dir).join(","),dest:e.map(e=>e.upload_root).join(","),pid:process.pid,message:"watcher ready"}),process.on("SIGINT",()=>{w("收到 SIGINT,监听已停止")}),process.on("SIGTERM",()=>{w("收到 SIGTERM,监听已停止")})}function rn(){const r=new e("claw-auto-backup").description("监听目录变化并自动备份到云盘");r.command("enable").description("启用自动备份监听").requiredOption("--source-dir <path>","监听的本地根目录,只能是一个目录").requiredOption("--claw-name <name>","云盘中的 OpenClaw 备份目录名").option("--event-log <path>","自动备份事件日志文件路径;仅传入时才写日志").action(async e=>{const o=Date.now(),i=r.parent?.opts()||{};try{const r=Bt(e),a=Wt(),c=Vt(i);if(!c.apiKey)return void L(new I(Ut,S.AUTH_ERROR),"claw-auto-backup enable",o);const u=Jt(e.eventLog);u&&function(e){try{if(t.existsSync(e)&&!t.statSync(e).isFile())throw new I(`event log 必须是文件路径,不能是目录: ${e}`,S.INVALID_ARGS);t.mkdirSync(n.dirname(e),{recursive:!0}),t.appendFileSync(e,"","utf8")}catch(t){if(t instanceof I)throw t;throw new I(`event log 不可写: ${e} (${t?.message||"未知错误"})`,S.INVALID_ARGS)}}(u),await Zt(a);const l=`${Date.now()}-${Math.random().toString(36).slice(2,10)}`,f=[function(){const e=process.argv[1];if(!e)throw new I("无法确定 CLI 入口文件路径",S.GENERAL);return e}(),...Qt(c),"claw-auto-backup","__watch","--instance-id",l];u&&f.push("--event-log",u);const[d]=r;f.push("--source-dir",d.source_dir,"--claw-name",d.claw_name);const p=h(process.execPath,f,{detached:!0,stdio:"ignore",env:(s=c,{...process.env,...s.apiKey?{API_KEY:s.apiKey}:{},...s.env?{ECS_ENV:s.env}:{},...s.subChannel?{SUB_CHANNEL:s.subChannel}:{}}),windowsHide:"win32"===process.platform});if(!p.pid)throw new I("启动自动备份 watcher 失败,未获取到进程 ID",S.GENERAL);k({enabled:!1,pid:p.pid,instance_id:l,started_at:(new Date).toISOString(),event_log_path:u,last_event_at:void 0,last_backup_at:void 0,last_backup_result:void 0,runtime_options:Gt(c),watch_pairs:r});const m=await en(p,l);p.unref(),"text"===i.format?C(`自动备份已启用\npid: ${p.pid}\nsource_dir: ${d.source_dir}\nclaw_name: ${d.claw_name}`):T(m,"claw-auto-backup enable",o,{quiet:i.quiet})}catch(e){L(e,"claw-auto-backup enable",o)}var s}),r.command("disable").description("停用自动备份监听").action(async()=>{const e=Date.now(),t=r.parent?.opts()||{};try{const n=Wt();await Zt(n);const r={...Wt(),enabled:!1,pid:void 0,instance_id:void 0};k(r),"text"===t.format?C("自动备份已停用"):T(r,"claw-auto-backup disable",e,{quiet:t.quiet})}catch(t){L(t,"claw-auto-backup disable",e)}}),r.command("status").description("查看自动备份状态").action(()=>{const e=Date.now(),t=r.parent?.opts()||{};try{const n=function(){const e=Wt(),t=e.enabled&&Ht(e);return{...e,enabled:e.enabled&&t,running:t}}();"text"===t.format?C(function(e,t){const n=["enabled: "+(e.enabled&&t?"true":"false"),"running: "+(t?"true":"false"),`pid: ${e.pid||"-"}`,`started_at: ${e.started_at||"-"}`,`event_log_path: ${e.event_log_path||"-"}`,`last_event_at: ${e.last_event_at||"-"}`,`last_backup_at: ${e.last_backup_at||"-"}`,"last_backup_result: "+(e.last_backup_result?`${e.last_backup_result.success?"success":"failed"} - ${e.last_backup_result.message}`:"-"),"watch_pairs:"];if(e.watch_pairs&&0!==e.watch_pairs.length)for(const t of e.watch_pairs)n.push(`- ${t.source_dir||t.source||"-"} -> ${t.upload_root||t.dest||"-"} (claw_name: ${t.claw_name||"-"})`);else n.push("- (empty)");return n.join("\n")}(n,n.running)):T(n,"claw-auto-backup status",e,{quiet:t.quiet})}catch(t){L(t,"claw-auto-backup status",e)}});const o=new e("__watch").description("内部后台 watcher 进程").option("--instance-id <id>","后台 watcher 实例标识").option("--source-dir <path>","监听的本地根目录").option("--claw-name <name>","云盘中的 OpenClaw 备份目录名").option("--event-log <path>","自动备份事件日志文件路径").action(async e=>{try{const t=Bt(e),n=e,r=n.instanceId;if(!r)throw new I("缺少 watcher instance id",S.INVALID_ARGS);const i=Vt(function(e){return e.parent?.parent?.opts?.()||e.parent?.opts?.()||{}}(o)),s=Jt(n.eventLog);await nn(t,i,r,s)}catch(t){const n=e.instanceId;n&&tn(n,{enabled:!1,pid:void 0,instance_id:void 0,event_log_path:Jt(e.eventLog),last_backup_result:{success:!1,message:t?.message||"自动备份监听启动失败"}}),process.exit(1)}});return o.u=!0,r.addCommand(o),r}(async function(){const o=function(){try{const e=n.dirname(r(import.meta.url)),o=[n.resolve(e,"../package.json"),n.resolve(e,"../../package.json"),n.resolve(e,"../../../package.json"),n.resolve(process.cwd(),"package.json")];for(const e of o)if(t.existsSync(e)){const n=JSON.parse(t.readFileSync(e,"utf8"));return{name:n.cli?.name||"360disk",version:n.version||"0.0.0",description:n.cli?.description||"360 AI 云盘 CLI"}}}catch{}return{name:"360disk",version:"0.0.0",description:"360 AI 云盘 CLI"}}(),i=new e;i.name(o.name).description(o.description).version(o.version).enablePositionalOptions().passThroughOptions().option("--api-key <key>","API 密钥").option("--env <env>","环境 (prod/test)").option("--sub-channel <channel>","子渠道").option("--format <type>","输出格式 (json|text)","json").option("--quiet","仅输出结果数据").option("--timeout <ms>","请求超时时间(毫秒)","30000").option("--retries <n>","失败重试次数","0"),i.addCommand(we()),i.addCommand(function(){const t=new e("user").description("用户信息");return t.command("info").description("获取用户详细信息").action(async()=>{const e=Date.now(),n=t.parent?.opts()||{};try{const t=P(n);if(!t.apiKey)return void L(new I("未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量",S.AUTH_ERROR),"user info",e);const r=new Oe(t),o=await r.info();F(o,"user info",e),"text"===n.format?C(Q(o)):T(o,"user info",e,{quiet:n.quiet})}catch(t){L(t,"user info",e)}}),t}()),i.addCommand(function(){const t=new e("dir").description("目录操作");return t.command("ls").description("列出云盘指定目录下的文件和文件夹").argument("[path]","目录路径","/").option("--page <n>","页码","0").option("--size <n>","每页数量","50").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=P(o);if(!t.apiKey)return void L(new I(Re,S.AUTH_ERROR),"dir ls",r);const i=new Ne(t),s=await i.list({path:e,page:parseInt(n.page),page_size:parseInt(n.size)});F(s,"dir ls",r),"text"===o.format?C(B(s)):T(s,"dir ls",r,{quiet:o.quiet})}catch(e){L(e,"dir ls",r)}}),t.command("mkdir").description("创建新文件夹").argument("<path>","文件夹路径").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=P(r);if(!t.apiKey)return void L(new I(Re,S.AUTH_ERROR),"dir mkdir",n);const o=new Ne(t),i=await o.mkdir(e);F(i,"dir mkdir",n),"text"===r.format?C(J(i,`已创建目录: ${e}`)):T(i,"dir mkdir",n,{quiet:r.quiet})}catch(e){L(e,"dir mkdir",n)}}),t}()),i.addCommand(Qe()),i.addCommand(Ft()),i.addCommand(xt()),i.addCommand(rn()),i.addCommand(rt()),await i.parseAsync()})().then(()=>{(function(){const e=process.argv.findIndex((e,t)=>t>=2&&"claw-auto-backup"===e);return!(e>=0&&"__watch"===process.argv[e+1])})()&&process.exit(process.exitCode??0)}).catch(e=>{process.stderr.write(JSON.stringify({success:!1,error:e.message||"CLI 启动失败"},null,2)+"\n"),process.exit(1)});
2
+ import{Command as e}from"commander";import t from"fs";import n from"path";import{fileURLToPath as r}from"url";import o,{homedir as i}from"os";import s from"qrcode-terminal";import a from"jsqr";import{Jimp as c}from"jimp";import{spawn as u,execFileSync as l}from"child_process";import f from"crypto";import{createRequire as d}from"module";import h from"pino";import{promises as p}from"dns";import m from"fs/promises";import w from"iconv-lite";function _(){try{const e=n.dirname(r(import.meta.url)),o=[n.resolve(e,"../package.json"),n.resolve(e,"../../package.json"),n.resolve(e,"../../../package.json"),n.resolve(process.cwd(),"package.json")];for(const e of o)if(t.existsSync(e)){const n=JSON.parse(t.readFileSync(e,"utf8"));if(n.cli?.name)return n.cli.name}}catch{}return"360disk"}function y(e){return n.join(i(),`.${e}`)}function g(){const e=process.env.AI_CLOUD_DISK_CONFIG_DIR?.trim();if(e)return function(e){const t=e.trim();if("~"===t||t.startsWith("~/")){const e="~"===t?"":t.slice(2);return e?n.join(i(),e):i()}return t}(e);return y(_())}function $(){return n.join(g(),"config.json")}function v(){return n.join(g(),"claw-auto-backup.json")}function b(){return y("360aidrive")}function E(){return n.join(b(),"claw-backup.db")}function k(){const e=g();t.existsSync(e)||t.mkdirSync(e,{recursive:!0,mode:448})}function P(){try{const e=$();if(t.existsSync(e))return JSON.parse(t.readFileSync(e,"utf8"))}catch{}return{}}function S(e){k();const n=$();t.writeFileSync(n,JSON.stringify(e,null,2),{mode:384})}function I(e){k();const n=v();t.writeFileSync(n,JSON.stringify(e,null,2),{mode:384})}function D(e){const t=P();return{apiKey:e.apiKey||process.env.API_KEY||t.api_key||"",ecsEnv:e.env||process.env.ECS_ENV||t.ecs_env||"prod",subChannel:e.subChannel||process.env.SUB_CHANNEL||t.sub_channel||"open",timeout:e.timeout?parseInt(e.timeout):void 0,retries:e.retries?parseInt(e.retries):void 0}}var O;!function(e){e[e.SUCCESS=0]="SUCCESS",e[e.GENERAL=1]="GENERAL",e[e.INVALID_ARGS=2]="INVALID_ARGS",e[e.AUTH_ERROR=3]="AUTH_ERROR",e[e.NOT_FOUND=4]="NOT_FOUND",e[e.PERMISSION_DENIED=5]="PERMISSION_DENIED",e[e.NETWORK_ERROR=6]="NETWORK_ERROR",e[e.CONFLICT=7]="CONFLICT",e[e.SERVER_ERROR=8]="SERVER_ERROR",e[e.QUOTA_EXCEEDED=10]="QUOTA_EXCEEDED",e[e.QR_INIT_FAILED=20]="QR_INIT_FAILED",e[e.QR_RENDER_FAILED=21]="QR_RENDER_FAILED",e[e.QR_POLL_TIMEOUT=22]="QR_POLL_TIMEOUT",e[e.QR_POLL_FAILED=23]="QR_POLL_FAILED",e[e.QR_PARSE_FAILED=24]="QR_PARSE_FAILED",e[e.QR_SETCOOKIE_FAILED=25]="QR_SETCOOKIE_FAILED",e[e.QR_INFO_FAILED=26]="QR_INFO_FAILED",e[e.QR_EXCHANGE_APIKEY_FAILED=27]="QR_EXCHANGE_APIKEY_FAILED"}(O||(O={}));class N extends Error{code;traceId;constructor(e,t=O.GENERAL,n){super(e),this.name="CLIError",this.code=t,this.traceId=n}}function R(e){switch(e){case 1001:case 1002:case 1003:case 3001:case 3002:case 3003:return O.AUTH_ERROR;case 3008:return O.NOT_FOUND;case 3007:case 3013:case 3129:return O.CONFLICT;case 3005:case 3006:return O.PERMISSION_DENIED;case 3010:case 3011:return O.QUOTA_EXCEEDED;default:return e>=5e3?O.SERVER_ERROR:O.GENERAL}}function T(e){const t=(e?.message||e?.toString()||"").toLowerCase();return t.includes("超时")||t.includes("timeout")||t.includes("aborted")||"AbortError"===e?.name||t.includes("econnreset")||t.includes("etimedout")||t.includes("econnrefused")||t.includes("fetch failed")||t.includes("网络")?O.NETWORK_ERROR:t.includes("api key")||t.includes("api_key")||t.includes("未配置")||t.includes("token")||t.includes("auth")||t.includes("登录")||t.includes("401")?O.AUTH_ERROR:t.includes("参数")||t.includes("argument")||t.includes("required")||t.includes("互斥")||t.includes("invalid")?O.INVALID_ARGS:t.includes("不存在")||t.includes("not found")||t.includes("404")||t.includes("no such")?O.NOT_FOUND:t.includes("权限")||t.includes("permission")||t.includes("forbidden")||t.includes("403")?O.PERMISSION_DENIED:t.includes("已存在")||t.includes("conflict")||t.includes("duplicate")||t.includes("409")?O.CONFLICT:t.includes("状态码: 5")||t.includes("500")||t.includes("502")||t.includes("503")||t.includes("服务端")?O.SERVER_ERROR:t.includes("空间不足")||t.includes("quota")||t.includes("limit")||t.includes("429")||t.includes("频率")?O.QUOTA_EXCEEDED:O.GENERAL}class A extends Error{traceId;apiErrno;constructor(e,t){super(e),this.name="ApiError",this.traceId=t?.traceId,this.apiErrno=t?.apiErrno}}function C(e){if(!e||"object"!=typeof e)return;const t=e.trace_id??e.traceId;return"string"==typeof t&&t.length>0?t:void 0}function L(e,t,n,r){if(r?.quiet)return void process.stdout.write(JSON.stringify(e)+"\n");const o={success:!0,result:e,meta:{duration_ms:Date.now()-n,command:t}};process.stdout.write(JSON.stringify(o,null,2)+"\n")}function F(e,t,n,r,o,i){if(i?.quiet)return process.stdout.write(JSON.stringify(e)+"\n"),void(process.exitCode=n||1);const s={success:!1,result:e,error:t,code:n,meta:{duration_ms:Date.now()-o,command:r}};process.stdout.write(JSON.stringify(s,null,2)+"\n"),process.exitCode=n||1}function x(e){process.stdout.write(e+"\n")}function M(e,t,n){const r=e instanceof Error?e.message:e;let o;o=e instanceof N?e.code:e instanceof A&&void 0!==e.apiErrno?R(e.apiErrno):T(e);const i={duration_ms:n?Date.now()-n:0,command:t};(e instanceof N&&e.traceId||e instanceof A&&e.traceId)&&(i.trace_id=e.traceId);process.stderr.write(JSON.stringify({success:!1,error:r,code:o,meta:i},null,2)+"\n"),process.exit(o||1)}function q(e,t,n){if(e&&"number"==typeof e.errno&&0!==e.errno){const r=R(e.errno);M(new N(e.errmsg?`${e.errmsg} (errno: ${e.errno})`:`API 错误 (errno: ${e.errno})`,r,"string"==typeof e.trace_id?e.trace_id:"string"==typeof e.traceId?e.traceId:void 0),t,n)}}function z(e,t=2){if(!e||e<=0)return"-";const n=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,n)).toFixed(t))+" "+["B","KB","MB","GB","TB"][n]}function U(e){if(!e)return"-";const t="string"==typeof e?parseInt(e):e;if(isNaN(t)||t<=0)return"-";const n=new Date(t<1e12?1e3*t:t),r=e=>String(e).padStart(2,"0");return`${n.getFullYear()}-${r(n.getMonth()+1)}-${r(n.getDate())} ${r(n.getHours())}:${r(n.getMinutes())}`}function K(e){let t=0;for(const n of e)t+=n.charCodeAt(0)>127?2:1;return t}function j(e,t){const n=t-K(e);return n>0?e+" ".repeat(n):e}function B(e,t){const n=e.map((e,n)=>{const r=t.reduce((e,t)=>Math.max(e,K(t[n]||"")),0);return Math.max(K(e),r)}),r=[];r.push(e.map((e,t)=>j(e,n[t])).join(" ")),r.push(n.map(e=>"-".repeat(e)).join(" "));for(const e of t)r.push(e.map((e,t)=>j(e||"",n[t])).join(" "));return r.join("\n")}function W(e){const t=e?.data;if(!t)return{nodes:[],total:0};const n=t.list||t.data||t.node_list||[],r=Array.isArray(n)?n:[];return{nodes:r,total:Number(t.total_count??t.total??r.length)||r.length,page:void 0!==t.page_num?Number(t.page_num):void 0}}function Y(e){return!!e&&(1===e.is_dir||"1"===e.is_dir||"dir"===e.type||(1===e.type||"1"===e.type))}function J(e){const{nodes:t,total:n,page:r}=W(e);if(0===t.length)return"(空目录)";const o=B(["类型","名称","大小","修改时间","NID"],t.map(e=>{const t=Y(e);return[t?"d":"-",e.name||e.fname||"-",t?"-":z(parseInt(e.count_size||e.size||"0")),U(e.modify_time||e.mtime),e.nid||"-"]}));let i=`共 ${n} 项`;return void 0===r||Number.isNaN(r)||(i+=`,第 ${r} 页`),o+"\n"+i}function G(e){const{nodes:t,total:n}=W(e);if(0===t.length)return"无搜索结果";return B(["类型","名称","大小","路径","NID"],t.map(e=>{const t=Y(e);return[t?"d":"-",e.name||e.fname||"-",t?"-":z(parseInt(e.count_size||e.size||"0")),e.path||"-",e.nid||"-"]}))+"\n"+`共找到 ${n} 项`}function Q(e,t,n){if(!e||"number"!=typeof e.errno||0!==e.errno)return V(e,n);const r=e.data;if(!r||"object"!=typeof r)return V(e,n);const o=e=>null==e?String(e):"object"==typeof e?JSON.stringify(e,null,2):String(e);if("config:read"===t&&"config"in r)return`${n}\n\n${o(r.config)}`;if("config:get"===t&&"value"in r)return`${n}\n\n${o(r.value)}`;if("config:list"===t&&"keys"in r){const e=r.keys;return`${n}\n\n${Array.isArray(e)?e.join("\n"):o(e)}`}return V(e,n)}function V(e,t){if(!e)return t;if(e.data?.share_url)return`${t}\n分享链接: ${e.data.share_url}`;if(e.data?.downloadUrl||e.downloadUrl){const n=e.data?.downloadUrl||e.downloadUrl,r=e.filename||"",o=e.fileSize||"",i=e.downloadPath||"";let s=t;return r&&(s+=`\n文件名: ${r}`),o&&(s+=`\n大小: ${o}`),i&&(s+=`\n保存到: ${i}`),n&&(s+=`\n链接: ${n}`),s}if(void 0!==e.fileCount){let n=`${t}\n上传: ${e.fileCount}/${e.totalFileCount} 文件,耗时 ${e.totalTime}秒`;if(e.uploadResults?.length)for(const t of e.uploadResults)n+=`\n - ${t.name||t.uploadRes?.name||"未知"}`;return e.duplicateFiles?.length&&(n+=`\n重名文件: ${e.duplicateFiles.map(e=>e.name).join(", ")}`),n}if(e.data?.task_id||e.data?.file_path){let n=t;return e.data?.file_path&&(n+=`\n路径: ${e.data.file_path}`),e.data?.file_size&&(n+=`\n大小: ${z(e.data.file_size)}`),n}return t}function X(e){if(null==e||""===e)return;if("number"==typeof e)return Number.isFinite(e)&&e>=0?e:void 0;const t=parseInt(String(e),10);return Number.isFinite(t)&&t>=0?t:void 0}function H(e){const t=e?.data;if(!t)return"无法获取用户信息";const n=function(e){const t=String(e.nickname??e.nick??e.name??"").trim(),n=null!=e.qid?String(e.qid):"";let r,o;if(null!=e.space_used){const t=Number(e.space_used);r=Number.isFinite(t)&&t>=0?t:void 0}else r=X(e.used_size);if(null!=e.space_total){const t=Number(e.space_total);o=Number.isFinite(t)&&t>=0?t:void 0}else o=X(e.total_size);let i="";const s=e.vip_type;i=null!=s&&""!==String(s).trim()?String(s).trim():1===e.is_vip||"1"===e.is_vip||!0===e.is_vip?(null!=e.vip_desc?String(e.vip_desc).trim():"")||"VIP用户":"普通用户";return{nickname:t,qid:n,usedBytes:r,totalBytes:o,vipLabel:i}}(t),r=void 0!==n.usedBytes?z(n.usedBytes):"-",o=void 0!==n.totalBytes?z(n.totalBytes):"-";return[`昵称: ${n.nickname||"-"}`,`QID: ${n.qid||"-"}`,`空间: ${r} / ${o}`,`VIP: ${n.vipLabel||"-"}`].join("\n")}const Z=Object.freeze({__proto__:null,checkApiResult:q,executeBatch:async function(e,t){const n=[];let r=0,o=0;for(let i=0;i<e.length;i++)try{const o=await t(e[i],i);n.push({index:i,input:e[i],success:!0,result:o}),r++}catch(t){n.push({index:i,input:e[i],success:!1,error:t.message}),o++}return{total:e.length,succeeded:r,failed:o,items:n}},formatBatchResult:function(e){const t=[];t.push(`批量操作完成: 成功 ${e.succeeded}/${e.total},失败 ${e.failed}/${e.total}`);for(const n of e.items){const e=n.success?"":` (${n.error})`;t.push(` ${n.success?"✓":"✗"} [${n.index+1}] ${JSON.stringify(n.input)}${e}`)}return t.join("\n")},formatBytes:z,formatFileList:J,formatMcpConfigTextResult:Q,formatSearchResult:G,formatSimpleResult:V,formatTable:B,formatUserInfo:H,outputError:M,outputFailureJson:F,outputJson:L,outputText:x}),ee="function"==typeof a?a:a.default;async function te(e){let t=e.text;if(e.imageBuffer)try{const n=await c.read(e.imageBuffer),r={data:new Uint8ClampedArray(n.bitmap.data),width:n.bitmap.width,height:n.bitmap.height},o=ee(r.data,r.width,r.height);if(!o||!o.data)throw new Error("无法从图片中识别出二维码内容");t=o.data}catch(e){throw new Error(`解析原始二维码图片失败: ${e.message}`)}if(!t)throw new Error("没有提供二维码文本或图片解析失败");return new Promise(e=>{s.generate(t,{small:!0},t=>{process.stdout.write("\n"+t+"\n"),e()})})}const ne={test:"https://qaopen.eyun.360.cn/intf.php",hgtest:"https://hg-openapi.eyun.360.cn/intf.php",prod:"https://openapi.eyun.360.cn/intf.php"};function re(e){const t=e||process.env.ECS_ENV||"prod";return{request_url:ne[t]||ne.prod,client_env:t}}const oe="https://login.360.cn/",ie="pcw_aidrive",se=`${oe}?o=sso&m=getLoginQrcode&s=3&src=${ie}&qrcodeType=miniprogram&userCenter=1`,ae="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",ce="image/png";function ue(e){const t=e.trim();try{return JSON.parse(t)}catch{}const n=t.match(/^[^(]+\((.+)\)\s*;?\s*$/s);if(!n)throw new N(`无法解析 JSONP 响应: ${t.substring(0,100)}`,O.QR_PARSE_FAILED);try{return JSON.parse(n[1])}catch(e){throw new N(`JSONP 内容 JSON 解析失败: ${e.message}`,O.QR_PARSE_FAILED)}}function le(e){const t=e.headers.get("set-cookie");return t?t.split(/,(?=\s*\w+=)/):[]}function fe(e){return Object.entries(e).map(([e,t])=>`${e}=${t}`).join("; ")}function de(e,t){for(const n of t){const t=n.match(/^([^=]+)=([^;]*)/);t&&(e[t[1].trim()]=t[2])}}function he(e){return new Promise(t=>setTimeout(t,e))}function pe(e){const t=Date.now();return`${oe}?func=${e}&src=${ie}&from=${ie}&charset=UTF-8&requestScema=https&quc_sdk_version=7.3.7&quc_sdk_name=jssdk&mid=&asc=&mname=&o=sso&m=qrLogin&_=${t}`}async function me(){const e={},t=Math.random().toString().substring(2),n=`${se}&t=0.${t}`;let r;try{r=await fetch(n,{headers:{"User-Agent":ae,Referer:"https://i.360.cn/"}})}catch(e){throw new N(`获取二维码请求失败: ${e.message}`,O.QR_INIT_FAILED)}const o=function(e,t){for(const n of e){const e=n.match(new RegExp(`${t}=([^;]+)`));if(e)return e[1]}}(le(r),"i360QRKEY");if(!o)throw new N("未能从二维码响应中获取 i360QRKEY",O.QR_INIT_FAILED);e.i360QRKEY=o;const i=Buffer.from(await r.arrayBuffer());if(i.length<100)throw new N("二维码图片数据异常",O.QR_INIT_FAILED);const s=`data:${ce};base64,${i.toString("base64")}`;return{qrKey:o,qrImageBuffer:i,qrImageDataUrl:s,cookies:e}}function we(e,r){const o=n.resolve(e.trim()),i=n.dirname(o);return t.existsSync(i)||t.mkdirSync(i,{recursive:!0,mode:448}),t.writeFileSync(o,r,{mode:384}),o}function _e(){return n.join(o.tmpdir(),`360-wechat-qr-${process.pid}-${Date.now()}.png`)}async function ye(e,t){const{cookies:n}=e,r=Date.now()+1e3*t.timeoutSec,o=`jQuery${Date.now()}_${Date.now()}`;let i;for(;Date.now()<r;){await he(t.intervalMs);const e=pe(o);let r;try{const t=await fetch(e,{headers:{"User-Agent":ae,Referer:"https://i.360.cn/",Cookie:fe(n)}});de(n,le(t)),r=await t.text()}catch(e){throw new N(`轮询请求失败: ${e.message}`,O.QR_POLL_FAILED)}const s=ue(r),a=String(s.errno);if("2"!==a){if("1020204"!==a){if("0"===a&&s.s){i=s.s;break}throw new N(`轮询返回异常 errno=${a}: ${s.errmsg||""}`,O.QR_POLL_FAILED)}process.stderr.write("已扫码,请在手机上确认登录...\n")}}if(!i)throw new N("扫码登录超时,请重试",O.QR_POLL_TIMEOUT);process.stderr.write("正在完成登录...\n");let s,a="";const c=async()=>{try{const e=await async function(e){const{q:t,t:n}={q:(r=e.cookies).Q||r.o,t:r.T||r.i};var r;if(!t||!n)return{apiKey:""};const{request_url:o,client_env:i}=re(e.env),s=new URL(o);let a,c;s.searchParams.append("method","Oauth.getApiKeyByQT"),s.searchParams.append("client_env",i),s.searchParams.append("client_src","pcw_aidrive"),s.searchParams.append("sub_channel",e.subChannel);try{a=await fetch(s.toString(),{method:"GET",headers:{Accept:"application/json",q:t,t:n}})}catch(e){throw new Error(`换取 API Key 请求失败: ${e.message}`)}if(!a.ok)throw new Error(`换取 API Key 失败,HTTP ${a.status}`);try{c=await a.json()}catch{throw new Error("换取 API Key 失败:响应不是合法 JSON")}if(0!==c.errno)throw new Error(c.errmsg||`换取 API Key 失败 errno=${c.errno}`);return{apiKey:c.data?.api_key||c.data?.apiKey||"",extra:c.data}}({cookies:n,env:t.env,subChannel:t.subChannel});e.apiKey&&(a=e.apiKey)}catch(e){s=e?.message||String(e)}};await c();const u=function(e,t){const n=Date.now(),r=encodeURIComponent(t);return`${oe}?func=${e}&src=${ie}&from=${ie}&charset=UTF-8&requestScema=https&quc_sdk_version=7.3.7&quc_sdk_name=jssdk&mid=&asc=&mname=&o=sso&m=setcookie&s=${r}&_=${n}`}(o,i);try{const e=await fetch(u,{headers:{"User-Agent":ae,Referer:"https://i.360.cn/",Cookie:fe(n)}});de(n,le(e));const t=ue(await e.text());if("0"!==String(t.errno))throw new N(`setcookie 失败: ${t.errmsg||"未知错误"}`,O.QR_SETCOOKIE_FAILED)}catch(e){if(e instanceof N)throw e;throw new N(`setcookie 请求失败: ${e.message}`,O.QR_SETCOOKIE_FAILED)}if(a||await c(),!a)throw new N(s||"换取 API Key 失败:未从登录 Cookie 中取到有效的 Q/T,或 Oauth.getApiKeyByQT 未返回 api_key",O.QR_EXCHANGE_APIKEY_FAILED);const l=function(e){const t=Date.now();return`${oe}?callback=${e}&src=${ie}&from=${ie}&charset=UTF-8&requestScema=https&quc_sdk_version=7.3.7&quc_sdk_name=jssdk&mid=&asc=&mname=&o=sso&m=info&show_name_flag=1&head_type=b&relmobile_format=hide&_=${t}`}(o);let f;try{const e=await fetch(l,{headers:{"User-Agent":ae,Referer:"https://i.360.cn/",Cookie:fe(n)}});f=ue(await e.text())}catch(e){throw new N(`获取用户信息失败: ${e.message}`,O.QR_INFO_FAILED)}if(!f.qid)throw new N("登录校验失败:未获取到用户 QID",O.QR_INFO_FAILED);return{qid:f.qid,username:f.username||f.userName,nickname:f.nickname,mobile:f.mobile,ssoCookies:n,apiKey:a}}function ge(e,r){const o={...r,ts:r.ts||(new Date).toISOString()};return function(e){const r=n.dirname(e);t.existsSync(r)||t.mkdirSync(r,{recursive:!0,mode:493})}(e),t.appendFileSync(e,`${JSON.stringify(o)}\n`,"utf8"),o}const $e=524288,ve=["","-wal","-shm"];function be(e){if(!function(e){return n.resolve(e)===n.resolve(E())}(e)||t.existsSync(e))return;const r=n.join(y("360disk"),"claw-backup.db");if(t.existsSync(r))try{!function(){const e=b();t.existsSync(e)||t.mkdirSync(e,{recursive:!0,mode:448})}();for(const n of ve){const o=`${r}${n}`,i=`${e}${n}`;t.existsSync(o)&&!t.existsSync(i)&&t.copyFileSync(o,i)}}catch{}}function Ee(e){const n=t.openSync(e,"r"),r=Buffer.allocUnsafe($e),o=[];try{let e=0;for(;;){const i=t.readSync(n,r,0,$e,e);if(0===i)break;const s=f.createHash("sha1").update(r.subarray(0,i)).digest("hex");o.push(s),e+=i}}finally{t.closeSync(n)}return f.createHash("sha1").update(o.join("")).digest("hex")}function ke(e){const t=JSON.stringify({apiKey:e.apiKey,ecsEnv:e.ecsEnv||"prod",subChannel:e.subChannel||"open"});return f.createHash("sha256").update(t).digest("hex")}function Pe(e){const t=Array.isArray(e?.uploadResults)?e.uploadResults[0]:void 0,n=t?.uploadRes;return{nid:void 0!==n?.nid?String(n.nid):void 0,remote_file_hash:void 0!==n?.file_hash?String(n.file_hash):void 0,remote_name:n?.name||t?.name}}class Se{db;constructor(e=E()){be(e),function(e){t.mkdirSync(n.dirname(e),{recursive:!0,mode:448})}(e);const r=d(import.meta.url)("better-sqlite3");this.db=new r(e),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("busy_timeout = 5000"),this.migrate()}getFileState(e){return this.db.prepare("\n SELECT *\n FROM claw_backup_files\n WHERE cache_scope = ?\n AND source_root = ?\n AND dest_root = ?\n AND relative_path = ?\n ").get(e.cacheScope,e.sourceRoot,e.destRoot,e.relativePath)}upsertFileState(e){this.db.prepare("\n INSERT INTO claw_backup_files (\n cache_scope,\n source_root,\n dest_root,\n relative_path,\n absolute_path,\n size,\n mtime_ms,\n fhash,\n nid,\n remote_file_hash,\n remote_name,\n remote_dir,\n uploaded_at,\n created_at,\n updated_at\n ) VALUES (\n @cache_scope,\n @source_root,\n @dest_root,\n @relative_path,\n @absolute_path,\n @size,\n @mtime_ms,\n @fhash,\n @nid,\n @remote_file_hash,\n @remote_name,\n @remote_dir,\n @uploaded_at,\n datetime('now'),\n datetime('now')\n )\n ON CONFLICT(cache_scope, source_root, dest_root, relative_path)\n DO UPDATE SET\n absolute_path = excluded.absolute_path,\n size = excluded.size,\n mtime_ms = excluded.mtime_ms,\n fhash = excluded.fhash,\n nid = excluded.nid,\n remote_file_hash = excluded.remote_file_hash,\n remote_name = excluded.remote_name,\n remote_dir = excluded.remote_dir,\n uploaded_at = excluded.uploaded_at,\n updated_at = datetime('now')\n ").run({...e,nid:e.nid??null,remote_file_hash:e.remote_file_hash??null,remote_name:e.remote_name??null})}touchUnchangedFileState(e,t){this.db.prepare("\n UPDATE claw_backup_files\n SET absolute_path = @absolute_path,\n size = @size,\n mtime_ms = @mtime_ms,\n fhash = @fhash,\n updated_at = datetime('now')\n WHERE id = @id\n ").run({id:e.id,...t})}close(){this.db.close()}migrate(){this.db.exec("\n CREATE TABLE IF NOT EXISTS claw_backup_files (\n -- 自增主键,仅用于本地数据库内部定位记录\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n -- 缓存作用域:由 apiKey、ECS_ENV、SUB_CHANNEL 计算出的 SHA-256 摘要,避免不同账号/环境共用缓存\n cache_scope TEXT NOT NULL,\n -- 本地备份源根路径;目录备份时为目录路径,单文件备份时为文件路径\n source_root TEXT NOT NULL,\n -- 云盘备份目标根目录,和 source_root 共同区分不同备份任务\n dest_root TEXT NOT NULL,\n -- 文件相对 source_root 的 POSIX 路径,用于保持目录结构和定位同一文件\n relative_path TEXT NOT NULL,\n -- 文件当前本地绝对路径,用于诊断源目录移动或单文件备份记录\n absolute_path TEXT NOT NULL,\n -- 本地文件大小,作为快速跳过未变化文件的第一层判断\n size INTEGER NOT NULL,\n -- 本地文件 mtime 毫秒时间戳,用于记录上传时的本地文件状态\n mtime_ms INTEGER NOT NULL,\n -- 业务文件 hash:按 512KB 分块算 bhash 后再 SHA1 得到的 fhash\n fhash TEXT NOT NULL,\n -- 云盘上传成功后返回的节点 ID,用于后续下载、定位或排查\n nid TEXT,\n -- 云盘服务端返回的 file_hash;正常应与本地 fhash 保持一致\n remote_file_hash TEXT,\n -- 云盘服务端返回的文件名或远端完整文件名,用于排查实际上传结果\n remote_name TEXT,\n -- 本次文件实际上传到的云盘目录\n remote_dir TEXT NOT NULL,\n -- 最近一次成功上传该文件的时间\n uploaded_at TEXT NOT NULL,\n -- 本地数据库记录首次创建时间\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n -- 本地数据库记录最近更新时间\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(cache_scope, source_root, dest_root, relative_path)\n );\n\n CREATE UNIQUE INDEX IF NOT EXISTS idx_claw_backup_files_scope_path\n ON claw_backup_files(cache_scope, source_root, dest_root, relative_path);\n\n -- 用于后续按内容 hash 排查或扩展去重能力\n CREATE INDEX IF NOT EXISTS idx_claw_backup_files_fhash\n ON claw_backup_files(fhash);\n ")}}function Ie(e){return e.split(n.sep).join(n.posix.sep)}function De(e){const t=String(e||"").trim();if(!t||"."===t||".."===t)return;const r=t.split(/[\\/]+/).filter(Boolean).join(n.posix.sep);return r&&"."!==r&&".."!==r&&!r.startsWith("../")?r:void 0}function Oe(e){if(!Array.isArray(e))return[];const t=new Set;for(const n of e){const e=De(n);e&&t.add(e)}return[...t].sort()}function Ne(e){const t=e?.data?.AFS;if(!t||"object"!=typeof t)throw new Error("系统云控配置缺少 data.AFS");const n=Oe(t.backup_dir),r=Oe(t.backup_match_dir),o=Oe(t.backup_file);if(0===n.length&&0===r.length&&0===o.length)throw new Error("系统云控配置 data.AFS 为空");return{backupDirs:n,backupMatchDirs:r,backupFiles:o}}function Re(e,t){const r=Ie(n.relative(e,t));return Boolean(r)&&".."!==r&&!r.startsWith("../")&&!n.posix.isAbsolute(r)}function Te(e,r){const o=t.realpathSync(e),i=t.realpathSync(r),s=Ie(n.relative(o,i));return Boolean(s)&&".."!==s&&!s.startsWith("../")&&!n.posix.isAbsolute(s)}function Ae(e){const r=[],o=[];if(!t.existsSync(e))return{files:r,errors:o};const i=[e];for(;i.length>0;){const e=i.pop();let s;try{s=t.readdirSync(e,{withFileTypes:!0})}catch(t){o.push({path:e,error:t?.message||"读取目录失败"});continue}for(const t of s){if(".DS_Store"===t.name)continue;const o=n.join(e,t.name);t.isDirectory()?i.push(o):t.isFile()&&r.push(o)}}return{files:r,errors:o}}function Ce(e,r,o,i){const s=n.resolve(e,r);if(!Re(e,s))return;if(!t.existsSync(s))return;let a;try{a=t.lstatSync(s)}catch(e){return void i.push({path:s,error:e?.message||"读取目录状态失败"})}if(a.isSymbolicLink())return;if(!a.isDirectory())return;try{if(!Te(e,s))return void i.push({path:s,error:"配置目录指向 source-dir 外部"})}catch(e){return void i.push({path:s,error:e?.message||"校验目录真实路径失败"})}const c=Ae(s);for(const e of c.files)o.add(e);i.push(...c.errors)}function Le(e,r,o,i){const s=n.resolve(e,r);if(Re(e,s)&&t.existsSync(s))try{const n=t.lstatSync(s);if(n.isSymbolicLink())return;if(n.isFile()){if(!Te(e,s))return void i.push({path:s,error:"配置文件指向 source-dir 外部"});o.add(s)}}catch(e){i.push({path:s,error:e?.message||"读取文件状态失败"})}}function Fe(e,t){return t.backupMatchDirs.some(t=>e.includes(t))}function xe(e,r){const o=new Set,i=[];let s;try{s=t.readdirSync(e,{withFileTypes:!0})}catch(t){return{files:[],errors:[{path:e,error:t?.message||"读取目录失败"}]}}for(const t of r.backupDirs)Ce(e,t,o,i);for(const t of r.backupFiles)Le(e,t,o,i);for(const t of s){const s=n.join(e,t.name);if(t.isDirectory()&&Fe(t.name,r)){const e=Ae(s);for(const t of e.files)o.add(t);i.push(...e.errors)}}return{files:[...o].sort(),errors:i}}function Me(e,t,r){const o=Ie(n.relative(e,n.resolve(t)));if(!o||".."===o||o.startsWith("../")||n.posix.isAbsolute(o))return!1;if(r.backupFiles.includes(o))return!0;if(r.backupDirs.some(e=>o===e||o.startsWith(`${e}/`)))return!0;const[i]=o.split("/");return!!i&&Fe(i,r)}function qe(e,t="e7b24b112a44fdd9ee93bdf998c6ca0e"){const n=Object.keys(e).sort().map(t=>{const n=function(e){return encodeURIComponent(e).replace(/%20/g,"+").replace(/[!'()*~]/g,e=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`)}(e[t]);return`${t}=${n}`});let r=n.join("&");return r+=t,o=r,f.createHash("md5").update(o,"utf8").digest("hex");var o}const ze="production"===process.env.NODE_ENV,Ue="true"===process.env.LOG_TO_FILE,Ke=process.env.LOG_FILE_PATH||"/data/logs/ecs-mcp/app.log",je=(process.env.LOG_TIME_FORMAT||"epoch").toLowerCase();function Be(){const e=new Date,t=new Intl.DateTimeFormat("en-CA",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}).formatToParts(e).reduce((e,t)=>("literal"!==t.type&&(e[t.type]=t.value),e),{}),n=String(e.getMilliseconds()).padStart(3,"0");return`,"time":"${`${t.year}-${t.month}-${t.day}T${t.hour}:${t.minute}:${t.second}.${n}+08:00`}"`}let We;if(Ue)We=h.destination({dest:Ke,minLength:512,sync:!1});else{const e=process.env.LOG_STDERR,t="string"==typeof e&&e.length>0,n=Array.isArray(process.argv)?process.argv.slice(2):[],r=n.includes("--stdio")||n.includes("--all"),o=t?"true"===e.toLowerCase():r;We=h.destination({fd:o?2:1,minLength:256,sync:!0})}const Ye=h({level:process.env.LOG_LEVEL||(ze?"info":"debug"),base:{service:process.env.APP_NAME||"ecs-mcp",version:process.env.APP_VERSION||"0.0.0",log_type:"app"},formatters:{level:(e,t)=>({level:e,level_number:t})},redact:{paths:["req.headers.authorization","authInfo.token","authInfo.access_token","req.body.params.arguments.access_token","req.body.params.arguments.token"],censor:"***"},timestamp:"iso"===je?h.stdTimeFunctions.isoTime:"epoch"===je?h.stdTimeFunctions.epochTime:"beijing"===je||"cst"===je||"asia/shanghai"===je?Be:h.stdTimeFunctions.isoTime},We);function Je(){try{We?.flushSync?.()}catch{}try{We?.end?.()}catch{}}async function Ge(e,t){const n=function(e){let t="",n="",r="",o="",i="";e&&(e.apiKey&&(t=e.apiKey),e.subChannel&&(n=e.subChannel),e.q&&(r=e.q),e.t&&(o=e.t),e.clientSrc&&(i=e.clientSrc)),t||(t=process.env.API_KEY||""),n||(n=process.env.SUB_CHANNEL||"open"),i||(i=process.env.CLIENT_SRC||"default");const{client_env:s}=re(e?.ecsEnv);return{apiKey:t,clientEnv:s,clientSrc:i,subChannel:n,q:r,t:o}}(t);if(!n.apiKey&&!n.q&&!n.t)throw new Error("未配置API_KEY环境变量");try{const{request_url:r}=re(t?.ecsEnv),o=n.subChannel,i=new URL(r);let s={};const a=e.extraParams;a&&"file-upload-stdio"===a.toolName?(s.method=a.method,s.client_env=a.clientEnv,s.client_src=a.clientSrc,s.qid=a.qid,s.grant_type=a.grantType,s.sub_channel=o):(s.method="Oauth.getAccessTokenByApiKeyOrQT",s.client_env=n.clientEnv,s.client_src=n.clientSrc,s.grant_type="authorization_code",s.sub_channel=o,n.apiKey&&(s.api_key=n.apiKey)),Object.entries(s).forEach(([e,t])=>{i.searchParams.append(e,t)});const c={Accept:"application/json"};n.apiKey&&(c.api_key=n.apiKey),n.q&&(c.q=n.q),n.t&&(c.t=n.t);const u=await fetch(i.toString(),{method:"GET",headers:c});if(!u.ok)throw new Error(`鉴权请求失败,状态码: ${u.status}`);const l=await u.json(),f=C(l);if(0!==l.errno)throw Ye.debug({trace_id:f,errno:l.errno,msg:"oauth response"},"oauth response error"),new A(`鉴权请求返回错误: ${l.errmsg}`,{traceId:f,apiErrno:l.errno});Ye.debug({trace_id:f,errno:0},"oauth response");const{access_token:d,qid:h,token:p}=l.data;return{access_token:d,qid:h,token:p,sub_channel:o}}catch(e){throw Ye.error({err:e},"获取鉴权信息失败"),e}}async function Qe(e={},t){Ye.debug({transportAuthInfo:t?{hasApiKey:!!t.apiKey,apiKey:t.apiKey||"",subChannel:t.subChannel,ecsEnv:t.ecsEnv,q:t.q||"",t:t.t||""}:void 0},"getAuthInfo");const n=await Ge(e,t);if(e.extraParams&&"file-upload-stdio"===e.extraParams.toolName)return n.qid=e.extraParams.qid,n;const r=qe(function(e,t,n={}){const r={};if(e.access_token&&(r.access_token=String(e.access_token)),t&&(r.method=String(t)),e.qid&&(r.qid=String(e.qid)),n)for(const[e,t]of Object.entries(n))null!=t&&(r[String(e)]=String(t));return{...r}}({access_token:n.access_token,qid:n.qid},e.method||"",e.extraParams));return n.sign=r,n}process.on("uncaughtException",e=>{Ye.error({err:e},"uncaught exception"),Je(),process.exit(1)}),process.on("unhandledRejection",e=>{Ye.error({err:e},"unhandled rejection"),Je(),process.exit(1)}),process.on("SIGINT",()=>{Je(),process.exit(0)}),process.on("SIGTERM",()=>{Je(),process.exit(0)}),process.on("beforeExit",()=>{Je()});class Ve{apiKey;ecsEnv;subChannel;timeout;retries;retryDelay;constructor(e){this.apiKey=e.apiKey,this.ecsEnv=e.ecsEnv||"prod",this.subChannel=e.subChannel||"open",this.timeout=e.timeout||3e4,this.retries=e.retries||0,this.retryDelay=e.retryDelay||1e3}getTransportAuthInfo(){return{apiKey:this.apiKey,ecsEnv:this.ecsEnv,subChannel:this.subChannel}}logApiResponse(e,t){if(!e)return;const n=C(t),r=t&&"object"==typeof t&&"errno"in t?t.errno:void 0;Ye.debug({apiMethod:e,trace_id:n,errno:"number"==typeof r?r:void 0},"api response")}throwIfApiError(e,t="API请求失败"){if(!e||"object"!=typeof e)return;if("number"!=typeof e.errno||0===e.errno)return;const n=C(e);throw new A(e.errmsg||t,{traceId:n,apiErrno:e.errno})}async getAuth(e={}){const t=this.getTransportAuthInfo(),n=await Qe(e,t);return n.request_url=re(this.ecsEnv).request_url,n}isRetryable(e){return"AbortError"===e.name||("ECONNRESET"===e.code||"ETIMEDOUT"===e.code||"ECONNREFUSED"===e.code||!!e.message?.includes("状态码: 5"))}async fetchWithRetry(e,t=0){const{url:n,init:r}=e(),o=new AbortController,i=setTimeout(()=>o.abort(),this.timeout),s={...r,signal:o.signal};try{const e=await fetch(n,s);if(clearTimeout(i),!e.ok)throw new Error(`API 请求失败,状态码: ${e.status}`);const t=await e.text();try{return JSON.parse(t)}catch{throw new Error(`无法解析API响应: ${t.substring(0,100)}...`)}}catch(n){if(clearTimeout(i),t<this.retries&&this.isRetryable(n)){const r=this.retryDelay*Math.pow(2,t);return Ye.debug({attempt:t+1,delay:r,error:n.message},"请求失败,准备重试"),await new Promise(e=>setTimeout(e,r)),this.fetchWithRetry(e,t+1)}if("AbortError"===n.name)throw new Error(`请求超时 (${this.timeout}ms)`);throw n}}async apiGet(e,t){const n=t.method,r=await this.fetchWithRetry(()=>{const n=new URL(e.request_url||"");return Object.entries(t).forEach(([e,t])=>{n.searchParams.append(e,String(t))}),{url:n.toString(),init:{method:"GET",headers:{"Access-Token":e.access_token||""}}}});return this.logApiResponse(n,r),r}async apiPost(e,t,n={}){const r=t.method,o=await this.fetchWithRetry(()=>{const r=new URL(e.request_url||"");Object.entries(t).forEach(([e,t])=>{r.searchParams.append(e,String(t))});const o=new URLSearchParams;return Object.entries(n).forEach(([e,t])=>{o.append(e,String(t))}),{url:r.toString(),init:{method:"POST",headers:{"Access-Token":e.access_token||"","Content-Type":"application/x-www-form-urlencoded"},body:o}}});return this.logApiResponse(r,o),o}buildBaseParams(e,t){return{method:t,access_token:e.access_token||"",qid:e.qid||"",sign:e.sign||"",sub_channel:e.sub_channel}}get logger(){return Ye}}const Xe=["http:","https:"],He=/[\\@]/,Ze=[/^127\./,/^10\./,/^11\./,/^172\.(1[6-9]|2\d|3[01])\./,/^192\.168\./,/^169\.254\./,/^224\./,/^0\./,/^255\./,/^::1$/,/^fe80::/i,/^fc00::/i,/^fd00::/i,/^localhost$/i,/\.local$/i,/\.lan$/i,/\.internal$/i,/\.intranet$/i,/\.corp$/i,/\.home$/i],et=[22,23,25,53,135,139,445,1433,1521,3306,3389,5432,6379,9200,11211,27017];async function tt(e,t=5e3){const n=function(e){if(!e||"string"!=typeof e)return{isValid:!1,error:"URL不能为空"};try{const n=e.trim();if(He.test(n))return{isValid:!1,error:"URL包含不安全的字符"};const r=new URL(n);if(!Xe.includes(r.protocol))return{isValid:!1,error:"不支持的协议,只支持 http/https"};const o=r.hostname.toLowerCase();if(!o)return{isValid:!1,error:"主机名不能为空"};for(const e of Ze)if(e.test(o))return{isValid:!1,error:"禁止访问受限地址"};if(nt(o)&&!rt(o))return{isValid:!1,error:"禁止访问受限IP地址"};if(r.port){const e=parseInt(r.port,10);if(et.includes(e))return{isValid:!1,error:"禁止访问受限端口"}}return t=r.pathname,[/\.\./,/%2e%2e/i,/%252e%252e/i,/\/etc\//i,/\/proc\//i,/\/sys\//i,/\/dev\//i].some(e=>e.test(t))?{isValid:!1,error:"路径包含不安全的内容"}:{isValid:!0,normalizedUrl:r.toString(),originalHostname:o}}catch(e){return{isValid:!1,error:"URL格式无效"}}var t}(e);if(!n.isValid)return n;try{const r=new URL(e).hostname.toLowerCase();if(nt(r))return{...n,resolvedIP:r,originalHostname:r,needsHostHeader:!1};let o=[];try{const e=p.lookup(r,{all:!0}),n=new Promise((e,n)=>{setTimeout(()=>n(new Error("DNS解析超时")),t)});o=(await Promise.race([e,n])).map(e=>e.address)}catch(e){return{isValid:!1,error:"域名解析失败"}}for(const e of o){for(const t of Ze)if(t.test(e))return{isValid:!1,error:"域名解析到受限IP地址"};if(!rt(e))return{isValid:!1,error:"域名解析到受限IP地址"}}return{isValid:!0,normalizedUrl:n.normalizedUrl,resolvedIP:o[0],resolvedIPs:o,originalHostname:r,needsHostHeader:!1}}catch(e){return{isValid:!1,error:"URL验证过程中发生错误"}}}function nt(e){if(/^(\d{1,3}\.){3}\d{1,3}$/.test(e)){return e.split(".").every(e=>{const t=parseInt(e,10);return t>=0&&t<=255})}return/^([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}$/i.test(e)}function rt(e){for(const t of Ze)if(t.test(e))return!1;return!0}class ot extends Ve{normalizeUploadResult(e){return{id:e?.id,name:e?.name||e?.uploadRes?.name,path:e?.uploadRes?.name,fid:e?.fid,size:e?.size||e?.uploadRes?.count_size,uploadRes:e?.uploadRes?{nid:e.uploadRes.nid,qid:e.uploadRes.qid,name:e.uploadRes.name,count_size:e.uploadRes.count_size,file_hash:e.uploadRes.file_hash,create_time:e.uploadRes.create_time,modify_time:e.uploadRes.modify_time}:void 0}}normalizeRemoteDir(e){let t=String(e||"").trim();if(!t)throw new Error("远端目录不能为空");return t.startsWith("/")||(t="/"+t),t.endsWith("/")||(t+="/"),t}toPosixRelativePath(e){return e.split(n.sep).join(n.posix.sep)}getMissingUploadPath(e){const t=e?.message||"",r="文件不存在: ";if(t.startsWith(r))return n.resolve(t.slice(7).trim())}async getSysConfig(){const e=await this.getAuth({method:"Oauth.getSysConfig"}),t=this.buildBaseParams(e,"Oauth.getSysConfig"),n=await this.apiGet(e,t);return this.throwIfApiError(n,"获取系统云控配置失败"),n}async search(e){const{key:t="",file_category:n=-1,page:r=1,page_size:o=20}=e;if(!t&&-1===n)throw new Error("必须提供搜索关键词(key)或指定文件类型(file_category)");const i={file_category:n,key:t,page:r,page_size:o},s=await this.getAuth({method:"File.searchList",extraParams:i}),a=this.buildBaseParams(s,"File.searchList"),c={};for(const[e,t]of Object.entries(i))"access_token"!==e&&(c[e]=String(t));return await this.apiPost(s,a,c)}async share(e){const t=await this.getAuth({method:"Share.preShare"}),n=this.buildBaseParams(t,"Share.preShare");return await this.apiPost(t,{},{...n,paths:e})}async getDownloadUrl(e){const{nid:t,fpath:n}=e;if(void 0===t&&!n)throw new Error("必须提供nid或fpath中的一个参数");const r={};void 0!==t?r.nid=t:n&&(r.fpath=n);const o=await this.getAuth({method:"MCP.getDownLoadUrl",extraParams:r}),i={...this.buildBaseParams(o,"MCP.getDownLoadUrl")};return void 0!==t?i.nid=String(t):n&&(i.fpath=n),await this.apiPost(o,{},i)}async getNodeInfoByNid(e){const{nid:t,ks_ext:n=0}=e;if(!t)throw new Error("nid 不能为空");if(0!==n&&1!==n)throw new Error("ks_ext 仅支持 0 或 1");const r={nid:t},o=await this.getAuth({method:"File.getNodeInfoByNid",extraParams:r}),i=this.buildBaseParams(o,"File.getNodeInfoByNid");return await this.apiGet(o,{...i,nid:t,ks_ext:String(n)})}async save(e){const{url:t,content:n,upload_path:r,file_name:o,is_rename:i=1}=e;if(!t&&!n||t&&n)throw new Error("url 与 content 互斥,必须且只能传一个");if(0!==i&&1!==i)throw new Error("is_rename 仅支持 0 或 1");if(t){if(t.includes("|"))throw new Error("MCP.saveFile 仅支持单个下载 URL,请勿使用英文竖线(|)连接多个地址;请对每个 URL 分别执行保存");const e=await tt(t.trim(),5e3);if(!e.isValid)throw new Error(`URL安全验证失败: ${e.error}`)}const s=await this.getAuth({method:"MCP.saveFile"}),a=this.buildBaseParams(s,"MCP.saveFile"),c={};r&&(c.upload_path=r),t?c.url=t:n&&(c.content=n),o&&(c.file_name=o),c.is_rename=String(i);const u=await this.apiPost(s,a,c);if(u&&0===u.errno){const e=Array.isArray(u.data)?u.data:u.data?[u.data]:[];if(0===e.length)throw new Error("保存文件失败: 未获取到任何任务信息");const t=[],n=[];for(const r of e){const e=r.task_id;if(e)try{t.push(await this.pollTaskStatus(s,e))}catch(t){n.push(`任务 ${e} 失败: ${t.message}`)}else n.push(`URL ${r.url||"未知"} 未获取到任务ID`)}if(0===t.length&&n.length>0)throw new Error(`文件保存失败: ${n.join("; ")}`);return 1===t.length?t[0]:{errno:0,errmsg:n.length>0?`部分任务失败: ${n.join("; ")}`:"",data:{tasks:t,is_multi:!0}}}throw new A(u?.errmsg||"API请求失败",{traceId:C(u),apiErrno:"number"==typeof u?.errno?u.errno:void 0})}async appendContent(e){const{path:t,content:n}=e;if(!t||!t.startsWith("/"))throw new Error("path 必须以 / 开头");if("string"!=typeof n||0===n.length)throw new Error("content 不能为空");const r={path:t,content:n},o=await this.getAuth({method:"File.appendContent",extraParams:r}),i=this.buildBaseParams(o,"File.appendContent");return await this.apiPost(o,{},{...i,path:t,content:n})}async detectFileExists(e){const{path:t,files:n}=e;if(!t||!t.startsWith("/"))throw new Error("path 必须以 / 开头");if(!Array.isArray(n)||0===n.length)throw new Error("files 不能为空");for(const e of n){if(!e?.fname||"string"!=typeof e.fname)throw new Error("files[].fname 必须为非空字符串");if(!Number.isFinite(e.fsize)||e.fsize<0)throw new Error("files[].fsize 必须为大于等于 0 的数字")}const r=JSON.stringify(n),o={path:t,data:r},i=await this.getAuth({method:"Sync.detectFileExists",extraParams:o}),s=this.buildBaseParams(i,"Sync.detectFileExists");return await this.apiGet(i,{...s,path:t,data:r})}async queryTaskStatus(e,t){const n={method:"MCP.query",qid:e.qid||"",access_token:e.access_token||"",sign:e.sign||""};return await this.apiPost(e,n,{task_id:t})}async pollTaskStatus(e,t,n=1e3,r=120){let o,i=0;for(;i<r;){if(i++,o=await this.queryTaskStatus(e,t),0!==o.errno)throw new A(o.errmsg||"查询任务状态失败",{traceId:C(o),apiErrno:o.errno});const r=o.data?.status;if(2===r)return o;if(3===r)throw new A(o.data?.error||"文件保存失败",{traceId:C(o),apiErrno:"number"==typeof o.errno?o.errno:void 0});await new Promise(e=>setTimeout(e,n))}return o}async upload(e){const{filePaths:n,uploadPath:r="/",is_rename:o}=e,i=void 0!==o?String(o):void 0;if(!n||0===n.length)throw new Error("filePaths 为必填参数且不能为空");if(void 0!==o&&0!==o&&1!==o)throw new Error("is_rename 仅支持 0 或 1");for(const e of n)if(!t.existsSync(e))throw new Error(`文件不存在: ${e}`);const s=await this.getAuth({});let a;try{const e=await import("@aicloud360/sec-sdk-node");a=e.UploadNode}catch{throw new Error("请先安装 @aicloud360/sec-sdk-node: npm install @aicloud360/sec-sdk-node")}const c={qid:s.qid||"0",token:s.token||"",access_token:s.access_token||"",env:this.ecsEnv,path:r,is_rename:i,preventAutoTokenRefresh:!1,retryForTokenError:!0,tokenRefreshCallback:async()=>{this.logger.info("SDK 触发 token 刷新");const e=await this.getAuth({});return{token:e.token,access_token:e.access_token}}};return new Promise((e,t)=>{const o=Date.now(),s=[],u=[],l=[],f=new a(c,{success:e=>{s.push(this.normalizeUploadResult(e))},progress:()=>{},error:(e,t)=>{const n=t?.errmsg||t?.message||JSON.stringify(t);u.push({fileName:e?.name||"未知文件",error:n})},duplicateList:e=>{l.push(...e.map(e=>({name:e.name,nid:e.nid,size:e.count_size,path:e.path||r})))},complete:()=>{if(u.length>0&&0===s.length){const e=new Error(`所有文件上传失败: ${u.map(e=>`${e.fileName}: ${e.error}`).join("; ")}`);return e.uploadErrors=u,e.primaryUploadError=u[0]?.error,void t(e)}e({uploadResults:s,uploadErrors:u,duplicateFiles:l,totalTime:((Date.now()-o)/1e3).toFixed(2),fileCount:s.length,totalFileCount:n.length})}});void 0!==i&&f.on("filequeue",(e,t)=>{t&&"object"==typeof t&&(t.is_rename=i,t.file&&"object"==typeof t.file&&(t.file.is_rename=i))}),f.addWaitFile(n)})}async uploadTree(e){const{rootDir:r,filePaths:o,uploadRoot:i,is_rename:s}=e,a=Date.now();if(!r)throw new Error("rootDir 为必填参数");if(!o||0===o.length)throw new Error("filePaths 为必填参数且不能为空");const c=n.resolve(r),u=this.normalizeRemoteDir(i),l=new Map;for(const e of o){const t=n.resolve(e),r=this.toPosixRelativePath(n.relative(c,t));if(!r||".."===r||r.startsWith("../")||n.posix.isAbsolute(r))throw new Error(`文件不在 rootDir 下: ${t}`);const o=n.posix.dirname(r),i="."===o?u:this.normalizeRemoteDir(n.posix.join(u,o)),s=l.get(i)||[];s.push({absolutePath:t,relativePath:r}),l.set(i,s)}const f=[],d=[],h=[];for(const[e,r]of l){let o=[...r];for(;o.length>0;){const r=[];for(const n of o)t.existsSync(n.absolutePath)?r.push(n):d.push({absolutePath:n.absolutePath,relativePath:n.relativePath,remoteDir:e,error:`文件不存在: ${n.absolutePath}`});if(o=r,0===o.length)break;let i;try{i=await this.upload({filePaths:o.map(e=>e.absolutePath),uploadPath:e,is_rename:s})}catch(t){const r=this.getMissingUploadPath(t);if(r){const n=o.findIndex(e=>e.absolutePath===r);if(n>=0){const[r]=o.splice(n,1);d.push({absolutePath:r.absolutePath,relativePath:r.relativePath,remoteDir:e,error:t.message});continue}}const i=t;if(Array.isArray(i.uploadErrors)&&i.uploadErrors.length>0)for(const t of i.uploadErrors){const r=o.find(e=>n.basename(e.absolutePath)===t.fileName);d.push({absolutePath:r?.absolutePath||t.fileName,relativePath:r?.relativePath||t.fileName,remoteDir:e,error:t.error})}else for(const n of o)d.push({absolutePath:n.absolutePath,relativePath:n.relativePath,remoteDir:e,error:t?.message||"上传失败"});break}h.push(...i.duplicateFiles);const a=[...o];for(const t of i.uploadResults){const r=n.basename(String(t?.name||t?.uploadRes?.name||"")),o=a.findIndex(e=>n.basename(e.absolutePath)===r),i=o>=0?a.splice(o,1)[0]:a.shift();i&&f.push({absolutePath:i.absolutePath,relativePath:i.relativePath,remoteDir:e,uploadResult:t})}for(const t of i.uploadErrors){const r=o.find(e=>n.basename(e.absolutePath)===t.fileName);d.push({absolutePath:r?.absolutePath||t.fileName,relativePath:r?.relativePath||t.fileName,remoteDir:e,error:t.error})}break}}return{uploadResults:f,uploadErrors:d,duplicateFiles:h,totalTime:((Date.now()-a)/1e3).toFixed(2),fileCount:f.length,totalFileCount:o.length}}async download(e){const{nid:t,auto:r=!0,downloadDir:o,forceForeground:s=!1}=e,a=o||n.join(i(),".mcp-downloads"),c=await this.getDownloadUrl({nid:String(t)});if(!c)throw new A("API请求失败");this.throwIfApiError(c);const u=c.data?.downloadUrl||"";if(!u)throw new A("未能获取到文件下载链接",{traceId:C(c)});const l=n.basename(String(c.data?.fname||"downloaded_file")),f=Number(c.data?.size||0),d=f>0?f/1048576:0,h=d>0?`${d.toFixed(2)} MB`:"未知大小";if(!r)return{downloadUrl:u,filename:l,fileSize:h,downloadPath:""};await m.mkdir(a,{recursive:!0});const p=n.join(a,l);return d>10&&!s?(this.spawnBackgroundDownload(u,p),{downloadUrl:u,filename:l,fileSize:h,downloadPath:p,background:!0}):(await this.downloadWithCurl(u,p),{downloadUrl:u,filename:l,fileSize:h,downloadPath:p})}extractFileInfoFromUrl(e){try{const t=new URL(e);let r="downloaded_file";const o=t.searchParams.get("fname");if(o?r=n.basename(decodeURIComponent(o)):t.pathname&&t.pathname.length>1&&(r=n.basename(t.pathname)),r=r.replace(/[\x00/]/g,"_"),r&&"."!==r||(r="downloaded_file"),r.length>200){const e=n.extname(r);r=n.basename(r,e).substring(0,200-e.length)+e}const i=t.searchParams.get("fsize");return{filename:r,sizeMB:i?parseInt(i,10)/1048576:0}}catch{return{filename:"downloaded_file",sizeMB:0}}}downloadWithCurl(e,t,n=3e5){return new Promise((r,o)=>{const i=u("curl",["-L","-s","-A","yunpan_mcp_server",e,"-o",t]),s=setTimeout(()=>{i.kill(),o(new Error("下载超时"))},n);i.on("close",e=>{clearTimeout(s),0===e?r():o(new Error(`下载失败,curl 退出码: ${e}`))}),i.on("error",e=>{clearTimeout(s),o(e)})})}spawnBackgroundDownload(e,t){u("curl",["-L","-s","-A","yunpan_mcp_server",e,"-o",t],{detached:!0,stdio:"ignore"}).unref()}}const it=["**/.DS_Store"];let st=null;const at="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";function ct(e){if(!Number.isInteger(e)||Number(e)<=0)return!1;try{return process.kill(Number(e),0),!0}catch(e){return"EPERM"===e?.code}}function ut(e){let t=String(e||"").trim();if(!t)throw new N("dest 不能为空",O.INVALID_ARGS);return t.startsWith("/")||(t=`/${t}`),t.endsWith("/")||(t+="/"),t}function lt(e){if(!e.sourceDir)throw new N("--source-dir 为必填参数",O.INVALID_ARGS);const r=n.resolve(String(e.sourceDir||"").trim());const o=function(e){const t=String(e||"").trim();if(!t)throw new N("--claw-name 不能为空",O.INVALID_ARGS);if("."===t||".."===t||t.includes("/")||t.includes("\\"))throw new N("--claw-name 必须是目录名,不能包含路径分隔符",O.INVALID_ARGS);return t}(e.clawName);if(!t.existsSync(r))throw new N(`source-dir 目录不存在: ${r}`,O.INVALID_ARGS);if(!t.statSync(r).isDirectory())throw new N(`source-dir 必须是本地目录: ${r}`,O.INVALID_ARGS);return[{source_dir:r,claw_name:o,upload_root:ut(o)}]}function ft(){return function(){try{const e=v();if(t.existsSync(e))return JSON.parse(t.readFileSync(e,"utf8"))}catch{}return null}()||{enabled:!1,watch_pairs:[]}}function dt(e,t){if(e)try{ge(e,t)}catch{}}function ht(e){const t=String(e||"").trim();return t?function(e,t){const r=String(e??t??"").trim();return r?n.resolve(r):"/tmp/360disk-claw-auto-backup-events.ndjson"}(t):void 0}function pt(e){return{env:e.env,subChannel:e.subChannel,timeout:e.timeout,retries:e.retries,hasApiKey:Boolean(e.apiKey)}}function mt(e){const t=[];return e.env&&t.push("--env",e.env),e.subChannel&&t.push("--sub-channel",e.subChannel),e.timeout&&t.push("--timeout",e.timeout),e.retries&&t.push("--retries",e.retries),t}function wt(e){const t=D(e);return{apiKey:t.apiKey,env:t.ecsEnv,subChannel:t.subChannel,timeout:e.timeout,retries:e.retries}}async function _t(e,t=1e4){if(!e||!ct(e))return;const n=Date.now();for(;ct(e);){if(Date.now()-n>=t)throw new N(`等待旧 watcher 退出超时: ${e}`,O.CONFLICT);await new Promise(e=>setTimeout(e,200))}}function yt(e){if(!e.pid||!e.instance_id||!ct(e.pid))return!1;const t=function(e){if(!Number.isInteger(e)||Number(e)<=0)return"";try{return"win32"===process.platform?l("powershell.exe",["-NoProfile","-Command",`(Get-CimInstance Win32_Process -Filter "ProcessId = ${String(e)}" | Select-Object -ExpandProperty CommandLine)`],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim():l("ps",["-p",String(e),"-o","command="],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{return""}}(e.pid);return!!t&&(t.includes("claw-auto-backup __watch")&&t.includes(`--instance-id ${e.instance_id}`))}async function gt(e){return!!e.pid&&(!!yt(e)&&(process.kill(e.pid,"SIGTERM"),await _t(e.pid),!0))}async function $t(){const e=ft();await gt(e);I({...ft(),enabled:!1,pid:void 0,instance_id:void 0})}async function vt(e,t){return new Promise((n,r)=>{let o=!1;const i=()=>{e.off("error",a),e.off("exit",c)},s=async(e,n=O.GENERAL)=>{o||(o=!0,i(),await async function(e,t){const n=ft();if(n.instance_id===e){if(n.pid&&ct(n.pid))try{process.kill(n.pid,"SIGTERM"),await _t(n.pid,2e3)}catch{}bt(e,{enabled:!1,pid:void 0,instance_id:void 0,last_backup_result:{success:!1,message:t}})}}(t,e),r(new N(e,n)))},a=e=>{s(`自动备份监听启动失败: ${e.message}`)},c=(e,t)=>{s(`自动备份监听启动失败,后台进程已退出${t?`,信号: ${t}`:`,退出码: ${e??"unknown"}`}`)};e.once("error",a),e.once("exit",c);const u=Date.now(),l=()=>{if(o)return;const r=ft();if(r.instance_id===t&&r.enabled&&r.pid===e.pid&&yt(r))return o=!0,i(),void n(r);Date.now()-u>=6e4?s("等待自动备份 watcher 启动超时",O.CONFLICT):setTimeout(l,100)};l()})}function bt(e,t){const n=ft();if(n.instance_id!==e)return null;const r={...n,...t};return I(r),r}async function Et(e,r,o,i){const s=await async function(){return st||(st=import("chokidar").then(e=>e.default||e)),st}(),a=new Map,c=[],u=new Set,l=new Set,f=new Map,d=new Map;let h=!1;const p=e=>{bt(o,e)},m=async e=>{const t=e.source_dir,n=Date.now(),o=d.get(t);if(o?.config&&o.expiresAt>n)return o.config;if(o?.loading)return o.loading;const i=(async()=>{try{const e=D(r),n=new ot(e),o=Ne(await n.getSysConfig());return d.set(t,{config:o,expiresAt:Date.now()+6e4}),o}catch(e){if(o?.config)return o.config;throw e}finally{const e=d.get(t);e?.loading&&d.set(t,{config:e.config,expiresAt:e.expiresAt})}})();return d.set(t,{config:o?.config,expiresAt:o?.expiresAt||0,loading:i}),i},w=async(e,t=!0)=>{if(!h){h=!0;for(const e of a.values())clearTimeout(e);await Promise.all(c.map(e=>e.close())),dt(i,{type:"watcher_stopped",instance_id:o,success:t,message:e||"watcher stopped"}),bt(o,{enabled:!1,pid:void 0,instance_id:void 0,event_log_path:i,last_backup_result:e?{success:t,message:e}:ft().last_backup_result}),process.exit(t?0:1)}},_=(e,s={})=>{const{markEvent:c=!0,fsEvent:d,filePath:m}=s,w=e.source_dir;if(c){const t=f.get(w)||[];t.push({fsEvent:d,filePath:m}),f.set(w,t),p({last_event_at:(new Date).toISOString()}),dt(i,{type:"watch_event",instance_id:o,source:e.source_dir,dest:e.upload_root,fs_event:d,path:m})}a.has(w)&&clearTimeout(a.get(w)),dt(i,{type:"backup_scheduled",instance_id:o,source:e.source_dir,dest:e.upload_root,fs_event:d,path:m,message:"debounce timer started"});const y=setTimeout(async()=>{if(a.delete(w),f.delete(w),u.has(w))return l.add(w),void dt(i,{type:"backup_deferred",instance_id:o,source:e.source_dir,dest:e.upload_root,message:"backup already running for source, queued next run"});u.add(w);const s=`${Date.now()}-${Math.random().toString(36).slice(2,8)}`,c=Date.now(),d=async function(e,r){const o=D(r);if(!o.apiKey)throw new N(at,O.AUTH_ERROR);const i=new ot(o),s=[];let a=0,c=0,u=0;const l=Ne(await i.getSysConfig()),f=xe(e.source_dir,l),d=f.files;c+=f.errors.length;const h=ke({apiKey:o.apiKey,ecsEnv:o.ecsEnv,subChannel:o.subChannel}),p=new Se;try{for(const r of d){const o=Ie(n.relative(e.source_dir,r));try{const n=t.statSync(r);if(!n.isFile())continue;const i=Math.trunc(n.mtimeMs),c=p.getFileState({cacheScope:h,sourceRoot:e.source_dir,destRoot:e.upload_root,relativePath:o}),u=c&&c.size!==n.size?void 0:Ee(r);if(c&&u&&c.fhash===u){p.touchUnchangedFileState(c,{absolute_path:r,size:n.size,mtime_ms:i,fhash:u}),a+=1;continue}s.push({absoluteFile:r,relativeFile:o,size:n.size,mtimeMs:i,fhash:u||Ee(r)})}catch{c+=1}}if(s.length>0){const t=await i.uploadTree({rootDir:e.source_dir,filePaths:s.map(e=>e.absoluteFile),uploadRoot:e.upload_root,is_rename:0}),n=new Map(s.map(e=>[e.absoluteFile,e]));for(const r of t.uploadResults){const t=n.get(r.absolutePath);if(!t)continue;const o=Pe({uploadResults:[r.uploadResult]});p.upsertFileState({cache_scope:h,source_root:e.source_dir,dest_root:e.upload_root,relative_path:t.relativeFile,absolute_path:t.absoluteFile,size:t.size,mtime_ms:t.mtimeMs,fhash:t.fhash,nid:o.nid,remote_file_hash:o.remote_file_hash||t.fhash,remote_name:o.remote_name,remote_dir:r.remoteDir,uploaded_at:(new Date).toISOString()}),u+=1}c+=t.uploadErrors.length}return{totalFiles:d.length,uploadedFiles:u,unchangedFiles:a,failedFiles:c}}finally{p.close()}}(e,r);dt(i,{type:"backup_started",instance_id:o,run_id:s,source:e.source_dir,dest:e.upload_root,pid:process.pid}),d.then(t=>{const n=Date.now()-c;dt(i,{type:"backup_finished",instance_id:o,run_id:s,source:e.source_dir,dest:e.upload_root,success:0===t.failedFiles,duration_ms:n,pid:process.pid,exit_code:0,message:`总计 ${t.totalFiles} 个,上传 ${t.uploadedFiles} 个,跳过 ${t.unchangedFiles} 个,失败 ${t.failedFiles} 个`}),h||p({last_backup_at:(new Date).toISOString(),last_backup_result:{success:0===t.failedFiles,message:`已备份: ${e.source_dir} -> ${e.upload_root},总计 ${t.totalFiles} 个,上传 ${t.uploadedFiles} 个,跳过 ${t.unchangedFiles} 个,失败 ${t.failedFiles} 个`}})}).catch(t=>{const n=Date.now()-c;dt(i,{type:"backup_failed",instance_id:o,run_id:s,source:e.source_dir,dest:e.upload_root,success:!1,duration_ms:n,pid:process.pid,exit_code:t?.code??null,error:t?.message||"备份失败"}),h||p({last_backup_at:(new Date).toISOString(),last_backup_result:{success:!1,message:t?.message||"备份失败"}})}).finally(()=>{u.delete(w),!h&&l.has(w)&&(l.delete(w),f.set(w,[{forceBackup:!0}]),_(e,{markEvent:!1}))})},5e3);a.set(w,y)};for(const t of e){const e=s.watch(t.source_dir,{persistent:!0,ignoreInitial:!0,usePolling:!0,interval:1e4,binaryInterval:1e4,ignored:it});e.on("all",(e,n)=>{["add","addDir","change","unlink","unlinkDir"].includes(e)&&(async()=>{try{const r=await m(t);if(!Me(t.source_dir,n,r))return;_(t,{fsEvent:e,filePath:n})}catch(r){p({last_event_at:(new Date).toISOString(),last_backup_result:{success:!1,message:r?.message||"读取自动备份云控配置失败"}}),dt(i,{type:"backup_failed",instance_id:o,source:t.source_dir,dest:t.upload_root,fs_event:e,path:n,success:!1,error:r?.message||"读取自动备份云控配置失败"})}})()}),e.on("error",e=>{w(`监听失败: ${e.message}`,!1)}),await new Promise((t,n)=>{const r=()=>{e.off("error",o),t()},o=t=>{e.off("ready",r),n(t)};e.once("ready",r),e.once("error",o)}),c.push(e)}p({enabled:!0,pid:process.pid,instance_id:o,started_at:(new Date).toISOString(),event_log_path:i,runtime_options:pt(r),watch_pairs:e}),dt(i,{type:"watcher_started",instance_id:o,source:e.map(e=>e.source_dir).join(","),dest:e.map(e=>e.upload_root).join(","),pid:process.pid,message:"watcher ready"}),process.on("SIGINT",()=>{w("收到 SIGINT,监听已停止")}),process.on("SIGTERM",()=>{w("收到 SIGTERM,监听已停止")})}function kt(){const r=new e("claw-auto-backup").description("监听目录变化并自动备份到云盘");r.command("enable").description("启用自动备份监听").requiredOption("--source-dir <path>","监听的本地根目录,只能是一个目录").requiredOption("--claw-name <name>","云盘中的 OpenClaw 备份目录名").option("--event-log <path>","自动备份事件日志文件路径;仅传入时才写日志").action(async e=>{const o=Date.now(),i=r.parent?.opts()||{};try{const r=lt(e),a=ft(),c=wt(i);if(!c.apiKey)return void M(new N(at,O.AUTH_ERROR),"claw-auto-backup enable",o);const l=ht(e.eventLog);l&&function(e){try{if(t.existsSync(e)&&!t.statSync(e).isFile())throw new N(`event log 必须是文件路径,不能是目录: ${e}`,O.INVALID_ARGS);t.mkdirSync(n.dirname(e),{recursive:!0}),t.appendFileSync(e,"","utf8")}catch(t){if(t instanceof N)throw t;throw new N(`event log 不可写: ${e} (${t?.message||"未知错误"})`,O.INVALID_ARGS)}}(l),await gt(a);const f=`${Date.now()}-${Math.random().toString(36).slice(2,10)}`,d=[function(){const e=process.argv[1];if(!e)throw new N("无法确定 CLI 入口文件路径",O.GENERAL);return e}(),...mt(c),"claw-auto-backup","__watch","--instance-id",f];l&&d.push("--event-log",l);const[h]=r;d.push("--source-dir",h.source_dir,"--claw-name",h.claw_name);const p=u(process.execPath,d,{detached:!0,stdio:"ignore",env:(s=c,{...process.env,...s.apiKey?{API_KEY:s.apiKey}:{},...s.env?{ECS_ENV:s.env}:{},...s.subChannel?{SUB_CHANNEL:s.subChannel}:{}}),windowsHide:"win32"===process.platform});if(!p.pid)throw new N("启动自动备份 watcher 失败,未获取到进程 ID",O.GENERAL);I({enabled:!1,pid:p.pid,instance_id:f,started_at:(new Date).toISOString(),event_log_path:l,last_event_at:void 0,last_backup_at:void 0,last_backup_result:void 0,runtime_options:pt(c),watch_pairs:r});const m=await vt(p,f);p.unref(),"text"===i.format?x(`自动备份已启用\npid: ${p.pid}\nsource_dir: ${h.source_dir}\nclaw_name: ${h.claw_name}`):L(m,"claw-auto-backup enable",o,{quiet:i.quiet})}catch(e){M(e,"claw-auto-backup enable",o)}var s}),r.command("disable").description("停用自动备份监听").action(async()=>{const e=Date.now(),t=r.parent?.opts()||{};try{await $t();const n=ft();"text"===t.format?x("自动备份已停用"):L(n,"claw-auto-backup disable",e,{quiet:t.quiet})}catch(t){M(t,"claw-auto-backup disable",e)}}),r.command("status").description("查看自动备份状态").action(()=>{const e=Date.now(),t=r.parent?.opts()||{};try{const n=function(){const e=ft(),t=e.enabled&&yt(e);return{...e,enabled:e.enabled&&t,running:t}}();"text"===t.format?x(function(e,t){const n=["enabled: "+(e.enabled&&t?"true":"false"),"running: "+(t?"true":"false"),`pid: ${e.pid||"-"}`,`started_at: ${e.started_at||"-"}`,`event_log_path: ${e.event_log_path||"-"}`,`last_event_at: ${e.last_event_at||"-"}`,`last_backup_at: ${e.last_backup_at||"-"}`,"last_backup_result: "+(e.last_backup_result?`${e.last_backup_result.success?"success":"failed"} - ${e.last_backup_result.message}`:"-"),"watch_pairs:"];if(e.watch_pairs&&0!==e.watch_pairs.length)for(const t of e.watch_pairs)n.push(`- ${t.source_dir||t.source||"-"} -> ${t.upload_root||t.dest||"-"} (claw_name: ${t.claw_name||"-"})`);else n.push("- (empty)");return n.join("\n")}(n,n.running)):L(n,"claw-auto-backup status",e,{quiet:t.quiet})}catch(t){M(t,"claw-auto-backup status",e)}});const o=new e("__watch").description("内部后台 watcher 进程").option("--instance-id <id>","后台 watcher 实例标识").option("--source-dir <path>","监听的本地根目录").option("--claw-name <name>","云盘中的 OpenClaw 备份目录名").option("--event-log <path>","自动备份事件日志文件路径").action(async e=>{try{const t=lt(e),n=e,r=n.instanceId;if(!r)throw new N("缺少 watcher instance id",O.INVALID_ARGS);const i=wt(function(e){return e.parent?.parent?.opts?.()||e.parent?.opts?.()||{}}(o)),s=ht(n.eventLog);await Et(t,i,r,s)}catch(t){const n=e.instanceId;n&&bt(n,{enabled:!1,pid:void 0,instance_id:void 0,event_log_path:ht(e.eventLog),last_backup_result:{success:!1,message:t?.message||"自动备份监听启动失败"}}),process.exit(1)}});return o.u=!0,r.addCommand(o),r}function Pt(){const r=new e("auth").description("鉴权管理").enablePositionalOptions().passThroughOptions(),o=e=>e.option("--format <type>","输出格式 (json/text)","json").option("--quiet","是否静默输出",!1);return o(r.command("login")).description("登录:保存 API Key 到本地配置").requiredOption("--api-key <api_key>","API 密钥").option("--env <env>","环境 (prod/test)","prod").option("--sub-channel <channel>","子渠道","open").action(e=>{const t=Date.now();try{const n=P();n.api_key=e.apiKey,n.login_mode="api_key",delete n.sso_login_at,delete n.sso_user,delete n.sso_cookies,e.env&&(n.ecs_env=e.env),e.subChannel&&(n.sub_channel=e.subChannel),S(n);const r=e;"text"===r.format?x(`登录成功!配置已保存到 ${$()}`):L({message:"登录成功",configPath:$()},"auth login",t,{quiet:r.quiet})}catch(e){M(e,"auth login",t)}}),o(r.command("login-wechat")).description("使用微信扫码登录").option("--timeout <sec>","轮询总超时秒数","120").option("--interval <ms>","轮询间隔毫秒","1500").option("--qr-mode <mode>","二维码呈现:terminal(终端字符码)或 file(PNG),默认 terminal","terminal").option("--qr-file <path>","与 --qr-mode=file 联用:PNG 输出路径(未指定则写入系统临时目录)").option("--env <env>","环境 (prod/test)","prod").option("--sub-channel <channel>","子渠道","open").action(async e=>{const t=Date.now(),r=e,o=process.argv.includes("--format")&&"json"===r.format;try{const i=String(e.qrMode||"terminal").toLowerCase();if("terminal"!==i&&"file"!==i)return void M(new N("无效 --qr-mode,仅支持 terminal 或 file",O.INVALID_ARGS),"auth login-wechat",t);const s={timeoutSec:parseInt(e.timeout,10),intervalMs:parseInt(e.interval,10),env:e.env,subChannel:e.subChannel,qrPresentMode:"file"===i?"file":"terminal",qrFilePath:e.qrFile};if(!o&&!r.quiet){const o=await async function(e){const t=await me();if("file"===e.qrPresentMode){process.stderr.write("正在获取登录二维码...\n");const r=we(e.qrFilePath?.trim()?n.resolve(e.qrFilePath.trim()):_e(),t.qrImageBuffer);process.stderr.write(`二维码已写入: ${r}\n请使用微信扫描该 PNG 图片完成登录\n`)}else{process.stderr.write("正在获取登录二维码...\n");const e=`https://login.360.cn/?o=sso&m=getLoginQrcode&s=3&src=${ie}&qrcodeType=miniprogram&userCenter=1&qrkey=${t.qrKey}`;try{await te({text:e,imageBuffer:t.qrImageBuffer})}catch(t){try{await te({text:e})}catch(e){throw new N(`二维码渲染失败: ${e.message}`,O.QR_RENDER_FAILED)}}process.stderr.write("请使用微信扫描上方二维码登录\n")}return await ye(t,e)}(s),i=P();i.api_key=o.apiKey,i.ecs_env=e.env,i.sub_channel=e.subChannel,i.login_mode="wechat_qr",i.sso_login_at=Date.now(),i.sso_user={qid:o.qid,username:o.username,nickname:o.nickname,mobile:o.mobile},i.sso_cookies=o.ssoCookies,S(i);const a={message:"微信扫码登录成功",login_mode:"wechat_qr",qid:o.qid,username:o.username,nickname:o.nickname,configPath:$()};return void("text"===r.format?x(`微信扫码登录成功\nQID: ${o.qid}\n昵称: ${o.nickname||"-"}\n配置已保存到 ${$()}`):L(a,"auth login-wechat",t,{quiet:r.quiet}))}const a=await me(),c=parseInt(e.timeout,10);let u;if("file"===i){const t="string"==typeof e.qrFile?e.qrFile.trim():"",r=we(t?n.resolve(t):_e(),a.qrImageBuffer);u={type:"qr_file",qr_key:a.qrKey,qr_image_path:r,qr_image_mime:ce,expires_in:c}}else u={type:"qr",qr_key:a.qrKey,qr_image_data_url:a.qrImageDataUrl,qr_image_mime:ce,qr_image_base64:a.qrImageBuffer.toString("base64"),expires_in:c};process.stdout.write(`${JSON.stringify(u)}\n`);const l=await ye(a,{timeoutSec:parseInt(e.timeout,10),intervalMs:parseInt(e.interval,10),env:e.env,subChannel:e.subChannel}),f=P();f.api_key=l.apiKey,f.ecs_env=e.env,f.sub_channel=e.subChannel,f.login_mode="wechat_qr",f.sso_login_at=Date.now(),f.sso_user={qid:l.qid,username:l.username,nickname:l.nickname,mobile:l.mobile},f.sso_cookies=l.ssoCookies,S(f);const d={success:!0,result:{message:"微信扫码登录成功",login_mode:"wechat_qr",qid:l.qid,username:l.username,nickname:l.nickname,configPath:$()},meta:{duration_ms:Date.now()-t,command:"auth login-wechat"}};process.stdout.write(`${JSON.stringify(d)}\n`)}catch(e){M(e,"auth login-wechat",t)}}),o(r.command("whoami")).description("查看当前鉴权状态").action(e=>{const t=Date.now();try{const n=P(),r=e,o={logged_in:!!n.api_key,login_mode:n.login_mode||(n.api_key?"api_key":void 0),api_key:n.api_key?n.api_key.substring(0,10)+"***":void 0,ecs_env:n.ecs_env||process.env.ECS_ENV||"prod",sub_channel:n.sub_channel||process.env.SUB_CHANNEL||"open",sso_user:n.sso_user,sso_login_at:n.sso_login_at,config_path:$()};if("text"===r.format)if(o.logged_in){const e=["已登录",`登录方式: ${o.login_mode||"-"}`,`API Key: ${o.api_key}`,`环境: ${o.ecs_env}`,`渠道: ${o.sub_channel}`];o.sso_user?.qid&&e.push(`QID: ${o.sso_user.qid}`),o.sso_user?.nickname&&e.push(`昵称: ${o.sso_user.nickname}`),x(e.join("\n"))}else x("未登录。请使用 auth login --api-key <API_KEY> 或 auth login-wechat 登录");else L(o,"auth whoami",t,{quiet:r.quiet})}catch(e){M(e,"auth whoami",t)}}),o(r.command("logout")).description("退出登录:停止自动备份监听并清除本地配置").action(async e=>{const n=Date.now();try{await async function(){try{await $t()}catch{}}(),function(){try{const e=$();t.existsSync(e)&&t.unlinkSync(e)}catch{}}();const r=e;"text"===r.format?x("已退出登录,本地配置已清除"):L({message:"已退出登录"},"auth logout",n,{quiet:r.quiet})}catch(e){M(e,"auth logout",n)}}),r}class St extends Ve{async info(){const e=await this.getAuth({}),t=this.buildBaseParams(e,"User.getUserDetail");return t.sign="",await this.apiGet(e,t)}}class It extends Ve{async list(e={}){const{path:t="/",page:n=0,page_size:r=50}=e;let o=t||"/";"/"!==o&&(o.startsWith("/")||(o="/"+o),o.endsWith("/")||(o+="/"));const i={path:o,page:n,page_size:r},s=await this.getAuth({method:"File.getList",extraParams:i}),a={...this.buildBaseParams(s,"File.getList")};for(const[e,t]of Object.entries(i))"access_token"!==e&&(a[e]=String(t));return await this.apiGet(s,a)}async mkdir(e){const t={fname:e},n=await this.getAuth({method:"File.mkdir",extraParams:t}),r=this.buildBaseParams(n,"File.mkdir");return await this.apiPost(n,{},{...r,fname:e})}}const Dt="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";function Ot(e){let t=String(e||"").trim();return t=t.replace(/^\uFEFF/,""),t.startsWith("-e ")&&(t=t.slice(3).trim(),(t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))&&(t=t.slice(1,-1))),t.replace(/\\r\\n/g,"\r\n").replace(/\\n/g,"\n")}async function Nt(){if(process.stdin.isTTY){if("win32"===process.platform)throw new Error("--stdin 模式需要管道输入。Windows cmd 勿用 echo -e(会把 -e 写入内容且编码易乱码),请用 --content、或 chcp 65001 后 echo、或 type/PowerShell Get-Content 读 UTF-8 文件管道;追加/保存示例:type x.md | 360disk file save --stdin ...");throw new Error('--stdin 模式需要管道输入,例如: echo "content" | 360disk file save --stdin')}const e=[];for await(const t of process.stdin)e.push(Buffer.from(t));return function(e){if(0===e.length)return"";const t=e=>e.replace(/^\uFEFF/,"");if("win32"!==process.platform)return t(e.toString("utf-8"));const n=t(e.toString("utf-8"));if(!/\uFFFD/.test(n))return n;try{const n=t(w.decode(e,"gbk"));if(!/\uFFFD/.test(n))return n}catch{}return n}(Buffer.concat(e))}async function Rt(){const e=await Nt();return Ot(e).split(/\r?\n/).map(e=>e.trim()).filter(Boolean)}class Tt extends Ve{async move(e){const{src_name:t,new_name:n}=e,r={src_name:t,new_name:n},o=await this.getAuth({method:"File.move",extraParams:r}),i=this.buildBaseParams(o,"File.move");return await this.apiPost(o,{},{...i,src_name:t,new_name:n})}async rename(e){const{src_name:t,new_name:n}=e,r={src_name:t,new_name:n},o=await this.getAuth({method:"File.rename",extraParams:r}),i=this.buildBaseParams(o,"File.rename");return await this.apiPost(o,{},{...i,src_name:t,new_name:n})}async delete(e){const t=await this.getAuth({method:"File.delete"}),n=this.buildBaseParams(t,"File.delete");return await this.apiPost(t,{},{...n,fname:e})}async transOrCopy(e){const{src_name:t,new_path:n,is_delete:r,is_replace:o=0,src_ks_id:i,new_ks_id:s}=e;if(0!==r&&1!==r)throw new Error("is_delete 仅支持 0 或 1");if(0!==o&&1!==o)throw new Error("is_replace 仅支持 0 或 1");const a={src_name:t,new_path:n,is_delete:r},c=await this.getAuth({method:"File.transOrCopy",extraParams:a}),u={...this.buildBaseParams(c,"File.transOrCopy"),src_name:t,new_path:n,is_delete:String(r),is_replace:String(o)};return i&&(u.src_ks_id=i),s&&(u.new_ks_id=s),await this.apiPost(c,{},u)}async countOriginSize(e){const{path:t}=e;if(!t||!t.startsWith("/"))throw new Error("path 必须以 / 开头");const n={path:t},r=await this.getAuth({method:"File.countFileOriginSize",extraParams:n}),o=this.buildBaseParams(r,"File.countFileOriginSize");return await this.apiPost(r,{},{...o,path:t})}async clearDir(e){const{fname:t}=e;if(!t)throw new Error("fname 不能为空");if(t.includes("|"))throw new Error("File.clearDir 仅支持单个目录路径,勿用英文竖线(|)拼接;竖线会被服务端视为路径非法字符(errno:3022)。请对每个目录分别调用");const n=await this.getAuth({method:"File.clearDir"}),r=this.buildBaseParams(n,"File.clearDir");return await this.apiPost(n,{},{...r,fname:t})}async config(e){const{path:t,command:n,type:r,key:o,value:i,content:s}=e;if(!t||!t.startsWith("/"))throw new Error("path 必须以 / 开头");const a="yml"===r?"yaml":r,c={path:t,command:n,type:a},u=await this.getAuth({method:"MCP.config",extraParams:c}),l={...this.buildBaseParams(u,"MCP.config"),path:t,command:n,type:a};return void 0!==o&&(l.key=o),void 0!==i&&(l.value=i),void 0!==s&&(l.content=s),await this.apiPost(u,{},l)}}const At="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";function Ct(e,t,n,r,o){"text"===r.format&&o?x(o(e)):L(e,t,n,{quiet:r.quiet})}async function Lt(e,t){const n=await e.delete(t);if(3008===n?.errno&&!t.endsWith("/")){const n=await e.delete(t+"/");if(0===n?.errno)return n}return n}function Ft(e){return"string"!=typeof e?e:e.replace(/\\r\\n/g,"\r\n").replace(/\\n/g,"\n")}function xt(e){let t;for(const n of function(e){const t=[],n=new Set,r=e=>{const r=String(e).trim();r&&!n.has(r)&&(n.add(r),t.push(r))},o=String(e??"").trim().replace(/^\uFEFF/,"");if(r(o),o.length>=2&&o.startsWith("'")&&o.endsWith("'")){const e=o.slice(1,-1).trim();r(e),r(e.replace(/\\"/g,'"'))}return r(o.replace(/\\"/g,'"')),t}(e))try{t=JSON.parse(n);break}catch{}if(void 0===t)throw new N('files 必须是合法 JSON(示例: [{"fname":"a.txt","fsize":123}])。Windows cmd 不支持单引号包裹参数,请用双引号并对内部 " 转义,或改用 --stdin。',O.INVALID_ARGS);if("string"==typeof t)try{t=JSON.parse(t)}catch{throw new N('files 必须是合法 JSON(示例: [{"fname":"a.txt","fsize":123}])',O.INVALID_ARGS)}if(!Array.isArray(t)||0===t.length)throw new N("files 必须是非空数组",O.INVALID_ARGS);for(const e of t){if(!e||"string"!=typeof e.fname||""===e.fname.trim())throw new N("files[].fname 必须为非空字符串",O.INVALID_ARGS);if(!Number.isFinite(e.fsize)||e.fsize<0)throw new N("files[].fsize 必须为大于等于 0 的数字",O.INVALID_ARGS)}return t}function Mt(){const t=new e("file").description("文件操作");return t.command("mv").description("移动文件或文件夹").argument("<src>","源路径,多个用 | 分隔").argument("<dest>","目标文件夹路径").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=D(o);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file mv",r);const i=new Tt(t),s=await async function(e,t,n){const r=await e.move({src_name:t,new_name:n}),o=!t.includes("|");if(3008===r?.errno&&o&&!t.endsWith("/")){const r=await e.move({src_name:t+"/",new_name:n});if(0===r?.errno)return r}return r}(i,e,n);q(s,"file mv",r),Ct(s,"file mv",r,o,t=>V(t,`已移动: ${e} -> ${n}`))}catch(e){M(e,"file mv",r)}}),t.command("trans-copy").description("转移或复制文件到目标目录").argument("<src>","源路径,多个用 | 分隔").argument("<dest>","目标目录路径").option("--delete <0|1>","是否删除源文件:1=转移,0=复制","1").option("--replace <0|1>","同名文件处理:0=重命名,1=覆盖","0").option("--src-ks-id <id>","源文件所在群组 ks_id(可选)").option("--new-ks-id <id>","目标目录所在群组 ks_id(可选)").action(async(e,n,r)=>{const o=Date.now(),i=t.parent?.opts()||{};try{const t=D(i);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file trans-copy",o);const s=parseInt(r.delete,10),a=parseInt(r.replace,10);if(![0,1].includes(s))return void M(new N("--delete 仅支持 0 或 1",O.INVALID_ARGS),"file trans-copy",o);if(![0,1].includes(a))return void M(new N("--replace 仅支持 0 或 1",O.INVALID_ARGS),"file trans-copy",o);if(!n.startsWith("/"))return void M(new N("dest 必须以 / 开头",O.INVALID_ARGS),"file trans-copy",o);const c=new Tt(t),u=await c.transOrCopy({src_name:e,new_path:n,is_delete:s,is_replace:a,src_ks_id:r.srcKsId,new_ks_id:r.newKsId});q(u,"file trans-copy",o),Ct(u,"file trans-copy",o,i,t=>V(t,`${1===s?"转移":"复制"}完成: ${e} -> ${n}`))}catch(e){M(e,"file trans-copy",o)}}),t.command("rename").description("重命名文件或文件夹").argument("<path>","原文件完整路径").argument("<new_name>","新名称").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=D(o);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file rename",r);const i=function(e,t){const n=String(e||"").trim();let r=String(t||"").trim();const o=n.endsWith("/");if(!n.startsWith("/"))throw new N("path 必须以 / 开头",O.INVALID_ARGS);if(!r)throw new N("new_name 不能为空",O.INVALID_ARGS);if(o){r=r.replace(/^\/+/,"");const e=r.replace(/\/+$/,"").split("/").filter(Boolean);if(0===e.length)throw new N("new_name 不能为空(目录场景需传新目录名,如 bbb_renamed/)",O.INVALID_ARGS);return r=e[e.length-1]+"/",{src_name:n,new_name:r,isDir:!0}}if(r.includes("/"))throw new N("new_name 仅支持文件名(不含路径)。若要改路径请使用 file mv",O.INVALID_ARGS);return{src_name:n,new_name:r,isDir:!1}}(e,n),s=new Tt(t),a=await s.rename({src_name:i.src_name,new_name:i.new_name});q(a,"file rename",r),Ct(a,"file rename",r,o,e=>V(e,`已重命名: ${i.src_name} -> ${i.new_name}`))}catch(e){M(e,"file rename",r)}}),t.command("rm").description("删除文件或文件夹").argument("<path>","文件路径,多个用 | 分隔").option("--batch","批量模式:从 stdin 读取路径列表(每行一个)").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=D(o);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file rm",r);const i=new Tt(t);if(n.batch){const t=await Rt(),n=[e,...t].filter(Boolean),{executeBatch:s,formatBatchResult:a}=await Promise.resolve().then(function(){return Z});Ct(await s(n,async e=>{const t=await Lt(i,e);if(t&&"number"==typeof t.errno&&0!==t.errno){throw new A(t.errmsg?`${t.errmsg} (errno: ${t.errno})`:`API 错误 (errno: ${t.errno})`,{traceId:C(t),apiErrno:t.errno})}return t}),"file rm --batch",r,o,e=>a(e))}else{const t=await Lt(i,e);q(t,"file rm",r),Ct(t,"file rm",r,o,t=>V(t,`已删除: ${e}`))}}catch(e){M(e,"file rm",r)}}),t.command("search").description("按关键词搜索文件").argument("<keyword>","搜索关键词").option("--type <type>","文件类型: -1(全部) 0(其他) 1(图片) 2(文档) 3(音乐) 4(视频)","-1").option("--page <n>","页码","1").option("--size <n>","每页数量","20").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=D(o);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file search",r);const i=new ot(t),s=await i.search({key:e,file_category:parseInt(n.type),page:parseInt(n.page),page_size:parseInt(n.size)});q(s,"file search",r),Ct(s,"file search",r,o,G)}catch(e){M(e,"file search",r)}}),t.command("share").description("生成文件分享链接").argument("<paths>","文件路径,多个用 | 分隔").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=D(r);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file share",n);const o=new ot(t),i=await o.share(e);q(i,"file share",n),Ct(i,"file share",n,r,e=>V(e,"分享链接已生成"))}catch(e){M(e,"file share",n)}}),t.command("url").description("获取文件下载链接").argument("<path>","文件路径").option("--nid <nid>","文件 NID(与路径二选一)").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=D(o);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file url",r);const i=new ot(t),s=await i.getDownloadUrl({nid:n.nid,fpath:n.nid?void 0:e});q(s,"file url",r),Ct(s,"file url",r,o,e=>V(e,"下载链接已获取"))}catch(e){M(e,"file url",r)}}),t.command("node-info").description("根据 nid 获取节点信息(可选返回 ks_info)").argument("<nid>","节点 nid(仅支持文件夹/知识库)").option("--ks-ext <0|1>","是否返回 ks_info:0=不返回,1=返回","0").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=D(o);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file node-info",r);const i=parseInt(n.ksExt,10);if(![0,1].includes(i))return void M(new N("--ks-ext 仅支持 0 或 1",O.INVALID_ARGS),"file node-info",r);const s=new ot(t),a=await s.getNodeInfoByNid({nid:e,ks_ext:i});q(a,"file node-info",r),Ct(a,"file node-info",r,o,t=>V(t,`节点信息获取成功: ${e}`))}catch(e){M(e,"file node-info",r)}}),t.command("origin-size").description("统计目录下文件和文件夹(递归)原始大小").argument("<path>","目录完整路径").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=D(o);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file origin-size",r);if(!e.startsWith("/"))return void M(new N("path 必须以 / 开头",O.INVALID_ARGS),"file origin-size",r);const n=new Tt(t),i=await n.countOriginSize({path:e});q(i,"file origin-size",r),Ct(i,"file origin-size",r,o,t=>V(t,`目录大小统计完成: ${e}`))}catch(e){M(e,"file origin-size",r)}}),t.command("clear-dir").description("清空目录下文件,保留目录本身(每次仅一个目录)").argument("<path>","单个目录路径,必须以 / 开头").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=D(o);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file clear-dir",r);if(!e.startsWith("/"))return void M(new N("目录 path 必须以 / 开头",O.INVALID_ARGS),"file clear-dir",r);if(e.includes("|"))return void M(new N("每次仅支持清空一个目录,不支持用英文竖线(|)连接多个路径(会触发服务端非法字符错误);请分多次执行 file clear-dir",O.INVALID_ARGS),"file clear-dir",r);const n=new Tt(t),i=await n.clearDir({fname:e});q(i,"file clear-dir",r),Ct(i,"file clear-dir",r,o,t=>V(t,`目录清空完成: ${e}`))}catch(e){M(e,"file clear-dir",r)}}),t.command("config").description("读取/写入/列出配置文件(INI/JSON/YAML)").requiredOption("--path <path>","配置文件路径,必须以 / 开头").requiredOption("--command <cmd>","操作命令:config:get|config:set|config:delete|config:list|config:read|config:write").requiredOption("--type <type>","配置类型:ini|json|yaml|yml").option("--key <key>","键路径,如 a.b.c").option("--value <value>","写入值(set 场景)").option("--content <text>","写入完整内容(write 场景,与 --stdin 互斥)").option("--stdin","从标准输入读取 content(write 场景,与 --content 互斥)").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=D(r);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file config",n);if(!e.path.startsWith("/"))return void M(new N("--path 必须以 / 开头",O.INVALID_ARGS),"file config",n);if(!new Set(["config:get","config:set","config:delete","config:list","config:read","config:write"]).has(e.command))return void M(new N("--command 非法",O.INVALID_ARGS),"file config",n);if(!new Set(["ini","json","yaml","yml"]).has(e.type))return void M(new N("--type 非法",O.INVALID_ARGS),"file config",n);if("config:write"===e.command){if(1!==[e.content,e.stdin].filter(Boolean).length)return void M(new N("config:write 场景下 --content 与 --stdin 互斥,必须且只能传一个",O.INVALID_ARGS),"file config",n)}else if(e.content||e.stdin)return void M(new N("--content/--stdin 仅在 config:write 场景使用",O.INVALID_ARGS),"file config",n);let o=Ft(e.content);if(e.stdin&&(o=await Nt(),!o.trim()))return void M(new N("stdin 输入内容为空",O.INVALID_ARGS),"file config",n);const i=new Tt(t),s=await i.config({path:e.path,command:e.command,type:e.type,key:e.key,value:e.value,content:o});q(s,"file config",n),Ct(s,"file config",n,r,t=>Q(t,e.command,`配置操作完成: ${e.command}`))}catch(e){M(e,"file config",n)}}),t.command("save").description("通过 URL 或文本内容保存文件到云盘").option("--url <url>","单个文件下载地址(与 --content/--stdin 互斥;勿用 | 连接多个 URL)").option("--content <text>","文件内容(与 --url/--stdin 互斥)").option("--stdin","从标准输入读取内容(与 --url/--content 互斥)").option("--dest <path>","云盘存储路径,必须以 / 开头和结尾").option("--filename <name>","保存的文件名").option("--rename <0|1>","同名文件处理策略:0=直接替换,1=自动重命名","1").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=D(r);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file save",n);if(1!==[e.url,e.content,e.stdin].filter(Boolean).length)return void M(new N("--url、--content、--stdin 三者互斥,必须且只能传一个",O.INVALID_ARGS),"file save",n);const o=parseInt(e.rename,10);if(![0,1].includes(o))return void M(new N("--rename 仅支持 0 或 1",O.INVALID_ARGS),"file save",n);if(e.url&&String(e.url).includes("|"))return void M(new N("`--url` 仅支持单个下载地址,不支持用英文竖线(|)连接多个 URL;请对每个地址分别执行 file save",O.INVALID_ARGS),"file save",n);let i=Ft(e.content);if(e.stdin&&(i=Ot(await Nt()),!i.trim()))return void M(new N("stdin 输入内容为空",O.INVALID_ARGS),"file save",n);const s=new ot(t),a=await s.save({url:e.url,content:i,upload_path:e.dest,file_name:e.filename,is_rename:o});q(a,"file save",n),Ct(a,"file save",n,r,e=>V(e,"文件保存成功"))}catch(e){M(e,"file save",n)}}),t.command("append").description("向云盘文本文件末尾追加内容").argument("<path>","文件完整路径,必须以 / 开头").option("--content <text>","追加的文本内容(与 --stdin 互斥)").option("--stdin","从标准输入读取追加内容(与 --content 互斥;Windows cmd 用 type 文件 | ... 勿用 cat)").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=D(o);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file append",r);if(!e.startsWith("/"))return void M(new N("path 必须以 / 开头",O.INVALID_ARGS),"file append",r);if(1!==[n.content,n.stdin].filter(Boolean).length)return void M(new N("--content 与 --stdin 互斥,必须且只能传一个",O.INVALID_ARGS),"file append",r);let i=Ft(n.content);if(n.stdin&&(i=Ot(await Nt()),!i||!i.trim()))return void M(new N("stdin 输入内容为空",O.INVALID_ARGS),"file append",r);const s=new ot(t),a=await s.appendContent({path:e,content:i});q(a,"file append",r),Ct(a,"file append",r,o,t=>V(t,`已追加内容到: ${e}`))}catch(e){M(e,"file append",r)}}),t.command("exists").description("检测目录下是否存在同名文件").option("--path <path>","目标目录,必须以 / 开头").option("--files <json>",'待检测文件数组 JSON,例如 [{"fname":"a.txt","fsize":123}](与 --stdin 互斥;Windows cmd 勿用单引号,见文档)').option("--stdin","从标准输入读取 files JSON(与 --files 互斥)").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=D(r);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file exists",n);if(!e.path||!e.path.startsWith("/"))return void M(new N("--path 必须以 / 开头",O.INVALID_ARGS),"file exists",n);if(1!==[e.files,e.stdin].filter(Boolean).length)return void M(new N("--files 与 --stdin 互斥,必须且只能传一个",O.INVALID_ARGS),"file exists",n);const o=xt(e.stdin?await Nt():e.files),i=new ot(t),s=await i.detectFileExists({path:e.path,files:o});q(s,"file exists",n),Ct(s,"file exists",n,r,t=>V(t,`检测完成: ${e.path}`))}catch(e){M(e,"file exists",n)}}),t.command("upload").description("上传本地文件到云盘").argument("<files>","本地文件路径,多个用逗号分隔").option("--dest <path>","云盘目标路径","/").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=D(o);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file upload",r);const i=e.split(",").map(e=>e.trim()),s=new ot(t);Ct(await s.upload({filePaths:i,uploadPath:n.dest}),"file upload",r,o,e=>V(e,"上传完成"))}catch(e){M(e,"file upload",r)}}),t.command("download").description("下载云盘文件到本地").argument("<nid>","文件 NID(可通过 file search / dir ls 获取)").option("--dir <path>","本地下载目录").option("--no-auto","仅获取下载链接,不自动下载").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=D(o);if(!t.apiKey)return void M(new N(At,O.AUTH_ERROR),"file download",r);const i=new ot(t);Ct(await i.download({nid:e,auto:n.auto,downloadDir:n.dir}),"file download",r,o,e=>V(e,"下载完成"))}catch(e){M(e,"file download",r)}}),t}function qt(e){return`${e} CLI auto-completion`}function zt(e){return"_"+e.replace(/[^a-zA-Z0-9]/g,"_")}function Ut(){return g()}function Kt(){const e=process.env.SHELL||"";return e.includes("zsh")?"zsh":e.includes("bash")?"bash":"unknown"}function jt(e){const t=zt(e),n=`~/.${e}`;return`# ${e} bash completion\n# 安装: source ${n}/completion.bash\n# 或添加到 ~/.bashrc: source ${n}/completion.bash\n\n${t}_completions() {\n local cur prev commands\n COMPREPLY=()\n cur="\${COMP_WORDS[COMP_CWORD]}"\n prev="\${COMP_WORDS[COMP_CWORD-1]}"\n\n # 顶级命令\n local top_commands="auth user dir file claw-backup claw-restore claw-auto-backup completion help"\n\n # 子命令映射\n local auth_commands="login whoami logout"\n local user_commands="info"\n local dir_commands="ls mkdir"\n local file_commands="mv rename rm search share url save upload download"\n local completion_commands="install uninstall script"\n\n # 全局选项\n local global_options="--api-key --env --sub-channel --format --quiet --timeout --retries --help --version"\n\n case "\${COMP_WORDS[1]}" in\n auth)\n case "$prev" in\n login) COMPREPLY=( $(compgen -W "--api-key --env --sub-channel --help" -- "$cur") ) ;;\n whoami|logout) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$auth_commands" -- "$cur") ) ;;\n esac\n ;;\n user)\n case "$prev" in\n info) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$user_commands" -- "$cur") ) ;;\n esac\n ;;\n dir)\n case "$prev" in\n ls) COMPREPLY=( $(compgen -W "--page --size --help" -- "$cur") ) ;;\n mkdir) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$dir_commands" -- "$cur") ) ;;\n esac\n ;;\n file)\n case "$prev" in\n search) COMPREPLY=( $(compgen -W "--type --page --size --help" -- "$cur") ) ;;\n url) COMPREPLY=( $(compgen -W "--nid --help" -- "$cur") ) ;;\n save) COMPREPLY=( $(compgen -W "--url --content --stdin --dest --filename --help" -- "$cur") ) ;;\n upload) COMPREPLY=( $(compgen -W "--dest --help" -- "$cur") ) ;;\n download) COMPREPLY=( $(compgen -W "--dir --no-auto --help" -- "$cur") ) ;;\n rm) COMPREPLY=( $(compgen -W "--batch --help" -- "$cur") ) ;;\n mv|rename|share) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$file_commands" -- "$cur") ) ;;\n esac\n ;;\n completion)\n case "$prev" in\n install|uninstall) COMPREPLY=( $(compgen -W "--bash --zsh --help" -- "$cur") ) ;;\n script) COMPREPLY=( $(compgen -W "--bash --zsh --help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$completion_commands" -- "$cur") ) ;;\n esac\n ;;\n claw-backup)\n COMPREPLY=( $(compgen -W "--source --dest --source-dir --claw-name --help" -- "$cur") )\n ;;\n claw-restore)\n COMPREPLY=( $(compgen -W "--remote --target --help" -- "$cur") )\n ;;\n claw-auto-backup)\n case "\${COMP_WORDS[2]}" in\n enable) COMPREPLY=( $(compgen -W "--source-dir --claw-name --event-log --help" -- "$cur") ) ;;\n disable|status) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "enable disable status --help" -- "$cur") ) ;;\n esac\n ;;\n *)\n if [[ "$cur" == -* ]]; then\n COMPREPLY=( $(compgen -W "$global_options" -- "$cur") )\n else\n COMPREPLY=( $(compgen -W "$top_commands" -- "$cur") )\n fi\n ;;\n esac\n}\n\ncomplete -F ${t}_completions ${e}\n`}function Bt(e){const t=zt(e),n=`~/.${e}`;return`#compdef ${e}\n# ${e} zsh completion\n# 安装: source ${n}/completion.zsh\n# 或添加到 ~/.zshrc: source ${n}/completion.zsh\n\n${t}() {\n local -a top_commands\n top_commands=(\n 'auth:鉴权管理'\n 'user:用户信息'\n 'dir:目录操作'\n 'file:文件操作'\n 'claw-backup:上传本地文件或目录到云盘'\n 'claw-restore:递归恢复云盘目录到本地'\n 'claw-auto-backup:监听目录变化并自动备份到云盘'\n 'completion:Shell 自动补全'\n 'help:显示帮助'\n )\n\n local -a auth_commands\n auth_commands=(\n 'login:登录(保存 API Key)'\n 'whoami:查看鉴权状态'\n 'logout:退出登录(含停止自动备份监听)'\n )\n\n local -a user_commands\n user_commands=(\n 'info:获取用户详细信息'\n )\n\n local -a dir_commands\n dir_commands=(\n 'ls:列出目录内容'\n 'mkdir:创建文件夹'\n )\n\n local -a file_commands\n file_commands=(\n 'mv:移动文件或文件夹'\n 'rename:重命名文件或文件夹'\n 'rm:删除文件或文件夹'\n 'search:搜索文件'\n 'share:生成分享链接'\n 'url:获取下载链接'\n 'save:保存文件到云盘'\n 'upload:上传文件'\n 'download:下载文件'\n )\n\n local -a completion_commands\n completion_commands=(\n 'install:安装补全脚本'\n 'uninstall:卸载补全脚本'\n 'script:输出补全脚本'\n )\n\n _arguments -C \\\n '--api-key[API 密钥]:key:' \\\n '--env[环境]:env:(prod test)' \\\n '--sub-channel[子渠道]:channel:' \\\n '--format[输出格式]:format:(json text)' \\\n '--quiet[仅输出结果]' \\\n '--timeout[超时时间(ms)]:ms:' \\\n '--retries[重试次数]:n:' \\\n '--help[显示帮助]' \\\n '--version[显示版本]' \\\n '1:command:->command' \\\n '2:subcommand:->subcommand' \\\n '*::arg:->args'\n\n case "$state" in\n command)\n _describe 'command' top_commands\n ;;\n subcommand)\n case "$words[2]" in\n auth) _describe 'auth command' auth_commands ;;\n user) _describe 'user command' user_commands ;;\n dir) _describe 'dir command' dir_commands ;;\n file) _describe 'file command' file_commands ;;\n completion) _describe 'completion command' completion_commands ;;\n claw-backup) _arguments '--source[本地源文件或目录]:path:_files' '--dest[云盘目标目录]:path:' '--source-dir[OpenClaw本地根目录]:path:_files -/' '--claw-name[云盘备份目录名]:name:' ;;\n claw-restore) _arguments '--remote[云盘源目录]:path:' '--target[本地目标目录]:path:_files -/' ;;\n claw-auto-backup) _arguments '1:subcommand:(enable disable status)' ;;\n esac\n ;;\n args)\n case "$words[2]" in\n claw-auto-backup)\n case "$words[3]" in\n enable) _arguments '--source-dir[监听的本地根目录]:path:_files -/' '--claw-name[云盘备份目录名]:name:' '--event-log[事件日志文件]:path:' ;;\n esac\n ;;\n file)\n case "$words[3]" in\n search) _arguments '--type[文件类型]:type:(-1 0 1 2 3 4)' '--page[页码]:n:' '--size[每页数量]:n:' ;;\n save) _arguments '--url[文件URL]:url:' '--content[文件内容]:text:' '--stdin[从stdin读取]' '--dest[目标路径]:path:' '--filename[文件名]:name:' ;;\n upload) _arguments '--dest[云盘路径]:path:' ;;\n download) _arguments '--dir[下载目录]:path:_files -/' '--no-auto[不自动下载]' ;;\n url) _arguments '--nid[文件NID]:nid:' ;;\n rm) _arguments '--batch[批量模式]' ;;\n esac\n ;;\n dir)\n case "$words[3]" in\n ls) _arguments '--page[页码]:n:' '--size[每页数量]:n:' ;;\n esac\n ;;\n auth)\n case "$words[3]" in\n login) _arguments '--api-key[API密钥]:key:' '--env[环境]:env:(prod test)' '--sub-channel[渠道]:channel:' ;;\n esac\n ;;\n esac\n ;;\n esac\n}\n\n${t} "$@"\n`}function Wt(e){const t=i();return n.join(t,"zsh"===e?".zshrc":".bashrc")}function Yt(){const r=new e("completion").description("Shell 自动补全");return r.command("install").description("安装补全脚本到当前 shell").option("--bash","安装 bash 补全").option("--zsh","安装 zsh 补全").action(e=>{try{const r=e.bash?"bash":e.zsh?"zsh":Kt();if("unknown"===r)return void M("无法检测 shell 类型,请使用 --bash 或 --zsh 指定","completion install");const o=_(),i="zsh"===r?Bt(o):jt(o),s="zsh"===r?"zsh":"bash",a=Ut(),c=n.join(a,`completion.${s}`);t.existsSync(a)||t.mkdirSync(a,{recursive:!0,mode:448}),t.writeFileSync(c,i,{mode:420});const u=Wt(r),l=function(e,t){return`source ~/.${e}/completion.${"zsh"===t?"zsh":"bash"} # ${qt(e)}`}(o,r);let f="";t.existsSync(u)&&(f=t.readFileSync(u,"utf8")),!function(e,t){return!!e.includes(qt(t))||!("360disk"!==t||!e.includes("360disk auto-completion"))}(f,o)?(t.appendFileSync(u,"\n"+l+"\n"),x(`补全脚本已安装:\n 脚本: ${c}\n 配置: ${u}\n\n请执行 source ${u} 或重新打开终端生效`)):x(`补全脚本已更新: ${c}\n配置已存在于 ${u},无需重复添加`)}catch(e){M(e,"completion install")}}),r.command("uninstall").description("卸载补全脚本").option("--bash","卸载 bash 补全").option("--zsh","卸载 zsh 补全").action(e=>{try{const r=e.bash?"bash":e.zsh?"zsh":Kt();if("unknown"===r)return void M("无法检测 shell 类型,请使用 --bash 或 --zsh 指定","completion uninstall");const o="zsh"===r?"zsh":"bash",i=n.join(Ut(),`completion.${o}`);t.existsSync(i)&&t.unlinkSync(i);const s=Wt(r);if(t.existsSync(s)){const e=t.readFileSync(s,"utf8"),n=_(),r=qt(n),o=e.split("\n").filter(e=>!e.includes(r)&&("360disk"!==n||!e.includes("360disk auto-completion")));t.writeFileSync(s,o.join("\n"))}x(`补全脚本已卸载\n请执行 source ${Wt(r)} 或重新打开终端生效`)}catch(e){M(e,"completion uninstall")}}),r.command("script").description("输出补全脚本到 stdout(手动安装用)").option("--bash","输出 bash 补全脚本").option("--zsh","输出 zsh 补全脚本").action(e=>{try{const t=e.bash?"bash":e.zsh?"zsh":Kt();if("unknown"===t)return void M("无法检测 shell 类型,请使用 --bash 或 --zsh 指定","completion script");const n=_(),r="zsh"===t?Bt(n):jt(n);process.stdout.write(r)}catch(e){M(e,"completion script")}}),r}const Jt="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";function Gt(e){return n.resolve(e)}function Qt(e){let t=String(e||"").trim();if(!t)throw new N("远端目录不能为空",O.INVALID_ARGS);return t.startsWith("/")||(t="/"+t),t.endsWith("/")||(t+="/"),t}function Vt(e){return String(e?.name||e?.fname||"").trim()}function Xt(e,t,r){const o=Vt(t);if(!o)return"";let i=o;return i.startsWith("/")||(i=n.posix.join(e,i)),r?Qt(i):i}function Ht(e){return!!e&&(1===e.is_dir||"1"===e.is_dir||"dir"===e.type||(1===e.type||"1"===e.type))}function Zt(e){const t=e?.data;if(!t)return[];const n=t.list||t.data||t.node_list||[];return Array.isArray(n)?n:[]}function en(e,t){const n=e?.data||{},r=Number(n.total_count??n.total??t);return Number.isFinite(r)?r:t}function tn(e){t.mkdirSync(e,{recursive:!0})}function nn(e){return".DS_Store"===e}function rn(e){const r=t.statSync(e);if(r.isDirectory())return function(e){const r=[""],o=[];return function e(i,s){const a=t.readdirSync(i,{withFileTypes:!0});for(const t of a){const a=n.join(i,t.name),c=s?n.posix.join(s,t.name):t.name;if(t.isDirectory())r.push(c),e(a,c);else if(t.isFile()){if(nn(t.name))continue;o.push(c)}}}(e,""),{dirs:r,files:o}}(e).files;if(r.isFile())return nn(n.basename(e))?[]:[n.basename(e)];throw new N(`--source 必须是文件或目录: ${e}`,O.INVALID_ARGS)}async function on(e,r){const o=Boolean(e.source||e.dest),i=Boolean(e.sourceDir||e.clawName);if(o&&i)throw new N("通用备份参数 --source/--dest 不能和 OpenClaw 参数 --source-dir/--claw-name 混用",O.INVALID_ARGS);if(!o&&!i)throw new N("必须传入 --source/--dest,或传入 --source-dir/--claw-name",O.INVALID_ARGS);if(i){if(!e.sourceDir)throw new N("--source-dir 为必填参数",O.INVALID_ARGS);const o=Gt(e.sourceDir),i=function(e){const t=String(e||"").trim();if(!t)throw new N("--claw-name 不能为空",O.INVALID_ARGS);if("."===t||".."===t||t.includes("/")||t.includes("\\"))throw new N("--claw-name 必须是目录名,不能包含路径分隔符",O.INVALID_ARGS);return t}(e.clawName);if(!t.existsSync(o))throw new N(`source-dir 目录不存在: ${o}`,O.INVALID_ARGS);if(!t.statSync(o).isDirectory())throw new N(`source-dir 必须是本地目录: ${o}`,O.INVALID_ARGS);const s=Ne(await r.getSysConfig()),a=xe(o,s);return{sourceRoot:o,uploadRoot:Qt(i),uploadTreeRoot:o,files:a.files.map(e=>({absoluteFile:e,relativeFile:sn(n.relative(o,e))})),initialFailures:a.errors.map(e=>({path:sn(n.relative(o,e.path))||e.path,error:e.error,code:T(new Error(e.error))}))}}if(!e.source||!e.dest)throw new N("--source 与 --dest 必须同时传入",O.INVALID_ARGS);const s=Gt(e.source);if(!t.existsSync(s))throw new N(`本地源路径不存在: ${s}`,O.INVALID_ARGS);const a=t.statSync(s).isDirectory(),c=rn(s);return{sourceRoot:s,uploadRoot:Qt(e.dest),uploadTreeRoot:a?s:n.dirname(s),files:c.map(e=>({absoluteFile:a?n.join(s,e):s,relativeFile:sn(e)})),initialFailures:[]}}function sn(e){return e.split(n.sep).join(n.posix.sep)}function an(e){return[O.AUTH_ERROR,O.PERMISSION_DENIED,O.QUOTA_EXCEEDED,O.NETWORK_ERROR,O.SERVER_ERROR].includes(e.code)}function cn(e){if(e instanceof N)return{message:e.message,code:e.code};const t=e;return{message:e instanceof Error?e.message:String(e||"未知错误"),code:T(t?.primaryUploadError||t?.uploadErrors?.[0]?.error||e)}}function un(){const n=new e("claw-backup").description("将本地文件或目录上传到云盘目录").option("--source <path>","本地源文件或目录").option("--dest <path>","云盘目标目录").option("--source-dir <path>","OpenClaw 本地根目录,只备份白名单目录和文件").option("--claw-name <name>","云盘中的 OpenClaw 备份目录名").action(async e=>{const r=Date.now(),o=n.parent?.opts()||{};try{const n=D(o);if(!n.apiKey)return void M(new N(Jt,O.AUTH_ERROR),"claw-backup",r);const i=new ot(n),s=await on(e,i),a=s.files,c=[],u=[],l=[...s.initialFailures],f=[];let d,h;const p=ke({apiKey:n.apiKey,ecsEnv:n.ecsEnv,subChannel:n.subChannel}),m=new Se;try{for(const e of a){const{absoluteFile:n,relativeFile:r}=e;try{const e=t.statSync(n),o=Math.trunc(e.mtimeMs),i=m.getFileState({cacheScope:p,sourceRoot:s.sourceRoot,destRoot:s.uploadRoot,relativePath:r}),a=i&&i.size!==e.size?void 0:Ee(n);if(i&&a&&i.fhash===a){m.touchUnchangedFileState(i,{absolute_path:n,size:e.size,mtime_ms:o,fhash:a}),u.push(r);continue}f.push({absoluteFile:n,relativeFile:r,size:e.size,mtimeMs:o,fhash:a||Ee(n)})}catch(e){const t=cn(e),n={path:r,error:t.message,code:t.code};if(l.push(n),an(n)){d=n,h=n.error;break}}}if(f.length>0&&!d){const e=await i.uploadTree({rootDir:s.uploadTreeRoot,filePaths:f.map(e=>e.absoluteFile),uploadRoot:s.uploadRoot,is_rename:0}),t=new Map(f.map(e=>[e.absoluteFile,e]));for(const n of e.uploadResults){const e=t.get(n.absolutePath);if(!e)continue;const r=Pe({uploadResults:[n.uploadResult]});m.upsertFileState({cache_scope:p,source_root:s.sourceRoot,dest_root:s.uploadRoot,relative_path:e.relativeFile,absolute_path:e.absoluteFile,size:e.size,mtime_ms:e.mtimeMs,fhash:e.fhash,nid:r.nid,remote_file_hash:r.remote_file_hash||e.fhash,remote_name:r.remote_name,remote_dir:n.remoteDir,uploaded_at:(new Date).toISOString()}),c.push(e.relativeFile)}for(const t of e.uploadErrors){const e=cn(new Error(t.error)),n={path:t.relativePath,error:t.error,code:e.code};if(l.push(n),an(n)){d=n,h=n.error;break}}}}finally{m.close()}const w=a.length+s.initialFailures.length,_=Math.max(w-c.length-l.length-u.length,0),y=u.length+_,g=_>0,$=l.length>0?function(e,t){if(t)return t.code;const n=e.find(e=>e.code!==O.GENERAL)?.code;return n??e[0]?.code??O.GENERAL}(l,d):void 0,v={source:s.sourceRoot,destination:s.uploadRoot,directories_created:0,total_files:w,files_uploaded:c.length,files_failed:l.length,files_skipped:y,files_unchanged:u.length,stopped_early:g,aborted_reason:h,files:c,unchanged_files:u,failed_items:l};if("text"===o.format)x(function(e){const t=[`source: ${e.source}`,`destination: ${e.destination}`,`directories_created: ${e.directories_created}`,`total_files: ${e.total_files}`,`files_uploaded: ${e.files_uploaded}`,`files_failed: ${e.files_failed}`,`files_skipped: ${e.files_skipped}`,`files_unchanged: ${e.files_unchanged}`];if(e.stopped_early&&e.aborted_reason&&t.push(`aborted_reason: ${e.aborted_reason}`),e.failed_items.length>0){t.push("failed_items:");for(const n of e.failed_items)t.push(`- ${n.path}: ${n.error}`)}return t.join("\n")}(v)),void 0!==$&&(process.exitCode=$);else if(l.length>0){F(v,g&&h?`备份提前终止: ${h}`:`部分文件备份失败: ${l.length}/${w}`,$??O.GENERAL,"claw-backup",r,{quiet:o.quiet})}else L(v,"claw-backup",r,{quiet:o.quiet})}catch(e){M(e,"claw-backup",r)}});return n}function ln(){const r=new e("claw-restore").description("将云盘目录递归恢复到本地目录").requiredOption("--remote <path>","云盘源目录").requiredOption("--target <path>","本地目标目录").action(async e=>{const o=Date.now(),i=r.parent?.opts()||{};try{const r=D(i);if(!r.apiKey)return void M(new N(Jt,O.AUTH_ERROR),"claw-restore",o);const s=Qt(e.remote),a=Gt(e.target);tn(a);const c=new It(r),u=new ot(r),l=await async function(e,t){const n=[Qt(t)],r=[];for(;n.length>0;){const t=n.shift();let o=0;const i=1e3;let s=0,a=0;do{const c=await e.list({path:t,page:o,page_size:i});if(0!==c?.errno)throw new A(c?.errmsg||`列目录失败: ${t}`,{traceId:C(c),apiErrno:"number"==typeof c?.errno?c.errno:void 0});const u=Zt(c);a=en(c,u.length),s+=u.length;for(const e of u){const o=Vt(e);if(!o)continue;const i=Ht(e),s={name:o,path:Xt(t,e,i),nid:e?.nid?String(e.nid):void 0,isDir:i};r.push(s),i&&n.push(s.path)}o+=1}while(s<a)}return r}(c,s),f=[];let d=0;for(const e of l){const r=e.path.slice(s.length).replace(/^\/+/,"");if(!r)continue;const o=n.join(a,r.split("/").join(n.sep));if(e.isDir){tn(o),d+=1;continue}if(!e.nid)throw new Error(`缺少文件 nid,无法下载: ${e.path}`);tn(n.dirname(o));const i=await u.download({nid:e.nid,downloadDir:n.dirname(o),auto:!0,forceForeground:!0});i.downloadPath&&i.downloadPath!==o&&t.renameSync(i.downloadPath,o),f.push(r)}const h={remote:s,target:a,directories_restored:d,files_restored:f.length,files:f};"text"===i.format?x(Object.entries({remote:s,target:a,directories_restored:d,files_restored:f.length}).map(([e,t])=>`${e}: ${t}`).join("\n")):L(h,"claw-restore",o,{quiet:i.quiet})}catch(e){M(e,"claw-restore",o)}});return r}(async function(){const o=function(){try{const e=n.dirname(r(import.meta.url)),o=[n.resolve(e,"../package.json"),n.resolve(e,"../../package.json"),n.resolve(e,"../../../package.json"),n.resolve(process.cwd(),"package.json")];for(const e of o)if(t.existsSync(e)){const n=JSON.parse(t.readFileSync(e,"utf8"));return{name:n.cli?.name||"360disk",version:n.version||"0.0.0",description:n.cli?.description||"360 AI 云盘 CLI"}}}catch{}return{name:"360disk",version:"0.0.0",description:"360 AI 云盘 CLI"}}(),i=new e;i.name(o.name).description(o.description).version(o.version).enablePositionalOptions().passThroughOptions().option("--api-key <key>","API 密钥").option("--env <env>","环境 (prod/test)").option("--sub-channel <channel>","子渠道").option("--format <type>","输出格式 (json|text)","json").option("--quiet","仅输出结果数据").option("--timeout <ms>","请求超时时间(毫秒)","30000").option("--retries <n>","失败重试次数","0"),i.addCommand(Pt()),i.addCommand(function(){const t=new e("user").description("用户信息");return t.command("info").description("获取用户详细信息").action(async()=>{const e=Date.now(),n=t.parent?.opts()||{};try{const t=D(n);if(!t.apiKey)return void M(new N("未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量",O.AUTH_ERROR),"user info",e);const r=new St(t),o=await r.info();q(o,"user info",e),"text"===n.format?x(H(o)):L(o,"user info",e,{quiet:n.quiet})}catch(t){M(t,"user info",e)}}),t}()),i.addCommand(function(){const t=new e("dir").description("目录操作");return t.command("ls").description("列出云盘指定目录下的文件和文件夹").argument("[path]","目录路径","/").option("--page <n>","页码","0").option("--size <n>","每页数量","50").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=D(o);if(!t.apiKey)return void M(new N(Dt,O.AUTH_ERROR),"dir ls",r);const i=new It(t),s=await i.list({path:e,page:parseInt(n.page),page_size:parseInt(n.size)});q(s,"dir ls",r),"text"===o.format?x(J(s)):L(s,"dir ls",r,{quiet:o.quiet})}catch(e){M(e,"dir ls",r)}}),t.command("mkdir").description("创建新文件夹").argument("<path>","文件夹路径").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=D(r);if(!t.apiKey)return void M(new N(Dt,O.AUTH_ERROR),"dir mkdir",n);const o=new It(t),i=await o.mkdir(e);q(i,"dir mkdir",n),"text"===r.format?x(V(i,`已创建目录: ${e}`)):L(i,"dir mkdir",n,{quiet:r.quiet})}catch(e){M(e,"dir mkdir",n)}}),t}()),i.addCommand(Mt()),i.addCommand(un()),i.addCommand(ln()),i.addCommand(kt()),i.addCommand(Yt()),await i.parseAsync()})().then(()=>{(function(){const e=process.argv.findIndex((e,t)=>t>=2&&"claw-auto-backup"===e);return!(e>=0&&"__watch"===process.argv[e+1])})()&&process.exit(process.exitCode??0)}).catch(e=>{process.stderr.write(JSON.stringify({success:!1,error:e.message||"CLI 启动失败"},null,2)+"\n"),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicloud360/360-aidrive",
3
- "version": "0.8.29",
3
+ "version": "0.8.30",
4
4
  "main": "build/index.js",
5
5
  "type": "module",
6
6
  "bin": {