publishport-opencli 1.0.15 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ import{cli as W,Strategy as X}from"@jackwener/opencli/registry";import{CommandExecutionError as O,EmptyResultError as Y}from"@jackwener/opencli/errors";import{requirePage as _,navigateToGeekChat as B,geekPost as H,fetchGeekBossData as S,fetchGeekLastMsgs as j,resolveGeekBossId as q,resolveGeekResumeId as f,verbose as K}from"./utils.js";const C=["是否同意","交换","附件简历","联系方式"];W({site:"boss",name:"accept",access:"write",description:"BOSS直聘同意/拒绝 Boss 发来的交换请求(求职端,手机/微信/简历)",domain:"www.zhipin.com",strategy:X.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"uid",positional:!0,required:!0,help:"Boss 的 uid(数字 friendId 或 encryptFriendId,从 boss chatlist --side geek 拿)"},{name:"reject",type:"boolean",default:!1,help:"拒绝请求(默认同意)"},{name:"resume-id",default:"",help:"HR 索要简历时指定发哪份 encryptResumeId(从 boss resumelist 拿)"}],columns:["status","detail"],func:async(F,Z)=>{_(F);await B(F);const z=await q(F,Z.uid),L=await j(F,[z]);if(L.length===0)throw new Y("boss accept",`找不到与 uid=${z} 的消息记录`);const M=L[0].lastMsgInfo||{},Q=M.msgId,J=M.showText||"";if(!Q)throw new O("无法获取消息 ID(lastMsgInfo.msgId 缺失)");if(!C.some((T)=>J.includes(T)))throw new O(`最新消息不是交换请求,不处理:「${J.slice(0,40)}」`);const U=await S(F,z),V=U.name||U.bossName||z,$=U.securityId?{securityId:U.securityId}:{};if(Z.reject){K(`Rejecting exchange from boss ${z}...`);await H(F,"/wapi/zpchat/geek/rejectItemContact",{bossId:z,mid:Q,...$});return[{status:"✅ 已拒绝交换请求",detail:`已拒绝 ${V} 的请求:${J.slice(0,40)}`}]}if(J.includes("微信")){K(`Accepting wechat exchange from boss ${z}...`);await H(F,"/wapi/zpchat/geek/acceptItemWeiXinRequest",{bossId:z,mid:Q,...$});return[{status:"✅ 已同意交换微信",detail:`已同意 ${V} 的微信交换请求`}]}const N=J.includes("简历")||J.includes("附件"),A={bossId:z,mid:Q,...$};if(N)A.encryptResumeId=await f(F,Z["resume-id"]);K(`Accepting exchange from boss ${z}...`);await H(F,"/wapi/zpchat/geek/acceptItemContact",A);return[{status:"✅ 已同意交换请求",detail:N?`已向 ${V} 发送附件简历 ${A.encryptResumeId}`:`已同意 ${V} 的请求:${J.slice(0,40)}`}]}});
@@ -0,0 +1 @@
1
+ import{cli as n,Strategy as m}from"@jackwener/opencli/registry";import{EmptyResultError as p}from"@jackwener/opencli/errors";import{requirePage as l,navigateToGeekJob as u,geekGet as d,verbose as c}from"./utils.js";n({site:"boss",name:"applied",access:"read",description:"BOSS直聘查看已投递的职位(求职端)",domain:"www.zhipin.com",strategy:m.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"page",type:"int",default:1,help:"Page number"},{name:"limit",type:"int",default:30,help:"Number of results to return"}],columns:["job_name","company","salary","status","time","security_id"],func:async(a,r)=>{l(a);c("Fetching applied jobs...");await u(a);const s=(await d(a,"/wapi/zprelation/resume/geekDeliverList",{page:r.page||1})).cardList??[];if(s.length===0)throw new p("boss applied","暂无投递记录。");const i=r.limit||30;return s.slice(0,i).map((e)=>{const t=e.jobInfo??e,o=e.brandInfo??e;return{job_name:t.jobName||e.jobName||"",company:o.brandName||e.brandName||"",salary:t.salaryDesc||e.salaryDesc||"",status:e.deliverStatusDesc||e.statusDesc||"",time:e.updateTimeDesc||e.createTimeDesc||"",security_id:t.securityId||e.securityId||""}})}});
package/clis/boss/auth.js CHANGED
@@ -1,13 +1 @@
1
- import{AuthRequiredError as i,CommandExecutionError as a}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as c}from"../_shared/site-auth.js";async function s(t){const e=await t.getCookies({url:"https://www.zhipin.com"}),r=new Set(e.filter((o)=>o.value).map((o)=>o.name));return r.has("wt2")||r.has("t")}async function n(t){if(!await s(t))throw new i("zhipin.com","Boss wt2 / t cookies missing");await t.goto("https://www.zhipin.com/web/geek/job-recommend");await t.wait(3);const e=await t.evaluate(`
2
- (() => {
3
- const path = location.pathname || '';
4
- if (/\\/web\\/user\\/login|\\/login\\.html/.test(location.href)) {
5
- return { kind: 'auth', detail: 'Boss redirected to login page' };
6
- }
7
- const userType = /\\/web\\/geek\\//.test(path) ? 'geek' : /\\/web\\/(boss|recruit|chat\\/boss)/.test(path) ? 'recruiter' : '';
8
- if (!userType) {
9
- return { kind: 'auth', detail: 'Boss path does not look like authenticated geek/recruiter page: ' + path };
10
- }
11
- return { ok: true, user_type: userType };
12
- })()
13
- `);if(e?.kind==="auth")throw new i("zhipin.com",e.detail);if(!e?.ok)throw new a(`Unexpected Boss probe: ${JSON.stringify(e)}`);return{user_type:e.user_type}}c({site:"boss",domain:"zhipin.com",loginUrl:"https://login.zhipin.com/",columns:["user_type"],quickCheck:s,verify:n,poll:async(t)=>{if(!await s(t))throw new i("zhipin.com","Waiting for Boss wt2 / t cookies");return n(t)}});
1
+ import{AuthRequiredError as J,CommandExecutionError as Z}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as W}from"../_shared/site-auth.js";async function M(b){const D=await b.getCookies({url:"https://www.zhipin.com"}),F=new Set(D.filter((K)=>K.value).map((K)=>K.name));return F.has("wt2")||F.has("t")}const P=/login\.zhipin\.com|\/web\/user\/|\/login\.html/;function X(b){const D=(()=>{try{return new URL(b).pathname||""}catch{return String(b||"")}})();if(/^\/web\/geek\//.test(D))return"geek";if(/^\/web\/(boss|recruit)\//.test(D))return"recruiter";if(/^\/web\/chat\//.test(D))return"recruiter";return""}async function Q(b){const D=await b.evaluate("location.href");return typeof D==="string"?D:""}async function V(b){if(!await M(b))throw new J("zhipin.com","Boss wt2 / t cookies missing");await b.goto("https://www.zhipin.com/web/geek/job-recommend");await b.wait(3);const D=await Q(b);if(P.test(D))throw new J("zhipin.com","Boss redirected to login page");const F=X(D);if(!F)return{user_type:"unknown"};return{user_type:F}}W({site:"boss",domain:"zhipin.com",loginUrl:"https://login.zhipin.com/",columns:["user_type"],quickCheck:M,verify:V,poll:async(b)=>{if(!await M(b))throw new J("zhipin.com","Waiting for Boss wt2 / t cookies");const D=await Q(b);if(!D||P.test(D))throw new J("zhipin.com","Waiting for Boss to leave the login page");return V(b)}});
@@ -1 +1 @@
1
- import{cli as l,Strategy as u}from"@jackwener/opencli/registry";import{requirePage as f,navigateToChat as h,fetchRecommendList as b,clickCandidateInList as g,typeAndSendMessage as p,verbose as w}from"./utils.js";l({site:"boss",name:"batchgreet",access:"write",description:"BOSS直聘批量向推荐候选人发送招呼",domain:"www.zhipin.com",strategy:u.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"job-id",default:"",help:"Filter by encrypted job ID (greet all jobs if empty)"},{name:"limit",type:"int",default:5,help:"Max candidates to greet"},{name:"text",fromFile:!0,default:"",help:"Custom greeting message (uses default if empty)"}],columns:["name","status","detail"],func:async(t,n)=>{f(t);const c=n["job-id"]||"",d=n.limit||5,r=n.text||"你好,请问您对这个职位感兴趣吗?";w(`Batch greeting up to ${d} candidates...`);await h(t,3);let e=await b(t);if(c)e=e.filter((i)=>i.encryptJobId===c);e=e.slice(0,d);if(e.length===0)return[{name:"-",status:"⚠️ 无候选人",detail:"当前没有待招呼的推荐候选人"}];const a=[];for(const i of e){const m=i.uid,s=i.name||"候选人";try{if(!await g(t,m)){a.push({name:s,status:"❌ 跳过",detail:"在聊天列表中未找到"});continue}await t.wait({time:2});if(!await p(t,r)){a.push({name:s,status:"❌ 失败",detail:"找不到消息输入框"});continue}await t.wait({time:1.5});a.push({name:s,status:"✅ 已发送",detail:r})}catch(o){a.push({name:s,status:"❌ 失败",detail:o.message?.substring(0,80)||"未知错误"})}}return a}});
1
+ import{cli as F,Strategy as P}from"@jackwener/opencli/registry";import{ArgumentError as U}from"@jackwener/opencli/errors";import{requirePage as C,navigateToChat as L,navigateToGeekJob as M,fetchRecommendList as T,clickCandidateInList as j,typeAndSendMessage as x,geekAddFriend as z,verbose as $}from"./utils.js";async function q(D,B){const V=String(B["security-ids"]||"").split(",").map((H)=>H.trim()).filter(Boolean);if(V.length===0)throw new U("求职端批量投递需要 --security-ids(逗号分隔的职位 securityId,先跑 boss search 拿 security_id 列)");const X=B.limit||5,Q=V.slice(0,X),Z=Math.round((B.delay??1.5)*1000);if(B["dry-run"])return Q.map((H,K)=>({name:`#${K+1}`,status:"\uD83D\uDCCB 预览",detail:`将投递 securityId=${H}(未实际发送)`}));$(`Batch applying to ${Q.length} jobs...`);await M(D);const O=[];for(let H=0;H<Q.length;H++){const K=Q[H];try{const W=await z(D,K,B.lid||"");O.push({name:W?.bossName||K.slice(0,12),status:"✅ 已投递",detail:W?.jobName?`${W.jobName}`:K})}catch(W){O.push({name:K.slice(0,12),status:"❌ 失败",detail:W.message?.substring(0,80)||"未知错误"})}if(H<Q.length-1)await D.wait({time:Z/1000})}return O}async function E(D,B){const V=B["job-id"]||"",X=B.limit||5,Q=B.text||"你好,请问您对这个职位感兴趣吗?",Z=B.delay??1.5;$(`Batch greeting up to ${X} candidates...`);await L(D,3);let O=await T(D);if(V)O=O.filter((K)=>K.encryptJobId===V);O=O.slice(0,X);if(O.length===0)return[{name:"-",status:"⚠️ 无候选人",detail:"当前没有待招呼的推荐候选人"}];if(B["dry-run"])return O.map((K)=>({name:K.name||"候选人",status:"\uD83D\uDCCB 预览",detail:`将发送:${Q}(未实际发送)`}));const H=[];for(const K of O){const W=K.uid,Y=K.name||"候选人";try{if(!await j(D,W)){H.push({name:Y,status:"❌ 跳过",detail:"在聊天列表中未找到"});continue}await D.wait({time:2});if(!await x(D,Q)){H.push({name:Y,status:"❌ 失败",detail:"找不到消息输入框"});continue}await D.wait({time:Z});H.push({name:Y,status:"✅ 已发送",detail:Q})}catch(_){H.push({name:Y,status:"❌ 失败",detail:_.message?.substring(0,80)||"未知错误"})}}return H}F({site:"boss",name:"batchgreet",access:"write",description:"BOSS直聘批量打招呼(求职端批量投递职位 / 招聘端批量招呼候选人)",domain:"www.zhipin.com",strategy:P.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"side",default:"auto",choices:["auto","geek","boss"],help:"Identity side: auto (default), geek (job-seeker), boss (recruiter)"},{name:"security-ids",default:"",help:"[geek] 逗号分隔的职位 securityId(先跑 boss search 拿 security_id 列)"},{name:"lid",default:"",help:"[geek] Lid parameter from search results (optional)"},{name:"job-id",default:"",help:"[boss] Filter by encrypted job ID (greet all jobs if empty)"},{name:"text",fromFile:!0,default:"",help:"[boss] Custom greeting message (uses default if empty)"},{name:"limit",type:"int",default:5,help:"Max targets to greet"},{name:"delay",type:"number",default:1.5,help:"Seconds to wait between greetings (风控节流,别调太低)"},{name:"dry-run",type:"boolean",default:!1,help:"只预览将要打招呼的目标,不实际发送"}],columns:["name","status","detail"],func:async(D,B)=>{C(D);const V=B.side||"auto";if(V==="geek")return q(D,B);if(V==="boss")return E(D,B);return B["security-ids"]?q(D,B):E(D,B)}});
@@ -0,0 +1 @@
1
+ import{cli as a,Strategy as i}from"@jackwener/opencli/registry";import{requirePage as c,navigateToGeekChat as r,fetchGeekBossData as d,resolveGeekBossId as m,verbose as b}from"./utils.js";a({site:"boss",name:"bossinfo",access:"read",description:"BOSS直聘查看与某个 Boss 的会话上下文(求职端)",domain:"www.zhipin.com",strategy:i.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"uid",positional:!0,required:!0,help:"Boss 的 uid(数字 friendId 或 encryptFriendId,从 boss chatlist --side geek 拿)"}],columns:["name","company","job","security_id","encrypt_job_id","phone","wechat","exchange"],func:async(o,n)=>{c(o);await r(o);const t=await m(o,n.uid);b(`Fetching boss data for ${t}...`);const e=await d(o,t),s=e.exchange||{};return[{name:e.name||e.bossName||"",company:e.brandName||e.company||"",job:e.jobName||"",security_id:e.securityId||"",encrypt_job_id:e.encryptJobId||"",phone:s.phone||e.phone||"",wechat:s.weixin||s.wechat||e.weixin||"",exchange:Object.keys(s).length?JSON.stringify(s):""}]}});
@@ -0,0 +1 @@
1
+ import{cli as c,Strategy as i}from"@jackwener/opencli/registry";import{CITY_CODES as t}from"./utils.js";c({site:"boss",name:"cities",access:"read",description:"BOSS直聘列出支持的城市与城市代码",domain:"www.zhipin.com",strategy:i.COOKIE,navigateBefore:!1,browser:!1,args:[],columns:["city","code"],func:async()=>Object.entries(t).map(([e,s])=>({city:e,code:s}))});
@@ -1 +1 @@
1
- import{cli as c,Strategy as o}from"@jackwener/opencli/registry";import{requirePage as s,navigateToChat as h,bossFetch as d,findFriendByUid as p,verbose as u}from"./utils.js";c({site:"boss",name:"exchange",access:"write",description:"BOSS直聘交换联系方式(请求手机/微信)",domain:"www.zhipin.com",strategy:o.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"uid",required:!0,positional:!0,help:"Encrypted UID of the candidate"},{name:"type",default:"phone",choices:["phone","wechat"],help:"Exchange type: phone or wechat"}],columns:["status","detail"],func:async(e,a)=>{s(e);const n=a.type||"phone";u(`Requesting ${n} exchange for ${a.uid}...`);await h(e);const t=await p(e,a.uid,{checkGreetList:!0});if(!t)throw Error("未找到该候选人");const i=t.name||"候选人",r=new URLSearchParams({type:String(n==="wechat"?2:1),securityId:t.securityId||"",uniqueId:String(t.uid),name:i});await d(e,"https://www.zhipin.com/wapi/zpchat/exchange/request",{method:"POST",body:r.toString()});return[{status:"✅ 交换请求已发送",detail:`已向 ${i} 发送${n==="wechat"?"微信":"手机号"}交换请求`}]}});
1
+ import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{CommandExecutionError as _}from"@jackwener/opencli/errors";import{requirePage as $,navigateToChat as q,navigateToGeekChat as U,bossFetch as E,findFriendByUid as F,geekPost as L,fetchGeekBossData as N,resolveGeekBossId as P,isRecruiterSide as S,verbose as Q}from"./utils.js";function V(z){return z==="wechat"?"微信":"手机号"}async function W(z,H){const J=H.type||"phone";await U(z);const K=await P(z,H.uid),M=await N(z,K),O=M.securityId||"";if(!O)throw new _(`无法获取 securityId (uid=${K}),该会话可能已失效`);Q(`Requesting ${J} exchange from boss ${K}...`);await L(z,"/wapi/zpchat/exchange/request",{type:J==="wechat"?2:1,bossId:K,securityId:O});return[{status:"✅ 交换请求已发送",detail:`已向 ${M.name||M.bossName||K} 请求交换${V(J)}`}]}async function X(z,H){const J=H.type||"phone";Q(`Requesting ${J} exchange for ${H.uid}...`);await q(z);const K=await F(z,H.uid,{checkGreetList:!0});if(!K)throw Error("未找到该候选人");const M=K.name||"候选人",O=new URLSearchParams({type:String(J==="wechat"?2:1),securityId:K.securityId||"",uniqueId:String(K.uid),name:M});await E(z,"https://www.zhipin.com/wapi/zpchat/exchange/request",{method:"POST",body:O.toString()});return[{status:"✅ 交换请求已发送",detail:`已向 ${M} 发送${V(J)}交换请求`}]}Y({site:"boss",name:"exchange",access:"write",description:"BOSS直聘请求交换联系方式(手机/微信,求职端与招聘端都支持)",domain:"www.zhipin.com",strategy:Z.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"uid",required:!0,positional:!0,help:"对方 uid:求职端=Boss 的 friendId/encryptFriendId;招聘端=候选人的 encryptUid"},{name:"type",default:"phone",choices:["phone","wechat"],help:"Exchange type: phone or wechat"},{name:"side",default:"auto",choices:["auto","geek","boss"],help:"Identity side: auto (default), geek (job-seeker), boss (recruiter)"}],columns:["status","detail"],func:async(z,H)=>{$(z);const J=H.side||"auto";if(J==="geek")return W(z,H);if(J==="boss")return X(z,H);return await S(z)?X(z,H):W(z,H)}});
@@ -1 +1 @@
1
- import{cli as K,Strategy as O}from"@jackwener/opencli/registry";import{requirePage as Q,navigateToChat as R,findFriendByUid as V,clickCandidateInList as W,typeAndSendMessage as X,verbose as Y}from"./utils.js";K({site:"boss",name:"greet",access:"write",description:"BOSS直聘向新候选人发送招呼(开始聊天)",domain:"www.zhipin.com",strategy:O.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"uid",positional:!0,required:!0,help:"Encrypted UID of the candidate (from recommend)"},{name:"security-id",required:!0,help:"Security ID of the candidate"},{name:"job-id",required:!0,help:"Encrypted job ID"},{name:"text",fromFile:!0,default:"",help:"Custom greeting message (uses default template if empty)"}],columns:["status","detail"],func:async(z,D)=>{Q(z);Y(`Greeting candidate ${D.uid}...`);await R(z,3);const E=await V(z,D.uid,{maxPages:1,checkGreetList:!0});if(!E)throw Error("未找到该候选人,请确认 uid 是否正确(可从 recommend 命令获取)");const H=E.uid,J=E.name||"候选人";if(!await W(z,H))throw Error("无法在聊天列表中找到该用户,候选人可能不在当前列表中");await z.wait({time:2});const G=D.text||"你好,请问您对这个职位感兴趣吗?";if(!await X(z,G))throw Error("找不到消息输入框");await z.wait({time:1});return[{status:"✅ 招呼已发送",detail:`已向 ${J} 发送: ${G}`}]}});
1
+ import{cli as _,Strategy as $}from"@jackwener/opencli/registry";import{ArgumentError as V}from"@jackwener/opencli/errors";import{requirePage as h,navigateToChat as q,navigateToGeekJob as B,findFriendByUid as J,clickCandidateInList as L,typeAndSendMessage as M,geekAddFriend as N,verbose as W}from"./utils.js";async function X(D,H){const K=H["security-id"];if(!K)throw new V("求职端打招呼需要 --security-id(从 boss search / boss recommend 的 security_id 列拿)");W(`Greeting boss for job ${K}...`);await B(D);const O=await N(D,K,H.lid||"");return[{status:"✅ 打招呼成功",detail:O?.bossName?`已向 ${O.bossName} 打招呼,用 boss chatlist --side geek 看会话`:"已发送打招呼 / 投递,用 boss chatlist --side geek 看会话"}]}async function Y(D,H){if(!H.uid)throw new V("招聘端打招呼需要 uid 参数(候选人 encryptUid,从 boss recommend 拿)");W(`Greeting candidate ${H.uid}...`);await q(D,3);const K=await J(D,H.uid,{maxPages:1,checkGreetList:!0});if(!K)throw Error("未找到该候选人,请确认 uid 是否正确(可从 recommend 命令获取)");const O=K.uid,Z=K.name||"候选人";if(!await L(D,O))throw Error("无法在聊天列表中找到该用户,候选人可能不在当前列表中");await D.wait({time:2});const Q=H.text||"你好,请问您对这个职位感兴趣吗?";if(!await M(D,Q))throw Error("找不到消息输入框");await D.wait({time:1});return[{status:"✅ 招呼已发送",detail:`已向 ${Z} 发送: ${Q}`}]}_({site:"boss",name:"greet",access:"write",description:"BOSS直聘打招呼(求职端向 Boss 投递简历 / 招聘端向候选人发招呼)",domain:"www.zhipin.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"uid",positional:!0,default:"",help:"[boss] Encrypted UID of the candidate (from recommend)"},{name:"security-id",required:!0,help:"Security ID:求职端=职位的 securityId(boss search 结果);招聘端=候选人的 securityId"},{name:"side",default:"auto",choices:["auto","geek","boss"],help:"Identity side: auto (default), geek (job-seeker), boss (recruiter)"},{name:"lid",default:"",help:"[geek] Lid parameter from search results (optional)"},{name:"job-id",default:"",help:"[boss] Encrypted job ID"},{name:"text",fromFile:!0,default:"",help:"[boss] Custom greeting message (uses default template if empty)"}],columns:["status","detail"],func:async(D,H)=>{h(D);const K=H.side||"auto";if(K==="geek")return X(D,H);if(K==="boss")return Y(D,H);return H.uid?Y(D,H):X(D,H)}});
@@ -0,0 +1 @@
1
+ import{cli as s,Strategy as a}from"@jackwener/opencli/registry";import{EmptyResultError as n}from"@jackwener/opencli/errors";import{requirePage as m,navigateToGeekJob as l,geekGet as c,verbose as b}from"./utils.js";s({site:"boss",name:"history",access:"read",description:"BOSS直聘查看职位浏览历史(求职端)",domain:"www.zhipin.com",strategy:a.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"page",type:"int",default:1,help:"Page number"},{name:"limit",type:"int",default:30,help:"Number of results to return"}],columns:["job_name","company","salary","area","boss","security_id","url"],func:async(t,i)=>{m(t);b("Fetching browsing history...");await l(t);const o=(await c(t,"/wapi/zpgeek/history/joblist.json",{page:i.page||1})).jobList??[];if(o.length===0)throw new n("boss history","暂无浏览历史。");const r=i.limit||30;return o.slice(0,r).map((e)=>({job_name:e.jobName||"",company:e.brandName||"",salary:e.salaryDesc||"",area:[e.cityName,e.areaDistrict,e.businessDistrict].filter(Boolean).join("·"),boss:[e.bossName,e.bossTitle].filter(Boolean).join(" · "),security_id:e.securityId||"",url:e.encryptJobId?`https://www.zhipin.com/job_detail/${e.encryptJobId}.html`:""}))}});
@@ -0,0 +1 @@
1
+ import{cli as M,Strategy as N}from"@jackwener/opencli/registry";import{EmptyResultError as D}from"@jackwener/opencli/errors";import{requirePage as O,navigateToChat as Q,navigateToGeekJob as U,geekGet as V,bossFetch as W,isRecruiterSide as X,verbose as H}from"./utils.js";function Y(f){return{job_name:f.jobName||"",company:f.brandName||"",time:f.interviewTime||f.startTimeDesc||"",address:f.address||f.addressText||"",contact:[f.contactName,f.contactPhone].filter(Boolean).join(" "),status:f.statusDesc||""}}function Z(f){return{job_name:f.jobName||"",company:f.geekName||"",time:f.interviewTime||f.startTimeDesc||"",address:f.address||f.addressText||"",contact:[f.contactName,f.contactPhone].filter(Boolean).join(" "),status:f.statusDesc||""}}async function K(f){H("Fetching interview invitations (geek side)...");await U(f);const A=await V(f,"/wapi/zpinterview/geek/interview/data.json"),x=A.interviewList??A.list??[];if(x.length===0)throw new D("boss interviews","暂无面试邀请。");return x.map(Y)}async function L(f){H("Fetching interview list (recruiter side)...");await Q(f);const A=await W(f,"https://www.zhipin.com/wapi/zpinterview/boss/interview/valid/list"),x=A.zpData?.list??A.zpData?.interviewList??[];if(x.length===0)throw new D("boss interviews","暂无面试安排。");return x.map(Z)}M({site:"boss",name:"interviews",access:"read",description:"BOSS直聘查看面试(求职端=收到的邀请 / 招聘端=我发出的面试)",domain:"www.zhipin.com",strategy:N.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"side",default:"auto",choices:["auto","geek","boss"],help:"Identity side: auto (default), geek (job-seeker), boss (recruiter)"}],columns:["job_name","company","time","address","contact","status"],func:async(f,A)=>{O(f);const x=A.side||"auto";if(x==="geek")return K(f);if(x==="boss")return L(f);return await X(f)?L(f):K(f)}});
package/clis/boss/mark.js CHANGED
@@ -1 +1 @@
1
- import{cli as b,Strategy as f}from"@jackwener/opencli/registry";import{requirePage as h,navigateToChat as p,bossFetch as w,findFriendByUid as S,verbose as $}from"./utils.js";import{ArgumentError as g,EmptyResultError as v}from"@jackwener/opencli/errors";const a={"新招呼":1,"沟通中":2,"已约面":3,"已获取简历":4,"已交换电话":5,"已交换微信":6,"不合适":7,"牛人发起":8,"收藏":11};b({site:"boss",name:"mark",access:"write",description:"BOSS直聘给候选人添加标签",domain:"www.zhipin.com",strategy:f.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"uid",positional:!0,required:!0,help:"Encrypted UID of the candidate"},{name:"label",required:!0,help:"Label name (新招呼/沟通中/已约面/已获取简历/已交换电话/已交换微信/不合适/收藏) or label ID"},{name:"remove",type:"boolean",default:!1,help:"Remove the label instead of adding"}],columns:["status","detail"],func:async(r,i)=>{h(r);const t=i.label,n=i.remove||!1;let e;if(a[t])e=a[t];else if(!isNaN(Number(t)))e=Number(t);else{const l=Object.entries(a).find(([u])=>u.includes(t));if(l)e=l[1];else throw new g(`未知标签: ${t}。可用标签: ${Object.keys(a).join(", ")}`)}$(`${n?"Removing":"Adding"} label ${e} for ${i.uid}...`);await p(r);const o=await S(r,i.uid,{checkGreetList:!0});if(!o)throw new v("boss candidate search");const s=o.name||"候选人",d=n?"deleteMark":"addMark",c=new URLSearchParams({friendId:String(o.uid),friendSource:String(o.friendSource??0),labelId:String(e)});await w(r,`https://www.zhipin.com/wapi/zprelation/friend/label/${d}?${c.toString()}`);const m=Object.entries(a).find(([,l])=>l===e)?.[0]||String(e);return[{status:n?"✅ 标签已移除":"✅ 标签已添加",detail:`${s}: ${n?"移除":"添加"}标签「${m}」`}]}});
1
+ import{cli as q,Strategy as U}from"@jackwener/opencli/registry";import{ArgumentError as C,EmptyResultError as F}from"@jackwener/opencli/errors";import{requirePage as T,navigateToChat as h,navigateToGeekChat as j,bossFetch as N,findFriendByUid as G,resolveGeekBoss as R,isRecruiterSide as S,verbose as Y}from"./utils.js";const O={"新招呼":1,"沟通中":2,"已约面":3,"已获取简历":4,"已交换电话":5,"已交换微信":6,"不合适":7,"牛人发起":8,"收藏":11};function M(x){if(O[x])return O[x];if(x!==""&&!isNaN(Number(x)))return Number(x);const D=Object.entries(O).find(([z])=>z.includes(x));if(D)return D[1];throw new C(`未知标签: ${x}。可用标签: ${Object.keys(O).join(", ")}`)}async function Z(x,{friendId:D,friendSource:z,labelId:H,remove:Q}){const V=Q?"deleteMark":"addMark",W=new URLSearchParams({friendId:String(D),friendSource:String(z??0),labelId:String(H)});await N(x,`https://www.zhipin.com/wapi/zprelation/friend/label/${V}?${W.toString()}`)}q({site:"boss",name:"mark",access:"write",description:"BOSS直聘给会话打标签(求职端标记 Boss / 招聘端标记候选人)",domain:"www.zhipin.com",strategy:U.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"uid",positional:!0,required:!0,help:"对方 uid(从 boss chatlist 拿)"},{name:"label",required:!0,help:"Label name (新招呼/沟通中/已约面/已获取简历/已交换电话/已交换微信/不合适/收藏) or label ID"},{name:"remove",type:"boolean",default:!1,help:"Remove the label instead of adding"},{name:"side",default:"auto",choices:["auto","geek","boss"],help:"Identity side: auto (default), geek (job-seeker), boss (recruiter)"}],columns:["status","detail"],func:async(x,D)=>{T(x);const z=D.remove||!1,H=M(D.label),Q=Object.entries(O).find(([,J])=>J===H)?.[0]||String(H),V=D.side||"auto",W=async()=>{await j(x);const{id:J,friend:K}=await R(x,D.uid);Y(`${z?"Removing":"Adding"} label ${H} for boss ${J}...`);await Z(x,{friendId:J,friendSource:K?.friendSource??0,labelId:H,remove:z});return[{status:z?"✅ 标签已移除":"✅ 标签已添加",detail:`Boss ${J}: ${z?"移除":"添加"}标签「${Q}」`}]},X=async(J)=>{const K=J??await G(x,D.uid,{checkGreetList:!0});if(!K)throw new F("boss candidate search");const $=K.name||"候选人";Y(`${z?"Removing":"Adding"} label ${H} for ${D.uid}...`);await Z(x,{friendId:K.uid,friendSource:K.friendSource??0,labelId:H,remove:z});return[{status:z?"✅ 标签已移除":"✅ 标签已添加",detail:`${$}: ${z?"移除":"添加"}标签「${Q}」`}]};if(V==="geek")return W();if(V==="boss"){await h(x);return X(null)}return await S(x)?X(null):W()}});
@@ -0,0 +1,26 @@
1
+ import{cli as $,Strategy as q}from"@jackwener/opencli/registry";import{requirePage as A,navigateTo as D,geekGet as L,verbose as O}from"./utils.js";const M="https://www.zhipin.com/web/geek/resume";async function N(B){return await B.evaluate(`
2
+ (() => {
3
+ // 1. 页面自己发过的请求里带 id=<expectId>
4
+ try {
5
+ for (const entry of performance.getEntriesByType('resource')) {
6
+ if (!entry.name.includes('resume/expect/query')) continue;
7
+ const v = new URL(entry.name).searchParams.get('id');
8
+ if (v) return v;
9
+ }
10
+ } catch (_) {}
11
+ // 2. SSR / 内联 script 里的 expectId 字段
12
+ try {
13
+ for (const s of document.querySelectorAll('script:not([src])')) {
14
+ const t = s.textContent || '';
15
+ const m = t.match(/"(?:encryptExpectId|expectId)":"([^"]+)"/);
16
+ if (m) return m[1];
17
+ }
18
+ } catch (_) {}
19
+ // 3. DOM 上挂的 data-expect-id
20
+ try {
21
+ const el = document.querySelector('[data-expect-id]');
22
+ if (el) return el.getAttribute('data-expect-id');
23
+ } catch (_) {}
24
+ return '';
25
+ })()
26
+ `)||""}$({site:"boss",name:"me",access:"read",description:"BOSS直聘查看我的资料与求职期望(求职端)",domain:"www.zhipin.com",strategy:q.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"expect-id",default:"",help:"encryptExpectId(不传则从简历页自动抠)"}],columns:["name","gender","age","degree","work_years","start_work","hometown","expect_position","expect_city","expect_salary","expect_industry","done_positions","interest_positions","interest_cities"],func:async(B,J)=>{A(B);O("Fetching profile...");await D(B,M,2);const f=await L(B,"/wapi/zpgeek/resume/baseinfo/query.json");let h={};const K=J["expect-id"]||await N(B);if(K){const C=await L(B,"/wapi/zpgeek/resume/expect/query.json",{id:K},{allowNonZero:!0});h=C&&C.code===void 0?C.expect??C:{}}else O("expectId not found — skipping job expectation section");const H=(C)=>(C||[]).map((F)=>typeof F==="string"?F:F?.name||F?.positionName||F?.locationName||F?.cityName).filter(Boolean).join(" / "),Q=f.gender===1?"男":f.gender===2?"女":"",V=f.workYearDesc||(typeof f.workYears==="number"&&f.workYears>0?`${f.workYears}年`:""),W=[h.locationName,h.subLocationName].filter(Boolean).join(" "),X=h.industryDesc&&h.industryDesc!=="行业不限"?h.industryDesc:"";return[{name:f.name||f.nickName||f.geekName||"",gender:Q,age:f.ageDesc||(f.age?`${f.age}岁`:""),degree:f.degreeCategory||"",work_years:V,start_work:f.startWorkDateDesc||"",hometown:f.hometownName||"",expect_position:h.positionName||"",expect_city:W,expect_salary:h.salaryDescNew||h.salaryDesc||"",expect_industry:X,done_positions:H(h.doneWorkPositionList),interest_positions:H(h.interestPositionList),interest_cities:H(h.interestLocationList)}]}});
@@ -1 +1 @@
1
- import{cli as K,Strategy as N}from"@jackwener/opencli/registry";import{requirePage as O,navigateToChat as Q,bossFetch as U,fetchRecommendList as V,verbose as W}from"./utils.js";K({site:"boss",name:"recommend",access:"read",description:"BOSS直聘查看推荐候选人(新招呼列表)",domain:"www.zhipin.com",strategy:N.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"limit",type:"int",default:20,help:"Number of results to return"}],columns:["name","job_name","last_time","labels","encrypt_uid","security_id","encrypt_job_id"],func:async(z,H)=>{O(z);W("Fetching recommended candidates...");await Q(z);const A=await U(z,"https://www.zhipin.com/wapi/zprelation/friend/label/get",{allowNonZero:!0}),B={};if(A.code===0&&A.zpData?.labels)for(const x of A.zpData.labels)B[x.labelId]=x.label;const I=await V(z),J=H.limit||20;return I.slice(0,J).map((x)=>({name:x.name||"",job_name:x.jobName||"",last_time:x.lastTime||"",labels:(x.relationLabelList||[]).map((E)=>B[E]||String(E)).join(", "),encrypt_uid:x.encryptUid||"",security_id:x.securityId||"",encrypt_job_id:x.encryptJobId||""}))}});
1
+ import{cli as K,Strategy as P}from"@jackwener/opencli/registry";import{EmptyResultError as z}from"@jackwener/opencli/errors";import{requirePage as I,navigateToChat as N,navigateToGeekJob as T,bossFetch as U,fetchRecommendList as _,geekGet as h,isRecruiterSide as D,verbose as q}from"./utils.js";const G=["name","salary","company","area","experience","degree","boss","security_id","url"],L=["name","job_name","last_time","labels","encrypt_uid","security_id","encrypt_job_id"];async function B(A,H){q("Fetching recommended jobs (geek side)...");await T(A);const V=await h(A,"/wapi/zprelation/interaction/geekGetJob",{page:H.page||1,tag:5,isActive:"true"}),W=V.jobList??V.cardList??[];if(!Array.isArray(W)||W.length===0)throw new z("boss recommend","推荐职位为空。请先在 BOSS 直聘 App 或网页完善求职期望(岗位 + 城市),推荐流才会有数据。");const Y=H.limit||20;return W.slice(0,Y).map((Q)=>{const x=Q.jobInfo??Q,X=Q.brandInfo??Q,Z=Q.bossInfo??Q,$=x.encryptJobId||Q.encryptJobId||"";return{name:x.jobName||"",salary:x.salaryDesc||"",company:X.brandName||"",area:[x.cityName,x.areaDistrict,x.businessDistrict].filter(Boolean).join("·"),experience:x.jobExperience||x.experienceName||"",degree:x.jobDegree||x.degreeName||"",boss:[Z.bossName,Z.bossTitle].filter(Boolean).join(" · "),security_id:x.securityId||Q.securityId||"",url:$?`https://www.zhipin.com/job_detail/${$}.html`:""}})}async function F(A,H){q("Fetching recommended candidates (recruiter side)...");await N(A);const V=await U(A,"https://www.zhipin.com/wapi/zprelation/friend/label/get",{allowNonZero:!0}),W={};if(V.code===0&&V.zpData?.labels)for(const x of V.zpData.labels)W[x.labelId]=x.label;const Y=await _(A),Q=H.limit||20;return Y.slice(0,Q).map((x)=>({name:x.name||"",job_name:x.jobName||"",last_time:x.lastTime||"",labels:(x.relationLabelList||[]).map((X)=>W[X]||String(X)).join(", "),encrypt_uid:x.encryptUid||"",security_id:x.securityId||"",encrypt_job_id:x.encryptJobId||""}))}K({site:"boss",name:"recommend",access:"read",description:"BOSS直聘查看推荐(求职端=推荐职位 / 招聘端=推荐候选人)",domain:"www.zhipin.com",strategy:P.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"side",default:"auto",choices:["auto","geek","boss"],help:"Identity side: auto (default), geek (job-seeker), boss (recruiter)"},{name:"limit",type:"int",default:20,help:"Number of results to return"},{name:"page",type:"int",default:1,help:"[geek] Page number"}],columns:[...new Set([...G,...L])],func:async(A,H)=>{I(A);const V=H.side||"auto";if(V==="geek")return B(A,H);if(V==="boss")return F(A,H);return await D(A)?F(A,H):B(A,H)}});
@@ -0,0 +1 @@
1
+ import{cli as o,Strategy as n}from"@jackwener/opencli/registry";import{ArgumentError as u}from"@jackwener/opencli/errors";import{requirePage as c,navigateTo as d,geekPost as w,fetchGeekAttachmentResumes as h,verbose as l}from"./utils.js";o({site:"boss",name:"resumedelete",access:"write",description:"BOSS直聘删除账号里的一份附件简历(求职端)",domain:"www.zhipin.com",strategy:n.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"resume-id",positional:!0,required:!0,help:"encryptResumeId(从 boss resumelist 拿)"}],columns:["status","detail"],func:async(t,a)=>{c(t);const e=String(a["resume-id"]);await d(t,"https://www.zhipin.com/web/geek/resume",2);const r=await h(t),m=r.find((s)=>String(s.resumeId)===e);if(!m){const s=r.map((i)=>`${i.resumeId}(${i.showName||"未命名"})`).join(", ")||"(空)";throw new u(`resume-id=${e} 不在账号简历列表 [${s}] 中,没有执行删除`)}l(`Deleting attachment resume ${e}...`);await w(t,"/wapi/zpgeek/resume/attachment/delete.json",{resumeId:e});return[{status:"✅ 简历已删除",detail:`已删除附件简历「${m.showName||e}」`}]}});
@@ -0,0 +1 @@
1
+ import{cli as i,Strategy as m}from"@jackwener/opencli/registry";import{EmptyResultError as r}from"@jackwener/opencli/errors";import{requirePage as a,navigateTo as o,fetchGeekAttachmentResumes as u,verbose as n}from"./utils.js";i({site:"boss",name:"resumelist",access:"read",description:"BOSS直聘列出账号绑定的附件简历(求职端)",domain:"www.zhipin.com",strategy:m.COOKIE,navigateBefore:!1,browser:!0,args:[],columns:["resume_id","name","suffix","size","upload_time"],func:async(s)=>{a(s);n("Fetching attachment resumes...");await o(s,"https://www.zhipin.com/web/geek/resume",2);const t=await u(s);if(t.length===0)throw new r("boss resumelist","账号还没有绑定附件简历,请先在 BOSS 直聘 App 或网页简历页上传。");return t.map((e)=>({resume_id:e.resumeId||"",name:e.showName||"",suffix:e.suffixName||"",size:e.resumeSizeDesc||"",upload_time:e.uploadTime?new Date(e.uploadTime).toLocaleString("zh-CN"):""}))}});
@@ -0,0 +1,53 @@
1
+ import*as C from"node:fs";import*as $ from"node:path";import{cli as D,Strategy as X}from"@jackwener/opencli/registry";import{ArgumentError as O,CommandExecutionError as Q}from"@jackwener/opencli/errors";import{setInputFiles as Y}from"../_shared/file-inject.js";import{requirePage as h,navigateTo as N,geekPost as S,verbose as y}from"./utils.js";const A="https://www.zhipin.com/web/geek/resume",V="pp-boss-resume-upload",j=10485760,q={pdf:"application/pdf",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg"};function F(J){const H=$.resolve(String(J).replace(/^~(?=$|\/)/,process.env.HOME||"~"));let z;try{z=C.statSync(H)}catch{throw new O(`文件不存在: ${H}`)}if(!z.isFile())throw new O(`不是一个文件: ${H}`);const K=$.extname(H).toLowerCase().replace(/^\./,"");if(!q[K])throw new O(`不支持的文件类型: .${K}(支持 ${Object.keys(q).sort().join("/")})`);if(z.size<=0)throw new O(`文件为空: ${H}`);if(z.size>j)throw new O(`文件过大: ${(z.size/1024/1024).toFixed(1)}MB(上限 ${j/1024/1024}MB)`);return{abs:H,name:$.basename(H),suffix:K,mime:q[K],size:z.size}}D({site:"boss",name:"resumeupload",access:"write",description:"BOSS直聘上传附件简历到账号库(求职端)",domain:"www.zhipin.com",strategy:X.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"file",positional:!0,required:!0,help:"本地简历文件路径(pdf/doc/docx/ppt/pptx/png/jpg,≤10MB)"},{name:"file-type",type:"int",default:1,help:"1=普通简历(默认),2=作品集"},{name:"annex-type",type:"int",default:1,help:"save 时的 annexType,1=普通简历(默认)"}],columns:["status","resume_id","name","detail"],func:async(J,H)=>{h(J);const z=F(H.file);y(`Uploading resume ${z.name} (${(z.size/1024).toFixed(0)}KB)...`);await N(J,A,2);await J.evaluate(`
2
+ (() => {
3
+ let el = document.getElementById(${JSON.stringify(V)});
4
+ if (!el) {
5
+ el = document.createElement('input');
6
+ el.type = 'file';
7
+ el.id = ${JSON.stringify(V)};
8
+ el.style.position = 'fixed';
9
+ el.style.left = '-9999px';
10
+ document.body.appendChild(el);
11
+ }
12
+ return true;
13
+ })()
14
+ `);try{await Y(J,`#${V}`,[z.abs])}catch(Z){throw new Q(`简历文件注入页面失败: ${Z instanceof Error?Z.message:String(Z)}`)}const K=await J.evaluate(`
15
+ (async () => {
16
+ const input = document.getElementById(${JSON.stringify(V)});
17
+ if (!input || !input.files || input.files.length === 0) {
18
+ return { ok: false, error: '文件未进入页面 input' };
19
+ }
20
+ const src = input.files[0];
21
+ const buf = await src.arrayBuffer();
22
+ // 按真实后缀重建 File,保证 MIME 正确(注入通道构造的 File 可能是 octet-stream)
23
+ const file = new File([buf], ${JSON.stringify(z.name)}, { type: ${JSON.stringify(z.mime)} });
24
+ const fd = new FormData();
25
+ fd.append('file', file, ${JSON.stringify(z.name)});
26
+ fd.append('fileType', ${JSON.stringify(String(H["file-type"]??1))});
27
+ const headers = { 'X-Requested-With': 'XMLHttpRequest' };
28
+ const bst = (document.cookie.match(/(?:^|;\\s*)bst=([^;]+)/) || [])[1];
29
+ if (bst) headers['zp_token'] = decodeURIComponent(bst);
30
+ try {
31
+ // 不设 Content-Type:让浏览器自己补 multipart boundary
32
+ const resp = await fetch('https://www.zhipin.com/wapi/zpupload/resume/uploadFile.json', {
33
+ method: 'POST',
34
+ credentials: 'include',
35
+ headers,
36
+ body: fd,
37
+ });
38
+ const text = await resp.text();
39
+ if (text.trim().startsWith('<')) {
40
+ return { ok: false, error: '收到 HTML 响应,登录态可能已失效' };
41
+ }
42
+ return { ok: true, status: resp.status, body: JSON.parse(text) };
43
+ } catch (e) {
44
+ return { ok: false, error: String(e && e.message || e) };
45
+ }
46
+ })()
47
+ `);await J.evaluate(`
48
+ (() => {
49
+ const el = document.getElementById(${JSON.stringify(V)});
50
+ if (el && el.parentNode) el.parentNode.removeChild(el);
51
+ return true;
52
+ })()
53
+ `);if(!K?.ok)throw new Q(`上传简历失败: ${K?.error||"未知错误"}`);const L=K.body||{};if(L.code!==0)throw new Q(`上传简历失败: ${L.message||"未知错误"} (code=${L.code})`);const G=L.zpData?.previewUrl,B=L.zpData?.attachmentName||z.name;if(!G)throw new Q(`上传响应缺少 previewUrl: ${JSON.stringify(L.zpData||{}).slice(0,200)}`);y("Saving attachment resume...");const R=await S(J,"/wapi/zpgeek/resume/attachment/save.json",{previewUrl:G,annexType:H["annex-type"]??1}),W=R?.resumeId;if(!W)throw new Q(`保存简历失败,响应缺少 resumeId: ${JSON.stringify(R||{}).slice(0,200)}`);return[{status:"✅ 简历已上传",resume_id:String(W),name:B,detail:`可用 boss sendresume <uid> --resume-id ${W} 发给 Boss`}]}});
@@ -1 +1 @@
1
- import{cli as O,Strategy as J}from"@jackwener/opencli/registry";import{ArgumentError as M}from"@jackwener/opencli/errors";import{requirePage as T,navigateTo as m,bossFetch as E,verbose as Z}from"./utils.js";const f={"全国":"100010000","北京":"101010100","上海":"101020100","广州":"101280100","深圳":"101280600","杭州":"101210100","成都":"101270100","南京":"101190100","武汉":"101200100","西安":"101110100","苏州":"101190400","长沙":"101250100","天津":"101030100","重庆":"101040100","郑州":"101180100","东莞":"101281600","青岛":"101120200","合肥":"101220100","佛山":"101280800","宁波":"101210400","厦门":"101230200","大连":"101070200","珠海":"101280700","无锡":"101190200","济南":"101120100","福州":"101230100","昆明":"101290100","哈尔滨":"101050100","沈阳":"101070100","石家庄":"101090100","贵阳":"101260100","南宁":"101300100","太原":"101100100","海口":"101310100","兰州":"101160100","乌鲁木齐":"101130100","长春":"101060100","南昌":"101240100","常州":"101191100","温州":"101210700","嘉兴":"101210300","徐州":"101190800","香港":"101320100"},Y={"不限":"0","在校/应届":"108","在校生":"108","在校":"108","应届生":"102","应届":"102","经验不限":"101","1年以内":"103","1-3年":"104","3-5年":"105","5-10年":"106","10年以上":"107"},d={"不限":"0","初中及以下":"209","中专/中技":"208","高中":"206","大专":"202","本科":"203","硕士":"204","博士":"205"},_={"不限":"0","3K以下":"401","3-5K":"402","5-10K":"403","10-15K":"404","15-20K":"405","20-30K":"406","30-50K":"407","50K以上":"408"},b={"不限":"0","互联网":"100020","电子商务":"100021","游戏":"100024","人工智能":"100901","大数据":"100902","金融":"100101","教育培训":"100200","医疗健康":"100300"},x={"不限":"0","全职":"1901","实习":"1902","兼职":"1903"},v=new Set(Object.values(x));function c(H){if(!H)return"101010100";if(/^\d+$/.test(H))return H;if(f[H])return f[H];for(const[Q,F]of Object.entries(f))if(Q.includes(H))return F;return"101010100"}function G(H,Q){if(!H)return"";if(Q[H]!==void 0)return Q[H];for(const[F,L]of Object.entries(Q))if(F.includes(H))return L;return H}function V(H){if(!H)return"";if(x[H]!==void 0)return x[H];if(v.has(H))return H;throw new M(`Invalid jobType: ${H}`,"Use one of: 全职, 兼职, 实习, 不限")}function A(H){if(H===!0)return"Y";if(H===!1)return"N";return""}O({site:"boss",name:"search",access:"read",description:"BOSS直聘搜索职位(不带关键词时返回为你推荐职位)",domain:"www.zhipin.com",strategy:J.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"query",positional:!0,help:"Search keyword (optional, empty = recommended jobs)"},{name:"city",default:"北京",help:"City name or code (e.g. 杭州, 上海, 101010100)"},{name:"experience",default:"",help:"Experience: 在校生(实习)/应届生(校招)/经验不限/1年以内/1-3年/3-5年/5-10年/10年以上"},{name:"degree",default:"",help:"Degree: 大专/本科/硕士/博士"},{name:"salary",default:"",help:"Salary: 3K以下/3-5K/5-10K/10-15K/15-20K/20-30K/30-50K/50K以上"},{name:"industry",default:"",help:"Industry code or name (e.g. 100020, 互联网)"},{name:"jobType",default:"",help:"Job type: 全职/兼职/实习(不传=不限,混合校招与实习)"},{name:"page",type:"int",default:1,help:"Page number"},{name:"limit",type:"int",default:15,help:"Number of results"}],columns:["name","salary","company","area","experience","degree","skills","boss","bossOnline","security_id","url"],func:async(H,Q)=>{T(H);const F=String(Q.query??"").trim(),L=c(Q.city);Z("Navigating to set referrer context...");await m(H,`https://www.zhipin.com/web/geek/job?query=${encodeURIComponent(F)}&city=${L}`);await new Promise((W)=>setTimeout(W,1000));const z=G(Q.experience,Y),U=G(Q.degree,d),X=G(Q.salary,_),R=G(Q.industry,b),h=V(Q.jobType),D=Q.limit||15;let N=Q.page||1,$=[];const I=new Set;while($.length<D){if($.length>0)await new Promise((K)=>setTimeout(K,1000+Math.random()*2000));const W=new URLSearchParams({scene:"1",query:F,city:L,page:String(N),pageSize:"15"});if(z)W.set("experience",z);if(U)W.set("degree",U);if(X)W.set("salary",X);if(R)W.set("industry",R);if(h)W.set("jobType",h);const C=`https://www.zhipin.com/wapi/zpgeek/search/joblist.json?${W.toString()}`;Z(`Fetching page ${N}... (current jobs: ${$.length})`);const q=(await E(H,C)).zpData||{},B=q.jobList||[];if(B.length===0)break;let S=0;for(const K of B){if(!K.encryptJobId||I.has(K.encryptJobId))continue;I.add(K.encryptJobId);$.push({name:K.jobName,salary:K.salaryDesc,company:K.brandName,area:[K.cityName,K.areaDistrict,K.businessDistrict].filter(Boolean).join("·"),experience:K.jobExperience,degree:K.jobDegree,skills:(K.skills||[]).join(","),boss:K.bossName+" · "+K.bossTitle,bossOnline:A(K.bossOnline),security_id:K.securityId||"",url:"https://www.zhipin.com/job_detail/"+K.encryptJobId+".html"});S++;if($.length>=D)break}if(S===0){Z(`API returned duplicate page, stopping pagination at ${$.length} items`);break}if(!q.hasMore)break;N++}return $}});export const __test__={EXP_MAP:Y,resolveMap:G,resolveJobType:V,formatBossOnline:A};
1
+ import{cli as O,Strategy as T}from"@jackwener/opencli/registry";import{ArgumentError as E}from"@jackwener/opencli/errors";import{requirePage as D,navigateTo as x,bossFetch as A,verbose as l,CITY_CODES as d}from"./utils.js";const K={"不限":"0","在校/应届":"108","在校生":"108","在校":"108","应届生":"102","应届":"102","经验不限":"101","1年以内":"103","1-3年":"104","3-5年":"105","5-10年":"106","10年以上":"107"},v={"不限":"0","初中及以下":"209","中专/中技":"208","高中":"206","大专":"202","本科":"203","硕士":"204","博士":"205"},J={"不限":"0","3K以下":"401","3-5K":"402","5-10K":"403","10-15K":"404","15-20K":"405","20-30K":"406","30-50K":"407","50K以上":"408"},z={"不限":"0","互联网":"100020","电子商务":"100021","游戏":"100024","人工智能":"100901","大数据":"100902","金融":"100101","教育培训":"100200","医疗健康":"100300"},f={"不限":"0","全职":"1901","实习":"1902","兼职":"1903"},B=new Set(Object.values(f));function C(e){if(!e)return"101010100";if(/^\d+$/.test(e))return e;if(d[e])return d[e];for(const[r,s]of Object.entries(d))if(r.includes(e))return s;return"101010100"}function a(e,r){if(!e)return"";if(r[e]!==void 0)return r[e];for(const[s,i]of Object.entries(r))if(s.includes(e))return i;return e}function P(e){if(!e)return"";if(f[e]!==void 0)return f[e];if(B.has(e))return e;throw new E(`Invalid jobType: ${e}`,"Use one of: 全职, 兼职, 实习, 不限")}function _(e){if(e===!0)return"Y";if(e===!1)return"N";return""}O({site:"boss",name:"search",access:"read",description:"BOSS直聘搜索职位(不带关键词时返回为你推荐职位)",domain:"www.zhipin.com",strategy:T.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"query",positional:!0,help:"Search keyword (optional, empty = recommended jobs)"},{name:"city",default:"北京",help:"City name or code (e.g. 杭州, 上海, 101010100)"},{name:"experience",default:"",help:"Experience: 在校生(实习)/应届生(校招)/经验不限/1年以内/1-3年/3-5年/5-10年/10年以上"},{name:"degree",default:"",help:"Degree: 大专/本科/硕士/博士"},{name:"salary",default:"",help:"Salary: 3K以下/3-5K/5-10K/10-15K/15-20K/20-30K/30-50K/50K以上"},{name:"industry",default:"",help:"Industry code or name (e.g. 100020, 互联网)"},{name:"jobType",default:"",help:"Job type: 全职/兼职/实习(不传=不限,混合校招与实习)"},{name:"page",type:"int",default:1,help:"Page number"},{name:"limit",type:"int",default:15,help:"Number of results"}],columns:["name","salary","company","area","experience","degree","skills","boss","bossOnline","security_id","url"],func:async(e,r)=>{D(e);const s=String(r.query??"").trim(),i=C(r.city);l("Navigating to set referrer context...");await x(e,`https://www.zhipin.com/web/geek/job?query=${encodeURIComponent(s)}&city=${i}`);await new Promise((n)=>setTimeout(n,1000));const m=a(r.experience,K),u=a(r.degree,v),y=a(r.salary,J),b=a(r.industry,z),p=P(r.jobType),h=r.limit||15;let c=r.page||1,o=[];const g=new Set;while(o.length<h){if(o.length>0)await new Promise((t)=>setTimeout(t,1000+Math.random()*2000));const n=new URLSearchParams({scene:"1",query:s,city:i,page:String(c),pageSize:"15"});if(m)n.set("experience",m);if(u)n.set("degree",u);if(y)n.set("salary",y);if(b)n.set("industry",b);if(p)n.set("jobType",p);const I=`https://www.zhipin.com/wapi/zpgeek/search/joblist.json?${n.toString()}`;l(`Fetching page ${c}... (current jobs: ${o.length})`);const j=(await A(e,I)).zpData||{},w=j.jobList||[];if(w.length===0)break;let S=0;for(const t of w){if(!t.encryptJobId||g.has(t.encryptJobId))continue;g.add(t.encryptJobId);o.push({name:t.jobName,salary:t.salaryDesc,company:t.brandName,area:[t.cityName,t.areaDistrict,t.businessDistrict].filter(Boolean).join("·"),experience:t.jobExperience,degree:t.jobDegree,skills:(t.skills||[]).join(","),boss:t.bossName+" · "+t.bossTitle,bossOnline:_(t.bossOnline),security_id:t.securityId||"",url:"https://www.zhipin.com/job_detail/"+t.encryptJobId+".html"});S++;if(o.length>=h)break}if(S===0){l(`API returned duplicate page, stopping pagination at ${o.length} items`);break}if(!j.hasMore)break;c++}return o}});export const __test__={EXP_MAP:K,resolveMap:a,resolveJobType:P,formatBossOnline:_};
package/clis/boss/send.js CHANGED
@@ -1 +1 @@
1
- import{cli as n,Strategy as o}from"@jackwener/opencli/registry";import{requirePage as d,navigateToChat as c,findFriendByUid as m,clickCandidateInList as u,typeAndSendMessage as l}from"./utils.js";import{EmptyResultError as h,selectorError as a}from"@jackwener/opencli/errors";n({site:"boss",name:"send",access:"write",description:"BOSS直聘发送聊天消息",domain:"www.zhipin.com",strategy:o.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"uid",positional:!0,required:!0,help:"Encrypted UID of the candidate (from chatlist)"},{name:"text",fromFile:!0,required:!0,positional:!0,help:"Message text to send"}],columns:["status","detail"],func:async(t,e)=>{d(t);await c(t,3);const i=await m(t,e.uid,{maxPages:5});if(!i)throw new h("boss candidate search","请确认 uid 是否正确");const r=i.uid,s=i.name||"候选人";if(!await u(t,r))throw a("聊天列表中的用户","请确认聊天列表中有此人");await t.wait({time:2});if(!await l(t,e.text))throw a("消息输入框","聊天页面 UI 可能已改变");await t.wait({time:1});return[{status:"✅ 发送成功",detail:`已向 ${s} 发送: ${e.text}`}]}});
1
+ import{cli as X,Strategy as Y}from"@jackwener/opencli/registry";import{EmptyResultError as Z,selectorError as H}from"@jackwener/opencli/errors";import{requirePage as _,navigateToChat as $,findFriendByUid as q,clickCandidateInList as F,typeAndSendMessage as K,geekOpenChat as L,resolveGeekBossId as N,navigateToGeekChat as O,isRecruiterSide as P,verbose as B}from"./utils.js";async function Q(x,z){await O(x);const D=await N(x,z.uid);B(`Opening geek chat with boss ${D}...`);await L(x,D);if(!await K(x,z.text))throw H("消息输入框","聊天页面 UI 可能已改变,或该会话不存在");await x.wait({time:1});return[{status:"✅ 发送成功",detail:`已向 Boss ${D} 发送: ${z.text}`}]}async function V(x,z){await $(x,3);const D=await q(x,z.uid,{maxPages:5});if(!D)throw new Z("boss candidate search","请确认 uid 是否正确");const J=D.uid,W=D.name||"候选人";if(!await F(x,J))throw H("聊天列表中的用户","请确认聊天列表中有此人");await x.wait({time:2});if(!await K(x,z.text))throw H("消息输入框","聊天页面 UI 可能已改变");await x.wait({time:1});return[{status:"✅ 发送成功",detail:`已向 ${W} 发送: ${z.text}`}]}X({site:"boss",name:"send",access:"write",description:"BOSS直聘发送聊天消息(求职端回复 Boss / 招聘端回复候选人)",domain:"www.zhipin.com",strategy:Y.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"uid",positional:!0,required:!0,help:"对方 uid(从 boss chatlist 拿)"},{name:"text",fromFile:!0,required:!0,positional:!0,help:"Message text to send"},{name:"side",default:"auto",choices:["auto","geek","boss"],help:"Identity side: auto (default), geek (job-seeker), boss (recruiter)"}],columns:["status","detail"],func:async(x,z)=>{_(x);const D=z.side||"auto";if(D==="geek")return Q(x,z);if(D==="boss")return V(x,z);return await P(x)?V(x,z):Q(x,z)}});
@@ -0,0 +1 @@
1
+ import{cli as a,Strategy as d}from"@jackwener/opencli/registry";import{CommandExecutionError as u}from"@jackwener/opencli/errors";import{requirePage as n,navigateToGeekChat as m,geekPost as c,fetchGeekBossData as y,resolveGeekBossId as p,resolveGeekResumeId as I,verbose as l}from"./utils.js";a({site:"boss",name:"sendresume",access:"write",description:"BOSS直聘向 Boss 发送附件简历(求职端)",domain:"www.zhipin.com",strategy:d.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"uid",positional:!0,required:!0,help:"Boss 的 uid(数字 friendId 或 encryptFriendId,从 boss chatlist --side geek 拿)"},{name:"resume-id",default:"",help:"encryptResumeId(从 boss resumelist 拿)。账号只有一份时可不传"}],columns:["status","detail"],func:async(e,i)=>{n(e);await m(e);const s=await p(e,i.uid),t=await y(e,s),o=t.securityId||"";if(!o)throw new u(`无法获取 securityId (uid=${s}),该会话可能已失效`);const r=await I(e,i["resume-id"]);l(`Sending resume ${r} to boss ${s}...`);await c(e,"/wapi/zpchat/exchange/request",{type:3,bossId:s,securityId:o,mid:"",encryptResumeId:r});return[{status:"✅ 简历已发送",detail:`已向 ${t.name||t.bossName||s} 发送附件简历 ${r}`}]}});
@@ -0,0 +1 @@
1
+ import{cli as $,Strategy as q}from"@jackwener/opencli/registry";import{EmptyResultError as W}from"@jackwener/opencli/errors";import{requirePage as F,navigateToGeekChat as A,readEncryptSystemId as B,fetchGeekFriendLabelList as H,fetchGeekLastMsgs as R,fetchGeekUserInfo as j,readPositiveInteger as v,verbose as C}from"./utils.js";const X=20;$({site:"boss",name:"unread",access:"read",description:"BOSS直聘查看未回复的消息(求职端,Boss 发来但我还没回)",domain:"www.zhipin.com",strategy:q.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"limit",type:"int",default:50,help:"How many recent conversations to scan"}],columns:["name","company","job","last_msg","last_time","uid"],func:async(J,Y)=>{F(J);const Z=v(Y.limit,"unread --limit",50,200);C("Scanning for unreplied conversations...");await A(J);const _=await B(J),K=(await H(J,{encryptSystemId:_})).slice(0,Z);if(K.length===0)throw new W("boss unread","暂无沟通记录。");const O=(await j(J)).userId,Q=[];for(let x=0;x<K.length;x+=X){const z=K.slice(x,x+X).map((D)=>D.friendId).filter(Boolean);Q.push(...await R(J,z))}const V=new Map;for(const x of Q){const z=x.lastMsgInfo||{},D=z.fromId===O?z.toId:z.fromId;if(D)V.set(String(D),x)}const N=[];for(const x of K){const z=V.get(String(x.friendId)),D=z?.lastMsgInfo||{};if(!D.fromId||String(D.fromId)===String(O))continue;N.push({name:x.name||"",company:x.brandName||"",job:x.jobName||"",last_msg:D.showText||"",last_time:z.lastTime||"",uid:String(x.friendId)})}if(N.length===0)throw new W("boss unread","没有未回复的消息。");return N}});
@@ -1,12 +1,12 @@
1
- import{ArgumentError as H,AuthRequiredError as G,CommandExecutionError as Z,EmptyResultError as v}from"@jackwener/opencli/errors";const q="www.zhipin.com",k=`https://${q}/web/chat/index`,F=new Set([7,37]),L="Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。",B="该命令仅支持招聘端(BOSS 端)账号,请使用招聘者账号登录后重试。",P=15000;export function requirePage(z){if(!z)throw new Z("Browser page required")}export function readPositiveInteger(z,J,Q,W){const $=z===void 0||z===null||z===""?Q:Number(z);if(!Number.isInteger($)||$<1)throw new H(`boss ${J} must be a positive integer`);if(W!==void 0&&$>W)throw new H(`boss ${J} must be <= ${W}`);return $}export function readRequiredString(z,J){const Q=String(z??"").trim();if(!Q)throw new H(`boss ${J} cannot be empty`);return Q}export async function navigateToChat(z,J=2){await z.goto(k);await z.wait({time:J})}export async function navigateTo(z,J,Q=1){await z.goto(J);await z.wait({time:Q})}export function checkAuth(z){if(F.has(z.code))throw new G(q,L)}function T(z){if(z.code===IDENTITY_MISMATCH_CODE)throw new G(q,B)}export function assertOk(z,J){if(!z||typeof z!=="object")throw new Z(`${J?`${J}: `:""}Boss API returned malformed response`);if(z.code===0)return;checkAuth(z);T(z);const Q=J?`${J}: `:"";throw new Z(`${Q}${z.message||"Unknown error"} (code=${z.code})`)}export async function bossFetch(z,J,Q={}){const W=Q.method??"GET",$=Q.timeout??P,j=Q.body??null,V=`
1
+ import{ArgumentError as P,AuthRequiredError as L,CommandExecutionError as z,EmptyResultError as F}from"@jackwener/opencli/errors";const K="www.zhipin.com",v=`https://${K}/web/chat/index`,G=new Set([7,37]),T="Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。",B="该命令仅支持招聘端(BOSS 端)账号,请使用招聘者账号登录后重试。",N=15000;export function requirePage(Q){if(!Q)throw new z("Browser page required")}export function readPositiveInteger(Q,V,W,J){const $=Q===void 0||Q===null||Q===""?W:Number(Q);if(!Number.isInteger($)||$<1)throw new P(`boss ${V} must be a positive integer`);if(J!==void 0&&$>J)throw new P(`boss ${V} must be <= ${J}`);return $}export function readRequiredString(Q,V){const W=String(Q??"").trim();if(!W)throw new P(`boss ${V} cannot be empty`);return W}export async function navigateToChat(Q,V=2){await Q.goto(v);await Q.wait({time:V})}export async function navigateTo(Q,V,W=1){await Q.goto(V);await Q.wait({time:W})}export function checkAuth(Q){if(G.has(Q.code))throw new L(K,T)}function k(Q){if(Q.code===IDENTITY_MISMATCH_CODE)throw new L(K,B)}export function assertOk(Q,V){if(!Q||typeof Q!=="object")throw new z(`${V?`${V}: `:""}Boss API returned malformed response`);if(Q.code===0)return;checkAuth(Q);k(Q);const W=V?`${V}: `:"";throw new z(`${W}${Q.message||"Unknown error"} (code=${Q.code})`)}export async function bossFetch(Q,V,W={}){const J=W.method??"GET",$=W.timeout??N,j=W.body??null,X=`
2
2
  async () => {
3
3
  return new Promise((resolve, reject) => {
4
4
  const xhr = new XMLHttpRequest();
5
- xhr.open(${JSON.stringify(W)}, ${JSON.stringify(J)}, true);
5
+ xhr.open(${JSON.stringify(J)}, ${JSON.stringify(V)}, true);
6
6
  xhr.withCredentials = true;
7
7
  xhr.timeout = ${$};
8
8
  xhr.setRequestHeader('Accept', 'application/json');
9
- ${W==="POST"?"xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');":""}
9
+ ${J==="POST"?"xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');":""}
10
10
  xhr.onload = () => {
11
11
  try { resolve(JSON.parse(xhr.responseText)); }
12
12
  catch(e) { reject(new Error('JSON parse failed: ' + xhr.responseText.substring(0, 200))); }
@@ -16,9 +16,9 @@ import{ArgumentError as H,AuthRequiredError as G,CommandExecutionError as Z,Empt
16
16
  xhr.send(${j?JSON.stringify(j):"null"});
17
17
  });
18
18
  }
19
- `;let Y;try{Y=await z.evaluate(V)}catch(X){if(X instanceof G||X instanceof Z)throw X;const K=X instanceof Error?X.message:String(X);throw new Z(`Boss API request failed: ${K}`)}if(!Y||typeof Y!=="object")throw new Z("Boss API returned malformed response");if(!Q.allowNonZero&&Y.code!==0)assertOk(Y);return Y}export async function fetchFriendList(z,J={}){const Q=J.pageNum??1,W=J.jobId??"0",$=`https://${q}/wapi/zprelation/friend/getBossFriendListV2.json?page=${Q}&status=0&jobId=${W}`,j=await bossFetch(z,$,{allowNonZero:J.allowNonZero});if(J.allowNonZero&&j.code!==0)return j;const V=j.zpData?.friendList;if(!Array.isArray(V))throw new Z("Boss friend list response did not include zpData.friendList");return V}export async function fetchRecommendList(z){const J=`https://${q}/wapi/zprelation/friend/greetRecSortList`,W=(await bossFetch(z,J)).zpData?.friendList;if(!Array.isArray(W))throw new Z("Boss recommend response did not include zpData.friendList");return W}export async function findFriendByUid(z,J,Q={}){const W=Q.maxPages??1,$=Q.checkGreetList??!1;for(let j=1;j<=W;j++){const V=await fetchFriendList(z,{pageNum:j,allowNonZero:Q.allowNonZero});if(Q.allowNonZero&&!Array.isArray(V))return{friend:null,code:V.code};const Y=Array.isArray(V)?V:[],X=Y.find((K)=>K.encryptUid===J);if(X)return Q.allowNonZero?{friend:X,code:0}:X;if(Y.length===0)break}if($){const V=(await fetchRecommendList(z)).find((Y)=>Y.encryptUid===J);if(V)return Q.allowNonZero?{friend:V,code:0}:V}return Q.allowNonZero?{friend:null,code:0}:null}export async function clickCandidateInList(z,J){const Q=String(J).replace(/[^0-9]/g,"");return(await z.evaluate(`
19
+ `;let Z;try{Z=await Q.evaluate(X)}catch(Y){if(Y instanceof L||Y instanceof z)throw Y;const H=Y instanceof Error?Y.message:String(Y);throw new z(`Boss API request failed: ${H}`)}if(!Z||typeof Z!=="object")throw new z("Boss API returned malformed response");if(!W.allowNonZero&&Z.code!==0)assertOk(Z);return Z}export async function fetchFriendList(Q,V={}){const W=V.pageNum??1,J=V.jobId??"0",$=`https://${K}/wapi/zprelation/friend/getBossFriendListV2.json?page=${W}&status=0&jobId=${J}`,j=await bossFetch(Q,$,{allowNonZero:V.allowNonZero});if(V.allowNonZero&&j.code!==0)return j;const X=j.zpData?.friendList;if(!Array.isArray(X))throw new z("Boss friend list response did not include zpData.friendList");return X}export async function fetchRecommendList(Q){const V=`https://${K}/wapi/zprelation/friend/greetRecSortList`,J=(await bossFetch(Q,V)).zpData?.friendList;if(!Array.isArray(J))throw new z("Boss recommend response did not include zpData.friendList");return J}export async function findFriendByUid(Q,V,W={}){const J=W.maxPages??1,$=W.checkGreetList??!1;for(let j=1;j<=J;j++){const X=await fetchFriendList(Q,{pageNum:j,allowNonZero:W.allowNonZero});if(W.allowNonZero&&!Array.isArray(X))return{friend:null,code:X.code};const Z=Array.isArray(X)?X:[],Y=Z.find((H)=>H.encryptUid===V);if(Y)return W.allowNonZero?{friend:Y,code:0}:Y;if(Z.length===0)break}if($){const X=(await fetchRecommendList(Q)).find((Z)=>Z.encryptUid===V);if(X)return W.allowNonZero?{friend:X,code:0}:X}return W.allowNonZero?{friend:null,code:0}:null}export async function clickCandidateInList(Q,V){const W=String(V).replace(/[^0-9]/g,"");return(await Q.evaluate(`
20
20
  async () => {
21
- const uid = ${JSON.stringify(Q)};
21
+ const uid = ${JSON.stringify(W)};
22
22
  const item = document.querySelector('#_' + uid + '-0') || document.querySelector('[id^="_' + uid + '"]');
23
23
  if (item) {
24
24
  item.click();
@@ -33,7 +33,7 @@ import{ArgumentError as H,AuthRequiredError as G,CommandExecutionError as Z,Empt
33
33
  }
34
34
  return { clicked: false };
35
35
  }
36
- `)).clicked}export async function typeAndSendMessage(z,J){if(!(await z.evaluate(`
36
+ `)).clicked}export async function typeAndSendMessage(Q,V){if(!(await Q.evaluate(`
37
37
  async () => {
38
38
  const selectors = [
39
39
  '.chat-editor [contenteditable="true"]',
@@ -48,12 +48,12 @@ import{ArgumentError as H,AuthRequiredError as G,CommandExecutionError as Z,Empt
48
48
  if (el && el.offsetParent !== null) {
49
49
  el.focus();
50
50
  if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
51
- el.value = ${JSON.stringify(J)};
51
+ el.value = ${JSON.stringify(V)};
52
52
  el.dispatchEvent(new Event('input', { bubbles: true }));
53
53
  } else {
54
54
  el.textContent = '';
55
55
  el.focus();
56
- document.execCommand('insertText', false, ${JSON.stringify(J)});
56
+ document.execCommand('insertText', false, ${JSON.stringify(V)});
57
57
  el.dispatchEvent(new Event('input', { bubbles: true }));
58
58
  }
59
59
  return { found: true };
@@ -61,7 +61,7 @@ import{ArgumentError as H,AuthRequiredError as G,CommandExecutionError as Z,Empt
61
61
  }
62
62
  return { found: false };
63
63
  }
64
- `)).found)return!1;await z.wait({time:0.5});if(!(await z.evaluate(`
64
+ `)).found)return!1;await Q.wait({time:0.5});if(!(await Q.evaluate(`
65
65
  async () => {
66
66
  const btn = document.querySelector('.conversation-editor .submit')
67
67
  || document.querySelector('.submit-content .submit')
@@ -72,7 +72,7 @@ import{ArgumentError as H,AuthRequiredError as G,CommandExecutionError as Z,Empt
72
72
  }
73
73
  return { clicked: false };
74
74
  }
75
- `)).clicked)await z.pressKey("Enter");return!0}export function verbose(z){if(process.env.OPENCLI_VERBOSE)console.error(`[ppcli:boss] ${z}`)}export const IDENTITY_MISMATCH_CODE=24;const N=`https://${q}/web/geek/chat`;export async function navigateToGeekChat(z,J=2){await z.goto(N);await z.wait({time:J})}export async function readEncryptSystemId(z){return await z.evaluate(`
75
+ `)).clicked)await Q.pressKey("Enter");return!0}export function verbose(Q){if(process.env.OPENCLI_VERBOSE)console.error(`[ppcli:boss] ${Q}`)}export const IDENTITY_MISMATCH_CODE=24;const q=`https://${K}/web/geek/chat`;export async function navigateToGeekChat(Q,V=2){await Q.goto(q);await Q.wait({time:V})}export async function readEncryptSystemId(Q){return await Q.evaluate(`
76
76
  (() => {
77
77
  // 1. Vue app state / Pinia / $route.query
78
78
  // The chat component reads encryptSystemId from the app runtime to build
@@ -158,4 +158,4 @@ import{ArgumentError as H,AuthRequiredError as G,CommandExecutionError as Z,Empt
158
158
  } catch (_) {}
159
159
  return '';
160
160
  })()
161
- `)||""}export async function fetchGeekFriendLabelList(z,J={}){const Q=J.labelId??0,W=J.encryptSystemId??"",$=`https://${q}/wapi/zprelation/friend/geekFilterByLabel?labelId=${Q}&encryptSystemId=${encodeURIComponent(W)}`,j=await bossFetch(z,$,{allowNonZero:J.allowNonZero});if(J.allowNonZero&&j.code!==0)return j;const V=j.zpData?.friendList;if(!Array.isArray(V))throw new Z("Boss geek chat list response did not include zpData.friendList");return V}export async function fetchGeekFriendInfoList(z,J=[]){if(!J.length)return[];const Q=50,W=[];for(let $=0;$<J.length;$+=Q){const V=`friendIds=${J.slice($,$+Q).map(String).join(",")}`,X=(await bossFetch(z,`https://${q}/wapi/zprelation/friend/getGeekFriendList.json`,{method:"POST",body:V})).zpData?.result;if(!Array.isArray(X))throw new Z("Boss geek friend enrichment response did not include zpData.result");W.push(...X)}return W}export async function findGeekFriendByUid(z,J,Q={}){const $=(await fetchGeekFriendLabelList(z,{encryptSystemId:Q.encryptSystemId})).find((V)=>V.encryptFriendId===J||String(V.uid)===String(J)||String(V.friendId)===String(J));if(!$)return null;const j=await fetchGeekFriendInfoList(z,[$.friendId]);return{...$,...j[0]||{}}}export async function fetchGeekHistoryMsg(z,J,Q={}){const W=Q.page??1,$=J.uid,j=encodeURIComponent(J.securityId||""),V=`https://${q}/wapi/zpchat/geek/historyMsg?bossId=${$}&securityId=${j}&page=${W}&c=20&src=0`,Y=await bossFetch(z,V),X=Y.zpData?.messages??Y.zpData?.historyMsgList;if(!Array.isArray(X))throw new Z("Boss geek history response did not include a message list");if(X.length===0)throw new v("boss chatmsg","Boss returned no messages for this chat.");return X}
161
+ `)||""}export async function fetchGeekFriendLabelList(Q,V={}){const W=V.labelId??0,J=V.encryptSystemId??"",$=`https://${K}/wapi/zprelation/friend/geekFilterByLabel?labelId=${W}&encryptSystemId=${encodeURIComponent(J)}`,j=await bossFetch(Q,$,{allowNonZero:V.allowNonZero});if(V.allowNonZero&&j.code!==0)return j;const X=j.zpData?.friendList;if(!Array.isArray(X))throw new z("Boss geek chat list response did not include zpData.friendList");return X}export async function fetchGeekFriendInfoList(Q,V=[]){if(!V.length)return[];const W=50,J=[];for(let $=0;$<V.length;$+=W){const X=`friendIds=${V.slice($,$+W).map(String).join(",")}`,Y=(await bossFetch(Q,`https://${K}/wapi/zprelation/friend/getGeekFriendList.json`,{method:"POST",body:X})).zpData?.result;if(!Array.isArray(Y))throw new z("Boss geek friend enrichment response did not include zpData.result");J.push(...Y)}return J}export async function findGeekFriendByUid(Q,V,W={}){const $=(await fetchGeekFriendLabelList(Q,{encryptSystemId:W.encryptSystemId})).find((X)=>X.encryptFriendId===V||String(X.uid)===String(V)||String(X.friendId)===String(V));if(!$)return null;const j=await fetchGeekFriendInfoList(Q,[$.friendId]);return{...$,...j[0]||{}}}export async function fetchGeekHistoryMsg(Q,V,W={}){const J=W.page??1,$=V.uid,j=encodeURIComponent(V.securityId||""),X=`https://${K}/wapi/zpchat/geek/historyMsg?bossId=${$}&securityId=${j}&page=${J}&c=20&src=0`,Z=await bossFetch(Q,X),Y=Z.zpData?.messages??Z.zpData?.historyMsgList;if(!Array.isArray(Y))throw new z("Boss geek history response did not include a message list");if(Y.length===0)throw new F("boss chatmsg","Boss returned no messages for this chat.");return Y}const M=`https://${K}/web/geek/job`;export async function navigateToGeekJob(Q,V=2){await Q.goto(M);await Q.wait({time:V})}export async function geekGet(Q,V,W={},J={}){const $=new URLSearchParams;for(const[Y,H]of Object.entries(W)){if(H===void 0||H===null||H==="")continue;$.set(Y,String(H))}const j=$.toString(),X=`https://${K}${V}${j?`?${j}`:""}`,Z=await bossFetch(Q,X,J);if(J.allowNonZero&&Z.code!==0)return Z;return Z.zpData??{}}export async function geekPost(Q,V,W={},J={}){const $=new URLSearchParams;for(const[X,Z]of Object.entries(W)){if(Z===void 0||Z===null)continue;$.set(X,String(Z))}const j=await bossFetch(Q,`https://${K}${V}`,{...J,method:"POST",body:$.toString()});if(J.allowNonZero&&j.code!==0)return j;return j.zpData??{}}export async function fetchGeekUserInfo(Q){return geekGet(Q,"/wapi/zpuser/wap/getUserInfo.json")}export async function fetchGeekBossData(Q,V){const W=await geekGet(Q,"/wapi/zpchat/geek/getBossData",{bossId:V});return W?.data??W??{}}export async function fetchGeekLastMsgs(Q,V=[]){if(!V.length)return[];const W=await geekGet(Q,"/wapi/zpchat/geek/userLastMsg",{friendIds:V.map(String).join(",")});return Array.isArray(W)?W:W?.result??[]}export async function fetchGeekAttachmentResumes(Q){return(await geekGet(Q,"/wapi/zpgeek/resume/attachment/checkbox.json"))?.resumeList??[]}export async function resolveGeekResumeId(Q,V){const W=await fetchGeekAttachmentResumes(Q);if(V){if(!W.find((j)=>String(j.resumeId)===String(V))){const j=W.map((X)=>X.resumeId||"?").join(", ")||"(空)";throw new P(`指定的 --resume-id=${V} 不在账号简历列表 [${j}] 中`)}return String(V)}if(W.length===0)throw new F("boss resumelist","账号还没有绑定附件简历,请先用 boss resumeupload 上传,或在 BOSS 直聘 App 里上传。");if(W.length===1){const $=W[0].resumeId;if(!$)throw new z("唯一附件简历缺失 resumeId 字段");return String($)}const J=W.map(($)=>`${$.resumeId}(${$.showName||"未命名"})`).join(", ");throw new P(`账号绑定了 ${W.length} 份附件简历,请用 --resume-id 指定其中一份:${J}`)}export async function resolveGeekBoss(Q,V){const W=String(V??"").trim();if(!W)throw new P("boss id cannot be empty");if(/^\d+$/.test(W))return{id:W,friend:null};const J=await findGeekFriendByUid(Q,W);if(!J)throw new F("boss chatlist",`未在沟通列表中找到 ${W},请先用 boss chatlist --side geek 查 uid。`);const $=J.uid??J.friendId;if(!$)throw new z("沟通列表条目缺失 uid / friendId");return{id:String($),friend:J}}export async function resolveGeekBossId(Q,V){return(await resolveGeekBoss(Q,V)).id}export async function geekAddFriend(Q,V,W=""){return geekGet(Q,"/wapi/zpgeek/friend/add.json",{securityId:V,lid:W})}export async function geekOpenChat(Q,V,W=3){await Q.goto(`${q}?id=${encodeURIComponent(String(V))}`);await Q.wait({time:W})}export const CITY_CODES={"全国":"100010000","北京":"101010100","上海":"101020100","广州":"101280100","深圳":"101280600","杭州":"101210100","成都":"101270100","南京":"101190100","武汉":"101200100","西安":"101110100","苏州":"101190400","长沙":"101250100","天津":"101030100","重庆":"101040100","郑州":"101180100","东莞":"101281600","青岛":"101120200","合肥":"101220100","佛山":"101280800","宁波":"101210400","厦门":"101230200","大连":"101070200","珠海":"101280700","无锡":"101190200","济南":"101120100","福州":"101230100","昆明":"101290100","哈尔滨":"101050100","沈阳":"101070100","石家庄":"101090100","贵阳":"101260100","南宁":"101300100","太原":"101100100","海口":"101310100","兰州":"101160100","乌鲁木齐":"101130100","长春":"101060100","南昌":"101240100","常州":"101191100","温州":"101210700","嘉兴":"101210300","徐州":"101190800","香港":"101320100"};export async function isRecruiterSide(Q){await navigateToChat(Q);const V=await bossFetch(Q,`https://${K}/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0`,{allowNonZero:!0});if(V.code===0)return!0;if(V.code===IDENTITY_MISMATCH_CODE)return!1;assertOk(V,"boss 身份探测");return!1}
@@ -1 +1 @@
1
- import{CliError as W,CommandExecutionError as F}from"@jackwener/opencli/errors";import{cli as O,Strategy as _}from"@jackwener/opencli/registry";import{readFile as q,stat as T}from"node:fs/promises";import{publishArticle as S}from"../_shared/article/publish.js";export const oschinaProfile={home:"https://my.oschina.net",outputFormat:"markdown",image:{spec:{url:"https://apiv1.oschina.net/oschinapi/ai/creation/project/uploadDetail",method:"POST",bodyType:"binary-multipart",fileField:"file",fileName:"image",body:{},responsePath:"result"},skip:["apiv1.oschina.net","oscimg.oschina.net","static.oschina.net"]},checkAuth:async()=>{const G=await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"});if(!G.ok)return{isAuthenticated:!1,error:"HTTP "+G.status};let z=null;try{z=await G.json()}catch(V){return{isAuthenticated:!1,error:"解析响应失败"}}if(!z||!z.success||!z.result||!z.result.userId)return{isAuthenticated:!1,error:z&&z.message||"未登录"};const B=String(z.result.userId),K=z.result.userVo&&z.result.userVo.name||B,M=z.result.userVo&&z.result.userVo.portraitUrl||"";return{isAuthenticated:!0,userId:B,username:K,avatar:M}},publish:async(G)=>{const z=G.params||{};let B="",K="";try{const H=await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"});if(!H.ok)return{ok:!1,stage:"auth",status:H.status,message:"获取开源中国用户信息失败:HTTP "+H.status+"(登录态可能已失效,请在客户端重新登录)"};const L=await H.json();if(L&&L.success&&L.result&&L.result.userId)B=String(L.result.userId);else K=L&&L.message||"myDetails 未返回 userId(登录态可能已失效或为匿名)"}catch(H){return{ok:!1,stage:"auth",message:"校验开源中国登录态时请求异常:"+String(H&&H.message||H)}}if(!B)return{ok:!1,stage:"auth",message:"未登录开源中国,无法获取用户 ID"+(K?":"+K:"")};let M=0;if(z.category){let H=[];try{const Y=await(await fetch("https://apiv1.oschina.net/oschinapi/blog_catalog/list_by_user",{credentials:"include"})).json();H=Y&&Y.result||[]}catch(J){return{ok:!1,stage:"category",message:"获取开源中国博客分类失败:"+String(J&&J.message||J)}}const L=H.find((J)=>String(J.name)===String(z.category));if(!L)return{ok:!1,stage:"category",message:"未找到博客分类「"+z.category+"」,可选:"+(H.map((J)=>J.name).join(" / ")||"(你还没有任何分类)")};M=L.id}if(G.draftOnly){const H=await fetch("https://apiv1.oschina.net/oschinapi/api/draft/save_draft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,user:Number(B),content:G.content,contentType:1,catalog:M,originUrl:"",privacy:!0,disableComment:!1})}),L=await H.text();let J=null;try{J=JSON.parse(L)}catch(j){}if(!H.ok||!J||!J.success||!J.result||!J.result.id)return{ok:!1,stage:"save_draft",status:H.status,message:J&&J.message||L.slice(0,300)};const Y=String(J.result.id);return{ok:!0,draft:!0,id:Y,url:"https://my.oschina.net/u/"+B+"/blog/ai-write/draft/"+Y}}const V=await fetch("https://apiv1.oschina.net/oschinapi/blog/web/add",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,content:G.content,contentType:1,type:"1",originUrl:"",catalog:M,privacy:!1,disableComment:!1,isAiRelated:!1,user:Number(B)})}),U=await V.text();let Q=null;try{Q=JSON.parse(U)}catch(H){}if(!V.ok||!Q||Q.code!==200||Q.result==null)return{ok:!1,stage:"publish",status:V.status,message:Q&&(Q.message||Q.msg)||U.slice(0,300)};const X=String(Q.result);return{ok:!0,draft:!1,id:X,url:"https://my.oschina.net/u/"+B+"/blog/"+X}}};function Z(G){if(!G.execute)throw new W("INVALID_INPUT","发布开源中国文章需要 --execute 参数。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}function $(G,z,B,K,M={}){return[{status:"success",outcome:K,message:G,target_type:z,target:B,...M}]}async function A(G){const z=typeof G.text==="string"?G.text:void 0,B=typeof G.file==="string"?G.file:void 0;if(z&&B)throw new W("INVALID_INPUT","正文和文件路径只能二选一,不能同时指定");let K=z??"";if(B){let M;try{M=await T(B)}catch{throw new W("INVALID_INPUT","文件不存在:"+B)}if(!M.isFile())throw new W("INVALID_INPUT","必须指定可读的文本文件:"+B);let V;try{V=await q(B)}catch{throw new W("INVALID_INPUT","文件读取失败:"+B)}try{K=new TextDecoder("utf-8",{fatal:!0}).decode(V)}catch{throw new W("INVALID_INPUT","文件编码不是 UTF-8:"+B)}}if(!K.trim())throw new W("INVALID_INPUT","文章正文不能为空");return K}O({site:"oschina",name:"article",access:"write",description:"发布文章到开源中国博客。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown,外链图片自动转存到开源中国图床。",domain:"my.oschina.net",strategy:_.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;--html 表示原始 HTML)"},{name:"file",help:"从文件读取正文(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"category",help:"个人博客分类名(精确匹配),合法值用 `oschina catalogs` 列举;可空=不分类"},{name:"draft",type:"boolean",help:"仅存草稿,不发布"},{name:"execute",type:"boolean",help:"真正执行写操作;不加此参数命令拒绝写入"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(G,z)=>{if(!G)throw new F("开源中国文章发布需要浏览器会话");Z(z);const B=String(z.title??"").trim();if(!B)throw new W("INVALID_INPUT","文章标题不能为空");const K=await A(z),M=Boolean(z.draft),V={category:typeof z.category==="string"?z.category.trim():""},U=await S(G,{title:B,body:K,format:z.html?"html":"markdown",draftOnly:M,profile:oschinaProfile,publishParams:V}),Q=U.images.uploaded.length|0,X=U.images.failed.length|0;let H=U.draft?"已保存到开源中国草稿箱":"已正式发布到开源中国博客";if(Q||X)H+=`·图片:${Q} 张已转存${X?`,${X} 张失败`:""}`;return $(H,"article","",U.draft?"draft":"created",{created_target:(U.draft?"draft:":"article:")+U.id,created_url:U.url})}});export const __test__={oschinaProfile,requireExecute:Z,buildResultRow:$,resolvePayload:A};
1
+ import{CliError as W,CommandExecutionError as F}from"@jackwener/opencli/errors";import{cli as O,Strategy as _}from"@jackwener/opencli/registry";import{readFile as q,stat as T}from"node:fs/promises";import{publishArticle as j}from"../_shared/article/publish.js";export const oschinaProfile={home:"https://my.oschina.net",outputFormat:"markdown",image:{spec:{url:"https://apiv1.oschina.net/oschinapi/ai/creation/project/uploadDetail",method:"POST",bodyType:"binary-multipart",fileField:"file",fileName:"image",body:{},responsePath:"result"},skip:["apiv1.oschina.net","oscimg.oschina.net","static.oschina.net"]},checkAuth:async()=>{const G=await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"});if(!G.ok)return{isAuthenticated:!1,error:"HTTP "+G.status};let z=null;try{z=await G.json()}catch(V){return{isAuthenticated:!1,error:"解析响应失败"}}if(!z||!z.success||!z.result||!z.result.userId)return{isAuthenticated:!1,error:z&&z.message||"未登录"};const B=String(z.result.userId),K=z.result.userVo&&z.result.userVo.name||B,M=z.result.userVo&&z.result.userVo.portraitUrl||"";return{isAuthenticated:!0,userId:B,username:K,avatar:M}},publish:async(G)=>{const z=G.params||{};let B="",K="";try{const H=await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"});if(!H.ok)return{ok:!1,stage:"auth",status:H.status,message:"获取开源中国用户信息失败:HTTP "+H.status+"(登录态可能已失效,请在客户端重新登录)"};const L=await H.json();if(L&&L.success&&L.result&&L.result.userId)B=String(L.result.userId);else K=L&&L.message||"myDetails 未返回 userId(登录态可能已失效或为匿名)"}catch(H){return{ok:!1,stage:"auth",message:"校验开源中国登录态时请求异常:"+String(H&&H.message||H)}}if(!B)return{ok:!1,stage:"auth",message:"未登录开源中国,无法获取用户 ID"+(K?":"+K:"")};let M=0;if(z.category){let H=[];try{const Y=await(await fetch("https://apiv1.oschina.net/oschinapi/blog_catalog/list_by_user",{credentials:"include"})).json();H=Y&&Y.result||[]}catch(J){return{ok:!1,stage:"category",message:"获取开源中国博客分类失败:"+String(J&&J.message||J)}}const L=H.find((J)=>String(J.name)===String(z.category));if(!L)return{ok:!1,stage:"category",message:"未找到博客分类「"+z.category+"」,可选:"+(H.map((J)=>J.name).join(" / ")||"(你还没有任何分类)")};M=L.id}if(G.draftOnly){const H=await fetch("https://apiv1.oschina.net/oschinapi/api/draft/save_draft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,user:Number(B),content:G.content,contentType:1,catalog:M,originUrl:"",privacy:!0,disableComment:!1})}),L=await H.text();let J=null;try{J=JSON.parse(L)}catch(S){}if(!H.ok||!J||!J.success||!J.result||!J.result.id)return{ok:!1,stage:"save_draft",status:H.status,message:J&&J.message||L.slice(0,300)};const Y=String(J.result.id);return{ok:!0,draft:!0,id:Y,url:"https://my.oschina.net/u/"+B+"/blog/ai-write/draft/"+Y}}const V=await fetch("https://apiv1.oschina.net/oschinapi/blog/web/add",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,content:G.content,contentType:1,type:"1",originUrl:"",catalog:M,privacy:!1,disableComment:!1,isAiRelated:!1,user:Number(B)})}),Q=await V.text();let U=null;try{U=JSON.parse(Q)}catch(H){}if(!V.ok||!U||U.code!==200||U.result==null)return{ok:!1,stage:"publish",status:V.status,message:U&&(U.message||U.msg)||Q.slice(0,300)};const X=String(U.result);return{ok:!0,draft:!1,id:X,url:"https://my.oschina.net/u/"+B+"/blog/"+X}}};function Z(G){if(!G.execute)throw new W("INVALID_INPUT","发布开源中国文章需要 --execute 参数。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}function $(G,z,B,K,M={}){return[{status:"success",outcome:K,message:G,target_type:z,target:B,...M}]}async function A(G){const z=typeof G.text==="string"?G.text:void 0,B=typeof G.file==="string"?G.file:void 0;if(z&&B)throw new W("INVALID_INPUT","正文和文件路径只能二选一,不能同时指定");let K=z??"";if(B){let M;try{M=await T(B)}catch{throw new W("INVALID_INPUT","文件不存在:"+B)}if(!M.isFile())throw new W("INVALID_INPUT","必须指定可读的文本文件:"+B);let V;try{V=await q(B)}catch{throw new W("INVALID_INPUT","文件读取失败:"+B)}try{K=new TextDecoder("utf-8",{fatal:!0}).decode(V)}catch{throw new W("INVALID_INPUT","文件编码不是 UTF-8:"+B)}}if(!K.trim())throw new W("INVALID_INPUT","文章正文不能为空");return K}O({site:"oschina",name:"article",access:"write",description:"发布文章到开源中国博客。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown,外链图片自动转存到开源中国图床。",domain:"my.oschina.net",strategy:_.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;--html 表示原始 HTML)"},{name:"file",help:"从文件读取正文(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"category",help:"个人博客分类名(精确匹配),合法值用 `oschina catalogs` 列举;可空=不分类"},{name:"draft",type:"boolean",help:"仅存草稿,不发布"},{name:"execute",type:"boolean",help:"真正执行写操作;不加此参数命令拒绝写入"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(G,z)=>{if(!G)throw new F("开源中国文章发布需要浏览器会话");Z(z);const B=String(z.title??"").trim();if(!B)throw new W("INVALID_INPUT","文章标题不能为空");const K=await A(z),M=Boolean(z.draft),V={category:typeof z.category==="string"?z.category.trim():""},Q=await j(G,{title:B,body:K,format:z.html?"html":"markdown",draftOnly:M,profile:oschinaProfile,publishParams:V}),U=Q.images.uploaded.length|0,X=Q.images.failed.length|0;let H=Q.draft?"已保存到开源中国草稿箱":"已正式发布到开源中国博客";if(U||X)H+=`·图片:${U} 张已转存${X?`,${X} 张失败`:""}`;if(!Q.draft)H+="。已按公开发布提交;开源中国对新账号/新文章有站方审核,过审前它不会出现在公开列表,"+"此时 stats 的 is_private 会显示「是」——这是审核中,不是发布失败,请勿重发,稍后回查即可";return $(H,"article","",Q.draft?"draft":"created",{created_target:(Q.draft?"draft:":"article:")+Q.id,created_url:Q.url})}});export const __test__={oschinaProfile,requireExecute:Z,buildResultRow:$,resolvePayload:A};
@@ -51,4 +51,4 @@ import{cli as Y,Strategy as $}from"@jackwener/opencli/registry";import{CliError
51
51
  }
52
52
  return out;
53
53
  }
54
- `}function f(h){if(h&&typeof h==="object"&&!Array.isArray(h)&&"session"in h&&"data"in h)return h.data;return h}function U(h){return typeof h==="number"&&Number.isFinite(h)?h:""}function W(h,H,Q,T,q){const F=h.id!=null?String(h.id):"",N=T&&F?T.get(F):null,B=(z)=>h[z]!=null?h[z]:N?N[z]:void 0,K=Number(B("type")),O={};for(const[z,D]of Object.entries({reply_count:U(B("replyCount")),state:B("state"),as_recomm:h.asRecomm,detritus:B("detritus"),scheduled_time:h.scheduledTime,scheduled_state:h.scheduledState,summary:typeof B("detail")==="string"?String(B("detail")).slice(0,80):void 0}))if(D!==void 0&&D!==null&&D!=="")O[z]=D;return{id:F,type:"博客",title:h.title!=null?String(h.title):"",url:F&&H?`https://my.oschina.net/u/${H}/blog/${F}`:"",published_at:B("createTime")||"",views:U(B("viewCount")),likes:U(B("voteCount")),comments:U(B("commentCount")),collects:"",shares:"",origin:K===2?"转载":K===1?"原创":"",is_private:Q!=="已发布"?"":q||N?"否":"是",recommended:Number(B("recomm"))===1?"推荐":"",top:Number(B("asTop"))===1?"置顶":"",status:Q,extra:Object.keys(O).length?JSON.stringify(O):""}}Y({site:"oschina",name:"stats",access:"read",description:"开源中国博客数据(每篇阅读/点赞/评论等全量运营指标)",domain:"my.oschina.net",strategy:$.COOKIE,browser:!0,args:[{name:"limit",type:"number",default:20,help:"返回条数"},{name:"user",help:"查看指定用户 ID 的公开博客数据(默认当前登录账号,含私密与定时博客)"}],columns:["id","type","title","url","published_at","views","likes","comments","collects","shares","origin","is_private","recommended","top","status","extra"],func:async(h,H)=>{const Q=Number(H.limit)||20,T=H.user!=null&&String(H.user).trim()!==""?String(H.user).trim():"";await h.goto("https://my.oschina.net/");await h.wait(1);const q=f(await h.evaluate(Z(Q,T)));if(!q||typeof q!=="object")throw Error("开源中国页面取数失败:未拿到任何响应,请确认 Chrome 已打开且能访问 my.oschina.net。");if(q.notLoggedIn)throw Error("未登录开源中国:user/myDetails 未返回用户信息。请先在 Chrome 中登录 my.oschina.net(可用 ppcli oschina login)。");const F=String(q.targetUser||q.me||""),N=q.pub&&q.pub.rows||[];if(q.pub&&q.pub.error)throw Error("获取开源中国博客公开列表失败:"+q.pub.error);const B=new Map(N.filter((z)=>z&&z.id!=null).map((z)=>[String(z.id),z])),K=[],O=new Set;if(q.isSelf){if(q.mine&&q.mine.error)throw Error("获取开源中国「我的创作」博客列表失败:"+q.mine.error);for(const z of q.mine&&q.mine.rows||[]){const D=W(z,F,"已发布",B);if(D.id)O.add(D.id);K.push(D)}for(const z of N){const D=z&&z.id!=null?String(z.id):"";if(!D||O.has(D))continue;O.add(D);K.push(W(z,F,"已发布",null,!0))}for(const z of q.scheduled&&q.scheduled.rows||[])K.push(W(z,F,"已定时未发布",null,!1))}else for(const z of N)K.push(W(z,F,"已发布",null,!0));if(K.length===0)throw new I("EMPTY_RESULT",q.isSelf?"当前开源中国账号没有任何博客(已发布/定时均为空)。":`用户 ${F} 没有任何公开博客。`,q.isSelf?"发布过内容后再来查看数据(ppcli oschina article)。":"请确认用户 ID 是否正确。");return K.slice(0,Q)}});
54
+ `}function j(h){if(h&&typeof h==="object"&&!Array.isArray(h)&&"session"in h&&"data"in h)return h.data;return h}function U(h){return typeof h==="number"&&Number.isFinite(h)?h:""}function W(h,H,Q,T,q){const F=h.id!=null?String(h.id):"",N=T&&F?T.get(F):null,B=(z)=>h[z]!=null?h[z]:N?N[z]:void 0,K=Number(B("type")),O={};for(const[z,D]of Object.entries({reply_count:U(B("replyCount")),state:B("state"),as_recomm:h.asRecomm,detritus:B("detritus"),scheduled_time:h.scheduledTime,scheduled_state:h.scheduledState,summary:typeof B("detail")==="string"?String(B("detail")).slice(0,80):void 0}))if(D!==void 0&&D!==null&&D!=="")O[z]=D;if(Q==="已发布"&&!(q||N))O.visibility_note="未出现在公开列表:可能是站方审核中(新账号/新文章常见),也可能是该文设为私密;请稍后回查再判定";return{id:F,type:"博客",title:h.title!=null?String(h.title):"",url:F&&H?`https://my.oschina.net/u/${H}/blog/${F}`:"",published_at:B("createTime")||"",views:U(B("viewCount")),likes:U(B("voteCount")),comments:U(B("commentCount")),collects:"",shares:"",origin:K===2?"转载":K===1?"原创":"",is_private:Q!=="已发布"?"":q||N?"否":"是",recommended:Number(B("recomm"))===1?"推荐":"",top:Number(B("asTop"))===1?"置顶":"",status:Q,extra:Object.keys(O).length?JSON.stringify(O):""}}Y({site:"oschina",name:"stats",access:"read",description:"开源中国博客数据(每篇阅读/点赞/评论等全量运营指标)",domain:"my.oschina.net",strategy:$.COOKIE,browser:!0,args:[{name:"limit",type:"number",default:20,help:"返回条数"},{name:"user",help:"查看指定用户 ID 的公开博客数据(默认当前登录账号,含私密与定时博客)"}],columns:["id","type","title","url","published_at","views","likes","comments","collects","shares","origin","is_private","recommended","top","status","extra"],func:async(h,H)=>{const Q=Number(H.limit)||20,T=H.user!=null&&String(H.user).trim()!==""?String(H.user).trim():"";await h.goto("https://my.oschina.net/");await h.wait(1);const q=j(await h.evaluate(Z(Q,T)));if(!q||typeof q!=="object")throw Error("开源中国页面取数失败:未拿到任何响应,请确认 Chrome 已打开且能访问 my.oschina.net。");if(q.notLoggedIn)throw Error("未登录开源中国:user/myDetails 未返回用户信息。请先在 Chrome 中登录 my.oschina.net(可用 ppcli oschina login)。");const F=String(q.targetUser||q.me||""),N=q.pub&&q.pub.rows||[];if(q.pub&&q.pub.error)throw Error("获取开源中国博客公开列表失败:"+q.pub.error);const B=new Map(N.filter((z)=>z&&z.id!=null).map((z)=>[String(z.id),z])),K=[],O=new Set;if(q.isSelf){if(q.mine&&q.mine.error)throw Error("获取开源中国「我的创作」博客列表失败:"+q.mine.error);for(const z of q.mine&&q.mine.rows||[]){const D=W(z,F,"已发布",B);if(D.id)O.add(D.id);K.push(D)}for(const z of N){const D=z&&z.id!=null?String(z.id):"";if(!D||O.has(D))continue;O.add(D);K.push(W(z,F,"已发布",null,!0))}for(const z of q.scheduled&&q.scheduled.rows||[])K.push(W(z,F,"已定时未发布",null,!1))}else for(const z of N)K.push(W(z,F,"已发布",null,!0));if(K.length===0)throw new I("EMPTY_RESULT",q.isSelf?"当前开源中国账号没有任何博客(已发布/定时均为空)。":`用户 ${F} 没有任何公开博客。`,q.isSelf?"发布过内容后再来查看数据(ppcli oschina article)。":"请确认用户 ID 是否正确。");return K.slice(0,Q)}});
@@ -1,12 +1,9 @@
1
- import{spawn as C}from"node:child_process";import{fileURLToPath as F}from"node:url";import*as M from"node:fs";import*as K from"node:path";import{DEFAULT_DAEMON_PORT}from"../constants.js";import{BrowserConnectError as J}from"../errors.js";import{PKG_VERSION as N}from"../version.js";import{waitForBridgeReady as b}from"./bridge-readiness.js";import{fetchDaemonStatus as B,getDaemonHealth as g,requestDaemonShutdown as O}from"./daemon-transport.js";export function resolveDaemonLaunchSpec(){const z=K.dirname(F(import.meta.url)),Q=K.resolve(z,".."),W=K.join(Q,"daemon.ts"),Z=K.join(Q,"daemon.js"),Y=M.existsSync(W),X=Y?W:Z,$=!!process.versions?.bun,U=Y&&!$;return{binary:process.execPath,args:U?["--import","tsx/esm",X]:[X],scriptPath:X}}export function spawnDaemonProcess(){const z=resolveDaemonLaunchSpec(),Q=C(z.binary,z.args,{detached:!0,stdio:"ignore",env:{...process.env}});Q.unref();return Q}export async function waitForDaemonStop(z){const Q=Date.now()+z;while(Date.now()<Q){await x(200);if((await g()).state==="stopped")return!0}return!1}export async function waitForDaemonStatus(z){const Q=Date.now()+z;while(Date.now()<Q){const W=await B({timeout:Math.min(1000,Math.max(100,Q-Date.now()))});if(W)return W;await x(200)}return null}export const daemonLifecycleHooks={requestDaemonShutdown:O,spawnDaemonProcess,waitForDaemonStop};export async function restartDaemon(z={}){const Q=await B();let W=Q===null;if(Q){W=await O()&&await waitForDaemonStop(z.stopTimeoutMs??3000);if(!W)return{previousStatus:Q,status:Q,stopped:!1,spawned:!1}}spawnDaemonProcess();const Z=await waitForDaemonStatus(z.startTimeoutMs??5000);return{previousStatus:Q,status:Z,stopped:W,spawned:!0}}export async function ensureBrowserBridgeReady(z={}){const W=(z.timeoutSeconds&&z.timeoutSeconds>0?z.timeoutSeconds:10)*1000,Z=z.verbose??!0,Y=z.contextId,X=await g({contextId:Y}),$=X.status?.daemonVersion,U=!!X.status&&(!$||$!==N);let q=!1,k=null;if(U){const y=$?`v${$} ≠ v${N}`:`pre-version daemon, CLI is v${N}`;if(Z&&(process.env.OPENCLI_VERBOSE||process.stderr.isTTY))process.stderr.write(`⚠️ Stale daemon detected (${y}). Restarting...
2
- `);let j=await daemonLifecycleHooks.requestDaemonShutdown()&&await daemonLifecycleHooks.waitForDaemonStop(3000);if(!j){const G=X.status?.pid;if(typeof G==="number"&&Number.isInteger(G)&&G>0){try{process.kill(G,"SIGKILL")}catch{}j=await daemonLifecycleHooks.waitForDaemonStop(2000)}}if(!j)throw new J("Stale daemon could not be replaced",`A stale daemon (${y}) is running but did not shut down (graceful + SIGKILL both failed).
3
- Run manually: ppcli daemon stop`,"daemon-not-running");q=!0}if(!q&&X.state==="ready")return{health:X,spawnedProcess:k};if(!q&&X.state==="profile-required")throw V(X,Y);if(q||X.state==="stopped"){if(Z&&(process.env.OPENCLI_VERBOSE||process.stderr.isTTY))process.stderr.write(`⏳ Starting daemon...
1
+ import{spawn as V}from"node:child_process";import{fileURLToPath as b}from"node:url";import*as x from"node:fs";import*as K from"node:path";import{DEFAULT_DAEMON_PORT}from"../constants.js";import{BrowserConnectError as J}from"../errors.js";import{PKG_VERSION as j}from"../version.js";import{waitForBridgeReady as F}from"./bridge-readiness.js";import{fetchDaemonStatus as B,getDaemonHealth as y,requestDaemonShutdown as C}from"./daemon-transport.js";export function resolveDaemonLaunchSpec(){const z=K.dirname(b(import.meta.url)),Q=K.resolve(z,".."),W=K.join(Q,"daemon.ts"),Z=K.join(Q,"daemon.js"),Y=x.existsSync(W),X=Y?W:Z,$=!!process.versions?.bun,U=Y&&!$;return{binary:process.execPath,args:U?["--import","tsx/esm",X]:[X],scriptPath:X}}export function spawnDaemonProcess(){const z=resolveDaemonLaunchSpec(),Q=V(z.binary,z.args,{detached:!0,stdio:"ignore",env:{...process.env}});Q.unref();return Q}export async function waitForDaemonStop(z){const Q=Date.now()+z;while(Date.now()<Q){await O(200);if((await y()).state==="stopped")return!0}return!1}export async function waitForDaemonStatus(z){const Q=Date.now()+z;while(Date.now()<Q){const W=await B({timeout:Math.min(1000,Math.max(100,Q-Date.now()))});if(W)return W;await O(200)}return null}export const daemonLifecycleHooks={requestDaemonShutdown:C,spawnDaemonProcess,waitForDaemonStop};export async function restartDaemon(z={}){const Q=await B();let W=Q===null;if(Q){W=await C()&&await waitForDaemonStop(z.stopTimeoutMs??3000);if(!W)return{previousStatus:Q,status:Q,stopped:!1,spawned:!1}}spawnDaemonProcess();const Z=await waitForDaemonStatus(z.startTimeoutMs??5000);return{previousStatus:Q,status:Z,stopped:W,spawned:!0}}export async function ensureBrowserBridgeReady(z={}){const W=(z.timeoutSeconds&&z.timeoutSeconds>0?z.timeoutSeconds:10)*1000,Z=z.verbose??!0,Y=z.contextId,X=await y({contextId:Y}),$=X.status?.daemonVersion,U=!!X.status&&(!$||$!==j);let q=!1,k=null;if(U){const N=$?`v${$} ≠ v${j}`:`pre-version daemon, CLI is v${j}`;if(Z&&(process.env.OPENCLI_VERBOSE||process.stderr.isTTY))process.stderr.write(`⚠️ Stale daemon detected (${N}). Restarting...
2
+ `);let g=await daemonLifecycleHooks.requestDaemonShutdown()&&await daemonLifecycleHooks.waitForDaemonStop(3000);if(!g){const G=X.status?.pid;if(typeof G==="number"&&Number.isInteger(G)&&G>0){try{process.kill(G,"SIGKILL")}catch{}g=await daemonLifecycleHooks.waitForDaemonStop(2000)}}if(!g)throw new J("Stale daemon could not be replaced",`A stale daemon (${N}) is running but did not shut down (graceful + SIGKILL both failed).
3
+ Run manually: ppcli daemon stop`,"daemon-not-running");q=!0}if(!q&&X.state==="ready")return{health:X,spawnedProcess:k};if(!q&&X.state==="profile-required")throw M(X,Y);if(q||X.state==="stopped"){if(Z&&(process.env.OPENCLI_VERBOSE||process.stderr.isTTY))process.stderr.write(`⏳ Starting daemon...
4
4
  `);k=daemonLifecycleHooks.spawnDaemonProcess()}else if(Z&&(process.env.OPENCLI_VERBOSE||process.stderr.isTTY)){process.stderr.write(`⏳ Waiting for Chrome/Chromium extension to connect...
5
5
  `);process.stderr.write(` Make sure Chrome or Chromium is open and the ppcli extension is enabled.
6
- `)}const A=await b(g,{timeoutMs:W,contextId:Y});if(A.state==="ready")return{health:A,spawnedProcess:k};throw V(A,Y)}function V(z,Q){if(z.state==="profile-required")return new J("Multiple Browser Bridge profiles are connected",`Select one with --profile <name>, OPENCLI_PROFILE=<name>, or ppcli profile use <name>.
7
- Run ppcli profile list to see connected profiles.`,"profile-required");if(z.state==="profile-disconnected"){const W=Q??z.status.contextId??"unknown";return new J(`Browser profile "${W}" is not connected`,"Open the matching Chrome profile and make sure the ppcli extension is enabled, or choose another profile with ppcli profile use <name>.","profile-disconnected")}if(z.state==="no-extension")return new J("Browser Bridge extension not connected",`Make sure Chrome/Chromium is open and the ppcli extension is enabled.
8
- If not installed:
9
- 1. Download: https://github.com/jackwener/OpenCLI/releases
10
- `+" 2. Open chrome://extensions → Developer Mode → Load unpacked","extension-not-connected");return new J("Failed to start ppcli daemon",`Try running manually:
6
+ `)}const A=await F(y,{timeoutMs:W,contextId:Y});if(A.state==="ready")return{health:A,spawnedProcess:k};throw M(A,Y)}function M(z,Q){if(z.state==="profile-required")return new J("Multiple Browser Bridge profiles are connected",`Select one with --profile <name>, OPENCLI_PROFILE=<name>, or ppcli profile use <name>.
7
+ Run ppcli profile list to see connected profiles.`,"profile-required");if(z.state==="profile-disconnected"){const W=Q??z.status.contextId??"unknown";return new J(`浏览器环境 "${W}" 未连接`,`这台机器上名为 "${W}" 的浏览器环境当前没连上。请用户打开 PublishPort 客户端,`+"在「高级设置 → 环境」里确认该环境已启动;或改用其它环境(--profile <名字>),"+"不带 --profile 则用默认环境。这是设备侧环境问题,重试同一条命令不会好转,请先告知用户。","profile-disconnected")}if(z.state==="no-extension")return new J("浏览器桥未连接","这台机器上的 PublishPort 托管浏览器当前没在运行(或浏览器插件被禁用了)。"+"请用户打开 PublishPort 客户端,在平台列表里点一次「检测登录」,客户端会把托管浏览器拉起来;"+"若仍不行,重启一次 PublishPort 客户端。这是设备侧环境问题,重试同一条命令不会好转,请先告知用户。","extension-not-connected");return new J("Failed to start ppcli daemon",`Try running manually:
11
8
  node ${resolveDaemonLaunchSpec().scriptPath}
12
- Make sure port ${DEFAULT_DAEMON_PORT} is available.`,"daemon-not-running")}function x(z){return new Promise((Q)=>setTimeout(Q,z))}export{DEFAULT_DAEMON_PORT};
9
+ Make sure port ${DEFAULT_DAEMON_PORT} is available.`,"daemon-not-running")}function O(z){return new Promise((Q)=>setTimeout(Q,z))}export{DEFAULT_DAEMON_PORT};
@@ -1,5 +1,5 @@
1
- import{BrowserConnectError as H}from"../errors.js";import{DEFAULT_DAEMON_PORT as Q}from"../constants.js";const W=["Extension disconnected","Extension not connected","attach failed","Detached while handling command","Debugger is not attached to the tab","no longer exists","No tab with id","CDP connection","Daemon command failed","No window with id"],Y=["Inspected target navigated or closed"];function Z(q){return q instanceof Error?q.message:String(q)}const K={attach_failed:{kind:"extension-transient",retryable:!0,delayMs:1500},tab_gone:{kind:"extension-transient",retryable:!0,delayMs:1500},target_navigated:{kind:"target-navigation",retryable:!0,delayMs:200},detached_mid_command:{kind:"non-retryable",retryable:!1,delayMs:0},cdp_timeout:{kind:"non-retryable",retryable:!1,delayMs:0}};export function classifyBrowserError(q){const x=q&&typeof q==="object"?q.code:void 0;if(typeof x==="string"&&K[x])return K[x];const z=Z(q);if(W.some((J)=>z.includes(J)))return{kind:"extension-transient",retryable:!0,delayMs:1500};if(Y.some((J)=>z.includes(J)))return{kind:"target-navigation",retryable:!0,delayMs:200};if(z.includes("-32000")&&/target|context/i.test(z))return{kind:"target-navigation",retryable:!0,delayMs:200};return{kind:"non-retryable",retryable:!1,delayMs:0}}export function isTransientBrowserError(q){return classifyBrowserError(q).retryable}export function formatBrowserConnectError(q,x){switch(q){case"daemon-not-running":return new H("Cannot connect to ppcli daemon."+(x?`
1
+ import{BrowserConnectError as x}from"../errors.js";import{DEFAULT_DAEMON_PORT as K}from"../constants.js";const Q=["Extension disconnected","Extension not connected","attach failed","Detached while handling command","Debugger is not attached to the tab","no longer exists","No tab with id","CDP connection","Daemon command failed","No window with id"],W=["Inspected target navigated or closed"];function Y(h){return h instanceof Error?h.message:String(h)}const J={attach_failed:{kind:"extension-transient",retryable:!0,delayMs:1500},tab_gone:{kind:"extension-transient",retryable:!0,delayMs:1500},target_navigated:{kind:"target-navigation",retryable:!0,delayMs:200},detached_mid_command:{kind:"non-retryable",retryable:!1,delayMs:0},cdp_timeout:{kind:"non-retryable",retryable:!1,delayMs:0}};export function classifyBrowserError(h){const q=h&&typeof h==="object"?h.code:void 0;if(typeof q==="string"&&J[q])return J[q];const v=Y(h);if(Q.some((z)=>v.includes(z)))return{kind:"extension-transient",retryable:!0,delayMs:1500};if(W.some((z)=>v.includes(z)))return{kind:"target-navigation",retryable:!0,delayMs:200};if(v.includes("-32000")&&/target|context/i.test(v))return{kind:"target-navigation",retryable:!0,delayMs:200};return{kind:"non-retryable",retryable:!1,delayMs:0}}export function isTransientBrowserError(h){return classifyBrowserError(h).retryable}export function formatBrowserConnectError(h,q){switch(h){case"daemon-not-running":return new x("Cannot connect to ppcli daemon."+(q?`
2
2
 
3
- ${x}`:""),`Run \`ppcli doctor\` to diagnose, or \`ppcli daemon restart\` to force a fresh daemon. Default port is ${Q}.`,q);case"extension-not-connected":return new H("Browser Bridge extension is not connected."+(x?`
3
+ ${q}`:""),`Run \`ppcli doctor\` to diagnose, or \`ppcli daemon restart\` to force a fresh daemon. Default port is ${K}.`,h);case"extension-not-connected":return new x("浏览器桥未连接。"+(q?`
4
4
 
5
- ${x}`:""),"Install the extension from GitHub Releases, then reload.",q);case"command-failed":return new H(`Browser command failed: ${x??"unknown error"}`,void 0,q);default:return new H(x??"Failed to connect to browser",void 0,q)}}
5
+ ${q}`:""),"请用户打开 PublishPort 客户端,在平台列表里点一次「检测登录」把托管浏览器拉起来;仍不行就重启客户端。",h);case"command-failed":return new x(`Browser command failed: ${q??"unknown error"}`,void 0,h);default:return new x(q??"Failed to connect to browser",void 0,h)}}