mdldm 1.0.0
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 +491 -0
- package/data.js +128 -0
- package/mdldm.js +132 -0
- package/package.json +35 -0
package/chat.js
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
// ── 配置 ──────────────────────────────────────────────
|
|
8
|
+
const API_BASE = 'www.mdldm.club';
|
|
9
|
+
const VERIFY_PATH = '/api/cli/verify';
|
|
10
|
+
const CHAT_PATH = '/api/cli/chat';
|
|
11
|
+
const CONFIG_FILE = path.join(process.env.HOME || '~', '.mdldm.json');
|
|
12
|
+
|
|
13
|
+
// ── ANSI ──────────────────────────────────────────────
|
|
14
|
+
const R = '\x1b[0m';
|
|
15
|
+
const B = '\x1b[1m';
|
|
16
|
+
const IT = '\x1b[3m';
|
|
17
|
+
const DM = '\x1b[2m';
|
|
18
|
+
const CY = '\x1b[36m';
|
|
19
|
+
const YL = '\x1b[33m';
|
|
20
|
+
const GR = '\x1b[32m';
|
|
21
|
+
const RD = '\x1b[31m';
|
|
22
|
+
|
|
23
|
+
// ── 本地 token 存取 ───────────────────────────────────
|
|
24
|
+
function loadConfig() {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function saveConfig(data) {
|
|
33
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function clearConfig() {
|
|
37
|
+
try { fs.unlinkSync(CONFIG_FILE); } catch {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── HTTP 请求(支持 http / https)────────────────────
|
|
41
|
+
function post(urlPath, body) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const payload = JSON.stringify(body);
|
|
44
|
+
const isLocal = API_BASE.startsWith('localhost') || API_BASE.startsWith('127.');
|
|
45
|
+
const mod = isLocal ? http : https;
|
|
46
|
+
const port = isLocal ? 3000 : 443;
|
|
47
|
+
|
|
48
|
+
const req = mod.request({
|
|
49
|
+
hostname: isLocal ? 'localhost' : API_BASE,
|
|
50
|
+
port,
|
|
51
|
+
path: urlPath,
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
56
|
+
},
|
|
57
|
+
}, (res) => {
|
|
58
|
+
let data = '';
|
|
59
|
+
res.on('data', d => data += d);
|
|
60
|
+
res.on('end', () => {
|
|
61
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
|
|
62
|
+
catch { reject(new Error(`响应解析失败: ${data}`)); }
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
req.on('error', reject);
|
|
66
|
+
req.write(payload);
|
|
67
|
+
req.end();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── 流式 POST(SSE)─────────────────────────────────
|
|
72
|
+
// onToken(str) 每个 token 回调
|
|
73
|
+
// onDone({cost, pointsLeft}) 完成回调
|
|
74
|
+
// returns Promise<void>
|
|
75
|
+
function streamPost(urlPath, body, onToken, onDone) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const payload = JSON.stringify(body);
|
|
78
|
+
const isLocal = API_BASE.startsWith('localhost') || API_BASE.startsWith('127.');
|
|
79
|
+
const mod = isLocal ? http : https;
|
|
80
|
+
const port = isLocal ? 3000 : 443;
|
|
81
|
+
|
|
82
|
+
const req = mod.request({
|
|
83
|
+
hostname: isLocal ? 'localhost' : API_BASE,
|
|
84
|
+
port,
|
|
85
|
+
path: urlPath,
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
90
|
+
},
|
|
91
|
+
}, (res) => {
|
|
92
|
+
// 非 2xx 先读完再当普通错误处理
|
|
93
|
+
if (res.statusCode !== 200) {
|
|
94
|
+
let raw = '';
|
|
95
|
+
res.on('data', d => raw += d);
|
|
96
|
+
res.on('end', () => {
|
|
97
|
+
try { reject(Object.assign(new Error('api-error'), { apiError: JSON.parse(raw) })); }
|
|
98
|
+
catch { reject(new Error(raw)); }
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let buf = '';
|
|
104
|
+
res.on('data', (chunk) => {
|
|
105
|
+
buf += chunk.toString();
|
|
106
|
+
const lines = buf.split('\n');
|
|
107
|
+
buf = lines.pop(); // 保留未完整的行
|
|
108
|
+
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
if (!line.startsWith('data: ')) continue;
|
|
111
|
+
const raw = line.slice(6).trim();
|
|
112
|
+
if (raw === '[DONE]') { resolve(); return; }
|
|
113
|
+
try {
|
|
114
|
+
const evt = JSON.parse(raw);
|
|
115
|
+
if (evt.type === 'token') onToken(evt.content);
|
|
116
|
+
if (evt.type === 'done') onDone(evt);
|
|
117
|
+
if (evt.type === 'error') reject(new Error(evt.message));
|
|
118
|
+
} catch { /* 跳过不完整 chunk */ }
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
res.on('end', resolve);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
req.on('error', reject);
|
|
125
|
+
req.write(payload);
|
|
126
|
+
req.end();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── 工具 ──────────────────────────────────────────────
|
|
131
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
132
|
+
|
|
133
|
+
// ── Spinner(支持自定义文案)──────────────────────────
|
|
134
|
+
function createSpinner(msg = '麦当mdldm寻思中...') {
|
|
135
|
+
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
136
|
+
let i = 0;
|
|
137
|
+
const id = setInterval(() => {
|
|
138
|
+
process.stdout.write(`\r ${CY}${frames[i++ % frames.length]}${R} ${DM}${msg}${R}`);
|
|
139
|
+
}, 80);
|
|
140
|
+
return () => { clearInterval(id); process.stdout.write('\r\x1b[K'); };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── 流式 Markdown 渲染器(逐行渲染,兼顾流式体验)────
|
|
144
|
+
class StreamingMarkdown {
|
|
145
|
+
constructor() {
|
|
146
|
+
this.buf = ''; // 当前未完成的行缓冲
|
|
147
|
+
this.inCode = false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
push(tok) {
|
|
151
|
+
this.buf += tok;
|
|
152
|
+
let idx;
|
|
153
|
+
while ((idx = this.buf.indexOf('\n')) !== -1) {
|
|
154
|
+
const line = this.buf.slice(0, idx);
|
|
155
|
+
this.buf = this.buf.slice(idx + 1);
|
|
156
|
+
this._printLine(line);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
flush() {
|
|
161
|
+
if (this.buf) { this._printLine(this.buf); this.buf = ''; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_printLine(line) {
|
|
165
|
+
if (line.startsWith('```')) {
|
|
166
|
+
this.inCode = !this.inCode;
|
|
167
|
+
if (this.inCode) {
|
|
168
|
+
const lang = line.slice(3).trim();
|
|
169
|
+
const label = lang ? ` ${lang} ` : '';
|
|
170
|
+
console.log(DM + ` ┌${label}` + '─'.repeat(Math.max(2, 38 - label.length)) + R);
|
|
171
|
+
} else {
|
|
172
|
+
console.log(DM + ' └' + '─'.repeat(40) + R);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (this.inCode) { console.log(`${CY} │${R} ${line}`); return; }
|
|
177
|
+
if (line.startsWith('### ')) { console.log(`${B}${YL} ${line.slice(4)}${R}`); return; }
|
|
178
|
+
if (line.startsWith('## ')) { console.log(`\n${B}${CY} ${line.slice(3)}${R}`); return; }
|
|
179
|
+
if (line.startsWith('# ')) { console.log(`\n${B}${CY} ${line.slice(2)}${R}`); return; }
|
|
180
|
+
if (/^[-*]{3,}$/.test(line.trim())) { console.log(`${DM} ${'─'.repeat(44)}${R}`); return; }
|
|
181
|
+
const li = line.match(/^(\s*)[-*] (.+)/);
|
|
182
|
+
if (li) { console.log(`${' '.repeat(Math.floor(li[1].length/2)+1)}${GR}·${R} ${renderInline(li[2])}`); return; }
|
|
183
|
+
const ol = line.match(/^(\s*)(\d+)\. (.+)/);
|
|
184
|
+
if (ol) { console.log(`${' '.repeat(Math.floor(ol[1].length/2)+1)}${YL}${ol[2]}.${R} ${renderInline(ol[3])}`); return; }
|
|
185
|
+
if (line.trim() === '') { console.log(''); return; }
|
|
186
|
+
console.log(` ${renderInline(line)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Markdown 渲染器 ───────────────────────────────────
|
|
191
|
+
function renderInline(text) {
|
|
192
|
+
return text
|
|
193
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, `${B}${IT}$1${R}`)
|
|
194
|
+
.replace(/\*\*(.+?)\*\*/g, `${B}$1${R}`)
|
|
195
|
+
.replace(/\*(.+?)\*/g, `${IT}$1${R}`)
|
|
196
|
+
.replace(/`([^`]+)`/g, `${CY}$1${R}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function renderMarkdown(text) {
|
|
200
|
+
const lines = text.split('\n');
|
|
201
|
+
const out = [];
|
|
202
|
+
let inCode = false;
|
|
203
|
+
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
if (line.startsWith('```')) {
|
|
206
|
+
if (!inCode) {
|
|
207
|
+
inCode = true;
|
|
208
|
+
const lang = line.slice(3).trim();
|
|
209
|
+
const label = lang ? ` ${lang} ` : '';
|
|
210
|
+
out.push(DM + ` ┌${label}` + '─'.repeat(Math.max(2, 38 - label.length)) + R);
|
|
211
|
+
} else {
|
|
212
|
+
inCode = false;
|
|
213
|
+
out.push(DM + ' └' + '─'.repeat(40) + R);
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (inCode) { out.push(`${CY} │${R} ${line}`); continue; }
|
|
218
|
+
|
|
219
|
+
if (line.startsWith('### ')) out.push(`${B}${YL} ${line.slice(4)}${R}`);
|
|
220
|
+
else if (line.startsWith('## ')) out.push(`\n${B}${CY} ${line.slice(3)}${R}`);
|
|
221
|
+
else if (line.startsWith('# ')) out.push(`\n${B}${CY} ${line.slice(2)}${R}\n`);
|
|
222
|
+
else if (/^[-*]{3,}$/.test(line.trim())) out.push(`${DM} ${'─'.repeat(44)}${R}`);
|
|
223
|
+
else if (/^(\s*)[-*] (.+)/.test(line)) {
|
|
224
|
+
const [, ind, c] = line.match(/^(\s*)[-*] (.+)/);
|
|
225
|
+
out.push(`${' '.repeat(Math.floor(ind.length / 2) + 1)}${GR}·${R} ${renderInline(c)}`);
|
|
226
|
+
} else if (/^(\s*)(\d+)\. (.+)/.test(line)) {
|
|
227
|
+
const [, ind, num, c] = line.match(/^(\s*)(\d+)\. (.+)/);
|
|
228
|
+
out.push(`${' '.repeat(Math.floor(ind.length / 2) + 1)}${YL}${num}.${R} ${renderInline(c)}`);
|
|
229
|
+
} else if (line.trim() === '') {
|
|
230
|
+
out.push('');
|
|
231
|
+
} else {
|
|
232
|
+
out.push(` ${renderInline(line)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return out.join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── mdldm login ───────────────────────────────────────
|
|
239
|
+
async function login() {
|
|
240
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
241
|
+
|
|
242
|
+
console.log();
|
|
243
|
+
console.log(B + CY + ' mdldm 登录' + R);
|
|
244
|
+
console.log(DM + ' 在知识站个人中心复制你的 Token' + R);
|
|
245
|
+
console.log(DM + ' 👉 https://mdldm.club/profile' + R);
|
|
246
|
+
console.log();
|
|
247
|
+
|
|
248
|
+
rl.question(`${YL} Token > ${R}`, async (token) => {
|
|
249
|
+
rl.close();
|
|
250
|
+
token = token.trim();
|
|
251
|
+
if (!token) { console.log(RD + '\n Token 不能为空\n' + R); return; }
|
|
252
|
+
|
|
253
|
+
const stopSpin = createSpinner();
|
|
254
|
+
try {
|
|
255
|
+
const res = await post(VERIFY_PATH, { token });
|
|
256
|
+
stopSpin();
|
|
257
|
+
|
|
258
|
+
if (res.status !== 200) {
|
|
259
|
+
console.log(RD + `\n ✗ ${res.body.error}\n` + R);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const { name, points, isVIP } = res.body;
|
|
264
|
+
saveConfig({ token, name });
|
|
265
|
+
|
|
266
|
+
console.log();
|
|
267
|
+
console.log(GR + B + ` ✓ 登录成功,欢迎回来 ${name}!` + R);
|
|
268
|
+
console.log(` 麦子余额:${YL}${B}${points}${R} ${isVIP ? GR + '[VIP]' + R : DM + '[普通用户]' + R}`);
|
|
269
|
+
console.log(DM + '\n 现在可以运行 mdldm chat 开始对话\n' + R);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
stopSpin();
|
|
272
|
+
console.log(RD + `\n ✗ 连接失败:${e.message}\n` + R);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── mdldm logout ──────────────────────────────────────
|
|
278
|
+
function logout() {
|
|
279
|
+
const cfg = loadConfig();
|
|
280
|
+
clearConfig();
|
|
281
|
+
console.log();
|
|
282
|
+
if (cfg?.name) console.log(DM + ` 已退出登录(${cfg.name})` + R);
|
|
283
|
+
else console.log(DM + ' 已清除本地登录信息' + R);
|
|
284
|
+
console.log();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── mdldm me ──────────────────────────────────────────
|
|
288
|
+
async function me() {
|
|
289
|
+
const cfg = loadConfig();
|
|
290
|
+
if (!cfg?.token) {
|
|
291
|
+
console.log(RD + '\n 未登录,请先运行 mdldm login\n' + R);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const stopSpin = createSpinner();
|
|
296
|
+
try {
|
|
297
|
+
const res = await post(VERIFY_PATH, { token: cfg.token });
|
|
298
|
+
stopSpin();
|
|
299
|
+
|
|
300
|
+
if (res.status !== 200) {
|
|
301
|
+
console.log(RD + `\n ✗ ${res.body.error}\n` + R);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const { name, email, points, isVIP, role } = res.body;
|
|
306
|
+
console.log();
|
|
307
|
+
console.log(B + ` ${name}` + R + (isVIP ? ' ' + GR + '[VIP]' + R : '') + (role === 'admin' ? ' ' + YL + '[管理员]' + R : ''));
|
|
308
|
+
console.log(DM + ` ${email}` + R);
|
|
309
|
+
console.log(` 麦子余额:${YL}${B}${points}${R}`);
|
|
310
|
+
console.log(DM + ' 每日签到可领取麦子 → https://mdldm.club/profile' + R);
|
|
311
|
+
console.log();
|
|
312
|
+
} catch (e) {
|
|
313
|
+
stopSpin();
|
|
314
|
+
console.log(RD + `\n ✗ 连接失败:${e.message}\n` + R);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Hero 开场动画 ──────────────────────────────────────
|
|
319
|
+
async function showHero() {
|
|
320
|
+
const BOLD_CY = '\x1b[1;36m';
|
|
321
|
+
const DIM_CY = '\x1b[2;36m';
|
|
322
|
+
|
|
323
|
+
const art = [
|
|
324
|
+
['BOLD', '███╗ ███╗██████╗ ██╗ ██████╗ ███╗ ███╗'],
|
|
325
|
+
['BOLD', '████╗ ████║██╔══██╗██║ ██╔══██╗████╗ ████║'],
|
|
326
|
+
['BOLD', '██╔████╔██║██║ ██║██║ ██║ ██║██╔████╔██║'],
|
|
327
|
+
['DIM', '██║╚██╔╝██║██║ ██║██║ ██║ ██║██║╚██╔╝██║'],
|
|
328
|
+
['DIM', '██║ ╚═╝ ██║██████╔╝███████╗██████╔╝██║ ╚═╝ ██║'],
|
|
329
|
+
['DIM', '╚═╝ ╚═╝╚═════╝ ╚══════╝╚═════╝ ╚═╝ ╚═╝'],
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
// ── ASCII art 逐行出现
|
|
333
|
+
console.log();
|
|
334
|
+
for (const [style, line] of art) {
|
|
335
|
+
console.log(' ' + (style === 'BOLD' ? BOLD_CY : DIM_CY) + line + R);
|
|
336
|
+
await sleep(40);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── 主标签(最有个性的一句)
|
|
340
|
+
await sleep(150);
|
|
341
|
+
console.log();
|
|
342
|
+
await sleep(30);
|
|
343
|
+
console.log(' ' + B + '最会讲 AI 的技术型相声演员' + R + ' 🎤 ' + DM + '· 致力于让 0 基础小白也能玩转 AI Agent' + R);
|
|
344
|
+
|
|
345
|
+
// ── 亮点四格(逐条出现)
|
|
346
|
+
await sleep(100);
|
|
347
|
+
console.log();
|
|
348
|
+
const items = [
|
|
349
|
+
['🏆', '全网 5w+ 粉丝', '📖', '独家 0基础 AI Agent 实战课'],
|
|
350
|
+
['🧠', '独创四大 AI 理论', '🏢', '覆盖 500+ 企业的 AI 培训经验'],
|
|
351
|
+
];
|
|
352
|
+
for (const [i1, t1, i2, t2] of items) {
|
|
353
|
+
await sleep(55);
|
|
354
|
+
console.log(` ${YL}${i1}${R} ${t1} ${YL}${i2}${R} ${t2}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ── 链接
|
|
358
|
+
await sleep(100);
|
|
359
|
+
console.log();
|
|
360
|
+
await sleep(30);
|
|
361
|
+
console.log(' ' + GR + '🌐 mdldm.club' + R + ' ' + YL + '📱 全网搜索 @麦当mdldm' + R);
|
|
362
|
+
await sleep(50);
|
|
363
|
+
console.log();
|
|
364
|
+
|
|
365
|
+
// ── 分隔线逐字符画出
|
|
366
|
+
process.stdout.write(' ' + DM);
|
|
367
|
+
for (const c of '─'.repeat(48)) { process.stdout.write(c); await sleep(6); }
|
|
368
|
+
process.stdout.write(R + '\n\n');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── mdldm chat ────────────────────────────────────────
|
|
372
|
+
async function chat() {
|
|
373
|
+
const cfg = loadConfig();
|
|
374
|
+
|
|
375
|
+
// ── Hero 开场
|
|
376
|
+
await showHero();
|
|
377
|
+
|
|
378
|
+
if (!cfg?.token) {
|
|
379
|
+
console.log(RD + ' 未登录,请先运行 mdldm login\n' + R);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── 入场:验证 token,顺便拿最新的用户信息
|
|
384
|
+
const stopConn = createSpinner('连接中...');
|
|
385
|
+
let userInfo;
|
|
386
|
+
try {
|
|
387
|
+
const res = await post(VERIFY_PATH, { token: cfg.token });
|
|
388
|
+
stopConn();
|
|
389
|
+
if (res.status !== 200) {
|
|
390
|
+
console.log(RD + `\n ✗ ${res.body.error}\n` + R);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
userInfo = res.body;
|
|
394
|
+
saveConfig({ ...cfg, name: userInfo.name });
|
|
395
|
+
} catch (e) {
|
|
396
|
+
stopConn();
|
|
397
|
+
console.log(RD + `\n ✗ 连接失败:${e.message}\n` + R);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const { name, points, isVIP } = userInfo;
|
|
402
|
+
const displayName = name.length > 10 ? name.slice(0, 10) + '…' : name;
|
|
403
|
+
const vipTag = isVIP ? GR + ' [VIP]' + R : '';
|
|
404
|
+
|
|
405
|
+
// 欢迎行 + 分隔线
|
|
406
|
+
console.log(B + ` 欢迎回来,${displayName}!` + R + vipTag + DM + ` · ${points} 麦子` + R);
|
|
407
|
+
await sleep(60);
|
|
408
|
+
console.log(DM + ' 输入问题开始对话,exit 退出' + R);
|
|
409
|
+
await sleep(60);
|
|
410
|
+
process.stdout.write(' ' + CY);
|
|
411
|
+
for (const c of '─'.repeat(46)) { process.stdout.write(c); await sleep(8); }
|
|
412
|
+
process.stdout.write(R + '\n\n');
|
|
413
|
+
|
|
414
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
415
|
+
const history = [];
|
|
416
|
+
|
|
417
|
+
const ask = () => {
|
|
418
|
+
rl.question(`${YL}${displayName} > ${R}`, async (input) => {
|
|
419
|
+
const text = input.trim();
|
|
420
|
+
if (!text) { ask(); return; }
|
|
421
|
+
if (text === 'exit' || text === 'quit') {
|
|
422
|
+
console.log(DM + '\n 再见!有问题随时回来 👋\n' + R);
|
|
423
|
+
rl.close();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
history.push({ role: 'user', content: text });
|
|
428
|
+
|
|
429
|
+
const stopSpin = createSpinner();
|
|
430
|
+
let renderer = null;
|
|
431
|
+
let costInfo = null;
|
|
432
|
+
let fullReply = '';
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
await streamPost(
|
|
436
|
+
CHAT_PATH,
|
|
437
|
+
{ token: cfg.token, messages: history },
|
|
438
|
+
(tok) => {
|
|
439
|
+
// 第一个 token 到了:停 spinner,打印前缀,创建渲染器
|
|
440
|
+
if (!renderer) {
|
|
441
|
+
stopSpin();
|
|
442
|
+
console.log(`${GR}${B}mdldm >${R}`);
|
|
443
|
+
renderer = new StreamingMarkdown();
|
|
444
|
+
}
|
|
445
|
+
renderer.push(tok);
|
|
446
|
+
fullReply += tok;
|
|
447
|
+
},
|
|
448
|
+
(info) => { costInfo = info; }
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
if (!renderer) {
|
|
452
|
+
// 一个 token 都没收到
|
|
453
|
+
stopSpin();
|
|
454
|
+
console.log(RD + ' ✗ 未收到回复,请确认知识站已部署最新版本\n' + R);
|
|
455
|
+
} else {
|
|
456
|
+
renderer.flush(); // 渲染末尾不带换行的片段
|
|
457
|
+
history.push({ role: 'assistant', content: fullReply });
|
|
458
|
+
if (costInfo) {
|
|
459
|
+
const { cost, tokens, pointsLeft } = costInfo;
|
|
460
|
+
const tip = cost === 0
|
|
461
|
+
? DM + ` ─ 管理员免费(${tokens} tokens)` + R
|
|
462
|
+
: DM + ` ─ ${tokens} tokens · 消耗 ${cost} 麦子 · 剩余 ${pointsLeft}` + R;
|
|
463
|
+
console.log(tip);
|
|
464
|
+
}
|
|
465
|
+
console.log();
|
|
466
|
+
}
|
|
467
|
+
} catch (e) {
|
|
468
|
+
if (!renderer) stopSpin();
|
|
469
|
+
if (e.apiError) {
|
|
470
|
+
const err = e.apiError;
|
|
471
|
+
if (err.points !== undefined) {
|
|
472
|
+
console.log(RD + `\n ✗ ${err.error}` + R);
|
|
473
|
+
console.log(DM + ' 去签到补充麦子 → https://mdldm.club/profile\n' + R);
|
|
474
|
+
rl.close();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
console.log(RD + `\n ✗ ${err.error}\n` + R);
|
|
478
|
+
} else {
|
|
479
|
+
console.log(RD + `\n ✗ 连接失败:${e.message}\n` + R);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
ask();
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
rl.on('close', () => process.exit(0));
|
|
488
|
+
ask();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
module.exports = { login, logout, me, chat };
|
package/data.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// 麦当mdldm — 全部信息数据源
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
profile: {
|
|
5
|
+
name: '麦当 mdldm',
|
|
6
|
+
slogan: '致力于面向0基础小白的AI Agent教学',
|
|
7
|
+
tags: ['0基础Coze教程专家', 'AI Agent实战派', '企业培训经验丰富'],
|
|
8
|
+
site: 'https://mdldm.club',
|
|
9
|
+
started: '2023年底',
|
|
10
|
+
highlights: [
|
|
11
|
+
'首档Coze 0基础教程被盗版传播,全网百万播放',
|
|
12
|
+
'曾面向500+企业做AI通识培训',
|
|
13
|
+
'独创大炮/桥梁/灯塔/权杖四大AI理论体系',
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
platforms: [
|
|
18
|
+
{
|
|
19
|
+
name: 'B 站',
|
|
20
|
+
id: '麦当mdldm',
|
|
21
|
+
url: 'https://space.bilibili.com/搜索麦当mdldm',
|
|
22
|
+
focus: 'Coze 0基础教程 / AI Agent实战',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: '小红书',
|
|
26
|
+
id: '麦当mdldm',
|
|
27
|
+
url: '小红书搜索 @麦当mdldm',
|
|
28
|
+
focus: 'AI工具快速上手 / 干货图文',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: '抖音',
|
|
32
|
+
id: '麦当mdldm',
|
|
33
|
+
url: '抖音搜索 @麦当mdldm',
|
|
34
|
+
focus: '备用渠道',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: '微信公众号',
|
|
38
|
+
id: '麦当mdldm',
|
|
39
|
+
url: '微信搜索 麦当mdldm',
|
|
40
|
+
focus: '资讯 / 活动通知',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
|
|
44
|
+
products: [
|
|
45
|
+
{
|
|
46
|
+
name: '麦当的知识站',
|
|
47
|
+
url: 'https://mdldm.club',
|
|
48
|
+
desc: '所有课程的观看平台,支持免费注册 + VIP付费解锁',
|
|
49
|
+
price: '按课程定价',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: '知识星球',
|
|
53
|
+
url: '知识星球搜索 麦当mdldm',
|
|
54
|
+
desc: '社区答疑 / 最新动态 / 学员交流',
|
|
55
|
+
price: '299元/年',
|
|
56
|
+
hidden: true,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
|
|
60
|
+
courses: [
|
|
61
|
+
{
|
|
62
|
+
id: 'agent-zero',
|
|
63
|
+
name: '0基础AI Agent开发实战',
|
|
64
|
+
status: '✅ 已完结',
|
|
65
|
+
episodes: 10,
|
|
66
|
+
duration: '约5.5小时',
|
|
67
|
+
level: '0基础',
|
|
68
|
+
desc: '从0到1完成一个AI产品,全链路覆盖:AI认知→Coze→前端→部署上线',
|
|
69
|
+
highlights: [
|
|
70
|
+
'独创四大理论:大炮 / 桥梁 / 灯塔 / 权杖',
|
|
71
|
+
'工具链:Coze + LangChain + V0 + Cursor + Vercel',
|
|
72
|
+
'企业500+培训经验提炼',
|
|
73
|
+
],
|
|
74
|
+
chapters: [
|
|
75
|
+
{ ep: 0, title: '最短路径打造一款AI产品的能力', duration: '-', paid: false },
|
|
76
|
+
{ ep: 1, title: '关于大模型你不得不知道的知识', duration: '14分钟', paid: false },
|
|
77
|
+
{ ep: 2, title: 'AI必读!大语言模型四大理论梳理', duration: '17分钟', paid: false },
|
|
78
|
+
{ ep: 3, title: '国内外AI模型大盘点', duration: '31分钟', paid: false },
|
|
79
|
+
{ ep: 4, title: 'Coze实战入门', duration: '-', paid: true },
|
|
80
|
+
{ ep: 5, title: 'Coze工作流实战', duration: '-', paid: true },
|
|
81
|
+
{ ep: 6, title: 'Coze + 前端项目集成', duration: '-', paid: true },
|
|
82
|
+
{ ep: 7, title: 'LangChain / LangGraph框架实战', duration: '-', paid: true },
|
|
83
|
+
{ ep: 8, title: 'RAG技术应用', duration: '-', paid: true },
|
|
84
|
+
{ ep: 9, title: 'AI Agent最小MVP部署上线', duration: '-', paid: true },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'coze2',
|
|
89
|
+
name: 'Coze 2.0 系列课程',
|
|
90
|
+
status: '🔄 更新中',
|
|
91
|
+
episodes: null,
|
|
92
|
+
duration: '-',
|
|
93
|
+
level: '0基础',
|
|
94
|
+
desc: 'Coze 2.0 全新特性深度教学,独创内容,与时俱进',
|
|
95
|
+
highlights: [
|
|
96
|
+
'跟进Coze最新版本功能',
|
|
97
|
+
'0基础可直接上手',
|
|
98
|
+
],
|
|
99
|
+
chapters: [],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'openclaw',
|
|
103
|
+
name: '我看谁不学🦞 OpenClaw小龙虾课程',
|
|
104
|
+
status: '🔄 制作中',
|
|
105
|
+
episodes: 10,
|
|
106
|
+
duration: '-',
|
|
107
|
+
level: '0基础',
|
|
108
|
+
desc: '全面讲解OpenClaw爆火开源项目,让每个0基础小伙伴都能玩起来',
|
|
109
|
+
highlights: [
|
|
110
|
+
'多种部署方案:Mac Mini / 旧电脑 / 云部署',
|
|
111
|
+
'Skill配置:联网/记忆/浏览器/多Agent路由',
|
|
112
|
+
'手把手创建自定义Skill',
|
|
113
|
+
],
|
|
114
|
+
chapters: [
|
|
115
|
+
{ ep: 1, title: 'OpenClaw是啥?能干啥?为啥火?', paid: false },
|
|
116
|
+
{ ep: 2, title: '各种部署方案的区别', paid: false },
|
|
117
|
+
{ ep: 3, title: '开始安装你的小龙虾(本地部署详细)', paid: false },
|
|
118
|
+
{ ep: 4, title: '选择适合你的模型', paid: true },
|
|
119
|
+
{ ep: 5, title: '打通协同平台(飞书/WhatsApp)', paid: true },
|
|
120
|
+
{ ep: 6, title: '必须要用的几个Skill让虾变聪明', paid: true },
|
|
121
|
+
{ ep: 7, title: '让你的虾学会用浏览器', paid: true },
|
|
122
|
+
{ ep: 8, title: '手把手创建一个Skill', paid: true },
|
|
123
|
+
{ ep: 9, title: '记忆系统:长期记忆管理', paid: true },
|
|
124
|
+
{ ep: 10, title: '高级:多Agent路由 / 移动端 / 定时任务', paid: true },
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
package/mdldm.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const data = require('./data');
|
|
4
|
+
const { chat, login, logout, me } = require('./chat');
|
|
5
|
+
|
|
6
|
+
// ── ANSI ──────────────────────────────────────────────
|
|
7
|
+
const R = '\x1b[0m';
|
|
8
|
+
const B = '\x1b[1m';
|
|
9
|
+
const DM = '\x1b[2m';
|
|
10
|
+
const CY = '\x1b[36m';
|
|
11
|
+
const YL = '\x1b[33m';
|
|
12
|
+
const GR = '\x1b[32m';
|
|
13
|
+
const RD = '\x1b[31m';
|
|
14
|
+
const MG = '\x1b[35m';
|
|
15
|
+
|
|
16
|
+
function hr(char = '─', len = 52) { return CY + char.repeat(len) + R; }
|
|
17
|
+
|
|
18
|
+
// ── 默认视图:个人名片 ─────────────────────────────────
|
|
19
|
+
function viewProfile() {
|
|
20
|
+
const { profile, platforms, products } = data;
|
|
21
|
+
|
|
22
|
+
console.log();
|
|
23
|
+
console.log(B + CY + ` ${profile.name}` + R);
|
|
24
|
+
console.log(DM + ` ${profile.slogan}` + R);
|
|
25
|
+
console.log(' ' + hr());
|
|
26
|
+
|
|
27
|
+
console.log();
|
|
28
|
+
console.log(B + ' 🌐 知识站' + R);
|
|
29
|
+
console.log(GR + B + ` ${profile.site}` + R + DM + ' ← 所有课程都在这里' + R);
|
|
30
|
+
|
|
31
|
+
console.log();
|
|
32
|
+
console.log(B + ' 📦 付费入口' + R);
|
|
33
|
+
for (const p of products.filter(p => !p.hidden)) {
|
|
34
|
+
console.log(YL + ` · ${p.name}` + R + ` ${p.price}`);
|
|
35
|
+
console.log(DM + ` ${p.url}` + R);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(B + ' 📱 全网同 ID:麦当mdldm' + R);
|
|
40
|
+
for (const p of platforms) {
|
|
41
|
+
console.log(YL + ` · ${p.name.padEnd(5)}` + R + ` ${p.url}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log();
|
|
45
|
+
console.log(' ' + hr());
|
|
46
|
+
console.log(DM + ' 运行 mdldm login 登录知识站账号' + R);
|
|
47
|
+
console.log(DM + ' 运行 mdldm courses 查看完整课程体系' + R);
|
|
48
|
+
console.log(DM + ' 运行 mdldm chat 和我对话 💬(消耗麦子)' + R);
|
|
49
|
+
console.log(DM + ' 运行 mdldm --json 输出结构化数据' + R);
|
|
50
|
+
console.log();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── courses 视图 ──────────────────────────────────────
|
|
54
|
+
function viewCourses() {
|
|
55
|
+
const { courses } = data;
|
|
56
|
+
|
|
57
|
+
console.log();
|
|
58
|
+
console.log(B + CY + ' 麦当mdldm — 课程体系' + R);
|
|
59
|
+
console.log(' ' + hr());
|
|
60
|
+
|
|
61
|
+
for (const course of courses) {
|
|
62
|
+
console.log();
|
|
63
|
+
console.log(B + MG + ` 【${course.name}】` + R + ` ${course.status}`);
|
|
64
|
+
if (course.episodes) {
|
|
65
|
+
console.log(DM + ` 共${course.episodes}集 · ${course.duration} · 难度:${course.level}` + R);
|
|
66
|
+
}
|
|
67
|
+
console.log(DM + ` ${course.desc}` + R);
|
|
68
|
+
|
|
69
|
+
if (course.highlights.length) {
|
|
70
|
+
console.log();
|
|
71
|
+
for (const h of course.highlights) {
|
|
72
|
+
console.log(GR + ` ✓ ` + R + h);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (course.chapters.length) {
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(B + ' 课程目录:' + R);
|
|
79
|
+
for (const ch of course.chapters) {
|
|
80
|
+
const tag = ch.paid ? RD + '[VIP]' + R : GR + '[免费]' + R;
|
|
81
|
+
const title = `EP${String(ch.ep).padStart(2, '0')} · ${ch.title}`;
|
|
82
|
+
const dur = ch.duration ? DM + ` ${ch.duration}` + R : '';
|
|
83
|
+
console.log(` ${tag} ${title}${dur}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log();
|
|
88
|
+
console.log(' ' + hr('·', 52));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log();
|
|
92
|
+
console.log(GR + B + ' 所有课程观看地址:https://mdldm.club' + R);
|
|
93
|
+
console.log();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── --json 视图 ───────────────────────────────────────
|
|
97
|
+
function viewJson() {
|
|
98
|
+
console.log(JSON.stringify(data, null, 2));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── --help 视图 ───────────────────────────────────────
|
|
102
|
+
function viewHelp() {
|
|
103
|
+
console.log();
|
|
104
|
+
console.log(B + ' mdldm CLI — 使用说明' + R);
|
|
105
|
+
console.log(' ' + hr());
|
|
106
|
+
console.log();
|
|
107
|
+
console.log(` ${YL}mdldm${R} 个人名片 + 账号信息`);
|
|
108
|
+
console.log(` ${YL}mdldm login${R} 登录知识站账号`);
|
|
109
|
+
console.log(` ${YL}mdldm logout${R} 退出登录`);
|
|
110
|
+
console.log(` ${YL}mdldm me${R} 查看当前账号和麦子余额`);
|
|
111
|
+
console.log(` ${YL}mdldm courses${R} 完整课程体系 + 目录`);
|
|
112
|
+
console.log(` ${YL}mdldm chat${R} 和我对话 💬(消耗麦子)`);
|
|
113
|
+
console.log(` ${YL}mdldm --json${R} 输出结构化 JSON(供 AI Agent 使用)`);
|
|
114
|
+
console.log(` ${YL}mdldm --help${R} 显示帮助`);
|
|
115
|
+
console.log();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── 路由 ──────────────────────────────────────────────
|
|
119
|
+
const arg = process.argv[2];
|
|
120
|
+
|
|
121
|
+
if (!arg) viewProfile();
|
|
122
|
+
else if (arg === 'login') login();
|
|
123
|
+
else if (arg === 'logout') logout();
|
|
124
|
+
else if (arg === 'me') me();
|
|
125
|
+
else if (arg === 'courses') viewCourses();
|
|
126
|
+
else if (arg === 'chat') chat();
|
|
127
|
+
else if (arg === '--json') viewJson();
|
|
128
|
+
else if (arg === '--help' || arg === '-h') viewHelp();
|
|
129
|
+
else {
|
|
130
|
+
console.log(`\n 未知命令:${arg},运行 mdldm --help 查看用法\n`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mdldm",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "麦当mdldm — 专注0基础教学的AI技术博主 | AI Agent 实战派",
|
|
5
|
+
"bin": {
|
|
6
|
+
"mdldm": "./mdldm.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "node mdldm.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"mdldm.js",
|
|
13
|
+
"chat.js",
|
|
14
|
+
"data.js"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mdldm",
|
|
18
|
+
"AI",
|
|
19
|
+
"agent",
|
|
20
|
+
"coze",
|
|
21
|
+
"课程",
|
|
22
|
+
"教学",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"author": "麦当mdldm <hi@mdldm.club>",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"homepage": "https://mdldm.club",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/mdldm/cli"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=16"
|
|
34
|
+
}
|
|
35
|
+
}
|