prepare_browser_waiting.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import argparse
  2. import asyncio
  3. import hashlib
  4. import json
  5. import secrets
  6. from datetime import timedelta
  7. from pathlib import Path
  8. from uuid import UUID, uuid4
  9. from langchain_core.messages import AIMessage, message_to_dict
  10. from sqlalchemy import select, update
  11. from zhifu.app import create_app
  12. from zhifu.core.config import Settings
  13. from zhifu.modules.after_sales.models import OrderProjection
  14. from zhifu.modules.agent_runtime.config import GRAPH_VERSION
  15. from zhifu.modules.agent_runtime.models import AgentRun
  16. from zhifu.modules.agent_runtime.runtime import build_tool_graph
  17. from zhifu.modules.conversations.models import Conversation
  18. from zhifu.modules.identity.access_models import AccessSession, Principal, StoreGrant
  19. from zhifu.modules.identity.auth import now, password_hasher
  20. OUTPUT = Path(".work/frontend-waiting.json")
  21. async def main(cleanup=False, business=False):
  22. output = Path(".work/frontend-business-waiting.json") if business else OUTPUT
  23. settings = Settings()
  24. if settings.environment == "production":
  25. raise RuntimeError("浏览器验证数据只能在独立开发环境准备")
  26. app = create_app(settings)
  27. async with app.router.lifespan_context(app):
  28. if cleanup:
  29. data = json.loads(await asyncio.to_thread(output.read_text, encoding="utf-8"))
  30. async with app.state.sessions.begin() as session:
  31. row = await session.get(
  32. Principal, UUID(data["actor_id"]).bytes, with_for_update=True)
  33. if row is None or not row.username.startswith("waiting-test-"):
  34. raise RuntimeError("待清理账户不属于本专项")
  35. row.status = "SUSPENDED"
  36. await session.execute(update(AccessSession).where(
  37. AccessSession.principal_id == row.id).values(revoked=True))
  38. await asyncio.to_thread(output.unlink)
  39. print("浏览器等待输入专项账户已停用,会话已撤销,临时凭据已删除")
  40. return
  41. if await asyncio.to_thread(output.exists):
  42. raise RuntimeError("请先清理已有浏览器等待输入专项账户")
  43. data = json.loads(await asyncio.to_thread(
  44. Path(".work/frontend-accounts.json").read_text, encoding="utf-8"))
  45. tenant_id, store_id = UUID(data["tenant_id"]), UUID(data["store_id"])
  46. actor, conversation_id, run_id, order_id = uuid4(), uuid4(), uuid4(), uuid4()
  47. username = "waiting-test-" + actor.hex[:12]
  48. password = secrets.token_urlsafe(24)
  49. token_hash = hashlib.sha256(secrets.token_bytes(32)).digest()
  50. question = "请补充键盘故障原因与申请数量。"
  51. tool_name = "create_exchange_proposal" if business else "request_clarification"
  52. tool_args = {
  53. "order_id": str(order_id), "item_id": "keyboard-1", "quantity": 1, "reason": "按键失灵",
  54. } if business else {
  55. "question": question, "missing_fields": ["quantity", "reason"],
  56. "candidate_order_ids": [str(order_id)],
  57. }
  58. async with app.state.sessions.begin() as session:
  59. session.add(Principal(id=actor.bytes, tenant_id=tenant_id.bytes, username=username,
  60. password_hash=password_hasher.hash(password), kind="CUSTOMER"))
  61. await session.flush()
  62. for permission in (
  63. "orders:read", "tickets:read", "after_sales:create", "after_sales:confirm"
  64. ):
  65. session.add(StoreGrant(tenant_id=tenant_id.bytes, principal_id=actor.bytes,
  66. store_id=store_id.bytes, permission=permission))
  67. session.add(AccessSession(token_hash=token_hash, tenant_id=tenant_id.bytes,
  68. principal_id=actor.bytes, expires_at=now() + timedelta(hours=1), revoked=False))
  69. session.add(OrderProjection(id=order_id.bytes, tenant_id=tenant_id.bytes,
  70. store_id=store_id.bytes, subject_id=actor.bytes, source_system="browser-acceptance",
  71. source_version=1, item_id="keyboard-1", product_type="keyboard",
  72. available_quantity=1,
  73. signed_at=now(), observed_at=now()))
  74. conversation = Conversation(id=conversation_id.bytes, tenant_id=tenant_id.bytes,
  75. store_id=store_id.bytes, subject_id=actor.bytes, request_key=uuid4().hex,
  76. created_at=now())
  77. session.add(conversation)
  78. await session.flush()
  79. # 直接执行已注册图的工具节点,浏览器验收不生成或替代模型输出。
  80. run = AgentRun(id=run_id.bytes, tenant_id=tenant_id.bytes, actor_id=actor.bytes,
  81. store_id=store_id.bytes, conversation_id=conversation_id.bytes, control_version=1,
  82. token_hash=token_hash, request_key=uuid4().hex,
  83. request_hash=hashlib.sha256(b"waiting-test").digest(),
  84. messages=[message_to_dict(AIMessage(content="", tool_calls=[{
  85. "name": tool_name, "args": tool_args,
  86. "id": uuid4().hex, "type": "tool_call"}]))],
  87. graph_version=GRAPH_VERSION, status="RUNNING", next_node="tools", model_name="",
  88. model_url="", model_credential_env="", lease_token=1,
  89. lease_until=now() + timedelta(seconds=125), created_at=now(),
  90. deadline_at=now() + timedelta(minutes=10))
  91. session.add(run)
  92. await session.flush()
  93. conversation.active_run_id = run.id
  94. await build_tool_graph(app.state.sessions).ainvoke({"run_id": run_id.bytes, "token": 1})
  95. async with app.state.sessions() as session:
  96. current = await session.scalar(select(AgentRun).where(AgentRun.id == run_id.bytes))
  97. if current.status != "WAITING_USER" or current.model_calls != 0:
  98. raise RuntimeError("工具节点未建立预期等待状态")
  99. await asyncio.to_thread(output.write_text, json.dumps({
  100. "tenant_id": str(tenant_id), "store_id": str(store_id),
  101. "actor_id": str(actor), "username": username, "password": password,
  102. "conversation_id": str(conversation_id), "run_id": str(run_id),
  103. "question": current.waiting["question"],
  104. "proposal_id": current.waiting.get("proposal_id")},
  105. ensure_ascii=False, indent=2), encoding="utf-8")
  106. print("浏览器等待输入资料已保存,生成调用0次")
  107. if __name__ == "__main__":
  108. parser = argparse.ArgumentParser(description="准备或清理真实浏览器等待输入验收资料")
  109. parser.add_argument("--cleanup", action="store_true")
  110. parser.add_argument("--business", action="store_true")
  111. options = parser.parse_args()
  112. asyncio.run(main(options.cleanup, options.business))