publishport-opencli 1.8.5-pp.37 → 1.8.5-pp.39

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
@@ -19136,6 +19136,123 @@
19136
19136
  "sourceFile": "indeed/search.js",
19137
19137
  "navigateBefore": false
19138
19138
  },
19139
+ {
19140
+ "site": "infoq",
19141
+ "name": "article",
19142
+ "description": "发布文章到 InfoQ 写作社区(xie.infoq.cn)。默认正式发布;加 --draft 仅存草稿。正文 Markdown(编辑器吃转换后的富文本)。",
19143
+ "access": "write",
19144
+ "domain": "infoq.cn",
19145
+ "strategy": "cookie",
19146
+ "browser": true,
19147
+ "args": [
19148
+ {
19149
+ "name": "title",
19150
+ "type": "str",
19151
+ "required": true,
19152
+ "positional": true,
19153
+ "help": "文章标题"
19154
+ },
19155
+ {
19156
+ "name": "text",
19157
+ "type": "str",
19158
+ "required": false,
19159
+ "positional": true,
19160
+ "help": "文章正文(默认 Markdown;传 --html 则视为 HTML)"
19161
+ },
19162
+ {
19163
+ "name": "file",
19164
+ "type": "str",
19165
+ "required": false,
19166
+ "help": "正文文件路径(UTF-8 编码,默认 Markdown)"
19167
+ },
19168
+ {
19169
+ "name": "html",
19170
+ "type": "boolean",
19171
+ "required": false,
19172
+ "help": "将正文视为原始 HTML 而非 Markdown"
19173
+ },
19174
+ {
19175
+ "name": "tags",
19176
+ "type": "str",
19177
+ "required": false,
19178
+ "help": "文章标签,逗号分隔(可选;按平台联想下拉选择,选不中则回车创建)"
19179
+ },
19180
+ {
19181
+ "name": "draft",
19182
+ "type": "boolean",
19183
+ "required": false,
19184
+ "help": "仅保存草稿,不发布"
19185
+ },
19186
+ {
19187
+ "name": "execute",
19188
+ "type": "boolean",
19189
+ "required": false,
19190
+ "help": "确认执行写操作。不加此参数则拒绝写入。"
19191
+ }
19192
+ ],
19193
+ "columns": [
19194
+ "status",
19195
+ "outcome",
19196
+ "message",
19197
+ "target_type",
19198
+ "target",
19199
+ "created_target",
19200
+ "created_url"
19201
+ ],
19202
+ "type": "js",
19203
+ "modulePath": "infoq/article.js",
19204
+ "sourceFile": "infoq/article.js",
19205
+ "navigateBefore": "https://infoq.cn"
19206
+ },
19207
+ {
19208
+ "site": "infoq",
19209
+ "name": "login",
19210
+ "description": "打开 InfoQ 写作社区并等待浏览器完成登录(供桌面客户端引导登录)。",
19211
+ "access": "write",
19212
+ "domain": "infoq.cn",
19213
+ "strategy": "cookie",
19214
+ "browser": true,
19215
+ "args": [
19216
+ {
19217
+ "name": "timeout",
19218
+ "type": "int",
19219
+ "default": 300,
19220
+ "required": false,
19221
+ "help": "等待用户完成登录的最长秒数"
19222
+ }
19223
+ ],
19224
+ "columns": [
19225
+ "status",
19226
+ "logged_in",
19227
+ "user_id",
19228
+ "username"
19229
+ ],
19230
+ "type": "js",
19231
+ "modulePath": "infoq/login.js",
19232
+ "sourceFile": "infoq/login.js",
19233
+ "navigateBefore": false,
19234
+ "siteSession": "persistent",
19235
+ "defaultWindowMode": "foreground"
19236
+ },
19237
+ {
19238
+ "site": "infoq",
19239
+ "name": "whoami",
19240
+ "description": "检测 InfoQ 写作社区(xie.infoq.cn)当前登录状态。",
19241
+ "access": "read",
19242
+ "domain": "infoq.cn",
19243
+ "strategy": "cookie",
19244
+ "browser": true,
19245
+ "args": [],
19246
+ "columns": [
19247
+ "logged_in",
19248
+ "user_id",
19249
+ "username"
19250
+ ],
19251
+ "type": "js",
19252
+ "modulePath": "infoq/whoami.js",
19253
+ "sourceFile": "infoq/whoami.js",
19254
+ "navigateBefore": "https://infoq.cn"
19255
+ },
19139
19256
  {
19140
19257
  "site": "instagram",
19141
19258
  "name": "collection-create",
@@ -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.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))}})}
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))}:{},...v.quickCheckRequiresNavigation===!0?{quickCheckRequiresNavigation:!0}:{},...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))}})}
@@ -7,4 +7,4 @@ import{AuthRequiredError as z}from"@jackwener/opencli/errors";import{registerSit
7
7
  const handle = handleEl ? (handleEl.textContent || '').trim() : '';
8
8
  if (!display && !handle) return null;
9
9
  return { display_name: display, username: handle || display };
10
- })()`)}async function B(b){await b.goto(J);for(let F=0;F<8;F++){const G=await K(b);if(G)return G;await b.wait(1)}const x=await b.evaluate("location.href");if(/\/login/.test(String(x||"")))throw new z("discord.com","Discord redirected to /login — not signed in");throw new z("discord.com","Discord account panel not found — not signed in or app failed to load")}H({site:"discord-web",domain:"discord.com",loginUrl:"https://discord.com/login",columns:["username","display_name"],quickCheck:async(b)=>{try{return{logged_in:!0,...await B(b)}}catch(x){if(x instanceof z)return{logged_in:!1};throw x}},verify:B,poll:async(b)=>B(b)});
10
+ })()`)}async function B(b){await b.goto(J);for(let F=0;F<8;F++){const G=await K(b);if(G)return G;await b.wait(1)}const x=await b.evaluate("location.href");if(/\/login/.test(String(x||"")))throw new z("discord.com","Discord redirected to /login — not signed in");throw new z("discord.com","Discord account panel not found — not signed in or app failed to load")}H({site:"discord-web",domain:"discord.com",loginUrl:"https://discord.com/login",columns:["username","display_name"],quickCheckRequiresNavigation:!0,quickCheck:async(b)=>{try{return{logged_in:!0,...await B(b)}}catch(x){if(x instanceof z)return{logged_in:!1};throw x}},verify:B,poll:async(b)=>B(b)});
@@ -0,0 +1,19 @@
1
+ import{CliError as N,CommandExecutionError as j}from"@jackwener/opencli/errors";import{cli as S,Strategy as T}from"@jackwener/opencli/registry";import{readFile as F,stat as h}from"node:fs/promises";import{publishArticle as x,gotoWritePage as C}from"../_shared/article/publish.js";export const infoqProfile={home:"https://xie.infoq.cn",outputFormat:"html",checkAuth:async(H)=>{try{const J={};document.cookie.split("; ").forEach((Q)=>{const G=Q.indexOf("=");if(G>0)J[Q.slice(0,G)]=Q.slice(G+1)});if(J.GCID)return{isAuthenticated:!0,userId:"",username:""};return{isAuthenticated:!1}}catch(J){return{isAuthenticated:!1,error:String(J&&J.message||J)}}},publish:async(H,J)=>{const Q=H.params||{};function G(z){return new Promise((V)=>setTimeout(V,z))}await G(1000);for(let z=0;z<3;z++){const V=[...document.querySelectorAll("button, [gk-button], [role='button'], span")].find((Y)=>/^知道了$/.test((Y.textContent||"").trim()));if(!V)break;V.click();await G(500)}let K=null;{const z=Date.now();while(Date.now()-z<1e4&&!K){K=document.querySelector(".draft-title");if(!K)await G(300)}}if(!K)return{ok:!1,stage:"editor",message:"未找到标题输入框 .draft-title(草稿编辑器未加载)"};if(K.tagName==="INPUT"||K.tagName==="TEXTAREA"){const z=K.tagName==="INPUT"?HTMLInputElement.prototype:HTMLTextAreaElement.prototype;Object.getOwnPropertyDescriptor(z,"value").set.call(K,H.title);K.dispatchEvent(new InputEvent("input",{bubbles:!0,cancelable:!0,inputType:"insertText"}));K.dispatchEvent(new Event("change",{bubbles:!0}))}else if(K.getAttribute("contenteditable")==="true"){K.focus();K.textContent=H.title;K.dispatchEvent(new Event("input",{bubbles:!0}));K.dispatchEvent(new Event("change",{bubbles:!0}))}else{const z=K.querySelector("input")||K.querySelector("textarea");if(z){const V=z.tagName==="INPUT"?HTMLInputElement.prototype:HTMLTextAreaElement.prototype;Object.getOwnPropertyDescriptor(V,"value").set.call(z,H.title);z.dispatchEvent(new InputEvent("input",{bubbles:!0,cancelable:!0,inputType:"insertText"}));z.dispatchEvent(new Event("change",{bubbles:!0}))}else{K.textContent=H.title;K.dispatchEvent(new Event("input",{bubbles:!0}))}}await G(500);const Z=document.querySelector(".DraftEditor-root")||document.querySelector(".ProseMirror")||document.querySelector("[contenteditable=true]"),X=Z?Z.getAttribute&&Z.getAttribute("contenteditable")==="true"?Z:Z.querySelector("[contenteditable=true]")||Z:null;if(!X)return{ok:!1,stage:"editor",message:"未找到正文编辑区(.DraftEditor-root / contenteditable)"};const O=X.innerText||"";let A=!1;try{X.focus();X.innerHTML=H.content;X.dispatchEvent(new Event("input",{bubbles:!0,cancelable:!0}));X.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0}));await G(300);const z=X.innerText||"";if(z!==O&&z.trim())A=!0}catch(z){}if(!A)try{X.focus();const z=window.getSelection(),V=document.createRange();V.selectNodeContents(X);z.removeAllRanges();z.addRange(V);const Y=new DataTransfer;Y.setData("text/plain",X.innerText||"");Y.setData("text/html",H.content);X.dispatchEvent(new ClipboardEvent("paste",{bubbles:!0,cancelable:!0,clipboardData:Y}));await G(500);const R=X.innerText||"";if(R!==O&&R.trim())A=!0}catch(z){}if(!A)try{X.focus();document.execCommand("selectAll",!1,null);const z=document.execCommand("insertHTML",!1,H.content);await G(300);const V=X.innerText||"";if(z&&V!==O&&V.trim())A=!0}catch(z){}if(!A)return{ok:!1,stage:"editor",message:"正文填充失败:innerHTML / 剪贴板粘贴 / execCommand 三种策略均未生效"};await G(800);const P=Q.tags||[];if(P.length){const z=document.querySelector('input[placeholder*="标签"], input[placeholder*="tag"], .tag-input input, [class*="tag"] input');if(z)for(const V of P.slice(0,5)){z.focus();Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"value").set.call(z,V);z.dispatchEvent(new Event("input",{bubbles:!0}));await G(800);const Y=document.querySelector('[class*="suggest"] li, [class*="dropdown"] li, [class*="option"]');if(Y&&Y.textContent.includes(V))Y.click();else z.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter",bubbles:!0}));await G(400)}}const D="https://xie.infoq.cn/draft/"+Q.draftId;if(H.draftOnly){await G(1500);return{ok:!0,draft:!0,id:String(Q.draftId),url:D}}const W=["[gk-button][gkbtn-color=blue]","[gk-button][gkbtn-color=green]","[gk-button][gkbtn-color=orange]",'[class*="Button_button_"]',".submit-btn",".draft-submit-btn"];let q=null;{const z=Date.now();while(Date.now()-z<1e4&&!q){q=[...document.querySelectorAll('[gk-button], [class*="Button_button_"], button, [role="button"]')].find((V)=>/^(发布|发布文章)$/.test(V.textContent.trim())&&!V.disabled)||null;if(!q)for(const V of W){const Y=document.querySelector(V);if(Y&&!Y.disabled&&/发布/.test(Y.textContent)){q=Y;break}}if(!q)await G(300)}}if(!q)return{ok:!1,stage:"publish",message:"未找到发布按钮"};q.click();const M=[".dialog-footer-buttons [gkbtn-color=green]",".dialog-footer-buttons [gk-button]","[gk-modal-footer] [gk-button]","[gkmodal-color=confirm] [gk-button]","[gkmodal-color=editor-action] [gk-button]","[gkmodal-color=common] [gk-button]","[gkmodal-color=base] [gk-button]",'[gk-modal-footer] [class*="Button_button_"]',".gkui-modal-layer [gk-button]","#gkui-modal-controller [gk-button]"];let L=null;{const z=Date.now();while(Date.now()-z<15000&&!L){const V=document.querySelectorAll(".dialog-setting");let Y=null;for(const $ of V)if($.innerHTML.length>50){Y=$;break}if(Y){const $=Y.querySelector('[gkbtn-color="green"]');if($&&!$.disabled){L=$;break}const _=Y.querySelectorAll(".dialog-footer-buttons [gk-button]");for(const U of _)if(/确定|确认/.test(U.textContent)&&!U.disabled){L=U;break}if(L)break}for(const $ of M){const _=document.querySelector($);if(_&&!_.disabled&&/发布|确认|确定/.test(_.textContent)){L=_;break}}if(L)break;L=[...document.querySelectorAll('[gk-button], [class*="Button_button_"], button')].find(($)=>{if($.disabled)return!1;const _=$.textContent.trim();if(!/^(发布|确认发布|确认|确定)$/.test(_))return!1;return!!$.closest('[gkmodal-color], .gkui-modal-layer, #gkui-modal-controller, .dialog-setting, [class*="modal"], [class*="dialog"]')})||null;if(!L)await G(300)}}if(!L)return{ok:!1,stage:"publish",message:"确认弹窗未出现,文章可能未发布(仅保存为草稿)",detail:{draftUrl:D}};L.click();await G(500);return{ok:!0,draft:!1,id:String(Q.draftId),url:"https://xie.infoq.cn/article/"+Q.draftId}}};function v(H){if(!H.execute)throw new N("INVALID_INPUT","此命令需要 --execute 参数才能执行写操作。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function B(H){const J=typeof H.text==="string"?H.text:void 0,Q=typeof H.file==="string"?H.file:void 0;if(J&&Q)throw new N("INVALID_INPUT","不能同时指定正文参数和 --file,请二选一");let G=J??"";if(Q){let K;try{K=await h(Q)}catch{throw new N("INVALID_INPUT","文件不存在: "+Q)}if(!K.isFile())throw new N("INVALID_INPUT","必须是可读的文本文件: "+Q);let Z;try{Z=await F(Q)}catch{throw new N("INVALID_INPUT","无法读取文件: "+Q)}try{G=new TextDecoder("utf-8",{fatal:!0}).decode(Z)}catch{throw new N("INVALID_INPUT","文件必须是 UTF-8 编码: "+Q)}}if(!G.trim())throw new N("INVALID_INPUT","正文不能为空");return G}function w(H,J,Q,G,K={}){return[{status:"success",outcome:G,message:H,target_type:J,target:Q,...K}]}function I(H){const J=[],Q=/!\[[^\]]*\]\(([^)\s]+)[^)]*\)/g;let G;while((G=Q.exec(H))!==null)J.push(G[1]);const K=/<img[^>]+src=["']([^"']+)["']/gi;while((G=K.exec(H))!==null)J.push(G[1]);const Z=J.filter((X)=>!/^(https?:)?\/\//i.test(X)&&!X.startsWith("data:"));if(Z.length)throw new N("INVALID_INPUT","InfoQ 写作社区暂不支持本机图片自动转存(无可靠上传通道),请先把图片发布到可公网访问的 URL 再引用:"+Z.slice(0,3).join(", "))}async function u(H){await C(H,"https://xie.infoq.cn");const J=await H.evaluate(`(async () => {
2
+ try {
3
+ const resp = await fetch('https://xie.infoq.cn/api/v1/draft/create', {
4
+ method: 'POST',
5
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
6
+ credentials: 'include',
7
+ body: JSON.stringify({}),
8
+ });
9
+ const json = await resp.json();
10
+ if (!json || json.code !== 0) {
11
+ return { ok: false, message: 'InfoQ 建草稿失败(可能未登录):' + JSON.stringify(json).slice(0, 300) };
12
+ }
13
+ const draftId = (json.data && (json.data.id || json.data.draftId)) || null;
14
+ if (!draftId) return { ok: false, message: 'InfoQ 建草稿响应无 draftId:' + JSON.stringify(json).slice(0, 300) };
15
+ return { ok: true, draftId: String(draftId) };
16
+ } catch (e) {
17
+ return { ok: false, message: String((e && e.message) || e) };
18
+ }
19
+ })()`);if(!J||J.ok===!1)throw new j("[draft] "+(J&&J.message||"InfoQ 建草稿失败"));return J.draftId}S({site:"infoq",name:"article",access:"write",description:"发布文章到 InfoQ 写作社区(xie.infoq.cn)。默认正式发布;加 --draft 仅存草稿。正文 Markdown(编辑器吃转换后的富文本)。",domain:"infoq.cn",strategy:T.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;传 --html 则视为 HTML)"},{name:"file",help:"正文文件路径(UTF-8 编码,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"tags",help:"文章标签,逗号分隔(可选;按平台联想下拉选择,选不中则回车创建)"},{name:"draft",type:"boolean",help:"仅保存草稿,不发布"},{name:"execute",type:"boolean",help:"确认执行写操作。不加此参数则拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(H,J)=>{if(!H)throw new j("InfoQ 文章发布需要浏览器会话");v(J);const Q=String(J.title??"").trim();if(!Q)throw new N("INVALID_INPUT","文章标题不能为空");const G=await B(J);I(G);const K=Boolean(J.draft),Z=String(J.tags??"").split(",").map((W)=>W.trim()).filter(Boolean),X=await u(H),O={...infoqProfile,home:"https://xie.infoq.cn/draft/"+X},A=await x(H,{title:Q,body:G,format:J.html?"html":"markdown",draftOnly:K,profile:O,publishParams:{draftId:X,tags:Z}});let P=A.url;if(!K)try{await H.wait({time:3});const W=await H.evaluate("location.href");if(typeof W==="string"&&/\/article\/[a-zA-Z0-9]+/.test(W))P=W.split("?")[0].split("#")[0]}catch(W){}const D=A.draft?"草稿已保存至 InfoQ 写作社区(编辑器自动保存,需在编辑器内手动发布)":"文章已发布至 InfoQ 写作社区";return w(D,"article","",A.draft?"draft":"publish",{created_target:"article:"+A.id,created_url:P})}});
@@ -0,0 +1 @@
1
+ import{registerArticleLogin as k}from"../_shared/article/login.js";import{infoqProfile as b}from"./article.js";k({site:"infoq",domain:"infoq.cn",profile:{home:b.home,checkAuth:b.checkAuth},loginUrl:"https://xie.infoq.cn",loginDescription:"打开 InfoQ 写作社区并等待浏览器完成登录(供桌面客户端引导登录)。"});
@@ -0,0 +1 @@
1
+ import{cli as A,Strategy as B}from"@jackwener/opencli/registry";import{checkLogin as D,cookieQuickCheck as E}from"../_shared/article/auth.js";import{infoqProfile as x}from"./article.js";const F={home:x.home,checkAuth:x.checkAuth};A({site:"infoq",name:"whoami",access:"read",description:"检测 InfoQ 写作社区(xie.infoq.cn)当前登录状态。",domain:"infoq.cn",strategy:B.COOKIE,browser:!0,columns:["logged_in","user_id","username"],authStatus:{quickCheck:E("https://xie.infoq.cn",["GCID"])},func:async(z)=>{const v=await D(z,F);return[{logged_in:v.isAuthenticated,user_id:v.userId||"",username:v.username||""}]}});
package/clis/jike/auth.js CHANGED
@@ -1,4 +1,4 @@
1
- import{AuthRequiredError as N,CommandExecutionError as K}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as V}from"../_shared/site-auth.js";const Q=`(async () => {
1
+ import{AuthRequiredError as L,CommandExecutionError as K}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as Q}from"../_shared/site-auth.js";const N=`(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 N,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 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(`(() => {
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,quickCheckRequiresNavigation:!0,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(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}}});
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}}});
@@ -1 +1 @@
1
- import{CliError as V,CommandExecutionError as T}from"@jackwener/opencli/errors";import{cli as h,Strategy as P}from"@jackwener/opencli/registry";import{publishArticle as _}from"../_shared/article/publish.js";import{readFile as x,stat as y}from"node:fs/promises";function b(J){if(!J.execute)throw new V("INVALID_INPUT","此头条号写操作需要 --execute 参数才能真正发布。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function C(J){const G=typeof J.text==="string"?J.text:void 0,z=typeof J.file==="string"?J.file:void 0;if(G&&z)throw new V("INVALID_INPUT","正文和 --file 只能选一个");let B=G??"";if(z){let Q;try{Q=await y(z)}catch{throw new V("INVALID_INPUT",`找不到文件: ${z}`)}if(!Q.isFile())throw new V("INVALID_INPUT",`--file 必须是可读文本文件: ${z}`);let X;try{X=await x(z)}catch{throw new V("INVALID_INPUT",`文件无法读取: ${z}`)}try{B=new TextDecoder("utf-8",{fatal:!0}).decode(X)}catch{throw new V("INVALID_INPUT",`文件必须是 UTF-8 编码: ${z}`)}}if(!B.trim())throw new V("INVALID_INPUT","正文不能为空");return B}function E(J,G,z,B,Q={}){return[{status:"success",outcome:B,message:J,target_type:G,target:z,...Q}]}export const toutiaoProfile={home:"https://mp.toutiao.com/profile_v3/graphic/publish",outputFormat:"html",preprocessConfig:{},publish:async(J,G)=>{const z=J.params||{},B=[],Q=[],X=["toutiao.com","pstatp.com","byteimg.com","bytecdn","ibytedtos","toutiaoimg","tos-cn"];async function W(D){const L=await fetch(D,{credentials:"omit"});if(!L.ok)throw Error("图片下载失败 HTTP "+L.status+":"+D.slice(0,80));const M=await L.blob(),S=((M.type||"").split("/")[1]||"jpg").split("+")[0].split(";")[0]||"jpg",H=new FormData;H.append("upfile",M,"image."+S);const U=await(await fetch("https://mp.toutiao.com/mp/agw/article_material/photo/upload_picture?type=ueditor&pgc_watermark=1&action=uploadimage&encode=utf-8",{method:"POST",credentials:"include",body:H})).json();if(U.code!==0||U.state!=="SUCCESS"||!U.url)throw Error(U.message||"图片上传失败");return{url:U.url,web_uri:U.origin_web_uri||U.web_uri,width:U.width,height:U.height}}const Y=document.createElement("div");Y.innerHTML=J.content;const Z=Array.prototype.slice.call(Y.querySelectorAll("img"));for(const D of Z){const L=D.getAttribute("src")||"";if(!L){D.remove();continue}if(X.some((M)=>L.indexOf(M)!==-1))continue;try{const M=await W(L);D.setAttribute("src",M.url);D.setAttribute("web_uri",M.web_uri);B.push({src:L.slice(0,120),url:M.url})}catch(M){Q.push({src:L.slice(0,120),error:String(M&&M.message||M)})}}const $=Y.innerHTML,A=[];if(z.cover&&String(z.cover).trim()){let D;try{D=await W(String(z.cover))}catch(L){return{ok:!1,stage:"cover",message:"封面图转存失败:"+String(L&&L.message||L),uploaded:B,failed:Q}}A.push({id:0,url:D.url,uri:D.web_uri,origin_uri:D.web_uri,ic_uri:"",thumb_width:D.width,thumb_height:D.height})}await fetch("https://mp.toutiao.com/profile_v3/graphic/publish",{credentials:"include"});const O=new URLSearchParams({title:J.title,article_ad_type:"2",article_type:"0",from_diagnosis:"0",origin_debut_check_pgc_normal:"0",tree_plan_article:"0",save:"0",pgc_id:"0",content:$,pgc_feed_covers:JSON.stringify(A)}),F=await fetch("https://mp.toutiao.com/mp/agw/article/publish?source=mp&type=article",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:O});let K=null;try{K=await F.json()}catch(D){}const R=K&&(K.code===0||K.err_no===0),q=K&&K.data&&K.data.pgc_id;if(!R||!q||String(q)==="0"){let D=K&&(K.message||K.reason)||"头条号发布失败";if(K&&(K.code===7050||K.err_no===7050))D+="(code 7050:官方编辑器页发布同样被拒,通常为账号被平台限制发文或内容风控拦截,"+"请登录 mp.toutiao.com 查看站内通知并手动发一篇确认;读接口不受影响)";return{ok:!1,stage:"publish",status:F.status,message:D,detail:K,uploaded:B,failed:Q}}const N="https://mp.toutiao.com/profile_v3/graphic/publish?pgc_id="+String(q);return{ok:!0,id:String(q),url:N,draft:!1,uploaded:B,failed:Q}},checkAuth:async(J)=>{try{const z=await(await fetch("https://mp.toutiao.com/mp/agw/media/get_media_info",{credentials:"include"})).json(),B=z&&z.data&&z.data.user;if(z&&z.code===0&&B&&B.id)return{isAuthenticated:!0,userId:String(B.id),username:B.screen_name||B.name||"",avatar:B.https_avatar_url||B.avatar_url||""};return{isAuthenticated:!1}}catch(G){return{isAuthenticated:!1,error:String(G&&G.message||G)}}}};h({site:"toutiao",name:"article",access:"write",description:"正式发布头条号图文文章(会立即公开,头条无可靠的存草稿接口,故不支持草稿)。正文默认 Markdown,图片自动转存到头条图床。标题需 2~30 字;封面可选(--cover,不传则头条自动从正文图选封面)。",domain:"mp.toutiao.com",strategy:P.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题(2~30 字)"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;传 --html 则视为原始 HTML)"},{name:"file",help:"正文文件路径(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"cover",help:"封面图 URL 或本机路径(可选;会先转存到头条图床)"},{name:"execute",type:"boolean",help:"真正执行写操作。不加此参数时命令拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(J,G)=>{if(!J)throw new T("头条号文章发布需要浏览器会话");b(G);const z=String(G.title??"").trim();if(!z)throw new V("INVALID_INPUT","文章标题不能为空");if(z.length<2||z.length>30)throw new V("INVALID_INPUT",`头条号标题需 2~30 字,当前 ${z.length} 字`);const B=await C(G),X={cover:typeof G.cover==="string"?G.cover.trim():""},W=await _(J,{title:z,body:B,format:G.html?"html":"markdown",profile:toutiaoProfile,publishParams:X}),Y=W.images.uploaded.length|0,Z=W.images.failed.length|0;let $="已正式发布头条号文章";if(Y||Z)$+=`(图片:${Y} 张已转存${Z?`,${Z} 张失败`:""})`;return E($,"article","","created",{created_target:"article:"+W.id,created_url:W.url})}});
1
+ import{CliError as V,CommandExecutionError as y}from"@jackwener/opencli/errors";import{cli as h,Strategy as T}from"@jackwener/opencli/registry";import{publishArticle as b}from"../_shared/article/publish.js";import{readFile as C,stat as E}from"node:fs/promises";function I(J){if(!J.execute)throw new V("INVALID_INPUT","此头条号写操作需要 --execute 参数才能真正发布。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function v(J){const G=typeof J.text==="string"?J.text:void 0,z=typeof J.file==="string"?J.file:void 0;if(G&&z)throw new V("INVALID_INPUT","正文和 --file 只能选一个");let D=G??"";if(z){let Q;try{Q=await E(z)}catch{throw new V("INVALID_INPUT",`找不到文件: ${z}`)}if(!Q.isFile())throw new V("INVALID_INPUT",`--file 必须是可读文本文件: ${z}`);let X;try{X=await C(z)}catch{throw new V("INVALID_INPUT",`文件无法读取: ${z}`)}try{D=new TextDecoder("utf-8",{fatal:!0}).decode(X)}catch{throw new V("INVALID_INPUT",`文件必须是 UTF-8 编码: ${z}`)}}if(!D.trim())throw new V("INVALID_INPUT","正文不能为空");return D}function j(J,G,z,D,Q={}){return[{status:"success",outcome:D,message:J,target_type:G,target:z,...Q}]}export const toutiaoProfile={home:"https://mp.toutiao.com/profile_v3/graphic/publish",outputFormat:"html",preprocessConfig:{},publish:async(J,G)=>{const z=J.params||{},D=[],Q=[],X=["toutiao.com","pstatp.com","byteimg.com","bytecdn","ibytedtos","toutiaoimg","tos-cn"];async function W(B){const L=await fetch(B,{credentials:"omit"});if(!L.ok)throw Error("图片下载失败 HTTP "+L.status+":"+B.slice(0,80));const M=await L.blob(),x=((M.type||"").split("/")[1]||"jpg").split("+")[0].split(";")[0]||"jpg",R=new FormData;R.append("upfile",M,"image."+x);const U=await(await fetch("https://mp.toutiao.com/mp/agw/article_material/photo/upload_picture?type=ueditor&pgc_watermark=1&action=uploadimage&encode=utf-8",{method:"POST",credentials:"include",body:R})).json();if(U.code!==0||U.state!=="SUCCESS"||!U.url)throw Error(U.message||"图片上传失败");return{url:U.url,web_uri:U.origin_web_uri||U.web_uri,width:U.width,height:U.height}}const Y=document.createElement("div");Y.innerHTML=J.content;const Z=Array.prototype.slice.call(Y.querySelectorAll("img"));for(const B of Z){const L=B.getAttribute("src")||"";if(!L){B.remove();continue}if(X.some((M)=>L.indexOf(M)!==-1))continue;try{const M=await W(L);B.setAttribute("src",M.url);B.setAttribute("web_uri",M.web_uri);D.push({src:L.slice(0,120),url:M.url})}catch(M){Q.push({src:L.slice(0,120),error:String(M&&M.message||M)})}}const $=Y.innerHTML,A=[];if(z.cover&&String(z.cover).trim()){let B;try{B=await W(String(z.cover))}catch(L){return{ok:!1,stage:"cover",message:"封面图转存失败:"+String(L&&L.message||L),uploaded:D,failed:Q}}A.push({id:0,url:B.url,uri:B.web_uri,origin_uri:B.web_uri,ic_uri:"",thumb_width:B.width,thumb_height:B.height})}await fetch("https://mp.toutiao.com/profile_v4/graphic/publish",{credentials:"include"});function N(){const B=document.cookie.match(/passport_csrf_token=([^;]+)/);return B?B[1]:""}const F=N(),S=new URLSearchParams({title:J.title,article_ad_type:"2",article_type:"0",from_diagnosis:"0",origin_debut_check_pgc_normal:"0",tree_plan_article:"0",save:"1",pgc_id:"0",content:$,pgc_feed_covers:JSON.stringify(A)}),H={"Content-Type":"application/x-www-form-urlencoded"};if(F)H["X-CSRFToken"]=F;const O=await fetch("https://mp.toutiao.com/mp/agw/article/publish?source=mp&type=article&aid=1231",{method:"POST",credentials:"include",headers:H,body:S});let K=null;try{K=await O.json()}catch(B){}const P=K&&(K.code===0||K.err_no===0),q=K&&K.data&&K.data.pgc_id;if(!P||!q||String(q)==="0"){let B=K&&(K.message||K.reason)||"头条号发布失败";if(K&&(K.code===7050||K.err_no===7050))B+="(code 7050「保存失败」:内容可能被风控拦截或账号被平台限制发文,"+"请登录 mp.toutiao.com 查看站内通知)";return{ok:!1,stage:"publish",status:O.status,message:B,detail:K,uploaded:D,failed:Q}}const _="https://mp.toutiao.com/profile_v4/graphic/publish?pgc_id="+String(q);return{ok:!0,id:String(q),url:_,draft:!1,uploaded:D,failed:Q}},checkAuth:async(J)=>{try{const z=await(await fetch("https://mp.toutiao.com/mp/agw/media/get_media_info",{credentials:"include"})).json(),D=z&&z.data&&z.data.user;if(z&&z.code===0&&D&&D.id)return{isAuthenticated:!0,userId:String(D.id),username:D.screen_name||D.name||"",avatar:D.https_avatar_url||D.avatar_url||""};return{isAuthenticated:!1}}catch(G){return{isAuthenticated:!1,error:String(G&&G.message||G)}}}};h({site:"toutiao",name:"article",access:"write",description:"正式发布头条号图文文章(会立即公开,头条无可靠的存草稿接口,故不支持草稿)。正文默认 Markdown,图片自动转存到头条图床。标题需 2~30 字;封面可选(--cover,不传则头条自动从正文图选封面)。",domain:"mp.toutiao.com",strategy:T.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题(2~30 字)"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;传 --html 则视为原始 HTML)"},{name:"file",help:"正文文件路径(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"cover",help:"封面图 URL 或本机路径(可选;会先转存到头条图床)"},{name:"execute",type:"boolean",help:"真正执行写操作。不加此参数时命令拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(J,G)=>{if(!J)throw new y("头条号文章发布需要浏览器会话");I(G);const z=String(G.title??"").trim();if(!z)throw new V("INVALID_INPUT","文章标题不能为空");if(z.length<2||z.length>30)throw new V("INVALID_INPUT",`头条号标题需 2~30 字,当前 ${z.length} 字`);const D=await v(G),X={cover:typeof G.cover==="string"?G.cover.trim():""},W=await b(J,{title:z,body:D,format:G.html?"html":"markdown",profile:toutiaoProfile,publishParams:X}),Y=W.images.uploaded.length|0,Z=W.images.failed.length|0;let $="已正式发布头条号文章";if(Y||Z)$+=`(图片:${Y} 张已转存${Z?`,${Z} 张失败`:""})`;return j($,"article","","created",{created_target:"article:"+W.id,created_url:W.url})}});
@@ -6,9 +6,7 @@ let j;const k=`
6
6
  try {
7
7
  _nativeAdd.call(window, 'beforeunload', function (e) {
8
8
  try { e.stopImmediatePropagation(); } catch (x) {}
9
- try { e.preventDefault(); } catch (x) {}
10
- try { delete e.returnValue; } catch (x) {}
11
- try { e.returnValue = undefined; } catch (x) {}
9
+ try { e.returnValue = ''; } catch (x) {}
12
10
  }, true);
13
11
  } catch (e) {}
14
12
  try { window.onbeforeunload = null; } catch (e) {}
@@ -7,7 +7,7 @@ export interface AuthStatusRow {
7
7
  status: AuthStatus;
8
8
  logged_in: boolean | '';
9
9
  identity: string;
10
- checked: AuthStatusMode | 'skipped';
10
+ checked: AuthStatusMode | 'skipped' | 'navigation_required';
11
11
  error: string;
12
12
  }
13
13
  interface AuthStatusOptions {
@@ -1,3 +1,2 @@
1
- import{mkdir as I,readFile as U,writeFile as _}from"node:fs/promises";import{homedir as F}from"node:os";import{dirname as x,join as b}from"node:path";import{pathToFileURL as p}from"node:url";import{InvalidArgumentError as v,Option as i}from"commander";import{AuthRequiredError as T,CliError as C,getErrorMessage as V}from"../errors.js";import{executeCommand as N}from"../execution.js";import{ensureBrowserBridgeReady as a}from"../browser/daemon-lifecycle.js";import{resolveProfile as m}from"../browser/profile.js";import{isElectronApp as j}from"../electron-apps.js";import{fullName as c,getRegistry as k}from"../registry.js";import{render as A}from"../output.js";const O=1,q=86400000;function z(G,J,X){if(G===void 0||G===null||G==="")return X;const Z=Number(G);if(!Number.isInteger(Z)||Z<=0)throw new v(`${J} must be a positive integer. Received: "${String(G)}"`);return Z}function E(G){if(!G||!G.trim())return null;const J=G.split(",").map((X)=>X.trim()).filter(Boolean);return J.length>0?new Set(J):null}function d(){return b(F(),".opencli","auth-refresh.json")}function o(){return{version:O,sites:{}}}async function s(G){try{const J=JSON.parse(await U(G,"utf8"));if(J&&J.version===O&&J.sites&&typeof J.sites==="object")return{version:O,sites:J.sites}}catch(J){if(J.code!=="ENOENT")throw J}return o()}async function r(G,J){await I(x(G),{recursive:!0});await _(G,`${JSON.stringify(J,null,2)}
2
- `,"utf8")}function S(G){if(!G?.last_touched_at)return null;const J=Date.parse(G.last_touched_at);return Number.isFinite(J)?J:null}function t(G,J){const X=S(G);return X!==null&&J.getTime()-X<q}function B(G){const J=S(G);return J===null?"":new Date(J+q).toISOString()}function R(){const G=new Set;return[...k().values()].filter((J)=>{if(G.has(J))return!1;G.add(J);return J.name==="whoami"&&J.access==="read"}).sort((J,X)=>J.site.localeCompare(X.site))}async function H(G){const J=G;if(!J._lazy||!J._modulePath)return G;await import(p(J._modulePath).href);return k().get(c(G))??G}function M(G,J){const X=G.args.some((Z)=>Z.name==="timeout");return{...G,args:X?G.args:[...G.args,{name:"timeout",type:"int",default:J,help:"Per-site auth command timeout in seconds"}]}}function e(G,J){if(G.browser!==!0||typeof G.authStatus?.quickCheck!=="function")return null;return M({...G,func:G.authStatus.quickCheck,navigateBefore:!1,siteSession:"ephemeral",defaultWindowMode:"background"},J)}function w(G){if(typeof G==="boolean")return G;if(G&&typeof G==="object"&&!Array.isArray(G)){const J=G.logged_in;if(typeof J==="boolean")return J}return null}function f(G){if(G===void 0||G===null)return"";if(typeof G==="string"||typeof G==="number"||typeof G==="boolean")return String(G);return""}function y(G){if(!G||typeof G!=="object"||Array.isArray(G))return"";const J=G,X=/(?:email|phone|real.?name|first.?name|last.?name|cookie|token|session|secret|password|csrf|jwt|bearer|wt2)/i;for(const Z of["username","handle","user_id","id","name","nickname","user_type","url"]){if(X.test(Z))continue;const Y=f(J[Z]);if(Y)return Y}for(const[Z,Y]of Object.entries(J)){if(Z==="site"||Z==="logged_in"||X.test(Z))continue;const $=f(Y);if($)return $}return""}function P(G){return G instanceof T||typeof G==="object"&&G!==null&&G.code==="AUTH_REQUIRED"}function g(G,J,X){if(P(X))return{site:G,status:"not_logged_in",logged_in:!1,identity:"",checked:J,error:""};const Z=X instanceof C?X.code:"",Y=V(X);return{site:G,status:"error",logged_in:"",identity:"",checked:J,error:Z?`${Z}: ${Y}`:Y}}function GG(G,J){if(G.browser!==!0)return null;let X=G.authStatus?.refresh;if(typeof X!=="function"){const Z=G.authStatus?.quickCheck;if(typeof Z!=="function"||!G.domain)return null;const Y=G.domain.startsWith("http://")||G.domain.startsWith("https://")?G.domain:`https://${G.domain}`;X=async($,D,K)=>{await $.goto(Y);await $.wait(1);if(w(await Z($,D,K))!==!0)throw new T(G.domain??G.site,`Auth refresh quickCheck failed for ${G.site}`);return{status:"touched"}}}return M({...G,func:X,navigateBefore:!1,siteSession:"persistent",defaultWindowMode:"background"},J)}function JG(G){if(G&&typeof G==="object"&&!Array.isArray(G)){const J=G;if(J.status==="refreshed"||J.refreshed===!0)return"refreshed"}return"touched"}function XG(G,J,X){if(P(X))return{site:G,status:"not_logged_in",last_touched_at:J?.last_touched_at??"",next_refresh_at:B(J),error:""};const Z=X instanceof C?X.code:"",Y=V(X);return{site:G,status:"error",last_touched_at:J?.last_touched_at??"",next_refresh_at:B(J),error:Z?`${Z}: ${Y}`:Y}}function h(){return process.env.PUBLISHPORT_SITE_SESSION==="ephemeral"?{siteSession:"ephemeral",keepTab:"false"}:{siteSession:"persistent",keepTab:"true"}}function u(G){return b(F(),".opencli","sites",G,"quickcheck-cache.json")}async function YG(G,J,X){try{const Y=JSON.parse(await U(u(G),"utf8"))?.[X??"default"];if(!Y||Y.logged_in!==!0)return!1;const $=Date.now()-Date.parse(Y.checked_at);return Number.isFinite($)&&$>=0&&$<J*1000}catch{return!1}}async function ZG(G,J,X){const Z=u(G),Y=X??"default";let $={};try{$=JSON.parse(await U(Z,"utf8"))}catch{}if(J)$[Y]={logged_in:!0,checked_at:new Date().toISOString()};else delete $[Y];try{await I(x(Z),{recursive:!0});await _(Z,`${JSON.stringify($,null,2)}
3
- `,"utf8")}catch{}}async function $G(G,J){try{const X=await H(G);if(X.browser!==!0){const K=M(X,J.timeoutSeconds),Q=await N(K,{timeout:J.timeoutSeconds},!1,{...J.profile?{profile:J.profile}:{}});return{site:G.site,status:"logged_in",logged_in:!0,identity:y(Q),checked:"quick",error:""}}const Z=X.authStatus?.quickCheckCacheTtlSeconds;if(typeof Z==="number"&&Z>0&&await YG(G.site,Z,J.profile))return{site:G.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};const Y=e(X,J.timeoutSeconds);if(!Y)return{site:G.site,status:"unknown",logged_in:"",identity:"",checked:"skipped",error:"quickCheck not implemented; use --full to run whoami"};const $=await N(Y,{timeout:J.timeoutSeconds},!1,{...h(),windowMode:"background",...J.profile?{profile:J.profile}:{}}),D=w($);if(typeof Z==="number"&&Z>0&&typeof D==="boolean")await ZG(G.site,D,J.profile);if(D===!0)return{site:G.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};if(D===!1)return{site:G.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"quick",error:""};return{site:G.site,status:"unknown",logged_in:"",identity:"",checked:"quick",error:"quickCheck returned no boolean logged_in signal"}}catch(X){return g(G.site,"quick",X)}}async function DG(G,J){try{const X=await H(G),Z=M(X,J.timeoutSeconds),Y=await N(Z,{timeout:J.timeoutSeconds},!1,{...h(),windowMode:"background",...J.profile?{profile:J.profile}:{}});return{site:G.site,status:"logged_in",logged_in:!0,identity:y(Y),checked:"full",error:""}}catch(X){return g(G.site,"full",X)}}function WG(G,J){return J?`${G}@${J}`:G}async function KG(G,J){const X=WG(G.site,J.profile),Z=J.state.sites[X];if(!J.force&&t(Z,J.now))return{site:G.site,status:"skipped",last_touched_at:Z?.last_touched_at??"",next_refresh_at:B(Z),error:""};const Y=J.now.toISOString();try{const $=await H(G),D=GG($,J.timeoutSeconds);if(!D){J.state.sites[X]={...Z,last_attempt_at:Y,last_status:"unsupported"};return{site:G.site,status:"unsupported",last_touched_at:Z?.last_touched_at??"",next_refresh_at:B(Z),error:"refresh probe is not available for this site"}}const K=await N(D,{timeout:J.timeoutSeconds},!1,{siteSession:"persistent",keepTab:"true",windowMode:"background",...J.profile?{profile:J.profile}:{}}),Q=JG(K);J.state.sites[X]={...Z,last_attempt_at:Y,last_touched_at:Y,last_status:Q};return{site:G.site,status:Q,last_touched_at:Y,next_refresh_at:new Date(J.now.getTime()+q).toISOString(),error:""}}catch($){const D=P($)?"not_logged_in":"error";J.state.sites[X]={...Z,last_attempt_at:Y,last_status:D};return XG(G.site,Z,$)}}async function l(G,J){const X=m(J);if(X.kind!=="extension")return null;if(!G.some((Y)=>Y.browser===!0&&!j(Y.site)))return null;try{await a({timeoutSeconds:15,contextId:X.contextId,verbose:!1});return null}catch(Y){return V(Y)}}function QG(G,J,X){return{site:G,status:"error",logged_in:"",identity:"",checked:J,error:`BROWSER_CONNECT: ${X}`}}async function n(G,J,X){const Z=Array(G.length);let Y=0;const $=Array.from({length:Math.min(J,G.length)},async()=>{while(Y<G.length){const D=Y++;Z[D]=await X(G[D])}});await Promise.all($);return Z}export async function collectAuthStatus(G){const J=E(G.sites),X=G.full?"full":"quick",Z=z(G.concurrency,"--concurrency",X==="full"?3:8),Y=z(G.timeout,"--timeout",X==="full"?20:8),$=String(G.only??"all");if(!["all","logged-in","not-logged-in","unknown","error"].includes($))throw new v("--only must be one of: all, logged-in, not-logged-in, unknown, error");const D=R().filter((W)=>!J||J.has(W.site)),K=await l(D,G.profile),Q=await n(D,Z,(W)=>K&&W.browser===!0&&!j(W.site)?Promise.resolve(QG(W.site,X,K)):X==="full"?DG(W,{timeoutSeconds:Y,profile:G.profile}):$G(W,{timeoutSeconds:Y,profile:G.profile})),L=$.replace(/-/g,"_");return L==="all"?Q:Q.filter((W)=>W.status===L)}export async function collectAuthRefresh(G){const J=E(G.sites),X=z(G.concurrency,"--concurrency",3),Z=z(G.timeout,"--timeout",20),Y=G.statePath??d(),$=G.now??new Date,D=await s(Y),K=R().filter((W)=>!J||J.has(W.site)),Q=await l(K,G.profile),L=await n(K,X,(W)=>Q&&W.browser===!0&&!j(W.site)?Promise.resolve({site:W.site,status:"error",last_touched_at:D.sites[W.site]?.last_touched_at??"",next_refresh_at:B(D.sites[W.site]),error:`BROWSER_CONNECT: ${Q}`}):KG(W,{timeoutSeconds:Z,profile:G.profile,now:$,state:D,force:G.all===!0}));await r(Y,D);return L}export function registerAuthCommands(G){const J=G.command("auth").description("Inspect website login status"),X=J.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 i("--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(Y)=>{const $=typeof X.optsWithGlobals==="function"?X.optsWithGlobals():{},D=await collectAuthStatus({sites:Y.site,full:Y.full===!0,concurrency:Y.concurrency,timeout:Y.timeout,only:Y.only,profile:typeof $.profile==="string"&&$.profile.trim()?$.profile.trim():void 0}),K=typeof Y.format==="string"?Y.format:"table";A(D,{fmt:K,fmtExplicit:X.getOptionValueSource("format")==="cli",columns:["site","status","identity","checked","error"],title:"opencli/auth status",source:Y.full?"full whoami probe":"quick auth check"})}),Z=J.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(Y)=>{const $=typeof Z.optsWithGlobals==="function"?Z.optsWithGlobals():{},D=await collectAuthRefresh({sites:Y.site,all:Y.all===!0,concurrency:Y.concurrency,timeout:Y.timeout,profile:typeof $.profile==="string"&&$.profile.trim()?$.profile.trim():void 0}),K=typeof Y.format==="string"?Y.format:"table";A(D,{fmt:K,fmtExplicit:Z.getOptionValueSource("format")==="cli",columns:["site","status","last_touched_at","next_refresh_at","error"],title:"opencli/auth refresh",source:Y.all?"forced persistent touch":"persistent touch with 24h throttle"})});return J}
1
+ import{mkdir as y,readFile as w,writeFile as g}from"node:fs/promises";import{homedir as h}from"node:os";import{dirname as l,join as u}from"node:path";import{pathToFileURL as n}from"node:url";import{InvalidArgumentError as I,Option as p}from"commander";import{AuthRequiredError as P,CliError as F,getErrorMessage as M}from"../errors.js";import{executeCommand as N}from"../execution.js";import{ensureBrowserBridgeReady as i}from"../browser/daemon-lifecycle.js";import{resolveProfile as a}from"../browser/profile.js";import{isElectronApp as U}from"../electron-apps.js";import{fullName as c,getRegistry as _}from"../registry.js";import{render as x}from"../output.js";const V=1,q=86400000;function j(G,J,X){if(G===void 0||G===null||G==="")return X;const Z=Number(G);if(!Number.isInteger(Z)||Z<=0)throw new I(`${J} must be a positive integer. Received: "${String(G)}"`);return Z}function C(G){if(!G||!G.trim())return null;const J=G.split(",").map((X)=>X.trim()).filter(Boolean);return J.length>0?new Set(J):null}function m(){return u(h(),".opencli","auth-refresh.json")}function d(){return{version:V,sites:{}}}async function o(G){try{const J=JSON.parse(await w(G,"utf8"));if(J&&J.version===V&&J.sites&&typeof J.sites==="object")return{version:V,sites:J.sites}}catch(J){if(J.code!=="ENOENT")throw J}return d()}async function s(G,J){await y(l(G),{recursive:!0});await g(G,`${JSON.stringify(J,null,2)}
2
+ `,"utf8")}function T(G){if(!G?.last_touched_at)return null;const J=Date.parse(G.last_touched_at);return Number.isFinite(J)?J:null}function r(G,J){const X=T(G);return X!==null&&J.getTime()-X<q}function B(G){const J=T(G);return J===null?"":new Date(J+q).toISOString()}function b(){const G=new Set;return[..._().values()].filter((J)=>{if(G.has(J))return!1;G.add(J);return J.name==="whoami"&&J.access==="read"}).sort((J,X)=>J.site.localeCompare(X.site))}async function O(G){const J=G;if(!J._lazy||!J._modulePath)return G;await import(n(J._modulePath).href);return _().get(c(G))??G}function z(G,J){const X=G.args.some((Z)=>Z.name==="timeout");return{...G,args:X?G.args:[...G.args,{name:"timeout",type:"int",default:J,help:"Per-site auth command timeout in seconds"}]}}function t(G,J){if(G.browser!==!0||typeof G.authStatus?.quickCheck!=="function")return null;return z({...G,func:G.authStatus.quickCheck,navigateBefore:!1,siteSession:"ephemeral",defaultWindowMode:"background"},J)}function v(G){if(typeof G==="boolean")return G;if(G&&typeof G==="object"&&!Array.isArray(G)){const J=G.logged_in;if(typeof J==="boolean")return J}return null}function k(G){if(G===void 0||G===null)return"";if(typeof G==="string"||typeof G==="number"||typeof G==="boolean")return String(G);return""}function S(G){if(!G||typeof G!=="object"||Array.isArray(G))return"";const J=G,X=/(?:email|phone|real.?name|first.?name|last.?name|cookie|token|session|secret|password|csrf|jwt|bearer|wt2)/i;for(const Z of["username","handle","user_id","id","name","nickname","user_type","url"]){if(X.test(Z))continue;const Y=k(J[Z]);if(Y)return Y}for(const[Z,Y]of Object.entries(J)){if(Z==="site"||Z==="logged_in"||X.test(Z))continue;const $=k(Y);if($)return $}return""}function H(G){return G instanceof P||typeof G==="object"&&G!==null&&G.code==="AUTH_REQUIRED"}function A(G,J,X){if(H(X))return{site:G,status:"not_logged_in",logged_in:!1,identity:"",checked:J,error:""};const Z=X instanceof F?X.code:"",Y=M(X);return{site:G,status:"error",logged_in:"",identity:"",checked:J,error:Z?`${Z}: ${Y}`:Y}}function e(G,J){if(G.browser!==!0)return null;let X=G.authStatus?.refresh;if(typeof X!=="function"){const Z=G.authStatus?.quickCheck;if(typeof Z!=="function"||!G.domain)return null;const Y=G.domain.startsWith("http://")||G.domain.startsWith("https://")?G.domain:`https://${G.domain}`;X=async($,D,Q)=>{await $.goto(Y);await $.wait(1);if(v(await Z($,D,Q))!==!0)throw new P(G.domain??G.site,`Auth refresh quickCheck failed for ${G.site}`);return{status:"touched"}}}return z({...G,func:X,navigateBefore:!1,siteSession:"persistent",defaultWindowMode:"background"},J)}function GG(G){if(G&&typeof G==="object"&&!Array.isArray(G)){const J=G;if(J.status==="refreshed"||J.refreshed===!0)return"refreshed"}return"touched"}function JG(G,J,X){if(H(X))return{site:G,status:"not_logged_in",last_touched_at:J?.last_touched_at??"",next_refresh_at:B(J),error:""};const Z=X instanceof F?X.code:"",Y=M(X);return{site:G,status:"error",last_touched_at:J?.last_touched_at??"",next_refresh_at:B(J),error:Z?`${Z}: ${Y}`:Y}}function E(){return process.env.PUBLISHPORT_SITE_SESSION==="ephemeral"?{siteSession:"ephemeral",keepTab:"false"}:{siteSession:"persistent",keepTab:"true"}}async function XG(G,J){try{const X=await O(G);if(X.browser!==!0){const D=z(X,J.timeoutSeconds),Q=await N(D,{timeout:J.timeoutSeconds},!1,{...J.profile?{profile:J.profile}:{}});return{site:G.site,status:"logged_in",logged_in:!0,identity:S(Q),checked:"quick",error:""}}if(X.authStatus?.quickCheckRequiresNavigation===!0)return{site:G.site,status:"unknown",logged_in:"",identity:"",checked:"navigation_required",error:""};const Z=t(X,J.timeoutSeconds);if(!Z)return{site:G.site,status:"unknown",logged_in:"",identity:"",checked:"skipped",error:"quickCheck not implemented; use --full to run whoami"};const Y=await N(Z,{timeout:J.timeoutSeconds},!1,{...E(),windowMode:"background",...J.profile?{profile:J.profile}:{}}),$=v(Y);if($===!0)return{site:G.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};if($===!1)return{site:G.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"quick",error:""};return{site:G.site,status:"unknown",logged_in:"",identity:"",checked:"quick",error:"quickCheck returned no boolean logged_in signal"}}catch(X){return A(G.site,"quick",X)}}async function YG(G,J){try{const X=await O(G),Z=z(X,J.timeoutSeconds),Y=await N(Z,{timeout:J.timeoutSeconds},!1,{...E(),windowMode:"background",...J.profile?{profile:J.profile}:{}});return{site:G.site,status:"logged_in",logged_in:!0,identity:S(Y),checked:"full",error:""}}catch(X){return A(G.site,"full",X)}}function ZG(G,J){return J?`${G}@${J}`:G}async function $G(G,J){const X=ZG(G.site,J.profile),Z=J.state.sites[X];if(!J.force&&r(Z,J.now))return{site:G.site,status:"skipped",last_touched_at:Z?.last_touched_at??"",next_refresh_at:B(Z),error:""};const Y=J.now.toISOString();try{const $=await O(G),D=e($,J.timeoutSeconds);if(!D){J.state.sites[X]={...Z,last_attempt_at:Y,last_status:"unsupported"};return{site:G.site,status:"unsupported",last_touched_at:Z?.last_touched_at??"",next_refresh_at:B(Z),error:"refresh probe is not available for this site"}}const Q=await N(D,{timeout:J.timeoutSeconds},!1,{siteSession:"persistent",keepTab:"true",windowMode:"background",...J.profile?{profile:J.profile}:{}}),K=GG(Q);J.state.sites[X]={...Z,last_attempt_at:Y,last_touched_at:Y,last_status:K};return{site:G.site,status:K,last_touched_at:Y,next_refresh_at:new Date(J.now.getTime()+q).toISOString(),error:""}}catch($){const D=H($)?"not_logged_in":"error";J.state.sites[X]={...Z,last_attempt_at:Y,last_status:D};return JG(G.site,Z,$)}}async function R(G,J){const X=a(J);if(X.kind!=="extension")return null;if(!G.some((Y)=>Y.browser===!0&&!U(Y.site)))return null;try{await i({timeoutSeconds:15,contextId:X.contextId,verbose:!1});return null}catch(Y){return M(Y)}}function DG(G,J,X){return{site:G,status:"error",logged_in:"",identity:"",checked:J,error:`BROWSER_CONNECT: ${X}`}}async function f(G,J,X){const Z=Array(G.length);let Y=0;const $=Array.from({length:Math.min(J,G.length)},async()=>{while(Y<G.length){const D=Y++;Z[D]=await X(G[D])}});await Promise.all($);return Z}export async function collectAuthStatus(G){const J=C(G.sites),X=G.full?"full":"quick",Z=j(G.concurrency,"--concurrency",X==="full"?3:8),Y=j(G.timeout,"--timeout",X==="full"?20:8),$=String(G.only??"all");if(!["all","logged-in","not-logged-in","unknown","error"].includes($))throw new I("--only must be one of: all, logged-in, not-logged-in, unknown, error");const D=b().filter((W)=>!J||J.has(W.site)),Q=await R(D,G.profile),K=await f(D,Z,(W)=>Q&&W.browser===!0&&!U(W.site)?Promise.resolve(DG(W.site,X,Q)):X==="full"?YG(W,{timeoutSeconds:Y,profile:G.profile}):XG(W,{timeoutSeconds:Y,profile:G.profile})),L=$.replace(/-/g,"_");return L==="all"?K:K.filter((W)=>W.status===L)}export async function collectAuthRefresh(G){const J=C(G.sites),X=j(G.concurrency,"--concurrency",3),Z=j(G.timeout,"--timeout",20),Y=G.statePath??m(),$=G.now??new Date,D=await o(Y),Q=b().filter((W)=>!J||J.has(W.site)),K=await R(Q,G.profile),L=await f(Q,X,(W)=>K&&W.browser===!0&&!U(W.site)?Promise.resolve({site:W.site,status:"error",last_touched_at:D.sites[W.site]?.last_touched_at??"",next_refresh_at:B(D.sites[W.site]),error:`BROWSER_CONNECT: ${K}`}):$G(W,{timeoutSeconds:Z,profile:G.profile,now:$,state:D,force:G.all===!0}));await s(Y,D);return L}export function registerAuthCommands(G){const J=G.command("auth").description("Inspect website login status"),X=J.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 p("--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(Y)=>{const $=typeof X.optsWithGlobals==="function"?X.optsWithGlobals():{},D=await collectAuthStatus({sites:Y.site,full:Y.full===!0,concurrency:Y.concurrency,timeout:Y.timeout,only:Y.only,profile:typeof $.profile==="string"&&$.profile.trim()?$.profile.trim():void 0}),Q=typeof Y.format==="string"?Y.format:"table";x(D,{fmt:Q,fmtExplicit:X.getOptionValueSource("format")==="cli",columns:["site","status","identity","checked","error"],title:"opencli/auth status",source:Y.full?"full whoami probe":"quick auth check"})}),Z=J.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(Y)=>{const $=typeof Z.optsWithGlobals==="function"?Z.optsWithGlobals():{},D=await collectAuthRefresh({sites:Y.site,all:Y.all===!0,concurrency:Y.concurrency,timeout:Y.timeout,profile:typeof $.profile==="string"&&$.profile.trim()?$.profile.trim():void 0}),Q=typeof Y.format==="string"?Y.format:"table";x(D,{fmt:Q,fmtExplicit:Z.getOptionValueSource("format")==="cli",columns:["site","status","last_touched_at","next_refresh_at","error"],title:"opencli/auth refresh",source:Y.all?"forced persistent touch":"persistent touch with 24h throttle"})});return J}
@@ -27,10 +27,13 @@ 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
+ /** [pp-only] 标记「快检必须真实导航页面」的站点(jike/discord:登录态在
31
+ * localStorage/IndexedDB,cookie 罐里没有登录信号,走不了免导航快检)。
32
+ * 置为 true 时,后台批量/单站 `auth status`(quick)**不再开页探测**,直接返回
33
+ * status='unknown' / checked='navigation_required'——由调用方(桌面)改成按需探测
34
+ * (用户打开该平台或发布前才 `--full` whoami),彻底消除 20s 轮询反复弹这两个页。
35
+ * 不缓存:`--full` whoami 仍会真实导航,语义与登出即时可见一致。 */
36
+ quickCheckRequiresNavigation?: boolean;
34
37
  }
35
38
  interface BaseCliCommand {
36
39
  site: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.8.5-pp.37",
3
+ "version": "1.8.5-pp.39",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false