Files
knowledge-adapter-service/deploy/nginx/nginx_adapter_helper.py
T
姚章浩 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

150 lines
5.3 KiB
Python

#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import urllib.error
import urllib.request
import uuid
ENV_PATH = pathlib.Path("/opt/knowledge-adapter/adapter.env")
NEW_BASE = "https://weknora-api-test.dobot.cn"
API_HOST = "weknora-api-test.dobot.cn"
UI_HOST = "weknora-test.dobot.cn"
DIFY_KEY_PATH = pathlib.Path("/opt/knowledge-adapter/secrets/dify_test_key")
def parse_env(path: pathlib.Path) -> dict[str, str]:
result: dict[str, str] = {}
for raw in path.read_text().splitlines():
line = raw.strip()
if line and not line.startswith("#") and "=" in line:
key, value = line.split("=", 1)
result[key] = value
return result
def update_env() -> None:
st = ENV_PATH.stat()
lines = ENV_PATH.read_text().splitlines(keepends=True)
found = 0
output: list[str] = []
for line in lines:
if line.startswith("ASSET_PUBLIC_BASE_URL="):
output.append(f"ASSET_PUBLIC_BASE_URL={NEW_BASE}\n")
found += 1
else:
output.append(line)
if found != 1:
raise RuntimeError(f"expected exactly one ASSET_PUBLIC_BASE_URL, got {found}")
fd, tmp_name = tempfile.mkstemp(prefix=".adapter.env.", dir=str(ENV_PATH.parent))
try:
with os.fdopen(fd, "w") as handle:
handle.writelines(output)
handle.flush()
os.fsync(handle.fileno())
os.chmod(tmp_name, st.st_mode & 0o777)
os.chown(tmp_name, st.st_uid, st.st_gid)
os.replace(tmp_name, ENV_PATH)
finally:
if os.path.exists(tmp_name):
os.unlink(tmp_name)
if parse_env(ENV_PATH).get("ASSET_PUBLIC_BASE_URL") != NEW_BASE:
raise RuntimeError("ASSET_PUBLIC_BASE_URL readback mismatch")
print("adapter_env_update=ok")
def request(method: str, path: str, host: str, *, data: bytes | None = None, headers: dict[str, str] | None = None) -> tuple[int, dict[str, str], bytes]:
merged = {"Host": host, "Accept": "application/json"}
if headers:
merged.update(headers)
req = urllib.request.Request(
f"http://127.0.0.1{path}", data=data, method=method, headers=merged
)
try:
with urllib.request.urlopen(req, timeout=25) as response:
return response.status, dict(response.headers.items()), response.read(2_000_000)
except urllib.error.HTTPError as exc:
return exc.code, dict(exc.headers.items()), exc.read(2_000_000)
def accept() -> None:
env = parse_env(ENV_PATH)
allowed = [x.strip() for x in env["WEKNORA_ALLOWED_KB_IDS"].split(",") if x.strip()]
if len(allowed) != 1:
raise RuntimeError("acceptance expects exactly one allowlisted KB")
raw_key = DIFY_KEY_PATH.read_text().strip()
if not raw_key:
raise RuntimeError("Dify key file is empty")
configured_hash = env["DIFY_KEY_HASH_CURRENT"].lower()
if hashlib.sha256(raw_key.encode()).hexdigest() != configured_hash:
raise RuntimeError("Dify key file does not match configured hash")
status, _, _ = request("GET", "/", UI_HOST)
if status != 200:
raise RuntimeError(f"UI Host route expected 200, got {status}")
print("ui_host_route=200")
status, _, _ = request("GET", "/", API_HOST)
if status != 404:
raise RuntimeError(f"API root expected 404, got {status}")
print("api_root=404")
status, _, body = request(
"POST", "/retrieval", API_HOST, data=b"{}", headers={"Content-Type": "application/json"}
)
if status != 401 or json.loads(body) != {"detail": "invalid API key"}:
raise RuntimeError("missing-auth boundary mismatch")
print("retrieval_missing_key=401_adapter")
payload = json.dumps(
{
"knowledge_id": allowed[0],
"query": "越疆机器人技术资料",
"retrieval_setting": {"top_k": 3, "score_threshold": 0.0},
}
).encode()
status, _, body = request(
"POST",
"/retrieval",
API_HOST,
data=payload,
headers={"Content-Type": "application/json", "Authorization": f"Bearer {raw_key}"},
)
if status != 200:
raise RuntimeError(f"allowlisted retrieval expected 200, got {status}")
result = json.loads(body)
records = result.get("records")
if not isinstance(records, list):
raise RuntimeError("retrieval response has no records list")
print(f"retrieval_allowed=200 records={len(records)}")
denied_payload = json.dumps(
{
"knowledge_id": str(uuid.UUID("00000000-0000-4000-8000-000000000001")),
"query": "boundary probe",
"retrieval_setting": {"top_k": 1, "score_threshold": 0.0},
}
).encode()
status, _, body = request(
"POST",
"/retrieval",
API_HOST,
data=denied_payload,
headers={"Content-Type": "application/json", "Authorization": f"Bearer {raw_key}"},
)
if status != 400 or json.loads(body) != {"detail": "invalid request"}:
raise RuntimeError("denied-KB boundary mismatch")
print("retrieval_denied_kb=400")
if __name__ == "__main__":
if len(sys.argv) != 2 or sys.argv[1] not in {"update-env", "accept"}:
raise SystemExit("usage: helper.py update-env|accept")
if sys.argv[1] == "update-env":
update_env()
else:
accept()