diff --git a/.env.example b/.env.example
index 090d590f..020338d3 100644
--- a/.env.example
+++ b/.env.example
@@ -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
diff --git a/README.md b/README.md
index 9f3e8cc2..e9d3ce68 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,7 @@ Memoh是一个专属于你的AI私人管家,你可以把它跑在你的NAS,
- Bun 1.2+
- PNPM
- Qdrant
+- Redis
```bash
cp .env.example .env
@@ -43,12 +44,17 @@ pnpm install
```
Environment Variables
+
- `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 路径
+
### 数据库初始化
@@ -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
```
- `--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": "" }
-
-然后你就可以通过Telegram Bot与Memoh进行交互了。
-
## Star History
[](https://www.star-history.com/#memohai/Memoh&type=date&legend=top-left)
diff --git a/packages/api/src/modules/container/service.ts b/packages/api/src/modules/container/service.ts
index 7f6d847a..1d75da4d 100644
--- a/packages/api/src/modules/container/service.ts
+++ b/packages/api/src/modules/container/service.ts
@@ -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',
})
// 在数据库中记录
diff --git a/packages/container/src/container.ts b/packages/container/src/container.ts
index afc551fe..e478640e 100644
--- a/packages/container/src/container.ts
+++ b/packages/container/src/container.ts
@@ -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 {
// 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,
diff --git a/packages/container/src/containerd.ts b/packages/container/src/containerd.ts
deleted file mode 100644
index 654f686b..00000000
--- a/packages/container/src/containerd.ts
+++ /dev/null
@@ -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 {
- await this.exec(['image', 'pull', image])
- }
-
- /**
- * Create a new container
- */
- async createContainer(config: ContainerConfig): Promise {
- 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 {
- await this.exec(['task', 'start', '--detach', name])
- }
-
- /**
- * Stop a container
- */
- async stopContainer(name: string, timeout: number = 10): Promise {
- 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 {
- await this.exec(['task', 'pause', name])
- }
-
- /**
- * Resume a paused container
- */
- async resumeContainer(name: string): Promise {
- await this.exec(['task', 'resume', name])
- }
-
- /**
- * Remove a container
- */
- async removeContainer(name: string, force: boolean = false): Promise {
- 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 {
- 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 {
- 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 {
- 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 {
- 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 {
- try {
- await this.getContainerInfo(name)
- return true
- } catch {
- return false
- }
- }
-}
-
diff --git a/packages/container/src/index.ts b/packages/container/src/index.ts
index 35535eb7..28582a29 100644
--- a/packages/container/src/index.ts
+++ b/packages/container/src/index.ts
@@ -12,7 +12,6 @@ export {
} from './container'
// Export clients
-export { ContainerdClient } from './containerd'
export { NerdctlClient } from './nerdctl'
// Export types
diff --git a/packages/container/src/nerdctl.ts b/packages/container/src/nerdctl.ts
index b50e711f..b08ac9ed 100644
--- a/packages/container/src/nerdctl.ts
+++ b/packages/container/src/nerdctl.ts
@@ -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)
}
diff --git a/packages/container/src/types.ts b/packages/container/src/types.ts
index 4601013b..2e29b65f 100644
--- a/packages/container/src/types.ts
+++ b/packages/container/src/types.ts
@@ -132,7 +132,7 @@ export interface ContainerdOptions {
namespace?: string;
/** Timeout for operations (ms) */
timeout?: number;
- /** ctr command */
- ctrCommand?: string;
+ /** nerdctl command */
+ nerdctlCommand?: string;
}