@geoly-ai/social-hub-cli 0.3.23 → 0.3.25
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 +13 -0
- package/dist/cmd-manifest.json +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/register-subreddit-assets.d.ts +5 -0
- package/dist/register-subreddit-assets.d.ts.map +1 -0
- package/dist/register-subreddit-assets.js +347 -0
- package/dist/register-subreddit-assets.js.map +1 -0
- package/dist/register-subreddit-assets.test.d.ts +2 -0
- package/dist/register-subreddit-assets.test.d.ts.map +1 -0
- package/dist/register-subreddit-assets.test.js +124 -0
- package/dist/register-subreddit-assets.test.js.map +1 -0
- package/package.json +3 -3
- package/skills/manifest.json +1 -1
- package/skills/social-hub-calendar-directives/SKILL.md +69 -0
- package/skills/social-hub-content-review/SKILL.md +10 -0
- package/skills/social-hub-publishing/SKILL.md +16 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/** 逗号分隔的权限位 → 数组。空串/未给 → undefined(= 不声明,不是"声明为空")。 */
|
|
3
|
+
export declare function parseDeclaredPermissions(raw: string | undefined): string[] | undefined;
|
|
4
|
+
export declare function registerSubredditAssetCommands(program: Command): void;
|
|
5
|
+
//# sourceMappingURL=register-subreddit-assets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-subreddit-assets.d.ts","sourceRoot":"","sources":["../src/register-subreddit-assets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0BzC,uDAAuD;AACvD,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,MAAM,EAAE,GAAG,SAAS,CAOtB;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA+arE"}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { hubRequest } 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
|
+
* 自建板块资产与版主绑定(P1A)—— `social-hub subreddit-asset ...`。
|
|
7
|
+
*
|
|
8
|
+
* ## 为什么必须有这层
|
|
9
|
+
* 需求明确「存量 20 板块 + 20 条版主绑定靠这组接口人工录入,**不做飞书导入代码**」。
|
|
10
|
+
* 没有 CLI,那 20 条就只能靠手写 curl —— 而其中每一条都要带 `rowVersion` 做 CAS、
|
|
11
|
+
* 要能当场看出「这个版主号是不是已经被封了」。
|
|
12
|
+
*
|
|
13
|
+
* ## 三个必须知道的语义
|
|
14
|
+
* 1. **写端点在 API 侧另有 `isUnrestrictedAdminSubject` 硬门**:连 role=admin 的
|
|
15
|
+
* API key 都会被拒。所以这里所有写命令都带 `userOnly: true`,本地先拒掉 api_key,
|
|
16
|
+
* 省掉一次必然 403 的往返。
|
|
17
|
+
* 2. **建一条绑定 = 把一个账号的全部自动化永久停掉**(enforce 档下,无个体 override)。
|
|
18
|
+
* 因此写命令一律走 `--dry-run` / `--apply` 双闸。
|
|
19
|
+
* 3. `protection set --mode enforce` 是**全局**动作,一次影响全部 20 个账号。
|
|
20
|
+
*/
|
|
21
|
+
const BASE = "/v1/system/subreddit-assets";
|
|
22
|
+
const PROTECTION = "/v1/system/moderator-protection";
|
|
23
|
+
/** 逗号分隔的权限位 → 数组。空串/未给 → undefined(= 不声明,不是"声明为空")。 */
|
|
24
|
+
export function parseDeclaredPermissions(raw) {
|
|
25
|
+
if (raw === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
const parts = raw
|
|
28
|
+
.split(",")
|
|
29
|
+
.map((part) => part.trim())
|
|
30
|
+
.filter((part) => part.length > 0);
|
|
31
|
+
return parts.length > 0 ? parts : [];
|
|
32
|
+
}
|
|
33
|
+
export function registerSubredditAssetCommands(program) {
|
|
34
|
+
const asset = program
|
|
35
|
+
.command("subreddit-asset")
|
|
36
|
+
.description("自建板块资产与版主绑定(系统级;读非 client 可用,写仅真实登录 admin 且拒 API Key)");
|
|
37
|
+
/* ── 读 ──────────────────────────────────────────────────────────────── */
|
|
38
|
+
asset
|
|
39
|
+
.command("list")
|
|
40
|
+
.description("GET /v1/system/subreddit-assets — 列出自建板块(含版主绑定与只读风险投影)")
|
|
41
|
+
.option("--asset-status <s>", "active | archived | lost")
|
|
42
|
+
.option("--moderator-risk <r>", "zero | single | redundant —— 零/单版主的发现入口(只发现,不做写入硬拒绝)")
|
|
43
|
+
.option("--brand-id <uuid>", "只看挂在该品牌下的资产")
|
|
44
|
+
.option("--limit <n>", "最多返回条数(1-200)")
|
|
45
|
+
.option("--offset <n>", "偏移")
|
|
46
|
+
.action(async (opts) => {
|
|
47
|
+
await withCliErrorBoundary(async () => {
|
|
48
|
+
await withPermissionGates({
|
|
49
|
+
command: "subreddit-asset list",
|
|
50
|
+
gates: [{ resource: "subredditAsset", action: "list" }],
|
|
51
|
+
}, async () => {
|
|
52
|
+
const params = new URLSearchParams();
|
|
53
|
+
if (opts.assetStatus)
|
|
54
|
+
params.set("assetStatus", opts.assetStatus);
|
|
55
|
+
if (opts.moderatorRisk)
|
|
56
|
+
params.set("moderatorRisk", opts.moderatorRisk);
|
|
57
|
+
if (opts.brandId)
|
|
58
|
+
params.set("brandId", opts.brandId);
|
|
59
|
+
if (opts.limit)
|
|
60
|
+
params.set("limit", opts.limit);
|
|
61
|
+
if (opts.offset)
|
|
62
|
+
params.set("offset", opts.offset);
|
|
63
|
+
const qs = params.toString();
|
|
64
|
+
printResult(await hubRequest(`${BASE}${qs ? `?${qs}` : ""}`));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
asset
|
|
69
|
+
.command("get")
|
|
70
|
+
.description("GET /v1/system/subreddit-assets/:id — 单条资产详情")
|
|
71
|
+
.requiredOption("--id <uuid>", "owned_subreddits.id")
|
|
72
|
+
.action(async (opts) => {
|
|
73
|
+
await withCliErrorBoundary(async () => {
|
|
74
|
+
await withPermissionGates({
|
|
75
|
+
command: "subreddit-asset get",
|
|
76
|
+
gates: [{ resource: "subredditAsset", action: "read" }],
|
|
77
|
+
}, async () => {
|
|
78
|
+
printResult(await hubRequest(`${BASE}/${encodeURIComponent(opts.id)}`));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
/* ── 写:板块身份 ────────────────────────────────────────────────────── */
|
|
83
|
+
asset
|
|
84
|
+
.command("register-subreddit")
|
|
85
|
+
.description("POST .../subreddits — 只登记 canonical 板块身份,**不建资产**(幂等)。先录名字核对拼写,再逐个确认所有权")
|
|
86
|
+
.requiredOption("--name <subreddit>", "板块名(带不带 r/ 都行,服务端归一)")
|
|
87
|
+
.option("--display-name <s>", "展示用原样名")
|
|
88
|
+
.option("--dry-run", "预览", false)
|
|
89
|
+
.option("--apply", "执行写入", false)
|
|
90
|
+
.action(async (opts) => {
|
|
91
|
+
await withCliErrorBoundary(async () => {
|
|
92
|
+
await withPermissionGates({
|
|
93
|
+
command: "subreddit-asset register-subreddit",
|
|
94
|
+
gates: [{ resource: "subredditAsset", action: "create" }],
|
|
95
|
+
userOnly: true,
|
|
96
|
+
}, async () => {
|
|
97
|
+
const body = { name: opts.name };
|
|
98
|
+
if (opts.displayName)
|
|
99
|
+
body.displayName = opts.displayName;
|
|
100
|
+
if (!assertApplyOrDryRun(opts, {
|
|
101
|
+
command: "subreddit-asset register-subreddit",
|
|
102
|
+
danger: "write",
|
|
103
|
+
summary: body,
|
|
104
|
+
})) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
printResult(await hubRequest(`${BASE}/subreddits`, { method: "POST", body }));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
/* ── 写:资产 ────────────────────────────────────────────────────────── */
|
|
112
|
+
asset
|
|
113
|
+
.command("register")
|
|
114
|
+
.description("POST /v1/system/subreddit-assets — 登记一条自建板块资产(幂等;已存在返回 created=false)")
|
|
115
|
+
.requiredOption("--name <subreddit>", "板块名(带不带 r/ 都行,服务端归一)")
|
|
116
|
+
.option("--display-name <s>", "展示用原样名")
|
|
117
|
+
.option("--brand-id <uuid>", "可选:板块是公司资产,不挂品牌也行(不挂则品牌受限主体看不到它)")
|
|
118
|
+
.option("--asset-status <s>", "active(默认)| archived | lost")
|
|
119
|
+
.option("--notes <s>", "备注")
|
|
120
|
+
.option("--dry-run", "预览", false)
|
|
121
|
+
.option("--apply", "执行写入", false)
|
|
122
|
+
.action(async (opts) => {
|
|
123
|
+
await withCliErrorBoundary(async () => {
|
|
124
|
+
await withPermissionGates({
|
|
125
|
+
command: "subreddit-asset register",
|
|
126
|
+
gates: [{ resource: "subredditAsset", action: "create" }],
|
|
127
|
+
userOnly: true,
|
|
128
|
+
}, async () => {
|
|
129
|
+
const body = { name: opts.name };
|
|
130
|
+
if (opts.displayName)
|
|
131
|
+
body.displayName = opts.displayName;
|
|
132
|
+
if (opts.brandId)
|
|
133
|
+
body.brandId = opts.brandId;
|
|
134
|
+
if (opts.assetStatus)
|
|
135
|
+
body.assetStatus = opts.assetStatus;
|
|
136
|
+
if (opts.notes)
|
|
137
|
+
body.notes = opts.notes;
|
|
138
|
+
if (!assertApplyOrDryRun(opts, {
|
|
139
|
+
command: "subreddit-asset register",
|
|
140
|
+
danger: "write",
|
|
141
|
+
summary: body,
|
|
142
|
+
})) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
printResult(await hubRequest(BASE, { method: "POST", body }));
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
asset
|
|
150
|
+
.command("set-status")
|
|
151
|
+
.description("PATCH .../:id/status — 改资产状态(CAS)。⚠️ lost 解除该板块所有绑定账号的阻断并停止核验;archived 保留阻断")
|
|
152
|
+
.requiredOption("--id <uuid>", "owned_subreddits.id")
|
|
153
|
+
.requiredOption("--asset-status <s>", "active | archived | lost")
|
|
154
|
+
.requiredOption("--row-version <n>", "乐观锁;漂移返 409")
|
|
155
|
+
.option("--reason <s>", "变更原因(进审计)")
|
|
156
|
+
.option("--dry-run", "预览", false)
|
|
157
|
+
.option("--apply", "执行写入", false)
|
|
158
|
+
.action(async (opts) => {
|
|
159
|
+
await withCliErrorBoundary(async () => {
|
|
160
|
+
await withPermissionGates({
|
|
161
|
+
command: "subreddit-asset set-status",
|
|
162
|
+
gates: [{ resource: "subredditAsset", action: "update" }],
|
|
163
|
+
userOnly: true,
|
|
164
|
+
}, async () => {
|
|
165
|
+
const body = {
|
|
166
|
+
assetStatus: opts.assetStatus,
|
|
167
|
+
rowVersion: Number(opts.rowVersion),
|
|
168
|
+
};
|
|
169
|
+
if (opts.reason)
|
|
170
|
+
body.reason = opts.reason;
|
|
171
|
+
if (!assertApplyOrDryRun(opts, {
|
|
172
|
+
command: "subreddit-asset set-status",
|
|
173
|
+
danger: "write",
|
|
174
|
+
summary: body,
|
|
175
|
+
})) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
printResult(await hubRequest(`${BASE}/${encodeURIComponent(opts.id)}/status`, { method: "PATCH", body }));
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
/* ── 写:版主绑定 ────────────────────────────────────────────────────── */
|
|
183
|
+
const mod = asset
|
|
184
|
+
.command("moderator")
|
|
185
|
+
.description("版主绑定:加 / 撤销 / 更替(更替 = 关旧建新,不就地改账号)");
|
|
186
|
+
mod
|
|
187
|
+
.command("add")
|
|
188
|
+
.description("POST .../:id/moderators — 加一条版主绑定。⚠️ 返回 accountAtRisk=true 表示该账号已 banned/restricted")
|
|
189
|
+
.requiredOption("--owned-subreddit-id <uuid>", "owned_subreddits.id")
|
|
190
|
+
.requiredOption("--social-account-id <uuid>", "我方账号 id(可跨 team)")
|
|
191
|
+
.option("--not-primary", "不作为该板块的责任主号", false)
|
|
192
|
+
.option("--declared-permissions <list>", "人工声明的权限位,逗号分隔(如 all,posts,flair)")
|
|
193
|
+
.option("--notes <s>", "备注")
|
|
194
|
+
.option("--dry-run", "预览", false)
|
|
195
|
+
.option("--apply", "执行写入", false)
|
|
196
|
+
.action(async (opts) => {
|
|
197
|
+
await withCliErrorBoundary(async () => {
|
|
198
|
+
await withPermissionGates({
|
|
199
|
+
command: "subreddit-asset moderator add",
|
|
200
|
+
gates: [{ resource: "subredditAsset", action: "update" }],
|
|
201
|
+
userOnly: true,
|
|
202
|
+
}, async () => {
|
|
203
|
+
const body = {
|
|
204
|
+
socialAccountId: opts.socialAccountId,
|
|
205
|
+
};
|
|
206
|
+
if (opts.notPrimary)
|
|
207
|
+
body.isPrimary = false;
|
|
208
|
+
const perms = parseDeclaredPermissions(opts.declaredPermissions);
|
|
209
|
+
if (perms !== undefined)
|
|
210
|
+
body.declaredPermissions = perms;
|
|
211
|
+
if (opts.notes)
|
|
212
|
+
body.notes = opts.notes;
|
|
213
|
+
if (!assertApplyOrDryRun(opts, {
|
|
214
|
+
command: "subreddit-asset moderator add",
|
|
215
|
+
danger: "write",
|
|
216
|
+
summary: body,
|
|
217
|
+
})) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
printResult(await hubRequest(`${BASE}/${encodeURIComponent(opts.ownedSubredditId)}/moderators`, { method: "POST", body }));
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
mod
|
|
225
|
+
.command("revoke")
|
|
226
|
+
.description("POST .../moderators/:bindingId/revoke — 撤销绑定(CAS)。行永久保留,历史不可抹")
|
|
227
|
+
.requiredOption("--binding-id <uuid>", "subreddit_moderator_bindings.id")
|
|
228
|
+
.requiredOption("--row-version <n>", "乐观锁;漂移返 409")
|
|
229
|
+
.option("--reason <s>", "撤销原因(进审计)")
|
|
230
|
+
.option("--dry-run", "预览", false)
|
|
231
|
+
.option("--apply", "执行写入", false)
|
|
232
|
+
.action(async (opts) => {
|
|
233
|
+
await withCliErrorBoundary(async () => {
|
|
234
|
+
await withPermissionGates({
|
|
235
|
+
command: "subreddit-asset moderator revoke",
|
|
236
|
+
gates: [{ resource: "subredditAsset", action: "update" }],
|
|
237
|
+
userOnly: true,
|
|
238
|
+
}, async () => {
|
|
239
|
+
const body = {
|
|
240
|
+
rowVersion: Number(opts.rowVersion),
|
|
241
|
+
};
|
|
242
|
+
if (opts.reason)
|
|
243
|
+
body.reason = opts.reason;
|
|
244
|
+
if (!assertApplyOrDryRun(opts, {
|
|
245
|
+
command: "subreddit-asset moderator revoke",
|
|
246
|
+
danger: "write",
|
|
247
|
+
summary: body,
|
|
248
|
+
})) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
printResult(await hubRequest(`${BASE}/moderators/${encodeURIComponent(opts.bindingId)}/revoke`, { method: "POST", body }));
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
mod
|
|
256
|
+
.command("replace")
|
|
257
|
+
.description("POST .../moderators/replace — 更替版主(关旧建新;就地改 accountId 会永久丢失「历史上是谁」)")
|
|
258
|
+
.requiredOption("--binding-id <uuid>", "被替换的绑定(必须仍 active)")
|
|
259
|
+
.requiredOption("--row-version <n>", "乐观锁;漂移返 409")
|
|
260
|
+
.requiredOption("--next-social-account-id <uuid>", "接任的账号 id")
|
|
261
|
+
.option("--declared-permissions <list>", "新绑定的权限位,逗号分隔")
|
|
262
|
+
.option("--reason <s>", "更替原因(进审计)")
|
|
263
|
+
.option("--dry-run", "预览", false)
|
|
264
|
+
.option("--apply", "执行写入", false)
|
|
265
|
+
.action(async (opts) => {
|
|
266
|
+
await withCliErrorBoundary(async () => {
|
|
267
|
+
await withPermissionGates({
|
|
268
|
+
command: "subreddit-asset moderator replace",
|
|
269
|
+
gates: [{ resource: "subredditAsset", action: "update" }],
|
|
270
|
+
userOnly: true,
|
|
271
|
+
}, async () => {
|
|
272
|
+
const body = {
|
|
273
|
+
bindingId: opts.bindingId,
|
|
274
|
+
rowVersion: Number(opts.rowVersion),
|
|
275
|
+
nextSocialAccountId: opts.nextSocialAccountId,
|
|
276
|
+
};
|
|
277
|
+
const perms = parseDeclaredPermissions(opts.declaredPermissions);
|
|
278
|
+
if (perms !== undefined)
|
|
279
|
+
body.declaredPermissions = perms;
|
|
280
|
+
if (opts.reason)
|
|
281
|
+
body.reason = opts.reason;
|
|
282
|
+
if (!assertApplyOrDryRun(opts, {
|
|
283
|
+
command: "subreddit-asset moderator replace",
|
|
284
|
+
danger: "write",
|
|
285
|
+
summary: body,
|
|
286
|
+
})) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
printResult(await hubRequest(`${BASE}/moderators/replace`, {
|
|
290
|
+
method: "POST",
|
|
291
|
+
body,
|
|
292
|
+
}));
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
/* ── rollout 档位 ────────────────────────────────────────────────────── */
|
|
297
|
+
const protection = asset
|
|
298
|
+
.command("protection")
|
|
299
|
+
.description("全局版主保护档位(off → shadow → enforce)。无个体 override,只保留受审计的全局回退");
|
|
300
|
+
protection
|
|
301
|
+
.command("get")
|
|
302
|
+
.description("GET /v1/system/moderator-protection — 读当前档位与 rowVersion")
|
|
303
|
+
.action(async () => {
|
|
304
|
+
await withCliErrorBoundary(async () => {
|
|
305
|
+
await withPermissionGates({
|
|
306
|
+
command: "subreddit-asset protection get",
|
|
307
|
+
gates: [{ resource: "subredditAsset", action: "read" }],
|
|
308
|
+
}, async () => {
|
|
309
|
+
printResult(await hubRequest(PROTECTION));
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
protection
|
|
314
|
+
.command("set")
|
|
315
|
+
.description("PATCH /v1/system/moderator-protection — 改全局档位(CAS + 审计)。⚠️ enforce 一次影响全部持有绑定的账号")
|
|
316
|
+
.requiredOption("--mode <m>", "off | shadow | enforce")
|
|
317
|
+
.requiredOption("--row-version <n>", "乐观锁;漂移返 409(不接受 last-writer-wins)")
|
|
318
|
+
.option("--note <s>", "变更说明(进审计)")
|
|
319
|
+
.option("--dry-run", "预览", false)
|
|
320
|
+
.option("--apply", "执行写入", false)
|
|
321
|
+
.action(async (opts) => {
|
|
322
|
+
await withCliErrorBoundary(async () => {
|
|
323
|
+
await withPermissionGates({
|
|
324
|
+
command: "subreddit-asset protection set",
|
|
325
|
+
gates: [{ resource: "subredditAsset", action: "update" }],
|
|
326
|
+
userOnly: true,
|
|
327
|
+
}, async () => {
|
|
328
|
+
const body = {
|
|
329
|
+
mode: opts.mode,
|
|
330
|
+
rowVersion: Number(opts.rowVersion),
|
|
331
|
+
};
|
|
332
|
+
if (opts.note)
|
|
333
|
+
body.note = opts.note;
|
|
334
|
+
if (!assertApplyOrDryRun(opts, {
|
|
335
|
+
command: "subreddit-asset protection set",
|
|
336
|
+
// 全局阻断开关:切 enforce 会一次停掉全部版主账号的自动化。
|
|
337
|
+
danger: "write",
|
|
338
|
+
summary: body,
|
|
339
|
+
})) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
printResult(await hubRequest(PROTECTION, { method: "PATCH", body }));
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
//# sourceMappingURL=register-subreddit-assets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-subreddit-assets.js","sourceRoot":"","sources":["../src/register-subreddit-assets.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-subreddit-assets.test.d.ts","sourceRoot":"","sources":["../src/register-subreddit-assets.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { parseDeclaredPermissions, registerSubredditAssetCommands, } from "./register-subreddit-assets.js";
|
|
4
|
+
import { collectCommandPaths } from "./cmd-manifest.js";
|
|
5
|
+
describe("parseDeclaredPermissions", () => {
|
|
6
|
+
it("逗号分隔 → 数组,去空白", () => {
|
|
7
|
+
expect(parseDeclaredPermissions("all, posts ,flair")).toEqual([
|
|
8
|
+
"all",
|
|
9
|
+
"posts",
|
|
10
|
+
"flair",
|
|
11
|
+
]);
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* 🔴 「没给这个选项」与「显式给了空」必须可区分。
|
|
15
|
+
* 前者 → `undefined`(不写这个字段,服务端保留既有声明);
|
|
16
|
+
* 后者 → `[]`(显式声明"没有任何权限位")。
|
|
17
|
+
* 混成一个值的话,一次漏传选项就会把人工声明的权限位清空 —— 而 declaredPermissions
|
|
18
|
+
* 是「我们应该拥有什么权限」的人工记录,被静默清掉就再也对不出来了。
|
|
19
|
+
*/
|
|
20
|
+
it("未给选项 → undefined(不是空数组)", () => {
|
|
21
|
+
expect(parseDeclaredPermissions(undefined)).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
it("给了空串 / 全是分隔符 → 空数组(显式声明为空)", () => {
|
|
24
|
+
expect(parseDeclaredPermissions("")).toEqual([]);
|
|
25
|
+
expect(parseDeclaredPermissions(" , , ")).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe("命令树", () => {
|
|
29
|
+
it("命令树完整(读 2 / 身份写 1 / 资产写 2 / 绑定写 3 / 档位 2 + 两个分组)", () => {
|
|
30
|
+
const program = new Command();
|
|
31
|
+
registerSubredditAssetCommands(program);
|
|
32
|
+
const paths = collectCommandPaths(program).map((entry) => entry.path);
|
|
33
|
+
expect(paths).toEqual(expect.arrayContaining([
|
|
34
|
+
"subreddit-asset",
|
|
35
|
+
"subreddit-asset list",
|
|
36
|
+
"subreddit-asset get",
|
|
37
|
+
"subreddit-asset register-subreddit",
|
|
38
|
+
"subreddit-asset register",
|
|
39
|
+
"subreddit-asset set-status",
|
|
40
|
+
"subreddit-asset moderator",
|
|
41
|
+
"subreddit-asset moderator add",
|
|
42
|
+
"subreddit-asset moderator revoke",
|
|
43
|
+
"subreddit-asset moderator replace",
|
|
44
|
+
"subreddit-asset protection",
|
|
45
|
+
"subreddit-asset protection get",
|
|
46
|
+
"subreddit-asset protection set",
|
|
47
|
+
]));
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* 🔴 每一个**写**命令都必须有 `--dry-run` / `--apply` 双闸。
|
|
51
|
+
* 建一条绑定 = enforce 档下把一个账号的全部自动化永久停掉(无个体 override);
|
|
52
|
+
* `protection set --mode enforce` 更是一次影响全部 20 个账号。
|
|
53
|
+
* 少一个闸就意味着有条路径能不经确认做出这种事。
|
|
54
|
+
*/
|
|
55
|
+
it("所有写命令都带 --dry-run / --apply", () => {
|
|
56
|
+
const program = new Command();
|
|
57
|
+
registerSubredditAssetCommands(program);
|
|
58
|
+
const asset = program.commands.find((cmd) => cmd.name() === "subreddit-asset");
|
|
59
|
+
const writeLeaves = [];
|
|
60
|
+
const walk = (cmd, prefix) => {
|
|
61
|
+
for (const sub of cmd.commands) {
|
|
62
|
+
const next = [...prefix, sub.name()];
|
|
63
|
+
if (sub.commands.length > 0)
|
|
64
|
+
walk(sub, next);
|
|
65
|
+
else
|
|
66
|
+
writeLeaves.push(next.join(" "));
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
walk(asset, []);
|
|
70
|
+
const writes = [
|
|
71
|
+
"register-subreddit",
|
|
72
|
+
"register",
|
|
73
|
+
"set-status",
|
|
74
|
+
"moderator add",
|
|
75
|
+
"moderator revoke",
|
|
76
|
+
"moderator replace",
|
|
77
|
+
"protection set",
|
|
78
|
+
];
|
|
79
|
+
for (const path of writes) {
|
|
80
|
+
expect(writeLeaves).toContain(path);
|
|
81
|
+
const parts = path.split(" ");
|
|
82
|
+
let cursor = asset;
|
|
83
|
+
for (const part of parts) {
|
|
84
|
+
cursor = cursor.commands.find((cmd) => cmd.name() === part);
|
|
85
|
+
expect(cursor).toBeDefined();
|
|
86
|
+
}
|
|
87
|
+
const flags = cursor.options.map((opt) => opt.long);
|
|
88
|
+
expect(flags).toContain("--dry-run");
|
|
89
|
+
expect(flags).toContain("--apply");
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
/** CAS 是这套接口的硬要求:改状态/撤销/更替/改档位都必须带 rowVersion。 */
|
|
93
|
+
it("所有 CAS 命令都把 --row-version 设为必填", () => {
|
|
94
|
+
const program = new Command();
|
|
95
|
+
registerSubredditAssetCommands(program);
|
|
96
|
+
const asset = program.commands.find((cmd) => cmd.name() === "subreddit-asset");
|
|
97
|
+
const casPaths = [
|
|
98
|
+
["set-status"],
|
|
99
|
+
["moderator", "revoke"],
|
|
100
|
+
["moderator", "replace"],
|
|
101
|
+
["protection", "set"],
|
|
102
|
+
];
|
|
103
|
+
for (const parts of casPaths) {
|
|
104
|
+
let cursor = asset;
|
|
105
|
+
for (const part of parts) {
|
|
106
|
+
cursor = cursor.commands.find((cmd) => cmd.name() === part);
|
|
107
|
+
}
|
|
108
|
+
const rowVersion = cursor.options.find((opt) => opt.long === "--row-version");
|
|
109
|
+
expect(rowVersion, parts.join(" ")).toBeDefined();
|
|
110
|
+
expect(rowVersion.required || rowVersion.mandatory).toBe(true);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
/** 读命令不该有写闸(否则 `list --apply` 这种误用会读起来像在写)。 */
|
|
114
|
+
it("读命令不带 --apply", () => {
|
|
115
|
+
const program = new Command();
|
|
116
|
+
registerSubredditAssetCommands(program);
|
|
117
|
+
const asset = program.commands.find((cmd) => cmd.name() === "subreddit-asset");
|
|
118
|
+
for (const name of ["list", "get"]) {
|
|
119
|
+
const cmd = asset.commands.find((c) => c.name() === name);
|
|
120
|
+
expect(cmd.options.map((opt) => opt.long)).not.toContain("--apply");
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
//# sourceMappingURL=register-subreddit-assets.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-subreddit-assets.test.js","sourceRoot":"","sources":["../src/register-subreddit-assets.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geoly-ai/social-hub-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "social-hub CLI for Social Ops Hub",
|
|
6
6
|
"repository": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"commander": "^12.1.0",
|
|
25
|
-
"@geoly-ai/social-hub-authz": "0.0.
|
|
26
|
-
"@geoly-ai/social-hub-sdk": "0.1.
|
|
25
|
+
"@geoly-ai/social-hub-authz": "0.0.19",
|
|
26
|
+
"@geoly-ai/social-hub-sdk": "0.1.2"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^22.10.2",
|
package/skills/manifest.json
CHANGED
|
@@ -341,6 +341,75 @@ state,确认那个 cron 仍属于你这次触发的 revision。preflight 是
|
|
|
341
341
|
完全可能已经用更高 revision 把同名 cron 换掉了 —— 无条件删就是把新的那个删掉,
|
|
342
342
|
于是改期后的发布静默消失。
|
|
343
343
|
|
|
344
|
+
## 🔴 preflight 之后:**先抢执行权,再发帖**(R7,执行权排他)
|
|
345
|
+
|
|
346
|
+
preflight 是**纯读**。两台机在状态变化前同时调它,会**都**拿到 `decision:"ok"` ——
|
|
347
|
+
它从来就不是那道闸门。真正的排他是下一步:**抢 `scheduled → running`**。
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
preflight 拿到 ok
|
|
351
|
+
↓
|
|
352
|
+
PATCH /calendar-entries/:id { "status": "running", "claimRequestId": "<你生成的 UUID>" }
|
|
353
|
+
├─ 200 + execution.token → 你赢了,继续
|
|
354
|
+
└─ 409 EXECUTION_NOT_ACQUIRED, reason=execution_already_acquired
|
|
355
|
+
→ 别人在发。安静退出 + 删本机 cron,**不要重试、不要告警**
|
|
356
|
+
↓
|
|
357
|
+
提交到 Reddit
|
|
358
|
+
↓
|
|
359
|
+
PATCH { "status": "succeeded", "permalink": "…", "executionToken": "<刚才那个 token>" }
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### ⛔ 绝不能先提交再抢 running
|
|
363
|
+
|
|
364
|
+
顺序反过来,排他就完全失效了 —— 两台机都发完了才发现只有一台能记账,Reddit 上已经两个帖子。
|
|
365
|
+
|
|
366
|
+
### 🔴 `claimRequestId` 必须带
|
|
367
|
+
|
|
368
|
+
它是「抢占请求已经到了服务端,但 HTTP 响应在回来的路上丢了」的**唯一**补救:
|
|
369
|
+
拿同一个 `claimRequestId` 重试,服务端会把**原来那个** token 奉还给你,epoch 不再递增。
|
|
370
|
+
不带它,那一次网络抖动就留下一条永久卡在 `running`、却没有任何人握着 token 的条目,
|
|
371
|
+
只能找管理员强制终结。十台机铺开之后,这是常态而不是异常。
|
|
372
|
+
|
|
373
|
+
⚠️ 每条条目的每一轮执行用**一个新的** `claimRequestId`;重试时**复用**那一个。
|
|
374
|
+
|
|
375
|
+
### 🔴 抢到执行权之后,你会收到一条自己触发的 `cancel` —— 不要停
|
|
376
|
+
|
|
377
|
+
条目一旦离开 `scheduled` 就不再 eligible,于是抢占**在同一个事务里**产生一条 `cancel`
|
|
378
|
+
指令、`directive_revision` +1。那条 cancel 会广播给**包括你自己在内**的全部机器。
|
|
379
|
+
|
|
380
|
+
**协议规定:`cancel` 只删除「未来的」cron,绝不中止已经持有 token 的在途执行。**
|
|
381
|
+
|
|
382
|
+
把这条实现错,十台机会在赢家刚抢到执行权的那一刻集体停手 —— **零发布**,
|
|
383
|
+
比根本不做执行权排他还糟。判据很简单:**你手上有没有这条条目的 execution token**。
|
|
384
|
+
有就继续发完并回写;没有才按 cancel 处置。
|
|
385
|
+
|
|
386
|
+
### token 的保管
|
|
387
|
+
|
|
388
|
+
`execution.token` 是 **bearer 凭据**:谁拿着它谁就能写这条条目的终态。
|
|
389
|
+
|
|
390
|
+
- 写进本机 state 文件(和 cron/`appliedRevision` 同一个锁内),**别打日志、别回显**。
|
|
391
|
+
- 进程重启后要能读回来 —— 读不回来就只能靠 `claimRequestId` 重取。
|
|
392
|
+
|
|
393
|
+
### 终态回写**不要**带 `expectedRevision`
|
|
394
|
+
|
|
395
|
+
抢占那一次自己把 revision 从 N 推到了 N+1(见上一节)。终态再带 N 必然失败,
|
|
396
|
+
你会拿到 `expected_revision_not_applicable`。**版本钉子只属于抢占那一次**,
|
|
397
|
+
终态靠 token 证明身份。
|
|
398
|
+
|
|
399
|
+
### 新增的 409 reason 怎么处置
|
|
400
|
+
|
|
401
|
+
| reason | 含义 | 本地动作 |
|
|
402
|
+
| ------------------------------------- | ----------------------------------------- | -------------------------------------------------------- |
|
|
403
|
+
| `execution_already_acquired` | 别人先抢到了 | 删 cron、安静退出。**这是日常路径,不要告警** |
|
|
404
|
+
| `execution_token_required` | 条目有在途授权,你没带 token | 你多半丢了 token。用原 `claimRequestId` 重取,或上报人工 |
|
|
405
|
+
| `execution_token_mismatch` | 带的 token 不是当前那个(迟到的旧 epoch) | **停手**。别重试,你这次执行已经作废 |
|
|
406
|
+
| `execution_token_not_expected` | 初次抢占时带了 token | 代码 bug:抢占不该带 token |
|
|
407
|
+
| `expected_revision_not_applicable` | 终态回写带了 `expectedRevision` | 代码 bug:去掉它 |
|
|
408
|
+
| `revision_stale` / `revision_unknown` | 抢占时的版本钉子对不上 | 同 preflight 同名 reason 的处置 |
|
|
409
|
+
|
|
410
|
+
⚠️ 这些都走 **HTTP 409**、`code = "EXECUTION_NOT_ACQUIRED"`、`details.reason` 取上表的值。
|
|
411
|
+
**按 reason 编程分流,别拿人类文案做子串匹配。**
|
|
412
|
+
|
|
344
413
|
## ack 时机(本 source 局部改写通用规则)
|
|
345
414
|
|
|
346
415
|
通用 notifications skill 说"副作用完成后才 ack"——这条仍然成立,但**这个 source 的副作用是
|