feat: finalize bilibili cli workflow

This commit is contained in:
home-dev-pookz
2026-03-25 04:29:35 +08:00
parent 03953df4ec
commit 0778e29a04
21 changed files with 99 additions and 1348 deletions
+1
View File
@@ -188,3 +188,4 @@ cookiesFile
uploadFile
videoFile
/uploader/bilibili_uploader/qrcode.png
+1
View File
@@ -116,6 +116,7 @@ sau bilibili upload-video --account creator --file videos/demo.mp4 --title "示
- Bilibili CLI 不要求用户手动安装 `biliup`
- 首次运行相关命令时,程序会自动下载 `biliup`
- 后续运行会自动检查上游 release 并更新
- Bilibili 登录建议由用户自己在本地真实终端里执行;如果终端二维码显示不完整,可以直接打开当前目录下的 `qrcode.png` 扫码
### 方式 2:使用 examples
+2
View File
@@ -71,6 +71,7 @@ sau bilibili upload-video --account creator --file videos/demo.mp4 --title "示
- `sau bilibili ...` 会自动准备 `biliup`
- 如果本地没有 `biliup`,第一次运行会自动下载
- 如果上游 GitHub Release 有更新,运行时会先自动更新
- `sau bilibili login --account <name>` 建议由用户自己在本地真实终端里执行;如果终端里的二维码显示不完整,可直接打开当前目录下的 `qrcode.png` 扫码
## 登录二维码说明
@@ -78,6 +79,7 @@ sau bilibili upload-video --account creator --file videos/demo.mp4 --title "示
- 对普通用户来说,可以直接打开该图片扫码
- 对可操作本地文件的 agent 来说,不要只把图片路径告诉用户
- 这类二维码图片本身就是给用户扫码的,agent 应优先直接展示/发送本地图片给用户
- Bilibili 当前不走这套本地二维码图片托管链路,登录按上面的 Bilibili CLI 说明处理即可
## 定时发布
+6
View File
@@ -122,6 +122,10 @@ sau bilibili upload-video --account creator --file videos/demo.mp4 --title "示
- 用户不需要手动安装 `biliup`
- 首次运行 Bilibili 相关命令时,程序会自动下载 `biliup`
- 后续运行会自动检查上游 release 并自动更新
- Bilibili 登录建议由用户自己在本地真实终端里执行;如果终端里的二维码显示不完整,可直接打开当前目录下的 `qrcode.png` 扫码
- 如果国内网络访问 GitHub Release 较慢,可先用 `https://gh-proxy.com/` 或 `https://gh-proxy.org/` 辅助访问对应 release 地址排障
- 示例:
- `https://gh-proxy.org/https://github.com/biliup/biliup/releases/download/v1.1.29/biliupR-v1.1.29-aarch64-linux.tar.xz`
## For AI Agents
@@ -190,6 +194,8 @@ sau bilibili upload-video
- 当登录流程生成本地二维码图片时,不要只把图片路径发给用户
- 这类二维码图片本身就是给用户扫码的,agent 应优先直接展示/发送本地图片给用户扫码
- 如果环境支持查看本地图片,优先用查看图片能力把二维码展示出来;路径只作为补充信息
- Bilibili 登录当前不建议 agent 在非交互环境里直接代跑
- 正确做法是让用户自己在本地终端执行 `sau bilibili login --account <name>`;如果二维码显示不完整,再提示用户打开 `qrcode.png`
- `requirements.txt` 目前是历史兼容文件,不是主安装入口
- `uploader/` 是核心实现目录
- `sau_cli.py` 是当前 CLI 主入口
+1
View File
@@ -114,3 +114,4 @@ sau bilibili check --account test
- Web 相关内容属于历史路径,见 `docs/legacy-web.md`
- 如果更新后的登录流程输出了本地二维码图片,agent 应直接把图片展示/发送给用户扫码,不要只回传图片路径
- Bilibili 命令运行时会自动检查并更新 `biliup`
- Bilibili 登录当前仍建议让用户自己在本地真实终端里执行;如果二维码显示不完整,可让用户直接打开 `qrcode.png` 扫码
+2
View File
@@ -8,6 +8,8 @@ from conf import BASE_DIR
if __name__ == "__main__":
# 建议直接在本地真实终端里运行这个脚本。
# 如果终端里的二维码显示不完整,可以打开当前目录下的 qrcode.png 扫码。
cli_path = Path(BASE_DIR) / "sau_cli.py"
subprocess.run(
[
-4
View File
@@ -21,10 +21,6 @@ dependencies = [
sau = "sau_cli:main"
[project.optional-dependencies]
bilibili = [
"biliup==0.4.98",
]
web = [
"Flask[async]==3.1.1",
"flask-cors==6.0.0",
+16 -1
View File
@@ -93,6 +93,10 @@ class BilibiliVideoUploadRequest:
publish_date: datetime | int
def has_interactive_terminal() -> bool:
return sys.stdin.isatty() and sys.stdout.isatty()
def resolve_runtime_home() -> Path:
return Path(BASE_DIR)
@@ -151,7 +155,18 @@ async def check_kuaishou_account(account_name: str) -> bool:
async def login_bilibili_account(account_name: str) -> dict:
account_file = resolve_account_file("bilibili", account_name)
result = run_biliup_command(["-u", str(account_file), "login"])
if not has_interactive_terminal():
return {
"success": False,
"message": (
"Bilibili login requires a local interactive terminal. "
f"Please run `sau bilibili login --account {account_name}` yourself in a local terminal. "
"If the terminal QR code does not render completely, open `./qrcode.png` and scan that image."
),
"account_file": str(account_file),
}
result = run_biliup_command(["-u", str(account_file), "login"], interactive=True)
success = result.returncode == 0
return {
"success": success,
+5 -2
View File
@@ -14,7 +14,7 @@ description: 当 agent 需要通过已安装的 `sau` CLI 完成 Bilibili 登录
| 功能 | 命令入口 | 说明 |
| --- | --- | --- |
| 登录 | `sau bilibili login --account <name>` | 生成或刷新指定账号的登录信息 |
| 登录 | `sau bilibili login --account <name>` | 需要用户自己在本地真实终端里执行,用于生成或刷新登录信息 |
| 校验 | `sau bilibili check --account <name>` | 检查指定账号当前是否有效 |
| 视频上传 | `sau bilibili upload-video ...` | 上传一条 Bilibili 视频 |
@@ -27,7 +27,7 @@ description: 当 agent 需要通过已安装的 `sau` CLI 完成 Bilibili 登录
## 命令选择建议
- 用户没有登录信息,先用 `login`
- 用户没有登录信息,先让用户自己在本地终端执行 `login`
- 用户只想确认账号状态,先用 `check`
- 用户要发视频,用 `upload-video`
@@ -37,6 +37,9 @@ description: 当 agent 需要通过已安装的 `sau` CLI 完成 Bilibili 登录
- 如果 `sau` 不在 PATH 中,可以用仓库里的 `sau_cli.py`
- 不要要求用户手动下载 `biliup`
- 第一次运行 Bilibili 命令时,程序可能会自动联网准备 `biliup`
- 对 agent 来说,不要在非交互环境里硬跑 `sau bilibili login`
- 正确做法是让用户自己在本地终端执行 `sau bilibili login --account <name>`
- 如果终端里的二维码显示不完整,提醒用户直接打开当前目录下的 `qrcode.png` 扫码
## 模板文件
@@ -15,6 +15,10 @@ sau bilibili login --account <account>
- 作用:
- 自动准备 `biliup`
- 触发 Bilibili 登录流程
- 登录方式说明:
- 这个命令应该由用户自己在本地真实终端里运行
- 如果终端二维码显示不完整,可直接打开当前目录下的 `qrcode.png` 扫码
- agent 不应该在非交互环境里硬跑这个命令
### 校验账号
@@ -42,3 +42,8 @@ python sau_cli.py bilibili --help
- 首次运行可能比其他平台更慢,因为要自动准备 `biliup`
- 如果网络无法访问 GitHub Release,Bilibili 命令会失败
- 一旦本地已经准备好 `biliup`,后续命令会直接复用
- `sau bilibili login --account <name>` 需要用户自己在本地真实终端里执行
- 如果终端二维码显示不完整,通常可以直接打开当前目录下的 `qrcode.png` 扫码
- 如果国内网络访问 GitHub Release 较慢,可先用 `https://gh-proxy.com/` 或 `https://gh-proxy.org/` 辅助访问对应 release 地址排障
- 示例:
- `https://gh-proxy.org/https://github.com/biliup/biliup/releases/download/v1.1.29/biliupR-v1.1.29-aarch64-linux.tar.xz`
@@ -13,6 +13,9 @@
- 当前网络是否能访问 GitHub
- GitHub Release 是否可访问
- 本地目录是否有写权限
- 国内网络较慢时,可先用 `https://gh-proxy.com/` 或 `https://gh-proxy.org/` 辅助访问对应 release 地址排障
- 示例:
- `https://gh-proxy.org/https://github.com/biliup/biliup/releases/download/v1.1.29/biliupR-v1.1.29-aarch64-linux.tar.xz`
## 3. `check` 返回 `invalid`
@@ -28,7 +31,24 @@
sau bilibili login --account <account>
```
## 4. 上传失败
## 4. 登录时报 `not a terminal`
常见原因:
- 你是在非交互环境里触发了 `sau bilibili login`
- 例如 agent 的命令执行器、管道环境、被接管标准输出的进程
建议:
- 改成由用户自己在本地真实终端里执行:
```bash
sau bilibili login --account <account>
```
- 如果终端里的二维码显示不完整,直接打开当前目录下的 `qrcode.png` 扫码
## 5. 上传失败
优先检查:
@@ -37,7 +57,7 @@ sau bilibili login --account <account>
- 标题、简介、标签是否符合平台要求
- 当前登录信息是否仍然有效
## 5. 上游更新后行为变化
## 6. 上游更新后行为变化
当前 Bilibili 集成会自动跟随上游 `biliup` 最新 release。
@@ -1,4 +1,6 @@
# 登录
# 建议由用户自己在本地真实终端里执行。
# 如果终端二维码显示不完整,可以打开当前目录下的 qrcode.png 扫码。
sau bilibili login --account creator
# 校验
@@ -1,4 +1,6 @@
# login
# Run this in a local interactive terminal.
# If the terminal QR code is incomplete, open ./qrcode.png and scan that image.
sau bilibili login --account creator
# check
+16 -2
View File
@@ -1,4 +1,5 @@
import unittest
from pathlib import Path
from unittest.mock import Mock, patch
from uploader.bilibili_uploader.runtime import (
@@ -23,8 +24,10 @@ class BiliupRuntimeTests(unittest.TestCase):
with patch("uploader.bilibili_uploader.runtime.read_local_biliup_version", return_value=None):
with patch("pathlib.Path.exists", return_value=False):
with patch("uploader.bilibili_uploader.runtime.download_biliup_asset") as mock_download:
ensure_biliup_binary(force_check=True)
with patch("uploader.bilibili_uploader.runtime.write_local_biliup_version") as mock_write_version:
ensure_biliup_binary(force_check=True)
mock_download.assert_called_once()
mock_write_version.assert_called_once_with("v1.0.0")
@patch("uploader.bilibili_uploader.runtime.fetch_latest_release")
def test_ensure_biliup_binary_reuses_local_when_up_to_date(self, mock_release):
@@ -36,8 +39,10 @@ class BiliupRuntimeTests(unittest.TestCase):
with patch("uploader.bilibili_uploader.runtime.read_local_biliup_version", return_value="v1.0.0"):
with patch("pathlib.Path.exists", return_value=True):
with patch("uploader.bilibili_uploader.runtime.download_biliup_asset") as mock_download:
ensure_biliup_binary(force_check=True)
with patch("uploader.bilibili_uploader.runtime.write_local_biliup_version") as mock_write_version:
ensure_biliup_binary(force_check=True)
mock_download.assert_not_called()
mock_write_version.assert_not_called()
@patch("uploader.bilibili_uploader.runtime.subprocess.run")
@patch("uploader.bilibili_uploader.runtime.ensure_biliup_binary")
@@ -46,3 +51,12 @@ class BiliupRuntimeTests(unittest.TestCase):
mock_run.return_value = Mock(returncode=0, stdout="ok", stderr="")
result = run_biliup_command(["login"])
self.assertEqual(result.returncode, 0)
@patch("uploader.bilibili_uploader.runtime.subprocess.run")
@patch("uploader.bilibili_uploader.runtime.ensure_biliup_binary")
def test_run_biliup_command_login_uses_interactive_stdio(self, mock_ensure_binary, mock_run):
mock_ensure_binary.return_value = Path("C:/mock/biliup.exe")
mock_run.return_value = Mock(returncode=0, stdout="", stderr="")
run_biliup_command(["login"], interactive=True)
_, kwargs = mock_run.call_args
self.assertNotIn("capture_output", kwargs)
+7
View File
@@ -36,3 +36,10 @@ class BilibiliCliTests(unittest.TestCase):
with patch("sau_cli.check_bilibili_account", new=AsyncMock(return_value=True)):
code = asyncio.run(sau_cli.dispatch(args))
self.assertEqual(code, 0)
def test_login_bilibili_account_returns_friendly_message_without_terminal(self):
with patch("sau_cli.has_interactive_terminal", return_value=False):
result = asyncio.run(sau_cli.login_bilibili_account("creator"))
self.assertFalse(result["success"])
self.assertIn("local interactive terminal", result["message"].lower())
self.assertIn("qrcode.png", result["message"].lower())
+1 -5
View File
@@ -1,5 +1 @@
from pathlib import Path
from conf import BASE_DIR
Path(BASE_DIR / "cookies" / "bilibili_uploader").mkdir(exist_ok=True)
"""Bilibili uploader package."""
Binary file not shown.
-79
View File
@@ -1,79 +0,0 @@
import json
import pathlib
import random
from biliup.plugins.bili_webup import BiliBili, Data
from utils.log import bilibili_logger
def extract_keys_from_json(data):
"""Extract specified keys from the provided JSON data."""
keys_to_extract = ["SESSDATA", "bili_jct", "DedeUserID__ckMd5", "DedeUserID", "access_token"]
extracted_data = {}
# Extracting cookie data
for cookie in data['cookie_info']['cookies']:
if cookie['name'] in keys_to_extract:
extracted_data[cookie['name']] = cookie['value']
# Extracting access_token
if "access_token" in data['token_info']:
extracted_data['access_token'] = data['token_info']['access_token']
return extracted_data
def read_cookie_json_file(filepath: pathlib.Path):
with open(filepath, 'r', encoding='utf-8') as file:
content = json.load(file)
return content
def random_emoji():
emoji_list = ["🍏", "🍎", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🍈", "🍒", "🍑", "🍍", "🥭", "🥥", "🥝",
"🍅", "🍆", "🥑", "🥦", "🥒", "🥬", "🌶", "🌽", "🥕", "🥔", "🍠", "🥐", "🍞", "🥖", "🥨", "🥯", "🧀", "🥚", "🍳", "🥞",
"🥓", "🥩", "🍗", "🍖", "🌭", "🍔", "🍟", "🍕", "🥪", "🥙", "🌮", "🌯", "🥗", "🥘", "🥫", "🍝", "🍜", "🍲", "🍛", "🍣",
"🍱", "🥟", "🍤", "🍙", "🍚", "🍘", "🍥", "🥮", "🥠", "🍢", "🍡", "🍧", "🍨", "🍦", "🥧", "🍰", "🎂", "🍮", "🍭", "🍬",
"🍫", "🍿", "🧂", "🍩", "🍪", "🌰", "🥜", "🍯", "🥛", "🍼", "☕️", "🍵", "🥤", "🍶", "🍻", "🥂", "🍷", "🥃", "🍸", "🍹",
"🍾", "🥄", "🍴", "🍽", "🥣", "🥡", "🥢"]
return random.choice(emoji_list)
class BilibiliUploader(object):
def __init__(self, cookie_data, file: pathlib.Path, title, desc, tid, tags, dtime):
self.upload_thread_num = 3
self.copyright = 1
self.lines = 'AUTO'
self.cookie_data = cookie_data
self.file = file
self.title = title
self.desc = desc
self.tid = tid
self.tags = tags
self.dtime = dtime
self._init_data()
def _init_data(self):
self.data = Data()
self.data.copyright = self.copyright
self.data.title = self.title
self.data.desc = self.desc
self.data.tid = self.tid
self.data.set_tag(self.tags)
self.data.dtime = self.dtime
def upload(self):
with BiliBili(self.data) as bili:
bili.login_by_cookies(self.cookie_data)
bili.access_token = self.cookie_data.get('access_token')
video_part = bili.upload_file(str(self.file), lines=self.lines,
tasks=self.upload_thread_num) # 上传视频,默认线路AUTO自动选择,线程数量3。
video_part['title'] = self.title
self.data.append(video_part)
ret = bili.submit() # 提交视频
if ret.get('code') == 0:
bilibili_logger.success(f'[+] {self.file.name}上传 成功')
return True
else:
bilibili_logger.error(f'[-] {self.file.name}上传 失败, error messge: {ret.get("message")}')
return False
+5 -2
View File
@@ -166,10 +166,13 @@ def ensure_biliup_binary(force_check: bool = True) -> Path:
return binary_path
def run_biliup_command(arguments: list[str]) -> subprocess.CompletedProcess[str]:
def run_biliup_command(arguments: list[str], interactive: bool = False) -> subprocess.CompletedProcess[str]:
binary_path = ensure_biliup_binary(force_check=True)
command = [str(binary_path), *arguments]
if interactive:
return subprocess.run(command, check=False)
return subprocess.run(
[str(binary_path), *arguments],
command,
check=False,
capture_output=True,
text=True,
Generated
+1 -1251
View File
File diff suppressed because it is too large Load Diff