Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63e9fc5920 |
@@ -5,3 +5,5 @@
|
||||
# acceptance/contract envelope tests.
|
||||
*.go text eol=lf
|
||||
cli/acceptance/testdata/**/*.json text eol=lf
|
||||
mcp-server/Dockerfile text eol=lf
|
||||
mcp-server/requirements.txt text eol=lf
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Copy to .env and replace every REQUIRED value before deployment.
|
||||
COMPOSE_PROJECT_NAME=weknora-v071
|
||||
APP_BIND_IP=127.0.0.1
|
||||
APP_PORT=8080
|
||||
FRONTEND_BIND_IP=127.0.0.1
|
||||
FRONTEND_PORT=8088
|
||||
MCP_BIND_IP=127.0.0.1
|
||||
MCP_PORT=18082
|
||||
|
||||
# RDS PostgreSQL 17 (private VPC)
|
||||
DB_HOST=REQUIRED_RDS_PRIVATE_HOST
|
||||
DB_PORT=5432
|
||||
DB_USER=REQUIRED_DB_USER
|
||||
DB_PASSWORD=REQUIRED_DB_PASSWORD
|
||||
DB_NAME=REQUIRED_DB_NAME
|
||||
|
||||
# Self-managed or Alibaba Cloud Elasticsearch 8.x
|
||||
ELASTICSEARCH_ADDR=REQUIRED_ELASTICSEARCH_URL
|
||||
ELASTICSEARCH_USERNAME=
|
||||
ELASTICSEARCH_PASSWORD=
|
||||
ELASTICSEARCH_INDEX=weknora_chunks
|
||||
|
||||
# Self-managed Redis/Tair
|
||||
REDIS_ADDR=REQUIRED_REDIS_HOST_PORT
|
||||
REDIS_USERNAME=
|
||||
REDIS_PASSWORD=REQUIRED_REDIS_PASSWORD
|
||||
REDIS_DB=0
|
||||
REDIS_PREFIX=weknora
|
||||
WEKNORA_REDIS_NAMESPACE=REQUIRED_ENV_NAMESPACE
|
||||
REDIS_USE_TLS=false
|
||||
REDIS_TLS_SERVER_NAME=
|
||||
REDIS_TLS_INSECURE_SKIP_VERIFY=false
|
||||
|
||||
# Private Alibaba Cloud OSS
|
||||
OSS_ENDPOINT=REQUIRED_OSS_ENDPOINT
|
||||
OSS_REGION=REQUIRED_OSS_REGION
|
||||
OSS_ACCESS_KEY=REQUIRED_OSS_ACCESS_KEY
|
||||
OSS_SECRET_KEY=REQUIRED_OSS_SECRET_KEY
|
||||
OSS_BUCKET_NAME=REQUIRED_PRIVATE_BUCKET
|
||||
OSS_PATH_PREFIX=weknora/
|
||||
OSS_TEMP_BUCKET_NAME=
|
||||
OSS_TEMP_REGION=
|
||||
|
||||
# These values must be generated and stored only on the server.
|
||||
SYSTEM_AES_KEY=REQUIRED_EXACTLY_32_BYTES
|
||||
JWT_SECRET=REQUIRED_RANDOM_SECRET
|
||||
WEKNORA_API_KEY=REQUIRED_AFTER_BOOTSTRAP
|
||||
MCP_SERVER_AUTH_TOKEN=REQUIRED_RANDOM_SECRET
|
||||
|
||||
# Optional integrations
|
||||
NEO4J_ENABLE=false
|
||||
NEO4J_URI=
|
||||
NEO4J_USERNAME=
|
||||
NEO4J_PASSWORD=
|
||||
OLLAMA_BASE_URL=http://host.docker.internal:11434
|
||||
LANGFUSE_ENABLED=false
|
||||
LANGFUSE_HOST=
|
||||
LANGFUSE_PUBLIC_KEY=
|
||||
LANGFUSE_SECRET_KEY=
|
||||
|
||||
# Exact trusted hostnames only; never whitelist a whole private/Fake-IP CIDR.
|
||||
SSRF_WHITELIST_EXTRA=host.docker.internal
|
||||
|
||||
# Runtime tuning matching the current low-concurrency baseline.
|
||||
MAX_FILE_SIZE_MB=100
|
||||
WEKNORA_ASYNQ_CORE_CONCURRENCY=4
|
||||
WEKNORA_ASYNQ_POSTPROCESS_CONCURRENCY=1
|
||||
WEKNORA_ASYNQ_ENRICHMENT_CONCURRENCY=4
|
||||
WEKNORA_WIKI_ASYNQ_CONCURRENCY=2
|
||||
CONCURRENCY_POOL_SIZE=4
|
||||
@@ -0,0 +1,18 @@
|
||||
# Runtime secrets and generated state
|
||||
.env
|
||||
*.env
|
||||
!.env.example
|
||||
secrets/
|
||||
credentials/
|
||||
|
||||
# Persistent data and backups
|
||||
data/
|
||||
backups/
|
||||
volumes/
|
||||
exports/
|
||||
|
||||
# Logs, caches, and generated evidence
|
||||
logs/
|
||||
*.log
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -0,0 +1,63 @@
|
||||
# 当前服务器 WeKnora v0.7.1 发布包
|
||||
|
||||
本目录固化 2026-08-26 正在运行的 WeKnora 核心版本,并提供迁移到新服务器时使用的无密钥部署模板。
|
||||
|
||||
## 版本身份
|
||||
|
||||
- WeKnora 源码:`c64a48647cd6f7eb8b0fb020b2e8fec74ee375fb`
|
||||
- 上游标签:`v0.7.1`
|
||||
- App 镜像:`wechatopenai/weknora-app:v0.7.1@sha256:d88bef9912f6abb8bc7c22144ee7f314016055b8075bda4ea8fbb28af41c3bcf`
|
||||
- UI 镜像:`wechatopenai/weknora-ui:v0.7.1@sha256:169678d4ba4c1f127b36c5f58a6ad567bd745d48488d693e4d3eb4117689c730`
|
||||
- DocReader 镜像:`wechatopenai/weknora-docreader:v0.7.1@sha256:262bab1a4b12f24dd99958e32868ffc2d2cb09270465a88820420ac378c0e149`
|
||||
- 当前 App 二进制 SHA-256:`aa6c6223f9980ef08120e65cdc091996b1eadbc828c69540e7e6b9cdc24bbca3`
|
||||
|
||||
`runtime-manifest.json` 保存运行容器、镜像内容 ID 和源码修订的脱敏取证结果。当前 App 容器的 OCI revision 与源码 Commit 一致。
|
||||
|
||||
## 与上游 v0.7.1 的唯一代码差异
|
||||
|
||||
当前 MCP 镜像需要 MCP Python SDK 1.x,因此本分支保留两处运行修复:
|
||||
|
||||
1. `mcp-server/requirements.txt` 将依赖固定为 `mcp>=1.0.0,<2.0.0`;
|
||||
2. `mcp-server/Dockerfile` 在构建期验证 `Server.list_tools()` 契约。
|
||||
|
||||
WeKnora App、UI、DocReader本身使用上述官方不可变镜像,没有把钉钉专用分支混入当前运行版本。
|
||||
|
||||
## 文件用途
|
||||
|
||||
- `compose.managed-data-plane.yaml`:新阿里云服务器部署核心服务的模板;RDS PostgreSQL、Elasticsearch、Redis和私有OSS从环境变量接入。
|
||||
- `.env.example`:只包含变量名和非敏感默认值;复制为`.env`后填写真实值。
|
||||
- `runtime-manifest.json`:当前服务器运行版本取证,不用于保存运行数据。
|
||||
|
||||
静态检查:
|
||||
|
||||
```bash
|
||||
cp deploy/server-running-v0.7.1/.env.example deploy/server-running-v0.7.1/.env
|
||||
# 填写所有 REQUIRED 值后:
|
||||
docker compose \
|
||||
--env-file deploy/server-running-v0.7.1/.env \
|
||||
-f deploy/server-running-v0.7.1/compose.managed-data-plane.yaml \
|
||||
config --quiet
|
||||
```
|
||||
|
||||
`.env`受Git忽略。生产密钥必须由服务器上的`0600`文件或云Secret管理注入。
|
||||
|
||||
## 不进入 Gitea 的内容
|
||||
|
||||
- 当前服务器`.env`和所有API Key、Token、密码、`SYSTEM_AES_KEY`;
|
||||
- PostgreSQL、Redis、Elasticsearch、Neo4j、ClickHouse数据卷;
|
||||
- OSS对象、上传文档、`/data/files`、日志、缓存和验收中的业务数据;
|
||||
- 已构建的DobotAI二进制;
|
||||
- 旧的本地API Adapter目录。Adapter和DobotAI必须使用各自独立仓库及固定Commit部署。
|
||||
|
||||
## 新服务器迁移边界
|
||||
|
||||
此分支固化了“代码和部署契约”,不等于已经迁移数据。迁移时还必须单独完成:
|
||||
|
||||
1. PostgreSQL原生逻辑备份和隔离恢复验证;
|
||||
2. OSS对象清单、大小和SHA-256核对;
|
||||
3. Elasticsearch索引重建或受控迁移;
|
||||
4. 保管并恢复原`SYSTEM_AES_KEY`,否则数据库内模型凭据无法解密;
|
||||
5. 健康、文档解析、混合检索、引用、图片、Agent和Adapter端到端验收;
|
||||
6. 回滚副本验证。
|
||||
|
||||
v0.7.1 PostgreSQL连接当前使用`sslmode=disable`。接入阿里云RDS时必须走私有VPC;若目标强制TLS,需要先改造并验证连接配置,不能直接上线。
|
||||
@@ -0,0 +1,148 @@
|
||||
name: ${COMPOSE_PROJECT_NAME:-weknora-v071}
|
||||
|
||||
services:
|
||||
docreader:
|
||||
image: wechatopenai/weknora-docreader:v0.7.1@sha256:262bab1a4b12f24dd99958e32868ffc2d2cb09270465a88820420ac378c0e149
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
LOG_LEVEL: INFO
|
||||
DOCREADER_IMAGE_OUTPUT_DIR: /tmp/docreader
|
||||
DOCREADER_GRPC_PORT: 50051
|
||||
DOCREADER_GRPC_MAX_WORKERS: 2
|
||||
DOCREADER_MARKITDOWN_MAX_WORKERS: 1
|
||||
DOCREADER_PDF_RENDER_MAX_WORKERS: 1
|
||||
volumes:
|
||||
- docreader_tmp:/tmp/docreader
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import socket;s=socket.create_connection(('127.0.0.1',50051),5);s.close()"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 20
|
||||
start_period: 30s
|
||||
restart: unless-stopped
|
||||
networks: [weknora]
|
||||
|
||||
app:
|
||||
image: wechatopenai/weknora-app:v0.7.1@sha256:d88bef9912f6abb8bc7c22144ee7f314016055b8075bda4ea8fbb28af41c3bcf
|
||||
depends_on:
|
||||
docreader:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
GIN_MODE: release
|
||||
TZ: Asia/Shanghai
|
||||
AUTO_MIGRATE: "true"
|
||||
AUTO_RECOVER_DIRTY: "true"
|
||||
DISABLE_REGISTRATION: "true"
|
||||
DB_DRIVER: postgres
|
||||
DB_HOST: ${DB_HOST:?DB_HOST is required}
|
||||
DB_PORT: ${DB_PORT:-5432}
|
||||
DB_USER: ${DB_USER:?DB_USER is required}
|
||||
DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
|
||||
DB_NAME: ${DB_NAME:?DB_NAME is required}
|
||||
RETRIEVE_DRIVER: elasticsearch_v8
|
||||
ELASTICSEARCH_ADDR: ${ELASTICSEARCH_ADDR:?ELASTICSEARCH_ADDR is required}
|
||||
ELASTICSEARCH_USERNAME: ${ELASTICSEARCH_USERNAME:-}
|
||||
ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD:-}
|
||||
ELASTICSEARCH_INDEX: ${ELASTICSEARCH_INDEX:-weknora_chunks}
|
||||
STREAM_MANAGER_TYPE: redis
|
||||
REDIS_ADDR: ${REDIS_ADDR:?REDIS_ADDR is required}
|
||||
REDIS_USERNAME: ${REDIS_USERNAME:-}
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
|
||||
REDIS_DB: ${REDIS_DB:-0}
|
||||
REDIS_PREFIX: ${REDIS_PREFIX:-weknora}
|
||||
WEKNORA_REDIS_NAMESPACE: ${WEKNORA_REDIS_NAMESPACE:?WEKNORA_REDIS_NAMESPACE is required}
|
||||
REDIS_USE_TLS: ${REDIS_USE_TLS:-false}
|
||||
REDIS_TLS_SERVER_NAME: ${REDIS_TLS_SERVER_NAME:-}
|
||||
REDIS_TLS_INSECURE_SKIP_VERIFY: ${REDIS_TLS_INSECURE_SKIP_VERIFY:-false}
|
||||
STORAGE_TYPE: oss
|
||||
OSS_ENDPOINT: ${OSS_ENDPOINT:?OSS_ENDPOINT is required}
|
||||
OSS_REGION: ${OSS_REGION:?OSS_REGION is required}
|
||||
OSS_ACCESS_KEY: ${OSS_ACCESS_KEY:?OSS_ACCESS_KEY is required}
|
||||
OSS_SECRET_KEY: ${OSS_SECRET_KEY:?OSS_SECRET_KEY is required}
|
||||
OSS_BUCKET_NAME: ${OSS_BUCKET_NAME:?OSS_BUCKET_NAME is required}
|
||||
OSS_PATH_PREFIX: ${OSS_PATH_PREFIX:-weknora/}
|
||||
OSS_TEMP_BUCKET_NAME: ${OSS_TEMP_BUCKET_NAME:-}
|
||||
OSS_TEMP_REGION: ${OSS_TEMP_REGION:-}
|
||||
DOCREADER_ADDR: docreader:50051
|
||||
DOCREADER_TRANSPORT: grpc
|
||||
NEO4J_ENABLE: ${NEO4J_ENABLE:-false}
|
||||
NEO4J_URI: ${NEO4J_URI:-}
|
||||
NEO4J_USERNAME: ${NEO4J_USERNAME:-}
|
||||
NEO4J_PASSWORD: ${NEO4J_PASSWORD:-}
|
||||
SYSTEM_AES_KEY: ${SYSTEM_AES_KEY:?SYSTEM_AES_KEY is required}
|
||||
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required}
|
||||
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
|
||||
OLLAMA_OPTIONAL: "true"
|
||||
LANGFUSE_ENABLED: ${LANGFUSE_ENABLED:-false}
|
||||
LANGFUSE_HOST: ${LANGFUSE_HOST:-}
|
||||
LANGFUSE_PUBLIC_KEY: ${LANGFUSE_PUBLIC_KEY:-}
|
||||
LANGFUSE_SECRET_KEY: ${LANGFUSE_SECRET_KEY:-}
|
||||
SSRF_WHITELIST_EXTRA: ${SSRF_WHITELIST_EXTRA:-host.docker.internal}
|
||||
WEKNORA_TENANT_AUTO_CREATE_API_KEY: "false"
|
||||
WEKNORA_SANDBOX_MODE: disabled
|
||||
WEKNORA_ASYNQ_CORE_CONCURRENCY: ${WEKNORA_ASYNQ_CORE_CONCURRENCY:-4}
|
||||
WEKNORA_ASYNQ_POSTPROCESS_CONCURRENCY: ${WEKNORA_ASYNQ_POSTPROCESS_CONCURRENCY:-1}
|
||||
WEKNORA_ASYNQ_ENRICHMENT_CONCURRENCY: ${WEKNORA_ASYNQ_ENRICHMENT_CONCURRENCY:-4}
|
||||
WEKNORA_WIKI_ASYNQ_CONCURRENCY: ${WEKNORA_WIKI_ASYNQ_CONCURRENCY:-2}
|
||||
CONCURRENCY_POOL_SIZE: ${CONCURRENCY_POOL_SIZE:-4}
|
||||
MAX_FILE_SIZE_MB: ${MAX_FILE_SIZE_MB:-100}
|
||||
ports:
|
||||
- "${APP_BIND_IP:-127.0.0.1}:${APP_PORT:-8080}:8080"
|
||||
volumes:
|
||||
- app_files:/data/files
|
||||
- docreader_tmp:/tmp/docreader:ro
|
||||
- ../../config/config.yaml:/app/config/config.yaml:ro
|
||||
- ../../skills/preloaded:/app/skills/preloaded
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
start_period: 60s
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
restart: unless-stopped
|
||||
networks: [weknora]
|
||||
|
||||
frontend:
|
||||
image: wechatopenai/weknora-ui:v0.7.1@sha256:169678d4ba4c1f127b36c5f58a6ad567bd745d48488d693e4d3eb4117689c730
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
APP_HOST: app
|
||||
APP_PORT: 8080
|
||||
APP_SCHEME: http
|
||||
MAX_FILE_SIZE_MB: ${MAX_FILE_SIZE_MB:-100}
|
||||
ports:
|
||||
- "${FRONTEND_BIND_IP:-127.0.0.1}:${FRONTEND_PORT:-8088}:80"
|
||||
restart: unless-stopped
|
||||
networks: [weknora]
|
||||
|
||||
mcp:
|
||||
profiles: ["mcp"]
|
||||
build:
|
||||
context: ../../mcp-server
|
||||
image: weknora-mcp:v0.7.1-server-running
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
WEKNORA_BASE_URL: http://app:8080/api/v1
|
||||
WEKNORA_API_KEY: ${WEKNORA_API_KEY:?WEKNORA_API_KEY is required for MCP profile}
|
||||
MCP_SERVER_AUTH_TOKEN: ${MCP_SERVER_AUTH_TOKEN:?MCP_SERVER_AUTH_TOKEN is required for MCP profile}
|
||||
WEKNORA_CHAT_TIMEOUT: 300
|
||||
WEKNORA_VERIFY_SSL: "true"
|
||||
MCP_ALLOWED_UPLOAD_DIRS: ""
|
||||
ports:
|
||||
- "${MCP_BIND_IP:-127.0.0.1}:${MCP_PORT:-18082}:8000"
|
||||
restart: unless-stopped
|
||||
networks: [weknora]
|
||||
|
||||
networks:
|
||||
weknora:
|
||||
name: ${COMPOSE_PROJECT_NAME:-weknora-v071}-network
|
||||
|
||||
volumes:
|
||||
app_files:
|
||||
docreader_tmp:
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"captured_at": "2026-08-26T18:35:58+08:00",
|
||||
"capture_scope": "current server, read-only, secrets excluded",
|
||||
"compose": {
|
||||
"project": "weknora071full",
|
||||
"config_file": "/home/zheromiku/weknora-local-full/compose.yaml",
|
||||
"api_health": "http://127.0.0.1:8080/health returned HTTP 200",
|
||||
"frontend_health": "http://127.0.0.1:8088 returned HTTP 200"
|
||||
},
|
||||
"source": {
|
||||
"repository": "https://gitea.zheromiku.top/Weknora/Enterprise-Knowledge-Base.git",
|
||||
"upstream_tag": "v0.7.1",
|
||||
"commit": "c64a48647cd6f7eb8b0fb020b2e8fec74ee375fb",
|
||||
"packaging_branch": "release/server-running-v0.7.1",
|
||||
"app_binary_sha256": "aa6c6223f9980ef08120e65cdc091996b1eadbc828c69540e7e6b9cdc24bbca3"
|
||||
},
|
||||
"core_images": [
|
||||
{
|
||||
"service": "app",
|
||||
"container_id": "08ffd65c295efbf732b52ca3b3728fe9b423d18bc7fefcb047d0a939be2c6a73",
|
||||
"image": "wechatopenai/weknora-app:v0.7.1@sha256:d88bef9912f6abb8bc7c22144ee7f314016055b8075bda4ea8fbb28af41c3bcf",
|
||||
"image_content_id": "sha256:5036f926f9888aec13b2713d90efc0d7e96320141493bdddded5f25788f1e73b",
|
||||
"oci_revision": "c64a48647cd6f7eb8b0fb020b2e8fec74ee375fb",
|
||||
"running_at_capture": true
|
||||
},
|
||||
{
|
||||
"service": "frontend",
|
||||
"container_id": "a62eb44dcd5e9f0dba666b991c69fff9bbd58592d73afa8f2d4e9e70f74fe64c",
|
||||
"image": "wechatopenai/weknora-ui:v0.7.1@sha256:169678d4ba4c1f127b36c5f58a6ad567bd745d48488d693e4d3eb4117689c730",
|
||||
"image_content_id": "sha256:5973deb6f996ee6a6a21a4f32f7a7e47cc5cfa4f50c0dfceee48c9335a691110",
|
||||
"oci_revision": "c64a48647cd6f7eb8b0fb020b2e8fec74ee375fb",
|
||||
"running_at_capture": true
|
||||
},
|
||||
{
|
||||
"service": "docreader",
|
||||
"container_id": "038a52048ae15daf2940d440ed8964cabf258b03bb5afcf9f9865baa72da3b11",
|
||||
"image": "wechatopenai/weknora-docreader:v0.7.1@sha256:262bab1a4b12f24dd99958e32868ffc2d2cb09270465a88820420ac378c0e149",
|
||||
"image_content_id": "sha256:154dc42ff610bd9d06e54f9c7e827df85b63987aeda484c7e6d412d97f08e45f",
|
||||
"oci_revision": "c64a48647cd6f7eb8b0fb020b2e8fec74ee375fb",
|
||||
"running_at_capture": true
|
||||
},
|
||||
{
|
||||
"service": "mcp",
|
||||
"container_id": "80bc682a0589e9efcdf8169101e507bd0fb64f49b4315f1ec181defe6cb92f41",
|
||||
"image": "weknora-mcp:local",
|
||||
"image_content_id": "sha256:3cd8bdf8a6db1c443a33cbd95db744ad28a4a8a8e55615cda1cd9703f6516a74",
|
||||
"source": "v0.7.1 plus the two MCP SDK compatibility changes committed on the packaging branch",
|
||||
"running_at_capture": true
|
||||
}
|
||||
],
|
||||
"data_plane_observed": [
|
||||
{
|
||||
"service": "postgres",
|
||||
"image": "postgres:17-alpine",
|
||||
"image_content_id": "sha256:93aa428db0aeeb71d24dcad1491bef6e1396a4255697e4bfc4c725bfeb981b74"
|
||||
},
|
||||
{
|
||||
"service": "redis",
|
||||
"image": "redis:7.0-alpine",
|
||||
"image_content_id": "sha256:426f6fb54a1b82a2254bce70e5f4a72d15c14ecd3cc70d799465b956e7a32548"
|
||||
},
|
||||
{
|
||||
"service": "elasticsearch",
|
||||
"image": "docker.elastic.co/elasticsearch/elasticsearch:8.17.10",
|
||||
"image_content_id": "sha256:89e31594e1405bc7f2aa8aca12b02a6da165540c37cadfe7394a62baef6924e3"
|
||||
},
|
||||
{
|
||||
"service": "neo4j",
|
||||
"image": "neo4j:2025.10.1",
|
||||
"image_content_id": "sha256:5ff6e5ba92302482483cb07e6278ed76511ca6692acb93a4df3df07d1fea88ad"
|
||||
}
|
||||
],
|
||||
"effective_non_secret_configuration": {
|
||||
"DB_DRIVER": "postgres",
|
||||
"RETRIEVE_DRIVER": "elasticsearch_v8",
|
||||
"STORAGE_TYPE": "oss",
|
||||
"OSS_REGION": "cn-shenzhen",
|
||||
"NEO4J_ENABLE": true,
|
||||
"APP_PORT": 8080,
|
||||
"FRONTEND_PORT": 8088
|
||||
},
|
||||
"excluded_from_git": [
|
||||
".env and all credentials",
|
||||
"SYSTEM_AES_KEY and JWT/API tokens",
|
||||
"database, cache, search, graph and observability data",
|
||||
"OSS objects and uploaded/generated files",
|
||||
"logs, caches, runtime reports containing business identifiers",
|
||||
"host-built DobotAI binary",
|
||||
"unversioned legacy API Adapter source"
|
||||
],
|
||||
"separately_versioned_integrations": {
|
||||
"dobotai": {
|
||||
"repository": "https://gitea.zheromiku.top/Weknora/dobotai-platform.git",
|
||||
"current_worktree_commit_at_capture": "aa0513eb59dc3189ad59bd28b4990f998d19e2bd",
|
||||
"note": "The host-mounted runtime binary is not copied into this repository."
|
||||
},
|
||||
"knowledge_adapter": {
|
||||
"note": "The currently running legacy adapter image came from an unversioned local directory and is intentionally excluded. The replacement adapter must be published from its own repository before cloud deployment."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -457,7 +457,29 @@ func (s *knowledgeService) processChunks(ctx context.Context,
|
||||
return insertChunks[i].ChunkIndex < insertChunks[j].ChunkIndex
|
||||
})
|
||||
|
||||
textChunks := processTextChunks(insertChunks, hasParentChild)
|
||||
// 仅为文本类型的Chunk设置前后关系(child chunks only, parents already linked above)
|
||||
textChunks := make([]*types.Chunk, 0, len(chunks))
|
||||
for _, chunk := range insertChunks {
|
||||
if chunk.ChunkType == types.ChunkTypeText && chunk.ParentChunkID != "" {
|
||||
// This is a child chunk in parent-child mode
|
||||
textChunks = append(textChunks, chunk)
|
||||
} else if chunk.ChunkType == types.ChunkTypeText && !hasParentChild {
|
||||
// Normal flat chunk (no parent-child mode)
|
||||
textChunks = append(textChunks, chunk)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置文本Chunk之间的前后关系 (skip if parent-child, children don't need prev/next links)
|
||||
if !hasParentChild {
|
||||
for i, chunk := range textChunks {
|
||||
if i > 0 {
|
||||
textChunks[i-1].NextChunkID = chunk.ID
|
||||
}
|
||||
if i < len(textChunks)-1 {
|
||||
textChunks[i+1].PreChunkID = chunk.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if knowledge is being deleted/cancelled before writing chunks.
|
||||
// Nothing has been persisted yet, so both branches just bail.
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package service
|
||||
|
||||
import "github.com/Tencent/WeKnora/internal/types"
|
||||
|
||||
// processTextChunks selects indexing inputs without linking parent-child context
|
||||
// as if it were a flat document.
|
||||
func processTextChunks(chunks []*types.Chunk, hasParentChild bool) []*types.Chunk {
|
||||
textChunks := make([]*types.Chunk, 0, len(chunks))
|
||||
for _, chunk := range chunks {
|
||||
// Parent-less text can coexist with parent-child output (for example
|
||||
// table chunks). Persisting it without indexing silently loses recall.
|
||||
if chunk.ChunkType == types.ChunkTypeText {
|
||||
textChunks = append(textChunks, chunk)
|
||||
}
|
||||
}
|
||||
if !hasParentChild {
|
||||
for i, chunk := range textChunks {
|
||||
if i > 0 {
|
||||
textChunks[i-1].NextChunkID = chunk.ID
|
||||
}
|
||||
if i < len(textChunks)-1 {
|
||||
textChunks[i+1].PreChunkID = chunk.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
return textChunks
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProcessTextChunksIncludesUnparentedText(t *testing.T) {
|
||||
parent := &types.Chunk{ID: "parent", ChunkType: types.ChunkTypeParentText}
|
||||
child := &types.Chunk{ID: "child", ChunkType: types.ChunkTypeText, ParentChunkID: "parent"}
|
||||
standalone := &types.Chunk{ID: "standalone", ChunkType: types.ChunkTypeText}
|
||||
got := processTextChunks([]*types.Chunk{parent, child, standalone}, true)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("indexed text count = %d, want 2; unparented text must not disappear in parent-child mode", len(got))
|
||||
}
|
||||
if got[0] != child || got[1] != standalone {
|
||||
t.Fatal("must preserve text identity/order and exclude parent_text")
|
||||
}
|
||||
if child.NextChunkID != "" || standalone.PreChunkID != "" {
|
||||
t.Fatal("parent-child mode must not create flat adjacency links")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessTextChunksFlatLinks(t *testing.T) {
|
||||
first := &types.Chunk{ID: "first", ChunkType: types.ChunkTypeText}
|
||||
second := &types.Chunk{ID: "second", ChunkType: types.ChunkTypeText}
|
||||
got := processTextChunks([]*types.Chunk{first, {ID: "summary", ChunkType: types.ChunkTypeSummary}, second}, false)
|
||||
if len(got) != 2 || got[0] != first || got[1] != second {
|
||||
t.Fatal("flat text identity/order changed")
|
||||
}
|
||||
if first.NextChunkID != "second" || second.PreChunkID != "first" {
|
||||
t.Fatal("flat adjacency links missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessTextChunksParentsOnly(t *testing.T) {
|
||||
if got := processTextChunks([]*types.Chunk{{ID: "parent", ChunkType: types.ChunkTypeParentText}}, true); len(got) != 0 {
|
||||
t.Fatal("parent_text must remain stored-only")
|
||||
}
|
||||
if got := processTextChunks(nil, false); len(got) != 0 {
|
||||
t.Fatal("empty input must remain empty")
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -14,7 +13,6 @@ import (
|
||||
)
|
||||
|
||||
type fakeTenantAPIKeyRepo struct {
|
||||
mu sync.Mutex
|
||||
byHash map[string]*types.TenantAPIKey
|
||||
nextID uint64
|
||||
lastUsedUpdateCount int
|
||||
@@ -45,8 +43,6 @@ func newFakeTenantAPIKeyRepo() *fakeTenantAPIKeyRepo {
|
||||
}
|
||||
|
||||
func (r *fakeTenantAPIKeyRepo) CreateAPIKey(_ context.Context, key *types.TenantAPIKey) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if _, ok := r.byHash[key.KeyHash]; ok {
|
||||
return errors.New("duplicate key hash")
|
||||
}
|
||||
@@ -59,8 +55,6 @@ func (r *fakeTenantAPIKeyRepo) CreateAPIKey(_ context.Context, key *types.Tenant
|
||||
}
|
||||
|
||||
func (r *fakeTenantAPIKeyRepo) GetAPIKeyByHash(_ context.Context, hash string) (*types.TenantAPIKey, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
key, ok := r.byHash[hash]
|
||||
if !ok {
|
||||
return nil, apprepo.ErrTenantAPIKeyNotFound
|
||||
@@ -70,8 +64,6 @@ func (r *fakeTenantAPIKeyRepo) GetAPIKeyByHash(_ context.Context, hash string) (
|
||||
}
|
||||
|
||||
func (r *fakeTenantAPIKeyRepo) ListAPIKeys(_ context.Context, tenantID uint64) ([]*types.TenantAPIKey, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
out := []*types.TenantAPIKey{}
|
||||
for _, key := range r.byHash {
|
||||
if key.TenantIDValue() == tenantID && key.RevokedAt == nil {
|
||||
@@ -83,8 +75,6 @@ func (r *fakeTenantAPIKeyRepo) ListAPIKeys(_ context.Context, tenantID uint64) (
|
||||
}
|
||||
|
||||
func (r *fakeTenantAPIKeyRepo) ListPlatformAPIKeys(_ context.Context) ([]*types.TenantAPIKey, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
out := []*types.TenantAPIKey{}
|
||||
for _, key := range r.byHash {
|
||||
if key.IsPlatform() && key.RevokedAt == nil {
|
||||
@@ -96,8 +86,6 @@ func (r *fakeTenantAPIKeyRepo) ListPlatformAPIKeys(_ context.Context) ([]*types.
|
||||
}
|
||||
|
||||
func (r *fakeTenantAPIKeyRepo) RevokeAPIKey(_ context.Context, tenantID uint64, id uint64) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
now := time.Now()
|
||||
for _, key := range r.byHash {
|
||||
if key.ID == id && key.TenantIDValue() == tenantID && key.RevokedAt == nil {
|
||||
@@ -109,8 +97,6 @@ func (r *fakeTenantAPIKeyRepo) RevokeAPIKey(_ context.Context, tenantID uint64,
|
||||
}
|
||||
|
||||
func (r *fakeTenantAPIKeyRepo) RevokePlatformAPIKey(_ context.Context, id uint64) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
now := time.Now()
|
||||
for _, key := range r.byHash {
|
||||
if key.ID == id && key.IsPlatform() && key.RevokedAt == nil {
|
||||
@@ -122,8 +108,6 @@ func (r *fakeTenantAPIKeyRepo) RevokePlatformAPIKey(_ context.Context, id uint64
|
||||
}
|
||||
|
||||
func (r *fakeTenantAPIKeyRepo) UpdateAPIKeyHash(_ context.Context, id uint64, hash string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for oldHash, key := range r.byHash {
|
||||
if key.ID == id && key.RevokedAt == nil {
|
||||
delete(r.byHash, oldHash)
|
||||
@@ -136,8 +120,6 @@ func (r *fakeTenantAPIKeyRepo) UpdateAPIKeyHash(_ context.Context, id uint64, ha
|
||||
}
|
||||
|
||||
func (r *fakeTenantAPIKeyRepo) HasKeysWithPlaceholderHash(_ context.Context) (bool, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for _, key := range r.byHash {
|
||||
if key.RevokedAt == nil && strings.HasPrefix(key.KeyHash, "migrated-tenant-") {
|
||||
return true, nil
|
||||
@@ -147,8 +129,6 @@ func (r *fakeTenantAPIKeyRepo) HasKeysWithPlaceholderHash(_ context.Context) (bo
|
||||
}
|
||||
|
||||
func (r *fakeTenantAPIKeyRepo) ListKeysWithPlaceholderHash(_ context.Context) ([]*types.TenantAPIKey, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
out := []*types.TenantAPIKey{}
|
||||
for _, key := range r.byHash {
|
||||
if key.RevokedAt == nil && strings.HasPrefix(key.KeyHash, "migrated-tenant-") {
|
||||
@@ -160,8 +140,6 @@ func (r *fakeTenantAPIKeyRepo) ListKeysWithPlaceholderHash(_ context.Context) ([
|
||||
}
|
||||
|
||||
func (r *fakeTenantAPIKeyRepo) UpdateAPIKeyLastUsed(_ context.Context, id uint64, at time.Time) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.lastUsedUpdateCount++
|
||||
for _, key := range r.byHash {
|
||||
if key.ID == id && key.RevokedAt == nil {
|
||||
@@ -275,11 +253,11 @@ func TestTenantAPIKeyServiceAuthenticateThrottlesLastUsedUpdates(t *testing.T) {
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(500 * time.Millisecond)
|
||||
for repo.lastUsedCount() == 0 && time.Now().Before(deadline) {
|
||||
for repo.lastUsedUpdateCount == 0 && time.Now().Before(deadline) {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if count := repo.lastUsedCount(); count != 1 {
|
||||
t.Fatalf("last_used update count = %d, want 1 (throttled async write)", count)
|
||||
if repo.lastUsedUpdateCount != 1 {
|
||||
t.Fatalf("last_used update count = %d, want 1 (throttled async write)", repo.lastUsedUpdateCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,9 +279,3 @@ func TestTenantAPIKeyServiceAuthenticateRejectsExpiredKey(t *testing.T) {
|
||||
t.Fatal("expired key should not authenticate")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *fakeTenantAPIKeyRepo) lastUsedCount() int {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.lastUsedUpdateCount
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package event
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -202,13 +201,13 @@ func TestEventBus_EmitAndWait(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
bus := NewAsyncEventBus()
|
||||
|
||||
var counter atomic.Int32
|
||||
counter := 0
|
||||
|
||||
// Register handlers
|
||||
for i := 0; i < 3; i++ {
|
||||
bus.On(EventQueryReceived, func(ctx context.Context, event Event) error {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
counter.Add(1)
|
||||
counter++
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -223,8 +222,8 @@ func TestEventBus_EmitAndWait(t *testing.T) {
|
||||
t.Errorf("EmitAndWait failed: %v", err)
|
||||
}
|
||||
|
||||
if got := counter.Load(); got != 3 {
|
||||
t.Errorf("Expected 3 handlers to complete, got %d", got)
|
||||
if counter != 3 {
|
||||
t.Errorf("Expected 3 handlers to complete, got %d", counter)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-17
@@ -1,17 +1,18 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
RUN pip install --no-cache-dir -e .
|
||||
|
||||
ENV MCP_HOST=0.0.0.0
|
||||
ENV MCP_PORT=8000
|
||||
ENV WEKNORA_BASE_URL=http://app:8080/api/v1
|
||||
# Set MCP_SERVER_AUTH_TOKEN at runtime; HTTP transport refuses to start without it.
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["weknora-mcp-server", "--transport", "http", "--host", "0.0.0.0", "--port", "8000"]
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN python -c "from mcp.server import Server; assert hasattr(Server('contract-check'), 'list_tools'), 'incompatible mcp SDK'"
|
||||
|
||||
COPY . .
|
||||
RUN pip install --no-cache-dir -e .
|
||||
|
||||
ENV MCP_HOST=0.0.0.0
|
||||
ENV MCP_PORT=8000
|
||||
ENV WEKNORA_BASE_URL=http://app:8080/api/v1
|
||||
# Set MCP_SERVER_AUTH_TOKEN at runtime; HTTP transport refuses to start without it.
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["weknora-mcp-server", "--transport", "http", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
mcp>=1.0.0
|
||||
requests>=2.31.0
|
||||
starlette>=0.27.0
|
||||
mcp>=1.0.0,<2.0.0
|
||||
requests>=2.31.0
|
||||
starlette>=0.27.0
|
||||
uvicorn>=0.24.0
|
||||
Reference in New Issue
Block a user