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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
<node-ip> geo-rankly.local
|
||||
<node-ip> 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 <pod-name>
|
||||
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` 即可。
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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: ""
|
||||
@@ -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 一致的语言。
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: geo-rankly
|
||||
@@ -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: ""
|
||||
Reference in New Issue
Block a user