methodalgo-cli 1.0.1
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/LICENSE +21 -0
- package/README.md +276 -0
- package/bin/index.js +2 -0
- package/package.json +56 -0
- package/src/commands/config.js +80 -0
- package/src/commands/dashboard.js +579 -0
- package/src/commands/logout.js +13 -0
- package/src/commands/news.js +76 -0
- package/src/commands/signals.js +234 -0
- package/src/commands/snapshot.js +82 -0
- package/src/index.js +70 -0
- package/src/utils/api.js +108 -0
- package/src/utils/config-manager.js +23 -0
- package/src/utils/i18n.js +204 -0
- package/src/utils/logger.js +27 -0
- package/src/utils/onboard.js +45 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { signedRequest } from "../utils/api.js";
|
|
4
|
+
import logger from "../utils/logger.js";
|
|
5
|
+
import { t } from "../utils/i18n.js";
|
|
6
|
+
|
|
7
|
+
const newsCmd = new Command("news")
|
|
8
|
+
.description(t("NEWS_DESC"))
|
|
9
|
+
.option("-t, --type <type>", "News type. Use 'methodalgo news --help' to see all types.")
|
|
10
|
+
.option("-l, --limit <number>", t("OPT_LIMIT_DESC"), "10")
|
|
11
|
+
.option("-g, --language <lang>", "Language (zh, en)", "zh")
|
|
12
|
+
.option("-s, --search <keyword>", t("OPT_SEARCH_DESC"))
|
|
13
|
+
.option("-S, --start-date <date>", t("OPT_START_DATE_DESC"))
|
|
14
|
+
.option("-E, --end-date <date>", t("OPT_END_DATE_DESC"))
|
|
15
|
+
.option("--json", "Output raw JSON data")
|
|
16
|
+
.addHelpText("after", `\n${t("NEWS_LIMIT_NOTE")}\n\n${t("LABEL_EXAMPLE")}\n $ ${t("NEWS_EXAMPLE")}\n\n${t("NEWS_TYPES")}`)
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
if (!options.type) {
|
|
19
|
+
newsCmd.help();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const params = {
|
|
25
|
+
type: options.type,
|
|
26
|
+
limit: options.limit,
|
|
27
|
+
lang: options.language,
|
|
28
|
+
search: options.search,
|
|
29
|
+
startDate: options.startDate,
|
|
30
|
+
endDate: options.endDate
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const res = await signedRequest("/mcp/news", params);
|
|
34
|
+
const { status, data, message } = res.data;
|
|
35
|
+
|
|
36
|
+
if (!status) {
|
|
37
|
+
logger.error(`${t("ERR_NETWORK")}: ${message}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (options.json) {
|
|
42
|
+
logger.json(data);
|
|
43
|
+
} else {
|
|
44
|
+
const lang = t("FETCH_SUCCESS").includes("获取") ? "zh" : "en";
|
|
45
|
+
logger.success(t("FETCH_SUCCESS", { count: data.length }));
|
|
46
|
+
data.forEach((item, index) => {
|
|
47
|
+
const title = (item.title[lang] || item.title["en"]).replace(/\n/g, ", ");
|
|
48
|
+
let source = "MethodAlgo";
|
|
49
|
+
try { if (item.url) source = new URL(item.url).hostname.replace("www.", ""); } catch (_) {}
|
|
50
|
+
const date = new Date(item.publish_date).toLocaleString(lang === "zh" ? "zh-CN" : "en-US", {
|
|
51
|
+
hour12: false, month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit"
|
|
52
|
+
}).replace(/\//g, "-");
|
|
53
|
+
const excerpt = item.excerpt ? (item.excerpt[lang] || item.excerpt["en"]) : "";
|
|
54
|
+
|
|
55
|
+
console.log(`\n${chalk.bold(`[${index + 1}] ${title}`)} ${chalk.dim(`(${source} · ${date})`)}`);
|
|
56
|
+
if (excerpt) {
|
|
57
|
+
console.log(` ${chalk.gray(excerpt.replace(/\n/g, " ").substring(0, 150) + "...")}`);
|
|
58
|
+
}
|
|
59
|
+
if (item.url) {
|
|
60
|
+
console.log(` ${chalk.blue.underline(item.url)}`);
|
|
61
|
+
}
|
|
62
|
+
if (item.image_url) {
|
|
63
|
+
console.log(` ${chalk.yellow("🖼 Image: ")}${chalk.dim.underline(item.image_url)}`);
|
|
64
|
+
}
|
|
65
|
+
if (item.video_url) {
|
|
66
|
+
console.log(` ${chalk.yellow("📹 Video: ")}${chalk.dim.underline(item.video_url)}`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
console.log(""); // 底部留白
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logger.error(`News Error: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export default newsCmd;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { signedRequest } from "../utils/api.js";
|
|
4
|
+
import logger from "../utils/logger.js";
|
|
5
|
+
import { t, getLang } from "../utils/i18n.js";
|
|
6
|
+
|
|
7
|
+
const signalsCmd = new Command("signals")
|
|
8
|
+
.description(t("SIGNALS_DESC"))
|
|
9
|
+
.argument("[channel]", t("ARG_CHANNEL_DESC") || "Channel name")
|
|
10
|
+
.addHelpText("after", `\n${t("LABEL_EXAMPLE")}\n $ ${t("SIGNALS_EXAMPLE")}\n\n${t("SIGNALS_CHANNELS")}`)
|
|
11
|
+
.option("-l, --limit <number>", t("OPT_LIMIT_DESC"), "10")
|
|
12
|
+
.option("-a, --after <id>", t("OPT_AFTER_DESC"))
|
|
13
|
+
.option("--json", "Output raw JSON data")
|
|
14
|
+
.addHelpText("after", `\n${t("SIGNALS_LIMIT_NOTE")}`)
|
|
15
|
+
.action(async (channel, options) => {
|
|
16
|
+
if (!channel) {
|
|
17
|
+
signalsCmd.help();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const res = await signedRequest("/mcp/signals", {
|
|
22
|
+
channelName: channel,
|
|
23
|
+
limit: options.limit,
|
|
24
|
+
after: options.after
|
|
25
|
+
});
|
|
26
|
+
const { status, data, message } = res.data;
|
|
27
|
+
|
|
28
|
+
if (!status) {
|
|
29
|
+
logger.error(`${t("ERR_NETWORK")}: ${message}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (options.json) {
|
|
34
|
+
if (channel === "token-unlock") {
|
|
35
|
+
const cleanData = data.map(item => {
|
|
36
|
+
const sig = item.signals && item.signals[0];
|
|
37
|
+
if (!sig) return item;
|
|
38
|
+
|
|
39
|
+
const baseTime = new Date(item.timestamp).getTime();
|
|
40
|
+
const tokens = [];
|
|
41
|
+
for (const [key, value] of Object.entries(sig.details || {})) {
|
|
42
|
+
const keyMatch = key.match(/(.+)\s*-\s*(.+)\s*\n\s*In\s+(.+)/i);
|
|
43
|
+
if (!keyMatch) continue;
|
|
44
|
+
|
|
45
|
+
const symbol = keyMatch[1].trim();
|
|
46
|
+
const percent = keyMatch[2].trim();
|
|
47
|
+
const offsetStr = keyMatch[3].trim();
|
|
48
|
+
|
|
49
|
+
let offsetMs = 0;
|
|
50
|
+
const timeParts = offsetStr.match(/(\d+)\s*(Day|Hour|Min)/gi);
|
|
51
|
+
if (timeParts) {
|
|
52
|
+
timeParts.forEach(part => {
|
|
53
|
+
const m = part.match(/(\d+)\s*(d|h|m)/i);
|
|
54
|
+
if (m) {
|
|
55
|
+
const v = parseInt(m[1]);
|
|
56
|
+
const u = m[2].toLowerCase();
|
|
57
|
+
if (u.startsWith("d")) offsetMs += v * 24 * 60 * 60 * 1000;
|
|
58
|
+
else if (u.startsWith("h")) offsetMs += v * 60 * 60 * 1000;
|
|
59
|
+
else if (u.startsWith("m")) offsetMs += v * 60 * 1000;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const unlockTime = new Date(baseTime + offsetMs);
|
|
64
|
+
const diff = unlockTime.getTime() - Date.now();
|
|
65
|
+
let remaining = "Unlocked";
|
|
66
|
+
if (diff > 0) {
|
|
67
|
+
const rd = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
68
|
+
const rh = Math.floor((diff / (1000 * 60 * 60)) % 24);
|
|
69
|
+
const rm = Math.floor((diff / (1000 * 60)) % 60);
|
|
70
|
+
remaining = `In ${rd > 0 ? `${rd}d ` : ""}${rh > 0 ? `${rh}h ` : ""}${rm}m`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let mcapPercent = "";
|
|
74
|
+
const vLines = value.replace(/```/g, "").trim().split("\n");
|
|
75
|
+
const circulationSupply = (vLines.find(l => l.includes("🔋")) || "").replace("🔋", "").trim();
|
|
76
|
+
const unlockingProgress = (vLines.find(l => l.includes("⌛")) || "").replace("⌛", "").trim();
|
|
77
|
+
const unlockingAmount = (vLines.find(l => l.includes("🔑")) || "").replace("🔑", "").trim();
|
|
78
|
+
const unlockingValueLine = vLines.find(l => l.includes("💰")) || "";
|
|
79
|
+
const vMatch = unlockingValueLine.match(/\((.+)\)/);
|
|
80
|
+
if (vMatch) mcapPercent = vMatch[1].replace("of M.Cap", "").trim();
|
|
81
|
+
const unlockingValue = unlockingValueLine.replace("💰", "").replace(/\s*\(.+\)/, "").trim();
|
|
82
|
+
|
|
83
|
+
tokens.push({
|
|
84
|
+
symbol, percent, unlockAt: unlockTime.toISOString(), remaining,
|
|
85
|
+
circulationSupply, unlockingProgress, unlockingAmount, unlockingValue, mcapPercent
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return { ...item, tokens, signals: undefined }; // Simplify JSON
|
|
89
|
+
});
|
|
90
|
+
logger.json(cleanData);
|
|
91
|
+
} else {
|
|
92
|
+
logger.json(data);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
logger.success(t("FETCH_SUCCESS", { count: data.length }));
|
|
96
|
+
data.forEach((item, index) => {
|
|
97
|
+
const signals = (item.signals && item.signals.length > 0) ? item.signals : [{ title: item.title || item.content?.substring(0, 50) + "...", description: "", details: {} }];
|
|
98
|
+
const lang = getLang();
|
|
99
|
+
const date = new Date(item.timestamp).toLocaleString(lang === "zh" ? "zh-CN" : "en-US", {
|
|
100
|
+
hour12: false, month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit"
|
|
101
|
+
}).replace(/\//g, "-");
|
|
102
|
+
|
|
103
|
+
// 打印项目头部(如果只有一个信号且标题相同,可简化,但此处为了通用性保留索引)
|
|
104
|
+
if (channel !== "token-unlock") {
|
|
105
|
+
console.log(`\n${chalk.bold(`[${index + 1}]`)} ${chalk.dim(`(${date})`)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
signals.forEach((sig, sigIndex) => {
|
|
109
|
+
const title = sig.title;
|
|
110
|
+
const desc = sig.description || "";
|
|
111
|
+
const indent = channel === "token-unlock" ? "" : " ";
|
|
112
|
+
|
|
113
|
+
if (channel !== "token-unlock") {
|
|
114
|
+
console.log(`${indent}${chalk.bold(title)}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (channel.startsWith("breakout")) {
|
|
118
|
+
const breakPrice = sig?.details?.BreakPrice || sig?.breakPrice || sig?.break_price;
|
|
119
|
+
if (breakPrice) console.log(`${indent} ${chalk.yellow(`BreakPrice: ${breakPrice}`)}`);
|
|
120
|
+
} else if (channel.startsWith("exhaustion")) {
|
|
121
|
+
const symbol = title.includes(" for ") ? title.split(" for ").pop()?.trim() : "";
|
|
122
|
+
if (symbol) console.log(`${indent} ${chalk.green(`Symbol: ${symbol}`)}`);
|
|
123
|
+
const timeframe = sig?.details?.Timeframe || sig?.details?.timeframe;
|
|
124
|
+
const exhaustionSide = sig?.details?.["Exhaustion Side"] || sig?.details?.exhaustion_side;
|
|
125
|
+
if (timeframe) console.log(`${indent} ${chalk.cyan(`Timeframe: ${timeframe}`)}`);
|
|
126
|
+
if (exhaustionSide) console.log(`${indent} ${chalk.magenta(`Exhaustion Side: ${exhaustionSide}`)}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (channel === "token-unlock") {
|
|
130
|
+
// Token Unlock 特有逻辑(保持原样,但适配遍历环境)
|
|
131
|
+
if (sigIndex === 0) console.log(`\n${chalk.bold(`[${index + 1}] ${title}`)} ${chalk.dim(`(${date})`)}`);
|
|
132
|
+
const baseTime = new Date(item.timestamp).getTime();
|
|
133
|
+
let subIndex = 1;
|
|
134
|
+
for (const [key, value] of Object.entries(sig.details || {})) {
|
|
135
|
+
const keyMatch = key.match(/(.+)\s*-\s*(.+)\s*\n\s*In\s+(.+)/i);
|
|
136
|
+
if (!keyMatch) continue;
|
|
137
|
+
const token = keyMatch[1].trim();
|
|
138
|
+
const percent = keyMatch[2].trim();
|
|
139
|
+
const offsetStr = keyMatch[3].trim();
|
|
140
|
+
let offsetMs = 0;
|
|
141
|
+
const timeParts = offsetStr.match(/(\d+)\s*(Day|Hour|Min)/gi);
|
|
142
|
+
if (timeParts) {
|
|
143
|
+
timeParts.forEach(part => {
|
|
144
|
+
const m = part.match(/(\d+)\s*(d|h|m)/i);
|
|
145
|
+
if (m) {
|
|
146
|
+
const v = parseInt(m[1]);
|
|
147
|
+
const u = m[2].toLowerCase();
|
|
148
|
+
if (u.startsWith("d")) offsetMs += v * 24 * 60 * 60 * 1000;
|
|
149
|
+
else if (u.startsWith("h")) offsetMs += v * 60 * 60 * 1000;
|
|
150
|
+
else if (u.startsWith("m")) offsetMs += v * 60 * 1000;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const unlockTime = new Date(baseTime + offsetMs);
|
|
155
|
+
const diff = unlockTime.getTime() - Date.now();
|
|
156
|
+
let remainingStr = "";
|
|
157
|
+
if (diff <= 0) remainingStr = chalk.red("Unlocked / Ongoing");
|
|
158
|
+
else {
|
|
159
|
+
const rd = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
160
|
+
const rh = Math.floor((diff / (1000 * 60 * 60)) % 24);
|
|
161
|
+
const rm = Math.floor((diff / (1000 * 60)) % 60);
|
|
162
|
+
remainingStr = "In " + (rd > 0 ? `${rd}d ` : "") + (rh > 0 ? `${rh}h ` : "") + `${rm}m`;
|
|
163
|
+
}
|
|
164
|
+
let percentOfMCap = "";
|
|
165
|
+
const formattedValue = value.replace(/```/g, "").trim().split("\n")
|
|
166
|
+
.map(line => {
|
|
167
|
+
let l = line.trim();
|
|
168
|
+
if (l.includes("💰") && l.includes("(") && l.includes(")")) {
|
|
169
|
+
const pMatch = l.match(/\((.+)\)/);
|
|
170
|
+
if (pMatch) {
|
|
171
|
+
percentOfMCap = pMatch[1].replace("of M.Cap", "").trim();
|
|
172
|
+
l = l.replace(/\s*\(.+\)/, "");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
l = l.replace(/🔋/g, "Circulation supply: ")
|
|
176
|
+
.replace(/⌛/g, "Unlocking progress: ")
|
|
177
|
+
.replace(/🔑/g, "Unlocking amount: ")
|
|
178
|
+
.replace(/💰/g, "Unlocking value: ");
|
|
179
|
+
return ` ${l}`;
|
|
180
|
+
}).join("\n");
|
|
181
|
+
console.log(`\n ${chalk.yellow.bold(`[${subIndex++}] Token: ${token}`)}`);
|
|
182
|
+
console.log(` ${chalk.dim("Unlock At:")} ${chalk.white(unlockTime.toLocaleString(lang === "zh" ? "zh-CN" : "en-US", { hour12: false, month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }).replace(/\//g, "-"))} (${chalk.cyan(remainingStr)})`);
|
|
183
|
+
console.log(chalk.white(formattedValue));
|
|
184
|
+
if (percentOfMCap) console.log(` ${chalk.white(`Percent: ${percentOfMCap} of its Market Cap`)}`);
|
|
185
|
+
}
|
|
186
|
+
} else if (channel === "market-today") {
|
|
187
|
+
// Market Today 精细清洗逻辑
|
|
188
|
+
if (title === "Fear And Greed Index") {
|
|
189
|
+
if (sig?.details) {
|
|
190
|
+
for (const [k, v] of Object.entries(sig.details)) {
|
|
191
|
+
console.log(`${indent} ${chalk.cyan(`${k}:`)} ${chalk.white(v)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} else if (sig?.details) {
|
|
195
|
+
// 其他子信号(如果有)显示 details
|
|
196
|
+
for (const [k, v] of Object.entries(sig.details)) {
|
|
197
|
+
console.log(`${indent} ${chalk.cyan(`${k}:`)} ${chalk.white(v)}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else if (channel === "etf-tracker") {
|
|
201
|
+
if (sig?.details) {
|
|
202
|
+
for (const [k, v] of Object.entries(sig.details)) {
|
|
203
|
+
console.log(`${indent} ${chalk.cyan(`${k}:`)} ${chalk.white(v)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 描述处理:token-unlock 已处理
|
|
209
|
+
if (desc && channel !== "token-unlock") {
|
|
210
|
+
const cleanDesc = desc.replace(/```/g, "").trim().split("\n").map(l => `${indent} ${l}`).join("\n");
|
|
211
|
+
if (cleanDesc) console.log(chalk.gray(cleanDesc));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 图片/附件处理
|
|
215
|
+
const attachments = [...(sig.image ? [sig.image] : [])];
|
|
216
|
+
if (sigIndex === signals.length - 1 && item.attachments) {
|
|
217
|
+
attachments.push(...item.attachments);
|
|
218
|
+
}
|
|
219
|
+
if (attachments.length > 0) {
|
|
220
|
+
attachments.forEach(att => {
|
|
221
|
+
const url = typeof att === "string" ? att : (att.url || att.proxy_url);
|
|
222
|
+
if (url) console.log(`${indent} ${chalk.blue.underline(url)}`);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
console.log("");
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
logger.error(`Signals Error: ${error.message}`);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
export default signalsCmd;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import { signedRequest } from "../utils/api.js";
|
|
5
|
+
import logger from "../utils/logger.js";
|
|
6
|
+
import { t } from "../utils/i18n.js";
|
|
7
|
+
|
|
8
|
+
const snapshotCmd = new Command("snapshot")
|
|
9
|
+
.description(t("SNAPSHOT_DESC"))
|
|
10
|
+
.argument("<symbol>", t("ARG_SYMBOL_DESC"))
|
|
11
|
+
.argument("[tf]", t("ARG_TF_DESC"), "60")
|
|
12
|
+
.option("--json", "Output raw JSON data")
|
|
13
|
+
.option("-u, --url", t("OPT_URL_DESC"))
|
|
14
|
+
.option("-b, --buffer", t("OPT_BUFFER_DESC"))
|
|
15
|
+
.addHelpText("after", `\n${t("LABEL_EXAMPLE")}\n $ ${t("SNAPSHOT_EXAMPLE")}\n`)
|
|
16
|
+
.action(async (symbol, tf, options) => {
|
|
17
|
+
try {
|
|
18
|
+
let ticker = symbol.toUpperCase();
|
|
19
|
+
if (!ticker.startsWith("BINANCE:")) ticker = `BINANCE:${ticker}`;
|
|
20
|
+
|
|
21
|
+
// 智能选择存储模式优先级:--buffer > --url/--json > iTerm2 自动检测
|
|
22
|
+
const useLocal = (options.buffer || logger.isIterm2) && !options.json && !options.url;
|
|
23
|
+
// 如果显式指定了 --buffer,即使不是 iterm2 也尝试 local
|
|
24
|
+
const storage = (options.buffer || useLocal) ? "local" : "localurl";
|
|
25
|
+
const isBinaryMode = storage === "local";
|
|
26
|
+
|
|
27
|
+
const res = await signedRequest(
|
|
28
|
+
"/mcp/snapshot",
|
|
29
|
+
{ ticker, tf, storage },
|
|
30
|
+
isBinaryMode ? { responseType: "arraybuffer" } : {}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (isBinaryMode) {
|
|
34
|
+
// 处理二进制返回
|
|
35
|
+
const contentType = res.headers["content-type"] || "";
|
|
36
|
+
if (contentType.includes("image")) {
|
|
37
|
+
const buffer = Buffer.from(res.data);
|
|
38
|
+
if (options.json) {
|
|
39
|
+
logger.json({ ticker, tf, size: buffer.length, format: contentType });
|
|
40
|
+
} else {
|
|
41
|
+
logger.image(buffer, options.buffer); // 如果指定了 --buffer,强制渲染
|
|
42
|
+
logger.success(t("SNAPSHOT_SUCCESS", { ticker, tf }));
|
|
43
|
+
const tip = t("VAL_ALLOWED_KEYS").includes("键") ? "提示: 二进制模式下不显示 URL" : "Note: URL not available in binary mode";
|
|
44
|
+
logger.info(tip);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
} else {
|
|
48
|
+
// 可能是错误响应被转成了 buffer
|
|
49
|
+
const result = JSON.parse(Buffer.from(res.data).toString());
|
|
50
|
+
const errMsg = result.message || result.error || JSON.stringify(result);
|
|
51
|
+
logger.error(`${t("ERR_NETWORK")}: ${errMsg}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 处理常规 JSON 返回 (localurl)
|
|
57
|
+
const result = res.data;
|
|
58
|
+
const snapshotUrl = result.url || (result.data && result.data.url);
|
|
59
|
+
|
|
60
|
+
if (snapshotUrl) {
|
|
61
|
+
if (options.json) {
|
|
62
|
+
logger.json(result);
|
|
63
|
+
} else {
|
|
64
|
+
logger.success(t("SNAPSHOT_SUCCESS", { ticker, tf }));
|
|
65
|
+
console.log(chalk.cyan("URL: ") + snapshotUrl);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (result.status === false) {
|
|
71
|
+
const errMsg = result.message || result.error || JSON.stringify(result);
|
|
72
|
+
logger.error(`${t("ERR_NETWORK")}: ${errMsg}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
logger.error(`${t("ERR_NETWORK")}: ${JSON.stringify(result)}`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger.error(`Snapshot Error: ${error.message}`, t("SUGGESTION_KEY"));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export default snapshotCmd;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { program } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
import configCmd from "./commands/config.js";
|
|
6
|
+
import snapshotCmd from "./commands/snapshot.js";
|
|
7
|
+
import newsCmd from "./commands/news.js";
|
|
8
|
+
import signalsCmd from "./commands/signals.js";
|
|
9
|
+
import dashboardCmd from "./commands/dashboard.js";
|
|
10
|
+
import logoutCmd from "./commands/logout.js";
|
|
11
|
+
import config from "./utils/config-manager.js";
|
|
12
|
+
import { startOnboarding } from "./utils/onboard.js";
|
|
13
|
+
import { t } from "./utils/i18n.js";
|
|
14
|
+
|
|
15
|
+
const pkgPath = resolve(process.cwd(), "package.json");
|
|
16
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
17
|
+
|
|
18
|
+
// 极简版: 仅保留用户指定的蓝色块状 ASCII 字
|
|
19
|
+
const title = chalk.blueBright.bold;
|
|
20
|
+
const finalBanner = `
|
|
21
|
+
${chalk.red.bold("▄▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄▄▄ ▄▄ ")}
|
|
22
|
+
${chalk.red.bold("████▄ ▄████ ██ ██ ██ ▄██▀▀██▄ ██ ")}
|
|
23
|
+
${chalk.red.bold("███▀████▀███ ▄█▀█▄ ▀██▀▀ ████▄ ▄███▄ ▄████ ███ ███ ██ ▄████ ▄███▄ ")}
|
|
24
|
+
${chalk.red.bold("███ ▀▀ ███ ██▄█▀ ██ ██ ██ ██ ██ ██ ██ ███▀▀███ ██ ██ ██ ██ ██ ")}
|
|
25
|
+
${chalk.white.bold("███ ███ ▀█▄▄▄ ██ ██ ██ ▀███▀ ▀████ ███ ███ ██ ▀████ ▀███▀ ")}
|
|
26
|
+
${chalk.white.bold(" ██ ")}
|
|
27
|
+
${chalk.white.bold(" ▀▀▀ ")}
|
|
28
|
+
${chalk.dim("Cli | v" + pkg.version)}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.name("methodalgo")
|
|
33
|
+
.description(t("HELP_DESC"))
|
|
34
|
+
.version(pkg.version)
|
|
35
|
+
.showHelpAfterError();
|
|
36
|
+
|
|
37
|
+
program.addCommand(configCmd.description(t("CONFIG_DESC")));
|
|
38
|
+
program.addCommand(snapshotCmd.description(t("SNAPSHOT_DESC")));
|
|
39
|
+
program.addCommand(newsCmd.description(t("NEWS_DESC")));
|
|
40
|
+
program.addCommand(signalsCmd.description(t("SIGNALS_DESC")));
|
|
41
|
+
program.addCommand(dashboardCmd.description(t("DASHBOARD_DESC")));
|
|
42
|
+
program.addCommand(logoutCmd);
|
|
43
|
+
|
|
44
|
+
// 确保所有层级的指令(包括嵌套子指令)在参数缺失/报错时也显示帮助信息
|
|
45
|
+
function applyShowHelp(cmd) {
|
|
46
|
+
cmd.showHelpAfterError();
|
|
47
|
+
cmd.commands.forEach(applyShowHelp);
|
|
48
|
+
}
|
|
49
|
+
applyShowHelp(program);
|
|
50
|
+
|
|
51
|
+
program.on("command:*", () => {
|
|
52
|
+
console.error(`无效的命令: ${program.args.join(" ")}\n使用 "methodalgo --help" 查看可用命令。`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
async function main() {
|
|
57
|
+
// 检查是否需要引导 (无 API Key 则引导)
|
|
58
|
+
if (!config.get("apiKey") && !process.argv.includes("config") && !process.argv.includes("--help") && !process.argv.includes("-h")) {
|
|
59
|
+
await startOnboarding(finalBanner);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (process.argv.length <= 2) {
|
|
63
|
+
process.stdout.write(finalBanner + "\n");
|
|
64
|
+
program.help();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await program.parseAsync(process.argv);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
main();
|
package/src/utils/api.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import config from "./config-manager.js";
|
|
3
|
+
import { t } from "./i18n.js";
|
|
4
|
+
|
|
5
|
+
const SALT = "methodalgoMcpSALT";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 带有 HMAC 签名的安全请求函数 (迁移自原 core.js)
|
|
9
|
+
* 使用原生 fetch 以获得更好的跨平台兼容性 (Node 18+, Bun)
|
|
10
|
+
*/
|
|
11
|
+
export async function signedRequest(endpoint, params = {}, extraOptions = {}) {
|
|
12
|
+
const apiKey = process.env.METHODALGO_API_KEY || config.get("apiKey");
|
|
13
|
+
const apiBase = config.get("apiBase");
|
|
14
|
+
|
|
15
|
+
if (!apiKey) {
|
|
16
|
+
throw new Error(t("ERR_MISSING_KEY"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const timestamp = Date.now().toString();
|
|
20
|
+
|
|
21
|
+
// 过滤并排序参数
|
|
22
|
+
const cleanParams = Object.fromEntries(
|
|
23
|
+
Object.entries(params).filter(([_, v]) => v !== undefined && v !== null)
|
|
24
|
+
);
|
|
25
|
+
const urlObj = new URL(`${apiBase}${endpoint}`);
|
|
26
|
+
Object.entries(cleanParams).forEach(([k, v]) => urlObj.searchParams.append(k, v));
|
|
27
|
+
|
|
28
|
+
const sortedKeys = Object.keys(cleanParams).sort();
|
|
29
|
+
const sortedParams = sortedKeys.map(key => `${key}=${encodeURIComponent(cleanParams[key])}`).join("&");
|
|
30
|
+
|
|
31
|
+
const signature = crypto.createHmac("sha256", apiKey + SALT)
|
|
32
|
+
.update(sortedParams)
|
|
33
|
+
.digest("hex");
|
|
34
|
+
|
|
35
|
+
const response = await fetch(urlObj.toString(), {
|
|
36
|
+
method: "GET",
|
|
37
|
+
headers: {
|
|
38
|
+
"x-mcp-signature": signature,
|
|
39
|
+
"x-mcp-timestamp": timestamp,
|
|
40
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
41
|
+
"Accept": "application/json",
|
|
42
|
+
...extraOptions.headers
|
|
43
|
+
},
|
|
44
|
+
signal: extraOptions.signal,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
let serverMsg = "";
|
|
49
|
+
try {
|
|
50
|
+
const errorData = await response.json();
|
|
51
|
+
serverMsg = errorData.msg || errorData.message || "";
|
|
52
|
+
} catch (_) {}
|
|
53
|
+
const error = new Error(serverMsg || `Request failed with status ${response.status}`);
|
|
54
|
+
error.status = response.status;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (extraOptions.responseType === "arraybuffer") {
|
|
59
|
+
const data = await response.arrayBuffer();
|
|
60
|
+
return { data, headers: Object.fromEntries(response.headers.entries()) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
return { data, headers: Object.fromEntries(response.headers.entries()) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 验证 API Key 是否有效 (拉取 1 条新闻测试)
|
|
69
|
+
*/
|
|
70
|
+
export async function validateApiKey(apiKey) {
|
|
71
|
+
if (!apiKey) return false;
|
|
72
|
+
const apiBase = config.get("apiBase");
|
|
73
|
+
const endpoint = "/mcp/news";
|
|
74
|
+
const timestamp = Date.now().toString();
|
|
75
|
+
|
|
76
|
+
// 构造签名
|
|
77
|
+
const sortedParams = "limit=1&type=news";
|
|
78
|
+
const signature = crypto.createHmac("sha256", apiKey + SALT)
|
|
79
|
+
.update(sortedParams)
|
|
80
|
+
.digest("hex");
|
|
81
|
+
|
|
82
|
+
const urlObj = new URL(`${apiBase}${endpoint}`);
|
|
83
|
+
urlObj.searchParams.append("type", "news");
|
|
84
|
+
urlObj.searchParams.append("limit", "1");
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
89
|
+
|
|
90
|
+
const response = await fetch(urlObj.toString(), {
|
|
91
|
+
method: "GET",
|
|
92
|
+
headers: {
|
|
93
|
+
"x-mcp-signature": signature,
|
|
94
|
+
"x-mcp-timestamp": timestamp,
|
|
95
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
96
|
+
"Accept": "application/json"
|
|
97
|
+
},
|
|
98
|
+
signal: controller.signal
|
|
99
|
+
});
|
|
100
|
+
clearTimeout(timeoutId);
|
|
101
|
+
|
|
102
|
+
if (!response.ok) return false;
|
|
103
|
+
const data = await response.json();
|
|
104
|
+
return data.status === true;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Conf from "conf";
|
|
2
|
+
|
|
3
|
+
const schema = {
|
|
4
|
+
apiKey: {
|
|
5
|
+
type: "string",
|
|
6
|
+
default: ""
|
|
7
|
+
},
|
|
8
|
+
apiBase: {
|
|
9
|
+
type: "string",
|
|
10
|
+
default: "https://mm.methodalgo.com"
|
|
11
|
+
},
|
|
12
|
+
lang: {
|
|
13
|
+
type: "string",
|
|
14
|
+
default: "en"
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const config = new Conf({
|
|
19
|
+
projectName: "methodalgo",
|
|
20
|
+
schema
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export default config;
|