publishport-opencli 1.0.23 → 1.0.25

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 (47) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/cli-manifest.json +931 -11
  4. package/clis/_shared/watch.js +59 -0
  5. package/clis/band/post.js +1 -1
  6. package/clis/bilibili/download.js +1 -1
  7. package/clis/bilibili/watch.js +1 -0
  8. package/clis/boss/__fixtures__/detail.html +30 -0
  9. package/clis/boss/detail.js +12 -1
  10. package/clis/boss/search.js +1 -1
  11. package/clis/douyin/stats.js +1 -1
  12. package/clis/github/watch.js +26 -0
  13. package/clis/jira/shared.js +1 -1
  14. package/clis/ke/auth.js +5 -3
  15. package/clis/nowcoder/article.js +5 -0
  16. package/clis/nowcoder/delete.js +1 -1
  17. package/clis/nowcoder/utils.js +14 -13
  18. package/clis/nowcoder/watch.js +1 -0
  19. package/clis/quark/save.js +1 -1
  20. package/clis/reddit/watch.js +1 -0
  21. package/clis/spotify/spotify.js +1 -1
  22. package/clis/twitter/watch.js +1 -0
  23. package/clis/v2ex/watch.js +22 -0
  24. package/clis/weibo/watch.js +1 -0
  25. package/clis/xianyu/watch.js +1 -0
  26. package/clis/xiaohongshu/ask.js +41 -7
  27. package/clis/xiaohongshu/comments.js +3 -3
  28. package/clis/xiaohongshu/download.js +2 -2
  29. package/clis/xiaohongshu/note.js +20 -5
  30. package/clis/xiaohongshu/risk-control.js +1 -0
  31. package/clis/xiaohongshu/watch.js +20 -0
  32. package/clis/zhihu/watch.js +19 -0
  33. package/dist/src/browser/cdp.d.ts +2 -0
  34. package/dist/src/browser/cdp.js +1 -1
  35. package/dist/src/build-manifest.js +1 -1
  36. package/dist/src/cli.js +15 -15
  37. package/dist/src/discovery.js +2 -2
  38. package/dist/src/doctor.d.ts +2 -1
  39. package/dist/src/doctor.js +7 -7
  40. package/dist/src/errors.js +2 -2
  41. package/dist/src/execution.js +2 -2
  42. package/dist/src/pipeline/steps/download.js +1 -1
  43. package/dist/src/pipeline/template.js +1 -1
  44. package/dist/src/plugin.js +1 -1
  45. package/dist/src/registry.d.ts +9 -0
  46. package/dist/src/registry.js +1 -1
  47. package/package.json +4 -4
@@ -0,0 +1,59 @@
1
+ import{existsSync as Q,mkdirSync as X,readFileSync as Y,writeFileSync as Z}from"node:fs";import{homedir as ee}from"node:os";import{dirname as te,join as M}from"node:path";import{cli as re,Strategy as ne}from"@jackwener/opencli/registry";import{AuthRequiredError as q,CommandExecutionError as ae}from"@jackwener/opencli/errors";const O=1000,F=300,J=60,g=30,B=2000;function ie(){return M(ee(),".ppcli","watch")}function G(e,t){return M(ie(),`${e}.${t}.seen.json`)}function oe(e,t){const r=G(e,t);try{if(!Q(r))return{ids:[],set:new Set,firstRun:!0};const i=JSON.parse(Y(r,"utf-8")),o=Array.isArray(i?.ids)?i.ids.map(String):[];return{ids:o,set:new Set(o),firstRun:!1}}catch{return{ids:[],set:new Set,firstRun:!0}}}function w(e,t,r){const i=G(e,t);try{X(te(i),{recursive:!0});Z(i,JSON.stringify({ids:r.ids.slice(-B)}),"utf-8")}catch{}}function se(e,t){if(e.set.has(t))return!1;e.set.add(t);e.ids.push(t);while(e.ids.length>B){const r=e.ids.shift();e.set.delete(r)}return!0}function T(e){process.stdout.write(`${JSON.stringify(e)}
2
+ `)}function u(e,t,r={}){T({type:"status",site:e,state:t,ts:Date.now(),...r})}function ce(e,t){if(!t||typeof t!=="object")return null;const r=t.id==null?"":String(t.id);if(!r)return null;const i=t.sender&&typeof t.sender==="object"?{id:t.sender.id==null?"":String(t.sender.id),name:String(t.sender.name??"")}:{id:"",name:String(t.sender??"")};return{type:"event",site:e,event:String(t.event??"message"),id:r,ts:Number.isFinite(t.ts)?Number(t.ts):Date.now(),conversation_id:t.conversation_id==null?"":String(t.conversation_id),sender:i,text:String(t.text??""),url:t.url==null?"":String(t.url),raw:t.raw??null}}function j(e){if(!e)return[];return Array.isArray(e)?e.filter(Boolean):[e]}function z(e){try{return new URL(e).origin}catch{return""}}function le(e){return`(function(){
3
+ if (window.__pp_stream_hooked) return;
4
+ window.__pp_stream_hooked = true;
5
+ var PATTERNS = ${JSON.stringify(String(e||"").split("|").map((r)=>r.trim()).filter(Boolean))};
6
+ var LIMIT = 500;
7
+ var CAP = 262144;
8
+ window.__pp_stream = [];
9
+ function want(u){ if(!PATTERNS.length) return true; for(var i=0;i<PATTERNS.length;i++){ if(String(u).indexOf(PATTERNS[i])>=0) return true; } return false; }
10
+ function push(ch, dir, url, data, name){
11
+ try {
12
+ if (!want(url)) return;
13
+ if (window.__pp_stream.length >= LIMIT) window.__pp_stream.shift();
14
+ var body = typeof data === 'string' ? data : null;
15
+ // 二进制帧(Blob / ArrayBuffer)在页面里同步转不了 base64,如实标记后交给上层,
16
+ // 不猜内容——大多数平台的业务帧是文本,二进制帧靠 CDP 那条通道拿。
17
+ var binary = body === null;
18
+ if (body && body.length > CAP) body = body.slice(0, CAP);
19
+ window.__pp_stream.push({ channel: ch, direction: dir, url: String(url||''), data: body, binary: binary, eventName: name || '', ts: Date.now() });
20
+ } catch (e) {}
21
+ }
22
+ var NativeWS = window.WebSocket;
23
+ if (NativeWS) {
24
+ var WrappedWS = function(url, protocols){
25
+ var ws = protocols === undefined ? new NativeWS(url) : new NativeWS(url, protocols);
26
+ try {
27
+ ws.addEventListener('message', function(ev){ push('websocket','received',url,ev.data); });
28
+ var send = ws.send;
29
+ ws.send = function(d){ push('websocket','sent',url,d); return send.apply(this, arguments); };
30
+ } catch (e) {}
31
+ return ws;
32
+ };
33
+ WrappedWS.prototype = NativeWS.prototype;
34
+ WrappedWS.CONNECTING = NativeWS.CONNECTING; WrappedWS.OPEN = NativeWS.OPEN;
35
+ WrappedWS.CLOSING = NativeWS.CLOSING; WrappedWS.CLOSED = NativeWS.CLOSED;
36
+ window.WebSocket = WrappedWS;
37
+ }
38
+ var NativeES = window.EventSource;
39
+ if (NativeES) {
40
+ var WrappedES = function(url, cfg){
41
+ var es = cfg === undefined ? new NativeES(url) : new NativeES(url, cfg);
42
+ try {
43
+ es.addEventListener('message', function(ev){ push('eventsource','received',url,ev.data,'message'); });
44
+ // 具名 SSE 事件不走 onmessage:劫持 addEventListener,把业务方注册的每个
45
+ // 事件名都顺带旁听一份。
46
+ var addL = es.addEventListener.bind(es);
47
+ es.addEventListener = function(name, fn, opts){
48
+ if (name && name !== 'message' && name !== 'open' && name !== 'error') {
49
+ try { addL(name, function(ev){ push('eventsource','received',url,ev.data,name); }); } catch(e){}
50
+ }
51
+ return addL(name, fn, opts);
52
+ };
53
+ } catch (e) {}
54
+ return es;
55
+ };
56
+ WrappedES.prototype = NativeES.prototype;
57
+ window.EventSource = WrappedES;
58
+ }
59
+ })()`}async function ue(e){let t;try{t=await e.evaluate("(function(){ var o = window.__pp_stream || []; window.__pp_stream = []; return JSON.stringify(o); })()")}catch{return[]}let r;try{r=JSON.parse(typeof t==="string"?t:"[]")}catch{return[]}if(!Array.isArray(r))return[];return r.map((i)=>({kind:"page-hook",channel:i.channel,direction:i.direction,url:i.url,method:i.channel==="websocket"?"WS":"SSE",eventName:i.eventName||void 0,responseStatus:200,responseContentType:i.binary?"application/octet-stream":"text/plain",responsePreview:i.binary?"":String(i.data??""),timestamp:i.ts||Date.now()}))}export function registerWatch(e){if(!e?.site||!e?.domain||!e?.watchUrl||typeof e.parse!=="function")throw Error("registerWatch 需要 site、domain、watchUrl 和 parse");const t=e.name??"watch",r=e.site,i=z(e.watchUrl);re({site:r,name:t,access:"read",description:e.description??`长驻监听${r}的新消息,旁听页面自身的实时通道并按 NDJSON 逐条输出(不对平台发轮询请求)`,domain:e.domain,strategy:ne.COOKIE,browser:!0,navigateBefore:!1,sessionKey:"watch",siteSession:"persistent",defaultWindowMode:"background",args:[{name:"interval",type:"int",required:!1,default:O,help:`本机取帧间隔毫秒(默认 ${O})。纯本机 IPC,不发平台请求`},{name:"reconcile-interval",type:"int",required:!1,default:0,help:`对账间隔秒(0 = 用该平台的默认值:可旁听实时通道的平台 ${F} 秒兜底,只能靠拉取的平台 ${J} 秒一轮);-1 = 彻底关闭对账`},{name:"heartbeat",type:"int",required:!1,default:g,help:`心跳间隔秒(默认 ${g}),无事件时输出一行 status 表示还活着`},{name:"events",type:"str",required:!1,help:"只输出这些事件类型,逗号分隔(如 dm,comment);默认全部"},{name:"once",type:"bool",required:!1,default:!1,help:"只跑一轮对账后退出,不长驻。用于验证解析器和补历史"},{name:"replay-seen",type:"bool",required:!1,default:!1,help:"忽略去重表,把解析到的事件全部重新输出(调试用,会导致重复触发)"},{name:"backfill",type:"bool",required:!1,default:!1,help:"首次运行时把已存在的历史消息也当成新事件输出(默认只静默登记,不触发)"},{name:"dump-frames",type:"bool",required:!1,default:!1,help:"侦察模式:把收到的每一条原始帧(url / 通道 / 内容片段)也打出来,用来给新平台摸清事件通道"},{name:"include-own",type:"bool",required:!1,default:!1,help:"连自己发出去的消息也当成事件(默认不算——否则自动回复会追着自己的回复自我触发)。调试解析用"}],columns:["event","id","sender","text","ts"],func:async(o,s)=>{const H=Math.max(200,Number(s.interval??O)),A=e.mode==="reconcile"?"reconcile":"stream",y=Number(s["reconcile-interval"]??0),v=y<0?0:y>0?y:A==="reconcile"?J:F,D=Math.max(0,Number(s.heartbeat??g)),I=s.once===!0||s.once==="true",L=s["replay-seen"]===!0||s["replay-seen"]==="true",K=s.backfill===!0||s.backfill==="true",V=s["dump-frames"]===!0||s["dump-frames"]==="true",P=String(s.events??"").split(",").map((c)=>c.trim()).filter(Boolean);if(typeof o.startNetworkCapture!=="function"||typeof o.readNetworkCapture!=="function")throw new ae("当前浏览器通道不支持网络捕获,watch 无法工作(需要 CDP 或扩展路径)");const f=oe(r,t),N={site:r,page:o,state:{},includeOwn:s["include-own"]===!0||s["include-own"]==="true"};let p=0,R=0,m=!1;const E=[];let b=f.firstRun&&!K&&!L;const k=(c)=>{const a=ce(r,c);if(!a)return!1;if(P.length&&!P.includes(a.event))return!1;if(!L&&!se(f,`${a.event}:${a.id}`))return!1;if(b){R++;return!1}if(!I)T(a);E.push(a);p++;return!0},S=async(c)=>{if(typeof e.reconcile!=="function")return;try{const a=j(await e.reconcile(o,N));let n=0;for(const l of a)if(k(l))n++;u(r,"reconciled",{reason:c,scanned:a.length,new:n})}catch(a){if(a instanceof q)throw a;u(r,"reconcile_failed",{reason:c,error:a?.message??String(a)})}},_=async(c)=>{let a=!1;try{a=await o.startNetworkCapture(e.pattern??"")!==!1}catch{a=!1}const n=le(e.pattern??"");m=!1;let l=!1;if(typeof o.cdp==="function")try{await o.cdp("Page.enable",{});await o.cdp("Page.addScriptToEvaluateOnNewDocument",{source:n});l=!0;m=!0}catch{}await o.goto(e.watchUrl,{waitUntil:l?"load":"none"});if(!l)try{await o.evaluate(n);m=!0;u(r,"page_hook_late",{note:"浏览器扩展版本较旧,旁听脚本只能在导航后注入;页面在此之前已建立的连接要等它重连才能被看到"})}catch(d){u(r,"page_hook_unavailable",{error:d?.message??String(d)})}if(!a){await o.startNetworkCapture(e.pattern??"");await o.goto(e.watchUrl,{waitUntil:"load"})}if(typeof e.onAttach==="function")await e.onAttach(o,N);u(r,"watching",{reason:c,mode:A,url:e.watchUrl,pattern:e.pattern??"",reconcile_seconds:v})};await _("start");await S("startup");if(b){w(r,t,f);u(r,"seeded",{seeded:R,note:"首次运行:已把当前收件箱登记为「已见过」,不触发任何事件。从现在起只有新消息才会上报(要补历史请加 --backfill)"});b=!1}if(I){w(r,t,f);return E}let W=!1;const h=()=>{W=!0};process.on("SIGINT",h);process.on("SIGTERM",h);let U=Date.now(),C=Date.now(),x=p;try{while(!W){await new Promise((n)=>setTimeout(n,H));if(W)break;let c=[];try{c=await o.readNetworkCapture()??[];if(m)c=c.concat(await ue(o))}catch(n){u(r,"reattaching",{error:n?.message??String(n)});try{await _("capture_lost");await S("reattach")}catch(l){if(l instanceof q)throw l;u(r,"reattach_failed",{error:l?.message??String(l)})}continue}for(const n of c){if(V)T({type:"frame",site:r,via:n?.kind==="page-hook"?"page-hook":"cdp",channel:n?.channel??"http",direction:n?.direction??"",method:n?.method??"",url:String(n?.url??"").slice(0,200),size:n?.responseBodyFullSize??String(n?.responsePreview??"").length,preview:String(n?.responsePreview??"").slice(0,600)});if(n?.direction==="sent")continue;let l;try{l=await e.parse(n,N)}catch(d){u(r,"parse_failed",{url:n?.url??"",error:d?.message??String(d)});continue}for(const d of j(l))k(d)}if(p!==x){w(r,t,f);x=p;C=Date.now()}if(i&&typeof o.getCurrentUrl==="function")try{const n=await o.getCurrentUrl();if(n&&z(n)!==i){u(r,"page_drifted",{url:n});await _("drift");await S("drift")}}catch{}const a=Date.now();if(v&&a-U>=v*1000){U=a;await S("interval")}if(D&&a-C>=D*1000){C=a;u(r,"alive",{emitted:p})}}}finally{process.off("SIGINT",h);process.off("SIGTERM",h);w(r,t,f);u(r,"stopped",{emitted:p})}return E}})}export function frameJson(e){const t=e?.responsePreview;if(typeof t!=="string"||!t)return null;if(t.startsWith("base64:"))return null;try{return JSON.parse(t)}catch{return null}}
package/clis/band/post.js CHANGED
@@ -1,4 +1,4 @@
1
- import{AuthRequiredError as g,EmptyResultError as C}from"@jackwener/opencli/errors";import{formatCookieHeader as w}from"@jackwener/opencli/download";import{downloadMedia as h}from"@jackwener/opencli/download/media-download";import{cli as x,Strategy as L}from"@jackwener/opencli/registry";x({site:"band",name:"post",access:"read",description:"Export full content of a post including comments",domain:"www.band.us",strategy:L.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"band_no",positional:!0,required:!0,type:"int",help:"Band number"},{name:"post_no",positional:!0,required:!0,type:"int",help:"Post number"},{name:"output",type:"str",default:"",help:"Directory to save attached photos"},{name:"comments",type:"bool",default:!0,help:"Include comments (default: true)"}],columns:["type","author","date","text"],func:async(n,r)=>{const f=Number(r.band_no),y=Number(r.post_no),s=r.output,b=r.comments;await n.goto(`https://www.band.us/band/${f}/post/${y}`);if(!(await n.getCookies({domain:"band.us"})).some((t)=>t.name==="band_session"))throw new g("band.us","Not logged in to Band");const o=await n.evaluate(`
1
+ import{AuthRequiredError as g,EmptyResultError as C}from"@jackwener/opencli/errors";import{formatCookieHeader as w}from"@jackwener/opencli/download";import{downloadMedia as h}from"@jackwener/opencli/download/media-download";import{cli as x,Strategy as L}from"@jackwener/opencli/registry";x({site:"band",name:"post",access:"read",description:"Export full content of a post including comments",domain:"www.band.us",strategy:L.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"band_no",positional:!0,required:!0,type:"int",help:"Band number"},{name:"post_no",positional:!0,required:!0,type:"int",help:"Post number"},{name:"output",type:"str",default:"",help:"Directory to save attached photos"},{name:"comments",type:"bool",default:!0,help:"Include comments (default: true)"}],columns:["type","author","date","text"],func:async(n,r)=>{const f=Number(r.band_no),y=Number(r.post_no),{output:s,comments:b}=r;await n.goto(`https://www.band.us/band/${f}/post/${y}`);if(!(await n.getCookies({domain:"band.us"})).some((t)=>t.name==="band_session"))throw new g("band.us","Not logged in to Band");const o=await n.evaluate(`
2
2
  (async () => {
3
3
  const withComments = ${b};
4
4
  const sleep = ms => new Promise(r => setTimeout(r, ms));
@@ -1,4 +1,4 @@
1
- import{cli as v,Strategy as x}from"@jackwener/opencli/registry";import{CliError as g,CommandExecutionError as n,EXIT_CODES as P}from"@jackwener/opencli/errors";import{checkYtdlp as _,sanitizeFilename as C}from"@jackwener/opencli/download";import{downloadMedia as E}from"@jackwener/opencli/download/media-download";import{apiGet as d,resolveBvid as O,parsePageArg as A,selectVideoPart as q}from"./utils.js";const T={vip:"大会员专享/付费 OGV",ugc_pay:"UGC 单点付费",upower:"充电专属"};function l(e){return e&&typeof e==="object"&&!Array.isArray(e)}async function B(e,r){let i;try{const t=await d(e,"/x/web-interface/view",{params:{bvid:r}});if(!l(t)||!Object.hasOwn(t,"code"))throw new n("Bilibili view API returned a malformed payload during paid-content pre-check");if(t.code!==0)return;if(!l(t.data)||!l(t.data.rights))throw new n("Bilibili view API returned malformed paid-content metadata");i=t.data}catch(t){if(t instanceof n)throw t;return}const a=i.rights,o=a.pay?"vip":a.ugc_pay||a.arc_pay?"ugc_pay":i.is_upower_exclusive?"upower":"";if(!o)return;if(o==="vip")try{const t=await d(e,"/x/web-interface/nav");if(t.code===0&&Number(t.data?.vipStatus)===1)return}catch{}throw new g("PAID_CONTENT",`该视频为付费内容(${T[o]}),当前账号无观看权益,无法获取完整视频流`,"若已购买/已充电/已开通会员,加 --force 跳过本检查直接下载",P.NOPERM)}async function S(e,r,i){let a;try{a=await d(e,"/x/web-interface/view",{params:{bvid:r}})}catch(o){throw new n(`获取视频分P信息失败: ${o?.message||o}`)}if(!l(a)||a.code!==0)throw new n(`获取视频分P信息失败: ${a?.message??"unknown"} (${a?.code??"malformed"})`);return q(a.data,i)}v({site:"bilibili",name:"download",access:"read",description:"下载B站视频(需要 yt-dlp)",domain:"www.bilibili.com",strategy:x.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID (e.g., BV1xxx)"},{name:"output",default:"./bilibili-downloads",help:"Output directory"},{name:"quality",default:"best",help:"Video quality (best, 1080p, 720p, 480p)"},{name:"force",type:"boolean",default:!1,help:"跳过付费内容预检直接下载(已购买/已充电/已开通会员时用)"},{name:"page",required:!1,help:"分P 选集序号(从 1 开始)。多 P 视频下载该集;缺省下载默认 P1"}],columns:["bvid","title","status","size"],func:async(e,r)=>{const i=await O(r.bvid),a=r.output,o=r.quality,t=A(r.page),c=t!=null?await S(e,i,t):null,u=t!=null?`https://www.bilibili.com/video/${i}?p=${t}`:`https://www.bilibili.com/video/${i}`;if(!_())return[{bvid:i,title:"-",status:"failed",size:"yt-dlp not installed. Run: pip install yt-dlp"}];await e.goto(u);await e.wait(3);if(!r.force)await B(e,i);const w=await e.evaluate(`
1
+ import{cli as v,Strategy as x}from"@jackwener/opencli/registry";import{CliError as g,CommandExecutionError as n,EXIT_CODES as P}from"@jackwener/opencli/errors";import{checkYtdlp as _,sanitizeFilename as C}from"@jackwener/opencli/download";import{downloadMedia as E}from"@jackwener/opencli/download/media-download";import{apiGet as d,resolveBvid as O,parsePageArg as A,selectVideoPart as q}from"./utils.js";const T={vip:"大会员专享/付费 OGV",ugc_pay:"UGC 单点付费",upower:"充电专属"};function l(e){return e&&typeof e==="object"&&!Array.isArray(e)}async function B(e,r){let i;try{const t=await d(e,"/x/web-interface/view",{params:{bvid:r}});if(!l(t)||!Object.hasOwn(t,"code"))throw new n("Bilibili view API returned a malformed payload during paid-content pre-check");if(t.code!==0)return;if(!l(t.data)||!l(t.data.rights))throw new n("Bilibili view API returned malformed paid-content metadata");i=t.data}catch(t){if(t instanceof n)throw t;return}const a=i.rights,o=a.pay?"vip":a.ugc_pay||a.arc_pay?"ugc_pay":i.is_upower_exclusive?"upower":"";if(!o)return;if(o==="vip")try{const t=await d(e,"/x/web-interface/nav");if(t.code===0&&Number(t.data?.vipStatus)===1)return}catch{}throw new g("PAID_CONTENT",`该视频为付费内容(${T[o]}),当前账号无观看权益,无法获取完整视频流`,"若已购买/已充电/已开通会员,加 --force 跳过本检查直接下载",P.NOPERM)}async function S(e,r,i){let a;try{a=await d(e,"/x/web-interface/view",{params:{bvid:r}})}catch(o){throw new n(`获取视频分P信息失败: ${o?.message||o}`)}if(!l(a)||a.code!==0)throw new n(`获取视频分P信息失败: ${a?.message??"unknown"} (${a?.code??"malformed"})`);return q(a.data,i)}v({site:"bilibili",name:"download",access:"read",description:"下载B站视频(需要 yt-dlp)",domain:"www.bilibili.com",strategy:x.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID (e.g., BV1xxx)"},{name:"output",default:"./bilibili-downloads",help:"Output directory"},{name:"quality",default:"best",help:"Video quality (best, 1080p, 720p, 480p)"},{name:"force",type:"boolean",default:!1,help:"跳过付费内容预检直接下载(已购买/已充电/已开通会员时用)"},{name:"page",required:!1,help:"分P 选集序号(从 1 开始)。多 P 视频下载该集;缺省下载默认 P1"}],columns:["bvid","title","status","size"],func:async(e,r)=>{const i=await O(r.bvid),{output:a,quality:o}=r,t=A(r.page),c=t!=null?await S(e,i,t):null,u=t!=null?`https://www.bilibili.com/video/${i}?p=${t}`:`https://www.bilibili.com/video/${i}`;if(!_())return[{bvid:i,title:"-",status:"failed",size:"yt-dlp not installed. Run: pip install yt-dlp"}];await e.goto(u);await e.wait(3);if(!r.force)await B(e,i);const w=await e.evaluate(`
2
2
  (() => {
3
3
  const title = document.querySelector('h1.video-title, .video-title')?.textContent?.trim() || 'video';
4
4
  const author = document.querySelector('.up-name, .username')?.textContent?.trim() || 'unknown';
@@ -0,0 +1 @@
1
+ import{registerWatch as d,frameJson as p}from"../_shared/watch.js";function parseMsgContent(e){const s=String(e??"");try{const t=JSON.parse(s);if(t&&typeof t==="object"){if(typeof t.content==="string")return t.content;if(typeof t.title==="string"||typeof t.text==="string")return[t.title,t.text].filter(Boolean).join(":");return s}}catch{}return s}const f="session_type=1&group_fold=1&unfollow_fold=0&sort_rule=2&build=0&mobi_app=web",m=`https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions?${f}`;function sessionsToEvents(e,s){const t=e?.data?.session_list;if(!Array.isArray(t))return[];const i=String(s?.state?.selfUid??""),a=[];for(const o of t){const n=o?.last_msg;if(!n||!n.msg_key)continue;const c=String(n.sender_uid??"");if(!s?.includeOwn&&i&&c===i)continue;const r=String(o.talker_id??""),l=Number(n.timestamp??0);a.push({event:"dm",id:String(n.msg_key),ts:l?l*1000:Date.now(),conversation_id:r,sender:{id:c,name:""},text:parseMsgContent(n.content),url:r?`https://message.bilibili.com/#/whisper/mid${r}`:"",raw:{msg_type:n.msg_type,unread_count:o.unread_count,msg_seqno:n.msg_seqno}})}return a}d({site:"bilibili",name:"watch",domain:"www.bilibili.com",description:"B站私信监听:长驻旁听消息页自身的推送通道,收到新私信按 NDJSON 逐条输出(不对 B 站发轮询请求)",watchUrl:"https://message.bilibili.com/#/whisper",pattern:"session_svr/v1/session_svr/get_sessions",onAttach:async(e,s)=>{try{const i=(await e.fetchJson("https://api.bilibili.com/x/web-interface/nav"))?.data?.mid;if(i)s.state.selfUid=String(i)}catch{}},parse:(e,s)=>{const t=p(e);if(!t)return null;return sessionsToEvents(t,s)},reconcile:async(e,s)=>{const t=await e.fetchJson(m);return sessionsToEvents(t,s)}});export{sessionsToEvents,parseMsgContent};
@@ -0,0 +1,30 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head><title>供应链实习生_OpenCLI招聘-BOSS直聘</title></head>
4
+ <body>
5
+ <section class="job-primary">
6
+ <div class="name"><h1 title="供应链实习生">供应链实习生</h1><span class="salary">180-230元/天</span></div>
7
+ <p><a class="text-desc text-city" href="/shanghai/">上海</a><span class="text-desc text-experiece">5天/周 6个月</span><span class="text-desc text-degree">本科</span></p>
8
+ <div class="tag-all job-tags"><span>五险一金</span><span>餐补</span></div>
9
+ </section>
10
+ <main class="job-detail">
11
+ <section class="job-detail-section">
12
+ <ul class="job-keyword-list"><li>供应链/物流类专业</li><li>采购/供应商管理经验</li></ul>
13
+ <div class="job-sec-text">负责订单、仓储与采购协同,使用 Excel 制作供应链报表。</div>
14
+ </section>
15
+ <div class="job-boss-info">
16
+ <h2 class="name">张女士<span class="boss-active-time">本周活跃</span></h2>
17
+ <div class="boss-info-attr">OpenCLI<em class="vdot">·</em>供应链负责人</div>
18
+ </div>
19
+ <div class="detail-section-item company-address"><h3>工作地址</h3><div class="location-address">上海示例路 8 号</div><p>点击查看地图</p></div>
20
+ </main>
21
+ <aside class="job-sider">
22
+ <p class="title">公司基本信息</p>
23
+ <a href="/gongsi/example.html" title="OpenCLI">OpenCLI</a>
24
+ <p>D轮及以上</p>
25
+ <p>1000-9999人</p>
26
+ <p><a href="/i101001/">互联网</a></p>
27
+ <a class="look-all">查看全部职位</a>
28
+ </aside>
29
+ </body>
30
+ </html>
@@ -1 +1,12 @@
1
- import{cli as c,Strategy as d}from"@jackwener/opencli/registry";import{requirePage as l,navigateTo as m,bossFetch as p,verbose as b}from"./utils.js";c({site:"boss",name:"detail",access:"read",description:"BOSS直聘查看职位详情",domain:"www.zhipin.com",strategy:d.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"security-id",positional:!0,required:!0,help:"Security ID from search results (securityId field)"}],columns:["name","salary","experience","degree","city","district","description","skills","welfare","boss_name","boss_title","active_time","company","industry","scale","stage","address","url"],func:async(a,r)=>{l(a);const o=r["security-id"];b("Fetching job detail...");await m(a,"https://www.zhipin.com/web/geek/job");const n=`https://www.zhipin.com/wapi/zpgeek/job/detail.json?securityId=${encodeURIComponent(o)}`,i=(await p(a,n)).zpData||{},e=i.jobInfo||{},s=i.bossInfo||{},t=i.brandComInfo||{};if(!e.jobName)throw Error("该职位信息不存在或已下架");return[{name:e.jobName||"",salary:e.salaryDesc||"",experience:e.experienceName||"",degree:e.degreeName||"",city:e.locationName||"",district:[e.areaDistrict,e.businessDistrict].filter(Boolean).join("·"),description:e.postDescription||"",skills:(e.showSkills||[]).join(", "),welfare:(t.labels||[]).join(", "),boss_name:s.name||"",boss_title:s.title||"",active_time:s.activeTimeDesc||"",company:t.brandName||s.brandName||"",industry:t.industryName||"",scale:t.scaleName||"",stage:t.stageName||"",address:e.address||"",url:e.encryptId?"https://www.zhipin.com/job_detail/"+e.encryptId+".html":""}]}});
1
+ import{cli as A,Strategy as _}from"@jackwener/opencli/registry";import{ArgumentError as B,AuthRequiredError as q,CommandExecutionError as k}from"@jackwener/opencli/errors";import{readRequiredString as N,requirePage as C,navigateTo as j,verbose as v}from"./utils.js";const O="www.zhipin.com";function s(e){return String(e||"").replace(/\u00a0/g," ").replace(/[ \t]+\n/g,`
2
+ `).replace(/\n[ \t]+/g,`
3
+ `).replace(/\n{3,}/g,`
4
+
5
+ `).trim()}function g(e,r){if(!e||typeof e!=="object"||!s(e.jobName))return null;const o=(n)=>[...new Set(n.map(s).filter(Boolean))],i=(n)=>s(n)||null;return{name:s(e.jobName),salary:s(e.salaryText),experience:s(e.experienceText),degree:s(e.degreeText),city:i(e.cityText),address:i(e.addressText),description:s(e.descriptionText),skills:o(Array.isArray(e.skillTexts)?e.skillTexts:[]).join(", "),welfare:o(Array.isArray(e.welfareTexts)?e.welfareTexts:[]).join(", "),boss_name:i(e.recruiterName),boss_title:i(e.recruiterTitle),active_time:i(e.recruiterActiveTime),company:s(e.companyName),industry:i(e.industryText),scale:i(e.scaleText),stage:i(e.stageText),url:`https://www.zhipin.com/job_detail/${r}.html`}}export function extractRenderedJob(){const e=(t,d=document)=>{const a=d.querySelector(t);return a?(a.innerText||a.textContent||"").trim():""},r=(t,d=document)=>Array.from(d.querySelectorAll(t)).map((a)=>(a.innerText||a.textContent||"").trim()).filter(Boolean),o=(t)=>{for(const d of t){const a=e(d);if(a)return a}return""},i=o([".job-primary .name h1",".job-primary h1","h1[title]"]),n=r(".job-primary .job-limit a, .job-primary .job-limit span, .job-primary p a, .job-primary p span"),c=Array.from(document.querySelectorAll('.job-sider a[href^="/gongsi/"], .detail-op a[href*="/gongsi/"]')).map((t)=>(t.getAttribute("title")||t.innerText||t.textContent||"").trim()).filter((t)=>t&&!/查看.*职位/.test(t)),T=document.title.match(/_([^_]+)招聘-BOSS直聘$/)?.[1]||"",u=c[0]||T,b=r('.job-sider p, .job-sider a[href^="/i"]').filter((t)=>t!=="公司基本信息"&&t!=="查看全部职位"),l=document.querySelector(".job-boss-info"),m=l?.querySelector("h2"),h=m?Array.from(m.childNodes).filter((t)=>t.nodeType===3).map((t)=>t.textContent.trim()).filter(Boolean).join(" "):"",y=l?(l.innerText||l.textContent||"").split(/\n|·/).map((t)=>t.trim()).filter(Boolean):[],f=m?e("span",m):y.find((t)=>/活跃|在线/.test(t))||"",x=h||y[0]||"",w=e(".boss-info-attr",l).split("·").map((t)=>t.trim()).filter(Boolean).findLast((t)=>t!==u)||y.findLast((t)=>t!==x&&t!==f&&t!==u)||"",p=document.querySelector(".company-address"),S=e(".location-address",p)||(p?(p.innerText||p.textContent||"").split(`
6
+ `).map((t)=>t.trim()).filter((t)=>t&&t!=="工作地址"&&t!=="点击查看地图")[0]||"":"");return{jobName:i,salaryText:o([".job-primary .salary",".job-banner .salary",".job-primary .name + span"]),cityText:e(".job-primary .text-city")||n[0]||"",experienceText:e(".job-primary .text-experiece")||n[1]||"",degreeText:e(".job-primary .text-degree")||n[2]||"",descriptionText:o([".job-detail .job-detail-section .job-sec-text:not(.fold-text)",".job-detail .job-sec-text:not(.fold-text)",".job-sec-text:not(.fold-text)"]),skillTexts:r(".job-detail .job-keyword-list li, .job-keyword-list li"),welfareTexts:r(".job-banner .job-tags span, .job-primary .job-tags span"),recruiterName:x,recruiterTitle:w,recruiterActiveTime:f,companyName:u,industryText:b.find((t)=>!/人|融资|上市|轮/.test(t))||"",scaleText:b.find((t)=>/\d+.*人/.test(t))||"",stageText:b.find((t)=>/轮|融资|上市/.test(t))||"",addressText:S}}async function R(e,r){const o=await e.evaluate(extractRenderedJob);return g(o,r)}async function z(e,r){const o=`https://www.zhipin.com/job_detail/${encodeURIComponent(r)}.html`;await j(e,"https://www.zhipin.com/web/geek/jobs",2);for(let i=0;i<2;i++){await j(e,o,5);for(let n=0;n<5;n++){try{const c=await R(e,r);if(c?.name&&c?.description&&c?.company)return c}catch{}if(n<4)await e.wait(1)}}if(await E(e))throw new q(O,"BOSS redirected the job detail page to the login flow");throw new k("BOSS detail page did not expose a complete job posting")}async function E(e){try{return await e.evaluate(`
7
+ (() => {
8
+ const href = window.location.href || '';
9
+ if (/\\/login|\\/user\\/login|passport/.test(href)) return true;
10
+ return !!document.querySelector('.sign-form, .login-card, [class*="login-register"]');
11
+ })()
12
+ `)===!0}catch{return!1}}A({site:"boss",name:"detail",access:"read",description:"BOSS直聘查看职位详情",domain:"www.zhipin.com",strategy:_.UI,navigateBefore:!1,browser:!0,defaultWindowMode:"background",siteSession:"persistent",args:[{name:"security-id",positional:!0,required:!0,help:"Security ID from search results (security_id field)"}],columns:["name","salary","experience","degree","city","address","description","skills","welfare","boss_name","boss_title","active_time","company","industry","scale","stage","url"],func:async(e,r)=>{C(e);const o=N(r["security-id"],"security-id");if(!/^[A-Za-z0-9_-]+$/.test(o))throw new B("boss security-id contains unsupported characters","Pass the security_id returned by `opencli boss search`");v("Fetching job detail from the rendered BOSS page...");return[await z(e,o)]}});export const __test__={cleanText:s,domSnapshotToRow:g};
@@ -1 +1 @@
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 v,verbose as l,CITY_CODES as d}from"./utils.js";const S={"不限":"0","在校/应届":"108","在校生":"108","在校":"108","应届生":"102","应届":"102","经验不限":"101","1年以内":"103","1-3年":"104","3-5年":"105","5-10年":"106","10年以上":"107"},A={"不限":"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"},C=new Set(Object.values(f));function M(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 _(e){if(!e)return"";if(f[e]!==void 0)return f[e];if(C.has(e))return e;throw new E(`Invalid jobType: ${e}`,"Use one of: 全职, 兼职, 实习, 不限")}function P(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=M(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,S),u=a(r.degree,A),y=a(r.salary,J),p=a(r.industry,z),b=_(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(p)n.set("industry",p);if(b)n.set("jobType",b);const I=`https://www.zhipin.com/wapi/zpgeek/search/joblist.json?${n.toString()}`;l(`Fetching page ${c}... (current jobs: ${o.length})`);const j=(await v(e,I)).zpData||{},w=j.jobList||[];if(w.length===0)break;let K=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:P(t.bossOnline),security_id:t.securityId||"",url:"https://www.zhipin.com/job_detail/"+t.encryptJobId+".html"});K++;if(o.length>=h)break}if(K===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:S,resolveMap:a,resolveJobType:_,formatBossOnline:P};
1
+ import{cli as T,Strategy as B}from"@jackwener/opencli/registry";import{ArgumentError as O,CommandExecutionError as P,EmptyResultError as D}from"@jackwener/opencli/errors";import{assertOk as v,readPositiveInteger as E,requirePage as A,navigateTo as x,verbose as u,CITY_CODES as p}from"./utils.js";const N={"不限":"0","在校/应届":"108","在校生(实习)":"108","在校生":"108","在校":"108","应届生(校招)":"102","应届生":"102","应届":"102","经验不限":"101","1年以内":"103","1-3年":"104","3-5年":"105","5-10年":"106","10年以上":"107"},J={"不限":"0","初中及以下":"209","中专/中技":"208","高中":"206","大专":"202","本科":"203","硕士":"204","博士":"205"},k={"不限":"0","3K以下":"401","3-5K":"402","5-10K":"403","10-15K":"404","15-20K":"405","20-30K":"406","30-50K":"407","50K以上":"408"},M={"不限":"0","互联网":"100020","电子商务":"100021","游戏":"100024","人工智能":"100901","大数据":"100902","金融":"100101","教育培训":"100200","医疗健康":"100300"},y={"不限":"0","全职":"1901","实习":"1902","兼职":"1903"},q=new Set(Object.values(y));function _(e){if(!e)return"101010100";if(/^\d+$/.test(e))return e;if(p[e])return p[e];for(const[r,o]of Object.entries(p))if(r.includes(e))return o;throw new O(`Invalid BOSS city: ${e}`,"Use a supported city name or a numeric BOSS city code")}function d(e,r){if(!e)return"";if(r[e]!==void 0)return r[e];for(const[o,c]of Object.entries(r))if(o.includes(e))return c;return e}function K(e){if(!e)return"";if(y[e]!==void 0)return y[e];if(q.has(e))return e;throw new O(`Invalid jobType: ${e}`,"Use one of: 全职, 兼职, 实习, 不限")}function C(e){if(e===!0)return"Y";if(e===!1)return"N";return""}async function z(e,r){if(typeof e.startNetworkCapture!=="function"||typeof e.readNetworkCapture!=="function"||!await e.startNetworkCapture("joblist.json"))throw new P("BOSS search requires CDP network capture");await e.readNetworkCapture();for(let o=0;o<2;o++){const c=r.includes("?")?"&":"?";await x(e,`${r}${c}_opencli=${Date.now()}`,5);await e.wait(1);const l=await e.readNetworkCapture();for(const s of Array.isArray(l)?l:[]){if(!String(s?.url||"").includes("joblist.json")||Number(s?.responseStatus||0)!==200||typeof s?.responsePreview!=="string")continue;let n;try{n=JSON.parse(s.responsePreview)}catch{continue}if(n&&typeof n==="object"&&"code"in n&&n.code!==0)v(n,"BOSS search failed");if(Array.isArray(n?.zpData?.jobList))return n}}throw new P("BOSS search page did not expose its job-list response")}T({site:"boss",name:"search",access:"read",description:"BOSS直聘搜索职位(不带关键词时返回为你推荐职位)",domain:"www.zhipin.com",strategy:B.INTERCEPT,navigateBefore:!1,browser:!0,defaultWindowMode:"background",siteSession:"persistent",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)=>{A(e);const o=String(r.query??"").trim(),c=_(r.city);u("Navigating to set referrer context...");const l=d(r.experience,N),s=d(r.degree,J),n=d(r.salary,k),m=d(r.industry,M),b=K(r.jobType),h=E(r.limit,"limit",15,100);let f=E(r.page,"page",1),a=[];const w=new Set;while(a.length<h){if(a.length>0)await new Promise((t)=>setTimeout(t,1000+Math.random()*2000));const i=new URLSearchParams({query:o,city:c,page:String(f)});if(l)i.set("experience",l);if(s)i.set("degree",s);if(n)i.set("salary",n);if(m)i.set("industry",m);if(b)i.set("jobType",b);const I=`https://www.zhipin.com/web/geek/jobs?${i.toString()}`;u(`Capturing page ${f}... (current jobs: ${a.length})`);const S=(await z(e,I)).zpData||{},g=S.jobList||[];if(g.length===0)break;let j=0;for(const t of g){if(!t.encryptJobId||w.has(t.encryptJobId))continue;w.add(t.encryptJobId);a.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:C(t.bossOnline),security_id:t.encryptJobId||"",url:"https://www.zhipin.com/job_detail/"+t.encryptJobId+".html"});j++;if(a.length>=h)break}if(j===0){u(`API returned duplicate page, stopping pagination at ${a.length} items`);break}if(!S.hasMore)break;f++}if(a.length===0)throw new D("boss search",o?`No BOSS jobs found for "${o}"`:"BOSS returned no recommended jobs");return a}});export const __test__={EXP_MAP:N,resolveCity:_,resolveMap:d,resolveJobType:K,formatBossOnline:C};
@@ -1 +1 @@
1
- import{cli as m,Strategy as d}from"@jackwener/opencli/registry";import{browserFetch as u}from"./_shared/browser-fetch.js";m({site:"douyin",name:"stats",access:"read",description:"作品数据分析",domain:"creator.douyin.com",strategy:d.COOKIE,args:[{name:"aweme_id",required:!0,positional:!0,help:"抖音作品 ID(aweme_id,可从作品 URL 末尾获取)"}],columns:["metric","value"],func:async(e,a)=>{const t=Math.floor(Date.now()/1000),o=t-604800,r="https://creator.douyin.com/janus/douyin/creator/data/item_analysis/metrics_trend",n={aweme_id:a.aweme_id,start_time:o,end_time:t,metrics:["play_count","like_count","comment_count","share_count"]},s=(await u(e,"POST",r,{body:n})).data??{};return Object.entries(s).map(([c,i])=>({metric:c,value:i}))}});
1
+ import{cli as l,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentError as p,EmptyResultError as m}from"@jackwener/opencli/errors";import{browserFetch as y}from"./_shared/browser-fetch.js";const _="https://creator.douyin.com/web/api/creator/item/list",g=50,b=50;export function normalizeAwemeId(t){const e=String(t??"").trim();if(!/^\d{16,20}$/.test(e))throw new p("douyin stats aweme_id must be a 16-20 digit numeric ID");return e}export function sameAwemeId(t,e){if(t==null)return!1;const r=String(t);if(r===e)return!0;return/^\d+$/.test(r)&&Number(r)===Number(e)}l({site:"douyin",name:"stats",access:"read",description:"获取单个作品的创作者中心深层指标",domain:"creator.douyin.com",strategy:w.COOKIE,args:[{name:"aweme_id",required:!0,positional:!0,help:"抖音作品 ID(aweme_id,可从作品 URL 末尾获取)"}],columns:["metric","value"],func:async(t,e)=>{const r=normalizeAwemeId(e.aweme_id);let n;for(let a=0;a<b;a++){const c=new URLSearchParams({count:String(g),order_by:"1",fields:"metrics,review,visibility",need_cooperation:"true",need_long_article:"true"});if(n!==void 0)c.set("max_cursor",String(n));const s=await y(t,"GET",`${_}?${c.toString()}`),u=(s.items??[]).find((i)=>sameAwemeId(i?.id,r));if(u){const i=u.metrics;if(!i||typeof i!=="object"||Array.isArray(i))throw new m(`douyin stats ${r}`,"The work exists, but creator metrics are unavailable");return Object.entries(i).map(([d,f])=>({metric:d,value:f}))}const o=s.max_cursor;if(!s.has_more||o===void 0||o===null||n!==void 0&&String(o)===String(n))break;n=o}throw new m(`douyin stats ${r}`,"The work was not found in the logged-in creator account")}});
@@ -0,0 +1,26 @@
1
+ import{registerWatch as a}from"../_shared/watch.js";const n="https://github.com/notifications",l=`
2
+ (function () {
3
+ var items = Array.prototype.slice.call(document.querySelectorAll('.notifications-list-item'));
4
+ return JSON.stringify(items.map(function (li) {
5
+ var link = li.querySelector('a[href*="/issues/"], a[href*="/pull/"], a[href*="/discussions/"], a[href*="/commit/"]');
6
+ var timeEl = li.querySelector('relative-time');
7
+ // 仓库名从通知指向的链接里切,别去猜 DOM 里哪个 a 是仓库链接——那个位置
8
+ // GitHub 改过好几次,切链接是稳的。
9
+ var titleEl = li.querySelector('.markdown-title') || li.querySelector('.notification-list-item-link .css-truncate-target');
10
+ var icon = li.querySelector('svg.octicon');
11
+ var unreadDot = li.querySelector('.notification-list-item-unread, [class*="unread"]');
12
+ return {
13
+ url: link ? link.getAttribute('href') : '',
14
+ title: titleEl ? (titleEl.textContent || '').replace(/\\s+/g, ' ').trim() : (li.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 200),
15
+ repo: (function () {
16
+ var h = link ? (link.getAttribute('href') || '') : '';
17
+ var m = h.replace(/^https?:\\/\\/github\\.com/, '').match(/^\\/([^/]+)\\/([^/]+)\\//);
18
+ return m ? m[1] + '/' + m[2] : '';
19
+ })(),
20
+ time: timeEl ? (timeEl.getAttribute('datetime') || '') : '',
21
+ kind: icon ? (icon.getAttribute('class') || '').replace('octicon ', '').trim() : '',
22
+ unread: !!unreadDot
23
+ };
24
+ }));
25
+ })()
26
+ `;function c(i){if(!Array.isArray(i))return[];const r=[];for(const t of i){if(!t||!t.url&&!t.title)continue;const e=String(t.url||""),o=t.time?Date.parse(t.time):NaN;r.push({event:"notification",id:`${e}|${t.time||""}`,ts:Number.isFinite(o)?o:Date.now(),conversation_id:e,sender:{id:"",name:String(t.repo||"")},text:String(t.title||""),url:e?e.startsWith("http")?e:`https://github.com${e}`:n,raw:{repo:t.repo||"",icon:t.kind||"",unread:!!t.unread}})}return r}a({site:"github",name:"watch",domain:"github.com",mode:"reconcile",description:"GitHub 通知监听:按间隔拉取通知页,有新的 issue/PR 活动按 NDJSON 逐条输出",watchUrl:n,pattern:"/notifications",parse:()=>null,reconcile:async(i)=>{await i.goto(n,{waitUntil:"load"});const r=await i.evaluate(l);try{return c(JSON.parse(typeof r==="string"?r:"[]"))}catch{return[]}}});
@@ -1 +1 @@
1
- import{adfToMarkdown as j,atlassianRequest as I,getJiraConfig as x,htmlToMarkdown as C,parseLimit as h,queryString as T,requireNonEmptyRows as b,requirePayloadArray as u,requirePayloadObject as a,requirePayloadString as c,requireString as N}from"../_atlassian/shared.js";import{ArgumentError as _}from"@jackwener/opencli/errors";const P=["summary","issuetype","status","priority","labels","description","comment","attachment","issuelinks","fixVersions","versions","components","project","reporter","assignee","created","updated"];function R(e){return`/rest/api/${e.deployment==="cloud"?"3":"2"}`}export function jiraApiPath(e,t,r){return`${R(e)}${t.startsWith("/")?t:`/${t}`}${r?T(r):""}`}export async function jiraRequest(e,t,r={}){return I(e,jiraApiPath(e,t,r.params),r)}function p(){return{acceptanceCriteria:process.env.ATLASSIAN_JIRA_ACCEPTANCE_FIELD?.trim()||"",sprint:process.env.ATLASSIAN_JIRA_SPRINT_FIELD?.trim()||"",storyPoints:process.env.ATLASSIAN_JIRA_STORY_POINTS_FIELD?.trim()||""}}function q(e=[]){const t=Object.values(p()).filter(Boolean);return[...new Set([...P,...t,...e.filter(Boolean)])].join(",")}export function parseJiraLimit(e,t=20,r=100){return h(e,t,r,"jira limit")}export function requireIssueKey(e){const t=N(e,"Jira issue key");if(!/^[A-Za-z][A-Za-z0-9_]+-\d+$/.test(t))throw new _(`Invalid Jira issue key: ${t}`,"Expected a key like PROJECT-123.");return t.toUpperCase()}function f(e){if(!e||typeof e!=="object")return"";return String(e.displayName??e.name??e.emailAddress??e.accountId??"")}function d(e){if(!e)return"";if(typeof e==="string")return e;if(typeof e==="object")return String(e.name??e.value??e.key??e.id??"");return String(e)}function l(e){return Array.isArray(e)?e.map(d).filter(Boolean):[]}export function jiraBodyToMarkdown(e,t){if(t)return C(t);if(e&&typeof e==="object")return j(e);if(typeof e==="string")return e.trim();return""}export function normalizeComment(e){const t=a(e,"jira comment");return{id:c(t.id,"comment id","jira comment"),author:f(t.author),created:t.created?String(t.created):"",updated:t.updated?String(t.updated):void 0,markdown:jiraBodyToMarkdown(t.body,t.renderedBody)}}export function normalizeAttachment(e){const t=a(e,"jira attachment");return{id:c(t.id,"attachment id","jira attachment"),filename:c(t.filename,"filename","jira attachment"),mimeType:t.mimeType?String(t.mimeType):void 0,size:t.size!=null?Number(t.size):void 0,url:c(t.content??t.self,"attachment url","jira attachment")}}export function normalizeIssueLink(e){const t=a(e,"jira issue link"),r=t.outwardIssue,i=t.inwardIssue;return{key:c((r??i??{}).key,"linked issue key","jira issue link"),type:String(t.type?.name??(r?t.type?.outward:t.type?.inward)??""),direction:r?"outward":"inward"}}function A(e){if(!e)return"";if(typeof e==="string")return e.trim();if(e&&typeof e==="object"&&e.type==="doc")return j(e);if(Array.isArray(e))return e.map(d).filter(Boolean).join(", ");return d(e)}function E(e,t,r){if(r.comments!==void 0)return u(r.comments,`jira issue ${t} comments`);if(r.requireNestedCollections===!1)return[];const i=a(e.comment,`jira issue ${t} comment field`);return u(i.comments,`jira issue ${t} comment field comments`)}export function normalizeJiraIssue(e,t,r={}){const i=a(e,"jira issue"),o=c(i.key,"issue key","jira issue"),n=a(i.fields,`jira issue ${o} fields`),S=i.renderedFields&&typeof i.renderedFields==="object"&&!Array.isArray(i.renderedFields)?i.renderedFields:{},s=p(),g=E(n,o,r),y=r.requireNestedCollections!==!1,w=y?u(n.attachment,`jira issue ${o} attachment field`):[],k=y?u(n.issuelinks,`jira issue ${o} issuelinks field`):[],m={key:o,id:i.id!=null?String(i.id):"",url:`${t.baseUrl}/browse/${o}`,summary:String(n.summary??""),issueType:d(n.issuetype)||void 0,status:d(n.status)||void 0,priority:d(n.priority)||void 0,project:d(n.project)||void 0,reporter:f(n.reporter)||void 0,assignee:f(n.assignee)||void 0,labels:Array.isArray(n.labels)?n.labels.map(String):[],description:{raw:n.description??null,markdown:jiraBodyToMarkdown(n.description,S.description)},comments:g.map(normalizeComment),attachments:w.map(normalizeAttachment),linkedIssues:k.map(normalizeIssueLink),fixVersions:l(n.fixVersions),affectedVersions:l(n.versions),components:l(n.components),created:n.created?String(n.created):void 0,updated:n.updated?String(n.updated):void 0};if(s.acceptanceCriteria&&n[s.acceptanceCriteria]!==void 0)m.acceptanceCriteria={raw:n[s.acceptanceCriteria],markdown:A(n[s.acceptanceCriteria])};if(s.sprint&&n[s.sprint]!==void 0)m.sprint=A(n[s.sprint]);if(s.storyPoints&&n[s.storyPoints]!==void 0)m.storyPoints=Number(n[s.storyPoints]);return m}export function issueSummaryRow(e,t){const r=normalizeJiraIssue(e,t,{comments:[],requireNestedCollections:!1});return{key:r.key,summary:r.summary,issueType:r.issueType,status:r.status,priority:r.priority,assignee:r.assignee,updated:r.updated,url:r.url}}export async function fetchIssue(e,t,r=[]){const i=await jiraRequest(e,`/issue/${encodeURIComponent(t)}`,{params:{fields:q(r),expand:"renderedFields"},label:`jira issue ${t}`});return a(i,`jira issue ${t}`)}export async function fetchComments(e,t,r=100){const i=parseJiraLimit(r,100,100),o=await jiraRequest(e,`/issue/${encodeURIComponent(t)}/comment`,{params:{startAt:0,maxResults:i,expand:"renderedBody"},label:`jira comments ${t}`}),n=a(o,`jira comments ${t}`);return u(n.comments,`jira comments ${t}`)}export function jiraRowsOrEmpty(e,t,r){return b(e,t,r)}export function jiraConfig(){return x()}export const __test__={configuredFieldNames:p,fetchIssue,issueSummaryRow,jiraApiPath,jiraBodyToMarkdown,normalizeAttachment,normalizeComment,normalizeIssueLink,normalizeJiraIssue,requireIssueKey};
1
+ import{adfToMarkdown as j,atlassianRequest as I,getJiraConfig as x,htmlToMarkdown as C,parseLimit as h,queryString as T,requireNonEmptyRows as b,requirePayloadArray as u,requirePayloadObject as a,requirePayloadString as c,requireString as N}from"../_atlassian/shared.js";import{ArgumentError as _}from"@jackwener/opencli/errors";const P=["summary","issuetype","status","priority","labels","description","comment","attachment","issuelinks","fixVersions","versions","components","project","reporter","assignee","created","updated"];function R(e){return`/rest/api/${e.deployment==="cloud"?"3":"2"}`}export function jiraApiPath(e,t,r){return`${R(e)}${t.startsWith("/")?t:`/${t}`}${r?T(r):""}`}export async function jiraRequest(e,t,r={}){return I(e,jiraApiPath(e,t,r.params),r)}function p(){return{acceptanceCriteria:process.env.ATLASSIAN_JIRA_ACCEPTANCE_FIELD?.trim()||"",sprint:process.env.ATLASSIAN_JIRA_SPRINT_FIELD?.trim()||"",storyPoints:process.env.ATLASSIAN_JIRA_STORY_POINTS_FIELD?.trim()||""}}function q(e=[]){const t=Object.values(p()).filter(Boolean);return[...new Set([...P,...t,...e.filter(Boolean)])].join(",")}export function parseJiraLimit(e,t=20,r=100){return h(e,t,r,"jira limit")}export function requireIssueKey(e){const t=N(e,"Jira issue key");if(!/^[A-Za-z][A-Za-z0-9_]+-\d+$/.test(t))throw new _(`Invalid Jira issue key: ${t}`,"Expected a key like PROJECT-123.");return t.toUpperCase()}function f(e){if(!e||typeof e!=="object")return"";return String(e.displayName??e.name??e.emailAddress??e.accountId??"")}function d(e){if(!e)return"";if(typeof e==="string")return e;if(typeof e==="object")return String(e.name??e.value??e.key??e.id??"");return String(e)}function l(e){return Array.isArray(e)?e.map(d).filter(Boolean):[]}export function jiraBodyToMarkdown(e,t){if(t)return C(t);if(e&&typeof e==="object")return j(e);if(typeof e==="string")return e.trim();return""}export function normalizeComment(e){const t=a(e,"jira comment");return{id:c(t.id,"comment id","jira comment"),author:f(t.author),created:t.created?String(t.created):"",updated:t.updated?String(t.updated):void 0,markdown:jiraBodyToMarkdown(t.body,t.renderedBody)}}export function normalizeAttachment(e){const t=a(e,"jira attachment");return{id:c(t.id,"attachment id","jira attachment"),filename:c(t.filename,"filename","jira attachment"),mimeType:t.mimeType?String(t.mimeType):void 0,size:t.size!=null?Number(t.size):void 0,url:c(t.content??t.self,"attachment url","jira attachment")}}export function normalizeIssueLink(e){const t=a(e,"jira issue link"),{outwardIssue:r,inwardIssue:i}=t;return{key:c((r??i??{}).key,"linked issue key","jira issue link"),type:String(t.type?.name??(r?t.type?.outward:t.type?.inward)??""),direction:r?"outward":"inward"}}function A(e){if(!e)return"";if(typeof e==="string")return e.trim();if(e&&typeof e==="object"&&e.type==="doc")return j(e);if(Array.isArray(e))return e.map(d).filter(Boolean).join(", ");return d(e)}function E(e,t,r){if(r.comments!==void 0)return u(r.comments,`jira issue ${t} comments`);if(r.requireNestedCollections===!1)return[];const i=a(e.comment,`jira issue ${t} comment field`);return u(i.comments,`jira issue ${t} comment field comments`)}export function normalizeJiraIssue(e,t,r={}){const i=a(e,"jira issue"),o=c(i.key,"issue key","jira issue"),n=a(i.fields,`jira issue ${o} fields`),S=i.renderedFields&&typeof i.renderedFields==="object"&&!Array.isArray(i.renderedFields)?i.renderedFields:{},s=p(),g=E(n,o,r),y=r.requireNestedCollections!==!1,w=y?u(n.attachment,`jira issue ${o} attachment field`):[],k=y?u(n.issuelinks,`jira issue ${o} issuelinks field`):[],m={key:o,id:i.id!=null?String(i.id):"",url:`${t.baseUrl}/browse/${o}`,summary:String(n.summary??""),issueType:d(n.issuetype)||void 0,status:d(n.status)||void 0,priority:d(n.priority)||void 0,project:d(n.project)||void 0,reporter:f(n.reporter)||void 0,assignee:f(n.assignee)||void 0,labels:Array.isArray(n.labels)?n.labels.map(String):[],description:{raw:n.description??null,markdown:jiraBodyToMarkdown(n.description,S.description)},comments:g.map(normalizeComment),attachments:w.map(normalizeAttachment),linkedIssues:k.map(normalizeIssueLink),fixVersions:l(n.fixVersions),affectedVersions:l(n.versions),components:l(n.components),created:n.created?String(n.created):void 0,updated:n.updated?String(n.updated):void 0};if(s.acceptanceCriteria&&n[s.acceptanceCriteria]!==void 0)m.acceptanceCriteria={raw:n[s.acceptanceCriteria],markdown:A(n[s.acceptanceCriteria])};if(s.sprint&&n[s.sprint]!==void 0)m.sprint=A(n[s.sprint]);if(s.storyPoints&&n[s.storyPoints]!==void 0)m.storyPoints=Number(n[s.storyPoints]);return m}export function issueSummaryRow(e,t){const r=normalizeJiraIssue(e,t,{comments:[],requireNestedCollections:!1});return{key:r.key,summary:r.summary,issueType:r.issueType,status:r.status,priority:r.priority,assignee:r.assignee,updated:r.updated,url:r.url}}export async function fetchIssue(e,t,r=[]){const i=await jiraRequest(e,`/issue/${encodeURIComponent(t)}`,{params:{fields:q(r),expand:"renderedFields"},label:`jira issue ${t}`});return a(i,`jira issue ${t}`)}export async function fetchComments(e,t,r=100){const i=parseJiraLimit(r,100,100),o=await jiraRequest(e,`/issue/${encodeURIComponent(t)}/comment`,{params:{startAt:0,maxResults:i,expand:"renderedBody"},label:`jira comments ${t}`}),n=a(o,`jira comments ${t}`);return u(n.comments,`jira comments ${t}`)}export function jiraRowsOrEmpty(e,t,r){return b(e,t,r)}export function jiraConfig(){return x()}export const __test__={configuredFieldNames:p,fetchIssue,issueSummaryRow,jiraApiPath,jiraBodyToMarkdown,normalizeAttachment,normalizeComment,normalizeIssueLink,normalizeJiraIssue,requireIssueKey};
package/clis/ke/auth.js CHANGED
@@ -1,14 +1,16 @@
1
- import{AuthRequiredError as o,CommandExecutionError as a}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as s}from"../_shared/site-auth.js";async function i(e){return(await e.getCookies({url:"https://www.ke.com"})).some((t)=>t.name==="lianjia_token"&&t.value)}async function r(e){if(!await i(e))throw new o("ke.com","Ke lianjia_token cookie missing — anonymous");await e.goto("https://www.ke.com/");await e.wait(2);const n=await e.evaluate(`
1
+ import{AuthRequiredError as o,CommandExecutionError as s}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as a}from"../_shared/site-auth.js";async function i(e){return(await e.getCookies({url:"https://www.ke.com"})).some((t)=>t.name==="lianjia_token"&&t.value)}async function r(e){if(!await i(e))throw new o("ke.com","Ke lianjia_token cookie missing — anonymous");await e.goto("https://www.ke.com/");await e.wait(2);const n=await e.evaluate(`
2
2
  (() => {
3
3
  const loginBtn = document.querySelector('.btn-login, a[class*=actLoginBtn], .login-btn');
4
4
  if (loginBtn && /登录|登陆/.test(loginBtn.innerText || '')) {
5
5
  return { kind: 'auth', detail: 'Ke shows 登录 button — anonymous session' };
6
6
  }
7
- const el = document.querySelector('.userNick, .user-name, .myInfo a, [class*=userNick]');
7
+ // 2026-08 贝壳新版 SSR:用户名在 .typeShowUser(脱敏手机号如 15****93),
8
+ // 旧锚点 .userNick/.user-name/.myInfo 已下线,故把 .typeShowUser 提到最前。
9
+ const el = document.querySelector('.typeShowUser a span, .typeShowUser a, .userNick, .user-name, .myInfo a, [class*=userNick]');
8
10
  const username = (el?.innerText || '').trim();
9
11
  if (!username) {
10
12
  return { kind: 'auth', detail: 'Ke no user-name DOM anchor — anonymous or SSR failed' };
11
13
  }
12
14
  return { ok: true, username };
13
15
  })()
14
- `);if(n?.kind==="auth")throw new o("ke.com",n.detail);if(!n?.ok)throw new a(`Unexpected Ke probe: ${JSON.stringify(n)}`);return{username:n.username}}s({site:"ke",domain:"ke.com",loginUrl:"https://clogin.ke.com/login/?service=https%3A%2F%2Fwww.ke.com",columns:["username"],verify:r,poll:async(e)=>{if(!await i(e))throw new o("ke.com","Waiting for Ke lianjia_token cookie");return r(e)}});
16
+ `);if(n?.kind==="auth")throw new o("ke.com",n.detail);if(!n?.ok)throw new s(`Unexpected Ke probe: ${JSON.stringify(n)}`);return{username:n.username}}a({site:"ke",domain:"ke.com",loginUrl:"https://clogin.ke.com/login/?service=https%3A%2F%2Fwww.ke.com",columns:["username"],verify:r,poll:async(e)=>{if(!await i(e))throw new o("ke.com","Waiting for Ke lianjia_token cookie");return r(e)}});
@@ -0,0 +1,5 @@
1
+ import{cli as E,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as i,CommandExecutionError as c}from"@jackwener/opencli/errors";import{GW_BASE as _,WWW_BASE as r,getJson as M,postJson as q,postMultipart as T,requireOk as m}from"./utils.js";import{assertLiteralContent as y}from"../_shared/content-guard.js";import{normalizeContent as A}from"../_shared/article/format.js";import{inlineLocalImages as C,transferImages as j}from"../_shared/article/images.js";const w={Accept:"application/json, text/plain, */*","X-Requested-With":"XMLHttpRequest"},L={url:`${r}/uploadImage?type=1`,method:"POST",bodyType:"binary-multipart",fileField:"file",fileName:"image.png",headers:w,responsePath:"url"},P=["uploadfiles.nowcoder.com"],g=50;function D(e){return String(e).replace(/<br\s*\/?>/gi,`
2
+ `).replace(/<\/(?:p|h[1-6]|li|blockquote|div)>/gi,`
3
+ `).replace(/<[^>]+>/g,"").replace(/&nbsp;/g," ").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,'"').replace(/&#39;/g,"'").replace(/&amp;/g,"&").split(`
4
+ `).map((n)=>n.trim()).filter(Boolean)}function H(e,n){const t=D(n);return JSON.stringify({editorChildren:t.map((a)=>({type:"paragraph",children:[{text:a}]})),toolData:{title:e,content:t.join(`
5
+ `),type:0,subjectIds:[]}})}E({site:"nowcoder",name:"article",access:"write",description:"发布长文文章(实名长贴,支持 Markdown 正文与图片自动转存;高风险写操作,务必低频且内容有价值)",domain:"www.nowcoder.com",strategy:S.COOKIE,args:[{name:"title",positional:!0,required:!0,help:`文章标题(不超过 ${g} 字)`},{name:"content",fromFile:!0,positional:!0,required:!0,help:"正文(默认 Markdown,自动转 HTML;也可直接给 HTML)"},{name:"html",type:"boolean",help:"把正文按原始 HTML 处理,不做 Markdown 转换"},{name:"allow-duplicate",type:"boolean",help:"允许与自己已发文章同标题(默认拦截,防止重试导致重复发布)"}],columns:["content_id","title","url","images_uploaded"],func:async(e,n)=>{const t=String(n.title??"").trim();y(t,{label:"标题"});if(!t)throw new i("文章标题不能为空");if([...t].length>g)throw new i(`文章标题超长: ${[...t].length} 字,牛客上限 ${g} 字`);const a=y(n.content,{label:"正文"});if(!a)throw new i("文章正文不能为空");const p=await C(a);if(p.missing.length){const s=p.missing.map((o)=>`${o.src}(${o.error})`).join("、");throw new i(`正文引用的本机图片读不到: ${s}`)}const{html:f}=A(p.content,{format:n.html?"html":"auto"});if(!f.trim())throw new i("正文转换后为空");const l=await j(e,f,{spec:L,skip:P});if(l.failed?.length){const s=l.failed.map((o)=>`${o.src}(${o.error??"上传失败"})`).join("、");throw new c(`图片转存牛客图床失败,已中止发布(未产生任何内容,可修好图片后重试): ${s}`)}const d=l.content;if(n["allow-duplicate"]!==!0){const b=(m(await M(e,`${r}/content/my-content?status=-1&page=1&_=${Date.now()}`),"我的内容列表").data?.tableData??[]).find((I)=>String(I.title??"").trim()===t);if(b)throw new c(`牛客上已有同标题的文章(${r}/discuss/${b.id}),已中止发布。`+"若上一次发布其实已成功,请不要重试;确需再发一篇同名文章加 --allow-duplicate。")}if(m(await T(e,`${r}/discuss/check/same?_=${Date.now()}`,{id:"",title:t,content:d,subjectIds:""},w),"长文查重").data===!0)throw new c("牛客站内已存在标题与正文都相同的内容,已中止发布(若上一次发布其实已成功,请勿重试;确需再发请修改内容)");const h=m(await q(e,`${_}/long-content/create?_=${Date.now()}`,{subjectIds:[],zhuanlanIdStr:"",askQuestion:!1,reward:"",tags:[],title:t,content:d,questionId:null,questionTitle:null,questionUrl:null,blogTags:[],isAcm:!1,distinctPost:!1,jsonContent:H(t,d),id:"",mdContent:"",anonymous:!1,publicEntrance:"发帖子",publicEntrancePage:"直接访问"},w),"长文发布"),u=h.data?.result;if(!u)throw new c(`牛客长文发布响应缺少 contentId: ${JSON.stringify(h).slice(0,200)}`);return[{content_id:String(u),title:t,url:`${r}/discuss/${u}`,images_uploaded:(l.uploaded??[]).length}]}});
@@ -1 +1 @@
1
- import{cli as m,Strategy as i}from"@jackwener/opencli/registry";import{GW_BASE as r,postMultipart as n,requireOk as d,resolveMomentId as s}from"./utils.js";m({site:"nowcoder",name:"delete",access:"write",description:"删除自己发布的动态",domain:"www.nowcoder.com",strategy:i.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"要删除的 momentId(my/post 返回的 moment_id)"}],columns:["moment_id","status"],func:async(e,o)=>{const t=await s(e,o.id);d(await n(e,`${r}/moment/delete`,{isWeb:"true",momentId:t}),"删帖");return[{moment_id:t,status:"deleted"}]}});
1
+ import{cli as u,Strategy as w}from"@jackwener/opencli/registry";import{GW_BASE as o,postMultipart as s,requireOk as a,resolveMomentId as p}from"./utils.js";const y={Accept:"application/json, text/plain, */*","X-Requested-With":"XMLHttpRequest"};u({site:"nowcoder",name:"delete",access:"write",description:"删除自己发布的动态或文章",domain:"www.nowcoder.com",strategy:w.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"要删除的 id(动态取 my/post 的 moment_id,文章取 article 的 content_id)"}],columns:["id","kind","status"],func:async(r,c)=>{const t=String(c.id??"").trim(),d=/^\d{17,}$/.test(t),n=async()=>{const e=await p(r,t);a(await s(r,`${o}/moment/delete`,{isWeb:"true",momentId:e}),"删除动态");return[{id:e,kind:"moment",status:"deleted"}]},i=async()=>{a(await s(r,`${o}/long-content/delete?_=${Date.now()}`,{id:t},y),"删除文章");return[{id:t,kind:"article",status:"deleted"}]},[m,l]=d?[i,n]:[n,i];try{return await m()}catch(e){try{return await l()}catch{throw e}}}});
@@ -1,6 +1,6 @@
1
- import{AuthRequiredError as i,CommandExecutionError as c}from"@jackwener/opencli/errors";export const GW_BASE="https://gw-c.nowcoder.com/api/sparta",WWW_BASE="https://www.nowcoder.com";export async function getJson(t,e){const r=JSON.stringify(e);return t.evaluate(`
1
+ import{AuthRequiredError as a,CommandExecutionError as i}from"@jackwener/opencli/errors";export const GW_BASE="https://gw-c.nowcoder.com/api/sparta",WWW_BASE="https://www.nowcoder.com";export async function getJson(t,e){const s=JSON.stringify(e);return t.evaluate(`
2
2
  async () => {
3
- const res = await fetch(${r}, { credentials: 'include' });
3
+ const res = await fetch(${s}, { credentials: 'include' });
4
4
  const text = await res.text();
5
5
  try {
6
6
  return JSON.parse(text);
@@ -8,13 +8,13 @@ import{AuthRequiredError as i,CommandExecutionError as c}from"@jackwener/opencli
8
8
  return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
9
9
  }
10
10
  }
11
- `)}export async function postJson(t,e,r){const s=JSON.stringify(e),n=JSON.stringify(JSON.stringify(r));return t.evaluate(`
11
+ `)}export async function postJson(t,e,s,r={}){const n=JSON.stringify(e),o=JSON.stringify(JSON.stringify(s)),c=JSON.stringify({"Content-Type":"application/json",...r});return t.evaluate(`
12
12
  async () => {
13
- const res = await fetch(${s}, {
13
+ const res = await fetch(${n}, {
14
14
  method: 'POST',
15
15
  credentials: 'include',
16
- headers: { 'Content-Type': 'application/json' },
17
- body: ${n},
16
+ headers: ${c},
17
+ body: ${o},
18
18
  });
19
19
  const text = await res.text();
20
20
  try {
@@ -23,10 +23,10 @@ import{AuthRequiredError as i,CommandExecutionError as c}from"@jackwener/opencli
23
23
  return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
24
24
  }
25
25
  }
26
- `)}export async function postForm(t,e,r){const s=JSON.stringify(e),n=JSON.stringify(r);return t.evaluate(`
26
+ `)}export async function postForm(t,e,s){const r=JSON.stringify(e),n=JSON.stringify(s);return t.evaluate(`
27
27
  async () => {
28
28
  const body = new URLSearchParams(${n});
29
- const res = await fetch(${s}, {
29
+ const res = await fetch(${r}, {
30
30
  method: 'POST',
31
31
  credentials: 'include',
32
32
  headers: {
@@ -42,13 +42,14 @@ import{AuthRequiredError as i,CommandExecutionError as c}from"@jackwener/opencli
42
42
  return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
43
43
  }
44
44
  }
45
- `)}export async function postMultipart(t,e,r){const s=JSON.stringify(e),n=JSON.stringify(r);return t.evaluate(`
45
+ `)}export async function postMultipart(t,e,s,r={}){const n=JSON.stringify(e),o=JSON.stringify(s),c=JSON.stringify(r);return t.evaluate(`
46
46
  async () => {
47
47
  const fd = new FormData();
48
- for (const [k, v] of Object.entries(${n})) fd.append(k, v);
49
- const res = await fetch(${s}, {
48
+ for (const [k, v] of Object.entries(${o})) fd.append(k, v);
49
+ const res = await fetch(${n}, {
50
50
  method: 'POST',
51
51
  credentials: 'include',
52
+ headers: ${c},
52
53
  body: fd,
53
54
  });
54
55
  const text = await res.text();
@@ -58,11 +59,11 @@ import{AuthRequiredError as i,CommandExecutionError as c}from"@jackwener/opencli
58
59
  return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
59
60
  }
60
61
  }
61
- `)}export function requireOk(t,e){if(!t||typeof t!=="object"||Array.isArray(t))throw new c(`牛客 ${e} 接口返回了畸形响应`);if(Object.hasOwn(t,"success")?!!t.success:Object.hasOwn(t,"code")?t.code===0:Object.hasOwn(t,"data"))return t;const s=String(t.msg??t.message??"未知错误");if(/登录|登陆|未授权|token|身份/i.test(s))throw new i("nowcoder.com",`牛客 ${e} 需要登录: ${s}`);throw new c(`牛客 ${e} 失败: ${s} (code=${t.code??"?"})`)}export async function resolveMomentId(t,e){let r=String(e??"").trim();for(const a of[/detail\/([0-9a-fA-F]{20,})/,/discuss\/(\d+)/]){const o=r.match(a);if(o){r=o[1];break}}if(/^\d+$/.test(r))return r;if(!/^[0-9a-fA-F]{20,}$/.test(r))throw new c(`无法从输入解析牛客帖子 id: ${e}`);const n=requireOk(await getJson(t,`${GW_BASE}/detail/moment-data/detail/${r}`),"帖子解析").data?.id;if(!n)throw new c(`牛客帖子 ${r} 的详情缺少数字 momentId`);return String(n)}export async function getSelfUid(t){const e=await t.evaluate(`
62
+ `)}export function requireOk(t,e){if(!t||typeof t!=="object"||Array.isArray(t))throw new i(`牛客 ${e} 接口返回了畸形响应`);if(Object.hasOwn(t,"success")?!!t.success:Object.hasOwn(t,"code")?t.code===0:Object.hasOwn(t,"data"))return t;const r=String(t.msg??t.message??"未知错误");if(/登录|登陆|未授权|token|身份/i.test(r))throw new a("nowcoder.com",`牛客 ${e} 需要登录: ${r}`);throw new i(`牛客 ${e} 失败: ${r} (code=${t.code??"?"})`)}export async function resolveMomentId(t,e){let s=String(e??"").trim();for(const o of[/detail\/([0-9a-fA-F]{20,})/,/discuss\/(\d+)/]){const c=s.match(o);if(c){s=c[1];break}}if(/^\d+$/.test(s))return s;if(!/^[0-9a-fA-F]{20,}$/.test(s))throw new i(`无法从输入解析牛客帖子 id: ${e}`);const n=requireOk(await getJson(t,`${GW_BASE}/detail/moment-data/detail/${s}`),"帖子解析").data?.id;if(!n)throw new i(`牛客帖子 ${s} 的详情缺少数字 momentId`);return String(n)}export async function getSelfUid(t){const e=await t.evaluate(`
62
63
  async () => {
63
64
  const link = document.querySelector('a[href*="/users/"]');
64
65
  const href = link ? (link.getAttribute('href') || '') : '';
65
66
  const match = href.match(/\\/users\\/(\\d+)/);
66
67
  return match ? match[1] : '';
67
68
  }
68
- `);if(!e)throw new i("nowcoder.com","导航栏无登录用户链接(未登录)");return e}
69
+ `);if(!e)throw new a("nowcoder.com","导航栏无登录用户链接(未登录)");return e}
@@ -0,0 +1 @@
1
+ import{registerWatch as d}from"../_shared/watch.js";import{WWW_BASE as w,getJson as m,getSelfUid as c,requireOk as g}from"./utils.js";function u(t,e){const r=t?.data?.conversations;if(!Array.isArray(r))return[];const i=[];for(const n of r){const a=Number(n?.unreadMessageCount??0);if(a<=0&&!e?.includeOwn)continue;const s=String(n?.conversationId??""),o=Number(n?.createDate??0);if(!s||!o)continue;i.push({event:"dm",id:`${s}:${o}`,ts:o,conversation_id:s,sender:{id:String(n?.targetUserId??""),name:String(n?.targetUserName??"")},text:String(n?.content??"").trim(),url:"https://www.nowcoder.com/message/private",raw:{unread_count:a,message_count:n?.messageCount??0}})}return i}d({site:"nowcoder",name:"watch",domain:"www.nowcoder.com",mode:"reconcile",description:"牛客私信监听:按间隔拉取会话列表,有未读私信按 NDJSON 逐条输出",watchUrl:"https://www.nowcoder.com/",pattern:"/sns/message/",parse:(t,e)=>{if(!String(t?.url??"").includes("get-conversation-list"))return null;try{return u(JSON.parse(String(t.responsePreview??"")),e)}catch{return null}},onAttach:async(t,e)=>{e.state.uid=await c(t)},reconcile:async(t,e)=>{if(!e.state.uid)e.state.uid=await c(t);const r=`${w}/sns/message/${e.state.uid}/get-conversation-list?page=1`;return u(g(await m(t,r),"私信会话列表"),e)}});
@@ -1 +1 @@
1
- import{ArgumentError as m,CommandExecutionError as w}from"@jackwener/opencli/errors";import{cli as k,Strategy as q}from"@jackwener/opencli/registry";import{SHARE_API as S,extractPwdId as I,apiPost as y,getToken as E,pollTask as A,findFolder as D}from"./utils.js";async function F(t,e,l,s,a,i){return(await y(t,`${S}/save?pr=ucpro&fr=pc`,{pwd_id:e,stoken:l,pdir_fid:"0",to_pdir_fid:a,fid_list:s,pdir_save_all:i,scene:"link"})).task_id}k({site:"quark",name:"save",access:"write",description:"Save shared files to your Quark Drive",domain:"pan.quark.cn",strategy:q.COOKIE,defaultFormat:"json",args:[{name:"url",required:!0,positional:!0,help:"Quark share URL or pwd_id"},{name:"to",default:"",help:"Destination folder path"},{name:"to-fid",default:"",help:"Destination folder ID (overrides --to)"},{name:"fids",default:"",help:"File IDs to save (comma-separated, from share-tree). Omit to save all."},{name:"stoken",default:"",help:"Share token (from share-tree output, required with --fids)"},{name:"passcode",default:"",help:"Share passcode (if required)"},{name:"timeout",type:"int",required:!1,default:120,help:"Max seconds for the overall command (default: 120)"}],func:async(t,e)=>{const{url:l,to:s}=e,a=e["to-fid"],i=e.fids,d=e.stoken,_=e.passcode;if(!s&&!a)throw new m("Either --to or --to-fid is required");if(s&&a)throw new m("Cannot use both --to and --to-fid");const p=I(l),f=!i;let u,n;if(f){u=d||await E(t,p,_);n=[]}else{if(!d)throw new m("--stoken is required when using --fids");u=d;n=[...new Set(i.split(",").map((r)=>r.trim()).filter(Boolean))]}const h=a||await D(t,s),c=await F(t,p,u,n,h,f),o={success:!1,task_id:c,saved_to:s||a,target_fid:h,...f?{}:{fids:n}};if(c){const r=await A(t,c,(v)=>{o.save_count=v.save_as?.save_as_sum_num});o.completed=r;o.success=r;if(!r)throw new w("quark: Save task timed out")}else o.success=!0;return o}});
1
+ import{ArgumentError as m,CommandExecutionError as w}from"@jackwener/opencli/errors";import{cli as k,Strategy as q}from"@jackwener/opencli/registry";import{SHARE_API as S,extractPwdId as I,apiPost as y,getToken as E,pollTask as A,findFolder as D}from"./utils.js";async function F(t,e,l,s,a,i){return(await y(t,`${S}/save?pr=ucpro&fr=pc`,{pwd_id:e,stoken:l,pdir_fid:"0",to_pdir_fid:a,fid_list:s,pdir_save_all:i,scene:"link"})).task_id}k({site:"quark",name:"save",access:"write",description:"Save shared files to your Quark Drive",domain:"pan.quark.cn",strategy:q.COOKIE,defaultFormat:"json",args:[{name:"url",required:!0,positional:!0,help:"Quark share URL or pwd_id"},{name:"to",default:"",help:"Destination folder path"},{name:"to-fid",default:"",help:"Destination folder ID (overrides --to)"},{name:"fids",default:"",help:"File IDs to save (comma-separated, from share-tree). Omit to save all."},{name:"stoken",default:"",help:"Share token (from share-tree output, required with --fids)"},{name:"passcode",default:"",help:"Share passcode (if required)"},{name:"timeout",type:"int",required:!1,default:120,help:"Max seconds for the overall command (default: 120)"}],func:async(t,e)=>{const{url:l,to:s}=e,a=e["to-fid"],{fids:i,stoken:d,passcode:_}=e;if(!s&&!a)throw new m("Either --to or --to-fid is required");if(s&&a)throw new m("Cannot use both --to and --to-fid");const p=I(l),f=!i;let u,n;if(f){u=d||await E(t,p,_);n=[]}else{if(!d)throw new m("--stoken is required when using --fids");u=d;n=[...new Set(i.split(",").map((r)=>r.trim()).filter(Boolean))]}const h=a||await D(t,s),c=await F(t,p,u,n,h,f),o={success:!1,task_id:c,saved_to:s||a,target_fid:h,...f?{}:{fids:n}};if(c){const r=await A(t,c,(v)=>{o.save_count=v.save_as?.save_as_sum_num});o.completed=r;o.success=r;if(!r)throw new w("quark: Save task timed out")}else o.success=!0;return o}});
@@ -0,0 +1 @@
1
+ import{registerWatch as c}from"../_shared/watch.js";const a="https://www.reddit.com/message/inbox.json?limit=25&raw_json=1";function d(e){const r=e?.data?.children;if(!Array.isArray(r))return[];const s=[];for(const n of r){const t=n?.data;if(!t)continue;const i=String(t.name||(n.kind&&t.id?`${n.kind}_${t.id}`:t.id||""));if(!i)continue;const o=n.kind==="t4";s.push({event:o?"dm":"reply",id:i,ts:t.created_utc?Number(t.created_utc)*1000:Date.now(),conversation_id:String(t.first_message_name||t.parent_id||i),sender:{id:String(t.author_fullname||""),name:String(t.author||"")},text:String(t.body||""),url:t.context?`https://www.reddit.com${t.context}`:"https://www.reddit.com/message/inbox/",raw:{subject:t.subject||"",subreddit:t.subreddit||"",link_title:t.link_title||"",unread:t.new===!0}})}return s}c({site:"reddit",name:"watch",domain:"www.reddit.com",mode:"reconcile",description:"Reddit 收件箱监听:按间隔拉取私信与评论回复,有新消息按 NDJSON 逐条输出",watchUrl:"https://www.reddit.com/message/inbox/",pattern:"/message/inbox",parse:(e)=>{if(!String(e?.url??"").includes("/message/inbox"))return null;try{return d(JSON.parse(String(e.responsePreview??"")))}catch{return null}},reconcile:async(e)=>d(await e.fetchJson(a))});
@@ -1 +1 @@
1
- import{cli as i,Strategy as c}from"@jackwener/opencli/registry";import{CliError as u}from"@jackwener/opencli/errors";import{readFileSync as g,writeFileSync as E,mkdirSync as I,existsSync as R}from"fs";import{createServer as C}from"http";import{homedir as m}from"os";import{join as y}from"path";import{exec as v}from"child_process";import{assertSpotifyCredentialsConfigured as L,getFirstSpotifyTrack as x,mapSpotifyTrackResults as A,parseDotEnv as q,resolveSpotifyCredentials as B}from"./utils.js";const d=y(m(),".ppcli","spotify.env");function O(){if(!R(d))return{};return q(g(d,"utf-8"))}const N=O(),h=B(N),w=h.clientId,_=h.clientSecret,T="http://127.0.0.1:8888/callback",D=["user-read-playback-state","user-modify-playback-state","user-read-currently-playing","playlist-read-private"].join(" "),S=y(m(),".ppcli","spotify-tokens.json");function F(){try{return JSON.parse(g(S,"utf-8"))}catch{return null}}function U(t){I(y(m(),".ppcli"),{recursive:!0});E(S,JSON.stringify(t,null,2))}async function M(t){const e=await fetch("https://accounts.spotify.com/api/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Authorization:"Basic "+Buffer.from(`${w}:${_}`).toString("base64")},body:new URLSearchParams({grant_type:"refresh_token",refresh_token:t})});if(!e.ok){const r=await e.json().catch(()=>({}));throw new u("REFRESH_FAILED",r?.error_description||`Token refresh failed (${e.status})`)}const s=await e.json(),a={access_token:s.access_token,refresh_token:s.refresh_token||t,expires_at:Date.now()+s.expires_in*1000};U(a);return a.access_token}async function j(){const t=F();if(!t)throw new u("AUTH_REQUIRED","Not authenticated. Run: ppcli spotify auth");if(!t.access_token||!t.refresh_token||!(t.expires_at>0))throw new u("AUTH_CORRUPTED","Token file is corrupted. Run: ppcli spotify auth");if(Date.now()>t.expires_at-60000)return M(t.refresh_token);return t.access_token}async function o(t,e,s){const a=await j(),r=await fetch(`https://api.spotify.com/v1${e}`,{method:t,headers:{Authorization:`Bearer ${a}`,"Content-Type":"application/json"},body:s?JSON.stringify(s):void 0});if(r.status===204||r.status===202)return null;if(!r.ok){const n=await r.json().catch(()=>({}));throw new u("API_ERROR",n?.error?.message||`Spotify API error ${r.status}`)}return r.json()}async function P(t){const e=await o("GET",`/search?q=${encodeURIComponent(t)}&type=track&limit=1`),s=x(e);if(!s)throw new u("EMPTY_RESULT",`No track found for: ${t}`);return s}function H(t){const e=process.platform==="win32"?`start "" "${t}"`:process.platform==="darwin"?`open "${t}"`:`xdg-open "${t}"`;v(e)}i({site:"spotify",name:"auth",access:"write",description:"Authenticate with Spotify (OAuth — run once)",strategy:c.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{L(h,d);return new Promise((t,e)=>{const s=C(async(r,n)=>{try{const p=new URL(r.url,"http://localhost:8888");if(p.pathname!=="/callback"){n.end();return}const k=p.searchParams.get("code");if(!k){n.end("Missing code");return}const l=await fetch("https://accounts.spotify.com/api/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Authorization:"Basic "+Buffer.from(`${w}:${_}`).toString("base64")},body:new URLSearchParams({grant_type:"authorization_code",code:k,redirect_uri:T})});if(!l.ok){const b=await l.json().catch(()=>({}));s.close();e(new u("AUTH_FAILED",b?.error_description||`Token exchange failed (${l.status})`));return}const f=await l.json();U({access_token:f.access_token,refresh_token:f.refresh_token,expires_at:Date.now()+f.expires_in*1000});n.writeHead(200,{"Content-Type":"text/html"});n.end("<h2>Spotify authenticated! You can close this tab.</h2>");s.close();t([{status:"Authenticated successfully"}])}catch(p){s.close();e(p)}});s.on("error",(r)=>{if(r.code==="EADDRINUSE")e(new u("PORT_IN_USE","Port 8888 is already in use. Stop the other process and retry."));else e(r)});const a=setTimeout(()=>{s.close();e(new u("AUTH_TIMEOUT","Authentication timed out after 5 minutes"))},300000);s.listen(8888,()=>{const r=`https://accounts.spotify.com/authorize?${new URLSearchParams({client_id:w,response_type:"code",redirect_uri:T,scope:D})}`;console.log("Opening browser for Spotify login...");console.log("If it does not open, visit:",r);H(r)});s.on("close",()=>clearTimeout(a))})}});i({site:"spotify",name:"status",access:"read",description:"Show current playback status",strategy:c.PUBLIC,browser:!1,args:[],columns:["track","artist","album","status","progress"],func:async()=>{const t=await o("GET","/me/player");if(!t||!t.item)return[{track:"Nothing playing",artist:"",album:"",status:"",progress:""}];const e=t.item;if(e.type!=="track")return[{track:e.name,artist:"",album:e.show?.name??"",status:t.is_playing?"playing":"paused",progress:""}];const s=(t.progress_ms??0)/1000|0,a=e.duration_ms/1000|0,r=(n)=>`${n/60|0}:${String(n%60).padStart(2,"0")}`;return[{track:e.name,artist:e.artists.map((n)=>n.name).join(", "),album:e.album.name,status:t.is_playing?"playing":"paused",progress:`${r(s)} / ${r(a)}`}]}});i({site:"spotify",name:"play",access:"write",description:"Resume playback or search and play a track/artist",strategy:c.PUBLIC,browser:!1,args:[{name:"query",type:"str",default:"",positional:!0,help:"Track or artist to play (optional)"}],columns:["track","artist","status"],func:async(t)=>{if(t.query){const{uri:e,name:s,artist:a}=await P(t.query);await o("PUT","/me/player/play",{uris:[e]});return[{track:s,artist:a,status:"playing"}]}await o("PUT","/me/player/play");return[{track:"",artist:"",status:"resumed"}]}});i({site:"spotify",name:"pause",access:"write",description:"Pause playback",strategy:c.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{await o("PUT","/me/player/pause");return[{status:"paused"}]}});i({site:"spotify",name:"next",access:"write",description:"Skip to next track",strategy:c.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{await o("POST","/me/player/next");return[{status:"skipped to next"}]}});i({site:"spotify",name:"prev",access:"write",description:"Skip to previous track",strategy:c.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{await o("POST","/me/player/previous");return[{status:"skipped to previous"}]}});i({site:"spotify",name:"volume",access:"write",description:"Set playback volume (0-100)",strategy:c.PUBLIC,browser:!1,args:[{name:"level",type:"int",default:50,positional:!0,required:!0,help:"Volume 0–100"}],columns:["volume"],func:async(t)=>{const e=Math.round(t.level);if(e<0||e>100)throw new u("INVALID_ARGS","Volume must be between 0 and 100");await o("PUT",`/me/player/volume?volume_percent=${e}`);return[{volume:`${e}%`}]}});i({site:"spotify",name:"search",access:"read",description:"Search for tracks",strategy:c.PUBLIC,browser:!1,args:[{name:"query",type:"str",required:!0,positional:!0,help:"Search query"},{name:"limit",type:"int",default:10,help:"Number of results (default: 10)"}],columns:["track","artist","album","uri"],func:async(t)=>{const e=Math.min(50,Math.max(1,Math.round(t.limit))),s=await o("GET",`/search?q=${encodeURIComponent(t.query)}&type=track&limit=${e}`),a=A(s);if(!a.length)throw new u("EMPTY_RESULT",`No results found for: ${t.query}`);return a}});i({site:"spotify",name:"queue",access:"write",description:"Add a track to the playback queue",strategy:c.PUBLIC,browser:!1,args:[{name:"query",type:"str",required:!0,positional:!0,help:"Track to add to queue"}],columns:["track","artist","status"],func:async(t)=>{const{uri:e,name:s,artist:a}=await P(t.query);await o("POST",`/me/player/queue?uri=${encodeURIComponent(e)}`);return[{track:s,artist:a,status:"added to queue"}]}});i({site:"spotify",name:"shuffle",access:"write",description:"Toggle shuffle on/off",strategy:c.PUBLIC,browser:!1,args:[{name:"state",type:"str",default:"on",positional:!0,choices:["on","off"],help:"on or off"}],columns:["shuffle"],func:async(t)=>{await o("PUT",`/me/player/shuffle?state=${t.state==="on"}`);return[{shuffle:t.state}]}});i({site:"spotify",name:"repeat",access:"write",description:"Set repeat mode (off / track / context)",strategy:c.PUBLIC,browser:!1,args:[{name:"mode",type:"str",default:"context",positional:!0,choices:["off","track","context"],help:"off / track / context"}],columns:["repeat"],func:async(t)=>{await o("PUT",`/me/player/repeat?state=${t.mode}`);return[{repeat:t.mode}]}});
1
+ import{cli as i,Strategy as c}from"@jackwener/opencli/registry";import{CliError as u}from"@jackwener/opencli/errors";import{readFileSync as _,writeFileSync as I,mkdirSync as R,existsSync as C}from"fs";import{createServer as v}from"http";import{homedir as m}from"os";import{join as y}from"path";import{execFile as d}from"child_process";import{assertSpotifyCredentialsConfigured as L,getFirstSpotifyTrack as x,mapSpotifyTrackResults as A,parseDotEnv as q,resolveSpotifyCredentials as B}from"./utils.js";const h=y(m(),".ppcli","spotify.env");function O(){if(!C(h))return{};return q(_(h,"utf-8"))}const N=O(),w=B(N),{clientId:k,clientSecret:T}=w,S="http://127.0.0.1:8888/callback",D=["user-read-playback-state","user-modify-playback-state","user-read-currently-playing","playlist-read-private"].join(" "),U=y(m(),".ppcli","spotify-tokens.json");function F(){try{return JSON.parse(_(U,"utf-8"))}catch{return null}}function P(t){R(y(m(),".ppcli"),{recursive:!0});I(U,JSON.stringify(t,null,2))}async function M(t){const e=await fetch("https://accounts.spotify.com/api/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Authorization:"Basic "+Buffer.from(`${k}:${T}`).toString("base64")},body:new URLSearchParams({grant_type:"refresh_token",refresh_token:t})});if(!e.ok){const r=await e.json().catch(()=>({}));throw new u("REFRESH_FAILED",r?.error_description||`Token refresh failed (${e.status})`)}const s=await e.json(),a={access_token:s.access_token,refresh_token:s.refresh_token||t,expires_at:Date.now()+s.expires_in*1000};P(a);return a.access_token}async function j(){const t=F();if(!t)throw new u("AUTH_REQUIRED","Not authenticated. Run: ppcli spotify auth");if(!t.access_token||!t.refresh_token||!(t.expires_at>0))throw new u("AUTH_CORRUPTED","Token file is corrupted. Run: ppcli spotify auth");if(Date.now()>t.expires_at-60000)return M(t.refresh_token);return t.access_token}async function o(t,e,s){const a=await j(),r=await fetch(`https://api.spotify.com/v1${e}`,{method:t,headers:{Authorization:`Bearer ${a}`,"Content-Type":"application/json"},body:s?JSON.stringify(s):void 0});if(r.status===204||r.status===202)return null;if(!r.ok){const n=await r.json().catch(()=>({}));throw new u("API_ERROR",n?.error?.message||`Spotify API error ${r.status}`)}return r.json()}async function b(t){const e=await o("GET",`/search?q=${encodeURIComponent(t)}&type=track&limit=1`),s=x(e);if(!s)throw new u("EMPTY_RESULT",`No track found for: ${t}`);return s}function H(t){if(process.platform==="win32")d("cmd",["/c","start","",t]);else if(process.platform==="darwin")d("open",[t]);else d("xdg-open",[t])}i({site:"spotify",name:"auth",access:"write",description:"Authenticate with Spotify (OAuth — run once)",strategy:c.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{L(w,h);return new Promise((t,e)=>{const s=v(async(r,n)=>{try{const p=new URL(r.url,"http://localhost:8888");if(p.pathname!=="/callback"){n.end();return}const g=p.searchParams.get("code");if(!g){n.end("Missing code");return}const l=await fetch("https://accounts.spotify.com/api/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Authorization:"Basic "+Buffer.from(`${k}:${T}`).toString("base64")},body:new URLSearchParams({grant_type:"authorization_code",code:g,redirect_uri:S})});if(!l.ok){const E=await l.json().catch(()=>({}));s.close();e(new u("AUTH_FAILED",E?.error_description||`Token exchange failed (${l.status})`));return}const f=await l.json();P({access_token:f.access_token,refresh_token:f.refresh_token,expires_at:Date.now()+f.expires_in*1000});n.writeHead(200,{"Content-Type":"text/html"});n.end("<h2>Spotify authenticated! You can close this tab.</h2>");s.close();t([{status:"Authenticated successfully"}])}catch(p){s.close();e(p)}});s.on("error",(r)=>{if(r.code==="EADDRINUSE")e(new u("PORT_IN_USE","Port 8888 is already in use. Stop the other process and retry."));else e(r)});const a=setTimeout(()=>{s.close();e(new u("AUTH_TIMEOUT","Authentication timed out after 5 minutes"))},300000);s.listen(8888,()=>{const r=`https://accounts.spotify.com/authorize?${new URLSearchParams({client_id:k,response_type:"code",redirect_uri:S,scope:D})}`;console.log("Opening browser for Spotify login...");console.log("If it does not open, visit:",r);H(r)});s.on("close",()=>clearTimeout(a))})}});i({site:"spotify",name:"status",access:"read",description:"Show current playback status",strategy:c.PUBLIC,browser:!1,args:[],columns:["track","artist","album","status","progress"],func:async()=>{const t=await o("GET","/me/player");if(!t||!t.item)return[{track:"Nothing playing",artist:"",album:"",status:"",progress:""}];const e=t.item;if(e.type!=="track")return[{track:e.name,artist:"",album:e.show?.name??"",status:t.is_playing?"playing":"paused",progress:""}];const s=(t.progress_ms??0)/1000|0,a=e.duration_ms/1000|0,r=(n)=>`${n/60|0}:${String(n%60).padStart(2,"0")}`;return[{track:e.name,artist:e.artists.map((n)=>n.name).join(", "),album:e.album.name,status:t.is_playing?"playing":"paused",progress:`${r(s)} / ${r(a)}`}]}});i({site:"spotify",name:"play",access:"write",description:"Resume playback or search and play a track/artist",strategy:c.PUBLIC,browser:!1,args:[{name:"query",type:"str",default:"",positional:!0,help:"Track or artist to play (optional)"}],columns:["track","artist","status"],func:async(t)=>{if(t.query){const{uri:e,name:s,artist:a}=await b(t.query);await o("PUT","/me/player/play",{uris:[e]});return[{track:s,artist:a,status:"playing"}]}await o("PUT","/me/player/play");return[{track:"",artist:"",status:"resumed"}]}});i({site:"spotify",name:"pause",access:"write",description:"Pause playback",strategy:c.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{await o("PUT","/me/player/pause");return[{status:"paused"}]}});i({site:"spotify",name:"next",access:"write",description:"Skip to next track",strategy:c.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{await o("POST","/me/player/next");return[{status:"skipped to next"}]}});i({site:"spotify",name:"prev",access:"write",description:"Skip to previous track",strategy:c.PUBLIC,browser:!1,args:[],columns:["status"],func:async()=>{await o("POST","/me/player/previous");return[{status:"skipped to previous"}]}});i({site:"spotify",name:"volume",access:"write",description:"Set playback volume (0-100)",strategy:c.PUBLIC,browser:!1,args:[{name:"level",type:"int",default:50,positional:!0,required:!0,help:"Volume 0–100"}],columns:["volume"],func:async(t)=>{const e=Math.round(t.level);if(e<0||e>100)throw new u("INVALID_ARGS","Volume must be between 0 and 100");await o("PUT",`/me/player/volume?volume_percent=${e}`);return[{volume:`${e}%`}]}});i({site:"spotify",name:"search",access:"read",description:"Search for tracks",strategy:c.PUBLIC,browser:!1,args:[{name:"query",type:"str",required:!0,positional:!0,help:"Search query"},{name:"limit",type:"int",default:10,help:"Number of results (default: 10)"}],columns:["track","artist","album","uri"],func:async(t)=>{const e=Math.min(50,Math.max(1,Math.round(t.limit))),s=await o("GET",`/search?q=${encodeURIComponent(t.query)}&type=track&limit=${e}`),a=A(s);if(!a.length)throw new u("EMPTY_RESULT",`No results found for: ${t.query}`);return a}});i({site:"spotify",name:"queue",access:"write",description:"Add a track to the playback queue",strategy:c.PUBLIC,browser:!1,args:[{name:"query",type:"str",required:!0,positional:!0,help:"Track to add to queue"}],columns:["track","artist","status"],func:async(t)=>{const{uri:e,name:s,artist:a}=await b(t.query);await o("POST",`/me/player/queue?uri=${encodeURIComponent(e)}`);return[{track:s,artist:a,status:"added to queue"}]}});i({site:"spotify",name:"shuffle",access:"write",description:"Toggle shuffle on/off",strategy:c.PUBLIC,browser:!1,args:[{name:"state",type:"str",default:"on",positional:!0,choices:["on","off"],help:"on or off"}],columns:["shuffle"],func:async(t)=>{await o("PUT",`/me/player/shuffle?state=${t.state==="on"}`);return[{shuffle:t.state}]}});i({site:"spotify",name:"repeat",access:"write",description:"Set repeat mode (off / track / context)",strategy:c.PUBLIC,browser:!1,args:[{name:"mode",type:"str",default:"context",positional:!0,choices:["off","track","context"],help:"off / track / context"}],columns:["repeat"],func:async(t)=>{await o("PUT",`/me/player/repeat?state=${t.mode}`);return[{repeat:t.mode}]}});
@@ -0,0 +1 @@
1
+ import{registerWatch as o}from"../_shared/watch.js";import{DM_REQUEST_QUERY as d,buildQuery as c,getDmAuth as r,dmApiFetch as m,shapeDmMessages as u}from"./dm-shared.js";const l=`https://x.com/i/api/1.1/dm/inbox_initial_state.json?${c(d)}`;function a(n,t){if(!n||typeof n!=="object")return[];const i=String(t?.state?.ownId??""),s=[];for(const e of u(n.entries,n.users||{})){if(!e.id)continue;if(!t?.includeOwn&&i&&String(e.sender_id??"")===i)continue;s.push({event:"dm",id:String(e.id),ts:e.time?Date.parse(e.time):Date.now(),conversation_id:e.conversation_id??"",sender:{id:e.sender_id??"",name:e.sender??""},text:e.text??"",url:e.conversation_id?`https://x.com/messages/${e.conversation_id}`:"https://x.com/messages",raw:{media_url:e.media_url??null}})}return s}o({site:"twitter",name:"watch",domain:"x.com",mode:"reconcile",description:"X 私信监听:按间隔拉取收件箱,收到新私信按 NDJSON 逐条输出(X 新版私信没有可旁听的实时通道,见文件头说明)",watchUrl:"https://x.com/messages",pattern:"dm/inbox_initial_state|dm/user_updates|/live_pipeline/",parse:(n,t)=>{if(!String(n?.url??"").includes("inbox_initial_state"))return null;let i;try{i=JSON.parse(String(n.responsePreview??""))}catch{return null}return a(i?.inbox_initial_state,t)},onAttach:async(n,t)=>{const{ownId:i,headers:s}=await r(n);t.state.ownId=i;t.state.headers=s},reconcile:async(n,t)=>{if(!t.state.headers){const{ownId:s,headers:e}=await r(n);t.state.ownId=s;t.state.headers=e}const i=await m(n,l,t.state.headers);if(i?.error)return[];return a(i?.inbox_initial_state,t)}});
@@ -0,0 +1,22 @@
1
+ import{registerWatch as r}from"../_shared/watch.js";const i=`
2
+ (function () {
3
+ var cells = Array.prototype.slice.call(document.querySelectorAll('#Main .box .cell[id^="n_"]'));
4
+ return JSON.stringify(cells.map(function (cell) {
5
+ var payloadEl = cell.querySelector('.payload');
6
+ var payload = payloadEl ? (payloadEl.textContent || '').trim() : '';
7
+ var memberLink = cell.querySelector('a[href^="/member/"]');
8
+ var topicLink = cell.querySelector('a[href*="/t/"]');
9
+ var timeEl = cell.querySelector('.snow');
10
+ var full = (cell.textContent || '').replace(/\\s+/g, ' ').trim();
11
+ return {
12
+ id: cell.id || '',
13
+ sender: memberLink ? (memberLink.getAttribute('href') || '').replace('/member/', '') : '',
14
+ payload: payload,
15
+ summary: full,
16
+ url: topicLink ? topicLink.getAttribute('href') : '',
17
+ time: timeEl ? (timeEl.textContent || '').trim() : '',
18
+ kind: payload ? 'reply' : 'notification'
19
+ };
20
+ }));
21
+ })()
22
+ `;function l(t){if(!Array.isArray(t))return[];return t.filter((e)=>e&&e.id).map((e)=>({event:e.kind==="reply"?"reply":"notification",id:String(e.id),ts:Date.now(),conversation_id:String(e.url||""),sender:{id:String(e.sender||""),name:String(e.sender||"")},text:String(e.payload||e.summary||"").slice(0,2000),url:e.url?`https://www.v2ex.com${e.url}`:"https://www.v2ex.com/notifications",raw:{relative_time:e.time||""}}))}r({site:"v2ex",name:"watch",domain:"www.v2ex.com",mode:"reconcile",description:"V2EX 提醒监听:按间隔拉取提醒页,有新的回复/感谢/收藏/提及按 NDJSON 逐条输出",watchUrl:"https://www.v2ex.com/notifications",pattern:"/notifications",parse:()=>null,reconcile:async(t)=>{await t.goto("https://www.v2ex.com/notifications",{waitUntil:"load"});const e=await t.evaluate(i);try{return l(JSON.parse(typeof e==="string"?e:"[]"))}catch{return[]}}});
@@ -0,0 +1 @@
1
+ import{registerWatch as i}from"../_shared/watch.js";const o="https://weibo.com/ajax/statuses/mentions?page=1",m="https://weibo.com/ajax/comments/mentions?page=1";function n(e){return String(e??"").replace(/<[^>]+>/g,"").replace(/\s+/g," ").trim()}function s(e){const r=e?.data?.statuses;if(!Array.isArray(r))return[];return r.filter((t)=>t?.idstr||t?.id).map((t)=>({event:"mention",id:`status:${t.idstr??t.id}`,ts:t.created_at?Date.parse(t.created_at)||Date.now():Date.now(),conversation_id:String(t.idstr??t.id??""),sender:{id:String(t.user?.idstr??""),name:String(t.user?.screen_name??"")},text:n(t.text_raw??t.text),url:t.user?.idstr&&t.mblogid?`https://weibo.com/${t.user.idstr}/${t.mblogid}`:"https://weibo.com/at/weibo",raw:{source:"mentions"}}))}function a(e){const r=e?.data?.comments;if(!Array.isArray(r))return[];return r.filter((t)=>t?.idstr||t?.id).map((t)=>({event:"comment",id:`comment:${t.idstr??t.id}`,ts:t.created_at?Date.parse(t.created_at)||Date.now():Date.now(),conversation_id:String(t.status?.idstr??t.rootidstr??""),sender:{id:String(t.user?.idstr??""),name:String(t.user?.screen_name??"")},text:n(t.text_raw??t.text),url:"https://weibo.com/at/comment",raw:{source:"comments",on_status:t.status?.text_raw?n(t.status.text_raw).slice(0,80):""}}))}i({site:"weibo",name:"watch",domain:"weibo.com",mode:"reconcile",description:"微博互动监听:按间隔拉取「@我的」和「评论我的」,有新互动按 NDJSON 逐条输出(不含私信)",watchUrl:"https://weibo.com/",pattern:"/ajax/statuses/mentions|/ajax/comments/mentions",parse:(e)=>{const r=String(e?.url??"");let t;try{t=JSON.parse(String(e.responsePreview??""))}catch{return null}if(r.includes("/ajax/statuses/mentions"))return s(t);if(r.includes("/ajax/comments/mentions"))return a(t);return null},reconcile:async(e)=>{const[r,t]=await Promise.all([e.fetchJson(o).catch(()=>null),e.fetchJson(m).catch(()=>null)]);return[...s(r),...a(t)]}});
@@ -0,0 +1 @@
1
+ import{registerWatch as s}from"../_shared/watch.js";import{buildExtractInboxEvaluate as u,buildInboxUrl as n,DEFAULT_INBOX_LIMIT as l,requireEvaluateObject as m}from"./im.js";function d(e){let r=5381;for(let i=0;i<e.length;i++)r=(r<<5)+r+e.charCodeAt(i)>>>0;return r.toString(36)}function w(e,r){if(!Array.isArray(e))return[];const i=[];for(const t of e){const o=Number(t?.unread_count??0)||(t?.unread?1:0);if(o<=0&&!r?.includeOwn)continue;const a=String(t?.peer_user_id||t?.peer_name||""),c=String(t?.last_message||"");i.push({event:"dm",id:d(`${a}|${t?.item_id||""}|${c}`),ts:Date.now(),conversation_id:String(t?.item_id||a),sender:{id:String(t?.peer_user_id||""),name:String(t?.peer_name||"")},text:c,url:String(t?.url||n()),raw:{unread_count:o,item_title:t?.item_title||"",price:t?.price||""}})}return i}s({site:"xianyu",name:"watch",domain:"www.goofish.com",mode:"reconcile",description:"闲鱼私信监听:按间隔读会话列表,有未读私信按 NDJSON 逐条输出",watchUrl:n(),pattern:"never-match-stream",parse:()=>null,reconcile:async(e,r)=>{const i=await(e.getCurrentUrl?e.getCurrentUrl().catch(()=>""):Promise.resolve(""));if(!/https:\/\/www\.goofish\.com\/im\b/.test(String(i||"")))await e.goto(n());await e.wait(4);const t=m(await e.evaluate(u(l)),"watch inbox");if(t?.requiresAuth||t?.blocked)return[];return w(t?.items,r)}});