diff --git a/.env.example b/.env.example index c5f3a51..e284776 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/Dockerfile b/Dockerfile index 65daa61..efdcb67 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 @@ -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 \ @@ -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 # ============================================================================== @@ -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))" diff --git a/Dockerfile.base b/Dockerfile.base index 47fcfb3..0dba8e5 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -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) diff --git a/docker-compose.yml b/docker-compose.yml index 5f0c2e4..cdbfb01 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 36e367d..fbb0ea3 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -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) @@ -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.'); " @@ -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 </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() { @@ -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 @@ -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 diff --git a/templates/ov.conf.template b/templates/ov.conf.template new file mode 100644 index 0000000..e173c70 --- /dev/null +++ b/templates/ov.conf.template @@ -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 + } +}