shennian 0.2.107 → 0.2.108
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.
|
@@ -9,6 +9,9 @@ export type WeChatChannelHelperClientOptions = {
|
|
|
9
9
|
skipUserActivityGuard?: boolean;
|
|
10
10
|
cleanupWindowsOverlays?: boolean;
|
|
11
11
|
requestLogger?: (event: WeChatChannelHelperRequestTraceEvent) => void;
|
|
12
|
+
openHelperAppForTest?: (command: string, args: string[]) => Promise<void>;
|
|
13
|
+
macosRuntimeInitialReadyTimeoutMs?: number;
|
|
14
|
+
macosRuntimeRelaunchReadyTimeoutMs?: number;
|
|
12
15
|
};
|
|
13
16
|
export type WeChatChannelHelperRequestTraceEvent = {
|
|
14
17
|
phase: 'request' | 'response' | 'error';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{spawn as w}from"node:child_process";import{randomUUID as
|
|
2
|
-
`)}catch(i){throw await this.stop().catch(()=>{}),i}return s}async startDarwinAppSocketFresh(e){const t=await this.connectDarwinHelperSocket(e);this.socket=t,this.stderrTail="",this.lines=m({input:t}),this.lines.on("line",i=>this.handleLine(i)),t.on("error",i=>{this.socket===t&&(this.socket=null,this.readyState=null,this.lines?.close(),this.lines=null),this.rejectAllPending(i instanceof Error?i:new Error(String(i)))}),t.once("close",()=>{this.socket===t&&(this.socket=null,this.readyState=null,this.lines?.close(),this.lines=null),this.rejectAllPending(new Error("WeChat channel helper socket closed"))});const s=new Promise((i,
|
|
3
|
-
`;if(this.socket){this.socket.write(t);return}if(this.child){this.child.stdin.write(t);return}throw new Error("WeChat channel helper is not started")}handleLine(e){let t;try{t=JSON.parse(e)}catch{return}if(!
|
|
1
|
+
import{spawn as w}from"node:child_process";import{randomUUID as _}from"node:crypto";import f from"node:fs";import W from"node:net";import b from"node:os";import d from"node:path";import{createInterface as m}from"node:readline";import{decideWeChatChannelActivityGate as T,normalizeWeChatChannelActivitySnapshot as v}from"./human-coordination.js";import{createWeChatChannelHelperHello as y,extractWeChatChannelHelperWarmupSnapshot as P,timeoutForWeChatChannelHelperCommand as R,validateWeChatChannelHelperReady as g}from"./helper-protocol.js";const M=["en","v"].join("");class se{options;child=null;socket=null;lines=null;readyState=null;warmupState=null;stderrTail="";startPromise=null;requestQueueTail=Promise.resolve();pending=new Map;constructor(e){this.options=e}async start(){return this.hasTransport()&&this.readyState?this.readyState:this.startPromise?this.startPromise:(this.hasTransport()&&await this.stop(),this.startPromise=this.startFresh().catch(async e=>{throw await this.stop().catch(()=>{}),e}).finally(()=>{this.startPromise=null}),this.startPromise)}async startFresh(){const e=F(this.options.helperPath,this.options.args??[]);return e.transport==="macos-socket"?this.startDarwinAppSocketFresh(e):this.startStdioFresh(e)}async startStdioFresh(e){const t=w(e.command,e.args,{cwd:this.options.cwd,stdio:["pipe","pipe","pipe"],windowsHide:!0});this.child=t,this.stderrTail="",this.lines=m({input:t.stdout}),this.lines.on("line",i=>this.handleLine(i)),t.stderr.on("data",i=>{this.captureStderr(i)}),t.stdin.on("error",i=>{this.rejectAllPending(i instanceof Error?i:new Error(String(i)))}),t.once("error",i=>{this.child===t&&(this.child=null,this.readyState=null,this.lines?.close(),this.lines=null),this.rejectAllPending(i instanceof Error?i:new Error(String(i)))}),t.once("exit",(i,n)=>{this.child===t&&(this.child=null,this.readyState=null,this.lines?.close(),this.lines=null),this.rejectAllPending(new Error(this.formatHelperExitMessage(i,n)))});const s=new Promise((i,n)=>{const a=setTimeout(()=>n(new Error("WeChat channel helper handshake timed out")),1e4),l=u=>{clearTimeout(a),this.lines?.off("line",c),t.off("exit",o),n(new Error(`WeChat channel helper failed to start: ${u.message}`))},o=(u,h)=>{clearTimeout(a),t.off("error",l),this.lines?.off("line",c),n(new Error(this.formatHelperExitMessage(u,h)))},c=u=>{let h;try{h=JSON.parse(u)}catch{clearTimeout(a),t.off("error",l),this.lines?.off("line",c),n(new Error("WeChat channel helper sent invalid handshake JSON"));return}if(!C(h))return;clearTimeout(a),t.off("error",l),t.off("exit",o),this.lines?.off("line",c);const p=g(h,this.options.expectedHelperVersion,this.options.requiredCapabilities);p.ok?(this.readyState=h,this.captureWarmupSnapshot(h),i(h)):n(new Error(`${p.errorCode}: ${p.errorSummary}`))};t.once("error",l),t.once("exit",o),this.lines?.on("line",c)});try{t.stdin.write(`${JSON.stringify(y(this.options.expectedHelperVersion,this.options.requiredCapabilities))}
|
|
2
|
+
`)}catch(i){throw await this.stop().catch(()=>{}),i}return s}async startDarwinAppSocketFresh(e){const t=await this.connectDarwinHelperSocket(e);this.socket=t,this.stderrTail="",this.lines=m({input:t}),this.lines.on("line",i=>this.handleLine(i)),t.on("error",i=>{this.socket===t&&(this.socket=null,this.readyState=null,this.lines?.close(),this.lines=null),this.rejectAllPending(i instanceof Error?i:new Error(String(i)))}),t.once("close",()=>{this.socket===t&&(this.socket=null,this.readyState=null,this.lines?.close(),this.lines=null),this.rejectAllPending(new Error("WeChat channel helper socket closed"))});const s=new Promise((i,n)=>{const a=setTimeout(()=>n(new Error("WeChat channel helper socket handshake timed out")),1e4),l=u=>{clearTimeout(a),this.lines?.off("line",c),t.off("close",o),n(new Error(`WeChat channel helper socket failed: ${u.message}`))},o=()=>{clearTimeout(a),t.off("error",l),this.lines?.off("line",c),n(new Error("WeChat channel helper socket closed before handshake"))},c=u=>{let h;try{h=JSON.parse(u)}catch{clearTimeout(a),t.off("error",l),t.off("close",o),this.lines?.off("line",c),n(new Error("WeChat channel helper sent invalid socket handshake JSON"));return}if(!C(h))return;clearTimeout(a),t.off("error",l),t.off("close",o),this.lines?.off("line",c);const p=g(h,this.options.expectedHelperVersion,this.options.requiredCapabilities);p.ok?(this.readyState=h,this.captureWarmupSnapshot(h),i(h)):n(new Error(`${p.errorCode}: ${p.errorSummary}`))};t.once("error",l),t.once("close",o),this.lines?.on("line",c)});try{this.writeFrame(y(this.options.expectedHelperVersion,this.options.requiredCapabilities))}catch(i){throw await this.stop().catch(()=>{}),i}return s}async request(e,t,s){const i=this.requestQueueTail.catch(()=>{}).then(()=>this.sendRequest(e,t,s));return this.requestQueueTail=i.then(()=>{},()=>{}),i}async sendRequest(e,t,s){await this.guardUnsafeWindowsCommand(e,s);try{return await this.sendRawRequest(e,t,s)}finally{await this.cleanupWindowsOverlaysAfterCommand(e,s).catch(()=>{})}}async sendRawRequest(e,t,s){if((!this.hasTransport()||!this.readyState)&&await this.start(),!this.hasTransport())throw new Error("WeChat channel helper is not started");const i=_(),n=R(e),a=Date.now();this.emitRequestTrace({phase:"request",at:new Date(a).toISOString(),id:i,command:e,traceId:s,params:t,timeoutMs:n});const l=new Promise((o,c)=>{const u=setTimeout(()=>{this.pending.delete(i),c(new Error(`helper_command_timeout: ${e}`))},n);this.pending.set(i,{resolve:o,reject:c,timer:u})});try{this.writeFrame({id:i,command:e,params:t,traceId:s})}catch(o){const c=this.pending.get(i);throw c&&clearTimeout(c.timer),this.pending.delete(i),this.emitRequestTrace({phase:"error",at:new Date().toISOString(),id:i,command:e,traceId:s,durationMs:Date.now()-a,errorSummary:o instanceof Error?o.message:String(o)}),o}try{const o=await l;return this.emitRequestTrace({phase:"response",at:new Date().toISOString(),id:i,command:e,traceId:s,durationMs:Date.now()-a,ok:o.ok,errorCode:o.errorCode,errorSummary:o.errorSummary,latencyMs:o.latencyMs,result:o.result}),this.captureWarmupSnapshot(o),this.captureWarmupSnapshot(o.result),o}catch(o){throw this.emitRequestTrace({phase:"error",at:new Date().toISOString(),id:i,command:e,traceId:s,durationMs:Date.now()-a,errorSummary:o instanceof Error?o.message:String(o)}),o}}async guardUnsafeWindowsCommand(e,t){if(!this.options.guardUnsafeWindowsCommands||!j(e))return;const s=await this.sendRawRequest("permissions.check",{},t);if(!s.ok)throw new Error(`${s.errorCode??"permissions_check_failed"}: ${s.errorSummary??e}`);const i=s.result??{},n=await this.requestFirstMissingMacPermissionPrompt(i,t);if(n==="screen-recording")throw new Error("permission_screen_recording_missing");if(n==="accessibility")throw new Error("permission_accessibility_missing");if(n==="input-monitoring")throw new Error("permission_input_monitoring_missing");if(i.automation===!1)throw new Error("permission_automation_missing");if(i.wechatRunning===!1)throw new Error("wechat_not_running");if(A(i))throw new Error("wechat_login_required");if($(i))throw new Error("windows_visible_desktop_unavailable");if(i.dpiMappingAvailable===!1||i.displayTopologySupported===!1)throw new Error("dpi_mapping_failed");if(i.wechatWindowAvailable===!1)throw new Error("wechat_window_unavailable");if(i.wechatMainWindowResponsive===!1)throw new Error("wechat_window_unresponsive");if(!this.options.skipUserActivityGuard&&B(e)){const a=await this.sendRawRequest("activity.snapshot",{},t);if(!a.ok)throw new Error(`${a.errorCode??"user_activity_unknown"}: ${a.errorSummary??e}`);const l=T({snapshot:v(a.result),stage:"dangerous_action"});if(!l.ok)throw new Error(`user_active:${l.reasonCode}`)}await this.cleanupWindowsOverlays("before",e,t)}async requestFirstMissingMacPermissionPrompt(e,t){const s=V(e);if(!s||process.platform!=="darwin")return s;const i=G(s);try{await this.sendRawRequest(i,{},t)}catch{}return s}async cleanupWindowsOverlaysAfterCommand(e,t){!this.options.guardUnsafeWindowsCommands||!z(e)||await this.cleanupWindowsOverlays("after",e,t)}async cleanupWindowsOverlays(e,t,s){if(this.options.cleanupWindowsOverlays===!1)return;if(!this.readyState?.capabilities.includes("overlayCleanup"))throw new Error("helper_capability_missing: overlayCleanup");const i=await this.sendRawRequest("windows.cleanupOverlays",{stage:e,command:t,includeGeneratedMediaPreviews:!0,includeToolingTerminals:!1,waitMs:e==="after"?220:120},s);if(!i.ok)throw new Error(`${i.errorCode??"overlay_cleanup_failed"}: ${i.errorSummary??t}`)}async healthCheck(e){return this.request("health.check",{},e)}getReadyState(){return this.readyState?{...this.readyState,capabilities:[...this.readyState.capabilities]}:null}getWarmupState(){return this.warmupState?{warmState:this.warmupState.warmState,metrics:this.warmupState.metrics?{...this.warmupState.metrics}:void 0}:null}async stop(){const e=this.child,t=this.socket;this.child=null,this.socket=null,this.readyState=null,this.lines?.close(),this.lines=null,this.rejectAllPending(new Error("WeChat channel helper stopped")),t&&!t.destroyed&&t.destroy(),!(!e||e.killed)&&e.kill("SIGTERM")}hasTransport(){return!!this.child||!!this.socket}writeFrame(e){const t=`${JSON.stringify(e)}
|
|
3
|
+
`;if(this.socket){this.socket.write(t);return}if(this.child){this.child.stdin.write(t);return}throw new Error("WeChat channel helper is not started")}handleLine(e){let t;try{t=JSON.parse(e)}catch{return}if(!J(t))return;const s=this.pending.get(t.id);s&&(clearTimeout(s.timer),this.pending.delete(t.id),s.resolve(t))}captureWarmupSnapshot(e){const t=P(e);t&&(this.warmupState=t)}captureStderr(e){const t=Buffer.isBuffer(e)?e.toString("utf8"):String(e??"");t&&(this.stderrTail=(this.stderrTail+t).slice(-2e3))}formatHelperExitMessage(e,t){const s=[e===null?null:`code=${e}`,t?`signal=${t}`:null,this.stderrTail.trim()?`stderr=${this.stderrTail.trim()}`:null].filter(Boolean);return s.length?`WeChat channel helper process exited (${s.join(", ")})`:"WeChat channel helper process exited"}rejectAllPending(e){for(const[t,s]of this.pending)clearTimeout(s.timer),s.reject(e),this.pending.delete(t)}emitRequestTrace(e){try{this.options.requestLogger?.(e)}catch{}}async connectDarwinHelperSocket(e){try{return await S(e.runtimeFile)}catch{f.rmSync(e.runtimeFile,{force:!0})}await E(e,{},this.options.openHelperAppForTest);const t=await k(e.runtimeFile,this.options.macosRuntimeInitialReadyTimeoutMs??5e3);if(t.ok)return t.socket;f.rmSync(e.runtimeFile,{force:!0}),await E(e,{forceNewInstance:!0},this.options.openHelperAppForTest);const s=await k(e.runtimeFile,this.options.macosRuntimeRelaunchReadyTimeoutMs??1e4);if(s.ok)return s.socket;throw new Error(`WeChat channel Helper.app socket did not become ready: ${s.error.message}`)}}function A(r){return r.wechatWindowAvailable!==!1?!1:![...r.captureCandidates??[],...r.restoreCandidates??[],...r.hiddenRestoreCandidates??[]].some(H)}function $(r){return r.windowsVisibleDesktopAvailable===!1||r.rdpVisibleDesktopAvailable===!1||r.desktopSessionVisible===!1||r.screenLocked===!0||r.rdpDisconnected===!0}function q(r){const e=String(r.appName||"").toLowerCase();return e==="wechat"||e==="weixin"}function H(r){return!(!q(r)||r.visible===!1||r.minimized===!0)}function F(r,e,t=process.platform){if(t==="darwin"){const s=x(r);if(s&&e.length===0){const i=D();return{transport:"macos-socket",command:"/usr/bin/open",args:["-g",s,"--args","--socket-runtime",i],appPath:s,runtimeDir:i,runtimeFile:d.join(i,"runtime.json")}}}return{transport:"legacy-stdio",command:r,args:e}}function x(r){const e=d.resolve(r),t=`${d.sep}Contents${d.sep}MacOS${d.sep}`,s=e.lastIndexOf(t);if(s<0)return null;const i=e.slice(0,s);return i.endsWith(".app")?i:null}function D(){const r=O().SHENNIAN_HELPER_RUNTIME_DIR?.trim();return r?d.resolve(r):d.join(b.homedir(),"Library","Application Support","Shennian","Helper")}function O(){return globalThis.process?.[M]??{}}function N(r){let e;try{e=JSON.parse(f.readFileSync(r,"utf8"))}catch(i){throw new Error(`runtime_file_unreadable: ${i instanceof Error?i.message:String(i)}`)}if(!e||typeof e!="object")throw new Error("runtime_file_invalid");const t=e.socketPath;if(typeof t!="string"||!t.trim())throw new Error("runtime_socket_missing");const s=e.pid;return{socketPath:t,...typeof s=="number"&&Number.isInteger(s)&&s>0?{pid:s}:{}}}async function S(r){const{socketPath:e,pid:t}=N(r);if(t&&!L(t))throw new Error(`runtime_process_stale: ${t}`);return new Promise((s,i)=>{const n=W.createConnection(e),a=setTimeout(()=>{n.destroy(),i(new Error(`runtime_socket_connect_timeout: ${e}`))},1e3);n.once("connect",()=>{clearTimeout(a),n.off("error",l),s(n)});const l=o=>{clearTimeout(a),n.destroy(),i(o)};n.once("error",l)})}async function k(r,e){const t=Date.now()+e;let s=null;for(;Date.now()<t;)try{return{ok:!0,socket:await S(r)}}catch(i){s=i instanceof Error?i:new Error(String(i));const n=t-Date.now();n>0&&await I(Math.min(150,n))}return{ok:!1,error:s??new Error("runtime file missing")}}async function E(r,e={},t=U){f.mkdirSync(r.runtimeDir,{recursive:!0,mode:448});const s=e.forceNewInstance?["-n",...r.args]:r.args;try{await t(r.command,s)}catch(i){if(!(i instanceof Error?i.message:String(i)).includes("-1712"))throw i;await t(r.command,["-n",...r.args])}}function L(r){try{return process.kill(r,0),!0}catch{return!1}}async function U(r,e){await new Promise((t,s)=>{const i=w(r,e,{stdio:["ignore","ignore","pipe"],windowsHide:!0});let n="";i.stderr.on("data",a=>{n=(n+String(a)).slice(-2e3)}),i.once("error",s),i.once("exit",(a,l)=>{a===0?t():s(new Error(`open Helper.app failed (${l?`signal=${l}`:`code=${a}`}${n.trim()?`, stderr=${n.trim()}`:""})`))})})}function I(r){return new Promise(e=>setTimeout(e,r))}function V(r){return r.screenRecording===!1?"screen-recording":r.accessibility===!1?"accessibility":r.inputMonitoring===!1?"input-monitoring":null}function G(r){return r==="screen-recording"?"permissions.requestScreenRecording":r==="accessibility"?"permissions.requestAccessibility":"permissions.requestInputMonitoring"}function C(r){if(!r||typeof r!="object")return!1;const e=r;return e.type==="ready"&&typeof e.helperVersion=="string"&&typeof e.protocolVersion=="number"&&Array.isArray(e.capabilities)&&typeof e.pid=="number"}function J(r){if(!r||typeof r!="object")return!1;const e=r;return typeof e.id=="string"&&typeof e.ok=="boolean"&&typeof e.latencyMs=="number"}function j(r){return r==="windows.focus"||r==="windows.capture"||r==="screen.capture"||r==="ocr.recognize"||r==="image.cropHash"||r==="wechat.searchConversation"||r==="wechat.focusMessageInput"||r.startsWith("mouse.")||r.startsWith("keyboard.")||r.startsWith("clipboard.")||r==="menu.pickItem"||r==="savePanel.saveToPath"}function z(r){return r.startsWith("mouse.")||r.startsWith("keyboard.")||r.startsWith("clipboard.")||r==="menu.pickItem"||r==="savePanel.saveToPath"}function B(r){return r==="windows.focus"||r==="wechat.searchConversation"||r==="wechat.focusMessageInput"||r.startsWith("mouse.")||r.startsWith("keyboard.")||r.startsWith("clipboard.set")||r==="clipboard.restore"||r==="menu.pickItem"||r==="savePanel.saveToPath"}export{se as WeChatChannelHelperClient,F as helperLaunchSpec};
|