🔄 卡若AI 同步 2026-03-11 20:22 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 11 个

This commit is contained in:
2026-03-11 20:22:50 +08:00
parent a8cfd69137
commit 6eb654a0cf
6 changed files with 548 additions and 0 deletions

View File

@@ -389,6 +389,49 @@ python3 "$SCRIPT_DIR/feishu_minutes_download_video.py" obcnc53697q9mj6h1go6v25e
---
## 🔧 飞书统一认证工具feishu_auth_helper.py
一个脚本统管所有飞书 Token / Cookie替代手动操作
```bash
SCRIPT_DIR="/Users/karuo/Documents/个人/卡若AI/02_卡人/水桥_平台对接/智能纪要/脚本"
# 获取 tenant_access_tokenOpen API 用)
python3 "$SCRIPT_DIR/feishu_auth_helper.py" tenant
# 自动获取最佳 Cookie5 级 fallback自动选最佳来源
python3 "$SCRIPT_DIR/feishu_auth_helper.py" cookie
# 从 Cursor 浏览器刷新 cookie_minutes.txt
python3 "$SCRIPT_DIR/feishu_auth_helper.py" refresh-cookie
# 综合测试tenant + Cookie 对指定妙记)
python3 "$SCRIPT_DIR/feishu_auth_helper.py" test --token obcnc53697q9mj6h1go6v25e
# 生成用户授权链接(重新授权 user_access_token
python3 "$SCRIPT_DIR/feishu_auth_helper.py" auth-url
# 授权码换 token
python3 "$SCRIPT_DIR/feishu_auth_helper.py" exchange --code AUTH_CODE
```
### 通用 Cookie 提取工具cursor_cookie_util.py
可复用于任何需要从 Cursor 浏览器提取 Cookie 的场景
```bash
python3 "$SCRIPT_DIR/cursor_cookie_util.py" feishu # 飞书
python3 "$SCRIPT_DIR/cursor_cookie_util.py" github # GitHub
python3 "$SCRIPT_DIR/cursor_cookie_util.py" --domain .example.com # 自定义
```
```python
from cursor_cookie_util import get_cursor_cookies
cookie = get_cursor_cookies(domains=[".feishu.cn"])
```
---
## 📋 妙记文字 + 视频完整解决方案(一键表)
| 需求 | 脚本 | 一键命令 |

View File

@@ -0,0 +1,101 @@
# 飞书妙记权限申请指南
> 2026-03-11 | 解决 tenant_access_token 调用妙记 API 返回 2091005 的问题
---
## 问题现状
| 调用方式 | 结果 | 原因 |
|:---|:---|:---|
| tenant_access_token + Open API | 2091005 permission deny | 应用未配置妙记 scope |
| Cookie + Web API/minutes/api/export | ✅ 成功 | 走用户 Session绕过应用权限 |
| user_access_token + Open API | 可行(需重新授权) | 用户身份有妙记阅读权限 |
---
## 解决方案 A在开发者后台申请妙记权限推荐
### 步骤
1. 打开飞书开发者后台https://open.feishu.cn/app/cli_a48818290ef8100d
2. 左侧菜单 → **权限管理**
3. 搜索以下权限并申请:
| 权限名称 | scope | 说明 |
|:---|:---|:---|
| 获取妙记信息 | `minutes:minute``minutes:minute:readonly` | 获取妙记标题、时长、封面等 |
| 获取妙记转写内容 | `minutes:transcript``minutes:transcript:readonly` | 获取文字记录正文 |
| 获取妙记统计 | `minutes:statistics:readonly` | 获取统计数据 |
4. 提交审核 → 企业管理员审批通过
5. 通过后tenant_access_token 即可直接调用妙记 Open API
### 验证
```bash
python3 feishu_auth_helper.py test --token obcnc53697q9mj6h1go6v25e
```
---
## 解决方案 B重新获取 user_access_token
当应用权限审批较慢,或需要访问用户个人妙记时:
### 步骤
1. 生成授权链接:
```bash
python3 feishu_auth_helper.py auth-url
```
2. 在浏览器中打开输出的 URL用飞书账号授权
3. 授权后页面会跳转,从 URL 中提取 `code` 参数
4. 用 code 换取 token
```bash
python3 feishu_auth_helper.py exchange --code <URL中的code>
```
5. 获得的 access_token 和 refresh_token 更新到 `运营中枢/工作台/00_账号与API索引.md`
### 注意
- user_access_token 有效期约 2 小时refresh_token 有效期约 30 天
- 需要在飞书开发者后台配置重定向 URI
- 当前应用的 redirect_uri 为 `https://open.feishu.cn`
---
## 解决方案 CCookie 方案(当前使用,已自动化)
无需任何审批5 级自动 fallback
```
cookie_minutes.txt → 环境变量 → browser_cookie3 → Cursor 浏览器 → 手动
```
### 一键刷新
```bash
python3 feishu_auth_helper.py refresh-cookie
```
### 优缺点
- ✅ 无需审批、即时生效
- ✅ 5 级 fallback 高可用
- ❌ Cookie 会过期(通常数小时到数天)
- ❌ 依赖浏览器曾访问过飞书页面
---
## 总结:三种方案优先级
| 优先级 | 方案 | 适用场景 | 稳定性 |
|:---|:---|:---|:---|
| 1 | 应用权限 + tenant_token | 日常自动化、脚本定时任务 | 最高token 自动刷新) |
| 2 | Cookie 5 级 fallback | 妙记文字/视频下载 | 中(需定期刷新) |
| 3 | user_access_token | 特殊场景、全权限 | 低(需手动授权) |

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
Cursor 浏览器 Cookie 提取工具(通用)
从 Cursor IDE 内置浏览器的 SQLite 数据库中提取指定域名的 Cookie。
Cursor 浏览器 Cookie 为明文存储,无需 Keychain 解密。
路径: ~/Library/Application Support/Cursor/Partitions/cursor-browser/Cookies
用法(命令行):
python3 cursor_cookie_util.py feishu # 提取飞书 Cookie
python3 cursor_cookie_util.py github # 提取 GitHub Cookie
python3 cursor_cookie_util.py --domain .example.com # 自定义域名
用法(导入):
from cursor_cookie_util import get_cursor_cookies
cookie_str = get_cursor_cookies(domains=[".feishu.cn", "cunkebao.feishu.cn"])
2026-03-11 created | 卡若AI · 水桥
"""
from __future__ import annotations
import sys
from pathlib import Path
CURSOR_COOKIE_DB = Path.home() / "Library/Application Support/Cursor/Partitions/cursor-browser/Cookies"
PRESET_DOMAINS = {
"feishu": [".feishu.cn", "cunkebao.feishu.cn", "meetings.feishu.cn"],
"github": [".github.com", "github.com"],
"google": [".google.com"],
}
def get_cursor_cookies(domains: list[str] | None = None, as_dict: bool = False) -> str | dict[str, str]:
"""
从 Cursor 浏览器 SQLite 提取指定域名的 Cookie。
Args:
domains: 要匹配的域名列表SQL LIKE 模式。None 则提取所有。
as_dict: True 返回 {name: value}False 返回 "name=value; ..." 字符串。
Returns:
Cookie 字符串 或 字典。失败返回空字符串/空字典。
"""
if not CURSOR_COOKIE_DB.exists():
return {} if as_dict else ""
try:
import sqlite3, shutil, tempfile
tmp = tempfile.mktemp(suffix=".db")
shutil.copy2(CURSOR_COOKIE_DB, tmp)
conn = sqlite3.connect(tmp)
cur = conn.cursor()
if domains:
where_clauses = " OR ".join([f"host_key LIKE '%{d}%'" for d in domains])
sql = f"SELECT host_key, name, value FROM cookies WHERE ({where_clauses}) AND value != ''"
else:
sql = "SELECT host_key, name, value FROM cookies WHERE value != ''"
cur.execute(sql)
rows = cur.fetchall()
conn.close()
Path(tmp).unlink(missing_ok=True)
if not rows:
return {} if as_dict else ""
if as_dict:
return {name: value for _, name, value in rows}
return "; ".join([f"{name}={value}" for _, name, value in rows])
except Exception:
return {} if as_dict else ""
def main():
import argparse
parser = argparse.ArgumentParser(description="Cursor 浏览器 Cookie 提取工具")
parser.add_argument("preset", nargs="?", default="", help=f"预设域名: {', '.join(PRESET_DOMAINS.keys())}")
parser.add_argument("--domain", "-d", action="append", default=[], help="自定义域名(可多次指定)")
parser.add_argument("--dict", action="store_true", help="输出为 JSON dict")
args = parser.parse_args()
domains = args.domain
if args.preset and args.preset in PRESET_DOMAINS:
domains = PRESET_DOMAINS[args.preset]
elif args.preset:
domains = [args.preset]
if not domains:
print(f"用法: python3 {Path(__file__).name} <preset|--domain DOMAIN>")
print(f"预设: {', '.join(PRESET_DOMAINS.keys())}")
return
result = get_cursor_cookies(domains, as_dict=args.dict)
if args.dict:
import json
print(json.dumps(result, ensure_ascii=False, indent=2))
print(f"\n{len(result)} 个 Cookie", file=sys.stderr)
else:
if result:
print(result)
print(f"\n✅ Cookie 长度: {len(result)} chars", file=sys.stderr)
else:
print("❌ 未找到匹配的 Cookie", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,289 @@
#!/usr/bin/env python3
"""
飞书统一认证工具 — 一个脚本搞定所有飞书 Token / Cookie
功能:
1. tenant_access_tokenAPP_ID + APP_SECRET适用 Open API
2. user_access_tokenOAuth 授权码换取,适用用户资源如妙记)
3. Cookie 自动获取链5 级 fallback适用妙记 Web API
4. 一键刷新 cookie_minutes.txt
用法:
# 获取 tenant_access_token
python3 feishu_auth_helper.py tenant
# 生成用户授权链接(需在浏览器打开授权后取 code
python3 feishu_auth_helper.py auth-url
# 用授权码换取 user_access_token
python3 feishu_auth_helper.py exchange --code AUTH_CODE_HERE
# 自动获取最佳可用 Cookie5 级 fallback
python3 feishu_auth_helper.py cookie
# 刷新 cookie_minutes.txt从 Cursor 浏览器提取)
python3 feishu_auth_helper.py refresh-cookie
# 测试当前 Cookie 是否有效
python3 feishu_auth_helper.py test --token obcnc53697q9mj6h1go6v25e
2026-03-11 created
"""
from __future__ import annotations
import argparse, json, os, re, sys, time
from pathlib import Path
try:
import requests
except ImportError:
sys.exit("请安装 requests: pip3 install requests")
SCRIPT_DIR = Path(__file__).resolve().parent
COOKIE_FILE = SCRIPT_DIR / "cookie_minutes.txt"
APP_ID = "cli_a48818290ef8100d"
APP_SECRET = "dhjU0qWd5AzicGWTf4cTqhCWJOrnuCk4"
REDIRECT_URI = "https://open.feishu.cn"
# ─── 1. tenant_access_token ───
def get_tenant_token() -> str:
r = requests.post("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": APP_ID, "app_secret": APP_SECRET}, timeout=10)
data = r.json()
if data.get("code") == 0:
return data["tenant_access_token"]
print(f"获取 tenant_token 失败: {data}", file=sys.stderr)
return ""
# ─── 2. user_access_token (OAuth) ───
def build_auth_url(scope: str = "") -> str:
base = f"https://accounts.feishu.cn/open-apis/authen/v1/authorize?app_id={APP_ID}&redirect_uri={REDIRECT_URI}&response_type=code&state=karuo_ai_{int(time.time())}"
if scope:
base += f"&scope={scope}"
return base
def exchange_code_for_token(code: str) -> dict:
tenant = get_tenant_token()
if not tenant:
return {"error": "无法获取 tenant_token"}
r = requests.post("https://open.feishu.cn/open-apis/authen/v1/oidc/access_token",
headers={"Authorization": f"Bearer {tenant}", "Content-Type": "application/json"},
json={"grant_type": "authorization_code", "code": code}, timeout=10)
return r.json()
def refresh_user_token(refresh_token: str) -> dict:
tenant = get_tenant_token()
if not tenant:
return {"error": "无法获取 tenant_token"}
r = requests.post("https://open.feishu.cn/open-apis/authen/v1/oidc/refresh_access_token",
headers={"Authorization": f"Bearer {tenant}", "Content-Type": "application/json"},
json={"grant_type": "refresh_token", "refresh_token": refresh_token}, timeout=10)
return r.json()
# ─── 3. Cookie 5 级 fallback ───
def get_cookie_from_file() -> str:
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
return ""
def get_cookie_from_env() -> str:
c = os.environ.get("FEISHU_MINUTES_COOKIE", "").strip()
return c if c and len(c) > 100 and "PASTE_YOUR" not in c else ""
def get_cookie_from_browser() -> str:
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.edge, browser_cookie3.firefox):
try:
cj = loader(domain_name=domain)
s = "; ".join([f"{c.name}={c.value}" for c in cj])
if len(s) > 100:
return s
except Exception:
continue
except ImportError:
pass
return ""
def get_cookie_from_cursor() -> str:
"""从 Cursor 内置浏览器 SQLite 提取飞书 Cookie明文无需解密"""
try:
import sqlite3, shutil, tempfile
cookie_path = Path.home() / "Library/Application Support/Cursor/Partitions/cursor-browser/Cookies"
if not cookie_path.exists():
return ""
tmp = tempfile.mktemp(suffix=".db")
shutil.copy2(cookie_path, tmp)
conn = sqlite3.connect(tmp)
cur = conn.cursor()
cur.execute("SELECT name, value FROM cookies WHERE (host_key LIKE '%feishu%' OR host_key LIKE '%cunkebao%') AND value != ''")
rows = cur.fetchall()
conn.close()
Path(tmp).unlink(missing_ok=True)
if rows:
s = "; ".join([f"{n}={v}" for n, v in rows])
if len(s) > 100:
return s
except Exception:
pass
return ""
def get_best_cookie(verbose: bool = False) -> str:
"""5 级 fallback 获取最佳可用 Cookie"""
sources = [
("cookie_minutes.txt", get_cookie_from_file),
("环境变量", get_cookie_from_env),
("本机浏览器", get_cookie_from_browser),
("Cursor 浏览器", get_cookie_from_cursor),
]
for name, fn in sources:
c = fn()
if c:
if verbose:
print(f"✅ Cookie 来源: {name} ({len(c)} chars)")
return c
elif verbose:
print(f"{name}: 无有效 Cookie")
return ""
def extract_bv_csrf(cookie: str) -> str:
for key in ("bv_csrf_token=", "minutes_csrf_token="):
i = cookie.find(key)
if i != -1:
start = i + len(key)
end = cookie.find(";", start)
if end == -1:
end = len(cookie)
val = cookie[start:end].strip()
if len(val) == 36:
return val
return ""
# ─── 4. 测试 ───
def test_cookie(cookie: str, object_token: str) -> bool:
bv = extract_bv_csrf(cookie)
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"Cookie": cookie,
"Referer": "https://cunkebao.feishu.cn/minutes/",
}
if bv:
headers["bv-csrf-token"] = bv
r = requests.get(
f"https://cunkebao.feishu.cn/minutes/api/status?object_token={object_token}&language=zh_cn&_t={int(time.time()*1000)}",
headers=headers, timeout=15)
if r.status_code == 200:
data = r.json()
if data.get("code") == 0:
topic = data.get("data", {}).get("topic", "")
video = bool(data.get("data", {}).get("video_info", {}).get("video_download_url"))
print(f"✅ Cookie 有效 | 标题: {topic} | 视频: {'' if video else ''}")
return True
print(f"❌ API 返回错误: code={data.get('code')}, msg={data.get('msg')}")
else:
print(f"❌ HTTP {r.status_code}: {r.text[:200]}")
return False
def test_tenant(object_token: str) -> bool:
token = get_tenant_token()
if not token:
return False
r = requests.get(f"https://open.feishu.cn/open-apis/minutes/v1/minutes/{object_token}",
headers={"Authorization": f"Bearer {token}"}, timeout=10)
data = r.json()
if data.get("code") == 0:
print(f"✅ tenant_token 可访问妙记")
return True
print(f"❌ tenant_token: code={data.get('code')}, msg={data.get('msg')}")
return False
# ─── CLI ───
def main():
parser = argparse.ArgumentParser(description="飞书统一认证工具")
sub = parser.add_subparsers(dest="cmd")
sub.add_parser("tenant", help="获取 tenant_access_token")
sub.add_parser("auth-url", help="生成用户授权链接")
p_ex = sub.add_parser("exchange", help="授权码换 user_access_token")
p_ex.add_argument("--code", required=True)
p_rf = sub.add_parser("refresh", help="刷新 user_access_token")
p_rf.add_argument("--token", required=True, help="refresh_token")
sub.add_parser("cookie", help="自动获取最佳 Cookie5 级 fallback")
sub.add_parser("refresh-cookie", help="从 Cursor 浏览器刷新 cookie_minutes.txt")
p_test = sub.add_parser("test", help="测试 Cookie / tenant_token 是否有效")
p_test.add_argument("--token", default="obcnc53697q9mj6h1go6v25e", help="妙记 object_token")
args = parser.parse_args()
if args.cmd == "tenant":
t = get_tenant_token()
if t:
print(f"✅ tenant_access_token: {t}")
return
if args.cmd == "auth-url":
url = build_auth_url()
print(f"请在浏览器中打开以下链接授权:\n{url}")
print(f"\n授权后,从重定向 URL 中提取 code 参数,执行:")
print(f" python3 {Path(__file__).name} exchange --code <CODE>")
return
if args.cmd == "exchange":
result = exchange_code_for_token(args.code)
print(json.dumps(result, ensure_ascii=False, indent=2))
data = result.get("data", {})
if data.get("access_token"):
print(f"\n✅ user_access_token: {data['access_token']}")
print(f" refresh_token: {data.get('refresh_token', 'N/A')}")
print(f" expires_in: {data.get('expires_in', 'N/A')}s")
return
if args.cmd == "refresh":
result = refresh_user_token(args.token)
print(json.dumps(result, ensure_ascii=False, indent=2))
return
if args.cmd == "cookie":
c = get_best_cookie(verbose=True)
if c:
print(f"\nCookie 长度: {len(c)} chars")
print(f"bv_csrf_token: {extract_bv_csrf(c) or '未找到'}")
else:
print("❌ 所有 Cookie 来源均无有效 Cookie")
return
if args.cmd == "refresh-cookie":
c = get_cookie_from_cursor()
if c:
COOKIE_FILE.write_text(c + f"\n# Cursor 浏览器自动提取 {time.strftime('%Y-%m-%d %H:%M:%S')}\n", encoding="utf-8")
print(f"✅ cookie_minutes.txt 已更新 ({len(c)} chars)")
else:
print("❌ Cursor 浏览器中无飞书 Cookie")
return
if args.cmd == "test":
print("=== 测试 tenant_access_token ===")
test_tenant(args.token)
print("\n=== 测试 Cookie ===")
c = get_best_cookie(verbose=True)
if c:
test_cookie(c, args.token)
return
parser.print_help()
if __name__ == "__main__":
main()