auto backup 2026-04-18 08:10:01

This commit is contained in:
--git_token 2026-04-18 08:10:01 +08:00
parent acdf0c712c
commit d2a5f46295
15 changed files with 1124 additions and 3 deletions

View File

@ -11,4 +11,4 @@ smart-auto-model-switch bfb3547dcd6029622c7062b49ae7922614a366b6dfe88c7d0fae9dcd
vala_git_workspace_backup.vala 4cf352bec88fe84af065ba1ffcbb06647b77df0e01860faaf0bca9fd64b968ec vala_git_workspace_backup.vala 4cf352bec88fe84af065ba1ffcbb06647b77df0e01860faaf0bca9fd64b968ec
feishu-group-msg-sync 338e821c4dd0b2f532b7f3ca31aea6d1881e16e0a76a6b7a0ecf2b723907690f feishu-group-msg-sync 338e821c4dd0b2f532b7f3ca31aea6d1881e16e0a76a6b7a0ecf2b723907690f
tencent-cos-upload 172517ed41d06c48425cd961ec5972a48495cfd62ec588bc1c2912ddf31b3a06 tencent-cos-upload 172517ed41d06c48425cd961ec5972a48495cfd62ec588bc1c2912ddf31b3a06
user-feedback-collector 7a2c9fc9b2b32f8dd0be52c1ac3cf6d33bc992c1ec200f061a26acaa7c637f77 user-feedback-collector 487eaa8446495d891c72d0f1171eec822b0877ab9dae4111eb477dc706e1ae71

View File

@ -1 +1 @@
2026-04-17T06:00:01.887056+08:00 2026-04-17T12:00:01.691097+08:00

View File

@ -0,0 +1 @@
2026-04-18T08:00:01.304881+08:00

1
data/last_wechat_sync_id Normal file
View File

@ -0,0 +1 @@
242

View File

@ -0,0 +1,45 @@
# Session: 2026-04-17 07:25:23 UTC
- **Session Key**: agent:xiaokui:feishu:direct:ou_088ee79216826be4a24af44f7268f880
- **Session ID**: cbba7f09-869f-45a9-baf8-ff35f64ec7b2
- **Source**: feishu
## Conversation Summary
assistant: ✅ New session started · model: volcengine/doubao-seed-2-0-pro-260215
user: System: [2026-04-15 19:11:21 GMT+8] Feishu[xiaokui] DM | 李若松 (ou_088ee79216826be4a24af44f7268f880) [msg:om_x100b52c36b4460bcb314283a8b23394]
A new session was started via /new or /reset. Run your Session Startup sequence - read the required files before responding to the user. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning.
Current time: Wednesday, April 15th, 2026 — 7:11 PM (Asia/Shanghai) / 2026-04-15 11:11 UTC
assistant: 若松好,我已准备就绪,有什么需求可以随时告诉我。
user: System: [2026-04-15 19:11:45 GMT+8] Feishu[xiaokui] DM | 李若松 (ou_088ee79216826be4a24af44f7268f880) [msg:om_x100b52c368abd0b0b4c78753a17ec00]
Conversation info (untrusted metadata):
```json
{
"message_id": "om_x100b52c368abd0b0b4c78753a17ec00",
"sender_id": "ou_088ee79216826be4a24af44f7268f880",
"sender": "李若松",
"timestamp": "Wed 2026-04-15 19:11 GMT+8"
}
```
Sender (untrusted metadata):
```json
{
"label": "李若松 (ou_088ee79216826be4a24af44f7268f880)",
"id": "ou_088ee79216826be4a24af44f7268f880",
"name": "李若松"
}
```
查看你的定时任务
assistant: 当前小葵的定时任务如下:
### 系统crontab任务
1. **VALA技能自动推送**每天凌晨2:00执行 `/root/.openclaw/workspace-xiaokui/scripts/vala_skill_auto_push.sh`,日志路径:`/root/.openclaw/workspace-xiaokui/logs/vala_skill_push.log`
2. **用户反馈群同步**每6小时执行一次 `/root/.openclaw/workspace-xiaokui/scripts/sync_feedback_group.py`,日志路径:`/var/log/feedback_sync.log`
3. **Git工作区备份**每天早上8:10执行 `/root/.openclaw/workspace-xiaokui/skills/vala_git_workspace_backup/scripts/backup.sh`,日志路径:`/root/.openclaw/workspace-xiaokui/logs/git_backup.log`
### openclaw cron任务
暂无创建的定时任务。

View File

@ -0,0 +1,161 @@
# Session: 2026-04-17 16:11:04 UTC
- **Session Key**: agent:xiaokui:feishu:direct:ou_088ee79216826be4a24af44f7268f880
- **Session ID**: 756e2dcf-7477-4385-a081-e7148a04e59a
- **Source**: feishu
## Conversation Summary
assistant: ✅ New session started · model: volcengine/doubao-seed-2-0-pro-260215
user: System: [2026-04-17 16:10:44 GMT+8] Feishu[xiaokui] DM | 李若松 (ou_088ee79216826be4a24af44f7268f880) [msg:om_x100b512afef0948cb2c3d0b78cebed9]
System: [2026-04-17 16:10:49 GMT+8] Feishu[xiaokui] DM | 李若松 (ou_088ee79216826be4a24af44f7268f880) [msg:om_x100b512afe48dcb0b215c867411ab87]
A new session was started via /new or /reset. Run your Session Startup sequence - read the required files before responding to the user. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning.
Current time: Friday, April 17th, 2026 — 4:11 PM (Asia/Shanghai) / 2026-04-17 08:11 UTC
assistant: 你好若松,我已就位,请问今天需要我处理什么工作?
user: System: [2026-04-17 16:11:33 GMT+8] Feishu[xiaokui] DM | 李若松 (ou_088ee79216826be4a24af44f7268f880) [msg:om_x100b512afd8e482cb282f22d314c588]
Conversation info (untrusted metadata):
```json
{
"message_id": "om_x100b512afd8e482cb282f22d314c588",
"sender_id": "ou_088ee79216826be4a24af44f7268f880",
"sender": "李若松",
"timestamp": "Fri 2026-04-17 16:11 GMT+8"
}
```
Sender (untrusted metadata):
```json
{
"label": "李若松 (ou_088ee79216826be4a24af44f7268f880)",
"id": "ou_088ee79216826be4a24af44f7268f880",
"name": "李若松"
}
```
查看一下 你的飞书消息数据库中 记录了多少条消息 最近一条是什么时候
assistant: 请提供一下MySQL数据库的连接信息主机地址、端口、用户名、密码我来查询飞书消息表的记录。
user: System: [2026-04-17 16:13:15 GMT+8] Feishu[xiaokui] DM | 李若松 (ou_088ee79216826be4a24af44f7268f880) [msg:om_x100b512af77d5090b2592e0af891675]
Conversation info (untrusted metadata):
```json
{
"message_id": "om_x100b512af77d5090b2592e0af891675",
"sender_id": "ou_088ee79216826be4a24af44f7268f880",
"sender": "李若松",
"timestamp": "Fri 2026-04-17 16:13 GMT+8"
}
```
Sender (untrusted metadata):
```json
{
"label": "李若松 (ou_088ee79216826be4a24af44f7268f880)",
"id": "ou_088ee79216826be4a24af44f7268f880",
"name": "李若松"
}
```
你查看下你的用户反馈 skill
assistant: 我已查看用户反馈收集skill的配置目前反馈数据的存储情况如下
1. **主动上报的用户反馈**实时存储在飞书知识库文档《2026年 用户反馈收集》Wiki Token: SB3dwaSshie7ifklKlLc2GswnqX
2. **飞书内部测试反馈群消息**每4小时同步到MySQL数据库 `vala_test.lark_group_message`
3. **微信用户反馈群消息**实时存储到MySQL数据库 `vala_test.wechat_group_message`
当前本地MySQL服务未启动无法查询数据库中的消息统计如需查询具体数据需要先启动本地MySQL服务或者提供远程数据库连接信息。
user: System: [2026-04-17 23:25:09 GMT+8] Feishu[xiaokui] DM | 李若松 (ou_088ee79216826be4a24af44f7268f880) [msg:om_x100b5111438994acb10ee4cd657ae45]
Conversation info (untrusted metadata):
```json
{
"message_id": "om_x100b5111438994acb10ee4cd657ae45",
"sender_id": "ou_088ee79216826be4a24af44f7268f880",
"sender": "李若松",
"timestamp": "Fri 2026-04-17 23:25 GMT+8"
}
```
Sender (untrusted metadata):
```json
{
"label": "李若松 (ou_088ee79216826be4a24af44f7268f880)",
"id": "ou_088ee79216826be4a24af44f7268f880",
"name": "李若松"
}
```
我给过你 mysql的用户名和密码啊 chatbot 账号
assistant: 当前我没有找到保存的MySQL chatbot账号凭证请你提供一下chatbot账号的密码我会将连接信息保存在secrets.env中后续查询就不需要再提供了。
user: System: [2026-04-17 23:34:02 GMT+8] Feishu[xiaokui] DM | 李若松 (ou_088ee79216826be4a24af44f7268f880) [msg:om_x100b5111606c28a4b3c6b5b9d603411]
Conversation info (untrusted metadata):
```json
{
"message_id": "om_x100b5111606c28a4b3c6b5b9d603411",
"sender_id": "ou_088ee79216826be4a24af44f7268f880",
"sender": "李若松",
"timestamp": "Fri 2026-04-17 23:34 GMT+8"
}
```
Sender (untrusted metadata):
```json
{
"label": "李若松 (ou_088ee79216826be4a24af44f7268f880)",
"id": "ou_088ee79216826be4a24af44f7268f880",
"name": "李若松"
}
```
user:chatbot
pass:xhuBx7d@uT2gUVv
assistant: 请提供一下MySQL数据库的主机地址IP或域名我来配置连接并查询数据。
user: System: [2026-04-18 00:06:17 GMT+8] Feishu[xiaokui] DM | 李若松 (ou_088ee79216826be4a24af44f7268f880) [msg:om_x100b5111e9445d30b4b65bcfa860db5]
Conversation info (untrusted metadata):
```json
{
"message_id": "om_x100b5111e9445d30b4b65bcfa860db5",
"sender_id": "ou_088ee79216826be4a24af44f7268f880",
"sender": "李若松",
"timestamp": "Sat 2026-04-18 00:06 GMT+8"
}
```
Sender (untrusted metadata):
```json
{
"label": "李若松 (ou_088ee79216826be4a24af44f7268f880)",
"id": "ou_088ee79216826be4a24af44f7268f880",
"name": "李若松"
}
```
MYSQL_HOST=bj-cdb-8frbdwju.sql.tencentcdb.com
MYSQL_PORT=25413
assistant: 飞书消息数据库目前共记录了 **175条** 群消息,最近一条消息的时间是 **2026-04-17 18:40:00**
user: System: [2026-04-18 00:09:06 GMT+8] Feishu[xiaokui] DM | 李若松 (ou_088ee79216826be4a24af44f7268f880) [msg:om_x100b5111fcd67ca0b4b587a67295205]
Conversation info (untrusted metadata):
```json
{
"message_id": "om_x100b5111fcd67ca0b4b587a67295205",
"sender_id": "ou_088ee79216826be4a24af44f7268f880",
"sender": "李若松",
"timestamp": "Sat 2026-04-18 00:09 GMT+8"
}
```
Sender (untrusted metadata):
```json
{
"label": "李若松 (ou_088ee79216826be4a24af44f7268f880)",
"id": "ou_088ee79216826be4a24af44f7268f880",
"name": "李若松"
}
```
好 记住以上 test mysql 环境 的 访问凭证
assistant: 已将test MySQL环境的访问凭证永久保存在本地secrets.env文件中后续查询相关数据无需再提供配置信息。

View File

@ -0,0 +1,60 @@
# Session: 2026-04-17 16:14:21 UTC
- **Session Key**: agent:xiaokui:feishu:direct:ou_088ee79216826be4a24af44f7268f880
- **Session ID**: a3993289-1785-4f14-b91f-768a86c6560a
- **Source**: feishu
## Conversation Summary
assistant: ✅ New session started · model: volcengine/doubao-seed-2-0-pro-260215
user: System: [2026-04-18 00:11:03 GMT+8] Feishu[xiaokui] DM | 李若松 (ou_088ee79216826be4a24af44f7268f880) [msg:om_x100b5111f7b9b938b32dce6b4ab6d27]
A new session was started via /new or /reset. Run your Session Startup sequence - read the required files before responding to the user. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning.
Current time: Saturday, April 18th, 2026 — 12:11 AM (Asia/Shanghai) / 2026-04-17 16:11 UTC
assistant: Hi 若松,新会话已启动,请问你需要我处理什么任务?
user: System: [2026-04-18 00:11:32 GMT+8] Feishu[xiaokui] DM | 李若松 (ou_088ee79216826be4a24af44f7268f880) [msg:om_x100b5111f5f2dc6cb2601197d6654cd]
Conversation info (untrusted metadata):
```json
{
"message_id": "om_x100b5111f5f2dc6cb2601197d6654cd",
"sender_id": "ou_088ee79216826be4a24af44f7268f880",
"sender": "李若松",
"timestamp": "Sat 2026-04-18 00:11 GMT+8"
}
```
Sender (untrusted metadata):
```json
{
"label": "李若松 (ou_088ee79216826be4a24af44f7268f880)",
"id": "ou_088ee79216826be4a24af44f7268f880",
"name": "李若松"
}
```
查看一下 你的飞书反馈消息数据库中 记录了多少条消息 最近一条是什么时候
assistant: 目前飞书反馈消息数据库中共记录了 **175条** 消息,最近一条的反馈时间为:**2026-04-17 18:40:00**(北京时间)。
user: System: [2026-04-18 00:13:00 GMT+8] Feishu[xiaokui] DM | 李若松 (ou_088ee79216826be4a24af44f7268f880) [msg:om_x100b51118e9d94a0b4b5831c86bfa9a]
Conversation info (untrusted metadata):
```json
{
"message_id": "om_x100b51118e9d94a0b4b5831c86bfa9a",
"sender_id": "ou_088ee79216826be4a24af44f7268f880",
"sender": "李若松",
"timestamp": "Sat 2026-04-18 00:12 GMT+8"
}
```
Sender (untrusted metadata):
```json
{
"label": "李若松 (ou_088ee79216826be4a24af44f7268f880)",
"id": "ou_088ee79216826be4a24af44f7268f880",
"name": "李若松"
}
```
飞书反馈消息数据库 默认指的就是 用户反馈技能中的 飞书 mysql 数据表
assistant: 好的,已记录该术语共识,后续查询将默认使用该数据表。

22
memory/2026-04-17.md Normal file
View File

@ -0,0 +1,22 @@
# 2026-04-17 工作日志
## 飞书群消息同步改造(李若松要求)
### 变更内容
- **存储从飞书表格改为 MySQL**:新建 `vala_test.lark_group_message` 表,结构参考 `wechat_group_message`
- **同步频率**从每6小时改为每4小时
- **数据范围**2026.4.1 起的「内容测试问题反馈」群消息
- **数据库账户**chatbottest环境仅对 lark_group_message 有写入权限)
### 完成事项
1. ✅ 创建 `lark_group_message` 表(唯一键 message_id 防重复)
2. ✅ 编写新同步脚本 `scripts/sync_lark_group_to_mysql.py`(基于原有 sync_group_to_sheet.py 改造)
3. ✅ 首次全量同步完成172 条记录2026-04-01 ~ 2026-04-17含文本134条、图片17条、视频10条、富文本9条、表情2条
4. ✅ crontab 定时任务已替换旧的每6小时飞书表格同步 → 新的每4小时MySQL同步
5. ✅ 更新 secrets.md 记录 chatbot 账户
6. ✅ 更新 user-feedback-collector SKILL.md 反馈数据源信息
### 文件变更
- 新增:`scripts/sync_lark_group_to_mysql.py`(核心同步脚本)
- 新增:`scripts/run_lark_group_sync.sh`crontab 入口脚本)
- 旧脚本保留:`scripts/sync_feedback_group.py`飞书表格版本已从crontab移除可后续清理

4
memory/2026-04-18.md Normal file
View File

@ -0,0 +1,4 @@
# 2026-04-18 工作日志
## 术语共识
[李若松确认] 术语「飞书反馈消息数据库」默认指代用户反馈收集技能中的飞书内部测试反馈MySQL数据表 `vala_test.lark_group_message`存储「内容测试问题反馈」群oc_fabff7672e62a9ced7b326ee4a286c26的同步消息数据。

10
scripts/run_lark_group_sync.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
# 飞书群消息同步到MySQL - 定时执行脚本
# crontab: 每4小时执行一次
# 0 */4 * * * /root/.openclaw/workspace-xiaokui/scripts/run_lark_group_sync.sh >> /var/log/lark_group_sync.log 2>&1
export PATH="/root/.nvm/versions/node/v24.14.0/bin:$PATH"
export LARK_SYNC_DB_PASS='xhuBx7d@uT2gUVv'
cd /root/.openclaw/workspace-xiaokui
python3 scripts/sync_lark_group_to_mysql.py

View File

@ -0,0 +1,98 @@
#!/bin/bash
# 任务名称:飞书用户反馈每分钟同步(时间倒序,全量覆盖)
# 执行时间:* * * * * 每分钟执行
# 归属 Agent小葵 xiaokui
set +e
# 环境配置
export PATH=/root/.nvm/versions/node/v24.14.0/bin:$PATH
LOG_FILE="/var/log/sync_feedback_minutely.log"
WORKSPACE="/root/.openclaw/workspace-xiaokui"
TMP_DATA="$WORKSPACE/tmp/all_feedback_sorted.json"
# 源表格配置
SOURCE_SPREADSHEET_TOKEN="E8vFsCmPBhT4SCtNmnJchqeJnJe"
SOURCE_SHEET_ID="7bce8f"
# 目标表格配置
TARGET_SPREADSHEET_TOKEN="AOxbsifk3hybRZteGowcMxNnnqc"
TARGET_SHEET_ID="f17380"
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
log "=== 同步任务开始 ==="
# 1. 读取源表格所有数据
log "读取源表格数据..."
SOURCE_RAW=$(LARKSUITE_CLI_CONFIG_DIR=/root/.openclaw/credentials/xiaoyan lark-cli sheets +read --spreadsheet-token "$SOURCE_SPREADSHEET_TOKEN" --range "$SOURCE_SHEET_ID!A:D" --as bot)
SOURCE_DATA=$(echo "$SOURCE_RAW" | jq -r '.data.valueRange.values | .[1:] | map(select(.[0] != null and .[0] != ""))')
if [ -z "$SOURCE_DATA" ] || [ "$SOURCE_DATA" == "null" ]; then
log "源表格无有效数据,退出"
exit 0
fi
# 2. 转换格式并按时间倒序排列
SORTED_DATA=$(echo "$SOURCE_DATA" | jq '
sort_by(.[0]) | reverse | to_entries | map(
.key as $idx |
.value as $row |
{
id: ($idx + 1),
time: $row[0],
submitter: $row[1],
type: $row[2],
content: ($row[3] | if type == "array" then .[0].link else . end)
} |
[
(.id | tostring),
"",
"",
"",
.time,
"飞书群",
.submitter,
"",
"",
(if .type == "文本" then .content else "" end),
(if .type == "图片" then .content else "" end),
(if .type | test("视频") then .content else "" end),
"",
"待确认",
""
]
)')
ROW_COUNT=$(echo "$SORTED_DATA" | jq length)
log "共获取到 $ROW_COUNT 条反馈记录,按时间倒序排列完成"
# 3. 清空目标表格原有数据(保留表头)
log "清空目标表格原有数据..."
EMPTY_DATA=$(printf '[]%.0s' {1..200} | jq -s '.')
LARKSUITE_CLI_CONFIG_DIR=/root/.openclaw/credentials/xiaoyan lark-cli sheets +write \
--spreadsheet-token "$TARGET_SPREADSHEET_TOKEN" \
--range "$TARGET_SHEET_ID!A2:O201" \
--values "$EMPTY_DATA" \
--as bot >/dev/null 2>&1
# 4. 写入排序后的数据
echo "$SORTED_DATA" > "$TMP_DATA"
END_ROW=$((ROW_COUNT + 1))
log "写入数据到目标表格..."
LARKSUITE_CLI_CONFIG_DIR=/root/.openclaw/credentials/xiaoyan lark-cli sheets +write \
--spreadsheet-token "$TARGET_SPREADSHEET_TOKEN" \
--range "$TARGET_SHEET_ID!A2:O$END_ROW" \
--values "$(cat $TMP_DATA)" \
--as bot
log "✅ 同步完成,共写入 $ROW_COUNT 条记录,最新反馈已排在最上方"
# 清理临时文件
rm -f "$TMP_DATA"
log "=== 同步任务完成 ==="
exit 0

View File

@ -0,0 +1,355 @@
#!/usr/bin/env python3
"""
飞书群聊消息同步到 MySQL (lark_group_message )
- Bot身份拉取群消息增量
- 非文本媒体上传到腾讯COS
- 记录写入 MySQL vala_test.lark_group_message
"""
import os
import sys
import json
import subprocess
import logging
import re
import pymysql
from datetime import datetime, timezone, timedelta
from pathlib import Path
# ============ 配置 ============
# 飞书群
CHAT_ID = "oc_fabff7672e62a9ced7b326ee4a286c26"
CHAT_NAME = "内容测试问题反馈"
# Bot凭证
LARK_CLI_CONFIG = "/root/.openclaw/credentials/xiaokui"
# 同步状态
LAST_SYNC_FILE = "/root/.openclaw/workspace-xiaokui/data/last_lark_group_sync_time"
WORK_DIR = "/tmp/lark_group_sync_workdir"
# COS
COS_BASE_PATH = "vala_llm/user_feedback"
# MySQL从环境变量或默认值
DB_HOST = os.environ.get("LARK_SYNC_DB_HOST", "bj-cdb-8frbdwju.sql.tencentcdb.com")
DB_PORT = int(os.environ.get("LARK_SYNC_DB_PORT", "25413"))
DB_USER = os.environ.get("LARK_SYNC_DB_USER", "chatbot")
DB_PASS = os.environ.get("LARK_SYNC_DB_PASS", "")
DB_NAME = os.environ.get("LARK_SYNC_DB_NAME", "vala_test")
# 时区
TZ = timezone(timedelta(hours=8))
# ============ 配置结束 ============
# 确保 lark-cli 在 PATH 中
_NODE_BIN = "/root/.nvm/versions/node/v24.14.0/bin"
if _NODE_BIN not in os.environ.get("PATH", ""):
os.environ["PATH"] = _NODE_BIN + ":" + os.environ.get("PATH", "")
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
log = logging.getLogger(__name__)
# 引入COS上传器
sys.path.insert(0, '/root/.openclaw/workspace-xiaokui/skills/tencent-cos-upload/scripts')
from cos_upload import CosUploader
def get_db_connection():
"""获取MySQL连接"""
return pymysql.connect(
host=DB_HOST,
port=DB_PORT,
user=DB_USER,
password=DB_PASS,
database=DB_NAME,
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor,
autocommit=False
)
def lark_cli(*args, cwd=None):
"""调用lark-cli并返回解析后的JSON"""
env = os.environ.copy()
env['LARKSUITE_CLI_CONFIG_DIR'] = LARK_CLI_CONFIG
cmd = ['lark-cli'] + list(args)
result = subprocess.run(cmd, capture_output=True, text=True, env=env, cwd=cwd or WORK_DIR)
if result.returncode != 0 and not result.stdout:
log.error(f"lark-cli error: {result.stderr}")
return None
try:
return json.loads(result.stdout)
except json.JSONDecodeError:
log.error(f"lark-cli JSON parse error: {result.stdout[:200]}")
return None
def fetch_messages(start_iso: str, end_iso: str) -> list:
"""拉取群消息,支持分页"""
all_messages = []
page_token = None
while True:
args = [
'im', '+chat-messages-list',
'--chat-id', CHAT_ID,
'--start', start_iso,
'--end', end_iso,
'--sort', 'asc',
'--page-size', '50',
'--as', 'bot'
]
if page_token:
args.extend(['--page-token', page_token])
result = lark_cli(*args)
if not result or not result.get('ok'):
err = result.get('error', {}).get('message', 'unknown') if result else 'no response'
log.error(f"拉取消息失败: {err}")
break
messages = result.get('data', {}).get('messages', [])
all_messages.extend(messages)
page_token = result.get('data', {}).get('page_token')
has_more = result.get('data', {}).get('has_more', False)
if not has_more or not page_token:
break
return all_messages
def download_resource(msg_id: str, file_key: str, res_type: str, filename: str) -> str:
"""下载飞书消息中的资源,返回本地路径"""
result = lark_cli(
'im', '+messages-resources-download',
'--message-id', msg_id,
'--file-key', file_key,
'--type', res_type,
'--output', filename,
'--as', 'bot',
cwd=WORK_DIR
)
local_path = os.path.join(WORK_DIR, filename)
if result and result.get('ok') and os.path.exists(local_path) and os.path.getsize(local_path) > 0:
return local_path
return None
def extract_file_key(msg_type: str, content: str) -> tuple:
"""从消息内容中提取file_key和资源类型"""
if msg_type == 'image':
match = re.search(r'img_[a-zA-Z0-9_-]+', content)
return (match.group(0), 'image') if match else (None, None)
elif msg_type in ('media', 'audio'):
match = re.search(r'file_[a-zA-Z0-9_-]+', content)
return (match.group(0), 'file') if match else (None, None)
elif msg_type == 'file':
try:
c = json.loads(content)
return (c.get('file_key'), 'file')
except Exception:
match = re.search(r'file_[a-zA-Z0-9_-]+', content)
return (match.group(0), 'file') if match else (None, None)
return None, None
def get_media_info(msg_type: str, content: str) -> tuple:
"""返回 (cos子目录, 文件扩展名, content_type)"""
if msg_type == 'image':
return 'image', '.png', 'image/png'
elif msg_type == 'media':
name_match = re.search(r'name="([^"]*)"', content)
ext = os.path.splitext(name_match.group(1))[1] if name_match else '.mp4'
return 'video', ext or '.mp4', 'video/mp4'
elif msg_type == 'audio':
return 'audio', '.ogg', 'audio/ogg'
elif msg_type == 'file':
try:
c = json.loads(content)
ext = os.path.splitext(c.get('file_name', ''))[1]
except Exception:
ext = ''
return 'file', ext or '.bin', 'application/octet-stream'
return 'other', '', 'application/octet-stream'
def process_message(msg: dict, cos_uploader, date_str: str) -> dict:
"""
处理单条消息返回字典用于插入数据库 None跳过
"""
msg_id = msg.get('message_id', '')
sender = msg.get('sender', {})
sender_id = sender.get('id', '')
sender_name = sender.get('name', '未知')
create_time_str = msg.get('create_time', '')
msg_type = msg.get('msg_type', '')
content_raw = msg.get('content', '')
deleted = msg.get('deleted', False)
if deleted or msg_type == 'system':
return None
# 解析消息时间
try:
# create_time 可能是毫秒时间戳字符串或ISO格式
if create_time_str.isdigit():
ts_ms = int(create_time_str)
msg_dt = datetime.fromtimestamp(ts_ms / 1000, tz=TZ)
else:
msg_dt = datetime.fromisoformat(create_time_str)
ts_ms = int(msg_dt.timestamp() * 1000)
except Exception:
msg_dt = datetime.now(TZ)
ts_ms = int(msg_dt.timestamp() * 1000)
record = {
'chat_id': CHAT_ID,
'chat_name': CHAT_NAME,
'sender_id': sender_id,
'sender_name': sender_name,
'message_id': msg_id,
'msg_type': msg_type,
'content': None,
'media_url': None,
'msg_time': msg_dt.strftime('%Y-%m-%d %H:%M:%S'),
'msg_timestamp': ts_ms,
}
# 文本消息
if msg_type in ('text', 'post'):
text = re.sub(r'<[^>]*>', '', content_raw).strip()
record['content'] = text
return record
# 表情包
if msg_type == 'sticker':
record['content'] = '(表情包)'
return record
# 媒体消息
cos_subdir, ext, content_type = get_media_info(msg_type, content_raw)
file_key, res_type = extract_file_key(msg_type, content_raw)
if not file_key:
record['content'] = f'无法提取资源key'
return record
short_id = msg_id[-12:]
if msg_type == 'media':
name_match = re.search(r'name="([^"]*)"', content_raw)
orig_ext = os.path.splitext(name_match.group(1))[1] if name_match else ext
filename = f'{short_id}{orig_ext or ext}'
elif msg_type == 'file':
try:
c = json.loads(content_raw)
orig_ext = os.path.splitext(c.get('file_name', ''))[1]
except Exception:
orig_ext = ext
filename = f'{short_id}{orig_ext or ext}'
else:
filename = f'{short_id}{ext}'
cos_key = f'{COS_BASE_PATH}/{cos_subdir}/{date_str}/{filename}'
# 下载
local_path = download_resource(msg_id, file_key, res_type, filename)
if not local_path:
log.warning(f"资源下载失败: {msg_id} ({msg_type})")
record['content'] = '(下载失败)'
return record
# 上传COS
try:
url = cos_uploader.upload(local_path, cos_key, content_type)
log.info(f"COS上传成功: {cos_key}")
record['media_url'] = url
except Exception as e:
log.error(f"COS上传失败: {cos_key} - {e}")
record['content'] = '(上传失败)'
finally:
try:
os.remove(local_path)
except Exception:
pass
return record
def insert_records(conn, records: list):
"""批量插入记录到MySQL使用 INSERT IGNORE 避免重复"""
if not records:
return 0
sql = """
INSERT IGNORE INTO lark_group_message
(chat_id, chat_name, sender_id, sender_name, message_id, msg_type, content, media_url, msg_time, msg_timestamp)
VALUES
(%(chat_id)s, %(chat_name)s, %(sender_id)s, %(sender_name)s, %(message_id)s,
%(msg_type)s, %(content)s, %(media_url)s, %(msg_time)s, %(msg_timestamp)s)
"""
inserted = 0
with conn.cursor() as cursor:
for record in records:
cursor.execute(sql, record)
inserted += cursor.rowcount
conn.commit()
return inserted
def main():
os.makedirs(WORK_DIR, exist_ok=True)
os.makedirs(os.path.dirname(LAST_SYNC_FILE), exist_ok=True)
# 读取上次同步时间
if os.path.exists(LAST_SYNC_FILE):
with open(LAST_SYNC_FILE) as f:
last_sync_iso = f.read().strip()
else:
# 首次运行:从 2026-04-01 开始
last_sync_iso = "2026-04-01T00:00:00+08:00"
current_iso = datetime.now(TZ).isoformat()
log.info(f"开始同步: {last_sync_iso} -> {current_iso}")
# 拉取消息
messages = fetch_messages(last_sync_iso, current_iso)
if not messages:
log.info("没有新消息,同步结束")
with open(LAST_SYNC_FILE, 'w') as f:
f.write(current_iso)
return
log.info(f"发现 {len(messages)} 条新消息")
# 初始化COS
cos_uploader = CosUploader()
date_str = datetime.now(TZ).strftime('%Y-%m-%d')
# 处理消息
records = []
for msg in messages:
record = process_message(msg, cos_uploader, date_str)
if record:
records.append(record)
# 写入MySQL
if records:
conn = get_db_connection()
try:
inserted = insert_records(conn, records)
log.info(f"同步完成: 处理 {len(records)} 条, 新增 {inserted} 条 (重复跳过 {len(records) - inserted} 条)")
finally:
conn.close()
else:
log.info("无有效消息需要写入")
# 更新同步时间
with open(LAST_SYNC_FILE, 'w') as f:
f.write(current_iso)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""
微信用户反馈每分钟同步到飞书收集表格
"""
import os
import json
import pymysql
import subprocess
from datetime import datetime
# 配置
LOG_FILE = "/var/log/sync_wechat_feedback.log"
LAST_SYNC_ID_FILE = "/root/.openclaw/workspace-xiaokui/data/last_wechat_sync_id"
TARGET_SPREADSHEET_TOKEN = "AOxbsifk3hybRZteGowcMxNnnqc"
TARGET_SHEET_ID = "f17380"
LARK_CLI_CONFIG = "/root/.openclaw/credentials/xiaoyan"
# 数据库配置
DB_CONFIG = {
"host": "bj-cdb-8frbdwju.sql.tencentcdb.com",
"port": 25413,
"user": "read_only",
"password": "fdsfiidier^$*hjfdijjd232",
"database": "vala_test",
"charset": "utf8mb4"
}
def log(message):
"""写日志"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{timestamp}] {message}\n")
print(f"[{timestamp}] {message}")
def get_last_sync_id():
"""获取上次同步的最大ID"""
if not os.path.exists(LAST_SYNC_ID_FILE):
return 0
with open(LAST_SYNC_ID_FILE, "r") as f:
return int(f.read().strip())
def save_last_sync_id(last_id):
"""保存上次同步的最大ID"""
with open(LAST_SYNC_ID_FILE, "w") as f:
f.write(str(last_id))
def fetch_new_wechat_data(last_id):
"""从MySQL读取新增的微信反馈数据"""
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor()
sql = """
SELECT id, msg_time, sender_name, msg_type, content
FROM wechat_group_message
WHERE id > %s
ORDER BY msg_time DESC
"""
cursor.execute(sql, (last_id,))
rows = cursor.fetchall()
cursor.close()
conn.close()
return rows
def convert_to_sheet_format(rows):
"""转换为飞书表格需要的格式"""
sheet_rows = []
max_id = 0
for row in rows:
id, msg_time, sender_name, msg_type, content = row
if id > max_id:
max_id = id
# 整理反馈内容
if msg_type == "text":
feedback_content = content
elif msg_type == "image":
feedback_content = f"[图片] {content}"
elif msg_type == "video":
feedback_content = f"[视频] {content}"
else:
feedback_content = f"[{msg_type}] {content}"
# 清理特殊字符
feedback_content = feedback_content.replace("\n", " ").replace("\r", "").replace('"', '\\"')
sheet_rows.append([
"", # A 序号后续统一生成
"微信-用户火线救火", # B 反馈渠道
msg_time.strftime("%Y-%m-%d %H:%M:%S"), # C 反馈时间
sender_name, # D 反馈人
feedback_content, # E 反馈内容
"", # F 备注 留空
"" # G 回复 留空
])
return sheet_rows, max_id
def get_existing_sheet_data():
"""读取目标表格现有数据"""
cmd = [
"lark-cli", "sheets", "+read",
"--spreadsheet-token", TARGET_SPREADSHEET_TOKEN,
"--range", f"{TARGET_SHEET_ID}!A2:O",
"--as", "bot"
]
env = os.environ.copy()
env["LARKSUITE_CLI_CONFIG_DIR"] = LARK_CLI_CONFIG
env["PATH"] = "/root/.nvm/versions/node/v24.14.0/bin:" + env.get("PATH", "")
result = subprocess.run(cmd, env=env, capture_output=True, text=True)
if result.returncode != 0:
log(f"读取表格失败: {result.stderr}")
return []
try:
data = json.loads(result.stdout)
values = data.get("data", {}).get("valueRange", {}).get("values", [])
# 过滤空行
valid_values = [row for row in values if row and len(row) >= 1 and row[0] and row[0].strip()]
return valid_values
except Exception as e:
log(f"解析表格数据失败: {str(e)}")
return []
def merge_and_sort_data(new_rows, existing_rows):
"""合并新老数据,按时间倒序排列,重新生成序号,去重"""
seen = set()
all_rows = []
# 合并新老数据
for row in new_rows + existing_rows:
if not row or len(row) < 5:
continue
# 去重:时间+反馈人+内容
key = f'{row[2]}_{row[3]}_{row[4]}'
if key in seen:
continue
seen.add(key)
all_rows.append(row)
# 按反馈时间倒序排列
all_rows.sort(key=lambda x: x[2], reverse=True)
# 重新生成序号
for i, row in enumerate(all_rows):
row[0] = str(i + 1)
return all_rows
def write_to_sheet(all_rows):
"""写入数据到飞书表格"""
if not all_rows:
log("无数据需要写入")
return
# 清空原有数据
empty_data = [["" for _ in range(15)] for _ in range(200)]
cmd_clear = [
"lark-cli", "sheets", "+write",
"--spreadsheet-token", TARGET_SPREADSHEET_TOKEN,
"--range", f"{TARGET_SHEET_ID}!A2:O201",
"--values", json.dumps(empty_data, ensure_ascii=False),
"--as", "bot"
]
env = os.environ.copy()
env["LARKSUITE_CLI_CONFIG_DIR"] = LARK_CLI_CONFIG
env["PATH"] = "/root/.nvm/versions/node/v24.14.0/bin:" + env.get("PATH", "")
subprocess.run(cmd_clear, env=env, capture_output=True)
# 写入新数据
end_row = len(all_rows) + 1
cmd_write = [
"lark-cli", "sheets", "+write",
"--spreadsheet-token", TARGET_SPREADSHEET_TOKEN,
"--range", f"{TARGET_SHEET_ID}!A2:O{end_row}",
"--values", json.dumps(all_rows, ensure_ascii=False),
"--as", "bot"
]
result = subprocess.run(cmd_write, env=env, capture_output=True, text=True)
if result.returncode != 0:
log(f"写入表格失败: {result.stderr}")
return False
return True
def main():
log("=== 微信反馈同步任务开始 ===")
last_sync_id = get_last_sync_id()
log(f"上次同步最大ID: {last_sync_id}")
# 获取新数据
new_rows = fetch_new_wechat_data(last_sync_id)
if not new_rows:
log("无新增微信反馈数据,退出")
return
new_count = len(new_rows)
log(f"发现 {new_count} 条新增微信反馈数据")
# 转换格式
sheet_rows, new_max_id = convert_to_sheet_format(new_rows)
# 获取现有数据
existing_rows = get_existing_sheet_data()
log(f"现有表格数据量: {len(existing_rows)}")
# 合并排序
all_rows = merge_and_sort_data(sheet_rows, existing_rows)
log(f"合并后总数据量: {len(all_rows)}")
# 写入表格
success = write_to_sheet(all_rows)
if success:
save_last_sync_id(new_max_id)
log(f"✅ 同步完成,新增 {new_count} 条,总 {len(all_rows)} 条,已按时间倒序排列")
else:
log("❌ 同步失败")
log("=== 同步任务完成 ===")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,145 @@
#!/bin/bash
# 任务名称:微信用户反馈每分钟同步到收集表格
# 执行时间:* * * * * 每分钟执行
# 归属 Agent小葵 xiaokui
set +e
# 环境配置
export PATH=/root/.nvm/versions/node/v24.14.0/bin:$PATH
LOG_FILE="/var/log/sync_wechat_feedback.log"
WORKSPACE="/root/.openclaw/workspace-xiaokui"
LAST_SYNC_ID_FILE="$WORKSPACE/data/last_wechat_sync_id"
TMP_DATA="$WORKSPACE/tmp/wechat_feedback.json"
# 数据库配置
DB_HOST="bj-cdb-8frbdwju.sql.tencentcdb.com"
DB_PORT="25413"
DB_USER="read_only"
DB_PASS="fdsfiidier^\$*hjfdijjd232"
DB_NAME="vala_test"
# 目标表格配置
TARGET_SPREADSHEET_TOKEN="AOxbsifk3hybRZteGowcMxNnnqc"
TARGET_SHEET_ID="f17380"
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# 初始化上次同步ID
if [ ! -f "$LAST_SYNC_ID_FILE" ]; then
echo "0" > "$LAST_SYNC_ID_FILE"
fi
LAST_SYNC_ID=$(cat "$LAST_SYNC_ID_FILE")
log "=== 微信反馈同步任务开始 ==="
log "上次同步最大ID$LAST_SYNC_ID"
# 1. 读取微信新增反馈数据
log "读取微信新增反馈数据..."
mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" -N -e "
SELECT id, msg_time, sender_name, msg_type, content
FROM wechat_group_message
WHERE id > $LAST_SYNC_ID
ORDER BY msg_time DESC;" > /tmp/wechat_raw.txt
if [ ! -s /tmp/wechat_raw.txt ]; then
log "无新增微信反馈数据,退出"
exit 0
fi
# 2. 转换为表格格式
log "转换数据格式..."
NEW_DATA=$(awk '
BEGIN {
print "["
first = 1
}
{
if (first == 0) print ","
first = 0
id = $1
time = $2 " " $3
sender = $4
type = $5
content = substr($0, index($0,$6))
# 处理字段
text = ""
image = ""
video = ""
if (type == "text") {
text = content
} else if (type == "image") {
image = content
} else if (type == "video") {
video = content
} else {
text = "[" type "] " content
}
gsub(/"/, "\\\"", text)
gsub(/"/, "\\\"", image)
gsub(/"/, "\\\"", video)
printf "[\"\", \"\", \"\", \"\", \"%s\", \"微信\", \"%s\", \"\", \"\", \"%s\", \"%s\", \"%s\", \"\", \"待确认\", \"\"]", time, sender, text, image, video
}
END {
print "\n]"
}' /tmp/wechat_raw.txt)
NEW_COUNT=$(echo "$NEW_DATA" | jq length)
NEW_MAX_ID=$(tail -1 /tmp/wechat_raw.txt | awk '{print $1}')
log "发现 $NEW_COUNT 条新增微信反馈数据最大ID$NEW_MAX_ID"
# 3. 读取目标表格现有数据(飞书+微信)
log "读取目标表格现有数据..."
TARGET_RAW=$(LARKSUITE_CLI_CONFIG_DIR=/root/.openclaw/credentials/xiaoyan lark-cli sheets +read --spreadsheet-token "$TARGET_SPREADSHEET_TOKEN" --range "$TARGET_SHEET_ID!A2:O" --as bot)
TARGET_DATA=$(echo "$TARGET_RAW" | jq '.data.valueRange.values | map(select(.[0] != null and .[0] != ""))')
if [ "$TARGET_DATA" == "null" ] || [ -z "$TARGET_DATA" ]; then
TARGET_DATA="[]"
fi
# 4. 合并数据并按时间倒序排列
echo "$NEW_DATA" > /tmp/new.json
echo "$TARGET_DATA" > /tmp/existing.json
ALL_DATA=$(jq -s 'add | sort_by(.[4]) | reverse' /tmp/new.json /tmp/existing.json)
# 5. 重新生成序号
ALL_DATA=$(echo "$ALL_DATA" | jq 'to_entries | map(.value[0] = (.key + 1 | tostring) | .value)')
echo "$ALL_DATA" > "$TMP_DATA"
ROW_COUNT=$(echo "$ALL_DATA" | jq length)
END_ROW=$((ROW_COUNT + 1))
# 6. 清空目标表格并写入合并后的数据
log "写入合并后的数据,共 $ROW_COUNT 条..."
# 先清空原有数据
EMPTY_DATA=$(printf '[]%.0s' {1..200} | jq -s '.')
LARKSUITE_CLI_CONFIG_DIR=/root/.openclaw/credentials/xiaoyan lark-cli sheets +write \
--spreadsheet-token "$TARGET_SPREADSHEET_TOKEN" \
--range "$TARGET_SHEET_ID!A2:O201" \
--values "$EMPTY_DATA" \
--as bot >/dev/null 2>&1
# 写入新数据
LARKSUITE_CLI_CONFIG_DIR=/root/.openclaw/credentials/xiaoyan lark-cli sheets +write \
--spreadsheet-token "$TARGET_SPREADSHEET_TOKEN" \
--range "$TARGET_SHEET_ID!A2:O$END_ROW" \
--values "$(cat $TMP_DATA)" \
--as bot
# 7. 更新上次同步ID
echo "$NEW_MAX_ID" > "$LAST_SYNC_ID_FILE"
log "✅ 同步完成:新增 $NEW_COUNT 条微信反馈,当前总记录数 $ROW_COUNT 条,已按时间倒序排列"
# 清理临时文件
rm -f /tmp/wechat_raw.txt /tmp/new.json /tmp/existing.json "$TMP_DATA"
log "=== 同步任务完成 ==="
exit 0

View File

@ -48,7 +48,7 @@ description: |
| 反馈渠道 | 来源 | 存储位置 | 更新频率 | | 反馈渠道 | 来源 | 存储位置 | 更新频率 |
|---------|------|----------|----------| |---------|------|----------|----------|
| 主动上报 | 用户主动发送 `用户反馈:` 前缀消息 | 飞书知识库文档 | 实时 | | 主动上报 | 用户主动发送 `用户反馈:` 前缀消息 | 飞书知识库文档 | 实时 |
| 飞书内部测试反馈 | 「内容测试问题反馈」群(`oc_fabff7672e62a9ced7b326ee4a286c26` | 飞书表格:`E8vFsCmPBhT4SCtNmnJchqeJnJe` | 每6小时自动同步 | | 飞书内部测试反馈 | 「内容测试问题反馈」群(`oc_fabff7672e62a9ced7b326ee4a286c26` | MySQL`vala_test.lark_group_message` 表 | 每4小时自动同步 |
| 微信用户反馈 | 微信用户反馈群 | MySQL`vala_test.wechat_group_message` 表 | 实时更新 | | 微信用户反馈 | 微信用户反馈群 | MySQL`vala_test.wechat_group_message` 表 | 实时更新 |
## 目标存储位置 ## 目标存储位置