publishport-opencli 1.0.9 → 1.0.10

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.
Files changed (42) hide show
  1. package/clis/_shared/file-inject.js +41 -0
  2. package/clis/_shared/video-publish.js +4 -4
  3. package/clis/bilibili/upload.js +1 -1
  4. package/clis/chatgpt/utils.js +5 -5
  5. package/clis/claude/utils.js +1 -1
  6. package/clis/deepseek/utils.js +7 -7
  7. package/clis/douyin/draft.js +6 -6
  8. package/clis/douyin/publish-image.js +10 -31
  9. package/clis/facebook/post.js +9 -36
  10. package/clis/instagram/post.js +19 -66
  11. package/clis/instagram/reel.js +11 -11
  12. package/clis/threads/post.js +15 -34
  13. package/clis/twitter/post.js +15 -42
  14. package/clis/twitter/utils.js +4 -36
  15. package/clis/wechat-channels/publish.js +29 -57
  16. package/clis/weibo/publish.js +8 -8
  17. package/clis/weixin/create-draft.js +7 -7
  18. package/clis/xianyu/publish.js +2 -2
  19. package/clis/xiaohongshu/publish-video.js +9 -27
  20. package/clis/xiaohongshu/publish.js +32 -72
  21. package/dist/src/browser/bridge-readiness.d.ts +1 -1
  22. package/dist/src/browser/daemon-client.d.ts +72 -1
  23. package/dist/src/browser/daemon-client.js +1 -1
  24. package/dist/src/browser/daemon-version.d.ts +1 -1
  25. package/dist/src/browser/errors.d.ts +2 -0
  26. package/dist/src/browser/errors.js +3 -3
  27. package/dist/src/browser/network-interceptor.d.ts +11 -0
  28. package/dist/src/browser/network-interceptor.js +1 -0
  29. package/dist/src/cli.js +24 -24
  30. package/dist/src/commands/daemon.js +1 -1
  31. package/dist/src/daemon-utils.d.ts +1 -0
  32. package/dist/src/daemon-utils.js +1 -1
  33. package/dist/src/daemon.js +1 -1
  34. package/dist/src/doctor.d.ts +1 -1
  35. package/dist/src/doctor.js +7 -7
  36. package/dist/src/errors.d.ts +8 -0
  37. package/dist/src/errors.js +1 -1
  38. package/dist/src/execution.js +2 -2
  39. package/dist/src/session-lease.d.ts +135 -0
  40. package/dist/src/session-lease.js +1 -0
  41. package/dist/src/session-lease.test.d.ts +1 -0
  42. package/package.json +1 -1
@@ -0,0 +1,41 @@
1
+ import*as X from"node:fs";import*as w from"node:path";import{CommandExecutionError as A}from"@jackwener/opencli/errors";const I=6291456,K={".mp4":"video/mp4",".mov":"video/quicktime",".avi":"video/x-msvideo",".mkv":"video/x-matroska",".m4v":"video/x-m4v",".webm":"video/webm",".flv":"video/x-flv",".wmv":"video/x-ms-wmv",".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".webp":"image/webp",".bmp":"image/bmp",".gif":"image/gif"};export function mimeForFile(q){return K[w.extname(q).toLowerCase()]??"application/octet-stream"}export function isFileInputCdpUnsupported(q){const O=q instanceof Error?q.message:String(q);return O.includes("Unknown action")||O.includes("not supported")||O.includes("set-file-input")}export function isFileAccessDenied(q){const O=q instanceof Error?q.message:String(q);return O.includes("Not allowed")||O.includes("File access not granted")}export function isStaleNodeError(q){const O=q instanceof Error?q.message:String(q);return O.includes("No element found matching selector")||O.includes("Could not find node with given id")||O.includes("No node with given id found")}function T(q){if(q&&typeof q==="object"&&"data"in q&&"session"in q)return q.data;return q}export async function injectInputFiles(q,O,z,{chunkBytes:j=I,onProgress:V,findInputJs:W}={}){const Z=`__ppInj_${Math.random().toString(36).slice(2,10)}`,G=JSON.stringify(Z),J=JSON.stringify(O),D=W??"((sel) => document.querySelector(sel))",L=T(await q.evaluate(`/*pp-file-inject:probe*/(() => ({ ok: !!(${D})(${J}) }))()`));if(!L||L.ok!==!0)throw new A(`未找到文件上传输入框: ${O}`);const R=z.reduce((Q,$)=>Q+X.statSync($).size,0);let U=0;try{for(const $ of z){const x=X.statSync($).size,_=X.openSync($,"r");let H=0;try{const N=Buffer.allocUnsafe(j);while(H<x){const M=X.readSync(_,N,0,j,H);if(M<=0)throw new A(`读取文件失败(提前 EOF): ${$}`);const F=JSON.stringify(N.subarray(0,M).toString("base64"));H+=M;U+=M;const Y=T(await q.evaluate(`
2
+ /*pp-file-inject:chunk*/(() => {
3
+ const S = window[${G}] ?? (window[${G}] = { parts: [], bytes: 0, files: [] });
4
+ const raw = atob(${F});
5
+ const len = raw.length;
6
+ const bytes = new Uint8Array(len);
7
+ for (let i = 0; i < len; i++) bytes[i] = raw.charCodeAt(i);
8
+ S.parts.push(new Blob([bytes]));
9
+ S.bytes += len;
10
+ return { ok: S.bytes === ${H}, bytes: S.bytes };
11
+ })()
12
+ `));if(!Y||Y.ok!==!0)throw new A(`文件注入中断:页面累计 ${Y?.bytes??"未知"} 字节 ≠ 已发送 ${H} 字节(标签页可能被刷新/导航走了)`,"保持发布页面前台稳定后重试本命令");if(V)V(U,R)}}finally{X.closeSync(_)}const v=T(await q.evaluate(`
13
+ /*pp-file-inject:seal*/(() => {
14
+ const S = window[${G}];
15
+ if (!S) return { ok: false };
16
+ S.files.push(new File(S.parts, ${JSON.stringify(w.basename($))}, { type: ${JSON.stringify(mimeForFile($))} }));
17
+ S.parts = [];
18
+ S.bytes = 0;
19
+ return { ok: true, files: S.files.length };
20
+ })()
21
+ `));if(!v||v.ok!==!0)throw new A("文件注入中断:页面注入状态丢失(标签页可能被刷新/导航走了)","保持发布页面前台稳定后重试本命令")}const Q=T(await q.evaluate(`
22
+ /*pp-file-inject:commit*/(() => {
23
+ const S = window[${G}];
24
+ delete window[${G}];
25
+ if (!S || !S.files.length) return { ok: false, error: '页面注入状态丢失(标签页可能被刷新/导航走了)' };
26
+ const input = (${D})(${J});
27
+ if (!input) return { ok: false, error: '提交时文件输入框已不在页面上: ' + ${J} };
28
+ const dt = new DataTransfer();
29
+ for (const f of S.files) dt.items.add(f);
30
+ try {
31
+ input.files = dt.files;
32
+ } catch {
33
+ // 个别站点用 Proxy 包过 input,直接赋值可能抛 —— defineProperty 兜底。
34
+ // configurable:true 让同一个 input 能被反复注入(重试/追加时 SPA 常复用同一元素)。
35
+ Object.defineProperty(input, 'files', { value: dt.files, writable: false, configurable: true });
36
+ }
37
+ input.dispatchEvent(new Event('input', { bubbles: true }));
38
+ input.dispatchEvent(new Event('change', { bubbles: true }));
39
+ return { ok: true, count: input.files.length };
40
+ })()
41
+ `));if(!Q||Q.ok!==!0)throw new A(`文件注入提交失败: ${Q?.error??"页面返回异常"}`,"保持发布页面前台稳定后重试本命令");return{count:Number(Q.count)||z.length,bytes:U}}catch(Q){await q.evaluate(`/*pp-file-inject:cleanup*/(() => { delete window[${G}]; })()`).catch(()=>{});throw Q}}export async function setInputFiles(q,O,z,j={}){const V=Array.isArray(O)?O:[O];let W;if(q.setFileInput)for(const Z of V)try{await q.setFileInput(z,Z);return{selector:Z,method:"cdp"}}catch(G){W=G;if(isFileAccessDenied(G)||isFileInputCdpUnsupported(G)){console.warn(`[file-inject] CDP 直读被拒(${G instanceof Error?G.message:G}),切无权限分块注入通道`);break}if(V.length===1&&isStaleNodeError(G))throw G}for(const Z of V)try{const G=await injectInputFiles(q,Z,z,j);return{selector:Z,method:"inject",...G}}catch(G){if(!(G instanceof Error?G.message:String(G)).includes("未找到文件上传输入框"))throw G;W=G}throw new A(`未找到可用的文件上传输入框(尝试过: ${V.join(" , ")}):${W instanceof Error?W.message:String(W)}`)}
@@ -1,4 +1,4 @@
1
- import*as Z from"node:fs";import*as L from"node:path";import{ArgumentError as J,CommandExecutionError as Y}from"@jackwener/opencli/errors";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(q){if(q==null||q==="")return null;const y=String(q),z=/^\d+$/.test(y)?new Date(Number(y)<1000000000000?Number(y)*1000:Number(y)):new Date(y);if(Number.isNaN(z.getTime()))throw new J(`无法解析定时时间: ${q}`);if(z.getTime()<=Date.now())throw new J("定时发布时间必须晚于当前时间");return z}export function unwrap(q){if(q&&typeof q==="object"&&"data"in q&&"session"in q)return q.data;return q}export async function evalPage(q,y){return unwrap(await q.evaluate(y))}export function sleep(q){return new Promise((y)=>setTimeout(y,q*1000))}export function resolveVideoFile(q){const y=L.resolve(String(q));let z;try{z=Z.statSync(y)}catch{throw new J(`视频文件不存在: ${y}`)}if(!z.isFile())throw new J(`视频路径不是文件: ${y}`);const B=L.extname(y).toLowerCase();if(!SUPPORTED_VIDEO_EXTENSIONS.has(B))throw new J(`不支持的视频格式: ${B}(支持 ${[...SUPPORTED_VIDEO_EXTENSIONS].join("/")})`);return y}export function resolveImageFile(q){const y=L.resolve(String(q));let z;try{z=Z.statSync(y)}catch{throw new J(`图片文件不存在: ${y}`)}if(!z.isFile())throw new J(`图片路径不是文件: ${y}`);const B=L.extname(y).toLowerCase();if(!SUPPORTED_IMAGE_EXTENSIONS.has(B))throw new J(`不支持的图片格式: ${B}(支持 ${[...SUPPORTED_IMAGE_EXTENSIONS].join("/")})`);return y}export function throwIfFileAccessDenied(q){if(!(q instanceof Error?q.message:String(q)).includes("Not allowed"))return;throw new Y("Chrome 拦截了扩展读取本地文件(DOM.setFileInputFiles → Not allowed):商店安装的扩展默认没有本地文件访问权限","请让用户在 Chrome 打开 chrome://extensions,找到「ppcli Browser Bridge」→「详情」→ 打开「允许访问文件网址」(Allow access to file URLs),开关即时生效,然后重试本命令")}export async function setVideoInput(q,y,z){if(!q.setFileInput)throw new Y("浏览器扩展不支持 CDP 文件上传(set-file-input),无法上传视频;请升级 PublishPort 客户端/扩展");let B;for(const H of y)try{await q.setFileInput(z,H);return H}catch(K){throwIfFileAccessDenied(K);B=K}throw new Y(`未找到可用的文件上传输入框(尝试过: ${y.join(" , ")}):${B instanceof Error?B.message:String(B)}`)}export async function currentUrl(q){try{if(q.getCurrentUrl){const y=await q.getCurrentUrl();if(y)return String(y)}}catch{}try{return String(await evalPage(q,"() => location.href")||"")}catch{return""}}export async function waitForUrlIncludes(q,y,z=60000,B=500){const H=Array.isArray(y)?y:[y],K=Date.now()+z;while(Date.now()<K){const Q=await currentUrl(q);if(Q&&H.some(($)=>Q.includes($)))return!0;await q.wait({time:B/1000})}return!1}export async function countByText(q,y){return Number(unwrap(await q.evaluate(`
1
+ import*as Y from"node:fs";import*as L from"node:path";import{ArgumentError as H,CommandExecutionError as $}from"@jackwener/opencli/errors";import{isFileAccessDenied as k,setInputFiles as C}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(q){if(q==null||q==="")return null;const y=String(q),z=/^\d+$/.test(y)?new Date(Number(y)<1000000000000?Number(y)*1000:Number(y)):new Date(y);if(Number.isNaN(z.getTime()))throw new H(`无法解析定时时间: ${q}`);if(z.getTime()<=Date.now())throw new H("定时发布时间必须晚于当前时间");return z}export function unwrap(q){if(q&&typeof q==="object"&&"data"in q&&"session"in q)return q.data;return q}export async function evalPage(q,y){return unwrap(await q.evaluate(y))}export function sleep(q){return new Promise((y)=>setTimeout(y,q*1000))}export function resolveVideoFile(q){const y=L.resolve(String(q));let z;try{z=Y.statSync(y)}catch{throw new H(`视频文件不存在: ${y}`)}if(!z.isFile())throw new H(`视频路径不是文件: ${y}`);const B=L.extname(y).toLowerCase();if(!SUPPORTED_VIDEO_EXTENSIONS.has(B))throw new H(`不支持的视频格式: ${B}(支持 ${[...SUPPORTED_VIDEO_EXTENSIONS].join("/")})`);return y}export function resolveImageFile(q){const y=L.resolve(String(q));let z;try{z=Y.statSync(y)}catch{throw new H(`图片文件不存在: ${y}`)}if(!z.isFile())throw new H(`图片路径不是文件: ${y}`);const B=L.extname(y).toLowerCase();if(!SUPPORTED_IMAGE_EXTENSIONS.has(B))throw new H(`不支持的图片格式: ${B}(支持 ${[...SUPPORTED_IMAGE_EXTENSIONS].join("/")})`);return y}export function throwIfFileAccessDenied(q){if(!k(q))return;throw new $("Chrome 拦截了扩展读取本地文件(DOM.setFileInputFiles → File access not granted):商店安装的扩展默认没有本地文件访问权限","请让用户在 Chrome 打开 chrome://extensions,找到「ppcli Browser Bridge」→「详情」→ 打开「允许访问文件网址」(Allow access to file URLs),开关即时生效,然后重试本命令")}export async function setVideoInput(q,y,z){return(await C(q,y,z)).selector}export async function currentUrl(q){try{if(q.getCurrentUrl){const y=await q.getCurrentUrl();if(y)return String(y)}}catch{}try{return String(await evalPage(q,"() => location.href")||"")}catch{return""}}export async function waitForUrlIncludes(q,y,z=60000,B=500){const J=Array.isArray(y)?y:[y],K=Date.now()+z;while(Date.now()<K){const Q=await currentUrl(q);if(Q&&J.some((Z)=>Q.includes(Z)))return!0;await q.wait({time:B/1000})}return!1}export async function countByText(q,y){return Number(unwrap(await q.evaluate(`
2
2
  ((want) => {
3
3
  let n = 0;
4
4
  const nodes = document.querySelectorAll('body *');
@@ -12,7 +12,7 @@ import*as Z from"node:fs";import*as L from"node:path";import{ArgumentError as J,
12
12
  }
13
13
  return n;
14
14
  })(${JSON.stringify(y)})
15
- `)))||0}export async function clickByText(q,y,{exact:z=!1,timeoutMs:B=0,pollMs:H=500}={}){const K=Date.now()+Math.max(0,B);for(;;){if(Boolean(unwrap(await q.evaluate(`
15
+ `)))||0}export async function clickByText(q,y,{exact:z=!1,timeoutMs:B=0,pollMs:J=500}={}){const K=Date.now()+Math.max(0,B);for(;;){if(Boolean(unwrap(await q.evaluate(`
16
16
  ((want, exact) => {
17
17
  const norm = (v) => (v || '').replace(/\\s+/g, ' ').trim();
18
18
  const isVisible = (el) => {
@@ -37,7 +37,7 @@ import*as Z from"node:fs";import*as L from"node:path";import{ArgumentError as J,
37
37
  }
38
38
  return false;
39
39
  })(${JSON.stringify(y)}, ${z?"true":"false"})
40
- `))))return!0;if(Date.now()>=K)return!1;await q.wait({time:H/1000})}}export async function existsByText(q,y,{exact:z=!1}={}){return Boolean(unwrap(await q.evaluate(`
40
+ `))))return!0;if(Date.now()>=K)return!1;await q.wait({time:J/1000})}}export async function existsByText(q,y,{exact:z=!1}={}){return Boolean(unwrap(await q.evaluate(`
41
41
  ((want, exact) => {
42
42
  const norm = (v) => (v || '').replace(/\\s+/g, ' ').trim();
43
43
  const isVisible = (el) => {
@@ -52,7 +52,7 @@ import*as Z from"node:fs";import*as L from"node:path";import{ArgumentError as J,
52
52
  }
53
53
  return false;
54
54
  })(${JSON.stringify(y)}, ${z?"true":"false"})
55
- `)))}export async function waitForSelector(q,y,z=30000,B=500){const H=Date.now()+z;while(Date.now()<H){if(Boolean(unwrap(await q.evaluate(`
55
+ `)))}export async function waitForSelector(q,y,z=30000,B=500){const J=Date.now()+z;while(Date.now()<J){if(Boolean(unwrap(await q.evaluate(`
56
56
  ((sel) => {
57
57
  const el = document.querySelector(sel);
58
58
  if (!el) return false;
@@ -1 +1 @@
1
- import*as I from"node:fs";import*as P from"node:path";import{cli as E,Strategy as b}from"@jackwener/opencli/registry";import{ArgumentError as D,AuthRequiredError as h,CommandExecutionError as S}from"@jackwener/opencli/errors";import{resolveVideoFile as l,resolveImageFile as u,throwIfFileAccessDenied as w}from"../_shared/video-publish.js";const U={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}},f="bda2",k=["SESSDATA","bili_jct","DedeUserID"],m="https://member.bilibili.com/platform/home",_="__pp_bili_video_input";function n(W){var J=document.getElementById(W.inputId);if(!J||!J.files||!J.files[0])return{error:"文件未就绪(input 为空)"};var Z=J.files[0],Y=Z.size,$=W.line;return async function(){try{var G=new URLSearchParams({profile:"ugcfx/bup",name:Z.name,size:String(Y),r:$.os,ssl:"0",version:"2.14.0",build:"2100400",upcdn:$.upcdn,probe_version:String($.probe_version)}),X=await(await fetch("https://member.bilibili.com/preupload?"+G.toString(),{credentials:"include",headers:{Referer:"https://www.bilibili.com"}})).json();if(X.OK!==1)return{error:"preupload 失败:"+JSON.stringify(X)};var K="https:"+X.endpoint+"/"+String(X.upos_uri).replace(/^upos:\/\//,""),O=new URLSearchParams({uploads:"",output:"json",profile:"ugcfx/bup",filesize:String(Y),partsize:String(X.chunk_size),biz_id:String(X.biz_id)}),H=await(await fetch(K+"?"+O.toString(),{method:"POST",headers:{"x-upos-auth":X.auth}})).json();if(H.OK!==1)return{error:"获取 upload_id 失败:"+JSON.stringify(H)};var T=Math.max(1,Math.ceil(Y/X.chunk_size));return{url:K,auth:X.auth,uploadId:H.upload_id,chunkSize:X.chunk_size,bizId:X.biz_id,size:Y,name:Z.name,total:T}}catch(R){return{error:"preupload 异常:"+String(R&&R.message||R)}}}()}function d(W){var J=document.getElementById(W.inputId);if(!J||!J.files||!J.files[0])return{ok:!1,error:"文件丢失(页面可能已导航)"};var Z=J.files[0],Y=W.idx*W.chunkSize,$=Z.slice(Y,Math.min(Y+W.chunkSize,W.size)),G=$.size,X=new URLSearchParams({partNumber:String(W.idx+1),uploadId:String(W.uploadId),chunk:String(W.idx),chunks:String(W.total),size:String(G),start:String(Y),end:String(Y+G),total:String(W.size)});return async function(){for(var K=0;K<5;K++){try{var O=await fetch(W.url+"?"+X.toString(),{method:"PUT",headers:{"x-upos-auth":W.auth},body:$});if(O.status<400){var H=await O.text();if(H==="MULTIPART_PUT_SUCCESS"||H==="")return{ok:!0}}}catch(T){}await new Promise(function(T){setTimeout(T,800*(K+1))})}return{ok:!1,error:"分块 "+W.idx+" 多次重试仍失败"}}()}function j(W){var J=[];for(var Z=1;Z<=W.total;Z++)J.push({partNumber:Z,eTag:"etag"});var Y=new URLSearchParams({output:"json",name:W.name,profile:"ugcfx/bup",uploadId:String(W.uploadId),biz_id:String(W.bizId)});return async function(){try{var $=await(await fetch(W.url+"?"+Y.toString(),{method:"POST",headers:{"x-upos-auth":W.auth,"content-type":"application/json; charset=UTF-8"},body:JSON.stringify({parts:J})})).json();if($.OK!==1)return{error:"complete 失败:"+JSON.stringify($)};var G=String($.key).replace(/^\//,"").replace(/\.[^.]+$/,"");return{filename:G,cid:W.bizId}}catch(X){return{error:"complete 异常:"+String(X&&X.message||X)}}}()}function c(W){return async function(){try{var J=await fetch("https://member.bilibili.com/x/vupre/web/topic/type?type_id="+encodeURIComponent(W.tid)+"&pn=0&ps=200",{credentials:"include"}),Z=await J.json();if(!Z||Z.code!==0||!Z.data||!Array.isArray(Z.data.topics))return{error:"获取话题列表失败:"+(Z&&Z.message||"code="+(Z&&Z.code))};var Y=null;for(var $=0;$<Z.data.topics.length;$++)if(Number(Z.data.topics[$].topic_id)===Number(W.topicId)){Y=Z.data.topics[$];break}if(!Y)return{error:"topic_id "+W.topicId+" 不在分区 "+W.tid+" 的可用话题里(用 bilibili topics --tid "+W.tid+" 查合法值)"};return{topicId:Number(Y.topic_id),missionId:Y.mission_id!=null?Number(Y.mission_id):null,name:Y.topic_name||""}}catch(G){return{error:"解析话题异常:"+String(G&&G.message||G)}}}()}function o(W){return async function(){var J=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!J)return{error:"未找到 bili_jct CSRF token"};var Z=decodeURIComponent(J[1]);try{var Y=W.dataUri;if(typeof Y==="string"&&Y.indexOf("data:image/webp")===0){var $=Y.indexOf(",");if($===-1)return{error:"封面 data URI 格式非法"};var G=Y.slice(5,$),X=Y.slice($+1),K;if(G.indexOf("base64")!==-1){var O=atob(X);K=new Uint8Array(O.length);for(var H=0;H<O.length;H++)K[H]=O.charCodeAt(H)}else K=new TextEncoder().encode(decodeURIComponent(X));var T=new Blob([K],{type:"image/webp"}),R=await createImageBitmap(T),B=document.createElement("canvas");B.width=R.width;B.height=R.height;var M=B.getContext("2d");M.fillStyle="#ffffff";M.fillRect(0,0,B.width,B.height);M.drawImage(R,0,0);var q=await new Promise(function(F){B.toBlob(F,"image/jpeg",0.92)});if(!q)return{error:"封面 webp 转码 JPEG 失败"};Y=await new Promise(function(F,x){var A=new FileReader;A.onload=function(){F(String(A.result))};A.onerror=function(){x(Error("FileReader 读取转码结果失败"))};A.readAsDataURL(q)})}var L=new URLSearchParams;L.append("cover",Y);L.append("csrf",Z);var V=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:L.toString()})).json();if(!V||V.code!==0||!V.data||!V.data.url)return{error:"封面上传失败:"+(V&&V.message?V.message:JSON.stringify(V))};return{url:V.data.url}}catch(F){return{error:"封面上传异常:"+String(F&&F.message||F)}}}()}function p(W){return async function(){var J=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!J)return{error:"未找到 bili_jct CSRF token"};var Z=decodeURIComponent(J[1]),Y=W.meta;for(var $ in Y)if(Y[$]===null||Y[$]===void 0)delete Y[$];Y.csrf=Z;try{var G=await fetch("https://member.bilibili.com/x/vu/web/add/v3?csrf="+encodeURIComponent(Z)+"&t="+Date.now(),{method:"POST",credentials:"include",headers:{"Content-Type":"application/json;charset=UTF-8"},body:JSON.stringify(Y)}),X=await G.json();if(!X||X.code!==0)return{error:"add/v3 提交失败:"+(X&&X.message?X.message:"")+"(code="+(X&&X.code)+" status="+G.status+")"};return{bvid:X.data&&X.data.bvid,aid:X.data&&X.data.aid}}catch(K){return{error:"add/v3 异常:"+String(K&&K.message||K)}}}()}function g(W){return async function(){var J=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!J)return{error:"未找到 bili_jct CSRF token"};var Z=decodeURIComponent(J[1]),Y=W.body;Y.csrf=Z;try{var $=await fetch("https://member.bilibili.com/x/vupre/web/draft/add?t="+Date.now()+"&csrf="+encodeURIComponent(Z),{method:"POST",credentials:"include",headers:{"Content-Type":"application/json;charset=UTF-8"},body:JSON.stringify(Y)}),G=await $.json();if(!G||G.code!==0)return{error:"draft/add 保存草稿失败:"+(G&&G.message?G.message:"")+"(code="+(G&&G.code)+" status="+$.status+")"};return{data:G.data}}catch(X){return{error:"draft/add 异常:"+String(X&&X.message||X)}}}()}const N=(W,J,Z)=>W.evaluateWithArgs(`(${J.toString()})(a)`,{a:Z});function s(W){const J=u(W),Z=P.extname(J).toLowerCase(),Y=Z===".png"?"image/png":Z===".webp"?"image/webp":Z===".bmp"?"image/bmp":"image/jpeg",$=I.readFileSync(J).toString("base64");return`data:${Y};base64,${$}`}async function t(W){for(let J=0;J<8;J++){await W.goto(m);await W.wait({time:1});let Z="";try{Z=String(await W.evaluate("location.href")||"")}catch{Z=""}if(/^https?:\/\/member\.bilibili\.com\//.test(Z)){const Y=await W.getCookies({url:"https://member.bilibili.com"}),$=new Map;for(const X of Array.isArray(Y)?Y:[])if(X&&typeof X.name==="string")$.set(X.name,String(X.value??""));const G=k.filter((X)=>!$.get(X));if(!G.length)return $;throw new h("member.bilibili.com",`缺少 B站登录 cookie(${G.join(", ")})。请先在客户端登录哔哩哔哩。`)}}throw new h("member.bilibili.com","无法进入 B站创作中心(可能未登录或被风控)。请先在客户端登录哔哩哔哩。")}E({site:"bilibili",name:"upload",access:"write",description:"投稿视频到 B站(在真实浏览器登录态里走 web 投稿:preupload→upos 分块→add/v3)。默认仅 dry-run 校验,加 --execute 才真正上传并提交。",domain:"member.bilibili.com",strategy:b.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",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(U).join("/")}(默认 ${f})`},{name:"concurrency",type:"number",help:"分块并发数(默认 3)"},{name:"draft",type:"boolean",help:"存草稿而非直接发布(上传视频后存到创作中心草稿箱,不公开)"},{name:"execute",type:"boolean",help:"真正发布投稿;不带(也不带 --draft)则只做 dry-run 校验"}],columns:["status","title","bvid","url"],func:async(W,J)=>{if(!W)throw new S("bilibili upload 需要浏览器会话");const Z=l(String(J.file??"")),Y=String(J.title??"").trim()||P.basename(Z).replace(/\.[^.]+$/,""),$=String(J.tag??"").split(",").map((Q)=>Q.trim()).filter(Boolean);if(!$.length)throw new D("B站投稿至少需要 1 个标签,用 --tag 传(逗号分隔)");if(J.tid==null||J.tid===""||!Number.isFinite(Number(J.tid)))throw new D("必须用 --tid 指定分区 id(数字)。合法值用 `bilibili partitions` 列举,禁止臆造。");const G=Number(J.tid),X=J.copyright!=null&&J.copyright!==""?Number(J.copyright):1;if(X!==1&&X!==2)throw new D("--copyright 只能是 1(自制) 或 2(转载)");const K=String(J.source??"").trim();if(X===2&&!K)throw new D("转载(copyright=2)必须用 --source 提供转载来源");if(J.cover)u(String(J.cover));const O=String(J.line??f),H=U[O];if(!H)throw new D(`未知上传线路「${O}」,可选:${Object.keys(U).join("/")}`);const T=J.concurrency!=null&&J.concurrency!==""?Number(J.concurrency):3,R=Boolean(J.draft),B=Boolean(J.execute)&&!R;await t(W);if(!W.setFileInput)throw new S("浏览器扩展不支持 CDP 文件上传(set-file-input),无法上传视频;请升级 PublishPort 客户端/扩展");await W.evaluate(`(() => { var id=${JSON.stringify(_)}; 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; })()`);try{await W.setFileInput([Z],`#${_}`)}catch(Q){w(Q);throw Q}if(!R&&!B)return{status:"dry-run",title:Y,bvid:"",url:`校验通过:登录态有效、视频已注入就绪、分区 ${G}、标签 [${$.join(", ")}]、版权 ${X===1?"自制":"转载"}。加 --execute 发布,或 --draft 存草稿。`};const M=await N(W,n,{inputId:_,line:H});if(!M||M.error)throw new S(`B站 preupload 失败:${M&&M.error||"无返回"}`);for(let Q=0;Q<M.total;Q++){const z=await N(W,d,{inputId:_,url:M.url,auth:M.auth,uploadId:M.uploadId,idx:Q,total:M.total,size:M.size,chunkSize:M.chunkSize});if(!z||!z.ok)throw new S(`B站分块上传失败(${Q+1}/${M.total}):${z&&z.error||"无返回"}`)}const q=await N(W,j,{url:M.url,auth:M.auth,uploadId:M.uploadId,bizId:M.bizId,name:M.name,total:M.total});if(!q||q.error||!q.filename)throw new S(`B站 complete 失败:${q&&q.error||"未拿到 filename"}`);let L="";if(J.cover){const Q=s(String(J.cover)),z=await N(W,o,{dataUri:Q});if(!z||z.error||!z.url)throw new S(`B站封面上传失败:${z&&z.error||"未拿到 url"}`);L=z.url}let V=null,F=null;if(J.topic!=null&&J.topic!==""){if(!Number.isFinite(Number(J.topic)))throw new D("--topic 必须是 topic_id 数字,用 `bilibili topics --tid <tid>` 取合法值");const Q=await N(W,c,{tid:G,topicId:Number(J.topic)});if(!Q||Q.error)throw new S(`B站话题解析失败:${Q&&Q.error||"无返回"}`);V=Q.topicId;F=Q.missionId}if(R){const Q={videos:[{filename:q.filename,title:Y,desc:"",cid:q.cid,is_4k:!1,is_8k:!1,is_hdr:!1}],cover:L,cover43:L,ai_cover:0,is_ab_cover:0,ab_cover_info:null,title:Y,copyright:X,tid:G,tag:$.join(","),desc:String(J.desc??""),recreate:-1,dynamic:String(J.dynamic??""),is_only_self:0,space_hidden:2,watermark:{state:0},no_reprint:J["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(X===2)Q.source=K;if(V!=null){Q.topic_id=V;Q.topic_detail={from_topic_id:V,from_source:"arc.web.search"};if(F!=null)Q.mission_id=F}const z=await N(W,g,{body:Q});if(!z||z.error)throw new S(`B站 ${z&&z.error||"draft/add 无返回"}`);const y=z.data&&(z.data.aid!=null?z.data.aid:z.data.draft_id!=null?z.data.draft_id:z.data.id);return{status:"✅ 草稿已保存",title:Y,bvid:y!=null?`draft:${y}`:"",url:"https://member.bilibili.com/platform/upload-manager/article?group=draft"}}const x=J.dtime!=null&&J.dtime!==""?Number(J.dtime):null,A={title:Y,copyright:X,tid:G,tag:$.join(","),mission_id:F,topic_id:V,topic_detail:V!=null?{from_topic_id:V,from_source:"arc.web.recommend"}:null,desc_format_id:9999,desc:String(J.desc??""),dtime:x,recreate:-1,dynamic:String(J.dynamic??""),interactive:0,act_reserve_create:0,no_disturbance:0,porder:null,adorder_type:9,no_reprint:J["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:X===2?K:null,watermark:{state:0},cover:L,videos:[{title:Y,desc:"",filename:q.filename,cid:q.cid}]},C=await N(W,p,{meta:A});if(!C||C.error)throw new S(`B站 ${C&&C.error||"add/v3 无返回"}`);const v=C.bvid?String(C.bvid):"";return{status:J.dtime?"✅ 定时投稿已提交":"✅ 投稿成功",title:Y,bvid:v,url:v?`https://www.bilibili.com/video/${v}`:"(已提交,稍后在创作中心查看)"}}});
1
+ import*as I from"node:fs";import*as P from"node:path";import{cli as E,Strategy as b}from"@jackwener/opencli/registry";import{ArgumentError as D,AuthRequiredError as h,CommandExecutionError as L}from"@jackwener/opencli/errors";import{resolveVideoFile as l,resolveImageFile as u}from"../_shared/video-publish.js";import{setInputFiles as w}from"../_shared/file-inject.js";const U={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}},f="bda2",k=["SESSDATA","bili_jct","DedeUserID"],m="https://member.bilibili.com/platform/home",x="__pp_bili_video_input";function n(W){var J=document.getElementById(W.inputId);if(!J||!J.files||!J.files[0])return{error:"文件未就绪(input 为空)"};var Z=J.files[0],Y=Z.size,$=W.line;return async function(){try{var G=new URLSearchParams({profile:"ugcfx/bup",name:Z.name,size:String(Y),r:$.os,ssl:"0",version:"2.14.0",build:"2100400",upcdn:$.upcdn,probe_version:String($.probe_version)}),X=await(await fetch("https://member.bilibili.com/preupload?"+G.toString(),{credentials:"include",headers:{Referer:"https://www.bilibili.com"}})).json();if(X.OK!==1)return{error:"preupload 失败:"+JSON.stringify(X)};var K="https:"+X.endpoint+"/"+String(X.upos_uri).replace(/^upos:\/\//,""),O=new URLSearchParams({uploads:"",output:"json",profile:"ugcfx/bup",filesize:String(Y),partsize:String(X.chunk_size),biz_id:String(X.biz_id)}),H=await(await fetch(K+"?"+O.toString(),{method:"POST",headers:{"x-upos-auth":X.auth}})).json();if(H.OK!==1)return{error:"获取 upload_id 失败:"+JSON.stringify(H)};var T=Math.max(1,Math.ceil(Y/X.chunk_size));return{url:K,auth:X.auth,uploadId:H.upload_id,chunkSize:X.chunk_size,bizId:X.biz_id,size:Y,name:Z.name,total:T}}catch(R){return{error:"preupload 异常:"+String(R&&R.message||R)}}}()}function d(W){var J=document.getElementById(W.inputId);if(!J||!J.files||!J.files[0])return{ok:!1,error:"文件丢失(页面可能已导航)"};var Z=J.files[0],Y=W.idx*W.chunkSize,$=Z.slice(Y,Math.min(Y+W.chunkSize,W.size)),G=$.size,X=new URLSearchParams({partNumber:String(W.idx+1),uploadId:String(W.uploadId),chunk:String(W.idx),chunks:String(W.total),size:String(G),start:String(Y),end:String(Y+G),total:String(W.size)});return async function(){for(var K=0;K<5;K++){try{var O=await fetch(W.url+"?"+X.toString(),{method:"PUT",headers:{"x-upos-auth":W.auth},body:$});if(O.status<400){var H=await O.text();if(H==="MULTIPART_PUT_SUCCESS"||H==="")return{ok:!0}}}catch(T){}await new Promise(function(T){setTimeout(T,800*(K+1))})}return{ok:!1,error:"分块 "+W.idx+" 多次重试仍失败"}}()}function c(W){var J=[];for(var Z=1;Z<=W.total;Z++)J.push({partNumber:Z,eTag:"etag"});var Y=new URLSearchParams({output:"json",name:W.name,profile:"ugcfx/bup",uploadId:String(W.uploadId),biz_id:String(W.bizId)});return async function(){try{var $=await(await fetch(W.url+"?"+Y.toString(),{method:"POST",headers:{"x-upos-auth":W.auth,"content-type":"application/json; charset=UTF-8"},body:JSON.stringify({parts:J})})).json();if($.OK!==1)return{error:"complete 失败:"+JSON.stringify($)};var G=String($.key).replace(/^\//,"").replace(/\.[^.]+$/,"");return{filename:G,cid:W.bizId}}catch(X){return{error:"complete 异常:"+String(X&&X.message||X)}}}()}function j(W){return async function(){try{var J=await fetch("https://member.bilibili.com/x/vupre/web/topic/type?type_id="+encodeURIComponent(W.tid)+"&pn=0&ps=200",{credentials:"include"}),Z=await J.json();if(!Z||Z.code!==0||!Z.data||!Array.isArray(Z.data.topics))return{error:"获取话题列表失败:"+(Z&&Z.message||"code="+(Z&&Z.code))};var Y=null;for(var $=0;$<Z.data.topics.length;$++)if(Number(Z.data.topics[$].topic_id)===Number(W.topicId)){Y=Z.data.topics[$];break}if(!Y)return{error:"topic_id "+W.topicId+" 不在分区 "+W.tid+" 的可用话题里(用 bilibili topics --tid "+W.tid+" 查合法值)"};return{topicId:Number(Y.topic_id),missionId:Y.mission_id!=null?Number(Y.mission_id):null,name:Y.topic_name||""}}catch(G){return{error:"解析话题异常:"+String(G&&G.message||G)}}}()}function o(W){return async function(){var J=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!J)return{error:"未找到 bili_jct CSRF token"};var Z=decodeURIComponent(J[1]);try{var Y=W.dataUri;if(typeof Y==="string"&&Y.indexOf("data:image/webp")===0){var $=Y.indexOf(",");if($===-1)return{error:"封面 data URI 格式非法"};var G=Y.slice(5,$),X=Y.slice($+1),K;if(G.indexOf("base64")!==-1){var O=atob(X);K=new Uint8Array(O.length);for(var H=0;H<O.length;H++)K[H]=O.charCodeAt(H)}else K=new TextEncoder().encode(decodeURIComponent(X));var T=new Blob([K],{type:"image/webp"}),R=await createImageBitmap(T),S=document.createElement("canvas");S.width=R.width;S.height=R.height;var Q=S.getContext("2d");Q.fillStyle="#ffffff";Q.fillRect(0,0,S.width,S.height);Q.drawImage(R,0,0);var q=await new Promise(function(F){S.toBlob(F,"image/jpeg",0.92)});if(!q)return{error:"封面 webp 转码 JPEG 失败"};Y=await new Promise(function(F,_){var A=new FileReader;A.onload=function(){F(String(A.result))};A.onerror=function(){_(Error("FileReader 读取转码结果失败"))};A.readAsDataURL(q)})}var B=new URLSearchParams;B.append("cover",Y);B.append("csrf",Z);var V=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(!V||V.code!==0||!V.data||!V.data.url)return{error:"封面上传失败:"+(V&&V.message?V.message:JSON.stringify(V))};return{url:V.data.url}}catch(F){return{error:"封面上传异常:"+String(F&&F.message||F)}}}()}function p(W){return async function(){var J=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!J)return{error:"未找到 bili_jct CSRF token"};var Z=decodeURIComponent(J[1]),Y=W.meta;for(var $ in Y)if(Y[$]===null||Y[$]===void 0)delete Y[$];Y.csrf=Z;try{var G=await fetch("https://member.bilibili.com/x/vu/web/add/v3?csrf="+encodeURIComponent(Z)+"&t="+Date.now(),{method:"POST",credentials:"include",headers:{"Content-Type":"application/json;charset=UTF-8"},body:JSON.stringify(Y)}),X=await G.json();if(!X||X.code!==0)return{error:"add/v3 提交失败:"+(X&&X.message?X.message:"")+"(code="+(X&&X.code)+" status="+G.status+")"};return{bvid:X.data&&X.data.bvid,aid:X.data&&X.data.aid}}catch(K){return{error:"add/v3 异常:"+String(K&&K.message||K)}}}()}function g(W){return async function(){var J=document.cookie.match(/(?:^|;\s*)bili_jct=([^;]+)/);if(!J)return{error:"未找到 bili_jct CSRF token"};var Z=decodeURIComponent(J[1]),Y=W.body;Y.csrf=Z;try{var $=await fetch("https://member.bilibili.com/x/vupre/web/draft/add?t="+Date.now()+"&csrf="+encodeURIComponent(Z),{method:"POST",credentials:"include",headers:{"Content-Type":"application/json;charset=UTF-8"},body:JSON.stringify(Y)}),G=await $.json();if(!G||G.code!==0)return{error:"draft/add 保存草稿失败:"+(G&&G.message?G.message:"")+"(code="+(G&&G.code)+" status="+$.status+")"};return{data:G.data}}catch(X){return{error:"draft/add 异常:"+String(X&&X.message||X)}}}()}const N=(W,J,Z)=>W.evaluateWithArgs(`(${J.toString()})(a)`,{a:Z});function s(W){const J=u(W),Z=P.extname(J).toLowerCase(),Y=Z===".png"?"image/png":Z===".webp"?"image/webp":Z===".bmp"?"image/bmp":"image/jpeg",$=I.readFileSync(J).toString("base64");return`data:${Y};base64,${$}`}async function t(W){for(let J=0;J<8;J++){await W.goto(m);await W.wait({time:1});let Z="";try{Z=String(await W.evaluate("location.href")||"")}catch{Z=""}if(/^https?:\/\/member\.bilibili\.com\//.test(Z)){const Y=await W.getCookies({url:"https://member.bilibili.com"}),$=new Map;for(const X of Array.isArray(Y)?Y:[])if(X&&typeof X.name==="string")$.set(X.name,String(X.value??""));const G=k.filter((X)=>!$.get(X));if(!G.length)return $;throw new h("member.bilibili.com",`缺少 B站登录 cookie(${G.join(", ")})。请先在客户端登录哔哩哔哩。`)}}throw new h("member.bilibili.com","无法进入 B站创作中心(可能未登录或被风控)。请先在客户端登录哔哩哔哩。")}E({site:"bilibili",name:"upload",access:"write",description:"投稿视频到 B站(在真实浏览器登录态里走 web 投稿:preupload→upos 分块→add/v3)。默认仅 dry-run 校验,加 --execute 才真正上传并提交。",domain:"member.bilibili.com",strategy:b.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",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(U).join("/")}(默认 ${f})`},{name:"concurrency",type:"number",help:"分块并发数(默认 3)"},{name:"draft",type:"boolean",help:"存草稿而非直接发布(上传视频后存到创作中心草稿箱,不公开)"},{name:"execute",type:"boolean",help:"真正发布投稿;不带(也不带 --draft)则只做 dry-run 校验"}],columns:["status","title","bvid","url"],func:async(W,J)=>{if(!W)throw new L("bilibili upload 需要浏览器会话");const Z=l(String(J.file??"")),Y=String(J.title??"").trim()||P.basename(Z).replace(/\.[^.]+$/,""),$=String(J.tag??"").split(",").map((z)=>z.trim()).filter(Boolean);if(!$.length)throw new D("B站投稿至少需要 1 个标签,用 --tag 传(逗号分隔)");if(J.tid==null||J.tid===""||!Number.isFinite(Number(J.tid)))throw new D("必须用 --tid 指定分区 id(数字)。合法值用 `bilibili partitions` 列举,禁止臆造。");const G=Number(J.tid),X=J.copyright!=null&&J.copyright!==""?Number(J.copyright):1;if(X!==1&&X!==2)throw new D("--copyright 只能是 1(自制) 或 2(转载)");const K=String(J.source??"").trim();if(X===2&&!K)throw new D("转载(copyright=2)必须用 --source 提供转载来源");if(J.cover)u(String(J.cover));const O=String(J.line??f),H=U[O];if(!H)throw new D(`未知上传线路「${O}」,可选:${Object.keys(U).join("/")}`);const T=J.concurrency!=null&&J.concurrency!==""?Number(J.concurrency):3,R=Boolean(J.draft),S=Boolean(J.execute)&&!R;await t(W);await W.evaluate(`(() => { var id=${JSON.stringify(x)}; 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 w(W,`#${x}`,[Z]);if(!R&&!S)return{status:"dry-run",title:Y,bvid:"",url:`校验通过:登录态有效、视频已注入就绪、分区 ${G}、标签 [${$.join(", ")}]、版权 ${X===1?"自制":"转载"}。加 --execute 发布,或 --draft 存草稿。`};const Q=await N(W,n,{inputId:x,line:H});if(!Q||Q.error)throw new L(`B站 preupload 失败:${Q&&Q.error||"无返回"}`);for(let z=0;z<Q.total;z++){const M=await N(W,d,{inputId:x,url:Q.url,auth:Q.auth,uploadId:Q.uploadId,idx:z,total:Q.total,size:Q.size,chunkSize:Q.chunkSize});if(!M||!M.ok)throw new L(`B站分块上传失败(${z+1}/${Q.total}):${M&&M.error||"无返回"}`)}const q=await N(W,c,{url:Q.url,auth:Q.auth,uploadId:Q.uploadId,bizId:Q.bizId,name:Q.name,total:Q.total});if(!q||q.error||!q.filename)throw new L(`B站 complete 失败:${q&&q.error||"未拿到 filename"}`);let B="";if(J.cover){const z=s(String(J.cover)),M=await N(W,o,{dataUri:z});if(!M||M.error||!M.url)throw new L(`B站封面上传失败:${M&&M.error||"未拿到 url"}`);B=M.url}let V=null,F=null;if(J.topic!=null&&J.topic!==""){if(!Number.isFinite(Number(J.topic)))throw new D("--topic 必须是 topic_id 数字,用 `bilibili topics --tid <tid>` 取合法值");const z=await N(W,j,{tid:G,topicId:Number(J.topic)});if(!z||z.error)throw new L(`B站话题解析失败:${z&&z.error||"无返回"}`);V=z.topicId;F=z.missionId}if(R){const z={videos:[{filename:q.filename,title:Y,desc:"",cid:q.cid,is_4k:!1,is_8k:!1,is_hdr:!1}],cover:B,cover43:B,ai_cover:0,is_ab_cover:0,ab_cover_info:null,title:Y,copyright:X,tid:G,tag:$.join(","),desc:String(J.desc??""),recreate:-1,dynamic:String(J.dynamic??""),is_only_self:0,space_hidden:2,watermark:{state:0},no_reprint:J["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(X===2)z.source=K;if(V!=null){z.topic_id=V;z.topic_detail={from_topic_id:V,from_source:"arc.web.search"};if(F!=null)z.mission_id=F}const M=await N(W,g,{body:z});if(!M||M.error)throw new L(`B站 ${M&&M.error||"draft/add 无返回"}`);const y=M.data&&(M.data.aid!=null?M.data.aid:M.data.draft_id!=null?M.data.draft_id:M.data.id);return{status:"✅ 草稿已保存",title:Y,bvid:y!=null?`draft:${y}`:"",url:"https://member.bilibili.com/platform/upload-manager/article?group=draft"}}const _=J.dtime!=null&&J.dtime!==""?Number(J.dtime):null,A={title:Y,copyright:X,tid:G,tag:$.join(","),mission_id:F,topic_id:V,topic_detail:V!=null?{from_topic_id:V,from_source:"arc.web.recommend"}:null,desc_format_id:9999,desc:String(J.desc??""),dtime:_,recreate:-1,dynamic:String(J.dynamic??""),interactive:0,act_reserve_create:0,no_disturbance:0,porder:null,adorder_type:9,no_reprint:J["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:X===2?K:null,watermark:{state:0},cover:B,videos:[{title:Y,desc:"",filename:q.filename,cid:q.cid}]},C=await N(W,p,{meta:A});if(!C||C.error)throw new L(`B站 ${C&&C.error||"add/v3 无返回"}`);const v=C.bvid?String(C.bvid):"";return{status:J.dtime?"✅ 定时投稿已提交":"✅ 投稿成功",title:Y,bvid:v,url:v?`https://www.bilibili.com/video/${v}`:"(已提交,稍后在创作中心查看)"}}});
@@ -1,4 +1,4 @@
1
- import{htmlToMarkdown as i}from"@jackwener/opencli/utils";import{ArgumentError as k,AuthRequiredError as _,CommandExecutionError as F,TimeoutError as O}from"@jackwener/opencli/errors";export const CHATGPT_DOMAIN="chatgpt.com",CHATGPT_URL="https://chatgpt.com";const I={instant:{label:"Instant",labels:["Instant","即时","极速"],optionLabels:["Instant","极速","即时"],testIds:["model-switcher-gpt-5-5"],intelligenceOrder:0},medium:{label:"Medium",labels:["Medium","均衡"],optionLabels:["Medium","均衡"],testIds:[],intelligenceOrder:1},high:{label:"High",labels:["High","高级","Thinking","思考"],optionLabels:["High","高级","Thinking","思考"],testIds:["model-switcher-gpt-5-5-thinking"],intelligenceOrder:2},"extra-high":{label:"Extra High",labels:["Extra High","超高"],optionLabels:["Extra High","超高"],testIds:[],intelligenceOrder:3},pro:{label:"Pro",labels:["Pro","进阶专业","专业"],optionLabels:["专业","Pro","进阶专业"],testIds:["model-switcher-gpt-5-5-pro"],intelligenceOrder:4}},w={thinking:"high"};export const CHATGPT_MODEL_CHOICES=[...Object.keys(I),...Object.keys(w)];const G={"deep-research":{label:"Deep Research",labels:["深度研究","Deep Research"]},"web-search":{label:"Web Search",labels:["网页搜索","搜索","Web Search","Search"]}};export const CHATGPT_TOOL_CHOICES=Object.keys(G);const x=['[contenteditable="true"][role="textbox"]','#prompt-textarea[contenteditable="true"]','[aria-label="Chat with ChatGPT"]','[aria-label="与 ChatGPT 聊天"]','[placeholder="Ask anything"]','[placeholder="有问题,尽管问"]',"#prompt-textarea",'[data-testid="prompt-textarea"]'],U='button[data-testid="send-button"]:not([disabled])',L=["#composer-submit-button:not([disabled])"],y=["Send prompt","Send message","Send","发送","发送消息","发送提示"],A=["Close sidebar","关闭边栏"];function N(Q,X){if(!Q||!X)return!1;return Q===X||Q.startsWith(`${X}?`)||Q.startsWith(`${X}#`)}function v(){return`
1
+ import{htmlToMarkdown as m}from"@jackwener/opencli/utils";import{ArgumentError as k,AuthRequiredError as _,CommandExecutionError as F,TimeoutError as O}from"@jackwener/opencli/errors";export const CHATGPT_DOMAIN="chatgpt.com",CHATGPT_URL="https://chatgpt.com";const I={instant:{label:"Instant",labels:["Instant","即时","极速"],optionLabels:["Instant","极速","即时"],testIds:["model-switcher-gpt-5-5"],intelligenceOrder:0},medium:{label:"Medium",labels:["Medium","均衡"],optionLabels:["Medium","均衡"],testIds:[],intelligenceOrder:1},high:{label:"High",labels:["High","高级","Thinking","思考"],optionLabels:["High","高级","Thinking","思考"],testIds:["model-switcher-gpt-5-5-thinking"],intelligenceOrder:2},"extra-high":{label:"Extra High",labels:["Extra High","超高"],optionLabels:["Extra High","超高"],testIds:[],intelligenceOrder:3},pro:{label:"Pro",labels:["Pro","进阶专业","专业"],optionLabels:["专业","Pro","进阶专业"],testIds:["model-switcher-gpt-5-5-pro"],intelligenceOrder:4}},w={thinking:"high"};export const CHATGPT_MODEL_CHOICES=[...Object.keys(I),...Object.keys(w)];const G={"deep-research":{label:"Deep Research",labels:["深度研究","Deep Research"]},"web-search":{label:"Web Search",labels:["网页搜索","搜索","Web Search","Search"]}};export const CHATGPT_TOOL_CHOICES=Object.keys(G);const x=['[contenteditable="true"][role="textbox"]','#prompt-textarea[contenteditable="true"]','[aria-label="Chat with ChatGPT"]','[aria-label="与 ChatGPT 聊天"]','[placeholder="Ask anything"]','[placeholder="有问题,尽管问"]',"#prompt-textarea",'[data-testid="prompt-textarea"]'],U='button[data-testid="send-button"]:not([disabled])',L=["#composer-submit-button:not([disabled])"],y=["Send prompt","Send message","Send","发送","发送消息","发送提示"],A=["Close sidebar","关闭边栏"];function N(Q,X){if(!Q||!X)return!1;return Q===X||Q.startsWith(`${X}?`)||Q.startsWith(`${X}#`)}function v(){return`
2
2
  const isVisible = (el) => {
3
3
  if (!(el instanceof HTMLElement)) return false;
4
4
  const style = window.getComputedStyle(el);
@@ -475,7 +475,7 @@ import{htmlToMarkdown as i}from"@jackwener/opencli/utils";import{ArgumentError a
475
475
  rows.push({ role, text, html });
476
476
  }
477
477
  return rows;
478
- })()`)),"chatgpt visible messages").map((Y,Z)=>({Index:Z+1,Role:Y?.role==="Assistant"?"Assistant":"User",Text:String(Y?.text||"").trim(),Html:String(Y?.html||"")})).filter((Y)=>Y.Text)}function h(Q,{wantMarkdown:X,generating:Y,stableSeconds:Z}){return Q.map(($)=>({Index:$.Index,Role:$.Role,Text:X&&$.Role==="Assistant"&&$.Html?messageHtmlToMarkdown($.Html)||$.Text:$.Text,Generating:Y,StableSeconds:Z}))}export async function getChatGPTDetailRows(Q,{wantMarkdown:X=!1,stableSeconds:Y=0}={}){const Z=await isGenerating(Q),$=await getVisibleMessages(Q);return{messages:$,rows:h($,{wantMarkdown:X,generating:Z,stableSeconds:Y}),generating:Z}}export async function waitForChatGPTDetailRows(Q,{wantMarkdown:X=!1,timeoutSeconds:Y=120,stableSeconds:Z=6}={}){const $=Date.now();let W="",V=0;while(Date.now()-$<Y*1000){const z=await isGenerating(Q),q=await getVisibleMessages(Q),j=JSON.stringify(q.map((J)=>[J.Role,J.Text]));if(!z&&q.length&&q[q.length-1]?.Role==="Assistant")if(j===W){if(!V)V=Date.now();const J=Math.floor((Date.now()-V)/1000);if(J>=Z)return{messages:q,rows:h(q,{wantMarkdown:X,generating:!1,stableSeconds:J}),generating:!1}}else{W=j;V=Date.now()}else{W=j;V=0}await Q.wait(3)}throw new O("chatgpt detail",Y,"Conversation did not finish or stabilize before timeout. Re-run with a higher --timeout if it is still generating.")}export function messageHtmlToMarkdown(Q){try{return i(Q).trim()}catch{return String(Q||"").replace(/<[^>]+>/g," ").replace(/\s+/g," ").trim()}}export async function getBubbleCount(Q){return(await getVisibleMessages(Q)).length}function M(Q){return String(Q||"").replace(/\s+/g," ").trim()}function E(Q,X){return JSON.stringify([M(Q?.Text),String(X?.Text||"").trim()])}export function getChatGPTResponsePairKeys(Q,X){const Y=M(X);if(!Y)return[];const Z=[];for(let $=0;$<Q.length;$+=1){const W=Q[$];if(W?.Role!=="User"||M(W.Text)!==Y)continue;const V=Q.slice($+1).find((z)=>z?.Role==="Assistant");if(!V||!String(V.Text||"").trim())continue;Z.push(E(W,V))}return Z}export function getChatGPTResponsePairCounts(Q,X){const Y=new Map;for(const Z of getChatGPTResponsePairKeys(Q,X))Y.set(Z,(Y.get(Z)||0)+1);return Y}function l(Q){if(Q.baselinePairCounts instanceof Map)return Q.baselinePairCounts;return new Map(Array.from(Q.baselinePairKeys||[]).map((X)=>[X,1]))}function r(Q,X,Y){const Z=M(X);if(!Z)return"";const $=getChatGPTResponsePairCounts(Q,X);for(let W=Q.length-1;W>=0;W-=1){const V=Q[W];if(V?.Role!=="User"||M(V.Text)!==Z)continue;const z=Q.findIndex((J,K)=>K>W&&J?.Role==="Assistant"&&String(J.Text||"").trim());if(z<0)continue;const q=Q[z],j=E(V,q);if(($.get(j)||0)<=(Y.get(j)||0))continue;return String(q.Text||"").trim()}return""}export async function waitForChatGPTResponse(Q,X,Y,Z,$={}){const W=Date.now();let V="",z=0;const q=l($);while(Date.now()-W<Z*1000){await Q.wait(3);if($.conversationUrl){const K=await currentChatGPTUrl(Q);if(K&&!N(K,$.conversationUrl))throw new F(`ChatGPT navigated away from the target conversation (${$.conversationUrl}); current URL is ${K}`)}if(await isGenerating(Q)){z=0;continue}const j=await getVisibleMessages(Q),J=r(j,Y,q);if(!J||J===String(Y||"").trim())continue;if(J===V){z+=1;if(z>=2)return J}else{V=J;z=0}}throw new O("chatgpt ask",Z,"No ChatGPT response appeared before timeout. Re-run with a higher --timeout if it is still generating.")}export async function getConversationList(Q){await ensureOnChatGPT(Q);if(requireBooleanEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`(() => {
478
+ })()`)),"chatgpt visible messages").map((Y,Z)=>({Index:Z+1,Role:Y?.role==="Assistant"?"Assistant":"User",Text:String(Y?.text||"").trim(),Html:String(Y?.html||"")})).filter((Y)=>Y.Text)}function h(Q,{wantMarkdown:X,generating:Y,stableSeconds:Z}){return Q.map(($)=>({Index:$.Index,Role:$.Role,Text:X&&$.Role==="Assistant"&&$.Html?messageHtmlToMarkdown($.Html)||$.Text:$.Text,Generating:Y,StableSeconds:Z}))}export async function getChatGPTDetailRows(Q,{wantMarkdown:X=!1,stableSeconds:Y=0}={}){const Z=await isGenerating(Q),$=await getVisibleMessages(Q);return{messages:$,rows:h($,{wantMarkdown:X,generating:Z,stableSeconds:Y}),generating:Z}}export async function waitForChatGPTDetailRows(Q,{wantMarkdown:X=!1,timeoutSeconds:Y=120,stableSeconds:Z=6}={}){const $=Date.now();let W="",V=0;while(Date.now()-$<Y*1000){const z=await isGenerating(Q),q=await getVisibleMessages(Q),j=JSON.stringify(q.map((J)=>[J.Role,J.Text]));if(!z&&q.length&&q[q.length-1]?.Role==="Assistant")if(j===W){if(!V)V=Date.now();const J=Math.floor((Date.now()-V)/1000);if(J>=Z)return{messages:q,rows:h(q,{wantMarkdown:X,generating:!1,stableSeconds:J}),generating:!1}}else{W=j;V=Date.now()}else{W=j;V=0}await Q.wait(3)}throw new O("chatgpt detail",Y,"Conversation did not finish or stabilize before timeout. Re-run with a higher --timeout if it is still generating.")}export function messageHtmlToMarkdown(Q){try{return m(Q).trim()}catch{return String(Q||"").replace(/<[^>]+>/g," ").replace(/\s+/g," ").trim()}}export async function getBubbleCount(Q){return(await getVisibleMessages(Q)).length}function M(Q){return String(Q||"").replace(/\s+/g," ").trim()}function E(Q,X){return JSON.stringify([M(Q?.Text),String(X?.Text||"").trim()])}export function getChatGPTResponsePairKeys(Q,X){const Y=M(X);if(!Y)return[];const Z=[];for(let $=0;$<Q.length;$+=1){const W=Q[$];if(W?.Role!=="User"||M(W.Text)!==Y)continue;const V=Q.slice($+1).find((z)=>z?.Role==="Assistant");if(!V||!String(V.Text||"").trim())continue;Z.push(E(W,V))}return Z}export function getChatGPTResponsePairCounts(Q,X){const Y=new Map;for(const Z of getChatGPTResponsePairKeys(Q,X))Y.set(Z,(Y.get(Z)||0)+1);return Y}function l(Q){if(Q.baselinePairCounts instanceof Map)return Q.baselinePairCounts;return new Map(Array.from(Q.baselinePairKeys||[]).map((X)=>[X,1]))}function r(Q,X,Y){const Z=M(X);if(!Z)return"";const $=getChatGPTResponsePairCounts(Q,X);for(let W=Q.length-1;W>=0;W-=1){const V=Q[W];if(V?.Role!=="User"||M(V.Text)!==Z)continue;const z=Q.findIndex((J,K)=>K>W&&J?.Role==="Assistant"&&String(J.Text||"").trim());if(z<0)continue;const q=Q[z],j=E(V,q);if(($.get(j)||0)<=(Y.get(j)||0))continue;return String(q.Text||"").trim()}return""}export async function waitForChatGPTResponse(Q,X,Y,Z,$={}){const W=Date.now();let V="",z=0;const q=l($);while(Date.now()-W<Z*1000){await Q.wait(3);if($.conversationUrl){const K=await currentChatGPTUrl(Q);if(K&&!N(K,$.conversationUrl))throw new F(`ChatGPT navigated away from the target conversation (${$.conversationUrl}); current URL is ${K}`)}if(await isGenerating(Q)){z=0;continue}const j=await getVisibleMessages(Q),J=r(j,Y,q);if(!J||J===String(Y||"").trim())continue;if(J===V){z+=1;if(z>=2)return J}else{V=J;z=0}}throw new O("chatgpt ask",Z,"No ChatGPT response appeared before timeout. Re-run with a higher --timeout if it is still generating.")}export async function getConversationList(Q){await ensureOnChatGPT(Q);if(requireBooleanEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`(() => {
479
479
  const button = Array.from(document.querySelectorAll('button'))
480
480
  .find((node) => /open sidebar/i.test(node.getAttribute('aria-label') || ''));
481
481
  if (button instanceof HTMLElement) {
@@ -535,7 +535,7 @@ import{htmlToMarkdown as i}from"@jackwener/opencli/utils";import{ArgumentError a
535
535
  const previewNodes = Array.from(scope.querySelectorAll('img[src], canvas, video, [style*="background-image"]')).filter(isVisibleMedia);
536
536
  return previewNodes.length >= names.length;
537
537
  })()
538
- `)),"chatgpt upload preview detection"))return!0}return!1}export async function uploadChatGPTImages(Q,X){const Y=await import("node:fs"),Z=await import("node:path"),$=await prepareChatGPTImagePaths(X);if(!$.ok)return $;const W=$.paths,V=W.map((j)=>Z.default.basename(j));let z=!1;if(Q.setFileInput)try{await Q.setFileInput(W,'input[type="file"]');z=!0}catch(j){const J=String(j?.message||j);if(!J.includes("Unknown action")&&!J.includes("not supported")&&!J.includes("Not allowed")&&!J.includes("No element found"))throw j}if(!z){const j=W.map((K)=>({name:Z.default.basename(K),mime:P(K),base64:Y.default.readFileSync(K).toString("base64")})),J=requireObjectEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`
538
+ `)),"chatgpt upload preview detection"))return!0}return!1}export async function uploadChatGPTImages(Q,X){const Y=await import("node:fs"),Z=await import("node:path"),$=await prepareChatGPTImagePaths(X);if(!$.ok)return $;const W=$.paths,V=W.map((j)=>Z.default.basename(j));let z=!1;if(Q.setFileInput)try{await Q.setFileInput(W,'input[type="file"]');z=!0}catch(j){const J=String(j?.message||j);if(!J.includes("Unknown action")&&!J.includes("not supported")&&!J.includes("Not allowed")&&!J.includes("File access not granted")&&!J.includes("No element found"))throw j}if(!z){const j=W.map((K)=>({name:Z.default.basename(K),mime:P(K),base64:Y.default.readFileSync(K).toString("base64")})),J=requireObjectEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`
539
539
  (() => {
540
540
  const files = ${JSON.stringify(j)};
541
541
  const input = document.querySelector('input[type="file"]');
@@ -919,7 +919,7 @@ import{htmlToMarkdown as i}from"@jackwener/opencli/utils";import{ArgumentError a
919
919
 
920
920
  return { ok: false };
921
921
  })()
922
- `)),Y=typeof X==="boolean"?{ok:X}:requireObjectEvaluateResult(X,"chatgpt project knowledge dialog open");if(Y.ok){if(Y.nativeClick&&typeof Q.nativeClick==="function")try{await Q.nativeClick(Y.nativeClick.x,Y.nativeClick.y)}catch{}if(Y.nativeClick&&typeof Q.click==="function")try{await Q.click('[role="tab"][id$="-sources"]')}catch{}await Q.wait(1);try{await Q.wait({selector:'[role="dialog"], [data-project-home-sources-surface="true"] input[type="file"]',timeout:5})}catch{}return!0}return!1}export async function uploadChatGPTProjectFiles(Q,X,Y){const Z=parseChatGPTProjectId(X),$=await import("node:fs"),W=await import("node:path"),V=await prepareChatGPTFilePaths(Y);if(!V.ok)return{...V,inputError:!0};const z=V.paths;await navigateToProject(Q,Z);await ensureChatGPTLogin(Q,"ChatGPT project file upload requires a logged-in ChatGPT session.");if(!await openProjectKnowledgeDialog(Q))return{ok:!1,reason:'could not find or click the project "Add files" button'};const j=z.map((B)=>W.default.basename(B));let J=!1;if(Q.setFileInput)for(const B of d)try{await Q.setFileInput(z,B);J=!0;break}catch(H){const D=String(H?.message||H);if(!D.includes("Unknown action")&&!D.includes("not supported")&&!D.includes("Not allowed")&&!D.includes("No element found"))throw H}if(!J){const B=z.map((D)=>({name:W.default.basename(D),mime:m(D),base64:$.default.readFileSync(D).toString("base64")})),H=requireObjectEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`
922
+ `)),Y=typeof X==="boolean"?{ok:X}:requireObjectEvaluateResult(X,"chatgpt project knowledge dialog open");if(Y.ok){if(Y.nativeClick&&typeof Q.nativeClick==="function")try{await Q.nativeClick(Y.nativeClick.x,Y.nativeClick.y)}catch{}if(Y.nativeClick&&typeof Q.click==="function")try{await Q.click('[role="tab"][id$="-sources"]')}catch{}await Q.wait(1);try{await Q.wait({selector:'[role="dialog"], [data-project-home-sources-surface="true"] input[type="file"]',timeout:5})}catch{}return!0}return!1}export async function uploadChatGPTProjectFiles(Q,X,Y){const Z=parseChatGPTProjectId(X),$=await import("node:fs"),W=await import("node:path"),V=await prepareChatGPTFilePaths(Y);if(!V.ok)return{...V,inputError:!0};const z=V.paths;await navigateToProject(Q,Z);await ensureChatGPTLogin(Q,"ChatGPT project file upload requires a logged-in ChatGPT session.");if(!await openProjectKnowledgeDialog(Q))return{ok:!1,reason:'could not find or click the project "Add files" button'};const j=z.map((B)=>W.default.basename(B));let J=!1;if(Q.setFileInput)for(const B of d)try{await Q.setFileInput(z,B);J=!0;break}catch(H){const D=String(H?.message||H);if(!D.includes("Unknown action")&&!D.includes("not supported")&&!D.includes("Not allowed")&&!D.includes("File access not granted")&&!D.includes("No element found"))throw H}if(!J){const B=z.map((D)=>({name:W.default.basename(D),mime:i(D),base64:$.default.readFileSync(D).toString("base64")})),H=requireObjectEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`
923
923
  (() => {
924
924
  const files = ${JSON.stringify(B)};
925
925
 
@@ -995,7 +995,7 @@ import{htmlToMarkdown as i}from"@jackwener/opencli/utils";import{ArgumentError a
995
995
  if (!missing.length) return { ok: true };
996
996
  return { ok: false, pending: true, reason: 'uploaded file did not appear in project knowledge: ' + missing.join(', ') };
997
997
  })()
998
- `)),"chatgpt project upload confirmation");if(W.ok===!0)return{ok:!0};Z=String(W.reason||Z);if(!W.pending)return{ok:!1,reason:Z};await Q.wait(0.5)}return{ok:!1,reason:Z}}export async function prepareChatGPTFilePaths(Q){const X=await import("node:fs"),Y=await import("node:path"),Z=Q.map(($)=>Y.default.resolve($));for(const $ of Z){if(!X.default.existsSync($))return{ok:!1,reason:`File not found: ${$}`};const W=X.default.statSync($);if(!W.isFile())return{ok:!1,reason:`Not a file: ${$}`};if(W.size>536870912)return{ok:!1,reason:`File too large (${(W.size/1024/1024).toFixed(1)} MB). Max: 512 MB`}}return{ok:!0,paths:Z}}function m(Q){const X=String(Q||"").toLowerCase();if(X.endsWith(".pdf"))return"application/pdf";if(X.endsWith(".doc"))return"application/msword";if(X.endsWith(".docx"))return"application/vnd.openxmlformats-officedocument.wordprocessingml.document";if(X.endsWith(".xls"))return"application/vnd.ms-excel";if(X.endsWith(".xlsx"))return"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";if(X.endsWith(".ppt"))return"application/vnd.ms-powerpoint";if(X.endsWith(".pptx"))return"application/vnd.openxmlformats-officedocument.presentationml.presentation";if(X.endsWith(".csv"))return"text/csv";if(X.endsWith(".txt"))return"text/plain";if(X.endsWith(".json"))return"application/json";if(X.endsWith(".xml"))return"application/xml";if(X.endsWith(".html")||X.endsWith(".htm"))return"text/html";if(X.endsWith(".md"))return"text/markdown";if(X.endsWith(".py"))return"text/x-python";if(X.endsWith(".js"))return"text/javascript";if(X.endsWith(".ts"))return"application/typescript";if(X.endsWith(".jsx"))return"text/jsx";if(X.endsWith(".tsx"))return"text/tsx";if(X.endsWith(".png"))return"image/png";if(X.endsWith(".jpg")||X.endsWith(".jpeg"))return"image/jpeg";if(X.endsWith(".gif"))return"image/gif";if(X.endsWith(".webp"))return"image/webp";if(X.endsWith(".svg"))return"image/svg+xml";return"application/octet-stream"}export const __test__={COMPOSER_SELECTORS:x,SEND_BUTTON_SELECTOR:U,SEND_BUTTON_FALLBACK_SELECTORS:L,SEND_BUTTON_LABELS:y,CLOSE_SIDEBAR_LABELS:A,buildComposerLocatorScript:v,isSameChatGPTConversation:N,parseChatGPTConversationId,parseChatGPTProjectId,imageMimeFromPath:P,mimeFromFilePath:m,PROJECT_LINK_SELECTOR:c};export async function getChatGPTImageAssets(Q,X){const Y=JSON.stringify(X);return requireArrayEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`
998
+ `)),"chatgpt project upload confirmation");if(W.ok===!0)return{ok:!0};Z=String(W.reason||Z);if(!W.pending)return{ok:!1,reason:Z};await Q.wait(0.5)}return{ok:!1,reason:Z}}export async function prepareChatGPTFilePaths(Q){const X=await import("node:fs"),Y=await import("node:path"),Z=Q.map(($)=>Y.default.resolve($));for(const $ of Z){if(!X.default.existsSync($))return{ok:!1,reason:`File not found: ${$}`};const W=X.default.statSync($);if(!W.isFile())return{ok:!1,reason:`Not a file: ${$}`};if(W.size>536870912)return{ok:!1,reason:`File too large (${(W.size/1024/1024).toFixed(1)} MB). Max: 512 MB`}}return{ok:!0,paths:Z}}function i(Q){const X=String(Q||"").toLowerCase();if(X.endsWith(".pdf"))return"application/pdf";if(X.endsWith(".doc"))return"application/msword";if(X.endsWith(".docx"))return"application/vnd.openxmlformats-officedocument.wordprocessingml.document";if(X.endsWith(".xls"))return"application/vnd.ms-excel";if(X.endsWith(".xlsx"))return"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";if(X.endsWith(".ppt"))return"application/vnd.ms-powerpoint";if(X.endsWith(".pptx"))return"application/vnd.openxmlformats-officedocument.presentationml.presentation";if(X.endsWith(".csv"))return"text/csv";if(X.endsWith(".txt"))return"text/plain";if(X.endsWith(".json"))return"application/json";if(X.endsWith(".xml"))return"application/xml";if(X.endsWith(".html")||X.endsWith(".htm"))return"text/html";if(X.endsWith(".md"))return"text/markdown";if(X.endsWith(".py"))return"text/x-python";if(X.endsWith(".js"))return"text/javascript";if(X.endsWith(".ts"))return"application/typescript";if(X.endsWith(".jsx"))return"text/jsx";if(X.endsWith(".tsx"))return"text/tsx";if(X.endsWith(".png"))return"image/png";if(X.endsWith(".jpg")||X.endsWith(".jpeg"))return"image/jpeg";if(X.endsWith(".gif"))return"image/gif";if(X.endsWith(".webp"))return"image/webp";if(X.endsWith(".svg"))return"image/svg+xml";return"application/octet-stream"}export const __test__={COMPOSER_SELECTORS:x,SEND_BUTTON_SELECTOR:U,SEND_BUTTON_FALLBACK_SELECTORS:L,SEND_BUTTON_LABELS:y,CLOSE_SIDEBAR_LABELS:A,buildComposerLocatorScript:v,isSameChatGPTConversation:N,parseChatGPTConversationId,parseChatGPTProjectId,imageMimeFromPath:P,mimeFromFilePath:i,PROJECT_LINK_SELECTOR:c};export async function getChatGPTImageAssets(Q,X){const Y=JSON.stringify(X);return requireArrayEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`
999
999
  (async (targetUrls) => {
1000
1000
  const blobToDataUrl = (blob) => new Promise((resolve, reject) => {
1001
1001
  const reader = new FileReader();
@@ -156,7 +156,7 @@ import{ArgumentError as Y,AuthRequiredError as F,CommandExecutionError as W}from
156
156
  }
157
157
  inp.dispatchEvent(new Event('change', { bubbles: true }));
158
158
  return { ok: true, via: 'native' };
159
- })()`);if(!j?.ok)return j;Z=!0}catch(j){const q=String(j?.message||j);if(!q.includes("Unknown action")&&!q.includes("not supported")&&!q.includes("Not allowed"))throw j}if(!Z){const q=K.default.readFileSync(Q).toString("base64"),J=await z.evaluate(`(async () => {
159
+ })()`);if(!j?.ok)return j;Z=!0}catch(j){const q=String(j?.message||j);if(!q.includes("Unknown action")&&!q.includes("not supported")&&!q.includes("Not allowed")&&!q.includes("File access not granted"))throw j}if(!Z){const q=K.default.readFileSync(Q).toString("base64"),J=await z.evaluate(`(async () => {
160
160
  var binary = atob('${q}');
161
161
  var bytes = new Uint8Array(binary.length);
162
162
  for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
@@ -55,7 +55,7 @@ import{ArgumentError as F}from"@jackwener/opencli/errors";export const DEEPSEEK_
55
55
  return { ok: true, method: 'enter' };
56
56
  })()`)}export async function getBubbleCount(q){return await q.evaluate(`(() => {
57
57
  return document.querySelectorAll('${MESSAGE_SELECTOR}').length;
58
- })()`)||0}export function parseThinkingResponse(q){if(!q)return null;const z=q.match(/^(Thought for ([\d.]+) seconds?|已思考(用时 ([\d.]+) 秒))\s*/);if(!z)return{response:q,thinking:null,thinking_time:null};const B=z[2]||z[3];return{response:"",thinking:q.slice(z[0].length).trim(),thinking_time:B}}export async function waitForResponse(q,z,B,Q,Z=!1){const Y=Date.now();let $="",J=0;while(Date.now()-Y<Q){await q.wait(3);let W;try{W=await q.evaluate(`(() => {
58
+ })()`)||0}export function parseThinkingResponse(q){if(!q)return null;const z=q.match(/^(Thought for ([\d.]+) seconds?|已思考(用时 ([\d.]+) 秒))\s*/);if(!z)return{response:q,thinking:null,thinking_time:null};const B=z[2]||z[3];return{response:"",thinking:q.slice(z[0].length).trim(),thinking_time:B}}export async function waitForResponse(q,z,B,Q,Z=!1){const Y=Date.now();let $="",K=0;while(Date.now()-Y<Q){await q.wait(3);let W;try{W=await q.evaluate(`(() => {
59
59
  const bubbles = document.querySelectorAll('${MESSAGE_SELECTOR}');
60
60
  const texts = Array.from(bubbles).map(b => (b.innerText || '').trim()).filter(Boolean);
61
61
  var last = texts[texts.length - 1] || '';
@@ -103,7 +103,7 @@ import{ArgumentError as F}from"@jackwener/opencli/errors";export const DEEPSEEK_
103
103
  answerText: answerEl ? (answerEl.innerText || '').trim() : null,
104
104
  thinkTime: thinkTime,
105
105
  };
106
- })()`)}catch{continue}if(!W)continue;const j=W.last;if(j&&W.count>z&&j!==B.trim()){if(j===$){J++;if(J>=3){if(Z){if(W.thinkText!=null||W.answerText!=null)return{thinking:W.thinkText||"",response:W.answerText||"",thinking_time:W.thinkTime||null};return parseThinkingResponse(j)}return j}}else J=0;$=j}}if(Z&&$)return parseThinkingResponse($);return $||null}export async function getVisibleMessages(q){const z=await q.evaluate(`(() => {
106
+ })()`)}catch{continue}if(!W)continue;const j=W.last;if(j&&W.count>z&&j!==B.trim()){if(j===$){K++;if(K>=3){if(Z){if(W.thinkText!=null||W.answerText!=null)return{thinking:W.thinkText||"",response:W.answerText||"",thinking_time:W.thinkTime||null};return parseThinkingResponse(j)}return j}}else K=0;$=j}}if(Z&&$)return parseThinkingResponse($);return $||null}export async function getVisibleMessages(q){const z=await q.evaluate(`(() => {
107
107
  const msgs = document.querySelectorAll('${MESSAGE_SELECTOR}');
108
108
  return Array.from(msgs).map(m => {
109
109
  // User messages carry an extra hash-class alongside ds-message
@@ -164,17 +164,17 @@ import{ArgumentError as F}from"@jackwener/opencli/errors";export const DEEPSEEK_
164
164
  while (c && !c.querySelector('div[role="button"]')) c = c.parentElement;
165
165
  if (!c) return false;
166
166
  return !!c.querySelector('img[src], canvas, video, [style*="background-image"], [class*="preview"], [class*="upload"]');
167
- })()`))return!0}return!1}export async function sendWithFile(q,z,B){const Q=await import("node:fs"),Z=await import("node:path"),Y=Z.default.resolve(z);if(!Q.default.existsSync(Y))return{ok:!1,reason:`File not found: ${Y}`};const $=Q.default.statSync(Y);if($.size>104857600)return{ok:!1,reason:`File too large (${($.size/1024/1024).toFixed(1)} MB). Max: 100 MB`};const J=Z.default.basename(Y);await q.evaluate(`(() => {
167
+ })()`))return!0}return!1}export async function sendWithFile(q,z,B){const Q=await import("node:fs"),Z=await import("node:path"),Y=Z.default.resolve(z);if(!Q.default.existsSync(Y))return{ok:!1,reason:`File not found: ${Y}`};const $=Q.default.statSync(Y);if($.size>104857600)return{ok:!1,reason:`File too large (${($.size/1024/1024).toFixed(1)} MB). Max: 100 MB`};const K=Z.default.basename(Y);await q.evaluate(`(() => {
168
168
  if (document.querySelectorAll('a[href*="/a/chat/s/"]').length > 0) {
169
169
  const btn = document.querySelector('div[tabindex="0"][role="button"]');
170
170
  if (btn) btn.click();
171
171
  }
172
- })()`);await q.wait(0.5);let W=!1;if(q.setFileInput)try{await q.setFileInput([Y],'input[type="file"]');W=!0}catch(y){const K=String(y?.message||y);if(!K.includes("Unknown action")&&!K.includes("not supported")&&!K.includes("Not allowed"))throw y}if(!W){const K=Q.default.readFileSync(Y).toString("base64"),U=await q.evaluate(`(async () => {
173
- var binary = atob('${K}');
172
+ })()`);await q.wait(0.5);let W=!1;if(q.setFileInput)try{await q.setFileInput([Y],'input[type="file"]');W=!0}catch(y){const J=String(y?.message||y);if(!J.includes("Unknown action")&&!J.includes("not supported")&&!J.includes("Not allowed")&&!J.includes("File access not granted"))throw y}if(!W){const J=Q.default.readFileSync(Y).toString("base64"),U=await q.evaluate(`(async () => {
173
+ var binary = atob('${J}');
174
174
  var bytes = new Uint8Array(binary.length);
175
175
  for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
176
176
 
177
- var file = new File([bytes], ${JSON.stringify(J)});
177
+ var file = new File([bytes], ${JSON.stringify(K)});
178
178
  var dt = new DataTransfer();
179
179
  dt.items.add(file);
180
180
 
@@ -190,7 +190,7 @@ import{ArgumentError as F}from"@jackwener/opencli/errors";export const DEEPSEEK_
190
190
  // Use inp.files, not dt.files; assignment transfers ownership
191
191
  inp[propsKey].onChange({ target: { files: inp.files } });
192
192
  return { ok: true };
193
- })()`);if(U&&!U.ok)return U}if(!await G(q,J))return{ok:!1,reason:"file preview did not appear"};let V=!1;for(let y=0;y<15;y++){if(await q.evaluate(`(() => {
193
+ })()`);if(U&&!U.ok)return U}if(!await G(q,K))return{ok:!1,reason:"file preview did not appear"};let V=!1;for(let y=0;y<15;y++){if(await q.evaluate(`(() => {
194
194
  var box = document.querySelector('${TEXTAREA_SELECTOR}');
195
195
  if (!box) return false;
196
196
  var c = box.parentElement;
@@ -1,11 +1,11 @@
1
- import*as Y from"node:fs";import*as J from"node:path";import{cli as V,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentError as N,CommandExecutionError as $}from"@jackwener/opencli/errors";const W={public:"公开",friends:"好友可见",private:"仅自己可见"},F="https://creator.douyin.com/creator-micro/content/upload",M=120,D=20,R=20,O=200;async function U(q){await q.evaluate(`() => {
1
+ import*as W from"node:fs";import*as J from"node:path";import{cli as M,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentError as N,CommandExecutionError as $}from"@jackwener/opencli/errors";import{setInputFiles as U}from"../_shared/file-inject.js";const j={public:"公开",friends:"好友可见",private:"仅自己可见"},R="https://creator.douyin.com/creator-micro/content/upload",D=120,F=20,O=20,y=200;async function B(q){await q.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 y(q){let G={href:"",ready:!1,bodyText:""};for(let z=0;z<M;z+=1){G=await q.evaluate(`() => ({
8
+ }`)}async function b(q){let G={href:"",ready:!1,bodyText:""};for(let z=0;z<D;z+=1){G=await q.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 Y from"node:fs";import*as J from"node:path";import{cli as V,Strategy a
13
13
  (el) => (el.textContent || '').includes('暂存离开')
14
14
  ),
15
15
  bodyText: document.body?.innerText || ''
16
- })`);if(G.ready)return;await q.wait({time:0.5})}throw new $("等待抖音草稿编辑页超时",`当前页面: ${G.href||"unknown"}`)}async function b(q,G){for(let z=0;z<O;z+=1){const H=await q.evaluate(`() => {
16
+ })`);if(G.ready)return;await q.wait({time:0.5})}throw new $("等待抖音草稿编辑页超时",`当前页面: ${G.href||"unknown"}`)}async function x(q,G){for(let z=0;z<y;z+=1){const H=await q.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(
@@ -71,7 +71,7 @@ import*as Y from"node:fs";import*as J from"node:path";import{cli as V,Strategy a
71
71
  if (!(visibility instanceof HTMLElement)) return false;
72
72
  visibility.click();
73
73
  return true;
74
- }`);if(Q)break;await q.wait({time:0.5})}if(!Q)throw new $("填写抖音草稿表单失败: visibility-missing")}async function x(q){let G="cover-input-missing";const z=await q.evaluate(`() => Array.from(document.querySelectorAll('input[type="file"]')).length`);for(let H=0;H<D;H+=1){const Q=await q.evaluate(`() => {
74
+ }`);if(Q)break;await q.wait({time:0.5})}if(!Q)throw new $("填写抖音草稿表单失败: visibility-missing")}async function h(q){let G="cover-input-missing";const z=await q.evaluate(`() => Array.from(document.querySelectorAll('input[type="file"]')).length`);for(let H=0;H<F;H+=1){const Q=await q.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 Y from"node:fs";import*as J from"node:path";import{cli as V,Strategy a
110
110
  root = root.parentElement;
111
111
  }
112
112
  return '';
113
- }`}async function h(q){return await q.evaluate(buildCoverCheckPanelTextJs())||""}async function L(q){let G="",z=!1;for(let H=0;H<R;H+=1){const Q=await h(q),X=Q.includes("检测中"),Z=Q.includes("重新检测")||Q.includes("横/竖双封面缺失");if(X)z=!0;if(z&&Z&&!X)return;G=Q;await q.wait({time:0.5})}throw new $("等待抖音封面处理完成超时",G||"unknown")}async function I(q){const G=60;let z=null;for(let H=0;H<G;H+=1){z=await q.evaluate(`() => {
113
+ }`}async function L(q){return await q.evaluate(buildCoverCheckPanelTextJs())||""}async function k(q){let G="",z=!1;for(let H=0;H<O;H+=1){const Q=await L(q),X=Q.includes("检测中"),Z=Q.includes("重新检测")||Q.includes("横/竖双封面缺失");if(X)z=!0;if(z&&Z&&!X)return;G=Q;await q.wait({time:0.5})}throw new $("等待抖音封面处理完成超时",G||"unknown")}async function S(q){const G=60;let z=null;for(let H=0;H<G;H+=1){z=await q.evaluate(`() => {
114
114
  const extractCreationId = () => {
115
115
  const titleInput = Array.from(document.querySelectorAll('input')).find(
116
116
  (el) => (el.placeholder || '').includes('填写作品标题')
@@ -163,4 +163,4 @@ import*as Y from"node:fs";import*as J from"node:path";import{cli as V,Strategy a
163
163
  }`);if(z?.ok)break;await q.wait({time:0.5})}if(!z?.ok)throw new $(`点击草稿按钮失败: ${z?.reason||"unknown"}`);if(!z.creationId)throw new $("点击草稿按钮失败: creation-id-missing");return{text:z.text||"暂存离开",creationId:z.creationId}}async function _(q,G){let z={href:"",bodyText:""};for(let H=0;H<40;H+=1){z=await q.evaluate(`() => ({
164
164
  href: location.href,
165
165
  bodyText: document.body?.innerText || ''
166
- })`);const Q=z.href||"",X=z.bodyText||"",Z=Q.includes("/creator-micro/content/upload")&&/继续编辑/.test(X),j=/草稿保存成功|已保存到草稿|存草稿成功|保存成功/.test(X),K=/\/creator-micro\/content\/(manage|drafts|works)/.test(Q)||/创作中心/.test(X)&&!Q.includes("/content/upload");if(Z||j||K)return G;await q.wait({time:1})}throw new $("未检测到抖音草稿保存确认",`当前页面: ${z.href||"unknown"}`)}V({site:"douyin",name:"draft",access:"write",description:"上传视频并保存为草稿",domain:"creator.douyin.com",strategy:f.COOKIE,navigateBefore:!1,defaultWindowMode:"foreground",args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"视频标题(≤30字)"},{name:"caption",default:"",help:"正文内容(≤1000字,支持 #话题)"},{name:"cover",default:"",help:"封面图片路径"},{name:"visibility",default:"public",choices:["public","friends","private"]},{name:"timeout",type:"int",default:180,help:"命令超时(秒),视频上传/转码慢时可调大"}],columns:["status","draft_id"],func:async(q,G)=>{const z=J.resolve(G.video);if(!Y.existsSync(z))throw new N(`视频文件不存在: ${z}`);const H=J.extname(z).toLowerCase();if(![".mp4",".mov",".avi",".webm"].includes(H))throw new N(`不支持的视频格式: ${H}(支持 mp4/mov/avi/webm)`);const Q=G.title;if(Q.length>30)throw new N("标题不能超过 30 字");const X=G.caption||"";if(X.length>1000)throw new N("正文不能超过 1000 字");const Z=G.cover;if(Z){if(!Y.existsSync(J.resolve(Z)))throw new N(`封面文件不存在: ${J.resolve(Z)}`)}if(!q.setFileInput)throw new $("当前浏览器适配器不支持文件注入","请使用 Browser Bridge 或支持 setFileInput 的浏览器模式");const j=W[G.visibility]??W.public;await q.goto(F);await q.wait({selector:'input[type="file"]',timeout:20});await U(q);await q.setFileInput([z],'input[type="file"]');await y(q);await b(q,j);await U(q);if(Z){const B=await x(q);await q.setFileInput([J.resolve(Z)],B);await L(q)}await w(q,{title:Q,caption:X,visibilityLabel:j});await q.wait({time:1});const K=await I(q);return[{status:"✅ 草稿已保存,可在创作中心继续编辑",draft_id:await _(q,K.creationId)}]}});
166
+ })`);const Q=z.href||"",X=z.bodyText||"",Z=Q.includes("/creator-micro/content/upload")&&/继续编辑/.test(X),K=/草稿保存成功|已保存到草稿|存草稿成功|保存成功/.test(X),Y=/\/creator-micro\/content\/(manage|drafts|works)/.test(Q)||/创作中心/.test(X)&&!Q.includes("/content/upload");if(Z||K||Y)return G;await q.wait({time:1})}throw new $("未检测到抖音草稿保存确认",`当前页面: ${z.href||"unknown"}`)}M({site:"douyin",name:"draft",access:"write",description:"上传视频并保存为草稿",domain:"creator.douyin.com",strategy:f.COOKIE,navigateBefore:!1,defaultWindowMode:"foreground",args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"视频标题(≤30字)"},{name:"caption",default:"",help:"正文内容(≤1000字,支持 #话题)"},{name:"cover",default:"",help:"封面图片路径"},{name:"visibility",default:"public",choices:["public","friends","private"]},{name:"timeout",type:"int",default:180,help:"命令超时(秒),视频上传/转码慢时可调大"}],columns:["status","draft_id"],func:async(q,G)=>{const z=J.resolve(G.video);if(!W.existsSync(z))throw new N(`视频文件不存在: ${z}`);const H=J.extname(z).toLowerCase();if(![".mp4",".mov",".avi",".webm"].includes(H))throw new N(`不支持的视频格式: ${H}(支持 mp4/mov/avi/webm)`);const Q=G.title;if(Q.length>30)throw new N("标题不能超过 30 字");const X=G.caption||"";if(X.length>1000)throw new N("正文不能超过 1000 字");const Z=G.cover;if(Z){if(!W.existsSync(J.resolve(Z)))throw new N(`封面文件不存在: ${J.resolve(Z)}`)}const K=j[G.visibility]??j.public;await q.goto(R);await q.wait({selector:'input[type="file"]',timeout:20});await B(q);await U(q,'input[type="file"]',[z]);await b(q);await x(q,K);await B(q);if(Z){const V=await h(q);await U(q,V,[J.resolve(Z)]);await k(q)}await w(q,{title:Q,caption:X,visibilityLabel:K});await q.wait({time:1});const Y=await S(q);return[{status:"✅ 草稿已保存,可在创作中心继续编辑",draft_id:await _(q,Y.creationId)}]}});
@@ -1,39 +1,18 @@
1
- import*as R from"node:fs";import*as G from"node:path";import{cli as P,Strategy as h}from"@jackwener/opencli/registry";import{ArgumentError as $,CommandExecutionError as j}from"@jackwener/opencli/errors";import{assertLiteralContent as T}from"../_shared/content-guard.js";import{browserFetch as c}from"./_shared/browser-fetch.js";const m="https://creator.douyin.com/creator-micro/content/upload?default-tab=3",w=35,S=20,M=1000,E=2500,A={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".bmp":"image/bmp",".webp":"image/webp",".tif":"image/tiff",".tiff":"image/tiff"},D='input[type="file"][accept*="image"]',C=['input[placeholder="添加作品标题"]','input[placeholder*="标题"]'],l='input[placeholder="添加作品标题"]',_='.zone-container[contenteditable="true"]';function n(q){return q.map((J)=>{const K=G.resolve(J);if(!R.existsSync(K))throw new $(`图片文件不存在: ${K}`);const V=G.extname(K).toLowerCase();if(!A[V])throw new $(`不支持的图片格式 "${V}",支持: jpg, png, bmp, webp, tif`);return K})}async function o(q,J=15000){const K=500,V=Math.max(1,Math.ceil(J/K));for(let z=0;z<V;z++){if(await q.evaluate(`(() => !!document.querySelector(${JSON.stringify(D)}))()`))return!0;if(z<V-1)await q.wait({time:K/1000})}return!1}async function i(q,J){if(!await o(q))return{ok:!1,error:"上传页未渲染出图片输入框(等待 15s 超时)"};if(q.setFileInput)try{await q.setFileInput(J,D);return{ok:!0}}catch(z){const Z=z instanceof Error?z.message:String(z);if(!(Z.includes("Unknown action")||Z.includes("not supported")||Z.includes("Not allowed")))return{ok:!1,error:Z}}const V=J.map((z)=>({name:G.basename(z),mimeType:A[G.extname(z).toLowerCase()],base64:R.readFileSync(z).toString("base64")}));return q.evaluate(`
2
- (async () => {
3
- const images = ${JSON.stringify(V)};
4
- const input = document.querySelector(${JSON.stringify(D)});
5
- if (!input) return { ok: false, error: '页面上找不到图片输入框' };
6
- const dt = new DataTransfer();
7
- for (const img of images) {
8
- try {
9
- const binary = atob(img.base64);
10
- const bytes = new Uint8Array(binary.length);
11
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
12
- dt.items.add(new File([new Blob([bytes], { type: img.mimeType })], img.name, { type: img.mimeType }));
13
- } catch (e) {
14
- return { ok: false, error: 'File 构造失败: ' + e.message };
15
- }
16
- }
17
- Object.defineProperty(input, 'files', { value: dt.files, writable: false });
18
- input.dispatchEvent(new Event('change', { bubbles: true }));
19
- input.dispatchEvent(new Event('input', { bubbles: true }));
20
- return { ok: true };
21
- })()
22
- `)}async function p(q,J=20000){const K=500,V=Math.ceil(J/K);for(let z=0;z<V;z++){if(await q.evaluate(`(() => {
1
+ import*as y from"node:fs";import*as N from"node:path";import{cli as h,Strategy as d}from"@jackwener/opencli/registry";import{ArgumentError as Z,CommandExecutionError as B}from"@jackwener/opencli/errors";import{assertLiteralContent as T}from"../_shared/content-guard.js";import{setInputFiles as u}from"../_shared/file-inject.js";import{browserFetch as m}from"./_shared/browser-fetch.js";const c="https://creator.douyin.com/creator-micro/content/upload?default-tab=3",x=35,L=20,w=1000,E=2500,l={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".bmp":"image/bmp",".webp":"image/webp",".tif":"image/tiff",".tiff":"image/tiff"},M='input[type="file"][accept*="image"]',A=['input[placeholder="添加作品标题"]','input[placeholder*="标题"]'],n='input[placeholder="添加作品标题"]',_='.zone-container[contenteditable="true"]';function o(q){return q.map((J)=>{const z=N.resolve(J);if(!y.existsSync(z))throw new Z(`图片文件不存在: ${z}`);const K=N.extname(z).toLowerCase();if(!l[K])throw new Z(`不支持的图片格式 "${K}",支持: jpg, png, bmp, webp, tif`);return z})}async function i(q,J=15000){const z=500,K=Math.max(1,Math.ceil(J/z));for(let Q=0;Q<K;Q++){if(await q.evaluate(`(() => !!document.querySelector(${JSON.stringify(M)}))()`))return!0;if(Q<K-1)await q.wait({time:z/1000})}return!1}async function p(q,J){if(!await i(q))return{ok:!1,error:"上传页未渲染出图片输入框(等待 15s 超时)"};try{await u(q,M,J);return{ok:!0}}catch(K){return{ok:!1,error:K instanceof Error?K.message:String(K)}}}async function s(q,J=20000){const z=500,K=Math.ceil(J/z);for(let Q=0;Q<K;Q++){if(await q.evaluate(`(() => {
23
2
  const onEditor = location.href.includes('/content/post/image');
24
- const titleReady = ${JSON.stringify(C)}.some((s) => !!document.querySelector(s));
3
+ const titleReady = ${JSON.stringify(A)}.some((s) => !!document.querySelector(s));
25
4
  return onEditor && titleReady;
26
- })()`))return!0;await q.wait({time:K/1000})}return!1}async function s(q,J){await q.typeText(l,J);await q.wait({time:0.3});const K=await q.evaluate(`
5
+ })()`))return!0;await q.wait({time:z/1000})}return!1}async function g(q,J){await q.typeText(n,J);await q.wait({time:0.3});const z=await q.evaluate(`
27
6
  ((selectors) => {
28
7
  const el = selectors.map((s) => document.querySelector(s)).find(Boolean);
29
8
  return el ? (el.value || '') : '';
30
- })(${JSON.stringify(C)})
31
- `);if(K.trim()!==J){await q.screenshot({path:"/tmp/douyin_image_title_debug.png"});throw new j(`填写标题失败,期望 "${J}",实际 "${K}"。截图: /tmp/douyin_image_title_debug.png`)}}async function g(q,J){await q.typeText(_,J);await q.wait({time:0.5});const K=await q.evaluate(`
9
+ })(${JSON.stringify(A)})
10
+ `);if(z.trim()!==J){await q.screenshot({path:"/tmp/douyin_image_title_debug.png"});throw new B(`填写标题失败,期望 "${J}",实际 "${z}"。截图: /tmp/douyin_image_title_debug.png`)}}async function r(q,J){await q.typeText(_,J);await q.wait({time:0.5});const z=await q.evaluate(`
32
11
  ((selector) => {
33
12
  const el = Array.from(document.querySelectorAll(selector)).find((n) => n && n.offsetParent !== null);
34
13
  return el ? (el.innerText || el.textContent || '').replace(/\\u200b/g, '').trim() : '';
35
14
  })(${JSON.stringify(_)})
36
- `),V=J.split("#")[0].replace(/\s+/g,"").trim(),z=K.replace(/\s+/g,"");if(V&&!z.includes(V.slice(0,Math.min(V.length,20)))){await q.screenshot({path:"/tmp/douyin_image_body_debug.png"});throw new j(`正文写入校验失败,实际读到 "${K.slice(0,80)}"。截图: /tmp/douyin_image_body_debug.png`)}}async function r(q,J){const K=J?["暂存离开"]:["发布"];return await q.evaluate(`
15
+ `),K=J.split("#")[0].replace(/\s+/g,"").trim(),Q=z.replace(/\s+/g,"");if(K&&!Q.includes(K.slice(0,Math.min(K.length,20)))){await q.screenshot({path:"/tmp/douyin_image_body_debug.png"});throw new B(`正文写入校验失败,实际读到 "${z.slice(0,80)}"。截图: /tmp/douyin_image_body_debug.png`)}}async function t(q,J){const z=J?["暂存离开"]:["发布"];return await q.evaluate(`
37
16
  ((labels) => {
38
17
  const isVisible = (el) => {
39
18
  if (!el || el.offsetParent === null) return false;
@@ -48,8 +27,8 @@ import*as R from"node:fs";import*as G from"node:path";import{cli as P,Strategy a
48
27
  if (btn) { btn.click(); return { ok: true, text: label }; }
49
28
  }
50
29
  return { ok: false, seen: btns.filter(isVisible).map((b) => norm(b.innerText || b.textContent)).filter(Boolean).slice(0, 12) };
51
- })(${JSON.stringify(K)})
52
- `)}P({site:"douyin",name:"publish-image",access:"write",description:"发布图文(图集)到抖音(UI 自动化,最多 35 张图;话题以 #词 写进正文)",domain:"creator.douyin.com",strategy:h.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",args:[{name:"content",required:!1,positional:!0,help:"正文(字面文本,不展开 @文件 引用;长正文用 --file)"},{name:"file",required:!1,help:"从本机文件读取正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"作品标题(≤20字)"},{name:"images",required:!0,help:"图片路径,逗号分隔,最多35张 (jpg/png/bmp/webp/tif)"},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"暂存为草稿,不直接发布"}],columns:["status","detail"],func:async(q,J)=>{if(!q)throw Error("Browser page required");const K=String(J.title??"").trim(),V=J.file?String(J.file).trim():"";let z=String(J.content??"").trim();if(V){if(z)throw new $("正文位置参数和 --file 只能二选一");const Q=G.resolve(V);if(!R.existsSync(Q))throw new $(`--file 指向的文件不存在: ${Q}`);z=R.readFileSync(Q,"utf-8").trim();if(!z)throw new $(`--file 指向的文件内容为空: ${Q}`)}const Z=String(J.images??"").split(",").map((Q)=>Q.trim()).filter(Boolean),F=J.topics?String(J.topics).split(",").map((Q)=>Q.trim().replace(/^#/,"")).filter(Boolean):[],Y=Boolean(J.draft);if(!K)throw new $("--title 必填");if(K.length>S)throw new $(`标题 ${K.length} 字,需 ≤ ${S} 字`);if(!z)throw new $("缺少正文:给位置参数 <content>,或用 --file <本机文件路径>");T(z,{fileFlag:"--file"});if(z.length>M)throw new $(`正文 ${z.length} 字,需 ≤ ${M} 字`);if(Z.length===0)throw new $("至少提供 1 张图片(--images)");if(Z.length>w)throw new $(`图片过多: ${Z.length}(最多 ${w} 张)`);const O=n(Z),f=F.length?`${z} ${F.map((Q)=>`#${Q}`).join(" ")} `:z;await q.goto(m);let W="";for(let Q=0;Q<30;Q++){await q.wait({time:0.5});W=await q.evaluate("() => location.href");if(W.includes("creator.douyin.com/creator-micro/content"))break}if(!W.includes("creator.douyin.com")){await q.screenshot({path:"/tmp/douyin_image_redirect_debug.png"});throw new j(`被重定向离开创作者中心(当前 ${W})——登录态可能已过期。`+"用 ppcli douyin login 重新登录。截图: /tmp/douyin_image_redirect_debug.png")}const k=await i(q,O);if(!k.ok){await q.screenshot({path:"/tmp/douyin_image_upload_debug.png"});throw new j(`图片上传失败: ${k.error??"unknown"}。截图: /tmp/douyin_image_upload_debug.png`)}if(!await p(q)){await q.screenshot({path:"/tmp/douyin_image_form_debug.png"});throw new j("上传后编辑页未就绪(标题输入框未出现),页面结构可能变化。截图: /tmp/douyin_image_form_debug.png")}await q.wait({time:E/1000});await s(q,K);await q.wait({time:0.4});await g(q,f);await q.wait({time:0.6});const I=await r(q,Y);if(!I?.ok){await q.screenshot({path:"/tmp/douyin_image_submit_debug.png"});throw new j(`找不到「${Y?"暂存离开":"发布"}」按钮。可见按钮: ${(I?.seen||[]).join(" | ")||"无"}。截图: /tmp/douyin_image_submit_debug.png`)}const d=Y?["暂存成功","保存成功","存入草稿"]:["发布成功","提交成功"];let B=W,X=!1,v="";for(let Q=0;Q<24;Q++){await q.wait({time:0.5});const H=await q.evaluate(`
30
+ })(${JSON.stringify(z)})
31
+ `)}h({site:"douyin",name:"publish-image",access:"write",description:"发布图文(图集)到抖音(UI 自动化,最多 35 张图;话题以 #词 写进正文)",domain:"creator.douyin.com",strategy:d.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",args:[{name:"content",required:!1,positional:!0,help:"正文(字面文本,不展开 @文件 引用;长正文用 --file)"},{name:"file",required:!1,help:"从本机文件读取正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"作品标题(≤20字)"},{name:"images",required:!0,help:"图片路径,逗号分隔,最多35张 (jpg/png/bmp/webp/tif)"},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"暂存为草稿,不直接发布"}],columns:["status","detail"],func:async(q,J)=>{if(!q)throw Error("Browser page required");const z=String(J.title??"").trim(),K=J.file?String(J.file).trim():"";let Q=String(J.content??"").trim();if(K){if(Q)throw new Z("正文位置参数和 --file 只能二选一");const V=N.resolve(K);if(!y.existsSync(V))throw new Z(`--file 指向的文件不存在: ${V}`);Q=y.readFileSync(V,"utf-8").trim();if(!Q)throw new Z(`--file 指向的文件内容为空: ${V}`)}const G=String(J.images??"").split(",").map((V)=>V.trim()).filter(Boolean),j=J.topics?String(J.topics).split(",").map((V)=>V.trim().replace(/^#/,"")).filter(Boolean):[],$=Boolean(J.draft);if(!z)throw new Z("--title 必填");if(z.length>L)throw new Z(`标题 ${z.length} 字,需 ≤ ${L} 字`);if(!Q)throw new Z("缺少正文:给位置参数 <content>,或用 --file <本机文件路径>");T(Q,{fileFlag:"--file"});if(Q.length>w)throw new Z(`正文 ${Q.length} 字,需 ≤ ${w} 字`);if(G.length===0)throw new Z("至少提供 1 张图片(--images)");if(G.length>x)throw new Z(`图片过多: ${G.length}(最多 ${x} 张)`);const D=o(G),C=j.length?`${Q} ${j.map((V)=>`#${V}`).join(" ")} `:Q;await q.goto(c);let W="";for(let V=0;V<30;V++){await q.wait({time:0.5});W=await q.evaluate("() => location.href");if(W.includes("creator.douyin.com/creator-micro/content"))break}if(!W.includes("creator.douyin.com")){await q.screenshot({path:"/tmp/douyin_image_redirect_debug.png"});throw new B(`被重定向离开创作者中心(当前 ${W})——登录态可能已过期。`+"用 ppcli douyin login 重新登录。截图: /tmp/douyin_image_redirect_debug.png")}const O=await p(q,D);if(!O.ok){await q.screenshot({path:"/tmp/douyin_image_upload_debug.png"});throw new B(`图片上传失败: ${O.error??"unknown"}。截图: /tmp/douyin_image_upload_debug.png`)}if(!await s(q)){await q.screenshot({path:"/tmp/douyin_image_form_debug.png"});throw new B("上传后编辑页未就绪(标题输入框未出现),页面结构可能变化。截图: /tmp/douyin_image_form_debug.png")}await q.wait({time:E/1000});await g(q,z);await q.wait({time:0.4});await r(q,C);await q.wait({time:0.6});const I=await t(q,$);if(!I?.ok){await q.screenshot({path:"/tmp/douyin_image_submit_debug.png"});throw new B(`找不到「${$?"暂存离开":"发布"}」按钮。可见按钮: ${(I?.seen||[]).join(" | ")||"无"}。截图: /tmp/douyin_image_submit_debug.png`)}const P=$?["暂存成功","保存成功","存入草稿"]:["发布成功","提交成功"];let Y=W,X=!1,F="";for(let V=0;V<24;V++){await q.wait({time:0.5});const H=await q.evaluate(`
53
32
  ((markers) => {
54
33
  const url = location.href;
55
34
  let toast = '';
@@ -60,5 +39,5 @@ import*as R from"node:fs";import*as G from"node:path";import{cli as P,Strategy a
60
39
  if (text && text.length <= 40 && markers.some((m) => text.includes(m))) { toast = text; break; }
61
40
  }
62
41
  return { url, toast };
63
- })(${JSON.stringify(d)})
64
- `);B=H?.url??B;if(H?.toast){v=H.toast;break}if(B.includes("/content/manage")||Y&&B.includes("/content/draft")){X=!0;break}}let y=null;if(!X&&!v&&B.includes("creator.douyin.com"))try{const H=await c(q,"GET",Y?"https://creator.douyin.com/web/api/media/aweme/draft/?aid=1128":"https://creator.douyin.com/janus/douyin/creator/pc/work_list?page_size=20&page_num=1&status=0"),u=H.aweme_list??H.data?.work_list??[],U=z.replace(/\s+/g,"").slice(0,20),b=Date.now()/1000;y=u.find((N)=>{const x=String(N.desc??N.title??"").replace(/\s+/g,""),L=Number(N.create_time??0);return L>0&&b-L<900&&(x.includes(K.replace(/\s+/g,""))||U&&x.includes(U))})??null}catch{}if(!X&&!v&&!y){await q.screenshot({path:"/tmp/douyin_image_verify_debug.png"});throw new j(`${Y?"暂存":"发布"}结果无法确认:未跳转作品管理页、未见成功提示,`+`后台${Y?"草稿箱":"作品列表"}回查也没找到本次内容——大概率没发出去,可以安全重试。`+`当前 URL: ${B}。截图: /tmp/douyin_image_verify_debug.png`)}return[{status:`✅ ${Y?"暂存成功":"发布成功"}`,detail:[`"${K}"`,`${O.length}张图片`,F.length?`话题: ${F.map((Q)=>`#${Q}`).join(" ")}`:"",y?.aweme_id?`aweme_id: ${y.aweme_id}(后台回查坐实)`:"",X?B:v].filter(Boolean).join(" · ")}]}});
42
+ })(${JSON.stringify(P)})
43
+ `);Y=H?.url??Y;if(H?.toast){F=H.toast;break}if(Y.includes("/content/manage")||$&&Y.includes("/content/draft")){X=!0;break}}let R=null;if(!X&&!F&&Y.includes("creator.douyin.com"))try{const H=await m(q,"GET",$?"https://creator.douyin.com/web/api/media/aweme/draft/?aid=1128":"https://creator.douyin.com/janus/douyin/creator/pc/work_list?page_size=20&page_num=1&status=0"),b=H.aweme_list??H.data?.work_list??[],U=Q.replace(/\s+/g,"").slice(0,20),f=Date.now()/1000;R=b.find((v)=>{const S=String(v.desc??v.title??"").replace(/\s+/g,""),k=Number(v.create_time??0);return k>0&&f-k<900&&(S.includes(z.replace(/\s+/g,""))||U&&S.includes(U))})??null}catch{}if(!X&&!F&&!R){await q.screenshot({path:"/tmp/douyin_image_verify_debug.png"});throw new B(`${$?"暂存":"发布"}结果无法确认:未跳转作品管理页、未见成功提示,`+`后台${$?"草稿箱":"作品列表"}回查也没找到本次内容——大概率没发出去,可以安全重试。`+`当前 URL: ${Y}。截图: /tmp/douyin_image_verify_debug.png`)}return[{status:`✅ ${$?"暂存成功":"发布成功"}`,detail:[`"${z}"`,`${D.length}张图片`,j.length?`话题: ${j.map((V)=>`#${V}`).join(" ")}`:"",R?.aweme_id?`aweme_id: ${R.aweme_id}(后台回查坐实)`:"",X?Y:F].filter(Boolean).join(" · ")}]}});