publishport-opencli 1.0.28 → 1.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ import{ArgumentError as o,CommandExecutionError as l}from"@jackwener/opencli/errors";export async function control(t,n,i){const e=await t.evaluate(`(({selector,label}) => {
2
+ const all = [...document.querySelectorAll(selector)];
3
+ const hits = all.filter(e => e.getClientRects().length && getComputedStyle(e).visibility !== 'hidden'
4
+ && (label === undefined || (e.textContent || '').trim() === label));
5
+ if (hits.length !== 1) return {error: '期望唯一控件,找到 ' + hits.length};
6
+ const e = hits[0];
7
+ if (e.disabled || e.getAttribute('aria-disabled') === 'true'
8
+ || [...e.classList].some(c => c === 'disabled' || c.startsWith('disabled-')))
9
+ return {error:'控件不可用'};
10
+ return {nth:all.indexOf(e)};
11
+ })(${JSON.stringify({selector:n,label:i})})`);if(!e||e.error)throw new l(`发布设置 ${i||n}:${e?.error||"页面响应异常"};已停止提交`);return{selector:n,nth:e.nth}}export async function clickControl(t,n,i){const e=await control(t,n,i);await t.evaluate(`document.querySelectorAll(${JSON.stringify(n)})[${e.nth}].scrollIntoView({block:'center',inline:'center',behavior:'instant'})`);await t.click(e.selector,{nth:e.nth});await t.wait({time:0.2})}export async function verifyControl(t,n,i,e=5000){for(let r=0;r<Math.ceil(e/200);r++){if(await t.evaluate(`Boolean(${n})`))return;await t.wait({time:0.2})}throw new l(`发布设置 ${i} 未生效;已停止提交`)}export async function fillControl(t,n,i,e=!0){const r=await control(t,n);await t.fillText(n,i,{nth:r.nth});if(e)await t.pressKey("Tab");await verifyControl(t,`document.querySelectorAll(${JSON.stringify(n)})[${r.nth}]?.value === ${JSON.stringify(i)}`,n)}export function publicationTime(t,n,i=Date.now()){if(t===void 0)return;const e=String(t);if(!/^\d+$/.test(e)&&!/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}$/.test(e)&&!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/i.test(e))throw new o("--schedule 使用北京时间 YYYY-MM-DD HH:mm、带时区 ISO8601 或 Unix 秒");const r=/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}$/.test(e)?e.replace(" ","T")+":00+08:00":e,a=/^\d+$/.test(e)?Number(e)*1000:Date.parse(r);if(!Number.isFinite(a)||a<i+n*3600000||a>i+1209600000)throw new o(`--schedule 必须在 ${n} 小时至 14 天后(无时区格式 YYYY-MM-DD HH:mm 按北京时间)`);return new Date(a+28800000).toISOString().slice(0,16).replace("T"," ")}export function resolveDeclaration(t,n){const i=t.declaration??(t["ai-generated"]?"ai":void 0);if(i!==void 0&&!Object.hasOwn(n,i))throw new o(`不支持的 --declaration: ${i}`);if(t["ai-generated"]&&i!=="ai")throw new o("--ai-generated 与 --declaration 冲突");return i}
@@ -0,0 +1 @@
1
+ import{CommandExecutionError as f}from"@jackwener/opencli/errors";export function douyinAiDeclarationInfo(t){return JSON.stringify(t?{choose_value:"aigc"}:{})}export function douyinAiDeclarationStep(t,s="内容由AI生成"){const n=(e)=>e.getClientRects().length>0&&getComputedStyle(e).visibility!=="hidden",i=(e)=>(e.textContent||"").replace(/\s+/g,""),o=[...document.querySelectorAll('[class*="selectBox-"]')].filter(n),r=[...document.querySelectorAll(".semi-modal")].filter((e)=>n(e)&&i(e).includes("对作品内容添加声明"));if(r.length===0&&o.filter((e)=>i(e)===s).length===1)return{selected:!0};if(t==="verify")return{selected:!1};if(t==="open"){if(r.length===1)return{opened:!0};const e=o.filter((d)=>i(d)==="请选择自主声明");if(e.length!==1)return{error:"未找到唯一的自主声明控件,或已有其他声明"};return{selector:'[class*="selectBox-"]',nth:[...document.querySelectorAll('[class*="selectBox-"]')].indexOf(e[0])}}if(r.length!==1)return{error:"未找到唯一的自主声明弹窗"};const c=[...r[0].querySelectorAll("label.semi-radio")].filter((e)=>n(e)&&i(e)===s);if(c.length!==1||!c[0].querySelector('input[type="radio"]'))return{error:"未找到唯一的“内容由AI生成”选项"};const l=c[0].querySelector('input[type="radio"]');if(l.disabled)return{error:"AI 声明选项不可用"};const a=c[0].classList.contains("semi-radio-checked");if(t==="select"){if(a)return{checked:!0};return{selector:'.semi-modal label.semi-radio input[type="radio"]',nth:[...document.querySelectorAll('.semi-modal label.semi-radio input[type="radio"]')].indexOf(l)}}if(!a)return{error:"AI 声明选项未选中"};const u=[...r[0].querySelectorAll("button")].filter((e)=>n(e)&&i(e)==="确定"&&!e.disabled&&e.getAttribute("aria-disabled")!=="true");if(u.length!==1)return{error:"声明确认按钮不可用"};return{selector:".semi-modal button",nth:[...document.querySelectorAll(".semi-modal button")].indexOf(u[0])}}export async function setDouyinAiDeclaration(t,s="内容由AI生成"){const n=(l)=>t.evaluate(`(${douyinAiDeclarationStep.toString()})(${JSON.stringify(l)}, ${JSON.stringify(s)})`),i=(l)=>{throw new f(`抖音 AI 内容声明设置失败:${l};已停止提交`)},o=await n("open");if(o?.selected)return;if(!o?.opened&&!o?.selector)i(o?.error||"声明控件响应异常");if(o.selector)await t.click(o.selector,{nth:o.nth});await t.wait({time:0.2});const r=await n("select");if(!r?.checked&&!r?.selector)i(r?.error||"声明选项响应异常");if(r.selector)await t.click(r.selector,{nth:r.nth});await t.wait({time:0.2});const c=await n("confirm");if(!c?.selector)i(c?.error||"声明确认响应异常");await t.click(c.selector,{nth:c.nth});for(let l=0;l<10;l++){if((await n("verify"))?.selected)return;await t.wait({time:0.2})}i("确认后未回显“"+s+"”")}
@@ -1,17 +1,17 @@
1
- import{AuthRequiredError as u,CommandExecutionError as i}from"@jackwener/opencli/errors";import{unwrapEvaluateResult as y}from"./evaluate-result.js";function f(s,t){const e=String(t??"");return s===401||s===403||/login|cookie|auth|captcha|verify|forbidden|permission|登录|登陆|权限|验证|验证码/i.test(e)}export async function browserFetch(s,t,e,n={}){const c=`
1
+ import{AuthRequiredError as u,CommandExecutionError as i}from"@jackwener/opencli/errors";import{unwrapEvaluateResult as y}from"./evaluate-result.js";function f(n,t){const e=String(t??"");return n===401||n===403||/login|cookie|auth|captcha|verify|forbidden|permission|登录|登陆|权限|验证|验证码/i.test(e)}export async function browserFetch(n,t,e,s={}){const c=`
2
2
  (async () => {
3
3
  const controller = new AbortController();
4
- const timer = setTimeout(() => controller.abort(), ${Number(n.timeoutMs??30000)});
4
+ const timer = setTimeout(() => controller.abort(), ${Number(s.timeoutMs??30000)});
5
5
  try {
6
6
  const res = await fetch(${JSON.stringify(e)}, {
7
7
  method: ${JSON.stringify(t)},
8
- credentials: 'include',
8
+ credentials: ${JSON.stringify(s.credentials??"include")},
9
9
  signal: controller.signal,
10
10
  headers: {
11
11
  'Content-Type': 'application/json',
12
- ...${JSON.stringify(n.headers??{})}
12
+ ...${JSON.stringify(s.headers??{})}
13
13
  },
14
- ${n.body?`body: JSON.stringify(${JSON.stringify(n.body)}),`:""}
14
+ ${s.body?`body: JSON.stringify(${JSON.stringify(s.body)}),`:""}
15
15
  });
16
16
  const text = await res.text();
17
17
  try {
@@ -25,4 +25,4 @@ import{AuthRequiredError as u,CommandExecutionError as i}from"@jackwener/opencli
25
25
  clearTimeout(timer);
26
26
  }
27
27
  })()
28
- `;let r;try{r=y(await s.evaluate(c))}catch(o){throw new i(`Douyin API request failed (${t} ${e}): ${o instanceof Error?o.message:String(o)}`)}if(r==null)throw new i(`Empty response from Douyin API (${t} ${e})`);if(Array.isArray(r)||typeof r!=="object")throw new i(`Malformed response from Douyin API (${t} ${e})`);if(r&&typeof r==="object"&&"status_code"in r){const o=r.status_code;if(o!==0){const a=r.status_msg??r.message??"unknown error";if(f(o,a))throw new u("creator.douyin.com",`Douyin API auth/permission error ${o} at ${t} ${e}: ${a}`);throw new i(`Douyin API error ${o} at ${t} ${e}: ${a}`)}}return r}
28
+ `;let r;try{r=y(await n.evaluate(c))}catch(o){throw new i(`Douyin API request failed (${t} ${e}): ${o instanceof Error?o.message:String(o)}`)}if(r==null)throw new i(`Empty response from Douyin API (${t} ${e})`);if(Array.isArray(r)||typeof r!=="object")throw new i(`Malformed response from Douyin API (${t} ${e})`);if(r&&typeof r==="object"&&"status_code"in r){const o=r.status_code;if(o!==0){const a=r.status_msg??r.message??"unknown error";if(f(o,a))throw new u("creator.douyin.com",`Douyin API auth/permission error ${o} at ${t} ${e}: ${a}`);throw new i(`Douyin API error ${o} at ${t} ${e}: ${a}`)}}return r}
@@ -0,0 +1 @@
1
+ import{ArgumentError as s,CommandExecutionError as u}from"@jackwener/opencli/errors";import{clickControl as c,fillControl as m,verifyControl as n,publicationTime as h,resolveDeclaration as y}from"../../_shared/publish-controls.js";import{setDouyinAiDeclaration as S}from"./ai-declaration.js";export const DOUYIN_DECLARATIONS={ai:["aigc","内容由AI生成"],opinion:["personal_opinion","内容为个人观点或见解"],marketing:["marketing","内容含营销推广信息"],fiction:["only_fun_new","虚构演绎,仅供娱乐"],none:["__NO_NEED_USER_DECLARATION__","无需添加自主声明"],reprint:["repost","内容为转载信息"]},DOUYIN_DECLARATION_ARGS=[{name:"declaration",choices:Object.keys(DOUYIN_DECLARATIONS),help:"自主声明:ai / opinion / marketing / fiction / reprint / none"},{name:"source",choices:["external","douyin"],help:"转载来源:external站外 / douyin站内,reprint必填"},{name:"source-video",help:"站内转载来源作品ID(UI命令须在当前观看历史中)"}],DOUYIN_UI_ARGS=[...DOUYIN_DECLARATION_ARGS,{name:"schedule",help:"定时发布,北京时间 YYYY-MM-DD HH:mm / ISO8601 / Unix秒;2小时至14天后"},{name:"allow-download",type:"boolean",help:"允许保存作品(true/false,不传保留平台默认)"},{name:"collection",help:"合集完整名称;先用 douyin collections 查看账号已有合集"},{name:"location",help:"地点完整名称,按平台搜索结果精确选择"},{name:"location-address",help:"地点完整地址,搜索结果重名时用于区分(需 --location)"},{name:"hotspot",help:"关联热点完整名称"},{name:"activity",help:"官方活动完整名称;先用 activities 查询,再用 --id 读参与要求"},{name:"music",help:"音乐完整名称(需要同时指定 --music-author)"},{name:"music-author",help:"音乐作者完整名称,用于区分同名曲目;先用 douyin music 搜索"},{name:"music-id",help:"查询结果的精确音乐 ID,避免同名同作者误选"},{name:"music-query",help:"查询结果中的原搜索词,保留原候选范围"},{name:"music-category",help:"查询结果中的分类名称,与 music-query 二选一"}],DOUYIN_VIDEO_ARGS=[...DOUYIN_UI_ARGS,{name:"sync-toutiao",type:"boolean",help:"同步到今日头条;需公开且平台控件可用,不修改记住我的选择"}];export function validateDouyinSettings(e){if(e["location-address"]&&!e.location)throw new s("--location-address 需要 --location");if(e["sync-toutiao"]&&e.visibility&&e.visibility!=="public")throw new s("同步头条需要公开可见");if(["music-id","music-query","music-category"].some((o)=>e[o])&&!e.music)throw new s("音乐选择参数需要 --music 与 --music-author");if(e["music-query"]&&e["music-category"])throw new s("--music-query 与 --music-category 二选一");if(e["music-id"]&&!/^\d+$/.test(e["music-id"]))throw new s("--music-id 必须为精确音乐 ID");const t=y(e,DOUYIN_DECLARATIONS);f(e,t);if(!!e.music!==!!e["music-author"])throw new s("--music 与 --music-author 必须同时提供");if(e.visibility!==void 0&&!["public","friends","private"].includes(e.visibility))throw new s("不支持的 --visibility");return{declaration:t,schedule:h(e.schedule,2)}}export function douyinDeclarationInfo(e){const t=y(e,DOUYIN_DECLARATIONS);f(e,t);return JSON.stringify(t&&t!=="none"?{choose_value:DOUYIN_DECLARATIONS[t][0],...t==="reprint"?{source:e.source==="external"?"站外":"抖音作品",...e["source-video"]?{from_item_id:e["source-video"]}:{}}:{}}:{})}function f(e,t){if(t==="reprint"&&!["external","douyin"].includes(e.source))throw new s("转载声明需要 --source external 或 douyin");if(e.source==="douyin"&&!/^\d+$/.test(e["source-video"]||""))throw new s("站内转载需要 --source-video 作品ID");if(e["source-video"]&&e.source!=="douyin")throw new s("--source-video 仅用于 --source douyin");if((e.source||e["source-video"])&&t!=="reprint")throw new s("转载来源参数仅用于 --declaration reprint")}async function x(e,t){await c(e,'[class*="selectBox-"]',"请选择自主声明");await c(e,".semi-modal label.semi-radio","内容为转载信息");await c(e,".semi-modal label.semi-radio",t.source==="external"?"取材站外":"取材站内");if(t.source==="douyin"){const o=`((id)=>{const all=[...document.querySelectorAll('[class*="itemContainer-"]')];const found=all.filter(e=>{const f=e[Object.keys(e).find(k=>k.startsWith('__reactFiber'))];return f?.key===id;});return found.length===1 ? all.indexOf(found[0]) : -1;})(${JSON.stringify(t["source-video"])})`;await n(e,`(${o})>=0`,"观看历史中的来源作品");await e.click('[class*="itemContainer-"]',{nth:await e.evaluate(o)});const i=await e.evaluate("([...document.querySelectorAll('.semi-modal')].filter(e=>e.querySelector('.semi-modal-title')?.textContent.trim()==='观看历史').map(e=>[...document.querySelectorAll('.semi-modal button')].indexOf([...e.querySelectorAll('button')].find(b=>b.textContent.trim()==='确定'))))");if(i.length!==1||i[0]<0)throw new u("来源作品确认按钮不可用;已停止提交");await e.click(".semi-modal button",{nth:i[0]});await n(e,"![...document.querySelectorAll('.semi-modal-title')].some(e=>e.textContent.trim()==='观看历史')","来源作品选择")}await c(e,".semi-modal button","确定");await n(e,`![...document.querySelectorAll('.semi-modal')].some(e=>e.getClientRects().length) && [...document.querySelectorAll('[class*="selectBox-"]')].some(e=>e.textContent.trim()==='内容为转载信息')`,"转载声明")}async function d(e,t){if(await e.evaluate(`Boolean([...document.querySelectorAll('label[data-checked], label.semi-radio')].some(e=>e.textContent.trim()===${JSON.stringify(t)} && (e.dataset.checked==='true'||e.classList.contains('semi-radio-checked'))))`))return;await c(e,"label[data-checked], label.semi-radio",t);await n(e,`([...document.querySelectorAll('label[data-checked], label.semi-radio')].filter(e=>e.textContent.trim()===${JSON.stringify(t)}).some(e=>e.dataset.checked==='true'||e.classList.contains('semi-radio-checked')))`,t)}async function w(e,t,o,i,r){const l=Array.isArray(t)?t:[t],a=await e.evaluate(`([...document.querySelectorAll('.semi-select')].filter(e=>${JSON.stringify(l)}.includes(e.textContent.trim())).map(e=>e.textContent.trim()))`);if(a.length!==1)throw new u("找不到唯一的发布选项 "+l.join(" / ")+";已停止提交");await c(e,".semi-select",a[0]);if(r)await m(e,".semi-select-open input",o,!1);await n(e,`([...document.querySelectorAll(${JSON.stringify(i)})].some(e=>e.getClientRects().length && e.textContent.trim()===${JSON.stringify(o)}))`,"候选项 "+o);await c(e,i,o);await n(e,`([...document.querySelectorAll('.semi-select .semi-select-selection-text')].some(e=>e.textContent.trim()===${JSON.stringify(o)}))`,o)}export async function setDouyinPublishSettings(e,t){const o=validateDouyinSettings(t);if(o.declaration==="reprint")await x(e,t);else if(o.declaration)await S(e,DOUYIN_DECLARATIONS[o.declaration][1]);if(t.visibility)await d(e,{public:"公开",friends:"好友可见",private:"仅自己可见"}[t.visibility]);if(t["allow-download"]!==void 0)await d(e,t["allow-download"]?"允许":"不允许");if(o.schedule){await d(e,"定时发布");await m(e,'input[placeholder="日期和时间"]',o.schedule)}if(t.collection)await w(e,["不选择合集","请选择合集"],t.collection,".semi-select-option-text",!1);if(t.location){const i=await e.evaluate("([...document.querySelectorAll('.semi-select')].find(e=>['输入相关位置,让更多人看到你的作品','输入地理位置'].includes(e.textContent.trim()))?.textContent.trim())");if(!i)throw new u("找不到地点选择器;已停止提交");await c(e,".semi-select",i);await m(e,".semi-select-open input",t.location,!1);const r=`(({name,address})=>{const all=[...document.querySelectorAll('.semi-select-option')];const hits=all.filter(e=>e.getClientRects().length && e.querySelector('[class*="name-"]')?.textContent.trim()===name && (!address || e.querySelector('[class*="address-"]')?.textContent.trim()===address));return hits.length===1 ? all.indexOf(hits[0]) : -1;})(${JSON.stringify({name:t.location,address:t["location-address"]})})`;await n(e,`(${r})>=0`,"唯一地点(重名时指定 --location-address)");await e.click(".semi-select-option",{nth:await e.evaluate(r)});await n(e,`Boolean([...document.querySelectorAll('.semi-select-selection-text')].some(e=>e.textContent.trim()===${JSON.stringify(t.location)}))`,"地点")}if(t.hotspot)await w(e,"点击输入热点词",t.hotspot,".semi-select-option .select-word",!0);if(t.music){const i=await e.evaluate("location.pathname.includes('/post/video')");await c(e,i?'[class*="preview-button-"]':'[class*="action-"]',i?"添加音乐":"选择音乐");await n(e,`!!document.querySelector('input[placeholder="搜索音乐"]')?.getClientRects().length`,"音乐选择器",15000);if(t["music-category"])await c(e,'.semi-sidesheet [role="tab"]',t["music-category"]);else{await m(e,'input[placeholder="搜索音乐"]',t["music-query"]||t.music,!1);await e.pressKey("Enter")}const r=`(({title,author,id})=>{const musicId=e=>{let f=e[Object.keys(e).find(k=>k.startsWith('__reactFiber'))];for(let i=0;f&&i<5;i++,f=f.return)if(f.memoizedProps?.id&&f.memoizedProps?.title)return String(f.memoizedProps.id);};const cards=[...document.querySelectorAll('[class*="card-wrapper-"]')].filter(e=>e.getClientRects().length && e.querySelector('[class*="song-name-"]')?.textContent.trim()===title && e.querySelector('.song-author')?.textContent.trim()===author && (!id || musicId(e)===id));return cards.length===1 ? [...document.querySelectorAll('[class*="apply-btn-"]')].indexOf(cards[0].querySelector('[class*="apply-btn-"]')) : -1;})(${JSON.stringify({title:t.music,author:t["music-author"],id:t["music-id"]})})`;let l=-1;for(let a=0;a<40;a++){l=await e.evaluate(r);if(l>=0)break;await e.evaluate(`(() => { const list=document.querySelector('[class*="music-collection-container-"]'); if(list){list.scrollTop=list.scrollHeight;list.dispatchEvent(new Event('scroll',{bubbles:true}));} })()`);await e.wait({time:0.5})}if(l<0)throw new u("未在音乐选择器加载到唯一候选(20秒上限);请重新查询或缩小关键词,已停止提交");await e.click('[class*="apply-btn-"]',{nth:l});await n(e,i?`document.querySelector('[class*="music-text-"]')?.textContent.includes(${JSON.stringify(t.music+" - @"+t["music-author"])}) && !document.querySelector('.semi-sidesheet-close')`:`document.querySelector('[class*="sub-desc-title-"]')?.textContent.trim()===${JSON.stringify(t.music)}`,"音乐")}if(t.activity){const i='[class*="activity-list-container-"] [class*="title-"]';if(await e.evaluate(`Boolean([...document.querySelectorAll(${JSON.stringify(i)})].some(e=>e.textContent.trim()===${JSON.stringify(t.activity)}))`)){const l=`Boolean([...document.querySelectorAll(${JSON.stringify(i)})].find(e=>e.textContent.trim()===${JSON.stringify(t.activity)})?.closest('[class*="activity-right-"]')?.textContent.includes('已添加'))`;if(!await e.evaluate(l))await c(e,i,t.activity);await n(e,l,"活动")}else{const l=await e.evaluate(`([...document.querySelectorAll('[class*="more-"]')].filter(e=>e.getClientRects().length && /^\\+\\d+$/.test(e.textContent.trim())).map(e=>e.textContent.trim()))`);if(l.length!==1)throw new u("活动不在推荐中,且未找到完整活动列表;已停止提交");await c(e,'[class*="more-"]',l[0]);await n(e,`Boolean([...document.querySelectorAll('.semi-sidesheet [class*="title-"]')].some(e=>e.textContent.trim()===${JSON.stringify(t.activity)}))`,"活动 "+t.activity);const a=`Boolean([...document.querySelectorAll('.semi-sidesheet [class*="title-"]')].find(e=>e.textContent.trim()===${JSON.stringify(t.activity)})?.closest('[class*="container-checked-"]'))`;if(!await e.evaluate(a))await c(e,'.semi-sidesheet [class*="title-"]',t.activity);await n(e,a,"选中活动");await c(e,'.semi-sidesheet [class*="apply-"]',"确认");await n(e,`!document.querySelector('.semi-sidesheet-close') && document.querySelector('.zone-container')?.textContent.includes(${JSON.stringify(t.activity)})`,"活动话题")}}if(t["sync-toutiao"]!==void 0){await d(e,t["sync-toutiao"]?"同时发布到":"不同时发布");if(t["sync-toutiao"]){const i='[class*="app-group-container-item-"] .semi-switch',r=`document.querySelector(${JSON.stringify(i+" input")})?.checked === true`;await n(e,`document.querySelector('[class*="app-group-container-item-"]')?.textContent.trim()==='今日头条'`,"头条同步选项");if(!await e.evaluate(r))await c(e,i);await n(e,r,"同步头条")}}}
@@ -1 +1 @@
1
- import{cli as e,Strategy as o}from"@jackwener/opencli/registry";import{browserFetch as r}from"./_shared/browser-fetch.js";e({site:"douyin",name:"activities",access:"read",description:"官方活动列表",domain:"creator.douyin.com",strategy:o.COOKIE,args:[],columns:["activity_id","title","end_time"],func:async(i,a)=>{return((await r(i,"GET","https://creator.douyin.com/web/api/media/activity/get/?aid=1128")).activity_list??[]).map((t)=>({activity_id:t.activity_id,title:t.title??t.activity_name??"",end_time:typeof t.end_time==="number"?new Date(t.end_time*1000).toLocaleString("zh-CN",{timeZone:"Asia/Tokyo"}):t.show_end_time??""}))}});
1
+ import{cli as m,Strategy as s}from"@jackwener/opencli/registry";import{ArgumentError as a,CommandExecutionError as c}from"@jackwener/opencli/errors";import{browserFetch as l}from"./_shared/browser-fetch.js";m({site:"douyin",name:"activities",access:"read",domain:"creator.douyin.com",strategy:s.COOKIE,description:"查看/筛选官方活动;--id 查看参与规则。ui_args 用于图文/草稿,api_args 用于 publish;规则可能另有必带话题或@要求",args:[{name:"query",help:"按活动名称筛选"},{name:"id",help:"活动 ID,读取完整参与规则"},{name:"limit",type:"int",default:20}],columns:["activity_id","title","start_time","end_time","topics","requirements","ui_args","api_args"],func:async(d,t)=>{const e=t.limit??20;if(!Number.isInteger(e)||e<1||e>100)throw new a("--limit 必须为1至100的整数");if(t.id&&!/^\d+$/.test(t.id))throw new a("--id 必须为活动 ID");if(t.id&&t.query)throw new a("--id 与 --query 二选一");const n=await l(d,"GET",t.id?`https://creator.douyin.com/web/api/media/activity/detail/?activity_id=${t.id}`:"https://creator.douyin.com/web/api/media/activity/get/?page=1&size=9999&need_challenge=1&aid=1128"),o=t.id?n.activity_info?[n.activity_info]:null:n.activity_list;if(!Array.isArray(o))throw new c("活动响应缺少活动列表或详情,不能视为空结果");return o.filter((i)=>String(i.title??i.activity_name??"").toLowerCase().includes(String(t.query||"").toLowerCase())).slice(0,e).map((i)=>{const r=i.title??i.activity_name;if(!i.activity_id||!r)throw new c("活动缺少 ID 或名称");return{activity_id:String(i.activity_id),title:r,start_time:i.show_start_time||"",end_time:typeof i.end_time==="number"?new Date(i.end_time*1000).toLocaleString("zh-CN",{timeZone:"Asia/Shanghai"}):i.show_end_time||"",topics:i.challenge??null,requirements:t.id?n.activity_description??null:"用 activities --id "+i.activity_id+" 查看完整规则",ui_args:i.challenge?.length?{activity:r}:null,api_args:{activity:String(i.activity_id)}}})}});
@@ -1 +1 @@
1
- import{cli as m,Strategy as r}from"@jackwener/opencli/registry";import{browserFetch as n}from"./_shared/browser-fetch.js";m({site:"douyin",name:"collections",access:"read",description:"合集列表",domain:"creator.douyin.com",strategy:r.COOKIE,args:[{name:"limit",type:"int",default:20}],columns:["mix_id","name","item_count"],func:async(t,e)=>{const o=`https://creator.douyin.com/web/api/mix/list/?status=0,1,2,3,6&count=${e.limit}&cursor=0&should_query_new_mix=1&device_platform=web&aid=1128`;return((await n(t,"GET",o)).mix_list??[]).map((i)=>({mix_id:i.mix_id,name:i.mix_name,item_count:i.item_count}))}});
1
+ import{cli as s,Strategy as a}from"@jackwener/opencli/registry";import{ArgumentError as e,CommandExecutionError as c}from"@jackwener/opencli/errors";import{browserFetch as u}from"./_shared/browser-fetch.js";s({site:"douyin",name:"collections",access:"read",description:"合集列表",domain:"creator.douyin.com",strategy:a.COOKIE,args:[{name:"limit",type:"int",default:20},{name:"cursor",default:"0",help:"上一页返回的 next_cursor"}],columns:["mix_id","name","item_count","next_cursor","has_more","ui_args","api_args"],func:async(n,o)=>{const i=o.limit??20;if(!Number.isInteger(i)||i<1||i>100)throw new e("--limit 必须为1至100的整数");if(!/^\d+$/.test(String(o.cursor??"0")))throw new e("--cursor 必须为非负整数游标");const m=`https://creator.douyin.com/web/api/mix/list/?status=0,1,2,3,6&count=${i}&cursor=${o.cursor??"0"}&should_query_new_mix=1&device_platform=web&aid=1128`,t=await u(n,"GET",m);if(!Array.isArray(t.mix_list))throw new c("合集响应缺少 mix_list,不能视为空列表");return t.mix_list.map((r)=>({mix_id:r.mix_id,name:r.mix_name,item_count:r.item_count,next_cursor:String(t.cursor??""),has_more:Boolean(t.has_more),ui_args:{collection:r.mix_name},api_args:{collection:String(r.mix_id)}}))}});
@@ -1,11 +1,11 @@
1
- import*as f from"node:fs";import*as s from"node:path";import{cli as w,Strategy as v}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as l}from"@jackwener/opencli/errors";import{setInputFiles as p}from"../_shared/file-inject.js";const m={public:"公开",friends:"好友可见",private:"仅自己可见"},h="https://creator.douyin.com/creator-micro/content/upload",T=120,A=20,x=20,E=200;async function b(t){await t.evaluate(`() => {
1
+ import{DOUYIN_VIDEO_ARGS as w,validateDouyinSettings as v,setDouyinPublishSettings as h}from"./_shared/publish-settings.js";import*as f from"node:fs";import*as s from"node:path";import{cli as T,Strategy as A}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as l}from"@jackwener/opencli/errors";import{setInputFiles as m}from"../_shared/file-inject.js";const p={public:"公开",friends:"好友可见",private:"仅自己可见"},x="https://creator.douyin.com/creator-micro/content/upload",S=120,E=20,I=20,C=200;async function b(t){await t.evaluate(`() => {
2
2
  const targets = ['我知道了', '知道了', '关闭'];
3
3
  for (const text of targets) {
4
4
  const btn = Array.from(document.querySelectorAll('button,[role="button"]'))
5
5
  .find((el) => (el.textContent || '').trim() === text);
6
6
  if (btn instanceof HTMLElement) btn.click();
7
7
  }
8
- }`)}async function S(t){let n={href:"",ready:!1,bodyText:""};for(let e=0;e<T;e+=1){n=await t.evaluate(`() => ({
8
+ }`)}async function k(t){let n={href:"",ready:!1,bodyText:""};for(let e=0;e<S;e+=1){n=await t.evaluate(`() => ({
9
9
  href: location.href,
10
10
  ready: !!Array.from(document.querySelectorAll('input')).find(
11
11
  (el) => (el.placeholder || '').includes('填写作品标题')
@@ -13,7 +13,7 @@ import*as f from"node:fs";import*as s from"node:path";import{cli as w,Strategy a
13
13
  (el) => (el.textContent || '').includes('暂存离开')
14
14
  ),
15
15
  bodyText: document.body?.innerText || ''
16
- })`);if(n.ready)return;await t.wait({time:0.5})}throw new l("等待抖音草稿编辑页超时",`当前页面: ${n.href||"unknown"}`)}async function I(t,n){for(let e=0;e<E;e+=1){const i=await t.evaluate(`() => {
16
+ })`);if(n.ready)return;await t.wait({time:0.5})}throw new l("等待抖音草稿编辑页超时",`当前页面: ${n.href||"unknown"}`)}async function _(t,n){for(let e=0;e<C;e+=1){const i=await t.evaluate(`() => {
17
17
  const text = document.body?.innerText || '';
18
18
  const uploading = /上传中|上传\\s*\\d+%|视频上传中|处理中/.test(text);
19
19
  const saveBtn = Array.from(document.querySelectorAll('button')).find(
@@ -27,7 +27,7 @@ import*as f from"node:fs";import*as s from"node:path";import{cli as w,Strategy a
27
27
  (el) => (el.textContent || '').includes(${JSON.stringify(n)})
28
28
  );
29
29
  return { uploading, saveEnabled, hasSaveBtn: !!saveBtn, hasCaptionEditor, hasVisibility };
30
- }`);if(i.hasSaveBtn&&i.saveEnabled&&i.hasCaptionEditor&&i.hasVisibility&&!i.uploading)return;await t.wait({time:0.5})}}async function k(t,n){const e=60;let i=!1;for(let o=0;o<e;o+=1){i=await t.evaluate(`() => {
30
+ }`);if(i.hasSaveBtn&&i.saveEnabled&&i.hasCaptionEditor&&i.hasVisibility&&!i.uploading)return;await t.wait({time:0.5})}}async function g(t,n){const e=60;let i=!1;for(let o=0;o<e;o+=1){i=await t.evaluate(`() => {
31
31
  const titleInput = Array.from(document.querySelectorAll('input')).find(
32
32
  (el) => (el.placeholder || '').includes('填写作品标题')
33
33
  );
@@ -71,7 +71,7 @@ import*as f from"node:fs";import*as s from"node:path";import{cli as w,Strategy a
71
71
  if (!(visibility instanceof HTMLElement)) return false;
72
72
  visibility.click();
73
73
  return true;
74
- }`);if(r)break;await t.wait({time:0.5})}if(!r)throw new l("填写抖音草稿表单失败: visibility-missing")}async function C(t){let n="cover-input-missing";const e=await t.evaluate(`() => Array.from(document.querySelectorAll('input[type="file"]')).length`);for(let i=0;i<A;i+=1){const r=await t.evaluate(`() => {
74
+ }`);if(r)break;await t.wait({time:0.5})}if(!r)throw new l("填写抖音草稿表单失败: visibility-missing")}async function L(t){let n="cover-input-missing";const e=await t.evaluate(`() => Array.from(document.querySelectorAll('input[type="file"]')).length`);for(let i=0;i<E;i+=1){const r=await t.evaluate(`() => {
75
75
  const coverLabel = Array.from(document.querySelectorAll('label')).find(
76
76
  (el) => (el.textContent || '').includes('上传新封面')
77
77
  );
@@ -110,7 +110,7 @@ import*as f from"node:fs";import*as s from"node:path";import{cli as w,Strategy a
110
110
  root = root.parentElement;
111
111
  }
112
112
  return '';
113
- }`}async function _(t){return await t.evaluate(buildCoverCheckPanelTextJs())||""}async function g(t){let n="",e=!1;for(let i=0;i<x;i+=1){const r=await _(t),o=r.includes("检测中"),a=r.includes("重新检测")||r.includes("横/竖双封面缺失");if(o)e=!0;if(e&&a&&!o)return;n=r;await t.wait({time:0.5})}throw new l("等待抖音封面处理完成超时",n||"unknown")}async function L(t){const n=60;let e=null;for(let i=0;i<n;i+=1){e=await t.evaluate(`() => {
113
+ }`}async function O(t){return await t.evaluate(buildCoverCheckPanelTextJs())||""}async function M(t){let n="",e=!1;for(let i=0;i<I;i+=1){const r=await O(t),o=r.includes("检测中"),a=r.includes("重新检测")||r.includes("横/竖双封面缺失");if(o)e=!0;if(e&&a&&!o)return;n=r;await t.wait({time:0.5})}throw new l("等待抖音封面处理完成超时",n||"unknown")}async function P(t){const n=60;let e=null;for(let i=0;i<n;i+=1){e=await t.evaluate(`() => {
114
114
  const extractCreationId = () => {
115
115
  const titleInput = Array.from(document.querySelectorAll('input')).find(
116
116
  (el) => (el.placeholder || '').includes('填写作品标题')
@@ -160,7 +160,7 @@ import*as f from"node:fs";import*as s from"node:path";import{cli as w,Strategy a
160
160
  text: (btn.textContent || '').trim(),
161
161
  creationId,
162
162
  };
163
- }`);if(e?.ok)break;await t.wait({time:0.5})}if(!e?.ok)throw new l(`点击草稿按钮失败: ${e?.reason||"unknown"}`);if(!e.creationId)throw new l("点击草稿按钮失败: creation-id-missing");return{text:e.text||"暂存离开",creationId:e.creationId}}async function M(t,n){let e={href:"",bodyText:""};for(let i=0;i<40;i+=1){e=await t.evaluate(`() => ({
163
+ }`);if(e?.ok)break;await t.wait({time:0.5})}if(!e?.ok)throw new l(`点击草稿按钮失败: ${e?.reason||"unknown"}`);if(!e.creationId)throw new l("点击草稿按钮失败: creation-id-missing");return{text:e.text||"暂存离开",creationId:e.creationId}}async function q(t,n){let e={href:"",bodyText:""};for(let i=0;i<40;i+=1){e=await t.evaluate(`() => ({
164
164
  href: location.href,
165
165
  bodyText: document.body?.innerText || ''
166
- })`);const r=e.href||"",o=e.bodyText||"",a=r.includes("/creator-micro/content/upload")&&/继续编辑/.test(o),u=/草稿保存成功|已保存到草稿|存草稿成功|保存成功/.test(o),d=/\/creator-micro\/content\/(manage|drafts|works)/.test(r)||/创作中心/.test(o)&&!r.includes("/content/upload");if(a||u||d)return n;await t.wait({time:1})}throw new l("未检测到抖音草稿保存确认",`当前页面: ${e.href||"unknown"}`)}w({site:"douyin",name:"draft",access:"write",description:"上传视频并保存为草稿",domain:"creator.douyin.com",strategy:v.COOKIE,navigateBefore:!1,defaultWindowMode:"foreground",args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"视频标题(≤30字)"},{name:"caption",fromFile:!0,default:"",help:"正文内容(≤1000字,支持 #话题)"},{name:"cover",default:"",help:"封面图片路径"},{name:"visibility",default:"public",choices:["public","friends","private"]},{name:"timeout",type:"int",default:180,help:"命令超时(秒),视频上传/转码慢时可调大"}],columns:["status","draft_id"],func:async(t,n)=>{const e=s.resolve(n.video);if(!f.existsSync(e))throw new c(`视频文件不存在: ${e}`);const i=s.extname(e).toLowerCase();if(![".mp4",".mov",".avi",".webm"].includes(i))throw new c(`不支持的视频格式: ${i}(支持 mp4/mov/avi/webm)`);const r=n.title;if(r.length>30)throw new c("标题不能超过 30 字");const o=n.caption||"";if(o.length>1000)throw new c("正文不能超过 1000 字");const a=n.cover;if(a){if(!f.existsSync(s.resolve(a)))throw new c(`封面文件不存在: ${s.resolve(a)}`)}const u=m[n.visibility]??m.public;await t.goto(h);await t.wait({selector:'input[type="file"]',timeout:20});await b(t);await p(t,'input[type="file"]',[e]);await S(t);await I(t,u);await b(t);if(a){const y=await C(t);await p(t,y,[s.resolve(a)]);await g(t)}await k(t,{title:r,caption:o,visibilityLabel:u});await t.wait({time:1});const d=await L(t);return[{status:"✅ 草稿已保存,可在创作中心继续编辑",draft_id:await M(t,d.creationId)}]}});
166
+ })`);const r=e.href||"",o=e.bodyText||"",a=r.includes("/creator-micro/content/upload")&&/继续编辑/.test(o),u=/草稿保存成功|已保存到草稿|存草稿成功|保存成功/.test(o),d=/\/creator-micro\/content\/(manage|drafts|works)/.test(r)||/创作中心/.test(o)&&!r.includes("/content/upload");if(a||u||d)return n;await t.wait({time:1})}throw new l("未检测到抖音草稿保存确认",`当前页面: ${e.href||"unknown"}`)}T({site:"douyin",name:"draft",access:"write",description:"上传视频并保存为草稿",domain:"creator.douyin.com",strategy:A.COOKIE,navigateBefore:!1,defaultWindowMode:"foreground",args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"视频标题(≤30字)"},{name:"caption",fromFile:!0,default:"",help:"正文内容(≤1000字,支持 #话题)"},{name:"cover",default:"",help:"封面图片路径"},{name:"visibility",default:"public",choices:["public","friends","private"]},{name:"timeout",type:"int",default:180,help:"命令超时(秒),视频上传/转码慢时可调大"},{name:"ai-generated",type:"boolean",help:"声明内容由 AI 生成(发布和草稿均支持;设置失败时停止提交)"},...w],columns:["status","draft_id"],func:async(t,n)=>{v(n);const e=s.resolve(n.video);if(!f.existsSync(e))throw new c(`视频文件不存在: ${e}`);const i=s.extname(e).toLowerCase();if(![".mp4",".mov",".avi",".webm"].includes(i))throw new c(`不支持的视频格式: ${i}(支持 mp4/mov/avi/webm)`);const r=n.title;if(r.length>30)throw new c("标题不能超过 30 字");const o=n.caption||"";if(o.length>1000)throw new c("正文不能超过 1000 字");const a=n.cover;if(a){if(!f.existsSync(s.resolve(a)))throw new c(`封面文件不存在: ${s.resolve(a)}`)}const u=p[n.visibility]??p.public;await t.goto(x);await t.wait({selector:'input[type="file"]',timeout:20});await b(t);await m(t,'input[type="file"]',[e]);await k(t);await _(t,u);await b(t);if(a){const y=await L(t);await m(t,y,[s.resolve(a)]);await M(t)}await g(t,{title:r,caption:o,visibilityLabel:u});await t.wait({time:1});await h(t,{...n,visibility:void 0});const d=await P(t);return[{status:"✅ 草稿已保存,可在创作中心继续编辑",draft_id:await q(t,d.creationId)}]}});
@@ -1 +1 @@
1
- import{cli as w,Strategy as f}from"@jackwener/opencli/registry";import{browserFetch as u}from"./_shared/browser-fetch.js";import{ArgumentError as h,CommandExecutionError as c}from"@jackwener/opencli/errors";function l(e){return e!==null&&typeof e==="object"&&!Array.isArray(e)}function y(e,n,i){if(!l(e))throw new c(`douyin hashtag ${i}: API returned malformed payload`);const r=e[n];if(r===void 0||r===null)return[];if(!Array.isArray(r))throw new c(`douyin hashtag ${i}: API returned malformed "${n}"`);return r}function g(e){const n=e.action;if(n==="search"){if(!String(e.keyword??"").trim())throw new h("douyin hashtag search 需要 --keyword <关键词>","示例: ppcli douyin hashtag search --keyword 美食");return}if(n==="suggest"){if(!String(e.cover??"").trim())throw new h("douyin hashtag suggest 需要 --cover <cover_uri>","suggest 基于已上传的视频封面做 AI 推荐, 不是关键词搜索. 关键词搜索请用 `douyin hashtag search --keyword <词>`.")}}w({site:"douyin",name:"hashtag",access:"read",description:"话题搜索 / AI推荐 / 热点词",domain:"creator.douyin.com",strategy:f.COOKIE,args:[{name:"action",required:!0,positional:!0,choices:["search","suggest","hot"],help:"search=关键词搜索 (--keyword 必填), suggest=AI推荐 (--cover 必填), hot=热点词 (--keyword 可选)"},{name:"keyword",default:"",help:"搜索关键词. search 必填; hot 可选; suggest 不使用 (传 --cover)"},{name:"cover",default:"",help:"封面 URI (cover_uri). suggest 必填; 其它 action 不使用"},{name:"limit",type:"int",default:10}],columns:["name","id","view_count"],validateArgs:g,func:async(e,n)=>{g(n);const i=n.action;if(i==="search"){const r=String(n.keyword??"").trim(),d=`https://creator.douyin.com/aweme/v1/challenge/search/?keyword=${encodeURIComponent(r)}&count=${n.limit}&aid=1128`,s=await u(e,"GET",d),a=y(s,"challenge_list","search"),o=a.flatMap((m)=>{const t=m?.challenge_info;if(!l(t))return[];return[{name:t.cha_name,id:t.cid,view_count:t.view_count}]});if(a.length>0&&o.length===0)throw new c("douyin hashtag search: API returned challenges but none had stable challenge_info shape");return o}if(i==="suggest"){const r=String(n.cover??"").trim(),d=`https://creator.douyin.com/web/api/media/hashtag/rec/?cover_uri=${encodeURIComponent(r)}&aid=1128`,s=await u(e,"GET",d);return y(s,"hashtag_list","suggest").map((o)=>({name:o?.name??"",id:o?.id??"",view_count:o?.view_count??0}))}if(i==="hot"){const r=String(n.keyword??"").trim(),d=`https://creator.douyin.com/aweme/v1/hotspot/recommend/?${r?`keyword=${encodeURIComponent(r)}&`:""}aid=1128`,s=await u(e,"GET",d);if(!l(s))throw new c("douyin hashtag hot: API returned malformed payload");const{hotspot_list:a,all_sentences:o}=s;if(a!==void 0&&a!==null&&!Array.isArray(a))throw new c('douyin hashtag hot: API returned malformed "hotspot_list"');if(o!==void 0&&o!==null&&!Array.isArray(o))throw new c('douyin hashtag hot: API returned malformed "all_sentences"');return(Array.isArray(a)?a:Array.isArray(o)?o.map((t)=>({sentence:t?.word??"",hot_value:t?.hot_value,sentence_id:t?.sentence_id??""})):[]).slice(0,n.limit).map((t)=>({name:t?.sentence??"",id:t&&"sentence_id"in t?t.sentence_id:"",view_count:t?.hot_value??0}))}throw new h(`未知的 action: ${i}`)}});
1
+ import{cli as w,Strategy as p}from"@jackwener/opencli/registry";import{browserFetch as d}from"./_shared/browser-fetch.js";import{ArgumentError as l,CommandExecutionError as a}from"@jackwener/opencli/errors";function m(t){return t!==null&&typeof t==="object"&&!Array.isArray(t)}function y(t,n,i){if(!m(t))throw new a(`douyin hashtag ${i}: API returned malformed payload`);const r=t[n];if(r===void 0||r===null)throw new a(`douyin hashtag ${i}: API missing "${n}"`);if(!Array.isArray(r))throw new a(`douyin hashtag ${i}: API returned malformed "${n}"`);return r}function g(t){const n=t.action;if(!Number.isInteger(t.limit??10)||(t.limit??10)<1||(t.limit??10)>100)throw new l("--limit 必须为1至100的整数");if(n==="search"){if(!String(t.keyword??"").trim())throw new l("douyin hashtag search 需要 --keyword <关键词>","示例: ppcli douyin hashtag search --keyword 美食");return}if(n==="suggest"){if(!String(t.cover??"").trim())throw new l("douyin hashtag suggest 需要 --cover <cover_uri>","suggest 基于已上传的视频封面做 AI 推荐, 不是关键词搜索. 关键词搜索请用 `douyin hashtag search --keyword <词>`.")}}w({site:"douyin",name:"hashtag",access:"read",description:"查询发布话题 / 封面推荐 / 热点;publish_args 用于 publish-image,视频话题需放入 caption 中的 #话题",domain:"creator.douyin.com",strategy:p.COOKIE,args:[{name:"action",required:!0,positional:!0,choices:["search","suggest","hot"],help:"search=关键词搜索 (--keyword 必填), suggest=AI推荐 (--cover 必填), hot=热点词 (--keyword 可选)"},{name:"keyword",default:"",help:"搜索关键词. search 必填; hot 可选; suggest 不使用 (传 --cover)"},{name:"cover",default:"",help:"封面 URI (cover_uri). suggest 必填; 其它 action 不使用"},{name:"limit",type:"int",default:10}],columns:["name","id","view_count","publish_args"],validateArgs:g,func:async(t,n)=>{g(n);const i=n.action;if(i==="search"){const r=String(n.keyword??"").trim(),u=`https://creator.douyin.com/aweme/v1/search/challengesug/?keyword=${encodeURIComponent(r)}&source=challenge_create&aid=2906`,c=await d(t,"GET",u),s=y(c,"sug_list","search").slice(0,n.limit),o=s.flatMap((h)=>{const e=h;if(!m(e)||!e.cid||!e.cha_name)throw new a("话题候选缺少 ID 或名称");return[{name:e.cha_name,id:e.cid,view_count:e.view_count,publish_args:{topics:e.cha_name}}]});if(s.length>0&&o.length===0)throw new a("douyin hashtag search: API returned challenges but none had stable challenge_info shape");return o}if(i==="suggest"){const r=String(n.cover??"").trim(),u=`https://creator.douyin.com/web/api/media/hashtag/rec/?cover_uri=${encodeURIComponent(r)}&aid=1128`,c=await d(t,"GET",u);return y(c,"challenge_list","suggest").slice(0,n.limit).map((o)=>({name:o.cha_name,id:o.cid,view_count:o.view_count??null,publish_args:{topics:o.cha_name}}))}if(i==="hot"){const r=String(n.keyword??"").trim(),u=`https://creator.douyin.com/aweme/v1/hotspot/recommend/?${r?`keyword=${encodeURIComponent(r)}&`:""}aid=1128`,c=await d(t,"GET",u);if(!m(c))throw new a("douyin hashtag hot: API returned malformed payload");const{hotspot_list:s,all_sentences:o}=c;if(s!==void 0&&s!==null&&!Array.isArray(s))throw new a('douyin hashtag hot: API returned malformed "hotspot_list"');if(o!==void 0&&o!==null&&!Array.isArray(o))throw new a('douyin hashtag hot: API returned malformed "all_sentences"');const h=Array.isArray(s)?s:Array.isArray(o)?o.map((e)=>({sentence:e?.word??"",hot_value:e?.hot_value,sentence_id:e?.sentence_id??""})):null;if(!h)throw new a("热点响应缺少候选数组,不能视为空结果");return h.slice(0,n.limit).map((e)=>({name:e?.sentence??"",id:e&&"sentence_id"in e?e.sentence_id:"",view_count:e?.hot_value??null,publish_args:{hotspot:e?.sentence}}))}throw new l(`未知的 action: ${i}`)}});
@@ -1 +1 @@
1
- import{cli as r,Strategy as a}from"@jackwener/opencli/registry";import{browserFetch as n}from"./_shared/browser-fetch.js";r({site:"douyin",name:"location",access:"read",description:"地理位置 POI 搜索",domain:"creator.douyin.com",strategy:a.COOKIE,args:[{name:"query",required:!0,positional:!0,help:"地名关键词"},{name:"limit",type:"int",default:20}],columns:["poi_id","name","address","city"],func:async(o,i)=>{const t=`https://creator.douyin.com/aweme/v1/life/video_api/search/poi/?keyword=${encodeURIComponent(i.query)}&count=${i.limit}&aid=1128`;return((await n(o,"GET",t)).poi_list??[]).map((e)=>({poi_id:e.poi_id,name:e.poi_name,address:e.address,city:e.city_name}))}});
1
+ import{ArgumentError as t,CommandExecutionError as n}from"@jackwener/opencli/errors";import{cli as m,Strategy as s}from"@jackwener/opencli/registry";import{browserFetch as d}from"./_shared/browser-fetch.js";m({site:"douyin",name:"location",access:"read",description:"搜索发布地点;ui_args 用于图文/视频草稿,api_args 用于 publish,重名地点按完整地址区分",domain:"creator.douyin.com",strategy:s.COOKIE,args:[{name:"query",required:!0,positional:!0,help:"地名关键词"},{name:"limit",type:"int",default:20},{name:"page",type:"int",default:1,help:"结果页码,从1开始"}],columns:["poi_id","name","address","city","has_more","next_page","ui_args","api_args"],func:async(a,e)=>{if(!String(e.query||"").trim()||!Number.isInteger(e.limit??20)||(e.limit??20)<1||(e.limit??20)>100||!Number.isInteger(e.page??1)||(e.page??1)<1)throw new t("请输入地点关键词、有效 limit(1至100) 和 page(>=1)");const r=`https://creator.douyin.com/aweme/v1/life/video_api/search/poi/?keywords=${encodeURIComponent(e.query)}&count=${e.limit??20}&page=${e.page??1}&from_webapp=1&aid=1128`,o=await d(a,"GET",r);if(!Array.isArray(o.poi_list))throw new n("地点响应缺少 poi_list,不能视为空结果");return o.poi_list.slice(0,e.limit??20).map((i)=>({poi_id:i.poi_id,name:i.poi_name,address:i.address_info?.simple_addr??i.address,city:i.address_info?.city??i.city_name,has_more:Boolean(o.has_more),next_page:o.has_more?o.page??(e.page??1)+1:null,ui_args:{location:i.poi_name,"location-address":i.address_info?.simple_addr??i.address},api_args:{poi_id:i.poi_id,poi_name:i.poi_name}}))}});
@@ -0,0 +1 @@
1
+ import{cli as i,Strategy as t}from"@jackwener/opencli/registry";import{musicCategories as o}from"./music.js";i({site:"douyin",name:"music-categories",access:"read",domain:"creator.douyin.com",strategy:t.COOKIE,description:"查看推荐、收藏、榜单与音乐分类;将 name 传给 douyin music --category",args:[],columns:["id","name","type"],func:async(a)=>(await o(a)).map((e)=>({id:String(e.category_id),name:e.category_name,type:e.type}))});
@@ -0,0 +1 @@
1
+ import{cli as p,Strategy as d}from"@jackwener/opencli/registry";import{ArgumentError as s,CommandExecutionError as h}from"@jackwener/opencli/errors";import{browserFetch as c}from"./_shared/browser-fetch.js";export function requireMusicList(i,t){if(!Array.isArray(i?.[t]))throw new h(`配乐响应缺少 ${t},不能视为空列表`);return i[t]}export async function musicCategories(i){return requireMusicList(await c(i,"GET","https://creator.douyin.com/web/api/media/music/category"),"categories")}p({site:"douyin",name:"music",access:"read",domain:"creator.douyin.com",strategy:d.COOKIE,description:"搜索/查看发布配乐,返回试听地址、作者、时长及 draft/publish-image 可直接使用的 publish_args;视频仅标记音乐信息",args:[{name:"query",positional:!0,help:"音乐关键词;不传时查看 --category(默认推荐)"},{name:"category",help:"分类完整名称,先用 music-categories 查看;与 query 二选一"},{name:"cursor",default:"0",help:"上一页返回的 next_cursor;默认第一页"},{name:"limit",type:"int",default:20,help:"每页1至20首,后续用 cursor 翻页"}],columns:["id","title","author","duration","uses","preview_url","next_cursor","has_more","publish_args"],func:async(i,t)=>{const u=t.limit??20;if(!Number.isInteger(u)||u<1||u>20)throw new s("--limit 必须为1至20的整数");if(!/^\d+$/.test(String(t.cursor??"0")))throw new s("--cursor 必须为返回的非负整数游标");if(t.query&&t.category)throw new s("query 与 --category 二选一");let o,n,m;if(t.query){const r=await c(i,"GET","https://creator.douyin.com/web/api/media/aweme/search/post/auth");if(!r.signature)throw new h("平台未返回音乐搜索授权");const e=new URLSearchParams({aid:"1128",count:String(u),search_source:"normal_search",keyword:t.query,cursor:String(t.cursor??"0")});o=await c(i,"GET","https://tsearch.amemv.com/openapi/aweme/v1/music/search/?"+e,{credentials:"omit",headers:{"Agw-Auth":r.signature,"Openapi-Omit-Shark":"1"}});n=requireMusicList(o,"music");m={"music-query":t.query}}else{const r=t.category||"推荐",e=(await musicCategories(i)).filter((a)=>a.category_name===r);if(e.length!==1)throw new s("找不到唯一音乐分类,请先运行 douyin music-categories");const l=e[0],y=new URLSearchParams({category_id:l.category_id,type:l.type,count:String(u),cursor:String(t.cursor??"0")});o=await c(i,"GET","https://creator.douyin.com/web/api/media/music/list?"+y);n=requireMusicList(o,"songs").map((a)=>({...a,id_str:a.music_id,title:a.music_name,author:a.music_author}));m={"music-category":r}}return n.map((r)=>{const e=r.id_str??r.id;if(!e||typeof e==="number"&&!Number.isSafeInteger(e)||!r.title||!r.author)throw new h("配乐缺少精确 ID、标题或作者,停止返回不可用候选");return{id:String(e),title:r.title,author:r.author,duration:r.duration??null,uses:r.user_count??null,preview_url:typeof r.play_url==="string"?r.play_url:r.play_url?.url_list?.[0]||"",next_cursor:String(o.cursor??""),has_more:Boolean(o.has_more),publish_args:{music:r.title,"music-author":r.author,"music-id":String(e),...m}}})}});
@@ -1,18 +1,18 @@
1
- import*as b from"node:fs";import*as y from"node:path";import{cli as k,Strategy as q}from"@jackwener/opencli/registry";import{ArgumentError as a,CommandExecutionError as u}from"@jackwener/opencli/errors";import{assertLiteralContent as j}from"../_shared/content-guard.js";import{setInputFiles as B}from"../_shared/file-inject.js";import{browserFetch as F}from"./_shared/browser-fetch.js";import{READ_IMAGE_DRAFT_JS as D,matchImageDraft as J}from"./_shared/draft-cache.js";const G="https://creator.douyin.com/creator-micro/content/upload?default-tab=3",A=35,C=20,L=1000,X=2500,V={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".bmp":"image/bmp",".webp":"image/webp",".tif":"image/tiff",".tiff":"image/tiff"},O='input[type="file"][accept*="image"]',R=['input[placeholder="添加作品标题"]','input[placeholder*="标题"]'],z='input[placeholder="添加作品标题"]',I='.zone-container[contenteditable="true"]';function Y(t){return t.map((n)=>{const e=y.resolve(n);if(!b.existsSync(e))throw new a(`图片文件不存在: ${e}`);const r=y.extname(e).toLowerCase();if(!V[r])throw new a(`不支持的图片格式 "${r}",支持: jpg, png, bmp, webp, tif`);return e})}async function H(t,n=15000){const e=500,r=Math.max(1,Math.ceil(n/e));for(let o=0;o<r;o++){if(await t.evaluate(`(() => !!document.querySelector(${JSON.stringify(O)}))()`))return!0;if(o<r-1)await t.wait({time:e/1000})}return!1}async function K(t,n){if(!await H(t))return{ok:!1,error:"上传页未渲染出图片输入框(等待 15s 超时)"};try{await B(t,O,n);return{ok:!0}}catch(r){return{ok:!1,error:r instanceof Error?r.message:String(r)}}}async function W(t,n=20000){const e=500,r=Math.ceil(n/e);for(let o=0;o<r;o++){if(await t.evaluate(`(() => {
1
+ import{DOUYIN_UI_ARGS as q,validateDouyinSettings as D,setDouyinPublishSettings as j}from"./_shared/publish-settings.js";import*as b from"node:fs";import*as y from"node:path";import{cli as B,Strategy as F}from"@jackwener/opencli/registry";import{ArgumentError as a,CommandExecutionError as u}from"@jackwener/opencli/errors";import{assertLiteralContent as k}from"../_shared/content-guard.js";import{setInputFiles as J}from"../_shared/file-inject.js";import{browserFetch as G}from"./_shared/browser-fetch.js";import{READ_IMAGE_DRAFT_JS as X,matchImageDraft as V}from"./_shared/draft-cache.js";const Y="https://creator.douyin.com/creator-micro/content/upload?default-tab=3",A=35,I=20,O=1000,z=2500,H={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".bmp":"image/bmp",".webp":"image/webp",".tif":"image/tiff",".tiff":"image/tiff"},R='input[type="file"][accept*="image"]',C=['input[placeholder="添加作品标题"]','input[placeholder*="标题"]'],K='input[placeholder="添加作品标题"]',L='.zone-container[contenteditable="true"]';function W(t){return t.map((e)=>{const i=y.resolve(e);if(!b.existsSync(i))throw new a(`图片文件不存在: ${i}`);const r=y.extname(i).toLowerCase();if(!H[r])throw new a(`不支持的图片格式 "${r}",支持: jpg, png, bmp, webp, tif`);return i})}async function Q(t,e=15000){const i=500,r=Math.max(1,Math.ceil(e/i));for(let o=0;o<r;o++){if(await t.evaluate(`(() => !!document.querySelector(${JSON.stringify(R)}))()`))return!0;if(o<r-1)await t.wait({time:i/1000})}return!1}async function Z(t,e){if(!await Q(t))return{ok:!1,error:"上传页未渲染出图片输入框(等待 15s 超时)"};try{await J(t,R,e);return{ok:!0}}catch(r){return{ok:!1,error:r instanceof Error?r.message:String(r)}}}async function tt(t,e=20000){const i=500,r=Math.ceil(e/i);for(let o=0;o<r;o++){if(await t.evaluate(`(() => {
2
2
  const onEditor = location.href.includes('/content/post/image');
3
- const titleReady = ${JSON.stringify(R)}.some((s) => !!document.querySelector(s));
3
+ const titleReady = ${JSON.stringify(C)}.some((s) => !!document.querySelector(s));
4
4
  return onEditor && titleReady;
5
- })()`))return!0;await t.wait({time:e/1000})}return!1}async function Q(t,n){await t.typeText(z,n);await t.wait({time:0.3});const e=await t.evaluate(`
5
+ })()`))return!0;await t.wait({time:i/1000})}return!1}async function et(t,e){await t.typeText(K,e);await t.wait({time:0.3});const i=await t.evaluate(`
6
6
  ((selectors) => {
7
7
  const el = selectors.map((s) => document.querySelector(s)).find(Boolean);
8
8
  return el ? (el.value || '') : '';
9
- })(${JSON.stringify(R)})
10
- `);if(e.trim()!==n){await t.screenshot({path:"/tmp/douyin_image_title_debug.png"});throw new u(`填写标题失败,期望 "${n}",实际 "${e}"。截图: /tmp/douyin_image_title_debug.png`)}}async function Z(t,n){await t.typeText(I,n);await t.wait({time:0.5});const e=await t.evaluate(`
9
+ })(${JSON.stringify(C)})
10
+ `);if(i.trim()!==e){await t.screenshot({path:"/tmp/douyin_image_title_debug.png"});throw new u(`填写标题失败,期望 "${e}",实际 "${i}"。截图: /tmp/douyin_image_title_debug.png`)}}async function it(t,e){await t.typeText(L,e);await t.wait({time:0.5});const i=await t.evaluate(`
11
11
  ((selector) => {
12
12
  const el = Array.from(document.querySelectorAll(selector)).find((n) => n && n.offsetParent !== null);
13
13
  return el ? (el.innerText || el.textContent || '').replace(/\\u200b/g, '').trim() : '';
14
- })(${JSON.stringify(I)})
15
- `),r=n.split("#")[0].replace(/\s+/g,"").trim(),o=e.replace(/\s+/g,"");if(r&&!o.includes(r.slice(0,Math.min(r.length,20)))){await t.screenshot({path:"/tmp/douyin_image_body_debug.png"});throw new u(`正文写入校验失败,实际读到 "${e.slice(0,80)}"。截图: /tmp/douyin_image_body_debug.png`)}}async function tt(t,n){const e=n?["暂存离开"]:["发布"];return await t.evaluate(`
14
+ })(${JSON.stringify(L)})
15
+ `),r=e.split("#")[0].replace(/\s+/g,"").trim(),o=i.replace(/\s+/g,"");if(r&&!o.includes(r.slice(0,Math.min(r.length,20)))){await t.screenshot({path:"/tmp/douyin_image_body_debug.png"});throw new u(`正文写入校验失败,实际读到 "${i.slice(0,80)}"。截图: /tmp/douyin_image_body_debug.png`)}}async function nt(t,e){const i=e?["暂存离开"]:["发布"];return await t.evaluate(`
16
16
  ((labels) => {
17
17
  const isVisible = (el) => {
18
18
  if (!el || el.offsetParent === null) return false;
@@ -27,8 +27,8 @@ import*as b from"node:fs";import*as y from"node:path";import{cli as k,Strategy a
27
27
  if (btn) { btn.click(); return { ok: true, text: label }; }
28
28
  }
29
29
  return { ok: false, seen: btns.filter(isVisible).map((b) => norm(b.innerText || b.textContent)).filter(Boolean).slice(0, 12) };
30
- })(${JSON.stringify(e)})
31
- `)}k({site:"douyin",name:"publish-image",access:"write",description:"发布图文(图集)到抖音(UI 自动化,最多 35 张图;话题以 #词 写进正文)",domain:"creator.douyin.com",strategy:q.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(t,n)=>{if(!t)throw Error("Browser page required");const e=String(n.title??"").trim(),r=n.file?String(n.file).trim():"";let o=String(n.content??"").trim();if(r){if(o)throw new a("正文位置参数和 --file 只能二选一");const i=y.resolve(r);if(!b.existsSync(i))throw new a(`--file 指向的文件不存在: ${i}`);o=b.readFileSync(i,"utf-8").trim();if(!o)throw new a(`--file 指向的文件内容为空: ${i}`)}const m=String(n.images??"").split(",").map((i)=>i.trim()).filter(Boolean),p=n.topics?String(n.topics).split(",").map((i)=>i.trim().replace(/^#/,"")).filter(Boolean):[],c=Boolean(n.draft);if(!e)throw new a("--title 必填");if(e.length>C)throw new a(`标题 ${e.length} 字,需 ≤ ${C} 字`);if(!o)throw new a("缺少正文:给位置参数 <content>,或用 --file <本机文件路径>");j(o,{fileFlag:"--file"});if(o.length>L)throw new a(`正文 ${o.length} 字,需 ≤ ${L} 字`);if(m.length===0)throw new a("至少提供 1 张图片(--images)");if(m.length>A)throw new a(`图片过多: ${m.length}(最多 ${A} 张)`);const _=Y(m),M=p.length?`${o} ${p.map((i)=>`#${i}`).join(" ")} `:o;await t.goto(G);let d="";for(let i=0;i<30;i++){await t.wait({time:0.5});d=await t.evaluate("() => location.href");if(d.includes("creator.douyin.com/creator-micro/content"))break}if(!d.includes("creator.douyin.com")){await t.screenshot({path:"/tmp/douyin_image_redirect_debug.png"});throw new u(`被重定向离开创作者中心(当前 ${d})——登录态可能已过期。`+"用 ppcli douyin login 重新登录。截图: /tmp/douyin_image_redirect_debug.png")}const S=await K(t,_);if(!S.ok){await t.screenshot({path:"/tmp/douyin_image_upload_debug.png"});throw new u(`图片上传失败: ${S.error??"unknown"}。截图: /tmp/douyin_image_upload_debug.png`)}if(!await W(t)){await t.screenshot({path:"/tmp/douyin_image_form_debug.png"});throw new u("上传后编辑页未就绪(标题输入框未出现),页面结构可能变化。截图: /tmp/douyin_image_form_debug.png")}await t.wait({time:X/1000});await Q(t,e);await t.wait({time:0.4});await Z(t,M);await t.wait({time:0.6});const E=await tt(t,c);if(!E?.ok){await t.screenshot({path:"/tmp/douyin_image_submit_debug.png"});throw new u(`找不到「${c?"暂存离开":"发布"}」按钮。可见按钮: ${(E?.seen||[]).join(" | ")||"无"}。截图: /tmp/douyin_image_submit_debug.png`)}const N=c?["暂存成功","保存成功","存入草稿"]:["发布成功","提交成功"];let l=d,w=!1,g="";for(let i=0;i<24;i++){await t.wait({time:0.5});const s=await t.evaluate(`
30
+ })(${JSON.stringify(i)})
31
+ `)}B({site:"douyin",name:"publish-image",access:"write",description:"发布图文(图集)到抖音(UI 自动化,最多 35 张图;话题以 #词 写进正文)",domain:"creator.douyin.com",strategy:F.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:"暂存为草稿,不直接发布"},{name:"ai-generated",type:"boolean",help:"声明内容由 AI 生成(发布和草稿均支持;设置失败时停止提交)"},...q,{name:"visibility",choices:["public","friends","private"],help:"可见范围(不传保留平台默认)"}],columns:["status","detail"],func:async(t,e)=>{D(e);if(!t)throw Error("Browser page required");const i=String(e.title??"").trim(),r=e.file?String(e.file).trim():"";let o=String(e.content??"").trim();if(r){if(o)throw new a("正文位置参数和 --file 只能二选一");const n=y.resolve(r);if(!b.existsSync(n))throw new a(`--file 指向的文件不存在: ${n}`);o=b.readFileSync(n,"utf-8").trim();if(!o)throw new a(`--file 指向的文件内容为空: ${n}`)}const m=String(e.images??"").split(",").map((n)=>n.trim()).filter(Boolean),p=e.topics?String(e.topics).split(",").map((n)=>n.trim().replace(/^#/,"")).filter(Boolean):[],c=Boolean(e.draft);if(!i)throw new a("--title 必填");if(i.length>I)throw new a(`标题 ${i.length} 字,需 ≤ ${I} 字`);if(!o)throw new a("缺少正文:给位置参数 <content>,或用 --file <本机文件路径>");k(o,{fileFlag:"--file"});if(o.length>O)throw new a(`正文 ${o.length} 字,需 ≤ ${O} 字`);if(m.length===0)throw new a("至少提供 1 张图片(--images)");if(m.length>A)throw new a(`图片过多: ${m.length}(最多 ${A} 张)`);const _=W(m),N=p.length?`${o} ${p.map((n)=>`#${n}`).join(" ")} `:o;await t.goto(Y);let d="";for(let n=0;n<30;n++){await t.wait({time:0.5});d=await t.evaluate("() => location.href");if(d.includes("creator.douyin.com/creator-micro/content"))break}if(!d.includes("creator.douyin.com")){await t.screenshot({path:"/tmp/douyin_image_redirect_debug.png"});throw new u(`被重定向离开创作者中心(当前 ${d})——登录态可能已过期。`+"用 ppcli douyin login 重新登录。截图: /tmp/douyin_image_redirect_debug.png")}const S=await Z(t,_);if(!S.ok){await t.screenshot({path:"/tmp/douyin_image_upload_debug.png"});throw new u(`图片上传失败: ${S.error??"unknown"}。截图: /tmp/douyin_image_upload_debug.png`)}if(!await tt(t)){await t.screenshot({path:"/tmp/douyin_image_form_debug.png"});throw new u("上传后编辑页未就绪(标题输入框未出现),页面结构可能变化。截图: /tmp/douyin_image_form_debug.png")}await t.wait({time:z/1000});await et(t,i);await t.wait({time:0.4});await it(t,N);await t.wait({time:0.6});await j(t,e);const E=await nt(t,c);if(!E?.ok){await t.screenshot({path:"/tmp/douyin_image_submit_debug.png"});throw new u(`找不到「${c?"暂存离开":"发布"}」按钮。可见按钮: ${(E?.seen||[]).join(" | ")||"无"}。截图: /tmp/douyin_image_submit_debug.png`)}const M=c?["暂存成功","保存成功","存入草稿"]:["发布成功","提交成功"];let l=d,w=!1,g="";for(let n=0;n<24;n++){await t.wait({time:0.5});const s=await t.evaluate(`
32
32
  ((markers) => {
33
33
  const url = location.href;
34
34
  let toast = '';
@@ -39,7 +39,7 @@ import*as b from"node:fs";import*as y from"node:path";import{cli as k,Strategy a
39
39
  if (text && text.length <= 40 && markers.some((m) => text.includes(m))) { toast = text; break; }
40
40
  }
41
41
  return { url, toast };
42
- })(${JSON.stringify(N)})
43
- `);l=s?.url??l;if(s?.toast){g=s.toast;break}if(l.includes("/content/manage")||c&&l.includes("/content/draft")){w=!0;break}}let f=null;if(!w&&!g&&l.includes("creator.douyin.com"))try{if(c){const i=await t.evaluate(D),s=J(i,{title:e,content:o});f=s.hit?s:null}else{const s=await F(t,"GET","https://creator.douyin.com/janus/douyin/creator/pc/work_list?page_size=20&page_num=1&status=0"),P=s.aweme_list??s.data?.work_list??[],T=o.replace(/\s+/g,"").slice(0,20),U=Date.now()/1000;f=P.find((h)=>{const x=String(h.desc??h.title??"").replace(/\s+/g,""),v=Number(h.create_time??0);return v>0&&U-v<900&&(x.includes(e.replace(/\s+/g,""))||T&&x.includes(T))})??null}}catch{}if(!w&&!g&&!f){await t.screenshot({path:"/tmp/douyin_image_verify_debug.png"});throw new u(c?"暂存结果无法确认:未见成功提示,本机草稿缓存里也没找到本次内容——大概率没存下,重试是安全的。"+`当前 URL: ${l}。截图: /tmp/douyin_image_verify_debug.png`:"发布结果无法确认:未跳转作品管理页、未见成功提示,作品列表回查也没找到本次内容。"+`
42
+ })(${JSON.stringify(M)})
43
+ `);l=s?.url??l;if(s?.toast){g=s.toast;break}if(l.includes("/content/manage")||c&&l.includes("/content/draft")){w=!0;break}}let f=null;if(!w&&!g&&l.includes("creator.douyin.com"))try{if(c){const n=await t.evaluate(X),s=V(n,{title:i,content:o});f=s.hit?s:null}else{const s=await G(t,"GET","https://creator.douyin.com/janus/douyin/creator/pc/work_list?page_size=20&page_num=1&status=0"),U=s.aweme_list??s.data?.work_list??[],T=o.replace(/\s+/g,"").slice(0,20),P=Date.now()/1000;f=U.find((h)=>{const v=String(h.desc??h.title??"").replace(/\s+/g,""),x=Number(h.create_time??0);return x>0&&P-x<900&&(v.includes(i.replace(/\s+/g,""))||T&&v.includes(T))})??null}}catch{}if(!w&&!g&&!f){await t.screenshot({path:"/tmp/douyin_image_verify_debug.png"});throw new u(c?"暂存结果无法确认:未见成功提示,本机草稿缓存里也没找到本次内容——大概率没存下,重试是安全的。"+`当前 URL: ${l}。截图: /tmp/douyin_image_verify_debug.png`:"发布结果无法确认:未跳转作品管理页、未见成功提示,作品列表回查也没找到本次内容。"+`
44
44
  ⚠️ 禁止直接重试——发布请求可能已经打到抖音,重试会发出重复作品。`+"\n正确做法:先用 `ppcli douyin videos` 按标题回查最近作品,确认没有再重发。"+`
45
- 当前 URL: ${l}。截图: /tmp/douyin_image_verify_debug.png`)}return[{status:`✅ ${c?"暂存成功":"发布成功"}`,detail:[`"${e}"`,`${_.length}张图片`,p.length?`话题: ${p.map((i)=>`#${i}`).join(" ")}`:"",f?f.aweme_id?`aweme_id: ${f.aweme_id}(作品列表回查坐实)`:"本机草稿缓存回查坐实":"",w?l:g].filter(Boolean).join(" · ")}]}});
45
+ 当前 URL: ${l}。截图: /tmp/douyin_image_verify_debug.png`)}return[{status:`✅ ${c?"暂存成功":"发布成功"}`,detail:[`"${i}"`,`${_.length}张图片`,p.length?`话题: ${p.map((n)=>`#${n}`).join(" ")}`:"",f?f.aweme_id?`aweme_id: ${f.aweme_id}(作品列表回查坐实)`:"本机草稿缓存回查坐实":"",w?l:g].filter(Boolean).join(" · ")}]}});
@@ -1,14 +1,15 @@
1
- import*as T from"node:fs";import*as u from"node:path";import{cli as Z,Strategy as Q}from"@jackwener/opencli/registry";import{ArgumentError as f,CommandExecutionError as m}from"@jackwener/opencli/errors";import{getUploadAuthV5Credentials as ee,applyVideoUploadInner as te,commitVideoUploadInner as oe}from"./_shared/vod-upload.js";import{tosUpload as ie}from"./_shared/tos-upload.js";import{imagexUpload as se}from"./_shared/imagex-upload.js";import{browserFetch as R}from"./_shared/browser-fetch.js";import{requireObjectEvaluateResult as V}from"./_shared/evaluate-result.js";import{generateCreationId as re}from"./_shared/creation-id.js";import{validateTiming as ne,toUnixSeconds as ae}from"./_shared/timing.js";import{parseTextExtra as ce,extractHashtagNames as le}from"./_shared/text-extra.js";const de={public:0,friends:1,private:2},M="https://imagex.bytedanceapi.com",z="1147",me="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",pe=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 _e(o){const e=o instanceof Error?o.message:String(o);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 A(o){return new Promise((e)=>setTimeout(e,o))}function j(o,e){const n=e?.ResponseMetadata?.Error??e?.Error;if(n)throw new m(`${o}失败: ${JSON.stringify(n)}`)}async function B(o,e,n,h){let y;for(let r=1;r<=3;r+=1)try{return{ok:!0,value:await R(o,e,n,h)}}catch(i){if(!_e(i))throw i;y=i;if(r<3)await A(500*r)}return{ok:!1,error:y}}Z({site:"douyin",name:"publish",access:"write",description:"定时发布视频到抖音(必须设置 2h ~ 14天后的发布时间)",domain:"creator.douyin.com",strategy:Q.COOKIE,defaultWindowMode:"foreground",args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"视频标题(≤30字)"},{name:"schedule",required:!0,help:"定时发布时间(ISO8601 或 Unix 秒,2h ~ 14天后)"},{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:"同步发布到头条"}],columns:["status","aweme_id","url","publish_time"],func:async(o,e)=>{const n=u.resolve(e.video);if(!T.existsSync(n))throw new f(`视频文件不存在: ${n}`);const h=u.extname(n).toLowerCase();if(![".mp4",".mov",".avi",".webm"].includes(h))throw new f(`不支持的视频格式: ${h}(支持 mp4/mov/avi/webm)`);const y=T.statSync(n).size,r=e.title;if(r.length>30)throw new f("标题不能超过 30 字");const i=e.caption||"";if(i.length>1000)throw new f("正文不能超过 1000 字");const g=ae(e.schedule);ne(g);const H=de[e.visibility]??0,x=e.cover;if(x){if(!T.existsSync(u.resolve(x)))throw new f(`封面文件不存在: ${u.resolve(x)}`)}const I=await ee(o),C=await te(y,I);let v="",U=720,O=1280;await ie({filePath:n,uploadInfo:C,credentials:I,onProgress:(t,s)=>{const a=Math.round(t/s*100);process.stderr.write(`\r 上传进度: ${a}%`)}});process.stderr.write(`
1
+ import{DOUYIN_DECLARATION_ARGS as k,douyinDeclarationInfo as ee}from"./_shared/publish-settings.js";import*as T from"node:fs";import*as v from"node:path";import{cli as te,Strategy as oe}from"@jackwener/opencli/registry";import{ArgumentError as _,CommandExecutionError as u}from"@jackwener/opencli/errors";import{getUploadAuthV5Credentials as ie,applyVideoUploadInner as se,commitVideoUploadInner as ne}from"./_shared/vod-upload.js";import{tosUpload as re}from"./_shared/tos-upload.js";import{imagexUpload as ae}from"./_shared/imagex-upload.js";import{browserFetch as V}from"./_shared/browser-fetch.js";import{requireObjectEvaluateResult as M}from"./_shared/evaluate-result.js";import{generateCreationId as ce}from"./_shared/creation-id.js";import{validateTiming as le,toUnixSeconds as de}from"./_shared/timing.js";import{parseTextExtra as me,extractHashtagNames as pe}from"./_shared/text-extra.js";const _e={public:0,friends:2,private:1},z="https://imagex.bytedanceapi.com",L="1147",ue="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",fe=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 he(o){const e=o instanceof Error?o.message:String(o);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 C(o){return new Promise((e)=>setTimeout(e,o))}function j(o,e){const m=e?.ResponseMetadata?.Error??e?.Error;if(m)throw new u(`${o}失败: ${JSON.stringify(m)}`)}async function B(o,e,m,r){let f;for(let p=1;p<=3;p+=1)try{return{ok:!0,value:await V(o,e,m,r)}}catch(s){if(!he(s))throw s;f=s;if(p<3)await C(500*p)}return{ok:!1,error:f}}te({site:"douyin",name:"publish",access:"write",description:"发布视频到抖音(指定 --schedule 定时,或显式 --immediate 立即发布)",domain:"creator.douyin.com",strategy:oe.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 生成(可选,随定时发布请求提交)"},...k],columns:["status","aweme_id","url","publish_time"],func:async(o,e)=>{const m=ee(e);if(!!e.immediate===!!e.schedule)throw new _("必须指定 --schedule 或 --immediate,且只能选择一个");const r=v.resolve(e.video);if(!T.existsSync(r))throw new _(`视频文件不存在: ${r}`);const f=v.extname(r).toLowerCase();if(![".mp4",".mov",".avi",".webm"].includes(f))throw new _(`不支持的视频格式: ${f}(支持 mp4/mov/avi/webm)`);const p=T.statSync(r).size,s=e.title;if(s.length>30)throw new _("标题不能超过 30 字");const n=e.caption||"";if(n.length>1000)throw new _("正文不能超过 1000 字");const x=e.immediate?0:de(e.schedule);if(!e.immediate)le(x);const H=_e[e.visibility]??0,U=e.cover;if(U){if(!T.existsSync(v.resolve(U)))throw new _(`封面文件不存在: ${v.resolve(U)}`)}const O=await ie(o),D=await se(p,O);let w="",g=720,E=1280;await re({filePath:r,uploadInfo:D,credentials:O,onProgress:(t,i)=>{const a=Math.round(t/i*100);process.stderr.write(`\r 上传进度: ${a}%`)}});process.stderr.write(`
2
2
  `);process.stderr.write(` 提交上传...
3
- `);const p=await oe(C,I),E=p.video_id;process.stderr.write(` 上传已提交: ${E}
4
- `);U=p.width||U;O=p.height||O;if(!v&&p.poster_uri)v=p.poster_uri;if(e.cover){const t=u.resolve(e.cover),s=`${M}/?Action=ApplyImageUpload&ServiceId=${z}&Version=2018-08-01&UploadNum=1`,a=`fetch(${JSON.stringify(s)}, { credentials: 'include' }).then(r => r.json())`,_=V(await o.evaluate(a),"抖音封面申请上传地址响应异常");j("抖音封面申请上传地址",_);const c=_.Result?.UploadAddress?.StoreInfos?.[0];if(!c?.UploadHost||!c?.StoreUri)throw new m(`抖音封面申请上传地址响应缺少 UploadHost/StoreUri: ${JSON.stringify(_).slice(0,500)}`);const S=`https://${c.UploadHost}/${c.StoreUri}`,l=await se(t,{upload_url:S,store_uri:c.StoreUri}),b=`${M}/?Action=CommitImageUpload&ServiceId=${z}&Version=2018-08-01`,d=JSON.stringify({SuccessObjKeys:[l]}),X=`
5
- fetch(${JSON.stringify(b)}, {
3
+ `);const h=await ne(D,O),A=h.video_id;process.stderr.write(` 上传已提交: ${A}
4
+ `);g=h.width||g;E=h.height||E;if(!w&&h.poster_uri)w=h.poster_uri;if(e.cover){const t=v.resolve(e.cover),i=`${z}/?Action=ApplyImageUpload&ServiceId=${L}&Version=2018-08-01&UploadNum=1`,a=`fetch(${JSON.stringify(i)}, { credentials: 'include' }).then(r => r.json())`,y=M(await o.evaluate(a),"抖音封面申请上传地址响应异常");j("抖音封面申请上传地址",y);const c=y.Result?.UploadAddress?.StoreInfos?.[0];if(!c?.UploadHost||!c?.StoreUri)throw new u(`抖音封面申请上传地址响应缺少 UploadHost/StoreUri: ${JSON.stringify(y).slice(0,500)}`);const b=`https://${c.UploadHost}/${c.StoreUri}`,l=await ae(t,{upload_url:b,store_uri:c.StoreUri}),I=`${z}/?Action=CommitImageUpload&ServiceId=${L}&Version=2018-08-01`,d=JSON.stringify({SuccessObjKeys:[l]}),Z=`
5
+ fetch(${JSON.stringify(I)}, {
6
6
  method: 'POST',
7
7
  credentials: 'include',
8
8
  headers: { 'Content-Type': 'application/json' },
9
9
  body: ${JSON.stringify(d)}
10
10
  }).then(r => r.json())
11
- `,Y=V(await o.evaluate(X),"抖音封面提交上传响应异常");j("抖音封面提交上传",Y);v=l}if(!e.no_safety_check){const t="https://creator.douyin.com/aweme/v1/post_assistant/fast_detect/pre_check",s={video_id:E,title:r,desc:i},a=await B(o,"POST",t,{body:s});if(!a.ok)process.stderr.write(` 内容安全预检接口无响应,继续轮询检测结果。
12
- `);const _="https://creator.douyin.com/aweme/v1/post_assistant/fast_detect/poll",c=Date.now()+30000;let S=!1,l=0;while(Date.now()<c){const b=await B(o,"POST",_,{body:s});if(!b.ok){l+=1;if(!a.ok&&l>=3)break;await A(2000);continue}l=0;const d=b.value;if(d.status===0||d.has_done===!0&&d.detect_result?.reason_code===0&&(d.detect_list?.length??0)===0){S=!0;break}if(d.status===1)throw new m("内容安全检测不通过,请修改后重试","使用 --no_safety_check 跳过");await A(2000)}if(!S)if(!a.ok&&l>=3)process.stderr.write(` 内容安全预检持续无响应,跳过本地预检,交由 create_v2 后的平台审核。
13
- `);else throw new m("内容安全检测超时(30s),请稍后重试","如确认要跳过本地预检,可使用 --no_safety_check;提交后仍会走抖音平台审核")}const L=le(i),J=[];let P=0;for(const t of L){const s=i.indexOf(`#${t}`,P);if(s===-1)continue;J.push({name:t,id:0,start:s,end:s+t.length+1});P=s+t.length+1}const F=i?`${r} ${i}`:r,D=i?r.length+1:0,q=ce(F,J.map((t)=>({...t,start:t.start+D,end:t.end+D}))),k={item:{common:{text:F,caption:i,item_title:r,activity:JSON.stringify(e.activity?[e.activity]:[]),text_extra:JSON.stringify(q),challenges:"[]",mentions:"[]",hashtag_source:"",hot_sentence:e.hotspot||"",interaction_stickers:"[]",visibility_type:H,download:e.allow_download?1:0,timing:g,creation_id:re(),media_type:4,video_id:E,music_source:0,music_id:null,...e.poi_id?{poi_id:e.poi_id,poi_name:e.poi_name}:{}},cover:{poster:v,custom_cover_image_height:O,custom_cover_image_width:U,poster_delay:0,cover_tools_info:pe,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:"{}"}}},G=`https://creator.douyin.com/web/api/media/aweme/create_v2/?read_aid=2906&${me}`;process.stderr.write(` 创建定时发布...
14
- `);let w;try{w=await R(o,"POST",G,{body:k})}catch(t){if((t instanceof Error?t.message:String(t)).includes("Failed to fetch"))throw new m("提交定时发布请求被浏览器侧拦截(疑似字节安全组件 secsdk)","不要直接重试:先用 `ppcli douyin videos` / 创作者中心确认作品是否已创建,避免重复投稿。"+"确认未创建后,用 `--window foreground` 前台窗口重试(视频已上传成功的部分无需重传)。");throw t}const N=w.aweme_id??w.item_id;if(!N)throw new m(`发布成功但未返回 aweme_id/item_id: ${JSON.stringify(w)}`);const K=`https://www.douyin.com/video/${N}`,W=new Date(g*1000).toLocaleString("zh-CN",{timeZone:"Asia/Tokyo"});return[{status:"✅ 定时发布成功!",aweme_id:N,url:K,publish_time:W}]}});
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:A,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 b=!1,l=0;while(Date.now()<c){const I=await B(o,"POST",y,{body:i});if(!I.ok){l+=1;if(!a.ok&&l>=3)break;await C(2000);continue}l=0;const d=I.value;if(d.status===0||d.has_done===!0&&d.detect_result?.reason_code===0&&(d.detect_list?.length??0)===0){b=!0;break}if(d.status===1)throw new u("内容安全检测不通过,请修改后重试","使用 --no_safety_check 跳过");await C(2000)}if(!b)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:x,creation_id:ce(),media_type:4,video_id:A,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:E,custom_cover_image_width:g,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?` 创建发布...
14
+ `:` 创建定时发布...
15
+ `);let S;try{S=await V(o,"POST",W,{body:K})}catch(t){if((t instanceof Error?t.message:String(t)).includes("Failed to fetch"))throw new u("提交发布请求被浏览器侧拦截(疑似字节安全组件 secsdk)","不要直接重试:先用 `ppcli douyin videos` / 创作者中心确认作品是否已创建,避免重复投稿。"+"确认未创建后,用 `--window foreground` 前台窗口重试(视频已上传成功的部分无需重传)。");throw t}const N=S.aweme_id??S.item_id;if(!N)throw new u(`发布成功但未返回 aweme_id/item_id: ${JSON.stringify(S)}`);const X=`https://www.douyin.com/video/${N}`,Y=e.immediate?"立即发布":new Date(x*1000).toLocaleString("zh-CN",{timeZone:"Asia/Shanghai"});return[{status:e.immediate?"✅ 发布请求已提交":"✅ 定时发布成功!",aweme_id:N,url:X,publish_time:Y}]}});
@@ -0,0 +1 @@
1
+ import{CommandExecutionError as d}from"@jackwener/opencli/errors";export function xhsAiDeclarationStep(r,l="笔记含AI合成内容"){const c=(e)=>e.getClientRects().length>0&&getComputedStyle(e).visibility!=="hidden",o=(e)=>(e.textContent||"").replace(/\s+/g,""),n=[...document.querySelectorAll(".d-select")].filter(c);if(n.filter((e)=>o(e.querySelector(".d-select-description")||e)===l).length===1)return{selected:!0};if(r==="verify")return{selected:!1};if(r==="open"){const e=n.filter((s)=>o(s.querySelector(".d-select-placeholder")||s)==="添加内容类型声明");if(e.length!==1)return{error:"未找到唯一的内容类型声明控件,或已有其他声明"};return{selector:".d-select",nth:[...document.querySelectorAll(".d-select")].indexOf(e[0])}}const t=[...document.querySelectorAll(".declaration-drop-down .d-option-content")].filter((e)=>c(e)&&o(e)===l);if(t.length!==1)return{error:"未找到唯一的“笔记含AI合成内容”选项"};return{selector:".declaration-drop-down .d-option-content",nth:[...document.querySelectorAll(".declaration-drop-down .d-option-content")].indexOf(t[0])}}export async function setXhsAiDeclaration(r,l="笔记含AI合成内容"){const c=(t)=>r.evaluate(`(${xhsAiDeclarationStep.toString()})(${JSON.stringify(t)}, ${JSON.stringify(l)})`),o=(t)=>{throw new d(`小红书 AI 内容声明设置失败:${t};已停止提交`)},n=await c("open");if(n?.selected)return;if(!n?.selector)o(n?.error||"声明控件响应异常");await r.click(n.selector,{nth:n.nth});await r.wait({time:0.2});const i=await c("select");if(i?.selected)return;if(!i?.selector)o(i?.error||"声明选项响应异常");await r.click(i.selector,{nth:i.nth});for(let t=0;t<10;t++){if((await c("verify"))?.selected)return;await r.wait({time:0.2})}o("选择后未回显“笔记含AI合成内容”")}
@@ -0,0 +1,19 @@
1
+ import{ArgumentError as s,AuthRequiredError as u,CommandExecutionError as n}from"@jackwener/opencli/errors";const a="https://creator.xiaohongshu.com",c="https://edith.xiaohongshu.com",p={topics:["post",a+"/api/galaxy/v2/creator/servicegw/v2/search/topics"],activities:["get","EVENTS_LIST"],location:["post",c+"/web_api/sns/v1/local/poi/creator/search"],collections:["post",c+"/api/sns/v1/note/collection/pc/list_v2"],groups:["get",c+"/api/im/web/nns/group_list"]};export function discoveryLimit(t=20){if(!Number.isInteger(t)||t<1||t>100)throw new s("--limit 必须为 1 至 100 的整数");return t}export function listField(t,e){if(!t||!Array.isArray(t[e]))throw new n(`发布候选响应缺少 ${e} 数组,不能视为空列表`);return t[e]}export async function prepareCreatorDiscovery(t){await t.goto(a+"/publish/publish?from=homepage&target=image");for(let e=0;e<30;e++){const o=await t.evaluate(`({ready:!!window.webpackChunkugc && !!document.querySelector('input[type="file"]'),login:/\\/login(?:[/?]|$)/.test(location.href) || !!document.querySelector('input[type="password"]')})`);if(o?.login)throw new u("creator.xiaohongshu.com","请先登录小红书创作服务平台");if(o?.ready)return;await t.wait({time:0.5})}throw new n("小红书发布查询运行时未就绪")}export async function creatorDiscoveryRequest(t,e,o){const i=p[e];if(!i)throw new s("未知发布候选类型: "+e);const r=await t.evaluate(`(async ({method,url,params})=>{
2
+ try {
3
+ let runtime;
4
+ if (!window.webpackChunkugc) throw new Error('创作者运行时未加载');
5
+ window.webpackChunkugc.push([['ppcli-discovery-'+Date.now()],{},req=>{runtime=req}]);
6
+ // Identify the native HTTP client by its exported contract, not a
7
+ // build-specific numeric module ID. Do not execute unrelated modules.
8
+ const modules=Object.entries(runtime.m).filter(([,factory])=>{
9
+ const source=factory.toString();
10
+ return source.includes('provideUseHttp') && source.includes('LV:') && source.includes('vF:');
11
+ });
12
+ if(modules.length!==1) throw new Error('无法定位唯一的创作者 HTTP 客户端');
13
+ const client=runtime(modules[0][0]).LV;
14
+ if(typeof client?.get!=='function'||typeof client?.post!=='function') throw new Error('创作者 HTTP 客户端接口已变化');
15
+ const options={transform:true,withCredentials:true,timeout:20000};
16
+ const data=method==='get' ? await client.get(url,{...options,params}) : await client.post(url,params,options);
17
+ return {ok:true,data};
18
+ } catch(error) {return {ok:false,error:String(error?.message||error),code:error?.code};}
19
+ })(${JSON.stringify({method:i[0],url:i[1],params:o})})`);if(!r?.ok){if([401,403].includes(Number(r?.code)))throw new u("creator.xiaohongshu.com",r.error);throw new n(`小红书 ${e} 查询失败:${r?.error||"无响应"}${r?.code!==void 0?" (code="+r.code+")":""}`)}return r.data}export const activityQuery={sort:"1",type:"1",source:3,topicActivity:0};
@@ -0,0 +1 @@
1
+ import*as h from"node:fs";import*as s from"node:path";import{ArgumentError as l,CommandExecutionError as f}from"@jackwener/opencli/errors";import{clickControl as u,fillControl as m,verifyControl as c}from"../../_shared/publish-controls.js";export function readChapters(t){if(!t)return;let r;try{r=JSON.parse(h.readFileSync(s.resolve(t),"utf8"))}catch(e){throw new l(`无法读取 --chapters JSON 文件:${e.message}`)}if(!Array.isArray(r)||!r.length)throw new l("--chapters 应为非空数组 [{time:秒数,title:章节名,description:描述}]");let n=-1;for(const e of r){if(!e||!Number.isInteger(e.time)||e.time<=n||typeof e.title!=="string"||!e.title.trim()||[...e.title].length>11||e.description!==void 0&&(typeof e.description!=="string"||[...e.description].length>100))throw new l("章节需按非负整数秒严格递增,名称1至11字,描述最多100字");n=e.time}return r}export async function setXhsChapters(t,r,n){await u(t,".entry-right .add-chapter-btn");await c(t,"!!document.querySelector('.creator-chapter-content .summary-textarea')","章节编辑器");const e=await t.evaluate("document.querySelector('.creator-chapter-content video')?.duration");if(!Number.isFinite(e)||r?.some((i)=>i.time>=e))throw new l("章节时间必须小于视频总时长");if(r&&await t.evaluate("document.querySelectorAll('.creator-chapter-content .chapter-item').length > 0")){await u(t,".creator-chapter-content .chapter-list-header button","清空");await c(t,"document.querySelectorAll('.creator-chapter-content .chapter-item').length===0","清空旧章节")}if(n!==void 0)await m(t,".creator-chapter-content .summary-textarea",n);for(let i=0;i<(r?.length||0);i++){const o=r[i];await t.evaluate(`((time)=>{const video=document.querySelector('.creator-chapter-content video');video.pause();video.currentTime=time;})(${o.time})`);await t.wait({time:0.3});await u(t,".creator-chapter-content button","添加章节");await c(t,`document.querySelectorAll('.creator-chapter-content .chapter-item').length===${i+1}`,"新增章节");const a=`.creator-chapter-content .chapter-item:nth-child(${i+1})`;await c(t,`Number(document.querySelector(${JSON.stringify(a+" .minutes-field")})?.value) * 60 + Number(document.querySelector(${JSON.stringify(a+" .time-field:not(.minutes-field)")})?.value) === ${o.time}`,"章节时间");await m(t,a+" .title-field",o.title);if(o.description!==void 0)await m(t,a+" .description-field",o.description)}await u(t,".creator-chapter-content button","保存章节");await c(t,"!document.querySelector('.creator-chapter-content')","章节保存");const d=await t.evaluate("document.querySelector('.add-chapter-title')?.parentElement?.parentElement?.textContent || ''");if(r?.length&&!d.includes(String(r.length)))throw new f("章节数未正确回显;已停止提交")}
@@ -0,0 +1 @@
1
+ import{readChapters as v,setXhsChapters as x}from"./publish-editors.js";import*as s from"node:fs";import*as a from"node:path";import{setInputFiles as f}from"../../_shared/file-inject.js";import{ArgumentError as l,CommandExecutionError as m}from"@jackwener/opencli/errors";import{clickControl as c,control as w,fillControl as u,verifyControl as r,publicationTime as q,resolveDeclaration as b}from"../../_shared/publish-controls.js";async function y(t,e){const o=await w(t,".d-select",e);await t.evaluate(`(() => {const el=document.querySelectorAll('.d-select')[${o.nth}].closest('.d-select-wrapper'); el.focus();return true;})()`);await t.wait({time:0.2})}async function h(t,e,o){const n=await w(t,e,o);await t.evaluate(`(() => {document.querySelectorAll(${JSON.stringify(e)})[${n.nth}].click();return true;})()`);await t.wait({time:0.2})}export const XHS_DECLARATIONS={ai:"笔记含AI合成内容",fiction:"虚构演绎,仅供娱乐",marketing:"内容包含营销广告",reprint:"来源转载","self-shot":"自主拍摄"};const d={public:"公开可见",private:"仅自己可见",friends:"仅互关好友可见",include:"只给谁看",exclude:"不给谁看"};export const XHS_PUBLISH_ARGS=[{name:"declaration",choices:Object.keys(XHS_DECLARATIONS),help:"内容声明:ai / fiction / marketing / reprint / self-shot"},{name:"source",help:"转载来源媒体名称(declaration=reprint 必填)"},{name:"shot-location",help:"自主拍摄地点(使用平台候选地点的完整名称)"},{name:"shot-date",help:"自主拍摄日期 YYYY-MM-DD"},{name:"original",type:"boolean",help:"声明原创;true 同意平台原创声明须知,false 取消"},{name:"visibility",choices:Object.keys(d),help:"可见范围;include/exclude 需同时指定 --audience"},{name:"audience",help:"定向可见用户昵称,逗号分隔(最多200人,须在平台可选列表中)"},{name:"schedule",help:"北京时间 YYYY-MM-DD HH:mm / 带时区 ISO8601 / Unix秒;1小时至14天后"},{name:"allow-duet",type:"boolean",help:"允许合拍(true/false,不传保留平台默认)"},{name:"allow-copy",type:"boolean",help:"允许正文复制(true/false,不传保留平台默认)"},{name:"location",help:"地点完整名称,按平台搜索结果精确选择"},{name:"location-address",help:"地点完整地址,使用 xiaohongshu location 返回的地址区分重名"},{name:"activity",help:"关联活动完整名称(平台会自动添加活动话题)"},{name:"quote-note",help:"引用笔记链接(小红书完整链接,包含访问所需参数)"},{name:"quote-title",help:"引用自己已发布笔记的完整标题,与 --quote-note 二选一"},{name:"attachment",help:"文件附件路径:pdf/doc/docx/ppt/pptx,最多100MB"}],XHS_VIDEO_ARGS=[...XHS_PUBLISH_ARGS.filter((t)=>!["allow-duet","allow-copy","attachment"].includes(t.name)),{name:"chapters",help:"章节 JSON 文件路径:[{time:整数秒,title:章节名,description:描述}],视频须至少15秒"},{name:"summary",help:"视频内容总结(最多100字)"},{name:"cover",help:"自定义视频封面图片路径(jpg/png/webp)"},{name:"cover-ratio",choices:["original","1:1","3:4","4:3"],help:"封面裁剪比例,需同时提供 --cover"}];export function validateXhsSettings(t){if(t["location-address"]&&!t.location)throw new l("--location-address 需要 --location");const e=v(t.chapters);if(t.summary!==void 0&&[...String(t.summary)].length>100)throw new l("--summary 最多100字");if(t["cover-ratio"]&&!t.cover)throw new l("--cover-ratio 需要 --cover");if(t.cover){const i=a.resolve(t.cover);if(!s.existsSync(i)||!s.statSync(i).isFile()||![".jpg",".jpeg",".png",".webp"].includes(a.extname(i).toLowerCase()))throw new l("封面需为存在的 jpg/png/webp 文件")}if(t["quote-note"]&&t["quote-title"])throw new l("--quote-note 与 --quote-title 二选一");if(t["quote-note"]){let i;try{i=new URL(t["quote-note"])}catch{throw new l("--quote-note 需要完整的小红书笔记链接")}if(i.protocol!=="https:"||!["xiaohongshu.com","www.xiaohongshu.com","xhslink.com"].includes(i.hostname))throw new l("--quote-note 需要 HTTPS 小红书笔记链接")}if(t.attachment){const i=a.resolve(t.attachment);if(!s.existsSync(i)||!s.statSync(i).isFile())throw new l("附件文件不存在: "+i);if(![".pdf",".doc",".docx",".ppt",".pptx"].includes(a.extname(i).toLowerCase())||s.statSync(i).size>104857600)throw new l("附件需为100MB以内的 pdf/doc/docx/ppt/pptx")}const o=b(t,XHS_DECLARATIONS);if(o==="reprint"&&!String(t.source||"").trim())throw new l("--declaration reprint 需要 --source 媒体名称");if(t.source&&o!=="reprint")throw new l("--source 仅用于 --declaration reprint");if(o==="reprint"&&t.original)throw new l("转载来源声明不能与 --original true 同时使用");if(o==="self-shot"&&(!t["shot-location"]||!/^\d{4}-\d{2}-\d{2}$/.test(t["shot-date"]||"")))throw new l("自主拍摄需 --shot-location 和 --shot-date YYYY-MM-DD");if((t["shot-date"]||t["shot-location"])&&o!=="self-shot")throw new l("拍摄地点和日期仅用于 --declaration self-shot");if(t.visibility!==void 0&&!Object.hasOwn(d,t.visibility))throw new l("不支持的 --visibility");const n=String(t.audience||"").split(",").map((i)=>i.trim()).filter(Boolean);if(["include","exclude"].includes(t.visibility)&&(!n.length||n.length>200||new Set(n).size!==n.length))throw new l("定向可见需要1至200个不重复的 --audience 用户");if(n.length&&!["include","exclude"].includes(t.visibility))throw new l("--audience 仅用于 include/exclude 可见范围");return{declaration:o,schedule:q(t.schedule,1),audience:n,chapters:e}}async function p(t,e,o){return t.evaluate(`([...document.querySelectorAll(${JSON.stringify(e)})].filter(e=>e.getClientRects().length && e.textContent.trim()===${JSON.stringify(o)}).length === 1)`)}export async function xhsSwitch(t,e,o){const i=await(()=>t.evaluate(`((label)=>{const rows=[...document.querySelectorAll('.custom-switch-card')].filter(e=>e.querySelector('.custom-switch-text-content > span')?.textContent.trim()===label);if(rows.length!==1)return null; const input=rows[0].querySelector('input[type="checkbox"]');return input ? {checked:input.checked,nth:[...document.querySelectorAll('.custom-switch-card .d-switch')].indexOf(rows[0].querySelector('.d-switch'))}:null;})(${JSON.stringify(e)})`))();if(!i||i.nth<0)throw new m("未找到发布设置开关:"+e+";已停止提交");if(i?.checked===o)return;await t.click(".custom-switch-card .d-switch",{nth:i.nth});await t.wait({time:0.2});if(e==="原创声明"&&o&&await t.evaluate("!!document.querySelector('.originalContainer')")){await c(t,".originalContainer .d-checkbox");await c(t,".originalContainer button","声明原创")}await r(t,`((label,value)=>{const rows=[...document.querySelectorAll('.custom-switch-card')].filter(e=>e.querySelector('.custom-switch-text-content > span')?.textContent.trim()===label);return rows.length===1 && rows[0].querySelector('input[type="checkbox"]')?.checked===value;})(${JSON.stringify(e)},${o})`,e)}async function C(t,e,o){const n=XHS_DECLARATIONS[o],i=await p(t,".d-select-description",n);if(i&&!["reprint","self-shot"].includes(o))return;await c(t,".d-select",i?n:"添加内容类型声明");if(o==="reprint"||o==="self-shot")await c(t,".declaration-drop-down .d-option-name","内容来源声明");await c(t,".d-popover .d-option-content",n);if(o==="reprint"){await u(t,'input[placeholder="请输入媒体名称"]',e.source);await c(t,".self-shooting-contain button","确认")}if(o==="self-shot"){await u(t,'input[placeholder="下拉选择地点"]',e["shot-location"],!1);await r(t,`!![...document.querySelectorAll('.el-autocomplete-suggestion .name')].find(e=>e.textContent.trim()===${JSON.stringify(e["shot-location"])})`,"拍摄地点候选");await c(t,".el-autocomplete-suggestion .name",e["shot-location"],!1);await u(t,".self-shooting-contain .d-datepicker-input-filter input",e["shot-date"]);await c(t,".self-shooting-contain button","确认")}await r(t,`([...document.querySelectorAll('.d-select-description')].filter(e=>e.textContent.trim()===${JSON.stringify(n)}).length===1) && !document.querySelector('.self-shooting-contain')`,n)}export async function setXhsPublishSettings(t,e){const o=validateXhsSettings(e);if(e.original!==void 0)await xhsSwitch(t,"原创声明",e.original);if(o.declaration)await C(t,e,o.declaration);if(e["allow-duet"]!==void 0)await xhsSwitch(t,"允许合拍",e["allow-duet"]);if(e["allow-copy"]!==void 0)await xhsSwitch(t,"允许正文复制",e["allow-copy"]);if(e.visibility){const n=d[e.visibility];if(!await p(t,".d-select-description",n)){const i=await t.evaluate(`([...document.querySelectorAll('.d-select-description')].filter(e=>${JSON.stringify(Object.values(d))}.includes(e.textContent.trim())).map(e=>e.textContent.trim()))`);if(i.length!==1)throw new m("无法确定当前可见范围;已停止提交");await y(t,i[0]);await h(t,".custom-option .name",n)}if(o.audience.length){for(const i of o.audience){await u(t,'.d-modal input[placeholder="搜索用户"]',i,!1);await r(t,`!![...document.querySelectorAll('.d-modal .left .user-list *')].find(e=>!e.children.length&&e.textContent.trim()===${JSON.stringify(i)})`,"可选用户 "+i);await c(t,".d-modal .left .user-list .user-name",i)}await c(t,".d-modal .confirm-btn","确认")}await r(t,`([...document.querySelectorAll('.d-select-description')].filter(e=>e.textContent.trim()===${JSON.stringify(n)}).length===1)`,"可见范围")}if(o.schedule){await xhsSwitch(t,"定时发布",!0);await u(t,".d-datepicker-input-filter input",o.schedule)}if(e.location){await y(t,"添加地点");await u(t,".d-select.focus input",e.location,!1);const n=`(({name,address})=>{const all=[...document.querySelectorAll('.d-popover .option-name')];const hits=all.filter(e=>e.getClientRects().length && e.textContent.trim()===name && (!address || e.parentElement.querySelector('.option-subname')?.textContent.trim()===address));return hits.length===1 ? all.indexOf(hits[0]) : -1;})(${JSON.stringify({name:e.location,address:e["location-address"]})})`;await r(t,`(${n})>=0`,"唯一地点(重名时指定 --location-address)");const i=await t.evaluate(n);if(i<0)throw new m("地点候选已变化,停止提交");await t.evaluate(`document.querySelectorAll('.d-popover .option-name')[${i}].click()`);await r(t,`!![...document.querySelectorAll('.d-select-description')].find(e=>e.textContent.trim()===${JSON.stringify(e.location)})`,"地点")}if(e["quote-note"]||e["quote-title"]){await c(t,".setting-card-text","引用笔记");if(e["quote-note"]){await c(t,".select-note-modal__tab","他人笔记");await u(t,'input[placeholder="请粘贴笔记链接 http://..."]',e["quote-note"],!1)}const n=e["quote-title"];await r(t,`([...document.querySelectorAll('.select-note-modal .note-card__title')].filter(e=>e.getClientRects().length ${n?"&& e.textContent.trim()==="+JSON.stringify(n):""}).length===1)`,"待引用笔记");const i=await w(t,".select-note-modal .note-card__title",n),S=await t.evaluate(`document.querySelectorAll('.select-note-modal .note-card__title')[${i.nth}].textContent.trim()`);await t.click(i.selector,{nth:i.nth});await c(t,".select-note-modal button","确认引用");await r(t,`!document.querySelector('.select-note-modal') && document.body.innerText.includes(${JSON.stringify("《")} + ${JSON.stringify(S)} + '》')`,"引用笔记")}if(e.activity){await h(t,".more","更多");const n=`((name)=>{const cards=[...document.querySelectorAll('.activity-card-drawer')].filter(e=>e.querySelector('.activity-name')?.textContent.trim()===name);return cards.length===1 ? {nth:[...document.querySelectorAll('.activity-card-drawer .activity-action')].indexOf(cards[0].querySelector('.activity-action')),selected:cards[0].querySelector('.activity-action')?.textContent.trim()==='取消关联'} : null;})(${JSON.stringify(e.activity)})`;await r(t,`!!(${n})`,"活动 "+e.activity);const i=await t.evaluate(n);if(!i.selected)await t.evaluate(`document.querySelectorAll('.activity-card-drawer .activity-action')[${i.nth}].click()`);await r(t,`(${n})?.selected === true`,"关联活动");await h(t,".d-drawer-close")}if(e.attachment){await f(t,'.file-relation-container input[type="file"]',[a.resolve(e.attachment)]);await r(t,`document.querySelector('.file-relation-container .setting-card-text')?.textContent.trim() === ${JSON.stringify(a.basename(e.attachment))} && !document.querySelector('.file-relation-container')?.textContent.includes('上传中')`,"附件上传",60000)}if(o.chapters||e.summary!==void 0)await x(t,o.chapters,e.summary);if(e.cover){const n=await t.evaluate("document.querySelector('.cover-plugin-preview > .cover > .default')?.style.backgroundImage");await c(t,".cover-plugin-preview > .cover > .default");await c(t,".cover-plugin-preview .cover-edit-entry");await c(t,".d-modal .d-tabs-header-label","上传封面");await f(t,'.d-modal input[type="file"]',[a.resolve(e.cover)]);await r(t,"!!document.querySelector('#upload-cover-containner .cropper-canvas img')?.getAttribute('src')","封面图片载入",30000);if(e["cover-ratio"]){const i=e["cover-ratio"]==="original"?"原始":e["cover-ratio"];await c(t,".d-modal .crop-ratio-item",i);await r(t,`document.querySelector('.d-modal .crop-ratio-item-active')?.textContent.trim()===${JSON.stringify(i)}`,"封面裁剪比例")}await c(t,".d-modal button","确定");await r(t,`!document.querySelector('#upload-cover-containner') && document.querySelector('.cover-plugin-preview')?.getAttribute('aria-busy')==='false' && document.querySelector('.cover-plugin-preview > .cover > .default')?.style.backgroundImage !== ${JSON.stringify(n)}`,"视频封面上传",60000)}}
@@ -0,0 +1,21 @@
1
+ import{ArgumentError as s,CommandExecutionError as r}from"@jackwener/opencli/errors";import{humanType as l}from"../human.js";export const TOPIC_ID_ARG={name:"topic-ids",help:"topics 查询返回的 ID,逗号分隔,与 --topics 一一对应;防止同名话题选错"};export function parseTopics(e){const i=e["topic-ids"]?String(e["topic-ids"]).split(",").map((t)=>t.trim()):[],n=e.topics?String(e.topics).split(",").map((t)=>(i.length?t:t.trim()).replace(/^#/,"")):[];if(n.some((t)=>!t.trim())||n.length>10)throw new s("--topics 需为 1–10 个非空话题");if(i.length&&(i.length!==n.length||i.some((t)=>!/^[a-f\d]{24}$/i.test(t))))throw new s("--topic-ids 需为与 --topics 一一对应的 24 位话题 ID");return n.map((t,a)=>({name:t,id:i[a]||null}))}export async function attachTopics(e,i,n){for(const t of n){const a=await e.evaluate(`((selectors) => {
2
+ const editor = selectors.map(s => document.querySelector(s)).find(e => e && e.offsetParent !== null && e.isContentEditable);
3
+ if (!editor) return null;
4
+ editor.focus(); const range = document.createRange(); range.selectNodeContents(editor); range.collapse(false);
5
+ const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range);
6
+ return [...editor.querySelectorAll('a[data-topic]')].map(e => JSON.parse(e.getAttribute('data-topic')));
7
+ })(${JSON.stringify(i)})`);if(a===null)throw new r("未找到话题正文编辑器");await l(e,` #${t.name}`);let o=null;for(let c=0;c<12;c++){await e.wait({time:0.5});o=await e.evaluate(`((topic) => {
8
+ const items = [...document.querySelectorAll('#creator-editor-topic-container .item')].filter(e => e.offsetParent !== null);
9
+ const candidates = items.map(el => ({ el, name: el.querySelector('.name')?.textContent.replace(/^#/, ''), id: JSON.parse(el.getAttribute('data-impression') || '{}').tagTarget?.value?.tagId }));
10
+ const matches = candidates.filter(c => topic.id ? c.id === topic.id : c.name === topic.name);
11
+ if (matches.length !== 1) return { count: matches.length };
12
+ const match = matches[0];
13
+ if (!match.id) return { count: 1, error: '候选缺少话题 ID' };
14
+ match.el.click();
15
+ return { count: 1, id: match.id, name: match.name };
16
+ })(${JSON.stringify(t)})`);if(o?.id||o?.error||o?.count>1)break}if(!o?.id)throw new r(`话题「${t.name}」没有唯一可选候选。请先运行 xiaohongshu topics 查询并传入 topic-ids;未提交笔记。`);await e.wait({time:0.3});if(!await e.evaluate(`((cfg) => {
17
+ const editor = cfg.selectors.map(s => document.querySelector(s)).find(e => e && e.offsetParent !== null && e.isContentEditable);
18
+ const entities = [...(editor?.querySelectorAll('a[data-topic]') || [])].map(e => JSON.parse(e.getAttribute('data-topic')));
19
+ const matches = list => list.filter(t => t.id === cfg.id && t.name === cfg.name).length;
20
+ return matches(entities) > matches(cfg.before);
21
+ })(${JSON.stringify({selectors:i,before:a,id:o.id,name:o.name})})`))throw new r(`话题「${t.name}」未生成所选 ID 的真实实体,已停止提交`)}return n.map((t)=>t.name)}
@@ -0,0 +1 @@
1
+ import{cli as o,Strategy as n}from"@jackwener/opencli/registry";import{CommandExecutionError as c}from"@jackwener/opencli/errors";import{prepareCreatorDiscovery as m,creatorDiscoveryRequest as d,discoveryLimit as l,listField as y,activityQuery as u}from"./_shared/publish-discovery.js";o({site:"xiaohongshu",name:"activities",access:"read",domain:"creator.xiaohongshu.com",description:"查看/筛选当前可关联活动及参与说明、截止时间、所需话题和发布参数",strategy:n.COOKIE,navigateBefore:!1,args:[{name:"query",help:"在当前完整候选列表中按名称或活动说明筛选"},{name:"limit",type:"int",default:20}],columns:["id","name","description","starts_at","ends_at","topics","details_url","publish_args"],func:async(i,e)=>{const r=l(e.limit);await m(i);const a=await d(i,"activities",u),s=String(e.query||"").toLowerCase();return y(a,"activityList").filter((t)=>[t.activityName,t.activityReward].join(" ").toLowerCase().includes(s)).slice(0,r).map((t)=>{if(!t.activityId||!t.activityName)throw new c("活动缺少 ID 或名称");return{id:String(t.activityId),name:t.activityName,description:t.activityReward||"",starts_at:t.startTime?new Date(t.startTime).toISOString():null,ends_at:t.endTime?new Date(t.endTime).toISOString():null,topics:t.topicInfos||[],details_url:t.activityLink||"",publish_args:{activity:t.activityName}}})}});
@@ -0,0 +1 @@
1
+ import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{cli as a,Strategy as i}from"@jackwener/opencli/registry";import{prepareCreatorDiscovery as n,creatorDiscoveryRequest as c,listField as m}from"./_shared/publish-discovery.js";a({site:"xiaohongshu",name:"collections",access:"read",domain:"creator.xiaohongshu.com",strategy:i.COOKIE,navigateBefore:!1,description:"查看当前账号图文/视频合集候选(普通合集);当前尚未接入发布选择,不能把 ID 直接作为发布参数",args:[{name:"cursor",default:"",help:"上一页的 next_cursor;按平台完整页返回,不截断候选"}],columns:["id","name","type","next_cursor","has_more","publish_supported"],func:async(o,t)=>{await n(o);const e=await c(o,"collections",{cursor:t.cursor||"",needTypeList:[0],targetUid:""});return m(e,"collectionInfoList").map((r)=>{if(!r.id||!r.name)throw new s("合集缺少 ID 或名称");return{id:String(r.id),name:r.name,type:r.type,next_cursor:e.nextCursor||"",has_more:Boolean(e.hasMore),publish_supported:!1}})}});
@@ -0,0 +1 @@
1
+ import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{cli as p,Strategy as d}from"@jackwener/opencli/registry";import{prepareCreatorDiscovery as m,creatorDiscoveryRequest as u,listField as i}from"./_shared/publish-discovery.js";p({site:"xiaohongshu",name:"groups",access:"read",domain:"creator.xiaohongshu.com",strategy:d.COOKIE,navigateBefore:!1,description:"查看当前账号创建/管理的群聊及平台可绑定状态;发布绑定尚未接入,不创建群聊",args:[],columns:["id","name","role","bindable","description","publish_supported"],func:async(o)=>{await m(o);const r=await u(o,"groups",{noteId:""}),a=i(r,"groupList"),n=i(r,"adminGroupList"),t=new Set(a.map((e)=>e.groupId));return[...a.map((e)=>({...e,role:"owner"})),...n.filter((e)=>!t.has(e.groupId)).map((e)=>({...e,role:"admin"}))].map((e)=>{if(!e.groupId||!e.groupName)throw new s("群聊缺少 ID 或名称");return{id:String(e.groupId),name:e.groupName,role:e.role,bindable:Boolean(e.linkable),description:e.linkable?e.desc||"":"平台提示存在生态问题,暂不支持绑定",publish_supported:!1}})}});
@@ -0,0 +1 @@
1
+ import{cli as s,Strategy as n}from"@jackwener/opencli/registry";import{ArgumentError as d,CommandExecutionError as l}from"@jackwener/opencli/errors";import{prepareCreatorDiscovery as m,creatorDiscoveryRequest as u,discoveryLimit as c,listField as f}from"./_shared/publish-discovery.js";s({site:"xiaohongshu",name:"location",access:"read",domain:"creator.xiaohongshu.com",description:"搜索发布地点,返回完整地址以区分同名地点,以及可直接使用的发布参数",strategy:n.COOKIE,navigateBefore:!1,args:[{name:"query",positional:!0,required:!0,help:"地点关键词"},{name:"limit",type:"int",default:20}],columns:["poi_id","name","address","city","publish_args"],func:async(r,i)=>{const t=c(i.limit);if(!String(i.query||"").trim())throw new d("请输入地点关键词");await m(r);const a=await u(r,"location",{latitude:0,longitude:0,keyword:i.query,page:1,size:t,source:"WEB",type:3}),o=new Set;return f(a,"poiList").filter((e)=>{if(o.has(e.poiId))return!1;o.add(e.poiId);return!0}).slice(0,t).map((e)=>{if(!e.poiId||!e.name||!e.fullAddress)throw new l("地点缺少 ID、名称或完整地址");return{poi_id:String(e.poiId),name:e.name,address:e.fullAddress,city:e.cityName||"",publish_args:{location:e.name,"location-address":e.fullAddress}}})}});