publishport-opencli 1.0.26 → 1.0.28
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 +308 -18
- package/clis/_shared/video-publish.js +2 -1
- package/clis/baijiahao/article.js +17 -1
- package/clis/bilibili/coin.js +1 -1
- package/clis/bilibili/danmaku.js +5 -0
- package/clis/bilibili/favorite-add.js +1 -1
- package/clis/bilibili/upload.js +1 -1
- package/clis/bilibili/video-edit.js +3 -0
- package/clis/bilibili/video.js +1 -1
- package/clis/jike/api.js +2 -2
- package/clis/jike/comments.js +1 -0
- package/clis/jike/delete.js +1 -0
- package/clis/jike/topic.js +1 -20
- package/clis/kuaishou/auth.js +8 -1
- package/clis/kuaishou/publish.js +6 -6
- package/clis/nowcoder/comment.js +1 -1
- 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/like.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 +14 -17
- package/clis/nowcoder/watch.js +1 -1
- package/clis/quora/create-answer.js +12 -8
- package/clis/reddit/comment.js +28 -7
- package/clis/reddit/upvote.js +21 -5
- package/clis/xiaohongshu/publish.js +1 -1
- package/clis/zhihu/comment.js +2 -2
- package/dist/src/cli.js +23 -23
- package/dist/src/commands/auth.js +3 -3
- package/dist/src/doctor.js +1 -1
- package/dist/src/pipeline/template.js +3 -1
- package/package.json +1 -1
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
import{ArgumentError as
|
|
1
|
+
import{ArgumentError as u,AuthRequiredError as g,CommandExecutionError as t}from"@jackwener/opencli/errors";import{cli as h,Strategy as p}from"@jackwener/opencli/registry";import{assertLiteralContent as q}from"../_shared/content-guard.js";import{tagEditorJs as y,readEditorTextJs as A,insertEditorText as S,clickSubmitJs as b,requireExecute as L,resolveTextInput as R}from"./shared.js";const E=20,B=15,v=["answer","add answer","回答","写回答","répondre","responder","antworten","回答する"],U=["\\bpost\\b","submit","发布","提交","publier","publicar","veröffentlichen","投稿"],x=["\\bpost\\b","done","save","continue","confirm","确定","完成","保存","继续"];export function probeAndOpenJs(e=!0){return`(() => {
|
|
2
2
|
const path = location.pathname || '';
|
|
3
3
|
if (/\\blogin\\b/i.test(path + (location.search || ''))) return 'login';
|
|
4
4
|
const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
|
|
5
5
|
// 编辑器已开着?
|
|
6
6
|
const scope = document.querySelector('[role="dialog"]') || document;
|
|
7
|
-
const editor = Array.from(scope.querySelectorAll('[contenteditable="true"]'))
|
|
7
|
+
const editor = Array.from(scope.querySelectorAll('[contenteditable="true"][data-kind="doc"]'))
|
|
8
8
|
.filter(visible)
|
|
9
9
|
.find((el) => (el.getBoundingClientRect().height || 0) >= 24);
|
|
10
10
|
if (editor) return 'editor';
|
|
11
|
+
if (!${JSON.stringify(e)}) return 'wait';
|
|
11
12
|
// 否则找「Answer」入口按钮点开
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
.
|
|
13
|
+
// 顶部 /answer 导航 aria-label 含 "questions to answer",绝不能作为回答入口。
|
|
14
|
+
const re = new RegExp(${JSON.stringify("^(?:"+v.join("|")+")(?:\\s*[·•]\\s*[\\d.,Kk万]+)?$")}, 'i');
|
|
15
|
+
const buttons = Array.from(document.querySelectorAll('button, [role="button"]'))
|
|
16
|
+
.filter(el => visible(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true')
|
|
17
|
+
.filter(el => re.test((el.getAttribute('aria-label') || el.textContent || '').replace(/\\s+/g, ' ').trim()));
|
|
18
|
+
if (buttons.length > 1) return 'ambiguous';
|
|
19
|
+
const btn = buttons[0];
|
|
16
20
|
if (btn) { btn.click(); return 'clicked'; }
|
|
17
21
|
return 'wait';
|
|
18
|
-
})()`}
|
|
22
|
+
})()`}h({site:"quora",name:"create-answer",access:"write",description:"Publish an answer to a Quora question from your logged-in account. Pass the target question URL and the answer body (multi-line allowed). Write a genuine, substantive answer — Quora aggressively flags low-effort/automated content.",domain:"quora.com",strategy:p.UI,browser:!0,args:[{name:"questionUrl",type:"string",required:!0,positional:!0,help:"Target Quora question URL (https://www.quora.com/<question-slug>)"},{name:"content",type:"string",positional:!0,help:"Answer body text, or use --file for long content"},{name:"file",type:"string",help:"Read answer body from a local UTF-8 text file"},{name:"execute",type:"boolean",help:"Actually post the answer"}],columns:["status","questionUrl","message"],func:async(e,n)=>{if(!e)throw new t("Browser session required for quora create-answer");L(n);const r=String(n.questionUrl??"").trim();if(!r)throw new u("Question URL is required.");let c;try{c=new URL(r)}catch{throw new u(`Invalid question URL: ${r}`)}if(!/(^|\.)quora\.com$/.test(c.hostname))throw new u("URL must be a quora.com question URL.");const a=await R(n,{field:"content",label:"Answer content"});if(!n.file)q(a,{label:"回答正文",fileFlag:"--file"});await e.goto(r,{waitUntil:"none"});let w=!1,d=!1;for(let o=0;o<E;o++){const i=await e.evaluate(probeAndOpenJs(!d));if(i==="login")throw new g("www.quora.com","Quora 重定向到登录页——未登录");if(i==="editor"){w=!0;break}if(i==="ambiguous")throw new t("当前页面存在多个回答入口,无法唯一定位目标问题的 Answer 按钮。");if(i==="clicked")d=!0;await e.wait(1)}if(!w)throw new t("未能打开回答编辑器——问题可能不可回答,或页面结构已变(需真机 recon 收紧锚点)。");if(await e.evaluate(y())!=="ready")throw new t("回答编辑器打标失败——未定位到可编辑正文区。");const f=await S(e,a);if(!f?.ok)throw new t(f?.message??"无法把正文写入 Quora 回答编辑器。");const m=await e.evaluate(b(U));if(!m?.ok)throw new t(m?.message??"未找到 Quora 回答提交按钮。");await e.wait(2);if((await e.evaluate(`(() => {
|
|
19
23
|
const dlg = document.querySelector('[role="dialog"]');
|
|
20
24
|
if (!dlg || (dlg.offsetParent === null && dlg.getClientRects().length === 0)) return { present: false };
|
|
21
25
|
return { present: true };
|
|
22
|
-
})()`))?.present){await e.evaluate(
|
|
26
|
+
})()`))?.present){await e.evaluate(b(x));await e.wait(2)}const s=(o)=>String(o||"").replace(/\s+/g," ").trim(),l=await e.evaluate(A());if(l!=null&&s(l).length>0&&s(l)===s(a))throw new t("Quora 回答提交后正文仍在编辑器中——提交被拦(限流/风控/无权限,或 credential 弹窗未确认)。回查后再决定是否重试。");return[{status:"success",questionUrl:r,message:"Answer posted."}]}});
|
package/clis/reddit/comment.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import{CommandExecutionError as
|
|
1
|
+
import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{cli as o,Strategy as a}from"@jackwener/opencli/registry";o({site:"reddit",name:"comment",access:"write",description:"Post a comment on a Reddit post",domain:"reddit.com",strategy:a.COOKIE,browser:!0,args:[{name:"post-id",type:"string",required:!0,positional:!0,help:"Post ID (e.g. 1abc123) or fullname (t3_xxx)"},{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"Comment text"}],columns:["status","message","comment_id","url"],func:async(t,r)=>{if(!t)throw new s("Browser session required");await t.goto("https://www.reddit.com");const e=await t.evaluate(`(async () => {
|
|
2
2
|
try {
|
|
3
|
-
let postId = ${JSON.stringify(
|
|
3
|
+
let postId = ${JSON.stringify(r["post-id"])};
|
|
4
4
|
const urlMatch = postId.match(/comments\\/([a-z0-9]+)/);
|
|
5
5
|
if (urlMatch) postId = urlMatch[1];
|
|
6
6
|
const fullname = postId.startsWith('t3_') || postId.startsWith('t1_')
|
|
7
7
|
? postId : 't3_' + postId;
|
|
8
8
|
|
|
9
|
-
const text = ${JSON.stringify(
|
|
9
|
+
const text = ${JSON.stringify(r.text)};
|
|
10
10
|
|
|
11
11
|
// Get modhash
|
|
12
12
|
const meRes = await fetch('/api/me.json', { credentials: 'include' });
|
|
@@ -23,14 +23,35 @@ import{CommandExecutionError as o}from"@jackwener/opencli/errors";import{cli as
|
|
|
23
23
|
+ (modhash ? '&uh=' + encodeURIComponent(modhash) : ''),
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const raw = await res.text();
|
|
27
|
+
let data;
|
|
28
|
+
try { data = JSON.parse(raw); } catch {
|
|
29
|
+
const doc = new DOMParser().parseFromString(raw, 'text/html');
|
|
30
|
+
doc.querySelectorAll('script,style').forEach(el => el.remove());
|
|
31
|
+
const detail = doc.body.textContent.replace(/\\s+/g, ' ').trim().slice(0, 500);
|
|
32
|
+
return { ok: false, message: 'HTTP ' + res.status + ': non-JSON response: ' + detail };
|
|
33
|
+
}
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const detail = data?.json?.errors?.map(e => e.join(': ')).join('; ') || data?.message || data?.reason || data?.error || 'Reddit rejected request';
|
|
36
|
+
return { ok: false, message: 'HTTP ' + res.status + ': ' + String(detail).slice(0, 500) };
|
|
37
|
+
}
|
|
28
38
|
const errors = data?.json?.errors;
|
|
29
39
|
if (errors && errors.length > 0) {
|
|
30
40
|
return { ok: false, message: errors.map(e => e.join(': ')).join('; ') };
|
|
31
41
|
}
|
|
32
|
-
|
|
42
|
+
const things = data?.json?.data?.things;
|
|
43
|
+
const created = Array.isArray(things) ? things.find(t => t?.kind === 't1')?.data : null;
|
|
44
|
+
const commentId = created?.id || String(created?.name || '').replace(/^t1_/, '');
|
|
45
|
+
if (!/^[a-z0-9]+$/i.test(commentId || '')) {
|
|
46
|
+
return { ok: false, message: 'Reddit response did not include a created comment id; publication is unconfirmed' };
|
|
47
|
+
}
|
|
48
|
+
let permalink = '';
|
|
49
|
+
if (created.permalink) {
|
|
50
|
+
const parsed = new URL(created.permalink, 'https://www.reddit.com');
|
|
51
|
+
if (parsed.protocol === 'https:' && (parsed.hostname === 'reddit.com' || parsed.hostname.endsWith('.reddit.com'))) permalink = parsed.href;
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, notice: 'Comment submitted on ' + fullname + '; public visibility not verified', commentId, permalink };
|
|
33
54
|
} catch (e) {
|
|
34
55
|
return { ok: false, message: e.toString() };
|
|
35
56
|
}
|
|
36
|
-
})()`);return[{status:
|
|
57
|
+
})()`);if(!e?.ok)throw new s(e?.message||"Reddit comment returned no result","403 时停止写入并检查平台提示;断连或回执不完整时,先用 user-comments 或 read 回查,禁止直接重复评论。");return[{status:"success",message:e.notice,comment_id:e.commentId,url:e.permalink}]}});
|
package/clis/reddit/upvote.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{cli as n,Strategy as i}from"@jackwener/opencli/registry";n({site:"reddit",name:"upvote",access:"write",description:"Upvote or downvote a Reddit post",domain:"reddit.com",strategy:i.COOKIE,browser:!0,args:[{name:"post-id",type:"string",required:!0,positional:!0,help:"Post ID (e.g. 1abc123) or fullname (t3_xxx)"},{name:"direction",type:"string",default:"up",help:"Vote direction: up, down, none"}],columns:["status","message"],func:async(e,
|
|
1
|
+
import{ArgumentError as a,CommandExecutionError as s}from"@jackwener/opencli/errors";import{cli as n,Strategy as i}from"@jackwener/opencli/registry";n({site:"reddit",name:"upvote",access:"write",description:"Upvote or downvote a Reddit post",domain:"reddit.com",strategy:i.COOKIE,browser:!0,args:[{name:"post-id",type:"string",required:!0,positional:!0,help:"Post ID (e.g. 1abc123) or fullname (t3_xxx)"},{name:"direction",type:"string",default:"up",help:"Vote direction: up, down, none"}],columns:["status","message"],func:async(e,o)=>{const r=o.direction??"up";if(!["up","down","none"].includes(r))throw new a("--direction must be up, down, or none");if(!e)throw new s("Browser session required");await e.goto("https://www.reddit.com");const t=await e.evaluate(`(async () => {
|
|
2
2
|
try {
|
|
3
|
-
let postId = ${JSON.stringify(
|
|
3
|
+
let postId = ${JSON.stringify(o["post-id"])};
|
|
4
4
|
// Extract ID from URL if needed
|
|
5
5
|
const urlMatch = postId.match(/comments\\/([a-z0-9]+)/);
|
|
6
6
|
if (urlMatch) postId = urlMatch[1];
|
|
@@ -8,7 +8,7 @@ import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{cli as
|
|
|
8
8
|
const fullname = postId.startsWith('t3_') || postId.startsWith('t1_')
|
|
9
9
|
? postId : 't3_' + postId;
|
|
10
10
|
|
|
11
|
-
const dir = ${JSON.stringify(
|
|
11
|
+
const dir = ${JSON.stringify(r)};
|
|
12
12
|
const direction = dir === 'down' ? -1 : dir === 'none' ? 0 : 1;
|
|
13
13
|
|
|
14
14
|
// Get modhash from Reddit config
|
|
@@ -33,11 +33,27 @@ import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{cli as
|
|
|
33
33
|
+ (modhash ? '&uh=' + encodeURIComponent(modhash) : ''),
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
const raw = await res.text();
|
|
37
|
+
let data;
|
|
38
|
+
try { data = JSON.parse(raw); } catch {
|
|
39
|
+
const doc = new DOMParser().parseFromString(raw, 'text/html');
|
|
40
|
+
doc.querySelectorAll('script,style').forEach(el => el.remove());
|
|
41
|
+
const detail = doc.body.textContent.replace(/\\s+/g, ' ').trim().slice(0, 500);
|
|
42
|
+
return { ok: false, message: 'HTTP ' + res.status + ': non-JSON response: ' + detail };
|
|
43
|
+
}
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const detail = data?.json?.errors?.map(e => e.join(': ')).join('; ') || data?.message || data?.reason || data?.error || 'Reddit rejected request';
|
|
46
|
+
return { ok: false, message: 'HTTP ' + res.status + ': ' + String(detail).slice(0, 500) };
|
|
47
|
+
}
|
|
48
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return { ok: false, message: 'Reddit vote returned a malformed response' };
|
|
49
|
+
if (data?.json?.errors?.length) {
|
|
50
|
+
return { ok: false, message: data.json.errors.map(e => e.join(': ')).join('; ') };
|
|
51
|
+
}
|
|
52
|
+
if (data?.error) return { ok: false, message: String(data.message || data.error).slice(0, 500) };
|
|
37
53
|
|
|
38
54
|
const labels = { '1': 'Upvoted', '-1': 'Downvoted', '0': 'Vote removed' };
|
|
39
55
|
return { ok: true, message: (labels[String(direction)] || 'Voted') + ' ' + fullname };
|
|
40
56
|
} catch (e) {
|
|
41
57
|
return { ok: false, message: e.toString() };
|
|
42
58
|
}
|
|
43
|
-
})()`);return[{status:
|
|
59
|
+
})()`);if(!t?.ok)throw new s(t?.message||"Reddit vote returned no result","403 时停止写入并检查平台提示;网络中断时先回查当前投票状态,不要循环重试。");return[{status:"success",message:t.message}]}});
|
|
@@ -537,7 +537,7 @@ import*as D from"node:fs";import*as M from"node:path";import{CommandExecutionErr
|
|
|
537
537
|
const dateText = (card.querySelector('.time_status, .time')?.innerText || '').trim();
|
|
538
538
|
return { id, title, dateText };
|
|
539
539
|
}).filter((entry) => entry.title);
|
|
540
|
-
})()`))}catch{return null}if(Array.isArray(i)){const a=i.find((s)=>{const d=String(s.title||"").trim();if(!(d===o||d.startsWith(o)||o.startsWith(d)))return!1;const c=String(s.dateText||"").match(/(\d{4})年(\d{2})月(\d{2})日 (\d{2}):(\d{2})/);if(!c)return!0;const m=Date.UTC(Number(c[1]),Number(c[2])-1,Number(c[3]),Number(c[4]),Number(c[5]))-28800000;return Math.abs(Date.now()-m)<=93600000});if(a)return a}}return null}mt({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:pt.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:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${G} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${S}。可选: ${X.map(([t,e])=>`${t}(${e})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号。平台话题库匹配不上的会自动跳过并在结果中提示,不影响发布"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail","url"],func:async(t,e)=>{if(!t)throw Error("Browser page required");const n=String(e.title??"").trim(),o=e.file?String(e.file).trim():"";let r=String(e.content??"").trim();if(o){if(r)throw new b("正文位置参数和 --file 只能二选一,不能同时给");const l=M.resolve(o);if(!D.existsSync(l))throw new b(`--file 指向的文件不存在: ${l}`);r=D.readFileSync(l,"utf-8").trim();if(!r)throw new b(`--file 指向的文件内容为空: ${l}`)}const i=e.images?String(e.images).split(",").map((l)=>l.trim()).filter(Boolean):[],a=e.topics?String(e.topics).split(",").map((l)=>l.trim()).filter(Boolean):[],s=Boolean(e.draft),d=e["card-text"]?String(e["card-text"]):"",c=d?d.split(G).map((l)=>l.trim()).filter(Boolean):[],m=e["card-style"]?String(e["card-style"]).trim():"",w=c.length>0;if(!n)throw new b("--title is required");if(n.length>z)throw new b(`Title is ${n.length} chars — must be ≤ ${z}`);if(!r)throw new b("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");wt(r,{fileFlag:"--file"});if(!w&&i.length===0)throw new b("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(i.length>V)throw new b(`Too many images: ${i.length} (max ${V})`);if(w&&i.some((l)=>M.extname(l).toLowerCase()===".gif"))throw new b("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const f=Tt(i);await t.goto(yt);await t.wait({time:1});await t.evaluate("() => { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }").catch(()=>{return});let k="";for(let l=0;l<30;l++){await t.wait({time:0.5});k=await t.evaluate("() => location.href");if(k.includes("creator.xiaohongshu.com"))break}if(!k.includes("creator.xiaohongshu.com")){await t.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${k}) — session may have expired. `+"Re-capture browser login via: ppcli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const E=await Nt(t);if((await Ft(t,E?.ok?5000:2000)).state==="video_surface"){await t.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const l=E?.ok?`clicked "${E.text}"`:`visible candidates: ${(E?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${l}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}const A=await jt(t);let C=m;if(w)C=await Jt(t,c,m);else{const l=typeof t.startNetworkCapture==="function"?await t.startNetworkCapture("").catch(()=>!1):!1,g=await tt(t,f);if(!g.ok){await t.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new u(`图片注入失败:${g.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_upload_debug.png")}await t.wait({time:K/1000});const x=await dt(t,f.length,l);if(!x.ok){await t.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new u(`图片上传未全部完成:只确认 ${x.completed}/${f.length} 张传到小红书 CDN,`+"继续发布会丢图(通常只剩封面)。直接重新运行本 publish 命令即可(会自动重置发布页重传);"+"请勿手动修改当前页面。Debug: /tmp/xhs_publish_upload_debug.png")}await et(t);if(A.length){await ct(t,A);await t.wait({time:0.5})}const N=await at(t);if(N&&typeof N.count==="number"&&N.count>f.length){await t.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new u(`清掉恢复态旧图后编辑器内仍有 ${N.count} 张图,超过本次注入的 ${f.length} 张。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。Debug: /tmp/xhs_publish_dup_debug.png")}}if(!await Ut(t)){await t.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new u("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(w)await st(t,c.length,"文字配图 generated images");if(w&&f.length>0){const l=typeof t.startNetworkCapture==="function"?await t.startNetworkCapture("").catch(()=>!1):!1,g=await tt(t,f);if(!g.ok){await t.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new u(`追加图片失败:${g.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await t.wait({time:K/1000});const x=await dt(t,f.length,l);if(!x.ok){await t.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new u(`追加图片上传未全部完成:只确认 ${x.completed}/${f.length} 张传到小红书 CDN。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await et(t);await st(t,c.length+f.length,"文字配图 appended images")}if(w&&A.length){await ct(t,A);await t.wait({time:0.5})}await nt(t,F,n,"title");await v(t,0.6);await nt(t,Q,r,"content");await v(t,0.6);let R=[],I=[];if(a.length){const l=await Ct(t,Q,a);R=l.added;I=l.failed}await v(t,1,0.6);const O=await xt(t,["xhs-publish-btn"]);if(O){await gt(t,O.x,O.y);await v(t,0.4)}const U=s?["暂存离开","存草稿"]:["发布","发布笔记"],p=await t.evaluate(`
|
|
540
|
+
})()`))}catch{return null}if(Array.isArray(i)){const a=i.find((s)=>{const d=String(s.title||"").trim();if(!(d===o||d.startsWith(o)||o.startsWith(d)))return!1;const c=String(s.dateText||"").match(/(\d{4})年(\d{2})月(\d{2})日 (\d{2}):(\d{2})/);if(!c)return!0;const m=Date.UTC(Number(c[1]),Number(c[2])-1,Number(c[3]),Number(c[4]),Number(c[5]))-28800000;return Math.abs(Date.now()-m)<=93600000});if(a)return a}}return null}mt({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:pt.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:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${G} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${S}。可选: ${X.map(([t,e])=>`${t}(${e})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号。平台话题库匹配不上的会自动跳过并在结果中提示,不影响发布"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"},{name:"timeout",type:"int",default:300,help:"整段发布流程的等待预算(秒);超时后先用 creator-notes 回查,勿直接重复发布"}],columns:["status","detail","url"],func:async(t,e)=>{if(!t)throw Error("Browser page required");const n=String(e.title??"").trim(),o=e.file?String(e.file).trim():"";let r=String(e.content??"").trim();if(o){if(r)throw new b("正文位置参数和 --file 只能二选一,不能同时给");const l=M.resolve(o);if(!D.existsSync(l))throw new b(`--file 指向的文件不存在: ${l}`);r=D.readFileSync(l,"utf-8").trim();if(!r)throw new b(`--file 指向的文件内容为空: ${l}`)}const i=e.images?String(e.images).split(",").map((l)=>l.trim()).filter(Boolean):[],a=e.topics?String(e.topics).split(",").map((l)=>l.trim()).filter(Boolean):[],s=Boolean(e.draft),d=e["card-text"]?String(e["card-text"]):"",c=d?d.split(G).map((l)=>l.trim()).filter(Boolean):[],m=e["card-style"]?String(e["card-style"]).trim():"",w=c.length>0;if(!n)throw new b("--title is required");if(n.length>z)throw new b(`Title is ${n.length} chars — must be ≤ ${z}`);if(!r)throw new b("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");wt(r,{fileFlag:"--file"});if(!w&&i.length===0)throw new b("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(i.length>V)throw new b(`Too many images: ${i.length} (max ${V})`);if(w&&i.some((l)=>M.extname(l).toLowerCase()===".gif"))throw new b("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const f=Tt(i);await t.goto(yt);await t.wait({time:1});await t.evaluate("() => { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }").catch(()=>{return});let k="";for(let l=0;l<30;l++){await t.wait({time:0.5});k=await t.evaluate("() => location.href");if(k.includes("creator.xiaohongshu.com"))break}if(!k.includes("creator.xiaohongshu.com")){await t.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${k}) — session may have expired. `+"Re-capture browser login via: ppcli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const E=await Nt(t);if((await Ft(t,E?.ok?5000:2000)).state==="video_surface"){await t.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const l=E?.ok?`clicked "${E.text}"`:`visible candidates: ${(E?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${l}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}const A=await jt(t);let C=m;if(w)C=await Jt(t,c,m);else{const l=typeof t.startNetworkCapture==="function"?await t.startNetworkCapture("").catch(()=>!1):!1,g=await tt(t,f);if(!g.ok){await t.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new u(`图片注入失败:${g.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_upload_debug.png")}await t.wait({time:K/1000});const x=await dt(t,f.length,l);if(!x.ok){await t.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new u(`图片上传未全部完成:只确认 ${x.completed}/${f.length} 张传到小红书 CDN,`+"继续发布会丢图(通常只剩封面)。直接重新运行本 publish 命令即可(会自动重置发布页重传);"+"请勿手动修改当前页面。Debug: /tmp/xhs_publish_upload_debug.png")}await et(t);if(A.length){await ct(t,A);await t.wait({time:0.5})}const N=await at(t);if(N&&typeof N.count==="number"&&N.count>f.length){await t.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new u(`清掉恢复态旧图后编辑器内仍有 ${N.count} 张图,超过本次注入的 ${f.length} 张。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。Debug: /tmp/xhs_publish_dup_debug.png")}}if(!await Ut(t)){await t.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new u("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(w)await st(t,c.length,"文字配图 generated images");if(w&&f.length>0){const l=typeof t.startNetworkCapture==="function"?await t.startNetworkCapture("").catch(()=>!1):!1,g=await tt(t,f);if(!g.ok){await t.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new u(`追加图片失败:${g.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await t.wait({time:K/1000});const x=await dt(t,f.length,l);if(!x.ok){await t.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new u(`追加图片上传未全部完成:只确认 ${x.completed}/${f.length} 张传到小红书 CDN。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await et(t);await st(t,c.length+f.length,"文字配图 appended images")}if(w&&A.length){await ct(t,A);await t.wait({time:0.5})}await nt(t,F,n,"title");await v(t,0.6);await nt(t,Q,r,"content");await v(t,0.6);let R=[],I=[];if(a.length){const l=await Ct(t,Q,a);R=l.added;I=l.failed}await v(t,1,0.6);const O=await xt(t,["xhs-publish-btn"]);if(O){await gt(t,O.x,O.y);await v(t,0.4)}const U=s?["暂存离开","存草稿"]:["发布","发布笔记"],p=await t.evaluate(`
|
|
541
541
|
(cfg => {
|
|
542
542
|
const { isDraftMode, publishNames, draftNames, labels } = cfg;
|
|
543
543
|
const isVisible = (el) => {
|
package/clis/zhihu/comment.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{CliError as s,CommandExecutionError as
|
|
1
|
+
import{CliError as s,CommandExecutionError as m}from"@jackwener/opencli/errors";import{cli as u,Strategy as d}from"@jackwener/opencli/registry";import{assertAllowedKinds as c,parseTarget as l}from"./target.js";import{buildResultRow as p,requireExecute as h,resolveCurrentUserIdentity as w,resolvePayload as y}from"./write-shared.js";u({site:"zhihu",name:"comment",access:"write",description:"Create a top-level comment on a Zhihu answer or article",domain:"zhihu.com",strategy:d.COOKIE,browser:!0,args:[{name:"target",positional:!0,required:!0,help:"Zhihu target URL or typed target"},{name:"text",positional:!0,help:"Comment text"},{name:"file",help:"Comment text file path"},{name:"execute",type:"boolean",help:"Actually perform the write action"}],columns:["status","outcome","message","target_type","target","author_identity","created_url"],func:async(e,r)=>{if(!e)throw new m("Browser session required for zhihu comment");h(r);const o=String(r.target),t=c("comment",l(o)),i=await y(r);await e.goto(t.url);await e.wait(3);const n=await w(e),a=await e.evaluate(`(async () => {
|
|
2
2
|
var targetKind = ${JSON.stringify(t.kind)};
|
|
3
3
|
var targetId = ${JSON.stringify(t.id)};
|
|
4
4
|
var content = ${JSON.stringify(i)};
|
|
@@ -14,4 +14,4 @@ import{CliError as s,CommandExecutionError as u}from"@jackwener/opencli/errors";
|
|
|
14
14
|
if (!resp.ok) return { ok: false, status: resp.status, message: data.error ? data.error.message : 'unknown error' };
|
|
15
15
|
if (!data || !data.id) return { ok: false, status: resp.status, message: 'Comment API response did not include a created comment id' };
|
|
16
16
|
return { ok: true, id: data.id, url: data.url };
|
|
17
|
-
})()`);if(!a?.ok)throw new s("COMMAND_EXEC",a?.message||"Failed to create comment");return p(`Commented on ${t.kind} ${t.id}`,t.kind,o,"created",{author_identity:n,created_url
|
|
17
|
+
})()`);if(!a?.ok)throw new s("COMMAND_EXEC",a?.message||"Failed to create comment");return p(`Commented on ${t.kind} ${t.id}`,t.kind,o,"created",{author_identity:n,created_url:`${t.url}#comment-${a.id}`})}});
|