mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
docs: update
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
[](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',
|
||||
})
|
||||
|
||||
// 在数据库中记录
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ export {
|
||||
} from './container'
|
||||
|
||||
// Export clients
|
||||
export { ContainerdClient } from './containerd'
|
||||
export { NerdctlClient } from './nerdctl'
|
||||
|
||||
// Export types
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ export interface ContainerdOptions {
|
||||
namespace?: string;
|
||||
/** Timeout for operations (ms) */
|
||||
timeout?: number;
|
||||
/** ctr command */
|
||||
ctrCommand?: string;
|
||||
/** nerdctl command */
|
||||
nerdctlCommand?: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user