publishport-opencli 1.0.25 → 1.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli-manifest.json +28 -10
- package/clis/_shared/watch.js +3 -3
- package/clis/nowcoder/comments.js +1 -1
- package/clis/nowcoder/detail.js +4 -2
- package/clis/nowcoder/dm-list.js +1 -1
- package/clis/nowcoder/dm-read.js +1 -1
- package/clis/nowcoder/dm-send.js +1 -1
- package/clis/nowcoder/drafts.js +1 -1
- package/clis/nowcoder/experience.js +4 -2
- package/clis/nowcoder/inbox.js +1 -1
- package/clis/nowcoder/my.js +1 -1
- package/clis/nowcoder/recommend.js +1 -1
- package/clis/nowcoder/search.js +3 -2
- package/clis/nowcoder/utils.js +16 -19
- package/clis/nowcoder/watch.js +1 -1
- package/dist/src/execution.js +2 -2
- package/dist/src/pipeline/template.js +3 -1
- package/package.json +1 -1
package/cli-manifest.json
CHANGED
|
@@ -29530,7 +29530,7 @@
|
|
|
29530
29530
|
{
|
|
29531
29531
|
"site": "nowcoder",
|
|
29532
29532
|
"name": "dm-read",
|
|
29533
|
-
"description": "读某私信会话的消息(target
|
|
29533
|
+
"description": "读某私信会话的消息(target 可填 dm-list 的 conversation_id 或 target_user_id)",
|
|
29534
29534
|
"access": "read",
|
|
29535
29535
|
"domain": "www.nowcoder.com",
|
|
29536
29536
|
"strategy": "cookie",
|
|
@@ -29541,7 +29541,7 @@
|
|
|
29541
29541
|
"type": "str",
|
|
29542
29542
|
"required": true,
|
|
29543
29543
|
"positional": true,
|
|
29544
|
-
"help": "
|
|
29544
|
+
"help": "dm-list 的 conversation_id,或对方 userId"
|
|
29545
29545
|
},
|
|
29546
29546
|
{
|
|
29547
29547
|
"name": "page",
|
|
@@ -29549,11 +29549,22 @@
|
|
|
29549
29549
|
"default": 1,
|
|
29550
29550
|
"required": false,
|
|
29551
29551
|
"help": "页码"
|
|
29552
|
+
},
|
|
29553
|
+
{
|
|
29554
|
+
"name": "page-size",
|
|
29555
|
+
"type": "int",
|
|
29556
|
+
"default": 15,
|
|
29557
|
+
"required": false,
|
|
29558
|
+
"help": "每页条数"
|
|
29552
29559
|
}
|
|
29553
29560
|
],
|
|
29554
29561
|
"columns": [
|
|
29562
|
+
"message_id",
|
|
29563
|
+
"direction",
|
|
29555
29564
|
"from_user_id",
|
|
29556
|
-
"
|
|
29565
|
+
"type",
|
|
29566
|
+
"content",
|
|
29567
|
+
"created_at"
|
|
29557
29568
|
],
|
|
29558
29569
|
"type": "js",
|
|
29559
29570
|
"modulePath": "nowcoder/dm-read.js",
|
|
@@ -29563,18 +29574,18 @@
|
|
|
29563
29574
|
{
|
|
29564
29575
|
"site": "nowcoder",
|
|
29565
29576
|
"name": "dm-send",
|
|
29566
|
-
"description": "发私信(
|
|
29577
|
+
"description": "发私信(target 可填对方 userId 或 dm-list 的 conversation_id)",
|
|
29567
29578
|
"access": "write",
|
|
29568
29579
|
"domain": "www.nowcoder.com",
|
|
29569
29580
|
"strategy": "cookie",
|
|
29570
29581
|
"browser": true,
|
|
29571
29582
|
"args": [
|
|
29572
29583
|
{
|
|
29573
|
-
"name": "
|
|
29584
|
+
"name": "target",
|
|
29574
29585
|
"type": "str",
|
|
29575
29586
|
"required": true,
|
|
29576
29587
|
"positional": true,
|
|
29577
|
-
"help": "
|
|
29588
|
+
"help": "对方 userId(dm-list 的 target_user_id),也接受 conversation_id"
|
|
29578
29589
|
},
|
|
29579
29590
|
{
|
|
29580
29591
|
"name": "content",
|
|
@@ -29594,7 +29605,8 @@
|
|
|
29594
29605
|
}
|
|
29595
29606
|
],
|
|
29596
29607
|
"columns": [
|
|
29597
|
-
"
|
|
29608
|
+
"message_id",
|
|
29609
|
+
"receiver",
|
|
29598
29610
|
"status"
|
|
29599
29611
|
],
|
|
29600
29612
|
"type": "js",
|
|
@@ -29805,7 +29817,7 @@
|
|
|
29805
29817
|
{
|
|
29806
29818
|
"site": "nowcoder",
|
|
29807
29819
|
"name": "my",
|
|
29808
|
-
"description": "
|
|
29820
|
+
"description": "我发布的内容列表(动态 + 长文,含可供 delete 使用的 id)",
|
|
29809
29821
|
"access": "read",
|
|
29810
29822
|
"domain": "www.nowcoder.com",
|
|
29811
29823
|
"strategy": "cookie",
|
|
@@ -29820,9 +29832,15 @@
|
|
|
29820
29832
|
}
|
|
29821
29833
|
],
|
|
29822
29834
|
"columns": [
|
|
29823
|
-
"
|
|
29835
|
+
"content_id",
|
|
29836
|
+
"kind",
|
|
29824
29837
|
"title",
|
|
29825
|
-
"content"
|
|
29838
|
+
"content",
|
|
29839
|
+
"url",
|
|
29840
|
+
"views",
|
|
29841
|
+
"likes",
|
|
29842
|
+
"comments",
|
|
29843
|
+
"created_at"
|
|
29826
29844
|
],
|
|
29827
29845
|
"type": "js",
|
|
29828
29846
|
"modulePath": "nowcoder/my.js",
|
package/clis/_shared/watch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{existsSync as
|
|
2
|
-
`)}function u(e,t,r={}){
|
|
1
|
+
import{existsSync as ee,mkdirSync as te,readFileSync as re,writeFileSync as ne}from"node:fs";import{homedir as ae}from"node:os";import{dirname as ie,join as G}from"node:path";import{cli as se,Strategy as oe}from"@jackwener/opencli/registry";import{AuthRequiredError as J,CommandExecutionError as ce}from"@jackwener/opencli/errors";const P=1000,B=300,j=60,D=30,z=2000,le=20;function ue(){return G(ae(),".ppcli","watch")}function H(e,t){const r=String(process.env.PPCLI_ACTIVE_PROFILE??"").trim();return G(ue(),`${e}${r?`@${r}`:""}.${t}.seen.json`)}function de(e,t){const r=H(e,t);try{if(!ee(r))return{ids:[],set:new Set,lastTs:0,firstRun:!0};const i=JSON.parse(re(r,"utf-8")),o=Array.isArray(i?.ids)?i.ids.map(String):[],c=Number.isFinite(i?.lastTs)?Number(i.lastTs):0;return{ids:o,set:new Set(o),lastTs:c,firstRun:c<=0}}catch{return{ids:[],set:new Set,lastTs:0,firstRun:!0}}}function E(e,t,r){const i=H(e,t);try{te(ie(i),{recursive:!0});ne(i,JSON.stringify({ids:r.ids.slice(-z),lastTs:r.lastTs||0}),"utf-8")}catch{}}function fe(e,t){if(e.set.has(t))return!1;e.set.add(t);e.ids.push(t);while(e.ids.length>z){const r=e.ids.shift();e.set.delete(r)}return!0}function R(e){process.stdout.write(`${JSON.stringify(e)}
|
|
2
|
+
`)}function u(e,t,r={}){R({type:"status",site:e,state:t,ts:Date.now(),...r})}function K(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 V(e){if(!e)return[];return Array.isArray(e)?e.filter(Boolean):[e]}function Q(e){try{return new URL(e).origin}catch{return""}}function pe(e){return`(function(){
|
|
3
3
|
if (window.__pp_stream_hooked) return;
|
|
4
4
|
window.__pp_stream_hooked = true;
|
|
5
5
|
var PATTERNS = ${JSON.stringify(String(e||"").split("|").map((r)=>r.trim()).filter(Boolean))};
|
|
@@ -56,4 +56,4 @@ import{existsSync as Q,mkdirSync as X,readFileSync as Y,writeFileSync as Z}from"
|
|
|
56
56
|
WrappedES.prototype = NativeES.prototype;
|
|
57
57
|
window.EventSource = WrappedES;
|
|
58
58
|
}
|
|
59
|
-
})()`}async function
|
|
59
|
+
})()`}async function me(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=Q(e.watchUrl);se({site:r,name:t,access:"read",description:e.description??`长驻监听${r}的新消息,旁听页面自身的实时通道并按 NDJSON 逐条输出(不对平台发轮询请求)`,domain:e.domain,strategy:oe.COOKIE,browser:!0,navigateBefore:!1,sessionKey:"watch",siteSession:"persistent",defaultWindowMode:"background",args:[{name:"interval",type:"int",required:!1,default:P,help:`本机取帧间隔毫秒(默认 ${P})。纯本机 IPC,不发平台请求`},{name:"reconcile-interval",type:"int",required:!1,default:0,help:`对账间隔秒(0 = 用该平台的默认值:可旁听实时通道的平台 ${B} 秒兜底,只能靠拉取的平台 ${j} 秒一轮);-1 = 彻底关闭对账`},{name:"heartbeat",type:"int",required:!1,default:D,help:`心跳间隔秒(默认 ${D}),无事件时输出一行 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,c)=>{const X=Math.max(200,Number(c.interval??P)),k=e.mode==="reconcile"?"reconcile":"stream",_=Number(c["reconcile-interval"]??0),b=_<0?0:_>0?_:k==="reconcile"?j:B,U=Math.max(0,Number(c.heartbeat??D)),x=c.once===!0||c.once==="true",S=c["replay-seen"]===!0||c["replay-seen"]==="true",T=c.backfill===!0||c.backfill==="true",Y=c["dump-frames"]===!0||c["dump-frames"]==="true",h=String(c.events??"").split(",").map((l)=>l.trim()).filter(Boolean);if(typeof o.startNetworkCapture!=="function"||typeof o.readNetworkCapture!=="function")throw new ce("当前浏览器通道不支持网络捕获,watch 无法工作(需要 CDP 或扩展路径)");const d=de(r,t),C={site:r,page:o,state:{},includeOwn:c["include-own"]===!0||c["include-own"]==="true"};let p=0,W=0,y=!1;const g=[];let m=d.firstRun&&!T&&!S,O=!1;const w=d.lastTs,F=(l)=>{const a=K(r,l);if(!a)return!1;if(h.length&&!h.includes(a.event))return!1;if(!S&&!fe(d,`${a.event}:${a.id}`))return!1;if(a.ts>d.lastTs)d.lastTs=a.ts;if(m){W++;return!1}if(!T&&!S&&w>0&&a.ts>0&&a.ts<=w){W++;return!1}if(!x)R(a);g.push(a);p++;return!0},Z=(l)=>{let a=0;for(const n of l){const s=K(r,n);if(!s)continue;if(h.length&&!h.includes(s.event))continue;if(d.set.has(`${s.event}:${s.id}`))continue;if(w>0&&s.ts>0&&s.ts<=w)continue;a++}return a},v=async(l,a=!1)=>{if(typeof e.reconcile!=="function")return;try{const n=V(await e.reconcile(o,C));if(a&&!m&&!T&&!S&&Z(n)>le){m=!0;O=!0}let s=0;for(const f of n)if(F(f))s++;u(r,"reconciled",{reason:l,scanned:n.length,new:s})}catch(n){if(n instanceof J)throw n;u(r,"reconcile_failed",{reason:l,error:n?.message??String(n)})}},I=async(l)=>{let a=!1;try{a=await o.startNetworkCapture(e.pattern??"")!==!1}catch{a=!1}const n=pe(e.pattern??"");y=!1;let s=!1;if(typeof o.cdp==="function")try{await o.cdp("Page.enable",{});await o.cdp("Page.addScriptToEvaluateOnNewDocument",{source:n});s=!0;y=!0}catch{}await o.goto(e.watchUrl,{waitUntil:s?"load":"none"});if(!s)try{await o.evaluate(n);y=!0;u(r,"page_hook_late",{note:"浏览器扩展版本较旧,旁听脚本只能在导航后注入;页面在此之前已建立的连接要等它重连才能被看到"})}catch(f){u(r,"page_hook_unavailable",{error:f?.message??String(f)})}if(!a){await o.startNetworkCapture(e.pattern??"");await o.goto(e.watchUrl,{waitUntil:"load"})}if(typeof e.onAttach==="function")await e.onAttach(o,C);u(r,"watching",{reason:l,mode:k,url:e.watchUrl,pattern:e.pattern??"",reconcile_seconds:b})};await I("start");await v("startup",!0);if(m){E(r,t,d);u(r,"seeded",{seeded:W,desynced:O,note:O?"去重表和收件箱对不上(上次没跑完 / 换了账号 / 表被裁剪),本轮把当前收件箱重新登记为「已见过」,不触发任何事件。从现在起只有新消息才会上报":"首次运行:已把当前收件箱登记为「已见过」,不触发任何事件。从现在起只有新消息才会上报(要补历史请加 --backfill)"});m=!1}if(x){E(r,t,d);return g}let A=!1;const N=()=>{A=!0};process.on("SIGINT",N);process.on("SIGTERM",N);let M=Date.now(),L=Date.now(),q=p;try{while(!A){await new Promise((n)=>setTimeout(n,X));if(A)break;let l=[];try{l=await o.readNetworkCapture()??[];if(y)l=l.concat(await me(o))}catch(n){u(r,"reattaching",{error:n?.message??String(n)});try{await I("capture_lost");await v("reattach")}catch(s){if(s instanceof J)throw s;u(r,"reattach_failed",{error:s?.message??String(s)})}continue}for(const n of l){if(Y)R({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 s;try{s=await e.parse(n,C)}catch(f){u(r,"parse_failed",{url:n?.url??"",error:f?.message??String(f)});continue}for(const f of V(s))F(f)}if(p!==q){E(r,t,d);q=p;L=Date.now()}if(i&&typeof o.getCurrentUrl==="function")try{const n=await o.getCurrentUrl();if(n&&Q(n)!==i){u(r,"page_drifted",{url:n});await I("drift");await v("drift")}}catch{}const a=Date.now();if(b&&a-M>=b*1000){M=a;await v("interval")}if(U&&a-L>=U*1000){L=a;u(r,"alive",{emitted:p})}}}finally{process.off("SIGINT",N);process.off("SIGTERM",N);E(r,t,d);u(r,"stopped",{emitted:p})}return g}})}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}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as i,Strategy as
|
|
1
|
+
import{cli as i,Strategy as d}from"@jackwener/opencli/registry";import{GW_BASE as m,decodeHtmlEntities as a,getJson as c,requireOk as s,resolveMomentId as p}from"./utils.js";i({site:"nowcoder",name:"comments",access:"read",description:"某帖的评论列表(评论前先读,避免重复/不合语境)",domain:"www.nowcoder.com",strategy:d.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(数字 momentId / UUID / 链接均可)"},{name:"page",type:"int",default:1,help:"页码"}],columns:["comment_id","user_id","nickname","content"],func:async(t,n)=>{const o=await p(t,n.id),r=`${m}/comment/list-by-page?entityId=${encodeURIComponent(o)}&entityType=74&order=2&pageNo=${n.page}&toCommentId=0&_=${Date.now()}`;return(s(await c(t,r),"评论列表").data?.records??[]).map((e)=>({comment_id:e.id??"",user_id:e.userBrief?.userId??"",nickname:e.userBrief?.nickname??"",content:a(e.content).trim()}))}});
|
package/clis/nowcoder/detail.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import{cli as t}from"@jackwener/opencli/registry";t({site:"nowcoder",name:"detail",access:"read",description:"Post detail view (supports ID / UUID / URL)",domain:"www.nowcoder.com",args:[{name:"id",positional:!0,required:!0,help:"Post ID, UUID, or URL"}],columns:["title","author","school","content","likes","comments","views","time","location"],pipeline:[{navigate:"https://www.nowcoder.com"},{evaluate:`(async () => {
|
|
2
2
|
const raw = \${{ args.id | json }};
|
|
3
3
|
const base = 'https://gw-c.nowcoder.com';
|
|
4
|
-
|
|
4
|
+
// 牛客把正文里的半角引号按 HTML 实体存库,网页端渲染时才反转义;这里一并解回来,
|
|
5
|
+
// 否则调用方读到的是 "。& 必须放最后,不然 &quot; 会被二次解码成引号。
|
|
6
|
+
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/"/g, '"').replace(/�?39;/g, "'").replace(/'/g, "'").replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
5
7
|
|
|
6
8
|
let id = raw;
|
|
7
9
|
const urlMatch = raw.match(/discuss\\/(\\d+)/);
|
|
@@ -32,7 +34,7 @@ import{cli as t}from"@jackwener/opencli/registry";t({site:"nowcoder",name:"detai
|
|
|
32
34
|
const user = data.userBrief || {};
|
|
33
35
|
const freq = data.frequencyData || {};
|
|
34
36
|
return [{
|
|
35
|
-
title: data.title || '(untitled)',
|
|
37
|
+
title: strip(data.title || '') || '(untitled)',
|
|
36
38
|
author: user.nickname || '',
|
|
37
39
|
school: user.educationInfo || '',
|
|
38
40
|
content: strip(data.content || '').substring(0, 500),
|
package/clis/nowcoder/dm-list.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as o,Strategy as r}from"@jackwener/opencli/registry";import{WWW_BASE as i,
|
|
1
|
+
import{cli as o,Strategy as r}from"@jackwener/opencli/registry";import{WWW_BASE as i,decodeHtmlEntities as c,getJson as d,getSelfUid as l,requireOk as g}from"./utils.js";o({site:"nowcoder",name:"dm-list",access:"read",description:"私信会话列表(conversationId / 最后一条消息 / 未读数)",domain:"www.nowcoder.com",strategy:r.COOKIE,args:[{name:"page",type:"int",default:1,help:"页码"}],columns:["conversation_id","target_user_id","target_name","last_content","unread","allow_send","is_official"],func:async(t,a)=>{const n=await l(t),s=`${i}/sns/message/${n}/get-conversation-list?page=${a.page}`;return(g(await d(t,s),"私信会话列表").data?.conversations??[]).map((e)=>({conversation_id:e.conversationId??"",target_user_id:e.targetUserId??"",target_name:e.targetUserName??"",last_content:c(e.content).replace(/\s+/g," ").slice(0,80),unread:e.unreadMessageCount??0,allow_send:e.allowSendMessage??"",is_official:e.targetIsOfficialAccount??""}))}});
|
package/clis/nowcoder/dm-read.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as s,Strategy as d}from"@jackwener/opencli/registry";import{WWW_BASE as c,decodeHtmlEntities as g,getJson as m,getSelfUid as p,requireOk as l,resolveConversation as u}from"./utils.js";const _={0:"text",1:"image"};s({site:"nowcoder",name:"dm-read",access:"read",description:"读某私信会话的消息(target 可填 dm-list 的 conversation_id 或 target_user_id)",domain:"www.nowcoder.com",strategy:d.COOKIE,args:[{name:"target",positional:!0,required:!0,help:"dm-list 的 conversation_id,或对方 userId"},{name:"page",type:"int",default:1,help:"页码"},{name:"page-size",type:"int",default:15,help:"每页条数"}],columns:["message_id","direction","from_user_id","type","content","created_at"],func:async(n,a)=>{const t=await p(n),{conversationId:i}=await u(n,a.target,t),r=`${c}/sns/message/${t}/get-msg-list?conversationId=${encodeURIComponent(i)}&ownerId=${t}&page=${a.page}&pageSize=${a["page-size"]}&_=${Date.now()}`;return((l(await m(n,r),"私信读取").data??{}).messages??[]).map((e)=>{const o=String(e.send??"");return{message_id:e.id??"",direction:o===String(t)?"out":"in",from_user_id:o,type:_[e.contentType]??String(e.contentType??""),content:g(e.content).trim(),created_at:e.createDate?new Date(e.createDate).toISOString():""}})}});
|
package/clis/nowcoder/dm-send.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as o,Strategy as n}from"@jackwener/opencli/registry";import{ArgumentError as a}from"@jackwener/opencli/errors";import{WWW_BASE as m,getSelfUid as d,postForm as c,requireOk as l,resolveConversation as p}from"./utils.js";import{assertLiteralContent as u}from"../_shared/content-guard.js";o({site:"nowcoder",name:"dm-send",access:"write",description:"发私信(target 可填对方 userId 或 dm-list 的 conversation_id)",domain:"www.nowcoder.com",strategy:n.COOKIE,args:[{name:"target",positional:!0,required:!0,help:"对方 userId(dm-list 的 target_user_id),也接受 conversation_id"},{name:"content",fromFile:!0,positional:!0,required:!0,help:"私信内容"}],columns:["message_id","receiver","status"],func:async(e,i)=>{const t=String(i.content??"").trim();u(t,{label:"私信内容"});if(!t)throw new a("私信内容不能为空");const r=String(i.target??"").trim(),s=/^\d+$/.test(r)?r:(await p(e,r,await d(e))).peerUid;return[{message_id:l(await c(e,`${m}/sns/message/send-msg?_=${Date.now()}`,{type:"0",title:"",content:t,images:"",receiver:s},{Accept:"application/json, text/plain, */*"}),"私信发送").data?.messageId??"",receiver:s,status:"sent"}]}});
|
package/clis/nowcoder/drafts.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as c,Strategy as n}from"@jackwener/opencli/registry";import{GW_BASE as s,decodeHtmlEntities as a,getJson as d,getSelfUid as l,requireOk as p}from"./utils.js";c({site:"nowcoder",name:"drafts",access:"read",description:"我的草稿列表",domain:"www.nowcoder.com",strategy:n.COOKIE,args:[{name:"page",type:"int",default:1,help:"页码"}],columns:["draft_id","title","content"],func:async(e,r)=>{const o=await l(e),i=`${s}/discuss-draft/list?authorId=${o}&pageNo=${r.page}&pageSize=10&entityType=250`;return(p(await d(e,i),"草稿列表").data?.records??[]).map((t)=>({draft_id:t.id??"",title:a(t.title).trim(),content:a(t.content).replace(/\s+/g," ").slice(0,120)}))}});
|
|
@@ -4,12 +4,14 @@ import{cli as e}from"@jackwener/opencli/registry";e({site:"nowcoder",name:"exper
|
|
|
4
4
|
const r = await fetch('https://gw-c.nowcoder.com/api/sparta/home/tab/content?tabId=818&categoryType=1&pageNo=' + page + '&pageSize=' + limit, {credentials: 'include'});
|
|
5
5
|
const d = await r.json();
|
|
6
6
|
if (!d.success) throw new Error(d.msg || 'API failed');
|
|
7
|
+
// 牛客正文里的引号按 HTML 实体存库,网页端渲染才反转义;& 放最后避免二次解码。
|
|
8
|
+
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/"/g, '"').replace(/�?39;/g, "'").replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
7
9
|
return (d.data?.records || []).map((item, i) => ({
|
|
8
10
|
rank: i + 1,
|
|
9
|
-
title: item.contentData?.title || item.momentData?.title || '',
|
|
11
|
+
title: strip(item.contentData?.title || item.momentData?.title || ''),
|
|
10
12
|
author: item.userBrief?.nickname || '',
|
|
11
13
|
school: item.userBrief?.educationInfo || '',
|
|
12
|
-
content: item.momentData?.content || item.contentData?.content || item.momentData?.newContent || item.contentData?.newContent || '',
|
|
14
|
+
content: strip(item.momentData?.content || item.contentData?.content || item.momentData?.newContent || item.contentData?.newContent || ''),
|
|
13
15
|
likes: item.frequencyData?.likeCnt || 0,
|
|
14
16
|
comments: item.frequencyData?.commentCnt || 0,
|
|
15
17
|
views: item.frequencyData?.viewCnt || 0,
|
package/clis/nowcoder/inbox.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as n,Strategy as i}from"@jackwener/opencli/registry";import{GW_BASE as m,
|
|
1
|
+
import{cli as n,Strategy as i}from"@jackwener/opencli/registry";import{GW_BASE as m,decodeHtmlEntities as c,getJson as a,requireOk as s}from"./utils.js";n({site:"nowcoder",name:"inbox",access:"read",description:"评论回复收件箱(谁回复/@了我们,含定向回复所需的评论 id)",domain:"www.nowcoder.com",strategy:i.COOKIE,args:[{name:"page",type:"int",default:1,help:"页码"}],columns:["msg_id","from_user_id","from_nickname","reply_content","reply_comment_id","quoted_ours","url","time"],func:async(t,r)=>{const o=`${m}/message/pc/comment/list?page=${r.page}&pageSize=20`;return(s(await a(t,o),"评论收件箱").data?.records??[]).map((e)=>({msg_id:e.id??"",from_user_id:e.user?.userId??"",from_nickname:e.user?.nickname??"",reply_content:c(e.triggerEntity?.content).trim(),reply_comment_id:e.triggerEntity?.entityId??"",quoted_ours:(e.quote?.content??"").trim(),url:e.pcUrl??"",time:e.createTime??""}))}});
|
package/clis/nowcoder/my.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as d,Strategy as m}from"@jackwener/opencli/registry";import{GW_BASE as l,decodeHtmlEntities as a,getJson as w,getSelfUid as u,requireOk as p}from"./utils.js";d({site:"nowcoder",name:"my",access:"read",description:"我发布的内容列表(动态 + 长文,含可供 delete 使用的 id)",domain:"www.nowcoder.com",strategy:m.COOKIE,args:[{name:"page",type:"int",default:1,help:"页码"}],columns:["content_id","kind","title","content","url","views","likes","comments","created_at"],func:async(c,i)=>{const r=await u(c),s=`${l}/user/content/my-content?pageNo=${i.page}&userId=${r}&_=${Date.now()}`;return(p(await w(c,s),"我的内容").data?.records??[]).map((n)=>{const t=n.contentData??{},e=n.frequencyData??{},o=String(t.id??n.contentId??"");return{content_id:o,kind:t.typeName??"",title:a(t.title).trim(),content:a(t.content).replace(/\s+/g," ").slice(0,120),url:o?`https://www.nowcoder.com/discuss/${o}`:"",views:e.viewCnt??0,likes:e.likeCnt??0,comments:e.totalCommentCnt??e.commentCnt??0,created_at:t.createTime?new Date(t.createTime).toISOString():""}})}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as t,Strategy as e}from"@jackwener/opencli/registry";t({site:"nowcoder",name:"recommend",access:"read",description:"Recommended feed",domain:"www.nowcoder.com",strategy:e.PUBLIC,browser:!1,args:[{name:"page",type:"int",default:1,help:"Page number"},{name:"limit",type:"int",default:15,help:"Number of items"}],columns:["rank","title","author","likes","comments","views","id"],pipeline:[{fetch:{url:"https://gw-c.nowcoder.com/api/sparta/home/recommend?page=${{ args.page }}&size=${{ args.limit }}"}},{select:"data.records"},{map:{rank:"${{ index + 1 }}",title:"${{ item.momentData?.title || item.longContentData?.title || item.contentData?.title || '' | text }}",author:"${{ item.userBrief?.nickname || '' }}",likes:"${{ item.frequencyData?.likeCnt || 0 }}",comments:"${{ item.frequencyData?.commentCnt || 0 }}",views:"${{ item.frequencyData?.viewCnt || 0 }}",content:"${{ item.momentData?.content || item.longContentData?.content || item.contentData?.content || '' | text }}",id:"${{ item.momentData?.uuid || item.longContentData?.uuid || item.contentData?.uuid || item.contentId || '' }}",momentId:"${{ item.contentId || item.momentData?.id || '' }}"}},{filter:"item.title"},{limit:"${{ args.limit }}"}]});
|
package/clis/nowcoder/search.js
CHANGED
|
@@ -2,7 +2,8 @@ import{cli as t}from"@jackwener/opencli/registry";t({site:"nowcoder",name:"searc
|
|
|
2
2
|
const query = \${{ args.query | json }};
|
|
3
3
|
const type = \${{ args.type | json }};
|
|
4
4
|
const limit = \${{ args.limit }};
|
|
5
|
-
|
|
5
|
+
// " 也要解(牛客把正文引号转义存库);& 挪到最后,否则 &quot; 会被二次解码。
|
|
6
|
+
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/"/g, '"').replace(/�?39;/g, "'").replace(/'/g, "'").replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
6
7
|
const r = await fetch('https://gw-c.nowcoder.com/api/sparta/pc/search', {
|
|
7
8
|
method: 'POST',
|
|
8
9
|
credentials: 'include',
|
|
@@ -20,7 +21,7 @@ import{cli as t}from"@jackwener/opencli/registry";t({site:"nowcoder",name:"searc
|
|
|
20
21
|
const id = data.contentId || '';
|
|
21
22
|
return {
|
|
22
23
|
rank: i + 1,
|
|
23
|
-
title: moment.title || contentData.title || user.nickname || '',
|
|
24
|
+
title: strip(moment.title || contentData.title || user.nickname || ''),
|
|
24
25
|
author: user.nickname || '',
|
|
25
26
|
school: user.educationInfo || '',
|
|
26
27
|
content: strip(moment.content || contentData.content || ''),
|
package/clis/nowcoder/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{AuthRequiredError as
|
|
1
|
+
import{AuthRequiredError as d,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,n){const e=JSON.stringify(n);return t.evaluate(`
|
|
2
2
|
async () => {
|
|
3
|
-
const res = await fetch(${
|
|
3
|
+
const res = await fetch(${e}, { 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 a,CommandExecutionError as i}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,
|
|
11
|
+
`)}export async function postJson(t,n,e,r={}){const s=JSON.stringify(n),c=JSON.stringify(JSON.stringify(e)),o=JSON.stringify({"Content-Type":"application/json",...r});return t.evaluate(`
|
|
12
12
|
async () => {
|
|
13
|
-
const res = await fetch(${
|
|
13
|
+
const res = await fetch(${s}, {
|
|
14
14
|
method: 'POST',
|
|
15
15
|
credentials: 'include',
|
|
16
|
-
headers: ${
|
|
17
|
-
body: ${
|
|
16
|
+
headers: ${o},
|
|
17
|
+
body: ${c},
|
|
18
18
|
});
|
|
19
19
|
const text = await res.text();
|
|
20
20
|
try {
|
|
@@ -23,16 +23,13 @@ import{AuthRequiredError as a,CommandExecutionError as i}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,
|
|
26
|
+
`)}export async function postForm(t,n,e,r={}){const s=JSON.stringify(n),c=JSON.stringify(e),o=JSON.stringify({"Content-Type":"application/x-www-form-urlencoded","x-requested-with":"XMLHttpRequest",...r});return t.evaluate(`
|
|
27
27
|
async () => {
|
|
28
|
-
const body = new URLSearchParams(${
|
|
29
|
-
const res = await fetch(${
|
|
28
|
+
const body = new URLSearchParams(${c});
|
|
29
|
+
const res = await fetch(${s}, {
|
|
30
30
|
method: 'POST',
|
|
31
31
|
credentials: 'include',
|
|
32
|
-
headers: {
|
|
33
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
34
|
-
'x-requested-with': 'XMLHttpRequest',
|
|
35
|
-
},
|
|
32
|
+
headers: ${o},
|
|
36
33
|
body: body.toString(),
|
|
37
34
|
});
|
|
38
35
|
const text = await res.text();
|
|
@@ -42,14 +39,14 @@ import{AuthRequiredError as a,CommandExecutionError as i}from"@jackwener/opencli
|
|
|
42
39
|
return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
|
|
43
40
|
}
|
|
44
41
|
}
|
|
45
|
-
`)}export async function postMultipart(t,e,
|
|
42
|
+
`)}export async function postMultipart(t,n,e,r={}){const s=JSON.stringify(n),c=JSON.stringify(e),o=JSON.stringify(r);return t.evaluate(`
|
|
46
43
|
async () => {
|
|
47
44
|
const fd = new FormData();
|
|
48
|
-
for (const [k, v] of Object.entries(${
|
|
49
|
-
const res = await fetch(${
|
|
45
|
+
for (const [k, v] of Object.entries(${c})) fd.append(k, v);
|
|
46
|
+
const res = await fetch(${s}, {
|
|
50
47
|
method: 'POST',
|
|
51
48
|
credentials: 'include',
|
|
52
|
-
headers: ${
|
|
49
|
+
headers: ${o},
|
|
53
50
|
body: fd,
|
|
54
51
|
});
|
|
55
52
|
const text = await res.text();
|
|
@@ -59,11 +56,11 @@ import{AuthRequiredError as a,CommandExecutionError as i}from"@jackwener/opencli
|
|
|
59
56
|
return { code: -1, msg: '非 JSON 响应 (HTTP ' + res.status + '): ' + text.slice(0, 200) };
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
|
-
`)}export function requireOk(t,
|
|
59
|
+
`)}export function requireOk(t,n){if(!t||typeof t!=="object"||Array.isArray(t))throw new i(`牛客 ${n} 接口返回了畸形响应`);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 d("nowcoder.com",`牛客 ${n} 需要登录: ${r}`);throw new i(`牛客 ${n} 失败: ${r} (code=${t.code??"?"})`)}export async function resolveMomentId(t,n){let e=String(n??"").trim();for(const c of[/detail\/([0-9a-fA-F]{20,})/,/discuss\/(\d+)/]){const o=e.match(c);if(o){e=o[1];break}}if(/^\d+$/.test(e))return e;if(!/^[0-9a-fA-F]{20,}$/.test(e))throw new i(`无法从输入解析牛客帖子 id: ${n}`);const s=requireOk(await getJson(t,`${GW_BASE}/detail/moment-data/detail/${e}`),"帖子解析").data?.id;if(!s)throw new i(`牛客帖子 ${e} 的详情缺少数字 momentId`);return String(s)}export async function getSelfUid(t){const n=await t.evaluate(`
|
|
63
60
|
async () => {
|
|
64
61
|
const link = document.querySelector('a[href*="/users/"]');
|
|
65
62
|
const href = link ? (link.getAttribute('href') || '') : '';
|
|
66
63
|
const match = href.match(/\\/users\\/(\\d+)/);
|
|
67
64
|
return match ? match[1] : '';
|
|
68
65
|
}
|
|
69
|
-
`);if(!
|
|
66
|
+
`);if(!n)throw new d("nowcoder.com","导航栏无登录用户链接(未登录)");return n}export function peerUidFromConversationId(t,n){const e=String(t??"").split("_");if(e.length!==2||!e.every((r)=>/^\d+$/.test(r)))return"";if(e[0]===e[1])return e[0];return e.find((r)=>r!==String(n))??""}export async function resolveConversation(t,n,e){const r=String(n??"").trim();if(!r)throw new i("缺少会话标识");if(r.includes("_")){const s=peerUidFromConversationId(r,e);if(!s)throw new i(`无法解析牛客会话 id: ${n}(应形如 <uidA>_<uidB>)`);return{conversationId:r,peerUid:s}}if(!/^\d+$/.test(r))throw new i(`无法解析牛客私信标识: ${n}(应为对方 userId 或 dm-list 的 conversation_id)`);for(let s=1;s<=5;s+=1){const o=requireOk(await getJson(t,`${WWW_BASE}/sns/message/${e}/get-conversation-list?page=${s}&_=${Date.now()}`),"私信会话列表").data?.conversations??[];if(!o.length)break;const a=o.find((u)=>String(u?.targetUserId??"")===r);if(a?.conversationId)return{conversationId:String(a.conversationId),peerUid:r}}throw new i(`在前 5 页会话列表里没找到与用户 ${r} 的会话(对方从未与你私信过时会话尚不存在,先用 dm-send 发起)`)}export function decodeHtmlEntities(t){return String(t??"").replace(/"/g,'"').replace(/�?39;/g,"'").replace(/'/g,"'").replace(/ /g," ").replace(/</g,"<").replace(/>/g,">").replace(/&#(\d+);/g,(n,e)=>String.fromCodePoint(Number(e))).replace(/&/g,"&")}
|
package/clis/nowcoder/watch.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{registerWatch as d}from"../_shared/watch.js";import{WWW_BASE as w,
|
|
1
|
+
import{registerWatch as d}from"../_shared/watch.js";import{WWW_BASE as w,decodeHtmlEntities as m,getJson as g,getSelfUid as c,requireOk as l}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:m(n?.content).trim(),url:"https://www.nowcoder.com/users/notice/privateChat",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(l(await g(t,r),"私信会话列表"),e)}});
|
package/dist/src/execution.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{getRegistry as oe,fullName as g}from"./registry.js";import{pathToFileURL as ie}from"node:url";import*as se from"node:crypto";import*as M from"node:fs";import*as j from"node:os";import*as N from"node:path";import{executePipeline as K}from"./pipeline/index.js";import{adapterLoadError as ae,ArgumentError as d,CommandExecutionError as
|
|
2
|
-
`);try{n?.(o)}catch(s){X.warn(`[trace] Trace export callback failed: ${s instanceof Error?s.message:String(s)}`)}return o}catch(o){X.warn(`[trace] Failed to export trace artifact: ${o instanceof Error?o.message:String(o)}`);return}}export function resolveFileBackedArg(e,r){const t=e.args.find((l)=>l.fromFile);if(!t)return;const n=typeof t.fromFile==="string"?t.fromFile:"file",o=t.positional?`<${t.name}>`:`--${t.name}`,s=r[n]==null?"":String(r[n]).trim(),u=r[t.name]==null?"":String(r[t.name]);if(s){if(u.trim())throw new d(`${o} 和 --${n} 只能二选一,不能同时给`);const l=N.resolve(s.startsWith("~/")?N.join(j.homedir(),s.slice(2)):s);let m;try{m=M.readFileSync(l,"utf-8")}catch(w){throw new d(`--${n} 指向的文件读不到: ${l}(${D(w)})`)}const i=m.trim();if(!i)throw new d(`--${n} 指向的文件内容为空: ${l}`);r[t.name]=i;$e(N.dirname(l));return}if(!u.trim()&&t.requiredUnlessFile)throw new d(`缺少 ${t.name}:${o} 或 --${n} 给其一`)}export function prepareCommandArgs(e,r){const t=coerceAndValidateArgs(e.args,r);resolveFileBackedArg(e,t);e.validateArgs?.(t);return t}const Z=30;function
|
|
1
|
+
import{getRegistry as oe,fullName as g}from"./registry.js";import{pathToFileURL as ie}from"node:url";import*as se from"node:crypto";import*as M from"node:fs";import*as j from"node:os";import*as N from"node:path";import{executePipeline as K}from"./pipeline/index.js";import{adapterLoadError as ae,ArgumentError as d,CommandExecutionError as A,SessionBusyError as ce,attachTraceReceipt as ue,getErrorMessage as D}from"./errors.js";import{shouldUseBrowserSession as le}from"./capabilityRouting.js";import{getBrowserFactory as fe,browserSession as me,runWithTimeout as V,DEFAULT_BROWSER_COMMAND_TIMEOUT as de}from"./runtime.js";import{withBrowserSessionLockIf as pe}from"./browser-session-lock.js";import{resolveProfile as he}from"./browser/profile.js";import{ensureManagedChrome as we}from"./browser/managed-chrome.js";import{generateFingerprintJs as ge}from"./browser/fingerprint.js";import{CDPBridge as ye}from"./browser/index.js";import{clearDaemonRunContext as ve,generateRunId as Se,isUnknownOutcomeError as G,releaseSiteSessionLease as J,setDaemonCommandTimeoutSeconds as be,setDaemonRunContext as Q}from"./browser/daemon-client.js";import{emitHook as _}from"./hooks.js";import{setContentBaseDir as $e}from"./utils.js";import{log as X}from"./logger.js";import{isElectronApp as Ee}from"./electron-apps.js";import{probeCDP as Pe,resolveElectronEndpoint as Ae}from"./launcher.js";import{ObservationSession as Ce,exportObservationSession as xe}from"./observation/index.js";import{resolveAdapterSourcePath as Ie}from"./adapter-source.js";const S=new Map,k=new Map,Re=`${j.homedir()}/.ppcli/clis/`;function Te(e){if(e===void 0||e===null||e===""||e==="off")return"off";if(e==="on"||e==="retain-on-failure")return e;throw new d(`--trace must be one of: off, on, retain-on-failure. Received: "${String(e)}"`)}export function coerceAndValidateArgs(e,r){const t={...r};for(const n of e){const o=t[n.name];if(n.required&&(o===void 0||o===null||o===""))throw new d(`Argument "${n.name}" is required.`,n.help??`Provide a value for --${n.name}`);if(o!==void 0&&o!==null){if(n.type==="int"||n.type==="number"){const u=Number(o);if(!Number.isFinite(u))throw new d(`Argument "${n.name}" must be a valid number. Received: "${o}"`);if(n.type==="int"&&!Number.isInteger(u))throw new d(`Argument "${n.name}" must be a valid integer. Received: "${o}"`);t[n.name]=u}else if(n.type==="boolean"||n.type==="bool")if(typeof o==="string"){const u=o.toLowerCase();if(u==="true"||u==="1")t[n.name]=!0;else if(u==="false"||u==="0")t[n.name]=!1;else throw new d(`Argument "${n.name}" must be a boolean (true/false). Received: "${o}"`)}else t[n.name]=Boolean(o);const s=t[n.name];if(n.choices&&n.choices.length>0){if(!n.choices.map(String).includes(String(s)))throw new d(`Argument "${n.name}" must be one of: ${n.choices.join(", ")}. Received: "${s}"`)}}else if(n.default!==void 0)t[n.name]=n.default}return t}async function B(e,r,t,n){const o=e;if(o._lazy&&o._modulePath){const s=o._modulePath;if(s.startsWith(Re)&&S.has(s))try{const m=M.statSync(s),i=k.get(s);if(i!==void 0&&m.mtimeMs!==i)S.delete(s)}catch{}if(!S.has(s)){const m=ie(s).href,w=import(k.has(s)?`${m}?t=${Date.now()}`:m).then(()=>{try{k.set(s,M.statSync(s).mtimeMs)}catch{}},(p)=>{S.delete(s);throw ae(`Failed to load adapter module ${s}: ${D(p)}`,"Check that the adapter file exists and has no syntax errors.")});S.set(s,w)}await S.get(s);const l=oe().get(g(e));if(l?.func)return Y(l,r,t,n);if(l?.pipeline)return K(r,l.pipeline,{args:t,debug:n})}if(e.func)return Y(e,r,t,n);if(e.pipeline)return K(r,e.pipeline,{args:t,debug:n});throw new A(`Command ${g(e)} has no func or pipeline`,"This is likely a bug in the adapter definition. Please report this issue.")}function Y(e,r,t,n){if(e.browser===!1)return e.func(t,n);if(!r)throw new A(`Command ${g(e)} requires a browser session but none was provided`);return e.func(r,t,n)}function Me(e){if(e.navigateBefore===!1)return null;if(typeof e.navigateBefore==="string")return e.navigateBefore;return null}function Ne(e,r){if(!e||!r)return!1;try{const t=new URL(e).hostname;return t===r||t.endsWith(`.${r}`)}catch{return!1}}function De(e,r){if(!r)return!1;try{const t=new URL(e),n=t.hostname===r||t.hostname.endsWith(`.${r}`),o=t.pathname===""||t.pathname==="/";return n&&o&&t.search===""&&t.hash===""}catch{return!1}}async function _e(e,r,t,n){if(t!=="persistent"||!e.domain)return!0;if(!De(n,e.domain))return!0;const o=await r.getCurrentUrl?.().catch(()=>null);return!Ne(o,e.domain)}export async function executeCommand(e,r,t=!1,n={}){let o;try{o=n.prepared?r:prepareCommandArgs(e,r)}catch(i){if(i instanceof d)throw i;throw new d(D(i))}const s=qe(e,o);be(s);const u=Te(n.trace),l={command:g(e),args:o,startedAt:Date.now()};await _("onBeforeExecute",l);let m;try{if(le(e)){const i=Ee(e.site),w=he(n.profile),p=!i&&w.kind==="managed"?w:null;process.env.PPCLI_ACTIVE_PROFILE=p?p.name:"";let P;const C=We(e.defaultWindowMode??"background",n.windowMode);let F,L;if(p){P=await we(p.name,p.entry,{windowMode:C});const c=p.entry.fingerprint;if(c?.enabled)F=ge(c);const a=p.entry.proxy;if(a?.username)L={username:a.username,password:a.password??""}}else if(i){const c=process.env.OPENCLI_CDP_ENDPOINT;if(c){const a=Number(new URL(c).port);if(!await Pe(a))throw new A(`CDP not reachable at ${c}`,"Check that the app is running with --remote-debugging-port and the endpoint is correct.");P=c}else P=await Ae(e.site)}const re=p?ye:fe(e.site),W=!p&&w.kind==="extension"?w.contextId:void 0,ne=e,b=Ue(e,n.siteSession),x=Oe(e,b),q=Le(b,n.keepTab),y=b==="persistent"&&e.access==="write"?{runId:Se(),session:x}:null;if(y)Q({runId:y.runId,command:g(e),access:"write"});let z,H=!1,I=null;try{m=await pe(C!=="foreground",()=>me(re,async(c)=>{const a=u==="off"?null:new Ce({scope:{contextId:W??p?.name,session:x,target:c.getActivePage?.(),site:e.site,command:g(e),adapterSourcePath:Ie(ne)}});if(a){a.record({stream:"action",name:"command",phase:"start",data:{args:o}});await c.startNetworkCapture?.().catch(()=>!1)}const h=Me(e);if(h&&await _e(e,c,b,h)){a?.record({stream:"action",name:"pre_navigate",phase:"start",data:{url:h}});try{await c.goto(h);a?.record({stream:"action",name:"pre_navigate",phase:"end",data:{url:h}})}catch(f){if(f instanceof ce)throw f;a?.record({stream:"action",name:"pre_navigate",phase:"error",data:{url:h,error:f instanceof Error?f.message:String(f)}});const v=new A(`Pre-navigation to ${h} failed: ${f instanceof Error?f.message:f}`,"Check that the site is reachable and the browser extension is running.");v.cause=f;if(a&&(u==="on"||u==="retain-on-failure")){a.record({stream:"error",message:v.message,stack:v.stack,code:v.code,hint:v.hint});await U(a,c).catch(()=>{});O(a,"failure",v,n.onTraceExport)}throw v}}const E=s!==null?s+Z:de,R=B(e,c,o,t);I=R;let T=!1;R.then(()=>{T=!0},()=>{T=!0});try{const f=await V(R,{timeout:E,label:g(e),hint:te(e,s)});a?.record({stream:"action",name:"command",phase:"end"});if(a&&u==="on"){await U(a,c).catch(()=>{});O(a,"success",void 0,n.onTraceExport)}if(!q)await c.closeWindow?.().catch(()=>{});return f}catch(f){if(!T)H=!0;if(a){a.record({stream:"action",name:"command",phase:"error",data:{error:f instanceof Error?f.message:String(f)}});a.record({stream:"error",message:f instanceof Error?f.message:String(f),stack:f instanceof Error?f.stack:void 0});if(u==="on"||u==="retain-on-failure"){await U(a,c).catch(()=>{});O(a,"failure",f,n.onTraceExport)}}if(!q)await c.closeWindow?.().catch(()=>{});throw f}},{session:x,cdpEndpoint:P,contextId:W,windowMode:C,surface:"adapter",siteSession:b,initScript:F,proxyAuth:L}),p?.name)}catch(c){z=c;throw c}finally{if(y)if(H&&I){const{runId:c,session:a}=y,h=(E)=>{ve(c);if(!G(E))J({runId:c,session:a,surface:"adapter"})};I.then(()=>h(),(E)=>h(E))}else{Q(null);if(!G(z))await J({runId:y.runId,session:y.session,surface:"adapter"})}}}else if(s!==null){const i=s+Z;m=await V(B(e,null,o,t),{timeout:i,label:g(e),hint:te(e,s)})}else m=await B(e,null,o,t)}catch(i){l.error=i;l.finishedAt=Date.now();await _("onAfterExecute",l);throw i}l.finishedAt=Date.now();await _("onAfterExecute",l,m);return m}async function U(e,r){const t=r.getActivePage?.()??e.scope.target,[n,o,s,u,l]=await Promise.all([r.getCurrentUrl?.().catch(()=>null)??Promise.resolve(null),r.snapshot().catch(()=>{return}),r.readNetworkCapture?.().catch(()=>[])??Promise.resolve([]),r.consoleMessages("all").catch(()=>[]),r.screenshot({format:"png"}).catch(()=>{return})]);if(o!==void 0||n!==void 0)e.record({stream:"state",url:n,target:t,snapshot:o,label:"final"});for(const m of Array.isArray(s)?s:[]){const i=m;e.record({stream:"network",url:String(i.url??""),method:typeof i.method==="string"?i.method:void 0,status:typeof i.responseStatus==="number"?i.responseStatus:void 0,contentType:typeof i.responseContentType==="string"?i.responseContentType:void 0,size:typeof i.responseBodyFullSize==="number"?i.responseBodyFullSize:void 0,requestHeaders:i.requestHeaders,responseHeaders:i.responseHeaders,requestBody:i.requestBodyPreview,responseBody:i.responsePreview,ts:typeof i.timestamp==="number"?i.timestamp:void 0})}for(const m of Array.isArray(u)?u:[])if(m&&typeof m==="object"){const i=m;e.record({stream:"console",level:String(i.type??i.level??"log"),text:String(i.text??i.message??""),ts:typeof i.timestamp==="number"?i.timestamp:void 0})}else e.record({stream:"console",level:"log",text:String(m)});if(typeof l==="string"&&l)e.record({stream:"screenshot",format:"png",data:l,label:"final"})}function O(e,r,t,n){try{const o=xe(e,{error:t,status:r});if(r==="failure"&&t!==void 0)ue(t,o.receipt);else process.stderr.write(`ppcli trace artifact: ${o.dir}
|
|
2
|
+
`);try{n?.(o)}catch(s){X.warn(`[trace] Trace export callback failed: ${s instanceof Error?s.message:String(s)}`)}return o}catch(o){X.warn(`[trace] Failed to export trace artifact: ${o instanceof Error?o.message:String(o)}`);return}}export function resolveFileBackedArg(e,r){const t=e.args.find((l)=>l.fromFile);if(!t)return;const n=typeof t.fromFile==="string"?t.fromFile:"file",o=t.positional?`<${t.name}>`:`--${t.name}`,s=r[n]==null?"":String(r[n]).trim(),u=r[t.name]==null?"":String(r[t.name]);if(s){if(u.trim())throw new d(`${o} 和 --${n} 只能二选一,不能同时给`);const l=N.resolve(s.startsWith("~/")?N.join(j.homedir(),s.slice(2)):s);let m;try{m=M.readFileSync(l,"utf-8")}catch(w){throw new d(`--${n} 指向的文件读不到: ${l}(${D(w)})`)}const i=m.trim();if(!i)throw new d(`--${n} 指向的文件内容为空: ${l}`);r[t.name]=i;$e(N.dirname(l));return}if(!u.trim()&&t.requiredUnlessFile)throw new d(`缺少 ${t.name}:${o} 或 --${n} 给其一`)}export function prepareCommandArgs(e,r){const t=coerceAndValidateArgs(e.args,r);resolveFileBackedArg(e,t);e.validateArgs?.(t);return t}const Z=30;function ke(e){if(e===void 0||e===null||e==="")return null;if(e==="ephemeral"||e==="persistent")return e;throw new d(`--site-session must be one of: ephemeral, persistent. Received: "${String(e)}"`)}function Be(){const e=process.env.PUBLISHPORT_SITE_SESSION;if(e==="persistent"||e==="ephemeral")return e;return null}function Ue(e,r){return ke(r)??Be()??e.siteSession??"persistent"}function Oe(e,r){const t=typeof e.sessionKey==="string"&&e.sessionKey.trim()?`:${e.sessionKey.trim()}`:"";if(r==="persistent")return`site:${e.site}${t}`;return`site:${e.site}${t}:${se.randomUUID()}`}function Fe(e,r){if(r===void 0||r==="")return null;if(r==="true")return!0;if(r==="false")return!1;throw new d(`${e} must be one of: true, false. Received: "${String(r)}"`)}function Le(e,r){if(e==="persistent")return!0;return Fe("--keep-tab",r)??!1}function ee(e,r){if(r===void 0||r==="")return null;if(r==="foreground"||r==="background")return r;throw new d(`${e} must be one of: foreground, background. Received: "${String(r)}"`)}function We(e="background",r){return ee("--window",r)??ee("OPENCLI_WINDOW",process.env.OPENCLI_WINDOW)??e}function te(e,r){const t=e.access==="write"?"⚠️ 这是写操作,超时不代表没写成功——请求可能已经到达平台。禁止直接重试:"+"先用该平台的列表/统计命令(stats / lists / articles 等)按标题回查,确认没写进去再重发。 ":"";if(r!==null)return`${t}Pass a higher --timeout value (currently ${r}s)`;if(e.args.some((n)=>n.name==="timeout"))return`${t}Try again, or pass a higher --timeout <seconds>`;return t+"This command has no --timeout option. Retry once; if it keeps timing out, "+"the page or browser bridge is likely stuck — have the user reopen the PublishPort client. "+"The global default can be raised with OPENCLI_BROWSER_COMMAND_TIMEOUT."}function qe(e,r){if(!e.args.some((o)=>o.name==="timeout"))return null;const t=r.timeout;if(t===void 0||t===null||t==="")throw new d(`Argument "timeout" must be a positive integer. Received: "${String(t)}"`);const n=Number(t);if(!Number.isInteger(n)||n<=0)throw new d(`Argument "timeout" must be a positive integer. Received: "${String(t)}"`);return n}
|
|
@@ -1 +1,3 @@
|
|
|
1
|
-
import y from"node:vm";import{isRecord as S}from"../utils.js";import{log as
|
|
1
|
+
import y from"node:vm";import{isRecord as S}from"../utils.js";import{log as b}from"../logger.js";export function render(e,t){if(typeof e!=="string")return e;const i=e.trim(),r=i.match(/^\$\{\{\s*([^}]*(?:\}[^}][^}]*)*)\s*\}\}$/);if(r&&!i.includes("}}-")&&!i.includes("}}${{"))return evalExpr(r[1].trim(),t);const f=i.match(/^\$\{\{\s*([\s\S]*?)\s*\}\}$/);if(f){const s=f[1];if(!s.includes("${{"))return evalExpr(s.trim(),t)}return e.replace(/\$\{\{\s*(.*?)\s*\}\}/g,(s,n)=>String(evalExpr(n.trim(),t)))}export function evalExpr(e,t){const i=t.args??{},r=t.item??{},{data:f,root:s}=t,n=t.index??0,o=e.split(/(?<!\|)\|(?!\|)/).map((a)=>a.trim());if(o.length>1){let a=evalExpr(o[0],t);for(let d=1;d<o.length;d++)a=A(o[d],a);return a}const p=e.match(/^(['"])(.*)\1$/);if(p)return p[2];if(/^\d+(\.\d+)?$/.test(e))return Number(e);const c=resolvePath(e,{args:i,item:r,data:f,root:s,index:n});if(c!==null&&c!==void 0)return c;return O(e,{args:i,item:r,data:f,root:s,index:n})}function A(e,t){const i=e.match(/^(\w+)(?:\((.+)\))?$/);if(!i)return t;const[,r,f]=i,s=f?.replace(/^['"]|['"]$/g,"")??"";switch(r){case"default":{if(t===null||t===void 0||t===""){const n=parseInt(s,10);if(!Number.isNaN(n)&&String(n)===s.trim())return n;return s}return t}case"join":return Array.isArray(t)?t.join(s||", "):t;case"upper":return typeof t==="string"?t.toUpperCase():t;case"lower":return typeof t==="string"?t.toLowerCase():t;case"trim":return typeof t==="string"?t.trim():t;case"truncate":{const n=parseInt(s,10)||50;return typeof t==="string"&&t.length>n?`${t.slice(0,n)}...`:t}case"replace":{if(typeof t!=="string")return t;if(f==null)return t;const n=f.indexOf(",");if(n===-1)return t;const o=(a)=>a.trim().replace(/^['"]|['"]$/g,""),p=o(f.slice(0,n)),c=o(f.slice(n+1));return t.replaceAll(p,c)}case"keys":return t&&typeof t==="object"?Object.keys(t):t;case"length":return Array.isArray(t)?t.length:typeof t==="string"?t.length:t;case"first":return Array.isArray(t)?t[0]:t;case"last":return Array.isArray(t)?t[t.length-1]:t;case"json":return JSON.stringify(t??null);case"text":{if(typeof t!=="string")return t;return t.replace(/<br\s*\/?>/gi,`
|
|
2
|
+
`).replace(/<\/p>/gi,`
|
|
3
|
+
`).replace(/<[^>]+>/g,"").replace(/"/g,'"').replace(/'/g,"'").replace(/ /g," ").replace(/</g,"<").replace(/>/g,">").replace(/&#(\d+);/g,(n,o)=>String.fromCodePoint(Number(o))).replace(/&/g,"&").trim()}case"slugify":return typeof t==="string"?t.toLowerCase().replace(/[^\p{L}\p{N}]+/gu,"-").replace(/^-|-$/g,""):t;case"sanitize":return typeof t==="string"?t.replace(/[<>:"/\\|?*\x00-\x1f]/g,"_"):t;case"ext":{if(typeof t!=="string")return t;const n=t.lastIndexOf("."),o=Math.max(t.lastIndexOf("/"),t.lastIndexOf("\\"));return n>o?t.slice(n):""}case"basename":{if(typeof t!=="string")return t;const n=t.split(/[/\\]/);return n[n.length-1]||t}case"urlencode":return typeof t==="string"?encodeURIComponent(t):t;case"urldecode":return typeof t==="string"?decodeURIComponent(t):t;default:return t}}export function resolvePath(e,t){const i=t.args??{},r=t.item??{},{data:f,root:s}=t,n=t.index??0,o=e.split("."),p=o[0];let c,a;if(p==="args"){c=i;a=o.slice(1)}else if(p==="item"){c=r;a=o.slice(1)}else if(p==="data"){c=f;a=o.slice(1)}else if(p==="root"){c=s;a=o.slice(1)}else if(p==="index")return n;else{c=r;a=o}for(const d of a)if(S(c))c=c[d];else if(Array.isArray(c)&&/^\d+$/.test(d))c=c[parseInt(d,10)];else return null;return c}const C=/\b(constructor|__proto__|prototype|globalThis|process|require|import|eval)\b/,h=new WeakMap;function l(e){if(e===null||e===void 0)return e;if(typeof e!=="object"&&typeof e!=="function")return e;const t=e,i=h.get(t);if(i!==void 0)return JSON.parse(i);try{const r=JSON.stringify(e,(f,s)=>typeof s==="bigint"?s.toString():s);h.set(t,r);return JSON.parse(r)}catch(r){b.warn(`[pipeline/template] sanitizeContext failed: ${r instanceof Error?r.message:String(r)}. Returning {} for this branch. Likely cause: circular reference, Symbol, or other non-serializable value in pipeline context.`);return{}}}const _=256,g=new Map;function N(e){let t=g.get(e);if(t)return t;if(g.size>=_){const i=g.keys().next().value;if(i!==void 0)g.delete(i)}t=new y.Script(`(${e})`);g.set(e,t);return t}let m=null,u=null;function I(){if(m&&u)return{sandbox:m,context:u};m={args:{},item:{},data:null,root:null,index:0,encodeURIComponent,decodeURIComponent,JSON,Math,Number,String,Boolean,Array,Date};u=y.createContext(m,{codeGeneration:{strings:!1,wasm:!1}});return{sandbox:m,context:u}}const x=new Set(["args","item","data","root","index","encodeURIComponent","decodeURIComponent","JSON","Math","Number","String","Boolean","Array","Date"]);function O(e,t){if(e.length>2000)return;if(C.test(e))return;try{const i=N(e),{sandbox:r,context:f}=I();for(const s of Object.keys(r))if(!x.has(s))delete r[s];r.args=l(t.args??{});r.item=l(t.item??{});r.data=l(t.data);r.root=l(t.root);r.index=t.index??0;return i.runInContext(f,{timeout:50})}catch{return}}export function normalizeEvaluateSource(e){const t=e.trim();if(!t)return"() => undefined";if(t.startsWith("(")&&t.endsWith(")()"))return`() => (${t})`;if(/^(async\s+)?\([^)]*\)\s*=>/.test(t))return t;if(/^(async\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=>/.test(t))return t;if(t.startsWith("function ")||t.startsWith("async function "))return t;return`() => (${t})`}
|