业务链说明本文档将SFTPGo部署手册与ELK日志采集手册合并为一个项目目录、一个docker-compose.yml，四个服务（sftpgo / elasticsearch / kibana / filebeat）统一编排、统一网络，形成完整业务链：SFTPGo产生日志 → 写入宿主机共享目录 → Filebeat采集 → 写入Elasticsearch → Kibana可视化查看sftpgo与filebeat通过同一个宿主机目录（./sftpgo/logs）实现日志传递，不依赖网络；filebeat与elasticsearch之间通过compose内部网络elastic-net用容器名互相访问，不再依赖外部域名，部署更内聚、迁移更方便。一、环境准备1.安装Docker（已安装可跳过）# 卸载旧版本(按需) yum remove docker docker-client docker-client-latest docker-common \ docker-latest docker-latest-logrotate docker-logrotate docker-engine # 配置阿里云docker-ce源 yum install -y yum-utils yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo # 安装docker-ce yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # 启动并设置开机自启 systemctl start docker && systemctl enable docker二、创建项目目录结构所有配置、数据、日志统一收拢在一个项目目录下，方便整体迁移与备份：mkdir -p /home/sftpgo-elk/{sftpgo/home,sftpgo/data,sftpgo/logs,filebeat/config,filebeat/data,filebeat/logs,elasticsearch/es_data,kibana/kibana_data} cd /home/sftpgo-elk目录规划：/home/sftpgo-elk/ ├── docker-compose.yml # 唯一的compose文件，四个服务统一编排 ├── sftpgo/ │ ├── home/ # 挂载至容器 /var/lib/sftpgo（配置/元数据） │ ├── data/ # 挂载至容器 /srv/sftpgo（文件存储） │ └── logs/ # 挂载至容器 /var/log/sftpgo，同时被filebeat只读挂载采集 ├── filebeat/ │ ├── config/filebeat.yml │ ├── data/ # filebeat注册表(registry)，记录采集进度，需持久化 │ └── logs/ # filebeat自身运行日志 ├── elasticsearch/ │ └── es_data/ └── kibana/ └── kibana_data/赋予数据目录读写权限chown -R 1000:1000 /home/sftpgo-elk/sftpgo/home chown -R 1000:1000 /home/sftpgo-elk/sftpgo/data chown -R 1000:1000 /home/sftpgo-elk/sftpgo/logs chown -R 1000:1000 /home/sftpgo-elk/elasticsearch/es_data chown -R 1000:1000 /home/sftpgo-elk/kibana/kibana_data1000为SFTPGo、Elasticsearch、Kibana官方镜像默认使用的容器内运行用户UID/GID，统一赋权避免EACCES权限报错。三、用cat生成统一docker-compose.yml⚠️ 以下配置以你实际生产环境正在使用的sftpgo配置为准（日志输出、限流轮转参数均保留），仅将挂载路径统一收拢到项目目录下，并把filebeat、elasticsearch、kibana并入同一份compose、同一网络。cat <<'EOF' > docker-compose.yml version: "3.8" services: # ------------------------- 业务服务：SFTPGo ------------------------- sftpgo: image: drakkan/sftpgo container_name: sftpgo user: "1000:1000" working_dir: /var/lib/sftpgo restart: always command: sftpgo serve ports: - "2022:2022" - "8081:8080" volumes: - ./sftpgo/home:/var/lib/sftpgo - ./sftpgo/data:/srv/sftpgo - ./sftpgo/logs:/var/log/sftpgo # 与filebeat共享的日志目录 environment: - SFTPGO_LOG_FILE_PATH=/var/log/sftpgo/sftpgo.log - SFTPGO_LOG_LEVEL=info - SFTPGO_LOG_ROTATE_MAX_SIZE=100 - SFTPGO_LOG_ROTATE_MAX_BACKUPS=30 - SFTPGO_LOG_ROTATE_MAX_AGE=30 # 保留30天 logging: driver: "json-file" options: max-size: "100m" max-file: "30" networks: - elastic-net # ------------------------- 存储与检索：Elasticsearch ------------------------- elasticsearch: image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/elasticsearch:8.19.18 container_name: elasticsearch restart: unless-stopped environment: - discovery.type=single-node - xpack.security.enabled=false # 内网环境关闭认证，简化Filebeat对接 - ES_JAVA_OPTS=-Xms1g -Xmx1g # 根据服务器内存调整，建议≥2g - TZ=Asia/Shanghai ports: - "9200:9200" volumes: - ./elasticsearch/es_data:/usr/share/elasticsearch/data ulimits: memlock: soft: -1 hard: -1 healthcheck: test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"] interval: 30s timeout: 10s retries: 5 networks: - elastic-net # ------------------------- 可视化：Kibana ------------------------- kibana: image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/kibana:8.19.18 container_name: kibana restart: unless-stopped depends_on: elasticsearch: condition: service_healthy environment: - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 - TZ=Asia/Shanghai ports: - "5601:5601" volumes: - ./kibana/kibana_data:/usr/share/kibana/data networks: - elastic-net # ------------------------- 采集：Filebeat ------------------------- filebeat: image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/elastic/filebeat:8.19.18 container_name: filebeat user: root restart: unless-stopped depends_on: elasticsearch: condition: service_healthy volumes: - ./filebeat/config/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro - ./sftpgo/logs:/var/log/sftpgo:ro # 与sftpgo共享同一宿主机目录，只读采集 - ./filebeat/data:/usr/share/filebeat/data - ./filebeat/logs:/var/log/filebeat environment: - TZ=Asia/Shanghai networks: - elastic-net networks: elastic-net: driver: bridge EOF相比原两份手册的关键调整：四个服务合并进同一个elastic-net网络，用容器名http://elasticsearch:9200，脱离宿主机DNS解析，迁移到其他环境无需改配置。sftpgo的日志目录/var/log/sftpgo与filebeat的采集源目录挂载同一个宿主机路径./sftpgo/logs，业务链闭环全部在compose内部完成。kibana/filebeat显式depends_on加condition: service_healthy，保证ES健康后再启动，避免filebeat启动时连接ES失败反复重试。四、用cat生成filebeat.ymlcat <<'EOF' > filebeat/config/filebeat.yml # ======================== Filebeat Configuration ========================= setup.template.name: "sftpgo-logs" setup.template.pattern: "sftpgo-logs-*" setup.ilm.enabled: false # ⭐ 关键：关闭Filebeat自动管理索引模板，防止其自动携带data_stream字段 # 导致索引被强制转换为Data Stream（.ds-前缀，无法按常规方式删除/管理） setup.template.enabled: false # ------------------------------ Inputs ----------------------------------- filebeat.inputs: - type: log enabled: true paths: - /var/log/sftpgo/*.log # 如果 SFTPGo 日志包含多行堆栈或跨行记录，请启用以下配置 # multiline.pattern: '^\d{4}-\d{2}-\d{2}' # multiline.negate: true # multiline.match: after fields: app: sftpgo env: production fields_under_root: true # ------------------------------ Output ------------------------------------ output.elasticsearch: # ⭐ 已并入同一compose网络，直接用容器名访问，无需外部域名 hosts: ["http://elasticsearch:9200/"] # 自定义索引名称（按天滚动） index: "sftpgo-logs-%{+yyyy.MM.dd}" data_stream.enabled: false # 如果 ES 开启了 X-Pack Security，取消下面的注释并填入密码 # username: "elastic" # password: "your_secure_password" # ------------------------------ Performance ------------------------------- queue.mem: events: 512 flush.min_events: 128 flush.timeout: 1s # ------------------------------ Logging ------------------------------------- logging.level: info logging.to_stderr: true EOF五、启动业务链由于filebeat自动创建的索引模板会导致索引被识别为Data Stream（详见附录2），需按以下固定顺序启动，中间插入一步手动建模板：1.先启动Elasticsearch、Kibana（filebeat依赖它们健康检查通过）docker compose up -d elasticsearch kibana2.等ES就绪后，手动创建不含data_stream字段的索引模板(实际测试可以跳过这一部分直接执行步骤3)# 等健康检查通过 until curl -sf http://localhost:9200/_cluster/health > /dev/null; do sleep 2; done curl -X PUT "http://localhost:9200/_index_template/sftpgo-logs" \ -H "Content-Type: application/json" \ -d '{ "index_patterns": ["sftpgo-logs-*"], "priority": 150, "template": { "settings": { "number_of_shards": 1, "number_of_replicas": 0 }, "mappings": { "properties": { "@timestamp": { "type": "date" }, "message": { "type": "text" } } } } }'number_of_replicas设为0是因为单节点ES无法分配副本分片，设1会导致索引长期处于yellow状态。3.启动(实测可以跳过步骤1和2直接执行这一步)docker compose up -d4.验证四个服务是否都正常运行docker compose ps预期四个服务的STATUS均为Up（elasticsearch/kibana会显示healthy）。5.验证日志是否成功写入EScurl "http://localhost:9200/_cat/indices/sftpgo-logs-*?v&s=index"预期返回类似：health status index uuid pri rep docs.count docs.deleted store.size pri.store.size dataset.size green open sftpgo-logs-2026.07.03 Uwlh87QdQ9ebIIEy9789vg 1 0 49 0 141.9kb 141.9kb 141.9kbhealth为green、索引名不带.ds-前缀，代表数据已按预期以普通索引形式写入。以后每次启动整个业务链（比如服务器重启后），直接：cd /home/sftpgo-elk docker compose up -d因为模板已经手动建好、restart: unless-stopped/always已配置，四个服务会按依赖关系自动拉起，无需再重复第2步。六、Kibana配置查看sftpgo日志1.创建Data View登入 http://<host_ip>:5601，左上角汉堡菜单 → Management → Stack Management → Kibana → Data Views，点击右上角Create data view，填写：Name：sftpgo-logsIndex pattern：sftpgo-logs-*Timestamp field：@timestamp保存后即完成配置，sftpgo-logs-*可自动匹配后续每天新生成的索引（如sftpgo-logs-2026.07.04）。2.进入Discover查看日志左上角汉堡菜单 → Analytics → Discover，顶部Data View下拉框选择sftpgo-logs，右上角时间范围选择器调整为Last 24 hours或Today，中间区域即可看到实时采集的日志条目。3.常用查询与筛选（KQL语法，输入于顶部搜索框）app: "sftpgo" and log.level: "error"message: *upload*点击单条日志左侧箭头可展开查看完整字段，包括filebeat.yml中自定义添加的app: sftpgo、env: production字段。4.保存查询与可视化（可选）筛选条件配置完成后，点击顶部Save可保存为常用查询；点击搜索框右侧Visualize可基于当前筛选条件快速生成柱状图，用于监控错误日志频率等场景。七、日常运维1.查看四个服务运行状态与日志docker compose ps docker compose logs -f sftpgo docker compose logs -f filebeat docker compose logs -f elasticsearch docker compose logs -f kibana2.索引磁盘占用查看curl "http://localhost:9200/_cat/indices/sftpgo-logs-*?v&s=index"3.历史索引清理（按需，测试/内网环境无ILM时需手动清理）curl -X DELETE "http://localhost:9200/sftpgo-logs-2026.07.01"4.整体业务链停止/重启cd /home/sftpgo-elk docker compose stop # 停止所有服务 docker compose start # 启动所有服务 docker compose restart # 重启所有服务5.SFTPGo容器日志轮转sftpgo容器已在compose中配置logging.driver: json-file，max-size: 100m、max-file: 30，Docker会自动轮转与清理容器标准输出日志，无需再额外配置系统级logrotate。应用层日志（sftpgo.log，即filebeat采集的对象）由SFTPGO_LOG_ROTATE_*系列环境变量控制轮转，二者互不冲突。八、附录1.Kibana容器启动报错Unable to write to UUID file ... EACCES原因：Kibana容器内部以非root用户（UID 1000）运行，宿主机挂载目录kibana_data属主/权限不匹配导致无法写入。处理：docker compose stop kibana chown -R 1000:1000 ./kibana/kibana_data docker compose start kibana若属主已正确但仍报错，大概率是SELinux拦截，检查：getenforce若为Enforcing，在docker-compose.yml对应挂载卷末尾加:Z标签：volumes: - ./kibana/kibana_data:/usr/share/kibana/data:Z修改后重建容器：docker compose up -d --force-recreate kibana2.索引出现.ds-前缀（被误判为Data Stream）问题现象：_cat/indices查询结果索引名类似.ds-sftpgo-logs-2026.07.03-2026.07.03-000001，而非期望的sftpgo-logs-2026.07.03。原因：Filebeat自动创建的composable template默认携带data_stream字段，导致匹配的索引被ES按Data Stream处理，不受filebeat.yml中data_stream.enabled: false配置项影响。处理（需先停止Filebeat）：docker compose stop filebeat # 1.删除已生成的数据流（会删除该数据流下已写入的日志数据） curl -X DELETE "http://localhost:9200/_data_stream/sftpgo-logs-2026.07.03" # 2.删除被data_stream字段污染的模板 curl -X DELETE "http://localhost:9200/_index_template/sftpgo-logs" # 3.按 五、2 手动重建不含data_stream字段的模板 # 4.确认 filebeat.yml 中已有 setup.template.enabled: false docker compose start filebeat3.单节点ES索引健康状态长期为yellow原因：默认number_of_replicas为1，单节点环境副本分片无法分配到其他节点。处理：在索引模板中显式设置number_of_replicas: 0（见五、2模板配置），或对已存在索引直接调整：curl -X PUT "http://localhost:9200/sftpgo-logs-*/_settings" \ -H "Content-Type: application/json" \ -d '{"index": {"number_of_replicas": 0}}'4.整体迁移到新服务器因所有数据（sftpgo数据/配置、ES索引、Kibana配置、filebeat采集进度）均已收拢在/home/sftpgo-elk项目目录下，迁移时只需：# 旧服务器 cd /home/sftpgo-elk docker compose down tar czf sftpgo-elk-backup.tar.gz /home/sftpgo-elk # 新服务器 tar xzf sftpgo-elk-backup.tar.gz -C /home/ cd /home/sftpgo-elk docker compose up -d
准备工作操作系统CentOS Stream 9 (x86)cat /etc/os-release NAME="CentOS Stream" VERSION="9" ID="centos" ID_LIKE="rhel fedora" VERSION_ID="9" PLATFORM_ID="platform:el9" PRETTY_NAME="CentOS Stream 9" ANSI_COLOR="0;31" LOGO="fedora-logo-icon" CPE_NAME="cpe:/o:centos:centos:9" HOME_URL="https://centos.org/" BUG_REPORT_URL="https://issues.redhat.com/" REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux 9" REDHAT_SUPPORT_PRODUCT_VERSION="CentOS Stream"已安装docker composeDocker version 29.5.3, build d1c06ef Docker Compose version v5.1.4一、部署difygit pull https://github.com/langgenius/dify.git创建.env文件cd dify/docker && cp .env.example .envdify的llm节点有内部流程60s超时限制，导致没法下载智能体文件，在.env中修改配置sed -i 's/TEXT_GENERATION_TIMEOUT_MS=60000/TEXT_GENERATION_TIMEOUT_MS=300000/' .env部署容器集群docker compose up -d测试访问webcurl http://localhost二、公文api开始构建部署FastAPI程序docker compose up -d五、测试chat用以下指令测试业务curl -s -o test.docx -w "HTTP状态码: %{http_code}\n文件大小: %{size_download} bytes\n" -X POST http://localhost:8765/generate_from_llm_text -H "Content-Type: application/json" -d '{"doc_redheader":"测试机关文件","doc_number":"测发〔2026〕1号","title_lines":["关于测试公文格式化服务的通知"],"recipient":"各测试单位","body":[{"type":"intro","text":"为验证公文格式化API服务是否正常，现通知如下："},{"type":"heading1","text":"一、测试内容"},{"type":"para","text":"本次测试涵盖红头、标题、正文、附件及版记等全部要素。"},{"type":"heading2","text":"（一）格式校验"},{"type":"para","text":"请检查生成文档的字体、字号、行距及页边距是否符合GB/T 9704-2012标准。"}],"attachments":["测试附件清单"],"signing_org_lines":["测试机关"],"date":"2026年7月2日","cc_orgs":"抄送测试部门","print_org":"测试机关办公室","print_date":"2026年7月2日"}' && echo "✅ 文件已保存为 test.docx，请用Word或WPS打开验证排版" || echo "❌ 请求失败"
准备工作操作系统CentOS Stream 9 (x86)cat /etc/os-release NAME="CentOS Stream" VERSION="9" ID="centos" ID_LIKE="rhel fedora" VERSION_ID="9" PLATFORM_ID="platform:el9" PRETTY_NAME="CentOS Stream 9" ANSI_COLOR="0;31" LOGO="fedora-logo-icon" CPE_NAME="cpe:/o:centos:centos:9" HOME_URL="https://centos.org/" BUG_REPORT_URL="https://issues.redhat.com/" REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux 9" REDHAT_SUPPORT_PRODUCT_VERSION="CentOS Stream"已安装docker composeDocker version 29.5.3, build d1c06ef Docker Compose version v5.1.4一、目录结构gongwen/├── docker-compose.yml├── Dockerfile└──main.py二、创建配置文件创建目录mkdir -p gongwen/ && cd gongwen部署文件cat > docker-compose.yml << 'EOF' services: gongwen-api: build: . container_name: gongwen-api restart: unless-stopped ports: - "8765:8765" EOF镜像构件文件cat > Dockerfile << 'EOF' FROM python:3.11-slim # 安装 Node.js（用于 docx.js 生成 Word 文件） RUN apt-get update && apt-get install -y curl && \ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ apt-get install -y nodejs && \ apt-get clean && rm -rf /var/lib/apt/lists/* WORKDIR /app # 安装 docx npm 包到 /app/node_modules（本地安装，避免临时目录找不到模块） RUN npm install docx # 安装 Python 依赖 RUN pip install --no-cache-dir fastapi uvicorn pydantic python-docx COPY main.py . EXPOSE 8765 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8765"] EOF部署主程序源码cat > main.py<< 'EOF' """ 公文格式化 API 服务（最终版） 接口结构与 v11 完全一致，Word 格式按 GB/T 9704-2012 重写 XML 元素严格按 OOXML CT_PPr schema 顺序插入： pBdr(8) → tabs(10) → spacing(21) → ind(22) → jc(26) """ from fastapi import FastAPI, HTTPException, Body from fastapi.responses import StreamingResponse from pydantic import BaseModel from typing import Optional, List, Literal, Any import io, json from urllib.parse import quote from lxml import etree from docx import Document from docx.shared import Pt, Cm, RGBColor from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING from docx.oxml.ns import qn from docx.oxml import OxmlElement app = FastAPI(title="公文格式化服务") # ── 数据模型（与 v11 完全一致）────────────────────────────────────────────────── class BodyItem(BaseModel): type: Literal["intro", "heading1", "heading2", "heading3", "para"] text: str index: Optional[int] = None class GongwenRequest(BaseModel): doc_redheader: Optional[str] = "" doc_number: Optional[str] = "" title_lines: List[str] = [] recipient: Optional[str] = "" body: List[BodyItem] = [] attachments: List[str] = [] signing_org_lines: List[str] = [] date: Optional[str] = "" cc_orgs: Optional[str] = "" print_org: Optional[str] = "" print_date: Optional[str] = "" urgency: Optional[str] = "" secrecy: Optional[str] = "" # ── pPr 子元素 schema 顺序（按 OOXML CT_PPr）─────────────────────────────────── _PPR_ORDER = [ "w:pStyle","w:keepNext","w:keepLines","w:pageBreakBefore","w:framePr", "w:widowControl","w:numPr","w:suppressLineNumbers", "w:pBdr", "w:shd", "w:tabs", "w:suppressAutoHyphens","w:kinsoku","w:wordWrap","w:overflowPunct", "w:topLinePunct","w:autoSpaceDE","w:autoSpaceDN","w:bidi", "w:adjustRightInd","w:snapToGrid", "w:spacing", "w:ind", "w:contextualSpacing","w:mirrorIndents","w:suppressOverlap", "w:jc", "w:textDirection","w:textAlignment","w:textboxTightWrap", "w:outlineLvl","w:divId","w:cnfStyle", "w:rPr","w:sectPr","w:pPrChange", ] def _ppr_insert(pPr, el): tag_local = el.tag.split("}")[-1] if "}" in el.tag else el.tag tag_full = f"w:{tag_local}" try: target_idx = _PPR_ORDER.index(tag_full) except ValueError: pPr.append(el) return children = list(pPr) insert_pos = len(children) for i, child in enumerate(children): child_local = child.tag.split("}")[-1] if "}" in child.tag else child.tag child_full = f"w:{child_local}" try: if _PPR_ORDER.index(child_full) > target_idx: insert_pos = i break except ValueError: continue pPr.insert(insert_pos, el) # ── 格式工具 ────────────────────────────────────────────────────────────────── def _set_font(run, name: str, size_pt: float, bold=False, color_rgb=None): run.bold = bold run.font.size = Pt(size_pt) run.font.name = name rPr = run._r.get_or_add_rPr() rFonts = rPr.get_or_add_rFonts() rFonts.set(qn("w:eastAsia"), name) if color_rgb: run.font.color.rgb = RGBColor(*color_rgb) def _apply_para_fmt(para, line_pt=28, before_pt=0, after_pt=0, align=WD_ALIGN_PARAGRAPH.LEFT, first_line_chars=0, pBdr=None, tabs=None): pPr = para._p.get_or_add_pPr() if pBdr is not None: _ppr_insert(pPr, pBdr) if tabs is not None: _ppr_insert(pPr, tabs) spacing = OxmlElement("w:spacing") spacing.set(qn("w:before"), str(int(before_pt * 20))) spacing.set(qn("w:after"), str(int(after_pt * 20))) spacing.set(qn("w:lineRule"), "exact") spacing.set(qn("w:line"), str(int(line_pt * 20))) _ppr_insert(pPr, spacing) if first_line_chars: ind = OxmlElement("w:ind") ind.set(qn("w:firstLine"), str(int(first_line_chars * 16 * 20))) _ppr_insert(pPr, ind) jc_map = { WD_ALIGN_PARAGRAPH.CENTER: "center", WD_ALIGN_PARAGRAPH.RIGHT: "right", WD_ALIGN_PARAGRAPH.JUSTIFY: "both", WD_ALIGN_PARAGRAPH.LEFT: "left", } jc_val = jc_map.get(align, "left") if jc_val != "left": jc = OxmlElement("w:jc") jc.set(qn("w:val"), jc_val) _ppr_insert(pPr, jc) def _make_pBdr_bottom(sz, color): pBdr = OxmlElement("w:pBdr") bot = OxmlElement("w:bottom") bot.set(qn("w:val"), "single") bot.set(qn("w:sz"), str(sz)) bot.set(qn("w:space"), "0") bot.set(qn("w:color"), color) pBdr.append(bot) return pBdr def _make_pBdr_top(sz, color): pBdr = OxmlElement("w:pBdr") top = OxmlElement("w:top") top.set(qn("w:val"), "single") top.set(qn("w:sz"), str(sz)) top.set(qn("w:space"), "0") top.set(qn("w:color"), color) pBdr.append(top) return pBdr def _make_tabs_right(pos_twip): tabs = OxmlElement("w:tabs") tab = OxmlElement("w:tab") tab.set(qn("w:val"), "right") tab.set(qn("w:pos"), str(pos_twip)) tabs.append(tab) return tabs def _add_redheader(doc: Document, text: str) -> None: """ 红头文字：固定字号 50pt，通过 w:w 元素（字符宽度缩放）压缩到单行。 关键：Word 字符缩放用的是 <w:w w:val="N"/>（不是 w:scale） val 单位：整数百分比，100 = 100% 版心宽 15.6cm，50pt 全角字宽 ≈ 17.65mm 单行最多 @ 100%：156/17.65 ≈ 8.8 字 所需缩放：scale% = min(100, floor(880 / 字数)) w:w 在 CT_RPr schema 中的位置（index 20）： rFonts(1) → b(2) → color(18) → spacing(19) → w:w(20) → kern(21) → sz(23) 必须按序插入，否则 Word 解析报错或忽略。 """ if not text or not text.strip(): return text = text.strip() char_count = len(text) scale_pct = min(100, int(880 / char_count)) if char_count > 0 else 100 # 段落（行距固定58磅 = 50pt字号 + 8pt留白） p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_before = Pt(0) p.paragraph_format.space_after = Pt(20) p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY p.paragraph_format.line_spacing = Pt(58) # Run：固定 50pt run = p.add_run(text) run.font.size = Pt(50) run.font.bold = True run.font.name = "方正小标宋简体" run.font.color.rgb = RGBColor(0xFF, 0x00, 0x00) # 按 CT_RPr schema 顺序手动构建 rPr，确保 w:w 在正确位置 rPr = run._r.get_or_add_rPr() # rFonts（index 1） rFonts = rPr.get_or_add_rFonts() rFonts.set(qn("w:eastAsia"), "方正小标宋简体") rFonts.set(qn("w:ascii"), "方正小标宋简体") rFonts.set(qn("w:hAnsi"), "方正小标宋简体") # w:w（index 20）：字符宽度缩放，插到 sz 之前 # CT_RPr 子元素完整顺序（仅列关键项） _RPR_ORDER = [ "w:rStyle", "w:rFonts", "w:b", "w:bCs", "w:i", "w:iCs", "w:caps", "w:smallCaps", "w:strike", "w:dstrike", "w:outline", "w:shadow", "w:emboss", "w:imprint", "w:noProof", "w:snapToGrid", "w:vanish", "w:webHidden", "w:color", "w:spacing", "w:w", "w:kern", "w:position", "w:sz", "w:szCs", ] w_el = OxmlElement("w:w") w_el.set(qn("w:val"), str(scale_pct)) target_idx = _RPR_ORDER.index("w:w") children = list(rPr) insert_pos = len(children) for i, child in enumerate(children): tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag full = f"w:{tag}" try: if _RPR_ORDER.index(full) > target_idx: insert_pos = i break except ValueError: continue rPr.insert(insert_pos, w_el) def _add_para(doc, text, font, size_pt, align=WD_ALIGN_PARAGRAPH.LEFT, bold=False, color_rgb=None, line_pt=28, before_pt=0, after_pt=0, first_line_chars=0, pBdr=None, tabs=None): p = doc.add_paragraph() _apply_para_fmt(p, line_pt, before_pt, after_pt, align, first_line_chars, pBdr, tabs) run = p.add_run(text) _set_font(run, font, size_pt, bold, color_rgb) return p def _add_empty(doc, line_pt=28): p = doc.add_paragraph() _apply_para_fmt(p, line_pt=line_pt, before_pt=0, after_pt=0) # ── 核心：生成公文 ───────────────────────────────────────────────────────────── def create_gongwen_doc(data: GongwenRequest) -> bytes: doc = Document() sec = doc.sections[0] sec.page_width = Cm(21.0) sec.page_height = Cm(29.7) sec.top_margin = Cm(3.7) sec.bottom_margin = Cm(3.5) sec.left_margin = Cm(2.8) sec.right_margin = Cm(2.6) normal = doc.styles["Normal"] normal.paragraph_format.space_before = Pt(0) normal.paragraph_format.space_after = Pt(0) # 0. 红头 if data.doc_redheader: _add_redheader(doc, data.doc_redheader) # 1. 密级 if data.secrecy: _add_para(doc, data.secrecy, "仿宋_GB2312", 14, align=WD_ALIGN_PARAGRAPH.CENTER) # 2. 紧急程度 if data.urgency: _add_para(doc, data.urgency, "仿宋_GB2312", 14, align=WD_ALIGN_PARAGRAPH.CENTER) # 3. 发文字号 if data.doc_number: _add_para(doc, data.doc_number, "仿宋_GB2312", 16, align=WD_ALIGN_PARAGRAPH.CENTER) # 4. 红色版头分隔线 _add_para(doc, "", "仿宋_GB2312", 14, line_pt=4, before_pt=4, after_pt=4, pBdr=_make_pBdr_bottom(18, "FF0000")) # 5. 标题（红线下空1行，行距35磅） _add_empty(doc, line_pt=35) for line in data.title_lines: _add_para(doc, line, "方正小标宋简体", 22, align=WD_ALIGN_PARAGRAPH.CENTER, line_pt=35) # 6. 标题与正文间距 _add_empty(doc, line_pt=29.8) # 7. 主送机关 if data.recipient: _add_para(doc, data.recipient + "：", "仿宋_GB2312", 16) # 8. 正文 for item in data.body: if item.type == "heading1": _add_para(doc, item.text, "黑体", 16, align=WD_ALIGN_PARAGRAPH.JUSTIFY, first_line_chars=2) elif item.type == "heading2": _add_para(doc, item.text, "楷体", 16, align=WD_ALIGN_PARAGRAPH.JUSTIFY, first_line_chars=2) else: _add_para(doc, item.text, "仿宋_GB2312", 16, align=WD_ALIGN_PARAGRAPH.JUSTIFY, first_line_chars=2) # 9. 附件 if data.attachments: _add_empty(doc) for i, att in enumerate(data.attachments): prefix = "附件：" if len(data.attachments) == 1 else f"附件{i+1}：" _add_para(doc, prefix + att, "仿宋_GB2312", 16) # 10. 空3行 _add_empty(doc) _add_empty(doc) _add_empty(doc) # 11. 署名机关 for line in data.signing_org_lines: if line.strip(): _add_para(doc, line, "仿宋_GB2312", 16, align=WD_ALIGN_PARAGRAPH.RIGHT) # 12. 成文日期 if data.date: _add_para(doc, data.date, "仿宋_GB2312", 16, align=WD_ALIGN_PARAGRAPH.RIGHT, after_pt=8) # 13. 版记 if data.cc_orgs or data.print_org: _add_para(doc, "", "仿宋_GB2312", 14, line_pt=6, pBdr=_make_pBdr_bottom(6, "000000")) if data.cc_orgs: _add_para(doc, "抄送：" + data.cc_orgs, "仿宋_GB2312", 14, line_pt=22) _add_para(doc, "", "仿宋_GB2312", 14, line_pt=6, pBdr=_make_pBdr_top(6, "000000")) if data.print_org or data.print_date: p = doc.add_paragraph() _apply_para_fmt(p, line_pt=20, tabs=_make_tabs_right(8827)) r_left = p.add_run(data.print_org or "") _set_font(r_left, "仿宋_GB2312", 14) p.add_run("\t") r_right = p.add_run((data.print_date or "") + "印发") _set_font(r_right, "仿宋_GB2312", 14) buf = io.BytesIO() doc.save(buf) raw = _fix_zoom(buf.getvalue()) return raw # ── settings.xml zoom 修复 ──────────────────────────────────────────────────── import zipfile, re as _re def _fix_zoom(docx_bytes: bytes) -> bytes: import io as _io buf_in = _io.BytesIO(docx_bytes) buf_out = _io.BytesIO() with zipfile.ZipFile(buf_in, 'r') as zin, \ zipfile.ZipFile(buf_out, 'w', zipfile.ZIP_DEFLATED) as zout: for item in zin.infolist(): data = zin.read(item.filename) if item.filename == 'word/settings.xml': text = data.decode('utf-8') text = _re.sub( r'<w:zoom((?:(?!percent)[^/])*?)/>', r'<w:zoom\1 w:percent="100"/>', text ) data = text.encode('utf-8') zout.writestr(item, data) return buf_out.getvalue() # ── 接口（与 v11 完全一致）─────────────────────────────────────────────────────── @app.post("/generate_from_llm_text") async def generate_from_llm_text(request_data: Any = Body(...)): """ 接收 LLM 输出的 JSON，生成 Word 文档并直接下载。 兼容两种输入格式： 1. {"llm_output": { ... }} (Dify 常见格式) 2. { ... } (直接传入公文对象) """ try: raw_data = request_data if isinstance(raw_data, dict) and "llm_output" in raw_data: content = raw_data["llm_output"] if isinstance(content, str): try: content = json.loads(content) except json.JSONDecodeError: raise HTTPException(status_code=400, detail="llm_output 内容不是有效的 JSON 字符串") else: content = raw_data try: gongwen_data = GongwenRequest(**content) except Exception as e: raise HTTPException(status_code=400, detail=f"数据格式校验失败: {str(e)}") safe_title = "公文" if gongwen_data.title_lines: safe_title = gongwen_data.title_lines[0].replace("/", "_").replace("\\", "_")[:20] filename = f"{safe_title}.docx" doc_bytes = create_gongwen_doc(gongwen_data) encoded_filename = quote(filename, encoding="utf-8") return StreamingResponse( io.BytesIO(doc_bytes), media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", headers={ "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" } ) except HTTPException: raise except Exception as e: import traceback traceback.print_exc() raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}") import base64 class Base64DocxRequest(BaseModel): base64_content: str # 传入的 base64 字符串 filename: Optional[str] = "公文.docx" # 可选文件名 class Base64DocxResponse(BaseModel): filename: str content_type: str base64_content: str # 解码后重新 base64（或直接透传） size_bytes: int @app.post("/base64_to_docx") async def base64_to_docx(req: Base64DocxRequest): """ 接收 base64 编码的 docx，解码后以二进制流返回（供 Dify 文档提取器使用） """ try: b64 = req.base64_content.strip() if "," in b64 and b64.startswith("data:"): b64 = b64.split(",", 1)[1] try: file_bytes = base64.b64decode(b64) except Exception: raise HTTPException(status_code=400, detail="base64 解码失败") if not file_bytes.startswith(b"PK"): raise HTTPException(status_code=400, detail="不是有效的 docx 文件") filename = req.filename or "公文.docx" encoded_filename = quote(filename, encoding="utf-8") return StreamingResponse( io.BytesIO(file_bytes), media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", headers={ "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" } ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}") if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8765) EOF三、公文api开始构建部署FastAPI程序docker compose up -d五、测试chat用以下指令测试业务curl -s -o test.docx -w "HTTP状态码: %{http_code}\n文件大小: %{size_download} bytes\n" -X POST http://localhost:8765/generate_from_llm_text -H "Content-Type: application/json" -d '{"doc_redheader":"测试机关文件","doc_number":"测发〔2026〕1号","title_lines":["关于测试公文格式化服务的通知"],"recipient":"各测试单位","body":[{"type":"intro","text":"为验证公文格式化API服务是否正常，现通知如下："},{"type":"heading1","text":"一、测试内容"},{"type":"para","text":"本次测试涵盖红头、标题、正文、附件及版记等全部要素。"},{"type":"heading2","text":"（一）格式校验"},{"type":"para","text":"请检查生成文档的字体、字号、行距及页边距是否符合GB/T 9704-2012标准。"}],"attachments":["测试附件清单"],"signing_org_lines":["测试机关"],"date":"2026年7月2日","cc_orgs":"抄送测试部门","print_org":"测试机关办公室","print_date":"2026年7月2日"}' && echo "✅ 文件已保存为 test.docx，请用Word或WPS打开验证排版" || echo "❌ 请求失败"
准备工作操作系统CentOS Stream 9 (x86)cat /etc/os-release NAME="CentOS Stream" VERSION="9" ID="centos" ID_LIKE="rhel fedora" VERSION_ID="9" PLATFORM_ID="platform:el9" PRETTY_NAME="CentOS Stream 9" ANSI_COLOR="0;31" LOGO="fedora-logo-icon" CPE_NAME="cpe:/o:centos:centos:9" HOME_URL="https://centos.org/" BUG_REPORT_URL="https://issues.redhat.com/" REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux 9" REDHAT_SUPPORT_PRODUCT_VERSION="CentOS Stream"已安装docker composeDocker version 29.5.3, build d1c06ef Docker Compose version v5.1.4AnythingLLM tag版本号为1.14.1配置好vLLM，并且可以curl访问chat和embedding模型接口配置好SearXNG，并且可以curl访问json接口一、目录结构anythingllm/├── docker-compose.yml└── storage/ (留空，需要777权限)二、创建配置文件mkdir -p -m 777 anythingllm/storage/ && cd anythingllmcat > docker-compose.yml << 'EOF' services: anythingllm: image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/mintplexlabs/anythingllm:1.14.1 container_name: anythingllm ports: - "3001:3001" cap_add: - SYS_ADMIN environment: - TZ=Asia/Shanghai - STORAGE_DIR=/app/server/storage - JWT_SECRET=your_super_secret_key_here_1234567890 volumes: - /etc/localtime:/etc/localtime:ro - ./storage:/app/server/storage restart: unless-stopped EOF三、启动AnythingLLMdocker compose up -d正常启动后可以访问http://localhost:3001来访问后台管理web四、配置web1、配置模型供应商首次访问会出现欢迎界面流程LLM偏好选择LiteLLM字段名值说明Base URLhttp://118.193.117.134:3000/v1配置LiteLLM的网关地址Chat Model Selectionqwen3-public配置好url和api key后，会连接到网关查询可用模型列表，自行选择。如果没有出现模型列表，说明LiteLLM配置有问题，或者网络有问题。Model context window4096模型最大上下文长度，不要太大，超过vLLM的配置阈值可能会报错API Keysk-mHz0miMx_QyR6xAhxVmSgw根据之前的配置填写key2、配置用户账户将有多少用户使用此实例？根据实际使用来选择，我这里是选择只有我你想要设置密码吗？选择否3、测试chat正常配置到目前可以正常对话了，可以在对话中进行QA4、配置embedding模型进入设置--->人工智能提供商--->嵌入器(Embedding)--->选择LiteLLM字段名值说明Base URLhttp://118.193.117.134:3000/v1同上网关地址Embedding Model Selectionembedding-public配置好url和api key后，会连接到网关查询可用模型列表，自行选择。如果没有出现模型列表，说明LiteLLM配置有问题，或者网络有问题。Max embedding chunk length4096默认值API Keysk-mHz0miMx_QyR6xAhxVmSgw根据之前的配置填写key保存更改5、配置联网搜索RAG进入设置--->代理技能--->实时网络搜索和浏览--->打开开关实时网络搜索和浏览--->选择SearXNG--->SearXNG API Base URL填写http://10.101.83.124:18080--->保存6、配置日期变量进入设置--->管理员--->Default System Prompt--->填写System Prompt：今天的日期时间是{datetime_24}五、测试chat提问：现在是什么时间，帮我搜索现在的新闻内容正常会回答当前日期时间，以及目前的时政新闻。当前时间是2026年6月27日。以下是最新新闻摘要： 福建新闻 福建省人民政府印发最新通知，《福建省牲畜屠宰管理条例》将于10月1日起施行。 “在中国式现代化建设中奋勇争先 奋力谱写新征程新福建建设新篇章”系列主题新闻发布会举行。 人民网福建频道推出“遇见福建之美”图片栏目，展示八闽精彩瞬间。 福州新闻 福州计划在2026年新增15公顷公园开放共享空间。 福州足球队客场1:0战胜漳州，实现闽超六连胜，进入争冠组。 福建发布暴雨警报，福州将迎来中雨、大雨、暴雨天气。 国内新闻 上海一名90岁老人取快递时被绊倒，股骨骨折两个月后去世。 一名13岁男孩失踪5天，救援人员一度失去信心，最终在母亲坚持下被找到。 一名8岁女孩在ICU抢救一天花费1万元。 国际新闻 巴基斯坦内政部长抵达德黑兰。 特朗普访华相关报道引发关注，首款载人变形机甲发布，引发对未来性能与应用的讨论。 经济与社会 5月末我国外汇储备升至34422亿美元。 全国麦收进度接近六成，夏收工作正在稳步推进。 如需更详细内容，可访问相关网站或提供具体新闻类别，我将为您进一步整理！
准备工作操作系统CentOS Stream 9 (x86)cat /etc/os-release NAME="CentOS Stream" VERSION="9" ID="centos" ID_LIKE="rhel fedora" VERSION_ID="9" PLATFORM_ID="platform:el9" PRETTY_NAME="CentOS Stream 9" ANSI_COLOR="0;31" LOGO="fedora-logo-icon" CPE_NAME="cpe:/o:centos:centos:9" HOME_URL="https://centos.org/" BUG_REPORT_URL="https://issues.redhat.com/" REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux 9" REDHAT_SUPPORT_PRODUCT_VERSION="CentOS Stream"已安装docker composeDocker version 29.5.3, build d1c06ef Docker Compose version v5.1.4SearXNG tag版本号为2026.6.17-4dfdc822c一、目录结构SearXNG/├── docker-compose.yml└── searxng-config/ └── settings.yml二、创建配置文件mkdir -p SearXNG/searxng-config/ && cd SearXNGcat > docker-compose.yml << 'EOF' services: searxng: image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/searxng/searxng:2026.6.17-4dfdc822c container_name: searxng ports: - "18080:8080" # 将宿主机的18080端口映射到容器的8080端口 volumes: - ./searxng-config:/etc/searxng # 持久化配置文件，方便后续自定义设置 environment: - BASE_URL=http://localhost:18080 # 设置基础URL，确保API调用正确 - INSTANCE_NAME=my-searxng # 实例名称，用于标识你的搜索服务 - SEARXNG_SECRET=your-secret-key-here # 强烈建议替换为你的随机密钥，增强安全性 restart: always # 容器异常退出后自动重启 EOFcat > searxng-config/settings.yml << 'EOF' use_default_settings: true server: secret_key: "5dibmPP2UapoQ0NsvmPHZcabgr1QLgCS" image_proxy: true search: formats: - html - json default_engines: - baidu engines: # 1. 国内可用及保留的搜索引擎 (已启用) - name: baidu engine: baidu shortcut: bd disabled: false - name: sogou engine: sogou shortcut: sogou timeout: 5.0 disabled: false - name: 360search engine: 360search shortcut: 360so disabled: false - name: quark engine: quark shortcut: qk disabled: false - name: bing engine: bing shortcut: bi disabled: false - name: bilibili engine: bilibili shortcut: bil disabled: false - name: sogou_video engine: sogou_video shortcut: sogouv disabled: false - name: sogou_wechat engine: sogou_wechat shortcut: sogouw disabled: false # 2. 你要求禁用的搜索引擎 (已禁用) - name: brave engine: brave shortcut: br disabled: true - name: duckduckgo engine: duckduckgo shortcut: ddg disabled: true - name: google engine: google shortcut: go disabled: true - name: startpage engine: startpage shortcut: sp disabled: true - name: wikipedia engine: wikipedia shortcut: wp disabled: true EOF三、启动SearXNGdocker compose up -d正常启动后可以访问http://localhost:18080来访问后台管理web四、测试接口curl测试json格式apicurl -s "http://10.101.83.124:18080/search?q=今天天气如何&format=json" | jq .正常结果如下{ "query": "今天天气如何", "results": [ { "title": "广州天气预报,广州7天天气预报,广州15天天气预报,广州天气查询", "url": "http://www.src.weather.com.cn/weather/101280101.shtml", "content": "2026年5月28日 - 蓝天预报综合天气现象、能见度、空气质量等因子,预测未来一周的天空状况。.35/27°C广州香江野生动物世界/...", "engine": "360search", "template": "default.html", "parsed_url": [ "http", "www.src.weather.com.cn", "/weather/101280101.shtml", ...
准备工作系统为openEuler 22.03 ARM架构 (x86也可以用，但是要自行修改镜像地址)cat /etc/os-release NAME="openEuler" VERSION="22.03 (LTS-SP4)" ID="openEuler" VERSION_ID="22.03" PRETTY_NAME="openEuler 22.03 (LTS-SP4)" ANSI_COLOR="0;31"已安装docker composeDocker version 29.5.3, build d1c06ef Docker Compose version v5.1.4LiteLLM版本号为v1.89.1vLLM已搭建，并且可以用curl请求，测试请求格式范例如下：chatcurl http://127.0.0.1:8000/v1/chat/completions -H "Content-Type: application/json" -d '{"model":"qwen3","messages":[{"role":"user","content":"你好，请简单介绍下自己"}],"temperature":0.7,"max_tokens":1024}'embeddingcurl http://127.0.0.1:9010/v1/embeddings -H "Content-Type: application/json" -d '{"model":"/model","input":"测试文本向量化"}'rerankcurl http://127.0.0.1:9011/v1/score -H "Content-Type: application/json" -d '{"model":"/model","pairs":[["人工智能是什么","人工智能是模拟人类智能的技术"],["人工智能是什么","今天下雨了"]]}'一、目录结构litellm/├── .env├── docker-compose.yml└── prometheus.yml二、创建配置文件mkdir litellm && cd litellm.env中的LITELLM_MASTER_KEY为默认登入密码，SEARXNG_API_BASE为可选配置cat > .env << 'EOF' LITELLM_MASTER_KEY="sk-c0oE4EgitT4DmXyRj4Av" SEARXNG_API_BASE="http://192.168.105.143:18080" EOFcat > docker-compose.yml << 'EOF' services: litellm: build: context: . args: target: runtime image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/docker.litellm.ai/berriai/litellm:main-stable-linuxarm64 ######################################### ## Uncomment these lines to start proxy with a config.yaml file ## # volumes: # - ./config.yaml:/app/config.yaml # command: # - "--config=/app/config.yaml" ############################################## ports: - "3000:4000" # Map the container port to the host, change the host port if necessary environment: DATABASE_URL: "postgresql://llmproxy:dbpassword9090@db:5432/litellm" # Optional: route read-only queries (find_*, count, group_by, query_raw/_first) # to a separate reader endpoint, e.g. an Aurora reader. Leave unset for # single-DB deployments. With IAM_TOKEN_DB_AUTH enabled, the reader URL # is auto-refreshed alongside the writer. # DATABASE_URL_READ_REPLICA: "postgresql://llmproxy:dbpassword9090@db-reader:5432/litellm" STORE_MODEL_IN_DB: "True" # allows adding models to proxy via UI env_file: - .env # Load local .env file depends_on: - db # Indicates that this service depends on the 'db' service, ensuring 'db' starts first healthcheck: # Defines the health check configuration for the container test: - CMD-SHELL - python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')" # Command to execute for health check interval: 30s # Perform health check every 30 seconds timeout: 10s # Health check command times out after 10 seconds retries: 3 # Retry up to 3 times if health check fails start_period: 40s # Wait 40 seconds after container start before beginning health checks db: image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/postgres:16-linuxarm64 restart: always container_name: litellm_db environment: POSTGRES_DB: litellm POSTGRES_USER: llmproxy POSTGRES_PASSWORD: dbpassword9090 ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data # Persists Postgres data across container restarts healthcheck: test: ["CMD-SHELL", "pg_isready -d litellm -U llmproxy"] interval: 1s timeout: 5s retries: 10 prometheus: image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/prom/prometheus:v3.5.4-linuxarm64 volumes: - prometheus_data:/prometheus - ./prometheus.yml:/etc/prometheus/prometheus.yml ports: - "9090:9090" command: - "--config.file=/etc/prometheus/prometheus.yml" - "--storage.tsdb.path=/prometheus" - "--storage.tsdb.retention.time=15d" restart: always volumes: prometheus_data: driver: local postgres_data: name: litellm_postgres_data # Named volume for Postgres data persistence EOFcat > prometheus.yml << 'EOF' global: scrape_interval: 15s scrape_configs: - job_name: 'litellm' static_configs: - targets: ['litellm:4000'] # Assuming Litellm exposes metrics at port 4000 EOF三、启动LiteLLMdocker compose up -d正常启动后可以访问http://localhost:3000/ui来访问后台管理web四、配置web1、配置模型地址Models + Endpoints ---> Add Model表单配置如下:字段名值说明Providervllm上游模型供应商类型LiteLLM Model Name(s)qwen3上游模型名称，根据vllm的docker启动配置的名称Model Mappings->Public Model Nameqwen3-public提供给下游平台的暴露模型名称Model Mappings->LiteLLM Model Name默认值上面的填好了这里自动完成，正常和上面配置的字符串相等ModeChat - /chat/completions聊天模型选择chat，embedding和rerank根据类型自行选择Existing Credentials留空 API Basehttp://127.0.0.1:8000/v1api地址，路由填写到v1API Key留空没配置秘钥就不用填写配置完成后点击Test Connect测试连接是否成功，成功后保存2、配置用户组Teams ---> Create Team表单配置如下:字段名值说明Team Nametest-team自行定义组名Modelsqwen3-public根据上面配置的对外服务模型名称选择其余字段全部留空或者默认3、配置用户Internal Users ---> Invite User表单配置如下:字段名值说明User Emailtest-person用户名，可以不是email地址，也支持中文Global Proxy RoleInternal User (View Only)可选，这里默认是只可使用模型的用户，无管理权限Teamtest-team用户组名，根据上面配置好的组名选择其余字段全部留空或者默认4、配置API KeyVirtual Keys ---> Create New Key表单配置如下:字段名值说明Owned ByAnother User配置给用户使用的key，还有其他的比如配置给服务或者agentUser IDtest-person上面创建的用户名Organization留空 Teamtest-team上面创建的用户组名Key Nametest-key秘钥名称，在log或usage中显示的名称ModelsAll team Models秘钥可以使用的模型名称，这里可以选所有可用模型，或者单个模型，单个模型场景下可以针对每个用户每个模型单独配额Key TypeAI APIs秘钥权限其余字段全部留空或者默认保存后会跳出对话框显示秘钥，注意此处的秘钥有且仅有一次机会可以复制，后续都会变为密文形式我这里的秘钥是sk-mHz0miMx_QyR6xAhxVmSgw五、测试连通性使用以下curl测试，key和model字段，替换为上面的值curl http://localhost:3000/v1/chat/completions -H "Content-Type: application/json" -H "Authorization: Bearer sk-mHz0miMx_QyR6xAhxVmSgw" -d '{"model": "qwen3-public", "messages": [{"role": "user", "content": "你好，请介绍一下你自己"}]}'正常的话，结果如下[root@localhost gongwen]# curl http://localhost:3000/v1/chat/completions -H "Content-Type: application/json" -H "Authorization: Bearer sk-mHz0miMx_QyR6xAhxVmSgw" -d '{"model": "qwen3-public", "messages": [{"role": "user", "content": "你好，请介绍一下你自己"}]}' {"id":"chatcmpl-bfbabeb7f39ecddb","created":1782720274,"model":"qwen3-public","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"你好！我是通义千问（Qwen），是阿里巴巴集团旗下的通义实验室研发的超大规模语言模型。我能够回答问题、创作文字，比如写故事、公文、邮件、剧本，进行逻辑推理、编程，甚至能玩游戏、表达观点和聊天。\n\n我的能力包括但不限于：\n\n- 多语言支持：我支持包括中文、英文、法语、西班牙语、葡萄牙语、俄语、阿拉伯语、日语、韩语、越南语、泰语、印尼语等在内的数十种语言。\n- 代码写作：我熟悉多种编程语言，可以帮你写代码、调试、解释算法。\n- 逻辑推理：我能处理复杂的逻辑问题，比如数学题、谜题、推理题等。\n- 长文本处理：我支持超长上下文（最高可达32768个token），适合处理长文档、书籍摘要、会议记录等。\n- 对话理解：我擅长多轮对话，能记住上下文，提供连贯、自然的交流体验。\n- 知识丰富：我训练数据截至2024年，涵盖广泛领域，能为你提供最新、最全面的信息（在训练数据范围内）。\n\n我叫“通义千问”，“通义”代表我具有广泛的知识和普适性，“千问”则代表我能够回答各种各样的问题，无论多么复杂或独特。\n\n如果你有任何问题或需要帮助，尽管告诉我，我会尽力为你提供支持！😊\n\n—— 你的AI助手 Qwen","role":"assistant","provider_specific_fields":{"refusal":null,"reasoning":null}},"provider_specific_fields":{"token_ids":null,"stop_reason":null}}],"usage":{"completion_tokens":318,"prompt_tokens":12,"total_tokens":330}}[root@localhost gongwen]#
将下面代码中的第一句中的muyijiang改为自己将要设置的密码。否则默认root密码为muyijiang。创建实例的时候，点击添加启动脚本，将下列代码复制进去(注意更改root密码)。#!/bin/bash echo root:muyijiang |sudo chpasswd root sudo sed -i 's/^.*PermitRootLogin.*/PermitRootLogin yes/g' /etc/ssh/sshd_config; sudo sed -i 's/^.*PasswordAuthentication.*/PasswordAuthentication yes/g' /etc/ssh/sshd_config; sudo reboot对于EC2，GCE等机器也可以使用.不能添加代码的机器，可以登录进ssh，再执行上面的代码。也可以开启root登陆。以上代码在Debian/Ubuntu/Centos上通过测试。其他发行版自行测试。原文来源：萌咖 木易酱
来源：樱花庄的白猫工具：OpenSSL 1.1.0g平台：Ubuntu 18.04准备工作检查 OpenSSL 配置文件：nano /etc/ssl/openssl.cnf找到 CA 配置部分，确认以下配置：[ CA_default ] dir = ./demoCA # Where everything is kept certs = $dir/certs # Where the issued certs are kept crl_dir = $dir/crl # Where the issued crl are kept database = $dir/index.txt # database index file. new_certs_dir = $dir/newcerts # default place for new certs. certificate = $dir/cacert.pem # The CA certificate serial = $dir/serial # The current serial number crlnumber = $dir/crlnumber # the current crl number crl = $dir/crl.pem # The current CRL private_key = $dir/private/cakey.pem# The private key RANDFILE = $dir/private/.rand # private random number file初始化目录：mkdir -p ./demoCA/{private,newcerts} && touch ./demoCA/index.txt && touch ./demoCA/serial && echo 01 > ./demoCA/serial签发 CA 证书创建 CA 配置文件：nano root.conf写入以下配置：[ req ] default_bits = 2048 default_keyfile = r.pem default_md = sha256 string_mask = nombstr distinguished_name = req_distinguished_name req_extensions = req_ext x509_extensions = x509_ext [ req_distinguished_name ] countryName = Country Name (2 letter code) countryName_default = CN stateOrProvinceName = State or Province Name (full name) stateOrProvinceName_default = Shanghai localityName = Locality Name (eg, city) localityName_default = Shanghai organizationName = Organization Name (eg, company) organizationName_default = Mashiro LLC commonName = Common Name (e.g. server FQDN or YOUR name) commonName_max = 64 commonName_default = Mashiro Internet Fake Authority CA [ x509_ext ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer basicConstraints = CA:TRUE keyUsage = digitalSignature, keyEncipherment, keyCertSign, cRLSign [ req_ext ] subjectKeyIdentifier = hash basicConstraints = CA:TRUE keyUsage = digitalSignature, keyEncipherment, keyCertSign, cRLSign参数含义：字段值countryName国家名缩写stateOrProvinceName州或省localityName地点，如城市organizationName组织名commonName商标（证书上显示的 CA 名称）*xxx_default设置该字段默认值，这样等一下生成证书时就不用手动填写信息，直接回车使用默认值就行了。生成 CA 根密钥：openssl genrsa -out ./demoCA/private/cakey.pem 2048自签发 CA 根证书：openssl req -new -x509 -key ./demoCA/private/cakey.pem -out ./demoCA/cacert.pem -days 7300 -config ./root.conf将 PEM 格式证书转为常用的 DER 格式：openssl x509 -inform PEM -in ./demoCA/cacert.pem -outform DER -out ./demoCA/CA.cer用 CA 证书签发 SSL 证书创建文件夹方便管理：mkdir 2heng.xin创建用户证书配置文件：nano server.conf写入以下配置：[ req ] default_bits = 2048 default_keyfile = r.pem default_md = sha256 string_mask = nombstr distinguished_name = req_distinguished_name req_extensions = req_ext x509_extensions = x509_ext [ req_distinguished_name ] countryName = Country Name (2 letter code) countryName_default = CN stateOrProvinceName = State or Province Name (full name) stateOrProvinceName_default = Shanghai localityName = Locality Name (eg, city) localityName_default = Shanghai organizationName = Organization Name (eg, company) organizationName_default = Mashiro LLC commonName = Common Name (e.g. server FQDN or YOUR name) commonName_max = 64 commonName_default = *.2heng.xin [ x509_ext ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer basicConstraints = CA:FALSE keyUsage = digitalSignature, keyEncipherment subjectAltName = @alt_names [ req_ext ] subjectKeyIdentifier = hash basicConstraints = CA:FALSE keyUsage = digitalSignature, keyEncipherment subjectAltName = @alt_names [ alt_names ] DNS.1 = *.2heng.xin DNS.2 = 2heng.xin IP.1 = 127.0.0.1 IP.2 = 8.8.8.8注意：在[ alt_names ]下填写要签发证书的域名或 IP，支持通配符；Firefox 下出现MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY，原因是basicConstraints 被设置成了CA:TRUE，改为CA:FALSE 即可。生成用户 RSA 密钥：openssl genrsa -out ./2heng.xin/2heng.xin.key 2048生成用户证书请求：openssl req -new -key ./2heng.xin/2heng.xin.key -out ./2heng.xin/2heng.xin.csr -config ./server.conf签发用户证书：openssl ca -in ./2heng.xin/2heng.xin.csr -out ./2heng.xin/2heng.xin.crt -days 3650 -extensions x509_ext -extfile ./server.conf附上证书签发目录结构：$ tree . ├── demoCA │ ├── CA.cer # CA 证书（DER 格式） │ ├── cacert.pem # CA 证书（PEM 格式） │ ├── index.txt # 签发记录数据库 │ ├── index.txt.attr │ ├── index.txt.old │ ├── newcerts │ │ └── 01.pem │ ├── private │ │ └── cakey.pem # CA 私钥 │ ├── serial │ └── serial.old ├── 2heng.xin │ ├── 2heng.xin.crt # 用户证书 │ ├── 2heng.xin.csr │ └── 2heng.xin.key # 用户证书私钥 ├── root.conf # CA 配置文件 └── server.conf # 用户配置文件参考：How to setup your own CA with OpenSSLSSL证书在线工具SSL Online Toolshttps://github.com/mashirozx/Pixiv-Nginx/issues/7
GitKraken 更新后免费版本无法打开私有库，可以退回6.5.1（最新可以私有库的版本）进行使用。安装完之后需要禁止自动更新，可通过添加下列host来禁止自动更新：127.0.0.1 release.gitkraken.com api.gitkraken.com防止官方封禁旧版本下载地址，这里进行备份。官方地址win-x86:release.axocdn.com/win32/GitKrakenSetup-6.5.1.exerelease.gitkraken.com/win32/GitKrakenSetup-6.5.1.exewin-x64:release.axocdn.com/win64/GitKrakenSetup-6.5.1.exerelease.gitkraken.com/win64/GitKrakenSetup-6.5.1.exelinux-deb:release.axocdn.com/linux/GitKraken-v6.5.1.debrelease.gitkraken.com/linux/GitKraken-v6.5.1.deblinux-rpm:release.axocdn.com/linux/GitKraken-v6.5.1.rpmrelease.gitkraken.com/linux/GitKraken-v6.5.1.rpmlinux-tar.gz:release.axocdn.com/linux/GitKraken-v6.5.1.tar.gzrelease.gitkraken.com/linux/GitKraken-v6.5.1.tar.gzmac:release.axocdn.com/darwin/GitKraken-v6.5.1.ziprelease.gitkraken.com/darwin/GitKraken-v6.5.1.zip本站备份win-x86: GitKrakenSetup-6.5.1-x86.exewin-x64: GitKrakenSetup-6.5.1-x64.exelinux-rpm:GitKraken-v6.5.1.rpmlinux-deb:GitKraken-v6.5.1.deblinux-tar.gz:GitKraken-v6.5.1.tar.gzmac:GitKraken-v6.5.1.zipGitKraken学习补丁（支持 7.7.0 ~ 8.2.0）：gitee.com/lepton-lee/GitCracken如无法使用请使用上方旧版GitKraken。