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

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
@@ -14689,6 +14689,40 @@
14689
14689
  "sourceFile": "facebook/add-friend.js",
14690
14690
  "navigateBefore": "https://www.facebook.com"
14691
14691
  },
14692
+ {
14693
+ "site": "facebook",
14694
+ "name": "comment",
14695
+ "description": "Post a comment on a Facebook post. Pass the post URL and the comment text (multi-line allowed). The comment is published from your logged-in account — write a genuine, on-topic remark, not filler.",
14696
+ "access": "write",
14697
+ "domain": "facebook.com",
14698
+ "strategy": "cookie",
14699
+ "browser": true,
14700
+ "args": [
14701
+ {
14702
+ "name": "url",
14703
+ "type": "string",
14704
+ "required": true,
14705
+ "positional": true,
14706
+ "help": "Post URL (e.g. https://www.facebook.com/<user>/posts/<id> or a permalink)"
14707
+ },
14708
+ {
14709
+ "name": "text",
14710
+ "type": "string",
14711
+ "required": true,
14712
+ "positional": true,
14713
+ "help": "Comment text (multi-line allowed)"
14714
+ }
14715
+ ],
14716
+ "columns": [
14717
+ "status",
14718
+ "url",
14719
+ "message"
14720
+ ],
14721
+ "type": "js",
14722
+ "modulePath": "facebook/comment.js",
14723
+ "sourceFile": "facebook/comment.js",
14724
+ "navigateBefore": "https://facebook.com"
14725
+ },
14692
14726
  {
14693
14727
  "site": "facebook",
14694
14728
  "name": "events",
@@ -14826,6 +14860,41 @@
14826
14860
  "sourceFile": "facebook/join-group.js",
14827
14861
  "navigateBefore": "https://www.facebook.com"
14828
14862
  },
14863
+ {
14864
+ "site": "facebook",
14865
+ "name": "like",
14866
+ "description": "Like a Facebook post (ends in the liked state; no-op if already liked). Pass the post URL. Reacts as a genuine like from your logged-in account — use it to engage with content you actually mean to endorse.",
14867
+ "access": "write",
14868
+ "domain": "facebook.com",
14869
+ "strategy": "cookie",
14870
+ "browser": true,
14871
+ "args": [
14872
+ {
14873
+ "name": "url",
14874
+ "type": "string",
14875
+ "required": true,
14876
+ "positional": true,
14877
+ "help": "Post URL (e.g. https://www.facebook.com/<user>/posts/<id> or a permalink)"
14878
+ },
14879
+ {
14880
+ "name": "remove",
14881
+ "type": "bool",
14882
+ "default": false,
14883
+ "required": false,
14884
+ "help": "Remove your like instead (end in the un-liked state)"
14885
+ }
14886
+ ],
14887
+ "columns": [
14888
+ "status",
14889
+ "action",
14890
+ "url",
14891
+ "message"
14892
+ ],
14893
+ "type": "js",
14894
+ "modulePath": "facebook/like.js",
14895
+ "sourceFile": "facebook/like.js",
14896
+ "navigateBefore": "https://facebook.com"
14897
+ },
14829
14898
  {
14830
14899
  "site": "facebook",
14831
14900
  "name": "login",
@@ -17330,6 +17399,42 @@
17330
17399
  "modulePath": "hackernews/best.js",
17331
17400
  "sourceFile": "hackernews/best.js"
17332
17401
  },
17402
+ {
17403
+ "site": "hackernews",
17404
+ "name": "comment",
17405
+ "description": "Post a comment on a Hacker News item. Pass the parent id (a story to comment on it, or a comment to reply). Contribute a substantive, on-topic comment — HN flags low-effort or off-topic replies.",
17406
+ "access": "write",
17407
+ "domain": "news.ycombinator.com",
17408
+ "strategy": "cookie",
17409
+ "browser": true,
17410
+ "args": [
17411
+ {
17412
+ "name": "parent",
17413
+ "type": "string",
17414
+ "required": true,
17415
+ "positional": true,
17416
+ "help": "Parent item id (story or comment) to reply to"
17417
+ },
17418
+ {
17419
+ "name": "text",
17420
+ "type": "string",
17421
+ "required": true,
17422
+ "positional": true,
17423
+ "help": "Comment text (HN markup: blank line = paragraph)"
17424
+ }
17425
+ ],
17426
+ "columns": [
17427
+ "status",
17428
+ "id",
17429
+ "url",
17430
+ "parent",
17431
+ "message"
17432
+ ],
17433
+ "type": "js",
17434
+ "modulePath": "hackernews/comment.js",
17435
+ "sourceFile": "hackernews/comment.js",
17436
+ "navigateBefore": "https://news.ycombinator.com"
17437
+ },
17333
17438
  {
17334
17439
  "site": "hackernews",
17335
17440
  "name": "jobs",
@@ -17552,6 +17657,47 @@
17552
17657
  "modulePath": "hackernews/show.js",
17553
17658
  "sourceFile": "hackernews/show.js"
17554
17659
  },
17660
+ {
17661
+ "site": "hackernews",
17662
+ "name": "submit",
17663
+ "description": "Submit a story to Hacker News. Link post by default (--url), or an Ask/text post with --text. HN gates submissions on account age/karma and dedupes URLs — rejections are surfaced verbatim. Post genuinely worthwhile content; low-effort submissions hurt the account.",
17664
+ "access": "write",
17665
+ "domain": "news.ycombinator.com",
17666
+ "strategy": "cookie",
17667
+ "browser": true,
17668
+ "args": [
17669
+ {
17670
+ "name": "title",
17671
+ "type": "string",
17672
+ "required": true,
17673
+ "positional": true,
17674
+ "help": "Story title (max 80 chars)"
17675
+ },
17676
+ {
17677
+ "name": "url",
17678
+ "type": "string",
17679
+ "required": false,
17680
+ "help": "External URL for a link post (mutually exclusive with --text)"
17681
+ },
17682
+ {
17683
+ "name": "text",
17684
+ "type": "string",
17685
+ "required": false,
17686
+ "help": "Body text for an Ask HN / text post (mutually exclusive with --url)"
17687
+ }
17688
+ ],
17689
+ "columns": [
17690
+ "status",
17691
+ "id",
17692
+ "url",
17693
+ "title",
17694
+ "message"
17695
+ ],
17696
+ "type": "js",
17697
+ "modulePath": "hackernews/submit.js",
17698
+ "sourceFile": "hackernews/submit.js",
17699
+ "navigateBefore": "https://news.ycombinator.com"
17700
+ },
17555
17701
  {
17556
17702
  "site": "hackernews",
17557
17703
  "name": "top",
@@ -20751,31 +20897,26 @@
20751
20897
  "positional": true,
20752
20898
  "help": "图片描述 prompt"
20753
20899
  },
20754
- {
20755
- "name": "model",
20756
- "type": "string",
20757
- "default": "high_aes_general_v50",
20758
- "required": false,
20759
- "help": "模型: high_aes_general_v50 (5.0 Lite), high_aes_general_v42 (4.6), high_aes_general_v40 (4.0)"
20760
- },
20761
20900
  {
20762
20901
  "name": "wait",
20763
20902
  "type": "int",
20764
- "default": 40,
20903
+ "default": 60,
20765
20904
  "required": false,
20766
- "help": "等待生成完成的秒数"
20905
+ "help": "等待生成完成的最长秒数(每 3 秒轮询一次,超时会返回 pending 并提示用 history 回查)"
20767
20906
  }
20768
20907
  ],
20769
20908
  "columns": [
20770
20909
  "status",
20771
20910
  "prompt",
20911
+ "model",
20772
20912
  "image_count",
20773
20913
  "image_urls"
20774
20914
  ],
20775
20915
  "type": "js",
20776
20916
  "modulePath": "jimeng/generate.js",
20777
20917
  "sourceFile": "jimeng/generate.js",
20778
- "navigateBefore": "https://jimeng.jianying.com"
20918
+ "navigateBefore": false,
20919
+ "siteSession": "persistent"
20779
20920
  },
20780
20921
  {
20781
20922
  "site": "jimeng",
@@ -1,11 +1,10 @@
1
- import{AuthRequiredError as t,CommandExecutionError as a}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as m}from"../_shared/site-auth.js";async function o(n){return(await n.getCookies({url:"https://www.dianping.com"})).some((i)=>i.name==="dper"&&i.value)}async function r(n){if(!await o(n))throw new t("dianping.com","Dianping dper cookie missing");await n.goto("https://www.dianping.com/member/myinformation");await n.wait(2);const e=await n.evaluate("location.href");if(/account\.dianping\.com\/(pc)?login/.test(String(e||"")))throw new t("dianping.com",`Dianping member page redirected to login: ${e}`);const i=await n.evaluate(`
1
+ import{AuthRequiredError as F,CommandExecutionError as K}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as L}from"../_shared/site-auth.js";async function H(z){return(await z.getCookies({url:"https://www.dianping.com"})).some((B)=>B.name==="dper"&&B.value)}async function J(z){if(!await H(z))throw new F("dianping.com","Dianping dper cookie missing");await z.goto("https://www.dianping.com/citylist");await z.wait(2);const G=await z.evaluate("location.href");if(/account\.dianping\.com\/(pc)?login/.test(String(G||"")))throw new F("dianping.com",`Dianping page redirected to login: ${G}`);const B=await z.evaluate(`
2
2
  (() => {
3
- const nicknameEl = document.querySelector('.user-name, .username, .nickname, .user-info .name');
4
- const nickname = (nicknameEl?.textContent || '').trim();
5
- const profileLink = Array.from(document.querySelectorAll('a[href*="/member/"]'))
6
- .map(a => a.getAttribute('href') || '')
7
- .find(h => /\\/member\\/\\d+/.test(h));
8
- const uidMatch = String(profileLink || '').match(/\\/member\\/(\\d+)/);
9
- return { user_id: uidMatch?.[1] || '', nickname };
3
+ const trigger = document.querySelector('.userinfo-container .J-user-trigger, .userinfo-container a.username');
4
+ if (!trigger) return { loggedOut: true };
5
+ const nickname = (trigger.textContent || '').trim();
6
+ const href = trigger.getAttribute('href') || '';
7
+ const uid = (String(href).match(/\\/member\\/(\\d+)/) || [])[1] || '';
8
+ return { user_id: uid, nickname };
10
9
  })()
11
- `);if(!i?.user_id)throw new a("Dianping member page rendered but no user_id link found stale dper or layout drift");return{user_id:String(i.user_id),nickname:String(i.nickname||"")}}m({site:"dianping",domain:"dianping.com",loginUrl:"https://account.dianping.com/pclogin",columns:["user_id","nickname"],quickCheck:o,verify:r,poll:async(n)=>{if(!await o(n))throw new t("dianping.com","Waiting for Dianping dper cookie");return r(n)}});
10
+ `);if(B?.loggedOut)throw new F("dianping.com","Dianping session anonymous no logged-in user trigger in top nav");if(!B?.user_id)throw new K("Dianping user trigger found but no /member/<id> href layout drift");return{user_id:String(B.user_id),nickname:String(B.nickname||"")}}L({site:"dianping",domain:"dianping.com",loginUrl:"https://account.dianping.com/pclogin",columns:["user_id","nickname"],quickCheck:H,verify:J,poll:async(z)=>{if(!await H(z))throw new F("dianping.com","Waiting for Dianping dper cookie");return J(z)}});
@@ -1 +1,4 @@
1
- import{ArgumentError as $,CommandExecutionError as B}from"@jackwener/opencli/errors";import{CITY_ID as J,resolveCityId as j}from"./utils.js";const b=/^[一-龥]+$/,v=/^[a-z]+$/,Z=new Map;export function clearCityResolverCache(){Z.clear()}export async function resolveCityIdAsync(K,Q){if(Q==null||Q==="")return null;const q=String(Q).trim();if(!q)return null;if(/^\d+$/.test(q))return Number(q);const W=q.toLowerCase();try{const G=j(q);if(G!=null)return G}catch(G){if(G?.code!=="ARGUMENT")throw G}if(Z.has(W))return Z.get(W);if(Z.has(q))return Z.get(q);let z=null;if(v.test(W))z=W;else if(b.test(q)){z=await F(K,q);if(!z){const G=Object.keys(J).filter((X)=>/^[a-z]+$/.test(X)).join(", ");throw new $("city",`unknown city '${Q}'. pass a numeric cityId, a pinyin slug (e.g. shantou), a Chinese name listed on dianping.com/citylist, or one of: ${G}`)}}else{const G=Object.keys(J).filter((X)=>/^[a-z]+$/.test(X)).join(", ");throw new $("city",`unknown city '${Q}'. pass a numeric cityId, a pinyin slug (e.g. shantou), a Chinese name listed on dianping.com/citylist, or one of: ${G}`)}const U=await T(K,z);if(!U)throw new B(`dianping could not resolve cityId for '${Q}' (pinyin=${z}); the city page rendered without a /search/keyword/{id}/ link`);Z.set(W,U);Z.set(z,U);if(b.test(q))Z.set(q,U);return U}async function F(K,Q){await K.goto("https://www.dianping.com/citylist");const q=await K.evaluate(`(${buildCitylistMap.toString()})()`);if(!q||typeof q!=="object"||Object.keys(q).length===0)throw new B("dianping citylist did not render any city anchors; cannot resolve Chinese city names");if(q&&typeof q==="object"&&q[Q])return String(q[Q]).toLowerCase();return null}export function buildCitylistMap(){const K={};document.querySelectorAll("a").forEach((q)=>{const W=q.getAttribute("href")||"",z=(q.textContent||"").trim();if(!z||!/^[一-龥]+$/.test(z))return;const U=W.replace(/^https?:/,""),G=U.match(/^\/\/(?:www\.)?dianping\.com\/([a-z]+)\/?$/)||U.match(/^\/([a-z]+)\/?$/);if(!G)return;const X=G[1].toLowerCase();if(X==="citylist"||X==="promo"||X==="events")return;if(!K[z])K[z]=X});return K}async function T(K,Q){await K.goto(`https://www.dianping.com/${Q}`);const q=await K.evaluate(`(${extractCityIdFromPage.toString()})()`);return Number.isInteger(q)&&q>0?q:null}export function extractCityIdFromPage(){const K=typeof location<"u"&&location.href||"https://www.dianping.com/",Q=document.querySelectorAll("a[href]");for(const q of Q){const W=q.getAttribute("href")||"";let z;try{z=new URL(W,K)}catch{continue}if(z.protocol!=="https:")continue;if(z.hostname!=="www.dianping.com"&&z.hostname!=="dianping.com")continue;const U=z.pathname.match(/^\/search\/keyword\/(\d+)(?:\/|$)/);if(!U)continue;const G=Number(U[1]);if(Number.isInteger(G)&&G>0)return G}return null}
1
+ import{ArgumentError as B,CommandExecutionError as J}from"@jackwener/opencli/errors";import{CITY_ID as b,resolveCityId as j}from"./utils.js";const $=/^[一-龥]+$/,F=/^[a-z]+$/,Z=new Map;export function clearCityResolverCache(){Z.clear()}export async function resolveCityIdAsync(z,G){if(G==null||G==="")return null;const q=String(G).trim();if(!q)return null;if(/^\d+$/.test(q))return Number(q);const W=q.toLowerCase();try{const Q=j(q);if(Q!=null)return Q}catch(Q){if(Q?.code!=="ARGUMENT")throw Q}if(Z.has(W))return Z.get(W);if(Z.has(q))return Z.get(q);let K=null;if(F.test(W))K=W;else if($.test(q)){K=await T(z,q);if(!K){const Q=Object.keys(b).filter((X)=>/^[a-z]+$/.test(X)).join(", ");throw new B("city",`unknown city '${G}'. pass a numeric cityId, a pinyin slug (e.g. shantou), a Chinese name listed on dianping.com/citylist, or one of: ${Q}`)}}else{const Q=Object.keys(b).filter((X)=>/^[a-z]+$/.test(X)).join(", ");throw new B("city",`unknown city '${G}'. pass a numeric cityId, a pinyin slug (e.g. shantou), a Chinese name listed on dianping.com/citylist, or one of: ${Q}`)}const U=await v(z,K);if(!U)throw new J(`dianping could not resolve cityId for '${G}' (pinyin=${K}); the city page rendered without a /search/keyword/{id}/ link`);Z.set(W,U);Z.set(K,U);if($.test(q))Z.set(q,U);return U}export async function resolveDefaultCityId(z){await z.goto("https://www.dianping.com/");await z.wait(1);const G=await z.evaluate(`(() => {
2
+ const el = document.querySelector('.city-select-text, .city-select-content');
3
+ return el ? (el.textContent || '').trim() : '';
4
+ })()`);if(!G||!$.test(G))return null;return resolveCityIdAsync(z,G)}async function T(z,G){await z.goto("https://www.dianping.com/citylist");const q=await z.evaluate(`(${buildCitylistMap.toString()})()`);if(!q||typeof q!=="object"||Object.keys(q).length===0)throw new J("dianping citylist did not render any city anchors; cannot resolve Chinese city names");if(q&&typeof q==="object"&&q[G])return String(q[G]).toLowerCase();return null}export function buildCitylistMap(){const z={};document.querySelectorAll("a").forEach((q)=>{const W=q.getAttribute("href")||"",K=(q.textContent||"").trim();if(!K||!/^[一-龥]+$/.test(K))return;const U=W.replace(/^https?:/,""),Q=U.match(/^\/\/(?:www\.)?dianping\.com\/([a-z]+)\/?$/)||U.match(/^\/([a-z]+)\/?$/);if(!Q)return;const X=Q[1].toLowerCase();if(X==="citylist"||X==="promo"||X==="events")return;if(!z[K])z[K]=X});return z}async function v(z,G){await z.goto(`https://www.dianping.com/${G}`);const q=await z.evaluate(`(${extractCityIdFromPage.toString()})()`);return Number.isInteger(q)&&q>0?q:null}export function extractCityIdFromPage(){const z=typeof location<"u"&&location.href||"https://www.dianping.com/",G=document.querySelectorAll("a[href]");for(const q of G){const W=q.getAttribute("href")||"";let K;try{K=new URL(W,z)}catch{continue}if(K.protocol!=="https:")continue;if(K.hostname!=="www.dianping.com"&&K.hostname!=="dianping.com")continue;const U=K.pathname.match(/^\/search\/keyword\/(\d+)(?:\/|$)/);if(!U)continue;const Q=Number(U[1]);if(Number.isInteger(Q)&&Q>0)return Q}return null}
@@ -1 +1 @@
1
- import{cli as U,Strategy as W}from"@jackwener/opencli/registry";import{ArgumentError as M,CommandExecutionError as $}from"@jackwener/opencli/errors";import{SEARCH_COLUMNS as P,detectAuthOrPageFailure as O,parsePrice as j,parseReviewCount as x,requireSearchLimit as D,wrapDianpingStep as H}from"./utils.js";import{resolveCityIdAsync as A}from"./cityResolver.js";export function extractSearchRows(){const K=document.querySelectorAll("#shop-all-list li");if(K.length===0){const z=document.body;return{ok:!1,sample:(z&&(z.innerText||z.textContent)||"").slice(0,800),url:location.href}}const V=[];K.forEach((z,f)=>{if(!(z.querySelector(".tit h4, .review-num, .mean-price, .tag-addr")||z.querySelector('.tit a, a[data-shopid], a[href*="/shop/"]')))return;const Q=z.querySelector('.tit a[href*="/shop/"]')||z.querySelector("a[data-shopid]")||z.querySelector('a[href*="/shop/"]'),X=Q&&Q.getAttribute("data-shopid")||Q&&(Q.getAttribute("href")||"").match(/\/shop\/([^?#/]+)/)?.[1]||"",G=z.querySelector(".tit h4"),Z=G&&G.textContent&&G.textContent.trim()||Q&&Q.getAttribute("title")||"",B=z.querySelector(".review-num b"),L=B&&B.textContent&&B.textContent.trim()||"",F=z.querySelector(".mean-price b"),T=F&&F.textContent&&F.textContent.trim()||"",J=Array.from(z.querySelectorAll(".tag-addr .tag")).map((Y)=>Y.textContent.trim()).filter(Boolean);let N="";const _=z.querySelector(".nebula_star");if(_){const Y=_.outerHTML.match(/star_(\d{2})/g);if(Y&&Y.length)N=Y[Y.length-1].match(/star_(\d{2})/)[1]}V.push({rank:f+1,shop_id:X,name:Z,starClass:N,reviewsRaw:L,priceRaw:T,cuisine:J[0]||"",district:J[1]||"",url:X?"https://www.dianping.com/shop/"+X:""})});return{ok:!0,rows:V}}U({site:"dianping",name:"search",access:"read",description:"大众点评店铺搜索(按关键词 + 城市)",domain:"www.dianping.com",strategy:W.COOKIE,args:[{name:"keyword",required:!0,positional:!0,help:'搜索关键词,例如 "火锅"'},{name:"city",help:"城市名(北京/上海/汕头/beijing/shantou/...)或 cityId 数字。未在静态表中的城市会通过 dianping.com 在线解析。不传则使用 cookie 默认城市"},{name:"limit",type:"int",default:15,help:"返回的店铺数量(最多 15,dianping 单页固定 15 条)"}],columns:P,func:async(K,V)=>{const z=String(V.keyword||"").trim();if(!z)throw new M("keyword","must be a non-empty string");const f=D(V.limit),q=await H("city resolve",()=>A(K,V.city)),X=`https://www.dianping.com${q?`/search/keyword/${q}/0_${encodeURIComponent(z)}`:`/search/keyword/0/0_${encodeURIComponent(z)}`}`;await H(`search "${z}" navigation`,async()=>{await K.goto(X);await K.wait(2)});const G=await H(`search "${z}" extraction`,()=>K.evaluate(`(${extractSearchRows.toString()})()`));if(!G||!G.ok)O({text:String(G?.sample||""),url:String(G?.url||X)},`search "${z}"`,{emptyPatterns:[/没有找到|暂无结果|暂无商户|换个关键词|未找到相关/i]});const Z=(G.rows||[]).slice(0,f);if(Z.length===0)throw new $("dianping search parser found no result-shaped shop cards");if(Z.some((B)=>!B?.shop_id))throw new $("dianping search parser found result cards without shop_id values");return Z.map((B)=>({rank:B.rank,shop_id:B.shop_id,name:B.name,rating:B.starClass?Number((Number(B.starClass)/10).toFixed(1)):null,reviews:x(B.reviewsRaw),price:j(B.priceRaw),cuisine:B.cuisine,district:B.district,url:B.url}))}});
1
+ import{cli as L,Strategy as O}from"@jackwener/opencli/registry";import{ArgumentError as W,CommandExecutionError as F}from"@jackwener/opencli/errors";import{SEARCH_COLUMNS as b,detectAuthOrPageFailure as P,parsePrice as j,parseReviewCount as x,requireSearchLimit as f,wrapDianpingStep as $}from"./utils.js";import{resolveCityIdAsync as D,resolveDefaultCityId as A}from"./cityResolver.js";export function extractSearchRows(){const J=document.querySelectorAll("#shop-all-list li");if(J.length===0){const z=document.body;return{ok:!1,sample:(z&&(z.innerText||z.textContent)||"").slice(0,800),url:location.href}}const Q=[];J.forEach((z,Z)=>{if(!(z.querySelector(".tit h4, .review-num, .mean-price, .tag-addr")||z.querySelector('.tit a, a[data-shopid], a[href*="/shop/"]')))return;const K=z.querySelector('.tit a[href*="/shop/"]')||z.querySelector("a[data-shopid]")||z.querySelector('a[href*="/shop/"]'),T=K&&K.getAttribute("data-shopid")||K&&(K.getAttribute("href")||"").match(/\/shop\/([^?#/]+)/)?.[1]||"",G=z.querySelector(".tit h4"),Y=G&&G.textContent&&G.textContent.trim()||K&&K.getAttribute("title")||"",B=z.querySelector(".review-num b"),U=B&&B.textContent&&B.textContent.trim()||"",q=z.querySelector(".mean-price b"),_=q&&q.textContent&&q.textContent.trim()||"",H=Array.from(z.querySelectorAll(".tag-addr .tag")).map((V)=>V.textContent.trim()).filter(Boolean);let M="";const N=z.querySelector(".nebula_star");if(N){const V=N.outerHTML.match(/star_(\d{2})/g);if(V&&V.length)M=V[V.length-1].match(/star_(\d{2})/)[1]}Q.push({rank:Z+1,shop_id:T,name:Y,starClass:M,reviewsRaw:U,priceRaw:_,cuisine:H[0]||"",district:H[1]||"",url:T?"https://www.dianping.com/shop/"+T:""})});return{ok:!0,rows:Q}}L({site:"dianping",name:"search",access:"read",description:"大众点评店铺搜索(按关键词 + 城市)",domain:"www.dianping.com",strategy:O.COOKIE,args:[{name:"keyword",required:!0,positional:!0,help:'搜索关键词,例如 "火锅"'},{name:"city",help:"城市名(北京/上海/汕头/beijing/shantou/...)或 cityId 数字。未在静态表中的城市会通过 dianping.com 在线解析。不传则使用 cookie 默认城市"},{name:"limit",type:"int",default:15,help:"返回的店铺数量(最多 15,dianping 单页固定 15 条)"}],columns:b,func:async(J,Q)=>{const z=String(Q.keyword||"").trim();if(!z)throw new W("keyword","must be a non-empty string");const Z=f(Q.limit);let X=await $("city resolve",()=>D(J,Q.city));if(X==null)X=await $("default city resolve",()=>A(J));if(X==null)throw new F("dianping search 需要城市:未传 --city 且无法确定默认城市。请显式指定,例如 --city 上海");const T=`https://www.dianping.com${`/search/keyword/${X}/0_${encodeURIComponent(z)}`}`;await $(`search "${z}" navigation`,async()=>{await J.goto(T);await J.wait(2)});const G=await $(`search "${z}" extraction`,()=>J.evaluate(`(${extractSearchRows.toString()})()`));if(!G||!G.ok)P({text:String(G?.sample||""),url:String(G?.url||T)},`search "${z}"`,{emptyPatterns:[/没有找到|暂无结果|暂无商户|换个关键词|未找到相关/i]});const Y=(G.rows||[]).slice(0,Z);if(Y.length===0)throw new F("dianping search parser found no result-shaped shop cards");if(Y.some((B)=>!B?.shop_id))throw new F("dianping search parser found result cards without shop_id values");return Y.map((B)=>({rank:B.rank,shop_id:B.shop_id,name:B.name,rating:B.starClass?Number((Number(B.starClass)/10).toFixed(1)):null,reviews:x(B.reviewsRaw),price:j(B.priceRaw),cuisine:B.cuisine,district:B.district,url:B.url}))}});
@@ -0,0 +1,18 @@
1
+ import{ArgumentError as H,AuthRequiredError as Z,CommandExecutionError as J}from"@jackwener/opencli/errors";import{cli as _,Strategy as $}from"@jackwener/opencli/registry";const Y='[data-lexical-editor="true"][contenteditable="true"]',K='[data-pp-fb-composer="1"]';_({site:"facebook",name:"comment",access:"write",description:"Post a comment on a Facebook post. Pass the post URL and the comment text (multi-line allowed). The comment is published from your logged-in account — write a genuine, on-topic remark, not filler.",domain:"facebook.com",strategy:$.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Post URL (e.g. https://www.facebook.com/<user>/posts/<id> or a permalink)"},{name:"text",type:"string",required:!0,positional:!0,help:"Comment text (multi-line allowed)"}],columns:["status","url","message"],func:async(v,U)=>{if(!v)throw new J("Browser session required");const B=String(U.url??"").trim();if(!B)throw new H("Post URL is required.");let L;try{L=new URL(B)}catch{throw new H(`Invalid post URL: ${B}`)}if(!/facebook\.com$/.test(L.hostname)&&!L.hostname.endsWith(".facebook.com"))throw new H("URL must be a facebook.com post URL.");const G=String(U.text??"");if(!G.trim())throw new H("Comment text cannot be empty.");await v.goto(B,{waitUntil:"none"});let V=!1;for(let z=0;z<15;z++){const X=await v.evaluate(`(() => {
2
+ const path = location.pathname || '';
3
+ if (/^\\/(login|checkpoint)(\\/|$|\\.php)/.test(path)) return 'login';
4
+ const dialog = document.querySelector('[role="dialog"]');
5
+ const scope = dialog || document;
6
+ const ed = scope.querySelector('${Y}') || document.querySelector('${Y}');
7
+ if (!ed) return 'wait';
8
+ ed.setAttribute('data-pp-fb-composer', '1');
9
+ ed.scrollIntoView({ block: 'center' });
10
+ return 'ready';
11
+ })()`);if(X==="login")throw new Z("www.facebook.com","Facebook redirected to login/checkpoint — not signed in");if(X==="ready"){V=!0;break}await v.wait(1)}if(!V)throw new J("Comment box not found — the post may be unavailable, or commenting is disabled.");await v.click(K);await v.wait(1);await v.pressKey("Control+A");await v.pressKey("Delete");const N=G.split(`
12
+ `);for(let z=0;z<N.length;z++){if(z>0)await v.pressKey("Shift+Enter");if(N[z].length)await v.typeText(K,N[z])}if(await v.evaluate(`(() => !!document.querySelector('[role="listbox"][aria-label], ul[role="listbox"]'))()`))await v.pressKey("Escape");const F=(z)=>String(z||"").replace(/\s+/g," ").trim(),Q=await v.evaluate(`(() => {
13
+ const ed = document.querySelector('${K}');
14
+ return ed ? (ed.textContent || '') : null;
15
+ })()`);if(Q===null||F(Q)!==F(G))throw new J(`Composed text did not match before send (got ${JSON.stringify((Q||"").slice(0,60))}).`);await v.pressKey("Enter");await v.wait(2);const W=await v.evaluate(`(() => {
16
+ const ed = document.querySelector('${K}');
17
+ return ed ? (ed.textContent || '') : '';
18
+ })()`);if(F(W).length>0&&F(W)===F(G))throw new J("Comment still in the box after Enter — submit blocked (rate limit, restricted post, or no permission to comment).");return[{status:"success",url:B,message:"Comment posted."}]}});
@@ -0,0 +1,29 @@
1
+ import{ArgumentError as Z,AuthRequiredError as y,CommandExecutionError as V}from"@jackwener/opencli/errors";import{cli as N,Strategy as R}from"@jackwener/opencli/registry";const $="data-pp-fb-like";function U(){return`(() => {
2
+ const path = location.pathname || '';
3
+ if (/^\\/(login|checkpoint)(\\/|$|\\.php)/.test(path)) return { auth: true, detail: 'on login/checkpoint page' };
4
+ // A permalink often renders as a modal dialog over the feed. Scope to the
5
+ // dialog so we don't grab a background feed post's Like button.
6
+ const dialog = document.querySelector('[role="dialog"]');
7
+ const art = (dialog && dialog.querySelector('div[role="article"]'))
8
+ || dialog
9
+ || document.querySelector('div[role="article"]');
10
+ if (!art) return { notfound: true, detail: 'no post found on page' };
11
+ // Action buttons = focusable role=button carrying a css-img icon, not part
12
+ // of the reaction-count toolbar. The first one is Like.
13
+ const btns = [...art.querySelectorAll('[role="button"][tabindex="0"]')]
14
+ .filter(b => !b.closest('[role="toolbar"]') && b.querySelector('i[data-visualcompletion="css-img"]'));
15
+ const like = btns[0];
16
+ if (!like) return { notfound: true, detail: 'like button not located' };
17
+ like.setAttribute(${JSON.stringify($)}, '1');
18
+ like.scrollIntoView({ block: 'center' });
19
+ const icon = like.querySelector('i[data-visualcompletion="css-img"]');
20
+ const y = parseFloat((getComputedStyle(icon).backgroundPosition.split(' ')[1] || '0').replace('px','')) || 0;
21
+ return { y, label: like.getAttribute('aria-label') || '' };
22
+ })()`}function G(){return`(() => {
23
+ const like = document.querySelector('[${$}="1"]');
24
+ if (!like) return null;
25
+ const icon = like.querySelector('i[data-visualcompletion="css-img"]');
26
+ if (!icon) return null;
27
+ const y = parseFloat((getComputedStyle(icon).backgroundPosition.split(' ')[1] || '0').replace('px','')) || 0;
28
+ return { y, label: like.getAttribute('aria-label') || '' };
29
+ })()`}async function v(z,O,H){for(let J=0;J<H;J++){await z.click(`[${$}="1"]`);await z.wait(1);const M=await z.evaluate(G());if(M&&M.y!==O)return M}return null}N({site:"facebook",name:"like",access:"write",description:"Like a Facebook post (ends in the liked state; no-op if already liked). Pass the post URL. Reacts as a genuine like from your logged-in account — use it to engage with content you actually mean to endorse.",domain:"facebook.com",strategy:R.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Post URL (e.g. https://www.facebook.com/<user>/posts/<id> or a permalink)"},{name:"remove",type:"bool",default:!1,help:"Remove your like instead (end in the un-liked state)"}],columns:["status","action","url","message"],func:async(z,O)=>{if(!z)throw new V("Browser session required");const H=String(O.url??"").trim();if(!H)throw new Z("Post URL is required.");let J;try{J=new URL(H)}catch{throw new Z(`Invalid post URL: ${H}`)}if(!/facebook\.com$/.test(J.hostname)&&!J.hostname.endsWith(".facebook.com"))throw new Z("URL must be a facebook.com post URL.");const M=O.remove===!0;await z.goto(H,{waitUntil:"none"});let B=null;for(let X=0;X<15;X++){await z.wait(1);B=await z.evaluate(U());if(B?.auth)throw new y("www.facebook.com",`Facebook ${B.detail} — not signed in`);if(typeof B?.y==="number")break}if(typeof B?.y!=="number")throw new V(`Could not act on this post: ${B?.detail||"like button never appeared"}.`);const P=await v(z,B.y,5);if(!P)throw new V("Like did not register — Facebook ignored the click (post may be unavailable or reactions disabled).");const q=Math.max(B.y,P.y),D=P.y===q,j=B.y===q,W=!M;let Q;if(D===W)Q=W?j?"already-liked":"liked":"unliked";else{if(!await v(z,P.y,3))throw new V("Could not settle the reaction to the desired state (second toggle ignored).");Q=W?"already-liked":"already-unliked"}return[{status:"success",action:Q,url:H,message:{liked:"Post liked.","already-liked":"Post was already liked (left liked).",unliked:"Like removed.","already-unliked":"Post was not liked (left un-liked)."}[Q]||"Reaction updated."}]}});
@@ -0,0 +1,61 @@
1
+ import{ArgumentError as Q,AuthRequiredError as T,CommandExecutionError as L}from"@jackwener/opencli/errors";import{cli as U,Strategy as V}from"@jackwener/opencli/registry";const M="https://news.ycombinator.com";export function normalizeItemId(F){const J=String(F||"").trim();if(!J)throw new Q("Item id is required.","Pass an HN item id (story or comment), e.g. 48811591.");if(/^\d+$/.test(J))return J;const B=J.match(/[?&]id=(\d+)/);if(B)return B[1];throw new Q(`Invalid HN item reference: ${JSON.stringify(J)}`,"Pass a numeric id or an item?id=… URL.")}U({site:"hackernews",name:"comment",access:"write",description:"Post a comment on a Hacker News item. Pass the parent id (a story to comment on it, or a comment to reply). Contribute a substantive, on-topic comment — HN flags low-effort or off-topic replies.",domain:"news.ycombinator.com",strategy:V.COOKIE,browser:!0,args:[{name:"parent",type:"string",required:!0,positional:!0,help:"Parent item id (story or comment) to reply to"},{name:"text",type:"string",required:!0,positional:!0,help:"Comment text (HN markup: blank line = paragraph)"}],columns:["status","id","url","parent","message"],func:async(F,J)=>{if(!F)throw new L("Browser session required");const B=normalizeItemId(J.parent),P=String(J.text??"");if(!P.trim())throw new Q("Comment text cannot be empty.");await F.goto(`${M}/item?id=${B}`);await F.wait(1);const D=await F.evaluate(`(async () => {
2
+ try {
3
+ if (/\\/login/.test(location.pathname)) return { kind: 'auth', detail: 'redirected to /login' };
4
+ // The reply form posts to "comment" (relative). Match it by the hidden
5
+ // fields it carries rather than the action attribute, which HN writes
6
+ // relative ("comment") so /comment suffix selectors miss it.
7
+ const forms = [...document.querySelectorAll('form')].filter(f => f.querySelector('input[name="hmac"]') && f.querySelector('input[name="parent"]'));
8
+ const form = forms.find(f => (f.querySelector('input[name="parent"]')?.value) === ${JSON.stringify(B)}) || forms[0];
9
+ if (!form) return { kind: 'notfound', detail: 'No comment form on this item — it may be closed, deleted, or you are signed out.' };
10
+ const hmac = form.querySelector('input[name="hmac"]')?.value;
11
+ const goto = form.querySelector('input[name="goto"]')?.value || ('item?id=' + ${JSON.stringify(B)});
12
+ const parentVal = form.querySelector('input[name="parent"]')?.value || ${JSON.stringify(B)};
13
+ if (!hmac) return { kind: 'auth', detail: 'comment hmac token missing — likely signed out' };
14
+
15
+ const p = new URLSearchParams();
16
+ p.set('parent', parentVal);
17
+ p.set('goto', goto);
18
+ p.set('hmac', hmac);
19
+ p.set('text', ${JSON.stringify(P)});
20
+
21
+ const res = await fetch('/comment', {
22
+ method: 'POST',
23
+ credentials: 'include',
24
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
25
+ body: p.toString(),
26
+ redirect: 'manual',
27
+ });
28
+ if (res.type === 'opaqueredirect' || res.status === 302 || res.status === 0) {
29
+ return { ok: true };
30
+ }
31
+ const body = await res.text();
32
+ let msg = '';
33
+ if (/Please try again/i.test(body)) msg = 'HN asked to try again (possible rate limit or expired token).';
34
+ else if (/You're posting too fast|too fast/i.test(body)) msg = 'Posting too fast — HN rate limited the comment.';
35
+ else {
36
+ const td = body.match(/<td[^>]*>\\s*([^<][^<]{0,200}?)\\s*<\\/td>/i);
37
+ if (td) msg = td[1].replace(/\\s+/g, ' ').trim();
38
+ }
39
+ return { kind: 'reject', detail: msg || ('HTTP ' + res.status + ' without redirect'), status: res.status };
40
+ } catch (e) {
41
+ return { kind: 'exception', detail: String((e && e.message) || e) };
42
+ }
43
+ })()`);if(D?.kind==="auth")throw new T("news.ycombinator.com",D.detail);if(D?.kind==="notfound")throw new L(D.detail);if(D?.kind==="exception")throw new L(`HN comment failed: ${D.detail}`);if(D?.kind==="reject")return[{status:"failed",id:"",url:"",parent:B,message:D.detail}];if(!D?.ok)throw new L(`Unexpected HN comment result: ${JSON.stringify(D)}`);await F.goto(`${M}/item?id=${B}`);await F.wait(1);const K=(await F.evaluate(`(() => {
44
+ try {
45
+ let username = '';
46
+ const navLink = document.querySelector('span.pagetop a[href^="user?id="]');
47
+ if (navLink) username = (navLink.getAttribute('href') || '').replace('user?id=', '');
48
+ const want = ${JSON.stringify(P)}.replace(/\\s+/g, ' ').trim();
49
+ const rows = [...document.querySelectorAll('tr.athing.comtr')];
50
+ // Newest first: our fresh comment is usually near the top of its thread.
51
+ for (const row of rows) {
52
+ const by = (row.querySelector('a.hnuser')?.textContent || '').trim();
53
+ if (username && by !== username) continue;
54
+ const commentText = (row.querySelector('.commtext')?.textContent || '').replace(/\\s+/g, ' ').trim();
55
+ if (commentText.startsWith(want.slice(0, 60))) {
56
+ return { id: row.id };
57
+ }
58
+ }
59
+ return null;
60
+ } catch { return null; }
61
+ })()`))?.id||"";return[{status:"success",id:K,url:K?`${M}/item?id=${K}`:`${M}/item?id=${B}`,parent:B,message:K?"Comment posted":"Comment posted (id not confirmed on reload)"}]}});
@@ -0,0 +1,65 @@
1
+ import{ArgumentError as F,AuthRequiredError as Q,CommandExecutionError as L}from"@jackwener/opencli/errors";import{cli as T,Strategy as V}from"@jackwener/opencli/registry";const M="https://news.ycombinator.com";T({site:"hackernews",name:"submit",access:"write",description:"Submit a story to Hacker News. Link post by default (--url), or an Ask/text post with --text. HN gates submissions on account age/karma and dedupes URLs — rejections are surfaced verbatim. Post genuinely worthwhile content; low-effort submissions hurt the account.",domain:"news.ycombinator.com",strategy:V.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!0,positional:!0,help:"Story title (max 80 chars)"},{name:"url",type:"string",help:"External URL for a link post (mutually exclusive with --text)"},{name:"text",type:"string",help:"Body text for an Ask HN / text post (mutually exclusive with --url)"}],columns:["status","id","url","title","message"],func:async(v,D)=>{if(!v)throw new L("Browser session required");const z=String(D.title??"").trim();if(!z)throw new F("Title is required.");if(z.length>80)throw new F("HN titles must be at most 80 characters.");const B=typeof D.url==="string"?D.url.trim():"",K=typeof D.text==="string"?D.text:"";if(B&&K)throw new F("Pass either --url (link post) or --text (text post), not both.");if(!B&&!K)throw new F("A story needs either --url or --text.","Give a link to submit, or --text for an Ask HN post.");if(B)try{new URL(B)}catch{throw new F(`Invalid --url: ${B}`)}await v.goto(`${M}/submit`);await v.wait(1);const f=await v.evaluate(`(async () => {
2
+ try {
3
+ // The submit form only renders for a signed-in user; if HN bounced us
4
+ // to /login the form (and its fnid) will be absent.
5
+ if (/\\/login/.test(location.pathname)) return { kind: 'auth', detail: 'redirected to /login' };
6
+ const form = document.querySelector('form[action$="/r"], form[action="/r"], form[action="https://news.ycombinator.com/r"]')
7
+ || [...document.querySelectorAll('form')].find(f => f.querySelector('input[name="fnid"]'));
8
+ if (!form) return { kind: 'auth', detail: 'submit form not found — not signed in' };
9
+ const fnid = form.querySelector('input[name="fnid"]')?.value;
10
+ if (!fnid) return { kind: 'auth', detail: 'fnid token missing' };
11
+
12
+ const p = new URLSearchParams();
13
+ p.set('fnid', fnid);
14
+ p.set('fnop', 'submit-page');
15
+ p.set('title', ${JSON.stringify(z)});
16
+ p.set('url', ${JSON.stringify(B)});
17
+ p.set('text', ${JSON.stringify(K)});
18
+
19
+ const res = await fetch('/r', {
20
+ method: 'POST',
21
+ credentials: 'include',
22
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
23
+ body: p.toString(),
24
+ redirect: 'manual',
25
+ });
26
+ // A same-origin 302 comes back as an opaque redirect (status 0) — that
27
+ // is the success path. A 200 means HN re-rendered the form with an error.
28
+ if (res.type === 'opaqueredirect' || res.status === 302 || res.status === 0) {
29
+ return { ok: true };
30
+ }
31
+ const body = await res.text();
32
+ // Pull the first human-readable error HN prints above the form.
33
+ let msg = '';
34
+ const td = body.match(/<td[^>]*>\\s*([^<][^<]{0,200}?)\\s*<\\/td>/i);
35
+ if (/Please try again/i.test(body)) msg = 'HN asked to try again (possible rate limit or expired token).';
36
+ else if (/already been submitted/i.test(body)) msg = 'That URL has already been submitted.';
37
+ else if (td) msg = td[1].replace(/\\s+/g, ' ').trim();
38
+ return { kind: 'reject', detail: msg || ('HTTP ' + res.status + ' without redirect'), status: res.status };
39
+ } catch (e) {
40
+ return { kind: 'exception', detail: String((e && e.message) || e) };
41
+ }
42
+ })()`);if(f?.kind==="auth")throw new Q("news.ycombinator.com",f.detail);if(f?.kind==="exception")throw new L(`HN submit failed: ${f.detail}`);if(f?.kind==="reject")return[{status:"failed",id:"",url:"",title:z,message:f.detail}];if(!f?.ok)throw new L(`Unexpected HN submit result: ${JSON.stringify(f)}`);await v.wait(1);const J=(await v.evaluate(`(async () => {
43
+ try {
44
+ const uc = document.cookie.match(/(?:^|; )user=([^;]+)/);
45
+ // Cookie value is "<username>&<hash>"; the nav link is more reliable.
46
+ let username = '';
47
+ const navLink = document.querySelector('span.pagetop a[href^="user?id="]');
48
+ if (navLink) username = (navLink.getAttribute('href') || '').replace('user?id=', '');
49
+ if (!username && uc) username = decodeURIComponent(uc[1]).split('&')[0];
50
+ if (!username) return null;
51
+ const res = await fetch('/submitted?id=' + encodeURIComponent(username), { credentials: 'include' });
52
+ const html = await res.text();
53
+ const doc = new DOMParser().parseFromString(html, 'text/html');
54
+ const rows = [...doc.querySelectorAll('tr.athing')];
55
+ const want = ${JSON.stringify(z)}.replace(/\\s+/g, ' ').trim();
56
+ for (const row of rows.slice(0, 5)) {
57
+ const link = row.querySelector('.titleline a, a.storylink, .title a');
58
+ const t = (link?.textContent || '').replace(/\\s+/g, ' ').trim();
59
+ if (t === want) {
60
+ return { id: row.id, href: link?.getAttribute('href') || '' };
61
+ }
62
+ }
63
+ return { id: rows[0]?.id || '', href: '' };
64
+ } catch { return null; }
65
+ })()`))?.id||"",P=J?`${M}/item?id=${J}`:"";return[{status:"success",id:J,url:P,title:z,message:J?"Story submitted":"Story submitted (item id not confirmed; check /newest)"}]}});
@@ -1,4 +1,4 @@
1
- import{AuthRequiredError as G,CommandExecutionError as F}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as L}from"../_shared/site-auth.js";const K=`(async () => {
1
+ import{AuthRequiredError as L,CommandExecutionError as F}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as T}from"../_shared/site-auth.js";const Q=`(async () => {
2
2
  try {
3
3
  const r = await fetch('/passport/account/info/v2/?aid=513695', { credentials: 'include', headers: { Accept: 'application/json' } });
4
4
  if (r.status === 401 || r.status === 403) return { kind: 'auth', detail: 'Jimeng passport HTTP ' + r.status };
@@ -10,4 +10,4 @@ import{AuthRequiredError as G,CommandExecutionError as F}from"@jackwener/opencli
10
10
  } catch (e) {
11
11
  return { kind: 'exception', detail: String(e && e.message || e) };
12
12
  }
13
- })()`;async function N(D){await D.goto("https://jimeng.jianying.com/ai-tool/generate?type=image&workspace=0");await D.wait(2);const z=await D.evaluate(K);if(z?.kind==="auth")throw new G("jimeng.jianying.com",z.detail);if(z?.kind==="http")throw new F(`HTTP ${z.httpStatus} from Jimeng passport`);if(z?.kind==="exception")throw new F(`Jimeng whoami failed: ${z.detail}`);if(!z?.ok)throw new F(`Unexpected Jimeng probe: ${JSON.stringify(z)}`);return{user_id:z.user_id,screen_name:z.screen_name}}L({site:"jimeng",domain:"jimeng.jianying.com",loginUrl:"https://jimeng.jianying.com/",columns:["user_id","screen_name"],verify:N,poll:async(D)=>{const z=await D.evaluate(K);if(!z?.ok)throw new G("jimeng.jianying.com","Waiting for Jimeng login");return{user_id:z.user_id,screen_name:z.screen_name}}});
13
+ })()`,U=new Set(["sessionid","sessionid_ss","sid_tt"]);async function V(D){return{logged_in:(await D.getCookies({url:"https://jimeng.jianying.com"})).some((G)=>G.value&&U.has(G.name))}}async function X(D){await D.goto("https://jimeng.jianying.com/ai-tool/generate?type=image&workspace=0");await D.wait(2);const z=await D.evaluate(Q);if(z?.kind==="auth")throw new L("jimeng.jianying.com",z.detail);if(z?.kind==="http")throw new F(`HTTP ${z.httpStatus} from Jimeng passport`);if(z?.kind==="exception")throw new F(`Jimeng whoami failed: ${z.detail}`);if(!z?.ok)throw new F(`Unexpected Jimeng probe: ${JSON.stringify(z)}`);return{user_id:z.user_id,screen_name:z.screen_name}}T({site:"jimeng",domain:"jimeng.jianying.com",loginUrl:"https://jimeng.jianying.com/",columns:["user_id","screen_name"],verify:X,quickCheck:V,poll:async(D)=>{const z=await D.evaluate(Q);if(!z?.ok)throw new L("jimeng.jianying.com","Waiting for Jimeng login");return{user_id:z.user_id,screen_name:z.screen_name}}});
@@ -1,53 +1,60 @@
1
- import{cli as e,Strategy as t}from"@jackwener/opencli/registry";e({site:"jimeng",name:"generate",access:"write",description:"即梦AI 文生图 — 输入 prompt 生成图片",domain:"jimeng.jianying.com",strategy:t.COOKIE,browser:!0,args:[{name:"prompt",type:"string",required:!0,positional:!0,help:"图片描述 prompt"},{name:"model",type:"string",default:"high_aes_general_v50",help:"模型: high_aes_general_v50 (5.0 Lite), high_aes_general_v42 (4.6), high_aes_general_v40 (4.0)"},{name:"wait",type:"int",default:40,help:"等待生成完成的秒数"}],columns:["status","prompt","image_count","image_urls"],pipeline:[{navigate:"https://jimeng.jianying.com/ai-tool/generate?type=image&workspace=0"},{wait:3},{evaluate:`(async () => {
2
- const prompt = \${{ args.prompt | json }};
3
- const waitSec = \${{ args.wait }};
4
-
5
- // Step 1: Count existing images before generation
6
- const beforeImgs = document.querySelectorAll('img[src*="dreamina-sign"], img[src*="tb4s082cfz"]').length;
7
-
8
- // Step 2: Clear and set prompt
9
- const editors = document.querySelectorAll('[contenteditable="true"]');
10
- const editor = editors[0];
11
- if (!editor) return [{ status: 'failed', prompt: prompt, image_count: 0, image_urls: 'Editor not found' }];
12
-
1
+ import{cli as h,Strategy as G}from"@jackwener/opencli/registry";import{CommandExecutionError as F}from"@jackwener/opencli/errors";const M="https://jimeng.jianying.com/ai-tool/generate?type=image&workspace=0",V="/mweb/v1/get_history?aid=513695&device_platform=web&region=cn&da_version=3.3.11&web_version=7.5.0&aigc_features=app_lip_sync",W=JSON.stringify({cursor:"",count:5,need_page_item:!0,need_aigc_data:!0,aigc_mode_list:["workbench"]}),N=(q)=>`(async () => {
2
+ const prompt = ${JSON.stringify(q)};
3
+ let existing = [];
4
+ try {
5
+ const r = await fetch(${JSON.stringify(V)}, {
6
+ method: 'POST', credentials: 'include',
7
+ headers: { 'Content-Type': 'application/json' },
8
+ body: ${JSON.stringify(W)},
9
+ });
10
+ const d = await r.json();
11
+ existing = (d && d.data && d.data.records_list || []).map((x) => x.submit_id).filter(Boolean);
12
+ } catch (e) { /* 基线拿不到就当空,轮询取最新一条 */ }
13
+
14
+ const editor = document.querySelector('[contenteditable="true"]');
15
+ if (!editor) return { ok: false, error: 'editor_not_found' };
13
16
  editor.focus();
14
- await new Promise(r => setTimeout(r, 200));
17
+ await new Promise((r) => setTimeout(r, 200));
15
18
  document.execCommand('selectAll');
16
- await new Promise(r => setTimeout(r, 100));
19
+ await new Promise((r) => setTimeout(r, 80));
17
20
  document.execCommand('delete');
18
- await new Promise(r => setTimeout(r, 200));
21
+ await new Promise((r) => setTimeout(r, 150));
19
22
  document.execCommand('insertText', false, prompt);
20
- await new Promise(r => setTimeout(r, 500));
21
-
22
- // Step 3: Click generate
23
+ await new Promise((r) => setTimeout(r, 400));
24
+
23
25
  const btn = document.querySelector('.lv-btn.lv-btn-primary[class*="circle"]');
24
- if (!btn) return [{ status: 'failed', prompt: prompt, image_count: 0, image_urls: 'Generate button not found' }];
26
+ if (!btn) return { ok: false, error: 'button_not_found' };
27
+ if (btn.disabled || btn.getAttribute('aria-disabled') === 'true') return { ok: false, error: 'button_disabled' };
25
28
  btn.click();
26
-
27
- // Step 4: Wait for new images to appear
28
- let newImgs = [];
29
- for (let i = 0; i < waitSec; i++) {
30
- await new Promise(r => setTimeout(r, 1000));
31
- const allImgs = document.querySelectorAll('img[src*="dreamina-sign"], img[src*="tb4s082cfz"]');
32
- if (allImgs.length > beforeImgs) {
33
- // New images appeared — generation complete
34
- newImgs = Array.from(allImgs).slice(0, allImgs.length - beforeImgs);
35
- break;
36
- }
29
+ return { ok: true, existing };
30
+ })()`,j=(q)=>`(async () => {
31
+ const existing = ${JSON.stringify(q)};
32
+ const r = await fetch(${JSON.stringify(V)}, {
33
+ method: 'POST', credentials: 'include',
34
+ headers: { 'Content-Type': 'application/json' },
35
+ body: ${JSON.stringify(W)},
36
+ });
37
+ const d = await r.json();
38
+ const recs = (d && d.data && d.data.records_list) || [];
39
+ const fresh = recs.find((x) => x.submit_id && existing.indexOf(x.submit_id) === -1);
40
+ if (!fresh) return { state: 'pending' };
41
+
42
+ const failMsg = fresh.fail_starling_message || '';
43
+ const items = fresh.item_list || [];
44
+ const done = items
45
+ .map((it) => it && it.image && it.image.large_images && it.image.large_images[0] && it.image.large_images[0].image_url)
46
+ .filter(Boolean);
47
+ const total = fresh.total_image_count || 0;
48
+
49
+ if (done.length && (!total || done.length >= total)) {
50
+ const p = items[0] && items[0].aigc_image_params && items[0].aigc_image_params.text2image_params;
51
+ return {
52
+ state: 'success',
53
+ urls: done,
54
+ model: (fresh.model_info && fresh.model_info.model_name) || (p && p.model_config && p.model_config.model_name) || '',
55
+ };
37
56
  }
38
-
39
- if (newImgs.length === 0) {
40
- return [{ status: 'timeout', prompt: prompt, image_count: 0, image_urls: 'Generation may still be in progress' }];
41
- }
42
-
43
- // Step 5: Extract image URLs (use thumbnail URLs which are accessible)
44
- const urls = newImgs.map(img => img.src);
45
-
46
- return [{
47
- status: 'success',
48
- prompt: prompt.substring(0, 80),
49
- image_count: urls.length,
50
- image_urls: urls.join('\\n')
51
- }];
52
- })()
53
- `},{map:{status:"${{ item.status }}",prompt:"${{ item.prompt }}",image_count:"${{ item.image_count }}",image_urls:"${{ item.image_urls }}"}}]});
57
+ if (!done.length && failMsg) return { state: 'failed', error: failMsg };
58
+ return { state: 'pending', got: done.length, total };
59
+ })()`;h({site:"jimeng",name:"generate",access:"write",description:"即梦AI 文生图 — 输入 prompt 生成图片",domain:"jimeng.jianying.com",strategy:G.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[{name:"prompt",type:"string",required:!0,positional:!0,help:"图片描述 prompt"},{name:"wait",type:"int",default:60,help:"等待生成完成的最长秒数(每 3 秒轮询一次,超时会返回 pending 并提示用 history 回查)"}],columns:["status","prompt","model","image_count","image_urls"],func:async(q,K)=>{const z=String(K.prompt??"").trim();if(!z)throw new F("prompt is required");const Q=Number(K.wait??60);await q.goto(M);await q.wait(3);const v=await q.evaluate(N(z));if(!v?.ok){const $={editor_not_found:"找不到 prompt 输入框(页面结构可能已变)",button_not_found:"找不到生成按钮(页面结构可能已变)",button_disabled:"生成按钮不可点击(可能未登录、额度用尽或 prompt 为空)"}[v?.error]||`提交失败:${v?.error||"unknown"}`;throw new F($)}const X=Array.isArray(v.existing)?v.existing:[],Z=Date.now()+Q*1000;let f=null;while(Date.now()<Z){await q.wait(3);f=await q.evaluate(j(X));if(f?.state==="success")return[{status:"success",prompt:z.slice(0,80),model:f.model||"",image_count:f.urls.length,image_urls:f.urls.join(`
60
+ `)}];if(f?.state==="failed")throw new F(`即梦生成失败:${f.error}`)}return[{status:"pending",prompt:z.slice(0,80),model:f&&f.model||"",image_count:f&&f.got||0,image_urls:`生成仍在进行(已 ${Q}s);稍后用 \`opencli jimeng history\` 回查结果`}]}});
@@ -1,23 +1,32 @@
1
1
  import{cli as e,Strategy as t}from"@jackwener/opencli/registry";e({site:"jimeng",name:"history",access:"read",description:"即梦AI 查看最近生成的作品",domain:"jimeng.jianying.com",strategy:t.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:5}],columns:["prompt","model","status","image_url","created_at"],pipeline:[{navigate:"https://jimeng.jianying.com/ai-tool/generate?type=image&workspace=0"},{evaluate:`(async () => {
2
2
  const limit = \${{ args.limit }};
3
+ // get_history 的响应结构已从 history_list 改版为 records_list:每条 record 是一次
4
+ // 生成任务,其 item_list 才是单张图片;prompt/model/image 仍在 item 层的老字段里
5
+ // (aigc_image_params / image.large_images / common_attr)。完成态以「拿到 large_images
6
+ // 的图片 URL」判定(状态码 102→144/50 已漂移,别再按魔法数字判),出图即完成。
3
7
  const res = await fetch('/mweb/v1/get_history?aid=513695&device_platform=web&region=cn&da_version=3.3.11&web_version=7.5.0&aigc_features=app_lip_sync', {
4
8
  method: 'POST',
5
9
  credentials: 'include',
6
10
  headers: { 'Content-Type': 'application/json' },
7
- body: JSON.stringify({ cursor: '', count: limit, need_page_item: true, need_aigc_data: true, aigc_mode_list: ['workbench'] })
11
+ body: JSON.stringify({ cursor: '', count: Math.max(limit, 10), need_page_item: true, need_aigc_data: true, aigc_mode_list: ['workbench'] })
8
12
  });
9
13
  const data = await res.json();
10
- const items = data?.data?.history_list || [];
11
- return items.slice(0, limit).map(item => {
12
- const params = item.aigc_image_params?.text2image_params || {};
13
- const images = item.image?.large_images || [];
14
- return {
15
- prompt: params.prompt || item.common_attr?.title || 'N/A',
16
- model: params.model_config?.model_name || 'unknown',
17
- status: item.common_attr?.status === 102 ? 'completed' : 'pending',
18
- image_url: images[0]?.image_url || '',
19
- created_at: new Date((item.common_attr?.create_time || 0) * 1000).toLocaleString('zh-CN'),
20
- };
21
- });
14
+ const records = data?.data?.records_list || [];
15
+ const rows = [];
16
+ for (const rec of records) {
17
+ for (const item of (rec.item_list || [])) {
18
+ const params = item.aigc_image_params?.text2image_params || {};
19
+ const imageUrl = item.image?.large_images?.[0]?.image_url || item.common_attr?.cover_url || '';
20
+ rows.push({
21
+ prompt: params.prompt || item.common_attr?.title || rec.fail_starling_message || 'N/A',
22
+ model: (rec.model_info?.model_name) || params.model_config?.model_name || 'unknown',
23
+ status: imageUrl ? 'completed' : 'pending',
24
+ image_url: imageUrl,
25
+ created_at: new Date((item.common_attr?.create_time || rec.finish_time || 0) * 1000).toLocaleString('zh-CN'),
26
+ });
27
+ if (rows.length >= limit) return rows;
28
+ }
29
+ }
30
+ return rows;
22
31
  })()
23
32
  `},{map:{prompt:"${{ item.prompt }}",model:"${{ item.model }}",status:"${{ item.status }}",image_url:"${{ item.image_url }}",created_at:"${{ item.created_at }}"}},{limit:"${{ args.limit }}"}]});
@@ -1,25 +1,16 @@
1
1
  import{AuthRequiredError as D,CommandExecutionError as F}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as K}from"../_shared/site-auth.js";async function G(B){return(await B.getCookies({url:"https://www.pixiv.net"})).some((H)=>H.name==="PHPSESSID"&&/^\d+_/.test(H.value||""))}async function J(B){if(!await G(B))throw new D("pixiv.net","Pixiv PHPSESSID cookie missing");await B.goto("https://www.pixiv.net/");await B.wait(2);const z=await B.evaluate(`(async () => {
2
2
  try {
3
- const meta = document.querySelector('meta[name="global-data"]')?.getAttribute('content') || '';
4
- let userData = null;
5
- if (meta) { try { userData = JSON.parse(meta); } catch {} }
6
- const u = userData?.userData;
7
- if (u?.id) {
8
- return { ok: true, user_id: String(u.id), name: String(u.name || u.account || '') };
9
- }
10
- const r = await fetch('/ajax/user/extra', { credentials: 'include', headers: { Accept: 'application/json' } });
3
+ const r = await fetch('/touch/ajax/user/self/status', { credentials: 'include', headers: { Accept: 'application/json' } });
11
4
  if (r.status === 401 || r.status === 403) {
12
- return { kind: 'auth', detail: 'Pixiv /ajax/user/extra HTTP ' + r.status };
5
+ return { kind: 'auth', detail: 'Pixiv self/status HTTP ' + r.status };
13
6
  }
14
7
  if (!r.ok) return { kind: 'http', httpStatus: r.status };
15
- const d = await r.json();
16
- if (d?.error) return { kind: 'auth', detail: 'Pixiv /ajax/user/extra error=true — anonymous' };
17
- const phpSess = (document.cookie.split('; ').find(c => c.startsWith('PHPSESSID=')) || '').split('=')[1] || '';
18
- const uid = phpSess.split('_')[0] || '';
19
- if (!uid) {
20
- return { kind: 'auth', detail: 'Pixiv PHPSESSID prefix unparseable' };
8
+ const d = await r.json().catch(() => null);
9
+ const u = d?.body?.user_status;
10
+ if (d?.error || !u || u.is_loggedin === false || !u.user_id) {
11
+ return { kind: 'auth', detail: 'Pixiv self/status reports anonymous' };
21
12
  }
22
- return { ok: true, user_id: uid, name: '' };
13
+ return { ok: true, user_id: String(u.user_id), name: String(u.user_name || u.user_account || '') };
23
14
  } catch (e) {
24
15
  return { kind: 'exception', detail: String(e && e.message || e) };
25
16
  }
package/clis/suno/auth.js CHANGED
@@ -1,24 +1,20 @@
1
1
  import{AuthRequiredError as D,CommandExecutionError as F}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as K}from"../_shared/site-auth.js";async function G(B){return(await B.getCookies({url:"https://clerk.suno.com"})).some((H)=>H.name==="__session"&&H.value)}async function J(B){if(!await G(B))throw new D("suno.com","Suno Clerk __session/__client cookie missing");await B.goto("https://suno.com/");await B.wait(2);const z=await B.evaluate(`(async () => {
2
2
  try {
3
- const r = await fetch('https://clerk.suno.com/v1/client?_clerk_js_version=5', {
4
- credentials: 'include',
5
- headers: { Accept: 'application/json' },
6
- });
7
- if (r.status === 401 || r.status === 403) {
8
- return { kind: 'auth', detail: 'clerk.suno.com /v1/client HTTP ' + r.status };
3
+ for (let i = 0; i < 20; i += 1) {
4
+ if (window.Clerk && window.Clerk.loaded) break;
5
+ await new Promise(r => setTimeout(r, 250));
9
6
  }
10
- if (!r.ok) return { kind: 'http', httpStatus: r.status };
11
- const d = await r.json();
12
- const sessions = d?.response?.sessions || [];
13
- if (!Array.isArray(sessions) || sessions.length === 0) {
14
- return { kind: 'auth', detail: 'clerk.suno.com sessions=[] — anonymous' };
7
+ const C = window.Clerk;
8
+ if (!C) return { kind: 'exception', detail: 'window.Clerk never mounted' };
9
+ const user = C.user;
10
+ const session = C.session;
11
+ if (!user || !session) {
12
+ return { kind: 'auth', detail: 'Suno Clerk mounted but no active user/session — anonymous' };
15
13
  }
16
- const active = sessions.find(s => s.status === 'active') || sessions[0];
17
- const user = active?.user;
18
- if (!user?.id) {
19
- return { kind: 'auth', detail: 'clerk.suno.com session present but no user.id stale session' };
20
- }
21
- return { ok: true, user_id: String(user.id), name: String(user.username || '') };
14
+ const email = user.primaryEmailAddress?.emailAddress
15
+ || user.emailAddresses?.[0]?.emailAddress
16
+ || '';
17
+ return { ok: true, user_id: String(user.id), name: String(user.username || email || '') };
22
18
  } catch (e) {
23
19
  return { kind: 'exception', detail: String(e && e.message || e) };
24
20
  }
@@ -1,9 +1,9 @@
1
- import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionError as H,ArgumentError as X}from"@jackwener/opencli/errors";import{cli as Qz,Strategy as Zz}from"@jackwener/opencli/registry";import{assertLiteralContent as $z}from"../_shared/content-guard.js";const qz="https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image",C=9,T=20,x=3000,u="|||",D="基础",d=[["基础","默认兜底,万能"],["边框","金句/要点卡"],["备忘","提醒/随手记"],["清新","日常/清单贴士"],["涂写","随笔/碎碎念"],["便签","笔记/提醒"],["光影","情绪/文艺"],["涂鸦","趣味/童话"],["简约","干货/观点"],["手写","日记/情感"],["插图","生活方式/轻松话题"],["美漫","活力/趣味/故事感"],["弥散","弥散光氛围"],["柔和","柔和/温柔金句"],["印刷","印刷海报/排版"],["科技","科技/产品"],["贺卡","节日祝福"],["札记","艺术/水彩氛围"],["书摘","书摘/引用"],["手帐","手帐拼贴"],["几何","醒目/有力主张"]],_z=d.map(([z])=>z),L="文字配图",c="再写一张",E="生成图片",n="下一步",M=".tiptap.ProseMirror",jz=["_onPublish","onPublish","_onSubmit","_handlePublish"],Vz=["_onSave","_onSaveDraft","_onDraft"],w=['[contenteditable="true"][placeholder*="标题"]','[contenteditable="true"][placeholder*="赞"]','input[placeholder*="标题"]','input[placeholder*="title" i]','[contenteditable="true"][class*="title"]','input[maxlength="20"]','input[class*="title"]',".title-input input",".note-title input","input[maxlength]"],m=['[contenteditable="true"][class*="content"]','[contenteditable="true"][class*="editor"]','[contenteditable="true"][placeholder*="描述"]','[contenteditable="true"][placeholder*="正文"]','[contenteditable="true"][placeholder*="内容"]','.note-content [contenteditable="true"]','.editor-content [contenteditable="true"]','[contenteditable="true"]:not([placeholder*="标题"]):not([placeholder*="赞"]):not([placeholder*="title" i])'],i={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".gif":"image/gif",".webp":"image/webp"};function Y(z){if(z&&typeof z==="object"&&typeof z.session==="string"&&Object.prototype.hasOwnProperty.call(z,"data"))return z.data;return z}function Hz(z){return z.map((J)=>{const K=B.resolve(J);if(!y.existsSync(K))throw new X(`Image file not found: ${K}`);const Z=B.extname(K).toLowerCase();if(!i[Z])throw new X(`Unsupported image format "${Z}". Supported: jpg, png, gif, webp`);return K})}const l='input[type="file"][accept*="image"],input[type="file"][accept*=".jpg"],input[type="file"][accept*=".jpeg"],input[type="file"][accept*=".png"],input[type="file"][accept*=".gif"],input[type="file"][accept*=".webp"]';async function Gz(z,J=15000){const K=500,Z=Math.max(1,Math.ceil(J/K));for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
1
+ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionError as H,ArgumentError as F}from"@jackwener/opencli/errors";import{cli as Vz,Strategy as Hz}from"@jackwener/opencli/registry";import{assertLiteralContent as Gz}from"../_shared/content-guard.js";const Yz="https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image",x=9,E=20,u=3000,c="|||",N="基础",d=[["基础","默认兜底,万能"],["边框","金句/要点卡"],["备忘","提醒/随手记"],["清新","日常/清单贴士"],["涂写","随笔/碎碎念"],["便签","笔记/提醒"],["光影","情绪/文艺"],["涂鸦","趣味/童话"],["简约","干货/观点"],["手写","日记/情感"],["插图","生活方式/轻松话题"],["美漫","活力/趣味/故事感"],["弥散","弥散光氛围"],["柔和","柔和/温柔金句"],["印刷","印刷海报/排版"],["科技","科技/产品"],["贺卡","节日祝福"],["札记","艺术/水彩氛围"],["书摘","书摘/引用"],["手帐","手帐拼贴"],["几何","醒目/有力主张"]],Ez=d.map(([z])=>z),R="文字配图",n="再写一张",m="生成图片",i="下一步",_=".tiptap.ProseMirror",Xz=["_onPublish","onPublish","_onSubmit","_handlePublish"],Wz=["_onSave","_onSaveDraft","_onDraft"],A=['[contenteditable="true"][placeholder*="标题"]','[contenteditable="true"][placeholder*="赞"]','input[placeholder*="标题"]','input[placeholder*="title" i]','[contenteditable="true"][class*="title"]','input[maxlength="20"]','input[class*="title"]',".title-input input",".note-title input","input[maxlength]"],l=['[contenteditable="true"][class*="content"]','[contenteditable="true"][class*="editor"]','[contenteditable="true"][placeholder*="描述"]','[contenteditable="true"][placeholder*="正文"]','[contenteditable="true"][placeholder*="内容"]','.note-content [contenteditable="true"]','.editor-content [contenteditable="true"]','[contenteditable="true"]:not([placeholder*="标题"]):not([placeholder*="赞"]):not([placeholder*="title" i])'],o={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".gif":"image/gif",".webp":"image/webp"};function W(z){if(z&&typeof z==="object"&&typeof z.session==="string"&&Object.prototype.hasOwnProperty.call(z,"data"))return z.data;return z}function Fz(z){return z.map((J)=>{const K=D.resolve(J);if(!b.existsSync(K))throw new F(`Image file not found: ${K}`);const Z=D.extname(K).toLowerCase();if(!o[Z])throw new F(`Unsupported image format "${Z}". Supported: jpg, png, gif, webp`);return K})}const s='input[type="file"][accept*="image"],input[type="file"][accept*=".jpg"],input[type="file"][accept*=".jpeg"],input[type="file"][accept*=".png"],input[type="file"][accept*=".gif"],input[type="file"][accept*=".webp"]';async function Oz(z,J=15000){const K=500,Z=Math.max(1,Math.ceil(J/K));for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
2
2
  (() => {
3
- const sels = ${JSON.stringify(l)};
3
+ const sels = ${JSON.stringify(s)};
4
4
  return !!document.querySelector(sels);
5
5
  })()
6
- `))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function o(z,J){if(!await Gz(z))return{ok:!1,count:0,error:"No file input found on page (waited 15s; publish surface did not finish rendering)"};if(z.setFileInput)try{await z.setFileInput(J,l);return{ok:!0,count:J.length}}catch($){const j=$ instanceof Error?$.message:String($);if(j.includes("Unknown action")||j.includes("not supported")||j.includes("Not allowed"));else return{ok:!1,count:0,error:j}}const Z=J.map(($)=>{const j=y.readFileSync($).toString("base64"),W=B.extname($).toLowerCase();return{name:B.basename($),mimeType:i[W],base64:j}}),Q=Z.reduce(($,j)=>$+j.base64.length,0);if(Q>500000)console.warn(`[warn] Total image payload is ${(Q/1024/1024).toFixed(1)}MB (base64). This may fail with the browser bridge. Update the extension to v1.6+ for CDP-based upload, or compress images before publishing.`);const q=JSON.stringify(Z);return z.evaluate(`
6
+ `))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function r(z,J){if(!await Oz(z))return{ok:!1,count:0,error:"No file input found on page (waited 15s; publish surface did not finish rendering)"};if(z.setFileInput)try{await z.setFileInput(J,s);return{ok:!0,count:J.length}}catch($){const j=$ instanceof Error?$.message:String($);if(j.includes("Unknown action")||j.includes("not supported")||j.includes("Not allowed"));else return{ok:!1,count:0,error:j}}const Z=J.map(($)=>{const j=b.readFileSync($).toString("base64"),Y=D.extname($).toLowerCase();return{name:D.basename($),mimeType:o[Y],base64:j}}),Q=Z.reduce(($,j)=>$+j.base64.length,0);if(Q>500000)console.warn(`[warn] Total image payload is ${(Q/1024/1024).toFixed(1)}MB (base64). This may fail with the browser bridge. Update the extension to v1.6+ for CDP-based upload, or compress images before publishing.`);const q=JSON.stringify(Z);return z.evaluate(`
7
7
  (async () => {
8
8
  const images = ${q};
9
9
 
@@ -35,17 +35,19 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
35
35
  }
36
36
  }
37
37
 
38
- Object.defineProperty(input, 'files', { value: dt.files, writable: false });
38
+ // configurable:true 让同一个 input 能被反复注入(重试 / 追加时 SPA 常复用同一元素,
39
+ // 否则第二次 defineProperty 会抛 "Cannot redefine property: files")。
40
+ Object.defineProperty(input, 'files', { value: dt.files, writable: false, configurable: true });
39
41
  input.dispatchEvent(new Event('change', { bubbles: true }));
40
42
  input.dispatchEvent(new Event('input', { bubbles: true }));
41
43
 
42
44
  return { ok: true, count: dt.files.length };
43
45
  })()
44
- `)}async function r(z,J=30000){const K=2000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(!await z.evaluate(`
46
+ `)}async function g(z,J=30000){const K=2000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(!await z.evaluate(`
45
47
  () => !!document.querySelector(
46
48
  '[class*="upload"][class*="progress"], [class*="uploading"], [class*="loading"][class*="image"]'
47
49
  )
48
- `))return;await z.wait({time:K/1000})}}async function g(z,J,K,Z){const Q=await z.evaluate(`
50
+ `))return;await z.wait({time:K/1000})}}async function p(z,J,K,Z){const Q=await z.evaluate(`
49
51
  (function(selectors) {
50
52
  const __opencli_xhs_fill_phase = "locate";
51
53
  for (const sel of selectors) {
@@ -167,7 +169,7 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
167
169
  const actual = normalize(el.innerText || el.textContent || '');
168
170
  return { ok: actual === normalize(expectedText), actual };
169
171
  })(${JSON.stringify(Q.sel)}, ${JSON.stringify(K)})
170
- `)}catch{$=await q()}}else $=await q();if(!$?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});const j=typeof $?.actual==="string"?$.actual:"";throw Error(`Failed to set ${Z}. Expected "${K}", got "${j}". Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}}async function Yz(z,J){return Y(await z.evaluate(`
172
+ `)}catch{$=await q()}}else $=await q();if(!$?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});const j=typeof $?.actual==="string"?$.actual:"";throw Error(`Failed to set ${Z}. Expected "${K}", got "${j}". Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}}async function Uz(z,J){return W(await z.evaluate(`
171
173
  (selectors => {
172
174
  const el = selectors
173
175
  .map(sel => Array.from(document.querySelectorAll(sel)))
@@ -183,7 +185,7 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
183
185
  selection?.addRange(range);
184
186
  return true;
185
187
  })(${JSON.stringify(J)})
186
- `))}function Az(z,{click:J=!1}={}){return`
188
+ `))}function uz(z,{click:J=!1}={}){return`
187
189
  (topicName => {
188
190
  const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
189
191
  const want = norm(topicName);
@@ -223,7 +225,7 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
223
225
  text: (target.innerText || target.textContent || '').trim().slice(0, 40),
224
226
  };
225
227
  })(${JSON.stringify(z)})
226
- `}function Sz(z,J){return`
228
+ `}function cz(z,J){return`
227
229
  ((topicName, selectors) => {
228
230
  const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
229
231
  const want = norm(topicName);
@@ -254,7 +256,7 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
254
256
  }
255
257
  return count;
256
258
  })(${JSON.stringify(z)}, ${JSON.stringify(J)})
257
- `}function s(z,J){return`
259
+ `}function t(z,J){return`
258
260
  ((topicName, selectors) => {
259
261
  const __opencli_xhs_topic_marker_count = true;
260
262
  const marker = '#' + topicName + '[话题]';
@@ -272,14 +274,14 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
272
274
  }
273
275
  return count;
274
276
  })(${JSON.stringify(z)}, ${JSON.stringify(J)})
275
- `}async function Cz(z,J){const K=`#${J}`;if(typeof z.insertText==="function")try{await z.insertText(K);return!0}catch{}return Y(await z.evaluate(`
277
+ `}async function dz(z,J){const K=`#${J}`;if(typeof z.insertText==="function")try{await z.insertText(K);return!0}catch{}return W(await z.evaluate(`
276
278
  (text => {
277
279
  const ok = document.execCommand('insertText', false, text);
278
280
  const active = document.activeElement;
279
281
  if (active) active.dispatchEvent(new Event('input', { bubbles: true }));
280
282
  return ok;
281
283
  })(${JSON.stringify(K)})
282
- `))}async function Xz(z,J,K){const Z=[];for(const Q of K){if(!await Yz(z,J))throw new H(`Could not attach topic "${Q}": body editor not found`);const $=Number(Y(await z.evaluate(s(Q,J))))||0;if(typeof z.pressKey==="function")try{await z.pressKey("Enter")}catch{}if(typeof z.insertText!=="function")throw new H(`Could not attach topic "${Q}": page.insertText is unavailable`);try{await z.insertText(`#${Q}`)}catch{throw new H(`Could not attach topic "${Q}": failed to type inline topic query`)}await z.wait({time:1.2});if(typeof z.pressKey!=="function")throw new H(`Could not attach topic "${Q}": page.pressKey is unavailable`);try{await z.pressKey("Enter")}catch(W){throw new H(`Could not attach topic "${Q}": failed to accept suggestion (${W&&W.message||W})`)}await z.wait({time:0.8});if((Number(Y(await z.evaluate(s(Q,J))))||0)<=$)throw new H(`Could not attach topic "${Q}": no real topic entity appeared after selection`);Z.push(Q);await z.wait({time:0.4})}return Z}async function Wz(z){const J=await z.evaluate(`
284
+ `))}async function Bz(z,J,K){const Z=[];for(const Q of K){if(!await Uz(z,J))throw new H(`Could not attach topic "${Q}": body editor not found`);const $=Number(W(await z.evaluate(t(Q,J))))||0;if(typeof z.pressKey==="function")try{await z.pressKey("Enter")}catch{}if(typeof z.insertText!=="function")throw new H(`Could not attach topic "${Q}": page.insertText is unavailable`);try{await z.insertText(`#${Q}`)}catch{throw new H(`Could not attach topic "${Q}": failed to type inline topic query`)}await z.wait({time:1.2});if(typeof z.pressKey!=="function")throw new H(`Could not attach topic "${Q}": page.pressKey is unavailable`);try{await z.pressKey("Enter")}catch(Y){throw new H(`Could not attach topic "${Q}": failed to accept suggestion (${Y&&Y.message||Y})`)}await z.wait({time:0.8});if((Number(W(await z.evaluate(t(Q,J))))||0)<=$)throw new H(`Could not attach topic "${Q}": no real topic entity appeared after selection`);Z.push(Q);await z.wait({time:0.4})}return Z}async function vz(z){const J=await z.evaluate(`
283
285
  () => {
284
286
  const isVisible = (el) => {
285
287
  if (!el || el.offsetParent === null) return false;
@@ -328,7 +330,7 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
328
330
  }
329
331
  return { ok: false, visibleTexts };
330
332
  }
331
- `);if(J?.ok)await z.wait({time:1});return J}async function f(z,J,K=7000){const Z=500,Q=Math.max(1,Math.ceil(K/Z));let q={ok:!1};for(let $=0;$<Q;$++){q=Y(await z.evaluate(`
333
+ `);if(J?.ok)await z.wait({time:1});return J}async function k(z,J,K=7000){const Z=500,Q=Math.max(1,Math.ceil(K/Z));let q={ok:!1};for(let $=0;$<Q;$++){q=W(await z.evaluate(`
332
334
  ((cfg) => {
333
335
  const __opencli_xhs_click_label = { wantLabel: ${JSON.stringify(J)} };
334
336
  const wantLabel = ${JSON.stringify(J)};
@@ -363,10 +365,10 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
363
365
  }
364
366
  return { ok: false };
365
367
  })()
366
- `));if(q?.ok)return q;await z.wait({time:Z/1000})}return q}async function Fz(z){const J=await z.evaluate(`
368
+ `));if(q?.ok)return q;await z.wait({time:Z/1000})}return q}async function Dz(z){const J=await z.evaluate(`
367
369
  (() => {
368
370
  const __opencli_xhs_focus_card = true;
369
- const sel = ${JSON.stringify(M)};
371
+ const sel = ${JSON.stringify(_)};
370
372
  const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
371
373
  // Prefer the editor inside the active swiper slide if present.
372
374
  const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
@@ -380,34 +382,34 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
380
382
  selection?.addRange(range);
381
383
  return { ok: true };
382
384
  })()
383
- `);return Y(J)}async function Oz(z){const J=await z.evaluate(`
385
+ `);return W(J)}async function fz(z){const J=await z.evaluate(`
384
386
  (() => {
385
387
  const __opencli_xhs_card_text = true;
386
- const sel = ${JSON.stringify(M)};
388
+ const sel = ${JSON.stringify(_)};
387
389
  const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
388
390
  const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
389
391
  const text = active ? (active.innerText || active.textContent || '').trim() : '';
390
392
  return { ok: !!text, text };
391
393
  })()
392
- `);return Y(J)}async function p(z){const J=await z.evaluate(`
394
+ `);return W(J)}async function a(z){const J=await z.evaluate(`
393
395
  (() => {
394
396
  const __opencli_xhs_card_count = true;
395
- const sel = ${JSON.stringify(M)};
397
+ const sel = ${JSON.stringify(_)};
396
398
  const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
397
399
  const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
398
400
  const activeText = active ? (active.innerText || active.textContent || '').trim() : '';
399
401
  return { ok: true, count: editors.length, activeEmpty: !activeText };
400
402
  })()
401
- `);return Y(J)}async function Uz(z,J=8000){const K=300,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if((await p(z))?.count>=1)return!0;await z.wait({time:K/1000})}return!1}async function Bz(z,J,K=6000){const Z=300,Q=Math.ceil(K/Z);for(let q=0;q<Q;q++){const $=await p(z);if($?.count>=J&&$?.activeEmpty)return!0;await z.wait({time:Z/1000})}return!1}async function vz(z,J,K=4){for(let Z=0;Z<K;Z++){if(!(await f(z,c))?.ok)return!1;if(await Bz(z,J,2500))return!0}return!1}async function Dz(z){const J=await z.evaluate(`
403
+ `);return W(J)}async function Nz(z,J=8000){const K=300,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if((await a(z))?.count>=1)return!0;await z.wait({time:K/1000})}return!1}async function kz(z,J,K=6000){const Z=300,Q=Math.ceil(K/Z);for(let q=0;q<Q;q++){const $=await a(z);if($?.count>=J&&$?.activeEmpty)return!0;await z.wait({time:Z/1000})}return!1}async function hz(z,J,K=4){for(let Z=0;Z<K;Z++){if(!(await k(z,n))?.ok)return!1;if(await kz(z,J,2500))return!0}return!1}async function Lz(z){const J=await z.evaluate(`
402
404
  (() => {
403
405
  const __opencli_xhs_preview_ready = true;
404
406
  const ready = Array.from(document.querySelectorAll('button'))
405
407
  .some((b) => b.offsetParent !== null && (b.innerText || '').replace(/\\s+/g, '') === '下一步');
406
408
  return { ok: ready };
407
409
  })()
408
- `);return Y(J)}async function fz(z,J=6){const K=400,Z=Math.ceil(3000/K);for(let Q=0;Q<J;Q++){if(!(await f(z,E))?.ok&&Q===0)return!1;for(let $=0;$<Z;$++){if((await Dz(z))?.ok)return!0;await z.wait({time:K/1000})}}return!1}async function Iz(z,J,K){J=String(J).replace(/\\n/g,`
409
- `);if(!(await Fz(z))?.ok)throw new H(`文字配图: could not focus card editor #${K+1}`);if(typeof z.insertText==="function"){const q=J.split(`
410
- `);for(let $=0;$<q.length;$++){if($>0&&typeof z.pressKey==="function")await z.pressKey("Enter");if(q[$])await z.insertText(q[$])}}else await z.evaluate(`(t => document.execCommand('insertText', false, t))(${JSON.stringify(J)})`);await z.wait({time:0.4});if(!(await Oz(z))?.ok)throw new H(`文字配图: card editor #${K+1} is empty after typing`)}async function Nz(z,J){if(!J||J===D)return D;const K=Y(await z.evaluate(`
410
+ `);return W(J)}async function Pz(z,J=6){const K=400,Z=Math.ceil(3000/K);for(let Q=0;Q<J;Q++){if(!(await k(z,m))?.ok&&Q===0)return!1;for(let $=0;$<Z;$++){if((await Lz(z))?.ok)return!0;await z.wait({time:K/1000})}}return!1}async function Iz(z,J,K){J=String(J).replace(/\\n/g,`
411
+ `);if(!(await Dz(z))?.ok)throw new H(`文字配图: could not focus card editor #${K+1}`);if(typeof z.insertText==="function"){const q=J.split(`
412
+ `);for(let $=0;$<q.length;$++){if($>0&&typeof z.pressKey==="function")await z.pressKey("Enter");if(q[$])await z.insertText(q[$])}}else await z.evaluate(`(t => document.execCommand('insertText', false, t))(${JSON.stringify(J)})`);await z.wait({time:0.4});if(!(await fz(z))?.ok)throw new H(`文字配图: card editor #${K+1} is empty after typing`)}async function Mz(z,J){if(!J||J===N)return N;const K=W(await z.evaluate(`
411
413
  (async () => {
412
414
  const __opencli_xhs_card_styles = true;
413
415
  const want = ${JSON.stringify(J)};
@@ -436,7 +438,7 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
436
438
  }
437
439
  return { ok: seen.length > 0, styles: seen, found: false };
438
440
  })()
439
- `));if(!K?.found)throw new H(`文字配图: requested style "${J}" is not available for this content (options: ${(K?.styles||[]).join(" / ")||"none"}). Choose an available style or omit --card-style to use ${D}.`);if(!(await f(z,J))?.ok)throw new H(`文字配图: could not click requested style "${J}".`);await z.wait({time:0.6});return J}async function t(z){const J=await z.evaluate(`
441
+ `));if(!K?.found)throw new H(`文字配图: requested style "${J}" is not available for this content (options: ${(K?.styles||[]).join(" / ")||"none"}). Choose an available style or omit --card-style to use ${N}.`);if(!(await k(z,J))?.ok)throw new H(`文字配图: could not click requested style "${J}".`);await z.wait({time:0.6});return J}async function e(z){const J=await z.evaluate(`
440
442
  (() => {
441
443
  const __opencli_xhs_composer_media_count = true;
442
444
  const visibleBox = (el) => {
@@ -449,7 +451,7 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
449
451
  const r = el.getBoundingClientRect();
450
452
  return r.width >= 48 && r.height >= 48;
451
453
  };
452
- const titleSelectors = ${JSON.stringify(w)};
454
+ const titleSelectors = ${JSON.stringify(A)};
453
455
  const titleEl = titleSelectors
454
456
  .map((sel) => Array.from(document.querySelectorAll(sel)))
455
457
  .flat()
@@ -468,7 +470,7 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
468
470
  }
469
471
  return { ok: true, count };
470
472
  })()
471
- `);return Y(J)}async function a(z,J,K){const Z=await t(z);if(!Z||typeof Z.count!=="number")throw new H(`${K}: could not verify current composer media count`);if(Z.count<J){await z.screenshot({path:"/tmp/xhs_publish_media_debug.png"});throw new H(`${K}: expected at least ${J} visible media item(s), got ${Z.count}. Debug screenshot: /tmp/xhs_publish_media_debug.png`)}}async function hz(z,J,K){if(!(await f(z,L))?.ok){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: could not click "${L}" entry. Debug: /tmp/xhs_publish_textimage_debug.png`)}if(!await Uz(z)){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: 写文字 card editor did not appear after clicking "${L}". Debug: /tmp/xhs_publish_textimage_debug.png`)}for(let j=0;j<J.length;j++){if(j>0){if(!await vz(z,j+1)){await z.screenshot({path:"/tmp/xhs_publish_addcard_debug.png"});throw new H(`文字配图: new card editor #${j+1} did not render after "${c}". Debug: /tmp/xhs_publish_addcard_debug.png`)}}await Iz(z,J[j],j)}if(!await fz(z)){await z.screenshot({path:"/tmp/xhs_publish_generate_debug.png"});throw new H(`文字配图: "${E}" did not advance to the 预览图片 step. `+"Debug: /tmp/xhs_publish_generate_debug.png")}const q=await Nz(z,K);if(!(await f(z,n))?.ok){await z.screenshot({path:"/tmp/xhs_publish_next_debug.png"});throw new H(`文字配图: could not click "${n}". Debug: /tmp/xhs_publish_next_debug.png`)}await z.wait({time:2});return q}async function e(z){return z.evaluate(`
473
+ `);return W(J)}async function zz(z,J,K){const Z=await e(z);if(!Z||typeof Z.count!=="number")throw new H(`${K}: could not verify current composer media count`);if(Z.count<J){await z.screenshot({path:"/tmp/xhs_publish_media_debug.png"});throw new H(`${K}: expected at least ${J} visible media item(s), got ${Z.count}. Debug screenshot: /tmp/xhs_publish_media_debug.png`)}}async function wz(z,J,K){if(!(await k(z,R))?.ok){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: could not click "${R}" entry. Debug: /tmp/xhs_publish_textimage_debug.png`)}if(!await Nz(z)){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: 写文字 card editor did not appear after clicking "${R}". Debug: /tmp/xhs_publish_textimage_debug.png`)}for(let j=0;j<J.length;j++){if(j>0){if(!await hz(z,j+1)){await z.screenshot({path:"/tmp/xhs_publish_addcard_debug.png"});throw new H(`文字配图: new card editor #${j+1} did not render after "${n}". Debug: /tmp/xhs_publish_addcard_debug.png`)}}await Iz(z,J[j],j)}if(!await Pz(z)){await z.screenshot({path:"/tmp/xhs_publish_generate_debug.png"});throw new H(`文字配图: "${m}" did not advance to the 预览图片 step. `+"Debug: /tmp/xhs_publish_generate_debug.png")}const q=await Mz(z,K);if(!(await k(z,i))?.ok){await z.screenshot({path:"/tmp/xhs_publish_next_debug.png"});throw new H(`文字配图: could not click "${i}". Debug: /tmp/xhs_publish_next_debug.png`)}await z.wait({time:2});return q}async function Jz(z){return z.evaluate(`
472
474
  () => {
473
475
  const text = (document.body?.innerText || '').replace(/s+/g, ' ').trim();
474
476
  const hasTitleInput = !!Array.from(document.querySelectorAll('input, textarea')).find((el) => {
@@ -498,41 +500,53 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
498
500
  const state = hasTitleInput ? 'editor_ready' : hasImageInput || !hasVideoSurface ? 'image_surface' : 'video_surface';
499
501
  return { state, hasTitleInput, hasImageInput, hasVideoSurface };
500
502
  }
501
- `)}async function kz(z,J=5000){const K=500,Z=Math.max(1,Math.ceil(J/K));let Q=await e(z);for(let q=0;q<Z;q++){if(Q.state!=="video_surface")return Q;if(q<Z-1){await z.wait({time:K/1000});Q=await e(z)}}return Q}async function yz(z,J=1e4){const K=1000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
503
+ `)}async function bz(z,J=5000){const K=500,Z=Math.max(1,Math.ceil(J/K));let Q=await Jz(z);for(let q=0;q<Z;q++){if(Q.state!=="video_surface")return Q;if(q<Z-1){await z.wait({time:K/1000});Q=await Jz(z)}}return Q}async function yz(z,J=1e4){const K=1000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
502
504
  (() => {
503
- const sels = ${JSON.stringify(w)};
505
+ const sels = ${JSON.stringify(A)};
504
506
  for (const sel of sels) {
505
507
  const el = document.querySelector(sel);
506
508
  if (el && el.offsetParent !== null) return true;
507
509
  }
508
510
  return false;
509
- })()`))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function Pz(z){const J=async()=>Y(await z.evaluate(`
511
+ })()`))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function Rz(z){const J=W(await z.evaluate(`
510
512
  (() => {
511
- const __opencli_xhs_restore_probe = true;
512
- const clearBtn = document.querySelector('.title-clear-all');
513
- const restored = !!clearBtn && clearBtn.offsetParent !== null && !!document.querySelector('.img-container');
514
- const cleanSurface = (document.body.innerText || '').includes('上传图片,或写文字生成图片');
515
- return { restored, cleanSurface };
513
+ const __opencli_xhs_restore_srcs = true;
514
+ return Array.from(document.querySelectorAll('.img-container img'))
515
+ .map((img) => img.currentSrc || img.src || '')
516
+ .filter(Boolean);
516
517
  })()
517
- `));let K=await J();if(!K?.restored)return!1;for(let Z=0;Z<3;Z++){await z.evaluate(`
518
- (async () => {
519
- const __opencli_xhs_restore_clear = true;
520
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
521
- const fire = (el) => {
522
- el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
523
- el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
524
- el.click();
525
- };
526
- const clearBtn = document.querySelector('.title-clear-all');
527
- if (!clearBtn) return false;
528
- fire(clearBtn);
529
- await sleep(800);
530
- const confirm = Array.from(document.querySelectorAll('button, [role="button"]'))
531
- .find((b) => b.offsetParent !== null && (b.innerText || '').replace(/\\s+/g, '') === '重新上传');
532
- if (confirm) fire(confirm);
533
- return !!confirm;
534
- })()
535
- `);for(let Q=0;Q<10;Q++){await z.wait({time:0.5});K=await J();if(K?.cleanSurface&&!K?.restored)return!0}}await z.screenshot({path:"/tmp/xhs_publish_restore_debug.png"});throw new H("检测到小红书恢复了上次未发布的编辑内容(含旧图),自动清空失败——继续发布会导致图片重复上传。"+"请到 creator.xiaohongshu.com 发布页手动删除恢复的内容后重试。Debug: /tmp/xhs_publish_restore_debug.png")}Qz({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:Zz.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不会展开 @文件 引用;长正文建议用 --file)"},{name:"file",required:!1,help:"从本机文件读取笔记正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题 (最多20字)"},{name:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${u} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${D}。可选: ${d.map(([z,J])=>`${z}(${J})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail"],func:async(z,J)=>{if(!z)throw Error("Browser page required");const K=String(J.title??"").trim(),Z=J.file?String(J.file).trim():"";let Q=String(J.content??"").trim();if(Z){if(Q)throw new X("正文位置参数和 --file 只能二选一,不能同时给");const V=B.resolve(Z);if(!y.existsSync(V))throw new X(`--file 指向的文件不存在: ${V}`);Q=y.readFileSync(V,"utf-8").trim();if(!Q)throw new X(`--file 指向的文件内容为空: ${V}`)}const q=J.images?String(J.images).split(",").map((V)=>V.trim()).filter(Boolean):[],$=J.topics?String(J.topics).split(",").map((V)=>V.trim()).filter(Boolean):[],j=Boolean(J.draft),W=J["card-text"]?String(J["card-text"]):"",v=W?W.split(u).map((V)=>V.trim()).filter(Boolean):[],b=J["card-style"]?String(J["card-style"]).trim():"",U=v.length>0;if(!K)throw new X("--title is required");if(K.length>T)throw new X(`Title is ${K.length} chars — must be ≤ ${T}`);if(!Q)throw new X("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");$z(Q,{fileFlag:"--file"});if(!U&&q.length===0)throw new X("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(q.length>C)throw new X(`Too many images: ${q.length} (max ${C})`);if(U&&q.some((V)=>B.extname(V).toLowerCase()===".gif"))throw new X("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const F=Hz(q);await z.goto(qz);let I="";for(let V=0;V<30;V++){await z.wait({time:0.5});I=await z.evaluate("() => location.href");if(I.includes("creator.xiaohongshu.com"))break}if(!I.includes("creator.xiaohongshu.com")){await z.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${I}) — session may have expired. `+"Re-capture browser login via: opencli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const N=await Wz(z);if((await kz(z,N?.ok?5000:2000)).state==="video_surface"){await z.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const V=N?.ok?`clicked "${N.text}"`:`visible candidates: ${(N?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${V}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}await Pz(z);let h=b;if(U)h=await hz(z,v,b);else{const V=await o(z,F);if(!V.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new H(`Image injection failed: ${V.error??"unknown"}. Debug screenshot: /tmp/xhs_publish_upload_debug.png`)}await z.wait({time:x/1000});await r(z);const O=await t(z);if(O&&typeof O.count==="number"&&O.count>F.length){await z.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new H(`上传后编辑器内有 ${O.count} 张图,超过本次注入的 ${F.length} 张——`+"疑似页面残留了上次未发布的旧图,已中止发布以避免图片重复。Debug: /tmp/xhs_publish_dup_debug.png")}}if(!await yz(z)){await z.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new H("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(U)await a(z,v.length,"文字配图 generated images");if(U&&F.length>0){const V=await o(z,F);if(!V.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new H(`Appending images failed: ${V.error??"unknown"}. Debug screenshot: /tmp/xhs_publish_append_debug.png`)}await z.wait({time:x/1000});await r(z);await a(z,v.length+F.length,"文字配图 appended images")}await g(z,w,K,"title");await z.wait({time:0.5});await g(z,m,Q,"content");await z.wait({time:0.5});let P=[];if($.length)P=await Xz(z,m,$);const R=j?["暂存离开","存草稿"]:["发布","发布笔记"],G=await z.evaluate(`
518
+ `));return Array.isArray(J)?J:[]}async function Kz(z,J){if(!Array.isArray(J)||J.length===0)return 0;const K=W(await z.evaluate(`
519
+ (async (wanted) => {
520
+ const __opencli_xhs_delete_by_src = true;
521
+ const want = new Set(wanted);
522
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
523
+ let removed = 0;
524
+ // guard 上限防死循环;每轮只删一张(删后 DOM 重排,重新查询)。
525
+ for (let guard = 0; guard < 60; guard++) {
526
+ const cards = Array.from(document.querySelectorAll('.img-container'));
527
+ const target = cards.find((c) => {
528
+ const img = c.querySelector('img');
529
+ const s = img ? (img.currentSrc || img.src || '') : '';
530
+ return want.has(s);
531
+ });
532
+ if (!target) break;
533
+ const img = target.querySelector('img');
534
+ // .close-btn 藏在 hover-mask 里,先补 hover 事件让它显形再点。
535
+ ['pointerover', 'mouseover', 'mouseenter', 'mousemove'].forEach((t) => {
536
+ target.dispatchEvent(new MouseEvent(t, { bubbles: true }));
537
+ if (img) img.dispatchEvent(new MouseEvent(t, { bubbles: true }));
538
+ });
539
+ const b = target.querySelector('.close-btn');
540
+ if (!b) break;
541
+ b.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
542
+ b.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
543
+ b.click();
544
+ removed += 1;
545
+ await sleep(350);
546
+ }
547
+ return removed;
548
+ })(${JSON.stringify(J)})
549
+ `));return Number(K)||0}function _z(z){const J=String(z&&(z.url||z.key)||""),K=String(z&&z.method||"").toUpperCase(),Z=Number(z&&z.status||0);if(K!=="PUT")return null;if(!/ros-upload[^/]*\/spectrum\//.test(J))return null;if(Z<200||Z>=300)return null;const Q=J.match(/\/spectrum\/([^?#]+)/);return Q?Q[1]:J}function Qz(z){return Math.min(45,6+z*4)}async function Zz(z,J,K,Z=90000){if(J<=0)return{ok:!0,completed:0};if(!K||typeof z.readNetworkCapture!=="function"){await z.wait({time:Qz(J)});return{ok:!0,completed:null,viaFallback:!0}}const Q=1500,$=Math.ceil(9000/Q),j=Math.ceil(Z/Q),Y=new Set;for(let U=0;U<j;U++){const f=await z.readNetworkCapture().catch(()=>[]);for(const B of Array.isArray(f)?f:[]){const G=_z(B);if(G)Y.add(G)}if(Y.size>=J)return{ok:!0,completed:Y.size};if(U>=$&&Y.size===0){await z.wait({time:Qz(J)});return{ok:!0,completed:null,viaFallback:!0}}await z.wait({time:Q/1000})}return{ok:!1,completed:Y.size}}Vz({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:Hz.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不会展开 @文件 引用;长正文建议用 --file)"},{name:"file",required:!1,help:"从本机文件读取笔记正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题 (最多20字)"},{name:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${c} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${N}。可选: ${d.map(([z,J])=>`${z}(${J})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail"],func:async(z,J)=>{if(!z)throw Error("Browser page required");const K=String(J.title??"").trim(),Z=J.file?String(J.file).trim():"";let Q=String(J.content??"").trim();if(Z){if(Q)throw new F("正文位置参数和 --file 只能二选一,不能同时给");const V=D.resolve(Z);if(!b.existsSync(V))throw new F(`--file 指向的文件不存在: ${V}`);Q=b.readFileSync(V,"utf-8").trim();if(!Q)throw new F(`--file 指向的文件内容为空: ${V}`)}const q=J.images?String(J.images).split(",").map((V)=>V.trim()).filter(Boolean):[],$=J.topics?String(J.topics).split(",").map((V)=>V.trim()).filter(Boolean):[],j=Boolean(J.draft),Y=J["card-text"]?String(J["card-text"]):"",U=Y?Y.split(c).map((V)=>V.trim()).filter(Boolean):[],f=J["card-style"]?String(J["card-style"]).trim():"",B=U.length>0;if(!K)throw new F("--title is required");if(K.length>E)throw new F(`Title is ${K.length} chars — must be ≤ ${E}`);if(!Q)throw new F("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");Gz(Q,{fileFlag:"--file"});if(!B&&q.length===0)throw new F("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(q.length>x)throw new F(`Too many images: ${q.length} (max ${x})`);if(B&&q.some((V)=>D.extname(V).toLowerCase()===".gif"))throw new F("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const G=Fz(q);await z.goto(Yz);await z.wait({time:1});await z.evaluate("() => { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }").catch(()=>{return});let h="";for(let V=0;V<30;V++){await z.wait({time:0.5});h=await z.evaluate("() => location.href");if(h.includes("creator.xiaohongshu.com"))break}if(!h.includes("creator.xiaohongshu.com")){await z.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${h}) — session may have expired. `+"Re-capture browser login via: opencli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const L=await vz(z);if((await bz(z,L?.ok?5000:2000)).state==="video_surface"){await z.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const V=L?.ok?`clicked "${L.text}"`:`visible candidates: ${(L?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${V}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}const P=await Rz(z);let I=f;if(B)I=await wz(z,U,f);else{const V=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,O=await r(z,G);if(!O.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new H(`图片注入失败:${O.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_upload_debug.png")}await z.wait({time:u/1000});const v=await Zz(z,G.length,V);if(!v.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new H(`图片上传未全部完成:只确认 ${v.completed}/${G.length} 张传到小红书 CDN,`+"继续发布会丢图(通常只剩封面)。直接重新运行本 publish 命令即可(会自动重置发布页重传);"+"请勿手动修改当前页面。Debug: /tmp/xhs_publish_upload_debug.png")}await g(z);if(P.length){await Kz(z,P);await z.wait({time:0.5})}const w=await e(z);if(w&&typeof w.count==="number"&&w.count>G.length){await z.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new H(`清掉恢复态旧图后编辑器内仍有 ${w.count} 张图,超过本次注入的 ${G.length} 张。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。Debug: /tmp/xhs_publish_dup_debug.png")}}if(!await yz(z)){await z.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new H("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(B)await zz(z,U.length,"文字配图 generated images");if(B&&G.length>0){const V=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,O=await r(z,G);if(!O.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new H(`追加图片失败:${O.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await z.wait({time:u/1000});const v=await Zz(z,G.length,V);if(!v.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new H(`追加图片上传未全部完成:只确认 ${v.completed}/${G.length} 张传到小红书 CDN。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await g(z);await zz(z,U.length+G.length,"文字配图 appended images")}if(B&&P.length){await Kz(z,P);await z.wait({time:0.5})}await p(z,A,K,"title");await z.wait({time:0.5});await p(z,l,Q,"content");await z.wait({time:0.5});let y=[];if($.length)y=await Bz(z,l,$);const T=j?["暂存离开","存草稿"]:["发布","发布笔记"],X=await z.evaluate(`
536
550
  (cfg => {
537
551
  const { isDraftMode, publishNames, draftNames, labels } = cfg;
538
552
  const isVisible = (el) => {
@@ -571,8 +585,8 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
571
585
  }
572
586
  }
573
587
  return { ok: false, via: 'none', hosts: hosts.length, lastMethodError };
574
- })(${JSON.stringify({isDraftMode:j,publishNames:jz,draftNames:Vz,labels:R})})
575
- `);if(!G?.ok){if(j){if(await z.evaluate(`
588
+ })(${JSON.stringify({isDraftMode:j,publishNames:Xz,draftNames:Wz,labels:T})})
589
+ `);if(!X?.ok){if(j){if(await z.evaluate(`
576
590
  (() => {
577
591
  const labels = ['返回', '关闭', '取消', '离开'];
578
592
  const buttons = document.querySelectorAll('button, [role="button"], div, span');
@@ -601,7 +615,7 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
601
615
  }
602
616
  return false;
603
617
  })(${JSON.stringify(O)})
604
- `)){G.ok=!0;G.via="leave-save-fallback"}}if(!G?.ok){await z.wait({time:2});if(await z.evaluate(`
618
+ `)){X.ok=!0;X.via="leave-save-fallback"}}if(!X?.ok){await z.wait({time:2});if(await z.evaluate(`
605
619
  () => {
606
620
  const markers = ['草稿箱(', '保存于', '编辑于'];
607
621
  for (const el of document.querySelectorAll('*')) {
@@ -610,7 +624,7 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
610
624
  }
611
625
  return false;
612
626
  }
613
- `)){G.ok=!0;G.via="auto-save"}}}}if(!G?.ok){await z.screenshot({path:"/tmp/xhs_publish_submit_debug.png"});const V=G?.via?` (via=${G.via})`:"",O=G?.error?`, error=${G.error}`:"",S=G?.lastMethodError?`, lastMethodError=${G.lastMethodError}`:"";throw Error(`Could not trigger "${R[0]}" action${V}${O}${S}. Debug screenshot: /tmp/xhs_publish_submit_debug.png`)}await z.wait({time:4});const k=await z.evaluate("() => location.href"),zz=j?["草稿已保存","暂存成功","保存成功","保存于","图文笔记("]:["发布成功","上传成功"],_=await z.evaluate(`
627
+ `)){X.ok=!0;X.via="auto-save"}}}}if(!X?.ok){await z.screenshot({path:"/tmp/xhs_publish_submit_debug.png"});const V=X?.via?` (via=${X.via})`:"",O=X?.error?`, error=${X.error}`:"",v=X?.lastMethodError?`, lastMethodError=${X.lastMethodError}`:"";throw Error(`未能触发「${T[0]}」按钮${V}${O}${v}。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_submit_debug.png")}await z.wait({time:4});const M=await z.evaluate("() => location.href"),$z=j?["草稿已保存","暂存成功","保存成功","保存于","图文笔记("]:["发布成功","上传成功"],S=await z.evaluate(`
614
628
  (markers => {
615
629
  for (const el of document.querySelectorAll('*')) {
616
630
  if (el.tagName === 'STYLE' || el.tagName === 'SCRIPT') continue;
@@ -619,5 +633,5 @@ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionErr
619
633
  if (el.children.length === 0 && markers.some(marker => text.includes(marker))) return text;
620
634
  }
621
635
  return '';
622
- })(${JSON.stringify(zz)})
623
- `),Jz=!k.includes("/publish/publish"),Kz=_.length>0||Jz,A=j?"暂存成功":"发布成功";if(!Kz)throw new H(`${A} could not be verified: no success marker or post-submit navigation was observed. `+(k?`Current URL: ${k}`:"Current URL was empty."));return[{status:`✅ ${A}`,detail:[`"${K}"`,U?`${v.length}张文字配图${F.length?` + ${F.length}张图片`:""}${h&&h!==D?` (${h})`:""}`:`${F.length}张图片`,P.length?`话题: ${P.join(" ")}`:"",_||k||""].filter(Boolean).join(" · ")}]}});
636
+ })(${JSON.stringify($z)})
637
+ `),qz=!M.includes("/publish/publish"),jz=S.length>0||qz,C=j?"暂存成功":"发布成功";if(!jz)throw new H(`${C}无法确认:未观察到成功提示或提交后跳转。`+(M?`当前 URL: ${M}。`:"当前 URL 为空。")+"若确属失败,直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。");return[{status:`✅ ${C}`,detail:[`"${K}"`,B?`${U.length}张文字配图${G.length?` + ${G.length}张图片`:""}${I&&I!==N?` (${I})`:""}`:`${G.length}张图片`,y.length?`话题: ${y.join(" ")}`:"",S||M||""].filter(Boolean).join(" · ")}]}});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.8.5-pp.35",
3
+ "version": "1.8.5-pp.37",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false