publishport-opencli 1.0.16 → 1.0.18

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.
Files changed (54) hide show
  1. package/cli-manifest.json +568 -37
  2. package/clis/_shared/article/publish.js +14 -7
  3. package/clis/bilibili/article.js +1 -1
  4. package/clis/bilibili/comments.js +1 -1
  5. package/clis/boss/accept.js +1 -0
  6. package/clis/boss/applied.js +1 -0
  7. package/clis/boss/auth.js +1 -13
  8. package/clis/boss/batchgreet.js +1 -1
  9. package/clis/boss/bossinfo.js +1 -0
  10. package/clis/boss/cities.js +1 -0
  11. package/clis/boss/exchange.js +1 -1
  12. package/clis/boss/greet.js +1 -1
  13. package/clis/boss/history.js +1 -0
  14. package/clis/boss/interviews.js +1 -0
  15. package/clis/boss/mark.js +1 -1
  16. package/clis/boss/me.js +26 -0
  17. package/clis/boss/recommend.js +1 -1
  18. package/clis/boss/resumedelete.js +1 -0
  19. package/clis/boss/resumelist.js +1 -0
  20. package/clis/boss/resumeupload.js +53 -0
  21. package/clis/boss/search.js +1 -1
  22. package/clis/boss/send.js +1 -1
  23. package/clis/boss/sendresume.js +1 -0
  24. package/clis/boss/unread.js +1 -0
  25. package/clis/boss/utils.js +11 -11
  26. package/clis/douyin/_shared/draft-cache.js +13 -0
  27. package/clis/douyin/drafts.js +1 -1
  28. package/clis/douyin/publish-image.js +12 -10
  29. package/clis/instagram/_shared/private-publish.js +3 -3
  30. package/clis/instagram/detail.js +3 -2
  31. package/clis/instagram/post.js +24 -24
  32. package/clis/instagram/user.js +2 -1
  33. package/clis/jike/api.js +30 -0
  34. package/clis/jike/comment.js +1 -82
  35. package/clis/jike/like.js +1 -38
  36. package/clis/jike/repost.js +20 -11
  37. package/clis/juejin/article.js +4 -4
  38. package/clis/sohu/article.js +1 -1
  39. package/clis/sohu/stats.js +2 -2
  40. package/clis/toutiao/article.js +1 -1
  41. package/clis/toutiao/articles.js +12 -3
  42. package/clis/toutiao/utils.js +2 -2
  43. package/clis/xiaohongshu/publish.js +14 -14
  44. package/clis/zhihu/answer.js +1 -15
  45. package/clis/zhihu/article.js +1 -7
  46. package/clis/zhihu/image-profile.js +7 -0
  47. package/dist/src/cli.js +3 -3
  48. package/dist/src/commands/auth.js +3 -3
  49. package/dist/src/daemon-utils.d.ts +27 -0
  50. package/dist/src/daemon-utils.js +1 -1
  51. package/dist/src/daemon.js +1 -1
  52. package/dist/src/errors.js +1 -1
  53. package/dist/src/execution.js +2 -2
  54. package/package.json +1 -1
@@ -1,10 +1,10 @@
1
- import{CommandExecutionError as _}from"@jackwener/opencli/errors";import{normalizeContent as k}from"./format.js";import{assertLiteralContent as C}from"../content-guard.js";import{inlineLocalImages as w,isLocalImagePath as I,localImageToDataUri as E}from"./images.js";import{PAGE_RUNTIME as S}from"./page-runtime.js";export function selectContent(K,Q,X="auto"){const Z=k(K,{format:X});return Q.outputFormat==="markdown"?Z.markdown:Z.html}export function originReFromHome(K){let Q="";try{Q=new URL(K).host}catch(Z){Q=""}return"^https?://([^/]*\\.)?"+Q.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(/|$)"}export async function gotoWritePage(K,Q,X){const Z=X||originReFromHome(Q);let O=null;for(let W=0;W<8;W++){await K.goto(Q);await K.wait({time:1});const $=await K.evaluate(`(() => {
1
+ import{CommandExecutionError as W}from"@jackwener/opencli/errors";import{normalizeContent as L}from"./format.js";import{assertLiteralContent as I}from"../content-guard.js";import{inlineLocalImages as y,isLocalImagePath as E,localImageToDataUri as S}from"./images.js";import{PAGE_RUNTIME as F}from"./page-runtime.js";export function selectContent(K,Q,X="auto"){const Z=L(K,{format:X});return Q.outputFormat==="markdown"?Z.markdown:Z.html}export function originReFromHome(K){let Q="";try{Q=new URL(K).host}catch(Z){Q=""}return"^https?://([^/]*\\.)?"+Q.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(/|$)"}export async function gotoWritePage(K,Q,X){const Z=X||originReFromHome(Q);let O=null;for(let _=0;_<8;_++){await K.goto(Q);await K.wait({time:1});const $=await K.evaluate(`(() => {
2
2
  var href = ''; try { href = location.href; } catch (e) {}
3
3
  var onSite = new RegExp(${JSON.stringify(Z)}).test(href);
4
4
  var cookieOk = true; try { void document.cookie; } catch (e) { cookieOk = false; }
5
5
  return { href: href, onSite: onSite, cookieOk: cookieOk };
6
- })()`);if($&&$.onSite&&$.cookieOk)return;if($)O=$;await K.wait({time:1})}const G=O&&O.href||"",q=new _(`无法在浏览器中打开已登录的写作页(未落到 ${Q}${G?`,当前停留在 ${G}`:",页面停留在空白页"})。`+"请确认登录该平台的 Chrome 已连接 ppcli 浏览器桥。");q.lastHref=G;q.landedOffSite=/^https?:/i.test(G);throw q}export function buildPublishJs(K,Q,X){return`(async () => {
7
- `+S+`
6
+ })()`);if($&&$.onSite&&$.cookieOk)return;if($)O=$;await K.wait({time:1})}const G=O&&O.href||"",q=new W(`无法在浏览器中打开已登录的写作页(未落到 ${Q}${G?`,当前停留在 ${G}`:",页面停留在空白页"})。`+"请确认登录该平台的 Chrome 已连接 ppcli 浏览器桥。");q.lastHref=G;q.landedOffSite=/^https?:/i.test(G);throw q}export function buildPublishJs(K,Q,X){return`(async () => {
7
+ `+F+`
8
8
  const I = `+JSON.stringify(K)+`;
9
9
  const __publish = (`+Q+`);
10
10
  `+(X?"const __upload = ("+X+`);
@@ -24,11 +24,18 @@ if (I.publishParams && typeof I.publishParams.cover === "string" && I.publishPar
24
24
  if (__cv.uploaded.length) I.publishParams.cover = __cv.uploaded[0].url;
25
25
  __t.uploaded = __t.uploaded.concat(__cv.uploaded);
26
26
  }
27
- const __pub = await __publish({ title: I.title, content: content, markdown: I.markdown != null ? I.markdown : (I.outputFormat === "markdown" ? __content0 : null), html: I.html != null ? I.html : (I.outputFormat === "html" ? __content0 : null), draftOnly: I.draftOnly, params: I.publishParams }, PP);
27
+ let __pub;
28
+ try {
29
+ __pub = await __publish({ title: I.title, content: content, markdown: I.markdown != null ? I.markdown : (I.outputFormat === "markdown" ? __content0 : null), html: I.html != null ? I.html : (I.outputFormat === "html" ? __content0 : null), draftOnly: I.draftOnly, params: I.publishParams }, PP);
30
+ } catch (e) {
31
+ return { ok: false, stage: "publish", unknownOutcome: true, message: String((e && e.message) || e), uploaded: __t.uploaded || [], failed: __t.failed || [] };
32
+ }
28
33
  var __upN = (__t.uploaded || []).concat((__pub && __pub.uploaded) || []);
29
34
  var __failN = (__t.failed || []).concat((__pub && __pub.failed) || []);
30
35
  if (!__pub || __pub.ok === false) {
31
- return { ok: false, stage: (__pub && __pub.stage) || "publish", status: __pub && __pub.status, message: (__pub && __pub.message) || "publish failed", detail: __pub && __pub.detail, uploaded: __upN, failed: __failN };
36
+ return { ok: false, stage: (__pub && __pub.stage) || "publish", status: __pub && __pub.status, message: (__pub && __pub.message) || "publish failed", detail: __pub && __pub.detail, unknownOutcome: !!(__pub && __pub.unknownOutcome), uploaded: __upN, failed: __failN };
32
37
  }
33
- return { ok: true, id: String(__pub.id == null ? "" : __pub.id), url: __pub.url || "", draft: !!__pub.draft, uploaded: __upN, failed: __failN };
34
- })()`}export async function publishArticle(K,Q){const{title:X,body:Z,format:O="auto",draftOnly:G=!1,profile:q,publishParams:W=null}=Q;if(!q)throw Error("publishArticle: profile is required");if(!q.home)throw Error("publishArticle: profile.home is required");if(typeof q.publish!=="function")throw Error("publishArticle: profile.publish must be a function");C(Z,{fileFlag:"--file"});const $=k(Z,{format:O}),L=q.outputFormat==="markdown"||q.needsMarkdown===!0,y=q.outputFormat==="html"||q.needsHtml===!0,z=L?await w($.markdown):{content:null,missing:[]},A=y?await w($.html):{content:null,missing:[]},J=z.content,N=A.content,R=q.outputFormat==="markdown"?J:N,T=[],U=new Set;for(const V of[...z.missing,...A.missing]){if(U.has(V.src))continue;U.add(V.src);T.push(V)}let Y=W;if(Y&&typeof Y.cover==="string"&&Y.cover){const V=Y.cover.trim();if(/["<>\s]/.test(V))throw new _("封面图路径/URL 含非法字符(引号/尖括号/空白):"+V.slice(0,120));if(I(V))try{const{dataUri:D}=await E(V);Y={...Y,cover:D}}catch(D){throw new _("封面图读取失败:"+V+"("+String(D&&D.message||D)+")")}else Y={...Y,cover:V}}await gotoWritePage(K,q.home,q.originRe);const j={title:X,content:R,markdown:q.needsMarkdown===!0&&q.outputFormat!=="markdown"?J:null,html:q.needsHtml===!0&&q.outputFormat!=="html"?N:null,draftOnly:!!G,outputFormat:q.outputFormat,preprocessConfig:q.preprocessConfig||null,imageSpec:q.image?.spec||null,imageSkip:q.image?.skip||[],imageAllowRelative:q.image?.allowRelative===!0,publishParams:Y},x=typeof q.image?.uploadFn==="function"?q.image.uploadFn.toString():null,H=buildPublishJs(j,q.publish.toString(),x),B=await K.evaluate(H);if(!B||B.ok===!1){const V=B?.stage||"publish",D=B?.status!=null?` (HTTP ${B.status})`:"",M=B?.detail!=null?`;接口原始响应:${typeof B.detail==="string"?B.detail:JSON.stringify(B.detail)}`:"";throw new _(`[${V}] ${B?.message||"发布失败"}${D}${M}`)}return{id:B.id,url:B.url,draft:B.draft,images:{uploaded:B.uploaded||[],failed:(B.failed||[]).concat(T)}}}export const __test__={selectContent,originReFromHome,buildPublishJs,publishArticle};
38
+ return { ok: true, id: String(__pub.id == null ? "" : __pub.id), url: __pub.url || "", publicUrl: __pub.publicUrl || "", draft: !!__pub.draft, uploaded: __upN, failed: __failN };
39
+ })()`}export async function publishArticle(K,Q){const{title:X,body:Z,format:O="auto",draftOnly:G=!1,profile:q,publishParams:_=null}=Q;if(!q)throw Error("publishArticle: profile is required");if(!q.home)throw Error("publishArticle: profile.home is required");if(typeof q.publish!=="function")throw Error("publishArticle: profile.publish must be a function");I(Z,{fileFlag:"--file"});const $=L(Z,{format:O}),R=q.outputFormat==="markdown"||q.needsMarkdown===!0,j=q.outputFormat==="html"||q.needsHtml===!0,z=R?await y($.markdown):{content:null,missing:[]},A=j?await y($.html):{content:null,missing:[]},J=z.content,N=A.content,H=q.outputFormat==="markdown"?J:N,T=[],U=new Set;for(const V of[...z.missing,...A.missing]){if(U.has(V.src))continue;U.add(V.src);T.push(V)}let Y=_;if(Y&&typeof Y.cover==="string"&&Y.cover){const V=Y.cover.trim();if(/["<>\s]/.test(V))throw new W("封面图路径/URL 含非法字符(引号/尖括号/空白):"+V.slice(0,120));if(E(V))try{const{dataUri:D}=await S(V);Y={...Y,cover:D}}catch(D){throw new W("封面图读取失败:"+V+"("+String(D&&D.message||D)+")")}else Y={...Y,cover:V}}await gotoWritePage(K,q.home,q.originRe);const M={title:X,content:H,markdown:q.needsMarkdown===!0&&q.outputFormat!=="markdown"?J:null,html:q.needsHtml===!0&&q.outputFormat!=="html"?N:null,draftOnly:!!G,outputFormat:q.outputFormat,preprocessConfig:q.preprocessConfig||null,imageSpec:q.image?.spec||null,imageSkip:q.image?.skip||[],imageAllowRelative:q.image?.allowRelative===!0,publishParams:Y},x=typeof q.image?.uploadFn==="function"?q.image.uploadFn.toString():null,C=buildPublishJs(M,q.publish.toString(),x),B=await K.evaluate(C);if(!B||B.ok===!1){const V=B?.stage||"publish",D=B?.status!=null?` (HTTP ${B.status})`:"",k=B?.detail!=null?`;接口原始响应:${typeof B.detail==="string"?B.detail:JSON.stringify(B.detail)}`:"";if(B?.unknownOutcome){const w=new W(`[${V}] 提交请求已发出但未收到平台响应,本次发布结果未知:${B?.message||"网络异常"}${D}${k}`+`
40
+ ⚠️ 禁止直接重试本命令——文章可能已经创建成功,重试会发出重复内容。`+`
41
+ 正确做法:先用该平台的列表/统计命令(如 stats / lists / articles)按标题回查最近内容,`+"确认未创建后再重发;已创建则直接以该条为准。");w.unknownOutcome=!0;throw w}throw new W(`[${V}] ${B?.message||"发布失败"}${D}${k}`)}return{id:B.id,url:B.url,publicUrl:B.publicUrl||"",draft:B.draft,images:{uploaded:B.uploaded||[],failed:(B.failed||[]).concat(T)}}}export const __test__={selectContent,originReFromHome,buildPublishJs,publishArticle};
@@ -1 +1 @@
1
- import{CliError as j,CommandExecutionError as P}from"@jackwener/opencli/errors";import{cli as m,Strategy as o}from"@jackwener/opencli/registry";import{publishArticle as c}from"../_shared/article/publish.js";import{readFile as g,stat as l}from"node:fs/promises";function d(K){if(!K.execute)throw new j("INVALID_INPUT","此写操作需要 --execute 才会真正提交。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function p(K){const G=typeof K.text==="string"?K.text:void 0,M=typeof K.file==="string"?K.file:void 0;if(G&&M)throw new j("INVALID_INPUT","正文和 --file 只能选一个");let z=G??"";if(M){let $;try{$=await l(M)}catch{throw new j("INVALID_INPUT",`文件不存在:${M}`)}if(!$.isFile())throw new j("INVALID_INPUT",`--file 必须是可读的文本文件:${M}`);let Z;try{Z=await g(M)}catch{throw new j("INVALID_INPUT",`文件无法读取:${M}`)}try{z=new TextDecoder("utf-8",{fatal:!0}).decode(Z)}catch{throw new j("INVALID_INPUT",`文件无法以 UTF-8 解码:${M}`)}}if(!z.trim())throw new j("INVALID_INPUT","正文不能为空");return z}function u(K,G,M,z,$={}){return[{status:"success",outcome:z,message:K,target_type:G,target:M,...$}]}export const bilibiliArticleProfile={home:"https://member.bilibili.com/platform/upload/text",originRe:"^https?://([^/]*\\.)?bilibili\\.com(/|$)",outputFormat:"html",preprocessConfig:{removeLinks:!0},image:{uploadFn:async(K,G)=>{var M=G.cookie("bili_jct");if(!M)throw Error("未找到 bili_jct CSRF token,请确认已登录 B站");var z=await fetch(K,{credentials:"omit"});if(!z.ok)throw Error("图片下载失败(HTTP "+z.status+"):"+K.slice(0,120));var $=await z.blob(),Z=new FormData;Z.append("binary",$,"image.jpg");Z.append("csrf",M);var _=await fetch("https://api.bilibili.com/x/article/creative/article/upcover",{method:"POST",credentials:"include",body:Z}),Y=await _.text(),V=null;try{V=JSON.parse(Y)}catch(h){}if(!_.ok||!V||V.code!==0||!V.data||!V.data.url)throw Error("B站图片上传失败:"+(V&&V.message?V.message:Y.slice(0,150)));return{url:V.data.url}},skip:["hdslb.com","bilibili.com","biliimg.com"]},publish:async(K,G)=>{var M=G.cookie("bili_jct");if(!M)return{ok:!1,stage:"csrf",status:0,message:"未找到 bili_jct CSRF token,请确认已登录 B站"};var z=K.params||{},$=z.tid?String(z.tid):"4",Z=z.listId!=null?String(z.listId):"0",_=z.bannerUrl?String(z.bannerUrl):"",Y=z.summary?String(z.summary):"";if(!_&&z.coverSrc)try{var V=await fetch(String(z.coverSrc),{credentials:"omit"});if(!V.ok)return{ok:!1,stage:"cover",status:V.status,message:"封面图下载失败(HTTP "+V.status+"):"+String(z.coverSrc).slice(0,120)};var h=await V.blob(),H=new FormData;H.append("binary",h,"cover.jpg");H.append("csrf",M);var T=await fetch("https://api.bilibili.com/x/article/creative/article/upcover",{method:"POST",credentials:"include",body:H}),v=await T.text(),B=null;try{B=JSON.parse(v)}catch(Q){}if(!T.ok||!B||B.code!==0||!B.data||!B.data.url)return{ok:!1,stage:"cover",status:T.status,message:"封面图上传失败:"+(B&&B.message?B.message:v.slice(0,150))};_=B.data.url}catch(Q){return{ok:!1,stage:"cover",status:0,message:"封面图上传异常:"+String(Q&&Q.message||Q)}}var F=z.tags?String(z.tags):"",U=z.original===!1?!1:!0,N=0;try{var y=document.createElement("div");y.innerHTML=K.content||"";N=(y.textContent||y.innerText||"").replace(/\s+/g,"").length}catch(Q){N=0}var W=new URLSearchParams;W.append("title",K.title);W.append("content",K.content);W.append("csrf",M);W.append("save","0");W.append("tid",$);if(z.category)W.append("category",String(z.category));if(Z)W.append("list_id",Z);if(_)W.append("banner_url",_);if(Y)W.append("summary",Y);if(F)W.append("tags",F);W.append("reprint",U?"0":"1");W.append("original",U?"1":"0");W.append("media_id","0");W.append("spoiler","0");if(N)W.append("words",String(N));var C=await fetch("https://api.bilibili.com/x/article/creative/draft/addupdate",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded",Origin:"https://member.bilibili.com",Referer:"https://member.bilibili.com/"},body:W.toString()}),R=await C.text(),L=null;try{L=JSON.parse(R)}catch(Q){}if(!C.ok||!L)return{ok:!1,stage:"draft",status:C.status,message:R.slice(0,300)};if(L.code!==0||!L.data||!L.data.aid)return{ok:!1,stage:"draft",status:C.status,message:(L.message||"保存草稿失败")+"(code="+L.code+")"};var q=String(L.data.aid);if(K.draftOnly){var k="https://member.bilibili.com/platform/upload/text/edit?aid="+q;return{ok:!0,draft:!0,id:q,url:k}}if(!z.category)return{ok:!1,stage:"category",status:0,message:"正式发布缺少必填的专栏分类 category(草稿 aid="+q+" 已存)。请用 `bilibili categories` 取合法分类 id 后传 --category。"};var J=[];try{var D=await fetch("https://api.bilibili.com/x/article/categories",{credentials:"include"}),S=await D.json();if(!S||S.code!==0||!Array.isArray(S.data))return{ok:!1,stage:"category",status:D.status,message:"获取 B站专栏分类失败,无法校验 category(草稿 aid="+q+" 已存):"+(S&&S.message||"code="+(S&&S.code))};(function Q(w){w.forEach(function(E){var I=Array.isArray(E.children)?E.children:[];J.push({id:String(E.id),name:E.name||"",leaf:I.length===0});if(I.length)Q(I)})})(S.data)}catch(Q){return{ok:!1,stage:"category",status:0,message:"获取 B站专栏分类异常,无法校验 category(草稿 aid="+q+" 已存):"+String(Q&&Q.message||Q)}}var x=J.find(function(Q){return Q.id===String(z.category)});if(!x){var f=J.filter(function(Q){return Q.leaf}).map(function(Q){return Q.id+"="+Q.name}).join(" / ");return{ok:!1,stage:"category",status:0,message:"专栏分类 id「"+z.category+"」不存在(草稿 aid="+q+" 已存)。合法叶子分类:"+(f||"(无)")}}if(!x.leaf)return{ok:!1,stage:"category",status:0,message:"专栏分类 id「"+z.category+"」("+x.name+")是父分类,请改用其下的叶子分类 id(草稿 aid="+q+" 已存)。"};var X=new URLSearchParams;X.append("aid",q);X.append("title",K.title);X.append("content",K.content);X.append("category",String(z.category));X.append("list_id",Z);X.append("tid",$);if(_)X.append("banner_url",_);if(Y)X.append("summary",Y);if(F)X.append("tags",F);X.append("reprint",U?"0":"1");X.append("original",U?"1":"0");X.append("media_id","0");X.append("spoiler","0");if(N)X.append("words",String(N));X.append("csrf",M);var O=await fetch("https://api.bilibili.com/x/article/creative/article/submit",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded",Origin:"https://member.bilibili.com",Referer:"https://member.bilibili.com/"},body:X.toString()}),b=await O.text(),A=null;try{A=JSON.parse(b)}catch(Q){}if(O.status===412)return{ok:!1,stage:"submit",status:412,message:"B站风控拦截了发表(HTTP 412),草稿已保存(aid="+q+"),可在创作中心改后重试。"};if(!O.ok||!A)return{ok:!1,stage:"submit",status:O.status,message:"正式发布失败:"+b.slice(0,300)+"(草稿 aid="+q+" 已存)"};if(A.code!==0)return{ok:!1,stage:"submit",status:O.status,message:(A.message||"正式发布失败")+"(code="+A.code+",草稿 aid="+q+" 已存)"};var n=A.data&&A.data.aid!=null?String(A.data.aid):q;return{ok:!0,draft:!1,id:n,url:"https://www.bilibili.com/read/cv"+n}}};m({site:"bilibili",name:"article",access:"write",description:"发布 B站专栏长文(opus/article)。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown,图片自动转存至 B站图床。正式发布必填 --category(合法值用 `bilibili categories` 取)。",domain:"member.bilibili.com",strategy:o.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:"专栏分类 id(正式发布必填,叶子分类)。合法值用 `bilibili categories` 列举,禁止臆造。"},{name:"tid",type:"int",default:4,help:"封面版式:4=单图 / 3=三图(按封面图数量定,默认 4)"},{name:"cover",help:"封面图源 URL(可空);会先经 upcover 上传到 B站图床再作 banner_url"},{name:"banner-url",help:"封面图的 B站 CDN url(可空,已有则直接用,跳过 upcover 上传)"},{name:"summary",help:"文章摘要/digest(可空)"},{name:"tags",help:"标签,逗号拼接的自由文本字符串(可空,非闭合词表)"},{name:"reprint",type:"boolean",help:"标记为转载(默认原创);加此参数则 original=0、reprint=1"},{name:"list-id",type:"int",default:0,help:"文集编号(默认 0=不加入文集)"},{name:"draft",type:"boolean",help:"仅保存草稿,不正式发布"},{name:"execute",type:"boolean",help:"实际提交。不加此参数时命令拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(K,G)=>{if(!K)throw new P("B站专栏发布需要浏览器会话");d(G);const M=String(G.title??"").trim();if(!M)throw new j("INVALID_INPUT","文章标题不能为空");const z=await p(G),$=Boolean(G.draft),Z=typeof G.category==="string"?G.category.trim():"";if(!$&&!Z)throw new j("INVALID_INPUT","正式发布 B站专栏必须指定 --category(专栏分类 id)。请先用 `bilibili categories` 列举合法分类 id;如只想存草稿请加 --draft。");const _={category:Z||void 0,tid:G.tid!=null?String(G.tid):"4",coverSrc:typeof G.cover==="string"?G.cover.trim():"",bannerUrl:typeof G["banner-url"]==="string"?G["banner-url"].trim():"",summary:typeof G.summary==="string"?G.summary:"",tags:typeof G.tags==="string"?G.tags.trim():"",original:!G.reprint,listId:G["list-id"]!=null?String(G["list-id"]):"0"},Y=await c(K,{title:M,body:z,format:G.html?"html":"markdown",draftOnly:$,profile:bilibiliArticleProfile,publishParams:_}),V=Y.images.uploaded.length|0,h=Y.images.failed.length|0;let H=Y.draft?"已保存 B站专栏草稿(需在创作中心手动发布)":"已正式提交 B站专栏发表(可能进入审核态,请用 `bilibili drafts` 或访问返回 URL 回查)";if(V||h)H+=`·图片:${V} 张已转存${h?`,${h} 张失败`:""}`;return u(H,"article","",Y.draft?"draft":"created",{created_target:(Y.draft?"draft:":"article:")+Y.id,created_url:Y.url})}});
1
+ import{CliError as A,CommandExecutionError as g}from"@jackwener/opencli/errors";import{cli as d,Strategy as l}from"@jackwener/opencli/registry";import{publishArticle as c}from"../_shared/article/publish.js";import{readFile as u,stat as o}from"node:fs/promises";function i(K){if(!K.execute)throw new A("INVALID_INPUT","此写操作需要 --execute 才会真正提交。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function p(K){const G=typeof K.text==="string"?K.text:void 0,M=typeof K.file==="string"?K.file:void 0;if(G&&M)throw new A("INVALID_INPUT","正文和 --file 只能选一个");let z=G??"";if(M){let Z;try{Z=await o(M)}catch{throw new A("INVALID_INPUT",`文件不存在:${M}`)}if(!Z.isFile())throw new A("INVALID_INPUT",`--file 必须是可读的文本文件:${M}`);let _;try{_=await u(M)}catch{throw new A("INVALID_INPUT",`文件无法读取:${M}`)}try{z=new TextDecoder("utf-8",{fatal:!0}).decode(_)}catch{throw new A("INVALID_INPUT",`文件无法以 UTF-8 解码:${M}`)}}if(!z.trim())throw new A("INVALID_INPUT","正文不能为空");return z}function e(K,G,M,z,Z={}){return[{status:"success",outcome:z,message:K,target_type:G,target:M,...Z}]}export const bilibiliArticleProfile={home:"https://member.bilibili.com/platform/upload/text",originRe:"^https?://([^/]*\\.)?bilibili\\.com(/|$)",outputFormat:"html",preprocessConfig:{removeLinks:!0},image:{uploadFn:async(K,G)=>{var M=G.cookie("bili_jct");if(!M)throw Error("未找到 bili_jct CSRF token,请确认已登录 B站");var z=await fetch(K,{credentials:"omit"});if(!z.ok)throw Error("图片下载失败(HTTP "+z.status+"):"+K.slice(0,120));var Z=await z.blob();function _(L,h){var B=String(h&&h.type||"").toLowerCase(),U=B==="image/png"?"png":B==="image/webp"?"webp":B==="image/gif"?"gif":B==="image/heif"||B==="image/heic"?"heif":"jpg";return L+"."+U}var $=new FormData;$.append("binary",Z,_("image",Z));$.append("csrf",M);var Y=await fetch("https://api.bilibili.com/x/article/creative/article/upcover",{method:"POST",credentials:"include",body:$}),j=await Y.text(),V=null;try{V=JSON.parse(j)}catch(L){}if(!Y.ok||!V||V.code!==0||!V.data||!V.data.url)throw Error("B站图片上传失败:"+(V&&V.message?V.message:j.slice(0,150)));return{url:V.data.url}},skip:["hdslb.com","bilibili.com","biliimg.com"]},publish:async(K,G)=>{var M=G.cookie("bili_jct");if(!M)return{ok:!1,stage:"csrf",status:0,message:"未找到 bili_jct CSRF token,请确认已登录 B站"};var z=K.params||{},Z=z.tid?String(z.tid):"4",_=z.listId!=null?String(z.listId):"0",$=z.bannerUrl?String(z.bannerUrl):"",Y=z.summary?String(z.summary):"";if(!$&&z.coverSrc)try{var j=await fetch(String(z.coverSrc),{credentials:"omit"});if(!j.ok)return{ok:!1,stage:"cover",status:j.status,message:"封面图下载失败(HTTP "+j.status+"):"+String(z.coverSrc).slice(0,120)};var V=await j.blob(),L=new FormData,h=String(V&&V.type||"").toLowerCase(),B=h==="image/png"?"png":h==="image/webp"?"webp":h==="image/gif"?"gif":h==="image/heif"||h==="image/heic"?"heif":"jpg";L.append("binary",V,"cover."+B);L.append("csrf",M);var U=await fetch("https://api.bilibili.com/x/article/creative/article/upcover",{method:"POST",credentials:"include",body:L}),D=await U.text(),S=null;try{S=JSON.parse(D)}catch(Q){}if(!U.ok||!S||S.code!==0||!S.data||!S.data.url)return{ok:!1,stage:"cover",status:U.status,message:"封面图上传失败:"+(S&&S.message?S.message:D.slice(0,150))};$=S.data.url}catch(Q){return{ok:!1,stage:"cover",status:0,message:"封面图上传异常:"+String(Q&&Q.message||Q)}}var E=z.tags?String(z.tags):"",J=z.original===!1?!1:!0,F=0;try{var T=document.createElement("div");T.innerHTML=K.content||"";F=(T.textContent||T.innerText||"").replace(/\s+/g,"").length}catch(Q){F=0}var W=new URLSearchParams;W.append("title",K.title);W.append("content",K.content);W.append("csrf",M);W.append("save","0");W.append("tid",Z);if(z.category)W.append("category",String(z.category));if(_)W.append("list_id",_);if($)W.append("banner_url",$);if(Y)W.append("summary",Y);if(E)W.append("tags",E);W.append("reprint",J?"0":"1");W.append("original",J?"1":"0");W.append("media_id","0");W.append("spoiler","0");if(F)W.append("words",String(F));var I=await fetch("https://api.bilibili.com/x/article/creative/draft/addupdate",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded",Origin:"https://member.bilibili.com",Referer:"https://member.bilibili.com/"},body:W.toString()}),n=await I.text(),H=null;try{H=JSON.parse(n)}catch(Q){}if(!I.ok||!H)return{ok:!1,stage:"draft",status:I.status,message:n.slice(0,300)};if(H.code!==0||!H.data||!H.data.aid)return{ok:!1,stage:"draft",status:I.status,message:(H.message||"保存草稿失败")+"(code="+H.code+")"};var q=String(H.data.aid);if(K.draftOnly){var P="https://member.bilibili.com/platform/upload/text/edit?aid="+q;return{ok:!0,draft:!0,id:q,url:P}}if(!z.category)return{ok:!1,stage:"category",status:0,message:"正式发布缺少必填的专栏分类 category(草稿 aid="+q+" 已存)。请用 `bilibili categories` 取合法分类 id 后传 --category。"};var y=[];try{var k=await fetch("https://api.bilibili.com/x/article/categories",{credentials:"include"}),N=await k.json();if(!N||N.code!==0||!Array.isArray(N.data))return{ok:!1,stage:"category",status:k.status,message:"获取 B站专栏分类失败,无法校验 category(草稿 aid="+q+" 已存):"+(N&&N.message||"code="+(N&&N.code))};(function Q(m){m.forEach(function(x){var v=Array.isArray(x.children)?x.children:[];y.push({id:String(x.id),name:x.name||"",leaf:v.length===0});if(v.length)Q(v)})})(N.data)}catch(Q){return{ok:!1,stage:"category",status:0,message:"获取 B站专栏分类异常,无法校验 category(草稿 aid="+q+" 已存):"+String(Q&&Q.message||Q)}}var R=y.find(function(Q){return Q.id===String(z.category)});if(!R){var b=y.filter(function(Q){return Q.leaf}).map(function(Q){return Q.id+"="+Q.name}).join(" / ");return{ok:!1,stage:"category",status:0,message:"专栏分类 id「"+z.category+"」不存在(草稿 aid="+q+" 已存)。合法叶子分类:"+(b||"(无)")}}if(!R.leaf)return{ok:!1,stage:"category",status:0,message:"专栏分类 id「"+z.category+"」("+R.name+")是父分类,请改用其下的叶子分类 id(草稿 aid="+q+" 已存)。"};var X=new URLSearchParams;X.append("aid",q);X.append("title",K.title);X.append("content",K.content);X.append("category",String(z.category));X.append("list_id",_);X.append("tid",Z);if($)X.append("banner_url",$);if(Y)X.append("summary",Y);if(E)X.append("tags",E);X.append("reprint",J?"0":"1");X.append("original",J?"1":"0");X.append("media_id","0");X.append("spoiler","0");if(F)X.append("words",String(F));X.append("csrf",M);var C=await fetch("https://api.bilibili.com/x/article/creative/article/submit",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded",Origin:"https://member.bilibili.com",Referer:"https://member.bilibili.com/"},body:X.toString()}),f=await C.text(),O=null;try{O=JSON.parse(f)}catch(Q){}if(C.status===412)return{ok:!1,stage:"submit",status:412,message:"B站风控拦截了发表(HTTP 412),草稿已保存(aid="+q+"),可在创作中心改后重试。"};if(!C.ok||!O)return{ok:!1,stage:"submit",status:C.status,message:"正式发布失败:"+f.slice(0,300)+"(草稿 aid="+q+" 已存)"};if(O.code!==0)return{ok:!1,stage:"submit",status:C.status,message:(O.message||"正式发布失败")+"(code="+O.code+",草稿 aid="+q+" 已存)"};var w=O.data&&O.data.aid!=null?String(O.data.aid):q;return{ok:!0,draft:!1,id:w,url:"https://www.bilibili.com/read/cv"+w}}};d({site:"bilibili",name:"article",access:"write",description:"发布 B站专栏长文(opus/article)。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown,图片自动转存至 B站图床。正式发布必填 --category(合法值用 `bilibili categories` 取)。",domain:"member.bilibili.com",strategy:l.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:"专栏分类 id(正式发布必填,叶子分类)。合法值用 `bilibili categories` 列举,禁止臆造。"},{name:"tid",type:"int",default:4,help:"封面版式:4=单图 / 3=三图(按封面图数量定,默认 4)"},{name:"cover",help:"封面图源 URL(可空);会先经 upcover 上传到 B站图床再作 banner_url"},{name:"banner-url",help:"封面图的 B站 CDN url(可空,已有则直接用,跳过 upcover 上传)"},{name:"summary",help:"文章摘要/digest(可空)"},{name:"tags",help:"标签,逗号拼接的自由文本字符串(可空,非闭合词表)"},{name:"reprint",type:"boolean",help:"标记为转载(默认原创);加此参数则 original=0、reprint=1"},{name:"list-id",type:"int",default:0,help:"文集编号(默认 0=不加入文集)"},{name:"draft",type:"boolean",help:"仅保存草稿,不正式发布"},{name:"execute",type:"boolean",help:"实际提交。不加此参数时命令拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(K,G)=>{if(!K)throw new g("B站专栏发布需要浏览器会话");i(G);const M=String(G.title??"").trim();if(!M)throw new A("INVALID_INPUT","文章标题不能为空");const z=await p(G),Z=Boolean(G.draft),_=typeof G.category==="string"?G.category.trim():"";if(!Z&&!_)throw new A("INVALID_INPUT","正式发布 B站专栏必须指定 --category(专栏分类 id)。请先用 `bilibili categories` 列举合法分类 id;如只想存草稿请加 --draft。");const $={category:_||void 0,tid:G.tid!=null?String(G.tid):"4",coverSrc:typeof G.cover==="string"?G.cover.trim():"",bannerUrl:typeof G["banner-url"]==="string"?G["banner-url"].trim():"",summary:typeof G.summary==="string"?G.summary:"",tags:typeof G.tags==="string"?G.tags.trim():"",original:!G.reprint,listId:G["list-id"]!=null?String(G["list-id"]):"0"},Y=await c(K,{title:M,body:z,format:G.html?"html":"markdown",draftOnly:Z,profile:bilibiliArticleProfile,publishParams:$}),j=Y.images.uploaded.length|0,V=Y.images.failed.length|0;let L=Y.draft?"已保存 B站专栏草稿(需在创作中心手动发布)":"已正式提交 B站专栏发表(可能进入审核态,请用 `bilibili drafts` 或访问返回 URL 回查)";if(j||V)L+=`·图片:${j} 张已转存${V?`,${V} 张失败`:""}`;return e(L,"article","",Y.draft?"draft":"created",{created_target:(Y.draft?"draft:":"article:")+Y.id,created_url:Y.url})}});
@@ -1 +1 @@
1
- import{cli as j,Strategy as G}from"@jackwener/opencli/registry";import{ArgumentError as Q,AuthRequiredError as L,CommandExecutionError as J,EmptyResultError as P}from"@jackwener/opencli/errors";import{apiGet as W,resolveBvid as T}from"./utils.js";const $=50;function _(z,F){return z===-101||z===-403||/登录|账号|权限|forbidden|permission|login/i.test(String(F??""))}function q(z){const F=z==null?20:z,H=Number(F);if(!Number.isInteger(H)||H<=0||H>$)throw new Q(`bilibili comments limit must be an integer between 1 and ${$}`);return H}const f={hot:3,time:2};function I(z){const F=z==null?"hot":String(z).trim().toLowerCase(),H=f[F];if(!H)throw new Q(`bilibili comments sort must be one of: ${Object.keys(f).join(", ")}`);return H}function M(z){if(z==null)return null;const F=Number(z);if(!Number.isInteger(F)||F<=0)throw new Q("bilibili comments parent must be a positive integer rpid");return F}function h(z,F){if(!z||typeof z!=="object"||Array.isArray(z)||!Object.hasOwn(z,"code"))throw new J(`Bilibili ${F} API returned a malformed payload`);if(z.code!==0){const H=z.message??"unknown error";if(_(z.code,H))throw new L("bilibili.com",`Bilibili ${F} API requires login or permission: ${H} (${z.code})`);throw new J(`Bilibili ${F} API failed: ${H} (${z.code})`)}return z.data}function O(z,F){if(!z||typeof z!=="object"||Array.isArray(z))throw new J(`Bilibili ${F} API returned malformed data`);if(!Object.hasOwn(z,"replies"))throw new J(`Bilibili ${F} API did not return replies`);if(z.replies===null)return[];if(!Array.isArray(z.replies))throw new J(`Bilibili ${F} API returned malformed replies`);return z.replies}function S(z,F){if(!z||typeof z!=="object"||Array.isArray(z))throw new J(`Bilibili comments reply ${F+1} was malformed`);const H=String(z.rpid??"").trim();if(!H)throw new J(`Bilibili comments reply ${F+1} was missing rpid`);const K=Number(z.ctime);if(!Number.isFinite(K))throw new J(`Bilibili comments reply ${F+1} was missing ctime`);return{rank:F+1,rpid:H,author:String(z.member?.uname??""),text:String(z.content?.message??"").replace(/\n/g," ").trim(),likes:z.like??0,replies:z.rcount??0,time:new Date(K*1000).toISOString().slice(0,16).replace("T"," ")}}j({site:"bilibili",name:"comments",access:"read",description:"获取 B站视频评论(官方 API;用 --parent <rpid> 读取某条评论下的「楼中楼」回复;--sort time 按最新排序,发评论后回查用)",domain:"www.bilibili.com",strategy:G.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID (e.g. BV1WtAGzYEBm)"},{name:"parent",type:"int",help:"rpid of a comment — fetch the replies under it instead of top-level comments"},{name:"sort",default:"hot",help:"Top-level order: hot (default) or time (newest first; use to verify a just-posted comment)"},{name:"limit",type:"int",default:20,help:"Number of comments (max 50)"}],columns:["rank","rpid","author","text","likes","replies","time"],func:async(z,F)=>{if(!z)throw new J("Browser session required for bilibili comments");let H;try{H=await T(F.bvid)}catch(V){throw new Q(`Cannot resolve Bilibili BV ID from input: ${String(F.bvid??"")}`,V instanceof Error?V.message:String(V))}const K=q(F.limit),N=M(F.parent),B=I(F.sort),D=await W(z,"/x/web-interface/view",{params:{bvid:H}}),U=h(D,"view")?.aid;if(!U)throw new J(`Cannot resolve aid for bvid: ${H}`);const X=N!=null?await W(z,"/x/v2/reply/reply",{params:{oid:U,type:1,root:N,pn:1,ps:K}}):await W(z,"/x/v2/reply/main",{params:{oid:U,type:1,mode:B,ps:K},signed:!0}),Y=N!=null?"reply thread":"reply main",Z=O(h(X,Y),Y);if(Z.length===0)throw new P(N!=null?`bilibili comment replies: ${N}`:`bilibili comments: ${H}`);return Z.slice(0,K).map(S)}});
1
+ import{cli as j,Strategy as G}from"@jackwener/opencli/registry";import{ArgumentError as Q,AuthRequiredError as q,CommandExecutionError as J,EmptyResultError as I}from"@jackwener/opencli/errors";import{apiGet as Z,resolveBvid as P}from"./utils.js";const U=50;function L(z,F){return z===-101||z===-403||/登录|账号|权限|forbidden|permission|login/i.test(String(F??""))}function _(z){const F=z==null?20:z,H=Number(F);if(!Number.isInteger(H)||H<=0||H>U)throw new Q(`bilibili comments limit must be an integer between 1 and ${U}`);return H}const Y={hot:3,time:2};function O(z){const F=z==null?"hot":String(z).trim().toLowerCase(),H=Y[F];if(!H)throw new Q(`bilibili comments sort must be one of: ${Object.keys(Y).join(", ")}`);return H}function T(z){if(z==null)return null;const F=Number(z);if(!Number.isInteger(F)||F<=0)throw new Q("bilibili comments parent must be a positive integer rpid");return F}function h(z,F){if(!z||typeof z!=="object"||Array.isArray(z)||!Object.hasOwn(z,"code"))throw new J(`Bilibili ${F} API returned a malformed payload`);if(z.code!==0){const H=z.message??"unknown error";if(L(z.code,H))throw new q("bilibili.com",`Bilibili ${F} API requires login or permission: ${H} (${z.code})`);throw new J(`Bilibili ${F} API failed: ${H} (${z.code})`)}return z.data}function M(z,F){if(!z||typeof z!=="object"||Array.isArray(z))throw new J(`Bilibili ${F} API returned malformed data`);if(!Object.hasOwn(z,"replies"))throw new J(`Bilibili ${F} API did not return replies`);if(z.replies===null)return[];if(!Array.isArray(z.replies))throw new J(`Bilibili ${F} API returned malformed replies`);return z.replies}function S(z,F){if(!z||typeof z!=="object"||Array.isArray(z))throw new J(`Bilibili comments reply ${F+1} was malformed`);const H=String(z.rpid??"").trim();if(!H)throw new J(`Bilibili comments reply ${F+1} was missing rpid`);const N=Number(z.ctime);if(!Number.isFinite(N))throw new J(`Bilibili comments reply ${F+1} was missing ctime`);return{rank:F+1,rpid:H,author:String(z.member?.uname??""),text:String(z.content?.message??"").replace(/\n/g," ").trim(),likes:z.like??0,replies:z.rcount??0,time:new Date(N*1000).toISOString().slice(0,16).replace("T"," ")}}j({site:"bilibili",name:"comments",access:"read",description:"获取 B站视频评论(官方 API;用 --parent <rpid> 读取某条评论下的「楼中楼」回复;--sort time 按最新排序,发评论后回查用)",domain:"www.bilibili.com",strategy:G.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID (e.g. BV1WtAGzYEBm)"},{name:"parent",type:"int",help:"rpid of a comment — fetch the replies under it instead of top-level comments"},{name:"sort",default:"hot",help:"Top-level order: hot (default) or time (newest first; use to verify a just-posted comment)"},{name:"limit",type:"int",default:20,help:"Number of comments (max 50)"}],columns:["rank","rpid","author","text","likes","replies","time"],func:async(z,F)=>{if(!z)throw new J("Browser session required for bilibili comments");let H;try{H=await P(F.bvid)}catch(W){throw new Q(`Cannot resolve Bilibili BV ID from input: ${String(F.bvid??"")}`,W instanceof Error?W.message:String(W))}const N=_(F.limit),K=T(F.parent),B=O(F.sort),D=await Z(z,"/x/web-interface/view",{params:{bvid:H}}),V=h(D,"view")?.aid;if(!V)throw new J(`Cannot resolve aid for bvid: ${H}`);const X=K!=null?await Z(z,"/x/v2/reply/reply",{params:{oid:V,type:1,root:K,pn:1,ps:N}}):await Z(z,"/x/v2/reply/main",{params:{oid:V,type:1,mode:B,ps:N},signed:!0}),$=K!=null?"reply thread":"reply main",f=M(h(X,$),$);if(f.length===0)throw new I(K!=null?`bilibili comment replies: ${K}`:`bilibili comments: ${H}`,K!=null?`该评论(rpid ${K})下当前没有可读到的回复。`+"用于去重时:读不到回复即代表本账号没在该楼回复过,可以安全发布。":`该视频(${H})当前没有可见评论(评论区为空或已关闭)。`+"用于去重时:这就是「本账号没在此视频评论过」的确证,可以安全发布。");return f.slice(0,N).map(S)}});
@@ -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}});