钉钉接入Hermes

1.前往钉钉后台创建对应消息应用

支持一键创建 配置应用名称即可

2.创建完成后获取到对应的ID,前往Hermes把对应的ID配置进入

3.测试效果

发现有thinking标志但是没有回复消息

查看日志发现Hermes版本过老 执行hermes update更新

仍旧无效 排查日志
2026-06-15 10:28:51,608 INFO gateway.platforms.dingtalk: [Dingtalk] _send_emotion: reply 🤔Thinking on msg=

怀疑可能是版本不兼容问题 执行降版本指令

/Users/mac/.hermes/hermes-agent/venv/bin/python -m pip install –force-reinstall –no-cache-dir dingtalk-stream==0.23.0

无效

原因:
# 安全:限制可与机器人交互的用户
DINGTALK_ALLOWED_USERS=user-id-1 用户ID错误导致

GATEWAY_ALLOW_ALL_USERS=true 取消白名单限制 消息回复正常

配置自动更新证书流程


场景与目标

  • 服务器:京东云 ECS(Ubuntu 22.04)
  • DNS:域名在腾讯云解析
  • 证书:Let’s Encrypt
  • 架构:所有外网 80/443 入口由 Docker 容器 nginx-proxy 统一接入与反向代理(HTTPS 终止在网关层)
  • 目标:
  • 自动签发:主域名 + 泛域名 (*.主域名)
  • 自动续期:到期前自动完成 DNS-01 验证并续签
  • 自动部署:续期后自动将新证书部署到 nginx-proxy 并 reload 生效

一、准备条件

1) 域名必须处于正常可用状态(及时续费,避免过期导致续签失败)。

2) 腾讯云侧准备 DNS API 密钥(CAM):

  • 生成 SecretId / SecretKey
  • 授权至少包含修改 DNS 记录(添加/删除 TXT)的权限

3) 服务器侧准备基础依赖:

  • curltarca-certificatessocatcron(DNS-01 + acme.sh 常用依赖)
  • 服务器能访问 Let’s Encrypt 的 ACME 接口
  • 若 GitHub 访问不稳定,可用国内镜像源(如 Gitee)拉取 acme.sh

二、安装 acme.sh(解决 GitHub 下载不稳定)

在大陆网络环境里,GitHub 下载常超时。可以改用 Gitee 镜像安装:

apt update
apt install -y git ca-certificates socat cron
update-ca-certificates

cd /root
git clone --depth 1 https://gitee.com/neilpang/acme.sh.git /root/acme.sh-src
cd /root/acme.sh-src
./acme.sh --install \
  --home /root/acme.sh \
  --config-home /root/.acme.sh \
  --accountemail <你的邮箱>

验证安装:

/root/acme.sh/acme.sh --version

三、配置 Let’s Encrypt 为默认 CA

acme.sh 可能默认使用其他 CA(如 ZeroSSL)。显式切到 Let’s Encrypt:

/root/acme.sh/acme.sh --set-default-ca --server letsencrypt

四、配置腾讯云 DNS API(敏感信息不要写进命令历史)

将密钥写入仅 root 可读文件(占位符替换):

cat > /root/.tencent_dns_api <<'EOF'
export Tencent_SecretId="<SECRET_ID>"
export Tencent_SecretKey="<SECRET_KEY>"
EOF
chmod 600 /root/.tencent_dns_api

五、签发泛域名证书(DNS-01)

加载密钥环境变量并签发:

source /root/.tencent_dns_api

/root/acme.sh/acme.sh --issue --server letsencrypt \
  --dns dns_tencent \
  -d <主域名> -d '*.<主域名>' \
  --dnssleep 120

说明:

  • -d <主域名> 用于根域(例如 example.com
  • -d '*.<主域名>' 用于所有子域(例如 api.example.comwww.example.com
  • --dnssleep 用于等待 TXT 记录生效,DNS 慢可改成 300/600

签发成功后,证书文件会在类似路径中生成:

  • /root/.acme.sh/<主域名>_ecc/fullchain.cer
  • /root/.acme.sh/<主域名>_ecc/<主域名>.key

六、将证书自动部署到 Docker 网关 nginx-proxy(关键:统一入口)

本方案的核心是:只让 nginx-proxy 使用证书,其他服务容器走内网 HTTP,由 nginx-proxy 统一提供 HTTPS。

1)确认 nginx-proxy 的证书目录挂载

通过 Docker inspect 可看到类似映射:

  • 宿主机:/home/<用户>/my-server/nginx-proxy/ssl
  • 容器内:/etc/nginx/ssl

且 Nginx 配置使用:

  • ssl_certificate /etc/nginx/ssl/cert.pem;
  • ssl_certificate_key /etc/nginx/ssl/key.pem;

2)用 acme.sh 安装证书到 nginx-proxy 对应宿主机目录,并设置自动 reload

mkdir -p /home/<用户>/my-server/nginx-proxy/ssl
chmod 700 /home/<用户>/my-server/nginx-proxy/ssl

/root/acme.sh/acme.sh --install-cert -d <主域名> \
  --key-file /home/<用户>/my-server/nginx-proxy/ssl/key.pem \
  --fullchain-file /home/<用户>/my-server/nginx-proxy/ssl/cert.pem \
  --reloadcmd "docker exec nginx-proxy nginx -s reload"

chmod 600 /home/<用户>/my-server/nginx-proxy/ssl/key.pem \
          /home/<用户>/my-server/nginx-proxy/ssl/cert.pem

要点:

  • fullchain-file 必须是 fullchain(含中间证书),兼容性最好
  • --reloadcmd 是“自动生效”的关键:续期成功后自动热加载 nginx-proxy

3)手动验证 nginx 配置并 reload

docker exec nginx-proxy nginx -t
docker exec nginx-proxy nginx -s reload

七、自动续期是如何发生的(cron)

安装 acme.sh 时会自动为 root 写入 crontab 定时任务,例如:

  • 每天固定时间运行 acme.sh --cron
  • 它会检查证书是否进入续期窗口(一般到期前约 30 天)
  • 若需要续期,会自动执行:
  • 调用腾讯云 DNS API 添加/验证/删除 TXT 记录
  • 向 Let’s Encrypt 申请新证书
  • 写入你在 --install-cert 指定的证书路径
  • 执行 --reloadcmd(例如 reload nginx-proxy)

检查定时任务与证书状态:

crontab -l | grep acme
/root/acme.sh/acme.sh --list

列表中会显示下一次进入续期窗口的时间(Renew Time)。


八、验证线上证书有效期(无需依赖腾讯云控制台)

服务器本机验证(查看签发者/有效期):

echo | openssl s_client -servername <主域名> -connect 127.0.0.1:443 2>/dev/null \
  | openssl x509 -noout -issuer -subject -dates

也可以浏览器访问站点,点击锁标志查看证书到期时间。


九、扩展:以后新增更多 Docker 服务怎么做?

因为使用的是“统一入口网关 + 泛域名证书”:

  • 新增服务只需:
  • 新增一个 DNS 解析记录(如 api.<主域名> 指向服务器)
  • 在 nginx-proxy 的 conf.d 增加一个 server_name api.<主域名> 的反代配置
  • 不需要为每个服务容器单独挂载证书,也不需要每个服务单独续期。

中间件

中间件(Middleware)是一个在每次请求进入 FastAPI 应用时都会被执行的函数。
它在请求到达实际的路径操作(路由处理函数)之前运行,并且在响应返回给客户端之前再运行一次。

作用: 为每个请求添加统一的处理逻辑(记录日志、身份认证、跨域、设置响应头、性能监控等)

@app.middleware('http')
async def add_process_time_header(request: Request, call_next):
print('process_time_header called')
response = await call_next(request)
print('process_time_header called with response {}'.format(response))

代码如上 在访问根路径时会报错

原因: 缺少return 框架后面会把“返回的响应”当成可执行对象继续处理;现在拿到的是 None ,所以报:

TypeError: ‘NoneType’ object is not callable

修改代码

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
print("process_time_header called")
response = await call_next(request)
print("process_time_header called with response {}".format(response))
return response

再次访问 控制台输出日志

process_time_header called
process_time_header called with response
INFO: 127.0.0.1:60206 – “GET / HTTP/1.1” 200 OK

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
print("process_time_header called")
response = await call_next(request)
print("process_time_header called with response {}".format(response))
return response

@app.middleware("http")
async def middleware_test(request,call):
print("middleware_test start")
res = await call(request)
print("middleware_test end")
return res

多个中间件执行顺序是由下而上的

middleware_test start
process_time_header called
process_time_header called with response
middleware_test end
INFO: 127.0.0.1:60431 – “GET / HTTP/1.1” 200 OK

响应类型

默认情况下,FastAPI会自动将路径操作函数返回的 Python对象(字典、列表、Pydantic 模型等),经由jsonable_encoder 转换为JSON 兼容格式,并包装为 JSONResponse 返回。这省去了手动序列化的步骤,让开发者能更专注于业务逻辑。如果需要返回非 JSON 数据(如 HTML、文件流),FastAP! 提供了丰富的响应类型来返回不同数据

自定义响应格式

response_model 是路径操作装饰器(如 @app.get或 @app.post)的关键参数,它通过一个 Pydantic 模型来严格定义和约束 AP! 端点的输出格式。这一机制在提供自动数据验证和序列化的同时,更是保障数据安全性的第一道防线。

class News(BaseModel):
    id: int
    title: str
    content: str


@app.get("/news/{id}", response_model=News)
async def get_news(id: int):
    return {
        "id": id,
        "title": f"这是第{id}本书",
        "content": "这是一本好书"
    }

参数

不同情况下的参数作用与意义各不相同

1.路径参数

@app.get("/book/{id}")
async def get_book(id: int):
    return {"id": id}

Path

@app.get("/get_name/{name}")
async def get_nanme(name: str = Path(...,max_length=10,min_length=2,description='长度范围2-10')):
    return {"name": name}

通过导入Path 可以控制参数的一些条件

第一个位置的 … 代表该参数必填

max_length和min_length分别限制了最大和最小长度

description 表示注解

2.查询参数

@app.get("/news/news_list")
async def get_news_list(
        page: int = Query(default=1, ge=1, le=10,description='范围在1-10'),
        limit: int = Query(default=5, ge=1, le=10),
        skip: int = Query(default=0, ge=0, le=10),
):
    
    return {"page": page, "limit": limit, "skip": skip}

Query

查询参数可以通过Query来控制

3.请求体参数

class User(BaseModel):
    username: str = Field(...,min_length=2,max_length=10,description='输入用户名,长度2-10')
    password: str = Field(...,min_length=2,max_length=10,description='输入密码,长度2-10')
    age: int

@app.post("/register")
async def register(user: User):
    return user

Filed

这里输入注解时是不提醒的

当输入类型错误时响应会有提示

创建web项目

运行服务

访问/docs (交互式文档)出现报错

可以看到这里解释器版本是3.9

执行命令

python -c "import sys,fastapi,pydantic,pydantic_core,importlib.metadata as m; print(sys.version); print('fastapi',fastapi.__version__); print('pydantic',pydantic.__version__); print('pydantic_core',pydantic_core.__version__); print('typing-inspection',m.version('typing-inspection'))"
3.9.0 (v3.9.0:9cf6752276, Oct  5 2020, 11:29:23) 
[Clang 6.0 (clang-600.0.57)]
Traceback (most recent call last):
  File "<string>", line 1, in <module>
AttributeError: module 'typing_inspection' has no attribute '__version__'

astAPI 已经移除 3.8 支持,并持续跟进新版本 Python(已提到支持到 3.14)

选择更新解释器版本并且重新配置虚拟环境,这里为了方便,重新创建项目并且选择比较适配的3.11版本

创建完成后重新运行服务并且访问/docs (交互式文档)

.env文件配置命名问题

今天配置.env文件发现一个BUG 代码执行是保存未获取到对应秘钥名称

.env文件内存在该名称

错误原因:

for k, v in os.environ.items ( ) :

if k.isupper ( ) :

先按所需的组合键,再按 Enter 键。

这个问题的原因是 server/settings.py 文件中有一段自动加载环境变量的代码

这段代码只会将 全大写 的环境变量自动注入到 Django 的 settings 中。由于您的变量名 例:Key 包含了小写字母,它被这段逻辑忽略了,导致 settings 对象中没有这个属性。

这个行为是由 server/settings.py 文件底部的这几行代码决定的:

for k, v in os.environ.items():

if k.isupper(): # <— 关键在这里

globals().setdefault(k, v)

所以将 Key 命名修改为 KEY即可

LangChain

  • LangChain 是地基。它定义了最基础的组件:什么是 Model,什么是 Prompt,什么是 Memory,什么是 Tool。
    • CrewAI 和 AutoGen 在底层大量复用了 LangChain 的概念(尤其是 CrewAI,它直接依赖 LangChain 的组件)。
    • 学习策略:只要懂了 LangChain 的核心概念(Chain, Agent, Tool),看 CrewAI 的代码就像看“封装好的高级脚本”,非常容易上手。
  • LangGraph 是关键进化:它是 LangChain 团队为了解决复杂 Agent 流程控制而推出的新核心库,代表了从“链式(Chain)”到“图式(Graph)”的思维转变。

基础概念

  1. 模型(Models):LLM的“大脑”
    模型是LangChain的核心动力,负责理解文本、生成内容。LangChain支持几乎所有主流LLM(比如OpenAI、GPT-4、LLaMA、Claude等),并统一了调用接口。

两种核心模型类型:

LLM:输入文本,输出文本(比如GPT-3.5的text-davinci-003)。

ChatModel:输入“消息列表”(比如用户消息、系统提示),输出“消息”(更适合对话场景,比如GPT-4的gpt-4)。

作用:负责核心的“思考”和“生成”工作,是整个应用的“智能来源”。

  1. 提示(Prompts):给模型的“指令”
    提示词是告诉模型“该做什么”的指令。LangChain的PromptTemplate能帮你动态生成提示词,避免手动拼接字符串的麻烦。

示例:
如果你想让模型生成“给产品起名字”,可以定义一个模板:
PromptTemplate(input_variables=[“product”], template=”给一个{product}起3个有科技感的名字”)
当传入product=”智能手表”时,自动生成提示词:“给一个智能手表起3个有科技感的名字”。

作用:精准控制模型的输出,让模型按你的需求工作(比如摘要、翻译、问答等)。

  1. 链(Chains):组件的“连接器”
    单个模型+提示只能完成简单任务(比如生成一句话),但复杂任务需要多步操作(比如先翻译再总结)。链就是把多个组件(模型、提示、其他链)按顺序组合起来的工具。

常见链类型:

LLMChain:最基础的链,由“提示模板”+“模型”组成,直接输出结果。

SequentialChain:按顺序执行多个链(比如链1的输出作为链2的输入)。

RouterChain:根据输入内容“选择”不同的链执行(比如判断问题类型,再调用对应链)。

示例:用SequentialChain实现“先翻译英文到中文,再总结中文内容”:
链1(翻译)→ 链2(总结),输入英文,输出总结后的中文。

  1. 记忆(Memory):让模型“记住”历史
    默认情况下,LLM是“健忘的”——每次调用都是独立的,不知道之前的对话。记忆组件的作用是保存对话历史,并把历史内容“喂”给模型,让对话有连贯性。

常见记忆类型:

ConversationBufferMemory:简单保存所有对话历史(适合短对话)。

ConversationSummaryMemory:自动总结对话历史(适合长对话,避免内容过长)。

ConversationTokenBufferMemory:按“Token数量”保存历史(防止超过模型输入限制)。

示例:聊天机器人中,用ConversationBufferMemory保存“用户问了什么”和“AI答了什么”,每次新对话时,把历史内容和新问题一起传给模型,模型就知道上下文了。

  1. 工具(Tools):模型的“外挂能力”
    LLM的知识截止到训练数据(比如GPT-4截止到2023年10月),也不会实时计算、查天气。工具是模型可以调用的外部能力,帮模型弥补这些缺陷。

常见工具类型:

搜索引擎(比如Google Search,查实时信息);

计算器(比如llm-math,做精确计算);

数据库(比如SQL工具,查数据库数据);

自定义函数(比如你自己写的“查天气”API)。

作用:让模型从“只能凭记忆回答”变成“能主动调用工具获取信息”。

  1. 代理(Agents):工具的“决策者”
    有了工具,谁来决定“什么时候用什么工具”?——代理。

代理是一个“决策者”:它会分析用户的问题,判断是否需要调用工具(如果需要,调用哪个工具),并根据工具返回的结果生成最终答案。

常见代理类型:

Zero-shot React Description:根据工具的“描述”直接决定调用(适合简单场景);

Conversational React Description:带记忆的代理(适合对话场景,能结合历史决定调用);

Structured Input:处理结构化输入(比如表格数据)的代理。

示例:用户问“今天北京的气温是多少?”
代理分析:“这个问题需要实时数据,模型不知道,所以调用天气工具”→ 调用工具获取气温→ 整理结果回答用户。

UV的使用以及与pip的区别

uv 与 pip 的核心区别

# 1. 创建虚拟环境(如果尚未创建)

uv venv

# 2. 激活虚拟环境

source .venv/bin/activate

# 3. 安装项目依赖(使用 uv 替代 pip,速度飞快)

uv pip install -r requirements.txt(我这里是uv与requirements.txt混用)

可以看到安装时报错了原因是在 macOS 上安装 mysqlclient 失败,因为系统缺少 MySQL 的 C 开发库(header files)构建工具无法找到它们

执行brew install mysql-client pkg-config

# 设置 pkg-config 路径以便找到 mysqlclient

export PKG_CONFIG_PATH=”$(brew –prefix mysql-client)/lib/pkgconfig”

而后重新执行uv pip install -r requirements.txt

安装完成

# 4. 同步开发工具(如 pre-commit,这是目前 uv.lock 管理的内容)

uv sync