methodalgo-cli 1.0.2 → 1.0.3
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 +2 -2
- package/package.json +1 -1
- package/src/commands/dashboard.js +23 -12
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ npm link # Link local command
|
|
|
64
64
|
#### 🖥️ Market Dashboard (`dashboard`)
|
|
65
65
|
Launch a real-time TUI (Terminal User Interface) dashboard for a global view of market insights, news, and signals.
|
|
66
66
|
|
|
67
|
-

|
|
68
68
|
|
|
69
69
|
* **Usage**: `methodalgo dashboard` (Alias: `top`)
|
|
70
70
|
* **Controls**: Use `TAB` to switch panels, `UP/DOWN` to scroll, and `ENTER` to view details.
|
|
@@ -192,7 +192,7 @@ npm link # 链接本地指令
|
|
|
192
192
|
#### 🖥️ 市场看板 (`dashboard`)
|
|
193
193
|
启动实时 TUI(终端用户界面)仪表盘,全局俯瞰市场洞察、新闻与信号。
|
|
194
194
|
|
|
195
|
-

|
|
196
196
|
|
|
197
197
|
* **用法**: `methodalgo dashboard` (别名: `top`)
|
|
198
198
|
* **操作**: 使用 `TAB` 切换面板,`UP/DOWN` 滚动列表,`ENTER` 弹出详情。
|
package/package.json
CHANGED
|
@@ -239,9 +239,18 @@ const Dashboard = () => {
|
|
|
239
239
|
const [statusInfo, setStatusInfo] = useState({ time: "", mem: "0", error: null });
|
|
240
240
|
const dataTimerRef = useRef(null);
|
|
241
241
|
const lastFetchRef = useRef(null); // 记录上次拉取时间,用于增量请求
|
|
242
|
+
const isFetchingRef = useRef(false); // 防止并发重入
|
|
243
|
+
const refreshRef = useRef(null); // 始终持有最新 refreshData 引用
|
|
242
244
|
const lang = getLang();
|
|
243
245
|
|
|
246
|
+
const scheduleRefresh = (delay = 60000) => {
|
|
247
|
+
if (dataTimerRef.current) clearTimeout(dataTimerRef.current);
|
|
248
|
+
dataTimerRef.current = setTimeout(() => refreshRef.current?.(), delay);
|
|
249
|
+
};
|
|
250
|
+
|
|
244
251
|
const refreshData = async () => {
|
|
252
|
+
if (isFetchingRef.current) return; // 防止并发
|
|
253
|
+
isFetchingRef.current = true;
|
|
245
254
|
const memUsage = (process.memoryUsage().rss / 1024 / 1024).toFixed(1);
|
|
246
255
|
try {
|
|
247
256
|
const newsTypes = ["article", "breaking", "onchain", "report"];
|
|
@@ -262,7 +271,7 @@ const Dashboard = () => {
|
|
|
262
271
|
(r.status === "fulfilled" && r.value.data.status === false &&
|
|
263
272
|
(r.value.data.msg?.includes("auth") || r.value.data.msg?.includes("key")))
|
|
264
273
|
);
|
|
265
|
-
if (authFailed) { setAuthError(true); setLoading(false); if (dataTimerRef.current)
|
|
274
|
+
if (authFailed) { setAuthError(true); setLoading(false); if (dataTimerRef.current) clearTimeout(dataTimerRef.current); return; }
|
|
266
275
|
|
|
267
276
|
lastFetchRef.current = new Date().toISOString();
|
|
268
277
|
|
|
@@ -294,7 +303,8 @@ const Dashboard = () => {
|
|
|
294
303
|
return (res?.status === "fulfilled" && res.value.data.status) ? res.value.data.data : [];
|
|
295
304
|
};
|
|
296
305
|
|
|
297
|
-
const formatSig = (
|
|
306
|
+
const formatSig = (rawItem, overrideDir, type) => {
|
|
307
|
+
const item = { ...rawItem }; // 浅拷贝,避免直接修改原始数据
|
|
298
308
|
const sig = item.signals?.[0];
|
|
299
309
|
let breakPrice = sig?.details?.BreakPrice || sig?.breakPrice || sig?.break_price || item.breakPrice || item.break_price;
|
|
300
310
|
if (!breakPrice && sig?.fields) {
|
|
@@ -363,7 +373,8 @@ const Dashboard = () => {
|
|
|
363
373
|
};
|
|
364
374
|
|
|
365
375
|
const mergeAndSort = (items, existing, overrideDir, type, preferNew = false) => {
|
|
366
|
-
const
|
|
376
|
+
const existingIds = new Set(existing.map(e => e.id)); // 提前构建 Set,避免 O(n²)
|
|
377
|
+
const combined = preferNew ? [...items, ...existing] : [...items.filter(i => !existingIds.has(i.id)), ...existing];
|
|
367
378
|
const unique = Array.from(new Map(combined.map(item => [item.id, item])).values());
|
|
368
379
|
return unique.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)).slice(0, 50).map(i => formatSig(i, overrideDir, type));
|
|
369
380
|
};
|
|
@@ -445,8 +456,7 @@ const Dashboard = () => {
|
|
|
445
456
|
return next;
|
|
446
457
|
});
|
|
447
458
|
setStatusInfo({ time: new Date().toLocaleTimeString(), mem: memUsage, error: null });
|
|
448
|
-
|
|
449
|
-
dataTimerRef.current = setTimeout(refreshData, 60000);
|
|
459
|
+
scheduleRefresh(60000);
|
|
450
460
|
} catch (error) {
|
|
451
461
|
if (error.status === 429) {
|
|
452
462
|
const secMatch = error.message.match(/(\d+)\s+seconds/);
|
|
@@ -455,21 +465,22 @@ const Dashboard = () => {
|
|
|
455
465
|
if (secMatch) delay = parseInt(secMatch[1]) * 1000 + 2000;
|
|
456
466
|
else if (minMatch) delay = parseInt(minMatch[1]) * 60000 + 2000;
|
|
457
467
|
setStatusInfo(prev => ({ ...prev, error: `Rate Limited (Retry in ${Math.round(delay / 1000)}s)` }));
|
|
458
|
-
|
|
459
|
-
dataTimerRef.current = setTimeout(refreshData, delay);
|
|
468
|
+
scheduleRefresh(delay);
|
|
460
469
|
} else {
|
|
461
470
|
setStatusInfo(prev => ({ ...prev, error: `Err: ${error.message.substring(0, 20)}` }));
|
|
462
|
-
|
|
463
|
-
dataTimerRef.current = setTimeout(refreshData, 60000);
|
|
471
|
+
scheduleRefresh(60000);
|
|
464
472
|
}
|
|
473
|
+
} finally {
|
|
474
|
+
isFetchingRef.current = false;
|
|
475
|
+
setLoading(false); // 确保任何路径退出都会重置 loading
|
|
465
476
|
}
|
|
466
|
-
setLoading(false);
|
|
467
477
|
};
|
|
478
|
+
refreshRef.current = refreshData; // 每次渲染后同步最新引用
|
|
468
479
|
|
|
469
480
|
useEffect(() => {
|
|
470
|
-
|
|
481
|
+
refreshRef.current(); // 挂载时首次拉取(ref 已在渲染体 L476 同步)
|
|
471
482
|
return () => { if (dataTimerRef.current) clearTimeout(dataTimerRef.current); };
|
|
472
|
-
}, []);
|
|
483
|
+
}, []); // 仅挂载执行一次,后续刷新由 scheduleRefresh + refreshRef 统一管理
|
|
473
484
|
|
|
474
485
|
useInput((input, key) => {
|
|
475
486
|
if (dialog) return; // 弹窗时由 DetailDialog 处理
|