tickflow-assist 0.2.15 → 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 +33 -8
- package/dist/background/daily-update.worker.d.ts +1 -5
- package/dist/background/daily-update.worker.js +9 -14
- package/dist/bootstrap.js +3 -1
- package/dist/dev/tickflow-assist-cli.js +73 -153
- package/dist/plugin-commands.js +36 -57
- package/dist/runtime/command-runner.d.ts +21 -0
- package/dist/runtime/command-runner.js +22 -0
- package/dist/services/alert-service.d.ts +2 -2
- package/dist/services/alert-service.js +7 -42
- package/dist/services/indicator-service.d.ts +3 -1
- package/dist/services/indicator-service.js +14 -25
- package/dist/tools/start-daily-update.tool.d.ts +1 -1
- package/dist/tools/start-daily-update.tool.js +24 -11
- package/dist/tools/start-monitor.tool.js +41 -17
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/runtime/daily-update-process.d.ts +0 -3
- package/dist/runtime/daily-update-process.js +0 -24
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.
|
|
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
|
|
|
@@ -13,21 +13,46 @@
|
|
|
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
|
-
|
|
23
|
+
`configure-openclaw` 会写入 `~/.openclaw/openclaw.json` 中的 `plugins.entries["tickflow-assist"].config`,并打印后续建议执行的命令;它不再自动执行 `openclaw`、`uv` 或系统包安装命令。
|
|
24
|
+
如果检测到 `plugins.installs["tickflow-assist"]` 来自 `clawhub`,向导还会把被旧版本钉死的 `spec` 归一化为 `clawhub:tickflow-assist`,避免后续升级继续锁在旧版本。
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
如果你希望先审阅配置,再只打印最少的后续步骤,可使用:
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
28
|
+
```bash
|
|
29
|
+
npx -y tickflow-assist configure-openclaw --no-enable --no-restart
|
|
30
|
+
```
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
如果你在 Linux 或 macOS 上需要 PNG 告警卡正常显示中文,请额外手动安装 `fontconfig` 与 Noto CJK 一类中文字体,例如:
|
|
28
33
|
|
|
29
34
|
```bash
|
|
30
|
-
|
|
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
|
|
31
56
|
```
|
|
32
57
|
|
|
33
58
|
社区安装后的升级方式:
|
|
@@ -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
|
|
40
|
-
--no-restart Do not
|
|
41
|
-
--no-python-setup Do not
|
|
42
|
-
--no-font-setup Do not
|
|
43
|
-
--openclaw-bin <path> OpenClaw CLI binary
|
|
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 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>
|
|
@@ -502,6 +501,34 @@ function assertRequired(config) {
|
|
|
502
501
|
throw new Error("llmApiKey is required");
|
|
503
502
|
}
|
|
504
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
|
+
}
|
|
505
532
|
async function ensurePathNotice(targetPath, label) {
|
|
506
533
|
try {
|
|
507
534
|
await access(targetPath);
|
|
@@ -586,60 +613,6 @@ async function writeConfig(configPath, root) {
|
|
|
586
613
|
await writeFile(configPath, `${JSON.stringify(root, null, 2)}\n`, "utf-8");
|
|
587
614
|
return backupPath;
|
|
588
615
|
}
|
|
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
616
|
function detectLinuxDistro() {
|
|
644
617
|
try {
|
|
645
618
|
const raw = readFileSync("/etc/os-release", "utf-8");
|
|
@@ -699,90 +672,45 @@ function getManualFontCommands(distro) {
|
|
|
699
672
|
];
|
|
700
673
|
}
|
|
701
674
|
}
|
|
702
|
-
function
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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;
|
|
675
|
+
function getManualMacosFontCommands() {
|
|
676
|
+
return [
|
|
677
|
+
"brew install fontconfig",
|
|
678
|
+
"brew install --cask font-noto-sans-cjk",
|
|
679
|
+
"fc-cache -fv",
|
|
680
|
+
];
|
|
723
681
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
if (
|
|
729
|
-
console.
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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;
|
|
757
|
-
}
|
|
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;
|
|
777
|
-
}
|
|
682
|
+
function printNextSteps(options, config) {
|
|
683
|
+
console.log("");
|
|
684
|
+
console.log("接下来的命令需要你手动执行。");
|
|
685
|
+
let step = 1;
|
|
686
|
+
if (options.pythonSetup) {
|
|
687
|
+
console.log(`${step}. 安装 Python 依赖`);
|
|
688
|
+
console.log(` cd ${config.pythonWorkdir}`);
|
|
689
|
+
console.log(" uv sync");
|
|
690
|
+
step += 1;
|
|
691
|
+
}
|
|
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) {
|
|
699
|
+
console.log(` ${command}`);
|
|
700
|
+
}
|
|
701
|
+
step += 1;
|
|
778
702
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
console.
|
|
703
|
+
if (options.enable) {
|
|
704
|
+
console.log(`${step}. 启用插件`);
|
|
705
|
+
console.log(` ${options.openclawBin} plugins enable ${PLUGIN_ID}`);
|
|
706
|
+
step += 1;
|
|
782
707
|
}
|
|
783
|
-
console.
|
|
784
|
-
|
|
785
|
-
|
|
708
|
+
console.log(`${step}. 校验 OpenClaw 配置`);
|
|
709
|
+
console.log(` ${options.openclawBin} config validate`);
|
|
710
|
+
step += 1;
|
|
711
|
+
if (options.restart) {
|
|
712
|
+
console.log(`${step}. 重启 Gateway`);
|
|
713
|
+
console.log(` ${options.openclawBin} gateway restart`);
|
|
786
714
|
}
|
|
787
715
|
}
|
|
788
716
|
async function configureOpenClaw(options) {
|
|
@@ -795,13 +723,8 @@ async function configureOpenClaw(options) {
|
|
|
795
723
|
const config = await promptForConfig(options, existing, pluginDir, configPath);
|
|
796
724
|
await ensurePathNotice(config.calendarFile, "calendarFile");
|
|
797
725
|
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
726
|
applyPluginConfig(root, config, target);
|
|
727
|
+
const normalizedInstallSpec = normalizeCommunityInstallSpec(root);
|
|
805
728
|
const backupPath = await writeConfig(configPath, root);
|
|
806
729
|
console.log("");
|
|
807
730
|
console.log(`Updated OpenClaw config: ${configPath}`);
|
|
@@ -810,13 +733,10 @@ async function configureOpenClaw(options) {
|
|
|
810
733
|
}
|
|
811
734
|
console.log(`Plugin dir: ${pluginDir}`);
|
|
812
735
|
console.log(`Allowlist target: ${target.type === "global" ? "global tools" : `agent:${target.id}`}`);
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
}
|
|
816
|
-
runOpenClaw(options.openclawBin, ["config", "validate"], "openclaw config validate");
|
|
817
|
-
if (options.restart) {
|
|
818
|
-
runOpenClaw(options.openclawBin, ["gateway", "restart"], "openclaw gateway restart");
|
|
736
|
+
if (normalizedInstallSpec) {
|
|
737
|
+
console.log(`Community install spec normalized: clawhub:${PLUGIN_ID}`);
|
|
819
738
|
}
|
|
739
|
+
printNextSteps(options, config);
|
|
820
740
|
}
|
|
821
741
|
async function main() {
|
|
822
742
|
try {
|
package/dist/plugin-commands.js
CHANGED
|
@@ -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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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) {
|
|
@@ -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
|
|
63
|
-
private trySendViaSpawn;
|
|
63
|
+
private trySendViaCommand;
|
|
64
64
|
private buildCliArgs;
|
|
65
65
|
}
|
|
66
66
|
export {};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
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.
|
|
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
|
|
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
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
42
|
+
const argv = [this.pythonBin, ...this.pythonArgs, scriptPath];
|
|
43
|
+
return this.runCommandWithTimeout(argv, {
|
|
42
44
|
cwd: path.dirname(scriptPath),
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
26
|
+
await dailyUpdateWorker.markSchedulerRunning(null, configSource);
|
|
27
|
+
return [
|
|
14
28
|
"✅ TickFlow 定时日更已启动",
|
|
15
|
-
|
|
16
|
-
|
|
29
|
+
"PID: 手动循环",
|
|
30
|
+
"运行方式: manual_loop",
|
|
17
31
|
`配置来源: ${configSource}`,
|
|
18
|
-
"
|
|
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",
|
|
@@ -7,9 +5,15 @@ export function startMonitorTool(monitorService, runtime) {
|
|
|
7
5
|
optional: true,
|
|
8
6
|
async run() {
|
|
9
7
|
if (runtime.pluginManagedServices) {
|
|
10
|
-
|
|
8
|
+
let result;
|
|
9
|
+
try {
|
|
10
|
+
result = await monitorService.enableManagedLoop();
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
return formatManagedStartError(error);
|
|
14
|
+
}
|
|
11
15
|
if (!result.started) {
|
|
12
|
-
return await monitorService
|
|
16
|
+
return await buildManagedAlreadyRunningSummary(monitorService);
|
|
13
17
|
}
|
|
14
18
|
return [
|
|
15
19
|
"✅ TickFlow 实时监控已启动",
|
|
@@ -22,24 +26,44 @@ export function startMonitorTool(monitorService, runtime) {
|
|
|
22
26
|
if (currentState.running
|
|
23
27
|
&& currentState.workerPid != null
|
|
24
28
|
&& isPidAlive(currentState.workerPid)) {
|
|
25
|
-
return
|
|
29
|
+
return [
|
|
30
|
+
"✅ TickFlow 实时监控已在运行",
|
|
31
|
+
"运行方式: manual_loop",
|
|
32
|
+
`PID: ${currentState.workerPid}`,
|
|
33
|
+
"说明: 本地监控循环已存在,无需重复启动。",
|
|
34
|
+
].join("\n");
|
|
26
35
|
}
|
|
27
36
|
const summary = await monitorService.start();
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
return [
|
|
38
|
+
summary,
|
|
39
|
+
"运行方式: manual_loop",
|
|
40
|
+
"下一步: 在另一个终端执行 `npm run monitor-loop` 启动本地监控循环。",
|
|
41
|
+
].join("\n");
|
|
31
42
|
},
|
|
32
43
|
};
|
|
33
44
|
}
|
|
34
|
-
function
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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");
|
|
43
67
|
}
|
|
44
68
|
function isPidAlive(pid) {
|
|
45
69
|
try {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tickflow-assist",
|
|
3
|
-
"version": "0.2.
|
|
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",
|
|
@@ -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
|
-
}
|