Files
daily-paper/app/services/claude_backend.py
T

85 lines
2.3 KiB
Python

"""Claude CLI 后端 — 调用 claude CLI 子进程生成总结。
和 pi_client.py 对称的接口,复用 prompt 构建、PDF 文本提取、JSON 提取逻辑。
"""
from __future__ import annotations
import asyncio
import logging
import uuid
from app.config import settings
logger = logging.getLogger(__name__)
class ClaudeTimeoutError(Exception):
pass
class ClaudeProcessError(Exception):
def __init__(self, returncode: int, stderr: str):
self.returncode = returncode
self.stderr = stderr
super().__init__(f"claude exited with code {returncode}: {stderr[:500]}")
async def call_claude(
prompt: str,
session_id: str | None = None,
fix_errors: list[str] | None = None,
) -> tuple[str, str]:
"""调用 claude CLI print 模式,返回 (stdout 文本, session_id)。
和 call_pi() 对称的接口,但 claude CLI 不需要文件路径和 pdf_mode——
所有内容已在 prompt 中准备好。
Args:
prompt: 完整的 prompt 文本
session_id: session ID(首次为 None 时自动生成)
fix_errors: 上一轮验证错误列表(用于重试)
"""
if session_id is None:
session_id = f"claude-summary-{uuid.uuid4().hex[:8]}"
cmd = [settings.CLAUDE_BIN, "-p", "--output-format", "text"]
if fix_errors and session_id:
# 重试:延续 session
cmd += ["--session-id", session_id, "--continue"]
else:
cmd += ["--session-id", session_id]
cmd.append(prompt)
logger.info(
"Calling claude (session=%s, fix=%s)",
session_id,
bool(fix_errors),
)
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
try:
stdout, stderr = await asyncio.wait_for(
proc.communicate(),
timeout=settings.SUMMARY_TIMEOUT_SECONDS,
)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
raise ClaudeTimeoutError(
f"claude timed out after {settings.SUMMARY_TIMEOUT_SECONDS}s"
)
if proc.returncode != 0:
raise ClaudeProcessError(
proc.returncode, stderr.decode("utf-8", errors="replace")
)
return stdout.decode("utf-8", errors="replace"), session_id