publishport-opencli 1.8.5-pp.24 → 1.8.5-pp.26

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.
package/cli-manifest.json CHANGED
@@ -23095,6 +23095,55 @@
23095
23095
  "sourceFile": "linkedin/connect.js",
23096
23096
  "navigateBefore": true
23097
23097
  },
23098
+ {
23099
+ "site": "linkedin",
23100
+ "name": "create",
23101
+ "description": "Publish a post to your LinkedIn feed via the Voyager API. Supports plain text plus images and visibility scope. Dry-run by default; pass --send true to actually publish.",
23102
+ "access": "write",
23103
+ "domain": "www.linkedin.com",
23104
+ "strategy": "cookie",
23105
+ "browser": true,
23106
+ "args": [
23107
+ {
23108
+ "name": "content",
23109
+ "type": "string",
23110
+ "required": true,
23111
+ "positional": true,
23112
+ "help": "Post body text (max 3000 chars). Emojis and line breaks allowed."
23113
+ },
23114
+ {
23115
+ "name": "image",
23116
+ "type": "string",
23117
+ "required": false,
23118
+ "help": "Up to 9 images: comma-separated local file paths and/or http(s) URLs. Local paths ≤10MB each (jpg/png/gif)."
23119
+ },
23120
+ {
23121
+ "name": "visibility",
23122
+ "type": "string",
23123
+ "default": "anyone",
23124
+ "required": false,
23125
+ "help": "Who can see the post: anyone (public, default) or connections (1st-degree only)."
23126
+ },
23127
+ {
23128
+ "name": "send",
23129
+ "type": "bool",
23130
+ "default": false,
23131
+ "required": false,
23132
+ "help": "Actually publish. Default false = dry-run (verifies the signed-in session only; no post is created)."
23133
+ }
23134
+ ],
23135
+ "columns": [
23136
+ "status",
23137
+ "content_chars",
23138
+ "images",
23139
+ "visibility",
23140
+ "activity_urn"
23141
+ ],
23142
+ "type": "js",
23143
+ "modulePath": "linkedin/create.js",
23144
+ "sourceFile": "linkedin/create.js",
23145
+ "navigateBefore": "https://www.linkedin.com"
23146
+ },
23098
23147
  {
23099
23148
  "site": "linkedin",
23100
23149
  "name": "inbox",
@@ -1 +1 @@
1
- import{AuthRequiredError as q,TimeoutError as z,getErrorMessage as N}from"@jackwener/opencli/errors";import{cli as $,Strategy as Q}from"@jackwener/opencli/registry";const V=300,b=2000;function D(v,x){const G=x&&typeof x==="object"&&!Array.isArray(x)?x:{};return{logged_in:!0,site:v,...G}}function j(v){return v instanceof q}async function Y(v,x,G){const K=G==="poll"&&v.poll?v.poll:v.verify;return D(v.site,await K(x,{phase:G}))}async function J(v){if(typeof v?.closeWindow==="function")try{await v.closeWindow()}catch{}}function F(v){return`Run \`opencli ${v.site} login\` to open the login page, then retry.`}function H(v){return["logged_in","site",...v.columns??["id","username","name"]]}function I(v){if(typeof v==="boolean")return{logged_in:v};if(v&&typeof v==="object"&&!Array.isArray(v))return{logged_in:!!v.logged_in,...v};return{logged_in:!1}}function P(v){if(v&&typeof v==="object"&&!Array.isArray(v))return v;return{touched:!0}}export function registerSiteAuthCommands(v){if(!v?.site||!v?.domain||!v?.loginUrl||typeof v.verify!=="function")throw Error("registerSiteAuthCommands requires site, domain, loginUrl, and verify(page)");$({site:v.site,name:"whoami",access:"read",description:v.whoamiDescription??`Show the current logged-in ${v.site} account`,domain:v.domain,strategy:Q.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[],columns:H(v),authStatus:{...typeof v.quickCheck==="function"?{quickCheck:async(x)=>I(await v.quickCheck(x))}:{},...typeof v.refresh==="function"?{refresh:async(x,G)=>P(await v.refresh(x,G))}:{}},func:async(x)=>Y(v,x,"identity")});$({site:v.site,name:"login",access:"write",description:v.loginDescription??`Open ${v.site} login and wait until the browser session is authenticated`,domain:v.domain,strategy:Q.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"timeout",type:"int",default:V,help:"Maximum seconds to wait for the user to finish login"}],columns:["status",...H(v)],func:async(x,G)=>{try{const B={status:"already_logged_in",...await Y(v,x,"identity")};await J(x);return B}catch(B){if(!j(B)){await J(x);throw B}}await x.goto(v.loginUrl);const K=Number(G.timeout??V),Z=Date.now()+K*1000;let X="";while(Date.now()<Z){await x.wait(Math.min(b/1000,Math.max(0.2,(Z-Date.now())/1000)));try{const B=await Y(v,x,"poll");await J(x);return{status:"login_complete",...B}}catch(B){if(!j(B)){await J(x);throw B}X=N(B)}}await J(x);throw new z(`${v.site} login`,K,X?`${F(v)} Last auth check: ${X}`:F(v))}})}
1
+ import{AuthRequiredError as q,TimeoutError as z,getErrorMessage as N}from"@jackwener/opencli/errors";import{cli as $,Strategy as Q}from"@jackwener/opencli/registry";const V=300,b=2000;function D(v,x){const G=x&&typeof x==="object"&&!Array.isArray(x)?x:{};return{logged_in:!0,site:v,...G}}function j(v){return v instanceof q}async function Y(v,x,G){const K=G==="poll"&&v.poll?v.poll:v.verify;return D(v.site,await K(x,{phase:G}))}async function J(v){if(typeof v?.closeWindow==="function")try{await v.closeWindow()}catch{}}function F(v){return`Run \`opencli ${v.site} login\` to open the login page, then retry.`}function H(v){return["logged_in","site",...v.columns??["id","username","name"]]}function I(v){if(typeof v==="boolean")return{logged_in:v};if(v&&typeof v==="object"&&!Array.isArray(v))return{logged_in:!!v.logged_in,...v};return{logged_in:!1}}function P(v){if(v&&typeof v==="object"&&!Array.isArray(v))return v;return{touched:!0}}export function registerSiteAuthCommands(v){if(!v?.site||!v?.domain||!v?.loginUrl||typeof v.verify!=="function")throw Error("registerSiteAuthCommands requires site, domain, loginUrl, and verify(page)");$({site:v.site,name:"whoami",access:"read",description:v.whoamiDescription??`Show the current logged-in ${v.site} account`,domain:v.domain,strategy:Q.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[],columns:H(v),authStatus:{...typeof v.quickCheck==="function"?{quickCheck:async(x)=>I(await v.quickCheck(x))}:{},...typeof v.quickCheckCacheTtlSeconds==="number"&&v.quickCheckCacheTtlSeconds>0?{quickCheckCacheTtlSeconds:v.quickCheckCacheTtlSeconds}:{},...typeof v.refresh==="function"?{refresh:async(x,G)=>P(await v.refresh(x,G))}:{}},func:async(x)=>Y(v,x,"identity")});$({site:v.site,name:"login",access:"write",description:v.loginDescription??`Open ${v.site} login and wait until the browser session is authenticated`,domain:v.domain,strategy:Q.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"timeout",type:"int",default:V,help:"Maximum seconds to wait for the user to finish login"}],columns:["status",...H(v)],func:async(x,G)=>{try{const B={status:"already_logged_in",...await Y(v,x,"identity")};await J(x);return B}catch(B){if(!j(B)){await J(x);throw B}}await x.goto(v.loginUrl);const K=Number(G.timeout??V),Z=Date.now()+K*1000;let X="";while(Date.now()<Z){await x.wait(Math.min(b/1000,Math.max(0.2,(Z-Date.now())/1000)));try{const B=await Y(v,x,"poll");await J(x);return{status:"login_complete",...B}}catch(B){if(!j(B)){await J(x);throw B}X=N(B)}}await J(x);throw new z(`${v.site} login`,K,X?`${F(v)} Last auth check: ${X}`:F(v))}})}
package/clis/jike/auth.js CHANGED
@@ -1,4 +1,4 @@
1
- import{AuthRequiredError as L,CommandExecutionError as K}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as Q}from"../_shared/site-auth.js";const N=`(async () => {
1
+ import{AuthRequiredError as N,CommandExecutionError as K}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as V}from"../_shared/site-auth.js";const Q=`(async () => {
2
2
  try {
3
3
  const token = localStorage.getItem('JK_ACCESS_TOKEN') || '';
4
4
  if (!token) return { kind: 'auth', detail: 'Jike JK_ACCESS_TOKEN missing from localStorage (anonymous)' };
@@ -14,6 +14,6 @@ import{AuthRequiredError as L,CommandExecutionError as K}from"@jackwener/opencli
14
14
  } catch (e) {
15
15
  return { kind: 'exception', detail: String(e && e.message || e) };
16
16
  }
17
- })()`;async function T(z){await z.goto("https://web.okjike.com/");await z.wait(2);let F="";for(let G=0;G<30;G++){F=await z.evaluate("() => location.href");if(F.includes("okjike.com"))break;await z.wait(0.5)}if(!F.includes("okjike.com")){await z.screenshot({path:"/tmp/jike_whoami_nav_debug.png"});throw new K(`Jike whoami: navigation never settled on okjike.com (landed on ${F}). Debug screenshot: /tmp/jike_whoami_nav_debug.png`)}let D=await z.evaluate(N);for(let G=0;G<30&&D?.kind==="auth";G++){await z.wait(0.5);D=await z.evaluate(N)}if(D?.kind==="auth")throw new L("web.okjike.com",D.detail);if(D?.kind==="http")throw new K(`HTTP ${D.httpStatus} from Jike users/profile`);if(D?.kind==="exception"){const G=await z.evaluate("() => location.href");if(/^data:/.test(G)||/\/login/.test(F))throw new L("web.okjike.com","Jike session anonymous (redirected to /login)");throw new K(`Jike whoami failed: ${D.detail}`)}if(!D?.ok)throw new K(`Unexpected Jike probe: ${JSON.stringify(D)}`);return{user_id:D.user_id,screen_name:D.screen_name,username:D.username}}Q({site:"jike",domain:"web.okjike.com",loginUrl:"https://web.okjike.com/login",columns:["user_id","screen_name","username"],verify:T,quickCheck:async(z)=>{await z.goto("https://web.okjike.com/robots.txt");return{logged_in:await z.evaluate(`(() => {
17
+ })()`;async function X(z){await z.goto("https://web.okjike.com/");await z.wait(2);let F="";for(let G=0;G<30;G++){F=await z.evaluate("() => location.href");if(F.includes("okjike.com"))break;await z.wait(0.5)}if(!F.includes("okjike.com")){await z.screenshot({path:"/tmp/jike_whoami_nav_debug.png"});throw new K(`Jike whoami: navigation never settled on okjike.com (landed on ${F}). Debug screenshot: /tmp/jike_whoami_nav_debug.png`)}let D=await z.evaluate(Q);for(let G=0;G<30&&D?.kind==="auth";G++){await z.wait(0.5);D=await z.evaluate(Q)}if(D?.kind==="auth")throw new N("web.okjike.com",D.detail);if(D?.kind==="http")throw new K(`HTTP ${D.httpStatus} from Jike users/profile`);if(D?.kind==="exception"){const G=await z.evaluate("() => location.href");if(/^data:/.test(G)||/\/login/.test(F))throw new N("web.okjike.com","Jike session anonymous (redirected to /login)");throw new K(`Jike whoami failed: ${D.detail}`)}if(!D?.ok)throw new K(`Unexpected Jike probe: ${JSON.stringify(D)}`);return{user_id:D.user_id,screen_name:D.screen_name,username:D.username}}V({site:"jike",domain:"web.okjike.com",loginUrl:"https://web.okjike.com/login",columns:["user_id","screen_name","username"],verify:X,quickCheckCacheTtlSeconds:300,quickCheck:async(z)=>{await z.goto("https://web.okjike.com/robots.txt");return{logged_in:await z.evaluate(`(() => {
18
18
  try { return !!localStorage.getItem('JK_ACCESS_TOKEN'); } catch { return false; }
19
- })()`)===!0}},poll:async(z)=>{const F=await z.evaluate(N);if(!F?.ok)throw new L("web.okjike.com","Waiting for Jike login");return{user_id:F.user_id,screen_name:F.screen_name,username:F.username}}});
19
+ })()`)===!0}},poll:async(z)=>{const F=await z.evaluate(Q);if(!F?.ok)throw new N("web.okjike.com","Waiting for Jike login");return{user_id:F.user_id,screen_name:F.screen_name,username:F.username}}});
@@ -0,0 +1,113 @@
1
+ import{cli as j,Strategy as G}from"@jackwener/opencli/registry";import{ArgumentError as Z,AuthRequiredError as V,CommandExecutionError as Y}from"@jackwener/opencli/errors";import{LINKEDIN_DOMAIN as q,unwrapEvaluateResult as k}from"./shared.js";import{isLocalImagePath as P,localImageToDataUri as h}from"../_shared/article/images.js";const x=3000,B=9,C=10485760,D={anyone:{visibleToConnectionsOnly:!1,label:"ANYONE"},connections:{visibleToConnectionsOnly:!0,label:"CONNECTIONS"}};function L(Q){const J=String(Q??"anyone").trim().toLowerCase(),z=D[J];if(!z)throw new Z(`--visibility 只能是 anyone 或 connections(收到 "${Q}")`);return z}function R(Q){return String(Q??"").split(",").map((J)=>J.trim()).filter(Boolean)}async function X(Q){const J=[];for(const z of Q)if(P(z)){const{dataUri:$,bytes:W,mime:f}=await h(z);if(W>C)throw new Z(`图片过大:${z}(${(W/1024/1024).toFixed(1)}MB,上限 10MB)`);J.push({src:$,filename:z.split(/[\\/]/).pop()||"image",mime:f})}else if(/^https?:\/\//i.test(z))J.push({src:z,filename:z.split("/").pop()||"image",mime:""});else throw new Z(`无法识别的图片项:${z}(需本机绝对路径或 http(s) URL)`);return J}function O({content:Q,images:J,visibleToConnectionsOnly:z}){return`(async () => {
2
+ const IMAGES = ${JSON.stringify(J)};
3
+ const CONTENT = ${JSON.stringify(Q)};
4
+ const VISIBLE_TO_CONNECTIONS_ONLY = ${JSON.stringify(z)};
5
+
6
+ const jsessionRaw = (document.cookie.split('; ').find((c) => c.startsWith('JSESSIONID=')) || '').split('=')[1] || '';
7
+ const csrf = jsessionRaw.replace(/^"|"$/g, '');
8
+ if (!csrf) return { ok: false, stage: 'auth', error: 'JSESSIONID_missing' };
9
+
10
+ const H = {
11
+ 'csrf-token': csrf,
12
+ 'accept': 'application/vnd.linkedin.normalized+json+2.1',
13
+ 'x-restli-protocol-version': '2.0.0',
14
+ 'x-li-lang': 'en_US',
15
+ 'content-type': 'application/json; charset=UTF-8',
16
+ };
17
+
18
+ async function dataUriToBlob(uri) {
19
+ const res = await fetch(uri);
20
+ return res.blob();
21
+ }
22
+ async function fetchBlob(url) {
23
+ const res = await fetch(url, { credentials: 'omit' });
24
+ if (!res.ok) throw new Error('image_fetch_' + res.status);
25
+ return res.blob();
26
+ }
27
+
28
+ const mediaUrns = [];
29
+ for (let i = 0; i < IMAGES.length; i++) {
30
+ const img = IMAGES[i];
31
+ let blob;
32
+ try {
33
+ blob = img.src.startsWith('data:') ? await dataUriToBlob(img.src) : await fetchBlob(img.src);
34
+ } catch (e) {
35
+ return { ok: false, stage: 'image_load', index: i, error: String(e && e.message || e) };
36
+ }
37
+
38
+ // Step 1: 申请上传元数据
39
+ const metaRes = await fetch('/voyager/api/voyagerVideoDashMediaUploadMetadata?action=upload', {
40
+ method: 'POST',
41
+ credentials: 'include',
42
+ headers: H,
43
+ body: JSON.stringify({ mediaUploadType: 'IMAGE_SHARING', fileSize: blob.size, filename: img.filename || ('image_' + i) }),
44
+ });
45
+ if (!metaRes.ok) {
46
+ return { ok: false, stage: 'image_metadata', index: i, httpStatus: metaRes.status, error: (await metaRes.text()).slice(0, 300) };
47
+ }
48
+ const metaJson = await metaRes.json();
49
+ const value = (metaJson && (metaJson.value || (metaJson.data && metaJson.data.value))) || {};
50
+ const uploadUrl = value.singleUploadUrl;
51
+ const urn = value.urn;
52
+ if (!uploadUrl || !urn) {
53
+ return { ok: false, stage: 'image_metadata', index: i, error: 'missing_uploadUrl_or_urn', raw: JSON.stringify(value).slice(0, 300) };
54
+ }
55
+
56
+ // Step 2: PUT 二进制到 CDN(外部 URL,不带 voyager headers/cookie)
57
+ const putRes = await fetch(uploadUrl, {
58
+ method: 'PUT',
59
+ credentials: 'omit',
60
+ headers: { 'content-type': blob.type || img.mime || 'application/octet-stream' },
61
+ body: blob,
62
+ });
63
+ if (putRes.status !== 200 && putRes.status !== 201) {
64
+ return { ok: false, stage: 'image_put', index: i, httpStatus: putRes.status, error: (await putRes.text().catch(() => '')).slice(0, 300) };
65
+ }
66
+ mediaUrns.push(urn);
67
+ }
68
+
69
+ // Step 3: 提交动态(normShares —— 无 queryId,稳,两个独立生产脚本印证仍可用)
70
+ const payload = {
71
+ visibleToConnectionsOnly: VISIBLE_TO_CONNECTIONS_ONLY,
72
+ externalAudienceProviders: [],
73
+ commentaryV2: { text: CONTENT, attributes: [] },
74
+ origin: 'FEED',
75
+ allowedCommentersScope: 'ALL',
76
+ postState: 'PUBLISHED',
77
+ media: mediaUrns.map((u) => ({ category: 'IMAGE', mediaUrn: u, tapTargets: [] })),
78
+ };
79
+ if (mediaUrns.length === 0) delete payload.media;
80
+ if (mediaUrns.length === 0) payload.mediaCategory = 'NONE';
81
+
82
+ const shareRes = await fetch('/voyager/api/contentcreation/normShares', {
83
+ method: 'POST',
84
+ credentials: 'include',
85
+ headers: H,
86
+ body: JSON.stringify(payload),
87
+ });
88
+ const shareText = await shareRes.text();
89
+ if (!shareRes.ok) {
90
+ // 401/403 = 会话失效;其它 = 接口拒绝
91
+ return { ok: false, stage: 'share', httpStatus: shareRes.status, error: shareText.slice(0, 500) };
92
+ }
93
+ let shareJson = null;
94
+ try { shareJson = JSON.parse(shareText); } catch (e) {}
95
+ // 从响应里捞新动态 urn(尽力而为,用于回链)
96
+ const m = shareText.match(/urn:li:(?:activity|share|ugcPost):[0-9]+/);
97
+ return {
98
+ ok: true,
99
+ images: mediaUrns.length,
100
+ activityUrn: m ? m[0] : '',
101
+ updateUrn: (shareJson && (shareJson.updateUrn || (shareJson.data && shareJson.data.urn))) || '',
102
+ };
103
+ })()`}function T(){return`(async () => {
104
+ const jsessionRaw = (document.cookie.split('; ').find((c) => c.startsWith('JSESSIONID=')) || '').split('=')[1] || '';
105
+ const csrf = jsessionRaw.replace(/^"|"$/g, '');
106
+ if (!csrf) return { ok: false, error: 'JSESSIONID_missing' };
107
+ const res = await fetch('/voyager/api/me', { credentials: 'include', headers: { 'csrf-token': csrf, 'accept': 'application/json' } });
108
+ if (res.status === 401 || res.status === 403) return { ok: false, error: 'HTTP_' + res.status };
109
+ if (!res.ok) return { ok: false, error: 'HTTP_' + res.status };
110
+ const d = await res.json();
111
+ const mini = d && d.miniProfile;
112
+ return { ok: Boolean(mini && mini.publicIdentifier), public_id: mini ? String(mini.publicIdentifier || '') : '' };
113
+ })()`}j({site:"linkedin",name:"create",access:"write",description:"Publish a post to your LinkedIn feed via the Voyager API. Supports plain text plus images and visibility scope. Dry-run by default; pass --send true to actually publish.",domain:q,strategy:G.COOKIE,browser:!0,args:[{name:"content",type:"string",required:!0,positional:!0,help:`Post body text (max ${x} chars). Emojis and line breaks allowed.`},{name:"image",type:"string",required:!1,help:`Up to ${B} images: comma-separated local file paths and/or http(s) URLs. Local paths ≤10MB each (jpg/png/gif).`},{name:"visibility",type:"string",required:!1,default:"anyone",help:"Who can see the post: anyone (public, default) or connections (1st-degree only)."},{name:"send",type:"bool",required:!1,default:!1,help:"Actually publish. Default false = dry-run (verifies the signed-in session only; no post is created)."}],columns:["status","content_chars","images","visibility","activity_urn"],func:async(Q,J)=>{if(!Q)throw new Y("Browser session required for linkedin create");const z=String(J?.content??"").replace(/ /g," ").trim();if(!z)throw new Z("content is required");if(z.length>x)throw new Z(`content must be ${x} characters or fewer (got ${z.length})`);const $=L(J?.visibility),W=R(J?.image);if(W.length>B)throw new Z(`最多 ${B} 张图片(收到 ${W.length})`);const f=await X(W),H=J?.send===!0||J?.send==="true";await Q.goto("https://www.linkedin.com/feed/");await Q.wait(3);const U=k(await Q.evaluate(T()));if(!U?.ok)throw new V(q,`LinkedIn create requires an active signed-in LinkedIn browser session (${U?.error||"probe_failed"}).`);if(!H)return[{status:"dry_run",content_chars:z.length,images:f.length,visibility:$.label,activity_urn:"(not published — pass --send true to publish)"}];const F=k(await Q.evaluate(O({content:z,images:f,visibleToConnectionsOnly:$.visibleToConnectionsOnly})));if(!F?.ok){if(F?.stage==="auth"||F?.httpStatus===401||F?.httpStatus===403)throw new V(q,"LinkedIn create: session rejected by Voyager API — re-login in the desktop client.");const K=[`stage=${F?.stage||"unknown"}`,F?.index!=null?`image#${F.index}`:"",F?.httpStatus?`http=${F.httpStatus}`:"",F?.error?`error=${F.error}`:"",F?.raw?`raw=${F.raw}`:""].filter(Boolean).join(" ");throw new Y(`LinkedIn create failed: ${K}`)}return[{status:"published",content_chars:z.length,images:F.images??f.length,visibility:$.label,activity_urn:F.activityUrn||F.updateUrn||""}]}});
@@ -6,13 +6,15 @@ import{cli as e}from"@jackwener/opencli/registry";e({site:"nowcoder",name:"exper
6
6
  if (!d.success) throw new Error(d.msg || 'API failed');
7
7
  return (d.data?.records || []).map((item, i) => ({
8
8
  rank: i + 1,
9
- title: item.contentData?.title || '',
9
+ title: item.contentData?.title || item.momentData?.title || '',
10
10
  author: item.userBrief?.nickname || '',
11
11
  school: item.userBrief?.educationInfo || '',
12
+ content: item.momentData?.content || item.contentData?.content || item.momentData?.newContent || item.contentData?.newContent || '',
12
13
  likes: item.frequencyData?.likeCnt || 0,
13
14
  comments: item.frequencyData?.commentCnt || 0,
14
15
  views: item.frequencyData?.viewCnt || 0,
15
16
  id: item.contentData?.uuid || item.contentData?.id || item.contentId || '',
17
+ momentId: item.contentId || item.momentData?.id || '',
16
18
  }));
17
19
  })()
18
20
  `},{filter:"item.title"},{limit:"${{ args.limit }}"}]});
@@ -1 +1 @@
1
- import{cli as e,Strategy as t}from"@jackwener/opencli/registry";e({site:"nowcoder",name:"recommend",access:"read",description:"Recommended feed",domain:"www.nowcoder.com",strategy:t.PUBLIC,browser:!1,args:[{name:"page",type:"int",default:1,help:"Page number"},{name:"limit",type:"int",default:15,help:"Number of items"}],columns:["rank","title","author","likes","comments","views","id"],pipeline:[{fetch:{url:"https://gw-c.nowcoder.com/api/sparta/home/recommend?page=${{ args.page }}&size=${{ args.limit }}"}},{select:"data.records"},{map:{rank:"${{ index + 1 }}",title:"${{ item.momentData?.title || item.longContentData?.title || item.contentData?.title || '' }}",author:"${{ item.userBrief?.nickname || '' }}",likes:"${{ item.frequencyData?.likeCnt || 0 }}",comments:"${{ item.frequencyData?.commentCnt || 0 }}",views:"${{ item.frequencyData?.viewCnt || 0 }}",id:"${{ item.momentData?.uuid || item.longContentData?.uuid || item.contentData?.uuid || item.contentId || '' }}"}},{filter:"item.title"},{limit:"${{ args.limit }}"}]});
1
+ import{cli as e,Strategy as t}from"@jackwener/opencli/registry";e({site:"nowcoder",name:"recommend",access:"read",description:"Recommended feed",domain:"www.nowcoder.com",strategy:t.PUBLIC,browser:!1,args:[{name:"page",type:"int",default:1,help:"Page number"},{name:"limit",type:"int",default:15,help:"Number of items"}],columns:["rank","title","author","likes","comments","views","id"],pipeline:[{fetch:{url:"https://gw-c.nowcoder.com/api/sparta/home/recommend?page=${{ args.page }}&size=${{ args.limit }}"}},{select:"data.records"},{map:{rank:"${{ index + 1 }}",title:"${{ item.momentData?.title || item.longContentData?.title || item.contentData?.title || '' }}",author:"${{ item.userBrief?.nickname || '' }}",likes:"${{ item.frequencyData?.likeCnt || 0 }}",comments:"${{ item.frequencyData?.commentCnt || 0 }}",views:"${{ item.frequencyData?.viewCnt || 0 }}",content:"${{ item.momentData?.content || item.longContentData?.content || item.contentData?.content || '' }}",id:"${{ item.momentData?.uuid || item.longContentData?.uuid || item.contentData?.uuid || item.contentId || '' }}",momentId:"${{ item.contentId || item.momentData?.id || '' }}"}},{filter:"item.title"},{limit:"${{ args.limit }}"}]});
@@ -1,2 +1,3 @@
1
- import{mkdir as f,readFile as R,writeFile as S}from"node:fs/promises";import{homedir as w}from"node:os";import{dirname as y,join as g}from"node:path";import{pathToFileURL as h}from"node:url";import{InvalidArgumentError as q,Option as l}from"commander";import{AuthRequiredError as O,CliError as H,getErrorMessage as P}from"../errors.js";import{executeCommand as j}from"../execution.js";import{fullName as n,getRegistry as I}from"../registry.js";import{render as b}from"../output.js";const N=1,z=86400000;function K(B,D,G){if(B===void 0||B===null||B==="")return G;const X=Number(B);if(!Number.isInteger(X)||X<=0)throw new q(`${D} must be a positive integer. Received: "${String(B)}"`);return X}function F(B){if(!B||!B.trim())return null;const D=B.split(",").map((G)=>G.trim()).filter(Boolean);return D.length>0?new Set(D):null}function u(){return g(w(),".opencli","auth-refresh.json")}function p(){return{version:N,sites:{}}}async function i(B){try{const D=JSON.parse(await R(B,"utf8"));if(D&&D.version===N&&D.sites&&typeof D.sites==="object")return{version:N,sites:D.sites}}catch(D){if(D.code!=="ENOENT")throw D}return p()}async function c(B,D){await f(y(B),{recursive:!0});await S(B,`${JSON.stringify(D,null,2)}
2
- `,"utf8")}function _(B){if(!B?.last_touched_at)return null;const D=Date.parse(B.last_touched_at);return Number.isFinite(D)?D:null}function a(B,D){const G=_(B);return G!==null&&D.getTime()-G<z}function L(B){const D=_(B);return D===null?"":new Date(D+z).toISOString()}function x(){const B=new Set;return[...I().values()].filter((D)=>{if(B.has(D))return!1;B.add(D);return D.name==="whoami"&&D.access==="read"}).sort((D,G)=>D.site.localeCompare(G.site))}async function U(B){const D=B;if(!D._lazy||!D._modulePath)return B;await import(h(D._modulePath).href);return I().get(n(B))??B}function M(B,D){const G=B.args.some((X)=>X.name==="timeout");return{...B,args:G?B.args:[...B.args,{name:"timeout",type:"int",default:D,help:"Per-site auth command timeout in seconds"}]}}function m(B,D){if(B.browser!==!0||typeof B.authStatus?.quickCheck!=="function")return null;return M({...B,func:B.authStatus.quickCheck,navigateBefore:!1,siteSession:"ephemeral",defaultWindowMode:"background"},D)}function C(B){if(typeof B==="boolean")return B;if(B&&typeof B==="object"&&!Array.isArray(B)){const D=B.logged_in;if(typeof D==="boolean")return D}return null}function T(B){if(B===void 0||B===null)return"";if(typeof B==="string"||typeof B==="number"||typeof B==="boolean")return String(B);return""}function v(B){if(!B||typeof B!=="object"||Array.isArray(B))return"";const D=B,G=/(?:email|phone|real.?name|first.?name|last.?name|cookie|token|session|secret|password|csrf|jwt|bearer|wt2)/i;for(const X of["username","handle","user_id","id","name","nickname","user_type","url"]){if(G.test(X))continue;const J=T(D[X]);if(J)return J}for(const[X,J]of Object.entries(D)){if(X==="site"||X==="logged_in"||G.test(X))continue;const Y=T(J);if(Y)return Y}return""}function V(B){return B instanceof O||typeof B==="object"&&B!==null&&B.code==="AUTH_REQUIRED"}function k(B,D,G){if(V(G))return{site:B,status:"not_logged_in",logged_in:!1,identity:"",checked:D,error:""};const X=G instanceof H?G.code:"",J=P(G);return{site:B,status:"error",logged_in:"",identity:"",checked:D,error:X?`${X}: ${J}`:J}}function d(B,D){if(B.browser!==!0)return null;let G=B.authStatus?.refresh;if(typeof G!=="function"){const X=B.authStatus?.quickCheck;if(typeof X!=="function"||!B.domain)return null;const J=B.domain.startsWith("http://")||B.domain.startsWith("https://")?B.domain:`https://${B.domain}`;G=async(Y,Z,$)=>{await Y.goto(J);await Y.wait(1);if(C(await X(Y,Z,$))!==!0)throw new O(B.domain??B.site,`Auth refresh quickCheck failed for ${B.site}`);return{status:"touched"}}}return M({...B,func:G,navigateBefore:!1,siteSession:"persistent",defaultWindowMode:"background"},D)}function o(B){if(B&&typeof B==="object"&&!Array.isArray(B)){const D=B;if(D.status==="refreshed"||D.refreshed===!0)return"refreshed"}return"touched"}function s(B,D,G){if(V(G))return{site:B,status:"not_logged_in",last_touched_at:D?.last_touched_at??"",next_refresh_at:L(D),error:""};const X=G instanceof H?G.code:"",J=P(G);return{site:B,status:"error",last_touched_at:D?.last_touched_at??"",next_refresh_at:L(D),error:X?`${X}: ${J}`:J}}function E(){return process.env.PUBLISHPORT_SITE_SESSION==="ephemeral"?{siteSession:"ephemeral",keepTab:"false"}:{siteSession:"persistent",keepTab:"true"}}async function r(B,D){try{const G=await U(B);if(G.browser!==!0){const Z=M(G,D.timeoutSeconds),$=await j(Z,{timeout:D.timeoutSeconds},!1,{...D.profile?{profile:D.profile}:{}});return{site:B.site,status:"logged_in",logged_in:!0,identity:v($),checked:"quick",error:""}}const X=m(G,D.timeoutSeconds);if(!X)return{site:B.site,status:"unknown",logged_in:"",identity:"",checked:"skipped",error:"quickCheck not implemented; use --full to run whoami"};const J=await j(X,{timeout:D.timeoutSeconds},!1,{...E(),windowMode:"background",...D.profile?{profile:D.profile}:{}}),Y=C(J);if(Y===!0)return{site:B.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};if(Y===!1)return{site:B.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"quick",error:""};return{site:B.site,status:"unknown",logged_in:"",identity:"",checked:"quick",error:"quickCheck returned no boolean logged_in signal"}}catch(G){return k(B.site,"quick",G)}}async function t(B,D){try{const G=await U(B),X=M(G,D.timeoutSeconds),J=await j(X,{timeout:D.timeoutSeconds},!1,{...E(),windowMode:"background",...D.profile?{profile:D.profile}:{}});return{site:B.site,status:"logged_in",logged_in:!0,identity:v(J),checked:"full",error:""}}catch(G){return k(B.site,"full",G)}}function e(B,D){return D?`${B}@${D}`:B}async function BB(B,D){const G=e(B.site,D.profile),X=D.state.sites[G];if(!D.force&&a(X,D.now))return{site:B.site,status:"skipped",last_touched_at:X?.last_touched_at??"",next_refresh_at:L(X),error:""};const J=D.now.toISOString();try{const Y=await U(B),Z=d(Y,D.timeoutSeconds);if(!Z){D.state.sites[G]={...X,last_attempt_at:J,last_status:"unsupported"};return{site:B.site,status:"unsupported",last_touched_at:X?.last_touched_at??"",next_refresh_at:L(X),error:"refresh probe is not available for this site"}}const $=await j(Z,{timeout:D.timeoutSeconds},!1,{siteSession:"persistent",keepTab:"true",windowMode:"background",...D.profile?{profile:D.profile}:{}}),Q=o($);D.state.sites[G]={...X,last_attempt_at:J,last_touched_at:J,last_status:Q};return{site:B.site,status:Q,last_touched_at:J,next_refresh_at:new Date(D.now.getTime()+z).toISOString(),error:""}}catch(Y){const Z=V(Y)?"not_logged_in":"error";D.state.sites[G]={...X,last_attempt_at:J,last_status:Z};return s(B.site,X,Y)}}async function A(B,D,G){const X=Array(B.length);let J=0;const Y=Array.from({length:Math.min(D,B.length)},async()=>{while(J<B.length){const Z=J++;X[Z]=await G(B[Z])}});await Promise.all(Y);return X}export async function collectAuthStatus(B){const D=F(B.sites),G=B.full?"full":"quick",X=K(B.concurrency,"--concurrency",G==="full"?3:8),J=K(B.timeout,"--timeout",G==="full"?20:8),Y=String(B.only??"all");if(!["all","logged-in","not-logged-in","unknown","error"].includes(Y))throw new q("--only must be one of: all, logged-in, not-logged-in, unknown, error");const Z=x().filter((W)=>!D||D.has(W.site)),$=await A(Z,X,(W)=>G==="full"?t(W,{timeoutSeconds:J,profile:B.profile}):r(W,{timeoutSeconds:J,profile:B.profile})),Q=Y.replace(/-/g,"_");return Q==="all"?$:$.filter((W)=>W.status===Q)}export async function collectAuthRefresh(B){const D=F(B.sites),G=K(B.concurrency,"--concurrency",3),X=K(B.timeout,"--timeout",20),J=B.statePath??u(),Y=B.now??new Date,Z=await i(J),$=x().filter((W)=>!D||D.has(W.site)),Q=await A($,G,(W)=>BB(W,{timeoutSeconds:X,profile:B.profile,now:Y,state:Z,force:B.all===!0}));await c(J,Z);return Q}export function registerAuthCommands(B){const D=B.command("auth").description("Inspect website login status"),G=D.command("status").description("Show login status for sites with auth adapters").option("--site <sites>","Comma-separated site names to check, e.g. github,chatgpt").option("--full","Run full per-site whoami probes instead of quick no-navigation checks",!1).option("--concurrency <n>","Maximum sites to check at once").option("--timeout <seconds>","Per-site timeout in seconds").addOption(new l("--only <status>","Filter rows by status").choices(["all","logged-in","not-logged-in","unknown","error"]).default("all")).option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(J)=>{const Y=typeof G.optsWithGlobals==="function"?G.optsWithGlobals():{},Z=await collectAuthStatus({sites:J.site,full:J.full===!0,concurrency:J.concurrency,timeout:J.timeout,only:J.only,profile:typeof Y.profile==="string"&&Y.profile.trim()?Y.profile.trim():void 0}),$=typeof J.format==="string"?J.format:"table";b(Z,{fmt:$,fmtExplicit:G.getOptionValueSource("format")==="cli",columns:["site","status","identity","checked","error"],title:"opencli/auth status",source:J.full?"full whoami probe":"quick auth check"})}),X=D.command("refresh").description("Touch logged-in site sessions to keep browser auth fresh").option("--site <sites>","Comma-separated site names to refresh, e.g. github,claude").option("--all","Ignore the 24h refresh throttle and force every selected site",!1).option("--concurrency <n>","Maximum sites to refresh at once").option("--timeout <seconds>","Per-site timeout in seconds").option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(J)=>{const Y=typeof X.optsWithGlobals==="function"?X.optsWithGlobals():{},Z=await collectAuthRefresh({sites:J.site,all:J.all===!0,concurrency:J.concurrency,timeout:J.timeout,profile:typeof Y.profile==="string"&&Y.profile.trim()?Y.profile.trim():void 0}),$=typeof J.format==="string"?J.format:"table";b(Z,{fmt:$,fmtExplicit:X.getOptionValueSource("format")==="cli",columns:["site","status","last_touched_at","next_refresh_at","error"],title:"opencli/auth refresh",source:J.all?"forced persistent touch":"persistent touch with 24h throttle"})});return D}
1
+ import{mkdir as q,readFile as N,writeFile as H}from"node:fs/promises";import{homedir as P}from"node:os";import{dirname as b,join as _}from"node:path";import{pathToFileURL as u}from"node:url";import{InvalidArgumentError as I,Option as l}from"commander";import{AuthRequiredError as F,CliError as x,getErrorMessage as v}from"../errors.js";import{executeCommand as Q}from"../execution.js";import{fullName as n,getRegistry as T}from"../registry.js";import{render as C}from"../output.js";const z=1,U=86400000;function j(B,D,G){if(B===void 0||B===null||B==="")return G;const X=Number(B);if(!Number.isInteger(X)||X<=0)throw new I(`${D} must be a positive integer. Received: "${String(B)}"`);return X}function E(B){if(!B||!B.trim())return null;const D=B.split(",").map((G)=>G.trim()).filter(Boolean);return D.length>0?new Set(D):null}function p(){return _(P(),".opencli","auth-refresh.json")}function i(){return{version:z,sites:{}}}async function m(B){try{const D=JSON.parse(await N(B,"utf8"));if(D&&D.version===z&&D.sites&&typeof D.sites==="object")return{version:z,sites:D.sites}}catch(D){if(D.code!=="ENOENT")throw D}return i()}async function c(B,D){await q(b(B),{recursive:!0});await H(B,`${JSON.stringify(D,null,2)}
2
+ `,"utf8")}function k(B){if(!B?.last_touched_at)return null;const D=Date.parse(B.last_touched_at);return Number.isFinite(D)?D:null}function a(B,D){const G=k(B);return G!==null&&D.getTime()-G<U}function L(B){const D=k(B);return D===null?"":new Date(D+U).toISOString()}function A(){const B=new Set;return[...T().values()].filter((D)=>{if(B.has(D))return!1;B.add(D);return D.name==="whoami"&&D.access==="read"}).sort((D,G)=>D.site.localeCompare(G.site))}async function V(B){const D=B;if(!D._lazy||!D._modulePath)return B;await import(u(D._modulePath).href);return T().get(n(B))??B}function M(B,D){const G=B.args.some((X)=>X.name==="timeout");return{...B,args:G?B.args:[...B.args,{name:"timeout",type:"int",default:D,help:"Per-site auth command timeout in seconds"}]}}function d(B,D){if(B.browser!==!0||typeof B.authStatus?.quickCheck!=="function")return null;return M({...B,func:B.authStatus.quickCheck,navigateBefore:!1,siteSession:"ephemeral",defaultWindowMode:"background"},D)}function R(B){if(typeof B==="boolean")return B;if(B&&typeof B==="object"&&!Array.isArray(B)){const D=B.logged_in;if(typeof D==="boolean")return D}return null}function S(B){if(B===void 0||B===null)return"";if(typeof B==="string"||typeof B==="number"||typeof B==="boolean")return String(B);return""}function w(B){if(!B||typeof B!=="object"||Array.isArray(B))return"";const D=B,G=/(?:email|phone|real.?name|first.?name|last.?name|cookie|token|session|secret|password|csrf|jwt|bearer|wt2)/i;for(const X of["username","handle","user_id","id","name","nickname","user_type","url"]){if(G.test(X))continue;const J=S(D[X]);if(J)return J}for(const[X,J]of Object.entries(D)){if(X==="site"||X==="logged_in"||G.test(X))continue;const Y=S(J);if(Y)return Y}return""}function O(B){return B instanceof F||typeof B==="object"&&B!==null&&B.code==="AUTH_REQUIRED"}function f(B,D,G){if(O(G))return{site:B,status:"not_logged_in",logged_in:!1,identity:"",checked:D,error:""};const X=G instanceof x?G.code:"",J=v(G);return{site:B,status:"error",logged_in:"",identity:"",checked:D,error:X?`${X}: ${J}`:J}}function o(B,D){if(B.browser!==!0)return null;let G=B.authStatus?.refresh;if(typeof G!=="function"){const X=B.authStatus?.quickCheck;if(typeof X!=="function"||!B.domain)return null;const J=B.domain.startsWith("http://")||B.domain.startsWith("https://")?B.domain:`https://${B.domain}`;G=async(Y,Z,$)=>{await Y.goto(J);await Y.wait(1);if(R(await X(Y,Z,$))!==!0)throw new F(B.domain??B.site,`Auth refresh quickCheck failed for ${B.site}`);return{status:"touched"}}}return M({...B,func:G,navigateBefore:!1,siteSession:"persistent",defaultWindowMode:"background"},D)}function s(B){if(B&&typeof B==="object"&&!Array.isArray(B)){const D=B;if(D.status==="refreshed"||D.refreshed===!0)return"refreshed"}return"touched"}function r(B,D,G){if(O(G))return{site:B,status:"not_logged_in",last_touched_at:D?.last_touched_at??"",next_refresh_at:L(D),error:""};const X=G instanceof x?G.code:"",J=v(G);return{site:B,status:"error",last_touched_at:D?.last_touched_at??"",next_refresh_at:L(D),error:X?`${X}: ${J}`:J}}function y(){return process.env.PUBLISHPORT_SITE_SESSION==="ephemeral"?{siteSession:"ephemeral",keepTab:"false"}:{siteSession:"persistent",keepTab:"true"}}function g(B){return _(P(),".opencli","sites",B,"quickcheck-cache.json")}async function t(B,D,G){try{const J=JSON.parse(await N(g(B),"utf8"))?.[G??"default"];if(!J||J.logged_in!==!0)return!1;const Y=Date.now()-Date.parse(J.checked_at);return Number.isFinite(Y)&&Y>=0&&Y<D*1000}catch{return!1}}async function e(B,D,G){const X=g(B),J=G??"default";let Y={};try{Y=JSON.parse(await N(X,"utf8"))}catch{}if(D)Y[J]={logged_in:!0,checked_at:new Date().toISOString()};else delete Y[J];try{await q(b(X),{recursive:!0});await H(X,`${JSON.stringify(Y,null,2)}
3
+ `,"utf8")}catch{}}async function BB(B,D){try{const G=await V(B);if(G.browser!==!0){const $=M(G,D.timeoutSeconds),W=await Q($,{timeout:D.timeoutSeconds},!1,{...D.profile?{profile:D.profile}:{}});return{site:B.site,status:"logged_in",logged_in:!0,identity:w(W),checked:"quick",error:""}}const X=G.authStatus?.quickCheckCacheTtlSeconds;if(typeof X==="number"&&X>0&&await t(B.site,X,D.profile))return{site:B.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};const J=d(G,D.timeoutSeconds);if(!J)return{site:B.site,status:"unknown",logged_in:"",identity:"",checked:"skipped",error:"quickCheck not implemented; use --full to run whoami"};const Y=await Q(J,{timeout:D.timeoutSeconds},!1,{...y(),windowMode:"background",...D.profile?{profile:D.profile}:{}}),Z=R(Y);if(typeof X==="number"&&X>0&&typeof Z==="boolean")await e(B.site,Z,D.profile);if(Z===!0)return{site:B.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};if(Z===!1)return{site:B.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"quick",error:""};return{site:B.site,status:"unknown",logged_in:"",identity:"",checked:"quick",error:"quickCheck returned no boolean logged_in signal"}}catch(G){return f(B.site,"quick",G)}}async function DB(B,D){try{const G=await V(B),X=M(G,D.timeoutSeconds),J=await Q(X,{timeout:D.timeoutSeconds},!1,{...y(),windowMode:"background",...D.profile?{profile:D.profile}:{}});return{site:B.site,status:"logged_in",logged_in:!0,identity:w(J),checked:"full",error:""}}catch(G){return f(B.site,"full",G)}}function GB(B,D){return D?`${B}@${D}`:B}async function JB(B,D){const G=GB(B.site,D.profile),X=D.state.sites[G];if(!D.force&&a(X,D.now))return{site:B.site,status:"skipped",last_touched_at:X?.last_touched_at??"",next_refresh_at:L(X),error:""};const J=D.now.toISOString();try{const Y=await V(B),Z=o(Y,D.timeoutSeconds);if(!Z){D.state.sites[G]={...X,last_attempt_at:J,last_status:"unsupported"};return{site:B.site,status:"unsupported",last_touched_at:X?.last_touched_at??"",next_refresh_at:L(X),error:"refresh probe is not available for this site"}}const $=await Q(Z,{timeout:D.timeoutSeconds},!1,{siteSession:"persistent",keepTab:"true",windowMode:"background",...D.profile?{profile:D.profile}:{}}),W=s($);D.state.sites[G]={...X,last_attempt_at:J,last_touched_at:J,last_status:W};return{site:B.site,status:W,last_touched_at:J,next_refresh_at:new Date(D.now.getTime()+U).toISOString(),error:""}}catch(Y){const Z=O(Y)?"not_logged_in":"error";D.state.sites[G]={...X,last_attempt_at:J,last_status:Z};return r(B.site,X,Y)}}async function h(B,D,G){const X=Array(B.length);let J=0;const Y=Array.from({length:Math.min(D,B.length)},async()=>{while(J<B.length){const Z=J++;X[Z]=await G(B[Z])}});await Promise.all(Y);return X}export async function collectAuthStatus(B){const D=E(B.sites),G=B.full?"full":"quick",X=j(B.concurrency,"--concurrency",G==="full"?3:8),J=j(B.timeout,"--timeout",G==="full"?20:8),Y=String(B.only??"all");if(!["all","logged-in","not-logged-in","unknown","error"].includes(Y))throw new I("--only must be one of: all, logged-in, not-logged-in, unknown, error");const Z=A().filter((K)=>!D||D.has(K.site)),$=await h(Z,X,(K)=>G==="full"?DB(K,{timeoutSeconds:J,profile:B.profile}):BB(K,{timeoutSeconds:J,profile:B.profile})),W=Y.replace(/-/g,"_");return W==="all"?$:$.filter((K)=>K.status===W)}export async function collectAuthRefresh(B){const D=E(B.sites),G=j(B.concurrency,"--concurrency",3),X=j(B.timeout,"--timeout",20),J=B.statePath??p(),Y=B.now??new Date,Z=await m(J),$=A().filter((K)=>!D||D.has(K.site)),W=await h($,G,(K)=>JB(K,{timeoutSeconds:X,profile:B.profile,now:Y,state:Z,force:B.all===!0}));await c(J,Z);return W}export function registerAuthCommands(B){const D=B.command("auth").description("Inspect website login status"),G=D.command("status").description("Show login status for sites with auth adapters").option("--site <sites>","Comma-separated site names to check, e.g. github,chatgpt").option("--full","Run full per-site whoami probes instead of quick no-navigation checks",!1).option("--concurrency <n>","Maximum sites to check at once").option("--timeout <seconds>","Per-site timeout in seconds").addOption(new l("--only <status>","Filter rows by status").choices(["all","logged-in","not-logged-in","unknown","error"]).default("all")).option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(J)=>{const Y=typeof G.optsWithGlobals==="function"?G.optsWithGlobals():{},Z=await collectAuthStatus({sites:J.site,full:J.full===!0,concurrency:J.concurrency,timeout:J.timeout,only:J.only,profile:typeof Y.profile==="string"&&Y.profile.trim()?Y.profile.trim():void 0}),$=typeof J.format==="string"?J.format:"table";C(Z,{fmt:$,fmtExplicit:G.getOptionValueSource("format")==="cli",columns:["site","status","identity","checked","error"],title:"opencli/auth status",source:J.full?"full whoami probe":"quick auth check"})}),X=D.command("refresh").description("Touch logged-in site sessions to keep browser auth fresh").option("--site <sites>","Comma-separated site names to refresh, e.g. github,claude").option("--all","Ignore the 24h refresh throttle and force every selected site",!1).option("--concurrency <n>","Maximum sites to refresh at once").option("--timeout <seconds>","Per-site timeout in seconds").option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(J)=>{const Y=typeof X.optsWithGlobals==="function"?X.optsWithGlobals():{},Z=await collectAuthRefresh({sites:J.site,all:J.all===!0,concurrency:J.concurrency,timeout:J.timeout,profile:typeof Y.profile==="string"&&Y.profile.trim()?Y.profile.trim():void 0}),$=typeof J.format==="string"?J.format:"table";C(Z,{fmt:$,fmtExplicit:X.getOptionValueSource("format")==="cli",columns:["site","status","last_touched_at","next_refresh_at","error"],title:"opencli/auth refresh",source:J.all?"forced persistent touch":"persistent touch with 24h throttle"})});return D}
@@ -3,6 +3,11 @@
3
3
  */
4
4
  /** Default daemon port for HTTP/WebSocket communication with browser extension */
5
5
  export declare const DEFAULT_DAEMON_PORT = 19825;
6
+ /**
7
+ * PublishPort 自有插件的专用端口。daemon 同时监听两个端口:
8
+ * 19825 兼容官方 OpenCLI 插件(存量用户),19826 供 PublishPort 品牌插件优先连接。
9
+ */
10
+ export declare const PUBLISHPORT_DAEMON_PORT = 19826;
6
11
  export declare function unsupportedDaemonPortEnvMessage(value?: string): string;
7
12
  /** URL query params that are volatile/ephemeral and should be stripped from patterns */
8
13
  export declare const VOLATILE_PARAMS: Set<string>;
@@ -1 +1 @@
1
- export const DEFAULT_DAEMON_PORT=19825;export function unsupportedDaemonPortEnvMessage(b){return`OPENCLI_DAEMON_PORT is no longer supported${b?` (received ${b})`:""}. The OpenCLI Chrome extension can only connect to localhost:${DEFAULT_DAEMON_PORT}. Unset OPENCLI_DAEMON_PORT and rerun opencli.`}export const VOLATILE_PARAMS=new Set(["w_rid","wts","_","callback","timestamp","t","nonce","sign"]),SEARCH_PARAMS=new Set(["q","query","keyword","search","wd","kw","search_query","w"]),PAGINATION_PARAMS=new Set(["page","pn","offset","cursor","next","page_num"]),LIMIT_PARAMS=new Set(["limit","count","size","per_page","page_size","ps","num"]),FIELD_ROLES={title:["title","name","text","content","desc","description","headline","subject"],url:["url","uri","link","href","permalink","jump_url","web_url","share_url"],author:["author","username","user_name","nickname","nick","owner","creator","up_name","uname"],score:["score","hot","heat","likes","like_count","view_count","views","play","favorite_count","reply_count"],time:["time","created_at","publish_time","pub_time","date","ctime","mtime","pubdate","created"],id:["id","aid","bvid","mid","uid","oid","note_id","item_id"],cover:["cover","pic","image","thumbnail","poster","avatar"],category:["category","tag","type","tname","channel","section"]};
1
+ export const DEFAULT_DAEMON_PORT=19825,PUBLISHPORT_DAEMON_PORT=19826;export function unsupportedDaemonPortEnvMessage(b){return`OPENCLI_DAEMON_PORT is no longer supported${b?` (received ${b})`:""}. The OpenCLI Chrome extension can only connect to localhost:${DEFAULT_DAEMON_PORT}. Unset OPENCLI_DAEMON_PORT and rerun opencli.`}export const VOLATILE_PARAMS=new Set(["w_rid","wts","_","callback","timestamp","t","nonce","sign"]),SEARCH_PARAMS=new Set(["q","query","keyword","search","wd","kw","search_query","w"]),PAGINATION_PARAMS=new Set(["page","pn","offset","cursor","next","page_num"]),LIMIT_PARAMS=new Set(["limit","count","size","per_page","page_size","ps","num"]),FIELD_ROLES={title:["title","name","text","content","desc","description","headline","subject"],url:["url","uri","link","href","permalink","jump_url","web_url","share_url"],author:["author","username","user_name","nickname","nick","owner","creator","up_name","uname"],score:["score","hot","heat","likes","like_count","view_count","views","play","favorite_count","reply_count"],time:["time","created_at","publish_time","pub_time","date","ctime","mtime","pubdate","created"],id:["id","aid","bvid","mid","uid","oid","note_id","item_id"],cover:["cover","pic","image","thumbnail","poster","avatar"],category:["category","tag","type","tname","channel","section"]};
@@ -1 +1 @@
1
- import{createServer as y}from"node:http";import{WebSocketServer as I,WebSocket as P}from"ws";import{DEFAULT_DAEMON_PORT as w,unsupportedDaemonPortEnvMessage as x}from"./constants.js";import{EXIT_CODES as O}from"./errors.js";import{log as V}from"./logger.js";import{PKG_VERSION as f}from"./version.js";import{DEFAULT_CONTEXT_ID as p}from"./browser/profile.js";import{recordExtensionVersion as m}from"./update-check.js";import{buildCommandDispatchFailure as g,buildExtensionDisconnectFailure as u,getResponseCorsHeaders as l}from"./daemon-utils.js";import*as L from"./browser/trace.js";const _=w;if(process.env.OPENCLI_DAEMON_PORT){V.error(x(process.env.OPENCLI_DAEMON_PORT));process.exit(O.USAGE_ERROR)}const W=new Map,N=new Map;let R=0;const c=200,B=[];class D extends Error{errorCode;errorHint;status;constructor(J,K,z,G=400){super(J);this.errorCode=K;this.errorHint=z;this.status=G;this.name="DaemonCommandFailure"}}function d(J){B.push(J);if(B.length>c)B.shift()}function j(){return[...W.values()].filter((J)=>J.ws.readyState===P.OPEN)}function C(J){const K=typeof J==="string"&&J.trim()?J.trim():void 0;if(K){const G=W.get(K);if(G?.ws.readyState===P.OPEN)return{connection:G};return{errorCode:"profile_disconnected",error:`Browser profile "${K}" is not connected.`,errorHint:"Open that Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>."}}const z=j();if(z.length===1)return{connection:z[0]};if(z.length>1)return{errorCode:"profile_required",error:"Multiple Browser Bridge profiles are connected; choose one with --profile.",errorHint:"Run opencli profile list, then use opencli --profile <name> ... or opencli profile use <name>."};return{errorCode:"extension_not_connected",error:"Extension not connected. Please install the opencli Browser Bridge extension."}}function a(J,K){const z=typeof K==="string"&&K.trim()?K.trim():p,G=W.get(z);if(G&&G.ws!==J)G.ws.close();const $=[...W.entries()].find(([,A])=>A.ws===J);if($&&$[0]!==z)W.delete($[0]);const Q=W.get(z),Z={contextId:z,ws:J,extensionVersion:Q?.ws===J?Q.extensionVersion:null,extensionCompatRange:Q?.ws===J?Q.extensionCompatRange:null,lastSeenAt:Date.now()};W.set(z,Z);return Z}function b(J){for(const[K,z]of W.entries()){if(z.ws!==J)continue;W.delete(K);L.traceExtDisconnect(K);for(const[G,$]of N){if($.contextId!==K)continue;clearTimeout($.timer);const Q=u({contextId:K,action:$.action,dispatched:$.dispatched});if(Q.countAsCommandResultUnknown){R++;V.warn(`[daemon] Command result unknown after extension disconnect (id=${G}, action=${$.action}, context=${K})`)}L.traceResult(G,!1,Date.now()-$.dispatchTs,Q.errorCode);$.reject(new D(Q.message,Q.errorCode,Q.errorHint,Q.status));N.delete(G)}}}const q=33554432;class E extends Error{status=413;constructor(){super(`请求体超过 ${q/1024/1024} MB 上限(正文内联图片过多/过大?可减少单次发布的图片体积)`);this.name="BodyTooLargeError"}}function i(J){return new Promise((K,z)=>{const G=[];let $=0,Q=!1;J.on("data",(Z)=>{if(Q)return;$+=Z.length;if($>q){Q=!0;G.length=0;z(new E);return}G.push(Z)});J.on("end",()=>{if(!Q)K(Buffer.concat(G).toString("utf-8"))});J.on("error",(Z)=>{if(!Q)z(Z)})})}function Y(J,K,z,G){J.writeHead(K,{"Content-Type":"application/json",...G});J.end(JSON.stringify(z))}async function t(J,K){const z=J.headers.origin;if(z&&!z.startsWith("chrome-extension://")){Y(K,403,{ok:!1,error:"Forbidden: cross-origin request blocked"});return}if(J.method==="OPTIONS"){K.writeHead(204);K.end();return}const G=J.url??"/",$=G.split("?")[0];if(J.method==="GET"&&$==="/ping"){Y(K,200,{ok:!0},l($,z));return}if(!J.headers["x-opencli"]){Y(K,403,{ok:!1,error:"Forbidden: missing X-OpenCLI header"});return}if(J.method==="GET"&&$==="/status"){const Q=process.uptime(),Z=process.memoryUsage(),k=new URL(G,`http://localhost:${_}`).searchParams.get("contextId")?.trim()||void 0,H=C(k),S=j().map((U)=>({contextId:U.contextId,extensionConnected:!0,extensionVersion:U.extensionVersion??void 0,extensionCompatRange:U.extensionCompatRange??void 0,pending:[...N.values()].filter((X)=>X.contextId===U.contextId).length,lastSeenAt:U.lastSeenAt}));Y(K,200,{ok:!0,pid:process.pid,uptime:Q,daemonVersion:f,extensionConnected:!!H.connection,extensionVersion:H.connection?.extensionVersion??void 0,extensionCompatRange:H.connection?.extensionCompatRange??void 0,contextId:H.connection?.contextId??k,profileRequired:H.errorCode==="profile_required",profileDisconnected:H.errorCode==="profile_disconnected",profiles:S,pending:N.size,commandResultUnknown:R,memoryMB:Math.round(Z.rss/1024/1024*10)/10,port:_});return}if(J.method==="GET"&&$==="/logs"){const Z=new URL(G,`http://localhost:${_}`).searchParams.get("level"),A=Z?B.filter((k)=>k.level===Z):B;Y(K,200,{ok:!0,logs:A});return}if(J.method==="DELETE"&&$==="/logs"){B.length=0;Y(K,200,{ok:!0});return}if(J.method==="POST"&&$==="/shutdown"){Y(K,200,{ok:!0,message:"Shutting down"});setTimeout(()=>v(),100);return}if(J.method==="POST"&&G==="/command"){try{const Q=JSON.parse(await i(J));if(!Q.id){Y(K,400,{ok:!1,error:"Missing command id"});return}const Z=C(typeof Q.contextId==="string"?Q.contextId:void 0);if(!Z.connection){Y(K,Z.errorCode==="profile_required"?409:503,{id:Q.id,ok:!1,errorCode:Z.errorCode,error:Z.error,...Z.errorHint?{errorHint:Z.errorHint}:{}});return}const A=typeof Q.timeout==="number"&&Q.timeout>0?Q.timeout*1000:120000;if(N.has(Q.id)){Y(K,409,{id:Q.id,ok:!1,error:"Duplicate command id already pending; retry"});return}const k=await new Promise((H,S)=>{const U=setTimeout(()=>{N.delete(Q.id);L.traceTimeout(Q.id,A,typeof Q.action==="string"?Q.action:void 0);S(Error(`Command timeout (${A/1000}s)`))},A),X={contextId:Z.connection.contextId,action:typeof Q.action==="string"?Q.action:"unknown",dispatched:!1,dispatchTs:Date.now(),resolve:H,reject:S,timer:U};N.set(Q.id,X);const h=(M)=>{if(N.get(Q.id)!==X)return;const F=g(X.contextId);clearTimeout(U);N.delete(Q.id);S(new D(F.message,F.errorCode,F.errorHint,F.status));V.warn(`[daemon] Failed to dispatch command ${Q.id}: ${M instanceof Error?M.message:String(M)}`)};try{Z.connection.ws.send(JSON.stringify(Q),(M)=>{if(M&&!X.dispatched)h(M)});X.dispatched=!0;L.traceDispatch(Q)}catch(M){h(M)}});Y(K,200,k)}catch(Q){const Z=Q instanceof D?Q:null;if(Q instanceof E){Y(K,413,{ok:!1,error:Q.message,errorCode:"body_too_large"});return}Y(K,Z?.status??(Q instanceof Error&&Q.message.includes("timeout")?408:400),{ok:!1,error:Q instanceof Error?Q.message:"Invalid request",...Z?.errorCode?{errorCode:Z.errorCode}:{},...Z?.errorHint?{errorHint:Z.errorHint}:{}})}return}Y(K,404,{error:"Not found"})}const T=y((J,K)=>{t(J,K).catch(()=>{K.writeHead(500);K.end()})}),s=new I({server:T,path:"/ext",verifyClient:({req:J})=>{const K=J.headers.origin;return!K||K.startsWith("chrome-extension://")}});s.on("connection",(J)=>{V.info("[daemon] Extension connected");let K=0;const z=setInterval(()=>{if(J.readyState!==P.OPEN){clearInterval(z);return}if(K>=2){V.warn("[daemon] Extension heartbeat lost, closing connection");clearInterval(z);J.terminate();return}K++;J.ping()},15000);J.on("pong",()=>{K=0});J.on("message",(G)=>{try{const $=JSON.parse(G.toString());if($.type==="hello"){const Z=a(J,$.contextId);Z.extensionVersion=typeof $.version==="string"?$.version:null;Z.extensionCompatRange=typeof $.compatRange==="string"?$.compatRange:null;Z.lastSeenAt=Date.now();if(Z.extensionVersion)m(Z.extensionVersion);V.info(`[daemon] Extension profile connected: ${Z.contextId}`);L.traceExtConnect(Z.contextId,Z.extensionVersion);return}if($.type==="log"){if($.level==="error")V.error(`[ext] ${$.msg}`);else if($.level==="warn")V.warn(`[ext] ${$.msg}`);else V.info(`[ext] ${$.msg}`);d({level:$.level,msg:$.msg,ts:$.ts??Date.now()});L.traceExtLog($.level,$.msg);return}const Q=N.get($.id);if(Q){clearTimeout(Q.timer);N.delete($.id);L.traceResult($.id,!!$.ok,Date.now()-Q.dispatchTs,typeof $.errorCode==="string"?$.errorCode:void 0,typeof $.page==="string"?$.page:void 0);Q.resolve($)}}catch($){const Q=G.toString().slice(0,200);V.warn(`[daemon] Ignoring malformed WS message from extension: ${$ instanceof Error?$.message:String($)} (first 200 chars: ${JSON.stringify(Q)})`)}});J.on("close",()=>{V.info("[daemon] Extension disconnected");clearInterval(z);b(J)});J.on("error",()=>{clearInterval(z);b(J)})});T.listen(_,"127.0.0.1",()=>{V.info(`[daemon] Listening on http://127.0.0.1:${_}`)});T.on("error",(J)=>{if(J.code==="EADDRINUSE"){V.error(`[daemon] Port ${_} already in use — another daemon is likely running. Exiting.`);process.exit(O.SERVICE_UNAVAIL)}V.error(`[daemon] Server error: ${J.message}`);process.exit(O.GENERIC_ERROR)});function v(){for(const[,J]of N){clearTimeout(J.timer);J.reject(Error("Daemon shutting down"))}N.clear();for(const J of W.values())J.ws.close();T.close();process.exit(O.SUCCESS)}process.on("SIGTERM",v);process.on("SIGINT",v);
1
+ import{createServer as P}from"node:http";import{WebSocketServer as g,WebSocket as E}from"ws";import{DEFAULT_DAEMON_PORT as u,PUBLISHPORT_DAEMON_PORT as p,unsupportedDaemonPortEnvMessage as l}from"./constants.js";import{EXIT_CODES as S}from"./errors.js";import{log as V}from"./logger.js";import{PKG_VERSION as c}from"./version.js";import{DEFAULT_CONTEXT_ID as d}from"./browser/profile.js";import{recordExtensionVersion as a}from"./update-check.js";import{buildCommandDispatchFailure as i,buildExtensionDisconnectFailure as t,getResponseCorsHeaders as n}from"./daemon-utils.js";import*as L from"./browser/trace.js";const k=u,h=p;if(process.env.OPENCLI_DAEMON_PORT){V.error(l(process.env.OPENCLI_DAEMON_PORT));process.exit(S.USAGE_ERROR)}const W=new Map,N=new Map;let y=0;const s=200,F=[];class v extends Error{errorCode;errorHint;status;constructor(J,K,z,G=400){super(J);this.errorCode=K;this.errorHint=z;this.status=G;this.name="DaemonCommandFailure"}}function o(J){F.push(J);if(F.length>s)F.shift()}function I(){return[...W.values()].filter((J)=>J.ws.readyState===E.OPEN)}function q(J){const K=typeof J==="string"&&J.trim()?J.trim():void 0;if(K){const G=W.get(K);if(G?.ws.readyState===E.OPEN)return{connection:G};return{errorCode:"profile_disconnected",error:`Browser profile "${K}" is not connected.`,errorHint:"Open that Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>."}}const z=I();if(z.length===1)return{connection:z[0]};if(z.length>1)return{errorCode:"profile_required",error:"Multiple Browser Bridge profiles are connected; choose one with --profile.",errorHint:"Run opencli profile list, then use opencli --profile <name> ... or opencli profile use <name>."};return{errorCode:"extension_not_connected",error:"Extension not connected. Please install the opencli Browser Bridge extension."}}function r(J,K){const z=typeof K==="string"&&K.trim()?K.trim():d,G=W.get(z);if(G&&G.ws!==J)G.ws.close();const $=[...W.entries()].find(([,A])=>A.ws===J);if($&&$[0]!==z)W.delete($[0]);const Q=W.get(z),Z={contextId:z,ws:J,extensionVersion:Q?.ws===J?Q.extensionVersion:null,extensionCompatRange:Q?.ws===J?Q.extensionCompatRange:null,lastSeenAt:Date.now()};W.set(z,Z);return Z}function w(J){for(const[K,z]of W.entries()){if(z.ws!==J)continue;W.delete(K);L.traceExtDisconnect(K);for(const[G,$]of N){if($.contextId!==K)continue;clearTimeout($.timer);const Q=t({contextId:K,action:$.action,dispatched:$.dispatched});if(Q.countAsCommandResultUnknown){y++;V.warn(`[daemon] Command result unknown after extension disconnect (id=${G}, action=${$.action}, context=${K})`)}L.traceResult(G,!1,Date.now()-$.dispatchTs,Q.errorCode);$.reject(new v(Q.message,Q.errorCode,Q.errorHint,Q.status));N.delete(G)}}}const x=33554432;class R extends Error{status=413;constructor(){super(`请求体超过 ${x/1024/1024} MB 上限(正文内联图片过多/过大?可减少单次发布的图片体积)`);this.name="BodyTooLargeError"}}function e(J){return new Promise((K,z)=>{const G=[];let $=0,Q=!1;J.on("data",(Z)=>{if(Q)return;$+=Z.length;if($>x){Q=!0;G.length=0;z(new R);return}G.push(Z)});J.on("end",()=>{if(!Q)K(Buffer.concat(G).toString("utf-8"))});J.on("error",(Z)=>{if(!Q)z(Z)})})}function Y(J,K,z,G){J.writeHead(K,{"Content-Type":"application/json",...G});J.end(JSON.stringify(z))}async function f(J,K){const z=J.headers.origin;if(z&&!z.startsWith("chrome-extension://")){Y(K,403,{ok:!1,error:"Forbidden: cross-origin request blocked"});return}if(J.method==="OPTIONS"){K.writeHead(204);K.end();return}const G=J.url??"/",$=G.split("?")[0];if(J.method==="GET"&&$==="/ping"){Y(K,200,{ok:!0},n($,z));return}if(!J.headers["x-opencli"]){Y(K,403,{ok:!1,error:"Forbidden: missing X-OpenCLI header"});return}if(J.method==="GET"&&$==="/status"){const Q=process.uptime(),Z=process.memoryUsage(),B=new URL(G,`http://localhost:${k}`).searchParams.get("contextId")?.trim()||void 0,X=q(B),_=I().map((M)=>({contextId:M.contextId,extensionConnected:!0,extensionVersion:M.extensionVersion??void 0,extensionCompatRange:M.extensionCompatRange??void 0,pending:[...N.values()].filter((H)=>H.contextId===M.contextId).length,lastSeenAt:M.lastSeenAt}));Y(K,200,{ok:!0,pid:process.pid,uptime:Q,daemonVersion:c,extensionConnected:!!X.connection,extensionVersion:X.connection?.extensionVersion??void 0,extensionCompatRange:X.connection?.extensionCompatRange??void 0,contextId:X.connection?.contextId??B,profileRequired:X.errorCode==="profile_required",profileDisconnected:X.errorCode==="profile_disconnected",profiles:_,pending:N.size,commandResultUnknown:y,memoryMB:Math.round(Z.rss/1024/1024*10)/10,port:k});return}if(J.method==="GET"&&$==="/logs"){const Z=new URL(G,`http://localhost:${k}`).searchParams.get("level"),A=Z?F.filter((B)=>B.level===Z):F;Y(K,200,{ok:!0,logs:A});return}if(J.method==="DELETE"&&$==="/logs"){F.length=0;Y(K,200,{ok:!0});return}if(J.method==="POST"&&$==="/shutdown"){Y(K,200,{ok:!0,message:"Shutting down"});setTimeout(()=>C(),100);return}if(J.method==="POST"&&G==="/command"){try{const Q=JSON.parse(await e(J));if(!Q.id){Y(K,400,{ok:!1,error:"Missing command id"});return}const Z=q(typeof Q.contextId==="string"?Q.contextId:void 0);if(!Z.connection){Y(K,Z.errorCode==="profile_required"?409:503,{id:Q.id,ok:!1,errorCode:Z.errorCode,error:Z.error,...Z.errorHint?{errorHint:Z.errorHint}:{}});return}const A=typeof Q.timeout==="number"&&Q.timeout>0?Q.timeout*1000:120000;if(N.has(Q.id)){Y(K,409,{id:Q.id,ok:!1,error:"Duplicate command id already pending; retry"});return}const B=await new Promise((X,_)=>{const M=setTimeout(()=>{N.delete(Q.id);L.traceTimeout(Q.id,A,typeof Q.action==="string"?Q.action:void 0);_(Error(`Command timeout (${A/1000}s)`))},A),H={contextId:Z.connection.contextId,action:typeof Q.action==="string"?Q.action:"unknown",dispatched:!1,dispatchTs:Date.now(),resolve:X,reject:_,timer:M};N.set(Q.id,H);const b=(U)=>{if(N.get(Q.id)!==H)return;const D=i(H.contextId);clearTimeout(M);N.delete(Q.id);_(new v(D.message,D.errorCode,D.errorHint,D.status));V.warn(`[daemon] Failed to dispatch command ${Q.id}: ${U instanceof Error?U.message:String(U)}`)};try{Z.connection.ws.send(JSON.stringify(Q),(U)=>{if(U&&!H.dispatched)b(U)});H.dispatched=!0;L.traceDispatch(Q)}catch(U){b(U)}});Y(K,200,B)}catch(Q){const Z=Q instanceof v?Q:null;if(Q instanceof R){Y(K,413,{ok:!1,error:Q.message,errorCode:"body_too_large"});return}Y(K,Z?.status??(Q instanceof Error&&Q.message.includes("timeout")?408:400),{ok:!1,error:Q instanceof Error?Q.message:"Invalid request",...Z?.errorCode?{errorCode:Z.errorCode}:{},...Z?.errorHint?{errorHint:Z.errorHint}:{}})}return}Y(K,404,{error:"Not found"})}const O=P((J,K)=>{f(J,K).catch(()=>{K.writeHead(500);K.end()})}),T=P((J,K)=>{f(J,K).catch(()=>{K.writeHead(500);K.end()})}),j=new g({noServer:!0});function m(J,K,z){if((J.url??"/").split("?")[0]!=="/ext"){K.destroy();return}const $=J.headers.origin;if($&&!$.startsWith("chrome-extension://")){K.destroy();return}j.handleUpgrade(J,K,z,(Q)=>{j.emit("connection",Q,J)})}O.on("upgrade",m);T.on("upgrade",m);j.on("connection",(J)=>{V.info("[daemon] Extension connected");let K=0;const z=setInterval(()=>{if(J.readyState!==E.OPEN){clearInterval(z);return}if(K>=2){V.warn("[daemon] Extension heartbeat lost, closing connection");clearInterval(z);J.terminate();return}K++;J.ping()},15000);J.on("pong",()=>{K=0});J.on("message",(G)=>{try{const $=JSON.parse(G.toString());if($.type==="hello"){const Z=r(J,$.contextId);Z.extensionVersion=typeof $.version==="string"?$.version:null;Z.extensionCompatRange=typeof $.compatRange==="string"?$.compatRange:null;Z.lastSeenAt=Date.now();if(Z.extensionVersion)a(Z.extensionVersion);V.info(`[daemon] Extension profile connected: ${Z.contextId}`);L.traceExtConnect(Z.contextId,Z.extensionVersion);return}if($.type==="log"){if($.level==="error")V.error(`[ext] ${$.msg}`);else if($.level==="warn")V.warn(`[ext] ${$.msg}`);else V.info(`[ext] ${$.msg}`);o({level:$.level,msg:$.msg,ts:$.ts??Date.now()});L.traceExtLog($.level,$.msg);return}const Q=N.get($.id);if(Q){clearTimeout(Q.timer);N.delete($.id);L.traceResult($.id,!!$.ok,Date.now()-Q.dispatchTs,typeof $.errorCode==="string"?$.errorCode:void 0,typeof $.page==="string"?$.page:void 0);Q.resolve($)}}catch($){const Q=G.toString().slice(0,200);V.warn(`[daemon] Ignoring malformed WS message from extension: ${$ instanceof Error?$.message:String($)} (first 200 chars: ${JSON.stringify(Q)})`)}});J.on("close",()=>{V.info("[daemon] Extension disconnected");clearInterval(z);w(J)});J.on("error",()=>{clearInterval(z);w(J)})});O.listen(k,"127.0.0.1",()=>{V.info(`[daemon] Listening on http://127.0.0.1:${k}`)});O.on("error",(J)=>{if(J.code==="EADDRINUSE"){V.error(`[daemon] Port ${k} already in use — another daemon is likely running. Exiting.`);process.exit(S.SERVICE_UNAVAIL)}V.error(`[daemon] Server error: ${J.message}`);process.exit(S.GENERIC_ERROR)});T.listen(h,"127.0.0.1",()=>{V.info(`[daemon] Listening on http://127.0.0.1:${h} (PublishPort extension)`)});T.on("error",(J)=>{V.warn(`[daemon] PublishPort port ${h} unavailable (${J.message}) — extension will fall back to ${k}`)});function C(){for(const[,J]of N){clearTimeout(J.timer);J.reject(Error("Daemon shutting down"))}N.clear();for(const J of W.values())J.ws.close();O.close();T.close();process.exit(S.SUCCESS)}process.on("SIGTERM",C);process.on("SIGINT",C);
@@ -27,6 +27,10 @@ export type SiteSessionMode = 'ephemeral' | 'persistent';
27
27
  export interface AuthStatusMetadata {
28
28
  quickCheck?: BrowserCommandFunc;
29
29
  refresh?: BrowserCommandFunc;
30
+ /** [pp-only] quickCheck「已登录」结果的落盘缓存秒数。给 jike 这种 quickCheck 必须
31
+ * 真实导航页面的站点用:命中缓存就完全不碰浏览器,避免桌面 20s 轮询反复开页。
32
+ * 只缓存 logged_in=true(登出要尽快被发现),按 profile 分键不跨账号串。 */
33
+ quickCheckCacheTtlSeconds?: number;
30
34
  }
31
35
  interface BaseCliCommand {
32
36
  site: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.8.5-pp.24",
3
+ "version": "1.8.5-pp.26",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false