publishport-opencli 1.0.46 → 1.0.47
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.
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps Chrome's "PublishPort started debugging this browser" banner steady.
|
|
3
|
+
*
|
|
4
|
+
* Chrome shows the banner while any extension debugger session is attached.
|
|
5
|
+
* Commands attach and release tabs one by one, so on its own the banner pops
|
|
6
|
+
* in and out with every command. While commands are flowing, this holds one
|
|
7
|
+
* extra "anchor" attachment on a long-lived page tab (no CDP domain enabled,
|
|
8
|
+
* so the page is unaffected); after `idleMs` without commands the anchor is
|
|
9
|
+
* released and the banner disappears until the next command.
|
|
10
|
+
*
|
|
11
|
+
* Needs the extension shell primitives `chrome-api` and `events` (1.1.5+).
|
|
12
|
+
*/
|
|
13
|
+
export declare const BANNER_IDLE_MS: number;
|
|
14
|
+
export declare const BANNER_CAPABILITIES: readonly ["chrome-api", "events"];
|
|
15
|
+
export type ExtensionCall = (body: Record<string, unknown>) => Promise<{
|
|
16
|
+
ok?: boolean;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
error?: string;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function supportsDebuggerBanner(capabilities: readonly string[]): boolean;
|
|
21
|
+
export declare class DebuggerBanner {
|
|
22
|
+
private readonly call;
|
|
23
|
+
private readonly warn;
|
|
24
|
+
private readonly idleMs;
|
|
25
|
+
private anchorTabId;
|
|
26
|
+
private idleTimer;
|
|
27
|
+
private queue;
|
|
28
|
+
/** Target ids used by commands: their attachment belongs to the command, never anchor there. */
|
|
29
|
+
private readonly commandTargets;
|
|
30
|
+
private active;
|
|
31
|
+
private disposed;
|
|
32
|
+
constructor(call: ExtensionCall, warn: (msg: string) => void, idleMs?: number);
|
|
33
|
+
/** Subscribe to detach events and drop an anchor left behind by a previous daemon. */
|
|
34
|
+
start(): Promise<void>;
|
|
35
|
+
/** A command is being dispatched; `page` is the target id it ran on, once known. */
|
|
36
|
+
onActivity(page?: string): void;
|
|
37
|
+
onEvent(name: string, args: unknown): void;
|
|
38
|
+
/** Daemon shutdown: release the anchor so the banner does not outlive PublishPort. */
|
|
39
|
+
shutdown(): void;
|
|
40
|
+
/** Extension connection closed: nothing can be sent any more. */
|
|
41
|
+
dispose(): void;
|
|
42
|
+
private enqueue;
|
|
43
|
+
private expect;
|
|
44
|
+
private rememberCommandTarget;
|
|
45
|
+
private ensureAnchor;
|
|
46
|
+
private releaseAnchor;
|
|
47
|
+
private detach;
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const BANNER_IDLE_MS=300000,BANNER_CAPABILITIES=["chrome-api","events"];const s="publishport:banner-anchor",r=500;export function supportsDebuggerBanner(t){return BANNER_CAPABILITIES.every((e)=>t.includes(e))}export class DebuggerBanner{call;warn;idleMs;anchorTabId=null;idleTimer=null;queue=Promise.resolve();commandTargets=new Set;active=!1;disposed=!1;constructor(t,e,a=BANNER_IDLE_MS){this.call=t;this.warn=e;this.idleMs=a}start(){return this.enqueue(async()=>{await this.expect(this.call({action:"events",events:[{name:"debugger.onDetach"}]}),"subscribe debugger.onDetach");const e=(await this.expect(this.call({action:"chrome-api",api:"storage.session.get",args:[s]}),"read stored anchor"))?.[s];if(typeof e==="number"&&!this.active)await this.detach(e)})}onActivity(t){if(this.disposed)return;if(t)this.rememberCommandTarget(t);this.active=!0;if(this.idleTimer)clearTimeout(this.idleTimer);this.idleTimer=setTimeout(()=>{this.idleTimer=null;this.active=!1;this.enqueue(()=>this.releaseAnchor())},this.idleMs);this.idleTimer.unref?.();this.enqueue(()=>this.ensureAnchor())}onEvent(t,e){if(this.disposed||t!=="debugger.onDetach"||!Array.isArray(e))return;const a=e[0];if(typeof a?.tabId!=="number"||a.tabId!==this.anchorTabId)return;this.anchorTabId=null;if(e[1]==="canceled_by_user")return;if(this.active)this.enqueue(()=>this.ensureAnchor())}shutdown(){this.active=!1;if(this.idleTimer)clearTimeout(this.idleTimer);this.idleTimer=null;const t=this.anchorTabId;this.anchorTabId=null;this.disposed=!0;if(t!==null)this.call({action:"chrome-api",api:"debugger.detach",args:[{tabId:t}]}).catch((e)=>{this.warn(`[daemon] debugger banner: detach on shutdown failed: ${e instanceof Error?e.message:String(e)}`)})}dispose(){this.disposed=!0;if(this.idleTimer)clearTimeout(this.idleTimer);this.idleTimer=null}enqueue(t){this.queue=this.queue.then(t).catch((e)=>{this.warn(`[daemon] debugger banner: ${e instanceof Error?e.message:String(e)}`)});return this.queue}async expect(t,e){const a=await t;if(!a?.ok)throw Error(`${e} failed: ${a?.error??"no result"}`);return a.data??null}rememberCommandTarget(t){this.commandTargets.delete(t);this.commandTargets.add(t);if(this.commandTargets.size>r){const e=this.commandTargets.values().next().value;if(e!==void 0)this.commandTargets.delete(e)}}async ensureAnchor(){if(!this.active||this.disposed)return;const t=await this.expect(this.call({action:"chrome-api",api:"debugger.getTargets"}),"list debugger targets");if(this.anchorTabId!==null){const a=t.find((i)=>i.tabId===this.anchorTabId);if(a?.attached&&!this.commandTargets.has(a.id))return;this.anchorTabId=null}const e=t.filter((a)=>a.type==="page"&&typeof a.tabId==="number"&&/^https?:/.test(a.url)&&!a.attached&&!this.commandTargets.has(a.id)).sort((a,i)=>a.tabId-i.tabId);for(const a of e){const i=a.tabId;if(!(await this.call({action:"chrome-api",api:"debugger.attach",args:[{tabId:i},"1.3"]}))?.ok)continue;this.anchorTabId=i;await this.expect(this.call({action:"chrome-api",api:"storage.session.set",args:[{[s]:i}]}),"store anchor");return}this.warn("[daemon] debugger banner: no http(s) tab available to hold the debugger session")}async releaseAnchor(){if(this.active||this.anchorTabId===null)return;const t=this.anchorTabId;this.anchorTabId=null;await this.detach(t)}async detach(t){const e=await this.call({action:"chrome-api",api:"debugger.detach",args:[{tabId:t}]});if(!e?.ok&&!/not attached|No tab with given id/i.test(e?.error??""))throw Error(`detach anchor ${t} failed: ${e?.error??"no result"}`);await this.expect(this.call({action:"chrome-api",api:"storage.session.remove",args:[s]}),"clear stored anchor")}}
|
package/dist/src/daemon.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createServer as M}from"node:http";import{WebSocketServer as z,WebSocket as V}from"ws";import{DEFAULT_DAEMON_PORT as Y,PUBLISHPORT_DAEMON_PORT as Z,unsupportedDaemonPortEnvMessage as Q}from"./constants.js";import{EXIT_CODES as b}from"./errors.js";import{log as a}from"./logger.js";import{PKG_VERSION as q}from"./version.js";import{DEFAULT_CONTEXT_ID as ee}from"./browser/profile.js";import{buildCommandDispatchFailure as B,buildCommandTimeoutFailure as ne,buildExtensionDisconnectFailure as te,getResponseCorsHeaders as oe,resolveProfileRoute as re}from"./daemon-utils.js";import*as w from"./browser/trace.js";import{SessionLeaseRegistry as ie,buildSessionBusyFailure as se,getSessionLeaseKey as ae,isSessionLeaseCommand as ce}from"./session-lease.js";const y=Y,D=Z;if(process.env.OPENCLI_DAEMON_PORT){a.error(Q(process.env.OPENCLI_DAEMON_PORT));process.exit(b.USAGE_ERROR)}const p=new Map,u=new Map,_=new ie;function G(n){for(const t of u.values())if(t.runId===n)return!0;return!1}function v(n,t,i){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(i.error)r.reject(i.error);else r.resolve(i.data)}let H=0;const de=200,P=[];class S extends Error{errorCode;errorHint;status;constructor(n,t,i,r=400){super(n);this.errorCode=t;this.errorHint=i;this.status=r;this.name="DaemonCommandFailure"}}function ue(n){P.push(n);if(P.length>de)P.shift()}function F(){return[...p.values()].filter((n)=>n.ws.readyState===V.OPEN)}function W(n,t){const i=F(),r=re(i.map((e)=>e.contextId),n,t);if(r.kind==="error"){if(r.errorCode==="profile_disconnected"&&t==="write"&&i.length===1)a.warn(`[daemon] Refusing to route a WRITE to fallback profile "${i[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=i.find((e)=>e.contextId===r.contextId);if(!o)return{errorCode:"extension_not_connected",error:"浏览器桥未连接:这台机器上的 PublishPort 托管浏览器没在运行。",errorHint:"请用户打开 PublishPort 客户端,在平台列表里点一次「检测登录」把托管浏览器拉起来;仍不行就重启客户端。"};return{connection:o}}function le(n,t){const i=typeof t==="string"&&t.trim()?t.trim():ee,r=p.get(i);if(r&&r.ws!==n)r.ws.close();const o=[...p.entries()].find(([,g])=>g.ws===n);if(o&&o[0]!==i)p.delete(o[0]);const e=p.get(i),s={contextId:i,ws:n,extensionVersion:e?.ws===n?e.extensionVersion:null,extensionCompatRange:e?.ws===n?e.extensionCompatRange:null,lastSeenAt:Date.now()};p.set(i,s);return s}function J(n){for(const[t,i]of p.entries()){if(i.ws!==n)continue;p.delete(t);w.traceExtDisconnect(t);for(const[r,o]of u){if(o.contextId!==t)continue;const e=te({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})`)}w.traceResult(r,!1,Date.now()-o.dispatchTs,e.errorCode);v(r,o,{error:new S(e.message,e.errorCode,e.errorHint,e.status)})}}}const j=33554432;class L extends Error{status=413;constructor(){super(`请求体超过 ${j/1024/1024} MB 上限(正文内联图片过多/过大?可减少单次发布的图片体积)`);this.name="BodyTooLargeError"}}function fe(n){return new Promise((t,i)=>{const r=[];let o=0,e=!1;n.on("data",(s)=>{if(e)return;o+=s.length;if(o>j){e=!0;r.length=0;i(new L);return}r.push(s)});n.on("end",()=>{if(!e)t(Buffer.concat(r).toString("utf-8"))});n.on("error",(s)=>{if(!e)i(s)})})}function c(n,t,i,r){n.writeHead(t,{"Content-Type":"application/json",...r});n.end(JSON.stringify(i))}async function K(n,t){const i=n.headers.origin;if(i&&!i.startsWith("chrome-extension://")){c(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"){c(t,200,{ok:!0},oe(o,i));return}if(!n.headers["x-opencli"]){c(t,403,{ok:!1,error:"Forbidden: missing X-OpenCLI header"});return}if(n.method==="GET"&&o==="/status"){const e=process.uptime(),s=process.memoryUsage(),h=new URL(r,`http://localhost:${y}`).searchParams.get("contextId")?.trim()||void 0,l=W(h),k=F().map((I)=>({contextId:I.contextId,extensionConnected:!0,extensionVersion:I.extensionVersion??void 0,extensionCompatRange:I.extensionCompatRange??void 0,pending:[...u.values()].filter((C)=>C.contextId===I.contextId).length,lastSeenAt:I.lastSeenAt}));c(t,200,{ok:!0,pid:process.pid,uptime:e,daemonVersion:q,extensionConnected:!!l.connection,extensionVersion:l.connection?.extensionVersion??void 0,extensionCompatRange:l.connection?.extensionCompatRange??void 0,contextId:l.connection?.contextId??h,profileRequired:l.errorCode==="profile_required",profileDisconnected:l.errorCode==="profile_disconnected",profiles:k,pending:u.size,sessionLeases:_.list(Date.now(),G),commandResultUnknown:H,memoryMB:Math.round(s.rss/1024/1024*10)/10,port:y});return}if(n.method==="GET"&&o==="/logs"){const s=new URL(r,`http://localhost:${y}`).searchParams.get("level"),g=s?P.filter((h)=>h.level===s):P;c(t,200,{ok:!0,logs:g});return}if(n.method==="DELETE"&&o==="/logs"){P.length=0;c(t,200,{ok:!0});return}if(n.method==="POST"&&o==="/shutdown"){c(t,200,{ok:!0,message:"Shutting down"});setTimeout(()=>N(),100);return}if(n.method==="POST"&&r==="/command"){try{const e=JSON.parse(await fe(n));if(!e.id){c(t,400,{ok:!1,error:"Missing command id"});return}if(e.action==="lease-release"){if(typeof e.runId==="string")_.releaseByRunId(e.runId);c(t,200,{id:e.id,ok:!0});return}const s=W(typeof e.contextId==="string"?e.contextId:void 0,e.access);if(!s.connection){c(t,s.errorCode==="profile_required"?409:503,{id:e.id,ok:!1,errorCode:s.errorCode,error:s.error,...s.errorHint?{errorHint:s.errorHint}:{}});return}let g,h;if(ce(e)){const R=Date.now(),E=ae(s.connection.contextId,e.surface,e.session),x=_.touch(E,{runId:e.runId,command:typeof e.command==="string"&&e.command?e.command:e.action,now:R,hasPendingWork:G});if(!x.granted){const f=se(e.session,x.holder,R);a.warn(`[daemon] Session ${E} busy — rejected ${e.command??e.action} (runId=${e.runId}); held by ${x.holder.command} (runId=${x.holder.runId})`);c(t,f.status,{id:e.id,ok:!1,errorCode:f.errorCode,error:f.message,errorHint:f.errorHint});return}g=E;h=e.runId}const l=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,x)=>{k.settlers.push({resolve:E,reject:x})});c(t,200,R);return}const I=s.connection.contextId,C=await new Promise((R,E)=>{const x=setTimeout(()=>{const d=u.get(e.id);if(!d)return;w.traceTimeout(e.id,l,typeof e.action==="string"?e.action:void 0);const m=ne(d.action,l);if(m.countAsCommandResultUnknown&&d.dispatched){H++;a.warn(`[daemon] Command timed out after dispatch (id=${e.id}, action=${d.action}, timeout=${l}ms)`)}v(e.id,d,{error:new S(m.message,m.errorCode,m.errorHint,m.status)})},l),f={contextId:s.connection.contextId,action:typeof e.action==="string"?e.action:"unknown",dispatched:!1,dispatchTs:Date.now(),settlers:[{resolve:R,reject:E}],timer:x,...g&&h?{leaseKey:g,runId:h}:{}};u.set(e.id,f);const U=(d)=>{if(u.get(e.id)!==f)return;const m=B(f.contextId);v(e.id,f,{error:new S(m.message,m.errorCode,m.errorHint,m.status)});a.warn(`[daemon] Failed to dispatch command ${e.id}: ${d instanceof Error?d.message:String(d)}`)};try{s.connection.ws.send(JSON.stringify(e),(d)=>{if(d&&!f.dispatched)U(d)});f.dispatched=!0;w.traceDispatch(e)}catch(d){U(d)}});c(t,200,C&&typeof C==="object"&&!Array.isArray(C)?{...C,routedContextId:I}:C)}catch(e){const s=e instanceof S?e:null;if(e instanceof L){c(t,413,{ok:!1,error:e.message,errorCode:"body_too_large"});return}c(t,s?.status??(e instanceof Error&&e.message.includes("timeout")?408:400),{ok:!1,error:e instanceof Error?e.message:"Invalid request",...s?.errorCode?{errorCode:s.errorCode}:{},...s?.errorHint?{errorHint:s.errorHint}:{}})}return}c(t,404,{error:"Not found"})}const O=M((n,t)=>{K(n,t).catch(()=>{t.writeHead(500);t.end()})}),T=M((n,t)=>{K(n,t).catch(()=>{t.writeHead(500);t.end()})}),A=new z({noServer:!0});function X(n,t,i){if((n.url??"/").split("?")[0]!=="/ext"){t.destroy();return}const o=n.headers.origin;if(o&&!o.startsWith("chrome-extension://")){t.destroy();return}A.handleUpgrade(n,t,i,(e)=>{A.emit("connection",e,n)})}O.on("upgrade",X);T.on("upgrade",X);A.on("connection",(n)=>{a.info("[daemon] Extension connected");let t=0;const i=setInterval(()=>{if(n.readyState!==V.OPEN){clearInterval(i);return}if(t>=2){a.warn("[daemon] Extension heartbeat lost, closing connection");clearInterval(i);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 s=le(n,o.contextId);s.extensionVersion=typeof o.version==="string"?o.version:null;s.extensionCompatRange=typeof o.compatRange==="string"?o.compatRange:null;s.lastSeenAt=Date.now();a.info(`[daemon] Extension profile connected: ${s.contextId}`);w.traceExtConnect(s.contextId,s.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}`);ue({level:o.level,msg:o.msg,ts:o.ts??Date.now()});w.traceExtLog(o.level,o.msg);return}if(o.type==="ping")return;const e=u.get(o.id);if(e){w.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);v(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(i);J(n)});n.on("error",()=>{clearInterval(i);J(n)})});O.listen(y,"127.0.0.1",()=>{a.info(`[daemon] Listening on http://127.0.0.1:${y}`)});O.on("error",(n)=>{if(n.code==="EADDRINUSE"){a.error(`[daemon] Port ${y} already in use — another daemon is likely running. Exiting.`);process.exit(b.SERVICE_UNAVAIL)}a.error(`[daemon] Server error: ${n.message}`);process.exit(b.GENERIC_ERROR)});T.listen(D,"127.0.0.1",()=>{a.info(`[daemon] Listening on http://127.0.0.1:${D} (PublishPort extension)`)});T.on("error",(n)=>{a.warn(`[daemon] PublishPort port ${D} unavailable (${n.message}) — extension will fall back to ${y}`)});function N(){for(const[n,t]of u){const i=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=B(t.contextId);return new S(r.message,r.errorCode,r.errorHint,r.status)})();v(n,t,{error:i})}u.clear();for(const n of p.values())n.ws.close();T.close();O.close(()=>process.exit(b.SUCCESS));setTimeout(()=>{O.closeIdleConnections?.();T.closeIdleConnections?.();setTimeout(()=>process.exit(b.SUCCESS),500).unref()},100).unref()}process.on("SIGTERM",N);process.on("SIGINT",N);
|
|
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);
|