commit ea960a523b3d285d9313c7a6e5cc93ede14d4dcf Author: JOJO <1498581755@qq.com> Date: Sun Jan 4 00:57:31 2026 +0800 feat: add ds CLI for DeepSeek with streaming and markdown diff --git a/bin/ds b/bin/ds new file mode 100755 index 0000000..5b16a5a --- /dev/null +++ b/bin/ds @@ -0,0 +1,204 @@ +#!/usr/bin/env node +// DeepSeek CLI (Node.js version) +// Commands: +// ds k 设置/替换 API Key +// ds s 设置 system prompt +// ds v 查看当前 prompt +// ds c 使用 deepseek-chat(流式) +// ds r 使用 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 设置/替换 API 密钥 + ds s 设置系统提示词 + ds v 查看当前提示词 + ds c 使用 deepseek-chat(流式) + ds r 使用 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 或设置环境变量 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(); + } +})();