tickflow-assist 0.2.15 → 0.2.16

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.15` 重新发布 npm 包以刷新包页 README 展示;功能与运行逻辑相对 `v0.2.14` 无新增变更。
5
+ 最近更新:`v0.2.16` 移除社区发布包中的 `child_process` 依赖,以兼容 OpenClaw `v2026.3.31` 的危险代码扫描;源码一键安装脚本仍保留自动依赖安装与 Gateway 配置能力。
6
6
 
7
7
  当前主线按 OpenClaw `v2026.3.31+` 对齐。
8
8
 
@@ -13,23 +13,23 @@
13
13
  ```bash
14
14
  openclaw plugins install tickflow-assist
15
15
  npx -y tickflow-assist configure-openclaw
16
+ cd ~/.openclaw/extensions/tickflow-assist/python && uv sync
17
+ openclaw plugins enable tickflow-assist
18
+ openclaw config validate
19
+ openclaw gateway restart
16
20
  ```
17
21
 
18
22
  安装阶段允许先落插件,再通过第二条命令写入 `tickflowApiKey`、`llmApiKey` 等正式配置。
19
- 在 Linux 上,`configure-openclaw` 还会 best-effort 安装 PNG 告警卡所需的中文字体;如需跳过,可追加 `--no-font-setup`。
20
-
21
- 第二条命令会写入 `~/.openclaw/openclaw.json` 中的 `plugins.entries["tickflow-assist"].config`,并默认执行:
23
+ `configure-openclaw` 会写入 `~/.openclaw/openclaw.json` 中的 `plugins.entries["tickflow-assist"].config`,并打印后续建议执行的命令;它不再自动执行 `openclaw`、`uv` 或系统包安装命令。
22
24
 
23
- - `openclaw plugins enable tickflow-assist`
24
- - `openclaw config validate`
25
- - `openclaw gateway restart`
26
-
27
- 如果你希望先审阅配置再手动启用或重启,可使用:
25
+ 如果你希望先审阅配置,再只打印最少的后续步骤,可使用:
28
26
 
29
27
  ```bash
30
28
  npx -y tickflow-assist configure-openclaw --no-enable --no-restart
31
29
  ```
32
30
 
31
+ 如果你在 Linux 上需要 PNG 告警卡正常显示中文,请额外手动安装 `fontconfig` 与 Noto CJK 一类中文字体。
32
+
33
33
  社区安装后的升级方式:
34
34
 
35
35
  ```bash
@@ -1,4 +1,3 @@
1
- import type { PluginConfig } from "../config/schema.js";
2
1
  import { UpdateService } from "../services/update-service.js";
3
2
  import { AlertService } from "../services/alert-service.js";
4
3
  import { PostCloseReviewService } from "../services/post-close-review-service.js";
@@ -16,10 +15,6 @@ export declare class DailyUpdateWorker {
16
15
  constructor(updateService: UpdateService, postCloseReviewService: PostCloseReviewService | null, tradingCalendarService: TradingCalendarService, baseDir: string, alertService: AlertService, notifyEnabled: boolean, configSource: "openclaw_plugin" | "local_config", intervalMs?: number);
17
16
  run(force?: boolean): Promise<string>;
18
17
  runLoop(signal?: AbortSignal, runtimeHost?: "project_scheduler" | "plugin_service", runtimeConfigSource?: "openclaw_plugin" | "local_config"): Promise<void>;
19
- ensureLoopRunning(config: PluginConfig, configSource: "openclaw_plugin" | "local_config"): Promise<{
20
- started: boolean;
21
- pid: number | null;
22
- }>;
23
18
  stopLoop(): Promise<{
24
19
  stopped: boolean;
25
20
  pid: number | null;
@@ -48,3 +43,4 @@ export declare class DailyUpdateWorker {
48
43
  private maybeSendDailyUpdateNotification;
49
44
  private maybeSendReviewNotification;
50
45
  }
46
+ export declare function isPidAlive(pid: number): boolean;
@@ -2,7 +2,6 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { chinaToday, formatChinaDateTime } from "../utils/china-time.js";
4
4
  import { sleepWithAbort } from "../utils/abortable-sleep.js";
5
- import { isPidAlive, spawnDailyUpdateLoop } from "../runtime/daily-update-process.js";
6
5
  const DAILY_UPDATE_READY_TIME = "15:25";
7
6
  const POST_CLOSE_REVIEW_READY_TIME = "20:00";
8
7
  const DEFAULT_STATE = {
@@ -67,19 +66,6 @@ export class DailyUpdateWorker {
67
66
  await sleepWithAbort(getNextAlignedDelayMs(this.intervalMs), signal);
68
67
  }
69
68
  }
70
- async ensureLoopRunning(config, configSource) {
71
- const state = await this.readState();
72
- if (state.workerPid != null && isPidAlive(state.workerPid)) {
73
- await this.markSchedulerRunning(state.workerPid, state.runtimeConfigSource ?? configSource);
74
- return { started: false, pid: state.workerPid };
75
- }
76
- const workerPid = spawnDailyUpdateLoop(config, configSource);
77
- if (workerPid == null) {
78
- throw new Error("无法启动 TickFlow 日更定时进程");
79
- }
80
- await this.markSchedulerRunning(workerPid, configSource);
81
- return { started: true, pid: workerPid };
82
- }
83
69
  async stopLoop() {
84
70
  const state = await this.readState();
85
71
  const workerPid = state.workerPid;
@@ -422,6 +408,15 @@ export class DailyUpdateWorker {
422
408
  }
423
409
  }
424
410
  }
411
+ export function isPidAlive(pid) {
412
+ try {
413
+ process.kill(pid, 0);
414
+ return true;
415
+ }
416
+ catch {
417
+ return false;
418
+ }
419
+ }
425
420
  function createReviewExecutionOutput(reviewResult) {
426
421
  return {
427
422
  resultType: "success",
package/dist/bootstrap.js CHANGED
@@ -64,6 +64,7 @@ import { testAlertTool } from "./tools/test-alert.tool.js";
64
64
  import { updateAllTool } from "./tools/update-all.tool.js";
65
65
  import { viewAnalysisTool } from "./tools/view-analysis.tool.js";
66
66
  import { backtestKeyLevelsTool } from "./tools/backtest-key-levels.tool.js";
67
+ import { createCommandRunner } from "./runtime/command-runner.js";
67
68
  import { resolvePreferredOpenClawTmpDir } from "./runtime/openclaw-temp-dir.js";
68
69
  import { RealtimeMonitorWorker } from "./background/realtime-monitor.worker.js";
69
70
  import { DailyUpdateWorker } from "./background/daily-update.worker.js";
@@ -74,6 +75,7 @@ export function createAppContext(config, options = {}) {
74
75
  openclawConfig: options.openclawConfig,
75
76
  pluginRuntime: options.pluginRuntime,
76
77
  };
78
+ const runCommandWithTimeout = createCommandRunner(runtime.pluginRuntime);
77
79
  const tickflowClient = new TickFlowClient(config.tickflowApiUrl, config.tickflowApiKey);
78
80
  const database = new Database(config.databasePath);
79
81
  const watchlistRepository = new WatchlistRepository(database);
@@ -110,7 +112,7 @@ export function createAppContext(config, options = {}) {
110
112
  : undefined,
111
113
  });
112
114
  const alertMediaService = new AlertMediaService(config.databasePath, undefined, undefined, resolveAlertMediaTempRootDir());
113
- const indicatorService = new IndicatorService(config.pythonBin, config.pythonArgs, config.pythonWorkdir);
115
+ const indicatorService = new IndicatorService(config.pythonBin, config.pythonArgs, config.pythonWorkdir, runCommandWithTimeout);
114
116
  const watchlistService = new WatchlistService(watchlistRepository, instrumentService, watchlistProfileService);
115
117
  const keyLevelsBacktestService = new KeyLevelsBacktestService(keyLevelsHistoryRepository, klinesRepository, intradayKlinesRepository, watchlistService);
116
118
  const reviewMemoryService = new ReviewMemoryService(keyLevelsBacktestService);
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
3
2
  import { readFileSync } from "node:fs";
4
3
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
5
4
  import os from "node:os";
@@ -36,11 +35,11 @@ Options:
36
35
  --agent <id> Apply tools.allow to a specific agent
37
36
  --global Apply tools.allow to top-level tools config
38
37
  --non-interactive Use existing config / flags only, no prompts
39
- --no-enable Do not run 'openclaw plugins enable'
40
- --no-restart Do not run 'openclaw gateway restart'
41
- --no-python-setup Do not run 'uv sync' for Python dependencies
42
- --no-font-setup Do not try to install Linux Chinese fonts for PNG alerts
43
- --openclaw-bin <path> OpenClaw CLI binary, default: openclaw
38
+ --no-enable Do not print 'openclaw plugins enable' in next steps
39
+ --no-restart Do not print 'openclaw gateway restart' in next steps
40
+ --no-python-setup Do not print Python dependency setup guidance
41
+ --no-font-setup Do not print Linux Chinese font setup guidance
42
+ --openclaw-bin <path> OpenClaw CLI binary name used in printed next steps
44
43
  --tickflow-api-key <key>
45
44
  --tickflow-api-key-level <Free|Start|Pro|Expert>
46
45
  --mx-search-api-key <key>
@@ -586,60 +585,6 @@ async function writeConfig(configPath, root) {
586
585
  await writeFile(configPath, `${JSON.stringify(root, null, 2)}\n`, "utf-8");
587
586
  return backupPath;
588
587
  }
589
- function runOpenClaw(bin, args, description) {
590
- const result = spawnSync(bin, args, { stdio: "inherit" });
591
- if (result.error) {
592
- console.warn(`Warning: failed to run ${description}: ${result.error.message}`);
593
- return;
594
- }
595
- if (result.status !== 0) {
596
- console.warn(`Warning: ${description} exited with status ${result.status}`);
597
- }
598
- }
599
- async function setupPythonDeps(pythonWorkdir, nonInteractive) {
600
- let uvBin = "uv";
601
- try {
602
- const which = spawnSync("which", ["uv"], { encoding: "utf-8" });
603
- if (which.status !== 0) {
604
- console.warn("\n ⚠️ 找不到 uv (Python 包管理工具),已跳过 Python 依赖安装。");
605
- console.warn(" 请先手动安装 uv,再在以下目录执行 `uv sync`:");
606
- console.warn(` ${pythonWorkdir}`);
607
- return;
608
- }
609
- else {
610
- uvBin = which.stdout.trim() || "uv";
611
- }
612
- }
613
- catch {
614
- // fall through with default "uv"
615
- }
616
- console.log(`Setting up Python dependencies in ${pythonWorkdir} ...`);
617
- const result = spawnSync(uvBin, ["sync"], { cwd: pythonWorkdir, stdio: "inherit" });
618
- if (result.error) {
619
- console.warn(`Warning: failed to run uv sync: ${result.error.message}`);
620
- console.warn("Please run 'uv sync' manually in:");
621
- console.warn(` ${pythonWorkdir}`);
622
- return;
623
- }
624
- if (result.status !== 0) {
625
- console.warn(`Warning: uv sync exited with status ${result.status}`);
626
- console.warn("Please check the output above and run 'uv sync' manually if needed in:");
627
- console.warn(` ${pythonWorkdir}`);
628
- return;
629
- }
630
- console.log("Python dependencies installed successfully.");
631
- }
632
- function isCommandAvailable(command) {
633
- const result = spawnSync("which", [command], { stdio: "ignore" });
634
- return result.status === 0;
635
- }
636
- function isRootUser() {
637
- return typeof process.getuid === "function" && process.getuid() === 0;
638
- }
639
- function hasChineseFonts() {
640
- const result = spawnSync("fc-list", [":lang=zh", "family"], { encoding: "utf-8" });
641
- return result.status === 0 && Boolean(result.stdout.trim());
642
- }
643
588
  function detectLinuxDistro() {
644
589
  try {
645
590
  const raw = readFileSync("/etc/os-release", "utf-8");
@@ -699,90 +644,34 @@ function getManualFontCommands(distro) {
699
644
  ];
700
645
  }
701
646
  }
702
- function buildPrivilegedCommand(argv, nonInteractive) {
703
- if (isRootUser()) {
704
- return argv;
705
- }
706
- if (!isCommandAvailable("sudo")) {
707
- return null;
708
- }
709
- return nonInteractive ? ["sudo", "-n", ...argv] : ["sudo", ...argv];
710
- }
711
- function runSetupCommand(argv, description) {
712
- console.log(description);
713
- const result = spawnSync(argv[0], argv.slice(1), { stdio: "inherit" });
714
- if (result.error) {
715
- console.warn(`Warning: failed to run ${description}: ${result.error.message}`);
716
- return false;
717
- }
718
- if (result.status !== 0) {
719
- console.warn(`Warning: ${description} exited with status ${result.status}`);
720
- return false;
721
- }
722
- return true;
723
- }
724
- async function setupLinuxChineseFonts(nonInteractive) {
725
- if (process.platform !== "linux") {
726
- return;
727
- }
728
- if (!isCommandAvailable("fc-list")) {
729
- console.warn("Warning: fontconfig is not installed; PNG alerts may not render Chinese text correctly.");
730
- }
731
- else if (hasChineseFonts()) {
732
- console.log("Chinese fonts detected for PNG alert cards.");
733
- return;
734
- }
735
- const distro = detectLinuxDistro();
736
- console.log("Chinese fonts not detected. TickFlow Assist will try to install Noto CJK fonts for PNG alert cards.");
737
- const attempts = [];
738
- switch (distro) {
739
- case "debian":
740
- attempts.push(["apt-get", "update"]);
741
- attempts.push(["apt-get", "install", "-y", "fontconfig", "fonts-noto-cjk"]);
742
- break;
743
- case "rhel":
744
- attempts.push(["dnf", "install", "-y", "fontconfig", "google-noto-sans-cjk-ttc-fonts"]);
745
- attempts.push(["dnf", "install", "-y", "fontconfig", "google-noto-cjk-fonts"]);
746
- attempts.push(["yum", "install", "-y", "fontconfig", "google-noto-sans-cjk-ttc-fonts"]);
747
- attempts.push(["yum", "install", "-y", "fontconfig", "google-noto-cjk-fonts"]);
748
- break;
749
- case "arch":
750
- attempts.push(["pacman", "-Sy", "--noconfirm", "fontconfig", "noto-fonts-cjk"]);
751
- break;
752
- case "alpine":
753
- attempts.push(["apk", "add", "fontconfig", "font-noto-cjk"]);
754
- break;
755
- default:
756
- break;
647
+ function printNextSteps(options, config) {
648
+ console.log("");
649
+ console.log("接下来的命令需要你手动执行。");
650
+ let step = 1;
651
+ if (options.pythonSetup) {
652
+ console.log(`${step}. 安装 Python 依赖`);
653
+ console.log(` cd ${config.pythonWorkdir}`);
654
+ console.log(" uv sync");
655
+ step += 1;
757
656
  }
758
- let attemptedInstall = false;
759
- for (const baseArgv of attempts) {
760
- if (!isCommandAvailable(baseArgv[0])) {
761
- continue;
762
- }
763
- const argv = buildPrivilegedCommand(baseArgv, nonInteractive);
764
- if (!argv) {
765
- break;
766
- }
767
- attemptedInstall = true;
768
- if (!runSetupCommand(argv, `Running ${argv.join(" ")}`)) {
769
- continue;
770
- }
771
- if (isCommandAvailable("fc-cache")) {
772
- runSetupCommand(["fc-cache", "-fv"], "Refreshing font cache with fc-cache -fv");
773
- }
774
- if (isCommandAvailable("fc-list") && hasChineseFonts()) {
775
- console.log("Chinese fonts installed successfully.");
776
- return;
657
+ if (options.fontSetup && process.platform === "linux") {
658
+ console.log(`${step}. 如需 PNG 告警卡正常显示中文,请按你的 Linux 发行版安装字体`);
659
+ for (const command of getManualFontCommands(detectLinuxDistro())) {
660
+ console.log(` ${command}`);
777
661
  }
662
+ step += 1;
778
663
  }
779
- console.warn("Warning: automatic Chinese font setup did not complete.");
780
- if (!attemptedInstall && nonInteractive) {
781
- console.warn("Non-interactive mode skipped any sudo password prompt.");
664
+ if (options.enable) {
665
+ console.log(`${step}. 启用插件`);
666
+ console.log(` ${options.openclawBin} plugins enable ${PLUGIN_ID}`);
667
+ step += 1;
782
668
  }
783
- console.warn("Manual install examples:");
784
- for (const command of getManualFontCommands(distro)) {
785
- console.warn(` ${command}`);
669
+ console.log(`${step}. 校验 OpenClaw 配置`);
670
+ console.log(` ${options.openclawBin} config validate`);
671
+ step += 1;
672
+ if (options.restart) {
673
+ console.log(`${step}. 重启 Gateway`);
674
+ console.log(` ${options.openclawBin} gateway restart`);
786
675
  }
787
676
  }
788
677
  async function configureOpenClaw(options) {
@@ -795,12 +684,6 @@ async function configureOpenClaw(options) {
795
684
  const config = await promptForConfig(options, existing, pluginDir, configPath);
796
685
  await ensurePathNotice(config.calendarFile, "calendarFile");
797
686
  await ensurePathNotice(config.pythonWorkdir, "pythonWorkdir");
798
- if (options.pythonSetup) {
799
- await setupPythonDeps(config.pythonWorkdir, options.nonInteractive);
800
- }
801
- if (options.fontSetup) {
802
- await setupLinuxChineseFonts(options.nonInteractive);
803
- }
804
687
  applyPluginConfig(root, config, target);
805
688
  const backupPath = await writeConfig(configPath, root);
806
689
  console.log("");
@@ -810,13 +693,7 @@ async function configureOpenClaw(options) {
810
693
  }
811
694
  console.log(`Plugin dir: ${pluginDir}`);
812
695
  console.log(`Allowlist target: ${target.type === "global" ? "global tools" : `agent:${target.id}`}`);
813
- if (options.enable) {
814
- runOpenClaw(options.openclawBin, ["plugins", "enable", PLUGIN_ID], "openclaw plugins enable");
815
- }
816
- runOpenClaw(options.openclawBin, ["config", "validate"], "openclaw config validate");
817
- if (options.restart) {
818
- runOpenClaw(options.openclawBin, ["gateway", "restart"], "openclaw gateway restart");
819
- }
696
+ printNextSteps(options, config);
820
697
  }
821
698
  async function main() {
822
699
  try {
@@ -0,0 +1,21 @@
1
+ import type { OpenClawPluginRuntime } from "./plugin-api.js";
2
+ export interface CommandRunResult {
3
+ pid?: number;
4
+ stdout: string;
5
+ stderr: string;
6
+ code: number | null;
7
+ signal: NodeJS.Signals | null;
8
+ killed: boolean;
9
+ termination: "exit" | "timeout" | "no-output-timeout" | "signal";
10
+ noOutputTimedOut?: boolean;
11
+ }
12
+ export interface CommandRunOptions {
13
+ timeoutMs: number;
14
+ cwd?: string;
15
+ input?: string;
16
+ env?: NodeJS.ProcessEnv;
17
+ windowsVerbatimArguments?: boolean;
18
+ noOutputTimeoutMs?: number;
19
+ }
20
+ export type RunCommandWithTimeout = (argv: string[], options: number | CommandRunOptions) => Promise<CommandRunResult>;
21
+ export declare function createCommandRunner(runtime?: OpenClawPluginRuntime): RunCommandWithTimeout;
@@ -0,0 +1,22 @@
1
+ let localRunnerPromise = null;
2
+ async function loadLocalRunner() {
3
+ localRunnerPromise ??= import("openclaw/plugin-sdk/process-runtime")
4
+ .then((module) => module.runCommandWithTimeout)
5
+ .catch((error) => {
6
+ localRunnerPromise = null;
7
+ const detail = error instanceof Error ? error.message : String(error);
8
+ throw new Error("OpenClaw command runner is unavailable outside plugin runtime. " +
9
+ "For source-mode local commands, run `npm install` with devDependencies " +
10
+ `or execute the feature through OpenClaw. Details: ${detail}`);
11
+ });
12
+ return localRunnerPromise;
13
+ }
14
+ export function createCommandRunner(runtime) {
15
+ if (runtime) {
16
+ return runtime.system.runCommandWithTimeout;
17
+ }
18
+ return async (argv, options) => {
19
+ const runner = await loadLocalRunner();
20
+ return runner(argv, options);
21
+ };
22
+ }
@@ -26,6 +26,7 @@ export interface AlertSendResult {
26
26
  export declare class AlertService {
27
27
  private readonly options;
28
28
  private lastError;
29
+ private readonly runCommandWithTimeout;
29
30
  constructor(options: AlertServiceOptions);
30
31
  send(input: string | AlertSendInput): Promise<boolean>;
31
32
  sendWithResult(input: string | AlertSendInput): Promise<AlertSendResult>;
@@ -59,8 +60,7 @@ export declare class AlertService {
59
60
  private combineErrors;
60
61
  private trySendPayload;
61
62
  private trySendViaRuntime;
62
- private trySendViaRuntimeCommand;
63
- private trySendViaSpawn;
63
+ private trySendViaCommand;
64
64
  private buildCliArgs;
65
65
  }
66
66
  export {};
@@ -1,10 +1,12 @@
1
- import { spawn } from "node:child_process";
1
+ import { createCommandRunner } from "../runtime/command-runner.js";
2
2
  import { calculateProfitPct, formatCostPrice } from "../utils/cost-price.js";
3
3
  export class AlertService {
4
4
  options;
5
5
  lastError = null;
6
+ runCommandWithTimeout;
6
7
  constructor(options) {
7
8
  this.options = options;
9
+ this.runCommandWithTimeout = createCommandRunner(options.runtime?.runtime);
8
10
  }
9
11
  async send(input) {
10
12
  const result = await this.sendWithResult(input);
@@ -135,9 +137,7 @@ export class AlertService {
135
137
  if (runtimeError === null) {
136
138
  return null;
137
139
  }
138
- return this.options.runtime
139
- ? await this.trySendViaRuntimeCommand(payload)
140
- : await this.trySendViaSpawn(payload);
140
+ return await this.trySendViaCommand(payload);
141
141
  }
142
142
  async trySendViaRuntime(payload) {
143
143
  const runtimeContext = this.options.runtime;
@@ -178,13 +178,9 @@ export class AlertService {
178
178
  return `runtime delivery failed: ${formatErrorMessage(error)}`;
179
179
  }
180
180
  }
181
- async trySendViaRuntimeCommand(payload) {
182
- const runtimeContext = this.options.runtime;
183
- if (!runtimeContext) {
184
- return "runtime command unavailable";
185
- }
181
+ async trySendViaCommand(payload) {
186
182
  try {
187
- const result = await runtimeContext.runtime.system.runCommandWithTimeout(this.buildCliArgs(payload), { timeoutMs: 15_000 });
183
+ const result = await this.runCommandWithTimeout(this.buildCliArgs(payload), { timeoutMs: 15_000 });
188
184
  if (result.code === 0) {
189
185
  return null;
190
186
  }
@@ -193,40 +189,9 @@ export class AlertService {
193
189
  || `command exited with ${result.code ?? "unknown"}`);
194
190
  }
195
191
  catch (error) {
196
- return `runtime command failed: ${formatErrorMessage(error)}`;
192
+ return `command delivery failed: ${formatErrorMessage(error)}`;
197
193
  }
198
194
  }
199
- async trySendViaSpawn(payload) {
200
- const argv = this.buildCliArgs(payload);
201
- const [command, ...args] = argv;
202
- return new Promise((resolve) => {
203
- const child = spawn(command, args, {
204
- stdio: ["ignore", "pipe", "pipe"],
205
- });
206
- let stdout = "";
207
- let stderr = "";
208
- let settled = false;
209
- const finish = (value) => {
210
- if (settled) {
211
- return;
212
- }
213
- settled = true;
214
- resolve(value);
215
- };
216
- child.stdout.on("data", (chunk) => {
217
- stdout += chunk.toString();
218
- });
219
- child.stderr.on("data", (chunk) => {
220
- stderr += chunk.toString();
221
- });
222
- child.on("error", (error) => {
223
- finish(`spawn failed: ${error.message}`);
224
- });
225
- child.on("close", (code) => {
226
- finish(code === 0 ? null : (stderr || stdout || `exit code ${code}`).trim());
227
- });
228
- });
229
- }
230
195
  buildCliArgs(payload) {
231
196
  const args = [
232
197
  this.options.openclawCliBin,
@@ -1,9 +1,11 @@
1
+ import type { RunCommandWithTimeout } from "../runtime/command-runner.js";
1
2
  import type { IndicatorInputRow, IndicatorRow } from "../types/indicator.js";
2
3
  export declare class IndicatorService {
3
4
  private readonly pythonBin;
4
5
  private readonly pythonArgs;
5
6
  private readonly pythonWorkdir;
6
- constructor(pythonBin: string, pythonArgs: string[], pythonWorkdir: string);
7
+ private readonly runCommandWithTimeout;
8
+ constructor(pythonBin: string, pythonArgs: string[], pythonWorkdir: string, runCommandWithTimeout: RunCommandWithTimeout);
7
9
  calculate(rows: IndicatorInputRow[]): Promise<IndicatorRow[]>;
8
10
  private runPythonJson;
9
11
  }
@@ -1,13 +1,14 @@
1
- import { spawn } from "node:child_process";
2
1
  import path from "node:path";
3
2
  export class IndicatorService {
4
3
  pythonBin;
5
4
  pythonArgs;
6
5
  pythonWorkdir;
7
- constructor(pythonBin, pythonArgs, pythonWorkdir) {
6
+ runCommandWithTimeout;
7
+ constructor(pythonBin, pythonArgs, pythonWorkdir, runCommandWithTimeout) {
8
8
  this.pythonBin = pythonBin;
9
9
  this.pythonArgs = pythonArgs;
10
10
  this.pythonWorkdir = pythonWorkdir;
11
+ this.runCommandWithTimeout = runCommandWithTimeout;
11
12
  }
12
13
  async calculate(rows) {
13
14
  if (rows.length === 0) {
@@ -38,30 +39,18 @@ export class IndicatorService {
38
39
  }
39
40
  runPythonJson(payload) {
40
41
  const scriptPath = path.join(this.pythonWorkdir, "indicator_runner.py");
41
- const child = spawn(this.pythonBin, [...this.pythonArgs, scriptPath], {
42
+ const argv = [this.pythonBin, ...this.pythonArgs, scriptPath];
43
+ return this.runCommandWithTimeout(argv, {
42
44
  cwd: path.dirname(scriptPath),
43
- stdio: ["pipe", "pipe", "pipe"],
44
- });
45
- return new Promise((resolve, reject) => {
46
- let stdout = "";
47
- let stderr = "";
48
- child.stdout.on("data", (chunk) => {
49
- stdout += chunk.toString();
50
- });
51
- child.stderr.on("data", (chunk) => {
52
- stderr += chunk.toString();
53
- });
54
- child.on("error", reject);
55
- child.on("close", (code) => {
56
- if (code === 0) {
57
- resolve(stdout);
58
- return;
59
- }
60
- reject(new Error(`indicator_runner failed with code ${code}: ${stderr || stdout}` +
61
- `\npython command: ${this.pythonBin} ${[...this.pythonArgs, scriptPath].join(" ")}`));
62
- });
63
- child.stdin.write(JSON.stringify(payload));
64
- child.stdin.end();
45
+ input: JSON.stringify(payload),
46
+ timeoutMs: 30_000,
47
+ noOutputTimeoutMs: 30_000,
48
+ }).then((result) => {
49
+ if (result.code === 0) {
50
+ return result.stdout;
51
+ }
52
+ throw new Error(`indicator_runner failed with code ${result.code}: ${result.stderr || result.stdout}` +
53
+ `\npython command: ${argv.join(" ")}`);
65
54
  });
66
55
  }
67
56
  }
@@ -1,6 +1,6 @@
1
1
  import type { PluginConfig } from "../config/schema.js";
2
2
  import { DailyUpdateWorker } from "../background/daily-update.worker.js";
3
- export declare function startDailyUpdateTool(dailyUpdateWorker: DailyUpdateWorker, config: PluginConfig, configSource: "openclaw_plugin" | "local_config", runtime: {
3
+ export declare function startDailyUpdateTool(dailyUpdateWorker: DailyUpdateWorker, _config: PluginConfig, configSource: "openclaw_plugin" | "local_config", runtime: {
4
4
  pluginManagedServices: boolean;
5
5
  }): {
6
6
  name: string;
@@ -1,23 +1,36 @@
1
- export function startDailyUpdateTool(dailyUpdateWorker, config, configSource, runtime) {
1
+ import { isPidAlive } from "../background/daily-update.worker.js";
2
+ export function startDailyUpdateTool(dailyUpdateWorker, _config, configSource, runtime) {
2
3
  return {
3
4
  name: "start_daily_update",
4
5
  description: "Start the TickFlow daily-update scheduler.",
5
6
  optional: true,
6
7
  async run() {
7
- const result = runtime.pluginManagedServices
8
- ? await dailyUpdateWorker.enableManagedLoop(configSource)
9
- : await dailyUpdateWorker.ensureLoopRunning(config, configSource);
10
- if (!result.started) {
8
+ if (runtime.pluginManagedServices) {
9
+ const result = await dailyUpdateWorker.enableManagedLoop(configSource);
10
+ if (!result.started) {
11
+ return await dailyUpdateWorker.getStatusReport();
12
+ }
13
+ const lines = [
14
+ "✅ TickFlow 定时日更已启动",
15
+ `PID: ${result.pid ?? "托管服务"}`,
16
+ "运行方式: plugin_service",
17
+ `配置来源: ${configSource}`,
18
+ "说明: 后台按 15 分钟对齐轮询,交易日 15:25 后最多执行一次日更,20:00 后最多执行一次复盘",
19
+ ];
20
+ return lines.join("\n");
21
+ }
22
+ const state = await dailyUpdateWorker.getState();
23
+ if (state.running && state.workerPid != null && isPidAlive(state.workerPid)) {
11
24
  return await dailyUpdateWorker.getStatusReport();
12
25
  }
13
- const lines = [
26
+ await dailyUpdateWorker.markSchedulerRunning(null, configSource);
27
+ return [
14
28
  "✅ TickFlow 定时日更已启动",
15
- `PID: ${result.pid ?? "托管服务"}`,
16
- `运行方式: ${runtime.pluginManagedServices ? "plugin_service" : "project_scheduler"}`,
29
+ "PID: 手动循环",
30
+ "运行方式: manual_loop",
17
31
  `配置来源: ${configSource}`,
18
- "说明: 后台按 15 分钟对齐轮询,交易日 15:25 后最多执行一次日更,20:00 后最多执行一次复盘",
19
- ];
20
- return lines.join("\n");
32
+ "下一步: 在另一个终端执行 `npm run daily-update-loop` 启动本地日更循环。",
33
+ ].join("\n");
21
34
  },
22
35
  };
23
36
  }
@@ -1,5 +1,3 @@
1
- import { spawn } from "node:child_process";
2
- import path from "node:path";
3
1
  export function startMonitorTool(monitorService, runtime) {
4
2
  return {
5
3
  name: "start_monitor",
@@ -25,22 +23,14 @@ export function startMonitorTool(monitorService, runtime) {
25
23
  return await monitorService.getStatusReport();
26
24
  }
27
25
  const summary = await monitorService.start();
28
- const workerPid = spawnMonitorLoop();
29
- await monitorService.setWorkerPid(workerPid);
30
- return summary;
26
+ return [
27
+ summary,
28
+ "运行方式: manual_loop",
29
+ "下一步: 在另一个终端执行 `npm run monitor-loop` 启动本地监控循环。",
30
+ ].join("\n");
31
31
  },
32
32
  };
33
33
  }
34
- function spawnMonitorLoop() {
35
- const scriptPath = path.resolve("dist/dev/run-monitor-loop.js");
36
- const child = spawn(process.execPath, [scriptPath], {
37
- cwd: process.cwd(),
38
- detached: true,
39
- stdio: "ignore",
40
- });
41
- child.unref();
42
- return child.pid ?? null;
43
- }
44
34
  function isPidAlive(pid) {
45
35
  try {
46
36
  process.kill(pid, 0);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "tickflow-assist",
3
3
  "name": "TickFlow Assist",
4
- "version": "0.2.15",
4
+ "version": "0.2.16",
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.15",
3
+ "version": "0.2.16",
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",
@@ -1,3 +0,0 @@
1
- import type { PluginConfig } from "../config/schema.js";
2
- export declare function spawnDailyUpdateLoop(config: PluginConfig, configSource: "openclaw_plugin" | "local_config"): number | null;
3
- export declare function isPidAlive(pid: number): boolean;
@@ -1,24 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { fileURLToPath } from "node:url";
3
- import { buildProcessConfigEnv } from "./process-config.js";
4
- const PROJECT_ROOT = fileURLToPath(new URL("../../", import.meta.url));
5
- const DAILY_UPDATE_LOOP_SCRIPT = fileURLToPath(new URL("../dev/run-daily-update-loop.js", import.meta.url));
6
- export function spawnDailyUpdateLoop(config, configSource) {
7
- const child = spawn(process.execPath, [DAILY_UPDATE_LOOP_SCRIPT], {
8
- cwd: PROJECT_ROOT,
9
- detached: true,
10
- stdio: "ignore",
11
- env: buildProcessConfigEnv(config, configSource),
12
- });
13
- child.unref();
14
- return child.pid ?? null;
15
- }
16
- export function isPidAlive(pid) {
17
- try {
18
- process.kill(pid, 0);
19
- return true;
20
- }
21
- catch {
22
- return false;
23
- }
24
- }