publishport-opencli 1.0.46 → 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;
|
|
@@ -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")}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fetchDaemonStatus as a,requestDaemonShutdown as u}from"../browser/daemon-transport.js";import{restartDaemon as
|
|
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)}`)}}
|
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 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);
|