Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 37 additions & 23 deletions api/answer.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,13 +490,16 @@ def _query_single(self, token: str = "", query: str = "") -> str:

def _parse_response(self, response):
"""
解析API响应
Parse the HTTP API response and extract an answer for the knowledge service.

Attempts to decode the response as JSON, validates the expected structure (results.output.questionType and results.output.answer),
and delegates extraction to _extract_answer_by_type.

Parameters:
response: HTTP response object with a .json() method.

Args:
response: HTTP响应对象

Returns:
解析后的答案,如果解析失败则返回None
Extracted answer string when the response contains a valid result, `None` otherwise.
"""
try:
res_json = response.json()
Expand All @@ -511,14 +514,7 @@ def _parse_response(self, response):
msg = res_json.get('message', '')
if msg:
logger.info(f'{self.name}响应消息: {msg}')

# 检查API返回的code字段,判断请求是否成功
code = res_json.get('code', 1)
if code != 1:
error_msg = res_json.get('message', '未知错误')
logger.error(f'{self.name}API返回错误: {error_msg}')
return None


results = res_json.get('results', {})
if not results or not isinstance(results, dict):
logger.error(f'{self.name}查询结果格式错误: API返回结果中results字段格式不正确')
Expand Down Expand Up @@ -601,28 +597,31 @@ def _extract_answer_by_type(self, q_type: str, answer: dict) -> str:
return None

def get_api_balance(self, token:str = ""):
"""
Fetches the available balance from the configured balance API for the provided bearer token.

Parameters:
token (str): Bearer token used for authorization when querying the balance API. If empty or invalid, the method returns 0.

Returns:
int: The balance reported by the API as an integer, or 0 if the token is missing, the request fails, the response is malformed, or any error occurs.
"""
if not token:
logger.error(f'{self.name}获取余额失败: 未提供有效的token')
return 0

temp_headers = self._headers.copy()
temp_headers['Authorization'] = f'Bearer {token}'
try:
res = requests.post(
res = requests.get(
self.balance_api,
headers=temp_headers,
verify=False,
timeout=self._timeout
)
if res.status_code == 200:
res_json = res.json()
code = res_json.get('code', 0)
if code == 1:
return int(res_json.get("balance", 0))
else:
error_msg = res_json.get('message', '未知错误')
logger.error(f'{self.name}获取余额失败: {error_msg}')
return 0
return int(res_json.get("balance", 0))
else:
logger.error(f'{self.name}请求余额接口失败,状态码: {res.status_code}')
return 0
Expand Down Expand Up @@ -664,13 +663,28 @@ def load_tokens(self) -> None:

def load_config(self) -> None:
# 从配置中获取参数,提供默认值
"""
Load LIKE provider configuration into instance attributes.

Reads LIKE-specific settings from self._conf and assigns defaults when keys are missing:
- Sets self._search from 'likeapi_search' (default: False).
- Sets self._model from 'likeapi_model' (default: None).
- Sets self._vision from 'likeapi_vision' (default: True).
- Sets self._retry from 'likeapi_retry' (default: True).
- Sets self._retry_times from 'likeapi_retry_times', cast to int (default: 3).
"""
self._search = self._conf.get('likeapi_search', False)
self._model = self._conf.get('likeapi_model', None)
self._vision = self._conf.get('likeapi_vision', True)
self._retry = self._conf.get("likeapi_retry", True)
self._retry_times = self._conf.get("likeapi_retry_times", 3)
self._retry_times = int(self._conf.get("likeapi_retry_times", 3))

def _init_tiku(self) -> None:
"""
Initialize the LIKE tiku by loading configuration and tokens, refreshing token balances, and disabling the provider if no tokens are available.

This loads provider-specific configuration, reads tokens from the configuration, calls update_times() to fetch token balances when tokens exist, and sets DISABLE to True and logs an error if no valid tokens were loaded.
"""
self.load_config()
self.load_tokens()
if self._tokens:
Expand Down Expand Up @@ -934,4 +948,4 @@ def _init_tiku(self):
self.model_name = self._conf.get('siliconflow_model', 'deepseek-ai/DeepSeek-V3')


self.min_interval = int(self._conf.get('min_interval_seconds', 3))
self.min_interval = int(self._conf.get('min_interval_seconds', 3))
40 changes: 37 additions & 3 deletions api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,25 @@ def study_document(self, _course, _job) -> StudyResult:

def study_work(self, _course, _job, _job_info) -> StudyResult:
# FIXME: 这一块可以单独搞一个类出来了,方法里面又套方法,每一次调用都会创建新的方法,十分浪费
"""
Attempt to answer and submit a work (homework/quiz) task using the configured question bank (tiku), falling back to randomized answers when necessary.

Parameters:
_course (dict): Course context containing keys like "courseId" and "clazzId".
_job (dict): Job/task metadata including "jobid", "enc" and identifiers required by the work API.
_job_info (dict): Task-specific information such as "knowledgeid", "ktoken" and "cpi" used to fetch questions.

Behavior:
- Fetches question data for the given work and tries to resolve answers via self.tiku.
- For each question, uses tiku-provided answers when they can be matched to options; otherwise generates a plausible random answer.
- Computes a coverage ratio from matched answers and decides whether to submit or only save answers based on tiku settings and coverage thresholds.
- Posts the final answers to the remote submission endpoint.
- Preserves original HTML content for diagnostic logging when option parsing fails.

Returns:
StudyResult.SUCCESS if answers were saved or submitted successfully;
StudyResult.ERROR on network, parsing, or submission failures.
"""
if self.tiku.DISABLE or not self.tiku:
return StudyResult.SUCCESS
_ORIGIN_HTML_CONTENT = "" # 用于配合输出网页源码, 帮助修复#391错误
Expand Down Expand Up @@ -678,11 +697,25 @@ def multi_cut(answer: str):
return res

def clean_res(res):
"""
Normalize and clean an answer string or a list of answer strings.

This function accepts a single string or a list of strings, ensures the input is treated as a list,
and for each element removes a leading ASCII letter only when the string length is greater than one,
removes common punctuation characters (.,!?;: and their Chinese equivalents), and trims surrounding whitespace.

Parameters:
res (str | list[str]): A single answer string or a list of answer strings to normalize.

Returns:
list[str]: A list of cleaned and trimmed answer strings.
"""
cleaned_res = []
if isinstance(res, str):
res = [res]
for c in res:
cleaned = re.sub(r'^[A-Za-z]|[.,!?;:,。!?;:]', '', c)
# 仅在字符串长度大于1时才尝试去除开头的字母编号,防止误删单个字母答案
cleaned = re.sub(r'^[A-Za-z]|[.,!?;:,。!?;:]', '', c) if len(c) > 1 else c
cleaned_res.append(cleaned.strip())

return cleaned_res
Expand Down Expand Up @@ -788,6 +821,7 @@ def fetch_response():
is_subsequence(_a, o) # 去掉各种符号和前面ABCD的答案应当是选项的子序列
):
answer += o[:1]
break # 找到匹配项后立即停止,防止重复添加
# 对答案进行排序, 否则会提交失败
answer = "".join(sorted(answer))
# else 如果分割失败那么就直接到下面去随机选
Expand All @@ -804,7 +838,7 @@ def fetch_response():
answer = "true" if self.tiku.judgement_select(res) else "false"
elif q["type"] == "completion":
if isinstance(res, list):
answer = "".join(answer)
answer = "".join(res)
elif isinstance(res, str):
answer = res
else:
Expand Down Expand Up @@ -929,4 +963,4 @@ def study_emptypage(self, _course, point):
return StudyResult.ERROR
else:
logger.info(f"空页面任务完成 -> {point['title']}")
return StudyResult.SUCCESS
return StudyResult.SUCCESS