chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:36 +08:00
commit 067d663756
3885 changed files with 1572473 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# Celery 的broker配置。存储ft指令执行结果。
# 密码加密规则和backend一致, 暂不支持集群redis
redis_url: "redis://bisheng-redis:6379/5"
+57
View File
@@ -0,0 +1,57 @@
# 可根据loguru的文档配置不同 handlers
logger_conf:
# 默认输出到控制台的日志级别, 大于等于此级别都会输出
level: DEBUG
# 默认输出格式
format: '[{time:YYYY-MM-DD HH:mm:ss.SSSSSS}] [{level.name} process-{process.id}-{thread.id} {name}:{line}] - trace={extra[trace_id]} {message}'
# 参考loguru.add()中的参数可以配置多个handler
handlers:
# 文件路径,支持插入一些系统环境变量,若环境变量不存在则置空。例如 HOSTNAME: 主机名。后端会处理环境变量的替换
- sink: "/app/logs/bisheng_uns.log"
# 日志级别
level: INFO
# 和原生不一样,后端会将配置使用eval()执行转为函数用来过滤特定日志级别。推荐lambda
# filter: "lambda record: record['level'].name == 'INFO'"
# 日志格式化函数,extra内支持trace_id
format: "[{time:YYYY-MM-DD HH:mm:ss.SSSSSS}]|{level}|BISHENG|{extra[trace_id]}|{process.id}|{thread.id}|{message}"
# 每天的几点进行切割
rotation: "00:00"
retention: "3 Days"
- sink: "/app/logs/err-v0-BISHENG-UNS-{HOSTNAME}.log"
level: ERROR
filter: "lambda record: record['level'].name == 'ERROR'"
format: "[{time:YYYY-MM-DD HH:mm:ss.SSSSSS}]|{level}|BISHENG|{extra[trace_id]}||{process.id}|{thread.id}|||#EX_ERR:POS={name},line {line},ERR=500,EMSG={message}"
rotation: "00:00"
retention: "3 Days"
# pdf解析需要用到的模型配置, 配置了rt_server环境变量的话会替换为对应的地址
pdf_model_params:
layout_ep: "http://192.168.106.12:9001/v2.1/models/elem_layout_v1/infer"
cell_model_ep: "http://192.168.106.12:9001/v2.1/models/elem_table_cell_detect_v1/infer"
rowcol_model_ep: "http://192.168.106.12:9001/v2.1/models/elem_table_rowcol_detect_v1/infer"
table_model_ep: "http://192.168.106.12:9001/v2.1/models/elem_table_detect_v1/infer"
ocr_model_ep: "http://192.168.106.12:9001/v2.1/models/elem_ocr_collection_v3/infer"
# 是否全部走ocr识别, false的话则由代码逻辑判断是否需要走ocr识别
is_all_ocr: false
# ocr识别需要的配置项
ocr_conf:
params:
sort_filter_boxes: true,
enable_huarong_box_adjust: true,
rotateupright: false,
support_long_image_segment: true,
split_long_sentence_blank: true
scene_mapping:
print:
det: general_text_det_mrcnn_v2.0
recog: transformer-blank-v0.2-faster
hand:
det: general_text_det_mrcnn_v2.0
recog: transformer-hand-v1.16-faster
print_recog:
recog: transformer-blank-v0.2-faster
hand_recog:
recog: transformer-hand-v1.16-faster
det:
det: general_text_det_mrcnn_v2.0
+87
View File
@@ -0,0 +1,87 @@
# 数据库配置, 当前加密串的密码是1234,
# 密码加密参考 https://dataelem.feishu.cn/wiki/BSCcwKd4Yiot3IkOEC8cxGW7nPc#Gxitd1xEeof1TzxdhINcGS6JnXd
database_url:
"mysql+pymysql://root:gAAAAABlp4b4c59FeVGF_OQRVf6NOUIGdxq8246EBD-b0hdK_jVKRs1x4PoAn0A6C5S6IiFKmWn0Nm5eBUWu-7jxcqw6TiVjQA==@mysql:3306/bisheng?charset=utf8mb4"
# 缓存配置 redis://[[username]:[password]]@localhost:6379/0
# 如果设置了密码,需要参考MySQL密码的加密逻辑对密码进行加密。eg: redis://root:gAAAAABlp4b4c59FeVGF_OQRVf6NOUIGdxq8246EBD-b0hdK_jVKRs1x4PoAn0A6C5S6IiFKmWn0Nm5eBUWu-7jxcqw6TiVjQA==@redis:6379/0
# 普通模式:
redis_url: "redis://redis:6379/1"
# 集群模式或者哨兵模式(只能选其一):
# redis_url:
# mode: "cluster"
# startup_nodes:
# - {"host": "192.168.106.115", "port": 6002}
# password: encrypt(gAAAAABlp4b4c59FeVGF_OQRVf6NOUIGdxq8246EBD-b0hdK_jVKRs1x4PoAn0A6C5S6IiFKmWn0Nm5eBUWu-7jxcqw6TiVjQA==)
# #sentinel
# mode: "sentinel"
# sentinel_hosts: [("redis", 6379)]
# sentinel_master: "mymaster"
# sentinel_password: encrypt(gAAAAABlp4b4c59FeVGF_OQRVf6NOUIGdxq8246EBD-b0hdK_jVKRs1x4PoAn0A6C5S6IiFKmWn0Nm5eBUWu-7jxcqw6TiVjQA==)
# db: 1
# celery的broken地址
celery_redis_url: "redis://redis:6379/2"
celery_task:
# 对celery熟悉的用户可以自定义配置任务的路由,启动不同类型的worker处理不同类型的异步任务。注意工作流的执行只能在一个进程内!!!
task_routers:
bisheng.worker.knowledge.*: # 知识库文件处理相关任务
queue: knowledge_celery
bisheng.worker.workflow.*: # 工作流相关任务
queue: workflow_celery
# 知识库的milvus和es配置 支持使用 !env ${PATH} 填写环境变量的值, 若环境变量不存在则会报错
vector_stores:
milvus:
connection_args: !env ${BS_MILVUS_CONNECTION_ARGS}
is_partition: !env ${BS_MILVUS_IS_PARTITION}
partition_suffix: !env ${BS_MILVUS_PARTITION_SUFFIX}
elasticsearch:
url: !env ${BS_ELASTICSEARCH_URL}
ssl_verify: !env ${BS_ELASTICSEARCH_SSL_VERIFY}
# 对象存储, 目前只支持minio
object_storage:
type: minio
minio:
schema: !env ${BS_MINIO_SCHEMA}
cert_check: !env ${BS_MINIO_CERT_CHECK}
endpoint: !env ${BS_MINIO_ENDPOINT}
sharepoint: !env ${BS_MINIO_SHAREPOINT}
access_key: !env ${BS_MINIO_ACCESS_KEY}
secret_key: !env ${BS_MINIO_SECRET_KEY}
public_bucket: 'bisheng' # 公共bucket,存储平台上一些需要持久化的文件。会设置为可公开访问
tmp_bucket: 'tmp-dir' # 临时bucket,会对传到此bucket内的文件设置有效期
environment:
env: dev
uns_support: ['png','jpg','jpeg','bmp','doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'txt', 'md', 'html', 'pdf', 'csv', 'tiff']
# 可根据loguru的文档配置不同 handlers
logger_conf:
# 默认输出到sys.stdout的日志级别, 大于等于此级别都会输出
level: DEBUG
# 默认输出格式
format: '<level>[{time:YYYY-MM-DD HH:mm:ss.SSSSSS}] [{level.name} process-{process.id}-{thread.id} {name}:{line}]</level> - <level>trace={extra[trace_id]} {message}</level>'
# 参考loguru.add()中的参数可以配置多个handler
handlers:
# 文件路径,支持插入一些系统环境变量,若环境变量不存在则置空。例如 HOSTNAME: 主机名。后端会处理环境变量的替换
- sink: "/app/data/bisheng.log"
# 日志级别
level: INFO
# 日志格式化函数,extra内支持trace_id
format: '<level>[{time:YYYY-MM-DD HH:mm:ss.SSSSSS}] [{level.name} process-{process.id}-{thread.id} {name}:{line}]</level> - <level>trace={extra[trace_id]} {message}</level>'
# 每天的几点进行切割
rotation: "00:00"
retention: "3 Days"
enqueue: ture
- sink: "/app/data/statistic.log"
level: INFO
# 和原生不一样,后端会将配置使用eval()执行转为函数用来过滤特定日志级别。推荐lambda
filter: "lambda record: record['level'].name == 'INFO' and record['message'].startswith('k=s')"
format: "[{time:YYYY-MM-DD HH:mm:ss.SSSSSS}]|{level}|BISHENG|{extra[trace_id]}||{process.id}|{thread.id}|||#EX_ERR:POS={name},line {line},ERR=500,EMSG={message}"
rotation: "00:00"
retention: "3 Days"
enqueue: ture
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
set -xe
export PYTHONPATH="./"
start_mode=${1:-api}
start_knowledge(){
# 知识库解析的celery worker
celery -A bisheng.worker.main worker -l info -c 20 -P threads -Q knowledge_celery -n knowledge@%h
}
start_workflow(){
# 工作流相关的celery worker
celery -A bisheng.worker.main worker -l info -c 100 -P threads -Q workflow_celery -n workflow@%h
}
start_beat(){
# 定时任务调度
celery -A bisheng.worker.main beat -l info
}
start_linsight(){
# 灵思后台任务worker
python bisheng/linsight/worker.py --worker_num 4 --max_concurrency 5
}
start_default(){
# 默认其他任务的执行worker,目前是定时统计埋点数据
celery -A bisheng.worker.main worker -l info -c 100 -P threads -Q celery -n celery@%h
}
if [ "$start_mode" = "api" ]; then
echo "Starting API server..."
uvicorn bisheng.main:app --host 0.0.0.0 --port 7860 --no-access-log --workers 8
elif [ "$start_mode" = "knowledge" ]; then
echo "Starting Knowledge Celery worker..."
start_knowledge
elif [ "$start_mode" = "workflow" ]; then
echo "Starting Workflow Celery worker..."
start_workflow
elif [ "$start_mode" = "beat" ]; then
echo "Starting Celery beat..."
start_beat
elif [ "$start_mode" = "default" ]; then
echo "Starting default celery worker..."
start_default
elif [ "$start_mode" = "linsight" ]; then
echo "Starting LinSight worker..."
start_linsight
elif [ "$start_mode" = "worker" ]; then
echo "Starting All worker..."
# 处理知识库相关任务的worker
start_knowledge &
# 处理工作流相关任务的worker
start_workflow &
# 处理linsight相关任务的worker
start_linsight &
# 默认其他任务的执行worker,目前是定时统计埋点数据
start_default &
start_beat
echo "All workers started successfully."
else
echo "Invalid start mode. Use api、worker、knowledge、workflow、beat、default、linsight."
exit 1
fi
+232
View File
@@ -0,0 +1,232 @@
#!/usr/bin/env bash
# =============================================================
# BiSheng 运维管理脚本
# 用法: ./bisheng.sh <命令> [参数]
# =============================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yml"
COMPOSE_CMD="docker compose -f ${COMPOSE_FILE}"
# 容器名常量(与 docker-compose.yml 对应)
BACKEND_CONTAINER="bisheng-backend"
WORKER_CONTAINER="bisheng-backend-worker"
# 所有可管理的 compose service 名称
ALL_SERVICES=(backend backend_worker frontend mysql redis elasticsearch minio milvus etcd)
# ─── 颜色输出 ────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
info() { echo -e "${GREEN}[INFO]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
header() { echo -e "${CYAN}${BOLD}$*${RESET}"; }
# ─── 帮助 ────────────────────────────────────────────────────
usage() {
header "═══════════════════════════════════════════════"
header " BiSheng 运维管理脚本"
header "═══════════════════════════════════════════════"
echo ""
echo -e "${BOLD}查看日志:${RESET}"
echo " $0 logs backend 实时跟踪 backend 日志(默认最近 200 行)"
echo " $0 logs worker 实时跟踪 backend_worker 日志(默认最近 200 行)"
echo " $0 logs backend -n 0 实时跟踪 backend 所有历史日志"
echo " $0 logs worker -n 500 查看 worker 最近 500 行日志"
echo ""
echo -e "${BOLD}镜像版本管理:${RESET}"
echo " $0 version 查看当前配置的镜像版本"
echo " $0 version v3.0.0 修改 backend、worker、frontend 的版本为 v3.0.0"
echo ""
echo -e "${BOLD}进入容器 Shell${RESET}"
echo " $0 exec backend 进入 backend 容器"
echo " $0 exec worker 进入 backend_worker 容器"
echo ""
echo -e "${BOLD}更新镜像并重启:${RESET}"
echo " $0 update 拉取最新镜像并重启 backend + worker"
echo " $0 update backend 只更新并重启 backend"
echo " $0 update worker 只更新并重启 worker"
echo ""
echo -e "${BOLD}重启容器:${RESET}"
echo " $0 restart 重启 backend + worker"
echo " $0 restart backend 重启 backend"
echo " $0 restart worker 重启 worker"
echo " $0 restart frontend 重启 frontend"
echo " $0 restart <service...> 重启任意多个 service"
echo ""
echo -e "${BOLD}可用 service 名称:${RESET}"
echo " ${ALL_SERVICES[*]}"
echo ""
}
# ─── service 别名解析 ─────────────────────────────────────────
resolve_service() {
case "$1" in
backend) echo "backend" ;;
worker|backend_worker) echo "backend_worker" ;;
frontend) echo "frontend" ;;
mysql) echo "mysql" ;;
redis) echo "redis" ;;
es|elasticsearch) echo "elasticsearch" ;;
minio) echo "minio" ;;
milvus) echo "milvus" ;;
etcd) echo "etcd" ;;
*) echo "$1" ;; # 原样传入,让 docker compose 自行报错
esac
}
# ─── 查看日志 ─────────────────────────────────────────────────
cmd_logs() {
local target="${1:-}"
shift || true
local lines=200
# 解析可选 -n <行数>
while [[ $# -gt 0 ]]; do
case "$1" in
-n)
lines="${2:-200}"
shift 2
;;
*)
shift
;;
esac
done
local service
case "$target" in
backend) service="backend" ;;
worker|backend_worker) service="backend_worker" ;;
*)
error "未知目标 '${target}',请使用 backend 或 worker"
exit 1
;;
esac
if [ "$lines" -eq 0 ]; then
info "实时跟踪 ${service} 所有历史日志(Ctrl+C 退出)..."
${COMPOSE_CMD} logs -f "${service}"
else
info "实时跟踪 ${service} 最近 ${lines} 行日志(Ctrl+C 退出)..."
${COMPOSE_CMD} logs -f --tail="${lines}" "${service}"
fi
}
# ─── 修改版本号 ───────────────────────────────────────────────
cmd_version() {
local new_version="${1:-}"
if [[ -z "$new_version" ]]; then
info "当前 docker-compose.yml 配置的版本:"
grep -E "image:.*dataelement/bisheng-(backend|frontend):" "$COMPOSE_FILE" | awk '{$1=$1};1'
return 0
fi
info "正在将 backend, backend_worker, frontend 版本修改为: ${new_version}"
# 兼容 macOS 和 Linux 的 sed -i 用法
if sed --version 2>/dev/null | grep -q GNU; then
sed -i -E "s|(image: dataelement/bisheng-backend):.*|\1:${new_version}|g" "$COMPOSE_FILE"
sed -i -E "s|(image: dataelement/bisheng-frontend):.*|\1:${new_version}|g" "$COMPOSE_FILE"
else
# macOS/BSD sed
sed -i '' -E "s|(image: dataelement/bisheng-backend):.*|\1:${new_version}|g" "$COMPOSE_FILE"
sed -i '' -E "s|(image: dataelement/bisheng-frontend):.*|\1:${new_version}|g" "$COMPOSE_FILE"
fi
info "✅ 版本修改完成:"
grep -E "image:.*dataelement/bisheng-(backend|frontend):" "$COMPOSE_FILE" | awk '{$1=$1};1'
warn "注意:只是修改了配置文件,若要生效请执行 '$0 update'"
}
# ─── 进入容器 ─────────────────────────────────────────────────
cmd_exec() {
local target="${1:-}"
local container
case "$target" in
backend) container="${BACKEND_CONTAINER}" ;;
worker|backend_worker) container="${WORKER_CONTAINER}" ;;
*)
error "未知目标 '${target}',请使用 backend 或 worker"
exit 1
;;
esac
info "进入容器 ${container} ..."
docker exec -it "${container}" /bin/bash 2>/dev/null \
|| docker exec -it "${container}" /bin/sh
}
# ─── 更新镜像并重启 ───────────────────────────────────────────
cmd_update() {
local targets=()
if [[ $# -eq 0 ]]; then
targets=("backend" "backend_worker" "frontend")
else
for t in "$@"; do
targets+=("$(resolve_service "$t")")
done
fi
info "拉取最新镜像:${targets[*]}"
${COMPOSE_CMD} pull "${targets[@]}"
info "重启服务(不重建依赖):${targets[*]}"
${COMPOSE_CMD} up -d --no-deps "${targets[@]}"
info "✅ 更新完成"
${COMPOSE_CMD} ps "${targets[@]}"
}
# ─── 重启容器 ─────────────────────────────────────────────────
cmd_restart() {
local targets=()
if [[ $# -eq 0 ]]; then
targets=("backend" "backend_worker" "frontend")
else
for t in "$@"; do
targets+=("$(resolve_service "$t")")
done
fi
info "重启服务:${targets[*]}"
${COMPOSE_CMD} restart "${targets[@]}"
info "✅ 重启完成"
${COMPOSE_CMD} ps "${targets[@]}"
}
# ─── 入口 ────────────────────────────────────────────────────
main() {
if [[ $# -eq 0 ]]; then
usage
exit 0
fi
local cmd="$1"; shift
case "$cmd" in
logs) cmd_logs "$@" ;;
version) cmd_version "$@" ;;
exec) cmd_exec "$@" ;;
update) cmd_update "$@" ;;
restart) cmd_restart "$@" ;;
help|-h|--help) usage ;;
*)
error "未知命令: ${cmd}"
usage
exit 1
;;
esac
}
main "$@"
+30
View File
@@ -0,0 +1,30 @@
services:
ft_server:
container_name: bisheng-ft-server
image: dataelement/bisheng-ft:v0.5.0
shm_size: "4g"
ports:
- "8000:8000"
environment:
TZ: Asia/Shanghai
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng-ft/config.yaml:/opt/bisheng-ft/sft_server/config.yaml # 服务启动所需的配置文件地址,默认不用改
- ${DOCKER_VOLUME_DIRECTORY:-.}/data/llm:/opt/bisheng-ft/models/model_repository # 这个是存放基座模型的目录,挂载到本机目录
- ${DOCKER_VOLUME_DIRECTORY:-.}/data/finetune_output:/opt/bisheng-ft/finetune_output # 这个是存放微调过程的中间日志和微调训练后模型的目录,挂载到本机目录,不能与存放基座模型的目录相同
security_opt:
- seccomp:unconfined
command: bash start-sft-server.sh # 启动服务
restart: on-failure
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
start_period: 30s
interval: 90s
timeout: 30s
retries: 3
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
+14
View File
@@ -0,0 +1,14 @@
services:
office:
container_name: bisheng-office
image: onlyoffice/documentserver:7.1.1
ports:
- "8701:80"
environment:
TZ: Asia/Shanghai
JWT_ENABLED: "false"
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/office/bisheng:/var/www/onlyoffice/documentserver/sdkjs-plugins/bisheng
command: bash -c "supervisorctl restart all"
restart: on-failure
+21
View File
@@ -0,0 +1,21 @@
services:
bisheng-unstructured:
container_name: bisheng-unstructured
image: dataelement/bisheng-unstructured:v0.0.3.14
ports:
- "10001:10001"
environment:
# 填写ocr_sdk或rt服务的根地址
# server_address: bisheng-rt:9001
# 这里填 ocr_sdk 或 rt
# server_type: ocr_sdk
TZ: Asia/Shanghai
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng-uns/config.yaml:/opt/bisheng-unstructured/bisheng_unstructured/config/config.yaml
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:10001/health"]
interval: 30s
timeout: 20s
retries: 3
restart: on-failure
+200
View File
@@ -0,0 +1,200 @@
services:
mysql:
container_name: bisheng-mysql
image: mysql:8.0
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: "1234" # 数据库密码,如果修改需要同步修改bisheng/congfig/config.yaml配置database_url的mysql连接密码
MYSQL_DATABASE: bisheng
TZ: Asia/Shanghai
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/mysql/conf/my.cnf:/etc/mysql/my.cnf
- ${DOCKER_VOLUME_DIRECTORY:-.}/mysql/data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "exit | mysql -u root -p$$MYSQL_ROOT_PASSWORD"]
start_period: 30s
interval: 20s
timeout: 10s
retries: 4
restart: on-failure
redis:
container_name: bisheng-redis
image: redis:7.0.4
ports:
- "6379:6379"
environment:
TZ: Asia/Shanghai
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/data/redis:/data
- ${DOCKER_VOLUME_DIRECTORY:-.}/redis/redis.conf:/etc/redis.conf
command: redis-server /etc/redis.conf
healthcheck:
test: ["CMD-SHELL", 'redis-cli ping|grep -e "PONG\|NOAUTH"']
interval: 10s
timeout: 5s
retries: 3
restart: on-failure
backend:
container_name: bisheng-backend
image: dataelement/bisheng-backend:v2.4.0
ports:
- "7860:7860"
environment:
TZ: Asia/Shanghai
BS_MILVUS_CONNECTION_ARGS: '{"host":"milvus","port":"19530","user":"","password":"","secure":false}'
BS_MILVUS_IS_PARTITION: 'true'
BS_MILVUS_PARTITION_SUFFIX: '1'
BS_ELASTICSEARCH_URL: 'http://elasticsearch:9200'
BS_ELASTICSEARCH_SSL_VERIFY: '{}' # 可根据自己部署的密码进行配置 '{"basic_auth": ("elastic", "elastic")}'
BS_MINIO_SCHEMA: 'false'
BS_MINIO_CERT_CHECK: 'false'
BS_MINIO_ENDPOINT: 'minio:9000'
BS_MINIO_SHAREPOINT: 'minio:9000'
BS_MINIO_ACCESS_KEY: 'minioadmin'
BS_MINIO_SECRET_KEY: 'minioadmin'
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng/config/config.yaml:/app/bisheng/config.yaml
- ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng/entrypoint.sh:/app/entrypoint.sh
- ${DOCKER_VOLUME_DIRECTORY:-.}/data/bisheng:/app/data
security_opt:
- seccomp:unconfined
command: sh entrypoint.sh api # 启动api服务
restart: on-failure
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:7860/health"]
start_period: 30s
interval: 90s
timeout: 30s
retries: 3
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
backend_worker:
container_name: bisheng-backend-worker
image: dataelement/bisheng-backend:v2.4.0
environment:
TZ: Asia/Shanghai
BS_MILVUS_CONNECTION_ARGS: '{"host":"milvus","port":"19530","user":"","password":"","secure":false}'
BS_MILVUS_IS_PARTITION: 'true'
BS_MILVUS_PARTITION_SUFFIX: '1'
BS_ELASTICSEARCH_URL: 'http://elasticsearch:9200'
BS_ELASTICSEARCH_SSL_VERIFY: '{}' # 可根据自己部署的密码进行配置 '{"basic_auth": ("elastic", "elastic")}'
BS_MINIO_SCHEMA: 'false'
BS_MINIO_CERT_CHECK: 'false'
BS_MINIO_ENDPOINT: 'minio:9000'
BS_MINIO_SHAREPOINT: 'minio:9000'
BS_MINIO_ACCESS_KEY: 'minioadmin'
BS_MINIO_SECRET_KEY: 'minioadmin'
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng/config/config.yaml:/app/bisheng/config.yaml
- ${DOCKER_VOLUME_DIRECTORY:-.}/bisheng/entrypoint.sh:/app/entrypoint.sh
- ${DOCKER_VOLUME_DIRECTORY:-.}/data/bisheng:/app/data
security_opt:
- seccomp:unconfined
command: sh entrypoint.sh worker # 启动celery的异步worker服务,用来处理一些耗时的任务
restart: on-failure
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
frontend:
container_name: bisheng-frontend
image: dataelement/bisheng-frontend:v2.4.0
ports:
- "3001:3001"
environment:
TZ: Asia/Shanghai
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/nginx/nginx.conf:/etc/nginx/nginx.conf
- ${DOCKER_VOLUME_DIRECTORY:-.}/nginx/conf.d:/etc/nginx/conf.d
restart: on-failure
depends_on:
- backend
elasticsearch:
container_name: bisheng-es
image: docker.io/bitnamilegacy/elasticsearch:8.12.0
user: root
ports:
- "9200:9200"
- "9300:9300"
environment:
TZ: Asia/Shanghai
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/data/es:/bitnami/elasticsearch/data
restart: on-failure
etcd:
container_name: bisheng-milvus-etcd
image: quay.io/coreos/etcd:v3.5.5
environment:
ETCD_AUTO_COMPACTION_MODE: revision
ETCD_AUTO_COMPACTION_RETENTION: "1000"
ETCD_QUOTA_BACKEND_BYTES: "4294967296"
ETCD_SNAPSHOT_COUNT: "50000"
TZ: Asia/Shanghai
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/data/milvus-etcd:/etcd
command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
restart: on-failure
healthcheck:
test: ["CMD", "etcdctl", "endpoint", "health"]
interval: 30s
timeout: 20s
retries: 3
minio:
container_name: bisheng-milvus-minio
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
ports:
- "9100:9000"
- "9101:9001"
volumes:
- /etc/localtime:/etc/localtime:ro
- ${DOCKER_VOLUME_DIRECTORY:-.}/data/milvus-minio:/minio_data
command: minio server /minio_data --console-address ":9001"
restart: on-failure
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
milvus:
container_name: bisheng-milvus-standalone
image: milvusdb/milvus:v2.5.10
command: ["milvus", "run", "standalone"]
security_opt:
- seccomp:unconfined
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
volumes:
- /etc/localtime:/etc/localtime:ro
- ${DOCKER_VOLUME_DIRECTORY:-.}/data/milvus:/var/lib/milvus
restart: on-failure
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
start_period: 90s
interval: 30s
timeout: 20s
retries: 3
ports:
- "19530:19530"
- "9091:9091"
depends_on:
- etcd
- minio
+12
View File
@@ -0,0 +1,12 @@
[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
[mysqld]
init_connect='SET collation_connection = utf8mb4_unicode_ci, NAMES utf8mb4'
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
# skip-character-set-client-handshake
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
+72
View File
@@ -0,0 +1,72 @@
# 在http区域内一定要添加下面配置, 支持websocket
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream backend_server {
server backend:7860; # backend api
}
upstream minio_server {
server minio:9000;
}
server {
gzip on;
gzip_comp_level 2;
gzip_min_length 1000;
gzip_types text/xml text/css;
gzip_http_version 1.1;
gzip_vary on;
gzip_disable "MSIE [4-6] \.";
listen 3001;
location / {
root /usr/share/nginx/html/platform;
index index.html index.htm;
# 禁止浏览器缓存 index.html
location = /index.html {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
add_header Pragma "no-cache" always;
add_header Expires 0 always;
}
try_files $uri $uri/ /index.html;
add_header X-Frame-Options SAMEORIGIN;
}
location /workspace/ {
alias /usr/share/nginx/html/client/;
index index.html index.htm;
# 禁止浏览器缓存 /workspace/index.html
location = /workspace/index.html {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
add_header Pragma "no-cache" always;
add_header Expires 0 always;
}
try_files $uri $uri/ /workspace/index.html;
}
location ~ ^(/workspace)?/api(/|$) {
rewrite ^/workspace(/.*)$ $1 break;
proxy_pass http://backend_server;
proxy_read_timeout 300s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
client_max_body_size 200m;
add_header Access-Control-Allow-Origin $host;
add_header X-Frame-Options SAMEORIGIN;
}
location ~ ^(/workspace)?/bisheng|/tmp-dir {
rewrite ^/workspace(/.*)$ $1 break;
proxy_pass http://minio_server;
}
}
+29
View File
@@ -0,0 +1,29 @@
# 在http区域内一定要添加下面配置, 支持websocket
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
gzip on;
gzip_comp_level 2;
gzip_min_length 1000;
gzip_types text/xml text/css;
gzip_http_version 1.1;
gzip_vary on;
gzip_disable "MSIE [4-6] \.";
listen 8443;
location /api {
proxy_pass http://backend:7860;
proxy_read_timeout 300s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
client_max_body_size 50m;
}
}
+32
View File
@@ -0,0 +1,32 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
+487
View File
@@ -0,0 +1,487 @@
(function (window, undefined) {
let selectText = ''
window.Asc.plugin.init = function (e) {
selectText = e
}
window.Asc.plugin.event_onClick = function () {
selectText = ''
}
window.Asc.plugin.button = function (id) {
}
const EventMap = {
sendToParent (method, data) {
let params = {
type: 'onExternalFrameMessage',
method,
data
}
window.top.postMessage(JSON.stringify(params), location.origin)
},
focusInDocument (data) {
window.Asc.scope.field = {
id: data.id,
fieldFlag: data.fieldFlag,
$index: data.$index || 1
}
window.Asc.plugin.callCommand(function () {
let field = Asc.scope.field || {}
let index = field.$index ? field.$index : 1
let oDoc = Api.GetDocument()
let flag = `{{${field.fieldFlag}}}`
let oRange = oDoc.Search(flag)
let cur = 1
for (let i = 0; i < oRange.length; i++) {
if (oRange[i].GetText() === flag) {
if (cur === index) {
oRange[i].Select()
break
}
cur = cur + 1
}
}
})
},
focusTableInDoc (data) {
window.Asc.scope.marker = data.marker
window.Asc.plugin.callCommand(function () {
let flag = Asc.scope.marker || ''
let oDoc = Api.GetDocument()
let oRange = oDoc.GetBookmarkRange(flag)
oRange.Select()
})
},
addMarker (data) {
let flag = '{{' + data.fieldFlag + '}}'
window.Asc.plugin.executeMethod('PasteText', [flag])
},
addBookMarker (data) {
window.Asc.scope.value = data
window.Asc.plugin.callCommand(function () {
let oDoc = Api.GetDocument()
let range = oDoc.GetRangeBySelect()
let params = {
type: 'onExternalFrameMessage',
method: 'addBookMarker'
}
let marker = Asc.scope.value
let markers = []
if (range) {
let texts = range.GetText()
let pars = range.GetAllParagraphs() || []
let txtList = []
for (let i = 0; i < pars.length; i++) {
let text = pars[i].GetText()
txtList.push(text)
}
let table = pars[0] ? pars[0].GetParentTable() : null
let count = table ? table.GetRowsCount() : 0
for (let i = 0; i < count; i++) {
let row = table.GetRow(i)
let firstCell = row.GetCell(0)
let cellText = firstCell ? firstCell.GetContent().GetElement(0).GetText() : ''
// 序号
let isNumbering = false
if (firstCell.GetContent().GetElement(0).GetNumbering()) {
isNumbering = true
let cellCount = row.GetCellsCount()
for (let j = 1; j < cellCount; j++) {
let cellItem = row.GetCell(j)
if (!cellItem.GetContent().GetElement(0).GetNumbering()) {
cellText = cellItem.GetContent().GetElement(0).GetText()
firstCell = cellItem
break
}
}
}
if (cellText && txtList.includes(cellText)) {
let cRange = firstCell.Search(cellText)[0]
cRange.AddBookmark(marker.key + i)
markers.push(marker.key + i)
}
}
// range.AddBookmark(Asc.scope.value.key)
params.data = Object.assign(marker, {
key: markers.join(','),
texts
})
} else {
params.data = false
}
window.top.postMessage(JSON.stringify(params), location.origin)
})
},
deleteBookMarker (data) {
window.Asc.scope.value = data
window.Asc.plugin.callCommand(function () {
let oDoc = Api.GetDocument()
let markers = Asc.scope.value || []
for (let i = 0; i < markers.length; i++) {
oDoc.DeleteBookmark(markers[i])
}
})
},
// 批量删除循环应用内标签
deleteLoopApp (list) {
window.Asc.scope.value = list
window.Asc.plugin.callCommand(function () {
let list = window.Asc.scope.value || []
let oDoc = Api.GetDocument()
list.forEach(row => {
if (row.loopType === 0) {
oDoc.SearchAndReplace({ searchString: `{{${row.startTag}}}`, replaceString: '' }, `{{${row.startTag}}}`, '')
oDoc.SearchAndReplace({ searchString: `{{${row.endTag}}}`, replaceString: '' }, `{{${row.endTag}}}`, '')
} else if (row.loopType === 1) {
oDoc.DeleteBookmark(row.bookmark)
}
})
})
},
// 更新占位符
replaceMarker (data) {
window.Asc.scope.st = '{{' + data.newValue + '}}'
// 原来的值
if (data.oldValue) {
window.Asc.scope.old = '{{' + data.oldValue + '}}'
} else {
this.addMarker(data)
return
}
window.Asc.plugin.callCommand(function () {
let oDocument = Api.GetDocument()
oDocument.SearchAndReplace({ searchString: Asc.scope.old, replaceString: Asc.scope.st }, Asc.scope.old, Asc.scope.st)
}, false)
},
// 查找并插入占位符
findAndInsertMarker (data) {
window.Asc.scope.st = '{{' + data.fieldName + '}}'
window.Asc.scope.searchStr = data.fieldValue
window.Asc.plugin.callCommand(function () {
let oDocument = Api.GetDocument()
oDocument.SearchAndReplace({ searchString: Asc.scope.searchStr, replaceString: Asc.scope.st }, Asc.scope.searchStr, Asc.scope.st)
}, false)
},
insertPosition (data) {
if (!selectText) {
let postData = {
text: selectText,
...data,
selected: false
}
this.sendToParent('addRange', postData)
return false
}
window.Asc.scope.postData = data
window.Asc.plugin.callCommand(function() {
let postData = Asc.scope.postData || {}
let oDoc = Api.GetDocument()
let oRange = oDoc.GetRangeBySelect()
let selectText = oRange.GetText()
let oAllPar = oRange.GetAllParagraphs()
let oPar = oAllPar[oAllPar.length - 1]
let parText = oPar.GetText()
if (oAllPar.length > 1) {
oRange.AddText(`{{${postData.start}}}`, 'before')
if (selectText.includes(parText)) {
let newRange = oPar.GetRange(0, parText.length - 1)
newRange.AddText(`{{${postData.end}}}`, 'after')
} else {
oRange.AddText(`{{${postData.end}}}`, 'after')
}
} else {
let isEnd = parText.substr(0 - selectText.length) === selectText
isEnd = isEnd || selectText.includes(parText)
console.log('end = ', isEnd)
let start = Math.max(parText.indexOf(selectText), 0)
let end = start + Math.min(parText.length, selectText.length) - 1
let newRange = oPar.GetRange(start, end)
oRange.AddText(`{{${postData.start}}}`, 'before')
newRange.AddText(`{{${postData.end}}}`, 'after')
}
postData.selected = true
postData.text = selectText
let params = {
type: 'onExternalFrameMessage',
method: 'addRange',
data: postData
}
window.top.postMessage(JSON.stringify(params), location.origin)
})
},
deletePosition (data) {
window.Asc.scope.range = data
window.Asc.plugin.callCommand(function () {
let oDocument = Api.GetDocument()
let { start, end } = Asc.scope.range
let markers = [`{{${start}}}`, `{{${end}}}`]
for (let j = 0; j < markers.length; j++) {
oDocument.SearchAndReplace({ searchString: markers[j], replaceString: '' }, markers[j], '')
}
})
},
deletePositionMarker (data) {
window.Asc.scope.data = data
window.Asc.plugin.callCommand(function () {
let oDocument = Api.GetDocument()
let markers = Asc.scope.data || []
for (let j = 0; j < markers.length; j++) {
oDocument.SearchAndReplace({ searchString: markers[j], replaceString: '' }, markers[j], '')
}
})
},
deletePositionArray (data) {
window.Asc.scope.data = data
window.Asc.plugin.callCommand(function () {
let oDocument = Api.GetDocument()
let markers = Asc.scope.data || []
for (let j = 0; j < markers.length; j++) {
oDocument.SearchAndReplace({ searchString: markers[j], replaceString: '' }, markers[j], '')
}
})
},
replaceRangePosition (data) {
window.Asc.scope.list = data
window.Asc.plugin.callCommand(function () {
let list = Asc.scope.list || []
let oDocument = Api.GetDocument()
list.forEach(row => {
oDocument.SearchAndReplace({ searchString: row.str, replaceString: row.newStr }, row.str, row.newStr)
})
})
},
delMarker (data) {
let fields = []
data.forEach(item => {
fields.push({
text: '{{' + item.fieldFlag + '}}',
type: 'field'
})
})
window.Asc.scope.st = fields
window.Asc.plugin.callCommand(function () {
let oDocument = Api.GetDocument()
let markers = Asc.scope.st.slice(0)
for (let j = 0; j < markers.length; j++) {
let marker = markers[j]
if (marker.type === 'field') {
oDocument.SearchAndReplace({ searchString: marker.text, replaceString: '' })
}
}
})
},
delMarkerGroup (data) {
this.delMarker(data.fields)
},
// excel
insertCellName (field) {
window.Asc.scope.field = field
window.Asc.plugin.callCommand(function () {
let fieldItem = Asc.scope.field
let sheetObj = Api.GetActiveSheet()
let sheetName = sheetObj.GetName()
let oRange = Api.GetSelection()
let oCount = oRange.GetCount()
let params = {
type: 'onExternalFrameMessage',
method: 'addCellName'
}
if (oCount !== 1) {
params.data = false
} else {
let oAddr = oRange.GetAddress(true, true, '', false)
let sheetFlag = `${sheetName}!${oAddr}`
// let name = [fieldItem.fieldName, 'DEF', fieldItem.id].join('')
// let nameObj = sheetObj.GetDefName(name)
// console.log('inser cell before = ', name, sheetFlag, nameObj)
// let result = sheetObj.AddDefName(name, sheetFlag)
// console.log('insert cell ', name, sheetFlag, result)
fieldItem.fieldFlag = sheetFlag
fieldItem.$success = oAddr !== ''
params.data = fieldItem
}
window.top.postMessage(JSON.stringify(params), location.origin)
})
},
getFocusedCell () {
window.Asc.plugin.callCommand(function () {
let sheetObj = Api.GetActiveSheet()
let sheetName = sheetObj.GetName()
let oRange = Api.GetSelection()
let params = {
type: 'onExternalFrameMessage',
method: 'getFocusedCell'
}
let oAddr = oRange.GetAddress(true, true, '', false)
let sheetFlag = `${sheetName}!${oAddr}`
params.data = sheetFlag
window.top.postMessage(JSON.stringify(params), location.origin)
})
},
loadFileFlags (data) {
window.Asc.scope.list = data
window.Asc.plugin.callCommand(function () {
let list = Asc.scope.list || []
let oDocument = Api.GetDocument()
let oParCount = oDocument.GetElementsCount()
let dataMap = {}
for (let i = 0; i < oParCount; i++) {
let oPar = oDocument.GetElement(i)
let ctype = oPar.GetClassType()
if (ctype === 'table') {
list.forEach(row => {
let flag = `{{${row.fieldFlag}}}`
let rs = oPar.Search(flag)
for (let i = 0; i < rs.length; i++) {
let oRange = rs[i]
if (oRange && oRange.GetText() === flag) {
if (dataMap[row.id]) {
dataMap[row.id] = dataMap[row.id] + 1
} else {
dataMap[row.id] = 1
}
}
}
})
} else if (ctype === 'paragraph') {
let oParText = oPar.GetText()
list.forEach(row => {
let count = oParText.split(`{{${row.fieldFlag}}}`).length - 1
if (dataMap[row.id]) {
dataMap[row.id] = dataMap[row.id] + count
} else {
dataMap[row.id] = count
}
})
}
}
let params = {
type: 'onExternalFrameMessage',
method: 'loadFieldFlagCount',
data: dataMap
}
window.top.postMessage(JSON.stringify(params), location.origin)
})
},
/**
* data.sheetName: 要聚焦的sheet名称
* data.cellName: 要聚焦的cell名称,如C1, D3等
*/
focusCell (data) {
window.Asc.scope.data = data
window.Asc.plugin.callCommand(function () {
const theData = Asc.scope.data
const theSheet = Api.GetSheet(theData.sheetName || '')
if (theSheet) {
theSheet.SetActive()
const theCell = theSheet.GetRange(theData.cellName || '')
if (theCell) {
theCell.Select()
}
}
})
},
getSelectedText (data) {
window.Asc.scope.data = data
window.Asc.plugin.callCommand(function () {
const theData = Asc.scope.data
const oDoc = Api.GetDocument()
const oRange = oDoc.GetRangeBySelect()
if (oRange) {
const oParas = oRange.GetAllParagraphs()
// 只能选择一个段落,否则认为不成功
if (oParas.length === 1) {
const params = {
type: 'onExternalFrameMessage',
method: 'getSelectedText',
data: {
id: theData.id,
text: oRange.GetText()
}
}
window.top.postMessage(JSON.stringify(params), location.origin)
}
}
})
}
}
function receiveMessage (e) {
let data = e.data ? JSON.parse(e.data) : {}
if (data.type === 'onExternalPluginMessage') {
switch (data.method) {
case 'focus':
EventMap.focusInDocument(data.data)
break
case 'focusTable':
EventMap.focusTableInDoc(data.data)
break
case 'insert':
EventMap.addMarker(data.data)
break
case 'addBookMarker':
EventMap.addBookMarker(data.data)
break
case 'delBookMarker':
EventMap.deleteBookMarker(data.data)
break
case 'delLoopApp':
EventMap.deleteLoopApp(data.data)
break
case 'update':
EventMap.replaceMarker(data.data)
break
case 'findAndInsertMarker':
EventMap.findAndInsertMarker(data.data)
break
case 'addRange':
EventMap.insertPosition(data.data)
break
case 'updateRange':
EventMap.replaceRangePosition(data.data)
break
case 'delRange':
EventMap.deletePosition(data.data)
break
case 'delRangeArray':
EventMap.deletePositionArray(data.data)
break
case 'delQuoteGroup':
EventMap.deletePositionMarker(data.data)
break
case 'remove':
EventMap.delMarker([ data.data ])
break
case 'removeQuestion':
EventMap.delMarkerGroup(data.data)
break
// excel
case 'addCellName':
EventMap.insertCellName(data.data)
break
case 'loadFieldFlagCount':
EventMap.loadFileFlags(data.data)
break
// 聚焦到某个单元格
case 'focusCell':
EventMap.focusCell(data.data)
break
// 获取当前选中的单元格
case 'getFocusedCell':
EventMap.getFocusedCell()
break
// 获取当前选中的文字
case 'getSelectedText':
EventMap.getSelectedText(data.data)
break
}
}
}
window.addEventListener('message', receiveMessage, false)
})(window, undefined)
+15
View File
@@ -0,0 +1,15 @@
(function () {
window.Asc.plugin.init = function (e) {}
window.Asc.plugin.event_onClick = function () {}
window.Asc.plugin.button = function (id) {}
function onMessage(e) {
var data = e.data ? JSON.parse(e.data) : {}
if (data.action === 'insetMarker') {
const flag = '{{' + data.data + '}}'
window.Asc.plugin.executeMethod('PasteText', [flag])
}
}
window.addEventListener('message', onMessage, false)
})()
+35
View File
@@ -0,0 +1,35 @@
{
"name": "文档自动化",
"guid": "asc.{D2A0F3BE-CC8D-4956-BCD9-6CBEA6E8960E}",
"variations": [
{
"description": "插入label-配置",
"url": "index.html",
"icons": [
"icon.png",
"icon.png",
"icon.png",
"icon.png"
],
"EditorsSupport": [
"word",
"cell",
"slide"
],
"isViewer": false,
"isVisual": false,
"isModal": true,
"isInsideMode": false,
"isSystem": false,
"initOnSelectionChanged": true,
"hideClose": true,
"initDataType": "text",
"isDisplayedInViewer": true,
"isUpdateOleOnResize": true,
"events": [
"onClick",
"onTargetPositionChanged"
]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<script src="../pluginBase.js"></script>
<script src="./bisheng.js"></script>
</head>
<body>
</body>
</html>
File diff suppressed because it is too large Load Diff