Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
15c20a3
refactor: 优化全局任务调度、修复多线程冲突与重构题库接口
Zropk66 Jun 1, 2026
589e0bc
feat(tiku): 新增手动输入模式并优化多线程并发安全与搜题机制
Zropk66 Jun 2, 2026
8fc2e28
fix(tiku): 增加搜题长度对齐防御与安全关闭 tqdm 进度条
Zropk66 Jun 2, 2026
ca6cb70
refactor(tiku): 优化连接测试锁粒度、规整手动模式逻辑并清理冗余比对
Zropk66 Jun 2, 2026
0acedc7
fix(tiku): 改善手动模式文本匹配逻辑,修复403时长刷新丢失与大模型并发锁
Zropk66 Jun 2, 2026
57ac976
fix(tiku): 在 SiliconFlow 连接测试中引入限频间隔等待以避免 API 触发频率限制
Zropk66 Jun 2, 2026
e3e88a5
refactor(tiku): 修复静态分析和圈复杂度过高的问题
Zropk66 Jun 2, 2026
90c77fd
refactor: 修复静态分析和圈复杂度过高的问题和优化PEP约定
Zropk66 Jun 2, 2026
d382cf6
refactor: 修复静态分析和圈复杂度过高的问题和优化PEP约定
Zropk66 Jun 2, 2026
19c334f
Merge remote-tracking branch 'origin/feat/manual-mode' into feat/manu…
Zropk66 Jun 2, 2026
70dd90e
refactor: 修复静态分析和圈复杂度过高的问题和优化PEP约定,优化了代码格式
Zropk66 Jun 2, 2026
53bf763
refactor: 优化了代码格式
Zropk66 Jun 2, 2026
9877df1
Merge remote-tracking branch 'origin/feat/manual-mode' into feat/manu…
Zropk66 Jun 2, 2026
43a64a1
refactor
Zropk66 Jun 2, 2026
84d0364
refactor
Zropk66 Jun 2, 2026
d45fce6
refactor
Zropk66 Jun 2, 2026
372ccd8
Merge branch 'main' into feat/manual-mode
Zropk66 Jun 2, 2026
9909329
refactor
Zropk66 Jun 2, 2026
c56a41f
Merge remote-tracking branch 'origin/feat/manual-mode' into feat/manu…
Zropk66 Jun 2, 2026
a5faddf
refactor
Zropk66 Jun 2, 2026
7abf7f0
feat: 增加登录成功后获取登录用户名。
Zropk66 Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
744 changes: 625 additions & 119 deletions api/answer.py

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions api/answer_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ def check_multiple(answer):


def check_judgement(answer, true_list, false_list):
if answer in true_list:
val = str(answer).strip().lower()
if val in ['true', 't', '1', '对', '正确', '√', '是', 'yes', 'y'] or val in [x.lower() for x in true_list]:
return 1
elif answer in false_list:
elif val in ['false', 'f', '0', '错', '错误', '×', '否', 'no', 'n', '不对', '不正确'] or val in [x.lower() for x in false_list]:
return 0
else:
return -1
Expand All @@ -41,6 +42,11 @@ def check_completion(answer):


def check_answer(answer, type, tiku): # 只会写小杯代码,这里用个tiku感觉怪怪的,但先这么写着
# 如果是手动模式或多题库回退包装器,直接信任
# (手动模式豁免常规校验;回退包装器因其子题库在各自环节均已单独校验过,此处无需二次校验,以防二次过滤误杀)
if getattr(tiku, 'is_manual', False) or getattr(tiku, 'skip_answer_validation', False):
return True

if type == 'single':
if check_single(answer) and check_judgement(answer, tiku.true_list, tiku.false_list) == -1:
return True
Expand Down
597 changes: 317 additions & 280 deletions api/base.py

Large diffs are not rendered by default.

46 changes: 35 additions & 11 deletions api/captcha.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,34 @@
from random import randint
from typing import Optional

from ddddocr import DdddOcr
from loguru import logger
from requests import session

try:
from ddddocr import DdddOcr
HAS_DDDDOCR = True
except ImportError:
DdddOcr = None
HAS_DDDDOCR = False

def ocr_init() -> DdddOcr:

def ocr_init() -> Optional[DdddOcr]:
"""
初始化OCR对象

Returns: DdddOcr对象
"""
return DdddOcr(show_ad=False)
if not HAS_DDDDOCR:
logger.warning("未检测到 ddddocr 依赖,自动验证码识别将不可用。如遇403限制请在浏览器端手动完成验证。")
return None
try:
return DdddOcr(show_ad=False)
except Exception as e:
logger.warning(f"ddddocr 初始化失败: {e},自动验证码识别将不可用")
return None


_MISSING = object()


class CxCaptcha:
Expand All @@ -50,14 +67,14 @@ class CxCaptcha:
'submit': '/html/processVerify.ac'
}

def __init__(self, user_agent: str, cookies: str, ocr: Optional[DdddOcr] = None):
def __init__(self, user_agent: str, cookies: str, ocr: Optional[DdddOcr] = _MISSING):
"""
初始化 CxCaptcha 实例。

Args:
user_agent (str): 用户代理字符串。
cookies (str): 会话 cookies。
ocr (DdddOcr, optional): 已初始化的 DdddOcr 对象。默认为 None。据DdddOcr官方说明,每次初始化和初始化后的首次识别速度都非常慢,所以推荐传入一个现成的DdddOcr对象实现复用。
ocr (DdddOcr, optional): 已初始化的 DdddOcr 对象。如果未提供则自动初始化;如果显式传入 None 则禁用 OCR。据DdddOcr官方说明,每次初始化和初始化后的首次识别速度都非常慢,所以推荐传入一个现成的DdddOcr对象实现复用。
"""

self.user_agent = user_agent
Expand All @@ -70,7 +87,7 @@ def __init__(self, user_agent: str, cookies: str, ocr: Optional[DdddOcr] = None)
})
self.s.verify = False

self.ocr = ocr if ocr else ocr_init()
self.ocr = ocr_init() if ocr is _MISSING else ocr

def getCaptcha(self) -> Optional[bytes]:
"""
Expand Down Expand Up @@ -115,11 +132,14 @@ def recognition(self, img: bytes) -> str:
使用 DdddOcr 对验证码图片进行识别。

Args:
img (bytes): 验证码图片的二进制数据
img (bytes): 验证码图片的二进制数据.

Returns:
str: 返回识别出的验证码字符串。
"""
if not self.ocr:
logger.error("ddddocr 实例未成功初始化,无法执行验证码识别")
raise RuntimeError("ddddocr is not available")
res = self.ocr.classification(img)
return res

Expand All @@ -132,8 +152,12 @@ def try_pass(self) -> bool:
Returns:
bool: 如果验证码成功通过验证,则返回 True;否则返回 False。
"""
cap_img = self.getCaptcha()
if not cap_img:
try:
cap_img = self.getCaptcha()
if not cap_img:
return False
cap_token = self.recognition(cap_img)
return self.submitCaptcha(cap_token)
except Exception as e:
logger.error(f"验证码自动验证时发生异常: {e}")
return False
cap_token = self.recognition(cap_img)
return self.submitCaptcha(cap_token)
43 changes: 29 additions & 14 deletions api/cookies.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
# -*- coding: utf-8 -*-
import os.path
import threading

import requests

from api.config import GlobalConst as gc

# 定义全局 Cookie 文件锁,保证读写绝对安全
cookie_lock = threading.RLock()


def save_cookies(session: requests.Session):
buffer=""
with open(gc.COOKIES_PATH, "w") as f:
with cookie_lock:
buffer = ""
for k, v in session.cookies.items():
buffer += f"{k}={v};"
buffer = buffer.removesuffix(";")
f.write(buffer)
with open(gc.COOKIES_PATH, "w") as f:
f.write(buffer)


def use_cookies() -> dict:
if not os.path.exists(gc.COOKIES_PATH):
return {}

cookies={}
with open(gc.COOKIES_PATH, "r") as f:
buffer = f.read().strip()
for item in buffer.split(";"):
k, v = item.strip().split("=")
cookies[k] = v

return cookies
with cookie_lock:
if not os.path.exists(gc.COOKIES_PATH):
return {}

cookies = {}
try:
with open(gc.COOKIES_PATH, "r") as f:
buffer = f.read().strip()
if not buffer:
return {}
for item in buffer.split(";"):
item = item.strip()
if not item:
continue
parts = item.split("=", 1)
if len(parts) == 2:
cookies[parts[0]] = parts[1]
except Exception:
return {}

return cookies
26 changes: 21 additions & 5 deletions api/decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,16 +417,32 @@ def _extract_form_data(soup: BeautifulSoup) -> Dict[str, Any]:
"""从BeautifulSoup对象中提取表单数据"""
form_data = {}
form_tag = soup.find("form")

if not form_tag:
return form_data

# 提取所有非答案字段的input
for input_tag in form_tag.find_all("input"):
if "name" not in input_tag.attrs or "answer" in input_tag.attrs["name"]:
name_attr = input_tag.attrs.get("name")
if name_attr is None:
continue
form_data[input_tag.attrs["name"]] = input_tag.attrs.get("value", "")


if isinstance(name_attr, list):
name_str = str(name_attr[0]) if name_attr else ""
else:
name_str = str(name_attr)

if not name_str or "answer" in name_str:
continue

val_attr = input_tag.attrs.get("value", "")
if isinstance(val_attr, list):
val_str = "".join(str(v) for v in val_attr)
else:
val_str = str(val_attr)

form_data[name_str] = val_str

return form_data


Expand Down
6 changes: 5 additions & 1 deletion api/font_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ def __init_font_map(self, html_content: str) -> None:
html_content: 包含加密字体信息的HTML内容
"""
try:
soup = BeautifulSoup(html_content, "lxml")
try:
soup = BeautifulSoup(html_content, "lxml")
except (ImportError, LookupError, ValueError) as e:
logger.trace(f"lxml parser not available, falling back to html.parser: {e}")
soup = BeautifulSoup(html_content, "html.parser")
style_tag = soup.find("style", id="cxSecretStyle")

if not style_tag or not style_tag.text:
Expand Down
4 changes: 1 addition & 3 deletions api/live.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
# -*- coding: utf-8 -*-
import json
import time
from urllib import parse

from api.logger import logger

from api.base import SessionManager
from api.config import GlobalConst as gc
from api.logger import logger


class Live:
Expand Down
8 changes: 3 additions & 5 deletions api/live_process.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import time

from api.config import GlobalConst as gc
from api.live import Live
from api.logger import logger
import time
import threading


class LiveProcessor:
@staticmethod
Expand Down Expand Up @@ -33,10 +31,10 @@ def run_live(live: Live, speed: float = 1.0):

# 循环提交时长(每59秒一次,模拟持续观看)
for i in range(total_minutes):
logger.info(f"直播'{live.name}'已观看{i+1}/{total_minutes}分钟")
logger.info(f"直播'{live.name}'已观看{i + 1}/{total_minutes}分钟")
success = live.do_finish() # 提交当前时长
if not success:
logger.warning(f"第{i+1}分钟时长提交失败,将重试")
logger.warning(f"第{i + 1}分钟时长提交失败,将重试")
# 失败重试一次
time.sleep(5)
live.do_finish()
Expand Down
28 changes: 26 additions & 2 deletions api/logger.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
import sys

from loguru import logger
from tqdm import tqdm
import sys

tqdm_stream = sys.stderr

# 日志缓冲区,用于在手动答题时缓存后台日志,答题结束后统一输出
log_buffer = []
MAX_LOG_BUFFER_SIZE = 1000


def tqdm_sink(msg):
tqdm.write(msg.rstrip(), file=tqdm_stream)
manual_locked = False
try:
# 动态获取 api.answer 模块中的 TikuManual 锁,避免循环导入
if 'api.answer' in sys.modules:
TikuManual = getattr(sys.modules['api.answer'], 'TikuManual', None)
if TikuManual and getattr(TikuManual, '_manual_lock', None):
manual_locked = TikuManual._manual_lock.locked()
except Exception:
pass

if manual_locked:
if len(log_buffer) < MAX_LOG_BUFFER_SIZE:
log_buffer.append(msg)
else:
if log_buffer:
for buffered_msg in log_buffer:
tqdm.write(buffered_msg.rstrip(), file=tqdm_stream)
log_buffer.clear()
tqdm.write(msg.rstrip(), file=tqdm_stream)
tqdm_stream.flush()

logger.remove()
Expand Down
11 changes: 11 additions & 0 deletions config_template.ini
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ jobs = 4

; 遇到关闭任务点时的行为: retry-重试(默认), continue-继续
notopen_action = retry

; 任务失败/未开启章节的重试等待时间, 单位秒(默认1.0)
retry_interval = 1.0
[tiku]
; 可选项 :
; 1. TikuYanxi(言溪题库 https://tk.enncy.cn/)
Expand All @@ -27,6 +30,7 @@ notopen_action = retry
; 4. AI(需自行寻找兼容openai格式的API Endpoint和Key)
; 5. SiliconFlow(硅基流动AI:https://siliconflow.cn/)
; 6. TikuGo(GO题/网课小工具题库 https://q.icodef.com/)
; 7. TikuManual(手动输入模式,命令行输入)
; 支持配置多个题库回退,按顺序依次查询,示例:provider=TikuGo,TikuYanxi,TikuLike
provider=TikuYanxi

Expand Down Expand Up @@ -97,6 +101,13 @@ siliconflow_endpoint = https://api.siliconflow.cn/v1/chat/completions
; 请求间隔时间
; min_interval_seconds = 3 请将硅基流动请求间隔时间填入第52行,不要去掉此行注释,否则会重复导致报错(临时解决)

; 手动模式配置
; manual_mode_default: batch-一次性输入所有(默认), single-一题一输入
manual_mode_default=batch
; manual_mode_separator: 批量输入时的答案分割符,默认为分号“;”
; 可选值:分号 ;、逗号 ,、空格 space、换行 \n
manual_mode_separator=;

; 用于判断判断题对应的选项,不要留有空格,不要留有引号,逗号为英文逗号
true_list=正确,对,√,是
false_list=错误,错,×,否,不对,不正确
Expand Down
Loading