@geoly-ai/social-hub-cli 0.2.19 → 0.2.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 +13 -0
- package/dist/cmd-manifest.json +3 -2
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +215 -0
- package/dist/index.js.map +1 -1
- package/dist/voc-acquire.test.d.ts +2 -0
- package/dist/voc-acquire.test.d.ts.map +1 -0
- package/dist/voc-acquire.test.js +243 -0
- package/dist/voc-acquire.test.js.map +1 -0
- package/package.json +2 -2
- package/skills/manifest.json +1 -1
- package/skills/social-hub-intelligence/SKILL.md +21 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to `@geoly-ai/social-hub-cli` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
Release workflow: `pnpm release:cli` → `pnpm publish:cli`.
|
|
6
6
|
|
|
7
|
+
## [0.2.21] - 2026-07-22
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 支持受限主体 + 维度派发前鉴权(板块情报收尾 B)
|
|
12
|
+
- 评论回灌直接带分析标签 + 正文编辑标签失效(板块情报收尾 A)
|
|
13
|
+
|
|
14
|
+
## [0.2.20] - 2026-07-22
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- VOC 有界编排 CLI 命令(板块情报 #6)
|
|
19
|
+
|
|
7
20
|
## [0.2.19] - 2026-07-22
|
|
8
21
|
|
|
9
22
|
### Added
|
package/dist/cmd-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-07-
|
|
3
|
-
"cliVersion": "0.2.
|
|
2
|
+
"generatedAt": "2026-07-22T12:34:00.265Z",
|
|
3
|
+
"cliVersion": "0.2.21",
|
|
4
4
|
"commands": [
|
|
5
5
|
"accounts",
|
|
6
6
|
"accounts brand-bindings",
|
|
@@ -197,6 +197,7 @@
|
|
|
197
197
|
"intelligence tier-rules-delete",
|
|
198
198
|
"intelligence tier-rules-list",
|
|
199
199
|
"intelligence tier-rules-update",
|
|
200
|
+
"intelligence voc-acquire",
|
|
200
201
|
"intelligence voc-query",
|
|
201
202
|
"intelligence voc-summary",
|
|
202
203
|
"intelligence watchlists-create",
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,24 @@ import { Command } from "commander";
|
|
|
3
3
|
declare const program: Command;
|
|
4
4
|
/** 返回放错位置字段的指引(无则 null)。用于 accounts update / sync core 防呆。 */
|
|
5
5
|
export declare function detectMisplacedAccountFields(body: Record<string, unknown>): string | null;
|
|
6
|
+
export type VocAcquireClient = {
|
|
7
|
+
vocQuery(body: unknown): Promise<Record<string, unknown>>;
|
|
8
|
+
getIntelRunProgress(runId: string): Promise<{
|
|
9
|
+
status: string;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
export declare function orchestrateVocAcquire(client: VocAcquireClient, params: {
|
|
13
|
+
query: Record<string, unknown>;
|
|
14
|
+
requirements: Record<string, unknown>;
|
|
15
|
+
maxRounds: number;
|
|
16
|
+
timeoutMs: number;
|
|
17
|
+
pollMs: number;
|
|
18
|
+
async: boolean;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
sleep?: (ms: number) => Promise<void>;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
exitCode: 0 | 1 | 2;
|
|
23
|
+
output: Record<string, unknown>;
|
|
24
|
+
}>;
|
|
6
25
|
export { program };
|
|
7
26
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2BpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAiqD9B,8DAA8D;AAC9D,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,GAAG,IAAI,CAOf;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2BpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAiqD9B,8DAA8D;AAC9D,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,GAAG,IAAI,CAOf;AAs+BD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1D,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjE,CAAC;AAQF,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE;IACN,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,GACA,OAAO,CAAC;IAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAkJnE;AA0pED,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2040,6 +2040,221 @@ intel
|
|
|
2040
2040
|
});
|
|
2041
2041
|
console.log(JSON.stringify(res, null, 2));
|
|
2042
2042
|
});
|
|
2043
|
+
const VOC_TERMINAL = new Set(["succeeded", "failed", "partial"]);
|
|
2044
|
+
function httpStatusOf(e) {
|
|
2045
|
+
const m = /^HTTP (\d+)/.exec(String(e?.message));
|
|
2046
|
+
return m ? Number(m[1]) : null;
|
|
2047
|
+
}
|
|
2048
|
+
class VocDeadlineError extends Error {
|
|
2049
|
+
}
|
|
2050
|
+
export async function orchestrateVocAcquire(client, params) {
|
|
2051
|
+
const now = params.now ?? (() => Date.now());
|
|
2052
|
+
const sleep = params.sleep ??
|
|
2053
|
+
((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2054
|
+
const deadline = now() + params.timeoutMs;
|
|
2055
|
+
const remaining = () => deadline - now();
|
|
2056
|
+
// 每个请求受"剩余总时限"约束——收 thunk 而非已创建的 promise:先查 remaining 再发起,
|
|
2057
|
+
// 否则超时后仍会启动下一轮派发副作用(rem<=0 时根本不调 fn)。
|
|
2058
|
+
const withDeadline = async (fn) => {
|
|
2059
|
+
const rem = remaining();
|
|
2060
|
+
if (rem <= 0)
|
|
2061
|
+
throw new VocDeadlineError();
|
|
2062
|
+
let timer;
|
|
2063
|
+
const timeout = new Promise((_, rej) => {
|
|
2064
|
+
timer = setTimeout(() => rej(new VocDeadlineError()), rem);
|
|
2065
|
+
});
|
|
2066
|
+
try {
|
|
2067
|
+
// 注:Promise.race 只限制 CLI 等待时间,不取消已发出的 fetch(真正取消需 SDK AbortSignal)。
|
|
2068
|
+
return await Promise.race([fn(), timeout]);
|
|
2069
|
+
}
|
|
2070
|
+
finally {
|
|
2071
|
+
clearTimeout(timer);
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
const satisfied = (r) => Boolean(r.coverage?.overall
|
|
2075
|
+
?.satisfied);
|
|
2076
|
+
const timeoutOut = (round, pending, coverage) => ({
|
|
2077
|
+
exitCode: 2,
|
|
2078
|
+
output: {
|
|
2079
|
+
status: "timeout",
|
|
2080
|
+
rounds: round,
|
|
2081
|
+
pendingRunIds: pending,
|
|
2082
|
+
coverage,
|
|
2083
|
+
},
|
|
2084
|
+
});
|
|
2085
|
+
try {
|
|
2086
|
+
// 无角色预检:权限由服务端强制。voc-query(第一次调用)若无 evidence create 会在**任何派发副作用前**
|
|
2087
|
+
// 403 → 抛出 → 命令层退 1;有权则派发 + 轮询(受限主体持 runId + dimension read 可轮询 /runs/:runId)。
|
|
2088
|
+
let round = 0;
|
|
2089
|
+
let last = {};
|
|
2090
|
+
while (round < params.maxRounds) {
|
|
2091
|
+
round += 1;
|
|
2092
|
+
last = await withDeadline(() => client.vocQuery({
|
|
2093
|
+
query: params.query,
|
|
2094
|
+
requirements: params.requirements,
|
|
2095
|
+
policy: { dispatchOnGap: true },
|
|
2096
|
+
}));
|
|
2097
|
+
if (satisfied(last)) {
|
|
2098
|
+
return {
|
|
2099
|
+
exitCode: 0,
|
|
2100
|
+
output: {
|
|
2101
|
+
status: "satisfied",
|
|
2102
|
+
rounds: round,
|
|
2103
|
+
coverage: last.coverage,
|
|
2104
|
+
},
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
const dispatched = (last.dispatched ?? []);
|
|
2108
|
+
const runIds = [...new Set(dispatched.map((d) => d.runId))];
|
|
2109
|
+
if (params.async) {
|
|
2110
|
+
return {
|
|
2111
|
+
exitCode: 2,
|
|
2112
|
+
output: {
|
|
2113
|
+
status: "dispatched",
|
|
2114
|
+
waited: false,
|
|
2115
|
+
dispatched,
|
|
2116
|
+
coverage: last.coverage,
|
|
2117
|
+
},
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
if (runIds.length === 0)
|
|
2121
|
+
break; // 缺口但无可派发(如 evidence 额度耗尽)→ 需外部 import。
|
|
2122
|
+
const pending = new Set(runIds);
|
|
2123
|
+
while (pending.size) {
|
|
2124
|
+
if (remaining() <= 0)
|
|
2125
|
+
return timeoutOut(round, [...pending], last.coverage);
|
|
2126
|
+
for (const rid of [...pending]) {
|
|
2127
|
+
try {
|
|
2128
|
+
const p = await withDeadline(() => client.getIntelRunProgress(rid));
|
|
2129
|
+
if (VOC_TERMINAL.has(p.status))
|
|
2130
|
+
pending.delete(rid);
|
|
2131
|
+
}
|
|
2132
|
+
catch (e) {
|
|
2133
|
+
if (e instanceof VocDeadlineError)
|
|
2134
|
+
return timeoutOut(round, [...pending], last.coverage);
|
|
2135
|
+
const st = httpStatusOf(e);
|
|
2136
|
+
// 401/403=能派不能读进度;404=run 竟不存在——都是明确错误,立即退 1(不误当完成)。
|
|
2137
|
+
if (st === 401 || st === 403)
|
|
2138
|
+
return {
|
|
2139
|
+
exitCode: 1,
|
|
2140
|
+
output: {
|
|
2141
|
+
status: "forbidden",
|
|
2142
|
+
reason: "run 进度无读权限(可能能派不能读)",
|
|
2143
|
+
runId: rid,
|
|
2144
|
+
},
|
|
2145
|
+
};
|
|
2146
|
+
if (st === 404)
|
|
2147
|
+
return {
|
|
2148
|
+
exitCode: 1,
|
|
2149
|
+
output: { status: "run_not_found", runId: rid },
|
|
2150
|
+
};
|
|
2151
|
+
// 5xx / 网络抖动:保留 pending,deadline 内重试(不误删为完成)。
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
if (pending.size)
|
|
2155
|
+
await sleep(Math.min(params.pollMs, remaining()));
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
// 多轮后重算 coverage 作最终验收(纯读,不再派发)。
|
|
2159
|
+
const finalRes = await withDeadline(() => client.vocQuery({
|
|
2160
|
+
query: params.query,
|
|
2161
|
+
requirements: params.requirements,
|
|
2162
|
+
policy: { dispatchOnGap: false },
|
|
2163
|
+
}));
|
|
2164
|
+
if (satisfied(finalRes)) {
|
|
2165
|
+
return {
|
|
2166
|
+
exitCode: 0,
|
|
2167
|
+
output: {
|
|
2168
|
+
status: "satisfied",
|
|
2169
|
+
rounds: round,
|
|
2170
|
+
coverage: finalRes.coverage,
|
|
2171
|
+
},
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
return {
|
|
2175
|
+
exitCode: 2,
|
|
2176
|
+
output: {
|
|
2177
|
+
status: "unsatisfied",
|
|
2178
|
+
rounds: round,
|
|
2179
|
+
coverage: finalRes.coverage,
|
|
2180
|
+
// 不武断断言"必须"外部 import(worker 失败/权限/内部 enqueue 失败也会到这)。
|
|
2181
|
+
externalImportMayBeRequired: true,
|
|
2182
|
+
reason: "多轮采集后 coverage 仍不满足(常见:evidence 额度耗尽 / 无命中帖;也可能是采集失败)",
|
|
2183
|
+
recommendedCommand: "本地 VOC 自爬后回灌:social-hub intelligence evidence-import --file <evidence.json>(和/或 posts-import/comments-import)",
|
|
2184
|
+
},
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
catch (e) {
|
|
2188
|
+
if (e instanceof VocDeadlineError)
|
|
2189
|
+
return { exitCode: 2, output: { status: "timeout", reason: "总超时" } };
|
|
2190
|
+
throw e; // 网络/鉴权等真实错误 → 交给命令层退 1。
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
intel
|
|
2194
|
+
.command("voc-acquire")
|
|
2195
|
+
.description("VOC 有界编排(Hub-native):循环 voc-query --dispatch-on-gap → 轮询 run 到终态 → 重算 coverage 验收 → " +
|
|
2196
|
+
"缺口再派一轮。退出码 0=coverage 满足 / 2=尚未满足(超时/多轮后仍缺口,可能需外部 import)/ 1=错误。" +
|
|
2197
|
+
"评估口径同 voc-query;受限主体亦可(权限由服务端强制:需 evidence create + 该 dimension read);--async 只派发不等待。")
|
|
2198
|
+
.option("--brand-keywords <list>", "品牌词,逗号分隔")
|
|
2199
|
+
.option("--competitor-keywords <list>", "竞品词,逗号分隔")
|
|
2200
|
+
.option("--subreddits <list>", "板块,逗号分隔")
|
|
2201
|
+
.option("--time-window <w>", "day|week|month", "week")
|
|
2202
|
+
.option("--min-posts <n>", "最小帖子数", "30")
|
|
2203
|
+
.option("--min-subreddits <n>", "最小板块数", "3")
|
|
2204
|
+
.option("--need-comments", "要求评论覆盖")
|
|
2205
|
+
.option("--min-comments <n>", "最小评论数")
|
|
2206
|
+
.option("--need-rules-evidence", "要求版规证据")
|
|
2207
|
+
.option("--freshness-hours <n>", "数据最大年龄(小时)")
|
|
2208
|
+
.option("--max-rounds <n>", "最多派发轮数", "3")
|
|
2209
|
+
.option("--timeout <s>", "总超时(秒)", "300")
|
|
2210
|
+
.option("--poll-interval <s>", "run 轮询间隔(秒)", "5")
|
|
2211
|
+
.option("--async", "只派发第一轮缺口、不等待(打印 runIds 后退 2)")
|
|
2212
|
+
.action(async (opts) => {
|
|
2213
|
+
const csv = (v) => v
|
|
2214
|
+
? String(v)
|
|
2215
|
+
.split(",")
|
|
2216
|
+
.map((s) => s.trim())
|
|
2217
|
+
.filter(Boolean)
|
|
2218
|
+
: undefined;
|
|
2219
|
+
// 显式校验正整数(负/0/小数应报错,不静默回退)。
|
|
2220
|
+
const posInt = (raw, name, dflt) => {
|
|
2221
|
+
if (raw === undefined)
|
|
2222
|
+
return dflt;
|
|
2223
|
+
const n = Number(raw);
|
|
2224
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
2225
|
+
console.error(`${name} 须为正整数,得到 ${raw}`);
|
|
2226
|
+
process.exit(1);
|
|
2227
|
+
}
|
|
2228
|
+
return n;
|
|
2229
|
+
};
|
|
2230
|
+
const client = requireClient();
|
|
2231
|
+
const query = {
|
|
2232
|
+
brandKeywords: csv(opts.brandKeywords),
|
|
2233
|
+
competitorKeywords: csv(opts.competitorKeywords),
|
|
2234
|
+
subreddits: csv(opts.subreddits),
|
|
2235
|
+
timeWindow: opts.timeWindow,
|
|
2236
|
+
};
|
|
2237
|
+
const requirements = {
|
|
2238
|
+
minPosts: Number(opts.minPosts),
|
|
2239
|
+
minSubreddits: Number(opts.minSubreddits),
|
|
2240
|
+
needComments: Boolean(opts.needComments),
|
|
2241
|
+
...(opts.minComments ? { minComments: Number(opts.minComments) } : {}),
|
|
2242
|
+
needRulesEvidence: Boolean(opts.needRulesEvidence),
|
|
2243
|
+
...(opts.freshnessHours
|
|
2244
|
+
? { freshnessHours: Number(opts.freshnessHours) }
|
|
2245
|
+
: {}),
|
|
2246
|
+
};
|
|
2247
|
+
const { exitCode, output } = await orchestrateVocAcquire(client, {
|
|
2248
|
+
query,
|
|
2249
|
+
requirements,
|
|
2250
|
+
maxRounds: posInt(opts.maxRounds, "--max-rounds", 3),
|
|
2251
|
+
timeoutMs: posInt(opts.timeout, "--timeout", 300) * 1000,
|
|
2252
|
+
pollMs: posInt(opts.pollInterval, "--poll-interval", 5) * 1000,
|
|
2253
|
+
async: Boolean(opts.async),
|
|
2254
|
+
});
|
|
2255
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2256
|
+
process.exit(exitCode);
|
|
2257
|
+
});
|
|
2043
2258
|
intel
|
|
2044
2259
|
.command("coverage")
|
|
2045
2260
|
.description("GET /v1/system/subreddit-intelligence/coverage(纯读覆盖率评估:posts/comments/subreddits/evidence 各维 available vs required)")
|