@social-data/social-neuron-subscription 1.0.3 → 1.0.5
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/bin/cli.js +65 -6
- package/bin/install-skills.js +1 -1
- package/manifest.json +7 -7
- package/package.json +1 -1
- package/skills/social-neuron-subscription/SKILL.md +77 -3
package/bin/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
-
// ⚠️ 本文件由
|
|
4
|
+
// ⚠️ 本文件由 core/cli/gen.py 生成,请勿手改;要改改 gen.py 的 _CLI_JS 后重跑 gen。
|
|
5
5
|
//
|
|
6
6
|
// 能力 CLI —— 通用薄运行时(纯 JS,零依赖):一能力一命令、一包、一配置文件,互不影响。
|
|
7
7
|
// <cmd> <action> [--flags] 调用一条命令
|
|
@@ -65,7 +65,9 @@ function normalize(body) {
|
|
|
65
65
|
const type = forgeType(body.code);
|
|
66
66
|
let message = body.msg || "";
|
|
67
67
|
if (type === "outdated") message += "(升级后请重新加载 SKILL.md 与 manifest 再调用)";
|
|
68
|
-
|
|
68
|
+
// 网关在 data 里带审计 trace_id 的业务错误(如查询失败):透出供 get-query 回查。
|
|
69
|
+
const extra = body.data && body.data.trace_id ? { trace_id: body.data.trace_id } : {};
|
|
70
|
+
return { ok: false, error: { type, code: body.code, message, ...extra } };
|
|
69
71
|
}
|
|
70
72
|
return { ok: false, error: { type: "api_error", code: "UNKNOWN", message: JSON.stringify(body).slice(0, 200) } };
|
|
71
73
|
}
|
|
@@ -113,15 +115,63 @@ function printWelcome() {
|
|
|
113
115
|
w("──────────────────────────────────────────────");
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
// ── 嵌套对象形状:object(json) / list[object](json) 这类 flag 光看类型名没法拼 JSON,
|
|
119
|
+
// 这里把 input_schema.$defs 里**被引用到的**对象摊开印出来(与 SKILL.md 同源同内容)。
|
|
120
|
+
// 没有这段时,调用方只能靠 422 报错反推字段名。
|
|
121
|
+
function typeStr(node) {
|
|
122
|
+
if (!node || typeof node !== "object") return "any";
|
|
123
|
+
if (typeof node.$ref === "string") return node.$ref.split("/").pop();
|
|
124
|
+
const union = node.anyOf || node.oneOf;
|
|
125
|
+
if (union) {
|
|
126
|
+
const parts = union.filter((n) => n && n.type !== "null").map(typeStr);
|
|
127
|
+
return parts.length ? parts.join("|") : "any";
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(node.enum)) return "enum(" + node.enum.join("|") + ")";
|
|
130
|
+
if (node.type === "array") return "list[" + typeStr(node.items) + "]";
|
|
131
|
+
return node.type || "any";
|
|
132
|
+
}
|
|
133
|
+
function refsOf(node, acc) {
|
|
134
|
+
if (!node || typeof node !== "object") return acc;
|
|
135
|
+
if (Array.isArray(node)) { for (const n of node) refsOf(n, acc); return acc; }
|
|
136
|
+
if (typeof node.$ref === "string") acc.add(node.$ref.split("/").pop());
|
|
137
|
+
for (const k of Object.keys(node)) if (k !== "$defs") refsOf(node[k], acc);
|
|
138
|
+
return acc;
|
|
139
|
+
}
|
|
140
|
+
function objectBlocks(schema) {
|
|
141
|
+
const defs = (schema && schema.$defs) || {};
|
|
142
|
+
const queue = [...refsOf((schema && schema.properties) || {}, new Set())], seen = new Set();
|
|
143
|
+
while (queue.length) { // 递归:GroupLevel 会引用自己、Leaf 会被 AndNode 引用
|
|
144
|
+
const name = queue.shift();
|
|
145
|
+
if (seen.has(name) || !defs[name]) continue;
|
|
146
|
+
seen.add(name);
|
|
147
|
+
for (const r of refsOf(defs[name], new Set())) queue.push(r);
|
|
148
|
+
}
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const name of [...seen].sort()) {
|
|
151
|
+
const d = defs[name], props = d.properties;
|
|
152
|
+
if (!props) continue;
|
|
153
|
+
const required = new Set(d.required || []);
|
|
154
|
+
const rows = Object.keys(props).map((k) => [k, typeStr(props[k]), required.has(k) ? "required" : "optional",
|
|
155
|
+
props[k].description || ""]);
|
|
156
|
+
const w0 = Math.max(...rows.map((r) => r[0].length)), w1 = Math.max(...rows.map((r) => r[1].length));
|
|
157
|
+
out.push("对象 " + name + ":" + (d.description ? " " + d.description.split("\n")[0] : ""));
|
|
158
|
+
for (const r of rows) out.push(" " + r[0].padEnd(w0) + " " + r[1].padEnd(w1) + " " + r[2].padEnd(8) + " " + r[3]);
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
116
163
|
function usageOf(cmd) {
|
|
117
164
|
const line = (f) => " " + f.flag + " " + f.type + (f.required ? " required" : " optional") + " " + (f.desc || "");
|
|
118
165
|
const req = cmd.flags.filter((f) => f.required), opt = cmd.flags.filter((f) => !f.required);
|
|
166
|
+
const blocks = objectBlocks(cmd.input_schema);
|
|
119
167
|
return {
|
|
120
168
|
command: COMMAND + " " + cmd.action,
|
|
121
169
|
required: req.map((f) => ({ flag: f.flag, type: f.type, desc: f.desc })),
|
|
122
170
|
optional: opt.map((f) => ({ flag: f.flag, type: f.type, desc: f.desc })),
|
|
171
|
+
objects: blocks, // 同样带进 INVALID_ARGS 报错里,拼错时直接能看到形状
|
|
123
172
|
example: (COMMAND + " " + cmd.action + " " + req.map((f) => f.flag + " <v>").join(" ")).trim(),
|
|
124
|
-
_text: [COMMAND + " " + cmd.action + " — " + cmd.summary, "Inputs:", ...cmd.flags.map(line)
|
|
173
|
+
_text: [COMMAND + " " + cmd.action + " — " + cmd.summary, "Inputs:", ...cmd.flags.map(line),
|
|
174
|
+
...(blocks.length ? ["", ...blocks] : [])].join("\n"),
|
|
125
175
|
};
|
|
126
176
|
}
|
|
127
177
|
function printHelp(action) {
|
|
@@ -134,7 +184,7 @@ function printHelp(action) {
|
|
|
134
184
|
w(COMMAND + " v" + VERSION + " — Social Neuron 能力 CLI(能力:" + CAPABILITY + ";供 AI agent 调用)");
|
|
135
185
|
w("");
|
|
136
186
|
w("用法:" + COMMAND + " <action> [--flags]");
|
|
137
|
-
w(" 标量 --flag v
|
|
187
|
+
w(" 标量 --flag v;数组/对象一律 JSON:--flag '[\"a\",\"b\"]' / --flag '{\"k\":\"v\"}';写/删/触发加 -y");
|
|
138
188
|
w("");
|
|
139
189
|
w("命令:");
|
|
140
190
|
for (const c of MANIFEST.tools) w(" " + COMMAND + " " + c.action + " — " + c.summary);
|
|
@@ -162,8 +212,17 @@ function parseArgs(cmd, argv) {
|
|
|
162
212
|
const raw = argv[++i];
|
|
163
213
|
if (raw === undefined) return { err: fail("validation", "MISSING_VALUE", a + " 缺少取值") };
|
|
164
214
|
let val = raw;
|
|
165
|
-
if (f.type.
|
|
166
|
-
|
|
215
|
+
if (f.type.indexOf("(json)") >= 0) {
|
|
216
|
+
// 数组/对象一律按 JSON 解析(与 MCP 入参同形)。不做「逗号分隔」这类特例:
|
|
217
|
+
// 同一份文档要能同时用于两个出口,取值写法就只能有一种。
|
|
218
|
+
try { val = JSON.parse(raw); } catch {
|
|
219
|
+
const hint = f.type.startsWith("list") ? "JSON 数组,如 '[\"a\",\"b\"]'" : "JSON 对象,如 '{\"k\":\"v\"}'";
|
|
220
|
+
return { err: fail("validation", "BAD_JSON", a + " 需为 " + hint + "(当前取值:" + raw + ")") };
|
|
221
|
+
}
|
|
222
|
+
if (f.type.startsWith("list") && !Array.isArray(val)) {
|
|
223
|
+
return { err: fail("validation", "BAD_JSON", a + " 需为 JSON 数组,如 '[\"a\",\"b\"]';单个取值也要写成数组") };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
167
226
|
else if (f.type === "integer") val = Number(raw);
|
|
168
227
|
args[f.field] = val;
|
|
169
228
|
}
|
package/bin/install-skills.js
CHANGED
package/manifest.json
CHANGED
|
@@ -425,14 +425,14 @@
|
|
|
425
425
|
{
|
|
426
426
|
"flag": "--rule-ids",
|
|
427
427
|
"field": "rule_ids",
|
|
428
|
-
"type": "list[str]",
|
|
428
|
+
"type": "list[str](json)",
|
|
429
429
|
"required": true,
|
|
430
430
|
"desc": "魔方规则ID数组,至少 1 个"
|
|
431
431
|
},
|
|
432
432
|
{
|
|
433
433
|
"flag": "--source",
|
|
434
434
|
"field": "source",
|
|
435
|
-
"type": "list[str]",
|
|
435
|
+
"type": "list[str](json)",
|
|
436
436
|
"required": true,
|
|
437
437
|
"desc": "站点数组,至少 1 个;只能取已配置站点"
|
|
438
438
|
},
|
|
@@ -467,7 +467,7 @@
|
|
|
467
467
|
{
|
|
468
468
|
"flag": "--fields",
|
|
469
469
|
"field": "fields",
|
|
470
|
-
"type": "list[str]",
|
|
470
|
+
"type": "list[str](json)",
|
|
471
471
|
"required": false,
|
|
472
472
|
"desc": "拉取/导出字段;不填拉全量"
|
|
473
473
|
},
|
|
@@ -776,7 +776,7 @@
|
|
|
776
776
|
"op": "job_trigger",
|
|
777
777
|
"name": "subscription/job_trigger",
|
|
778
778
|
"summary": "临时触发一次本人 job,落一条 ik_subscription_task(manual);默认用 job 存的 target,可覆盖。",
|
|
779
|
-
"description": "临时触发一次本人 job,落一条 ik_subscription_task(manual);默认用 job 存的 target,可覆盖。\n\n落 pending task 后触发 Airflow DAG(``
|
|
779
|
+
"description": "临时触发一次本人 job,落一条 ik_subscription_task(manual);默认用 job 存的 target,可覆盖。\n\n落 pending task 后触发 Airflow DAG(``social_capability_subscription``)跑取数→组装→投递(上传取 url);\n未配 airflow_base_url 时(本地/测试)触发被跳过,仅留 pending task。**无通知**:结果落\n``file_info.url``,调用方轮询 ``task-list`` 拉取。",
|
|
780
780
|
"input_schema": {
|
|
781
781
|
"$defs": {
|
|
782
782
|
"KafkaTarget": {
|
|
@@ -1664,14 +1664,14 @@
|
|
|
1664
1664
|
{
|
|
1665
1665
|
"flag": "--rule-ids",
|
|
1666
1666
|
"field": "rule_ids",
|
|
1667
|
-
"type": "list[str]",
|
|
1667
|
+
"type": "list[str](json)",
|
|
1668
1668
|
"required": false,
|
|
1669
1669
|
"desc": "整组全量覆盖"
|
|
1670
1670
|
},
|
|
1671
1671
|
{
|
|
1672
1672
|
"flag": "--source",
|
|
1673
1673
|
"field": "source",
|
|
1674
|
-
"type": "list[str]",
|
|
1674
|
+
"type": "list[str](json)",
|
|
1675
1675
|
"required": false,
|
|
1676
1676
|
"desc": "整组全量覆盖"
|
|
1677
1677
|
},
|
|
@@ -1699,7 +1699,7 @@
|
|
|
1699
1699
|
{
|
|
1700
1700
|
"flag": "--fields",
|
|
1701
1701
|
"field": "fields",
|
|
1702
|
-
"type": "list[str]",
|
|
1702
|
+
"type": "list[str](json)",
|
|
1703
1703
|
"required": false,
|
|
1704
1704
|
"desc": "整组全量覆盖"
|
|
1705
1705
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@social-data/social-neuron-subscription",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Social Neuron subscription 能力 CLI —— 一能力一命令(social-neuron-subscription),薄客户端直达 /subscription/{op}。",
|
|
5
5
|
"bin": {
|
|
6
6
|
"social-neuron-subscription": "bin/cli.js"
|
|
@@ -10,7 +10,81 @@ metadata:
|
|
|
10
10
|
|
|
11
11
|
> ⚠️ 本技能随该能力 CLI 版本更新。若刚升级过 CLI、重装过 skill,或调用返回 426(版本过旧需升级),请**重新加载本 SKILL.md 与 manifest** 再用——以最新文件为准。
|
|
12
12
|
|
|
13
|
-
> 命令为一能力一命令:`social-neuron-subscription <action> --flags`(无子命令层)
|
|
13
|
+
> 命令为一能力一命令:`social-neuron-subscription <action> --flags`(无子命令层)。
|
|
14
|
+
> **取值写法**:标量直接写(`--size 20`);**数组和对象一律写成一个 JSON 串**,与 MCP/HTTP 入参完全同形:`--platforms '["video"]'`、`--where '{...}'`。数组**不能**用逗号分隔,单个取值也要写成数组(`'["video"]'` 而不是 `video`)。
|
|
15
|
+
|
|
16
|
+
## 怎么用(操作手册)
|
|
17
|
+
|
|
18
|
+
这是**异步导出**:创建 job → 系统按 cron 或你手动触发 → 产出 task → **轮询 task 拿文件 url**。
|
|
19
|
+
没有主动通知,不轮询就拿不到结果。
|
|
20
|
+
|
|
21
|
+
### 一、写法约定
|
|
22
|
+
|
|
23
|
+
数组和对象一律写成 **JSON 串**,和 MCP 入参同形:`--rule-ids '["r1","r2"]'`、`--target '{...}'`。
|
|
24
|
+
数组不能用逗号分隔,**单个取值也要写成数组**。写错会直接报 `BAD_JSON` 并回显原值。
|
|
25
|
+
|
|
26
|
+
### 二、两个概念
|
|
27
|
+
|
|
28
|
+
| | 是什么 | 谁创建 |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| **job** | 订阅定义:数据范围 + 频率 + 投递方式 | 你 `job-create` |
|
|
31
|
+
| **task** | job 的**一次执行**,产物和下载地址落在 `file_info` | 系统按 cron,或你 `job-trigger` |
|
|
32
|
+
|
|
33
|
+
### 三、标准流程
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
job-create → 拿到 job_id
|
|
37
|
+
job-trigger → 立刻跑一次(不等 cron);返回 task_id
|
|
38
|
+
task-list → 轮询该 task,status 变终态后读 file_info 里的下载 url
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
改配置用 `job-update`(只传要改的字段);不想再跑就 `job-update --active false`,彻底不要才 `job-delete`。
|
|
42
|
+
|
|
43
|
+
### 四、三个时间口径别搞混
|
|
44
|
+
|
|
45
|
+
- `--published` **发布时间,必填**:它同时决定查哪个月的索引,所以不能缺省。
|
|
46
|
+
- `--idate` 入库时间、`--udate` 更新时间:**选填**,是在发布时间窗**之内**再 AND 一层收窄;
|
|
47
|
+
不传 = 该维度不过滤。
|
|
48
|
+
|
|
49
|
+
### 五、容易踩的
|
|
50
|
+
|
|
51
|
+
- **拉不到数据就不会有文件**,`file_info` 里也就没有 url —— 这不是失败,是没命中。
|
|
52
|
+
- 单次导出 `--size` 默认且最大 **50 万行**,超出会截断。
|
|
53
|
+
- 每个 token 生效的 job 默认 **≤10 个**,满了要先 `job-delete` 或停用。
|
|
54
|
+
- `--source` 只能填**已配置的站点**,不是随便写域名。
|
|
55
|
+
- 投递目标本期只支持 **octo / sftp**(kafka 规划中);省略 `--target` 默认 octo。
|
|
56
|
+
|
|
57
|
+
## 示例
|
|
58
|
+
|
|
59
|
+
### 建一个每天跑的订阅,并立刻先跑一次看看
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# 1) 建 job(频率用自然语言,系统转 cron)
|
|
63
|
+
social-neuron-subscription job-create --name 每日舆情 \
|
|
64
|
+
--rule-ids '["12345"]' --source '["douyin.com"]' \
|
|
65
|
+
--published '近7天' --frequency '每天早上8点' -y
|
|
66
|
+
|
|
67
|
+
# 2) 不等 cron,手动触发一次
|
|
68
|
+
social-neuron-subscription job-trigger --job-id 1001 -y
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> 写操作要加 -y。job-trigger 返回 task_id,拿它去轮询。
|
|
72
|
+
|
|
73
|
+
### 轮询拿下载地址
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
social-neuron-subscription task-list --job-id 1001 --page 1 --page-size 10
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> status 到终态后读 file_info 里的 url;没命中数据时不会有文件,这不是失败。
|
|
80
|
+
|
|
81
|
+
### 临时停用一个订阅(保留定义)
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
social-neuron-subscription job-update --job-id 1001 --active false -y
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> 只传要改的字段即可;彻底不要才用 job-delete。
|
|
14
88
|
|
|
15
89
|
## 命令
|
|
16
90
|
|
|
@@ -84,7 +158,7 @@ metadata:
|
|
|
84
158
|
| created_at | string | 是 | - | 创建时间(ISO8601,UTC+8) |
|
|
85
159
|
| updated_at | string | 是 | - | 更新时间(ISO8601,UTC+8) |
|
|
86
160
|
|
|
87
|
-
示例:social-neuron-subscription job-create --name <v> --rule-ids
|
|
161
|
+
示例:social-neuron-subscription job-create --name <v> --rule-ids '["a","b"]' --source '["a","b"]' --published <v> --frequency <v>
|
|
88
162
|
|
|
89
163
|
### social-neuron-subscription job-delete
|
|
90
164
|
软删除本人 job:置 active=0(记录保留,可 job_update 恢复)。
|
|
@@ -146,7 +220,7 @@ metadata:
|
|
|
146
220
|
### social-neuron-subscription job-trigger
|
|
147
221
|
临时触发一次本人 job,落一条 ik_subscription_task(manual);默认用 job 存的 target,可覆盖。
|
|
148
222
|
|
|
149
|
-
落 pending task 后触发 Airflow DAG(``
|
|
223
|
+
落 pending task 后触发 Airflow DAG(``social_capability_subscription``)跑取数→组装→投递(上传取 url);
|
|
150
224
|
未配 airflow_base_url 时(本地/测试)触发被跳过,仅留 pending task。**无通知**:结果落
|
|
151
225
|
``file_info.url``,调用方轮询 ``task-list`` 拉取。
|
|
152
226
|
|