feat: cli

This commit is contained in:
Acbox
2026-01-11 02:08:55 +08:00
parent 9445680bb8
commit 0f28176fd4
23 changed files with 3032 additions and 24 deletions
+29
View File
@@ -0,0 +1,29 @@
import { treaty } from '@elysiajs/eden'
import { getApiUrl, getToken } from './config'
// 使用动态导入来避免类型错误
export function createClient() {
const apiUrl = getApiUrl()
const token = getToken()
// Eden Treaty 配置
const client = treaty(apiUrl, {
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return client as any
}
export function requireAuth(): string {
const token = getToken()
if (!token) {
throw new Error('未登录,请先使用 "memohome auth login" 命令登录')
}
return token
}
export { getApiUrl, getToken }
+223
View File
@@ -0,0 +1,223 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import { requireAuth, getApiUrl, getToken } from '../client'
export function agentCommands(program: Command) {
program
.command('chat <message>')
.description('与 AI Agent 对话')
.option('-t, --max-context-time <minutes>', '上下文加载时间(分钟)', '60')
.option('-l, --language <language>', '回复语言', 'Chinese')
.action(async (message, options) => {
try {
requireAuth()
const token = getToken()!
const apiUrl = getApiUrl()
console.log(chalk.blue('🤖 Agent: '))
const response = await fetch(`${apiUrl}/agent/stream`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
message,
maxContextLoadTime: parseInt(options.maxContextTime),
language: options.language,
}),
})
if (!response.ok) {
const errorData = await response.json() as { error?: string }
console.error(chalk.red('对话失败:'), errorData.error || '未知错误')
process.exit(1)
}
const reader = response.body?.getReader()
const decoder = new TextDecoder()
if (!reader) {
throw new Error('无法读取响应流')
}
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
buffer += chunk
// 按行处理
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim()
if (data === '[DONE]') {
console.log('\n')
return
}
try {
const event = JSON.parse(data)
if (event.type === 'text-delta' && event.text) {
process.stdout.write(event.text)
} else if (event.type === 'tool-call') {
console.log(chalk.dim(`\n[🔧 使用工具: ${event.toolName}]`))
} else if (event.type === 'error') {
console.error(chalk.red('\n❌ 错误:'), event.error)
}
} catch {
// 跳过无法解析的JSON
}
}
}
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('错误:'), message)
process.exit(1)
}
})
program
.command('interactive')
.alias('i')
.description('进入交互式对话模式')
.option('-t, --max-context-time <minutes>', '上下文加载时间(分钟)', '60')
.option('-l, --language <language>', '回复语言', 'Chinese')
.action(async (options) => {
try {
requireAuth()
const token = getToken()!
const apiUrl = getApiUrl()
console.log(chalk.green.bold('🤖 MemoHome Agent 交互模式'))
console.log(chalk.dim('输入 /exit 或 /quit 退出,输入 /help 查看帮助\n'))
const { createInterface } = await import('readline')
const rl = createInterface({
input: process.stdin,
output: process.stdout,
prompt: chalk.blue('You: '),
})
rl.prompt()
rl.on('line', async (line: string) => {
const input = line.trim()
if (input === '/exit' || input === '/quit') {
console.log(chalk.yellow('再见!👋'))
rl.close()
process.exit(0)
return
}
if (input === '/help') {
console.log(chalk.green('\n可用命令:'))
console.log(chalk.dim(' /exit, /quit - 退出交互模式'))
console.log(chalk.dim(' /help - 显示帮助信息\n'))
rl.prompt()
return
}
if (!input) {
rl.prompt()
return
}
try {
console.log(chalk.green('Agent: '))
const response = await fetch(`${apiUrl}/agent/stream`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: input,
maxContextLoadTime: parseInt(options.maxContextTime),
language: options.language,
}),
})
if (!response.ok) {
const errorData = await response.json() as { error?: string }
console.error(chalk.red('对话失败:'), errorData.error || '未知错误')
rl.prompt()
return
}
const reader = response.body?.getReader()
const decoder = new TextDecoder()
if (!reader) {
throw new Error('无法读取响应流')
}
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
buffer += chunk
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim()
if (data === '[DONE]') {
console.log('\n')
rl.prompt()
return
}
try {
const event = JSON.parse(data)
if (event.type === 'text-delta' && event.text) {
process.stdout.write(event.text)
} else if (event.type === 'tool-call') {
console.log(chalk.dim(`\n[🔧 ${event.toolName}]`))
} else if (event.type === 'error') {
console.error(chalk.red('\n❌'), event.error)
}
} catch {
// 跳过无法解析的JSON
}
}
}
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('错误:'), message)
rl.prompt()
}
})
rl.on('close', () => {
console.log(chalk.yellow('\n再见!👋'))
process.exit(0)
})
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('错误:'), message)
process.exit(1)
}
})
}
+143
View File
@@ -0,0 +1,143 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import inquirer from 'inquirer'
import ora from 'ora'
import { createClient } from '../client'
import { setToken, clearToken, getToken, getApiUrl, setApiUrl } from '../config'
import { formatError } from '../utils'
export function authCommands(program: Command) {
program
.command('login')
.description('登录到 MemoHome')
.option('-u, --username <username>', '用户名')
.option('-p, --password <password>', '密码')
.action(async (options) => {
try {
let username = options.username
let password = options.password
if (!username || !password) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'username',
message: '请输入用户名:',
when: !username,
},
{
type: 'password',
name: 'password',
message: '请输入密码:',
when: !password,
mask: '*',
},
])
username = username || answers.username
password = password || answers.password
}
const spinner = ora('正在登录...').start()
const client = createClient()
const response = await client.auth.login.post({
username,
password,
})
if (response.error) {
spinner.fail(chalk.red('登录失败'))
console.error(chalk.red(formatError(response.error.value)))
process.exit(1)
}
const data = response.data as { success?: boolean; data?: { token?: string; user?: { username: string; role: string } } } | null
if (data?.success && data?.data?.token && data?.data?.user) {
setToken(data.data.token)
spinner.succeed(chalk.green('登录成功!'))
console.log(chalk.blue(`用户: ${data.data.user.username}`))
console.log(chalk.blue(`角色: ${data.data.user.role}`))
} else {
spinner.fail(chalk.red('登录失败'))
console.error(chalk.red('无效的响应格式'))
process.exit(1)
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('登录错误:'), message)
process.exit(1)
}
})
program
.command('logout')
.description('登出当前用户')
.action(() => {
const token = getToken()
if (!token) {
console.log(chalk.yellow('当前未登录'))
return
}
clearToken()
console.log(chalk.green('✓ 已登出'))
})
program
.command('whoami')
.description('查看当前登录用户')
.action(async () => {
try {
const token = getToken()
if (!token) {
console.log(chalk.yellow('当前未登录'))
console.log(chalk.dim('使用 "memohome auth login" 登录'))
return
}
const spinner = ora('获取用户信息...').start()
const client = createClient()
const response = await client.auth.me.get()
if (response.error) {
spinner.fail(chalk.red('获取用户信息失败'))
console.error(chalk.red(formatError(response.error.value)))
process.exit(1)
}
const data = response.data as { success?: boolean; data?: { username: string; role: string; id: string } } | null
if (data?.success && data?.data) {
spinner.succeed(chalk.green('已登录'))
console.log(chalk.blue(`用户名: ${data.data.username}`))
console.log(chalk.blue(`角色: ${data.data.role}`))
console.log(chalk.blue(`用户ID: ${data.data.id}`))
} else {
spinner.fail(chalk.red('获取用户信息失败'))
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('错误:'), message)
process.exit(1)
}
})
program
.command('config')
.description('查看或设置 API 配置')
.option('-s, --set <url>', '设置 API URL')
.action((options) => {
if (options.set) {
const url = options.set
setApiUrl(url)
console.log(chalk.green(`✓ API URL 已设置为: ${url}`))
} else {
const apiUrl = getApiUrl()
const token = getToken()
console.log(chalk.blue('当前配置:'))
console.log(chalk.dim(`API URL: ${apiUrl}`))
console.log(chalk.dim(`已登录: ${token ? '是' : '否'}`))
}
})
}
+55
View File
@@ -0,0 +1,55 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import ora from 'ora'
import { getApiUrl, getToken } from '../client'
export function debugCommands(program: Command) {
program
.command('ping')
.description('测试 API 服务器连接')
.action(async () => {
const apiUrl = getApiUrl()
const token = getToken()
console.log(chalk.blue('连接信息:'))
console.log(chalk.dim(` API URL: ${apiUrl}`))
console.log(chalk.dim(` Token: ${token ? '已设置' : '未设置'}`))
console.log()
const spinner = ora('正在连接...').start()
try {
// 尝试直接 fetch
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000)
const response = await fetch(`${apiUrl}/`, {
signal: controller.signal,
headers: token ? {
'Authorization': `Bearer ${token}`
} : {}
})
clearTimeout(timeoutId)
if (response.ok) {
spinner.succeed(chalk.green('连接成功!'))
const text = await response.text()
console.log(chalk.dim('响应:'), text.substring(0, 100))
} else {
spinner.fail(chalk.red(`连接失败: HTTP ${response.status}`))
}
} catch (error) {
spinner.fail(chalk.red('连接失败'))
if (error instanceof Error) {
if (error.name === 'AbortError') {
console.error(chalk.yellow('连接超时 (5秒)'))
console.error(chalk.dim('请检查 API 服务器是否正在运行'))
} else {
console.error(chalk.red('错误:'), error.message)
}
}
}
})
}
+191
View File
@@ -0,0 +1,191 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import ora from 'ora'
import { table } from 'table'
import { createClient, requireAuth } from '../client'
export function memoryCommands(program: Command) {
program
.command('search <query>')
.description('搜索记忆')
.option('-l, --limit <limit>', '返回结果数量', '10')
.action(async (query, options) => {
try {
requireAuth()
const spinner = ora('搜索记忆...').start()
const client = createClient()
const response = await client.memory.search.get({
query: {
q: query,
limit: parseInt(options.limit),
},
})
if (response.error) {
spinner.fail(chalk.red('搜索失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
spinner.succeed(chalk.green(`找到 ${data.data.length} 条记忆`))
if (data.data.length === 0) {
console.log(chalk.yellow('未找到相关记忆'))
return
}
data.data.forEach((item: any, index: number) => {
console.log()
console.log(chalk.blue(`[${index + 1}] 相似度: ${(item.similarity * 100).toFixed(2)}%`))
console.log(chalk.dim(`时间: ${new Date(item.timestamp).toLocaleString('zh-CN')}`))
console.log(chalk.white(item.content))
})
}
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('add <content>')
.description('添加记忆')
.action(async (content) => {
try {
requireAuth()
const spinner = ora('添加记忆...').start()
const client = createClient()
const response = await client.memory.post({
content,
})
if (response.error) {
spinner.fail(chalk.red('添加记忆失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success) {
spinner.succeed(chalk.green('记忆已添加'))
}
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('messages')
.alias('msg')
.description('获取消息历史')
.option('-p, --page <page>', '页码', '1')
.option('-l, --limit <limit>', '每页数量', '20')
.action(async (options) => {
try {
requireAuth()
const spinner = ora('获取消息历史...').start()
const client = createClient()
const response = await client.memory.message.get({
query: {
page: parseInt(options.page),
limit: parseInt(options.limit),
},
})
if (response.error) {
spinner.fail(chalk.red('获取消息失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
const { messages, pagination } = data.data
spinner.succeed(chalk.green(`消息历史 (${pagination.page}/${pagination.totalPages} 页)`))
if (messages.length === 0) {
console.log(chalk.yellow('暂无消息'))
return
}
console.log(chalk.dim(`\n总计: ${pagination.total} 条消息\n`))
messages.forEach((msg: any) => {
const roleColor = msg.role === 'user' ? chalk.blue : chalk.green
const roleIcon = msg.role === 'user' ? '👤' : '🤖'
console.log(roleColor(`${roleIcon} ${msg.role.toUpperCase()}`))
console.log(chalk.dim(new Date(msg.timestamp).toLocaleString('zh-CN')))
console.log(chalk.white(msg.content))
console.log()
})
}
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('filter')
.description('按日期范围过滤消息')
.option('-s, --start <date>', '开始日期 (ISO 8601)')
.option('-e, --end <date>', '结束日期 (ISO 8601)')
.action(async (options) => {
try {
requireAuth()
if (!options.start || !options.end) {
console.error(chalk.red('请提供开始和结束日期'))
console.log(chalk.dim('示例: memohome memory filter -s 2024-01-01T00:00:00Z -e 2024-12-31T23:59:59Z'))
process.exit(1)
}
const spinner = ora('过滤消息...').start()
const client = createClient()
const response = await client.memory.message.filter.get({
query: {
startDate: options.start,
endDate: options.end,
},
})
if (response.error) {
spinner.fail(chalk.red('过滤消息失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
spinner.succeed(chalk.green(`找到 ${data.data.length} 条消息`))
if (data.data.length === 0) {
console.log(chalk.yellow('未找到消息'))
return
}
console.log()
data.data.forEach((msg: any) => {
const roleColor = msg.role === 'user' ? chalk.blue : chalk.green
const roleIcon = msg.role === 'user' ? '👤' : '🤖'
console.log(roleColor(`${roleIcon} ${msg.role.toUpperCase()}`))
console.log(chalk.dim(new Date(msg.timestamp).toLocaleString('zh-CN')))
console.log(chalk.white(msg.content))
console.log()
})
}
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
}
+400
View File
@@ -0,0 +1,400 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import inquirer from 'inquirer'
import ora from 'ora'
import { table } from 'table'
import { createClient, requireAuth } from '../client'
import type { ApiResponse, Model } from '../types'
import { formatError } from '../utils'
export function modelCommands(program: Command) {
program
.command('list')
.description('列出所有模型配置')
.action(async () => {
const spinner = ora('获取模型列表...').start()
try {
requireAuth()
const client = createClient()
const response = await client.model.get()
if (response.error) {
spinner.fail(chalk.red('获取模型列表失败'))
console.error(chalk.red(formatError(response.error.value)))
process.exit(1)
}
// API 返回格式: { success, items, pagination }
const data = response.data as { success?: boolean; items?: Model[]; pagination?: unknown } | null
if (data?.success && data?.items) {
spinner.succeed(chalk.green('模型列表'))
const models = data.items
if (models.length === 0) {
console.log(chalk.yellow('暂无模型配置'))
return
}
const tableData = [
['ID', '名称', '模型ID', '类型', '客户端'],
...models.map((item: unknown) => {
const modelItem = item as { id: string; model: Model }
return [
modelItem.id.substring(0, 8) + '...',
modelItem.model.name || '-',
modelItem.model.modelId,
modelItem.model.type === 'embedding' ? chalk.yellow('embedding') : chalk.blue('chat'),
modelItem.model.clientType,
]
}),
]
console.log(table(tableData))
}
} catch (error) {
spinner.fail(chalk.red('操作失败'))
if (error instanceof Error) {
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
const { getApiUrl: getUrl } = await import('../config')
console.error(chalk.red('连接超时,请检查:'))
console.error(chalk.yellow(' 1. API 服务器是否正在运行'))
console.error(chalk.yellow(' 2. API 地址是否正确'))
console.error(chalk.dim(` 当前配置: ${getUrl()}`))
} else {
console.error(chalk.red('错误:'), error.message)
}
} else {
console.error(chalk.red('错误:'), String(error))
}
process.exit(1)
}
})
program
.command('create')
.description('创建模型配置')
.option('-n, --name <name>', '模型名称')
.option('-m, --model-id <modelId>', '模型ID')
.option('-u, --base-url <baseUrl>', 'API Base URL')
.option('-k, --api-key <apiKey>', 'API Key')
.option('-c, --client-type <clientType>', '客户端类型 (openai/anthropic/google)')
.option('-t, --type <type>', '模型类型 (chat/embedding)', 'chat')
.option('-d, --dimensions <dimensions>', 'Embedding 维度 (仅 embedding 类型需要)')
.action(async (options) => {
const spinner = ora('创建模型配置...').start()
try {
requireAuth()
let { name, modelId, baseUrl, apiKey, clientType, type, dimensions } = options
if (!name || !modelId || !baseUrl || !apiKey || !clientType) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: '模型名称:',
when: !name,
},
{
type: 'input',
name: 'modelId',
message: '模型ID (如 gpt-4 或 text-embedding-3-small):',
when: !modelId,
},
{
type: 'input',
name: 'baseUrl',
message: 'API Base URL:',
default: 'https://api.openai.com/v1',
when: !baseUrl,
},
{
type: 'password',
name: 'apiKey',
message: 'API Key:',
when: !apiKey,
mask: '*',
},
{
type: 'list',
name: 'clientType',
message: '客户端类型:',
choices: ['openai', 'anthropic', 'google'],
default: 'openai',
when: !clientType,
},
{
type: 'list',
name: 'type',
message: '模型类型:',
choices: ['chat', 'embedding'],
default: 'chat',
when: !type,
},
])
name = name || answers.name
modelId = modelId || answers.modelId
baseUrl = baseUrl || answers.baseUrl
apiKey = apiKey || answers.apiKey
clientType = clientType || answers.clientType
type = type || answers.type
}
// 如果是 embedding 类型,需要 dimensions
if (type === 'embedding' && !dimensions) {
const answer = await inquirer.prompt([
{
type: 'number',
name: 'dimensions',
message: 'Embedding 维度 (如 1536):',
validate: (value: number) => {
if (value > 0) return true
return '维度必须是正整数'
},
},
])
dimensions = answer.dimensions
}
spinner.text = '创建模型配置...'
const client = createClient()
const payload: Record<string, unknown> = {
name,
modelId,
baseUrl,
apiKey,
clientType,
type,
}
// 如果是 embedding 类型,添加 dimensions
if (type === 'embedding') {
if (!dimensions) {
console.error(chalk.red('Embedding 模型需要指定 dimensions'))
process.exit(1)
}
payload.dimensions = typeof dimensions === 'number' ? dimensions : parseInt(dimensions)
}
const response = await client.model.post(payload)
if (response.error) {
spinner.fail(chalk.red('创建模型配置失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as ApiResponse<Model> | null
if (data?.success && data?.data) {
spinner.succeed(chalk.green('模型配置创建成功'))
console.log(chalk.blue(`名称: ${data.data.name}`))
console.log(chalk.blue(`模型ID: ${data.data.modelId}`))
console.log(chalk.blue(`类型: ${data.data.type || 'chat'}`))
if (data.data.type === 'embedding' && data.data.dimensions) {
console.log(chalk.blue(`维度: ${data.data.dimensions}`))
}
console.log(chalk.blue(`ID: ${data.data.id}`))
}
} catch (error) {
spinner.fail(chalk.red('操作失败'))
if (error instanceof Error) {
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
const { getApiUrl: getUrl } = await import('../config')
console.error(chalk.red('连接超时,请检查:'))
console.error(chalk.yellow(' 1. API 服务器是否正在运行'))
console.error(chalk.yellow(' 2. API 地址是否正确'))
console.error(chalk.dim(` 当前配置: ${getUrl()}`))
} else {
console.error(chalk.red('错误:'), error.message)
}
} else {
console.error(chalk.red('错误:'), String(error))
}
process.exit(1)
}
})
program
.command('delete <id>')
.description('删除模型配置')
.action(async (id) => {
let spinner: ReturnType<typeof ora> | undefined
try {
requireAuth()
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: chalk.yellow(`确定要删除模型配置 ${id} 吗?`),
default: false,
},
])
if (!confirm) {
console.log(chalk.yellow('已取消'))
return
}
spinner = ora('删除模型配置...').start()
const client = createClient()
const response = await client.model({ id }).delete()
if (response.error) {
spinner.fail(chalk.red('删除模型配置失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
if (spinner) spinner.succeed(chalk.green('模型配置已删除'))
} catch (error) {
if (spinner) spinner.fail(chalk.red('操作失败'))
if (error instanceof Error) {
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
const { getApiUrl: getUrl } = await import('../config')
console.error(chalk.red('连接超时,请检查:'))
console.error(chalk.yellow(' 1. API 服务器是否正在运行'))
console.error(chalk.yellow(' 2. API 地址是否正确'))
console.error(chalk.dim(` 当前配置: ${getUrl()}`))
} else {
console.error(chalk.red('错误:'), error.message)
}
} else {
console.error(chalk.red('错误:'), String(error))
}
process.exit(1)
}
})
program
.command('get <id>')
.description('获取模型配置详情')
.action(async (id) => {
const spinner = ora('获取模型配置...').start()
try {
requireAuth()
const client = createClient()
const response = await client.model({ id }).get()
if (response.error) {
spinner.fail(chalk.red('获取模型配置失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as ApiResponse<Model> | null
if (data?.success && data?.data) {
const model = data.data
spinner.succeed(chalk.green('模型配置'))
console.log(chalk.blue(`ID: ${model.id}`))
console.log(chalk.blue(`名称: ${model.name}`))
console.log(chalk.blue(`模型ID: ${model.modelId}`))
console.log(chalk.blue(`类型: ${model.type || 'chat'}`))
if (model.type === 'embedding' && model.dimensions) {
console.log(chalk.blue(`维度: ${model.dimensions}`))
}
console.log(chalk.blue(`Base URL: ${model.baseUrl}`))
console.log(chalk.blue(`客户端类型: ${model.clientType}`))
console.log(chalk.blue(`创建时间: ${new Date(model.createdAt).toLocaleString('zh-CN')}`))
}
} catch (error) {
spinner.fail(chalk.red('操作失败'))
if (error instanceof Error) {
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
const { getApiUrl: getUrl } = await import('../config')
console.error(chalk.red('连接超时,请检查:'))
console.error(chalk.yellow(' 1. API 服务器是否正在运行'))
console.error(chalk.yellow(' 2. API 地址是否正确'))
console.error(chalk.dim(` 当前配置: ${getUrl()}`))
} else {
console.error(chalk.red('错误:'), error.message)
}
} else {
console.error(chalk.red('错误:'), String(error))
}
process.exit(1)
}
})
program
.command('defaults')
.description('查看默认模型配置')
.action(async () => {
const spinner = ora('获取默认模型配置...').start()
try {
requireAuth()
const client = createClient()
const [chatRes, summaryRes, embeddingRes] = await Promise.all([
client.model.chat.default.get(),
client.model.summary.default.get(),
client.model.embedding.default.get(),
])
spinner.stop()
console.log(chalk.green.bold('默认模型配置:'))
console.log()
// Chat Model
const chatData = chatRes.data as ApiResponse<Model> | null
if (chatData?.success && chatData.data) {
const model = chatData.data
console.log(chalk.blue('💬 聊天模型:'))
console.log(chalk.dim(` 名称: ${model.name}`))
console.log(chalk.dim(` 模型ID: ${model.modelId}`))
console.log(chalk.dim(` ID: ${model.id}`))
} else {
console.log(chalk.yellow('💬 聊天模型: 未配置'))
}
console.log()
// Summary Model
const summaryData = summaryRes.data as ApiResponse<Model> | null
if (summaryData?.success && summaryData.data) {
const model = summaryData.data
console.log(chalk.blue('📝 摘要模型:'))
console.log(chalk.dim(` 名称: ${model.name}`))
console.log(chalk.dim(` 模型ID: ${model.modelId}`))
console.log(chalk.dim(` ID: ${model.id}`))
} else {
console.log(chalk.yellow('📝 摘要模型: 未配置'))
}
console.log()
// Embedding Model
const embeddingData = embeddingRes.data as ApiResponse<Model> | null
if (embeddingData?.success && embeddingData.data) {
const model = embeddingData.data
console.log(chalk.blue('🔍 嵌入模型:'))
console.log(chalk.dim(` 名称: ${model.name}`))
console.log(chalk.dim(` 模型ID: ${model.modelId}`))
console.log(chalk.dim(` ID: ${model.id}`))
} else {
console.log(chalk.yellow('🔍 嵌入模型: 未配置'))
}
} catch (error) {
spinner.fail(chalk.red('操作失败'))
if (error instanceof Error) {
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
const { getApiUrl: getUrl } = await import('../config')
console.error(chalk.red('连接超时,请检查:'))
console.error(chalk.yellow(' 1. API 服务器是否正在运行'))
console.error(chalk.yellow(' 2. API 地址是否正确'))
console.error(chalk.dim(` 当前配置: ${getUrl()}`))
} else {
console.error(chalk.red('错误:'), error.message)
}
} else {
console.error(chalk.red('错误:'), String(error))
}
process.exit(1)
}
})
}
+305
View File
@@ -0,0 +1,305 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import inquirer from 'inquirer'
import ora from 'ora'
import { table } from 'table'
import { createClient, requireAuth } from '../client'
export function scheduleCommands(program: Command) {
program
.command('list')
.description('列出所有定时任务')
.action(async () => {
try {
requireAuth()
const spinner = ora('获取定时任务列表...').start()
const client = createClient()
const response = await client.schedule.get()
if (response.error) {
spinner.fail(chalk.red('获取定时任务列表失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
spinner.succeed(chalk.green('定时任务列表'))
const schedules = data.data
if (schedules.length === 0) {
console.log(chalk.yellow('暂无定时任务'))
return
}
const tableData = [
['ID', '标题', 'Cron', '启用', '创建时间'],
...schedules.map((schedule: any) => [
schedule.id.substring(0, 8) + '...',
schedule.title,
schedule.cronExpression,
schedule.enabled ? chalk.green('是') : chalk.red('否'),
new Date(schedule.createdAt).toLocaleString('zh-CN'),
]),
]
console.log(table(tableData))
}
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('create')
.description('创建定时任务')
.option('-t, --title <title>', '任务标题')
.option('-d, --description <description>', '任务描述')
.option('-c, --cron <expression>', 'Cron 表达式')
.option('-e, --enabled', '启用任务', false)
.action(async (options) => {
try {
requireAuth()
let { title, description, cron, enabled } = options
if (!title || !cron) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'title',
message: '任务标题:',
when: !title,
},
{
type: 'input',
name: 'description',
message: '任务描述 (可选):',
when: !description,
},
{
type: 'input',
name: 'cron',
message: 'Cron 表达式 (如: 0 9 * * *):',
when: !cron,
},
{
type: 'confirm',
name: 'enabled',
message: '启用任务?',
default: false,
when: enabled === undefined,
},
])
title = title || answers.title
description = description || answers.description
cron = cron || answers.cron
enabled = enabled !== undefined ? enabled : answers.enabled
}
const spinner = ora('创建定时任务...').start()
const client = createClient()
const payload: any = {
title,
cronExpression: cron,
enabled,
}
if (description) {
payload.description = description
}
const response = await client.schedule.post(payload)
if (response.error) {
spinner.fail(chalk.red('创建定时任务失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
spinner.succeed(chalk.green('定时任务创建成功'))
console.log(chalk.blue(`标题: ${data.data.title}`))
console.log(chalk.blue(`Cron: ${data.data.cronExpression}`))
console.log(chalk.blue(`ID: ${data.data.id}`))
}
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('get <id>')
.description('获取定时任务详情')
.action(async (id) => {
try {
requireAuth()
const spinner = ora('获取定时任务详情...').start()
const client = createClient()
const response = await client.schedule({ id }).get()
if (response.error) {
spinner.fail(chalk.red('获取定时任务失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
const schedule = data.data
spinner.succeed(chalk.green('定时任务详情'))
console.log(chalk.blue(`ID: ${schedule.id}`))
console.log(chalk.blue(`标题: ${schedule.title}`))
if (schedule.description) {
console.log(chalk.blue(`描述: ${schedule.description}`))
}
console.log(chalk.blue(`Cron: ${schedule.cronExpression}`))
console.log(
chalk.blue(`启用: ${schedule.enabled ? chalk.green('是') : chalk.red('否')}`)
)
console.log(
chalk.blue(`创建时间: ${new Date(schedule.createdAt).toLocaleString('zh-CN')}`)
)
console.log(
chalk.blue(`更新时间: ${new Date(schedule.updatedAt).toLocaleString('zh-CN')}`)
)
}
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('update <id>')
.description('更新定时任务')
.option('-t, --title <title>', '任务标题')
.option('-d, --description <description>', '任务描述')
.option('-c, --cron <expression>', 'Cron 表达式')
.option('-e, --enabled <boolean>', '启用任务 (true/false)')
.action(async (id, options) => {
try {
requireAuth()
const updates: any = {}
if (options.title) updates.title = options.title
if (options.description) updates.description = options.description
if (options.cron) updates.cronExpression = options.cron
if (options.enabled !== undefined) {
updates.enabled = options.enabled === 'true' || options.enabled === true
}
if (Object.keys(updates).length === 0) {
console.log(chalk.yellow('未提供任何更新参数'))
return
}
const spinner = ora('更新定时任务...').start()
const client = createClient()
const response = await client.schedule({ id }).put(updates)
if (response.error) {
spinner.fail(chalk.red('更新定时任务失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
spinner.succeed(chalk.green('定时任务已更新'))
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('delete <id>')
.description('删除定时任务')
.action(async (id) => {
try {
requireAuth()
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: chalk.yellow(`确定要删除定时任务 ${id} 吗?`),
default: false,
},
])
if (!confirm) {
console.log(chalk.yellow('已取消'))
return
}
const spinner = ora('删除定时任务...').start()
const client = createClient()
const response = await client.schedule({ id }).delete()
if (response.error) {
spinner.fail(chalk.red('删除定时任务失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
spinner.succeed(chalk.green('定时任务已删除'))
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('toggle <id>')
.description('切换定时任务启用状态')
.action(async (id) => {
try {
requireAuth()
const spinner = ora('切换任务状态...').start()
const client = createClient()
// 首先获取当前状态
const getResponse = await client.schedule({ id }).get()
if (getResponse.error) {
spinner.fail(chalk.red('获取任务失败'))
console.error(chalk.red(getResponse.error.value))
process.exit(1)
}
const getData = getResponse.data as any
if (getData?.success && getData?.data) {
const currentEnabled = getData.data.enabled
// 更新状态
const updateResponse = await client.schedule({ id }).put({
enabled: !currentEnabled,
})
if (updateResponse.error) {
spinner.fail(chalk.red('更新任务失败'))
console.error(chalk.red(updateResponse.error.value))
process.exit(1)
}
spinner.succeed(
chalk.green(`任务已${!currentEnabled ? '启用' : '禁用'}`)
)
}
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
}
+181
View File
@@ -0,0 +1,181 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import inquirer from 'inquirer'
import ora from 'ora'
import { createClient, requireAuth } from '../client'
export function settingsCommands(program: Command) {
program
.command('get')
.description('获取当前用户设置')
.action(async () => {
try {
requireAuth()
const spinner = ora('获取设置...').start()
const client = createClient()
const response = await client.settings.get()
if (response.error) {
spinner.fail(chalk.red('获取设置失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
const settings = data.data
spinner.succeed(chalk.green('当前设置'))
console.log()
console.log(chalk.blue('🎯 Agent 配置:'))
console.log(chalk.dim(` 语言: ${settings.language || '未设置'}`))
console.log(chalk.dim(` 上下文加载时间: ${settings.maxContextLoadTime || '未设置'} 分钟`))
console.log()
console.log(chalk.blue('🤖 默认模型:'))
console.log(chalk.dim(` 聊天模型ID: ${settings.defaultChatModel || '未设置'}`))
console.log(chalk.dim(` 摘要模型ID: ${settings.defaultSummaryModel || '未设置'}`))
console.log(chalk.dim(` 嵌入模型ID: ${settings.defaultEmbeddingModel || '未设置'}`))
console.log()
console.log(chalk.blue('📊 其他:'))
console.log(chalk.dim(` 用户ID: ${settings.userId}`))
console.log(chalk.dim(` 创建时间: ${new Date(settings.createdAt).toLocaleString('zh-CN')}`))
console.log(chalk.dim(` 更新时间: ${new Date(settings.updatedAt).toLocaleString('zh-CN')}`))
}
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('set')
.description('更新用户设置')
.option('--language <language>', '首选语言')
.option('--max-context-time <minutes>', '上下文加载时间(分钟)')
.option('--chat-model <id>', '默认聊天模型ID')
.option('--summary-model <id>', '默认摘要模型ID')
.option('--embedding-model <id>', '默认嵌入模型ID')
.action(async (options) => {
try {
requireAuth()
const updates: any = {}
if (options.language) updates.language = options.language
if (options.maxContextTime)
updates.maxContextLoadTime = parseInt(options.maxContextTime)
if (options.chatModel) updates.defaultChatModel = options.chatModel
if (options.summaryModel) updates.defaultSummaryModel = options.summaryModel
if (options.embeddingModel)
updates.defaultEmbeddingModel = options.embeddingModel
if (Object.keys(updates).length === 0) {
console.log(chalk.yellow('未提供任何更新参数'))
console.log(chalk.dim('\n可用选项:'))
console.log(chalk.dim(' --language <language>'))
console.log(chalk.dim(' --max-context-time <minutes>'))
console.log(chalk.dim(' --chat-model <id>'))
console.log(chalk.dim(' --summary-model <id>'))
console.log(chalk.dim(' --embedding-model <id>'))
return
}
const spinner = ora('更新设置...').start()
const client = createClient()
const response = await client.settings.put(updates)
if (response.error) {
spinner.fail(chalk.red('更新设置失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success) {
spinner.succeed(chalk.green('设置已更新'))
console.log()
console.log(chalk.blue('更新的设置:'))
Object.entries(updates).forEach(([key, value]) => {
console.log(chalk.dim(` ${key}: ${value}`))
})
}
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('setup')
.description('交互式设置向导')
.action(async () => {
try {
requireAuth()
console.log(chalk.green.bold('\n🎨 设置向导\n'))
const answers = await inquirer.prompt([
{
type: 'input',
name: 'language',
message: '首选语言:',
default: 'Chinese',
},
{
type: 'number',
name: 'maxContextLoadTime',
message: '上下文加载时间(分钟):',
default: 60,
validate: (value) => {
const num = parseInt(value)
if (num < 1 || num > 1440) {
return '请输入 1-1440 之间的数字'
}
return true
},
},
{
type: 'input',
name: 'defaultChatModel',
message: '默认聊天模型ID (留空跳过):',
},
{
type: 'input',
name: 'defaultSummaryModel',
message: '默认摘要模型ID (留空跳过):',
},
{
type: 'input',
name: 'defaultEmbeddingModel',
message: '默认嵌入模型ID (留空跳过):',
},
])
// 过滤掉空值
const updates: any = {}
Object.entries(answers).forEach(([key, value]) => {
if (value) {
updates[key] = value
}
})
const spinner = ora('保存设置...').start()
const client = createClient()
const response = await client.settings.put(updates)
if (response.error) {
spinner.fail(chalk.red('保存设置失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
spinner.succeed(chalk.green('设置已保存'))
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
}
+238
View File
@@ -0,0 +1,238 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import inquirer from 'inquirer'
import ora from 'ora'
import { table } from 'table'
import { createClient, requireAuth } from '../client'
export function userCommands(program: Command) {
program
.command('list')
.description('列出所有用户 (需要管理员权限)')
.action(async () => {
try {
requireAuth()
const spinner = ora('获取用户列表...').start()
const client = createClient()
const response = await client.user.get()
if (response.error) {
spinner.fail(chalk.red('获取用户列表失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
spinner.succeed(chalk.green('用户列表'))
const users = data.data
if (users.length === 0) {
console.log(chalk.yellow('暂无用户'))
return
}
const tableData = [
['ID', '用户名', '角色', '创建时间'],
...users.map((user: any) => [
user.id,
user.username,
user.role === 'admin' ? chalk.red('管理员') : chalk.blue('用户'),
new Date(user.createdAt).toLocaleString('zh-CN'),
]),
]
console.log(table(tableData))
}
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('create')
.description('创建新用户 (需要管理员权限)')
.option('-u, --username <username>', '用户名')
.option('-p, --password <password>', '密码')
.option('-r, --role <role>', '角色 (user/admin)', 'user')
.action(async (options) => {
try {
requireAuth()
let username = options.username
let password = options.password
let role = options.role
if (!username || !password) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'username',
message: '用户名:',
when: !username,
},
{
type: 'password',
name: 'password',
message: '密码:',
when: !password,
mask: '*',
},
{
type: 'list',
name: 'role',
message: '角色:',
choices: ['user', 'admin'],
default: 'user',
when: !role,
},
])
username = username || answers.username
password = password || answers.password
role = role || answers.role
}
const spinner = ora('创建用户...').start()
const client = createClient()
const response = await client.user.post({
username,
password,
role,
})
if (response.error) {
spinner.fail(chalk.red('创建用户失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
spinner.succeed(chalk.green('用户创建成功'))
console.log(chalk.blue(`用户名: ${data.data.username}`))
console.log(chalk.blue(`角色: ${data.data.role}`))
console.log(chalk.blue(`ID: ${data.data.id}`))
}
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('delete <id>')
.description('删除用户 (需要管理员权限)')
.action(async (id) => {
try {
requireAuth()
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: chalk.yellow(`确定要删除用户 ${id} 吗?`),
default: false,
},
])
if (!confirm) {
console.log(chalk.yellow('已取消'))
return
}
const spinner = ora('删除用户...').start()
const client = createClient()
const response = await client.user({ id }).delete()
if (response.error) {
spinner.fail(chalk.red('删除用户失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
spinner.succeed(chalk.green('用户已删除'))
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('get <id>')
.description('获取用户详情')
.action(async (id) => {
try {
requireAuth()
const spinner = ora('获取用户信息...').start()
const client = createClient()
const response = await client.user({ id }).get()
if (response.error) {
spinner.fail(chalk.red('获取用户信息失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
const user = data.data
spinner.succeed(chalk.green('用户信息'))
console.log(chalk.blue(`ID: ${user.id}`))
console.log(chalk.blue(`用户名: ${user.username}`))
console.log(chalk.blue(`角色: ${user.role}`))
console.log(chalk.blue(`创建时间: ${new Date(user.createdAt).toLocaleString('zh-CN')}`))
}
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
program
.command('update-password <id>')
.description('更新用户密码 (需要管理员权限)')
.option('-p, --password <password>', '新密码')
.action(async (id, options) => {
try {
requireAuth()
let password = options.password
if (!password) {
const answers = await inquirer.prompt([
{
type: 'password',
name: 'password',
message: '新密码:',
mask: '*',
},
])
password = answers.password
}
const spinner = ora('更新密码...').start()
const client = createClient()
const response = await client.user({ id }).password.patch({
password,
})
if (response.error) {
spinner.fail(chalk.red('更新密码失败'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
spinner.succeed(chalk.green('密码已更新'))
} catch (error: any) {
console.error(chalk.red('错误:'), error.message)
process.exit(1)
}
})
}
+72
View File
@@ -0,0 +1,72 @@
import { homedir } from 'os'
import { join } from 'path'
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
const CONFIG_DIR = join(homedir(), '.memohome')
const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
export interface Config {
apiUrl: string
token?: string
}
const DEFAULT_CONFIG: Config = {
apiUrl: process.env.API_BASE_URL || 'http://localhost:7002',
}
export function ensureConfigDir() {
if (!existsSync(CONFIG_DIR)) {
mkdirSync(CONFIG_DIR, { recursive: true })
}
}
export function loadConfig(): Config {
ensureConfigDir()
if (!existsSync(CONFIG_FILE)) {
saveConfig(DEFAULT_CONFIG)
return DEFAULT_CONFIG
}
try {
const data = readFileSync(CONFIG_FILE, 'utf-8')
return { ...DEFAULT_CONFIG, ...JSON.parse(data) }
} catch {
console.error('Failed to load config, using defaults')
return DEFAULT_CONFIG
}
}
export function saveConfig(config: Config) {
ensureConfigDir()
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
}
export function getToken(): string | null {
const config = loadConfig()
return config.token || null
}
export function setToken(token: string) {
const config = loadConfig()
config.token = token
saveConfig(config)
}
export function clearToken() {
const config = loadConfig()
delete config.token
saveConfig(config)
}
export function getApiUrl(): string {
const config = loadConfig()
return config.apiUrl
}
export function setApiUrl(url: string) {
const config = loadConfig()
config.apiUrl = url
saveConfig(config)
}
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bun
import { Command } from 'commander'
import chalk from 'chalk'
import { authCommands } from './commands/auth'
import { userCommands } from './commands/user'
import { modelCommands } from './commands/model'
import { agentCommands } from './commands/agent'
import { memoryCommands } from './commands/memory'
import { settingsCommands } from './commands/settings'
import { scheduleCommands } from './commands/schedule'
import { debugCommands } from './commands/debug'
const program = new Command()
program
.name('memohome')
.description(chalk.bold.blue('🏠 MemoHome CLI - 智能记忆管理助手'))
.version('1.0.0')
// 认证命令
const auth = program.command('auth').description('用户认证管理')
authCommands(auth)
// 用户管理命令
const user = program.command('user').description('用户管理 (需要管理员权限)')
userCommands(user)
// 模型管理命令
const model = program.command('model').description('AI 模型配置管理')
modelCommands(model)
// Agent 对话命令
const agent = program.command('agent').description('与 AI Agent 对话')
agentCommands(agent)
// 记忆管理命令
const memory = program.command('memory').description('记忆管理')
memoryCommands(memory)
// 设置管理命令
const settings = program.command('settings').description('用户设置管理')
settingsCommands(settings)
// 日程管理命令
const schedule = program.command('schedule').description('日程管理')
scheduleCommands(schedule)
// 调试命令
const debug = program.command('debug').description('调试工具')
debugCommands(debug)
program.parse()
+71
View File
@@ -0,0 +1,71 @@
// API 响应类型定义
export interface ApiResponse<T = unknown> {
success?: boolean
data?: T
error?: string
}
export interface User {
id: string
username: string
role: string
createdAt: string
}
export interface Model {
id: string
name: string
modelId: string
baseUrl: string
apiKey?: string
clientType: string
type?: 'chat' | 'embedding'
dimensions?: number
createdAt: string
updatedAt?: string
}
export interface Memory {
content: string
timestamp: string
similarity?: number
}
export interface Message {
role: 'user' | 'assistant'
content: string
timestamp: string
}
export interface MessageListResponse {
messages: Message[]
pagination: {
page: number
limit: number
total: number
totalPages: number
}
}
export interface Settings {
userId: string
language?: string
maxContextLoadTime?: number
defaultChatModel?: string
defaultSummaryModel?: string
defaultEmbeddingModel?: string
createdAt: string
updatedAt: string
}
export interface Schedule {
id: string
title: string
description?: string
cronExpression: string
enabled: boolean
createdAt: string
updatedAt: string
}
+63
View File
@@ -0,0 +1,63 @@
import chalk from 'chalk'
/**
* API
*/
export function formatError(error: unknown): string {
if (error === null || error === undefined) {
return '未知错误'
}
if (typeof error === 'string') {
return error
}
if (typeof error === 'object') {
// 尝试提取常见的错误字段
const errorObj = error as Record<string, unknown>
if ('message' in errorObj && typeof errorObj.message === 'string') {
return errorObj.message
}
if ('error' in errorObj && typeof errorObj.error === 'string') {
return errorObj.error
}
// 如果有 status 和 statusText
if ('status' in errorObj && 'statusText' in errorObj) {
return `${errorObj.status} ${errorObj.statusText}`
}
// 否则返回格式化的 JSON
try {
return JSON.stringify(error, null, 2)
} catch {
return String(error)
}
}
return String(error)
}
/**
* 退
*/
export function exitWithError(message: string, error?: unknown): never {
console.error(chalk.red(message))
if (error) {
console.error(chalk.red(formatError(error)))
}
process.exit(1)
}
/**
* Eden Treaty
*/
export function handleApiError(response: { error?: { value: unknown } }, defaultMessage = '操作失败'): never {
if (response.error) {
exitWithError(defaultMessage, response.error.value)
}
exitWithError(defaultMessage, '未知错误')
}