{"record":{"id":"480aab3cf8072255","repo":"ZhuLinsen/daily_stock_analysis","slug":"error-480aab","errorCode":null,"errorMessage":"图片内容为空","messagePattern":"图片内容为空","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"src/services/image_stock_extractor.py","lineNumber":370,"sourceCode":"    优先级：Gemini -> Anthropic -> OpenAI（首个可用）。\n    支持多 Key 轮询与重试（最多 3 次，指数退避）。\n\n    Args:\n        image_bytes: 原始图片字节\n        mime_type: MIME 类型（如 image/jpeg, image/png）\n\n    Returns:\n        (items, raw_text) - items 为 [(code, name?, confidence), ...]，raw_text 为原始 LLM 响应。\n\n    Raises:\n        ValueError: 图片无效、未配置 Vision API 或提取失败时。\n    \"\"\"\n    mime_type = (mime_type or \"image/jpeg\").strip().lower().split(\";\")[0].strip()\n    if mime_type not in ALLOWED_MIME:\n        raise ValueError(f\"不支持的图片类型: {mime_type}。允许: {list(ALLOWED_MIME)}\")\n\n    if not image_bytes:\n        raise ValueError(\"图片内容为空\")\n\n    if len(image_bytes) > MAX_SIZE_BYTES:\n        raise ValueError(f\"Image too large (max {MAX_SIZE_BYTES // (1024 * 1024)}MB)\")\n\n    _verify_image_magic_bytes(image_bytes, mime_type)\n\n    image_b64 = base64.b64encode(image_bytes).decode(\"ascii\")\n    model = _resolve_vision_model()\n    keys = _get_api_keys_for_model(model, get_config())\n\n    last_error: Optional[Exception] = None\n    for attempt in range(3):\n        try:\n            key = random.choice(keys) if keys else None\n            raw = _call_litellm_vision(image_b64, mime_type, api_key=key)\n            logger.debug(\"[ImageExtractor] raw LLM response:\\n%s\", raw)\n            items = _parse_items_from_text(raw)\n            logger.info(","sourceCodeStart":352,"sourceCodeEnd":388,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/image_stock_extractor.py#L352-L388","documentation":"Simple guard in extract_stock_codes_from_image: image_bytes is empty (falsy) after MIME validation passed. The function refuses to base64-encode and call the vision API on an empty payload.","triggerScenarios":"Calling with b'' or None as image_bytes; upstream file read returning empty bytes (0-byte upload, truncated multipart part).","commonSituations":"Front-end sending an empty FormData entry; file handle read after position exhausted; race where upload file was deleted before read completed.","solutions":["Check the upload path: verify file.size > 0 before submitting","Verify the read: seek(0) before .read() if reusing a file handle","Add client-side validation rejecting empty files"],"exampleFix":"# before\nwith open(p,'rb') as f: data = f.read()  # f at EOF -> b''\nitems, raw = extract_stock_codes_from_image(data, 'image/png')\n# after\nwith open(p,'rb') as f:\n    f.seek(0)\n    data = f.read()\nassert data, 'empty file'\nitems, raw = extract_stock_codes_from_image(data, 'image/png')","handlingStrategy":"validation","validationCode":"if not image_bytes:\n    raise ValueError('上传文件为空，请重新选择文件')\n# or: if uploaded.size == 0: reject","typeGuard":"def has_image_payload(b: bytes | None) -> bool:\n    return isinstance(b, (bytes, bytearray)) and len(b) > 0","tryCatchPattern":null,"preventionTips":["Reject 0-byte files at the upload form (file.size > 0)","seek(0) before read() when reusing file handles","Never fire the extraction request when the client-side File object is empty"],"tags":["validation","image","upload"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}