@geoly-ai/social-hub-cli 0.3.7 → 0.3.9
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 +34 -0
- package/dist/cmd-manifest.json +18 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +35 -0
- package/dist/index.test.js.map +1 -1
- package/dist/permissions-gates-admin.d.ts.map +1 -1
- package/dist/permissions-gates-admin.js +8 -0
- package/dist/permissions-gates-admin.js.map +1 -1
- package/dist/permissions-gates-admin.test.js +10 -0
- package/dist/permissions-gates-admin.test.js.map +1 -1
- package/dist/permissions-gates-notify.d.ts.map +1 -1
- package/dist/permissions-gates-notify.js +81 -1
- package/dist/permissions-gates-notify.js.map +1 -1
- package/dist/permissions-gates-notify.test.js +10 -3
- package/dist/permissions-gates-notify.test.js.map +1 -1
- package/dist/register-notify.d.ts +38 -0
- package/dist/register-notify.d.ts.map +1 -1
- package/dist/register-notify.js +329 -14
- package/dist/register-notify.js.map +1 -1
- package/dist/register-notify.test.js +40 -1
- package/dist/register-notify.test.js.map +1 -1
- package/package.json +3 -3
- package/skills/manifest.json +1 -1
- package/skills/reddit-matrix.lock.json +14 -14
- package/skills/reddit-voc-volume/services/firecrawl/examples/o1_web_crawler/o1_web_crawler.py +152 -152
- package/skills/social-hub-admin/SKILL.md +41 -1
- package/skills/social-hub-notifications/SKILL.md +230 -14
- package/skills/social-hub-openclaw-context/SKILL.md +10 -0
- package/skills/social-hub-posts/SKILL.md +9 -2
- package/skills/social-hub-subreddit-pools/SKILL.md +51 -2
- package/dist/register-subreddit-post-style.d.ts +0 -8
- package/dist/register-subreddit-post-style.d.ts.map +0 -1
- package/dist/register-subreddit-post-style.js +0 -161
- package/dist/register-subreddit-post-style.js.map +0 -1
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
不用于 OpenClaw 任务领取(那是 social-hub-ops-runtime 的 claim/complete,有租约与终态)、
|
|
10
10
|
也不用于飞书审核通知渠道(social-hub-admin)。
|
|
11
11
|
metadata:
|
|
12
|
-
cliVersion: ">=0.3.
|
|
12
|
+
cliVersion: ">=0.3.9"
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
# social-hub-notifications
|
|
@@ -59,11 +59,31 @@ stdout **只有事件**(`--format jsonl` 时每行一个完整 envelope)。
|
|
|
59
59
|
|
|
60
60
|
## 建订阅
|
|
61
61
|
|
|
62
|
-
> **首次启用必读:该 team
|
|
63
|
-
>
|
|
64
|
-
>
|
|
65
|
-
>
|
|
66
|
-
>
|
|
62
|
+
> **首次启用必读:该 team 必须有一条 active 的 source config,否则建订阅会被拒**
|
|
63
|
+
> ——这是故意的,避免产生「看起来 active、永远收不到事件」的死订阅。
|
|
64
|
+
>
|
|
65
|
+
> config 有两条来路,**满足任意一条即可**:
|
|
66
|
+
>
|
|
67
|
+
> 1. **系统级模板自动派生**(推荐,迁移 0127)。如果运维已经为该 source
|
|
68
|
+
> 建好模板、激活、并打开了自动派生总闸,那么**这个 team 什么都不用做** ——
|
|
69
|
+
> 建订阅这一步本身就会当场派生出一条真正的 team config,
|
|
70
|
+
> 哪怕这个 team **从来没发过帖**。这正是「agent 在另一台机器上,先建订阅再等事件」
|
|
71
|
+
> 这个场景能走通的原因。查状态:`social-hub notify templates list`
|
|
72
|
+
> (或 `notify source-configs sources` 看该 team 的 `activeTemplate` 摘要)。
|
|
73
|
+
> 2. **人工建一条 team config**:见 [后台侧配置](#后台侧配置运维不是-agent-日常)。
|
|
74
|
+
> ⚠️ 一旦某个 team 有过**任何**人工版本(哪怕只是 draft、哪怕已经全部作废),
|
|
75
|
+
> 它就**永久脱离模板**、再也不会被自动派生顶回来。
|
|
76
|
+
>
|
|
77
|
+
> 两条都不满足时是 **409 `SUBSCRIPTION_SOURCE_NOT_CONFIGURED`**(不是 400;这个码是**可自愈**的信号:
|
|
78
|
+
> 等运维开总闸 / 建 config 后重试即可),message 里带上**为什么没派生成功**,直接照着排:
|
|
79
|
+
>
|
|
80
|
+
> | 文案里的 reason | 含义 / 处理 |
|
|
81
|
+
> | ---------------------------- | ----------------------------------------------------------------------------------- |
|
|
82
|
+
> | `template_missing` | 该 source 根本没有 active 模板 → 运维 `notify templates create/activate` |
|
|
83
|
+
> | `template_disabled` | 模板有了但**总闸没开** → 运维 `notify templates set-auto-derivation --enabled true` |
|
|
84
|
+
> | `identity_exists` | 这个 team 有过人工 config(可能全是 draft/已作废)→ 走人工路径激活一条 |
|
|
85
|
+
> | `template_registry_mismatch` | 模板钉的版本与当前 registry 不一致 → 运维要出新模板版本 |
|
|
86
|
+
> | `lock_busy` | 运维正在改模板的极短窗口 → **直接重试** |
|
|
67
87
|
|
|
68
88
|
```bash
|
|
69
89
|
# 一台机器一个订阅、各自独立游标(订阅之间是广播,不是竞争消费)
|
|
@@ -304,14 +324,143 @@ reset 会 **bump `receiptEpoch`**,作废全部在途 receipt。**绝不要**
|
|
|
304
324
|
|
|
305
325
|
## 常见故障对照
|
|
306
326
|
|
|
307
|
-
| 现象 | 原因
|
|
308
|
-
| ---------------------------------------------- |
|
|
309
|
-
| `--follow` 一直不动、心跳报同一个 `pendingSeq` | 你没 ack(多半是未知 schema 被隔离了)
|
|
310
|
-
| 游标永远卡在某条 | 把 tombstone 当业务事件处理失败了
|
|
311
|
-
| ack 一直 409 | 在重放旧 receipt
|
|
312
|
-
| pull 403 且 details 有 `missingPermissions` | 字段权限不足
|
|
313
|
-
| 某字段突然不见了 | 后台改了 `selectedFields`(`fieldSetVersion` 会变)
|
|
314
|
-
| 一整段时间没有事件 | source 被 kill switch 关了
|
|
327
|
+
| 现象 | 原因 | 处理 |
|
|
328
|
+
| ---------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
329
|
+
| `--follow` 一直不动、心跳报同一个 `pendingSeq` | 你没 ack(多半是未知 schema 被隔离了) | 看 stderr 的 eventId;升级 handler 后重跑 |
|
|
330
|
+
| 游标永远卡在某条 | 把 tombstone 当业务事件处理失败了 | `payloadStatus: "expired"` **必须** ack |
|
|
331
|
+
| ack 一直 409 | 在重放旧 receipt | 重新 pull 拿新 receipt;409 是 CAS 语义,不做幂等成功 |
|
|
332
|
+
| pull 403 且 details 有 `missingPermissions` | 字段权限不足 | **整个订阅**被拒(不做字段级脱敏):减少 `--require-fields` 或补权限 |
|
|
333
|
+
| 某字段突然不见了 | 后台改了 `selectedFields`(`fieldSetVersion` 会变) | 按 envelope 版本分支;**不要**自己补默认值 |
|
|
334
|
+
| 一整段时间没有事件 | source 被 kill switch 关了 | 先 `notify schedules summary`(**不要**从事件流反推:kill switch 的 missed 不产生事件),再 `notify source-configs list`;disabled 期间到点的节点标 missed,**重新启用不补发** |
|
|
335
|
+
| 某个帖子一条通知都没收到 | 主体被删/未发布/无锚点 → schedule 被 cancel | `notify schedules list --subject <snapshotId> --status cancelled`,看 `cancelReason` |
|
|
336
|
+
| 老 schedule 该被清理却一直在 | 保留期 GC cron 没跑 / 追不上 | `notify schedules purge-status`:`hasRunSinceRegistration: false` = **自本次 worker 注册以来**没观测到运行(**不是**「跑了零删除」;刚重启完看到 false 是正常的);连续多轮 `backlog: true` = 真的追不上,调预算/频率。`protectedGroups` 非零是**正常**的 sentinel 保护,别当积压 |
|
|
337
|
+
| 建订阅被拒,文案带 `template_disabled` | 系统级模板做好了但**自动派生总闸没开** | 运维 `notify templates set-auto-derivation --source <id> --enabled true --apply` |
|
|
338
|
+
| 建订阅被拒,文案带 `template_missing` | 该 source 根本没有系统级模板,该 team 也没人工 config | 运维二选一:建模板(`notify templates create/activate` + 开总闸)或人工建 team config |
|
|
339
|
+
| 激活 409 `SOURCE_CONFIG_ACTIVE_STATE_CONFLICT` | 你的 `--expect-active` 与服务端实际 active 不符(别人抢先改了) | 读 `details.actualActiveConfigId` / `actualActiveVersion`,确认对方的改动后再决定是否重试(**无需**再 GET 一次) |
|
|
340
|
+
|
|
341
|
+
## 运维查看面(`notify events` / `notify schedules`)
|
|
342
|
+
|
|
343
|
+
> 🔴 **这不是消费面。** `notify events list` 不推进游标、不签 receipt,拿它当消费手段
|
|
344
|
+
> 事件必然被漏掉。要不漏地消费只有 `notify pull` + `notify ack` 一条路。
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
# 最近 N 条事件(元数据摘要,不含 payload)。默认 --order desc
|
|
348
|
+
social-hub notify events list -t <team> --limit 20
|
|
349
|
+
# 往更老翻页:把上一页响应的 page.lastSeq 传给 --before
|
|
350
|
+
social-hub notify events list -t <team> --order desc --before <page.lastSeq> --limit 20
|
|
351
|
+
# 指定窗口 / 正序追
|
|
352
|
+
social-hub notify events list -t <team> --after 100 --before 200 --order asc
|
|
353
|
+
|
|
354
|
+
# 单条完整 envelope(按事件行上**冻结**的权限逐条重验,可能比当前 config 更严)
|
|
355
|
+
social-hub notify events get -t <team> --seq 12345
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**边界语义(记牢,翻页写反了会静默停在空白页):**
|
|
359
|
+
|
|
360
|
+
- `--after` 恒为排他**下界**、`--before` 恒为排他**上界**;
|
|
361
|
+
- `--order` **只**决定取区间哪一端的 N 条与返回顺序,**不改变**上下界含义;
|
|
362
|
+
- `--after >= --before` → **400**(矛盾区间恒为空,不会静默返回空数组);
|
|
363
|
+
- 响应里的 `latestSeq` / `earliestAvailableSeq` 是**流**的,与排序方向无关;
|
|
364
|
+
本页首尾在 `page.firstSeq` / `page.lastSeq`;
|
|
365
|
+
- tombstone(`payloadStatus: "expired"`)照常出现在列表里 —— 过滤掉会让你以为序号有空洞。
|
|
366
|
+
|
|
367
|
+
### 节点 schedule:`missed` / `cancelled` 只有查这里才看得见
|
|
368
|
+
|
|
369
|
+
> 🔴 **绝不能从事件流反推 schedule 状态。** kill switch 期间到点的 `missed` 与
|
|
370
|
+
> 锚点消失的 `cancelled` **根本不产生任何事件**。靠事件流反推只会把它们算成 0,
|
|
371
|
+
> 于是「今晚一条都没发」与「今晚本来就没有到期节点」长得一模一样。
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
# 按 source × status 聚合(scheduled/emitted/missed/cancelled/superseded)
|
|
375
|
+
social-hub notify schedules summary -t <team>
|
|
376
|
+
social-hub notify schedules summary -t <team> --due-from 2026-07-01T00:00:00Z --due-to 2026-07-08T00:00:00Z
|
|
377
|
+
|
|
378
|
+
# 列表(keyset 分页:把响应的 nextCursor 原样回传;null = 没有下一页)
|
|
379
|
+
social-hub notify schedules list -t <team> --status missed --limit 50
|
|
380
|
+
social-hub notify schedules list -t <team> --status missed --cursor <nextCursor>
|
|
381
|
+
|
|
382
|
+
# 单条:状态原因 + 关联事件 seq
|
|
383
|
+
social-hub notify schedules get -t <team> --id <scheduleId>
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
- 时间窗**必须有界**:缺省 = 最近 30 天,上限 **90 天**(超了 400)。
|
|
387
|
+
`--due-from` **含**、`--due-to` **不含**,相邻窗口拼接不会重复计数。
|
|
388
|
+
- 🔴 **空结果 ≠ 没有异常** —— 本表有保留期 GC(`emitted` / `superseded` **180 天**、
|
|
389
|
+
`missed` / `cancelled` **730 天**,判据是 `dueAt`;`scheduled` 永不删)。
|
|
390
|
+
90 天只限制窗口**跨度**,不限制窗口必须在最近 90 天内:查一年前的 30 天窗口完全合法,
|
|
391
|
+
但那已经在 `emitted` 档的保留期之外。summary / list 的响应都带 `retention`:
|
|
392
|
+
- `retention.completenessByStatus.<status>`:`full` = 窗口整体在保证保留期内,**没有就是真没有**;
|
|
393
|
+
`partial` = 窗口横跨保留 floor,更早的部分**可能**已被清理;`outside` = 窗口整体在保留期之外。
|
|
394
|
+
- `retention.policy.<status>.guaranteedRetainedFrom`:**保证**保留到的最早 `dueAt`
|
|
395
|
+
(不是实际最老行 —— 每个 subject 组会永久留一行 sentinel,比它更老的行也可能还在)。
|
|
396
|
+
- `retention.windowCompleteness`:只在**带 `--status` 过滤**时给出,否则为 `null`。
|
|
397
|
+
报结论时按它分流:`full` 才能说「窗口内没有异常」;`partial` / `outside` 只能说
|
|
398
|
+
「该窗口早于保证保留期,更早的历史**可能**已被清理」。
|
|
399
|
+
- 失败原因是**归一化枚举** `failureReason`:`kill_switch` / `max_lateness_exceeded` /
|
|
400
|
+
`processing_error`。**没有** `lastError` 原文(那是任意异常字符串,可能含内部细节);
|
|
401
|
+
`hasLastError` 只告诉你「存在原始异常」,原文在服务端日志里。
|
|
402
|
+
- ⚠️ `processing_error` **不代表终态** —— 它会出现在仍会重试的 `scheduled` 行上
|
|
403
|
+
(记账只 bump `attempts`,绝不按次数把节点判死)。判终态看 `status`。
|
|
404
|
+
- ⚠️ `status: "emitted"` 且 `eventId: null` 是**合法历史状态**:事件过 tombstone 期后整行
|
|
405
|
+
被清理,FK 会把 `event_id` 置 null。不是数据损坏。
|
|
406
|
+
|
|
407
|
+
### 保留期 GC 自己跑得怎么样(`notify schedules purge-status`)
|
|
408
|
+
|
|
409
|
+
上面那个 `retention` 讲的是**策略**(保证保留到哪天)。真正执行清理的是 worker 里的
|
|
410
|
+
GC cron(默认每天 **03:17**,与 dispatcher 刻意错开时刻,避免抢同一把 subject group 锁)。
|
|
411
|
+
它跑没跑、追不追得上,只有这里看得到:
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
# 系统级:**没有 --team**(状态是 worker 写的全局单 key,不是某个 team 的清理进度)
|
|
415
|
+
social-hub notify schedules purge-status
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
- 权限 `systemSetting:read`(**仅 admin/manager**)。与既有两个 cron status 端点同口径,
|
|
419
|
+
比 `notificationEvent:read` 更严 —— 响应带 `workerId` 与异常原文摘要。
|
|
420
|
+
|
|
421
|
+
🔴 **`null` ≠ `0`,别拿 `?? 0` 糊过去。** 判「跑没跑过」只看一个字段:
|
|
422
|
+
|
|
423
|
+
| 场景 | `hasRunSinceRegistration` | `status` | `deleted` |
|
|
424
|
+
| ---------------------- | ------------------------- | -------------- | --------- |
|
|
425
|
+
| 没有可读的状态快照 | `false` | `null` | `null` |
|
|
426
|
+
| 注册了但还没到点 | `false` | `"registered"` | `null` |
|
|
427
|
+
| 跑了、但这轮没东西可删 | `true` | `"success"` | `0` |
|
|
428
|
+
| 跑了、删了 | `true` | `"success"` | `>0` |
|
|
429
|
+
| 跑挂了 | `true` | `"failed"` | `null` |
|
|
430
|
+
| 正在跑 | `true` | `"running"` | `null` |
|
|
431
|
+
|
|
432
|
+
⚠️ 第一行的准确说法是「**没有可读的状态快照**」:可能 worker 从没起过这段代码,也可能状态
|
|
433
|
+
写失败 / 被清掉 / 存的值坏到无法归一。**不能**断言成「worker 从未启动」。
|
|
434
|
+
|
|
435
|
+
⚠️ `hasRunSinceRegistration` 的语义是「**自本次 worker 注册以来**跑过」,不是「历史上跑过」:
|
|
436
|
+
worker 每次重启都会重写 registered 快照并把运行字段清回 `null`,昨天跑成功的记录不会保留。
|
|
437
|
+
所以刚重启完看到 `false` 是正常的,不等于 GC 从来没工作过。
|
|
438
|
+
|
|
439
|
+
🔴 **三个字段长得像故障,但都不是**:
|
|
440
|
+
|
|
441
|
+
| 字段 | 含义 | 是不是错误 |
|
|
442
|
+
| ----------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
443
|
+
| `protectedGroups` | 被 sentinel / `has_scheduled` 不变量**有意保护**、本轮跳过不删的 subject 组 | **不是**。sentinel 保证永不把一个 subject 组删空 —— 删空了同一个帖子会再收到第二轮通知。这个数常年非零才是正常状态 |
|
|
444
|
+
| `lockBusyGroups` | 该组的 advisory lock 正被物化/dispatcher 持有,本轮让路 | **不是**。下一轮自然重试。只有连续多轮居高不下才值得看锁竞争 |
|
|
445
|
+
| `backlog` | 由**独立的 `LIMIT 1` probe** 判定的「还有待清理候选」 | **不是**错误,但是**确定的**追赶压力信号 |
|
|
446
|
+
| `budgetExhausted` | 本轮撞到单次删除预算上限而收工 | **不是**。容量信号 |
|
|
447
|
+
| `failedGroups` | 组级删除真的抛异常 | 🔴 **是**。>0 要查 worker 日志 |
|
|
448
|
+
|
|
449
|
+
- 🔴 `backlog` 的判据要说清楚:它**不是**拿「最后一批恰好取满」当证据(那种猜法在总数
|
|
450
|
+
恰好整除批大小时会假阳性)。它是清理跑完后单独发一条 `LIMIT 1` probe 去问「还有没有
|
|
451
|
+
符合删除条件的行」。因此 `backlog: true` 表示**确实还有**待清理候选,而不是「可能还有」;
|
|
452
|
+
`backlog: false` 表示**确实追平了**。
|
|
453
|
+
- 判「GC 追不上」的正确姿势:连续多天 `backlog: true`(尤其同时 `budgetExhausted: true`),
|
|
454
|
+
说明单轮预算或频率不够,调 `NOTIFICATION_SCHEDULE_PURGE_CRON` / 预算,而不是去看
|
|
455
|
+
`protectedGroups`。
|
|
456
|
+
- `deletedByTier` 是逐档删除数(状态档 → 行数);`{}` = 这轮没有任何一档删到东西。
|
|
457
|
+
- `schedule` 是 **API 进程**的 env 回退值,**不权威**(worker 只认自己进程的
|
|
458
|
+
`NOTIFICATION_SCHEDULE_PURGE_CRON`)。推算下次触发用的是 `effectiveCron`
|
|
459
|
+
—— 它优先取 worker 注册时自报的 `registeredCron`。⚠️ `registeredCron` 只说明「某个 worker
|
|
460
|
+
曾经用这个表达式注册过」,**不证明它还活着**(没有 heartbeat),所以 `nextRunAt` 是**推算值**,
|
|
461
|
+
不是「保证会在这一刻跑」。`nextRunAt` 按 `effectiveCron` 推算,
|
|
462
|
+
表达式非法时为 `null`(不会 500)。
|
|
463
|
+
⚠️ `nextRunAt` 用 **API 进程的本地时区**计算;API 与 worker 的 `TZ` 不一致时它会算偏。
|
|
315
464
|
|
|
316
465
|
## 后台侧配置(运维,不是 agent 日常)
|
|
317
466
|
|
|
@@ -328,11 +477,78 @@ social-hub notify source-configs create -t <team> --source post-performance-node
|
|
|
328
477
|
social-hub notify source-configs create ... --no-activate --apply # 只建 draft
|
|
329
478
|
social-hub notify source-configs activate -t <team> --source post-performance-node.v1 --id <uuid> --apply
|
|
330
479
|
|
|
480
|
+
# 🔴 两个管理员同时激活不同版本时,**不带 CAS 的那次会静默覆盖对方**。
|
|
481
|
+
# 先看当前 active,再把它作为期望值传回去:
|
|
482
|
+
social-hub notify source-configs sources -t <team> # 取 activeConfig.id / .version
|
|
483
|
+
social-hub notify source-configs activate -t <team> --source post-performance-node.v1 \
|
|
484
|
+
--id <uuid> --expect-active <当前 activeConfig.id> --expect-active-version <version> --apply
|
|
485
|
+
# 「我认为当前根本没有 active」:
|
|
486
|
+
social-hub notify source-configs activate ... --expect-active none --apply
|
|
487
|
+
# 创建即激活也支持同一套(否则「新建并激活」就是绕过 CAS 的后门):
|
|
488
|
+
social-hub notify source-configs create ... --expect-active <当前 activeConfig.id> --apply
|
|
489
|
+
|
|
331
490
|
# 实时 kill switch —— 立刻影响【全部在途 schedule】,且不 bump sourceConfigVersion
|
|
332
491
|
social-hub notify source-configs set-enabled -t <team> \
|
|
333
492
|
--source post-performance-node.v1 --enabled false --apply
|
|
334
493
|
```
|
|
335
494
|
|
|
495
|
+
### 系统级模板(`notify templates`,改的是**全体 team** 的默认值)
|
|
496
|
+
|
|
497
|
+
用途:节点档位是**运营口径**,不该逐团队配。模板给出一份系统级默认档位 + 字段集,
|
|
498
|
+
team 首次物化 / backfill / **建订阅**时各自派生出一条真正的 team config 行
|
|
499
|
+
(`origin = "template"`,`activeConfig` 里能看到 `templateId` / `templateVersion`)。
|
|
500
|
+
|
|
501
|
+
🔴 **不带 `-t/--team`** —— 这些端点在 `/v1/system/*`,只有**系统 admin**(且必须是不受限的
|
|
502
|
+
交互式登录:api_key / 服务 token / job-bound 或带动作白名单的 CLI token 一律 403)能写。
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
# 各 source 的当前 active 模板 + 总闸状态
|
|
506
|
+
social-hub notify templates list
|
|
507
|
+
social-hub notify templates versions --source post-performance-node.v1 [--limit 50]
|
|
508
|
+
social-hub notify templates get --source post-performance-node.v1 --id <uuid>
|
|
509
|
+
|
|
510
|
+
# 建模板版本(默认【不】激活;body 与 source config 的 upsert 同款,多一个必填 sourceId)
|
|
511
|
+
social-hub notify templates create --source post-performance-node.v1 --json-file ./tpl.json --apply
|
|
512
|
+
social-hub notify templates activate --source post-performance-node.v1 --id <uuid> --apply
|
|
513
|
+
|
|
514
|
+
# 🔴 自动派生总闸 —— 打开 = 全体没配过该 source 的 team 开始产生通知
|
|
515
|
+
social-hub notify templates set-auto-derivation \
|
|
516
|
+
--source post-performance-node.v1 --enabled true --apply
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
**两道闸缺一不可**:模板 `isActive`(activate)**且** `autoDerivationEnabled`(set-auto-derivation)。
|
|
520
|
+
所以「建了模板还是没人收到通知」多半是总闸没开 —— `notify templates list` 一眼可见。
|
|
521
|
+
|
|
522
|
+
⚠️ **`autoDerivationEnabled` 不是 kill switch**(刻意不叫 `enabled` 就是为了防混):
|
|
523
|
+
关掉它**只**停止给**新** team 派生;已经派生出来的 team config 照常发通知。
|
|
524
|
+
要停某个 team,用那个 team 的 `notify source-configs set-enabled --enabled false`。
|
|
525
|
+
|
|
526
|
+
⚠️ 模板出新版本**不会**跟进已派生的 team(它们已经有版本行了 → `identity_exists`)。
|
|
527
|
+
模板只管 bootstrap,不做全量下发。
|
|
528
|
+
|
|
529
|
+
### 🔴 写 `selectedFields` 前必读(踩过两次的坑)
|
|
530
|
+
|
|
531
|
+
1. **必须 ASCII 升序去重**,否则 400。这是 `fieldSetVersion` 哈希稳定性的要求(同一字段集合必须得到同一标识)。
|
|
532
|
+
2. **有一组字段「自动包含」,勾了会被拒**——它们本来就会出现在 payload 里,不用也不能勾:
|
|
533
|
+
|
|
534
|
+
```
|
|
535
|
+
trigger.emittedAt trigger.lateBySeconds trigger.effectiveNode
|
|
536
|
+
trigger.winnerScheduleId trigger.supersededNodes trigger.missedNodes
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
(`trigger.offsetSeconds` **可以**勾,别搞混。)
|
|
540
|
+
|
|
541
|
+
3. **`--dry-run` 不做任何服务端校验**,只打印将发出的请求。字段集/排序/归属错误**只有 `--apply` 才会暴露**——预检通过不代表能建成。
|
|
542
|
+
4. 权威清单用 `social-hub notify source-configs sources` 现查:`requiredFields` 恒在、
|
|
543
|
+
`optionalFields` 里除上面那 6 个之外都可勾;带 `requires` 的字段必须**连同依赖一起勾**
|
|
544
|
+
(例如 `browserEnvironment.environmentId` 要求先勾 `routingStatus` + `routingBasis`)。
|
|
545
|
+
5. **`browserEnvironment.*` 按需勾,不要默认勾上。** 判据是**这次截图需不需要登录态**:
|
|
546
|
+
- **公开帖页截图**(打开 permalink 截个图)→ **不需要**,`subject.permalink` 就够了。
|
|
547
|
+
多勾这组只是白白扩大暴露面(其中 `environmentId` 还是 `restricted` 敏感级,
|
|
548
|
+
会连带抬高订阅方所需的 authz 权限)。
|
|
549
|
+
- **需要登录态**(内部账号视角、只有登录后可见的后台数据)→ 才勾。理由是一个账号在
|
|
550
|
+
Hub 里可能挂多个浏览器环境,不带这组字段 agent 不知道该开哪个。
|
|
551
|
+
|
|
336
552
|
- 后台**不能**扩大数据暴露面:`selectedFields` 只能勾 registry 声明的 optional 字段,
|
|
337
553
|
required 不可勾也不可取消,未知路径直接拒。
|
|
338
554
|
- 后台**不能**绕过 authz,也**不能**改 canonical topic。
|
|
@@ -95,6 +95,16 @@ social-hub context job -t <team-id> --job <scheduledJobId>
|
|
|
95
95
|
|
|
96
96
|
互动选板 **主读** `subreddit-candidates`;发布前查池可用 `account-pools`。
|
|
97
97
|
|
|
98
|
+
> ⚠️ **`account-pools` 的 `allowedActions` 按 `sourceScope` 收敛**(2026-08-03 起):
|
|
99
|
+
>
|
|
100
|
+
> - `sourceScope: "account"`(人设/品牌展开)→ `browse` / `upvote` / `comment` / `comment_deep` / `publish_post`
|
|
101
|
+
> - `sourceScope: "system"`(全局共享 catalog)→ **只有** `browse` / `upvote`
|
|
102
|
+
>
|
|
103
|
+
> 系统级共享池**不作评论/发帖目标**。此前该端点对所有行一律返回 5 个动作,
|
|
104
|
+
> 与 `subreddit-candidates` 的口径(早已排除 system-scope 评论候选)不一致 ——
|
|
105
|
+
> 那是**错误的授权信号**,不是可用能力。若你的脚本此前拿 system 行发帖/评论,
|
|
106
|
+
> 请改从 `subreddit-candidates` 取候选。
|
|
107
|
+
|
|
98
108
|
```bash
|
|
99
109
|
# 账号板块聚合(系统级 + 账号级;与 account-pool-context 同响应)
|
|
100
110
|
social-hub query account-pools -t <team-id> --account A1 \
|
|
@@ -158,11 +158,18 @@ social-hub reddit stats -t "$BUCKET" # 桶内聚合
|
|
|
158
158
|
- **桶不是权限旁路**:品牌/(账号,品牌) 成对授权在桶里照常生效,受限主体在桶路径下也只看得到自己品牌的帖子;`batch-upsert` 对受限(client)主体**整条关闭**,不看权限矩阵怎么配。
|
|
159
159
|
- 环境变量 API key(`SOCIAL_HUB_API_KEY`)**不能**访问桶路径,也不能把帖子改派进桶——API key 严格绑定单团队。回填一律用 `auth login` 的 CLI token。
|
|
160
160
|
- 桶是**多方共享**的团队,桶里的行有**归属主体**(谁导入/创建/改派进来的)。
|
|
161
|
-
-
|
|
161
|
+
- 归属在**所有请求侧(人工/agent 发起)的写入面**强制:`batch-upsert` 的覆盖、刷新批次的查询/取消、`create-snapshot` 去重、**按 ID 的 `update`(PUT)/`delete`/批量 patch/`refresh`(单帖)/`refresh-selected`**,以及**按筛选条件的 `refresh-all` / `refresh-filtered`**(`refresh-preview` 是它们配套的预估面,受同一套目标授权口径)。**例外只有系统自己**:后台 worker 的全局刷新 cron 不带归属闸门(否则系统刷新会被静默关掉),它刷全部行是设计,不是漏洞。
|
|
162
|
+
- **刷新也是编辑面**,不是只读:它会覆盖 title/body/permalink,抓取失败(限流、临时 404)时还可能把 `opsStatus` 刷成 `deleted`。所以请求侧的刷新面与 `update`/`delete` 受同一道归属闸门——别人的桶行你刷不动(单帖 404,批量/按筛选逐行跳过)。别拿刷新当改帖用。
|
|
163
|
+
- **`refresh-all` / `refresh-filtered` / `refresh-preview` 按目标行判定,不看你从哪个团队路径进来**:在普通团队路径上用 `--aggregate/scopeTeamId` 把桶拉进范围也一样受闸门。越权表现为**目标集里没有那些行**(不是 403):`refresh-preview` 报的 `matched`/`distinctJobs` 已经是**你真刷得到的**数量,可能小于同筛选下列表看到的总数——这不是漏刷,是归属过滤。
|
|
164
|
+
- `refresh-preview` 与 `refresh-filtered` 用**同一个判据**算目标集,所以正常确认不会因为归属过滤而报 `REFRESH_PREVIEW_STALE`;真报了就是筛选期间目标集确实变了(新帖入库、可见范围变化),重新 preview 即可。
|
|
165
|
+
- ⚠️ 残余风险:闸门只保证"越权的行进不了批次"。**跨团队聚合刷新的执行侧仍按批次的路径团队推进**(工作项没带目标行团队),所以从普通团队路径聚合刷桶里的行时,`queued` 数量不等于"这些行都被正确刷新了"——跨团队批量刷新请直接在目标团队/桶路径上发起。
|
|
166
|
+
- **CLI token(agent)在桶里只动得了自己拥有的行**:别人的行、迁移前的存量行(`system:legacy`)、以及无主行,单条 `update`(PUT)/`delete`/`refresh` 一律 404(与"不存在"同码,不用状态码差异探测帖子是否存在);批量 patch 与 `refresh-selected` 见下条,是逐行跳过而不是整批 404。这不是 bug,是共享桶的写隔离——桶里混着多方的数据。
|
|
167
|
+
- **后台网页(会话用户)不受此限**:桶的用途就是「归属未定的暂存区,靠人工分派」,所以后台运营仍可修改/删除/分派桶里任何人的行(`client` 角色除外,它同样受归属约束)。**要改别人导入的行,走后台,不要用 CLI 绕。**
|
|
168
|
+
- 批量 patch、`refresh-selected` 与按筛选的刷新都是**逐行**判定:一批里有权的照改/照刷、无权的原样跳过,不整批失败。返回的 `{updated, requested}` / `{requested, authorized, eligible}` 里少掉的条数可能是越权、不存在、或不在可见范围——**不区分**,别据此推断某一行的归属。
|
|
162
169
|
- `batch-upsert` 命中别人的行、或迁移前就存在的存量桶行时**不会覆盖**,计入返回的 `conflicted`(`{imported, updated, skipped, conflicted}`)。`conflicted > 0` 不是失败,是"这些行不归你"——不要改 permalink 绕开,找归属方或管理员处理。
|
|
163
170
|
- 用 `create-snapshot` 往桶里补录时,若该 permalink 已属于别人,返回 404(与"不存在"同码),不会把别人的行 id 交给你。
|
|
164
171
|
- 归属按**用户身份**(登录 `auth login` 的那个人),不按 token:换一把 CLI token 仍能维护自己导入的行。
|
|
165
|
-
- 刷新批次同理按发起方归属:桶上的 `refresh-all` 进度查询/取消**只对发起者可见**,别人的批次一律 404。桶上跑 `refresh-all`
|
|
172
|
+
- 刷新批次同理按发起方归属:桶上的 `refresh-all` 进度查询/取消**只对发起者可见**,别人的批次一律 404。桶上跑 `refresh-all` 时行级品牌授权与归属闸门**同时**生效(取交集:既要品牌可见,又要行归你)。
|
|
166
173
|
|
|
167
174
|
### 幂等与风险
|
|
168
175
|
|
|
@@ -11,10 +11,53 @@ description: >-
|
|
|
11
11
|
|
|
12
12
|
先读 `social-hub-shared` 完成 `auth login` 与 `context use`。
|
|
13
13
|
|
|
14
|
+
## ⚠️ 先看清楚:catalog / tier-rules 是**系统级共享**,aliases 才是团队级
|
|
15
|
+
|
|
16
|
+
| 资源 | 层级 | 全局唯一键 | `-t <team>` 的作用 |
|
|
17
|
+
| ------------ | -------------- | ------------------------------- | -------------------------------- |
|
|
18
|
+
| `catalog` | **系统级共享** | `(industryKey, subreddit)` | 鉴权 + 新建行署名 + 列表视图过滤 |
|
|
19
|
+
| `tier-rules` | **系统级共享** | `subreddit` | 鉴权 + 新建行署名 + 列表视图过滤 |
|
|
20
|
+
| `aliases` | 团队级(真隔离) | `(team, aliasRef, industryKey)` | 真正的数据边界 |
|
|
21
|
+
|
|
22
|
+
对 catalog / tier-rules:
|
|
23
|
+
|
|
24
|
+
- **任何团队的改动对全局生效**。板块目录回答的是"这个行业有哪些板块值得抓",
|
|
25
|
+
是跨团队共享的知识;抓取任务(worker)读的就是全表,不分团队。
|
|
26
|
+
- `-t` **不做数据隔离**:`catalog-update` / `catalog-delete` / `tier-rules-update` /
|
|
27
|
+
`tier-rules-delete` 的 `--id` 是**全局 id**,任何团队路径都能命中任何一行。
|
|
28
|
+
所以 `catalog-list -t A` 列不出来的行,`catalog-delete -t A --id <行id>` 照样能改
|
|
29
|
+
—— 这是系统级资源的固有结果,**不是 bug**。
|
|
30
|
+
- `catalog-list` / `tier-rules-list` 的 `-t` 只是**按归属署名过滤列表视图**
|
|
31
|
+
(一个 view filter,不是授权边界)。⚠️ **列表只能看到本团队署名的行,但改是全局的**
|
|
32
|
+
—— 别把"列表里没有"当成"这个板块不存在"。
|
|
33
|
+
⚠️ 加 `--industry-key` **绕不过**署名过滤(它只是再叠一个条件)。要跨团队查重,
|
|
34
|
+
必须显式加 `--aggregate-visible-teams`(`catalog-list` / `tier-rules-list` 都支持):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 建行前查重:这个 (industryKey, subreddit) 是不是已经有别的团队录过了
|
|
38
|
+
social-hub pools catalog-list -t <team-id> --aggregate-visible-teams --industry-key coffee -n 200
|
|
39
|
+
social-hub pools tier-rules-list -t <team-id> --aggregate-visible-teams -n 200
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
🔴 **它是「跨可见团队的署名聚合视图」,不是无条件全表**:范围 = 当前登录主体
|
|
43
|
+
**可见的团队**,不可见团队署名的行仍然看不到(API key 主体还会被直接拒)。
|
|
44
|
+
所以 `--aggregate-visible-teams` 查不到 ≠ 全局不存在 —— 它把漏判概率压到很低,
|
|
45
|
+
但**不是**并发正确性保证。真正兜底重复的是 DB 的全局唯一约束 + upsert 语义:
|
|
46
|
+
撞上了会**更新那一行**(见下),不会新建副本。
|
|
47
|
+
|
|
48
|
+
- `catalog-create` / `tier-rules-create` 是 **upsert**:提交一个已存在的
|
|
49
|
+
`(industryKey, subreddit)` / `subreddit` 会**更新那一行**(可能是别的团队建的),
|
|
50
|
+
不会新建一份团队私有副本。
|
|
51
|
+
- **归属署名(行上的 `teamId`)不可变**:只在真正新建时写入,后续任何团队编辑都不改它。
|
|
52
|
+
想知道"谁改的"看审计,别看这一列。
|
|
53
|
+
- ⚠️ **`catalog-delete` 实际是 archive**(`status='archived'`),而且影响**全局**:
|
|
54
|
+
任一团队归档一个板块,**所有团队的抓取任务都不再选它**。归档前先确认没有别的团队
|
|
55
|
+
在依赖这个板块。
|
|
56
|
+
|
|
14
57
|
## 命令组
|
|
15
58
|
|
|
16
59
|
```bash
|
|
17
|
-
social-hub pools catalog-list -t <team-id> [--pool-kind pet|common|industry|brand|other]
|
|
60
|
+
social-hub pools catalog-list -t <team-id> [--pool-kind pet|common|industry|brand|other] [--industry-key <key>] [--status active|paused|archived] [--aggregate-visible-teams]
|
|
18
61
|
social-hub pools catalog-create -t <team-id> -j '{"industryKey":"...","industryLabel":"...","subreddit":"..."}'
|
|
19
62
|
social-hub pools catalog-update -t <team-id> --id <pool-id> -j '{...}'
|
|
20
63
|
social-hub pools catalog-delete -t <team-id> --id <pool-id>
|
|
@@ -24,7 +67,7 @@ social-hub pools aliases-create -t <team-id> -j '{"aliasRef":"persona_pools.P23.
|
|
|
24
67
|
social-hub pools aliases-update -t <team-id> --id <alias-id> -j '{...}'
|
|
25
68
|
social-hub pools aliases-delete -t <team-id> --id <alias-id>
|
|
26
69
|
|
|
27
|
-
social-hub pools tier-rules-list -t <team-id>
|
|
70
|
+
social-hub pools tier-rules-list -t <team-id> [--tier S0|S1|S2|S3] [--enabled true|false] [--aggregate-visible-teams]
|
|
28
71
|
social-hub pools tier-rules-create -t <team-id> -j '{...}'
|
|
29
72
|
social-hub pools tier-rules-update -t <team-id> --id <rule-id> -j '{...}'
|
|
30
73
|
social-hub pools tier-rules-delete -t <team-id> --id <rule-id>
|
|
@@ -53,6 +96,12 @@ social-hub pools tier-rules-delete -t <team-id> --id <rule-id>
|
|
|
53
96
|
|
|
54
97
|
## 常见排障
|
|
55
98
|
|
|
99
|
+
- `catalog-delete -t A --id X` 改到了"别的团队的同名行":**这是预期行为**,不是 bug。
|
|
100
|
+
catalog 全局只有一行同 `(industryKey, subreddit)`,`-t` 不参与定位。改之前先用
|
|
101
|
+
`catalog-list --aggregate-visible-teams`(或后台页面的跨团队聚合视图)确认这一行的
|
|
102
|
+
归属与内容。
|
|
103
|
+
- `tier-rules-update` 想把规则的 `subreddit` 改成另一条规则已占用的值 → 返回 409。
|
|
104
|
+
以前这里会静默改到那另一条规则上、无视 URL 里的 `--id`;现在明确报冲突。
|
|
56
105
|
- Pool Aliases 页面为空:先确认是否跑过 `db:seed` 或 persona pool bootstrap;再用 `aliases-list` 查当前 team,而不是只看 catalog。
|
|
57
106
|
- `personaPools.unresolvedRefs` 仍有 `persona_pools.Pxx.starter/advanced`:说明 alias 缺失,或 alias 指向的 `industryKey` catalog rows 未创建。
|
|
58
107
|
- catalog 有数据但候选为空:检查 subreddit 是否被 `bot_bouncer_blocklist`、`previously_banned`、team blocklist 或 tier policy 过滤。
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
/**
|
|
3
|
-
* 板块运营数据 Phase 1 —— 热帖风格标注(subreddit_post_style_labels)CLI 读 + 单人复核。
|
|
4
|
-
* SDK 未覆盖(同 subreddit-style/annotations 域约定,HTTP 走 hubRequest)。复核 user-only、
|
|
5
|
-
* 单人(非四眼);accept 可修正 primary/secondary/other,写一条 team 级 human 行(不改 LLM 行)。
|
|
6
|
-
*/
|
|
7
|
-
export declare function registerSubredditPostStyleCommands(program: Command): void;
|
|
8
|
-
//# sourceMappingURL=register-subreddit-post-style.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"register-subreddit-post-style.d.ts","sourceRoot":"","sources":["../src/register-subreddit-post-style.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMzC;;;;GAIG;AACH,wBAAgB,kCAAkC,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2LzE"}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { hubRequest, resolveTeamId } from "./client.js";
|
|
2
|
-
import { assertApplyOrDryRun } from "./dry-run.js";
|
|
3
|
-
import { printResult, withCliErrorBoundary } from "./output.js";
|
|
4
|
-
import { withPermissionGates } from "./permission-runner.js";
|
|
5
|
-
/**
|
|
6
|
-
* 板块运营数据 Phase 1 —— 热帖风格标注(subreddit_post_style_labels)CLI 读 + 单人复核。
|
|
7
|
-
* SDK 未覆盖(同 subreddit-style/annotations 域约定,HTTP 走 hubRequest)。复核 user-only、
|
|
8
|
-
* 单人(非四眼);accept 可修正 primary/secondary/other,写一条 team 级 human 行(不改 LLM 行)。
|
|
9
|
-
*/
|
|
10
|
-
export function registerSubredditPostStyleCommands(program) {
|
|
11
|
-
const group = program
|
|
12
|
-
.command("subreddit-post-styles")
|
|
13
|
-
.description("Reddit 热帖风格标注(风格标签底座 Phase 1;分析数据,非发布 gate)");
|
|
14
|
-
group
|
|
15
|
-
.command("list")
|
|
16
|
-
.description("GET /v1/teams/:teamId/subreddit-post-styles(本团队行+全局行;teamId=null 为全局)")
|
|
17
|
-
.requiredOption("--subreddit <name>", "板块名")
|
|
18
|
-
.option("-t, --team <teamId>", "Team UUID(缺省用当前 context)")
|
|
19
|
-
.option("--taxonomy <v>", "taxonomyVersion 过滤")
|
|
20
|
-
.option("--source <s>", "llm | human")
|
|
21
|
-
.option("--review-status <s>", "proposed | accepted | rejected")
|
|
22
|
-
.option("--no-global", "只看本团队行(排除全局)")
|
|
23
|
-
.option("-n, --limit <n>", "limit", "50")
|
|
24
|
-
.option("--offset <n>", "offset", "0")
|
|
25
|
-
.action(async (opts) => {
|
|
26
|
-
await withCliErrorBoundary(async () => {
|
|
27
|
-
const teamId = resolveTeamId(opts.team);
|
|
28
|
-
await withPermissionGates({
|
|
29
|
-
command: "subreddit-post-styles list",
|
|
30
|
-
gates: [{ resource: "subredditPostAnnotation", action: "read" }],
|
|
31
|
-
teamId,
|
|
32
|
-
}, async () => {
|
|
33
|
-
const params = new URLSearchParams();
|
|
34
|
-
params.set("subreddit", opts.subreddit);
|
|
35
|
-
if (opts.taxonomy)
|
|
36
|
-
params.set("taxonomyVersion", opts.taxonomy);
|
|
37
|
-
if (opts.source)
|
|
38
|
-
params.set("source", opts.source);
|
|
39
|
-
if (opts.reviewStatus)
|
|
40
|
-
params.set("reviewStatus", opts.reviewStatus);
|
|
41
|
-
if (opts.global === false)
|
|
42
|
-
params.set("includeGlobal", "false");
|
|
43
|
-
params.set("limit", String(opts.limit));
|
|
44
|
-
params.set("offset", String(opts.offset));
|
|
45
|
-
printResult(await hubRequest(`/v1/teams/${encodeURIComponent(teamId)}/subreddit-post-styles?${params.toString()}`));
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
group
|
|
50
|
-
.command("summary")
|
|
51
|
-
.description("GET .../subreddit-post-styles/summary — 风格分布 + 覆盖率 + 表现关联(posted_at 窗口;human>llm、team>global)")
|
|
52
|
-
.requiredOption("--subreddit <name>", "板块名")
|
|
53
|
-
.option("-t, --team <teamId>", "Team UUID(缺省用当前 context)")
|
|
54
|
-
.option("--taxonomy <v>", "taxonomyVersion 过滤")
|
|
55
|
-
.option("--since <iso>", "posted_at 窗口起(ISO)")
|
|
56
|
-
.option("--until <iso>", "posted_at 窗口止(ISO)")
|
|
57
|
-
.option("--no-global", "只看本团队行(排除全局)")
|
|
58
|
-
.action(async (opts) => {
|
|
59
|
-
await withCliErrorBoundary(async () => {
|
|
60
|
-
const teamId = resolveTeamId(opts.team);
|
|
61
|
-
await withPermissionGates({
|
|
62
|
-
command: "subreddit-post-styles summary",
|
|
63
|
-
gates: [{ resource: "subredditPostAnnotation", action: "read" }],
|
|
64
|
-
teamId,
|
|
65
|
-
}, async () => {
|
|
66
|
-
const params = new URLSearchParams();
|
|
67
|
-
params.set("subreddit", opts.subreddit);
|
|
68
|
-
if (opts.taxonomy)
|
|
69
|
-
params.set("taxonomyVersion", opts.taxonomy);
|
|
70
|
-
if (opts.since)
|
|
71
|
-
params.set("since", opts.since);
|
|
72
|
-
if (opts.until)
|
|
73
|
-
params.set("until", opts.until);
|
|
74
|
-
if (opts.global === false)
|
|
75
|
-
params.set("includeGlobal", "false");
|
|
76
|
-
printResult(await hubRequest(`/v1/teams/${encodeURIComponent(teamId)}/subreddit-post-styles/summary?${params.toString()}`));
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
group
|
|
81
|
-
.command("accept")
|
|
82
|
-
.description("POST .../subreddit-post-styles/accept — 单人接受一条 LLM proposed 标签(写 team 级 human accepted 行;可修正)")
|
|
83
|
-
.requiredOption("--label-id <uuid>", "被接受的 LLM proposed 标签 id")
|
|
84
|
-
.option("-t, --team <teamId>", "Team UUID(缺省用当前 context)")
|
|
85
|
-
.option("--primary-style <s>", "修正主风格(省略则原样采纳)")
|
|
86
|
-
.option("--secondary-styles <csv>", "修正副风格(逗号分隔,≤3)")
|
|
87
|
-
.option("--other-label <text>", "primary=other 时的自由标签")
|
|
88
|
-
.option("--note <text>", "备注(≤2000)")
|
|
89
|
-
.option("--dry-run", "预览", false)
|
|
90
|
-
.option("--apply", "执行写入", false)
|
|
91
|
-
.action(async (opts) => {
|
|
92
|
-
await withCliErrorBoundary(async () => {
|
|
93
|
-
const teamId = resolveTeamId(opts.team);
|
|
94
|
-
const body = { labelId: opts.labelId };
|
|
95
|
-
if (opts.primaryStyle)
|
|
96
|
-
body.primaryStyle = opts.primaryStyle;
|
|
97
|
-
if (opts.secondaryStyles)
|
|
98
|
-
body.secondaryStyles = String(opts.secondaryStyles)
|
|
99
|
-
.split(",")
|
|
100
|
-
.map((s) => s.trim())
|
|
101
|
-
.filter(Boolean);
|
|
102
|
-
if (opts.otherLabel !== undefined)
|
|
103
|
-
body.otherLabel = opts.otherLabel;
|
|
104
|
-
if (opts.note)
|
|
105
|
-
body.note = opts.note;
|
|
106
|
-
await withPermissionGates({
|
|
107
|
-
command: "subreddit-post-styles accept",
|
|
108
|
-
gates: [
|
|
109
|
-
{ resource: "subredditPostAnnotation", action: "create" },
|
|
110
|
-
{ resource: "subredditPostAnnotation", action: "update" },
|
|
111
|
-
],
|
|
112
|
-
teamId,
|
|
113
|
-
userOnly: true,
|
|
114
|
-
}, async () => {
|
|
115
|
-
if (!assertApplyOrDryRun(opts, {
|
|
116
|
-
command: "subreddit-post-styles accept",
|
|
117
|
-
danger: "write",
|
|
118
|
-
summary: { teamId, ...body },
|
|
119
|
-
})) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
printResult(await hubRequest(`/v1/teams/${encodeURIComponent(teamId)}/subreddit-post-styles/accept`, { method: "POST", body }));
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
group
|
|
127
|
-
.command("reject")
|
|
128
|
-
.description("POST .../subreddit-post-styles/reject — 单人拒绝一条 LLM proposed 标签(写 team 级 human rejected 行)")
|
|
129
|
-
.requiredOption("--label-id <uuid>", "被拒绝的 LLM proposed 标签 id")
|
|
130
|
-
.option("-t, --team <teamId>", "Team UUID(缺省用当前 context)")
|
|
131
|
-
.option("--note <text>", "备注(≤2000)")
|
|
132
|
-
.option("--dry-run", "预览", false)
|
|
133
|
-
.option("--apply", "执行写入", false)
|
|
134
|
-
.action(async (opts) => {
|
|
135
|
-
await withCliErrorBoundary(async () => {
|
|
136
|
-
const teamId = resolveTeamId(opts.team);
|
|
137
|
-
const body = { labelId: opts.labelId };
|
|
138
|
-
if (opts.note)
|
|
139
|
-
body.note = opts.note;
|
|
140
|
-
await withPermissionGates({
|
|
141
|
-
command: "subreddit-post-styles reject",
|
|
142
|
-
gates: [
|
|
143
|
-
{ resource: "subredditPostAnnotation", action: "create" },
|
|
144
|
-
{ resource: "subredditPostAnnotation", action: "update" },
|
|
145
|
-
],
|
|
146
|
-
teamId,
|
|
147
|
-
userOnly: true,
|
|
148
|
-
}, async () => {
|
|
149
|
-
if (!assertApplyOrDryRun(opts, {
|
|
150
|
-
command: "subreddit-post-styles reject",
|
|
151
|
-
danger: "write",
|
|
152
|
-
summary: { teamId, ...body },
|
|
153
|
-
})) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
printResult(await hubRequest(`/v1/teams/${encodeURIComponent(teamId)}/subreddit-post-styles/reject`, { method: "POST", body }));
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
//# sourceMappingURL=register-subreddit-post-style.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"register-subreddit-post-style.js","sourceRoot":"","sources":["../src/register-subreddit-post-style.ts"],"names":[],"mappings":"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"}
|