fix(mcp): preserve structured file asset identifiers
Preserve curated source-registry identifiers when rebuilding the file asset index so part-number queries such as 101049 can match drawings even when the filename omits the part number. Raise ranking for structured identifier matches above incidental substrings like timestamps, expose safe part-number fields through the MCP result payload, and allow customer-shareable engineering deliverables in the source-file MCP profile path while keeping blocked doc types enforced. Validation: python3 -m unittest discover -v; python3 -m py_compile obsidian_wiki/file_assets.py obsidian_wiki/mcp_assets.py tests/test_file_assets.py tests/test_mcp_assets.py; git diff --check; rebuilt the configured bjd vault file index; verified HTTP MCP search_file_assets and asset resource read for 101049 图纸.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from obsidian_wiki.file_assets import rebuild_from_registry, search_assets
|
||||
|
||||
|
||||
class FileAssetIndexTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tempdir = tempfile.TemporaryDirectory()
|
||||
self.vault = Path(self.tempdir.name)
|
||||
(self.vault / "_meta").mkdir()
|
||||
self.registry_path = self.vault / "_meta" / "source-registry.jsonl"
|
||||
self.db_path = self.vault / "_meta" / "file-assets.sqlite"
|
||||
|
||||
def tearDown(self):
|
||||
self.tempdir.cleanup()
|
||||
|
||||
def test_rebuild_preserves_registry_identifiers_for_part_number_search(self):
|
||||
drawing_id = "sha256:" + "a" * 64
|
||||
image_id = "sha256:" + "b" * 64
|
||||
self._write_registry_record(
|
||||
source_id=drawing_id,
|
||||
archive_path="_sources/archive/bjd/pdf/a.pdf",
|
||||
original_name="北极大-0.5-18客户图.pdf",
|
||||
title="北极大 FPC 0.5-18 客户图纸",
|
||||
summary="FPC 0.5-18 客户图 PDF 归档。",
|
||||
doc_type="drawing",
|
||||
source_type="pdf",
|
||||
bjd_part_numbers=["101049-X0XXX0", "101049-20XXX0"],
|
||||
series=["0.5-18"],
|
||||
pitch=["0.5"],
|
||||
pin_count=["18"],
|
||||
)
|
||||
self._write_registry_record(
|
||||
source_id=image_id,
|
||||
archive_path="_sources/archive/bjd/quality/photo.jpg",
|
||||
original_name="微信图片_20250714101049.jpg",
|
||||
title="微信图片_20250714101049",
|
||||
summary="客诉图片。",
|
||||
doc_type="evidence_media",
|
||||
source_type="image",
|
||||
business_categories=["quality/customer-complaints"],
|
||||
)
|
||||
|
||||
rebuild_from_registry(self.vault, db_path=self.db_path)
|
||||
|
||||
rows = search_assets(self.vault, "101049 图纸", db_path=self.db_path, limit=5, read_only=True)
|
||||
|
||||
self.assertGreaterEqual(len(rows), 2)
|
||||
self.assertEqual(rows[0]["file_id"], drawing_id)
|
||||
self.assertEqual(json.loads(rows[0]["bjd_part_numbers"]), ["101049-X0XXX0", "101049-20XXX0"])
|
||||
self.assertEqual(json.loads(rows[0]["series"]), ["0.5-18"])
|
||||
self.assertEqual(json.loads(rows[0]["pitch"]), ["0.5"])
|
||||
self.assertEqual(json.loads(rows[0]["pin_count"]), ["18"])
|
||||
|
||||
def _write_registry_record(self, **fields):
|
||||
record = {
|
||||
"record_type": "source_archive",
|
||||
"project": "bjd",
|
||||
"visibility": "internal",
|
||||
"business_categories": ["products/fpc"],
|
||||
"product_lines": ["FPC", "FFC"],
|
||||
"customers": [],
|
||||
"competitors": [],
|
||||
"standards": [],
|
||||
"market_tags": [],
|
||||
"related_pages": ["projects/bjd/references/example.md"],
|
||||
"catalog_paths": ["_sources/catalog/bjd/products/fpc.md"],
|
||||
"classification_confidence": 0.9,
|
||||
"size_bytes": 1024,
|
||||
**fields,
|
||||
}
|
||||
with self.registry_path.open("a", encoding="utf-8") as fh:
|
||||
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -42,6 +42,10 @@ class McpAssetTests(unittest.TestCase):
|
||||
data_classification="technical-confidential",
|
||||
visibility="internal",
|
||||
summary="B3501 customer drawing.",
|
||||
bjd_part_numbers=["101049-X0XXX0"],
|
||||
series=["0.5-18"],
|
||||
pitch=["0.5"],
|
||||
pin_count=["18"],
|
||||
)
|
||||
self._insert_asset(
|
||||
self.order_id,
|
||||
@@ -106,11 +110,11 @@ class McpAssetTests(unittest.TestCase):
|
||||
"competitors": "[]",
|
||||
"standards": "[]",
|
||||
"market_tags": "[]",
|
||||
"bjd_part_numbers": "[]",
|
||||
"competitor_part_numbers": "[]",
|
||||
"series": "[]",
|
||||
"pitch": "[]",
|
||||
"pin_count": "[]",
|
||||
"bjd_part_numbers": json.dumps(fields.get("bjd_part_numbers", []), ensure_ascii=False),
|
||||
"competitor_part_numbers": json.dumps(fields.get("competitor_part_numbers", []), ensure_ascii=False),
|
||||
"series": json.dumps(fields.get("series", []), ensure_ascii=False),
|
||||
"pitch": json.dumps(fields.get("pitch", []), ensure_ascii=False),
|
||||
"pin_count": json.dumps(fields.get("pin_count", []), ensure_ascii=False),
|
||||
"related_pages": json.dumps(record["related_pages"], ensure_ascii=False),
|
||||
"catalog_paths": "[]",
|
||||
"extracted_text": None,
|
||||
@@ -124,6 +128,25 @@ class McpAssetTests(unittest.TestCase):
|
||||
with connect(self.db_path) as conn:
|
||||
upsert_asset(conn, asset, status="active")
|
||||
|
||||
def _write_profile_file(self):
|
||||
data = {
|
||||
"version": 2,
|
||||
"profiles": {
|
||||
"bjd-general-bot": {
|
||||
"allowed_visibility": ["public", "internal"],
|
||||
"allowed_data_classification": [
|
||||
"public",
|
||||
"internal",
|
||||
"technical-confidential",
|
||||
],
|
||||
"allowed_business_domains": ["products", "engineering"],
|
||||
"blocked_doc_types": ["order_data", "evidence_media"],
|
||||
"include_source_files": False,
|
||||
}
|
||||
},
|
||||
}
|
||||
(self.vault / "_meta" / "bot-profiles.json").write_text(json.dumps(data), encoding="utf-8")
|
||||
|
||||
def test_search_returns_allowed_engineering_assets_only(self):
|
||||
rows = search_file_assets(self.vault, "B3501 drawing", db_path=self.db_path)
|
||||
|
||||
@@ -131,6 +154,38 @@ class McpAssetTests(unittest.TestCase):
|
||||
self.assertEqual(rows[0]["resource_uri"], asset_uri_for_file_id(self.drawing_id))
|
||||
self.assertNotIn("absolute_path", rows[0])
|
||||
|
||||
def test_search_exposes_safe_identifier_fields(self):
|
||||
rows = search_file_assets(self.vault, "101049 图纸", db_path=self.db_path)
|
||||
|
||||
self.assertEqual([row["file_id"] for row in rows], [self.drawing_id])
|
||||
self.assertEqual(rows[0]["bjd_part_numbers"], ["101049-X0XXX0"])
|
||||
self.assertEqual(rows[0]["series"], ["0.5-18"])
|
||||
self.assertEqual(rows[0]["pitch"], ["0.5"])
|
||||
self.assertEqual(rows[0]["pin_count"], ["18"])
|
||||
|
||||
def test_customer_shareable_engineering_files_work_with_source_file_profile(self):
|
||||
self._write_profile_file()
|
||||
shareable_id = "sha256:" + "d" * 64
|
||||
self._write_asset_file("drawings/customer-shareable.pdf", b"%PDF customer drawing")
|
||||
self._insert_asset(
|
||||
shareable_id,
|
||||
archive_path="_sources/archive/drawings/customer-shareable.pdf",
|
||||
original_name="北极大-0.5-18客户图.pdf",
|
||||
doc_type="drawing",
|
||||
source_type="pdf",
|
||||
product_lines=["FPC"],
|
||||
business_categories=["products/fpc", "drawings/customer-drawings"],
|
||||
data_classification="customer-shareable",
|
||||
visibility="public",
|
||||
summary="FPC 0.5-18 customer-shareable drawing.",
|
||||
bjd_part_numbers=["101049-X0XXX0"],
|
||||
series=["0.5-18"],
|
||||
)
|
||||
|
||||
rows = search_file_assets(self.vault, "101049 图纸", db_path=self.db_path, profile_name="bjd-general-bot")
|
||||
|
||||
self.assertEqual({row["file_id"] for row in rows}, {shareable_id, self.drawing_id})
|
||||
|
||||
def test_search_blocks_sensitive_order_and_complaint_sources(self):
|
||||
self.assertEqual(search_file_assets(self.vault, "订单", db_path=self.db_path), [])
|
||||
self.assertEqual(search_file_assets(self.vault, "来料标签错误", db_path=self.db_path), [])
|
||||
|
||||
Reference in New Issue
Block a user