Skill: liji3597/solana-alerts (version 0.1.0)

--- name: solana-alerts description: 创建和管理价格警报 — 当代币价格高于或低于目标值时通知用户。当用户想设置价格提醒、查看或删除警报时触发。 version: 1.0.0 metadata: openclaw: requires: env: ["SOLANA_NETWORK"] bins: ["node"] emoji: "🔔" --- # Solana 价格警报系统 ## When to Use - 用户想在代币达到某个价格时收到通知 - 用户想查看或删除已设置的警报 - 用户提到"提醒"、"通知"、"警报"、"到了xxxx通知我" ## Workflow ### 用户想设置价格警报 1. **确定代币** — 从用户消息中提取，或询问："监控哪个代币？" 2. **确定方向** — 判断用户是想在价格**上涨**还是**下跌**时通知 - "SOL 到 200" → `above`（高于） - "SOL 跌到 100" → `below`（低于） - 如果不确定，询问："是价格高于还是低于这个值时通知你？" 3. **确定目标价** — 从用户消息中提取数字 4. **确认** — "好的，当 SOL 价格高于 $200 时通知你，确认吗？" 5. **执行** — `node skills/solana-alerts/scripts/create-alert.js <user_id> <token> <above|below> <price>` 6. **后续** — "警报已设置！你可以说'我的警报'来查看所有活跃警报" ### 用户想查看警报 1. `node skills/solana-alerts/scripts/list-alerts.js <user_id>` 2. 如果无警报，引导创建 3. 展示每个警报的代币、方向、目标价 ### 用户想删除警报 1. 如果未指定 ID，先列出所有警报让用户选择 2. 确认："确定删除警报 #X 吗？" 3. `node skills/solana-alerts/scripts/delete-alert.js <user_id> <alert_id>` ### 手动检查价格触发 1. `node skills/solana-alerts/scripts/check-prices.js` 2. 将触发的警报信息通知用户 ## Guardrails - **最多 20 个警报** — 超出时提示用户清理旧警报 - **合理性检查** — 如果目标价偏离当前价格超过 50%，温和提醒用户确认 - **不预测价格** — 不说"SOL 很可能到 200"，只设置警报 - **不重复创建** — 如果已存在相同条件的警报，提醒用户 ## Available Scripts | 脚本 | 用途 | 参数 | |------|------|------| | `create-alert.js` | 创建警报 | `<user_id> <token> <above\|below> <price>` | | `list-alerts.js` | 列出警报 | `<user_id> [--lang en]` | | `delete-alert.js` | 删除警报 | `<user_id> <alert_id>` | | `check-prices.js` | 手动检查 | （无参数） |

path: scripts/check-prices.js
content: #!/usr/bin/env node
/**
 * Check all active alerts against current prices
 * Usage: node check-prices.js [--system] [--lang zh|en]
 */
const path = require('path');
const sharedDir = path.resolve(__dirname, '..', '..', '..', 'shared');

const { runAlertCheck } = require(path.join(sharedDir, 'scheduler'));
const { formatError } = require(path.join(sharedDir, 'errors'));

const lang = process.argv.includes('--lang') ? process.argv[process.argv.indexOf('--lang') + 1] : 'zh';

async function main() {
    // Security: verify this is a system-authorized call
    if (process.env.OPENCLAW_SYSTEM !== 'true' && !process.argv.includes('--system')) {
        console.log(`❌ ${formatError('INTERNAL_ERROR', lang)}: ${lang === 'zh' ? '未授权的系统调用' : 'Unauthorized system call'}`);
        process.exit(1);
    }

    if (typeof runAlertCheck !== 'function') {
        console.log(`❌ ${formatError('INTERNAL_ERROR', lang)}`);
        process.exit(1);
    }

    const summary = await runAlertCheck();
    if (!summary || summary.checked === 0) {
        console.log('📭 暂无活跃警报 / No active alerts');
        process.exit(0);
    }

    if (summary.triggered === 0) {
        console.log(`✅ ${lang === 'zh' ? '已检查，暂无触发警报' : 'Checked, no alerts triggered'}`);
        process.exit(0);
    }

    for (const item of summary.triggeredAlerts || []) {
        let condStr = item.condition === 'above' ? '高于 / above' : '低于 / below';
        if (item.alertType === 'stop_loss') condStr = '止损 / stop-loss';
        if (item.alertType === 'take_profit') condStr = '止盈 / take-profit';
        console.log(
            `🚨 TRIGGERED: ${item.symbol} ${condStr} ${item.targetPrice} (current: ${item.currentPrice})`
        );
    }

    console.log(
        `✅ ${lang === 'zh' ? `本次触发 ${summary.triggered} 条，已通知 ${summary.notified} 位用户` : `Triggered ${summary.triggered}, notified ${summary.notified} users`}`
    );
}

main().catch((err) => {
    console.log(`❌ ${formatError(err.code || 'INTERNAL_ERROR', lang)}`);
    process.exit(1);
});


path: scripts/create-alert.js
content: #!/usr/bin/env node
/**
 * Create a price alert
 * Usage: node create-alert.js <telegram_user_id> <token_symbol> <condition> <target_price> [--lang zh|en]
 */
const path = require('path');
const sharedDir = path.resolve(__dirname, '..', '..', '..', 'shared');

const services = require(path.join(sharedDir, 'services'));
const { formatError } = require(path.join(sharedDir, 'errors'));
const { formatUSD } = require(path.join(sharedDir, 'formatter'));

const createAlert = services.createAlert || services.createAlertForUser;

const telegramId = process.argv[2];
const tokenSymbolRaw = process.argv[3];
const condition = process.argv[4];
const targetPriceRaw = process.argv[5];
const lang = process.argv.includes('--lang') ? process.argv[process.argv.indexOf('--lang') + 1] : 'zh';

const missing = [];
if (!telegramId) missing.push('用户ID / User ID');
if (!tokenSymbolRaw) missing.push('代币符号 / Token symbol');
if (!condition) missing.push('条件 / Condition');
if (!targetPriceRaw) missing.push('目标价格 / Target price');
if (missing.length > 0) {
    console.log(JSON.stringify({
        code: 'MISSING_PARAMS',
        missing,
        message: `我需要更多信息才能继续：${missing.join('、')} / I need more information to proceed: ${missing.join(', ')}`
    }, null, 2));
    process.exit(0);
}

if (typeof createAlert !== 'function') {
    console.log(`❌ ${formatError('INTERNAL_ERROR', lang)}`);
    process.exit(1);
}

const tokenSymbol = tokenSymbolRaw.toUpperCase();
const result = createAlert(telegramId, {
    tokenSymbol,
    condition,
    targetPrice: targetPriceRaw,
});

if (!result.ok) {
    console.log(`❌ ${formatError(result.code, lang)}`);
    process.exit(1);
}

const targetPrice = Number.parseFloat(targetPriceRaw);
const condStr = condition === 'above' ? '高于 / above' : '低于 / below';

console.log(`🔔 ${formatError(result.code || 'ALERT_CREATED', lang)} / Alert created!`);
console.log(`  ID: #${result.alertId}`);
console.log(`  代币 / [REDACTED_SECRET]`);
console.log(`  条件 / Condition: ${condStr} ${formatUSD(targetPrice)}`);


path: scripts/create-stop-loss.js
content: #!/usr/bin/env node
/**
 * Create a stop-loss alert based on cost basis drawdown.
 * Usage: node create-stop-loss.js <telegram_user_id> <token_symbol> <loss_percent> [--lang zh|en]
 */
const path = require('path');
const sharedDir = path.resolve(__dirname, '..', '..', '..', 'shared');

const { createStopLossAlertForUser } = require(path.join(sharedDir, 'services'));
const { formatError } = require(path.join(sharedDir, 'errors'));

const telegramId = process.argv[2];
const tokenSymbolRaw = process.argv[3];
const lossPercentRaw = process.argv[4];
const lang = process.argv.includes('--lang') ? process.argv[process.argv.indexOf('--lang') + 1] : 'zh';
const isZh = lang !== 'en';

const missing = [];
if (!telegramId) missing.push('用户ID / User ID');
if (!tokenSymbolRaw) missing.push('代币符号 / Token symbol');
if (!lossPercentRaw) missing.push('止损百分比 / Stop-loss percent');

if (missing.length > 0) {
    console.log(JSON.stringify({
        code: 'MISSING_PARAMS',
        missing,
        message: `我需要更多信息才能继续：${missing.join('、')} / I need more information to proceed: ${missing.join(', ')}`,
    }, null, 2));
    process.exit(0);
}

if (typeof createStopLossAlertForUser !== 'function') {
    console.log(`❌ ${formatError('INTERNAL_ERROR', lang)}`);
    process.exit(1);
}

const tokenSymbol = tokenSymbolRaw.toUpperCase();
const lossPercent = Number.parseFloat(lossPercentRaw);
if (!Number.isFinite(lossPercent) || lossPercent <= 0 || lossPercent >= 100) {
    console.log(`❌ ${formatError('INVALID_ALERT_PERCENTAGE', lang)}: ${isZh ? '止损百分比需在 0-100 之间' : 'Stop-loss percent must be between 0 and 100'}`);
    process.exit(1);
}

const result = createStopLossAlertForUser(telegramId, {
    tokenSymbol,
    lossPercent,
});

if (!result.ok) {
    console.log(`❌ ${formatError(result.code, lang)}`);
    process.exit(1);
}

console.log(`🛑 ${isZh ? '止损提醒已创建' : 'Stop-loss alert created'} / Alert created!`);
console.log(`  ID: #${result.alertId}`);
console.log(`  代币 / [REDACTED_SECRET]`);
console.log(`  阈值 / Threshold: ${lossPercent.toFixed(2)}%`);


path: scripts/create-take-profit.js
content: #!/usr/bin/env node
/**
 * Create a take-profit alert based on cost basis gain.
 * Usage: node create-take-profit.js <telegram_user_id> <token_symbol> <profit_percent> [--lang zh|en]
 */
const path = require('path');
const sharedDir = path.resolve(__dirname, '..', '..', '..', 'shared');

const { createTakeProfitAlertForUser } = require(path.join(sharedDir, 'services'));
const { formatError } = require(path.join(sharedDir, 'errors'));

const telegramId = process.argv[2];
const tokenSymbolRaw = process.argv[3];
const profitPercentRaw = process.argv[4];
const lang = process.argv.includes('--lang') ? process.argv[process.argv.indexOf('--lang') + 1] : 'zh';
const isZh = lang !== 'en';

const missing = [];
if (!telegramId) missing.push('用户ID / User ID');
if (!tokenSymbolRaw) missing.push('代币符号 / Token symbol');
if (!profitPercentRaw) missing.push('止盈百分比 / Take-profit percent');

if (missing.length > 0) {
    console.log(JSON.stringify({
        code: 'MISSING_PARAMS',
        missing,
        message: `我需要更多信息才能继续：${missing.join('、')} / I need more information to proceed: ${missing.join(', ')}`,
    }, null, 2));
    process.exit(0);
}

if (typeof createTakeProfitAlertForUser !== 'function') {
    console.log(`❌ ${formatError('INTERNAL_ERROR', lang)}`);
    process.exit(1);
}

const tokenSymbol = tokenSymbolRaw.toUpperCase();
const profitPercent = Number.parseFloat(profitPercentRaw);
if (!Number.isFinite(profitPercent) || profitPercent <= 0 || profitPercent > 1000) {
    console.log(`❌ ${formatError('INVALID_ALERT_PERCENTAGE', lang)}: ${isZh ? '止盈百分比需大于 0 且不超过 1000' : 'Take-profit percent must be > 0 and <= 1000'}`);
    process.exit(1);
}

const result = createTakeProfitAlertForUser(telegramId, {
    tokenSymbol,
    profitPercent,
});

if (!result.ok) {
    console.log(`❌ ${formatError(result.code, lang)}`);
    process.exit(1);
}

console.log(`🎯 ${isZh ? '止盈提醒已创建' : 'Take-profit alert created'} / Alert created!`);
console.log(`  ID: #${result.alertId}`);
console.log(`  代币 / [REDACTED_SECRET]`);
console.log(`  阈值 / Threshold: ${profitPercent.toFixed(2)}%`);


path: scripts/delete-alert.js
content: #!/usr/bin/env node
/**
 * Delete a price alert
 * Usage: node delete-alert.js <telegram_user_id> <alert_id> [--lang zh|en]
 */
const path = require('path');
const sharedDir = path.resolve(__dirname, '..', '..', '..', 'shared');

const services = require(path.join(sharedDir, 'services'));
const { formatError } = require(path.join(sharedDir, 'errors'));

const deleteAlert = services.deleteAlert || services.deleteAlertForUser;

const telegramId = process.argv[2];
const alertIdRaw = process.argv[3];
const lang = process.argv.includes('--lang') ? process.argv[process.argv.indexOf('--lang') + 1] : 'zh';

const missing = [];
if (!telegramId) missing.push('用户ID / User ID');
if (!alertIdRaw) missing.push('警报ID / Alert ID');
if (missing.length > 0) {
    console.log(JSON.stringify({
        code: 'MISSING_PARAMS',
        missing,
        message: `我需要更多信息才能继续：${missing.join('、')} / I need more information to proceed: ${missing.join(', ')}`
    }, null, 2));
    process.exit(0);
}

if (typeof deleteAlert !== 'function') {
    console.log(`❌ ${formatError('INTERNAL_ERROR', lang)}`);
    process.exit(1);
}

const alertId = Number.parseInt(alertIdRaw, 10);
if (!Number.isInteger(alertId) || alertId <= 0) {
    console.log(`❌ ${formatError('INVALID_ALERT_ID', lang)}`);
    process.exit(1);
}

const result = deleteAlert(telegramId, alertId);
if (!result.ok) {
    if (result.code === 'ALERT_NOT_FOUND') {
        console.log(`⚠️ ${formatError(result.code, lang)}`);
        process.exit(0);
    }
    console.log(`❌ ${formatError(result.code, lang)}`);
    process.exit(1);
}

console.log(`✅ ${formatError(result.code || 'ALERT_DELETED', lang)} / Alert #${alertId} deleted`);


path: scripts/list-alerts.js
content: #!/usr/bin/env node
/**
 * List active price alerts for a user
 * Usage: node list-alerts.js <telegram_user_id> [--lang zh|en]
 */
const path = require('path');
const sharedDir = path.resolve(__dirname, '..', '..', '..', 'shared');

const { listAlerts } = require(path.join(sharedDir, 'services'));
const { formatError } = require(path.join(sharedDir, 'errors'));
const { formatAlert } = require(path.join(sharedDir, 'formatter'));

const telegramId = process.argv[2];
const lang = process.argv.includes('--lang') ? process.argv[process.argv.indexOf('--lang') + 1] : 'zh';

if (!telegramId) {
    console.log(JSON.stringify({
        code: 'MISSING_PARAMS',
        missing: ['用户ID / User ID'],
        message: '我需要更多信息才能继续：用户ID / User ID / I need more information to proceed: User ID'
    }, null, 2));
    process.exit(0);
}

if (typeof listAlerts !== 'function') {
    console.log(`❌ ${formatError('INTERNAL_ERROR', lang)}`);
    process.exit(1);
}

const result = listAlerts(telegramId);
if (!result.ok) {
    console.log(`❌ ${formatError(result.code, lang)}`);
    process.exit(1);
}

const alerts = result.alerts || [];
if (alerts.length === 0) {
    console.log('📭 暂无价格警报 / No active alerts');
    process.exit(0);
}

console.log(`🔔 价格警报列表 / Price Alerts (${alerts.length}):\n`);
for (const alert of alerts) {
    console.log(formatAlert(alert, lang));
}


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