@geoly-ai/social-hub-cli 0.3.40 → 0.3.42

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,25 @@ 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.42] - 2026-09-10
8
+
9
+ ### Added
10
+
11
+ - watchlist 筛选补齐 CLI/skills 层 + 清两处 sticky 死代码
12
+
13
+ ## [0.3.41] - 2026-09-09
14
+
15
+ ### Added
16
+
17
+ - 板块新帖监听 P2 —— 近义词扩展 + 帖子类型标注
18
+ - 板块新帖监听 P1 —— 版本化关键词规则 + 命中账本 + 覆盖诊断
19
+ - calendar list-full 补 --offset —— 服务端一直支持,只有下游没暴露
20
+ - 帖子表现新增「序列号范围」筛选
21
+
22
+ ### Changed
23
+
24
+ - 给偶发失败的用例加取证钩子 —— 默认不激活,只有设 CLI_FLAKE_LOG 才写盘
25
+
7
26
  ## [0.3.40] - 2026-09-04
8
27
 
9
28
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
- "generatedAt": "2026-09-04T03:00:49.281Z",
3
- "cliVersion": "0.3.40",
2
+ "generatedAt": "2026-09-10T02:54:19.672Z",
3
+ "cliVersion": "0.3.42",
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;AA0qCD;;;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),
@@ -2981,12 +2985,41 @@ intel
2981
2985
  .description("GET .../subreddit-intelligence/watchlists")
2982
2986
  .option("--campaign <uuid>", "campaignId")
2983
2987
  .option("--subreddit <name>", "按板块名精确查归属(r/ 可省)")
2988
+ .option("--subreddit-query <text>", "板块名**子串**搜索(大小写不敏感,r/ 可省);与 --subreddit 是两个独立谓词,后者是精确匹配")
2989
+ .option("--status <st>", "active|paused|archived")
2990
+ .option("--brand <uuid>", "brandId:只返回**显式绑定**该品牌的监控池(不含全局池)")
2991
+ .option("--industry-keys <list>", "industryKey 过滤(逗号分隔可多个)")
2992
+ .option("--tag <tag>", "标签精确包含(tags 是 jsonb 数组)")
2993
+ .option("--monitor-state <state>", "新帖监听状态,三值:enabled=在监听 | disabled=加过但已停(软停,配置行还在) | never=从没加过。" +
2994
+ "⚠️ 受限主体(client)传这个会 403")
2995
+ .option("--sort-by <f>", "createdAt|subreddit|monitorLastObservedAt")
2996
+ .option("--sort-dir <d>", "asc|desc")
2984
2997
  .option("-n, --limit <n>", "limit", "100")
2985
2998
  .option("--offset <n>", "offset", "0")
2986
2999
  .action(async (opts) => {
3000
+ // 🔴 显式校验只给 --monitor-state 一个,不是漏了别的:本仓 CLI 的既有惯例是
3001
+ // 枚举透传、由服务端 zod 400(见 hot-posts / voc 的 --sort-by),只有**会被静默
3002
+ // 强转**的选项才本地拦(monitors-list 的 `--enabled` 是 Boolean() 强转)。
3003
+ // monitorState 拦下来的理由不是强转,而是**语义**:它最容易被当成布尔用
3004
+ // (`--monitor-state true` / `false`),而服务端那句 "invalid enum value"
3005
+ // 讲不清 disabled(停了的) 与 never(从没加过) 的区别 —— 而用户提这个需求
3006
+ // 要的正是这个区别。
3007
+ if (opts.monitorState !== undefined &&
3008
+ !["enabled", "disabled", "never"].includes(opts.monitorState))
3009
+ throw new Error("--monitor-state 必须是 enabled | disabled | never 之一(三值,不是布尔):" +
3010
+ "disabled = 加过监听后停了(软停,配置行与观察水位都还在,重新启用不会重复报旧帖);" +
3011
+ "never = 压根没加过监听(需要新建配置行)。想要「没在监听的全部」请分两次查。");
2987
3012
  const res = await requireClient().listSubredditWatchlists({
2988
3013
  campaignId: opts.campaign,
2989
3014
  subreddit: opts.subreddit,
3015
+ subredditQuery: opts.subredditQuery,
3016
+ status: opts.status,
3017
+ brandId: opts.brand,
3018
+ industryKeys: opts.industryKeys,
3019
+ tag: opts.tag,
3020
+ monitorState: opts.monitorState,
3021
+ sortBy: opts.sortBy,
3022
+ sortDir: opts.sortDir,
2990
3023
  limit: Number(opts.limit),
2991
3024
  offset: Number(opts.offset),
2992
3025
  });
@@ -3095,6 +3128,322 @@ intel
3095
3128
  });
3096
3129
  console.log(JSON.stringify(res, null, 2));
3097
3130
  });
3131
+ /* ── 监听关键词规则集(0168)。P1「确定性监听骨架」 ──────────────────────────
3132
+ *
3133
+ * 🔴 版本语义:draft 可反复改 → activate 之后**不可原地修改,只能新建版本**。
3134
+ * 🔴 激活/停用**不会重发历史命中** —— 观察的水位在被评估的那一轮就推进了(无论命中与否)。
3135
+ * 这是刻意的:否则调一次关键词,负责人就被历史帖子刷一次屏。追溯需显式 replay(P3)。
3136
+ * ⚠️ 停用当前版本 = 回到「不设关键词门」,报告**恢复全量新帖**,不是一条都不报。
3137
+ * ⚠️ 本期只有 include(排除词是 P2,契约层就表达不出来)。
3138
+ * ────────────────────────────────────────────────────────────────────────── */
3139
+ intel
3140
+ .command("monitor-rulesets-list")
3141
+ .description("GET .../monitors/:monitorId/rule-sets —— 某监听的全部规则版本(version DESC)")
3142
+ .requiredOption("--monitor <uuid>", "Monitor UUID(不是 watchlist id)")
3143
+ .option("--status <status>", "draft|active|retired")
3144
+ .option("-n, --limit <n>", "limit", "20")
3145
+ .action(async (opts) => {
3146
+ const res = await requireClient().listSubredditMonitorRuleSets(opts.monitor, {
3147
+ status: opts.status,
3148
+ limit: Number(opts.limit),
3149
+ });
3150
+ console.log(JSON.stringify(res, null, 2));
3151
+ });
3152
+ intel
3153
+ .command("monitor-ruleset-get")
3154
+ .description("GET .../monitors/:monitorId/rule-sets/:ruleSetId —— 含全部规则")
3155
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3156
+ .requiredOption("--rule-set <uuid>", "Rule set UUID")
3157
+ .action(async (opts) => {
3158
+ const res = await requireClient().getSubredditMonitorRuleSet(opts.monitor, opts.ruleSet);
3159
+ console.log(JSON.stringify(res, null, 2));
3160
+ });
3161
+ intel
3162
+ .command("monitor-ruleset-create")
3163
+ .description("POST .../monitors/:monitorId/rule-sets —— 新建 **draft** 版本(不会自动激活)。" +
3164
+ "规则形状:[{term,scope?,matchMode?,weight?}];scope=title|body|both,matchMode=word|substring|phrase")
3165
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3166
+ .option("--lookback <minutes>", "回看窗口,只允许 720(12h)或 1440(24h);run 窗口取所有启用监听里最长的一档")
3167
+ .option("--note <text>", "本版说明(会随版本链留痕,作为审计的一部分)")
3168
+ .option("-j, --json <json>", "规则数组 JSON;省略则建空 draft 再用 rules-set 填")
3169
+ .action(async (opts) => {
3170
+ const res = await requireClient().createSubredditMonitorRuleSet(opts.monitor, {
3171
+ ...(opts.lookback === undefined
3172
+ ? {}
3173
+ : { lookbackMinutes: Number(opts.lookback) }),
3174
+ ...(opts.note === undefined ? {} : { note: opts.note }),
3175
+ ...(opts.json === undefined ? {} : { rules: JSON.parse(opts.json) }),
3176
+ });
3177
+ console.log(JSON.stringify(res, null, 2));
3178
+ });
3179
+ intel
3180
+ .command("monitor-ruleset-rules-set")
3181
+ .description("PUT .../rule-sets/:ruleSetId/rules —— **整批替换** draft 的规则(传什么就是最终全量)。" +
3182
+ "已激活的版本会被服务端 409 拒绝:改规则必须新建版本")
3183
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3184
+ .requiredOption("--rule-set <uuid>", "Rule set UUID(必须是 draft)")
3185
+ .requiredOption("-j, --json <json>", "规则数组 JSON(全量)")
3186
+ .option("--lookback <minutes>", "顺带改回看窗口(720|1440)")
3187
+ .action(async (opts) => {
3188
+ const res = await requireClient().replaceSubredditMonitorRules(opts.monitor, opts.ruleSet, {
3189
+ rules: JSON.parse(opts.json),
3190
+ ...(opts.lookback === undefined
3191
+ ? {}
3192
+ : { lookbackMinutes: Number(opts.lookback) }),
3193
+ });
3194
+ console.log(JSON.stringify(res, null, 2));
3195
+ });
3196
+ intel
3197
+ .command("monitor-ruleset-activate")
3198
+ .description("POST .../rule-sets/:ruleSetId/activate —— 激活该版本,当前 active 自动 retire。" +
3199
+ "🔴 不会重发历史命中;空规则集会被拒(那等于把整个板块静音)")
3200
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3201
+ .requiredOption("--rule-set <uuid>", "Rule set UUID(必须是 draft)")
3202
+ .action(async (opts) => {
3203
+ const res = await requireClient().activateSubredditMonitorRuleSet(opts.monitor, opts.ruleSet);
3204
+ console.log(JSON.stringify(res, null, 2));
3205
+ });
3206
+ intel
3207
+ .command("monitor-ruleset-retire")
3208
+ .description("POST .../rule-sets/:ruleSetId/retire —— 停用。" +
3209
+ "⚠️ 停用后该监听回到「不设关键词门」,报告**恢复全量新帖**,不是一条都不报")
3210
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3211
+ .requiredOption("--rule-set <uuid>", "Rule set UUID")
3212
+ .action(async (opts) => {
3213
+ const res = await requireClient().retireSubredditMonitorRuleSet(opts.monitor, opts.ruleSet);
3214
+ console.log(JSON.stringify(res, null, 2));
3215
+ });
3216
+ intel
3217
+ .command("monitor-ruleset-delete")
3218
+ .description("DELETE .../rule-sets/:ruleSetId —— 只能删 draft。已激活/已停用的版本必须留痕(服务端 409)")
3219
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3220
+ .requiredOption("--rule-set <uuid>", "Rule set UUID(必须是 draft)")
3221
+ .action(async (opts) => {
3222
+ const res = await requireClient().deleteSubredditMonitorRuleSetDraft(opts.monitor, opts.ruleSet);
3223
+ console.log(JSON.stringify(res, null, 2));
3224
+ });
3225
+ intel
3226
+ .command("monitor-ruleset-dry-run")
3227
+ .description("POST .../rule-sets/:ruleSetId/dry-run —— 🔴 **试跑**:拿过去 12h/24h 的历史帖子跑一遍," +
3228
+ "看会命中什么。**只读:不写命中账本、不推进水位、不发任何通知**。" +
3229
+ "先看 sampled:它是 0 时「0 命中」说明的是没有样本,不是规则太窄。" +
3230
+ "termHitCounts 看哪个词过宽,unusedTerms 看哪个词拼错了")
3231
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3232
+ .requiredOption("--rule-set <uuid>", "Rule set UUID(draft 或 active 都可以)")
3233
+ .option("--lookback <minutes>", "720|1440;缺省用规则集自己的 lookback")
3234
+ .option("-n, --limit <n>", "取样上限", "200")
3235
+ .option("--matched-only", "只回命中的。⚠️ 默认给全量是刻意的 —— 要判断词是不是配窄了,必须看到没命中的长什么样")
3236
+ .action(async (opts) => {
3237
+ const res = await requireClient().dryRunSubredditMonitorRules(opts.monitor, opts.ruleSet, {
3238
+ ...(opts.lookback === undefined
3239
+ ? {}
3240
+ : { lookbackMinutes: Number(opts.lookback) }),
3241
+ limit: Number(opts.limit),
3242
+ ...(opts.matchedOnly ? { matchedOnly: true } : {}),
3243
+ });
3244
+ console.log(JSON.stringify(res, null, 2));
3245
+ });
3246
+ intel
3247
+ .command("monitor-brand-keyword-candidates")
3248
+ .description("GET .../monitors/:monitorId/brand-keyword-candidates —— 从品牌词取导入候选(只读)。" +
3249
+ "⚠️ 语义是**快照复制**不是订阅:导入后品牌词再变,已激活的规则集不会跟着变 —— " +
3250
+ "否则给某品牌加一个词、或把品牌绑到这个 watchlist,都会让监听范围悄然扩大")
3251
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3252
+ .option("--brands <ids>", "逗号分隔的品牌 id;缺省取该 watchlist 已绑定的全部品牌")
3253
+ .option("-n, --limit <n>", "limit", "200")
3254
+ .action(async (opts) => {
3255
+ const res = await requireClient().listSubredditMonitorBrandKeywordCandidates(opts.monitor, {
3256
+ ...(opts.brands ? { brandIds: String(opts.brands).split(",") } : {}),
3257
+ limit: Number(opts.limit),
3258
+ });
3259
+ console.log(JSON.stringify(res, null, 2));
3260
+ });
3261
+ /* ── 近义词扩展集(0169)· 板块新帖监听 P2 ──────────────────────────────────
3262
+ *
3263
+ * 🔴 链路是 **离线生成 → 审核 → 固化 → 物化进 draft → 激活**,四步缺一不可:
3264
+ * 1. expansion-generate(或 expansion-create 手工录词)→ 产出 **proposed**
3265
+ * 2. expansion-approve → 内容**就此冻结**
3266
+ * 3. ruleset-apply-expansions → **复制**进某个 draft
3267
+ * 4. monitor-ruleset-activate → 才真正生效
3268
+ *
3269
+ * ⛔ **生产匹配路径上没有任何即时展开**。理由:同一原词在不同时间可能得到不同词集
3270
+ * (模型会漂、prompt 会改),即时展开会让「昨天没命中、今天命中了」**无法解释** ——
3271
+ * 而这个监听的全部价值就建立在「能说清为什么命中」上。
3272
+ *
3273
+ * ⚠️ 否决(proposed→retired)与下架(approved→retired)是**两件事**:
3274
+ * 被否决过的词**永远**进不了 active 规则集(DB 激活闸拦);而下架只是不再供新的物化使用,
3275
+ * 已经复制进规则集的词不受影响 —— 它们拿的是拷贝。
3276
+ * ────────────────────────────────────────────────────────────────────────── */
3277
+ intel
3278
+ .command("monitor-expansions-list")
3279
+ .description("GET .../monitors/:monitorId/expansion-sets —— 某监听的近义词扩展集(created_at DESC)。" +
3280
+ "--status proposed 就是「还有哪些等我审」")
3281
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3282
+ .option("--status <status>", "proposed|approved|retired")
3283
+ .option("-n, --limit <n>", "limit", "20")
3284
+ .action(async (opts) => {
3285
+ const res = await requireClient().listSubredditMonitorExpansionSets(opts.monitor, {
3286
+ status: opts.status,
3287
+ limit: Number(opts.limit),
3288
+ });
3289
+ console.log(JSON.stringify(res, null, 2));
3290
+ });
3291
+ intel
3292
+ .command("monitor-expansion-get")
3293
+ .description("GET .../expansion-sets/:expansionSetId —— 含固化的完整词集")
3294
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3295
+ .requiredOption("--expansion-set <uuid>", "Expansion set UUID")
3296
+ .action(async (opts) => {
3297
+ const res = await requireClient().getSubredditMonitorExpansionSet(opts.monitor, opts.expansionSet);
3298
+ console.log(JSON.stringify(res, null, 2));
3299
+ });
3300
+ intel
3301
+ .command("monitor-expansion-generate")
3302
+ .description("POST .../expansion-sets/generate —— 🔴 **离线**用 LLM 生成近义词候选。" +
3303
+ "产出恒为 **proposed**,进不了任何 active 规则集,必须再 approve + apply。" +
3304
+ "provider 没配 ⇒ 409(功能未启用,可改用 monitor-expansion-create 手工录词)")
3305
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3306
+ .requiredOption("--term <text>", "原词(必须是规则集里已有/将有的那个词)")
3307
+ .option("--language <tag>", "语言标记;und=未指定。同一原词不同语言各批一套", "und")
3308
+ .action(async (opts) => {
3309
+ const res = await requireClient().generateSubredditMonitorExpansion(opts.monitor, { baseTerm: opts.term, language: opts.language });
3310
+ console.log(JSON.stringify(res, null, 2));
3311
+ });
3312
+ intel
3313
+ .command("monitor-expansion-create")
3314
+ .description("POST .../expansion-sets —— 人工录入候选词表(provider 没配时的路径)。产出恒为 proposed")
3315
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3316
+ .requiredOption("--term <text>", "原词")
3317
+ .requiredOption("-j, --json <json>", '候选词数组 JSON,如 ["latte","espresso shot"]')
3318
+ .option("--language <tag>", "语言标记", "und")
3319
+ .action(async (opts) => {
3320
+ const res = await requireClient().createSubredditMonitorExpansionSet(opts.monitor, {
3321
+ baseTerm: opts.term,
3322
+ language: opts.language,
3323
+ terms: JSON.parse(opts.json),
3324
+ });
3325
+ console.log(JSON.stringify(res, null, 2));
3326
+ });
3327
+ intel
3328
+ .command("monitor-expansion-approve")
3329
+ .description("POST .../expansion-sets/:id/approve —— 批准。" +
3330
+ "🔴 批准之后**内容就冻住了**(DB 焊死,只能另提一份新候选)。" +
3331
+ "批准 ≠ 生效:还要 ruleset-apply-expansions 物化 + activate")
3332
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3333
+ .requiredOption("--expansion-set <uuid>", "Expansion set UUID(必须是 proposed)")
3334
+ .option("--note <text>", "审核意见(写进版本链,是审计的一部分)")
3335
+ .action(async (opts) => {
3336
+ const res = await requireClient().approveSubredditMonitorExpansionSet(opts.monitor, opts.expansionSet, { ...(opts.note === undefined ? {} : { reviewNote: opts.note }) });
3337
+ console.log(JSON.stringify(res, null, 2));
3338
+ });
3339
+ intel
3340
+ .command("monitor-expansion-retire")
3341
+ .description("POST .../expansion-sets/:id/retire —— 否决(从 proposed)或下架(从 approved)。" +
3342
+ "⚠️ **被否决的永远进不了 active 规则集**;而下架只是不再供新的物化使用," +
3343
+ "已经复制进规则集的词不受影响(它们拿的是拷贝)")
3344
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3345
+ .requiredOption("--expansion-set <uuid>", "Expansion set UUID")
3346
+ .option("--note <text>", "审核意见")
3347
+ .action(async (opts) => {
3348
+ const res = await requireClient().retireSubredditMonitorExpansionSet(opts.monitor, opts.expansionSet, { ...(opts.note === undefined ? {} : { reviewNote: opts.note }) });
3349
+ console.log(JSON.stringify(res, null, 2));
3350
+ });
3351
+ intel
3352
+ .command("monitor-expansion-delete")
3353
+ .description("DELETE .../expansion-sets/:id —— 只能删 proposed。审核过的必须留痕(改用 retire)")
3354
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3355
+ .requiredOption("--expansion-set <uuid>", "Expansion set UUID")
3356
+ .action(async (opts) => {
3357
+ const res = await requireClient().deleteSubredditMonitorExpansionSet(opts.monitor, opts.expansionSet);
3358
+ console.log(JSON.stringify(res, null, 2));
3359
+ });
3360
+ intel
3361
+ .command("monitor-ruleset-apply-expansions")
3362
+ .description("POST .../rule-sets/:ruleSetId/apply-expansions —— 🔴 **物化**:把 approved 的近义词" +
3363
+ "**复制**成 draft 规则集里的真实规则行(与品牌词导入同一手法)。" +
3364
+ "⚠️ 物化 ≠ 生效,之后还要 monitor-ruleset-activate。" +
3365
+ "⚠️ 点名的扩展集里有未批准的 ⇒ 409(⛔ 不静默少物化几条 —— 那会让你以为词已经生效了)")
3366
+ .requiredOption("--monitor <uuid>", "Monitor UUID")
3367
+ .requiredOption("--rule-set <uuid>", "Rule set UUID(必须是 draft)")
3368
+ .option("-j, --json <json>", "限定扩展集 id 数组 JSON;省略 = 该监听全部 approved 的")
3369
+ .action(async (opts) => {
3370
+ const res = await requireClient().applyExpansionsToRuleSet(opts.monitor, opts.ruleSet, {
3371
+ ...(opts.json === undefined
3372
+ ? {}
3373
+ : { expansionSetIds: JSON.parse(opts.json) }),
3374
+ });
3375
+ console.log(JSON.stringify(res, null, 2));
3376
+ });
3377
+ /* ── 标签 spec / taxonomy(0169)· 系统级 ─────────────────────────────────
3378
+ * ⛔ 刻意**不走**四眼审批(需求明说太重、会拖慢提醒),但发布本身是不可变版本链。
3379
+ * ⚠️ taxonomyVersion **全局唯一**:换 prompt/换模型/加 key 都要起新值,
3380
+ * 否则标签行会互相覆盖,「这个标签是哪版规则打的」失去答案。
3381
+ * ⚠️ 停用当前 spec = **标签功能整体关闭**(卡片不再显示标签行)。
3382
+ * ─────────────────────────────────────────────────────────────────────── */
3383
+ intel
3384
+ .command("monitor-label-specs-list")
3385
+ .description("GET .../label-specs —— 标签 spec 版本链(version DESC)")
3386
+ .option("--status <status>", "draft|active|retired")
3387
+ .option("-n, --limit <n>", "limit", "20")
3388
+ .action(async (opts) => {
3389
+ const res = await requireClient().listSubredditMonitorLabelSpecs({
3390
+ status: opts.status,
3391
+ limit: Number(opts.limit),
3392
+ });
3393
+ console.log(JSON.stringify(res, null, 2));
3394
+ });
3395
+ intel
3396
+ .command("monitor-label-spec-create")
3397
+ .description("POST .../label-specs —— 新建 **draft** spec(不会自动激活)。" +
3398
+ "--keys 是稳定 key 白名单,模型只能从里面选,自创的一律丢弃")
3399
+ .requiredOption("--taxonomy <version>", "🔴 全局唯一。换 prompt/换模型/加 key 都要起新值")
3400
+ .requiredOption("--keys <keys>", "逗号分隔,如 recommendation_request,comparison,review")
3401
+ .requiredOption("--prompt-version <v>", "prompt 版本(审计的一部分)")
3402
+ .option("--model <name>", "指定模型;省略则用 env 里那一档")
3403
+ .option("--temperature <n>", "温度")
3404
+ .option("--note <text>", "本版说明")
3405
+ .action(async (opts) => {
3406
+ const res = await requireClient().createSubredditMonitorLabelSpec({
3407
+ taxonomyVersion: opts.taxonomy,
3408
+ labelKeys: String(opts.keys)
3409
+ .split(",")
3410
+ .map((k) => k.trim())
3411
+ .filter(Boolean),
3412
+ promptVersion: opts.promptVersion,
3413
+ ...(opts.model === undefined ? {} : { model: opts.model }),
3414
+ ...(opts.temperature === undefined
3415
+ ? {}
3416
+ : { temperature: Number(opts.temperature) }),
3417
+ ...(opts.note === undefined ? {} : { note: opts.note }),
3418
+ });
3419
+ console.log(JSON.stringify(res, null, 2));
3420
+ });
3421
+ intel
3422
+ .command("monitor-label-spec-activate")
3423
+ .description("POST .../label-specs/:specId/activate —— 激活(当前 active 自动 retire)。" +
3424
+ "激活后 active 版本**不可原地改**,只能新建版本")
3425
+ .requiredOption("--spec <uuid>", "Label spec UUID(必须是 draft)")
3426
+ .action(async (opts) => {
3427
+ const res = await requireClient().activateSubredditMonitorLabelSpec(opts.spec);
3428
+ console.log(JSON.stringify(res, null, 2));
3429
+ });
3430
+ intel
3431
+ .command("monitor-label-spec-retire")
3432
+ .description("POST .../label-specs/:specId/retire —— ⚠️ 停用 = **标签功能整体关闭**" +
3433
+ "(卡片不再显示标签行);新帖提醒本身不受影响")
3434
+ .requiredOption("--spec <uuid>", "Label spec UUID(必须是 active)")
3435
+ .action(async (opts) => {
3436
+ const res = await requireClient().retireSubredditMonitorLabelSpec(opts.spec);
3437
+ console.log(JSON.stringify(res, null, 2));
3438
+ });
3439
+ intel
3440
+ .command("monitor-label-spec-delete")
3441
+ .description("DELETE .../label-specs/:specId —— 只能删 draft")
3442
+ .requiredOption("--spec <uuid>", "Label spec UUID")
3443
+ .action(async (opts) => {
3444
+ const res = await requireClient().deleteSubredditMonitorLabelSpecDraft(opts.spec);
3445
+ console.log(JSON.stringify(res, null, 2));
3446
+ });
3098
3447
  intel
3099
3448
  .command("hot-posts")
3100
3449
  .description("GET .../subreddit-intelligence/hot-posts")
@@ -4064,7 +4413,10 @@ calendar
4064
4413
  .option("--status <st>", "scheduled|running|succeeded|…")
4065
4414
  .option("--from <iso>", "from ISO 时间")
4066
4415
  .option("--to <iso>", "to ISO 时间")
4067
- .option("-n, --limit <n>", "limit", "50")
4416
+ .option("-n, --limit <n>", "limit(服务端上限 200)", "50")
4417
+ // 🔴 服务端一直支持 offset,此前只有 CLI 没暴露 —— 调用方要么撞上限,
4418
+ // 要么自己拿时间窗二分绕(矩阵侧实测:那样会静默漏掉更早的条目且不报错)。
4419
+ .option("--offset <n>", "offset(翻页;默认 0)")
4068
4420
  .action(async (opts) => {
4069
4421
  const res = await requireClient().listCalendarEntries(resolveTeamId(opts.team), {
4070
4422
  campaignId: opts.campaign,
@@ -4074,6 +4426,7 @@ calendar
4074
4426
  from: opts.from,
4075
4427
  to: opts.to,
4076
4428
  limit: Number(opts.limit),
4429
+ ...(opts.offset === undefined ? {} : { offset: Number(opts.offset) }),
4077
4430
  });
4078
4431
  console.log(JSON.stringify(res, null, 2));
4079
4432
  });
@@ -4109,6 +4462,8 @@ reddit
4109
4462
  .option("--repost-type <t>", "repostType filter (none|first|second)")
4110
4463
  .option("--contains-brand-keyword <v>", "true | false | unset")
4111
4464
  .option("--primary-product <text>", "primary product contains match")
4465
+ .option("--seq-no-min <n>", "序列号下界(含);与 --seq-no-max 组成闭区间,无编号(null)的帖会落选")
4466
+ .option("--seq-no-max <n>", "序列号上界(含)")
4112
4467
  .option("--sort-by <f>", "score|comments_count|posted_at|view_count|seq_no")
4113
4468
  .option("--sort-dir <d>", "asc|desc")
4114
4469
  .option("-n, --limit <n>", "limit per page", "100")
@@ -4127,6 +4482,8 @@ reddit
4127
4482
  repostType: opts.repostType,
4128
4483
  containsBrandKeyword: opts.containsBrandKeyword,
4129
4484
  primaryProduct: opts.primaryProduct,
4485
+ seqNoMin: opts.seqNoMin !== undefined ? Number(opts.seqNoMin) : undefined,
4486
+ seqNoMax: opts.seqNoMax !== undefined ? Number(opts.seqNoMax) : undefined,
4130
4487
  sortBy: opts.sortBy,
4131
4488
  sortDir: opts.sortDir,
4132
4489
  limit: Number(opts.limit),