docs: update

This commit is contained in:
Acbox
2026-01-15 20:26:21 +08:00
parent 9ee8b19475
commit 52731b9fa1
8 changed files with 50 additions and 321 deletions
+4
View File
@@ -50,6 +50,10 @@ NODE_ENV=development
REDIS_URL=redis://example
CONTAINER_DATA_DIR=
NERDCTL_COMMAND=nerdctl
CONTAINERD_SOCKET=
# Enable debug logging (optional)
# DEBUG=true
+39 -23
View File
@@ -36,6 +36,7 @@ Memoh是一个专属于你的AI私人管家,你可以把它跑在你的NAS,
- Bun 1.2+
- PNPM
- Qdrant
- Redis
```bash
cp .env.example .env
@@ -43,12 +44,17 @@ pnpm install
```
<details><summary>Environment Variables</summary>
- `DATABASE_URL`: PostgreSQL 连接字符串
- `ROOT_USER`: 超级管理员用户名
- `ROOT_USER_PASSWORD`: 超级管理员密码
- `JWT_SECRET`: JWT 签名密钥
- `QDRANT_URL`: Qdrant 连接字符串
- `REDIS_URL`: Redis 连接字符串
- `CONTAINER_DATA_DIR`: Container 数据目录
- `CONTAINERD_SOCKET`: Containerd Socket 路径
- `NERDCTL_COMMAND`: Nerdctl Command 路径
</details>
### 数据库初始化
@@ -65,6 +71,39 @@ pnpm run api:dev
API服务将在 `http://localhost:7002` 启动。
### Containerd 设置
Containerd 是容器管理的核心组件,Memoh 使用 Nerdctl 作为其容器管理工具。
你需要确保 Containerd 已经安装并运行。
然后设置一个目录用于存储容器数据,这个目录需要是绝对路径。
```env
CONTAINER_DATA_DIR=/Users/yourname/memoh/container
```
#### MacOS下使用Lima虚拟机运行
Containerd不支持MacOS的本地运行,你需要使用Lima虚拟机运行。
```bash
brew install lima
limactl start template://default
```
然后你需要设置环境变量,将 `nerdctl` 命令的路径设置为 `lima nerdctl`
```env
NERDCTL_COMMAND=lima nerdctl
```
可能会出现sock文件找不到的报错,你需要正确找出socket文件的路径,并设置环境变量。
```env
CONTAINERD_SOCKET=/Users/yourname/.lima/default/sock/containerd/containerd.sock
```
### 命令行工具
首先你需要登录:
@@ -111,29 +150,6 @@ pnpm cli config set --max-context-time <minutes>
```
- `--max-context-time`: 最大上下文加载时间,单位为分钟
## Telegram Bot
你需要获取你的Telegram Bot Token 然后启动Telegram Service
```bash
pnpm telegram:start
```
Telegram Service将在 `http://localhost:7101` 启动,这个是endpoint,你需要在Memoh中配置你的Telegram Bot Token
使用Memoh Cli:
```bash
pnpm cli platform create
```
根据提示配置platform
- name: telegram
- endpoint: http://localhost:7101
- config: { "botToken": "<your-telegram-bot-token>" }
然后你就可以通过Telegram Bot与Memoh进行交互了。
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=memohai/Memoh&type=date&legend=top-left)](https://www.star-history.com/#memohai/Memoh&type=date&legend=top-left)
@@ -90,7 +90,7 @@ export const createUserContainer = async (
// 在 containerd 中创建容器
const containerInfo = await createContainer(config, {
namespace,
ctrCommand: process.env.CTR_COMMAND || 'ctr',
nerdctlCommand: process.env.NERDCTL_COMMAND || 'nerdctl',
})
// 在数据库中记录
+3 -3
View File
@@ -161,11 +161,11 @@ export function useContainer(
/**
* Get container stats
* Note: This is a placeholder implementation
* Real implementation would require parsing ctr metrics
*/
* Real implementation would require parsing nerdctl metrics
*/
async stats(): Promise<ContainerStats> {
// This is a simplified implementation
// Full implementation would require parsing ctr metrics output
// Full implementation would require parsing nerdctl metrics output
return {
cpuUsage: 0,
memoryUsage: 0,
-290
View File
@@ -1,290 +0,0 @@
/**
* Containerd client implementation using ctr CLI
*/
import { execa } from 'execa'
import type { ContainerConfig, ContainerInfo, ContainerStatus, ContainerdOptions } from './types'
/**
* Containerd client for managing containers
*/
export class ContainerdClient {
private namespace: string
private socket?: string
private timeout: number
private ctrCommand: string
constructor(options: ContainerdOptions = {}) {
this.namespace = options.namespace || 'default'
this.socket = options.socket || process.env.CONTAINERD_SOCKET
this.timeout = options.timeout || 30000
this.ctrCommand = options.ctrCommand || process.env.CTR_COMMAND || 'ctr'
}
buildExecCommand(name: string, command: string[]): string[] {
return this.buildCtrCommand(['task', 'exec', '--exec-id', `exec-${Date.now()}`, name, ...command])
}
/**
* Build ctr command with options
*/
buildCtrCommand(args: string[]): string[] {
// Split ctrCommand and filter out empty strings (supports "lima sudo ctr")
const cmd = this.ctrCommand.split(' ').filter(part => part.length > 0)
if (this.socket) {
cmd.push('--address', this.socket)
}
cmd.push('--namespace', this.namespace)
cmd.push(...args)
return cmd
}
/**
* Execute ctr command
*/
private async exec(args: string[]): Promise<{ stdout: string; stderr: string }> {
const cmd = this.buildCtrCommand(args)
const [program, ...programArgs] = cmd
try {
const result = await execa(program, programArgs, {
timeout: this.timeout,
})
return {
stdout: result.stdout,
stderr: result.stderr,
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
throw new Error(`Containerd command failed: ${message}`)
}
}
/**
* Pull container image
*/
async pullImage(image: string): Promise<void> {
await this.exec(['image', 'pull', image])
}
/**
* Create a new container
*/
async createContainer(config: ContainerConfig): Promise<ContainerInfo> {
const args = ['container', 'create']
// Add mounts if specified
if (config.mounts && config.mounts.length > 0) {
for (const mount of config.mounts) {
// ctr uses 'src' and 'dst' instead of 'source' and 'target'
const mountStr = `type=${mount.type},src=${mount.source},dst=${mount.target}${mount.readonly ? ',readonly' : ''}`
args.push('--mount', mountStr)
}
}
// Add image
args.push(config.image)
// Add container name
args.push(config.name)
// Add command if specified
if (config.command && config.command.length > 0) {
args.push(...config.command)
}
await this.exec(args)
// Return container info
return this.getContainerInfo(config.name)
}
/**
* Start a container
*/
async startContainer(name: string): Promise<void> {
await this.exec(['task', 'start', '--detach', name])
}
/**
* Stop a container
*/
async stopContainer(name: string, timeout: number = 10): Promise<void> {
try {
await this.exec(['task', 'kill', '--signal', 'SIGTERM', name])
// Wait for graceful shutdown
await new Promise(resolve => setTimeout(resolve, timeout * 1000))
// Force kill if still running
try {
await this.exec(['task', 'kill', '--signal', 'SIGKILL', name])
} catch {
// Container might have already stopped
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : ''
if (!message.includes('not found')) {
throw error
}
}
}
/**
* Pause a container
*/
async pauseContainer(name: string): Promise<void> {
await this.exec(['task', 'pause', name])
}
/**
* Resume a paused container
*/
async resumeContainer(name: string): Promise<void> {
await this.exec(['task', 'resume', name])
}
/**
* Remove a container
*/
async removeContainer(name: string, force: boolean = false): Promise<void> {
if (force) {
// Try to stop the task first
try {
await this.exec(['task', 'kill', '--signal', 'SIGKILL', name])
await this.exec(['task', 'delete', name])
} catch {
// Task might not exist
}
}
await this.exec(['container', 'delete', name])
}
/**
* Execute command in container
*/
async execInContainer(name: string, command: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const args = this.buildExecCommand(name, command)
try {
const result = await this.exec(args)
return {
stdout: result.stdout,
stderr: result.stderr,
exitCode: 0,
}
} catch (error: unknown) {
const err = error as { stdout?: string; stderr?: string; exitCode?: number; message?: string }
return {
stdout: err.stdout || '',
stderr: err.stderr || err.message || '',
exitCode: err.exitCode || 1,
}
}
}
/**
* Get container information
*/
async getContainerInfo(name: string): Promise<ContainerInfo> {
const result = await this.exec(['container', 'info', name])
try {
const info = JSON.parse(result.stdout)
return {
id: info.ID || name,
name: name,
image: info.Image || '',
status: await this.getContainerStatus(name),
namespace: this.namespace,
createdAt: info.CreatedAt ? new Date(info.CreatedAt) : new Date(),
labels: info.Labels || {},
}
} catch {
// Fallback if JSON parsing fails
return {
id: name,
name: name,
image: '',
status: 'unknown',
namespace: this.namespace,
createdAt: new Date(),
}
}
}
/**
* Get container status
*/
async getContainerStatus(name: string): Promise<ContainerStatus> {
try {
const result = await this.exec(['task', 'list'])
const lines = result.stdout.split('\n')
for (const line of lines) {
if (line.includes(name)) {
if (line.includes('RUNNING')) return 'running'
if (line.includes('PAUSED')) return 'paused'
if (line.includes('STOPPED')) return 'stopped'
}
}
// Container exists but no task
return 'created'
} catch {
return 'unknown'
}
}
/**
* Get container logs
*/
async getContainerLogs(name: string): Promise<string> {
try {
const result = await this.exec(['task', 'logs', name])
return result.stdout
} catch (error: unknown) {
return error instanceof Error ? error.message : ''
}
}
/**
* List all containers
*/
async listContainers(): Promise<ContainerInfo[]> {
const result = await this.exec(['container', 'list', '--quiet'])
const containerNames = result.stdout.split('\n').filter(name => name.trim())
const containers: ContainerInfo[] = []
for (const name of containerNames) {
try {
const info = await this.getContainerInfo(name)
containers.push(info)
} catch {
// Skip containers that can't be accessed
}
}
return containers
}
/**
* Check if container exists
*/
async containerExists(name: string): Promise<boolean> {
try {
await this.getContainerInfo(name)
return true
} catch {
return false
}
}
}
-1
View File
@@ -12,7 +12,6 @@ export {
} from './container'
// Export clients
export { ContainerdClient } from './containerd'
export { NerdctlClient } from './nerdctl'
// Export types
+1 -1
View File
@@ -20,7 +20,7 @@ export class NerdctlClient {
this.socket = options.socket || process.env.CONTAINERD_SOCKET
this.timeout = options.timeout || 30000
// Support commands like "lima nerdctl"
const rawCommand = options.ctrCommand || process.env.CTR_COMMAND || 'nerdctl'
const rawCommand = options.nerdctlCommand || process.env.NERDCTL_COMMAND || 'nerdctl'
this.nerdctlCommand = rawCommand.split(' ').filter(part => part.length > 0)
}
+2 -2
View File
@@ -132,7 +132,7 @@ export interface ContainerdOptions {
namespace?: string;
/** Timeout for operations (ms) */
timeout?: number;
/** ctr command */
ctrCommand?: string;
/** nerdctl command */
nerdctlCommand?: string;
}