publishport-opencli 1.0.17 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli-manifest.json +33 -15
- package/clis/_shared/article/publish.js +14 -7
- package/clis/bilibili/article.js +1 -1
- package/clis/bilibili/comments.js +1 -1
- package/clis/douyin/_shared/draft-cache.js +13 -0
- package/clis/douyin/drafts.js +1 -1
- package/clis/douyin/publish-image.js +12 -10
- package/clis/instagram/_shared/private-publish.js +3 -3
- package/clis/instagram/detail.js +3 -2
- package/clis/instagram/post.js +24 -24
- package/clis/instagram/user.js +2 -1
- package/clis/jike/api.js +30 -0
- package/clis/jike/comment.js +1 -82
- package/clis/jike/like.js +1 -38
- package/clis/jike/repost.js +20 -11
- package/clis/juejin/article.js +4 -4
- package/clis/sohu/article.js +1 -1
- package/clis/sohu/stats.js +2 -2
- package/clis/toutiao/article.js +1 -1
- package/clis/toutiao/articles.js +12 -3
- package/clis/toutiao/utils.js +2 -2
- package/clis/xiaohongshu/publish.js +14 -14
- package/clis/zhihu/answer.js +1 -15
- package/clis/zhihu/article.js +1 -7
- package/clis/zhihu/image-profile.js +7 -0
- package/dist/src/cli.js +3 -3
- package/dist/src/commands/auth.js +3 -3
- package/dist/src/daemon-utils.d.ts +27 -0
- package/dist/src/daemon-utils.js +1 -1
- package/dist/src/daemon.js +1 -1
- package/dist/src/errors.js +1 -1
- package/dist/src/execution.js +2 -2
- package/package.json +1 -1
package/clis/instagram/post.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as E from"node:fs";import*as v from"node:path";import{cli as r,Strategy as t}from"@jackwener/opencli/registry";import{ArgumentError as H,AuthRequiredError as A,CommandExecutionError as q}from"@jackwener/opencli/errors";import{installInstagramProtocolCapture as e,readInstagramProtocolCapture as jj}from"./_shared/protocol-capture.js";import{publishMediaViaPrivateApi as Kj,publishImagesViaPrivateApi as Qj,resolveInstagramPrivatePublishConfig as Yj,isPublishOutcomeUncertain as Zj}from"./_shared/private-publish.js";import{resolveInstagramRuntimeInfo as $j}from"./_shared/runtime-info.js";import{setInputFiles as qj}from"../_shared/file-inject.js";const N="https://www.instagram.com/",zj=new Set([".jpg",".jpeg",".png",".webp"]),Jj=new Set([".mp4"]),h=10,Vj="/tmp/instagram_post_protocol_trace.json";async function k(j,K=!1){if(K){await j.goto(`${N}?__opencli_reset=${Date.now()}`);await j.wait({time:1})}await j.goto(N)}export function buildEnsureComposerOpenJs(){return`
|
|
2
2
|
(() => {
|
|
3
3
|
const path = window.location?.pathname || '';
|
|
4
4
|
const onLoginRoute = /\\/accounts\\/login\\/?/.test(path);
|
|
@@ -61,7 +61,7 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
61
61
|
const settled = !shared && !composerOpen && !/sharing/.test(visibleText);
|
|
62
62
|
return { ok: shared, failed, settled, url: /\\/p\\//.test(url) ? url : '' };
|
|
63
63
|
})()
|
|
64
|
-
`}function
|
|
64
|
+
`}function Gj(j){if(!j)throw new q("Browser session required for instagram post");return j}function Oj(j){if(!j.length)throw new H('Argument "media" is required.',"Provide --media /path/to/file.jpg or --media /path/a.jpg,/path/b.mp4");if(j.length>h)throw new H(`Too many media items: ${j.length}`,`Instagram carousel posts support at most ${h} items`);return j.map((Q)=>{const Y=v.resolve(String(Q||"").trim());if(!Y)throw new H("Media path cannot be empty");if(!E.existsSync(Y))throw new H(`Media file not found: ${Y}`);const Z=v.extname(Y).toLowerCase();if(zj.has(Z))return{type:"image",filePath:Y};if(Jj.has(Z))return{type:"video",filePath:Y};throw new H(`Unsupported media format: ${Z}`,"Supported formats: images (.jpg, .jpeg, .png, .webp) and videos (.mp4)")})}function Xj(j){const K=String(j.media??"").trim();return Oj(K.split(",").map((Q)=>Q.trim()).filter(Boolean))}const b=2200;function Hj(j){if(j.media===void 0)throw new H('Argument "media" is required.',"Provide --media /path/to/file.jpg or --media /path/a.jpg,/path/b.mp4");I(j.content)}function I(j){const K=[...String(j??"")].length;if(K>b)throw new H(`Instagram caption is too long: ${K} characters (limit ${b}).`,`Shorten the caption by ${K-b} characters. Nothing was uploaded or posted.`)}function Bj(j){const K=j instanceof Error?j.message:String(j);return/failed to normalize/i.test(K)||/caption_too_long|caption too long/i.test(K)}function Nj(j){if(!(j instanceof q))return!1;if(Bj(j))return!1;return j.message.startsWith("Instagram private publish")||j.message.startsWith("Instagram private route")}function R(j,K,Q=""){return[{status:"✅ Posted",detail:Aj(j),url:K,media_id:String(Q||""),account_id:String(Q||"").split("_")[1]||""}]}function Tj(j,K){const Q=j instanceof Error?j.message:String(j),Y=K instanceof Error?K.message:String(K);return`Private route failed first: ${Q}. UI fallback then failed: ${Y}`}function _j(j,K){const Q=j instanceof Error?j.message:String(j),Y=K instanceof Error?K.message:String(K);return`Instagram publish failed. Cause (private API route): ${Q}`+` — the UI fallback then also failed with: ${Y}`}async function Dj(j){const K=await Yj(j.page),Q=j.mediaItems.every((Z)=>Z.type==="image")?await Qj({page:j.page,imagePaths:j.mediaItems.map((Z)=>Z.filePath),caption:j.content,apiContext:K.apiContext,jazoest:K.jazoest}):await Kj({page:j.page,mediaItems:j.mediaItems,caption:j.content,apiContext:K.apiContext,jazoest:K.jazoest}),Y=Q.code?new URL(`/p/${Q.code}/`,N).toString():j.canDiffProfile?await x(j.page,j.existingPostPaths):"";return R(j.mediaItems,Y,Q.mediaId)}async function Fj(j){let K,Q=null;for(let Y=0;Y<j.commandAttemptBudget;Y++){let Z=!1;try{await k(j.page,j.forceFreshStart||Y>0);await j.installProtocolCapture();await j.page.wait({time:2});await F(j.page);await y(j.page);const $=await c(j.page,j.mediaItems);if(j.preUploadDelaySeconds>0)await j.page.wait({time:j.preUploadDelaySeconds});let J=!1,z=null;for(const X of $){let T=X;for(let _=0;_<j.uploadAttemptBudget;_++){await Mj(j.page,j.mediaItems,T);const D=await m(j.page,j.previewProbeWindowSeconds);if(D.state==="preview"){J=!0;break}if(D.state==="failed"){z=W(D.detail);for(let M=0;M<j.inlineUploadRetryBudget;M++){if(!await fj(j.page))break;await j.page.wait({time:3});const B=await m(j.page,Math.max(3,Math.floor(j.previewProbeWindowSeconds/2)));if(B.state==="preview"){J=!0;break}if(B.state!=="failed")break}if(J)break;await Cj(j.page);await F(j.page);if(_<j.uploadAttemptBudget-1){try{await j.drainProtocolCapture();await k(j.page,!0);await j.installProtocolCapture();await j.page.wait({time:2});await F(j.page);await y(j.page);T=await d(j.page,T,j.mediaItems);if(j.preUploadDelaySeconds>0)await j.page.wait({time:j.preUploadDelaySeconds})}catch{throw z}await j.page.wait({time:1.5});continue}break}break}if(J)break}if(!J){if(z)throw z;await Uj(j.page,j.finalPreviewWaitSeconds)}try{await Pj(j.page)}catch(X){await Ej(j.page,X)}if(j.content){await Ij(j.page,j.content);await dj(j.page,j.content)}if(j.preShareDelaySeconds>0)await j.page.wait({time:j.preShareDelaySeconds});await l(j.page,["Share","分享"],"caption");Z=!0;let G="";try{G=await n(j.page)}catch(X){if(X instanceof q&&X.message==="Instagram post share failed"&&await Sj(j.page)){await j.page.wait({time:Math.max(2,j.preShareDelaySeconds)});G=await n(j.page)}else throw X}await j.drainProtocolCapture();if(!G&&j.canDiffProfile)G=await x(j.page,j.existingPostPaths);return R(j.mediaItems,G)}catch($){K=$;if($ instanceof q&&$.message!=="Failed to open Instagram post composer")Q=$;if($ instanceof A)throw $;if(Z){let z="";if(j.canDiffProfile)try{z=await x(j.page,j.existingPostPaths)}catch{}if(z)return R(j.mediaItems,z);const G=$ instanceof Error?$.message:String($);throw new q(`Instagram share was clicked but the outcome could not be confirmed: ${G}`,"The post may already be live. Do NOT rerun this command blindly — open the profile (or rerun after checking) and confirm no new post exists first; retrying on a false failure creates a duplicate post.")}if(!($ instanceof q)||Y===j.commandAttemptBudget-1){if($ instanceof q&&$.message==="Failed to open Instagram post composer"&&Q)throw Q;throw $}let J=!1;if(j.mediaItems.length>=10&&j.page.closeWindow)try{await j.drainProtocolCapture();await j.page.closeWindow();J=!0}catch{}if(!J){await F(j.page);await j.page.wait({time:1})}}}throw K instanceof Error?K:new q("Instagram post failed")}async function y(j){const K=await j.evaluate(buildEnsureComposerOpenJs());if(!K?.ok){if(K?.reason==="auth")throw new A("www.instagram.com","Instagram login required before posting");throw new q("Failed to open Instagram post composer")}}async function F(j){for(let K=0;K<4;K++){if(!(await j.evaluate(`
|
|
65
65
|
(() => {
|
|
66
66
|
const isVisible = (el) => {
|
|
67
67
|
if (!(el instanceof HTMLElement)) return false;
|
|
@@ -105,7 +105,7 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
105
105
|
|
|
106
106
|
return { ok: false };
|
|
107
107
|
})()
|
|
108
|
-
`))?.ok)return;await j.wait({time:0.5})}}async function w(j,
|
|
108
|
+
`))?.ok)return;await j.wait({time:0.5})}}async function w(j,K){const Q=K.some((Z)=>Z.type==="video"),Y=await j.evaluate(`
|
|
109
109
|
((includesVideo) => {
|
|
110
110
|
const isVisible = (el) => {
|
|
111
111
|
if (!(el instanceof HTMLElement)) return false;
|
|
@@ -164,10 +164,10 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
164
164
|
return '[data-opencli-ig-upload-index="' + index + '"]';
|
|
165
165
|
});
|
|
166
166
|
return { ok: true, selectors };
|
|
167
|
-
})(${JSON.stringify(
|
|
168
|
-
`);if(!
|
|
167
|
+
})(${JSON.stringify(Q)})
|
|
168
|
+
`);if(!Y?.ok||!Y.selectors?.length)throw new q("Instagram upload input not found","Open the new-post composer in a logged-in browser session and retry");return Y.selectors}async function c(j,K){try{return await w(j,K)}catch(Q){if(!(Q instanceof q)||!Q.message.includes("upload input not found"))throw Q;await y(j);await j.wait({time:1.5});try{return await w(j,K)}catch(Y){if(!(Y instanceof q)||!Y.message.includes("upload input not found"))throw Y;await k(j,!0);await j.wait({time:2});await F(j);await y(j);await j.wait({time:2});return w(j,K)}}}function Lj(j){const K=j.match(/data-opencli-ig-upload-index="(\d+)"/);if(!K)return null;const Q=Number.parseInt(K[1]||"",10);return Number.isNaN(Q)?null:Q}async function d(j,K,Q){const Y=await c(j,Q),Z=Lj(K);if(Z!==null&&Y[Z])return Y[Z];return Y[0]||K}async function Wj(j,K){await j.evaluate(`
|
|
169
169
|
(() => {
|
|
170
|
-
const input = document.querySelector(${JSON.stringify(
|
|
170
|
+
const input = document.querySelector(${JSON.stringify(K)});
|
|
171
171
|
if (!(input instanceof HTMLInputElement)) return { ok: false };
|
|
172
172
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
173
173
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
@@ -208,7 +208,7 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
208
208
|
if (failed) return { state: 'failed', detail: dialogText || 'Something went wrong' };
|
|
209
209
|
return { state: 'pending', detail: dialogText || '' };
|
|
210
210
|
})()
|
|
211
|
-
`}async function
|
|
211
|
+
`}async function L(j){const K=await j.evaluate(buildInspectUploadStageJs());if(K?.state)return K;if(K?.ok===!0)return{state:"preview",detail:K.detail};return{state:"pending",detail:K?.detail}}function W(j){return new q("Instagram image upload failed",j?`Instagram rejected the upload: ${j}`:"Instagram rejected the upload before the preview stage")}async function Mj(j,K,Q){const Y=K.map(($)=>$.filePath);let Z=Q;for(let $=0;$<2;$++)try{if((await qj(j,Z,Y)).method==="cdp")await Wj(j,Z);return}catch(J){const z=J instanceof Error?J.message:String(J);if((z.includes("No element found matching selector")||z.includes("Could not find node with given id")||z.includes("No node with given id found")||z.includes("未找到可用的文件上传输入框"))&&$===0){Z=await d(j,Z,K);continue}throw J}}function Aj(j){if(j.every((K)=>K.type==="image"))return j.length===1?"Single image post shared successfully":`${j.length}-image carousel post shared successfully`;return j.length===1?"Single mixed-media post shared successfully":`${j.length}-item mixed-media carousel post shared successfully`}function Rj(j){if(j.length>=10)return 6;if(j.length>=5)return 4;return 3}function yj(j){if(j.length>=10)return 3;if(j.length>=5)return 1.5;return 0}function xj(j){if(j.length>=10)return 3;if(j.length>=5)return 3;return 2}function hj(j){if(j.length>=10)return 6;if(j.length>=5)return 6;return 4}function kj(j){if(j.length>=10)return 12;if(j.length>=5)return 16;return 12}function bj(j){if(j.length>=10)return 4;if(j.length>=5)return 3;return 0}function wj(j){if(j.length>=10)return 3;if(j.length>=5)return 2;return 1}async function Cj(j){return!!(await j.evaluate(`
|
|
212
212
|
(() => {
|
|
213
213
|
const isVisible = (el) => {
|
|
214
214
|
if (!(el instanceof HTMLElement)) return false;
|
|
@@ -231,7 +231,7 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
231
231
|
}
|
|
232
232
|
return { ok: false };
|
|
233
233
|
})()
|
|
234
|
-
`))?.ok}async function
|
|
234
|
+
`))?.ok}async function fj(j){return!!(await j.evaluate(`
|
|
235
235
|
(() => {
|
|
236
236
|
const isVisible = (el) => {
|
|
237
237
|
if (!(el instanceof HTMLElement)) return false;
|
|
@@ -265,7 +265,7 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
265
265
|
}
|
|
266
266
|
return { ok: false };
|
|
267
267
|
})()
|
|
268
|
-
`))?.ok}async function
|
|
268
|
+
`))?.ok}async function Uj(j,K=12){const Q=Math.max(1,Math.ceil(K));for(let Y=0;Y<Q;Y++){const Z=await L(j);if(Z.state==="preview")return;if(Z.state==="failed"){await j.screenshot({path:"/tmp/instagram_post_preview_debug.png"});throw W("Inspect /tmp/instagram_post_preview_debug.png. "+(Z.detail||""))}if(Y<Q-1)await j.wait({time:1})}await j.screenshot({path:"/tmp/instagram_post_preview_debug.png"});throw new q("Instagram image preview did not appear after upload","The selected file input may not match the active composer; inspect /tmp/instagram_post_preview_debug.png")}async function m(j,K=4){const Q=Math.max(1,Math.ceil(K*2));for(let Y=0;Y<Q;Y++){const Z=await L(j);if(Z.state!=="pending")return Z;if(Y<Q-1)await j.wait({time:0.5})}return{state:"pending"}}export function buildClickActionJs(j,K="any"){return`
|
|
269
269
|
((labels, scope) => {
|
|
270
270
|
const isVisible = (el) => {
|
|
271
271
|
if (!(el instanceof HTMLElement)) return false;
|
|
@@ -319,8 +319,8 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
return { ok: false };
|
|
322
|
-
})(${JSON.stringify(j)}, ${JSON.stringify(
|
|
323
|
-
`}async function
|
|
322
|
+
})(${JSON.stringify(j)}, ${JSON.stringify(K)})
|
|
323
|
+
`}async function l(j,K,Q="any"){const Y=await j.evaluate(buildClickActionJs(K,Q));if(!Y?.ok)throw new q(`Instagram action button not found: ${K.join(" / ")}`);return Y.label||K[0]}async function Sj(j){return!!(await j.evaluate(`
|
|
324
324
|
(() => {
|
|
325
325
|
const isVisible = (el) => {
|
|
326
326
|
if (!(el instanceof HTMLElement)) return false;
|
|
@@ -358,12 +358,12 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
358
358
|
|
|
359
359
|
return { ok: false };
|
|
360
360
|
})()
|
|
361
|
-
`))?.ok}async function
|
|
361
|
+
`))?.ok}async function g(j){return!!(await j.evaluate(`
|
|
362
362
|
(() => {
|
|
363
363
|
const editable = document.querySelector('textarea, [contenteditable="true"]');
|
|
364
364
|
return { ok: !!editable };
|
|
365
365
|
})()
|
|
366
|
-
`))?.ok}async function
|
|
366
|
+
`))?.ok}async function s(j){return!!(await j.evaluate(`
|
|
367
367
|
(() => {
|
|
368
368
|
const editable = document.querySelector('textarea, [contenteditable="true"]');
|
|
369
369
|
const dialogText = Array.from(document.querySelectorAll('[role="dialog"]'))
|
|
@@ -377,7 +377,7 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
377
377
|
|| dialogText.includes('advanced settings'),
|
|
378
378
|
};
|
|
379
379
|
})()
|
|
380
|
-
`))?.ok}async function
|
|
380
|
+
`))?.ok}async function Pj(j){for(let K=0;K<3;K++){if(await s(j))return;try{await l(j,["Next","下一步"],"media")}catch(Y){if(Y instanceof q){await j.wait({time:1.5});if(await s(j))return;const Z=await L(j);if(Z.state==="failed")throw W(Z.detail);if(K<2)continue}throw Y}await j.wait({time:1.5});if(await g(j))return;const Q=await L(j);if(Q.state==="failed")throw W(Q.detail)}await j.screenshot({path:"/tmp/instagram_post_caption_debug.png"});throw new q("Instagram caption editor did not appear","Instagram may have changed the publish flow; inspect /tmp/instagram_post_caption_debug.png")}async function ij(j){if(!await g(j)){await j.screenshot({path:"/tmp/instagram_post_caption_debug.png"});throw new q("Instagram caption editor did not appear","Instagram may have changed the publish flow; inspect /tmp/instagram_post_caption_debug.png")}}async function Ej(j,K){const Q=await L(j);if(Q.state==="failed")throw W(Q.detail);throw K}async function vj(j){return!!(await j.evaluate(`
|
|
381
381
|
(() => {
|
|
382
382
|
const textarea = document.querySelector('[aria-label="Write a caption..."], textarea');
|
|
383
383
|
if (textarea instanceof HTMLTextAreaElement) {
|
|
@@ -437,7 +437,7 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
437
437
|
|
|
438
438
|
return { ok: true, kind: 'contenteditable' };
|
|
439
439
|
})()
|
|
440
|
-
`))?.ok}async function
|
|
440
|
+
`))?.ok}async function Ij(j,K){if(j.insertText&&await vj(j))try{await j.insertText(K);await j.wait({time:0.3});await j.evaluate(`
|
|
441
441
|
(() => {
|
|
442
442
|
const textarea = document.querySelector('[aria-label="Write a caption..."], textarea');
|
|
443
443
|
if (textarea instanceof HTMLTextAreaElement) {
|
|
@@ -548,8 +548,8 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
548
548
|
}
|
|
549
549
|
|
|
550
550
|
return { ok: false };
|
|
551
|
-
})(${JSON.stringify(
|
|
552
|
-
`))?.ok)throw new
|
|
551
|
+
})(${JSON.stringify(K)})
|
|
552
|
+
`))?.ok)throw new q("Failed to fill Instagram caption")}async function cj(j,K){return!!(await j.evaluate(`
|
|
553
553
|
((content) => {
|
|
554
554
|
const normalized = content.trim();
|
|
555
555
|
const readLexicalText = (node) => {
|
|
@@ -602,11 +602,11 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
602
602
|
}
|
|
603
603
|
|
|
604
604
|
return { ok: false };
|
|
605
|
-
})(${JSON.stringify(
|
|
606
|
-
`))?.ok}async function
|
|
605
|
+
})(${JSON.stringify(K)})
|
|
606
|
+
`))?.ok}async function dj(j,K){for(let Q=0;Q<6;Q++){if(await cj(j,K))return;if(Q<5)await j.wait({time:0.5})}await j.screenshot({path:"/tmp/instagram_post_caption_fill_debug.png"});throw new q("Instagram caption did not stick before sharing","Inspect /tmp/instagram_post_caption_fill_debug.png for the caption editor state")}async function n(j){let K=0;for(let Q=0;Q<90;Q++){const Y=await j.evaluate(buildPublishStatusProbeJs());if(Y?.failed){await j.screenshot({path:"/tmp/instagram_post_share_debug.png"});throw new q("Instagram post share failed","Inspect /tmp/instagram_post_share_debug.png for the share failure state")}if(Y?.ok)return Y.url||"";if(Y?.settled){K+=1;if(K>=3)return""}else K=0;if(Q<89)await j.wait({time:1})}await j.screenshot({path:"/tmp/instagram_post_share_debug.png"});throw new q("Instagram post share confirmation did not appear","Inspect /tmp/instagram_post_share_debug.png for the final publish state")}async function o(j){return(await j.getCookies({domain:"instagram.com"})).find((Q)=>Q.name==="ds_user_id")?.value||""}async function u(j,K=""){if(K){const Y=await $j(j),Z=await j.evaluate(`
|
|
607
607
|
(async () => {
|
|
608
|
-
const userId = ${JSON.stringify(
|
|
609
|
-
const appId = ${JSON.stringify(
|
|
608
|
+
const userId = ${JSON.stringify(K)};
|
|
609
|
+
const appId = ${JSON.stringify(Y.appId||"")};
|
|
610
610
|
try {
|
|
611
611
|
const res = await fetch(
|
|
612
612
|
'https://www.instagram.com/api/v1/users/' + encodeURIComponent(userId) + '/info/',
|
|
@@ -623,7 +623,7 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
623
623
|
return { ok: false };
|
|
624
624
|
}
|
|
625
625
|
})()
|
|
626
|
-
`);if(
|
|
626
|
+
`);if(Z?.ok&&Z.username)return new URL(`/${Z.username}/`,N).toString()}const Q=await j.evaluate(`
|
|
627
627
|
(() => {
|
|
628
628
|
const isVisible = (el) => {
|
|
629
629
|
if (!(el instanceof HTMLElement)) return false;
|
|
@@ -648,7 +648,7 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
648
648
|
const path = explicitProfile;
|
|
649
649
|
return { ok: !!path, path };
|
|
650
650
|
})()
|
|
651
|
-
`);if(!
|
|
651
|
+
`);if(!Q?.ok||!Q.path)return"";return new URL(Q.path,N).toString()}async function i(j){const K=await j.evaluate(`
|
|
652
652
|
(() => {
|
|
653
653
|
const isVisible = (el) => {
|
|
654
654
|
if (!(el instanceof HTMLElement)) return false;
|
|
@@ -668,4 +668,4 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
|
|
|
668
668
|
|
|
669
669
|
return { ok: hrefs.length > 0, hrefs };
|
|
670
670
|
})()
|
|
671
|
-
`);return Array.isArray(
|
|
671
|
+
`);return Array.isArray(K?.hrefs)?K.hrefs.filter(Boolean):[]}async function mj(j){const K=await o(j);if(!K)return null;const Q=await u(j,K);if(!Q)return null;try{await j.goto(Q);await j.wait({time:3});return new Set(await i(j))}catch{return null}}async function x(j,K){const Q=await j.getCurrentUrl?.();if(Q&&/\/p\//.test(Q))return Q;const Y=await o(j),Z=await u(j,Y);if(!Z)return"";await j.goto(Z);await j.wait({time:4});for(let $=0;$<8;$++){const z=(await i(j)).find((G)=>!K.has(G))||"";if(z)return new URL(z,N).toString();if($<7)await j.wait({time:1})}return""}r({site:"instagram",name:"post",access:"write",description:"Post an Instagram feed image or mixed-media carousel",domain:"www.instagram.com",strategy:t.UI,browser:!0,args:[{name:"media",required:!1,valueRequired:!0,help:`Comma-separated media paths (images/videos, up to ${h})`},{name:"content",fromFile:!0,positional:!0,required:!1,help:"Caption text"},{name:"timeout",type:"int",required:!1,default:300,help:"Max seconds for the overall command (default: 300)"}],columns:["status","detail","url","media_id","account_id"],validateArgs:Hj,func:async(j,K)=>{const Q=Gj(j),Y=Xj(K);I(K.content);const Z=String(K.content??"").trim(),$=await mj(Q),J=$!==null,z=$??new Set,G=Rj(Y),X=yj(Y),T=xj(Y),_=hj(Y),D=kj(Y),M=bj(Y),C=wj(Y),B=process.env.OPENCLI_INSTAGRAM_CAPTURE==="1",f=[],U=[],p=async()=>{if(!B)return;await e(Q)},S=async()=>{if(!B)return;const V=await jj(Q);if(V.data.length)f.push(...V.data);if(V.errors.length)U.push(...V.errors)};try{try{return await Dj({page:Q,mediaItems:Y,content:Z,existingPostPaths:z,canDiffProfile:J})}catch(V){if(V instanceof A)throw V;if(Zj(V)){let O="",P=!1;if(J)try{O=await x(Q,z);P=!0}catch{}if(O)return R(Y,O);if(!P){const a=V instanceof Error?V.message:String(V);throw new q(`Instagram publish outcome uncertain: ${a}`,"The post may already be live. Do NOT rerun this command blindly — check the profile for a new post first; retrying on a false failure creates a duplicate post.")}}if(!Nj(V))throw V;try{return await Fj({page:Q,mediaItems:Y,content:Z,existingPostPaths:z,canDiffProfile:J,commandAttemptBudget:G,preUploadDelaySeconds:X,uploadAttemptBudget:T,previewProbeWindowSeconds:_,finalPreviewWaitSeconds:D,preShareDelaySeconds:M,inlineUploadRetryBudget:C,installProtocolCapture:p,drainProtocolCapture:S,forceFreshStart:!0})}catch(O){if(O instanceof A)throw O;if(O instanceof q)throw new q(_j(V,O),Tj(V,O));throw O}}}finally{if(B){try{await S()}catch{}E.writeFileSync(Vj,JSON.stringify({data:f,errors:U},null,2))}}}});
|
package/clis/instagram/user.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as a,Strategy as r}from"@jackwener/opencli/registry";import{ArgumentError as j,AuthRequiredError as _,CommandExecutionError as B,EmptyResultError as t}from"@jackwener/opencli/errors";const e="936619743392459",z=12,C=1000,S=50,QQ="https://www.instagram.com",h="contains",w=["contains","regex"],ZQ=/^\d{4}-\d{2}-\d{2}$/,$Q=/^\d{10}$/,GQ=/^\d{13}$/;function I(Q,Z){const{defaultValue:$,max:G,label:V}=Z,Y=Q===void 0?$:Number(Q);if(!Number.isInteger(Y)||Y<=0)throw new j(`${V} must be a positive integer`);if(Y>G)throw new j(`${V} must be <= ${G}`);return Y}function x(Q){return String(Q?.code||"").trim()}function W(Q){return String(Q||"").replace(/\s+/g," ").trim()}function VQ(Q){return String(Q||"").replace(/\r\n?/g,`
|
|
2
|
+
`).trim()}function c(Q){const Z=String(Q||h).trim().toLowerCase();if(!w.includes(Z))throw new j(`Invalid caption-filter-mode: ${Q}. Supported values: contains, regex`);return Z}function M(Q,Z){const{mode:$,caseSensitive:G,label:V}=Z,Y=W(Q);if(!Y)return null;if($==="regex"){const J=G?"":"i";try{const K=new RegExp(Y,J);return(N)=>K.test(W(N))}catch(K){throw new j(`Invalid ${V} regular expression: ${K?.message||K}`)}}const X=G?Y:Y.toLowerCase();return(J)=>{const K=W(J);return G?K.includes(X):K.toLowerCase().includes(X)}}function g(Q){const Z=c(Q["caption-filter-mode"]),$=W(Q["caption-filter"]||Q.captionFilter),G=W(Q["caption-reject"]||Q.captionReject),V=Q["caption-case-sensitive"]===!0||Q.captionCaseSensitive===!0;if(Z==="regex"&&!$&&!G)return null;if(!$&&!G)return null;const Y=$?M($,{mode:Z,caseSensitive:V,label:"caption-filter"}):null,X=G?M(G,{mode:Z,caseSensitive:V,label:"caption-reject"}):null;return{test(J){const K=W(J);if(X&&X(K))return!1;if(!Y)return!0;return Y(K)}}}function p(Q){if(Q?.media_type===1)return"photo";if(Q?.media_type===2)return"video";return"carousel"}function u(Q){const Z=String(Q?.product_type||"").toLowerCase();if(Z==="clips"||Z==="reel")return"reel";if(Q?.is_reel_media)return"reel";return"p"}function l(Q){const Z=Number(Q||0);return Number.isFinite(Z)&&Z>0?new Date(Z*1000).toLocaleDateString():""}function d(Q){const Z=Number(Q||0);return Number.isFinite(Z)&&Z>0?new Date(Z*1000).toISOString():""}function m(Q,Z="p"){return Q?`${QQ}/${Z}/${Q}/`:""}function n(Q,Z={}){const $=Z.label||"date",G=String(Q||"").trim();if(!ZQ.test(G))throw new j(`Invalid --${$}: ${JSON.stringify(Q)}. Use YYYY-MM-DD.`);const V=new Date(`${G}T00:00:00`).getTime(),Y=new Date(`${G}T23:59:59.999`).getTime();if(!Number.isFinite(V)||!Number.isFinite(Y))throw new j(`Invalid --${$}: ${JSON.stringify(Q)}. Use YYYY-MM-DD.`);return{startSec:Math.floor(V/1000),endSec:Math.floor(Y/1000)}}function k(Q,Z={}){const $=Z.label||"from",G=String(Q||"").trim();if(!G)throw new j(`Missing --${$} timestamp`);if($Q.test(G))return Number(G);if(GQ.test(G))return Math.floor(Number(G)/1000);const V=new Date(G).getTime();if(!Number.isFinite(V))throw new j(`Invalid --${$}: ${JSON.stringify(Q)}. Use ISO 8601 like 2026-07-11T18:30:00+05:30 or Unix seconds.`);return Math.floor(V/1000)}function s(Q){const Z=String(Q.date||"").trim(),$=String(Q.from||"").trim(),G=String(Q.to||"").trim();if(Z&&($||G))throw new j("Cannot combine --date with --from/--to. Use --date YYYY-MM-DD for a whole day, or --from/--to for an exact time range.");if(G&&!$)throw new j("Using --to requires --from.");if(Z)return n(Z,{label:"date"});if(!$&&!G)return null;const V=k($,{label:"from"}),Y=G?k(G,{label:"to"}):Number.POSITIVE_INFINITY;if(Y<V)throw new j("--from must be earlier than or equal to --to.");return{startSec:V,endSec:Y}}async function T(Q,Z){try{const $=await Q.evaluate(async(G,V)=>{const X=await fetch(G,{credentials:"include",headers:{"X-IG-App-ID":V}}),J=await X.text();let K=null;try{K=J?JSON.parse(J):null}catch{K=null}return{ok:X.ok,status:X.status,text:J,data:K}},Z,e);if(!$||typeof $!=="object")throw Error("Instagram API returned no payload");return $}catch($){throw new B(`Instagram API request failed for ${Z}: ${$?.message||$}`)}}function P(Q){const Z=String(Q?.text||"").toLowerCase();return Number(Q?.status||0)===429||Z.includes("rate limit")||Z.includes("wait a few minutes")}function R(Q){const Z=Number(Q?.status||0);if(Z===401||Z===403)return!0;const $=String(Q?.text||"").toLowerCase();return $.includes("login")||$.includes("require_login")}function E(Q,Z){const $=x(Q),G=u(Q),V=Q?.caption?.text||Q?.caption||"";return{index:Z,caption:VQ(V),likes:Q?.like_count??0,comments:Q?.comment_count??0,type:p(Q),date:l(Q?.taken_at),posted_at:d(Q?.taken_at),kind:G,shortcode:$,media_id:String(Q?.id||""),url:m($,G)}}function A(Q){return Array.isArray(Q)?Q:[]}function o(Q,Z){if(!Z)return!0;const $=Number(Q||0);if(!Number.isFinite($)||$<=0)return!1;return $>=Z.startSec&&$<=Z.endSec}async function YQ(Q,Z,$,G){const V=[],Y=new Set;let X="",J=!0;while(V.length<$&&J){const K=$-V.length,N=K>S?S:K,L=`https://www.instagram.com/api/v1/feed/user/${encodeURIComponent(Z)}/?count=${N}`,f=X?`${L}&max_id=${encodeURIComponent(X)}`:L,y=await T(Q,f);if(!y.ok){if(R(y))throw new _("www.instagram.com",y.text||`HTTP ${y.status}`);if(P(y))throw new B("Instagram feed request is rate limited","Wait a few minutes and retry.");throw new B("Failed to fetch user posts",`Instagram returned HTTP ${y.status} for ${f}`)}const H=y.data||{};if(!H||typeof H!=="object")throw new B("Instagram returned an unexpected response for user feed");const b=A(H.items);if(b.length===0)break;for(const q of b){const O=x(q);if(!O||Y.has(O))continue;const U=q?.caption?.text||q?.caption||"";if(G&&!G.test(U))continue;Y.add(O);V.push(E(q,V.length+1));if(V.length>=$)return V}if(!H.more_available){J=!1;break}const D=String(H.next_max_id||"").trim();if(!D||D===X)break;X=D;if(V.length<$)await Q.wait({time:0.5})}return V}async function KQ(Q,Z,$,G,V){const Y=[],X=new Set;let J="",K=!0;while(Y.length<$&&K){const N=$-Y.length,L=N>S?S:N,f=`https://www.instagram.com/api/v1/feed/user/${encodeURIComponent(Z)}/?count=${L}`,y=J?`${f}&max_id=${encodeURIComponent(J)}`:f,H=await T(Q,y);if(!H.ok){if(R(H))throw new _("www.instagram.com",H.text||`HTTP ${H.status}`);if(P(H))throw new B("Instagram feed request is rate limited","Wait a few minutes and retry.");throw new B("Failed to fetch user posts",`Instagram returned HTTP ${H.status} for ${y}`)}const b=H.data||{};if(!b||typeof b!=="object")throw new B("Instagram returned an unexpected response for user feed");const D=A(b.items);if(D.length===0)break;let q=!1;for(const U of D){const v=Number(U?.taken_at||0);if(Number.isFinite(v)&&v>0&&v<V.startSec){q=!0;break}const F=x(U);if(!F||X.has(F))continue;if(!o(v,V))continue;const i=U?.caption?.text||U?.caption||"";if(G&&!G.test(i))continue;X.add(F);Y.push(E(U,Y.length+1));if(Y.length>=$)return Y}if(q||!b.more_available){K=!1;break}const O=String(b.next_max_id||"").trim();if(!O||O===J)break;J=O;if(Y.length<$)await Q.wait({time:0.5})}return Y}a({site:"instagram",name:"user",access:"read",description:"Get recent posts from an Instagram user",domain:"www.instagram.com",strategy:r.COOKIE,browser:!0,navigateBefore:"https://www.instagram.com",args:[{name:"username",required:!0,positional:!0,help:"Instagram username"},{name:"limit",type:"int",default:z,help:`Number of posts (1-${C})`},{name:"date",default:"",help:"Fetch posts from one full local day. Format: YYYY-MM-DD. Cannot be combined with --from/--to."},{name:"from",default:"",help:"Fetch posts from this timestamp onward. Use ISO 8601 like 2026-07-11T18:30:00+05:30 or Unix seconds."},{name:"to",default:"",help:"Fetch posts up to this timestamp. Requires --from. Use ISO 8601 like 2026-07-12T18:30:00+05:30 or Unix seconds."},{name:"caption-filter-mode",default:h,choices:w,help:"Caption matching mode: contains or regex"},{name:"caption-filter",default:"",help:"Keep only posts whose caption matches this pattern"},{name:"caption-reject",default:"",help:"Drop posts whose caption matches this pattern (same mode as caption-filter-mode)"},{name:"caption-case-sensitive",type:"bool",default:!1,help:"Match captions case-sensitively"}],columns:["index","caption","likes","comments","type","date","posted_at","kind","shortcode","media_id","url"],func:async(Q,Z)=>{const $=String(Z.username||"").trim().replace(/^@+/,"");if(!$)throw new j("Instagram username is required");const G=I(Z.limit,{defaultValue:z,max:C,label:"Instagram user limit"}),V=g(Z),Y=s(Z),X=`https://www.instagram.com/api/v1/users/web_profile_info/?username=${encodeURIComponent($)}`,J=await T(Q,X);if(!J.ok){if(R(J))throw new _("www.instagram.com","Instagram login required to read user profile");if(P(J))throw new B("Instagram profile request is rate limited","Wait a few minutes and retry.");throw new B("Failed to resolve Instagram user",`Instagram returned HTTP ${J.status} for profile lookup`)}const K=String(J.data?.data?.user?.id||"").trim();if(!K)throw new B(`Instagram user not found: ${$}`);const N=Y?await KQ(Q,K,G,V,Y):await YQ(Q,K,G,V);if(N.length===0)throw new t("instagram user",`No posts found for ${$}`);return N},__test__:{DEFAULT_LIMIT:z,MAX_LIMIT:C,DEFAULT_CAPTION_FILTER_MODE:h,BATCH_LIMIT:S,parseFeedItems:A,normalizeCaptionFilterMode:c,compileCaptionMatcher:M,makeCaptionFilter:g,resolvePostKind:u,normalizeFeedItem:E,normalizeMediaType:p,requirePositiveBoundedInt:I,buildInstagramPostUrl:m,parseDayBounds:n,parseTimestampArg:k,resolveTimeWindow:s,isWithinTimeWindow:o,isAuthRequired:R,isRateLimited:P,formatInstagramDate:l,formatInstagramPostedAt:d}});
|
package/clis/jike/api.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import{CommandExecutionError as H,AuthRequiredError as L}from"@jackwener/opencli/errors";export const JIKE_API_ORIGIN="https://web.okjike.com";export async function gotoJikeApiContext(B){await B.goto(JIKE_API_ORIGIN);for(let D=0;D<20;D++){const z=await B.evaluate(`(() => {
|
|
2
|
+
var href = ''; try { href = location.href; } catch (e) {}
|
|
3
|
+
var token = null; try { token = localStorage.getItem('JK_ACCESS_TOKEN'); } catch (e) {}
|
|
4
|
+
return { href: href, hasToken: !!token };
|
|
5
|
+
})()`);if(z&&/^https:\/\/web\.okjike\.com/.test(z.href)){if(z.hasToken)return;throw new L("web.okjike.com","即刻未登录(web.okjike.com 的 localStorage 里没有 JK_ACCESS_TOKEN)")}await B.wait(0.5)}throw new H("无法在浏览器中打开 web.okjike.com(页面未落到该站点)。请确认 PublishPort 托管浏览器已连接。")}export async function jikeApi(B,D,z=null){const M=`(async () => {
|
|
6
|
+
var token = null;
|
|
7
|
+
try { token = localStorage.getItem('JK_ACCESS_TOKEN'); } catch (e) {}
|
|
8
|
+
if (!token) return { authMissing: true };
|
|
9
|
+
var init = {
|
|
10
|
+
method: ${z?"'POST'":"'GET'"},
|
|
11
|
+
headers: {
|
|
12
|
+
'x-jike-access-token': token,
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
},
|
|
15
|
+
credentials: 'omit',
|
|
16
|
+
};
|
|
17
|
+
${z?`init.body = ${JSON.stringify(JSON.stringify(z))};`:""}
|
|
18
|
+
var resp;
|
|
19
|
+
try {
|
|
20
|
+
resp = await fetch(${JSON.stringify("https://api.ruguoapp.com"+D)}, init);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
// 请求根本没发出去(网络/CORS),此时平台侧一定没收到,重试是安全的
|
|
23
|
+
return { networkError: String((e && e.message) || e) };
|
|
24
|
+
}
|
|
25
|
+
var raw = '';
|
|
26
|
+
try { raw = await resp.text(); } catch (e) {}
|
|
27
|
+
var data = null;
|
|
28
|
+
try { data = JSON.parse(raw); } catch (e) {}
|
|
29
|
+
return { status: resp.status, ok: resp.ok, data: data, raw: raw.slice(0, 500) };
|
|
30
|
+
})()`,f=await B.evaluate(M);if(f&&f.authMissing)throw new L("web.okjike.com","即刻未登录(读不到 JK_ACCESS_TOKEN)");if(f&&f.networkError)throw new H(`即刻 API 请求未能发出(${f.networkError})。请求没有到达平台,重试是安全的。`);if(!f)throw new H("即刻 API 调用没有返回结果");if(f.status===401)throw new L("web.okjike.com","即刻登录态已失效(API 返回 401),请重新登录");return f}
|
package/clis/jike/comment.js
CHANGED
|
@@ -1,82 +1 @@
|
|
|
1
|
-
import{CommandExecutionError as
|
|
2
|
-
const editor =
|
|
3
|
-
document.querySelector('[class*="_comment_"] [contenteditable="true"]') ||
|
|
4
|
-
document.querySelector('[contenteditable="true"]');
|
|
5
|
-
const textarea = document.querySelector('textarea');
|
|
6
|
-
return !!(editor || textarea);
|
|
7
|
-
})()`))break;await f.wait(0.5)}const z=await f.evaluate(`(async () => {
|
|
8
|
-
try {
|
|
9
|
-
const textToInsert = ${JSON.stringify(v.text)};
|
|
10
|
-
|
|
11
|
-
// 优先在评论区容器内找 contenteditable,避免误选页面其他编辑器;
|
|
12
|
-
// 若评论区 class 名变更则回退到全页查找
|
|
13
|
-
const editor =
|
|
14
|
-
document.querySelector('[class*="_comment_"] [contenteditable="true"]') ||
|
|
15
|
-
document.querySelector('[contenteditable="true"]');
|
|
16
|
-
if (editor) {
|
|
17
|
-
editor.focus();
|
|
18
|
-
const dt = new DataTransfer();
|
|
19
|
-
dt.setData('text/plain', textToInsert);
|
|
20
|
-
editor.dispatchEvent(new ClipboardEvent('paste', {
|
|
21
|
-
clipboardData: dt, bubbles: true, cancelable: true,
|
|
22
|
-
}));
|
|
23
|
-
await new Promise(r => setTimeout(r, 800));
|
|
24
|
-
if (editor.textContent?.length > 0) {
|
|
25
|
-
return { ok: true, message: 'contenteditable' };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// 回退:textarea(带评论相关 placeholder)
|
|
30
|
-
const textareas = document.querySelectorAll('textarea');
|
|
31
|
-
for (const ta of textareas) {
|
|
32
|
-
const ph = ta.getAttribute('placeholder') || '';
|
|
33
|
-
if (ph.includes('评论') || ph.includes('回复') || ph.includes('说点什么')) {
|
|
34
|
-
ta.focus();
|
|
35
|
-
const setter = Object.getOwnPropertyDescriptor(
|
|
36
|
-
HTMLTextAreaElement.prototype, 'value'
|
|
37
|
-
)?.set;
|
|
38
|
-
setter?.call(ta, textToInsert);
|
|
39
|
-
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
40
|
-
await new Promise(r => setTimeout(r, 500));
|
|
41
|
-
return { ok: true, message: 'textarea' };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 兜底:任意 textarea
|
|
46
|
-
if (textareas.length > 0) {
|
|
47
|
-
const ta = textareas[0];
|
|
48
|
-
ta.focus();
|
|
49
|
-
const setter = Object.getOwnPropertyDescriptor(
|
|
50
|
-
HTMLTextAreaElement.prototype, 'value'
|
|
51
|
-
)?.set;
|
|
52
|
-
setter?.call(ta, textToInsert);
|
|
53
|
-
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
54
|
-
await new Promise(r => setTimeout(r, 500));
|
|
55
|
-
return { ok: true, message: 'textarea-fallback' };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return { ok: false, message: '未找到评论输入框' };
|
|
59
|
-
} catch (e) {
|
|
60
|
-
return { ok: false, message: e.toString() };
|
|
61
|
-
}
|
|
62
|
-
})()`);if(!z.ok)throw new A(`评论失败:${z.message}`);for(let h=0;h<30;h++){if(await f.evaluate(`(() => {
|
|
63
|
-
return Array.from(document.querySelectorAll('button')).some(btn => {
|
|
64
|
-
const text = btn.textContent?.trim() || '';
|
|
65
|
-
return (text === '回复' || text === '发布' || text === '发送' || text === '评论') && !btn.disabled;
|
|
66
|
-
});
|
|
67
|
-
})()`))break;await f.wait(0.5)}const q=await f.evaluate(`(async () => {
|
|
68
|
-
try {
|
|
69
|
-
await new Promise(r => setTimeout(r, 500));
|
|
70
|
-
const btns = Array.from(document.querySelectorAll('button')).filter(btn => {
|
|
71
|
-
const text = btn.textContent?.trim() || '';
|
|
72
|
-
return (text === '回复' || text === '发布' || text === '发送' || text === '评论') && !btn.disabled;
|
|
73
|
-
});
|
|
74
|
-
if (btns.length === 0) {
|
|
75
|
-
return { ok: false, message: '未找到可用的回复按钮(可能因内容为空而禁用)' };
|
|
76
|
-
}
|
|
77
|
-
btns[0].click();
|
|
78
|
-
return { ok: true, message: '评论发布成功' };
|
|
79
|
-
} catch (e) {
|
|
80
|
-
return { ok: false, message: e.toString() };
|
|
81
|
-
}
|
|
82
|
-
})()`);if(!q.ok)throw new A(`评论失败:${q.message}`);await f.wait(3);return[{status:"success",message:q.message}]}});
|
|
1
|
+
import{CommandExecutionError as f}from"@jackwener/opencli/errors";import{cli as D,Strategy as F}from"@jackwener/opencli/registry";import{gotoJikeApiContext as G,jikeApi as H}from"./api.js";D({site:"jike",name:"comment",access:"write",description:"评论即刻帖子(走即刻内部 API,不依赖页面渲染)",domain:"web.okjike.com",strategy:F.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"帖子 ID"},{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"评论内容(含空格时请用引号整体传入)"}],columns:["status","message","comment_id"],func:async(h,q)=>{const v=String(q.id??"").trim(),z=String(q.text??"").trim();if(!v)throw new f("帖子 ID 不能为空");if(!z)throw new f("评论内容不能为空");await G(h);const b=await H(h,"/1.0/comments/add",{targetId:v,targetType:"ORIGINAL_POST",content:z,pictureKeys:[],syncToPersonalUpdates:!1,force:!1,currentPageName:14,sourcePageName:50});if(!b.ok){const B=b.data&&(b.data.error||b.data.message)||b.raw||`HTTP ${b.status}`;throw new f(`评论失败(HTTP ${b.status}):${B}`)}return[{status:"success",message:"评论成功",comment_id:String(b.data&&b.data.data&&b.data.data.id||b.data&&b.data.id||"")}]}});
|
package/clis/jike/like.js
CHANGED
|
@@ -1,38 +1 @@
|
|
|
1
|
-
import{CommandExecutionError as
|
|
2
|
-
return !!document.querySelector('[class*="_likeButton_"]');
|
|
3
|
-
})()`))break;await b.wait(0.5)}const f=await b.evaluate(`(async () => {
|
|
4
|
-
try {
|
|
5
|
-
// 点赞按钮:class 包含 _likeButton_,在 _actions_ 容器内
|
|
6
|
-
const likeBtn = document.querySelector('[class*="_likeButton_"]');
|
|
7
|
-
if (!likeBtn) {
|
|
8
|
-
// 按钮缺失原因很多(帖子被删 / 未登录 / 选择器失效),把页面现场一并带回,
|
|
9
|
-
// 让调用方(云端 AI)能分诊,而不是只看到一句「未找到点赞按钮」。
|
|
10
|
-
const bodyText = (document.body && document.body.innerText || '').slice(0, 400);
|
|
11
|
-
const deleted = /不存在|已删除|已被删除/.test(bodyText);
|
|
12
|
-
const loginWall = /登录|下载即刻/.test(bodyText) && !document.querySelector('[class*="_avatar_"]');
|
|
13
|
-
let hint = '页面已加载但没有点赞按钮,可能是选择器失效';
|
|
14
|
-
if (deleted) hint = '帖子疑似不存在或已被删除';
|
|
15
|
-
else if (loginWall) hint = '页面疑似处于未登录态(出现登录/下载引导)';
|
|
16
|
-
return { ok: false, message: '未找到点赞按钮:' + hint + '(页面标题「' + document.title + '」)' };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// 已赞状态挂在内层 SVG 图标的类上(_likeIconLiked_),外层容器 div 的
|
|
20
|
-
// 类名点击前后不变——按容器类名判定会把成功点赞误报为失败(真机踩过)。
|
|
21
|
-
const isLiked = () => !!likeBtn.querySelector('[class*="_likeIconLiked_"]');
|
|
22
|
-
if (isLiked()) {
|
|
23
|
-
return { ok: true, message: '该帖子已赞过' };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const beforeCount = (likeBtn.textContent || '').trim();
|
|
27
|
-
likeBtn.click();
|
|
28
|
-
await new Promise(r => setTimeout(r, 1500));
|
|
29
|
-
|
|
30
|
-
// 验证:已赞图标出现或计数变化,任一即成功
|
|
31
|
-
if (isLiked() || (likeBtn.textContent || '').trim() !== beforeCount) {
|
|
32
|
-
return { ok: true, message: '点赞成功' };
|
|
33
|
-
}
|
|
34
|
-
return { ok: false, message: '点赞状态未确认,请手动检查' };
|
|
35
|
-
} catch (e) {
|
|
36
|
-
return { ok: false, message: e.toString() };
|
|
37
|
-
}
|
|
38
|
-
})()`);if(!f.ok)throw new v(`点赞失败:${f.message}`);return[{status:"success",message:f.message}]}});
|
|
1
|
+
import{CommandExecutionError as F}from"@jackwener/opencli/errors";import{cli as H,Strategy as K}from"@jackwener/opencli/registry";import{gotoJikeApiContext as N,jikeApi as Q}from"./api.js";H({site:"jike",name:"like",access:"write",description:"点赞即刻帖子(走即刻内部 API,不依赖页面渲染)",domain:"web.okjike.com",strategy:K.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"帖子 ID"},{name:"undo",type:"boolean",help:"取消点赞(改调 unlike)"}],columns:["status","message"],func:async(q,v)=>{const z=String(v.id??"").trim();if(!z)throw new F("帖子 ID 不能为空");const h=Boolean(v.undo);await N(q);const f=await Q(q,h?"/1.0/originalPosts/unlike":"/1.0/originalPosts/like",{id:z});if(!f.ok){const G=f.data&&(f.data.error||f.data.message)||f.raw||`HTTP ${f.status}`;throw new F(`${h?"取消点赞":"点赞"}失败(HTTP ${f.status}):${G}`)}return[{status:"success",message:h?"已取消点赞":"点赞成功"}]}});
|
package/clis/jike/repost.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import{CommandExecutionError as
|
|
1
|
+
import{CommandExecutionError as z}from"@jackwener/opencli/errors";import{cli as I,Strategy as K}from"@jackwener/opencli/registry";I({site:"jike",name:"repost",access:"write",description:"转发即刻帖子",domain:"web.okjike.com",strategy:K.UI,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"帖子 ID"},{name:"text",fromFile:!0,positional:!0,type:"string",required:!1,help:"转发附言(可选)"}],columns:["status","message"],func:async(h,B)=>{await h.goto(`https://web.okjike.com/originalPost/${B.id}`);for(let q=0;q<30;q++){if(await h.evaluate("(() => document.querySelectorAll('button').length > 0)()"))break;await h.wait(0.5)}const A=await h.evaluate(`(() => {
|
|
2
|
+
var root = document.getElementById('root');
|
|
3
|
+
return {
|
|
4
|
+
buttons: document.querySelectorAll('button').length,
|
|
5
|
+
rootChildren: root ? root.children.length : -1,
|
|
6
|
+
href: location.href,
|
|
7
|
+
};
|
|
8
|
+
})()`);if(A&&A.buttons===0)throw new z("即刻 web 端未渲染出任何可交互元素,无法完成转发:"+`当前地址 ${A.href},#root 子节点 ${A.rootChildren} 个、button 0 个。`+`
|
|
9
|
+
web.okjike.com 已改版为 SPA,在本环境下不渲染组件树,转发这条链路尚未迁移到内部 API。`+`
|
|
10
|
+
这不是帖子状态或选择器的问题,重试无用;评论(jike comment)与点赞(jike like)已改走 API,不受影响。`);for(let q=0;q<30;q++){if(await h.evaluate(`(() => {
|
|
2
11
|
const actions = document.querySelector('[class*="_actions_"]');
|
|
3
12
|
if (!actions) return false;
|
|
4
13
|
const children = Array.from(actions.children).filter(c => c.offsetHeight > 0);
|
|
5
14
|
return !!children[2];
|
|
6
|
-
})()`))break;await
|
|
15
|
+
})()`))break;await h.wait(0.5)}const F=await h.evaluate(`(async () => {
|
|
7
16
|
try {
|
|
8
17
|
const actions = document.querySelector('[class*="_actions_"]');
|
|
9
18
|
if (!actions) return { ok: false, message: '未找到操作栏' };
|
|
@@ -15,11 +24,11 @@ import{CommandExecutionError as v}from"@jackwener/opencli/errors";import{cli as
|
|
|
15
24
|
} catch (e) {
|
|
16
25
|
return { ok: false, message: e.toString() };
|
|
17
26
|
}
|
|
18
|
-
})()`);if(!
|
|
27
|
+
})()`);if(!F.ok)throw new z(`转发失败:${F.message}`);await h.wait(1);for(let q=0;q<30;q++){if(await h.evaluate(`(() => {
|
|
19
28
|
return Array.from(document.querySelectorAll('button')).some(
|
|
20
29
|
b => b.textContent?.trim() === '转发动态'
|
|
21
30
|
);
|
|
22
|
-
})()`))break;await
|
|
31
|
+
})()`))break;await h.wait(0.5)}const G=await h.evaluate(`(async () => {
|
|
23
32
|
try {
|
|
24
33
|
const btn = Array.from(document.querySelectorAll('button')).find(
|
|
25
34
|
b => b.textContent?.trim() === '转发动态'
|
|
@@ -30,11 +39,11 @@ import{CommandExecutionError as v}from"@jackwener/opencli/errors";import{cli as
|
|
|
30
39
|
} catch (e) {
|
|
31
40
|
return { ok: false, message: e.toString() };
|
|
32
41
|
}
|
|
33
|
-
})()`);if(!
|
|
42
|
+
})()`);if(!G.ok)throw new z(`转发失败:${G.message}`);await h.wait(2);if(B.text){for(let v=0;v<30;v++){if(await h.evaluate(`(() => {
|
|
34
43
|
return !!document.querySelector('[class*="Modal"] [contenteditable="true"], [class*="modal"] [contenteditable="true"]');
|
|
35
|
-
})()`))break;await
|
|
44
|
+
})()`))break;await h.wait(0.5)}const q=await h.evaluate(`(async () => {
|
|
36
45
|
try {
|
|
37
|
-
const textToInsert = ${JSON.stringify(
|
|
46
|
+
const textToInsert = ${JSON.stringify(B.text)};
|
|
38
47
|
const editor = document.querySelector('[class*="Modal"] [contenteditable="true"], [class*="modal"] [contenteditable="true"]');
|
|
39
48
|
if (!editor) return { ok: false, message: '未找到转发弹窗内的附言输入框' };
|
|
40
49
|
editor.focus();
|
|
@@ -46,7 +55,7 @@ import{CommandExecutionError as v}from"@jackwener/opencli/errors";import{cli as
|
|
|
46
55
|
await new Promise(r => setTimeout(r, 500));
|
|
47
56
|
return { ok: true };
|
|
48
57
|
} catch(e) { return { ok: false, message: '附言写入失败: ' + e.toString() }; }
|
|
49
|
-
})()`);if(!
|
|
58
|
+
})()`);if(!q.ok)throw new z(`转发失败:${q.message}`)}const H=`(() => {
|
|
50
59
|
return Array.from(document.querySelectorAll('button')).find(b => {
|
|
51
60
|
const text = b.textContent?.trim() || '';
|
|
52
61
|
if (text !== '转发动态' && text !== '发送' && text !== '发布') return false;
|
|
@@ -54,14 +63,14 @@ import{CommandExecutionError as v}from"@jackwener/opencli/errors";import{cli as
|
|
|
54
63
|
if (b.closest('[class*="Popover"]')) return false;
|
|
55
64
|
return !!(b.closest('[class*="Modal"], [class*="modal"], [class*="_submitButton_"]'));
|
|
56
65
|
});
|
|
57
|
-
})`;for(let
|
|
66
|
+
})`;for(let q=0;q<30;q++){if(await h.evaluate(`(() => !!${H}())()`))break;await h.wait(0.5)}const D=await h.evaluate(`(async () => {
|
|
58
67
|
try {
|
|
59
68
|
await new Promise(r => setTimeout(r, 500));
|
|
60
|
-
const btn = ${
|
|
69
|
+
const btn = ${H}();
|
|
61
70
|
if (!btn) return { ok: false, message: '未找到转发弹窗内的确认按钮' };
|
|
62
71
|
btn.click();
|
|
63
72
|
return { ok: true, message: '转发成功' };
|
|
64
73
|
} catch (e) {
|
|
65
74
|
return { ok: false, message: e.toString() };
|
|
66
75
|
}
|
|
67
|
-
})()`);if(!
|
|
76
|
+
})()`);if(!D.ok)throw new z(`转发失败:${D.message}`);await h.wait(3);return[{status:"success",message:D.message}]}});
|