From 52731b9fa10c8212e8fb8de489771840fd6051c7 Mon Sep 17 00:00:00 2001 From: Acbox Date: Thu, 15 Jan 2026 20:26:21 +0800 Subject: [PATCH] docs: update --- .env.example | 4 + README.md | 62 ++-- packages/api/src/modules/container/service.ts | 2 +- packages/container/src/container.ts | 6 +- packages/container/src/containerd.ts | 290 ------------------ packages/container/src/index.ts | 1 - packages/container/src/nerdctl.ts | 2 +- packages/container/src/types.ts | 4 +- 8 files changed, 50 insertions(+), 321 deletions(-) delete mode 100644 packages/container/src/containerd.ts 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 [![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) 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; }