@geoly-ai/social-hub-sdk 0.1.2 → 0.1.3

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/dist/index.d.ts CHANGED
@@ -1,7 +1,77 @@
1
+ /**
2
+ * 「返回了不完整结果、但看起来像完整的」这一族缺陷的统一探针。
3
+ *
4
+ * 触发条件:JSON 响应体是个分页列表,而这一页并不是全部。判据有两条,任一成立即触发:
5
+ * - `hasMore === true`(**最可靠**的信号,总数被封顶/缺失时唯一可用的那条);
6
+ * - `items.length < total`,且 `total` 确实是**精确条数**(见 `totalRelation`)。
7
+ *
8
+ * ⚠️ 判定必须在**运行时按响应体形状**做,不能按 SDK 方法的返回类型做:
9
+ * 实测 apps/api 里返回该形状的端点有 43 个,而 SDK 在类型上声明了 `items`+`total`
10
+ * 的方法只有 20 个(`listSystemSubredditBlocklists` 就曾把返回类型误写成
11
+ * `{ items: unknown[] }`,漏掉了 `total`)。按类型判会漏掉一半以上。
12
+ *
13
+ * SDK 默认**不设**这个回调,所以对既有 SDK 消费者零行为变化;由 CLI 装上一个
14
+ * 写 stderr 的实现(stdout 必须保持干净可 `jq` 解析)。
15
+ */
16
+ export type PartialListInfo = {
17
+ /**
18
+ * **只有 pathname,不含 query string**。查询串里可能带 `search=<关键词>` 这类
19
+ * 调用方输入,而这条信息是要打到 stderr 上的 —— 不该顺手把用户的搜索词写进
20
+ * 日志。(Codex 评审提出)
21
+ */
22
+ path: string;
23
+ itemsLength: number;
24
+ /** 服务端给出的总数原值;`totalRelation` 决定它能不能当精确条数用。 */
25
+ total?: number;
26
+ /**
27
+ * 服务端对 `total` 精确度的自述(迁移 0140 起部分端点会封顶计数):
28
+ * - `eq` / 缺省 —— `total` 是精确条数;
29
+ * - `gte` —— `total` 只是**下界**(计数被封顶),不能据此算「还缺几条」;
30
+ * - `unavailable` —— 没有总数,分页信号只有 `hasMore`。
31
+ */
32
+ totalRelation?: string;
33
+ /** 服务端明说还有下一页。比任何基于 `total` 的推算都可靠。 */
34
+ hasMore?: boolean;
35
+ limit?: number;
36
+ offset?: number;
37
+ };
1
38
  export type SocialHubClientOptions = {
2
39
  baseUrl: string;
3
40
  apiKey: string;
41
+ /** 见 {@link PartialListInfo};不传则完全不做通知(默认行为不变)。 */
42
+ onPartialList?: (info: PartialListInfo) => void;
4
43
  };
44
+ /** system subreddit blocklist 的一行(与 contracts subredditBlocklistSchema 同形)。 */
45
+ export type SystemSubredditBlocklistEntry = {
46
+ id: string;
47
+ kind: string;
48
+ subreddit: string;
49
+ reason: string | null;
50
+ source: string | null;
51
+ expiresAt: string | null;
52
+ createdAt: string;
53
+ };
54
+ /**
55
+ * 按形状识别「分页列表返回体」并判断是否被截断。返回 undefined 表示不是该形状,
56
+ * 或者这一页已经是全部。
57
+ *
58
+ * 刻意**只**认 `items` + `total` 两个字段同时存在:仓库里 `total` 作为「条数」
59
+ * 的用法与 `items` 恒定成对出现(43 个端点),而把 `total` 用作金额/总分之类
60
+ * 其它语义的返回体不带 `items` 数组,不会误伤。
61
+ */
62
+ export declare function detectPartialList(path: string, body: unknown): PartialListInfo | undefined;
63
+ /**
64
+ * 「拿到的是一页、不是全量」的统一措辞。与仓库既有的 CSV 导出约定同源 ——
65
+ * 那边命中行数硬顶时必须在末行显式写 `mode=truncated`(见 contracts/src/common.ts),
66
+ * 一样是「绝不静默截断」。这里是同一条纪律在列表端点上的落点。
67
+ *
68
+ * ⚠️ 措辞刻意保持**通用**,两处不能写死(Codex 评审提出):
69
+ * - 不能写「用 --limit/--offset 翻页」—— `compliance blocklists-list` 这类安全闸
70
+ * 刻意**不提供**这两个参数(它自己自动翻页),照着提示做只会撞上「未知选项」;
71
+ * - 不能写「漏掉的是最早的那批」—— 那只对按时间倒序的 blocklist 成立,
72
+ * 别的端点排序不同,写死就是在编造事实。
73
+ */
74
+ export declare function formatPartialListWarning(info: PartialListInfo): string;
5
75
  export type HealthCheckJson = {
6
76
  ok?: boolean;
7
77
  db?: string;
@@ -1129,6 +1199,14 @@ export declare class SocialHubClient {
1129
1199
  /** 共享请求管线:统一 base-url 拼接、鉴权头与错误体解析,返回原始响应文本。 */
1130
1200
  private requestText;
1131
1201
  private fetchJson;
1202
+ /**
1203
+ * 所有 JSON 响应的**唯一**汇合点上做一次截断探测(见 {@link PartialListInfo})。
1204
+ *
1205
+ * 为什么钩在传输层而不是打印层:CLI 根本没有统一的打印函数 —— index.ts 里有 235 处
1206
+ * 裸 `console.log(JSON.stringify(...))`,另有 79 处 `printResult`。逐个打补丁正是
1207
+ * 这个缺陷已经复发四次的原因。而所有命令都必经 `fetchJson`。
1208
+ */
1209
+ private notifyIfPartialList;
1132
1210
  /** 与 `fetchJson` 同一条 base-url/鉴权/错误处理路径,但返回文本(CSV 等非 JSON 响应)。 */
1133
1211
  private fetchText;
1134
1212
  appendEvent(teamId: string, body: {
@@ -1950,6 +2028,7 @@ export declare class SocialHubClient {
1950
2028
  expiresAt?: string;
1951
2029
  }): Promise<{
1952
2030
  id: string;
2031
+ created: boolean;
1953
2032
  }>;
1954
2033
  updateSubredditSanction(teamId: string, id: string, body: {
1955
2034
  status?: SubredditSanctionStatus;
@@ -2904,12 +2983,65 @@ export declare class SocialHubClient {
2904
2983
  }): Promise<{
2905
2984
  id: string;
2906
2985
  }>;
2986
+ /**
2987
+ * ⚠️ **单页**,不是全量 —— 服务端按 `createdAt DESC` 排序,`limit` 上限 200、默认 100。
2988
+ *
2989
+ * 历史上这个方法**一个分页参数都不传**,于是永远只拿服务端默认的第一页 100 条,
2990
+ * 而同一个返回体里的 `total` 是 112 —— 调用方看到 `items` 就以为是全量,漏掉的
2991
+ * 恰恰是**最早加入**的那 12 条(排序是 DESC),往往是最确定该封的那批。
2992
+ *
2993
+ * 🔴 **板块准入这类安全判定不要用这个方法**,用
2994
+ * {@link SocialHubClient.listAllSystemSubredditBlocklists}(自动翻页 + 一致性校验)。
2995
+ * 这里保留分页版只是为了后台列表分页展示,以及不破坏已发布 SDK 的方法签名。
2996
+ */
2907
2997
  listSystemSubredditBlocklists(params?: {
2908
2998
  kind?: string;
2909
2999
  includeExpired?: boolean;
3000
+ limit?: number;
3001
+ offset?: number;
2910
3002
  }): Promise<{
2911
- items: unknown[];
3003
+ items: SystemSubredditBlocklistEntry[];
3004
+ total: number;
3005
+ limit: number;
3006
+ offset: number;
3007
+ }>;
3008
+ /**
3009
+ * 取**全量** system subreddit blocklist(自动翻页),供板块准入这类安全判定使用。
3010
+ *
3011
+ * 为什么必须有这个方法:分页版每页上限 200(contracts 硬上限,本次不改),所以
3012
+ * 「让调用方自己传一个够大的 limit」根本表达不了「全量」;而把正确性交给调用方
3013
+ * 记得传参,正是本次要消灭的失效模式。
3014
+ *
3015
+ * **翻页期间集合变了怎么办**:offset 分页**不是快照分页**,并发增删会导致跨页
3016
+ * 重复或跳过。口径是「要么给出可自证一致的结果,要么报错」,
3017
+ * **绝不静默拼一个可能不一致的结果**:
3018
+ * - 一趟内所有页的 `total` 必须一致、跨页不得出现重复行、去重后条数必须 === `total`;
3019
+ * - 任一条不满足 → 整趟从 `offset=0` 重跑,最多 {@link BLOCKLIST_FULL_SCAN_MAX_ATTEMPTS} 趟;
3020
+ * - 全部失败 → **抛错**,并带上每趟的**具体**原因(漂移 / 重复 / 空页 / 超预算)。
3021
+ *
3022
+ * 🔴 **这是尽力校验,不是快照证明 —— 不要把它当成后者**(Codex 评审提出)。
3023
+ * 反例:翻页期间「删一条 + 加一条」会让 `total` 纹丝不动、去重后条数也刚好对上,
3024
+ * 于是校验通过,而结果里含着已删的、缺着新加的。`expiresAt` 过滤同理 ——
3025
+ * 没人写库,集合也会随时间变化。要真正的快照语义需要服务端提供
3026
+ * as-of / ETag / keyset cursor;在那之前这里只能做到「**能查出来的**不一致一律
3027
+ * 拒绝」,查不出来的那一类仍会漏过去。所以本方法的承诺是
3028
+ * 「没报错 = 没发现不一致」,**不是**「保证是某一时刻的完整快照」。
3029
+ *
3030
+ * 抛错而不是「返回一个带 inconsistent 标记的结果」是有意的:这是安全闸的取数路径,
3031
+ * 「没有答案」是安全的(调用方重跑一次即可),而「一个可能不全、但看起来完整的答案」
3032
+ * 正是本次缺陷本身 —— 标记会被 `jq '.items'` 一秒钟丢掉。
3033
+ */
3034
+ listAllSystemSubredditBlocklists(params?: {
3035
+ kind?: string;
3036
+ includeExpired?: boolean;
3037
+ }): Promise<{
3038
+ items: SystemSubredditBlocklistEntry[];
3039
+ total: number;
3040
+ /** 为达成一致所重跑的趟数,1 表示一次就一致。 */
3041
+ attempts: number;
2912
3042
  }>;
3043
+ /** 一趟完整翻页;`consistent` 为假时调用方应整趟重来(见上)。 */
3044
+ private scanAllSystemSubredditBlocklistsOnce;
2913
3045
  upsertSystemSubredditBlocklist(subreddit: string, body: {
2914
3046
  kind?: string;
2915
3047
  reason?: string;