publishport-opencli 1.0.1 → 1.0.3
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/README.md +61 -61
- package/README.zh-CN.md +68 -68
- package/cli-manifest.json +502 -59
- package/clis/1688/shared.js +2 -2
- package/clis/36kr/news.js +1 -1
- package/clis/_atlassian/shared.js +1 -1
- package/clis/_shared/article/auth.js +1 -1
- package/clis/_shared/article/publish.js +1 -1
- package/clis/_shared/content-guard.js +1 -1
- package/clis/_shared/self-hosted-sites.js +2 -0
- package/clis/_shared/site-auth.js +1 -1
- package/clis/_shared/token-auth.js +2 -2
- package/clis/_shared/video-publish.js +1 -1
- package/clis/aibase/news.js +1 -1
- package/clis/amazon/shared.js +2 -2
- package/clis/antigravity/SKILL.md +12 -12
- package/clis/antigravity/storage.js +1 -1
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/archive/item.js +1 -1
- package/clis/archive/search.js +1 -1
- package/clis/archive/snapshots.js +1 -1
- package/clis/archive/wayback.js +1 -1
- package/clis/arxiv/author.js +1 -1
- package/clis/baijiahao/publish.js +1 -1
- package/clis/bbc/utils.js +1 -1
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/utils.js +1 -1
- package/clis/booking/search.js +1 -1
- package/clis/boss/utils.js +1 -1
- package/clis/chatgpt/ask.js +1 -1
- package/clis/chatgpt/detail.js +1 -1
- package/clis/chatgpt/history.js +1 -1
- package/clis/chatgpt/project-file-add.js +1 -1
- package/clis/chatgpt/project-list.js +1 -1
- package/clis/chatgpt/utils.js +2 -2
- package/clis/chess/utils.js +1 -1
- package/clis/claude/ask.js +1 -1
- package/clis/claude/history.js +1 -1
- package/clis/claude/utils.js +1 -1
- package/clis/codex/extract-diff.js +1 -1
- package/clis/coingecko/coin.js +1 -1
- package/clis/crates/utils.js +1 -1
- package/clis/csdn/stats.js +1 -1
- package/clis/dblp/utils.js +1 -1
- package/clis/defillama/utils.js +1 -1
- package/clis/devto/publish.js +1 -1
- package/clis/discord-app/utils.js +1 -1
- package/clis/dockerhub/utils.js +1 -1
- package/clis/douban/utils.js +1 -1
- package/clis/douyin/_shared/tos-upload.js +1 -1
- package/clis/douyin/hashtag.js +1 -1
- package/clis/douyin/publish-image.js +1 -1
- package/clis/douyin/publish.js +1 -1
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/endoflife/utils.js +1 -1
- package/clis/flathub/utils.js +1 -1
- package/clis/gemini/detail.js +1 -1
- package/clis/gemini/read.js +1 -1
- package/clis/geogebra/add-circle.js +1 -1
- package/clis/geogebra/add-line.js +1 -1
- package/clis/geogebra/add-point.js +1 -1
- package/clis/geogebra/add-polygon.js +1 -1
- package/clis/geogebra/eval.js +1 -1
- package/clis/geogebra/hexagon.js +1 -1
- package/clis/geogebra/info.js +1 -1
- package/clis/geogebra/triangle.js +1 -1
- package/clis/ghost/login.js +1 -0
- package/clis/ghost/publish.js +1 -1
- package/clis/ghost/shared.js +24 -1
- package/clis/ghost/sites.js +1 -0
- package/clis/ghost/whoami.js +1 -1
- package/clis/gitee/user.js +2 -2
- package/clis/github-trending/repos.js +1 -1
- package/clis/goproxy/utils.js +1 -1
- package/clis/grok/export-all.js +1 -1
- package/clis/grok/export.js +1 -1
- package/clis/hashnode/publish.js +1 -1
- package/clis/hf/datasets.js +1 -1
- package/clis/hf/models.js +1 -1
- package/clis/hf/paper.js +1 -1
- package/clis/hf/spaces.js +1 -1
- package/clis/homebrew/utils.js +1 -1
- package/clis/instagram/note.js +1 -1
- package/clis/jimeng/generate.js +1 -1
- package/clis/juejin/utils.js +1 -1
- package/clis/kuaishou/publish.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lichess/utils.js +1 -1
- package/clis/linkedin/search.js +2 -2
- package/clis/linux-do/feed.js +2 -2
- package/clis/lobsters/domain.js +1 -1
- package/clis/mastodon/login.js +1 -0
- package/clis/mastodon/post.js +1 -1
- package/clis/mastodon/shared.js +10 -10
- package/clis/mastodon/sites.js +1 -0
- package/clis/mastodon/whoami.js +1 -1
- package/clis/maven/utils.js +1 -1
- package/clis/mdn/search.js +1 -1
- package/clis/medium/tag.js +1 -1
- package/clis/notebooklm/current.js +1 -1
- package/clis/notebooklm/get.js +1 -1
- package/clis/notebooklm/history.js +1 -1
- package/clis/notebooklm/note-list.js +1 -1
- package/clis/notebooklm/notes-get.js +1 -1
- package/clis/notebooklm/open.js +1 -1
- package/clis/notebooklm/source-fulltext.js +1 -1
- package/clis/notebooklm/source-get.js +1 -1
- package/clis/notebooklm/source-guide.js +1 -1
- package/clis/notebooklm/source-list.js +1 -1
- package/clis/notebooklm/summary.js +1 -1
- package/clis/notebooklm/utils.js +1 -1
- package/clis/npm/utils.js +1 -1
- package/clis/nuget/utils.js +1 -1
- package/clis/nvd/cve.js +1 -1
- package/clis/oeis/utils.js +1 -1
- package/clis/ones/common.js +2 -2
- package/clis/ones/me.js +1 -1
- package/clis/ones/tasks.js +1 -1
- package/clis/ones/token-info.js +1 -1
- package/clis/ones/worklog.js +1 -1
- package/clis/openalex/utils.js +1 -1
- package/clis/oschina/stats.js +1 -1
- package/clis/osv/utils.js +1 -1
- package/clis/packagist/utils.js +1 -1
- package/clis/pixiv/detail.js +1 -1
- package/clis/pixiv/user.js +1 -1
- package/clis/producthunt/utils.js +1 -1
- package/clis/pypi/utils.js +1 -1
- package/clis/qiita/gql.js +2 -2
- package/clis/rest-countries/utils.js +1 -1
- package/clis/rfc/utils.js +1 -1
- package/clis/rubygems/utils.js +1 -1
- package/clis/semanticscholar/utils.js +1 -1
- package/clis/slock/errors.js +1 -1
- package/clis/spotify/spotify.js +1 -1
- package/clis/spotify/utils.js +1 -1
- package/clis/stackoverflow/utils.js +1 -1
- package/clis/steam/utils.js +1 -1
- package/clis/substack/auth.js +2 -2
- package/clis/suno/generate.js +2 -2
- package/clis/suno/list.js +3 -3
- package/clis/telegram/send.js +1 -1
- package/clis/tiktok/creator-videos.js +1 -1
- package/clis/tiktok/publish.js +1 -1
- package/clis/tiktok/utils.js +1 -1
- package/clis/trae-cn/activity.js +1 -1
- package/clis/trae-cn/approve.js +1 -1
- package/clis/trae-cn/ask.js +1 -1
- package/clis/trae-cn/dump.js +1 -1
- package/clis/trae-cn/export.js +1 -1
- package/clis/trae-cn/model.js +1 -1
- package/clis/trae-cn/new.js +1 -1
- package/clis/trae-cn/read.js +1 -1
- package/clis/trae-cn/screenshot.js +1 -1
- package/clis/trae-cn/select-model.js +1 -1
- package/clis/trae-cn/send.js +1 -1
- package/clis/trae-cn/setup.js +1 -1
- package/clis/trae-cn/status.js +1 -1
- package/clis/trae-cn/targets.js +1 -1
- package/clis/trae-cn/watch.js +1 -1
- package/clis/trae-solo/state-fs.js +1 -1
- package/clis/tumblr/publish.js +1 -1
- package/clis/tvmaze/utils.js +1 -1
- package/clis/twitter/bookmark-folder.js +1 -1
- package/clis/twitter/download.js +1 -1
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +2 -2
- package/clis/twitter/likes.js +1 -1
- package/clis/twitter/list-add-batch.js +1 -1
- package/clis/twitter/list-add-core.js +1 -1
- package/clis/twitter/list-add.js +1 -1
- package/clis/twitter/list-create.js +1 -1
- package/clis/twitter/list-delete.js +3 -3
- package/clis/twitter/list-remove-batch.js +1 -1
- package/clis/twitter/list-remove.js +1 -1
- package/clis/twitter/list-tweets.js +1 -1
- package/clis/twitter/profile.js +1 -1
- package/clis/twitter/search.js +1 -1
- package/clis/twitter/shared.js +1 -1
- package/clis/twitter/tweets.js +1 -1
- package/clis/uisdc/news.js +1 -1
- package/clis/v2ex/daily.js +2 -2
- package/clis/v2ex/me.js +2 -2
- package/clis/v2ex/notifications.js +1 -1
- package/clis/wikidata/utils.js +1 -1
- package/clis/wikipedia/page.js +2 -2
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/utils.js +1 -1
- package/clis/wordpress/login.js +1 -1
- package/clis/wordpress/publish.js +1 -1
- package/clis/wordpress/shared.js +3 -3
- package/clis/wordpress/sites.js +1 -0
- package/clis/wordpress/whoami.js +1 -1
- package/clis/woshipm/stats.js +1 -1
- package/clis/xiaoe/content.js +1 -1
- package/clis/xiaohongshu/draft-delete.js +1 -1
- package/clis/xiaohongshu/draft-open.js +1 -1
- package/clis/xiaohongshu/draft-utils.js +2 -2
- package/clis/xiaohongshu/publish-video.js +1 -1
- package/clis/xiaohongshu/publish.js +1 -1
- package/clis/xiaoyuzhou/auth.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/yollomi/background.js +1 -1
- package/clis/yollomi/edit.js +1 -1
- package/clis/yollomi/generate.js +1 -1
- package/clis/yollomi/try-on.js +1 -1
- package/clis/youtube/publish.js +1 -1
- package/clis/zhihu/answer-comments.js +1 -1
- package/clis/zhihu/answer-detail.js +1 -1
- package/clis/zhihu/collection.js +2 -2
- package/clis/zhihu/collections.js +1 -1
- package/clis/zhihu/question.js +1 -1
- package/clis/zhihu/search.js +1 -1
- package/clis/zhihu/target.js +1 -1
- package/clis/zhihu/user-arg.js +1 -1
- package/dist/src/adapter-shadow.js +1 -1
- package/dist/src/browser/analyze.js +1 -1
- package/dist/src/browser/article-extract.d.ts +1 -1
- package/dist/src/browser/base-page.js +3 -3
- package/dist/src/browser/cdp.js +1 -1
- package/dist/src/browser/daemon-client.d.ts +1 -1
- package/dist/src/browser/daemon-lifecycle.js +6 -6
- package/dist/src/browser/daemon-version.js +1 -1
- package/dist/src/browser/dom-snapshot.d.ts +1 -1
- package/dist/src/browser/dom-snapshot.js +2 -2
- package/dist/src/browser/errors.js +2 -2
- package/dist/src/browser/network-cache.js +1 -1
- package/dist/src/browser/profile.js +1 -1
- package/dist/src/browser/target-resolver.js +4 -4
- package/dist/src/browser/verify-fixture.js +1 -1
- package/dist/src/browser-session-lock.js +1 -1
- package/dist/src/build-manifest.d.ts +1 -1
- package/dist/src/build-manifest.js +1 -1
- package/dist/src/cli-argv-preprocess.d.ts +4 -4
- package/dist/src/cli-argv-preprocess.js +1 -1
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +19 -19
- package/dist/src/commanderAdapter.js +1 -1
- package/dist/src/commands/auth.js +2 -2
- package/dist/src/commands/daemon.d.ts +4 -4
- package/dist/src/commands/daemon.js +1 -1
- package/dist/src/completion-shared.js +12 -12
- package/dist/src/completion.d.ts +2 -2
- package/dist/src/constants.d.ts +1 -1
- package/dist/src/constants.js +1 -1
- package/dist/src/daemon-utils.d.ts +1 -1
- package/dist/src/daemon-utils.js +1 -1
- package/dist/src/daemon.d.ts +2 -2
- package/dist/src/daemon.js +1 -1
- package/dist/src/discovery.d.ts +7 -7
- package/dist/src/discovery.js +2 -2
- package/dist/src/doctor.d.ts +1 -1
- package/dist/src/doctor.js +6 -6
- package/dist/src/electron-apps.d.ts +1 -1
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/errors.d.ts +2 -2
- package/dist/src/errors.js +1 -1
- package/dist/src/execution.js +1 -1
- package/dist/src/external.d.ts +1 -1
- package/dist/src/external.js +1 -1
- package/dist/src/help.d.ts +1 -1
- package/dist/src/help.js +3 -3
- package/dist/src/hooks.d.ts +1 -1
- package/dist/src/launcher.js +2 -2
- package/dist/src/logger.d.ts +1 -1
- package/dist/src/main.d.ts +1 -1
- package/dist/src/main.js +1 -1
- package/dist/src/observation/artifact.js +2 -2
- package/dist/src/package-paths.js +1 -1
- package/dist/src/package-paths.test.d.ts +1 -0
- package/dist/src/plugin-manifest.d.ts +12 -3
- package/dist/src/plugin-manifest.js +1 -1
- package/dist/src/plugin-scaffold.d.ts +2 -2
- package/dist/src/plugin-scaffold.js +9 -9
- package/dist/src/plugin.d.ts +4 -4
- package/dist/src/plugin.js +10 -10
- package/dist/src/rate-limit.js +1 -1
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/runtime-detect.d.ts +1 -1
- package/dist/src/serialization.js +1 -1
- package/dist/src/skills.js +6 -6
- package/dist/src/user-runtime.d.ts +8 -0
- package/dist/src/user-runtime.js +1 -0
- package/dist/src/user-runtime.test.d.ts +1 -0
- package/dist/src/validate.js +1 -1
- package/dist/src/validate.test.d.ts +1 -1
- package/docs/guide/browser-bridge.md +23 -23
- package/docs/guide/electron-app-cli.md +3 -3
- package/docs/guide/exit-codes.md +3 -3
- package/docs/guide/extending-opencli.md +37 -37
- package/docs/guide/getting-started.md +19 -19
- package/docs/guide/installation.md +6 -6
- package/docs/guide/plugins.md +32 -32
- package/docs/guide/remote-orchestration.md +5 -5
- package/docs/guide/troubleshooting.md +9 -9
- package/package.json +3 -2
- package/scripts/fetch-adapters.js +6 -6
- package/scripts/postinstall.js +14 -14
- package/skills/opencli-adapter-author/SKILL.md +24 -24
- package/skills/opencli-adapter-author/references/adapter-template.md +13 -13
- package/skills/opencli-adapter-author/references/api-discovery.md +25 -25
- package/skills/opencli-adapter-author/references/coverage-matrix.md +3 -3
- package/skills/opencli-adapter-author/references/field-decode-playbook.md +10 -10
- package/skills/opencli-adapter-author/references/jsdom-fixture-pattern.md +2 -2
- package/skills/opencli-adapter-author/references/site-memory.md +11 -11
- package/skills/opencli-adapter-author/references/site-recon.md +7 -7
- package/skills/opencli-adapter-author/references/strategy-selection.md +3 -3
- package/skills/opencli-adapter-author/references/success-rate-pitfalls.md +4 -4
- package/skills/opencli-adapter-author/references/typed-errors.md +2 -2
- package/skills/opencli-autofix/SKILL.md +29 -29
- package/skills/opencli-browser/SKILL.md +55 -55
- package/skills/opencli-browser-sitemap/SKILL.md +7 -7
- package/skills/opencli-sitemap-author/SKILL.md +11 -11
- package/skills/opencli-sitemap-author/references/sitemap-schema.md +19 -19
- package/skills/opencli-usage/SKILL.md +43 -43
package/clis/v2ex/me.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{CommandExecutionError as z}from"@jackwener/opencli/errors";import{cli as B,Strategy as F}from"@jackwener/opencli/registry";B({site:"v2ex",name:"me",access:"read",description:"V2EX 获取个人资料 (余额/未读提醒)",domain:"www.v2ex.com",strategy:F.COOKIE,browser:!0,args:[],columns:["username","balance","unread_notifications","daily_reward_ready"],func:async(h)=>{if(!h)throw new z("Browser page required");if(process.env.OPENCLI_VERBOSE)console.error("[
|
|
1
|
+
import{CommandExecutionError as z}from"@jackwener/opencli/errors";import{cli as B,Strategy as F}from"@jackwener/opencli/registry";B({site:"v2ex",name:"me",access:"read",description:"V2EX 获取个人资料 (余额/未读提醒)",domain:"www.v2ex.com",strategy:F.COOKIE,browser:!0,args:[],columns:["username","balance","unread_notifications","daily_reward_ready"],func:async(h)=>{if(!h)throw new z("Browser page required");if(process.env.OPENCLI_VERBOSE)console.error("[ppcli:v2ex] Navigating to /");await h.goto("https://www.v2ex.com/");for(let q=0;q<5;q++){await new Promise((A)=>setTimeout(A,1500));if(!(await h.evaluate("() => document.title"))?.includes("Just a moment"))break;if(process.env.OPENCLI_VERBOSE)console.error("[ppcli:v2ex] Waiting for Cloudflare...")}const b=await h.evaluate(`
|
|
2
2
|
async () => {
|
|
3
3
|
let username = 'Unknown';
|
|
4
4
|
const navLinks = Array.from(document.querySelectorAll('a.top')).map(a => a.textContent?.trim());
|
|
@@ -55,4 +55,4 @@ import{CommandExecutionError as z}from"@jackwener/opencli/errors";import{cli as
|
|
|
55
55
|
daily_reward_ready: daily_reward_ready ? '是' : '否'
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
-
`);if(b.error){if(process.env.OPENCLI_VERBOSE){console.error(`[
|
|
58
|
+
`);if(b.error){if(process.env.OPENCLI_VERBOSE){console.error(`[ppcli:v2ex:debug] Page Title: ${b.debug_title}`);console.error(`[ppcli:v2ex:debug] Page Body: ${b.debug_body}`)}throw new z(b.error)}return[b]}});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{CommandExecutionError as q}from"@jackwener/opencli/errors";import{cli as F,Strategy as G}from"@jackwener/opencli/registry";F({site:"v2ex",name:"notifications",access:"read",description:"V2EX 获取提醒 (回复/由于)",domain:"www.v2ex.com",strategy:G.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:20,help:"Number of notifications"}],columns:["type","content","time"],func:async(b,z)=>{if(!b)throw new q("Browser page required");if(process.env.OPENCLI_VERBOSE)console.error("[
|
|
1
|
+
import{CommandExecutionError as q}from"@jackwener/opencli/errors";import{cli as F,Strategy as G}from"@jackwener/opencli/registry";F({site:"v2ex",name:"notifications",access:"read",description:"V2EX 获取提醒 (回复/由于)",domain:"www.v2ex.com",strategy:G.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:20,help:"Number of notifications"}],columns:["type","content","time"],func:async(b,z)=>{if(!b)throw new q("Browser page required");if(process.env.OPENCLI_VERBOSE)console.error("[ppcli:v2ex] Navigating to /notifications");await b.goto("https://www.v2ex.com/notifications");await new Promise((B)=>setTimeout(B,1500));const h=await b.evaluate(`
|
|
2
2
|
async () => {
|
|
3
3
|
const items = Array.from(document.querySelectorAll('#Main .box .cell[id^="n_"]'));
|
|
4
4
|
return items.map(item => {
|
package/clis/wikidata/utils.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ArgumentError as K,CommandExecutionError as L,EmptyResultError as O}from"@jackwener/opencli/errors";export const WIKIDATA_BASE="https://www.wikidata.org";const Q="opencli-wikidata-adapter/1.0 (+https://github.com/jackwener/
|
|
1
|
+
import{ArgumentError as K,CommandExecutionError as L,EmptyResultError as O}from"@jackwener/opencli/errors";export const WIKIDATA_BASE="https://www.wikidata.org";const Q="opencli-wikidata-adapter/1.0 (+https://github.com/jackwener/OpenCLI; mailto:ppcli@example.com)",S=/^[QPL]\d+$/;export function requireString(z,F){const B=String(z??"").trim();if(!B)throw new K(`wikidata ${F} cannot be empty`);return B}export function requireBoundedInt(z,F,B,H="limit"){const G=z??F,J=typeof G==="number"?G:Number(G);if(!Number.isInteger(J)||J<=0)throw new K(`wikidata ${H} must be a positive integer`);if(J>B)throw new K(`wikidata ${H} must be <= ${B}`);return J}export function requireEntityId(z){const F=String(z??"").trim().toUpperCase();if(!F)throw new K('wikidata entity id is required (e.g. "Q937")');const B=F.replace(/^HTTPS?:\/\/[^/]+\/WIKI\//i,"");if(!S.test(B))throw new K(`wikidata entity id "${z}" is not a valid Q/P/L identifier`,'Expected format: "Q<digits>" (item), "P<digits>" (property), or "L<digits>" (lexeme).');return B}export function requireLanguage(z,F="en"){const B=String(z??F).trim().toLowerCase();if(!/^[a-z]{2,3}(-[a-z]{2,8})?$/.test(B))throw new K(`wikidata language "${z}" is not a valid language code`,'Expected an ISO 639 language code such as "en", "fr", "zh", "zh-hans".');return B}export async function wikidataFetch(z,F){let B;try{B=await fetch(z,{headers:{"user-agent":Q,accept:"application/json"}})}catch(G){throw new L(`${F} request failed: ${G?.message??G}`,"Check that www.wikidata.org is reachable from this network.")}if(B.status===404)throw new O(F,`Wikidata returned 404 for ${z}.`);if(B.status===429)throw new L(`${F} returned HTTP 429 (rate limited)`,"Wikidata throttles anonymous traffic; back off and retry.");if(!B.ok)throw new L(`${F} returned HTTP ${B.status}`);let H;try{H=await B.json()}catch(G){throw new L(`${F} returned malformed JSON: ${G?.message??G}`)}return H}export function pickLocalised(z,F){if(!z||typeof z!=="object")return null;const B=z[F];if(B&&typeof B.value==="string"&&B.value.trim())return B.value;if(F!=="en"&&z.en&&typeof z.en.value==="string"&&z.en.value.trim())return z.en.value;return null}export function joinAliases(z,F,B=5){if(!z||typeof z!=="object")return"";let H=Array.isArray(z[F])?z[F]:[];if(H.length===0&&F!=="en"&&Array.isArray(z.en))H=z.en;const G=H.map((J)=>J&&typeof J.value==="string"?J.value:"").filter(Boolean);if(G.length===0)return"";if(G.length>B)return[...G.slice(0,B),`(+${G.length-B})`].join(", ");return G.join(", ")}
|
package/clis/wikipedia/page.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{cli as u,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as o,EmptyResultError as m}from"@jackwener/opencli/errors";u({site:"wikipedia",name:"page",access:"read",description:"Full plain-text extract of a Wikipedia article (optional paragraph cap).",domain:"wikipedia.org",strategy:w.PUBLIC,browser:!1,args:[{name:"title",positional:!0,required:!0,type:"string",help:'Article title (e.g. "Transformer (machine learning model)")'},{name:"lang",type:"string",default:"en",help:"Language code (en, zh, ja, de, ...)."},{name:"paragraphs",type:"int",default:0,help:"Cap to first N paragraphs (0 = full article)."}],columns:["title","description","pageId","paragraphs","extract","url"],func:async(i)=>{const l=String(i.title??"").trim();if(!l)throw new c("wikipedia page title cannot be empty");const
|
|
1
|
+
import{cli as u,Strategy as w}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as o,EmptyResultError as m}from"@jackwener/opencli/errors";u({site:"wikipedia",name:"page",access:"read",description:"Full plain-text extract of a Wikipedia article (optional paragraph cap).",domain:"wikipedia.org",strategy:w.PUBLIC,browser:!1,args:[{name:"title",positional:!0,required:!0,type:"string",help:'Article title (e.g. "Transformer (machine learning model)")'},{name:"lang",type:"string",default:"en",help:"Language code (en, zh, ja, de, ...)."},{name:"paragraphs",type:"int",default:0,help:"Cap to first N paragraphs (0 = full article)."}],columns:["title","description","pageId","paragraphs","extract","url"],func:async(i)=>{const l=String(i.title??"").trim();if(!l)throw new c("wikipedia page title cannot be empty");const s=String(i.lang??"en").trim().toLowerCase();if(!/^[a-z]{2,3}(?:-[a-z0-9]+)?$/.test(s))throw new c(`wikipedia lang must be a language code like en, zh, ja (got "${i.lang}")`);const n=Number(i.paragraphs??0);if(!Number.isInteger(n)||n<0)throw new c("paragraphs must be a non-negative integer (0 = full article)");const e=new URL(`https://${s}.wikipedia.org/w/api.php`);e.searchParams.set("action","query");e.searchParams.set("format","json");e.searchParams.set("formatversion","2");e.searchParams.set("prop","extracts|info|description");e.searchParams.set("inprop","url");e.searchParams.set("explaintext","1");e.searchParams.set("redirects","1");e.searchParams.set("titles",l);let p;try{p=await fetch(e,{headers:{"User-Agent":"ppcli/1.0 (+https://github.com/jackwener/OpenCLI)",Accept:"application/json"}})}catch(t){throw new o(`wikipedia page request failed: ${t?.message||t}`)}if(!p.ok)throw new o(`wikipedia page failed: HTTP ${p.status}`);let r;try{r=await p.json()}catch(t){throw new o(`wikipedia returned malformed JSON: ${t?.message||t}`)}if(r?.error)throw new o(`wikipedia API error: ${r.error.info||r.error.code}`);const a=(Array.isArray(r?.query?.pages)?r.query.pages:[])[0];if(!a||a.missing)throw new m("wikipedia page",`No article "${l}" on ${s}.wikipedia.org. Try \`ppcli wikipedia search\` first.`);const g=String(a.extract??"");if(!g.trim())throw new m("wikipedia page",`Article "${a.title}" exists but has no plain-text extract (likely a disambiguation/redirect page).`);const h=g.split(/\n{2,}/).map((t)=>t.trim()).filter(Boolean),d=n>0?h.slice(0,n):h;return[{title:a.title,description:a.description||"",pageId:a.pageid??null,paragraphs:d.length,extract:d.join(`
|
|
2
2
|
|
|
3
|
-
`),url:a.fullurl||`https://${
|
|
3
|
+
`),url:a.fullurl||`https://${s}.wikipedia.org/wiki/${encodeURIComponent(a.title.replace(/ /g,"_"))}`}]}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CliError as a}from"@jackwener/opencli/errors";import{cli as n,Strategy as o}from"@jackwener/opencli/registry";import{formatSummaryRow as l,wikiFetch as c}from"./utils.js";n({site:"wikipedia",name:"summary",access:"read",description:"Get Wikipedia article summary",strategy:o.PUBLIC,browser:!1,args:[{name:"title",positional:!0,required:!0,help:'Article title (e.g. "Transformer (machine learning model)")'},{name:"lang",default:"en",help:"Language code (e.g. en, zh, ja)"}],columns:["title","description","extract","url"],func:async(e)=>{const t=e.lang||"en",i=encodeURIComponent(e.title.replace(/ /g,"_")),r=await c(t,`/api/rest_v1/page/summary/${i}`);if(!r?.title)throw new a("NOT_FOUND",`Article "${e.title}" not found`,"Try searching first:
|
|
1
|
+
import{CliError as a}from"@jackwener/opencli/errors";import{cli as n,Strategy as o}from"@jackwener/opencli/registry";import{formatSummaryRow as l,wikiFetch as c}from"./utils.js";n({site:"wikipedia",name:"summary",access:"read",description:"Get Wikipedia article summary",strategy:o.PUBLIC,browser:!1,args:[{name:"title",positional:!0,required:!0,help:'Article title (e.g. "Transformer (machine learning model)")'},{name:"lang",default:"en",help:"Language code (e.g. en, zh, ja)"}],columns:["title","description","extract","url"],func:async(e)=>{const t=e.lang||"en",i=encodeURIComponent(e.title.replace(/ /g,"_")),r=await c(t,`/api/rest_v1/page/summary/${i}`);if(!r?.title)throw new a("NOT_FOUND",`Article "${e.title}" not found`,"Try searching first: ppcli wikipedia search <keyword>");return[l(r,t)]}});
|
package/clis/wikipedia/utils.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CliError as x}from"@jackwener/opencli/errors";export const EXTRACT_MAX_LEN=300,DESC_MAX_LEN=80;export async function wikiFetch(b,f){const v=`https://${b}.wikipedia.org${f}`,q=await fetch(v,{headers:{"User-Agent":"
|
|
1
|
+
import{CliError as x}from"@jackwener/opencli/errors";export const EXTRACT_MAX_LEN=300,DESC_MAX_LEN=80;export async function wikiFetch(b,f){const v=`https://${b}.wikipedia.org${f}`,q=await fetch(v,{headers:{"User-Agent":"ppcli/1.0 (https://github.com/jackwener/OpenCLI)"}});if(!q.ok)throw new x("FETCH_ERROR",`Wikipedia API HTTP ${q.status}`,"Check your title or search term");return q.json()}export function formatSummaryRow(b,f){return{title:b.title,description:b.description??"-",extract:(b.extract??"").slice(0,EXTRACT_MAX_LEN),url:b.content_urls?.desktop?.page??`https://${f}.wikipedia.org`}}
|
package/clis/wordpress/login.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{TimeoutError as $}from"@jackwener/opencli/errors";import{resolveBaseUrl as h,readCookieLogin as x,gotoWpAdmin as v,WP_ADMIN as B,WP_LOGIN as G}from"./shared.js";const Q=300,R=3000;async function z(f){if(typeof f?.closeWindow==="function")try{await f.closeWindow()}catch{}}Y({site:"wordpress",name:"login",access:"write",description:"打开自建 WordPress 站点的登录页并等待浏览器会话完成登录(加站模式:站点网址取自连接里的 WORDPRESS_BASE_URL,或用 --base-url)。",domain:"wordpress.org",strategy:Z.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"base-url",help:"站点根网址(默认读连接里的 WORDPRESS_BASE_URL)"},{name:"timeout",type:"int",default:Q,help:"等待用户完成登录的最长秒数"}],columns:["status","logged_in","site"],func:async(f,H)=>{const q=h(H);if(await x(f,q)){await z(f);return[{status:"already_logged_in",logged_in:!0,site:q}]}if(!(await v(f,q,B)).onLogin&&await x(f,q)){await z(f);return[{status:"already_logged_in",logged_in:!0,site:q}]}await f.goto(q+G);const J=Number(H.timeout??Q),K=Date.now()+J*1000;while(Date.now()<K){const X=K-Date.now();await f.wait({time:Math.min(R/1000,Math.max(0.2,X/1000))});if(await x(f,q)){await z(f);return[{status:"login_complete",logged_in:!0,site:q}]}}throw new $("wordpress login",J,`请在打开的窗口里完成 ${q} 的登录后重试。`)}});
|
|
1
|
+
import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{TimeoutError as $}from"@jackwener/opencli/errors";import{resolveBaseUrl as h,readCookieLogin as x,gotoWpAdmin as v,WP_ADMIN as B,WP_LOGIN as G}from"./shared.js";const Q=300,R=3000;async function z(f){if(typeof f?.closeWindow==="function")try{await f.closeWindow()}catch{}}Y({site:"wordpress",name:"login",access:"write",description:"打开自建 WordPress 站点的登录页并等待浏览器会话完成登录(加站模式:站点网址取自连接里的 WORDPRESS_BASE_URL,或用 --base-url)。",domain:"wordpress.org",strategy:Z.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"site",help:"连接名称或站点网址;省略时使用默认站点"},{name:"base-url",help:"站点根网址(默认读连接里的 WORDPRESS_BASE_URL)"},{name:"timeout",type:"int",default:Q,help:"等待用户完成登录的最长秒数"}],columns:["status","logged_in","site"],func:async(f,H)=>{const q=h(H);if(await x(f,q)){await z(f);return[{status:"already_logged_in",logged_in:!0,site:q}]}if(!(await v(f,q,B)).onLogin&&await x(f,q)){await z(f);return[{status:"already_logged_in",logged_in:!0,site:q}]}await f.goto(q+G);const J=Number(H.timeout??Q),K=Date.now()+J*1000;while(Date.now()<K){const X=K-Date.now();await f.wait({time:Math.min(R/1000,Math.max(0.2,X/1000))});if(await x(f,q)){await z(f);return[{status:"login_complete",logged_in:!0,site:q}]}}throw new $("wordpress login",J,`请在打开的窗口里完成 ${q} 的登录后重试。`)}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as F,Strategy as H}from"@jackwener/opencli/registry";import{CliError as G,AuthRequiredError as M,CommandExecutionError as O}from"@jackwener/opencli/errors";import{readFileSync as L}from"node:fs";import{normalizeContent as N}from"../_shared/article/format.js";import{assertLiteralContent as f}from"../_shared/content-guard.js";import{resolveBaseUrl as k,getAppPasswordCreds as q,basicAuthHeader as R,wpRequest as W,gotoWpAdmin as I,buildPublishJs as T,WP_ADMIN_POST_NEW as j}from"./shared.js";const U=["draft","publish","pending","private"];function V(K){return String(K).split(",").map((Q)=>Number(Q.trim())).filter((Q)=>Number.isFinite(Q))}function w(K,Q){const Z={title:String(K.title),content:Q,status:String(K.status)};if(K.excerpt)Z.excerpt=String(K.excerpt);if(K.slug)Z.slug=String(K.slug);if(K.categories)Z.categories=V(K.categories);if(K.tags)Z.tags=V(K.tags);return Z}F({site:"wordpress",name:"publish",access:"write",description:"发布一篇文章到自建 WordPress 站点。默认走登录态 cookie(零配置:只需在「连接」里填站点网址并在浏览器里登录),通过同源页面 fetch + REST nonce 调 POST /wp/v2/posts。Markdown 正文自动转 HTML。为安全默认存草稿,--status publish 才真发。分类/标签是 WordPress 的 term ID(数字),不是名字。",domain:"wordpress.org",strategy:H.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[{name:"title",required:!0,help:"文章标题"},{name:"content",help:"正文(Markdown 或 HTML),或用 --file"},{name:"file",help:"从本地文件读正文,替代 --content"},{name:"markup",default:"auto",choices:["auto","markdown","html"],help:"正文格式:auto 自动识别(默认) / markdown / html"},{name:"status",default:"draft",choices:U,help:"文章状态(默认 draft)"},{name:"excerpt",help:"摘要(可选)"},{name:"slug",help:"URL 别名 slug(可选)"},{name:"categories",help:"分类 term ID,逗号分隔(数字,非名字)"},{name:"tags",help:"标签 term ID,逗号分隔(数字,非名字)"},{name:"base-url",help:"
|
|
1
|
+
import{cli as F,Strategy as H}from"@jackwener/opencli/registry";import{CliError as G,AuthRequiredError as M,CommandExecutionError as O}from"@jackwener/opencli/errors";import{readFileSync as L}from"node:fs";import{normalizeContent as N}from"../_shared/article/format.js";import{assertLiteralContent as f}from"../_shared/content-guard.js";import{resolveBaseUrl as k,getAppPasswordCreds as q,basicAuthHeader as R,wpRequest as W,gotoWpAdmin as I,buildPublishJs as T,WP_ADMIN_POST_NEW as j}from"./shared.js";const U=["draft","publish","pending","private"];function V(K){return String(K).split(",").map((Q)=>Number(Q.trim())).filter((Q)=>Number.isFinite(Q))}function w(K,Q){const Z={title:String(K.title),content:Q,status:String(K.status)};if(K.excerpt)Z.excerpt=String(K.excerpt);if(K.slug)Z.slug=String(K.slug);if(K.categories)Z.categories=V(K.categories);if(K.tags)Z.tags=V(K.tags);return Z}F({site:"wordpress",name:"publish",access:"write",description:"发布一篇文章到自建 WordPress 站点。默认走登录态 cookie(零配置:只需在「连接」里填站点网址并在浏览器里登录),通过同源页面 fetch + REST nonce 调 POST /wp/v2/posts。Markdown 正文自动转 HTML。为安全默认存草稿,--status publish 才真发。分类/标签是 WordPress 的 term ID(数字),不是名字。",domain:"wordpress.org",strategy:H.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[{name:"site",help:"连接名称或站点网址;省略时使用默认站点"},{name:"title",required:!0,help:"文章标题"},{name:"content",help:"正文(Markdown 或 HTML),或用 --file"},{name:"file",help:"从本地文件读正文,替代 --content"},{name:"markup",default:"auto",choices:["auto","markdown","html"],help:"正文格式:auto 自动识别(默认) / markdown / html"},{name:"status",default:"draft",choices:U,help:"文章状态(默认 draft)"},{name:"excerpt",help:"摘要(可选)"},{name:"slug",help:"URL 别名 slug(可选)"},{name:"categories",help:"分类 term ID,逗号分隔(数字,非名字)"},{name:"tags",help:"标签 term ID,逗号分隔(数字,非名字)"},{name:"base-url",help:"兼容旧版:覆盖站点根网址(推荐改用 --site)"}],columns:["id","status","title","url"],func:async(K,Q)=>{const Z=k(Q);let $=typeof Q.content==="string"?Q.content:"";if(Q.file)try{$=L(String(Q.file),"utf8")}catch(Y){throw new G("CONFIG",`Cannot read --file ${Q.file}: ${Y?.message??Y}`,"Pass a readable UTF-8 text file.")}else f($,{fileFlag:"--file",label:"正文"});if(!$.trim())throw new G("CONFIG","Post body is empty",'Pass --content "..." or --file <path>.');const x=N($,{format:Q.markup}).html,v=w(Q,x),J=q();if(J){const Y=await W(Z,"/wp/v2/posts",{method:"POST",body:v,authorization:R(J),label:"WordPress publish"});return[{id:Y?.id??"",status:Y?.status??"",title:Y?.title?.rendered??v.title,url:Y?.link??""}]}if(!K)throw new O("WordPress publish 需要浏览器会话(默认走登录态;或配置 WORDPRESS_APP_PASSWORD 走 Basic auth)。");if((await I(K,Z,j)).onLogin)throw new M("wordpress",`未登录 ${Z}(打开 wp-admin 被重定向到登录页)。请先在浏览器里登录该站点(可用 wordpress login)。`);const X=await K.evaluate(T(v));if(!X||X.ok===!1){const Y=X?.stage||"publish",z=X?.httpStatus!=null?` (HTTP ${X.httpStatus})`:"",D=X?.bodyText?`;接口原始响应:${String(X.bodyText).slice(0,500)}`:"",B=X?.message||"发布失败";if(Y==="auth")throw new M("wordpress",`WordPress publish 未授权${z}:${B}`);if(Y==="not_found")throw new G("NOT_FOUND",`WordPress publish:REST 接口不可达${z}:${B}`,"部分主机/安全插件会禁用 /wp-json(REST API)。请确认站点开启了 REST API。");throw new O(`[${Y}] WordPress publish 失败${z}:${B}${D}`)}return[{id:X.id!=null?String(X.id):"",status:X.status??"",title:X.title??v.title,url:X.link??""}]}});
|
package/clis/wordpress/shared.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import{CliError as
|
|
1
|
+
import{CliError as K}from"@jackwener/opencli/errors";import{loadSiteConnections as R,normalizeSiteUrl as T,resolveSiteConnection as j}from"../_shared/self-hosted-sites.js";export const WP_LOGIN_COOKIE_PREFIX="wordpress_logged_in_",WP_ADMIN_POST_NEW="/wp-admin/post-new.php",WP_ADMIN="/wp-admin/",WP_LOGIN="/wp-login.php";export function normalizeBaseUrl(q,D){return T(q,{label:D})}export function getWordpressBaseUrl(q=process.env){return normalizeBaseUrl(q.WORDPRESS_BASE_URL,"WORDPRESS_BASE_URL")}export function resolveBaseUrl(q={},D=process.env){return j("wordpress",q,{selector:"site",legacySelectors:["base-url","baseUrl","base_url"],envUrl:D.WORDPRESS_BASE_URL}).url}export function configuredWordpressSites(q=process.env){const D=R("wordpress").sites;if(D.length>0)return D;if(!q.WORDPRESS_BASE_URL)return[];return[{name:"",url:normalizeBaseUrl(q.WORDPRESS_BASE_URL,"WORDPRESS_BASE_URL")}]}export function getAppPasswordCreds(q=process.env){const D=q.WORDPRESS_USER?.trim(),M=q.WORDPRESS_APP_PASSWORD?.replace(/\s+/g,"");if(!D||!M)return null;return{user:D,appPassword:M}}export function basicAuthHeader(q){return`Basic ${Buffer.from(`${q.user}:${q.appPassword}`,"utf8").toString("base64")}`}export async function wpRequest(q,D,M={}){const V=D.startsWith("/")?D:`/${D}`,Y=`${q}/wp-json${V}`,Z=M.label||`WordPress ${M.method||"GET"} ${V}`;let Q;try{Q=await fetch(Y,{method:M.method||"GET",headers:{...M.authorization?{Authorization:M.authorization}:{},Accept:"application/json",...M.body!==void 0?{"Content-Type":"application/json"}:{},...M.headers||{}},body:M.body!==void 0?JSON.stringify(M.body):void 0})}catch(H){throw new K("FETCH_ERROR",`${Z} failed: ${H?.message??H}`,"Check the site URL is reachable and the REST API (/wp-json) is enabled.")}const $=await Q.text();let F=null;try{F=$?JSON.parse($):null}catch{F=null}if(Q.status===401||Q.status===403)throw new K("AUTH_REQUIRED",`${Z} returned HTTP ${Q.status} (unauthorized)`,"Check WORDPRESS_USER / WORDPRESS_APP_PASSWORD — the Application Password must belong to that user and not be revoked.");if(Q.status===404)throw new K("NOT_FOUND",`${Z} returned HTTP 404`,"Check the site URL and that the WordPress REST API is enabled (some hosts/security plugins disable /wp-json).");if(!Q.ok){const H=F?.message?`: ${F.message}`:Q.statusText?`: ${Q.statusText}`:"";throw new K("HTTP_ERROR",`${Z} returned HTTP ${Q.status}${H}`)}return F}export async function readCookieLogin(q,D){if(typeof q?.getCookies!=="function")return!1;const M=await q.getCookies({url:D});return Array.isArray(M)&&M.some((V)=>V&&V.value&&typeof V.name==="string"&&V.name.startsWith(WP_LOGIN_COOKIE_PREFIX))}export async function gotoWpAdmin(q,D,M){const V=D+M;let Y={};for(let Z=0;Z<6;Z++){await q.goto(V);await q.wait({time:1});Y=await q.evaluate(`(() => {
|
|
2
2
|
var href = ''; try { href = location.href; } catch (e) {}
|
|
3
3
|
var cookieOk = true; try { void document.cookie; } catch (e) { cookieOk = false; }
|
|
4
4
|
var hasNonce = !!(window.wpApiSettings && window.wpApiSettings.nonce);
|
|
5
5
|
return { href: href, cookieOk: cookieOk, hasNonce: hasNonce };
|
|
6
|
-
})()`)||{};const
|
|
6
|
+
})()`)||{};const $=Y.href||"";if(/wp-login\.php/i.test($))return{onLogin:!0,href:$};if(Y.cookieOk&&(Y.hasNonce||/\/wp-admin\//i.test($)))return{onLogin:!1,href:$};await q.wait({time:1})}return{onLogin:!1,href:Y.href||"",uncertain:!0}}export function buildPublishJs(q){return`(async () => {
|
|
7
7
|
var s = (window.wpApiSettings || {});
|
|
8
8
|
var nonce = s.nonce || "";
|
|
9
9
|
var root = s.root || (location.origin + "/wp-json/");
|
|
@@ -19,7 +19,7 @@ import{CliError as $}from"@jackwener/opencli/errors";export const WP_LOGIN_COOKI
|
|
|
19
19
|
`+` if (!nonce) return { ok: false, stage: "nonce", message: "无法从 wp-admin 页面取到 REST nonce(window.wpApiSettings.nonce 缺失)。可能未登录、REST API 被禁用,或该页面未加载区块编辑器。" };
|
|
20
20
|
`+` if (!/\\/$/.test(root)) root += "/";
|
|
21
21
|
var url = root + "wp/v2/posts";
|
|
22
|
-
var payload = `+JSON.stringify(
|
|
22
|
+
var payload = `+JSON.stringify(q)+`;
|
|
23
23
|
try {
|
|
24
24
|
var r = await fetch(url, { method: "POST", headers: { "X-WP-Nonce": nonce, "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(payload) });
|
|
25
25
|
var text = await r.text();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as m}from"@jackwener/opencli/registry";import{connectionCommandDefinitions as n}from"../_shared/self-hosted-sites.js";for(const o of n("wordpress"))m(o);
|
package/clis/wordpress/whoami.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{resolveBaseUrl as f,configuredWordpressSites as v,getAppPasswordCreds as X,basicAuthHeader as x,wpRequest as B,readCookieLogin as Y,gotoWpAdmin as D,buildWhoamiJs as E,WP_ADMIN_POST_NEW as H}from"./shared.js";async function I(F){if(X())return{logged_in:!0};for(const Q of v())if(await Y(F,Q.url))return{logged_in:!0};return{logged_in:!1}}Z({site:"wordpress",name:"whoami",access:"read",description:"检测自建 WordPress 站点的登录状态(返回布尔)。默认走登录态 cookie / 页面上下文 GET /wp/v2/users/me?context=edit;配置了 WORDPRESS_APP_PASSWORD 时走 Basic auth。",domain:"wordpress.org",strategy:$.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"site",help:"连接名称或站点网址;省略时使用默认站点"},{name:"base-url",help:"兼容旧版:站点根网址(推荐改用 --site)"}],columns:["logged_in","user_id","username","site"],authStatus:{quickCheck:I},func:async(F,Q)=>{const z=f(Q),V=X();if(V)try{const G=await B(z,"/wp/v2/users/me?context=edit",{authorization:x(V),label:"WordPress whoami"});return[{logged_in:!!(G&&G.id!=null),user_id:G?.id!=null?String(G.id):"",username:G?.name??G?.slug??"",site:z}]}catch{return[{logged_in:!1,user_id:"",username:"",site:z}]}if(!F)return[{logged_in:!1,user_id:"",username:"",site:z}];if((await D(F,z,H)).onLogin)return[{logged_in:!1,user_id:"",username:"",site:z}];const K=await F.evaluate(E());if(K&&K.loggedIn)return[{logged_in:!0,user_id:K.id||"",username:K.name||K.slug||"",site:z}];return[{logged_in:await Y(F,z),user_id:"",username:"",site:z}]}});
|
package/clis/woshipm/stats.js
CHANGED
|
@@ -13,7 +13,7 @@ import{cli as Q,Strategy as V}from"@jackwener/opencli/registry";import{CommandEx
|
|
|
13
13
|
if (loose) nonce = loose[1];
|
|
14
14
|
}
|
|
15
15
|
if (!nonce) {
|
|
16
|
-
return { err: '页面里找不到 X-WP-Nonce(PURE.nonce),可能未登录人人都是产品经理,请先
|
|
16
|
+
return { err: '页面里找不到 X-WP-Nonce(PURE.nonce),可能未登录人人都是产品经理,请先 ppcli woshipm login' };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// ── 分页拉「全部文章」列表(post_status=all,与页面自发请求一致)──
|
package/clis/xiaoe/content.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{ArgumentError as W,CommandExecutionError as q,EmptyResultError as Y}from"@jackwener/opencli/errors";export const CONTENT_SELECTORS=[".rich-text-wrap",".content-wrap",".article-content",".text-content",".course-detail",".detail-content",'[class*="richtext"]','[class*="rich-text"]',".ql-editor"],CONTENT_MIN_LENGTH=50;export function pickContentText(B,K,J=CONTENT_MIN_LENGTH){for(const F of K){const Q=B.querySelector(F);if(!Q)continue;const V=(Q.innerText||Q.textContent||"").trim();if(V.length>J)return V}const z=B.querySelector("main")||B.querySelector("#app")||B.body;if(!z)return"";return(z.innerText||z.textContent||"").trim()}export function countXiaoeImages(B){let K=0;const J=B.querySelectorAll("img");for(let z=0;z<J.length;z+=1){const F=J[z].getAttribute("src")||J[z].src||"";if(!F)continue;if(F.startsWith("data:"))continue;if(!F.includes("xiaoe"))continue;K+=1}return K}export function requireXiaoePageUrl(B,K){const J=typeof B==="string"?B.trim():"";if(!J)throw new W("url is required (positional)");let z;try{z=new URL(J)}catch{throw new W(`invalid xiaoe URL: ${J}`,`Example:
|
|
1
|
+
import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{ArgumentError as W,CommandExecutionError as q,EmptyResultError as Y}from"@jackwener/opencli/errors";export const CONTENT_SELECTORS=[".rich-text-wrap",".content-wrap",".article-content",".text-content",".course-detail",".detail-content",'[class*="richtext"]','[class*="rich-text"]',".ql-editor"],CONTENT_MIN_LENGTH=50;export function pickContentText(B,K,J=CONTENT_MIN_LENGTH){for(const F of K){const Q=B.querySelector(F);if(!Q)continue;const V=(Q.innerText||Q.textContent||"").trim();if(V.length>J)return V}const z=B.querySelector("main")||B.querySelector("#app")||B.body;if(!z)return"";return(z.innerText||z.textContent||"").trim()}export function countXiaoeImages(B){let K=0;const J=B.querySelectorAll("img");for(let z=0;z<J.length;z+=1){const F=J[z].getAttribute("src")||J[z].src||"";if(!F)continue;if(F.startsWith("data:"))continue;if(!F.includes("xiaoe"))continue;K+=1}return K}export function requireXiaoePageUrl(B,K){const J=typeof B==="string"?B.trim():"";if(!J)throw new W("url is required (positional)");let z;try{z=new URL(J)}catch{throw new W(`invalid xiaoe URL: ${J}`,`Example: ppcli xiaoe ${K} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`)}if(z.protocol!=="https:")throw new W(`xiaoe URL must use https (got ${z.protocol.replace(":","")})`,`Example: ppcli xiaoe ${K} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`);const F=z.hostname.toLowerCase();if(F!=="h5.xet.citv.cn"&&!F.endsWith(".h5.xet.citv.cn"))throw new W(`url must be on h5.xet.citv.cn or a shop subdomain (got ${z.hostname})`,`Example: ppcli xiaoe ${K} https://appxxxx.h5.xet.citv.cn/p/course/ecourse/v_xxxxx`);return z.toString()}export function buildContentScript(){return`
|
|
2
2
|
(() => {
|
|
3
3
|
${pickContentText.toString()}
|
|
4
4
|
${countXiaoeImages.toString()}
|
|
@@ -34,4 +34,4 @@ import{CommandExecutionError as i}from"@jackwener/opencli/errors";import{cli as
|
|
|
34
34
|
return { ok: false, error: String(error && error.message || error) };
|
|
35
35
|
}
|
|
36
36
|
})()
|
|
37
|
-
`}f({site:"xiaohongshu",name:"draft-delete",access:"write",description:"删除一条小红书本地草稿",domain:"creator.xiaohongshu.com",strategy:u.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"id",positional:!0,required:!0,help:"Draft id returned by `
|
|
37
|
+
`}f({site:"xiaohongshu",name:"draft-delete",access:"write",description:"删除一条小红书本地草稿",domain:"creator.xiaohongshu.com",strategy:u.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"id",positional:!0,required:!0,help:"Draft id returned by `ppcli xiaohongshu drafts`"},{name:"type",default:"image",help:"Draft type: image, video, article, audio"},{name:"execute",type:"bool",default:!1,help:"Actually delete the local draft. Default is dry-run verification only."}],columns:["status","id","type","message"],func:async(e,t)=>{const a=g(t.id),r=h(t.type),d=t.execute===!0;await p(e);const c=await b(e,r),o=x(c,a);if(!o)throw w(a,r,"xiaohongshu/draft-delete");const n=D(o.row,o.key,r,1);if(!d)return[{status:"dry-run",id:n.id,type:r,message:"Draft exists. Re-run with --execute to delete."}];const l=m[r],s=q(await e.evaluate(S(l,o.key)));if(!s?.ok)throw new i(s?.error||`Failed to delete draft ${n.id}`);if(!s.deleted)throw new i(`Draft ${n.id} still exists after delete`);return[{status:"deleted",id:n.id,type:r,message:"Draft deleted."}]}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as d,Strategy as s}from"@jackwener/opencli/registry";import{draftNotFound as f,ensureDraftDbPage as
|
|
1
|
+
import{cli as d,Strategy as s}from"@jackwener/opencli/registry";import{draftNotFound as f,ensureDraftDbPage as p,findDraftEntry as c,normalizeDraftId as m,normalizeDraftRecord as u,normalizeDraftType as l,readDraftEntries as y}from"./draft-utils.js";d({site:"xiaohongshu",name:"draft-open",access:"read",description:"读取一条小红书本地草稿详情",domain:"creator.xiaohongshu.com",strategy:s.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"id",positional:!0,required:!0,help:"Draft id returned by `ppcli xiaohongshu drafts`"},{name:"type",default:"image",help:"Draft type: image, video, article, audio"}],columns:["id","type","title","updated_at","images","content"],func:async(r,i)=>{const o=m(i.id),e=l(i.type);await p(r);const n=await y(r,e),a=c(n,o);if(!a)throw f(o,e,"xiaohongshu/draft-open");const t=u(a.row,a.key,e,1,{contentLimit:500});return[{id:t.id,type:t.type,title:t.title,updated_at:t.updated_at,images:t.images,content:t.text_preview}]}});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{ArgumentError as a,CommandExecutionError as c,EmptyResultError as
|
|
1
|
+
import{ArgumentError as a,CommandExecutionError as c,EmptyResultError as g}from"@jackwener/opencli/errors";export const PUBLISH_URL="https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image",DRAFT_DB_NAME="draft-database-v1",STORE_NAME_MAP={image:"image-draft",video:"video-draft",article:"article-draft",audio:"audio-draft"};export function unwrapBrowserResult(e){if(e&&typeof e==="object"&&typeof e.session==="string"&&Object.prototype.hasOwnProperty.call(e,"data"))return e.data;return e}export function normalizeDraftType(e,{allowAll:t=!1}={}){const r=String(e??"image").trim().toLowerCase();if(t&&r==="all")return r;if(!STORE_NAME_MAP[r]){const o=t?"image, video, article, audio, all":Object.keys(STORE_NAME_MAP).join(", ");throw new a(`Unsupported draft type "${r}". Expected one of: ${o}`)}return r}export function normalizeDraftId(e){const t=String(e??"").trim();if(!t)throw new a("Draft id is required");return t}export function encodeDraftKey(e){const t=typeof e;if(t==="string")return`s:${e}`;if(t==="number")return`n:${String(e)}`;if(t==="boolean")return`b:${String(e)}`;try{return`j:${encodeURIComponent(JSON.stringify(e))}`}catch{return`s:${String(e)}`}}export function findDraftEntry(e,t){return e.find((r)=>encodeDraftKey(r?.key)===t)||null}export function normalizeDraftRecord(e,t,r,o,{contentLimit:d=80}={}){const n=e?.content??{},l=n?.contextStore?.liveContext??{},i=n?.draftStore??{},f=i?.title||l?.title||e?.title||e?.noteTitle||"",u=n?.editorContent?.text||n?.editorContent?.plainText||"",s=e?.updatedAt||e?.updateTime||e?.mtime||n?.updateTime||"",p=i?.imgList?.length||n?.noteImageConfig?.items?.length||n?.noteImageConfig?.imageList?.length||e?.images?.length||e?.imageList?.length||0;return{rank:o,id:encodeDraftKey(t),type:r,title:f||"(untitled)",updated_at:s?String(s):"",images:p,text_preview:String(u||"").replace(/\s+/g," ").trim().slice(0,d)}}export async function ensureDraftDbPage(e){await e.goto(PUBLISH_URL);await e.wait({time:2})}function m(e){return`
|
|
2
2
|
(async () => {
|
|
3
3
|
const openDb = () => new Promise((resolve, reject) => {
|
|
4
4
|
const req = indexedDB.open(${JSON.stringify(DRAFT_DB_NAME)});
|
|
@@ -33,4 +33,4 @@ import{ArgumentError as a,CommandExecutionError as c,EmptyResultError as p}from"
|
|
|
33
33
|
return { ok: false, error: String(error && error.message || error) };
|
|
34
34
|
}
|
|
35
35
|
})()
|
|
36
|
-
`}export async function readDraftEntries(e,t){const r=STORE_NAME_MAP[t],o=unwrapBrowserResult(await e.evaluate(m(r)));if(!o?.ok)throw new c(o?.error||`Failed to read ${t} drafts`);if(!Array.isArray(o.entries))throw new c(`Malformed ${t} draft payload`);return o.entries}export function draftNotFound(e,t,r){return new
|
|
36
|
+
`}export async function readDraftEntries(e,t){const r=STORE_NAME_MAP[t],o=unwrapBrowserResult(await e.evaluate(m(r)));if(!o?.ok)throw new c(o?.error||`Failed to read ${t} drafts`);if(!Array.isArray(o.entries))throw new c(`Malformed ${t} draft payload`);return o.entries}export function draftNotFound(e,t,r){return new g(r,`Draft ${e} was not found in ${t} drafts. Run ppcli xiaohongshu drafts --type ${t} to list current ids.`)}
|
|
@@ -73,7 +73,7 @@ import*as X from"node:fs";import*as Y from"node:path";import{cli as h,Strategy a
|
|
|
73
73
|
}
|
|
74
74
|
return { ok: false, via: 'none', hosts: hosts.length, lastMethodError };
|
|
75
75
|
})(${JSON.stringify({isDraftMode:z,publishNames:_,draftNames:u,labels:G})})
|
|
76
|
-
`)}h({site:"xiaohongshu",name:"publish-video",access:"write",description:"小红书发布视频笔记(creator center UI 自动化)",domain:"creator.xiaohongshu.com",strategy:D.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不展开 @文件 引用;长正文用 --file)"},{name:"file",required:!1,help:"从本机文件读取正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题(≤20字)"},{name:"video",required:!0,help:"视频文件路径 (mp4/mov/mkv/...)"},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号,最多10个"},{name:"draft",type:"bool",default:!1,help:"暂存为草稿,不直接发布"}],columns:["status","detail"],func:async(q,z)=>{if(!q)throw Error("Browser page required");const G=String(z.title??"").trim(),K=z.file?String(z.file).trim():"";let J=String(z.content??"").trim();if(K){if(J)throw new $("正文位置参数和 --file 只能二选一");const Z=Y.resolve(K);if(!X.existsSync(Z))throw new $(`--file 指向的文件不存在: ${Z}`);J=X.readFileSync(Z,"utf-8").trim();if(!J)throw new $(`--file 指向的文件内容为空: ${Z}`)}const Q=z.topics?String(z.topics).split(",").map((Z)=>Z.trim().replace(/^#/,"")).filter(Boolean):[],v=Boolean(z.draft);if(!G)throw new $("--title 必填");if(G.length>k)throw new $(`标题 ${G.length} 字,需 ≤ ${k} 字`);if(!J)throw new $("缺少正文:给位置参数 <content>,或用 --file <本机文件路径>");C(J,{fileFlag:"--file"});if(J.length>x)throw new $(`正文 ${J.length} 字,需 ≤ ${x} 字`);if(!z.video)throw new $("--video 必填");if(Q.length>R)throw new $(`话题过多: ${Q.length}(最多 ${R} 个)`);const N=T(String(z.video));await q.goto(O);let y="";for(let Z=0;Z<30;Z++){await q.wait({time:0.5});y=await q.evaluate("() => location.href");if(y.includes("creator.xiaohongshu.com"))break}if(!y.includes("creator.xiaohongshu.com")){await q.screenshot({path:"/tmp/xhs_video_redirect_debug.png"});throw new j(`被重定向离开创作者中心(当前 ${y})——登录态可能已过期。`+"用
|
|
76
|
+
`)}h({site:"xiaohongshu",name:"publish-video",access:"write",description:"小红书发布视频笔记(creator center UI 自动化)",domain:"creator.xiaohongshu.com",strategy:D.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不展开 @文件 引用;长正文用 --file)"},{name:"file",required:!1,help:"从本机文件读取正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题(≤20字)"},{name:"video",required:!0,help:"视频文件路径 (mp4/mov/mkv/...)"},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号,最多10个"},{name:"draft",type:"bool",default:!1,help:"暂存为草稿,不直接发布"}],columns:["status","detail"],func:async(q,z)=>{if(!q)throw Error("Browser page required");const G=String(z.title??"").trim(),K=z.file?String(z.file).trim():"";let J=String(z.content??"").trim();if(K){if(J)throw new $("正文位置参数和 --file 只能二选一");const Z=Y.resolve(K);if(!X.existsSync(Z))throw new $(`--file 指向的文件不存在: ${Z}`);J=X.readFileSync(Z,"utf-8").trim();if(!J)throw new $(`--file 指向的文件内容为空: ${Z}`)}const Q=z.topics?String(z.topics).split(",").map((Z)=>Z.trim().replace(/^#/,"")).filter(Boolean):[],v=Boolean(z.draft);if(!G)throw new $("--title 必填");if(G.length>k)throw new $(`标题 ${G.length} 字,需 ≤ ${k} 字`);if(!J)throw new $("缺少正文:给位置参数 <content>,或用 --file <本机文件路径>");C(J,{fileFlag:"--file"});if(J.length>x)throw new $(`正文 ${J.length} 字,需 ≤ ${x} 字`);if(!z.video)throw new $("--video 必填");if(Q.length>R)throw new $(`话题过多: ${Q.length}(最多 ${R} 个)`);const N=T(String(z.video));await q.goto(O);let y="";for(let Z=0;Z<30;Z++){await q.wait({time:0.5});y=await q.evaluate("() => location.href");if(y.includes("creator.xiaohongshu.com"))break}if(!y.includes("creator.xiaohongshu.com")){await q.screenshot({path:"/tmp/xhs_video_redirect_debug.png"});throw new j(`被重定向离开创作者中心(当前 ${y})——登录态可能已过期。`+"用 ppcli xiaohongshu creator-profile 重新登录。截图: /tmp/xhs_video_redirect_debug.png")}const I=await d(q,N);if(!I.ok){await q.screenshot({path:"/tmp/xhs_video_upload_debug.png"});throw new j(`视频上传失败: ${I.error??"unknown"}。截图: /tmp/xhs_video_upload_debug.png`)}if(!await l(q)){await q.screenshot({path:"/tmp/xhs_video_form_debug.png"});throw new j("上传后编辑表单未就绪(标题/正文未出现)。截图: /tmp/xhs_video_form_debug.png")}process.stderr.write(` 等待视频转码(可发布状态)...
|
|
77
77
|
`);if(!await c(q)){await q.screenshot({path:"/tmp/xhs_video_transcode_debug.png"});throw new j("视频转码超时(10min),发布按钮未解禁。截图: /tmp/xhs_video_transcode_debug.png")}await E(q,G);await q.wait({time:0.4});await n(q,J);await q.wait({time:0.5});let F=[];if(Q.length){F=await o(q,Q);const Z=Q.filter((S)=>!F.includes(S));if(Z.length){await q.screenshot({path:"/tmp/xhs_video_topic_debug.png"});throw new j(`话题未能生成实体: ${Z.join(", ")}(联想下拉未出现或无匹配项)。截图: /tmp/xhs_video_topic_debug.png`)}}const b=await q.evaluate("(() => { const b = document.querySelector('xhs-publish-btn'); return !!b && b.getAttribute('submit-disabled') === 'false'; })()");if(!v&&!b){await q.screenshot({path:"/tmp/xhs_video_prepublish_debug.png"});throw new j("发布前复查:发布按钮不可用(submit-disabled≠false)。截图: /tmp/xhs_video_prepublish_debug.png")}const W=await s(q,v);if(!W?.ok){await q.screenshot({path:"/tmp/xhs_video_submit_debug.png"});throw new j(`找不到「${v?"暂存离开":"发布"}」触发方式(hosts=${W?.hosts??0}${W?.lastMethodError?`, lastMethodError=${W.lastMethodError}`:""})。截图: /tmp/xhs_video_submit_debug.png`)}await q.wait({time:4});let H=await q.evaluate("() => location.href");const w=v?["草稿已保存","暂存成功","保存成功","保存于"]:["发布成功","上传成功"],B=await q.evaluate(`
|
|
78
78
|
((markers) => {
|
|
79
79
|
for (const el of document.querySelectorAll('*')) {
|
|
@@ -546,7 +546,7 @@ import*as b from"node:fs";import*as D from"node:path";import{CommandExecutionErr
|
|
|
546
546
|
}
|
|
547
547
|
return removed;
|
|
548
548
|
})(${JSON.stringify(J)})
|
|
549
|
-
`));return Number(K)||0}function _z(z){const J=String(z&&(z.url||z.key)||""),K=String(z&&z.method||"").toUpperCase(),Z=Number(z&&z.status||0);if(K!=="PUT")return null;if(!/ros-upload[^/]*\/spectrum\//.test(J))return null;if(Z<200||Z>=300)return null;const Q=J.match(/\/spectrum\/([^?#]+)/);return Q?Q[1]:J}function Qz(z){return Math.min(45,6+z*4)}async function Zz(z,J,K,Z=90000){if(J<=0)return{ok:!0,completed:0};if(!K||typeof z.readNetworkCapture!=="function"){await z.wait({time:Qz(J)});return{ok:!0,completed:null,viaFallback:!0}}const Q=1500,$=Math.ceil(9000/Q),j=Math.ceil(Z/Q),Y=new Set;for(let U=0;U<j;U++){const f=await z.readNetworkCapture().catch(()=>[]);for(const B of Array.isArray(f)?f:[]){const G=_z(B);if(G)Y.add(G)}if(Y.size>=J)return{ok:!0,completed:Y.size};if(U>=$&&Y.size===0){await z.wait({time:Qz(J)});return{ok:!0,completed:null,viaFallback:!0}}await z.wait({time:Q/1000})}return{ok:!1,completed:Y.size}}Vz({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:Hz.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不会展开 @文件 引用;长正文建议用 --file)"},{name:"file",required:!1,help:"从本机文件读取笔记正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题 (最多20字)"},{name:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${c} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${N}。可选: ${d.map(([z,J])=>`${z}(${J})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail"],func:async(z,J)=>{if(!z)throw Error("Browser page required");const K=String(J.title??"").trim(),Z=J.file?String(J.file).trim():"";let Q=String(J.content??"").trim();if(Z){if(Q)throw new F("正文位置参数和 --file 只能二选一,不能同时给");const V=D.resolve(Z);if(!b.existsSync(V))throw new F(`--file 指向的文件不存在: ${V}`);Q=b.readFileSync(V,"utf-8").trim();if(!Q)throw new F(`--file 指向的文件内容为空: ${V}`)}const q=J.images?String(J.images).split(",").map((V)=>V.trim()).filter(Boolean):[],$=J.topics?String(J.topics).split(",").map((V)=>V.trim()).filter(Boolean):[],j=Boolean(J.draft),Y=J["card-text"]?String(J["card-text"]):"",U=Y?Y.split(c).map((V)=>V.trim()).filter(Boolean):[],f=J["card-style"]?String(J["card-style"]).trim():"",B=U.length>0;if(!K)throw new F("--title is required");if(K.length>E)throw new F(`Title is ${K.length} chars — must be ≤ ${E}`);if(!Q)throw new F("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");Gz(Q,{fileFlag:"--file"});if(!B&&q.length===0)throw new F("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(q.length>x)throw new F(`Too many images: ${q.length} (max ${x})`);if(B&&q.some((V)=>D.extname(V).toLowerCase()===".gif"))throw new F("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const G=Fz(q);await z.goto(Yz);await z.wait({time:1});await z.evaluate("() => { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }").catch(()=>{return});let h="";for(let V=0;V<30;V++){await z.wait({time:0.5});h=await z.evaluate("() => location.href");if(h.includes("creator.xiaohongshu.com"))break}if(!h.includes("creator.xiaohongshu.com")){await z.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${h}) — session may have expired. `+"Re-capture browser login via:
|
|
549
|
+
`));return Number(K)||0}function _z(z){const J=String(z&&(z.url||z.key)||""),K=String(z&&z.method||"").toUpperCase(),Z=Number(z&&z.status||0);if(K!=="PUT")return null;if(!/ros-upload[^/]*\/spectrum\//.test(J))return null;if(Z<200||Z>=300)return null;const Q=J.match(/\/spectrum\/([^?#]+)/);return Q?Q[1]:J}function Qz(z){return Math.min(45,6+z*4)}async function Zz(z,J,K,Z=90000){if(J<=0)return{ok:!0,completed:0};if(!K||typeof z.readNetworkCapture!=="function"){await z.wait({time:Qz(J)});return{ok:!0,completed:null,viaFallback:!0}}const Q=1500,$=Math.ceil(9000/Q),j=Math.ceil(Z/Q),Y=new Set;for(let U=0;U<j;U++){const f=await z.readNetworkCapture().catch(()=>[]);for(const B of Array.isArray(f)?f:[]){const G=_z(B);if(G)Y.add(G)}if(Y.size>=J)return{ok:!0,completed:Y.size};if(U>=$&&Y.size===0){await z.wait({time:Qz(J)});return{ok:!0,completed:null,viaFallback:!0}}await z.wait({time:Q/1000})}return{ok:!1,completed:Y.size}}Vz({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:Hz.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!1,positional:!0,help:"笔记正文(字面文本,不会展开 @文件 引用;长正文建议用 --file)"},{name:"file",required:!1,help:"从本机文件读取笔记正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"笔记标题 (最多20字)"},{name:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${c} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${N}。可选: ${d.map(([z,J])=>`${z}(${J})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail"],func:async(z,J)=>{if(!z)throw Error("Browser page required");const K=String(J.title??"").trim(),Z=J.file?String(J.file).trim():"";let Q=String(J.content??"").trim();if(Z){if(Q)throw new F("正文位置参数和 --file 只能二选一,不能同时给");const V=D.resolve(Z);if(!b.existsSync(V))throw new F(`--file 指向的文件不存在: ${V}`);Q=b.readFileSync(V,"utf-8").trim();if(!Q)throw new F(`--file 指向的文件内容为空: ${V}`)}const q=J.images?String(J.images).split(",").map((V)=>V.trim()).filter(Boolean):[],$=J.topics?String(J.topics).split(",").map((V)=>V.trim()).filter(Boolean):[],j=Boolean(J.draft),Y=J["card-text"]?String(J["card-text"]):"",U=Y?Y.split(c).map((V)=>V.trim()).filter(Boolean):[],f=J["card-style"]?String(J["card-style"]).trim():"",B=U.length>0;if(!K)throw new F("--title is required");if(K.length>E)throw new F(`Title is ${K.length} chars — must be ≤ ${E}`);if(!Q)throw new F("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");Gz(Q,{fileFlag:"--file"});if(!B&&q.length===0)throw new F("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(q.length>x)throw new F(`Too many images: ${q.length} (max ${x})`);if(B&&q.some((V)=>D.extname(V).toLowerCase()===".gif"))throw new F("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const G=Fz(q);await z.goto(Yz);await z.wait({time:1});await z.evaluate("() => { try { window.onbeforeunload = null; } catch (e) {} location.reload(); }").catch(()=>{return});let h="";for(let V=0;V<30;V++){await z.wait({time:0.5});h=await z.evaluate("() => location.href");if(h.includes("creator.xiaohongshu.com"))break}if(!h.includes("creator.xiaohongshu.com")){await z.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${h}) — session may have expired. `+"Re-capture browser login via: ppcli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const L=await vz(z);if((await bz(z,L?.ok?5000:2000)).state==="video_surface"){await z.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const V=L?.ok?`clicked "${L.text}"`:`visible candidates: ${(L?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${V}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}const P=await Rz(z);let I=f;if(B)I=await wz(z,U,f);else{const V=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,O=await r(z,G);if(!O.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new H(`图片注入失败:${O.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_upload_debug.png")}await z.wait({time:u/1000});const v=await Zz(z,G.length,V);if(!v.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new H(`图片上传未全部完成:只确认 ${v.completed}/${G.length} 张传到小红书 CDN,`+"继续发布会丢图(通常只剩封面)。直接重新运行本 publish 命令即可(会自动重置发布页重传);"+"请勿手动修改当前页面。Debug: /tmp/xhs_publish_upload_debug.png")}await g(z);if(P.length){await Kz(z,P);await z.wait({time:0.5})}const w=await e(z);if(w&&typeof w.count==="number"&&w.count>G.length){await z.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new H(`清掉恢复态旧图后编辑器内仍有 ${w.count} 张图,超过本次注入的 ${G.length} 张。`+"直接重新运行本 publish 命令即可(会自动重置发布页);请勿手动修改当前页面。Debug: /tmp/xhs_publish_dup_debug.png")}}if(!await yz(z)){await z.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new H("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(B)await zz(z,U.length,"文字配图 generated images");if(B&&G.length>0){const V=typeof z.startNetworkCapture==="function"?await z.startNetworkCapture("").catch(()=>!1):!1,O=await r(z,G);if(!O.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new H(`追加图片失败:${O.error??"unknown"}。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await z.wait({time:u/1000});const v=await Zz(z,G.length,V);if(!v.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new H(`追加图片上传未全部完成:只确认 ${v.completed}/${G.length} 张传到小红书 CDN。`+"直接重新运行本 publish 命令即可(会自动重置发布页重传);请勿手动修改当前页面。"+"Debug: /tmp/xhs_publish_append_debug.png")}await g(z);await zz(z,U.length+G.length,"文字配图 appended images")}if(B&&P.length){await Kz(z,P);await z.wait({time:0.5})}await p(z,A,K,"title");await z.wait({time:0.5});await p(z,l,Q,"content");await z.wait({time:0.5});let y=[];if($.length)y=await Bz(z,l,$);const T=j?["暂存离开","存草稿"]:["发布","发布笔记"],X=await z.evaluate(`
|
|
550
550
|
(cfg => {
|
|
551
551
|
const { isDraftMode, publishNames, draftNames, labels } = cfg;
|
|
552
552
|
const isVisible = (el) => {
|
package/clis/xiaoyuzhou/auth.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import*as Q from"node:fs";import*as z from"node:os";import*as W from"node:path";import{CliError as L,CommandExecutionError as M,ConfigError as S,EXIT_CODES as X,getErrorMessage as N}from"@jackwener/opencli/errors";export const XIAOYUZHOU_API_BASE_URL="https://api.xiaoyuzhoufm.com",XIAOYUZHOU_TOKEN_TTL_MS=1200000,XIAOYUZHOU_REFRESH_SKEW_MS=60000,XIAOYUZHOU_DEFAULT_DEVICE_ID="81ADBFD6-6921-482B-9AB9-A29E7CC7BB55",XIAOYUZHOU_DEFAULT_DEVICE_PROPERTIES="",XIAOYUZHOU_DEFAULT_USER_AGENT="Xiaoyuzhou/2.98.0 (build:2908; iOS 26.2.1)";function Y(){return Date.now()}export function getXiaoyuzhouCredentialFile(){return W.join(z.homedir(),".
|
|
1
|
+
import*as Q from"node:fs";import*as z from"node:os";import*as W from"node:path";import{CliError as L,CommandExecutionError as M,ConfigError as S,EXIT_CODES as X,getErrorMessage as N}from"@jackwener/opencli/errors";export const XIAOYUZHOU_API_BASE_URL="https://api.xiaoyuzhoufm.com",XIAOYUZHOU_TOKEN_TTL_MS=1200000,XIAOYUZHOU_REFRESH_SKEW_MS=60000,XIAOYUZHOU_DEFAULT_DEVICE_ID="81ADBFD6-6921-482B-9AB9-A29E7CC7BB55",XIAOYUZHOU_DEFAULT_DEVICE_PROPERTIES="",XIAOYUZHOU_DEFAULT_USER_AGENT="Xiaoyuzhou/2.98.0 (build:2908; iOS 26.2.1)";function Y(){return Date.now()}export function getXiaoyuzhouCredentialFile(){return W.join(z.homedir(),".ppcli","xiaoyuzhou.json")}function V(j){return new L("AUTH_REQUIRED",j,`Update ${getXiaoyuzhouCredentialFile()} with fresh Xiaoyuzhou credentials before retrying.`,X.NOPERM)}function Z(j){const D=Number(j);return Number.isFinite(D)?D:0}export function normalizeXiaoyuzhouCredentials(j={}){const D=Z(j.last_updated_ts??j.lastUpdatedTs);let F=Z(j.expires_at??j.expiresAt);if(F>0&&F<10000000000)F*=1000;if(!F&&D>0)F=D*1000+XIAOYUZHOU_TOKEN_TTL_MS;return{access_token:String(j.access_token??j.accessToken??"").trim(),refresh_token:String(j.refresh_token??j.refreshToken??"").trim(),expires_at:F,device_id:String(j.device_id??j.deviceId??XIAOYUZHOU_DEFAULT_DEVICE_ID).trim()||XIAOYUZHOU_DEFAULT_DEVICE_ID,device_properties:String(j.device_properties??j.deviceProperties??XIAOYUZHOU_DEFAULT_DEVICE_PROPERTIES)}}export function loadXiaoyuzhouCredentials(){const j=getXiaoyuzhouCredentialFile();if(Q.existsSync(j))try{const D=JSON.parse(Q.readFileSync(j,"utf-8")),F=normalizeXiaoyuzhouCredentials(D);if(!F.access_token||!F.refresh_token)throw new S(`Xiaoyuzhou credential file is missing access_token or refresh_token: ${j}`,"Recreate the file with valid credentials.");return F}catch(D){if(D instanceof S)throw D;throw new S(`Failed to parse Xiaoyuzhou credential file: ${j}`,`Ensure ${j} contains valid JSON. (${N(D)})`)}throw new S(`Missing Xiaoyuzhou credentials. Expected ${j}`,`Create ${j} with access_token and refresh_token.`)}export function saveXiaoyuzhouCredentials(j){const D=getXiaoyuzhouCredentialFile();Q.mkdirSync(W.dirname(D),{recursive:!0});Q.writeFileSync(D,`${JSON.stringify({access_token:j.access_token,refresh_token:j.refresh_token,expires_at:j.expires_at,device_id:j.device_id,device_properties:j.device_properties},null,2)}
|
|
2
2
|
`,"utf-8")}export function shouldRefreshXiaoyuzhouCredentials(j,D=Y()){return Number.isFinite(j.expires_at)&&j.expires_at>0&&D>=j.expires_at-XIAOYUZHOU_REFRESH_SKEW_MS}export function buildXiaoyuzhouHeaders(j,D={}){const{contentType:F="application/json",includeLocalTime:H=!1,includeRefreshToken:G=!1}=D,J={"Content-Type":F,Host:"api.xiaoyuzhoufm.com","User-Agent":XIAOYUZHOU_DEFAULT_USER_AGENT,Market:"AppStore","App-BuildNo":"2908",OS:"ios",Manufacturer:"Apple",BundleID:"app.podcast.cosmos",Connection:"keep-alive","abtest-info":'{"old_user_discovery_feed":"enable"}',"Accept-Language":"en-HK;q=1.0, zh-Hans-HK;q=0.9",Model:"iPhone18,1","app-permissions":"100000",Accept:"*/*","App-Version":"2.98.0",WifiConnected:"true","OS-Version":"26.2.1","x-custom-xiaoyuzhou-app-dev":"","x-jike-device-id":j.device_id||XIAOYUZHOU_DEFAULT_DEVICE_ID,"x-jike-device-properties":j.device_properties??XIAOYUZHOU_DEFAULT_DEVICE_PROPERTIES};if(j.access_token)J["x-jike-access-token"]=j.access_token;if(G&&j.refresh_token)J["x-jike-refresh-token"]=j.refresh_token;if(H){J["Local-Time"]=new Date().toISOString();J.Timezone=Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"}return J}export async function refreshXiaoyuzhouCredentials(j,D=fetch){if(!j.refresh_token)throw V("Xiaoyuzhou refresh token is missing");let F;try{F=await D(`${XIAOYUZHOU_API_BASE_URL}/app_auth_tokens.refresh`,{method:"POST",headers:buildXiaoyuzhouHeaders(j,{contentType:"application/x-www-form-urlencoded; charset=utf-8",includeLocalTime:!0,includeRefreshToken:!0}),signal:AbortSignal.timeout(20000)})}catch(K){throw new M(`Failed to refresh Xiaoyuzhou credentials: ${N(K)}`)}const H=await F.text();if(!F.ok)throw V(`Xiaoyuzhou token refresh failed with HTTP ${F.status}${H?`: ${H}`:""}`);let G;try{G=JSON.parse(H)}catch(K){throw new M(`Xiaoyuzhou refresh returned invalid JSON: ${N(K)}`)}if(!G?.success)throw V("Xiaoyuzhou refresh API returned success=false");const J=normalizeXiaoyuzhouCredentials({...j,access_token:G["x-jike-access-token"]||"",refresh_token:G["x-jike-refresh-token"]||"",expires_at:Y()+XIAOYUZHOU_TOKEN_TTL_MS});if(!J.access_token||!J.refresh_token)throw V("Xiaoyuzhou refresh API returned empty access_token or refresh_token");saveXiaoyuzhouCredentials(J);return J}function B(j,D){const F=new URL(j,XIAOYUZHOU_API_BASE_URL);if(D){for(const[H,G]of Object.entries(D))if(G!==void 0&&G!==null&&G!=="")F.searchParams.set(H,String(G))}return F.toString()}async function _(j,D,F,H){const{method:G="GET",query:J,body:K}=D;let O;try{O=await H(B(j,J),{method:G,headers:buildXiaoyuzhouHeaders(F,{contentType:"application/json",includeLocalTime:!0}),body:K===void 0?void 0:JSON.stringify(K),signal:AbortSignal.timeout(20000)})}catch($){throw new M(`Failed to reach Xiaoyuzhou API: ${N($)}`)}return O}export async function requestXiaoyuzhouJson(j,D={},F=fetch){let H=D.credentials??loadXiaoyuzhouCredentials();if(shouldRefreshXiaoyuzhouCredentials(H))H=await refreshXiaoyuzhouCredentials(H,F);let G=await _(j,D,H,F);if(G.status===401){H=await refreshXiaoyuzhouCredentials(H,F);G=await _(j,D,H,F)}const J=await G.text();if(!G.ok)throw new M(`Xiaoyuzhou API request failed with HTTP ${G.status}${J?`: ${J}`:""}`);let K;try{K=JSON.parse(J)}catch(O){throw new M(`Xiaoyuzhou API returned invalid JSON: ${N(O)}`)}if(K?.success===!1)throw new M(K?.message||K?.msg||"Xiaoyuzhou API returned success=false");return{credentials:H,raw:K,data:K?.data}}export async function fetchXiaoyuzhouTranscriptBody(j,D=fetch){let F;try{F=await D(j,{method:"GET",headers:{"User-Agent":XIAOYUZHOU_DEFAULT_USER_AGENT,Accept:"*/*",Market:"AppStore"},signal:AbortSignal.timeout(20000)})}catch(G){throw new M(`Failed to fetch Xiaoyuzhou transcript content: ${N(G)}`)}const H=await F.text();if(!F.ok)throw new M(`Xiaoyuzhou transcript download failed with HTTP ${F.status}${H?`: ${H}`:""}`);return H}export function extractTranscriptText(j){let D;try{D=JSON.parse(j)}catch{return{text:"",segmentCount:0}}let F=[];if(Array.isArray(D))F=D;else if(D&&typeof D==="object"){for(const G of["segments","data","transcript","items"])if(Array.isArray(D[G])){F=D[G];break}if(F.length===0){const G=typeof D.text==="string"?D.text.trim():"";if(G)return{text:G,segmentCount:1}}}const H=[];for(const G of F){if(!G||typeof G!=="object"||typeof G.text!=="string")continue;const J=G.text.trim();if(J)H.push(J)}return{text:H.join(`
|
|
3
3
|
`),segmentCount:H.length}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as s from"node:fs";import*as n from"node:path";import{cli as y,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as R,CliError as i}from"@jackwener/opencli/errors";import{loadXiaoyuzhouCredentials as j,requestXiaoyuzhouJson as h,fetchXiaoyuzhouTranscriptBody as w,extractTranscriptText as E}from"./auth.js";y({site:"xiaoyuzhou",name:"transcript",access:"read",description:"Download Xiaoyuzhou transcript as JSON and text (requires local credentials)",domain:"www.xiaoyuzhoufm.com",strategy:S.PUBLIC,browser:!1,args:[{name:"id",positional:!0,required:!0,help:"Episode ID (eid from podcast-episodes output)"},{name:"output",default:"./xiaoyuzhou-transcripts",help:"Output directory"},{name:"json",type:"boolean",default:!0,help:"Save transcript JSON file"},{name:"text",type:"boolean",default:!0,help:"Save extracted transcript text file"}],columns:["title","podcast","status","segments","json_file","text_file"],func:async(t)=>{if(t.json===!1&&t.text===!1)throw new R("At least one of --json or --text must be enabled","Example:
|
|
1
|
+
import*as s from"node:fs";import*as n from"node:path";import{cli as y,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as R,CliError as i}from"@jackwener/opencli/errors";import{loadXiaoyuzhouCredentials as j,requestXiaoyuzhouJson as h,fetchXiaoyuzhouTranscriptBody as w,extractTranscriptText as E}from"./auth.js";y({site:"xiaoyuzhou",name:"transcript",access:"read",description:"Download Xiaoyuzhou transcript as JSON and text (requires local credentials)",domain:"www.xiaoyuzhoufm.com",strategy:S.PUBLIC,browser:!1,args:[{name:"id",positional:!0,required:!0,help:"Episode ID (eid from podcast-episodes output)"},{name:"output",default:"./xiaoyuzhou-transcripts",help:"Output directory"},{name:"json",type:"boolean",default:!0,help:"Save transcript JSON file"},{name:"text",type:"boolean",default:!0,help:"Save extracted transcript text file"}],columns:["title","podcast","status","segments","json_file","text_file"],func:async(t)=>{if(t.json===!1&&t.text===!1)throw new R("At least one of --json or --text must be enabled","Example: ppcli xiaoyuzhou transcript 69dd0c98e2c8be31551f6a33 --text true");let o=j();const d=await h("/v1/episode/get",{query:{eid:t.id},credentials:o});o=d.credentials;const e=d.data;if(!e)throw new i("NOT_FOUND","Episode not found","Please check the episode ID");const p=String(e.transcript?.mediaId||e.media?.id||e.transcriptMediaId||"").trim();if(!p)throw new i("PARSE_ERROR","mediaId not found in episode payload","Transcript metadata requires episode.transcript.mediaId, episode.media.id, or episode.transcriptMediaId");const c=(await h("/v1/episode-transcript/get",{method:"POST",body:{eid:t.id,mediaId:p},credentials:o})).data,u=String(c?.transcriptUrl||c?.url||"").trim();if(!u)throw new i("EMPTY_RESULT","Transcript URL not found","This episode may not have transcript data available");const r=await w(u),{text:l,segmentCount:x}=E(r);if(t.text!==!1&&r.trim()&&!l.trim())throw new i("PARSE_ERROR","Failed to extract transcript text","Transcript payload format is unsupported. Re-run with --json true to inspect the raw payload.");const a=n.join(String(t.output||"./xiaoyuzhou-transcripts"),String(t.id));s.mkdirSync(a,{recursive:!0});const f=n.join(a,"transcript.json"),m=n.join(a,"transcript.txt");if(t.json!==!1)s.writeFileSync(f,r,"utf-8");if(t.text!==!1)s.writeFileSync(m,l,"utf-8");return[{title:e.title||"episode",podcast:e.podcast?.title||"",status:"success",segments:t.text===!1?"-":String(x),json_file:t.json===!1?"-":f,text_file:t.text===!1?"-":m}]}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as u from"node:path";import{cli as
|
|
1
|
+
import*as u from"node:path";import{cli as p,Strategy as m}from"@jackwener/opencli/registry";import{CliError as d}from"@jackwener/opencli/errors";import{log as c}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as f,yollomiPost as g,downloadOutput as y,fmtBytes as O}from"./utils.js";p({site:"yollomi",name:"background",access:"write",description:"Generate AI background for a product/object image (5 credits)",domain:f,strategy:m.COOKIE,args:[{name:"image",positional:!0,required:!0,help:'Image URL (upload via "ppcli yollomi upload" first)'},{name:"prompt",default:"",help:"Background description (optional)"},{name:"output",default:"./yollomi-output",help:"Output directory"},{name:"no-download",type:"boolean",default:!1,help:"Only show URL"}],columns:["status","file","size","url"],func:async(a,e)=>{const{image:i,prompt:r}=e;c.status("Generating background...");const o=await g(a,"/api/ai/ai-background-generator",{images:[i],prompt:r||void 0,aspect_ratio:"1:1"}),t=o.image||o.images?.[0];if(!t)throw new d("EMPTY_RESPONSE","No result","Try a different image");if(e["no-download"])return[{status:"generated",file:"-",size:"-",url:t}];try{const n=`yollomi_bg_${Date.now()}.png`,{path:l,size:s}=await y(t,e.output,n);return[{status:"saved",file:u.relative(".",l),size:O(s),url:t}]}catch{return[{status:"download-failed",file:"-",size:"-",url:t}]}}});
|
package/clis/yollomi/edit.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as f from"node:path";import{cli as w,Strategy as h}from"@jackwener/opencli/registry";import{CliError as y}from"@jackwener/opencli/errors";import{log as l}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as q,yollomiPost as O,downloadOutput as E,fmtBytes as I}from"./utils.js";w({site:"yollomi",name:"edit",access:"write",description:"Edit images with AI text prompts (Qwen image edit)",domain:q,strategy:h.COOKIE,args:[{name:"image",positional:!0,required:!0,help:'Input image URL (upload via "
|
|
1
|
+
import*as f from"node:path";import{cli as w,Strategy as h}from"@jackwener/opencli/registry";import{CliError as y}from"@jackwener/opencli/errors";import{log as l}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as q,yollomiPost as O,downloadOutput as E,fmtBytes as I}from"./utils.js";w({site:"yollomi",name:"edit",access:"write",description:"Edit images with AI text prompts (Qwen image edit)",domain:q,strategy:h.COOKIE,args:[{name:"image",positional:!0,required:!0,help:'Input image URL (upload via "ppcli yollomi upload" first)'},{name:"prompt",positional:!0,required:!0,help:'Editing instruction (e.g. "Make it look vintage")'},{name:"model",default:"qwen-image-edit",choices:["qwen-image-edit","qwen-image-edit-plus"],help:"Edit model"},{name:"output",default:"./yollomi-output",help:"Output directory"},{name:"no-download",type:"boolean",default:!1,help:"Only show URL"}],columns:["status","file","size","credits","url"],func:async(d,e)=>{const{image:s,prompt:m,model:a}=e;let n;if(a==="qwen-image-edit-plus")n={prompt:m,images:[s]};else n={image:s,prompt:m,go_fast:!0,output_format:"png"};const p=a==="qwen-image-edit-plus"?"/api/ai/qwen-image-edit-plus":"/api/ai/qwen-image-edit";l.status(`Editing with ${a}...`);const i=await O(d,p,n),r=i.images||(i.image?[i.image]:[]);if(!r.length)throw new y("EMPTY_RESPONSE","No result","Try a different prompt");const t=i.remainingCredits,o=r[0];if(e["no-download"])return[{status:"edited",file:"-",size:"-",credits:t??"-",url:o}];try{const u=`yollomi_edit_${Date.now()}.png`,{path:g,size:c}=await E(o,e.output,u);if(t!==void 0)l.status(`Credits remaining: ${t}`);return[{status:"saved",file:f.relative(".",g),size:I(c),credits:t??"-",url:o}]}catch{return[{status:"download-failed",file:"-",size:"-",credits:t??"-",url:o}]}}});
|
package/clis/yollomi/generate.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as w from"node:path";import{cli as b,Strategy as I}from"@jackwener/opencli/registry";import{CliError as c}from"@jackwener/opencli/errors";import{log as f}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as U,yollomiPost as z,downloadOutput as D,fmtBytes as E,MODEL_ROUTES as L}from"./utils.js";function P(p){const e={"1:1":[1024,1024],"16:9":[1344,768],"9:16":[768,1344],"4:3":[1152,896],"3:4":[896,1152]},[n,t]=e[p]||[1024,1024];return{width:n,height:t}}b({site:"yollomi",name:"generate",access:"write",description:"Generate images with AI (text-to-image or image-to-image)",domain:U,strategy:I.COOKIE,args:[{name:"prompt",positional:!0,required:!0,help:"Text prompt describing the image"},{name:"model",default:"z-image-turbo",help:"Model ID (z-image-turbo, flux-schnell, nano-banana, flux-2-pro, ...)"},{name:"ratio",default:"1:1",choices:["1:1","16:9","9:16","4:3","3:4"],help:"Aspect ratio"},{name:"image",help:'Input image URL for image-to-image (upload via "
|
|
1
|
+
import*as w from"node:path";import{cli as b,Strategy as I}from"@jackwener/opencli/registry";import{CliError as c}from"@jackwener/opencli/errors";import{log as f}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as U,yollomiPost as z,downloadOutput as D,fmtBytes as E,MODEL_ROUTES as L}from"./utils.js";function P(p){const e={"1:1":[1024,1024],"16:9":[1344,768],"9:16":[768,1344],"4:3":[1152,896],"3:4":[896,1152]},[n,t]=e[p]||[1024,1024];return{width:n,height:t}}b({site:"yollomi",name:"generate",access:"write",description:"Generate images with AI (text-to-image or image-to-image)",domain:U,strategy:I.COOKIE,args:[{name:"prompt",positional:!0,required:!0,help:"Text prompt describing the image"},{name:"model",default:"z-image-turbo",help:"Model ID (z-image-turbo, flux-schnell, nano-banana, flux-2-pro, ...)"},{name:"ratio",default:"1:1",choices:["1:1","16:9","9:16","4:3","3:4"],help:"Aspect ratio"},{name:"image",help:'Input image URL for image-to-image (upload via "ppcli yollomi upload" first)'},{name:"output",default:"./yollomi-output",help:"Output directory"},{name:"no-download",type:"boolean",default:!1,help:"Only show URLs, skip download"}],columns:["index","status","file","size","url"],func:async(p,e)=>{const{prompt:n,model:t,ratio:l}=e,d=L[t];if(!d)throw new c("INVALID_MODEL",`Unknown model: ${t}`,'Run "ppcli yollomi models --type image" to see available models');let i;if(t==="z-image-turbo"){const{width:o,height:a}=P(l);i={prompt:n,width:o,height:a,output_format:"jpg",output_quality:85,guidance_scale:0,num_inference_steps:8}}else if(t==="flux-2-pro"){i={prompt:n,aspectRatio:l,outputNumber:1};if(e.image)i.imageUrl=e.image}else if(t==="flux-kontext-pro"){i={prompt:n,output_format:"jpg"};if(e.image)i.imageUrl=e.image;if(l!=="1:1")i.aspect_ratio=l}else{i={prompt:n,aspect_ratio:l};if(e.image)i.imageUrl=e.image}f.status(`Generating with ${t}...`);const r=await z(p,d,i),u=r.images||(r.image?[r.image]:[]);if(!u.length)throw new c("EMPTY_RESPONSE","No images returned","Try a different prompt or model");const g=e["no-download"],h=e.output,m=[];for(let o=0;o<u.length;o++){const a=u[o];if(g){m.push({index:o+1,status:"generated",file:"-",size:"-",url:a});continue}try{const s=(()=>{try{return new URL(a).pathname}catch{return a}})(),y=s.endsWith(".png")||s.endsWith(".webp")?s.slice(s.lastIndexOf(".")):".jpg",x=`yollomi_${t}_${Date.now()}_${o+1}${y}`,{path:O,size:_}=await D(a,h,x);m.push({index:o+1,status:"saved",file:w.relative(".",O),size:E(_),url:a})}catch{m.push({index:o+1,status:"download-failed",file:"-",size:"-",url:a})}}if(r.remainingCredits!==void 0)f.status(`Credits remaining: ${r.remainingCredits}`);return m}});
|
package/clis/yollomi/try-on.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as n from"node:path";import{cli as s,Strategy as u}from"@jackwener/opencli/registry";import{CliError as p}from"@jackwener/opencli/errors";import{log as m}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as c,yollomiPost as h,downloadOutput as y,fmtBytes as d}from"./utils.js";s({site:"yollomi",name:"try-on",access:"write",description:"Virtual try-on — see how clothes look on a person (3 credits)",domain:c,strategy:u.COOKIE,args:[{name:"person",required:!0,help:'Person photo URL (upload via "
|
|
1
|
+
import*as n from"node:path";import{cli as s,Strategy as u}from"@jackwener/opencli/registry";import{CliError as p}from"@jackwener/opencli/errors";import{log as m}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as c,yollomiPost as h,downloadOutput as y,fmtBytes as d}from"./utils.js";s({site:"yollomi",name:"try-on",access:"write",description:"Virtual try-on — see how clothes look on a person (3 credits)",domain:c,strategy:u.COOKIE,args:[{name:"person",required:!0,help:'Person photo URL (upload via "ppcli yollomi upload" first)'},{name:"cloth",required:!0,help:"Clothing image URL"},{name:"cloth-type",default:"upper",choices:["upper","lower","overall"],help:"Clothing type"},{name:"output",default:"./yollomi-output",help:"Output directory"},{name:"no-download",type:"boolean",default:!1,help:"Only show URL"}],columns:["status","file","size","url"],func:async(l,t)=>{m.status("Processing virtual try-on...");const e=await h(l,"/api/ai/virtual-try-on",{person_image:t.person,cloth_image:t.cloth,cloth_type:t["cloth-type"],output_format:"png",output_quality:100}),o=e.image||e.images?.[0];if(!o)throw new p("EMPTY_RESPONSE","No result","Check both images have clear subjects");if(t["no-download"])return[{status:"generated",file:"-",size:"-",url:o}];try{const a=`yollomi_tryon_${Date.now()}.png`,{path:i,size:r}=await y(o,t.output,a);return[{status:"saved",file:n.relative(".",i),size:d(r),url:o}]}catch{return[{status:"download-failed",file:"-",size:"-",url:o}]}}});
|
package/clis/youtube/publish.js
CHANGED
|
@@ -48,7 +48,7 @@ import{cli as U,Strategy as _}from"@jackwener/opencli/registry";import{ArgumentE
|
|
|
48
48
|
}
|
|
49
49
|
return '';
|
|
50
50
|
})()
|
|
51
|
-
`))||"");if(W&&H.test(W))return!0;await q.wait({time:5})}return!1}U({site:"youtube",name:"publish",access:"write",description:"Publish a video to YouTube via Studio (one command: upload + metadata + publish)",domain:"www.youtube.com",strategy:_.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",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"}],columns:["status","title","visibility","url"],func:async(q,z)=>{const H=String(z.title??"").trim();if(!H)throw new C("--title is required");const Q=h(z.video),W=String(z.description||""),Y=z.thumbnail?y(z.thumbnail):"",$=String(z.playlist||"").trim(),j=O[z.visibility]?z.visibility:"public";await q.goto(F);await q.wait({time:3});const A=await v(q);if(/accounts\.google\.com/.test(A)||/signin/i.test(A))throw new X("YouTube 登录态失效,请先
|
|
51
|
+
`))||"");if(W&&H.test(W))return!0;await q.wait({time:5})}return!1}U({site:"youtube",name:"publish",access:"write",description:"Publish a video to YouTube via Studio (one command: upload + metadata + publish)",domain:"www.youtube.com",strategy:_.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",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"}],columns:["status","title","visibility","url"],func:async(q,z)=>{const H=String(z.title??"").trim();if(!H)throw new C("--title is required");const Q=h(z.video),W=String(z.description||""),Y=z.thumbnail?y(z.thumbnail):"",$=String(z.playlist||"").trim(),j=O[z.visibility]?z.visibility:"public";await q.goto(F);await q.wait({time:3});const A=await v(q);if(/accounts\.google\.com/.test(A)||/signin/i.test(A))throw new X("YouTube 登录态失效,请先 ppcli youtube login");if(!await D(q,'input[type="file"]',60000))throw new X("YouTube 上传文件输入框未出现");await B(q,['input[type="file"]'],[Q]);if(!await D(q,"#title-textarea",120000))throw new X("YouTube 视频详情对话框未出现");await R(q,"#title-textarea #textbox",H.slice(0,100));if(W.trim())await R(q,"#description-textarea #textbox",W);if(Y)try{await B(q,["#file-loader input[type='file']","ytcp-thumbnail-uploader input[type='file']"],[Y]);await q.wait({time:2})}catch{}if($)try{await G(q,"#basics ytcp-text-dropdown-trigger, ytcp-video-metadata-playlists ytcp-dropdown-trigger",8000);await q.wait({time:1.2});const K=`tp-yt-paper-checkbox:has-text('${$}'), ytcp-checkbox-group:has-text('${$}')`;if(await M(q,K))await G(q,K,3000);else if(await G(q,"ytcp-button:has-text('New playlist'), ytcp-button:has-text('创建播放列表')",4000)){await q.wait({time:0.8});await G(q,"tp-yt-paper-item:has-text('New playlist'), tp-yt-paper-item:has-text('新建播放列表')",3000);const J="ytcp-playlist-metadata-editor #textbox, #create-playlist-form #textbox";if(await D(q,J.split(",")[0].trim(),4000)||await M(q,J)){await G(q,J,2000);await q.insertText($);await G(q,"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 G(q,"ytcp-playlist-dialog #save-button, ytcp-button:has-text('Done'), ytcp-button:has-text('完成')",3000);await q.pressKey("Escape");await q.wait({time:0.6})}if(!await G(q,"tp-yt-paper-radio-button[name='VIDEO_MADE_FOR_KIDS_NOT_MFK']",1e4))await G(q,"tp-yt-paper-radio-button:has-text('not made for kids'), tp-yt-paper-radio-button:has-text('不是面向儿童')",6000);if(z.tags){const K=String(z.tags).split(",").map((J)=>J.trim()).filter(Boolean).join(",");try{await G(q,"#toggle-button",6000);await q.wait({time:0.8});const J="#tags-container #text-input, ytcp-form-input-container#tags-container input";if(await M(q,J)){await G(q,J,3000);await q.insertText(`${K.slice(0,500)},`)}}catch{}}for(let K=0;K<5;K++){if(await M(q,"tp-yt-paper-radio-button[name='PUBLIC']"))break;if(!await G(q,"#next-button",6000))await q.wait({time:1.2});await q.wait({time:1})}await G(q,`tp-yt-paper-radio-button[name='${O[j]}']`,1e4);await L(q);await q.wait({time:1.2});let N="";if(!await G(q,"#done-button",15000))throw new X("未找到 YouTube 发布按钮(上传可能未到可发布进度),请在窗口内手动发布");await q.wait({time:4});N=String(Z(await q.evaluate(`
|
|
52
52
|
(() => {
|
|
53
53
|
const a = document.querySelector("a[href*='youtu.be'], a[href*='watch?v=']");
|
|
54
54
|
return a ? (a.getAttribute('href') || '') : '';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as S,Strategy as L}from"@jackwener/opencli/registry";import{ArgumentError as g,AuthRequiredError as N,CommandExecutionError as c,EmptyResultError as _}from"@jackwener/opencli/errors";import{stripHtml as T}from"./text.js";function E(e){return T(e,{preserveBlocks:!0})}const Z=/^\d+$/,C=/^answer:(\d+):(\d+)$/,I=/^\/question\/(\d+)\/answer\/(\d+)\/?$/,U=/^\/answer\/(\d+)\/?$/;function $(e){const n=String(e??"").trim();if(!n)return null;if(Z.test(n))return{answerId:n,questionId:""};const t=n.match(C);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(I);if(i)return{questionId:i[1],answerId:i[2]};i=r.pathname.match(U);if(i)return{answerId:i[1],questionId:""}}catch{return null}return null}function q(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(I)?.[1]||""}catch{return""}}function x(e){return Number.isInteger(e)&&e>=0?e:0}function k(e){return typeof e==="number"&&Number.isFinite(e)&&e>0?new Date(e*1000).toISOString():""}function b(e){return e?.member?.name||e?.name||""}function v(e){if(typeof e==="string")return e.trim();if(typeof e==="number"&&Number.isSafeInteger(e)&&e>=0)return String(e);return""}function H(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 R(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 y(e,{answerId:n,questionId:t,topLevelLimit:r,repliesLimit:i}){const a=[];let w=0,l=0,u=0,f=!1,h=0;for(const s of e){if(!s||typeof s!=="object"||Array.isArray(s)){h+=1;continue}const o=v(s.id);if(!o){h+=1;continue}const d=b(s.author),p=b(s.reply_to_author),m=Boolean(p);if(!m){if(w>=r){f=!0;break}w+=1;l=w;u=0}else if(!l||u>=i)continue;else u+=1;a.push({rank:a.length+1,comment_rank:l,reply_rank:m?u:0,depth:0,id:o,parent_id:"",author:d||"anonymous",reply_to:p,likes:x(s.vote_count),created_at:k(s.created_time),url:H(s.url,t,n,o),content:E(s.content||"")})}return{rows:a,topLevelCount:w,reachedTopLevelLimit:f,malformedComments:h}}const A=1000,z=100,P=20;S({site:"zhihu",name:"answer-comments",access:"read",description:"知乎回答评论列表",domain:"www.zhihu.com",strategy:L.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)"},{name:"replies-limit",type:"int",default:3,help:"Number of replies to include per top-level comment (max 100)"}],columns:["rank","comment_rank","reply_rank","depth","id","parent_id","author","reply_to","likes","created_at","url","content"],func:async(e,n)=>{const t=$(n.id);if(!t)throw new g("Answer ID must be a numeric id, a Zhihu answer URL, or answer:<qid>:<aid>","Example:
|
|
1
|
+
import{cli as S,Strategy as L}from"@jackwener/opencli/registry";import{ArgumentError as g,AuthRequiredError as N,CommandExecutionError as c,EmptyResultError as _}from"@jackwener/opencli/errors";import{stripHtml as T}from"./text.js";function E(e){return T(e,{preserveBlocks:!0})}const Z=/^\d+$/,C=/^answer:(\d+):(\d+)$/,I=/^\/question\/(\d+)\/answer\/(\d+)\/?$/,U=/^\/answer\/(\d+)\/?$/;function $(e){const n=String(e??"").trim();if(!n)return null;if(Z.test(n))return{answerId:n,questionId:""};const t=n.match(C);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(I);if(i)return{questionId:i[1],answerId:i[2]};i=r.pathname.match(U);if(i)return{answerId:i[1],questionId:""}}catch{return null}return null}function q(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(I)?.[1]||""}catch{return""}}function x(e){return Number.isInteger(e)&&e>=0?e:0}function k(e){return typeof e==="number"&&Number.isFinite(e)&&e>0?new Date(e*1000).toISOString():""}function b(e){return e?.member?.name||e?.name||""}function v(e){if(typeof e==="string")return e.trim();if(typeof e==="number"&&Number.isSafeInteger(e)&&e>=0)return String(e);return""}function H(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 R(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 y(e,{answerId:n,questionId:t,topLevelLimit:r,repliesLimit:i}){const a=[];let w=0,l=0,u=0,f=!1,h=0;for(const s of e){if(!s||typeof s!=="object"||Array.isArray(s)){h+=1;continue}const o=v(s.id);if(!o){h+=1;continue}const d=b(s.author),p=b(s.reply_to_author),m=Boolean(p);if(!m){if(w>=r){f=!0;break}w+=1;l=w;u=0}else if(!l||u>=i)continue;else u+=1;a.push({rank:a.length+1,comment_rank:l,reply_rank:m?u:0,depth:0,id:o,parent_id:"",author:d||"anonymous",reply_to:p,likes:x(s.vote_count),created_at:k(s.created_time),url:H(s.url,t,n,o),content:E(s.content||"")})}return{rows:a,topLevelCount:w,reachedTopLevelLimit:f,malformedComments:h}}const A=1000,z=100,P=20;S({site:"zhihu",name:"answer-comments",access:"read",description:"知乎回答评论列表",domain:"www.zhihu.com",strategy:L.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)"},{name:"replies-limit",type:"int",default:3,help:"Number of replies to include per top-level comment (max 100)"}],columns:["rank","comment_rank","reply_rank","depth","id","parent_id","author","reply_to","likes","created_at","url","content"],func:async(e,n)=>{const t=$(n.id);if(!t)throw new g("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>A)throw new g(`--limit must be a positive integer no greater than ${A}`);const i=Number(n["replies-limit"]??3);if(!Number.isInteger(i)||i<0||i>z)throw new g(`--replies-limit must be an integer between 0 and ${z}`);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 w=e.getCurrentUrl?q(await e.getCurrentUrl().catch(()=>"")):"",l=t.questionId||w;let u=`https://www.zhihu.com/api/v4/answers/${a}/comments?order=normal&limit=${P}&offset=0&status=open`;const f=[],h=new Set;while(u&&!h.has(u)){h.add(u);const o=await e.evaluate(`
|
|
2
2
|
(async () => {
|
|
3
3
|
const r = await fetch(${JSON.stringify(u)}, { credentials: 'include' });
|
|
4
4
|
if (!r.ok) return { __httpError: r.status };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as y,Strategy as N}from"@jackwener/opencli/registry";import{ArgumentError as O,AuthRequiredError as P,CommandExecutionError as $,EmptyResultError as q}from"@jackwener/opencli/errors";import{stripHtml as H}from"./text.js";function j(G){return H(G,{preserveBlocks:!0})}const U=/^\d+$/,S=/^answer:(\d+):(\d+)$/,F=/^\/question\/(\d+)\/answer\/(\d+)\/?$/,T=/^\/answer\/(\d+)\/?$/,R=/^\/question\/(\d+)\/?$/,C=/^\/api\/v4\/questions\/(\d+)\/?$/;function D(G){const X=String(G??"").trim();if(!X)return null;if(U.test(X))return{answerId:X,questionId:""};const L=X.match(S);if(L)return{questionId:L[1],answerId:L[2]};try{const K=new URL(X);if(K.protocol!=="https:"||K.username||K.password||K.port||K.hostname!=="www.zhihu.com"&&K.hostname!=="zhihu.com")return null;let Z=K.pathname.match(F);if(Z)return{questionId:Z[1],answerId:Z[2]};Z=K.pathname.match(T);if(Z)return{answerId:Z[1],questionId:""}}catch{return null}return null}function _(G){return D(G)?.answerId??null}function M(G){const X=String(G??"").trim();if(!X)return"";try{const L=new URL(X);if(L.protocol!=="https:"||L.hostname!=="www.zhihu.com"&&L.hostname!=="zhihu.com")return"";return L.pathname.match(F)?.[1]||L.pathname.match(R)?.[1]||L.pathname.match(C)?.[1]||""}catch{return""}}function z(G){return Number.isInteger(G)&&G>=0?G:0}function h(G){return typeof G==="number"&&Number.isFinite(G)&&G>0?new Date(G*1000).toISOString():""}y({site:"zhihu",name:"answer-detail",access:"read",description:"知乎单个回答完整内容(按 answer ID 获取)",domain:"www.zhihu.com",strategy:N.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","content"],func:async(G,X)=>{const L=D(X.id);if(!L)throw new O("Answer ID must be a numeric id, a Zhihu answer URL, or answer:<qid>:<aid>","Example:
|
|
1
|
+
import{cli as y,Strategy as N}from"@jackwener/opencli/registry";import{ArgumentError as O,AuthRequiredError as P,CommandExecutionError as $,EmptyResultError as q}from"@jackwener/opencli/errors";import{stripHtml as H}from"./text.js";function j(G){return H(G,{preserveBlocks:!0})}const U=/^\d+$/,S=/^answer:(\d+):(\d+)$/,F=/^\/question\/(\d+)\/answer\/(\d+)\/?$/,T=/^\/answer\/(\d+)\/?$/,R=/^\/question\/(\d+)\/?$/,C=/^\/api\/v4\/questions\/(\d+)\/?$/;function D(G){const X=String(G??"").trim();if(!X)return null;if(U.test(X))return{answerId:X,questionId:""};const L=X.match(S);if(L)return{questionId:L[1],answerId:L[2]};try{const K=new URL(X);if(K.protocol!=="https:"||K.username||K.password||K.port||K.hostname!=="www.zhihu.com"&&K.hostname!=="zhihu.com")return null;let Z=K.pathname.match(F);if(Z)return{questionId:Z[1],answerId:Z[2]};Z=K.pathname.match(T);if(Z)return{answerId:Z[1],questionId:""}}catch{return null}return null}function _(G){return D(G)?.answerId??null}function M(G){const X=String(G??"").trim();if(!X)return"";try{const L=new URL(X);if(L.protocol!=="https:"||L.hostname!=="www.zhihu.com"&&L.hostname!=="zhihu.com")return"";return L.pathname.match(F)?.[1]||L.pathname.match(R)?.[1]||L.pathname.match(C)?.[1]||""}catch{return""}}function z(G){return Number.isInteger(G)&&G>=0?G:0}function h(G){return typeof G==="number"&&Number.isFinite(G)&&G>0?new Date(G*1000).toISOString():""}y({site:"zhihu",name:"answer-detail",access:"read",description:"知乎单个回答完整内容(按 answer ID 获取)",domain:"www.zhihu.com",strategy:N.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","content"],func:async(G,X)=>{const L=D(X.id);if(!L)throw new O("Answer ID must be a numeric id, a Zhihu answer URL, or answer:<qid>:<aid>","Example: ppcli zhihu answer-detail 1937205528846655537");const{answerId:K}=L,Z=X["max-content"],b=Z==null?0:Number(Z);if(!Number.isInteger(b)||b<0)throw new O("--max-content must be a non-negative integer (0 = no cap, full content)","Example: --max-content 2000");try{await G.goto(`https://www.zhihu.com/answer/${K}`)}catch(V){throw new $(`Failed to open Zhihu answer ${K}: ${V instanceof Error?V.message:String(V)}`,"Open the answer URL in Chrome and retry after the page is reachable.")}const W=G.getCurrentUrl?M(await G.getCurrentUrl().catch(()=>"")):"",k=`https://www.zhihu.com/api/v4/answers/${K}?include=content,voteup_count,comment_count,author,created_time,updated_time,question`,J=await G.evaluate(`
|
|
2
2
|
(async () => {
|
|
3
3
|
const r = await fetch(${JSON.stringify(k)}, { credentials: 'include' });
|
|
4
4
|
if (!r.ok) return { __httpError: r.status };
|
package/clis/zhihu/collection.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import{cli as y,Strategy as I}from"@jackwener/opencli/registry";import{ArgumentError as R,AuthRequiredError as L,CommandExecutionError as j,EmptyResultError as v}from"@jackwener/opencli/errors";import{log as w}from"@jackwener/opencli/logger";import{stripHtml as _}from"./text.js";function H(M,D){const z=Number(M);if(!Number.isInteger(z)||z<=0)throw new R(`zhihu collection --${D} must be a positive integer`,"Example:
|
|
1
|
+
import{cli as y,Strategy as I}from"@jackwener/opencli/registry";import{ArgumentError as R,AuthRequiredError as L,CommandExecutionError as j,EmptyResultError as v}from"@jackwener/opencli/errors";import{log as w}from"@jackwener/opencli/logger";import{stripHtml as _}from"./text.js";function H(M,D){const z=Number(M);if(!Number.isInteger(z)||z<=0)throw new R(`zhihu collection --${D} must be a positive integer`,"Example: ppcli zhihu collection 83283292 --limit 20");return z}function C(M,D){const z=Number(M);if(!Number.isInteger(z)||z<0)throw new R(`zhihu collection --${D} must be a non-negative integer`,"Example: ppcli zhihu collection 83283292 --offset 0");return z}async function O(M,D,z,T){const V=`https://www.zhihu.com/api/v4/collections/${D}/items?offset=${z}&limit=${T}`,G=await M.evaluate(`
|
|
2
2
|
(async () => {
|
|
3
3
|
const r = await fetch(${JSON.stringify(V)}, { credentials: 'include' });
|
|
4
4
|
if (!r.ok) return { __httpError: r.status };
|
|
5
5
|
return await r.json();
|
|
6
6
|
})()
|
|
7
|
-
`);if(!G||G.__httpError){const B=G?.__httpError;if(B===401||B===403)throw new L("www.zhihu.com","Failed to fetch collection data from Zhihu. Please ensure you are logged in.");throw new j(B?`Zhihu collection request failed (HTTP ${B})`:"Zhihu collection request failed","Try again later or rerun with -v for more detail")}return G}function P(M){const D=M?.content||{};return`${D.type||""}:${D.id||D.url||JSON.stringify(D).slice(0,80)}`}function h(M,D){const z=M.content||{},T=z.type||"";if(!["answer","article","pin"].includes(T))throw new j(`Zhihu collection returned unsupported content type: ${T||"missing"}`,"Collection items require a supported content.type so the row identity, title, and URL are not silently blank.");let V="",G="",B="",Q="",W=0;if(T==="answer"){const J=z.question||{};V=J.title||"";G=_(z.content||"").substring(0,150);B=z.url||`https://www.zhihu.com/question/${J.id}/answer/${z.id}`;Q=z.author?.name||"匿名用户";W=z.voteup_count||0}else if(T==="article"){V=z.title||"";G=_(z.content||"").substring(0,150);B=z.url||`https://zhuanlan.zhihu.com/p/${z.id}`;Q=z.author?.name||"匿名用户";W=z.voteup_count||0}else if(T==="pin"){V="想法";G=_((z.content||[]).map((J)=>J.content||"").join(" ")).substring(0,150);B=z.url||`https://www.zhihu.com/pin/${z.id}`;Q=z.author?.name||"匿名用户";W=z.reaction_count||0}if(!String(V||"").trim()||!String(B||"").trim()||B.includes("undefined"))throw new j("Zhihu collection returned a malformed item without title or URL identity","Collection item rows require type, title, and URL so malformed payloads do not become blank listing rows.");return{rank:D,type:T,title:_(V).substring(0,100),author:Q,votes:W,excerpt:G,url:B}}y({site:"zhihu",name:"collection",access:"read",description:"知乎收藏夹内容列表(需要登录)",domain:"www.zhihu.com",strategy:I.COOKIE,browser:!0,args:[{name:"id",positional:!0,required:!0,help:"收藏夹 ID (数字,可从收藏夹 URL 中获取)"},{name:"offset",type:"int",default:0,help:"起始偏移量(用于分页)"},{name:"limit",type:"int",default:20,help:"每页数量(最大 20)"}],columns:["rank","type","title","author","votes","excerpt","url"],func:async(M,D)=>{const{id:z,offset:T=0,limit:V=20}=D,G=String(z);if(!/^\d+$/.test(G))throw new R("Collection ID must be numeric","Example:
|
|
7
|
+
`);if(!G||G.__httpError){const B=G?.__httpError;if(B===401||B===403)throw new L("www.zhihu.com","Failed to fetch collection data from Zhihu. Please ensure you are logged in.");throw new j(B?`Zhihu collection request failed (HTTP ${B})`:"Zhihu collection request failed","Try again later or rerun with -v for more detail")}return G}function P(M){const D=M?.content||{};return`${D.type||""}:${D.id||D.url||JSON.stringify(D).slice(0,80)}`}function h(M,D){const z=M.content||{},T=z.type||"";if(!["answer","article","pin"].includes(T))throw new j(`Zhihu collection returned unsupported content type: ${T||"missing"}`,"Collection items require a supported content.type so the row identity, title, and URL are not silently blank.");let V="",G="",B="",Q="",W=0;if(T==="answer"){const J=z.question||{};V=J.title||"";G=_(z.content||"").substring(0,150);B=z.url||`https://www.zhihu.com/question/${J.id}/answer/${z.id}`;Q=z.author?.name||"匿名用户";W=z.voteup_count||0}else if(T==="article"){V=z.title||"";G=_(z.content||"").substring(0,150);B=z.url||`https://zhuanlan.zhihu.com/p/${z.id}`;Q=z.author?.name||"匿名用户";W=z.voteup_count||0}else if(T==="pin"){V="想法";G=_((z.content||[]).map((J)=>J.content||"").join(" ")).substring(0,150);B=z.url||`https://www.zhihu.com/pin/${z.id}`;Q=z.author?.name||"匿名用户";W=z.reaction_count||0}if(!String(V||"").trim()||!String(B||"").trim()||B.includes("undefined"))throw new j("Zhihu collection returned a malformed item without title or URL identity","Collection item rows require type, title, and URL so malformed payloads do not become blank listing rows.");return{rank:D,type:T,title:_(V).substring(0,100),author:Q,votes:W,excerpt:G,url:B}}y({site:"zhihu",name:"collection",access:"read",description:"知乎收藏夹内容列表(需要登录)",domain:"www.zhihu.com",strategy:I.COOKIE,browser:!0,args:[{name:"id",positional:!0,required:!0,help:"收藏夹 ID (数字,可从收藏夹 URL 中获取)"},{name:"offset",type:"int",default:0,help:"起始偏移量(用于分页)"},{name:"limit",type:"int",default:20,help:"每页数量(最大 20)"}],columns:["rank","type","title","author","votes","excerpt","url"],func:async(M,D)=>{const{id:z,offset:T=0,limit:V=20}=D,G=String(z);if(!/^\d+$/.test(G))throw new R("Collection ID must be numeric","Example: ppcli zhihu collection 83283292");const B=C(T,"offset"),Q=H(V,"limit"),W=Math.min(Q,20);await M.goto("https://www.zhihu.com");const J=[],A=new Set;let X=0,Y=B;const q=Math.ceil(Q/W)+2;for(let $=0;$<q&&J.length<Q;$+=1){const S=Math.min(W,Q-J.length),K=await O(M,G,Y,S),U=Array.isArray(K.data)?K.data:[],b=K.paging||{};X=Number(b.totals||X||0);for(const F of U){const Z=P(F);if(!A.has(Z)){A.add(Z);J.push(F)}if(J.length>=Q)break}if(U.length===0||b.is_end||J.length>=Q)break;if(typeof b.next==="string")try{const F=new URL(b.next),Z=Number(F.searchParams.get("offset"));if(Number.isInteger(Z)&&Z>Y){Y=Z;continue}}catch{}if(U.length<S)break;const N=Y+U.length;if(N<=Y)break;Y=N;if(X&&Y>=X)break}const k=Math.ceil(X/W),E=Math.floor(B/W)+1;if(X>0){w.info(`收藏夹共有 ${X} 条内容,共 ${k} 页`);w.info(`当前第 ${E} 页,显示第 ${B+1} - ${Math.min(B+J.length,X)} 条`)}if(J.length===0)throw new v("zhihu collection",`No items found for collection ${G}. The collection may be empty, private, or the offset may be out of range.`);return J.slice(0,Q).map(($,S)=>h($,B+S+1))}});export const __test__={stripHtml:_,validatePositiveInt:H,validateNonNegativeInt:C,itemKey:P,mapCollectionItem:h};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as P,Strategy as I}from"@jackwener/opencli/registry";import{ArgumentError as h,AuthRequiredError as w,CommandExecutionError as F,EmptyResultError as O}from"@jackwener/opencli/errors";import{log as v}from"@jackwener/opencli/logger";function J(B,V){const D=Number(B);if(!Number.isInteger(D)||D<=0)throw new h(`zhihu collections --${V} must be a positive integer`,"Example:
|
|
1
|
+
import{cli as P,Strategy as I}from"@jackwener/opencli/registry";import{ArgumentError as h,AuthRequiredError as w,CommandExecutionError as F,EmptyResultError as O}from"@jackwener/opencli/errors";import{log as v}from"@jackwener/opencli/logger";function J(B,V){const D=Number(B);if(!Number.isInteger(D)||D<=0)throw new h(`zhihu collections --${V} must be a positive integer`,"Example: ppcli zhihu collections --limit 20");return D}async function K(B,V,D){const z=await B.evaluate(`
|
|
2
2
|
(async () => {
|
|
3
3
|
const r = await fetch(${JSON.stringify(V)}, { credentials: 'include' });
|
|
4
4
|
if (!r.ok) return { __httpError: r.status };
|
package/clis/zhihu/question.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as H,Strategy as K}from"@jackwener/opencli/registry";import{AuthRequiredError as R,CliError as W}from"@jackwener/opencli/errors";import{stripHtml as S}from"./text.js";function f(b){if(typeof b!=="string")return"";try{const z=new URL(b);if(z.hostname!=="www.zhihu.com"&&z.hostname!=="zhihu.com")return"";return z.pathname.match(/^\/question\/\d+\/answer\/(\d+)\/?$/)?.[1]||z.pathname.match(/^\/api\/v4\/answers\/(\d+)\/?$/)?.[1]||z.pathname.match(/^\/answer\/(\d+)\/?$/)?.[1]||""}catch{return""}}function $(b){const z=f(b.url);if(z)return z;if(typeof b.id==="string"&&/^\d+$/.test(b.id))return b.id;if(typeof b.id==="number"&&Number.isSafeInteger(b.id)&&b.id>0)return String(b.id);return""}function h(b){const z=$(b);if(z)return`id:${z}`;return`fallback:${b.author?.name||"anonymous"}:${b.content||""}`}const x=1000;H({site:"zhihu",name:"question",access:"read",description:"知乎问题详情和回答",domain:"www.zhihu.com",strategy:K.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"],func:async(b,z)=>{const{id:D,limit:F=5}=z,N=String(D);if(!/^\d+$/.test(N))throw new W("INVALID_INPUT","Question ID must be numeric","Example:
|
|
1
|
+
import{cli as H,Strategy as K}from"@jackwener/opencli/registry";import{AuthRequiredError as R,CliError as W}from"@jackwener/opencli/errors";import{stripHtml as S}from"./text.js";function f(b){if(typeof b!=="string")return"";try{const z=new URL(b);if(z.hostname!=="www.zhihu.com"&&z.hostname!=="zhihu.com")return"";return z.pathname.match(/^\/question\/\d+\/answer\/(\d+)\/?$/)?.[1]||z.pathname.match(/^\/api\/v4\/answers\/(\d+)\/?$/)?.[1]||z.pathname.match(/^\/answer\/(\d+)\/?$/)?.[1]||""}catch{return""}}function $(b){const z=f(b.url);if(z)return z;if(typeof b.id==="string"&&/^\d+$/.test(b.id))return b.id;if(typeof b.id==="number"&&Number.isSafeInteger(b.id)&&b.id>0)return String(b.id);return""}function h(b){const z=$(b);if(z)return`id:${z}`;return`fallback:${b.author?.name||"anonymous"}:${b.content||""}`}const x=1000;H({site:"zhihu",name:"question",access:"read",description:"知乎问题详情和回答",domain:"www.zhihu.com",strategy:K.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"],func:async(b,z)=>{const{id:D,limit:F=5}=z,N=String(D);if(!/^\d+$/.test(N))throw new W("INVALID_INPUT","Question ID must be numeric","Example: ppcli zhihu question 123456789");const O=Number(F);if(!Number.isInteger(O)||O<=0||O>x)throw new W("INVALID_INPUT",`Limit must be a positive integer no greater than ${x}`,"Use a normal-sized limit to avoid slow requests or Zhihu risk controls");const Q=String(z.sort||"default");if(Q!=="default"&&Q!=="created")throw new W("INVALID_INPUT","Sort must be one of: default, created","Example: ppcli zhihu question 123456789 --sort created");await b.goto(Q==="created"?`https://www.zhihu.com/question/${N}/answers/updated`:`https://www.zhihu.com/question/${N}`);let P=`https://www.zhihu.com/api/v4/questions/${N}/answers?limit=${20}&offset=0&sort_by=${Q}&include=data[*].content,url,voteup_count,comment_count,author`;const V=[],Y=new Set,Z=new Set;while(P&&V.length<O&&!Z.has(P)){Z.add(P);const B=await b.evaluate(`
|
|
2
2
|
(async () => {
|
|
3
3
|
const r = await fetch(${JSON.stringify(P)}, { credentials: 'include' });
|
|
4
4
|
if (!r.ok) return { __httpError: r.status };
|