publishport-opencli 1.0.9 → 1.0.11
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/clis/_shared/file-inject.js +41 -0
- package/clis/_shared/video-publish.js +4 -4
- package/clis/bilibili/upload.js +1 -1
- package/clis/chatgpt/utils.js +5 -5
- package/clis/claude/utils.js +1 -1
- package/clis/deepseek/utils.js +7 -7
- package/clis/douyin/draft.js +6 -6
- package/clis/douyin/publish-image.js +10 -31
- package/clis/facebook/post.js +9 -36
- package/clis/instagram/post.js +19 -66
- package/clis/instagram/reel.js +11 -11
- package/clis/threads/post.js +15 -34
- package/clis/twitter/post.js +15 -42
- package/clis/twitter/utils.js +4 -36
- package/clis/wechat-channels/publish.js +29 -57
- package/clis/weibo/publish.js +8 -8
- package/clis/weixin/create-draft.js +7 -7
- package/clis/xianyu/publish.js +2 -2
- package/clis/xiaohongshu/publish-video.js +9 -27
- package/clis/xiaohongshu/publish.js +42 -86
- package/dist/src/browser/bridge-readiness.d.ts +1 -1
- package/dist/src/browser/daemon-client.d.ts +72 -1
- package/dist/src/browser/daemon-client.js +1 -1
- package/dist/src/browser/daemon-version.d.ts +1 -1
- package/dist/src/browser/errors.d.ts +2 -0
- package/dist/src/browser/errors.js +3 -3
- package/dist/src/browser/network-interceptor.d.ts +11 -0
- package/dist/src/browser/network-interceptor.js +1 -0
- package/dist/src/cli.js +24 -24
- package/dist/src/commands/daemon.js +1 -1
- package/dist/src/daemon-utils.d.ts +1 -0
- package/dist/src/daemon-utils.js +1 -1
- package/dist/src/daemon.js +1 -1
- package/dist/src/doctor.d.ts +1 -1
- package/dist/src/doctor.js +7 -7
- package/dist/src/errors.d.ts +8 -0
- package/dist/src/errors.js +1 -1
- package/dist/src/execution.js +2 -2
- package/dist/src/session-lease.d.ts +135 -0
- package/dist/src/session-lease.js +1 -0
- package/dist/src/session-lease.test.d.ts +1 -0
- package/package.json +1 -1
package/clis/weibo/publish.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as U from"node:fs";import*as j from"node:path";import{cli as f,Strategy as x}from"@jackwener/opencli/registry";import{ArgumentError as B,AuthRequiredError as A,CommandExecutionError as K}from"@jackwener/opencli/errors";import{getSelfUid as h}from"./utils.js";import{setInputFiles as I}from"../_shared/file-inject.js";const v=9,X=1500,c=30000,W=300,N=1e4,F=500,C=20000,m=new Set([".jpg",".jpeg",".png",".gif",".webp"]),k=['textarea[placeholder*="有什么新鲜事"]','textarea[placeholder*="新鲜事"]',"textarea._input_13iqr_8"],R='input[type="file"][class*="_file_"]';function w(z){const J=String(z??"").trim();if(!J)throw new B("weibo publish text cannot be empty");if(J.length>2000)throw new B("weibo publish text exceeds 2000 characters");return J}function b(z){if(!z)return[];const J=z.split(",").map((Z)=>Z.trim()).filter(Boolean);if(J.length>v)throw new B(`Too many images: ${J.length} (max ${v})`);return J.map((Z)=>{const Q=j.resolve(Z),Y=j.extname(Q).toLowerCase();if(!m.has(Y))throw new B(`Unsupported image format "${Y}". Supported: jpg, png, gif, webp`);const q=U.statSync(Q,{throwIfNoEntry:!1});if(!q||!q.isFile())throw new B(`Not a valid file: ${Q}`);return Q})}f({site:"weibo",name:"publish",access:"write",description:"Publish a new Weibo post immediately",domain:"weibo.com",strategy:x.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"Weibo text content (max 2000 chars)"},{name:"images",type:"string",required:!1,help:`Image paths, comma-separated, max ${v} (jpg/png/gif/webp)`}],columns:["status","message","text"],func:async(z,J)=>{if(!z)throw new K("Browser session required for weibo publish");const Z=w(J.text),Q=b(J.images);await z.goto("https://weibo.com",{waitUntil:"load",settleMs:2000});await z.wait({time:2});try{await h(z)}catch(H){if(H instanceof A)throw H;throw new K("Not logged into Weibo. Please login at weibo.com in your Chrome browser.")}let Y=null;for(let H=0;H<Math.ceil(N/W);H++){Y=await z.evaluate(`
|
|
2
2
|
() => {
|
|
3
3
|
const visible = el => !!el && el.offsetParent !== null && !el.disabled;
|
|
4
4
|
const buttons = document.querySelectorAll('button[title="发微博"], button[title="写微博"]');
|
|
@@ -10,7 +10,7 @@ import*as f from"node:fs";import*as D from"node:path";import{cli as U,Strategy a
|
|
|
10
10
|
}
|
|
11
11
|
return { ok: false, message: 'Could not find 发微博 button' };
|
|
12
12
|
}
|
|
13
|
-
`);if(Y?.ok)break;await z.wait({time:W/1000})}if(!Y?.ok)throw new
|
|
13
|
+
`);if(Y?.ok)break;await z.wait({time:W/1000})}if(!Y?.ok)throw new K(Y?.message??"Could not open compose editor.");let q=!1;for(let H=0;H<Math.ceil(N/W);H++){const V=await z.evaluate(`
|
|
14
14
|
(selectors => {
|
|
15
15
|
// Pick the LAST visible match across all selectors so
|
|
16
16
|
// the modal (rendered on top of the home-feed strip)
|
|
@@ -24,12 +24,12 @@ import*as f from"node:fs";import*as D from"node:path";import{cli as U,Strategy a
|
|
|
24
24
|
if (!last) return { found: false };
|
|
25
25
|
return { found: true, visible: true, rectTop: last.getBoundingClientRect().top };
|
|
26
26
|
})(${JSON.stringify(k)})
|
|
27
|
-
`);if(V?.found&&V.visible&&V.rectTop>=0){q=!0;break}await z.wait({time:W/1000})}if(!q)throw new
|
|
27
|
+
`);if(V?.found&&V.visible&&V.rectTop>=0){q=!0;break}await z.wait({time:W/1000})}if(!q)throw new K("Weibo compose editor did not appear");if(Q.length>0){let H=!1;for(let y=0;y<Math.ceil(N/W);y++){H=await z.evaluate(`
|
|
28
28
|
() => {
|
|
29
29
|
const input = document.querySelector('input[type="file"][class*="_file_"]');
|
|
30
30
|
return !!input;
|
|
31
31
|
}
|
|
32
|
-
`);if(H)break;await z.wait({time:W/1000})}if(!H)throw new
|
|
32
|
+
`);if(H)break;await z.wait({time:W/1000})}if(!H)throw new K("Could not find image file input on Weibo compose page. UI may have changed.");await I(z,R,Q);let V=null;for(let y=0;y<Math.ceil(c/X);y++){await z.wait({time:X/1000});V=await z.evaluateWithArgs(`
|
|
33
33
|
(() => {
|
|
34
34
|
const expectedCount = expected;
|
|
35
35
|
const uploading = document.querySelector('[class*="upload"], [class*="progress"]');
|
|
@@ -38,7 +38,7 @@ import*as f from"node:fs";import*as D from"node:path";import{cli as U,Strategy a
|
|
|
38
38
|
if (pics.length >= expectedCount) return { ok: true, count: pics.length };
|
|
39
39
|
return null;
|
|
40
40
|
})()
|
|
41
|
-
`,{expected:Q.length});if(V!==null)break}if(!V?.ok)throw new
|
|
41
|
+
`,{expected:Q.length});if(V!==null)break}if(!V?.ok)throw new K(V?.message??"Image upload did not complete before timeout")}const D=await z.evaluateWithArgs(`
|
|
42
42
|
((selectors) => {
|
|
43
43
|
let ta = null;
|
|
44
44
|
for (const sel of selectors) {
|
|
@@ -58,7 +58,7 @@ import*as f from"node:fs";import*as D from"node:path";import{cli as U,Strategy a
|
|
|
58
58
|
ta.dispatchEvent(new Event('change', { bubbles: true }));
|
|
59
59
|
return { ok: true, valueLength: ta.value.length };
|
|
60
60
|
})(${JSON.stringify(k)})
|
|
61
|
-
`,{textContent:Z});if(!
|
|
61
|
+
`,{textContent:Z});if(!D?.ok)throw new K(D?.message??"Could not insert text.");await z.wait({time:0.5});let G=null;for(let H=0;H<Math.ceil(N/W);H++){G=await z.evaluate(`
|
|
62
62
|
() => {
|
|
63
63
|
const visible = el => !!el && el.offsetParent !== null && !el.disabled;
|
|
64
64
|
const labels = ['发送', '发布'];
|
|
@@ -74,7 +74,7 @@ import*as f from"node:fs";import*as D from"node:path";import{cli as U,Strategy a
|
|
|
74
74
|
}
|
|
75
75
|
return { ok: false, message: 'Could not find send button' };
|
|
76
76
|
}
|
|
77
|
-
`);if(
|
|
77
|
+
`);if(G?.ok)break;await z.wait({time:W/1000})}if(!G?.ok)throw new K(G?.message??"Could not click publish.");let $=null;for(let H=0;H<Math.ceil(C/F);H++){await z.wait({time:F/1000});$=await z.evaluate(`
|
|
78
78
|
(() => {
|
|
79
79
|
const successMarkers = ['发布成功', '已发布', '发送成功'];
|
|
80
80
|
const errorMarkers = ['发布失败', '发送失败', '内容违规', '请稍后再试', '频繁'];
|
|
@@ -95,4 +95,4 @@ import*as f from"node:fs";import*as D from"node:path";import{cli as U,Strategy a
|
|
|
95
95
|
}
|
|
96
96
|
return null;
|
|
97
97
|
})()
|
|
98
|
-
`);if($!==null)break}if(!$)throw new
|
|
98
|
+
`);if($!==null)break}if(!$)throw new K("Publish button clicked but result was unclear. Check Weibo manually.");if(!$.ok)throw new K($.message||"Weibo publish failed");return[{status:"success",message:$.message||"Published successfully",text:Z}]}});export const __test__={validateText:w,validateImagePaths:b};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{CommandExecutionError as J}from"@jackwener/opencli/errors";const
|
|
1
|
+
import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{CommandExecutionError as J}from"@jackwener/opencli/errors";import{setInputFiles as $}from"../_shared/file-inject.js";const A="mp.weixin.qq.com",H="https://mp.weixin.qq.com/";async function D(q){return q.evaluate("(window.location.href.match(/token=(\\d+)/)||[])[1]")}async function F(q){await q.goto(H);await q.wait(3);const z=await D(q);if(!z)throw new J("Could not extract session token. Please log in to mp.weixin.qq.com");await q.goto(`https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit_v2&action=edit&isNew=1&type=77&token=${z}&lang=zh_CN`);for(let B=0;B<15;B++){if(await q.evaluate("!!document.querySelector('.ProseMirror')"))break;await q.wait(1)}await q.wait(2)}async function L(q,z,B){return q.evaluate(`(() => {
|
|
2
2
|
var el = document.querySelector(${JSON.stringify(z)});
|
|
3
3
|
if (!el) return { ok: false, reason: 'not found: ' + ${JSON.stringify(z)} };
|
|
4
4
|
el.focus();
|
|
@@ -62,7 +62,7 @@ import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{CommandEx
|
|
|
62
62
|
} catch (e) {}
|
|
63
63
|
}
|
|
64
64
|
return { ok: true, textLen: (el.innerText || '').length };
|
|
65
|
-
})()`);await q.wait(1);return K}async function
|
|
65
|
+
})()`);await q.wait(1);return K}async function O(q,z){const B=await import("node:fs"),G=(await import("node:path")).default.resolve(z);if(!B.default.existsSync(G))throw new J(`Image not found: ${G}`);await q.evaluate(`(() => {
|
|
66
66
|
var li = document.querySelector('#js_editor_insertimage')
|
|
67
67
|
|| document.querySelector('.jsInsertIcon.img');
|
|
68
68
|
if (li) li.click();
|
|
@@ -73,11 +73,11 @@ import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{CommandEx
|
|
|
73
73
|
if (t.includes('本地上传') || t.includes('上传')) { items[i].click(); return; }
|
|
74
74
|
}
|
|
75
75
|
if (items[0]) items[0].click();
|
|
76
|
-
})()`);await q.wait(1);await q
|
|
76
|
+
})()`);await q.wait(1);await $(q,'input[type="file"][name="file"]',[G]);await q.wait(8);if(await q.evaluate(`(() => {
|
|
77
77
|
var editor = document.querySelector('.rich_media_content')
|
|
78
78
|
|| document.querySelector('#ueditor_0');
|
|
79
79
|
return editor ? editor.querySelectorAll('img[src*="mmbiz"]').length : 0;
|
|
80
|
-
})()`)===0)throw new J("Image did not upload to WeChat CDN")}async function
|
|
80
|
+
})()`)===0)throw new J("Image did not upload to WeChat CDN")}async function W(q){await q.evaluate('document.querySelector("#js_cover_area")?.scrollIntoView()');await q.wait(1);await q.evaluate('document.querySelector(".js_cover_btn_area")?.click()');await q.wait(1);await q.evaluate(`(() => {
|
|
81
81
|
var links = document.querySelectorAll('a.pop-opr__button, button.pop-opr__button');
|
|
82
82
|
for (var i = 0; i < links.length; i++) {
|
|
83
83
|
var t = (links[i].textContent || '').trim();
|
|
@@ -114,7 +114,7 @@ import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{CommandEx
|
|
|
114
114
|
if (bg && bg.includes('mmbiz')) found = true;
|
|
115
115
|
});
|
|
116
116
|
return found;
|
|
117
|
-
})()`)}async function
|
|
117
|
+
})()`)}async function X(q){if(!(await q.evaluate(`(() => {
|
|
118
118
|
var btns = document.querySelectorAll('span, button, a');
|
|
119
119
|
for (var i = 0; i < btns.length; i++) {
|
|
120
120
|
if ((btns[i].textContent || '').trim() === '保存为草稿') { btns[i].click(); return { ok: true }; }
|
|
@@ -124,7 +124,7 @@ import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{CommandEx
|
|
|
124
124
|
var el = document.querySelector('#js_save_success');
|
|
125
125
|
if (el && window.getComputedStyle(el).display !== 'none') return true;
|
|
126
126
|
return document.body.innerText.includes('已保存');
|
|
127
|
-
})()`))return!0}return!1}export const createDraftCommand=Y({site:"weixin",name:"create-draft",access:"write",description:"创建微信公众号图文草稿",domain
|
|
127
|
+
})()`))return!0}return!1}export const createDraftCommand=Y({site:"weixin",name:"create-draft",access:"write",description:"创建微信公众号图文草稿",domain:A,strategy:Z.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"title",required:!0,help:"文章标题 (最长64字)"},{name:"content",required:!0,positional:!0,help:"文章正文"},{name:"author",help:"作者名 (最长8字)"},{name:"cover-image",help:"封面图片路径 (会先上传到正文再设为封面)"},{name:"summary",help:"文章摘要"},{name:"timeout",type:"int",required:!1,default:180,help:"Max seconds for the overall command (default: 180)"}],columns:["status","detail"],func:async(q,z)=>{await F(q);if(!(await V(q,".title-editor__input .ProseMirror",z.title))?.ok){if(!(await L(q,"textarea#title",z.title))?.ok)throw new J("Failed to fill title")}if(z.author){if(!(await L(q,"input#author",z.author))?.ok)throw new J("Failed to fill author")}if(!(await V(q,".rich_media_content.ProseMirror, .rich_media_content .ProseMirror",z.content))?.ok)throw new J("Failed to fill content");if(z["cover-image"]){await O(q,z["cover-image"]);const R=await W(q)}if(z.summary)await L(q,"textarea#js_description",z.summary);const G=await q.evaluate(`(() => {
|
|
128
128
|
var t = document.querySelector('.title-editor__input .ProseMirror');
|
|
129
129
|
var c = document.querySelector('.rich_media_content.ProseMirror, .rich_media_content .ProseMirror');
|
|
130
130
|
var tt = document.querySelector('textarea#title');
|
|
@@ -135,7 +135,7 @@ import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{CommandEx
|
|
|
135
135
|
contentText: c ? (c.innerText || '').slice(0, 40) : '',
|
|
136
136
|
textareaTitleValue: tt ? (tt.value || '').slice(0, 40) : '(no textarea#title)',
|
|
137
137
|
};
|
|
138
|
-
})()`);if(G.titleLen===0||G.contentLen===0)throw new J(`ProseMirror state not synced: title_len=${G.titleLen}, content_len=${G.contentLen}. title="${G.titleText}" content="${G.contentText}"`);await L(q,"textarea#title",z.title);await q.wait(3);const U=await
|
|
138
|
+
})()`);if(G.titleLen===0||G.contentLen===0)throw new J(`ProseMirror state not synced: title_len=${G.titleLen}, content_len=${G.contentLen}. title="${G.titleText}" content="${G.contentText}"`);await L(q,"textarea#title",z.title);await q.wait(3);const U=await X(q);await q.wait(3);const Q=await q.evaluate(`(() => {
|
|
139
139
|
var t = document.querySelector('.title-editor__input .ProseMirror');
|
|
140
140
|
var c = document.querySelector('.rich_media_content.ProseMirror, .rich_media_content .ProseMirror');
|
|
141
141
|
var tt = document.querySelector('textarea#title');
|
package/clis/xianyu/publish.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import*as z from"node:fs";import*as C from"node:path";import{ArgumentError as L,AuthRequiredError as I,CommandExecutionError as M}from"@jackwener/opencli/errors";import{cli as S,Strategy as F}from"@jackwener/opencli/registry";const
|
|
1
|
+
import*as z from"node:fs";import*as C from"node:path";import{ArgumentError as L,AuthRequiredError as I,CommandExecutionError as M}from"@jackwener/opencli/errors";import{cli as S,Strategy as F}from"@jackwener/opencli/registry";import{setInputFiles as f}from"../_shared/file-inject.js";const R=new Set([".jpg",".jpeg",".png",".webp"]),V=9,W=["全新","几乎全新","轻微使用","明显使用","老旧"];function X(){return"https://www.goofish.com/publish"}async function Y(j){if(j.getCurrentUrl)try{const B=await j.getCurrentUrl();if(B)return B}catch{}return X()}function O(j,B){const q=String(j??"").replace(/\s+/g," ").trim();if(!q)throw new L(`xianyu publish ${B} cannot be empty`);return q}function x(j,B){if(j==null||String(j).trim()==="")return null;const q=String(j).trim();if(!/^\d+(?:\.\d{1,2})?$/.test(q))throw new L(`xianyu publish ${B} must be a positive price with at most 2 decimals`);const G=Number(q);if(!Number.isFinite(G)||G<=0)throw new L(`xianyu publish ${B} must be a positive price`);return q}function v(j){const B=O(j,"condition");if(!W.includes(B))throw new L(`xianyu publish condition must be one of: ${W.join(", ")}`);return B}function y(j){if(!j)return[];const B=String(j).split(",").map((q)=>q.trim()).filter(Boolean);if(B.length===0)return[];if(B.length>V)throw new L(`xianyu publish images supports at most ${V} files`);return B.map((q)=>{const G=C.resolve(q),J=C.extname(G).toLowerCase();if(!R.has(J))throw new L(`Unsupported image format "${J}". Supported: jpg, jpeg, png, webp`);const K=z.statSync(G,{throwIfNoEntry:!1});if(!K||!K.isFile())throw new L(`Not a valid image file: ${G}`);return G})}function P(j){const B=x(j.price,"price");if(B==null)throw new L("xianyu publish price cannot be empty");const q={};q.title=O(j.title,"title");q.description=O(j.description,"description");q.price=B;q.condition=v(j.condition);q.category=O(j.category,"category");q.original_price=x(j.original_price,"original_price");q.location=j.location?O(j.location,"location"):"";q.images=y(j.images);return q}function Z(j){return`
|
|
2
2
|
(() => {
|
|
3
3
|
const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
|
|
4
4
|
|
|
@@ -242,4 +242,4 @@ import*as z from"node:fs";import*as C from"node:path";import{ArgumentError as L,
|
|
|
242
242
|
bodySnippet: bodyText.slice(0, 500),
|
|
243
243
|
};
|
|
244
244
|
})()
|
|
245
|
-
`}export const publishCommand=S({site:"xianyu",name:"publish",access:"write",description:"发布闲鱼宝贝(需先在浏览器中登录闲鱼)",domain:"www.goofish.com",strategy:F.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"title",required:!0,positional:!0,help:"商品标题"},{name:"description",required:!0,positional:!0,help:"商品描述/详情"},{name:"price",required:!0,positional:!0,type:"float",help:"出售价格(元)"},{name:"condition",required:!0,positional:!0,help:"成色:全新 / 几乎全新 / 轻微使用 / 明显使用 / 老旧"},{name:"category",required:!0,positional:!0,help:"商品分类关键词(如:手机、衣服、图书)"},{name:"original_price",type:"float",help:"原价(选填,用于显示折扣)"},{name:"location",help:"所在地区(选填,如:杭州)"},{name:"images",help:"本地图片路径,多张用逗号分隔(选填,如:/tmp/a.jpg,/tmp/b.jpg)"}],columns:["status","item_id","title","price","condition","url","message"],func:async(j,B)=>{const q=P(B);await j.goto(X());await j.wait(3);let G=await j.evaluate(h());for(let A=0;A<30;A++){if(G?.requiresAuth||G?.hasPublishForm)break;await j.wait(0.5);G=await j.evaluate(h())}if(G?.requiresAuth)throw new I("www.goofish.com","发布闲鱼需要先登录,请在 Chrome 中打开 goofish.com 并完成登录");if(!G?.hasPublishForm)throw new M("Xianyu publish form was not detected","Confirm goofish.com is logged in and the publish page finished loading.");let J=await j.evaluate(_(q.category));for(let A=0;A<30;A++){if(J?.ok||J?.reason!=="category-trigger-not-found")break;await j.wait(0.5);J=await j.evaluate(_(q.category))}if(!J?.ok)throw new M(`Xianyu category selection failed: ${J?.reason||"unknown-reason"}`);await j.wait(1.5);let K=await j.evaluate(Z(q));for(let A=0;A<30;A++){if(K?.ok)break;await j.wait(0.5);K=await j.evaluate(Z(q))}if(!K?.ok){const A=Array.isArray(K?.missing)?K.missing.join(", "):"unknown";throw new M(`Xianyu publish form fill failed; missing fields: ${A}`)}await j.wait(1);if(q.images.length>0){
|
|
245
|
+
`}export const publishCommand=S({site:"xianyu",name:"publish",access:"write",description:"发布闲鱼宝贝(需先在浏览器中登录闲鱼)",domain:"www.goofish.com",strategy:F.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"title",required:!0,positional:!0,help:"商品标题"},{name:"description",required:!0,positional:!0,help:"商品描述/详情"},{name:"price",required:!0,positional:!0,type:"float",help:"出售价格(元)"},{name:"condition",required:!0,positional:!0,help:"成色:全新 / 几乎全新 / 轻微使用 / 明显使用 / 老旧"},{name:"category",required:!0,positional:!0,help:"商品分类关键词(如:手机、衣服、图书)"},{name:"original_price",type:"float",help:"原价(选填,用于显示折扣)"},{name:"location",help:"所在地区(选填,如:杭州)"},{name:"images",help:"本地图片路径,多张用逗号分隔(选填,如:/tmp/a.jpg,/tmp/b.jpg)"}],columns:["status","item_id","title","price","condition","url","message"],func:async(j,B)=>{const q=P(B);await j.goto(X());await j.wait(3);let G=await j.evaluate(h());for(let A=0;A<30;A++){if(G?.requiresAuth||G?.hasPublishForm)break;await j.wait(0.5);G=await j.evaluate(h())}if(G?.requiresAuth)throw new I("www.goofish.com","发布闲鱼需要先登录,请在 Chrome 中打开 goofish.com 并完成登录");if(!G?.hasPublishForm)throw new M("Xianyu publish form was not detected","Confirm goofish.com is logged in and the publish page finished loading.");let J=await j.evaluate(_(q.category));for(let A=0;A<30;A++){if(J?.ok||J?.reason!=="category-trigger-not-found")break;await j.wait(0.5);J=await j.evaluate(_(q.category))}if(!J?.ok)throw new M(`Xianyu category selection failed: ${J?.reason||"unknown-reason"}`);await j.wait(1.5);let K=await j.evaluate(Z(q));for(let A=0;A<30;A++){if(K?.ok)break;await j.wait(0.5);K=await j.evaluate(Z(q))}if(!K?.ok){const A=Array.isArray(K?.missing)?K.missing.join(", "):"unknown";throw new M(`Xianyu publish form fill failed; missing fields: ${A}`)}await j.wait(1);if(q.images.length>0){let A=await j.evaluate($());for(let H=0;H<30;H++){if(A?.ok)break;await j.wait(0.5);A=await j.evaluate($())}if(!A?.ok)throw new M(`Xianyu image upload input was not found: ${A?.reason||"unknown-reason"}`);try{await f(j,A.selector||'input[type="file"]',q.images);await j.wait(3)}catch(H){throw new M(`Xianyu image upload failed: ${H?.message||H}`)}}let Q=await j.evaluate(E());for(let A=0;A<30;A++){if(Q?.ok)break;await j.wait(0.5);Q=await j.evaluate(E())}if(!Q?.ok)throw new M(`Xianyu publish submit failed: ${Q?.reason||"unknown-reason"}`);await j.wait(2);let D="",T=await Y(j),N="";for(let A=0;A<10;A++){await j.wait(1.5);const H=await j.evaluate(U());T=await Y(j);if(H?.status==="published"){D=String(H.item_id||"").replace(/\D/g,"");return[{status:"published",item_id:D,title:q.title.slice(0,50),price:`¥${q.price}`,condition:q.condition,url:H.url||T,message:"发布成功"}]}if(H?.status==="failed"){N=H.message||"发布失败";break}}throw new M(N||"Xianyu publish result was not confirmed before timeout",`Open ${T} and verify whether the listing was published.`)}}),__test__={CONDITION_CHOICES:W,MAX_IMAGES:V,validateImagePaths:y,normalizePublishArgs:P,buildPublishUrl:X,getCurrentPageUrl:Y,buildFillFormEvaluate:Z,buildSelectCategoryEvaluate:_,buildFindFileInputSelectorEvaluate:$,buildDetectSuccessEvaluate:U};
|
|
@@ -1,22 +1,4 @@
|
|
|
1
|
-
import*as
|
|
2
|
-
(async () => {
|
|
3
|
-
const input = document.querySelector(${JSON.stringify(U)});
|
|
4
|
-
if (!input) return { ok: false, error: '页面上找不到视频输入框' };
|
|
5
|
-
try {
|
|
6
|
-
const binary = atob(${JSON.stringify(K)});
|
|
7
|
-
const bytes = new Uint8Array(binary.length);
|
|
8
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
9
|
-
const dt = new DataTransfer();
|
|
10
|
-
dt.items.add(new File([new Blob([bytes], { type: 'video/mp4' })], ${JSON.stringify(Y.basename(z))}, { type: 'video/mp4' }));
|
|
11
|
-
Object.defineProperty(input, 'files', { value: dt.files, writable: false });
|
|
12
|
-
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
13
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
14
|
-
return { ok: true };
|
|
15
|
-
} catch (e) {
|
|
16
|
-
return { ok: false, error: 'File 构造失败: ' + e.message };
|
|
17
|
-
}
|
|
18
|
-
})()
|
|
19
|
-
`)}async function c(q,z=600000){const G=2000,K=Math.ceil(z/G);for(let J=0;J<K;J++){if((await q.evaluate(`(() => {
|
|
1
|
+
import*as F from"node:fs";import*as X from"node:path";import{cli as A,Strategy as D}from"@jackwener/opencli/registry";import{ArgumentError as Z,CommandExecutionError as $}from"@jackwener/opencli/errors";import{assertLiteralContent as C}from"../_shared/content-guard.js";import{setInputFiles as P}from"../_shared/file-inject.js";const O="https://creator.xiaohongshu.com/publish/publish?from=homepage&target=video",I=20,R=1000,b=10,k=[".mp4",".mov",".flv",".f4v",".mkv",".rm",".rmvb",".m4v",".mpg",".mpeg",".ts"],x='input.upload-input, input[type="file"][accept*=".mp4"]',_='input[placeholder*="标题"]',L=['input[placeholder*="标题"]','input.d-text[placeholder*="标题"]'],Y=".tiptap.ProseMirror",h="#creator-editor-topic-container",u=["_onPublish","onPublish","_onSubmit","_handlePublish"],T=["_onSave","_onSaveDraft","_onDraft"];function m(q){const z=X.resolve(q);if(!F.existsSync(z))throw new Z(`视频文件不存在: ${z}`);const G=X.extname(z).toLowerCase();if(!k.includes(G))throw new Z(`不支持的视频格式 "${G}",支持: ${k.join(" ")}`);return z}async function d(q,z=15000){const G=500,K=Math.max(1,Math.ceil(z/G));for(let J=0;J<K;J++){if(await q.evaluate(`(() => !!document.querySelector(${JSON.stringify(x)}))()`))return!0;if(J<K-1)await q.wait({time:G/1000})}return!1}async function c(q,z){if(!await d(q))return{ok:!1,error:"视频发布页未渲染出上传输入框(等待 15s 超时)"};try{await P(q,x,[z]);return{ok:!0}}catch(K){return{ok:!1,error:K instanceof Error?K.message:String(K)}}}async function l(q,z=600000){const G=2000,K=Math.ceil(z/G);for(let J=0;J<K;J++){if((await q.evaluate(`(() => {
|
|
20
2
|
const b = document.querySelector('xhs-publish-btn');
|
|
21
3
|
if (!b) return { ready: false, reason: 'no_button' };
|
|
22
4
|
return {
|
|
@@ -24,7 +6,7 @@ import*as X from"node:fs";import*as Y from"node:path";import{cli as h,Strategy a
|
|
|
24
6
|
submitDisabled: b.getAttribute('submit-disabled'),
|
|
25
7
|
isPublish: b.getAttribute('is-publish'),
|
|
26
8
|
};
|
|
27
|
-
})()`))?.ready)return!0;await q.wait({time:G/1000})}return!1}async function
|
|
9
|
+
})()`))?.ready)return!0;await q.wait({time:G/1000})}return!1}async function E(q,z=20000){const G=500,K=Math.ceil(z/G);for(let J=0;J<K;J++){if(await q.evaluate(`(() => ${JSON.stringify(L)}.some((s) => !!document.querySelector(s)) && !!document.querySelector(${JSON.stringify(Y)}))()`))return!0;await q.wait({time:G/1000})}return!1}async function n(q,z){await q.typeText(_,z);await q.wait({time:0.3});const G=await q.evaluate(`((sels) => { const el = sels.map((s) => document.querySelector(s)).find(Boolean); return el ? (el.value || '') : ''; })(${JSON.stringify(L)})`);if(G.trim()!==z){await q.screenshot({path:"/tmp/xhs_video_title_debug.png"});throw new $(`填写标题失败,期望 "${z}",实际 "${G}"。截图: /tmp/xhs_video_title_debug.png`)}}async function i(q,z){await q.typeText(Y,z);await q.wait({time:0.5});const G=await q.evaluate(`((s) => { const el = document.querySelector(s); return el ? (el.innerText || el.textContent || '').replace(/\\u200b/g, '').trim() : ''; })(${JSON.stringify(Y)})`),K=z.replace(/\s+/g,"").slice(0,20);if(K&&!G.replace(/\s+/g,"").includes(K)){await q.screenshot({path:"/tmp/xhs_video_body_debug.png"});throw new $(`正文写入校验失败,实际读到 "${G.slice(0,80)}"。截图: /tmp/xhs_video_body_debug.png`)}}async function o(q){await q.evaluate(`((s) => {
|
|
28
10
|
const el = document.querySelector(s);
|
|
29
11
|
if (!el) return false;
|
|
30
12
|
el.focus();
|
|
@@ -35,7 +17,7 @@ import*as X from"node:fs";import*as Y from"node:path";import{cli as h,Strategy a
|
|
|
35
17
|
sel?.removeAllRanges();
|
|
36
18
|
sel?.addRange(range);
|
|
37
19
|
return true;
|
|
38
|
-
})(${JSON.stringify(
|
|
20
|
+
})(${JSON.stringify(Y)})`)}async function s(q,z){const G=[];for(const K of z){await o(q);await q.typeText(Y,` #${K}`);let J=!1;for(let H=0;H<12;H++){await q.wait({time:0.5});J=await q.evaluate(`(() => { const c = document.querySelector(${JSON.stringify(h)}); if (!c || c.offsetParent === null) return false; return !!c.querySelector('.item'); })()`);if(J)break}if(!J)continue;if(await q.evaluate(`((cfg) => {
|
|
39
21
|
const c = document.querySelector(cfg.dropdown);
|
|
40
22
|
if (!c) return false;
|
|
41
23
|
const items = Array.from(c.querySelectorAll('.item')).filter((el) => el.offsetParent !== null);
|
|
@@ -45,7 +27,7 @@ import*as X from"node:fs";import*as Y from"node:path";import{cli as h,Strategy a
|
|
|
45
27
|
const exact = items.find((el) => norm(el.innerText || el.textContent).includes(norm(cfg.topic)));
|
|
46
28
|
(exact || items[0]).click();
|
|
47
29
|
return true;
|
|
48
|
-
})(${JSON.stringify({dropdown:
|
|
30
|
+
})(${JSON.stringify({dropdown:h,topic:K})})`)){G.push(K);await q.wait({time:0.5})}}return G}async function r(q,z){const G=z?["暂存离开","存草稿"]:["发布","发布笔记"];return q.evaluate(`
|
|
49
31
|
((cfg) => {
|
|
50
32
|
const { isDraftMode, publishNames, draftNames, labels } = cfg;
|
|
51
33
|
const isVisible = (el) => {
|
|
@@ -72,9 +54,9 @@ import*as X from"node:fs";import*as Y from"node:path";import{cli as h,Strategy a
|
|
|
72
54
|
}
|
|
73
55
|
}
|
|
74
56
|
return { ok: false, via: 'none', hosts: hosts.length, lastMethodError };
|
|
75
|
-
})(${JSON.stringify({isDraftMode:z,publishNames:
|
|
76
|
-
`)}
|
|
77
|
-
`);if(!await
|
|
57
|
+
})(${JSON.stringify({isDraftMode:z,publishNames:u,draftNames:T,labels:G})})
|
|
58
|
+
`)}A({site:"xiaohongshu",name:"publish-video",access:"write",description:"小红书发布视频笔记(creator center UI 自动化)",domain:"creator.xiaohongshu.com",strategy:D.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不展开 @文件 引用;长正文用 --file)"},{name:"file",required:!1,help:"从本机文件读取正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题(≤20字)"},{name:"video",required:!0,help:"视频文件路径 (mp4/mov/mkv/...)"},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号,最多10个"},{name:"draft",type:"bool",default:!1,help:"暂存为草稿,不直接发布"}],columns:["status","detail"],func:async(q,z)=>{if(!q)throw Error("Browser page required");const G=String(z.title??"").trim(),K=z.file?String(z.file).trim():"";let J=String(z.content??"").trim();if(K){if(J)throw new Z("正文位置参数和 --file 只能二选一");const Q=X.resolve(K);if(!F.existsSync(Q))throw new Z(`--file 指向的文件不存在: ${Q}`);J=F.readFileSync(Q,"utf-8").trim();if(!J)throw new Z(`--file 指向的文件内容为空: ${Q}`)}const v=z.topics?String(z.topics).split(",").map((Q)=>Q.trim().replace(/^#/,"")).filter(Boolean):[],H=Boolean(z.draft);if(!G)throw new Z("--title 必填");if(G.length>I)throw new Z(`标题 ${G.length} 字,需 ≤ ${I} 字`);if(!J)throw new Z("缺少正文:给位置参数 <content>,或用 --file <本机文件路径>");C(J,{fileFlag:"--file"});if(J.length>R)throw new Z(`正文 ${J.length} 字,需 ≤ ${R} 字`);if(!z.video)throw new Z("--video 必填");if(v.length>b)throw new Z(`话题过多: ${v.length}(最多 ${b} 个)`);const U=m(String(z.video));await q.goto(O);let V="";for(let Q=0;Q<30;Q++){await q.wait({time:0.5});V=await q.evaluate("() => location.href");if(V.includes("creator.xiaohongshu.com"))break}if(!V.includes("creator.xiaohongshu.com")){await q.screenshot({path:"/tmp/xhs_video_redirect_debug.png"});throw new $(`被重定向离开创作者中心(当前 ${V})——登录态可能已过期。`+"用 ppcli xiaohongshu creator-profile 重新登录。截图: /tmp/xhs_video_redirect_debug.png")}const N=await c(q,U);if(!N.ok){await q.screenshot({path:"/tmp/xhs_video_upload_debug.png"});throw new $(`视频上传失败: ${N.error??"unknown"}。截图: /tmp/xhs_video_upload_debug.png`)}if(!await E(q)){await q.screenshot({path:"/tmp/xhs_video_form_debug.png"});throw new $("上传后编辑表单未就绪(标题/正文未出现)。截图: /tmp/xhs_video_form_debug.png")}process.stderr.write(` 等待视频转码(可发布状态)...
|
|
59
|
+
`);if(!await l(q)){await q.screenshot({path:"/tmp/xhs_video_transcode_debug.png"});throw new $("视频转码超时(10min),发布按钮未解禁。截图: /tmp/xhs_video_transcode_debug.png")}await n(q,G);await q.wait({time:0.4});await i(q,J);await q.wait({time:0.5});let y=[];if(v.length){y=await s(q,v);const Q=v.filter((w)=>!y.includes(w));if(Q.length){await q.screenshot({path:"/tmp/xhs_video_topic_debug.png"});throw new $(`话题未能生成实体: ${Q.join(", ")}(联想下拉未出现或无匹配项)。截图: /tmp/xhs_video_topic_debug.png`)}}const f=await q.evaluate("(() => { const b = document.querySelector('xhs-publish-btn'); return !!b && b.getAttribute('submit-disabled') === 'false'; })()");if(!H&&!f){await q.screenshot({path:"/tmp/xhs_video_prepublish_debug.png"});throw new $("发布前复查:发布按钮不可用(submit-disabled≠false)。截图: /tmp/xhs_video_prepublish_debug.png")}const W=await r(q,H);if(!W?.ok){await q.screenshot({path:"/tmp/xhs_video_submit_debug.png"});throw new $(`找不到「${H?"暂存离开":"发布"}」触发方式(hosts=${W?.hosts??0}${W?.lastMethodError?`, lastMethodError=${W.lastMethodError}`:""})。截图: /tmp/xhs_video_submit_debug.png`)}await q.wait({time:4});let j=await q.evaluate("() => location.href");const M=H?["草稿已保存","暂存成功","保存成功","保存于"]:["发布成功","上传成功"],B=await q.evaluate(`
|
|
78
60
|
((markers) => {
|
|
79
61
|
for (const el of document.querySelectorAll('*')) {
|
|
80
62
|
if (el.tagName === 'STYLE' || el.tagName === 'SCRIPT') continue;
|
|
@@ -83,5 +65,5 @@ import*as X from"node:fs";import*as Y from"node:path";import{cli as h,Strategy a
|
|
|
83
65
|
if (text && text.length <= 40 && markers.some((m) => text.includes(m))) return text;
|
|
84
66
|
}
|
|
85
67
|
return '';
|
|
86
|
-
})(${JSON.stringify(
|
|
87
|
-
`),
|
|
68
|
+
})(${JSON.stringify(M)})
|
|
69
|
+
`),S=!j.includes("/publish/publish");if(!B&&!S){await q.wait({time:3});j=await q.evaluate("() => location.href")}if(!(B.length>0||!j.includes("/publish/publish"))){await q.screenshot({path:"/tmp/xhs_video_verify_debug.png"});throw new $(`${H?"暂存":"发布"}结果无法确认:未见成功提示、也未跳离发布页。当前 URL: ${j}。截图: /tmp/xhs_video_verify_debug.png`)}return[{status:`✅ ${H?"暂存成功":"发布成功"}`,detail:[`"${G}"`,X.basename(U),y.length?`话题: ${y.map((Q)=>`#${Q}`).join(" ")}`:"",B||j].filter(Boolean).join(" · ")}]}});
|