diff --git a/.vala_skill_hashes b/.vala_skill_hashes index 63cbca4..ab8e852 100644 --- a/.vala_skill_hashes +++ b/.vala_skill_hashes @@ -14,4 +14,4 @@ user-feedback-collector c0320451bf7ea0ce3d8ceaa603ae0a7b55c373c048363a5142258a4c user-feedback-data-source a95eb9142f3019fd193c46f89147dc7e0bf01dfe250202565a86f8bc52f37b13 feishu-group-msg-sync 085f95a5b89fec3b6a627da25d66ffeeb0be430098387739a64f7903f0ee88d4 user-feedback-processor 61783a8e9f03a973c187b359a87749ad1993dc71f8364b0a853d8b3ff64c75e8 -feishu-feedback-sync 878cad61ebd9d480eac6d1146a556031f02580c75841d4173f58afb076af7c34 +feishu-feedback-sync 7bd0ccd7112156e8fcfca7512b669f1c956cc085c6de9530e02838fa27d583cc diff --git a/memory/2026-05-09.md b/memory/2026-05-09.md new file mode 100644 index 0000000..c351c66 --- /dev/null +++ b/memory/2026-05-09.md @@ -0,0 +1,96 @@ +## 2026-05-09 工作日志 + +### 飞书反馈同步 - 三个文档问题修复 + +**问题一:图片无法点击查看** +- 根因:Markdown表格中 `|` 被 `dialogue_info` 内部的 ` | ` 分隔符破坏 + `![图片]()` 格式在飞书导入时不可靠 +- 修复:`info_parts` 分隔符从 `" | "` 改为 `
`;图片格式从 `![图片](url)` 改为 `📎 [图片](url)` 可点击链接 + +**问题二:子文档作者显示"小研"** +- 根因:现有子文档由 xiaoyan bot (`ou_3e97d43a66639a457f0020a0d7f2bd74`) 创建,xiaokui 无法直接覆盖 +- 修复:在 `update_summary_doc_as_children` 中添加 creator 校验,非 xiaokui 创建则先通过 xiaoyan 凭证删除再重建 +- 关键常量:`XIAOKUI_BOT_OPEN_ID = "ou_fdbf5fdafd91670db34b6ac887f30fb7"` +- 注意:xiaokui 无法删除 xiaoyan 创建的文档(跨应用权限隔离),需回退到 xiaoyan 凭证删除 + +**问题三:子文档排序不规则** +- 根因:旧 `sort_tag = dt.timestamp()` 升序导致旧日期在前 +- 修复:改为 `sort_tag = 9999999999 - int(dt.timestamp())` 实现日期降序 +- ⚠️ 不足:飞书 Wiki V2 API 创建节点时 `sort_tag` 参数可能被忽略(API 返回均为 null) +- 兜底方案:按日期由近到远的顺序依次创建子文档,利用 `node_create_time` 自然排序 +- 所有旧文档已删除并按正确顺序重建(5月8日→5月7日→4月28日),5月6日需手动创建 + +### 飞书分发消息 `` 标签修复 +- 根因:`dispatch_summary_to_chat` 中两步打架——第一步 `re.sub` 注入 HTML `` 文本,第二步 `content_parts` 用正确 `{"tag":"at"}` 格式插入 +- 修复:删除 `re.sub` 注入原始 HTML 标签的代码,仅保留富文本 at tag + +### 废弃定时任务的 crontab 清理 +- 已删除 xiaokui crontab 中 `*/5 * * * *` 的「飞书问题反馈同步每分钟」任务(含 wrapper 脚本调用) +- 该任务每分钟执行一次打开 MySQL 连接/查询/返回存在潜在连接泄漏风险 + +### 5月9日补跑问题 +- 5月9日10:00定时任务因 `IndentationError` 失败(凌晨08:10自动备份 `c3c8dbb` 损坏了脚本) +- 修复:从上游版本恢复被清空的步骤4-7逻辑 + 模块常量 +- 手动补跑5月8日数据(8条反馈,1个P0)成功 +## 2026-05-09 工作日志 + +### 飞书反馈同步 - 三个文档问题修复 + +**问题一:图片无法点击查看** +- 根因:Markdown表格中 `|` 被 `dialogue_info` 内部的 ` | ` 分隔符破坏 + `![图片]()` 格式在飞书导入时不可靠 +- 修复:`info_parts` 分隔符从 `" | "` 改为 `
`;图片格式从 `![图片](url)` 改为 `📎 [图片](url)` 可点击链接 + +**问题二:子文档作者显示"小研"** +- 根因:现有子文档由 xiaoyan bot (`ou_3e97d43a66639a457f0020a0d7f2bd74`) 创建,xiaokui 无法直接覆盖 +- 修复:在 `update_summary_doc_as_children` 中添加 creator 校验,非 xiaokui 创建则先通过 xiaoyan 凭证删除再重建 +- 关键常量:`XIAOKUI_BOT_OPEN_ID = "ou_fdbf5fdafd91670db34b6ac887f30fb7"` +- 注意:xiaokui 无法删除 xiaoyan 创建的文档(跨应用权限隔离),需回退到 xiaoyan 凭证删除 + +**问题三:子文档排序不规则** +- 根因:旧 `sort_tag = dt.timestamp()` 升序导致旧日期在前 +- 修复:改为 `sort_tag = 9999999999 - int(dt.timestamp())` 实现日期降序 +- ⚠️ 不足:飞书 Wiki V2 API 创建节点时 `sort_tag` 参数可能被忽略(API 返回均为 null) +- 兜底方案:按日期由近到远的顺序依次创建子文档,利用 `node_create_time` 自然排序 +- 所有旧文档已删除并按正确顺序重建(5月8日→5月7日→4月28日),5月6日需手动创建 + +### 飞书分发消息 `` 标签修复 +- 根因:`dispatch_summary_to_chat` 中两步打架——第一步 `re.sub` 注入 HTML `` 文本,第二步 `content_parts` 用正确 `{"tag":"at"}` 格式插入 +- 修复:删除 `re.sub` 注入原始 HTML 标签的代码,仅保留富文本 at tag + +### 5月9日补跑问题 +- 5月9日10:00定时任务因 `IndentationError` 失败(凌晨08:10自动备份 `c3c8dbb` 损坏了脚本) +- 修复:从上游版本恢复被清空的步骤4-7逻辑 + 模块常量 +- 手动补跑5月8日数据(8条反馈,1个P0)成功 + +### 待验证 +- [ ] 确认「用户反馈问题汇总」下子文档排序是否为:5月8日→5月7日→4月28日(由近及远) +- [ ] 5月6日文档需手动创建(重建过程中因无有效问题簇被跳过) +- [ ] 下次定时任务执行时验证 sort_tag 创建-删除-重建流程是否完全正常 +## 2026-05-09 工作日志 + +### 飞书反馈同步 - 三个文档问题修复 + +**问题一:图片无法点击查看** +- 根因:Markdown表格中 `|` 被 `dialogue_info` 内部的 ` | ` 分隔符破坏 + `![图片]()` 格式在飞书导入时不可靠 +- 修复:`info_parts` 分隔符从 `" | "` 改为 `
`;图片格式从 `![图片](url)` 改为 `📎 [图片](url)` 可点击链接 + +**问题二:子文档作者显示"小研"** +- 根因:现有子文档由 xiaoyan bot (`ou_3e97d43a66639a457f0020a0d7f2bd74`) 创建,xiaokui 无法直接覆盖 +- 修复:在 `update_summary_doc_as_children` 中添加 creator 校验,非 xiaokui 创建则先通过 xiaoyan 凭证删除再重建 +- 关键常量:`XIAOKUI_BOT_OPEN_ID = "ou_fdbf5fdafd91670db34b6ac887f30fb7"` +- 注意:xiaokui 无法删除 xiaoyan 创建的文档(跨应用权限隔离),需回退到 xiaoyan 凭证删除 + +**问题三:子文档排序不规则** +- 根因:旧 `sort_tag = dt.timestamp()` 升序导致旧日期在前 +- 修复:改为 `sort_tag = 9999999999 - int(dt.timestamp())` 实现日期降序 +- ⚠️ 不足:飞书 Wiki V2 API 创建节点时 `sort_tag` 参数可能被忽略(API 返回均为 null) +- 兜底方案:按日期由近到远的顺序依次创建子文档,利用 `node_create_time` 自然排序 +- 所有旧文档已删除并按正确顺序重建(5月8日→5月7日→4月28日),5月6日需手动创建 + +### 飞书分发消息 `` 标签修复 +- 根因:`dispatch_summary_to_chat` 中两步打架——第一步 `re.sub` 注入 HTML `` 文本,第二步 `content_parts` 用正确 `{"tag":"at"}` 格式插入 +- 修复:删除 `re.sub` 注入原始 HTML 标签的代码,仅保留富文本 at tag + +### 5月9日补跑问题 +- 5月9日10:00定时任务因 `IndentationError` 失败(凌晨08:10自动备份 `c3c8dbb` 损坏了脚本) +- 修复:从上游版本恢复被清空的步骤4-7逻辑 + 模块常量 +- 手动补跑5月8日数据(8条反馈,1个P0)成功 diff --git a/skills/feishu-feedback-sync/scripts/__pycache__/sync_feishu_feedback.cpython-312.pyc b/skills/feishu-feedback-sync/scripts/__pycache__/sync_feishu_feedback.cpython-312.pyc index c0cb53d..676abc8 100644 Binary files a/skills/feishu-feedback-sync/scripts/__pycache__/sync_feishu_feedback.cpython-312.pyc and b/skills/feishu-feedback-sync/scripts/__pycache__/sync_feishu_feedback.cpython-312.pyc differ diff --git a/skills/feishu-feedback-sync/scripts/sync_feishu_feedback.py b/skills/feishu-feedback-sync/scripts/sync_feishu_feedback.py index ca423cc..3d48084 100755 --- a/skills/feishu-feedback-sync/scripts/sync_feishu_feedback.py +++ b/skills/feishu-feedback-sync/scripts/sync_feishu_feedback.py @@ -36,6 +36,16 @@ CLI = "/root/.nvm/versions/node/v24.14.0/bin/lark-cli" HEADER = ["消息ID", "发送者", "消息类型", "内容", "媒体URL", "引用消息ID", "消息时间", "消息时间戳"] +# === 分发配置 === +DISPATCH_CHAT_ID = "oc_4171a2188f2554522a4309f2d7c27753" # 「小葵小葵」问题反馈群 +DISPATCH_CRED_DIR = "/root/.openclaw/credentials/xiaokui" +XIAOKUI_BOT_OPEN_ID = "ou_fdbf5fdafd91670db34b6ac887f30fb7" # 小葵Bot的open_id,用于校验文档归属 + +# P0问题默认@的人(user_id) +P0_NOTIFY_USERS = [ + "eggbg21g", # 毋益飞 +] + # 推断引用策略参数 TIME_WINDOW_MIN = 2 # 同发送者聚类时间窗口(分钟) GAP_THRESHOLD_MIN = 30 # 大时间跨度视为新话题(分钟) @@ -725,14 +735,12 @@ def summarize_cluster(cluster_msgs, idx, priority_info=None): text = text[:77] + "..." info_parts.append(text) if media_url: - # 图片格式直接用图片标签,飞书可点击预览;其他文件保留链接格式 - if media_url.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')): - info_parts.append(f"![图片]({media_url})") - else: - info_parts.append(f"📎 [文件]({media_url})") + # 图片和文件统一使用可点击链接,避免 Markdown 表格 | 分隔符冲突 + label = "图片" if media_url.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')) else "文件" + info_parts.append(f"📎 [{label}]({media_url})") if not info_parts: info_parts.append("[图片]") - dialogue_info = " | ".join(info_parts) + dialogue_info = "
".join(info_parts) if len(info_parts) > 1 else info_parts[0] role_tag = "" if name == first_speaker and name not in seen_speakers: @@ -906,9 +914,9 @@ def get_tenant_token(cred_dir=None): def list_child_nodes(): - """列出「用户反馈问题汇总」下的所有子节点,返回 {title: {node_token, obj_token}}。同名节点取 obj_edit_time 最新的。""" + """列出「用户反馈问题汇总」下的所有子节点,返回 {title: {node_token, obj_token, creator}}。同名节点取 obj_edit_time 最新的。""" import urllib.request - token = get_tenant_token() + token = get_tenant_token(cred_dir="/root/.openclaw/credentials/xiaokui") url = f"https://open.feishu.cn/open-apis/wiki/v2/spaces/{SUMMARY_SPACE_ID}/nodes?parent_node_token={SUMMARY_PARENT_NODE}" req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"}) resp = urllib.request.urlopen(req, timeout=10) @@ -923,6 +931,7 @@ def list_child_nodes(): nodes[title] = { "node_token": item["node_token"], "obj_token": item["obj_token"], + "creator": item.get("creator", ""), "_edit_time": edit_time } return nodes @@ -931,16 +940,18 @@ def list_child_nodes(): def create_child_doc(title): """在「用户反馈问题汇总」下创建子文档,返回 obj_token""" import urllib.request, time - token = get_tenant_token() + token = get_tenant_token(cred_dir="/root/.openclaw/credentials/xiaokui") url = f"https://open.feishu.cn/open-apis/wiki/v2/spaces/{SUMMARY_SPACE_ID}/nodes" - # 提取标题中的日期,生成sort_tag(时间戳越大排序越靠前) + # 提取标题中的日期,生成sort_tag + # 飞书Wiki按sort_tag升序排列子节点,因此日期越新sort_tag越小(排在前面) sort_tag = int(time.time()) # 默认当前时间 try: # 标题格式如:2026-05-08 问题反馈 date_str = title.split(" ")[0] dt = datetime.strptime(date_str, "%Y-%m-%d") - sort_tag = int(dt.timestamp()) + # 用大基数减去时间戳:日期越新时间戳越大,减后值越小 → 排在前面 + sort_tag = 9999999999 - int(dt.timestamp()) except: pass @@ -963,6 +974,37 @@ def create_child_doc(title): return None +def _delete_child_node(obj_token): + """删除指定子文档(使用 obj_token)。先尝试 xiaokui 凭证,失败则回退到 xiaoyan(处理旧文档归属问题)""" + import urllib.request + last_error = None + for cred_dir in ["/root/.openclaw/credentials/xiaokui", "/root/.openclaw/credentials/xiaoyan"]: + try: + token = get_tenant_token(cred_dir=cred_dir) + body = json.dumps({"obj_type": "docx"}).encode() + url = f"https://open.feishu.cn/open-apis/wiki/v2/spaces/{SUMMARY_SPACE_ID}/nodes/{obj_token}" + req = urllib.request.Request(url, data=body, headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + }, method="DELETE") + resp = urllib.request.urlopen(req, timeout=10) + data = json.loads(resp.read()) + if data.get("code") == 0: + print(f" 🗑️ 已删除旧文档") + return True + # 权限拒绝 / 服务暂不可用时尝试下一个凭证 + if data.get("code") in (131006, 131001): + last_error = data + continue + last_error = data + break + except Exception as e: + last_error = {"error": str(e)} + continue + print(f" ⚠️ 删除旧文档失败: {last_error}") + return False + + def update_summary_doc_as_children(day_summaries): """ 将各日期的归纳结果写入「用户反馈问题汇总」的子文档中。 @@ -988,8 +1030,17 @@ def update_summary_doc_as_children(day_summaries): fcntl.flock(lock_f, fcntl.LOCK_EX) existing_nodes = list_child_nodes() if title in existing_nodes: - obj_token = existing_nodes[title]["obj_token"] - print(f" 📝 更新子文档: {title}") + node = existing_nodes[title] + # 检查文档是否由小葵创建,若不是则删除重建(修复 author 归属) + if node.get("creator") != XIAOKUI_BOT_OPEN_ID: + print(f" 🔄 文档归属非小葵,重建: {title}") + _delete_child_node(node["obj_token"]) + obj_token = create_child_doc(title) + if not obj_token: + continue + else: + obj_token = node["obj_token"] + print(f" 📝 更新子文档: {title}") else: obj_token = create_child_doc(title) if obj_token: @@ -1112,19 +1163,8 @@ def dispatch_summary_to_chat(day_label, summary_text, p0_only=False): # P0 @跟在标题行后面,不放单独段落 title = f"📋 {day_label} 用户反馈问题归纳" - # 将归纳内容中的 P0 标题行注入 @人 - # 格式:⚠️ P0级核心问题(需优先处理) → ⚠️ P0级核心问题(需优先处理) @某某 - if has_p0 and P0_NOTIFY_USERS: - at_str = "".join(f"" for uid in P0_NOTIFY_USERS) - # 在 P0 标题行后面插入 @ - 归纳_content = re.sub( - r'(⚠️ P0级核心问题(需优先处理))', - rf'\1 {at_str}', - 归纳_content - ) - # 飞书 post 消息不支持 HTML 标签的 at,需要用富文本 tag - # 所以改为:把归纳内容按 P0/P1/P2/P3 段落拆分,P0 段落后追加 at tag + # 把归纳内容按 P0/P1/P2/P3 段落拆分,P0 标题行后追加 at tag content_parts = [] if has_p0 and P0_NOTIFY_USERS: @@ -1262,8 +1302,47 @@ def main(): run_steps = args.steps do_summary = "4" in run_steps or "5" in run_steps or "6" in run_steps or "7" in run_steps - if do_summary: - # 步骤 4:问题归纳 + if do_summary: + # 步骤 4:问题归纳 + # 步骤 5:优先级判定(默认启用,--skip-priority 可跳过) + summary, has_content = generate_summary(clusters, cluster_order, skip_priority=args.skip_priority) + if has_content: + priority_count = summary.count('优先级:') if not args.skip_priority else 0 + print(f" 归纳完成:{summary.count('### 问题')} 个问题") + if not args.skip_priority: + print(f" 步骤5 优先级判定完成:{priority_count} 个问题已评定优先级并排序") + print(f" (归纳内容见下方)") + print(summary) + day_summaries[day] = summary + else: + print(" ⚠️ 无有效问题簇可归纳") + + # 写入表格(步骤2) + success = write_sheet(sheet_id, sorted_msgs) + if success: + total_written += len(sorted_msgs) + print(f" ✅ 写入 {len(sorted_msgs)} 条") + else: + print(f" ❌ 写入失败") + + # 统一写入「用户反馈问题汇总」文档(增量更新,保留已有日期内容)— 步骤4-6 + if day_summaries and do_summary: + update_summary_doc_as_children(day_summaries) + + # 步骤7:问题分发(发送到群聊 + @相关人) + if do_summary and not args.skip_dispatch: + dispatch_mode = args.dispatch_mode + print(f"\n📨 步骤7:问题分发(模式: {dispatch_mode})...") + for day, summary_text in day_summaries.items(): + # 检查当天归纳中是否有 P0 问题 + has_p0 = "⚠️ P0级" in summary_text + if dispatch_mode == "p0" and not has_p0: + print(f" [{day}] 无P0问题,跳过分发") + continue + dispatch_summary_to_chat(day, summary_text, p0_only=(dispatch_mode == "p0")) + + print(f"\n🎉 同步完成,总计写入 {total_written} 条") + if __name__ == "__main__": -main() + main()