🔄 卡若AI 同步 2026-02-19 11:54 | 更新:总索引与入口、金仓、水桥平台对接、运营中枢工作台 | 排除 >20MB: 5 个
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,6 +18,9 @@ __pycache__/
|
||||
.env.*
|
||||
*.log
|
||||
sync_tokens.env
|
||||
# 飞书妙记(用户 token / Cookie,勿提交)
|
||||
**/智能纪要/脚本/feishu_user_token.txt
|
||||
**/智能纪要/脚本/cookie_minutes.txt
|
||||
|
||||
# Node / 前端
|
||||
node_modules/
|
||||
|
||||
143
01_卡资(金)/金仓_存储备份/服务器管理/scripts/tencent_cloud_bill_recent_days.py
Normal file
143
01_卡资(金)/金仓_存储备份/服务器管理/scripts/tencent_cloud_bill_recent_days.py
Normal file
@@ -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))
|
||||
@@ -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** | ✅ 配置飞书凭证,支持自动发送图片 |
|
||||
|
||||
152
02_卡人(水)/水桥_平台对接/智能纪要/参考资料/飞书妙记下载-权限与排查说明.md
Normal file
152
02_卡人(水)/水桥_平台对接/智能纪要/参考资料/飞书妙记下载-权限与排查说明.md
Normal file
@@ -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/<minute_token>" --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/<minute_token>" --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`
|
||||
91
02_卡人(水)/水桥_平台对接/智能纪要/脚本/cdp_104_export.py
Normal file
91
02_卡人(水)/水桥_平台对接/智能纪要/脚本/cdp_104_export.py
Normal file
@@ -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())
|
||||
7
02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_101_to_103.sh
Normal file
7
02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_101_to_103.sh
Normal file
@@ -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 "$@"
|
||||
42
02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_104_to_soul.sh
Executable file
42
02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_104_to_soul.sh
Executable file
@@ -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
|
||||
354
02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_soul_minutes_101_to_103.py
Normal file
354
02_卡人(水)/水桥_平台对接/智能纪要/脚本/download_soul_minutes_101_to_103.py
Normal file
@@ -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 "<html" in text.lower()[:100] or "Something went wrong" in text:
|
||||
continue
|
||||
return text
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
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 "妙记"))
|
||||
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())
|
||||
190
02_卡人(水)/水桥_平台对接/智能纪要/脚本/feishu_minutes_export_github.py
Normal file
190
02_卡人(水)/水桥_平台对接/智能纪要/脚本/feishu_minutes_export_github.py
Normal file
@@ -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 "<html" in text.lower()[:100] or "Something went wrong" in text:
|
||||
return None
|
||||
return text
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def extract_token_from_url(url_or_token: str) -> 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())
|
||||
@@ -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")
|
||||
|
||||
228
02_卡人(水)/水桥_平台对接/智能纪要/脚本/fetch_single_minute_by_cookie.py
Normal file
228
02_卡人(水)/水桥_平台对接/智能纪要/脚本/fetch_single_minute_by_cookie.py
Normal file
@@ -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 "<html" not in text.lower()[:200] and "Something went wrong" not in text:
|
||||
return (None, text)
|
||||
except Exception:
|
||||
continue
|
||||
return (None, None)
|
||||
|
||||
|
||||
def try_get_minute_detail(cookie: str, minute_token: str) -> 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())
|
||||
110
02_卡人(水)/水桥_平台对接/智能纪要/脚本/playwright_104_export.py
Normal file
110
02_卡人(水)/水桥_平台对接/智能纪要/脚本/playwright_104_export.py
Normal file
@@ -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())
|
||||
3
02_卡人(水)/水桥_平台对接/智能纪要/脚本/soul_minutes_104_104_list.txt
Normal file
3
02_卡人(水)/水桥_平台对接/智能纪要/脚本/soul_minutes_104_104_list.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# 场次 标题 object_token
|
||||
# 配置 cookie_minutes.txt 后重新执行本脚本即可只做导出
|
||||
104 soul 派对 104场 20260219 obcnyg5nj2l8q281v32de6qz
|
||||
19
02_卡人(水)/水桥_平台对接/智能纪要/脚本/soul_minutes_90_102_list.txt
Normal file
19
02_卡人(水)/水桥_平台对接/智能纪要/脚本/soul_minutes_90_102_list.txt
Normal file
@@ -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
|
||||
1
02_卡人(水)/水桥_平台对接/智能纪要/脚本/url_single_104.txt
Normal file
1
02_卡人(水)/水桥_平台对接/智能纪要/脚本/url_single_104.txt
Normal file
@@ -0,0 +1 @@
|
||||
https://cunkebao.feishu.cn/minutes/obcnxrkz6k459k669544228c
|
||||
30
02_卡人(水)/水桥_平台对接/智能纪要/脚本/一键104_先开调试再导出.sh
Executable file
30
02_卡人(水)/水桥_平台对接/智能纪要/脚本/一键104_先开调试再导出.sh
Executable file
@@ -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
|
||||
21
02_卡人(水)/水桥_平台对接/智能纪要/脚本/下载104场.sh
Executable file
21
02_卡人(水)/水桥_平台对接/智能纪要/脚本/下载104场.sh
Executable file
@@ -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
|
||||
9
02_卡人(水)/水桥_平台对接/智能纪要/脚本/妙记104_企业TOKEN命令行.sh
Executable file
9
02_卡人(水)/水桥_平台对接/智能纪要/脚本/妙记104_企业TOKEN命令行.sh
Executable file
@@ -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 $?
|
||||
@@ -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存储
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
274
02_卡人(水)/水桥_平台对接/飞书管理/脚本/soul_party_to_feishu_sheet.py
Normal file
274
02_卡人(水)/水桥_平台对接/飞书管理/脚本/soul_party_to_feishu_sheet.py
Normal file
@@ -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()
|
||||
@@ -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` | 微信小程序发布与维护 |
|
||||
|
||||
## 木组 · 卡木(产品内容创造)
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
| 项 | 值 |
|
||||
|----|-----|
|
||||
| APPID | `1251077262` |
|
||||
| 密钥 | `AKIDjc6yO3nPeOuK2OKsJPBBVbTiiz0aPNHl` |
|
||||
| SecretId(密钥) | `AKIDjc6yO3nPeOuK2OKsJPBBVbTiiz0aPNHl` |
|
||||
| SecretKey | (请到 控制台 → 访问管理 → API密钥 获取并填写,用于账单等 API) |
|
||||
|
||||
### 阿里云
|
||||
| 项 | 值 |
|
||||
|
||||
@@ -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 个 |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
Reference in New Issue
Block a user