Skill: valuescan-io/valuescan-monitor-skill (version 1.0.3)

--- name: valuescan-monitor-skill description: ValueScan后台实时监控Skill。订阅Stream推送（大盘分析/代币信号），将数据持久化写入本地文件。 version: 1.0.0 user-invocable: true category: Monitoring license: MIT homepage: https://www.valuescan.ai dependencies: - python: ">=3.10" - node: ">=18" tags: - cryptocurrency - monitoring - stream - realtime credentials: [REDACTED_SECRET] ValueScan API Key required: true secret_key: description: ValueScan Secret Key required: true --- # valuescan-monitor-skill 后台常驻监控 ValueScan Stream 推送，将大盘分析和代币信号实时写入本地文件。 ## 首次安装 当用户首次使用监控相关指令时，若 `~/.vs-monitor/config.json` 不存在，Claude 按以下步骤引导安装： 1. 询问 **API Key**（ValueScan 开放平台获取） 2. 询问 **Secret Key** 3. 询问**数据输出目录**（如 `/Users/xxx/valuescan-monitor`） 4. 将以上内容写入 `~/.vs-monitor/config.json`： ```json { "apiKey": "...", "secretKey": "...", "outputDir": "/path/to/output", "streamBaseUrl": "https://stream.valuescan.ai", "apiBaseUrl": "https://api.valuescan.io" } ``` ## 触发语与 Claude 动作 | 用户说 | Claude 动作 | |--------|------------| | 监控大盘分析 / 开始大盘监控 | 检查 `market.pid`，kill 旧进程后启动 `python monitor.py --market`，写 `~/.vs-monitor/market.pid` | | 监控所有代币信号 | 检查 `signal.pid`，kill 旧进程后启动 `python monitor.py --signal`，写 `signal.pid` | | 监控 BTC 信号 | 启动 `python monitor.py --signal --tokens=BTC`，写 `signal.pid` | | 监控 ETH 和 SOL 信号 | 启动 `python monitor.py --signal --tokens=ETH,SOL`，写 `signal.pid` | | 同时监控大盘和所有代币信号 | 启动两个独立进程，分别写 `market.pid` 和 `signal.pid` | | 停止大盘监控 | 读取 `market.pid`，kill 对应进程，删除 PID 文件 | | 停止信号监控 / 停止代币监控 | 读取 `signal.pid`，kill 对应进程，删除 PID 文件 | | 停止所有监控 | kill 两个 PID，删除对应 PID 文件 | | 查看监控状态 / 监控运行中吗 | 检查两个 PID 文件是否存在且进程存活，报告状态 | ## 进程管理 PID 文件目录：`~/.vs-monitor/` | 模式 | PID 文件 | |------|---------| | 大盘分析 | `~/.vs-monitor/market.pid` | | 代币信号 | `~/.vs-monitor/signal.pid` | **启动前检查：** ```bash # 检查进程是否存活 PID=$(cat ~/.vs-monitor/market.pid 2>/dev/null) if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then kill "$PID" fi ``` **后台启动：** ```bash nohup python /path/to/vs-monitor-skill/script/monitor.py --market \ --config=~/.vs-monitor/config.json > ~/.vs-monitor/market.log 2>&1 & echo $! > ~/.vs-monitor/market.pid ``` ## 文件写入规则 | 来源 | 路径 | 格式 | |------|------|------| | 大盘分析（market event） | `{outputDir}/大盘分析/大盘分析-YYYY-MM-DD.txt` | `[HH:MM:SS]\n{content}\n---\n` | | 代币信号（signal event） | `{outputDir}/代币信号/YYYY-MM-DD/{symbol}.txt` | `[HH:MM:SS] [{type}]\n{content}\n---\n` | - `symbol` 从 signal content JSON 的 `symbol` 字段取 - `type`：OPPORTUNITY / RISK / ANOMALY - heartbeat / connected 事件忽略，不写文件 - 目录不存在时自动创建 ## 脚本使用 ### Python（推荐） ```bash pip install sseclient-py requests python monitor.py --market python monitor.py --signal --tokens=BTC,ETH ``` ### TypeScript ```bash cd script && npm install npx ts-node monitor.ts --market npx ts-node monitor.ts --signal --tokens=BTC,ETH ``` ## 认证签名 Stream SSE 使用 query params 认证： ``` timestamp = 当前毫秒时间戳 nonce = 随机 UUID sign = HMAC-SHA256(key=secretKey, message=timestamp + nonce) ``` 参数拼接到 SSE URL：`?[REDACTED_SECRET]` ## 安全注意事项 - **凭证存储**：API Key 和 Secret Key 将以明文存储在 `~/.vs-monitor/config.json`，请确保该目录权限受控 - **进程管理**：技能会创建后台进程并通过 PID 文件管理（kill 旧进程），PID 文件位于 `~/.vs-monitor/` - **文件写入**：所有数据写入用户指定的 `outputDir`，请确保目录路径安全 - **隔离环境**：建议在容器、VM 或权限受限的账户中运行，以限制影响范围 ## 已知限制 - 断线后进程退出，需 Claude 手动重启（不含自动重连） - 同类型只允许一个进程（signal 或 market 各一个）

path: script/monitor.py
content: #!/usr/bin/env python3
# Dependencies: pip install sseclient-py requests pysocks
"""
ValueScan Stream Monitor (Python)

Usage:
  python monitor.py --market [--config=~/.vs-monitor/config.json]
  python monitor.py --signal [--tokens=BTC,ETH] [--config=~/.vs-monitor/config.json]

Config (~/.vs-monitor/config.json):
  {
    "apiKey": "...",
    "secretKey": "...",
    "outputDir": "/path/to/output",
    "streamBaseUrl": "https://stream.valuescan.ai"  # optional
  }
"""

import argparse
import hashlib
import hmac
import json
import os
import signal as signal_module
import sys
import time
import uuid
from datetime import datetime
from pathlib import Path

import requests
import sseclient

PID_DIR = Path.home() / ".vs-monitor"


# ── Auth ──────────────────────────────────────────────────────────────────────

def _hmac_hex(secret_key: str, message: str) -> str:
    return hmac.new(
        secret_key.encode("utf-8"),
        message.encode("utf-8"),
        hashlib.sha256
    ).hexdigest()


def build_stream_params(api_key: str, secret_key: str) -> dict:
    """Query params for SSE connections (GET requests, body fixed as '{}')."""
    timestamp = str(int(time.time() * 1000))
    nonce = str(uuid.uuid4())
    sign = _hmac_hex(secret_key, timestamp + nonce)
    return {"apiKey": api_key, "timestamp": timestamp, "nonce": nonce, "sign": sign}


def build_api_headers(api_key: str, secret_key: str, body: str) -> dict:
    """Request headers for main API calls (POST)."""
    timestamp = str(int(time.time() * 1000))
    sign = _hmac_hex(secret_key, timestamp + body)
    return {
        "X-API-KEY": api_key,
        "X-TIMESTAMP": timestamp,
        "X-SIGN": sign,
        "Content-Type": "application/json; charset=utf-8",
    }


# ── Token resolution ──────────────────────────────────────────────────────────

def resolve_token_ids(symbols: list, api_key: str, secret_key: str, api_base_url: str) -> list:
    """Convert token symbols (e.g. 'BTC') to vsTokenId strings."""
    token_ids = []
    for symbol in symbols:
        body = json.dumps({"search": symbol})
        headers = build_api_headers(api_key, secret_key, body)
        try:
            resp = requests.post(
                f"{api_base_url}/api/open/v1/vs-token/list",
                headers=headers,
                data=body,
                timeout=10,
            )
            records = resp.json().get("data", [])
        except Exception as e:
            print(f"[warn] Failed to resolve symbol {symbol}: {e}", file=sys.stderr)
            continue
        if not records:
            print(f"[warn] Symbol not found: {symbol}", file=sys.stderr)
            continue
        match = next((r for r in records if r.get("symbol", "").upper() == symbol.upper()), None)
        if match is None:
            match = records[0]
        if match:
            token_ids.append(str(match["id"]))
        else:
            print(f"[warn] Symbol not found: {symbol}", file=sys.stderr)
    return token_ids


# ── File writing ──────────────────────────────────────────────────────────────

def write_market(content: str, output_dir: str) -> None:
    now = datetime.now()
    dir_path = Path(output_dir) / "大盘分析"
    dir_path.mkdir(parents=True, exist_ok=True)
    file_path = dir_path / f"大盘分析-{now.strftime('%Y-%m-%d')}.txt"
    with open(file_path, "a", encoding="utf-8") as f:
        f.write(f"[{now.strftime('%H:%M:%S')}]\n{content}\n---\n")


def write_signal(payload: str, output_dir: str) -> None:
    msg = json.loads(payload)
    signal_type = msg.get("type", "UNKNOWN")
    try:
        inner = json.loads(msg.get("content", "{}"))
    except Exception:
        inner = {}
    symbol = inner.get("symbol", "UNKNOWN")
    content = msg.get("content", "")
    now = datetime.now()
    dir_path = Path(output_dir) / "代币信号" / now.strftime("%Y-%m-%d")
    dir_path.mkdir(parents=True, exist_ok=True)
    file_path = dir_path / f"{symbol}.txt"
    with open(file_path, "a", encoding="utf-8") as f:
        f.write(f"[{now.strftime('%H:%M:%S')}] [{signal_type}]\n{content}\n---\n")


# ── PID management ────────────────────────────────────────────────────────────

def write_pid(mode: str) -> None:
    PID_DIR.mkdir(parents=True, exist_ok=True)
    (PID_DIR / f"{mode}.pid").write_text(str(os.getpid()))


def cleanup_pid(mode: str) -> None:
    pid_file = PID_DIR / f"{mode}.pid"
    if pid_file.exists():
        pid_file.unlink(missing_ok=True)


# ── Monitor loops ─────────────────────────────────────────────────────────────

def run_market(config: dict) -> None:
    stream_base = config.get("streamBaseUrl", "https://stream.valuescan.ai")
    output_dir = config["outputDir"]
    params = build_stream_params(config["apiKey"], config["secretKey"])

    resp = requests.get(
        f"{stream_base}/stream/market/subscribe",
        params=params,
        stream=True,
        timeout=None,
    )
    resp.raise_for_status()
    for event in sseclient.SSEClient(resp).events():
        if event.event in ("heartbeat", "connected"):
            continue
        if event.event == "market":
            write_market(event.data, output_dir)


def run_signal(config: dict, token_ids: list = None) -> None:
    stream_base = config.get("streamBaseUrl", "https://stream.valuescan.ai")
    output_dir = config["outputDir"]
    params = build_stream_params(config["apiKey"], config["secretKey"])
    if token_ids:
        params["tokens"] = ",".join(token_ids)

    resp = requests.get(
        f"{stream_base}/stream/signal/subscribe",
        params=params,
        stream=True,
        timeout=None,
    )
    resp.raise_for_status()
    for event in sseclient.SSEClient(resp).events():
        if event.event in ("heartbeat", "connected"):
            continue
        if event.event == "signal":
            write_signal(event.data, output_dir)


# ── Entry point ───────────────────────────────────────────────────────────────

def main() -> None:
    parser = argparse.ArgumentParser(description="ValueScan Stream Monitor")
    parser.add_argument("--market", action="store_true", help="Subscribe to market analysis stream")
    parser.add_argument("--signal", action="store_true", help="Subscribe to token signal stream")
    parser.add_argument("--tokens", type=str, default="", help="Comma-separated token symbols, e.g. BTC,ETH")
    parser.add_argument("--config", type=str, default=str(PID_DIR / "config.json"))
    args = parser.parse_args()

    if not args.market and not args.signal:
        print("Error: specify --market or --signal", file=sys.stderr)
        sys.exit(1)
    if args.market and args.signal:
        print("Error: use --market or --signal in separate processes", file=sys.stderr)
        sys.exit(1)

    with open(Path(args.config).expanduser(), "r", encoding="utf-8") as f:
        config = json.load(f)

    mode = "market" if args.market else "signal"
    write_pid(mode)

    def _shutdown(signum=None, frame=None):
        cleanup_pid(mode)
        sys.exit(0)

    signal_module.signal(signal_module.SIGTERM, _shutdown)
    signal_module.signal(signal_module.SIGINT, _shutdown)

    try:
        if args.market:
            run_market(config)
        else:
            token_ids = None
            if args.tokens:
                symbols = [s.strip() for s in args.tokens.split(",") if s.strip()]
                api_base = config.get("apiBaseUrl", "https://api.valuescan.io")
                token_ids = resolve_token_ids(symbols, config["apiKey"], config["secretKey"], api_base)
                if not token_ids:
                    print("Error: none of the specified tokens could be resolved", file=sys.stderr)
                    sys.exit(1)
            run_signal(config, token_ids)
    finally:
        cleanup_pid(mode)


if __name__ == "__main__":
    main()


path: script/sign.js
content: 'use strict';

const crypto = require('crypto');

/**
 * Build stream SSE authentication query parameters.
 * Auth: HMAC-SHA256(key=secretKey, message=timestamp+'{}')
 *
 * @param {string} apiKey
 * @param {string} secretKey
 * @returns {{ [REDACTED_SECRET], timestamp: string, nonce: string, sign: string }}
 */
function buildStreamParams(apiKey, secretKey) {
    const timestamp = Date.now().toString();
    const nonce = crypto.randomUUID();
    const sign = crypto.createHmac('sha256', secretKey)
        .update(timestamp + nonce)
        .digest('hex');
    return { apiKey, timestamp, nonce, sign };
}

/**
 * Build main API authentication headers.
 * Auth: HMAC-SHA256(key=secretKey, message=timestamp+body)
 *
 * @param {string} apiKey
 * @param {string} secretKey
 * @param {string} body  Raw POST body string
 * @returns {Record<string, string>}
 */
function buildApiHeaders(apiKey, secretKey, body) {
    const timestamp = Date.now().toString();
    const sign = crypto.createHmac('sha256', secretKey)
        .update(timestamp + body)
        .digest('hex');
    return {
        'X-API-KEY': apiKey,
        'X-TIMESTAMP': timestamp,
        'X-SIGN': sign,
        'Content-Type': 'application/json; charset=utf-8',
    };
}

module.exports = { buildStreamParams, buildApiHeaders };


path: script/package.json
content: {
  "name": "vs-monitor-skill-script",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "monitor": "ts-node monitor.ts"
  },
  "dependencies": {
    "eventsource": "^2.0.2"
  },
  "devDependencies": {
    "@types/eventsource": "^1.1.15",
    "@types/node": "^20.0.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.0.0"
  }
}


path: script/monitor.ts
content: #!/usr/bin/env npx ts-node
// Dependencies: npm install eventsource @types/eventsource ts-node typescript
/**
 * ValueScan Stream Monitor (TypeScript)
 *
 * Usage:
 *   npx ts-node monitor.ts --market [--config=~/.vs-monitor/config.json]
 *   npx ts-node monitor.ts --signal [--tokens=BTC,ETH] [--config=~/.vs-monitor/config.json]
 */

import * as crypto from 'crypto';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import EventSource from 'eventsource';

const PID_DIR = path.join(os.homedir(), '.vs-monitor');

interface Config {
    [REDACTED_SECRET];
    secretKey: string;
    outputDir: string;
    streamBaseUrl?: string;
    apiBaseUrl?: string;
}

// ── Auth ──────────────────────────────────────────────────────────────────────

function hmacHex(secretKey: string, message: string): string {
    return crypto.createHmac('sha256', secretKey).update(message).digest('hex');
}

function buildStreamUrl(base: string, endpoint: string, [REDACTED_SECRET], secretKey: string, tokens?: string): string {
    const timestamp = Date.now().toString();
    const nonce = crypto.randomUUID();
    const sign = hmacHex(secretKey, timestamp + nonce);
    const params = new URLSearchParams({ apiKey, timestamp, nonce, sign });
    if (tokens) params.set('tokens', tokens);
    return `${base}${endpoint}?${params.toString()}`;
}

function buildApiHeaders([REDACTED_SECRET], secretKey: string, body: string): Record<string, string> {
    const timestamp = Date.now().toString();
    const sign = hmacHex(secretKey, timestamp + body);
    return {
        'X-API-KEY': apiKey,
        'X-TIMESTAMP': timestamp,
        'X-SIGN': sign,
        'Content-Type': 'application/json; charset=utf-8',
    };
}

// ── Token resolution ──────────────────────────────────────────────────────────

async function resolveTokenIds(symbols: string[], [REDACTED_SECRET], secretKey: string, apiBaseUrl: string): Promise<string[]> {
    const tokenIds: string[] = [];
    for (const symbol of symbols) {
        const body = JSON.stringify({ search: symbol });
        const headers = buildApiHeaders(apiKey, secretKey, body);
        try {
            const resp = await fetch(`${apiBaseUrl}/api/open/v1/vs-token/list`, {
                method: 'POST', headers, body,
            });
            const data = await resp.json() as { data?: Array<{ id: number; symbol: string }> };
            const records = data.data ?? [];
            if (records.length === 0) {
                console.warn(`[warn] Symbol not found: ${symbol}`);
                continue;
            }
            const match = records.find(r => r.symbol.toUpperCase() === symbol.toUpperCase()) ?? records[0];
            if (match) tokenIds.push(match.id.toString());
            else console.warn(`[warn] Symbol not found: ${symbol}`);
        } catch (e) {
            console.warn(`[warn] Failed to resolve symbol ${symbol}:`, e);
        }
    }
    return tokenIds;
}

// ── File writing ──────────────────────────────────────────────────────────────

function writeMarket(content: string, outputDir: string): void {
    const now = new Date();
    const dateStr = now.toISOString().slice(0, 10);
    const timeStr = now.toTimeString().slice(0, 8);
    const dir = path.join(outputDir, '大盘分析');
    fs.mkdirSync(dir, { recursive: true });
    fs.appendFileSync(path.join(dir, `大盘分析-${dateStr}.txt`), `[${timeStr}]\n${content}\n---\n`, 'utf-8');
}

function writeSignal(payload: string, outputDir: string): void {
    const msg = JSON.parse(payload) as { type: string; content: string };
    let inner: { symbol?: string } = {};
    try { inner = JSON.parse(msg.content); } catch { /* ignore */ }
    const symbol = inner.symbol ?? 'UNKNOWN';
    const now = new Date();
    const dateStr = now.toISOString().slice(0, 10);
    const timeStr = now.toTimeString().slice(0, 8);
    const dir = path.join(outputDir, '代币信号', dateStr);
    fs.mkdirSync(dir, { recursive: true });
    fs.appendFileSync(path.join(dir, `${symbol}.txt`), `[${timeStr}] [${msg.type}]\n${msg.content}\n---\n`, 'utf-8');
}

// ── PID management ────────────────────────────────────────────────────────────

function writePid(mode: string): void {
    fs.mkdirSync(PID_DIR, { recursive: true });
    fs.writeFileSync(path.join(PID_DIR, `${mode}.pid`), process.pid.toString());
}

function cleanupPid(mode: string): void {
    const pidFile = path.join(PID_DIR, `${mode}.pid`);
    if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
}

// ── Monitor ───────────────────────────────────────────────────────────────────

function runMarket(config: Config): void {
    const base = config.streamBaseUrl ?? 'https://stream.valuescan.ai';
    const url = buildStreamUrl(base, '/stream/market/subscribe', config.apiKey, config.secretKey);
    const es = new EventSource(url);

    es.addEventListener('market', (event: MessageEvent) => {
        writeMarket(event.data, config.outputDir);
    });

    es.onerror = () => { console.error('SSE connection error'); es.close(); process.exit(1); };
}

async function runSignal(config: Config, tokens?: string): Promise<void> {
    const base = config.streamBaseUrl ?? 'https://stream.valuescan.ai';
    let tokenParam: string | undefined;

    if (tokens) {
        const symbols = tokens.split(',').map(s => s.trim()).filter(Boolean);
        const apiBase = config.apiBaseUrl ?? 'https://api.valuescan.io';
        const ids = await resolveTokenIds(symbols, config.apiKey, config.secretKey, apiBase);
        if (ids.length === 0) {
            console.error('Error: none of the specified tokens could be resolved');
            process.exit(1);
        }
        tokenParam = ids.join(',');
    }

    const url = buildStreamUrl(base, '/stream/signal/subscribe', config.apiKey, config.secretKey, tokenParam);
    const es = new EventSource(url);

    es.addEventListener('signal', (event: MessageEvent) => {
        writeSignal(event.data, config.outputDir);
    });

    es.onerror = () => { console.error('SSE connection error'); es.close(); process.exit(1); };
}

// ── Entry point ───────────────────────────────────────────────────────────────

function loadConfig(configPath: string): Config {
    const resolved = configPath.startsWith('~') ? path.join(os.homedir(), configPath.slice(1)) : configPath;
    return JSON.parse(fs.readFileSync(resolved, 'utf-8'));
}

function getFlag(args: string[], flag: string): boolean {
    return args.includes(flag);
}

function getFlagValue(args: string[], flag: string): string | undefined {
    const entry = args.find(a => a.startsWith(`${flag}=`));
    return entry ? entry.slice(flag.length + 1) : undefined;
}

async function main(): Promise<void> {
    const args = process.argv.slice(2);
    const isMarket = getFlag(args, '--market');
    const isSignal = getFlag(args, '--signal');
    const tokens = getFlagValue(args, '--tokens');
    const configPath = getFlagValue(args, '--config') ?? path.join(PID_DIR, 'config.json');

    if (!isMarket && !isSignal) {
        console.error('Error: specify --market or --signal');
        process.exit(1);
    }
    if (isMarket && isSignal) {
        console.error('Error: use --market or --signal in separate processes');
        process.exit(1);
    }

    const config = loadConfig(configPath);
    const mode = isMarket ? 'market' : 'signal';
    writePid(mode);

    const cleanup = () => { cleanupPid(mode); process.exit(0); };
    process.on('SIGTERM', cleanup);
    process.on('SIGINT', cleanup);

    if (isMarket) {
        runMarket(config);
    } else {
        await runSignal(config, tokens);
    }
}

main().catch(err => { console.error(err); process.exit(1); });


path: references/stream-api.json
content: [
  {
    "name": "大盘分析订阅",
    "endpoint": "GET /stream/market/subscribe",
    "description": "SSE 长连接，实时接收大盘综合分析推送。连接成功后推送 connected 事件，定期推送 heartbeat 事件，有分析时推送 market 事件。",
    "needToken": true,
    "auth": {
      "method": "query_params",
      "params": {
        "apiKey": "API Key",
        "timestamp": "当前毫秒时间戳（字符串）",
        "nonce": "随机 UUID",
        "sign": "HMAC-SHA256(key=secretKey, message=timestamp+nonce)，16 进制字符串"
      }
    },
    "events": {
      "connected": "连接成功确认，data='subscribed'",
      "heartbeat": "心跳保活，data='ping'，忽略即可",
      "market": "大盘分析内容，data 为分析文本字符串"
    }
  },
  {
    "name": "代币信号订阅",
    "endpoint": "GET /stream/signal/subscribe",
    "description": "SSE 长连接，实时接收代币信号推送。tokens 不传则订阅所有代币信号（全量订阅）。",
    "needToken": true,
    "auth": {
      "method": "query_params",
      "params": {
        "apiKey": "API Key",
        "timestamp": "当前毫秒时间戳（字符串）",
        "nonce": "随机 UUID",
        "sign": "HMAC-SHA256(key=secretKey, message=timestamp+nonce)，16 进制字符串"
      }
    },
    "params": {
      "tokens": "（可选）逗号分隔的 vsTokenId 列表，不传则全量订阅"
    },
    "events": {
      "connected": "连接成功确认，data='subscribed'",
      "heartbeat": "心跳保活，data='ping'，忽略即可",
      "signal": "代币信号，data 为 JSON 字符串，结构见 signal_schema"
    },
    "signal_schema": {
      "tokenId": "long — vsTokenId",
      "type": "string — CHANCE | RISK | FUNDS",
      "content": "string — 信号详情 JSON，含 symbol、name 等字段",
      "ts": "long — 信号时间戳（毫秒）",
      "uniqueKey": "string — 信号唯一标识"
    }
  }
]


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)