Files
company-kb/tools/kb-bot/handle.py
yangqianqian 84f7950589 Initial commit: company-kb framework
- 完整的知识库框架(P0-P5 路线图)
- 12篇治理文档
- 工具脚本(kb-init.sh, kb-lint-fm.py, feishu-sync.py, kb-bot)
- 7个 Claude 命令
- 示例项目和测试项目(wanniu-l1)
- 契约文件(meta/kb-contract.yaml)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 18:24:39 +08:00

121 lines
3.8 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""handle.py —— 消费飞书群消息事件,调 /kb-ask 回答,回复到群。
轮询 events/ 目录subscribe.sh 落的事件文件):
过滤 @机器人 的消息 → 抽问题 → CC headless 跑 /kb-ask → lark-cli 回复。
用法:
python3 tools/kb-bot/handle.py # 轮询一轮已有事件后退出
python3 tools/kb-bot/handle.py --watch # 常驻轮询
铁律:只读知识库、答案带溯源、无据报 Gap、绝不编造由 /kb-ask 保证)。
凭据由 lark-cli 自管,不在本脚本。
"""
import sys
import os
import re
import json
import time
import subprocess
BOT_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(BOT_DIR, "..", ".."))
EVENTS_DIR = os.path.join(BOT_DIR, "events")
DONE_DIR = os.path.join(BOT_DIR, "events", ".done")
SEEN_FILE = os.path.join(BOT_DIR, ".seen-msg-ids")
os.environ["PATH"] = os.path.expanduser("~/.npm-global/bin") + ":" + os.environ.get("PATH", "")
def load_seen():
if os.path.isfile(SEEN_FILE):
return set(open(SEEN_FILE, encoding="utf-8").read().split())
return set()
def mark_seen(mid):
with open(SEEN_FILE, "a", encoding="utf-8") as f:
f.write(mid + "\n")
def extract(event):
"""从事件里取 (message_id, chat_id, 纯文本问题);非群 @ 消息返回 None。"""
# --compact 扁平结构;兼容原始嵌套结构
mid = event.get("message_id") or event.get("event", {}).get("message", {}).get("message_id")
chat_id = event.get("chat_id") or event.get("event", {}).get("message", {}).get("chat_id")
text = event.get("text") or ""
if not text:
content = event.get("content") or event.get("event", {}).get("message", {}).get("content", "")
if isinstance(content, str) and content:
try:
text = json.loads(content).get("text", "")
except json.JSONDecodeError:
text = content
if not (mid and chat_id and text):
return None
# 剥离 @_user_1 等 @ 标记
q = re.sub(r"@_?\w+", "", text).strip()
if not q:
return None
return mid, chat_id, q
def ask_kb(question):
"""调 CC headless 跑 /kb-ask返回答案文本。"""
try:
r = subprocess.run(
["claude", "-p", f"/kb-ask {question}"],
cwd=ROOT, capture_output=True, text=True, timeout=180,
)
return (r.stdout or "").strip() or "(知识库查询无输出,请稍后重试)"
except FileNotFoundError:
return "CC 未安装,无法查询)"
except subprocess.TimeoutExpired:
return "(查询超时,请缩小问题范围)"
def reply(chat_id, mid, answer):
subprocess.run(
["lark-cli", "im", "+messages-reply",
"--message-id", mid, "--markdown", answer, "--as", "bot"],
capture_output=True, text=True,
)
def process_once(seen):
if not os.path.isdir(EVENTS_DIR):
return
os.makedirs(DONE_DIR, exist_ok=True)
for fn in sorted(os.listdir(EVENTS_DIR)):
fp = os.path.join(EVENTS_DIR, fn)
if not fn.endswith(".json") or not os.path.isfile(fp):
continue
try:
event = json.load(open(fp, encoding="utf-8"))
except (json.JSONDecodeError, OSError):
continue
parsed = extract(event)
if parsed:
mid, chat_id, q = parsed
if mid not in seen:
answer = ask_kb(q)
reply(chat_id, mid, answer)
mark_seen(mid)
seen.add(mid)
print(f"[kb-bot] answered {mid}: {q[:40]}")
os.replace(fp, os.path.join(DONE_DIR, fn))
def main():
seen = load_seen()
watch = "--watch" in sys.argv[1:]
while True:
process_once(seen)
if not watch:
break
time.sleep(3)
if __name__ == "__main__":
main()