11 Commits

Author SHA1 Message Date
suyiiyii b0f690d14f 🐛 _current_user_cookie -> current_identified_cookie 2025-01-13 12:27:52 +08:00
suyiiyii 672405255d Merge branch 'main' into feat/bilibili-cookie 2025-01-04 11:00:44 +08:00
Azide f35c6c2fd3 ♻️ 迁移到 uv 并重写 Dockerfile (#667)
Co-authored-by: BalconyJH <balconyjh@gmail.com>
Co-authored-by: suyiiyii <suyiiyii@gmail.com>
2025-01-03 17:55:06 +08:00
suyiiyii eb2eae61bf ♻️ user_cookie -> identified_cookie 2025-01-03 17:44:53 +08:00
renovate[bot] 504cbdff25 ⬆️ Update astral-sh/ruff-action action to v3 2025-01-03 15:56:52 +08:00
suyiiyii 1031e9f9a5 💡 在 _choose_cookie 方法中添加适当注释 2024-12-24 22:11:21 +08:00
suyiiyii 57e68ac0b9 👷 fix by ruff 2024-12-24 22:11:21 +08:00
suyiiyii 29a807df5a 🐛 fix type hint 2024-12-24 22:11:20 +08:00
suyiiyii 59846642cc B站 cookie选择策略优化 2024-12-24 22:11:20 +08:00
github-actions[bot] e45bf03e49 📝 Update changelog 2024-12-24 07:42:44 +00:00
suyiiyii 3421e3ac5e 🐛 change "proxies" to "proxy" in http.py 2024-12-24 15:42:14 +08:00
25 changed files with 774 additions and 606 deletions
+3 -5
View File
@@ -2,9 +2,9 @@
"name": "Default Linux Universal", "name": "Default Linux Universal",
"image": "mcr.microsoft.com/devcontainers/universal:2-linux", "image": "mcr.microsoft.com/devcontainers/universal:2-linux",
"features": { "features": {
"ghcr.io/devcontainers-contrib/features/poetry:2": {} "ghcr.io/va-h/devcontainers-features/uv:1": {}
}, },
"postCreateCommand": "poetry config virtualenvs.in-project true && poetry install -E all && poetry run pre-commit install && npm install -g pnpm && pnpm install", "postCreateCommand": "uv venv && uv sync --all-groups --all-extras && uv run pre-commit install && npm install -g pnpm && pnpm install",
"customizations": { "customizations": {
"vscode": { "vscode": {
"settings": { "settings": {
@@ -12,7 +12,7 @@
"python.analysis.typeCheckingMode": "basic", "python.analysis.typeCheckingMode": "basic",
"ruff.organizeImports": false, "ruff.organizeImports": false,
"[python]": { "[python]": {
"editor.defaultFormatter": "ms-python.black-formatter", "editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.ruff": true, "source.fixAll.ruff": true,
"source.organizeImports": true "source.organizeImports": true
@@ -44,8 +44,6 @@
"extensions": [ "extensions": [
"ms-python.python", "ms-python.python",
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"ms-python.isort",
"ms-python.black-formatter",
"charliermarsh.ruff", "charliermarsh.ruff",
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
+3 -3
View File
@@ -1,12 +1,9 @@
*.ini *.ini
#*.yaml
*.yml *.yml
*.md *.md
#*.json
*.xml *.xml
tests tests
node_modules node_modules
#admin-frontend
data* data*
htmlcov htmlcov
docker docker
@@ -22,3 +19,6 @@ venv
/.prettierrc /.prettierrc
/.devcontainer/ /.devcontainer/
/.github/ /.github/
# self install 时需要
!README.md
+1 -1
View File
@@ -27,7 +27,7 @@ body:
description: 请选择安装方式 description: 请选择安装方式
options: options:
- 通过 nb-cli 安装 - 通过 nb-cli 安装
- 使用 poetry/pdm 等现代包管理器安装 - 使用 uv/poetry/pdm 等现代包管理器安装
- 通过 pip install 安装 - 通过 pip install 安装
- 克隆或下载项目直接使用 - 克隆或下载项目直接使用
+9
View File
@@ -10,6 +10,10 @@ inputs:
description: Install dependencies description: Install dependencies
required: false required: false
default: "false" default: "false"
install-playwright:
description: Install Playwright
required: false
default: "false"
runs: runs:
using: "composite" using: "composite"
@@ -29,3 +33,8 @@ runs:
if: ${{ inputs.install-deps == 'true' }} if: ${{ inputs.install-deps == 'true' }}
run: uv sync --frozen run: uv sync --frozen
shell: bash shell: bash
- name: Install Playwright
if: ${{ inputs.install-playwright == 'true' }}
run: uv run playwright install
shell: bash
+4 -8
View File
@@ -73,9 +73,7 @@ jobs:
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
install-deps: "true" install-deps: "true"
install-playwright: "true"
- name: Install playwright
run: uv run playwright install
- name: Run Pytest - name: Run Pytest
run: uv run pytest --cov-report xml --cov=./nonebot_bison -k 'not compare and not render' -n auto run: uv run pytest --cov-report xml --cov=./nonebot_bison -k 'not compare and not render' -n auto
@@ -108,9 +106,7 @@ jobs:
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
install-deps: "true" install-deps: "true"
install-playwright: "true"
- name: Install playwright
run: uv run playwright install
- name: Run Pytest - name: Run Pytest
run: uv run pytest --cov-report xml --cov=./nonebot_bison -k 'not compare' -n auto run: uv run pytest --cov-report xml --cov=./nonebot_bison -k 'not compare' -n auto
@@ -176,8 +172,8 @@ jobs:
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
docker-main-sentry: docker-main-nobrowser:
name: Docker main sentry name: Docker main without browser
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build-frontend, test] needs: [build-frontend, test]
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
+2 -2
View File
@@ -37,8 +37,8 @@ jobs:
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
docker-main-sentry: docker-main-nobrowser:
name: Docker main sentry name: Docker main without browser
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
+1 -4
View File
@@ -55,10 +55,7 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install prerequisites - name: Install prerequisites
run: uv add pydantic==1.10 && uv sync --frozen run: uv add pydantic==1.10 && uv sync --frozen && uv run playwright install
- name: Install playwright
run: uv run playwright install
- name: Run Pytest - name: Run Pytest
run: uv run pytest -k 'not compare and not render' -n auto run: uv run pytest -k 'not compare and not render' -n auto
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Run Ruff Lint - name: Run Ruff Lint
uses: astral-sh/ruff-action@v2 uses: astral-sh/ruff-action@v3
with: with:
src: >- src: >-
nonebot_bison/ nonebot_bison/
+1
View File
@@ -9,6 +9,7 @@
### Bug 修复 ### Bug 修复
- :bug: 修复新版 httpx 中 proxy 不起效的问题 [@suyiiyii](https://github.com/suyiiyii) ([#669](https://github.com/MountainDash/nonebot-bison/pull/669))
- :bug: 修复 cookie 模块 type hint [@suyiiyii](https://github.com/suyiiyii) ([#658](https://github.com/MountainDash/nonebot-bison/pull/658)) - :bug: 修复 cookie 模块 type hint [@suyiiyii](https://github.com/suyiiyii) ([#658](https://github.com/MountainDash/nonebot-bison/pull/658))
- :bug: B站转发动态补充 DeletedItem 类型解析 [@AzideCupric](https://github.com/AzideCupric) ([#659](https://github.com/MountainDash/nonebot-bison/pull/659)) - :bug: B站转发动态补充 DeletedItem 类型解析 [@AzideCupric](https://github.com/AzideCupric) ([#659](https://github.com/MountainDash/nonebot-bison/pull/659))
- :bug: 小刻食堂cdn使用https [@phidiaLam](https://github.com/phidiaLam) ([#650](https://github.com/MountainDash/nonebot-bison/pull/650)) - :bug: 小刻食堂cdn使用https [@phidiaLam](https://github.com/phidiaLam) ([#650](https://github.com/MountainDash/nonebot-bison/pull/650))
+47 -32
View File
@@ -56,7 +56,7 @@ ENV TZ=Asia/Shanghai \
COPY ./admin-frontend/package.json ./admin-frontend/pnpm-lock.yaml ./ COPY ./admin-frontend/package.json ./admin-frontend/pnpm-lock.yaml ./
RUN corepack enable && \ RUN corepack enable && \
corepack prepare pnpm@9.15.1 --activate && \ corepack prepare pnpm@latest --activate && \
echo "🔔 Verifying Node.js installation..." && \ echo "🔔 Verifying Node.js installation..." && \
if NODE_VERSION=$(node --version 2>&1); then \ if NODE_VERSION=$(node --version 2>&1); then \
echo "✅ Node.js version ${NODE_VERSION} installed successfully"; \ echo "✅ Node.js version ${NODE_VERSION} installed successfully"; \
@@ -79,7 +79,7 @@ RUN echo "🏗️ Starting frontend build..." && \
pnpm run build && \ pnpm run build && \
echo "✅ Frontend build completed" echo "✅ Frontend build completed"
FROM python:3.12-slim AS playwright-stage FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS playwright-stage
WORKDIR /tmp WORKDIR /tmp
@@ -87,22 +87,43 @@ ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
TZ=Asia/Shanghai TZ=Asia/Shanghai
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
RUN echo "📦 Installing playwright..." && \ RUN echo "📦 Installing playwright..." && \
uvx playwright install --with-deps chromium && \ uvx playwright install --with-deps chromium && \
echo "✅ Playwright installed successfully" echo "✅ Playwright installed successfully"
FROM python:3.12-slim AS production FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv-stage
WORKDIR /app WORKDIR /app
#ENV UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
UV_COMPILE_BYTECODE=1 \
UV_SYSTEM_PYTHON=true \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
UV_COMPILE_BYTECODE=1 \
UV_FROZEN=1 \
UV_LINK_MODE=copy \
TZ=Asia/Shanghai
RUN echo "🔔 Verifying uv installation..." && \
if UV_VERSION=$(uv --version 2>&1); then \
echo "✅ uv version ${UV_VERSION} installed successfully"; \
else \
echo "❌ uv installation failed" && exit 1; \
fi
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
echo "📦 Installing project dependencies..." && \
uv sync --no-install-project --no-dev --extra docker && \
echo "✅ Dependencies installed successfully"
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS production
WORKDIR /app
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \
UV_COMPILE_BYTECODE=1 \
UV_FROZEN=1 \
UV_LINK_MODE=copy \ UV_LINK_MODE=copy \
TZ=Asia/Shanghai \ TZ=Asia/Shanghai \
SHELL="/bin/bash" SHELL="/bin/bash"
@@ -156,36 +177,30 @@ RUN echo "📦 Installing system dependencies..." && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
echo "✅ System dependencies installed successfully" echo "✅ System dependencies installed successfully"
# 少变动的放上面,频繁变动的放下面
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ COPY ./docker.env.prod ./.env
RUN echo "🔔 Verifying uv installation..." && \
if UV_VERSION=$(uv --version 2>&1); then \
echo "✅ uv version ${UV_VERSION} installed successfully"; \
else \
echo "❌ uv installation failed" && exit 1; \
fi
COPY ./pyproject.toml ./uv.lock ./
RUN --mount=type=cache,target=/root/.cache/ \
--mount=type=cache,target=/root/.uv \
echo "📦 Installing bison dependencies..." && \
uv pip install --extra=docker --requirement pyproject.toml --no-cache && \
echo "✅ Dependencies installed successfully" && \
echo "🔔 Cleaning up uv..." && \
rm -rf /bin/uv /bin/uvx && \
echo "✅ UV cleanup completed"
COPY --from=metadata-stage /tmp/VERSION /app/VERSION
COPY nonebot_bison/ /app/nonebot_bison/
COPY --from=frontend-stage /tmp/nonebot_bison/admin_page/dist ./nonebot_bison/admin_page/dist
COPY --from=playwright-stage /root/.cache/ms-playwright /root/.cache/ms-playwright COPY --from=playwright-stage /root/.cache/ms-playwright /root/.cache/ms-playwright
COPY extra_plugins/ /app/extra_plugins/
COPY --from=frontend-stage /tmp/nonebot_bison/admin_page/dist ./nonebot_bison/admin_page/dist
COPY --from=uv-stage /app /app
COPY ./pyproject.toml ./uv.lock ./README.md ./
COPY nonebot_bison/ /app/nonebot_bison/
COPY --from=metadata-stage /tmp/VERSION /app/VERSION
RUN --mount=type=cache,target=/root/.cache/uv \
echo "📦 Installing bison project..." && \
uv sync --no-dev --extra docker && \
uv run nb adapter install nonebot-adapter-satori && \
uv run nb adapter install nonebot-adapter-qq && \
uv run nb plugin install nonebot-plugin-sentry && \
echo "✅ Bison installed successfully"
EXPOSE 8080 EXPOSE 8080
RUN echo '#!/bin/bash' > /app/start.sh && \ RUN echo '#!/bin/bash' > /app/start.sh && \
echo 'echo "📌 Current bison Version: $(cat /app/VERSION)"' >> /app/start.sh && \ echo 'echo "📌 Current bison Version: $(cat /app/VERSION)"' >> /app/start.sh && \
echo 'echo "🚀 Starting service..."' >> /app/start.sh && \ echo 'echo "🚀 Starting service..."' >> /app/start.sh && \
echo 'source /app/.venv/bin/activate' >> /app/start.sh && \
echo 'nb run' >> /app/start.sh && \ echo 'nb run' >> /app/start.sh && \
chmod +x /app/start.sh chmod +x /app/start.sh
+39 -37
View File
@@ -32,7 +32,6 @@ RUN echo "📋 Collecting metadata..." && \
echo "$DESCRIPTION" > /tmp/DESCRIPTION && \ echo "$DESCRIPTION" > /tmp/DESCRIPTION && \
echo "$AUTHOR" > /tmp/AUTHOR echo "$AUTHOR" > /tmp/AUTHOR
RUN --mount=type=bind,source=./.git/,target=/tmp/.git/ \ RUN --mount=type=bind,source=./.git/,target=/tmp/.git/ \
echo "🏷️ Checking version information..." && \ echo "🏷️ Checking version information..." && \
VERSION=$(git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD || echo "unknown") && \ VERSION=$(git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD || echo "unknown") && \
@@ -56,7 +55,7 @@ ENV TZ=Asia/Shanghai \
COPY ./admin-frontend/package.json ./admin-frontend/pnpm-lock.yaml ./ COPY ./admin-frontend/package.json ./admin-frontend/pnpm-lock.yaml ./
RUN corepack enable && \ RUN corepack enable && \
corepack prepare pnpm@9.15.1 --activate && \ corepack prepare pnpm@latest --activate && \
echo "🔔 Verifying Node.js installation..." && \ echo "🔔 Verifying Node.js installation..." && \
if NODE_VERSION=$(node --version 2>&1); then \ if NODE_VERSION=$(node --version 2>&1); then \
echo "✅ Node.js version ${NODE_VERSION} installed successfully"; \ echo "✅ Node.js version ${NODE_VERSION} installed successfully"; \
@@ -79,35 +78,17 @@ RUN echo "🏗️ Starting frontend build..." && \
pnpm run build && \ pnpm run build && \
echo "✅ Frontend build completed" echo "✅ Frontend build completed"
FROM python:3.12-slim AS playwright-stage FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv-stage
WORKDIR /tmp
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \
TZ=Asia/Shanghai
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
RUN echo "📦 Installing playwright..." && \
uvx playwright install --with-deps chromium && \
echo "✅ Playwright installed successfully"
FROM python:3.12-slim AS production
WORKDIR /app WORKDIR /app
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
UV_COMPILE_BYTECODE=1 \
UV_SYSTEM_PYTHON=true \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
UV_COMPILE_BYTECODE=1 \
UV_FROZEN=1 \
UV_LINK_MODE=copy \ UV_LINK_MODE=copy \
TZ=Asia/Shanghai \ TZ=Asia/Shanghai
SHELL="/bin/bash"
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
RUN echo "🔔 Verifying uv installation..." && \ RUN echo "🔔 Verifying uv installation..." && \
if UV_VERSION=$(uv --version 2>&1); then \ if UV_VERSION=$(uv --version 2>&1); then \
echo "✅ uv version ${UV_VERSION} installed successfully"; \ echo "✅ uv version ${UV_VERSION} installed successfully"; \
@@ -115,21 +96,41 @@ RUN echo "🔔 Verifying uv installation..." && \
echo "❌ uv installation failed" && exit 1; \ echo "❌ uv installation failed" && exit 1; \
fi fi
COPY ./pyproject.toml ./uv.lock ./ RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
echo "📦 Installing project dependencies..." && \
uv sync --no-install-project --no-dev --extra docker && \
echo "✅ Dependencies installed successfully"
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS production
WORKDIR /app
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \
UV_COMPILE_BYTECODE=1 \
UV_FROZEN=1 \
UV_LINK_MODE=copy \
TZ=Asia/Shanghai \
SHELL="/bin/bash"
# 少变动的放上面,频繁变动的放下面
COPY ./docker.env.prod ./.env COPY ./docker.env.prod ./.env
COPY extra_plugins/ /app/extra_plugins/
RUN --mount=type=cache,target=/root/.cache/ \
--mount=type=cache,target=/root/.uv \
echo "📦 Installing bison dependencies..." && \
uv pip install --extra=docker --requirement pyproject.toml --no-cache && \
echo "✅ Dependencies installed successfully" && \
echo "🔔 Cleaning up uv..." && \
rm -rf /bin/uv /bin/uvx && \
echo "✅ UV cleanup completed"
COPY --from=metadata-stage /tmp/VERSION /app/VERSION
COPY nonebot_bison/ /app/nonebot_bison/
COPY --from=frontend-stage /tmp/nonebot_bison/admin_page/dist ./nonebot_bison/admin_page/dist COPY --from=frontend-stage /tmp/nonebot_bison/admin_page/dist ./nonebot_bison/admin_page/dist
COPY --from=uv-stage /app /app
COPY ./pyproject.toml ./uv.lock ./README.md ./
COPY nonebot_bison/ /app/nonebot_bison/
COPY --from=metadata-stage /tmp/VERSION /app/VERSION
RUN --mount=type=cache,target=/root/.cache/uv \
echo "📦 Installing bison project..." && \
uv sync --no-dev --extra docker && \
uv run nb adapter install nonebot-adapter-satori && \
uv run nb adapter install nonebot-adapter-qq && \
uv run nb plugin install nonebot-plugin-sentry && \
echo "✅ Bison installed successfully"
EXPOSE 8080 EXPOSE 8080
@@ -139,6 +140,7 @@ ENV BISON_SKIP_BROWSER_CHECK=True \
RUN echo '#!/bin/bash' > /app/start.sh && \ RUN echo '#!/bin/bash' > /app/start.sh && \
echo 'echo "📌 Current bison Version: $(cat /app/VERSION)"' >> /app/start.sh && \ echo 'echo "📌 Current bison Version: $(cat /app/VERSION)"' >> /app/start.sh && \
echo 'echo "🚀 Starting service..."' >> /app/start.sh && \ echo 'echo "🚀 Starting service..."' >> /app/start.sh && \
echo 'source /app/.venv/bin/activate' >> /app/start.sh && \
echo 'nb run' >> /app/start.sh && \ echo 'nb run' >> /app/start.sh && \
chmod +x /app/start.sh chmod +x /app/start.sh
+3 -3
View File
@@ -8,7 +8,7 @@ next: /dev/cookie
## 语言以及工具 ## 语言以及工具
1. 本项目使用了`python3.10`的特性进行开发,所以请确保你的 Python 版本>=3.10 1. 本项目使用了`python3.10`的特性进行开发,所以请确保你的 Python 版本>=3.10
2. 本项目使用 poetry 进行依赖管理,请确保开发之前已经进行过`poetry install`,运行时在`poetry shell`的环境中进行运行 2. 本项目使用 uv 进行依赖管理,请确保开发之前已经进行过`uv sync`,运行时在`uv venv`的环境中进行运行
3. 本项目使用的 node 项目管理工具是 pnpm 3. 本项目使用的 node 项目管理工具是 pnpm
::: tip 参考 ::: tip 参考
@@ -30,7 +30,7 @@ next: /dev/cookie
2. 运行 bot 2. 运行 bot
```bash ```bash
poetry run nb run uv run nb run
``` ```
3. 运行前端: 3. 运行前端:
@@ -50,7 +50,7 @@ next: /dev/cookie
## 代码格式 ## 代码格式
本项目使用了 pre-commit 来进行代码美化和格式化。在`poetry shell`状态下执行`pre-commit install`来安装 git hook,可自动在 commit 时 本项目使用了 pre-commit 来进行代码美化和格式化。在`uv venv`状态下执行`pre-commit install`来安装 git hook,可自动在 commit 时
格式化代码。 格式化代码。
## 适配新网站 ## 适配新网站
+3 -5
View File
@@ -83,10 +83,8 @@ sequenceDiagram
目前 CookieClientManager 具有以下方法 目前 CookieClientManager 具有以下方法
- `refresh_anonymous_cookie(cls)` 移除已有的匿名 cookie,添加一个新的匿名 cookie,应该在 CCM 初始化时调用 - `refresh_anonymous_cookie(cls)` 移除已有的匿名 cookie,添加一个新的匿名 cookie,应该在 CCM 初始化时调用
- `add_user_cookie(cls, content: str)` 添加用户 cookie,在这里对 Cookie 进行检查并获取 cookie_name,写入数据库
- `_generate_hook(self, cookie: Cookie) -> Callable` hook 函数生成器,用于回写请求状态到数据库
- `_choose_cookie(self, target: Target) -> Cookie` 选择 cookie 的具体算法 - `_choose_cookie(self, target: Target) -> Cookie` 选择 cookie 的具体算法
- `add_user_cookie(cls, content: str, cookie_name: str | None = None) -> Cookie` 对外的接口,添加用户 cookie,内部会调用 Site 的方法进行检查 - `add_identified_cookie(cls, content: str, cookie_name: str | None = None) -> Cookie` 对外的接口,添加实名 cookie,内部会调用 Site 的方法进行检查
- `get_client(self, target: Target | None) -> AsyncClient` 对外的接口,获取 client,根据 target 选择 cookie - `get_client(self, target: Target | None) -> AsyncClient` 对外的接口,获取 client,根据 target 选择 cookie
- `_assemble_client(self, client, cookie) -> AsyncClient` 组装 client,可以自定义 cookie 对象的 content 装配到 client 中的方式 - `_assemble_client(self, client, cookie) -> AsyncClient` 组装 client,可以自定义 cookie 对象的 content 装配到 client 中的方式
@@ -100,7 +98,7 @@ sequenceDiagram
简单来说: 简单来说:
- 如果需要修改 Cookie 的默认参数,可以重写`add_user_cookie`方法,这里设置需要的字段 - 如果需要修改 Cookie 的默认参数,可以重写`add_identified_cookie`方法,这里设置需要的字段
- 如果需要修改选择 Cookie 的逻辑,可以重写`_choose_cookie`方法,使用自己的算法选择合适的 Cookie 并返回 - 如果需要修改选择 Cookie 的逻辑,可以重写`_choose_cookie`方法,使用自己的算法选择合适的 Cookie 并返回
- 如果需要自定义 Cookie 的格式,可以重写`valid_cookie`方法,自定义验证 Cookie 的逻辑,并重写`_assemble_client`方法,自定义将 Cookie 装配到 Client 中的逻辑 - 如果需要自定义 Cookie 的格式,可以重写`valid_cookie`方法,自定义验证 Cookie 的逻辑,并重写`_assemble_client`方法,自定义将 Cookie 装配到 Client 中的逻辑
- 如果要在请求结束后做一些操作(例如保存此次请求的结果/状态),可以重写`_response_hook`方法,自定义请求结束后的行为 - 如果要在请求结束后做一些操作(例如保存此次请求的结果/状态),可以重写`_response_hook`方法,自定义请求结束后的行为
@@ -137,7 +135,7 @@ sequenceDiagram
- **无 Target 平台的 Cookie 处理方式** - **无 Target 平台的 Cookie 处理方式**
对于不存在 Target 的平台,如小刻食堂,可以重写 add_user_cookie 方法,为用户 Cookie 设置 is_universal 字段。这样,在获取 Client 时,由于传入的 Target 为空,就只会选择 is_universal 的 cookie。实现了无 Target 平台的用户 Cookie 调度。 对于不存在 Target 的平台,如小刻食堂,可以重写 add_identified_cookie 方法,为实名 cookie 设置 is_universal 字段。这样,在获取 Client 时,由于传入的 Target 为空,就只会选择 is_universal 的 cookie。实现了无 Target 平台的实名 cookie 调度。
## 默认的调度策略 ## 默认的调度策略
+8 -8
View File
@@ -23,7 +23,7 @@ next: /usage/easy-use
参考视频教程 [保姆级新手教学 - Well404](https://www.bilibili.com/video/BV1984y1b7JY) 参考视频教程 [保姆级新手教学 - Well404](https://www.bilibili.com/video/BV1984y1b7JY)
::: :::
::: warning 防止环境冲突! ::: warning 防止环境冲突!
建议所有操作都在虚拟环境下进行,推荐使用[`poetry`](https://python-poetry.org/)或者 python 自带的[`venv`](https://docs.python.org/zh-cn/3/library/venv.html) 建议所有操作都在虚拟环境下进行,推荐使用[`uv`](https://docs.astral.sh/uv/)或者 python 自带的[`venv`](https://docs.python.org/zh-cn/3/library/venv.html)
::: :::
### 使用 nb-cli 安装 <Badge type="tip" text="推荐" vertical="top" /> ### 使用 nb-cli 安装 <Badge type="tip" text="推荐" vertical="top" />
@@ -43,17 +43,17 @@ next: /usage/easy-use
3. 在项目中添加依赖 3. 在项目中添加依赖
```bash ```bash
poetry add nonebot-bison uv add nonebot-bison
``` ```
### 手动安装 ### 手动安装
1. 安装 pip 包`nonebot-bison` 1. 安装 pip 包`nonebot-bison`
::: code-tabs ::: code-tabs
@tab poetry @tab uv
```bash ```bash
poetry add nonebot-bison uv add nonebot-bison
``` ```
@tab pip @tab pip
@@ -169,13 +169,13 @@ Bison 的 WebUI 是需要编译后才能使用的,直接克隆源代码需要
本项目中使用了 Python 3.10 的语法,如果出现问题,请检查 Python 版本 本项目中使用了 Python 3.10 的语法,如果出现问题,请检查 Python 版本
::: :::
1. 首先安装 poetry[安装方法](https://python-poetry.org/docs/#installation) 1. 首先安装 uv[安装方法](https://docs.astral.sh/uv/getting-started/installation/)
2. clone 本项目,在项目中`poetry install`安装依赖 2. clone 本项目,在项目中使用`uv sync`安装依赖
```bash ```bash
git clone https://github.com/felinae98/nonebot-bison.git git clone https://github.com/felinae98/nonebot-bison.git
cd nonebot-bison cd nonebot-bison
poetry install uv sync
``` ```
#### WebUI 安装 #### WebUI 安装
@@ -235,5 +235,5 @@ Bison 的 WebUI 是需要编译后才能使用的,直接克隆源代码需要
2. 启动 Bot 2. 启动 Bot
```bash ```bash
poetry run nb run uv run nb run
``` ```
+1 -1
View File
@@ -235,7 +235,7 @@ async def get_cookie(site_name: str | None = None, target: str | None = None) ->
@router.post("/cookie", dependencies=[Depends(check_is_superuser)]) @router.post("/cookie", dependencies=[Depends(check_is_superuser)])
async def add_cookie(site_name: str, content: str) -> StatusResp: async def add_cookie(site_name: str, content: str) -> StatusResp:
client_mgr = cast(CookieClientManager, scheduler_dict[site_manager[site_name]].client_mgr) client_mgr = cast(CookieClientManager, scheduler_dict[site_manager[site_name]].client_mgr)
await client_mgr.add_user_cookie(content) await client_mgr.add_identified_cookie(content)
return StatusResp(ok=True, msg="") return StatusResp(ok=True, msg="")
+43 -1
View File
@@ -1,14 +1,17 @@
from collections.abc import Callable
from datetime import datetime, timedelta from datetime import datetime, timedelta
import json import json
import random import random
from typing import TYPE_CHECKING, ClassVar, TypeVar from typing import TYPE_CHECKING, ClassVar, TypeVar
from typing_extensions import override from typing_extensions import override
from httpx import AsyncClient from httpx import AsyncClient, Response
from nonebot import logger, require from nonebot import logger, require
from playwright.async_api import Cookie from playwright.async_api import Cookie
from nonebot_bison.config import config
from nonebot_bison.config.db_model import Cookie as CookieModel from nonebot_bison.config.db_model import Cookie as CookieModel
from nonebot_bison.config.db_model import Target
from nonebot_bison.plugin_config import plugin_config from nonebot_bison.plugin_config import plugin_config
from nonebot_bison.utils import Site, http_client from nonebot_bison.utils import Site, http_client
from nonebot_bison.utils.site import CookieClientManager from nonebot_bison.utils.site import CookieClientManager
@@ -25,6 +28,8 @@ B = TypeVar("B", bound="Bilibili")
class BilibiliClientManager(CookieClientManager): class BilibiliClientManager(CookieClientManager):
_default_cookie_cd = timedelta(seconds=120) _default_cookie_cd = timedelta(seconds=120)
current_identified_cookie: CookieModel | None = None
_site_name = "bilibili.com"
async def _get_cookies(self) -> list[Cookie]: async def _get_cookies(self) -> list[Cookie]:
browser = await get_browser() browser = await get_browser()
@@ -59,6 +64,43 @@ class BilibiliClientManager(CookieClientManager):
) )
return cookie return cookie
def _generate_hook(self, cookie: CookieModel) -> Callable:
"""hook 函数生成器,用于回写请求状态到数据库"""
async def _response_hook(resp: Response):
await resp.aread()
if resp.status_code == 200 and "-352" not in resp.text:
logger.trace(f"请求成功: {cookie.id} {resp.request.url}")
cookie.status = "success"
else:
logger.warning(f"请求失败: {cookie.id} {resp.request.url}, 状态码: {resp.status_code}")
cookie.status = "failed"
self.current_identified_cookie = None
cookie.last_usage = datetime.now()
await config.update_cookie(cookie)
return _response_hook
async def _get_next_identified_cookie(self) -> CookieModel | None:
"""选择下一个实名 cookie"""
cookies = await config.get_cookie(self._site_name, is_anonymous=False)
available_cookies = [cookie for cookie in cookies if cookie.last_usage + cookie.cd < datetime.now()]
if not available_cookies:
return None
cookie = min(available_cookies, key=lambda x: x.last_usage)
return cookie
async def _choose_cookie(self, target: Target | None) -> CookieModel:
"""选择 cookie 的具体算法"""
if self.current_identified_cookie is None:
# 若当前没有选定实名 cookie 则尝试获取
self.current_identified_cookie = await self._get_next_identified_cookie()
if self.current_identified_cookie:
# 如果当前有选定的实名 cookie 则直接返回
return self.current_identified_cookie
# 否则返回匿名 cookie
return (await config.get_cookie(self._site_name, is_anonymous=True))[0]
@override @override
async def refresh_client(self): async def refresh_client(self):
await self._refresh_anonymous_cookie() await self._refresh_anonymous_cookie()
+1 -1
View File
@@ -76,7 +76,7 @@ def do_add_cookie(add_cookie: type[Matcher]):
@add_cookie.handle() @add_cookie.handle()
async def add_cookie_process(state: T_State): async def add_cookie_process(state: T_State):
client_mgr = cast(CookieClientManager, scheduler_dict[platform_manager[state["platform"]].site].client_mgr) client_mgr = cast(CookieClientManager, scheduler_dict[platform_manager[state["platform"]].site].client_mgr)
new_cookie = await client_mgr.add_user_cookie(state["cookie"], state["cookie_name"]) new_cookie = await client_mgr.add_identified_cookie(state["cookie"], state["cookie_name"])
await add_cookie.finish( await add_cookie.finish(
f"已添加 Cookie: {new_cookie.cookie_name} 到平台 {state['platform']}" f"已添加 Cookie: {new_cookie.cookie_name} 到平台 {state['platform']}"
+ "\n请使用“关联cookie”为 Cookie 关联订阅" + "\n请使用“关联cookie”为 Cookie 关联订阅"
@@ -34,7 +34,7 @@ def do_add_cookie_target(add_cookie_target_matcher: type[Matcher]):
@add_cookie_target_matcher.handle() @add_cookie_target_matcher.handle()
async def init_promote_cookie(state: T_State): async def init_promote_cookie(state: T_State):
# 获取 site 的所有用户 cookie,再排除掉已经关联的 cookie,剩下的就是可以关联的 cookie # 获取 site 的所有实名 cookie,再排除掉已经关联的 cookie,剩下的就是可以关联的 cookie
cookies = await config.get_cookie(site_name=state["site"].name, is_anonymous=False) cookies = await config.get_cookie(site_name=state["site"].name, is_anonymous=False)
associated_cookies = await config.get_cookie( associated_cookies = await config.get_cookie(
target=state["target"]["target"], target=state["target"]["target"],
+6 -6
View File
@@ -79,8 +79,8 @@ class CookieClientManager(ClientManager):
new_anonymous_cookie = await self._generate_anonymous_cookie() new_anonymous_cookie = await self._generate_anonymous_cookie()
await config.add_cookie(new_anonymous_cookie) await config.add_cookie(new_anonymous_cookie)
async def add_user_cookie(self, content: str, cookie_name: str | None = None) -> Cookie: async def add_identified_cookie(self, content: str, cookie_name: str | None = None) -> Cookie:
"""添加用户 cookie""" """添加实名 cookie"""
if not await self.validate_cookie(content): if not await self.validate_cookie(content):
raise ValueError() raise ValueError()
@@ -124,8 +124,8 @@ class CookieClientManager(ClientManager):
async def _choose_cookie(self, target: Target | None) -> Cookie: async def _choose_cookie(self, target: Target | None) -> Cookie:
"""选择 cookie 的具体算法""" """选择 cookie 的具体算法"""
cookies = await config.get_cookie(self._site_name, target) cookies = await config.get_cookie(self._site_name, target)
avaliable_cookies = (cookie for cookie in cookies if cookie.last_usage + cookie.cd < datetime.now()) available_cookies = (cookie for cookie in cookies if cookie.last_usage + cookie.cd < datetime.now())
cookie = min(avaliable_cookies, key=lambda x: x.last_usage) cookie = min(available_cookies, key=lambda x: x.last_usage)
return cookie return cookie
async def get_client(self, target: Target | None) -> AsyncClient: async def get_client(self, target: Target | None) -> AsyncClient:
@@ -133,9 +133,9 @@ class CookieClientManager(ClientManager):
client = http_client() client = http_client()
cookie = await self._choose_cookie(target) cookie = await self._choose_cookie(target)
if cookie.is_universal: if cookie.is_universal:
logger.trace(f"平台 {self._site_name} 未获取到用户cookie, 使用匿名cookie") logger.trace(f"平台 {self._site_name} 未获取到实名cookie, 使用匿名cookie")
else: else:
logger.trace(f"平台 {self._site_name} 获取到用户cookie: {cookie.id}") logger.trace(f"平台 {self._site_name} 获取到实名cookie: {cookie.id}")
return await self._assemble_client(client, cookie) return await self._assemble_client(client, cookie)
+41 -41
View File
@@ -3,26 +3,26 @@ authors = [{ name = "felinae98", email = "felinae225@qq.com" }]
license = { text = "MIT" } license = { text = "MIT" }
requires-python = "<4.0.0,>=3.10" requires-python = "<4.0.0,>=3.10"
dependencies = [ dependencies = [
"beautifulsoup4==4.12.3", "beautifulsoup4>=4.12.3",
"feedparser==6.0.11", "feedparser<7.0.0,>=6.0.11",
"httpx==0.27.2", "httpx>=0.27.2",
"nonebot2[fastapi]==2.3.3", "nonebot2[fastapi]<3.0.0,>=2.3.3",
"nonebot-adapter-onebot==2.4.5", "nonebot-adapter-onebot<3.0.0,>=2.4.5",
"nonebot-plugin-htmlrender==0.3.5", "nonebot-plugin-htmlrender>=0.3.5,!=0.5.0",
"nonebot-plugin-datastore==1.3.0", "nonebot-plugin-datastore<2.0.0,>=1.3.0",
"nonebot-plugin-apscheduler==0.5.0", "nonebot-plugin-apscheduler>=0.5.0",
"nonebot-plugin-send-anything-anywhere==0.7.1", "nonebot-plugin-send-anything-anywhere<0.7.2,>=0.7.1",
"pillow==11.0.0", "pillow<11.1,>=11.0.0",
"pyjwt==2.9.0", "pyjwt<3.0.0,>=2.9.0",
"python-socketio==5.11.4", "python-socketio<6.0.0,>=5.11.4",
"tinydb==4.8.0", "tinydb<5.0.0,>=4.8.0",
"qrcode==7.4.2", "qrcode<8.0.0,>=7.4.2",
"Pydantic==2.9.2", "pydantic!=2.5.0,!=2.5.1,<3.0.0,>=2.9.2",
"lxml==5.3.0", "lxml>=5.3.0",
"yarl==1.11.1", "yarl>=1.11.1",
"hishel==0.0.30", "hishel<1.0.0,>=0.0.30",
"expiringdictx==1.1.0", "expiringdictx<2.0.0,>=1.1.0",
"rapidfuzz==3.9.7", "rapidfuzz<4.0.0,>=3.9.7",
] ]
name = "nonebot-bison" name = "nonebot-bison"
version = "0.9.5" version = "0.9.5"
@@ -49,33 +49,33 @@ bison = "nonebot_bison.script.cli:main"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"ipdb==0.13.13", "ipdb<1.0.0,>=0.13.13",
"nonemoji==0.1.4", "nonemoji<1.0.0,>=0.1.4",
"nb-cli==1.4.2", "nb-cli<2.0.0,>=1.4.2",
"pre-commit==4.0.1", "pre-commit<5.0.0,>=4.0.1",
"ruff==0.8.2", "ruff<1.0.0,>=0.8.2",
] ]
test = [ test = [
"flaky==3.8.1", "flaky<4.0.0,>=3.8.1",
"nonebug==0.3.7", "nonebug<1.0.0,>=0.3.7",
"pytz==2024.2", "nonebug-saa<1.0.0,>=0.4.1",
"nonebug-saa==0.4.1", "pytest<9.0.0,>=8.3.3",
"pytest==8.3.3", "pytest-asyncio<0.24.1,>=0.24.0",
"pytest-asyncio==0.24.0", "pytest-cov<7,>=6.0.0",
"pytest-cov==6.0.0", "pytest-mock<4.0.0,>=3.14.0",
"pytest-mock==3.14.0", "pytest-xdist[psutil]<4.0.0,>=3.6.1",
"pytest-xdist[psutil]==3.6.1", "respx<0.23.0,>=0.22.0",
"respx==0.21.1", "freezegun<2.0.0,>=1.5.1",
"freezegun==1.5.1", "pytz>=2024.2",
] ]
[project.optional-dependencies] [project.optional-dependencies]
docker = [ docker = [
"nb-cli==1.4.2", "nb-cli<2.0.0,>=1.4.2",
"nonebot2[aiohttp,fastapi]==2.3.3", "nonebot2[aiohttp,fastapi]<3.0.0,>=2.3.3",
"nonebot-adapter-red==0.9.0", "nonebot-adapter-qq<2.0.0,>=1.5.1",
"nonebot-adapter-qq==1.5.1", "nonebot-adapter-satori>=0.13.1",
"poetry-core==1.9.0", "pip>=24.3.1",
] ]
[tool.uv] [tool.uv]
+4 -4
View File
@@ -34,9 +34,9 @@ async def test_cookie(app: App, init_scheduler):
cookies = await config.get_cookie(site_name=site.name) cookies = await config.get_cookie(site_name=site.name)
assert len(cookies) == 1 assert len(cookies) == 1
# 添加用户cookie # 添加实名cookie
await client_mgr.add_user_cookie(json.dumps({"test_cookie": "1"})) await client_mgr.add_identified_cookie(json.dumps({"test_cookie": "1"}))
await client_mgr.add_user_cookie(json.dumps({"test_cookie": "2"})) await client_mgr.add_identified_cookie(json.dumps({"test_cookie": "2"}))
cookies = await config.get_cookie(site_name=site.name) cookies = await config.get_cookie(site_name=site.name)
assert len(cookies) == 3 assert len(cookies) == 3
@@ -68,7 +68,7 @@ async def test_cookie(app: App, init_scheduler):
tags=[], tags=[],
) )
await client_mgr.add_user_cookie(json.dumps({"test_cookie": "3"})) await client_mgr.add_identified_cookie(json.dumps({"test_cookie": "3"}))
cookies = await config.get_cookie(site_name=site.name, is_anonymous=False) cookies = await config.get_cookie(site_name=site.name, is_anonymous=False)
# 多个target,多个cookie # 多个target,多个cookie
+1 -25
View File
@@ -4,31 +4,7 @@ from nonebug.app import App
import pytest import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from tests.utils import BotReply, fake_admin_user, fake_private_message_event, fake_superuser from tests.utils import BotReply, fake_private_message_event, fake_superuser
async def test_add_cookie_rule(app: App, mocker: MockerFixture):
from nonebot.adapters.onebot.v11.bot import Bot
from nonebot.adapters.onebot.v11.message import Message
from nonebot_bison.plugin_config import plugin_config
from nonebot_bison.sub_manager import add_cookie_matcher
mocker.patch.object(plugin_config, "bison_to_me", True)
async with app.test_matcher(add_cookie_matcher) as ctx:
bot = ctx.create_bot(base=Bot)
event = fake_private_message_event(message=Message("添加cookie"), sender=fake_superuser)
ctx.receive_event(bot, event)
ctx.should_pass_rule()
ctx.should_pass_permission()
async with app.test_matcher(add_cookie_matcher) as ctx:
bot = ctx.create_bot(base=Bot)
event = fake_private_message_event(message=Message("添加cookie"), sender=fake_admin_user)
ctx.receive_event(bot, event)
ctx.should_not_pass_rule()
ctx.should_pass_permission()
@pytest.mark.usefixtures("_clear_db") @pytest.mark.usefixtures("_clear_db")
+2 -1
View File
@@ -42,4 +42,5 @@ async def test_without_permission(app: App):
True, True,
) )
ctx.should_pass_rule() ctx.should_pass_rule()
ctx.should_pass_permission() ctx.should_pass_permission(no_permission_matcher)
ctx.should_not_pass_permission(add_sub_matcher)
+2 -2
View File
@@ -19,6 +19,6 @@ async def test_http_error(app: App):
"https://example.com Headers({'host': 'example.com', 'accept': '*/*', 'accept-encoding': 'gzip, deflate'," "https://example.com Headers({'host': 'example.com', 'accept': '*/*', 'accept-encoding': 'gzip, deflate',"
" 'connection': 'keep-alive', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" " 'connection': 'keep-alive', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
" (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0'}) | [403] Headers({'content-length': '" " (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0'}) | [403] Headers({'content-length': '"
"15', 'content-type':" "14', 'content-type':"
' \'application/json\'}) {"error": "gg"}' ' \'application/json\'}) {"error":"gg"}'
] ]
Generated
+547 -414
View File
File diff suppressed because it is too large Load Diff