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)` 可点击链接
+
+**问题二:子文档作者显示"小研"**
+- 根因:现有子文档由 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)` 可点击链接
+
+**问题二:子文档作者显示"小研"**
+- 根因:现有子文档由 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)` 可点击链接
+
+**问题二:子文档作者显示"小研"**
+- 根因:现有子文档由 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"")
- 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()