3
0

5 Ревизии bdb9b21daa ... 7592fee901

Автор SHA1 Съобщение Дата
  lkatzey 7592fee901 revert(deploy): 恢复 Dockerfile 阿里云 apt/pip 镜像源 преди 3 дни
  lkatzey 28f40b5073 fix(deploy): 容器以 node 用户运行,修复 output/jobs 卷权限 преди 3 дни
  lkatzey 6706b8c9c9 Merge branch 'main' of ssh://gogs.corp.shuidi.tech:30022/incubator/pipeline преди 4 дни
  lkatzey 6b7ffb8ef0 fix(deploy): 透传全部环境变量到容器 преди 4 дни
  lkatzey 8564d3b3fe fix(web): 读取运行期环境变量,修复容器内设置页为空 преди 4 дни
променени са 6 файла, в които са добавени 72 реда и са изтрити 46 реда
  1. 3 0
      .gitignore
  2. 4 4
      Dockerfile
  3. 13 4
      apps/web/next.config.mjs
  4. 25 8
      apps/web/src/app/api/settings/route.ts
  5. 6 0
      deploy/k8s/01-configmap.yaml
  6. 21 30
      docker-compose.yml

+ 3 - 0
.gitignore

@@ -19,5 +19,8 @@ next-env.d.ts
 output/
 tmp/
 
+# Docker bind-mounted runtime data (jobs store, pre-created as uid 1000)
+.data/
+
 # Claude Code
 .claude/settings.local.json

+ 4 - 4
Dockerfile

@@ -2,7 +2,7 @@
 FROM node:22-bookworm AS deps
 
 # 使用阿里云镜像源
-# RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources
+RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources
 
 RUN corepack enable && corepack prepare pnpm@10.24.0 --activate
 
@@ -10,7 +10,7 @@ RUN apt-get update && \
     apt-get install -y --no-install-recommends ffmpeg python3 python3-pip && \
     rm -rf /var/lib/apt/lists/*
 
-RUN pip3 install --break-system-packages faster-whisper
+RUN pip3 install --break-system-packages -i https://mirrors.aliyun.com/pypi/simple/ faster-whisper
 
 WORKDIR /app
 
@@ -35,7 +35,7 @@ RUN pnpm build
 # Stage 3: Runtime — fresh prod install with hoisted layout
 FROM node:22-bookworm-slim AS runtime
 
-# RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources
+RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources
 
 RUN corepack enable && corepack prepare pnpm@10.24.0 --activate
 
@@ -47,7 +47,7 @@ RUN apt-get update && \
       libgbm1 libpango-1.0-0 libcairo2 libasound2 && \
     rm -rf /var/lib/apt/lists/*
 
-RUN pip3 install --break-system-packages faster-whisper
+RUN pip3 install --break-system-packages -i https://mirrors.aliyun.com/pypi/simple/ faster-whisper
 
 WORKDIR /app
 

+ 13 - 4
apps/web/next.config.mjs

@@ -1,9 +1,18 @@
 import { existsSync, readFileSync } from "node:fs";
 import { resolve } from "node:path";
 
-// Load .env from monorepo root at build/start time
+// Expose the monorepo-root .env to server-side `process.env.*` reads.
+//
+// IMPORTANT: do NOT put these into `nextConfig.env`. The `env` config key is
+// inlined into the JS bundle at BUILD time, which (a) bakes build-time values
+// into the image and ignores the container's runtime environment
+// (docker-compose `environment:` / k8s envFrom), and (b) inlines secrets into
+// the bundle. Assigning to process.env here runs at config-load (build and
+// `next start`); in the container there is no .env (it isn't copied into the
+// runtime image — see .dockerignore), so this is a no-op there and the
+// container's injected env vars are used as-is. Real/container env always wins
+// — .env only fills keys that are still undefined.
 const envPath = resolve(import.meta.dirname, "../../.env");
-const envVars = {};
 if (existsSync(envPath)) {
   const content = readFileSync(envPath, "utf-8");
   for (const line of content.split("\n")) {
@@ -12,15 +21,15 @@ if (existsSync(envPath)) {
     const eq = trimmed.indexOf("=");
     if (eq < 0) continue;
     const key = trimmed.slice(0, eq).trim();
+    if (!key) continue;
     const val = trimmed.slice(eq + 1).trim();
-    envVars[key] = val;
+    if (process.env[key] === undefined) process.env[key] = val;
   }
 }
 
 /** @type {import('next').NextConfig} */
 const nextConfig = {
   transpilePackages: ["@pipeline/shared", "@pipeline/core", "@pipeline/tts"],
-  env: envVars,
 };
 
 export default nextConfig;

+ 25 - 8
apps/web/src/app/api/settings/route.ts

@@ -58,15 +58,23 @@ function serializeEnv(vars: Record<string, string>, original: string): string {
   return updated.join("\n");
 }
 
+// Keys shown on the Settings page. GET reads these from the LIVE environment
+// (container-injected vars; or .env via next.config in dev) — NOT the .env
+// file, which does not exist in the container. Reading process.env is what
+// makes Settings reflect the actual runtime config instead of appearing empty.
+const SETTINGS_KEYS = [
+  "OPENAI_API_KEY", "OPENAI_BASE_URL",
+  "OPENAI_TTS_API_KEY", "OPENAI_TTS_BASE_URL", "OPENAI_TTS_MODEL",
+  "FISH_AUDIO_API_KEY", "MINIMAX_API_KEY", "MINIMAX_GROUP_ID", "ELEVENLABS_API_KEY",
+  "OSS_REGION", "OSS_BUCKET", "OSS_ACCESS_KEY_ID", "OSS_ACCESS_KEY_SECRET", "OSS_PUBLIC_DOMAIN",
+  "FEISHU_WEBHOOK_URL", "FEISHU_WEBHOOK_SECRET",
+] as const;
+
 export async function GET() {
-  if (!existsSync(ENV_PATH)) {
-    return NextResponse.json({});
-  }
-  const content = readFileSync(ENV_PATH, "utf-8");
-  const parsed = parseEnv(content);
   const masked: Record<string, string> = {};
-  for (const [key, val] of Object.entries(parsed)) {
-    masked[key] = maskValue(key, val);
+  for (const key of SETTINGS_KEYS) {
+    const raw = process.env[key];
+    masked[key] = raw === undefined ? "" : maskValue(key, raw);
   }
   return NextResponse.json(masked);
 }
@@ -89,7 +97,16 @@ export async function POST(request: Request) {
   }
 
   const serialized = serializeEnv(filtered, original);
-  writeFileSync(ENV_PATH, serialized, "utf-8");
+  try {
+    writeFileSync(ENV_PATH, serialized, "utf-8");
+  } catch (e) {
+    // In a container the env is injected externally and .env is usually not
+    // writable (k8s runs as non-root) — surface that clearly instead of a 500.
+    return NextResponse.json(
+      { ok: false, error: `无法写入 .env(容器内环境变量通常由外部注入,不可在此修改): ${(e as Error).message}` },
+      { status: 403 },
+    );
+  }
 
   // Re-read to return masked values
   const reparsed = parseEnv(serialized);

+ 6 - 0
deploy/k8s/01-configmap.yaml

@@ -14,3 +14,9 @@ data:
   TZ: Asia/Shanghai
   LOG_LEVEL: info
   NODE_ENV: production
+  # Scheduled renders — the in-process scheduler (instrumentation + node-cron)
+  # reads `schedules` from config/default.yaml by default (baked into the
+  # image). Override with SCHEDULES (JSON array, same shape) or disable the
+  # scheduler with SCHEDULER_ENABLED=false.
+  SCHEDULER_ENABLED: "true"
+  # SCHEDULES: '[{"cron":"50 7 * * *","template":"github-trending","platform":"bilibili","source":"github-trending","publish":true}]'

+ 21 - 30
docker-compose.yml

@@ -1,41 +1,35 @@
 services:
   pipeline:
     build: .
-    # The image runs as the non-root `node` user for Kubernetes. For local dev
-    # we override back to root so the ./output bind mount keeps working
-    # regardless of the host user's uid; Kubernetes sets fsGroup instead.
-    user: "0:0"
+    # Run as the image's non-root `node` user by default (uid 1000 == host
+    # `lkattey` here, and the same user Kubernetes runs the image as, so local
+    # compose mirrors prod's filesystem ownership). Bind-mounted files (./output,
+    # ./.data) therefore stay host-accessible instead of root-owned. On a host
+    # whose uid isn't 1000, set HOST_UID/HOST_GID (in .env or the shell) to your
+    # numeric uid:gid so the bind mounts match your user.
+    user: "${HOST_UID:-node}:${HOST_GID:-node}"
     ports:
       - "${PORT:-13000}:3000"
+    # Pass EVERY variable from .env (API keys, OSS, Feishu, SCHEDULES,
+    # SCHEDULER_ENABLED, LOG_LEVEL, …) into the container automatically via
+    # env_file — no per-variable list to maintain. The previous explicit list
+    # silently dropped any var not enumerated, so new knobs set in .env never
+    # reached the container. `environment` below only adds container-specific
+    # defaults for keys that may be absent from .env, and overrides env_file for
+    # those keys.
+    env_file:
+      - .env
     environment:
-      - OPENAI_API_KEY=${OPENAI_API_KEY}
-      - OPENAI_BASE_URL=${OPENAI_BASE_URL}
-      - OPENAI_TTS_API_KEY=${OPENAI_TTS_API_KEY}
-      - OPENAI_TTS_BASE_URL=${OPENAI_TTS_BASE_URL}
-      - FISH_AUDIO_API_KEY=${FISH_AUDIO_API_KEY}
-      - MINIMAX_API_KEY=${MINIMAX_API_KEY}
-      - MINIMAX_GROUP_ID=${MINIMAX_GROUP_ID}
-      - ELEVENLABS_API_KEY=${ELEVENLABS_API_KEY}
-      # Unified output + cache retention
       - OUTPUT_DIR=${OUTPUT_DIR:-/app/output}
       - OUTPUT_RETENTION_DAYS=${OUTPUT_RETENTION_DAYS:-30}
-      # Container timezone — keeps logs, ffmpeg, and new Date() on Beijing time
-      # so the daily-trending cover date never rolls over a day early/late.
       - TZ=${TZ:-Asia/Shanghai}
-      # OSS (Alibaba Cloud Object Storage) — publish stage
-      - OSS_ACCESS_KEY_ID=${OSS_ACCESS_KEY_ID:-}
-      - OSS_ACCESS_KEY_SECRET=${OSS_ACCESS_KEY_SECRET:-}
-      - OSS_REGION=${OSS_REGION:-}
-      - OSS_BUCKET=${OSS_BUCKET:-}
-      - OSS_ENDPOINT=${OSS_ENDPOINT:-}
-      - OSS_PATH=${OSS_PATH:-}
-      - OSS_PUBLIC_DOMAIN=${OSS_PUBLIC_DOMAIN:-}
-      # Feishu custom-bot webhook — publish stage
-      - FEISHU_WEBHOOK_URL=${FEISHU_WEBHOOK_URL:-}
-      - FEISHU_WEBHOOK_SECRET=${FEISHU_WEBHOOK_SECRET:-}
+      - TIMEZONE=${TIMEZONE:-Asia/Shanghai}
     volumes:
       - ./output:/app/output
-      - pipeline-jobs:/tmp/pipeline-jobs
+      # Bind mount (pre-created as uid 1000 on the host) instead of a named
+      # volume: named volumes are root-owned, which the non-root container
+      # (uid 1000) below couldn't write to. .data is gitignored.
+      - ./.data/pipeline-jobs:/tmp/pipeline-jobs
     # Use public DNS resolvers directly. The container inherits the host's
     # resolver, which can silently fail to resolve some public domains (e.g.
     # open.feishu.cn behind a VPN/proxy) and break the Feishu webhook. Override
@@ -44,6 +38,3 @@ services:
       - ${DNS_SERVER_1:-223.5.5.5}
       - ${DNS_SERVER_2:-114.114.114.114}
     restart: unless-stopped
-
-volumes:
-  pipeline-jobs: