@social-data/social-neuron-subscription 1.0.2 → 1.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
- // ⚠️ 本文件由 backend/app/cli_spec/gen.py 生成,请勿手改;要改改 gen.py 的 _CLI_JS 后重跑 gen。
4
+ // ⚠️ 本文件由 core/cli/gen.py 生成,请勿手改;要改改 gen.py 的 _CLI_JS 后重跑 gen。
5
5
  //
6
6
  // 能力 CLI —— 通用薄运行时(纯 JS,零依赖):一能力一命令、一包、一配置文件,互不影响。
7
7
  // <cmd> <action> [--flags] 调用一条命令
@@ -40,7 +40,11 @@ function resolveConfig() {
40
40
 
41
41
  function emit(env) {
42
42
  process.stdout.write(JSON.stringify(env) + "\n");
43
- process.exit(env.ok ? 0 : (EXIT[env.error && env.error.type] ?? 1));
43
+ const dataExit = env.ok && env.data && env.data.meta && env.data.meta.exit_code;
44
+ const status = env.ok
45
+ ? ([0, 1, 2].includes(dataExit) ? dataExit : 0)
46
+ : (EXIT[env.error && env.error.type] ?? 1);
47
+ process.exit(status);
44
48
  }
45
49
  function fail(type, code, message, extra) {
46
50
  emit({ ok: false, error: { type, code, message, ...(extra || {}) } });
@@ -61,7 +65,9 @@ function normalize(body) {
61
65
  const type = forgeType(body.code);
62
66
  let message = body.msg || "";
63
67
  if (type === "outdated") message += "(升级后请重新加载 SKILL.md 与 manifest 再调用)";
64
- return { ok: false, error: { type, code: body.code, message } };
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 } };
65
71
  }
66
72
  return { ok: false, error: { type: "api_error", code: "UNKNOWN", message: JSON.stringify(body).slice(0, 200) } };
67
73
  }
@@ -109,15 +115,63 @@ function printWelcome() {
109
115
  w("──────────────────────────────────────────────");
110
116
  }
111
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
+
112
163
  function usageOf(cmd) {
113
164
  const line = (f) => " " + f.flag + " " + f.type + (f.required ? " required" : " optional") + " " + (f.desc || "");
114
165
  const req = cmd.flags.filter((f) => f.required), opt = cmd.flags.filter((f) => !f.required);
166
+ const blocks = objectBlocks(cmd.input_schema);
115
167
  return {
116
168
  command: COMMAND + " " + cmd.action,
117
169
  required: req.map((f) => ({ flag: f.flag, type: f.type, desc: f.desc })),
118
170
  optional: opt.map((f) => ({ flag: f.flag, type: f.type, desc: f.desc })),
171
+ objects: blocks, // 同样带进 INVALID_ARGS 报错里,拼错时直接能看到形状
119
172
  example: (COMMAND + " " + cmd.action + " " + req.map((f) => f.flag + " <v>").join(" ")).trim(),
120
- _text: [COMMAND + " " + cmd.action + " — " + cmd.summary, "Inputs:", ...cmd.flags.map(line)].join("\n"),
173
+ _text: [COMMAND + " " + cmd.action + " — " + cmd.summary, "Inputs:", ...cmd.flags.map(line),
174
+ ...(blocks.length ? ["", ...blocks] : [])].join("\n"),
121
175
  };
122
176
  }
123
177
  function printHelp(action) {
@@ -130,7 +184,7 @@ function printHelp(action) {
130
184
  w(COMMAND + " v" + VERSION + " — Social Neuron 能力 CLI(能力:" + CAPABILITY + ";供 AI agent 调用)");
131
185
  w("");
132
186
  w("用法:" + COMMAND + " <action> [--flags]");
133
- w(" 标量 --flag v;数组 --flag a,b;对象 --flag '<json>';写/删/触发加 -y");
187
+ w(" 标量 --flag v;数组/对象一律 JSON:--flag '[\"a\",\"b\"]' / --flag '{\"k\":\"v\"}';写/删/触发加 -y");
134
188
  w("");
135
189
  w("命令:");
136
190
  for (const c of MANIFEST.tools) w(" " + COMMAND + " " + c.action + " — " + c.summary);
@@ -158,8 +212,17 @@ function parseArgs(cmd, argv) {
158
212
  const raw = argv[++i];
159
213
  if (raw === undefined) return { err: fail("validation", "MISSING_VALUE", a + " 缺少取值") };
160
214
  let val = raw;
161
- if (f.type.startsWith("list")) val = raw.split(",").map((s) => s.trim()).filter(Boolean);
162
- else if (f.type.startsWith("object")) { try { val = JSON.parse(raw); } catch { return { err: fail("validation", "BAD_JSON", a + " 需为合法 JSON") }; } }
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
+ }
163
226
  else if (f.type === "integer") val = Number(raw);
164
227
  args[f.field] = val;
165
228
  }
@@ -167,7 +230,8 @@ function parseArgs(cmd, argv) {
167
230
  }
168
231
 
169
232
  async function main() {
170
- const argv = process.argv.slice(2);
233
+ let argv = process.argv.slice(2);
234
+ if (argv.length === 0 && MANIFEST.default_action) argv = [MANIFEST.default_action];
171
235
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") return printHelp();
172
236
  if (argv[0] === "--version" || argv[0] === "-V") { process.stdout.write(VERSION + "\n"); return; }
173
237
  if (argv[0] === "bind") return doBind(argv.slice(1));
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
- // ⚠️ 本文件由 backend/app/cli_spec/gen.py 生成,请勿手改。
4
+ // ⚠️ 本文件由 core/cli/gen.py 生成,请勿手改。
5
5
  // postinstall:把包内 SKILL.md 覆盖写到 OpenClaw 技能目录(OPENCLAW_SKILLS_DIR 或 ~/.openclaw/skills)。
6
6
 
7
7
  const fs = require("fs");
package/manifest.json CHANGED
@@ -2,6 +2,7 @@
2
2
  "revision": 6,
3
3
  "capability": "subscription",
4
4
  "command": "social-neuron-subscription",
5
+ "default_action": null,
5
6
  "mcp_path": "/subscription/mcp",
6
7
  "tools": [
7
8
  {
@@ -84,7 +85,7 @@
84
85
  },
85
86
  "SftpTarget": {
86
87
  "additionalProperties": false,
87
- "description": "sftp(本期):把导出文件上传到 SFTP 远端目录。\n目录与文件名都支持日期变量 ``yyyyMMdd`` / ``yyyyMM`` / ``yyyy``,同一个 task 重跑多少次,远端路径与文件名都不变。",
88
+ "description": "sftp(本期):把导出文件上传到 SFTP 远端目录。\n目录与文件名都支持日期变量 ``yyyyMMdd`` / ``yyyyMM`` / ``yyyy``,同一个 task\n重跑多少次,远端路径与文件名都不变。",
88
89
  "properties": {
89
90
  "type": {
90
91
  "const": "sftp",
@@ -233,6 +234,14 @@
233
234
  "title": "Fields",
234
235
  "type": "array"
235
236
  },
237
+ "size": {
238
+ "default": 500000,
239
+ "description": "每次执行最多导出的记录行数,默认且最大为 500000",
240
+ "maximum": 500000,
241
+ "minimum": 1,
242
+ "title": "Size",
243
+ "type": "integer"
244
+ },
236
245
  "target": {
237
246
  "anyOf": [
238
247
  {
@@ -342,6 +351,11 @@
342
351
  "title": "Fields",
343
352
  "type": "array"
344
353
  },
354
+ "size": {
355
+ "description": "每次执行最多导出的记录行数,范围 1–500000",
356
+ "title": "Size",
357
+ "type": "integer"
358
+ },
345
359
  "frequency": {
346
360
  "description": "自然语言频率",
347
361
  "title": "Frequency",
@@ -388,6 +402,7 @@
388
402
  "idate",
389
403
  "udate",
390
404
  "fields",
405
+ "size",
391
406
  "frequency",
392
407
  "cron",
393
408
  "target_type",
@@ -410,14 +425,14 @@
410
425
  {
411
426
  "flag": "--rule-ids",
412
427
  "field": "rule_ids",
413
- "type": "list[str]",
428
+ "type": "list[str](json)",
414
429
  "required": true,
415
430
  "desc": "魔方规则ID数组,至少 1 个"
416
431
  },
417
432
  {
418
433
  "flag": "--source",
419
434
  "field": "source",
420
- "type": "list[str]",
435
+ "type": "list[str](json)",
421
436
  "required": true,
422
437
  "desc": "站点数组,至少 1 个;只能取已配置站点"
423
438
  },
@@ -452,10 +467,17 @@
452
467
  {
453
468
  "flag": "--fields",
454
469
  "field": "fields",
455
- "type": "list[str]",
470
+ "type": "list[str](json)",
456
471
  "required": false,
457
472
  "desc": "拉取/导出字段;不填拉全量"
458
473
  },
474
+ {
475
+ "flag": "--size",
476
+ "field": "size",
477
+ "type": "integer",
478
+ "required": false,
479
+ "desc": "每次执行最多导出的记录行数,默认且最大为 500000"
480
+ },
459
481
  {
460
482
  "flag": "--target",
461
483
  "field": "target",
@@ -640,6 +662,11 @@
640
662
  "title": "Fields",
641
663
  "type": "array"
642
664
  },
665
+ "size": {
666
+ "description": "每次执行最多导出的记录行数,范围 1–500000",
667
+ "title": "Size",
668
+ "type": "integer"
669
+ },
643
670
  "frequency": {
644
671
  "description": "自然语言频率",
645
672
  "title": "Frequency",
@@ -686,6 +713,7 @@
686
713
  "idate",
687
714
  "udate",
688
715
  "fields",
716
+ "size",
689
717
  "frequency",
690
718
  "cron",
691
719
  "target_type",
@@ -748,7 +776,7 @@
748
776
  "op": "job_trigger",
749
777
  "name": "subscription/job_trigger",
750
778
  "summary": "临时触发一次本人 job,落一条 ik_subscription_task(manual);默认用 job 存的 target,可覆盖。",
751
- "description": "临时触发一次本人 job,落一条 ik_subscription_task(manual);默认用 job 存的 target,可覆盖。\n\n落 pending task 后触发 Airflow DAG(``social_neuron_subscription``)跑取数→组装→投递(上传取 url);\n未配 airflow_base_url 时(本地/测试)触发被跳过,仅留 pending task。**无通知**:结果落\n``file_info.url``,调用方轮询 ``task-list`` 拉取。",
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`` 拉取。",
752
780
  "input_schema": {
753
781
  "$defs": {
754
782
  "KafkaTarget": {
@@ -823,7 +851,7 @@
823
851
  },
824
852
  "SftpTarget": {
825
853
  "additionalProperties": false,
826
- "description": "sftp(本期):把导出文件上传到 SFTP 远端目录。\n目录与文件名都支持日期变量 ``yyyyMMdd`` / ``yyyyMM`` / ``yyyy``,同一个 task 重跑多少次,远端路径与文件名都不变。",
854
+ "description": "sftp(本期):把导出文件上传到 SFTP 远端目录。\n目录与文件名都支持日期变量 ``yyyyMMdd`` / ``yyyyMM`` / ``yyyy``,同一个 task\n重跑多少次,远端路径与文件名都不变。",
827
855
  "properties": {
828
856
  "type": {
829
857
  "const": "sftp",
@@ -1014,6 +1042,21 @@
1014
1042
  "default": null,
1015
1043
  "description": "覆盖本次投递目标",
1016
1044
  "title": "Target"
1045
+ },
1046
+ "size": {
1047
+ "anyOf": [
1048
+ {
1049
+ "maximum": 500000,
1050
+ "minimum": 1,
1051
+ "type": "integer"
1052
+ },
1053
+ {
1054
+ "type": "null"
1055
+ }
1056
+ ],
1057
+ "default": null,
1058
+ "description": "覆盖本次导出行数;不传则继承 job 的 size,最大 500000",
1059
+ "title": "Size"
1017
1060
  }
1018
1061
  },
1019
1062
  "required": [
@@ -1099,6 +1142,13 @@
1099
1142
  "type": "object(json)",
1100
1143
  "required": false,
1101
1144
  "desc": "覆盖本次投递目标"
1145
+ },
1146
+ {
1147
+ "flag": "--size",
1148
+ "field": "size",
1149
+ "type": "integer",
1150
+ "required": false,
1151
+ "desc": "覆盖本次导出行数;不传则继承 job 的 size,最大 500000"
1102
1152
  }
1103
1153
  ],
1104
1154
  "transport": {
@@ -1194,7 +1244,7 @@
1194
1244
  },
1195
1245
  "SftpTarget": {
1196
1246
  "additionalProperties": false,
1197
- "description": "sftp(本期):把导出文件上传到 SFTP 远端目录。\n目录与文件名都支持日期变量 ``yyyyMMdd`` / ``yyyyMM`` / ``yyyy``,同一个 task 重跑多少次,远端路径与文件名都不变。",
1247
+ "description": "sftp(本期):把导出文件上传到 SFTP 远端目录。\n目录与文件名都支持日期变量 ``yyyyMMdd`` / ``yyyyMM`` / ``yyyy``,同一个 task\n重跑多少次,远端路径与文件名都不变。",
1198
1248
  "properties": {
1199
1249
  "type": {
1200
1250
  "const": "sftp",
@@ -1381,6 +1431,21 @@
1381
1431
  "description": "整组全量覆盖",
1382
1432
  "title": "Fields"
1383
1433
  },
1434
+ "size": {
1435
+ "anyOf": [
1436
+ {
1437
+ "maximum": 500000,
1438
+ "minimum": 1,
1439
+ "type": "integer"
1440
+ },
1441
+ {
1442
+ "type": "null"
1443
+ }
1444
+ ],
1445
+ "default": null,
1446
+ "description": "每次执行最多导出的记录行数;不传则不修改,最大 500000",
1447
+ "title": "Size"
1448
+ },
1384
1449
  "frequency": {
1385
1450
  "anyOf": [
1386
1451
  {
@@ -1518,6 +1583,11 @@
1518
1583
  "title": "Fields",
1519
1584
  "type": "array"
1520
1585
  },
1586
+ "size": {
1587
+ "description": "每次执行最多导出的记录行数,范围 1–500000",
1588
+ "title": "Size",
1589
+ "type": "integer"
1590
+ },
1521
1591
  "frequency": {
1522
1592
  "description": "自然语言频率",
1523
1593
  "title": "Frequency",
@@ -1564,6 +1634,7 @@
1564
1634
  "idate",
1565
1635
  "udate",
1566
1636
  "fields",
1637
+ "size",
1567
1638
  "frequency",
1568
1639
  "cron",
1569
1640
  "target_type",
@@ -1593,14 +1664,14 @@
1593
1664
  {
1594
1665
  "flag": "--rule-ids",
1595
1666
  "field": "rule_ids",
1596
- "type": "list[str]",
1667
+ "type": "list[str](json)",
1597
1668
  "required": false,
1598
1669
  "desc": "整组全量覆盖"
1599
1670
  },
1600
1671
  {
1601
1672
  "flag": "--source",
1602
1673
  "field": "source",
1603
- "type": "list[str]",
1674
+ "type": "list[str](json)",
1604
1675
  "required": false,
1605
1676
  "desc": "整组全量覆盖"
1606
1677
  },
@@ -1628,10 +1699,17 @@
1628
1699
  {
1629
1700
  "flag": "--fields",
1630
1701
  "field": "fields",
1631
- "type": "list[str]",
1702
+ "type": "list[str](json)",
1632
1703
  "required": false,
1633
1704
  "desc": "整组全量覆盖"
1634
1705
  },
1706
+ {
1707
+ "flag": "--size",
1708
+ "field": "size",
1709
+ "type": "integer",
1710
+ "required": false,
1711
+ "desc": "每次执行最多导出的记录行数;不传则不修改,最大 500000"
1712
+ },
1635
1713
  {
1636
1714
  "flag": "--frequency",
1637
1715
  "field": "frequency",
@@ -1788,6 +1866,11 @@
1788
1866
  "title": "Job Id",
1789
1867
  "type": "integer"
1790
1868
  },
1869
+ "size": {
1870
+ "description": "本次执行最多导出的记录行数",
1871
+ "title": "Size",
1872
+ "type": "integer"
1873
+ },
1791
1874
  "status": {
1792
1875
  "description": "pending/running/success/failed/terminated",
1793
1876
  "title": "Status",
@@ -1834,6 +1917,7 @@
1834
1917
  "required": [
1835
1918
  "task_id",
1836
1919
  "job_id",
1920
+ "size",
1837
1921
  "status",
1838
1922
  "created_at",
1839
1923
  "updated_at"
@@ -1939,7 +2023,7 @@
1939
2023
  ],
1940
2024
  "skill": {
1941
2025
  "slug": "social-neuron-subscription",
1942
- "description": "使用场景:定时导出/订阅推送/定期拉数/大批量出文件——需异步产出文件并下载时用能力:按魔方规则、定时 cron 或手动触发,把大库数据异步导出为文件,上传 octo/sftp,产出下载 url 供调用方轮询拉取;可创建/修改/列出/删除/触发订阅 job、翻页查询 task。概念:job=订阅定义(数据范围+频率+投递方式,系统按频率生成 cron);task=job 的一次执行,产物与下载地址落在 file_info。边界:①每 token 生效 job 默认≤10;②单次拉数≤10 万行,超出截断;③投递仅 octo/sftp(kafka 规划中,暂不支持),无主动通知、须轮询 task 取结果。④如果拉取不到数据则不会产生文件上传,也就没有文件url信息",
2026
+ "description": "使用场景:定时导出/订阅推送/定期拉数/大批量出文件——需异步产出文件并下载时用能力:按魔方规则、定时 cron 或手动触发,把大库数据异步导出为文件,上传 octo/sftp,产出下载 url 供调用方轮询拉取;可创建/修改/列出/删除/触发订阅 job、翻页查询 task。概念:job=订阅定义(数据范围+频率+投递方式,系统按频率生成 cron);task=job 的一次执行,产物与下载地址落在 file_info。边界:①每 token 生效 job 默认≤10;②单次拉数可指定 size,默认且最大 50 万行,超出截断;③投递仅 octo/sftp(kafka 规划中,暂不支持),无主动通知、须轮询 task 取结果。④如果拉取不到数据则不会产生文件上传,也就没有文件url信息",
1943
2027
  "actions": [
1944
2028
  "job-create",
1945
2029
  "job-delete",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@social-data/social-neuron-subscription",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Social Neuron subscription 能力 CLI —— 一能力一命令(social-neuron-subscription),薄客户端直达 /subscription/{op}。",
5
5
  "bin": {
6
6
  "social-neuron-subscription": "bin/cli.js"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: social-neuron-subscription
3
- description: "使用场景:定时导出/订阅推送/定期拉数/大批量出文件——需异步产出文件并下载时用能力:按魔方规则、定时 cron 或手动触发,把大库数据异步导出为文件,上传 octo/sftp,产出下载 url 供调用方轮询拉取;可创建/修改/列出/删除/触发订阅 job、翻页查询 task。概念:job=订阅定义(数据范围+频率+投递方式,系统按频率生成 cron);task=job 的一次执行,产物与下载地址落在 file_info。边界:①每 token 生效 job 默认≤10;②单次拉数≤10 万行,超出截断;③投递仅 octo/sftp(kafka 规划中,暂不支持),无主动通知、须轮询 task 取结果。④如果拉取不到数据则不会产生文件上传,也就没有文件url信息"
3
+ description: "使用场景:定时导出/订阅推送/定期拉数/大批量出文件——需异步产出文件并下载时用能力:按魔方规则、定时 cron 或手动触发,把大库数据异步导出为文件,上传 octo/sftp,产出下载 url 供调用方轮询拉取;可创建/修改/列出/删除/触发订阅 job、翻页查询 task。概念:job=订阅定义(数据范围+频率+投递方式,系统按频率生成 cron);task=job 的一次执行,产物与下载地址落在 file_info。边界:①每 token 生效 job 默认≤10;②单次拉数可指定 size,默认且最大 50 万行,超出截断;③投递仅 octo/sftp(kafka 规划中,暂不支持),无主动通知、须轮询 task 取结果。④如果拉取不到数据则不会产生文件上传,也就没有文件url信息"
4
4
  metadata:
5
5
  openclaw:
6
6
  requires: { bins: ['social-neuron-subscription'], skills: [] }
@@ -10,7 +10,81 @@ metadata:
10
10
 
11
11
  > ⚠️ 本技能随该能力 CLI 版本更新。若刚升级过 CLI、重装过 skill,或调用返回 426(版本过旧需升级),请**重新加载本 SKILL.md 与 manifest** 再用——以最新文件为准。
12
12
 
13
- > 命令为一能力一命令:`social-neuron-subscription <action> --flags`(无子命令层)。对象类型(如 notify/target)用一个 JSON 串:`--notify '{...}'`。
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
 
@@ -29,6 +103,7 @@ metadata:
29
103
  | --udate | udate | string | 否 | - | **更新时间**口径(选填,不传=该维度不过滤)。在发布时间窗**之内**再 AND 一层:更新时间落在本区间的数据。记法 **T-M~T-N**:从 M 天前到 N 天前的闭区间(要求 M>=N),端点按天计,基准日是本次触发的 task_time(东八区;定时=cron 槽位日,手动=触发当日)。**两个端点都必须写全,不接受单值简写** —— 「昨天」写 T-1~T-1,写 T-1 会被拒绝。举例:T-0~T-0=今天;T-1~T-1=昨天;T-2~T-2=前天;T-1~T-0=昨天到今天(2天);T-2~T-1=前天到昨天(2天,口语的「前两天」);T-3~T-1=前三天(3天);T-7~T-3=7天前到3天前(5天);T-7~T-1=近7天到昨天(7天)。跨度最多 120 天、偏移最多 720 天(近两年),M<N 或超限直接报错(不会静默回落)。 |
30
104
  | --frequency | frequency | string | 是 | minLen=1, maxLen=64 | 自然语言同步频率,系统转 cron(东八区)。可指定具体时间、并**支持一天多次**:如「每天9点」「每天9点和15点」(一天跑两次)「每周一9点」「每小时」「每30分钟」。无法解析为 cron 会被拒绝。 |
31
105
  | --fields | fields | list[string] | 否 | minLen=1 | 拉取/导出字段;不填拉全量 |
106
+ | --size | size | integer | 否 | min=1, max=500000 | 每次执行最多导出的记录行数,默认且最大为 500000 |
32
107
  | --target | target | OctoTarget、SftpTarget、KafkaTarget | 否 | - | 投递目标;省略默认 octo(json)。本期支持 octo(上传存储→CDN url)与 sftp(账密上传远端,路径/文件名可用日期变量);kafka: 暂未实现。 |
33
108
 
34
109
  **对象 `KafkaTarget`** —— kafka(规划):把记录流投送到 topic(无文件)。
@@ -48,7 +123,8 @@ metadata:
48
123
  | format | enum(csv、excel、json) | 否 | - | 文件格式:csv/excel/json(默认 json) |
49
124
 
50
125
  **对象 `SftpTarget`** —— sftp(本期):把导出文件上传到 SFTP 远端目录。
51
- 目录与文件名都支持日期变量 ``yyyyMMdd`` / ``yyyyMM`` / ``yyyy``,同一个 task 重跑多少次,远端路径与文件名都不变。
126
+ 目录与文件名都支持日期变量 ``yyyyMMdd`` / ``yyyyMM`` / ``yyyy``,同一个 task
127
+ 重跑多少次,远端路径与文件名都不变。
52
128
 
53
129
  | 键 | 类型 | 必填 | 约束 | 说明 |
54
130
  |---|---|---|---|---|
@@ -73,6 +149,7 @@ metadata:
73
149
  | idate | string | 是 | - | 入库时间口径(相对,T-M~T-N);null=该维度不过滤 |
74
150
  | udate | string | 是 | - | 更新时间口径(相对,T-M~T-N);null=该维度不过滤 |
75
151
  | fields | list[string] | 是 | - | 拉取/导出字段(空=全量) |
152
+ | size | integer | 是 | - | 每次执行最多导出的记录行数,范围 1–500000 |
76
153
  | frequency | string | 是 | - | 自然语言频率 |
77
154
  | cron | string | 是 | - | 系统由 frequency 转换的 cron |
78
155
  | target_type | string | 是 | - | 投递类型:octo、sftp、kafka(暂不支持) |
@@ -81,7 +158,7 @@ metadata:
81
158
  | created_at | string | 是 | - | 创建时间(ISO8601,UTC+8) |
82
159
  | updated_at | string | 是 | - | 更新时间(ISO8601,UTC+8) |
83
160
 
84
- 示例:social-neuron-subscription job-create --name <v> --rule-ids <v> --source <v> --published <v> --frequency <v>
161
+ 示例:social-neuron-subscription job-create --name <v> --rule-ids '["a","b"]' --source '["a","b"]' --published <v> --frequency <v>
85
162
 
86
163
  ### social-neuron-subscription job-delete
87
164
  软删除本人 job:置 active=0(记录保留,可 job_update 恢复)。
@@ -129,6 +206,7 @@ metadata:
129
206
  | idate | string | 是 | - | 入库时间口径(相对,T-M~T-N);null=该维度不过滤 |
130
207
  | udate | string | 是 | - | 更新时间口径(相对,T-M~T-N);null=该维度不过滤 |
131
208
  | fields | list[string] | 是 | - | 拉取/导出字段(空=全量) |
209
+ | size | integer | 是 | - | 每次执行最多导出的记录行数,范围 1–500000 |
132
210
  | frequency | string | 是 | - | 自然语言频率 |
133
211
  | cron | string | 是 | - | 系统由 frequency 转换的 cron |
134
212
  | target_type | string | 是 | - | 投递类型:octo、sftp、kafka(暂不支持) |
@@ -137,12 +215,12 @@ metadata:
137
215
  | created_at | string | 是 | - | 创建时间(ISO8601,UTC+8) |
138
216
  | updated_at | string | 是 | - | 更新时间(ISO8601,UTC+8) |
139
217
 
140
- 示例:social-neuron-subscription job-list
218
+ 示例:social-neuron-subscription job-list
141
219
 
142
220
  ### social-neuron-subscription job-trigger
143
221
  临时触发一次本人 job,落一条 ik_subscription_task(manual);默认用 job 存的 target,可覆盖。
144
222
 
145
- 落 pending task 后触发 Airflow DAG(``social_neuron_subscription``)跑取数→组装→投递(上传取 url);
223
+ 落 pending task 后触发 Airflow DAG(``social_capability_subscription``)跑取数→组装→投递(上传取 url);
146
224
  未配 airflow_base_url 时(本地/测试)触发被跳过,仅留 pending task。**无通知**:结果落
147
225
  ``file_info.url``,调用方轮询 ``task-list`` 拉取。
148
226
 
@@ -158,6 +236,7 @@ metadata:
158
236
  | --udate-start | udate_start | string | 否 | - | **更新时间**下界(绝对日期 yyyy-MM-dd,东八区);与 udate_end 同传同不传;不传则按 job 上的 udate 相对口径折算,job 也没配=该维度不过滤 |
159
237
  | --udate-end | udate_end | string | 否 | - | **更新时间**上界(绝对日期 yyyy-MM-dd,东八区,该日整天含在窗内) |
160
238
  | --target | target | OctoTarget、SftpTarget、KafkaTarget | 否 | - | 覆盖本次投递目标 |
239
+ | --size | size | integer | 否 | min=1, max=500000 | 覆盖本次导出行数;不传则继承 job 的 size,最大 500000 |
161
240
 
162
241
  **对象 `KafkaTarget`** —— kafka(规划):把记录流投送到 topic(无文件)。
163
242
 
@@ -176,7 +255,8 @@ metadata:
176
255
  | format | enum(csv、excel、json) | 否 | - | 文件格式:csv/excel/json(默认 json) |
177
256
 
178
257
  **对象 `SftpTarget`** —— sftp(本期):把导出文件上传到 SFTP 远端目录。
179
- 目录与文件名都支持日期变量 ``yyyyMMdd`` / ``yyyyMM`` / ``yyyy``,同一个 task 重跑多少次,远端路径与文件名都不变。
258
+ 目录与文件名都支持日期变量 ``yyyyMMdd`` / ``yyyyMM`` / ``yyyy``,同一个 task
259
+ 重跑多少次,远端路径与文件名都不变。
180
260
 
181
261
  | 键 | 类型 | 必填 | 约束 | 说明 |
182
262
  |---|---|---|---|---|
@@ -213,6 +293,7 @@ metadata:
213
293
  | --idate | idate | string | 否 | - | **入库时间**口径。不传=不改;传空串 ""=**清除**该维度(恢复成不过滤)。记法 **T-M~T-N**:从 M 天前到 N 天前的闭区间(要求 M>=N),端点按天计,基准日是本次触发的 task_time(东八区;定时=cron 槽位日,手动=触发当日)。**两个端点都必须写全,不接受单值简写** —— 「昨天」写 T-1~T-1,写 T-1 会被拒绝。举例:T-0~T-0=今天;T-1~T-1=昨天;T-2~T-2=前天;T-1~T-0=昨天到今天(2天);T-2~T-1=前天到昨天(2天,口语的「前两天」);T-3~T-1=前三天(3天);T-7~T-3=7天前到3天前(5天);T-7~T-1=近7天到昨天(7天)。跨度最多 120 天、偏移最多 720 天(近两年),M<N 或超限直接报错(不会静默回落)。 |
214
294
  | --udate | udate | string | 否 | - | **更新时间**口径。不传=不改;传空串 ""=**清除**该维度(恢复成不过滤)。记法 **T-M~T-N**:从 M 天前到 N 天前的闭区间(要求 M>=N),端点按天计,基准日是本次触发的 task_time(东八区;定时=cron 槽位日,手动=触发当日)。**两个端点都必须写全,不接受单值简写** —— 「昨天」写 T-1~T-1,写 T-1 会被拒绝。举例:T-0~T-0=今天;T-1~T-1=昨天;T-2~T-2=前天;T-1~T-0=昨天到今天(2天);T-2~T-1=前天到昨天(2天,口语的「前两天」);T-3~T-1=前三天(3天);T-7~T-3=7天前到3天前(5天);T-7~T-1=近7天到昨天(7天)。跨度最多 120 天、偏移最多 720 天(近两年),M<N 或超限直接报错(不会静默回落)。 |
215
295
  | --fields | fields | list[string] | 否 | minLen=1 | 整组全量覆盖 |
296
+ | --size | size | integer | 否 | min=1, max=500000 | 每次执行最多导出的记录行数;不传则不修改,最大 500000 |
216
297
  | --frequency | frequency | string | 否 | minLen=1, maxLen=64 | 传入则按新频率重算 cron(用法同创建;支持一天多次,如「每天9点和15点」);无法解析为 cron 会被拒绝 |
217
298
  | --target | target | OctoTarget、SftpTarget、KafkaTarget | 否 | - | 整体替换投递目标 |
218
299
  | --active | active | enum(0、1) | 否 | - | 启停:1 / 0 |
@@ -234,7 +315,8 @@ metadata:
234
315
  | format | enum(csv、excel、json) | 否 | - | 文件格式:csv/excel/json(默认 json) |
235
316
 
236
317
  **对象 `SftpTarget`** —— sftp(本期):把导出文件上传到 SFTP 远端目录。
237
- 目录与文件名都支持日期变量 ``yyyyMMdd`` / ``yyyyMM`` / ``yyyy``,同一个 task 重跑多少次,远端路径与文件名都不变。
318
+ 目录与文件名都支持日期变量 ``yyyyMMdd`` / ``yyyyMM`` / ``yyyy``,同一个 task
319
+ 重跑多少次,远端路径与文件名都不变。
238
320
 
239
321
  | 键 | 类型 | 必填 | 约束 | 说明 |
240
322
  |---|---|---|---|---|
@@ -259,6 +341,7 @@ metadata:
259
341
  | idate | string | 是 | - | 入库时间口径(相对,T-M~T-N);null=该维度不过滤 |
260
342
  | udate | string | 是 | - | 更新时间口径(相对,T-M~T-N);null=该维度不过滤 |
261
343
  | fields | list[string] | 是 | - | 拉取/导出字段(空=全量) |
344
+ | size | integer | 是 | - | 每次执行最多导出的记录行数,范围 1–500000 |
262
345
  | frequency | string | 是 | - | 自然语言频率 |
263
346
  | cron | string | 是 | - | 系统由 frequency 转换的 cron |
264
347
  | target_type | string | 是 | - | 投递类型:octo、sftp、kafka(暂不支持) |
@@ -298,6 +381,7 @@ metadata:
298
381
  |---|---|---|---|---|
299
382
  | task_id | integer | 是 | - | task ID |
300
383
  | job_id | integer | 是 | - | 所属 job |
384
+ | size | integer | 是 | - | 本次执行最多导出的记录行数 |
301
385
  | status | string | 是 | - | pending/running/success/failed/terminated |
302
386
  | file_info | object | 否 | - | 产物 + 下载入口,含 type 标识:octo→{type,name,format,rows,url};sftp→{...,url,path};kafka→{type,brokers,topic} |
303
387
  | error_msg | string | 否 | - | 失败原因(status=failed 时给出;成功/进行中为空) |