diff --git a/pip_audit_local.json b/pip_audit_local.json new file mode 100644 index 0000000..50fc8ff --- /dev/null +++ b/pip_audit_local.json @@ -0,0 +1 @@ +{"dependencies": [{"name": "a2a", "version": "0.44", "vulns": []}, {"name": "a2a-sdk", "version": "0.3.24", "vulns": []}, {"name": "acryl-datahub", "version": "1.5.0.9", "vulns": []}, {"name": "aiofiles", "version": "24.1.0", "vulns": []}, {"name": "aiohappyeyeballs", "version": "2.6.1", "vulns": []}, {"name": "aiohttp", "version": "3.13.3", "vulns": [{"id": "CVE-2026-34515", "fix_versions": ["3.13.4"], "aliases": ["GHSA-p998-jp59-783m"], "description": "### Summary On Windows the static resource handler may expose information about a NTLMv2 remote path. ### Impact If an application is running on Windows, and using aiohttp's static resource handler (not recommended in production), then it may be possible for an attacker to extract the hash from an NTLMv2 path and then extract the user's credentials from there. ----- Patch: https://github.com/aio-libs/aiohttp/commit/0ae2aa076c84573df83fc1fdc39eec0f5862fe3d"}, {"id": "CVE-2026-34513", "fix_versions": ["3.13.4"], "aliases": ["GHSA-hcc4-c3v8-rx92"], "description": "### Summary An unbounded DNS cache could result in excessive memory usage possibly resulting in a DoS situation. ### Impact If an application makes requests to a very large number of hosts, this could cause the DNS cache to continue growing and slowly use excessive amounts of memory. ----- Patch: https://github.com/aio-libs/aiohttp/commit/c4d77c3533122be353b8afca8e8675e3b4cbda98"}, {"id": "CVE-2026-34516", "fix_versions": ["3.13.4"], "aliases": ["GHSA-m5qp-6w8w-w647"], "description": "### Summary A response with an excessive number of multipart headers may be allowed to use more memory than intended, potentially allowing a DoS vulnerability. ### Impact Multipart headers were not subject to the same size restrictions in place for normal headers, potentially allowing substantially more data to be loaded into memory than intended. However, other restrictions in place limit the impact of this vulnerability. ----- Patch: https://github.com/aio-libs/aiohttp/commit/8a74257b3804c9aac0bf644af93070f68f6c5a6f"}, {"id": "CVE-2026-34517", "fix_versions": ["3.13.4"], "aliases": ["GHSA-3wq7-rqq7-wx6j"], "description": "### Summary For some multipart form fields, aiohttp read the entire field into memory before checking client_max_size. ### Impact If an application uses `Request.post()` an attacker can send a specially crafted multipart request to force significant temporary memory allocation even when the request is ultimately rejected. ----- Patch: https://github.com/aio-libs/aiohttp/commit/cbb774f38330563422ca0c413a71021d7b944145"}, {"id": "CVE-2026-34519", "fix_versions": ["3.13.4"], "aliases": ["GHSA-mwh4-6h8g-pg8w"], "description": "### Summary An attacker who controls the `reason` parameter when creating a `Response` may be able to inject extra headers or similar exploits. ### Impact In the unlikely situation that an application allows untrusted data to be used in the response's `reason` parameter, then an attacker could manipulate the response to send something different from what the developer intended. ----- Patch: https://github.com/aio-libs/aiohttp/commit/53b35a2f8869c37a133e60bf1a82a1c01642ba2b"}, {"id": "CVE-2026-34518", "fix_versions": ["3.13.4"], "aliases": ["GHSA-966j-vmvw-g2g9"], "description": "### Summary When following redirects to a different origin, aiohttp drops the Authorization header, but retains the Cookie and Proxy-Authorization headers. ### Impact The Cookie and Proxy-Authorizations headers could contain sensitive information which may be leaked to an unintended party after following a redirect. ----- Patch: https://github.com/aio-libs/aiohttp/commit/5351c980dcec7ad385730efdf4e1f4338b24fdb6"}, {"id": "CVE-2026-34520", "fix_versions": ["3.13.4"], "aliases": ["GHSA-63hf-3vf5-4wqf"], "description": "### Summary The C parser (the default for most installs) accepted null bytes and control characters is response headers. ### Impact An attacker could send header values that are interpreted differently than expected due to the presence of control characters. For example, `request.url.origin()` may return a different value than the raw Host header, or what a reverse proxy interpreted it as., potentially resulting in some kind of security bypass. ----- Patch: https://github.com/aio-libs/aiohttp/commit/9370b9714a7a56003cacd31a9b4ae16eab109ba4"}, {"id": "CVE-2026-34525", "fix_versions": ["3.13.4"], "aliases": ["GHSA-c427-h43c-vf67"], "description": "### Summary Multiple Host headers were allowed in aiohttp. ### Impact Mostly this doesn't affect aiohttp security itself, but if a reverse proxy is applying security rules depending on the target Host, it is theoretically possible that the proxy and aiohttp could process different host names, possibly resulting in bypassing a security check on the proxy and getting a request processed by aiohttp in a privileged sub app when using `Application.add_domain()`. ----- Patch: https://github.com/aio-libs/aiohttp/commit/e00ca3cca92c465c7913c4beb763a72da9ed8349 Patch: https://github.com/aio-libs/aiohttp/commit/53e2e6fc58b89c6185be7820bd2c9f40216b3000"}, {"id": "CVE-2026-22815", "fix_versions": ["3.13.4"], "aliases": ["GHSA-w2fm-2cpv-w7v5"], "description": "### Summary Insufficient restrictions in header/trailer handling could cause uncapped memory usage. ### Impact An application could cause memory exhaustion when receiving an attacker controlled request or response. A vulnerable web application could mitigate these risks with a typical reverse proxy configuration. ----- Patch: https://github.com/aio-libs/aiohttp/commit/0c2e9da51126238a421568eb7c5b53e5b5d17b36"}, {"id": "CVE-2026-34514", "fix_versions": ["3.13.4"], "aliases": ["GHSA-2vrm-gr82-f7m5"], "description": "### Summary An attacker who controls the `content_type` parameter in aiohttp could use this to inject extra headers or similar exploits. ### Impact If an application allows untrusted data to be used for the multipart `content_type` parameter when constructing a request, an attacker may be able to manipulate the request to send something other than what the developer intended. ----- Patch: https://github.com/aio-libs/aiohttp/commit/9a6ada97e2c6cf1ce31727c6c9fcea17c21f6f06"}]}, {"name": "aiosignal", "version": "1.4.0", "vulns": []}, {"name": "alibabacloud-credentials", "version": "1.0.7", "vulns": []}, {"name": "alibabacloud-credentials-api", "version": "1.0.0", "vulns": []}, {"name": "alibabacloud-darabonba-array", "version": "0.1.0", "vulns": []}, {"name": "alibabacloud-darabonba-encode-util", "version": "0.0.2", "vulns": []}, {"name": "alibabacloud-darabonba-map", "version": "0.0.1", "vulns": []}, {"name": "alibabacloud-darabonba-signature-util", "version": "0.0.4", "vulns": []}, {"name": "alibabacloud-darabonba-string", "version": "0.0.4", "vulns": []}, {"name": "alibabacloud-endpoint-util", "version": "0.0.4", "vulns": []}, {"name": "alibabacloud-gateway-pop", "version": "0.1.3", "vulns": []}, {"name": "alibabacloud-gateway-spi", "version": "0.0.3", "vulns": []}, {"name": "alibabacloud-kms20160120", "version": "2.4.0", "vulns": []}, {"name": "alibabacloud-openapi-util", "version": "0.2.4", "vulns": []}, {"name": "alibabacloud-tea", "version": "0.4.3", "vulns": []}, {"name": "alibabacloud-tea-openapi", "version": "0.4.3", "vulns": []}, {"name": "alibabacloud-tea-util", "version": "0.3.14", "vulns": []}, {"name": "alibabacloud-tea-xml", "version": "0.0.3", "vulns": []}, {"name": "annotated-doc", "version": "0.0.4", "vulns": []}, {"name": "annotated-types", "version": "0.7.0", "vulns": []}, {"name": "anyio", "version": "4.12.1", "vulns": []}, {"name": "apscheduler", "version": "3.11.2", "vulns": []}, {"name": "argon2-cffi", "version": "25.1.0", "vulns": []}, {"name": "argon2-cffi-bindings", "version": "25.1.0", "vulns": []}, {"name": "asgiref", "version": "3.11.1", "vulns": []}, {"name": "attrs", "version": "25.4.0", "vulns": []}, {"name": "automat", "version": "25.4.16", "vulns": []}, {"name": "avro", "version": "1.12.1", "vulns": []}, {"name": "avro-gen3", "version": "0.7.16", "vulns": []}, {"name": "backoff", "version": "2.2.1", "vulns": []}, {"name": "bcrypt", "version": "5.0.0", "vulns": []}, {"name": "boolean-py", "version": "5.0", "vulns": []}, {"name": "build", "version": "1.4.0", "vulns": []}, {"name": "cachecontrol", "version": "0.14.4", "vulns": []}, {"name": "cached-property", "version": "2.0.1", "vulns": []}, {"name": "cachetools", "version": "7.0.3", "vulns": []}, {"name": "certifi", "version": "2026.2.25", "vulns": []}, {"name": "cffi", "version": "2.0.0", "vulns": []}, {"name": "charset-normalizer", "version": "3.4.5", "vulns": []}, {"name": "chromadb", "version": "1.5.2", "vulns": []}, {"name": "click", "version": "8.3.1", "vulns": []}, {"name": "click-default-group", "version": "1.2.4", "vulns": []}, {"name": "click-spinner", "version": "0.1.10", "vulns": []}, {"name": "colorama", "version": "0.4.6", "vulns": []}, {"name": "constantly", "version": "23.10.4", "vulns": []}, {"name": "cryptography", "version": "44.0.3", "vulns": [{"id": "CVE-2026-26007", "fix_versions": ["46.0.5"], "aliases": ["GHSA-r6ph-v2qm-q3c2"], "description": "## Vulnerability Summary The `public_key_from_numbers` (or `EllipticCurvePublicNumbers.public_key()`), `EllipticCurvePublicNumbers.public_key()`, `load_der_public_key()` and `load_pem_public_key()` functions do not verify that the point belongs to the expected prime-order subgroup of the curve. This missing validation allows an attacker to provide a public key point `P` from a small-order subgroup. This can lead to security issues in various situations, such as the most commonly used signature verification (ECDSA) and shared key negotiation (ECDH). When the victim computes the shared secret as `S = [victim_private_key]P` via ECDH, this leaks information about `victim_private_key mod (small_subgroup_order)`. For curves with cofactor > 1, this reveals the least significant bits of the private key. When these weak public keys are used in ECDSA , it's easy to forge signatures on the small subgroup. Only SECT curves are impacted by this. ## Credit This vulnerability was discovered by: - XlabAI Team of Tencent Xuanwu Lab - Atuin Automated Vulnerability Discovery Engine"}, {"id": "CVE-2026-34073", "fix_versions": ["46.0.6"], "aliases": ["GHSA-m959-cc7f-wv43"], "description": "## Summary In versions of cryptography prior to 46.0.5, DNS name constraints were only validated against SANs within child certificates, and not the \"peer name\" presented during each validation. Consequently, cryptography would allow a peer named `bar.example.com` to validate against a wildcard leaf certificate for `*.example.com`, even if the leaf's parent certificate (or upwards) contained an excluded subtree constraint for `bar.example.com`. This behavior resulted from a gap between RFC 5280 (which defines Name Constraint semantics) and RFC 9525 (which defines service identity semantics): put together, neither states definitively whether Name Constraints should be applied to peer names. To close this gap, cryptography now conservatively rejects any validation where the peer name would be rejected by a name constraint if it were a SAN instead. In practice, exploitation of this bypass requires an uncommon X.509 topology, one that the Web PKI avoids because it exhibits these kinds of problems. Consequently, we consider this a medium-to-low impact severity. See CVE-2025-61727 for a similar bypass in Go's `crypto/x509`. ## Remediation Users should upgrade to 46.0.6 or newer. ## Attribution Reporter: @1seal"}]}, {"name": "cssselect", "version": "1.4.0", "vulns": []}, {"name": "cyclonedx-python-lib", "version": "11.7.0", "vulns": []}, {"name": "darabonba-core", "version": "1.0.5", "vulns": []}, {"name": "decompyle3", "version": "3.9.3", "vulns": []}, {"name": "defusedxml", "version": "0.7.1", "vulns": []}, {"name": "deprecated", "version": "1.3.1", "vulns": []}, {"name": "distro", "version": "1.9.0", "vulns": []}, {"name": "docker", "version": "7.1.0", "vulns": []}, {"name": "durationpy", "version": "0.10", "vulns": []}, {"name": "et-xmlfile", "version": "2.0.0", "vulns": []}, {"name": "expandvars", "version": "1.1.2", "vulns": []}, {"name": "fastapi", "version": "0.128.0", "vulns": []}, {"name": "filelock", "version": "3.25.0", "vulns": []}, {"name": "flatbuffers", "version": "25.12.19", "vulns": []}, {"name": "frozenlist", "version": "1.8.0", "vulns": []}, {"name": "fsspec", "version": "2026.2.0", "vulns": []}, {"name": "google-api-core", "version": "2.30.0", "vulns": []}, {"name": "google-auth", "version": "2.48.0", "vulns": []}, {"name": "googleapis-common-protos", "version": "1.72.0", "vulns": []}, {"name": "greenlet", "version": "3.4.0", "vulns": []}, {"name": "grpcio", "version": "1.78.0", "vulns": []}, {"name": "h11", "version": "0.16.0", "vulns": []}, {"name": "hf-xet", "version": "1.3.2", "vulns": []}, {"name": "html-table-takeout", "version": "1.1.2", "vulns": []}, {"name": "httpcore", "version": "1.0.9", "vulns": []}, {"name": "httptools", "version": "0.7.1", "vulns": []}, {"name": "httpx", "version": "0.28.1", "vulns": []}, {"name": "httpx-sse", "version": "0.4.3", "vulns": []}, {"name": "huggingface-hub", "version": "1.5.0", "vulns": []}, {"name": "humanfriendly", "version": "10.0", "vulns": []}, {"name": "hyperlink", "version": "21.0.0", "vulns": []}, {"name": "idna", "version": "3.11", "vulns": []}, {"name": "ijson", "version": "3.5.0", "vulns": []}, {"name": "importlib-metadata", "version": "8.7.1", "vulns": []}, {"name": "importlib-resources", "version": "6.5.2", "vulns": []}, {"name": "incremental", "version": "24.11.0", "vulns": []}, {"name": "iniconfig", "version": "2.3.0", "vulns": []}, {"name": "itemadapter", "version": "0.13.1", "vulns": []}, {"name": "itemloaders", "version": "1.4.0", "vulns": []}, {"name": "jiter", "version": "0.13.0", "vulns": []}, {"name": "jmespath", "version": "1.1.0", "vulns": []}, {"name": "json-logic", "version": "0.7.0a0", "vulns": []}, {"name": "jsonpatch", "version": "1.33", "vulns": []}, {"name": "jsonpointer", "version": "3.0.0", "vulns": []}, {"name": "jsonref", "version": "1.1.0", "vulns": []}, {"name": "jsonschema", "version": "4.26.0", "vulns": []}, {"name": "jsonschema-specifications", "version": "2025.9.1", "vulns": []}, {"name": "kubernetes", "version": "35.0.0", "vulns": []}, {"name": "langchain", "version": "1.2.3", "vulns": []}, {"name": "langchain-core", "version": "1.2.17", "vulns": [{"id": "CVE-2026-40087", "fix_versions": ["0.3.84", "1.2.28"], "aliases": ["GHSA-926x-3r5x-gfhw"], "description": "LangChain's f-string prompt-template validation was incomplete in two respects. First, some prompt template classes accepted f-string templates and formatted them without enforcing the same attribute-access validation as `PromptTemplate`. In particular, `DictPromptTemplate` and `ImagePromptTemplate` could accept templates containing attribute access or indexing expressions and subsequently evaluate those expressions during formatting. Examples of the affected shape include: ```python \"{message.additional_kwargs[secret]}\" \"https://example.com/{image.__class__.__name__}.png\" ``` Second, f-string validation based on parsed top-level field names did not reject nested replacement fields inside format specifiers. For example: ```python \"{name:{name.__class__.__name__}}\" ``` In this pattern, the nested replacement field appears in the format specifier rather than in the top-level field name. As a result, earlier validation based on parsed field names did not reject the template even though Python formatting would still attempt to resolve the nested expression at runtime. ## Affected usage This issue is only relevant for applications that accept untrusted template strings, rather than only untrusted template variable values. In addition, practical impact depends on what objects are passed into template formatting: - If applications only format simple values such as strings and numbers, impact is limited and may only result in formatting errors. - If applications format richer Python objects, attribute access and indexing may interact with internal object state during formatting. In many deployments, these conditions are not commonly present together. Applications that allow end users to author arbitrary templates often expose only a narrow set of simple template variables, while applications that work with richer internal Python objects often keep template structure under developer control. As a result, the highest-impact scenario is plausible but is not representative of all LangChain applications. Applications that use hardcoded templates or that only allow users to provide variable values are not affected by this issue. ## Impact The direct issue in `DictPromptTemplate` and `ImagePromptTemplate` allowed attribute access and indexing expressions to survive template construction and then be evaluated during formatting. When richer Python objects were passed into formatting, this could expose internal fields or nested data to prompt output, model context, or logs. The nested format-spec issue is narrower in scope. It bypassed the intended validation rules for f-string templates, but in simple cases it results in an invalid format specifier error rather than direct disclosure. Accordingly, its practical impact is lower than that of direct top-level attribute traversal. Overall, the practical severity depends on deployment. Meaningful confidentiality impact requires attacker control over the template structure itself, and higher impact further depends on the surrounding application passing richer internal Python objects into formatting. ## Fix The fix consists of two changes. First, LangChain now applies f-string safety validation consistently to `DictPromptTemplate` and `ImagePromptTemplate`, so templates containing attribute access or indexing expressions are rejected during construction and deserialization. Second, LangChain now rejects nested replacement fields inside f-string format specifiers. Concretely, LangChain validates parsed f-string fields and raises an error for: - variable names containing attribute access or indexing syntax such as `.` or `[]` - format specifiers containing `{` or `}` This blocks templates such as: ```python \"{message.additional_kwargs[secret]}\" \"https://example.com/{image.__class__.__name__}.png\" \"{name:{name.__class__.__name__}}\" ``` The fix preserves ordinary f-string formatting features such as standard format specifiers and conversions, including examples like: ```python \"{value:.2f}\" \"{value:>10}\" \"{value!r}\" ``` In addition, the explicit template-validation path now applies the same structural f-string checks before performing placeholder validation, ensuring that the security checks and validation checks remain aligned."}, {"id": "CVE-2026-44843", "fix_versions": ["0.3.85", "1.3.3"], "aliases": ["GHSA-pjwx-r37v-7724"], "description": "LangChain contains older runtime code paths that deserialize run inputs, run outputs, or other application-controlled payloads using overly broad object allowlists. These paths may call `load()` with `allowed_objects=\"all\"`. This does not enable arbitrary Python object deserialization, but it does allow any trusted LangChain-serializable object to be revived, which is broader than these runtime paths require. As a result, attacker-supplied LangChain serialized constructor dictionaries may cause trusted runtime paths to instantiate classes with untrusted constructor arguments. Applications are exposed only when all of the following are true: 1. The application accepts untrusted structured input, such as JSON, from a user or network request. 2. The application does not validate or canonicalize that input into an inert schema before invoking LangChain. 3. Attacker-controlled nested dictionaries or lists are preserved in LangChain run inputs or outputs. 4. The application uses an affected API path that later deserializes that run data. Known affected runtime surfaces include: - `RunnableWithMessageHistory` - `astream_log()` - `astream_events(version=\"v1\")` Related unsafe deserialization patterns may also affect applications that explicitly load serialized LangChain prompt or runnable objects from untrusted sources, including shared prompt stores, Hub artifacts with model configuration, or other application-controlled serialization stores. Applications that validate incoming requests against a fixed schema, such as coercing user input to a plain string or message-content field before invoking LangChain, are unlikely to expose this deserialization primitive. This release also fixes a related secret-marker validation bypass in the serialization and deserialization layer (`_is_lc_secret`). That issue creates an additional path by which attacker-controlled constructor dictionaries can avoid escaping during `dumps()` -> `loads()` round-trips and reach LangChain object revival logic. ## Impact An attacker who can submit untrusted structured input to an affected application, and have that structure preserved in LangChain run data, may be able to inject LangChain serialized constructor payloads such as: ```json { \"lc\": 1, \"type\": \"constructor\", \"id\": [\"langchain_core\", \"messages\", \"ai\", \"AIMessage\"], \"kwargs\": {\"content\": \"attacker-controlled content\"} } ``` If this payload reaches a broad `load()` call, LangChain may instantiate the referenced class instead of treating the payload as inert user data. Realistic impacts include: - Persistent chat-history poisoning when revived `AIMessage`, `HumanMessage`, or `SystemMessage` objects are stored by `RunnableWithMessageHistory`. - Prompt injection or behavior manipulation if attacker-controlled messages are later included in model context. - Instantiation of unexpected trusted LangChain objects with attacker-controlled constructor arguments. - Possible credential disclosure or server-side requests if a reachable object reads environment credentials, creates clients, or contacts attacker-controlled endpoints during initialization. - Additional prompt-template or runnable-configuration impacts in applications that separately load and execute untrusted serialized LangChain objects. ## Remediation LangChain will deprecate the affected APIs as part of this fix: - `RunnableWithMessageHistory` - `astream_log()` - `astream_events(version=\"v1\")` These are older code paths that are no longer recommended for new applications. They were not previously marked as deprecated, but recent LangChain documentation has primarily directed users toward newer streaming and memory patterns, including the `stream` API. Applications should migrate to the currently recommended APIs rather than continue depending on these older surfaces. Separately, LangChain will update `load()` and `loads()` to tighten deserialization behavior so broad object revival is not applied implicitly to untrusted or application-controlled payloads. The older runtime surfaces listed above are being deprecated rather than preserved as supported paths for broad runtime deserialization. This release also fixes a related secret-marker validation bypass in the serialization and deserialization layer (`_is_lc_secret`). That issue creates an additional path by which attacker-controlled constructor dictionaries can avoid escaping during `dumps()` -> `loads()` round-trips and reach LangChain object revival logic. ## Guidance for `load()` and `loads()` `load()` and `loads()` should be used only with trusted LangChain manifests or serialized objects from trusted storage. Do not pass user-controlled data to `load()` or `loads()`, and do not use them as general parsers for request bodies, tool inputs, chat messages, or other attacker-controlled data. `load()` and `loads()` are beta APIs, and their behavior may change as LangChain narrows unsafe defaults. Future LangChain versions will require callers to be explicit about which objects may be revived. Users should pass a narrow `allowed_objects` value appropriate for the specific trusted manifest they are loading, rather than relying on broad defaults or `allowed_objects=\"all\"`, which permits the full trusted LangChain serialization allowlist. ## Credits The original issue was first reported by @u-ktdi. Similar findings were reported by @dewankpant, @shrutilohani, @Moaaz-0x, @pucagit. A related `_is_lc_secret` marker bypass affecting `dumps()` -> `loads()` round-trips was reported by @yardenporat353 (and a similar report by @localhost-detect)"}]}, {"name": "langchain-openai", "version": "1.1.10", "vulns": [{"id": "CVE-2026-41488", "fix_versions": ["1.1.14"], "aliases": ["GHSA-r7w7-9xr2-qq2r"], "description": "## Summary `langchain-openai`'s `_url_to_size()` helper (used by `get_num_tokens_from_messages` for image token counting) validated URLs for SSRF protection and then fetched them in a separate network operation with independent DNS resolution. This left a TOCTOU / DNS rebinding window: an attacker-controlled hostname could resolve to a public IP during validation and then to a private/localhost IP during the actual fetch. The practical impact is limited because the fetched response body is passed directly to Pillow's `Image.open()` to extract dimensions \u2014 the response content is never returned, logged, or otherwise exposed to the caller. An attacker cannot exfiltrate data from internal services through this path. A potential risk is blind probing (inferring whether an internal host/port is open based on timing or error behavior). ## Affected versions - `langchain-openai` < 1.1.14 ## Patched versions - `langchain-openai` >= 1.1.14 (requires `langchain-core` >= 1.2.31) ## Affected code **File:** `libs/partners/openai/langchain_openai/chat_models/base.py` \u2014 `_url_to_size()` The vulnerable pattern was a validate-then-fetch with separate DNS resolution: ```python validate_safe_url(image_source, allow_private=False, allow_http=True) # ... separate network operation with independent DNS resolution ... response = httpx.get(image_source, timeout=timeout) ``` ## Fix The fix replaces the validate-then-fetch pattern with an SSRF-safe httpx transport (`SSRFSafeSyncTransport` from `langchain-core`) that: - Resolves DNS once and validates all returned IPs against a policy (private ranges, cloud metadata, localhost, k8s internal DNS) - Pins the connection to the validated IP, eliminating the DNS rebinding window - Disables redirect following to prevent redirect-based SSRF bypasses This fix was released in langchain-openai 1.1.14."}]}, {"name": "langgraph", "version": "1.0.5", "vulns": [{"id": "CVE-2026-28277", "fix_versions": ["1.0.10"], "aliases": ["GHSA-g48c-2wqr-h844"], "description": "LangGraph checkpointers can load msgpack-encoded checkpoints that reconstruct Python objects during deserialization. If an attacker can modify checkpoint data in the backing store (for example, after a database compromise or other privileged write access to the persistence layer), they can potentially supply a crafted payload that triggers unsafe object reconstruction when the checkpoint is loaded. This is a **post-exploitation / defense-in-depth** issue. Exploitation requires the ability to write attacker-controlled checkpoint bytes at rest. In most deployments that prerequisite already implies a serious incident; the additional risk is turning \u201ccheckpoint-store write access\u201d into code execution in the application runtime, which can expand blast radius (for example by exposing environment variables or cloud credentials available to the runtime). There is no evidence of exploitation in the wild, and LangGraph is not aware of a practical exploitation path in existing deployments today. This change is intended to reduce the blast radius of a checkpoint-store compromise. ## Affected users / systems Users may be affected if they: - use a persistent checkpointer (database, remote store, shared filesystem, etc.), - load/resume from checkpoints, and - operate in an environment where an attacker could gain privileged write access to checkpoint data in the backing store. This issue requires the attacker to be able to modify persisted checkpoint bytes (or to compromise a trusted component that writes them). It is generally not reachable by an unauthenticated remote attacker in a correctly configured deployment. ## Impact - Potential **arbitrary code execution** or other unsafe side effects during checkpoint deserialization. - Escalation from \u201cwrite access to checkpoint store\u201d to \u201ccode execution in the application runtime,\u201d which may expose runtime secrets or provide access to other systems the runtime can reach. ## Exploitation scenario (high level) 1. Attacker gains privileged write access to the checkpoint store (for example, via database compromise, leaked credentials, or abuse of an administrative data path). 2. Attacker writes a crafted checkpoint payload containing msgpack data intended to reconstruct dangerous objects. 3. Application resumes and deserializes the checkpoint; unsafe reconstruction could execute attacker-controlled behavior. ## Mitigation / remediation LangGraph provides an allowlist-based hardening mechanism for msgpack checkpoint deserialization. ### Strict mode (environment variable) - **`LANGGRAPH_STRICT_MSGPACK`** - When set truthy (`1`, `true`, `yes`), the default msgpack deserialization policy becomes strict. - Concretely: `JsonPlusSerializer()` will default `allowed_msgpack_modules` to `None` (strict) instead of `True` (warn-and-allow), unless `allowed_msgpack_modules=...` is explicitly passed. ### `allowed_msgpack_modules` (serializer/checkpointer config) This setting controls what msgpack \u201cext\u201d types are allowed to be reconstructed. - `True` (default when strict mode is not enabled): allow all ext types, but log a warning when deserializing a type that is not explicitly registered. - `None` (strict): only a built-in safe set is reconstructed; other ext types are blocked. - `[(module, class_name), ...]` (strict allowlist): the built-in safe set plus exactly the listed symbols are reconstructed (exact-match). ### Built-in safe set A small set of types is always treated as safe to reconstruct (for example `datetime` types, `uuid.UUID`, `decimal.Decimal`, `set`/`frozenset`/`deque`, `ipaddress` types, `pathlib` paths, `zoneinfo.ZoneInfo`, compiled regex patterns, and selected LangGraph internal types). ### Automatically derived allowlist (only when compiling graphs) When `LANGGRAPH_STRICT_MSGPACK` is enabled and `StateGraph` is compiled, LangGraph derives an allowlist from the graph\u2019s schemas and channels and applies it to the checkpointer. - The allowlist is built by walking the state/input/output/context schemas (plus node/branch input schemas) and channel value/update types. It includes Pydantic v1/v2 models, dataclasses, enums, TypedDict field types, and common typing constructs (containers, unions, `Annotated`). - LangGraph also includes a curated set of common LangChain message classes. This derived allowlist is only applied if the selected checkpointer supports `with_allowlist(...)`. If a user is constructing serializers/checkpointers manually (or using a checkpointer that does not support allowlist propagation), they will need to configure `allowed_msgpack_modules` themselves. ### Operational guidance - Treat checkpoint stores as integrity-sensitive. Restrict write access and rotate credentials if compromise is suspected. - Enable strict mode (`LANGGRAPH_STRICT_MSGPACK=true`) in production if feasible, and rely on schema-driven allowlisting to reduce incompatibilities. - Avoid providing custom msgpack deserialization hooks that reconstruct arbitrary types unless checkpoint data is fully trusted. ## Limitations / important notes - If a checkpointer implementation does **not** support allowlist application (i.e., does not implement `with_allowlist`), allowlist enforcement may be skipped (with a warning). In that situation, strict expectations may not hold. - If an application supplies a custom msgpack unpack hook (`ext_hook`), the custom hook controls reconstruction and can bypass the default allowlist checks (intentional escape hatch, but it weakens the protection). ## LangSmith / hosted deployments note LangSmith is not aware of this issue presenting risk to existing LangSmith-hosted deployments. The described threat model requires an attacker to tamper with the checkpoint persistence layer used by the deployment; typical hosted configurations are designed to prevent such access. First reported by: yardenporat353"}]}, {"name": "langgraph-checkpoint", "version": "3.0.1", "vulns": [{"id": "CVE-2026-27794", "fix_versions": ["4.0.0"], "aliases": ["GHSA-mhr3-j7m5-c7c9"], "description": "## Context A Remote Code Execution vulnerability exists in LangGraph's caching layer when applications enable cache backends that inherit from `BaseCache` and opt nodes into caching via `CachePolicy`. Prior to `langgraph-checkpoint` 4.0.0, `BaseCache` defaults to `JsonPlusSerializer(pickle_fallback=True)`. When msgpack serialization fails, cached values can be deserialized via `pickle.loads(...)`. ### Who is affected? Caching is not enabled by default. Applications are affected only when: - The application explicitly enables a cache backend (for example by passing `cache=...` to `StateGraph.compile(...)` or otherwise configuring a `BaseCache` implementation) - One or more nodes opt into caching via `CachePolicy` - The attacker can write to the cache backend (for example a network-accessible Redis instance with weak/no auth, shared cache infrastructure reachable by other tenants/services, or a writable SQLite cache file) Example (enabling a cache backend and opting a node into caching): ```py from langgraph.cache.memory import InMemoryCache from langgraph.graph import StateGraph from langgraph.types import CachePolicy def my_node(state: dict) -> dict: return {\"value\": state.get(\"value\", 0) + 1} builder = StateGraph(dict) builder.add_node(\"my_node\", my_node, cache_policy=CachePolicy(ttl=120)) builder.set_entry_point(\"my_node\") graph = builder.compile(cache=InMemoryCache()) result = graph.invoke({\"value\": 1}) ``` With `pickle_fallback=True`, when msgpack serialization fails, `JsonPlusSerializer` can fall back to storing values as a `(\"pickle\", )` tuple and later deserialize them via `pickle.loads(...)`. If an attacker can place a malicious pickle payload into the cache backend such that the LangGraph process reads and deserializes it, this can lead to arbitrary code execution. Exploitation requires attacker write access to the cache backend. The serializer is not exposed as a network-facing API. This is fixed in `langgraph-checkpoint>=4.0.0` by disabling pickle fallback by default (`pickle_fallback=False`). ## Impact Arbitrary code execution in the LangGraph process when attacker-controlled cache entries are deserialized. ## Root Cause - `BaseCache` default serializer configuration inherited by cache implementations (`InMemoryCache`, `RedisCache`, `SqliteCache`): - `libs/checkpoint/langgraph/cache/base/__init__.py` (pre-fix default: `JsonPlusSerializer(pickle_fallback=True)`) - `JsonPlusSerializer` deserialization sink: - `libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py` - `loads_typed(...)` calls `pickle.loads(data_)` when `type_ == \"pickle\"` and pickle fallback is enabled ## Attack preconditions An attacker must be able to write attacker-controlled bytes into the cache backend such that the LangGraph process later reads and deserializes them. This typically requires write access to a networked cache (for example a network-accessible Redis instance with weak/no auth or shared cache infrastructure reachable by other tenants/services) or write access to local cache storage (for example a writable SQLite cache file via permissive file permissions or a shared writable volume). Because exploitation requires write access to the cache storage layer, this is a post-compromise / post-access escalation vector. ## Remediation - Upgrade to `langgraph-checkpoint>=4.0.0`. ## Resources - ZDI-CAN-28385 - Patch: https://github.com/langchain-ai/langgraph/pull/6677 - Patch diff: https://patch-diff.githubusercontent.com/raw/langchain-ai/langgraph/pull/6677.patch - Credit: Peter Girnus (@gothburz), Demeng Chen, and Brandon Niemczyk (Trend Micro Zero Day Initiative)"}]}, {"name": "langgraph-checkpoint-postgres", "version": "3.0.4", "vulns": []}, {"name": "langgraph-prebuilt", "version": "1.0.8", "vulns": []}, {"name": "langgraph-sdk", "version": "0.3.9", "vulns": []}, {"name": "langsmith", "version": "0.7.13", "vulns": [{"id": "CVE-2026-41182", "fix_versions": ["0.7.31"], "aliases": ["GHSA-rr7j-v2q5-chgv"], "description": "## Summary The LangSmith SDK's output redaction controls (hideOutputs in JS, hide_outputs in Python) do not apply to streaming token events. When an LLM run produces streaming output, each chunk is recorded as a new_token event containing the raw token value. These events bypass the redaction pipeline entirely \u2014 prepareRunCreateOrUpdateInputs (JS) and _hide_run_outputs (Python) only process the inputs and outputs fields on a run, never the events array. As a result, applications relying on output redaction to prevent sensitive LLM output from being stored in LangSmith will still leak the full streamed content via run events. ## Details **Both JS and Python SDKs are affected.** The same pattern exists in both: - **JS SDK**: `traceable.ts:997-1003` and `traceable.ts:1044-1050` - **Python SDK**: `run_helpers.py:1924` and `run_helpers.py:1996` In both SDKs, `new_token` events with raw `kwargs.token` values are added during streaming, and the redaction pipeline (`hideOutputs` in JS, `hide_outputs` in Python) only processes `inputs`/`outputs` \u2014 never `events`."}, {"id": "CVE-2026-45134", "fix_versions": ["0.8.0"], "aliases": ["GHSA-3644-q5cj-c5c7"], "description": "## Description The LangSmith SDK's prompt pull methods (`pull_prompt` / `pull_prompt_commit` in Python, `pullPrompt` / `pullPromptCommit` in JS/TS) fetch and deserialize prompt manifests from the LangSmith Hub. These manifests may contain serialized LangChain objects and model configuration that affect runtime behavior. When pulling a public prompt by `owner/name` identifier, the manifest content is controlled by an external party, but prior versions of the SDK did not distinguish this from pulling a prompt within the caller's own organization. Prompt manifests can intentionally configure a model with a custom base URL, default headers, model name, or other constructor arguments. These are supported features, but they also mean the prompt contents should be treated as executable configuration rather than plain text. A prompt can also include serialized LangChain `Runnable` or `PromptTemplate` objects with attacker-controlled constructor kwargs, or secret references that, if `secrets_from_env` is enabled, read environment variables at deserialization time. Applications are exposed when all of the following are true: - The application calls `pull_prompt` or `pull_prompt_commit` (Python) or `pullPrompt` or `pullPromptCommit` (JS/TS) with a public `owner/name` prompt identifier. - The prompt was published or modified by an untrusted or compromised account. - The application uses the pulled prompt without independently validating its contents. Applications that only pull prompts from their own organization (referenced by name only, without an `owner/` prefix) are not affected by the public prompt trust boundary issue described above. However, same-organization prompts carry their own risk. If an attacker gains write access to the organization (for example, through a leaked `LANGSMITH_API_KEY` or a compromised team member account), they can push a malicious prompt that is pulled and deserialized without any additional warning. ## Impact An attacker who publishes a malicious prompt to LangSmith Hub may be able to affect applications that pull that prompt by `owner/name`. If the prompt manifest reaches the SDK's deserialization path, the SDK will instantiate the referenced LangChain objects with the attacker-supplied constructor arguments rather than treating the manifest as inert data. Realistic impacts include: - Server-side request forgery (SSRF), outbound request redirection, and interception of LLM traffic if a prompt manifest configures an LLM client with an attacker-controlled `base_url`, proxy, or equivalent endpoint-setting parameter. In typical deployments, redirected requests may include prompt contents, system prompts, retrieved context, model parameters, provider credentials, or other secrets and may disclose them to the attacker-controlled endpoint. - Prompt injection or behavior manipulation if a manifest embeds attacker-controlled system messages, prompt templates, or model parameters that alter the application's behavior. - Additional deserialization risk when `include_model=True` is passed, because this expands the allowlist to partner integration classes. This is not the default, but it materially increases risk when pulling prompts from outside the caller's organization. ## Remediation The LangSmith SDK now blocks pulling public prompts by `owner/name` by default. Callers must explicitly opt in by passing `dangerously_pull_public_prompt=True` (Python) or `dangerouslyPullPublicPrompt: true` (JS/TS) to acknowledge the trust boundary. This flag should only be set after reviewing and trusting the prompt contents, not merely the publishing account. Upgrade to LangSmith SDK **Python >= 0.8.0** or **JS/TS >= 0.6.0**. ### Guidance for prompt pull methods The prompt pull methods (`pull_prompt` / `pull_prompt_commit` in Python, `pullPrompt` / `pullPromptCommit` in JS/TS) should be used only with trusted prompts. Do not pull public prompts by `owner/name` from untrusted or unreviewed sources without understanding that the manifest contents will be deserialized and may affect runtime behavior. When pulling prompts that include model configuration (`include_model=True` in Python, `includeModel: true` in JS/TS), the deserialization allowlist expands to include partner integration classes. Because this mode is not the default and is often unnecessary for third-party prompts, prefer the default (`false`) when pulling prompts from sources outside your organization. Avoid passing `secrets_from_env=True` (Python) when pulling untrusted prompts. This parameter allows prompt manifests to read environment variables during deserialization. Only use it with trusted prompts from your own organization. ### Same-organization prompts Prompts pulled from the caller's own organization (referenced by name only, without an `owner/` prefix) are not gated by the new `dangerously_pull_public_prompt` flag, but they are not inherently safe. If an attacker gains write access to the organization (for example, through a leaked `LANGSMITH_API_KEY` or a compromised team member account), they can push a malicious prompt that redirects LLM traffic to attacker-controlled infrastructure and may disclose any credentials attached to those requests. The security of same-organization prompts follows a shared responsibility model. The LangSmith SDK enforces trust boundaries for public prompts pulled from external accounts, but it cannot protect against compromised credentials or accounts within the caller's own organization. Securing API keys, managing team member access, and reviewing prompt contents before production deployment are the responsibility of the organization. Organizations should treat prompts as executable configuration and apply the same review and audit practices they would apply to application code. ## Credits First reported by @Moaaz-0x."}]}, {"name": "license-expression", "version": "30.4.4", "vulns": []}, {"name": "lxml", "version": "6.0.2", "vulns": [{"id": "CVE-2026-41066", "fix_versions": ["6.1.0"], "aliases": ["GHSA-vfmq-68hx-4jfw"], "description": "### Impact Using either of the two parsers in the default configuration (with `resolve_entities=True`) allows untrusted XML input to read local files. ### Patches lxml 6.1.0 changes the default to `resolve_entities='internal'`, thus disallowing local file access by default. ### Workarounds Setting the `resolve_entities` option explicitly to `resolve_entities='internal'` or `resolve_entities=False` disables the local file access. ### Resources Original report: https://bugs.launchpad.net/lxml/+bug/2146291 The default option was changed to `resolve_entities='internal'` for the normal XML and HTML parsers in lxml 5.0. The default was not changed for `iterparse()` and `ETCompatXMLParser()` at the time. lxml 6.1 makes the safe option the default for all parsers."}]}, {"name": "markdown-it-py", "version": "4.0.0", "vulns": []}, {"name": "mdurl", "version": "0.1.2", "vulns": []}, {"name": "minio", "version": "7.2.20", "vulns": []}, {"name": "mixpanel", "version": "5.1.0", "vulns": []}, {"name": "mmh3", "version": "5.2.1", "vulns": []}, {"name": "mpmath", "version": "1.3.0", "vulns": []}, {"name": "msgpack", "version": "1.1.2", "vulns": []}, {"name": "multidict", "version": "6.7.1", "vulns": []}, {"name": "mypy-extensions", "version": "1.1.0", "vulns": []}, {"name": "nacos-sdk-python", "version": "2.0.9", "vulns": []}, {"name": "numpy", "version": "2.4.2", "vulns": []}, {"name": "oauthlib", "version": "3.3.1", "vulns": []}, {"name": "onnxruntime", "version": "1.24.3", "vulns": []}, {"name": "openai", "version": "2.26.0", "vulns": []}, {"name": "openpyxl", "version": "3.1.5", "vulns": []}, {"name": "opentelemetry-api", "version": "1.40.0", "vulns": []}, {"name": "opentelemetry-exporter-otlp-proto-common", "version": "1.40.0", "vulns": []}, {"name": "opentelemetry-exporter-otlp-proto-grpc", "version": "1.40.0", "vulns": []}, {"name": "opentelemetry-proto", "version": "1.40.0", "vulns": []}, {"name": "opentelemetry-sdk", "version": "1.40.0", "vulns": []}, {"name": "opentelemetry-semantic-conventions", "version": "0.61b0", "vulns": []}, {"name": "orjson", "version": "3.11.7", "vulns": []}, {"name": "ormsgpack", "version": "1.12.2", "vulns": []}, {"name": "overrides", "version": "7.7.0", "vulns": []}, {"name": "packageurl-python", "version": "0.17.6", "vulns": []}, {"name": "packaging", "version": "25.0", "vulns": []}, {"name": "pandas", "version": "2.3.3", "vulns": []}, {"name": "parsel", "version": "1.11.0", "vulns": []}, {"name": "pip", "version": "26.0.1", "vulns": [{"id": "CVE-2026-3219", "fix_versions": [], "aliases": ["GHSA-58qw-9mgm-455v"], "description": "pip handles concatenated tar and ZIP files as ZIP files regardless of filename or whether a file is both a tar and ZIP file. This behavior could result in confusing installation behavior, such as installing \"incorrect\" files according to the filename of the archive. New behavior only proceeds with installation if the file identifies uniquely as a ZIP or tar archive, not as both."}, {"id": "CVE-2026-6357", "fix_versions": ["26.1"], "aliases": ["GHSA-jp4c-xjxw-mgf9"], "description": "pip prior to version 26.1 would run self-update check functionality after installing wheel files which required importing well-known Python modules names. These module imports were intentionally deferred to increase startup time of the pip CLI. The patch changes self-update functionality to run before wheels are installed to prevent newly-installed modules from being imported shortly after the installation of a wheel package. Users should still review package contents prior to installation."}]}, {"name": "pip-api", "version": "0.0.34", "vulns": []}, {"name": "pip-audit", "version": "2.10.0", "vulns": []}, {"name": "pip-requirements-parser", "version": "32.0.1", "vulns": []}, {"name": "platformdirs", "version": "4.9.6", "vulns": []}, {"name": "pluggy", "version": "1.6.0", "vulns": []}, {"name": "posthog", "version": "5.4.0", "vulns": []}, {"name": "progressbar2", "version": "4.5.0", "vulns": []}, {"name": "propcache", "version": "0.4.1", "vulns": []}, {"name": "protego", "version": "0.6.0", "vulns": []}, {"name": "proto-plus", "version": "1.27.1", "vulns": []}, {"name": "protobuf", "version": "6.33.5", "vulns": []}, {"name": "psutil", "version": "7.2.2", "vulns": []}, {"name": "psycopg", "version": "3.3.3", "vulns": []}, {"name": "psycopg-binary", "version": "3.3.3", "vulns": []}, {"name": "psycopg-pool", "version": "3.3.0", "vulns": []}, {"name": "py-serializable", "version": "2.1.0", "vulns": []}, {"name": "pyasn1", "version": "0.6.2", "vulns": [{"id": "CVE-2026-30922", "fix_versions": ["0.6.3"], "aliases": ["GHSA-jr27-m4p2-rc6r"], "description": "### Summary The `pyasn1` library is vulnerable to a Denial of Service (DoS) attack caused by uncontrolled recursion when decoding ASN.1 data with deeply nested structures. An attacker can supply a crafted payload containing nested `SEQUENCE` (`0x30`) or `SET` (`0x31`) tags with Indefinite Length (`0x80`) markers. This forces the decoder to recursively call itself until the Python interpreter crashes with a `RecursionError` or consumes all available memory (OOM), crashing the host application. ### Details The vulnerability exists because the decoder iterates through the input stream and recursively calls `decodeFun` (the decoding callback) for every nested component found, without tracking or limiting the recursion depth. Vulnerable Code Locations: 1. `indefLenValueDecoder` (Line 998): ```for component in decodeFun(substrate, asn1Spec, allowEoo=True, **options):``` This method handles indefinite-length constructed types. It sits inside a `while True` loop and recursively calls the decoder for every nested tag. 2. `valueDecoder` (Lines 786 and 907): ```for component in decodeFun(substrate, componentType, **options):``` This method handles standard decoding when a schema is present. It contains two distinct recursive calls that lack depth checks: Line 786: Recursively decodes components of `SEQUENCE` or `SET` types. Line 907: Recursively decodes elements of `SEQUENCE OF` or `SET OF` types. 4. `_decodeComponentsSchemaless` (Line 661): ```for component in decodeFun(substrate, **options):``` This method handles decoding when no schema is provided. In all three cases, `decodeFun` is invoked without passing a `depth` parameter or checking against a global `MAX_ASN1_NESTING` limit. ### PoC ``` import sys from pyasn1.codec.ber import decoder sys.setrecursionlimit(100000) print(\"[*] Generating Recursion Bomb Payload...\") depth = 50_000 chunk = b'\\x30\\x80' payload = chunk * depth print(f\"[*] Payload size: {len(payload) / 1024:.2f} KB\") print(\"[*] Triggering Decoder...\") try: decoder.decode(payload) except RecursionError: print(\"[!] Crashed: Recursion Limit Hit\") except MemoryError: print(\"[!] Crashed: Out of Memory\") except Exception as e: print(f\"[!] Crashed: {e}\") ``` ``` [*] Payload size: 9.77 KB [*] Triggering Decoder... [!] Crashed: Recursion Limit Hit ``` ### Impact - This is an unhandled runtime exception that typically terminates the worker process or thread handling the request. This allows a remote attacker to trivially kill service workers with a small payload (<100KB), resulting in a Denial of Service. Furthermore, in environments where recursion limits are increased, this leads to server-wide memory exhaustion. - Service Crash: Any service using `pyasn1` to parse untrusted ASN.1 data (e.g., LDAP, SNMP, Kerberos, X.509 parsers) can be crashed remotely. - Resource Exhaustion: The attack consumes RAM linearly with the nesting depth. A small payload (<200KB) can consume hundreds of megabytes of RAM or exhaust the stack. ### Credits Vulnerability discovered by Kevin Tu of TMIR at ByteDance."}]}, {"name": "pyasn1-modules", "version": "0.4.2", "vulns": []}, {"name": "pybase64", "version": "1.4.3", "vulns": []}, {"name": "pycparser", "version": "3.0", "vulns": []}, {"name": "pycryptodome", "version": "3.23.0", "vulns": []}, {"name": "pydantic", "version": "2.12.5", "vulns": []}, {"name": "pydantic-core", "version": "2.41.5", "vulns": []}, {"name": "pydispatcher", "version": "2.0.7", "vulns": []}, {"name": "pygments", "version": "2.19.2", "vulns": [{"id": "CVE-2026-4539", "fix_versions": ["2.20.0"], "aliases": ["GHSA-5239-wwwm-4pmq"], "description": "A security flaw has been discovered in pygments before 2.20.0. The impacted element is the function AdlLexer of the file pygments/lexers/archetype.py. The manipulation results in inefficient regular expression complexity. The attack is only possible with local access. The exploit has been released to the public and may be used for attacks. The project was informed of the problem early through an issue report but has not responded yet."}]}, {"name": "pymilvus", "version": "2.6.9", "vulns": []}, {"name": "pymysql", "version": "1.1.2", "vulns": []}, {"name": "pyopenssl", "version": "25.1.0", "vulns": [{"id": "CVE-2026-27448", "fix_versions": ["26.0.0"], "aliases": ["GHSA-vp96-hxj8-p424"], "description": "If a user provided callback to `set_tlsext_servername_callback` raised an unhandled exception, this would result in a connection being accepted. If a user was relying on this callback for any security-sensitive behavior, this could allow bypassing it. Unhandled exceptions now result in rejecting the connection. Credit to **Leury Castillo** for reporting this issue."}, {"id": "CVE-2026-27459", "fix_versions": ["26.0.0"], "aliases": ["GHSA-5pwr-322w-8jr4"], "description": "If a user provided callback to `set_cookie_generate_callback` returned a cookie value greater than 256 bytes, pyOpenSSL would overflow an OpenSSL provided buffer. Cookie values that are too long are now rejected."}]}, {"name": "pyparsing", "version": "3.3.2", "vulns": []}, {"name": "pypika", "version": "0.51.1", "vulns": []}, {"name": "pyproject-hooks", "version": "1.2.0", "vulns": []}, {"name": "pyreadline3", "version": "3.5.4", "vulns": []}, {"name": "pytest", "version": "9.0.2", "vulns": [{"id": "CVE-2025-71176", "fix_versions": ["9.0.3"], "aliases": ["GHSA-6w46-j5rx-g56g"], "description": "pytest through 9.0.2 on UNIX relies on directories with the `/tmp/pytest-of-{user}` name pattern, which allows local users to cause a denial of service or possibly gain privileges."}]}, {"name": "python-dateutil", "version": "2.9.0.post0", "vulns": []}, {"name": "python-dotenv", "version": "1.2.2", "vulns": []}, {"name": "python-json-logger", "version": "4.1.0", "vulns": []}, {"name": "python-utils", "version": "3.9.1", "vulns": []}, {"name": "pytz", "version": "2026.1.post1", "vulns": []}, {"name": "pywin32", "version": "311", "vulns": []}, {"name": "pyyaml", "version": "6.0.3", "vulns": []}, {"name": "queuelib", "version": "1.9.0", "vulns": []}, {"name": "redis", "version": "7.2.1", "vulns": []}, {"name": "referencing", "version": "0.37.0", "vulns": []}, {"name": "regex", "version": "2026.2.28", "vulns": []}, {"name": "requests", "version": "2.32.5", "vulns": [{"id": "CVE-2026-25645", "fix_versions": ["2.33.0"], "aliases": ["GHSA-gc5v-m9x4-r6x2"], "description": "### Impact The `requests.utils.extract_zipped_paths()` utility function uses a predictable filename when extracting files from zip archives into the system temporary directory. If the target file already exists, it is reused without validation. A local attacker with write access to the temp directory could pre-create a malicious file that would be loaded in place of the legitimate one. ### Affected usages **Standard usage of the Requests library is not affected by this vulnerability.** Only applications that call `extract_zipped_paths()` directly are impacted. ### Remediation Upgrade to at least Requests 2.33.0, where the library now extracts files to a non-deterministic location. If developers are unable to upgrade, they can set `TMPDIR` in their environment to a directory with restricted write access."}]}, {"name": "requests-file", "version": "3.0.1", "vulns": []}, {"name": "requests-oauthlib", "version": "2.0.0", "vulns": []}, {"name": "requests-toolbelt", "version": "1.0.0", "vulns": []}, {"name": "rich", "version": "14.3.3", "vulns": []}, {"name": "rpds-py", "version": "0.30.0", "vulns": []}, {"name": "rsa", "version": "4.9.1", "vulns": []}, {"name": "ruamel-yaml", "version": "0.18.17", "vulns": []}, {"name": "ruamel-yaml-clib", "version": "0.2.15", "vulns": []}, {"name": "scrapy", "version": "2.14.1", "vulns": [{"id": "PYSEC-2017-83", "fix_versions": [], "aliases": ["GHSA-h7wm-ph43-c39p", "CVE-2017-14158"], "description": "Scrapy 1.4 allows remote attackers to cause a denial of service (memory consumption) via large files because arbitrarily many files are read into memory, which is especially problematic if the files are then individually written in a separate thread to a slow storage resource, as demonstrated by interaction between dataReceived (in core/downloader/handlers/http11.py) and S3FilesStore."}, {"id": "GHSA-cwxj-rr6w-m6w7", "fix_versions": ["2.14.2"], "aliases": [], "description": "### Impact Since version 1.4.0, Scrapy respects the `Referrer-Policy` response header to decide whether and how to set a `Referer` header on follow-up requests. If the header value looked like a valid Python import path, Scrapy would import the referenced object and call it, assuming it referred to a referrer policy class (for example, `scrapy.spidermiddlewares.referer.DefaultReferrerPolicy`) and attempting to instantiate it to handle the `Referer` header. A malicious site could exploit this by setting `Referrer-Policy` to a path such as `sys.exit`, causing Scrapy to import and execute it and potentially terminate the process. ### Patches Upgrade to Scrapy 2.14.2 (or later). ### Workarounds If you cannot upgrade to Scrapy 2.14.2, consider the following mitigations. - **Disable the middleware:** If you don't need the `Referer` header on follow-up requests, set [`REFERER_ENABLED`](https://docs.scrapy.org/en/latest/topics/spider-middleware.html#referer-enabled) to `False`. - **Set headers manually:** If you do need a `Referer`, disable the middleware and set the header explicitly on the requests that require it. - **Set `referrer_policy` in request metadata:** If disabling the middleware is not viable, set the [`referrer_policy`](https://docs.scrapy.org/en/latest/topics/spider-middleware.html#referrer-policy) request meta key on all requests to prevent evaluating preceding responses' `Referrer-Policy`. For example: ```python Request( url, meta={ \"referrer_policy\": \"scrapy.spidermiddlewares.referer.DefaultReferrerPolicy\", }, ) ``` Instead of editing requests individually, you can: - implement a custom [spider middleware](https://docs.scrapy.org/en/latest/topics/spider-middleware.html) that runs before the built-in referrer policy middleware and sets the `referrer_policy` meta key; or - set the meta key in start requests and use the [scrapy-sticky-meta-params](https://github.com/heylouiz/scrapy-sticky-meta-params) plugin to propagate it to follow-up requests. If you want to continue respecting legitimate `Referrer-Policy` headers while protecting against malicious ones, disable the built-in referrer policy middleware by setting it to `None` in [`SPIDER_MIDDLEWARES`](https://docs.scrapy.org/en/latest/topics/settings.html#std-setting-SPIDER_MIDDLEWARES) and replace it with the fixed implementation from Scrapy 2.14.2. If the Scrapy 2.14.2 implementation is incompatible with your project (for example, because your Scrapy version is older), copy the corresponding middleware from your Scrapy version, apply the same patch, and use that as a replacement."}]}, {"name": "sentry-sdk", "version": "2.58.0", "vulns": []}, {"name": "service-identity", "version": "24.2.0", "vulns": []}, {"name": "setuptools", "version": "81.0.0", "vulns": []}, {"name": "shellingham", "version": "1.5.4", "vulns": []}, {"name": "six", "version": "1.17.0", "vulns": []}, {"name": "sniffio", "version": "1.3.1", "vulns": []}, {"name": "sortedcontainers", "version": "2.4.0", "vulns": []}, {"name": "spark-parser", "version": "1.9.0", "vulns": []}, {"name": "sqlalchemy", "version": "2.0.49", "vulns": []}, {"name": "starlette", "version": "0.50.0", "vulns": []}, {"name": "sympy", "version": "1.14.0", "vulns": []}, {"name": "tabulate", "version": "0.9.0", "vulns": []}, {"name": "tenacity", "version": "9.1.4", "vulns": []}, {"name": "tiktoken", "version": "0.12.0", "vulns": []}, {"name": "tldextract", "version": "5.3.1", "vulns": []}, {"name": "tokenizers", "version": "0.22.2", "vulns": []}, {"name": "toml", "version": "0.10.2", "vulns": []}, {"name": "tomli", "version": "2.4.1", "vulns": []}, {"name": "tomli-w", "version": "1.2.0", "vulns": []}, {"name": "tqdm", "version": "4.67.3", "vulns": []}, {"name": "twisted", "version": "25.5.0", "vulns": [{"id": "CVE-2026-42304", "fix_versions": ["26.4.0rc2"], "aliases": ["GHSA-grgv-6hw6-v9g4"], "description": "### Details The twisted.names module is vulnerable to a Denial of Service (DoS) attack via resource exhaustion during DNS name decompression. A remote, unauthenticated attacker can exploit this by sending a crafted TCP DNS packet containing deeply chained compression pointers. This flaw bypasses previous loop-prevention logic, causing the single-threaded Twisted reactor to hang while processing millions of recursive lookups, effectively freezing the server. --- ### Technical Details The main issue is in twisted.names.dns.Name.decode. A visited set was added in 2011 (commit e11cd82) to prevent infinite loops, but there is still no limit on the number of pointer dereferences per message. Also, the visited set is reset for each Question record. Because DNSServerFactory handles every record in QDCOUNT without checking them, an attacker can add thousands of questions that all refer to the same long chain of pointers. This makes the parser repeat a complex and unnecessary search. ```python ## src/twisted/names/dns.py (Lines 595-631) def decode(self, strio, length=None): visited = set() self.name = b\"\" off = 0 while 1: l = ord(readPrecisely(strio, 1)) if l == 0: if off > 0: strio.seek(off) return if (l >> 6) == 3: new_off = (l & 63) << 8 | ord(readPrecisely(strio, 1)) if new_off in visited: raise ValueError(\"Compression loop in encoded name\") visited.add(new_off) if off == 0: off = strio.tell() strio.seek(new_off) continue label = readPrecisely(strio, l) if self.name == b\"\": self.name = label else: self.name = self.name + b\".\" + label ``` --- ### PoC ```python import struct, time from twisted.names import dns, server from twisted.test import proto_helpers def create_tcp_payload(): num_pointers = 8000 packet_length = 65533 num_questions = (packet_length - (num_pointers * 2) - 12) // 6 buffer = bytearray(packet_length) struct.pack_into(\"!HHHHHH\", buffer, 0, 1, 0, num_questions, 0, 0, 0) ptr_offset = 12 for _ in range(num_pointers - 1): struct.pack_into(\"!H\", buffer, ptr_offset, 0xC000 | (ptr_offset + 2)) ptr_offset += 2 null_byte_offset = ptr_offset + 2 struct.pack_into(\"!H\", buffer, ptr_offset, 0xC000 | null_byte_offset) buffer[null_byte_offset] = 0 question_offset = null_byte_offset + 1 for _ in range(num_questions): if question_offset + 6 <= packet_length: struct.pack_into(\"!HHH\", buffer, question_offset, 0xC000 | 12, 1, 1) question_offset += 6 return packet_length, num_pointers, num_questions, struct.pack(\"!H\", packet_length) + buffer def test_dns_server(): factory = server.DNSServerFactory(clients=[]) protocol = factory.buildProtocol((\"127.0.0.1\", 10053)) transport = proto_helpers.StringTransport() protocol.makeConnection(transport) pkt_len, num_ptrs, num_qs, payload = create_tcp_payload() print(\"payload\") print(f\"len={pkt_len} ptrs={num_ptrs} qs={num_qs}\") start = time.time() protocol.dataReceived(payload) end = time.time() print(f\"time={end - start:.4f}s\") if __name__ == \"__main__\": test_dns_server() ``` --- ### Impact A single malformed TCP packet is sufficient to block the Twisted reactor's event loop for several seconds. Because Twisted operates on a single-threaded cooperative multitasking model, this is a common Denial of Service (DoS). The process becomes unable to handle new connections, process I/O, or respond to existing requests, effectively paralyzing the server for the duration of the decompression. --- ### Remediation - Update twisted.names.dns.Name.decode to add a required limit on pointer resolutions per DNS message - Share the \"resolved offset\" state across all records in a single message to prevent redundant processing. - Validate the number of questions before entering the decoding loop in Message.decode. --- ### Resources https://cwe.mitre.org/data/definitions/400.html https://cwe.mitre.org/data/definitions/407.html https://datatracker.ietf.org/doc/html/rfc9267 https://github.com/twisted/twisted/blob/trunk/src/twisted/names/dns.py#L595 https://github.com/twisted/twisted/commit/e11cd82bdd79b3ebbb0e8635cbb9c76df2b5af09 --- **Author**: Tomas Illuminati"}]}, {"name": "typer", "version": "0.24.1", "vulns": []}, {"name": "typing-extensions", "version": "4.15.0", "vulns": []}, {"name": "typing-inspect", "version": "0.9.0", "vulns": []}, {"name": "typing-inspection", "version": "0.4.2", "vulns": []}, {"name": "tzdata", "version": "2025.3", "vulns": []}, {"name": "tzlocal", "version": "5.3.1", "vulns": []}, {"name": "uncompyle6", "version": "3.9.3", "vulns": []}, {"name": "urllib3", "version": "2.6.3", "vulns": [{"id": "CVE-2026-44431", "fix_versions": ["2.7.0"], "aliases": ["GHSA-qccp-gfcp-xxvc"], "description": "### Impact When following cross-origin redirects for requests made using urllib3\u2019s high-level APIs, such as `urllib3.request()`, `PoolManager.request()`, and `ProxyManager.request()`, sensitive headers \u2014 `Authorization`, `Cookie`, and `Proxy-Authorization` (defined in `Retry.DEFAULT_REMOVE_HEADERS_ON_REDIRECT`) \u2014 are stripped by default, as expected. However, cross-origin redirects followed from the low-level API via `ProxyManager.connection_from_url().urlopen(..., assert_same_host=False)` still forward these sensitive headers. ### Affected usage Applications and libraries using urllib3 versions earlier than 2.7.0 may be affected if they allow cross-origin redirects while making requests through `HTTPConnection.urlopen()` instances created via `ProxyManager.connection_from_url()`. ### Remediation Upgrade to urllib3 version 2.7.0 or later, in which sensitive headers are stripped from redirects followed by `HTTPConnection`. If upgrading is not immediately possible, avoid using this low-level redirect flow for cross-origin redirects. If appropriate for your use case, switch to `ProxyManager.request()`."}, {"id": "CVE-2026-44432", "fix_versions": ["2.7.0"], "aliases": ["GHSA-mf9v-mfxr-j63j"], "description": "### Impact urllib3's [streaming API](https://urllib3.readthedocs.io/en/2.7.0/advanced-usage.html#streaming-and-i-o) is designed for the efficient handling of large HTTP responses by reading the content in chunks, rather than loading the entire response body into memory at once. urllib3 can perform decompression based on the HTTP `Content-Encoding` header (e.g., `gzip`, `deflate`, `br`, or `zstd`). When using the streaming API since version 2.6.0, the library decompresses only the necessary bytes, enabling partial content consumption. However, urllib3 before version 2.7.0 could still decompress the whole response instead of the requested portion in two cases: 1. During the second `HTTPResponse.read(amt=N)` call when the response was decompressed using the official [Brotli](https://pypi.org/project/brotli/) library. 2. When `HTTPResponse.drain_conn()` was called after the response had been read and decompressed partially (compression algorithm did not matter here). These issues could cause urllib3 to fully decode a small amount of highly compressed data in a single operation. This could result in excessive resource consumption (high CPU usage and massive memory allocation for the decompressed data; CWE-409) on the client side. ### Affected usages Applications and libraries using urllib3 versions earlier than 2.7.0 may be affected when streaming compressed responses from untrusted sources in either of these cases, unless decompression is explicitly disabled: 1. A response encoded with `br` is read incrementally with at least two `HTTPResponse.read(amt=N)` or `HTTPResponse.stream(amt=N)` calls while using the official [Brotli](https://pypi.org/project/brotli/) library. 2. `HTTPResponse.drain_conn()` is called after response decompression has already started. ### Remediation Upgrade to at least urllib3 version 2.7.0 in which the library: 1. Is more efficient for reads with Brotli. 2. Always skips decompression for `HTTPResponse.drain_conn()`. If upgrading is not immediately possible, the following workarounds may reduce exposure in specific cases: 1. For the Brotli-specific issue only, switch from [brotli](https://pypi.org/project/brotli/) to [brotlicffi](https://pypi.org/project/brotlicffi/) until you can upgrade urllib3; the official Brotli package is affected because of https://github.com/google/brotli/issues/1396. 2. If your code explicitly calls `HTTPResponse.drain_conn()`, call `HTTPResponse.close()` instead when connection reuse is not important. ### Credits The Brotli-specific issue was reported by @kimkou2024. `HTTPResponse.drain_conn()` inefficiency was reported by @Cycloctane."}]}, {"name": "uuid-utils", "version": "0.14.1", "vulns": []}, {"name": "uvicorn", "version": "0.41.0", "vulns": []}, {"name": "w3lib", "version": "2.4.0", "vulns": []}, {"name": "watchfiles", "version": "1.1.1", "vulns": []}, {"name": "websocket-client", "version": "1.9.0", "vulns": []}, {"name": "websockets", "version": "16.0", "vulns": []}, {"name": "wheel", "version": "0.46.3", "vulns": []}, {"name": "wrapt", "version": "2.1.2", "vulns": []}, {"name": "xdis", "version": "6.1.8", "vulns": []}, {"name": "xxhash", "version": "3.6.0", "vulns": []}, {"name": "yarl", "version": "1.22.0", "vulns": []}, {"name": "zipp", "version": "3.23.0", "vulns": []}, {"name": "zope-interface", "version": "8.2", "vulns": []}, {"name": "zstandard", "version": "0.25.0", "vulns": []}], "fixes": []} diff --git a/requirements.txt b/requirements.txt index 9002e61..dad4f68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,7 +39,7 @@ alembic>=1.11.0 # 对象存储 # ============================================ minio>=7.1.0 -aiohttp>=3.8.0 +aiohttp>=3.13.4 # ============================================ # 消息队列 @@ -62,6 +62,7 @@ aiofiles>=23.0.0 orjson>=3.9.0 python-dotenv>=1.0.0 jinja2>=3.1.0 +Pillow>=12.2.0 pyyaml>=6.0 python-dateutil>=2.8.0 diff --git a/src/api/v1/advanced_router.py b/src/api/v1/advanced_router.py index 0a279be..b68f64a 100644 --- a/src/api/v1/advanced_router.py +++ b/src/api/v1/advanced_router.py @@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request from services.auth_service import get_current_active_user from services.redis_task_manager import redis_task_manager +from services.processing_service import processing_service from models.database import User from utils.logger import get_logger @@ -281,6 +282,7 @@ async def export_mold_results( ): body = await request.json() task_id = body.get("task_id") + scheme_id = body.get("scheme_id") formats = body.get("formats", ["step", "stl"]) components = body.get("components", ["cavity", "core"]) @@ -291,16 +293,17 @@ async def export_mold_results( if not task_data: raise HTTPException(404, "任务不存在") - cavity_shapes = task_data.get("cavity_shapes") + cavity_shapes = processing_service.get_export_shapes( + task_id, + scheme_id or task_data.get("best_scheme_id"), + ) filename = task_data.get("filename", f"mold_{task_id}") if not cavity_shapes: - file_path = task_data.get("file_path") - if file_path and os.path.exists(str(file_path)): - cavity_shapes = await _reparse_stp_for_export(str(file_path), task_data.get("material", "ABS")) - - if not cavity_shapes: - raise HTTPException(400, "该任务尚未完成模具生成或形状数据不可用") + raise HTTPException( + 409, + "导出缓存已失效或任务尚未完成,请重新分析后再导出以保证方案一致性", + ) exporter = _get_cached("cad_exporter") if not exporter: @@ -352,17 +355,3 @@ async def get_export_recommendations( return {"status": "success", "data": result} -async def _reparse_stp_for_export(file_path: str, material: str = "ABS") -> dict: - try: - from core.stp_parser import STPParser - from core.mold_generator import MoldCavityGenerator - stp_parser = STPParser() - shape = stp_parser.load_step_file(Path(file_path)) - mold_gen = MoldCavityGenerator(shrinkage_rate=0.005) - mold_gen.set_material(material) - cavity_result = mold_gen.generate_mold_cavities(shape) - logger.info(f"重新解析 STP 用于导出: {file_path}") - return cavity_result - except Exception as e: - logger.warning(f"重新解析 STP 导出失败: {e}") - return None diff --git a/src/api/v1/upload_router.py b/src/api/v1/upload_router.py index ca0d8e0..cb66cd1 100644 --- a/src/api/v1/upload_router.py +++ b/src/api/v1/upload_router.py @@ -1,6 +1,5 @@ # api/v1/upload_router.py -from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks, Depends -from typing import Optional +from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks, Depends, Form import uuid from datetime import datetime from pathlib import Path @@ -27,14 +26,25 @@ file_handler = FileHandler() async def upload_stp( background_tasks: BackgroundTasks, file: UploadFile = File(...), - material: Optional[str] = "ABS", + material: str = Form(...), + draft_angle: float = Form(...), + shrinkage_rate: float = Form(...), + parting_precision: float = Form(...), + cavity_match: int = Form(...), db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): """上传STP文件并存储到数据库""" + process_params = { + "material": material, + "draft_angle": float(draft_angle), + "shrinkage_rate": float(shrinkage_rate), + "parting_precision": float(parting_precision), + "cavity_match": int(cavity_match), + } logger.info( f"[UPLOAD] 用户={current_user.username}(id={current_user.id}) " - f"文件={file.filename} 材料={material} " + f"文件={file.filename} 参数={process_params} " f"大小={file.size if hasattr(file, 'size') else 'unknown'}" ) @@ -44,7 +54,11 @@ async def upload_stp( task_id = str(uuid.uuid4()) - file_path, file_size = await file_handler.save_uploaded_file(file) + try: + file_path, file_size, file_meta = await file_handler.save_uploaded_file(file) + except ValueError as exc: + logger.warning(f"[UPLOAD] 拒绝非法文件: {file.filename}, 原因={exc}") + raise HTTPException(400, str(exc)) from exc logger.info(f"[UPLOAD] 文件已保存: {file_path} ({file_size} bytes), task_id={task_id}") storage_service = StorageIntegrationService() @@ -52,12 +66,17 @@ async def upload_stp( stp_file = await storage_service.save_stp_file( session=db_session, file_path=file_path, - original_filename=file.filename, + original_filename=file_meta["safe_original_name"], user_id=current_user.id ) logger.info(f"[UPLOAD] STP文件已存入RustFS+PG: stp_file.id={stp_file.id}") - await storage_service.create_processing_task(db_session, task_id, stp_file.id) + await storage_service.create_processing_task( + db_session, + task_id, + stp_file.id, + parameters=process_params, + ) task_info = create_task_info( task_id=task_id, @@ -67,11 +86,14 @@ async def upload_stp( file_size=file_size, upload_time=str(datetime.now()) ) + task_info["material"] = material + task_info["parameters"] = process_params + task_info["file_hash"] = file_meta["sha256"] await redis_task_manager.set_task(task_id, task_info) background_tasks.add_task( processing_service.process_file_with_storage, - task_id, file_path, stp_file.id, material + task_id, file_path, stp_file.id, process_params ) logger.info(f"[UPLOAD] 后台处理已调度: task_id={task_id}") @@ -83,6 +105,8 @@ async def upload_stp( "filename": file.filename, "size": file_size, "pythonocc_available": True, - "database_file_id": stp_file.id - } + "database_file_id": stp_file.id, + "sha256": file_meta["sha256"], + }, + "parameters": process_params, } diff --git a/src/core/multi_scheme_planner.py b/src/core/multi_scheme_planner.py index c92f7b3..874b3be 100644 --- a/src/core/multi_scheme_planner.py +++ b/src/core/multi_scheme_planner.py @@ -35,9 +35,11 @@ class MultiSchemeMoldPlanner: material: Dict[str, Any], is_foam_material: bool = False, max_schemes: int = 3, + process_params: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: generator = self.aluminum_foam_generator if is_foam_material else self.mold_generator generator.set_material(material["name"]) + self._apply_process_params(generator, material, process_params) analysis = generator._analyze_product_geometry(shape) analysis["axis_normal_stats"] = self._collect_axis_normal_stats(generator, shape) @@ -67,16 +69,20 @@ class MultiSchemeMoldPlanner: raise ValueError("未能生成任何可用分模方案") scored_schemes = self.scheme_scorer.score_schemes(schemes)[:max_schemes] + export_shapes = {} for idx, scheme in enumerate(scored_schemes, start=1): scheme["raw_scheme_id"] = scheme.get("scheme_id") scheme["scheme_id"] = f"scheme_{idx}" if scheme.get("cavity_data", {}).get("metadata") is not None: scheme["cavity_data"]["metadata"]["scheme_id"] = scheme["scheme_id"] + scheme["cavity_data"]["metadata"]["process_parameters"] = dict(process_params or {}) + export_shapes[scheme["scheme_id"]] = scheme.pop("_export_shapes", {}) best_scheme = scored_schemes[0] return { "best_scheme_id": best_scheme["scheme_id"], "candidate_schemes": scored_schemes, + "_export_shapes": export_shapes, "global_summary": { "scheme_count": len(scored_schemes), "recommended_reason": best_scheme.get("summary", ""), @@ -178,8 +184,26 @@ class MultiSchemeMoldPlanner: "side_actions": side_action_result, "cavity_data": cavity_data, "key_info": key_info, + "_export_shapes": { + "cavity": cavity, + "core": core, + "parting_surface": parting_surface, + }, } + @staticmethod + def _apply_process_params(generator: Any, material: Dict[str, Any], process_params: Optional[Dict[str, Any]]): + params = process_params or {} + draft_angle = float(params.get("draft_angle", getattr(generator, "draft_angle", 2.0))) + shrinkage_rate = float(params.get("shrinkage_rate", material.get("shrinkage", 0.005) * 100.0)) / 100.0 + parting_precision = float(params.get("parting_precision", getattr(generator, "parting_line_tolerance", 0.1))) + cavity_match = float(params.get("cavity_match", getattr(generator, "cavity_match_rate", 95.0))) + + generator.draft_angle = draft_angle + generator.shrinkage_rate = shrinkage_rate + generator.parting_line_tolerance = parting_precision + generator.cavity_match_rate = cavity_match + def _build_parting_surface( self, generator: Any, diff --git a/src/main.py b/src/main.py index 54ebd29..ce92f66 100644 --- a/src/main.py +++ b/src/main.py @@ -74,15 +74,10 @@ async def log_requests(request: Request, call_next): status = response.status_code if status >= 400: - auth_header = request.headers.get("authorization", "") - token_preview = "" - if auth_header.startswith("Bearer "): - token_raw = auth_header[7:] - token_preview = token_raw[:20] + "..." if len(token_raw) > 20 else token_raw logger.warning( f"[HTTP] {request.method} {request.url.path} -> {status} " f"({duration:.2f}s) " - f"token={token_preview or 'none'}" + f"client={request.client.host if request.client else 'unknown'}" ) return response diff --git a/src/services/calculation_service.py b/src/services/calculation_service.py index b08b947..ef821ea 100644 --- a/src/services/calculation_service.py +++ b/src/services/calculation_service.py @@ -316,7 +316,7 @@ class CalculationService: file_path=file_path, cavity_mesh_data=None, ) - return { + result = { "best_scheme_id": "scheme_1", "candidate_schemes": [ { @@ -349,6 +349,8 @@ class CalculationService: "cavity_data": legacy, "key_info": legacy.get("mold_cavities", {}).get("cavity_key_info", {}), } + cls.attach_injection_system_summaries(result, material["name"]) + return result candidate_schemes = plan_result.get("candidate_schemes", []) best_scheme = cls.get_best_scheme(plan_result) @@ -359,8 +361,54 @@ class CalculationService: "cavity_data": best_scheme.get("cavity_data", {}) if best_scheme else {}, "key_info": best_scheme.get("key_info", {}) if best_scheme else {}, } + cls.attach_injection_system_summaries(result, material["name"]) return result + @classmethod + def attach_injection_system_summaries( + cls, + plan_result: Dict[str, Any], + material_name: str, + ) -> Dict[str, Any]: + """为每个候选方案补充注塑模冷却/浇注摘要。""" + from core.mold_system_designer import MoldSystemDesigner + + designer = MoldSystemDesigner() + for scheme in plan_result.get("candidate_schemes", []): + cavity_data = scheme.get("cavity_data") or {} + product_bbox = cavity_data.get("product_analysis", {}).get("bounding_box", {}) + mold_size = cavity_data.get("manufacturing_info", {}).get("estimated_mold_size", {}) + cavity_count = cavity_data.get("mold_cavities", {}).get("cavity_count", 1) + + if not product_bbox or not mold_size: + continue + + system_result = designer.design_complete_system( + mold_size=mold_size, + product_bbox=product_bbox, + material=material_name, + cavity_count=cavity_count, + ) + + cavity_data["injection_system"] = system_result + cavity_data.setdefault("manufacturing_info", {}) + cavity_data["manufacturing_info"]["cooling_summary"] = { + "cooling_time": system_result.get("cooling", {}).get("cooling_time"), + "channel_count": system_result.get("cooling", {}).get("thermal_check", {}).get("channel_count"), + "flow_rate_lpm": system_result.get("cooling", {}).get("flow_rate", {}).get("flow_rate_lpm"), + } + cavity_data["manufacturing_info"]["gating_summary"] = { + "gate_type": system_result.get("gating", {}).get("gate_type"), + "runner_type": system_result.get("gating", {}).get("runner", {}).get("type"), + "estimated_cycle_time": system_result.get("overall_assessment", {}).get("estimated_cycle_time"), + } + + best_scheme = cls.get_best_scheme(plan_result) + if best_scheme: + plan_result["injection_system"] = best_scheme.get("cavity_data", {}).get("injection_system") + + return plan_result + @staticmethod def get_best_scheme(plan_result: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: if not plan_result: diff --git a/src/services/processing_service.py b/src/services/processing_service.py index d3973d6..6791955 100644 --- a/src/services/processing_service.py +++ b/src/services/processing_service.py @@ -2,6 +2,7 @@ """STP 文件处理流程编排器 — 协调解析、网格生成、型腔生成、保存、验证""" import asyncio +import time import traceback from datetime import datetime from pathlib import Path @@ -42,6 +43,7 @@ class ProcessingService: self.html_generator = HTMLGenerator() self.storage_service = StorageIntegrationService() self.multi_scheme_planner = MultiSchemeMoldPlanner() + self._export_shapes_cache: Dict[str, Dict[str, Dict[str, Any]]] = {} # ─── 对外入口 ─── @@ -50,7 +52,7 @@ class ProcessingService: task_id: str, file_path: str, stp_file_id: int, - material: str = "ABS", + process_params: Optional[Dict[str, Any]] = None, ): """处理文件的后台任务 — 使用独立数据库会话""" @@ -65,7 +67,7 @@ class ProcessingService: try: await asyncio.wait_for( self.process_file_core( - task_id, file_path, stp_file_id, db_session, material + task_id, file_path, stp_file_id, db_session, process_params ), timeout_seconds, ) @@ -96,29 +98,35 @@ class ProcessingService: file_path: str, stp_file_id: int, db_session: AsyncSession, - material: str = "ABS", + process_params: Optional[Dict[str, Any]] = None, ): """核心处理逻辑""" try: logger.info(f"开始处理文件并生成模具型腔: {file_path}") + process_params = self._normalize_process_params(process_params) + stage_timings: Dict[str, float] = {} # 1. 解析STP文件 await self.storage_service.update_task_status( db_session, task_id, "processing", 20, "解析STP文件" ) + stage_started = time.perf_counter() shape = self.stp_parser.load_step_file(Path(file_path)) geometry_data = self.stp_parser.analyze_geometry(shape) + stage_timings["parse_stp"] = round(time.perf_counter() - stage_started, 3) # 2. 生成网格数据并持久化 await self.storage_service.update_task_status( db_session, task_id, "processing", 30, "生成网格数据" ) + stage_started = time.perf_counter() mesh_result = await self._step_generate_mesh( shape, geometry_data, file_path, db_session, stp_file_id, task_id ) + stage_timings["generate_mesh"] = round(time.perf_counter() - stage_started, 3) # 3. 生成模具型腔 await self.storage_service.update_task_status( @@ -126,25 +134,35 @@ class ProcessingService: ) # 材料属性 — 通过 MaterialService 集中管理 - requested_material = MaterialService.resolve_material(material) - selected_material = MaterialService.get_material(requested_material) + requested_material = MaterialService.resolve_material(process_params["material"]) + selected_material = dict(MaterialService.get_material(requested_material)) + selected_material["shrinkage"] = process_params["shrinkage_rate"] / 100.0 is_foam_material = MaterialService.is_foam_material(requested_material) + stage_started = time.perf_counter() plan_result = await self._step_generate_cavity( - shape, selected_material, is_foam_material + shape, selected_material, is_foam_material, process_params ) + stage_timings["generate_cavity"] = round(time.perf_counter() - stage_started, 3) + export_shapes = {} + if plan_result: + export_shapes = plan_result.pop("_export_shapes", {}) or {} + if export_shapes: + self._cache_export_shapes(task_id, export_shapes) # 4. 生成详细JSON数据 — 委托 CalculationService await self.storage_service.update_task_status( db_session, task_id, "processing", 60, "生成型腔详细数据" ) + stage_started = time.perf_counter() detailed_cavity_json = CalculationService.build_plan_result( geometry_data=geometry_data, material=selected_material, file_path=str(file_path), plan_result=plan_result, ) + stage_timings["build_plan_result"] = round(time.perf_counter() - stage_started, 3) best_scheme = CalculationService.get_best_scheme(detailed_cavity_json) best_cavity_data = best_scheme.get("cavity_data", {}) if best_scheme else {} @@ -164,6 +182,7 @@ class ProcessingService: db_session, task_id, "processing", 70, "保存几何数据" ) + stage_started = time.perf_counter() await self.storage_service.save_geometry_data( db_session, stp_file_id, @@ -229,9 +248,15 @@ class ProcessingService: Path(html_file_path).name, html_file_path, ) + stage_timings["persist_artifacts"] = round(time.perf_counter() - stage_started, 3) # 9. 分析模具设计 - analysis_result = self.geometry_analyzer.analyze_mold_design(geometry_data) + stage_started = time.perf_counter() + analysis_result = self.geometry_analyzer.analyze_mold_design( + geometry_data, + product_material=requested_material, + shape=shape, + ) if analysis_result: await self.storage_service.save_features_and_recommendations( @@ -242,6 +267,7 @@ class ProcessingService: ) await self._save_analysis_metrics(db_session, stp_file_id, analysis_result) + stage_timings["analyze_design"] = round(time.perf_counter() - stage_started, 3) # 9.6 更新STP文件的分析摘要字段 await self.storage_service.update_stp_file_analysis_summary( @@ -255,20 +281,35 @@ class ProcessingService: ) # 9.7 FreeCAD 几何验证 + stage_started = time.perf_counter() verification_result = await self._step_verify( file_path, db_session, task_id, stp_file_id, analysis_result ) + stage_timings["verify_geometry"] = round(time.perf_counter() - stage_started, 3) # 9.8 LLM 增强分析 llm_report = None + stage_started = time.perf_counter() if analysis_result: llm_report = await llm_service.generate_design_report(analysis_result, detailed_cavity_json) + stage_timings["generate_llm_report"] = round(time.perf_counter() - stage_started, 3) # 10. 完成处理 await self.storage_service.update_stp_file_status(db_session, stp_file_id, "completed") await self.storage_service.update_task_status( db_session, task_id, "completed", 100, "模具型腔生成完成" ) + await self.storage_service.update_task_parameters( + db_session, + task_id, + { + "stage_timings": stage_timings, + "material": requested_material, + "verification": verification_result, + "llm_report": llm_report, + **process_params, + }, + ) # 更新任务缓存状态 await redis_task_manager.update_task(task_id, { @@ -279,6 +320,9 @@ class ProcessingService: "best_scheme_id": detailed_cavity_json.get("best_scheme_id"), "cavity_data": best_cavity_data, "key_info": best_key_info, + "material": requested_material, + "parameters": process_params, + "stage_timings": stage_timings, "html_file": best_scheme.get("html_file", f"/html/{Path(html_file_path).name}") if best_scheme else f"/html/{Path(html_file_path).name}", "verification": verification_result, "llm_report": llm_report, @@ -370,7 +414,7 @@ class ProcessingService: return mesh_result async def _step_generate_cavity( - self, shape, selected_material: dict, is_foam_material: bool, + self, shape, selected_material: dict, is_foam_material: bool, process_params: Dict[str, Any], ) -> Optional[Dict[str, Any]]: """生成多方案分模结果""" plan_result = None @@ -380,6 +424,7 @@ class ProcessingService: shape=shape, material=selected_material, is_foam_material=is_foam_material, + process_params=process_params, ) logger.info( f"多方案分模完成: 生成 {len(plan_result.get('candidate_schemes', []))} 套方案" @@ -391,6 +436,28 @@ class ProcessingService: return plan_result + def _cache_export_shapes(self, task_id: str, export_shapes: Dict[str, Dict[str, Any]]): + self._export_shapes_cache[task_id] = export_shapes + + def get_export_shapes(self, task_id: str, scheme_id: Optional[str] = None) -> Optional[Dict[str, Any]]: + scheme_map = self._export_shapes_cache.get(task_id, {}) + if not scheme_map: + return None + if scheme_id: + return scheme_map.get(scheme_id) + return next(iter(scheme_map.values()), None) + + @staticmethod + def _normalize_process_params(process_params: Optional[Dict[str, Any]]) -> Dict[str, Any]: + payload = dict(process_params or {}) + return { + "material": MaterialService.resolve_material(str(payload.get("material", "ABS"))), + "draft_angle": float(payload.get("draft_angle", 2.0)), + "shrinkage_rate": float(payload.get("shrinkage_rate", 0.5)), + "parting_precision": float(payload.get("parting_precision", 0.1)), + "cavity_match": int(payload.get("cavity_match", 95)), + } + async def _step_verify( self, file_path: str, db_session: AsyncSession, task_id: str, stp_file_id: int, analysis_result: Optional[dict], diff --git a/src/services/storage_integration_rustfs.py b/src/services/storage_integration_rustfs.py index 6408fe7..ce833d8 100644 --- a/src/services/storage_integration_rustfs.py +++ b/src/services/storage_integration_rustfs.py @@ -127,10 +127,14 @@ class StorageIntegrationService: logger.info(f"STP文件保存成功 RustFS: {stp_file.id}, 批次: {batch_id}") return stp_file - async def create_processing_task(self, session: AsyncSession, - task_id: str, - stp_file_id: int, - task_type: str = "stp_parsing") -> ProcessingTask: + async def create_processing_task( + self, + session: AsyncSession, + task_id: str, + stp_file_id: int, + task_type: str = "stp_parsing", + parameters: Optional[Dict[str, Any]] = None, + ) -> ProcessingTask: """创建处理任务记录""" try: task = ProcessingTask( @@ -138,7 +142,8 @@ class StorageIntegrationService: stp_file_id=stp_file_id, task_type=task_type, status="pending", - started_time=datetime.now() + started_time=datetime.now(), + parameters=parameters or {}, ) session.add(task) @@ -189,6 +194,30 @@ class StorageIntegrationService: logger.error(f"更新任务状态失败: {e}") raise + async def update_task_parameters( + self, + session: AsyncSession, + task_id: str, + parameters: Dict[str, Any], + ): + """合并更新任务参数,便于保存阶段耗时等元数据。""" + try: + task = await session.execute( + select(ProcessingTask).where(ProcessingTask.task_id == task_id) + ) + task = task.scalar_one_or_none() + if task is None: + return + + merged = dict(task.parameters or {}) + merged.update(parameters or {}) + task.parameters = merged + await session.commit() + except Exception as e: + await session.rollback() + logger.error(f"更新任务参数失败: {e}") + raise + async def update_stp_file_status(self, session: AsyncSession, stp_file_id: int, status: str): """更新STP文件状态""" try: @@ -616,7 +645,11 @@ class StorageIntegrationService: 'volume_utilization': stp_file.analysis_metrics.volume_utilization, 'topology_complexity': stp_file.analysis_metrics.topology_complexity, 'wall_uniformity': stp_file.analysis_metrics.wall_uniformity, - 'analysis_summary': stp_file.analysis_metrics.analysis_summary + 'analysis_summary': stp_file.analysis_metrics.analysis_summary, + 'verification_status': stp_file.analysis_metrics.verification_status, + 'verification_volume_diff': stp_file.analysis_metrics.verification_volume_diff, + 'verification_area_diff': stp_file.analysis_metrics.verification_area_diff, + 'verification_details': stp_file.analysis_metrics.verification_details, } return result diff --git a/src/services/task_query_service.py b/src/services/task_query_service.py index d7eba93..e786228 100644 --- a/src/services/task_query_service.py +++ b/src/services/task_query_service.py @@ -84,8 +84,10 @@ class TaskQueryService: # 构造与内存任务兼容的任务视图 cam_preferences = {} + task_parameters = {} if isinstance(processing_task.parameters, dict): cam_preferences = processing_task.parameters.get("cam_preferences", {}) or {} + task_parameters = dict(processing_task.parameters) task_view = { "task_id": processing_task.task_id, @@ -108,6 +110,12 @@ class TaskQueryService: "plan_result": cavity_json, "mesh_summary": mesh_summary, "html_file": html_file_url, + "material": task_parameters.get("material"), + "parameters": task_parameters, + "stage_timings": task_parameters.get("stage_timings", {}), + "verification": task_parameters.get("verification") + or file_with_data.get("analysis_metrics", {}).get("verification_details"), + "llm_report": task_parameters.get("llm_report"), "analysis_result": { "geometry_data": geometry_json, "detected_features": features_json, diff --git a/src/utils/file_handler.py b/src/utils/file_handler.py index cf76366..0d7422c 100644 --- a/src/utils/file_handler.py +++ b/src/utils/file_handler.py @@ -1,24 +1,66 @@ # utils/file_handler.py import aiofiles +import hashlib +import re +import uuid from pathlib import Path from fastapi import UploadFile -from typing import Tuple +from typing import Tuple, Dict, Any class FileHandler: - def __init__(self, upload_dir: str = "uploads"): + def __init__(self, upload_dir: str = "uploads", max_file_size: int = 50 * 1024 * 1024): self.upload_dir = Path(upload_dir) self.upload_dir.mkdir(exist_ok=True) + self.max_file_size = max_file_size - async def save_uploaded_file(self, file: UploadFile) -> Tuple[Path, int]: - """保存上传的文件""" - file_path = self.upload_dir / file.filename + def _sanitize_filename(self, filename: str) -> str: + original = Path(filename or "upload.step").name + suffix = Path(original).suffix.lower() + stem = Path(original).stem or "upload" + safe_stem = re.sub(r"[^A-Za-z0-9._-]+", "_", stem).strip("._-") or "upload" + if suffix not in {".stp", ".step"}: + suffix = ".step" + return f"{safe_stem}{suffix}" + @staticmethod + def _looks_like_step(content: bytes) -> bool: + if not content: + return False + head = content[:4096].decode("utf-8", errors="ignore").upper() + return ( + "ISO-10303-21" in head + or "HEADER;" in head + or "FILE_SCHEMA" in head + or "DATA;" in head + ) + + async def save_uploaded_file(self, file: UploadFile) -> Tuple[Path, int, Dict[str, Any]]: + """保存上传的文件并返回安全元数据""" content = await file.read() - async with aiofiles.open(file_path, 'wb') as f: + + if not content: + raise ValueError("上传文件为空") + if len(content) > self.max_file_size: + raise ValueError(f"上传文件过大,限制 {self.max_file_size // (1024 * 1024)}MB") + if not self._looks_like_step(content): + raise ValueError("文件内容不是有效的 STP/STEP 数据") + + safe_name = self._sanitize_filename(file.filename) + unique_name = f"{uuid.uuid4().hex}_{safe_name}" + file_path = self.upload_dir / unique_name + + async with aiofiles.open(file_path, "wb") as f: await f.write(content) - return file_path, len(content) + metadata = { + "original_filename": file.filename, + "safe_original_name": safe_name, + "stored_filename": unique_name, + "sha256": hashlib.sha256(content).hexdigest(), + } + + return file_path, len(content), metadata def cleanup_file(self, file_path: Path): """清理文件""" diff --git a/static/style.css b/static/style.css index 257e65a..8ef0e5c 100644 --- a/static/style.css +++ b/static/style.css @@ -604,6 +604,57 @@ body { margin-bottom: var(--space-8); } +.moldinsight-intro-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-4); + margin-bottom: var(--space-6); +} + +.intro-card { + background: var(--bg-primary); + border: 1px solid var(--border-light); + border-radius: var(--radius-xl); + padding: var(--space-5); + box-shadow: var(--shadow-xs); +} + +.intro-card-title { + font-size: var(--text-xs); + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: var(--space-2); +} + +.intro-card-text { + font-size: var(--text-sm); + line-height: 1.7; + color: var(--text-primary); + font-weight: var(--font-medium); +} + +.upload-layout { + display: grid; + grid-template-columns: minmax(0, 1.6fr) minmax(320px, 0.9fr); + gap: var(--space-6); + align-items: start; + margin-bottom: var(--space-8); +} + +.upload-main-card, +.upload-side-card { + background: var(--bg-primary); + border: 1px solid var(--border-light); + border-radius: var(--radius-xl); + padding: var(--space-6); +} + +.upload-main-card .section-title, +.upload-side-card .section-title { + margin-bottom: var(--space-4); +} + .upload-section .btn-primary { min-width: 200px; padding: var(--space-4) var(--space-8); @@ -649,6 +700,10 @@ body { border: 1px solid #e0e0e0; } +.compact-panel { + margin-top: 0; +} + .panel-header { margin-bottom: 16px; } @@ -749,6 +804,58 @@ optgroup { font-weight: 500; } +.advanced-params { + margin-top: var(--space-4); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + background: var(--bg-primary); +} + +.advanced-params summary { + cursor: pointer; + list-style: none; + padding: var(--space-4); + font-size: var(--text-sm); + font-weight: var(--font-semibold); + color: var(--text-primary); +} + +.advanced-params summary::-webkit-details-marker { + display: none; +} + +.advanced-params-body { + padding: 0 var(--space-4) var(--space-4); +} + +.upload-submit-btn { + width: 100%; + margin-top: var(--space-4); +} + +.history-collapsible { + background: var(--bg-primary); + border: 1px solid var(--border-light); + border-radius: var(--radius-xl); + padding: var(--space-5); +} + +.history-summary { + cursor: pointer; + font-size: var(--text-lg); + font-weight: var(--font-semibold); + color: var(--text-primary); + list-style: none; +} + +.history-summary::-webkit-details-marker { + display: none; +} + +.history-collapsible[open] .table-container { + margin-top: var(--space-4); +} + .btn-clear { background: none; border: none; @@ -1191,6 +1298,19 @@ optgroup { grid-column: 1 / -1; } +.result-card-highlight { + border: 1px solid var(--primary-100); + background: linear-gradient(180deg, var(--bg-primary) 0%, var(--primary-50) 100%); +} + +.result-priority-grid { + margin-bottom: var(--space-6); +} + +.compact-metrics-grid { + margin-bottom: var(--space-6); +} + .single-scheme-grid { grid-template-columns: 1fr !important; } @@ -1276,6 +1396,22 @@ optgroup { line-height: 1.6; } +.diagnostic-panel summary { + cursor: pointer; + list-style: none; + font-size: var(--text-lg); + font-weight: var(--font-semibold); + color: var(--text-primary); +} + +.diagnostic-panel summary::-webkit-details-marker { + display: none; +} + +.diagnostic-panel-body { + margin-top: var(--space-4); +} + /* ================================ 设计建议列表 ================================ */ @@ -2636,6 +2772,11 @@ select.form-input { flex-wrap: wrap; } +.export-buttons-block { + margin-left: 0; + margin-top: var(--space-4); +} + .export-buttons .btn-sm { font-size: 0.75rem; padding: 0.25rem 0.6rem; @@ -2695,6 +2836,13 @@ select.form-input { color: var(--text-primary); } +@media (max-width: 960px) { + .moldinsight-intro-grid, + .upload-layout { + grid-template-columns: 1fr; + } +} + /* ================================ 历史记录 ================================ */ diff --git a/static/vue-app.js b/static/vue-app.js index 0977b6d..33ace19 100644 --- a/static/vue-app.js +++ b/static/vue-app.js @@ -981,8 +981,6 @@ const UsersView = { const MoldInsightView = { setup() { const router = useRouter(); - const deliveryDate = ref(null); - const expectedDate = ref(null); const state = reactive({ selectedFile: null, selectedMaterial: 'ABS', @@ -1079,6 +1077,10 @@ const MoldInsightView = { const formData = new FormData(); formData.append("file", state.selectedFile); formData.append("material", state.selectedMaterial); + formData.append("draft_angle", String(state.moldParams.draftAngle)); + formData.append("shrinkage_rate", String(state.moldParams.shrinkageRate)); + formData.append("parting_precision", String(state.moldParams.partingPrecision)); + formData.append("cavity_match", String(state.moldParams.cavityMatch)); try { const res = await fetch("/api/upload", { @@ -1154,11 +1156,6 @@ const MoldInsightView = { loadHistory(); }); - const isFoamMaterial = (material) => { - const foamMaterials = ['AlSi10Mg', 'AlSi12', 'Pure Al Foam', 'AlSi7Mg']; - return foamMaterials.includes(material); - }; - return { state, handleFileChange, @@ -1168,56 +1165,74 @@ const MoldInsightView = { formatDateTime, formatNumber, toggleFileHistory, - viewResult, - isFoamMaterial + viewResult }; }, template: `
- -
-
- -
📁
-
- 点击选择或拖拽文件 - 支持 .stp, .step 格式,最大 100MB + +
+
+
输入
+
STEP/STP 产品件,面向注塑模主流程
+
+
+
输出
+
分模方案、DFM 风险、注塑模系统摘要与 CAD 导出
+
+
+
目标
+
先确认推荐方案,再进入导出与 CAM 准备
+
+
+ +
+
+

1. 上传产品件

+
+ +
📁
+
+ 点击选择或拖拽 STP/STEP 文件 + 支持注塑模产品件分析,最大 100MB +
+
+ +
+ {{ state.selectedFile.name }} + {{ formatFileSize(state.selectedFile.size) }} + +
+ +
{{ state.error }}
+ +
+
- -
- {{ state.selectedFile.name }} - {{ formatFileSize(state.selectedFile.size) }} - -
- - -
-
- 📦 材料与参数设置 -
- - -
- - @@ -1226,70 +1241,58 @@ const MoldInsightView = { - - - - - - - - -
- - -
-
- ⚙️ 铝泡沫专用参数 - -
-
- - - {{ state.moldParams.draftAngle }}° -
- -
- - - {{ state.moldParams.shrinkageRate }}% -
-
- -
-
- - - {{ state.moldParams.partingPrecision }} mm -
- -
- - - {{ state.moldParams.cavityMatch }}% -
-
+
+ +
+ 高级工艺参数 +
+
默认值适用于多数注塑件;仅在已知工艺约束时再调整。
+
+
+ + + {{ state.moldParams.draftAngle }}° +
+ +
+ + + {{ state.moldParams.shrinkageRate }}% +
+
+ +
+
+ + + {{ state.moldParams.partingPrecision }} mm +
+ +
+ + + {{ state.moldParams.cavityMatch }}% +
+
+
+
-
- -
{{ state.error }}
- - - -
-
+ + +
先选择 STP 文件,再填写材料并开始分析。
-
-

分析历史

+
+ 分析历史({{ state.history.files.length }} 个文件)
@@ -1369,7 +1372,7 @@ const MoldInsightView = {
-
+
` }; @@ -1457,10 +1460,6 @@ const ResultView = { return priorityMap[priority] || priority; }; - const isFoamMaterial = (material) => { - return material === 'aluminum_foam'; - }; - const candidateSchemes = computed(() => state.task?.candidate_schemes || []); const hasSingleScheme = computed(() => candidateSchemes.value.length === 1); const selectedScheme = computed(() => { @@ -1471,6 +1470,33 @@ const ResultView = { const selectedKeyInfo = computed(() => selectedScheme.value?.key_info || state.task?.key_info || null); const selectedHtmlFile = computed(() => selectedScheme.value?.html_file || state.task?.html_file || ''); const selectedDfmViolations = computed(() => selectedScheme.value?.dfm_violations || []); + const selectedInjectionSystem = computed(() => selectedCavityData.value?.injection_system || state.task?.plan_result?.injection_system || null); + const stageTimingEntries = computed(() => { + const timings = state.task?.stage_timings || {}; + return Object.entries(timings) + .filter(([, value]) => typeof value === 'number') + .sort((a, b) => b[1] - a[1]); + }); + const sortedRecommendations = computed(() => { + const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; + return [...(state.task?.analysis_result?.design_recommendations || [])].sort((a, b) => { + return (priorityOrder[a.priority] ?? 99) - (priorityOrder[b.priority] ?? 99); + }); + }); + const primaryRecommendation = computed(() => sortedRecommendations.value[0] || null); + const selectedDfmCount = computed(() => selectedDfmViolations.value?.length || 0); + const selectedCavityCount = computed(() => { + const cavityValue = selectedCavityData.value?.mold_cavities?.cavity_count; + if (typeof cavityValue === 'number') return cavityValue; + const cavityObj = selectedCavityData.value?.mold_cavities || selectedKeyInfo.value?.mold_cavities || {}; + return cavityObj.cavity_count || Object.keys(cavityObj).filter(key => key.startsWith('cavity_')).length || 1; + }); + const selectedRiskLabel = computed(() => { + if (!selectedDfmCount.value) return '低风险'; + if (selectedDfmCount.value >= 4) return '高风险'; + if (selectedDfmCount.value >= 2) return '中风险'; + return '低风险'; + }); const selectScheme = (schemeId) => { state.selectedSchemeId = schemeId; @@ -1495,6 +1521,40 @@ const ResultView = { return `[${dir.map(v => Number(v).toFixed(2)).join(', ')}]`; }; + const analysisFeatures = computed(() => state.task?.analysis_result?.detected_features || []); + const getFeatureByTypes = (...types) => analysisFeatures.value.find(f => types.includes(f.feature_type)); + const countFeaturesByTypes = (...types) => analysisFeatures.value.filter(f => types.includes(f.feature_type)).length; + const getWallThicknessSummary = () => { + const feature = getFeatureByTypes('thin_wall', 'thick_wall', 'wall_non_uniform'); + if (!feature) return '待分析'; + const params = feature.parameters || {}; + if (params.min_thickness != null && params.max_thickness != null) { + return `${Number(params.min_thickness).toFixed(2)} - ${Number(params.max_thickness).toFixed(2)} mm`; + } + if (params.average_thickness != null) { + return `平均 ${Number(params.average_thickness).toFixed(2)} mm`; + } + return feature.recommendations?.[0] || '已完成分析'; + }; + const getUndercutCount = () => { + const fromScheme = selectedScheme.value?.undercut_regions?.length || selectedCavityData.value?.undercut_regions?.length || 0; + return fromScheme || countFeaturesByTypes('undercut'); + }; + const formatStageName = (name) => { + const stageNameMap = { + parse_stp: 'STP解析', + generate_mesh: '网格生成', + generate_cavity: '分模与型腔生成', + build_plan_result: '方案结果组装', + persist_artifacts: '结果持久化', + analyze_design: '几何分析', + verify_geometry: '几何验证', + generate_llm_report: 'LLM报告生成' + }; + return stageNameMap[name] || name; + }; + const formatTiming = (value) => `${Number(value || 0).toFixed(3)} s`; + const exportCAD = async (format) => { try { const taskId = route.params.taskId; @@ -1503,6 +1563,7 @@ const ResultView = { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_id: taskId, + scheme_id: selectedScheme.value?.scheme_id || state.selectedSchemeId, formats: [format], components: ['cavity', 'core', 'parting_surface'] }) @@ -1586,11 +1647,22 @@ const ResultView = { selectedKeyInfo, selectedHtmlFile, selectedDfmViolations, + selectedInjectionSystem, + stageTimingEntries, + getWallThicknessSummary, + countFeaturesByTypes, + getUndercutCount, + formatStageName, + formatTiming, + primaryRecommendation, + sortedRecommendations, + selectedDfmCount, + selectedCavityCount, + selectedRiskLabel, formatFileSize, formatDateTime, formatNumber, getPriorityText, - isFoamMaterial, exportCAD, generateCamPlan, camSteelOptions, @@ -1623,28 +1695,90 @@ const ResultView = { {{ state.task.status }} -
- - - - - +
+ +
+
+
+

推荐方案

+ {{ selectedScheme?.title || selectedScheme?.scheme_id || '方案待定' }} +
+
+
+ 分型方向 + {{ formatSchemeDirection(selectedScheme) }} +
+
+ 分型面位置 + {{ selectedScheme?.offset_label || '中面' }} +
+
+ 方案总分 + {{ formatNumber(selectedScheme?.score || 0) }} +
+
+ 方案可信度 + {{ formatNumber(selectedScheme?.confidence_score || 0) }} +
+
+ 主要理由 + {{ selectedScheme?.summary || primaryRecommendation?.description || '基于当前几何与制造约束自动推荐' }} +
+
+ 高优先级建议 + {{ primaryRecommendation?.description || '未发现高优先级工艺建议' }} +
+
+
+ +
+
+

关键操作

+ + {{ selectedRiskLabel }} + +
+
+
+ DFM 风险数 + {{ selectedDfmCount }} 项 +
+
+ 推荐型腔数 + {{ selectedCavityCount }} 腔 +
+
+ 预计成型周期 + {{ selectedInjectionSystem?.overall_assessment?.estimated_cycle_time || 'N/A' }} s +
+
+ 下一步 + {{ selectedDfmCount ? '先处理 DFM 风险,再确认导出或 CAM' : '可进入 3D 复核、导出与 CAM 准备' }} +
+
+
+ + + + + +
- -
+ +
-

文件信息

+

任务信息

文件大小 @@ -1662,7 +1796,7 @@ const ResultView = {
-

几何数据

+

几何概览

顶点数 @@ -1690,6 +1824,7 @@ const ResultView = {

候选分模方案

+
先切换并确认推荐方案,再进入后续工程判断、导出与 CAM。
-
-

方案对比

-
+
+ 方案对比 +
@@ -1801,13 +1936,13 @@ const ResultView = {
-
+
-

模具信息

+

工程摘要

-

基本信息

+

方案与零件

零件名称 @@ -1824,11 +1959,44 @@ const ResultView = {
-

型腔信息

+

型腔与锁模

型腔数量 - {{ Object.keys(selectedCavityData?.mold_cavities || selectedKeyInfo?.mold_cavities || {}).length }} + {{ selectedCavityCount }} +
+
+ 预估锁模力 + {{ selectedCavityData?.manufacturing_info?.estimated_clamping_force || '自动计算' }} +
+
+
+
+

注塑模系统

+
+
+ 预计成型周期 + {{ selectedInjectionSystem?.overall_assessment?.estimated_cycle_time || 'N/A' }} s +
+
+ 冷却时间 + {{ selectedInjectionSystem?.cooling?.cooling_time || 'N/A' }} s +
+
+ 水路数量 + {{ selectedInjectionSystem?.cooling?.thermal_check?.channel_count || 0 }} +
+
+ 冷却流量 + {{ selectedInjectionSystem?.cooling?.flow_rate?.flow_rate_lpm || 'N/A' }} L/min +
+
+ 浇口类型 + {{ selectedInjectionSystem?.gating?.gate_type || 'N/A' }} +
+
+ 流道形式 + {{ selectedInjectionSystem?.gating?.runner?.type || 'N/A' }}
@@ -1861,8 +2029,31 @@ const ResultView = {
+
+

3D 预览

+
+
预览加载失败
+
HTML 已生成但加载异常,请检查该链接是否可访问:{{ selectedHtmlFile }}
+
+ +
+ +
+

3D 预览

+
+
预览未生成
+
当前任务没有返回 HTML 预览链接,属于未生成状态。
+
+
+
-

CAM 准备包

+

导出与 CAM

CAM 参数

@@ -1973,62 +2164,9 @@ const ResultView = {
- -
-

3D 预览

-
-
预览加载失败
-
HTML 已生成但加载异常,请检查该链接是否可访问:{{ selectedHtmlFile }}
-
- -
- -
-

3D 预览

-
-
预览未生成
-
当前任务没有返回 HTML 预览链接,属于未生成状态。
-
-
- +

分析结果详情

- - -
-
-
-

FreeCAD 几何验证

- - {{ state.task.verification.status === 'passed' ? '验证通过' : state.task.verification.status === 'failed' ? '验证失败' : state.task.verification.status === 'error' ? '验证错误' : state.task.verification.status === 'skipped' ? '已跳过' : '未知' }} - -
-
-
-
- 体积差异 - - {{ state.task.verification.comparison.volume.difference_percent?.toFixed(4) || 0 }}% - -
-
- 表面积差异 - - {{ state.task.verification.comparison.surface_area.difference_percent?.toFixed(4) || 0 }}% - -
-
-
-
-
- -

分析摘要

@@ -2038,7 +2176,7 @@ const ResultView = { {{ state.task.analysis_result.analysis_summary }}
- +

1. 产品特征识别

@@ -2052,19 +2190,19 @@ const ResultView = {
壁厚分布 - {{ state.task.analysis_result.detected_features?.find(f => f.feature_type === 'wall_thickness')?.description || '待分析' }} + {{ getWallThicknessSummary() }}
加强筋位置和密度 - {{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'rib').length || 0 }} 个加强筋 + {{ countFeaturesByTypes('rib_structure') || 0 }} 个加强筋
- 孔洞和凹槽位置 - {{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'hole' || f.feature_type === 'pocket').length || 0 }} 个孔洞/凹槽 + 柱位和功能特征 + {{ countFeaturesByTypes('boss_feature') || 0 }} 个柱位/功能特征
倒扣区域检测 - {{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'undercut').length || 0 }} 个倒扣区域 + {{ getUndercutCount() }} 个倒扣区域
对称性分析 @@ -2081,27 +2219,27 @@ const ResultView = {
-

2. 泡沫包装设计决策

+

2. 注塑模设计决策

- 泡沫厚度建议 - {{ state.task.analysis_result.design_recommendations?.find(r => r.priority === 'high')?.recommendation || '根据产品重量和脆弱程度自动计算' }} + 高优先级工艺建议 + {{ state.task.analysis_result.design_recommendations?.find(r => r.priority === 'high')?.description || '根据几何分析自动生成' }}
加强筋布局 - 基于产品薄弱区域自动布置 + {{ countFeaturesByTypes('rib_structure') ? '已识别加强筋区域,建议校核厚度和脱模方向' : '当前未识别明显加强筋特征' }}
- 取手槽位置 - 基于重心位置:{{ state.task.analysis_result.geometry_data?.center_of_mass ? '自动优化' : '手动设置' }} + 浇口与进胶提示 + {{ state.task.analysis_result.design_recommendations?.find(r => r.type === 'wall_thickness' || r.type === 'draft_angle')?.reason || '建议结合主分型方向确认浇口位置' }}
- 通风孔位置 - 防止真空吸附:建议在产品最大平面区域设置通风孔 + 冷却设计提示 + {{ getWallThicknessSummary() !== '待分析' ? '建议按壁厚分布校核冷却均匀性' : '需先完成壁厚分析后再校核冷却' }}
定位结构设计 - {{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === '定位').length || 0 }} 个定位特征 + {{ countFeaturesByTypes('boss_feature') || 0 }} 个柱位/定位相关特征
分型面选择 @@ -2109,13 +2247,10 @@ const ResultView = {
-
+
详细设计建议
-
+
{{ getPriorityText(rec.priority) }} {{ rec.description }} ({{ rec.reason }}) @@ -2179,6 +2314,65 @@ const ResultView = {
+ +
+ 诊断信息 +
+
+

处理耗时

+ + + + + + + + + + + + + +
阶段耗时
{{ formatStageName(stage) }}{{ formatTiming(duration) }}
+
+ +
+
+
+

FreeCAD 几何验证

+ + {{ state.task.verification.status === 'passed' ? '验证通过' : state.task.verification.status === 'failed' ? '验证失败' : state.task.verification.status === 'error' ? '验证错误' : state.task.verification.status === 'skipped' ? '已跳过' : '未知' }} + +
+
+
+
+ 体积差异 + + {{ state.task.verification.comparison.volume.difference_percent?.toFixed(4) || 0 }}% + +
+
+ 表面积差异 + + {{ state.task.verification.comparison.surface_area.difference_percent?.toFixed(4) || 0 }}% + +
+
+
+
+
+ +
+
+

LLM 设计报告

+
+
+
{{ state.task.llm_report }}
+
+
+
+
` diff --git a/temp_requirements_audit.txt b/temp_requirements_audit.txt new file mode 100644 index 0000000..d372145 --- /dev/null +++ b/temp_requirements_audit.txt @@ -0,0 +1,35 @@ +pythonocc-core>=7.7.0 +trimesh>=3.21.0 +numpy>=1.24.0 +scipy>=1.10.0 +pyvista>=0.38.0 +fastapi>=0.100.0 +uvicorn[standard]>=0.22.0 +pydantic>=2.0.0 +python-multipart>=0.0.6 +sqlalchemy>=2.0.0 +psycopg2-binary>=2.9.0 +asyncpg>=0.28.0 +alembic>=1.11.0 +minio>=7.1.0 +aiohttp>=3.8.0 +kafka-python>=2.0.2 +redis>=4.5.0 +python-jose[cryptography]>=3.3.0 +bcrypt>=4.0.0 +passlib>=1.7.4 +email-validator>=2.0.0 +aiofiles>=23.0.0 +orjson>=3.9.0 +python-dotenv>=1.0.0 +jinja2>=3.1.0 +pyyaml>=6.0 +python-dateutil>=2.8.0 +loguru>=0.7.0 +pytest>=7.0.0 +pytest-asyncio>=0.21.0 +httpx>=0.24.0 +aiosqlite>=0.19.0 +black>=23.0.0 +flake8>=6.0.0 +mypy>=1.0.0 diff --git a/tests/temp_test_injection_p0.py b/tests/temp_test_injection_p0.py new file mode 100644 index 0000000..324ed2b --- /dev/null +++ b/tests/temp_test_injection_p0.py @@ -0,0 +1,265 @@ +import io +import importlib.util +import sys +import types +from pathlib import Path +from types import SimpleNamespace + +import pytest +from fastapi import FastAPI, UploadFile +from httpx import ASGITransport, AsyncClient + +from services.calculation_service import CalculationService +from utils.file_handler import FileHandler + + +VALID_STEP_BYTES = ( + b"ISO-10303-21;\n" + b"HEADER;\n" + b"FILE_DESCRIPTION(('STEP AP214'),'1');\n" + b"ENDSEC;\n" + b"DATA;\n" + b"ENDSEC;\n" + b"END-ISO-10303-21;\n" +) + + +def _load_module_from_path(module_name: str, file_path: str, stub_modules: dict[str, object]): + originals = {} + for name, module in stub_modules.items(): + originals[name] = sys.modules.get(name) + sys.modules[name] = module + + try: + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + finally: + for name, original in originals.items(): + if original is None: + sys.modules.pop(name, None) + else: + sys.modules[name] = original + + +@pytest.mark.asyncio +async def test_file_handler_sanitizes_step_filename(tmp_path): + handler = FileHandler(upload_dir=str(tmp_path)) + upload = UploadFile(filename="../../bad name?.step", file=io.BytesIO(VALID_STEP_BYTES)) + + file_path, file_size, meta = await handler.save_uploaded_file(upload) + + assert file_path.exists() + assert file_size == len(VALID_STEP_BYTES) + assert file_path.parent == tmp_path + assert ".." not in file_path.name + assert meta["safe_original_name"] == "bad_name.step" + assert len(meta["sha256"]) == 64 + + +@pytest.mark.asyncio +async def test_file_handler_rejects_invalid_step_content(tmp_path): + handler = FileHandler(upload_dir=str(tmp_path)) + upload = UploadFile(filename="fake.step", file=io.BytesIO(b"not-a-step")) + + with pytest.raises(ValueError, match="不是有效的 STP/STEP 数据"): + await handler.save_uploaded_file(upload) + + +@pytest.mark.asyncio +async def test_export_route_requires_cached_shapes(monkeypatch): + fake_processing_service = types.ModuleType("services.processing_service") + fake_processing_service.processing_service = SimpleNamespace( + get_export_shapes=lambda task_id, scheme_id=None: None, + ) + fake_auth_service = types.ModuleType("services.auth_service") + async def fake_current_user(): + return SimpleNamespace(id=1) + fake_auth_service.get_current_active_user = fake_current_user + + fake_redis_task_manager = types.ModuleType("services.redis_task_manager") + fake_redis_task_manager.redis_task_manager = SimpleNamespace(get_task=None) + + fake_models_database = types.ModuleType("models.database") + fake_models_database.User = SimpleNamespace + + advanced_router = _load_module_from_path( + "temp_advanced_router", + "d:\\Project\\geMoldInsight\\src\\api\\v1\\advanced_router.py", + { + "services.processing_service": fake_processing_service, + "services.auth_service": fake_auth_service, + "services.redis_task_manager": fake_redis_task_manager, + "models.database": fake_models_database, + }, + ) + + app = FastAPI() + app.include_router(advanced_router.router) + app.dependency_overrides[advanced_router.get_current_active_user] = lambda: SimpleNamespace(id=1) + + async def fake_get_task_data(task_id): + return { + "task_id": task_id, + "filename": "demo.step", + "best_scheme_id": "scheme_1", + } + + monkeypatch.setattr(advanced_router, "_get_task_data", fake_get_task_data) + + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.post( + "/export-mold", + json={"task_id": "task-1", "scheme_id": "scheme_1", "formats": ["step"]}, + ) + + assert response.status_code == 409 + assert "导出缓存已失效" in response.json()["detail"] + + +def test_calculation_service_attaches_injection_system_summary(): + plan_result = { + "best_scheme_id": "scheme_1", + "candidate_schemes": [ + { + "scheme_id": "scheme_1", + "cavity_data": { + "product_analysis": { + "bounding_box": {"dimensions": [100, 80, 30]} + }, + "manufacturing_info": { + "estimated_mold_size": {"length": 200, "width": 180, "height": 110} + }, + "mold_cavities": {"cavity_count": 1}, + }, + "key_info": {}, + } + ], + } + + result = CalculationService.attach_injection_system_summaries(plan_result, "ABS") + best_scheme = result["candidate_schemes"][0] + + assert "injection_system" in best_scheme["cavity_data"] + assert best_scheme["cavity_data"]["manufacturing_info"]["cooling_summary"]["channel_count"] >= 1 + assert best_scheme["cavity_data"]["manufacturing_info"]["gating_summary"]["gate_type"] in {"auto", "side", "center", "submarine", "fan"} + assert "injection_system" in result + + +@pytest.mark.asyncio +async def test_upload_route_persists_process_parameters(monkeypatch, tmp_path): + captured = {} + + class DummyStorageService: + async def save_stp_file(self, session, file_path, original_filename, user_id): + captured["saved_file"] = { + "file_path": str(file_path), + "original_filename": original_filename, + "user_id": user_id, + } + return SimpleNamespace(id=42) + + async def create_processing_task(self, session, task_id, stp_file_id, task_type="stp_parsing", parameters=None): + captured["task"] = { + "task_id": task_id, + "stp_file_id": stp_file_id, + "task_type": task_type, + "parameters": parameters, + } + + async def fake_save_uploaded_file(file): + target = Path(tmp_path) / "cached_demo.step" + target.write_bytes(VALID_STEP_BYTES) + return target, len(VALID_STEP_BYTES), { + "safe_original_name": "demo.step", + "sha256": "a" * 64, + "original_filename": "demo.step", + "stored_filename": target.name, + } + + async def fake_set_task(task_id, task_info): + captured["redis"] = {"task_id": task_id, "task_info": task_info} + + async def fake_process_file_with_storage(task_id, file_path, stp_file_id, process_params): + captured["background"] = { + "task_id": task_id, + "file_path": str(file_path), + "stp_file_id": stp_file_id, + "process_params": process_params, + } + + fake_processing_service_module = types.ModuleType("services.processing_service") + fake_processing_service_module.processing_service = SimpleNamespace( + process_file_with_storage=fake_process_file_with_storage, + ) + + fake_storage_module = types.ModuleType("services.storage_integration_rustfs") + fake_storage_module.StorageIntegrationService = lambda: DummyStorageService() + + fake_redis_task_manager = types.ModuleType("services.redis_task_manager") + fake_redis_task_manager.redis_task_manager = SimpleNamespace( + set_task=fake_set_task, + ) + + fake_database_module = types.ModuleType("database.database") + async def override_get_db_session(): + yield object() + fake_database_module.get_db_session = override_get_db_session + + fake_auth_service = types.ModuleType("services.auth_service") + async def override_get_current_user(): + return SimpleNamespace(id=7, username="tester") + fake_auth_service.get_current_active_user = override_get_current_user + + fake_models_database = types.ModuleType("models.database") + fake_models_database.User = SimpleNamespace + + upload_router = _load_module_from_path( + "temp_upload_router", + "d:\\Project\\geMoldInsight\\src\\api\\v1\\upload_router.py", + { + "services.processing_service": fake_processing_service_module, + "services.storage_integration_rustfs": fake_storage_module, + "services.redis_task_manager": fake_redis_task_manager, + "database.database": fake_database_module, + "services.auth_service": fake_auth_service, + "models.database": fake_models_database, + }, + ) + + monkeypatch.setattr(upload_router.file_handler, "save_uploaded_file", fake_save_uploaded_file) + + app = FastAPI() + app.include_router(upload_router.router) + app.dependency_overrides[upload_router.get_db_session] = override_get_db_session + app.dependency_overrides[upload_router.get_current_active_user] = override_get_current_user + + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.post( + "/upload", + files={"file": ("demo.step", VALID_STEP_BYTES, "application/step")}, + data={ + "material": "ABS", + "draft_angle": "3.5", + "shrinkage_rate": "0.8", + "parting_precision": "0.05", + "cavity_match": "96", + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["parameters"] == { + "material": "ABS", + "draft_angle": 3.5, + "shrinkage_rate": 0.8, + "parting_precision": 0.05, + "cavity_match": 96, + } + assert captured["task"]["parameters"] == payload["parameters"] + assert captured["redis"]["task_info"]["parameters"] == payload["parameters"] + assert captured["background"]["process_params"] == payload["parameters"]