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
package/clis/devto/publish.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as f from"node:fs";import{cli as b,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as s,AuthRequiredError as p,CommandExecutionError as d}from"@jackwener/opencli/errors";import{assertLiteralContent as v}from"../_shared/content-guard.js";const h=4;export function resolveBody(e){if(e["body-file"]){const i=String(e["body-file"]);if(!f.statSync(i,{throwIfNoEntry:!1})?.isFile())throw new s(`--body-file not found: ${i}`);return f.readFileSync(i,"utf8")}if(e.body!==void 0&&e.body!==null){v(String(e.body),{fileFlag:"--body-file"});return String(e.body)}return}export function resolveTagList(e){if(e===void 0||e===null||e==="")return;const i=String(e).split(",").map((n)=>n.trim().toLowerCase()).filter(Boolean);if(i.length>h)throw new s(`Too many tags: ${i.length} (DEV.to max ${h})`);for(const n of i)if(!/^[a-z0-9]+$/.test(n))throw new s(`Invalid tag "${n}". DEV.to tags must be lowercase alphanumeric (no spaces/punctuation).`);return i.join(", ")}export function buildArticle(e,i,n){const t={};if(e.title!==void 0)t.title=String(e.title);if(i!==void 0)t.body_markdown=i;if(e.published!==void 0&&e.published!==null)t.published=!!e.published;if(n!==void 0)t.tag_list=n;if(e["cover-image"])t.main_image=String(e["cover-image"]);if(e["canonical-url"])t.canonical_url=String(e["canonical-url"]);if(e.series)t.series=String(e.series);if(e.description)t.description=String(e.description);return t}const m=1000,w=10;export function findCanonicalConflict(e,i,n){const t=String(i||"").trim();if(!t)return null;return(Array.isArray(e)?e:[]).find((a)=>String(a?.canonical_url||"").trim()===t&&String(a?.id??"")!==String(n||""))??null}async function _(e,i,n){const t=await e.evaluate(`(async () => {
|
|
2
|
+
try {
|
|
3
|
+
var du = document.body && document.body.getAttribute('data-user');
|
|
4
|
+
var user = du ? JSON.parse(du) : null;
|
|
5
|
+
if (!user || !user.username) return { kind: 'auth', detail: 'no data-user — not logged in?' };
|
|
6
|
+
var all = [];
|
|
7
|
+
for (var pageNum = 1; pageNum <= ${w}; pageNum++) {
|
|
8
|
+
var r = await fetch('/api/articles?username=' + encodeURIComponent(user.username) + '&per_page=${m}&page=' + pageNum, { headers: { 'Accept': 'application/json' } });
|
|
9
|
+
if (!r.ok) return { kind: 'http', status: r.status };
|
|
10
|
+
var list = await r.json();
|
|
11
|
+
if (!Array.isArray(list)) return { kind: 'shape' };
|
|
12
|
+
for (var i = 0; i < list.length; i++) all.push({ id: list[i].id, canonical_url: list[i].canonical_url, url: list[i].url, title: list[i].title });
|
|
13
|
+
if (list.length < ${m}) return { kind: 'ok', articles: all };
|
|
14
|
+
}
|
|
15
|
+
return { kind: 'truncated', articles: all };
|
|
16
|
+
} catch (e) { return { kind: 'exception', detail: String(e && e.message || e) }; }
|
|
17
|
+
})()`);if(t?.kind==="auth")throw new p("dev.to",`Not logged in (${t.detail}). Run \`ppcli devto login\`.`);if(t?.kind!=="ok"&&t?.kind!=="truncated")throw new d(`dev.to canonical 预检失败,未提交:读取账号已发布文章列表出错(${t?.kind==="http"?`HTTP ${t.status}`:t?.detail||t?.kind||"unknown"})。`);const a=findCanonicalConflict(t.articles,i,n);if(a)throw new s(`--canonical-url 已被本账号的文章占用,未提交:${a.url}(id ${a.id},《${a.title}》)。同一账号下 canonical_url 不能重复;要改那篇用 --id ${a.id},否则换一个 canonical 或不传。`)}b({site:"devto",name:"publish",access:"write",description:"Publish, update or unpublish a DEV.to article (markdown). New articles are drafts unless --published; unpublish with --id <id> --published false.",domain:"dev.to",strategy:S.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Article title (required for new articles)"},{name:"body",type:"string",required:!1,help:"Article body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"published",type:"boolean",required:!1,help:"新建:传 --published 直接上线,不传存草稿。更新(--id):--published 上线,--published false 撤回为草稿(取消发布),不传则不改发布状态"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags, max 4, lowercase alphanumeric"},{name:"cover-image",type:"string",required:!1,help:"Cover image URL (main_image)"},{name:"canonical-url",type:"string",required:!1,help:"Canonical URL (original source);同一账号下不能重复,提交前会预检已发布文章"},{name:"series",type:"string",required:!1,help:"Series name to group articles"},{name:"description",type:"string",required:!1,help:"Social/SEO description"},{name:"id",type:"string",required:!1,help:"要更新的文章 id。部分更新:只提交本次给出的参数(标题/正文/标签/封面等),没给的字段不随请求发送,保持原值;发布状态也只在显式传 --published 时才改"}],columns:["status","id","url","published","slug"],func:async(e,i)=>{if(!e)throw new d("Browser session required for devto publish");const n=i.id?String(i.id).trim():"",t=resolveBody(i),a=resolveTagList(i.tags);if(!n&&!i.title)throw new s("--title is required when creating a new article");if(!n&&t===void 0)throw new s("--body or --body-file is required when creating a new article");const l=buildArticle(i,t,a);if(!n)l.published=i.published===!0;if(n&&Object.keys(l).length===0)throw new s("--id 更新至少要给一个要改的参数(如 --title / --body-file / --tags / --published false)");const g=n?"PUT":"POST",y=n?`/articles/${encodeURIComponent(n)}`:"/articles";await e.goto("https://dev.to/");if(l.canonical_url)await _(e,l.canonical_url,n);const r=await e.evaluate(`(async () => {
|
|
2
18
|
try {
|
|
3
19
|
var csrf = (document.querySelector('meta[name="csrf-token"]') || {}).content || window.csrfToken;
|
|
4
20
|
if (!csrf) return { kind: 'auth', detail: 'no csrf-token meta — not logged in?' };
|
|
5
|
-
var r = await fetch(${JSON.stringify(
|
|
6
|
-
method: ${JSON.stringify(
|
|
21
|
+
var r = await fetch(${JSON.stringify(y)}, {
|
|
22
|
+
method: ${JSON.stringify(g)},
|
|
7
23
|
credentials: 'include',
|
|
8
24
|
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
|
|
9
25
|
body: JSON.stringify({ article: ${JSON.stringify(l)} }),
|
|
@@ -14,4 +30,4 @@ import*as c from"node:fs";import{cli as y,Strategy as g}from"@jackwener/opencli/
|
|
|
14
30
|
if (!r.ok) return { kind: 'http', status: r.status, detail: text.slice(0, 300) };
|
|
15
31
|
return { kind: 'ok', data: data };
|
|
16
32
|
} catch (e) { return { kind: 'exception', detail: String(e && e.message || e) }; }
|
|
17
|
-
})()`);if(r?.kind==="auth")throw new
|
|
33
|
+
})()`);if(r?.kind==="auth")throw new p("dev.to",`Not logged in (${r.detail}). Run \`ppcli devto login\`.`);if(r?.kind==="http")throw new d(`dev.to publish failed: HTTP ${r.status} ${r.detail}`);if(r?.kind==="exception")throw new d(`dev.to publish error: ${r.detail}`);if(r?.kind!=="ok"||!r.data)throw new d(`Unexpected dev.to response: ${JSON.stringify(r)}`);const o=r.data,c=l.published===void 0?null:l.published;let u="";if(o.current_state_path)try{u=new URL(o.current_state_path,"https://dev.to").href}catch(A){u=String(o.current_state_path)}else if(o.url)u=String(o.url);return[{status:n?"updated":"created",id:o.id??"",url:c===!0?u:"",published:c,slug:o.slug??""}]}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{browserFetch as m}from"./browser-fetch.js";export const WORK_LIST_URL="https://creator.douyin.com/janus/douyin/creator/pc/work_list",TIMER_STATUS={PENDING:0,ONLINE:1,ACCOUNT_BANNED:4,ACCOUNT_UNBANNED:5};const c={[TIMER_STATUS.PENDING]:"scheduled",[TIMER_STATUS.ACCOUNT_BANNED]:"schedule_account_banned",[TIMER_STATUS.ACCOUNT_UNBANNED]:"schedule_account_unbanned"};export function pendingTimerStatus(t){const r=t?.timer?.status;return typeof r==="number"&&r in c?c[r]:null}export function scheduledPublicTime(t){if(!pendingTimerStatus(t))return null;const r=Number(t.timer.public_time);return Number.isFinite(r)&&r>0?r:null}export function formatBeijingTime(t){if(!t)return"";return new Date(t*1000).toLocaleString("zh-CN",{timeZone:"Asia/Shanghai"})}export async function fetchWorkListPage(t,{status:r=0,count:n=20,cursor:o=0}={}){const i=new URLSearchParams({status:String(r),count:String(n),max_cursor:String(o),scene:"star_atlas",device_platform:"android",aid:"1128"}),e=await m(t,"GET",`${WORK_LIST_URL}?${i.toString()}`);return{works:e.aweme_list??e.data?.work_list??[],hasMore:!!e.has_more,nextCursor:e.max_cursor}}export async function walkWorkList(t,r,{status:n=0,count:o=20,maxHops:i=50}={}){let e=0;for(let s=0;s<i;s++){const{works:u,hasMore:l,nextCursor:a}=await fetchWorkListPage(t,{status:n,count:o,cursor:e});for(const N of u)if(await r(N,s)===!0)return;if(!l||a==null||String(a)===String(e)||u.length===0)return;e=a}}
|
package/clis/douyin/publish.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import{DOUYIN_DECLARATION_ARGS as k,douyinDeclarationInfo as ee}from"./_shared/publish-settings.js";import*as T from"node:fs";import*as v from"node:path";import{cli as te,Strategy as oe}from"@jackwener/opencli/registry";import{ArgumentError as _,CommandExecutionError as u}from"@jackwener/opencli/errors";import{getUploadAuthV5Credentials as ie,applyVideoUploadInner as se,commitVideoUploadInner as ne}from"./_shared/vod-upload.js";import{tosUpload as re}from"./_shared/tos-upload.js";import{imagexUpload as ae}from"./_shared/imagex-upload.js";import{browserFetch as V}from"./_shared/browser-fetch.js";import{requireObjectEvaluateResult as M}from"./_shared/evaluate-result.js";import{generateCreationId as ce}from"./_shared/creation-id.js";import{validateTiming as le,toUnixSeconds as de}from"./_shared/timing.js";import{parseTextExtra as me,extractHashtagNames as pe}from"./_shared/text-extra.js";const _e={public:0,friends:2,private:1},z="https://imagex.bytedanceapi.com",L="1147",ue="aid=1128&cookie_enabled=true&screen_width=1512&screen_height=982&browser_language=zh-CN&browser_platform=MacIntel&browser_name=Mozilla&browser_online=true&timezone_name=Asia%2FTokyo&support_h265=1",fe=JSON.stringify({video_cover_source:2,cover_timestamp:0,recommend_timestamp:0,is_cover_edit:0,is_cover_template:0,cover_template_id:"",is_text_template:0,text_template_id:"",text_template_content:"",is_text:0,text_num:0,text_content:"",is_use_sticker:0,sticker_id:"",is_use_filter:0,filter_id:"",is_cover_modify:0,to_status:0,cover_type:0,initial_cover_uri:"",cut_coordinate:""});function he(o){const e=o instanceof Error?o.message:String(o);return e.includes("post_assistant/fast_detect")&&(e.includes("Empty response")||e.includes("404")||e.includes("Not Found")||e.includes("Timeout")||e.includes("timed out")||e.includes("Failed to fetch"))}function C(o){return new Promise((e)=>setTimeout(e,o))}function j(o,e){const m=e?.ResponseMetadata?.Error??e?.Error;if(m)throw new u(`${o}失败: ${JSON.stringify(m)}`)}async function B(o,e,m,r){let f;for(let p=1;p<=3;p+=1)try{return{ok:!0,value:await V(o,e,m,r)}}catch(s){if(!he(s))throw s;f=s;if(p<3)await C(500*p)}return{ok:!1,error:f}}te({site:"douyin",name:"publish",access:"write",description:"发布视频到抖音(指定 --schedule 定时,或显式 --immediate 立即发布)",domain:"creator.douyin.com",strategy:oe.COOKIE,defaultWindowMode:"foreground",args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"视频标题(≤30字)"},{name:"schedule",help:"定时发布时间(ISO8601 或 Unix 秒,2h ~ 14天后),与 --immediate 二选一"},{name:"immediate",type:"boolean",help:"立即发布(必须显式指定,不能与 --schedule 同用)"},{name:"caption",fromFile:!0,default:"",help:"正文内容(≤1000字,支持 #话题)"},{name:"cover",default:"",help:"封面图片路径(不提供时使用视频截帧)"},{name:"visibility",default:"public",choices:["public","friends","private"]},{name:"allow_download",type:"bool",default:!1,help:"允许下载"},{name:"collection",default:"",help:"合集 ID"},{name:"activity",default:"",help:"活动 ID"},{name:"poi_id",default:"",help:"地理位置 ID"},{name:"poi_name",default:"",help:"地理位置名称"},{name:"hotspot",default:"",help:"关联热点词"},{name:"no_safety_check",type:"bool",default:!1,help:"跳过内容安全检测"},{name:"sync_toutiao",type:"bool",default:!1,help:"同步发布到头条"},{name:"ai-generated",type:"boolean",help:"声明内容由 AI 生成(可选,随定时发布请求提交)"},...k],columns:["status","aweme_id","url","publish_time"],func:async(o,e)=>{const m=ee(e);if(!!e.immediate===!!e.schedule)throw new _("必须指定 --schedule 或 --immediate,且只能选择一个");const r=v.resolve(e.video);if(!T.existsSync(r))throw new _(`视频文件不存在: ${r}`);const f=v.extname(r).toLowerCase();if(![".mp4",".mov",".avi",".webm"].includes(f))throw new _(`不支持的视频格式: ${f}(支持 mp4/mov/avi/webm)`);const p=T.statSync(r).size,s=e.title;if(s.length>30)throw new _("标题不能超过 30 字");const n=e.caption||"";if(n.length>1000)throw new _("正文不能超过 1000 字");const
|
|
1
|
+
import{DOUYIN_DECLARATION_ARGS as k,douyinDeclarationInfo as ee}from"./_shared/publish-settings.js";import*as T from"node:fs";import*as v from"node:path";import{cli as te,Strategy as oe}from"@jackwener/opencli/registry";import{ArgumentError as _,CommandExecutionError as u}from"@jackwener/opencli/errors";import{getUploadAuthV5Credentials as ie,applyVideoUploadInner as se,commitVideoUploadInner as ne}from"./_shared/vod-upload.js";import{tosUpload as re}from"./_shared/tos-upload.js";import{imagexUpload as ae}from"./_shared/imagex-upload.js";import{browserFetch as V}from"./_shared/browser-fetch.js";import{requireObjectEvaluateResult as M}from"./_shared/evaluate-result.js";import{generateCreationId as ce}from"./_shared/creation-id.js";import{validateTiming as le,toUnixSeconds as de}from"./_shared/timing.js";import{parseTextExtra as me,extractHashtagNames as pe}from"./_shared/text-extra.js";const _e={public:0,friends:2,private:1},z="https://imagex.bytedanceapi.com",L="1147",ue="aid=1128&cookie_enabled=true&screen_width=1512&screen_height=982&browser_language=zh-CN&browser_platform=MacIntel&browser_name=Mozilla&browser_online=true&timezone_name=Asia%2FTokyo&support_h265=1",fe=JSON.stringify({video_cover_source:2,cover_timestamp:0,recommend_timestamp:0,is_cover_edit:0,is_cover_template:0,cover_template_id:"",is_text_template:0,text_template_id:"",text_template_content:"",is_text:0,text_num:0,text_content:"",is_use_sticker:0,sticker_id:"",is_use_filter:0,filter_id:"",is_cover_modify:0,to_status:0,cover_type:0,initial_cover_uri:"",cut_coordinate:""});function he(o){const e=o instanceof Error?o.message:String(o);return e.includes("post_assistant/fast_detect")&&(e.includes("Empty response")||e.includes("404")||e.includes("Not Found")||e.includes("Timeout")||e.includes("timed out")||e.includes("Failed to fetch"))}function C(o){return new Promise((e)=>setTimeout(e,o))}function j(o,e){const m=e?.ResponseMetadata?.Error??e?.Error;if(m)throw new u(`${o}失败: ${JSON.stringify(m)}`)}async function B(o,e,m,r){let f;for(let p=1;p<=3;p+=1)try{return{ok:!0,value:await V(o,e,m,r)}}catch(s){if(!he(s))throw s;f=s;if(p<3)await C(500*p)}return{ok:!1,error:f}}te({site:"douyin",name:"publish",access:"write",description:"发布视频到抖音(指定 --schedule 定时,或显式 --immediate 立即发布)",domain:"creator.douyin.com",strategy:oe.COOKIE,defaultWindowMode:"foreground",args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"视频标题(≤30字)"},{name:"schedule",help:"定时发布时间(ISO8601 或 Unix 秒,2h ~ 14天后),与 --immediate 二选一"},{name:"immediate",type:"boolean",help:"立即发布(必须显式指定,不能与 --schedule 同用)"},{name:"caption",fromFile:!0,default:"",help:"正文内容(≤1000字,支持 #话题)"},{name:"cover",default:"",help:"封面图片路径(不提供时使用视频截帧)"},{name:"visibility",default:"public",choices:["public","friends","private"]},{name:"allow_download",type:"bool",default:!1,help:"允许下载"},{name:"collection",default:"",help:"合集 ID"},{name:"activity",default:"",help:"活动 ID"},{name:"poi_id",default:"",help:"地理位置 ID"},{name:"poi_name",default:"",help:"地理位置名称"},{name:"hotspot",default:"",help:"关联热点词"},{name:"no_safety_check",type:"bool",default:!1,help:"跳过内容安全检测"},{name:"sync_toutiao",type:"bool",default:!1,help:"同步发布到头条"},{name:"ai-generated",type:"boolean",help:"声明内容由 AI 生成(可选,随定时发布请求提交)"},...k],columns:["status","aweme_id","url","publish_time"],func:async(o,e)=>{const m=ee(e);if(!!e.immediate===!!e.schedule)throw new _("必须指定 --schedule 或 --immediate,且只能选择一个");const r=v.resolve(e.video);if(!T.existsSync(r))throw new _(`视频文件不存在: ${r}`);const f=v.extname(r).toLowerCase();if(![".mp4",".mov",".avi",".webm"].includes(f))throw new _(`不支持的视频格式: ${f}(支持 mp4/mov/avi/webm)`);const p=T.statSync(r).size,s=e.title;if(s.length>30)throw new _("标题不能超过 30 字");const n=e.caption||"";if(n.length>1000)throw new _("正文不能超过 1000 字");const U=e.immediate?0:de(e.schedule);if(!e.immediate)le(U);const H=_e[e.visibility]??0,O=e.cover;if(O){if(!T.existsSync(v.resolve(O)))throw new _(`封面文件不存在: ${v.resolve(O)}`)}const g=await ie(o),D=await se(p,g);let w="",E=720,A=1280;await re({filePath:r,uploadInfo:D,credentials:g,onProgress:(t,i)=>{const a=Math.round(t/i*100);process.stderr.write(`\r 上传进度: ${a}%`)}});process.stderr.write(`
|
|
2
2
|
`);process.stderr.write(` 提交上传...
|
|
3
|
-
`);const h=await ne(D,
|
|
4
|
-
`);
|
|
5
|
-
fetch(${JSON.stringify(
|
|
3
|
+
`);const h=await ne(D,g),N=h.video_id;process.stderr.write(` 上传已提交: ${N}
|
|
4
|
+
`);E=h.width||E;A=h.height||A;if(!w&&h.poster_uri)w=h.poster_uri;if(e.cover){const t=v.resolve(e.cover),i=`${z}/?Action=ApplyImageUpload&ServiceId=${L}&Version=2018-08-01&UploadNum=1`,a=`fetch(${JSON.stringify(i)}, { credentials: 'include' }).then(r => r.json())`,y=M(await o.evaluate(a),"抖音封面申请上传地址响应异常");j("抖音封面申请上传地址",y);const c=y.Result?.UploadAddress?.StoreInfos?.[0];if(!c?.UploadHost||!c?.StoreUri)throw new u(`抖音封面申请上传地址响应缺少 UploadHost/StoreUri: ${JSON.stringify(y).slice(0,500)}`);const I=`https://${c.UploadHost}/${c.StoreUri}`,l=await ae(t,{upload_url:I,store_uri:c.StoreUri}),x=`${z}/?Action=CommitImageUpload&ServiceId=${L}&Version=2018-08-01`,d=JSON.stringify({SuccessObjKeys:[l]}),Z=`
|
|
5
|
+
fetch(${JSON.stringify(x)}, {
|
|
6
6
|
method: 'POST',
|
|
7
7
|
credentials: 'include',
|
|
8
8
|
headers: { 'Content-Type': 'application/json' },
|
|
9
9
|
body: ${JSON.stringify(d)}
|
|
10
10
|
}).then(r => r.json())
|
|
11
|
-
`,Q=M(await o.evaluate(Z),"抖音封面提交上传响应异常");j("抖音封面提交上传",Q);w=l}if(!e.no_safety_check){const t="https://creator.douyin.com/aweme/v1/post_assistant/fast_detect/pre_check",i={video_id:
|
|
12
|
-
`);const y="https://creator.douyin.com/aweme/v1/post_assistant/fast_detect/poll",c=Date.now()+30000;let
|
|
13
|
-
`);else throw new u("内容安全检测超时(30s),请稍后重试","如确认要跳过本地预检,可使用 --no_safety_check;提交后仍会走抖音平台审核")}const q=pe(n),J=[];let P=0;for(const t of q){const i=n.indexOf(`#${t}`,P);if(i===-1)continue;J.push({name:t,id:0,start:i,end:i+t.length+1});P=i+t.length+1}const R=n?`${s} ${n}`:s,F=n?s.length+1:0,G=me(R,J.map((t)=>({...t,start:t.start+F,end:t.end+F}))),K={item:{common:{text:R,caption:n,item_title:s,activity:JSON.stringify(e.activity?[e.activity]:[]),text_extra:JSON.stringify(G),challenges:"[]",mentions:"[]",hashtag_source:"",hot_sentence:e.hotspot||"",interaction_stickers:"[]",visibility_type:H,download:e.allow_download?1:0,timing:
|
|
11
|
+
`,Q=M(await o.evaluate(Z),"抖音封面提交上传响应异常");j("抖音封面提交上传",Q);w=l}if(!e.no_safety_check){const t="https://creator.douyin.com/aweme/v1/post_assistant/fast_detect/pre_check",i={video_id:N,title:s,desc:n},a=await B(o,"POST",t,{body:i});if(!a.ok)process.stderr.write(` 内容安全预检接口无响应,继续轮询检测结果。
|
|
12
|
+
`);const y="https://creator.douyin.com/aweme/v1/post_assistant/fast_detect/poll",c=Date.now()+30000;let I=!1,l=0;while(Date.now()<c){const x=await B(o,"POST",y,{body:i});if(!x.ok){l+=1;if(!a.ok&&l>=3)break;await C(2000);continue}l=0;const d=x.value;if(d.status===0||d.has_done===!0&&d.detect_result?.reason_code===0&&(d.detect_list?.length??0)===0){I=!0;break}if(d.status===1)throw new u("内容安全检测不通过,请修改后重试","使用 --no_safety_check 跳过");await C(2000)}if(!I)if(!a.ok&&l>=3)process.stderr.write(` 内容安全预检持续无响应,跳过本地预检,交由 create_v2 后的平台审核。
|
|
13
|
+
`);else throw new u("内容安全检测超时(30s),请稍后重试","如确认要跳过本地预检,可使用 --no_safety_check;提交后仍会走抖音平台审核")}const q=pe(n),J=[];let P=0;for(const t of q){const i=n.indexOf(`#${t}`,P);if(i===-1)continue;J.push({name:t,id:0,start:i,end:i+t.length+1});P=i+t.length+1}const R=n?`${s} ${n}`:s,F=n?s.length+1:0,G=me(R,J.map((t)=>({...t,start:t.start+F,end:t.end+F}))),K={item:{common:{text:R,caption:n,item_title:s,activity:JSON.stringify(e.activity?[e.activity]:[]),text_extra:JSON.stringify(G),challenges:"[]",mentions:"[]",hashtag_source:"",hot_sentence:e.hotspot||"",interaction_stickers:"[]",visibility_type:H,download:e.allow_download?1:0,timing:U,creation_id:ce(),media_type:4,video_id:N,music_source:0,music_id:null,...e.poi_id?{poi_id:e.poi_id,poi_name:e.poi_name}:{}},cover:{poster:w,custom_cover_image_height:A,custom_cover_image_width:E,poster_delay:0,cover_tools_info:fe,cover_tools_extend_info:"{}"},mix:e.collection?{mix_id:e.collection,mix_order:0}:{},chapter:{chapter:JSON.stringify({chapter_abstract:"",chapter_details:[],chapter_type:0})},anchor:{},sync:{should_sync:!1,sync_to_toutiao:e.sync_toutiao?1:0},open_platform:{},assistant:{is_preview:0,is_post_assistant:1},declare:{user_declare_info:m}}},W=`https://creator.douyin.com/web/api/media/aweme/create_v2/?read_aid=2906&${ue}`;process.stderr.write(e.immediate?` 创建发布...
|
|
14
14
|
`:` 创建定时发布...
|
|
15
|
-
`);let S;try{S=await V(o,"POST",W,{body:K})}catch(t){if((t instanceof Error?t.message:String(t)).includes("Failed to fetch"))throw new u("提交发布请求被浏览器侧拦截(疑似字节安全组件 secsdk)","不要直接重试:先用 `ppcli douyin videos` / 创作者中心确认作品是否已创建,避免重复投稿。"+"确认未创建后,用 `--window foreground` 前台窗口重试(视频已上传成功的部分无需重传)。");throw t}const
|
|
15
|
+
`);let S;try{S=await V(o,"POST",W,{body:K})}catch(t){if((t instanceof Error?t.message:String(t)).includes("Failed to fetch"))throw new u("提交发布请求被浏览器侧拦截(疑似字节安全组件 secsdk)","不要直接重试:先用 `ppcli douyin videos` / 创作者中心确认作品是否已创建,避免重复投稿。"+"确认未创建后,用 `--window foreground` 前台窗口重试(视频已上传成功的部分无需重传)。");throw t}const b=S.aweme_id??S.item_id;if(!b)throw new u(`发布成功但未返回 aweme_id/item_id: ${JSON.stringify(S)}`);const X=`https://www.douyin.com/video/${b}`,Y=e.immediate?"立即发布":new Date(U*1000).toLocaleString("zh-CN",{timeZone:"Asia/Shanghai"});if(!e.immediate)process.stderr.write(` 定时稿已创建,到点前作品链接不可访问。核对:ppcli douyin videos --status scheduled(应能看到 ${b} 及计划时间)。
|
|
16
|
+
`);return[{status:e.immediate?"✅ 发布请求已提交":"✅ 定时发布成功!",aweme_id:b,url:X,publish_time:Y}]}});
|
package/clis/douyin/stats.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as y,Strategy as g}from"@jackwener/opencli/registry";import{ArgumentError as h,EmptyResultError as d}from"@jackwener/opencli/errors";import{browserFetch as b}from"./_shared/browser-fetch.js";import{formatBeijingTime as S,pendingTimerStatus as E,scheduledPublicTime as I,walkWorkList as T}from"./_shared/work-list.js";const x="https://creator.douyin.com/web/api/creator/item/list",l=50,f=50;async function A(t,r){let e=null;await T(t,(n)=>{if(!sameAwemeId(n?.aweme_id,r))return!1;e=n;return!0},{count:l,maxHops:f});if(!e)return null;const i=E(e);if(i){const n=S(I(e));return`作品存在,是尚未发出的定时稿(${i}${n?`,计划 ${n} 北京时间发布`:""}),发布前没有数据指标。定时已生效,不要重复投稿;可用 \`ppcli douyin videos --status scheduled\` 核对`}if(e.status?.in_reviewing)return"作品存在,正在审核中,暂时没有数据指标";return"作品存在于作品管理列表,但数据中心暂未返回它的指标(新发布的作品指标会延迟产出)"}export function normalizeAwemeId(t){const r=String(t??"").trim();if(!/^\d{16,20}$/.test(r))throw new h("douyin stats aweme_id must be a 16-20 digit numeric ID");return r}export function sameAwemeId(t,r){if(t==null)return!1;const e=String(t);if(e===r)return!0;return/^\d+$/.test(e)&&Number(e)===Number(r)}y({site:"douyin",name:"stats",access:"read",description:"获取单个作品的创作者中心深层指标",domain:"creator.douyin.com",strategy:g.COOKIE,args:[{name:"aweme_id",required:!0,positional:!0,help:"抖音作品 ID(aweme_id,可从作品 URL 末尾获取)"}],columns:["metric","value"],func:async(t,r)=>{const e=normalizeAwemeId(r.aweme_id);let i;const n=Date.now();for(let c=0;c<f;c++){const a=new URLSearchParams({count:String(l),order_by:"1",fields:"metrics,review,visibility",need_cooperation:"true",need_long_article:"true"});a.set("min_cursor","0");a.set("max_cursor",String(i!==void 0?i:n));const u=await b(t,"GET",`${x}?${a.toString()}`),m=(u.items??[]).find((o)=>sameAwemeId(o?.id,e));if(m){const o=m.metrics;if(!o||typeof o!=="object"||Array.isArray(o))throw new d(`douyin stats ${e}`,"The work exists, but creator metrics are unavailable");return Object.entries(o).map(([p,_])=>({metric:p,value:_}))}const s=u.max_cursor;if(!u.has_more||s===void 0||s===null||i!==void 0&&String(s)===String(i))break;i=s}const w=await A(t,e);throw new d(`douyin stats ${e}`,w??"The work was not found in the logged-in creator account")}});
|
package/clis/douyin/videos.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as p,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentError as g}from"@jackwener/opencli/errors";import{formatBeijingTime as c,pendingTimerStatus as d,scheduledPublicTime as h,walkWorkList as _}from"./_shared/work-list.js";const b={all:0,published:1,reviewing:0,scheduled:0};function v(n){const e=n.status;if(typeof e==="number")return e;const t=d(n);if(!e)return t??"published";if(e.is_delete)return"deleted";if(e.is_prohibited)return"prohibited";if(e.in_reviewing)return"reviewing";if(t)return t;if(e.is_private)return"private";return"published"}function m(n,e,t){const s=n==null||n===""?t:Number(n);if(!Number.isInteger(s)||s<1)throw new g(`--${e} 必须是正整数`);return s}p({site:"douyin",name:"videos",access:"read",description:"获取作品列表(--status scheduled 列出尚未发出的定时稿及其计划发布时间)",domain:"creator.douyin.com",strategy:f.COOKIE,args:[{name:"limit",type:"int",default:20,help:"每页数量"},{name:"page",type:"int",default:1,help:"页码"},{name:"status",default:"all",choices:["all","published","reviewing","scheduled"]}],columns:["aweme_id","title","status","scheduled_time","play_count","digg_count","create_time"],func:async(n,e)=>{const t=m(e.limit,"limit",20),s=m(e.page,"page",1),u=e.status??"all",o=(s-1)*t,a=[];let r=0;await _(n,(i)=>{const l=v(i);if(u==="scheduled"&&!d(i))return!1;if(u==="reviewing"&&l!=="reviewing")return!1;r+=1;if(r<=o)return!1;a.push({aweme_id:i.aweme_id,title:i.desc??"",status:l,scheduled_time:c(h(i)),play_count:i.statistics?.play_count??0,digg_count:i.statistics?.digg_count??0,create_time:c(i.create_time??i.timer?.public_time??0)});return a.length>=t},{status:b[u]??0,count:Math.min(Math.max(t,12),50)});return a}});
|
package/clis/jike/comments.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as D,Strategy as E}from"@jackwener/opencli/registry";import{ArgumentError as d,CliError as f,CommandExecutionError as m,EXIT_CODES as S}from"@jackwener/opencli/errors";import{gotoJikeApiContext as C,jikeApi as h}from"./api.js";function y(t,i){if(!t||typeof t.id!=="string"||!t.id||t.targetId!==i||t.targetType!=="ORIGINAL_POST")throw new m("即刻返回了缺少 ID 或不属于目标帖子的评论");return t}async function k(t,i,n){const e=await h(t,"/1.0/comments/get?id="+encodeURIComponent(i)+"&targetType=ORIGINAL_POST");if(!e.ok){const o=e.data&&(e.data.error||e.data.message)||e.raw||"";if(e.status>=500)throw new f("TEMPORARILY_UNAVAILABLE",`即刻评论精确回查接口失败(HTTP ${e.status}:${o})。这是接口侧失败(对较早的评论常见),不代表评论已被删除。`,`改用列表回查:ppcli jike comments ${n}(楼中楼加 --thread <顶级评论ID>),在结果里按 id 找这条评论。`,S.TEMPFAIL);throw new f("NOT_FOUND",`即刻评论精确回查未找到 ${i}(HTTP ${e.status}:${o})。可能是评论 ID 抄错/不完整,也可能评论已删除或对当前账号不可见,仅凭这一条无法区分。`,`先核对评论 ID,再用 ppcli jike comments ${n} 列表回查确认。`)}const r=y(e.data?.data,n);if(r.id!==i)throw new m("即刻返回的评论 ID 与请求不一致");return r}function g(t){return{id:t.id,postId:t.targetId,threadId:t.threadId??null,author:t.user?.screenName??null,content:t.content??null,likes:t.likeCount??null,replies:t.replyCount??null,time:t.createdAt??null,level:t.level??null,status:t.status??null}}D({site:"jike",name:"comments",description:"读取即刻帖子的评论、楼中楼,或按评论回执 ID 精确回查(需登录)",access:"read",example:"ppcli jike comments 6a9f43388262cfba44f550db --limit 20 -f json",domain:"web.okjike.com",strategy:E.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"帖子 ID"},{name:"comment-id",type:"string",help:"只回查这一条评论(例如 comment 返回的 comment_id)"},{name:"thread",type:"string",help:"读取这个顶级评论 ID 下的楼中楼;与 --comment-id 互斥"},{name:"sort",type:"string",default:"time",choices:["time","likes"],help:"列表排序:time 时间 / likes 官网智能排序"},{name:"limit",type:"int",default:20,help:"列表最多返回多少条;不含各条 hotReplies 的预览"}],columns:["id","postId","threadId","author","content","likes","replies","time","level","status"],func:async(t,i)=>{const n=String(i.id??"").trim(),e=String(i["comment-id"]??"").trim(),r=String(i.thread??"").trim(),o=i.limit??20,u=i.sort??"time";if(!n)throw new d("帖子 ID 不能为空");if(e&&r)throw new d("--comment-id 与 --thread 不能同时使用");if(!Number.isSafeInteger(o)||o<1)throw new d("--limit 必须是正整数");if(!["time","likes"].includes(u))throw new d("--sort 必须是 time 或 likes");await C(t);if(e)return[g(await k(t,e,n))];if(r){const a=await k(t,r,n);if(a.level!==1||a.threadId!==r)throw new d("--thread 必须是顶级评论 ID;请使用该评论返回的 threadId")}const T=r?"/1.0/comments/list":"/1.0/comments/listPrimary",s=new Map,p=new Set;let w;while(s.size<o){const a=await h(t,T,{targetId:n,targetType:"ORIGINAL_POST",order:u==="time"?"TIME":"LIKES",limit:20,loadMoreKey:w,...r?{primaryCommentId:r,level:1}:{}}),l=a.data;if(!a.ok||!Array.isArray(l?.data))throw new m(`即刻评论列表读取失败(HTTP ${a.status}):${a.raw}`);const A=s.size;for(const O of l.data){const c=y(O,n);if(r&&c.threadId!==r)throw new m("即刻返回了其他评论串的回复");s.set(c.id,c)}if(s.size>=o||l.loadMoreKey==null||l.data.length===0)break;const I=JSON.stringify(l.loadMoreKey);if(p.has(I)||s.size===A)throw new m("即刻评论分页没有前进");p.add(I);w=l.loadMoreKey}if(!s.size){process.stderr.write(`jike comments ${n}: 当前${r?"评论串":"帖子"}没有对本账号可见的评论。空列表是真实结果,不是取数失败。
|
|
2
|
+
`);return[]}return[...s.values()].slice(0,o).map(g)}});
|
package/clis/jike/like.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CommandExecutionError as
|
|
1
|
+
import{CliError as d,CommandExecutionError as k,EXIT_CODES as w}from"@jackwener/opencli/errors";import{cli as g,Strategy as y}from"@jackwener/opencli/registry";import{gotoJikeApiContext as I,jikeApi as c}from"./api.js";const T=[{type:"post",label:"动态",get:"/1.0/originalPosts/get",like:"/1.0/originalPosts/like",unlike:"/1.0/originalPosts/unlike"},{type:"repost",label:"转发",get:"/1.0/reposts/get",like:"/1.0/reposts/like",unlike:"/1.0/reposts/unlike"}];function p(e){return e.data&&(e.data.error||e.data.message)||e.raw||""}function m(e){return new d("TEMPORARILY_UNAVAILABLE",e,"接口侧暂时性失败:稍后重试同一条命令。",w.TEMPFAIL)}async function f(e,o){for(const i of T){const t=await c(e,i.get+"?id="+encodeURIComponent(o));if(t.ok&&t.data&&t.data.data&&t.data.data.id===o)return{kind:i,liked:t.data.data.liked===!0,likes:t.data.data.likeCount??null};if(t.status>=500)throw m(`即刻${i.label}读取接口暂时失败(HTTP ${t.status}:${p(t)}),未执行任何点赞操作。`)}return null}g({site:"jike",name:"like",access:"write",description:"点赞即刻动态或转发(走即刻内部 API,不依赖页面渲染);回执带最终的点赞状态",domain:"web.okjike.com",strategy:y.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"帖子 ID(动态或转发的完整 24 位 ID)"},{name:"undo",type:"boolean",help:"取消点赞(改调 unlike)"}],columns:["status","message","liked","likes","type"],func:async(e,o)=>{const i=String(o.id??"").trim();if(!i)throw new k("帖子 ID 不能为空");const t=Boolean(o.undo),r=t?"取消点赞":"点赞";await I(e);const s=await f(e,i);if(!s)throw new d("NOT_FOUND",`即刻找不到 ID 为 ${i} 的动态或转发,未执行${r}。多为帖子 ID 抄错或不完整(应为完整的 24 位 ID),不代表帖子已被删除。`,"从 topic / search / feed / user 的输出里重新取 id 再试。");const l=!t;if(s.liked===l)return[{status:"success",message:l?"此前已点过赞,未重复请求":"此前并未点赞,无需取消",liked:s.liked,likes:s.likes,type:s.kind.type}];const n=await c(e,t?s.kind.unlike:s.kind.like,{id:i});if(!n.ok){const u=p(n)||`HTTP ${n.status}`;if(n.status>=500)throw m(`${r}暂时失败(HTTP ${n.status}:${u})。目标确认存在,稍后重试。`);throw new k(`${r}失败(HTTP ${n.status},平台返回:${u})。目标${s.kind.label}刚刚还能读到,这不代表它已被删除;多为瞬时失败或平台限制。`,"可重试同一条命令(它会先读当前状态,不会重复点赞)。")}let a=null;try{a=await f(e,i)}catch{a=null}return[{status:"success",message:(t?"已取消点赞":"点赞成功")+(a?"":"(未能读回最终状态,liked 未确认)"),liked:a?a.liked:null,likes:a?a.likes:null,type:s.kind.type}]}});
|
|
@@ -1,115 +1,2 @@
|
|
|
1
|
-
import{cli as
|
|
2
|
-
|
|
3
|
-
function getNotificationData(element) {
|
|
4
|
-
for (const key of Object.keys(element)) {
|
|
5
|
-
if (key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$')) {
|
|
6
|
-
let fiber = element[key];
|
|
7
|
-
for (let i = 0; i < 15 && fiber; i++) {
|
|
8
|
-
const props = fiber.memoizedProps || fiber.pendingProps;
|
|
9
|
-
if (props && props.data) {
|
|
10
|
-
const d = props.data;
|
|
11
|
-
// 通知条目特征:含 type/actionType 字段,以及来源用户字段
|
|
12
|
-
if (d.type || d.actionType || d.notificationType) return d;
|
|
13
|
-
}
|
|
14
|
-
fiber = fiber.return;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const results = [];
|
|
22
|
-
const seen = new Set();
|
|
23
|
-
|
|
24
|
-
// 通知页使用 _item_ 类前缀作为条目容器
|
|
25
|
-
const elements = Array.from(document.querySelectorAll('[class*="_item_"]'));
|
|
26
|
-
|
|
27
|
-
for (const el of elements) {
|
|
28
|
-
const data = getNotificationData(el);
|
|
29
|
-
if (!data) continue;
|
|
30
|
-
|
|
31
|
-
const actionType = data.actionType || data.type || data.notificationType || '';
|
|
32
|
-
const fromUser =
|
|
33
|
-
(data.sourceUser && data.sourceUser.screenName) ||
|
|
34
|
-
(data.user && data.user.screenName) ||
|
|
35
|
-
(data.actionUser && data.actionUser.screenName) ||
|
|
36
|
-
(data.actor && data.actor.screenName) ||
|
|
37
|
-
'';
|
|
38
|
-
const targetContent =
|
|
39
|
-
(data.targetPost && data.targetPost.content) ||
|
|
40
|
-
(data.post && data.post.content) ||
|
|
41
|
-
'';
|
|
42
|
-
const commentContent =
|
|
43
|
-
(data.comment && data.comment.content) ||
|
|
44
|
-
data.commentContent ||
|
|
45
|
-
'';
|
|
46
|
-
const content = commentContent || targetContent;
|
|
47
|
-
const time = data.createdAt || data.updatedAt || '';
|
|
48
|
-
|
|
49
|
-
// 用 user+time+type 去重,避免同一用户同一时间不同类型通知被合并
|
|
50
|
-
const key = fromUser + '\x00' + time + '\x00' + (data.type || data.actionType || '');
|
|
51
|
-
if (seen.has(key)) continue;
|
|
52
|
-
seen.add(key);
|
|
53
|
-
|
|
54
|
-
if (!fromUser && !content) continue;
|
|
55
|
-
|
|
56
|
-
results.push({ actionType, fromUser, content, time });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return results;
|
|
60
|
-
})()`);if(a.length>0)return a.map((n)=>({type:c(n.actionType),user:n.fromUser,content:n.content.replace(/\n/g," ").slice(0,100),time:n.time})).slice(0,i);await t.autoScroll({times:Math.ceil(i/10),delayMs:2000});return(await t.evaluate(`(() => {
|
|
61
|
-
const results = [];
|
|
62
|
-
const items = document.querySelectorAll('[class*="_item_"]');
|
|
63
|
-
|
|
64
|
-
// 动作关键词,用于定位通知类型行
|
|
65
|
-
// 长模式优先匹配,避免"赞了你"截断"赞了你的动态"
|
|
66
|
-
const actionPatterns = [
|
|
67
|
-
'赞了你的动态', '赞了你的评论', '赞了你的转发',
|
|
68
|
-
'评论了你的动态', '评论了你的转发',
|
|
69
|
-
'回复了你的评论', '回复了你',
|
|
70
|
-
'转发了你的动态',
|
|
71
|
-
'提到了你', '关注了你',
|
|
72
|
-
'赞了你', '评论了你', '转发了你',
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
for (const item of items) {
|
|
76
|
-
const text = item.innerText || '';
|
|
77
|
-
const lines = text.split('\\n').map(l => l.trim()).filter(Boolean);
|
|
78
|
-
if (lines.length < 2) continue;
|
|
79
|
-
|
|
80
|
-
// 策略:在全文中搜索动作关键词来定位类型
|
|
81
|
-
let type = '';
|
|
82
|
-
let user = '';
|
|
83
|
-
let time = '';
|
|
84
|
-
let content = '';
|
|
85
|
-
|
|
86
|
-
const fullText = lines.join(' ');
|
|
87
|
-
for (const pattern of actionPatterns) {
|
|
88
|
-
const idx = fullText.indexOf(pattern);
|
|
89
|
-
if (idx >= 0) {
|
|
90
|
-
// 关键词前面是用户名(可能多人用、分隔)
|
|
91
|
-
user = fullText.slice(0, idx).replace(/[、,]/g, ' ').trim();
|
|
92
|
-
type = pattern;
|
|
93
|
-
// 关键词后面可能是时间和原帖内容
|
|
94
|
-
const rest = fullText.slice(idx + pattern.length).trim();
|
|
95
|
-
// 时间通常以数字或"刚刚"/"分钟前"等开头
|
|
96
|
-
const timeMatch = rest.match(/^[\\S]*?(?:刚刚|\\d+.*?前|\\d{4}\\/\\d{2}\\/\\d{2})/);
|
|
97
|
-
time = timeMatch ? timeMatch[0] : '';
|
|
98
|
-
content = rest.slice(time.length).trim().slice(0, 100);
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 没匹配到动作关键词,回退到简单行序
|
|
104
|
-
if (!type) {
|
|
105
|
-
user = lines[0] || '';
|
|
106
|
-
type = lines[1] || '';
|
|
107
|
-
time = lines[2] || '';
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (!user && !type) continue;
|
|
111
|
-
results.push({ type, user, content, time });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return results;
|
|
115
|
-
})()`)).slice(0,i)}});
|
|
1
|
+
import{cli as p,Strategy as y}from"@jackwener/opencli/registry";import{ArgumentError as g,CommandExecutionError as d}from"@jackwener/opencli/errors";import{gotoJikeApiContext as w,jikeApi as I}from"./api.js";function m(t){const e=String(t||"").toUpperCase();if(!e)return"通知";if(e.includes("REPL"))return"回复了你";if(e.includes("LIKE"))return"赞了你";if(e.includes("COMMENT"))return"评论了你";if(e.includes("FOLLOW"))return"关注了你";if(e.includes("REPOST"))return"转发了你";if(e.includes("MENTION"))return"提到了你";return String(t)}const l=(t)=>String(t??"").replace(/\s*\n\s*/g," ").trim();function f(t){const e=t.actionItem||{},r=t.referenceItem||{},n=(Array.isArray(e.users)?e.users:[]).map((i)=>i?.screenName).filter(Boolean),s=Number.isInteger(e.usersCount)&&e.usersCount>n.length?` 等 ${e.usersCount} 人`:"",c=r.type==="ORIGINAL_POST"||r.type==="REPOST";return{type:m(t.type),user:n.join("、")+s,content:l(e.content),time:t.createdAt||t.updatedAt||"",reference:l(r.content).slice(0,100),post_id:e.targetId||r.targetId||(c?r.id:"")||"",comment_id:e.type==="COMMENT"&&e.id?e.id:"",kind:t.type||""}}p({site:"jike",name:"notifications",access:"read",description:"即刻通知(content = 对方评论/回复的正文,reference = 被作用的我方内容;post_id / comment_id 可直接用于回查与回复)",domain:"web.okjike.com",strategy:y.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:20}],columns:["type","user","content","time","reference","post_id","comment_id","kind"],func:async(t,e)=>{const r=e.limit??20;if(!Number.isSafeInteger(r)||r<1)throw new g("--limit 必须是正整数");await w(t);const n=[],s=new Set;let c;while(n.length<r){const i=await I(t,"/1.0/notifications/list",{limit:20,loadMoreKey:c}),o=i.data;if(!i.ok||!Array.isArray(o?.data))throw new d(`即刻通知读取失败:HTTP ${i.status} / ${i.raw}`);n.push(...o.data.filter((u)=>u&&typeof u==="object"));if(o.loadMoreKey==null||o.data.length===0)break;const a=JSON.stringify(o.loadMoreKey);if(s.has(a))throw new d("即刻通知分页没有前进");s.add(a);c=o.loadMoreKey}if(n.length===0)process.stderr.write(`jike notifications: 当前账号没有任何通知。空列表是真实结果,不是取数失败。
|
|
2
|
+
`);return n.slice(0,r).map(f)}});export const __test__={label:m,row:f};
|
package/clis/jike/post.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as n}from"@jackwener/opencli/registry";import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{gotoJikeApiContext as i,jikeApi as p}from"./api.js";async function c(e,r){try{await i(e);const t=await p(e,"/1.0/originalPosts/get?id="+encodeURIComponent(r));if(t.ok&&t.data?.data?.id===r)return t.data.data.liked===!0}catch{return null}return null}n({site:"jike",name:"post",access:"read",description:"即刻帖子详情及评论(post 行的 liked = 当前账号是否已赞;未登录或读不到时为空)",domain:"m.okjike.com",browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"Post ID (from post URL)"}],columns:["type","author","content","likes","liked","time"],func:async(e,r)=>{await e.goto(`https://m.okjike.com/originalPosts/${r.id}`);const t=await e.evaluate(`(() => {
|
|
2
2
|
const el = document.querySelector('script[type="application/json"]');
|
|
3
3
|
if (!el) return { ok: false, reason: 'missing-data-script' };
|
|
4
4
|
try {
|
|
@@ -30,4 +30,4 @@ import{cli as a}from"@jackwener/opencli/registry";import{CommandExecutionError a
|
|
|
30
30
|
return { ok: false, reason: 'parse-error', message: e?.message || String(e) };
|
|
31
31
|
}
|
|
32
32
|
})()
|
|
33
|
-
`);if(Array.isArray(e))return
|
|
33
|
+
`);if(Array.isArray(t)){const a=await c(e,String(r.id));return t.map((o)=>({type:o.type??"",author:o.author??"",content:o.content??"",likes:o.likes??0,liked:o.type==="post"?a:null,time:o.time??""}))}if(t?.reason==="missing-data-script")throw new s("Jike post page did not expose the expected data script");if(t?.reason==="parse-error")throw new s(`Failed to parse Jike post data: ${t.message||"unknown error"}`);throw new s("Jike post returned an unreadable payload")}});
|
package/clis/jike/topic.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as b,Strategy as h}from"@jackwener/opencli/registry";import{ArgumentError as k,CommandExecutionError as a,EmptyResultError as g}from"@jackwener/opencli/errors";import{gotoJikeApiContext as A,jikeApi as w}from"./api.js";b({site:"jike",name:"topic",access:"read",description:"即刻话题/圈子帖子(登录后按游标分页)",domain:"web.okjike.com",strategy:h.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"Topic ID (from topic URL, e.g. 553870e8e4b0cafb0a1bef68; find one by name with `jike topics <keyword>`)"},{name:"limit",type:"int",default:20,help:"最多返回多少条帖子"}],columns:["id","content","author","likes","comments","time","url"],func:async(c,d)=>{const r=d.limit??20;if(!Number.isSafeInteger(r)||r<1)throw new k("--limit 必须是正整数");await A(c);const s=await w(c,"/1.0/topics/getDetail?id="+encodeURIComponent(d.id)),n=s.data?.data;if(!s.ok||!n||!Array.isArray(n.tabs))throw new a("即刻话题信息读取失败:"+s.status+" / "+s.raw);const m=n.tabs.find((e)=>e.type==="square")??n.tabs.find((e)=>e.type===n.entryTab);if(!m||typeof m.type!=="string")throw new a("即刻话题未提供可读取的帖子栏目");const i=new Map,l=new Set;let p;while(i.size<r){const e=await w(c,"/1.0/topics/tabs/"+encodeURIComponent(m.type)+"/feed",{limit:20,loadMoreKey:p,topicId:d.id}),o=e.data;if(!e.ok||!Array.isArray(o?.data))throw new a("即刻话题帖子读取失败:"+e.status+" / "+e.raw);const y=i.size;let f=0;for(const t of o.data){if(t&&typeof t==="object"&&t.id==null&&t.type==null){f+=1;continue}if(!t||typeof t.id!=="string"||!t.id)throw new a("即刻话题帖子缺少 id");i.set(t.id,t)}if(i.size>=r||!o.loadMoreKey||o.data.length===0)break;const u=JSON.stringify(o.loadMoreKey);if(l.has(u)||i.size===y&&f===0)throw new a("即刻话题分页没有前进");l.add(u);p=o.loadMoreKey}if(i.size===0)throw new g("jike topic","该圈子没有返回帖子");return[...i.values()].slice(0,r).map((e)=>({id:e.id,content:(e.content||"").replace(/\n/g," ").slice(0,80),author:e.user?.screenName||"",likes:e.likeCount??0,comments:e.commentCount??0,time:e.actionTime||e.createdAt||"",url:"https://web.okjike.com/"+(e.type==="REPOST"?"repost":"originalPost")+"/"+e.id}))}});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{cli as T,Strategy as j}from"@jackwener/opencli/registry";import{ArgumentError as g,CommandExecutionError as c}from"@jackwener/opencli/errors";import{gotoJikeApiContext as E,jikeApi as l}from"./api.js";const b=20;function A(t,r){return{id:t.id,name:t.content??"",subscribers:t.subscribersCount??null,intro:String(t.briefIntro??"").replace(/\s+/g," ").slice(0,80),postable:typeof t.enableForUserPost==="boolean"?t.enableForUserPost:r.postable,subscribed:r.subscribed,url:"https://web.okjike.com/topic/"+t.id}}function S(t,r){const i=t.data?.data;if(!t.ok||!Array.isArray(i))throw new c(`即刻${r}读取失败:HTTP ${t.status} / ${t.raw}`);for(const s of i)if(!s||typeof s.id!=="string"||!s.id)throw new c(`即刻${r}返回了缺少 id 的圈子`);return i}T({site:"jike",name:"topics",access:"read",description:"找即刻圈子:带关键词按名字搜(仅返回可发帖的圈子),不带关键词列出我关注的圈子;输出的 id 可直接传给 topic",example:"ppcli jike topics 求职 -f json",domain:"web.okjike.com",strategy:j.COOKIE,browser:!0,args:[{name:"query",type:"string",positional:!0,help:"圈子名关键词;不传则列出我关注的圈子"},{name:"limit",type:"int",default:20,help:"最多返回多少个圈子(按名字搜时官网接口单次最多 20 个)"}],columns:["id","name","subscribers","intro","postable","subscribed","url"],func:async(t,r)=>{const i=String(r.query??"").trim(),s=r.limit??20;if(!Number.isSafeInteger(s)||s<1)throw new g("--limit 必须是正整数");if(i&&s>b)throw new g(`按名字搜圈子时 --limit 不能超过 ${b}(官网该接口不翻页)`,"换更具体的关键词缩小范围");await E(t);if(i){const e=await l(t,"/1.0/users/topics/search",{keywords:i,limit:b,onlyUserPostEnabled:!0,type:"ALL"}),n=S(e,"圈子搜索");if(n.length===0)process.stderr.write(`jike topics: 没有名字匹配「${i}」且允许发帖的圈子。空列表是真实结果,不是取数失败。
|
|
2
|
+
`);return n.slice(0,s).map((p)=>A(p,{postable:!0,subscribed:null}))}const a=await l(t,"/1.0/users/profile"),d=a.data?.user?.username;if(!a.ok||typeof d!=="string"||!d)throw new c(`即刻当前账号信息读取失败:HTTP ${a.status} / ${a.raw}`);const o=new Map,u=await l(t,"/1.0/shortcuts/listTopping"),m=u.data?.data?.shortcuts;if(!u.ok||!Array.isArray(m))throw new c(`即刻置顶圈子读取失败:HTTP ${u.status} / ${u.raw}`);for(const e of m)if(e&&typeof e.id==="string"&&e.id)o.set(e.id,e);const w=new Set;let y;while(o.size<s){const e=await l(t,"/1.0/topics/listSubscribed",{includeRecentPost:"true",filterShortcuts:"true",username:d,sortBy:"DEFAULT",limit:20,loadMoreKey:y}),n=S(e,"已关注圈子"),p=o.size;for(const k of n)o.set(k.id,k);const f=e.data.loadMoreKey;if(f==null||n.length===0)break;const h=JSON.stringify(f);if(w.has(h)||o.size===p)throw new c("即刻已关注圈子分页没有前进");w.add(h);y=f}if(o.size===0)process.stderr.write(`jike topics: 当前账号没有关注任何圈子。空列表是真实结果;按名字找圈子请带关键词。
|
|
3
|
+
`);return[...o.values()].slice(0,s).map((e)=>A(e,{postable:null,subscribed:!0}))}});
|
package/clis/nowcoder/auth.js
CHANGED
|
@@ -1,24 +1 @@
|
|
|
1
|
-
import{AuthRequiredError as
|
|
2
|
-
try {
|
|
3
|
-
const link = document.querySelector('a[href*="/users/"]');
|
|
4
|
-
const href = link ? (link.getAttribute('href') || '') : '';
|
|
5
|
-
const match = href.match(/\\/users\\/(\\d+)/);
|
|
6
|
-
if (!match) {
|
|
7
|
-
return { kind: 'auth', detail: 'No logged-in nowcoder profile link in nav (anonymous)' };
|
|
8
|
-
}
|
|
9
|
-
const uid = match[1];
|
|
10
|
-
const r = await fetch('https://gw-c.nowcoder.com/api/sparta/user/profile/' + uid, {
|
|
11
|
-
credentials: 'include',
|
|
12
|
-
headers: { Accept: 'application/json' },
|
|
13
|
-
});
|
|
14
|
-
if (r.status === 401 || r.status === 403) return { kind: 'auth', detail: 'nowcoder profile HTTP ' + r.status };
|
|
15
|
-
if (!r.ok) return { kind: 'http', httpStatus: r.status };
|
|
16
|
-
const d = await r.json();
|
|
17
|
-
if (!d || !d.success || !d.data || !d.data.id) {
|
|
18
|
-
return { kind: 'auth', detail: 'nowcoder profile returned no user data (anonymous)' };
|
|
19
|
-
}
|
|
20
|
-
return { ok: true, user_id: String(d.data.id), nickname: String(d.data.nickname || '') };
|
|
21
|
-
} catch (e) {
|
|
22
|
-
return { kind: 'exception', detail: String(e && e.message || e) };
|
|
23
|
-
}
|
|
24
|
-
})()`;async function a(o){if(!await r(o))throw new e("nowcoder.com","Nowcoder t cookie missing (anonymous)");await o.goto("https://www.nowcoder.com/");await o.wait(2);const t=await o.evaluate(d);if(t?.kind==="auth")throw new e("nowcoder.com",t.detail);if(t?.kind==="http")throw new n(`HTTP ${t.httpStatus} from nowcoder profile API`);if(t?.kind==="exception")throw new n(`Nowcoder whoami failed: ${t.detail}`);if(!t?.ok)throw new n(`Unexpected nowcoder probe: ${JSON.stringify(t)}`);return{user_id:t.user_id,nickname:t.nickname}}c({site:"nowcoder",domain:"nowcoder.com",loginUrl:"https://www.nowcoder.com/login",columns:["user_id","nickname"],quickCheck:r,verify:a,poll:async(o)=>{if(!await r(o))throw new e("nowcoder.com","Waiting for Nowcoder login");return a(o)}});
|
|
1
|
+
import{AuthRequiredError as r}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as s}from"../_shared/site-auth.js";import{getSession as c}from"./utils.js";async function e(o){return(await o.getCookies({url:"https://www.nowcoder.com"})).some((n)=>n.name==="t"&&n.value)}async function t(o){if(!await e(o))throw new r("nowcoder.com","Nowcoder t cookie missing (anonymous)");await o.goto("https://www.nowcoder.com/");await o.wait(2);const i=await c(o);return{user_id:i.uid,nickname:i.nickname}}s({site:"nowcoder",domain:"nowcoder.com",loginUrl:"https://www.nowcoder.com/login",columns:["user_id","nickname"],quickCheck:async(o)=>{return{logged_in:await e(o),verified:!1,basis:"cookie"}},verify:t,poll:async(o)=>{if(!await e(o))throw new r("nowcoder.com","Waiting for Nowcoder login");return t(o)}});
|
package/clis/nowcoder/comment.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as w,Strategy as I}from"@jackwener/opencli/registry";import{ArgumentError as _}from"@jackwener/opencli/errors";import{GW_BASE as C,NowcoderError as p,WWW_BASE as f,getJson as h,getSession as S,plainText as T,postForm as E,requireOk as b,resolvePostTarget as x,resolveReplyTarget as A}from"./utils.js";import{assertLiteralContent as z}from"../_shared/content-guard.js";function y(e){return String(e??"").replace(/\s+/g,"")}function F(e){if(e.url)return`帖页地址: ${e.url}`;return`这是动态帖(7 位 momentId ${e.id}),帖页地址形如 ${f}/feed/main/detail/<uuid>,`+"不能用 /discuss/<momentId>(打不开);uuid 见 recommend / search / experience / my 的 uuid 列。"}w({site:"nowcoder",name:"comment",access:"write",description:"评论帖子;配 --to-user/--to-comment 可定向回复某条评论",domain:"www.nowcoder.com",strategy:I.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(数字 momentId / UUID / 链接均可)"},{name:"content",fromFile:!0,positional:!0,required:!0,help:"评论内容"},{name:"to-user",type:"str",default:"0",help:"帖主/对方 userId(定向回复时必传)"},{name:"to-comment",type:"str",default:"0",help:"定向回复的评论 id(inbox 的 reply_comment_id)"}],columns:["post_id","kind","status","comment_id","stored_content","content_altered","uid","url"],func:async(e,n)=>{const g=await S(e),o=await x(e,n.id),r=String(n.content??"").trim();z(r,{label:"评论内容"});if(!r)throw new _("评论内容不能为空");const u=String(n["to-user"]??"0").trim()||"0",i=String(n["to-comment"]??"0").trim()||"0",s=i==="0"?o:await A(e,o,i,u),l=s.authorId||u;let m;try{m=b(await E(e,`${f}/comment/create?_=${Date.now()}`,{entityType:String(s.entityType),entityId:s.id,commentContent:r,contentV2:JSON.stringify({pureText:r,imgs:[]}),toFeed:"false",toUserId:l,toCommentId:i,entityOwnerId:l,isAnonymousFlag:"false"}),"评论",{id:n.id,dumpBody:!0})}catch(t){if(t instanceof p&&t.code==="CAPTCHA_REQUIRED")throw new p(t.code,t.message,`${t.hint} ${F(o)}`,t.exitCode,{bizCode:t.bizCode,bizMsg:t.bizMsg});throw t}const c=String(m.commentId??m.data?.commentId??"");let d=typeof m.commentContent==="string"?m.commentContent:null;if(d===null&&/^\d+$/.test(c)){const t=await h(e,`${C}/detail/comment-terminal/${c}`);if(t?.success&&typeof t.data?.content==="string")d=t.data.content}const a=d===null?null:T(d);return[{post_id:o.id,kind:i!=="0"?`reply-to-${i}`:"comment",status:"ok",comment_id:c,stored_content:a,content_altered:a===null?null:y(a)!==y(r),uid:g.uid,url:o.url}]}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as c,Strategy as u}from"@jackwener/opencli/registry";import{ArgumentError as y,CommandExecutionError as l,EmptyResultError as p}from"@jackwener/opencli/errors";import{GW_BASE as f,getJson as g,noteTrustedEmpty as w,plainText as C,requireOk as I,resolvePostTarget as _,toIso as h}from"./utils.js";async function s(m,o,r,e,n){const i=`${f}/comment/list-by-page?entityId=${encodeURIComponent(o)}&entityType=${r}&order=2&pageNo=${e}&toCommentId=0&_=${Date.now()}`;return I(await g(m,i),"评论列表",{id:n}).data??{}}c({site:"nowcoder",name:"comments",access:"read",description:"某帖的评论列表(评论前先读,避免重复/不合语境)",domain:"www.nowcoder.com",strategy:u.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(18 位讨论帖 id / 动态 uuid / 7 位 momentId / 链接均可)"},{name:"page",type:"int",default:1,help:"页码"}],columns:["comment_id","user_id","nickname","content","to_comment_id","likes","replies","time","total","total_all"],func:async(m,o)=>{const r=o.page;if(!Number.isSafeInteger(r)||r<1)throw new y("--page 必须是正整数");const e=await _(m,o.id);let n=await s(m,e.id,e.entityType,r,o.id);const i=Number(e.totalComments??0);if(Number(n.total??0)===0&&i>0&&e.legacy)n=await s(m,e.legacy.id,e.legacy.entityType,r,o.id);const d=n.records;if(!Array.isArray(d))throw new l("牛客评论列表未返回 records 数组");const a=Number(n.total??0);if(a===0&&i>0)throw new l(`牛客帖子 ${o.id} 的计数显示有 ${i} 条评论,但评论列表为空`,"已按该帖型能用的实体类型都查过。这不是空楼,是读不到;不要据此判断「没人评论」。");if(d.length===0){if(a>0)throw new p("nowcoder comments",`第 ${r} 页已超出范围(该帖共 ${a} 条一级评论,${Number(n.totalPage??0)} 页)。`);return w("comments",`帖子 ${o.id} 评论列表服务端 total=0`+(e.totalComments===null?"(7 位 momentId 读不到帖子自身计数,仅以列表为准)":`,帖子自身的评论计数也是 ${i}`)+"。")}return d.map((t)=>({comment_id:t.id??"",user_id:t.authorId??t.userBrief?.userId??"",nickname:t.userBrief?.nickname??"",content:C(t.content),to_comment_id:t.toCommentId?String(t.toCommentId):"",likes:t.frequencyData?.likeCnt??0,replies:t.frequencyData?.totalCommentCnt??t.frequencyData?.commentCnt??0,time:h(t.createTime),total:a,total_all:Number(n.totalCommentCnt??a)}))}});
|
package/clis/nowcoder/delete.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as u,Strategy as w}from"@jackwener/opencli/registry";import{GW_BASE as o,postMultipart as s,requireOk as a,resolveMomentId as p}from"./utils.js";const y={Accept:"application/json, text/plain, */*","X-Requested-With":"XMLHttpRequest"};u({site:"nowcoder",name:"delete",access:"write",description:"删除自己发布的动态或文章",domain:"www.nowcoder.com",strategy:w.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"要删除的 id(动态取 my/post 的 moment_id,文章取 article 的 content_id)"}],columns:["id","kind","status"],func:async(
|
|
1
|
+
import{cli as u,Strategy as w}from"@jackwener/opencli/registry";import{GW_BASE as o,postMultipart as s,requireOk as a,resolveMomentId as p}from"./utils.js";const y={Accept:"application/json, text/plain, */*","X-Requested-With":"XMLHttpRequest"};u({site:"nowcoder",name:"delete",access:"write",description:"删除自己发布的动态或文章",domain:"www.nowcoder.com",strategy:w.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"要删除的 id(动态取 my/post 的 moment_id,文章取 article 的 content_id)"}],columns:["id","kind","status"],func:async(i,c)=>{const t=String(c.id??"").trim(),d=/^\d{17,}$/.test(t),r=async()=>{const e=await p(i,t);a(await s(i,`${o}/moment/delete`,{isWeb:"true",momentId:e}),"删除动态",{id:t});return[{id:e,kind:"moment",status:"deleted"}]},n=async()=>{a(await s(i,`${o}/long-content/delete?_=${Date.now()}`,{id:t},y),"删除文章",{id:t});return[{id:t,kind:"article",status:"deleted"}]},[m,l]=d?[n,r]:[r,n];try{return await m()}catch(e){try{return await l()}catch{throw e}}}});
|
package/clis/nowcoder/detail.js
CHANGED
|
@@ -1,48 +1 @@
|
|
|
1
|
-
import{cli as
|
|
2
|
-
const raw = \${{ args.id | json }};
|
|
3
|
-
const base = 'https://gw-c.nowcoder.com';
|
|
4
|
-
// 牛客把正文里的半角引号按 HTML 实体存库,网页端渲染时才反转义;这里一并解回来,
|
|
5
|
-
// 否则调用方读到的是 "。& 必须放最后,不然 &quot; 会被二次解码成引号。
|
|
6
|
-
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/"/g, '"').replace(/�?39;/g, "'").replace(/'/g, "'").replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
7
|
-
|
|
8
|
-
let id = raw;
|
|
9
|
-
const urlMatch = raw.match(/discuss\\/(\\d+)/);
|
|
10
|
-
if (urlMatch) id = urlMatch[1];
|
|
11
|
-
|
|
12
|
-
let data = null;
|
|
13
|
-
|
|
14
|
-
if (/[a-f]/.test(id) && id.length > 20) {
|
|
15
|
-
const r = await fetch(base + '/api/sparta/detail/moment-data/detail/' + id, {credentials: 'include'});
|
|
16
|
-
const d = await r.json();
|
|
17
|
-
if (d.success && d.data) data = d.data;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (!data && /^\\d+$/.test(id)) {
|
|
21
|
-
const r = await fetch(base + '/api/sparta/detail/content-data/detail/' + id, {credentials: 'include'});
|
|
22
|
-
const d = await r.json();
|
|
23
|
-
if (d.success && d.data) data = d.data;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (!data && /^\\d+$/.test(id)) {
|
|
27
|
-
const r = await fetch(base + '/api/sparta/detail/moment-data/detail/' + id, {credentials: 'include'});
|
|
28
|
-
const d = await r.json();
|
|
29
|
-
if (d.success && d.data) data = d.data;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (!data) throw new Error('Post not found: ' + id);
|
|
33
|
-
|
|
34
|
-
const user = data.userBrief || {};
|
|
35
|
-
const freq = data.frequencyData || {};
|
|
36
|
-
return [{
|
|
37
|
-
title: strip(data.title || '') || '(untitled)',
|
|
38
|
-
author: user.nickname || '',
|
|
39
|
-
school: user.educationInfo || '',
|
|
40
|
-
content: strip(data.content || '').substring(0, 500),
|
|
41
|
-
likes: freq.likeCnt || 0,
|
|
42
|
-
comments: freq.commentCnt || freq.totalCommentCnt || 0,
|
|
43
|
-
views: freq.viewCnt || 0,
|
|
44
|
-
time: data.createdAt ? new Date(data.createdAt).toISOString().slice(0, 19) : '',
|
|
45
|
-
location: data.ip4Location || '',
|
|
46
|
-
}];
|
|
47
|
-
})()
|
|
48
|
-
`}]});
|
|
1
|
+
import{cli as u,Strategy as a}from"@jackwener/opencli/registry";import{ArgumentError as m}from"@jackwener/opencli/errors";import{plainText as n,resolvePostTarget as c,toIso as s}from"./utils.js";u({site:"nowcoder",name:"detail",access:"read",description:"帖子详情(18 位讨论帖 id / 动态 uuid / 帖子链接)",domain:"www.nowcoder.com",strategy:a.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"18 位讨论帖 id、动态 uuid 或帖子链接(7 位 momentId 查不了详情)"}],columns:["title","author","school","content","likes","comments","views","time","location","id","uuid","entity_type","author_id","liked","url"],func:async(r,d)=>{const e=await c(r,d.id),t=e.detail;if(!t)throw new m(`7 位 momentId ${e.id} 查不了详情`,"动态详情接口只认 uuid。请改传该动态的 uuid 或 /feed/main/detail/<uuid> 链接"+"(recommend / search / experience / my 的 uuid、url 列);评论列表可以直接用 momentId 查(comments)。");const o=t.userBrief??{},i=t.frequencyData??{};return[{title:n(t.title),author:o.nickname??"",school:o.educationInfo??"",content:n(t.content).substring(0,500),likes:i.likeCnt??0,comments:i.totalCommentCnt??i.commentCnt??0,views:i.viewCnt??0,time:s(t.createdAt??t.createTime).slice(0,19),location:t.ip4Location??"",id:e.id,uuid:e.uuid,entity_type:e.entityType,author_id:e.authorId,liked:e.liked,url:e.url}]}});
|
package/clis/nowcoder/dm-list.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as i,Strategy as d}from"@jackwener/opencli/registry";import{EmptyResultError as l}from"@jackwener/opencli/errors";import{WWW_BASE as c,decodeHtmlEntities as u,getJson as m,getSession as g,noteTrustedEmpty as _,requireOk as p}from"./utils.js";i({site:"nowcoder",name:"dm-list",access:"read",description:"私信会话列表(conversationId / 最后一条消息 / 未读数)",domain:"www.nowcoder.com",strategy:d.COOKIE,args:[{name:"page",type:"int",default:1,help:"页码"}],columns:["conversation_id","target_user_id","target_name","last_content","unread","allow_send","is_official","uid"],func:async(o,n)=>{const{uid:e}=await g(o),r=`${c}/sns/message/${e}/get-conversation-list?page=${n.page}`,a=p(await m(o,r),"私信会话列表"),s=a.data?.conversations??[];if(s.length===0){const t=Number(a.data?.totalCount??0);if(t>0)throw new l("nowcoder dm-list",`第 ${n.page} 页已超出范围(当前账号 uid=${e} 共 ${t} 个会话,${a.data?.totalPage??"?"} 页)。`);return _("dm-list",`已鉴权(当前账号 uid=${e}),服务端 totalCount=0,没有私信会话。`)}return s.map((t)=>({conversation_id:t.conversationId??"",target_user_id:t.targetUserId??"",target_name:t.targetUserName??"",last_content:u(t.content).replace(/\s+/g," ").slice(0,80),unread:t.unreadMessageCount??0,allow_send:t.allowSendMessage??"",is_official:t.targetIsOfficialAccount??"",uid:e}))}});
|
package/clis/nowcoder/dm-read.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as s,Strategy as d}from"@jackwener/opencli/registry";import{WWW_BASE as c,decodeHtmlEntities as g,getJson as m,getSelfUid as p,requireOk as l,resolveConversation as u}from"./utils.js";const _={0:"text",1:"image"};s({site:"nowcoder",name:"dm-read",access:"read",description:"读某私信会话的消息(target 可填 dm-list 的 conversation_id 或 target_user_id)",domain:"www.nowcoder.com",strategy:d.COOKIE,args:[{name:"target",positional:!0,required:!0,help:"dm-list 的 conversation_id,或对方 userId"},{name:"page",type:"int",default:1,help:"页码"},{name:"page-size",type:"int",default:15,help:"每页条数"}],columns:["message_id","direction","from_user_id","type","content","created_at"],func:async(
|
|
1
|
+
import{cli as s,Strategy as d}from"@jackwener/opencli/registry";import{WWW_BASE as c,decodeHtmlEntities as g,getJson as m,getSelfUid as p,requireOk as l,resolveConversation as u}from"./utils.js";const _={0:"text",1:"image"};s({site:"nowcoder",name:"dm-read",access:"read",description:"读某私信会话的消息(target 可填 dm-list 的 conversation_id 或 target_user_id)",domain:"www.nowcoder.com",strategy:d.COOKIE,args:[{name:"target",positional:!0,required:!0,help:"dm-list 的 conversation_id,或对方 userId"},{name:"page",type:"int",default:1,help:"页码"},{name:"page-size",type:"int",default:15,help:"每页条数"}],columns:["message_id","direction","from_user_id","type","content","created_at"],func:async(a,t)=>{const n=await p(a),{conversationId:i}=await u(a,t.target,n),r=`${c}/sns/message/${n}/get-msg-list?conversationId=${encodeURIComponent(i)}&ownerId=${n}&page=${t.page}&pageSize=${t["page-size"]}&_=${Date.now()}`;return((l(await m(a,r),"私信读取",{id:t.target}).data??{}).messages??[]).map((e)=>{const o=String(e.send??"");return{message_id:e.id??"",direction:o===String(n)?"out":"in",from_user_id:o,type:_[e.contentType]??String(e.contentType??""),content:g(e.content).trim(),created_at:e.createDate?new Date(e.createDate).toISOString():""}})}});
|
package/clis/nowcoder/dm-send.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as s,Strategy as n}from"@jackwener/opencli/registry";import{ArgumentError as a}from"@jackwener/opencli/errors";import{WWW_BASE as d,getSelfUid as m,postForm as c,requireOk as l,resolveConversation as u}from"./utils.js";import{assertLiteralContent as p}from"../_shared/content-guard.js";s({site:"nowcoder",name:"dm-send",access:"write",description:"发私信(target 可填对方 userId 或 dm-list 的 conversation_id)",domain:"www.nowcoder.com",strategy:n.COOKIE,args:[{name:"target",positional:!0,required:!0,help:"对方 userId(dm-list 的 target_user_id),也接受 conversation_id"},{name:"content",fromFile:!0,positional:!0,required:!0,help:"私信内容"}],columns:["message_id","receiver","status"],func:async(t,i)=>{const r=String(i.content??"").trim();p(r,{label:"私信内容"});if(!r)throw new a("私信内容不能为空");const e=String(i.target??"").trim(),o=/^\d+$/.test(e)?e:(await u(t,e,await m(t))).peerUid;return[{message_id:l(await c(t,`${d}/sns/message/send-msg?_=${Date.now()}`,{type:"0",title:"",content:r,images:"",receiver:o},{Accept:"application/json, text/plain, */*"}),"私信发送",{id:e,dumpBody:!0}).data?.messageId??"",receiver:o,status:"sent"}]}});
|
|
@@ -1,22 +1 @@
|
|
|
1
|
-
import{cli as
|
|
2
|
-
const page = \${{ args.page }};
|
|
3
|
-
const limit = \${{ args.limit }};
|
|
4
|
-
const r = await fetch('https://gw-c.nowcoder.com/api/sparta/home/tab/content?tabId=818&categoryType=1&pageNo=' + page + '&pageSize=' + limit, {credentials: 'include'});
|
|
5
|
-
const d = await r.json();
|
|
6
|
-
if (!d.success) throw new Error(d.msg || 'API failed');
|
|
7
|
-
// 牛客正文里的引号按 HTML 实体存库,网页端渲染才反转义;& 放最后避免二次解码。
|
|
8
|
-
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/"/g, '"').replace(/�?39;/g, "'").replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
9
|
-
return (d.data?.records || []).map((item, i) => ({
|
|
10
|
-
rank: i + 1,
|
|
11
|
-
title: strip(item.contentData?.title || item.momentData?.title || ''),
|
|
12
|
-
author: item.userBrief?.nickname || '',
|
|
13
|
-
school: item.userBrief?.educationInfo || '',
|
|
14
|
-
content: strip(item.momentData?.content || item.contentData?.content || item.momentData?.newContent || item.contentData?.newContent || ''),
|
|
15
|
-
likes: item.frequencyData?.likeCnt || 0,
|
|
16
|
-
comments: item.frequencyData?.commentCnt || 0,
|
|
17
|
-
views: item.frequencyData?.viewCnt || 0,
|
|
18
|
-
id: item.contentData?.uuid || item.contentData?.id || item.contentId || '',
|
|
19
|
-
momentId: item.contentId || item.momentData?.id || '',
|
|
20
|
-
}));
|
|
21
|
-
})()
|
|
22
|
-
`},{filter:"item.title"},{limit:"${{ args.limit }}"}]});
|
|
1
|
+
import{cli as l,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentError as a,CommandExecutionError as y,EmptyResultError as s}from"@jackwener/opencli/errors";import{GW_BASE as f,feedRecordToPost as g,getJson as h,noteTrustedEmpty as I,requireOk as E}from"./utils.js";const i=20;async function m(o,n){const t=`${f}/home/tab/content?tabId=818&categoryType=1&pageNo=${n}&pageSize=${i}`,r=E(await h(o,t),"面经列表").data?.records;if(!Array.isArray(r))throw new y("牛客面经列表未返回 records 数组");return r}const u=(o)=>o.map((n)=>`${n?.contentType}:${n?.momentData?.id??n?.contentData?.id??n?.contentId}`).join(",");l({site:"nowcoder",name:"experience",access:"read",description:"Interview experience posts (每页固定 20 条)",domain:"www.nowcoder.com",strategy:w.COOKIE,args:[{name:"page",type:"int",default:1,help:"Page number (每页固定 20 条)"},{name:"limit",type:"int",default:i,help:`只取本页前 N 条(1-${i})`}],columns:["rank","title","author","school","likes","comments","views","id","momentId","uuid","entityType","url","time","content"],func:async(o,n)=>{const{page:t,limit:r}=n;if(!Number.isSafeInteger(t)||t<1)throw new a("--page 必须是正整数");if(!Number.isSafeInteger(r)||r<1||r>i)throw new a(`--limit 必须在 1-${i} 之间`,`接口每页固定 ${i} 条,要更多请翻 --page。`);const c=await m(o,t);if(c.length===0){if(t>1)throw new s("nowcoder experience",`面经列表第 ${t} 页已超出真实页数(服务端返回 0 条)。`);return I("experience","面经列表第 1 页服务端返回 0 条记录。")}if(t>1&&u(await m(o,t-1))===u(c))throw new s("nowcoder experience",`面经列表第 ${t} 页已超出真实页数:服务端返回的内容与第 ${t-1} 页完全相同(到底后它只会重复最后一页)。`);return c.slice(0,r).map((d,p)=>{const e=g(d);return{rank:p+1,title:e.title,author:e.author,school:e.school,likes:e.likes,comments:e.comments,views:e.views,id:e.entityType===74&&e.uuid?e.uuid:e.writeId,momentId:e.writeId,uuid:e.uuid,entityType:e.entityType,url:e.url,time:e.time,content:e.content}})}});
|