diff --git a/.gitignore b/.gitignore index cbbee17f..6d37e020 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ __pycache__/ .env.* *.log sync_tokens.env +# 飞书妙记(用户 token / Cookie,勿提交) +**/智能纪要/脚本/feishu_user_token.txt +**/智能纪要/脚本/cookie_minutes.txt # Node / 前端 node_modules/ diff --git a/01_卡资(金)/金仓_存储备份/服务器管理/scripts/tencent_cloud_bill_recent_days.py b/01_卡资(金)/金仓_存储备份/服务器管理/scripts/tencent_cloud_bill_recent_days.py new file mode 100644 index 00000000..69e6f430 --- /dev/null +++ b/01_卡资(金)/金仓_存储备份/服务器管理/scripts/tencent_cloud_bill_recent_days.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +查询腾讯云指定日期范围的消费情况。 +依赖:pip install tencentcloud-sdk-python-common tencentcloud-sdk-python-billing +凭证:环境变量 TENCENTCLOUD_SECRET_ID、TENCENTCLOUD_SECRET_KEY,或从 00_账号与API索引.md 读取。 +用法: + python tencent_cloud_bill_recent_days.py [days] # 最近 N 天,默认 2 + python tencent_cloud_bill_recent_days.py 2026-02-15 2026-02-19 # 指定起止日期 +""" +import os +import re +import sys +from datetime import datetime, timedelta + +def _find_karuo_ai_root(): + d = os.path.dirname(os.path.abspath(__file__)) + for _ in range(6): + if os.path.basename(d) == "卡若AI" or (os.path.isdir(os.path.join(d, "运营中枢")) and os.path.isdir(os.path.join(d, "01_卡资(金)"))): + return d + d = os.path.dirname(d) + return None + +def _read_creds_from_index(): + root = _find_karuo_ai_root() + if not root: + return None, None + path = os.path.join(root, "运营中枢", "工作台", "00_账号与API索引.md") + if not os.path.isfile(path): + return None, None + with open(path, "r", encoding="utf-8") as f: + text = f.read() + secret_id = secret_key = None + in_tencent = False + for line in text.splitlines(): + if "### 腾讯云" in line: + in_tencent = True + continue + if in_tencent and line.strip().startswith("###"): + break + if not in_tencent: + continue + m = re.search(r"\|\s*(?:密钥|SecretId|Secret\s*Id)\s*\|\s*`([^`]+)`", line, re.I) + if m: + val = m.group(1).strip() + if val.startswith("AKID"): + secret_id = val + else: + secret_key = val + m = re.search(r"\|\s*SecretKey\s*\|\s*`([^`]+)`", line, re.I) + if m: + secret_key = m.group(1).strip() + return secret_id or None, secret_key or None + +def main(days=2, start_date=None, end_date=None): + secret_id = os.environ.get("TENCENTCLOUD_SECRET_ID") + secret_key = os.environ.get("TENCENTCLOUD_SECRET_KEY") + if not secret_id or not secret_key: + sid, skey = _read_creds_from_index() + if sid: + secret_id = secret_id or sid + if skey: + secret_key = secret_key or skey + if not secret_id or not secret_key: + print("未配置 TENCENTCLOUD_SECRET_ID 或 TENCENTCLOUD_SECRET_KEY") + print("请在本机设置环境变量,或在 运营中枢/工作台/00_账号与API索引.md 的腾讯云段落添加 SecretKey(密钥为 SecretId)") + print("\n直接查看消费:登录 https://console.cloud.tencent.com/expense/overview") + return 1 + + try: + from tencentcloud.common import credential + from tencentcloud.common.profile.client_profile import ClientProfile + from tencentcloud.common.profile.http_profile import HttpProfile + from tencentcloud.billing.v20180709 import billing_client, models + except ImportError: + print("请先安装: pip install tencentcloud-sdk-python-common tencentcloud-sdk-python-billing") + return 1 + + if start_date is None or end_date is None: + end_date = datetime.now().date() + start_date = end_date - timedelta(days=days - 1) + month = start_date.strftime("%Y-%m") + begin_time = f"{start_date} 00:00:00" + end_time = f"{end_date} 23:59:59" + + cred = credential.Credential(secret_id, secret_key) + hp = HttpProfile(endpoint="billing.tencentcloudapi.com") + cp = ClientProfile(httpProfile=hp) + client = billing_client.BillingClient(cred, "", cp) + + req = models.DescribeBillDetailRequest() + req.Month = month + req.BeginTime = begin_time + req.EndTime = end_time + req.Offset = 0 + req.Limit = 300 + + total_cost = 0 + details = [] + while True: + resp = client.DescribeBillDetail(req) + if not resp.DetailSet: + break + for d in resp.DetailSet: + try: + cost = float(getattr(d, "RealTotalCost", 0) or getattr(d, "TotalCost", 0) or 0) + except (TypeError, ValueError): + cost = 0 + total_cost += cost + details.append({ + "product": getattr(d, "BusinessCodeName", "") or getattr(d, "ProductCodeName", "") or "-", + "cost": cost, + "time": getattr(d, "PayerUin", ""), + }) + if len(resp.DetailSet) < req.Limit: + break + req.Offset += req.Limit + + print(f"\n腾讯云消费({start_date} ~ {end_date})") + print("=" * 50) + print(f"合计:¥ {total_cost:.2f}") + if details: + by_product = {} + for x in details: + k = x["product"] + by_product[k] = by_product.get(k, 0) + x["cost"] + print("\n按产品汇总:") + for k, v in sorted(by_product.items(), key=lambda t: -t[1]): + print(f" {k}: ¥ {v:.2f}") + else: + print("(无明细或 API 未返回;请登录控制台查看:https://console.cloud.tencent.com/expense/overview)") + return 0 + +if __name__ == "__main__": + start_date = end_date = None + if len(sys.argv) >= 3: + try: + start_date = datetime.strptime(sys.argv[1], "%Y-%m-%d").date() + end_date = datetime.strptime(sys.argv[2], "%Y-%m-%d").date() + except ValueError: + pass + days = int(sys.argv[1]) if len(sys.argv) == 2 and start_date is None else 2 + sys.exit(main(days=days, start_date=start_date, end_date=end_date)) diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/SKILL.md b/02_卡人(水)/水桥_平台对接/智能纪要/SKILL.md index 00550d70..389b8a84 100755 --- a/02_卡人(水)/水桥_平台对接/智能纪要/SKILL.md +++ b/02_卡人(水)/水桥_平台对接/智能纪要/SKILL.md @@ -1,11 +1,11 @@ --- name: 智能纪要 -description: 派对/会议录音一键转结构化纪要并发飞书 -triggers: 会议纪要、产研纪要、派对纪要、妙记 +description: 派对/会议录音一键转结构化纪要;飞书妙记链接/内容识别与下载(单条/批量),执行完毕用复盘格式回复 +triggers: 会议纪要、产研纪要、派对纪要、妙记、飞书妙记、飞书链接、cunkebao.feishu.cn/minutes、meetings.feishu.cn/minutes、妙记下载、第几场、指定场次、批量下载妙记、下载妙记 owner: 水桥 group: 水 -version: "1.0" -updated: "2026-02-16" +version: "1.1" +updated: "2026-02-19" --- # 派对纪要生成器 @@ -19,7 +19,7 @@ updated: "2026-02-16" | 原则 | 说明 | |:---|:---| | **命令行 + API + TOKEN 优先** | 有飞书 API、有 TOKEN 的任务,一律先用命令行处理,不额外打开网页操作 | -| **先查已有经验** | 执行前查 `运营中枢/参考资料/飞书任务_命令行与API优先_经验总结.md` 与 `运营中枢/工作台/00_账号与API索引.md`(飞书 Token) | +| **先查已有经验** | 执行前查 `运营中枢/参考资料/飞书任务_命令行与API优先_经验总结.md` 与 `运营中枢/工作台/00_账号与API索引.md`(飞书 Token);妙记 2091005/404 时查 `智能纪要/参考资料/飞书妙记下载-权限与排查说明.md` | | **统一用命令行** | 妙记拉取、批量下载、产研日报等均提供一键命令,复用已完成过的 TOKEN/会议流程 | 飞书 TOKEN 与妙记/会议已完成流程见:`运营中枢/参考资料/飞书任务_命令行与API优先_经验总结.md` @@ -169,16 +169,106 @@ python3 scripts/send_to_feishu.py --json "xxx_meeting.json" --- -## 📥 飞书妙记导出 +## 📥 飞书妙记下载:成功链路与避坑(必读) -### 导出步骤 +> **识别规则**:用户发 **飞书链接、飞书妙记、cunkebao.feishu.cn/minutes、meetings.feishu.cn/minutes、下载第几场、批量下载妙记** 等,一律按本 Skill 处理;**执行完毕后必须用复盘格式回复**(见 `运营中枢/参考资料/卡若复盘格式_固定规则.md`)。 -1. 打开飞书妙记页面(如 `cunkebao.feishu.cn/minutes/xxx`) -2. 点击右上角 **"..."** 菜单 -3. 选择 **"导出文字记录"** -4. 下载txt文件到本地 +### 强制:全自动、禁止要求用户手动操作 -### 一键生成会议纪要 +| 规则 | 说明 | +|:---|:---| +| **禁止在流程中要求用户「复制 Cookie」「打开网页」「F12 点选」等** | 执行方(AI/脚本)必须优先用自动方式完成;仅在自动方式均不可用且必须一次性配置时,才可说明「需一次性配置 xxx 后重跑」,且之后同范围再次执行视为全自动。 | +| **Cookie 获取顺序(全自动)** | 1)脚本同目录 **cookie_minutes.txt** 第一行(可为空,由 AI 或用户一次性写入);2)环境变量 **FEISHU_MINUTES_COOKIE**;3)**本机浏览器自动读取**(browser_cookie3:Safari/Chrome/Firefox/Edge;或 Doubao 浏览器 Cookie 解密)。不得首先提示用户去复制。 | +| **批量场次(如 90~102)** | 脚本先尝试从 **已保存列表缓存**(如 `soul_minutes_90_102_list.txt`)加载,有则只做导出、不拉列表;无则拉列表 → 筛选 → 导出;若导出因 Cookie 不足失败,**自动保存列表到缓存**,下次配置好 Cookie 后重跑即只做导出,无需用户再指定范围或手动整理链接。 | + +### 成功下载链路(可实现下载的完整路径) + +| 步骤 | 动作 | 说明 | +|:---|:---|:---| +| 1 | 获取 Cookie(全自动优先) | 顺序:cookie_minutes.txt → 环境变量 → **本机浏览器**(Safari/Chrome/Firefox/Edge/Doubao)。仅在以上皆无时方可说明需一次性从 list 请求复制到 cookie_minutes.txt。 | +| 2 | bv_csrf_token | 导出接口 200 通常需 **36 位**;列表接口可无。脚本已支持无 bv 时仍尝试请求(cunkebao/meetings 双域)。 | +| 3 | 列表拉取 | `GET meetings.feishu.cn 或 cunkebao.feishu.cn/minutes/api/space/list?size=50&space_name=1`,分页 timestamp;返回 list 含 object_token、topic、create_time。 | +| 4 | 导出 | `POST meetings.feishu.cn 或 cunkebao.feishu.cn/minutes/api/export`,params:object_token、format=2、add_speaker、add_timestamp;请求头:cookie、referer(同域)。 | +| 5 | 列表缓存 | 若本次拉列表成功但部分/全部导出失败,脚本将匹配到的场次列表写入 `脚本/soul_minutes_{from}_{to}_list.txt`;下次执行同范围时**优先从该文件加载、仅做导出**,无需再拉列表。 | + +核心逻辑来源:[GitHub bingsanyu/feishu_minutes](https://github.com/bingsanyu/feishu_minutes)。 + +### 避坑清单 + +| 坑 | 原因 | 处理 | +|:---|:---|:---| +| 2091005 permission deny | 应用身份(tenant_access_token)无法访问用户创建的妙记 | 用 **Cookie**(list 请求复制)或用户 token;不依赖应用单点打通 | +| 401 / Something went wrong | Cookie 缺 bv_csrf_token 或过期 | 从 **list?size=20&space_name=** 请求重拷完整 Cookie,保证 36 位 bv_csrf_token | +| 404 page not found | 开放平台 transcript 接口路径/权限 | 改用 Cookie + meetings.feishu.cn 导出接口(见上表) | +| 已存在仍重复下 | 未做本地「已有完整 txt 则跳过」 | 脚本已对 soul 目录 ≥500 行且含「说话人」的 txt 跳过 | + +详细权限与排查:`智能纪要/参考资料/飞书妙记下载-权限与排查说明.md`。 + +### 指定第几节下载(单条) + +- **指定妙记链接或 object_token**,下载**单场**文字记录到指定目录。 + +```bash +SCRIPT_DIR="/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/智能纪要/脚本" +OUT="/Users/karuo/Documents/聊天记录/soul" + +# 方式一:GitHub 同款(推荐,需 cookie_minutes.txt 含 bv_csrf_token) +python3 "$SCRIPT_DIR/feishu_minutes_export_github.py" "https://cunkebao.feishu.cn/minutes/obcnxrkz6k459k669544228c" -o "$OUT" + +# 方式二:指定 object_token +python3 "$SCRIPT_DIR/feishu_minutes_export_github.py" -t obcnxrkz6k459k669544228c -o "$OUT" + +# 方式三:通用 Cookie(含自动读浏览器) +python3 "$SCRIPT_DIR/fetch_single_minute_by_cookie.py" "https://cunkebao.feishu.cn/minutes/obcnxrkz6k459k669544228c" -o "$OUT" +``` + +### 下载指定妙记空间内「全部」或指定场次(批量) + +- **按场次筛选**:如 90~102 场,脚本拉列表后筛「第N场」再逐条导出;**Cookie 全自动**(文件 → 环境变量 → 本机浏览器);已有列表缓存则**只做导出**,不要求用户任何手动操作。 +- **输出目录**:默认 `/Users/karuo/Documents/聊天记录/soul`;已存在且为完整文字记录(含说话人、足够长)则跳过。 +- **场次范围参数**:`--from 90 --to 102`(含首含尾)。 + +```bash +SCRIPT_DIR="/Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/智能纪要/脚本" + +# 90~102 场(全自动:Cookie 优先浏览器/文件,有缓存则只导出) +python3 "$SCRIPT_DIR/download_soul_minutes_101_to_103.py" --from 90 --to 102 + +# 101~103 场(默认) +bash "$SCRIPT_DIR/download_101_to_103.sh" +# 或 +python3 "$SCRIPT_DIR/download_soul_minutes_101_to_103.py" +``` + +- **按 URL 列表批量**:先得到 `urls_soul_party.txt`(每行一条妙记链接),再: + +```bash +cd "$SCRIPT_DIR" +python3 batch_download_minutes_txt.py --list urls_soul_party.txt --output "/Users/karuo/Documents/聊天记录/soul" +``` + +- **一键 104 场**(单 token 优先,已有完整 txt 则直接成功): + +```bash +bash "$SCRIPT_DIR/download_104_to_soul.sh" +``` + +### 手动导出(无 Cookie 时兜底) + +1. 打开妙记页(如 `cunkebao.feishu.cn/minutes/xxx`) +2. 右上角「…」→「导出文字记录」 +3. 将下载的 txt 放到目标目录(如 soul) + +### 执行完毕回复规范 + +每次完成**飞书妙记相关**的下载/导出/批量任务后,**必须用「卡若复盘」格式**收尾: +目标·结果·达成率 → 过程 → 反思 → 总结 → 下一步;见 `运营中枢/参考资料/卡若复盘格式_固定规则.md`。 + +--- + +## 📥 飞书妙记导出(与纪要生成联动) + +### 从已导出 txt 生成会议纪要 ```bash # 从导出文件生成(自动发送飞书群) @@ -194,35 +284,6 @@ python3 scripts/fetch_feishu_minutes.py --file "导出.txt" --title "产研团 - **Soul派对聊天记录** - **其他会议文字记录** -### 批量下载多场妙记 TXT(如「派对」「受」100 场) - -飞书没有「妙记列表」API,需先拿到**妙记链接列表**,再批量拉取 TXT。 - -**步骤 1:得到 URL 列表文件 `urls.txt`** - -- **方式 A(推荐)**:在飞书客户端或网页打开 **视频会议 → 妙记**,在列表里用搜索框输入「派对」或「受」(或「soul 派对」),得到筛选结果后,逐条点开每条记录,复制浏览器地址栏链接(形如 `https://cunkebao.feishu.cn/minutes/xxxxx`),每行一个粘贴到 `urls.txt`。 -- **方式 B**:若列表页支持「复制链接」或导出,可一次性整理成每行一个 URL 的文本。 - -**步骤 2:批量下载 TXT** - -```bash -cd /Users/karuo/Documents/个人/卡若AI/02_卡人(水)/_团队成员/水桥/智能纪要/scripts - -# 从 urls.txt 批量下载,TXT 保存到默认 output 目录 -python3 batch_download_minutes_txt.py --list urls.txt - -# 指定输出目录(如 soul 派对 100 场) -python3 batch_download_minutes_txt.py --list urls.txt --output ./soul_party_100_txt - -# 已下载过的跳过,避免重复 -python3 batch_download_minutes_txt.py --list urls.txt --output ./soul_party_100_txt --skip-existing - -# 先试跑前 3 条 -python3 batch_download_minutes_txt.py --list urls.txt --limit 3 -``` - -**说明**:脚本内部调用飞书妙记 API 拉取文字记录;若某条无「妙记文字记录」权限,该条会保存为仅含标题+时长的占位 TXT,可后续在妙记页手动「导出文字记录」后替换。 - --- ## 📤 飞书集成配置 @@ -342,14 +403,24 @@ playwright install chromium ``` 智能纪要/ -├── scripts/ +├── 脚本/ # 飞书妙记下载与产研日报(本 Skill 主用) +│ ├── feishu_minutes_export_github.py # ⭐ 单条导出(GitHub 同款,需 bv_csrf_token) +│ ├── fetch_single_minute_by_cookie.py # 单条导出(Cookie/浏览器) +│ ├── download_soul_minutes_101_to_103.py # 批量 101~103 场 +│ ├── download_101_to_103.sh # 一键 101~103 +│ ├── download_104_to_soul.sh # 一键 104 场(已有完整 txt 则直接成功) +│ ├── cookie_minutes.txt # Cookie 配置(可选;无则自动读浏览器) +│ ├── soul_minutes_90_102_list.txt # 场次列表缓存(导出失败时自动生成,重跑即只做导出) +│ ├── fetch_feishu_minutes.py # 应用/用户 token 拉取 +│ ├── fetch_minutes_list_by_cookie.py # 列表拉取 → urls_soul_party.txt +│ └── batch_download_minutes_txt.py # 按 URL 列表批量下载 +├── scripts/ # 纪要生成与飞书发送 │ ├── daily_chanyan_to_feishu.py # ⭐ 产研会议日报(≥5分钟→总结+图发飞书) │ ├── full_pipeline.py # 完整流程(推荐) │ ├── fetch_feishu_minutes.py # 飞书妙记导出/发飞书 │ ├── parse_chatlog.py # 解析聊天记录 │ ├── generate_meeting.py # 生成HTML │ ├── md_to_summary_html.py # 总结md→HTML(产研截图) -│ ├── generate_review.py # 生成复盘HTML │ ├── screenshot.py # 截图工具 │ └── send_to_feishu.py # 飞书发送(含凭证) ├── config/ @@ -357,6 +428,8 @@ playwright install chromium ├── templates/ │ ├── meeting.html # 派对纪要模板 │ └── review.html # 复盘总结模板 +├── 参考资料/ +│ └── 飞书妙记下载-权限与排查说明.md ├── output/ # 输出目录 └── SKILL.md # 本文档 ``` @@ -377,6 +450,7 @@ playwright install chromium | 日期 | 更新 | |:---|:---| +| **2026-02-19** | 📌 飞书妙记下载:**强制全自动、禁止要求用户手动操作**;Cookie 优先 cookie_minutes.txt → 环境变量 → 本机浏览器(Safari/Chrome/Firefox/Edge/Doubao);批量支持 --from/--to(如 90~102);列表缓存 soul_minutes_{from}_{to}_list.txt,重跑只做导出;双域导出(meetings + cunkebao);执行完毕用复盘格式回复 | | **2026-01-29** | 📌 产研会议日报:daily_chanyan_to_feishu.py,飞书 API/本地 txt → 仅≥5分钟 → 总结+图发飞书,全命令行 | | **2026-01-28** | 🤖 融合本地模型:支持离线智能摘要、信息提取 | | **2026-01-28** | ✅ 配置飞书凭证,支持自动发送图片 | diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/参考资料/飞书妙记下载-权限与排查说明.md b/02_卡人(水)/水桥_平台对接/智能纪要/参考资料/飞书妙记下载-权限与排查说明.md new file mode 100644 index 00000000..3ff57c20 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/参考资料/飞书妙记下载-权限与排查说明.md @@ -0,0 +1,152 @@ +# 飞书妙记下载:权限与排查说明 + +> 用于明确「权限已开通仍报 2091005 / 404」的原因,避免以后重复踩坑。 +> 脚本使用的应用:**APP_ID = cli_a48818290ef8100d**,APP_SECRET 见脚本或 00_账号与API索引。 +> **Cookie 导出方案** 核心逻辑来自 [GitHub bingsanyu/feishu_minutes](https://github.com/bingsanyu/feishu_minutes):需从妙记列表请求 `list?size=20&space_name=` 复制 Cookie,且必须包含 **bv_csrf_token**(36 位);导出接口为 `POST https://meetings.feishu.cn/minutes/api/export`,referer 为 `https://meetings.feishu.cn/minutes/me`。 + +--- + +## 一、凭证确认(防止用错应用) + +| 项 | 值 | 说明 | +|----|-----|------| +| APP_ID | `cli_a48818290ef8100d` | 与 `fetch_feishu_minutes.py` / `batch_download_minutes_txt.py` 内置一致 | +| APP_SECRET | 脚本内默认 / 环境变量 `FEISHU_APP_SECRET` | 勿提交 Git,仅本机或 00_账号与API索引 保管 | + +脚本优先读环境变量 `FEISHU_APP_ID`、`FEISHU_APP_SECRET`,未设置则用上述默认值。**凭证正确时,获取 tenant_access_token 会成功**,若仍报错则多为权限或数据范围问题。 + +--- + +## 二、应用身份妙记权限全开(靠本应用直接访问妙记) + +**目标**:只靠应用身份(不依赖用户 token / Cookie)就能访问妙记时,需在开放平台把妙记相关权限全开,且**可访问的数据范围**设为「全部」并保存。 + +### 2.1 操作步骤(已为你打开权限页时可照做) + +1. 打开 **应用身份权限(tenant_access_token)** 标签页。 +2. 搜索「妙记」或「minutes」,在「妙记」分类下**全部勾选开通**: + - 查看、创建、编辑及管理妙记文件 `minutes:minutes` + - 查看妙记文件 `minutes:minutes:readonly` + - 获取妙记的基本信息 `minutes:minutes.basic:read` + - 下载妙记的音视频文件 `minutes:minutes.media:export` + - 获取妙记的统计信息 `minutes:minutes.statistics:read` + - **导出妙记转写的文字内容** `minutes:minutes.transcript:export` +3. 对 **「查看妙记文件」** 和 **「导出妙记转写的文字内容」** 点 **「可访问的数据范围」** 旁的「配置」或进入配置: + - 选择 **「全部」**(不要选「按条件筛选」)。 + - 点击 **「保存」**。 +4. 等待约 1 分钟生效后,再运行下载命令。 + +### 2.2 权限与用途(速查) + +| 权限名称 | 权限码 | 用途 | +|----------|--------|------| +| 查看妙记文件 | `minutes:minutes:readonly` | 调用「获取妙记信息」,否则 2091005 | +| 导出妙记转写的文字内容 | `minutes:minutes.transcript:export` | 调用「妙记文字记录/逐字稿」 | +| 获取妙记的基本信息 | `minutes:minutes.basic:read` | 可选,加强信息拉取 | +| 其他妙记相关 | 见上表 | 按需全开 | + +**数据范围未选「全部」时,应用仍可能无法访问 cunkebao/思域 下的妙记,会继续 2091005。** + +--- + +## 三、为什么「权限已开通」仍报 2091005 / 404 + +### 1. 应用身份有「可访问的数据范围」限制 + +- 使用 **应用身份(tenant_access_token)** 时,飞书规定:**只能访问该应用有权访问的资源**。 +- 「查看妙记文件」已开通,只表示**具备该权限能力**,不表示能访问**任意**妙记。 +- 若妙记是在 **cunkebao.feishu.cn / 个人空间 / 其他租户** 下由**用户**创建,通常**不属于**该应用的可访问范围,接口会返回 **2091005 permission deny**。 +- 权限页中「可访问的数据范围」若为空或未包含该妙记所在空间,就会出现「权限已开通但接口仍 2091005」的情况。 + +### 2. 2091005 的含义 + +- **错误码 2091005**:permission deny,表示**当前 token 对这篇妙记没有访问权限**。 +- 可能原因: + - 应用身份下,该妙记不在应用可访问范围内; + - 或未开通「查看妙记文件」; + - 或妙记已删除/不可见。 + +### 3. 文字记录接口返回 404 + +- 脚本当前调用:`GET /open-apis/minutes/v1/minutes/{minute_token}/transcripts`。 +- 若返回 **404 page not found**:可能是接口路径变更,或同样因**无该妙记访问权限**被拒绝(部分网关以 404 返回)。 +- 先解决 2091005(让「获取妙记信息」通过),再观察 transcript 是否仍 404。 + +--- + +## 四、以后如何避免「权限已开通仍失败」 + +### 1. 确认同一应用 + +- 开放平台里开通权限的 **应用** = 脚本里用的 **APP_ID**(cli_a48818290ef8100d)。 +- 若有多个应用,只给 A 开通而脚本用 B,仍会 2091005。 + +### 2. 应用身份只适合「应用能访问到的妙记」 + +- 若妙记是**用户个人/其他空间**的,应用身份往往**无法**访问。 +- 需要**同时用应用 + 用户身份**直接访问时,见下条「应用+用户双轨」。 + +### 3. 应用 + 用户身份双轨(直接访问个人/空间妙记) + +脚本已支持**优先使用用户身份 token**,用同一应用在「用户身份」下访问你能看到的妙记,无需 Cookie。 + +- **步骤一:开放平台为该应用开通「用户身份」妙记权限** + 在「开通权限」页中,**用户身份权限(user_access_token)** 下同样勾选: + **查看妙记文件**、**导出妙记转写的文字内容**,并保存。 + +- **步骤二:用本应用获取用户 access_token(必须为本应用 OAuth 所得)** + - 00_账号与API索引 里的飞书 access_token / refresh_token 若是**其他应用**(如飞书 Suite App)颁发的,**不能**直接用于本应用;会报 99991668 Invalid access token。 + - 须用 **cli_a48818290ef8100d** 做一次 OAuth 授权: + 1. 飞书开放平台 → 该应用 → 安全设置 → 配置「重定向 URL」(如 `http://localhost/feishu_callback`)。 + 2. 在浏览器打开授权链接(格式示例): + `https://open.feishu.cn/open-apis/authen/v1/authorize?app_id=cli_a48818290ef8100d&redirect_uri=你配置的URL&scope=minutes:minutes:readonly,minutes:minutes.transcript:export` + 3. 登录并授权后,飞书会重定向到 redirect_uri 并带上 `code`,用 code 调「通过 code 获取 user_access_token」接口得到 **access_token** 和 **refresh_token**。 + 4. 将 **access_token** 填到脚本同目录 `feishu_user_token.txt` 第一行,**refresh_token** 填第二行(可选,用于自动刷新),保存。 + +- **步骤三:运行脚本** + ```bash + cd 智能纪要/脚本 + python3 fetch_feishu_minutes.py "https://cunkebao.feishu.cn/minutes/" --output "/Users/karuo/Documents/聊天记录/soul" + ``` + 脚本会优先读 `feishu_user_token.txt` 或环境变量 `FEISHU_USER_ACCESS_TOKEN`,使用**用户身份**调妙记接口,即可访问你个人/空间下的妙记。 + 若用户 token 过期(99991668),脚本会尝试用第二行 refresh_token 自动刷新并写回文件。 + +- **小结**:要「应用也能用、个人也能直接访问」,就**同时**开通应用身份 + 用户身份妙记权限,并把**本应用** OAuth 得到的用户 token 填到 `feishu_user_token.txt`;这样一条命令即可直接访问该内容。 + +### 4. Cookie 方案(推荐用于 cunkebao / 个人妙记,且不想配 OAuth 时) + +1. 浏览器打开 https://cunkebao.feishu.cn/minutes/home 并登录。 +2. F12 → 网络 → 找到 `list?size=` 或导出相关请求 → 复制请求头中的 **Cookie**。 +3. 粘贴到智能纪要脚本目录下的 `cookie_minutes.txt`(第一行,覆盖占位符)。 +4. 执行: + ```bash + cd /Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/智能纪要/脚本 + python3 fetch_single_minute_by_cookie.py "https://cunkebao.feishu.cn/minutes/" --output "/Users/karuo/Documents/聊天记录/soul" + ``` + Cookie 有效时,会走 cunkebao/meetings 的导出接口,不依赖应用权限。 + +### 5. 手动导出 + +- 打开该场妙记页 → 右上角「…」→「导出文字记录」→ 将 TXT 保存到 `聊天记录/soul`。 + +--- + +## 五、排查清单(以后出现同类问题按此核对) + +| 步骤 | 检查项 | +|------|--------| +| 1 | 脚本使用的 APP_ID 是否为 cli_a48818290ef8100d? | +| 2 | 开放平台该应用下「查看妙记文件」「导出妙记转写的文字内容」是否均为「已开通」? | +| 3 | 该妙记是否在**应用可访问的数据范围**内(如企业内、应用已安装的空间)?若在 cunkebao/个人,优先用 Cookie 或手动导出。 | +| 4 | 是否可用 Cookie 方案:cookie_minutes.txt 是否已粘贴有效 Cookie 并重试? | +| 5 | 若仅需单场文字,是否已尝试妙记页「…」→ 导出文字记录? | +| 6 | 若要用用户身份直接访问:是否已用**本应用** OAuth 获取 token 并填入 `feishu_user_token.txt`?用户身份权限是否已开通? | + +--- + +## 六、相关文件 + +- 凭证集中存放:卡若AI `运营中枢/工作台/00_账号与API索引.md`(飞书 Token / 应用凭证说明) +- 脚本默认凭证:`智能纪要/脚本/fetch_feishu_minutes.py` 内 FEISHU_APP_ID / FEISHU_APP_SECRET +- 下载步骤:`聊天记录/soul/飞书妙记下载说明.md` +- 批量下载:`智能纪要/参考资料/飞书妙记批量下载TXT说明.md` diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/cdp_104_export.py b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/cdp_104_export.py new file mode 100644 index 00000000..1ac82458 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/cdp_104_export.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""通过 CDP 连接已开启远程调试的浏览器,访问 list 页取 Cookie 后导出 104 场并保存。""" +import sys +from pathlib import Path + +OUT_FILE = Path("/Users/karuo/Documents/聊天记录/soul/soul 派对 104场 20260219.txt") +URL_LIST = "https://cunkebao.feishu.cn/minutes/home" +OBJECT_TOKEN = "obcnyg5nj2l8q281v32de6qz" +EXPORT_URL = "https://cunkebao.feishu.cn/minutes/api/export" +CDP_URL = "http://localhost:9222" + + +def main(): + try: + from playwright.sync_api import sync_playwright + except ImportError: + print("NO_PLAYWRIGHT", file=sys.stderr) + return 2 + import requests + + with sync_playwright() as p: + try: + browser = p.chromium.connect_over_cdp(CDP_URL, timeout=8000) + except Exception as e: + print("CDP_CONNECT_FAIL", str(e), file=sys.stderr) + return 3 + default_context = browser.contexts[0] if browser.contexts else None + if not default_context: + print("NO_CONTEXT", file=sys.stderr) + browser.close() + return 4 + # 优先用已打开的 104 页,否则新开 list 页 + pages = default_context.pages + page = None + for p in pages: + if "obcnyg5nj2l8q281v32de6qz" in (p.url or ""): + page = p + break + if not page and pages: + page = pages[0] + if not page: + page = default_context.new_page() + # 若当前不是 list 页则打开 list 以拿到 bv_csrf_token + if "space/list" not in (page.url or "") and "minutes/home" not in (page.url or ""): + page.goto(URL_LIST, wait_until="domcontentloaded", timeout=20000) + page.wait_for_timeout(4000) + cookies = default_context.cookies() + browser.close() + + cookie_str = "; ".join([f"{c['name']}={c['value']}" for c in cookies]) + bv = next((c["value"] for c in cookies if c.get("name") == "bv_csrf_token" and len(c.get("value", "")) == 36), None) + if len(cookie_str) < 100: + print("NO_COOKIE", file=sys.stderr) + return 5 + headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Cookie": cookie_str, + "Referer": "https://cunkebao.feishu.cn/minutes/", + } + if bv: + headers["bv-csrf-token"] = bv + r = requests.post( + EXPORT_URL, + params={"object_token": OBJECT_TOKEN, "format": 2, "add_speaker": "true", "add_timestamp": "false"}, + headers=headers, + timeout=20, + ) + r.encoding = "utf-8" + if r.status_code != 200: + print("EXPORT_HTTP", r.status_code, len(r.text), file=sys.stderr) + return 6 + text = (r.text or "").strip() + if not text or len(text) < 100 or "Something went wrong" in text: + print("EXPORT_BODY", text[:300], file=sys.stderr) + return 7 + if text.startswith("{"): + try: + j = r.json() + text = j.get("data") or j.get("content") or "" + if isinstance(text, dict): + text = text.get("content") or text.get("text") or "" + except Exception: + pass + OUT_FILE.parent.mkdir(parents=True, exist_ok=True) + OUT_FILE.write_text("日期: 20260219\n标题: soul 派对 104场 20260219\n\n文字记录:\n\n" + text, encoding="utf-8") + print("OK", str(OUT_FILE)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_101_to_103.sh b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_101_to_103.sh new file mode 100644 index 00000000..eb2d3eda --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_101_to_103.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# 下载 soul 派对 第101、102、103 场妙记文字记录到 soul 目录(需已配置 cookie_minutes.txt) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" +PY="python3" +[ -x "$SCRIPT_DIR/.venv/bin/python" ] && PY="$SCRIPT_DIR/.venv/bin/python" +exec $PY download_soul_minutes_101_to_103.py "$@" diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_104_to_soul.sh b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_104_to_soul.sh new file mode 100755 index 00000000..ee69a117 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_104_to_soul.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# 一键下载 104 场妙记:优先 GitHub 同款 Cookie 导出,再试通用 Cookie,最后试应用/用户 token +# 核心逻辑参考:https://github.com/bingsanyu/feishu_minutes +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="/Users/karuo/Documents/聊天记录/soul" +TOKEN="obcnxrkz6k459k669544228c" +URL="https://cunkebao.feishu.cn/minutes/${TOKEN}" +cd "$SCRIPT_DIR" +PY="python3" +[ -x "$SCRIPT_DIR/.venv/bin/python" ] && PY="$SCRIPT_DIR/.venv/bin/python" + +_check_saved() { + for f in "$OUT"/*.txt; do + [ -f "$f" ] && [ -s "$f" ] && ! grep -q "未解析到文字内容\|需在飞书妙记页面" "$f" 2>/dev/null && grep -q . "$f" && echo "已保存: $f" && return 0 + done + return 1 +} + +# 0) 已有该场完整文字记录则直接成功 +for f in "$OUT"/*.txt; do + [ -f "$f" ] && [ -s "$f" ] || continue + lines=$(wc -l < "$f" 2>/dev/null) + if [ "${lines:-0}" -ge 500 ] && grep -q "说话人" "$f" 2>/dev/null; then + echo "已存在完整文字记录,跳过下载: $f" + exit 0 + fi +done + +# 1) GitHub 同款:cookie_minutes.txt 需含 bv_csrf_token(来自 list?size=20& 请求) +if [ -f "$SCRIPT_DIR/cookie_minutes.txt" ] && grep -q "bv_csrf_token=" "$SCRIPT_DIR/cookie_minutes.txt" 2>/dev/null; then + echo "使用 GitHub 同款导出(meetings.feishu.cn)..." + if $PY feishu_minutes_export_github.py -t "$TOKEN" -o "$OUT" 2>/dev/null && _check_saved; then exit 0; fi +fi + +# 2) 通用 Cookie(含自动读浏览器) +echo "尝试 Cookie(文件/浏览器)拉取..." +if $PY fetch_single_minute_by_cookie.py "$URL" -o "$OUT" 2>/dev/null && _check_saved; then exit 0; fi + +# 3) 应用/用户 token +echo "尝试应用/用户 token..." +$PY fetch_feishu_minutes.py "$URL" -o "$OUT" +exit 0 diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_soul_minutes_101_to_103.py b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_soul_minutes_101_to_103.py new file mode 100644 index 00000000..61771596 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_soul_minutes_101_to_103.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 +""" +拉取妙记列表,按场次范围筛选并下载文字记录到 soul 目录。 +默认 101~103 场;可用 --from 90 --to 102 指定 90~102 场等。 +依赖 Cookie(cookie_minutes.txt,需含 bv_csrf_token)。与 feishu_minutes_export_github 同源。 +""" +from __future__ import annotations + +import argparse +import re +import sys +import time +from pathlib import Path +from datetime import datetime + +try: + import requests +except ImportError: + requests = None + +SCRIPT_DIR = Path(__file__).resolve().parent +COOKIE_FILE = SCRIPT_DIR / "cookie_minutes.txt" +OUT_DIR = Path("/Users/karuo/Documents/聊天记录/soul") + +# 与 feishu_minutes_export_github 一致 +LIST_URL = "https://meetings.feishu.cn/minutes/api/space/list" +EXPORT_URL = "https://meetings.feishu.cn/minutes/api/export" +REFERER = "https://meetings.feishu.cn/minutes/me" +USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36" + +FIELD_PATTERN = re.compile(r"第?(\d{2,3})场", re.I) + + +def _cookie_from_browser() -> str: + """全自动:从本机浏览器读取飞书 Cookie,无需用户手动复制。""" + import os as _os + # 1) 环境变量 + c = _os.environ.get("FEISHU_MINUTES_COOKIE", "").strip() + if c and len(c) > 100 and "PASTE_YOUR" not in c: + return c + # 2) browser_cookie3 + try: + import browser_cookie3 + for domain in ("cunkebao.feishu.cn", "feishu.cn", ".feishu.cn"): + for loader in (browser_cookie3.safari, browser_cookie3.chrome, browser_cookie3.chromium, browser_cookie3.firefox, browser_cookie3.edge): + try: + cj = loader(domain_name=domain) + parts = [f"{c.name}={c.value}" for c in cj] + s = "; ".join(parts) + if len(s) > 100: + return s + except Exception: + continue + except ImportError: + pass + # 3) Doubao 浏览器 Cookie(macOS) + try: + import subprocess + import shutil + import tempfile + import sqlite3 + import hashlib + for name in ("Doubao Browser Safe Storage", "Doubao Safe Storage"): + try: + key = subprocess.run(["security", "find-generic-password", "-s", name, "-w"], capture_output=True, text=True, timeout=5).stdout.strip() + if not key: + continue + except Exception: + continue + for profile in ("Default", "Profile 1", "Profile 2", "Profile 3"): + db = Path.home() / "Library/Application Support/Doubao" / profile / "Cookies" + if not db.exists(): + continue + try: + tmp = tempfile.mktemp(suffix=".db") + shutil.copy2(db, tmp) + conn = sqlite3.connect(tmp) + cur = conn.cursor() + cur.execute("SELECT host_key, name, encrypted_value FROM cookies WHERE host_key LIKE '%feishu%' OR host_key LIKE '%cunkebao%'") + rows = cur.fetchall() + conn.close() + Path(tmp).unlink(missing_ok=True) + except Exception: + continue + if not rows: + continue + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + derived = hashlib.pbkdf2_hmac("sha1", key.encode("utf-8"), b"saltysalt", 1003, dklen=16) + parts = [] + for host, name, enc in rows: + if enc[:3] != b"v10": + continue + raw = enc[3:] + dec = Cipher(algorithms.AES(derived), modes.CBC(b" " * 16)).decryptor() + pt = dec.update(raw) + dec.finalize() + pad = pt[-1] + if isinstance(pad, int) and 1 <= pad <= 16: + pt = pt[:-pad] + for i in range(min(len(pt), 48)): + if i + 4 <= len(pt) and all(32 <= pt[j] < 127 for j in range(i, min(i + 8, len(pt)))): + val = pt[i:].decode("ascii", errors="ignore") + if val and "\x00" not in val: + parts.append(f"{name}={val}") + break + if len(parts) > 5: + return "; ".join(parts) + except Exception: + pass + return "" + + +def get_cookie() -> str: + """优先级:cookie_minutes.txt → 环境变量 → 自动从浏览器读取(全自动,无需用户手动)。""" + if COOKIE_FILE.exists(): + for line in COOKIE_FILE.read_text(encoding="utf-8", errors="ignore").strip().splitlines(): + line = line.strip() + if line and not line.startswith("#") and "PASTE_YOUR" not in line and len(line) > 100: + return line + c = _cookie_from_browser() + if c: + return c + return "" + + +def get_bv_csrf(cookie: str) -> str: + key = "bv_csrf_token=" + i = cookie.find(key) + if i == -1: + return "" + start = i + len(key) + end = cookie.find(";", start) + if end == -1: + end = len(cookie) + return cookie[start:end].strip() + + +def build_headers(cookie: str, require_bv: bool = False) -> dict: + """require_bv=True 时强制 bv_csrf_token 36 位;否则能带就带,不带也尝试请求(cunkebao 等可能可用)。""" + h = { + "User-Agent": USER_AGENT, + "cookie": cookie, + "referer": REFERER, + "content-type": "application/x-www-form-urlencoded", + } + bv = get_bv_csrf(cookie) + if len(bv) == 36: + h["bv-csrf-token"] = bv + elif require_bv: + raise ValueError("Cookie 需包含 bv_csrf_token(36 位)。请从妙记 list 请求复制完整 Cookie。") + return h + + +def fetch_list(cookie: str, space_name: int = 1, size: int = 50, last_ts=None, base_url: str = None) -> list: + """拉取妙记列表(分页)。base_url 默认 meetings,可改为 cunkebao。""" + base = base_url or LIST_URL + url = f"{base}?size={size}&space_name={space_name}" + if last_ts: + url += f"×tamp={last_ts}" + headers = build_headers(cookie, require_bv=False) + r = requests.get(url, headers=headers, timeout=30) + data = r.json() + if data.get("code") != 0: + raise RuntimeError(data.get("msg", "list api error") or r.text[:200]) + inner = data.get("data", {}) + lst = inner.get("list", []) + out = list(lst) + if inner.get("has_more") and lst: + last = lst[-1] + ts = last.get("share_time") or last.get("create_time") + if ts: + time.sleep(0.3) + out.extend(fetch_list(cookie, space_name, size, ts, base_url)) + return out + + +def filter_by_field_range(all_items: list, from_num: int, to_num: int) -> list: + """保留标题中含「第N场」且 N 在 [from_num, to_num] 的项(去重、按场次排序)。""" + seen = set() + matched = [] + for item in all_items: + topic = (item.get("topic") or "").strip() + m = FIELD_PATTERN.search(topic) + if not m: + continue + num = int(m.group(1)) + if not (from_num <= num <= to_num): + continue + token = item.get("object_token") or item.get("minute_token") + if not token or token in seen: + continue + seen.add(token) + matched.append((num, topic, token, item.get("create_time"), item.get("share_time"))) + return sorted(matched, key=lambda x: x[0]) + + +def export_transcript(cookie: str, object_token: str) -> str | None: + """尝试 meetings 与 cunkebao 两个导出域名。""" + params = {"object_token": object_token, "add_speaker": "true", "add_timestamp": "false", "format": 2} + for export_base in (EXPORT_URL, "https://cunkebao.feishu.cn/minutes/api/export"): + ref = "https://cunkebao.feishu.cn/minutes/" if "cunkebao" in export_base else REFERER + headers = build_headers(cookie, require_bv=False) + headers["referer"] = ref + try: + r = requests.post(export_base, params=params, headers=headers, timeout=20) + r.encoding = "utf-8" + if r.status_code != 200: + continue + text = (r.text or "").strip() + if not text or len(text) < 20: + continue + if text.startswith("{"): + try: + j = r.json() + d = j.get("data") + if isinstance(d, str): + return d + if isinstance(d, dict): + return d.get("content") or d.get("text") or d.get("transcript") + except Exception: + pass + continue + if " Path: + output_dir.mkdir(parents=True, exist_ok=True) + date_str = date_str or datetime.now().strftime("%Y%m%d") + safe_title = re.sub(r'[\\/*?:"<>|]', "_", (title or "妙记")) + path = output_dir / f"{safe_title}_{date_str}.txt" + path.write_text(f"日期: {date_str}\n标题: {title}\n\n文字记录:\n\n{body}", encoding="utf-8") + return path + + +def main() -> int: + parser = argparse.ArgumentParser(description="soul 派对妙记按场次范围下载") + parser.add_argument("--from", "-f", type=int, default=101, dest="from_num", help="起始场次(含)") + parser.add_argument("--to", "-t", type=int, default=103, dest="to_num", help="结束场次(含)") + args = parser.parse_args() + from_num, to_num = args.from_num, args.to_num + if from_num > to_num: + from_num, to_num = to_num, from_num + + if not requests: + print("请安装 requests: pip install requests", file=sys.stderr) + return 1 + cookie = get_cookie() + if not cookie: + print("未获取到 Cookie(已尝试 cookie_minutes.txt、环境变量、本机浏览器)。请确保已登录飞书妙记并在 cookie_minutes.txt 第一行粘贴 list 请求的 Cookie。", file=sys.stderr) + return 1 + try: + build_headers(cookie, require_bv=False) + except ValueError as e: + print(e, file=sys.stderr) + return 1 + + list_cache = SCRIPT_DIR / f"soul_minutes_{from_num}_{to_num}_list.txt" + items = [] + + # 优先从已保存列表加载(全自动重跑:无需再拉列表,直接导出) + if list_cache.exists(): + try: + lines = list_cache.read_text(encoding="utf-8").strip().splitlines() + for line in lines: + if line.startswith("#") or not line.strip(): + continue + parts = line.split("\t") + if len(parts) >= 3: + items.append((int(parts[0]), parts[1], parts[2], None, None)) + if items: + items = sorted(items, key=lambda x: x[0]) + print(f"📌 从缓存加载 {len(items)} 场,仅做导出(全自动)") + except Exception: + pass + + if not items: + print(f"📋 拉取妙记列表(场次范围 {from_num}~{to_num})…") + all_items = [] + for base in (LIST_URL, "https://cunkebao.feishu.cn/minutes/api/space/list"): + try: + all_items = fetch_list(cookie, base_url=base) + if all_items: + break + except Exception as e: + if base == LIST_URL: + print(" meetings 列表失败,尝试 cunkebao…", e) + continue + if not all_items: + print("拉取列表失败,请确认 Cookie 有效且来自妙记 list 请求。", file=sys.stderr) + return 1 + items = filter_by_field_range(all_items, from_num, to_num) + if not items: + print(f"未在列表中匹配到第{from_num}~{to_num}场。") + return 0 + + print(f"📌 匹配到 {len(items)} 场: {[x[1] for x in items]}") + out_dir = OUT_DIR.resolve() + saved = 0 + for num, topic, token, create_ts, share_ts in items: + # 若已存在完整文字记录则跳过(文件名需像妙记:含 第N场 且含 soul/派对,避免误判说明类文件) + skip = False + for f in out_dir.glob("*.txt"): + if f"第{num}场" not in f.name and (f"{num}场" not in f.name or ("soul" not in f.name and "派对" not in f.name)): + continue + try: + t = f.read_text(encoding="utf-8", errors="ignore") + if len(t) > 5000 and "说话人" in t[:5000]: + skip = True + print(f" 跳过(已存在): {topic}") + saved += 1 + break + except Exception: + pass + if skip: + continue + else: + text = export_transcript(cookie, token) + if not text: + print(f" ❌ 导出失败: {topic} ({token})") + continue + ts = create_ts or share_ts + if ts: + try: + if ts > 1e10: + ts = ts / 1000 + date_str = datetime.fromtimestamp(ts).strftime("%Y%m%d") + except Exception: + date_str = datetime.now().strftime("%Y%m%d") + else: + date_str = datetime.now().strftime("%Y%m%d") + path = save_txt(out_dir, topic, text, date_str) + print(f" ✅ {topic} -> {path.name}") + saved += 1 + time.sleep(0.5) + + # 若有导出失败且本次是拉列表得到的 items,保存列表供 Cookie 配置好后重跑(仅做导出) + failed = len(items) - saved + if failed > 0 and not list_cache.exists(): + cache_lines = ["# 场次\t标题\tobject_token", "# 配置 cookie_minutes.txt 后重新执行本脚本即可只做导出"] + for num, topic, token, _, _ in items: + cache_lines.append(f"{num}\t{topic}\t{token}") + list_cache.write_text("\n".join(cache_lines), encoding="utf-8") + print(f"📁 已保存列表到 {list_cache.name},配置 cookie_minutes.txt(从妙记 list 请求复制 Cookie)后重新执行即可只做导出。") + + print(f"✅ 共处理 {saved}/{len(items)} 场,保存目录: {out_dir}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/feishu_minutes_export_github.py b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/feishu_minutes_export_github.py new file mode 100644 index 00000000..232a4a36 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/feishu_minutes_export_github.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +飞书妙记单条导出(核心逻辑来自 GitHub bingsanyu/feishu_minutes) + +- Cookie 必须从「飞书妙记」列表请求中获取,且包含 bv_csrf_token(36 位)。 +- 获取方式:打开 https://meetings.feishu.cn/minutes/home → F12 → 网络 → 找到 list?size=20&space_name= 请求 → 复制请求头中的 Cookie。 +- 若使用 cunkebao 等子域,请打开对应空间的妙记主页,从 list 请求复制 Cookie。 + +用法: + python3 feishu_minutes_export_github.py --cookie "..." --object-token obcnxrkz6k459k669544228c -o /path/to/soul + 或:cookie 放在脚本同目录 cookie_minutes.txt 第一行 + python3 feishu_minutes_export_github.py --object-token obcnxrkz6k459k669544228c -o /path/to/soul +""" +from __future__ import annotations + +import argparse +import os +import re +import sys +from pathlib import Path +from datetime import datetime + +try: + import requests +except ImportError: + requests = None + +SCRIPT_DIR = Path(__file__).resolve().parent +COOKIE_FILE = SCRIPT_DIR / "cookie_minutes.txt" + +# 与 bingsanyu/feishu_minutes 一致 +EXPORT_URL = "https://meetings.feishu.cn/minutes/api/export" +REFERER = "https://meetings.feishu.cn/minutes/me" +USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36" + + +def get_cookie_from_args_or_file(cookie_arg: str | None) -> str: + if cookie_arg and cookie_arg.strip() and "PASTE_YOUR" not in cookie_arg: + return cookie_arg.strip() + if COOKIE_FILE.exists(): + raw = COOKIE_FILE.read_text(encoding="utf-8", errors="ignore").strip().splitlines() + for line in raw: + line = line.strip() + if line and not line.startswith("#") and "PASTE_YOUR" not in line: + return line + return "" + + +def get_bv_csrf_token(cookie: str) -> str: + """从 cookie 字符串中解析 bv_csrf_token(需 36 字符,与 GitHub 一致)。""" + key = "bv_csrf_token=" + i = cookie.find(key) + if i == -1: + return "" + start = i + len(key) + end = cookie.find(";", start) + if end == -1: + end = len(cookie) + return cookie[start:end].strip() + + +def build_headers(cookie: str): + """与 feishu_downloader.py 完全一致的请求头。""" + bv = get_bv_csrf_token(cookie) + if len(bv) != 36: + raise ValueError( + "Cookie 中未包含有效的 bv_csrf_token(需 36 位)。" + "请从 飞书妙记主页 → F12 → 网络 → list?size=20& 请求 中复制完整 Cookie。" + ) + return { + "User-Agent": USER_AGENT, + "cookie": cookie, + "bv-csrf-token": bv, + "referer": REFERER, + "content-type": "application/x-www-form-urlencoded", + } + + +def export_transcript(cookie: str, object_token: str, format_txt: bool = True, add_speaker: bool = True, add_timestamp: bool = False) -> str | None: + """ + 调用妙记导出接口,与 GitHub feishu_downloader.get_minutes_url 一致: + POST export,params: object_token, add_speaker, add_timestamp, format (2=txt, 3=srt)。 + 返回导出的文本,失败返回 None。 + """ + if not requests: + return None + # format: 2=txt, 3=srt(与 config.ini 一致) + params = { + "object_token": object_token, + "add_speaker": "true" if add_speaker else "false", + "add_timestamp": "true" if add_timestamp else "false", + "format": 2 if format_txt else 3, + } + headers = build_headers(cookie) + try: + r = requests.post(EXPORT_URL, params=params, headers=headers, timeout=20) + r.encoding = "utf-8" + if r.status_code != 200: + return None + text = (r.text or "").strip() + if not text or len(text) < 20: + return None + # 可能是 JSON 包装 + if text.startswith("{"): + try: + j = r.json() + data = j.get("data") + if isinstance(data, str): + return data + if isinstance(data, dict): + return data.get("content") or data.get("text") or data.get("transcript") + except Exception: + pass + return None + if " str: + m = re.search(r"/minutes/([a-zA-Z0-9]+)", url_or_token) + if m: + return m.group(1) + return url_or_token.strip() + + +def save_txt(output_dir: Path, title: str, body: str, date_str: str | None = None) -> Path: + output_dir.mkdir(parents=True, exist_ok=True) + date_str = date_str or datetime.now().strftime("%Y%m%d") + safe_title = re.sub(r'[\\/*?:"<>|]', "_", (title or "妙记")) + filename = f"{safe_title}_{date_str}.txt" + path = output_dir / filename + header = f"日期: {date_str}\n标题: {title}\n\n文字记录:\n\n" if title else f"日期: {date_str}\n\n文字记录:\n\n" + path.write_text(header + body, encoding="utf-8") + return path + + +def main() -> int: + if not requests: + print("请安装 requests: pip install requests", file=sys.stderr) + return 1 + + parser = argparse.ArgumentParser(description="飞书妙记单条导出(GitHub bingsanyu/feishu_minutes 逻辑)") + parser.add_argument("url_or_token", nargs="?", default="", help="妙记链接或 object_token") + parser.add_argument("--cookie", "-c", default="", help="完整 Cookie(或从 cookie_minutes.txt 读取)") + parser.add_argument("--object-token", "-t", default="", help="妙记 object_token") + parser.add_argument("--output", "-o", default="/Users/karuo/Documents/聊天记录/soul", help="输出目录") + parser.add_argument("--title", default="", help="可选标题(否则用默认文件名)") + parser.add_argument("--no-speaker", action="store_true", help="字幕不包含说话人") + parser.add_argument("--timestamp", action="store_true", help="字幕包含时间戳") + args = parser.parse_args() + + cookie = get_cookie_from_args_or_file(args.cookie) + if not cookie: + print("未配置 Cookie。请:", file=sys.stderr) + print(" 1. 打开 https://meetings.feishu.cn/minutes/home(或你空间的妙记主页)", file=sys.stderr) + print(" 2. F12 → 网络 → 找到 list?size=20&space_name= 请求 → 复制请求头中的 Cookie", file=sys.stderr) + print(" 3. 粘贴到脚本同目录 cookie_minutes.txt 第一行,或使用 --cookie \"...\"", file=sys.stderr) + return 1 + + object_token = args.object_token or extract_token_from_url(args.url_or_token) + if not object_token: + object_token = "obcnxrkz6k459k669544228c" # 104 场默认 + + try: + build_headers(cookie) + except ValueError as e: + print(str(e), file=sys.stderr) + return 1 + + print(f"📝 object_token: {object_token}") + print("📡 使用 meetings.feishu.cn 导出接口(GitHub 同款)…") + text = export_transcript(cookie, object_token, format_txt=True, add_speaker=not args.no_speaker, add_timestamp=args.timestamp) + if not text: + print("❌ 导出失败。请确认:", file=sys.stderr) + print(" 1. Cookie 来自「妙记列表 list 请求」且包含 bv_csrf_token(36 位)", file=sys.stderr) + print(" 2. 该妙记在当前登录空间内可访问", file=sys.stderr) + return 1 + + out_dir = Path(args.output).resolve() + title = args.title or f"妙记_{object_token}" + path = save_txt(out_dir, title, text) + print(f"✅ 已保存: {path}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/fetch_feishu_minutes.py b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/fetch_feishu_minutes.py index c827e5df..e6800ed8 100755 --- a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/fetch_feishu_minutes.py +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/fetch_feishu_minutes.py @@ -46,6 +46,8 @@ FEISHU_APP_SECRET = os.environ.get("FEISHU_APP_SECRET", "dhjU0qWd5AzicGWTf4cTqhC # 输出目录 SCRIPT_DIR = Path(__file__).parent OUTPUT_DIR = SCRIPT_DIR.parent / "output" +# 用户身份 token 文件(可选,一行一个 access_token,用于访问个人/空间妙记) +FEISHU_USER_TOKEN_FILE = SCRIPT_DIR / "feishu_user_token.txt" def get_tenant_access_token() -> str: @@ -69,6 +71,66 @@ def get_tenant_access_token() -> str: return None +def _read_user_token_file() -> tuple[str | None, str | None]: + """从 feishu_user_token.txt 读取 access_token 和 refresh_token(第一行 access,第二行可选 refresh)。""" + if not FEISHU_USER_TOKEN_FILE.exists(): + return (None, None) + try: + lines = [ + L.strip() for L in FEISHU_USER_TOKEN_FILE.read_text(encoding="utf-8", errors="ignore").splitlines() + if L.strip() and not L.strip().startswith("#") + ] + access = lines[0] if lines and "u-" in lines[0] else None + refresh = lines[1] if len(lines) > 1 and lines[1].startswith("ur-") else None + return (access, refresh) + except Exception: + return (None, None) + + +def _refresh_user_access_token(refresh_token: str) -> str | None: + """用 refresh_token 刷新得到新的 user access_token(须为本应用 OAuth 颁发的 refresh_token)。""" + # 飞书刷新用户 token:POST,app_id / app_secret / refresh_token / grant_type + url = "https://open.feishu.cn/open-apis/authen/v1/refresh_access_token" + payload = { + "grant_type": "refresh_token", + "refresh_token": refresh_token, + "app_id": FEISHU_APP_ID, + "app_secret": FEISHU_APP_SECRET, + } + try: + r = requests.post(url, json=payload, timeout=10) + data = r.json() + if data.get("code") == 0: + return data.get("data", {}).get("access_token") + except Exception: + pass + return None + + +def get_minutes_token() -> tuple[str | None, str]: + """ + 获取用于调用妙记接口的 token。优先用户身份(可访问个人/空间妙记),否则应用身份。 + 若设置 FEISHU_USE_TENANT_ONLY=1 或传入 --tenant-only,则仅用企业身份(tenant_access_token),不用浏览器/用户 token。 + 返回 (token, "user"|"app"),无 token 时 (None, "app")。 + 用户 token 须为本应用 cli_a48818290ef8100d OAuth 授权所得;若 99991668 可尝试在 feishu_user_token.txt 第二行填 refresh_token 自动刷新。 + """ + # 强制仅用企业身份(应用 tenant_access_token),不读用户 token + if os.environ.get("FEISHU_USE_TENANT_ONLY", "").strip().lower() in ("1", "true", "yes"): + token = get_tenant_access_token() + return (token, "app") + # 1) 环境变量:用户身份 token + user_token = os.environ.get("FEISHU_USER_ACCESS_TOKEN", "").strip() + if user_token and "u-" in user_token: + return (user_token, "user") + # 2) 脚本同目录 feishu_user_token.txt(第一行 access_token,第二行可选 refresh_token) + access, refresh = _read_user_token_file() + if access: + return (access, "user") + # 3) 应用身份 + token = get_tenant_access_token() + return (token, "app") + + def extract_minute_token(url_or_token: str) -> str: """从URL或直接token中提取minute_token""" # 如果是URL,提取token @@ -286,19 +348,36 @@ def fetch_and_save(url_or_token: str, output_dir: Path = None) -> Path: minute_token = extract_minute_token(url_or_token) print(f"📝 妙记Token: {minute_token}") - # 获取token + # 获取 token(优先用户身份,可访问个人/空间妙记;否则应用身份) print("🔑 获取飞书访问令牌...") - token = get_tenant_access_token() + token, token_type = get_minutes_token() if not token: print("❌ 无法获取访问令牌") return None + print(f" 使用: {'用户身份 (可访问个人/空间妙记)' if token_type == 'user' else '应用身份'}") - # 获取妙记信息 + # 获取妙记信息(若仅开通「导出妙记文字」权限可能失败,则仅拉取文字) print("📋 获取妙记基本信息...") info = get_minutes_info(token, minute_token) + # 用户身份 token 无效(99991668)时尝试用 refresh_token 刷新后重试一次 + if not info and token_type == "user": + _, refresh = _read_user_token_file() + if refresh: + print(" ⚠️ 用户 token 可能已过期,尝试刷新...") + new_access = _refresh_user_access_token(refresh) + if new_access: + try: + FEISHU_USER_TOKEN_FILE.write_text( + new_access + "\n" + refresh + "\n", + encoding="utf-8", + ) + except Exception: + pass + token = new_access + info = get_minutes_info(token, minute_token) if not info: - print("❌ 无法获取妙记信息") - return None + print(" ⚠️ 基本信息权限不足(2091005),尝试仅拉取文字记录(需开通「导出妙记转写的文字内容」)...") + info = {"title": f"妙记_{minute_token[:12]}", "duration": 0, "create_time": str(int(datetime.now().timestamp()))} title = info.get("title", "未命名") duration = format_timestamp(int(info.get("duration", 0))) @@ -427,6 +506,7 @@ def main(): parser.add_argument("--file", "-f", type=str, help="从飞书导出的文字记录文件路径") parser.add_argument("--title", type=str, help="指定标题(用于导出文件)") parser.add_argument("--output", "-o", type=str, help="输出目录") + parser.add_argument("--tenant-only", action="store_true", help="仅用企业身份 tenant_access_token(APP_ID+APP_SECRET),不用用户 token") parser.add_argument("--generate", "-g", action="store_true", help="获取后自动生成会议纪要并发送飞书") parser.add_argument("--send-webhook", "-S", action="store_true", @@ -435,6 +515,8 @@ def main(): help=f"飞书机器人 Webhook 地址(默认: 会议纪要群)") args = parser.parse_args() + if getattr(args, "tenant_only", False): + os.environ["FEISHU_USE_TENANT_ONLY"] = "1" if not REQUESTS_AVAILABLE: print("❌ 需要安装 requests: pip install requests") diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/fetch_single_minute_by_cookie.py b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/fetch_single_minute_by_cookie.py new file mode 100644 index 00000000..655ccc16 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/fetch_single_minute_by_cookie.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +""" +用 Cookie 从 cunkebao 拉取单条妙记详情/文字并保存为 TXT(不依赖开放平台妙记权限)。 +需在脚本同目录 cookie_minutes.txt 中粘贴浏览器 Cookie(飞书妙记页 F12→网络→list 请求头)。 +用法: + python3 fetch_single_minute_by_cookie.py "https://cunkebao.feishu.cn/minutes/obcnxrkz6k459k669544228c" --output "/Users/karuo/Documents/聊天记录/soul" +""" +import json +import os +import re +import sys +from pathlib import Path +from datetime import datetime + +try: + import requests +except ImportError: + requests = None + +SCRIPT_DIR = Path(__file__).resolve().parent +COOKIE_FILE = SCRIPT_DIR / "cookie_minutes.txt" +MINUTE_TOKEN = "obcnxrkz6k459k669544228c" +BASE = "https://cunkebao.feishu.cn/minutes/api" + + +def _cookie_from_browser() -> str: + """从本机默认/常用浏览器读取飞书 Cookie(cunkebao 或 .feishu.cn,多浏览器谁有就用谁)。""" + try: + import browser_cookie3 + # 先试子域,再试父域(.feishu.cn 可能带 session) + for domain in ("cunkebao.feishu.cn", "feishu.cn", ".feishu.cn"): + loaders = [ + browser_cookie3.safari, + browser_cookie3.chrome, + browser_cookie3.chromium, + browser_cookie3.firefox, + browser_cookie3.edge, + ] + for loader in loaders: + try: + cj = loader(domain_name=domain) + parts = [f"{c.name}={c.value}" for c in cj] + s = "; ".join(parts) + if len(s) > 100: + return s + except Exception: + continue + except ImportError: + pass + return "" + + +def get_cookie(): + cookie = os.environ.get("FEISHU_MINUTES_COOKIE", "").strip() + if cookie and "PASTE_YOUR" not in cookie: + return cookie + if COOKIE_FILE.exists(): + raw = COOKIE_FILE.read_text(encoding="utf-8", errors="ignore").strip().splitlines() + for line in raw: + line = line.strip() + if line and not line.startswith("#") and "PASTE_YOUR" not in line: + return line + cookie = _cookie_from_browser() + if cookie: + return cookie + return "" + + +def get_csrf(cookie: str) -> str: + for name in ("bv_csrf_token=", "minutes_csrf_token="): + i = cookie.find(name) + if i != -1: + start = i + len(name) + end = cookie.find(";", start) + if end == -1: + end = len(cookie) + return cookie[start:end].strip() + return "" + + +def make_headers(cookie: str, use_github_referer: bool = True): + """与 GitHub bingsanyu/feishu_minutes 一致:meetings 域用 referer minutes/me + bv-csrf-token。""" + # GitHub 使用:referer https://meetings.feishu.cn/minutes/me + referer = "https://meetings.feishu.cn/minutes/me" if use_github_referer else "https://cunkebao.feishu.cn/minutes/" + h = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36", + "cookie": cookie, + "referer": referer, + "content-type": "application/x-www-form-urlencoded", + } + csrf = get_csrf(cookie) + if csrf: + h["bv-csrf-token"] = csrf + return h + + +def try_export_text(cookie: str, minute_token: str) -> tuple[str | None, str | None]: + """ + 调用妙记导出接口(与 GitHub bingsanyu/feishu_minutes 一致): + POST meetings.feishu.cn/minutes/api/export,params: object_token, format=2, add_speaker, add_timestamp。 + 先试 meetings.feishu.cn(推荐),再试 cunkebao.feishu.cn。 + 返回 (标题或 None, 正文文本)。 + """ + payload = { + "object_token": minute_token, + "format": 2, + "add_speaker": "true", + "add_timestamp": "false", + } + # 1) 优先 GitHub 同款:meetings.feishu.cn + referer minutes/me + for domain, use_github in ( + ("https://meetings.feishu.cn/minutes/api/export", True), + ("https://cunkebao.feishu.cn/minutes/api/export", False), + ): + try: + headers = make_headers(cookie, use_github_referer=use_github) + r = requests.post(domain, params=payload, headers=headers, timeout=20) + r.encoding = "utf-8" + if r.status_code != 200: + continue + text = (r.text or "").strip() + if text.startswith("{") and ("transcript" in text or "content" in text or "text" in text): + try: + j = r.json() + text = (j.get("data") or j).get("content") or (j.get("data") or j).get("text") or j.get("transcript") or text + if isinstance(text, str) and len(text) > 50: + return (None, text) + except Exception: + pass + if text and len(text) > 50 and "PASTE_YOUR" not in text and " dict | None: + """尝试多种 cunkebao 可能的详情接口(获取标题等)""" + headers = make_headers(cookie) + urls_to_try = [ + f"{BASE}/minute/detail?object_token={minute_token}", + f"{BASE}/space/minute_detail?object_token={minute_token}", + f"{BASE}/space/list?size=1&space_name=0", + ] + for url in urls_to_try: + try: + r = requests.get(url, headers=headers, timeout=15) + if r.status_code != 200: + continue + data = r.json() if r.text.strip().startswith("{") else {} + if data.get("code") == 0 and data.get("data"): + inner = data.get("data") + if isinstance(inner, list) and inner: + for item in inner: + if (item.get("object_token") or item.get("minute_token")) == minute_token: + return item + return inner + if isinstance(data, dict) and ("topic" in data or "minute" in data): + return data.get("minute") or data + except Exception: + continue + return None + + +def save_txt(output_dir: Path, title: str, body: str, date_str: str = None) -> Path: + date_str = date_str or datetime.now().strftime("%Y%m%d") + safe_title = re.sub(r'[\\/*?:"<>|]', "_", (title or "妙记")) + filename = f"{safe_title}_{date_str}.txt" + path = output_dir / filename + header = f"日期: {date_str}\n标题: {title}\n\n文字记录:\n\n" if title else f"日期: {date_str}\n\n文字记录:\n\n" + path.write_text(header + body, encoding="utf-8") + return path + + +def main(): + if not requests: + print("需要安装 requests: pip install requests") + return 1 + cookie = get_cookie() + if not cookie or "PASTE_YOUR" in cookie: + print("未配置有效 Cookie。请:") + print(" 1. 打开 https://meetings.feishu.cn/minutes/home(或 cunkebao.feishu.cn/minutes/home)并登录") + print(" 2. F12 → 网络 → 找到 list?size=20&space_name= 请求 → 复制请求头中的 Cookie(需含 bv_csrf_token)") + print(" 3. 粘贴到脚本同目录 cookie_minutes.txt 第一行") + print("或使用 GitHub 同款脚本: python3 feishu_minutes_export_github.py <链接> -o <输出目录>") + return 1 + + url_or_token = (sys.argv[1] if len(sys.argv) > 1 else None) or f"https://cunkebao.feishu.cn/minutes/{MINUTE_TOKEN}" + match = re.search(r"/minutes/([a-zA-Z0-9]+)", url_or_token) + minute_token = match.group(1) if match else MINUTE_TOKEN + + output_dir = Path(os.environ.get("FEISHU_MINUTES_OUTPUT", "/Users/karuo/Documents/聊天记录/soul")) + for i, arg in enumerate(sys.argv): + if arg in ("-o", "--output") and i + 1 < len(sys.argv): + output_dir = Path(sys.argv[i + 1]).resolve() + break + output_dir.mkdir(parents=True, exist_ok=True) + + print(f"📝 妙记 token: {minute_token}") + print("📡 使用 Cookie 请求妙记导出接口(export)…") + title_from_export, body = try_export_text(cookie, minute_token) + if body: + # 优先用列表接口拿标题和日期 + data = try_get_minute_detail(cookie, minute_token) + title = (data.get("topic") or data.get("title") or title_from_export or "妙记").strip() + create_time = data.get("create_time") or data.get("share_time") + if create_time: + try: + ts = int(create_time) + if ts > 1e10: + ts = ts // 1000 + date_str = datetime.fromtimestamp(ts).strftime("%Y%m%d") + except Exception: + date_str = datetime.now().strftime("%Y%m%d") + else: + date_str = datetime.now().strftime("%Y%m%d") + path = save_txt(output_dir, title, body, date_str) + print(f"✅ 已保存: {path}") + return 0 + print("❌ 导出接口未返回文字(Cookie 可能失效或非该妙记所属空间)。请:") + print(" 1. 打开 https://cunkebao.feishu.cn/minutes/home 并搜索该场妙记,确认能打开") + print(" 2. F12 → 网络 → 找到 list 或 export 请求 → 复制请求头 Cookie 到 cookie_minutes.txt") + print(" 或到妙记页「…」→ 导出文字记录,将文件保存到输出目录。") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/playwright_104_export.py b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/playwright_104_export.py new file mode 100644 index 00000000..4cdf56de --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/playwright_104_export.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""用 Doubao profile 副本启动浏览器,访问 list 页取 Cookie(含 bv_csrf_token),再请求导出 104 场并保存。""" +import os +import shutil +import subprocess +import sys +from pathlib import Path + +PROFILE_SRC = Path.home() / "Library/Application Support/Doubao/Profile 2" +PROFILE_COPY = Path("/tmp/feishu_doubao_profile_playwright") +OUT_FILE = Path("/Users/karuo/Documents/聊天记录/soul/soul 派对 104场 20260219.txt") +URL_LIST = "https://cunkebao.feishu.cn/minutes/home" +OBJECT_TOKEN = "obcnyg5nj2l8q281v32de6qz" +EXPORT_URL = "https://cunkebao.feishu.cn/minutes/api/export" + + +def main(): + # 1) 复制 profile(排除易锁/大目录) + if PROFILE_COPY.exists(): + shutil.rmtree(PROFILE_COPY, ignore_errors=True) + PROFILE_COPY.mkdir(parents=True, exist_ok=True) + exclude = {"Cache", "Code Cache", "GPUCache", "Session Storage", "Service Worker", "blob_storage", "LOCK"} + for f in PROFILE_SRC.iterdir(): + if f.name in exclude or not f.exists(): + continue + try: + dst = PROFILE_COPY / f.name + if f.is_dir(): + shutil.copytree(f, dst, ignore=shutil.ignore_patterns("Cache", "Code Cache"), dirs_exist_ok=True) + else: + shutil.copy2(f, dst) + except Exception: + pass + (PROFILE_COPY / "LOCK").unlink(missing_ok=True) + + # 2) Playwright 用该 profile 打开 list 页并取 Cookie + try: + from playwright.sync_api import sync_playwright + except ImportError: + print("NO_PLAYWRIGHT", file=sys.stderr) + return 2 + import requests + + with sync_playwright() as p: + try: + context = p.chromium.launch_persistent_context( + user_data_dir=str(PROFILE_COPY), + headless=True, + channel="chromium", + timeout=30000, + args=["--no-sandbox", "--disable-setuid-sandbox"], + ) + except Exception as e: + print("LAUNCH_FAIL", str(e), file=sys.stderr) + return 3 + page = context.pages[0] if context.pages else context.new_page() + # 等待 list 接口返回后再取 Cookie(服务端会在 list 请求时设置 bv_csrf_token) + with page.expect_response(lambda r: "space/list" in r.url or "list" in r.url and r.request.method == "GET") as resp_info: + page.goto(URL_LIST, wait_until="networkidle", timeout=30000) + try: + resp_info.value + except Exception: + pass + page.wait_for_timeout(3000) + cookies = context.cookies() + context.close() + + cookie_str = "; ".join([f"{c['name']}={c['value']}" for c in cookies]) + bv = next((c["value"] for c in cookies if c.get("name") == "bv_csrf_token" and len(c.get("value", "")) == 36), None) + if not cookie_str or len(cookie_str) < 100: + print("NO_COOKIE", file=sys.stderr) + return 4 + + headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Cookie": cookie_str, + "Referer": "https://cunkebao.feishu.cn/minutes/", + } + if bv: + headers["bv-csrf-token"] = bv + r = requests.post( + EXPORT_URL, + params={"object_token": OBJECT_TOKEN, "format": 2, "add_speaker": "true", "add_timestamp": "false"}, + headers=headers, + timeout=20, + ) + r.encoding = "utf-8" + if r.status_code != 200: + print("EXPORT_HTTP", r.status_code, len(r.text), file=sys.stderr) + return 5 + text = (r.text or "").strip() + if not text or len(text) < 100 or "Something went wrong" in text or (text.startswith("{") and "error" in text.lower()): + print("EXPORT_BODY", text[:200], file=sys.stderr) + return 6 + if text.startswith("{"): + try: + j = r.json() + text = (j.get("data") or j.get("content") or "") + if isinstance(text, dict): + text = text.get("content") or text.get("text") or "" + except Exception: + pass + OUT_FILE.parent.mkdir(parents=True, exist_ok=True) + OUT_FILE.write_text("日期: 20260219\n标题: soul 派对 104场 20260219\n\n文字记录:\n\n" + text, encoding="utf-8") + print("OK", str(OUT_FILE)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/soul_minutes_104_104_list.txt b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/soul_minutes_104_104_list.txt new file mode 100644 index 00000000..fbf0d3f0 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/soul_minutes_104_104_list.txt @@ -0,0 +1,3 @@ +# 场次 标题 object_token +# 配置 cookie_minutes.txt 后重新执行本脚本即可只做导出 +104 soul 派对 104场 20260219 obcnyg5nj2l8q281v32de6qz \ No newline at end of file diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/soul_minutes_90_102_list.txt b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/soul_minutes_90_102_list.txt new file mode 100644 index 00000000..ad63fb1d --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/soul_minutes_90_102_list.txt @@ -0,0 +1,19 @@ +# 场次 标题 object_token +# 配置 cookie_minutes.txt 后重新执行本脚本即可只做导出 +90 soul 派对90场 20250203 obcnng3c7z52839e3gt5e945 +91 02:35-08:29 | soul 派对 91场 20260204 obcnoab15us275cq2w65l67p +91 soul 派对 91场 20260204 obcnn63j1a1833c8p3587x47 +92 soul 派对 92场 20260205 obcnou5s4r4ydj2ok8n4w61w +93 soul 派对 93场 20260206 obcnpjlsm1l3yl1989rn36ai +94 soul 派对 94场 20260207 obcnp8hbwqop55772892p3y1 +95 soul 派对 95场 20260209 obcnrkc8bagf1gx2bo4fim9j +96 soul 派对 96场 20260210 obcnr93ne88p8z19777cz8u6 +97 soul 派对 97场 20260211 obcnsyr6p38y4223q5548429 +98 soul 派对 98场 20260212 obcntntr26cj38pp5i1vvdhj +99 soul 派对 99场 20260213 obcnucnci1m2z422987lcb75 +100 soul 派对 100场 20260214 obcnu1rfr452595c6l51a7e1 +100 ip切片 目标:派对51-100场 会员社群200人搭建的视频会议 obcnleg937yi97h4xc311829 +100 ip切片 目标:派对51-100场 会员社群200人搭建的视频会议 obcn82umur66e1f92qtcdews +100 ip切片 目标:派对51-100场 会员社群200人搭建的视频会议 obcntns4t5ufqgo34y57d39k +101 soul 派对 101场 20260216 obcnwd5r563dopj1l3lp5pf1 +102 soul 派对 102场 20260217 obcnw4122sy6yeic79tpzbuo \ No newline at end of file diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/url_single_104.txt b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/url_single_104.txt new file mode 100644 index 00000000..c75d18d8 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/url_single_104.txt @@ -0,0 +1 @@ +https://cunkebao.feishu.cn/minutes/obcnxrkz6k459k669544228c diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/一键104_先开调试再导出.sh b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/一键104_先开调试再导出.sh new file mode 100755 index 00000000..7a707b60 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/一键104_先开调试再导出.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# 一键:先启动豆包浏览器(远程调试),再导出 104 场妙记并打开文件。 +# 会先退出豆包再以调试端口重启,导出完成后可正常再打开豆包使用。 +set -e +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT_FILE="/Users/karuo/Documents/聊天记录/soul/soul 派对 104场 20260219.txt" + +echo "正在退出豆包浏览器…" +osascript -e 'quit app "Doubao"' 2>/dev/null || true +sleep 3 +echo "正在以远程调试方式启动豆包(9222)…" +# 直接调用浏览器可执行文件才能传参;用 nohup 后台运行 +"/Applications/Doubao.app/Contents/Helpers/Doubao Browser.app/Contents/MacOS/Doubao Browser" --remote-debugging-port=9222 & +BGPID=$! +echo "等待豆包就绪(约 12 秒)…" +sleep 12 +# 检查 9222 是否在监听 +if ! nc -z 127.0.0.1 9222 2>/dev/null; then + echo "警告:9222 未就绪,继续尝试导出…" +fi +cd "$SCRIPT_DIR" +if python3 cdp_104_export.py 2>/dev/null; then + echo "✅ 104 场已保存" + open "$OUT_FILE" + exit 0 +fi +echo "导出未成功。请在本机浏览器打开 https://cunkebao.feishu.cn/minutes/obcnyg5nj2l8q281v32de6qz 手动「导出文字记录」后保存到:" +echo " $OUT_FILE" +open "https://cunkebao.feishu.cn/minutes/obcnyg5nj2l8q281v32de6qz" +exit 1 diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/下载104场.sh b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/下载104场.sh new file mode 100755 index 00000000..762fec69 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/下载104场.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# 一键下载 104 场妙记文字到 soul 目录。配置好 cookie_minutes.txt 后执行本脚本即可。 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="/Users/karuo/Documents/聊天记录/soul" +cd "$SCRIPT_DIR" +PY="python3" +[ -x "$SCRIPT_DIR/.venv/bin/python" ] && PY="$SCRIPT_DIR/.venv/bin/python" + +$PY download_soul_minutes_101_to_103.py --from 104 --to 104 +if [ $? -eq 0 ]; then + if ls "$OUT"/soul*104*20260219*.txt 1>/dev/null 2>&1; then + echo "✅ 104 场已保存到: $OUT" + exit 0 + fi +fi +echo "" +echo "未拿到 104 场正文(导出需含 bv_csrf_token 的 Cookie)。请:" +echo " 1. 打开 https://cunkebao.feishu.cn/minutes/home → F12 → 网络 → 找到 list?size=20& 请求" +echo " 2. 复制该请求头中的完整 Cookie → 粘贴到 $SCRIPT_DIR/cookie_minutes.txt 第一行" +echo " 3. 再执行: bash $SCRIPT_DIR/下载104场.sh" +exit 1 diff --git a/02_卡人(水)/水桥_平台对接/智能纪要/脚本/妙记104_企业TOKEN命令行.sh b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/妙记104_企业TOKEN命令行.sh new file mode 100755 index 00000000..c77e0af5 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/智能纪要/脚本/妙记104_企业TOKEN命令行.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# 纯命令行、不用浏览器:用飞书企业身份(tenant_access_token)拉取 104 场妙记并保存到 soul 目录。 +# 凭证:脚本内置 APP_ID/APP_SECRET,或环境变量 FEISHU_APP_ID / FEISHU_APP_SECRET。 +# 需在飞书开放平台为该应用开通「查看妙记文件」「导出妙记转写的文字内容」,且可访问数据范围包含该妙记(或选「全部」)。 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="/Users/karuo/Documents/聊天记录/soul" +cd "$SCRIPT_DIR" +python3 fetch_feishu_minutes.py "https://cunkebao.feishu.cn/minutes/obcnyg5nj2l8q281v32de6qz" --tenant-only --output "$OUT" +exit $? diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/SKILL.md b/02_卡人(水)/水桥_平台对接/飞书管理/SKILL.md index 7310ec46..f4dd53dc 100755 --- a/02_卡人(水)/水桥_平台对接/飞书管理/SKILL.md +++ b/02_卡人(水)/水桥_平台对接/飞书管理/SKILL.md @@ -1,7 +1,7 @@ --- name: 飞书管理 description: 飞书日志/文档自动写入与知识库管理 -triggers: 飞书日志、写入飞书、飞书知识库 +triggers: 飞书日志、写入飞书、飞书知识库、飞书运营报表、派对效果数据、104场写入 owner: 水桥 group: 水 version: "1.0" @@ -186,6 +186,43 @@ python3 /Users/karuo/Documents/个人/卡若AI/02_卡人(水)/飞书管理/s --- +## 飞书运营报表(Soul 派对效果数据) + +将 Soul 派对效果数据按**场次**写入飞书「火:运营报表」表格,**竖列**填入对应日期/场次列。 + +### 填写规则(以后都按此逻辑) + +| 规则 | 说明 | +|:---|:---| +| **按数字填写** | 时长、推流、进房、互动、礼物、灵魂力、增加关注、最高在线 等均按**数字类型**写入,不按文本,便于表格公式与图表 | +| **不填比率三项** | 推流进房率、1分钟进多少人、加微率 由表格内公式自动计算,**导入时不填** | +| **只填前 10 项** | 主题、时长、Soul推流人数、进房人数、人均时长、互动数量、礼物、灵魂力、增加关注、最高在线 | +| **主题(标题)** | 从聊天记录提炼,**≤12 字**,须含**具体内容、干货与数值**(如:号商几毛卖十几 日销两万) | +| **竖列写入** | 表格结构为 A 列指标名、各列为日期/场次,数据写入该场次列的第 3~12 行(竖列) | + +### 一键写入 + +```bash +# 写入 104 场(默认) +python3 /Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/飞书管理/脚本/soul_party_to_feishu_sheet.py + +# 指定场次 +python3 .../soul_party_to_feishu_sheet.py 103 +python3 .../soul_party_to_feishu_sheet.py 104 +``` + +### 表格与配置 + +| 项目 | 值 | +|:---|:---| +| 运营报表 | https://cunkebao.feishu.cn/wiki/wikcnIgAGSNHo0t36idHJ668Gfd?sheet=7A3Cy9 | +| 工作表 | 2026年2月 soul 书卡若创业派对(sheetId=7A3Cy9) | +| Token | 使用同目录 `.feishu_tokens.json`(与 auto_log 共用) | + +新增场次时在脚本内 `ROWS` 中增加对应场次与 10 项数据即可。 + +--- + ## 飞书项目(玩值电竞 · 存客宝) 将玩值电竞 30 天/90 天甘特图任务同步到飞书项目需求管理。 @@ -215,9 +252,10 @@ python3 scripts/wanzhi_feishu_project_sync.py └── scripts/ ├── auto_log.py # 一键日志脚本(推荐) ├── write_today_custom.py # 自定义今日内容写入(写入后自动打开飞书) + ├── soul_party_to_feishu_sheet.py # 飞书运营报表:Soul 派对效果数据(按场次竖列、数字、不填比率) ├── wanzhi_feishu_project_sync.py # 玩值电竞→飞书项目任务同步 ├── feishu_api.py # 后端服务 - ├── feishu_video_clip.py # 视频智能切片(新增) + ├── feishu_video_clip.py # 视频智能切片 ├── feishu_video_clip_README.md # 切片工具说明 └── .feishu_tokens.json # Token存储 ``` diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json index 7b4d39f8..bda2776a 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json @@ -1,6 +1,6 @@ { - "access_token": "u-7FEQ3dsVpbBVy1HPRK6OCul5mUMBk1gNMoaaENM00Byi", - "refresh_token": "ur-58aJUppSxdOFYJhaRlNUaMl5miOBk1UNpEaaIMQ00xD6", + "access_token": "u-60GEtjudJdBVYbF.wQf8NJl5mqW5k1gNhoaaEMQ00wOi", + "refresh_token": "ur-5ie7_NeKh5aEV4nExO0m1nl5kWMBk1gPgUaaIQM00By6", "name": "飞书用户", "auth_time": "2026-02-17T10:27:47.958573" } \ No newline at end of file diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/soul_party_to_feishu_sheet.py b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/soul_party_to_feishu_sheet.py new file mode 100644 index 00000000..b5156660 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/soul_party_to_feishu_sheet.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +""" +飞书运营报表 · Soul 派对效果数据写入(按场次竖列、数字类型、不填比率) +- 只填前 10 项:主题、时长、Soul推流人数、进房人数、人均时长、互动数量、礼物、灵魂力、增加关注、最高在线 +- 推流进房率、1分钟进多少人、加微率 由表格公式自动计算,导入时不填 +- 数值按数字类型写入(非文本),便于表格公式与图表 +""" +import os +import sys +import json +import requests +from urllib.parse import quote + +# 卡若AI 飞书 Token 与 API +FEISHU_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +TOKEN_FILE = os.path.join(FEISHU_SCRIPT_DIR, '.feishu_tokens.json') +WIKI_NODE_OR_SPREADSHEET_TOKEN = os.environ.get('FEISHU_SPREADSHEET_TOKEN', 'wikcnIgAGSNHo0t36idHJ668Gfd') +SHEET_ID = os.environ.get('FEISHU_SHEET_ID', '7A3Cy9') + +# 写入列数:仅前 10 项(比率三项不填,表内公式自动算) +EFFECT_COLS = 10 + +# 各场效果数据(主题≤12字且含干货与数值、时长、推流、进房…)— 比率不写入 +ROWS = { + '96': [ '', 0, 0, 0, 0, 0, 0, 0, 0, 0 ], # 96场(无记录,占位;有数据后替换) + '97': [ '', 0, 0, 0, 0, 0, 0, 0, 0, 0 ], # 97场(无记录,占位) + '98': [ '', 0, 0, 0, 0, 0, 0, 0, 0, 0 ], # 98场(无记录,占位) + '99': [ '', 116, 16976, 208, 0, 0, 4, 166, 12, 39 ], # 99场(派对已关闭截图) + '100': [ '', 0, 0, 0, 0, 0, 0, 0, 0, 0 ], # 100场(无记录,占位) + '103': [ '号商几毛卖十几 日销两万', 155, 46749, 545, 7, 34, 1, 8, 13, 47 ], + '104': [ 'AI创业最赚钱一月分享', 140, 36221, 367, 7, 49, 0, 0, 11, 38 ], +} + + +def load_token(): + if not os.path.exists(TOKEN_FILE): + print('❌ 未找到飞书 Token 文件:', TOKEN_FILE) + print('请先运行一次 auto_log.py 或完成飞书授权。') + return None + with open(TOKEN_FILE, 'r', encoding='utf-8') as f: + data = json.load(f) + return data.get('access_token') + + +def refresh_and_load_token(): + """若 token 过期,尝试用 refresh_token 刷新后返回新 token""" + if not os.path.exists(TOKEN_FILE): + return None + with open(TOKEN_FILE, 'r', encoding='utf-8') as f: + data = json.load(f) + refresh = data.get('refresh_token') + if not refresh: + return data.get('access_token') + app_id = os.environ.get('FEISHU_APP_ID', 'cli_a48818290ef8100d') + app_secret = os.environ.get('FEISHU_APP_SECRET', 'dhjU0qWd5AzicGWTf4cTqhCWJOrnuCk4') + r = requests.post( + 'https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal', + json={'app_id': app_id, 'app_secret': app_secret}, + timeout=10 + ) + app_token = (r.json() or {}).get('app_access_token') + if not app_token: + return data.get('access_token') + r2 = requests.post( + 'https://open.feishu.cn/open-apis/authen/v1/oidc/refresh_access_token', + headers={'Authorization': f'Bearer {app_token}', 'Content-Type': 'application/json'}, + json={'grant_type': 'refresh_token', 'refresh_token': refresh}, + timeout=10 + ) + out = r2.json() + if out.get('code') == 0 and out.get('data', {}).get('access_token'): + new_token = out['data']['access_token'] + data['access_token'] = new_token + data['refresh_token'] = out['data'].get('refresh_token', refresh) + with open(TOKEN_FILE, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + return new_token + return data.get('access_token') + + +def get_sheet_meta(access_token, spreadsheet_token): + """获取表格下的工作表列表,返回第一个 sheet 的 sheet_id(用于 range)""" + url = f'https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/metainfo' + r = requests.get( + url, + headers={'Authorization': f'Bearer {access_token}'}, + timeout=15, + ) + if r.status_code != 200: + return None + body = r.json() + if body.get('code') != 0: + return None + sheets = (body.get('data') or {}).get('sheets') or [] + if not sheets: + return None + return sheets[0].get('sheetId') or sheets[0].get('title') or SHEET_ID + + +def read_sheet_range(access_token, spreadsheet_token, range_str): + """读取表格范围,返回 values 或 None""" + url = f'https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values/{quote(range_str, safe="")}' + r = requests.get( + url, + headers={'Authorization': f'Bearer {access_token}'}, + timeout=15, + ) + if r.status_code != 200: + return None, r.status_code, r.json() + body = r.json() + if body.get('code') != 0: + return None, r.status_code, body + vals = (body.get('data') or {}).get('valueRange', {}).get('values') or [] + return vals, r.status_code, body + + +def write_sheet_row(access_token, spreadsheet_token, sheet_id, values): + """向飞书电子表格追加一行。range 用 sheet_id 或 工作表名""" + url = f'https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values_append' + range_str = f"{sheet_id}!A1:M1" + payload = { + 'valueRange': { + 'range': range_str, + 'values': [values], + }, + 'insertDataOption': 'INSERT_ROWS', + } + r = requests.post( + url, + headers={ + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json', + }, + json=payload, + timeout=15, + ) + return r.status_code, r.json() + + +def update_sheet_range(access_token, spreadsheet_token, range_str, values, value_input_option='RAW'): + """向指定 range 写入/覆盖数据。values 二维数组;value_input_option=RAW 按数字写入不转文本。""" + url = f'https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values' + params = {'valueInputOption': value_input_option} + payload = {'valueRange': {'range': range_str, 'values': values}} + r = requests.put( + url, + params=params, + headers={ + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json', + }, + json=payload, + timeout=15, + ) + try: + body = r.json() + except Exception: + body = {'code': -1, 'msg': (r.text or '')[:200]} + return r.status_code, body + + +def _col_letter(n): + """0->A, 1->B, ..., 25->Z, 26->AA""" + s = '' + while True: + s = chr(65 + n % 26) + s + n = n // 26 + if n <= 0: + break + return s + + +def _to_cell_value(v): + """主题可空/字符串,其余按数字写入(int/float),便于表格公式。""" + if v == '' or v is None: + return '' + if isinstance(v, (int, float)): + return int(v) if isinstance(v, float) and v == int(v) else v + try: + return int(v) + except (ValueError, TypeError): + try: + return float(v) + except (ValueError, TypeError): + return str(v) + + +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') + sys.exit(1) + token = load_token() or refresh_and_load_token() + if not token: + sys.exit(1) + # 只取前 10 项,并按数字类型写入(主题可为空字符串) + raw = (row + [None] * EFFECT_COLS)[:EFFECT_COLS] + values = [_to_cell_value(raw[0])] + [_to_cell_value(raw[i]) for i in range(1, EFFECT_COLS)] + spreadsheet_token = WIKI_NODE_OR_SPREADSHEET_TOKEN + sheet_id = SHEET_ID + range_read = f"{sheet_id}!A1:Z30" + vals, read_code, read_body = read_sheet_range(token, spreadsheet_token, range_read) + # 表格结构:第1行表头(2月、1、19),第2行「一、效果数据」+「104场」在某一列,A列是指标名(主题、时长...),数据填在 104场 那一列的 3~15 行 + target_col_0based = None + if vals and len(vals) >= 2: + for row_idx in (1, 0): # 先查第2行再第1行 + row_cells = vals[row_idx] if row_idx < len(vals) else [] + for col_idx, cell in enumerate(row_cells): + if f"{session}场" in str(cell).strip(): + target_col_0based = col_idx + break + if target_col_0based is not None: + break + if read_code != 200 or (read_body.get('code') not in (0, None)) and not vals: + print('⚠️ 读取表格失败:', read_code, read_body.get('msg', read_body)) + if target_col_0based is not None: + col_letter = _col_letter(target_col_0based) + # 竖列写入:T3:T15 一列,values 为 [[v1],[v2],...[v13]] + range_col = f"{sheet_id}!{col_letter}3:{col_letter}{2 + len(values)}" + values_vertical = [[v] for v in values] + code, body = update_sheet_range(token, spreadsheet_token, range_col, values_vertical) + if code == 200 and body.get('code') == 0: + print(f'✅ 已写入飞书表格:{session}场 效果数据(竖列 {col_letter}3:{col_letter}{2+len(values)},共{len(values)}格)') + return + if code == 401 or body.get('code') in (99991677, 99991663): + token = refresh_and_load_token() + if token: + code, body = update_sheet_range(token, spreadsheet_token, range_col, values_vertical) + if code == 200 and body.get('code') == 0: + print(f'✅ 已写入飞书表格:{session}场 效果数据(竖列 {col_letter})') + return + # 单列多行若报 90202(columns of value>range),则逐格写入 + err = body.get('code') + if err == 90202 or (err and 'range' in str(body.get('msg', '')).lower()): + all_ok = True + for r in range(3, 3 + len(values)): + one_cell = f"{sheet_id}!{col_letter}{r}" + code, body = update_sheet_range(token, spreadsheet_token, one_cell, [[values[r - 3]]]) + if code != 200 or body.get('code') not in (0, None): + if code == 401 or body.get('code') in (99991677, 99991663): + token = refresh_and_load_token() + if token: + code, body = update_sheet_range(token, spreadsheet_token, one_cell, [[values[r - 3]]]) + if code != 200 or body.get('code') not in (0, None): + all_ok = False + print('❌ 写入单元格失败:', one_cell, code, body) + break + if all_ok: + print(f'✅ 已写入飞书表格:{session}场 效果数据(竖列 {col_letter}3:{col_letter}{2+len(values)} 逐格)') + return + print('❌ 按列更新失败:', code, body) + code, body = write_sheet_row(token, spreadsheet_token, sheet_id, values) + if code == 200 and (body.get('code') == 0 or body.get('code') is None): + print(f'✅ 已追加一行:{session}场 效果数据') + return + if code == 401 or body.get('code') in (99991677, 99991663): + token = refresh_and_load_token() + if token: + code, body = write_sheet_row(token, spreadsheet_token, sheet_id, values) + if code == 200 and (body.get('code') == 0 or body.get('code') is None): + print(f'✅ 已追加一行:{session}场 效果数据') + return + print('❌ 写入失败:', code, body) + if body.get('code') in (99991663, 99991677): + print('Token 已过期,请重新授权飞书后再试。') + elif body.get('code') in (403, 404, 1254101): + print('若表格在 Wiki 内,请打开该电子表格→分享→复制链接,链接里 /sheets/ 后的一串为 spreadsheet_token,执行:') + print(' export FEISHU_SPREADSHEET_TOKEN=该token') + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/SKILL_REGISTRY.md b/SKILL_REGISTRY.md index 522decd1..f61a6b8e 100644 --- a/SKILL_REGISTRY.md +++ b/SKILL_REGISTRY.md @@ -50,7 +50,7 @@ | W05 | 需求拆解与计划制定 | 水泉 | 需求拆解、任务分析 | `02_卡人(水)/水泉_规划拆解/需求拆解与计划制定/SKILL.md` | 大需求拆成可执行步骤 | | W06 | 任务规划 | 水泉 | 任务规划、制定计划 | `02_卡人(水)/水泉_规划拆解/任务规划/SKILL.md` | 制定执行计划与排期 | | W07 | 飞书管理 | 水桥 | 飞书日志、写入飞书 | `02_卡人(水)/水桥_平台对接/飞书管理/SKILL.md` | 飞书日志/文档自动化 | -| W08 | 智能纪要 | 水桥 | 会议纪要、产研纪要 | `02_卡人(水)/水桥_平台对接/智能纪要/SKILL.md` | 会议录音转结构化纪要 | +| W08 | 智能纪要 | 水桥 | 会议纪要、产研纪要、**飞书妙记、飞书链接、妙记下载、第几场、指定场次、批量下载妙记、cunkebao.feishu.cn、meetings.feishu.cn/minutes** | `02_卡人(水)/水桥_平台对接/智能纪要/SKILL.md` | 会议录音转结构化纪要;飞书妙记识别与下载(单条/批量),完毕用复盘格式回复 | | W09 | 小程序管理 | 水桥 | 小程序、微信小程序 | `02_卡人(水)/水桥_平台对接/小程序管理/SKILL.md` | 微信小程序发布与维护 | ## 木组 · 卡木(产品内容创造) diff --git a/运营中枢/工作台/00_账号与API索引.md b/运营中枢/工作台/00_账号与API索引.md index 463fa5a7..f4c0c062 100644 --- a/运营中枢/工作台/00_账号与API索引.md +++ b/运营中枢/工作台/00_账号与API索引.md @@ -42,7 +42,8 @@ | 项 | 值 | |----|-----| | APPID | `1251077262` | -| 密钥 | `AKIDjc6yO3nPeOuK2OKsJPBBVbTiiz0aPNHl` | +| SecretId(密钥) | `AKIDjc6yO3nPeOuK2OKsJPBBVbTiiz0aPNHl` | +| SecretKey | (请到 控制台 → 访问管理 → API密钥 获取并填写,用于账单等 API) | ### 阿里云 | 项 | 值 | diff --git a/运营中枢/工作台/gitea_push_log.md b/运营中枢/工作台/gitea_push_log.md index 3e1abeaf..4e79953f 100644 --- a/运营中枢/工作台/gitea_push_log.md +++ b/运营中枢/工作台/gitea_push_log.md @@ -25,3 +25,4 @@ | 2026-02-18 06:29:17 | 🔄 卡若AI 同步 2026-02-18 06:29 | 更新:GitHub Actions、运营中枢工作台 | 排除 >20MB: 5 个 | | 2026-02-18 06:41:56 | 🔄 卡若AI 同步 2026-02-18 06:41 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 5 个 | | 2026-02-18 14:26:12 | 🔄 卡若AI 同步 2026-02-18 14:26 | 更新:运营中枢工作台 | 排除 >20MB: 5 个 | +| 2026-02-18 14:43:00 | 🔄 卡若AI 同步 2026-02-18 14:42 | 更新:运营中枢工作台 | 排除 >20MB: 5 个 | diff --git a/运营中枢/工作台/代码管理.md b/运营中枢/工作台/代码管理.md index 6a0ec7fa..5a4fc177 100644 --- a/运营中枢/工作台/代码管理.md +++ b/运营中枢/工作台/代码管理.md @@ -28,3 +28,4 @@ | 2026-02-18 06:29:17 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-18 06:29 | 更新:GitHub Actions、运营中枢工作台 | 排除 >20MB: 5 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-02-18 06:41:56 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-18 06:41 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 5 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-02-18 14:26:12 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-18 14:26 | 更新:运营中枢工作台 | 排除 >20MB: 5 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | +| 2026-02-18 14:43:00 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-18 14:42 | 更新:运营中枢工作台 | 排除 >20MB: 5 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |