@geoly-ai/social-hub-cli 0.3.41 → 0.3.43
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 +12 -0
- package/dist/cmd-manifest.json +5 -2
- package/dist/cmd-manifest.test.js +10 -0
- package/dist/cmd-manifest.test.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +116 -5
- package/dist/index.js.map +1 -1
- package/dist/watchlists-list-filters.test.d.ts +2 -0
- package/dist/watchlists-list-filters.test.d.ts.map +1 -0
- package/dist/watchlists-list-filters.test.js +143 -0
- package/dist/watchlists-list-filters.test.js.map +1 -0
- package/package.json +2 -2
- package/skills/integrity.json +2 -2
- package/skills/manifest.json +1 -1
- package/skills/social-hub-intelligence/SKILL.md +119 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watchlists-list-filters.test.d.ts","sourceRoot":"","sources":["../src/watchlists-list-filters.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 五层同步补口:`intel watchlists-list` 的服务端筛选参数。
|
|
3
|
+
*
|
|
4
|
+
* 服务端(contracts/db/api)与 SDK 在 cf63e6b4 已经支持
|
|
5
|
+
* subredditQuery / tag / monitorState / sortBy / sortDir,并顺带补上此前漏传的
|
|
6
|
+
* status / brandId / industryKeys —— 缺的只有 CLI 这一层。
|
|
7
|
+
*
|
|
8
|
+
* 🔴 本文件跑的是**真实 handler**,不是「断言选项已注册」。
|
|
9
|
+
* 只断言注册挡不住本任务最可能的静默失败形状:**解析了但没往下传**
|
|
10
|
+
* (`.option()` 写了、`listSubredditWatchlists({...})` 里忘了那一行)——
|
|
11
|
+
* 那种情况下 `--help` 看着完全正常、命令也不报错,只是筛选悄悄没生效,
|
|
12
|
+
* 返回的是**未筛选的全量**,读起来完全可信。
|
|
13
|
+
*
|
|
14
|
+
* 🔴 monitorState 是**三值**不是布尔。软停语义:
|
|
15
|
+
* · disabled = 加过监听后停了(配置行还在)
|
|
16
|
+
* · never = 压根没加过
|
|
17
|
+
* 用户提这个需求要的正是这个区别,所以这里除了「三个值都能透传」之外,
|
|
18
|
+
* 还钉死「布尔值被拒且错误信息讲清三值」——防的是后人把它简化成 `--monitored`。
|
|
19
|
+
*/
|
|
20
|
+
import { beforeAll, afterEach, describe, expect, it, vi } from "vitest";
|
|
21
|
+
const listSubredditWatchlists = vi
|
|
22
|
+
.fn()
|
|
23
|
+
.mockResolvedValue({ items: [], total: 0 });
|
|
24
|
+
vi.mock("@geoly-ai/social-hub-sdk", () => ({
|
|
25
|
+
SocialHubClient: vi.fn(),
|
|
26
|
+
getSocialHubHealth: vi.fn(),
|
|
27
|
+
}));
|
|
28
|
+
vi.mock("./client.js", () => ({
|
|
29
|
+
requireClient: () => ({ listSubredditWatchlists }),
|
|
30
|
+
resolveTeamId: (t) => t ?? "team-1",
|
|
31
|
+
getBaseUrl: () => "http://localhost",
|
|
32
|
+
}));
|
|
33
|
+
let program;
|
|
34
|
+
let cmd;
|
|
35
|
+
/**
|
|
36
|
+
* ⚠️ commander 把上一次 parse 的 option 值**留在同一个 command 实例上**,
|
|
37
|
+
* 而真实 CLI 每次都是新进程。不清的话「不传 --subreddit-query」会读到上一个
|
|
38
|
+
* 用例残留的值 —— 那既不是真实行为,也会让用例顺序变成隐性依赖
|
|
39
|
+
* (`accounts-list-offset.test.ts` 是靠「把两次 parse 塞进同一个用例、不传的放前面」
|
|
40
|
+
* 绕开的;这里参数多,绕不动,直接清)。
|
|
41
|
+
*/
|
|
42
|
+
const resetOpts = () => {
|
|
43
|
+
for (const opt of cmd.options)
|
|
44
|
+
cmd.setOptionValue(opt.attributeName(), opt.defaultValue);
|
|
45
|
+
};
|
|
46
|
+
const run = (...args) => {
|
|
47
|
+
resetOpts();
|
|
48
|
+
return program.parseAsync([
|
|
49
|
+
"node",
|
|
50
|
+
"social-hub",
|
|
51
|
+
"intelligence",
|
|
52
|
+
"watchlists-list",
|
|
53
|
+
...args,
|
|
54
|
+
]);
|
|
55
|
+
};
|
|
56
|
+
/** 最后一次调用收到的参数对象。 */
|
|
57
|
+
const lastArgs = () => listSubredditWatchlists.mock.calls.at(-1)?.[0];
|
|
58
|
+
beforeAll(async () => {
|
|
59
|
+
program = (await import("./index.js")).program;
|
|
60
|
+
const intel = program.commands.find((c) => c.name() === "intelligence");
|
|
61
|
+
const found = intel?.commands.find((c) => c.name() === "watchlists-list");
|
|
62
|
+
// 正向对照:命令真的存在。找不到时下面 resetOpts 会静默变成空循环,
|
|
63
|
+
// 于是整份用例退化成「跑了个不存在的命令」而依然可能全绿。
|
|
64
|
+
if (!found)
|
|
65
|
+
throw new Error("intelligence watchlists-list 命令没找到");
|
|
66
|
+
cmd = found;
|
|
67
|
+
vi.spyOn(console, "log").mockImplementation(() => { });
|
|
68
|
+
}, 30000);
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
listSubredditWatchlists.mockClear();
|
|
71
|
+
});
|
|
72
|
+
describe("intelligence watchlists-list 服务端筛选参数", () => {
|
|
73
|
+
it("🔴 每个筛选参数都真的传到了 SDK;不传时不发送(保持服务端默认)", async () => {
|
|
74
|
+
await run();
|
|
75
|
+
// 正向对照:handler 确实跑到了、确实带着默认分页调了 SDK。
|
|
76
|
+
// 没有这一条的话,下面一串 toBeUndefined() 在「命令压根没执行」时也全绿。
|
|
77
|
+
expect(listSubredditWatchlists).toHaveBeenCalledTimes(1);
|
|
78
|
+
expect(lastArgs()).toMatchObject({ limit: 100, offset: 0 });
|
|
79
|
+
for (const key of [
|
|
80
|
+
"subredditQuery",
|
|
81
|
+
"status",
|
|
82
|
+
"brandId",
|
|
83
|
+
"industryKeys",
|
|
84
|
+
"tag",
|
|
85
|
+
"monitorState",
|
|
86
|
+
"sortBy",
|
|
87
|
+
"sortDir",
|
|
88
|
+
])
|
|
89
|
+
expect(lastArgs()[key], `${key} 不传时不应发送`).toBeUndefined();
|
|
90
|
+
await run("--subreddit-query", "garden", "--status", "active", "--brand", "11111111-1111-4111-8111-111111111111", "--industry-keys", "home,garden", "--tag", "seed", "--monitor-state", "disabled", "--sort-by", "monitorLastObservedAt", "--sort-dir", "asc", "--limit", "25", "--offset", "50");
|
|
91
|
+
expect(listSubredditWatchlists).toHaveBeenCalledTimes(2);
|
|
92
|
+
expect(lastArgs()).toMatchObject({
|
|
93
|
+
subredditQuery: "garden",
|
|
94
|
+
status: "active",
|
|
95
|
+
brandId: "11111111-1111-4111-8111-111111111111",
|
|
96
|
+
industryKeys: "home,garden",
|
|
97
|
+
tag: "seed",
|
|
98
|
+
monitorState: "disabled",
|
|
99
|
+
sortBy: "monitorLastObservedAt",
|
|
100
|
+
sortDir: "asc",
|
|
101
|
+
limit: 25,
|
|
102
|
+
offset: 50,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it("--subreddit(精确)与 --subreddit-query(子串)是两个独立谓词,不许互相顶替", async () => {
|
|
106
|
+
await run("--subreddit", "r/gardening");
|
|
107
|
+
expect(lastArgs()).toMatchObject({ subreddit: "r/gardening" });
|
|
108
|
+
// 变异形状:把 subredditQuery 接到 opts.subreddit 上(或反过来)。
|
|
109
|
+
expect(lastArgs().subredditQuery).toBeUndefined();
|
|
110
|
+
await run("--subreddit-query", "garden");
|
|
111
|
+
expect(lastArgs()).toMatchObject({ subredditQuery: "garden" });
|
|
112
|
+
expect(lastArgs().subreddit).toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
describe("🔴 monitorState 三值(⛔ 不是布尔)", () => {
|
|
115
|
+
it.each(["enabled", "disabled", "never"])("%s 原样透传为字符串", async (state) => {
|
|
116
|
+
await run("--monitor-state", state);
|
|
117
|
+
expect(lastArgs().monitorState).toBe(state);
|
|
118
|
+
// 变异形状:`monitorState: opts.monitorState === "enabled"` 这类布尔退化。
|
|
119
|
+
// 只写 toBe(state) 也能抓到,这条是把「类型」这层意图写死,防后人改成布尔。
|
|
120
|
+
expect(typeof lastArgs().monitorState).toBe("string");
|
|
121
|
+
});
|
|
122
|
+
it.each(["true", "false", "1", "monitored", ""])("⛔ 布尔式/拼错的值 %j 直接报错,不静默落到某一档", async (bad) => {
|
|
123
|
+
await expect(run("--monitor-state", bad)).rejects.toThrow(/--monitor-state/);
|
|
124
|
+
// 正向对照:报错是「拒绝了」,不是「转发了一个坏值」。
|
|
125
|
+
expect(listSubredditWatchlists).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
it("错误信息必须讲清 disabled 与 never 的区别(不然读者仍会以为是布尔)", async () => {
|
|
128
|
+
// parseAsync 成功时 resolve 的是 Command,所以这里显式把两种结局收成 unknown,
|
|
129
|
+
// 再由下面的 toBeInstanceOf 判定 —— ⛔ 不要直接 `as Error`,那会让
|
|
130
|
+
// 「命令没报错」这种情况静默走到 err.message 上(undefined.contains 反而报别的错)。
|
|
131
|
+
const err = await run("--monitor-state", "true").then((ok) => ok, (e) => e);
|
|
132
|
+
// 正向对照:确实拿到了一个 Error(而不是命令压根没报错、下面对 undefined 断言)。
|
|
133
|
+
expect(err).toBeInstanceOf(Error);
|
|
134
|
+
const message = err.message;
|
|
135
|
+
for (const token of ["enabled", "disabled", "never"])
|
|
136
|
+
expect(message).toContain(token);
|
|
137
|
+
// 光列三个词还不够 —— 必须说清「停了的」和「从没加过」不是一回事。
|
|
138
|
+
expect(message).toMatch(/软停|配置行/);
|
|
139
|
+
expect(message).toMatch(/没加过|新建配置行/);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
//# sourceMappingURL=watchlists-list-filters.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watchlists-list-filters.test.js","sourceRoot":"","sources":["../src/watchlists-list-filters.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.43",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "social-hub CLI for Social Ops Hub",
|
|
6
6
|
"repository": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"commander": "^12.1.0",
|
|
25
25
|
"@geoly-ai/social-hub-authz": "0.0.20",
|
|
26
|
-
"@geoly-ai/social-hub-sdk": "0.1.
|
|
26
|
+
"@geoly-ai/social-hub-sdk": "0.1.16"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^22.10.2",
|
package/skills/integrity.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"fileCount": 1656,
|
|
5
5
|
"files": {
|
|
6
6
|
"README.md": "5246d4ae2712e3e656b101041929183d7ae7f8ea487dacf5854187826ab7c335",
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "aa878b3b0abb3e120e88d54f293f7ca30157e9fd4467ce26e148976d26e31952",
|
|
8
8
|
"reddit-answers-research/SKILL.md": "ab6fd12eeb2fef4dc31a6f3c21a84810d6b5bdb309f130bcdc9eb902f075294a",
|
|
9
9
|
"reddit-answers-research/references/browser-playbook.md": "a8f61d3d43ca93d46dadc8a092a515ddcd32df12fb1d08d334cce2e02709d915",
|
|
10
10
|
"reddit-answers-research/references/report-template.md": "9aa9acf555ba8b2ca503bdbdf5750a5f53c66e19989bf6807fa6200c7317e306",
|
|
@@ -1643,7 +1643,7 @@
|
|
|
1643
1643
|
"social-hub-content-review/SKILL.md": "6d10a84ab56552fd953863b97978bddc0e925e3a2667d64a7a4c51e33a3ddb58",
|
|
1644
1644
|
"social-hub-events-observability/SKILL.md": "f4c17487c64bb1f48381d8d60e97cfa160a94a0f120153b1c0e798a0c7ee7ebe",
|
|
1645
1645
|
"social-hub-graph-compliance/SKILL.md": "2270aeb321733f0318f75da4838ca1ca03996c6cac59e5a12f7152bd1ecdfa26",
|
|
1646
|
-
"social-hub-intelligence/SKILL.md": "
|
|
1646
|
+
"social-hub-intelligence/SKILL.md": "69f4a3d97b36e13f8ee594708f84e268996559ace86982194dbc23d353cc2923",
|
|
1647
1647
|
"social-hub-migration/SKILL.md": "1c13f231bb81ae8d242c834eec93a499830481804c66f6dba95922972f56f9ce",
|
|
1648
1648
|
"social-hub-notifications/SKILL.md": "f80f4cf2f9ef0dbf60bd6e8bb4ad7e27dc0ff1fe59f4c7e79e49cc691ed4be6a",
|
|
1649
1649
|
"social-hub-openclaw-context/SKILL.md": "97c48f38f1f1a206c3af2af49fcd445a1cd5b78d4a9c805c2456de4ce98fec09",
|
package/skills/manifest.json
CHANGED
|
@@ -197,7 +197,12 @@ installed_apps 7 天。
|
|
|
197
197
|
## Watchlists
|
|
198
198
|
|
|
199
199
|
```bash
|
|
200
|
-
social-hub intelligence watchlists-list [--campaign <uuid>]
|
|
200
|
+
social-hub intelligence watchlists-list [--campaign <uuid>] [--subreddit <name>] \
|
|
201
|
+
[--subreddit-query <text>] [--status active|paused|archived] [--brand <uuid>] \
|
|
202
|
+
[--industry-keys <k1,k2>] [--tag <tag>] \
|
|
203
|
+
[--monitor-state enabled|disabled|never] \
|
|
204
|
+
[--sort-by createdAt|subreddit|monitorLastObservedAt] [--sort-dir asc|desc] \
|
|
205
|
+
[-n 100] [--offset 0]
|
|
201
206
|
social-hub intelligence watchlists-create -j '{"subreddit":"technology","tags":["core"]}'
|
|
202
207
|
# 品牌洞察(VOC/SOV)用的监控池:显式绑定品牌 + 指定主行业
|
|
203
208
|
social-hub intelligence watchlists-create -j '{"subreddit":"jewelry","primaryIndustryKey":"industry_pools__jewelry","brandIds":["<brand-uuid>"]}'
|
|
@@ -212,6 +217,40 @@ brand 池 > industry 池 > 其他,稳定排序;此前首匹配会把养号宠物
|
|
|
212
217
|
品牌 VOC/SOV 扫描取"全局 + 绑定当前品牌"的池。做品牌洞察(如 Aporro)时创建
|
|
213
218
|
watchlist 须显式传 `brandIds`。
|
|
214
219
|
|
|
220
|
+
**筛选一律在服务端**(分页是 `limit/offset` + `total`)。⛔ 不要"拉一页回来自己过滤":
|
|
221
|
+
那只过滤到**当前页**,筛完看到 3 条会以为总共就 3 条。板块多时用 `--offset` 翻页。
|
|
222
|
+
|
|
223
|
+
**`--subreddit` 与 `--subreddit-query` 是两个谓词,别混**:前者**精确**匹配(板块详情
|
|
224
|
+
聚合视图靠它拿到确定的那一行),后者是**子串**搜索(给"我想找个板块加监听"用)。
|
|
225
|
+
两者都大小写不敏感、`r/` 前缀可省。`--tag` 是**精确包含**单个标签(不是子串)。
|
|
226
|
+
`--industry-keys` 逗号分隔可多个。`--brand` 只返回**显式绑定**该品牌的池,**不含全局池**
|
|
227
|
+
——想要"这个品牌实际会扫到的池",还得再查一次不带 `--brand` 的全局池并合并。
|
|
228
|
+
|
|
229
|
+
### 🔴 `--monitor-state` 是**三值**,不是布尔
|
|
230
|
+
|
|
231
|
+
停止监听是**软停**(`enabled=false`,配置行和观察 ledger 都还在),所以"没在监听"
|
|
232
|
+
在业务上是**两种截然不同**的状态,`--monitor-state` 必须分开表达:
|
|
233
|
+
|
|
234
|
+
| 值 | 含义 | 要做什么才能恢复监听 |
|
|
235
|
+
| ---------- | ---------------------- | ----------------------------------------------------------------- |
|
|
236
|
+
| `enabled` | 正在监听 | — |
|
|
237
|
+
| `disabled` | **加过监听、后来停了** | `monitors-enable` 直接重启;历史水位还在,不会重复报旧帖 |
|
|
238
|
+
| `never` | **压根没加过监听** | `monitors-enable` 会**新建**配置行;该板块历史帖会被当成新帖报一遍 |
|
|
239
|
+
|
|
240
|
+
⛔ 不要把它当成 `--monitored true/false` 用——CLI 会直接报错。想要"所有没在监听的",
|
|
241
|
+
得**分两次查**(`disabled` 一次、`never` 一次),因为对这两批要采取的动作不一样:
|
|
242
|
+
`disabled` 是"我停过它,要不要恢复";`never` 是"这个板块我还没碰过"。
|
|
243
|
+
⚠️ 服务端也没有"合并版"的布尔筛选可用,别去猜一个 `--monitored`。
|
|
244
|
+
|
|
245
|
+
**权限**:`--monitor-state` 与 `--sort-by monitorLastObservedAt` 属**系统级运维配置**面,
|
|
246
|
+
受限主体(client)传任一个都会 **403**;同一道门也把 `monitorLastObservedAt` 字段从
|
|
247
|
+
受限主体的响应里摘掉了。排序也算旁路——按它排序等于把"在监听的"全顶到前面。
|
|
248
|
+
|
|
249
|
+
**排序**:`--sort-by createdAt|subreddit|monitorLastObservedAt`(缺省 `createdAt`)。
|
|
250
|
+
⚠️ 不传 `--sort-dir` 时方向**随排序键而变**:`subreddit` 缺省 `asc`(字母序),
|
|
251
|
+
其余缺省 `desc`(最新在前)。要稳定就显式传。
|
|
252
|
+
⚠️ 分页仍是 offset,排序键的值在翻页期间变化时理论上会跨页漂移(当前规模几十行,可接受)。
|
|
253
|
+
|
|
215
254
|
## 板块新帖监听(定时抓 `/new` → 飞书卡片 @ 负责人)
|
|
216
255
|
|
|
217
256
|
系统级(与 watchlist 同源,**无 `-t/--team`**)。cron 每 12h 一轮:抓已启用板块的
|
|
@@ -239,7 +278,8 @@ social-hub intelligence monitor-runs-list [--status degraded] [-n 20]
|
|
|
239
278
|
social-hub intelligence monitor-ruleset-create --monitor <uuid> --lookback 1440 \
|
|
240
279
|
--note "第一版:品牌词 + 竞品词" \
|
|
241
280
|
-j '[{"term":"aporro","scope":"both","matchMode":"word","weight":3},
|
|
242
|
-
{"term":"cold brew","scope":"body","matchMode":"phrase"}
|
|
281
|
+
{"term":"cold brew","scope":"body","matchMode":"phrase"},
|
|
282
|
+
{"term":"espresso martini","polarity":"exclude","matchMode":"phrase"}]'
|
|
243
283
|
|
|
244
284
|
# 2) 🔴 试跑:拿过去 24h 的历史帖子跑一遍,看会命中什么(只读,不发任何通知)
|
|
245
285
|
social-hub intelligence monitor-ruleset-dry-run --monitor <uuid> --rule-set <uuid> --lookback 1440
|
|
@@ -262,7 +302,8 @@ social-hub intelligence monitor-brand-keyword-candidates --monitor <uuid> [--bra
|
|
|
262
302
|
1. **active 之后不可原地修改,只能新建版本**(DB 触发器焊死)。改词的正确流程是
|
|
263
303
|
create draft → dry-run → activate,不是去改已激活的那一版。
|
|
264
304
|
2. **激活/停用不会重发历史命中**。观察的水位在被评估的那一轮就推进了,**无论命中与否** ——
|
|
265
|
-
|
|
305
|
+
否则调宽一次关键词,负责人就被历史帖子刷一次屏。要看「新规则放回历史窗口会怎样」,
|
|
306
|
+
用 `monitor-replay-run`(见下面「回放」)—— 它**同样不发通知**。
|
|
266
307
|
3. **停用 ≠ 静音**。retire 之后该监听回到「不设关键词门」,报告**恢复全量新帖**。
|
|
267
308
|
要减少推送就改规则,不要指望 retire。
|
|
268
309
|
4. **词边界**:`matchMode=word` 时 `art` **不会**命中 `earth`,但会命中 `art-house`。
|
|
@@ -282,9 +323,82 @@ social-hub intelligence monitor-brand-keyword-candidates --monitor <uuid> [--bra
|
|
|
282
323
|
watchlist 与品牌是多对多,若监听直接读品牌词并集,「给某品牌加一个词」或「把品牌绑到
|
|
283
324
|
这个 watchlist」都会让**监听范围悄然扩大**,而运营没做过任何监听侧的动作。
|
|
284
325
|
|
|
285
|
-
|
|
326
|
+
**卡片上「未发现新帖」要分五种读法**(卡片会自己区分,别再一律当成「没动静」):
|
|
286
327
|
覆盖完整且没被过滤 ⇒ 真的没新帖;板块疑似截断 / 拿不到分页游标 ⇒ **采集没抓全**,
|
|
287
|
-
|
|
328
|
+
按采集问题排查;全被**排除词**否掉 ⇒ 去看排除词是不是配宽了;
|
|
329
|
+
全都没命中 include 词 ⇒ 去看 include 词是不是配窄了;
|
|
330
|
+
正文补抓全部失败 ⇒ **采集问题**,关键词只在正文里的帖子这轮判不了。
|
|
331
|
+
🔴 后两组的处置方向**相反**(放宽 vs 收窄),所以卡片刻意分开表述,别当成同一句话。
|
|
332
|
+
|
|
333
|
+
### 排除词 / 布尔规则(0171)
|
|
334
|
+
|
|
335
|
+
规则对象加 `"polarity":"exclude"` 就是排除词(缺省 `include`)。
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
# espresso 收进来,但 espresso martini 排出去 —— 两条可以共存(唯一键含 polarity)
|
|
339
|
+
-j '[{"term":"espresso","scope":"both","matchMode":"word"},
|
|
340
|
+
{"term":"espresso martini","polarity":"exclude","matchMode":"phrase"}]'
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**四条口径**:
|
|
344
|
+
|
|
345
|
+
1. **exclude 只撤销,自己不产生命中**。所以**一条 include 都没有的规则集不许激活**
|
|
346
|
+
(409 `monitor_rule_set_exclude_only`)—— 那等于把整个板块静音,而卡片上看起来
|
|
347
|
+
只是「今天没有相关新帖」。
|
|
348
|
+
2. **撤销按来源优先级由低到高**:`manual`(30) > `brand_keyword`(20) > `expansion`(10)。
|
|
349
|
+
一条 exclude 只能撤销**优先级不高于它自己**的 include 命中 —— 近义词扩展出来的
|
|
350
|
+
否定词压不住运营亲手填的 include 词。同源的一对(都是 manual)**可以**互相作用。
|
|
351
|
+
3. **被排除掉 ≠ 没命中**。dry-run 与回放都把它们分成 `excluded` / `no_match` 两个数,
|
|
352
|
+
卡片上也是两句话。⛔ 别合并看:前者去看排除词是不是配宽了,后者去看 include 词
|
|
353
|
+
是不是配窄了 —— 方向相反。
|
|
354
|
+
4. **正文拿不到时排除判定也可能不完整**:有 `scope=body` 的排除词而正文没采到时,
|
|
355
|
+
帖子**照发**,但卡片标注「正文缺失,排除词未能完整判定(可能本应被排除)」。
|
|
356
|
+
取向与 include 侧一致:拿不准时不敢当成已排除,那会漏报。
|
|
357
|
+
|
|
358
|
+
⚠️ **加了排除词一定先 dry-run**。一条配宽的排除词能把整个板块吃掉,
|
|
359
|
+
而通知上看起来只是「今天没有相关新帖」。
|
|
360
|
+
|
|
361
|
+
### 回放:改完规则想看效果,别等下一轮 cron(0171)
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
# 把某版规则(缺省 active;可传 draft)放回指定历史窗口重判
|
|
365
|
+
social-hub intelligence monitor-replay-run --monitor <uuid> \
|
|
366
|
+
--from 2026-09-01T00:00:00Z --to 2026-09-08T00:00:00Z \
|
|
367
|
+
[--rule-set <draft-uuid>] [-n 500] [--note "第 3 版排除词效果"]
|
|
368
|
+
|
|
369
|
+
social-hub intelligence monitor-replays-list --monitor <uuid>
|
|
370
|
+
social-hub intelligence monitor-replay-get --monitor <uuid> --replay <uuid> \
|
|
371
|
+
[--decision matched|indeterminate|excluded|no_match]
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
🔴 **回放不写命中账本、不推进投递水位、不发任何通知**。它算的是「如果当时用这套规则
|
|
375
|
+
会怎样」这个**假设**,结果落在独立的回放表上 —— 原始命中账本(「当初按哪版规则、
|
|
376
|
+
因为什么命中」这个审计事实)一个字都不会被改。响应里的 `sideEffects` 显式声明了这三条。
|
|
377
|
+
|
|
378
|
+
**差分怎么读**(`diff`,回放的产品价值就在这里):
|
|
379
|
+
|
|
380
|
+
| 字段 | 含义 | 怎么用 |
|
|
381
|
+
| -------------------- | ------------------------------ | ----------------------------- |
|
|
382
|
+
| `wouldStopReporting` | 当初报过、新规则下不再报 | 排除词的收益;顺便看有没有误杀 |
|
|
383
|
+
| `wouldNewlyReport` | 当初**被筛掉**、新规则下会报 | 新加的 include 词的收益 |
|
|
384
|
+
| `neverObserved` | 当时**根本没被这个监听观察到** | ⛔ **不是**「规则漏了」 |
|
|
385
|
+
|
|
386
|
+
🔴 `neverObserved` 单列的理由:命中账本对「不命中」**一条都不写**,所以「当初没有账本行」
|
|
387
|
+
既可能是被规则筛掉、也可能是监听那时还没开(或这条帖子是 VOC / 回灌进库的)。
|
|
388
|
+
把两者合并成一句「原来没报」,会让「监听还没开」被读成「规则漏了」。
|
|
389
|
+
|
|
390
|
+
**与 dry-run 的分工**:dry-run 是**不落库**的即时预览、窗口只能取 720/1440;
|
|
391
|
+
回放**落库**、窗口任意(最长 30 天)、并给出上面那份差分。要拿去做上线决策的用回放。
|
|
392
|
+
|
|
393
|
+
### 正文补抓(0171)
|
|
394
|
+
|
|
395
|
+
listing 走渲染兜底时经常拿不到正文,采集端如实标 `unknown`,匹配侧据此判 `missing`。
|
|
396
|
+
0171 起,当该板块**确实有 `scope=body`/`both` 的规则**时,系统会对这些帖子回头补抓一次
|
|
397
|
+
(走既有的 Firecrawl 兜底链,每轮硬顶 12 条、看 deadline、失败不影响报告)。
|
|
398
|
+
|
|
399
|
+
卡片覆盖行会给出「候选 N 条 · 补到 M 条 · 仍拿不到 K 条」。
|
|
400
|
+
🔴 **只报成功数是不够的**:「12 条候选补了 0 条」与「根本没有候选」在通知上必须能分辨 ——
|
|
401
|
+
前者意味着关键词只出现在正文里的帖子这一轮判不了,是**采集问题**。
|
|
288
402
|
|
|
289
403
|
### 近义词扩展(0169)
|
|
290
404
|
|