publishport-opencli 1.0.47 → 1.0.48

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.
@@ -6,6 +6,8 @@ export interface DaemonStatus {
6
6
  extensionConnected: boolean;
7
7
  extensionVersion?: string;
8
8
  extensionCompatRange?: string;
9
+ /** Shell primitives advertised by the extension (1.1.5+: chrome-api, cdp-raw, events, bind-tab). */
10
+ extensionCapabilities?: string[];
9
11
  contextId?: string;
10
12
  profileRequired?: boolean;
11
13
  profileDisconnected?: boolean;
@@ -1 +1 @@
1
- import{fetchDaemonStatus as a,requestDaemonShutdown as u}from"../browser/daemon-transport.js";import{restartDaemon as f}from"../browser/daemon-lifecycle.js";import{formatDuration as m}from"../download/progress.js";import{log as o}from"../logger.js";import{PKG_VERSION as c}from"../version.js";import{formatDaemonVersion as l,isDaemonStale as d}from"../browser/daemon-version.js";import{browserSession as w,getBrowserFactory as x}from"../runtime.js";export async function daemonStatus(s={}){const e=await a();if(s.json){console.log(JSON.stringify(e?{daemon:"running",pid:e.pid,daemonVersion:l(e),stale:d(e,c),uptimeSeconds:Math.round(e.uptime),bridgeReady:!!e.extensionConnected,extensionConnected:!!e.extensionConnected,extensionVersion:e.extensionVersion??null,profileRequired:!!e.profileRequired,profileDisconnected:!!e.profileDisconnected,profiles:(e.profiles??[]).map((t)=>({contextId:t.contextId,extensionVersion:t.extensionVersion??null})),port:e.port}:{daemon:"not_running",bridgeReady:!1},null,2));return}if(!e){console.log("Daemon: not running");return}let r;if(e.extensionConnected)r=e.extensionVersion?`connected (v${e.extensionVersion})`:"connected (version unknown)";else if(e.profileRequired){const t=e.profiles?.length??0;r=`${t} ${t===1?"profile":"profiles"} connected, none selected — run \`ppcli profile use <name>\``}else if(e.profileDisconnected)r="requested profile not connected — run `ppcli profile use <name>`";else r="disconnected";const i=l(e),n=d(e,c);console.log(`Daemon: ${n?"stale":"running"} (PID ${e.pid})`);console.log(`Version: ${i}${n?` (CLI v${c}; run: ppcli daemon restart)`:""}`);console.log(`Uptime: ${m(Math.round(e.uptime*1000))}`);console.log(`Extension: ${r}`);if(e.profiles&&e.profiles.length>0)console.log(`Profiles: ${e.profiles.map((t)=>{const p=t.extensionVersion?` v${t.extensionVersion}`:"";return`${t.contextId}${p}`}).join(", ")}`);console.log(`Memory: ${e.memoryMB} MB`);console.log(`Port: ${e.port}`)}export async function daemonStop(){if(!await a()){o.info("Daemon is not running.");return}if(await u())o.success("Daemon stopped.");else{o.error("Failed to stop daemon.");process.exitCode=1}}export async function daemonRestart(){const s=await a();if(s?.profiles&&s.profiles.length>0)o.warn(`Restarting daemon will disconnect ${s.profiles.length} browser profile(s); the extension should reconnect automatically.`);const e=await f();if(!e.stopped){o.error("Failed to stop daemon before restart.");process.exitCode=1;return}if(!e.status){o.error("Daemon restart timed out before the new daemon reported status.");process.exitCode=1;return}const r=e.previousStatus?"restarted":"started",i=l(e.status);o.success(`Daemon ${r} on port ${e.status.port} (${i}).`);if(e.status.extensionConnected){const n=e.status.profiles?.length??0,t=n>0?`; profiles connected: ${n}`:"";o.status(`Extension connected${t}.`)}else o.warn("Daemon is running, but the Browser Bridge extension has not connected yet.")}export async function daemonWarm(s){const e=await a();if(!e){o.warn("Daemon/extension not ready; skip warm (retry after the browser extension connects).");return}if(!e.extensionConnected){o.warn("Browser extension not connected yet; skip warm.");return}const r=s&&/^https?:\/\//i.test(s)?s:"about:blank",i=x("__warm__");try{await w(i,async(n)=>{await n.goto(r).catch(()=>{})},{session:"site:__warm__",windowMode:"background",surface:"adapter",siteSession:"persistent"});o.success(`Automation window warmed (${r}, kept alive for reuse).`)}catch(n){o.warn(`Warm failed (non-fatal): ${n instanceof Error?n.message:String(n)}`)}}
1
+ import{fetchDaemonStatus as a,requestDaemonShutdown as u}from"../browser/daemon-transport.js";import{restartDaemon as m}from"../browser/daemon-lifecycle.js";import{formatDuration as f}from"../download/progress.js";import{log as o}from"../logger.js";import{PKG_VERSION as c}from"../version.js";import{formatDaemonVersion as l,isDaemonStale as d}from"../browser/daemon-version.js";import{browserSession as w,getBrowserFactory as x}from"../runtime.js";export async function daemonStatus(s={}){const e=await a();if(s.json){console.log(JSON.stringify(e?{daemon:"running",pid:e.pid,daemonVersion:l(e),stale:d(e,c),uptimeSeconds:Math.round(e.uptime),bridgeReady:!!e.extensionConnected,extensionConnected:!!e.extensionConnected,extensionVersion:e.extensionVersion??null,profileRequired:!!e.profileRequired,profileDisconnected:!!e.profileDisconnected,profiles:(e.profiles??[]).map((t)=>({contextId:t.contextId,extensionVersion:t.extensionVersion??null})),port:e.port}:{daemon:"not_running",bridgeReady:!1},null,2));return}if(!e){console.log("Daemon: not running");return}let r;if(e.extensionConnected)r=e.extensionVersion?`connected (v${e.extensionVersion})`:"connected (version unknown)";else if(e.profileRequired){const t=e.profiles?.length??0;r=`${t} ${t===1?"profile":"profiles"} connected, none selected — run \`ppcli profile use <name>\``}else if(e.profileDisconnected)r="requested profile not connected — run `ppcli profile use <name>`";else r="disconnected";const i=l(e),n=d(e,c);console.log(`Daemon: ${n?"stale":"running"} (PID ${e.pid})`);console.log(`Version: ${i}${n?` (CLI v${c}; run: ppcli daemon restart)`:""}`);console.log(`Uptime: ${f(Math.round(e.uptime*1000))}`);console.log(`Extension: ${r}`);if(e.profiles&&e.profiles.length>0)console.log(`Profiles: ${e.profiles.map((t)=>{const p=t.extensionVersion?` v${t.extensionVersion}`:"";return`${t.contextId}${p}`}).join(", ")}`);console.log(`Memory: ${e.memoryMB} MB`);console.log(`Port: ${e.port}`)}export async function daemonStop(){if(!await a()){o.info("Daemon is not running.");return}if(await u())o.success("Daemon stopped.");else{o.error("Failed to stop daemon.");process.exitCode=1}}export async function daemonRestart(){const s=await a();if(s?.profiles&&s.profiles.length>0)o.warn(`Restarting daemon will disconnect ${s.profiles.length} browser profile(s); the extension should reconnect automatically.`);const e=await m();if(!e.stopped){o.error("Failed to stop daemon before restart.");process.exitCode=1;return}if(!e.status){o.error("Daemon restart timed out before the new daemon reported status.");process.exitCode=1;return}const r=e.previousStatus?"restarted":"started",i=l(e.status);o.success(`Daemon ${r} on port ${e.status.port} (${i}).`);if(e.status.extensionConnected){const n=e.status.profiles?.length??0,t=n>0?`; profiles connected: ${n}`:"";o.status(`Extension connected${t}.`)}else o.warn("Daemon is running, but the Browser Bridge extension has not connected yet.")}export async function daemonWarm(s){const e=await a();if(!e){o.warn("Daemon/extension not ready; skip warm (retry after the browser extension connects).");return}if(!e.extensionConnected){o.warn("Browser extension not connected yet; skip warm.");return}if(e.extensionCapabilities?.includes("chrome-api")){o.status("Extension hosts background tabs in existing windows; nothing to warm.");return}const r=s&&/^https?:\/\//i.test(s)?s:"about:blank",i=x("__warm__");try{await w(i,async(n)=>{await n.goto(r).catch(()=>{})},{session:"site:__warm__",windowMode:"background",surface:"adapter",siteSession:"persistent"});o.success(`Automation window warmed (${r}, kept alive for reuse).`)}catch(n){o.warn(`Warm failed (non-fatal): ${n instanceof Error?n.message:String(n)}`)}}
@@ -1 +1 @@
1
- import{createServer as B}from"node:http";import{WebSocketServer as Y,WebSocket as V}from"ws";import{DEFAULT_DAEMON_PORT as Z,PUBLISHPORT_DAEMON_PORT as Q,unsupportedDaemonPortEnvMessage as q}from"./constants.js";import{EXIT_CODES as v}from"./errors.js";import{log as a}from"./logger.js";import{PKG_VERSION as ee}from"./version.js";import{DEFAULT_CONTEXT_ID as ne}from"./browser/profile.js";import{buildCommandDispatchFailure as G,buildCommandTimeoutFailure as te,buildExtensionDisconnectFailure as oe,getResponseCorsHeaders as re,resolveProfileRoute as ie}from"./daemon-utils.js";import*as C from"./browser/trace.js";import{DebuggerBanner as se,supportsDebuggerBanner as ae}from"./browser/debugger-banner.js";import{SessionLeaseRegistry as ce,buildSessionBusyFailure as de,getSessionLeaseKey as le,isSessionLeaseCommand as ue}from"./session-lease.js";const S=Z,D=Q;if(process.env.OPENCLI_DAEMON_PORT){a.error(q(process.env.OPENCLI_DAEMON_PORT));process.exit(v.USAGE_ERROR)}const x=new Map,u=new Map,_=new ce;function F(n){for(const t of u.values())if(t.runId===n)return!0;return!1}function w(n,t,s){clearTimeout(t.timer);u.delete(n);if(t.leaseKey&&t.runId)_.heartbeat(t.leaseKey,t.runId,Date.now());for(const r of t.settlers)if(s.error)r.reject(s.error);else r.resolve(s.data)}let fe=0;function me(n,t,s=15000){const r=`daemon-${process.pid}-${++fe}`;return new Promise((o,e)=>{const i=setTimeout(()=>{const d=u.get(r);if(d)w(r,d,{error:Error(`daemon ${String(t.action)} timed out after ${s}ms`)})},s),c={contextId:n.contextId,action:String(t.action),dispatched:!1,dispatchTs:Date.now(),settlers:[{resolve:(d)=>o(d),reject:e}],timer:i};u.set(r,c);try{n.ws.send(JSON.stringify({...t,id:r}),(d)=>{if(d&&u.get(r)===c)w(r,c,{error:d})});c.dispatched=!0}catch(d){w(r,c,{error:d instanceof Error?d:Error(String(d))})}})}let A=0;const pe=200,P=[];class b extends Error{errorCode;errorHint;status;constructor(n,t,s,r=400){super(n);this.errorCode=t;this.errorHint=s;this.status=r;this.name="DaemonCommandFailure"}}function ge(n){P.push(n);if(P.length>pe)P.shift()}function W(){return[...x.values()].filter((n)=>n.ws.readyState===V.OPEN)}function J(n,t){const s=W(),r=ie(s.map((e)=>e.contextId),n,t);if(r.kind==="error"){if(r.errorCode==="profile_disconnected"&&t==="write"&&s.length===1)a.warn(`[daemon] Refusing to route a WRITE to fallback profile "${s[0].contextId}" (requested "${n}")`);return{errorCode:r.errorCode,error:r.error,errorHint:r.errorHint}}if(r.kind==="fallback")a.warn(`[daemon] Requested profile "${r.requestedContextId}" not connected; falling back to the only connected profile "${r.contextId}"`);const o=s.find((e)=>e.contextId===r.contextId);if(!o)return{errorCode:"extension_not_connected",error:"浏览器桥未连接:这台机器上的 PublishPort 托管浏览器没在运行。",errorHint:"请用户打开 PublishPort 客户端,在平台列表里点一次「检测登录」把托管浏览器拉起来;仍不行就重启客户端。"};return{connection:o}}function he(n,t){const s=typeof t==="string"&&t.trim()?t.trim():ne,r=x.get(s);if(r&&r.ws!==n)r.ws.close();const o=[...x.entries()].find(([,c])=>c.ws===n);if(o&&o[0]!==s)x.delete(o[0]);const e=x.get(s),i={contextId:s,ws:n,extensionVersion:e?.ws===n?e.extensionVersion:null,extensionCompatRange:e?.ws===n?e.extensionCompatRange:null,lastSeenAt:Date.now(),capabilities:e?.ws===n?e.capabilities:[],banner:e?.ws===n?e.banner:null};x.set(s,i);return i}function j(n){for(const[t,s]of x.entries()){if(s.ws!==n)continue;s.banner?.dispose();x.delete(t);C.traceExtDisconnect(t);for(const[r,o]of u){if(o.contextId!==t)continue;const e=oe({contextId:t,action:o.action,dispatched:o.dispatched});if(e.countAsCommandResultUnknown){A++;a.warn(`[daemon] Command result unknown after extension disconnect (id=${r}, action=${o.action}, context=${t})`)}C.traceResult(r,!1,Date.now()-o.dispatchTs,e.errorCode);w(r,o,{error:new b(e.message,e.errorCode,e.errorHint,e.status)})}}}const K=33554432;class L extends Error{status=413;constructor(){super(`请求体超过 ${K/1024/1024} MB 上限(正文内联图片过多/过大?可减少单次发布的图片体积)`);this.name="BodyTooLargeError"}}function xe(n){return new Promise((t,s)=>{const r=[];let o=0,e=!1;n.on("data",(i)=>{if(e)return;o+=i.length;if(o>K){e=!0;r.length=0;s(new L);return}r.push(i)});n.on("end",()=>{if(!e)t(Buffer.concat(r).toString("utf-8"))});n.on("error",(i)=>{if(!e)s(i)})})}function l(n,t,s,r){n.writeHead(t,{"Content-Type":"application/json",...r});n.end(JSON.stringify(s))}async function X(n,t){const s=n.headers.origin;if(s&&!s.startsWith("chrome-extension://")){l(t,403,{ok:!1,error:"Forbidden: cross-origin request blocked"});return}if(n.method==="OPTIONS"){t.writeHead(204);t.end();return}const r=n.url??"/",o=r.split("?")[0];if(n.method==="GET"&&o==="/ping"){l(t,200,{ok:!0},re(o,s));return}if(!n.headers["x-opencli"]){l(t,403,{ok:!1,error:"Forbidden: missing X-OpenCLI header"});return}if(n.method==="GET"&&o==="/status"){const e=process.uptime(),i=process.memoryUsage(),d=new URL(r,`http://localhost:${S}`).searchParams.get("contextId")?.trim()||void 0,m=J(d),k=W().map((y)=>({contextId:y.contextId,extensionConnected:!0,extensionVersion:y.extensionVersion??void 0,extensionCompatRange:y.extensionCompatRange??void 0,pending:[...u.values()].filter((g)=>g.contextId===y.contextId).length,lastSeenAt:y.lastSeenAt}));l(t,200,{ok:!0,pid:process.pid,uptime:e,daemonVersion:ee,extensionConnected:!!m.connection,extensionVersion:m.connection?.extensionVersion??void 0,extensionCompatRange:m.connection?.extensionCompatRange??void 0,contextId:m.connection?.contextId??d,profileRequired:m.errorCode==="profile_required",profileDisconnected:m.errorCode==="profile_disconnected",profiles:k,pending:u.size,sessionLeases:_.list(Date.now(),F),commandResultUnknown:A,memoryMB:Math.round(i.rss/1024/1024*10)/10,port:S});return}if(n.method==="GET"&&o==="/logs"){const i=new URL(r,`http://localhost:${S}`).searchParams.get("level"),c=i?P.filter((d)=>d.level===i):P;l(t,200,{ok:!0,logs:c});return}if(n.method==="DELETE"&&o==="/logs"){P.length=0;l(t,200,{ok:!0});return}if(n.method==="POST"&&o==="/shutdown"){l(t,200,{ok:!0,message:"Shutting down"});setTimeout(()=>N(),100);return}if(n.method==="POST"&&r==="/command"){try{const e=JSON.parse(await xe(n));if(!e.id){l(t,400,{ok:!1,error:"Missing command id"});return}if(e.action==="lease-release"){if(typeof e.runId==="string")_.releaseByRunId(e.runId);l(t,200,{id:e.id,ok:!0});return}const i=J(typeof e.contextId==="string"?e.contextId:void 0,e.access);if(!i.connection){l(t,i.errorCode==="profile_required"?409:503,{id:e.id,ok:!1,errorCode:i.errorCode,error:i.error,...i.errorHint?{errorHint:i.errorHint}:{}});return}let c,d;if(ue(e)){const R=Date.now(),E=le(i.connection.contextId,e.surface,e.session),I=_.touch(E,{runId:e.runId,command:typeof e.command==="string"&&e.command?e.command:e.action,now:R,hasPendingWork:F});if(!I.granted){const p=de(e.session,I.holder,R);a.warn(`[daemon] Session ${E} busy — rejected ${e.command??e.action} (runId=${e.runId}); held by ${I.holder.command} (runId=${I.holder.runId})`);l(t,p.status,{id:e.id,ok:!1,errorCode:p.errorCode,error:p.message,errorHint:p.errorHint});return}c=E;d=e.runId}const m=typeof e.deadlineAt==="number"&&e.deadlineAt>0?Math.max(1000,e.deadlineAt-Date.now()):typeof e.timeout==="number"&&e.timeout>0?e.timeout*1000:120000,k=u.get(e.id);if(k){const R=await new Promise((E,I)=>{k.settlers.push({resolve:E,reject:I})});l(t,200,R);return}const y=i.connection.contextId;i.connection.banner?.onActivity();const g=await new Promise((R,E)=>{const I=setTimeout(()=>{const f=u.get(e.id);if(!f)return;C.traceTimeout(e.id,m,typeof e.action==="string"?e.action:void 0);const h=te(f.action,m);if(h.countAsCommandResultUnknown&&f.dispatched){A++;a.warn(`[daemon] Command timed out after dispatch (id=${e.id}, action=${f.action}, timeout=${m}ms)`)}w(e.id,f,{error:new b(h.message,h.errorCode,h.errorHint,h.status)})},m),p={contextId:i.connection.contextId,action:typeof e.action==="string"?e.action:"unknown",dispatched:!1,dispatchTs:Date.now(),settlers:[{resolve:R,reject:E}],timer:I,...c&&d?{leaseKey:c,runId:d}:{}};u.set(e.id,p);const M=(f)=>{if(u.get(e.id)!==p)return;const h=G(p.contextId);w(e.id,p,{error:new b(h.message,h.errorCode,h.errorHint,h.status)});a.warn(`[daemon] Failed to dispatch command ${e.id}: ${f instanceof Error?f.message:String(f)}`)};try{i.connection.ws.send(JSON.stringify(e),(f)=>{if(f&&!p.dispatched)M(f)});p.dispatched=!0;C.traceDispatch(e)}catch(f){M(f)}}),U=g&&typeof g==="object"?g.page:void 0;i.connection.banner?.onActivity(typeof U==="string"?U:void 0);l(t,200,g&&typeof g==="object"&&!Array.isArray(g)?{...g,routedContextId:y}:g)}catch(e){const i=e instanceof b?e:null;if(e instanceof L){l(t,413,{ok:!1,error:e.message,errorCode:"body_too_large"});return}l(t,i?.status??(e instanceof Error&&e.message.includes("timeout")?408:400),{ok:!1,error:e instanceof Error?e.message:"Invalid request",...i?.errorCode?{errorCode:i.errorCode}:{},...i?.errorHint?{errorHint:i.errorHint}:{}})}return}l(t,404,{error:"Not found"})}const T=B((n,t)=>{X(n,t).catch(()=>{t.writeHead(500);t.end()})}),O=B((n,t)=>{X(n,t).catch(()=>{t.writeHead(500);t.end()})}),H=new Y({noServer:!0});function z(n,t,s){if((n.url??"/").split("?")[0]!=="/ext"){t.destroy();return}const o=n.headers.origin;if(o&&!o.startsWith("chrome-extension://")){t.destroy();return}H.handleUpgrade(n,t,s,(e)=>{H.emit("connection",e,n)})}T.on("upgrade",z);O.on("upgrade",z);H.on("connection",(n)=>{a.info("[daemon] Extension connected");let t=0;const s=setInterval(()=>{if(n.readyState!==V.OPEN){clearInterval(s);return}if(t>=2){a.warn("[daemon] Extension heartbeat lost, closing connection");clearInterval(s);n.terminate();return}t++;n.ping()},15000);n.on("pong",()=>{t=0});n.on("message",(r)=>{try{const o=JSON.parse(r.toString());if(o.type==="hello"){if(o.brand!=="publishport"){a.warn(`[daemon] 拒绝非 PublishPort 官方扩展连接(brand=${JSON.stringify(o.brand)})`);n.close(4003,"Only the official PublishPort extension is supported");return}const i=he(n,o.contextId);i.extensionVersion=typeof o.version==="string"?o.version:null;i.extensionCompatRange=typeof o.compatRange==="string"?o.compatRange:null;i.capabilities=Array.isArray(o.capabilities)?o.capabilities.filter((c)=>typeof c==="string"):[];i.lastSeenAt=Date.now();i.banner?.dispose();i.banner=ae(i.capabilities)?new se((c)=>me(i,c),(c)=>a.warn(c)):null;i.banner?.start();a.info(`[daemon] Extension profile connected: ${i.contextId}`);C.traceExtConnect(i.contextId,i.extensionVersion);return}if(o.type==="log"){if(o.level==="error")a.error(`[ext] ${o.msg}`);else if(o.level==="warn")a.warn(`[ext] ${o.msg}`);else a.info(`[ext] ${o.msg}`);ge({level:o.level,msg:o.msg,ts:o.ts??Date.now()});C.traceExtLog(o.level,o.msg);return}if(o.type==="ping")return;if(o.type==="event"){for(const i of x.values())if(i.ws===n)i.banner?.onEvent(String(o.name),o.args);return}const e=u.get(o.id);if(e){C.traceResult(o.id,!!o.ok,Date.now()-e.dispatchTs,typeof o.errorCode==="string"?o.errorCode:void 0,typeof o.page==="string"?o.page:void 0);w(o.id,e,{data:o})}}catch(o){const e=r.toString().slice(0,200);a.warn(`[daemon] Ignoring malformed WS message from extension: ${o instanceof Error?o.message:String(o)} (first 200 chars: ${JSON.stringify(e)})`)}});n.on("close",()=>{a.info("[daemon] Extension disconnected");clearInterval(s);j(n)});n.on("error",()=>{clearInterval(s);j(n)})});T.listen(S,"127.0.0.1",()=>{a.info(`[daemon] Listening on http://127.0.0.1:${S}`)});T.on("error",(n)=>{if(n.code==="EADDRINUSE"){a.error(`[daemon] Port ${S} already in use — another daemon is likely running. Exiting.`);process.exit(v.SERVICE_UNAVAIL)}a.error(`[daemon] Server error: ${n.message}`);process.exit(v.GENERIC_ERROR)});O.listen(D,"127.0.0.1",()=>{a.info(`[daemon] Listening on http://127.0.0.1:${D} (PublishPort extension)`)});O.on("error",(n)=>{a.warn(`[daemon] PublishPort port ${D} unavailable (${n.message}) — extension will fall back to ${S}`)});function N(){for(const[n,t]of u){const s=t.dispatched?new b("Daemon shutting down before the command completed.","daemon_shutting_down","The daemon is being replaced; a journaling extension replays the command result on retry.",503):(()=>{const r=G(t.contextId);return new b(r.message,r.errorCode,r.errorHint,r.status)})();w(n,t,{error:s})}u.clear();for(const n of x.values()){n.banner?.shutdown();n.ws.close()}O.close();T.close(()=>process.exit(v.SUCCESS));setTimeout(()=>{T.closeIdleConnections?.();O.closeIdleConnections?.();setTimeout(()=>process.exit(v.SUCCESS),500).unref()},100).unref()}process.on("SIGTERM",N);process.on("SIGINT",N);
1
+ import{createServer as V}from"node:http";import{WebSocketServer as Q,WebSocket as G}from"ws";import{DEFAULT_DAEMON_PORT as q,PUBLISHPORT_DAEMON_PORT as ee,unsupportedDaemonPortEnvMessage as ne}from"./constants.js";import{EXIT_CODES as P}from"./errors.js";import{log as a}from"./logger.js";import{PKG_VERSION as te}from"./version.js";import{DEFAULT_CONTEXT_ID as oe}from"./browser/profile.js";import{buildCommandDispatchFailure as W,buildCommandTimeoutFailure as re,buildExtensionDisconnectFailure as ie,getResponseCorsHeaders as se,resolveProfileRoute as ae}from"./daemon-utils.js";import*as E from"./browser/trace.js";import{DebuggerBanner as ce,supportsDebuggerBanner as F}from"./browser/debugger-banner.js";import{SessionLeaseRegistry as de,buildSessionBusyFailure as le,getSessionLeaseKey as ue,isSessionLeaseCommand as fe}from"./session-lease.js";const C=q,A=ee;if(process.env.OPENCLI_DAEMON_PORT){a.error(ne(process.env.OPENCLI_DAEMON_PORT));process.exit(P.USAGE_ERROR)}const x=new Map,u=new Map,_=new de;function J(n){for(const t of u.values())if(t.runId===n)return!0;return!1}function I(n,t,s){clearTimeout(t.timer);u.delete(n);if(t.leaseKey&&t.runId)_.heartbeat(t.leaseKey,t.runId,Date.now());for(const r of t.settlers)if(s.error)r.reject(s.error);else r.resolve(s.data)}let me=0;function D(n,t,s=15000){const r=`daemon-${process.pid}-${++me}`;return new Promise((o,e)=>{const i=setTimeout(()=>{const d=u.get(r);if(d)I(r,d,{error:Error(`daemon ${String(t.action)} timed out after ${s}ms`)})},s),c={contextId:n.contextId,action:String(t.action),dispatched:!1,dispatchTs:Date.now(),settlers:[{resolve:(d)=>o(d),reject:e}],timer:i};u.set(r,c);try{n.ws.send(JSON.stringify({...t,id:r}),(d)=>{if(d&&u.get(r)===c)I(r,c,{error:d})});c.dispatched=!0}catch(d){I(r,c,{error:d instanceof Error?d:Error(String(d))})}})}const pe="https://publishport.app/automation*";async function ge(n){try{const t=await D(n,{action:"chrome-api",api:"tabs.query",args:[{url:pe}]});if(!t.ok)throw Error(t.error??"tabs.query failed");const s=(Array.isArray(t.data)?t.data:[]).map((o)=>o.id).filter((o)=>typeof o==="number");if(s.length===0)return;const r=await D(n,{action:"chrome-api",api:"tabs.remove",args:[s]});if(!r.ok)throw Error(r.error??"tabs.remove failed");a.info(`[daemon] Closed ${s.length} legacy automation info tab(s)`)}catch(t){a.warn(`[daemon] Closing legacy automation info tabs failed: ${t instanceof Error?t.message:String(t)}`)}}let H=0;const he=200,v=[];class S extends Error{errorCode;errorHint;status;constructor(n,t,s,r=400){super(n);this.errorCode=t;this.errorHint=s;this.status=r;this.name="DaemonCommandFailure"}}function xe(n){v.push(n);if(v.length>he)v.shift()}function j(){return[...x.values()].filter((n)=>n.ws.readyState===G.OPEN)}function K(n,t){const s=j(),r=ae(s.map((e)=>e.contextId),n,t);if(r.kind==="error"){if(r.errorCode==="profile_disconnected"&&t==="write"&&s.length===1)a.warn(`[daemon] Refusing to route a WRITE to fallback profile "${s[0].contextId}" (requested "${n}")`);return{errorCode:r.errorCode,error:r.error,errorHint:r.errorHint}}if(r.kind==="fallback")a.warn(`[daemon] Requested profile "${r.requestedContextId}" not connected; falling back to the only connected profile "${r.contextId}"`);const o=s.find((e)=>e.contextId===r.contextId);if(!o)return{errorCode:"extension_not_connected",error:"浏览器桥未连接:这台机器上的 PublishPort 托管浏览器没在运行。",errorHint:"请用户打开 PublishPort 客户端,在平台列表里点一次「检测登录」把托管浏览器拉起来;仍不行就重启客户端。"};return{connection:o}}function ye(n,t){const s=typeof t==="string"&&t.trim()?t.trim():oe,r=x.get(s);if(r&&r.ws!==n)r.ws.close();const o=[...x.entries()].find(([,c])=>c.ws===n);if(o&&o[0]!==s)x.delete(o[0]);const e=x.get(s),i={contextId:s,ws:n,extensionVersion:e?.ws===n?e.extensionVersion:null,extensionCompatRange:e?.ws===n?e.extensionCompatRange:null,lastSeenAt:Date.now(),capabilities:e?.ws===n?e.capabilities:[],banner:e?.ws===n?e.banner:null};x.set(s,i);return i}function X(n){for(const[t,s]of x.entries()){if(s.ws!==n)continue;s.banner?.dispose();x.delete(t);E.traceExtDisconnect(t);for(const[r,o]of u){if(o.contextId!==t)continue;const e=ie({contextId:t,action:o.action,dispatched:o.dispatched});if(e.countAsCommandResultUnknown){H++;a.warn(`[daemon] Command result unknown after extension disconnect (id=${r}, action=${o.action}, context=${t})`)}E.traceResult(r,!1,Date.now()-o.dispatchTs,e.errorCode);I(r,o,{error:new S(e.message,e.errorCode,e.errorHint,e.status)})}}}const z=33554432;class U extends Error{status=413;constructor(){super(`请求体超过 ${z/1024/1024} MB 上限(正文内联图片过多/过大?可减少单次发布的图片体积)`);this.name="BodyTooLargeError"}}function Ie(n){return new Promise((t,s)=>{const r=[];let o=0,e=!1;n.on("data",(i)=>{if(e)return;o+=i.length;if(o>z){e=!0;r.length=0;s(new U);return}r.push(i)});n.on("end",()=>{if(!e)t(Buffer.concat(r).toString("utf-8"))});n.on("error",(i)=>{if(!e)s(i)})})}function l(n,t,s,r){n.writeHead(t,{"Content-Type":"application/json",...r});n.end(JSON.stringify(s))}async function Y(n,t){const s=n.headers.origin;if(s&&!s.startsWith("chrome-extension://")){l(t,403,{ok:!1,error:"Forbidden: cross-origin request blocked"});return}if(n.method==="OPTIONS"){t.writeHead(204);t.end();return}const r=n.url??"/",o=r.split("?")[0];if(n.method==="GET"&&o==="/ping"){l(t,200,{ok:!0},se(o,s));return}if(!n.headers["x-opencli"]){l(t,403,{ok:!1,error:"Forbidden: missing X-OpenCLI header"});return}if(n.method==="GET"&&o==="/status"){const e=process.uptime(),i=process.memoryUsage(),d=new URL(r,`http://localhost:${C}`).searchParams.get("contextId")?.trim()||void 0,m=K(d),k=j().map((w)=>({contextId:w.contextId,extensionConnected:!0,extensionVersion:w.extensionVersion??void 0,extensionCompatRange:w.extensionCompatRange??void 0,pending:[...u.values()].filter((g)=>g.contextId===w.contextId).length,lastSeenAt:w.lastSeenAt}));l(t,200,{ok:!0,pid:process.pid,uptime:e,daemonVersion:te,extensionConnected:!!m.connection,extensionVersion:m.connection?.extensionVersion??void 0,extensionCompatRange:m.connection?.extensionCompatRange??void 0,extensionCapabilities:m.connection?.capabilities??[],contextId:m.connection?.contextId??d,profileRequired:m.errorCode==="profile_required",profileDisconnected:m.errorCode==="profile_disconnected",profiles:k,pending:u.size,sessionLeases:_.list(Date.now(),J),commandResultUnknown:H,memoryMB:Math.round(i.rss/1024/1024*10)/10,port:C});return}if(n.method==="GET"&&o==="/logs"){const i=new URL(r,`http://localhost:${C}`).searchParams.get("level"),c=i?v.filter((d)=>d.level===i):v;l(t,200,{ok:!0,logs:c});return}if(n.method==="DELETE"&&o==="/logs"){v.length=0;l(t,200,{ok:!0});return}if(n.method==="POST"&&o==="/shutdown"){l(t,200,{ok:!0,message:"Shutting down"});setTimeout(()=>L(),100);return}if(n.method==="POST"&&r==="/command"){try{const e=JSON.parse(await Ie(n));if(!e.id){l(t,400,{ok:!1,error:"Missing command id"});return}if(e.action==="lease-release"){if(typeof e.runId==="string")_.releaseByRunId(e.runId);l(t,200,{id:e.id,ok:!0});return}const i=K(typeof e.contextId==="string"?e.contextId:void 0,e.access);if(!i.connection){l(t,i.errorCode==="profile_required"?409:503,{id:e.id,ok:!1,errorCode:i.errorCode,error:i.error,...i.errorHint?{errorHint:i.errorHint}:{}});return}let c,d;if(fe(e)){const R=Date.now(),b=ue(i.connection.contextId,e.surface,e.session),y=_.touch(b,{runId:e.runId,command:typeof e.command==="string"&&e.command?e.command:e.action,now:R,hasPendingWork:J});if(!y.granted){const p=le(e.session,y.holder,R);a.warn(`[daemon] Session ${b} busy — rejected ${e.command??e.action} (runId=${e.runId}); held by ${y.holder.command} (runId=${y.holder.runId})`);l(t,p.status,{id:e.id,ok:!1,errorCode:p.errorCode,error:p.message,errorHint:p.errorHint});return}c=b;d=e.runId}const m=typeof e.deadlineAt==="number"&&e.deadlineAt>0?Math.max(1000,e.deadlineAt-Date.now()):typeof e.timeout==="number"&&e.timeout>0?e.timeout*1000:120000,k=u.get(e.id);if(k){const R=await new Promise((b,y)=>{k.settlers.push({resolve:b,reject:y})});l(t,200,R);return}const w=i.connection.contextId;i.connection.banner?.onActivity();const g=await new Promise((R,b)=>{const y=setTimeout(()=>{const f=u.get(e.id);if(!f)return;E.traceTimeout(e.id,m,typeof e.action==="string"?e.action:void 0);const h=re(f.action,m);if(h.countAsCommandResultUnknown&&f.dispatched){H++;a.warn(`[daemon] Command timed out after dispatch (id=${e.id}, action=${f.action}, timeout=${m}ms)`)}I(e.id,f,{error:new S(h.message,h.errorCode,h.errorHint,h.status)})},m),p={contextId:i.connection.contextId,action:typeof e.action==="string"?e.action:"unknown",dispatched:!1,dispatchTs:Date.now(),settlers:[{resolve:R,reject:b}],timer:y,...c&&d?{leaseKey:c,runId:d}:{}};u.set(e.id,p);const B=(f)=>{if(u.get(e.id)!==p)return;const h=W(p.contextId);I(e.id,p,{error:new S(h.message,h.errorCode,h.errorHint,h.status)});a.warn(`[daemon] Failed to dispatch command ${e.id}: ${f instanceof Error?f.message:String(f)}`)};try{i.connection.ws.send(JSON.stringify(e),(f)=>{if(f&&!p.dispatched)B(f)});p.dispatched=!0;E.traceDispatch(e)}catch(f){B(f)}}),M=g&&typeof g==="object"?g.page:void 0;i.connection.banner?.onActivity(typeof M==="string"?M:void 0);l(t,200,g&&typeof g==="object"&&!Array.isArray(g)?{...g,routedContextId:w}:g)}catch(e){const i=e instanceof S?e:null;if(e instanceof U){l(t,413,{ok:!1,error:e.message,errorCode:"body_too_large"});return}l(t,i?.status??(e instanceof Error&&e.message.includes("timeout")?408:400),{ok:!1,error:e instanceof Error?e.message:"Invalid request",...i?.errorCode?{errorCode:i.errorCode}:{},...i?.errorHint?{errorHint:i.errorHint}:{}})}return}l(t,404,{error:"Not found"})}const T=V((n,t)=>{Y(n,t).catch(()=>{t.writeHead(500);t.end()})}),O=V((n,t)=>{Y(n,t).catch(()=>{t.writeHead(500);t.end()})}),N=new Q({noServer:!0});function Z(n,t,s){if((n.url??"/").split("?")[0]!=="/ext"){t.destroy();return}const o=n.headers.origin;if(o&&!o.startsWith("chrome-extension://")){t.destroy();return}N.handleUpgrade(n,t,s,(e)=>{N.emit("connection",e,n)})}T.on("upgrade",Z);O.on("upgrade",Z);N.on("connection",(n)=>{a.info("[daemon] Extension connected");let t=0;const s=setInterval(()=>{if(n.readyState!==G.OPEN){clearInterval(s);return}if(t>=2){a.warn("[daemon] Extension heartbeat lost, closing connection");clearInterval(s);n.terminate();return}t++;n.ping()},15000);n.on("pong",()=>{t=0});n.on("message",(r)=>{try{const o=JSON.parse(r.toString());if(o.type==="hello"){if(o.brand!=="publishport"){a.warn(`[daemon] 拒绝非 PublishPort 官方扩展连接(brand=${JSON.stringify(o.brand)})`);n.close(4003,"Only the official PublishPort extension is supported");return}const i=ye(n,o.contextId);i.extensionVersion=typeof o.version==="string"?o.version:null;i.extensionCompatRange=typeof o.compatRange==="string"?o.compatRange:null;i.capabilities=Array.isArray(o.capabilities)?o.capabilities.filter((c)=>typeof c==="string"):[];i.lastSeenAt=Date.now();i.banner?.dispose();i.banner=F(i.capabilities)?new ce((c)=>D(i,c),(c)=>a.warn(c)):null;i.banner?.start();if(F(i.capabilities))ge(i);a.info(`[daemon] Extension profile connected: ${i.contextId}`);E.traceExtConnect(i.contextId,i.extensionVersion);return}if(o.type==="log"){if(o.level==="error")a.error(`[ext] ${o.msg}`);else if(o.level==="warn")a.warn(`[ext] ${o.msg}`);else a.info(`[ext] ${o.msg}`);xe({level:o.level,msg:o.msg,ts:o.ts??Date.now()});E.traceExtLog(o.level,o.msg);return}if(o.type==="ping")return;if(o.type==="event"){for(const i of x.values())if(i.ws===n)i.banner?.onEvent(String(o.name),o.args);return}const e=u.get(o.id);if(e){E.traceResult(o.id,!!o.ok,Date.now()-e.dispatchTs,typeof o.errorCode==="string"?o.errorCode:void 0,typeof o.page==="string"?o.page:void 0);I(o.id,e,{data:o})}}catch(o){const e=r.toString().slice(0,200);a.warn(`[daemon] Ignoring malformed WS message from extension: ${o instanceof Error?o.message:String(o)} (first 200 chars: ${JSON.stringify(e)})`)}});n.on("close",()=>{a.info("[daemon] Extension disconnected");clearInterval(s);X(n)});n.on("error",()=>{clearInterval(s);X(n)})});T.listen(C,"127.0.0.1",()=>{a.info(`[daemon] Listening on http://127.0.0.1:${C}`)});T.on("error",(n)=>{if(n.code==="EADDRINUSE"){a.error(`[daemon] Port ${C} already in use — another daemon is likely running. Exiting.`);process.exit(P.SERVICE_UNAVAIL)}a.error(`[daemon] Server error: ${n.message}`);process.exit(P.GENERIC_ERROR)});O.listen(A,"127.0.0.1",()=>{a.info(`[daemon] Listening on http://127.0.0.1:${A} (PublishPort extension)`)});O.on("error",(n)=>{a.warn(`[daemon] PublishPort port ${A} unavailable (${n.message}) — extension will fall back to ${C}`)});function L(){for(const[n,t]of u){const s=t.dispatched?new S("Daemon shutting down before the command completed.","daemon_shutting_down","The daemon is being replaced; a journaling extension replays the command result on retry.",503):(()=>{const r=W(t.contextId);return new S(r.message,r.errorCode,r.errorHint,r.status)})();I(n,t,{error:s})}u.clear();for(const n of x.values()){n.banner?.shutdown();n.ws.close()}O.close();T.close(()=>process.exit(P.SUCCESS));setTimeout(()=>{T.closeIdleConnections?.();O.closeIdleConnections?.();setTimeout(()=>process.exit(P.SUCCESS),500).unref()},100).unref()}process.on("SIGTERM",L);process.on("SIGINT",L);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.0.47",
3
+ "version": "1.0.48",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false