auto backup 2026-04-18 08:10:01
This commit is contained in:
parent
acdf0c712c
commit
d2a5f46295
@ -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
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
2026-04-17T06:00:01.887056+08:00
|
2026-04-17T12:00:01.691097+08:00
|
||||||
1
data/last_lark_group_sync_time
Normal file
1
data/last_lark_group_sync_time
Normal file
@ -0,0 +1 @@
|
|||||||
|
2026-04-18T08:00:01.304881+08:00
|
||||||
1
data/last_wechat_sync_id
Normal file
1
data/last_wechat_sync_id
Normal file
@ -0,0 +1 @@
|
|||||||
|
242
|
||||||
45
memory/2026-04-17-cron-check.md
Normal file
45
memory/2026-04-17-cron-check.md
Normal 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任务:
|
||||||
|
暂无创建的定时任务。
|
||||||
161
memory/2026-04-17-feishu-message-stats.md
Normal file
161
memory/2026-04-17-feishu-message-stats.md
Normal 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文件中,后续查询相关数据无需再提供配置信息。
|
||||||
60
memory/2026-04-17-request-timed-out-before-a-res.md
Normal file
60
memory/2026-04-17-request-timed-out-before-a-res.md
Normal 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
22
memory/2026-04-17.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# 2026-04-17 工作日志
|
||||||
|
|
||||||
|
## 飞书群消息同步改造(李若松要求)
|
||||||
|
|
||||||
|
### 变更内容
|
||||||
|
- **存储从飞书表格改为 MySQL**:新建 `vala_test.lark_group_message` 表,结构参考 `wechat_group_message`
|
||||||
|
- **同步频率**:从每6小时改为每4小时
|
||||||
|
- **数据范围**:2026.4.1 起的「内容测试问题反馈」群消息
|
||||||
|
- **数据库账户**:chatbot(test环境,仅对 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
4
memory/2026-04-18.md
Normal 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
10
scripts/run_lark_group_sync.sh
Executable 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
|
||||||
98
scripts/sync_feedback_minutely.sh
Executable file
98
scripts/sync_feedback_minutely.sh
Executable 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
|
||||||
355
scripts/sync_lark_group_to_mysql.py
Normal file
355
scripts/sync_lark_group_to_mysql.py
Normal 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()
|
||||||
219
scripts/sync_wechat_feedback_minutely.py
Normal file
219
scripts/sync_wechat_feedback_minutely.py
Normal 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()
|
||||||
145
scripts/sync_wechat_feedback_minutely.sh
Executable file
145
scripts/sync_wechat_feedback_minutely.sh
Executable 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
|
||||||
@ -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` 表 | 实时更新 |
|
||||||
|
|
||||||
## 目标存储位置
|
## 目标存储位置
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user