Tools
Native Moonshot
OpenClaw Moonshot Plugin 安裝教學
Configuration Example
{
"id": "moonshot-native",
"name": "Moonshot Native Commands",
"version": "1.0.0",
"description": "Native commands for querying Moonshot API balance with trend analysis",
"entry": "index.ts",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"alertThreshold": {
"type": "number",
"description": "Balance alert threshold in USD",
"default": 5
}
}
}
}
README
好的,這是一份完整的 **OpenClaw Plugin 安裝教學**,專門針對 Moonshot API 餘額查詢插件,可以直接放到 GitHub 上。
---
# OpenClaw Moonshot Plugin 安裝教學
[](https://openclaw.io)
[](https://platform.moonshot.ai)
> 一個 OpenClaw Native Plugin,提供 Telegram 與 TUI 雙介面的 Moonshot API 餘額即時查詢功能。
---
## 目錄
1. [功能特色](#功能特色)
2. [系統需求](#系統需求)
3. [安裝步驟](#安裝步驟)
4. [配置說明](#配置說明)
5. [指令使用](#指令使用)
6. [故障排除](#故障排除)
7. [進階設定](#進階設定)
8. [檔案結構](#檔案結構)
---
## 功能特色
| 功能 | 說明 |
|------|------|
| **即時餘額查詢** | 總餘額、代金券、現金分項顯示 |
| **趨勢預估** | 自動計算日均消耗與預估可用天數 |
| **歷史記錄** | 自動儲存查詢記錄,支援趨勢分析 |
| **智能警報** | 低餘額、負債、即將到期多層級提醒 |
| **雙介面支援** | Telegram Bot + TUI 終端機 |
| **Native Command** | 直接執行,無需 AI 解析,反應极速 |
---
## 系統需求
- **OpenClaw** >= 0.3.0
- **Node.js** >= 18.0.0
- **curl** 與 **jq** 指令
- **Moonshot API Key** ([取得方式](https://platform.moonshot.ai))
---
## 安裝步驟
### Step 1: 確認 OpenClaw 已安裝
```bash
# 檢查版本
openclaw --version
# 應顯示 >= 0.3.0
# 若未安裝,請參考官網 https://openclaw.io/docs/installation
```
### Step 2: 建立 Plugin 目錄結構
```bash
# 建立 extensions 目錄(OpenClaw 會自動載入此目錄下的 Plugin)
mkdir -p ~/.openclaw/extensions/moonshot-native
```
### Step 3: 下載 Plugin 檔案
```bash
# 方式 A: 直接複製(本教學)
# 將下方提供的檔案內容複製到對應位置
# 方式 B: Git Clone(如果你 fork 到 GitHub)
git clone https://github.com/YOUR_USERNAME/openclaw-moonshot-plugin.git
cp -r openclaw-moonshot-plugin/* ~/.openclaw/extensions/moonshot-native/
```
### Step 4: 建立必要檔案
#### 4.1 Plugin Manifest(必需)
建立 `~/.openclaw/extensions/moonshot-native/openclaw.plugin.json`:
```json
{
"id": "moonshot-native",
"name": "Moonshot Native Commands",
"version": "1.0.0",
"description": "Native commands for querying Moonshot API balance with trend analysis",
"entry": "index.ts",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"alertThreshold": {
"type": "number",
"description": "Balance alert threshold in USD",
"default": 5
}
}
}
}
```
#### 4.2 主程式檔案
建立 `~/.openclaw/extensions/moonshot-native/index.ts`:
```typescript
import { exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs';
import * as path from 'path';
const execAsync = promisify(exec);
export default function (api: any) {
const MOONSHOT_API_KEY = process.env.MOONSHOT_API_KEY;
const ALERT_THRESHOLD = parseFloat(process.env.MOONSHOT_ALERT_THRESHOLD || '5');
if (!MOONSHOT_API_KEY) {
api.registerCommand({
name: 'moonshot',
handler: () => ({
text: '❌ 錯誤:未設定 MOONSHOT_API_KEY\n\n請在 ~/.openclaw/.env 中加入:\nMOONSHOT_API_KEY=sk-你的-api-key'
})
});
return;
}
const HISTORY_DIR = path.join(process.env.HOME || '/root', '.openclaw', 'extensions', 'moonshot-native', 'history');
if (!fs.existsSync(HISTORY_DIR)) {
fs.mkdirSync(HISTORY_DIR, { recursive: true });
}
async function queryBalance() {
const { stdout } = await execAsync(
`curl -s --max-time 10 -H "Authorization: Bearer ${MOONSHOT_API_KEY}" -H "Content-Type: application/json" "https://api.moonshot.ai/v1/users/me/balance"`,
{ timeout: 15000 }
);
const data = JSON.parse(stdout);
if (data.code !== 0) throw new Error(data.message || 'API error');
return data.data;
}
function saveHistory(balanceData: any) {
try {
const record = { timestamp: new Date().toISOString(), balance: balanceData };
const historyFile = path.join(HISTORY_DIR, `history_${new Date().toISOString().slice(0,7)}.jsonl`);
fs.appendFileSync(historyFile, JSON.stringify(record) + '\n');
} catch (e) {
api.logger?.warn?.('[moonshot-native] Save history failed: ' + e);
}
}
function getHistoryTrend(days: number = 7): any[] {
try {
const files = fs.readdirSync(HISTORY_DIR).filter(f => f.endsWith('.jsonl'));
const records: any[] = [];
files.forEach(file => {
const content = fs.readFileSync(path.join(HISTORY_DIR, file), 'utf8');
content.split('\n').forEach(line => {
if (line.trim()) {
try {
const record = JSON.parse(line);
if (record.timestamp && record.balance) records.push(record);
} catch (e) {}
}
});
});
records.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
return records.slice(0, days);
} catch (e) {
return [];
}
}
function calculateTrend(currentBalance: number, history: any[]) {
const result = {
avgDailyCost: 0,
estimatedDays: 'N/A',
trendDirection: '→',
trendEmoji: '➡️',
totalSpent: 0,
recordsCount: history.length
};
if (history.length < 2) return result;
const oldest = parseFloat(history[history.length - 1].balance?.available_balance || 0);
const newest = history[0].balance?.available_balance
? parseFloat(history[0].balance.available_balance)
: currentBalance;
const totalDiff = oldest - newest;
result.totalSpent = totalDiff > 0 ? totalDiff : 0;
const oldestDate = new Date(history[history.length - 1].timestamp);
const newestDate = new Date(history[0].timestamp);
const daysDiff = Math.max(1, (newestDate.getTime() - oldestDate.getTime()) / (1000 * 60 * 60 * 24));
result.avgDailyCost = result.totalSpent / daysDiff;
if (result.avgDailyCost > 0) {
result.trendDirection = '下降';
result.trendEmoji = '📉';
}
if (result.avgDailyCost > 0 && currentBalance > 0) {
const days = Math.floor(currentBalance / result.avgDailyCost);
result.estimatedDays = days > 999 ? '999+' : days.toString();
} else if (currentBalance > 0) {
result.estimatedDays = '∞';
}
return result;
}
function formatDetailedReport(data: any, history: any[]): string {
const available = parseFloat(data.available_balance);
const voucher = parseFloat(data.voucher_balance);
const cash = parseFloat(data.cash_balance);
const availableStr = available.toFixed(5);
const voucherStr = voucher.toFixed(5);
const cashStr = cash.toFixed(5);
const total = available;
const voucherPct = total > 0 ? ((voucher / total) * 100).toFixed(2) : 0;
const status = available > 0 ? '✅ 正常' : '❌ 已停用';
const trend = calculateTrend(available, history);
let changeText = '';
if (history.length >= 1 && history[0].balance?.available_balance) {
const lastBalance = parseFloat(history[0].balance.available_balance);
const diff = available - lastBalance;
if (Math.abs(diff) > 0.00001) {
const emoji = diff > 0 ? '📈' : '📉';
changeText = `\n └─ 較上次: ${emoji} $${Math.abs(diff).toFixed(5)} USD`;
}
}
let report = `
🟢 Moonshot API 詳細報告
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⏰ ${new Date().toLocaleString('zh-TW')}
🔑 ${MOONSHOT_API_KEY.slice(0, 8)}...${MOONSHOT_API_KEY.slice(-4)}
💰 餘額明細
├─ 總可用: $${availableStr} USD
│ ├─ 🎫 代金券: $${voucherStr} USD (${voucherPct}%)
│ └─ 💳 現金: $${cashStr} USD
├─ 組成: ${'█'.repeat(Math.floor(parseFloat(voucherPct)/5))}${'░'.repeat(20-Math.floor(parseFloat(voucherPct)/5))}${changeText}
📈 使用趨勢與預估
├─ 歷史記錄: ${trend.recordsCount} 筆
├─ 總消耗: $${trend.totalSpent.toFixed(5)} USD
├─ 日均消耗: $${trend.avgDailyCost.toFixed(5)} USD
├─ 趨勢方向: ${trend.trendEmoji} ${trend.trendDirection}
└─ ⏳ 預估可用: ${trend.estimatedDays} 天
💳 帳戶狀態
├─ API 呼叫: ${status}
├─ 警報閾值: $${ALERT_THRESHOLD} USD
└─ 帳戶健康: ${cash < 0 ? '⚠️ 現金欠款' : '✅ 正常'}`;
if (available <= 0) {
report += `\n\n🚨 嚴重警報:可用餘額已用完!\n API 呼叫已停用,請立即儲值!`;
} else if (available < ALERT_THRESHOLD) {
report += `\n\n⚠️ 警報:餘額低於 $${ALERT_THRESHOLD} USD\n 建議盡快儲值以避免服務中斷。`;
}
if (cash < 0) {
report += `\n\n⚠️ 注意:現金餘額為負數 ($${cashStr} USD)\n 可用餘額僅包含代金券部分。`;
}
if (trend.estimatedDays !== 'N/A' && trend.estimatedDays !== '∞' && parseInt(trend.estimatedDays) < 7) {
report += `\n\n⏰ 提醒:預估僅剩 ${trend.estimatedDays} 天可用\n 建議本週內完成儲值。`;
}
report += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`;
return report;
}
function formatShortReport(data: any): string {
const available = parseFloat(data.available_balance).toFixed(2);
const voucher = parseFloat(data.voucher_balance).toFixed(2);
const cash = parseFloat(data.cash_balance).toFixed(2);
const status = parseFloat(available) > 0 ? '✅' : '❌';
return `💰 $${available} | 🎫 $${voucher} | 💳 $${cash} ${status}`;
}
function formatHistoryReport(records: any[]): string {
if (records.length === 0) {
return `📈 歷史趨勢\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n尚無記錄,請先執行 /moonshot 建立基準`;
}
const balances = records.map(r => parseFloat(r.balance?.available_balance || 0));
const maxBalance = Math.max(...balances);
const minBalance = Math.min(...balances);
const firstBalance = balances[balances.length - 1];
const lastBalance = balances[0];
const totalChange = firstBalance - lastBalance;
let report = `📈 Moonshot 餘額歷史(最近 ${records.length} 筆)\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
records.forEach((record, index) => {
const date = new Date(record.timestamp).toLocaleString('zh-TW', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
const balance = parseFloat(record.balance?.available_balance || 0).toFixed(2);
const emoji = index === 0 ? '📍' : ' ';
let diffText = '';
if (index < records.length - 1) {
const prev = parseFloat(records[index + 1].balance?.available_balance || 0);
const curr = parseFloat(record.balance?.available_balance || 0);
const diff = curr - prev;
if (Math.abs(diff) > 0.001) {
const diffEmoji = diff > 0 ? '↑' : '↓';
diffText = ` (${diffEmoji}$${Math.abs(diff).toFixed(2)})`;
}
}
... (truncated)
tools
Comments
Sign in to leave a comment