publishport-opencli 1.0.27 → 1.0.29
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 +1113 -21
- package/clis/_shared/publish-controls.js +11 -0
- 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/douyin/_shared/ai-declaration.js +1 -0
- package/clis/douyin/_shared/browser-fetch.js +6 -6
- package/clis/douyin/_shared/publish-settings.js +1 -0
- package/clis/douyin/activities.js +1 -1
- package/clis/douyin/collections.js +1 -1
- package/clis/douyin/draft.js +8 -8
- package/clis/douyin/hashtag.js +1 -1
- package/clis/douyin/location.js +1 -1
- package/clis/douyin/music-categories.js +1 -0
- package/clis/douyin/music.js +1 -0
- package/clis/douyin/publish-image.js +12 -12
- package/clis/douyin/publish.js +9 -8
- 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/inbox.js +1 -1
- package/clis/nowcoder/like.js +1 -1
- package/clis/nowcoder/recommend.js +1 -1
- package/clis/nowcoder/utils.js +13 -13
- 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/_shared/ai-declaration.js +1 -0
- package/clis/xiaohongshu/_shared/publish-discovery.js +19 -0
- package/clis/xiaohongshu/_shared/publish-editors.js +1 -0
- package/clis/xiaohongshu/_shared/publish-settings.js +1 -0
- package/clis/xiaohongshu/_shared/topics.js +21 -0
- package/clis/xiaohongshu/activities.js +1 -0
- package/clis/xiaohongshu/collections.js +1 -0
- package/clis/xiaohongshu/groups.js +1 -0
- package/clis/xiaohongshu/location.js +1 -0
- package/clis/xiaohongshu/publish-video.js +10 -28
- package/clis/xiaohongshu/publish.js +43 -165
- package/clis/xiaohongshu/topics.js +1 -0
- 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/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import{ArgumentError as o,CommandExecutionError as l}from"@jackwener/opencli/errors";export async function control(t,n,i){const e=await t.evaluate(`(({selector,label}) => {
|
|
2
|
+
const all = [...document.querySelectorAll(selector)];
|
|
3
|
+
const hits = all.filter(e => e.getClientRects().length && getComputedStyle(e).visibility !== 'hidden'
|
|
4
|
+
&& (label === undefined || (e.textContent || '').trim() === label));
|
|
5
|
+
if (hits.length !== 1) return {error: '期望唯一控件,找到 ' + hits.length};
|
|
6
|
+
const e = hits[0];
|
|
7
|
+
if (e.disabled || e.getAttribute('aria-disabled') === 'true'
|
|
8
|
+
|| [...e.classList].some(c => c === 'disabled' || c.startsWith('disabled-')))
|
|
9
|
+
return {error:'控件不可用'};
|
|
10
|
+
return {nth:all.indexOf(e)};
|
|
11
|
+
})(${JSON.stringify({selector:n,label:i})})`);if(!e||e.error)throw new l(`发布设置 ${i||n}:${e?.error||"页面响应异常"};已停止提交`);return{selector:n,nth:e.nth}}export async function clickControl(t,n,i){const e=await control(t,n,i);await t.evaluate(`document.querySelectorAll(${JSON.stringify(n)})[${e.nth}].scrollIntoView({block:'center',inline:'center',behavior:'instant'})`);await t.click(e.selector,{nth:e.nth});await t.wait({time:0.2})}export async function verifyControl(t,n,i,e=5000){for(let r=0;r<Math.ceil(e/200);r++){if(await t.evaluate(`Boolean(${n})`))return;await t.wait({time:0.2})}throw new l(`发布设置 ${i} 未生效;已停止提交`)}export async function fillControl(t,n,i,e=!0){const r=await control(t,n);await t.fillText(n,i,{nth:r.nth});if(e)await t.pressKey("Tab");await verifyControl(t,`document.querySelectorAll(${JSON.stringify(n)})[${r.nth}]?.value === ${JSON.stringify(i)}`,n)}export function publicationTime(t,n,i=Date.now()){if(t===void 0)return;const e=String(t);if(!/^\d+$/.test(e)&&!/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}$/.test(e)&&!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/i.test(e))throw new o("--schedule 使用北京时间 YYYY-MM-DD HH:mm、带时区 ISO8601 或 Unix 秒");const r=/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}$/.test(e)?e.replace(" ","T")+":00+08:00":e,a=/^\d+$/.test(e)?Number(e)*1000:Date.parse(r);if(!Number.isFinite(a)||a<i+n*3600000||a>i+1209600000)throw new o(`--schedule 必须在 ${n} 小时至 14 天后(无时区格式 YYYY-MM-DD HH:mm 按北京时间)`);return new Date(a+28800000).toISOString().slice(0,16).replace("T"," ")}export function resolveDeclaration(t,n){const i=t.declaration??(t["ai-generated"]?"ai":void 0);if(i!==void 0&&!Object.hasOwn(n,i))throw new o(`不支持的 --declaration: ${i}`);if(t["ai-generated"]&&i!=="ai")throw new o("--ai-generated 与 --declaration 冲突");return i}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as c from"node:fs";import*as a from"node:path";import{ArgumentError as o,CommandExecutionError as f}from"@jackwener/opencli/errors";import{isFileAccessDenied as w,setInputFiles as m}from"./file-inject.js";export const SUPPORTED_VIDEO_EXTENSIONS=new Set([".mp4",".mov",".avi",".mkv",".m4v",".webm",".flv",".wmv"]),SUPPORTED_IMAGE_EXTENSIONS=new Set([".jpg",".jpeg",".png",".webp",".bmp"]);export function parseScheduleDate(e){if(e==null||e==="")return null;const t=String(e),n=/^\d+$/.test(t)?new Date(Number(t)<1000000000000?Number(t)*1000:Number(t)):new Date(t);if(Number.isNaN(n.getTime()))throw new o(`无法解析定时时间: ${e}`);if(n.getTime()<=Date.now())throw new o("定时发布时间必须晚于当前时间");return n}export function unwrap(e){if(e&&typeof e==="object"&&"data"in e&&"session"in e)return e.data;return e}export async function evalPage(e,t){return unwrap(await e.evaluate(t))}export function sleep(e){return new Promise((t)=>setTimeout(t,e*1000))}export const videoTimeoutArg=(e=540)=>({name:"timeout",type:"int",default:e,help:`命令超时(秒,默认 ${e})。大视频/上行慢时调大;`+"超时不代表没发成功,先用回查命令确认再决定要不要重发。"});export function resolveVideoFile(e){const t=a.resolve(String(e));let n;try{n=c.statSync(t)}catch{throw new o(`视频文件不存在: ${t}`)}if(!n.isFile())throw new o(`视频路径不是文件: ${t}`);const r=a.extname(t).toLowerCase();if(!SUPPORTED_VIDEO_EXTENSIONS.has(r))throw new o(`不支持的视频格式: ${r}(支持 ${[...SUPPORTED_VIDEO_EXTENSIONS].join("/")})`);return t}export function resolveImageFile(e){const t=a.resolve(String(e));let n;try{n=c.statSync(t)}catch{throw new o(`图片文件不存在: ${t}`)}if(!n.isFile())throw new o(`图片路径不是文件: ${t}`);const r=a.extname(t).toLowerCase();if(!SUPPORTED_IMAGE_EXTENSIONS.has(r))throw new o(`不支持的图片格式: ${r}(支持 ${[...SUPPORTED_IMAGE_EXTENSIONS].join("/")})`);return t}export function throwIfFileAccessDenied(e){if(!w(e))return;throw new f("Chrome 拦截了扩展读取本地文件(DOM.setFileInputFiles → File access not granted):商店安装的扩展默认没有本地文件访问权限","请让用户在 Chrome 打开 chrome://extensions,找到「ppcli Browser Bridge」→「详情」→ 打开「允许访问文件网址」(Allow access to file URLs),开关即时生效,然后重试本命令")}export async function setVideoInput(e,t,n){return(await m(e,t,n)).selector}export async function currentUrl(e){try{if(e.getCurrentUrl){const t=await e.getCurrentUrl();if(t)return String(t)}}catch{}try{return String(await evalPage(e,"() => location.href")||"")}catch{return""}}export async function waitForUrlIncludes(e,t,n=60000,r=500){const i=Array.isArray(t)?t:[t],s=Date.now()+n;while(Date.now()<s){const l=await currentUrl(e);if(l&&i.some((u)=>l.includes(u)))return!0;await e.wait({time:r/1000})}return!1}export async function countByText(e,t){return Number(unwrap(await e.evaluate(`
|
|
2
2
|
((want) => {
|
|
3
3
|
let n = 0;
|
|
4
4
|
const nodes = document.querySelectorAll('body *');
|
|
@@ -56,6 +56,7 @@ import*as l from"node:fs";import*as a from"node:path";import{ArgumentError as o,
|
|
|
56
56
|
((sel) => {
|
|
57
57
|
const el = document.querySelector(sel);
|
|
58
58
|
if (!el) return false;
|
|
59
|
+
if (el.tagName === 'INPUT' && el.type === 'file') return true;
|
|
59
60
|
const r = el.getBoundingClientRect();
|
|
60
61
|
return r.width > 0 && r.height > 0;
|
|
61
62
|
})(${JSON.stringify(t)})
|
|
@@ -1 +1,17 @@
|
|
|
1
|
-
import{CliError as u,CommandExecutionError as P}from"@jackwener/opencli/errors";import{cli as
|
|
1
|
+
import{CliError as u,CommandExecutionError as P}from"@jackwener/opencli/errors";import{cli as U,Strategy as k}from"@jackwener/opencli/registry";import{publishArticle as C}from"../_shared/article/publish.js";import{readFile as L,stat as O}from"node:fs/promises";function R(a){if(!a.execute)throw new u("INVALID_INPUT","此百家号写操作需要 --execute 参数才能真正发布。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function E(a){const e=typeof a.text==="string"?a.text:void 0,t=typeof a.file==="string"?a.file:void 0;if(e&&t)throw new u("INVALID_INPUT","正文和 --file 只能选一个");let r=e??"";if(t){let i;try{i=await O(t)}catch{throw new u("INVALID_INPUT",`找不到文件: ${t}`)}if(!i.isFile())throw new u("INVALID_INPUT",`--file 必须是可读文本文件: ${t}`);let n;try{n=await L(t)}catch{throw new u("INVALID_INPUT",`文件无法读取: ${t}`)}try{r=new TextDecoder("utf-8",{fatal:!0}).decode(n)}catch{throw new u("INVALID_INPUT",`文件必须是 UTF-8 编码: ${t}`)}}if(!r.trim())throw new u("INVALID_INPUT","正文不能为空");return r}function J(a,e,t,r,i={}){return[{status:"success",outcome:r,message:a,target_type:e,target:t,...i}]}export async function resolvePublishedReceipt(a,e){const t=new URL(e,"https://baijiahao.baidu.com");if(t.hostname!=="baijiahao.baidu.com")throw Error("发布回执域名不匹配");const r=t.searchParams.get("id")||t.searchParams.get("nid");if(!r||!/^\d+$/.test(r))throw Error("发布回执缺少可识别的文章 ID");if(t.pathname==="/s")return{id:r,url:`https://baijiahao.baidu.com/s?id=${r}`};if(!t.pathname.startsWith("/builder/preview"))throw Error("未识别的发布回执地址");const i=await a.evaluate(`(async () => {
|
|
2
|
+
const html = await (await fetch('/builder/rc/edit', { credentials: 'include' })).text();
|
|
3
|
+
const token = html.match(/window\\.__BJH__INIT__AUTH__\\s*=\\s*['"]([^'"]+)['"]/)?.[1];
|
|
4
|
+
if (!token) throw new Error('无法读取百家号登录凭据');
|
|
5
|
+
const response = await fetch('/pcui/article/lists?currentPage=1&pageSize=50&search=&type=&collection=&dynamic=1', {
|
|
6
|
+
credentials: 'include', headers: { token },
|
|
7
|
+
});
|
|
8
|
+
const payload = await response.json();
|
|
9
|
+
if (!response.ok || payload.errno !== 0 || !Array.isArray(payload.data?.list)) {
|
|
10
|
+
throw new Error('百家号文章列表读取失败:' + response.status + ' / ' + payload.errmsg);
|
|
11
|
+
}
|
|
12
|
+
const matches = payload.data.list.filter(a => a.nid === ${JSON.stringify(r)});
|
|
13
|
+
if (matches.length !== 1) throw new Error('文章列表尚未唯一匹配本次发布的 nid');
|
|
14
|
+
const articleId = matches[0].article_id;
|
|
15
|
+
if (typeof articleId !== 'string' || !/^\\d+$/.test(articleId)) throw new Error('列表未返回精确的 article_id');
|
|
16
|
+
return articleId;
|
|
17
|
+
})()`);return{id:i,url:`https://baijiahao.baidu.com/s?id=${i}`}}export const baijiahaoProfile={home:"https://baijiahao.baidu.com",outputFormat:"html",preprocessConfig:{},image:{skip:["baijiahao.baidu.com","bdstatic.com","bcebos.com"],uploadFn:async(a,e)=>{const t=await fetch(a,{credentials:"omit"});if(!t.ok)throw Error("图片下载失败: "+a+" HTTP "+t.status);let r=await t.blob();r=await e.webpToJpeg(r);const i=new FormData;i.append("media",r,"image.jpg");i.append("type","image");i.append("app_id","1589639493090963");i.append("is_waterlog","1");i.append("save_material","1");i.append("no_compress","0");i.append("is_events","");i.append("article_type","news");const c=await(await fetch("https://baijiahao.baidu.com/pcui/picture/uploadproxy",{method:"POST",credentials:"include",body:i})).json();if(c.errmsg!=="success"||!c.ret||!c.ret.https_url)throw Error(c.errmsg||"图片上传失败");return{url:c.ret.https_url}}},publish:async(a,e)=>{const t=a.params||{},r=await fetch("https://baijiahao.baidu.com/builder/rc/edit",{credentials:"include"}),n=(await r.text()).match(/window\.__BJH__INIT__AUTH__\s*=\s*['"]([^'"]+)['"]/);if(!n)return{ok:!1,stage:"auth",status:r.status,message:"登录失效,请重新登录百家号"};const c=n[1],s=a.content;if(a.draftOnly){const d=new URLSearchParams({title:a.title,content:s,feed_cat:"1",len:String(s.length),activity_list:JSON.stringify([{id:408,is_checked:0}]),source_reprinted_allow:"0",original_status:"0",original_handler_status:"1",isBeautify:"false",subtitle:"",bjhtopic_id:"",bjhtopic_info:"",type:"news"}),g=await fetch("https://baijiahao.baidu.com/pcui/article/save?callback=bjhdraft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded",token:c},body:d}),o=(await g.text()).replace(/^bjhdraft\(/,"").replace(/\)$/,"");let p=null;try{p=JSON.parse(o)}catch(H){}if(!p||p.errmsg!=="success"||!p.ret||!p.ret.article_id)return{ok:!1,stage:"save",status:g.status,message:p&&p.errmsg||"保存草稿失败"};const m=String(p.ret.article_id),x="https://baijiahao.baidu.com/builder/rc/edit?type=news&article_id="+m;return{ok:!0,id:m,url:x,draft:!0}}if(!t.cover||!String(t.cover).trim())return{ok:!1,stage:"cover",message:"正式发布百家号文章必须提供封面图(--cover)"};const b=String(t.cover);let f="";try{f=new URL(b,location.href).hostname}catch(d){}const w=/(^|\.)baijiahao\.baidu\.com$|(^|\.)bdstatic\.com$|(^|\.)bcebos\.com$/.test(f);let l="";if(w)l=b;else try{const d=f===location.hostname,g=await fetch(b,{credentials:d?"include":"omit"});if(!g.ok)return{ok:!1,stage:"cover",status:g.status,message:"封面图下载失败:"+t.cover};let y=await g.blob();if(/text\/html/i.test(y.type))return{ok:!1,stage:"cover",message:"封面图下载到的是 HTML 页面而非图片(防盗链/登录墙?):"+t.cover};y=await e.webpToJpeg(y);const o=new FormData;o.append("media",y,"cover.jpg");o.append("type","image");o.append("app_id","1589639493090963");o.append("is_waterlog","1");o.append("save_material","1");o.append("no_compress","0");o.append("is_events","");o.append("article_type","news");const m=await(await fetch("https://baijiahao.baidu.com/pcui/picture/uploadproxy",{method:"POST",credentials:"include",body:o})).json();if(m.errmsg!=="success"||!m.ret||!m.ret.https_url)return{ok:!1,stage:"cover",message:"封面图上传失败:"+(m.errmsg||"未知错误")};l=m.ret.https_url}catch(d){return{ok:!1,stage:"cover",message:"封面图处理异常:"+String(d&&d.message||d)}}const _=s+'<img src="'+l+'"><br>',j=[{src:l,cropData:{x:0,y:0,width:2048,height:1365},machine_chooseimg:0,isLegal:1}],D=[{src:l}],A={type:"news",title:a.title,author:t.author?String(t.author):"",abstract:t.abstract?String(t.abstract):"",content:_,auto_mount_goods:"1",len:String(_.length),vertical_cover:l,cover_images:JSON.stringify(j),_cover_images_map:JSON.stringify(D)},S="&activity_list%5B0%5D%5Bid%5D=408&activity_list%5B0%5D%5Bis_checked%5D=0&activity_list%5B1%5D%5Bid%5D=ttv&activity_list%5B1%5D%5Bis_checked%5D=1&activity_list%5B2%5D%5Bid%5D=reward&activity_list%5B2%5D%5Bis_checked%5D=1&activity_list%5B3%5D%5Bid%5D=aigc_bjh_status&activity_list%5B3%5D%5Bis_checked%5D=0&source_reprinted_allow=0&abstract_from=2&isBeautify=false&usingImgFilter=false&cover_layout=one",T="&source=upload&cover_source=upload&subtitle=&bjhtopic_id=&bjhtopic_info=&clue=1&bjhmt=&order_id=&aigc_rebuild=&image_edit_point=%5B%7B%22img_type%22%3A%22cover%22%2C%22img_num%22%3A%7B%22template%22%3A0%2C%22font%22%3A0%2C%22filter%22%3A0%2C%22paster%22%3A0%2C%22cut%22%3A0%2C%22any%22%3A0%7D%7D%2C%7B%22img_type%22%3A%22body%22%2C%22img_num%22%3A%7B%22template%22%3A0%2C%22font%22%3A0%2C%22filter%22%3A0%2C%22paster%22%3A0%2C%22cut%22%3A0%2C%22any%22%3A0%7D%7D%5D",B=new URLSearchParams(A).toString()+S+T,v=await fetch("https://baijiahao.baidu.com/pcui/article/publish?callback=bjhpublish",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded",token:c},body:B}),N=(await v.text()).replace(/^bjhpublish\(/,"").replace(/\)$/,"");let h=null;try{h=JSON.parse(N)}catch(d){}if(!h||h.errno!==0||!h.ret)return{ok:!1,stage:"publish",status:v.status,message:h&&(h.errmsg||h.errno)||"百家号发布失败"};const I=h.ret.url||"";return{ok:!0,id:I,url:I,draft:!1}},checkAuth:async(a)=>{try{const t=await(await fetch("https://baijiahao.baidu.com/builder/app/appinfo?_="+Date.now(),{credentials:"include"})).json();if(t.errmsg==="success"&&t.data&&t.data.user){const r=t.data.user;return{isAuthenticated:!0,userId:String(r.userid||""),username:r.name||"",avatar:r.avatar||""}}return{isAuthenticated:!1}}catch(e){return{isAuthenticated:!1,error:String(e&&e.message||e)}}}};U({site:"baijiahao",name:"article",access:"write",description:"发布百家号文章。默认正式发布(需 --cover 封面),加 --draft 仅存草稿(草稿不需要封面)。正文默认 Markdown,图片自动转存到百家号图床。",domain:"baijiahao.baidu.com",strategy:k.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;传 --html 则视为原始 HTML)"},{name:"file",help:"正文文件路径(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"cover",help:"封面图 URL(正式发布必填;会先转存到百家号图床。草稿模式可不传)"},{name:"abstract",help:"文章摘要(可空,正式发布时写入;草稿模式忽略)"},{name:"author",help:"作者署名(可空,正式发布时写入;草稿模式忽略)"},{name:"draft",type:"boolean",help:"仅保存草稿,不正式发布(草稿不需要封面)"},{name:"execute",type:"boolean",help:"真正执行写操作。不加此参数时命令拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url","management_url"],func:async(a,e)=>{if(!a)throw new P("百家号文章发布需要浏览器会话");R(e);const t=String(e.title??"").trim();if(!t)throw new u("INVALID_INPUT","文章标题不能为空");const r=await E(e),i=Boolean(e.draft),n=typeof e.cover==="string"?e.cover.trim():"";if(!i&&!n)throw new u("INVALID_INPUT","正式发布百家号文章必须提供封面图:请传 --cover <图片URL>(或加 --draft 只存草稿)");const c={cover:n,abstract:typeof e.abstract==="string"?e.abstract:"",author:typeof e.author==="string"?e.author:""},s=await C(a,{title:t,body:r,format:e.html?"html":"markdown",draftOnly:i,profile:baijiahaoProfile,publishParams:c}),b=s.images.uploaded.length|0,f=s.images.failed.length|0;let w=s.draft?"已保存百家号文章草稿":"已正式发布百家号文章";const l=s.url;if(!s.draft){s.url="";try{Object.assign(s,await resolvePublishedReceipt(a,l))}catch(_){w+=`;公开链接尚未确认:${_ instanceof Error?_.message:String(_)}。请用 lists 按预览 nid 回查,禁止重复发布`}}if(b||f)w+=`(图片:${b} 张已转存${f?`,${f} 张失败`:""})`;return J(w,"article","",s.draft?"draft":"created",{created_target:"article:"+s.id,created_url:s.url,management_url:l})}});
|
package/clis/bilibili/coin.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as f,Strategy as h}from"@jackwener/opencli/registry";import{ArgumentError as s,CommandExecutionError as c}from"@jackwener/opencli/errors";import{apiGet as d,apiPost as y,requireOkPayload as m,resolveVideoIds as v}from"./utils.js";function w(t){const i=m(t,"archive coins")?.multiply;if(!Number.isInteger(i)||i<0||i>2)throw new c("Bilibili archive coins returned a missing or invalid multiply");return i}f({site:"bilibili",name:"coin",access:"write",description:"给 B站视频投币(官方 API,需登录;消耗硬币不可撤销,需 --execute)",domain:"www.bilibili.com",strategy:h.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"count",type:"int",default:1,help:"投币数量,1 或 2(单视频累计上限 2 枚)"},{name:"with-like",type:"boolean",help:"投币同时点赞"},{name:"execute",type:"boolean",help:"Actually spend coins. Without it the command refuses to write."}],columns:["bvid","coined","total","status","url"],func:async(t,i)=>{if(!t)throw new c("Browser session required for bilibili coin");const e=Number(i.count??1);if(e!==1&&e!==2)throw new s("bilibili coin --count must be 1 or 2");if(!i.execute)throw new s("Refusing to spend coins: pass --execute to actually pay coins");const{bvid:o,aid:l}=await v(t,i.bvid),u=`https://www.bilibili.com/video/${o}`,b=await d(t,"/x/web-interface/archive/coins",{params:{aid:l,bvid:o}}),n=w(b);if(n>=2)return[{bvid:o,coined:0,total:n,status:"already-maxed",url:u}];if(n+e>2)throw new s(`已投 ${n} 枚,再投 ${e} 枚会超过单视频 2 枚上限;改用 --count ${2-n}`);const p=await y(t,"/x/web-interface/coin/add",{params:{aid:l,bvid:o,multiply:e,like:i["with-like"]?1:0}});m(p,"coin add");let a;try{const r=await d(t,"/x/web-interface/archive/coins",{params:{aid:l,bvid:o}});a=w(r);if(a<n+e)throw new c(`multiply=${a}, expected at least ${n+e}`)}catch(r){throw new c(`Bilibili 已受理 ${o} 的 ${e} 枚投币请求,但回查未确认:${r instanceof Error?r.message:String(r)}`,"硬币可能已扣除,禁止直接重投。请核对当前账号的硬币余额、投币记录和目标视频;本次请求只提交了一次。")}return[{bvid:o,coined:e,total:a,status:"coined",url:u}]}});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{cli as N,Strategy as k}from"@jackwener/opencli/registry";import{ArgumentError as e,AuthRequiredError as I,CommandExecutionError as r}from"@jackwener/opencli/errors";import{apiGet as _,apiPost as S,requireOkPayload as h,resolveBvid as x,parsePageArg as P,selectVideoPart as U}from"./utils.js";import{assertLiteralContent as E}from"../_shared/content-guard.js";N({site:"bilibili",name:"danmaku",access:"write",description:"给 B站视频指定时间发送普通滚动弹幕(默认预览,--execute 才发送)",domain:"www.bilibili.com",strategy:k.COOKIE,browser:!0,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"message",required:!0,positional:!0,fromFile:!0,help:"弹幕正文,亦可使用 --file"},{name:"at",required:!0,help:"弹幕出现时间,单位秒,支持小数(例如 12.5)"},{name:"page",help:"分 P 序号,默认使用 URL 的 p 或第 1 P"},{name:"execute",type:"boolean",help:"实际发送一次;平台受理不代表公开可见"}],columns:["dmid","bvid","cid","page","at","message","status","url"],func:async(n,t)=>{const m=String(t.message??"");if(!m.trim())throw new e("弹幕正文不能为空");E(m,{label:"弹幕正文"});const g=String(t.at??"");if(!/^\d+(?:\.\d+)?$/.test(g))throw new e("--at 必须是非负秒数,例如 0 或 12.5");const s=Math.ceil(Number(g)*1000);if(!Number.isSafeInteger(s))throw new e("--at 超出有效范围");let u=null;if(/^https?:\/\//i.test(String(t.bvid))){let i;try{i=new URL(t.bvid)}catch{throw new e("视频 URL 无效")}if(i.searchParams.has("p")){const f=i.searchParams.getAll("p");if(f.length!==1||!f[0])throw new e("视频 URL 的 p 参数无效");u=P(f[0])}}const w=P(t.page);if(w!==null&&u!==null&&w!==u)throw new e("--page 与视频 URL 的 p 不一致,请明确目标分 P");const c=w??u??1;let d;try{d=await x(t.bvid)}catch(i){throw new e("无法解析视频 BV ID",String(i))}if(!n)throw new r("Browser session required for bilibili danmaku");const l=h(await _(n,"/x/web-interface/view",{params:{bvid:d}}),"view");if(l?.bvid!==d||!Number.isSafeInteger(l?.aid)||l.aid<=0)throw new r("Bilibili view 返回的视频 ID 与目标不符或 aid 无效");const a=U(l,c);if(!Number.isFinite(a.duration)||a.duration<=0)throw new r("Bilibili view 未返回有效分 P 时长");if(s>=a.duration*1000)throw new e(`--at 必须小于第 ${c} P 的时长 ${a.duration} 秒`);const v=h(await _(n,"/x/web-interface/nav"),"nav");if(v?.isLogin!==!0||!v?.mid)throw new I("bilibili.com");const y={dmid:null,bvid:d,cid:String(a.cid),page:c,at:s/1000,message:m,status:"dry-run",url:`https://www.bilibili.com/video/${d}/?p=${c}&t=${s/1000}`};if(!t.execute)return[y];const p=await n.evaluate(`() => {
|
|
2
|
+
const key = '__NANO_UNIQUE_IDENTIFIER__';
|
|
3
|
+
if (typeof document[key] !== 'number' || isNaN(document[key])) document[key] = 0;
|
|
4
|
+
return ++document[key];
|
|
5
|
+
}`);if(!Number.isSafeInteger(p)||p<=0)throw new r("无法生成播放器弹幕请求编号");let o;try{o=await S(n,"/x/v2/dm/post",{params:{type:1,oid:a.cid,aid:l.aid,msg:m,progress:s,rnd:p,mode:1,fontsize:25,color:16777215,pool:0,plat:1,checkbox_type:0,gaiasource:"main_web",polaris_app_id:100,polaris_platform:5}})}catch(i){throw new r(`弹幕发送结果未确认:${String(i)}`,"请求可能已生效,禁止直接重发;请回查目标视频。")}if(!o||typeof o.code!=="number"||o.code===-1)throw new r(`弹幕发送回执无效,结果未确认:${o?.message??"missing response code"}`,"禁止直接重发,请回查目标视频。");const b=h(o,"danmaku post");if(typeof b?.dmid_str!=="string"||!/^[1-9]\d*$/.test(b.dmid_str))throw new r("平台已受理弹幕但未返回精确 dmid_str,结果未确认","禁止直接重发,请回查目标视频。");return[{...y,dmid:b.dmid_str,status:"accepted"}]}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as g,Strategy as y}from"@jackwener/opencli/registry";import{ArgumentError as v,CommandExecutionError as s}from"@jackwener/opencli/errors";import{apiGet as S,apiPost as B,getSelfUid as I,requireOkPayload as m,resolveVideoIds as E}from"./utils.js";async function u(t,a){const i=await I(t),o=await S(t,"/x/v3/fav/folder/created/list-all",{params:{up_mid:i,type:2,rid:a},signed:!0}),n=m(o,"fav folder list")?.list??[];if(!Array.isArray(n)||n.length===0)throw new s("Bilibili fav folder list returned no folders for this account");return n}function w(t,a){const i=String(a??"").trim();if(!i)return t[0];const o=t.find((r)=>String(r.id)===i);if(o)return o;const e=t.filter((r)=>String(r.title??"")===i);if(e.length===1)return e[0];if(e.length>1)throw new v(`收藏夹标题「${i}」有 ${e.length} 个同名,请改用 ID:${e.map((r)=>r.id).join(", ")}`);const n=t.map((r)=>`${r.id}=${r.title}`).join(", ");throw new v(`找不到收藏夹「${i}」;现有收藏夹:${n}`)}function b(t){if(!t||t.fav_state!==0&&t.fav_state!==1)throw new s("Bilibili fav folder list is missing the target folder or its fav_state");return t.fav_state===1}g({site:"bilibili",name:"favorite-add",access:"write",description:"把 B站视频加入 / 移出收藏夹(官方 API,需登录;--folder 缺省用默认收藏夹)",domain:"www.bilibili.com",strategy:y.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"folder",help:"目标收藏夹 ID 或标题(缺省用默认收藏夹)"},{name:"remove",type:"boolean",help:"从收藏夹移出(默认是加入)"}],columns:["bvid","folder_id","folder","status","url"],func:async(t,a)=>{if(!t)throw new s("Browser session required for bilibili favorite-add");const i=Boolean(a.remove),{bvid:o,aid:e}=await E(t,a.bvid),n=`https://www.bilibili.com/video/${o}`,r=await u(t,e),d=w(r,a.folder),h=b(d),f={bvid:o,folder_id:String(d.id),folder:String(d.title??""),url:n};if(h===!i)return[{...f,status:i?"not-in-folder":"already-favorited"}];const p=await B(t,"/x/v3/fav/resource/deal",{params:{rid:e,type:2,add_media_ids:i?"":String(d.id),del_media_ids:i?String(d.id):""}});m(p,"fav resource deal");try{const c=(await u(t,e)).find((_)=>String(_.id)===String(d.id));if(b(c)!==!i)throw new s(`fav_state=${c.fav_state} after ${i?"remove":"add"}`)}catch(l){throw new s(`Bilibili 已受理 ${o} 在收藏夹 ${d.id} 的${i?"移除":"添加"}请求,但回查未确认:${l instanceof Error?l.message:String(l)}`,"请先在同一账号的目标收藏夹核对该视频,禁止直接重跑写命令;本次请求只提交了一次。")}return[{...f,status:i?"removed":"favorited"}]}});export const __test__={pickFolder:w};
|
package/clis/bilibili/upload.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as B from"node:fs";import*as z from"node:path";import{cli as A,Strategy as F}from"@jackwener/opencli/registry";import{ArgumentError as P,AuthRequiredError as T,CommandExecutionError as y}from"@jackwener/opencli/errors";import{resolveVideoFile as L,resolveImageFile as N,videoTimeoutArg as D}from"../_shared/video-publish.js";import{setInputFiles as J}from"../_shared/file-inject.js";const R={bda2:{os:"upos",upcdn:"bda2",probe_version:20221109},bldsa:{os:"upos",upcdn:"bldsa",probe_version:20221109},qn:{os:"upos",upcdn:"qn",probe_version:20221109},ws:{os:"upos",upcdn:"ws",probe_version:20221109}},C="bda2",M=["SESSDATA","bili_jct","DedeUserID"],q="https://member.bilibili.com/platform/home",O="__pp_bili_video_input";function K(t){var e=document.getElementById(t.inputId);if(!e||!e.files||!e.files[0])return{error:"文件未就绪(input 为空)"};var n=e.files[0],r=n.size,o=t.line;return async function(){try{var a=new URLSearchParams({profile:"ugcfx/bup",name:n.name,size:String(r),r:o.os,ssl:"0",version:"2.14.0",build:"2100400",upcdn:o.upcdn,probe_version:String(o.probe_version)}),i=await(await fetch("https://member.bilibili.com/preupload?"+a.toString(),{credentials:"include",headers:{Referer:"https://www.bilibili.com"}})).json();if(i.OK!==1)return{error:"preupload 失败:"+JSON.stringify(i)};var s="https:"+i.endpoint+"/"+String(i.upos_uri).replace(/^upos:\/\//,""),b=new URLSearchParams({uploads:"",output:"json",profile:"ugcfx/bup",filesize:String(r),partsize:String(i.chunk_size),biz_id:String(i.biz_id)}),m=await(await fetch(s+"?"+b.toString(),{method:"POST",headers:{"x-upos-auth":i.auth}})).json();if(m.OK!==1)return{error:"获取 upload_id 失败:"+JSON.stringify(m)};var S=Math.max(1,Math.ceil(r/i.chunk_size));return{url:s,auth:i.auth,uploadId:m.upload_id,chunkSize:i.chunk_size,bizId:i.biz_id,size:r,name:n.name,total:S}}catch(h){return{error:"preupload 异常:"+String(h&&h.message||h)}}}()}function V(t){var e=document.getElementById(t.inputId);if(!e||!e.files||!e.files[0])return{ok:!1,error:"文件丢失(页面可能已导航)"};var n=e.files[0],r=t.idx*t.chunkSize,o=n.slice(r,Math.min(r+t.chunkSize,t.size)),a=o.size,i=new URLSearchParams({partNumber:String(t.idx+1),uploadId:String(t.uploadId),chunk:String(t.idx),chunks:String(t.total),size:String(a),start:String(r),end:String(r+a),total:String(t.size)});return async function(){for(var s=0;s<5;s++){try{var b=await fetch(t.url+"?"+i.toString(),{method:"PUT",headers:{"x-upos-auth":t.auth},body:o});if(b.status<400){var m=await b.text();if(m==="MULTIPART_PUT_SUCCESS"||m==="")return{ok:!0}}}catch(S){}await new Promise(function(S){setTimeout(S,800*(s+1))})}return{ok:!1,error:"分块 "+t.idx+" 多次重试仍失败"}}()}function G(t){var e=[];for(var n=1;n<=t.total;n++)e.push({partNumber:n,eTag:"etag"});var r=new URLSearchParams({output:"json",name:t.name,profile:"ugcfx/bup",uploadId:String(t.uploadId),biz_id:String(t.bizId)});return async function(){try{var o=await(await fetch(t.url+"?"+r.toString(),{method:"POST",headers:{"x-upos-auth":t.auth,"content-type":"application/json; charset=UTF-8"},body:JSON.stringify({parts:e})})).json();if(o.OK!==1)return{error:"complete 失败:"+JSON.stringify(o)};var a=String(o.key).replace(/^\//,"").replace(/\.[^.]+$/,"");return{filename:a,cid:t.bizId}}catch(i){return{error:"complete 异常:"+String(i&&i.message||i)}}}()}function H(t){return async function(){try{var e=await fetch("https://member.bilibili.com/x/vupre/web/topic/type?type_id="+encodeURIComponent(t.tid)+"&pn=0&ps=200",{credentials:"include"}),n=await e.json();if(!n||n.code!==0||!n.data||!Array.isArray(n.data.topics))return{error:"获取话题列表失败:"+(n&&n.message||"code="+(n&&n.code))};var r=null;for(var o=0;o<n.data.topics.length;o++)if(Number(n.data.topics[o].topic_id)===Number(t.topicId)){r=n.data.topics[o];break}if(!r)return{error:"topic_id "+t.topicId+" 不在分区 "+t.tid+" 的可用话题里(用 bilibili topics --tid "+t.tid+" 查合法值)"};return{topicId:Number(r.topic_id),missionId:r.mission_id!=null?Number(r.mission_id):null,name:r.topic_name||""}}catch(a){return{error:"解析话题异常:"+String(a&&a.message||a)}}}()}function Q(t){return async function(){var e=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!e)return{error:"未找到 bili_jct CSRF token"};var n=decodeURIComponent(e[1]);try{var r=t.dataUri;if(typeof r==="string"&&r.indexOf("data:image/webp")===0){var o=r.indexOf(",");if(o===-1)return{error:"封面 data URI 格式非法"};var a=r.slice(5,o),i=r.slice(o+1),s;if(a.indexOf("base64")!==-1){var b=atob(i);s=new Uint8Array(b.length);for(var m=0;m<b.length;m++)s[m]=b.charCodeAt(m)}else s=new TextEncoder().encode(decodeURIComponent(i));var S=new Blob([s],{type:"image/webp"}),h=await createImageBitmap(S),v=document.createElement("canvas");v.width=h.width;v.height=h.height;var c=v.getContext("2d");c.fillStyle="#ffffff";c.fillRect(0,0,v.width,v.height);c.drawImage(h,0,0);var p=await new Promise(function(f){v.toBlob(f,"image/jpeg",0.92)});if(!p)return{error:"封面 webp 转码 JPEG 失败"};r=await new Promise(function(f,x){var w=new FileReader;w.onload=function(){f(String(w.result))};w.onerror=function(){x(Error("FileReader 读取转码结果失败"))};w.readAsDataURL(p)})}var _=new URLSearchParams;_.append("cover",r);_.append("csrf",n);var u=await(await fetch("https://member.bilibili.com/x/vu/web/cover/up",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:_.toString()})).json();if(!u||u.code!==0||!u.data||!u.data.url)return{error:"封面上传失败:"+(u&&u.message?u.message:JSON.stringify(u))};return{url:u.data.url}}catch(f){return{error:"封面上传异常:"+String(f&&f.message||f)}}}()}function W(t){return async function(){var e=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!e)return{error:"未找到 bili_jct CSRF token"};var n=decodeURIComponent(e[1]),r=t.meta;for(var o in r)if(r[o]===null||r[o]===void 0)delete r[o];r.csrf=n;try{var a=await fetch("https://member.bilibili.com/x/vu/web/add/v3?csrf="+encodeURIComponent(n)+"&t="+Date.now(),{method:"POST",credentials:"include",headers:{"Content-Type":"application/json;charset=UTF-8"},body:JSON.stringify(r)}),i=await a.json();if(!i||i.code!==0)return{error:"add/v3 提交失败:"+(i&&i.message?i.message:"")+"(code="+(i&&i.code)+" status="+a.status+")"};return{bvid:i.data&&i.data.bvid,aid:i.data&&i.data.aid}}catch(s){return{error:"add/v3 异常:"+String(s&&s.message||s)}}}()}function X(t){return async function(){var e=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!e)return{error:"未找到 bili_jct CSRF token"};var n=decodeURIComponent(e[1]),r=t.body;r.csrf=n;try{var o=await fetch("https://member.bilibili.com/x/vupre/web/draft/add?t="+Date.now()+"&csrf="+encodeURIComponent(n),{method:"POST",credentials:"include",headers:{"Content-Type":"application/json;charset=UTF-8"},body:JSON.stringify(r)}),a=await o.json();if(!a||a.code!==0)return{error:"draft/add 保存草稿失败:"+(a&&a.message?a.message:"")+"(code="+(a&&a.code)+" status="+o.status+")"};return{data:a.data}}catch(i){return{error:"draft/add 异常:"+String(i&&i.message||i)}}}()}const g=(t,e,n)=>t.evaluateWithArgs(`(${e.toString()})(a)`,{a:n});function Y(t){const e=N(t),n=z.extname(e).toLowerCase(),r=n===".png"?"image/png":n===".webp"?"image/webp":n===".bmp"?"image/bmp":"image/jpeg",o=B.readFileSync(e).toString("base64");return`data:${r};base64,${o}`}async function Z(t){for(let e=0;e<8;e++){await t.goto(q);await t.wait({time:1});let n="";try{n=String(await t.evaluate("location.href")||"")}catch{n=""}if(/^https?:\/\/member\.bilibili\.com\//.test(n)){const r=await t.getCookies({url:"https://member.bilibili.com"}),o=new Map;for(const i of Array.isArray(r)?r:[])if(i&&typeof i.name==="string")o.set(i.name,String(i.value??""));const a=M.filter((i)=>!o.get(i));if(!a.length)return o;throw new T("member.bilibili.com",`缺少 B站登录 cookie(${a.join(", ")})。请先在客户端登录哔哩哔哩。`)}}throw new T("member.bilibili.com","无法进入 B站创作中心(可能未登录或被风控)。请先在客户端登录哔哩哔哩。")}A({site:"bilibili",name:"upload",access:"write",description:"投稿视频到 B站(在真实浏览器登录态里走 web 投稿:preupload→upos 分块→add/v3)。默认仅 dry-run 校验,加 --execute 才真正上传并提交。",domain:"member.bilibili.com",strategy:F.COOKIE,browser:!0,args:[{name:"file",required:!0,positional:!0,help:"视频文件路径"},{name:"title",help:"稿件标题(默认取文件名)"},{name:"tid",type:"number",required:!0,help:"分区 id(B站 typeid,必填,禁止臆造)。合法值用 `bilibili partitions` 列举后取。"},{name:"tag",required:!0,help:"标签,逗号分隔(B站要求至少 1 个)"},{name:"desc",fromFile:"desc-file",help:"简介"},{name:"topic",type:"number",help:"参与话题的 topic_id(重要流量入口)。合法值用 `bilibili topics --tid <tid>` 列举,禁止臆造;mission_id 自动匹配。"},{name:"cover",help:"封面图片路径(不传由 B站自动截取)"},{name:"copyright",type:"number",help:"1=自制 2=转载(默认 1)"},{name:"source",help:"转载来源 URL(copyright=2 时必填)"},{name:"dynamic",help:"同步发布的动态文案(可空)"},{name:"no-reprint",type:"boolean",help:"禁止转载(默认允许)"},{name:"dtime",type:"number",help:"定时发布的 10 位 unix 时间戳(可空=立即)"},{name:"line",help:`上传线路:${Object.keys(R).join("/")}(默认 ${C})`},{name:"concurrency",type:"number",help:"分块并发数(默认 3)"},{name:"draft",type:"boolean",help:"存草稿而非直接发布(上传视频后存到创作中心草稿箱,不公开)"},{name:"execute",type:"boolean",help:"真正发布投稿;不带(也不带 --draft)则只做 dry-run 校验"},D()],columns:["status","title","bvid","url"],func:async(t,e)=>{if(!t)throw new y("bilibili upload 需要浏览器会话");const n=L(String(e.file??"")),r=String(e.title??"").trim()||z.basename(n).replace(/\.[^.]+$/,""),o=String(e.tag??"").split(",").map((d)=>d.trim()).filter(Boolean);if(!o.length)throw new P("B站投稿至少需要 1 个标签,用 --tag 传(逗号分隔)");if(e.tid==null||e.tid===""||!Number.isFinite(Number(e.tid)))throw new P("必须用 --tid 指定分区 id(数字)。合法值用 `bilibili partitions` 列举,禁止臆造。");const a=Number(e.tid),i=e.copyright!=null&&e.copyright!==""?Number(e.copyright):1;if(i!==1&&i!==2)throw new P("--copyright 只能是 1(自制) 或 2(转载)");const s=String(e.source??"").trim();if(i===2&&!s)throw new P("转载(copyright=2)必须用 --source 提供转载来源");if(e.cover)N(String(e.cover));const b=String(e.line??C),m=R[b];if(!m)throw new P(`未知上传线路「${b}」,可选:${Object.keys(R).join("/")}`);const S=e.concurrency!=null&&e.concurrency!==""?Number(e.concurrency):3,h=Boolean(e.draft),v=Boolean(e.execute)&&!h;await Z(t);await t.evaluate(`(() => { var id=${JSON.stringify(O)}; var el=document.getElementById(id); if(!el){ el=document.createElement('input'); el.type='file'; el.id=id; el.style.cssText='position:fixed;left:-9999px;top:0;'; document.body.appendChild(el);} el.value=''; return true; })()`);await J(t,`#${O}`,[n]);if(!h&&!v)return{status:"dry-run",title:r,bvid:"",url:`校验通过:登录态有效、视频已注入就绪、分区 ${a}、标签 [${o.join(", ")}]、版权 ${i===1?"自制":"转载"}。加 --execute 发布,或 --draft 存草稿。`};const c=await g(t,K,{inputId:O,line:m});if(!c||c.error)throw new y(`B站 preupload 失败:${c&&c.error||"无返回"}`);for(let d=0;d<c.total;d++){const l=await g(t,V,{inputId:O,url:c.url,auth:c.auth,uploadId:c.uploadId,idx:d,total:c.total,size:c.size,chunkSize:c.chunkSize});if(!l||!l.ok)throw new y(`B站分块上传失败(${d+1}/${c.total}):${l&&l.error||"无返回"}`)}const p=await g(t,G,{url:c.url,auth:c.auth,uploadId:c.uploadId,bizId:c.bizId,name:c.name,total:c.total});if(!p||p.error||!p.filename)throw new y(`B站 complete 失败:${p&&p.error||"未拿到 filename"}`);let _="";if(e.cover){const d=Y(String(e.cover)),l=await g(t,Q,{dataUri:d});if(!l||l.error||!l.url)throw new y(`B站封面上传失败:${l&&l.error||"未拿到 url"}`);_=l.url}let u=null,f=null;if(e.topic!=null&&e.topic!==""){if(!Number.isFinite(Number(e.topic)))throw new P("--topic 必须是 topic_id 数字,用 `bilibili topics --tid <tid>` 取合法值");const d=await g(t,H,{tid:a,topicId:Number(e.topic)});if(!d||d.error)throw new y(`B站话题解析失败:${d&&d.error||"无返回"}`);u=d.topicId;f=d.missionId}if(h){const d={videos:[{filename:p.filename,title:r,desc:"",cid:p.cid,is_4k:!1,is_8k:!1,is_hdr:!1}],cover:_,cover43:_,ai_cover:0,is_ab_cover:0,ab_cover_info:null,title:r,copyright:i,tid:a,tag:o.join(","),desc:String(e.desc??""),recreate:-1,dynamic:String(e.dynamic??""),is_only_self:0,space_hidden:2,watermark:{state:0},no_reprint:e["no-reprint"]?1:0,subtitle:{open:0,lan:""},dolby:0,lossless_music:0,up_selection_reply:!1,up_close_reply:!1,up_close_danmu:!1};if(i===2)d.source=s;if(u!=null){d.topic_id=u;d.topic_detail={from_topic_id:u,from_source:"arc.web.search"};if(f!=null)d.mission_id=f}const l=await g(t,X,{body:d});if(!l||l.error)throw new y(`B站 ${l&&l.error||"draft/add 无返回"}`);const U=l.data&&(l.data.aid!=null?l.data.aid:l.data.draft_id!=null?l.data.draft_id:l.data.id);return{status:"✅ 草稿已保存",title:r,bvid:U!=null?`draft:${U}`:"",url:"https://member.bilibili.com/platform/upload-manager/article?group=draft"}}const x=e.dtime!=null&&e.dtime!==""?Number(e.dtime):null,w={title:r,copyright:i,tid:a,tag:o.join(","),mission_id:f,topic_id:u,topic_detail:u!=null?{from_topic_id:u,from_source:"arc.web.recommend"}:null,desc_format_id:9999,desc:String(e.desc??""),dtime:x,recreate:-1,dynamic:String(e.dynamic??""),interactive:0,act_reserve_create:0,no_disturbance:0,porder:null,adorder_type:9,no_reprint:e["no-reprint"]?1:0,subtitle:{open:0,lan:""},neutral_mark:null,dolby:0,lossless_music:0,up_selection_reply:!1,up_close_reply:!1,up_close_danmu:!1,web_os:1,source:i===2?s:null,watermark:{state:0},cover:_,videos:[{title:r,desc:"",filename:p.filename,cid:p.cid}]},I=await g(t,W,{meta:w});if(!I||I.error)throw new y(`B站 ${I&&I.error||"add/v3 无返回"}`);const E=I.bvid?String(I.bvid):"";return{status:e.dtime?"✅ 定时投稿已提交":"✅ 投稿成功",title:r,bvid:E,url:E?`https://www.bilibili.com/video/${E}`:"(已提交,稍后在创作中心查看)"}}});
|
|
1
|
+
import*as B from"node:fs";import*as N from"node:path";import{cli as L,Strategy as F}from"@jackwener/opencli/registry";import{ArgumentError as w,AuthRequiredError as U,CommandExecutionError as v}from"@jackwener/opencli/errors";import{resolveVideoFile as D,resolveImageFile as z,videoTimeoutArg as J}from"../_shared/video-publish.js";import{setInputFiles as M}from"../_shared/file-inject.js";const O={bda2:{os:"upos",upcdn:"bda2",probe_version:20221109},bldsa:{os:"upos",upcdn:"bldsa",probe_version:20221109},qn:{os:"upos",upcdn:"qn",probe_version:20221109},ws:{os:"upos",upcdn:"ws",probe_version:20221109}},A="bda2",q=["SESSDATA","bili_jct","DedeUserID"],K="https://member.bilibili.com/platform/home",T="__pp_bili_video_input";export async function PP_RESOLVE_AI_STATEMENT(){try{const t=await fetch("https://member.bilibili.com/x/vupre/web/archive/pre?lang=cn",{credentials:"include"}),e=await t.json();if(!t.ok||e?.code!==0)return{error:`读取创作声明失败:HTTP ${t.status} / ${e?.message??e?.code}`};const n=e.data?.neutral_mark?.mark_list;if(!Array.isArray(n))return{error:"当前账号的投稿配置缺少创作声明列表"};const r=n.filter((o)=>o.content==="含AI生成内容");if(r.length!==1||!Number.isSafeInteger(r[0].id)||r[0].id<=0)return{error:"当前账号未返回唯一有效的“含AI生成内容”选项"};return{id:r[0].id,label:r[0].content}}catch(t){return{error:`读取创作声明失败:${t instanceof Error?t.message:String(t)}`}}}function V(t){var e=document.getElementById(t.inputId);if(!e||!e.files||!e.files[0])return{error:"文件未就绪(input 为空)"};var n=e.files[0],r=n.size,o=t.line;return async function(){try{var a=new URLSearchParams({profile:"ugcfx/bup",name:n.name,size:String(r),r:o.os,ssl:"0",version:"2.14.0",build:"2100400",upcdn:o.upcdn,probe_version:String(o.probe_version)}),i=await(await fetch("https://member.bilibili.com/preupload?"+a.toString(),{credentials:"include",headers:{Referer:"https://www.bilibili.com"}})).json();if(i.OK!==1)return{error:"preupload 失败:"+JSON.stringify(i)};var s="https:"+i.endpoint+"/"+String(i.upos_uri).replace(/^upos:\/\//,""),h=new URLSearchParams({uploads:"",output:"json",profile:"ugcfx/bup",filesize:String(r),partsize:String(i.chunk_size),biz_id:String(i.biz_id)}),m=await(await fetch(s+"?"+h.toString(),{method:"POST",headers:{"x-upos-auth":i.auth}})).json();if(m.OK!==1)return{error:"获取 upload_id 失败:"+JSON.stringify(m)};var g=Math.max(1,Math.ceil(r/i.chunk_size));return{url:s,auth:i.auth,uploadId:m.upload_id,chunkSize:i.chunk_size,bizId:i.biz_id,size:r,name:n.name,total:g}}catch(_){return{error:"preupload 异常:"+String(_&&_.message||_)}}}()}function H(t){var e=document.getElementById(t.inputId);if(!e||!e.files||!e.files[0])return{ok:!1,error:"文件丢失(页面可能已导航)"};var n=e.files[0],r=t.idx*t.chunkSize,o=n.slice(r,Math.min(r+t.chunkSize,t.size)),a=o.size,i=new URLSearchParams({partNumber:String(t.idx+1),uploadId:String(t.uploadId),chunk:String(t.idx),chunks:String(t.total),size:String(a),start:String(r),end:String(r+a),total:String(t.size)});return async function(){for(var s=0;s<5;s++){try{var h=await fetch(t.url+"?"+i.toString(),{method:"PUT",headers:{"x-upos-auth":t.auth},body:o});if(h.status<400){var m=await h.text();if(m==="MULTIPART_PUT_SUCCESS"||m==="")return{ok:!0}}}catch(g){}await new Promise(function(g){setTimeout(g,800*(s+1))})}return{ok:!1,error:"分块 "+t.idx+" 多次重试仍失败"}}()}function G(t){var e=[];for(var n=1;n<=t.total;n++)e.push({partNumber:n,eTag:"etag"});var r=new URLSearchParams({output:"json",name:t.name,profile:"ugcfx/bup",uploadId:String(t.uploadId),biz_id:String(t.bizId)});return async function(){try{var o=await(await fetch(t.url+"?"+r.toString(),{method:"POST",headers:{"x-upos-auth":t.auth,"content-type":"application/json; charset=UTF-8"},body:JSON.stringify({parts:e})})).json();if(o.OK!==1)return{error:"complete 失败:"+JSON.stringify(o)};var a=String(o.key).replace(/^\//,"").replace(/\.[^.]+$/,"");return{filename:a,cid:t.bizId}}catch(i){return{error:"complete 异常:"+String(i&&i.message||i)}}}()}function Q(t){return async function(){try{var e=await fetch("https://member.bilibili.com/x/vupre/web/topic/type?type_id="+encodeURIComponent(t.tid)+"&pn=0&ps=200",{credentials:"include"}),n=await e.json();if(!n||n.code!==0||!n.data||!Array.isArray(n.data.topics))return{error:"获取话题列表失败:"+(n&&n.message||"code="+(n&&n.code))};var r=null;for(var o=0;o<n.data.topics.length;o++)if(Number(n.data.topics[o].topic_id)===Number(t.topicId)){r=n.data.topics[o];break}if(!r)return{error:"topic_id "+t.topicId+" 不在分区 "+t.tid+" 的可用话题里(用 bilibili topics --tid "+t.tid+" 查合法值)"};return{topicId:Number(r.topic_id),missionId:r.mission_id!=null?Number(r.mission_id):null,name:r.topic_name||""}}catch(a){return{error:"解析话题异常:"+String(a&&a.message||a)}}}()}function W(t){return async function(){var e=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!e)return{error:"未找到 bili_jct CSRF token"};var n=decodeURIComponent(e[1]);try{var r=t.dataUri;if(typeof r==="string"&&r.indexOf("data:image/webp")===0){var o=r.indexOf(",");if(o===-1)return{error:"封面 data URI 格式非法"};var a=r.slice(5,o),i=r.slice(o+1),s;if(a.indexOf("base64")!==-1){var h=atob(i);s=new Uint8Array(h.length);for(var m=0;m<h.length;m++)s[m]=h.charCodeAt(m)}else s=new TextEncoder().encode(decodeURIComponent(i));var g=new Blob([s],{type:"image/webp"}),_=await createImageBitmap(g),y=document.createElement("canvas");y.width=_.width;y.height=_.height;var p=y.getContext("2d");p.fillStyle="#ffffff";p.fillRect(0,0,y.width,y.height);p.drawImage(_,0,0);var d=await new Promise(function(u){y.toBlob(u,"image/jpeg",0.92)});if(!d)return{error:"封面 webp 转码 JPEG 失败"};r=await new Promise(function(u,I){var P=new FileReader;P.onload=function(){u(String(P.result))};P.onerror=function(){I(Error("FileReader 读取转码结果失败"))};P.readAsDataURL(d)})}var b=new URLSearchParams;b.append("cover",r);b.append("csrf",n);var f=await(await fetch("https://member.bilibili.com/x/vu/web/cover/up",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:b.toString()})).json();if(!f||f.code!==0||!f.data||!f.data.url)return{error:"封面上传失败:"+(f&&f.message?f.message:JSON.stringify(f))};return{url:f.data.url}}catch(u){return{error:"封面上传异常:"+String(u&&u.message||u)}}}()}function X(t){return async function(){var e=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!e)return{error:"未找到 bili_jct CSRF token"};var n=decodeURIComponent(e[1]),r=t.meta;for(var o in r)if(r[o]===null||r[o]===void 0)delete r[o];r.csrf=n;try{var a=await fetch("https://member.bilibili.com/x/vu/web/add/v3?csrf="+encodeURIComponent(n)+"&t="+Date.now(),{method:"POST",credentials:"include",headers:{"Content-Type":"application/json;charset=UTF-8"},body:JSON.stringify(r)}),i=await a.json();if(!i||i.code!==0)return{error:"add/v3 提交失败:"+(i&&i.message?i.message:"")+"(code="+(i&&i.code)+" status="+a.status+")"};return{bvid:i.data&&i.data.bvid,aid:i.data&&i.data.aid}}catch(s){return{error:"add/v3 异常:"+String(s&&s.message||s)}}}()}function Y(t){return async function(){var e=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!e)return{error:"未找到 bili_jct CSRF token"};var n=decodeURIComponent(e[1]),r=t.body;r.csrf=n;try{var o=await fetch("https://member.bilibili.com/x/vupre/web/draft/add?t="+Date.now()+"&csrf="+encodeURIComponent(n),{method:"POST",credentials:"include",headers:{"Content-Type":"application/json;charset=UTF-8"},body:JSON.stringify(r)}),a=await o.json();if(!a||a.code!==0)return{error:"draft/add 保存草稿失败:"+(a&&a.message?a.message:"")+"(code="+(a&&a.code)+" status="+o.status+")"};return{data:a.data}}catch(i){return{error:"draft/add 异常:"+String(i&&i.message||i)}}}()}const S=(t,e,n)=>t.evaluateWithArgs(`(${e.toString()})(a)`,{a:n});function Z(t){const e=z(t),n=N.extname(e).toLowerCase(),r=n===".png"?"image/png":n===".webp"?"image/webp":n===".bmp"?"image/bmp":"image/jpeg",o=B.readFileSync(e).toString("base64");return`data:${r};base64,${o}`}async function k(t){for(let e=0;e<8;e++){await t.goto(K);await t.wait({time:1});let n="";try{n=String(await t.evaluate("location.href")||"")}catch{n=""}if(/^https?:\/\/member\.bilibili\.com\//.test(n)){const r=await t.getCookies({url:"https://member.bilibili.com"}),o=new Map;for(const i of Array.isArray(r)?r:[])if(i&&typeof i.name==="string")o.set(i.name,String(i.value??""));const a=q.filter((i)=>!o.get(i));if(!a.length)return o;throw new U("member.bilibili.com",`缺少 B站登录 cookie(${a.join(", ")})。请先在客户端登录哔哩哔哩。`)}}throw new U("member.bilibili.com","无法进入 B站创作中心(可能未登录或被风控)。请先在客户端登录哔哩哔哩。")}L({site:"bilibili",name:"upload",access:"write",description:"投稿视频到 B站(在真实浏览器登录态里走 web 投稿:preupload→upos 分块→add/v3)。默认仅 dry-run 校验,加 --execute 才真正上传并提交。",domain:"member.bilibili.com",strategy:F.COOKIE,browser:!0,args:[{name:"file",required:!0,positional:!0,help:"视频文件路径"},{name:"title",help:"稿件标题(默认取文件名)"},{name:"tid",type:"number",required:!0,help:"分区 id(B站 typeid,必填,禁止臆造)。合法值用 `bilibili partitions` 列举后取。"},{name:"tag",required:!0,help:"标签,逗号分隔(B站要求至少 1 个)"},{name:"desc",fromFile:"desc-file",help:"简介"},{name:"topic",type:"number",help:"参与话题的 topic_id(重要流量入口)。合法值用 `bilibili topics --tid <tid>` 列举,禁止臆造;mission_id 自动匹配。"},{name:"cover",help:"封面图片路径(不传由 B站自动截取)"},{name:"copyright",type:"number",help:"1=自制 2=转载(默认 1)"},{name:"ai-generated",type:"boolean",help:"声明含 AI 生成内容(发布和草稿均支持;按当前账号可用选项校验,不能与转载同时选择)"},{name:"source",help:"转载来源 URL(copyright=2 时必填)"},{name:"dynamic",help:"同步发布的动态文案(可空)"},{name:"no-reprint",type:"boolean",help:"禁止转载(默认允许)"},{name:"dtime",type:"number",help:"定时发布的 10 位 unix 时间戳(可空=立即)"},{name:"line",help:`上传线路:${Object.keys(O).join("/")}(默认 ${A})`},{name:"concurrency",type:"number",help:"分块并发数(默认 3)"},{name:"draft",type:"boolean",help:"存草稿而非直接发布(上传视频后存到创作中心草稿箱,不公开)"},{name:"execute",type:"boolean",help:"真正发布投稿;不带(也不带 --draft)则只做 dry-run 校验"},J()],columns:["status","title","bvid","url"],func:async(t,e)=>{if(!t)throw new v("bilibili upload 需要浏览器会话");const n=D(String(e.file??"")),r=String(e.title??"").trim()||N.basename(n).replace(/\.[^.]+$/,""),o=String(e.tag??"").split(",").map((c)=>c.trim()).filter(Boolean);if(!o.length)throw new w("B站投稿至少需要 1 个标签,用 --tag 传(逗号分隔)");if(e.tid==null||e.tid===""||!Number.isFinite(Number(e.tid)))throw new w("必须用 --tid 指定分区 id(数字)。合法值用 `bilibili partitions` 列举,禁止臆造。");const a=Number(e.tid),i=e.copyright!=null&&e.copyright!==""?Number(e.copyright):1;if(i!==1&&i!==2)throw new w("--copyright 只能是 1(自制) 或 2(转载)");const s=String(e.source??"").trim();if(i===2&&!s)throw new w("转载(copyright=2)必须用 --source 提供转载来源");if(e["ai-generated"]&&i===2)throw new w("B站当前创作声明只能选一项,--ai-generated 不能与 --copyright 2 同时使用");if(e.cover)z(String(e.cover));const h=String(e.line??A),m=O[h];if(!m)throw new w(`未知上传线路「${h}」,可选:${Object.keys(O).join("/")}`);const g=e.concurrency!=null&&e.concurrency!==""?Number(e.concurrency):3,_=Boolean(e.draft),y=Boolean(e.execute)&&!_;await k(t);let p;if(e["ai-generated"]){p=await S(t,PP_RESOLVE_AI_STATEMENT,{});if(!p||p.error)throw new v(p?.error||"B站创作声明查询未返回结果")}await t.evaluate(`(() => { var id=${JSON.stringify(T)}; var el=document.getElementById(id); if(!el){ el=document.createElement('input'); el.type='file'; el.id=id; el.style.cssText='position:fixed;left:-9999px;top:0;'; document.body.appendChild(el);} el.value=''; return true; })()`);await M(t,`#${T}`,[n]);if(!_&&!y)return{status:"dry-run",title:r,bvid:"",url:`校验通过:登录态有效、视频已注入就绪、分区 ${a}、标签 [${o.join(", ")}]、版权 ${i===1?"自制":"转载"}${p?"、创作声明:"+p.label:""}。加 --execute 发布,或 --draft 存草稿。`};const d=await S(t,V,{inputId:T,line:m});if(!d||d.error)throw new v(`B站 preupload 失败:${d&&d.error||"无返回"}`);for(let c=0;c<d.total;c++){const l=await S(t,H,{inputId:T,url:d.url,auth:d.auth,uploadId:d.uploadId,idx:c,total:d.total,size:d.size,chunkSize:d.chunkSize});if(!l||!l.ok)throw new v(`B站分块上传失败(${c+1}/${d.total}):${l&&l.error||"无返回"}`)}const b=await S(t,G,{url:d.url,auth:d.auth,uploadId:d.uploadId,bizId:d.bizId,name:d.name,total:d.total});if(!b||b.error||!b.filename)throw new v(`B站 complete 失败:${b&&b.error||"未拿到 filename"}`);let f="";if(e.cover){const c=Z(String(e.cover)),l=await S(t,W,{dataUri:c});if(!l||l.error||!l.url)throw new v(`B站封面上传失败:${l&&l.error||"未拿到 url"}`);f=l.url}let u=null,I=null;if(e.topic!=null&&e.topic!==""){if(!Number.isFinite(Number(e.topic)))throw new w("--topic 必须是 topic_id 数字,用 `bilibili topics --tid <tid>` 取合法值");const c=await S(t,Q,{tid:a,topicId:Number(e.topic)});if(!c||c.error)throw new v(`B站话题解析失败:${c&&c.error||"无返回"}`);u=c.topicId;I=c.missionId}if(_){const c={videos:[{filename:b.filename,title:r,desc:"",cid:b.cid,is_4k:!1,is_8k:!1,is_hdr:!1}],cover:f,cover43:f,ai_cover:0,is_ab_cover:0,ab_cover_info:null,title:r,copyright:i,tid:a,tag:o.join(","),desc:String(e.desc??""),recreate:-1,dynamic:String(e.dynamic??""),is_only_self:0,space_hidden:2,watermark:{state:0},no_reprint:e["no-reprint"]?1:0,subtitle:{open:0,lan:""},dolby:0,lossless_music:0,up_selection_reply:!1,up_close_reply:!1,up_close_danmu:!1};if(i===2)c.source=s;if(p)c.creation_statement_id=p.id;if(u!=null){c.topic_id=u;c.topic_detail={from_topic_id:u,from_source:"arc.web.search"};if(I!=null)c.mission_id=I}const l=await S(t,Y,{body:c});if(!l||l.error)throw new v(`B站 ${l&&l.error||"draft/add 无返回"}`);const R=l.data&&(l.data.aid!=null?l.data.aid:l.data.draft_id!=null?l.data.draft_id:l.data.id);return{status:"✅ 草稿已保存",title:r,bvid:R!=null?`draft:${R}`:"",url:"https://member.bilibili.com/platform/upload-manager/article?group=draft"}}const P=e.dtime!=null&&e.dtime!==""?Number(e.dtime):null,C={title:r,copyright:i,tid:a,tag:o.join(","),mission_id:I,topic_id:u,topic_detail:u!=null?{from_topic_id:u,from_source:"arc.web.recommend"}:null,desc_format_id:9999,desc:String(e.desc??""),dtime:P,recreate:-1,dynamic:String(e.dynamic??""),interactive:0,act_reserve_create:0,no_disturbance:0,porder:null,adorder_type:9,no_reprint:e["no-reprint"]?1:0,subtitle:{open:0,lan:""},creation_statement:p?{id:p.id}:null,dolby:0,lossless_music:0,up_selection_reply:!1,up_close_reply:!1,up_close_danmu:!1,web_os:1,source:i===2?s:null,watermark:{state:0},cover:f,videos:[{title:r,desc:"",filename:b.filename,cid:b.cid}]},E=await S(t,X,{meta:C});if(!E||E.error)throw new v(`B站 ${E&&E.error||"add/v3 无返回"}`);const x=E.bvid?String(E.bvid):"";return{status:e.dtime?"✅ 定时投稿已提交":"✅ 投稿成功",title:r,bvid:x,url:x?`https://www.bilibili.com/video/${x}`:"(已提交,稍后在创作中心查看)"}}});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{readFileSync as S}from"node:fs";import{cli as T,Strategy as x}from"@jackwener/opencli/registry";import{ArgumentError as c,AuthRequiredError as E,CommandExecutionError as u}from"@jackwener/opencli/errors";const b='input[placeholder="请输入稿件标题"]',A='[editor_id="desc_at_editor"] .ql-editor',j='#tag-container input[placeholder="按回车键Enter创建标签"]',q="#tag-container .tag-pre-wrp .close";export function readEditForm(){const i=document.querySelector('input[placeholder="请输入稿件标题"]'),r=document.querySelector('[editor_id="desc_at_editor"] .ql-editor')?.parentElement.__quill,n=document.querySelectorAll("#tag-container");if(!i||!r||n.length!==1)return null;const e=r.getText();if(typeof e!=="string"||!e.endsWith(`
|
|
2
|
+
`))return null;return{title:i.value,desc:e.slice(0,-1),tags:[...n[0].querySelectorAll(".tag-pre-wrp .label-item-v2-container")].map((a)=>({name:a.querySelector(".label-item-v2-content")?.textContent,removable:Boolean(a.querySelector(".close"))}))}}export async function readArchive({bvid:i}){try{const r=await fetch("/x/vupre/web/archive/view?"+new URLSearchParams({bvid:i,topic_grey:"1"}),{credentials:"include"}),n=await r.json();if(r.status===401||n?.code===-101)return{auth:!0,error:"创作中心登录已失效"};if(!r.ok||n?.code!==0)return{error:`HTTP ${r.status} / ${n?.message??n?.code}`};const e=n.data?.archive,a=document.cookie.split(";").map((o)=>o.trim()).find((o)=>o.startsWith("DedeUserID="))?.slice(11);if(!a||!e||String(e.mid)!==a||e.bvid!==i)return{error:"稿件归属或 BV 号不匹配,停止编辑"};if(typeof e.title!=="string"||typeof e.desc!=="string"||typeof e.tag!=="string")return{error:"稿件缺少标题、简介或标签字段"};return{title:e.title,desc:e.desc,tags:e.tag.split(",").filter(Boolean),revision:JSON.stringify(e)}}catch(r){return{error:r instanceof Error?r.message:String(r)}}}const w=(i,r,n={})=>i.evaluateWithArgs(`(${r.toString()})(args)`,{args:n}),v=(i,r)=>i.length===r.length&&[...i].sort().every((n,e)=>n===[...r].sort()[e]),p=(i,r)=>i?.title===r.title&&i.desc===r.desc&&Array.isArray(i.tags)&&v(i.tags,r.tags);async function y(i,r){const n=await w(i,readArchive,{bvid:r});if(n?.auth)throw new E("member.bilibili.com",n.error);if(!n||n.error)throw new u(`读取稿件 ${r} 失败:${n?.error||"空响应"}`);return n}T({site:"bilibili",name:"video-edit",access:"write",description:"编辑自己的视频标题、简介和普通标签;未指定字段及活动话题保持原样。默认 dry-run,加 --execute 才保存并回查后台。AI 声明在官网编辑页不可修改。",domain:"member.bilibili.com",strategy:x.UI,browser:!0,args:[{name:"bvid",type:"string",required:!0,positional:!0,help:"已有视频的完整 BV 号"},{name:"title",type:"string",help:"新的完整标题,最多 80 字"},{name:"desc",type:"string",help:"替换简介(纯文本),可传空字符串清空"},{name:"desc-file",type:"string",help:"从本机 UTF-8 文件读取新的完整简介"},{name:"tag",type:"string",help:"替换全部普通标签,逗号分隔;活动标签保留,不要重复传入"},{name:"execute",type:"boolean",help:"保存修改;缺省仅读取并展示修改内容"},{name:"timeout",type:"int",default:120,help:"整段编辑流程的等待预算(秒);超时后先回查稿件,勿直接重提"}],columns:["bvid","status","field","before","after","url"],func:async(i,r)=>{const n=String(r.bvid||"");if(!/^BV[0-9A-Za-z]{10}$/.test(n))throw new c("请传入完整 BV 号");if(r.desc!==void 0&&r["desc-file"]!==void 0)throw new c("--desc 与 --desc-file 不能同时使用");const e={};if(r.title!==void 0){e.title=String(r.title);if(!e.title.trim()||e.title.length>80)throw new c("标题不能为空且不能超过 80 字")}if(r.desc!==void 0)e.desc=String(r.desc);if(r["desc-file"]!==void 0)try{e.desc=new TextDecoder("utf-8",{fatal:!0}).decode(S(String(r["desc-file"])))}catch(t){throw new c(`读取 UTF-8 简介文件失败:${t.message}`)}if(e.desc!==void 0){e.desc=e.desc.replace(/\r\n/g,`
|
|
3
|
+
`);if(e.desc.length>2000)throw new c("简介不能超过 2000 字")}if(r.tag!==void 0){e.tags=String(r.tag).split(",").map((t)=>t.trim());if(e.tags.some((t)=>!t||t.length>20)||new Set(e.tags).size!==e.tags.length)throw new c("标签不能为空、不能重复,每项最多 20 字")}if(!Object.keys(e).length)throw new c("至少指定 --title、--desc、--desc-file 或 --tag");if(!i)throw new u("稿件编辑需要浏览器会话");const a=`https://member.bilibili.com/platform/upload/video/frame?type=edit&bvid=${n}`;await i.goto(a);await i.wait({selector:b,timeout:20000});const o=await y(i,n),d=await w(i,readEditForm);if(!d||d.tags.some((t)=>typeof t.name!=="string")||!p({...d,tags:d.tags.map((t)=>t.name)},o))throw new u("编辑表单与后台稿件不一致或尚未加载完成,停止修改");const m=d.tags.filter((t)=>!t.removable).map((t)=>t.name);if(e.tags&&(e.tags.length+m.length>10||e.tags.some((t)=>m.includes(t))))throw new c(`普通标签与保留的活动标签合计不能超过 10 个,且不能重复活动标签:${m.join(",")}`);const s={...o,...e,tags:e.tags?[...m,...e.tags]:o.tags},f=Object.fromEntries(Object.keys(e).filter((t)=>t==="tags"?!v(o.tags,s.tags):o[t]!==s[t]).map((t)=>[t,{from:o[t],to:s[t]}])),g=(t)=>(Object.keys(f).length?Object.keys(f):Object.keys(e)).map((l)=>({bvid:n,status:t,field:l,before:l==="tags"?o.tags.join(","):o[l],after:l==="tags"?s.tags.join(","):s[l],url:a}));if(!Object.keys(f).length)return g("unchanged");if(!r.execute)return g("dry-run");if(f.title)await i.fillText(b,s.title);if(f.desc)await i.fillText(A,s.desc);if(f.tags){for(let t=0;t<d.tags.filter((l)=>l.removable).length;t++)await i.click(q,{nth:0});for(const t of e.tags){await i.fillText(j,t);await i.pressKey("Enter")}}const h=await w(i,readEditForm);if(!h||!p({...h,tags:h.tags.map((t)=>t.name)},s))throw new u("写入表单后的内容不匹配,尚未提交;请检查当前编辑页");if((await y(i,n)).revision!==o.revision)throw new u("后台稿件在编辑期间已变化,停止提交,请重新读取");try{await i.click(".submit-add");for(let t=0;t<15;t++){await i.wait({time:1});if(p(await y(i,n),s))return g("updated")}throw Error("后台尚未返回期望的修改内容")}catch(t){throw new u(`稿件 ${n} 已尝试提交,但保存结果未确认:${t.message}。请先回查该稿件,不要直接重复提交。`)}}});
|
package/clis/bilibili/video.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as N,Strategy as j}from"@jackwener/opencli/registry";import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{apiGet as k,resolveBvid as D,parsePageArg as E,selectVideoPart as U}from"./utils.js";function f(i,t){if(!i||typeof i!=="object"||Array.isArray(i))throw new s(`${t} returned a malformed payload`);return i}function C(i){if(i&&typeof i==="object"&&typeof i.session==="string"&&Object.hasOwn(i,"data"))return i.data;return i}function l(i,t){if(i==null)return!1;if(typeof i==="boolean")return i;if(typeof i==="number")return i!==0;throw new s(`${t} returned a malformed flag`)}function T(i,t){if(i==null)return"";if(typeof i==="string")return i;throw new s(`${t} returned a malformed string`)}N({site:"bilibili",name:"video",access:"read",description:"Get Bilibili video metadata (title, author, duration, stats, etc.)",strategy:j.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"BV ID, video URL, or b23.tv short link"},{name:"page",required:!1,help:"分P 选集序号(从 1 开始)。多 P 视频指定某一集,title/cid 返回该集;缺省取整集默认(P1)"}],columns:["field","value"],func:async(i,t)=>{if(!i)throw new s("Browser session required for bilibili video");const n=E(t.page),m=String(t.bvid??"").trim(),g=m.match(/bilibili\.com\/(?:video|bangumi\/play)\/(BV[A-Za-z0-9]+)/i),b=g?g[1]:await D(m);await i.goto(`https://www.bilibili.com/video/${b}/`);const o=C(await k(i,"/x/web-interface/view",{params:{bvid:b}}));f(o,"Bilibili view API");if(o.code!==0)throw new s(`Bilibili view API failed: ${o.message} (${o.code})`);const e=f(o.data,"Bilibili view API data"),r=e.stat||{},p=e.owner||{},d=f(e.rights,"Bilibili view API data.rights"),_=l(d.pay,"Bilibili rights.pay"),S=l(d.ugc_pay,"Bilibili rights.ugc_pay"),B=l(d.arc_pay,"Bilibili rights.arc_pay"),P=l(e.is_upower_exclusive,"Bilibili is_upower_exclusive"),v=_?"vip":S||B?"ugc_pay":P?"upower":"",A=l(d.ugc_pay_preview,"Bilibili rights.ugc_pay_preview")||l(e.is_upower_preview,"Bilibili is_upower_preview"),O=T(e.redirect_url,"Bilibili redirect_url"),I=e.pubdate?new Date((Number(e.pubdate)+28800)*1000).toISOString().slice(0,19)+"+08:00":"";let w=e.title??"",y="",h=e.duration||0;if(n!=null){const a=U(e,n);y=String(a.cid??"");w=(typeof a.part==="string"?a.part.trim():"")||`${e.title??""} P${n}`;if(Number.isFinite(Number(a.duration))&&Number(a.duration)>0)h=Number(a.duration)}const u=h||0,q=Math.floor(u/60),x=u%60,c=[{field:"bvid",value:e.bvid??""},{field:"aid",value:String(e.aid??"")},{field:"title",value:w},{field:"author",value:p.name?`${p.name} (mid: ${p.mid})`:""},{field:"category",value:e.tname_v2||e.tname||""},{field:"publish_time",value:I},{field:"duration",value:u?`${q}m${x}s (${u}s)`:""},{field:"view",value:String(r.view??"")},{field:"danmaku",value:String(r.danmaku??"")},{field:"reply",value:String(r.reply??"")},{field:"like",value:String(r.like??"")},{field:"coin",value:String(r.coin??"")},{field:"favorite",value:String(r.favorite??"")},{field:"share",value:String(r.share??"")},{field:"parts",value:String(e.videos??1)},{field:"thumbnail",value:e.pic??""},{field:"description",value:e.desc??""},{field:"requires_payment",value:String(!!v)},{field:"payment_type",value:v},{field:"pay_preview",value:String(A)},{field:"redirect_url",value:O}];if(n!=null){c.push({field:"page",value:String(n)});c.push({field:"cid",value:y});c.push({field:"series_title",value:e.title??""})}return c}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CommandExecutionError as f}from"@jackwener/opencli/errors";export function douyinAiDeclarationInfo(t){return JSON.stringify(t?{choose_value:"aigc"}:{})}export function douyinAiDeclarationStep(t,s="内容由AI生成"){const n=(e)=>e.getClientRects().length>0&&getComputedStyle(e).visibility!=="hidden",i=(e)=>(e.textContent||"").replace(/\s+/g,""),o=[...document.querySelectorAll('[class*="selectBox-"]')].filter(n),r=[...document.querySelectorAll(".semi-modal")].filter((e)=>n(e)&&i(e).includes("对作品内容添加声明"));if(r.length===0&&o.filter((e)=>i(e)===s).length===1)return{selected:!0};if(t==="verify")return{selected:!1};if(t==="open"){if(r.length===1)return{opened:!0};const e=o.filter((d)=>i(d)==="请选择自主声明");if(e.length!==1)return{error:"未找到唯一的自主声明控件,或已有其他声明"};return{selector:'[class*="selectBox-"]',nth:[...document.querySelectorAll('[class*="selectBox-"]')].indexOf(e[0])}}if(r.length!==1)return{error:"未找到唯一的自主声明弹窗"};const c=[...r[0].querySelectorAll("label.semi-radio")].filter((e)=>n(e)&&i(e)===s);if(c.length!==1||!c[0].querySelector('input[type="radio"]'))return{error:"未找到唯一的“内容由AI生成”选项"};const l=c[0].querySelector('input[type="radio"]');if(l.disabled)return{error:"AI 声明选项不可用"};const a=c[0].classList.contains("semi-radio-checked");if(t==="select"){if(a)return{checked:!0};return{selector:'.semi-modal label.semi-radio input[type="radio"]',nth:[...document.querySelectorAll('.semi-modal label.semi-radio input[type="radio"]')].indexOf(l)}}if(!a)return{error:"AI 声明选项未选中"};const u=[...r[0].querySelectorAll("button")].filter((e)=>n(e)&&i(e)==="确定"&&!e.disabled&&e.getAttribute("aria-disabled")!=="true");if(u.length!==1)return{error:"声明确认按钮不可用"};return{selector:".semi-modal button",nth:[...document.querySelectorAll(".semi-modal button")].indexOf(u[0])}}export async function setDouyinAiDeclaration(t,s="内容由AI生成"){const n=(l)=>t.evaluate(`(${douyinAiDeclarationStep.toString()})(${JSON.stringify(l)}, ${JSON.stringify(s)})`),i=(l)=>{throw new f(`抖音 AI 内容声明设置失败:${l};已停止提交`)},o=await n("open");if(o?.selected)return;if(!o?.opened&&!o?.selector)i(o?.error||"声明控件响应异常");if(o.selector)await t.click(o.selector,{nth:o.nth});await t.wait({time:0.2});const r=await n("select");if(!r?.checked&&!r?.selector)i(r?.error||"声明选项响应异常");if(r.selector)await t.click(r.selector,{nth:r.nth});await t.wait({time:0.2});const c=await n("confirm");if(!c?.selector)i(c?.error||"声明确认响应异常");await t.click(c.selector,{nth:c.nth});for(let l=0;l<10;l++){if((await n("verify"))?.selected)return;await t.wait({time:0.2})}i("确认后未回显“"+s+"”")}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import{AuthRequiredError as u,CommandExecutionError as i}from"@jackwener/opencli/errors";import{unwrapEvaluateResult as y}from"./evaluate-result.js";function f(
|
|
1
|
+
import{AuthRequiredError as u,CommandExecutionError as i}from"@jackwener/opencli/errors";import{unwrapEvaluateResult as y}from"./evaluate-result.js";function f(n,t){const e=String(t??"");return n===401||n===403||/login|cookie|auth|captcha|verify|forbidden|permission|登录|登陆|权限|验证|验证码/i.test(e)}export async function browserFetch(n,t,e,s={}){const c=`
|
|
2
2
|
(async () => {
|
|
3
3
|
const controller = new AbortController();
|
|
4
|
-
const timer = setTimeout(() => controller.abort(), ${Number(
|
|
4
|
+
const timer = setTimeout(() => controller.abort(), ${Number(s.timeoutMs??30000)});
|
|
5
5
|
try {
|
|
6
6
|
const res = await fetch(${JSON.stringify(e)}, {
|
|
7
7
|
method: ${JSON.stringify(t)},
|
|
8
|
-
credentials:
|
|
8
|
+
credentials: ${JSON.stringify(s.credentials??"include")},
|
|
9
9
|
signal: controller.signal,
|
|
10
10
|
headers: {
|
|
11
11
|
'Content-Type': 'application/json',
|
|
12
|
-
...${JSON.stringify(
|
|
12
|
+
...${JSON.stringify(s.headers??{})}
|
|
13
13
|
},
|
|
14
|
-
${
|
|
14
|
+
${s.body?`body: JSON.stringify(${JSON.stringify(s.body)}),`:""}
|
|
15
15
|
});
|
|
16
16
|
const text = await res.text();
|
|
17
17
|
try {
|
|
@@ -25,4 +25,4 @@ import{AuthRequiredError as u,CommandExecutionError as i}from"@jackwener/opencli
|
|
|
25
25
|
clearTimeout(timer);
|
|
26
26
|
}
|
|
27
27
|
})()
|
|
28
|
-
`;let r;try{r=y(await
|
|
28
|
+
`;let r;try{r=y(await n.evaluate(c))}catch(o){throw new i(`Douyin API request failed (${t} ${e}): ${o instanceof Error?o.message:String(o)}`)}if(r==null)throw new i(`Empty response from Douyin API (${t} ${e})`);if(Array.isArray(r)||typeof r!=="object")throw new i(`Malformed response from Douyin API (${t} ${e})`);if(r&&typeof r==="object"&&"status_code"in r){const o=r.status_code;if(o!==0){const a=r.status_msg??r.message??"unknown error";if(f(o,a))throw new u("creator.douyin.com",`Douyin API auth/permission error ${o} at ${t} ${e}: ${a}`);throw new i(`Douyin API error ${o} at ${t} ${e}: ${a}`)}}return r}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ArgumentError as s,CommandExecutionError as u}from"@jackwener/opencli/errors";import{clickControl as c,fillControl as m,verifyControl as n,publicationTime as h,resolveDeclaration as y}from"../../_shared/publish-controls.js";import{setDouyinAiDeclaration as S}from"./ai-declaration.js";export const DOUYIN_DECLARATIONS={ai:["aigc","内容由AI生成"],opinion:["personal_opinion","内容为个人观点或见解"],marketing:["marketing","内容含营销推广信息"],fiction:["only_fun_new","虚构演绎,仅供娱乐"],none:["__NO_NEED_USER_DECLARATION__","无需添加自主声明"],reprint:["repost","内容为转载信息"]},DOUYIN_DECLARATION_ARGS=[{name:"declaration",choices:Object.keys(DOUYIN_DECLARATIONS),help:"自主声明:ai / opinion / marketing / fiction / reprint / none"},{name:"source",choices:["external","douyin"],help:"转载来源:external站外 / douyin站内,reprint必填"},{name:"source-video",help:"站内转载来源作品ID(UI命令须在当前观看历史中)"}],DOUYIN_UI_ARGS=[...DOUYIN_DECLARATION_ARGS,{name:"schedule",help:"定时发布,北京时间 YYYY-MM-DD HH:mm / ISO8601 / Unix秒;2小时至14天后"},{name:"allow-download",type:"boolean",help:"允许保存作品(true/false,不传保留平台默认)"},{name:"collection",help:"合集完整名称;先用 douyin collections 查看账号已有合集"},{name:"location",help:"地点完整名称,按平台搜索结果精确选择"},{name:"location-address",help:"地点完整地址,搜索结果重名时用于区分(需 --location)"},{name:"hotspot",help:"关联热点完整名称"},{name:"activity",help:"官方活动完整名称;先用 activities 查询,再用 --id 读参与要求"},{name:"music",help:"音乐完整名称(需要同时指定 --music-author)"},{name:"music-author",help:"音乐作者完整名称,用于区分同名曲目;先用 douyin music 搜索"},{name:"music-id",help:"查询结果的精确音乐 ID,避免同名同作者误选"},{name:"music-query",help:"查询结果中的原搜索词,保留原候选范围"},{name:"music-category",help:"查询结果中的分类名称,与 music-query 二选一"}],DOUYIN_VIDEO_ARGS=[...DOUYIN_UI_ARGS,{name:"sync-toutiao",type:"boolean",help:"同步到今日头条;需公开且平台控件可用,不修改记住我的选择"}];export function validateDouyinSettings(e){if(e["location-address"]&&!e.location)throw new s("--location-address 需要 --location");if(e["sync-toutiao"]&&e.visibility&&e.visibility!=="public")throw new s("同步头条需要公开可见");if(["music-id","music-query","music-category"].some((o)=>e[o])&&!e.music)throw new s("音乐选择参数需要 --music 与 --music-author");if(e["music-query"]&&e["music-category"])throw new s("--music-query 与 --music-category 二选一");if(e["music-id"]&&!/^\d+$/.test(e["music-id"]))throw new s("--music-id 必须为精确音乐 ID");const t=y(e,DOUYIN_DECLARATIONS);f(e,t);if(!!e.music!==!!e["music-author"])throw new s("--music 与 --music-author 必须同时提供");if(e.visibility!==void 0&&!["public","friends","private"].includes(e.visibility))throw new s("不支持的 --visibility");return{declaration:t,schedule:h(e.schedule,2)}}export function douyinDeclarationInfo(e){const t=y(e,DOUYIN_DECLARATIONS);f(e,t);return JSON.stringify(t&&t!=="none"?{choose_value:DOUYIN_DECLARATIONS[t][0],...t==="reprint"?{source:e.source==="external"?"站外":"抖音作品",...e["source-video"]?{from_item_id:e["source-video"]}:{}}:{}}:{})}function f(e,t){if(t==="reprint"&&!["external","douyin"].includes(e.source))throw new s("转载声明需要 --source external 或 douyin");if(e.source==="douyin"&&!/^\d+$/.test(e["source-video"]||""))throw new s("站内转载需要 --source-video 作品ID");if(e["source-video"]&&e.source!=="douyin")throw new s("--source-video 仅用于 --source douyin");if((e.source||e["source-video"])&&t!=="reprint")throw new s("转载来源参数仅用于 --declaration reprint")}async function x(e,t){await c(e,'[class*="selectBox-"]',"请选择自主声明");await c(e,".semi-modal label.semi-radio","内容为转载信息");await c(e,".semi-modal label.semi-radio",t.source==="external"?"取材站外":"取材站内");if(t.source==="douyin"){const o=`((id)=>{const all=[...document.querySelectorAll('[class*="itemContainer-"]')];const found=all.filter(e=>{const f=e[Object.keys(e).find(k=>k.startsWith('__reactFiber'))];return f?.key===id;});return found.length===1 ? all.indexOf(found[0]) : -1;})(${JSON.stringify(t["source-video"])})`;await n(e,`(${o})>=0`,"观看历史中的来源作品");await e.click('[class*="itemContainer-"]',{nth:await e.evaluate(o)});const i=await e.evaluate("([...document.querySelectorAll('.semi-modal')].filter(e=>e.querySelector('.semi-modal-title')?.textContent.trim()==='观看历史').map(e=>[...document.querySelectorAll('.semi-modal button')].indexOf([...e.querySelectorAll('button')].find(b=>b.textContent.trim()==='确定'))))");if(i.length!==1||i[0]<0)throw new u("来源作品确认按钮不可用;已停止提交");await e.click(".semi-modal button",{nth:i[0]});await n(e,"![...document.querySelectorAll('.semi-modal-title')].some(e=>e.textContent.trim()==='观看历史')","来源作品选择")}await c(e,".semi-modal button","确定");await n(e,`![...document.querySelectorAll('.semi-modal')].some(e=>e.getClientRects().length) && [...document.querySelectorAll('[class*="selectBox-"]')].some(e=>e.textContent.trim()==='内容为转载信息')`,"转载声明")}async function d(e,t){if(await e.evaluate(`Boolean([...document.querySelectorAll('label[data-checked], label.semi-radio')].some(e=>e.textContent.trim()===${JSON.stringify(t)} && (e.dataset.checked==='true'||e.classList.contains('semi-radio-checked'))))`))return;await c(e,"label[data-checked], label.semi-radio",t);await n(e,`([...document.querySelectorAll('label[data-checked], label.semi-radio')].filter(e=>e.textContent.trim()===${JSON.stringify(t)}).some(e=>e.dataset.checked==='true'||e.classList.contains('semi-radio-checked')))`,t)}async function w(e,t,o,i,r){const l=Array.isArray(t)?t:[t],a=await e.evaluate(`([...document.querySelectorAll('.semi-select')].filter(e=>${JSON.stringify(l)}.includes(e.textContent.trim())).map(e=>e.textContent.trim()))`);if(a.length!==1)throw new u("找不到唯一的发布选项 "+l.join(" / ")+";已停止提交");await c(e,".semi-select",a[0]);if(r)await m(e,".semi-select-open input",o,!1);await n(e,`([...document.querySelectorAll(${JSON.stringify(i)})].some(e=>e.getClientRects().length && e.textContent.trim()===${JSON.stringify(o)}))`,"候选项 "+o);await c(e,i,o);await n(e,`([...document.querySelectorAll('.semi-select .semi-select-selection-text')].some(e=>e.textContent.trim()===${JSON.stringify(o)}))`,o)}export async function setDouyinPublishSettings(e,t){const o=validateDouyinSettings(t);if(o.declaration==="reprint")await x(e,t);else if(o.declaration)await S(e,DOUYIN_DECLARATIONS[o.declaration][1]);if(t.visibility)await d(e,{public:"公开",friends:"好友可见",private:"仅自己可见"}[t.visibility]);if(t["allow-download"]!==void 0)await d(e,t["allow-download"]?"允许":"不允许");if(o.schedule){await d(e,"定时发布");await m(e,'input[placeholder="日期和时间"]',o.schedule)}if(t.collection)await w(e,["不选择合集","请选择合集"],t.collection,".semi-select-option-text",!1);if(t.location){const i=await e.evaluate("([...document.querySelectorAll('.semi-select')].find(e=>['输入相关位置,让更多人看到你的作品','输入地理位置'].includes(e.textContent.trim()))?.textContent.trim())");if(!i)throw new u("找不到地点选择器;已停止提交");await c(e,".semi-select",i);await m(e,".semi-select-open input",t.location,!1);const r=`(({name,address})=>{const all=[...document.querySelectorAll('.semi-select-option')];const hits=all.filter(e=>e.getClientRects().length && e.querySelector('[class*="name-"]')?.textContent.trim()===name && (!address || e.querySelector('[class*="address-"]')?.textContent.trim()===address));return hits.length===1 ? all.indexOf(hits[0]) : -1;})(${JSON.stringify({name:t.location,address:t["location-address"]})})`;await n(e,`(${r})>=0`,"唯一地点(重名时指定 --location-address)");await e.click(".semi-select-option",{nth:await e.evaluate(r)});await n(e,`Boolean([...document.querySelectorAll('.semi-select-selection-text')].some(e=>e.textContent.trim()===${JSON.stringify(t.location)}))`,"地点")}if(t.hotspot)await w(e,"点击输入热点词",t.hotspot,".semi-select-option .select-word",!0);if(t.music){const i=await e.evaluate("location.pathname.includes('/post/video')");await c(e,i?'[class*="preview-button-"]':'[class*="action-"]',i?"添加音乐":"选择音乐");await n(e,`!!document.querySelector('input[placeholder="搜索音乐"]')?.getClientRects().length`,"音乐选择器",15000);if(t["music-category"])await c(e,'.semi-sidesheet [role="tab"]',t["music-category"]);else{await m(e,'input[placeholder="搜索音乐"]',t["music-query"]||t.music,!1);await e.pressKey("Enter")}const r=`(({title,author,id})=>{const musicId=e=>{let f=e[Object.keys(e).find(k=>k.startsWith('__reactFiber'))];for(let i=0;f&&i<5;i++,f=f.return)if(f.memoizedProps?.id&&f.memoizedProps?.title)return String(f.memoizedProps.id);};const cards=[...document.querySelectorAll('[class*="card-wrapper-"]')].filter(e=>e.getClientRects().length && e.querySelector('[class*="song-name-"]')?.textContent.trim()===title && e.querySelector('.song-author')?.textContent.trim()===author && (!id || musicId(e)===id));return cards.length===1 ? [...document.querySelectorAll('[class*="apply-btn-"]')].indexOf(cards[0].querySelector('[class*="apply-btn-"]')) : -1;})(${JSON.stringify({title:t.music,author:t["music-author"],id:t["music-id"]})})`;let l=-1;for(let a=0;a<40;a++){l=await e.evaluate(r);if(l>=0)break;await e.evaluate(`(() => { const list=document.querySelector('[class*="music-collection-container-"]'); if(list){list.scrollTop=list.scrollHeight;list.dispatchEvent(new Event('scroll',{bubbles:true}));} })()`);await e.wait({time:0.5})}if(l<0)throw new u("未在音乐选择器加载到唯一候选(20秒上限);请重新查询或缩小关键词,已停止提交");await e.click('[class*="apply-btn-"]',{nth:l});await n(e,i?`document.querySelector('[class*="music-text-"]')?.textContent.includes(${JSON.stringify(t.music+" - @"+t["music-author"])}) && !document.querySelector('.semi-sidesheet-close')`:`document.querySelector('[class*="sub-desc-title-"]')?.textContent.trim()===${JSON.stringify(t.music)}`,"音乐")}if(t.activity){const i='[class*="activity-list-container-"] [class*="title-"]';if(await e.evaluate(`Boolean([...document.querySelectorAll(${JSON.stringify(i)})].some(e=>e.textContent.trim()===${JSON.stringify(t.activity)}))`)){const l=`Boolean([...document.querySelectorAll(${JSON.stringify(i)})].find(e=>e.textContent.trim()===${JSON.stringify(t.activity)})?.closest('[class*="activity-right-"]')?.textContent.includes('已添加'))`;if(!await e.evaluate(l))await c(e,i,t.activity);await n(e,l,"活动")}else{const l=await e.evaluate(`([...document.querySelectorAll('[class*="more-"]')].filter(e=>e.getClientRects().length && /^\\+\\d+$/.test(e.textContent.trim())).map(e=>e.textContent.trim()))`);if(l.length!==1)throw new u("活动不在推荐中,且未找到完整活动列表;已停止提交");await c(e,'[class*="more-"]',l[0]);await n(e,`Boolean([...document.querySelectorAll('.semi-sidesheet [class*="title-"]')].some(e=>e.textContent.trim()===${JSON.stringify(t.activity)}))`,"活动 "+t.activity);const a=`Boolean([...document.querySelectorAll('.semi-sidesheet [class*="title-"]')].find(e=>e.textContent.trim()===${JSON.stringify(t.activity)})?.closest('[class*="container-checked-"]'))`;if(!await e.evaluate(a))await c(e,'.semi-sidesheet [class*="title-"]',t.activity);await n(e,a,"选中活动");await c(e,'.semi-sidesheet [class*="apply-"]',"确认");await n(e,`!document.querySelector('.semi-sidesheet-close') && document.querySelector('.zone-container')?.textContent.includes(${JSON.stringify(t.activity)})`,"活动话题")}}if(t["sync-toutiao"]!==void 0){await d(e,t["sync-toutiao"]?"同时发布到":"不同时发布");if(t["sync-toutiao"]){const i='[class*="app-group-container-item-"] .semi-switch',r=`document.querySelector(${JSON.stringify(i+" input")})?.checked === true`;await n(e,`document.querySelector('[class*="app-group-container-item-"]')?.textContent.trim()==='今日头条'`,"头条同步选项");if(!await e.evaluate(r))await c(e,i);await n(e,r,"同步头条")}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as m,Strategy as s}from"@jackwener/opencli/registry";import{ArgumentError as a,CommandExecutionError as c}from"@jackwener/opencli/errors";import{browserFetch as l}from"./_shared/browser-fetch.js";m({site:"douyin",name:"activities",access:"read",domain:"creator.douyin.com",strategy:s.COOKIE,description:"查看/筛选官方活动;--id 查看参与规则。ui_args 用于图文/草稿,api_args 用于 publish;规则可能另有必带话题或@要求",args:[{name:"query",help:"按活动名称筛选"},{name:"id",help:"活动 ID,读取完整参与规则"},{name:"limit",type:"int",default:20}],columns:["activity_id","title","start_time","end_time","topics","requirements","ui_args","api_args"],func:async(d,t)=>{const e=t.limit??20;if(!Number.isInteger(e)||e<1||e>100)throw new a("--limit 必须为1至100的整数");if(t.id&&!/^\d+$/.test(t.id))throw new a("--id 必须为活动 ID");if(t.id&&t.query)throw new a("--id 与 --query 二选一");const n=await l(d,"GET",t.id?`https://creator.douyin.com/web/api/media/activity/detail/?activity_id=${t.id}`:"https://creator.douyin.com/web/api/media/activity/get/?page=1&size=9999&need_challenge=1&aid=1128"),o=t.id?n.activity_info?[n.activity_info]:null:n.activity_list;if(!Array.isArray(o))throw new c("活动响应缺少活动列表或详情,不能视为空结果");return o.filter((i)=>String(i.title??i.activity_name??"").toLowerCase().includes(String(t.query||"").toLowerCase())).slice(0,e).map((i)=>{const r=i.title??i.activity_name;if(!i.activity_id||!r)throw new c("活动缺少 ID 或名称");return{activity_id:String(i.activity_id),title:r,start_time:i.show_start_time||"",end_time:typeof i.end_time==="number"?new Date(i.end_time*1000).toLocaleString("zh-CN",{timeZone:"Asia/Shanghai"}):i.show_end_time||"",topics:i.challenge??null,requirements:t.id?n.activity_description??null:"用 activities --id "+i.activity_id+" 查看完整规则",ui_args:i.challenge?.length?{activity:r}:null,api_args:{activity:String(i.activity_id)}}})}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as s,Strategy as a}from"@jackwener/opencli/registry";import{ArgumentError as e,CommandExecutionError as c}from"@jackwener/opencli/errors";import{browserFetch as u}from"./_shared/browser-fetch.js";s({site:"douyin",name:"collections",access:"read",description:"合集列表",domain:"creator.douyin.com",strategy:a.COOKIE,args:[{name:"limit",type:"int",default:20},{name:"cursor",default:"0",help:"上一页返回的 next_cursor"}],columns:["mix_id","name","item_count","next_cursor","has_more","ui_args","api_args"],func:async(n,o)=>{const i=o.limit??20;if(!Number.isInteger(i)||i<1||i>100)throw new e("--limit 必须为1至100的整数");if(!/^\d+$/.test(String(o.cursor??"0")))throw new e("--cursor 必须为非负整数游标");const m=`https://creator.douyin.com/web/api/mix/list/?status=0,1,2,3,6&count=${i}&cursor=${o.cursor??"0"}&should_query_new_mix=1&device_platform=web&aid=1128`,t=await u(n,"GET",m);if(!Array.isArray(t.mix_list))throw new c("合集响应缺少 mix_list,不能视为空列表");return t.mix_list.map((r)=>({mix_id:r.mix_id,name:r.mix_name,item_count:r.item_count,next_cursor:String(t.cursor??""),has_more:Boolean(t.has_more),ui_args:{collection:r.mix_name},api_args:{collection:String(r.mix_id)}}))}});
|
package/clis/douyin/draft.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import*as f from"node:fs";import*as s from"node:path";import{cli as
|
|
1
|
+
import{DOUYIN_VIDEO_ARGS as w,validateDouyinSettings as v,setDouyinPublishSettings as h}from"./_shared/publish-settings.js";import*as f from"node:fs";import*as s from"node:path";import{cli as T,Strategy as A}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as l}from"@jackwener/opencli/errors";import{setInputFiles as m}from"../_shared/file-inject.js";const p={public:"公开",friends:"好友可见",private:"仅自己可见"},x="https://creator.douyin.com/creator-micro/content/upload",S=120,E=20,I=20,C=200;async function b(t){await t.evaluate(`() => {
|
|
2
2
|
const targets = ['我知道了', '知道了', '关闭'];
|
|
3
3
|
for (const text of targets) {
|
|
4
4
|
const btn = Array.from(document.querySelectorAll('button,[role="button"]'))
|
|
5
5
|
.find((el) => (el.textContent || '').trim() === text);
|
|
6
6
|
if (btn instanceof HTMLElement) btn.click();
|
|
7
7
|
}
|
|
8
|
-
}`)}async function
|
|
8
|
+
}`)}async function k(t){let n={href:"",ready:!1,bodyText:""};for(let e=0;e<S;e+=1){n=await t.evaluate(`() => ({
|
|
9
9
|
href: location.href,
|
|
10
10
|
ready: !!Array.from(document.querySelectorAll('input')).find(
|
|
11
11
|
(el) => (el.placeholder || '').includes('填写作品标题')
|
|
@@ -13,7 +13,7 @@ import*as f from"node:fs";import*as s from"node:path";import{cli as w,Strategy a
|
|
|
13
13
|
(el) => (el.textContent || '').includes('暂存离开')
|
|
14
14
|
),
|
|
15
15
|
bodyText: document.body?.innerText || ''
|
|
16
|
-
})`);if(n.ready)return;await t.wait({time:0.5})}throw new l("等待抖音草稿编辑页超时",`当前页面: ${n.href||"unknown"}`)}async function
|
|
16
|
+
})`);if(n.ready)return;await t.wait({time:0.5})}throw new l("等待抖音草稿编辑页超时",`当前页面: ${n.href||"unknown"}`)}async function _(t,n){for(let e=0;e<C;e+=1){const i=await t.evaluate(`() => {
|
|
17
17
|
const text = document.body?.innerText || '';
|
|
18
18
|
const uploading = /上传中|上传\\s*\\d+%|视频上传中|处理中/.test(text);
|
|
19
19
|
const saveBtn = Array.from(document.querySelectorAll('button')).find(
|
|
@@ -27,7 +27,7 @@ import*as f from"node:fs";import*as s from"node:path";import{cli as w,Strategy a
|
|
|
27
27
|
(el) => (el.textContent || '').includes(${JSON.stringify(n)})
|
|
28
28
|
);
|
|
29
29
|
return { uploading, saveEnabled, hasSaveBtn: !!saveBtn, hasCaptionEditor, hasVisibility };
|
|
30
|
-
}`);if(i.hasSaveBtn&&i.saveEnabled&&i.hasCaptionEditor&&i.hasVisibility&&!i.uploading)return;await t.wait({time:0.5})}}async function
|
|
30
|
+
}`);if(i.hasSaveBtn&&i.saveEnabled&&i.hasCaptionEditor&&i.hasVisibility&&!i.uploading)return;await t.wait({time:0.5})}}async function g(t,n){const e=60;let i=!1;for(let o=0;o<e;o+=1){i=await t.evaluate(`() => {
|
|
31
31
|
const titleInput = Array.from(document.querySelectorAll('input')).find(
|
|
32
32
|
(el) => (el.placeholder || '').includes('填写作品标题')
|
|
33
33
|
);
|
|
@@ -71,7 +71,7 @@ import*as f from"node:fs";import*as s from"node:path";import{cli as w,Strategy a
|
|
|
71
71
|
if (!(visibility instanceof HTMLElement)) return false;
|
|
72
72
|
visibility.click();
|
|
73
73
|
return true;
|
|
74
|
-
}`);if(r)break;await t.wait({time:0.5})}if(!r)throw new l("填写抖音草稿表单失败: visibility-missing")}async function
|
|
74
|
+
}`);if(r)break;await t.wait({time:0.5})}if(!r)throw new l("填写抖音草稿表单失败: visibility-missing")}async function L(t){let n="cover-input-missing";const e=await t.evaluate(`() => Array.from(document.querySelectorAll('input[type="file"]')).length`);for(let i=0;i<E;i+=1){const r=await t.evaluate(`() => {
|
|
75
75
|
const coverLabel = Array.from(document.querySelectorAll('label')).find(
|
|
76
76
|
(el) => (el.textContent || '').includes('上传新封面')
|
|
77
77
|
);
|
|
@@ -110,7 +110,7 @@ import*as f from"node:fs";import*as s from"node:path";import{cli as w,Strategy a
|
|
|
110
110
|
root = root.parentElement;
|
|
111
111
|
}
|
|
112
112
|
return '';
|
|
113
|
-
}`}async function
|
|
113
|
+
}`}async function O(t){return await t.evaluate(buildCoverCheckPanelTextJs())||""}async function M(t){let n="",e=!1;for(let i=0;i<I;i+=1){const r=await O(t),o=r.includes("检测中"),a=r.includes("重新检测")||r.includes("横/竖双封面缺失");if(o)e=!0;if(e&&a&&!o)return;n=r;await t.wait({time:0.5})}throw new l("等待抖音封面处理完成超时",n||"unknown")}async function P(t){const n=60;let e=null;for(let i=0;i<n;i+=1){e=await t.evaluate(`() => {
|
|
114
114
|
const extractCreationId = () => {
|
|
115
115
|
const titleInput = Array.from(document.querySelectorAll('input')).find(
|
|
116
116
|
(el) => (el.placeholder || '').includes('填写作品标题')
|
|
@@ -160,7 +160,7 @@ import*as f from"node:fs";import*as s from"node:path";import{cli as w,Strategy a
|
|
|
160
160
|
text: (btn.textContent || '').trim(),
|
|
161
161
|
creationId,
|
|
162
162
|
};
|
|
163
|
-
}`);if(e?.ok)break;await t.wait({time:0.5})}if(!e?.ok)throw new l(`点击草稿按钮失败: ${e?.reason||"unknown"}`);if(!e.creationId)throw new l("点击草稿按钮失败: creation-id-missing");return{text:e.text||"暂存离开",creationId:e.creationId}}async function
|
|
163
|
+
}`);if(e?.ok)break;await t.wait({time:0.5})}if(!e?.ok)throw new l(`点击草稿按钮失败: ${e?.reason||"unknown"}`);if(!e.creationId)throw new l("点击草稿按钮失败: creation-id-missing");return{text:e.text||"暂存离开",creationId:e.creationId}}async function q(t,n){let e={href:"",bodyText:""};for(let i=0;i<40;i+=1){e=await t.evaluate(`() => ({
|
|
164
164
|
href: location.href,
|
|
165
165
|
bodyText: document.body?.innerText || ''
|
|
166
|
-
})`);const r=e.href||"",o=e.bodyText||"",a=r.includes("/creator-micro/content/upload")&&/继续编辑/.test(o),u=/草稿保存成功|已保存到草稿|存草稿成功|保存成功/.test(o),d=/\/creator-micro\/content\/(manage|drafts|works)/.test(r)||/创作中心/.test(o)&&!r.includes("/content/upload");if(a||u||d)return n;await t.wait({time:1})}throw new l("未检测到抖音草稿保存确认",`当前页面: ${e.href||"unknown"}`)}
|
|
166
|
+
})`);const r=e.href||"",o=e.bodyText||"",a=r.includes("/creator-micro/content/upload")&&/继续编辑/.test(o),u=/草稿保存成功|已保存到草稿|存草稿成功|保存成功/.test(o),d=/\/creator-micro\/content\/(manage|drafts|works)/.test(r)||/创作中心/.test(o)&&!r.includes("/content/upload");if(a||u||d)return n;await t.wait({time:1})}throw new l("未检测到抖音草稿保存确认",`当前页面: ${e.href||"unknown"}`)}T({site:"douyin",name:"draft",access:"write",description:"上传视频并保存为草稿",domain:"creator.douyin.com",strategy:A.COOKIE,navigateBefore:!1,defaultWindowMode:"foreground",args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"视频标题(≤30字)"},{name:"caption",fromFile:!0,default:"",help:"正文内容(≤1000字,支持 #话题)"},{name:"cover",default:"",help:"封面图片路径"},{name:"visibility",default:"public",choices:["public","friends","private"]},{name:"timeout",type:"int",default:180,help:"命令超时(秒),视频上传/转码慢时可调大"},{name:"ai-generated",type:"boolean",help:"声明内容由 AI 生成(发布和草稿均支持;设置失败时停止提交)"},...w],columns:["status","draft_id"],func:async(t,n)=>{v(n);const e=s.resolve(n.video);if(!f.existsSync(e))throw new c(`视频文件不存在: ${e}`);const i=s.extname(e).toLowerCase();if(![".mp4",".mov",".avi",".webm"].includes(i))throw new c(`不支持的视频格式: ${i}(支持 mp4/mov/avi/webm)`);const r=n.title;if(r.length>30)throw new c("标题不能超过 30 字");const o=n.caption||"";if(o.length>1000)throw new c("正文不能超过 1000 字");const a=n.cover;if(a){if(!f.existsSync(s.resolve(a)))throw new c(`封面文件不存在: ${s.resolve(a)}`)}const u=p[n.visibility]??p.public;await t.goto(x);await t.wait({selector:'input[type="file"]',timeout:20});await b(t);await m(t,'input[type="file"]',[e]);await k(t);await _(t,u);await b(t);if(a){const y=await L(t);await m(t,y,[s.resolve(a)]);await M(t)}await g(t,{title:r,caption:o,visibilityLabel:u});await t.wait({time:1});await h(t,{...n,visibility:void 0});const d=await P(t);return[{status:"✅ 草稿已保存,可在创作中心继续编辑",draft_id:await q(t,d.creationId)}]}});
|
package/clis/douyin/hashtag.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as w,Strategy as
|
|
1
|
+
import{cli as w,Strategy as p}from"@jackwener/opencli/registry";import{browserFetch as d}from"./_shared/browser-fetch.js";import{ArgumentError as l,CommandExecutionError as a}from"@jackwener/opencli/errors";function m(t){return t!==null&&typeof t==="object"&&!Array.isArray(t)}function y(t,n,i){if(!m(t))throw new a(`douyin hashtag ${i}: API returned malformed payload`);const r=t[n];if(r===void 0||r===null)throw new a(`douyin hashtag ${i}: API missing "${n}"`);if(!Array.isArray(r))throw new a(`douyin hashtag ${i}: API returned malformed "${n}"`);return r}function g(t){const n=t.action;if(!Number.isInteger(t.limit??10)||(t.limit??10)<1||(t.limit??10)>100)throw new l("--limit 必须为1至100的整数");if(n==="search"){if(!String(t.keyword??"").trim())throw new l("douyin hashtag search 需要 --keyword <关键词>","示例: ppcli douyin hashtag search --keyword 美食");return}if(n==="suggest"){if(!String(t.cover??"").trim())throw new l("douyin hashtag suggest 需要 --cover <cover_uri>","suggest 基于已上传的视频封面做 AI 推荐, 不是关键词搜索. 关键词搜索请用 `douyin hashtag search --keyword <词>`.")}}w({site:"douyin",name:"hashtag",access:"read",description:"查询发布话题 / 封面推荐 / 热点;publish_args 用于 publish-image,视频话题需放入 caption 中的 #话题",domain:"creator.douyin.com",strategy:p.COOKIE,args:[{name:"action",required:!0,positional:!0,choices:["search","suggest","hot"],help:"search=关键词搜索 (--keyword 必填), suggest=AI推荐 (--cover 必填), hot=热点词 (--keyword 可选)"},{name:"keyword",default:"",help:"搜索关键词. search 必填; hot 可选; suggest 不使用 (传 --cover)"},{name:"cover",default:"",help:"封面 URI (cover_uri). suggest 必填; 其它 action 不使用"},{name:"limit",type:"int",default:10}],columns:["name","id","view_count","publish_args"],validateArgs:g,func:async(t,n)=>{g(n);const i=n.action;if(i==="search"){const r=String(n.keyword??"").trim(),u=`https://creator.douyin.com/aweme/v1/search/challengesug/?keyword=${encodeURIComponent(r)}&source=challenge_create&aid=2906`,c=await d(t,"GET",u),s=y(c,"sug_list","search").slice(0,n.limit),o=s.flatMap((h)=>{const e=h;if(!m(e)||!e.cid||!e.cha_name)throw new a("话题候选缺少 ID 或名称");return[{name:e.cha_name,id:e.cid,view_count:e.view_count,publish_args:{topics:e.cha_name}}]});if(s.length>0&&o.length===0)throw new a("douyin hashtag search: API returned challenges but none had stable challenge_info shape");return o}if(i==="suggest"){const r=String(n.cover??"").trim(),u=`https://creator.douyin.com/web/api/media/hashtag/rec/?cover_uri=${encodeURIComponent(r)}&aid=1128`,c=await d(t,"GET",u);return y(c,"challenge_list","suggest").slice(0,n.limit).map((o)=>({name:o.cha_name,id:o.cid,view_count:o.view_count??null,publish_args:{topics:o.cha_name}}))}if(i==="hot"){const r=String(n.keyword??"").trim(),u=`https://creator.douyin.com/aweme/v1/hotspot/recommend/?${r?`keyword=${encodeURIComponent(r)}&`:""}aid=1128`,c=await d(t,"GET",u);if(!m(c))throw new a("douyin hashtag hot: API returned malformed payload");const{hotspot_list:s,all_sentences:o}=c;if(s!==void 0&&s!==null&&!Array.isArray(s))throw new a('douyin hashtag hot: API returned malformed "hotspot_list"');if(o!==void 0&&o!==null&&!Array.isArray(o))throw new a('douyin hashtag hot: API returned malformed "all_sentences"');const h=Array.isArray(s)?s:Array.isArray(o)?o.map((e)=>({sentence:e?.word??"",hot_value:e?.hot_value,sentence_id:e?.sentence_id??""})):null;if(!h)throw new a("热点响应缺少候选数组,不能视为空结果");return h.slice(0,n.limit).map((e)=>({name:e?.sentence??"",id:e&&"sentence_id"in e?e.sentence_id:"",view_count:e?.hot_value??null,publish_args:{hotspot:e?.sentence}}))}throw new l(`未知的 action: ${i}`)}});
|
package/clis/douyin/location.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{ArgumentError as t,CommandExecutionError as n}from"@jackwener/opencli/errors";import{cli as m,Strategy as s}from"@jackwener/opencli/registry";import{browserFetch as d}from"./_shared/browser-fetch.js";m({site:"douyin",name:"location",access:"read",description:"搜索发布地点;ui_args 用于图文/视频草稿,api_args 用于 publish,重名地点按完整地址区分",domain:"creator.douyin.com",strategy:s.COOKIE,args:[{name:"query",required:!0,positional:!0,help:"地名关键词"},{name:"limit",type:"int",default:20},{name:"page",type:"int",default:1,help:"结果页码,从1开始"}],columns:["poi_id","name","address","city","has_more","next_page","ui_args","api_args"],func:async(a,e)=>{if(!String(e.query||"").trim()||!Number.isInteger(e.limit??20)||(e.limit??20)<1||(e.limit??20)>100||!Number.isInteger(e.page??1)||(e.page??1)<1)throw new t("请输入地点关键词、有效 limit(1至100) 和 page(>=1)");const r=`https://creator.douyin.com/aweme/v1/life/video_api/search/poi/?keywords=${encodeURIComponent(e.query)}&count=${e.limit??20}&page=${e.page??1}&from_webapp=1&aid=1128`,o=await d(a,"GET",r);if(!Array.isArray(o.poi_list))throw new n("地点响应缺少 poi_list,不能视为空结果");return o.poi_list.slice(0,e.limit??20).map((i)=>({poi_id:i.poi_id,name:i.poi_name,address:i.address_info?.simple_addr??i.address,city:i.address_info?.city??i.city_name,has_more:Boolean(o.has_more),next_page:o.has_more?o.page??(e.page??1)+1:null,ui_args:{location:i.poi_name,"location-address":i.address_info?.simple_addr??i.address},api_args:{poi_id:i.poi_id,poi_name:i.poi_name}}))}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as i,Strategy as t}from"@jackwener/opencli/registry";import{musicCategories as o}from"./music.js";i({site:"douyin",name:"music-categories",access:"read",domain:"creator.douyin.com",strategy:t.COOKIE,description:"查看推荐、收藏、榜单与音乐分类;将 name 传给 douyin music --category",args:[],columns:["id","name","type"],func:async(a)=>(await o(a)).map((e)=>({id:String(e.category_id),name:e.category_name,type:e.type}))});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as p,Strategy as d}from"@jackwener/opencli/registry";import{ArgumentError as s,CommandExecutionError as h}from"@jackwener/opencli/errors";import{browserFetch as c}from"./_shared/browser-fetch.js";export function requireMusicList(i,t){if(!Array.isArray(i?.[t]))throw new h(`配乐响应缺少 ${t},不能视为空列表`);return i[t]}export async function musicCategories(i){return requireMusicList(await c(i,"GET","https://creator.douyin.com/web/api/media/music/category"),"categories")}p({site:"douyin",name:"music",access:"read",domain:"creator.douyin.com",strategy:d.COOKIE,description:"搜索/查看发布配乐,返回试听地址、作者、时长及 draft/publish-image 可直接使用的 publish_args;视频仅标记音乐信息",args:[{name:"query",positional:!0,help:"音乐关键词;不传时查看 --category(默认推荐)"},{name:"category",help:"分类完整名称,先用 music-categories 查看;与 query 二选一"},{name:"cursor",default:"0",help:"上一页返回的 next_cursor;默认第一页"},{name:"limit",type:"int",default:20,help:"每页1至20首,后续用 cursor 翻页"}],columns:["id","title","author","duration","uses","preview_url","next_cursor","has_more","publish_args"],func:async(i,t)=>{const u=t.limit??20;if(!Number.isInteger(u)||u<1||u>20)throw new s("--limit 必须为1至20的整数");if(!/^\d+$/.test(String(t.cursor??"0")))throw new s("--cursor 必须为返回的非负整数游标");if(t.query&&t.category)throw new s("query 与 --category 二选一");let o,n,m;if(t.query){const r=await c(i,"GET","https://creator.douyin.com/web/api/media/aweme/search/post/auth");if(!r.signature)throw new h("平台未返回音乐搜索授权");const e=new URLSearchParams({aid:"1128",count:String(u),search_source:"normal_search",keyword:t.query,cursor:String(t.cursor??"0")});o=await c(i,"GET","https://tsearch.amemv.com/openapi/aweme/v1/music/search/?"+e,{credentials:"omit",headers:{"Agw-Auth":r.signature,"Openapi-Omit-Shark":"1"}});n=requireMusicList(o,"music");m={"music-query":t.query}}else{const r=t.category||"推荐",e=(await musicCategories(i)).filter((a)=>a.category_name===r);if(e.length!==1)throw new s("找不到唯一音乐分类,请先运行 douyin music-categories");const l=e[0],y=new URLSearchParams({category_id:l.category_id,type:l.type,count:String(u),cursor:String(t.cursor??"0")});o=await c(i,"GET","https://creator.douyin.com/web/api/media/music/list?"+y);n=requireMusicList(o,"songs").map((a)=>({...a,id_str:a.music_id,title:a.music_name,author:a.music_author}));m={"music-category":r}}return n.map((r)=>{const e=r.id_str??r.id;if(!e||typeof e==="number"&&!Number.isSafeInteger(e)||!r.title||!r.author)throw new h("配乐缺少精确 ID、标题或作者,停止返回不可用候选");return{id:String(e),title:r.title,author:r.author,duration:r.duration??null,uses:r.user_count??null,preview_url:typeof r.play_url==="string"?r.play_url:r.play_url?.url_list?.[0]||"",next_cursor:String(o.cursor??""),has_more:Boolean(o.has_more),publish_args:{music:r.title,"music-author":r.author,"music-id":String(e),...m}}})}});
|