🔄 卡若AI 同步 2026-03-14 10:36 | 更新:水桥平台对接、水溪整理归档、火炬、运营中枢工作台 | 排除 >20MB: 11 个
This commit is contained in:
192
02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_all_soul_txt.py
Normal file
192
02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_all_soul_txt.py
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""下载妙记中所有 Soul 相关文字记录,从第1场(最早)开始。"""
|
||||
import re
|
||||
import sys
|
||||
import sqlite3
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
from datetime import datetime
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
TXT_DIR = Path("/Users/karuo/Documents/聊天记录/soul")
|
||||
COOKIE_PATH = Path.home() / "Library/Application Support/Cursor/Partitions/cursor-browser/Cookies"
|
||||
LIST_URL = "https://cunkebao.feishu.cn/minutes/api/space/list"
|
||||
EXPORT_URL = "https://cunkebao.feishu.cn/minutes/api/export"
|
||||
MAX_PAGES = 80
|
||||
PAGE_SIZE = 50
|
||||
|
||||
SOUL_KEYWORDS = ("soul", "派对", "团队会议")
|
||||
|
||||
|
||||
def get_cookie():
|
||||
if not COOKIE_PATH.exists():
|
||||
return "", ""
|
||||
tmp = tempfile.mktemp(suffix=".db")
|
||||
shutil.copy2(COOKIE_PATH, tmp)
|
||||
try:
|
||||
conn = sqlite3.connect(tmp)
|
||||
rows = conn.execute(
|
||||
"SELECT name, value FROM cookies WHERE (host_key LIKE '%feishu%' OR host_key LIKE '%cunkebao%') AND value != ''"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
finally:
|
||||
Path(tmp).unlink(missing_ok=True)
|
||||
cookie_str = "; ".join([f"{n}={v}" for n, v in rows])
|
||||
bv = ""
|
||||
for key in ("bv_csrf_token=", "minutes_csrf_token="):
|
||||
i = cookie_str.find(key)
|
||||
if i != -1:
|
||||
s = i + len(key)
|
||||
e = cookie_str.find(";", s)
|
||||
val = cookie_str[s : e if e != -1 else len(cookie_str)].strip()
|
||||
if len(val) == 36:
|
||||
bv = val
|
||||
break
|
||||
return cookie_str, bv
|
||||
|
||||
|
||||
def is_soul_related(topic):
|
||||
t = (topic or "").lower()
|
||||
return any(k in t for k in SOUL_KEYWORDS)
|
||||
|
||||
|
||||
def sanitize(topic):
|
||||
s = (topic or "").strip()
|
||||
for c in r'\/:*?"<>|':
|
||||
s = s.replace(c, "_")
|
||||
return re.sub(r"\s+", " ", s)[:90].strip()
|
||||
|
||||
|
||||
def fetch_list(headers):
|
||||
all_items = []
|
||||
last_ts = ""
|
||||
for page in range(1, MAX_PAGES + 1):
|
||||
url = f"{LIST_URL}?size={PAGE_SIZE}&space_name=1"
|
||||
if last_ts:
|
||||
url += f"&last_time={last_ts}"
|
||||
r = requests.get(url, headers=headers, timeout=30)
|
||||
if r.status_code != 200:
|
||||
break
|
||||
data = r.json()
|
||||
if data.get("code") != 0:
|
||||
break
|
||||
items = data.get("data", {}).get("list", [])
|
||||
if not items:
|
||||
break
|
||||
all_items.extend(items)
|
||||
last_ts = items[-1].get("create_time", "")
|
||||
if len(items) < PAGE_SIZE:
|
||||
break
|
||||
time.sleep(0.25)
|
||||
return all_items
|
||||
|
||||
|
||||
def export_txt(headers, object_token):
|
||||
params = {"object_token": object_token, "format": 2, "add_speaker": "true", "add_timestamp": "false"}
|
||||
r = requests.post(EXPORT_URL, params=params, headers=headers, timeout=30)
|
||||
r.encoding = "utf-8"
|
||||
if r.status_code == 200 and (r.text or "").strip():
|
||||
return (r.text or "").strip()
|
||||
return None
|
||||
|
||||
|
||||
def ts_to_date(ts):
|
||||
try:
|
||||
t = int(ts)
|
||||
if t > 1e12:
|
||||
t = t // 1000
|
||||
return datetime.fromtimestamp(t).strftime("%Y%m%d")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(description="下载妙记中所有 Soul 相关文字,从第1场开始")
|
||||
ap.add_argument("--dry-run", action="store_true", help="只列出待下载,不实际下载")
|
||||
ap.add_argument("--max-download", type=int, default=0, help="最多下载条数,0=全部")
|
||||
args = ap.parse_args()
|
||||
|
||||
TXT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
cookie_str, bv = get_cookie()
|
||||
if len(cookie_str) < 100:
|
||||
print("无法获取 Cookie(请用 Cursor 打开过飞书妙记)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
"Cookie": cookie_str,
|
||||
"Referer": "https://cunkebao.feishu.cn/minutes/",
|
||||
}
|
||||
if bv:
|
||||
headers["bv-csrf-token"] = bv
|
||||
|
||||
have_basenames = set()
|
||||
for f in TXT_DIR.iterdir():
|
||||
if f.is_file() and f.suffix.lower() == ".txt":
|
||||
have_basenames.add(f.name)
|
||||
|
||||
print("正在拉取妙记列表...", flush=True)
|
||||
all_items = fetch_list(headers)
|
||||
soul_items = [it for it in all_items if is_soul_related(it.get("topic", ""))]
|
||||
seen = {}
|
||||
for it in soul_items:
|
||||
tok = it.get("object_token", "")
|
||||
if tok and tok not in seen:
|
||||
seen[tok] = it
|
||||
soul_items = list(seen.values())
|
||||
soul_items.sort(key=lambda x: int(x.get("create_time", 0) or 0)) # 最早在前
|
||||
|
||||
print(f"Soul 相关共 {len(soul_items)} 条(已去重,按时间升序)", flush=True)
|
||||
|
||||
to_download = []
|
||||
for it in soul_items:
|
||||
topic = it.get("topic", "")
|
||||
token = it.get("object_token", "")
|
||||
ts = it.get("create_time", "")
|
||||
date_str = ts_to_date(ts)
|
||||
base = sanitize(topic) + ".txt"
|
||||
alt_base = (sanitize(topic) + "_" + date_str + ".txt") if date_str else base
|
||||
if base in have_basenames or alt_base in have_basenames:
|
||||
continue
|
||||
to_download.append({"topic": topic, "object_token": token, "date_str": date_str})
|
||||
|
||||
print(f"待下载(目录暂无): {len(to_download)} 条", flush=True)
|
||||
if not to_download:
|
||||
print("目录已齐全,无需下载。")
|
||||
return 0
|
||||
|
||||
if args.dry_run:
|
||||
for i, m in enumerate(to_download[:80]):
|
||||
print(f" {i+1}. {m['topic'][:60]}", flush=True)
|
||||
if len(to_download) > 80:
|
||||
print(f" ... 共 {len(to_download)} 条", flush=True)
|
||||
return 0
|
||||
|
||||
to_do = to_download[: args.max_download] if args.max_download else to_download
|
||||
ok = 0
|
||||
for i, m in enumerate(to_do):
|
||||
topic = m["topic"]
|
||||
token = m["object_token"]
|
||||
date_str = m["date_str"]
|
||||
base = sanitize(topic) + ".txt"
|
||||
body = export_txt(headers, token)
|
||||
if body and len(body) > 30:
|
||||
out_path = TXT_DIR / base
|
||||
out_path.write_text("标题: " + topic + "\n\n" + body, encoding="utf-8")
|
||||
have_basenames.add(base)
|
||||
ok += 1
|
||||
print(f" [{i+1}/{len(to_do)}] 已保存 {base[:55]}", flush=True)
|
||||
else:
|
||||
print(f" [{i+1}/{len(to_do)}] 无转写 {topic[:45]}", flush=True)
|
||||
time.sleep(0.4)
|
||||
|
||||
print(f"完成: 新写入 {ok} 个 txt", flush=True)
|
||||
return ok
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"access_token": "u-e1gGA5qAp2yXEyJOa2k9Zalh3KxxghONV0GaJwk0274U",
|
||||
"refresh_token": "ur-cuP.iNyXxflV6qpKn0c2Lnlh3AxxghqPpwGaIQ4022gJ",
|
||||
"access_token": "u-etChap5Od26rxUgaVXv42ulh16H1ghqNN0GaIB40265Z",
|
||||
"refresh_token": "ur-fZNnr2K6F3DHz69kjFmQOGlh3C91ghqVrwGaIMg023gI",
|
||||
"name": "飞书用户",
|
||||
"auth_time": "2026-03-12T22:44:05.347461"
|
||||
}
|
||||
@@ -55,11 +55,13 @@ ROWS = {
|
||||
'118': [ '3D打印 游戏代充 正财偏财', 190, 74873, 586, 10, 149, 46, 7456, 33, 65 ],
|
||||
# 119场 2026-03-08:关闭页 154min/446成员/64最高/26新增/20礼物/3315灵魂力/50692曝光,小助手 151min/441进房/11人均/110互动/23关注
|
||||
'119': [ '开派对初心 早上不影响老婆', 154, 50692, 446, 11, 110, 20, 3315, 26, 64 ],
|
||||
# 124场 2026-03-14:关闭页 95min/171成员/32最高/7新增/0礼物/0灵魂力,小助手 91min建房/169进房/8人均/48互动/6关注,妙记提炼主题
|
||||
'124': [ '房主号设备与手机业务线', 95, 0, 171, 8, 48, 0, 0, 7, 32 ],
|
||||
}
|
||||
# 场次→按日期列填写时的日期(表头为当月日期 1~31)
|
||||
SESSION_DATE_COLUMN = {'105': '20', '106': '21', '107': '23', '113': '2', '114': '3', '115': '4', '116': '5', '117': '6', '118': '7', '119': '8'}
|
||||
SESSION_DATE_COLUMN = {'105': '20', '106': '21', '107': '23', '113': '2', '114': '3', '115': '4', '116': '5', '117': '6', '118': '7', '119': '8', '124': '14'}
|
||||
# 场次→月份(用于选择 2月/3月 等工作表标签,避免写入错月)
|
||||
SESSION_MONTH = {'105': 2, '106': 2, '107': 2, '113': 3, '114': 3, '115': 3, '116': 3, '117': 3, '118': 3, '119': 3}
|
||||
SESSION_MONTH = {'105': 2, '106': 2, '107': 2, '113': 3, '114': 3, '115': 3, '116': 3, '117': 3, '118': 3, '119': 3, '124': 3}
|
||||
|
||||
# 派对录屏(飞书妙记)链接:场次 → 完整 URL,填表时写入「派对录屏」行对应列
|
||||
# 从飞书妙记复制链接后填入,新场次需补全
|
||||
@@ -71,6 +73,7 @@ PARTY_VIDEO_LINKS = {
|
||||
'117': 'https://cunkebao.feishu.cn/minutes/obcn9phnds9a96ma6t8ixa3z',
|
||||
'118': 'https://cunkebao.feishu.cn/minutes/obcnaee1h83l1s169e3a18qp',
|
||||
'119': 'https://cunkebao.feishu.cn/minutes/obcnbrc925796a6u4c667931',
|
||||
'124': 'https://cunkebao.feishu.cn/minutes/obcne7q5dto13494k9a56881',
|
||||
}
|
||||
|
||||
# 小程序当日运营数据:日期号 → {访问次数, 访客, 交易金额},填表时自动写入对应日期列
|
||||
@@ -90,6 +93,7 @@ MINIPROGRAM_EXTRA_3 = {
|
||||
'6': {'访问次数': 0, '访客': 0, '交易金额': 0}, # 3月6日 117场
|
||||
'7': {'访问次数': 0, '访客': 0, '交易金额': 0}, # 3月7日 118场
|
||||
'8': {'访问次数': 0, '访客': 0, '交易金额': 0}, # 3月8日 119场
|
||||
'14': {'访问次数': 0, '访客': 0, '交易金额': 0}, # 3月14日 124场
|
||||
}
|
||||
|
||||
|
||||
@@ -353,7 +357,7 @@ def main():
|
||||
session = (sys.argv[1] if len(sys.argv) > 1 else '104').strip()
|
||||
row = ROWS.get(session)
|
||||
if not row:
|
||||
print('❌ 未知场次,可用: 96, 97, 98, 99, 100, 103, 104, 105, 106, 107, 113, 114, 115, 116, 117, 118, 119')
|
||||
print('❌ 未知场次,可用: 96, 97, 98, 99, 100, 103, 104, 105, 106, 107, 113, 114, 115, 116, 117, 118, 119, 124')
|
||||
sys.exit(1)
|
||||
token = load_token() or refresh_and_load_token()
|
||||
if not token:
|
||||
@@ -398,9 +402,9 @@ def main():
|
||||
LABELS_GROUP = ['主题', '时长(分钟)', 'Soul推流人数', '进房人数', '人均时长(分钟)', '互动数量', '礼物', '灵魂力', '增加关注', '最高在线']
|
||||
|
||||
def _maybe_send_group(sess, raw_vals):
|
||||
if sess not in ('105', '106', '107', '113', '114', '115', '116', '117', '118', '119'):
|
||||
if sess not in ('105', '106', '107', '113', '114', '115', '116', '117', '118', '119', '124'):
|
||||
return
|
||||
date_label = {'105': '2月20日', '106': '2月21日', '107': '2月23日', '113': '3月2日', '114': '3月3日', '115': '3月4日', '116': '3月5日', '117': '3月6日', '118': '3月7日', '119': '3月8日'}.get(sess, sess + '场')
|
||||
date_label = {'105': '2月20日', '106': '2月21日', '107': '2月23日', '113': '3月2日', '114': '3月3日', '115': '3月4日', '116': '3月5日', '117': '3月6日', '118': '3月7日', '119': '3月8日', '124': '3月14日'}.get(sess, sess + '场')
|
||||
report_link = OPERATION_REPORT_LINK if sheet_id == SHEET_ID else f'https://cunkebao.feishu.cn/wiki/wikcnIgAGSNHo0t36idHJ668Gfd?sheet={sheet_id}'
|
||||
lines = [
|
||||
'【Soul 派对运营报表】',
|
||||
@@ -411,7 +415,7 @@ def main():
|
||||
for i, label in enumerate(LABELS_GROUP):
|
||||
val = raw_vals[i] if i < len(raw_vals) else ''
|
||||
lines.append(f'{label}:{val}')
|
||||
src_date = {'105': '20260220', '106': '20260221', '107': '20260223', '113': '20260302', '114': '20260303', '115': '20260304', '116': '20260305', '117': '20260306', '118': '20260307', '119': '20260308'}.get(sess, '20260220')
|
||||
src_date = {'105': '20260220', '106': '20260221', '107': '20260223', '113': '20260302', '114': '20260303', '115': '20260304', '116': '20260305', '117': '20260306', '118': '20260307', '119': '20260308', '124': '20260314'}.get(sess, '20260220')
|
||||
lines.append(f'数据来源:soul 派对 {sess}场 {src_date}.txt')
|
||||
msg = '\n'.join(lines)
|
||||
ok, _ = send_feishu_group_message(FEISHU_GROUP_WEBHOOK, msg)
|
||||
|
||||
58
02_卡人(水)/水溪_整理归档/经验库/待沉淀/卡若AI网站全站测试v2_20260314.md
Normal file
58
02_卡人(水)/水溪_整理归档/经验库/待沉淀/卡若AI网站全站测试v2_20260314.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 卡若AI 网站全站测试 v2 经验沉淀
|
||||
|
||||
> 日期:2026-03-14 | 执行者:卡若AI · 火炬(全栈开发)
|
||||
> 项目:卡若AI 官网(Next.js 14 + MongoDB)| 端口:3102
|
||||
> SKILL 版本:全栈测试 v2.0(本次同步升级)
|
||||
|
||||
---
|
||||
|
||||
## 一、测试范围
|
||||
|
||||
18 个页面(15 控制台 + 1 登录 + 2 营销)、66 个 API 端点、MongoDB 数据一致性、安全审计。
|
||||
|
||||
## 二、本次发现并修复的问题
|
||||
|
||||
### 数据库连接(P0)
|
||||
|
||||
| 问题 | 根因 | 修复 |
|
||||
|:-----|:-----|:-----|
|
||||
| MongoDB 容器无端口映射 | Docker 容器创建时未加 `-p 27017:27017` | 重建容器并映射端口 + `--restart unless-stopped` |
|
||||
| 数据库连接异常(better-sqlite3 报错) | MongoDB 不可达时 fallback 到 SQLite,但 SQLite 缺 native binding | 修复 MongoDB 连接后问题消失 |
|
||||
|
||||
### 错误处理补全(P1)
|
||||
|
||||
| 页面 | 缺失 | 修复 |
|
||||
|:-----|:-----|:-----|
|
||||
| docs | load/add/runAction 全无 try/catch | 已补全 |
|
||||
| users | load/updateRole 全无 try/catch | 已补全 |
|
||||
| platform | toggleApi/toggleLimit 无 try/catch | 已补全 |
|
||||
| scheduler | saveTask/toggle/runAll/runSelected/batchToggle 无 try/catch;clearHistory 无确认 | 已补全全部 + 确认弹窗 |
|
||||
| skills | addSkill/toggleSkill/deleteSkill/saveEdit/batchSetEnabled 无 try/catch | 已补全 |
|
||||
| workflows | addWorkflow/deleteWorkflow/toggleStatus/saveWorkflowEdit 无 try/catch | 已补全 |
|
||||
| gene-capsule | toggleEnabled/deleteCapsule/addCapsule 无 try/catch | 已补全 |
|
||||
| heartbeat | runAll/toggle/approveAction/save/triggerLearn/depositAll 无 try/catch | 已补全 |
|
||||
|
||||
## 三、SKILL v2.0 升级要点
|
||||
|
||||
将全栈测试 SKILL 从 v1.0 升级为 v2.0 通用版,核心变化:
|
||||
|
||||
1. **十步法替代五维度**:更系统的执行流程
|
||||
2. **逐按钮清单**:每个页面的每个按钮、链接、表单都有检查项
|
||||
3. **UI/UX 检查维度**:布局挤压、文案误导、解释性小问号、间距统一
|
||||
4. **安全审计清单**:11 项必查安全点
|
||||
5. **重复功能清理**:废弃功能检测
|
||||
6. **经验库持续沉淀**:两个项目的完整经验表
|
||||
|
||||
## 四、通用教训
|
||||
|
||||
1. **Docker 容器创建必须加 `-p` 端口映射**,且加 `--restart unless-stopped` 防止重启丢失
|
||||
2. **每个 fetch 调用都要 try/catch + res.ok**——这是全站最普遍的遗漏
|
||||
3. **MongoDB 重建后数据丢失**——需要 seed 接口快速恢复默认数据
|
||||
4. **clearHistory 等批量破坏性操作必须 confirm**
|
||||
|
||||
## 五、页面最终状态
|
||||
|
||||
全部 15 个控制台页面 + 登录页 HTTP 200 通过:
|
||||
- 技能 73 项 ✅ | 网关 7 项 ✅ | 任务 24 项 ✅
|
||||
- 调度 8 项 ✅ | 心跳 3 项 ✅ | 集成 2 项 ✅
|
||||
- 接口 7 项 ✅ | 限流 3 项 ✅ | 用户 1 项 ✅
|
||||
Reference in New Issue
Block a user