publishport-opencli 1.0.35 → 1.0.36
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as g from"node:fs";import{CommandExecutionError as i}from"@jackwener/opencli/errors";import{computeAws4Headers as w}from"./vod-upload.js";import{crc32Hex as A}from"./tos-upload.js";const O="https://imagex.bytedanceapi.com/",P="jm8ajry58r",T="2018-08-01",b="2906";async function U(t,e){const o=await t.text();let s;try{s=JSON.parse(o)}catch{throw new i(`${e}失败,非 JSON 响应: HTTP ${t.status} ${o.slice(0,300)}`)}const n=s?.ResponseMetadata?.Error;if(!t.ok||n)throw new i(`${e}失败: HTTP ${t.status} ${JSON.stringify(n??s).slice(0,500)}`);return s}function I(t,e){const o=new URLSearchParams({Action:t,Version:T,ServiceId:P,app_id:b,user_id:e??""});return`${O}?${o.toString()}`}export async function imagexUpload(t,e){if(!g.existsSync(t))throw new i(`Cover image file not found: ${t}`,"Ensure the file path is correct and accessible.");const o=g.readFileSync(t),s=I("ApplyImageUpload",e.user_id),n=await fetch(s,{headers:w(s,e,{service:"imagex"}),signal:AbortSignal.timeout(30000)}),u=await U(n,"抖音封面申请上传地址"),r=u?.Result?.UploadAddress,c=r?.StoreInfos?.[0],p=r?.UploadHosts?.[0];if(!c?.StoreUri||!c?.Auth||!p||!r?.SessionKey)throw new i(`抖音封面申请上传地址响应缺少必要字段: ${JSON.stringify(u).slice(0,500)}`);const m=await fetch(`https://${p}/upload/v1/${c.StoreUri}`,{method:"POST",headers:{Authorization:c.Auth,"Content-CRC32":A(o),"Content-Type":"application/octet-stream","Content-Disposition":'attachment; filename="undefined"',"X-Storage-U":encodeURIComponent(e.user_id??"")},body:o,signal:AbortSignal.timeout(120000)}),f=await m.text();let d;try{d=JSON.parse(f)}catch{d=null}if(!m.ok||d?.code!==2000)throw new i(`抖音封面上传失败: HTTP ${m.status} ${f.slice(0,300)}`);const h=I("CommitImageUpload",e.user_id),y=JSON.stringify({SessionKey:r.SessionKey}),R=await fetch(h,{method:"POST",headers:w(h,e,{service:"imagex",method:"POST",body:y,headers:{"content-type":"application/json"}}),body:y,signal:AbortSignal.timeout(30000)}),a=await U(R,"抖音封面提交上传"),l=a?.Result?.Results?.[0];if(!l?.Uri||l.UriStatus!==2000)throw new i(`抖音封面提交上传未成功: ${JSON.stringify(a?.Result??a).slice(0,500)}`);const S=a?.Result?.PluginResult?.find((x)=>x?.ImageUri===l.Uri)??a?.Result?.PluginResult?.[0];return{uri:l.Uri,width:Number(S?.ImageWidth)||0,height:Number(S?.ImageHeight)||0}}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import*as g from"node:crypto";import{AuthRequiredError as
|
|
1
|
+
import*as g from"node:crypto";import{AuthRequiredError as x,CommandExecutionError as d}from"@jackwener/opencli/errors";import{unwrapEvaluateResult as v}from"./evaluate-result.js";const J="https://creator.douyin.com/web/api/media/upload/auth/v5/",_="https://vod.bytedanceapi.com/",A="aweme";function y(s,e){return g.createHmac("sha256",s).update(e,"utf8").digest()}function U(s){const e=g.createHash("sha256");if(Buffer.isBuffer(s)||s instanceof Uint8Array)e.update(s);else e.update(s??"","utf8");return e.digest("hex")}function P(){return new Date().toISOString().replace(/[-:]/g,"").replace(/\.\d+Z$/,"Z")}function V(s){return[...s.searchParams.entries()].sort(([e],[t])=>e<t?-1:e>t?1:0).map(([e,t])=>`${encodeURIComponent(e)}=${encodeURIComponent(t)}`).join("&")}export function computeAws4Headers(s,e,t={}){const o=new URL(s),i=P(),u=i.slice(0,8),a=t.method??"GET",h=t.body??"",r=U(h),c={...t.headers??{},host:o.host,"x-amz-content-sha256":r,"x-amz-date":i,"x-amz-security-token":e.session_token},n=Object.keys(c).sort(),S=n.map((w)=>`${w}:${String(c[w]).trim()}`).join(`
|
|
2
2
|
`)+`
|
|
3
|
-
`,
|
|
4
|
-
`),
|
|
5
|
-
`),
|
|
3
|
+
`,p=n.join(";"),O=[a,o.pathname||"/",V(o),S,p,r].join(`
|
|
4
|
+
`),l=t.service??"vod",m="cn-north-1",f=`${u}/${m}/${l}/aws4_request`,N=["AWS4-HMAC-SHA256",i,f,U(O)].join(`
|
|
5
|
+
`),H=y(`AWS4${e.secret_access_key}`,u),T=y(H,m),k=y(T,l),I=y(k,"aws4_request"),R=y(I,N).toString("hex");return{...c,Authorization:`AWS4-HMAC-SHA256 Credential=${e.access_key_id}/${f}, SignedHeaders=${p}, Signature=${R}`}}function b(s){try{const e=s.startsWith("STS2")?s.slice(4):s,t=JSON.parse(Buffer.from(e,"base64").toString("utf8")),i=JSON.parse(t.PolicyString||"{}")?.Statement?.[0]?.Condition;if(typeof i==="string")return JSON.parse(i).UserId||""}catch{return""}return""}export async function getUploadAuthV5Credentials(s){const e=v(await s.evaluate(`fetch(${JSON.stringify(J)}, { credentials: 'include' }).then(r => r.json())`));if(!e||Array.isArray(e)||typeof e!=="object")throw new d(`获取抖音上传授权失败: ${JSON.stringify(e)}`);if(e.status_code!==0){const o=e.status_msg??e.message??"unknown error";if(e.status_code===401||e.status_code===403||/login|cookie|auth|captcha|verify|forbidden|permission|登录|登陆|权限|验证|验证码/i.test(String(o)))throw new x("creator.douyin.com",`获取抖音上传授权失败: ${o}`);throw new d(`获取抖音上传授权失败: ${JSON.stringify(e)}`)}if(!e.auth)throw new d(`获取抖音上传授权失败: ${JSON.stringify(e)}`);let t;try{t=JSON.parse(e.auth)}catch(o){throw new d(`解析抖音上传授权失败: ${o instanceof Error?o.message:String(o)}`)}if(!t.AccessKeyID||!t.SecretAccessKey||!t.SessionToken)throw new d("抖音上传授权缺少 AccessKeyID/SecretAccessKey/SessionToken");return{access_key_id:t.AccessKeyID,secret_access_key:t.SecretAccessKey,session_token:t.SessionToken,user_id:b(t.SessionToken),expired_time:t.ExpiredTime,current_time:t.CurrentTime}}export async function applyVideoUploadInner(s,e){const t=new URLSearchParams({Action:"ApplyUploadInner",Version:"2020-11-19",SpaceName:A,FileType:"video",IsInner:"1",FileSize:String(s)}),o=`${_}?${t.toString()}`,i=await fetch(o,{headers:computeAws4Headers(o,e),signal:AbortSignal.timeout(30000)}),u=await i.text();let a;try{a=JSON.parse(u)}catch{throw new d(`申请抖音上传地址失败,非 JSON 响应: HTTP ${i.status} ${u.slice(0,300)}`)}const h=a?.ResponseMetadata?.Error;if(!i.ok||h)throw new d(`申请抖音上传地址失败: HTTP ${i.status} ${JSON.stringify(h??a)}`);const r=a?.Result?.InnerUploadAddress?.UploadNodes?.[0],c=r?.StoreInfos?.[0],n=a?.Result?.Vid||r?.Vid,S=r?.SessionKey??c?.SessionKey??a?.Result?.SessionKey;if(!r?.UploadHost||!c?.StoreUri||!c?.Auth||!n||!S)throw new d(`申请抖音上传地址响应缺少必要字段: ${JSON.stringify(a).slice(0,500)}`);return{video_id:n,tos_upload_url:`https://${r.UploadHost}/${c.StoreUri}`,auth:c.Auth,session_key:S,upload_header:r.UploadHeader??{},user_id:e.user_id??""}}export async function commitVideoUploadInner(s,e){if(!s?.session_key)throw new d("抖音上传提交缺少 SessionKey");const t=new URLSearchParams({Action:"CommitUploadInner",Version:"2020-11-19",SpaceName:A}),o=`${_}?${t.toString()}`,i=JSON.stringify({SessionKey:s.session_key}),u=computeAws4Headers(o,e,{method:"POST",body:i,headers:{"content-type":"application/json;charset=UTF-8"}}),a=await fetch(o,{method:"POST",headers:u,body:i,signal:AbortSignal.timeout(30000)}),h=await a.text();let r;try{r=JSON.parse(h)}catch{throw new d(`提交抖音上传失败,非 JSON 响应: HTTP ${a.status} ${h.slice(0,300)}`)}const c=r?.ResponseMetadata?.Error;if(!a.ok||c)throw new d(`提交抖音上传失败: HTTP ${a.status} ${JSON.stringify(c??r)}`);const n=r?.Result?.Results?.[0]??r?.Result??{},S=n.Vid??n.VideoId??n.VideoID??n.vid??s.video_id;if(!S)throw new d(`提交抖音上传响应缺少 video id: ${JSON.stringify(r).slice(0,500)}`);const p=n.Meta??n.VideoMeta??{};return{video_id:S,poster_uri:n.PosterUri??n.PosterURI??n.SnapshotUri??n.SnapshotURI??"",width:Number(p.Width??p.width??720)||720,height:Number(p.Height??p.height??1280)||1280,raw:n}}
|
package/clis/douyin/publish.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import{DOUYIN_DECLARATION_ARGS as
|
|
1
|
+
import{DOUYIN_DECLARATION_ARGS as G,douyinDeclarationInfo as K}from"./_shared/publish-settings.js";import*as T from"node:fs";import*as f from"node:path";import{cli as Z,Strategy as j}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as x}from"@jackwener/opencli/errors";import{getUploadAuthV5Credentials as Q,applyVideoUploadInner as X,commitVideoUploadInner as k}from"./_shared/vod-upload.js";import{tosUpload as ee}from"./_shared/tos-upload.js";import{imagexUpload as te}from"./_shared/imagex-upload.js";import{browserFetch as R}from"./_shared/browser-fetch.js";import{generateCreationId as ie}from"./_shared/creation-id.js";import{validateTiming as oe,toUnixSeconds as se}from"./_shared/timing.js";import{parseTextExtra as ne,extractHashtagNames as re}from"./_shared/text-extra.js";const ae={public:0,friends:2,private:1},ce="aid=1128&cookie_enabled=true&screen_width=1512&screen_height=982&browser_language=zh-CN&browser_platform=MacIntel&browser_name=Mozilla&browser_online=true&timezone_name=Asia%2FTokyo&support_h265=1",le=JSON.stringify({video_cover_source:2,cover_timestamp:0,recommend_timestamp:0,is_cover_edit:0,is_cover_template:0,cover_template_id:"",is_text_template:0,text_template_id:"",text_template_content:"",is_text:0,text_num:0,text_content:"",is_use_sticker:0,sticker_id:"",is_use_filter:0,filter_id:"",is_cover_modify:0,to_status:0,cover_type:0,initial_cover_uri:"",cut_coordinate:""});function de(i){const e=i instanceof Error?i.message:String(i);return e.includes("post_assistant/fast_detect")&&(e.includes("Empty response")||e.includes("404")||e.includes("Not Found")||e.includes("Timeout")||e.includes("timed out")||e.includes("Failed to fetch"))}function E(i){return new Promise((e)=>setTimeout(e,i))}async function V(i,e,I,r){let l;for(let a=1;a<=3;a+=1)try{return{ok:!0,value:await R(i,e,I,r)}}catch(o){if(!de(o))throw o;l=o;if(a<3)await E(500*a)}return{ok:!1,error:l}}Z({site:"douyin",name:"publish",access:"write",description:"发布视频到抖音(指定 --schedule 定时,或显式 --immediate 立即发布)",domain:"creator.douyin.com",strategy:j.COOKIE,defaultWindowMode:"foreground",args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"视频标题(≤30字)"},{name:"schedule",help:"定时发布时间(ISO8601 或 Unix 秒,2h ~ 14天后),与 --immediate 二选一"},{name:"immediate",type:"boolean",help:"立即发布(必须显式指定,不能与 --schedule 同用)"},{name:"caption",fromFile:!0,default:"",help:"正文内容(≤1000字,支持 #话题)"},{name:"cover",default:"",help:"封面图片路径(不提供时使用视频截帧)"},{name:"visibility",default:"public",choices:["public","friends","private"]},{name:"allow_download",type:"bool",default:!1,help:"允许下载"},{name:"collection",default:"",help:"合集 ID"},{name:"activity",default:"",help:"活动 ID"},{name:"poi_id",default:"",help:"地理位置 ID"},{name:"poi_name",default:"",help:"地理位置名称"},{name:"hotspot",default:"",help:"关联热点词"},{name:"no_safety_check",type:"bool",default:!1,help:"跳过内容安全检测"},{name:"sync_toutiao",type:"bool",default:!1,help:"同步发布到头条"},{name:"ai-generated",type:"boolean",help:"声明内容由 AI 生成(可选,随定时发布请求提交)"},...G],columns:["status","aweme_id","url","publish_time"],func:async(i,e)=>{const I=K(e);if(!!e.immediate===!!e.schedule)throw new c("必须指定 --schedule 或 --immediate,且只能选择一个");const r=f.resolve(e.video);if(!T.existsSync(r))throw new c(`视频文件不存在: ${r}`);const l=f.extname(r).toLowerCase();if(![".mp4",".mov",".avi",".webm"].includes(l))throw new c(`不支持的视频格式: ${l}(支持 mp4/mov/avi/webm)`);const a=T.statSync(r).size,o=e.title;if(o.length>30)throw new c("标题不能超过 30 字");const n=e.caption||"";if(n.length>1000)throw new c("正文不能超过 1000 字");const S=e.immediate?0:se(e.schedule);if(!e.immediate)oe(S);const z=ae[e.visibility]??0,O=e.cover;if(O){if(!T.existsSync(f.resolve(O)))throw new c(`封面文件不存在: ${f.resolve(O)}`)}const y=await Q(i),U=await X(a,y);let d="",m=720,p=1280;await ee({filePath:r,uploadInfo:U,credentials:y,onProgress:(t,s)=>{const u=Math.round(t/s*100);process.stderr.write(`\r 上传进度: ${u}%`)}});process.stderr.write(`
|
|
2
2
|
`);process.stderr.write(` 提交上传...
|
|
3
|
-
`);const
|
|
4
|
-
`);
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
body: ${JSON.stringify(d)}
|
|
10
|
-
}).then(r => r.json())
|
|
11
|
-
`,Q=M(await o.evaluate(Z),"抖音封面提交上传响应异常");j("抖音封面提交上传",Q);w=l}if(!e.no_safety_check){const t="https://creator.douyin.com/aweme/v1/post_assistant/fast_detect/pre_check",i={video_id:N,title:s,desc:n},a=await B(o,"POST",t,{body:i});if(!a.ok)process.stderr.write(` 内容安全预检接口无响应,继续轮询检测结果。
|
|
12
|
-
`);const y="https://creator.douyin.com/aweme/v1/post_assistant/fast_detect/poll",c=Date.now()+30000;let I=!1,l=0;while(Date.now()<c){const x=await B(o,"POST",y,{body:i});if(!x.ok){l+=1;if(!a.ok&&l>=3)break;await C(2000);continue}l=0;const d=x.value;if(d.status===0||d.has_done===!0&&d.detect_result?.reason_code===0&&(d.detect_list?.length??0)===0){I=!0;break}if(d.status===1)throw new u("内容安全检测不通过,请修改后重试","使用 --no_safety_check 跳过");await C(2000)}if(!I)if(!a.ok&&l>=3)process.stderr.write(` 内容安全预检持续无响应,跳过本地预检,交由 create_v2 后的平台审核。
|
|
13
|
-
`);else throw new u("内容安全检测超时(30s),请稍后重试","如确认要跳过本地预检,可使用 --no_safety_check;提交后仍会走抖音平台审核")}const q=pe(n),J=[];let P=0;for(const t of q){const i=n.indexOf(`#${t}`,P);if(i===-1)continue;J.push({name:t,id:0,start:i,end:i+t.length+1});P=i+t.length+1}const R=n?`${s} ${n}`:s,F=n?s.length+1:0,G=me(R,J.map((t)=>({...t,start:t.start+F,end:t.end+F}))),K={item:{common:{text:R,caption:n,item_title:s,activity:JSON.stringify(e.activity?[e.activity]:[]),text_extra:JSON.stringify(G),challenges:"[]",mentions:"[]",hashtag_source:"",hot_sentence:e.hotspot||"",interaction_stickers:"[]",visibility_type:H,download:e.allow_download?1:0,timing:U,creation_id:ce(),media_type:4,video_id:N,music_source:0,music_id:null,...e.poi_id?{poi_id:e.poi_id,poi_name:e.poi_name}:{}},cover:{poster:w,custom_cover_image_height:A,custom_cover_image_width:E,poster_delay:0,cover_tools_info:fe,cover_tools_extend_info:"{}"},mix:e.collection?{mix_id:e.collection,mix_order:0}:{},chapter:{chapter:JSON.stringify({chapter_abstract:"",chapter_details:[],chapter_type:0})},anchor:{},sync:{should_sync:!1,sync_to_toutiao:e.sync_toutiao?1:0},open_platform:{},assistant:{is_preview:0,is_post_assistant:1},declare:{user_declare_info:m}}},W=`https://creator.douyin.com/web/api/media/aweme/create_v2/?read_aid=2906&${ue}`;process.stderr.write(e.immediate?` 创建发布...
|
|
3
|
+
`);const _=await k(U,y),g=_.video_id;process.stderr.write(` 上传已提交: ${g}
|
|
4
|
+
`);m=_.width||m;p=_.height||p;if(!d&&_.poster_uri)d=_.poster_uri;if(e.cover){process.stderr.write(` 上传封面...
|
|
5
|
+
`);const t=await te(f.resolve(e.cover),y);d=t.uri;m=t.width||m;p=t.height||p;process.stderr.write(` 封面已上传: ${d}
|
|
6
|
+
`)}if(!e.no_safety_check){const t="https://creator.douyin.com/aweme/v1/post_assistant/fast_detect/pre_check",s={video_id:g,title:o,desc:n},u=await V(i,"POST",t,{body:s});if(!u.ok)process.stderr.write(` 内容安全预检接口无响应,继续轮询检测结果。
|
|
7
|
+
`);const W="https://creator.douyin.com/aweme/v1/post_assistant/fast_detect/poll",Y=Date.now()+30000;let F=!1,b=0;while(Date.now()<Y){const P=await V(i,"POST",W,{body:s});if(!P.ok){b+=1;if(!u.ok&&b>=3)break;await E(2000);continue}b=0;const h=P.value;if(h.status===0||h.has_done===!0&&h.detect_result?.reason_code===0&&(h.detect_list?.length??0)===0){F=!0;break}if(h.status===1)throw new x("内容安全检测不通过,请修改后重试","使用 --no_safety_check 跳过");await E(2000)}if(!F)if(!u.ok&&b>=3)process.stderr.write(` 内容安全预检持续无响应,跳过本地预检,交由 create_v2 后的平台审核。
|
|
8
|
+
`);else throw new x("内容安全检测超时(30s),请稍后重试","如确认要跳过本地预检,可使用 --no_safety_check;提交后仍会走抖音平台审核")}const L=re(n),A=[];let D=0;for(const t of L){const s=n.indexOf(`#${t}`,D);if(s===-1)continue;A.push({name:t,id:0,start:s,end:s+t.length+1});D=s+t.length+1}const N=n?`${o} ${n}`:o,C=n?o.length+1:0,M=ne(N,A.map((t)=>({...t,start:t.start+C,end:t.end+C}))),J={item:{common:{text:N,caption:n,item_title:o,activity:JSON.stringify(e.activity?[e.activity]:[]),text_extra:JSON.stringify(M),challenges:"[]",mentions:"[]",hashtag_source:"",hot_sentence:e.hotspot||"",interaction_stickers:"[]",visibility_type:z,download:e.allow_download?1:0,timing:S,creation_id:ie(),media_type:4,video_id:g,music_source:0,music_id:null,...e.poi_id?{poi_id:e.poi_id,poi_name:e.poi_name}:{}},cover:{poster:d,custom_cover_image_height:p,custom_cover_image_width:m,poster_delay:0,cover_tools_info:le,cover_tools_extend_info:"{}"},mix:e.collection?{mix_id:e.collection,mix_order:0}:{},chapter:{chapter:JSON.stringify({chapter_abstract:"",chapter_details:[],chapter_type:0})},anchor:{},sync:{should_sync:!1,sync_to_toutiao:e.sync_toutiao?1:0},open_platform:{},assistant:{is_preview:0,is_post_assistant:1},declare:{user_declare_info:I}}},B=`https://creator.douyin.com/web/api/media/aweme/create_v2/?read_aid=2906&${ce}`;process.stderr.write(e.immediate?` 创建发布...
|
|
14
9
|
`:` 创建定时发布...
|
|
15
|
-
`);let
|
|
16
|
-
`);return[{status:e.immediate?"✅ 发布请求已提交":"✅ 定时发布成功!",aweme_id:
|
|
10
|
+
`);let v;try{v=await R(i,"POST",B,{body:J})}catch(t){if((t instanceof Error?t.message:String(t)).includes("Failed to fetch"))throw new x("提交发布请求被浏览器侧拦截(疑似字节安全组件 secsdk)","不要直接重试:先用 `ppcli douyin videos` / 创作者中心确认作品是否已创建,避免重复投稿。"+"确认未创建后,用 `--window foreground` 前台窗口重试(视频已上传成功的部分无需重传)。");throw t}const w=v.aweme_id??v.item_id;if(!w)throw new x(`发布成功但未返回 aweme_id/item_id: ${JSON.stringify(v)}`);const q=`https://www.douyin.com/video/${w}`,H=e.immediate?"立即发布":new Date(S*1000).toLocaleString("zh-CN",{timeZone:"Asia/Shanghai"});if(!e.immediate)process.stderr.write(` 定时稿已创建,到点前作品链接不可访问。核对:ppcli douyin videos --status scheduled(应能看到 ${w} 及计划时间)。
|
|
11
|
+
`);return[{status:e.immediate?"✅ 发布请求已提交":"✅ 定时发布成功!",aweme_id:w,url:q,publish_time:H}]}});
|