mdldm 1.0.0 → 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/chat.js +48 -10
- package/mdldm.js +24 -20
- package/package.json +1 -1
package/chat.js
CHANGED
|
@@ -6,8 +6,9 @@ const path = require('path');
|
|
|
6
6
|
|
|
7
7
|
// ── 配置 ──────────────────────────────────────────────
|
|
8
8
|
const API_BASE = 'www.mdldm.club';
|
|
9
|
-
const VERIFY_PATH
|
|
10
|
-
const CHAT_PATH
|
|
9
|
+
const VERIFY_PATH = '/api/cli/verify';
|
|
10
|
+
const CHAT_PATH = '/api/cli/chat';
|
|
11
|
+
const COURSES_PATH = '/api/cli/courses';
|
|
11
12
|
const CONFIG_FILE = path.join(process.env.HOME || '~', '.mdldm.json');
|
|
12
13
|
|
|
13
14
|
// ── ANSI ──────────────────────────────────────────────
|
|
@@ -68,11 +69,43 @@ function post(urlPath, body) {
|
|
|
68
69
|
});
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
// ── HTTP GET ─────────────────────────────────────────
|
|
73
|
+
function get(urlPath) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const isLocal = API_BASE.startsWith('localhost') || API_BASE.startsWith('127.');
|
|
76
|
+
const mod = isLocal ? http : https;
|
|
77
|
+
const port = isLocal ? 3000 : 443;
|
|
78
|
+
|
|
79
|
+
const req = mod.request({
|
|
80
|
+
hostname: isLocal ? 'localhost' : API_BASE,
|
|
81
|
+
port,
|
|
82
|
+
path: urlPath,
|
|
83
|
+
method: 'GET',
|
|
84
|
+
}, (res) => {
|
|
85
|
+
let data = '';
|
|
86
|
+
res.on('data', d => data += d);
|
|
87
|
+
res.on('end', () => {
|
|
88
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
|
|
89
|
+
catch { reject(new Error(`响应解析失败: ${data}`)); }
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
req.on('error', reject);
|
|
93
|
+
req.end();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── 获取课程列表 ─────────────────────────────────────
|
|
98
|
+
async function getCourses() {
|
|
99
|
+
const res = await get(COURSES_PATH);
|
|
100
|
+
if (res.status !== 200) throw new Error(res.body?.error || '获取课程失败');
|
|
101
|
+
return res.body;
|
|
102
|
+
}
|
|
103
|
+
|
|
71
104
|
// ── 流式 POST(SSE)─────────────────────────────────
|
|
72
105
|
// onToken(str) 每个 token 回调
|
|
73
106
|
// onDone({cost, pointsLeft}) 完成回调
|
|
74
107
|
// returns Promise<void>
|
|
75
|
-
function streamPost(urlPath, body, onToken, onDone) {
|
|
108
|
+
function streamPost(urlPath, body, onToken, onDone, onStatus) {
|
|
76
109
|
return new Promise((resolve, reject) => {
|
|
77
110
|
const payload = JSON.stringify(body);
|
|
78
111
|
const isLocal = API_BASE.startsWith('localhost') || API_BASE.startsWith('127.');
|
|
@@ -112,9 +145,10 @@ function streamPost(urlPath, body, onToken, onDone) {
|
|
|
112
145
|
if (raw === '[DONE]') { resolve(); return; }
|
|
113
146
|
try {
|
|
114
147
|
const evt = JSON.parse(raw);
|
|
115
|
-
if (evt.type === 'token')
|
|
116
|
-
if (evt.type === 'done')
|
|
117
|
-
if (evt.type === '
|
|
148
|
+
if (evt.type === 'token') onToken(evt.content);
|
|
149
|
+
if (evt.type === 'done') onDone(evt);
|
|
150
|
+
if (evt.type === 'status' && onStatus) onStatus(evt.message);
|
|
151
|
+
if (evt.type === 'error') reject(new Error(evt.message));
|
|
118
152
|
} catch { /* 跳过不完整 chunk */ }
|
|
119
153
|
}
|
|
120
154
|
});
|
|
@@ -134,10 +168,13 @@ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
|
134
168
|
function createSpinner(msg = '麦当mdldm寻思中...') {
|
|
135
169
|
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
136
170
|
let i = 0;
|
|
171
|
+
let currentMsg = msg;
|
|
137
172
|
const id = setInterval(() => {
|
|
138
|
-
process.stdout.write(`\r ${CY}${frames[i++ % frames.length]}${R} ${DM}${
|
|
173
|
+
process.stdout.write(`\r ${CY}${frames[i++ % frames.length]}${R} ${DM}${currentMsg}${R}`);
|
|
139
174
|
}, 80);
|
|
140
|
-
|
|
175
|
+
const stop = () => { clearInterval(id); process.stdout.write('\r\x1b[K'); };
|
|
176
|
+
stop.update = (newMsg) => { currentMsg = newMsg; };
|
|
177
|
+
return stop;
|
|
141
178
|
}
|
|
142
179
|
|
|
143
180
|
// ── 流式 Markdown 渲染器(逐行渲染,兼顾流式体验)────
|
|
@@ -445,7 +482,8 @@ async function chat() {
|
|
|
445
482
|
renderer.push(tok);
|
|
446
483
|
fullReply += tok;
|
|
447
484
|
},
|
|
448
|
-
(info) => { costInfo = info; }
|
|
485
|
+
(info) => { costInfo = info; },
|
|
486
|
+
(msg) => { stopSpin.update(msg); }
|
|
449
487
|
);
|
|
450
488
|
|
|
451
489
|
if (!renderer) {
|
|
@@ -488,4 +526,4 @@ async function chat() {
|
|
|
488
526
|
ask();
|
|
489
527
|
}
|
|
490
528
|
|
|
491
|
-
module.exports = { login, logout, me, chat };
|
|
529
|
+
module.exports = { login, logout, me, chat, getCourses, createSpinner };
|
package/mdldm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const data = require('./data');
|
|
4
|
-
const { chat, login, logout, me } = require('./chat');
|
|
4
|
+
const { chat, login, logout, me, getCourses, createSpinner } = require('./chat');
|
|
5
5
|
|
|
6
6
|
// ── ANSI ──────────────────────────────────────────────
|
|
7
7
|
const R = '\x1b[0m';
|
|
@@ -51,8 +51,19 @@ function viewProfile() {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// ── courses 视图 ──────────────────────────────────────
|
|
54
|
-
function viewCourses() {
|
|
55
|
-
const
|
|
54
|
+
async function viewCourses() {
|
|
55
|
+
const stopSpin = createSpinner('拉取最新课程...');
|
|
56
|
+
let courses;
|
|
57
|
+
try {
|
|
58
|
+
courses = await getCourses();
|
|
59
|
+
stopSpin();
|
|
60
|
+
} catch (e) {
|
|
61
|
+
stopSpin();
|
|
62
|
+
console.log(RD + `\n ✗ 获取课程失败:${e.message}\n` + R);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const levelMap = { beginner: '入门', intermediate: '进阶', advanced: '高级' };
|
|
56
67
|
|
|
57
68
|
console.log();
|
|
58
69
|
console.log(B + CY + ' 麦当mdldm — 课程体系' + R);
|
|
@@ -60,26 +71,19 @@ function viewCourses() {
|
|
|
60
71
|
|
|
61
72
|
for (const course of courses) {
|
|
62
73
|
console.log();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
console.log(DM + ` ${course.
|
|
68
|
-
|
|
69
|
-
if (course.highlights.length) {
|
|
70
|
-
console.log();
|
|
71
|
-
for (const h of course.highlights) {
|
|
72
|
-
console.log(GR + ` ✓ ` + R + h);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
74
|
+
const statusTag = course.status === 'published' ? GR + '● 更新中' + R : DM + '○ 已完结' + R;
|
|
75
|
+
console.log(B + MG + ` 【${course.name}】` + R + ' ' + statusTag);
|
|
76
|
+
const level = levelMap[course.level] || course.level;
|
|
77
|
+
console.log(DM + ` 难度:${level}` + (course.isVip ? ' ' + GR + '[VIP]' + R : '') + R);
|
|
78
|
+
if (course.description) console.log(DM + ` ${course.description}` + R);
|
|
75
79
|
|
|
76
|
-
if (course.
|
|
80
|
+
if (course.episodes?.length) {
|
|
77
81
|
console.log();
|
|
78
82
|
console.log(B + ' 课程目录:' + R);
|
|
79
|
-
for (const
|
|
80
|
-
const tag =
|
|
81
|
-
const title = `EP${String(
|
|
82
|
-
const dur =
|
|
83
|
+
for (const ep of course.episodes) {
|
|
84
|
+
const tag = ep.paid ? RD + '[VIP]' + R : GR + '[免费]' + R;
|
|
85
|
+
const title = `EP${String(ep.ep).padStart(2, '0')} · ${ep.title}`;
|
|
86
|
+
const dur = ep.duration ? DM + ` ${ep.duration}` + R : '';
|
|
83
87
|
console.log(` ${tag} ${title}${dur}`);
|
|
84
88
|
}
|
|
85
89
|
}
|