Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,31 @@ GIT_USER_EMAIL=dev@openclaw.io
# gogcli keyring 密码(用于解密 refresh token)
# 首次在容器内完成 gog auth 后设置,重启容器时自动注入
GOG_KEYRING_PASSWORD=your_gog_keyring_password

# ─────────────────────────────────────────────────────────────────
# OpenViking 配置 (本地 VLM 和 Embedding 服务)
# ─────────────────────────────────────────────────────────────────
# OpenViking 插件启用开关 (true=启用, false=禁用)
# 启用后: openclaw config set plugins.entries.openviking.enabled true
# 禁用后: openclaw config set plugins.entries.openviking.enabled false
OPENVIKING_ENABLED=true

# OpenViking Embedding 配置
# 启用后必填,值可选 volcengine (Doubao), openai, jina, voyage, minimax, vikingdb, and gemini (requires pip install "google-genai>=1.0.0")
OPENVIKING_EMBEDDING_PROVIDER=volcengine
OPENVIKING_EMBEDDING_API_KEY=your_api_key
# embedding 模型名称 (e.g., doubao-embedding-vision-250615 or text-embedding-3-large)
OPENVIKING_EMBEDDING_MODEL=doubao-embedding-vision-251215
OPENVIKING_EMBEDDING_API_BASE=https://ark.cn-beijing.volces.com/api/v3
# embedding向量维度
OPENVIKING_EMBEDDING_DIMENSION=1024
# 设置输入类型为多模态
OPENVIKING_EMBEDDING_INPUT=multimodal

# OpenViking VLM 配置
# 启用后必填,值可选 volcengine, openai, and litellm
OPENVIKING_VLM_PROVIDER=volcengine
OPENVIKING_VLM_API_KEY=your_api_key
# VLM 模型名称 (e.g., doubao-seed-2-0-pro-260215 or gpt-4-vision-preview)
OPENVIKING_VLM_MODEL=doubao-seed-2-0-pro-260215
OPENVIKING_VLM_API_BASE=https://ark.cn-beijing.volces.com/api/v3
28 changes: 27 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ RUN echo '' >> /etc/profile && \
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

# OpenViking configuration template
COPY templates/ov.conf.template /tmp/ov.conf.template

# Bake best-practice shell environment
COPY scripts/.bashrc.devkit /home/node/.bashrc
RUN chown node:node /home/node/.bashrc
Expand Down Expand Up @@ -121,12 +124,14 @@ RUN --mount=type=cache,target=/root/.npm,uid=1000,gid=1000 \
echo "[npm-retry] FAILED after $max_attempts attempts"; return 1; \
}; \
npm_retry npm install -g openclaw@${OPENCLAW_VERSION} && \
npm_retry npm install -g @buape/carbon@latest && \
npm_retry npm install -g @larksuite/openclaw-lark@latest && \
npm_retry npm install -g clawhub@latest && \
npm_retry npm install -g openclaw-openviking-setup-helper && \
chown -R node:node /home/node/.global'

# Layer 3b: AI Coding Tools (optional, ~500MB, skip for office variant)
# Includes: claude-code, pi-coding-agent, opencode
# Includes: claude-code, pi-coding-agent, opencode, openviking
# Set INSTALL_AI_TOOLS=0 when building office variant
RUN --mount=type=cache,target=/root/.npm,uid=1000,gid=1000 \
if [ "${INSTALL_AI_TOOLS}" = "1" ]; then \
Expand All @@ -146,6 +151,24 @@ RUN if [ "${INSTALL_AI_TOOLS}" = "1" ]; then \
runuser -u node -- sh -c 'curl -fsSL https://opencode.ai/install | INSTALL_DIR=/home/node/.opencode/bin bash'; \
fi

# Initialize OpenViking (run as node user)
# Note: ov-install requires network access to download the plugin
# Skip in build if no proxy available to avoid build failures
# OpenViking is now installed in Layer 3 (core layer), not conditional on INSTALL_AI_TOOLS
RUN if command -v ov-install >/dev/null 2>&1; then \
su - node -c 'ov-install -y' || echo "WARNING: ov-install failed, will retry on container start"; \
fi

# Stage OpenViking artifacts to non-mounted path (/app survives bind mount shadowing)
# At runtime, entrypoint copies from staging into the bind-mounted ~/.openclaw/
RUN if [ -d /home/node/.openclaw/extensions/openviking ]; then \
mkdir -p /app/openviking-staging/extensions && \
cp -r /home/node/.openclaw/extensions/openviking /app/openviking-staging/extensions/ && \
if [ -f /home/node/.openclaw/openviking.env ]; then \
cp /home/node/.openclaw/openviking.env /app/openviking-staging/; \
fi; \
fi

# ==============================================================================
# Layer 4: Optional Components
# ==============================================================================
Expand All @@ -158,6 +181,9 @@ RUN if [ "${INSTALL_BROWSER}" = "1" ]; then \

ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright

## new openclaw version needed
RUN npm install -g grammy @slack/web-api

# Healthcheck
HEALTHCHECK --interval=3m --timeout=10s --start-period=15s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:18789/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ deb http://$APT_MIRROR/debian bookworm-updates main contrib non-free\n" > /etc/a
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates gnupg git jq ripgrep fd-find build-essential pkg-config \
unzip file sqlite3 zip wget procps openssl less vim tree \
fzf zoxide tldr locales && \
fzf zoxide tldr locales python3-venv && \
apt-get clean && rm -rf /var/lib/apt/lists/*

# Generate Chinese locale (UTF-8)
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,10 @@ services:
- ${HOST_CLAWHUB_DIR:-${HOME}/.config/clawhub}:/home/node/.config/clawhub:rw

# ─────────────────────────────────────────────────────────────────
# Layer 4: 脚本 (只读)
# Layer 4: 脚本和模板 (只读)
# ─────────────────────────────────────────────────────────────────
- ./docker-entrypoint.sh:/usr/local/bin/docker-entrypoint.sh:ro
- ./templates:/app/templates:ro
# Run as root so the entrypoint can fix volume permissions before switching to node.
# The entrypoint uses `exec runuser -u node` to switch to node after setup.
user: root
Expand Down
154 changes: 153 additions & 1 deletion docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,37 @@ _sync_image_extensions() {
}
_sync_image_extensions

# ── OpenViking ────────────────────────────────────────────────────────────────
# ov-install runs during image build, but /home/node/.openclaw is bind-mounted
# from host at runtime, shadowing everything the image placed there.
# We stage the artifacts to /app/openviking-staging (not mounted) during build,
# then restore them into the mounted volume on first start.
# ------------------------------------------------------------------------------
_sync_openviking() {
local staging="/app/openviking-staging"
local ext_target="/home/node/.openclaw/extensions/openviking"
local env_target="/home/node/.openclaw/openviking.env"

# No staging dir means image was built without OpenViking
[[ -d "${staging}" ]] || return 0

# Extension already present in volume — skip
[[ -d "${ext_target}" ]] && return 0

echo "--> Syncing openviking from image staging..."
mkdir -p "${ext_target}"
cp -r "${staging}/extensions/openviking/"* "${ext_target}/" 2>/dev/null || true
chown -R 0:0 "${ext_target}" 2>/dev/null || true

# Restore openviking.env if missing
if [[ ! -f "${env_target}" && -f "${staging}/openviking.env" ]]; then
cp "${staging}/openviking.env" "${env_target}"
fi

echo "--> OpenViking extension synced."
}
_sync_openviking

# ------------------------------------------------------------------------------
# 2. Configuration Health Check & Surgical Repair
# - Surgery: runs once (path migration + Node.js cleanup)
Expand Down Expand Up @@ -625,6 +656,29 @@ if (cfg.plugins && cfg.plugins.entries && cfg.plugins.entries.feishu) {
delete cfg.plugins.entries.feishu;
}

// Configure OpenViking plugin based on OPENVIKING_ENABLED environment variable
const openvikingEnabled = '${OPENVIKING_ENABLED:-false}'.toLowerCase() === 'true';
cfg.plugins = cfg.plugins || {};
cfg.plugins.entries = cfg.plugins.entries || {};
cfg.plugins.entries.openviking = cfg.plugins.entries.openviking || {};

// enabled: 每次启动都设置
cfg.plugins.entries.openviking.enabled = openvikingEnabled;

// 首次初始化时设置 config 和 contextEngine
const surgeryFlag = '${SURGERY_FLAG}';
if (!fs.existsSync(surgeryFlag)) {
cfg.plugins.entries.openviking.config = {
mode: 'local',
configPath: '/home/node/.openclaw/openviking/ov.conf',
port: 1933
};
cfg.plugins.slots = cfg.plugins.slots || {};
cfg.plugins.slots.contextEngine = 'openviking';
console.log('--> OpenViking config and slots initialized.');
}
console.log('--> OpenViking plugin', openvikingEnabled ? 'enabled' : 'disabled');

fs.writeFileSync(path, JSON.stringify(cfg, null, 2));
console.log('--> Config batch update done.');
"
Expand Down Expand Up @@ -662,7 +716,90 @@ mkdir -p /home/node/.local
# Ensures all files created by the app belong to 'node' user
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# 6. Write Environment Variables to Node User's Shell Profile
# 6a. Generate OpenViking Configuration File from Template
# Replaces template variables with actual environment variable values
# ------------------------------------------------------------------------------
_generate_openviking_config() {
local template_file="/app/templates/ov.conf.template"
local config_dir="/home/node/.openclaw/openviking"
local config_file="${config_dir}/ov.conf"

# Skip if template file doesn't exist (not all image variants have OpenViking)
if [[ ! -f "${template_file}" ]]; then
echo "--> OpenViking template not found, skipping config generation."
return 0
fi

# Skip if OpenViking is disabled
if [[ "${OPENVIKING_ENABLED}" != "true" ]]; then
echo "--> OpenViking is disabled, skipping config generation."
return 0
fi

echo "--> Generating OpenViking configuration file..."

# Create config directory if it doesn't exist
if [[ "$(id -u)" = "0" ]]; then
run_as_node mkdir -p "${config_dir}" 2>/dev/null || true
else
mkdir -p "${config_dir}" 2>/dev/null || true
fi

# Use Node.js to replace template variables (handles JSON safely)
# Optimized: only process known template variables, don't scan all environment variables
run_as_node node <<NODE_EOF
const fs = require('fs');
const configPath = '${config_file}';
const templateFile = '${template_file}';

// Read template file
const template = fs.readFileSync(templateFile, 'utf8');

// Directly use known template variables (much faster than scanning process.env)
const vars = {
'OPENVIKING_EMBEDDING_PROVIDER': process.env.OPENVIKING_EMBEDDING_PROVIDER || '',
'OPENVIKING_EMBEDDING_API_KEY': process.env.OPENVIKING_EMBEDDING_API_KEY || '',
'OPENVIKING_EMBEDDING_MODEL': process.env.OPENVIKING_EMBEDDING_MODEL || '',
'OPENVIKING_EMBEDDING_API_BASE': process.env.OPENVIKING_EMBEDDING_API_BASE || '',
'OPENVIKING_EMBEDDING_DIMENSION': process.env.OPENVIKING_EMBEDDING_DIMENSION || '1024',
'OPENVIKING_EMBEDDING_INPUT': process.env.OPENVIKING_EMBEDDING_INPUT || 'multimodal',
'OPENVIKING_VLM_PROVIDER': process.env.OPENVIKING_VLM_PROVIDER || '',
'OPENVIKING_VLM_API_KEY': process.env.OPENVIKING_VLM_API_KEY || '',
'OPENVIKING_VLM_MODEL': process.env.OPENVIKING_VLM_MODEL || '',
'OPENVIKING_VLM_API_BASE': process.env.OPENVIKING_VLM_API_BASE || ''
};

// Replace template variables using simple string replacement (faster than regex)
let config = template;
for (const [key, value] of Object.entries(vars)) {
config = config.split('\${' + key + '}').join(value);
}

// Ensure output directory exists
const dir = require('path').dirname(configPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
}

// Write configuration file
fs.writeFileSync(configPath, config, { mode: 0o600 });
console.log(' ✓ OpenViking configuration written to:', configPath);
NODE_EOF
if [[ $? -ne 0 ]]; then
echo " ✗ Failed to generate OpenViking configuration"
return 1
fi

# Ensure correct ownership
if [[ "$(id -u)" = "0" ]]; then
chown -R node:node "${config_dir}" 2>/dev/null || true
fi

echo "--> OpenViking configuration generation completed."
}

# ------------------------------------------------------------------------------
# 6b. Write Environment Variables to Node User's Shell Profile
# Ensures environment variables are available in interactive shell sessions
# ------------------------------------------------------------------------------
_write_env_to_profile() {
Expand Down Expand Up @@ -694,6 +831,18 @@ _write_env_to_profile() {
"HTTPS_PROXY"
"NO_PROXY"
"TZ"
# OpenViking configuration
"OPENVIKING_ENABLED"
"OPENVIKING_EMBEDDING_PROVIDER"
"OPENVIKING_EMBEDDING_API_KEY"
"OPENVIKING_EMBEDDING_MODEL"
"OPENVIKING_EMBEDDING_API_BASE"
"OPENVIKING_EMBEDDING_DIMENSION"
"OPENVIKING_EMBEDDING_INPUT"
"OPENVIKING_VLM_PROVIDER"
"OPENVIKING_VLM_API_KEY"
"OPENVIKING_VLM_MODEL"
"OPENVIKING_VLM_API_BASE"
)

for var in "${env_vars[@]}"; do
Expand Down Expand Up @@ -779,6 +928,9 @@ _disable_builtin_feishu
# Configure npm cache
_configure_npm_cache

# Generate OpenViking configuration file
_generate_openviking_config

# Write environment variables to profile
_write_env_to_profile

Expand Down
49 changes: 49 additions & 0 deletions templates/ov.conf.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"server": {
"host": "127.0.0.1",
"port": 1933,
"root_api_key": null,
"cors_origins": ["*"]
},
"storage": {
"workspace": "/home/node/.openclaw/openviking/data",
"vectordb": {
"name": "context",
"backend": "local",
"project": "default"
},
"agfs": {
"port": 1833,
"log_level": "warn",
"backend": "local",
"timeout": 10,
"retry_times": 3
}
},
"log": {
"level": "WARNING",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"output": "file",
"rotation": true,
"rotation_days": 3,
"rotation_interval": "midnight"
},
"embedding": {
"dense": {
"provider": "${OPENVIKING_EMBEDDING_PROVIDER}",
"api_key": "${OPENVIKING_EMBEDDING_API_KEY}",
"model": "${OPENVIKING_EMBEDDING_MODEL}",
"api_base": "${OPENVIKING_EMBEDDING_API_BASE}",
"dimension": ${OPENVIKING_EMBEDDING_DIMENSION},
"input": "${OPENVIKING_EMBEDDING_INPUT}"
}
},
"vlm": {
"provider": "${OPENVIKING_VLM_PROVIDER}",
"api_key": "${OPENVIKING_VLM_API_KEY}",
"model": "${OPENVIKING_VLM_MODEL}",
"api_base": "${OPENVIKING_VLM_API_BASE}",
"temperature": 0.1,
"max_retries": 3
}
}