@geoly-ai/social-hub-cli 0.3.16 → 0.3.18
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 +17 -0
- package/dist/cmd-manifest.json +6 -2
- package/dist/cmd-manifest.test.js +4 -0
- package/dist/cmd-manifest.test.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/permissions-gates-admin.d.ts.map +1 -1
- package/dist/permissions-gates-admin.js +12 -0
- package/dist/permissions-gates-admin.js.map +1 -1
- package/dist/permissions-gates-notify.d.ts.map +1 -1
- package/dist/permissions-gates-notify.js +6 -0
- package/dist/permissions-gates-notify.js.map +1 -1
- package/dist/register-extensions.d.ts.map +1 -1
- package/dist/register-extensions.js +15 -1
- package/dist/register-extensions.js.map +1 -1
- package/dist/register-feishu-identities.d.ts +47 -0
- package/dist/register-feishu-identities.d.ts.map +1 -0
- package/dist/register-feishu-identities.js +346 -0
- package/dist/register-feishu-identities.js.map +1 -0
- package/dist/register-feishu-identities.test.d.ts +2 -0
- package/dist/register-feishu-identities.test.d.ts.map +1 -0
- package/dist/register-feishu-identities.test.js +124 -0
- package/dist/register-feishu-identities.test.js.map +1 -0
- package/dist/register-notify.js +27 -0
- package/dist/register-notify.js.map +1 -1
- package/package.json +3 -3
- package/skills/manifest.json +1 -1
- package/skills/reddit-matrix.lock.json +1 -1
- package/skills/reddit-voc-volume/SKILL.md +2 -7
- package/skills/reddit-voc-volume/matrix-contract.md +4 -3
- package/skills/reddit-voc-volume/references/arctic-shift.md +2 -1
- package/skills/reddit-voc-volume/scripts/reddit_voc_volume.py +157 -69
- package/skills/reddit-voc-volume/tests/test_archived_comments.py +104 -0
- package/skills/social-hub-admin/SKILL.md +47 -1
- package/skills/social-hub-cli/SKILL.md +1 -0
- package/skills/social-hub-notifications/SKILL.md +35 -20
- package/skills/social-hub-posts/SKILL.md +14 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { hubRequest } from "./client.js";
|
|
4
|
+
import { assertApplyOrDryRun } from "./dry-run.js";
|
|
5
|
+
import { withPermissionGates } from "./permission-runner.js";
|
|
6
|
+
/**
|
|
7
|
+
* `social-hub feishu-identities …` —— Hub 用户 ↔ 飞书 open_id 映射的运维面。
|
|
8
|
+
*
|
|
9
|
+
* 替代 `scripts/sync-feishu-identities.mjs`:那个脚本要求运维手里有**生产
|
|
10
|
+
* `DATABASE_URL`**,而这里只需要一次 `social-hub auth login`。
|
|
11
|
+
*
|
|
12
|
+
* ## 为什么飞书查询在本机而不是服务端
|
|
13
|
+
*
|
|
14
|
+
* 飞书 `contact` 查询目前只能用**个人授权**(bot 缺 `contact:user.id:readonly`),
|
|
15
|
+
* 服务端既没有应用凭据也拿不到这个身份。所以形态是:本机 `lark-cli` 查 → PUT 给
|
|
16
|
+
* Hub 落库。Hub 侧对收到的值做形态白名单 + 占用冲突检测,写入面只认不受限的交互式
|
|
17
|
+
* admin 登录。
|
|
18
|
+
*
|
|
19
|
+
* ## 两个实测出来的坑(改这段前先读)
|
|
20
|
+
*
|
|
21
|
+
* 1. **飞书按邮箱批量查时,查不到的邮箱是静默省略、不报错的。** 未命中只能靠
|
|
22
|
+
* 「请求集合 − 返回集合」求差发现,不能指望非空校验或异常。
|
|
23
|
+
* 2. **`ok=true` 但结构不对(CLI 升级换 schema、输出被截断)绝不能当空结果。**
|
|
24
|
+
* 空结果的语义是「这批人都不在通讯录」,会把整批真人写成 `not_found` 并清空
|
|
25
|
+
* 既有 open_id;而我们其实根本没拿到答案,那是 `lookup_failed`。
|
|
26
|
+
*
|
|
27
|
+
* ## 防「一次误操作清空全部映射」
|
|
28
|
+
*
|
|
29
|
+
* `not_found` 会清空旧身份(离职语义)。如果登错飞书租户 / 授权对应的是另一个组织,
|
|
30
|
+
* 全部人都会「查不到」,一次 `--apply` 就能把所有映射抹掉。因此当**此前 resolved 的人
|
|
31
|
+
* 里有超过一半这次变成 not_found** 时直接拒绝,要显式 `--force-mass-not-found`。
|
|
32
|
+
*/
|
|
33
|
+
const execFileAsync = promisify(execFile);
|
|
34
|
+
/** 一次 lark-cli 调用查多少个邮箱。飞书搜索接口对超长 query 不友好。 */
|
|
35
|
+
const LOOKUP_BATCH_SIZE = 20;
|
|
36
|
+
/** 一次 PUT 提交多少条(与服务端契约上限一致)。 */
|
|
37
|
+
const UPSERT_BATCH_SIZE = 100;
|
|
38
|
+
/** 超过这个比例的既有 resolved 变 not_found 就停机。 */
|
|
39
|
+
const MASS_NOT_FOUND_RATIO = 0.5;
|
|
40
|
+
class LookupError extends Error {
|
|
41
|
+
code;
|
|
42
|
+
constructor(code, message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.code = code;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** 错误码要能进 `^[a-z0-9_.-]{1,64}$`(服务端契约)。 */
|
|
48
|
+
function normalizeErrorCode(raw) {
|
|
49
|
+
const cleaned = raw
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9_.-]/g, "_")
|
|
52
|
+
.slice(0, 64);
|
|
53
|
+
return cleaned || "lookup_failed";
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 批量按邮箱查飞书 open_id,返回 email(小写)→ 用户对象。
|
|
57
|
+
*
|
|
58
|
+
* 抛 {@link LookupError} 表示「没拿到答案」;返回值里缺某个邮箱才表示「通讯录里没有」。
|
|
59
|
+
*/
|
|
60
|
+
async function lookupBatch(emails) {
|
|
61
|
+
let stdout;
|
|
62
|
+
try {
|
|
63
|
+
({ stdout } = await execFileAsync("lark-cli", [
|
|
64
|
+
"contact",
|
|
65
|
+
"+search-user",
|
|
66
|
+
"--queries",
|
|
67
|
+
emails.join(","),
|
|
68
|
+
"--as",
|
|
69
|
+
"user",
|
|
70
|
+
"--format",
|
|
71
|
+
"json",
|
|
72
|
+
], { maxBuffer: 16 * 1024 * 1024 }));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw new LookupError("cli_invocation_failed", `lark-cli 调用失败:${error instanceof Error ? error.message : String(error)}`);
|
|
76
|
+
}
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(stdout);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
throw new LookupError("malformed_response", "lark-cli 输出不是合法 JSON");
|
|
83
|
+
}
|
|
84
|
+
if (!parsed.ok) {
|
|
85
|
+
throw new LookupError(normalizeErrorCode(String(parsed.error?.subtype ?? parsed.error?.type ?? "lookup_failed")), `lark-cli lookup failed: ${JSON.stringify(parsed.error ?? {})}`);
|
|
86
|
+
}
|
|
87
|
+
// ok=true 但没有 users[] —— 见文件头第 2 条,这是 lookup_failed 不是空结果。
|
|
88
|
+
if (!Array.isArray(parsed.data?.users)) {
|
|
89
|
+
throw new LookupError("malformed_response", "lark-cli lookup returned no users[] array");
|
|
90
|
+
}
|
|
91
|
+
const byEmail = new Map();
|
|
92
|
+
for (const user of parsed.data.users) {
|
|
93
|
+
const openId = typeof user["open_id"] === "string" ? user["open_id"] : undefined;
|
|
94
|
+
const unionId = typeof user["union_id"] === "string" ? user["union_id"] : undefined;
|
|
95
|
+
// 同一条记录可能挂 email 与 enterprise_email 两个地址,两个都索引。
|
|
96
|
+
for (const raw of [user["email"], user["enterprise_email"]]) {
|
|
97
|
+
const key = String(raw ?? "").toLowerCase();
|
|
98
|
+
if (!key)
|
|
99
|
+
continue;
|
|
100
|
+
const existing = byEmail.get(key);
|
|
101
|
+
if (existing && existing.openId !== openId) {
|
|
102
|
+
// 一个邮箱命中两个不同的飞书人 —— 这是**歧义**,不是「后来的那个对」。
|
|
103
|
+
// 直接 Map 覆盖会静默挑一个,可能把告警 @ 给完全不相干的人。
|
|
104
|
+
byEmail.set(key, { ambiguous: true });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (existing?.ambiguous)
|
|
108
|
+
continue;
|
|
109
|
+
byEmail.set(key, {
|
|
110
|
+
...(openId === undefined ? {} : { openId }),
|
|
111
|
+
...(unionId === undefined ? {} : { unionId }),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return byEmail;
|
|
116
|
+
}
|
|
117
|
+
const OPEN_ID_RE = /^ou_[A-Za-z0-9]{1,128}$/;
|
|
118
|
+
const UNION_ID_RE = /^on_[A-Za-z0-9]{1,128}$/;
|
|
119
|
+
/**
|
|
120
|
+
* 一条查询结果 → 一条待写入 entry。
|
|
121
|
+
*
|
|
122
|
+
* 🔴 **只有「飞书明确没返回这个邮箱」才是 `not_found`**(那一条会清空既有 open_id)。
|
|
123
|
+
* 其余所有"没拿到可用答案"的情况一律 `lookup_failed`:
|
|
124
|
+
*
|
|
125
|
+
* - 命中了人但**没有 open_id**(字段缺失/被裁剪)—— 我们并不知道这人不在通讯录;
|
|
126
|
+
* - open_id / union_id 形态非法 —— 与其写进库让渲染侧面对,不如当查询异常;
|
|
127
|
+
* - 一个邮箱命中多个不同的人 —— 歧义,挑一个可能 @ 到完全不相干的人。
|
|
128
|
+
*
|
|
129
|
+
* 把上面任何一种当成 `not_found`,代价都是**静默清空一个本来能 @ 的映射**。
|
|
130
|
+
*/
|
|
131
|
+
export function classifyHit(userId, hit) {
|
|
132
|
+
if (hit === undefined) {
|
|
133
|
+
// 飞书对查不到的邮箱是静默省略 —— 这才是真正的"通讯录里没这人"。
|
|
134
|
+
return { userId, resolveStatus: "not_found" };
|
|
135
|
+
}
|
|
136
|
+
if (hit.ambiguous) {
|
|
137
|
+
return {
|
|
138
|
+
userId,
|
|
139
|
+
resolveStatus: "lookup_failed",
|
|
140
|
+
lastErrorCode: "ambiguous_match",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const openId = hit.openId ?? null;
|
|
144
|
+
if (openId === null) {
|
|
145
|
+
return {
|
|
146
|
+
userId,
|
|
147
|
+
resolveStatus: "lookup_failed",
|
|
148
|
+
lastErrorCode: "missing_open_id",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (!OPEN_ID_RE.test(openId)) {
|
|
152
|
+
return {
|
|
153
|
+
userId,
|
|
154
|
+
resolveStatus: "lookup_failed",
|
|
155
|
+
lastErrorCode: "malformed_open_id",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const unionId = hit.unionId ?? null;
|
|
159
|
+
if (unionId !== null && !UNION_ID_RE.test(unionId)) {
|
|
160
|
+
return {
|
|
161
|
+
userId,
|
|
162
|
+
resolveStatus: "lookup_failed",
|
|
163
|
+
lastErrorCode: "malformed_union_id",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
userId,
|
|
168
|
+
resolveStatus: "resolved",
|
|
169
|
+
openId,
|
|
170
|
+
...(unionId === null ? {} : { unionId }),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 「这次同步是不是把大批既有映射抹掉了」。
|
|
175
|
+
*
|
|
176
|
+
* 单独抽出来是为了能被测试锁住:这是唯一挡在「登错飞书租户 → 全员 not_found →
|
|
177
|
+
* 一次 --apply 清空所有映射」前面的判据。注意 `lookup_failed` **不算**回归 ——
|
|
178
|
+
* 它根本不会清空 open_id。
|
|
179
|
+
*/
|
|
180
|
+
export function evaluateMassNotFound(previouslyResolved, entries, ratio = MASS_NOT_FOUND_RATIO) {
|
|
181
|
+
const regressions = entries.filter((entry) => entry.resolveStatus === "not_found" &&
|
|
182
|
+
previouslyResolved.has(entry.userId)).length;
|
|
183
|
+
return {
|
|
184
|
+
regressions,
|
|
185
|
+
massNotFound: previouslyResolved.size > 0 &&
|
|
186
|
+
regressions / previouslyResolved.size > ratio,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function buildQuery(params) {
|
|
190
|
+
const search = new URLSearchParams();
|
|
191
|
+
for (const [key, value] of Object.entries(params)) {
|
|
192
|
+
if (value !== undefined && value !== "")
|
|
193
|
+
search.set(key, value);
|
|
194
|
+
}
|
|
195
|
+
const rendered = search.toString();
|
|
196
|
+
return rendered ? `?${rendered}` : "";
|
|
197
|
+
}
|
|
198
|
+
export function registerFeishuIdentityCommands(program) {
|
|
199
|
+
const cmd = program
|
|
200
|
+
.command("feishu-identities")
|
|
201
|
+
.description("Hub 用户 ↔ 飞书 open_id 映射(运维同步面)");
|
|
202
|
+
cmd
|
|
203
|
+
.command("list")
|
|
204
|
+
.description("GET /v1/system/feishu-identities —— 含从未同步过的用户(resolveStatus=null)")
|
|
205
|
+
.option("--app-id <appId>", "只看某个飞书应用查出来的映射(默认不过滤)")
|
|
206
|
+
.option("--provider <provider>", "默认 feishu")
|
|
207
|
+
.option("--status <status>", "resolved | not_found | lookup_failed | unmapped")
|
|
208
|
+
.option("--include-inactive-users", "含已停用用户", false)
|
|
209
|
+
.action(async (opts) => {
|
|
210
|
+
await withPermissionGates({
|
|
211
|
+
command: "feishu-identities list",
|
|
212
|
+
gates: [{ resource: "systemMember", action: "list" }],
|
|
213
|
+
}, async () => {
|
|
214
|
+
const res = await hubRequest(`/v1/system/feishu-identities${buildQuery({
|
|
215
|
+
appId: opts.appId,
|
|
216
|
+
provider: opts.provider,
|
|
217
|
+
status: opts.status,
|
|
218
|
+
includeInactiveUsers: opts.includeInactiveUsers
|
|
219
|
+
? "true"
|
|
220
|
+
: undefined,
|
|
221
|
+
})}`);
|
|
222
|
+
console.log(JSON.stringify(res, null, 2));
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
cmd
|
|
226
|
+
.command("sync")
|
|
227
|
+
.description("本机 lark-cli 查 open_id → PUT /v1/system/feishu-identities 落库(不需要 DATABASE_URL)")
|
|
228
|
+
.requiredOption("--app-id <appId>", "查询用的飞书应用 id(溯源:记录这批 open_id 从哪查来)")
|
|
229
|
+
.option("--domain <domain>", "只同步该邮箱域的用户", "cyberklick.com")
|
|
230
|
+
.option("--dry-run", "预览,不写库", false)
|
|
231
|
+
.option("--apply", "执行写入", false)
|
|
232
|
+
.option("--force-mass-not-found", "允许「超过一半既有 resolved 变 not_found」的写入(查错租户时正是这个形态)", false)
|
|
233
|
+
.action(async (opts) => {
|
|
234
|
+
await withPermissionGates({
|
|
235
|
+
command: "feishu-identities sync",
|
|
236
|
+
gates: [
|
|
237
|
+
{ resource: "systemMember", action: "list" },
|
|
238
|
+
{ resource: "systemMember", action: "update" },
|
|
239
|
+
],
|
|
240
|
+
// 写入面只认交互式登录:API key 一律拒(服务端也会拒,这里提前给人话)。
|
|
241
|
+
userOnly: true,
|
|
242
|
+
}, async () => {
|
|
243
|
+
const domain = String(opts.domain).toLowerCase();
|
|
244
|
+
// 一次拿到「用户名册 + 当前映射状态」:这个端点本身就是
|
|
245
|
+
// users LEFT JOIN identities,不必再分页拉 /v1/system/users。
|
|
246
|
+
const current = await hubRequest(`/v1/system/feishu-identities${buildQuery({ appId: opts.appId })}`);
|
|
247
|
+
const targets = current.items.filter((item) => item.email.toLowerCase().endsWith(`@${domain}`));
|
|
248
|
+
const previouslyResolved = new Set(targets
|
|
249
|
+
.filter((item) => item.resolveStatus === "resolved")
|
|
250
|
+
.map((item) => item.userId));
|
|
251
|
+
const entries = [];
|
|
252
|
+
const report = {
|
|
253
|
+
domain,
|
|
254
|
+
appId: opts.appId,
|
|
255
|
+
candidates: targets.length,
|
|
256
|
+
resolved: 0,
|
|
257
|
+
notFound: 0,
|
|
258
|
+
lookupFailed: 0,
|
|
259
|
+
notFoundDetail: [],
|
|
260
|
+
lookupFailedDetail: [],
|
|
261
|
+
};
|
|
262
|
+
for (let i = 0; i < targets.length; i += LOOKUP_BATCH_SIZE) {
|
|
263
|
+
const batch = targets.slice(i, i + LOOKUP_BATCH_SIZE);
|
|
264
|
+
let byEmail = null;
|
|
265
|
+
let errorCode = "lookup_failed";
|
|
266
|
+
try {
|
|
267
|
+
byEmail = await lookupBatch(batch.map((item) => item.email));
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
errorCode =
|
|
271
|
+
error instanceof LookupError ? error.code : "lookup_failed";
|
|
272
|
+
console.error(JSON.stringify({
|
|
273
|
+
batch: Math.floor(i / LOOKUP_BATCH_SIZE),
|
|
274
|
+
error: error instanceof Error ? error.message : String(error),
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
for (const item of batch) {
|
|
278
|
+
if (byEmail === null) {
|
|
279
|
+
// 整批查询失败 —— 这是「不知道」,绝不能写成 not_found。
|
|
280
|
+
entries.push({
|
|
281
|
+
userId: item.userId,
|
|
282
|
+
resolveStatus: "lookup_failed",
|
|
283
|
+
lastErrorCode: errorCode,
|
|
284
|
+
});
|
|
285
|
+
report.lookupFailed += 1;
|
|
286
|
+
report.lookupFailedDetail.push(`${item.email} (${errorCode})`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const entry = classifyHit(item.userId, byEmail.get(item.email.toLowerCase()));
|
|
290
|
+
entries.push(entry);
|
|
291
|
+
if (entry.resolveStatus === "resolved") {
|
|
292
|
+
report.resolved += 1;
|
|
293
|
+
}
|
|
294
|
+
else if (entry.resolveStatus === "not_found") {
|
|
295
|
+
report.notFound += 1;
|
|
296
|
+
report.notFoundDetail.push(item.email);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
report.lookupFailed += 1;
|
|
300
|
+
report.lookupFailedDetail.push(`${item.email} (${entry.lastErrorCode})`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// 防「查错租户 → 一次抹掉全部映射」:既有 resolved 大面积变 not_found
|
|
305
|
+
// 时停机。这个形态与「公司集体离职」不可区分,所以要人来拍板。
|
|
306
|
+
const { regressions, massNotFound } = evaluateMassNotFound(previouslyResolved, entries);
|
|
307
|
+
console.log(JSON.stringify({
|
|
308
|
+
...report,
|
|
309
|
+
previouslyResolved: previouslyResolved.size,
|
|
310
|
+
resolvedBecameNotFound: regressions,
|
|
311
|
+
massNotFound,
|
|
312
|
+
}, null, 2));
|
|
313
|
+
if (massNotFound && !opts.forceMassNotFound) {
|
|
314
|
+
console.error(`Refusing to write: ${regressions}/${previouslyResolved.size} 个此前已解析的用户这次变成 not_found。` +
|
|
315
|
+
"这通常意味着查询用的是另一个飞书租户/授权失效,而不是这些人真的离职了。" +
|
|
316
|
+
"确认无误后加 --force-mass-not-found。");
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
if (entries.length === 0) {
|
|
320
|
+
console.log("没有待写入的条目。");
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!assertApplyOrDryRun(opts, {
|
|
324
|
+
command: "feishu-identities sync",
|
|
325
|
+
danger: "write",
|
|
326
|
+
summary: {
|
|
327
|
+
appId: opts.appId,
|
|
328
|
+
resolved: report.resolved,
|
|
329
|
+
notFound: report.notFound,
|
|
330
|
+
lookupFailed: report.lookupFailed,
|
|
331
|
+
},
|
|
332
|
+
})) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
for (let i = 0; i < entries.length; i += UPSERT_BATCH_SIZE) {
|
|
336
|
+
const chunk = entries.slice(i, i + UPSERT_BATCH_SIZE);
|
|
337
|
+
const res = await hubRequest("/v1/system/feishu-identities", {
|
|
338
|
+
method: "PUT",
|
|
339
|
+
body: { appId: opts.appId, entries: chunk },
|
|
340
|
+
});
|
|
341
|
+
console.log(JSON.stringify(res, null, 2));
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
//# sourceMappingURL=register-feishu-identities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-feishu-identities.js","sourceRoot":"","sources":["../src/register-feishu-identities.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-feishu-identities.test.d.ts","sourceRoot":"","sources":["../src/register-feishu-identities.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { classifyHit, evaluateMassNotFound, } from "./register-feishu-identities.js";
|
|
3
|
+
/**
|
|
4
|
+
* `not_found` 会**清空**已有的 open_id(离职语义)。如果 lark-cli 挂在另一个飞书
|
|
5
|
+
* 租户的授权上,全体查不到,一次 `--apply` 就能抹掉所有映射 —— 而这与「公司集体
|
|
6
|
+
* 离职」在数据上不可区分。这道判据是唯一挡在中间的东西,必须被锁住。
|
|
7
|
+
*/
|
|
8
|
+
describe("evaluateMassNotFound", () => {
|
|
9
|
+
const previously = new Set(["u1", "u2", "u3", "u4"]);
|
|
10
|
+
it("全部既有 resolved 变 not_found —— 停机", () => {
|
|
11
|
+
const result = evaluateMassNotFound(previously, [...previously].map((userId) => ({
|
|
12
|
+
userId,
|
|
13
|
+
resolveStatus: "not_found",
|
|
14
|
+
})));
|
|
15
|
+
expect(result).toEqual({ regressions: 4, massNotFound: true });
|
|
16
|
+
});
|
|
17
|
+
it("只掉一个人(真的有人离职)—— 放行", () => {
|
|
18
|
+
const result = evaluateMassNotFound(previously, [
|
|
19
|
+
{ userId: "u1", resolveStatus: "not_found" },
|
|
20
|
+
{ userId: "u2", resolveStatus: "resolved", openId: "ou_a" },
|
|
21
|
+
{ userId: "u3", resolveStatus: "resolved", openId: "ou_b" },
|
|
22
|
+
{ userId: "u4", resolveStatus: "resolved", openId: "ou_c" },
|
|
23
|
+
]);
|
|
24
|
+
expect(result).toEqual({ regressions: 1, massNotFound: false });
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* `lookup_failed` 不清空 open_id,所以「整批查询失败」不该触发这道闸
|
|
28
|
+
* —— 否则真正危险的信号会被日常噪音淹没。
|
|
29
|
+
*/
|
|
30
|
+
it("整批 lookup_failed 不算回归", () => {
|
|
31
|
+
const result = evaluateMassNotFound(previously, [...previously].map((userId) => ({
|
|
32
|
+
userId,
|
|
33
|
+
resolveStatus: "lookup_failed",
|
|
34
|
+
lastErrorCode: "token_expired",
|
|
35
|
+
})));
|
|
36
|
+
expect(result).toEqual({ regressions: 0, massNotFound: false });
|
|
37
|
+
});
|
|
38
|
+
it("此前一个都没映射上时不停机(首次同步)", () => {
|
|
39
|
+
const result = evaluateMassNotFound(new Set(), [
|
|
40
|
+
{ userId: "u1", resolveStatus: "not_found" },
|
|
41
|
+
{ userId: "u2", resolveStatus: "not_found" },
|
|
42
|
+
]);
|
|
43
|
+
expect(result).toEqual({ regressions: 0, massNotFound: false });
|
|
44
|
+
});
|
|
45
|
+
it("恰好一半不触发,过半才触发(边界)", () => {
|
|
46
|
+
const half = evaluateMassNotFound(new Set(["u1", "u2"]), [
|
|
47
|
+
{ userId: "u1", resolveStatus: "not_found" },
|
|
48
|
+
{ userId: "u2", resolveStatus: "resolved", openId: "ou_a" },
|
|
49
|
+
]);
|
|
50
|
+
expect(half.massNotFound).toBe(false);
|
|
51
|
+
const overHalf = evaluateMassNotFound(new Set(["u1", "u2", "u3"]), [
|
|
52
|
+
{ userId: "u1", resolveStatus: "not_found" },
|
|
53
|
+
{ userId: "u2", resolveStatus: "not_found" },
|
|
54
|
+
{ userId: "u3", resolveStatus: "resolved", openId: "ou_a" },
|
|
55
|
+
]);
|
|
56
|
+
expect(overHalf.massNotFound).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it("不在既有 resolved 名单里的 not_found 不算回归(本来就没映射)", () => {
|
|
59
|
+
const result = evaluateMassNotFound(new Set(["u1"]), [
|
|
60
|
+
{ userId: "u9", resolveStatus: "not_found" },
|
|
61
|
+
{ userId: "u8", resolveStatus: "not_found" },
|
|
62
|
+
{ userId: "u1", resolveStatus: "resolved", openId: "ou_a" },
|
|
63
|
+
]);
|
|
64
|
+
expect(result).toEqual({ regressions: 0, massNotFound: false });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* 分类判据。只有「飞书压根没返回这个邮箱」才允许写 `not_found`
|
|
69
|
+
* —— 那一条会**清空**既有 open_id。其余"没拿到可用答案"必须是 `lookup_failed`。
|
|
70
|
+
*/
|
|
71
|
+
describe("classifyHit", () => {
|
|
72
|
+
it("飞书静默省略该邮箱 → not_found(真的不在通讯录)", () => {
|
|
73
|
+
expect(classifyHit("u1", undefined)).toEqual({
|
|
74
|
+
userId: "u1",
|
|
75
|
+
resolveStatus: "not_found",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
it("命中了人但没有 open_id → lookup_failed,不清空既有映射", () => {
|
|
79
|
+
expect(classifyHit("u1", {})).toEqual({
|
|
80
|
+
userId: "u1",
|
|
81
|
+
resolveStatus: "lookup_failed",
|
|
82
|
+
lastErrorCode: "missing_open_id",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
it("一个邮箱命中多个不同的人 → lookup_failed(歧义,不能挑一个)", () => {
|
|
86
|
+
expect(classifyHit("u1", { ambiguous: true })).toEqual({
|
|
87
|
+
userId: "u1",
|
|
88
|
+
resolveStatus: "lookup_failed",
|
|
89
|
+
lastErrorCode: "ambiguous_match",
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
it.each([
|
|
93
|
+
["注入串", "ou_x></at><at user_id=all"],
|
|
94
|
+
["缺前缀", "abc123"],
|
|
95
|
+
["带引号", 'ou_abc"'],
|
|
96
|
+
])("open_id 畸形(%s) → lookup_failed 而不是写进库", (_label, openId) => {
|
|
97
|
+
expect(classifyHit("u1", { openId })).toMatchObject({
|
|
98
|
+
resolveStatus: "lookup_failed",
|
|
99
|
+
lastErrorCode: "malformed_open_id",
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it("union_id 畸形 → lookup_failed(不退化成只写 open_id)", () => {
|
|
103
|
+
expect(classifyHit("u1", { openId: "ou_ok1", unionId: "on_x></at>" })).toMatchObject({
|
|
104
|
+
resolveStatus: "lookup_failed",
|
|
105
|
+
lastErrorCode: "malformed_union_id",
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
it("合法命中 → resolved", () => {
|
|
109
|
+
expect(classifyHit("u1", { openId: "ou_ok1", unionId: "on_ok1" })).toEqual({
|
|
110
|
+
userId: "u1",
|
|
111
|
+
resolveStatus: "resolved",
|
|
112
|
+
openId: "ou_ok1",
|
|
113
|
+
unionId: "on_ok1",
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
it("没有 union_id 也能 resolved(该字段可缺)", () => {
|
|
117
|
+
expect(classifyHit("u1", { openId: "ou_ok1" })).toEqual({
|
|
118
|
+
userId: "u1",
|
|
119
|
+
resolveStatus: "resolved",
|
|
120
|
+
openId: "ou_ok1",
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
//# sourceMappingURL=register-feishu-identities.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-feishu-identities.test.js","sourceRoot":"","sources":["../src/register-feishu-identities.test.ts"],"names":[],"mappings":"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"}
|
package/dist/register-notify.js
CHANGED
|
@@ -1336,6 +1336,33 @@ function registerSourceConfigCommands(notify) {
|
|
|
1336
1336
|
printJson(await requireClient().activateNotificationSourceConfig(teamId, opts.source, opts.id, expectedActive === undefined ? {} : { expectedActive }));
|
|
1337
1337
|
});
|
|
1338
1338
|
});
|
|
1339
|
+
cfg
|
|
1340
|
+
.command("narrow-history")
|
|
1341
|
+
.description("POST .../configs/:fromConfigId/narrow-history — 保留 seq/游标,净化旧事件字段并迁移未触发 schedule(admin only)")
|
|
1342
|
+
.option("-t, --team <teamId>", "Team UUID(缺省用当前 context)")
|
|
1343
|
+
.requiredOption("--source <sourceId>", "sourceId")
|
|
1344
|
+
.requiredOption("--from-config <uuid>", "需要净化的旧 config version id")
|
|
1345
|
+
.requiredOption("--target-config <uuid>", "已激活、字段为旧版子集的目标 config version id")
|
|
1346
|
+
.option("--expect-selection-hash <sha256>", "--apply 时必填:上一轮服务端 dry-run 返回的 selectionHash")
|
|
1347
|
+
.option("--apply", "执行净化;缺省为服务端真实 dry-run(会校验配置、事件和 schedule,但不写数据)")
|
|
1348
|
+
.action(async (opts) => {
|
|
1349
|
+
await runNotify("admin", async () => {
|
|
1350
|
+
const teamId = resolveTeamId(opts.team);
|
|
1351
|
+
const apply = opts.apply === true;
|
|
1352
|
+
const expectedSelectionHash = opts.expectSelectionHash;
|
|
1353
|
+
if (apply && !expectedSelectionHash) {
|
|
1354
|
+
fail("--apply 必须同时给 --expect-selection-hash <上一轮 dry-run 的 selectionHash>。");
|
|
1355
|
+
}
|
|
1356
|
+
if (!apply && expectedSelectionHash) {
|
|
1357
|
+
fail("--expect-selection-hash 只可与 --apply 一起使用。");
|
|
1358
|
+
}
|
|
1359
|
+
printJson(await requireClient().narrowNotificationHistory(teamId, opts.source, opts.fromConfig, {
|
|
1360
|
+
targetConfigId: opts.targetConfig,
|
|
1361
|
+
apply,
|
|
1362
|
+
...(expectedSelectionHash ? { expectedSelectionHash } : {}),
|
|
1363
|
+
}));
|
|
1364
|
+
});
|
|
1365
|
+
});
|
|
1339
1366
|
cfg
|
|
1340
1367
|
.command("set-enabled")
|
|
1341
1368
|
.description("PUT .../notification-sources/:sourceId/enabled — 实时 kill switch(影响全部在途 schedule;不 bump sourceConfigVersion)")
|