Files
姚章浩 693cd952cd feat(asset): 清理被截断的结尾图片引用,修复整单 502
仓库首次入库(此前仅存在于本地工作区与测试服构建产物中,未上传)。

改动(src/knowledge_adapter/asset_content_processor.py):
- 新增 _TRUNCATED_TRAILING_MARKDOWN_RESOURCE_IMAGE 规则,清除 chunk 末尾被截断的
  ![](resource://xxxx 残片(WeKnora 分块在图片引用中间截断时产生,旧逻辑无法匹配完整引用,
  残留 resource:// 触发 ContentProcessingError,导致整单 502)
- 回归测试 tests/test_asset_content_processor.py::
  test_truncated_trailing_markdown_resource_image_is_removed

验证证据:
- 测试服 120.76.217.51,镜像 knowledge-adapter-service:reviewed-test-aeab4b5fca2d
  (release aeab4b5fca2d8661):KB d015d05c-25c8-4d63-b2ab-e8590d3c2dfb 5 个查询全部
  http=200、records=5、残留 resource:// = 0;KB b9dacd62 无退化;容器日志无 error/exception
- 本地:pytest 141 项全通过、ruff 全通过
- 该文件与 258c31e0fe07fd08 源码的差异经逐文件 md5 比对,为唯一变更文件
2026-09-16 10:01:27 +08:00

151 lines
4.6 KiB
Python

from typing import ClassVar
from urllib.parse import parse_qs, unquote, urlparse
import pytest
from alibabacloud_credentials.models import Config, CredentialModel
from pydantic import SecretStr
import knowledge_adapter.oss_signer as oss_signer_module
from knowledge_adapter.asset_redirect import AssetSignerError
from knowledge_adapter.oss_signer import (
AlibabaOssV4Signer,
create_ecs_ram_role_credential_client,
)
class RecordingCredentialClient:
def __init__(self) -> None:
self.calls = 0
def get_credential(self) -> CredentialModel:
self.calls += 1
return CredentialModel(
access_key_id="test-access-key-id",
access_key_secret="test-access-key-secret",
security_token="test-security-token",
type="ecs_ram_role",
)
async def test_oss_signer_generates_v4_temporary_get_url() -> None:
credential_client = RecordingCredentialClient()
signer = AlibabaOssV4Signer(
credential_client=credential_client,
region="cn-shenzhen",
bucket="private-assets",
)
signed_url = await signer.sign_get(
object_key="weknora/10008/assets/image.png",
expires_seconds=600,
)
parsed = urlparse(signed_url)
query = parse_qs(parsed.query)
assert parsed.scheme == "https"
assert parsed.hostname == "private-assets.oss-cn-shenzhen.aliyuncs.com"
assert unquote(parsed.path) == "/weknora/10008/assets/image.png"
assert query["x-oss-signature-version"] == ["OSS4-HMAC-SHA256"]
assert query["x-oss-expires"] == ["600"]
assert query["x-oss-security-token"] == ["test-security-token"]
assert credential_client.calls == 1
@pytest.mark.parametrize(
("object_key", "expires_seconds"),
[
("../other-tenant/secret.png", 600),
("weknora/10008/assets/image.png", 601),
("weknora/10008/assets/image.png", 60),
],
)
async def test_oss_signer_rejects_unsafe_key_or_ttl_before_credentials(
object_key: str,
expires_seconds: int,
) -> None:
credential_client = RecordingCredentialClient()
signer = AlibabaOssV4Signer(
credential_client=credential_client,
region="cn-shenzhen",
bucket="private-assets",
)
with pytest.raises(AssetSignerError, match=r"^asset signing failed$"):
await signer.sign_get(
object_key=object_key,
expires_seconds=expires_seconds,
)
assert credential_client.calls == 0
class UnsafeBucket:
def sign_url(
self,
method: str,
object_key: str,
expires_seconds: int,
) -> str:
return "http://127.0.0.1/internal?x-oss-signature-version=OSS4-HMAC-SHA256"
async def test_oss_signer_rejects_untrusted_signed_url() -> None:
signer = AlibabaOssV4Signer(
credential_client=RecordingCredentialClient(),
region="cn-shenzhen",
bucket="private-assets",
)
signer._bucket = UnsafeBucket()
with pytest.raises(AssetSignerError, match=r"^asset signing failed$"):
await signer.sign_get(
object_key="weknora/10008/assets/image.png",
expires_seconds=600,
)
class RecordingCredentialsClientFactory:
configs: ClassVar[list[Config]] = []
def __init__(self, config: Config) -> None:
self.configs.append(config)
async def test_ecs_ram_role_client_uses_imdsv2_without_static_keys(
monkeypatch: pytest.MonkeyPatch,
) -> None:
RecordingCredentialsClientFactory.configs = []
monkeypatch.setattr(
oss_signer_module,
"CredentialsClient",
RecordingCredentialsClientFactory,
)
create_ecs_ram_role_credential_client("knowledge-adapter-role")
assert len(RecordingCredentialsClientFactory.configs) == 1
config = RecordingCredentialsClientFactory.configs[0]
assert config.type == "ecs_ram_role"
assert config.role_name == "knowledge-adapter-role"
assert config.enable_imds_v2 is True
assert config.disable_imds_v1 is True
assert config.connect_timeout == 1000
assert config.timeout == 1000
assert not config.access_key_id
assert not config.access_key_secret
def test_static_ak_client_uses_secret_values_without_security_token() -> None:
client = oss_signer_module.create_static_ak_credential_client(
SecretStr("test-access-key-id"),
SecretStr("test-access-key-secret"),
)
credential = client.get_credential()
assert credential.access_key_id == "test-access-key-id"
assert credential.access_key_secret == "test-access-key-secret"
assert not credential.security_token
assert credential.type == "access_key"
assert "test-access-key-secret" not in repr(client)