tickflow-assist 0.2.16 → 0.2.17

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  基于 [OpenClaw](https://openclaw.ai) 的 A 股监控与分析插件。它使用 [TickFlow](https://tickflow.org/auth/register?ref=BUJ54JEDGE) 获取行情与财务数据,结合 LLM 生成技术面、基本面、资讯面的综合判断,并把结果持久化到本地 LanceDB。
4
4
 
5
- 最近更新:`v0.2.16` 移除社区发布包中的 `child_process` 依赖,以兼容 OpenClaw `v2026.3.31` 的危险代码扫描;源码一键安装脚本仍保留自动依赖安装与 Gateway 配置能力。
5
+ 最近更新:`v0.2.17` 补充 Linux / macOS 的字体安装命令,`configure-openclaw` 会自动把被旧版本钉死的 ClawHub install spec 归一化为 `clawhub:tickflow-assist`,并将空自选时的 `ta_startmonitor` 失败改为可见提示。
6
6
 
7
7
  当前主线按 OpenClaw `v2026.3.31+` 对齐。
8
8
 
@@ -21,6 +21,7 @@ openclaw gateway restart
21
21
 
22
22
  安装阶段允许先落插件,再通过第二条命令写入 `tickflowApiKey`、`llmApiKey` 等正式配置。
23
23
  `configure-openclaw` 会写入 `~/.openclaw/openclaw.json` 中的 `plugins.entries["tickflow-assist"].config`,并打印后续建议执行的命令;它不再自动执行 `openclaw`、`uv` 或系统包安装命令。
24
+ 如果检测到 `plugins.installs["tickflow-assist"]` 来自 `clawhub`,向导还会把被旧版本钉死的 `spec` 归一化为 `clawhub:tickflow-assist`,避免后续升级继续锁在旧版本。
24
25
 
25
26
  如果你希望先审阅配置,再只打印最少的后续步骤,可使用:
26
27
 
@@ -28,7 +29,31 @@ openclaw gateway restart
28
29
  npx -y tickflow-assist configure-openclaw --no-enable --no-restart
29
30
  ```
30
31
 
31
- 如果你在 Linux 上需要 PNG 告警卡正常显示中文,请额外手动安装 `fontconfig` 与 Noto CJK 一类中文字体。
32
+ 如果你在 Linux 或 macOS 上需要 PNG 告警卡正常显示中文,请额外手动安装 `fontconfig` 与 Noto CJK 一类中文字体,例如:
33
+
34
+ ```bash
35
+ # Debian / Ubuntu
36
+ sudo apt-get update
37
+ sudo apt-get install -y fontconfig fonts-noto-cjk
38
+ fc-cache -fv
39
+
40
+ # RHEL / Fedora / Rocky / AlmaLinux
41
+ sudo dnf install -y fontconfig google-noto-sans-cjk-ttc-fonts
42
+ fc-cache -fv
43
+
44
+ # Arch / Manjaro
45
+ sudo pacman -Sy --noconfirm fontconfig noto-fonts-cjk
46
+ fc-cache -fv
47
+
48
+ # Alpine
49
+ sudo apk add fontconfig font-noto-cjk
50
+ fc-cache -fv
51
+
52
+ # macOS (Homebrew)
53
+ brew install fontconfig
54
+ brew install --cask font-noto-sans-cjk
55
+ fc-cache -fv
56
+ ```
32
57
 
33
58
  社区安装后的升级方式:
34
59
 
@@ -38,7 +38,7 @@ Options:
38
38
  --no-enable Do not print 'openclaw plugins enable' in next steps
39
39
  --no-restart Do not print 'openclaw gateway restart' in next steps
40
40
  --no-python-setup Do not print Python dependency setup guidance
41
- --no-font-setup Do not print Linux Chinese font setup guidance
41
+ --no-font-setup Do not print Chinese font setup guidance
42
42
  --openclaw-bin <path> OpenClaw CLI binary name used in printed next steps
43
43
  --tickflow-api-key <key>
44
44
  --tickflow-api-key-level <Free|Start|Pro|Expert>
@@ -501,6 +501,34 @@ function assertRequired(config) {
501
501
  throw new Error("llmApiKey is required");
502
502
  }
503
503
  }
504
+ function normalizeCommunityInstallSpec(root) {
505
+ const plugins = root.plugins;
506
+ if (typeof plugins !== "object" || plugins === null || Array.isArray(plugins)) {
507
+ return false;
508
+ }
509
+ const installs = plugins.installs;
510
+ if (typeof installs !== "object" || installs === null || Array.isArray(installs)) {
511
+ return false;
512
+ }
513
+ const installEntry = installs[PLUGIN_ID];
514
+ if (typeof installEntry !== "object" || installEntry === null || Array.isArray(installEntry)) {
515
+ return false;
516
+ }
517
+ const source = stringValue(installEntry.source).toLowerCase();
518
+ if (source !== "clawhub") {
519
+ return false;
520
+ }
521
+ const canonicalSpec = `clawhub:${PLUGIN_ID}`;
522
+ const currentSpec = stringValue(installEntry.spec);
523
+ if (currentSpec === canonicalSpec) {
524
+ return false;
525
+ }
526
+ if (!currentSpec || currentSpec.startsWith(`${canonicalSpec}@`)) {
527
+ installEntry.spec = canonicalSpec;
528
+ return true;
529
+ }
530
+ return false;
531
+ }
504
532
  async function ensurePathNotice(targetPath, label) {
505
533
  try {
506
534
  await access(targetPath);
@@ -644,6 +672,13 @@ function getManualFontCommands(distro) {
644
672
  ];
645
673
  }
646
674
  }
675
+ function getManualMacosFontCommands() {
676
+ return [
677
+ "brew install fontconfig",
678
+ "brew install --cask font-noto-sans-cjk",
679
+ "fc-cache -fv",
680
+ ];
681
+ }
647
682
  function printNextSteps(options, config) {
648
683
  console.log("");
649
684
  console.log("接下来的命令需要你手动执行。");
@@ -654,9 +689,13 @@ function printNextSteps(options, config) {
654
689
  console.log(" uv sync");
655
690
  step += 1;
656
691
  }
657
- if (options.fontSetup && process.platform === "linux") {
658
- console.log(`${step}. 如需 PNG 告警卡正常显示中文,请按你的 Linux 发行版安装字体`);
659
- for (const command of getManualFontCommands(detectLinuxDistro())) {
692
+ if (options.fontSetup && (process.platform === "linux" || process.platform === "darwin")) {
693
+ const commands = process.platform === "darwin"
694
+ ? getManualMacosFontCommands()
695
+ : getManualFontCommands(detectLinuxDistro());
696
+ const platformLabel = process.platform === "darwin" ? "macOS" : "Linux 发行版";
697
+ console.log(`${step}. 如需 PNG 告警卡正常显示中文,请按你的 ${platformLabel} 安装字体`);
698
+ for (const command of commands) {
660
699
  console.log(` ${command}`);
661
700
  }
662
701
  step += 1;
@@ -685,6 +724,7 @@ async function configureOpenClaw(options) {
685
724
  await ensurePathNotice(config.calendarFile, "calendarFile");
686
725
  await ensurePathNotice(config.pythonWorkdir, "pythonWorkdir");
687
726
  applyPluginConfig(root, config, target);
727
+ const normalizedInstallSpec = normalizeCommunityInstallSpec(root);
688
728
  const backupPath = await writeConfig(configPath, root);
689
729
  console.log("");
690
730
  console.log(`Updated OpenClaw config: ${configPath}`);
@@ -693,6 +733,9 @@ async function configureOpenClaw(options) {
693
733
  }
694
734
  console.log(`Plugin dir: ${pluginDir}`);
695
735
  console.log(`Allowlist target: ${target.type === "global" ? "global tools" : `agent:${target.id}`}`);
736
+ if (normalizedInstallSpec) {
737
+ console.log(`Community install spec normalized: clawhub:${PLUGIN_ID}`);
738
+ }
696
739
  printNextSteps(options, config);
697
740
  }
698
741
  async function main() {
@@ -36,6 +36,19 @@ function parseRequiredSymbol(args, usage) {
36
36
  async function runToolText(tool, rawInput) {
37
37
  return tool.run({ rawInput });
38
38
  }
39
+ function formatCommandError(error) {
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ const normalized = message.trim() || "未知错误";
42
+ return `⚠️ ${normalized}`;
43
+ }
44
+ async function runCommandText(task) {
45
+ try {
46
+ return { text: await task() };
47
+ }
48
+ catch (error) {
49
+ return { text: formatCommandError(error) };
50
+ }
51
+ }
39
52
  async function renderWatchlistDebug(app) {
40
53
  const snapshot = await buildWatchlistDebugSnapshot(app);
41
54
  const lines = [
@@ -79,148 +92,114 @@ export function registerPluginCommands(api, tools, app) {
79
92
  description: "添加自选股,不经过 AI 对话。用法: /ta_addstock <symbol> [costPrice] [count]",
80
93
  acceptsArgs: true,
81
94
  requireAuth: true,
82
- handler: async ({ args }) => ({
83
- text: await runToolText(addStock, parseAddStockArgs(args)),
84
- }),
95
+ handler: async ({ args }) => runCommandText(() => runToolText(addStock, parseAddStockArgs(args))),
85
96
  },
86
97
  {
87
98
  name: "ta_rmstock",
88
99
  description: "删除自选股,不经过 AI 对话。用法: /ta_rmstock <symbol>",
89
100
  acceptsArgs: true,
90
101
  requireAuth: true,
91
- handler: async ({ args }) => ({
92
- text: await runToolText(removeStock, {
93
- symbol: parseRequiredSymbol(args, "/ta_rmstock <symbol>"),
94
- }),
95
- }),
102
+ handler: async ({ args }) => runCommandText(() => runToolText(removeStock, {
103
+ symbol: parseRequiredSymbol(args, "/ta_rmstock <symbol>"),
104
+ })),
96
105
  },
97
106
  {
98
107
  name: "ta_analyze",
99
108
  description: "分析单只股票,不经过 AI 对话。用法: /ta_analyze <symbol>",
100
109
  acceptsArgs: true,
101
110
  requireAuth: true,
102
- handler: async ({ args }) => ({
103
- text: await runToolText(analyze, {
104
- symbol: parseRequiredSymbol(args, "/ta_analyze <symbol>"),
105
- }),
106
- }),
111
+ handler: async ({ args }) => runCommandText(() => runToolText(analyze, {
112
+ symbol: parseRequiredSymbol(args, "/ta_analyze <symbol>"),
113
+ })),
107
114
  },
108
115
  {
109
116
  name: "ta_backtest",
110
117
  description: "回测活动价位,不经过 AI 对话。用法: /ta_backtest [symbol] [recentLimit]",
111
118
  acceptsArgs: true,
112
119
  requireAuth: true,
113
- handler: async ({ args }) => ({
114
- text: await runToolText(backtestKeyLevels, args?.trim() || undefined),
115
- }),
120
+ handler: async ({ args }) => runCommandText(() => runToolText(backtestKeyLevels, args?.trim() || undefined)),
116
121
  },
117
122
  {
118
123
  name: "ta_viewanalysis",
119
124
  description: "查看单只股票最近一次保存的分析结果,不经过 AI 对话。用法: /ta_viewanalysis <symbol>",
120
125
  acceptsArgs: true,
121
126
  requireAuth: true,
122
- handler: async ({ args }) => ({
123
- text: await runToolText(viewAnalysis, {
124
- symbol: parseRequiredSymbol(args, "/ta_viewanalysis <symbol>"),
125
- }),
126
- }),
127
+ handler: async ({ args }) => runCommandText(() => runToolText(viewAnalysis, {
128
+ symbol: parseRequiredSymbol(args, "/ta_viewanalysis <symbol>"),
129
+ })),
127
130
  },
128
131
  {
129
132
  name: "ta_watchlist",
130
133
  description: "查看当前自选列表,不经过 AI 对话。",
131
134
  requireAuth: true,
132
- handler: async () => ({
133
- text: await runToolText(listWatchlist),
134
- }),
135
+ handler: async () => runCommandText(() => runToolText(listWatchlist)),
135
136
  },
136
137
  {
137
138
  name: "ta_refreshnames",
138
139
  description: "刷新自选股名称,不经过 AI 对话。",
139
140
  requireAuth: true,
140
- handler: async () => ({
141
- text: await runToolText(refreshWatchlistNames),
142
- }),
141
+ handler: async () => runCommandText(() => runToolText(refreshWatchlistNames)),
143
142
  },
144
143
  {
145
144
  name: "ta_refreshprofiles",
146
145
  description: "刷新自选股行业分类与概念板块,不经过 AI 对话。用法: /ta_refreshprofiles [symbol]",
147
146
  acceptsArgs: true,
148
147
  requireAuth: true,
149
- handler: async ({ args }) => ({
150
- text: await runToolText(refreshWatchlistProfiles, args?.trim() || undefined),
151
- }),
148
+ handler: async ({ args }) => runCommandText(() => runToolText(refreshWatchlistProfiles, args?.trim() || undefined)),
152
149
  },
153
150
  {
154
151
  name: "ta_startmonitor",
155
152
  description: "启动实时监控,不经过 AI 对话。",
156
153
  requireAuth: true,
157
- handler: async () => ({
158
- text: await runToolText(startMonitor),
159
- }),
154
+ handler: async () => runCommandText(() => runToolText(startMonitor)),
160
155
  },
161
156
  {
162
157
  name: "ta_stopmonitor",
163
158
  description: "停止实时监控,不经过 AI 对话。",
164
159
  requireAuth: true,
165
- handler: async () => ({
166
- text: await runToolText(stopMonitor),
167
- }),
160
+ handler: async () => runCommandText(() => runToolText(stopMonitor)),
168
161
  },
169
162
  {
170
163
  name: "ta_monitorstatus",
171
164
  description: "查看实时监控状态,不经过 AI 对话。",
172
165
  requireAuth: true,
173
- handler: async () => ({
174
- text: await runToolText(monitorStatus),
175
- }),
166
+ handler: async () => runCommandText(() => runToolText(monitorStatus)),
176
167
  },
177
168
  {
178
169
  name: "ta_startdailyupdate",
179
170
  description: "启动定时日更任务,不经过 AI 对话。",
180
171
  requireAuth: true,
181
- handler: async () => ({
182
- text: await runToolText(startDailyUpdate),
183
- }),
172
+ handler: async () => runCommandText(() => runToolText(startDailyUpdate)),
184
173
  },
185
174
  {
186
175
  name: "ta_stopdailyupdate",
187
176
  description: "停止定时日更任务,不经过 AI 对话。",
188
177
  requireAuth: true,
189
- handler: async () => ({
190
- text: await runToolText(stopDailyUpdate),
191
- }),
178
+ handler: async () => runCommandText(() => runToolText(stopDailyUpdate)),
192
179
  },
193
180
  {
194
181
  name: "ta_updateall",
195
182
  description: "立即执行一次完整日更,不经过 AI 对话。",
196
183
  requireAuth: true,
197
- handler: async () => ({
198
- text: await runToolText(updateAll),
199
- }),
184
+ handler: async () => runCommandText(() => runToolText(updateAll)),
200
185
  },
201
186
  {
202
187
  name: "ta_dailyupdatestatus",
203
188
  description: "查看定时日更状态,不经过 AI 对话。",
204
189
  requireAuth: true,
205
- handler: async () => ({
206
- text: await runToolText(dailyUpdateStatus),
207
- }),
190
+ handler: async () => runCommandText(() => runToolText(dailyUpdateStatus)),
208
191
  },
209
192
  {
210
193
  name: "ta_testalert",
211
194
  description: "发送一条文本 + PNG 测试告警,不经过 AI 对话。",
212
195
  requireAuth: true,
213
- handler: async () => ({
214
- text: await runToolText(testAlert),
215
- }),
196
+ handler: async () => runCommandText(() => runToolText(testAlert)),
216
197
  },
217
198
  {
218
199
  name: "ta_debug",
219
200
  description: "查看 TickFlow 插件运行时调试信息。",
220
201
  requireAuth: true,
221
- handler: async () => ({
222
- text: await renderWatchlistDebug(app),
223
- }),
202
+ handler: async () => runCommandText(() => renderWatchlistDebug(app)),
224
203
  },
225
204
  ];
226
205
  for (const command of commands) {
@@ -5,9 +5,15 @@ export function startMonitorTool(monitorService, runtime) {
5
5
  optional: true,
6
6
  async run() {
7
7
  if (runtime.pluginManagedServices) {
8
- const result = await monitorService.enableManagedLoop();
8
+ let result;
9
+ try {
10
+ result = await monitorService.enableManagedLoop();
11
+ }
12
+ catch (error) {
13
+ return formatManagedStartError(error);
14
+ }
9
15
  if (!result.started) {
10
- return await monitorService.getStatusReport();
16
+ return await buildManagedAlreadyRunningSummary(monitorService);
11
17
  }
12
18
  return [
13
19
  "✅ TickFlow 实时监控已启动",
@@ -20,7 +26,12 @@ export function startMonitorTool(monitorService, runtime) {
20
26
  if (currentState.running
21
27
  && currentState.workerPid != null
22
28
  && isPidAlive(currentState.workerPid)) {
23
- return await monitorService.getStatusReport();
29
+ return [
30
+ "✅ TickFlow 实时监控已在运行",
31
+ "运行方式: manual_loop",
32
+ `PID: ${currentState.workerPid}`,
33
+ "说明: 本地监控循环已存在,无需重复启动。",
34
+ ].join("\n");
24
35
  }
25
36
  const summary = await monitorService.start();
26
37
  return [
@@ -31,6 +42,29 @@ export function startMonitorTool(monitorService, runtime) {
31
42
  },
32
43
  };
33
44
  }
45
+ async function buildManagedAlreadyRunningSummary(monitorService) {
46
+ const state = await monitorService.getState();
47
+ return [
48
+ "✅ TickFlow 实时监控已在运行",
49
+ "运行方式: plugin_service",
50
+ `最近心跳: ${state.lastHeartbeatAt ?? "暂无"}`,
51
+ "说明: 后台服务按配置间隔轮询,交易时段自动执行监控。",
52
+ ].join("\n");
53
+ }
54
+ function formatManagedStartError(error) {
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ if (message.includes("关注列表为空")) {
57
+ return [
58
+ "⚠️ 无法启动实时监控",
59
+ "原因: 关注列表为空,请先添加至少一只自选股。",
60
+ "示例: /ta_addstock 000001 10.50",
61
+ ].join("\n");
62
+ }
63
+ return [
64
+ "⚠️ 启动实时监控失败",
65
+ `原因: ${message}`,
66
+ ].join("\n");
67
+ }
34
68
  function isPidAlive(pid) {
35
69
  try {
36
70
  process.kill(pid, 0);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "tickflow-assist",
3
3
  "name": "TickFlow Assist",
4
- "version": "0.2.16",
4
+ "version": "0.2.17",
5
5
  "description": "A-share watchlist analysis, monitoring, and alert delivery powered by TickFlow and OpenClaw.",
6
6
  "skills": [
7
7
  "skills"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickflow-assist",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
4
4
  "description": "OpenClaw smart stock plugin for A-share investing and watchlist workflows, powered by TickFlow API for realtime monitoring, post-close review, multi-dimensional analysis, key level tracking, and alerts. 面向 A 股投资与盯盘场景的 OpenClaw 智能股票插件,基于 TickFlow API 提供实时监控、收盘后复盘、多维综合分析、关键价位跟踪与告警能力。",
5
5
  "license": "MIT",
6
6
  "type": "module",