@geoly-ai/social-hub-cli 0.3.19 → 0.3.21
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 +23 -0
- package/dist/accounts-list-offset.test.d.ts +2 -0
- package/dist/accounts-list-offset.test.d.ts.map +1 -0
- package/dist/accounts-list-offset.test.js +53 -0
- package/dist/accounts-list-offset.test.js.map +1 -0
- package/dist/cmd-manifest.json +4 -2
- package/dist/compliance-risk-list-offset.test.d.ts +2 -0
- package/dist/compliance-risk-list-offset.test.d.ts.map +1 -0
- package/dist/compliance-risk-list-offset.test.js +63 -0
- package/dist/compliance-risk-list-offset.test.js.map +1 -0
- package/dist/content-review.test.js +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -4
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +16 -0
- package/dist/index.test.js.map +1 -1
- package/dist/register-calendar-directives.d.ts +64 -0
- package/dist/register-calendar-directives.d.ts.map +1 -0
- package/dist/register-calendar-directives.js +135 -0
- package/dist/register-calendar-directives.js.map +1 -0
- package/dist/register-calendar-directives.test.d.ts +2 -0
- package/dist/register-calendar-directives.test.d.ts.map +1 -0
- package/dist/register-calendar-directives.test.js +127 -0
- package/dist/register-calendar-directives.test.js.map +1 -0
- package/dist/register-shared.d.ts.map +1 -1
- package/dist/register-shared.js +13 -2
- package/dist/register-shared.js.map +1 -1
- package/dist/register-shared.test.js +78 -0
- package/dist/register-shared.test.js.map +1 -1
- package/package.json +2 -2
- package/skills/README.md +23 -22
- package/skills/manifest.json +6 -1
- package/skills/reddit-content-rewrite/SKILL.md +29 -0
- package/skills/reddit-content-writing/SKILL.md +53 -4
- package/skills/reddit-delivery-check/SKILL.md +38 -6
- package/skills/reddit-matrix.lock.json +6 -6
- package/skills/reddit-phase-brief/SKILL.md +39 -4
- package/skills/reddit-phase-brief/references/phase4-brief-workflow.md +43 -3
- package/skills/reddit-strategy-shared/references/content-diversity-contract.md +7 -2
- package/skills/reddit-strategy-shared/references/content-review-gate.md +48 -1
- package/skills/reddit-strategy-shared/references/handoff-schemas.md +72 -2
- package/skills/reddit-strategy-shared/references/native-copy-profile.md +207 -0
- package/skills/reddit-strategy-shared/references/persona-card-contract.md +210 -0
- package/skills/reddit-strategy-shared/references/version-manifest.md +60 -0
- package/skills/reddit-subreddit-compliance/SKILL.md +28 -1
- package/skills/social-hub-calendar-directives/SKILL.md +548 -0
- package/skills/social-hub-calendar-jobs/SKILL.md +10 -0
- package/skills/social-hub-cli/SKILL.md +2 -0
- package/skills/social-hub-notifications/SKILL.md +7 -0
- package/skills/social-hub-publishing/SKILL.md +5 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: social-hub-calendar-directives
|
|
3
|
+
description: >-
|
|
4
|
+
内容发布日历指令(`publishing-calendar-directive.v1`):让**你自己机器上的 agent**
|
|
5
|
+
接管 Hub 日历里 `deliveryMode=external_agent` 的条目 —— 收 `upsert` / `cancel` 指令、
|
|
6
|
+
建 UTC 一次性 cron、到点先调 `social-hub calendar preflight` 拿到 `decision:"ok"` 才发帖。
|
|
7
|
+
用户说「让我本地 agent 按 Hub 的发布日历定时发帖」「日历条目改期了 cron 要跟着换」
|
|
8
|
+
「这条内容改成外部 agent 发 / 改回 Hub 发」「preflight 说 review_not_passed / revision_stale
|
|
9
|
+
怎么办」「改了一次期结果发了两遍」「审核撤回了 cron 还在」时用本 skill。
|
|
10
|
+
**Hub 只广播期望状态,不管执行**——不下发任务、不收结果、没有租约。
|
|
11
|
+
不用于 OpenClaw 任务领取(social-hub-ops-runtime,有租约与终态),
|
|
12
|
+
也不用于 Hub 自己发的 `hub_managed` 条目(那条链路 agent 完全不参与)。
|
|
13
|
+
metadata:
|
|
14
|
+
cliVersion: ">=0.3.20"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# social-hub-calendar-directives
|
|
18
|
+
|
|
19
|
+
> **上线前提:** Hub 侧的 worker 必须在跑(指令搬运器),且该 team 已为
|
|
20
|
+
> `publishing-calendar-directive.v1` 建过 **active** 的 source config。
|
|
21
|
+
> 没有 config 时指令会**留在 outbox 里等**(不会丢),但你也收不到任何事件 ——
|
|
22
|
+
> 「配了 external_agent 却一条指令都没来」十有八九是这一条。
|
|
23
|
+
|
|
24
|
+
> **前置条件(两条都要):**
|
|
25
|
+
>
|
|
26
|
+
> 1. [`../social-hub-shared/SKILL.md`](../social-hub-shared/SKILL.md) —— `auth login` 与 `context use`。
|
|
27
|
+
> 2. [`../social-hub-notifications/SKILL.md`](../social-hub-notifications/SKILL.md) ——
|
|
28
|
+
> **游标 / receipt / dedupeKey / tombstone / 退出码全部沿用那一套**,本 skill 不重复讲。
|
|
29
|
+
> 本 skill 只讲这个 source 特有的东西:指令语义、revision 状态机、preflight。
|
|
30
|
+
|
|
31
|
+
设计与冻结决定见 `docs/publishing-calendar-directive-plan.md` §8(D1–D6)。
|
|
32
|
+
|
|
33
|
+
## 这是什么
|
|
34
|
+
|
|
35
|
+
Hub 的内容发布日历里,一条条目有两种发布主体(`deliveryMode`,冻结决定 D1):
|
|
36
|
+
|
|
37
|
+
| deliveryMode | 谁来发 | 会不会产生指令事件 |
|
|
38
|
+
| --------------------- | -------------------------------------- | ------------------ |
|
|
39
|
+
| `hub_managed`(默认) | Hub 自己建内部 `publish_post` job 去发 | **不会** |
|
|
40
|
+
| `external_agent` | **你的 agent**,靠本 skill 这套指令流 | 会 |
|
|
41
|
+
|
|
42
|
+
**两者互斥,这就是防重复发帖的唯一闸门。** 一条条目不会既有内部 job 又有指令事件;
|
|
43
|
+
所以你**绝不能**给 `hub_managed` 的条目建 cron —— 那才是真正的双发来源。
|
|
44
|
+
|
|
45
|
+
Hub 广播的是**期望状态**,不是任务:
|
|
46
|
+
|
|
47
|
+
- `upsert`:「这条条目现在应该有一个在 `plannedAt` 触发的 cron」
|
|
48
|
+
- `cancel`:「这条条目现在不该有任何 cron」
|
|
49
|
+
|
|
50
|
+
Hub 不知道也不关心你有没有建成、有没有发成功。它保证的是:**在 source config 处于
|
|
51
|
+
`enabled` 且条目满足发射条件时**,条目每变一次你就会收到一条更高 `revision` 的指令。
|
|
52
|
+
|
|
53
|
+
⚠️ 这句话有个刻意的例外,别读漏:`enabled=false`(kill switch)会**阻止新的 `upsert`**,
|
|
54
|
+
但**已经发过 `upsert` 的条目,其 `cancel` 仍会投递**(否则关掉开关反而会给你留下一堆
|
|
55
|
+
撤不掉的 cron)。所以 kill switch 期间你**仍然要继续拉取**。
|
|
56
|
+
|
|
57
|
+
## 五个必须先内化的点
|
|
58
|
+
|
|
59
|
+
1. **事件 payload 里没有正文,永远不会有。** 只有身份 + 时间 + 版本号。正文的唯一出口是
|
|
60
|
+
preflight(D5)。拿 `subject.contentDraftId` 直接读草稿就发是**明令禁止**的:审核在排期
|
|
61
|
+
之后被撤回时你一无所知,那条 cron 照发不误。
|
|
62
|
+
2. **`revision` 是唯一排序依据**,单调非负、只增不减。高版本覆盖低版本。
|
|
63
|
+
3. **事件身份 ≠ cron 身份**(见下一节,这是本 skill 最容易出人命的一条)。
|
|
64
|
+
4. **`plannedAt` 是 Z 结尾的 UTC 绝对时间**,Hub 刻意不下发 cron 表达式。
|
|
65
|
+
5. **ack 的含义在本 source 里被局部改写**(见「ack 时机」):副作用是「把指令应用到本地
|
|
66
|
+
cron/state」,**不是**未来那次发帖。
|
|
67
|
+
|
|
68
|
+
## 🔴 事件身份 ≠ cron 身份
|
|
69
|
+
|
|
70
|
+
| 身份 | 形状 | 生命周期 |
|
|
71
|
+
| ------------------ | --------------------------------------------- | --------------------------- |
|
|
72
|
+
| 事件 `dedupeKey` | `calendar-directive:<entryId>:rev:<N>:<kind>` | **一次性**,只用于事件去重 |
|
|
73
|
+
| agent 侧 cron 身份 | `calendar-directive:<teamId>:<entryId>` | **长生命周期**,跨 revision |
|
|
74
|
+
|
|
75
|
+
cron 身份**不含 revision**,因为同一条目的新版本必须**替换**上一个 cron,而不是新增一个。
|
|
76
|
+
|
|
77
|
+
**拿 dedupeKey 当 cron 名字 = 条目每改一次期就多一个 cron,到点发两遍。**
|
|
78
|
+
这是本能力最典型的事故形态,改期是日常运营动作,所以它一定会发生。
|
|
79
|
+
|
|
80
|
+
## 一次性接入(顺序不能反)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 1) 确认这个 team 拿得到 active 的 source config。两条合法路径,满足其一即可:
|
|
84
|
+
# a) 该 team 已有 enabled 的 config;
|
|
85
|
+
# b) 存在 active 的系统级模板且**开启了自动派生** —— 建订阅时会自动派生出 config。
|
|
86
|
+
# 两条都不满足才会 409。
|
|
87
|
+
social-hub notify source-configs sources -t <team>
|
|
88
|
+
# directive source 的 nodes 必须是 [](它是 state_change 型,没有节点档位)
|
|
89
|
+
# ⚠️ config 处于 enabled=false 时不要把条目切到 external_agent:
|
|
90
|
+
# 新的 upsert 不会发出,条目就成了"谁都不发"。
|
|
91
|
+
|
|
92
|
+
# 2) 建订阅
|
|
93
|
+
social-hub notify subscriptions create -t <team> \
|
|
94
|
+
--name my-mac-agent \
|
|
95
|
+
--source publishing-calendar-directive.v1 \
|
|
96
|
+
--start earliest
|
|
97
|
+
|
|
98
|
+
# 3) 最后才把日历条目切到外部 agent
|
|
99
|
+
social-hub calendar set-delivery-mode -t <team> -e <entryId> --mode external_agent --apply
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**顺序反了会静默漏事件。** 两个理由:
|
|
103
|
+
|
|
104
|
+
- `--start earliest` 而不是默认的 `now`:如果这个 team 已经有 `external_agent` 的条目,
|
|
105
|
+
它们的 `upsert` 早就发出去了。用 `now` 起订阅 = 那些条目永远等不到指令,也永远不会
|
|
106
|
+
有人告诉你。
|
|
107
|
+
- 先切 deliveryMode 再建订阅,同样漏掉切换那一刻发出的 `upsert`。
|
|
108
|
+
|
|
109
|
+
**不要给这个 source 加 `brandIds` / `socialAccountIds` / `subreddits` 过滤。**
|
|
110
|
+
`cancel` payload 刻意只带身份和 cancel reason,**没有账号/板块字段**(撤销一个 cron 不需要
|
|
111
|
+
知道原本要发什么)。按这些维度过滤的订阅会收得到 `upsert` 却收不到对应的 `cancel` ——
|
|
112
|
+
结果是撤不掉的 cron。
|
|
113
|
+
|
|
114
|
+
## 事件形状
|
|
115
|
+
|
|
116
|
+
`upsert`:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"directive": {
|
|
121
|
+
"kind": "upsert",
|
|
122
|
+
"id": "<指令 outbox 行 UUID,审计用>",
|
|
123
|
+
"revision": 7,
|
|
124
|
+
"emittedAt": "2026-08-20T03:00:00.000Z"
|
|
125
|
+
},
|
|
126
|
+
"subject": {
|
|
127
|
+
"calendarEntryId": "…",
|
|
128
|
+
"publishingPlanId": "…",
|
|
129
|
+
"contentDraftId": "…",
|
|
130
|
+
"contentDraftVersion": 3,
|
|
131
|
+
"socialAccountId": "…",
|
|
132
|
+
"subreddit": "coffee"
|
|
133
|
+
},
|
|
134
|
+
"schedule": { "plannedAt": "2026-08-21T14:30:00.000Z" }
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`cancel`(**只有身份**,没有账号/板块/时间/正文):
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"directive": {
|
|
143
|
+
"kind": "cancel",
|
|
144
|
+
"id": "…",
|
|
145
|
+
"revision": 8,
|
|
146
|
+
"cancelReason": "review_revoked",
|
|
147
|
+
"emittedAt": "2026-08-20T05:00:00.000Z"
|
|
148
|
+
},
|
|
149
|
+
"subject": { "calendarEntryId": "…", "publishingPlanId": "…" }
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`cancelReason` 是枚举而不是自由文本,因为你要按它分流告警:
|
|
154
|
+
|
|
155
|
+
| cancelReason | 含义 | 你的动作 |
|
|
156
|
+
| ----------------------- | ----------------------------------------- | ----------------------------------- |
|
|
157
|
+
| `entry_cancelled` | 运营主动取消这条条目 | 删 cron,正常运营动作,不必告警 |
|
|
158
|
+
| `entry_deleted` | 条目行被删(含 plan / account 级联) | 删 cron |
|
|
159
|
+
| `review_revoked` | 审核从 passed 回退 | 删 cron,**值得告警**(内容有问题) |
|
|
160
|
+
| `delivery_mode_changed` | 条目改回 `hub_managed`,Hub 自己发 | 删 cron,**不要**再管这条条目 |
|
|
161
|
+
| `draft_detached` | 关联草稿被解绑(`content_draft_id` 置空) | 删 cron,**值得告警**(内容没了) |
|
|
162
|
+
|
|
163
|
+
### 缺字段怎么判
|
|
164
|
+
|
|
165
|
+
`upsert` 上的 `schedule.plannedAt` / `subject.socialAccountId` / `subject.subreddit` /
|
|
166
|
+
`subject.contentDraftId` / `subject.contentDraftVersion` 是 **variant-required** ——
|
|
167
|
+
registry 不允许后台把它们从 `upsert` 上取消勾选。所以收到一条**缺了它们**的 `upsert`
|
|
168
|
+
**不是**「配置没勾」这条正常分支,而是 **malformed / 不支持的 envelope**:
|
|
169
|
+
|
|
170
|
+
- **隔离 + 告警 + 不 ack**(同通用 skill 里「未知 payloadSchemaVersion」的处理)。
|
|
171
|
+
- **绝不要猜默认值**:拿 `emittedAt` 当 `plannedAt`、拿最近一次的账号顶上,都会真的发出去。
|
|
172
|
+
|
|
173
|
+
只有 `directive.emittedAt` 这类**真·可选**字段才允许缺失,缺了照常处理。
|
|
174
|
+
沿用通用规则:**CLI 只原样输出,绝不补齐。**
|
|
175
|
+
|
|
176
|
+
## revision 状态机(本 skill 的核心)
|
|
177
|
+
|
|
178
|
+
本地必须有一张**持久**表,键是 `(teamId, calendarEntryId)`:
|
|
179
|
+
|
|
180
|
+
| 字段 | 说明 |
|
|
181
|
+
| ----------------- | --------------------------------------- |
|
|
182
|
+
| `appliedRevision` | 已经**成功应用**的最高 revision |
|
|
183
|
+
| `kind` | 该 revision 是 `upsert` 还是 `cancel` |
|
|
184
|
+
| `cronId` | 当前 cron 的本地标识(`cancel` 态为空) |
|
|
185
|
+
| `phase` | `pending` / `applied` —— 崩溃恢复用 |
|
|
186
|
+
|
|
187
|
+
🔴 **「没有本地行」必须是一个独立状态,不能用 `0` 表示。** revision `0` 是每条条目的
|
|
188
|
+
**合法初始值**:若读不到记录时让 helper 返回 `0`,那条条目的第一条 `upsert`(rev 0)
|
|
189
|
+
就会被下面的规则一当成重复投递丢掉 —— 表现为「全新条目永远收不到 cron」。
|
|
190
|
+
用 `NULL` / `-1` / `Option::None` 都行,只要它**严格小于 0**。
|
|
191
|
+
|
|
192
|
+
### 规则一:低版本一律丢弃,`cancel` 也要留痕
|
|
193
|
+
|
|
194
|
+
收到一条指令,先比 `revision`(无本地行 = 无已应用 revision,任何 revision 都更大):
|
|
195
|
+
|
|
196
|
+
- `revision <= appliedRevision` → **丢弃**(重复投递或乱序),照常 ack。
|
|
197
|
+
- `revision > appliedRevision`,或本地根本没有这条记录 → 应用它。
|
|
198
|
+
|
|
199
|
+
🔴 **`cancel` 必须写成 revision tombstone,而不是把整条本地记录删掉。**
|
|
200
|
+
删掉记录 = `appliedRevision` 归零,此后一条乱序到达的旧 `upsert`(rev 7 < 已撤销的 rev 8)
|
|
201
|
+
会被当成新指令,**把已撤销的 cron 建回来**。Hub 侧刻意保证同一 revision 上不可能同时存在
|
|
202
|
+
`upsert` 与 `cancel`(唯一键 `(entry, revision)`),本地必须配合这个保证。
|
|
203
|
+
|
|
204
|
+
### 规则二:应用顺序不能颠倒(崩溃可恢复)
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
比较 revision → 创建/替换 或 删除 cron → durable 标记该 revision 已应用 → ack
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**绝不能先标记 revision 已应用再去建 cron。** 中间崩溃时,重投的同一条事件会因
|
|
211
|
+
`revision <= appliedRevision` 被丢弃,于是 cron 永远不存在 —— 一条到点该发的内容静默不发。
|
|
212
|
+
用本地事务包住「改 cron + 写 state」,或者写 `pending` → 改 cron → 改 `applied`,
|
|
213
|
+
并在进程启动时把所有 `pending` 重放一遍。
|
|
214
|
+
|
|
215
|
+
### 规则三:`currentRevision` 只能用来诊断
|
|
216
|
+
|
|
217
|
+
preflight 拒绝时会回一个 `currentRevision`(`entry_not_found` 除外,恒为 `null`)。
|
|
218
|
+
|
|
219
|
+
🔴 **绝不能拿它推进本地的 `appliedRevision`。** 举例:手上是 rev 5 的 cron,preflight 回
|
|
220
|
+
`revision_stale / currentRevision: 6`。若你把本地升到 6,随后真正到达的 rev 6 `upsert`
|
|
221
|
+
就会被规则一丢弃 —— 结果是**一个 cron 都没有**。只等真实到达的指令。
|
|
222
|
+
|
|
223
|
+
### 规则四:cron 身份是共享可变状态,删之前先确认它还属于你
|
|
224
|
+
|
|
225
|
+
cron 身份 `calendar-directive:<teamId>:<entryId>` 是**跨 revision 稳定的**,
|
|
226
|
+
所以消费循环和已触发的 cron **会争同一个名字**。一个真实会发生的交错:
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
rev 7 的 cron 触发 → 调 preflight(在途)
|
|
230
|
+
↓ 与此同时消费循环收到 rev 8 的 upsert
|
|
231
|
+
↓ 按规则替换掉 cron(现在这个名字属于 rev 8)
|
|
232
|
+
preflight 返回 revision_stale(因为问的是 rev 7)
|
|
233
|
+
↓ rev 7 若无条件 cron_delete → 把 rev 8 的 cron 删了
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
结果:Hub 侧一切正常,agent 侧一个 cron 都没有,改期后的那次发布**静默不发**。
|
|
237
|
+
|
|
238
|
+
所以**每一次对 cron 的写(建/删)都必须**:
|
|
239
|
+
|
|
240
|
+
1. 拿 `(teamId, entryId)` 的本地锁;
|
|
241
|
+
2. 在锁内**重读**本地 state,确认它仍是你这次操作对应的 revision;
|
|
242
|
+
3. 不是了就**什么都不做**(那个 cron 属于更高 revision,让它去)。
|
|
243
|
+
|
|
244
|
+
这条对消费侧和触发侧都适用。触发侧尤其容易漏——它感觉上「只是清理自己的 cron」。
|
|
245
|
+
|
|
246
|
+
### 应用 `upsert`
|
|
247
|
+
|
|
248
|
+
**原子替换**(在规则四的锁内):删掉该 `(teamId, entryId)` 现有的 cron(如果有),
|
|
249
|
+
再按新的 `plannedAt` 建一个。不是「先建新的再删旧的」(中间窗口会双发),
|
|
250
|
+
也不是「已经有 cron 就跳过」(改期就不生效了)。
|
|
251
|
+
|
|
252
|
+
删除这一步同样要 **fail-stop**:**「本来就没有」算成功,其它错误一律失败**
|
|
253
|
+
(权限、调度器不可达)。删失败却继续建新 cron,就是两个 cron 同时在跑;
|
|
254
|
+
删失败还标记 applied 并 ack,则旧 cron 永远撤不掉。
|
|
255
|
+
|
|
256
|
+
### 建的是 UTC 一次性 cron
|
|
257
|
+
|
|
258
|
+
`plannedAt` 是 **Z 结尾的 UTC**(契约层收窄过,不接受 `+08:00` 这类等价写法)。
|
|
259
|
+
|
|
260
|
+
- **直接用 UTC 排程**。可移植的做法是把 `plannedAt` 转成 **epoch 秒**再交给调度器
|
|
261
|
+
(`date -u -d "$plannedAt" +%s` / GNU `date`;BSD/macOS 用 `date -j -u -f`),
|
|
262
|
+
或用明确接受 UTC 的调度器语法(systemd:`systemd-run --on-calendar="2026-08-21 14:30:00 UTC"`)。
|
|
263
|
+
⚠️ **不要指望 `at` 的时区行为可移植** —— `at -u` 在 GNU at 与 macOS 上语义并不一致,
|
|
264
|
+
很多实现干脆只认本地时区。拿不准就自己算 epoch 秒。
|
|
265
|
+
- **不要先转成本地时间再排**。DST 切换那两天,本地时间会出现不存在的一小时和重复的一小时,
|
|
266
|
+
那正是"发早了一小时/发了两遍"的来源。Hub 之所以只给绝对时间、不给 cron 表达式,
|
|
267
|
+
就是不想让每个 agent 各自去踩这个坑。
|
|
268
|
+
- 触发一次就结束。**不要建重复触发的 cron** —— 到点条目已经不是 `scheduled` 了,
|
|
269
|
+
后续每次触发都只会拿到一个 `entry_not_scheduled`。
|
|
270
|
+
- `plannedAt` 已经过去(临时插单、agent 离线补收):**Hub 不会为你伪造补发**,
|
|
271
|
+
由你本地决策——立刻试一次、还是告警交人工。不管选哪条,**都仍然要先过 preflight**。
|
|
272
|
+
|
|
273
|
+
## 到点:先 preflight,拿到 `ok` 才发
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
social-hub calendar preflight -t <team> -e <calendarEntryId> --revision <你手上那条的 revision>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`--revision` 是**版本钉子,必填、不能猜**。revision `0` 是每条条目的合法初始值,所以任何
|
|
280
|
+
"缺省当 0"的实现都会真的放行一条条目的内容。CLI 会拒绝空串/空白/负数/小数/超安全整数。
|
|
281
|
+
|
|
282
|
+
放行时 stdout:
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"decision": "ok",
|
|
287
|
+
"calendarEntryId": "…",
|
|
288
|
+
"publishingPlanId": "…",
|
|
289
|
+
"revision": 7,
|
|
290
|
+
"socialAccountId": "…",
|
|
291
|
+
"subreddit": "coffee",
|
|
292
|
+
"plannedAt": "2026-08-21T14:30:00.000Z",
|
|
293
|
+
"content": {
|
|
294
|
+
"contentDraftId": "…",
|
|
295
|
+
"contentDraftVersion": 3,
|
|
296
|
+
"title": "…",
|
|
297
|
+
"body": "…"
|
|
298
|
+
},
|
|
299
|
+
"review": { "targetId": "…", "status": "passed", "contentHash": "…" }
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**服务端在同一个只读快照里确认四件事**:条目仍 `scheduled`、仍 `external_agent`、
|
|
304
|
+
revision 仍是你那条、审核仍 `passed`。四条全成立才有正文——判定不过就没有内容可拿。
|
|
305
|
+
把 `review.contentHash` 留痕,事后对账"我发的是不是那一版"。
|
|
306
|
+
|
|
307
|
+
### 🔴 退出码:业务拒绝**不是**错误
|
|
308
|
+
|
|
309
|
+
`rejected` 走 **HTTP 200**,命令**默认 exit 0**。这是刻意的:条目被正常取消是日常路径,
|
|
310
|
+
让它把 `set -e` 的脚本打崩没有任何好处。**你必须显式判 `decision`**,别用退出码代替。
|
|
311
|
+
|
|
312
|
+
想让它在非 ok 时非零退出(例如塞进 `&&` 链),加 `--require-ok`:JSON 照常完整输出,
|
|
313
|
+
之后才 exit 1。
|
|
314
|
+
|
|
315
|
+
### 七个 rejected reason 的处置(判定顺序已在服务端冻结)
|
|
316
|
+
|
|
317
|
+
| reason | 含义 | 本地动作 |
|
|
318
|
+
| ---------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
319
|
+
| `entry_not_found` | 不存在 / 不属于本 team / 你的品牌作用域看不到 | 删 cron。`currentRevision` 恒 `null`(这个 reason 同时是越权掩码) |
|
|
320
|
+
| `entry_not_scheduled` | 已取消 / 发布中 / 已发 / 失败 | 删 cron。**不要重试** |
|
|
321
|
+
| `delivery_mode_not_external` | 已改回 `hub_managed`,Hub 自己发 | 删 cron。**再发就是双发** |
|
|
322
|
+
| `revision_stale` | 你手上的版本旧了,有更新的指令在路上 | 删旧 cron,**不发**。等真实的新指令,**不要**拿 `currentRevision` 升本地状态 |
|
|
323
|
+
| `revision_unknown` | 你手上的版本比 Hub 还新 —— 正常不可能 | 删 cron、**隔离 + 告警**。别猜、别重试 |
|
|
324
|
+
| `draft_detached` | 条目没绑草稿或草稿行没了 | 删 cron。等后续更高 revision 的 `upsert` |
|
|
325
|
+
| `review_not_passed` | canonical review target 不存在、身份漂移,或状态非 passed | 删 cron,**不要轮询等它变绿**。审核重新通过时 Hub 会发新的 `upsert` |
|
|
326
|
+
|
|
327
|
+
**`review_not_passed` 与 `revision_stale` 的区别**(最常被搞混的一对):
|
|
328
|
+
|
|
329
|
+
- `revision_stale` = **条目本身还好好的**,只是你手上那条指令过期了。Hub 已经发过一条更高
|
|
330
|
+
revision 的指令,它要么在你的队列里没消费到,要么被你的订阅过滤器挡住了。
|
|
331
|
+
→ 排查方向是**消费链路**(游标卡住?订阅被过滤了?)。
|
|
332
|
+
- `review_not_passed` = **指令版本没问题,内容不能发**。审核被撤回或草稿改版导致 target 漂移。
|
|
333
|
+
→ 排查方向是**内容侧**(`social-hub-content-review` / `social-hub-post-review`)。
|
|
334
|
+
这条**不会自愈**:审核重新通过时,条目的 `directive_revision` 会 +1 并发一条新的 `upsert`,
|
|
335
|
+
你到那时才该重新建 cron。在这之前一直轮询 preflight 只是白烧配额。
|
|
336
|
+
|
|
337
|
+
两者共同点:**都不发帖,都不改本地 `appliedRevision`。**
|
|
338
|
+
|
|
339
|
+
⚠️ 上表里所有的「删 cron」都受**规则四**约束:删之前必须在 `(teamId, entryId)` 锁内重读本地
|
|
340
|
+
state,确认那个 cron 仍属于你这次触发的 revision。preflight 是一次网络往返,在途期间消费循环
|
|
341
|
+
完全可能已经用更高 revision 把同名 cron 换掉了 —— 无条件删就是把新的那个删掉,
|
|
342
|
+
于是改期后的发布静默消失。
|
|
343
|
+
|
|
344
|
+
## ack 时机(本 source 局部改写通用规则)
|
|
345
|
+
|
|
346
|
+
通用 notifications skill 说"副作用完成后才 ack"——这条仍然成立,但**这个 source 的副作用是
|
|
347
|
+
「把指令原子地应用到本地 cron + state」,不是未来那次发帖**。
|
|
348
|
+
|
|
349
|
+
所以:**cron 与 state 落盘之后立刻 ack**。
|
|
350
|
+
|
|
351
|
+
- 早 ack(cron 还没建成就 ack)→ 游标推进了,事件再也拉不回来,那条内容永远不发。
|
|
352
|
+
- 晚 ack(等到真的发完帖才 ack)→ 更糟:游标是**连续 watermark**,卡在这一条上意味着
|
|
353
|
+
后面所有的 `cancel` 和新 revision 全被堵在后面。条目改期或审核撤回的指令根本到不了你手上,
|
|
354
|
+
而你手上那个过期的 cron 会照常触发(好在 preflight 会拦住它,但你已经等于没在消费了)。
|
|
355
|
+
|
|
356
|
+
`payloadStatus: "expired"`(tombstone,过保留期):**照常 ack,但绝不能据此建/删 cron**。
|
|
357
|
+
payload 已经被清空了,任何"猜"都是编数据。标记 skipped 并告警,走人工恢复。
|
|
358
|
+
|
|
359
|
+
## kill switch 的语义(D4)
|
|
360
|
+
|
|
361
|
+
后台把这个 source 的 config 停用(`enabled=false`)时:
|
|
362
|
+
|
|
363
|
+
- **只挡新的 `upsert`**;
|
|
364
|
+
- **已经发过 `upsert` 的条目,`cancel` 仍然必须投递**。
|
|
365
|
+
|
|
366
|
+
所以停用之后**你也要继续 pull**。停掉消费循环会让一批 cron 永久留在你机器上,
|
|
367
|
+
到点全部触发(preflight 会拒,但那已经是靠兜底而不是靠设计了)。
|
|
368
|
+
|
|
369
|
+
## 消费循环怎么写
|
|
370
|
+
|
|
371
|
+
直接复用 [`../social-hub-notifications/SKILL.md`](../social-hub-notifications/SKILL.md)
|
|
372
|
+
的「agent 消费循环」骨架(`store_claim` / `store_state` / `store_mark` 三个 fail-stop 原语、
|
|
373
|
+
幂等键 `${HUB}|${TEAM}|${dedupeKey}`、`--receipt-file`、退出码分流全部照抄),
|
|
374
|
+
只把 `do_side_effects` 换成「应用指令」——**并且必须在 `(team, entry)` 锁内**:
|
|
375
|
+
|
|
376
|
+
你还要自己提供四个原语,**全部 fail-stop**:
|
|
377
|
+
|
|
378
|
+
| 原语 | 契约 |
|
|
379
|
+
| ------------------------------------------ | ------------------------------------------------------------------------ |
|
|
380
|
+
| `with_entry_lock <team> <entry> -- <cmd>` | `(team, entry)` 互斥锁;拿不到就非 0 退出,**不许降级成"不加锁直接干"** |
|
|
381
|
+
| `local_applied_revision <team> <entry>` | 打印已应用 revision;**无记录时打印 `-1`**(不是 0,见上);读不到就非 0 |
|
|
382
|
+
| `cron_delete <id>` | **「本来就没有」= 成功**;其它错误非 0 |
|
|
383
|
+
| `cron_create_utc_oneshot <id> <utc> <cmd>` | 按 UTC 绝对时间建一次性任务;建不成非 0 |
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
# 🔴 通用循环调的是 do_side_effects;锁必须在这里拿,不是"假设外面已经拿了"。
|
|
387
|
+
# 消费侧不加锁的话,规则四那个交错照样成立:触发侧重读完 rev7、还没删的一瞬间,
|
|
388
|
+
# 消费侧可以并发把 cron 换成 rev8,然后被触发侧删掉。
|
|
389
|
+
do_side_effects() { # $1 = envelope JSON
|
|
390
|
+
local entry kind rev
|
|
391
|
+
|
|
392
|
+
# 🔴 先验三个**公共必填**字段,再谈别的。通用循环只验 payloadSchemaVersion,
|
|
393
|
+
# 不验 payload 形状 —— 缺 revision 时 jq 会给出 "null",而脚本用的是
|
|
394
|
+
# `set -uo pipefail`(没有 -e),算术比较报错后**流程会继续往下走**,最后
|
|
395
|
+
# 建出一个 cron 还把事件 ack 掉。所以这里必须显式 fail-stop。
|
|
396
|
+
entry=$(jq -r '.payload.subject.calendarEntryId // empty' <<<"$1")
|
|
397
|
+
kind=$(jq -r '.payload.directive.kind // empty' <<<"$1")
|
|
398
|
+
rev=$(jq -r '.payload.directive.revision // empty' <<<"$1")
|
|
399
|
+
[ -z "$entry" ] || [ -z "$kind" ] || [ -z "$rev" ] && return 2
|
|
400
|
+
# revision 必须是非负整数;`8.5` / `-1` / `1e3` 都是 malformed,不是"约等于"
|
|
401
|
+
case "$rev" in ''|*[!0-9]*) return 2 ;; esac
|
|
402
|
+
|
|
403
|
+
with_entry_lock "$TEAM" "$entry" -- apply_directive "$1"
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
apply_directive() { # $1 = envelope JSON;由 do_side_effects 保证已在锁内
|
|
407
|
+
local kind rev entry cron_id
|
|
408
|
+
kind=$(jq -r .payload.directive.kind <<<"$1")
|
|
409
|
+
rev=$(jq -r .payload.directive.revision <<<"$1")
|
|
410
|
+
entry=$(jq -r .payload.subject.calendarEntryId <<<"$1")
|
|
411
|
+
cron_id="calendar-directive:$TEAM:$entry" # 🔴 不含 revision
|
|
412
|
+
|
|
413
|
+
# 规则一:低版本丢弃(cancel 的 tombstone 也参与比较;无记录 = -1)
|
|
414
|
+
local applied
|
|
415
|
+
applied=$(local_applied_revision "$TEAM" "$entry") || return 2
|
|
416
|
+
[ "$rev" -le "$applied" ] && return 0 # 重复/乱序:不动 cron,照常 ack
|
|
417
|
+
|
|
418
|
+
case "$kind" in
|
|
419
|
+
cancel)
|
|
420
|
+
local_mark_pending "$TEAM" "$entry" "$rev" cancel || return 2
|
|
421
|
+
cron_delete "$cron_id" || return 2
|
|
422
|
+
;;
|
|
423
|
+
upsert)
|
|
424
|
+
# 🔴 五个 variant-required 字段**全部**要验,不是只验 plannedAt。
|
|
425
|
+
# 少验一个,就会拿着一条缺账号/缺板块的 envelope 建出一个到点必然发不出去
|
|
426
|
+
# 的 cron,而且已经 ack 掉了 —— 事件再也拉不回来。
|
|
427
|
+
local planned draft draft_ver account sub_name
|
|
428
|
+
planned=$(jq -r '.payload.schedule.plannedAt // empty' <<<"$1")
|
|
429
|
+
draft=$(jq -r '.payload.subject.contentDraftId // empty' <<<"$1")
|
|
430
|
+
draft_ver=$(jq -r '.payload.subject.contentDraftVersion // empty' <<<"$1")
|
|
431
|
+
account=$(jq -r '.payload.subject.socialAccountId // empty' <<<"$1")
|
|
432
|
+
sub_name=$(jq -r '.payload.subject.subreddit // empty' <<<"$1")
|
|
433
|
+
# 缺任何一个 = malformed / 不支持的 envelope:隔离告警,别猜默认值、别 ack
|
|
434
|
+
[ -z "$planned" ] || [ -z "$draft" ] || [ -z "$draft_ver" ] \
|
|
435
|
+
|| [ -z "$account" ] || [ -z "$sub_name" ] && return 2
|
|
436
|
+
local_mark_pending "$TEAM" "$entry" "$rev" upsert || return 2
|
|
437
|
+
# 原子替换:先删后建,同一个 cron 身份。删失败就停,别留两个 cron。
|
|
438
|
+
cron_delete "$cron_id" || return 2
|
|
439
|
+
cron_create_utc_oneshot "$cron_id" "$planned" \
|
|
440
|
+
"fire_entry $TEAM $entry $rev" || return 2
|
|
441
|
+
;;
|
|
442
|
+
*) return 2 ;; # 未知 kind:隔离,别 ack
|
|
443
|
+
esac
|
|
444
|
+
local_mark_applied "$TEAM" "$entry" "$rev" # 🔴 cron 落地之后才标记
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
cron 到点执行的 `fire_entry`(**同样在锁内**,见规则四):
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
fire_entry() { # $1=team $2=entryId $3=revision
|
|
452
|
+
with_entry_lock "$1" "$2" -- fire_entry_locked "$@"
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
fire_entry_locked() {
|
|
456
|
+
local team=$1 entry=$2 rev=$3 out decision applied
|
|
457
|
+
# 🔴 锁内先重读:这条 cron 可能在 preflight 之前就已经被更高 revision 换掉了
|
|
458
|
+
applied=$(local_applied_revision "$team" "$entry") || return 1
|
|
459
|
+
[ "$rev" -ne "$applied" ] && return 0 # 不是我的了,什么都别碰
|
|
460
|
+
|
|
461
|
+
out=$(social-hub calendar preflight -t "$team" -e "$entry" --revision "$rev") || return 1
|
|
462
|
+
decision=$(jq -r .decision <<<"$out")
|
|
463
|
+
|
|
464
|
+
if [ "$decision" != "ok" ]; then
|
|
465
|
+
log_reason "$(jq -r .reason <<<"$out")" # 分流告警,见上表
|
|
466
|
+
# 再验一次:preflight 是网络调用,在途期间本地 state 可能又被换掉了。
|
|
467
|
+
# 无条件删 = 有概率删掉更高 revision 刚建好的 cron(规则四那个交错)。
|
|
468
|
+
applied=$(local_applied_revision "$team" "$entry") || return 1
|
|
469
|
+
if [ "$rev" -eq "$applied" ]; then
|
|
470
|
+
# 删失败不能吞:`cron_delete` 的契约是「本来就没有」才算成功,其余都是
|
|
471
|
+
# 真故障。静默 return 0 会留下一个撤不掉的 cron,而且没人知道。
|
|
472
|
+
cron_delete "calendar-directive:$team:$entry" || return 1
|
|
473
|
+
fi
|
|
474
|
+
return 0 # 🔴 不改 appliedRevision
|
|
475
|
+
fi
|
|
476
|
+
|
|
477
|
+
publish_reddit_post "$out" # 只有这里才拿得到正文
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## CLI 速查
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
# 到点校验(拿到 decision=ok 才能发;rejected 也是 exit 0)
|
|
485
|
+
social-hub calendar preflight -t <team> -e <entryId> --revision <n>
|
|
486
|
+
social-hub calendar preflight -t <team> -e <entryId> --revision <n> --require-ok # 非 ok 时 exit 1
|
|
487
|
+
|
|
488
|
+
# 切换发布主体(高风险写,必须 --apply;--dry-run 先看)
|
|
489
|
+
social-hub calendar set-delivery-mode -t <team> -e <entryId> --mode external_agent --dry-run
|
|
490
|
+
social-hub calendar set-delivery-mode -t <team> -e <entryId> --mode external_agent --apply
|
|
491
|
+
social-hub calendar set-delivery-mode -t <team> -e <entryId> --mode hub_managed --apply
|
|
492
|
+
|
|
493
|
+
# 看条目当前的 deliveryMode / directiveRevision
|
|
494
|
+
social-hub calendar list -t <team> --status scheduled
|
|
495
|
+
|
|
496
|
+
# raw patch 同样被 --apply 门挡住(body 里出现 deliveryMode 时)
|
|
497
|
+
social-hub calendar patch -t <team> -e <entryId> --body '{"deliveryMode":"external_agent"}' --apply
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
建计划时就指定,比事后切干净(不经过"先建内部 job 再撤"的窗口):
|
|
501
|
+
|
|
502
|
+
```bash
|
|
503
|
+
social-hub plans create -t <team> --json '{
|
|
504
|
+
"brandId":"…","name":"…",
|
|
505
|
+
"entries":[{"socialAccountId":"…","subreddit":"coffee",
|
|
506
|
+
"plannedAt":"2026-08-21T14:30:00.000Z",
|
|
507
|
+
"deliveryMode":"external_agent"}]
|
|
508
|
+
}'
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## 权限
|
|
512
|
+
|
|
513
|
+
preflight 需要**同时**具备 `publishingPlan:read`、`contentDraft:read`、`socialAccount:read`
|
|
514
|
+
(分别对应响应里的身份/时间、正文、账号三类字段)。
|
|
515
|
+
|
|
516
|
+
⚠️ **能 pull 到事件 ≠ 到点能 preflight 成功。** 事件里的字段权限是按 `includedFields` 逐字段
|
|
517
|
+
判的,而 preflight 三道都要。接入时**先跑一次 preflight** 验权限,别等到 cron 到点才发现 403。
|
|
518
|
+
|
|
519
|
+
凭证要求:**订阅与 pull 沿用 notifications 那套**(交互式 `auth login` 的 CLI token 或后台
|
|
520
|
+
session;聚合/订阅面拒 `api_key`)。
|
|
521
|
+
|
|
522
|
+
⚠️ **preflight 本身不挑凭证类型**:它只查那三道读权限,`api_key` 只要权限够就能调(这与
|
|
523
|
+
上面那条不同,别混着记)。实际接入仍建议用 `auth login` 的 CLI token —— 你需要的是同一个
|
|
524
|
+
主体既能 pull 事件、又能 preflight,而 pull 那一侧是拒 api_key 的。
|
|
525
|
+
|
|
526
|
+
## 排障
|
|
527
|
+
|
|
528
|
+
| 现象 | 根因 | 处理 |
|
|
529
|
+
| ----------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
530
|
+
| 同一条内容发了两遍 | 拿 `dedupeKey` 当了 cron 身份,改期后多出一个 cron | 改成 `calendar-directive:<teamId>:<entryId>`,清理重复 cron |
|
|
531
|
+
| 同一条内容发了两遍(另一种) | 条目是 `hub_managed`,Hub 的内部 job 和你都发了 | 只给 `external_agent` 建 cron;确认 `deliveryMode` |
|
|
532
|
+
| 已撤销的 cron 又被建回来 | `cancel` 把本地记录删了而不是留 tombstone,旧 `upsert` 乱序到达被当新的 | 改成写 tombstone(规则一) |
|
|
533
|
+
| 改期后 cron 没换 | 「已有 cron 就跳过」,没做原子替换 | 收到更高 revision 一律先删后建 |
|
|
534
|
+
| 改期后新 cron 莫名消失(偶发) | 旧 revision 的触发拿到 `revision_stale` 后无条件删了同名 cron | 删前在锁内重读 state 比对 revision(规则四) |
|
|
535
|
+
| 全新条目从来收不到 cron | 「无本地记录」被当成 `appliedRevision = 0`,rev 0 的首条 upsert 被丢弃 | 无记录用 `-1` / `NULL` 表示(见 revision 状态机开头) |
|
|
536
|
+
| 到点什么都没发,也没报错 | 先标记 `appliedRevision` 再建 cron,中间崩了,重投被丢弃 | 改成 cron 落地后才标记 + 启动时重放 `pending`(规则二) |
|
|
537
|
+
| 条目一直等不到新指令,preflight 恒 stale | 拿 `currentRevision` 升了本地状态,真正的新 `upsert` 被当旧的丢弃 | 只信真实到达的指令(规则三);必要时人工核对本地 state |
|
|
538
|
+
| 收得到 `upsert` 收不到 `cancel` | 订阅按 brand/account/subreddit 过滤了,而 `cancel` 不带这些字段 | 去掉该 source 的过滤条件 |
|
|
539
|
+
| 新机器一条指令都收不到 | `--start now` 起的订阅,历史 `upsert` 早就发过了 | `--start earliest` 重建订阅(或 reset 游标) |
|
|
540
|
+
| 建订阅 409 `SUBSCRIPTION_SOURCE_NOT_CONFIGURED` | 该 team 没有 active 的 source config | 见 notifications skill 的「后台侧配置」 |
|
|
541
|
+
| preflight 403 | 三道读权限没配齐 | 补 `publishingPlan` / `contentDraft` / `socialAccount` read |
|
|
542
|
+
|
|
543
|
+
## 相关 skill
|
|
544
|
+
|
|
545
|
+
- [`social-hub-notifications`](../social-hub-notifications/SKILL.md) —— 订阅/游标/ack 的通用机制
|
|
546
|
+
- [`social-hub-publishing`](../social-hub-publishing/SKILL.md) —— 发布计划与日历条目的创建
|
|
547
|
+
- [`social-hub-calendar-jobs`](../social-hub-calendar-jobs/SKILL.md) —— 日历条目回填与内部调度任务
|
|
548
|
+
- [`social-hub-content-review`](../social-hub-content-review/SKILL.md) —— `review_not_passed` 的排查入口
|
|
@@ -21,6 +21,16 @@ social-hub calendar list-full -t <team-id> --account <accountId> --status schedu
|
|
|
21
21
|
social-hub calendar patch -t <team-id> -e <entryId> --body '{"status":"succeeded","permalink":"..."}'
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
> **`deliveryMode` 决定这条条目由谁发布,改它必须带 `--apply`。**
|
|
25
|
+
> `hub_managed`(默认)= Hub 内部 job 发;`external_agent` = 广播指令给你自己机器上的
|
|
26
|
+
> agent 去建 cron 发。两者互斥,这是防重复发帖的唯一闸门 —— 切错就是双发或谁都不发。
|
|
27
|
+
> 切换入口与整套外部 agent 协议(`calendar preflight`、revision 状态机、UTC 一次性 cron)
|
|
28
|
+
> 见 [`../social-hub-calendar-directives/SKILL.md`](../social-hub-calendar-directives/SKILL.md)。
|
|
29
|
+
>
|
|
30
|
+
> ```bash
|
|
31
|
+
> social-hub calendar set-delivery-mode -t <team-id> -e <entryId> --mode external_agent --apply
|
|
32
|
+
> ```
|
|
33
|
+
|
|
24
34
|
## 调度任务
|
|
25
35
|
|
|
26
36
|
```bash
|
|
@@ -52,6 +52,8 @@ social-hub version --json
|
|
|
52
52
|
| 运营板块池 catalog / aliases / tier rules | `social-hub-subreddit-pools` |
|
|
53
53
|
| 通用网页抓取 / Firecrawl / scrape | `social-hub-scrape` |
|
|
54
54
|
| 导入导出 / runbook / openclaw-ingest | `social-hub-migration` |
|
|
55
|
+
| 事件订阅通知(notify pull/ack/订阅) | `social-hub-notifications` |
|
|
56
|
+
| 日历指令:本地 agent 建 cron 定时发帖 / preflight | `social-hub-calendar-directives` |
|
|
55
57
|
|
|
56
58
|
命中上表场景时,**打开对应领域 skill**;不要在总路由里重复展开领域命令细节。
|
|
57
59
|
|
|
@@ -30,6 +30,13 @@ Hub 在某个节点**发出一条事件**,你的 agent 通过 HTTP long-poll
|
|
|
30
30
|
|
|
31
31
|
**别把 ack 当执行回执。** ack 唯一的作用是推进游标。
|
|
32
32
|
|
|
33
|
+
> **`publishing-calendar-directive.v1` 有一套额外的消费协议**,本 skill 的通用机制
|
|
34
|
+
> (游标 / receipt / dedupeKey / tombstone / 退出码)全部适用,但它多出:revision 状态机、
|
|
35
|
+
> 「事件身份 ≠ cron 身份」、到点必须过 preflight,以及 **ack 时机的局部改写**
|
|
36
|
+
> (副作用是「把指令应用到本地 cron/state」,cron 落盘即 ack;等到真的发帖才 ack 会把
|
|
37
|
+
> 后续 cancel 堵在连续 watermark 后面)。订阅这个 source 前先读
|
|
38
|
+
> [`../social-hub-calendar-directives/SKILL.md`](../social-hub-calendar-directives/SKILL.md)。
|
|
39
|
+
|
|
33
40
|
## 五个必须先内化的概念
|
|
34
41
|
|
|
35
42
|
1. **游标是连续 watermark**:`cursorSeq = N` 表示该订阅**所有匹配事件**中 `seq ≤ N` 的
|
|
@@ -74,6 +74,11 @@ social-hub plans cancel -t <team-id> --plan <uuid> # 破坏性:先 list
|
|
|
74
74
|
|
|
75
75
|
调度列表与 patch 见 **`social-hub-calendar-jobs`**(本 skill 不重复展开 jobs 全量命令)。
|
|
76
76
|
|
|
77
|
+
条目上的 `deliveryMode` 决定谁来发:默认 `hub_managed`(Hub 建内部 job 自己发),
|
|
78
|
+
`external_agent` 则改由**用户自己机器上的 agent** 收指令建 cron 发。建计划时就能指定
|
|
79
|
+
(`entries[].deliveryMode`),比事后切干净。外部 agent 那条链路见
|
|
80
|
+
**`social-hub-calendar-directives`**。
|
|
81
|
+
|
|
77
82
|
## Agent 执行与回填
|
|
78
83
|
|
|
79
84
|
优先 **`social-hub-ops-runtime`**:`ops claim-next` → `ops complete --permalink ...`。
|