| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- """Deterministic payload assembly for decoded creation knowledge."""
- from __future__ import annotations
- import json
- from typing import Any
- from decode_content.contracts import DIM_ATTRIBUTE_BY_TYPE
- TYPE2ATTR = DIM_ATTRIBUTE_BY_TYPE
- CUSTOM_EXT_TYPES = {"str", "num", "date"}
- DIM_ATTRIBUTES = {"how", "what", "why"}
- class IngestPayloadValidationError(ValueError):
- """Raised when a generated payload cannot satisfy the ingest API contract."""
- def _text(value: Any) -> str:
- return value if isinstance(value, str) else ""
- def _post_value(post: Any, key: str, default: Any = "") -> Any:
- if isinstance(post, dict):
- return post.get(key, default)
- return getattr(post, key, default)
- def build_content(knowledge: dict[str, Any]) -> str | dict[str, Any]:
- t = knowledge.get("type")
- if t == "how":
- lines = [f"目标:{knowledge.get('purpose', '')}"]
- for idx, step in enumerate(knowledge.get("steps", []), 1):
- intent = step.get("intent") or step.get("purpose") or ""
- label = f"步骤{idx}" + (f"(目的:{intent})" if intent else "")
- lines.extend(
- [
- label,
- f" 输入:{step.get('input', '')}",
- f" 指引:{step.get('directive', '')}",
- f" 产出:{step.get('output', '')}",
- ]
- )
- return "\n".join(lines)
- if t == "what":
- return {
- "name": knowledge.get("title") or "",
- "kind": knowledge.get("kind"),
- "维度拆分规则": knowledge.get("维度拆分规则") if knowledge.get("kind") == "子集" else None,
- "body": [
- {
- "item_name": item.get("item_name", ""),
- "item_desc": item.get("item_desc", ""),
- "作用域": item.get("作用域") or [],
- }
- for item in (knowledge.get("body") or [])
- ],
- }
- return knowledge.get("阐述") or knowledge.get("desc") or knowledge.get("explanation") or ""
- def _payload_content(knowledge: dict[str, Any]) -> str:
- content = build_content(knowledge)
- return content if isinstance(content, str) else json.dumps(content, ensure_ascii=False)
- def validate_ingest_payload(payload: dict[str, Any]) -> None:
- """Validate the minimal external ingest API contract before storing/sending."""
- errors: list[str] = []
- source = payload.get("source")
- if not isinstance(source, dict):
- source = {}
- errors.append("source 必须是对象")
- source_id = _text(source.get("id")).strip()
- if not source_id:
- errors.append("source.id 不能为空")
- elif len(source_id) > 64:
- errors.append("source.id 最长 64 字符")
- source_type = _text(source.get("source_type"))
- if len(source_type) > 32:
- errors.append("source.source_type 最长 32 字符")
- source_title = _text(source.get("title"))
- if len(source_title) > 512:
- errors.append("source.title 最长 512 字符")
- source_author = _text(source.get("author"))
- if len(source_author) > 128:
- errors.append("source.author 最长 128 字符")
- title = payload.get("title")
- if title is not None and len(_text(title)) > 512:
- errors.append("title 最长 512 字符")
- content = payload.get("content")
- if not isinstance(content, str) or not content.strip():
- errors.append("content 必须是非空字符串")
- dim_attributes = payload.get("dim_attributes")
- if not isinstance(dim_attributes, list) or len(dim_attributes) != 1:
- errors.append("dim_attributes 必须恰好包含一个值")
- elif dim_attributes[0] not in DIM_ATTRIBUTES:
- errors.append("dim_attributes 只能是 how/what/why")
- for idx, scope in enumerate(payload.get("scopes") or [], 1):
- if not isinstance(scope, dict):
- errors.append(f"scopes[{idx}] 必须是对象")
- continue
- value = _text(scope.get("value"))
- if len(value) > 128:
- errors.append(f"scopes[{idx}].value 最长 128 字符")
- for idx, ext in enumerate(payload.get("custom_ext") or [], 1):
- if not isinstance(ext, dict):
- errors.append(f"custom_ext[{idx}] 必须是对象")
- continue
- ext_type = ext.get("type")
- if ext_type not in CUSTOM_EXT_TYPES:
- errors.append(f"custom_ext[{idx}].type 必须是 str/num/date")
- if errors:
- title_hint = _text(payload.get("title")) or source_id or "unknown"
- raise IngestPayloadValidationError(f"{title_hint}: {'; '.join(errors)}")
- def _add_scopes(scopes: list[dict[str, str]], seen: set[tuple[str, str]], items: list[dict[str, Any]]) -> None:
- for item in items:
- scope_type = item.get("scope_type")
- value = item.get("value")
- if not (scope_type and value):
- continue
- key = (scope_type, value)
- if key in seen:
- continue
- seen.add(key)
- scopes.append({"scope_type": scope_type, "value": value})
- def collect_payload_scopes(knowledge: dict[str, Any]) -> list[dict[str, str]]:
- scopes: list[dict[str, str]] = []
- seen: set[tuple[str, str]] = set()
- if knowledge.get("type") == "how":
- for step in knowledge.get("steps", []):
- _add_scopes(scopes, seen, step.get("作用域", []))
- else:
- _add_scopes(scopes, seen, knowledge.get("作用域", []))
- return scopes
- def _source_metadata(post: Any) -> dict[str, Any]:
- metadata = {
- "platform": _post_value(post, "platform"),
- "url": _post_value(post, "url"),
- "unique_key": _post_value(post, "unique_key"),
- "platform_item_id": _post_value(post, "content_id"),
- "content_type": _post_value(post, "content_type"),
- "content_mode": _post_value(post, "content_mode"),
- "media_count": _post_value(post, "media_count", 0),
- "cover_url": _post_value(post, "cover_url"),
- }
- return {
- key: value
- for key, value in metadata.items()
- if value is not None and value != ""
- }
- def build_payload(post: Any, knowledge: dict[str, Any], how_titles: dict[str, str] | None = None) -> dict[str, Any]:
- how_titles = how_titles or {}
- t = knowledge.get("type")
- if t not in TYPE2ATTR:
- raise IngestPayloadValidationError(f"{knowledge.get('title') or 'unknown'}: unknown knowledge type {t}")
- ext = [
- {"key": "业务阶段", "type": "str", "value": stage}
- for stage in (knowledge.get("业务阶段") or [])
- ]
- if t == "what" and knowledge.get("概要"):
- ext.append({"key": "概要", "type": "str", "value": knowledge["概要"]})
- if t == "how":
- creation_stages: list[str] = []
- seen_stages: set[str] = set()
- for step in knowledge.get("steps", []):
- stage = step.get("创作阶段")
- if stage and stage not in seen_stages:
- seen_stages.add(stage)
- creation_stages.append(stage)
- ext.extend({"key": "创作阶段", "type": "str", "value": stage} for stage in creation_stages)
- ext.extend(
- {"key": "动作", "type": "str", "value": step["动作"]}
- for step in knowledge.get("steps", [])
- if step.get("动作")
- )
- if knowledge.get("role") == "组件" and knowledge.get("parent"):
- parent = knowledge["parent"]
- ext.append(
- {
- "key": "出自",
- "type": "str",
- "value": f"{how_titles.get(parent.get('how_id'), parent.get('how_id'))} 第{parent.get('step')}步",
- }
- )
- payload = {
- "source": {
- "id": _post_value(post, "id"),
- "source_type": "post",
- "title": _post_value(post, "title") or "",
- "author": _post_value(post, "author_name") or "",
- "source_metadata": _source_metadata(post),
- },
- "title": knowledge.get("title"),
- "content": _payload_content(knowledge),
- "dim_creations": ["创作"],
- "dim_attributes": [TYPE2ATTR[t]],
- "scopes": collect_payload_scopes(knowledge),
- "custom_ext": ext,
- }
- validate_ingest_payload(payload)
- return payload
- def build_payloads(post: Any, knowledges: list[dict[str, Any]]) -> list[dict[str, Any]]:
- how_titles = {k.get("id"): k.get("title") for k in knowledges if k.get("type") == "how"}
- return [build_payload(post, knowledge, how_titles) for knowledge in knowledges]
|