@geoly-ai/social-hub-cli 0.3.45 → 0.3.47
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/CHANGELOG.md +22 -0
- package/dist/cmd-manifest.json +8 -2
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +236 -3
- package/dist/index.js.map +1 -1
- package/dist/public-reddit.test.d.ts +2 -0
- package/dist/public-reddit.test.d.ts.map +1 -0
- package/dist/public-reddit.test.js +339 -0
- package/dist/public-reddit.test.js.map +1 -0
- package/dist/reddit-account-type-filter.test.d.ts +2 -0
- package/dist/reddit-account-type-filter.test.d.ts.map +1 -0
- package/dist/reddit-account-type-filter.test.js +92 -0
- package/dist/reddit-account-type-filter.test.js.map +1 -0
- package/dist/register-extensions.d.ts.map +1 -1
- package/dist/register-extensions.js +2 -0
- package/dist/register-extensions.js.map +1 -1
- package/dist/register-subreddit-assets.d.ts.map +1 -1
- package/dist/register-subreddit-assets.js +43 -0
- package/dist/register-subreddit-assets.js.map +1 -1
- package/dist/register-subreddit-assets.test.js +4 -1
- package/dist/register-subreddit-assets.test.js.map +1 -1
- package/package.json +3 -3
- package/skills/integrity.json +4 -4
- package/skills/manifest.json +1 -1
- package/skills/social-hub-admin/SKILL.md +6 -0
- package/skills/social-hub-intelligence/SKILL.md +111 -0
- package/skills/social-hub-posts/SKILL.md +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to `@geoly-ai/social-hub-cli` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
Release workflow: `pnpm release:cli` → `pnpm publish:cli`.
|
|
6
6
|
|
|
7
|
+
## [0.3.47] - 2026-10-22
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- owned subreddit weekly visitors/contributions column, trend, refresh + skills
|
|
12
|
+
|
|
13
|
+
## [0.3.46] - 2026-10-20
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- add internal/external account type filter across list, stats, refresh, CLI, SDK
|
|
18
|
+
- public-reddit read methods and intelligence commands (B5)
|
|
19
|
+
- archive-observations 评论来源退役为 410 ARCHIVE_SOURCE_RETIRED,评论列表加范围说明
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- 系统级 users/members 列表的 role 改为与登录同口径的有效角色
|
|
24
|
+
|
|
25
|
+
### Documentation
|
|
26
|
+
|
|
27
|
+
- public-reddit read commands usage (B6)
|
|
28
|
+
|
|
7
29
|
## [0.3.45] - 2026-09-22
|
|
8
30
|
|
|
9
31
|
### Added
|
package/dist/cmd-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-
|
|
3
|
-
"cliVersion": "0.3.
|
|
2
|
+
"generatedAt": "2026-10-22T03:31:57.523Z",
|
|
3
|
+
"cliVersion": "0.3.47",
|
|
4
4
|
"commands": [
|
|
5
5
|
"accounts",
|
|
6
6
|
"accounts brand-bindings",
|
|
@@ -282,6 +282,10 @@
|
|
|
282
282
|
"intelligence monitors-update",
|
|
283
283
|
"intelligence opportunity-map",
|
|
284
284
|
"intelligence posts-import",
|
|
285
|
+
"intelligence public-communities",
|
|
286
|
+
"intelligence public-post-count",
|
|
287
|
+
"intelligence public-posts",
|
|
288
|
+
"intelligence public-status",
|
|
285
289
|
"intelligence runs-list",
|
|
286
290
|
"intelligence sentiment-intent",
|
|
287
291
|
"intelligence snapshot-latest",
|
|
@@ -507,9 +511,11 @@
|
|
|
507
511
|
"subreddit-asset protection",
|
|
508
512
|
"subreddit-asset protection get",
|
|
509
513
|
"subreddit-asset protection set",
|
|
514
|
+
"subreddit-asset refresh-weekly-stats",
|
|
510
515
|
"subreddit-asset register",
|
|
511
516
|
"subreddit-asset register-subreddit",
|
|
512
517
|
"subreddit-asset set-status",
|
|
518
|
+
"subreddit-asset weekly-stats",
|
|
513
519
|
"users",
|
|
514
520
|
"users create",
|
|
515
521
|
"users list",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import { type SocialHubClient } from "@geoly-ai/social-hub-sdk";
|
|
3
4
|
declare const program: Command;
|
|
4
5
|
/** 返回放错位置字段的指引(无则 null)。用于 accounts update / sync core 防呆。 */
|
|
5
6
|
export declare function detectMisplacedAccountFields(body: Record<string, unknown>): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* `--subreddit a,b` -> ["a","b"]:逗号分隔、trim、去空项、去重(保序)。
|
|
9
|
+
* 空结果由调用方拒绝(不能把「没给板块」悄悄变成不带板块的请求)。
|
|
10
|
+
*/
|
|
11
|
+
export declare function parsePublicRedditSubreddits(raw: string): string[];
|
|
12
|
+
/**
|
|
13
|
+
* 把「公开库错误」格式化成 stderr 文本;不是公开库错误返回 null(调用方原样重抛,
|
|
14
|
+
* 交给默认错误边界)。🔴 鸭子类型:跨包副本的 `instanceof SocialHubHttpError` 会假阴性
|
|
15
|
+
* (与 output.ts `restrictionBlockFromError` 同理),CLI 测试整包 mock SDK 时更是如此。
|
|
16
|
+
*/
|
|
17
|
+
export declare function formatPublicRedditError(e: unknown): string | null;
|
|
18
|
+
export type PublicRedditCliClient = Pick<SocialHubClient, "listPublicRedditCommunities" | "searchPublicRedditPosts" | "countPublicRedditPosts" | "getPublicRedditStatus">;
|
|
19
|
+
type PublicRedditWindowOpts = {
|
|
20
|
+
window?: string;
|
|
21
|
+
from?: string;
|
|
22
|
+
to?: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function runPublicCommunities(client: PublicRedditCliClient, opts: {
|
|
25
|
+
industryKeys?: string;
|
|
26
|
+
tierKeys?: string;
|
|
27
|
+
poolKinds?: string;
|
|
28
|
+
hasProfile?: string;
|
|
29
|
+
limit?: string;
|
|
30
|
+
cursor?: string;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
export declare function runPublicPosts(client: PublicRedditCliClient, opts: PublicRedditWindowOpts & {
|
|
33
|
+
subreddit: string;
|
|
34
|
+
query?: string;
|
|
35
|
+
limit?: string;
|
|
36
|
+
cursor?: string;
|
|
37
|
+
includeBody?: boolean;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
export declare function runPublicPostCount(client: PublicRedditCliClient, opts: PublicRedditWindowOpts & {
|
|
40
|
+
subreddit: string;
|
|
41
|
+
query: string;
|
|
42
|
+
type?: string;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
export declare function runPublicStatus(client: PublicRedditCliClient): Promise<void>;
|
|
6
45
|
export type VocAcquireClient = {
|
|
7
46
|
vocQuery(body: unknown): Promise<Record<string, unknown>>;
|
|
8
47
|
getIntelRunProgress(runId: string): Promise<{
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import { registerDoctorAndVersion } from "./register-shared.js";
|
|
|
20
20
|
import { registerSkillsCommands } from "./register-skills.js";
|
|
21
21
|
import { readJsonArgOrExit, resolveJsonArg } from "./lib/json-arg.js";
|
|
22
22
|
import { applyContentReviewFlags, AUTHOR_RELATION_MISMATCH_HINT, ContentReviewArgError, } from "./lib/content-review-args.js";
|
|
23
|
-
import { runCliProgram, withCliErrorBoundary } from "./output.js";
|
|
23
|
+
import { runCliProgram, withCliErrorBoundary, writeStderrSync, } from "./output.js";
|
|
24
24
|
import { maybeNotifyUpdate } from "./update-check.js";
|
|
25
25
|
import { maybeNotifySkillUpdate } from "./skill-update-check.js";
|
|
26
26
|
import { getCliVersion } from "./version-info.js";
|
|
@@ -370,6 +370,7 @@ reddit
|
|
|
370
370
|
.option("--draft-status <st>", "draftStatus filter (pending|drafted|reviewed|published|unknown)")
|
|
371
371
|
.option("--post-type <t>", "postType filter")
|
|
372
372
|
.option("--repost-type <t>", "repostType filter (none|first|second)")
|
|
373
|
+
.option("--account-type <t>", "账号类型筛选:internal|external(可逗号分隔);proxy/海外兼职/未知/空值在选了任一项时不命中")
|
|
373
374
|
.option("--contains-brand-keyword <v>", "true | false | unset")
|
|
374
375
|
.option("--primary-product <text>", "primary product contains match")
|
|
375
376
|
.option("--seq-no-min <n>", "序列号下界(含);与 --seq-no-max 组成闭区间,无编号(null)的帖会落选")
|
|
@@ -391,6 +392,7 @@ reddit
|
|
|
391
392
|
draftStatus: opts.draftStatus,
|
|
392
393
|
postType: opts.postType,
|
|
393
394
|
repostType: opts.repostType,
|
|
395
|
+
accountType: opts.accountType,
|
|
394
396
|
containsBrandKeyword: opts.containsBrandKeyword,
|
|
395
397
|
primaryProduct: opts.primaryProduct,
|
|
396
398
|
seqNoMin: opts.seqNoMin !== undefined ? Number(opts.seqNoMin) : undefined,
|
|
@@ -2460,6 +2462,191 @@ pools
|
|
|
2460
2462
|
const res = await requireClient().deleteSubredditPoolTierRule(resolveTeamId(opts.team), opts.id);
|
|
2461
2463
|
console.log(JSON.stringify(res, null, 2));
|
|
2462
2464
|
});
|
|
2465
|
+
// ── 公开 Reddit 只读读路径(intelligence public-*;B5)────────────────────────────────
|
|
2466
|
+
/** 与 contracts `PUBLIC_REDDIT_SEARCH_MAX_COMMUNITIES` / `…COUNT_MAX_COMMUNITIES` 同值。 */
|
|
2467
|
+
const PUBLIC_REDDIT_SEARCH_MAX = 20;
|
|
2468
|
+
const PUBLIC_REDDIT_COUNT_MAX = 50;
|
|
2469
|
+
/**
|
|
2470
|
+
* `--subreddit a,b` -> ["a","b"]:逗号分隔、trim、去空项、去重(保序)。
|
|
2471
|
+
* 空结果由调用方拒绝(不能把「没给板块」悄悄变成不带板块的请求)。
|
|
2472
|
+
*/
|
|
2473
|
+
export function parsePublicRedditSubreddits(raw) {
|
|
2474
|
+
const out = [];
|
|
2475
|
+
for (const part of raw.split(",")) {
|
|
2476
|
+
const v = part.trim();
|
|
2477
|
+
if (v && !out.includes(v))
|
|
2478
|
+
out.push(v);
|
|
2479
|
+
}
|
|
2480
|
+
return out;
|
|
2481
|
+
}
|
|
2482
|
+
/** 本地校验失败:写 stderr、退出码 1(服务端同样会 400,这里省一次上游预算与往返)。 */
|
|
2483
|
+
class PublicRedditUsageError extends Error {
|
|
2484
|
+
}
|
|
2485
|
+
function requirePublicRedditSubreddits(raw, max) {
|
|
2486
|
+
const list = parsePublicRedditSubreddits(raw);
|
|
2487
|
+
if (list.length === 0) {
|
|
2488
|
+
throw new PublicRedditUsageError("--subreddit 至少需要一个板块(逗号分隔)");
|
|
2489
|
+
}
|
|
2490
|
+
if (list.length > max) {
|
|
2491
|
+
throw new PublicRedditUsageError(`--subreddit 最多 ${max} 个板块(收到 ${list.length} 个),请拆分后分别查询`);
|
|
2492
|
+
}
|
|
2493
|
+
return list;
|
|
2494
|
+
}
|
|
2495
|
+
const PUBLIC_REDDIT_NOT_NO_DATA = "这不是「没有数据」:本次读取没有成功完成,结果未知,不能当作空结果。";
|
|
2496
|
+
/** 已知错误码的明确文案;未知的 PUBLIC_REDDIT_* 走兜底(同样不是空结果)。 */
|
|
2497
|
+
const PUBLIC_REDDIT_ERROR_TEXT = {
|
|
2498
|
+
PUBLIC_REDDIT_DISABLED: "公开库检索当前对本次请求未启用(服务端开关为 off,或 shadow 模式下当前身份无权读取)。",
|
|
2499
|
+
PUBLIC_REDDIT_POLICY_DENIED: "上游数据服务拒绝了本次读取(策略拒绝,通常是准入/范围问题,需要管理员处理)。",
|
|
2500
|
+
PUBLIC_REDDIT_AUTH_FAILED: "上游数据服务的凭据校验失败(key 过期或已轮换),需要管理员处理。",
|
|
2501
|
+
PUBLIC_REDDIT_BUDGET_EXHAUSTED: "Hub 对上游的调用预算已用完,请稍后再试。",
|
|
2502
|
+
PUBLIC_REDDIT_RATE_LIMITED: "上游数据服务限流,请稍后再试,不要轮询。",
|
|
2503
|
+
PUBLIC_REDDIT_DEGRADED: "上游数据服务暂时降级,请稍后再试。",
|
|
2504
|
+
PUBLIC_REDDIT_UNAVAILABLE: "上游数据服务暂时不可用,请稍后再试。",
|
|
2505
|
+
PUBLIC_REDDIT_COUNT_TIMEOUT: "计数超时:请缩小板块范围或按月切分窗口后重试(不要原样重试)。",
|
|
2506
|
+
PUBLIC_REDDIT_BAD_UPSTREAM_RESPONSE: "上游返回了无法解析的响应(契约漂移),需要管理员处理。",
|
|
2507
|
+
PUBLIC_REDDIT_COMMUNITY_NOT_AVAILABLE: "指定板块不在公开库可读范围内(板块未被上游准入),不代表该板块没有帖子。",
|
|
2508
|
+
PUBLIC_REDDIT_SCOPE_TOO_LARGE: "查询范围过大(板块 × 窗口超出上游成本上限):请减少板块或按月切分窗口后分别查询。",
|
|
2509
|
+
PUBLIC_REDDIT_INVALID_WINDOW: "窗口无效:检查 --window / --from / --to(互斥、from<to、跨度 ≤366 天)。",
|
|
2510
|
+
PUBLIC_REDDIT_QUERY_REQUIRED: "该读取必须带 --query。",
|
|
2511
|
+
PUBLIC_REDDIT_INVALID_COMMUNITY: "板块名无效(小写 3–21 位,不带 r/ 或 u_ 前缀)。",
|
|
2512
|
+
PUBLIC_REDDIT_INVALID_REQUEST: "请求参数被服务端拒绝,详见下方 error.message。",
|
|
2513
|
+
PUBLIC_REDDIT_CURSOR_RESET: "分页游标已失效,请去掉 --cursor 从第一页重新查询。",
|
|
2514
|
+
PUBLIC_REDDIT_CURSOR_MISMATCH: "--cursor 与当前查询条件不匹配,请使用同一组条件翻页。",
|
|
2515
|
+
};
|
|
2516
|
+
/**
|
|
2517
|
+
* 把「公开库错误」格式化成 stderr 文本;不是公开库错误返回 null(调用方原样重抛,
|
|
2518
|
+
* 交给默认错误边界)。🔴 鸭子类型:跨包副本的 `instanceof SocialHubHttpError` 会假阴性
|
|
2519
|
+
* (与 output.ts `restrictionBlockFromError` 同理),CLI 测试整包 mock SDK 时更是如此。
|
|
2520
|
+
*/
|
|
2521
|
+
export function formatPublicRedditError(e) {
|
|
2522
|
+
if (!(e instanceof Error))
|
|
2523
|
+
return null;
|
|
2524
|
+
const { code, status, details, retryAfterSeconds } = e;
|
|
2525
|
+
if (typeof code !== "string" || !code.startsWith("PUBLIC_REDDIT_")) {
|
|
2526
|
+
return null;
|
|
2527
|
+
}
|
|
2528
|
+
const text = PUBLIC_REDDIT_ERROR_TEXT[code] ?? "公开库读取失败(服务端返回了错误码)。";
|
|
2529
|
+
const lines = [`[${code}] ${text}`, PUBLIC_REDDIT_NOT_NO_DATA];
|
|
2530
|
+
if (typeof retryAfterSeconds === "number") {
|
|
2531
|
+
lines.push(`Retry-After: ${retryAfterSeconds}s`);
|
|
2532
|
+
}
|
|
2533
|
+
lines.push(JSON.stringify({
|
|
2534
|
+
error: {
|
|
2535
|
+
status: typeof status === "number" ? status : undefined,
|
|
2536
|
+
code,
|
|
2537
|
+
message: e.message,
|
|
2538
|
+
details,
|
|
2539
|
+
retryable: false,
|
|
2540
|
+
},
|
|
2541
|
+
}));
|
|
2542
|
+
return lines.join("\n");
|
|
2543
|
+
}
|
|
2544
|
+
/**
|
|
2545
|
+
* 公开库命令的统一外壳:本地用法错误与公开库错误 -> stderr + 退出码 1;
|
|
2546
|
+
* 其余错误原样重抛(走默认边界)。绝不把错误转成空结果/退出码 0。
|
|
2547
|
+
*/
|
|
2548
|
+
async function runPublicRedditCommand(run) {
|
|
2549
|
+
try {
|
|
2550
|
+
await run();
|
|
2551
|
+
}
|
|
2552
|
+
catch (e) {
|
|
2553
|
+
if (e instanceof PublicRedditUsageError) {
|
|
2554
|
+
writeStderrSync(e.message);
|
|
2555
|
+
process.exit(1);
|
|
2556
|
+
}
|
|
2557
|
+
const text = formatPublicRedditError(e);
|
|
2558
|
+
if (text === null)
|
|
2559
|
+
throw e;
|
|
2560
|
+
writeStderrSync(text);
|
|
2561
|
+
process.exit(1);
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
/** 成功输出:stdout 永远是完整 JSON;0 条时 stderr 另给事实性提示(退出码仍为 0)。 */
|
|
2565
|
+
function printPublicRedditResult(res) {
|
|
2566
|
+
console.log(JSON.stringify(res, null, 2));
|
|
2567
|
+
if (Array.isArray(res.items) && res.items.length === 0) {
|
|
2568
|
+
const excluded = res.source?.excludedCommunities ?? [];
|
|
2569
|
+
writeStderrSync(excluded.length > 0
|
|
2570
|
+
? `无结果(0 条)。注意:${excluded.length} 个板块被排除未查询(${excluded
|
|
2571
|
+
.map((x) => x.communityId)
|
|
2572
|
+
.join(",")}),空结果不代表这些板块没有数据,详见 source.excludedCommunities。`
|
|
2573
|
+
: "无结果(0 条)。");
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
function windowParams(opts) {
|
|
2577
|
+
// 枚举与互斥由服务端 zod 400(与本组 --sort 的透传惯例一致),这里只转发。
|
|
2578
|
+
return {
|
|
2579
|
+
window: opts.window,
|
|
2580
|
+
from: opts.from,
|
|
2581
|
+
to: opts.to,
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
export async function runPublicCommunities(client, opts) {
|
|
2585
|
+
await runPublicRedditCommand(async () => {
|
|
2586
|
+
let hasProfile;
|
|
2587
|
+
if (opts.hasProfile !== undefined) {
|
|
2588
|
+
// Boolean 强转类参数本地拦(见 monitors-list --enabled 先例),避免 "yes" 被悄悄当成 false。
|
|
2589
|
+
if (opts.hasProfile !== "true" && opts.hasProfile !== "false") {
|
|
2590
|
+
throw new PublicRedditUsageError("--has-profile 只接受 true|false");
|
|
2591
|
+
}
|
|
2592
|
+
hasProfile = opts.hasProfile === "true";
|
|
2593
|
+
}
|
|
2594
|
+
const res = await client.listPublicRedditCommunities({
|
|
2595
|
+
industryKey: opts.industryKeys,
|
|
2596
|
+
tierKey: opts.tierKeys,
|
|
2597
|
+
poolKind: opts.poolKinds,
|
|
2598
|
+
hasProfile,
|
|
2599
|
+
limit: opts.limit === undefined ? undefined : Number(opts.limit),
|
|
2600
|
+
cursor: opts.cursor,
|
|
2601
|
+
});
|
|
2602
|
+
printPublicRedditResult(res);
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
export async function runPublicPosts(client, opts) {
|
|
2606
|
+
await runPublicRedditCommand(async () => {
|
|
2607
|
+
const subreddit = requirePublicRedditSubreddits(opts.subreddit, PUBLIC_REDDIT_SEARCH_MAX);
|
|
2608
|
+
const res = await client.searchPublicRedditPosts({
|
|
2609
|
+
subreddit,
|
|
2610
|
+
query: opts.query,
|
|
2611
|
+
...windowParams(opts),
|
|
2612
|
+
limit: opts.limit === undefined ? undefined : Number(opts.limit),
|
|
2613
|
+
cursor: opts.cursor,
|
|
2614
|
+
includeBody: opts.includeBody ? true : undefined,
|
|
2615
|
+
});
|
|
2616
|
+
printPublicRedditResult(res);
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
export async function runPublicPostCount(client, opts) {
|
|
2620
|
+
await runPublicRedditCommand(async () => {
|
|
2621
|
+
// 本地先拦:count 是昂贵读,服务端同样会 400,但不该为此白花一次往返与上游预算。
|
|
2622
|
+
const subreddits = requirePublicRedditSubreddits(opts.subreddit, PUBLIC_REDDIT_COUNT_MAX);
|
|
2623
|
+
const query = opts.query?.trim();
|
|
2624
|
+
if (!query) {
|
|
2625
|
+
throw new PublicRedditUsageError("--query 必填且不能为空(对方生产不接受无 query 的计数)");
|
|
2626
|
+
}
|
|
2627
|
+
if (!opts.window && !opts.from && !opts.to) {
|
|
2628
|
+
throw new PublicRedditUsageError("计数必须给窗口:--window <day|week|month|last_90d|last_360d|last_full_week> 或 --from + --to");
|
|
2629
|
+
}
|
|
2630
|
+
if (opts.type !== undefined &&
|
|
2631
|
+
opts.type !== "post" &&
|
|
2632
|
+
opts.type !== "comment") {
|
|
2633
|
+
throw new PublicRedditUsageError("--type 只接受 post|comment");
|
|
2634
|
+
}
|
|
2635
|
+
const res = await client.countPublicRedditPosts({
|
|
2636
|
+
subreddits,
|
|
2637
|
+
query,
|
|
2638
|
+
artifactType: opts.type,
|
|
2639
|
+
...windowParams(opts),
|
|
2640
|
+
});
|
|
2641
|
+
// 计数响应没有 items:不走空页提示。exact=false 时 total 为 null,原样输出。
|
|
2642
|
+
console.log(JSON.stringify(res, null, 2));
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
export async function runPublicStatus(client) {
|
|
2646
|
+
await runPublicRedditCommand(async () => {
|
|
2647
|
+
console.log(JSON.stringify(await client.getPublicRedditStatus(), null, 2));
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2463
2650
|
const intel = program
|
|
2464
2651
|
.command("intelligence")
|
|
2465
2652
|
.description("板块情报与 KOL 挖掘");
|
|
@@ -2508,7 +2695,7 @@ intel
|
|
|
2508
2695
|
});
|
|
2509
2696
|
intel
|
|
2510
2697
|
.command("comments")
|
|
2511
|
-
.description("GET /v1/system/subreddit-intelligence/comments(评论样本查询:postId/subreddit/keyword/intent/sentiment)")
|
|
2698
|
+
.description("GET /v1/system/subreddit-intelligence/comments(评论样本查询:postId/subreddit/keyword/intent/sentiment;只含 Hub 内现有评论样本(非归档,约 5.7 万条),历史/归档评论已迁至 CyberRedd)")
|
|
2512
2699
|
.option("--post-id <id>", "t3_xxx")
|
|
2513
2700
|
.option("--subreddit <name>", "板块名")
|
|
2514
2701
|
.option("--keyword <kw>", "正文关键词")
|
|
@@ -2532,7 +2719,7 @@ intel
|
|
|
2532
2719
|
});
|
|
2533
2720
|
intel
|
|
2534
2721
|
.command("comments-count")
|
|
2535
|
-
.description("GET /v1/system/subreddit-intelligence/comments/count(评论样本精确总数:不封顶、无分页;不接受 keyword
|
|
2722
|
+
.description("GET /v1/system/subreddit-intelligence/comments/count(评论样本精确总数:不封顶、无分页;不接受 keyword;昂贵读,禁止轮询;只含 Hub 内现有评论样本(非归档,约 5.7 万条),历史/归档评论已迁至 CyberRedd)")
|
|
2536
2723
|
.option("--post-id <id>", "t3_xxx")
|
|
2537
2724
|
.option("--subreddit <name>", "板块名")
|
|
2538
2725
|
.option("--intent <intent>", "意图标签")
|
|
@@ -3556,6 +3743,50 @@ intel
|
|
|
3556
3743
|
});
|
|
3557
3744
|
console.log(JSON.stringify(res, null, 2));
|
|
3558
3745
|
});
|
|
3746
|
+
intel
|
|
3747
|
+
.command("public-communities")
|
|
3748
|
+
.description("GET /v1/system/public-reddit/communities(公开库社区目录,只读;DISABLED/POLICY_DENIED 等错误码 = 读取失败,不是没有数据)")
|
|
3749
|
+
.option("--industry-keys <csv>", "行业 key,逗号分隔")
|
|
3750
|
+
.option("--tier-keys <csv>", "tier key,逗号分隔")
|
|
3751
|
+
.option("--pool-kinds <csv>", "primary|secondary|experimental|excluded,逗号分隔")
|
|
3752
|
+
.option("--has-profile <bool>", "true|false")
|
|
3753
|
+
.option("-n, --limit <n>", "每页条数(缺省用服务端默认)")
|
|
3754
|
+
.option("--cursor <token>", "原样回传上一页的 nextCursor")
|
|
3755
|
+
.action(async (opts) => {
|
|
3756
|
+
await runPublicCommunities(requireClient(), opts);
|
|
3757
|
+
});
|
|
3758
|
+
intel
|
|
3759
|
+
.command("public-posts")
|
|
3760
|
+
.description("GET /v1/system/public-reddit/posts(公开库帖子检索,只读;必须带 --subreddit,单次 ≤20 个;--query 会发往外部数据服务;DISABLED/POLICY_DENIED 等错误码 = 读取失败,不是没有数据)")
|
|
3761
|
+
.requiredOption("--subreddit <a,b>", "板块,逗号分隔(小写,不带 r/)")
|
|
3762
|
+
.option("--query <text>", "全文关键词(⚠️ 会发往外部数据服务)")
|
|
3763
|
+
.option("--window <name>", "day|week|month|last_90d|last_360d|last_full_week(与 --from/--to 互斥)")
|
|
3764
|
+
.option("--from <iso>", "窗口起(含),ISO 8601 带时区")
|
|
3765
|
+
.option("--to <iso>", "窗口止(不含),ISO 8601 带时区")
|
|
3766
|
+
.option("-n, --limit <n>", "每页条数(缺省用服务端默认)")
|
|
3767
|
+
.option("--cursor <token>", "原样回传上一页的 nextCursor")
|
|
3768
|
+
.option("--include-body", "返回正文(缺省只回摘录)")
|
|
3769
|
+
.action(async (opts) => {
|
|
3770
|
+
await runPublicPosts(requireClient(), opts);
|
|
3771
|
+
});
|
|
3772
|
+
intel
|
|
3773
|
+
.command("public-post-count")
|
|
3774
|
+
.description("POST /v1/system/public-reddit/post-counts(公开库计数;昂贵读,禁止轮询;必须带 query;query 会发往外部数据服务;必须给窗口;DISABLED/POLICY_DENIED 等错误码 = 读取失败,不是没有数据)")
|
|
3775
|
+
.requiredOption("--subreddit <a,b>", "板块,逗号分隔,单次 ≤50 个")
|
|
3776
|
+
.requiredOption("--query <text>", "必填(⚠️ 会发往外部数据服务)")
|
|
3777
|
+
.option("--window <name>", "day|week|month|last_90d|last_360d|last_full_week(与 --from/--to 互斥;二选一必给)")
|
|
3778
|
+
.option("--from <iso>", "窗口起(含),ISO 8601 带时区")
|
|
3779
|
+
.option("--to <iso>", "窗口止(不含),ISO 8601 带时区")
|
|
3780
|
+
.option("--type <t>", "post|comment(缺省 post;comment 覆盖不完整)")
|
|
3781
|
+
.action(async (opts) => {
|
|
3782
|
+
await runPublicPostCount(requireClient(), opts);
|
|
3783
|
+
});
|
|
3784
|
+
intel
|
|
3785
|
+
.command("public-status")
|
|
3786
|
+
.description("GET /v1/system/public-reddit/status(开关/熔断/预算;仅 unrestricted admin)")
|
|
3787
|
+
.action(async () => {
|
|
3788
|
+
await runPublicStatus(requireClient());
|
|
3789
|
+
});
|
|
3559
3790
|
intel
|
|
3560
3791
|
.command("hot-posts-create")
|
|
3561
3792
|
.description("POST .../subreddit-intelligence/hot-posts")
|
|
@@ -4556,6 +4787,7 @@ reddit
|
|
|
4556
4787
|
.option("--draft-status <st>", "draftStatus filter (pending|drafted|reviewed|published|unknown)")
|
|
4557
4788
|
.option("--post-type <t>", "postType filter")
|
|
4558
4789
|
.option("--repost-type <t>", "repostType filter (none|first|second)")
|
|
4790
|
+
.option("--account-type <t>", "账号类型筛选:internal|external(可逗号分隔);proxy/海外兼职/未知/空值在选了任一项时不命中")
|
|
4559
4791
|
.option("--contains-brand-keyword <v>", "true | false | unset")
|
|
4560
4792
|
.option("--primary-product <text>", "primary product contains match")
|
|
4561
4793
|
.option("--seq-no-min <n>", "序列号下界(含);与 --seq-no-max 组成闭区间,无编号(null)的帖会落选")
|
|
@@ -4576,6 +4808,7 @@ reddit
|
|
|
4576
4808
|
draftStatus: opts.draftStatus,
|
|
4577
4809
|
postType: opts.postType,
|
|
4578
4810
|
repostType: opts.repostType,
|
|
4811
|
+
accountType: opts.accountType,
|
|
4579
4812
|
containsBrandKeyword: opts.containsBrandKeyword,
|
|
4580
4813
|
primaryProduct: opts.primaryProduct,
|
|
4581
4814
|
seqNoMin: opts.seqNoMin !== undefined ? Number(opts.seqNoMin) : undefined,
|