From 60d7d5a59807440074d1b33b0ecbf8df68c83ef5 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sun, 4 Jan 2026 12:10:06 +0800 Subject: [PATCH] refactor: modularize ds cli --- bin/ds | 207 ++-------------------------------------- bin/ds-lib/api.js | 90 +++++++++++++++++ bin/ds-lib/commands.js | 56 +++++++++++ bin/ds-lib/config.js | 53 ++++++++++ bin/ds-lib/main.js | 48 ++++++++++ bin/ds-lib/output.js | 31 ++++++ bin/ds-lib/package.json | 3 + 7 files changed, 291 insertions(+), 197 deletions(-) create mode 100644 bin/ds-lib/api.js create mode 100644 bin/ds-lib/commands.js create mode 100644 bin/ds-lib/config.js create mode 100644 bin/ds-lib/main.js create mode 100644 bin/ds-lib/output.js create mode 100644 bin/ds-lib/package.json diff --git a/bin/ds b/bin/ds index 5b16a5a..1581a4e 100755 --- a/bin/ds +++ b/bin/ds @@ -1,204 +1,17 @@ #!/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(流式,含思考) +// DeepSeek CLI 入口:将解析和命令逻辑委托给模块化实现,便于后续扩展。 +const path = require('path'); +const { pathToFileURL } = require('url'); -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(); +async function bootstrap() { + const entry = path.join(__dirname, 'ds-lib', 'main.js'); 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}`); + const mod = await import(pathToFileURL(entry).href); + await mod.main(process.argv.slice(2)); + } catch (err) { + console.error('程序运行失败:', err?.message || err); 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(); - } -})(); +bootstrap(); diff --git a/bin/ds-lib/api.js b/bin/ds-lib/api.js new file mode 100644 index 0000000..c1c2002 --- /dev/null +++ b/bin/ds-lib/api.js @@ -0,0 +1,90 @@ +import { TextDecoder } from 'util'; +import { renderChunk } from './output.js'; + +const API_URL = 'https://api.deepseek.com/chat/completions'; + +export 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(API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + const t = await res.text().catch(() => ''); + throw new Error(`请求失败 ${res.status}: ${t}`); + } + + process.stdout.write(`发送中 (模型: ${model})...\n`); + + const decoder = new TextDecoder(); + const reader = res.body.getReader(); + let buffer = ''; + let sawReason = false; + let sawContent = false; + const enableReason = model === 'deepseek-reasoner'; + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const 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)); + } + } + } + // flush possible remaining buffered text (unlikely but safe) + if (buffer.trim()) { + try { + const delta = JSON.parse(buffer)?.choices?.[0]?.delta || {}; + const ct = delta.content || ''; + if (ct) process.stdout.write(ct); + } catch { + // ignore malformed tail + } + } + process.stdout.write('\n'); +} diff --git a/bin/ds-lib/commands.js b/bin/ds-lib/commands.js new file mode 100644 index 0000000..e7542bb --- /dev/null +++ b/bin/ds-lib/commands.js @@ -0,0 +1,56 @@ +import { readConfig, saveConfig, resolveApiKey, DEFAULT_PROMPT } from './config.js'; +import { streamChat } from './api.js'; +import { printInfo, printError } from './output.js'; + +export async function setKey(args) { + if (args.length < 1) { + printError('请输入密钥'); + process.exit(1); + } + const cfg = readConfig(); + saveConfig({ apiKey: args[0], prompt: cfg.prompt }); + printInfo('API 密钥已保存。'); +} + +export async function setPrompt(args) { + if (args.length < 1) { + printError('请输入新的 system prompt'); + process.exit(1); + } + const cfg = readConfig(); + const prompt = args.join(' '); + saveConfig({ apiKey: cfg.apiKey, prompt }); + printInfo('system prompt 已更新。'); +} + +export function viewPrompt() { + const cfg = readConfig(); + printInfo('当前 system prompt:'); + printInfo(cfg.prompt || DEFAULT_PROMPT); +} + +async function runChat(model, args) { + if (args.length < 1) { + printError('请输入要发送的内容'); + process.exit(1); + } + const userInput = args.join(' '); + const cfg = readConfig(); + const { apiKey, source } = resolveApiKey(cfg); + if (!apiKey) { + printError('未设置 API 密钥,请先运行: ds k 或设置环境变量 DEEPSEEK_API_KEY'); + process.exit(1); + } + if (source === 'env') { + printInfo('使用环境变量 DEEPSEEK_API_KEY'); + } + await streamChat({ model, apiKey, prompt: cfg.prompt, userInput }); +} + +export async function chat(args) { + await runChat('deepseek-chat', args); +} + +export async function reason(args) { + await runChat('deepseek-reasoner', args); +} diff --git a/bin/ds-lib/config.js b/bin/ds-lib/config.js new file mode 100644 index 0000000..b4fe582 --- /dev/null +++ b/bin/ds-lib/config.js @@ -0,0 +1,53 @@ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +export const CONFIG_DIR = path.join(os.homedir(), '.config', 'deepseek'); +export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json'); +export const DEFAULT_PROMPT = '你是Deepseek人工智能助手'; + +function createDefaultConfig() { + return { api_key: '', system_prompt: DEFAULT_PROMPT }; +} + +export function ensureConfigFile() { + if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + } + if (!fs.existsSync(CONFIG_FILE)) { + fs.writeFileSync(CONFIG_FILE, JSON.stringify(createDefaultConfig(), null, 2), 'utf8'); + fs.chmodSync(CONFIG_FILE, 0o600); + } +} + +export function readConfig() { + ensureConfigFile(); + 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 }; + } +} + +export function saveConfig({ apiKey, prompt }) { + ensureConfigFile(); + const data = { api_key: apiKey ?? '', system_prompt: prompt ?? DEFAULT_PROMPT }; + fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8'); + fs.chmodSync(CONFIG_FILE, 0o600); +} + +export function resolveApiKey(config) { + const envKey = process.env.DEEPSEEK_API_KEY; + if (envKey && envKey.trim()) { + return { apiKey: envKey.trim(), source: 'env' }; + } + if (config.apiKey && config.apiKey.trim()) { + return { apiKey: config.apiKey.trim(), source: 'file' }; + } + return { apiKey: '', source: 'none' }; +} diff --git a/bin/ds-lib/main.js b/bin/ds-lib/main.js new file mode 100644 index 0000000..c925997 --- /dev/null +++ b/bin/ds-lib/main.js @@ -0,0 +1,48 @@ +import { chat, reason, setKey, setPrompt, viewPrompt } from './commands.js'; +import { printInfo } from './output.js'; + +function usage(exit = true) { + printInfo(`用法: + ds k 设置/替换 API 密钥 + ds s 设置系统提示词 + ds v 查看当前提示词 + ds c 使用 deepseek-chat(流式) + ds r 使用 deepseek-reasoner(流式,含思考) +环境优先级: 命令行 > 环境变量 DEEPSEEK_API_KEY > 配置文件 +默认模型: deepseek-reasoner`); + if (exit) process.exit(1); +} + +export async function main(argv) { + if (!argv || argv.length === 0 || ['-h', '--help'].includes(argv[0])) { + usage(false); + return; + } + + const [cmd, ...rest] = argv; + + switch (cmd) { + case 'k': + await setKey(rest); + break; + case 's': + await setPrompt(rest); + break; + case 'v': + viewPrompt(); + break; + case 'c': + await chat(rest); + break; + case 'r': + await reason(rest); + break; + default: + usage(); + } +} + +// 若作为独立模块直接调用 +if (process.argv[1] === new URL(import.meta.url).pathname) { + main(process.argv.slice(2)); +} diff --git a/bin/ds-lib/output.js b/bin/ds-lib/output.js new file mode 100644 index 0000000..871b9ac --- /dev/null +++ b/bin/ds-lib/output.js @@ -0,0 +1,31 @@ +const BLUE = '\x1b[34m'; +const RESET = '\x1b[0m'; + +let inBold = false; + +// Render **bold** blocks using ANSI blue, keep other characters unchanged. +export 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; +} + +export function printHeading(label) { + process.stdout.write(`${label}\n`); +} + +export function printError(msg) { + console.error(msg); +} + +export function printInfo(msg) { + console.log(msg); +} diff --git a/bin/ds-lib/package.json b/bin/ds-lib/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/bin/ds-lib/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +}