@rennii/deepseek-cli 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/deepseek-cli.js +792 -0
- package/deepseek_pow_solver.js +74 -0
- package/package.json +25 -0
- package/sha3_wasm_bg.7b9ca65ddd.wasm +0 -0
- package/test-deepseek-cli.js +117 -0
package/deepseek-cli.js
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const { execFile } = require("node:child_process");
|
|
6
|
+
const { promisify } = require("node:util");
|
|
7
|
+
const {
|
|
8
|
+
existsSync,
|
|
9
|
+
chmodSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} = require("node:fs");
|
|
16
|
+
const { dirname, join } = require("node:path");
|
|
17
|
+
const { randomUUID } = require("node:crypto");
|
|
18
|
+
const readline = require("node:readline/promises");
|
|
19
|
+
|
|
20
|
+
const execFileAsync = promisify(execFile);
|
|
21
|
+
const APP_DIR = __dirname;
|
|
22
|
+
const BASE_URL = "https://chat.deepseek.com/api/v0";
|
|
23
|
+
const LOGIN_URL = "https://chat.deepseek.com/";
|
|
24
|
+
const DATA_DIR = join(process.env.HOME || process.cwd(), ".deepseek");
|
|
25
|
+
const CONFIG_PATH = join(DATA_DIR, "config.json");
|
|
26
|
+
const AUTH_PATH = join(DATA_DIR, "auth.json");
|
|
27
|
+
const SESSION_DIR = join(DATA_DIR, "sessions");
|
|
28
|
+
const FLAT_SESSION_PATH = join(DATA_DIR, "sessions.json");
|
|
29
|
+
const LEGACY_SESSION_PATH = join(process.env.HOME || process.cwd(), ".local", "share", "deepseek-cli", "sessions.json");
|
|
30
|
+
const MAX_AGENT_STEPS = 25;
|
|
31
|
+
const OUTPUT_TRUNCATE = 4000;
|
|
32
|
+
const COMMAND_TIMEOUT = 120_000;
|
|
33
|
+
const CWD_SENTINEL = "__DEEPSEEK_CWD__";
|
|
34
|
+
|
|
35
|
+
const RESET = "\x1b[0m";
|
|
36
|
+
const BOLD = "\x1b[1m";
|
|
37
|
+
const DIM = "\x1b[2m";
|
|
38
|
+
const BLUE = "\x1b[38;5;110m";
|
|
39
|
+
const CYAN = "\x1b[38;5;116m";
|
|
40
|
+
const GREEN = "\x1b[38;5;150m";
|
|
41
|
+
const YELLOW = "\x1b[38;5;180m";
|
|
42
|
+
const RED = "\x1b[38;5;174m";
|
|
43
|
+
|
|
44
|
+
const COMMAND_SUGGESTIONS = [
|
|
45
|
+
["/login", "mở trang đăng nhập"],
|
|
46
|
+
["/logout", "xóa token đã lưu"],
|
|
47
|
+
["/new", "phiên mới"],
|
|
48
|
+
["/resume", "mở phiên đã lưu"],
|
|
49
|
+
["/agent", "bật/tắt terminal"],
|
|
50
|
+
["/clear", "xóa màn hình"],
|
|
51
|
+
["/help", "trợ giúp"],
|
|
52
|
+
["/exit", "đóng CLI"],
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const AGENT_PREAMBLE = [
|
|
56
|
+
"Bạn đang chạy trong một terminal CLI trên Termux (Android/Linux) và CÓ THỂ chạy lệnh shell trên máy của người dùng.",
|
|
57
|
+
"Quy tắc:",
|
|
58
|
+
"- Khi cần chạy lệnh, xuất một khối mã ```bash chứa lệnh. Hệ thống sẽ chạy (sau khi người dùng xác nhận) và gửi lại stdout/stderr/exit code cho bạn ở lượt tiếp theo.",
|
|
59
|
+
"- DỪNG ngay sau khi xuất khối lệnh, chờ kết quả thật rồi mới tiếp tục. Tuyệt đối không bịa kết quả.",
|
|
60
|
+
"- Chỉ dùng tag ```bash cho lệnh muốn chạy. Ví dụ minh hoạ không cần chạy thì dùng khối mã không có tag bash/sh/shell.",
|
|
61
|
+
"- Thư mục làm việc được giữ giữa các lệnh (cd có tác dụng); biến môi trường thì không.",
|
|
62
|
+
"- Khi đã đủ thông tin, trả lời trực tiếp, không kèm khối bash.",
|
|
63
|
+
].join("\n");
|
|
64
|
+
|
|
65
|
+
class UserInterrupted extends Error {}
|
|
66
|
+
|
|
67
|
+
function readJson(path, fallback) {
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
70
|
+
} catch {
|
|
71
|
+
return fallback;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function saveJson(path, value, mode = 0o600) {
|
|
76
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
77
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
78
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode });
|
|
79
|
+
renameSync(temporary, path);
|
|
80
|
+
chmodSync(path, mode);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function bootstrapDataDirectory({
|
|
84
|
+
dataDir = DATA_DIR,
|
|
85
|
+
legacySessionPath = LEGACY_SESSION_PATH,
|
|
86
|
+
} = {}) {
|
|
87
|
+
const configPath = join(dataDir, "config.json");
|
|
88
|
+
const authPath = join(dataDir, "auth.json");
|
|
89
|
+
const sessionDir = join(dataDir, "sessions");
|
|
90
|
+
mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
91
|
+
try { chmodSync(dataDir, 0o700); } catch {}
|
|
92
|
+
const config = readJson(configPath, {});
|
|
93
|
+
const auth = readJson(authPath, {});
|
|
94
|
+
if (!auth.token && config.token) {
|
|
95
|
+
auth.token = config.token;
|
|
96
|
+
delete config.token;
|
|
97
|
+
saveJson(authPath, auth);
|
|
98
|
+
saveJson(configPath, config);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
config,
|
|
102
|
+
configPath,
|
|
103
|
+
auth,
|
|
104
|
+
authPath,
|
|
105
|
+
sessionDir,
|
|
106
|
+
legacySessionPaths: [join(dataDir, "sessions.json"), legacySessionPath],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const RUNTIME_DATA = bootstrapDataDirectory();
|
|
111
|
+
|
|
112
|
+
function resolveToken(runtimeData = RUNTIME_DATA) {
|
|
113
|
+
return process.env.DEEPSEEK_TOKEN || runtimeData.auth.token || "";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function nowIso() {
|
|
117
|
+
return new Date().toISOString();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function newLocalSession() {
|
|
121
|
+
const timestamp = nowIso();
|
|
122
|
+
return {
|
|
123
|
+
id: randomUUID(),
|
|
124
|
+
title: "Phiên mới",
|
|
125
|
+
created_at: timestamp,
|
|
126
|
+
updated_at: timestamp,
|
|
127
|
+
chat_session_id: null,
|
|
128
|
+
parent_message_id: null,
|
|
129
|
+
messages: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sessionPreview(session, limit = 56) {
|
|
134
|
+
return String(session.title || "Phiên không tên").replace(/\n/g, " ").slice(0, limit);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function truncateTerminalText(text, limit) {
|
|
138
|
+
if (limit <= 0) return "";
|
|
139
|
+
if (text.length <= limit) return text;
|
|
140
|
+
return limit === 1 ? "…" : `${text.slice(0, limit - 1)}…`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function sessionPickerLine(session, index, selected, columns) {
|
|
144
|
+
const marker = index === selected ? `${CYAN}›${RESET}` : " ";
|
|
145
|
+
const updated = String(session.updated_at || "").slice(0, 19).replace("T", " ");
|
|
146
|
+
const title = truncateTerminalText(sessionPreview(session, 1000), Math.max(1, columns - updated.length - 4));
|
|
147
|
+
const color = index === selected ? BOLD : DIM;
|
|
148
|
+
return `${marker} ${color}${title}${RESET} ${DIM}${updated}${RESET}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function extractCommands(text) {
|
|
152
|
+
return [...text.matchAll(/```(?:bash|sh|shell)[^\n]*\n([\s\S]*?)```/gi)]
|
|
153
|
+
.map((match) => match[1].trim())
|
|
154
|
+
.filter(Boolean);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatCommandFeedback(results) {
|
|
158
|
+
const parts = ["Kết quả các lệnh đã chạy:"];
|
|
159
|
+
for (const [command, code, output] of results) {
|
|
160
|
+
if (code === null) {
|
|
161
|
+
parts.push(`\n$ ${command}\n(người dùng bỏ qua, không chạy)`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
let clipped = output.slice(0, OUTPUT_TRUNCATE);
|
|
165
|
+
if (output.length > OUTPUT_TRUNCATE) clipped += "\n...(cắt bớt output)";
|
|
166
|
+
parts.push(`\n$ ${command}\nexit=${code}\n${clipped || "(không có output)"}`);
|
|
167
|
+
}
|
|
168
|
+
return parts.join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function matchingCommands(prefix) {
|
|
172
|
+
return COMMAND_SUGGESTIONS.filter(([command]) => command.startsWith(prefix.toLowerCase()));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseSseEvent(event, state, onDelta) {
|
|
176
|
+
if (event.response_message_id != null) state.parentMessageId = event.response_message_id;
|
|
177
|
+
const path = event.p;
|
|
178
|
+
const value = event.v;
|
|
179
|
+
if (value && typeof value === "object" && value.response && typeof value.response === "object") {
|
|
180
|
+
const response = value.response;
|
|
181
|
+
if (response.message_id != null) state.parentMessageId = response.message_id;
|
|
182
|
+
state.fragmentTypes = (response.fragments || []).map((fragment) => fragment.type);
|
|
183
|
+
for (const fragment of response.fragments || []) {
|
|
184
|
+
if (fragment.type === "RESPONSE" && typeof fragment.content === "string") onDelta(fragment.content);
|
|
185
|
+
}
|
|
186
|
+
state.activeFragmentType = state.fragmentTypes.at(-1) || null;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (path === "response/content") state.activeFragmentType = "RESPONSE";
|
|
190
|
+
else if (typeof path === "string" && /^response\/fragments\/\d+\/content$/.test(path)) {
|
|
191
|
+
state.activeFragmentType = state.fragmentTypes[Number(path.split("/")[2])] || null;
|
|
192
|
+
} else if (typeof path === "string" && path.includes("thinking_content")) state.activeFragmentType = "THINK";
|
|
193
|
+
else if (path != null) return;
|
|
194
|
+
if (state.activeFragmentType === "RESPONSE" && typeof value === "string") onDelta(value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
class SessionStore {
|
|
198
|
+
constructor(
|
|
199
|
+
sessionDir = SESSION_DIR,
|
|
200
|
+
legacySessionPaths = [FLAT_SESSION_PATH, LEGACY_SESSION_PATH],
|
|
201
|
+
archiveLegacyPaths = [FLAT_SESSION_PATH],
|
|
202
|
+
) {
|
|
203
|
+
this.sessionDir = sessionDir;
|
|
204
|
+
this.legacySessionPaths = legacySessionPaths;
|
|
205
|
+
this.archiveLegacyPaths = new Set(archiveLegacyPaths);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
load() {
|
|
209
|
+
this.migrateLegacySessions();
|
|
210
|
+
if (!existsSync(this.sessionDir)) return [];
|
|
211
|
+
const sessions = [];
|
|
212
|
+
for (const path of this.sessionFiles(this.sessionDir)) {
|
|
213
|
+
const session = readJson(path, null);
|
|
214
|
+
if (session && typeof session === "object" && !Array.isArray(session)) sessions.push(session);
|
|
215
|
+
}
|
|
216
|
+
return sessions.sort((left, right) => String(right.updated_at || "").localeCompare(String(left.updated_at || "")));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
save(sessions) {
|
|
220
|
+
if (!Array.isArray(sessions)) throw new Error("Lịch sử phiên không có định dạng danh sách");
|
|
221
|
+
for (const session of sessions) saveJson(this.sessionPath(session), session);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
sessionPath(session) {
|
|
225
|
+
const timestamp = new Date(session.created_at || session.updated_at || nowIso());
|
|
226
|
+
const date = Number.isNaN(timestamp.getTime()) ? new Date() : timestamp;
|
|
227
|
+
const year = String(date.getUTCFullYear());
|
|
228
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
229
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
230
|
+
const clock = date.toISOString().replace(/\.\d{3}Z$/, "").replace(/:/g, "-");
|
|
231
|
+
const id = String(session.id || randomUUID()).replace(/[^a-zA-Z0-9-]/g, "_");
|
|
232
|
+
return join(this.sessionDir, year, month, day, `session-${clock}-${id}.json`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
sessionFiles(directory) {
|
|
236
|
+
const files = [];
|
|
237
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
238
|
+
const path = join(directory, entry.name);
|
|
239
|
+
if (entry.isDirectory()) files.push(...this.sessionFiles(path));
|
|
240
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) files.push(path);
|
|
241
|
+
}
|
|
242
|
+
return files;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
migrateLegacySessions() {
|
|
246
|
+
for (const legacyPath of this.legacySessionPaths) {
|
|
247
|
+
if (!existsSync(legacyPath)) continue;
|
|
248
|
+
const sessions = readJson(legacyPath, null);
|
|
249
|
+
if (!Array.isArray(sessions)) continue;
|
|
250
|
+
for (const session of sessions) {
|
|
251
|
+
if (session && typeof session === "object") {
|
|
252
|
+
const target = this.sessionPath(session);
|
|
253
|
+
if (!existsSync(target)) saveJson(target, session);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (this.archiveLegacyPaths.has(legacyPath)) {
|
|
257
|
+
renameSync(legacyPath, `${legacyPath}.migrated`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
class RawReader {
|
|
264
|
+
constructor(stream = process.stdin) {
|
|
265
|
+
this.stream = stream;
|
|
266
|
+
this.queue = [];
|
|
267
|
+
this.waiter = null;
|
|
268
|
+
this.onData = (chunk) => {
|
|
269
|
+
const text = String(chunk);
|
|
270
|
+
if (this.waiter) {
|
|
271
|
+
const resolve = this.waiter;
|
|
272
|
+
this.waiter = null;
|
|
273
|
+
resolve(text);
|
|
274
|
+
} else this.queue.push(text);
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
open() {
|
|
279
|
+
this.stream.setEncoding("utf8");
|
|
280
|
+
this.stream.setRawMode?.(true);
|
|
281
|
+
this.stream.resume();
|
|
282
|
+
this.stream.on("data", this.onData);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
close() {
|
|
286
|
+
this.stream.off("data", this.onData);
|
|
287
|
+
this.stream.setRawMode?.(false);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
next(timeout) {
|
|
291
|
+
if (this.queue.length) return Promise.resolve(this.queue.shift());
|
|
292
|
+
return new Promise((resolve) => {
|
|
293
|
+
let timer;
|
|
294
|
+
this.waiter = (value) => {
|
|
295
|
+
if (timer) clearTimeout(timer);
|
|
296
|
+
resolve(value);
|
|
297
|
+
};
|
|
298
|
+
if (timeout != null) {
|
|
299
|
+
timer = setTimeout(() => {
|
|
300
|
+
if (this.waiter) this.waiter = null;
|
|
301
|
+
resolve(null);
|
|
302
|
+
}, timeout);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
class TerminalUI {
|
|
309
|
+
write(text) {
|
|
310
|
+
process.stdout.write(text);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
header(session) {
|
|
314
|
+
console.log(`${BOLD}${BLUE}DeepSeek${RESET} ${DIM}· ${sessionPreview(session, 32)} · /help${RESET}`);
|
|
315
|
+
console.log(`${DIM}${"─".repeat(48)}${RESET}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
clear(session) {
|
|
319
|
+
this.write("\x1b[3J\x1b[2J\x1b[H");
|
|
320
|
+
this.header(session);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
notice(message) { console.log(`${DIM}${message}${RESET}`); }
|
|
324
|
+
error(message) { console.log(`${RED}${message}${RESET}`); }
|
|
325
|
+
assistantStart() { console.log(`\n${BOLD}${GREEN}DeepSeek${RESET}`); }
|
|
326
|
+
stream(fragment) { this.write(fragment); }
|
|
327
|
+
|
|
328
|
+
help() {
|
|
329
|
+
console.log([
|
|
330
|
+
`${BOLD}Lệnh${RESET}`,
|
|
331
|
+
` ${CYAN}/login${RESET} Mở trang đăng nhập DeepSeek`,
|
|
332
|
+
` ${CYAN}/logout${RESET} Xóa token DeepSeek đã lưu`,
|
|
333
|
+
` ${CYAN}/new${RESET} Tạo phiên mới`,
|
|
334
|
+
` ${CYAN}/resume${RESET} Chọn phiên đã lưu để tiếp tục`,
|
|
335
|
+
` ${CYAN}/agent${RESET} Bật/tắt chế độ chạy lệnh terminal`,
|
|
336
|
+
` ${CYAN}/clear${RESET} Xóa nội dung khỏi màn hình`,
|
|
337
|
+
` ${CYAN}/help${RESET} Hiện trợ giúp`,
|
|
338
|
+
` ${CYAN}/exit${RESET} Đóng CLI`,
|
|
339
|
+
`${DIM}Khi bật terminal: DeepSeek đề xuất lệnh, bạn xác nhận [Enter]/s/a/q rồi nó tự đọc kết quả.${RESET}`,
|
|
340
|
+
`${DIM}Ctrl+C hủy phản hồi đang chạy và đóng CLI.${RESET}`,
|
|
341
|
+
].join("\n"));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
showCommandSuggestions(prefix) {
|
|
345
|
+
const suggestions = matchingCommands(prefix).map(([command, description]) =>
|
|
346
|
+
` ${CYAN}${command.slice(0, prefix.length)}${RESET}${DIM}${command.slice(prefix.length)}${RESET} ${DIM}${description}${RESET}`,
|
|
347
|
+
);
|
|
348
|
+
this.write(`\x1b[s\n\x1b[J${suggestions.join("\n")}\x1b[u`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
clearCommandSuggestions() { this.write("\x1b[J"); }
|
|
352
|
+
|
|
353
|
+
completeCommand(characters) {
|
|
354
|
+
const prefix = characters.join("");
|
|
355
|
+
const matches = matchingCommands(prefix);
|
|
356
|
+
if (matches.length !== 1 || matches[0][0] === prefix.toLowerCase()) return false;
|
|
357
|
+
const suffix = matches[0][0].slice(prefix.length);
|
|
358
|
+
characters.push(...suffix);
|
|
359
|
+
this.write(`${DIM}${suffix}${RESET}`);
|
|
360
|
+
this.clearCommandSuggestions();
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async readPrompt() {
|
|
365
|
+
if (!process.stdin.isTTY) {
|
|
366
|
+
const iterator = process.stdin[Symbol.asyncIterator]();
|
|
367
|
+
const first = await iterator.next();
|
|
368
|
+
return first.done ? "" : String(first.value).trim();
|
|
369
|
+
}
|
|
370
|
+
this.write(`\n${CYAN}› ${RESET}`);
|
|
371
|
+
const reader = new RawReader();
|
|
372
|
+
const characters = [];
|
|
373
|
+
let pending = null;
|
|
374
|
+
reader.open();
|
|
375
|
+
try {
|
|
376
|
+
while (true) {
|
|
377
|
+
const chunk = pending ?? await reader.next();
|
|
378
|
+
pending = null;
|
|
379
|
+
for (let index = 0; index < chunk.length; index += 1) {
|
|
380
|
+
const character = chunk[index];
|
|
381
|
+
if (character === "\u0003") throw new UserInterrupted();
|
|
382
|
+
if (character === "\r" || character === "\n") {
|
|
383
|
+
if (characters[0] === "/" && !characters.includes("\n") && this.completeCommand(characters)) continue;
|
|
384
|
+
if (characters[0] === "/") this.clearCommandSuggestions();
|
|
385
|
+
if (index === chunk.length - 1) {
|
|
386
|
+
const next = await reader.next(120);
|
|
387
|
+
if (next == null) {
|
|
388
|
+
this.write("\n");
|
|
389
|
+
return characters.join("").trim();
|
|
390
|
+
}
|
|
391
|
+
pending = next;
|
|
392
|
+
}
|
|
393
|
+
characters.push("\n");
|
|
394
|
+
this.write("\n");
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (character === "\u007f" || character === "\b") {
|
|
398
|
+
if (characters.length) {
|
|
399
|
+
characters.pop();
|
|
400
|
+
this.write("\b \b");
|
|
401
|
+
}
|
|
402
|
+
if (characters[0] === "/" && !characters.includes("\n")) this.showCommandSuggestions(characters.join(""));
|
|
403
|
+
else if (!characters.length) this.clearCommandSuggestions();
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (character === "\u001b") {
|
|
407
|
+
while (index + 1 < chunk.length && !/[A-Za-z~]/.test(chunk[index + 1])) index += 1;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (character >= " ") {
|
|
411
|
+
characters.push(character);
|
|
412
|
+
this.write(character);
|
|
413
|
+
if (characters[0] === "/" && !characters.includes("\n")) this.showCommandSuggestions(characters.join(""));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} finally {
|
|
418
|
+
reader.close();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async readKey() {
|
|
423
|
+
const reader = new RawReader();
|
|
424
|
+
reader.open();
|
|
425
|
+
try {
|
|
426
|
+
const key = await reader.next();
|
|
427
|
+
if (key === "\u0003") throw new UserInterrupted();
|
|
428
|
+
if (key === "\r" || key === "\n") return "ENTER";
|
|
429
|
+
if (key === "\u001b[A") return "UP";
|
|
430
|
+
if (key === "\u001b[B") return "DOWN";
|
|
431
|
+
if (key.startsWith("\u001b")) return "ESC";
|
|
432
|
+
return key[0] || "";
|
|
433
|
+
} finally {
|
|
434
|
+
reader.close();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async confirmCommand(command) {
|
|
439
|
+
this.write(`\n${BOLD}${YELLOW}⚙ Lệnh đề xuất:${RESET}\n`);
|
|
440
|
+
for (const line of command.split("\n")) this.write(` ${CYAN}${line}${RESET}\n`);
|
|
441
|
+
this.write(`${DIM}[Enter]=chạy [s]=bỏ qua [a]=chạy hết [q]=dừng${RESET} `);
|
|
442
|
+
if (!process.stdin.isTTY) return "run";
|
|
443
|
+
const key = await this.readKey();
|
|
444
|
+
this.write("\n");
|
|
445
|
+
if (key === "ENTER") return "run";
|
|
446
|
+
if (key.toLowerCase() === "s") return "skip";
|
|
447
|
+
if (key.toLowerCase() === "a") return "all";
|
|
448
|
+
if (key.toLowerCase() === "q") return "quit";
|
|
449
|
+
return "run";
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
showCommandResult(code, output) {
|
|
453
|
+
this.write(`${code === 0 ? GREEN : RED}exit=${code}${RESET}\n`);
|
|
454
|
+
for (const line of output.split("\n")) if (line) this.write(`${DIM}│${RESET} ${line}\n`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
renderTranscript(messages) {
|
|
458
|
+
for (const message of messages) {
|
|
459
|
+
if (message.role === "user") this.write(`\n${BOLD}${CYAN}Bạn${RESET}\n${message.content}\n`);
|
|
460
|
+
if (message.role === "assistant") this.write(`\n${BOLD}${GREEN}DeepSeek${RESET}\n${message.content}\n`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async selectSession(sessions) {
|
|
465
|
+
if (!sessions.length) {
|
|
466
|
+
this.notice("Chưa có phiên nào được lưu.");
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
470
|
+
sessions.forEach((session, index) => console.log(`${index + 1}. ${sessionPreview(session)}`));
|
|
471
|
+
const answer = await readline.createInterface({ input: process.stdin, output: process.stdout }).question("Chọn phiên (Enter để hủy): ");
|
|
472
|
+
const index = Number(answer) - 1;
|
|
473
|
+
return Number.isInteger(index) && sessions[index] ? sessions[index] : null;
|
|
474
|
+
}
|
|
475
|
+
let selected = 0;
|
|
476
|
+
while (true) {
|
|
477
|
+
const columns = process.stdout.columns || 80;
|
|
478
|
+
const visibleRows = Math.max(1, (process.stdout.rows || 24) - 4);
|
|
479
|
+
const first = Math.min(Math.max(0, selected - visibleRows + 1), Math.max(0, sessions.length - visibleRows));
|
|
480
|
+
this.write("\x1b[2J\x1b[H");
|
|
481
|
+
console.log(`${BOLD}${BLUE}Tiếp tục phiên${RESET}`);
|
|
482
|
+
console.log(`${DIM}↑ ↓ để chọn · Enter để mở · Esc để hủy${RESET}\n`);
|
|
483
|
+
for (let index = first; index < Math.min(first + visibleRows, sessions.length); index += 1) {
|
|
484
|
+
console.log(sessionPickerLine(sessions[index], index, selected, columns));
|
|
485
|
+
}
|
|
486
|
+
const key = await this.readKey();
|
|
487
|
+
if (key === "ENTER") return sessions[selected];
|
|
488
|
+
if (key === "ESC") return null;
|
|
489
|
+
if (key === "UP") selected = (selected - 1 + sessions.length) % sessions.length;
|
|
490
|
+
if (key === "DOWN") selected = (selected + 1) % sessions.length;
|
|
491
|
+
if (/^[1-9]$/.test(key) && Number(key) <= sessions.length) selected = Number(key) - 1;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
class DeepSeekCLI {
|
|
497
|
+
constructor(store = new SessionStore(), runtimeData = RUNTIME_DATA) {
|
|
498
|
+
this.runtimeData = runtimeData;
|
|
499
|
+
this.token = resolveToken(runtimeData);
|
|
500
|
+
this.openUrl = (url) => execFileAsync("termux-open-url", [url]);
|
|
501
|
+
this.store = store;
|
|
502
|
+
this.ui = new TerminalUI();
|
|
503
|
+
this.sessionId = null;
|
|
504
|
+
this.parentMessageId = null;
|
|
505
|
+
this.persistenceError = null;
|
|
506
|
+
try { this.savedSessions = this.store.load(); }
|
|
507
|
+
catch (error) { this.savedSessions = []; this.persistenceError = error.message; }
|
|
508
|
+
this.activeSession = newLocalSession();
|
|
509
|
+
this.agentEnabled = true;
|
|
510
|
+
this.agentPrimed = false;
|
|
511
|
+
this.agentCwd = process.cwd();
|
|
512
|
+
this.abortController = null;
|
|
513
|
+
this.interrupted = false;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
headers(extra = {}) {
|
|
517
|
+
return {
|
|
518
|
+
Authorization: `Bearer ${this.token}`,
|
|
519
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0",
|
|
520
|
+
"Content-Type": "application/json",
|
|
521
|
+
Referer: "https://chat.deepseek.com/",
|
|
522
|
+
Origin: "https://chat.deepseek.com",
|
|
523
|
+
Accept: "application/json, text/plain, */*",
|
|
524
|
+
"x-client-locale": "en_US",
|
|
525
|
+
"x-client-platform": "web",
|
|
526
|
+
"x-client-version": "2.0.2",
|
|
527
|
+
...extra,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
apiMessage(payload) {
|
|
532
|
+
return payload?.data?.biz_msg || payload?.msg || "Phản hồi API không hợp lệ";
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async fetch(path, options = {}, timeout = 30_000) {
|
|
536
|
+
const controller = new AbortController();
|
|
537
|
+
this.abortController = controller;
|
|
538
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
539
|
+
try {
|
|
540
|
+
return await globalThis.fetch(`${BASE_URL}${path}`, { ...options, signal: controller.signal });
|
|
541
|
+
} catch (error) {
|
|
542
|
+
if (error.name === "AbortError" && this.interrupted) throw new UserInterrupted();
|
|
543
|
+
throw error;
|
|
544
|
+
} finally {
|
|
545
|
+
clearTimeout(timer);
|
|
546
|
+
this.abortController = null;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async createSession() {
|
|
551
|
+
const response = await this.fetch("/chat_session/create", { method: "POST", headers: this.headers(), body: "{}" });
|
|
552
|
+
const payload = await response.json();
|
|
553
|
+
if (!response.ok || payload.code !== 0 || ![null, undefined, 0].includes(payload.data?.biz_code)) throw new Error(this.apiMessage(payload));
|
|
554
|
+
const sessionId = payload.data?.biz_data?.chat_session?.id;
|
|
555
|
+
if (!sessionId) throw new Error("API không trả về chat_session.id");
|
|
556
|
+
this.sessionId = sessionId;
|
|
557
|
+
this.parentMessageId = null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async getPowResponse() {
|
|
561
|
+
const response = await this.fetch("/chat/create_pow_challenge", { method: "POST", headers: this.headers(), body: JSON.stringify({ target_path: "/api/v0/chat/completion" }) });
|
|
562
|
+
const payload = await response.json();
|
|
563
|
+
const challenge = payload.data?.biz_data?.challenge;
|
|
564
|
+
if (!response.ok || payload.code !== 0 || ![null, undefined, 0].includes(payload.data?.biz_code) || !challenge) throw new Error(this.apiMessage(payload));
|
|
565
|
+
const solver = join(APP_DIR, "deepseek_pow_solver.js");
|
|
566
|
+
const wasm = join(APP_DIR, "sha3_wasm_bg.7b9ca65ddd.wasm");
|
|
567
|
+
if (!existsSync(solver) || !existsSync(wasm)) throw new Error("Thiếu deepseek_pow_solver.js hoặc sha3_wasm_bg.7b9ca65ddd.wasm");
|
|
568
|
+
const { stdout } = await execFileAsync(process.execPath, [solver, JSON.stringify(challenge)], { timeout: 30_000 });
|
|
569
|
+
if (!stdout.trim()) throw new Error("PoW solver không trả về kết quả");
|
|
570
|
+
return stdout.trim();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async parseSse(response, onDelta) {
|
|
574
|
+
const decoder = new TextDecoder();
|
|
575
|
+
const state = { parentMessageId: this.parentMessageId, fragmentTypes: [], activeFragmentType: null };
|
|
576
|
+
const content = [];
|
|
577
|
+
let buffered = "";
|
|
578
|
+
const append = (fragment) => { content.push(fragment); onDelta?.(fragment); };
|
|
579
|
+
for await (const chunk of response.body) {
|
|
580
|
+
buffered += decoder.decode(chunk, { stream: true });
|
|
581
|
+
const lines = buffered.split(/\r?\n/);
|
|
582
|
+
buffered = lines.pop();
|
|
583
|
+
for (const line of lines) {
|
|
584
|
+
if (!line.startsWith("data:")) continue;
|
|
585
|
+
const raw = line.slice(5).trimStart();
|
|
586
|
+
if (!raw || raw === "[DONE]") continue;
|
|
587
|
+
try { parseSseEvent(JSON.parse(raw), state, append); } catch {}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
this.parentMessageId = state.parentMessageId;
|
|
591
|
+
return content.join("");
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async ask(prompt, onDelta) {
|
|
595
|
+
try {
|
|
596
|
+
if (!this.token) return "Thiếu DEEPSEEK_TOKEN.";
|
|
597
|
+
if (!this.sessionId) await this.createSession();
|
|
598
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
599
|
+
const pow = await this.getPowResponse();
|
|
600
|
+
const response = await this.fetch("/chat/completion", {
|
|
601
|
+
method: "POST",
|
|
602
|
+
headers: this.headers({ Referer: `https://chat.deepseek.com/a/chat/s/${this.sessionId}`, "x-ds-pow-response": pow }),
|
|
603
|
+
body: JSON.stringify({ prompt, chat_session_id: this.sessionId, parent_message_id: this.parentMessageId, thinking_enabled: false, search_enabled: false, ref_file_ids: [], model_type: "default" }),
|
|
604
|
+
}, 120_000);
|
|
605
|
+
if (response.status === 200 && response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
606
|
+
return (await this.parseSse(response, onDelta)) || "Lỗi: API không trả về nội dung";
|
|
607
|
+
}
|
|
608
|
+
if (response.status === 401) return "Token hết hạn.";
|
|
609
|
+
if (response.status === 429) return "Đã chạm giới hạn yêu cầu.";
|
|
610
|
+
const text = await response.text();
|
|
611
|
+
let payload;
|
|
612
|
+
try { payload = JSON.parse(text); } catch { return `Lỗi ${response.status}: ${text.slice(0, 200)}`; }
|
|
613
|
+
const bizCode = payload.data?.biz_code;
|
|
614
|
+
if (bizCode === 26 && attempt === 0) { this.parentMessageId = null; continue; }
|
|
615
|
+
if (!response.ok || payload.code !== 0 || ![null, undefined, 0].includes(bizCode)) return `Lỗi API ${bizCode ?? payload.code}: ${this.apiMessage(payload)}`;
|
|
616
|
+
return payload.content || payload.message || JSON.stringify(payload);
|
|
617
|
+
}
|
|
618
|
+
} catch (error) {
|
|
619
|
+
if (error instanceof UserInterrupted) throw error;
|
|
620
|
+
return `Lỗi: ${String(error.message || error).slice(0, 300)}`;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
persistActiveSession() {
|
|
625
|
+
if (this.persistenceError) return;
|
|
626
|
+
Object.assign(this.activeSession, { chat_session_id: this.sessionId, parent_message_id: this.parentMessageId, updated_at: nowIso() });
|
|
627
|
+
const index = this.savedSessions.findIndex((session) => session.id === this.activeSession.id);
|
|
628
|
+
if (index >= 0) this.savedSessions[index] = this.activeSession;
|
|
629
|
+
else this.savedSessions.push(this.activeSession);
|
|
630
|
+
this.savedSessions.sort((left, right) => String(right.updated_at).localeCompare(String(left.updated_at)));
|
|
631
|
+
this.store.save(this.savedSessions);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async runCommand(command) {
|
|
635
|
+
const wrapped = `${command}\n__dsk_rc=$?\nprintf '\\n%s%s\\n' '${CWD_SENTINEL}' "$PWD"\nexit $__dsk_rc`;
|
|
636
|
+
try {
|
|
637
|
+
const { stdout, stderr } = await execFileAsync("bash", ["-c", wrapped], { cwd: this.agentCwd, timeout: COMMAND_TIMEOUT, maxBuffer: 1_048_576 });
|
|
638
|
+
return this.commandResult(0, stdout, stderr);
|
|
639
|
+
} catch (error) {
|
|
640
|
+
if (error.killed) return [124, `Lệnh vượt quá ${COMMAND_TIMEOUT / 1000}s và bị hủy.`];
|
|
641
|
+
return this.commandResult(Number.isInteger(error.code) ? error.code : 1, error.stdout || "", error.stderr || error.message || "");
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
commandResult(code, stdout, stderr) {
|
|
646
|
+
const kept = [];
|
|
647
|
+
for (const line of String(stdout).split("\n")) {
|
|
648
|
+
if (line.startsWith(CWD_SENTINEL)) {
|
|
649
|
+
const cwd = line.slice(CWD_SENTINEL.length);
|
|
650
|
+
if (cwd && existsSync(cwd)) this.agentCwd = cwd;
|
|
651
|
+
} else kept.push(line);
|
|
652
|
+
}
|
|
653
|
+
let output = kept.join("\n").replace(/^\n+|\n+$/g, "");
|
|
654
|
+
if (stderr) output = output ? `${output}\n${stderr}` : stderr;
|
|
655
|
+
return [code, output.replace(/\n+$/, "")];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
newSession() {
|
|
659
|
+
this.activeSession = newLocalSession();
|
|
660
|
+
this.sessionId = null;
|
|
661
|
+
this.parentMessageId = null;
|
|
662
|
+
this.agentPrimed = false;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async login() {
|
|
666
|
+
try {
|
|
667
|
+
await this.openUrl(LOGIN_URL);
|
|
668
|
+
this.ui.notice("Đã mở trang đăng nhập DeepSeek.");
|
|
669
|
+
} catch (error) {
|
|
670
|
+
this.ui.error(`Không mở được trang đăng nhập: ${error.message || error}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
logout() {
|
|
675
|
+
delete this.runtimeData.auth.token;
|
|
676
|
+
saveJson(this.runtimeData.authPath, this.runtimeData.auth);
|
|
677
|
+
this.token = "";
|
|
678
|
+
this.sessionId = null;
|
|
679
|
+
this.parentMessageId = null;
|
|
680
|
+
this.agentPrimed = false;
|
|
681
|
+
this.ui.notice("Đã đăng xuất khỏi CLI.");
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async resumeSession() {
|
|
685
|
+
const session = await this.ui.selectSession(this.savedSessions);
|
|
686
|
+
if (!session) { this.ui.clear(this.activeSession); return; }
|
|
687
|
+
this.activeSession = session;
|
|
688
|
+
this.sessionId = session.chat_session_id;
|
|
689
|
+
this.parentMessageId = session.parent_message_id;
|
|
690
|
+
this.agentPrimed = false;
|
|
691
|
+
this.ui.clear(this.activeSession);
|
|
692
|
+
this.ui.renderTranscript(session.messages || []);
|
|
693
|
+
this.ui.notice("Đã tiếp tục phiên đã chọn.");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async handleCommand(prompt) {
|
|
697
|
+
const command = prompt.toLowerCase();
|
|
698
|
+
if (["/exit", "exit", "quit", "q"].includes(command)) return false;
|
|
699
|
+
if (command === "/login") await this.login();
|
|
700
|
+
else if (command === "/logout") this.logout();
|
|
701
|
+
else if (command === "/help") { this.ui.clear(this.activeSession); this.ui.help(); }
|
|
702
|
+
else if (command === "/clear") this.ui.clear(this.activeSession);
|
|
703
|
+
else if (command === "/new") { this.newSession(); this.ui.clear(this.activeSession); this.ui.notice("Đã tạo phiên mới."); }
|
|
704
|
+
else if (command === "/resume") await this.resumeSession();
|
|
705
|
+
else if (command === "/agent") { this.agentEnabled = !this.agentEnabled; this.ui.notice(`Chế độ terminal: ${this.agentEnabled ? "bật" : "tắt"}`); }
|
|
706
|
+
else if (command.startsWith("/")) this.ui.error("Lệnh không hợp lệ. Dùng /help để xem danh sách lệnh.");
|
|
707
|
+
else return null;
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async sendPrompt(prompt) {
|
|
712
|
+
this.activeSession.messages.push({ role: "user", content: prompt, created_at: nowIso() });
|
|
713
|
+
if (this.activeSession.title === "Phiên mới") this.activeSession.title = sessionPreview({ title: prompt });
|
|
714
|
+
this.persistActiveSession();
|
|
715
|
+
let sendText = prompt;
|
|
716
|
+
if (this.agentEnabled && !this.agentPrimed) { sendText = `${AGENT_PREAMBLE}\n\n${prompt}`; this.agentPrimed = true; }
|
|
717
|
+
for (let step = 0; step < MAX_AGENT_STEPS; step += 1) {
|
|
718
|
+
let streamed = false;
|
|
719
|
+
this.ui.assistantStart();
|
|
720
|
+
const reply = await this.ask(sendText, (fragment) => { streamed = true; this.ui.stream(fragment); });
|
|
721
|
+
if (streamed) console.log(); else console.log(reply);
|
|
722
|
+
this.activeSession.messages.push({ role: "assistant", content: reply, created_at: nowIso() });
|
|
723
|
+
this.persistActiveSession();
|
|
724
|
+
if (!this.agentEnabled) return;
|
|
725
|
+
const commands = extractCommands(reply);
|
|
726
|
+
if (!commands.length) return;
|
|
727
|
+
let runAll = false;
|
|
728
|
+
let stopped = false;
|
|
729
|
+
const results = [];
|
|
730
|
+
for (const command of commands) {
|
|
731
|
+
const choice = runAll ? "run" : await this.ui.confirmCommand(command);
|
|
732
|
+
if (choice === "quit") { stopped = true; break; }
|
|
733
|
+
if (choice === "skip") { results.push([command, null, ""]); continue; }
|
|
734
|
+
if (choice === "all") runAll = true;
|
|
735
|
+
const [code, output] = await this.runCommand(command);
|
|
736
|
+
this.ui.showCommandResult(code, output);
|
|
737
|
+
results.push([command, code, output]);
|
|
738
|
+
}
|
|
739
|
+
if (stopped || !results.some(([, code]) => code !== null)) return;
|
|
740
|
+
sendText = formatCommandFeedback(results);
|
|
741
|
+
}
|
|
742
|
+
this.ui.notice(`Đã đạt giới hạn ${MAX_AGENT_STEPS} bước agent, dừng.`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async run() {
|
|
746
|
+
this.newSession();
|
|
747
|
+
this.ui.clear(this.activeSession);
|
|
748
|
+
if (this.persistenceError) this.ui.error(`Không thể dùng lịch sử: ${this.persistenceError}`);
|
|
749
|
+
const onSigint = () => {
|
|
750
|
+
this.interrupted = true;
|
|
751
|
+
this.abortController?.abort();
|
|
752
|
+
};
|
|
753
|
+
process.on("SIGINT", onSigint);
|
|
754
|
+
try {
|
|
755
|
+
while (true) {
|
|
756
|
+
try {
|
|
757
|
+
const prompt = await this.ui.readPrompt();
|
|
758
|
+
if (!prompt) continue;
|
|
759
|
+
const handled = await this.handleCommand(prompt);
|
|
760
|
+
if (handled === false) break;
|
|
761
|
+
if (handled === true) continue;
|
|
762
|
+
await this.sendPrompt(prompt);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
if (error instanceof UserInterrupted) break;
|
|
765
|
+
this.ui.error(`Lỗi: ${error.message || error}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
} finally {
|
|
769
|
+
process.off("SIGINT", onSigint);
|
|
770
|
+
console.log(`\n${DIM}Đã đóng DeepSeek CLI.${RESET}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
async function main(argv = process.argv.slice(2)) {
|
|
776
|
+
const promptIndex = argv.findIndex((value) => value === "-p" || value === "--prompt");
|
|
777
|
+
const cli = new DeepSeekCLI();
|
|
778
|
+
if (promptIndex >= 0) {
|
|
779
|
+
console.log(await cli.ask(argv[promptIndex + 1] || ""));
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
await cli.run();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
module.exports = { DATA_DIR, DeepSeekCLI, RUNTIME_DATA, SessionStore, bootstrapDataDirectory, extractCommands, formatCommandFeedback, main, matchingCommands, newLocalSession, parseSseEvent, sessionPickerLine };
|
|
786
|
+
|
|
787
|
+
if (require.main === module) {
|
|
788
|
+
main().catch((error) => {
|
|
789
|
+
process.stderr.write(`Lỗi: ${error.message || error}\n`);
|
|
790
|
+
process.exitCode = 1;
|
|
791
|
+
});
|
|
792
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
let cachedMemory = null;
|
|
5
|
+
let cachedDataView = null;
|
|
6
|
+
|
|
7
|
+
function uint8Memory(wasm) {
|
|
8
|
+
if (cachedMemory === null || cachedMemory.buffer !== wasm.memory.buffer) {
|
|
9
|
+
cachedMemory = new Uint8Array(wasm.memory.buffer);
|
|
10
|
+
}
|
|
11
|
+
return cachedMemory;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function dataView(wasm) {
|
|
15
|
+
if (cachedDataView === null || cachedDataView.buffer !== wasm.memory.buffer) {
|
|
16
|
+
cachedDataView = new DataView(wasm.memory.buffer);
|
|
17
|
+
}
|
|
18
|
+
return cachedDataView;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeString(wasm, value) {
|
|
22
|
+
const encoded = new TextEncoder().encode(value);
|
|
23
|
+
const pointer = wasm.__wbindgen_export_0(encoded.length, 1) >>> 0;
|
|
24
|
+
uint8Memory(wasm).set(encoded, pointer);
|
|
25
|
+
return { pointer, length: encoded.length };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
const challenge = JSON.parse(process.argv[2]);
|
|
30
|
+
if (challenge.algorithm !== "DeepSeekHashV1") {
|
|
31
|
+
throw new Error(`Unsupported PoW algorithm: ${challenge.algorithm}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const wasmPath = path.join(__dirname, "sha3_wasm_bg.7b9ca65ddd.wasm");
|
|
35
|
+
const { instance } = await WebAssembly.instantiate(fs.readFileSync(wasmPath), { wbg: {} });
|
|
36
|
+
const wasm = instance.exports;
|
|
37
|
+
const prefix = `${challenge.salt}_${challenge.expire_at}_`;
|
|
38
|
+
const stackPointer = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const challengeString = writeString(wasm, challenge.challenge);
|
|
42
|
+
const prefixString = writeString(wasm, prefix);
|
|
43
|
+
wasm.wasm_solve(
|
|
44
|
+
stackPointer,
|
|
45
|
+
challengeString.pointer,
|
|
46
|
+
challengeString.length,
|
|
47
|
+
prefixString.pointer,
|
|
48
|
+
prefixString.length,
|
|
49
|
+
challenge.difficulty,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const status = dataView(wasm).getInt32(stackPointer, true);
|
|
53
|
+
if (status === 0) {
|
|
54
|
+
throw new Error("PoW solver không tìm thấy đáp án");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const response = {
|
|
58
|
+
algorithm: challenge.algorithm,
|
|
59
|
+
challenge: challenge.challenge,
|
|
60
|
+
salt: challenge.salt,
|
|
61
|
+
answer: dataView(wasm).getFloat64(stackPointer + 8, true),
|
|
62
|
+
signature: challenge.signature,
|
|
63
|
+
target_path: challenge.target_path,
|
|
64
|
+
};
|
|
65
|
+
process.stdout.write(Buffer.from(JSON.stringify(response)).toString("base64"));
|
|
66
|
+
} finally {
|
|
67
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main().catch((error) => {
|
|
72
|
+
process.stderr.write(error.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rennii/deepseek-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "DeepSeek terminal client for Termux",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"deepseek": "deepseek-cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node deepseek-cli.js",
|
|
12
|
+
"check": "node --check deepseek-cli.js",
|
|
13
|
+
"test": "node --test test-deepseek-cli.js"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/rennii11/deepseek-cli.git"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("node:assert/strict");
|
|
4
|
+
const { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } = require("node:fs");
|
|
5
|
+
const { tmpdir } = require("node:os");
|
|
6
|
+
const { join } = require("node:path");
|
|
7
|
+
const test = require("node:test");
|
|
8
|
+
const {
|
|
9
|
+
SessionStore,
|
|
10
|
+
DeepSeekCLI,
|
|
11
|
+
extractCommands,
|
|
12
|
+
formatCommandFeedback,
|
|
13
|
+
bootstrapDataDirectory,
|
|
14
|
+
matchingCommands,
|
|
15
|
+
newLocalSession,
|
|
16
|
+
parseSseEvent,
|
|
17
|
+
sessionPickerLine,
|
|
18
|
+
} = require("./deepseek-cli");
|
|
19
|
+
|
|
20
|
+
test("filters slash commands by typed prefix", () => {
|
|
21
|
+
assert.deepEqual(matchingCommands("/res").map(([command]) => command), ["/resume"]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("login opens the official DeepSeek page", async () => {
|
|
25
|
+
const directory = mkdtempSync(join(tmpdir(), "deepseek-cli-"));
|
|
26
|
+
try {
|
|
27
|
+
const runtimeData = { config: {}, configPath: join(directory, "config.json"), auth: {}, authPath: join(directory, "auth.json") };
|
|
28
|
+
const cli = new DeepSeekCLI(new SessionStore(join(directory, "sessions"), []), runtimeData);
|
|
29
|
+
let openedUrl;
|
|
30
|
+
cli.openUrl = async (url) => { openedUrl = url; };
|
|
31
|
+
cli.ui = { notice() {}, error() {} };
|
|
32
|
+
await cli.login();
|
|
33
|
+
assert.equal(openedUrl, "https://chat.deepseek.com/");
|
|
34
|
+
} finally {
|
|
35
|
+
rmSync(directory, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("logout removes only the saved CLI auth token", () => {
|
|
40
|
+
const directory = mkdtempSync(join(tmpdir(), "deepseek-cli-"));
|
|
41
|
+
try {
|
|
42
|
+
const authPath = join(directory, "auth.json");
|
|
43
|
+
const runtimeData = { config: { setting: true }, configPath: join(directory, "config.json"), auth: { token: "stored-token" }, authPath };
|
|
44
|
+
const cli = new DeepSeekCLI(new SessionStore(join(directory, "sessions"), []), runtimeData);
|
|
45
|
+
cli.ui = { notice() {} };
|
|
46
|
+
cli.logout();
|
|
47
|
+
assert.equal(cli.token, "");
|
|
48
|
+
assert.deepEqual(JSON.parse(readFileSync(authPath, "utf8")), {});
|
|
49
|
+
} finally {
|
|
50
|
+
rmSync(directory, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("migrates a legacy config token into auth.json", () => {
|
|
55
|
+
const directory = mkdtempSync(join(tmpdir(), "deepseek-cli-"));
|
|
56
|
+
try {
|
|
57
|
+
const configPath = join(directory, "config.json");
|
|
58
|
+
writeFileSync(configPath, '{"token":"legacy-token","setting":true}\n');
|
|
59
|
+
const data = bootstrapDataDirectory({ dataDir: directory, legacySessionPath: join(directory, "legacy-sessions.json") });
|
|
60
|
+
assert.deepEqual(JSON.parse(readFileSync(data.authPath, "utf8")), { token: "legacy-token" });
|
|
61
|
+
assert.deepEqual(JSON.parse(readFileSync(data.configPath, "utf8")), { setting: true });
|
|
62
|
+
} finally {
|
|
63
|
+
rmSync(directory, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("extracts terminal commands and formats their output", () => {
|
|
68
|
+
assert.deepEqual(extractCommands("```bash\npwd\n```"), ["pwd"]);
|
|
69
|
+
assert.match(formatCommandFeedback([["pwd", 0, "/tmp"]]), /exit=0/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("persists a local session", () => {
|
|
73
|
+
const directory = mkdtempSync(join(tmpdir(), "deepseek-cli-"));
|
|
74
|
+
try {
|
|
75
|
+
const store = new SessionStore(join(directory, "sessions"), []);
|
|
76
|
+
const session = newLocalSession();
|
|
77
|
+
session.created_at = "2026-08-30T19:45:10.000Z";
|
|
78
|
+
session.title = "Kiểm tra phiên";
|
|
79
|
+
store.save([session]);
|
|
80
|
+
assert.equal(store.load()[0].title, "Kiểm tra phiên");
|
|
81
|
+
assert.match(store.sessionPath(session), /sessions\/2026\/08\/30\/session-2026-08-30T19-45-10-/);
|
|
82
|
+
} finally {
|
|
83
|
+
rmSync(directory, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("migrates flat session history into one file per dated session", () => {
|
|
88
|
+
const directory = mkdtempSync(join(tmpdir(), "deepseek-cli-"));
|
|
89
|
+
try {
|
|
90
|
+
const flatPath = join(directory, "sessions.json");
|
|
91
|
+
writeFileSync(flatPath, '[{"id":"legacy","created_at":"2026-08-30T19:45:10.000Z","updated_at":"2026-08-30T19:45:10.000Z"}]\n');
|
|
92
|
+
const store = new SessionStore(join(directory, "sessions"), [flatPath], [flatPath]);
|
|
93
|
+
const sessions = store.load();
|
|
94
|
+
const perSessionFile = store.sessionPath(sessions[0]);
|
|
95
|
+
assert.equal(sessions[0].id, "legacy");
|
|
96
|
+
assert.deepEqual(JSON.parse(readFileSync(perSessionFile, "utf8")).id, "legacy");
|
|
97
|
+
assert.ok(existsSync(`${flatPath}.migrated`));
|
|
98
|
+
} finally {
|
|
99
|
+
rmSync(directory, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("parses SSE response fragments and tracks parent message", () => {
|
|
104
|
+
const state = { parentMessageId: null, fragmentTypes: [], activeFragmentType: null };
|
|
105
|
+
const received = [];
|
|
106
|
+
parseSseEvent({ response_message_id: "parent", p: "response/content", v: "Xin chào" }, state, (fragment) => received.push(fragment));
|
|
107
|
+
assert.equal(state.parentMessageId, "parent");
|
|
108
|
+
assert.deepEqual(received, ["Xin chào"]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("session picker lines do not overflow the terminal", () => {
|
|
112
|
+
const session = newLocalSession();
|
|
113
|
+
session.title = "Chạy lệnh in ra đúng chữ DEEPSEEK_AGENT_OK rồi cho tôi biết";
|
|
114
|
+
session.updated_at = "2026-08-30T19:37:17+00:00";
|
|
115
|
+
const line = sessionPickerLine(session, 0, 0, 40).replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
|
|
116
|
+
assert.ok(line.length <= 40);
|
|
117
|
+
});
|