publishport-opencli 1.0.42 → 1.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli-manifest.json CHANGED
@@ -52119,7 +52119,7 @@
52119
52119
  {
52120
52120
  "site": "xianyu",
52121
52121
  "name": "watch",
52122
- "description": "闲鱼私信监听:旁听网页版 IM 自己的 WebSocket 推送,一有新消息就读已打开的会话列表,按 NDJSON 逐条输出",
52122
+ "description": "闲鱼私信监听:解网页版 IM 长连接上的新消息推送,买家的新消息按 NDJSON 逐条输出",
52123
52123
  "access": "read",
52124
52124
  "domain": "www.goofish.com",
52125
52125
  "strategy": "cookie",
@@ -52137,7 +52137,7 @@
52137
52137
  "type": "int",
52138
52138
  "default": 0,
52139
52139
  "required": false,
52140
- "help": "对账间隔秒(0 = 用该平台的默认值:可旁听实时通道的平台 300 秒兜底,只能靠拉取的平台 60 秒一轮);-1 = 彻底关闭对账"
52140
+ "help": "对账间隔秒(0 = 用该平台的默认值:本平台 180 秒);-1 = 彻底关闭对账"
52141
52141
  },
52142
52142
  {
52143
52143
  "name": "quiet-seconds",
@@ -63,6 +63,8 @@ import{existsSync as ue,mkdirSync as le,readFileSync as de,writeFileSync as fe}f
63
63
  ws.addEventListener('message', function(ev){ push('websocket','received',url,ev.data); });
64
64
  // 记下最近一次连接关闭的原因:静默看门狗报 quiet 时带上它,分得清是平台踢的还是页面自己断的。
65
65
  if (want(url)) ws.addEventListener('close', function(ev){ window.__pp_stream_closed = { code: ev.code, reason: String(ev.reason || ''), clean: !!ev.wasClean, ts: Date.now() }; });
66
+ // 最近一条命中 pattern 的连接留给适配器用:有的平台推送里不带全字段,要在页面自己的连接上补一次查询。
67
+ if (want(url)) window.__pp_ws = ws;
66
68
  var send = ws.send;
67
69
  ws.send = function(d){ push('websocket','sent',url,d); return send.apply(this, arguments); };
68
70
  } catch (e) {}
@@ -1,43 +1,83 @@
1
- import{evaluateWithCaptcha as w}from"./captcha.js";import{AuthRequiredError as d,CommandExecutionError as u}from"@jackwener/opencli/errors";import{registerWatch as _}from"../_shared/watch.js";import{buildChatUrl as p,buildInboxUrl as o,requireEvaluateObject as g}from"./im.js";const y=/"lwp"\s*:\s*"\/s\//,S=[300,700,1000,1500],c=1e4,h=`
1
+ import{existsSync as I,mkdirSync as b,readFileSync as k,renameSync as N,writeFileSync as P}from"node:fs";import{homedir as w}from"node:os";import{join as p}from"node:path";import{AuthRequiredError as T}from"@jackwener/opencli/errors";import{registerWatch as C}from"../_shared/watch.js";import{buildChatUrl as O,buildInboxUrl as f}from"./im.js";const A=/"lwp"\s*:\s*"\/s\//,J=40000,M=20,U=20000,D=20000,B=120000,F=1e4,h=`
2
2
  (() => {
3
- const rows = Array.from(document.querySelectorAll('#conv-list-scrollable [class*="conversation-item"]'));
4
- const sessions = [];
5
- for (const row of rows) {
6
- const fk = Object.keys(row).find((k) => k.startsWith('__reactFiber'));
7
- let f = fk ? row[fk] : null;
8
- let info = null;
9
- for (let d = 0; f && d < 25; d++, f = f.return) {
10
- const p = f.memoizedProps;
11
- if (p && p.sessionInfo) { info = p.sessionInfo; break; }
12
- }
13
- if (!info) continue;
14
- const summary = info.summary || {};
15
- const lm = summary.latestMessage || {};
16
- const c = lm.content || {};
17
- const item = info.itemInfo || {};
18
- sessions.push({
19
- session_id: String(info.sessionId || ''),
20
- session_type: info.sessionType,
21
- peer_user_id: String((info.userInfo && info.userInfo.userId) || ''),
22
- peer_name: String((lm.reminder && lm.reminder.title) || (info.userInfo && (info.userInfo.fishNick || info.userInfo.nick)) || ''),
23
- item_id: String(item.itemId || ''),
24
- item_title: String(item.title || ''),
25
- price: String(item.price || ''),
26
- seller_id: String((item.sellerInfo && item.sellerInfo.userId) || ''),
27
- sender_id: String((lm.senderInfo && lm.senderInfo.userId) || ''),
28
- message_id: String(lm.messageId || ''),
29
- content_type: c.contentType,
30
- text: String((c.text && c.text.text) || summary.summaryContent || ''),
31
- ts: Number(lm.timeStamp || summary.timeStamp || 0),
32
- unread: Number(info.unread || 0),
3
+ const P = WebSocket.prototype;
4
+ if (!P.__ppXianyuSend) {
5
+ const send = P.send;
6
+ P.__ppXianyuSend = send;
7
+ P.send = function () {
8
+ try {
9
+ if (/wss-goofish\\.dingtalk\\.com/.test(this.url || '')) {
10
+ window.__pp_ws = this;
11
+ if (!this.__ppTapped && !Object.prototype.hasOwnProperty.call(this, 'send')) {
12
+ this.__ppTapped = true;
13
+ const url = String(this.url);
14
+ this.addEventListener('message', (ev) => {
15
+ const buf = window.__pp_stream;
16
+ if (!Array.isArray(buf)) return;
17
+ if (buf.length >= 500) buf.shift();
18
+ const data = typeof ev.data === 'string' ? ev.data.slice(0, 262144) : null;
19
+ buf.push({ channel: 'websocket', direction: 'received', url, data, binary: data === null, eventName: '', ts: Date.now() });
20
+ });
21
+ }
22
+ }
23
+ } catch (e) {}
24
+ return send.apply(this, arguments);
25
+ };
26
+ }
27
+ const ws = window.__pp_ws;
28
+ const me = (document.cookie.match(/(?:^|;\\s*)unb=(\\d+)/) || [])[1] || '';
29
+ return JSON.stringify({ ready: !!(ws && ws.readyState === 1), me });
30
+ })()
31
+ `,L=`
32
+ (async () => {
33
+ const out = (o) => JSON.stringify(o);
34
+ if (!/goofish\\.com\\/im/.test(location.href)) return out({ state: 'nopage', href: location.href });
35
+ const me = (document.cookie.match(/(?:^|;\\s*)unb=(\\d+)/) || [])[1];
36
+ if (!me) return out({ state: 'nologin' });
37
+ const ws = window.__pp_ws;
38
+ if (!ws || ws.readyState !== 1) return out({ state: 'nows', ready: ws ? ws.readyState : -1 });
39
+ const r = await new Promise((res) => {
40
+ const mid = Math.floor(Math.random() * 1000) + '' + Date.now() + ' 0';
41
+ const h = (ev) => {
42
+ let m;
43
+ try { m = JSON.parse(ev.data); } catch (e) { return; }
44
+ if (m && m.headers && m.headers.mid === mid) { clearTimeout(t); ws.removeEventListener('message', h); res(m); }
45
+ };
46
+ const t = setTimeout(() => { ws.removeEventListener('message', h); res(null); }, 5000);
47
+ ws.addEventListener('message', h);
48
+ ws.send(JSON.stringify({ lwp: '/r/Conversation/listNewestPagination', headers: { mid }, body: [9007199254740991, ${M}] }));
49
+ });
50
+ if (!r) return out({ state: 'timeout' });
51
+ if (r.code !== 200) return out({ state: 'error', reason: JSON.stringify(r.body || {}).slice(0, 300) });
52
+ const convs = [];
53
+ for (const c of (r.body && r.body.userConvs) || []) {
54
+ const uc = c.singleChatUserConversation || {};
55
+ const sc = uc.singleChatConversation;
56
+ const m = (uc.lastMessage || {}).message;
57
+ if (!sc || uc.visible === 0 || !m || !m.messageId) continue;
58
+ const ext = sc.extension || {};
59
+ const mx = m.extension || {};
60
+ const content = m.content || {};
61
+ convs.push({
62
+ cid: String(sc.cid || ''),
63
+ item_id: String(ext.itemId || ''),
64
+ item_title: String(ext.itemTitle || ''),
65
+ seller_id: String(ext.itemSellerId || ext.ownerUserId || ''),
66
+ peer: [sc.pairFirst, sc.pairSecond].map((x) => String(x || '').split('@')[0]).find((x) => x && x !== me) || '',
67
+ last: {
68
+ id: String(m.messageId), at: Number(m.createAt || 0), sender: String((m.sender && m.sender.uid) || '').split('@')[0],
69
+ type: content.contentType, custom: content.custom ? { data: content.custom.data || '', summary: content.custom.summary || '' } : null,
70
+ text: content.text ? (content.text.text || content.text.content || '') : '',
71
+ title: String(mx.reminderTitle || ''), reminder: String(mx.reminderContent || ''),
72
+ },
33
73
  });
34
74
  }
35
- const bodyText = rows.length ? '' : (document.body ? document.body.innerText : '');
36
- return {
37
- rows: rows.length,
38
- sessions,
39
- requiresAuth: /请先登录|登录后/.test(bodyText),
40
- blocked: /验证码|安全验证|异常访问/.test(bodyText),
41
- };
75
+ return out({ state: 'ok', me, convs });
42
76
  })()
43
- `;async function a(t){const s=g(await w(t,h),"watch sessions");if(!s.rows&&s.requiresAuth)throw new d("www.goofish.com","Xianyu IM requires a logged-in browser session");if(!s.rows&&s.blocked)throw new d("www.goofish.com","Xianyu IM is blocked by verification or risk control");if(s.rows&&!s.sessions.length)throw new u("闲鱼会话列表渲染出来了,但行上读不到 sessionInfo:页面结构可能改版了");return s}function l(t,s){const n=[];for(const e of t){if(!e.message_id||!e.peer_user_id)continue;const i=e.sender_id===e.peer_user_id;if(!i&&!s?.includeOwn)continue;n.push({event:"dm",id:e.message_id,ts:e.ts,conversation_id:e.session_id,sender:{id:e.sender_id,name:i?e.peer_name:""},text:e.text,url:e.item_id?p(e.item_id,e.peer_user_id):o(),raw:{session_id:e.session_id,session_type:e.session_type,peer_user_id:e.peer_user_id,item_id:e.item_id,item_title:e.item_title,price:e.price,seller_id:e.seller_id,content_type:e.content_type,unread_count:e.unread,from_peer:i}})}return n}function m(t){return t.map((s)=>`${s.session_id}:${s.message_id}`).join("|")}_({site:"xianyu",name:"watch",domain:"www.goofish.com",description:"闲鱼私信监听:旁听网页版 IM 自己的 WebSocket 推送,一有新消息就读已打开的会话列表,按 NDJSON 逐条输出",watchUrl:o(),pattern:"wss-goofish.dingtalk.com",quietSeconds:50,parse:async(t,s)=>{if(!y.test(String(t?.responsePreview??"")))return null;const n=Number(t?.timestamp??0);if(n&&s.state.readStartedAt&&n<s.state.readStartedAt)return null;s.state.readStartedAt=Date.now();const e=s.state.fingerprint;let i=null;for(const r of S){await new Promise((f)=>setTimeout(f,r));i=await a(s.page);if(m(i.sessions)!==e)break}s.state.fingerprint=m(i.sessions);return l(i.sessions,s)},reconcile:async(t,s)=>{const n=await(t.getCurrentUrl?t.getCurrentUrl().catch(()=>""):Promise.resolve(""));if(!/https:\/\/www\.goofish\.com\/im\b/.test(String(n||"")))await t.goto(o());const e=Date.now()+c;let i=await a(t);while(!i.rows&&Date.now()<e){await new Promise((r)=>setTimeout(r,500));i=await a(t)}if(!i.rows)throw new u(`闲鱼会话列表 ${c/1000} 秒内没有渲染出来(收件箱为空,或页面没加载完)`);s.state.fingerprint=m(i.sessions);return l(i.sessions,s)}});
77
+ `,q="(() => { setTimeout(() => location.reload(), 200); return 1; })()",R=(e)=>new Promise((t)=>setTimeout(t,e));export function unpackMsgpack(e){let t=0;const n=(r)=>{const d=e.toString("utf8",t,t+r);t+=r;return d},o=(r)=>{const d=e.subarray(t,t+r);t+=r;return d},s=(r)=>{const d=[];for(let l=0;l<r;l++)d.push(u());return d},i=(r)=>{const d={};for(let l=0;l<r;l++){const E=u();d[E]=u()}return d},c=(r)=>{t+=1;return o(r)},a=(r)=>{const d=e.readUIntBE(t,r);t+=r;return d},m=(r)=>{const d=e.readIntBE(t,r);t+=r;return d};function u(){if(t>=e.length)throw Error("msgpack 数据提前结束");const r=e[t++];if(r<=127)return r;if(r>=224)return r-256;if((r&240)===128)return i(r&15);if((r&240)===144)return s(r&15);if((r&224)===160)return n(r&31);switch(r){case 192:return null;case 194:return!1;case 195:return!0;case 196:return o(a(1));case 197:return o(a(2));case 198:return o(a(4));case 199:return c(a(1));case 200:return c(a(2));case 201:return c(a(4));case 202:{const d=e.readFloatBE(t);t+=4;return d}case 203:{const d=e.readDoubleBE(t);t+=8;return d}case 204:return a(1);case 205:return a(2);case 206:return a(4);case 207:{const d=Number(e.readBigUInt64BE(t));t+=8;return d}case 208:return m(1);case 209:return m(2);case 210:return m(4);case 211:{const d=Number(e.readBigInt64BE(t));t+=8;return d}case 212:return c(1);case 213:return c(2);case 214:return c(4);case 215:return c(8);case 216:return c(16);case 217:return n(a(1));case 218:return n(a(2));case 219:return n(a(4));case 220:return s(a(2));case 221:return s(a(4));case 222:return i(a(2));case 223:return i(a(4))}throw Error(`msgpack 不认识的类型 0x${r.toString(16)}(第 ${t-1} 字节)`)}return u()}function g(e,t,n,o,s){if(e===101&&t){const i=JSON.parse(t);return[i.contentType,i.contentType===1&&i.text&&i.text.text||n||s||""]}return[e,o||n||s||""]}export function decodePushFrame(e){if(!A.test(e))return[];const t=JSON.parse(e).body||{},n=[];for(const o of t.syncPushPackage?.data||[]){if(o.objectType!==J||!o.data)continue;const s=unpackMsgpack(Buffer.from(o.data,"base64"))[1]||{},i=s[10]||{},c=s[6]||{},a=c[3]||{},[m,u]=g(c[1],a[5],a[2],"",i.reminderContent),r=new URLSearchParams(String(i.reminderUrl||"").split("?")[1]||"");n.push({cid:String(s[2]||""),id:String(s[3]||""),at:Number(s[5]||0),sender:String(s[1]&&s[1][1]||i.senderUserId||"").split("@")[0],type:m,text:u,title:String(i.reminderTitle||""),item_id:r.get("itemId")||""})}return n}function y(){const e=String(process.env.PPCLI_ACTIVE_PROFILE??"").trim();return p(w(),".ppcli","watch",`xianyu${e?`@${e}`:""}.watch.convs.json`)}function j(){const e=y();if(!I(e))return{};try{return JSON.parse(k(e,"utf-8"))}catch(t){process.stderr.write(`[xianyu-watch] 会话表 ${e} 读不了,从空表开始:${t?.message??t}
78
+ `);return{}}}function W(e){const t=y();b(p(w(),".ppcli","watch"),{recursive:!0});const n=`${t}.${process.pid}`;P(n,JSON.stringify(e),"utf-8");N(n,t)}function X(e,t){const n=e.state.convs[t.cid]||{},o=n.peer||(t.sender&&t.sender!==e.state.me?t.sender:""),s=!!o&&t.sender===o,i=n.item_id||t.item_id||"",c=t.cid.split("@")[0];return{event:"dm",id:t.id,ts:t.at,conversation_id:c,sender:{id:t.sender,name:s?t.title:""},text:t.text,url:i&&o?O(i,o):f(),raw:{session_id:c,session_type:1,peer_user_id:o,item_id:i,item_title:n.item_title||"",price:"",seller_id:n.seller_id||"",content_type:t.type,unread_count:0,from_peer:s}}}function S(e,t){return t.map((n)=>X(e,n)).filter((n)=>n.raw.from_peer||e.includeOwn)}async function _(e,t){let n;try{n=JSON.parse(await e.evaluate(L))}catch(i){n={state:"evalfail",reason:String(i?.message??i).slice(0,300)}}if(n.state==="nologin")throw new T("www.goofish.com","Xianyu IM requires a logged-in browser session");if(n.state!=="ok"){if(n.state==="nows")await e.evaluate(h).catch(()=>null);process.stderr.write(`[xianyu-watch] 调会话列表没成功:${JSON.stringify(n)}
79
+ `);return null}t.state.me||=n.me;let o=!1;const s=[];for(const i of n.convs){const c={item_id:i.item_id,item_title:i.item_title,seller_id:i.seller_id,peer:i.peer};if(JSON.stringify(t.state.convs[i.cid])!==JSON.stringify(c)){t.state.convs[i.cid]=c;o=!0}const a=i.last,m=a.custom?Buffer.from(a.custom.data,"base64").toString("utf8"):"",[u,r]=g(a.type,m,a.custom?.summary,a.text,a.reminder);s.push({cid:i.cid,id:a.id,at:a.at,sender:a.sender,type:u,text:r,title:a.title,item_id:i.item_id})}if(o)W(t.state.convs);return s}async function x(e,t){process.stderr.write(`[xianyu-watch] ${t},重新加载监听页后退出,由 supervisor 重启监听
80
+ `);await e.evaluate(q).catch((n)=>process.stderr.write(`[xianyu-watch] 重新加载监听页失败:${n?.message??n}
81
+ `));process.exit(75)}async function v(e,t){const n=Date.now()-Math.max(t.state.lastFrameAt||0,t.state.firstAttachedAt||0);if(t.state.firstAttachedAt&&n>B)await x(e,`IM 长连接已 ${Math.round(n/1000)} 秒没有任何帧`)}C({site:"xianyu",name:"watch",domain:"www.goofish.com",description:"闲鱼私信监听:解网页版 IM 长连接上的新消息推送,买家的新消息按 NDJSON 逐条输出",watchUrl:f(),pattern:"wss-goofish.dingtalk.com",reconcileSeconds:180,quietSeconds:50,onAttach:async(e,t)=>{t.state.convs??=j();t.state.known??=new Set;t.state.startedAt??=Date.now();t.state.firstAttachedAt??=Date.now();const n=Date.now()+U;for(;;){const o=JSON.parse(await e.evaluate(h).catch(()=>"")||"{}");if(o.me)t.state.me=o.me;if(o.ready||Date.now()>n)break;await R(500)}t.state.quietPage=e;t.state.quietTimer??=setInterval(()=>v(t.state.quietPage,t),F).unref()},parse:async(e,t)=>{t.state.lastFrameAt=Date.now();const n=String(e?.responsePreview??"");let o;try{o=decodePushFrame(n)}catch(s){process.stderr.write(`[xianyu-watch] 解推送失败:${s?.message??s};原帧前 500 字:${n.slice(0,500)}
82
+ `);return null}if(!o.length)return null;for(const s of o)t.state.known.add(s.id);if(o.some((s)=>!t.state.convs[s.cid])){await _(t.page,t);for(const s of o)if(!t.state.convs[s.cid])process.stderr.write(`[xianyu-watch] 会话 ${s.cid} 查不到商品标题和卖家,按推送里有的字段先发出去
83
+ `)}return S(t,o)},reconcile:async(e,t)=>{await v(e,t);const n=await(e.getCurrentUrl?e.getCurrentUrl().catch(()=>""):Promise.resolve(""));if(!/https:\/\/www\.goofish\.com\/im\b/.test(String(n||"")))await e.goto(f());const o=await _(e,t);if(!o)return[];const s=S(t,o),i=s.filter((c)=>c.raw.from_peer&&!t.state.known.has(c.id)&&c.ts>t.state.startedAt&&Date.now()-c.ts>D);for(const c of s)t.state.known.add(c.id);if(t.state.known.size>5000)t.state.known=new Set([...t.state.known].slice(-2000));if(i.length){const c=`对账发现推送没带来的买家消息 ${i.map((a)=>`${a.id}(说完 ${Math.round((Date.now()-a.ts)/1000)} 秒)`).join("、")},这条长连接收不到推送了`;setTimeout(()=>x(e,c),3000)}return s}});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.0.42",
3
+ "version": "1.0.43",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false