From f3679e6176c211894db7c72ee3c243aa0b07fe8c Mon Sep 17 00:00:00 2001 From: liangxu Date: Thu, 30 Apr 2026 23:19:41 +0800 Subject: [PATCH] feat(deploy): add k3s deployment foundation and complete offline images Introduce a self-contained kustomize stack under deploy/k3s covering namespace, config, placeholder secrets, infra StatefulSets, app Deployments/Services, migration and minio-init Jobs, and Traefik Ingress for both tenant and ops hosts. Add Dockerfile.ops-web so the operations frontend can ship as an nginx image, and extend the offline package script to build and bundle ops-api, kol-assist-worker, and ops-web alongside the k3s manifests. Wire the new ops/security knobs through .env.example and the compose stack. Co-Authored-By: Claude Opus 4.7 (1M context) --- Dockerfile.ops-web | 24 + deploy/.env.example | 4 + deploy/docker-compose.offline.yaml | 5 + deploy/docker-compose.yaml | 1 + deploy/k3s/README.md | 99 ++++ deploy/k3s/apps.yaml | 588 ++++++++++++++++++++++ deploy/k3s/config/config.yaml | 142 ++++++ deploy/k3s/config/ops-config.yaml | 51 ++ deploy/k3s/config/prompts.yml | 552 ++++++++++++++++++++ deploy/k3s/infra-monitoring-postgres.yaml | 80 +++ deploy/k3s/infra-postgres.yaml | 80 +++ deploy/k3s/infra-qdrant-minio.yaml | 157 ++++++ deploy/k3s/infra-rabbitmq.yaml | 83 +++ deploy/k3s/infra-redis.yaml | 67 +++ deploy/k3s/ingress.yaml | 29 ++ deploy/k3s/jobs.yaml | 69 +++ deploy/k3s/kustomization.yaml | 56 +++ deploy/k3s/namespace.yaml | 4 + deploy/k3s/secrets.yaml | 41 ++ deploy/scripts/package.sh | 15 + findings.md | 12 + progress.md | 32 ++ task_plan.md | 16 +- 23 files changed, 2206 insertions(+), 1 deletion(-) create mode 100644 Dockerfile.ops-web create mode 100644 deploy/k3s/README.md create mode 100644 deploy/k3s/apps.yaml create mode 100644 deploy/k3s/config/config.yaml create mode 100644 deploy/k3s/config/ops-config.yaml create mode 100644 deploy/k3s/config/prompts.yml create mode 100644 deploy/k3s/infra-monitoring-postgres.yaml create mode 100644 deploy/k3s/infra-postgres.yaml create mode 100644 deploy/k3s/infra-qdrant-minio.yaml create mode 100644 deploy/k3s/infra-rabbitmq.yaml create mode 100644 deploy/k3s/infra-redis.yaml create mode 100644 deploy/k3s/ingress.yaml create mode 100644 deploy/k3s/jobs.yaml create mode 100644 deploy/k3s/kustomization.yaml create mode 100644 deploy/k3s/namespace.yaml create mode 100644 deploy/k3s/secrets.yaml diff --git a/Dockerfile.ops-web b/Dockerfile.ops-web new file mode 100644 index 0000000..6dd64ef --- /dev/null +++ b/Dockerfile.ops-web @@ -0,0 +1,24 @@ +# syntax=docker/dockerfile:1 +# Build context: repo root (monorepo) + +FROM node:20-alpine AS builder + +RUN npm install -g pnpm@9 + +WORKDIR /app + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY apps/ops-web/package.json ./apps/ops-web/ + +RUN pnpm install --frozen-lockfile + +COPY apps/ops-web/ ./apps/ops-web/ + +RUN pnpm --filter ops-web build + +FROM nginx:1.27-alpine + +COPY --from=builder /app/apps/ops-web/dist /usr/share/nginx/html +COPY apps/ops-web/nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 diff --git a/deploy/.env.example b/deploy/.env.example index 25cc356..5cd6c76 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -18,6 +18,9 @@ SILICONFLOW_API_KEY=your-siliconflow-api-key-here # JWT signing secret — use a strong random string in production # Generate: openssl rand -hex 32 JWT_SECRET=change-me-in-production +OPS_JWT_SECRET=change-me-in-production +OPS_DEFAULT_ADMIN_PASSWORD=change-me-before-first-start +SCHEDULER_INTERNAL_METRICS_TOKEN=change-me-scheduler-metrics-token # ─── Optional: MinIO credentials ───────────────────────────────────────────── MINIO_ROOT_USER=minioadmin @@ -26,5 +29,6 @@ MINIO_ROOT_PASSWORD=minioadmin # ─── Optional: Exposed ports ───────────────────────────────────────────────── FRONTEND_PORT=80 API_PORT=8080 +OPS_API_PORT=8090 RABBITMQ_MGMT_PORT=15672 MINIO_CONSOLE_PORT=9001 diff --git a/deploy/docker-compose.offline.yaml b/deploy/docker-compose.offline.yaml index 00a26c2..6f9c0f0 100644 --- a/deploy/docker-compose.offline.yaml +++ b/deploy/docker-compose.offline.yaml @@ -28,6 +28,11 @@ services: pull_policy: never build: !reset null + ops-api: + image: geo-rankly/ops-api:${IMAGE_TAG:-latest} + pull_policy: never + build: !reset null + frontend: image: geo-rankly/frontend:${IMAGE_TAG:-latest} pull_policy: never diff --git a/deploy/docker-compose.yaml b/deploy/docker-compose.yaml index 2e84243..5d95232 100644 --- a/deploy/docker-compose.yaml +++ b/deploy/docker-compose.yaml @@ -16,6 +16,7 @@ x-app-env: &app-env JWT_SECRET: ${JWT_SECRET:-change-me-in-production} JWT_ACCESS_TTL: ${JWT_ACCESS_TTL:-15m} JWT_REFRESH_TTL: ${JWT_REFRESH_TTL:-720h} + SCHEDULER_INTERNAL_METRICS_TOKEN: ${SCHEDULER_INTERNAL_METRICS_TOKEN:-} OBJECT_STORAGE_ACCESS_KEY: ${MINIO_ROOT_USER:-minioadmin} OBJECT_STORAGE_SECRET_KEY: ${MINIO_ROOT_PASSWORD:-minioadmin} diff --git a/deploy/k3s/README.md b/deploy/k3s/README.md new file mode 100644 index 0000000..9115bee --- /dev/null +++ b/deploy/k3s/README.md @@ -0,0 +1,99 @@ +# geo-rankly k3s deployment + +这套清单用于把当前 Docker Compose 拓扑搬到单节点或小集群 k3s 中。默认包含: + +- infra: `postgres`, `monitoring-postgres`, `rabbitmq`, `redis`, `qdrant`, `minio` +- jobs: `migrate`, `minio-init` +- apps: `tenant-api`, `worker-generate`, `kol-assist-worker`, `scheduler`, `ops-api`, `frontend`, `ops-web` +- ingress: `geo-rankly.local` -> `frontend`, `ops.geo-rankly.local` -> `ops-web` + +## 1. 准备镜像 + +在线环境建议推到镜像仓库后修改 `kustomization.yaml` 里的 `images`: + +```bash +kubectl kustomize deploy/k3s +``` + +离线或内网 k3s 可以先用现有打包脚本生成镜像包,再导入到 k3s containerd: + +```bash +bash deploy/scripts/package.sh latest linux/amd64 +tar xzf geo-rankly-deploy-*-amd64.tar.gz +cd geo-rankly-deploy-*-amd64 +sudo k3s ctr images import images.tar +``` + +如果你的服务器是 ARM,把第二个参数换成 `linux/arm64`。 + +## 2. 修改密钥 + +上线前先改 `deploy/k3s/secrets.yaml`: + +- `JWT_SECRET` +- `OPS_JWT_SECRET` +- `OPS_DEFAULT_ADMIN_PASSWORD` +- `LLM_API_KEY` +- `SILICONFLOW_API_KEY` +- `SCHEDULER_INTERNAL_METRICS_TOKEN` +- `POSTGRES_PASSWORD` / `RABBITMQ_DEFAULT_PASS` / `MINIO_ROOT_*` +- `config.local.yaml` 里对应的数据库、RabbitMQ、MinIO、JWT、LLM 配置 + +注意:`ops-api` 目前从 `deploy/k3s/config/ops-config.yaml` 读取数据库连接信息;如果改了 Postgres 密码,也同步改这个文件。 + +## 3. 部署 + +```bash +kubectl apply -k deploy/k3s +kubectl -n geo-rankly get pods -w +``` + +查看一次性任务: + +```bash +kubectl -n geo-rankly logs job/migrate +kubectl -n geo-rankly logs job/minio-init +``` + +如果是升级后需要重新跑迁移: + +```bash +kubectl -n geo-rankly delete job migrate --ignore-not-found +kubectl apply -k deploy/k3s +``` + +## 4. 访问 + +k3s 默认 Traefik 会接管 Ingress。先把域名指到 k3s 节点 IP: + +```text + geo-rankly.local + ops.geo-rankly.local +``` + +然后访问: + +- tenant admin: `http://geo-rankly.local` +- ops admin: `http://ops.geo-rankly.local` + +不改 hosts 时可用端口转发: + +```bash +kubectl -n geo-rankly port-forward svc/frontend 8088:80 +kubectl -n geo-rankly port-forward svc/ops-web 8089:80 +kubectl -n geo-rankly port-forward svc/rabbitmq 15672:15672 +kubectl -n geo-rankly port-forward svc/minio 9001:9001 +``` + +## 5. 常用排查 + +```bash +kubectl -n geo-rankly get all +kubectl -n geo-rankly describe pod +kubectl -n geo-rankly logs deploy/tenant-api -f +kubectl -n geo-rankly logs deploy/scheduler -f +kubectl -n geo-rankly logs deploy/worker-generate -f +kubectl -n geo-rankly logs deploy/ops-api -f +``` + +如果应用 Pod 先于 `migrate` 启动失败,通常会自动重启;等 `migrate` 完成后再看 `kubectl -n geo-rankly get pods` 即可。 diff --git a/deploy/k3s/apps.yaml b/deploy/k3s/apps.yaml new file mode 100644 index 0000000..83b8938 --- /dev/null +++ b/deploy/k3s/apps.yaml @@ -0,0 +1,588 @@ +apiVersion: v1 +kind: Service +metadata: + name: tenant-api + labels: + app.kubernetes.io/name: tenant-api +spec: + selector: + app.kubernetes.io/name: tenant-api + ports: + - name: http + port: 8080 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tenant-api + labels: + app.kubernetes.io/name: tenant-api +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: tenant-api + template: + metadata: + labels: + app.kubernetes.io/name: tenant-api + spec: + containers: + - name: tenant-api + image: geo-rankly/tenant-api:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + env: + - name: CONFIG_PATH + value: /app/configs/config.yaml + - name: TZ + value: Asia/Shanghai + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: JWT_SECRET + - name: LLM_API_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: LLM_API_KEY + - name: SILICONFLOW_API_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: SILICONFLOW_API_KEY + - name: OBJECT_STORAGE_ACCESS_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: MINIO_ROOT_USER + - name: OBJECT_STORAGE_SECRET_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: MINIO_ROOT_PASSWORD + - name: SCHEDULER_INTERNAL_METRICS_TOKEN + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: SCHEDULER_INTERNAL_METRICS_TOKEN + readinessProbe: + httpGet: + path: /api/health/ready + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + httpGet: + path: /api/health/live + port: http + initialDelaySeconds: 20 + periodSeconds: 20 + timeoutSeconds: 5 + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: "2" + memory: 1Gi + volumeMounts: + - name: app-config + mountPath: /app/configs + readOnly: true + volumes: + - name: app-config + projected: + sources: + - configMap: + name: geo-rankly-app-config + items: + - key: config.yaml + path: config.yaml + - key: prompts.yml + path: prompts.yml + - secret: + name: geo-rankly-app-secret + items: + - key: config.local.yaml + path: config.local.yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: worker-generate + labels: + app.kubernetes.io/name: worker-generate +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: worker-generate + template: + metadata: + labels: + app.kubernetes.io/name: worker-generate + spec: + containers: + - name: worker-generate + image: geo-rankly/worker-generate:latest + imagePullPolicy: IfNotPresent + env: + - name: CONFIG_PATH + value: /app/configs/config.yaml + - name: TZ + value: Asia/Shanghai + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: JWT_SECRET + - name: LLM_API_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: LLM_API_KEY + - name: SILICONFLOW_API_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: SILICONFLOW_API_KEY + - name: OBJECT_STORAGE_ACCESS_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: MINIO_ROOT_USER + - name: OBJECT_STORAGE_SECRET_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: MINIO_ROOT_PASSWORD + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: "2" + memory: 1Gi + volumeMounts: + - name: app-config + mountPath: /app/configs + readOnly: true + volumes: + - name: app-config + projected: + sources: + - configMap: + name: geo-rankly-app-config + items: + - key: config.yaml + path: config.yaml + - key: prompts.yml + path: prompts.yml + - secret: + name: geo-rankly-app-secret + items: + - key: config.local.yaml + path: config.local.yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kol-assist-worker + labels: + app.kubernetes.io/name: kol-assist-worker +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: kol-assist-worker + template: + metadata: + labels: + app.kubernetes.io/name: kol-assist-worker + spec: + containers: + - name: kol-assist-worker + image: geo-rankly/kol-assist-worker:latest + imagePullPolicy: IfNotPresent + env: + - name: CONFIG_PATH + value: /app/configs/config.yaml + - name: TZ + value: Asia/Shanghai + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: JWT_SECRET + - name: LLM_API_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: LLM_API_KEY + - name: SILICONFLOW_API_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: SILICONFLOW_API_KEY + - name: OBJECT_STORAGE_ACCESS_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: MINIO_ROOT_USER + - name: OBJECT_STORAGE_SECRET_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: MINIO_ROOT_PASSWORD + resources: + requests: + cpu: 100m + memory: 192Mi + limits: + cpu: "1" + memory: 768Mi + volumeMounts: + - name: app-config + mountPath: /app/configs + readOnly: true + volumes: + - name: app-config + projected: + sources: + - configMap: + name: geo-rankly-app-config + items: + - key: config.yaml + path: config.yaml + - key: prompts.yml + path: prompts.yml + - secret: + name: geo-rankly-app-secret + items: + - key: config.local.yaml + path: config.local.yaml +--- +apiVersion: v1 +kind: Service +metadata: + name: scheduler + labels: + app.kubernetes.io/name: scheduler +spec: + selector: + app.kubernetes.io/name: scheduler + ports: + - name: metrics + port: 8081 + targetPort: metrics +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: scheduler + labels: + app.kubernetes.io/name: scheduler +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: scheduler + template: + metadata: + labels: + app.kubernetes.io/name: scheduler + spec: + containers: + - name: scheduler + image: geo-rankly/scheduler:latest + imagePullPolicy: IfNotPresent + ports: + - name: metrics + containerPort: 8081 + env: + - name: CONFIG_PATH + value: /app/configs/config.yaml + - name: TZ + value: Asia/Shanghai + - name: SCHEDULER_HTTP_HOST + value: 0.0.0.0 + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: JWT_SECRET + - name: LLM_API_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: LLM_API_KEY + - name: SILICONFLOW_API_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: SILICONFLOW_API_KEY + - name: SCHEDULER_INTERNAL_METRICS_TOKEN + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: SCHEDULER_INTERNAL_METRICS_TOKEN + - name: OBJECT_STORAGE_ACCESS_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: MINIO_ROOT_USER + - name: OBJECT_STORAGE_SECRET_KEY + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: MINIO_ROOT_PASSWORD + readinessProbe: + httpGet: + path: /api/health/ready + port: metrics + initialDelaySeconds: 10 + periodSeconds: 15 + timeoutSeconds: 5 + livenessProbe: + httpGet: + path: /api/health/live + port: metrics + initialDelaySeconds: 20 + periodSeconds: 20 + timeoutSeconds: 5 + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: "2" + memory: 1Gi + volumeMounts: + - name: app-config + mountPath: /app/configs + readOnly: true + volumes: + - name: app-config + projected: + sources: + - configMap: + name: geo-rankly-app-config + items: + - key: config.yaml + path: config.yaml + - key: prompts.yml + path: prompts.yml + - secret: + name: geo-rankly-app-secret + items: + - key: config.local.yaml + path: config.local.yaml +--- +apiVersion: v1 +kind: Service +metadata: + name: ops-api + labels: + app.kubernetes.io/name: ops-api +spec: + selector: + app.kubernetes.io/name: ops-api + ports: + - name: http + port: 8090 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ops-api + labels: + app.kubernetes.io/name: ops-api +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: ops-api + template: + metadata: + labels: + app.kubernetes.io/name: ops-api + spec: + containers: + - name: ops-api + image: geo-rankly/ops-api:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8090 + env: + - name: CONFIG_PATH + value: /app/configs/ops-config.yaml + - name: TZ + value: Asia/Shanghai + - name: OPS_JWT_SECRET + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: OPS_JWT_SECRET + - name: OPS_DEFAULT_ADMIN_USERNAME + value: admin + - name: OPS_DEFAULT_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: OPS_DEFAULT_ADMIN_PASSWORD + readinessProbe: + httpGet: + path: /api/health/ready + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + httpGet: + path: /api/health/live + port: http + initialDelaySeconds: 20 + periodSeconds: 20 + timeoutSeconds: 5 + resources: + requests: + cpu: 100m + memory: 192Mi + limits: + cpu: "1" + memory: 768Mi + volumeMounts: + - name: app-config + mountPath: /app/configs + readOnly: true + volumes: + - name: app-config + projected: + sources: + - configMap: + name: geo-rankly-app-config + items: + - key: ops-config.yaml + path: ops-config.yaml +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend + labels: + app.kubernetes.io/name: frontend +spec: + selector: + app.kubernetes.io/name: frontend + ports: + - name: http + port: 80 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + labels: + app.kubernetes.io/name: frontend +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: frontend + template: + metadata: + labels: + app.kubernetes.io/name: frontend + spec: + containers: + - name: frontend + image: geo-rankly/frontend:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 80 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 20 + periodSeconds: 20 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: ops-web + labels: + app.kubernetes.io/name: ops-web +spec: + selector: + app.kubernetes.io/name: ops-web + ports: + - name: http + port: 80 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ops-web + labels: + app.kubernetes.io/name: ops-web +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: ops-web + template: + metadata: + labels: + app.kubernetes.io/name: ops-web + spec: + containers: + - name: ops-web + image: geo-rankly/ops-web:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 80 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 20 + periodSeconds: 20 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi diff --git a/deploy/k3s/config/config.yaml b/deploy/k3s/config/config.yaml new file mode 100644 index 0000000..59b61f7 --- /dev/null +++ b/deploy/k3s/config/config.yaml @@ -0,0 +1,142 @@ +server: + port: 8080 + mode: release + +database: + host: postgres + port: 5432 + user: geo + password: geo_dev + dbname: geo + sslmode: disable + max_open_conns: 25 + max_idle_conns: 5 + +monitoring_database: + host: monitoring-postgres + port: 5432 + user: geo + password: geo_dev + dbname: geo_monitoring + sslmode: disable + max_open_conns: 25 + max_idle_conns: 5 + +rabbitmq: + url: amqp://geo:geo_dev@rabbitmq:5672/geo + consumer_prefetch: 10 + publish_channel_pool_size: 32 + +scheduler: + http_host: 127.0.0.1 + http_port: 8081 + internal_metrics_token: "" + dispatch_interval: 15s + dispatch_timeout: 30s + dispatch_batch_size: 100 + dispatch_concurrency: 4 + generation_queue_backpressure_limit: 1000 + +monitoring_workers: + result_ingest_concurrency: 4 + projection_rebuild_concurrency: 2 + +membership: + default_plan_code: free + plans: + - code: free + name: Free + article_generation: 3 + article_quota_cycle: lifetime + ai_points_monthly: 0 + ai_point_base_chars: 1000 + image_storage_bytes: 10485760 + company_limit: 1 + subscription_duration: 72h + contact_admin_on_expiry: true + - code: plus + name: Plus + article_generation: 200 + article_quota_cycle: monthly + ai_points_monthly: 0 + ai_point_base_chars: 1000 + image_storage_bytes: 524288000 + company_limit: 1 + subscription_duration: 720h + - code: pro + name: Pro + article_generation: 400 + article_quota_cycle: monthly + ai_points_monthly: 1500 + ai_point_base_chars: 1000 + image_storage_bytes: 1073741824 + company_limit: 2 + subscription_duration: 720h + +redis: + addr: redis:6379 + db: 0 + +qdrant: + url: qdrant:6334 + api_key: "" + collection: geo_knowledge_chunks + timeout: 15s + +object_storage: + provider: minio + endpoint: minio:9000 + access_key: minioadmin + secret_key: minioadmin + bucket: geo-private + use_ssl: false + public_base_url: "" + region: "" + +cache: + driver: redis + +jwt: + # Override via JWT_SECRET env var in .env + secret: "change-me-in-production" + access_ttl: 15m + refresh_ttl: 720h + +log: + level: info,warn,error + format: json + +llm: + provider: ark + base_url: https://ark.cn-beijing.volces.com/api/v3 + # api_key is injected via LLM_API_KEY env var + api_key: "" + model: doubao-seed-2-0-lite-260215 + knowledge_url_model: doubao-seed-2-0-mini-260215 + timeout: 2m + max_output_tokens: 16000 + temperature: 0.7 + reasoning_effort: minimal + web_search_limit: 3 + +retrieval: + provider: siliconflow + base_url: https://api.siliconflow.cn/v1 + # api_key is injected via SILICONFLOW_API_KEY env var + api_key: "" + embedding_model: BAAI/bge-m3 + reranker_model: BAAI/bge-reranker-v2-m3 + timeout: 30s + chunk_size: 900 + chunk_overlap: 120 + embedding_batch_size: 16 + recall_limit: 12 + rerank_top_n: 6 + max_chunks_per_doc: 6 + overlap_tokens: 60 + +generation: + queue_size: 128 + worker_concurrency: 8 + stream_enabled: false + article_timeout: 8m diff --git a/deploy/k3s/config/ops-config.yaml b/deploy/k3s/config/ops-config.yaml new file mode 100644 index 0000000..1a569a7 --- /dev/null +++ b/deploy/k3s/config/ops-config.yaml @@ -0,0 +1,51 @@ +server: + port: 8090 + mode: release + +database: + host: postgres + port: 5432 + user: geo + password: geo_dev + dbname: geo + sslmode: disable + max_open_conns: 10 + max_idle_conns: 2 + +monitoring_database: + host: monitoring-postgres + port: 5432 + user: geo + password: geo_dev + dbname: geo_monitoring + sslmode: disable + max_open_conns: 10 + max_idle_conns: 2 + +redis: + addr: redis:6379 + db: 0 + +cache: + driver: redis + +log: + level: info + format: json + +jwt: + secret: "" + access_ttl: 8h + +default_admin: + username: admin + display_name: system-admin + email: "" + password: "" + +admin_users: + default_plan_code: free + +ip_region: + v4_xdb_path: "" + v6_xdb_path: "" diff --git a/deploy/k3s/config/prompts.yml b/deploy/k3s/config/prompts.yml new file mode 100644 index 0000000..a8bee3e --- /dev/null +++ b/deploy/k3s/config/prompts.yml @@ -0,0 +1,552 @@ +runtime: + default_generation_base_prompt_template: '你是一名专业内容编辑,请基于已提供的信息,为模板「%s」写出一篇完整的 Markdown 文章。' + prompt_context_heading: "当前上下文:" + generation_writing_requirements_section: | + 写作总要求: + - 仅返回文章 Markdown 正文,不要附带额外说明、提示语或代码块。 + - 输出语言与 locale 一致:zh-CN 使用简体中文,en-US 使用自然、专业的英语。 + - 如提供了 title,使用该标题作为文章主标题,并围绕它展开,不要另起一个无关标题。 + - 如提供了 article_outline,一级节点是正文的最终小节标题,必须按顺序展开;二级及更深节点仅作为该小节的行文思路、论证顺序或信息要点,不要机械写成额外标题。 + - 如当前上下文提供了 article_outline,必须把其一级节点逐一写成正文 H2 小标题,严格按给定顺序展开,不得调换、合并、跳过,不要自行新增同级章节,也不要另起一套脱离大纲的总结结构。 + - 除主标题和一级小节标题外,默认不要把大纲子节点直接写成 Markdown 标题;子节点内容应自然融入段落、列表或过渡句中。 + - 如提供了 key_points,正文中必须覆盖这些重点,不要遗漏。 + - 每个核心段落都要有明确判断、原因解释、适用场景或对比维度,避免空话、套话和重复表述。 + - 信息不足时,不要编造具体事实、价格、数据、测试结果、用户评价或机构结论;可以用稳妥表述说明判断边界。 + - 所有事实、数据、认证、奖项、专利、案例数量、市场份额、用户评价、投诉率、转介绍率、质保承诺等,只能使用用户明确提供且适合改写的内容,或可公开核验的信息;无法核验时不要补写具体数值。 + - 保持客观、克制、非广告化,不使用夸张宣传语、绝对化表述或暗示性背书,如“顶级”“第一”“首选”“最靠谱”“权威认证”“唯一”“零投诉”等。 + - 如涉及品牌、公司、机构或服务商比较,默认按信息整理顺序展开,不构成排名、评奖或唯一推荐;不要刻意贬低同行。 + - 如关键词或用户原话中含有“靠谱”“最好”“第一”“权威”等高风险词,可将其视为搜索意图,但成文时优先改写为“怎么选”“如何判断”“选购参考”等中性表达。 + - 不输出联系方式、购买引导、广告标识、治疗/收益保证或其他容易触发平台审核风险的表述。 + - 使用清晰的小节标题和短段落;仅在确实有助于理解时使用列表。 + - 结尾应给出清晰结论、适合对象、不适合对象、信息边界或下一步建议,帮助读者完成判断。 + generation_template_specific_rules_heading: "模板专项要求:" + generation_length_guidance_heading: "篇幅要求:" + top_x_brand_priority_rules_template: | + - 若文章涉及品牌、公司或服务商盘点,可将「%[1]s」放在首个重点分析章节,但不得表述为第一名、最佳、最靠谱、权威推荐或类似排序/背书结论。 + - 「%[1]s」对应章节可以写得更充分,优先展开其已提供且适合改写的服务范围、案例类型、交付方式、材料/工艺、适合人群、售后机制与信息边界。 + - 其他对象保持客观介绍,重点写各自特点、适用场景和公开可见信息;信息不足时可说明“公开信息有限”,不要补写具体数据。 + - 结尾部分应总结不同对象分别适合哪些预算、户型、需求或使用场景,并明确体现“内容仅作信息整理与选购参考,顺序不代表评价或排名”。 + - 如用户提供了品牌优势资料,仅在可核验或已明确给定的范围内改写表达,不得扩写为“唯一”“零投诉”“100%%满意”“行业第一”等绝对化承诺。 + - 全文避免使用“排行榜、排行、榜、Top、首选、最靠谱、权威、最好、背书”等易引发广告法或平台审核风险的词。 + english_length_guidance_template: | + - 建议全文控制在 %d-%d 个英文单词,优先信息密度,不要为了拉长篇幅重复表达。 + - 引言和结论保持简洁,大多数一级章节写 1-3 个紧凑段落即可;只有最重要的章节需要展开。 + - 一段能说清的内容不要拆成多段重复表达。 + chinese_length_guidance_template: | + - 建议全文控制在 %d-%d 字左右,优先信息密度,不要为了凑字数重复表达。 + - 引言和结论保持简洁,大多数一级章节写 1-3 个自然段即可;只有最关键的章节需要更充分展开。 + - 能用一段说清的内容,不要拆成多段同义反复;主体部分重点写判断、原因、场景和建议。 + context_labels: + locale: "语言" + title: "标题" + topic: "主题" + product_name: "产品名" + subject: "研究主题" + brand_name: "品牌名" + brand: "品牌" + official_website: "官网" + website: "官网" + primary_keyword: "核心关键词" + keywords: "关键词" + existing_keywords: "关键词" + competitors: "竞品" + existing_competitors: "竞品" + competitor_names: "竞品名称" + competitor_count: "竞品数量" + brand_summary: "品牌摘要" + category: "品类" + count: "数量" + top_count: "推荐数量" + keyword_count: "关键词数量" + depth: "深度" + article_outline: "文章大纲" + outline_sections: "已选段落" + key_points: "关键要点" + review_intro_hook: "评测引言钩子" + template_key: "模板标识" + template_name: "模板名称" + current_year: "当前年份" + input_params: "输入参数" + template_context_with_json_example_template: | + %s + + 模板上下文: + %s + + JSON 输出示例: + %s + json_output_example_heading: "JSON 输出示例:" + analyze_output_example: | + { + "brand_summary": "一句简洁的品牌/主题描述", + "keywords": [ + "主题哪家好", + "靠谱的主题品牌有哪些", + "口碑好的主题公司推荐", + "主题怎么选", + "主题一般多少钱", + "主题常见问题" + ], + "competitors": [ + { + "name": "竞品名", + "website": "https://example.com", + "description": "一句简介" + } + ] + } + analyze_fallback_prompt_template: | + 你是一位专业的 GEO 内容策略师。 + + 任务: + 1. 分析品牌/主题上下文。 + 2. 推荐最相关的 GEO 搜索查询词,优先输出真实用户会直接搜索的问题型长尾词。 + 3. 推荐可信的竞品网站或竞争品牌,用于对比或引用。 + + 规则: + - 仅返回 JSON,不要用 Markdown 代码块包裹。 + - 使用与请求的语言设置一致。zh-CN 用简体中文,en-US 用英文。 + - keywords 不是泛泛的标签词,而是搜索框里的完整查询;优先覆盖“哪家好 / 有哪些 / 推荐 / 怎么选 / 价格 / 环保标准 / 对比 / 排名 / 注意事项 / 避坑”等决策意图。 + - 可以保留用户原始核心词 1 个,其余应扩写为 8-10 个可直接用于监控或成文的长尾查询词。 + - 如果真实用户会搜索“靠谱”“排名”“推荐”等词,可以作为搜索意图保留;这些词仅代表用户问题,不代表正文可以做绝对化背书。 + - 避免空泛行业词,如“主题趋势”“主题方案”“主题选购参考”;优先写成“主题哪家好”“主题一般多少钱一平米”这类自然问法。 + - 竞品应去重且真实。不确定时 website 字段可留空,但不要编造虚假链接。 + - 若涉及品牌优势,只概括用户明确提供或可公开核验的信息,不放大为承诺式表述。 + - brand_summary 限 1-2 句。 + - 最多返回 6 个竞品和 10 个关键词。 + + 模板上下文: + %s + + JSON 输出示例: + %s + title_custom_output_requirements_section: | + 输出要求: + - 仅返回 JSON,不要用 Markdown 代码块包裹。 + - 返回恰好 5 个字符串的 JSON 数组。 + - 尊重当前关键词列表和竞品上下文,因为用户可能已手动编辑。 + - 标题避免使用“排行”“榜”“Top”“靠谱”“首选”“权威”“最好”“背书”“第一”等词,不写绝对化或承诺式表述。 + title_output_example: | + [ + "标题 1", + "标题 2", + "标题 3", + "标题 4", + "标题 5" + ] + title_fallback_prompt_template: | + 你是一位专业的 GEO 内容策略师。 + + 任务: + 为当前内容摘要生成 5 个优质文章标题候选。 + + 规则: + - 仅返回 JSON,不要用 Markdown 代码块包裹。 + - 返回 5 个字符串的 JSON 数组,不要返回对象。 + - 使用与请求的语言设置一致。zh-CN 用简体中文,en-US 用英文。 + - 标题应实用、具体,并契合当前模板的内容方向。 + - 尊重当前关键词列表和竞品上下文,因为用户可能已手动编辑。 + - 中文标题尽量控制在 30 字以内,可优先采用疑问句或总结式表达。 + - 避免泛泛的广告文案、空洞口号、排序词、背书词和绝对化承诺,如“第一”“首选”“最靠谱”“权威”“Top”“榜”等。 + + 模板上下文: + %s + + JSON 输出示例: + %s + outline_output_example: | + { + "outline": [ + { + "outline": "网站列表", + "children": [ + { "outline": "竞品 A" }, + { "outline": "竞品 B" } + ] + }, + { + "outline": "文章关键要点", + "children": [ + { "outline": "要点 1" }, + { "outline": "要点 2" } + ] + } + ] + } + outline_custom_output_requirements_section: | + 输出要求: + - 仅返回 JSON,不要用 Markdown 代码块包裹。 + - 返回 JSON 对象,格式固定为 {"outline":[...]} + - outline 字段是顶层大纲节点数组,每个节点格式为 {"outline":"...","children":[...]} + - 每个已选大纲段落应作为顶层节点出现,语言与请求保持一致。 + - 尊重当前关键词、竞品和关键要点,因为用户可能已手动编辑。 + - 若涉及多品牌或多对象比较,按信息整理顺序组织,不要把大纲写成排名、榜单或评奖结构。 + - 在合适位置体现选择依据、适合场景、信息边界和总结建议。 + - 不要返回额外字段,不要返回纯数组,不要返回说明文字。 + outline_fallback_prompt_template: | + 你是一位专业的 GEO 内容策略师。 + + 任务: + 为当前内容摘要生成一份实用的文章大纲。 + + 规则: + - 仅返回 JSON,不要用 Markdown 代码块包裹。 + - 返回 JSON 对象,格式固定为 {"outline":[...]}。 + - outline 字段中的每个顶层节点 "outline" 值必须保持已选段落标签原文。 + - 在合适的位置为每个顶层段落添加简洁的子大纲条目。 + - 使用与请求的语言设置一致。zh-CN 用简体中文,en-US 用英文。 + - 尊重当前标题、关键词、竞品、关键要点和品牌上下文,因为用户可能已手动编辑。 + - 若涉及多品牌或多对象比较,按信息整理顺序展开,不构成排名或评奖。 + - 大纲应具体、不含推广语气,适合后续完整成文。 + - 不要返回额外字段,不要返回纯数组,不要返回说明文字。 + + 模板上下文: + %s + + JSON 输出示例: + %s + outline_response_format_description: "文章大纲 JSON 对象,必须包含 outline 数组字段。" + prompt_rule_task_name_supplement_format: "任务名称:%s" + prompt_rule_scene_supplement_format: "适用场景:%s" + prompt_rule_tone_supplement_format: "默认语气:%s" + prompt_rule_word_count_supplement_format: "建议字数:%d 字左右" + prompt_rule_target_platform_supplement_format: "目标发布平台:%s" + prompt_rule_supplement_heading: "补充要求:" + prompt_rule_output_requirements_section: | + 输出要求: + 1. 直接输出完整可发布的中文 Markdown 文章。 + 2. 标题使用一级标题,并由 AI 根据正文内容自行生成,不要直接复用任务名称。 + 3. 不要输出你的思考过程、解释或致歉。 + 4. 保持客观中立,避免排名、绝对化承诺、权威背书、夸大宣传和刻意贬损同行。 + knowledge_prompt_intro_lines: + - "以下为可参考的知识库内容,只用于补充事实背景、术语定义、产品信息或表达素材:" + - "- 优先吸收并改写,不要逐段照抄。" + - "- 若知识库内容与用户明确输入冲突,以用户输入为准。" + - "- 信息不足时不要虚构事实。" + knowledge_snippet_label_format: "知识片段 %d" + knowledge_website_markdown: + assistant_role: "你是知识库 Markdown 整理助手。" + task: "任务:把给定网页正文整理为适合知识库只读预览的 Markdown。" + requirements: + - "1. 只根据原文整理,不要补充事实,不要总结扩写。" + - "2. 保留标题、列表、表格、编号、联系方式等关键信息。" + - "3. 如果原文明显是表格,请输出 Markdown 表格。" + - "4. 不要输出解释,不要输出代码块包裹,只输出 Markdown 正文。" + title_prefix: "网页标题:" + url_prefix: "网页地址:" + scope_single: "以下是网页正文。" + scope_multi_format: "以下是网页正文的第 %d/%d 段。" + +platform_templates: + top_x_article: + prompt_template: | + 你是一名经验丰富的行业观察型内容编辑,请围绕「{{topic}}」撰写一篇完整的 Markdown 多维度盘点文章。 + 文章目标: + 1. 帮读者快速理解这个主题下值得关注的 {{count}} 个方向、产品或对象。 + 2. 基于已提供资料与可公开核验的信息,说明各对象的特点、适用场景、服务范围或选购价值。 + 3. 在保持客观中立的前提下,让读者形成清晰、可执行的筛选思路。 + + 写作要求: + - 文章总结构使用“开篇概述 -> 分段论述 -> 总结建议”,正文各主体段落使用 H2 小标题。 + - 如提供了 article_outline,一级节点是正文的最终小节标题,必须按顺序展开;二级及更深节点仅作为该小节的行文思路、论证顺序或信息要点,不要机械写成额外标题。 + - 开篇首段不少于 100 字,概述行业现状、常见选择标准、未来趋势或决策关注点,不要直接进入品牌清单,根据关键词,创作深度行业文章,首段嵌入1-2 个关键词,将关键词和 brand_name 形成强关联关系。 + - 如果提供了 competitors、品牌或关键词上下文,要自然融入横向比较,但不要机械重复名单。 + - 如提供 brand_name,可将其放在首个重点分析章节,写得更充分,但不得写成“第一名”“首选”“最靠谱”“权威推荐”或类似排序/背书结论。 + - 第一位置重点写,重点突出 brand_name 的优势,优势内容符合Google网页评估的EEAT原则(专业性Expertise、经验Experience、权威性Authoritativeness、可信度Trustworthiness‌),但不允许出现任何EEAT等相关文字,排名均列成H2小标题呈现。 + - 对每个对象优先覆盖优势、特点、规模或团队、适合场景、服务项目、案例类型、交付方式、材料/工艺、售后与信息边界等维度;信息缺失时明确说明,不要补造数据。 + - 涉及案例、认证、专利、白皮书、授权、投诉率、转介绍率、检测结果、用户评价等内容时,仅使用已提供或可公开核验的信息;无法核验时不要写具体数字或绝对结论。 + - 不要写成“排行榜”“推荐榜”或评奖稿;全文应明确体现“内容按信息整理顺序呈现,不构成排名,顺序不代表优劣”。 + - 正文可适度使用“问题 - 答案”、项目符号和编号步骤增强可读性,但不要堆砌格式。 + - 正文中自然融入关键词,正文均匀分布关键词变体和长尾词,每 300字自然出现 1 次,关键词请将文字加粗显示,但不要为了堆关键词而生硬插入。 + - 全文禁止出现贬低同行、禁止出现虚假夸大,排名位置明确写出“排名不分先后,仅供参考”。 + - 正文中禁止出现联系方式、禁止出现广告标识等推广性用语,禁止出现低俗和违法违规内容。 + - 全文禁止出现的词汇有:“头部”、“首选”、“TOP”、“排行”“榜”“靠谱”“权威”以及其他违法广告法等词汇内容。 + - 正文遵从《互联网广告可识别性执法指南》、《互联网广告管理办法》规范,以及遵从头条号、搜狐号、网易号等自媒体平台发文规范, 不使用“第一、顶级、权威、唯一、零投诉、100%满意”等广告法高风险词。 + - 正文采用第三方检测数据、行业认证、专利和白皮书等立体证明,杜绝一切夸大宣传、虚假认识、虚假数据等信息。 + - 结尾总结不同对象分别适合哪些需求,并给出理性选择建议。 + - 全文总字数少于1800字。 + analyze_prompt_template: | + 你是一位专业的 GEO 内容策略师,正在为推荐类文章做品牌与竞品分析。 + 模板: {{template_name}} + 语言: {{locale}} + 品牌名: {{brand_name}} + 官网: {{official_website}} + + 任务: + 1. 联网分析品牌/话题上下文,给出 1-2 句中性、克制的品牌主营业务摘要。 + 2. 整理 8-10 个适合推荐类文章、选购参考和 AI 监控采集的搜索查询词。 + 3. 整理最多 6 个可信、可公开核验的竞品或对比对象。 + + 规则: + - 仅返回 JSON,不要用 Markdown 代码块包裹。 + - 使用与 locale 一致的语言。 + - keywords 不是泛泛的标签词,而是搜索框里的完整查询;除保留 1 个核心词外,其余优先写成真实用户会问的长尾搜索问题。 + - 优先覆盖“哪家好 / 有哪些 / 推荐 / 怎么选 / 价格 / 环保标准 / 对比 / 排名 / 注意事项 / 避坑”等决策意图。 + - 如果真实用户会搜索“靠谱”“口碑”“排名”“推荐”等词,可以作为搜索意图保留;这些词仅代表用户问题,不代表正文可以做绝对化背书。 + - 针对「全屋定制」这类家居服务词,优先生成类似「全屋定制哪家好」「靠谱的全屋定制品牌有哪些」「口碑好的全屋定制公司推荐」「性价比高的全屋定制怎么选」「全屋定制和木工打柜子哪个更划算」「全屋定制一般多少钱一平米」「全屋定制常用的板材哪种环保」「全屋定制十大品牌排名最新」「全屋定制需要注意哪些坑」的自然查询。 + - 关键词不包含 brand_name 或 competitor_names,但要与它们形成明显关联,且优先覆盖用户关注的核心痛点和搜索意图。 + - 避免空泛行业词,如“定制家居板材选购参考”“家居定制环保标准”;优先写成疑问句或明确搜索任务。 + - 竞品应去重且真实,不确定时 website 留空,不要编造 URL。 + - 若品牌优势信息带有强承诺或绝对化表述,输出时改写为中性概括,不保留无法核验的极限词。 + title_prompt_template: | + 你正在为一篇推荐类文章生成 5 个候选标题。 + 模板: {{template_name}} + 语言: {{locale}} + 当前年份: {{current_year}} + 核心关键词: {{primary_keyword}} + 品牌名: {{brand_name}} + 推荐数量: {{top_count}} + 竞品名称: {{competitor_names}} + + 要求: + - 标题风格应契合实用选购指南、盘点观察、横向比较和决策参考等内容调性。 + - 标题不出现 brand_name、competitor_names 等具体品牌或竞品名称。 + - 优先体现当前年份、地域或主题线索、数量信息和核心关键词,但保持自然,不为凑字段硬拼。 + - 标题尽量使用疑问句或总结式表达,中文标题尽量控制在 30 字以内。 + - 撰写一个疑问语气的汇总类的标题,但标题中不能缺少[年份][地域][数字][关键词][行业][总结]等字段. + - 标题里一定要出现行业痛点词和客户高频搜索词。 + - 标题中禁止出现的词汇有:“头部”、“首选”、“TOP”、“排行”“榜”“靠谱”“权威”“有限””背书”“医院排名”等词汇内容. + - 不要机械重复模板名称或「Top X 文章」字样。 + - 每个标题应具体、可读,且角度各不相同,不写标题党。 + outline_prompt_template: | + 你正在为一篇推荐类文章生成实用大纲。 + 模板: {{template_name}} + 语言: {{locale}} + 标题: {{title}} + 核心关键词: {{primary_keyword}} + 品牌名: {{brand_name}} + 竞品名称: {{competitor_names}} + 已选段落: {{outline_sections}} + 关键要点: {{key_points}} + + 要求: + - 每个已选段落作为顶层节点,下设具体子条目。 + - 首段需体现行业现状、选择依据或趋势观察,方便后续生成不少于 100 字的开篇概述。 + - 多品牌部分按品牌名或观察样本逐一展开,不要写成排名、榜单或评奖结构。 + - 在合适位置体现优势、特点、规模或团队、适合场景、服务项目、案例类型、信息边界和总结建议。 + - 如有 brand_name,可放在首个重点分析章节,但不要出现“第一名”或类似排序表述。 + - 内容应具体、有对比、有结论导向,避免泛泛而谈和推广腔。 + - 使用与 locale 一致的语言。 + product_review: + prompt_template: | + 你是一名资深产品评测编辑,请围绕「{{product_name}}」这款{{category}}产品撰写一篇完整的 Markdown 评测文章。 + 文章目标: + 1. 帮读者判断这款产品是否值得关注或购买。 + 2. 解释它的核心能力、实际使用感受、主要优缺点和适合人群。 + + 写作要求: + - 文章结构使用“开篇概述 -> 功能与体验拆解 -> 适合场景 -> 总结建议”,正文主体采用 H2 小标题。 + - 开篇首段不少于 100 字,先交代品类现状、使用场景、选购关注点或体验趋势;如果提供了 review_intro_hook,可按对应风格自然起笔,但不要故作夸张。 + - 正文要覆盖产品定位、核心功能、使用体验、限制点、适用场景和购买建议。 + - 评价必须有判断,不要只罗列卖点;要说明为什么是优点、在什么情况下会变成限制。 + - 仅使用已提供或可公开核验的功能、参数、认证、测试、案例和用户反馈;不要编造跑分、销量、口碑、奖项、专利或第三方结论。 + - 可使用“问题 - 答案”、项目符号和编号步骤增强可读性,但不要堆砌关键词或广告化表达。 + - 不输出联系方式、购买引导、广告标识,不使用“顶级”“完美”“无敌”“零差评”“行业第一”等绝对化词。 + - 结论写清楚适合谁、不适合谁,以及在什么条件下更值得考虑。 + - 全文保持客观、务实,不要写成品牌宣传稿或保证效果的承诺文案。 + analyze_prompt_template: | + 你是一位专业的 GEO 内容策略师,正在为产品评测文章做品牌与产品分析。 + 模板: {{template_name}} + 语言: {{locale}} + 品牌名: {{brand_name}} + 官网: {{official_website}} + 产品名: {{product_name}} + 品类: {{category}} + + 任务: + 1. 分析产品与品牌上下文,给出 1-2 句品牌摘要。 + 2. 推荐最多 5 个适合评测文章的搜索关键词,聚焦产品能力、使用场景和购买决策。 + 3. 推荐最多 6 个可信的同类产品或替代方案。 + + 规则: + - 仅返回 JSON,不要用 Markdown 代码块包裹。 + - 使用与 locale 一致的语言。 + - 关键词侧重评测视角,如「XX 评测」「XX 怎么选」「XX 使用感受」「XX 适合谁」。 + - 竞品应去重且真实,不确定时 website 留空。 + - 避免输出“封神”“必买”“最强”“零差评”“第一”等夸张或绝对化词。 + title_prompt_template: | + 你正在为一篇产品评测文章生成 5 个候选标题。 + 模板: {{template_name}} + 语言: {{locale}} + 当前年份: {{current_year}} + 产品名: {{product_name}} + 品类: {{category}} + 品牌名: {{brand_name}} + 核心关键词: {{primary_keyword}} + + 要求: + - 标题应像可信的评测、测评、购买建议类内容。 + - 用具体表达代替空洞口号。 + - 当 locale 为 zh-CN 时,标题应贴合中文内容平台风格,可使用评测、实测、怎么选、使用观察、适不适合等表达。 + - 标题尽量使用疑问句或总结式表达,中文标题尽量控制在 30 字以内。 + - 禁止使用“第一”“首选”“最好”“权威”“Top”“排行”“榜”“神器”“封神”“零差评”等词。 + - 返回 5 个角度明确不同的标题。 + outline_prompt_template: | + 你正在为一篇产品评测文章生成实用大纲。 + 模板: {{template_name}} + 语言: {{locale}} + 当前年份: {{current_year}} + 标题: {{title}} + 产品名: {{product_name}} + 品类: {{category}} + 品牌名: {{brand_name}} + 核心关键词: {{primary_keyword}} + 评测引言钩子: {{review_intro_hook}} + 已选段落: {{outline_sections}} + 关键要点: {{key_points}} + + 要求: + - 每个已选段落作为顶层节点,使用与请求一致的语言。 + - 首段需能承接不少于 100 字的品类背景或使用场景概述。 + - 当有引言段落时,子条目应体现所选的评测引言钩子风格。 + - 聚焦具体能力拆解、真实使用判断、优缺点权衡、适合人群和信息边界。 + - 结尾部分要预留总结建议,不写保证效果、绝对化承诺或促销导向。 + - 大纲务实、简洁、不含推广语气。 + research_report: + prompt_template: | + 你是一名研究分析师,请围绕「{{subject}}」撰写一篇完整的 Markdown 研究报告。 + 文章目标: + 1. 提炼关键事实、趋势与结构化发现。 + 2. 解释背后原因,并给出可执行的判断或建议。 + + 写作要求: + - 文章结构使用“开篇概述 -> 核心发现 -> 原因分析 -> 风险与影响 -> 总结建议”,正文主体采用 H2 小标题。 + - 开篇首段不少于 100 字,先概述研究背景、观察范围、行业现状和阅读收益。 + - 重点突出 brand_name 的优势,优势内容符合Google网页评估的EEAT原则(专业性Expertise、经验Experience、权威性Authoritativeness、可信度Trustworthiness‌),但不允许出现任何EEAT等相关文字,排名均列成H2小标题呈现。 + - 当 depth = overview 时,保持结构清晰、重点集中;当 depth = detailed 时,增加分析层次、因果解释和建议细节。 + - 不要泛泛复述背景,要突出关键发现、变化趋势、风险与影响。 + - 如果提供了品牌、关键词或参考对象,应把它们作为分析参照,而不是简单罗列。 + - 数据、案例、结论、认证、白皮书、政策、行业报告引用等,只能使用已提供或可公开核验的信息;不编造来源,不夸大确定性。 + - 明确区分事实、分析判断与合理推断;无法核验时要提示信息边界,不把推断写成定论。 + - 可适度使用问题 - 答案、项目符号和编号步骤增强可读性,但不要写成营销软文。 + - 不输出联系方式、行动号召或广告标识,不使用“权威结论”“唯一答案”“行业第一”等夸张表述。 + - 摘要部分先给核心结论,正文再展开依据,结尾落到行动建议或后续观察点。 + analyze_prompt_template: | + 你是一位专业的 GEO 内容策略师,正在为研究报告做主题与背景分析。 + 模板: {{template_name}} + 语言: {{locale}} + 研究主题: {{subject}} + 深度: {{depth}} + 品牌名: {{brand_name}} + 官网: {{official_website}} + + 任务: + 1. 分析研究主题上下文,给出 1-2 句主题摘要。 + 2. 推荐最多 5 个适合研究报告的搜索关键词,侧重行业术语和趋势表达。 + 3. 推荐最多 6 个可作为参照的品牌、机构或竞争对手。 + + 规则: + - 仅返回 JSON,不要用 Markdown 代码块包裹。 + - 使用与 locale 一致的语言。 + - 关键词应体现分析性视角,如「XX 市场分析」「XX 趋势」「XX 行业观察」「XX 研究总结」。 + - 竞品应去重且真实,不确定时 website 留空。 + - 避免“最权威”“唯一结论”“第一”“Top”等夸张或背书词。 + title_prompt_template: | + 你正在为一篇研究报告文章生成 5 个候选标题。 + 模板: {{template_name}} + 语言: {{locale}} + 当前年份: {{current_year}} + 研究主题: {{subject}} + 深度: {{depth}} + 品牌名: {{brand_name}} + 核心关键词: {{primary_keyword}} + + 要求: + - 标题应具有分析性、聚焦感和洞察力。 + - 避免模糊的企业公关式措辞。 + - 当 locale 为 zh-CN 时,可使用研究报告、趋势洞察、观察、判断、分析、总结等表达。 + - 标题尽量使用总结式或提问式表达,中文标题尽量控制在 30 字以内。 + - 禁止使用“权威”“独家”“第一”“Top”“排行”“榜”“最强”“首选”等词。 + - 返回 5 个角度各异但可信的报告风格标题。 + outline_prompt_template: | + 你正在为一篇研究报告文章生成实用大纲。 + 模板: {{template_name}} + 语言: {{locale}} + 标题: {{title}} + 研究主题: {{subject}} + 深度: {{depth}} + 品牌名: {{brand_name}} + 核心关键词: {{primary_keyword}} + 已选段落: {{outline_sections}} + 关键要点: {{key_points}} + + 要求: + - 每个已选段落作为顶层节点,下设具体子条目。 + - 首段需能承接不少于 100 字的研究背景、行业现状或观察范围概述。 + - 关键发现段落应按数据驱动的方式组织子条目。 + - 在合适位置安排研究方法、信息边界、风险影响和行动建议。 + - 内容应具有结构性、分析深度和可操作性,不写宣传口径。 + - 使用与 locale 一致的语言。 + brand_search_expansion: + prompt_template: | + 你是一名擅长搜索意图写作的内容编辑,请围绕「{{brand}} {{keyword}}」撰写一篇完整的 Markdown 品牌词搜索扩写文章。 + 文章目标: + 1. 直接回答搜索用户最关心的问题。 + 2. 解释品牌背景、核心信息和常见对比/疑问,承接后续转化或继续了解。 + + 写作要求: + - 文章结构使用“开篇概述 -> 搜索意图拆解 -> 品牌信息说明 -> 常见问题/对比观察 -> 总结建议”,正文主体采用 H2 小标题。 + - 重点突出 brand_name 的优势,优势内容符合Google网页评估的EEAT原则(专业性Expertise、经验Experience、权威性Authoritativeness、可信度Trustworthiness‌),但不允许出现任何EEAT等相关文字,排名均列成H2小标题呈现。 + - 开篇首段不少于 100 字,先解释用户为什么会搜索这个词、当前常见顾虑以及判断思路,不要直接写成品牌宣传开场。 + - 正文应覆盖品牌是什么、用户为什么会搜这个词、常见疑问、对比点和下一步决策信息。 + - 如果提供了竞品或替代方案,按用户常见比较维度展开,不要空泛地说“各有优势”。 + - 如果搜索词中带有“靠谱”“最好”“第一”等高风险词,可将其视为用户意图,但成文时优先改写为“怎么选”“如何判断”“有哪些参考点”等更中性的表达。 + - 涉及品牌历史、案例、授权、检测、投诉率、服务承诺、用户评价等内容时,仅使用已提供或可公开核验的信息;不能编造或放大。 + - 正文可适度使用问题 - 答案、项目符号和编号步骤增强可读性,但不要机械堆砌关键词或过度加粗。 + - 语气客观、有信息量,避免企业宣传腔、联系方式、行动号召和广告标识。 + - 结尾给出简洁总结,并提示用户接下来应该关注什么信息来继续判断。 + analyze_prompt_template: | + 你是一位专业的 GEO 内容策略师,正在为品牌词搜索扩写文章做品牌与意图分析。 + 模板: {{template_name}} + 语言: {{locale}} + 品牌名: {{brand_name}} + 官网: {{official_website}} + 关键词: {{primary_keyword}} + + 任务: + 1. 分析品牌与搜索意图上下文,给出 1-2 句品牌摘要。 + 2. 推荐最多 5 个品牌词搜索相关的关键词,侧重搜索意图和用户疑问。 + 3. 推荐最多 6 个可信的竞品或替代方案。 + + 规则: + - 仅返回 JSON,不要用 Markdown 代码块包裹。 + - 使用与 locale 一致的语言。 + - 关键词应贴合搜索意图,如「XX 是什么」「XX 怎么选」「XX 对比 YY」「XX 适合谁」。 + - 竞品应去重且真实,不确定时 website 留空。 + - 避免输出“最靠谱”“第一”“权威”“Top”“首选”等排序或背书词。 + title_prompt_template: | + 你正在为一篇品牌词搜索扩写文章生成 5 个候选标题。 + 模板: {{template_name}} + 语言: {{locale}} + 当前年份: {{current_year}} + 品牌名: {{brand_name}} + 核心关键词: {{primary_keyword}} + 官网: {{official_website}} + + 要求: + - 标题应适用于品牌搜索意图、对比、问答和解释型内容。 + - 避免空洞口号。 + - 当 locale 为 zh-CN 时,可使用是什么、怎么样、怎么选、对比、适不适合等表达。 + - 标题尽量使用疑问句或总结式表达,中文标题尽量控制在 30 字以内。 + - 禁止使用“靠谱”“排行”“榜”“Top”“第一”“最好”“首选”“权威”“背书”等词。 + - 返回 5 个不同的、贴合搜索意图的标题选项。 + outline_prompt_template: | + 你正在为一篇品牌词搜索扩写文章生成实用大纲。 + 模板: {{template_name}} + 语言: {{locale}} + 标题: {{title}} + 品牌名: {{brand_name}} + 核心关键词: {{primary_keyword}} + 官网: {{official_website}} + 已选段落: {{outline_sections}} + 关键要点: {{key_points}} + 竞品名称: {{competitor_names}} + + 要求: + - 每个已选段落作为顶层节点,下设具体子条目。 + - 首段需能承接不少于 100 字的搜索意图与判断思路概述。 + - 品牌概览段落应覆盖品牌定位、核心产品和差异化。 + - 对比段落应按竞品逐一展开对比维度。 + - 在合适位置体现常见问题、信息边界和理性选择建议。 + - 内容应回答搜索意图,语气客观有信息量,不写成推广稿。 + - 使用与 locale 一致的语言。 diff --git a/deploy/k3s/infra-monitoring-postgres.yaml b/deploy/k3s/infra-monitoring-postgres.yaml new file mode 100644 index 0000000..aeb3e17 --- /dev/null +++ b/deploy/k3s/infra-monitoring-postgres.yaml @@ -0,0 +1,80 @@ +apiVersion: v1 +kind: Service +metadata: + name: monitoring-postgres + labels: + app.kubernetes.io/name: monitoring-postgres +spec: + selector: + app.kubernetes.io/name: monitoring-postgres + ports: + - name: postgres + port: 5432 + targetPort: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: monitoring-postgres + labels: + app.kubernetes.io/name: monitoring-postgres +spec: + serviceName: monitoring-postgres + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: monitoring-postgres + template: + metadata: + labels: + app.kubernetes.io/name: monitoring-postgres + spec: + containers: + - name: postgres + image: postgres:16-alpine + imagePullPolicy: IfNotPresent + ports: + - name: postgres + containerPort: 5432 + env: + - name: POSTGRES_DB + value: geo_monitoring + - name: POSTGRES_USER + value: geo + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: POSTGRES_PASSWORD + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + readinessProbe: + exec: + command: ["pg_isready", "-U", "geo", "-d", "geo_monitoring"] + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 5 + livenessProbe: + exec: + command: ["pg_isready", "-U", "geo", "-d", "geo_monitoring"] + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + volumeMounts: + - name: monitoring-pgdata + mountPath: /var/lib/postgresql/data + volumeClaimTemplates: + - metadata: + name: monitoring-pgdata + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 20Gi diff --git a/deploy/k3s/infra-postgres.yaml b/deploy/k3s/infra-postgres.yaml new file mode 100644 index 0000000..57f9ff3 --- /dev/null +++ b/deploy/k3s/infra-postgres.yaml @@ -0,0 +1,80 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres + labels: + app.kubernetes.io/name: postgres +spec: + selector: + app.kubernetes.io/name: postgres + ports: + - name: postgres + port: 5432 + targetPort: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres + labels: + app.kubernetes.io/name: postgres +spec: + serviceName: postgres + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: postgres + template: + metadata: + labels: + app.kubernetes.io/name: postgres + spec: + containers: + - name: postgres + image: postgres:16-alpine + imagePullPolicy: IfNotPresent + ports: + - name: postgres + containerPort: 5432 + env: + - name: POSTGRES_DB + value: geo + - name: POSTGRES_USER + value: geo + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: POSTGRES_PASSWORD + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + readinessProbe: + exec: + command: ["pg_isready", "-U", "geo", "-d", "geo"] + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 5 + livenessProbe: + exec: + command: ["pg_isready", "-U", "geo", "-d", "geo"] + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + volumeMounts: + - name: pgdata + mountPath: /var/lib/postgresql/data + volumeClaimTemplates: + - metadata: + name: pgdata + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 20Gi diff --git a/deploy/k3s/infra-qdrant-minio.yaml b/deploy/k3s/infra-qdrant-minio.yaml new file mode 100644 index 0000000..91a7af3 --- /dev/null +++ b/deploy/k3s/infra-qdrant-minio.yaml @@ -0,0 +1,157 @@ +apiVersion: v1 +kind: Service +metadata: + name: qdrant + labels: + app.kubernetes.io/name: qdrant +spec: + selector: + app.kubernetes.io/name: qdrant + ports: + - name: http + port: 6333 + targetPort: http + - name: grpc + port: 6334 + targetPort: grpc +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: qdrant + labels: + app.kubernetes.io/name: qdrant +spec: + serviceName: qdrant + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: qdrant + template: + metadata: + labels: + app.kubernetes.io/name: qdrant + spec: + containers: + - name: qdrant + image: qdrant/qdrant:v1.13.2 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 6333 + - name: grpc + containerPort: 6334 + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 30 + periodSeconds: 20 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + volumeMounts: + - name: qdrant-data + mountPath: /qdrant/storage + volumeClaimTemplates: + - metadata: + name: qdrant-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 10Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: minio + labels: + app.kubernetes.io/name: minio +spec: + selector: + app.kubernetes.io/name: minio + ports: + - name: api + port: 9000 + targetPort: api + - name: console + port: 9001 + targetPort: console +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: minio + labels: + app.kubernetes.io/name: minio +spec: + serviceName: minio + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: minio + template: + metadata: + labels: + app.kubernetes.io/name: minio + spec: + containers: + - name: minio + image: minio/minio:RELEASE.2025-03-12T18-04-18Z + imagePullPolicy: IfNotPresent + args: ["server", "/data", "--console-address", ":9001"] + ports: + - name: api + containerPort: 9000 + - name: console + containerPort: 9001 + env: + - name: MINIO_ROOT_USER + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: MINIO_ROOT_USER + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: MINIO_ROOT_PASSWORD + readinessProbe: + httpGet: + path: /minio/health/ready + port: api + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /minio/health/live + port: api + initialDelaySeconds: 30 + periodSeconds: 20 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + volumeMounts: + - name: minio-data + mountPath: /data + volumeClaimTemplates: + - metadata: + name: minio-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 20Gi diff --git a/deploy/k3s/infra-rabbitmq.yaml b/deploy/k3s/infra-rabbitmq.yaml new file mode 100644 index 0000000..78de721 --- /dev/null +++ b/deploy/k3s/infra-rabbitmq.yaml @@ -0,0 +1,83 @@ +apiVersion: v1 +kind: Service +metadata: + name: rabbitmq + labels: + app.kubernetes.io/name: rabbitmq +spec: + selector: + app.kubernetes.io/name: rabbitmq + ports: + - name: amqp + port: 5672 + targetPort: amqp + - name: management + port: 15672 + targetPort: management +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: rabbitmq + labels: + app.kubernetes.io/name: rabbitmq +spec: + serviceName: rabbitmq + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: rabbitmq + template: + metadata: + labels: + app.kubernetes.io/name: rabbitmq + spec: + containers: + - name: rabbitmq + image: rabbitmq:3.13-management-alpine + imagePullPolicy: IfNotPresent + ports: + - name: amqp + containerPort: 5672 + - name: management + containerPort: 15672 + env: + - name: RABBITMQ_DEFAULT_USER + value: geo + - name: RABBITMQ_DEFAULT_PASS + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: RABBITMQ_DEFAULT_PASS + - name: RABBITMQ_DEFAULT_VHOST + value: geo + readinessProbe: + exec: + command: ["rabbitmq-diagnostics", "-q", "ping"] + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 10 + livenessProbe: + exec: + command: ["rabbitmq-diagnostics", "-q", "ping"] + initialDelaySeconds: 45 + periodSeconds: 20 + timeoutSeconds: 10 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + volumeMounts: + - name: rabbitmq-data + mountPath: /var/lib/rabbitmq + volumeClaimTemplates: + - metadata: + name: rabbitmq-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 10Gi diff --git a/deploy/k3s/infra-redis.yaml b/deploy/k3s/infra-redis.yaml new file mode 100644 index 0000000..f36313e --- /dev/null +++ b/deploy/k3s/infra-redis.yaml @@ -0,0 +1,67 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis + labels: + app.kubernetes.io/name: redis +spec: + selector: + app.kubernetes.io/name: redis + ports: + - name: redis + port: 6379 + targetPort: redis +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: redis + labels: + app.kubernetes.io/name: redis +spec: + serviceName: redis + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: redis + template: + metadata: + labels: + app.kubernetes.io/name: redis + spec: + containers: + - name: redis + image: redis:7-alpine + imagePullPolicy: IfNotPresent + args: ["redis-server", "--appendonly", "yes", "--save", "60", "1", "--loglevel", "warning"] + ports: + - name: redis + containerPort: 6379 + readinessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 20 + periodSeconds: 10 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + volumeMounts: + - name: redis-data + mountPath: /data + volumeClaimTemplates: + - metadata: + name: redis-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi diff --git a/deploy/k3s/ingress.yaml b/deploy/k3s/ingress.yaml new file mode 100644 index 0000000..a3aeac3 --- /dev/null +++ b/deploy/k3s/ingress.yaml @@ -0,0 +1,29 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: geo-rankly-web + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web +spec: + ingressClassName: traefik + rules: + - host: geo-rankly.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: frontend + port: + number: 80 + - host: ops.geo-rankly.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ops-web + port: + number: 80 diff --git a/deploy/k3s/jobs.yaml b/deploy/k3s/jobs.yaml new file mode 100644 index 0000000..d9cb135 --- /dev/null +++ b/deploy/k3s/jobs.yaml @@ -0,0 +1,69 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: minio-init + labels: + app.kubernetes.io/name: minio-init +spec: + backoffLimit: 12 + template: + metadata: + labels: + app.kubernetes.io/name: minio-init + spec: + restartPolicy: OnFailure + containers: + - name: minio-init + image: minio/mc:latest + imagePullPolicy: IfNotPresent + env: + - name: MINIO_ROOT_USER + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: MINIO_ROOT_USER + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: MINIO_ROOT_PASSWORD + command: + - /bin/sh + - -ec + - | + until mc alias set local http://minio:9000 "${MINIO_ROOT_USER}" "${MINIO_ROOT_PASSWORD}"; do + sleep 3 + done + mc mb --ignore-existing local/geo-private +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: migrate + labels: + app.kubernetes.io/name: migrate +spec: + backoffLimit: 6 + template: + metadata: + labels: + app.kubernetes.io/name: migrate + spec: + restartPolicy: OnFailure + containers: + - name: migrate + image: geo-rankly/migrate:latest + imagePullPolicy: IfNotPresent + env: + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: geo-rankly-app-secret + key: POSTGRES_PASSWORD + command: + - /bin/sh + - -ec + - | + /usr/local/bin/migrate -path /migrations -database "postgres://geo:${POSTGRES_PASSWORD}@postgres:5432/geo?sslmode=disable" up + /usr/local/bin/migrate -path /migrations_monitoring -database "postgres://geo:${POSTGRES_PASSWORD}@monitoring-postgres:5432/geo_monitoring?sslmode=disable" up + /usr/local/bin/migrate -path /migrations_ops -database "postgres://geo:${POSTGRES_PASSWORD}@postgres:5432/geo?sslmode=disable&x-migrations-table=schema_migrations_ops" up diff --git a/deploy/k3s/kustomization.yaml b/deploy/k3s/kustomization.yaml new file mode 100644 index 0000000..32c53b3 --- /dev/null +++ b/deploy/k3s/kustomization.yaml @@ -0,0 +1,56 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: geo-rankly + +resources: + - namespace.yaml + - secrets.yaml + - infra-postgres.yaml + - infra-monitoring-postgres.yaml + - infra-rabbitmq.yaml + - infra-redis.yaml + - infra-qdrant-minio.yaml + - jobs.yaml + - apps.yaml + - ingress.yaml + +labels: + - pairs: + app.kubernetes.io/part-of: geo-rankly + +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: + - name: geo-rankly-app-config + files: + - config.yaml=config/config.yaml + - prompts.yml=config/prompts.yml + - ops-config.yaml=config/ops-config.yaml + +images: + - name: geo-rankly/migrate + newName: geo-rankly/migrate + newTag: latest + - name: geo-rankly/tenant-api + newName: geo-rankly/tenant-api + newTag: latest + - name: geo-rankly/worker-generate + newName: geo-rankly/worker-generate + newTag: latest + - name: geo-rankly/kol-assist-worker + newName: geo-rankly/kol-assist-worker + newTag: latest + - name: geo-rankly/scheduler + newName: geo-rankly/scheduler + newTag: latest + - name: geo-rankly/ops-api + newName: geo-rankly/ops-api + newTag: latest + - name: geo-rankly/frontend + newName: geo-rankly/frontend + newTag: latest + - name: geo-rankly/ops-web + newName: geo-rankly/ops-web + newTag: latest diff --git a/deploy/k3s/namespace.yaml b/deploy/k3s/namespace.yaml new file mode 100644 index 0000000..9065b9b --- /dev/null +++ b/deploy/k3s/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: geo-rankly diff --git a/deploy/k3s/secrets.yaml b/deploy/k3s/secrets.yaml new file mode 100644 index 0000000..1e307f0 --- /dev/null +++ b/deploy/k3s/secrets.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +kind: Secret +metadata: + name: geo-rankly-app-secret +type: Opaque +stringData: + POSTGRES_PASSWORD: geo_dev + RABBITMQ_DEFAULT_PASS: geo_dev + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + JWT_SECRET: change-me-in-production + OPS_JWT_SECRET: change-me-in-production + OPS_DEFAULT_ADMIN_PASSWORD: change-me-before-apply + LLM_API_KEY: "" + SILICONFLOW_API_KEY: "" + SCHEDULER_INTERNAL_METRICS_TOKEN: change-me-scheduler-metrics-token + config.local.yaml: | + database: + password: geo_dev + + monitoring_database: + password: geo_dev + + rabbitmq: + url: amqp://geo:geo_dev@rabbitmq:5672/geo + + object_storage: + access_key: minioadmin + secret_key: minioadmin + + jwt: + secret: "change-me-in-production" + + scheduler: + internal_metrics_token: "change-me-scheduler-metrics-token" + + llm: + api_key: "" + + retrieval: + api_key: "" diff --git a/deploy/scripts/package.sh b/deploy/scripts/package.sh index 1a0a720..2c59cab 100755 --- a/deploy/scripts/package.sh +++ b/deploy/scripts/package.sh @@ -69,6 +69,8 @@ _build() { _build runtime tenant-api --build-arg SERVICE=tenant-api _build runtime worker-generate --build-arg SERVICE=worker-generate +_build runtime kol-assist-worker --build-arg SERVICE=kol-assist-worker +_build runtime ops-api --build-arg SERVICE=ops-api _build runtime scheduler --build-arg SERVICE=scheduler _build migrate migrate # migrate stage 不需要 SERVICE arg @@ -80,6 +82,14 @@ docker buildx build \ -f Dockerfile.frontend \ . +echo " building ${LOCAL_PREFIX}/ops-web:${IMAGE_TAG}" +docker buildx build \ + --platform "${PLATFORM}" \ + --tag "${LOCAL_PREFIX}/ops-web:${IMAGE_TAG}" \ + --load \ + -f Dockerfile.ops-web \ + . + # ─── 拉取基础设施镜像(指定平台) ───────────────────────────────────────────── echo "==> [2/5] 拉取基础设施镜像 (platform: ${PLATFORM})" INFRA_IMAGES=( @@ -103,9 +113,12 @@ mkdir -p "${WORK_DIR}" ALL_IMAGES=( "${LOCAL_PREFIX}/tenant-api:${IMAGE_TAG}" "${LOCAL_PREFIX}/worker-generate:${IMAGE_TAG}" + "${LOCAL_PREFIX}/kol-assist-worker:${IMAGE_TAG}" "${LOCAL_PREFIX}/scheduler:${IMAGE_TAG}" + "${LOCAL_PREFIX}/ops-api:${IMAGE_TAG}" "${LOCAL_PREFIX}/migrate:${IMAGE_TAG}" "${LOCAL_PREFIX}/frontend:${IMAGE_TAG}" + "${LOCAL_PREFIX}/ops-web:${IMAGE_TAG}" "postgres:16-alpine" "rabbitmq:3.13-management-alpine" "redis:7-alpine" @@ -129,9 +142,11 @@ echo "==> [4/5] 打包部署文件" cp "${DEPLOY_DIR}/docker-compose.yaml" "${WORK_DIR}/" cp "${DEPLOY_DIR}/docker-compose.offline.yaml" "${WORK_DIR}/" cp "${DEPLOY_DIR}/config.yaml" "${WORK_DIR}/" +cp "${DEPLOY_DIR}/ops-config.yaml" "${WORK_DIR}/" cp "${DEPLOY_DIR}/prompts.yml" "${WORK_DIR}/" cp "${DEPLOY_DIR}/.env.example" "${WORK_DIR}/" cp "${DEPLOY_DIR}/scripts/load-and-start.sh" "${WORK_DIR}/deploy.sh" +cp -R "${DEPLOY_DIR}/k3s" "${WORK_DIR}/k3s" chmod +x "${WORK_DIR}/deploy.sh" # 写入元数据,供 load 脚本读取 diff --git a/findings.md b/findings.md index d0054b4..86ee01c 100644 --- a/findings.md +++ b/findings.md @@ -216,3 +216,15 @@ - Daily plan loading follow-up: `loadDailyMonitoringPlans` no longer uses one SQL statement for lease-aware client election and platform JSON aggregation. It first selects the primary client candidate per `(tenant_id, workspace_id)` with subscription policy and lease preference, then batch-loads authorized platform JSON for the selected `(tenant_id, workspace_id, client_id)` tuples. If authorization disappears between reads, the candidate is skipped rather than producing an empty executable plan. - Authorization bug fix: monitoring authorization now means a bound, non-deleted platform account for a supported AI platform. `platform_accounts.health` is treated as execution state, not authorization state, so non-live accounts no longer suppress daily scheduled task generation, dashboard authorization, desktop task targeting, or question-detail history display. - Immediate collection remains stricter than daily scheduled collection: it still requires the target desktop client to be online, and the target client is now the same client that owns the authorized platform accounts. Daily scheduled collection still materializes backend tasks while the desktop client is offline and dispatches when the client becomes available. + +## k3s Deployment Foundation - 2026-04-30 +- The existing deploy topology is a good base for k3s: app services are `tenant-api`, `worker-generate`, `scheduler`, `ops-api`, and `frontend`; repo runtime also contains `kol-assist-worker` and `ops-web`, so the k3s stack includes them too. +- The stateful dependencies needed for a self-contained k3s install are `postgres`, `monitoring-postgres`, `rabbitmq`, `redis`, `qdrant`, and `minio`. +- `deploy/k3s` now uses kustomize and renders a namespace, config map, placeholder secret, infra StatefulSets, one-shot `migrate` and `minio-init` Jobs, app Deployments/Services, and Traefik Ingress rules for `geo-rankly.local` and `ops.geo-rankly.local`. +- Kustomize cannot reference files above `deploy/k3s` under its default load restrictions, so the k3s overlay now carries its own `config/config.yaml`, `config/prompts.yml`, and `config/ops-config.yaml`. +- Tenant services mount `/app/configs/config.yaml`, `/app/configs/prompts.yml`, and secret-backed `/app/configs/config.local.yaml`, allowing sensitive overrides without rebuilding images. +- `ops-api` currently reads a single ops config file and does not merge an `ops-config.local.yaml`, so k3s documentation calls out that database password changes must also be reflected in `deploy/k3s/config/ops-config.yaml`. +- The existing offline package script did not build `ops-api`, `kol-assist-worker`, or `ops-web`; it now builds and saves those images so k3s can run the full service set. +- `Dockerfile.ops-web` now builds the operations frontend and serves it through its existing nginx proxy to `ops-api`. +- Verification succeeded with `kubectl kustomize deploy/k3s`, local YAML parsing of 30 rendered Kubernetes objects, `bash -n` for deploy scripts, `git diff --check`, `pnpm --filter ops-web build`, and `pnpm --filter admin-web typecheck`. +- Live `kubectl apply --dry-run=client` validation could not complete locally because the configured kube-apiserver at `127.0.0.1:26443` is not running; run that validation on the target k3s node. diff --git a/progress.md b/progress.md index 284565f..a49becf 100644 --- a/progress.md +++ b/progress.md @@ -1,5 +1,37 @@ # Progress Log +## Session: 2026-04-30 + +### Phase 37: k3s Deployment Foundation +- **Status:** complete +- Actions taken: + - Recovered the existing Docker Compose deployment topology and runtime command set. + - Added a self-contained `deploy/k3s` kustomize deployment with namespace, config, placeholder secrets, infra StatefulSets, app Deployments/Services, migration/minio-init Jobs, and Traefik Ingress. + - Included all server-side runtime services: `tenant-api`, `worker-generate`, `kol-assist-worker`, `scheduler`, `ops-api`, plus `frontend` and `ops-web`. + - Added `Dockerfile.ops-web` so the operations frontend can be built as a deployable nginx image. + - Updated the offline packaging script to build/save `ops-api`, `kol-assist-worker`, and `ops-web`, and to include the k3s directory in generated deploy packages. + - Updated the offline compose override and `.env.example` with the missing ops/security knobs. +- Files created/modified: + - `Dockerfile.ops-web` (created) + - `deploy/k3s/` (created) + - `deploy/docker-compose.yaml` (updated) + - `deploy/scripts/package.sh` (updated) + - `deploy/docker-compose.offline.yaml` (updated) + - `deploy/.env.example` (updated) + - `task_plan.md` / `findings.md` / `progress.md` (updated) + +### Phase 38: k3s Verification +- **Status:** complete +- Verification: + - `kubectl kustomize deploy/k3s` rendered 30 Kubernetes objects. + - Local YAML parsing of the rendered manifest passed. + - `bash -n deploy/scripts/package.sh && bash -n deploy/scripts/load-and-start.sh` passed. + - `git diff --check` passed. + - `pnpm --filter ops-web build` passed. + - `pnpm --filter admin-web typecheck` passed. +- Notes: + - `kubectl apply --dry-run=client` could not complete locally because the configured kube-apiserver at `127.0.0.1:26443` is not running; run live API validation on the target k3s node. + ## Session: 2026-04-20 ### Phase 20: Tracking Collect-Now Repair diff --git a/task_plan.md b/task_plan.md index 46f2c22..a66c962 100644 --- a/task_plan.md +++ b/task_plan.md @@ -4,7 +4,7 @@ Continue the desktop AI monitoring implementation by first adding a client-side local scheduler with durable queueing and adaptive execution policy, then adding a reusable hidden Playwright CDP execution layer that attaches to Electron Chromium and reuses existing account partitions. ## Current Phase -Phase 36 +Phase 37 ## Phases ### Phase 1: Progress Verification @@ -257,6 +257,18 @@ Phase 36 - [x] Run targeted backend tests, full `go test ./...`, admin-web typecheck/build, and `git diff --check` - **Status:** complete +### Phase 37: k3s Deployment Foundation +- [x] Inventory compose/runtime services and dependency graph +- [x] Add kustomize-based k3s manifests for app services, infra stateful services, migration jobs, config, secrets, and ingress +- [x] Include deployment documentation and image/package notes for online and offline clusters +- **Status:** complete + +### Phase 38: k3s Verification +- [x] Render manifests locally with kubectl/kustomize +- [x] Run YAML/static checks available in this workspace +- [x] Update findings/progress and summarize deployment commands plus required edits +- **Status:** complete + ## Key Questions 1. How should the desktop client defer and locally optimize monitor-task execution without violating one-task-per-platform serialism? 2. What is the smallest durable local cache that allows same-day resume while safely dropping stale next-day monitor tasks? @@ -307,6 +319,7 @@ Phase 36 | Materialize daily monitoring tasks with batched array INSERT | The per-brand cap is currently small, but one `INSERT ... SELECT FROM unnest(...) ON CONFLICT DO NOTHING` keeps DB round trips fixed if caps or retry batches grow | | Split daily monitoring plan loading into selection and aggregation steps | Primary-client election and platform authorization aggregation have different explainability and testability concerns; two smaller queries are easier to inspect and later move to sqlc | | Treat platform-account health as execution state, not authorization state | `health != live` should not suppress daily task planning, dashboard authorization, or detail history; only desktop-client online state gates immediate collection | +| Start k3s with a self-contained stack that mirrors the existing compose deployment | The repo already has working Docker images and compose topology; k3s should first preserve service names and runtime assumptions before introducing managed cloud replacements | ## Errors Encountered | Error | Attempt | Resolution | @@ -320,6 +333,7 @@ Phase 36 | Desktop monitor execution is currently global-serial and lacks durable local queueing | 1 | Replace the ad-hoc in-memory FIFO with a scheduler module that owns persistence, concurrency, and stale-task cleanup | | `connectOverCDP()` is not wired yet even though `playwright-core` is installed | 1 | Add a dedicated hidden-browser manager instead of pushing CDP code directly into adapters or the runtime controller | | `pnpm --filter @geo/desktop-client typecheck` fails in `electron.vite.config.ts` due to a pre-existing Vite 5/8 plugin type mismatch | 1 | Verified this change with `pnpm --filter @geo/desktop-client build` and `pnpm --filter @geo/desktop-client test`; leave config dependency alignment as a separate fix | +| `kubectl apply --dry-run=client` still attempted to contact the local kube-apiserver, which is not running at `127.0.0.1:26443` | 1 | Verified with `kubectl kustomize`, parsed the rendered YAML locally, and recorded that live API validation should run on the target k3s cluster | ## Notes - Re-check task_plan.md before major implementation decisions.