205 lines
5.5 KiB
JavaScript
Executable File
205 lines
5.5 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
// DeepSeek CLI (Node.js version)
|
||
// Commands:
|
||
// ds k <key> 设置/替换 API Key
|
||
// ds s <prompt> 设置 system prompt
|
||
// ds v 查看当前 prompt
|
||
// ds c <text> 使用 deepseek-chat(流式)
|
||
// ds r <text> 使用 deepseek-reasoner(流式,含思考)
|
||
|
||
import fs from 'fs';
|
||
import os from 'os';
|
||
import path from 'path';
|
||
import { TextDecoder } from 'util';
|
||
|
||
const CONFIG_DIR = path.join(os.homedir(), '.config', 'deepseek');
|
||
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
||
const DEFAULT_PROMPT = '你是Deepseek人工智能助手';
|
||
|
||
function ensureConfig() {
|
||
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||
if (!fs.existsSync(CONFIG_FILE)) {
|
||
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ api_key: '', system_prompt: DEFAULT_PROMPT }, null, 2), 'utf8');
|
||
fs.chmodSync(CONFIG_FILE, 0o600);
|
||
}
|
||
}
|
||
|
||
function readConfig() {
|
||
ensureConfig();
|
||
try {
|
||
const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||
const data = JSON.parse(raw);
|
||
return {
|
||
apiKey: data.api_key || '',
|
||
prompt: data.system_prompt || DEFAULT_PROMPT,
|
||
};
|
||
} catch {
|
||
return { apiKey: '', prompt: DEFAULT_PROMPT };
|
||
}
|
||
}
|
||
|
||
function saveConfig(apiKey, prompt) {
|
||
ensureConfig();
|
||
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ api_key: apiKey, system_prompt: prompt }, null, 2), 'utf8');
|
||
fs.chmodSync(CONFIG_FILE, 0o600);
|
||
}
|
||
|
||
function usage() {
|
||
console.log(`用法:
|
||
ds k <key> 设置/替换 API 密钥
|
||
ds s <prompt> 设置系统提示词
|
||
ds v 查看当前提示词
|
||
ds c <text> 使用 deepseek-chat(流式)
|
||
ds r <text> 使用 deepseek-reasoner(流式,含思考)
|
||
环境优先级: 命令行 > 环境变量 DEEPSEEK_API_KEY > 配置文件
|
||
默认模型: deepseek-reasoner`);
|
||
process.exit(1);
|
||
}
|
||
|
||
const args = process.argv.slice(2);
|
||
if (args.length === 0 || ['-h', '--help'].includes(args[0])) usage();
|
||
const cmd = args.shift();
|
||
|
||
// ANSI blue for **bold**
|
||
const BLUE = '\x1b[34m';
|
||
const RESET = '\x1b[0m';
|
||
let inBold = false;
|
||
function renderChunk(text) {
|
||
let out = '';
|
||
for (let i = 0; i < text.length; i++) {
|
||
if (text[i] === '*' && text[i + 1] === '*') {
|
||
inBold = !inBold;
|
||
out += inBold ? BLUE : RESET;
|
||
i++;
|
||
continue;
|
||
}
|
||
out += text[i];
|
||
}
|
||
return out;
|
||
}
|
||
|
||
async function streamChat({ model, apiKey, prompt, userInput }) {
|
||
const body = {
|
||
model,
|
||
messages: [
|
||
{ role: 'system', content: prompt },
|
||
{ role: 'user', content: userInput },
|
||
],
|
||
stream: true,
|
||
};
|
||
|
||
const res = await fetch('https://api.deepseek.com/chat/completions', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
Authorization: `Bearer ${apiKey}`,
|
||
},
|
||
body: JSON.stringify(body),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const t = await res.text();
|
||
console.error(`请求失败 ${res.status}: ${t}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
const decoder = new TextDecoder();
|
||
let buffer = '';
|
||
let sawReason = false;
|
||
let sawContent = false;
|
||
const enableReason = model === 'deepseek-reasoner';
|
||
|
||
process.stdout.write(`发送中 (模型: ${model})...\n`);
|
||
|
||
for await (const chunk of res.body) {
|
||
buffer += decoder.decode(chunk, { stream: true });
|
||
let lines = buffer.split(/\r?\n/);
|
||
buffer = lines.pop(); // keep incomplete line
|
||
for (const line of lines) {
|
||
if (!line.startsWith('data: ')) continue;
|
||
const data = line.slice(6);
|
||
if (data === '[DONE]') {
|
||
process.stdout.write('\n');
|
||
return;
|
||
}
|
||
let delta;
|
||
try {
|
||
delta = JSON.parse(data)?.choices?.[0]?.delta || {};
|
||
} catch {
|
||
continue;
|
||
}
|
||
const rc = delta.reasoning_content || '';
|
||
const ct = delta.content || '';
|
||
|
||
if (enableReason && rc) {
|
||
if (!sawReason) {
|
||
process.stdout.write('思考:\n');
|
||
sawReason = true;
|
||
}
|
||
process.stdout.write(renderChunk(rc));
|
||
}
|
||
|
||
if (ct) {
|
||
if (!sawContent) {
|
||
if (sawReason) process.stdout.write('\n\n');
|
||
process.stdout.write('回复:\n');
|
||
sawContent = true;
|
||
}
|
||
process.stdout.write(renderChunk(ct));
|
||
}
|
||
}
|
||
}
|
||
process.stdout.write('\n');
|
||
}
|
||
|
||
(async () => {
|
||
switch (cmd) {
|
||
case 'k': {
|
||
if (args.length < 1) {
|
||
console.error('请输入密钥');
|
||
process.exit(1);
|
||
}
|
||
const cfg = readConfig();
|
||
saveConfig(args[0], cfg.prompt);
|
||
console.log(`API 密钥已保存到 ${CONFIG_FILE}`);
|
||
break;
|
||
}
|
||
case 's': {
|
||
if (args.length < 1) {
|
||
console.error('请输入新的 system prompt');
|
||
process.exit(1);
|
||
}
|
||
const cfg = readConfig();
|
||
const prompt = args.join(' ');
|
||
saveConfig(cfg.apiKey, prompt);
|
||
console.log('system prompt 已更新。');
|
||
break;
|
||
}
|
||
case 'v': {
|
||
const cfg = readConfig();
|
||
console.log('当前 system prompt:');
|
||
console.log(cfg.prompt);
|
||
break;
|
||
}
|
||
case 'c':
|
||
case 'r': {
|
||
if (args.length < 1) {
|
||
console.error('请输入要发送的内容');
|
||
process.exit(1);
|
||
}
|
||
const userInput = args.join(' ');
|
||
const cfg = readConfig();
|
||
const apiKey = process.env.DEEPSEEK_API_KEY || cfg.apiKey;
|
||
if (!apiKey) {
|
||
console.error('未设置 API 密钥,请先运行: ds k <key> 或设置环境变量 DEEPSEEK_API_KEY');
|
||
process.exit(1);
|
||
}
|
||
const model = cmd === 'c' ? 'deepseek-chat' : 'deepseek-reasoner';
|
||
await streamChat({ model, apiKey, prompt: cfg.prompt, userInput });
|
||
break;
|
||
}
|
||
default:
|
||
usage();
|
||
}
|
||
})();
|