mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
fix(deploy): many docker compose bug
This commit is contained in:
+1
-1
@@ -127,7 +127,7 @@ host = "postgres"
|
|||||||
password = "your_secure_password" # Must change in config.toml
|
password = "your_secure_password" # Must change in config.toml
|
||||||
|
|
||||||
[containerd]
|
[containerd]
|
||||||
socket_path = "unix:///var/run/docker.sock" # Use host Docker
|
socket_path = "/run/containerd/containerd.sock"
|
||||||
|
|
||||||
[qdrant]
|
[qdrant]
|
||||||
base_url = "http://qdrant:6334"
|
base_url = "http://qdrant:6334"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { parse } from 'toml'
|
|||||||
type AgentGatewayConfig = {
|
type AgentGatewayConfig = {
|
||||||
'agent_gateway': {
|
'agent_gateway': {
|
||||||
host?: string
|
host?: string
|
||||||
|
server_addr?: string
|
||||||
port?: number
|
port?: number
|
||||||
},
|
},
|
||||||
'server': {
|
'server': {
|
||||||
|
|||||||
+27
-11
@@ -3,7 +3,6 @@ import { chatModule } from './modules/chat'
|
|||||||
import { corsMiddleware } from './middlewares/cors'
|
import { corsMiddleware } from './middlewares/cors'
|
||||||
import { errorMiddleware } from './middlewares/error'
|
import { errorMiddleware } from './middlewares/error'
|
||||||
import { loadConfig } from './config'
|
import { loadConfig } from './config'
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
const config = loadConfig('../config.toml')
|
const config = loadConfig('../config.toml')
|
||||||
|
|
||||||
@@ -15,17 +14,26 @@ export const getBraveConfig = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getBaseUrl = () => {
|
export const getBaseUrl = () => {
|
||||||
let baseUrl = ''
|
const rawAddr =
|
||||||
if (!baseUrl) {
|
typeof config.agent_gateway.server_addr === 'string'
|
||||||
baseUrl = 'http://127.0.0.1'
|
? config.agent_gateway.server_addr.trim()
|
||||||
|
: typeof config.server.addr === 'string'
|
||||||
|
? config.server.addr.trim()
|
||||||
|
: ''
|
||||||
|
|
||||||
|
if (!rawAddr) {
|
||||||
|
return 'http://127.0.0.1'
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
typeof config.server.addr === 'string' &&
|
if (rawAddr.startsWith('http://') || rawAddr.startsWith('https://')) {
|
||||||
config.server.addr.startsWith(':')
|
return rawAddr.replace(/\/+$/, '')
|
||||||
) {
|
|
||||||
baseUrl = `http://127.0.0.1${config.server.addr}`
|
|
||||||
}
|
}
|
||||||
return baseUrl
|
|
||||||
|
if (rawAddr.startsWith(':')) {
|
||||||
|
return `http://127.0.0.1${rawAddr}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `http://${rawAddr}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthFetcher = (
|
export type AuthFetcher = (
|
||||||
@@ -40,7 +48,12 @@ export const createAuthFetcher = (bearer: string | undefined): AuthFetcher => {
|
|||||||
headers.set('Authorization', `Bearer ${bearer}`)
|
headers.set('Authorization', `Bearer ${bearer}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return await fetch(join(getBaseUrl(), url), {
|
const requestUrl = new URL(
|
||||||
|
url,
|
||||||
|
`${getBaseUrl().replace(/\/+$/, '')}/`,
|
||||||
|
).toString()
|
||||||
|
|
||||||
|
return await fetch(requestUrl, {
|
||||||
...requestOptions,
|
...requestOptions,
|
||||||
headers,
|
headers,
|
||||||
})
|
})
|
||||||
@@ -50,6 +63,9 @@ export const createAuthFetcher = (bearer: string | undefined): AuthFetcher => {
|
|||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
.use(corsMiddleware)
|
.use(corsMiddleware)
|
||||||
.use(errorMiddleware)
|
.use(errorMiddleware)
|
||||||
|
.get('/health', () => ({
|
||||||
|
status: 'ok',
|
||||||
|
}))
|
||||||
.use(chatModule)
|
.use(chatModule)
|
||||||
.listen({
|
.listen({
|
||||||
port: config.agent_gateway.port ?? 8081,
|
port: config.agent_gateway.port ?? 8081,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ timeout_seconds = 10
|
|||||||
[agent_gateway]
|
[agent_gateway]
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = 8081
|
port = 8081
|
||||||
|
server_addr = ":8080"
|
||||||
|
|
||||||
[brave]
|
[brave]
|
||||||
api_key = ""
|
api_key = ""
|
||||||
|
|||||||
@@ -44,12 +44,39 @@ if [ ! -f config.toml ]; then
|
|||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build MCP image
|
# Prepare data root path for host/containerd compatibility
|
||||||
echo -e "${GREEN}Building MCP image...${NC}"
|
MEMOH_DATA_ROOT="$(pwd)/.data/memoh"
|
||||||
if docker build -f docker/Dockerfile.mcp -t memoh-mcp:latest . > /dev/null 2>&1; then
|
mkdir -p "${MEMOH_DATA_ROOT}"
|
||||||
echo -e "${GREEN}✓ MCP image built successfully${NC}"
|
export MEMOH_DATA_ROOT
|
||||||
|
if grep -q '^data_root[[:space:]]*=' config.toml; then
|
||||||
|
awk -v path="${MEMOH_DATA_ROOT}" '
|
||||||
|
$0 ~ /^data_root[[:space:]]*=/ { print "data_root = \"" path "\""; next }
|
||||||
|
{ print }
|
||||||
|
' config.toml > config.toml.tmp && mv config.toml.tmp config.toml
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}✓ Data root: ${MEMOH_DATA_ROOT}${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prepare container runtime environment
|
||||||
|
echo -e "${GREEN}Preparing container runtime environment...${NC}"
|
||||||
|
if sh scripts/containerd-install.sh > /dev/null 2>&1; then
|
||||||
|
echo -e "${GREEN}✓ Container runtime environment is ready${NC}"
|
||||||
else
|
else
|
||||||
echo -e "${YELLOW}⚠ MCP image build failed, will try to pull at runtime${NC}"
|
echo -e "${YELLOW}⚠ Failed to prepare container runtime environment, MCP build may be skipped${NC}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build MCP image on host with nerdctl
|
||||||
|
MCP_IMAGE="docker.io/library/memoh-mcp:latest"
|
||||||
|
echo -e "${GREEN}Building MCP image on host with nerdctl...${NC}"
|
||||||
|
if command -v nerdctl &> /dev/null && command -v buildctl &> /dev/null && command -v buildkitd &> /dev/null; then
|
||||||
|
if nerdctl build -f docker/Dockerfile.mcp -t "${MCP_IMAGE}" . > /dev/null 2>&1; then
|
||||||
|
echo -e "${GREEN}✓ MCP image built successfully (on host)${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ MCP image build failed on host, will try to pull at runtime${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ nerdctl/buildkit environment not found on host, skipping MCP build${NC}"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
+16
-18
@@ -1,5 +1,5 @@
|
|||||||
|
name: "memoh"
|
||||||
services:
|
services:
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:18.1-alpine
|
image: postgres:18.1-alpine
|
||||||
container_name: memoh-postgres
|
container_name: memoh-postgres
|
||||||
@@ -8,7 +8,7 @@ services:
|
|||||||
POSTGRES_USER: memoh
|
POSTGRES_USER: memoh
|
||||||
POSTGRES_PASSWORD: memoh123
|
POSTGRES_PASSWORD: memoh123
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql
|
||||||
- ./db/migrations:/docker-entrypoint-initdb.d:ro
|
- ./db/migrations:/docker-entrypoint-initdb.d:ro
|
||||||
expose:
|
expose:
|
||||||
- "5432"
|
- "5432"
|
||||||
@@ -38,26 +38,24 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- memoh-network
|
- memoh-network
|
||||||
|
|
||||||
docker-cli:
|
|
||||||
image: docker:27-cli
|
|
||||||
container_name: memoh-docker-cli
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- memoh_bot_data:/var/lib/memoh/data
|
|
||||||
command: ["tail", "-f", "/dev/null"]
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- memoh-network
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
build:
|
build:
|
||||||
context: ./docker
|
context: .
|
||||||
dockerfile: Dockerfile.server
|
dockerfile: docker/Dockerfile.server
|
||||||
container_name: memoh-server
|
container_name: memoh-server
|
||||||
|
pid: host
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.toml:/app/config.toml:ro
|
- ./config.toml:/app/config.toml:ro
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /run/containerd/containerd.sock:/run/containerd/containerd.sock
|
||||||
- memoh_bot_data:/var/lib/memoh/data
|
- /var/lib/containerd:/var/lib/containerd
|
||||||
|
- server_cni_state:/var/lib/cni
|
||||||
|
- ${MEMOH_DATA_ROOT:-/opt/memoh/data}:${MEMOH_DATA_ROOT:-/opt/memoh/data}
|
||||||
|
cap_add:
|
||||||
|
- SYS_ADMIN
|
||||||
|
- NET_ADMIN
|
||||||
|
security_opt:
|
||||||
|
- seccomp:unconfined
|
||||||
|
- apparmor:unconfined
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -106,7 +104,7 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
qdrant_data:
|
qdrant_data:
|
||||||
driver: local
|
driver: local
|
||||||
memoh_bot_data:
|
server_cni_state:
|
||||||
driver: local
|
driver: local
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -17,11 +17,43 @@ FROM alpine:latest
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates tzdata wget
|
RUN apk add --no-cache ca-certificates tzdata wget nerdctl cni-plugins iptables \
|
||||||
|
&& mkdir -p /opt/cni/bin \
|
||||||
|
&& (cp -a /usr/lib/cni/. /opt/cni/bin/ 2>/dev/null || true) \
|
||||||
|
&& (cp -a /usr/libexec/cni/. /opt/cni/bin/ 2>/dev/null || true) \
|
||||||
|
&& mkdir -p /etc/cni/net.d /var/lib/cni \
|
||||||
|
&& printf '%s\n' \
|
||||||
|
'{' \
|
||||||
|
' "cniVersion": "1.0.0",' \
|
||||||
|
' "name": "memoh-cni",' \
|
||||||
|
' "plugins": [' \
|
||||||
|
' {' \
|
||||||
|
' "type": "bridge",' \
|
||||||
|
' "bridge": "cni0",' \
|
||||||
|
' "isGateway": true,' \
|
||||||
|
' "ipMasq": true,' \
|
||||||
|
' "promiscMode": true,' \
|
||||||
|
' "ipam": {' \
|
||||||
|
' "type": "host-local",' \
|
||||||
|
' "ranges": [[' \
|
||||||
|
' { "subnet": "10.88.0.0/16" }' \
|
||||||
|
' ]],' \
|
||||||
|
' "routes": [' \
|
||||||
|
' { "dst": "0.0.0.0/0" }' \
|
||||||
|
' ]' \
|
||||||
|
' }' \
|
||||||
|
' },' \
|
||||||
|
' {' \
|
||||||
|
' "type": "portmap",' \
|
||||||
|
' "capabilities": { "portMappings": true }' \
|
||||||
|
' }' \
|
||||||
|
' ]' \
|
||||||
|
'}' > /etc/cni/net.d/10-memoh.conflist
|
||||||
|
|
||||||
COPY --from=builder /build/memoh-server /app/memoh-server
|
COPY --from=builder /build/memoh-server /app/memoh-server
|
||||||
|
COPY --from=builder /build/spec /app/spec
|
||||||
|
|
||||||
RUN mkdir -p /var/lib/memoh/data
|
RUN mkdir -p /opt/memoh/data
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ COPY docker/config/nginx.conf /etc/nginx/conf.d/default.conf
|
|||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
|
CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ level = "info"
|
|||||||
format = "text"
|
format = "text"
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
addr = ":8080"
|
addr = "server:8080"
|
||||||
|
|
||||||
## Admin
|
## Admin
|
||||||
[admin]
|
[admin]
|
||||||
@@ -19,13 +19,13 @@ jwt_expires_in = "168h"
|
|||||||
|
|
||||||
## Docker configuration
|
## Docker configuration
|
||||||
[containerd]
|
[containerd]
|
||||||
socket_path = "unix:///var/run/docker.sock"
|
socket_path = "/run/containerd/containerd.sock"
|
||||||
namespace = "default"
|
namespace = "default"
|
||||||
|
|
||||||
[mcp]
|
[mcp]
|
||||||
busybox_image = "memoh-mcp:latest"
|
busybox_image = "docker.io/library/memoh-mcp:latest"
|
||||||
snapshotter = "overlayfs"
|
snapshotter = "overlayfs"
|
||||||
data_root = "/var/lib/memoh/data"
|
data_root = "/opt/memoh/data"
|
||||||
data_mount = "/data"
|
data_mount = "/data"
|
||||||
|
|
||||||
## Postgres configuration
|
## Postgres configuration
|
||||||
@@ -48,6 +48,7 @@ timeout_seconds = 10
|
|||||||
[agent_gateway]
|
[agent_gateway]
|
||||||
host = "agent"
|
host = "agent"
|
||||||
port = 8081
|
port = 8081
|
||||||
|
server_addr = "server:8080"
|
||||||
|
|
||||||
[brave]
|
[brave]
|
||||||
api_key = ""
|
api_key = ""
|
||||||
|
|||||||
+27
-27
@@ -1,5 +1,6 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
server_name _;
|
server_name _;
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
@@ -15,40 +16,39 @@ server {
|
|||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# API 代理
|
# Nginx 健康检查
|
||||||
|
location = /health {
|
||||||
|
access_log off;
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
return 200 "ok\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
# 统一代理参数
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
|
||||||
|
# Swagger 文档(保留 /api 前缀)
|
||||||
|
location ~ ^/api/(docs(?:/.*)?|swagger\.json)$ {
|
||||||
|
proxy_pass http://memoh-server:8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API 代理(其余 /api/* 去掉 /api 前缀转发)
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://memoh-server:8080/;
|
proxy_pass http://memoh-server:8080/;
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# 超时设置
|
|
||||||
proxy_connect_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Agent Gateway 代理
|
# Agent Gateway 代理
|
||||||
location /agent/ {
|
location /agent/ {
|
||||||
proxy_pass http://memoh-agent:8081/;
|
proxy_pass http://memoh-agent:8081/;
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# 超时设置
|
|
||||||
proxy_connect_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 静态资源缓存
|
# 静态资源缓存
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -673,15 +672,8 @@ func (s *DefaultService) ExecTaskStreaming(ctx context.Context, containerID stri
|
|||||||
if req.Terminal {
|
if req.Terminal {
|
||||||
ioOpts = append(ioOpts, cio.WithTerminal)
|
ioOpts = append(ioOpts, cio.WithTerminal)
|
||||||
}
|
}
|
||||||
fifoDir := strings.TrimSpace(req.FIFODir)
|
fifoDir, err := resolveExecFIFODir(req.FIFODir)
|
||||||
if fifoDir == "" {
|
if err != nil {
|
||||||
if homeDir, err := os.UserHomeDir(); err == nil && homeDir != "" {
|
|
||||||
fifoDir = filepath.Join(homeDir, ".memoh", "containerd-fifo")
|
|
||||||
} else {
|
|
||||||
fifoDir = "/tmp/memoh-containerd-fifo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(fifoDir, 0o755); err != nil {
|
|
||||||
_ = stdinR.Close()
|
_ = stdinR.Close()
|
||||||
_ = stdinW.Close()
|
_ = stdinW.Close()
|
||||||
_ = stdoutR.Close()
|
_ = stdoutR.Close()
|
||||||
@@ -752,6 +744,27 @@ func (s *DefaultService) ExecTaskStreaming(ctx context.Context, containerID stri
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveExecFIFODir(preferred string) (string, error) {
|
||||||
|
candidates := make([]string, 0, 3)
|
||||||
|
if p := strings.TrimSpace(preferred); p != "" {
|
||||||
|
candidates = append(candidates, p)
|
||||||
|
}
|
||||||
|
candidates = append(candidates, "/var/lib/containerd/memoh-fifo", "/tmp/memoh-containerd-fifo")
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for _, dir := range candidates {
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err == nil {
|
||||||
|
return dir, nil
|
||||||
|
} else {
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lastErr == nil {
|
||||||
|
lastErr = fmt.Errorf("no fifo directory candidate available")
|
||||||
|
}
|
||||||
|
return "", lastErr
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DefaultService) ListContainersByLabel(ctx context.Context, key, value string) ([]containerd.Container, error) {
|
func (s *DefaultService) ListContainersByLabel(ctx context.Context, key, value string) ([]containerd.Container, error) {
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return nil, ErrInvalidArgument
|
return nil, ErrInvalidArgument
|
||||||
|
|||||||
+37
-5
@@ -4,11 +4,13 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -188,7 +190,8 @@ func (h *ContainerdHandler) getMCPSession(ctx context.Context, containerID strin
|
|||||||
|
|
||||||
func (h *ContainerdHandler) startContainerdMCPSession(ctx context.Context, containerID string) (*mcpSession, error) {
|
func (h *ContainerdHandler) startContainerdMCPSession(ctx context.Context, containerID string) (*mcpSession, error) {
|
||||||
execSession, err := h.service.ExecTaskStreaming(ctx, containerID, ctr.ExecTaskRequest{
|
execSession, err := h.service.ExecTaskStreaming(ctx, containerID, ctr.ExecTaskRequest{
|
||||||
Args: []string{"/app/mcp"},
|
Args: []string{"/app/mcp"},
|
||||||
|
FIFODir: h.mcpFIFODir(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -207,11 +210,15 @@ func (h *ContainerdHandler) startContainerdMCPSession(ctx context.Context, conta
|
|||||||
go func() {
|
go func() {
|
||||||
_, err := execSession.Wait()
|
_, err := execSession.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if isBenignMCPSessionExit(err) {
|
||||||
|
sess.closeWithError(io.EOF)
|
||||||
|
return
|
||||||
|
}
|
||||||
h.logger.Error("mcp session exited", slog.Any("error", err), slog.String("container_id", containerID))
|
h.logger.Error("mcp session exited", slog.Any("error", err), slog.String("container_id", containerID))
|
||||||
sess.closeWithError(err)
|
sess.closeWithError(err)
|
||||||
} else {
|
return
|
||||||
sess.closeWithError(io.EOF)
|
|
||||||
}
|
}
|
||||||
|
sess.closeWithError(io.EOF)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return sess, nil
|
return sess, nil
|
||||||
@@ -273,11 +280,15 @@ func (h *ContainerdHandler) startLimaMCPSession(containerID string) (*mcpSession
|
|||||||
go sess.readLoop()
|
go sess.readLoop()
|
||||||
go func() {
|
go func() {
|
||||||
if err := cmd.Wait(); err != nil {
|
if err := cmd.Wait(); err != nil {
|
||||||
|
if isBenignMCPSessionExit(err) {
|
||||||
|
sess.closeWithError(io.EOF)
|
||||||
|
return
|
||||||
|
}
|
||||||
h.logger.Error("mcp session exited", slog.Any("error", err), slog.String("container_id", containerID))
|
h.logger.Error("mcp session exited", slog.Any("error", err), slog.String("container_id", containerID))
|
||||||
sess.closeWithError(err)
|
sess.closeWithError(err)
|
||||||
} else {
|
return
|
||||||
sess.closeWithError(io.EOF)
|
|
||||||
}
|
}
|
||||||
|
sess.closeWithError(io.EOF)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return sess, nil
|
return sess, nil
|
||||||
@@ -320,11 +331,32 @@ func (h *ContainerdHandler) startMCPStderrLogger(stderr io.ReadCloser, container
|
|||||||
h.logger.Warn("mcp stderr", slog.String("container_id", containerID), slog.String("message", line))
|
h.logger.Warn("mcp stderr", slog.String("container_id", containerID), slog.String("message", line))
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
|
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) || strings.Contains(err.Error(), "closed pipe") {
|
||||||
|
return
|
||||||
|
}
|
||||||
h.logger.Error("mcp stderr read failed", slog.Any("error", err), slog.String("container_id", containerID))
|
h.logger.Error("mcp stderr read failed", slog.Any("error", err), slog.String("container_id", containerID))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isBenignMCPSessionExit(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if errors.Is(err, context.Canceled) || errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
msg := strings.ToLower(err.Error())
|
||||||
|
return strings.Contains(msg, "code = canceled") || strings.Contains(msg, "context canceled") || strings.Contains(msg, "closed pipe")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ContainerdHandler) mcpFIFODir() string {
|
||||||
|
if root := strings.TrimSpace(h.cfg.DataRoot); root != "" {
|
||||||
|
return filepath.Join(root, ".containerd-fifo")
|
||||||
|
}
|
||||||
|
return "/tmp/memoh-containerd-fifo"
|
||||||
|
}
|
||||||
|
|
||||||
func (s *mcpSession) readLoop() {
|
func (s *mcpSession) readLoop() {
|
||||||
scanner := bufio.NewScanner(s.stdout)
|
scanner := bufio.NewScanner(s.stdout)
|
||||||
scanner.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)
|
scanner.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ func (h *ContainerdHandler) startContainerdMCPCommandSession(ctx context.Context
|
|||||||
Args: args,
|
Args: args,
|
||||||
Env: env,
|
Env: env,
|
||||||
WorkDir: strings.TrimSpace(req.Cwd),
|
WorkDir: strings.TrimSpace(req.Cwd),
|
||||||
|
FIFODir: h.mcpFIFODir(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -195,11 +196,15 @@ func (h *ContainerdHandler) startContainerdMCPCommandSession(ctx context.Context
|
|||||||
go func() {
|
go func() {
|
||||||
_, err := execSession.Wait()
|
_, err := execSession.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if isBenignMCPSessionExit(err) {
|
||||||
|
sess.closeWithError(io.EOF)
|
||||||
|
return
|
||||||
|
}
|
||||||
h.logger.Error("mcp stdio session exited", slog.Any("error", err), slog.String("container_id", containerID))
|
h.logger.Error("mcp stdio session exited", slog.Any("error", err), slog.String("container_id", containerID))
|
||||||
sess.closeWithError(err)
|
sess.closeWithError(err)
|
||||||
} else {
|
return
|
||||||
sess.closeWithError(io.EOF)
|
|
||||||
}
|
}
|
||||||
|
sess.closeWithError(io.EOF)
|
||||||
}()
|
}()
|
||||||
return sess, nil
|
return sess, nil
|
||||||
}
|
}
|
||||||
@@ -342,11 +347,15 @@ func (h *ContainerdHandler) startLimaMCPCommandSession(containerID string, req M
|
|||||||
go sess.readLoop()
|
go sess.readLoop()
|
||||||
go func() {
|
go func() {
|
||||||
if err := cmd.Wait(); err != nil {
|
if err := cmd.Wait(); err != nil {
|
||||||
|
if isBenignMCPSessionExit(err) {
|
||||||
|
sess.closeWithError(io.EOF)
|
||||||
|
return
|
||||||
|
}
|
||||||
h.logger.Error("mcp stdio session exited", slog.Any("error", err), slog.String("container_id", containerID))
|
h.logger.Error("mcp stdio session exited", slog.Any("error", err), slog.String("container_id", containerID))
|
||||||
sess.closeWithError(err)
|
sess.closeWithError(err)
|
||||||
} else {
|
return
|
||||||
sess.closeWithError(io.EOF)
|
|
||||||
}
|
}
|
||||||
|
sess.closeWithError(io.EOF)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return sess, nil
|
return sess, nil
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ func NewPingHandler(log *slog.Logger) *PingHandler {
|
|||||||
|
|
||||||
func (h *PingHandler) Register(e *echo.Echo) {
|
func (h *PingHandler) Register(e *echo.Echo) {
|
||||||
e.GET("/ping", h.Ping)
|
e.GET("/ping", h.Ping)
|
||||||
|
e.HEAD("/health", h.PingHead)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *PingHandler) Ping(c echo.Context) error {
|
func (h *PingHandler) Ping(c echo.Context) error {
|
||||||
@@ -24,3 +25,7 @@ func (h *PingHandler) Ping(c echo.Context) error {
|
|||||||
"status": "ok",
|
"status": "ok",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *PingHandler) PingHead(c echo.Context) error {
|
||||||
|
return c.NoContent(http.StatusOK)
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func NewServer(log *slog.Logger, addr string, jwtSecret string, pingHandler *han
|
|||||||
}))
|
}))
|
||||||
e.Use(auth.JWTMiddleware(jwtSecret, func(c echo.Context) bool {
|
e.Use(auth.JWTMiddleware(jwtSecret, func(c echo.Context) bool {
|
||||||
path := c.Request().URL.Path
|
path := c.Request().URL.Path
|
||||||
if path == "/ping" || path == "/api/swagger.json" || path == "/auth/login" {
|
if path == "/ping" || path == "/health" || path == "/api/swagger.json" || path == "/auth/login" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(path, "/api/docs") {
|
if strings.HasPrefix(path, "/api/docs") {
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ if [ "$(uname -s)" = "Darwin" ]; then
|
|||||||
exit $?
|
exit $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v containerd >/dev/null 2>&1 && command -v nerdctl >/dev/null 2>&1 && command -v buildctl >/dev/null 2>&1 && command -v buildkitd >/dev/null 2>&1; then
|
if command -v containerd >/dev/null 2>&1 \
|
||||||
|
&& command -v nerdctl >/dev/null 2>&1 \
|
||||||
|
&& command -v buildctl >/dev/null 2>&1 \
|
||||||
|
&& command -v buildkitd >/dev/null 2>&1; then
|
||||||
containerd --version
|
containerd --version
|
||||||
nerdctl --version
|
nerdctl --version
|
||||||
buildctl --version
|
buildctl --version
|
||||||
|
|||||||
Reference in New Issue
Block a user