publishport-opencli 1.0.30 → 1.0.32
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 +345 -71
- package/clis/_shared/article/login.js +1 -1
- package/clis/_shared/login-wait.js +1 -0
- package/clis/_shared/site-auth.js +1 -1
- package/clis/baijiahao/article.js +2 -2
- package/clis/baijiahao/auth.js +18 -0
- package/clis/baijiahao/lists.js +1 -1
- package/clis/baijiahao/stats.js +1 -1
- package/clis/bilibili/coin.js +1 -1
- package/clis/bilibili/comment.js +1 -1
- package/clis/bilibili/comments.js +1 -1
- package/clis/bilibili/dm-list.js +1 -1
- package/clis/bilibili/dm-read.js +1 -1
- package/clis/bilibili/favorite-add.js +1 -1
- package/clis/bilibili/me.js +1 -1
- package/clis/bilibili/search.js +1 -1
- package/clis/bilibili/stats.js +1 -1
- package/clis/bilibili/user-videos.js +1 -1
- package/clis/bilibili/utils.js +7 -7
- package/clis/cnblogs/article.js +1 -1
- package/clis/csdn/stats.js +7 -13
- package/clis/devto/publish.js +20 -4
- package/clis/douyin/_shared/work-list.js +1 -0
- package/clis/douyin/publish.js +9 -8
- package/clis/douyin/stats.js +1 -1
- package/clis/douyin/videos.js +1 -1
- package/clis/jike/comments.js +2 -1
- package/clis/jike/like.js +1 -1
- package/clis/jike/notifications.js +2 -115
- package/clis/jike/post.js +2 -2
- package/clis/jike/topic.js +1 -1
- package/clis/jike/topics.js +3 -0
- package/clis/nowcoder/auth.js +1 -24
- package/clis/nowcoder/comment.js +1 -1
- package/clis/nowcoder/comments.js +1 -1
- package/clis/nowcoder/delete.js +1 -1
- package/clis/nowcoder/detail.js +1 -48
- package/clis/nowcoder/dm-list.js +1 -1
- package/clis/nowcoder/dm-read.js +1 -1
- package/clis/nowcoder/dm-send.js +1 -1
- package/clis/nowcoder/experience.js +1 -22
- package/clis/nowcoder/inbox.js +1 -1
- package/clis/nowcoder/like-status.js +1 -0
- package/clis/nowcoder/like.js +1 -1
- package/clis/nowcoder/my.js +1 -1
- package/clis/nowcoder/notifications.js +1 -16
- package/clis/nowcoder/post.js +1 -1
- package/clis/nowcoder/recommend.js +1 -1
- package/clis/nowcoder/search.js +1 -32
- package/clis/nowcoder/utils.js +38 -44
- package/clis/qiita/publish.js +2 -2
- package/clis/quora/__fixtures__/dom.html +24 -0
- package/clis/quora/answer-detail.js +1 -1
- package/clis/quora/question.js +25 -1
- package/clis/quora/search.js +1 -1
- package/clis/quora/shared.js +18 -17
- package/clis/quora/upvote.js +5 -13
- package/clis/reddit/comment.js +25 -25
- package/clis/reddit/reply.js +7 -5
- package/clis/reddit/search.js +1 -51
- package/clis/reddit/shared.js +13 -0
- package/clis/reddit/subreddit-info.js +10 -4
- package/clis/reddit/subreddit-rules.js +1 -0
- package/clis/reddit/subreddit.js +1 -52
- package/clis/reddit/upvote.js +33 -10
- package/clis/reddit/user-comments.js +1 -20
- package/clis/reddit/user-posts.js +1 -17
- package/clis/twitter/post.js +63 -39
- package/clis/wechat-channels/auth.js +34 -31
- package/clis/wechat-channels/publish.js +295 -130
- package/clis/xianyu/watch.js +43 -1
- package/clis/xiaohongshu/_shared/note-manager.js +37 -0
- package/clis/xiaohongshu/creator-notes.js +9 -43
- package/clis/xiaohongshu/delete-note.js +7 -19
- package/clis/xiaohongshu/draft-delete.js +2 -2
- package/clis/xiaohongshu/publish.js +34 -43
- package/clis/xiaohongshu/search.js +18 -3
- package/clis/youtube/publish.js +11 -11
- package/clis/zhihu/answer-comments.js +2 -11
- package/clis/zhihu/answer-detail.js +1 -11
- package/clis/zhihu/comment.js +4 -4
- package/clis/zhihu/like.js +17 -3
- package/clis/zhihu/question.js +20 -6
- package/clis/zhihu/read-shared.js +26 -0
- package/clis/zhihu/recommend.js +1 -7
- package/clis/zhihu/search.js +1 -11
- package/dist/src/browser/base-page.d.ts +41 -1
- package/dist/src/browser/base-page.js +29 -16
- package/dist/src/browser/find.d.ts +31 -4
- package/dist/src/browser/find.js +76 -23
- package/dist/src/browser/utils.d.ts +30 -0
- package/dist/src/browser/utils.js +7 -1
- package/dist/src/cli.js +22 -22
- package/dist/src/commanderAdapter.d.ts +14 -0
- package/dist/src/commanderAdapter.js +4 -3
- package/dist/src/doctor.d.ts +7 -0
- package/dist/src/doctor.js +9 -9
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.js +1 -1
- package/dist/src/execution.d.ts +1 -0
- package/dist/src/execution.js +2 -2
- package/dist/src/main.js +6 -6
- package/dist/src/types.d.ts +28 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as d,Strategy as m}from"@jackwener/opencli/registry";import{ArgumentError as l,AuthRequiredError as h,CommandExecutionError as c}from"@jackwener/opencli/errors";const w=`
|
|
2
2
|
new Promise((resolve) => {
|
|
3
3
|
const findNoteCard = () => document.querySelector(
|
|
4
4
|
'section.note-item, section:has(a[href*="/search_result/"]), section:has(a[href*="/explore/"])'
|
|
@@ -48,7 +48,22 @@ import{cli as h,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentE
|
|
|
48
48
|
resolve('timeout');
|
|
49
49
|
}, 8000);
|
|
50
50
|
})
|
|
51
|
-
`;export function noteIdToDate(e){const t=e.match(/\/(?:search_result|explore|note)\/([0-9a-f]{24})(?=[?#/]|$)/i);if(!t)return"";const r=t[1].substring(0,8),
|
|
51
|
+
`;export function noteIdToDate(e){const t=e.match(/\/(?:search_result|explore|note)\/([0-9a-f]{24})(?=[?#/]|$)/i);if(!t)return"";const r=t[1].substring(0,8),n=parseInt(r,16);if(!n||n<1e9||n>4000000000)return"";return new Date((n+28800)*1000).toISOString().slice(0,10)}export function stripXhsAuthorDateSuffix(e){const t=(e||"").replace(/\s+/g," ").trim();return t.replace(/\s*(?:\d{1,2}天前|\d+小时前|\d+分钟前|\d+秒前|刚刚|昨天|前天|\d+周前|\d+个月前|\d{1,2}-\d{1,2}|\d{4}-\d{1,2}-\d{1,2})$/u,"").trim()||t}export function unwrapEvaluateResult(e){if(e&&!Array.isArray(e)&&typeof e==="object"&&"session"in e&&"data"in e)return e.data;return e}function f(e,t){const r=unwrapEvaluateResult(e);if(!Array.isArray(r))throw new c(`Unexpected Xiaohongshu search ${t} payload shape; expected an array of rows.`);return r}const u="#search-input-in-feeds",y=`
|
|
52
|
+
(() => {
|
|
53
|
+
const userStore = window.__INITIAL_STATE__?.user;
|
|
54
|
+
if (!userStore || typeof userStore !== 'object') return null;
|
|
55
|
+
const value = userStore.loggedIn?._value ?? userStore.loggedIn;
|
|
56
|
+
return typeof value === 'boolean' ? value : null;
|
|
57
|
+
})()
|
|
58
|
+
`,b=`
|
|
59
|
+
(() => {
|
|
60
|
+
const input = document.querySelector(${JSON.stringify(u)});
|
|
61
|
+
return {
|
|
62
|
+
href: location.href,
|
|
63
|
+
inputValueLength: input && typeof input.value === 'string' ? input.value.length : -1,
|
|
64
|
+
};
|
|
65
|
+
})()
|
|
66
|
+
`;function g(){return new h("www.xiaohongshu.com","小红书主站(www.xiaohongshu.com)未登录,搜索需要主站登录态;创作者后台的登录不覆盖主站。请在浏览器里登录 www.xiaohongshu.com 后重试。")}async function S(e,t){const r=unwrapEvaluateResult(await e.evaluate(p));if(r==="search_route")return;if(r==="security_block")throw new c("Xiaohongshu search hit a security restriction page instead of search results.");if(unwrapEvaluateResult(await e.evaluate(y))===!1)throw g();const n=unwrapEvaluateResult(await e.evaluate(b))||{};throw new c("Xiaohongshu search did not reach a search results route after submitting the query. "+`当前页面: ${n.href||"未知"};搜索框内文本长度 ${n.inputValueLength??"未知"}(期望 ${String(t).length})。`)}async function x(e,t){await e.goto("https://www.xiaohongshu.com/");await e.wait?.(1);await e.click(u);await e.typeText(u,t);await e.pressKey("Enter")}export function parseLimit(e){const t=Number(e??20);if(!Number.isFinite(t)||!Number.isInteger(t))throw new l(`--limit must be an integer between 1 and 100, got ${JSON.stringify(e)}`);if(t<1||t>100)throw new l(`--limit must be between 1 and 100, got ${t}`);return t}export function buildScrollUntilJs(e,t=15){if(!Number.isSafeInteger(e)||e<1)throw new l(`targetCount must be a positive integer, got ${JSON.stringify(e)}`);if(!Number.isSafeInteger(t)||t<1)throw new l(`maxScrolls must be a positive integer, got ${JSON.stringify(t)}`);return`
|
|
52
67
|
(async () => {
|
|
53
68
|
const isVisibleNote = (el) => {
|
|
54
69
|
if (el.classList.contains('query-note-item')) return false;
|
|
@@ -191,4 +206,4 @@ import{cli as h,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentE
|
|
|
191
206
|
|
|
192
207
|
return results;
|
|
193
208
|
})()
|
|
194
|
-
`}export const command=
|
|
209
|
+
`}export const command=d({site:"xiaohongshu",name:"search",access:"read",description:"搜索小红书笔记",domain:"www.xiaohongshu.com",strategy:m.COOKIE,navigateBefore:!1,args:[{name:"query",required:!0,positional:!0,help:"Search keyword"},{name:"limit",type:"int",default:20,help:"Number of results"}],columns:["rank","title","author","likes","published_at","url"],func:async(e,t)=>{const r=parseLimit(t.limit);await x(e,t.query);await S(e,t.query);if(unwrapEvaluateResult(await e.evaluate(w))==="login_wall")throw new h("www.xiaohongshu.com","Xiaohongshu search results are blocked behind a login wall");const i=[...f(await e.evaluate(buildSearchExtractJs("www.xiaohongshu.com")),"initial extraction")];if(i.length<r){await e.evaluate(buildScrollUntilJs(r));const s=f(await e.evaluate(buildSearchExtractJs("www.xiaohongshu.com")),"post-scroll extraction"),a=new Set(i.map((o)=>o.url).filter(Boolean));for(const o of s){if(o?.url&&a.has(o.url))continue;if(o?.url)a.add(o.url);i.push(o);if(i.length>=r)break}}return i.filter((s)=>s.title).slice(0,r).map((s,a)=>({rank:a+1,...s,published_at:noteIdToDate(s.url)}))}}),__test__={stripXhsAuthorDateSuffix};
|
package/clis/youtube/publish.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import*as v from"node:path";import{cli as T,Strategy as I}from"@jackwener/opencli/registry";import{ArgumentError as m,CommandExecutionError as u}from"@jackwener/opencli/errors";import{resolveVideoFile as P,resolveImageFile as O,setVideoInput as x,unwrap as c,currentUrl as V,waitForSelector as p,videoTimeoutArg as C,SUPPORTED_VIDEO_EXTENSIONS as _,SUPPORTED_IMAGE_EXTENSIONS as N}from"../_shared/video-publish.js";const D="https://www.youtube.com/upload";export function assertSingleVideoInput(t){const e=String(t??"").trim(),a="youtube publish 仅支持视频文件(一次一个),没有图文 / 图片集发布能力",n=e.split(",").map((s)=>s.trim()).filter(Boolean);if(n.length>1&&n.every((s)=>v.extname(s)))throw new m(`${a};收到 ${n.length} 个路径。`);const o=v.extname(e).toLowerCase();if(N.has(o))throw new m(`${a};收到的是图片文件(${o})。`);if(!_.has(o))throw new m(`${a};收到的文件扩展名是「${o||"无"}」,支持 ${[..._].join("/")}。`)}const S={public:"PUBLIC",unlisted:"UNLISTED",private:"PRIVATE"},f=`
|
|
2
2
|
function __ytNorm(v) { return (v || '').replace(/\\s+/g, ' ').trim(); }
|
|
3
3
|
function __ytVisible(el) {
|
|
4
4
|
if (!el || el.offsetParent === null) return false;
|
|
@@ -27,19 +27,19 @@ import{cli as _,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentE
|
|
|
27
27
|
}
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
|
-
`;async function
|
|
30
|
+
`;async function i(t,e,a=4000){const n=Date.now()+a;for(;;){if(Boolean(c(await t.evaluate(`
|
|
31
31
|
(() => {
|
|
32
|
-
${
|
|
33
|
-
var el = __ytResolve(${JSON.stringify(
|
|
32
|
+
${f}
|
|
33
|
+
var el = __ytResolve(${JSON.stringify(e)});
|
|
34
34
|
if (!el) return false;
|
|
35
35
|
el.click();
|
|
36
36
|
return true;
|
|
37
37
|
})()
|
|
38
|
-
`))))return!0;if(Date.now()>=
|
|
39
|
-
(() => { ${
|
|
40
|
-
`)))}async function B(t){try{await t.evaluate("() => { const a = document.activeElement; if (a && a.blur) a.blur(); }")}catch{}try{if(Boolean(
|
|
41
|
-
(() => { ${
|
|
42
|
-
`)))){await t.pressKey("Escape");await t.wait({time:0.2})}}catch{}}async function
|
|
38
|
+
`))))return!0;if(Date.now()>=n)return!1;await t.wait({time:0.4})}}async function y(t,e){return Boolean(c(await t.evaluate(`
|
|
39
|
+
(() => { ${f} return !!__ytResolve(${JSON.stringify(e)}); })()
|
|
40
|
+
`)))}async function B(t){try{await t.evaluate("() => { const a = document.activeElement; if (a && a.blur) a.blur(); }")}catch{}try{if(Boolean(c(await t.evaluate(`
|
|
41
|
+
(() => { ${f} return !!__ytResolve('tp-yt-iron-dropdown'); })()
|
|
42
|
+
`)))){await t.pressKey("Escape");await t.wait({time:0.2})}}catch{}}async function E(t,e,a){if(!await p(t,e,30000))throw new u(`YouTube 输入框未出现: ${e}`);await t.click(e);await t.pressKey("Control+a");await t.pressKey("Delete");await t.insertText(a);await t.wait({time:0.4});await B(t)}async function R(t,e=360){const a=/处理|检查|上传完成|已上传|Processing|complete|Checks|Finished/;for(let n=0;n<e;n++){const o=String(c(await t.evaluate(`
|
|
43
43
|
(() => {
|
|
44
44
|
const sels = ['.progress-label', 'span.progress-label', 'ytcp-video-upload-progress'];
|
|
45
45
|
for (const sel of sels) {
|
|
@@ -48,9 +48,9 @@ import{cli as _,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentE
|
|
|
48
48
|
}
|
|
49
49
|
return '';
|
|
50
50
|
})()
|
|
51
|
-
`))||"");if(
|
|
51
|
+
`))||"");if(o&&a.test(o))return!0;await t.wait({time:5})}return!1}T({site:"youtube",name:"publish",access:"write",description:"Publish a video to YouTube via Studio (one command: upload + metadata + publish)",domain:"www.youtube.com",strategy:I.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"video",required:!0,positional:!0,help:"Video file path(仅支持单个视频文件;不支持图片 / 图文)"},{name:"title",required:!0,help:"Video title (≤100 chars)"},{name:"description",fromFile:!0,default:"",help:"Video description"},{name:"tags",default:"",help:"Comma-separated tags"},{name:"thumbnail",default:"",help:"Thumbnail image path (optional)"},{name:"playlist",default:"",help:"Playlist name to add to (optional)"},{name:"visibility",default:"public",choices:["public","unlisted","private"],help:"Visibility"},C()],columns:["status","title","visibility","url"],func:async(t,e)=>{const a=String(e.title??"").trim();if(!a)throw new m("--title is required");assertSingleVideoInput(e.video);const n=P(e.video),o=String(e.description||""),s=e.thumbnail?O(e.thumbnail):"",w=String(e.playlist||"").trim(),d=S[e.visibility]?e.visibility:"public";await t.goto(D);await t.wait({time:3});const b=await V(t);if(/accounts\.google\.com/.test(b)||/signin/i.test(b))throw new u("YouTube 登录态失效,请先 ppcli youtube login");if(!await p(t,'input[type="file"]',60000))throw new u("YouTube 上传文件输入框未出现");await x(t,['input[type="file"]'],[n]);if(!await p(t,"#title-textarea",120000))throw new u("YouTube 视频详情对话框未出现");await E(t,"#title-textarea #textbox",a.slice(0,100));if(o.trim())await E(t,"#description-textarea #textbox",o);if(s)try{await x(t,["#file-loader input[type='file']","ytcp-thumbnail-uploader input[type='file']"],[s]);await t.wait({time:2})}catch{}if(w)try{await i(t,"#basics ytcp-text-dropdown-trigger, ytcp-video-metadata-playlists ytcp-dropdown-trigger",8000);await t.wait({time:1.2});const l=`tp-yt-paper-checkbox:has-text('${w}'), ytcp-checkbox-group:has-text('${w}')`;if(await y(t,l))await i(t,l,3000);else if(await i(t,"ytcp-button:has-text('New playlist'), ytcp-button:has-text('创建播放列表')",4000)){await t.wait({time:0.8});await i(t,"tp-yt-paper-item:has-text('New playlist'), tp-yt-paper-item:has-text('新建播放列表')",3000);const r="ytcp-playlist-metadata-editor #textbox, #create-playlist-form #textbox";if(await p(t,r.split(",")[0].trim(),4000)||await y(t,r)){await i(t,r,2000);await t.insertText(w);await i(t,"ytcp-button#create-button, tp-yt-paper-dialog ytcp-button:has-text('Create'), tp-yt-paper-dialog ytcp-button:has-text('创建')",4000)}}}catch{}finally{await i(t,"ytcp-playlist-dialog #save-button, ytcp-button:has-text('Done'), ytcp-button:has-text('完成')",3000);await t.pressKey("Escape");await t.wait({time:0.6})}if(!await i(t,"tp-yt-paper-radio-button[name='VIDEO_MADE_FOR_KIDS_NOT_MFK']",1e4))await i(t,"tp-yt-paper-radio-button:has-text('not made for kids'), tp-yt-paper-radio-button:has-text('不是面向儿童')",6000);if(e.tags){const l=String(e.tags).split(",").map((r)=>r.trim()).filter(Boolean).join(",");try{await i(t,"#toggle-button",6000);await t.wait({time:0.8});const r="#tags-container #text-input, ytcp-form-input-container#tags-container input";if(await y(t,r)){await i(t,r,3000);await t.insertText(`${l.slice(0,500)},`)}}catch{}}for(let l=0;l<5;l++){if(await y(t,"tp-yt-paper-radio-button[name='PUBLIC']"))break;if(!await i(t,"#next-button",6000))await t.wait({time:1.2});await t.wait({time:1})}await i(t,`tp-yt-paper-radio-button[name='${S[d]}']`,1e4);await R(t);await t.wait({time:1.2});let h="";if(!await i(t,"#done-button",15000))throw new u("未找到 YouTube 发布按钮(上传可能未到可发布进度),请在窗口内手动发布");await t.wait({time:4});h=String(c(await t.evaluate(`
|
|
52
52
|
(() => {
|
|
53
53
|
const a = document.querySelector("a[href*='youtu.be'], a[href*='watch?v=']");
|
|
54
54
|
return a ? (a.getAttribute('href') || '') : '';
|
|
55
55
|
})()
|
|
56
|
-
`))||"");await
|
|
56
|
+
`))||"");await i(t,"ytcp-button:has-text('Close'), ytcp-button:has-text('关闭'), #close-button",8000);return[{status:"✅ Published",title:a,visibility:d,url:h}]}});
|
|
@@ -1,11 +1,2 @@
|
|
|
1
|
-
import{cli as
|
|
2
|
-
|
|
3
|
-
const r = await fetch(${JSON.stringify(u)}, { credentials: 'include' });
|
|
4
|
-
if (!r.ok) return { __httpError: r.status };
|
|
5
|
-
try {
|
|
6
|
-
return await r.json();
|
|
7
|
-
} catch (error) {
|
|
8
|
-
return { __malformedJson: error instanceof Error ? error.message : String(error) };
|
|
9
|
-
}
|
|
10
|
-
})()
|
|
11
|
-
`).catch((m)=>{throw new c(`Zhihu answer comments request failed: ${m instanceof Error?m.message:String(m)}`,"Try again later or rerun with -v for more detail.")});if(!o||o.__httpError){const m=o?.__httpError;if(m===401||m===403)throw new T("www.zhihu.com","Failed to fetch Zhihu answer comments");if(m===404)throw new _("zhihu answer-comments",`No Zhihu answer comments resource was found for ${a}.`);throw new c(m?`Zhihu answer comments request failed (HTTP ${m})`:"Zhihu answer comments request failed","Try again later or rerun with -v for more detail")}if(o.__malformedJson)throw new c(`Zhihu answer comments returned malformed JSON: ${o.__malformedJson}`,"Try again later or rerun with -v for more detail");if(!Array.isArray(o.data)||!o.paging||typeof o.paging!=="object")throw new c("Zhihu answer comments returned a malformed payload","Try again later or rerun with -v for more detail");f.push(...o.data);const d=y(f,{answerId:a,questionId:l,topLevelLimit:r,repliesLimit:i});if(d.malformedComments>0)throw new c("Zhihu answer comments contained rows without comment ids");if(d.reachedTopLevelLimit||o.paging?.is_end){if(d.rows.length===0)throw new _("zhihu answer-comments",`No comments found for answer ${a}.`);return d.rows}const p=A(o.paging?.next,a);if(!p)throw new c("Zhihu answer comments pagination returned malformed next URL");if(h.has(p))throw new c("Zhihu answer comments pagination returned a repeated next URL");u=p}const s=y(f,{answerId:a,questionId:l,topLevelLimit:r,repliesLimit:i});if(s.malformedComments>0)throw new c("Zhihu answer comments contained rows without comment ids");if(s.rows.length===0)throw new _("zhihu answer-comments",`No comments found for answer ${a}.`);return s.rows}});export const __test__={stripHtml:E,parseAnswerTarget:b,normalizeCommentsApiUrl:A,buildRows:y};
|
|
1
|
+
import{cli as C,Strategy as Z}from"@jackwener/opencli/registry";import{ArgumentError as y,CommandExecutionError as c,EmptyResultError as U}from"@jackwener/opencli/errors";import{stripHtml as x}from"./text.js";import{fetchZhihuJson as q}from"./read-shared.js";function R(e){return x(e,{preserveBlocks:!0})}const v=/^\d+$/,k=/^answer:(\d+):(\d+)$/,E=/^\/question\/(\d+)\/answer\/(\d+)\/?$/,H=/^\/answer\/(\d+)\/?$/;function S(e){const n=String(e??"").trim();if(!n)return null;if(v.test(n))return{answerId:n,questionId:""};const t=n.match(k);if(t)return{questionId:t[1],answerId:t[2]};try{const r=new URL(n);if(r.protocol!=="https:"||r.username||r.password||r.port||r.hostname!=="www.zhihu.com"&&r.hostname!=="zhihu.com")return null;let i=r.pathname.match(E);if(i)return{questionId:i[1],answerId:i[2]};i=r.pathname.match(H);if(i)return{answerId:i[1],questionId:""}}catch{return null}return null}function O(e){const n=String(e??"").trim();if(!n)return"";try{const t=new URL(n);if(t.protocol!=="https:"||t.hostname!=="www.zhihu.com"&&t.hostname!=="zhihu.com")return"";return t.pathname.match(E)?.[1]||""}catch{return""}}function P(e){return Number.isInteger(e)&&e>=0?e:0}function D(e){return typeof e==="number"&&Number.isFinite(e)&&e>0?new Date(e*1000).toISOString():""}function A(e){return e?.member?.name||e?.name||""}function W(e){if(typeof e==="string")return e.trim();if(typeof e==="number"&&Number.isSafeInteger(e)&&e>=0)return String(e);return""}function j(e,n,t,r){if(n&&t&&r)return`https://www.zhihu.com/question/${n}/answer/${t}#comment-${r}`;return typeof e==="string"?e:""}function z(e,n){if(typeof e!=="string"||!e)return"";try{const t=new URL(e),r=`/api/v4/answers/${n}/comments`,i=`/answers/${n}/comments`;if(t.protocol!=="https:"||t.username||t.password||t.port)return"";if(t.hostname==="www.zhihu.com"&&t.pathname===r)return t.toString();if(t.hostname==="api.zhihu.com"&&t.pathname===i)return`https://www.zhihu.com${r}${t.search}`}catch{return""}return""}function b(e,{answerId:n,questionId:t,topLevelLimit:r,repliesLimit:i,flat:h=!1}){const a=[];let f=0,d=0,m=0,l=!1,w=0;for(const s of e){if(!s||typeof s!=="object"||Array.isArray(s)){w+=1;continue}const p=W(s.id);if(!p){w+=1;continue}const o=A(s.author),_=A(s.reply_to_author),g=Boolean(_);if(h||!g){if(f>=r){l=!0;break}f+=1;d=f;m=0}else if(!d||m>=i)continue;else m+=1;a.push({rank:a.length+1,comment_rank:d,reply_rank:g&&!h?m:0,depth:0,id:p,parent_id:"",author:o||"anonymous",reply_to:_,likes:P(s.vote_count),created_at:D(s.created_time),url:j(s.url,t,n,p),content:R(s.content||"")})}return{rows:a,topLevelCount:f,reachedTopLevelLimit:l,malformedComments:w}}const L=1000,N=100,B=20,I=["default","newest"],J={default:"normal",newest:"reverse"};function M(e){const n=e?.paging?.totals;if(Number.isInteger(n)&&n>=0)return n;return Number.isInteger(e?.common_counts)&&e.common_counts>=0?e.common_counts:null}function T(e,n,t){if(e.length===0)process.stderr.write(`zhihu answer-comments ${t}: 该回答当前 ${n??0} 条评论。空列表是真实结果,不是取数失败。
|
|
2
|
+
`);return e.map((r)=>({...r,total_comments:n}))}C({site:"zhihu",name:"answer-comments",access:"read",description:"知乎回答评论列表",domain:"www.zhihu.com",strategy:Z.COOKIE,args:[{name:"id",required:!0,positional:!0,help:"Answer ID, full Zhihu answer URL, or typed target (answer:<qid>:<aid>)"},{name:"limit",type:"int",default:20,help:"Number of top-level comments (max 1000); with --sort newest it counts every row, replies included"},{name:"replies-limit",type:"int",default:3,help:"Number of replies to include per top-level comment (max 100); ignored with --sort newest"},{name:"sort",default:"default",choices:I,help:"Comment order: default (Zhihu default order, oldest first) or newest (newest first — use this to read back a comment you just posted)"}],columns:["rank","comment_rank","reply_rank","depth","id","parent_id","author","reply_to","likes","created_at","url","content","total_comments"],func:async(e,n)=>{const t=S(n.id);if(!t)throw new y("Answer ID must be a numeric id, a Zhihu answer URL, or answer:<qid>:<aid>","Example: ppcli zhihu answer-comments 1937205528846655537");const r=Number(n.limit??20);if(!Number.isInteger(r)||r<=0||r>L)throw new y(`--limit must be a positive integer no greater than ${L}`);const i=Number(n["replies-limit"]??3);if(!Number.isInteger(i)||i<0||i>N)throw new y(`--replies-limit must be an integer between 0 and ${N}`);const h=String(n.sort??"default");if(!I.includes(h))throw new y(`--sort must be one of: ${I.join(", ")}`);const{answerId:a}=t;try{await e.goto(`https://www.zhihu.com/answer/${a}`)}catch(o){throw new c(`Failed to open Zhihu answer ${a}: ${o instanceof Error?o.message:String(o)}`,"Open the answer URL in Chrome and retry after the page is reachable.")}const f=e.getCurrentUrl?O(await e.getCurrentUrl().catch(()=>"")):"",d=t.questionId||f;let m=`https://www.zhihu.com/api/v4/answers/${a}/comments?order=${J[h]}&limit=${B}&offset=0&status=open`,l=null;const w=[],s=new Set;while(m&&!s.has(m)){s.add(m);const o=await q(e,m,{label:"回答评论"}).catch((u)=>{if(u&&typeof u.code==="string")throw u;throw new c(`Zhihu answer comments request failed: ${u instanceof Error?u.message:String(u)}`,"Try again later or rerun with -v for more detail.")});if(!o||o.__httpError){const u=o?.__httpError;if(u===404)throw new U("zhihu answer-comments",`No Zhihu answer comments resource was found for ${a}.`);throw new c(u?`Zhihu answer comments request failed (HTTP ${u})`:"Zhihu answer comments request failed","Try again later or rerun with -v for more detail")}if(o.__malformedJson)throw new c(`Zhihu answer comments returned malformed JSON: ${o.__malformedJson}`,"Try again later or rerun with -v for more detail");if(!Array.isArray(o.data)||!o.paging||typeof o.paging!=="object")throw new c("Zhihu answer comments returned a malformed payload","Try again later or rerun with -v for more detail");w.push(...o.data);if(l==null)l=M(o);const _=b(w,{answerId:a,questionId:d,topLevelLimit:r,repliesLimit:i,flat:h==="newest"});if(_.malformedComments>0)throw new c("Zhihu answer comments contained rows without comment ids");if(_.reachedTopLevelLimit||o.paging?.is_end)return T(_.rows,l,a);const g=z(o.paging?.next,a);if(!g)throw new c("Zhihu answer comments pagination returned malformed next URL");if(s.has(g))throw new c("Zhihu answer comments pagination returned a repeated next URL");m=g}const p=b(w,{answerId:a,questionId:d,topLevelLimit:r,repliesLimit:i,flat:h==="newest"});if(p.malformedComments>0)throw new c("Zhihu answer comments contained rows without comment ids");return T(p.rows,l,a)}});export const __test__={stripHtml:R,parseAnswerTarget:S,normalizeCommentsApiUrl:z,buildRows:b};
|
|
@@ -1,11 +1 @@
|
|
|
1
|
-
import{cli as
|
|
2
|
-
(async () => {
|
|
3
|
-
const r = await fetch(${JSON.stringify(E)}, { credentials: 'include' });
|
|
4
|
-
if (!r.ok) return { __httpError: r.status };
|
|
5
|
-
try {
|
|
6
|
-
return await r.json();
|
|
7
|
-
} catch (error) {
|
|
8
|
-
return { __malformedJson: error instanceof Error ? error.message : String(error) };
|
|
9
|
-
}
|
|
10
|
-
})()
|
|
11
|
-
`).catch((o)=>{throw new s(`Zhihu answer detail request failed: ${o instanceof Error?o.message:String(o)}`,"Try again later or rerun with -v for more detail.")});if(!e||e.__httpError){const o=e?.__httpError;if(o===401||o===403)throw new R("www.zhihu.com","Failed to fetch Zhihu answer detail");if(o===404)throw new S("zhihu answer-detail",`No Zhihu answer was found for ${r}.`);throw new s(o?`Zhihu answer detail request failed (HTTP ${o})`:"Zhihu answer detail request failed","Try again later or rerun with -v for more detail")}if(e.__malformedJson)throw new s(`Zhihu answer detail returned malformed JSON: ${e.__malformedJson}`,"Try again later or rerun with -v for more detail");if(typeof e!=="object"||Array.isArray(e))throw new s("Zhihu answer detail returned a malformed payload","Try again later or rerun with -v for more detail");if(e.error||e.error_msg||e.message)throw new s(`Zhihu answer detail returned an error payload: ${e.error?.message||e.error_msg||e.message}`,"Try again later or rerun with -v for more detail");if(!Object.prototype.hasOwnProperty.call(e,"content"))throw new s("Zhihu answer detail payload did not include answer content","Try again later or rerun with -v for more detail");const c=e.question||{},h=n.questionId||y||w(c.url)||(c.id==null?"":String(c.id)),m=p(e.content||""),I=u>0&&m.length>u?m.substring(0,u):m;return[{id:r,author:e.author?.name||"anonymous",votes:_(e.voteup_count),comments:_(e.comment_count),question_id:h,question_title:c.title||"",url:h?`https://www.zhihu.com/question/${h}/answer/${r}`:`https://www.zhihu.com/answer/${r}`,created_at:g(e.created_time),updated_at:g(e.updated_time),content:I}]}});export const __test__={stripHtml:p,extractAnswerId:Z,parseAnswerTarget:d,extractQuestionIdFromAnswerUrl:w};
|
|
1
|
+
import{cli as S,Strategy as x}from"@jackwener/opencli/registry";import{ArgumentError as p,CommandExecutionError as u,EmptyResultError as T}from"@jackwener/opencli/errors";import{stripHtml as z}from"./text.js";import{fetchZhihuJson as b}from"./read-shared.js";function f(t){return z(t,{preserveBlocks:!0})}const N=/^\d+$/,U=/^answer:(\d+):(\d+)$/,g=/^\/question\/(\d+)\/answer\/(\d+)\/?$/,Z=/^\/answer\/(\d+)\/?$/,v=/^\/question\/(\d+)\/?$/,O=/^\/api\/v4\/questions\/(\d+)\/?$/;function d(t){const o=String(t??"").trim();if(!o)return null;if(N.test(o))return{answerId:o,questionId:""};const n=o.match(U);if(n)return{questionId:n[1],answerId:n[2]};try{const r=new URL(o);if(r.protocol!=="https:"||r.username||r.password||r.port||r.hostname!=="www.zhihu.com"&&r.hostname!=="zhihu.com")return null;let a=r.pathname.match(g);if(a)return{questionId:a[1],answerId:a[2]};a=r.pathname.match(Z);if(a)return{answerId:a[1],questionId:""}}catch{return null}return null}function C(t){return d(t)?.answerId??null}function w(t){const o=String(t??"").trim();if(!o)return"";try{const n=new URL(o);if(n.protocol!=="https:"||n.hostname!=="www.zhihu.com"&&n.hostname!=="zhihu.com")return"";return n.pathname.match(g)?.[1]||n.pathname.match(v)?.[1]||n.pathname.match(O)?.[1]||""}catch{return""}}function _(t){return Number.isInteger(t)&&t>=0?t:0}function y(t){return typeof t==="number"&&Number.isFinite(t)&&t>0?new Date(t*1000).toISOString():""}function E(t,o){const n=t.match(new RegExp(`\\s${o}="([^"]*)"`,"i"));return n?n[1].replace(/&/g,"&").trim():""}function I(t){const o=[],n=new Set;for(const r of String(t||"").match(/<img\b[^>]*>/gi)||[]){const a=E(r,"src");if(!a||/^data:/i.test(a))continue;if(/\/equation\?tex=/.test(a))continue;const s=E(r,"data-original")||a;if(n.has(s))continue;n.add(s);o.push(s)}return o}S({site:"zhihu",name:"answer-detail",access:"read",description:"知乎单个回答完整内容(按 answer ID 获取)",domain:"www.zhihu.com",strategy:x.COOKIE,args:[{name:"id",required:!0,positional:!0,help:"Answer ID, full Zhihu answer URL, or typed target (answer:<qid>:<aid>)"},{name:"max-content",type:"int",default:0,help:"Optional cap on stripped content length in characters (0 = no truncation, return the full answer)"}],columns:["id","author","votes","comments","question_id","question_title","url","created_at","updated_at","image_count","images","content"],func:async(t,o)=>{const n=d(o.id);if(!n)throw new p("Answer ID must be a numeric id, a Zhihu answer URL, or answer:<qid>:<aid>","Example: ppcli zhihu answer-detail 1937205528846655537");const{answerId:r}=n,a=o["max-content"],s=a==null?0:Number(a);if(!Number.isInteger(s)||s<0)throw new p("--max-content must be a non-negative integer (0 = no cap, full content)","Example: --max-content 2000");try{await t.goto(`https://www.zhihu.com/answer/${r}`)}catch(i){throw new u(`Failed to open Zhihu answer ${r}: ${i instanceof Error?i.message:String(i)}`,"Open the answer URL in Chrome and retry after the page is reachable.")}const q=t.getCurrentUrl?w(await t.getCurrentUrl().catch(()=>"")):"",A=`https://www.zhihu.com/api/v4/answers/${r}?include=content,voteup_count,comment_count,author,created_time,updated_time,question`,e=await b(t,A,{label:"回答详情"}).catch((i)=>{if(i&&typeof i.code==="string")throw i;throw new u(`Zhihu answer detail request failed: ${i instanceof Error?i.message:String(i)}`,"Try again later or rerun with -v for more detail.")});if(!e||e.__httpError){const i=e?.__httpError;if(i===404)throw new T("zhihu answer-detail",`No Zhihu answer was found for ${r}.`);throw new u(i?`Zhihu answer detail request failed (HTTP ${i})`:"Zhihu answer detail request failed","Try again later or rerun with -v for more detail")}if(e.__malformedJson)throw new u(`Zhihu answer detail returned malformed JSON: ${e.__malformedJson}`,"Try again later or rerun with -v for more detail");if(typeof e!=="object"||Array.isArray(e))throw new u("Zhihu answer detail returned a malformed payload","Try again later or rerun with -v for more detail");if(e.error||e.error_msg||e.message)throw new u(`Zhihu answer detail returned an error payload: ${e.error?.message||e.error_msg||e.message}`,"Try again later or rerun with -v for more detail");if(!Object.prototype.hasOwnProperty.call(e,"content"))throw new u("Zhihu answer detail payload did not include answer content","Try again later or rerun with -v for more detail");const c=e.question||{},h=n.questionId||q||w(c.url)||(c.id==null?"":String(c.id)),l=I(e.content||""),m=f(e.content||""),R=s>0&&m.length>s?m.substring(0,s):m;return[{id:r,author:e.author?.name||"anonymous",votes:_(e.voteup_count),comments:_(e.comment_count),question_id:h,question_title:c.title||"",url:h?`https://www.zhihu.com/question/${h}/answer/${r}`:`https://www.zhihu.com/answer/${r}`,created_at:y(e.created_time),updated_at:y(e.updated_time),image_count:l.length,images:l,content:R}]}});export const __test__={stripHtml:f,extractAnswerId:C,parseAnswerTarget:d,extractQuestionIdFromAnswerUrl:w,extractAnswerImages:I};
|
package/clis/zhihu/comment.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{CliError as s,CommandExecutionError as
|
|
2
|
-
var targetKind = ${JSON.stringify(
|
|
3
|
-
var targetId = ${JSON.stringify(
|
|
1
|
+
import{CliError as s,CommandExecutionError as d}from"@jackwener/opencli/errors";import{cli as u,Strategy as m}from"@jackwener/opencli/registry";import{assertAllowedKinds as c,parseTarget as l}from"./target.js";import{buildResultRow as p,requireExecute as w,resolveCurrentUserIdentity as h,resolvePayload as y}from"./write-shared.js";u({site:"zhihu",name:"comment",access:"write",description:"Create a top-level comment on a Zhihu answer or article",domain:"zhihu.com",strategy:m.COOKIE,browser:!0,args:[{name:"target",positional:!0,required:!0,help:"Full Zhihu answer/article URL, or typed target answer:<questionId>:<answerId> / article:<id>. A bare answer id is NOT accepted"},{name:"text",positional:!0,help:"Comment text"},{name:"file",help:"Comment text file path"},{name:"execute",type:"boolean",help:"Actually perform the write action"}],columns:["status","outcome","message","target_type","target","author_identity","created_url"],func:async(t,r)=>{if(!t)throw new d("Browser session required for zhihu comment");w(r);const o=String(r.target),e=c("comment",l(o)),i=await y(r);await t.goto(e.url);await t.wait(3);const n=await h(t),a=await t.evaluate(`(async () => {
|
|
2
|
+
var targetKind = ${JSON.stringify(e.kind)};
|
|
3
|
+
var targetId = ${JSON.stringify(e.id)};
|
|
4
4
|
var content = ${JSON.stringify(i)};
|
|
5
5
|
var resourceType = targetKind === 'answer' ? 'answers' : 'articles';
|
|
6
6
|
var url = 'https://www.zhihu.com/api/v4/' + resourceType + '/' + targetId + '/comments';
|
|
@@ -14,4 +14,4 @@ import{CliError as s,CommandExecutionError as m}from"@jackwener/opencli/errors";
|
|
|
14
14
|
if (!resp.ok) return { ok: false, status: resp.status, message: data.error ? data.error.message : 'unknown error' };
|
|
15
15
|
if (!data || !data.id) return { ok: false, status: resp.status, message: 'Comment API response did not include a created comment id' };
|
|
16
16
|
return { ok: true, id: data.id, url: data.url };
|
|
17
|
-
})()`);if(!a?.ok)throw new s("COMMAND_EXEC",a?.message||"Failed to create comment");return p(`Commented on ${
|
|
17
|
+
})()`);if(!a?.ok)throw new s("COMMAND_EXEC",a?.message||"Failed to create comment");return p(`Commented on ${e.kind} ${e.id}`,e.kind,o,"created",{author_identity:n,created_url:`${e.url}#comment-${a.id}`})}});
|
package/clis/zhihu/like.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{CliError as
|
|
1
|
+
import{CliError as n,CommandExecutionError as u}from"@jackwener/opencli/errors";import{cli as d,Strategy as c}from"@jackwener/opencli/registry";import{assertAllowedKinds as l,parseTarget as w}from"./target.js";import{buildResultRow as h,requireExecute as m}from"./write-shared.js";d({site:"zhihu",name:"like",access:"write",description:"Like a Zhihu answer or article",domain:"zhihu.com",strategy:c.COOKIE,browser:!0,args:[{name:"target",positional:!0,required:!0,help:"Full Zhihu answer/article URL, or typed target answer:<questionId>:<answerId> / article:<id>. A bare answer id is NOT accepted"},{name:"execute",type:"boolean",help:"Actually perform the write action"}],columns:["status","outcome","message","target_type","target","question_id"],func:async(t,s)=>{if(!t)throw new u("Browser session required for zhihu like");m(s);const o=String(s.target),e=l("like",w(o));await t.goto("https://www.zhihu.com");await t.wait(2);const a=await t.evaluate(`(async () => {
|
|
2
2
|
var targetKind = ${JSON.stringify(e.kind)};
|
|
3
3
|
var targetId = ${JSON.stringify(e.id)};
|
|
4
4
|
var resourceType = targetKind === 'answer' ? 'answers' : 'articles';
|
|
@@ -12,5 +12,19 @@ import{CliError as s,CommandExecutionError as o}from"@jackwener/opencli/errors";
|
|
|
12
12
|
var data = await resp.json();
|
|
13
13
|
if (!resp.ok) return { ok: false, message: data.error ? data.error.message : 'unknown error' };
|
|
14
14
|
if (data && data.success === false) return { ok: false, message: 'Zhihu like API reported success=false' };
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// 点赞按 answer id 生效,URL 里的 question id 不参与。回读该回答真正所属的问题
|
|
16
|
+
// (只读),供回执核对。回读失败不影响已生效的点赞。
|
|
17
|
+
var actualQuestionId = '';
|
|
18
|
+
if (targetKind === 'answer') {
|
|
19
|
+
try {
|
|
20
|
+
var qr = await fetch('https://www.zhihu.com/api/v4/answers/' + targetId + '?include=question', { credentials: 'include' });
|
|
21
|
+
if (qr.ok) {
|
|
22
|
+
var q = (await qr.json()).question;
|
|
23
|
+
var m = q && typeof q.url === 'string' ? q.url.match(//questions/(d+)/) : null;
|
|
24
|
+
if (m) actualQuestionId = m[1];
|
|
25
|
+
else if (q && typeof q.id === 'string' && /^d+$/.test(q.id)) actualQuestionId = q.id;
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {}
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, success: data.success, actualQuestionId: actualQuestionId };
|
|
30
|
+
})()`);if(!a?.ok)throw new n("COMMAND_EXEC",a?.message||"Failed to like");const r=a.actualQuestionId||"";let i=`Liked ${e.kind} ${e.id}`;if(e.kind==="answer"){if(!r)i+="(未能回读该回答所属的问题,question_id 留空)";else if(r!==e.questionId)i+=`。注意:该回答实际属于问题 ${r},与传入的问题 ${e.questionId} 不一致——点赞按 answer id 生效,请核对目标是否正确`}return h(i,e.kind,o,"applied",{question_id:r})}});
|
package/clis/zhihu/question.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import{cli as g,Strategy as
|
|
2
|
-
(
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import{cli as g,Strategy as y}from"@jackwener/opencli/registry";import{CliError as h}from"@jackwener/opencli/errors";import{stripHtml as I}from"./text.js";import{fetchZhihuJson as b}from"./read-shared.js";function C(t){if(typeof t!=="string")return"";try{const e=new URL(t);if(e.hostname!=="www.zhihu.com"&&e.hostname!=="zhihu.com")return"";return e.pathname.match(/^\/question\/\d+\/answer\/(\d+)\/?$/)?.[1]||e.pathname.match(/^\/api\/v4\/answers\/(\d+)\/?$/)?.[1]||e.pathname.match(/^\/answer\/(\d+)\/?$/)?.[1]||""}catch{return""}}function q(t){const e=C(t.url);if(e)return e;if(typeof t.id==="string"&&/^\d+$/.test(t.id))return t.id;if(typeof t.id==="number"&&Number.isSafeInteger(t.id)&&t.id>0)return String(t.id);return""}function v(t){const e=q(t);if(e)return`id:${e}`;return`fallback:${t.author?.name||"anonymous"}:${t.content||""}`}const _=1000;function N(t){return`
|
|
2
|
+
(() => {
|
|
3
|
+
try {
|
|
4
|
+
const el = document.getElementById('js-initialData');
|
|
5
|
+
if (!el) return null;
|
|
6
|
+
const questions = JSON.parse(el.textContent || '{}')?.initialState?.entities?.questions;
|
|
7
|
+
const q = questions && questions[${JSON.stringify(t)}];
|
|
8
|
+
if (!q || typeof q !== 'object') return null;
|
|
9
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
10
|
+
return {
|
|
11
|
+
title: typeof q.title === 'string' ? q.title : '',
|
|
12
|
+
answerCount: num(q.answerCount),
|
|
13
|
+
followerCount: num(q.followerCount),
|
|
14
|
+
visitCount: num(q.visitCount),
|
|
15
|
+
};
|
|
16
|
+
} catch (_) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
6
19
|
})()
|
|
7
|
-
`);if(!
|
|
20
|
+
`}function z(t){const e=t&&typeof t==="object"&&"session"in t&&"data"in t?t.data:t;if(!e||typeof e!=="object")return null;const u=(a)=>typeof a==="number"&&Number.isFinite(a)?a:null;return{title:typeof e.title==="string"?e.title:"",answerCount:u(e.answerCount),followerCount:u(e.followerCount),visitCount:u(e.visitCount)}}g({site:"zhihu",name:"question",access:"read",description:"知乎问题详情和回答",domain:"www.zhihu.com",strategy:y.COOKIE,args:[{name:"id",required:!0,positional:!0,help:"Question ID (numeric)"},{name:"limit",type:"int",default:5,help:"Number of answers (max 1000; use normal-sized requests)"},{name:"sort",default:"default",choices:["default","created"],help:"Answer order: default or created"}],columns:["rank","id","author","votes","url","content","question_title","question_answers","question_followers","question_views"],func:async(t,e)=>{const{id:u,limit:a=5}=e,i=String(u);if(!/^\d+$/.test(i))throw new h("INVALID_INPUT","Question ID must be numeric","Example: ppcli zhihu question 123456789");const l=Number(a);if(!Number.isInteger(l)||l<=0||l>_)throw new h("INVALID_INPUT",`Limit must be a positive integer no greater than ${_}`,"Use a normal-sized limit to avoid slow requests or Zhihu risk controls");const d=String(e.sort||"default");if(d!=="default"&&d!=="created")throw new h("INVALID_INPUT","Sort must be one of: default, created","Example: ppcli zhihu question 123456789 --sort created");await t.goto(d==="created"?`https://www.zhihu.com/question/${i}/answers/updated`:`https://www.zhihu.com/question/${i}`);let c=`https://www.zhihu.com/api/v4/questions/${i}/answers?limit=${20}&offset=0&sort_by=${d}&include=data[*].content,url,voteup_count,comment_count,author`;const f=[],w=new Set,p=new Set;while(c&&f.length<l&&!p.has(c)){p.add(c);const n=await b(t,c,{label:"问题回答列表"});if(!n||n.__httpError||n.__malformedJson){const r=n?.__httpError;if(r===404)throw new h("NOT_FOUND",`知乎问题 ${i} 的回答接口返回 HTTP 404:该问题 ID 可能不存在`,"核对问题 ID 是否抄错");throw new h("FETCH_ERROR",r?`Zhihu question answers request failed (HTTP ${r})`:"Zhihu question answers request failed","Try again later or rerun with -v for more detail")}for(const r of n.data||[]){const s=v(r);if(w.has(s))continue;w.add(s);f.push(r);if(f.length>=l)break}if(n.paging?.is_end)break;c=typeof n.paging?.next==="string"?n.paging.next:""}let m=null;try{m=await t.evaluate(N(i))}catch{m=null}const o=z(m);if(f.length===0){const n=o?`题目「${o.title}」,回答数 ${o.answerCount??"未知"},关注 ${o.followerCount??"未知"},浏览 ${o.visitCount??"未知"}`:"未能从页面读到该题的回答数";process.stderr.write(`zhihu question ${i}: 接口返回 0 个回答(${n})。空列表是真实结果,不是取数失败。
|
|
21
|
+
`)}return f.map((n,r)=>{const s=q(n);return{rank:r+1,id:s,author:n.author?.name||"anonymous",votes:n.voteup_count||0,url:s?`https://www.zhihu.com/question/${i}/answer/${s}`:"",content:I(n.content||"").substring(0,200),question_title:o?.title??"",question_answers:o?.answerCount??null,question_followers:o?.followerCount??null,question_views:o?.visitCount??null}})}});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import{AuthRequiredError as f,CliError as w,EXIT_CODES as m}from"@jackwener/opencli/errors";export const ZHIHU_TEMPORARY_CODE="TEMPORARILY_UNAVAILABLE";const E=3;export function buildZhihuJsonFetchJs(r){return`
|
|
2
|
+
(async () => {
|
|
3
|
+
try {
|
|
4
|
+
const r = await fetch(${JSON.stringify(r)}, { credentials: 'include' });
|
|
5
|
+
if (!r.ok) {
|
|
6
|
+
const out = { __httpError: r.status };
|
|
7
|
+
try {
|
|
8
|
+
const body = await r.json();
|
|
9
|
+
const e = body && body.error;
|
|
10
|
+
if (e && typeof e === 'object') {
|
|
11
|
+
out.__errorCode = e.code == null ? '' : String(e.code);
|
|
12
|
+
out.__errorMessage = e.message == null ? '' : String(e.message);
|
|
13
|
+
}
|
|
14
|
+
} catch (_) {}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
return await r.json();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return { __malformedJson: error instanceof Error ? error.message : String(error) };
|
|
21
|
+
}
|
|
22
|
+
} catch (err) {
|
|
23
|
+
return { __fetchError: (err && err.message) || String(err) };
|
|
24
|
+
}
|
|
25
|
+
})()
|
|
26
|
+
`}function u(r){if(r&&typeof r==="object"&&"data"in r&&"session"in r)return r.data;return r}function a(r){if(!r||typeof r!=="object")return!1;if(r.__fetchError)return!0;const e=r.__httpError;return e===401||e===403||e===429||typeof e==="number"&&e>=500}function h(r){if(r.__fetchError)return`页面内请求异常:${r.__fetchError}`;const e=[`HTTP ${r.__httpError}`];if(r.__errorCode)e.push(`code ${r.__errorCode}`);if(r.__errorMessage)e.push(r.__errorMessage);return e.join(",")}export function zhihuTemporaryError(r){return new w(ZHIHU_TEMPORARY_CODE,r,"接口侧暂时性失败,与登录无关:隔几分钟重试同一条命令即可,不要让用户重新登录。",m.TEMPFAIL)}export async function probeZhihuLogin(r){let e;try{e=await r.getCookies({url:"https://www.zhihu.com"})}catch(o){return{state:"unknown",reason:`读取 cookie 失败:${o&&o.message||o}`}}if(!Array.isArray(e)||!e.some((o)=>o.name==="z_c0"&&o.value))return{state:"anonymous",reason:"浏览器里没有知乎登录 cookie(z_c0)"};const t=u(await r.evaluate(buildZhihuJsonFetchJs("https://www.zhihu.com/api/v4/me?include=url_token")));if(t&&typeof t==="object"&&!t.__httpError&&!t.__fetchError&&!t.__malformedJson){if(t.url_token)return{state:"logged_in",urlToken:String(t.url_token)};return{state:"anonymous",reason:"/api/v4/me 未返回 url_token"}}if(t&&(t.__httpError===401||t.__httpError===403))return{state:"anonymous",reason:`/api/v4/me 返回 HTTP ${t.__httpError}`};return{state:"unknown",reason:t?h(t):"/api/v4/me 无响应"}}export async function fetchZhihuJson(r,e,{label:t,reanchorUrl:o}={}){const _=buildZhihuJsonFetchJs(e);let n=u(await r.evaluate(_));if(!a(n))return n;if(n.__fetchError&&o)await r.goto(o);else await r.wait(E);n=u(await r.evaluate(_));if(!a(n))return n;const i=h(n),c=n.__httpError;if(c===401||c===403){const s=await probeZhihuLogin(r);if(s.state==="anonymous")throw new f("www.zhihu.com",`知乎未登录或登录已失效(${s.reason}),${t}接口返回 ${i}。需要用户在 PublishPort 客户端重新登录知乎。`);if(s.state==="logged_in")throw zhihuTemporaryError(`知乎${t}接口暂时取不到数据(${i})。登录态已确认正常(账号 ${s.urlToken}),`+"这不是会话失效,无需重新登录;已自动重试 1 次仍失败,请隔几分钟再试。");throw zhihuTemporaryError(`知乎${t}接口暂时取不到数据(${i}),且登录态暂时无法确认(${s.reason})。`+"已自动重试 1 次仍失败,请隔几分钟再试;持续失败再用 `ppcli zhihu whoami` 核对登录。")}throw zhihuTemporaryError(`知乎${t}接口暂时取不到数据(${i})。已自动重试 1 次仍失败,请隔几分钟再试。`)}
|
package/clis/zhihu/recommend.js
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
import{cli as w,Strategy as
|
|
2
|
-
(async () => {
|
|
3
|
-
const r = await fetch(${JSON.stringify(n)}, { credentials: 'include' });
|
|
4
|
-
if (!r.ok) return { __httpError: r.status };
|
|
5
|
-
return await r.json();
|
|
6
|
-
})()
|
|
7
|
-
`);if(!i||i.__httpError){const r=i?.__httpError;if(r===401||r===403)throw new f("www.zhihu.com","Failed to fetch Zhihu recommendations");throw new m("FETCH_ERROR",r?`Zhihu recommendations request failed (HTTP ${r})`:"Zhihu recommendations request failed","Try again later or rerun with -v for more detail")}for(const r of i.data||[]){const o=r.target||{},c=o.id;let a=null;if(c!=null)a=`${o.type??""}:${c}`;else if(r.id!=null)a=`__feed:${r.id}`;if(a!=null){if(l.has(a))continue;l.add(a)}u.push(r);if(u.length>=e)break}if(i.paging?.is_end)break;n=typeof i.paging?.next==="string"?i.paging.next:""}return u.map((i,r)=>{const o=i.target||{};return{rank:r+1,type:o.type||i.type||"",title:y(i),author:o.author?.name||"",votes:o.voteup_count??o.reaction?.statistics?.like_count??0,url:g(i)}})}});
|
|
1
|
+
import{cli as w,Strategy as g}from"@jackwener/opencli/registry";import{CliError as c}from"@jackwener/opencli/errors";import{fetchZhihuJson as y}from"./read-shared.js";function d(s){const t=s.target||{},i=t.id==null?"":String(t.id);if(t.type==="answer"){const n=t.question?.id==null?"":String(t.question.id);return n&&i?`https://www.zhihu.com/question/${n}/answer/${i}`:""}if(t.type==="article")return i?`https://zhuanlan.zhihu.com/p/${i}`:"";if(t.type==="question")return i?`https://www.zhihu.com/question/${i}`:"";return""}function p(s){const t=s.target||{};if(t.type==="answer")return t.question?.title||"";return t.title||t.question?.title||""}const f=1000;w({site:"zhihu",name:"recommend",access:"read",description:"知乎首页推荐",domain:"www.zhihu.com",strategy:g.COOKIE,args:[{name:"limit",type:"int",default:20,help:"Number of items to return (max 1000; use normal-sized requests)"}],columns:["rank","type","title","author","votes","url"],func:async(s,t)=>{const i=Number(t.limit??20);if(!Number.isInteger(i)||i<=0||i>f)throw new c("INVALID_INPUT",`Limit must be a positive integer no greater than ${f}`,"Use a normal-sized limit to avoid slow requests or Zhihu risk controls");await s.goto("https://www.zhihu.com");let n="https://www.zhihu.com/api/v3/feed/topstory/recommend?limit=10&desktop=true";const u=[],l=new Set,h=new Set;while(n&&u.length<i&&!h.has(n)){h.add(n);const e=await y(s,n,{label:"首页推荐"});if(!e||e.__httpError||e.__malformedJson){const r=e?.__httpError;throw new c("FETCH_ERROR",r?`Zhihu recommendations request failed (HTTP ${r})`:"Zhihu recommendations request failed","Try again later or rerun with -v for more detail")}for(const r of e.data||[]){const o=r.target||{};if(!p(r)&&!d(r))continue;const m=o.id;let a=null;if(m!=null)a=`${o.type??""}:${m}`;else if(r.id!=null)a=`__feed:${r.id}`;if(a!=null){if(l.has(a))continue;l.add(a)}u.push(r);if(u.length>=i)break}if(e.paging?.is_end)break;n=typeof e.paging?.next==="string"?e.paging.next:""}return u.map((e,r)=>{const o=e.target||{};return{rank:r+1,type:o.type||e.type||"",title:p(e),author:o.author?.name||"",votes:o.voteup_count??o.reaction?.statistics?.like_count??0,url:d(e)}})}});
|
package/clis/zhihu/search.js
CHANGED
|
@@ -1,11 +1 @@
|
|
|
1
|
-
import{cli as
|
|
2
|
-
(async () => {
|
|
3
|
-
try {
|
|
4
|
-
const r = await fetch(${JSON.stringify(e)}, { credentials: 'include' });
|
|
5
|
-
if (!r.ok) return { __httpError: r.status };
|
|
6
|
-
return await r.json();
|
|
7
|
-
} catch (err) {
|
|
8
|
-
return { __fetchError: err?.message || String(err) };
|
|
9
|
-
}
|
|
10
|
-
})()
|
|
11
|
-
`}async function Z(e,t){let r=l(await e.evaluate(R(t)));if(r&&typeof r==="object"&&!Array.isArray(r)&&r.__fetchError){await e.goto("https://www.zhihu.com");r=l(await e.evaluate(R(t)))}return x(r,t)}function A(e){if(!e||typeof e!=="object"||e.type!=="search_result"||!e.object||typeof e.object!=="object")return null;const t=e.object;if(t.type!=="answer"&&t.type!=="article"&&t.type!=="question")return null;const r=q(e),n=_(t),i=t.question||{},o=g(t.title||i.name||i.title||"");if(!r||!n||!o)throw new a("Zhihu search returned malformed result row identity");return{item:e,key:r,row:{title:o,type:t.type,author:t.author?.name||"",votes:t.voteup_count||0,url:n}}}j({site:"zhihu",name:"search",access:"read",description:"知乎搜索",domain:"www.zhihu.com",strategy:U.COOKIE,args:[{name:"query",required:!0,positional:!0,help:"Search query"},{name:"limit",type:"int",default:10,help:"Number of results (max 1000; use normal-sized requests)"},{name:"type",default:"all",choices:w,help:"Result type: all, answer, article, or question"}],columns:["rank","title","type","author","votes","url"],func:async(e,t)=>{const r=b(t.query),n=S(t.limit),i=v(t.type);await e.goto("https://www.zhihu.com");let o=`https://www.zhihu.com/api/v4/search_v3?q=${encodeURIComponent(r)}&t=general&offset=0&limit=${L}`;const s=[],m=new Set,f=new Set;while(o&&s.length<n&&!f.has(o)){f.add(o);const u=await Z(e,o);for(const d of u.data){const y=d?.object?.type;if(i!=="all"&&y&&y!==i)continue;const c=A(d);if(!c)continue;if(i!=="all"&&c.row.type!==i)continue;if(m.has(c.key))continue;m.add(c.key);s.push(c.row);if(s.length>=n)break}if(s.length>=n)break;if(u.paging?.is_end)break;const h=z(u.paging?.next);if(!h)throw new a("Zhihu search pagination returned malformed next URL");if(f.has(h))throw new a("Zhihu search pagination returned a repeated next URL");o=h}if(s.length===0)throw new I("zhihu search",`No ${i==="all"?"":`${i} `}results found for "${r}"`);return s.map((u,h)=>{return{rank:h+1,...u}})}});export const __test__={stripHtml:g,itemKey:q,itemUrl:_,normalizeSearchUrl:z,parseLimit:S,requireQuery:b,requireType:v,unwrapEvaluateResult:l,requireSearchPayload:x,fetchSearchPage:Z,normalizeResultItem:A};
|
|
1
|
+
import{cli as A,Strategy as U}from"@jackwener/opencli/registry";import{ArgumentError as m,AuthRequiredError as I,CommandExecutionError as u,EmptyResultError as L}from"@jackwener/opencli/errors";import{stripHtml as d}from"./text.js";import{fetchZhihuJson as T}from"./read-shared.js";function g(e){const t=e.object||{};if(t.id!=null)return`${t.type||""}:${t.id}`;return null}function z(e){const t=e.id==null?"":String(e.id);if(e.type==="answer"){const r=e.question?.id==null?"":String(e.question.id);return r&&t?`https://www.zhihu.com/question/${r}/answer/${t}`:""}if(e.type==="article")return t?`https://zhuanlan.zhihu.com/p/${t}`:"";if(e.type==="question")return t?`https://www.zhihu.com/question/${t}`:"";return""}function b(e){if(typeof e!=="string"||!e)return"";try{const t=new URL(e);if(t.hostname==="api.zhihu.com"&&t.pathname==="/search_v3")return`https://www.zhihu.com/api/v4/search_v3${t.search}`;if(t.hostname==="www.zhihu.com"&&t.pathname==="/api/v4/search_v3")return t.toString()}catch{return""}return""}const _=1000,P=20,y=["all","answer","article","question"];function v(e){const t=Number(e??10);if(!Number.isInteger(t)||t<=0||t>_)throw new m(`zhihu search --limit must be a positive integer no greater than ${_}`,"Use a normal-sized limit to avoid slow requests or Zhihu risk controls");return t}function E(e){const t=String(e||"").trim();if(!t)throw new m("zhihu search query must not be empty","Example: ppcli zhihu search codex");return t}function S(e){const t=String(e||"all");if(!y.includes(t))throw new m(`zhihu search --type must be one of: ${y.join(", ")}`,"Example: ppcli zhihu search codex --type answer");return t}function k(e){if(e&&typeof e==="object"&&"data"in e&&"session"in e)return e.data;return e}function R(e,t){const r=k(e);if(!r||typeof r!=="object"||Array.isArray(r))throw new u("Zhihu search returned malformed payload");if(r.__httpError){const n=r.__httpError;if(n===401||n===403)throw new I("www.zhihu.com","Failed to fetch search results from Zhihu");throw new u(`Zhihu search request failed${n?` (HTTP ${n})`:""}`,"Try again later or rerun with -v for more detail")}if(r.__fetchError)throw new u("Zhihu search request failed",String(r.__fetchError));if(!Array.isArray(r.data))throw new u("Zhihu search returned malformed data list",`URL: ${t}`);if(!r.paging||typeof r.paging!=="object")throw new u("Zhihu search returned malformed paging data",`URL: ${t}`);return r}async function Z(e,t){const r=await T(e,t,{label:"搜索",reanchorUrl:"https://www.zhihu.com"});return R(r,t)}function j(e){if(!e||typeof e!=="object"||e.type!=="search_result")return null;const t=e.object;if(!t||typeof t!=="object"||t.type!=="answer")return null;const r=t.question;if(!r||r.id==null)return null;const n=String(r.id),i=d(r.name||r.title||"");if(!/^\d+$/.test(n)||!i)return null;return{key:`question:${n}`,row:{title:i,type:"question",author:"",votes:0,url:`https://www.zhihu.com/question/${n}`}}}function x(e){if(!e||typeof e!=="object"||e.type!=="search_result"||!e.object||typeof e.object!=="object")return null;const t=e.object;if(t.type!=="answer"&&t.type!=="article"&&t.type!=="question")return null;const r=g(e),n=z(t),i=t.question||{},o=d(t.title||i.name||i.title||"");if(!r||!n||!o)throw new u("Zhihu search returned malformed result row identity");return{item:e,key:r,row:{title:o,type:t.type,author:t.author?.name||"",votes:t.voteup_count||0,url:n}}}A({site:"zhihu",name:"search",access:"read",description:"知乎搜索",domain:"www.zhihu.com",strategy:U.COOKIE,args:[{name:"query",required:!0,positional:!0,help:"Search query"},{name:"limit",type:"int",default:10,help:"Number of results (max 1000; use normal-sized requests)"},{name:"type",default:"all",choices:y,help:"Result type: all, answer, article, or question (question = distinct questions the matching answers belong to; author/votes are blank for question rows)"}],columns:["rank","title","type","author","votes","url"],func:async(e,t)=>{const r=E(t.query),n=v(t.limit),i=S(t.type);await e.goto("https://www.zhihu.com");let o=`https://www.zhihu.com/api/v4/search_v3?q=${encodeURIComponent(r)}&t=general&offset=0&limit=${P}`;const s=[],l=new Set,p=new Set;while(o&&s.length<n&&!p.has(o)){p.add(o);const a=await Z(e,o);for(const w of a.data){const f=i==="question"?j(w):null;if(f){if(!l.has(f.key)){l.add(f.key);s.push(f.row);if(s.length>=n)break}continue}const q=w?.object?.type;if(i!=="all"&&q&&q!==i)continue;const c=x(w);if(!c)continue;if(i!=="all"&&c.row.type!==i)continue;if(l.has(c.key))continue;l.add(c.key);s.push(c.row);if(s.length>=n)break}if(s.length>=n)break;if(a.paging?.is_end)break;const h=b(a.paging?.next);if(!h)throw new u("Zhihu search pagination returned malformed next URL");if(p.has(h))throw new u("Zhihu search pagination returned a repeated next URL");o=h}if(s.length===0)throw new L("zhihu search",`No ${i==="all"?"":`${i} `}results found for "${r}"`);return s.map((a,h)=>{return{rank:h+1,...a}})}});export const __test__={stripHtml:d,itemKey:g,itemUrl:z,normalizeSearchUrl:b,parseLimit:v,requireQuery:E,requireType:S,unwrapEvaluateResult:k,requireSearchPayload:R,fetchSearchPage:Z,questionRowFromAnswer:j,normalizeResultItem:x};
|
|
@@ -68,6 +68,26 @@ export interface DragResult {
|
|
|
68
68
|
source_match_level: TargetMatchLevel;
|
|
69
69
|
target_match_level: TargetMatchLevel;
|
|
70
70
|
}
|
|
71
|
+
export interface DragByResult {
|
|
72
|
+
dragged: boolean;
|
|
73
|
+
source: string;
|
|
74
|
+
/** Offset actually applied, in CSS pixels (rounded). */
|
|
75
|
+
dx: number;
|
|
76
|
+
dy: number;
|
|
77
|
+
/** Viewport coordinates of the press and release points. */
|
|
78
|
+
from: {
|
|
79
|
+
x: number;
|
|
80
|
+
y: number;
|
|
81
|
+
};
|
|
82
|
+
to: {
|
|
83
|
+
x: number;
|
|
84
|
+
y: number;
|
|
85
|
+
};
|
|
86
|
+
/** Pointer-move samples dispatched between press and release. */
|
|
87
|
+
steps: number;
|
|
88
|
+
source_matches_n: number;
|
|
89
|
+
source_match_level: TargetMatchLevel;
|
|
90
|
+
}
|
|
71
91
|
export declare abstract class BasePage implements IPage {
|
|
72
92
|
protected _lastUrl: string | null;
|
|
73
93
|
/** Cached previous snapshot hashes for incremental diff marking */
|
|
@@ -110,13 +130,19 @@ export declare abstract class BasePage implements IPage {
|
|
|
110
130
|
protected tryNativeClick(x: number, y: number): Promise<boolean>;
|
|
111
131
|
protected tryNativeMouseMove(x: number, y: number): Promise<boolean>;
|
|
112
132
|
protected tryNativeDoubleClick(x: number, y: number): Promise<boolean>;
|
|
133
|
+
/**
|
|
134
|
+
* Press at `from`, move to `to` through `steps` evenly spaced pointer
|
|
135
|
+
* positions (the last one is `to` itself), release. The default of 2 is the
|
|
136
|
+
* original midpoint + endpoint sequence; slider widgets that sample the
|
|
137
|
+
* pointer path need more.
|
|
138
|
+
*/
|
|
113
139
|
protected tryNativeDrag(from: {
|
|
114
140
|
x: number;
|
|
115
141
|
y: number;
|
|
116
142
|
}, to: {
|
|
117
143
|
x: number;
|
|
118
144
|
y: number;
|
|
119
|
-
}): Promise<boolean>;
|
|
145
|
+
}, steps?: number): Promise<boolean>;
|
|
120
146
|
protected tryClickAxRef(ref: string): Promise<ResolveSuccess | null>;
|
|
121
147
|
private resolveAxRefPoint;
|
|
122
148
|
private axBoxCenter;
|
|
@@ -143,9 +169,23 @@ export declare abstract class BasePage implements IPage {
|
|
|
143
169
|
setChecked(ref: string, checked: boolean, opts?: ResolveOptions): Promise<SetCheckedResult>;
|
|
144
170
|
private setFileInputBySelector;
|
|
145
171
|
uploadFiles(ref: string, files: string[], opts?: ResolveOptions): Promise<UploadFilesResult>;
|
|
172
|
+
/**
|
|
173
|
+
* Drag an element by a pixel offset instead of onto another element — for
|
|
174
|
+
* sliders (captcha, range, resize handles) where the drop point is a
|
|
175
|
+
* distance, not a DOM node. Same real CDP mouse events as `drag`; the press
|
|
176
|
+
* point is the source's centre and the release point is centre + (dx, dy).
|
|
177
|
+
*/
|
|
178
|
+
dragBy(source: string, offset: {
|
|
179
|
+
dx: number;
|
|
180
|
+
dy: number;
|
|
181
|
+
}, opts?: {
|
|
182
|
+
from?: ResolveOptions;
|
|
183
|
+
steps?: number;
|
|
184
|
+
}): Promise<DragByResult>;
|
|
146
185
|
drag(source: string, target: string, opts?: {
|
|
147
186
|
from?: ResolveOptions;
|
|
148
187
|
to?: ResolveOptions;
|
|
188
|
+
steps?: number;
|
|
149
189
|
}): Promise<DragResult>;
|
|
150
190
|
fillText(ref: string, text: string, opts?: ResolveOptions): Promise<FillTextResult>;
|
|
151
191
|
/**
|