Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
11 KiB
JSON

{"repo": "beachanger/rightnow-3.2", "files": {"backend/src/chat/llm-chat.helper.ts": {"7022972bbe9c4b092a12ce8283e0f3b814dd4bb4": {"t": 1786068470.3502393, "n": 3621, "h": "8a28a1139efbc498", "c": "/**\n * Lightweight OpenAI-compatible chat client.\n *\n * Used by ChatService for the free-form coach conversation. Intentionally\n * separate from AiService — AiService owns the task-specific prompt catalog\n * and structured-output flows (training feedback, food analysis, ...);\n * this helper owns plain message-list -> reply chat.\n *\n * Provider precedence:\n * 1. STEPFUN_BASE_URL + STEPFUN_API_KEY (OpenAI-compatible)\n * 2. DEEPSEEK_BASE_URL + DEEPSEEK_API_KEY (OpenAI-compatible)\n * 3. (no provider) -> throws so the caller can fall back gracefully.\n */\n\nimport { setDefaultResultOrder } from 'node:dns';\n\n// Force IPv4 — api.stepfun.com resolves to IPv6 which breaks on many Docker hosts.\n// Must be called before any fetch() that may hit IPv6-only resolving domains.\nsetDefaultResultOrder('ipv4first');\n\nexport interface ChatTurn {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\nexport interface ChatProviderConfig {\n stepfunBaseUrl?: string;\n stepfunApiKey?: string;\n stepfunModel?: string;\n deepseekBaseUrl?: string;\n deepseekApiKey?: string;\n deepseekModel?: string;\n}\n\nexport interface ChatCallOptions {\n temperature?: number;\n maxTokens?: number;\n}\n\ninterface OpenAIChatResponse {\n choices?: Array<{ message?: { content?: string } }>;\n error?: { message?: string };\n}\n\nfunction pickProvider(cfg: ChatProviderConfig): {\n baseUrl: string;\n apiKey: string;\n model: string;\n name: 'stepfun' | 'deepseek';\n} | null {\n if (cfg.stepfunBaseUrl?.trim() && cfg.stepfunApiKey?.trim()) {\n return {\n baseUrl: cfg.stepfunBaseUrl.trim(),\n apiKey: cfg.stepfunApiKey.trim(),\n model: cfg.stepfunModel?.trim() || 'step-3.7-flash',\n name: 'stepfun',\n };\n }\n if (cfg.deepseekBaseUrl?.trim() && cfg.deepseekApiKey?.trim()) {\n return {\n baseUrl: cfg.deepseekBaseUrl.trim(),\n apiKey: cfg.deepseekApiKey.trim(),\n model: cfg.deepseekModel?.trim() || 'deepseek-chat',\n name: 'deepseek',\n };\n }\n return null;\n}\n\nconst LLM_TIMEOUT_MS = 12_000;\n\nexport async function callChatLlm(\n messages: ChatTurn[],\n cfg: ChatProviderConfig,\n opts: ChatCallOptions = {},\n): Promise<{ reply: string; provider: string; model: string }> {\n const provider = pickProvider(cfg);\n if (!provider) {\n throw new Error(\n 'No chat provider configured (set STEPFUN_API_KEY or DEEPSEEK_API_KEY in backend .env)',\n );\n }\n\n const url = `${provider.baseUrl.replace(/\\/$/, '')}/chat/completions`;\n const body = {\n model: provider.model,\n messages,\n temperature: opts.temperature ?? 0.7,\n max_tokens: opts.maxTokens ?? 800,\n };\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);\n\n try {\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${provider.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n const text = await res.text();\n let data: OpenAIChatResponse;\n try {\n data = JSON.parse(text);\n } catch {\n throw new Error(`Chat provider returned non-JSON (HTTP ${res.status}): ${text.slice(0, 300)}`);\n }\n\n if (!res.ok) {\n const msg = data.error?.message || `HTTP ${res.status}`;\n throw new Error(`Chat provider failed: ${msg}`);\n }\n\n const reply = data.choices?.[0]?.message?.content?.trim();\n if (!reply) {\n throw new Error('Chat provider returned empty reply');\n }\n\n return { reply, provider: provider.name, model: provider.model };\n } finally {\n clearTimeout(timer);\n }\n}\n"}}, "docker-compose.prod.yml": {"7022972bbe9c4b092a12ce8283e0f3b814dd4bb4": {"t": 1786068527.4757438, "n": 6536, "h": "3b5503ba4752bda3", "c": "services:\n postgres:\n image: postgres:16-alpine\n container_name: rn-postgres\n restart: unless-stopped\n environment:\n POSTGRES_DB: ${POSTGRES_DB:-rightnow_fitness}\n POSTGRES_USER: ${POSTGRES_USER:-postgres}\n POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}\n volumes:\n - pgdata:/var/lib/postgresql/data\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U postgres\"]\n interval: 5s\n timeout: 5s\n retries: 5\n\n backend:\n build:\n context: .\n dockerfile: Dockerfile.backend\n container_name: rn-backend\n restart: unless-stopped\n environment:\n DATABASE_URL: ${DATABASE_URL}\n JWT_SECRET: ${JWT_SECRET}\n PORT: 5000\n HOST: 0.0.0.0\n CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:5173,http://localhost:5174}\n RAG_SERVICE_URL: http://rag:8000\n ADMIN_SEED_EMAIL: ${ADMIN_SEED_EMAIL:-admin@example.com}\n ADMIN_SEED_PASSWORD: ${ADMIN_SEED_PASSWORD}\n ADMIN_SEED_NAME: ${ADMIN_SEED_NAME:-RightNow Admin}\n IMAGE_GEN_BASE_URL: ${IMAGE_GEN_BASE_URL:-}\n IMAGE_GEN_MODEL: ${IMAGE_GEN_MODEL:-gpt-image-2}\n IMAGE_GEN_API_KEY: ${IMAGE_GEN_API_KEY:-}\n IMAGE_GEN_PROXY_URL: ${IMAGE_GEN_PROXY_URL:-}\n IMAGE_GEN_FALLBACK_MODEL: ${IMAGE_GEN_FALLBACK_MODEL:-}\n ARK_IMAGE_API_KEY: ${ARK_IMAGE_API_KEY:-}\n ARK_IMAGE_BASE_URL: ${ARK_IMAGE_BASE_URL:-}\n ARK_IMAGE_MODEL: ${ARK_IMAGE_MODEL:-doubao-seedream-5-0-260128}\n LEGACY_IMAGE_GEN_API_KEY: ${LEGACY_IMAGE_GEN_API_KEY:-}\n LEGACY_IMAGE_GEN_BASE_URL: ${LEGACY_IMAGE_GEN_BASE_URL:-}\n LEGACY_IMAGE_GEN_MODEL: ${LEGACY_IMAGE_GEN_MODEL:-gpt-image-2}\n MIRROR_IMAGE_API_KEY: ${MIRROR_IMAGE_API_KEY:-}\n MIRROR_IMAGE_BASE_URL: ${MIRROR_IMAGE_BASE_URL:-}\n MIRROR_IMAGE_MODEL: ${MIRROR_IMAGE_MODEL:-gpt-image-2}\n STEPFUN_BASE_URL: ${STEPFUN_BASE_URL:-https://api.stepfun.com/v1}\n STEPFUN_API_KEY: ${STEPFUN_API_KEY:-}\n STEPFUN_CHAT_MODEL: ${STEPFUN_CHAT_MODEL:-step-3.7-flash}\n DEEPSEEK_BASE_URL: ${DEEPSEEK_BASE_URL:-}\n DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}\n DEEPSEEK_CHAT_MODEL: ${DEEPSEEK_CHAT_MODEL:-deepseek-chat}\n AGENT_SERVICE_TOKEN: ${AGENT_SERVICE_TOKEN:-}\n SINGLE_USER_MODE: ${SINGLE_USER_MODE:-false}\n SINGLE_USER_ID: ${SINGLE_USER_ID:-}\n SINGLE_USER_CHANNELS: ${SINGLE_USER_CHANNELS:-}\n INTERNAL_API_TOKEN: ${INTERNAL_API_TOKEN:-}\n OPENCLAW_GATEWAY_URL: http://rn-openclaw-gateway:18789\n OPENCLAW_CONFIG_PATH: /root/.openclaw/openclaw.json\n OPENCLAW_PROVISION_MODE: \n OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-}\n PUSH_BRIDGE_URL: http://wechat-bridge:3000\n depends_on:\n postgres:\n condition: service_healthy\n volumes:\n - uploads:/app/uploads\n - /root/.openclaw:/root/.openclaw\n\n rag:\n build:\n context: .\n dockerfile: Dockerfile.rag\n container_name: rn-rag\n restart: unless-stopped\n environment:\n RAG_CORS_ORIGINS: \"*\"\n RAG_USE_L1_FAQ: \"true\"\n RAG_L1_COLLECTION: kb_l1_faq\n RAG_L1_CHROMA_DIR: /app/chroma_l1_faq\n RAG_L1_FAQ_PATH: /data/l1-faq/faq.json\n RAG_L1_FAST_THRESHOLD: \"0.30\"\n RAG_L1_SEARCH_EF: \"16\"\n RAG_L1_WAIT_MS: \"150\"\n RAG_USE_L2_CORE: \"true\"\n RAG_L2_COLLECTION: kb_l2_core\n RAG_L2_CHROMA_DIR: /app/chroma_l2_core\n RAG_L2_DATA_PATH: /data/l2-core\n RAG_USE_L3_BOOKS: \"true\"\n RAG_L3_COLLECTION: kb_l3_books\n RAG_L3_CHROMA_DIR: /app/chroma_l3_books\n RAG_L3_DATA_PATH: /data/l3-books\n RAG_FAST_PATH: \"true\"\n RAG_USE_LEGACY: \"false\"\n RAG_CHROMA_PERSIST_DIR: /app/chroma_db\n RAG_BLOGGER_CHROMA_DIR: /app/chroma_blogger\n RAG_CLEANED_DATA_PATH: /data/cleaned-data\n RAG_BLOGGER_DATA_PATH: /data/blogger-data\n RAG_EMBEDDING_MODEL: BAAI/bge-small-zh-v1.5\n RAG_USE_BLOGGER: \"true\"\n RAG_USE_WEB_SEARCH: \"true\"\n RAG_WEB_SEARCH_ENGINE: duckduckgo\n RAG_SIMILARITY_THRESHOLD: \"0.35\"\n RAG_CHUNK_SIZE: \"800\"\n RAG_CHUNK_OVERLAP: \"100\"\n HF_ENDPOINT: https://hf-mirror.com\n volumes:\n - chroma_data:/app/chroma_db\n - chroma_blogger_data:/app/chroma_blogger\n - chroma_l1_data:/app/chroma_l1_faq\n - chroma_l2_data:/app/chroma_l2_core\n - chroma_l3_data:/app/chroma_l3_books\n - ./cleaned-data:/data/cleaned-data:ro\n - ./blogger-data:/data/blogger-data:ro\n - ./l1-faq:/data/l1-faq:ro\n - ./l2-core:/data/l2-core:ro\n - ./l3-books:/data/l3-books:ro\n\n wechat-bridge:\n build:\n context: ./wechat-bridge\n dockerfile: Dockerfile\n container_name: rn-wechat-bridge\n restart: unless-stopped\n environment:\n INTERNAL_API_TOKEN: ${INTERNAL_API_TOKEN:-}\n RIGHTNOW_API_BASE: http://backend:5000/api\n PUSH_PORT: \"3000\"\n depends_on:\n - backend\n ports:\n - \"127.0.0.1:3000:3000\"\n volumes:\n - ./wechat-bridge/data:/app/data\n\n frontend:\n build:\n context: .\n dockerfile: Dockerfile.frontend\n container_name: rn-frontend\n restart: unless-stopped\n ports:\n - \"80:80\"\n - \"443:443\"\n volumes:\n - ./nginx.conf:/etc/nginx/nginx.conf:ro\n - ./letsencrypt:/etc/letsencrypt:ro\n - ./certbot-www:/var/www/certbot\n depends_on:\n - backend\n\n openclaw-gateway:\n build:\n context: ./openclaw\n dockerfile: Dockerfile\n args:\n OPENCLAW_EXTENSIONS: \"stepfun,deepseek,memory-core,feishu,rightnow\"\n OPENCLAW_BUNDLED_PLUGIN_DIR: extensions\n container_name: rn-openclaw-gateway\n user: \"root\"\n restart: unless-stopped\n environment:\n OPENCLAW_MODEL: stepfun/step-3.7-flash\n STEPFUN_API_KEY: ${STEPFUN_API_KEY}\n STEPFUN_BASE_URL: ${STEPFUN_BASE_URL:-https://api.stepfun.com/v1}\n OPENCLAW_GATEWAY_BIND: lan\n OPENCLAW_GATEWAY_PORT: \"18789\"\n OPENCLAW_GATEWAY_AUTH_MODE: token\n OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN}\n OPENCLAW_WORKSPACE_DIR: /root/.openclaw/workspace\n RIGHTNOW_API_BASE: http://rn-backend:5000/api\n AGENT_SERVICE_TOKEN: ${AGENT_SERVICE_TOKEN}\n ports:\n - \"127.0.0.1:18789:18789\"\n volumes:\n - /root/.openclaw:/root/.openclaw\n healthcheck:\n test: [\"CMD-SHELL\", \"node -e \\\"fetch('http://127.0.0.1:18789/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\\\"\"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 30s\n\nvolumes:\n pgdata:\n uploads:\n chroma_data:\n chroma_blogger_data:\n chroma_l1_data:\n chroma_l2_data:\n chroma_l3_data:\n"}}}}