Chandra实战教程:为Chandra添加Webhook通知能力,对接飞书/钉钉/企业微信消息推送
1. 为什么需要给Chandra加Webhook通知
你有没有遇到过这样的场景:团队正在用Chandra做内部AI知识问答,但每次模型出错、服务重启或用户提交了重要咨询请求时,没人能第一时间知道?或者你想把用户在Chandra里提出的高频问题自动同步到飞书群,让运营同事快速响应?又或者,希望当有人连续三次提问失败时,系统自动发一条告警消息到钉钉运维群?
这些需求,原生Chandra并不支持——它专注做好一件事:提供一个轻量、私有、流畅的本地聊天界面。但它本身不带通知能力,也不连接外部消息通道。
好消息是:Chandra基于Ollama运行,所有对话请求都通过标准API(/api/chat)流转,而Ollama的调用链路完全可控。这意味着,我们不需要修改Chandra前端,也不用动Ollama核心,只需在请求入口层加一层轻量级代理,就能实现对飞书、钉钉、企业微信等主流IM平台的Webhook全兼容通知。
这不是“魔改”,而是典型的“能力外挂”:保持原有系统纯净,用最小侵入方式扩展价值。本教程将手把手带你完成三件事:
- 搭建一个可拦截并转发Chandra请求的中间服务
- 配置飞书/钉钉/企业微信的Webhook地址与消息模板
- 实现「用户提问触发→记录日志→条件判断→推送消息」的完整闭环
整个过程无需Python高级知识,不依赖Docker Compose编排,所有代码可直接复制运行,5分钟内即可看到第一条飞书通知弹出。
2. 理解Chandra的通信结构与拦截点
2.1 Chandra如何与Ollama协作
Chandra本质是一个静态Web应用(HTML+JS),它不处理模型推理,只负责把用户输入打包成JSON,发给后端代理接口。这个代理接口默认指向Ollama的/api/chat,路径通常是:
POST http://localhost:11434/api/chat而Chandra前端实际调用的是镜像内置的反向代理服务(比如Nginx或Caddy),该服务把/api/chat请求转发给本地Ollama。关键在于:这个代理层是可替换、可增强的。
我们不碰Ollama,也不改Chandra源码,而是把原代理服务替换成一个“智能中继”——它既能原样转发请求给Ollama,又能在转发前后执行自定义逻辑,比如记录日志、提取关键词、触发Webhook。
2.2 为什么选HTTP中间件而非修改前端
你可能会想:直接在Chandra的JS里加个fetch()调用飞书Webhook不就行了?
不行。原因有三:
- 跨域限制:浏览器禁止前端JS直连飞书/钉钉Webhook(它们要求
Content-Type: application/json且无CORS头),会报CORS policy blocked错误; - 密钥暴露风险:Webhook地址含
secret或token,若写在前端JS里,任何人打开开发者工具都能看到,等于公开告警通道; - 不可靠性:用户可能关掉页面、网络中断、JS加载失败,导致通知永远发不出。
真正可靠的方式,是把通知逻辑放在服务端——也就是和Ollama同机部署的代理层。这里既可访问本地Ollama,又能安全持有Webhook密钥,还能捕获每一次成功/失败的请求。
2.3 我们的方案架构图
用户浏览器 ↓ (HTTPS) Chandra前端(/chat.html) ↓ (AJAX POST /api/chat) [智能代理服务] ←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←......# Chandra实战教程:为Chandra添加Webhook通知能力,对接飞书/钉钉/企业微信消息推送 ## 1. 为什么需要给Chandra加Webhook通知 你有没有遇到过这样的场景:团队正在用Chandra做内部AI知识问答,但每次模型出错、服务重启或用户提交了重要咨询请求时,没人能第一时间知道?或者你想把用户在Chandra里提出的高频问题自动同步到飞书群,让运营同事快速响应?又或者,希望当有人连续三次提问失败时,系统自动发一条告警消息到钉钉运维群? 这些需求,原生Chandra并不支持——它专注做好一件事:提供一个轻量、私有、流畅的本地聊天界面。但它本身不带通知能力,也不连接外部消息通道。 好消息是:Chandra基于Ollama运行,所有对话请求都通过标准API(`/api/chat`)流转,而Ollama的调用链路完全可控。这意味着,我们不需要修改Chandra前端,也不用动Ollama核心,只需在**请求入口层**加一层轻量级代理,就能实现对飞书、钉钉、企业微信等主流IM平台的Webhook全兼容通知。 这不是“魔改”,而是典型的“能力外挂”:保持原有系统纯净,用最小侵入方式扩展价值。本教程将手把手带你完成三件事: - 搭建一个可拦截并转发Chandra请求的中间服务 - 配置飞书/钉钉/企业微信的Webhook地址与消息模板 - 实现「用户提问触发→记录日志→条件判断→推送消息」的完整闭环 整个过程无需Python高级知识,不依赖Docker Compose编排,所有代码可直接复制运行,5分钟内即可看到第一条飞书通知弹出。 ## 2. 理解Chandra的通信结构与拦截点 ### 2.1 Chandra如何与Ollama协作 Chandra本质是一个静态Web应用(HTML+JS),它不处理模型推理,只负责把用户输入打包成JSON,发给后端代理接口。这个代理接口默认指向Ollama的`/api/chat`,路径通常是:POST http://localhost:11434/api/chat
而Chandra前端实际调用的是镜像内置的反向代理服务(比如Nginx或Caddy),该服务把`/api/chat`请求转发给本地Ollama。关键在于:**这个代理层是可替换、可增强的**。 我们不碰Ollama,也不改Chandra源码,而是把原代理服务替换成一个“智能中继”——它既能原样转发请求给Ollama,又能在转发前后执行自定义逻辑,比如记录日志、提取关键词、触发Webhook。 ### 2.2 为什么选HTTP中间件而非修改前端 你可能会想:直接在Chandra的JS里加个`fetch()`调用飞书Webhook不就行了? 不行。原因有三: - **跨域限制**:浏览器禁止前端JS直连飞书/钉钉Webhook(它们要求`Content-Type: application/json`且无CORS头),会报`CORS policy blocked`错误; - **密钥暴露风险**:Webhook地址含`secret`或`token`,若写在前端JS里,任何人打开开发者工具都能看到,等于公开告警通道; - **不可靠性**:用户可能关掉页面、网络中断、JS加载失败,导致通知永远发不出。 真正可靠的方式,是把通知逻辑放在服务端——也就是和Ollama同机部署的代理层。这里既可访问本地Ollama,又能安全持有Webhook密钥,还能捕获每一次成功/失败的请求。 ### 2.3 我们的方案架构图用户浏览器 ↓ (HTTPS) Chandra前端(/chat.html) ↓ (AJAX POST /api/chat) [智能代理服务] ←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←...... ├─→ 记录请求时间、用户IP、提问内容、模型名(gemma:2b) ├─→ 判断是否需通知(如含“紧急”、“报错”、“help”等关键词) ├─→ 调用飞书/钉钉/企微Webhook API └─→ 原样转发请求给 http://localhost:11434/api/chat ↓ Ollama(本地推理) ↓ 智能代理服务 ←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←............ ↓ (返回响应) Chandra前端(渲染AI回复)
这个架构干净、安全、可维护。接下来,我们用一个不到100行的Python脚本实现它。 ## 3. 搭建智能代理服务:5分钟完成部署 ### 3.1 准备工作:确认运行环境 Chandra镜像基于Linux容器,已预装Python 3.9+和pip。你无需额外安装Python——只需进入容器执行命令即可。 打开终端,进入Chandra容器(若使用CSDN星图平台,点击镜像右侧“终端”按钮): ```bash # 进入容器后,先确认Python版本 python3 --version # 应输出类似:Python 3.9.18 # 安装必要依赖(requests用于调用Webhook,flask作为轻量Web服务器) pip install flask requests注意:所有操作都在容器内进行,不影响宿主机。如提示
Permission denied,请在命令前加sudo,或联系平台管理员确认权限。
3.2 创建代理服务脚本
新建文件webhook-proxy.py:
# webhook-proxy.py from flask import Flask, request, jsonify, Response import requests import json import time import logging # 配置日志(输出到控制台,方便调试) logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) app = Flask(__name__) # ====== 请在此处配置你的Webhook地址 ====== # 飞书Webhook示例(替换为你自己的地址) FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxx" # 钉钉Webhook示例(替换为你自己的地址,含access_token) DINGTALK_WEBHOOK = "https://oapi.dingtalk.com/robot/send?access_token=yyyyyy" # 企业微信Webhook示例(替换为你自己的key) WEWORK_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=zzzzzz" # Ollama服务地址(默认不变) OLLAMA_URL = "http://localhost:11434/api/chat" # ====== 通知触发条件:关键词匹配 ====== ALERT_KEYWORDS = ["紧急", "报错", "error", "help", "崩溃", "无法响应", "timeout"] def send_feishu_message(content): """发送消息到飞书群""" payload = { "msg_type": "text", "content": {"text": content} } try: resp = requests.post(FEISHU_WEBHOOK, json=payload, timeout=5) if resp.status_code == 200: logger.info(" 飞书通知发送成功") else: logger.warning(f" 飞书通知失败,状态码:{resp.status_code}") except Exception as e: logger.error(f" 飞书通知异常:{e}") def send_dingtalk_message(content): """发送消息到钉钉群""" payload = { "msgtype": "text", "text": {"content": content} } try: resp = requests.post(DINGTALK_WEBHOOK, json=payload, timeout=5) if resp.status_code == 200: logger.info(" 钉钉通知发送成功") else: logger.warning(f" 钉钉通知失败,状态码:{resp.status_code}") except Exception as e: logger.error(f" 钉钉通知异常:{e}") def send_wework_message(content): """发送消息到企业微信群""" payload = { "msgtype": "text", "text": {"content": content} } try: resp = requests.post(WEWORK_WEBHOOK, json=payload, timeout=5) if resp.status_code == 200: logger.info(" 企业微信通知发送成功") else: logger.warning(f" 企业微信通知失败,状态码:{resp.status_code}") except Exception as e: logger.error(f" 企业微信通知异常:{e}") @app.route('/api/chat', methods=['POST']) def proxy_chat(): # 1. 记录原始请求 start_time = time.time() try: req_data = request.get_json() user_message = req_data.get("messages", [{}])[-1].get("content", "未知内容") model_name = req_data.get("model", "unknown") logger.info(f"📩 收到提问:'{user_message}' | 模型:{model_name}") # 2. 判断是否触发通知(简单关键词匹配) should_alert = any(kw in user_message for kw in ALERT_KEYWORDS) if should_alert: alert_text = f"[Chandra告警] 用户提问触发关键词\n模型:{model_name}\n提问:{user_message}\n时间:{time.strftime('%H:%M:%S')}" # 同时发三端(可按需注释掉不需要的) send_feishu_message(alert_text) send_dingtalk_message(alert_text) send_wework_message(alert_text) except Exception as e: logger.error(f" 解析请求失败:{e}") # 3. 原样转发请求给Ollama try: ollama_resp = requests.post( OLLAMA_URL, json=req_data, stream=True, timeout=300 # Ollama推理可能较长,设为5分钟 ) # 4. 将Ollama响应流式返回给Chandra前端 def generate(): for chunk in ollama_resp.iter_content(chunk_size=1024): if chunk: yield chunk return Response(generate(), content_type=ollama_resp.headers.get('content-type')) except Exception as e: logger.error(f" 转发至Ollama失败:{e}") return jsonify({"error": "服务暂时不可用,请稍后重试"}), 502 if __name__ == '__main__': logger.info(" Webhook代理服务已启动,监听端口 5000") app.run(host='0.0.0.0', port=5000, debug=False)3.3 获取并配置Webhook地址
- 飞书:在飞书群 → 群设置 → 机器人 → 添加机器人 → 选择“自定义机器人” → 复制Webhook地址(形如
https://open.feishu.cn/...) - 钉钉:在钉钉群 → 群设置 → 智能助手 → 添加机器人 → 选择“自定义” → 复制Webhook地址(含
access_token=参数) - 企业微信:在管理后台 → 应用管理 → 自建应用 → 创建「群机器人」→ 复制Key
将三个地址分别填入脚本中对应变量(FEISHU_WEBHOOK、DINGTALK_WEBHOOK、WEWORK_WEBHOOK),保存文件。
3.4 启动代理服务
在容器内执行:
# 后台启动,不阻塞终端 nohup python3 webhook-proxy.py > proxy.log 2>&1 & # 查看是否启动成功 ps aux | grep webhook-proxy # 应看到类似:python3 webhook-proxy.py # 查看实时日志(按 Ctrl+C 退出) tail -f proxy.log此时,代理服务已在http://localhost:5000/api/chat监听。
4. 修改Chandra前端指向新代理
Chandra前端默认调用/api/chat,该路径由镜像内置的Nginx反向代理指向Ollama。我们需要把它重新指向我们的新代理。
进入容器,编辑Nginx配置:
# 编辑Nginx配置文件(路径因镜像而异,常见位置如下) nano /etc/nginx/conf.d/default.conf # 或 nano /usr/share/nginx/html/config.js找到类似以下的代理配置(通常在location /api/chat块中):
location /api/chat { proxy_pass http://localhost:11434/api/chat; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }将其改为:
location /api/chat { proxy_pass http://localhost:5000/api/chat; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 保持长连接,支持流式响应 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }保存后重启Nginx:
nginx -s reload验证:刷新Chandra页面,输入问题,观察
proxy.log是否有收到提问日志。若有,说明代理已生效。
5. 实战测试:从提问到收到飞书通知
现在我们来一次完整验证。
5.1 测试关键词触发
在Chandra聊天框中输入:
紧急!我的模型突然无法响应了,怎么办?按下回车。几秒后,你应该在飞书群中看到类似消息:
[Chandra告警] 用户提问触发关键词 模型:gemma:2b 提问:紧急!我的模型突然无法响应了,怎么办? 时间:14:22:07同时,终端日志会显示:
2024-06-15 14:22:07 - INFO - 📩 收到提问:'紧急!我的模型突然无法响应了,怎么办?' | 模型:gemma:2b 2024-06-15 14:22:08 - INFO - 飞书通知发送成功 2024-06-15 14:22:08 - INFO - 钉钉通知发送成功 2024-06-15 14:22:08 - INFO - 企业微信通知发送成功5.2 测试非关键词提问(不触发)
输入普通问题,例如:
你好,介绍一下你自己。日志中只会显示收到提问,但不会出现飞书通知发送成功,证明条件判断准确。
5.3 扩展建议:让通知更智能
当前是关键词匹配,你还可以轻松升级:
- 添加频率限制:同一IP 5分钟内只发1条,防刷屏
- 区分通知等级:
error发钉钉+飞书,help只发飞书 - 附带上下文:把用户历史对话片段也发过去,方便快速定位
- 对接数据库:把每次提问存入SQLite,供后续分析高频问题
这些都只需在proxy_chat()函数中增加几行代码,完全不改变架构。
6. 总结:一条轻量路径,解锁无限可能
我们没有修改一行Chandra前端代码,没有动Ollama核心,甚至没重启整个镜像——只是加了一个5000端口的代理服务,就让原本“安静”的本地AI聊天工具,拥有了主动触达团队的能力。
这背后体现的是一种务实的工程思维:不追求大而全的重构,而专注小而准的增强。Webhook不是炫技,它是让AI真正融入工作流的“神经末梢”。
你学到的不仅是三行Webhook调用,更是一种可复用的方法论:
- 找到系统通信的“咽喉点”(这里是
/api/chat) - 用轻量中间件做“流量镜像”(记录+转发+扩展)
- 把密钥、逻辑、策略全部收束在可信服务端
- 用最简代码,解决最痛的问题
下次当你想给任何本地AI工具加告警、审计、统计、审批能力时,这套模式依然适用。
现在,去你的飞书群里,看看那条刚刚弹出的告警消息吧——那是你亲手赋予Chandra的第一声“心跳”。
7. 常见问题与避坑指南
7.1 为什么改了Nginx配置后Chandra打不开?
- 检查
proxy_pass地址是否拼写错误(注意末尾斜杠:http://localhost:5000/api/chat不能写成.../chat/) - 查看Nginx错误日志:
tail -n 20 /var/log/nginx/error.log - 确认代理服务确实在运行:
ps aux | grep webhook-proxy
7.2 发送Webhook时提示400或403错误?
- 飞书/钉钉/企微Webhook地址是否复制完整?尤其注意飞书地址末尾是否有空格
- 钉钉地址必须含
access_token=,企微必须含key=,缺一不可 - 检查代理服务日志中的具体错误信息(如
Connection refused说明端口未监听)
7.3 想只对接其中一种IM,怎么关闭其他?
直接注释掉对应send_xxx_message()三行调用即可,例如只留飞书:
# send_dingtalk_message(alert_text) # ← 注释掉 # send_wework_message(alert_text) # ← 注释掉 send_feishu_message(alert_text) # ← 保留7.4 如何让通知包含更多上下文(比如用户IP、时间戳)?
在alert_text变量中直接拼接:
user_ip = request.headers.get('X-Real-IP', request.remote_addr) alert_text = f"[Chandra告警] {user_ip} 提问触发关键词\n模型:{model_name}\n提问:{user_message}\n时间:{time.strftime('%Y-%m-%d %H:%M:%S')}"获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。