| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 |
- import argparse
- import asyncio
- import hashlib
- import json
- import secrets
- from datetime import timedelta
- from pathlib import Path
- from uuid import UUID, uuid4
- from langchain_core.messages import AIMessage, message_to_dict
- from sqlalchemy import select, update
- from zhifu.app import create_app
- from zhifu.core.config import Settings
- from zhifu.modules.after_sales.models import OrderProjection
- from zhifu.modules.agent_runtime.config import GRAPH_VERSION
- from zhifu.modules.agent_runtime.models import AgentRun
- from zhifu.modules.agent_runtime.runtime import build_tool_graph
- from zhifu.modules.conversations.models import Conversation
- from zhifu.modules.identity.access_models import AccessSession, Principal, StoreGrant
- from zhifu.modules.identity.auth import now, password_hasher
- OUTPUT = Path(".work/frontend-waiting.json")
- async def main(cleanup=False, business=False):
- output = Path(".work/frontend-business-waiting.json") if business else OUTPUT
- settings = Settings()
- if settings.environment == "production":
- raise RuntimeError("浏览器验证数据只能在独立开发环境准备")
- app = create_app(settings)
- async with app.router.lifespan_context(app):
- if cleanup:
- data = json.loads(await asyncio.to_thread(output.read_text, encoding="utf-8"))
- async with app.state.sessions.begin() as session:
- row = await session.get(
- Principal, UUID(data["actor_id"]).bytes, with_for_update=True)
- if row is None or not row.username.startswith("waiting-test-"):
- raise RuntimeError("待清理账户不属于本专项")
- row.status = "SUSPENDED"
- await session.execute(update(AccessSession).where(
- AccessSession.principal_id == row.id).values(revoked=True))
- await asyncio.to_thread(output.unlink)
- print("浏览器等待输入专项账户已停用,会话已撤销,临时凭据已删除")
- return
- if await asyncio.to_thread(output.exists):
- raise RuntimeError("请先清理已有浏览器等待输入专项账户")
- data = json.loads(await asyncio.to_thread(
- Path(".work/frontend-accounts.json").read_text, encoding="utf-8"))
- tenant_id, store_id = UUID(data["tenant_id"]), UUID(data["store_id"])
- actor, conversation_id, run_id, order_id = uuid4(), uuid4(), uuid4(), uuid4()
- username = "waiting-test-" + actor.hex[:12]
- password = secrets.token_urlsafe(24)
- token_hash = hashlib.sha256(secrets.token_bytes(32)).digest()
- question = "请补充键盘故障原因与申请数量。"
- tool_name = "create_exchange_proposal" if business else "request_clarification"
- tool_args = {
- "order_id": str(order_id), "item_id": "keyboard-1", "quantity": 1, "reason": "按键失灵",
- } if business else {
- "question": question, "missing_fields": ["quantity", "reason"],
- "candidate_order_ids": [str(order_id)],
- }
- async with app.state.sessions.begin() as session:
- session.add(Principal(id=actor.bytes, tenant_id=tenant_id.bytes, username=username,
- password_hash=password_hasher.hash(password), kind="CUSTOMER"))
- await session.flush()
- for permission in (
- "orders:read", "tickets:read", "after_sales:create", "after_sales:confirm"
- ):
- session.add(StoreGrant(tenant_id=tenant_id.bytes, principal_id=actor.bytes,
- store_id=store_id.bytes, permission=permission))
- session.add(AccessSession(token_hash=token_hash, tenant_id=tenant_id.bytes,
- principal_id=actor.bytes, expires_at=now() + timedelta(hours=1), revoked=False))
- session.add(OrderProjection(id=order_id.bytes, tenant_id=tenant_id.bytes,
- store_id=store_id.bytes, subject_id=actor.bytes, source_system="browser-acceptance",
- source_version=1, item_id="keyboard-1", product_type="keyboard",
- available_quantity=1,
- signed_at=now(), observed_at=now()))
- conversation = Conversation(id=conversation_id.bytes, tenant_id=tenant_id.bytes,
- store_id=store_id.bytes, subject_id=actor.bytes, request_key=uuid4().hex,
- created_at=now())
- session.add(conversation)
- await session.flush()
- # 直接执行已注册图的工具节点,浏览器验收不生成或替代模型输出。
- run = AgentRun(id=run_id.bytes, tenant_id=tenant_id.bytes, actor_id=actor.bytes,
- store_id=store_id.bytes, conversation_id=conversation_id.bytes, control_version=1,
- token_hash=token_hash, request_key=uuid4().hex,
- request_hash=hashlib.sha256(b"waiting-test").digest(),
- messages=[message_to_dict(AIMessage(content="", tool_calls=[{
- "name": tool_name, "args": tool_args,
- "id": uuid4().hex, "type": "tool_call"}]))],
- graph_version=GRAPH_VERSION, status="RUNNING", next_node="tools", model_name="",
- model_url="", model_credential_env="", lease_token=1,
- lease_until=now() + timedelta(seconds=125), created_at=now(),
- deadline_at=now() + timedelta(minutes=10))
- session.add(run)
- await session.flush()
- conversation.active_run_id = run.id
- await build_tool_graph(app.state.sessions).ainvoke({"run_id": run_id.bytes, "token": 1})
- async with app.state.sessions() as session:
- current = await session.scalar(select(AgentRun).where(AgentRun.id == run_id.bytes))
- if current.status != "WAITING_USER" or current.model_calls != 0:
- raise RuntimeError("工具节点未建立预期等待状态")
- await asyncio.to_thread(output.write_text, json.dumps({
- "tenant_id": str(tenant_id), "store_id": str(store_id),
- "actor_id": str(actor), "username": username, "password": password,
- "conversation_id": str(conversation_id), "run_id": str(run_id),
- "question": current.waiting["question"],
- "proposal_id": current.waiting.get("proposal_id")},
- ensure_ascii=False, indent=2), encoding="utf-8")
- print("浏览器等待输入资料已保存,生成调用0次")
- if __name__ == "__main__":
- parser = argparse.ArgumentParser(description="准备或清理真实浏览器等待输入验收资料")
- parser.add_argument("--cleanup", action="store_true")
- parser.add_argument("--business", action="store_true")
- options = parser.parse_args()
- asyncio.run(main(options.cleanup, options.business))
|