feat: 实现用户画像、占卜历史与后端用户管理模块
This commit is contained in:
@@ -135,6 +135,7 @@ class SqlAlchemyEventStore:
|
||||
"result_type",
|
||||
"suggested_actions",
|
||||
"error",
|
||||
"divination_derived",
|
||||
"ui_hints",
|
||||
)
|
||||
worker_output_payload: dict[str, object] = {}
|
||||
@@ -187,7 +188,9 @@ class SqlAlchemyEventStore:
|
||||
content=content,
|
||||
model_code=model_code if isinstance(model_code, str) else None,
|
||||
tool_name=tool_name_value,
|
||||
metadata=metadata_model.model_dump(mode="json", exclude_none=True),
|
||||
metadata=metadata_model.model_dump(
|
||||
mode="json", by_alias=True, exclude_none=True
|
||||
),
|
||||
input_tokens=input_tokens,
|
||||
output_tokens=output_tokens,
|
||||
cost=cost,
|
||||
@@ -200,7 +203,9 @@ class SqlAlchemyEventStore:
|
||||
visibility_mask=visibility_mask,
|
||||
role=role.value,
|
||||
content=content,
|
||||
metadata=metadata_model.model_dump(mode="json", exclude_none=True),
|
||||
metadata=metadata_model.model_dump(
|
||||
mode="json", by_alias=True, exclude_none=True
|
||||
),
|
||||
timestamp=self._resolve_message_timestamp(persisted),
|
||||
)
|
||||
|
||||
@@ -272,7 +277,9 @@ class SqlAlchemyEventStore:
|
||||
role=AgentChatMessageRole.TOOL,
|
||||
content=content,
|
||||
tool_name=tool_output.tool_name,
|
||||
metadata=metadata_model.model_dump(mode="json", exclude_none=True),
|
||||
metadata=metadata_model.model_dump(
|
||||
mode="json", by_alias=True, exclude_none=True
|
||||
),
|
||||
visibility_mask=visibility_mask,
|
||||
)
|
||||
await self._append_context_cache_message(
|
||||
@@ -281,7 +288,9 @@ class SqlAlchemyEventStore:
|
||||
visibility_mask=visibility_mask,
|
||||
role=AgentChatMessageRole.TOOL.value,
|
||||
content=content,
|
||||
metadata=metadata_model.model_dump(mode="json", exclude_none=True),
|
||||
metadata=metadata_model.model_dump(
|
||||
mode="json", by_alias=True, exclude_none=True
|
||||
),
|
||||
timestamp=self._resolve_message_timestamp(persisted),
|
||||
)
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ def _worker_rules(llm_config: SystemAgentLLMConfig | None) -> list[str]:
|
||||
"[六爻分析流程]",
|
||||
"- 第1步:准确复述用户问题,确认问题类型与诉求焦点。",
|
||||
"- 第2步:围绕用神、世应、动爻、月建日辰、旺衰关系形成核心判断。",
|
||||
"- 第3步:给出签级,仅允许 上上签 / 中上签 / 中下签。",
|
||||
"- 第3步:给出签级,仅允许 上上签 / 中上签 / 中下签 / 下下签。",
|
||||
"- 第4步:输出结论与重点,解释外部阻力或有利转机出现条件。",
|
||||
"- 第5步:给出可执行建议,避免空泛正确话。",
|
||||
"- 第6步:提炼关键词,优先四字表达,简洁且可复述。",
|
||||
|
||||
@@ -232,8 +232,10 @@ class AgentScopeRunner:
|
||||
pipeline=pipeline,
|
||||
runtime_client_time=runtime_client_time,
|
||||
runtime_mode=runtime_mode,
|
||||
derived_divination=derived_divination,
|
||||
)
|
||||
worker_output = worker_output_model.model_validate(worker_result.payload)
|
||||
worker_output.divination_derived = derived_divination
|
||||
await self._emit_step_event(
|
||||
pipeline=pipeline,
|
||||
run_input=run_input,
|
||||
@@ -255,6 +257,7 @@ class AgentScopeRunner:
|
||||
pipeline: PipelineLike,
|
||||
runtime_client_time: ClientTimeContext | None,
|
||||
runtime_mode: RuntimeMode,
|
||||
derived_divination: DerivedDivinationData,
|
||||
) -> StageExecutionResult:
|
||||
tracking_model = self._build_model(stage_config=stage_config)
|
||||
formatter = OpenAIChatFormatter()
|
||||
@@ -290,7 +293,12 @@ class AgentScopeRunner:
|
||||
usage_summary=tracking_model.usage_summary(),
|
||||
)
|
||||
await emitter.emit_final_text_end(
|
||||
worker_output=worker_payload.model_dump(mode="json", exclude_none=True),
|
||||
worker_output={
|
||||
**worker_payload.model_dump(mode="json", exclude_none=True),
|
||||
"divination_derived": derived_divination.model_dump(
|
||||
mode="json", by_alias=True, exclude_none=True
|
||||
),
|
||||
},
|
||||
response_metadata=response_metadata,
|
||||
)
|
||||
return StageExecutionResult(
|
||||
|
||||
@@ -70,6 +70,7 @@ class PipelineStageEmitter:
|
||||
"suggested_actions": worker_output.get("suggested_actions")
|
||||
or worker_output.get("advice", []),
|
||||
"error": worker_output.get("error"),
|
||||
"divination_derived": worker_output.get("divination_derived"),
|
||||
**response_metadata,
|
||||
}
|
||||
ui_hints = worker_output.get("ui_hints")
|
||||
|
||||
@@ -0,0 +1,962 @@
|
||||
{
|
||||
"111111": {
|
||||
"name": "乾为天",
|
||||
"binary": "111111",
|
||||
"upper_name": "乾",
|
||||
"lower_name": "乾",
|
||||
"yao_relations": ["子孙", "妻财", "父母", "官鬼", "兄弟", "父母"],
|
||||
"yao_tigan": ["子", "寅", "辰", "午", "申", "戌"],
|
||||
"yao_elements": ["水", "木", "土", "火", "金", "土"],
|
||||
"world_position": 6,
|
||||
"response_position": 3,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"011111": {
|
||||
"name": "天风姤",
|
||||
"binary": "011111",
|
||||
"upper_name": "乾",
|
||||
"lower_name": "巽",
|
||||
"yao_relations": ["父母", "子孙", "兄弟", "官鬼", "兄弟", "父母"],
|
||||
"yao_tigan": ["丑", "亥", "酉", "午", "申", "戌"],
|
||||
"yao_elements": ["土", "水", "金", "火", "金", "土"],
|
||||
"world_position": 1,
|
||||
"response_position": 4,
|
||||
"fushen_positions": [1],
|
||||
"fushen_relations": ["妻财"],
|
||||
"fushen_tigan": ["寅"],
|
||||
"fushen_elements": ["木"]
|
||||
},
|
||||
"001111": {
|
||||
"name": "天山遁",
|
||||
"binary": "001111",
|
||||
"upper_name": "乾",
|
||||
"lower_name": "艮",
|
||||
"yao_relations": ["父母", "官鬼", "兄弟", "官鬼", "兄弟", "父母"],
|
||||
"yao_tigan": ["辰", "午", "申", "午", "申", "戌"],
|
||||
"yao_elements": ["土", "火", "金", "火", "金", "土"],
|
||||
"world_position": 2,
|
||||
"response_position": 5,
|
||||
"fushen_positions": [0, 1],
|
||||
"fushen_relations": ["子孙", "妻财"],
|
||||
"fushen_tigan": ["子", "寅"],
|
||||
"fushen_elements": ["水", "木"]
|
||||
},
|
||||
"000111": {
|
||||
"name": "天地否",
|
||||
"binary": "000111",
|
||||
"upper_name": "乾",
|
||||
"lower_name": "坤",
|
||||
"yao_relations": ["父母", "官鬼", "妻财", "官鬼", "兄弟", "父母"],
|
||||
"yao_tigan": ["未", "巳", "卯", "午", "申", "戌"],
|
||||
"yao_elements": ["土", "火", "木", "火", "金", "土"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [0],
|
||||
"fushen_relations": ["子孙"],
|
||||
"fushen_tigan": ["子"],
|
||||
"fushen_elements": ["水"]
|
||||
},
|
||||
"000011": {
|
||||
"name": "风地观",
|
||||
"binary": "000011",
|
||||
"upper_name": "巽",
|
||||
"lower_name": "坤",
|
||||
"yao_relations": ["父母", "官鬼", "妻财", "父母", "官鬼", "妻财"],
|
||||
"yao_tigan": ["未", "巳", "卯", "未", "巳", "卯"],
|
||||
"yao_elements": ["土", "火", "木", "土", "火", "木"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [0, 4],
|
||||
"fushen_relations": ["子孙", "兄弟"],
|
||||
"fushen_tigan": ["子", "申"],
|
||||
"fushen_elements": ["水", "金"]
|
||||
},
|
||||
"000001": {
|
||||
"name": "山地剥",
|
||||
"binary": "000001",
|
||||
"upper_name": "艮",
|
||||
"lower_name": "坤",
|
||||
"yao_relations": ["父母", "官鬼", "妻财", "父母", "子孙", "妻财"],
|
||||
"yao_tigan": ["未", "巳", "卯", "戌", "子", "寅"],
|
||||
"yao_elements": ["土", "火", "木", "土", "水", "木"],
|
||||
"world_position": 5,
|
||||
"response_position": 2,
|
||||
"fushen_positions": [4],
|
||||
"fushen_relations": ["兄弟"],
|
||||
"fushen_tigan": ["申"],
|
||||
"fushen_elements": ["金"]
|
||||
},
|
||||
"000101": {
|
||||
"name": "火地晋",
|
||||
"binary": "000101",
|
||||
"upper_name": "离",
|
||||
"lower_name": "坤",
|
||||
"yao_relations": ["父母", "官鬼", "妻财", "兄弟", "父母", "官鬼"],
|
||||
"yao_tigan": ["未", "巳", "卯", "酉", "未", "巳"],
|
||||
"yao_elements": ["土", "火", "木", "金", "土", "火"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [0],
|
||||
"fushen_relations": ["子孙"],
|
||||
"fushen_tigan": ["子"],
|
||||
"fushen_elements": ["水"]
|
||||
},
|
||||
"111101": {
|
||||
"name": "火天大有",
|
||||
"binary": "111101",
|
||||
"upper_name": "离",
|
||||
"lower_name": "乾",
|
||||
"yao_relations": ["子孙", "妻财", "父母", "兄弟", "父母", "官鬼"],
|
||||
"yao_tigan": ["子", "寅", "辰", "酉", "未", "巳"],
|
||||
"yao_elements": ["水", "木", "土", "金", "土", "火"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"010010": {
|
||||
"name": "坎为水",
|
||||
"binary": "010010",
|
||||
"upper_name": "坎",
|
||||
"lower_name": "坎",
|
||||
"yao_relations": ["子孙", "官鬼", "妻财", "父母", "官鬼", "兄弟"],
|
||||
"yao_tigan": ["寅", "辰", "午", "申", "戌", "子"],
|
||||
"yao_elements": ["木", "土", "火", "金", "土", "水"],
|
||||
"world_position": 6,
|
||||
"response_position": 3,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"110010": {
|
||||
"name": "水泽节",
|
||||
"binary": "110010",
|
||||
"upper_name": "坎",
|
||||
"lower_name": "兑",
|
||||
"yao_relations": ["妻财", "子孙", "官鬼", "父母", "官鬼", "兄弟"],
|
||||
"yao_tigan": ["巳", "卯", "丑", "申", "戌", "子"],
|
||||
"yao_elements": ["火", "木", "土", "金", "土", "水"],
|
||||
"world_position": 1,
|
||||
"response_position": 4,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"100010": {
|
||||
"name": "水雷屯",
|
||||
"binary": "100010",
|
||||
"upper_name": "坎",
|
||||
"lower_name": "震",
|
||||
"yao_relations": ["兄弟", "子孙", "官鬼", "父母", "官鬼", "兄弟"],
|
||||
"yao_tigan": ["子", "寅", "辰", "申", "戌", "子"],
|
||||
"yao_elements": ["水", "木", "土", "金", "土", "水"],
|
||||
"world_position": 2,
|
||||
"response_position": 5,
|
||||
"fushen_positions": [2],
|
||||
"fushen_relations": ["妻财"],
|
||||
"fushen_tigan": ["午"],
|
||||
"fushen_elements": ["火"]
|
||||
},
|
||||
"101010": {
|
||||
"name": "水火既济",
|
||||
"binary": "101010",
|
||||
"upper_name": "坎",
|
||||
"lower_name": "离",
|
||||
"yao_relations": ["子孙", "官鬼", "兄弟", "父母", "官鬼", "兄弟"],
|
||||
"yao_tigan": ["卯", "丑", "亥", "申", "戌", "子"],
|
||||
"yao_elements": ["木", "土", "水", "金", "土", "水"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [2],
|
||||
"fushen_relations": ["妻财"],
|
||||
"fushen_tigan": ["午"],
|
||||
"fushen_elements": ["火"]
|
||||
},
|
||||
"101110": {
|
||||
"name": "泽火革",
|
||||
"binary": "101110",
|
||||
"upper_name": "兑",
|
||||
"lower_name": "离",
|
||||
"yao_relations": ["子孙", "官鬼", "兄弟", "兄弟", "父母", "官鬼"],
|
||||
"yao_tigan": ["卯", "丑", "亥", "亥", "酉", "未"],
|
||||
"yao_elements": ["木", "土", "水", "水", "金", "土"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [2],
|
||||
"fushen_relations": ["妻财"],
|
||||
"fushen_tigan": ["午"],
|
||||
"fushen_elements": ["火"]
|
||||
},
|
||||
"101100": {
|
||||
"name": "雷火丰",
|
||||
"binary": "101100",
|
||||
"upper_name": "震",
|
||||
"lower_name": "离",
|
||||
"yao_relations": ["子孙", "官鬼", "兄弟", "妻财", "父母", "官鬼"],
|
||||
"yao_tigan": ["卯", "丑", "亥", "午", "申", "戌"],
|
||||
"yao_elements": ["木", "土", "水", "火", "金", "土"],
|
||||
"world_position": 5,
|
||||
"response_position": 2,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"101000": {
|
||||
"name": "地火明夷",
|
||||
"binary": "101000",
|
||||
"upper_name": "坤",
|
||||
"lower_name": "离",
|
||||
"yao_relations": ["子孙", "官鬼", "兄弟", "官鬼", "兄弟", "父母"],
|
||||
"yao_tigan": ["卯", "丑", "亥", "丑", "亥", "酉"],
|
||||
"yao_elements": ["木", "土", "水", "土", "水", "金"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [2],
|
||||
"fushen_relations": ["妻财"],
|
||||
"fushen_tigan": ["午"],
|
||||
"fushen_elements": ["火"]
|
||||
},
|
||||
"010000": {
|
||||
"name": "地水师",
|
||||
"binary": "010000",
|
||||
"upper_name": "坤",
|
||||
"lower_name": "坎",
|
||||
"yao_relations": ["子孙", "官鬼", "妻财", "官鬼", "兄弟", "父母"],
|
||||
"yao_tigan": ["寅", "辰", "午", "丑", "亥", "酉"],
|
||||
"yao_elements": ["木", "土", "火", "土", "水", "金"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"001001": {
|
||||
"name": "艮为山",
|
||||
"binary": "001001",
|
||||
"upper_name": "艮",
|
||||
"lower_name": "艮",
|
||||
"yao_relations": ["兄弟", "父母", "子孙", "兄弟", "妻财", "官鬼"],
|
||||
"yao_tigan": ["辰", "午", "申", "戌", "子", "寅"],
|
||||
"yao_elements": ["土", "火", "金", "土", "水", "木"],
|
||||
"world_position": 6,
|
||||
"response_position": 3,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"101001": {
|
||||
"name": "山火贲",
|
||||
"binary": "101001",
|
||||
"upper_name": "艮",
|
||||
"lower_name": "离",
|
||||
"yao_relations": ["官鬼", "兄弟", "妻财", "兄弟", "妻财", "官鬼"],
|
||||
"yao_tigan": ["卯", "丑", "亥", "戌", "子", "寅"],
|
||||
"yao_elements": ["木", "土", "水", "土", "水", "木"],
|
||||
"world_position": 1,
|
||||
"response_position": 4,
|
||||
"fushen_positions": [1, 2],
|
||||
"fushen_relations": ["父母", "子孙"],
|
||||
"fushen_tigan": ["午", "申"],
|
||||
"fushen_elements": ["火", "金"]
|
||||
},
|
||||
"111001": {
|
||||
"name": "山天大畜",
|
||||
"binary": "111001",
|
||||
"upper_name": "艮",
|
||||
"lower_name": "乾",
|
||||
"yao_relations": ["妻财", "官鬼", "兄弟", "兄弟", "妻财", "官鬼"],
|
||||
"yao_tigan": ["子", "寅", "辰", "戌", "子", "寅"],
|
||||
"yao_elements": ["水", "木", "土", "土", "水", "木"],
|
||||
"world_position": 2,
|
||||
"response_position": 5,
|
||||
"fushen_positions": [1, 2],
|
||||
"fushen_relations": ["父母", "子孙"],
|
||||
"fushen_tigan": ["午", "申"],
|
||||
"fushen_elements": ["火", "金"]
|
||||
},
|
||||
"110001": {
|
||||
"name": "山泽损",
|
||||
"binary": "110001",
|
||||
"upper_name": "艮",
|
||||
"lower_name": "兑",
|
||||
"yao_relations": ["父母", "官鬼", "兄弟", "兄弟", "妻财", "官鬼"],
|
||||
"yao_tigan": ["巳", "卯", "丑", "戌", "子", "寅"],
|
||||
"yao_elements": ["火", "木", "土", "土", "水", "木"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [2],
|
||||
"fushen_relations": ["子孙"],
|
||||
"fushen_tigan": ["申"],
|
||||
"fushen_elements": ["金"]
|
||||
},
|
||||
"110101": {
|
||||
"name": "火泽睽",
|
||||
"binary": "110101",
|
||||
"upper_name": "离",
|
||||
"lower_name": "兑",
|
||||
"yao_relations": ["父母", "官鬼", "兄弟", "子孙", "兄弟", "父母"],
|
||||
"yao_tigan": ["巳", "卯", "丑", "酉", "未", "巳"],
|
||||
"yao_elements": ["火", "木", "土", "金", "土", "火"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [4],
|
||||
"fushen_relations": ["妻财"],
|
||||
"fushen_tigan": ["子"],
|
||||
"fushen_elements": ["水"]
|
||||
},
|
||||
"110111": {
|
||||
"name": "天泽履",
|
||||
"binary": "110111",
|
||||
"upper_name": "乾",
|
||||
"lower_name": "兑",
|
||||
"yao_relations": ["父母", "官鬼", "兄弟", "父母", "子孙", "兄弟"],
|
||||
"yao_tigan": ["巳", "卯", "丑", "午", "申", "戌"],
|
||||
"yao_elements": ["火", "木", "土", "火", "金", "土"],
|
||||
"world_position": 5,
|
||||
"response_position": 2,
|
||||
"fushen_positions": [4],
|
||||
"fushen_relations": ["妻财"],
|
||||
"fushen_tigan": ["子"],
|
||||
"fushen_elements": ["水"]
|
||||
},
|
||||
"110011": {
|
||||
"name": "风泽中孚",
|
||||
"binary": "110011",
|
||||
"upper_name": "巽",
|
||||
"lower_name": "兑",
|
||||
"yao_relations": ["父母", "官鬼", "兄弟", "兄弟", "父母", "官鬼"],
|
||||
"yao_tigan": ["巳", "卯", "丑", "未", "巳", "卯"],
|
||||
"yao_elements": ["火", "木", "土", "土", "火", "木"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [2, 4],
|
||||
"fushen_relations": ["子孙", "妻财"],
|
||||
"fushen_tigan": ["申", "子"],
|
||||
"fushen_elements": ["金", "水"]
|
||||
},
|
||||
"001011": {
|
||||
"name": "风山渐",
|
||||
"binary": "001011",
|
||||
"upper_name": "巽",
|
||||
"lower_name": "艮",
|
||||
"yao_relations": ["兄弟", "父母", "子孙", "兄弟", "父母", "官鬼"],
|
||||
"yao_tigan": ["辰", "午", "申", "未", "巳", "卯"],
|
||||
"yao_elements": ["土", "火", "金", "土", "火", "木"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [4],
|
||||
"fushen_relations": ["妻财"],
|
||||
"fushen_tigan": ["子"],
|
||||
"fushen_elements": ["水"]
|
||||
},
|
||||
"100100": {
|
||||
"name": "震为雷",
|
||||
"binary": "100100",
|
||||
"upper_name": "震",
|
||||
"lower_name": "震",
|
||||
"yao_relations": ["父母", "兄弟", "妻财", "子孙", "官鬼", "妻财"],
|
||||
"yao_tigan": ["子", "寅", "辰", "午", "申", "戌"],
|
||||
"yao_elements": ["水", "木", "土", "火", "金", "土"],
|
||||
"world_position": 6,
|
||||
"response_position": 3,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"000100": {
|
||||
"name": "雷地豫",
|
||||
"binary": "000100",
|
||||
"upper_name": "震",
|
||||
"lower_name": "坤",
|
||||
"yao_relations": ["妻财", "子孙", "兄弟", "子孙", "官鬼", "妻财"],
|
||||
"yao_tigan": ["未", "巳", "卯", "午", "申", "戌"],
|
||||
"yao_elements": ["土", "火", "木", "火", "金", "土"],
|
||||
"world_position": 1,
|
||||
"response_position": 4,
|
||||
"fushen_positions": [0],
|
||||
"fushen_relations": ["父母"],
|
||||
"fushen_tigan": ["子"],
|
||||
"fushen_elements": ["水"]
|
||||
},
|
||||
"010100": {
|
||||
"name": "雷水解",
|
||||
"binary": "010100",
|
||||
"upper_name": "震",
|
||||
"lower_name": "坎",
|
||||
"yao_relations": ["兄弟", "妻财", "子孙", "子孙", "官鬼", "妻财"],
|
||||
"yao_tigan": ["寅", "辰", "午", "午", "申", "戌"],
|
||||
"yao_elements": ["木", "土", "火", "火", "金", "土"],
|
||||
"world_position": 2,
|
||||
"response_position": 5,
|
||||
"fushen_positions": [0],
|
||||
"fushen_relations": ["父母"],
|
||||
"fushen_tigan": ["子"],
|
||||
"fushen_elements": ["水"]
|
||||
},
|
||||
"011100": {
|
||||
"name": "雷风恒",
|
||||
"binary": "011100",
|
||||
"upper_name": "震",
|
||||
"lower_name": "巽",
|
||||
"yao_relations": ["妻财", "父母", "官鬼", "子孙", "官鬼", "妻财"],
|
||||
"yao_tigan": ["丑", "亥", "酉", "午", "申", "戌"],
|
||||
"yao_elements": ["土", "水", "金", "火", "金", "土"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [1],
|
||||
"fushen_relations": ["兄弟"],
|
||||
"fushen_tigan": ["寅"],
|
||||
"fushen_elements": ["木"]
|
||||
},
|
||||
"011000": {
|
||||
"name": "地风升",
|
||||
"binary": "011000",
|
||||
"upper_name": "坤",
|
||||
"lower_name": "巽",
|
||||
"yao_relations": ["妻财", "父母", "官鬼", "妻财", "父母", "官鬼"],
|
||||
"yao_tigan": ["丑", "亥", "酉", "丑", "亥", "酉"],
|
||||
"yao_elements": ["土", "水", "金", "土", "水", "金"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [1, 3],
|
||||
"fushen_relations": ["兄弟", "子孙"],
|
||||
"fushen_tigan": ["寅", "午"],
|
||||
"fushen_elements": ["木", "火"]
|
||||
},
|
||||
"011010": {
|
||||
"name": "水风井",
|
||||
"binary": "011010",
|
||||
"upper_name": "坎",
|
||||
"lower_name": "巽",
|
||||
"yao_relations": ["妻财", "父母", "官鬼", "官鬼", "妻财", "父母"],
|
||||
"yao_tigan": ["丑", "亥", "酉", "申", "戌", "子"],
|
||||
"yao_elements": ["土", "水", "金", "金", "土", "水"],
|
||||
"world_position": 5,
|
||||
"response_position": 2,
|
||||
"fushen_positions": [1, 3],
|
||||
"fushen_relations": ["兄弟", "子孙"],
|
||||
"fushen_tigan": ["寅", "午"],
|
||||
"fushen_elements": ["木", "火"]
|
||||
},
|
||||
"011110": {
|
||||
"name": "泽风大过",
|
||||
"binary": "011110",
|
||||
"upper_name": "兑",
|
||||
"lower_name": "巽",
|
||||
"yao_relations": ["妻财", "父母", "官鬼", "父母", "官鬼", "妻财"],
|
||||
"yao_tigan": ["丑", "亥", "酉", "亥", "酉", "未"],
|
||||
"yao_elements": ["土", "水", "金", "水", "金", "土"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [1, 3],
|
||||
"fushen_relations": ["兄弟", "子孙"],
|
||||
"fushen_tigan": ["寅", "午"],
|
||||
"fushen_elements": ["木", "火"]
|
||||
},
|
||||
"100110": {
|
||||
"name": "泽雷随",
|
||||
"binary": "100110",
|
||||
"upper_name": "兑",
|
||||
"lower_name": "震",
|
||||
"yao_relations": ["父母", "兄弟", "妻财", "父母", "官鬼", "妻财"],
|
||||
"yao_tigan": ["子", "寅", "辰", "亥", "酉", "未"],
|
||||
"yao_elements": ["水", "木", "土", "水", "金", "土"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [3],
|
||||
"fushen_relations": ["子孙"],
|
||||
"fushen_tigan": ["午"],
|
||||
"fushen_elements": ["火"]
|
||||
},
|
||||
"011011": {
|
||||
"name": "巽为风",
|
||||
"binary": "011011",
|
||||
"upper_name": "巽",
|
||||
"lower_name": "巽",
|
||||
"yao_relations": ["妻财", "父母", "官鬼", "妻财", "子孙", "兄弟"],
|
||||
"yao_tigan": ["丑", "亥", "酉", "未", "巳", "卯"],
|
||||
"yao_elements": ["土", "水", "金", "土", "火", "木"],
|
||||
"world_position": 6,
|
||||
"response_position": 3,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"111011": {
|
||||
"name": "风天小畜",
|
||||
"binary": "111011",
|
||||
"upper_name": "巽",
|
||||
"lower_name": "乾",
|
||||
"yao_relations": ["父母", "兄弟", "妻财", "妻财", "子孙", "兄弟"],
|
||||
"yao_tigan": ["子", "寅", "辰", "未", "巳", "卯"],
|
||||
"yao_elements": ["水", "木", "土", "土", "火", "木"],
|
||||
"world_position": 1,
|
||||
"response_position": 4,
|
||||
"fushen_positions": [2],
|
||||
"fushen_relations": ["官鬼"],
|
||||
"fushen_tigan": ["酉"],
|
||||
"fushen_elements": ["金"]
|
||||
},
|
||||
"101011": {
|
||||
"name": "风火家人",
|
||||
"binary": "101011",
|
||||
"upper_name": "巽",
|
||||
"lower_name": "离",
|
||||
"yao_relations": ["兄弟", "妻财", "父母", "妻财", "子孙", "兄弟"],
|
||||
"yao_tigan": ["卯", "丑", "亥", "未", "巳", "卯"],
|
||||
"yao_elements": ["木", "土", "水", "土", "火", "木"],
|
||||
"world_position": 2,
|
||||
"response_position": 5,
|
||||
"fushen_positions": [2],
|
||||
"fushen_relations": ["官鬼"],
|
||||
"fushen_tigan": ["酉"],
|
||||
"fushen_elements": ["金"]
|
||||
},
|
||||
"100011": {
|
||||
"name": "风雷益",
|
||||
"binary": "100011",
|
||||
"upper_name": "巽",
|
||||
"lower_name": "震",
|
||||
"yao_relations": ["父母", "兄弟", "妻财", "妻财", "子孙", "兄弟"],
|
||||
"yao_tigan": ["子", "寅", "辰", "未", "巳", "卯"],
|
||||
"yao_elements": ["水", "木", "土", "土", "火", "木"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [2],
|
||||
"fushen_relations": ["官鬼"],
|
||||
"fushen_tigan": ["酉"],
|
||||
"fushen_elements": ["金"]
|
||||
},
|
||||
"100111": {
|
||||
"name": "天雷无妄",
|
||||
"binary": "100111",
|
||||
"upper_name": "乾",
|
||||
"lower_name": "震",
|
||||
"yao_relations": ["父母", "兄弟", "妻财", "子孙", "官鬼", "妻财"],
|
||||
"yao_tigan": ["子", "寅", "辰", "午", "申", "戌"],
|
||||
"yao_elements": ["水", "木", "土", "火", "金", "土"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"100101": {
|
||||
"name": "火雷噬嗑",
|
||||
"binary": "100101",
|
||||
"upper_name": "离",
|
||||
"lower_name": "震",
|
||||
"yao_relations": ["父母", "兄弟", "妻财", "官鬼", "妻财", "子孙"],
|
||||
"yao_tigan": ["子", "寅", "辰", "酉", "未", "巳"],
|
||||
"yao_elements": ["水", "木", "土", "金", "土", "火"],
|
||||
"world_position": 5,
|
||||
"response_position": 2,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"100001": {
|
||||
"name": "山雷颐",
|
||||
"binary": "100001",
|
||||
"upper_name": "艮",
|
||||
"lower_name": "震",
|
||||
"yao_relations": ["父母", "兄弟", "妻财", "妻财", "父母", "兄弟"],
|
||||
"yao_tigan": ["子", "寅", "辰", "戌", "子", "寅"],
|
||||
"yao_elements": ["水", "木", "土", "土", "水", "木"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [2, 4],
|
||||
"fushen_relations": ["官鬼", "子孙"],
|
||||
"fushen_tigan": ["酉", "巳"],
|
||||
"fushen_elements": ["金", "火"]
|
||||
},
|
||||
"011001": {
|
||||
"name": "山风蛊",
|
||||
"binary": "011001",
|
||||
"upper_name": "艮",
|
||||
"lower_name": "巽",
|
||||
"yao_relations": ["妻财", "父母", "官鬼", "妻财", "父母", "兄弟"],
|
||||
"yao_tigan": ["丑", "亥", "酉", "戌", "子", "寅"],
|
||||
"yao_elements": ["土", "水", "金", "土", "水", "木"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [4],
|
||||
"fushen_relations": ["子孙"],
|
||||
"fushen_tigan": ["巳"],
|
||||
"fushen_elements": ["火"]
|
||||
},
|
||||
"101101": {
|
||||
"name": "离为火",
|
||||
"binary": "101101",
|
||||
"upper_name": "离",
|
||||
"lower_name": "离",
|
||||
"yao_relations": ["父母", "子孙", "官鬼", "妻财", "子孙", "兄弟"],
|
||||
"yao_tigan": ["卯", "丑", "亥", "酉", "未", "巳"],
|
||||
"yao_elements": ["木", "土", "水", "金", "土", "火"],
|
||||
"world_position": 6,
|
||||
"response_position": 3,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"001101": {
|
||||
"name": "火山旅",
|
||||
"binary": "001101",
|
||||
"upper_name": "离",
|
||||
"lower_name": "艮",
|
||||
"yao_relations": ["子孙", "兄弟", "妻财", "妻财", "子孙", "兄弟"],
|
||||
"yao_tigan": ["辰", "午", "申", "酉", "未", "巳"],
|
||||
"yao_elements": ["土", "火", "金", "金", "土", "火"],
|
||||
"world_position": 1,
|
||||
"response_position": 4,
|
||||
"fushen_positions": [0, 2],
|
||||
"fushen_relations": ["父母", "官鬼"],
|
||||
"fushen_tigan": ["卯", "亥"],
|
||||
"fushen_elements": ["木", "水"]
|
||||
},
|
||||
"011101": {
|
||||
"name": "火风鼎",
|
||||
"binary": "011101",
|
||||
"upper_name": "离",
|
||||
"lower_name": "巽",
|
||||
"yao_relations": ["子孙", "官鬼", "妻财", "妻财", "子孙", "兄弟"],
|
||||
"yao_tigan": ["丑", "亥", "酉", "酉", "未", "巳"],
|
||||
"yao_elements": ["土", "水", "金", "金", "土", "火"],
|
||||
"world_position": 2,
|
||||
"response_position": 5,
|
||||
"fushen_positions": [0],
|
||||
"fushen_relations": ["父母"],
|
||||
"fushen_tigan": ["卯"],
|
||||
"fushen_elements": ["木"]
|
||||
},
|
||||
"010101": {
|
||||
"name": "火水未济",
|
||||
"binary": "010101",
|
||||
"upper_name": "离",
|
||||
"lower_name": "坎",
|
||||
"yao_relations": ["父母", "子孙", "兄弟", "妻财", "子孙", "兄弟"],
|
||||
"yao_tigan": ["寅", "辰", "午", "酉", "未", "巳"],
|
||||
"yao_elements": ["木", "土", "火", "金", "土", "火"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [2],
|
||||
"fushen_relations": ["官鬼"],
|
||||
"fushen_tigan": ["亥"],
|
||||
"fushen_elements": ["水"]
|
||||
},
|
||||
"010001": {
|
||||
"name": "山水蒙",
|
||||
"binary": "010001",
|
||||
"upper_name": "艮",
|
||||
"lower_name": "坎",
|
||||
"yao_relations": ["父母", "子孙", "兄弟", "子孙", "官鬼", "父母"],
|
||||
"yao_tigan": ["寅", "辰", "午", "戌", "子", "寅"],
|
||||
"yao_elements": ["木", "土", "火", "土", "水", "木"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [3],
|
||||
"fushen_relations": ["妻财"],
|
||||
"fushen_tigan": ["酉"],
|
||||
"fushen_elements": ["金"]
|
||||
},
|
||||
"010011": {
|
||||
"name": "风水涣",
|
||||
"binary": "010011",
|
||||
"upper_name": "巽",
|
||||
"lower_name": "坎",
|
||||
"yao_relations": ["父母", "子孙", "兄弟", "子孙", "兄弟", "父母"],
|
||||
"yao_tigan": ["寅", "辰", "午", "未", "巳", "卯"],
|
||||
"yao_elements": ["木", "土", "火", "土", "火", "木"],
|
||||
"world_position": 5,
|
||||
"response_position": 2,
|
||||
"fushen_positions": [2, 3],
|
||||
"fushen_relations": ["官鬼", "妻财"],
|
||||
"fushen_tigan": ["亥", "酉"],
|
||||
"fushen_elements": ["水", "金"]
|
||||
},
|
||||
"010111": {
|
||||
"name": "天水讼",
|
||||
"binary": "010111",
|
||||
"upper_name": "乾",
|
||||
"lower_name": "坎",
|
||||
"yao_relations": ["父母", "子孙", "兄弟", "兄弟", "妻财", "子孙"],
|
||||
"yao_tigan": ["寅", "辰", "午", "午", "申", "戌"],
|
||||
"yao_elements": ["木", "土", "火", "火", "金", "土"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [2],
|
||||
"fushen_relations": ["官鬼"],
|
||||
"fushen_tigan": ["亥"],
|
||||
"fushen_elements": ["水"]
|
||||
},
|
||||
"101111": {
|
||||
"name": "天火同人",
|
||||
"binary": "101111",
|
||||
"upper_name": "乾",
|
||||
"lower_name": "离",
|
||||
"yao_relations": ["父母", "子孙", "官鬼", "兄弟", "妻财", "子孙"],
|
||||
"yao_tigan": ["卯", "丑", "亥", "午", "申", "戌"],
|
||||
"yao_elements": ["木", "土", "水", "火", "金", "土"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"000000": {
|
||||
"name": "坤为地",
|
||||
"binary": "000000",
|
||||
"upper_name": "坤",
|
||||
"lower_name": "坤",
|
||||
"yao_relations": ["兄弟", "父母", "官鬼", "兄弟", "妻财", "子孙"],
|
||||
"yao_tigan": ["未", "巳", "卯", "丑", "亥", "酉"],
|
||||
"yao_elements": ["土", "火", "木", "土", "水", "金"],
|
||||
"world_position": 6,
|
||||
"response_position": 3,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"100000": {
|
||||
"name": "地雷复",
|
||||
"binary": "100000",
|
||||
"upper_name": "坤",
|
||||
"lower_name": "震",
|
||||
"yao_relations": ["妻财", "官鬼", "兄弟", "兄弟", "妻财", "子孙"],
|
||||
"yao_tigan": ["子", "寅", "辰", "丑", "亥", "酉"],
|
||||
"yao_elements": ["水", "木", "土", "土", "水", "金"],
|
||||
"world_position": 1,
|
||||
"response_position": 4,
|
||||
"fushen_positions": [1],
|
||||
"fushen_relations": ["父母"],
|
||||
"fushen_tigan": ["巳"],
|
||||
"fushen_elements": ["火"]
|
||||
},
|
||||
"110000": {
|
||||
"name": "地泽临",
|
||||
"binary": "110000",
|
||||
"upper_name": "坤",
|
||||
"lower_name": "兑",
|
||||
"yao_relations": ["父母", "官鬼", "兄弟", "兄弟", "妻财", "子孙"],
|
||||
"yao_tigan": ["巳", "卯", "丑", "丑", "亥", "酉"],
|
||||
"yao_elements": ["火", "木", "土", "土", "水", "金"],
|
||||
"world_position": 2,
|
||||
"response_position": 5,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"111000": {
|
||||
"name": "地天泰",
|
||||
"binary": "111000",
|
||||
"upper_name": "坤",
|
||||
"lower_name": "乾",
|
||||
"yao_relations": ["妻财", "官鬼", "兄弟", "兄弟", "妻财", "子孙"],
|
||||
"yao_tigan": ["子", "寅", "辰", "丑", "亥", "酉"],
|
||||
"yao_elements": ["水", "木", "土", "土", "水", "金"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [1],
|
||||
"fushen_relations": ["父母"],
|
||||
"fushen_tigan": ["巳"],
|
||||
"fushen_elements": ["火"]
|
||||
},
|
||||
"111100": {
|
||||
"name": "雷天大壮",
|
||||
"binary": "111100",
|
||||
"upper_name": "震",
|
||||
"lower_name": "乾",
|
||||
"yao_relations": ["妻财", "官鬼", "兄弟", "父母", "子孙", "兄弟"],
|
||||
"yao_tigan": ["子", "寅", "辰", "午", "申", "戌"],
|
||||
"yao_elements": ["水", "木", "土", "火", "金", "土"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"111110": {
|
||||
"name": "泽天夬",
|
||||
"binary": "111110",
|
||||
"upper_name": "兑",
|
||||
"lower_name": "乾",
|
||||
"yao_relations": ["妻财", "官鬼", "兄弟", "妻财", "子孙", "兄弟"],
|
||||
"yao_tigan": ["子", "寅", "辰", "亥", "酉", "未"],
|
||||
"yao_elements": ["水", "木", "土", "水", "金", "土"],
|
||||
"world_position": 5,
|
||||
"response_position": 2,
|
||||
"fushen_positions": [1],
|
||||
"fushen_relations": ["父母"],
|
||||
"fushen_tigan": ["巳"],
|
||||
"fushen_elements": ["火"]
|
||||
},
|
||||
"111010": {
|
||||
"name": "水天需",
|
||||
"binary": "111010",
|
||||
"upper_name": "坎",
|
||||
"lower_name": "乾",
|
||||
"yao_relations": ["妻财", "官鬼", "兄弟", "子孙", "兄弟", "妻财"],
|
||||
"yao_tigan": ["子", "寅", "辰", "申", "戌", "子"],
|
||||
"yao_elements": ["水", "木", "土", "金", "土", "水"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [1],
|
||||
"fushen_relations": ["父母"],
|
||||
"fushen_tigan": ["巳"],
|
||||
"fushen_elements": ["火"]
|
||||
},
|
||||
"000010": {
|
||||
"name": "水地比",
|
||||
"binary": "000010",
|
||||
"upper_name": "坎",
|
||||
"lower_name": "坤",
|
||||
"yao_relations": ["兄弟", "父母", "官鬼", "子孙", "兄弟", "妻财"],
|
||||
"yao_tigan": ["未", "巳", "卯", "申", "戌", "子"],
|
||||
"yao_elements": ["土", "火", "木", "金", "土", "水"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"110110": {
|
||||
"name": "兑为泽",
|
||||
"binary": "110110",
|
||||
"upper_name": "兑",
|
||||
"lower_name": "兑",
|
||||
"yao_relations": ["官鬼", "妻财", "父母", "子孙", "兄弟", "父母"],
|
||||
"yao_tigan": ["巳", "卯", "丑", "亥", "酉", "未"],
|
||||
"yao_elements": ["火", "木", "土", "水", "金", "土"],
|
||||
"world_position": 6,
|
||||
"response_position": 3,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"010110": {
|
||||
"name": "泽水困",
|
||||
"binary": "010110",
|
||||
"upper_name": "兑",
|
||||
"lower_name": "坎",
|
||||
"yao_relations": ["妻财", "父母", "官鬼", "子孙", "兄弟", "父母"],
|
||||
"yao_tigan": ["寅", "辰", "午", "亥", "酉", "未"],
|
||||
"yao_elements": ["木", "土", "火", "水", "金", "土"],
|
||||
"world_position": 1,
|
||||
"response_position": 4,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"000110": {
|
||||
"name": "泽地萃",
|
||||
"binary": "000110",
|
||||
"upper_name": "兑",
|
||||
"lower_name": "坤",
|
||||
"yao_relations": ["父母", "官鬼", "妻财", "子孙", "兄弟", "父母"],
|
||||
"yao_tigan": ["未", "巳", "卯", "亥", "酉", "未"],
|
||||
"yao_elements": ["土", "火", "木", "水", "金", "土"],
|
||||
"world_position": 2,
|
||||
"response_position": 5,
|
||||
"fushen_positions": [],
|
||||
"fushen_relations": [],
|
||||
"fushen_tigan": [],
|
||||
"fushen_elements": []
|
||||
},
|
||||
"001110": {
|
||||
"name": "泽山咸",
|
||||
"binary": "001110",
|
||||
"upper_name": "兑",
|
||||
"lower_name": "艮",
|
||||
"yao_relations": ["父母", "官鬼", "兄弟", "子孙", "兄弟", "父母"],
|
||||
"yao_tigan": ["辰", "午", "申", "亥", "酉", "未"],
|
||||
"yao_elements": ["土", "火", "金", "水", "金", "土"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [1],
|
||||
"fushen_relations": ["妻财"],
|
||||
"fushen_tigan": ["卯"],
|
||||
"fushen_elements": ["木"]
|
||||
},
|
||||
"001010": {
|
||||
"name": "水山蹇",
|
||||
"binary": "001010",
|
||||
"upper_name": "坎",
|
||||
"lower_name": "艮",
|
||||
"yao_relations": ["父母", "官鬼", "兄弟", "兄弟", "父母", "子孙"],
|
||||
"yao_tigan": ["辰", "午", "申", "申", "戌", "子"],
|
||||
"yao_elements": ["土", "火", "金", "金", "土", "水"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [1],
|
||||
"fushen_relations": ["妻财"],
|
||||
"fushen_tigan": ["卯"],
|
||||
"fushen_elements": ["木"]
|
||||
},
|
||||
"001000": {
|
||||
"name": "地山谦",
|
||||
"binary": "001000",
|
||||
"upper_name": "坤",
|
||||
"lower_name": "艮",
|
||||
"yao_relations": ["父母", "官鬼", "兄弟", "父母", "子孙", "兄弟"],
|
||||
"yao_tigan": ["辰", "午", "申", "丑", "亥", "酉"],
|
||||
"yao_elements": ["土", "火", "金", "土", "水", "金"],
|
||||
"world_position": 5,
|
||||
"response_position": 2,
|
||||
"fushen_positions": [1],
|
||||
"fushen_relations": ["妻财"],
|
||||
"fushen_tigan": ["卯"],
|
||||
"fushen_elements": ["木"]
|
||||
},
|
||||
"001100": {
|
||||
"name": "雷山小过",
|
||||
"binary": "001100",
|
||||
"upper_name": "震",
|
||||
"lower_name": "艮",
|
||||
"yao_relations": ["父母", "官鬼", "兄弟", "官鬼", "兄弟", "父母"],
|
||||
"yao_tigan": ["辰", "午", "申", "午", "申", "戌"],
|
||||
"yao_elements": ["土", "火", "金", "火", "金", "土"],
|
||||
"world_position": 4,
|
||||
"response_position": 1,
|
||||
"fushen_positions": [1, 3],
|
||||
"fushen_relations": ["妻财", "子孙"],
|
||||
"fushen_tigan": ["卯", "亥"],
|
||||
"fushen_elements": ["木", "水"]
|
||||
},
|
||||
"110100": {
|
||||
"name": "雷泽归妹",
|
||||
"binary": "110100",
|
||||
"upper_name": "震",
|
||||
"lower_name": "兑",
|
||||
"yao_relations": ["官鬼", "妻财", "父母", "官鬼", "兄弟", "父母"],
|
||||
"yao_tigan": ["巳", "卯", "丑", "午", "申", "戌"],
|
||||
"yao_elements": ["火", "木", "土", "火", "金", "土"],
|
||||
"world_position": 3,
|
||||
"response_position": 6,
|
||||
"fushen_positions": [3],
|
||||
"fushen_relations": ["子孙"],
|
||||
"fushen_tigan": ["亥"],
|
||||
"fushen_elements": ["水"]
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -23,116 +23,42 @@ class GuaCatalogItem:
|
||||
fushen_elements: tuple[str, ...]
|
||||
|
||||
|
||||
_ENTRY_HEAD_RE = re.compile(r'put\("([01]{6})",\s*GuaInfo\(', re.MULTILINE)
|
||||
_STRING_FIELD_RE = re.compile(r'\b%s\s*=\s*"([^"]*)"')
|
||||
_INT_FIELD_RE = re.compile(r"\b%s\s*=\s*(\d+)")
|
||||
_LIST_STRING_FIELD_RE = re.compile(r"\b%s\s*=\s*listOf\((.*?)\)", re.DOTALL)
|
||||
_LIST_INT_FIELD_RE = re.compile(r"\b%s\s*=\s*listOf\((.*?)\)", re.DOTALL)
|
||||
|
||||
|
||||
def _extract_gua_body(source: str, start_idx: int) -> tuple[str, int]:
|
||||
depth = 1
|
||||
idx = start_idx
|
||||
while idx < len(source):
|
||||
ch = source[idx]
|
||||
if ch == "(":
|
||||
depth += 1
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start_idx:idx], idx
|
||||
idx += 1
|
||||
raise ValueError("invalid Guaxiang.kt structure: unmatched parenthesis")
|
||||
|
||||
|
||||
def _parse_string_field(body: str, field_name: str) -> str:
|
||||
match = _STRING_FIELD_RE.pattern % re.escape(field_name)
|
||||
found = re.search(match, body)
|
||||
if found is None:
|
||||
raise ValueError(f"missing field: {field_name}")
|
||||
return found.group(1)
|
||||
|
||||
|
||||
def _parse_int_field(body: str, field_name: str) -> int:
|
||||
match = _INT_FIELD_RE.pattern % re.escape(field_name)
|
||||
found = re.search(match, body)
|
||||
if found is None:
|
||||
raise ValueError(f"missing field: {field_name}")
|
||||
return int(found.group(1))
|
||||
|
||||
|
||||
def _parse_list_of_strings(
|
||||
body: str, field_name: str, *, optional: bool = False
|
||||
) -> tuple[str, ...]:
|
||||
if f"{field_name} = emptyList()" in body:
|
||||
return ()
|
||||
match = _LIST_STRING_FIELD_RE.pattern % re.escape(field_name)
|
||||
found = re.search(match, body)
|
||||
if found is None:
|
||||
if optional:
|
||||
return ()
|
||||
raise ValueError(f"missing list field: {field_name}")
|
||||
inner = found.group(1)
|
||||
values = re.findall(r'"([^"]+)"', inner)
|
||||
return tuple(values)
|
||||
|
||||
|
||||
def _parse_list_of_ints(
|
||||
body: str, field_name: str, *, optional: bool = False
|
||||
) -> tuple[int, ...]:
|
||||
if f"{field_name} = emptyList()" in body:
|
||||
return ()
|
||||
match = _LIST_INT_FIELD_RE.pattern % re.escape(field_name)
|
||||
found = re.search(match, body)
|
||||
if found is None:
|
||||
if optional:
|
||||
return ()
|
||||
raise ValueError(f"missing list field: {field_name}")
|
||||
inner = found.group(1)
|
||||
values = [int(item.strip()) for item in inner.split(",") if item.strip()]
|
||||
return tuple(values)
|
||||
|
||||
|
||||
def _resolve_guaxiang_file() -> Path:
|
||||
def _resolve_catalog_file() -> Path:
|
||||
current = Path(__file__).resolve()
|
||||
root = current.parents[4]
|
||||
target = (
|
||||
root / "old/app/src/main/java/com/example/eryaoapp/screens/result/Guaxiang.kt"
|
||||
)
|
||||
target = current.parent / "data/gua_catalog.json"
|
||||
if not target.exists():
|
||||
raise FileNotFoundError(f"Guaxiang.kt not found: {target}")
|
||||
raise FileNotFoundError(f"gua_catalog.json not found: {target}")
|
||||
return target
|
||||
|
||||
|
||||
def _to_item(raw: object) -> GuaCatalogItem:
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("invalid gua catalog item: expected object")
|
||||
return GuaCatalogItem(
|
||||
name=str(raw["name"]),
|
||||
binary=str(raw["binary"]),
|
||||
upper_name=str(raw["upper_name"]),
|
||||
lower_name=str(raw["lower_name"]),
|
||||
yao_relations=tuple(str(v) for v in raw["yao_relations"]),
|
||||
yao_tigan=tuple(str(v) for v in raw["yao_tigan"]),
|
||||
yao_elements=tuple(str(v) for v in raw["yao_elements"]),
|
||||
world_position=int(raw["world_position"]),
|
||||
response_position=int(raw["response_position"]),
|
||||
fushen_positions=tuple(int(v) for v in raw["fushen_positions"]),
|
||||
fushen_relations=tuple(str(v) for v in raw["fushen_relations"]),
|
||||
fushen_tigan=tuple(str(v) for v in raw["fushen_tigan"]),
|
||||
fushen_elements=tuple(str(v) for v in raw["fushen_elements"]),
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def load_gua_catalog() -> dict[str, GuaCatalogItem]:
|
||||
source = _resolve_guaxiang_file().read_text(encoding="utf-8")
|
||||
result: dict[str, GuaCatalogItem] = {}
|
||||
for head in _ENTRY_HEAD_RE.finditer(source):
|
||||
binary = head.group(1)
|
||||
body, _ = _extract_gua_body(source, head.end())
|
||||
item = GuaCatalogItem(
|
||||
name=_parse_string_field(body, "name"),
|
||||
binary=_parse_string_field(body, "binary"),
|
||||
upper_name=_parse_string_field(body, "upperName"),
|
||||
lower_name=_parse_string_field(body, "lowerName"),
|
||||
yao_relations=_parse_list_of_strings(body, "yaoRelations"),
|
||||
yao_tigan=_parse_list_of_strings(body, "yaoTiGan"),
|
||||
yao_elements=_parse_list_of_strings(body, "yaoElements"),
|
||||
world_position=_parse_int_field(body, "worldPosition"),
|
||||
response_position=_parse_int_field(body, "responsePosition"),
|
||||
fushen_positions=_parse_list_of_ints(
|
||||
body, "fushenPositions", optional=True
|
||||
),
|
||||
fushen_relations=_parse_list_of_strings(
|
||||
body, "fushenRelations", optional=True
|
||||
),
|
||||
fushen_tigan=_parse_list_of_strings(body, "fushenTiGan", optional=True),
|
||||
fushen_elements=_parse_list_of_strings(
|
||||
body, "fushenElements", optional=True
|
||||
),
|
||||
)
|
||||
result[binary] = item
|
||||
source = _resolve_catalog_file().read_text(encoding="utf-8")
|
||||
raw_data = json.loads(source)
|
||||
if not isinstance(raw_data, dict):
|
||||
raise ValueError("invalid gua catalog payload")
|
||||
|
||||
result = {str(binary): _to_item(item) for binary, item in raw_data.items()}
|
||||
|
||||
if len(result) != 64:
|
||||
raise ValueError(f"invalid gua catalog size: {len(result)}")
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from .agent_chat_message import AgentChatMessage
|
||||
from .agent_chat_session import AgentChatSession
|
||||
from .auth_user import AuthUser
|
||||
from .llm import Llm
|
||||
from .llm_factory import LlmFactory
|
||||
from .points_ledger import PointsLedger
|
||||
@@ -12,6 +13,7 @@ from .user_points import UserPoints
|
||||
__all__ = [
|
||||
"AgentChatMessage",
|
||||
"AgentChatSession",
|
||||
"AuthUser",
|
||||
"Llm",
|
||||
"LlmFactory",
|
||||
"PointsLedger",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from core.db.base import Base
|
||||
|
||||
|
||||
class AuthUser(Base):
|
||||
__tablename__ = "users"
|
||||
__table_args__ = {"schema": "auth"}
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True)
|
||||
email: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
@@ -7,6 +7,7 @@ from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
|
||||
from models.auth_user import AuthUser # noqa: F401
|
||||
|
||||
|
||||
class Profile(TimestampMixin, SoftDeleteMixin, Base):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Literal
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
from schemas.agent.ui_hints import UiHintsPayload
|
||||
from schemas.domain.divination import DerivedDivinationData
|
||||
|
||||
|
||||
class RunStatus(str, Enum):
|
||||
@@ -43,7 +44,7 @@ class WorkerAgentOutputLite(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
status: RunStatus = RunStatus.SUCCESS
|
||||
sign_level: Literal["上上签", "中上签", "中下签"]
|
||||
sign_level: Literal["上上签", "中上签", "中下签", "下下签"]
|
||||
summary: str = Field(min_length=1, max_length=300)
|
||||
conclusion: list[str] = Field(min_length=1, max_length=6)
|
||||
focus_points: list[str] = Field(default_factory=list, max_length=6)
|
||||
@@ -56,6 +57,7 @@ class WorkerAgentOutputLite(BaseModel):
|
||||
key_points: list[str] = Field(default_factory=list, max_length=6)
|
||||
result_type: str = Field(default="structured_payload")
|
||||
suggested_actions: list[str] = Field(default_factory=list, max_length=6)
|
||||
divination_derived: DerivedDivinationData | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def sync_compatibility_fields(self) -> WorkerAgentOutputLite:
|
||||
|
||||
@@ -335,6 +335,59 @@ class AgentRepository:
|
||||
return None
|
||||
return str(latest_id)
|
||||
|
||||
async def get_latest_assistant_messages_by_user_sessions(
|
||||
self,
|
||||
*,
|
||||
user_id: str,
|
||||
visibility_mask: int | None = None,
|
||||
session_limit: int = 50,
|
||||
) -> list[dict[str, object]]:
|
||||
try:
|
||||
user_uuid = UUID(user_id)
|
||||
except ValueError as exc:
|
||||
raise ApiProblemError(
|
||||
status_code=422,
|
||||
code="AGENT_USER_ID_INVALID",
|
||||
detail="Invalid user_id",
|
||||
) from exc
|
||||
|
||||
safe_limit = max(int(session_limit), 1)
|
||||
session_stmt = (
|
||||
select(AgentChatSession.id)
|
||||
.where(AgentChatSession.user_id == user_uuid)
|
||||
.where(AgentChatSession.deleted_at.is_(None))
|
||||
.order_by(AgentChatSession.last_activity_at.desc())
|
||||
.limit(safe_limit)
|
||||
)
|
||||
session_ids = (await self._session.execute(session_stmt)).scalars().all()
|
||||
if not session_ids:
|
||||
return []
|
||||
|
||||
snapshots: list[dict[str, object]] = []
|
||||
for session_id in session_ids:
|
||||
message_stmt = (
|
||||
select(AgentChatMessage)
|
||||
.where(AgentChatMessage.session_id == session_id)
|
||||
.where(AgentChatMessage.deleted_at.is_(None))
|
||||
.where(AgentChatMessage.role == AgentChatMessageRole.ASSISTANT)
|
||||
.order_by(AgentChatMessage.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
message_stmt = self._apply_visibility_filter(
|
||||
stmt=message_stmt,
|
||||
visibility_mask=visibility_mask,
|
||||
)
|
||||
message = (await self._session.execute(message_stmt)).scalar_one_or_none()
|
||||
if message is None:
|
||||
continue
|
||||
snapshots.append(await self._to_snapshot_message(message))
|
||||
|
||||
snapshots.sort(
|
||||
key=lambda item: str(item.get("timestamp") or ""),
|
||||
reverse=True,
|
||||
)
|
||||
return snapshots
|
||||
|
||||
async def get_system_agent_config(
|
||||
self, *, agent_type: str
|
||||
) -> dict[str, object] | None:
|
||||
|
||||
@@ -7,7 +7,7 @@ from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from schemas.agent.ui_schema import UiSchemaRenderer
|
||||
from schemas.domain.divination import DerivedDivinationData
|
||||
|
||||
|
||||
class AgentRepositoryLike(Protocol):
|
||||
@@ -31,6 +31,14 @@ class AgentRepositoryLike(Protocol):
|
||||
|
||||
async def get_latest_session_id_for_user(self, *, user_id: str) -> str | None: ...
|
||||
|
||||
async def get_latest_assistant_messages_by_user_sessions(
|
||||
self,
|
||||
*,
|
||||
user_id: str,
|
||||
visibility_mask: int | None = None,
|
||||
session_limit: int = 50,
|
||||
) -> list[dict[str, object]]: ...
|
||||
|
||||
async def persist_user_message(
|
||||
self,
|
||||
*,
|
||||
@@ -187,13 +195,31 @@ class HistoryMessage(BaseModel):
|
||||
default_factory=list,
|
||||
description="Temporary signed URLs for user-attached images",
|
||||
)
|
||||
ui_schema: UiSchemaRenderer | None = Field(
|
||||
|
||||
agent_output: HistoryAgentOutput | None = Field(
|
||||
default=None,
|
||||
description="Compiled UI schema from worker ui_hints for frontend rendering",
|
||||
description="Structured assistant output for history replay",
|
||||
)
|
||||
timestamp: str = Field(description="Message creation timestamp in ISO-8601 format")
|
||||
|
||||
|
||||
class HistoryAgentOutput(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
status: Literal["success", "failed"] | None = None
|
||||
sign_level: Literal["上上签", "中上签", "中下签", "下下签"] | None = None
|
||||
summary: str | None = None
|
||||
conclusion: list[str] = Field(default_factory=list)
|
||||
focus_points: list[str] = Field(default_factory=list)
|
||||
advice: list[str] = Field(default_factory=list)
|
||||
keywords: list[str] = Field(default_factory=list)
|
||||
answer: str | None = None
|
||||
key_points: list[str] = Field(default_factory=list)
|
||||
result_type: str | None = None
|
||||
suggested_actions: list[str] = Field(default_factory=list)
|
||||
divination_derived: DerivedDivinationData | None = None
|
||||
|
||||
|
||||
class HistorySnapshotResponse(BaseModel):
|
||||
"""Response schema for GET /api/v1/agent/history"""
|
||||
|
||||
|
||||
@@ -641,23 +641,37 @@ class AgentService:
|
||||
thread_id: str | None,
|
||||
before: date | None,
|
||||
) -> HistorySnapshotResponse:
|
||||
target_thread_id = thread_id
|
||||
if target_thread_id is None:
|
||||
target_thread_id = await self._repository.get_latest_session_id_for_user(
|
||||
user_id=str(current_user.id)
|
||||
from schemas.domain.chat_message import AgentChatMessage
|
||||
from v1.agent.utils import convert_message_to_history
|
||||
from v1.agent.schemas import HistoryMessage
|
||||
|
||||
if thread_id is not None:
|
||||
return await self.get_history_snapshot(
|
||||
thread_id=thread_id,
|
||||
before=before,
|
||||
current_user=current_user,
|
||||
)
|
||||
if target_thread_id is None:
|
||||
return HistorySnapshotResponse(
|
||||
scope="history_day",
|
||||
threadId=None,
|
||||
day=None,
|
||||
hasMore=False,
|
||||
messages=[],
|
||||
|
||||
raw_messages = (
|
||||
await self._repository.get_latest_assistant_messages_by_user_sessions(
|
||||
user_id=str(current_user.id),
|
||||
visibility_mask=bit_mask(bit=int(SystemVisibilityBit.UI_HISTORY)),
|
||||
session_limit=50,
|
||||
)
|
||||
return await self.get_history_snapshot(
|
||||
thread_id=target_thread_id,
|
||||
before=before,
|
||||
current_user=current_user,
|
||||
)
|
||||
|
||||
messages: list[HistoryMessage] = []
|
||||
for msg_dict in raw_messages:
|
||||
msg = AgentChatMessage.model_validate(msg_dict)
|
||||
converted = convert_message_to_history(msg)
|
||||
messages.append(HistoryMessage.model_validate(converted))
|
||||
|
||||
return HistorySnapshotResponse(
|
||||
scope="history_sessions_latest_assistant",
|
||||
threadId=None,
|
||||
day=None,
|
||||
hasMore=False,
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
def _validate_binary_signed_url(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from core.agentscope.runtime.ui_compiler import compile as compile_ui_hints
|
||||
from schemas.agent.runtime_models import AgentOutput
|
||||
from schemas.domain.chat_message import (
|
||||
AgentChatMessage,
|
||||
AgentChatMessageMetadata,
|
||||
@@ -29,20 +29,20 @@ def convert_message_to_history(
|
||||
|
||||
转换规则:
|
||||
- role=user: 读取 metadata.user_message_attachments,转换为 attachments[]
|
||||
- role=assistant: 读取 metadata.agent_output.ui_hints,编译成 ui_schema
|
||||
- role=assistant: 读取 metadata.agent_output,输出受控 agent_output
|
||||
"""
|
||||
role = message.role
|
||||
content = message.content
|
||||
metadata = message.metadata
|
||||
|
||||
attachments: list[dict[str, str]] = []
|
||||
ui_schema: dict[str, Any] | None = None
|
||||
agent_output: dict[str, Any] | None = None
|
||||
|
||||
if role == "user":
|
||||
attachments = _convert_user_attachments(metadata, get_signed_url_fn)
|
||||
|
||||
elif role == "assistant":
|
||||
ui_schema = _compile_worker_ui_hints(metadata)
|
||||
agent_output = _extract_worker_agent_output(metadata)
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"id": str(message.id),
|
||||
@@ -55,8 +55,8 @@ def convert_message_to_history(
|
||||
if attachments:
|
||||
result["attachments"] = attachments
|
||||
|
||||
if ui_schema:
|
||||
result["ui_schema"] = ui_schema
|
||||
if agent_output:
|
||||
result["agent_output"] = agent_output
|
||||
|
||||
return result
|
||||
|
||||
@@ -93,10 +93,10 @@ def _convert_user_attachments(
|
||||
return signed_attachments
|
||||
|
||||
|
||||
def _compile_worker_ui_hints(
|
||||
def _extract_worker_agent_output(
|
||||
metadata: AgentChatMessageMetadata | dict[str, Any] | None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""编译 assistant 消息的 agent ui_hints"""
|
||||
"""提取 assistant 消息的结构化 agent_output。"""
|
||||
if not metadata:
|
||||
return None
|
||||
|
||||
@@ -106,29 +106,52 @@ def _compile_worker_ui_hints(
|
||||
agent_output_data = metadata.get("agent_output")
|
||||
if not agent_output_data:
|
||||
return None
|
||||
if isinstance(agent_output_data, dict):
|
||||
raw_ui_schema = agent_output_data.get("ui_schema")
|
||||
if isinstance(raw_ui_schema, dict):
|
||||
return raw_ui_schema
|
||||
from schemas.agent.runtime_models import AgentOutput
|
||||
|
||||
try:
|
||||
agent_output = AgentOutput.model_validate(agent_output_data)
|
||||
except Exception:
|
||||
return None
|
||||
normalized_payload = _normalize_agent_output_payload(agent_output_data)
|
||||
try:
|
||||
agent_output = AgentOutput.model_validate(normalized_payload)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if not agent_output:
|
||||
return None
|
||||
|
||||
ui_hints = agent_output.ui_hints
|
||||
if not ui_hints:
|
||||
return None
|
||||
payload = agent_output.model_dump(mode="json", by_alias=True, exclude_none=True)
|
||||
payload.pop("ui_hints", None)
|
||||
return payload or None
|
||||
|
||||
try:
|
||||
compiled = compile_ui_hints(ui_hints)
|
||||
return compiled
|
||||
except Exception:
|
||||
|
||||
def _normalize_agent_output_payload(agent_output_data: Any) -> dict[str, Any] | None:
|
||||
if not isinstance(agent_output_data, dict):
|
||||
return None
|
||||
normalized = dict(agent_output_data)
|
||||
derived = normalized.get("divination_derived")
|
||||
if isinstance(derived, dict):
|
||||
normalized["divination_derived"] = _normalize_divination_derived(derived)
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalize_divination_derived(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
result: dict[str, Any] = {}
|
||||
for key, item in value.items():
|
||||
normalized_key = _snake_to_camel(key)
|
||||
result[normalized_key] = _normalize_divination_derived(item)
|
||||
return result
|
||||
if isinstance(value, list):
|
||||
return [_normalize_divination_derived(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def _snake_to_camel(value: str) -> str:
|
||||
if "_" not in value:
|
||||
return value
|
||||
parts = value.split("_")
|
||||
if not parts:
|
||||
return value
|
||||
return parts[0] + "".join(part[:1].upper() + part[1:] for part in parts[1:])
|
||||
|
||||
|
||||
def mime_to_suffix(mime_type: str) -> str:
|
||||
|
||||
@@ -5,9 +5,11 @@ from fastapi import APIRouter
|
||||
from v1.agent.router import router as agent_router
|
||||
from v1.auth.router import router as auth_router
|
||||
from v1.points.router import router as points_router
|
||||
from v1.users.router import router as users_router
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/v1")
|
||||
router.include_router(auth_router)
|
||||
router.include_router(agent_router)
|
||||
router.include_router(points_router)
|
||||
router.include_router(users_router)
|
||||
|
||||
@@ -11,6 +11,7 @@ from core.auth.models import CurrentUser
|
||||
from core.db import get_db
|
||||
from core.http.errors import ApiProblemError, problem_payload
|
||||
from services.base.supabase import supabase_service
|
||||
from v1.users.repository import SQLAlchemyUserRepository
|
||||
from v1.users.service import UserService
|
||||
|
||||
|
||||
@@ -53,5 +54,8 @@ def get_user_service(
|
||||
session: Annotated[AsyncSession, Depends(get_db)],
|
||||
user: Annotated[CurrentUser, Depends(get_current_user)],
|
||||
) -> UserService:
|
||||
_ = session
|
||||
return UserService(current_user=user)
|
||||
return UserService(
|
||||
current_user=user,
|
||||
repository=SQLAlchemyUserRepository(session=session),
|
||||
attachment_storage=supabase_service,
|
||||
)
|
||||
|
||||
@@ -3,12 +3,35 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.profile import Profile
|
||||
|
||||
|
||||
@dataclass
|
||||
class SQLAlchemyUserRepository:
|
||||
session: object
|
||||
session: AsyncSession
|
||||
|
||||
async def get_by_user_ids(self, user_ids: list[UUID]) -> dict[UUID, object]:
|
||||
_ = self.session
|
||||
_ = user_ids
|
||||
return {}
|
||||
async def get_by_user_ids(self, user_ids: list[UUID]) -> dict[UUID, Profile]:
|
||||
if not user_ids:
|
||||
return {}
|
||||
stmt = (
|
||||
select(Profile)
|
||||
.where(Profile.id.in_(user_ids))
|
||||
.where(Profile.deleted_at.is_(None))
|
||||
)
|
||||
rows = (await self.session.execute(stmt)).scalars().all()
|
||||
return {row.id: row for row in rows}
|
||||
|
||||
async def get_profile_by_user_id(self, *, user_id: UUID) -> Profile | None:
|
||||
stmt = (
|
||||
select(Profile)
|
||||
.where(Profile.id == user_id)
|
||||
.where(Profile.deleted_at.is_(None))
|
||||
.limit(1)
|
||||
)
|
||||
return (await self.session.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
async def save(self) -> None:
|
||||
await self.session.commit()
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, File, UploadFile
|
||||
|
||||
from v1.users.dependencies import get_user_service
|
||||
from v1.users.schemas import (
|
||||
AvatarUploadUrlRequest,
|
||||
AvatarUploadUrlResponse,
|
||||
ProfileResponse,
|
||||
UpdateProfileRequest,
|
||||
)
|
||||
from v1.users.service import UserService
|
||||
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
@router.get("/me/profile", response_model=ProfileResponse)
|
||||
async def get_my_profile(
|
||||
service: UserService = Depends(get_user_service),
|
||||
) -> ProfileResponse:
|
||||
return await service.get_profile()
|
||||
|
||||
|
||||
@router.patch("/me/profile", response_model=ProfileResponse)
|
||||
async def update_my_profile(
|
||||
payload: UpdateProfileRequest,
|
||||
service: UserService = Depends(get_user_service),
|
||||
) -> ProfileResponse:
|
||||
return await service.update_profile(payload)
|
||||
|
||||
|
||||
@router.post("/me/avatar/upload-url", response_model=AvatarUploadUrlResponse)
|
||||
async def create_avatar_upload_url(
|
||||
payload: AvatarUploadUrlRequest,
|
||||
service: UserService = Depends(get_user_service),
|
||||
) -> AvatarUploadUrlResponse:
|
||||
raw = await service.create_avatar_upload_url(payload)
|
||||
return AvatarUploadUrlResponse.model_validate(raw)
|
||||
|
||||
|
||||
@router.post("/me/avatar", response_model=ProfileResponse)
|
||||
async def upload_avatar(
|
||||
file: UploadFile = File(...),
|
||||
service: UserService = Depends(get_user_service),
|
||||
) -> ProfileResponse:
|
||||
return await service.upload_avatar(file)
|
||||
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class ProfileResponse(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
user_id: str
|
||||
display_name: str
|
||||
bio: str | None = None
|
||||
avatar_path: str | None = None
|
||||
avatar_url: str | None = None
|
||||
settings: dict[str, Any] = Field(default_factory=dict)
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class UpdateProfileRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
display_name: str | None = Field(default=None, max_length=30)
|
||||
bio: str | None = Field(default=None, max_length=200)
|
||||
avatar_path: str | None = None
|
||||
|
||||
|
||||
class AvatarUploadUrlRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
mime_type: str
|
||||
file_size: int = Field(gt=0)
|
||||
ext: str
|
||||
|
||||
|
||||
class AvatarUploadUrlResponse(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
bucket: str
|
||||
path: str
|
||||
upload_url: str
|
||||
expires_in: int
|
||||
@@ -1,22 +1,291 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import UploadFile
|
||||
from structlog import get_logger
|
||||
|
||||
from core.config.settings import config
|
||||
from core.auth.models import CurrentUser
|
||||
from core.http.errors import ApiProblemError, problem_payload
|
||||
from services.base.supabase import SupabaseService
|
||||
from schemas.shared.user import UserContext
|
||||
from v1.users.repository import SQLAlchemyUserRepository
|
||||
from v1.users.schemas import (
|
||||
AvatarUploadUrlRequest,
|
||||
ProfileResponse,
|
||||
UpdateProfileRequest,
|
||||
)
|
||||
|
||||
|
||||
logger = get_logger("v1.users.service")
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserService:
|
||||
current_user: CurrentUser
|
||||
repository: SQLAlchemyUserRepository
|
||||
attachment_storage: SupabaseService
|
||||
|
||||
async def get_me(self) -> UserContext:
|
||||
profile = await self.repository.get_profile_by_user_id(
|
||||
user_id=self.current_user.id
|
||||
)
|
||||
user_id = str(self.current_user.id)
|
||||
return UserContext(
|
||||
id=user_id,
|
||||
username=f"user_{user_id[:8]}",
|
||||
username=profile.username if profile is not None else f"user_{user_id[:8]}",
|
||||
email=self.current_user.email,
|
||||
avatar_url=None,
|
||||
bio=None,
|
||||
settings=None,
|
||||
avatar_url=profile.avatar_url if profile is not None else None,
|
||||
bio=profile.bio if profile is not None else None,
|
||||
settings=profile.settings if profile is not None else None,
|
||||
)
|
||||
|
||||
async def get_profile(self) -> ProfileResponse:
|
||||
profile = await self.repository.get_profile_by_user_id(
|
||||
user_id=self.current_user.id
|
||||
)
|
||||
if profile is None:
|
||||
raise ApiProblemError(
|
||||
status_code=404,
|
||||
detail=problem_payload(
|
||||
code="PROFILE_NOT_FOUND",
|
||||
detail="Profile not found",
|
||||
),
|
||||
)
|
||||
avatar_url = await self._resolve_avatar_url(profile.avatar_url)
|
||||
return ProfileResponse(
|
||||
user_id=str(self.current_user.id),
|
||||
display_name=profile.username,
|
||||
bio=profile.bio,
|
||||
avatar_path=profile.avatar_url,
|
||||
avatar_url=avatar_url,
|
||||
settings=profile.settings,
|
||||
updated_at=profile.updated_at,
|
||||
)
|
||||
|
||||
async def update_profile(self, payload: UpdateProfileRequest) -> ProfileResponse:
|
||||
if (
|
||||
payload.display_name is None
|
||||
and payload.bio is None
|
||||
and payload.avatar_path is None
|
||||
):
|
||||
raise ApiProblemError(
|
||||
status_code=422,
|
||||
detail=problem_payload(
|
||||
code="PROFILE_PAYLOAD_INVALID",
|
||||
detail="At least one profile field must be provided",
|
||||
),
|
||||
)
|
||||
|
||||
profile = await self.repository.get_profile_by_user_id(
|
||||
user_id=self.current_user.id
|
||||
)
|
||||
if profile is None:
|
||||
raise ApiProblemError(
|
||||
status_code=404,
|
||||
detail=problem_payload(
|
||||
code="PROFILE_NOT_FOUND",
|
||||
detail="Profile not found",
|
||||
),
|
||||
)
|
||||
|
||||
if payload.display_name is not None:
|
||||
next_name = payload.display_name.strip()
|
||||
if not next_name:
|
||||
raise ApiProblemError(
|
||||
status_code=422,
|
||||
detail=problem_payload(
|
||||
code="PROFILE_PAYLOAD_INVALID",
|
||||
detail="display_name cannot be empty",
|
||||
),
|
||||
)
|
||||
profile.username = next_name
|
||||
|
||||
if payload.bio is not None:
|
||||
profile.bio = payload.bio.strip() or None
|
||||
|
||||
if payload.avatar_path is not None:
|
||||
expected_prefix = f"{config.storage.avatar.bucket}/{self.current_user.id}/"
|
||||
if not payload.avatar_path.startswith(expected_prefix):
|
||||
raise ApiProblemError(
|
||||
status_code=422,
|
||||
detail=problem_payload(
|
||||
code="AVATAR_PATH_SCOPE_INVALID",
|
||||
detail="Invalid avatar path scope",
|
||||
),
|
||||
)
|
||||
profile.avatar_url = payload.avatar_path
|
||||
|
||||
await self.repository.save()
|
||||
return await self.get_profile()
|
||||
|
||||
async def create_avatar_upload_url(
|
||||
self, payload: AvatarUploadUrlRequest
|
||||
) -> dict[str, str | int]:
|
||||
max_bytes = config.storage.avatar.max_size_mb * 1024 * 1024
|
||||
if payload.file_size > max_bytes:
|
||||
raise ApiProblemError(
|
||||
status_code=422,
|
||||
detail=problem_payload(
|
||||
code="AVATAR_FILE_INVALID",
|
||||
detail="Avatar file size exceeds limit",
|
||||
),
|
||||
)
|
||||
|
||||
if payload.mime_type not in {"image/png", "image/jpeg", "image/webp"}:
|
||||
raise ApiProblemError(
|
||||
status_code=422,
|
||||
detail=problem_payload(
|
||||
code="AVATAR_FILE_INVALID",
|
||||
detail="Avatar mime type not allowed",
|
||||
),
|
||||
)
|
||||
|
||||
ext = payload.ext.lower().strip()
|
||||
if ext not in {"png", "jpg", "jpeg", "webp"}:
|
||||
raise ApiProblemError(
|
||||
status_code=422,
|
||||
detail=problem_payload(
|
||||
code="AVATAR_FILE_INVALID",
|
||||
detail="Avatar extension not allowed",
|
||||
),
|
||||
)
|
||||
|
||||
bucket = config.storage.avatar.bucket
|
||||
storage_path = f"{self.current_user.id}/{uuid4()}.{ext}"
|
||||
try:
|
||||
upload_url = await self.attachment_storage.create_signed_url(
|
||||
bucket=bucket,
|
||||
path=storage_path,
|
||||
expires_in_seconds=config.storage.signed_url_ttl_seconds,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise ApiProblemError(
|
||||
status_code=502,
|
||||
detail=problem_payload(
|
||||
code="AVATAR_SIGNED_URL_FAILED",
|
||||
detail="Failed to generate avatar signed URL",
|
||||
),
|
||||
) from exc
|
||||
|
||||
return {
|
||||
"bucket": bucket,
|
||||
"path": f"{bucket}/{storage_path}",
|
||||
"upload_url": upload_url,
|
||||
"expires_in": config.storage.signed_url_ttl_seconds,
|
||||
}
|
||||
|
||||
async def upload_avatar(self, upload: UploadFile) -> ProfileResponse:
|
||||
profile = await self.repository.get_profile_by_user_id(
|
||||
user_id=self.current_user.id
|
||||
)
|
||||
if profile is None:
|
||||
raise ApiProblemError(
|
||||
status_code=404,
|
||||
detail=problem_payload(
|
||||
code="PROFILE_NOT_FOUND",
|
||||
detail="Profile not found",
|
||||
),
|
||||
)
|
||||
|
||||
filename = upload.filename or "avatar"
|
||||
ext = Path(filename).suffix.lower().lstrip(".")
|
||||
if ext not in {"png", "jpg", "jpeg", "webp"}:
|
||||
raise ApiProblemError(
|
||||
status_code=422,
|
||||
detail=problem_payload(
|
||||
code="AVATAR_FILE_INVALID",
|
||||
detail="Avatar extension not allowed",
|
||||
),
|
||||
)
|
||||
|
||||
mime_type = (upload.content_type or "").lower().strip()
|
||||
if mime_type not in {"image/png", "image/jpeg", "image/webp"}:
|
||||
if ext == "png":
|
||||
mime_type = "image/png"
|
||||
elif ext in {"jpg", "jpeg"}:
|
||||
mime_type = "image/jpeg"
|
||||
elif ext == "webp":
|
||||
mime_type = "image/webp"
|
||||
else:
|
||||
raise ApiProblemError(
|
||||
status_code=422,
|
||||
detail=problem_payload(
|
||||
code="AVATAR_FILE_INVALID",
|
||||
detail="Avatar mime type not allowed",
|
||||
),
|
||||
)
|
||||
|
||||
content = await upload.read()
|
||||
if not content:
|
||||
raise ApiProblemError(
|
||||
status_code=422,
|
||||
detail=problem_payload(
|
||||
code="AVATAR_FILE_INVALID",
|
||||
detail="Avatar content is empty",
|
||||
),
|
||||
)
|
||||
|
||||
max_bytes = config.storage.avatar.max_size_mb * 1024 * 1024
|
||||
if len(content) > max_bytes:
|
||||
raise ApiProblemError(
|
||||
status_code=422,
|
||||
detail=problem_payload(
|
||||
code="AVATAR_FILE_INVALID",
|
||||
detail="Avatar file size exceeds limit",
|
||||
),
|
||||
)
|
||||
|
||||
bucket = config.storage.avatar.bucket
|
||||
storage_path = f"{self.current_user.id}/{uuid4()}.{ext}"
|
||||
try:
|
||||
await self.attachment_storage.upload_bytes(
|
||||
bucket=bucket,
|
||||
path=storage_path,
|
||||
content=content,
|
||||
content_type=mime_type,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
"Avatar upload to storage failed",
|
||||
user_id=str(self.current_user.id),
|
||||
bucket=bucket,
|
||||
path=storage_path,
|
||||
mime_type=mime_type,
|
||||
size_bytes=len(content),
|
||||
)
|
||||
raise ApiProblemError(
|
||||
status_code=502,
|
||||
detail=problem_payload(
|
||||
code="AVATAR_UPLOAD_FAILED",
|
||||
detail="Failed to upload avatar",
|
||||
),
|
||||
) from exc
|
||||
|
||||
profile.avatar_url = f"{bucket}/{storage_path}"
|
||||
await self.repository.save()
|
||||
return await self.get_profile()
|
||||
|
||||
async def _resolve_avatar_url(self, avatar_path: str | None) -> str | None:
|
||||
if avatar_path is None:
|
||||
return None
|
||||
normalized = avatar_path.strip()
|
||||
if not normalized:
|
||||
return None
|
||||
parts = normalized.split("/", 1)
|
||||
if len(parts) != 2:
|
||||
return normalized
|
||||
bucket, path = parts
|
||||
if bucket != config.storage.avatar.bucket:
|
||||
return normalized
|
||||
try:
|
||||
return await self.attachment_storage.create_signed_url(
|
||||
bucket=bucket,
|
||||
path=path,
|
||||
expires_in_seconds=config.storage.signed_url_ttl_seconds,
|
||||
)
|
||||
except Exception:
|
||||
return normalized
|
||||
|
||||
Reference in New Issue
Block a user