@geoly-ai/social-hub-cli 0.3.39 → 0.3.41

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 CHANGED
@@ -4,6 +4,29 @@ 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.41] - 2026-09-09
8
+
9
+ ### Added
10
+
11
+ - 板块新帖监听 P2 —— 近义词扩展 + 帖子类型标注
12
+ - 板块新帖监听 P1 —— 版本化关键词规则 + 命中账本 + 覆盖诊断
13
+ - calendar list-full 补 --offset —— 服务端一直支持,只有下游没暴露
14
+ - 帖子表现新增「序列号范围」筛选
15
+
16
+ ### Changed
17
+
18
+ - 给偶发失败的用例加取证钩子 —— 默认不激活,只有设 CLI_FLAKE_LOG 才写盘
19
+
20
+ ## [0.3.40] - 2026-09-04
21
+
22
+ ### Fixed
23
+
24
+ - 内部品牌可用于养号内容 —— 但放行判据收成三重约束
25
+
26
+ ### Changed
27
+
28
+ - 偶发失败的用例改成自解释 —— 报错里带上退出码与三个通道
29
+
7
30
  ## [0.3.39] - 2026-09-03
8
31
 
9
32
  ### Added
@@ -1,6 +1,6 @@
1
1
  {
2
- "generatedAt": "2026-09-03T09:37:20.454Z",
3
- "cliVersion": "0.3.39",
2
+ "generatedAt": "2026-09-09T10:36:57.032Z",
3
+ "cliVersion": "0.3.41",
4
4
  "commands": [
5
5
  "accounts",
6
6
  "accounts brand-bindings",
@@ -249,6 +249,28 @@
249
249
  "intelligence kol-intents-create",
250
250
  "intelligence kol-profiles-get",
251
251
  "intelligence kol-profiles-list",
252
+ "intelligence monitor-brand-keyword-candidates",
253
+ "intelligence monitor-expansion-approve",
254
+ "intelligence monitor-expansion-create",
255
+ "intelligence monitor-expansion-delete",
256
+ "intelligence monitor-expansion-generate",
257
+ "intelligence monitor-expansion-get",
258
+ "intelligence monitor-expansion-retire",
259
+ "intelligence monitor-expansions-list",
260
+ "intelligence monitor-label-spec-activate",
261
+ "intelligence monitor-label-spec-create",
262
+ "intelligence monitor-label-spec-delete",
263
+ "intelligence monitor-label-spec-retire",
264
+ "intelligence monitor-label-specs-list",
265
+ "intelligence monitor-ruleset-activate",
266
+ "intelligence monitor-ruleset-apply-expansions",
267
+ "intelligence monitor-ruleset-create",
268
+ "intelligence monitor-ruleset-delete",
269
+ "intelligence monitor-ruleset-dry-run",
270
+ "intelligence monitor-ruleset-get",
271
+ "intelligence monitor-ruleset-retire",
272
+ "intelligence monitor-ruleset-rules-set",
273
+ "intelligence monitor-rulesets-list",
252
274
  "intelligence monitor-runs-list",
253
275
  "intelligence monitors-disable",
254
276
  "intelligence monitors-enable",
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+DpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AA8mF9B,8DAA8D;AAC9D,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,GAAG,IAAI,CAOf;AAg+BD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1D,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjE,CAAC;AAQF,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE;IACN,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,GACA,OAAO,CAAC;IAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAkJnE;AAyqBD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,sBAAsB,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpE,GAAG,MAAM,CAiBT;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAyCpD;AAwyDD,OAAO,EAAE,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+DpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAunF9B,8DAA8D;AAC9D,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,GAAG,IAAI,CAOf;AAg+BD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1D,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjE,CAAC;AAQF,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE;IACN,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,GACA,OAAO,CAAC;IAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAkJnE;AAgoCD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,sBAAsB,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpE,GAAG,MAAM,CAiBT;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAyCpD;AAqzDD,OAAO,EAAE,OAAO,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -370,6 +370,8 @@ reddit
370
370
  .option("--repost-type <t>", "repostType filter (none|first|second)")
371
371
  .option("--contains-brand-keyword <v>", "true | false | unset")
372
372
  .option("--primary-product <text>", "primary product contains match")
373
+ .option("--seq-no-min <n>", "序列号下界(含);与 --seq-no-max 组成闭区间,无编号(null)的帖会落选")
374
+ .option("--seq-no-max <n>", "序列号上界(含)")
373
375
  .option("--sort-by <f>", "score|comments_count|posted_at|view_count|seq_no")
374
376
  .option("--sort-dir <d>", "asc|desc")
375
377
  .option("-n, --limit <n>", "limit per page", "100")
@@ -389,6 +391,8 @@ reddit
389
391
  repostType: opts.repostType,
390
392
  containsBrandKeyword: opts.containsBrandKeyword,
391
393
  primaryProduct: opts.primaryProduct,
394
+ seqNoMin: opts.seqNoMin !== undefined ? Number(opts.seqNoMin) : undefined,
395
+ seqNoMax: opts.seqNoMax !== undefined ? Number(opts.seqNoMax) : undefined,
392
396
  sortBy: opts.sortBy,
393
397
  sortDir: opts.sortDir,
394
398
  ...redditAggregateQueryParams(opts),
@@ -3095,6 +3099,322 @@ intel
3095
3099
  });
3096
3100
  console.log(JSON.stringify(res, null, 2));
3097
3101
  });
3102
+ /* ── 监听关键词规则集(0168)。P1「确定性监听骨架」 ──────────────────────────
3103
+ *
3104
+ * 🔴 版本语义:draft 可反复改 → activate 之后**不可原地修改,只能新建版本**。
3105
+ * 🔴 激活/停用**不会重发历史命中** —— 观察的水位在被评估的那一轮就推进了(无论命中与否)。
3106
+ * 这是刻意的:否则调一次关键词,负责人就被历史帖子刷一次屏。追溯需显式 replay(P3)。
3107
+ * ⚠️ 停用当前版本 = 回到「不设关键词门」,报告**恢复全量新帖**,不是一条都不报。
3108
+ * ⚠️ 本期只有 include(排除词是 P2,契约层就表达不出来)。
3109
+ * ────────────────────────────────────────────────────────────────────────── */
3110
+ intel
3111
+ .command("monitor-rulesets-list")
3112
+ .description("GET .../monitors/:monitorId/rule-sets —— 某监听的全部规则版本(version DESC)")
3113
+ .requiredOption("--monitor <uuid>", "Monitor UUID(不是 watchlist id)")
3114
+ .option("--status <status>", "draft|active|retired")
3115
+ .option("-n, --limit <n>", "limit", "20")
3116
+ .action(async (opts) => {
3117
+ const res = await requireClient().listSubredditMonitorRuleSets(opts.monitor, {
3118
+ status: opts.status,
3119
+ limit: Number(opts.limit),
3120
+ });
3121
+ console.log(JSON.stringify(res, null, 2));
3122
+ });
3123
+ intel
3124
+ .command("monitor-ruleset-get")
3125
+ .description("GET .../monitors/:monitorId/rule-sets/:ruleSetId —— 含全部规则")
3126
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3127
+ .requiredOption("--rule-set <uuid>", "Rule set UUID")
3128
+ .action(async (opts) => {
3129
+ const res = await requireClient().getSubredditMonitorRuleSet(opts.monitor, opts.ruleSet);
3130
+ console.log(JSON.stringify(res, null, 2));
3131
+ });
3132
+ intel
3133
+ .command("monitor-ruleset-create")
3134
+ .description("POST .../monitors/:monitorId/rule-sets —— 新建 **draft** 版本(不会自动激活)。" +
3135
+ "规则形状:[{term,scope?,matchMode?,weight?}];scope=title|body|both,matchMode=word|substring|phrase")
3136
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3137
+ .option("--lookback <minutes>", "回看窗口,只允许 720(12h)或 1440(24h);run 窗口取所有启用监听里最长的一档")
3138
+ .option("--note <text>", "本版说明(会随版本链留痕,作为审计的一部分)")
3139
+ .option("-j, --json <json>", "规则数组 JSON;省略则建空 draft 再用 rules-set 填")
3140
+ .action(async (opts) => {
3141
+ const res = await requireClient().createSubredditMonitorRuleSet(opts.monitor, {
3142
+ ...(opts.lookback === undefined
3143
+ ? {}
3144
+ : { lookbackMinutes: Number(opts.lookback) }),
3145
+ ...(opts.note === undefined ? {} : { note: opts.note }),
3146
+ ...(opts.json === undefined ? {} : { rules: JSON.parse(opts.json) }),
3147
+ });
3148
+ console.log(JSON.stringify(res, null, 2));
3149
+ });
3150
+ intel
3151
+ .command("monitor-ruleset-rules-set")
3152
+ .description("PUT .../rule-sets/:ruleSetId/rules —— **整批替换** draft 的规则(传什么就是最终全量)。" +
3153
+ "已激活的版本会被服务端 409 拒绝:改规则必须新建版本")
3154
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3155
+ .requiredOption("--rule-set <uuid>", "Rule set UUID(必须是 draft)")
3156
+ .requiredOption("-j, --json <json>", "规则数组 JSON(全量)")
3157
+ .option("--lookback <minutes>", "顺带改回看窗口(720|1440)")
3158
+ .action(async (opts) => {
3159
+ const res = await requireClient().replaceSubredditMonitorRules(opts.monitor, opts.ruleSet, {
3160
+ rules: JSON.parse(opts.json),
3161
+ ...(opts.lookback === undefined
3162
+ ? {}
3163
+ : { lookbackMinutes: Number(opts.lookback) }),
3164
+ });
3165
+ console.log(JSON.stringify(res, null, 2));
3166
+ });
3167
+ intel
3168
+ .command("monitor-ruleset-activate")
3169
+ .description("POST .../rule-sets/:ruleSetId/activate —— 激活该版本,当前 active 自动 retire。" +
3170
+ "🔴 不会重发历史命中;空规则集会被拒(那等于把整个板块静音)")
3171
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3172
+ .requiredOption("--rule-set <uuid>", "Rule set UUID(必须是 draft)")
3173
+ .action(async (opts) => {
3174
+ const res = await requireClient().activateSubredditMonitorRuleSet(opts.monitor, opts.ruleSet);
3175
+ console.log(JSON.stringify(res, null, 2));
3176
+ });
3177
+ intel
3178
+ .command("monitor-ruleset-retire")
3179
+ .description("POST .../rule-sets/:ruleSetId/retire —— 停用。" +
3180
+ "⚠️ 停用后该监听回到「不设关键词门」,报告**恢复全量新帖**,不是一条都不报")
3181
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3182
+ .requiredOption("--rule-set <uuid>", "Rule set UUID")
3183
+ .action(async (opts) => {
3184
+ const res = await requireClient().retireSubredditMonitorRuleSet(opts.monitor, opts.ruleSet);
3185
+ console.log(JSON.stringify(res, null, 2));
3186
+ });
3187
+ intel
3188
+ .command("monitor-ruleset-delete")
3189
+ .description("DELETE .../rule-sets/:ruleSetId —— 只能删 draft。已激活/已停用的版本必须留痕(服务端 409)")
3190
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3191
+ .requiredOption("--rule-set <uuid>", "Rule set UUID(必须是 draft)")
3192
+ .action(async (opts) => {
3193
+ const res = await requireClient().deleteSubredditMonitorRuleSetDraft(opts.monitor, opts.ruleSet);
3194
+ console.log(JSON.stringify(res, null, 2));
3195
+ });
3196
+ intel
3197
+ .command("monitor-ruleset-dry-run")
3198
+ .description("POST .../rule-sets/:ruleSetId/dry-run —— 🔴 **试跑**:拿过去 12h/24h 的历史帖子跑一遍," +
3199
+ "看会命中什么。**只读:不写命中账本、不推进水位、不发任何通知**。" +
3200
+ "先看 sampled:它是 0 时「0 命中」说明的是没有样本,不是规则太窄。" +
3201
+ "termHitCounts 看哪个词过宽,unusedTerms 看哪个词拼错了")
3202
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3203
+ .requiredOption("--rule-set <uuid>", "Rule set UUID(draft 或 active 都可以)")
3204
+ .option("--lookback <minutes>", "720|1440;缺省用规则集自己的 lookback")
3205
+ .option("-n, --limit <n>", "取样上限", "200")
3206
+ .option("--matched-only", "只回命中的。⚠️ 默认给全量是刻意的 —— 要判断词是不是配窄了,必须看到没命中的长什么样")
3207
+ .action(async (opts) => {
3208
+ const res = await requireClient().dryRunSubredditMonitorRules(opts.monitor, opts.ruleSet, {
3209
+ ...(opts.lookback === undefined
3210
+ ? {}
3211
+ : { lookbackMinutes: Number(opts.lookback) }),
3212
+ limit: Number(opts.limit),
3213
+ ...(opts.matchedOnly ? { matchedOnly: true } : {}),
3214
+ });
3215
+ console.log(JSON.stringify(res, null, 2));
3216
+ });
3217
+ intel
3218
+ .command("monitor-brand-keyword-candidates")
3219
+ .description("GET .../monitors/:monitorId/brand-keyword-candidates —— 从品牌词取导入候选(只读)。" +
3220
+ "⚠️ 语义是**快照复制**不是订阅:导入后品牌词再变,已激活的规则集不会跟着变 —— " +
3221
+ "否则给某品牌加一个词、或把品牌绑到这个 watchlist,都会让监听范围悄然扩大")
3222
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3223
+ .option("--brands <ids>", "逗号分隔的品牌 id;缺省取该 watchlist 已绑定的全部品牌")
3224
+ .option("-n, --limit <n>", "limit", "200")
3225
+ .action(async (opts) => {
3226
+ const res = await requireClient().listSubredditMonitorBrandKeywordCandidates(opts.monitor, {
3227
+ ...(opts.brands ? { brandIds: String(opts.brands).split(",") } : {}),
3228
+ limit: Number(opts.limit),
3229
+ });
3230
+ console.log(JSON.stringify(res, null, 2));
3231
+ });
3232
+ /* ── 近义词扩展集(0169)· 板块新帖监听 P2 ──────────────────────────────────
3233
+ *
3234
+ * 🔴 链路是 **离线生成 → 审核 → 固化 → 物化进 draft → 激活**,四步缺一不可:
3235
+ * 1. expansion-generate(或 expansion-create 手工录词)→ 产出 **proposed**
3236
+ * 2. expansion-approve → 内容**就此冻结**
3237
+ * 3. ruleset-apply-expansions → **复制**进某个 draft
3238
+ * 4. monitor-ruleset-activate → 才真正生效
3239
+ *
3240
+ * ⛔ **生产匹配路径上没有任何即时展开**。理由:同一原词在不同时间可能得到不同词集
3241
+ * (模型会漂、prompt 会改),即时展开会让「昨天没命中、今天命中了」**无法解释** ——
3242
+ * 而这个监听的全部价值就建立在「能说清为什么命中」上。
3243
+ *
3244
+ * ⚠️ 否决(proposed→retired)与下架(approved→retired)是**两件事**:
3245
+ * 被否决过的词**永远**进不了 active 规则集(DB 激活闸拦);而下架只是不再供新的物化使用,
3246
+ * 已经复制进规则集的词不受影响 —— 它们拿的是拷贝。
3247
+ * ────────────────────────────────────────────────────────────────────────── */
3248
+ intel
3249
+ .command("monitor-expansions-list")
3250
+ .description("GET .../monitors/:monitorId/expansion-sets —— 某监听的近义词扩展集(created_at DESC)。" +
3251
+ "--status proposed 就是「还有哪些等我审」")
3252
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3253
+ .option("--status <status>", "proposed|approved|retired")
3254
+ .option("-n, --limit <n>", "limit", "20")
3255
+ .action(async (opts) => {
3256
+ const res = await requireClient().listSubredditMonitorExpansionSets(opts.monitor, {
3257
+ status: opts.status,
3258
+ limit: Number(opts.limit),
3259
+ });
3260
+ console.log(JSON.stringify(res, null, 2));
3261
+ });
3262
+ intel
3263
+ .command("monitor-expansion-get")
3264
+ .description("GET .../expansion-sets/:expansionSetId —— 含固化的完整词集")
3265
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3266
+ .requiredOption("--expansion-set <uuid>", "Expansion set UUID")
3267
+ .action(async (opts) => {
3268
+ const res = await requireClient().getSubredditMonitorExpansionSet(opts.monitor, opts.expansionSet);
3269
+ console.log(JSON.stringify(res, null, 2));
3270
+ });
3271
+ intel
3272
+ .command("monitor-expansion-generate")
3273
+ .description("POST .../expansion-sets/generate —— 🔴 **离线**用 LLM 生成近义词候选。" +
3274
+ "产出恒为 **proposed**,进不了任何 active 规则集,必须再 approve + apply。" +
3275
+ "provider 没配 ⇒ 409(功能未启用,可改用 monitor-expansion-create 手工录词)")
3276
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3277
+ .requiredOption("--term <text>", "原词(必须是规则集里已有/将有的那个词)")
3278
+ .option("--language <tag>", "语言标记;und=未指定。同一原词不同语言各批一套", "und")
3279
+ .action(async (opts) => {
3280
+ const res = await requireClient().generateSubredditMonitorExpansion(opts.monitor, { baseTerm: opts.term, language: opts.language });
3281
+ console.log(JSON.stringify(res, null, 2));
3282
+ });
3283
+ intel
3284
+ .command("monitor-expansion-create")
3285
+ .description("POST .../expansion-sets —— 人工录入候选词表(provider 没配时的路径)。产出恒为 proposed")
3286
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3287
+ .requiredOption("--term <text>", "原词")
3288
+ .requiredOption("-j, --json <json>", '候选词数组 JSON,如 ["latte","espresso shot"]')
3289
+ .option("--language <tag>", "语言标记", "und")
3290
+ .action(async (opts) => {
3291
+ const res = await requireClient().createSubredditMonitorExpansionSet(opts.monitor, {
3292
+ baseTerm: opts.term,
3293
+ language: opts.language,
3294
+ terms: JSON.parse(opts.json),
3295
+ });
3296
+ console.log(JSON.stringify(res, null, 2));
3297
+ });
3298
+ intel
3299
+ .command("monitor-expansion-approve")
3300
+ .description("POST .../expansion-sets/:id/approve —— 批准。" +
3301
+ "🔴 批准之后**内容就冻住了**(DB 焊死,只能另提一份新候选)。" +
3302
+ "批准 ≠ 生效:还要 ruleset-apply-expansions 物化 + activate")
3303
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3304
+ .requiredOption("--expansion-set <uuid>", "Expansion set UUID(必须是 proposed)")
3305
+ .option("--note <text>", "审核意见(写进版本链,是审计的一部分)")
3306
+ .action(async (opts) => {
3307
+ const res = await requireClient().approveSubredditMonitorExpansionSet(opts.monitor, opts.expansionSet, { ...(opts.note === undefined ? {} : { reviewNote: opts.note }) });
3308
+ console.log(JSON.stringify(res, null, 2));
3309
+ });
3310
+ intel
3311
+ .command("monitor-expansion-retire")
3312
+ .description("POST .../expansion-sets/:id/retire —— 否决(从 proposed)或下架(从 approved)。" +
3313
+ "⚠️ **被否决的永远进不了 active 规则集**;而下架只是不再供新的物化使用," +
3314
+ "已经复制进规则集的词不受影响(它们拿的是拷贝)")
3315
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3316
+ .requiredOption("--expansion-set <uuid>", "Expansion set UUID")
3317
+ .option("--note <text>", "审核意见")
3318
+ .action(async (opts) => {
3319
+ const res = await requireClient().retireSubredditMonitorExpansionSet(opts.monitor, opts.expansionSet, { ...(opts.note === undefined ? {} : { reviewNote: opts.note }) });
3320
+ console.log(JSON.stringify(res, null, 2));
3321
+ });
3322
+ intel
3323
+ .command("monitor-expansion-delete")
3324
+ .description("DELETE .../expansion-sets/:id —— 只能删 proposed。审核过的必须留痕(改用 retire)")
3325
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3326
+ .requiredOption("--expansion-set <uuid>", "Expansion set UUID")
3327
+ .action(async (opts) => {
3328
+ const res = await requireClient().deleteSubredditMonitorExpansionSet(opts.monitor, opts.expansionSet);
3329
+ console.log(JSON.stringify(res, null, 2));
3330
+ });
3331
+ intel
3332
+ .command("monitor-ruleset-apply-expansions")
3333
+ .description("POST .../rule-sets/:ruleSetId/apply-expansions —— 🔴 **物化**:把 approved 的近义词" +
3334
+ "**复制**成 draft 规则集里的真实规则行(与品牌词导入同一手法)。" +
3335
+ "⚠️ 物化 ≠ 生效,之后还要 monitor-ruleset-activate。" +
3336
+ "⚠️ 点名的扩展集里有未批准的 ⇒ 409(⛔ 不静默少物化几条 —— 那会让你以为词已经生效了)")
3337
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3338
+ .requiredOption("--rule-set <uuid>", "Rule set UUID(必须是 draft)")
3339
+ .option("-j, --json <json>", "限定扩展集 id 数组 JSON;省略 = 该监听全部 approved 的")
3340
+ .action(async (opts) => {
3341
+ const res = await requireClient().applyExpansionsToRuleSet(opts.monitor, opts.ruleSet, {
3342
+ ...(opts.json === undefined
3343
+ ? {}
3344
+ : { expansionSetIds: JSON.parse(opts.json) }),
3345
+ });
3346
+ console.log(JSON.stringify(res, null, 2));
3347
+ });
3348
+ /* ── 标签 spec / taxonomy(0169)· 系统级 ─────────────────────────────────
3349
+ * ⛔ 刻意**不走**四眼审批(需求明说太重、会拖慢提醒),但发布本身是不可变版本链。
3350
+ * ⚠️ taxonomyVersion **全局唯一**:换 prompt/换模型/加 key 都要起新值,
3351
+ * 否则标签行会互相覆盖,「这个标签是哪版规则打的」失去答案。
3352
+ * ⚠️ 停用当前 spec = **标签功能整体关闭**(卡片不再显示标签行)。
3353
+ * ─────────────────────────────────────────────────────────────────────── */
3354
+ intel
3355
+ .command("monitor-label-specs-list")
3356
+ .description("GET .../label-specs —— 标签 spec 版本链(version DESC)")
3357
+ .option("--status <status>", "draft|active|retired")
3358
+ .option("-n, --limit <n>", "limit", "20")
3359
+ .action(async (opts) => {
3360
+ const res = await requireClient().listSubredditMonitorLabelSpecs({
3361
+ status: opts.status,
3362
+ limit: Number(opts.limit),
3363
+ });
3364
+ console.log(JSON.stringify(res, null, 2));
3365
+ });
3366
+ intel
3367
+ .command("monitor-label-spec-create")
3368
+ .description("POST .../label-specs —— 新建 **draft** spec(不会自动激活)。" +
3369
+ "--keys 是稳定 key 白名单,模型只能从里面选,自创的一律丢弃")
3370
+ .requiredOption("--taxonomy <version>", "🔴 全局唯一。换 prompt/换模型/加 key 都要起新值")
3371
+ .requiredOption("--keys <keys>", "逗号分隔,如 recommendation_request,comparison,review")
3372
+ .requiredOption("--prompt-version <v>", "prompt 版本(审计的一部分)")
3373
+ .option("--model <name>", "指定模型;省略则用 env 里那一档")
3374
+ .option("--temperature <n>", "温度")
3375
+ .option("--note <text>", "本版说明")
3376
+ .action(async (opts) => {
3377
+ const res = await requireClient().createSubredditMonitorLabelSpec({
3378
+ taxonomyVersion: opts.taxonomy,
3379
+ labelKeys: String(opts.keys)
3380
+ .split(",")
3381
+ .map((k) => k.trim())
3382
+ .filter(Boolean),
3383
+ promptVersion: opts.promptVersion,
3384
+ ...(opts.model === undefined ? {} : { model: opts.model }),
3385
+ ...(opts.temperature === undefined
3386
+ ? {}
3387
+ : { temperature: Number(opts.temperature) }),
3388
+ ...(opts.note === undefined ? {} : { note: opts.note }),
3389
+ });
3390
+ console.log(JSON.stringify(res, null, 2));
3391
+ });
3392
+ intel
3393
+ .command("monitor-label-spec-activate")
3394
+ .description("POST .../label-specs/:specId/activate —— 激活(当前 active 自动 retire)。" +
3395
+ "激活后 active 版本**不可原地改**,只能新建版本")
3396
+ .requiredOption("--spec <uuid>", "Label spec UUID(必须是 draft)")
3397
+ .action(async (opts) => {
3398
+ const res = await requireClient().activateSubredditMonitorLabelSpec(opts.spec);
3399
+ console.log(JSON.stringify(res, null, 2));
3400
+ });
3401
+ intel
3402
+ .command("monitor-label-spec-retire")
3403
+ .description("POST .../label-specs/:specId/retire —— ⚠️ 停用 = **标签功能整体关闭**" +
3404
+ "(卡片不再显示标签行);新帖提醒本身不受影响")
3405
+ .requiredOption("--spec <uuid>", "Label spec UUID(必须是 active)")
3406
+ .action(async (opts) => {
3407
+ const res = await requireClient().retireSubredditMonitorLabelSpec(opts.spec);
3408
+ console.log(JSON.stringify(res, null, 2));
3409
+ });
3410
+ intel
3411
+ .command("monitor-label-spec-delete")
3412
+ .description("DELETE .../label-specs/:specId —— 只能删 draft")
3413
+ .requiredOption("--spec <uuid>", "Label spec UUID")
3414
+ .action(async (opts) => {
3415
+ const res = await requireClient().deleteSubredditMonitorLabelSpecDraft(opts.spec);
3416
+ console.log(JSON.stringify(res, null, 2));
3417
+ });
3098
3418
  intel
3099
3419
  .command("hot-posts")
3100
3420
  .description("GET .../subreddit-intelligence/hot-posts")
@@ -4064,7 +4384,10 @@ calendar
4064
4384
  .option("--status <st>", "scheduled|running|succeeded|…")
4065
4385
  .option("--from <iso>", "from ISO 时间")
4066
4386
  .option("--to <iso>", "to ISO 时间")
4067
- .option("-n, --limit <n>", "limit", "50")
4387
+ .option("-n, --limit <n>", "limit(服务端上限 200)", "50")
4388
+ // 🔴 服务端一直支持 offset,此前只有 CLI 没暴露 —— 调用方要么撞上限,
4389
+ // 要么自己拿时间窗二分绕(矩阵侧实测:那样会静默漏掉更早的条目且不报错)。
4390
+ .option("--offset <n>", "offset(翻页;默认 0)")
4068
4391
  .action(async (opts) => {
4069
4392
  const res = await requireClient().listCalendarEntries(resolveTeamId(opts.team), {
4070
4393
  campaignId: opts.campaign,
@@ -4074,6 +4397,7 @@ calendar
4074
4397
  from: opts.from,
4075
4398
  to: opts.to,
4076
4399
  limit: Number(opts.limit),
4400
+ ...(opts.offset === undefined ? {} : { offset: Number(opts.offset) }),
4077
4401
  });
4078
4402
  console.log(JSON.stringify(res, null, 2));
4079
4403
  });
@@ -4109,6 +4433,8 @@ reddit
4109
4433
  .option("--repost-type <t>", "repostType filter (none|first|second)")
4110
4434
  .option("--contains-brand-keyword <v>", "true | false | unset")
4111
4435
  .option("--primary-product <text>", "primary product contains match")
4436
+ .option("--seq-no-min <n>", "序列号下界(含);与 --seq-no-max 组成闭区间,无编号(null)的帖会落选")
4437
+ .option("--seq-no-max <n>", "序列号上界(含)")
4112
4438
  .option("--sort-by <f>", "score|comments_count|posted_at|view_count|seq_no")
4113
4439
  .option("--sort-dir <d>", "asc|desc")
4114
4440
  .option("-n, --limit <n>", "limit per page", "100")
@@ -4127,6 +4453,8 @@ reddit
4127
4453
  repostType: opts.repostType,
4128
4454
  containsBrandKeyword: opts.containsBrandKeyword,
4129
4455
  primaryProduct: opts.primaryProduct,
4456
+ seqNoMin: opts.seqNoMin !== undefined ? Number(opts.seqNoMin) : undefined,
4457
+ seqNoMax: opts.seqNoMax !== undefined ? Number(opts.seqNoMax) : undefined,
4130
4458
  sortBy: opts.sortBy,
4131
4459
  sortDir: opts.sortDir,
4132
4460
  limit: Number(opts.limit),