diff --git a/.claude/settings.json b/.claude/settings.json index e02ee52c..e6bc4a38 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -4,7 +4,8 @@ "PowerShell(uv --version)", "PowerShell(netstat -ano)", "PowerShell(Get-NetTCPConnection -LocalPort 8001 -ErrorAction SilentlyContinue)", - "PowerShell(Get-Process -Id 20560 -ErrorAction SilentlyContinue | Select-Object Id,ProcessName,Path,StartTime; try { \\(Invoke-WebRequest -Uri \"http://127.0.0.1:8001/health\" -UseBasicParsing -TimeoutSec 5\\).Content } catch { \"health check failed: $_\" })" + "PowerShell(Get-Process -Id 20560 -ErrorAction SilentlyContinue | Select-Object Id,ProcessName,Path,StartTime; try { \\(Invoke-WebRequest -Uri \"http://127.0.0.1:8001/health\" -UseBasicParsing -TimeoutSec 5\\).Content } catch { \"health check failed: $_\" })", + "Bash(curl -s -X POST -H \"Content-Type: application/json\" -d '{\"username\":\"demo\"}' http://localhost:8001/api/v1/auth/login/username)" ] } } diff --git a/offline-backend-20260512/.env b/offline-backend-20260512/.env index 91938f1e..ca19078c 100644 --- a/offline-backend-20260512/.env +++ b/offline-backend-20260512/.env @@ -17,7 +17,7 @@ SERPER_API_KEY=your-serper-api-key # AUTH_JWT_SECRET must stay stable across restarts, or existing logins expire. # GATEWAY_CORS_ORIGINS must include the exact frontend origins, separated by commas. AUTH_JWT_SECRET=i3_BiBK44sYHiFIkChhDs46y57ZewJKO3diQDw01anI -GATEWAY_CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,http://localhost:5174,http://127.0.0.1:5174,http://localhost:4200 +GATEWAY_CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,http://localhost:5183,http://localhost:5174,http://127.0.0.1:5174,http://localhost:4200 GATEWAY_CORS_ALLOW_ALL=0 GATEWAY_ENABLE_DOCS=true GATEWAY_WORKERS=2 diff --git a/offline-backend-20260512/backend/_reqs.txt b/offline-backend-20260512/backend/_reqs.txt new file mode 100644 index 00000000..2bd4a9cf --- /dev/null +++ b/offline-backend-20260512/backend/_reqs.txt @@ -0,0 +1,760 @@ +# This file was autogenerated by uv via the following command: +# uv export --no-hashes --no-emit-project --no-dev -o _reqs.txt +-e ./packages/harness + # via deer-flow +agent-client-protocol==0.10.1 + # via deerflow-harness +agent-sandbox==0.0.30 + # via deerflow-harness +aiohappyeyeballs==2.6.2 + # via aiohttp +aiohttp==3.14.1 + # via + # aiohttp-retry + # dingtalk-stream + # firecrawl-py + # hindsight-client + # kubernetes + # wecom-aibot-python-sdk +aiohttp-retry==2.9.1 + # via hindsight-client +aiosignal==1.4.0 + # via aiohttp +aiosqlite==0.22.1 + # via + # deerflow-harness + # langgraph-checkpoint-sqlite +alembic==1.18.4 + # via deerflow-harness +annotated-doc==0.0.4 + # via fastapi +annotated-types==0.7.0 + # via pydantic +anthropic==0.107.1 + # via langchain-anthropic +anyio==4.13.0 + # via + # anthropic + # google-genai + # httpx + # mcp + # openai + # sse-starlette + # starlette + # watchfiles +asyncmy==0.2.11 + # via deer-flow +attrs==26.1.0 + # via + # aiohttp + # jsonschema + # referencing +audioop-lts==0.2.2 ; python_full_version >= '3.13' + # via + # speechrecognition + # standard-aifc +azure-ai-documentintelligence==1.0.2 + # via markitdown +azure-core==1.41.0 + # via + # azure-ai-documentintelligence + # azure-identity +azure-identity==1.25.3 + # via markitdown +backoff==2.2.1 + # via langfuse +bcrypt==5.0.0 + # via deer-flow +beautifulsoup4==4.15.0 + # via + # markdownify + # markitdown + # readabilipy +blockbuster==1.5.26 + # via langgraph-runtime-inmem +brotli==1.2.0 ; platform_python_implementation == 'CPython' + # via httpx +brotlicffi==1.2.0.1 ; platform_python_implementation != 'CPython' + # via httpx +certifi==2026.5.20 + # via + # httpcore + # httpx + # kubernetes + # requests + # volcengine-python-sdk + # wecom-aibot-python-sdk +cffi==2.0.0 + # via + # brotlicffi + # cryptography +charset-normalizer==3.4.7 + # via + # markitdown + # pdfminer-six + # requests +click==8.4.1 + # via + # ddgs + # langgraph-cli + # magika + # uvicorn +cloudpickle==3.1.2 + # via langgraph-api +cobble==0.1.4 + # via mammoth +colorama==0.4.6 ; sys_platform == 'win32' + # via + # click + # tqdm + # uvicorn +coloredlogs==15.0.1 + # via onnxruntime +croniter==6.2.2 + # via + # deer-flow + # langgraph-runtime-inmem +cryptography==48.0.0 + # via + # azure-identity + # google-auth + # langgraph-api + # msal + # pdfminer-six + # pyjwt + # wecom-aibot-python-sdk +ddgs==9.14.4 + # via deerflow-harness +defusedxml==0.7.1 + # via + # markitdown + # youtube-transcript-api +dingtalk-stream==0.24.3 + # via deer-flow +distro==1.9.0 + # via + # anthropic + # google-genai + # openai +dnspython==2.8.0 + # via email-validator +docstring-parser==0.18.0 + # via anthropic +dotenv==0.9.9 + # via deerflow-harness +duckdb==1.5.3 + # via deerflow-harness +durationpy==0.10 + # via kubernetes +email-validator==2.3.0 + # via deer-flow +et-xmlfile==2.0.0 + # via openpyxl +exa-py==2.13.0 + # via deerflow-harness +fake-useragent==2.2.0 + # via ddgs +fastapi==0.136.3 + # via deer-flow +filetype==1.2.0 + # via langchain-google-genai +firecrawl-py==4.28.2 + # via deerflow-harness +flatbuffers==25.12.19 + # via onnxruntime +forbiddenfruit==0.1.4 ; implementation_name == 'cpython' + # via blockbuster +frozenlist==1.8.0 + # via + # aiohttp + # aiosignal +google-auth==2.53.0 + # via google-genai +google-genai==2.8.0 + # via langchain-google-genai +googleapis-common-protos==1.75.0 + # via opentelemetry-exporter-otlp-proto-http +greenlet==3.5.1 + # via sqlalchemy +grpcio==1.80.0 + # via + # grpcio-health-checking + # grpcio-tools + # langgraph-api +grpcio-health-checking==1.80.0 + # via langgraph-api +grpcio-tools==1.80.0 + # via langgraph-api +h11==0.16.0 + # via + # httpcore + # uvicorn +h2==4.3.0 + # via httpx +hindsight-client==0.7.2 + # via deer-flow +hpack==4.1.0 + # via h2 +html5lib==1.1 + # via readabilipy +httpcore==1.0.9 + # via + # exa-py + # httpx + # python-telegram-bot +httptools==0.8.0 + # via + # langgraph-api + # uvicorn +httpx==0.28.1 + # via + # agent-sandbox + # anthropic + # ddgs + # deer-flow + # deerflow-harness + # exa-py + # firecrawl-py + # google-genai + # langfuse + # langgraph-api + # langgraph-cli + # langgraph-sdk + # langsmith + # lark-oapi + # mcp + # openai + # python-telegram-bot + # tavily-python +httpx-sse==0.4.3 + # via mcp +humanfriendly==10.0 + # via coloredlogs +hyperframe==6.1.0 + # via h2 +idna==3.18 + # via + # anyio + # email-validator + # httpx + # requests + # yarl +isodate==0.7.2 + # via azure-ai-documentintelligence +jiter==0.15.0 + # via + # anthropic + # openai +jsonpatch==1.33 + # via langchain-core +jsonpointer==3.1.1 + # via jsonpatch +jsonschema==4.26.0 + # via mcp +jsonschema-rs==0.44.1 + # via langgraph-api +jsonschema-specifications==2025.9.1 + # via jsonschema +kubernetes==36.0.2 + # via deerflow-harness +langchain==1.3.4 + # via deerflow-harness +langchain-anthropic==1.4.4 + # via deerflow-harness +langchain-core==1.4.1 + # via + # langchain + # langchain-anthropic + # langchain-deepseek + # langchain-google-genai + # langchain-mcp-adapters + # langchain-openai + # langgraph + # langgraph-api + # langgraph-checkpoint + # langgraph-prebuilt + # langgraph-sdk +langchain-deepseek==1.1.0 + # via deerflow-harness +langchain-google-genai==4.2.4 + # via deerflow-harness +langchain-mcp-adapters==0.2.2 + # via deerflow-harness +langchain-openai==1.2.2 + # via + # deerflow-harness + # langchain-deepseek +langchain-protocol==0.0.16 + # via + # langchain-core + # langgraph-api + # langgraph-sdk +langfuse==4.7.1 + # via deerflow-harness +langgraph==1.2.4 + # via + # deerflow-harness + # langchain + # langgraph-api + # langgraph-runtime-inmem +langgraph-api==0.9.0 + # via deerflow-harness +langgraph-checkpoint==4.1.1 + # via + # langgraph + # langgraph-api + # langgraph-checkpoint-postgres + # langgraph-checkpoint-sqlite + # langgraph-prebuilt + # langgraph-runtime-inmem +langgraph-checkpoint-postgres==3.1.0 + # via deerflow-harness +langgraph-checkpoint-sqlite==3.1.0 + # via deerflow-harness +langgraph-cli==0.4.27 + # via deerflow-harness +langgraph-prebuilt==1.1.0 + # via langgraph +langgraph-runtime-inmem==0.29.0 + # via + # deerflow-harness + # langgraph-api +langgraph-sdk==0.4.2 + # via + # deer-flow + # deerflow-harness + # langgraph + # langgraph-api + # langgraph-cli +langsmith==0.8.9 + # via + # langchain-core + # langgraph-api +lark-oapi==1.6.8 + # via deer-flow +lxml==6.1.1 + # via + # ddgs + # markitdown + # python-pptx + # readabilipy +magika==0.6.3 + # via markitdown +mako==1.3.12 + # via alembic +mammoth==1.11.0 + # via markitdown +markdown-to-mrkdwn==0.3.3 + # via deer-flow +markdownify==1.2.2 + # via + # deerflow-harness + # markitdown +markitdown==0.1.5 + # via deerflow-harness +markupsafe==3.0.3 + # via mako +mcp==1.27.2 + # via langchain-mcp-adapters +mpmath==1.3.0 + # via sympy +msal==1.37.0 + # via + # azure-identity + # msal-extensions +msal-extensions==1.3.1 + # via azure-identity +multidict==6.7.1 + # via + # aiohttp + # yarl +nest-asyncio==1.6.0 + # via firecrawl-py +numpy==2.4.6 + # via + # magika + # onnxruntime + # pandas +oauthlib==3.3.1 + # via requests-oauthlib +olefile==0.47 + # via markitdown +onnxruntime==1.20.1 + # via magika +openai==2.41.0 + # via + # exa-py + # langchain-openai +openpyxl==3.1.5 + # via markitdown +opentelemetry-api==1.42.1 + # via + # langfuse + # langgraph-api + # langsmith + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-sdk + # opentelemetry-semantic-conventions +opentelemetry-exporter-otlp-proto-common==1.42.1 + # via opentelemetry-exporter-otlp-proto-http +opentelemetry-exporter-otlp-proto-http==1.42.1 + # via + # langfuse + # langgraph-api + # langsmith +opentelemetry-proto==1.42.1 + # via + # opentelemetry-exporter-otlp-proto-common + # opentelemetry-exporter-otlp-proto-http +opentelemetry-sdk==1.42.1 + # via + # langfuse + # langgraph-api + # langsmith + # opentelemetry-exporter-otlp-proto-http +opentelemetry-semantic-conventions==0.63b1 + # via opentelemetry-sdk +orjson==3.11.9 + # via + # langgraph-api + # langgraph-checkpoint-postgres + # langgraph-sdk + # langsmith +ormsgpack==1.12.2 + # via langgraph-checkpoint +packaging==26.2 + # via + # langchain-core + # langfuse + # langsmith + # onnxruntime +pandas==3.0.3 + # via markitdown +pathspec==1.1.1 + # via langgraph-cli +pdfminer-six==20251230 + # via + # markitdown + # pdfplumber +pdfplumber==0.11.9 + # via markitdown +pillow==12.2.0 + # via + # pdfplumber + # python-pptx +primp==1.3.1 + # via ddgs +propcache==0.5.2 + # via + # aiohttp + # yarl +protobuf==6.33.6 + # via + # googleapis-common-protos + # grpcio-health-checking + # grpcio-tools + # langgraph-api + # onnxruntime + # opentelemetry-proto +psycopg==3.3.4 + # via + # deerflow-harness + # langgraph-checkpoint-postgres +psycopg-binary==3.3.4 ; implementation_name != 'pypy' + # via psycopg +psycopg-pool==3.3.1 + # via + # deerflow-harness + # langgraph-checkpoint-postgres +pyasn1==0.6.3 + # via pyasn1-modules +pyasn1-modules==0.4.2 + # via google-auth +pycparser==3.0 ; implementation_name != 'PyPy' + # via cffi +pycryptodome==3.23.0 + # via lark-oapi +pydantic==2.13.4 + # via + # agent-client-protocol + # agent-sandbox + # anthropic + # deerflow-harness + # exa-py + # fastapi + # firecrawl-py + # google-genai + # hindsight-client + # langchain + # langchain-anthropic + # langchain-core + # langchain-google-genai + # langfuse + # langgraph + # langsmith + # mcp + # openai + # pydantic-settings +pydantic-core==2.46.4 + # via pydantic +pydantic-settings==2.14.1 + # via mcp +pydub==0.25.1 + # via markitdown +pyee==13.0.1 + # via wecom-aibot-python-sdk +pyjwt==2.13.0 + # via + # deer-flow + # langgraph-api + # mcp + # msal +pypdfium2==5.9.0 + # via pdfplumber +pyreadline3==3.5.6 ; sys_platform == 'win32' + # via humanfriendly +python-dateutil==2.9.0.post0 + # via + # croniter + # hindsight-client + # kubernetes + # pandas + # volcengine-python-sdk +python-dotenv==1.2.2 + # via + # dotenv + # exa-py + # firecrawl-py + # langgraph-cli + # magika + # pydantic-settings + # uvicorn +python-multipart==0.0.32 + # via + # deer-flow + # mcp +python-pptx==1.0.2 + # via markitdown +python-telegram-bot==22.7 + # via deer-flow +pywin32==312 ; sys_platform == 'win32' + # via mcp +pyyaml==6.0.3 + # via + # deerflow-harness + # kubernetes + # langchain-core + # uvicorn +readabilipy==0.3.0 + # via deerflow-harness +referencing==0.37.0 + # via + # jsonschema + # jsonschema-specifications +regex==2026.5.9 + # via + # readabilipy + # tiktoken +requests==2.34.2 + # via + # azure-core + # dingtalk-stream + # exa-py + # firecrawl-py + # google-auth + # google-genai + # kubernetes + # langsmith + # lark-oapi + # markitdown + # msal + # opentelemetry-exporter-otlp-proto-http + # requests-oauthlib + # requests-toolbelt + # tavily-python + # tiktoken + # youtube-transcript-api +requests-oauthlib==2.0.0 + # via kubernetes +requests-toolbelt==1.0.0 + # via + # langsmith + # lark-oapi +rpds-py==2026.5.1 + # via + # jsonschema + # referencing +setuptools==82.0.1 + # via grpcio-tools +six==1.17.0 + # via + # html5lib + # kubernetes + # markdownify + # python-dateutil + # volcengine-python-sdk +slack-sdk==3.42.0 + # via deer-flow +sniffio==1.3.1 + # via + # anthropic + # google-genai + # openai +socksio==1.0.0 + # via httpx +soupsieve==2.8.4 + # via beautifulsoup4 +speechrecognition==3.16.1 + # via markitdown +sqlalchemy==2.0.50 + # via + # alembic + # deerflow-harness +sqlite-vec==0.1.9 + # via langgraph-checkpoint-sqlite +sse-starlette==3.3.4 + # via + # deer-flow + # langgraph-api + # langgraph-runtime-inmem + # mcp +standard-aifc==3.13.0 ; python_full_version >= '3.13' + # via speechrecognition +standard-chunk==3.13.0 ; python_full_version >= '3.13' + # via standard-aifc +starlette==1.2.1 + # via + # fastapi + # langgraph-api + # langgraph-runtime-inmem + # mcp + # sse-starlette +structlog==25.5.0 + # via + # langgraph-api + # langgraph-runtime-inmem +sympy==1.14.0 + # via onnxruntime +tavily-python==0.7.25 + # via deerflow-harness +tenacity==9.1.4 + # via + # google-genai + # langchain-core + # langgraph-api +tiktoken==0.13.0 + # via + # deerflow-harness + # langchain-openai + # tavily-python +tqdm==4.68.1 + # via openai +truststore==0.10.4 + # via langgraph-api +typing-extensions==4.15.0 + # via + # aiohttp + # aiosignal + # alembic + # anthropic + # anyio + # azure-ai-documentintelligence + # azure-core + # azure-identity + # beautifulsoup4 + # exa-py + # fastapi + # google-genai + # grpcio + # hindsight-client + # langchain-core + # langchain-mcp-adapters + # langchain-protocol + # mcp + # openai + # opentelemetry-api + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-sdk + # opentelemetry-semantic-conventions + # psycopg + # psycopg-pool + # pydantic + # pydantic-core + # pyee + # python-pptx + # referencing + # speechrecognition + # sqlalchemy + # starlette + # typing-inspection +typing-inspection==0.4.2 + # via + # fastapi + # mcp + # pydantic + # pydantic-settings +tzdata==2026.2 ; sys_platform == 'emscripten' or sys_platform == 'win32' + # via + # pandas + # psycopg +urllib3==2.7.0 + # via + # hindsight-client + # kubernetes + # requests + # volcengine-python-sdk +uuid-utils==0.16.0 + # via + # langchain-core + # langgraph-api + # langsmith +uvicorn==0.49.0 + # via + # deer-flow + # langgraph-api + # mcp +uvloop==0.22.1 ; sys_platform != 'win32' + # via + # langgraph-api + # uvicorn +volcengine-python-sdk==5.0.33 + # via agent-sandbox +watchfiles==1.2.0 + # via + # langgraph-api + # uvicorn +webencodings==0.5.1 + # via html5lib +websocket-client==1.9.0 + # via kubernetes +websockets==15.0.1 + # via + # dingtalk-stream + # firecrawl-py + # google-genai + # langgraph-api + # langgraph-sdk + # langsmith + # lark-oapi + # uvicorn + # wecom-aibot-python-sdk +wecom-aibot-python-sdk==1.0.2 + # via deer-flow +wrapt==1.17.3 + # via langfuse +xlrd==2.0.2 + # via markitdown +xlsxwriter==3.2.9 + # via python-pptx +xxhash==3.7.0 + # via + # langgraph + # langsmith +yarl==1.24.2 + # via aiohttp +youtube-transcript-api==1.0.3 + # via markitdown +zstandard==0.25.0 + # via + # langgraph-api + # langsmith diff --git a/offline-backend-20260512/backend/app/gateway/app.py b/offline-backend-20260512/backend/app/gateway/app.py index 80f1c2ce..06797fce 100644 --- a/offline-backend-20260512/backend/app/gateway/app.py +++ b/offline-backend-20260512/backend/app/gateway/app.py @@ -30,9 +30,9 @@ from app.gateway.routers import ( artifacts, assistants_compat, auth, - channels, - checkpoint_migration, - feedback, + channels, + checkpoint_migration, + feedback, html_page_favorites, intent, llm_metrics, @@ -41,6 +41,8 @@ from app.gateway.routers import ( models, multi_agent, notifications, + parallel_agent_tasks, + parallel_agents, public_embed, public_scheduled_tasks, recommend, @@ -378,7 +380,7 @@ def _release_singleton_lock(handle) -> None: pass -async def _curator_scheduler_loop() -> None: +async def _curator_scheduler_loop() -> None: """常驻后台任务:每隔 _CURATOR_CHECK_INTERVAL_SECONDS 检查一次需不需要进化。 多 worker 部署下用文件锁做 leader 选举 —— 只有抢到锁的那个 worker 进程真正 @@ -404,248 +406,248 @@ async def _curator_scheduler_loop() -> None: except Exception: logger.exception("Curator scheduler tick failed (non-fatal)") await asyncio.sleep(_CURATOR_CHECK_INTERVAL_SECONDS) - finally: - _release_singleton_lock(lock_handle) - - -def _env_bool(name: str, default: bool) -> bool: - raw = os.getenv(name) - if raw is None: - return default - return raw.strip().lower() not in {"0", "false", "no", "off"} - - -def _env_int(name: str, default: int, *, alias: str | None = None, minimum: int = 1) -> int: - raw = os.getenv(name) - if raw is None and alias: - raw = os.getenv(alias) - if raw is None: - raw = str(default) - try: - value = int(raw) - except ValueError: - return default - return max(minimum, value) - - -def _env_float(name: str, default: float, *, alias: str | None = None, minimum: float = 0.0) -> float: - raw = os.getenv(name) - if raw is None and alias: - raw = os.getenv(alias) - if raw is None: - raw = str(default) - try: - value = float(raw) - except ValueError: - return default - return max(minimum, value) - - -_LEADERBOARD_SNAPSHOT_BACKGROUND_ENABLED = _env_bool("ADMIN_LEADERBOARD_BACKGROUND_ENABLED", True) -_LEADERBOARD_SNAPSHOT_PREWARM_DAYS = _env_int( - "ADMIN_LEADERBOARD_BACKGROUND_PREWARM_DAYS", - 30, -) -_LEADERBOARD_SNAPSHOT_BATCH_SIZE = _env_int( - "ADMIN_LEADERBOARD_BACKGROUND_BATCH_SIZE", - 1, -) -_LEADERBOARD_SNAPSHOT_INTERVAL_SECONDS = _env_float( - "ADMIN_LEADERBOARD_BACKGROUND_INTERVAL_SECONDS", - 300.0, -) -_LEADERBOARD_SNAPSHOT_SLEEP_SECONDS = _env_float( - "ADMIN_LEADERBOARD_BACKGROUND_SLEEP_SECONDS", - 2.0, -) -_LEADERBOARD_SNAPSHOT_RUNNING_TTL_SECONDS = _env_int( - "ADMIN_LEADERBOARD_BACKGROUND_RUNNING_TTL_SECONDS", - 1800, -) -_LEADERBOARD_SNAPSHOT_STARTUP_DELAY_SECONDS = _env_float( - "ADMIN_LEADERBOARD_BACKGROUND_STARTUP_DELAY_SECONDS", - 60.0, -) - - -def _recent_leaderboard_stat_dates(days: int) -> list[str]: - from datetime import datetime, timedelta - - from deerflow.persistence.types import BEIJING_TZ - - today = datetime.now(BEIJING_TZ).date() - start = today - timedelta(days=max(1, days) - 1) - return [(start + timedelta(days=i)).isoformat() for i in range((today - start).days + 1)] - - -async def _leaderboard_snapshot_due_dates( - session_factory, - *, - settings_hash: str, - limit: int, - running_ttl_seconds: int, -) -> list[str]: - from datetime import UTC, datetime, timedelta - - from sqlalchemy import and_, or_, select - - from deerflow.persistence.admin_stats.model import AdminLeaderboardDailyStatRow - - cutoff = datetime.now(UTC) - timedelta(seconds=running_ttl_seconds) - async with session_factory() as session: - rows = ( - await session.execute( - select(AdminLeaderboardDailyStatRow.stat_date) - .where( - AdminLeaderboardDailyStatRow.settings_hash == settings_hash, - or_( - AdminLeaderboardDailyStatRow.status.in_(["queued", "error"]), - and_( - AdminLeaderboardDailyStatRow.status == "running", - AdminLeaderboardDailyStatRow.updated_at < cutoff, - ), - ), - ) - .order_by(AdminLeaderboardDailyStatRow.stat_date.desc()) - .limit(limit) - ) - ).all() - return [str(row[0]) for row in rows] - - -async def _claim_leaderboard_snapshot( - session_factory, - *, - stat_date: str, - settings_hash: str, - running_ttl_seconds: int, -) -> bool: - from datetime import UTC, datetime - - from app.gateway.routers.admin_users import _snapshot_is_fresh - from deerflow.persistence.admin_stats.model import AdminLeaderboardDailyStatRow - from deerflow.persistence.types import BEIJING_TZ - - async with session_factory() as session: - row = await session.get( - AdminLeaderboardDailyStatRow, - {"stat_date": stat_date, "settings_hash": settings_hash}, - ) - if row is None or _snapshot_is_fresh(row, stat_date): - return False - if row.status == "running": - updated_at = row.updated_at - if isinstance(updated_at, datetime): - if updated_at.tzinfo is None: - updated_at = updated_at.replace(tzinfo=BEIJING_TZ) - age = (datetime.now(BEIJING_TZ) - updated_at.astimezone(BEIJING_TZ)).total_seconds() - if age < running_ttl_seconds: - return False - row.status = "running" - row.message = "排行榜后台统计任务已认领。" - row.error = None - row.updated_at = datetime.now(UTC) - await session.commit() - return True - - -async def _run_leaderboard_snapshot_tick() -> tuple[int, int]: - from app.gateway.routers.admin_users import ( - _daily_job_key, - _queue_daily_leaderboard_snapshots, - _refresh_daily_leaderboard_snapshot, - _resolve_leaderboard_snapshot_scope, - ) - from deerflow.config.system_settings import load_system_settings - from deerflow.persistence.engine import get_session_factory - - session_factory = get_session_factory() - if session_factory is None: - return 0, 0 - - settings = load_system_settings().leaderboard - excluded, include_scheduled, include_admins, include_failed, settings_hash = _resolve_leaderboard_snapshot_scope(settings) - stat_dates = _recent_leaderboard_stat_dates(_LEADERBOARD_SNAPSHOT_PREWARM_DAYS) - queued_dates = await _queue_daily_leaderboard_snapshots( - session_factory, - stat_dates, - settings_hash, - message="排行榜后台统计任务定期预热/补算队列。", - ) - - due_dates = await _leaderboard_snapshot_due_dates( - session_factory, - settings_hash=settings_hash, - limit=_LEADERBOARD_SNAPSHOT_BATCH_SIZE, - running_ttl_seconds=_LEADERBOARD_SNAPSHOT_RUNNING_TTL_SECONDS, - ) - processed = 0 - for stat_date in due_dates: - claimed = await _claim_leaderboard_snapshot( - session_factory, - stat_date=stat_date, - settings_hash=settings_hash, - running_ttl_seconds=_LEADERBOARD_SNAPSHOT_RUNNING_TTL_SECONDS, - ) - if not claimed: - continue - logger.info("Leaderboard snapshot scheduler: building date=%s hash=%s", stat_date, settings_hash[:10]) - await _refresh_daily_leaderboard_snapshot( - _daily_job_key(stat_date, settings_hash), - session_factory=session_factory, - settings=settings, - stat_date=stat_date, - settings_hash=settings_hash, - excluded=excluded, - include_scheduled=include_scheduled, - include_admins=include_admins, - include_failed=include_failed, - ) - processed += 1 - if _LEADERBOARD_SNAPSHOT_SLEEP_SECONDS > 0 and processed < len(due_dates): - await asyncio.sleep(_LEADERBOARD_SNAPSHOT_SLEEP_SECONDS) - return processed, len(queued_dates) - - -async def _leaderboard_snapshot_scheduler_loop() -> None: - """Background task inside the existing gateway process for daily snapshots.""" - if not _LEADERBOARD_SNAPSHOT_BACKGROUND_ENABLED: - logger.info("Leaderboard snapshot scheduler disabled by ADMIN_LEADERBOARD_BACKGROUND_ENABLED") - return - - from deerflow.config.runtime_paths import runtime_home - - lock_handle = _acquire_singleton_lock(runtime_home() / ".leaderboard_snapshot_scheduler.lock") - if lock_handle is None: - logger.info("Leaderboard snapshot scheduler: another gateway worker holds the lock; this worker skips") - return - - logger.info( - "Leaderboard snapshot scheduler started: prewarm_days=%d batch_size=%d interval=%.1fs", - _LEADERBOARD_SNAPSHOT_PREWARM_DAYS, - _LEADERBOARD_SNAPSHOT_BATCH_SIZE, - _LEADERBOARD_SNAPSHOT_INTERVAL_SECONDS, - ) - try: - if _LEADERBOARD_SNAPSHOT_STARTUP_DELAY_SECONDS > 0: - await asyncio.sleep(_LEADERBOARD_SNAPSHOT_STARTUP_DELAY_SECONDS) - while True: - try: - processed, queued = await _run_leaderboard_snapshot_tick() - if processed or queued: - logger.info( - "Leaderboard snapshot scheduler tick: queued=%d processed=%d", - queued, - processed, - ) - except Exception: - logger.exception("Leaderboard snapshot scheduler tick failed (non-fatal)") - await asyncio.sleep(_LEADERBOARD_SNAPSHOT_INTERVAL_SECONDS) - finally: - _release_singleton_lock(lock_handle) - - -def _warn_if_multi_worker() -> None: + finally: + _release_singleton_lock(lock_handle) + + +def _env_bool(name: str, default: bool) -> bool: + raw = os.getenv(name) + if raw is None: + return default + return raw.strip().lower() not in {"0", "false", "no", "off"} + + +def _env_int(name: str, default: int, *, alias: str | None = None, minimum: int = 1) -> int: + raw = os.getenv(name) + if raw is None and alias: + raw = os.getenv(alias) + if raw is None: + raw = str(default) + try: + value = int(raw) + except ValueError: + return default + return max(minimum, value) + + +def _env_float(name: str, default: float, *, alias: str | None = None, minimum: float = 0.0) -> float: + raw = os.getenv(name) + if raw is None and alias: + raw = os.getenv(alias) + if raw is None: + raw = str(default) + try: + value = float(raw) + except ValueError: + return default + return max(minimum, value) + + +_LEADERBOARD_SNAPSHOT_BACKGROUND_ENABLED = _env_bool("ADMIN_LEADERBOARD_BACKGROUND_ENABLED", True) +_LEADERBOARD_SNAPSHOT_PREWARM_DAYS = _env_int( + "ADMIN_LEADERBOARD_BACKGROUND_PREWARM_DAYS", + 30, +) +_LEADERBOARD_SNAPSHOT_BATCH_SIZE = _env_int( + "ADMIN_LEADERBOARD_BACKGROUND_BATCH_SIZE", + 1, +) +_LEADERBOARD_SNAPSHOT_INTERVAL_SECONDS = _env_float( + "ADMIN_LEADERBOARD_BACKGROUND_INTERVAL_SECONDS", + 300.0, +) +_LEADERBOARD_SNAPSHOT_SLEEP_SECONDS = _env_float( + "ADMIN_LEADERBOARD_BACKGROUND_SLEEP_SECONDS", + 2.0, +) +_LEADERBOARD_SNAPSHOT_RUNNING_TTL_SECONDS = _env_int( + "ADMIN_LEADERBOARD_BACKGROUND_RUNNING_TTL_SECONDS", + 1800, +) +_LEADERBOARD_SNAPSHOT_STARTUP_DELAY_SECONDS = _env_float( + "ADMIN_LEADERBOARD_BACKGROUND_STARTUP_DELAY_SECONDS", + 60.0, +) + + +def _recent_leaderboard_stat_dates(days: int) -> list[str]: + from datetime import datetime, timedelta + + from deerflow.persistence.types import BEIJING_TZ + + today = datetime.now(BEIJING_TZ).date() + start = today - timedelta(days=max(1, days) - 1) + return [(start + timedelta(days=i)).isoformat() for i in range((today - start).days + 1)] + + +async def _leaderboard_snapshot_due_dates( + session_factory, + *, + settings_hash: str, + limit: int, + running_ttl_seconds: int, +) -> list[str]: + from datetime import UTC, datetime, timedelta + + from sqlalchemy import and_, or_, select + + from deerflow.persistence.admin_stats.model import AdminLeaderboardDailyStatRow + + cutoff = datetime.now(UTC) - timedelta(seconds=running_ttl_seconds) + async with session_factory() as session: + rows = ( + await session.execute( + select(AdminLeaderboardDailyStatRow.stat_date) + .where( + AdminLeaderboardDailyStatRow.settings_hash == settings_hash, + or_( + AdminLeaderboardDailyStatRow.status.in_(["queued", "error"]), + and_( + AdminLeaderboardDailyStatRow.status == "running", + AdminLeaderboardDailyStatRow.updated_at < cutoff, + ), + ), + ) + .order_by(AdminLeaderboardDailyStatRow.stat_date.desc()) + .limit(limit) + ) + ).all() + return [str(row[0]) for row in rows] + + +async def _claim_leaderboard_snapshot( + session_factory, + *, + stat_date: str, + settings_hash: str, + running_ttl_seconds: int, +) -> bool: + from datetime import UTC, datetime + + from app.gateway.routers.admin_users import _snapshot_is_fresh + from deerflow.persistence.admin_stats.model import AdminLeaderboardDailyStatRow + from deerflow.persistence.types import BEIJING_TZ + + async with session_factory() as session: + row = await session.get( + AdminLeaderboardDailyStatRow, + {"stat_date": stat_date, "settings_hash": settings_hash}, + ) + if row is None or _snapshot_is_fresh(row, stat_date): + return False + if row.status == "running": + updated_at = row.updated_at + if isinstance(updated_at, datetime): + if updated_at.tzinfo is None: + updated_at = updated_at.replace(tzinfo=BEIJING_TZ) + age = (datetime.now(BEIJING_TZ) - updated_at.astimezone(BEIJING_TZ)).total_seconds() + if age < running_ttl_seconds: + return False + row.status = "running" + row.message = "排行榜后台统计任务已认领。" + row.error = None + row.updated_at = datetime.now(UTC) + await session.commit() + return True + + +async def _run_leaderboard_snapshot_tick() -> tuple[int, int]: + from app.gateway.routers.admin_users import ( + _daily_job_key, + _queue_daily_leaderboard_snapshots, + _refresh_daily_leaderboard_snapshot, + _resolve_leaderboard_snapshot_scope, + ) + from deerflow.config.system_settings import load_system_settings + from deerflow.persistence.engine import get_session_factory + + session_factory = get_session_factory() + if session_factory is None: + return 0, 0 + + settings = load_system_settings().leaderboard + excluded, include_scheduled, include_admins, include_failed, settings_hash = _resolve_leaderboard_snapshot_scope(settings) + stat_dates = _recent_leaderboard_stat_dates(_LEADERBOARD_SNAPSHOT_PREWARM_DAYS) + queued_dates = await _queue_daily_leaderboard_snapshots( + session_factory, + stat_dates, + settings_hash, + message="排行榜后台统计任务定期预热/补算队列。", + ) + + due_dates = await _leaderboard_snapshot_due_dates( + session_factory, + settings_hash=settings_hash, + limit=_LEADERBOARD_SNAPSHOT_BATCH_SIZE, + running_ttl_seconds=_LEADERBOARD_SNAPSHOT_RUNNING_TTL_SECONDS, + ) + processed = 0 + for stat_date in due_dates: + claimed = await _claim_leaderboard_snapshot( + session_factory, + stat_date=stat_date, + settings_hash=settings_hash, + running_ttl_seconds=_LEADERBOARD_SNAPSHOT_RUNNING_TTL_SECONDS, + ) + if not claimed: + continue + logger.info("Leaderboard snapshot scheduler: building date=%s hash=%s", stat_date, settings_hash[:10]) + await _refresh_daily_leaderboard_snapshot( + _daily_job_key(stat_date, settings_hash), + session_factory=session_factory, + settings=settings, + stat_date=stat_date, + settings_hash=settings_hash, + excluded=excluded, + include_scheduled=include_scheduled, + include_admins=include_admins, + include_failed=include_failed, + ) + processed += 1 + if _LEADERBOARD_SNAPSHOT_SLEEP_SECONDS > 0 and processed < len(due_dates): + await asyncio.sleep(_LEADERBOARD_SNAPSHOT_SLEEP_SECONDS) + return processed, len(queued_dates) + + +async def _leaderboard_snapshot_scheduler_loop() -> None: + """Background task inside the existing gateway process for daily snapshots.""" + if not _LEADERBOARD_SNAPSHOT_BACKGROUND_ENABLED: + logger.info("Leaderboard snapshot scheduler disabled by ADMIN_LEADERBOARD_BACKGROUND_ENABLED") + return + + from deerflow.config.runtime_paths import runtime_home + + lock_handle = _acquire_singleton_lock(runtime_home() / ".leaderboard_snapshot_scheduler.lock") + if lock_handle is None: + logger.info("Leaderboard snapshot scheduler: another gateway worker holds the lock; this worker skips") + return + + logger.info( + "Leaderboard snapshot scheduler started: prewarm_days=%d batch_size=%d interval=%.1fs", + _LEADERBOARD_SNAPSHOT_PREWARM_DAYS, + _LEADERBOARD_SNAPSHOT_BATCH_SIZE, + _LEADERBOARD_SNAPSHOT_INTERVAL_SECONDS, + ) + try: + if _LEADERBOARD_SNAPSHOT_STARTUP_DELAY_SECONDS > 0: + await asyncio.sleep(_LEADERBOARD_SNAPSHOT_STARTUP_DELAY_SECONDS) + while True: + try: + processed, queued = await _run_leaderboard_snapshot_tick() + if processed or queued: + logger.info( + "Leaderboard snapshot scheduler tick: queued=%d processed=%d", + queued, + processed, + ) + except Exception: + logger.exception("Leaderboard snapshot scheduler tick failed (non-fatal)") + await asyncio.sleep(_LEADERBOARD_SNAPSHOT_INTERVAL_SECONDS) + finally: + _release_singleton_lock(lock_handle) + + +def _warn_if_multi_worker() -> None: """检测 uvicorn / gunicorn 多 worker 部署并打 ERROR 日志。 主 agent / canvas / AI 写作流式响应在 Gateway 进程内通过 ``StreamBridge`` @@ -799,21 +801,21 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: # 默认每天凌晨 3 点扫一次,删 retention_days(默认 7 天)以前的会话,联动 # 清理 MySQL 业务表 + SQLite checkpointer 里的 thread 状态。多 worker # 下用文件锁选 leader,只有抢到锁的 worker 真正跑清理。 - try: - from app.gateway.ai_writing_cleanup import cleanup_scheduler_loop - - app.state.ai_writing_cleanup_task = asyncio.create_task(cleanup_scheduler_loop(app)) - except Exception: - logger.exception("AI writing cleanup scheduler failed to start (non-fatal)") - - try: - app.state.leaderboard_snapshot_scheduler_task = asyncio.create_task(_leaderboard_snapshot_scheduler_loop()) - except Exception: - logger.exception("Leaderboard snapshot scheduler failed to start (non-fatal)") - - # Start IM channel service if any channels are configured - try: - from app.channels.service import start_channel_service + try: + from app.gateway.ai_writing_cleanup import cleanup_scheduler_loop + + app.state.ai_writing_cleanup_task = asyncio.create_task(cleanup_scheduler_loop(app)) + except Exception: + logger.exception("AI writing cleanup scheduler failed to start (non-fatal)") + + try: + app.state.leaderboard_snapshot_scheduler_task = asyncio.create_task(_leaderboard_snapshot_scheduler_loop()) + except Exception: + logger.exception("Leaderboard snapshot scheduler failed to start (non-fatal)") + + # Start IM channel service if any channels are configured + try: + from app.channels.service import start_channel_service channel_service = await start_channel_service(app.state.config) logger.info("Channel service started: %s", channel_service.get_status()) @@ -857,17 +859,17 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: except asyncio.CancelledError: pass except Exception: - logger.exception("AI writing cleanup scheduler shutdown error") - - leaderboard_snapshot_task = getattr(app.state, "leaderboard_snapshot_scheduler_task", None) - if leaderboard_snapshot_task is not None: - leaderboard_snapshot_task.cancel() - try: - await leaderboard_snapshot_task - except asyncio.CancelledError: - pass - except Exception: - logger.exception("Leaderboard snapshot scheduler shutdown error") + logger.exception("AI writing cleanup scheduler shutdown error") + + leaderboard_snapshot_task = getattr(app.state, "leaderboard_snapshot_scheduler_task", None) + if leaderboard_snapshot_task is not None: + leaderboard_snapshot_task.cancel() + try: + await leaderboard_snapshot_task + except asyncio.CancelledError: + pass + except Exception: + logger.exception("Leaderboard snapshot scheduler shutdown error") # 阶段 7 删冗余:AI 写作 graph 不再自管 checkpointer / in-flight task; # thread 状态由 LangGraph Server 主 checkpointer 接管,lifespan 出口由 @@ -1122,6 +1124,10 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an # Roundtable-planning business chains at /api/roundtable-chains app.include_router(roundtable_chains.router) + # Standalone parallel multi-agent panel: fan-out streaming + saved tasks + app.include_router(parallel_agents.router) + app.include_router(parallel_agent_tasks.router) + # Roundtable-planning background jobs at /api/roundtable-jobs app.include_router(roundtable_jobs.router) @@ -1159,10 +1165,10 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an app.include_router(tool_metrics.router) # Channels API is mounted at /api/channels - app.include_router(channels.router) - - # Admin checkpoint migration API: SQLite legacy checkpoints -> Postgres. - app.include_router(checkpoint_migration.router) + app.include_router(channels.router) + + # Admin checkpoint migration API: SQLite legacy checkpoints -> Postgres. + app.include_router(checkpoint_migration.router) # Assistants compatibility API (LangGraph Platform stub) app.include_router(assistants_compat.router) diff --git a/offline-backend-20260512/backend/app/gateway/deps.py b/offline-backend-20260512/backend/app/gateway/deps.py index e440e95d..22973c3b 100644 --- a/offline-backend-20260512/backend/app/gateway/deps.py +++ b/offline-backend-20260512/backend/app/gateway/deps.py @@ -179,6 +179,10 @@ async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]: app.state.roundtable_chain_store = make_roundtable_chain_store(sf) + from deerflow.persistence.parallel_agent_tasks import make_parallel_agent_task_store + + app.state.parallel_agent_task_store = make_parallel_agent_task_store(sf) + from deerflow.persistence.roundtable_jobs import ( make_roundtable_job_store, register_default_store as _register_roundtable_job_default_store, diff --git a/offline-backend-20260512/backend/app/gateway/routers/parallel_agent_tasks.py b/offline-backend-20260512/backend/app/gateway/routers/parallel_agent_tasks.py new file mode 100644 index 00000000..e8c41080 --- /dev/null +++ b/offline-backend-20260512/backend/app/gateway/routers/parallel_agent_tasks.py @@ -0,0 +1,148 @@ +"""CRUD API for parallel multi-agent task records (并行多智能体任务). + +A task = one saved run of the standalone parallel multi-agent panel: a task name ++ brief dispatched to several agents in parallel, plus the resulting message list. +Bound to the requesting user (not the browser), so it follows the account across +devices/sessions — same model as roundtable drafts. + +Routes (prefix ``/api/parallel-agent-tasks``): + GET "" list current user's tasks (lightweight meta, no messages) + POST "" create a task + GET "/{task_id}" one task (full, incl. messages) + PUT "/{task_id}" partial update (title / brief / agents / messages) + DELETE "/{task_id}" delete +""" + +from __future__ import annotations + +import logging +from datetime import datetime +from typing import Any +from uuid import uuid4 + +from fastapi import APIRouter, HTTPException, Request +from pydantic import BaseModel, Field + +from deerflow.runtime.user_context import get_effective_user_id + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/api/parallel-agent-tasks", tags=["parallel-agent-tasks"]) + + +# ── schemas ──────────────────────────────────────────────────────────────── + + +class TaskAgentSchema(BaseModel): + agent_id: str = Field(min_length=1, max_length=128) + name: str = "" + avatarType: str | None = None + + +class TaskResponse(BaseModel): + id: str + title: str = "" + brief: str | None = None + agents: list[TaskAgentSchema] = [] + # messages is an opaque dialogue list (mirrors the frontend Dialogue[] shape); + # absent on list responses (lightweight), present on get. + messages: list[dict[str, Any]] | None = None + created_at: datetime | str | None = None + updated_at: datetime | str | None = None + + +class TaskListResponse(BaseModel): + tasks: list[TaskResponse] + + +class TaskCreateRequest(BaseModel): + id: str | None = Field(default=None, max_length=64) + title: str = Field(default="", max_length=512) + brief: str | None = None + agents: list[TaskAgentSchema] = [] + messages: list[dict[str, Any]] = [] + + +class TaskUpdateRequest(BaseModel): + # All optional → callers send only what changed. + title: str | None = Field(default=None, max_length=512) + brief: str | None = None + agents: list[TaskAgentSchema] | None = None + messages: list[dict[str, Any]] | None = None + + +# ── helpers ────────────────────────────────────────────────────────────── + + +def _current_user_id(request: Request) -> str: + user = getattr(request.state, "user", None) + if user is not None: + return str(user.id) + return get_effective_user_id() + + +def _get_store(request: Request): + store = getattr(request.app.state, "parallel_agent_task_store", None) + if store is None: + raise HTTPException(status_code=503, detail="Parallel agent task store not available") + return store + + +# ── routes ─────────────────────────────────────────────────────────────── + + +@router.get("", response_model=TaskListResponse) +async def list_tasks(request: Request) -> TaskListResponse: + store = _get_store(request) + user_id = _current_user_id(request) + rows = await store.list_tasks(user_id) + return TaskListResponse(tasks=[TaskResponse(**r) for r in rows]) + + +@router.post("", response_model=TaskResponse, status_code=201) +async def create_task(request: Request, body: TaskCreateRequest) -> TaskResponse: + store = _get_store(request) + user_id = _current_user_id(request) + row = await store.create_task( + user_id, + { + "id": (body.id or uuid4().hex), + "title": body.title.strip(), + "brief": body.brief, + "agents": [a.model_dump() for a in body.agents], + "messages": body.messages, + }, + ) + return TaskResponse(**row) + + +@router.get("/{task_id}", response_model=TaskResponse) +async def get_task(request: Request, task_id: str) -> TaskResponse: + store = _get_store(request) + user_id = _current_user_id(request) + row = await store.get_task(task_id, user_id) + if row is None: + raise HTTPException(status_code=404, detail="Task not found") + return TaskResponse(**row) + + +@router.put("/{task_id}", response_model=TaskResponse) +async def update_task(request: Request, task_id: str, body: TaskUpdateRequest) -> TaskResponse: + store = _get_store(request) + user_id = _current_user_id(request) + fields = body.model_dump(exclude_unset=True) + # Normalize agents (pydantic models) to plain dicts for JSON storage. + if "agents" in fields and fields["agents"] is not None: + fields["agents"] = [a if isinstance(a, dict) else a.model_dump() for a in body.agents or []] + row = await store.update_task(task_id, user_id, **fields) + if row is None: + raise HTTPException(status_code=404, detail="Task not found") + return TaskResponse(**row) + + +@router.delete("/{task_id}", status_code=204) +async def delete_task(request: Request, task_id: str) -> None: + store = _get_store(request) + user_id = _current_user_id(request) + deleted = await store.delete_task(task_id, user_id) + if not deleted: + raise HTTPException(status_code=404, detail="Task not found") diff --git a/offline-backend-20260512/backend/app/gateway/routers/parallel_agents.py b/offline-backend-20260512/backend/app/gateway/routers/parallel_agents.py new file mode 100644 index 00000000..f16791c1 --- /dev/null +++ b/offline-backend-20260512/backend/app/gateway/routers/parallel_agents.py @@ -0,0 +1,247 @@ +"""Server-side parallel fan-out for the standalone parallel multi-agent panel. + +The Vue embeddable panel sends a single request — ``{task_name, brief, agent_ids}`` +— and this endpoint **fans out** to every agent in parallel (file writes disabled), +then **merges** their LangGraph streams into ONE SSE tagged per ``agent_id``, so the +client only consumes one stream and never parses LangGraph internals. + +It reuses the roundtable single-agent machinery from ``multi_agent``: +``_special_run(..., no_file=True)`` runs one seat under the ``seat_parallel`` run +policy (which excludes ``write_file`` / ``str_replace`` / ``bash`` / ``web_search`` +/ ``agent_orchestration`` …), so parallel agents can't write files or dispatch. + +Unified event protocol (one JSON object per ``data:`` frame):: + + {"type": "agent_start", "agent_id": "..."} + {"type": "delta", "agent_id": "...", "text": "..."} # AI text increment + {"type": "step", "agent_id": "...", "key": "...", "label": "...", "args": {...}} + {"type": "agent_done", "agent_id": "...", "content": "..."} + {"type": "agent_error", "agent_id": "...", "message": "..."} + {"type": "all_done"} + +The frame-normalisation (LangGraph ``messages-tuple`` → delta / tool step) mirrors +the frontend parser in ``frontend-web/src/roundtable-planning/api/multi-agent.ts`` +(``extractAiTextDelta`` + ``tool_call_chunks`` accumulation). +""" + +from __future__ import annotations + +import asyncio +import json +import logging +from typing import Any, AsyncIterator + +from fastapi import APIRouter, Body, HTTPException, Request +from fastapi.responses import StreamingResponse + +from app.gateway.routers.multi_agent import ( + _ROUNDTABLE_THREAD_METADATA, + _auth_headers, + _create_thread, + _loopback_base, + _make_loopback_client, + _special_run, + _validate_agent_id, +) + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/api/parallel-agents", tags=["parallel-agents"]) + +# Soft cap on parallel degree — protects remote model concurrency + sandbox. +MAX_PARALLEL_AGENTS = 8 + + +# ── frame normalisation (mirrors multi-agent.ts) ───────────────────────────── + + +def _extract_ai_text(chunk: dict[str, Any]) -> str: + """AI text delta from a ``messages-tuple`` chunk obj; "" when not AI text.""" + type_ = str(chunk.get("type") or "").lower() + if type_ not in ("ai", "aimessagechunk", "aimessage"): + return "" + content = chunk.get("content") + if isinstance(content, str): + return content + if isinstance(content, list): + out: list[str] = [] + for item in content: + if isinstance(item, str): + out.append(item) + elif isinstance(item, dict): + t = item.get("text") + if isinstance(t, str): + out.append(t) + return "".join(out) + return "" + + +def _extract_tool_steps(chunk: dict[str, Any]) -> list[dict[str, Any]]: + """Tool-call steps from a chunk obj — both complete ``tool_calls`` and + incremental ``tool_call_chunks`` (we only need the name to surface a step).""" + steps: list[dict[str, Any]] = [] + tool_calls = chunk.get("tool_calls") + if isinstance(tool_calls, list): + for tc in tool_calls: + if not isinstance(tc, dict): + continue + name = tc.get("name") + if not isinstance(name, str) or not name: + continue + ident = tc.get("id") or name + args = tc.get("args") if isinstance(tc.get("args"), dict) else None + steps.append({"key": f"tool:{ident}", "label": f"调用工具 {name}", "args": args}) + tcc = chunk.get("tool_call_chunks") + if isinstance(tcc, list): + for c in tcc: + if not isinstance(c, dict): + continue + name = c.get("name") + if not isinstance(name, str) or not name: + continue + idx = c.get("index") if isinstance(c.get("index"), int) else 0 + ident = c.get("id") or f"{name}:{idx}" + steps.append({"key": f"tool:{ident}", "label": f"调用工具 {name}", "args": None}) + return steps + + +def _build_task_message(task_name: str, brief: str) -> str: + parts: list[str] = [] + if task_name.strip(): + parts.append(f"任务名称:{task_name.strip()}") + if brief.strip(): + parts.append(f"任务简介:\n{brief.strip()}") + parts.append( + "请你从你的专业职责出发,针对以上任务给出你的分析与交付。" + "本轮为**并行**研讨:请把你的产出**直接写在回复正文里**," + "不要写文件、不要执行脚本/命令。" + ) + return "\n\n".join(parts) + + +# ── endpoint ───────────────────────────────────────────────────────────────── + + +@router.post("/run/stream") +async def run_parallel_agents_stream(request: Request, payload: dict = Body(...)) -> StreamingResponse: + """并行扇出 N 个智能体,合并成一条带 ``agent_id`` 标记的 SSE。文件写入固定禁用。""" + task_name: str = str(payload.get("task_name") or "").strip() + brief: str = str(payload.get("brief") or "").strip() + raw_ids = payload.get("agent_ids") or [] + model_name: str = str(payload.get("model") or "") + + if not isinstance(raw_ids, list) or not raw_ids: + raise HTTPException(status_code=400, detail="agent_ids 不能为空") + agent_ids: list[str] = [] + for aid in raw_ids: + sid = str(aid or "").strip().lower() + _validate_agent_id(sid, "agent_ids") + if sid not in agent_ids: + agent_ids.append(sid) + if len(agent_ids) > MAX_PARALLEL_AGENTS: + raise HTTPException( + status_code=400, + detail=f"并行智能体数量上限 {MAX_PARALLEL_AGENTS}(当前 {len(agent_ids)})", + ) + if not task_name and not brief: + raise HTTPException(status_code=400, detail="任务名称与简介至少填写其一") + + base = _loopback_base(request) + headers = _auth_headers(request) + new_message = _build_task_message(task_name, brief) + + async def _consume_agent( + client: Any, + agent_id: str, + queue: asyncio.Queue[dict[str, Any]], + ) -> None: + await queue.put({"type": "agent_start", "agent_id": agent_id}) + last_event = "" + seen_steps: set[str] = set() + buffer: list[str] = [] + done_sent = False + try: + thread_id = await _create_thread(client, base, headers, metadata=_ROUNDTABLE_THREAD_METADATA) + gen = _special_run( + client, + base, + headers, + {agent_id: thread_id}, + agent_id, + new_message, + [], + model_name, + display_name="", + no_file=True, # seat_parallel policy → 禁 write_file/bash/str_replace… + ) + async for chunk in gen: + if isinstance(chunk, dict): + # 终态帧 {status, content, agent_name} + content = str(chunk.get("content") or "") + await queue.put({"type": "agent_done", "agent_id": agent_id, "content": content}) + done_sent = True + continue + line = chunk.strip() if isinstance(chunk, str) else "" + if line.startswith("event:"): + last_event = line[6:].strip() + continue + if not line.startswith("data:"): + continue + raw = line[5:].strip() + if not raw: + continue + try: + parsed = json.loads(raw) + except Exception: + continue + if last_event == "messages" and isinstance(parsed, list) and parsed: + obj = parsed[0] + if isinstance(obj, dict): + text = _extract_ai_text(obj) + if text: + buffer.append(text) + await queue.put({"type": "delta", "agent_id": agent_id, "text": text}) + for step in _extract_tool_steps(obj): + key = step["key"] + if key in seen_steps: + continue + seen_steps.add(key) + await queue.put({"type": "step", "agent_id": agent_id, **step}) + if not done_sent: + # 流被抢占/截断,没收到终态帧 —— 用已累积正文兜底收口本席位。 + await queue.put( + {"type": "agent_done", "agent_id": agent_id, "content": "".join(buffer)} + ) + except Exception as exc: # noqa: BLE001 — 单个 agent 失败不拖垮其它 + logger.warning("[parallel-agents] %s failed: %r", agent_id, exc) + await queue.put({"type": "agent_error", "agent_id": agent_id, "message": str(exc)}) + finally: + await queue.put({"type": "__agent_end__", "agent_id": agent_id}) + + async def stream_generator() -> AsyncIterator[str]: + queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue() + async with _make_loopback_client(request) as client: + tasks = [asyncio.create_task(_consume_agent(client, aid, queue)) for aid in agent_ids] + ended = 0 + try: + while ended < len(agent_ids): + event = await queue.get() + if event.get("type") == "__agent_end__": + ended += 1 + continue + yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n" + yield f"data: {json.dumps({'type': 'all_done'}, ensure_ascii=False)}\n\n" + finally: + for t in tasks: + if not t.done(): + t.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + + return StreamingResponse( + stream_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + "Connection": "keep-alive", + }, + ) diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260608_05_parallel_agent_tasks.py b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260608_05_parallel_agent_tasks.py new file mode 100644 index 00000000..f51b680e --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260608_05_parallel_agent_tasks.py @@ -0,0 +1,61 @@ +"""Create parallel_agent_tasks table. + +Saved runs of the standalone parallel multi-agent panel (Vue embeddable +component): task name + brief + agents snapshot + the resulting message list, +bound per user. ``brief`` / ``agents`` / ``messages`` are LONGTEXT on MySQL +(mirror ``roundtable_drafts`` headroom) and plain TEXT elsewhere — matching +``PortableLongText``. ``create_all`` builds the table for brand-new DBs; this +revision patches DBs that already exist. + +Chained after ``20260608_04`` (chain stage_goals) so the revision graph stays +single-headed — otherwise ``alembic upgrade head`` aborts with "multiple heads" +and the startup auto-migrate silently falls back to the create_all schema. + +Revision ID: 20260608_05 +Revises: 20260608_04 +Create Date: 2026-06-08 +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op +from sqlalchemy import inspect +from sqlalchemy.dialects.mysql import LONGTEXT + + +revision = "20260608_05" +down_revision = "20260608_04" +branch_labels = None +depends_on = None + + +def _has_table(table_name: str) -> bool: + inspector = inspect(op.get_bind()) + return table_name in inspector.get_table_names() + + +def upgrade() -> None: + if _has_table("parallel_agent_tasks"): + return + long_text = LONGTEXT() if op.get_bind().dialect.name == "mysql" else sa.Text() + op.create_table( + "parallel_agent_tasks", + sa.Column("id", sa.String(length=64), primary_key=True, nullable=False), + sa.Column("user_id", sa.String(length=64), nullable=False), + sa.Column("title", sa.String(length=512), nullable=False, server_default=""), + sa.Column("brief", long_text, nullable=True), + sa.Column("agents", long_text, nullable=True), + sa.Column("messages", long_text, nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + ) + op.create_index( + "ix_parallel_agent_tasks_user_id", "parallel_agent_tasks", ["user_id"] + ) + + +def downgrade() -> None: + if _has_table("parallel_agent_tasks"): + op.drop_index("ix_parallel_agent_tasks_user_id", table_name="parallel_agent_tasks") + op.drop_table("parallel_agent_tasks") diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/models/__init__.py b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/models/__init__.py index 45122546..7cb7408b 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/models/__init__.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/models/__init__.py @@ -27,6 +27,7 @@ from deerflow.persistence.feedback.model import FeedbackRow from deerflow.persistence.llm_metrics.model import LlmCallMetricRow from deerflow.persistence.models.run_event import RunEventRow from deerflow.persistence.notifications.model import NotificationRow +from deerflow.persistence.parallel_agent_tasks.model import ParallelAgentTaskRow from deerflow.persistence.recommended_questions.model import RecommendedQuestionRow from deerflow.persistence.roundtable_chains.model import RoundtableChainRow from deerflow.persistence.roundtable_draft_shares.model import RoundtableDraftShareRow @@ -62,6 +63,7 @@ __all__ = [ "AdminLeaderboardDailyStatRow", "LlmCallMetricRow", "NotificationRow", + "ParallelAgentTaskRow", "RecommendedQuestionRow", "RoundtableChainRow", "RoundtableDraftRow", diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/parallel_agent_tasks/__init__.py b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/parallel_agent_tasks/__init__.py new file mode 100644 index 00000000..4dae1d28 --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/parallel_agent_tasks/__init__.py @@ -0,0 +1,18 @@ +"""Parallel multi-agent task persistence.""" + +from __future__ import annotations + +from deerflow.persistence.parallel_agent_tasks.model import ParallelAgentTaskRow +from deerflow.persistence.parallel_agent_tasks.sql import ParallelAgentTaskRepository + +__all__ = [ + "ParallelAgentTaskRepository", + "ParallelAgentTaskRow", + "make_parallel_agent_task_store", +] + + +def make_parallel_agent_task_store(session_factory): + if session_factory is None: + return None + return ParallelAgentTaskRepository(session_factory) diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/parallel_agent_tasks/model.py b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/parallel_agent_tasks/model.py new file mode 100644 index 00000000..8237ddfc --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/parallel_agent_tasks/model.py @@ -0,0 +1,46 @@ +"""ORM model for parallel multi-agent task records. + +A "parallel-agent task" is one saved run of the standalone *parallel multi-agent* +panel (the Vue3 embeddable component): a task name + brief dispatched to several +agents in parallel, plus the resulting chat/message list. Persisted per user so it +follows the account across devices/sessions — same model as ``roundtable_drafts``. + +``agents`` and ``messages`` are stored as JSON serialized into ``PortableLongText`` +(MySQL ``LONGTEXT``) rather than ``PortableJSON`` (MySQL ``TEXT``, 64 KB cap), +because the parallel transcript can carry several agents' full deliveries that +easily exceed 64 KB — mirroring ``roundtable_drafts``. +""" + +from __future__ import annotations + +from datetime import UTC, datetime + +from sqlalchemy import String +from sqlalchemy.orm import Mapped, mapped_column + +from deerflow.persistence.base import Base +from deerflow.persistence.types import BeijingDateTime, PortableLongText + + +class ParallelAgentTaskRow(Base): + __tablename__ = "parallel_agent_tasks" + + id: Mapped[str] = mapped_column(String(64), primary_key=True) + user_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) + # Task name (用户输入的任务名称) shown on the history list. + title: Mapped[str] = mapped_column(String(512), nullable=False, default="") + # Task brief / 简介 — the free-text task description. + brief: Mapped[str | None] = mapped_column(PortableLongText, nullable=True) + # JSON: snapshot of the agents dispatched, [{"agent_id", "name", "avatarType"?}]. + agents: Mapped[str | None] = mapped_column(PortableLongText, nullable=True) + # JSON: the full message/dialogue list (mirrors the frontend Dialogue[] shape). + messages: Mapped[str | None] = mapped_column(PortableLongText, nullable=True) + created_at: Mapped[datetime] = mapped_column( + BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC) + ) + updated_at: Mapped[datetime] = mapped_column( + BeijingDateTime(), + nullable=False, + default=lambda: datetime.now(UTC), + onupdate=lambda: datetime.now(UTC), + ) diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/parallel_agent_tasks/sql.py b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/parallel_agent_tasks/sql.py new file mode 100644 index 00000000..48bb014e --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/parallel_agent_tasks/sql.py @@ -0,0 +1,131 @@ +"""SQLAlchemy-backed store for parallel multi-agent task records. + +``agents`` / ``messages`` JSON columns are stored as serialized strings in +``PortableLongText`` and round-tripped here with ``json.dumps`` / ``json.loads`` +— same approach as ``roundtable_drafts`` / ``roundtable_chains``. + +All write/read paths are scoped by ``user_id`` for ownership isolation; a row +that belongs to another user is treated as not found. +""" + +from __future__ import annotations + +import json +from datetime import UTC, datetime +from typing import Any + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker + +from deerflow.persistence.parallel_agent_tasks.model import ParallelAgentTaskRow + +_UNSET = object() + + +def _loads(value: Any) -> Any: + if not isinstance(value, str): + return value + if not value: + return None + try: + return json.loads(value) + except Exception: + return None + + +def _dumps(value: Any) -> str | None: + if value is None: + return None + return json.dumps(value, ensure_ascii=False) + + +class ParallelAgentTaskRepository: + def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None: + self._sf = session_factory + + @staticmethod + def _to_dict(row: ParallelAgentTaskRow, *, with_messages: bool = True) -> dict[str, Any]: + data: dict[str, Any] = { + "id": row.id, + "title": row.title, + "brief": row.brief, + "agents": _loads(row.agents) or [], + "created_at": row.created_at.isoformat() if isinstance(row.created_at, datetime) else row.created_at, + "updated_at": row.updated_at.isoformat() if isinstance(row.updated_at, datetime) else row.updated_at, + } + if with_messages: + data["messages"] = _loads(row.messages) or [] + return data + + async def list_tasks(self, user_id: str, *, limit: int = 200) -> list[dict[str, Any]]: + """列出当前用户的任务(轻量元信息,不含 messages,供历史列表)。""" + stmt = ( + select(ParallelAgentTaskRow) + .where(ParallelAgentTaskRow.user_id == user_id) + .order_by(ParallelAgentTaskRow.updated_at.desc()) + .limit(limit) + ) + async with self._sf() as session: + result = await session.execute(stmt) + return [self._to_dict(row, with_messages=False) for row in result.scalars()] + + async def get_task(self, task_id: str, user_id: str) -> dict[str, Any] | None: + async with self._sf() as session: + row = await session.get(ParallelAgentTaskRow, task_id) + if row is None or row.user_id != user_id: + return None + return self._to_dict(row) + + async def create_task(self, user_id: str, data: dict[str, Any]) -> dict[str, Any]: + now = datetime.now(UTC) + row = ParallelAgentTaskRow( + id=data["id"], + user_id=user_id, + title=(data.get("title") or "").strip(), + brief=(data.get("brief") or None), + agents=_dumps(data.get("agents") or []), + messages=_dumps(data.get("messages") or []), + created_at=now, + updated_at=now, + ) + async with self._sf() as session: + session.add(row) + await session.commit() + await session.refresh(row) + return self._to_dict(row) + + async def update_task( + self, + task_id: str, + user_id: str, + *, + title=_UNSET, + brief=_UNSET, + agents=_UNSET, + messages=_UNSET, + ) -> dict[str, Any] | None: + async with self._sf() as session: + row = await session.get(ParallelAgentTaskRow, task_id) + if row is None or row.user_id != user_id: + return None + if title is not _UNSET and title is not None: + row.title = str(title).strip() + if brief is not _UNSET: + row.brief = brief or None + if agents is not _UNSET: + row.agents = _dumps(agents) + if messages is not _UNSET: + row.messages = _dumps(messages) + row.updated_at = datetime.now(UTC) + await session.commit() + await session.refresh(row) + return self._to_dict(row) + + async def delete_task(self, task_id: str, user_id: str) -> bool: + async with self._sf() as session: + row = await session.get(ParallelAgentTaskRow, task_id) + if row is None or row.user_id != user_id: + return False + await session.delete(row) + await session.commit() + return True diff --git a/parallel-agents-vue/README.md b/parallel-agents-vue/README.md new file mode 100644 index 00000000..c4602f35 --- /dev/null +++ b/parallel-agents-vue/README.md @@ -0,0 +1,79 @@ +# parallel-agents-vue + +并行多智能体面板 —— Vue3 可嵌入组件。输入「任务名称 + 简介」,**并行调用多个智能体** +(智能体清单由宿主传入,运行期间后端固定**禁止写文件**),消息列表照搬 DeerFlow 圆桌 +研讨第二步并行多席位的**深色样式与完整结构**(步骤条 / 并行双列 / 席位状态条),支持 +**保存聊天记录 + 回显**与**重新分析**。 + +## 开发调试 + +```bash +pnpm install +pnpm dev # http://localhost:5183 ,填后端 baseUrl / token / 智能体 id 即可联调 +``` + +后端需要一个跑着的 DeerFlow 网关(默认 `http://localhost:8001`),它提供: + +- `POST /api/parallel-agents/run/stream` —— 并行扇出,合并 SSE(事件带 `agent_id`); +- `/api/parallel-agent-tasks` —— 任务保存 / 回显 CRUD。 + +## 构建为组件库 + +```bash +pnpm build:lib # 产出 dist/parallel-agents-vue.{js,umd.cjs} + dist/parallel-agents-vue.css +``` + +## 嵌入其它 Vue 项目 + +```ts +import { ParallelAgentsPanel } from "parallel-agents-vue"; +import "parallel-agents-vue/style.css"; +``` + +```vue + + + + + + +``` + +### 接口地址 & 登录 + +- **接口地址**写在配置文件 `src/config.ts` 的 `DEFAULT_BASE_URL`(dev 可用 `VITE_PA_BASE_URL` 覆盖); + 也可用 `:base-url` prop 覆盖。 +- **登录**:把宿主侧的**用户名**通过 `:username` 传进来即可,组件会自动走 DeerFlow 的 + **免密「用户名登录」**端点(`POST /api/v1/auth/login/username`,公共 + CSRF 豁免)换取 token + 并缓存、自动续期(401 重登)。**不需要宿主自己管 token**。 + - 若你已有 token,也可直接传 `:token` / `:get-token`(优先级高于 username)。 + - 若后端开了口令门(`auth_login.username_login.require_password=true`),再传 `:login-password`。 + +### Props + +| prop | 类型 | 说明 | +| --------------- | -------------------------------------- | ----------------------------------------------------- | +| `agents` | `{agent_id,name,avatarType?}[]`(必填)| 要并行调用的智能体 | +| `username` | `string` | 免密自动登录用户名(推荐方式,宿主自己传) | +| `baseUrl` | `string` | 后端地址;不传用 `config.ts` 默认 | +| `token` | `string` | 已有 Bearer token(与 `username` 二选一,优先) | +| `getToken` | `() => string \| Promise` | 动态取 token | +| `loginPassword` | `string` | 仅当后端开启口令门时需要 | +| `taskId` | `string` | 传入则挂载时自动加载并回显该已保存任务 | +| `model` | `string` | 可选模型名(不传用 `config.ts` 默认 / 后端回退) | +| `dark` | `boolean`(默认 `true`) | 深色面板(自带成色,不依赖宿主主题) | + +### 事件 + +- `@saved="(task) => ..."` —— 保存成功(含新建/更新)后回传 `SavedTask`。 +- `@update:taskId="(id) => ..."` —— 当前任务 id 变化(保存 / 加载历史时)。 + +> 样式作用域全部包在根类 `.parallel-agents` 下,且**不引入 Tailwind preflight**,不会污染宿主全局样式。 diff --git a/parallel-agents-vue/docs/00-测试说明.md b/parallel-agents-vue/docs/00-测试说明.md new file mode 100644 index 00000000..d762c1e4 --- /dev/null +++ b/parallel-agents-vue/docs/00-测试说明.md @@ -0,0 +1,47 @@ +# 并行多智能体面板 · 测试用例总览 + +本目录是 `parallel-agents-vue` 的**手工测试用例**(Markdown)。覆盖:并行调用与流式、文件写入禁用、 +保存与回显、重新分析、免密登录与鉴权、嵌入与深色样式。 + +## 用例清单 + +| 文件 | 主题 | +| -------------------------------------------- | ------------------------------------- | +| [01-并行调用与流式.md](./01-并行调用与流式.md) | 并行扇出、流式输出、步骤条、席位状态条、禁写文件 | +| [02-保存与回显.md](./02-保存与回显.md) | 保存聊天记录、历史列表、加载回显、更新保存 | +| [03-重新分析.md](./03-重新分析.md) | 重新分析(覆盖重跑)、停止、空输入校验 | +| [04-免密登录与鉴权.md](./04-免密登录与鉴权.md) | username 免密登录、token 优先级、401 重登、口令门 | +| [05-嵌入与深色样式.md](./05-嵌入与深色样式.md) | 配置文件接口地址、抽屉/弹窗嵌入、深色自适应、不污染宿主 | +| [06-可用智能体清单.md](./06-可用智能体清单.md) | **测试用真实 agent_id 清单(可直接复制粘贴)** + 示例任务 | +| [07-使用说明.md](./07-使用说明.md) | **在其它 Vue 项目中引入哪些组件、如何使用**(props/事件/嵌入) | + +## 测试环境 + +| 组件 | 地址 / 命令 | +| --------------- | ------------------------------------------------------ | +| DeerFlow 后端 | `http://localhost:8001`(`make dev`,需含本功能的新路由)| +| Vue dev demo | `http://localhost:5183`(`pnpm dev`) | +| 接口地址配置 | `src/config.ts` 的 `DEFAULT_BASE_URL` | + +## 前置准备(所有用例通用) + +1. 后端已启动,且包含本功能: + - `POST /api/parallel-agents/run/stream`(并行扇出,合并 SSE) + - `GET/POST/PUT/DELETE /api/parallel-agent-tasks`(任务保存/回显) + - `POST /api/v1/auth/login/username`(免密登录,公共) + - 数据表 `parallel_agent_tasks` 已建(`alembic upgrade head` 或 `create_all`)。 +2. 至少准备 **2 个真实存在的 agent_id**(必须是后端注册过的真实 id,不能只写名字)。 + 本环境可直接用:`roundtable-solution-design`(方案设计)、`roundtable-risk-review`(风险审查)等 + —— 完整可复制清单见 [06-可用智能体清单.md](./06-可用智能体清单.md)。 +3. 打开 dev demo `http://localhost:5183`,填: + - 后端 baseUrl:`http://localhost:8001` + - 用户名:`demo`(免密自动登录) + - 智能体:每行 `agent_id, 显示名` + 点「打开面板(弹窗内)」。 + +## 用例字段约定 + +- **用例ID**:`PA-<模块>-<序号>` +- **优先级**:P0(核心,必过)/ P1(重要)/ P2(边界) +- **前置条件 → 操作步骤 → 预期结果**:逐条核对,全部命中才算通过。 +- **结果记录**:建议在「实际结果 / 通过?」列里回填。 diff --git a/parallel-agents-vue/docs/01-并行调用与流式.md b/parallel-agents-vue/docs/01-并行调用与流式.md new file mode 100644 index 00000000..5a736d00 --- /dev/null +++ b/parallel-agents-vue/docs/01-并行调用与流式.md @@ -0,0 +1,105 @@ +# 测试用例 · 并行调用与流式 + +覆盖:并行扇出多个智能体、流式逐字输出、步骤条(工具调用)、点头像切换显示(单选)、席位状态条、 +运行期间**禁止写文件**。 + +--- + +## PA-RUN-01 · 并行调用多个智能体(P0) + +**前置条件** +- 面板已打开,填入任务名「竞品调研」、简介「对比 A、B 两款产品的核心功能与定价」。 +- 传入 2 个智能体(情报收集、风险审查)。 + +**操作步骤** +1. 点击「开始分析」。 + +**预期结果** +- 顶部出现**席位状态条**,显示「并行 2 席」,两个席位头像角标先后转为「进行中」(转圈)。 +- 两个智能体**同时**开始执行(并行,而非一个跑完再跑下一个)——可在状态条上看到都在「进行中」。 +- 会话区每次显示**一个**席位的消息列表(默认第 1 个),点头像可切换查看另一个(见 PA-RUN-06)。 +- 气泡头部显示:头像、智能体名、状态徽标「并行执行中」、时间。 + +--- + +## PA-RUN-02 · 流式逐字输出 + 光标(P0) + +**前置条件**:PA-RUN-01 正在运行中。 + +**操作步骤** +1. 观察当前显示席位的气泡正文(点头像可切换看其它席位)。 + +**预期结果** +- 正文**逐步增量**出现(流式),不是一次性整段刷出。 +- 正在输出的气泡末尾有**闪烁光标**(`.pa-cursor`)。 +- 内容还没来时,显示 loading 占位(转圈 + 跳动圆点 + 「并行执行中」文案)。 +- Markdown 能正确渲染(标题/列表/代码块/表格按 `.pa-md` 样式)。 + +--- + +## PA-RUN-03 · 步骤条(思维链 / 工具调用)(P1) + +**前置条件**:智能体会调用工具(如检索类技能)。 + +**操作步骤** +1. 运行一轮,观察气泡顶部的「执行步骤」折叠卡。 + +**预期结果** +- 出现「执行步骤(N)」折叠卡(`MessageStepsCard`),点击可展开/收起。 +- 每条步骤显示:工具标签(如「调用工具 xxx」)、参数摘要(如有 `path` 优先显示)、时间。 +- 同一工具调用**不重复**出条目(按 key 去重)。 +- 若智能体本轮不调工具,则不出现步骤卡(属正常)。 + +--- + +## PA-RUN-04 · 席位状态流转(P1) + +**操作步骤** +1. 运行一轮,全程观察席位状态条各头像角标。 + +**预期结果** +- 角标状态依次:灰(pending)→ 蓝色转圈(running)→ 绿色对勾(done)。 +- 某智能体失败时其角标变**红色叉**(error),且对应气泡显示「该智能体执行出错:…」、状态徽标「出错」。 +- 单个智能体失败**不影响**其它智能体继续完成(互不拖累)。 +- 全部结束后,运行按钮从「停止」恢复为「重新分析」。 + +--- + +## PA-RUN-05 · 运行期间禁止写文件(P0) + +**目的**:核对后端 `seat_parallel` 策略生效——并行席位被禁用 `write_file` / `str_replace` / `bash` 等。 + +**操作步骤** +1. 简介里**故意要求写文件**,例如:「请把结果保存成一个 .md 文件」。 +2. 点击「开始分析」,等待完成。 + +**预期结果** +- 智能体**不会**真正落地文件,产出**直接呈现在气泡正文**里。 +- 步骤条里**不出现** `write_file` / `bash` / `str_replace` 工具调用(被策略排除)。 +- 可旁证:后端日志中该席位 `role=seat_parallel`。 + +--- + +## PA-RUN-06 · 点头像切换显示(单选)(P1) + +**前置条件**:传入 ≥2 个智能体并运行。 + +**操作步骤** +1. 点击席位状态条上的某个头像。 + +**预期结果** +- 主区**切换为只显示**该智能体的消息列表(**单选**,每次显示一个)。 +- 被选中的头像高亮(外发光描边);再点别的头像即切到那个。 +- 切换不影响后台并行执行:未显示的智能体仍在跑/已完成,其增量在切回时能看到。 +- 默认显示第 1 个席位。 + +--- + +## PA-RUN-07 · 后端不可达 / 智能体 id 非法(P2) + +**操作步骤** +1. 把 baseUrl 改成一个打不通的地址,或传入不存在的 agent_id,点「开始分析」。 + +**预期结果** +- 顶部出现红色错误提示(请求失败信息),不白屏、不卡死。 +- 非法 agent_id 时,对应席位以 error 角标 + 出错气泡呈现,其它正常席位不受影响。 diff --git a/parallel-agents-vue/docs/02-保存与回显.md b/parallel-agents-vue/docs/02-保存与回显.md new file mode 100644 index 00000000..cb3298fa --- /dev/null +++ b/parallel-agents-vue/docs/02-保存与回显.md @@ -0,0 +1,94 @@ +# 测试用例 · 保存与回显 + +覆盖:保存聊天记录(新建/更新)、历史列表、加载已存任务并回显、`taskId` 自动加载、跨刷新持久化。 + +--- + +## PA-SAVE-01 · 首次保存(新建)(P0) + +**前置条件**:已跑完一轮、会话区有结果。任务名「竞品调研」。 + +**操作步骤** +1. 点击「保存」。 + +**预期结果** +- 按钮短暂显示「保存中…」,成功后变为「更新保存」(说明已绑定 taskId)。 +- 触发 `@saved` 事件,回传 `SavedTask`(含 id、title、agents、messages)。 +- 「历史」下拉里出现这条记录(标题 + 更新时间)。 +- 后端 `parallel_agent_tasks` 表新增一行(owner = 当前登录用户)。 + +--- + +## PA-SAVE-02 · 再次保存(更新覆盖)(P0) + +**前置条件**:PA-SAVE-01 已保存、当前仍是同一任务。 + +**操作步骤** +1. 修改任务名 / 简介,或重新分析得到新结果。 +2. 点击「更新保存」。 + +**预期结果** +- **不新增**记录,而是更新原 taskId 那条(title / brief / agents / messages 被覆盖为最新)。 +- 历史列表里该条的更新时间刷新、排到最前。 + +--- + +## PA-SAVE-03 · 历史加载并回显(P0) + +**前置条件**:历史里至少有 1 条已保存任务。 + +**操作步骤** +1. 点击「历史 ▾」展开列表。 +2. 选择一条历史任务。 + +**预期结果** +- 任务名、简介**回填**为该任务保存时的值。 +- 会话区**完整回显**保存时的消息列表(各智能体气泡、正文、步骤条),且**均为非流式终态**(无光标、无 loading)。 +- 席位状态条据消息重建:正常席位显示 done(绿勾),出错的显示 error(红叉)。 +- 当前 taskId 切换为该任务(再点「保存」是「更新保存」该条)。 + +--- + +## PA-SAVE-04 · taskId 进入即自动回显(P1) + +**操作步骤** +1. 以 `:task-id="<已存在任务id>"` 方式挂载面板(嵌入场景)。 + +**预期结果** +- 面板挂载后**自动加载**该任务并回显(等同于 PA-SAVE-03 的结果),无需手动点历史。 +- `@update:taskId` 随之触发。 + +--- + +## PA-SAVE-05 · 跨刷新持久化(P1) + +**操作步骤** +1. 保存一条任务后,刷新浏览器(或重开宿主页面)。 +2. 用相同用户名进入,打开历史。 + +**预期结果** +- 历史里仍能看到之前保存的任务(数据存在后端、按用户隔离,非浏览器 localStorage)。 +- 加载后能正常回显。 + +--- + +## PA-SAVE-06 · 无结果时禁止保存(P2) + +**操作步骤** +1. 新开面板、尚未运行任何分析。 + +**预期结果** +- 「保存」按钮**禁用**(无可保存内容)。 +- 运行中也不应因保存导致状态错乱(保存按钮在有结果后才可用)。 + +--- + +## PA-SAVE-07 · 用户隔离(P1) + +**操作步骤** +1. 用户名 A 保存一条任务。 +2. 切换用户名为 B(重新进入面板),打开历史。 + +**预期结果** +- B 的历史里**看不到** A 的任务。 +- B 直接用 A 的 taskId 加载应失败(404 / 无内容),不报错崩溃。 diff --git a/parallel-agents-vue/docs/03-重新分析.md b/parallel-agents-vue/docs/03-重新分析.md new file mode 100644 index 00000000..7a0ef853 --- /dev/null +++ b/parallel-agents-vue/docs/03-重新分析.md @@ -0,0 +1,77 @@ +# 测试用例 · 重新分析与运行控制 + +覆盖:重新分析(覆盖重跑)、运行中停止、空输入校验、运行态按钮切换。 + +--- + +## PA-RE-01 · 重新分析(覆盖重跑)(P0) + +**前置条件**:已跑完一轮、会话区有结果(按钮显示「重新分析」)。 + +**操作步骤** +1. (可选)修改任务名 / 简介。 +2. 点击「重新分析」。 + +**预期结果** +- **清空**上一轮的所有气泡与席位状态,按当前输入**重新并行**调用各智能体。 +- 不会和上一轮结果混在一起(旧消息被替换,不是追加)。 +- 重跑过程与首跑一致(流式、单列消息 + 点头像切换、步骤条、席位状态流转)。 + +--- + +## PA-RE-02 · 运行中按钮为「停止」(P1) + +**操作步骤** +1. 开始分析,运行未结束时观察操作区。 + +**预期结果** +- 运行期间,主按钮显示「停止」(不再是「开始/重新分析」)。 +- 「保存」按钮在运行中可点(保存当前已产出的内容)或按实现禁用——以不报错为准。 + +--- + +## PA-RE-03 · 停止当前运行(P0) + +**前置条件**:正在运行中。 + +**操作步骤** +1. 点击「停止」。 + +**预期结果** +- 进行中的请求被取消(AbortController),不再有新增量进来。 +- 仍在 streaming 的气泡被收尾为终态(去掉光标,徽标置「已完成」)。 +- 按钮恢复为「重新分析」,可立即再次发起。 +- 控制台不应出现未捕获的 AbortError 报错(已被吞掉)。 + +--- + +## PA-RE-04 · 空输入校验(P1) + +**操作步骤** +1. 任务名与简介**都留空**,点击「开始分析」。 + +**预期结果** +- 按钮处于**禁用**态,点击无效(`canStart` 为假需任务名或简介至少其一 + 至少一个智能体)。 +- 只填任务名或只填简介,按钮可用。 + +--- + +## PA-RE-05 · 未传智能体(P2) + +**操作步骤** +1. `:agents="[]"`(空数组)挂载面板。 + +**预期结果** +- 操作区显示提示「⚠️ 未传入任何智能体(agents 为空)」。 +- 「开始分析」禁用,不会发起空请求。 + +--- + +## PA-RE-06 · 连续多次重新分析(P2) + +**操作步骤** +1. 连续点击「重新分析」若干次(每次等上一轮结束或中途停止再点)。 + +**预期结果** +- 每次都干净重置 + 重跑,无残留旧气泡、无席位状态错乱。 +- 不出现多份 SSE 叠加导致的内容串列 / 重复。 diff --git a/parallel-agents-vue/docs/04-免密登录与鉴权.md b/parallel-agents-vue/docs/04-免密登录与鉴权.md new file mode 100644 index 00000000..662a6610 --- /dev/null +++ b/parallel-agents-vue/docs/04-免密登录与鉴权.md @@ -0,0 +1,106 @@ +# 测试用例 · 免密登录与鉴权 + +覆盖:username 免密自动登录、token 优先级、token 缓存与 401 重登、口令门、CSRF 绕过。 + +> 背景:面板优先用宿主传入的 token;没有 token 时,用宿主传入的 **username** 走 DeerFlow 公共免密端点 +> `POST /api/v1/auth/login/username` 换取 `access_token`,以 `Authorization: Bearer` 发起后续请求 +> (带 Bearer 会绕过 CSRF)。token 解析优先级:`token` > `getToken` > 缓存 > `username` 登录。 + +--- + +## PA-AUTH-01 · username 免密自动登录(P0) + +**前置条件**:后端 `auth_login.username_login.require_password = false`(默认)。 + +**操作步骤** +1. 只传 `:username="demo"`(不传 token),打开面板。 +2. 开始分析 / 打开历史。 + +**预期结果** +- 面板能正常拉历史、能发起并行流式——说明已用 username 换到 token 并附带在请求头。 +- 旁证(手工 curl): + ```bash + curl -s -X POST -H "Content-Type: application/json" \ + -d '{"username":"demo"}' http://localhost:8001/api/v1/auth/login/username + # → 返回 {"access_token":"...","token_type":"bearer",...} + ``` + +--- + +## PA-AUTH-02 · token 优先级高于 username(P1) + +**操作步骤** +1. 同时传 `:token="<有效token>"` 和 `:username="demo"`。 +2. 触发请求(开始分析 / 历史)。 + +**预期结果** +- 直接用传入的 `token`(不会再去调登录端点)。 +- `:get-token` 同理优先于 username(动态取 token)。 + +--- + +## PA-AUTH-03 · token 缓存(不重复登录)(P1) + +**操作步骤** +1. 只传 username,先后触发多次请求(拉历史 → 开始分析 → 保存)。 + +**预期结果** +- 登录端点**只调用一次**(首个请求),后续请求复用缓存 token(按 `baseUrl::username` 缓存)。 +- 可在浏览器 Network 面板核对 `login/username` 只出现一次。 + +--- + +## PA-AUTH-04 · 401 自动重登重试(P1) + +**前置条件**:能制造 token 失效(例如等待过期,或后端清理)。 + +**操作步骤** +1. 用 username 登录并缓存 token 后,使其失效,再触发一次 CRUD 请求。 + +**预期结果** +- 首次请求收到 401 → 面板**清缓存并用 username 重登一次**,再自动重试该请求。 +- 用户无感知(不需手动重新登录),请求最终成功。 + +--- + +## PA-AUTH-05 · 口令门(require_password=true)(P2) + +**前置条件**:后端把 `auth_login.username_login.require_password` 设为 `true`、配置 `password`。 + +**操作步骤** +1. 只传 username、不传 `loginPassword` → 触发请求。 +2. 再传上正确的 `:login-password="<口令>"` → 触发请求。 + +**预期结果** +- 不传口令时:登录返回 401「口令错误」,面板顶部红色错误提示,不崩溃。 +- 传对口令时:登录成功,后续请求正常。 + +--- + +## PA-AUTH-06 · Bearer 绕过 CSRF(P1) + +**目的**:核对带 Bearer 的 POST 不被 CSRF 拦截。 + +**操作步骤(手工 curl 旁证)** +```bash +TOKEN=... # 上面登录拿到的 access_token +# 带 Bearer 的 POST:参数非法应为 400/422,而非 403(403=CSRF 拦截) +curl -s -o /dev/null -w "%{http_code}\n" -X POST \ + -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + -d '{}' http://localhost:8001/api/parallel-agents/run/stream +``` + +**预期结果** +- 返回 **400/422**(业务参数校验),**不是 403**——证明 Bearer 已绕过 CSRF。 +- 不带任何鉴权时该 POST 返回 401/403(被拦截),属正常。 + +--- + +## PA-AUTH-07 · 未传 token 且未传 username(P2) + +**操作步骤** +1. 既不传 token / getToken,也不传 username,触发需要鉴权的请求。 + +**预期结果** +- 请求因缺少鉴权失败(401/403),面板给出可读的错误提示(不白屏)。 +- 控制台/提示能反映「缺少 token 且未提供 username,无法登录」之类信息。 diff --git a/parallel-agents-vue/docs/05-嵌入与深色样式.md b/parallel-agents-vue/docs/05-嵌入与深色样式.md new file mode 100644 index 00000000..c6f22d13 --- /dev/null +++ b/parallel-agents-vue/docs/05-嵌入与深色样式.md @@ -0,0 +1,105 @@ +# 测试用例 · 嵌入与深色样式 + +覆盖:接口地址走配置文件、组件库构建产物、嵌入其它 Vue 项目、放进抽屉/弹窗自适应、深色成色、 +不污染宿主全局样式。 + +--- + +## PA-EMBED-01 · 接口地址走配置文件(P0) + +**操作步骤** +1. 不传 `:base-url`,只改 `src/config.ts` 的 `DEFAULT_BASE_URL` 为目标后端地址。 +2. 运行面板触发请求。 + +**预期结果** +- 请求打到配置文件里的地址。 +- 传 `:base-url` prop 时**优先**用 prop(覆盖配置默认)。 +- dev 下设 `VITE_PA_BASE_URL` 环境变量也能覆盖默认。 + +--- + +## PA-EMBED-02 · 组件库构建产物(P0) + +**操作步骤** +```bash +pnpm build:lib +``` + +**预期结果** +- `dist/` 产出:`parallel-agents-vue.js`(ESM)、`parallel-agents-vue.umd.cjs`(UMD)、`parallel-agents-vue.css`。 +- `vue` 被 external(不打进库,由宿主提供)。 +- 构建无报错、无「named + default exports」警告。 + +--- + +## PA-EMBED-03 · 嵌入其它 Vue 项目(P0) + +**操作步骤** +1. 在另一个 Vue3 项目里: + ```ts + import { ParallelAgentsPanel } from "parallel-agents-vue"; + import "parallel-agents-vue/style.css"; + ``` +2. 模板中放置: + ```vue + + ``` + +**预期结果** +- 组件正常渲染、可交互(深色面板,与 demo 一致)。 +- 只需 `:username` + `:agents` 两个 prop 即可跑起来(接口地址走配置)。 +- 也支持 `app.use(ParallelAgentsVue)` 全局注册后用 ``。 + +--- + +## PA-EMBED-04 · 放进抽屉 / 弹窗自适应(P0) + +**操作步骤** +1. 把面板放进宿主的抽屉 / 对话框,外层给一个固定高度容器: + ```vue + + + + ``` +2. 改变容器尺寸 / 视口大小。 + +**预期结果** +- 面板**充满容器**(根元素 `h-full w-full flex flex-col min-h-0`),不溢出、不塌缩。 +- 会话区内部独立滚动,输入区与席位条固定不被挤掉。 +- 窄容器下消息列表正常显示、内部滚动,不溢出。 + +--- + +## PA-EMBED-05 · 深色成色自带、与宿主主题无关(P1) + +**操作步骤** +1. 分别在**浅色**宿主页和**深色**宿主页里嵌入面板。 + +**预期结果** +- 面板始终是深蓝深色(自带 `--pa-*` token,作用域在 `.parallel-agents`),不随宿主明暗变化。 +- 文案、徽标、步骤卡、并行列底、滚动条等观感与圆桌 Step2 深色一致。 +- 传 `:dark="false"` 时按实现切换(如未实现浅色则保持深色,至少不报错)。 + +--- + +## PA-EMBED-06 · 不污染宿主全局样式(P0) + +**目的**:核对**未引入 Tailwind preflight**(不重置宿主全局标签样式)。 + +**操作步骤** +1. 在一个有自己样式的宿主页里嵌入面板(页面有 `h1/p/button/ul` 等默认样式)。 + +**预期结果** +- 嵌入后,宿主页面**自身**的 `h1/p/button/列表/边距` 等样式**不被改变**(没有被 Tailwind 的 reset 抹平)。 +- 面板内部样式正常(不依赖宿主提供 Tailwind)。 +- 类名都在 `.parallel-agents` 作用域下,不与宿主类名冲突。 + +--- + +## PA-EMBED-07 · 多实例并存(P2) + +**操作步骤** +1. 同一页面放置 2 个面板实例(不同 agents / 不同 username)。 + +**预期结果** +- 两个实例各自独立运行、各自保存/回显,互不串流、互不串 token 缓存(缓存按 `baseUrl::username` 区分)。 diff --git a/parallel-agents-vue/docs/06-可用智能体清单.md b/parallel-agents-vue/docs/06-可用智能体清单.md new file mode 100644 index 00000000..a5d5f6a1 --- /dev/null +++ b/parallel-agents-vue/docs/06-可用智能体清单.md @@ -0,0 +1,73 @@ +# 测试用 · 可用智能体清单(可直接复制粘贴) + +测试时面板需要传入**真实存在的 `agent_id`**(后端真正拿去跑的标识,不能只写名字)。 +下面是当前后端里**可直接用于测试**的内置智能体(已排除 intent / recommender / coordinator 这些系统角色)。 + +> 注:`name` 只是显示用、可省略;`agent_id` 必须照抄。下列为本环境实测可用的内置智能体; +> 你部署里的智能体可能不同,用文末方法自查。 + +## 可用智能体 + +| agent_id | 显示名 | +| --------------------------- | -------- | +| `roundtable-solution-design`| 方案设计 | +| `roundtable-risk-review` | 风险审查 | +| `roundtable-execution-plan` | 执行规划 | +| `roundtable-report` | 方案总结 | +| `demo-expert` | 演示专家 | + +## A. 粘贴进 dev demo 页「智能体」输入框(每行 `agent_id, 显示名`) + +推荐至少 2 个以便点头像切换查看不同智能体: + +``` +roundtable-solution-design, 方案设计 +roundtable-risk-review, 风险审查 +``` + +想多看几列就多贴几行: + +``` +roundtable-solution-design, 方案设计 +roundtable-risk-review, 风险审查 +roundtable-execution-plan, 执行规划 +``` + +## B. 粘贴进组件 `:agents` prop(嵌入场景) + +```ts +const agents = [ + { agent_id: "roundtable-solution-design", name: "方案设计" }, + { agent_id: "roundtable-risk-review", name: "风险审查" }, + { agent_id: "roundtable-execution-plan", name: "执行规划" }, +]; +``` + +```vue + +``` + +## C. 配一个示例任务(直接粘贴到面板) + +- **任务名称**:`新功能上线方案评估` +- **任务简介**: + ``` + 我们要给 App 上线「扫码点餐」功能。请从你的专业角度,评估方案可行性、列出关键风险、 + 并给出落地步骤与里程碑。 + ``` + +## 自查:列出你部署里所有可用的 agent_id + +```bash +# 1) 免密登录拿 token(用户名换成你的) +TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \ + -d '{"username":"demo"}' http://localhost:8001/api/v1/auth/login/username \ + | python -c "import sys,json;print(json.load(sys.stdin)['access_token'])") + +# 2) 列出 agent_id name +curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8001/api/agents -o agents.json +python -c "import json,io;[print(a['id'],'\t',a['name']) for a in json.load(io.open('agents.json',encoding='utf-8')).get('agents',[])]" +``` + +第一列 `id` 才是要填的 `agent_id`。挑你需要的智能体即可(避开 `roundtable-intent` / +`roundtable-recommender` / `roundtable-coordinator*` 这些系统角色——它们不是用来当并行席位的)。 diff --git a/parallel-agents-vue/docs/07-使用说明.md b/parallel-agents-vue/docs/07-使用说明.md new file mode 100644 index 00000000..3d11b05e --- /dev/null +++ b/parallel-agents-vue/docs/07-使用说明.md @@ -0,0 +1,163 @@ +# 使用说明 · 在其它 Vue 项目中引入 + +本组件库只对外暴露**一个**组件 `ParallelAgentsPanel`(其余 `MessageBubble` / `MessageStepsCard` / +`ParallelSeatStrip` / `Avatar` / `MarkdownContent` 都是它的内部子组件,**无需也不要单独引入**)。 +宿主只管把它放进自己的页面 / 抽屉 / 弹窗,并传几个 prop。 + +--- + +## 一、要引入的东西(就这两样) + +1. **组件**:`ParallelAgentsPanel` +2. **样式**:`parallel-agents-vue/style.css`(一次性引入即可,样式作用域全在 `.parallel-agents` 下) + +> 依赖说明:`vue ^3.4`(宿主提供,库不打包 vue);`markdown-it` 已打进库内,无需宿主安装。 + +--- + +## 二、两种引入方式 + +### 方式 A:构建成库后按包引入(推荐) + +1. 在本项目构建: + ```bash + pnpm install + pnpm build:lib # 产出 dist/parallel-agents-vue.{js,umd.cjs,css} + ``` +2. 宿主项目里安装(任选其一): + - 把本目录作为本地依赖:`"parallel-agents-vue": "file:../parallel-agents-vue"`; + - 或发到你们的私有 npm 后正常 `pnpm add parallel-agents-vue`。 +3. 代码里引入: + ```ts + import { ParallelAgentsPanel } from "parallel-agents-vue"; + import "parallel-agents-vue/style.css"; + ``` + +### 方式 B:直接拷贝源码(无需发包) + +把本项目 `src/` 下这些拷进宿主项目(保持相对结构),按相对路径引入 `ParallelAgentsPanel.vue` + +`styles/parallel-agents.css` 即可: + +``` +src/ + components/ ParallelAgentsPanel.vue · MessageBubble.vue · MessageStepsCard.vue + ParallelSeatStrip.vue · Avatar.vue · MarkdownContent.vue + composables/ useParallelRun.ts + lib/ avatar-constants.ts + styles/ parallel-agents.css + api.ts · config.ts · types.ts +``` +宿主需有 `markdown-it` 依赖(`pnpm add markdown-it`),并保证构建支持 `.vue`(Vite/@vitejs/plugin-vue)。 + +--- + +## 三、注册与使用 + +### 局部引入(推荐) + +```vue + + + + + + + +``` + +### 全局注册(可选) + +```ts +import ParallelAgentsVue from "parallel-agents-vue"; +import "parallel-agents-vue/style.css"; +app.use(ParallelAgentsVue); // 之后任意模板可直接用 +``` + +--- + +## 四、放进抽屉 / 弹窗 + +组件根元素是 `h-full w-full flex flex-col min-h-0`,会**充满父容器**。所以**父容器要给一个高度** +(弹窗/抽屉本身有高度即可): + +```vue + + + + + +``` + +--- + +## 五、接口地址与登录 + +- **接口地址**:默认读 `src/config.ts` 的 `DEFAULT_BASE_URL`(可改源码,或 dev 设 `VITE_PA_BASE_URL`); + 也可用 `:base-url` prop 临时覆盖。 +- **登录**:把宿主的**用户名**用 `:username` 传进来即可——组件自动走 DeerFlow 免密登录端点换 token、 + 缓存、401 自动重登。**宿主不用管 token**。若已有 token,可改传 `:token` / `:get-token`(优先级更高)。 +- token 缓存按 `baseUrl::username` 维度,多个实例/多用户互不串。 + +--- + +## 六、Props + +| prop | 类型 | 必填 | 说明 | +| --------------- | --------------------------------------- | :--: | ----------------------------------------------- | +| `agents` | `{ agent_id, name, avatarType? }[]` | ✔ | 要并行调用的智能体(`agent_id` 必须是真实 id) | +| `username` | `string` | | 免密自动登录用户名(推荐方式) | +| `baseUrl` | `string` | | 后端地址;不传用 `config.ts` 默认 | +| `token` | `string` | | 已有 Bearer token(优先于 username) | +| `getToken` | `() => string \| Promise` | | 动态取 token | +| `loginPassword` | `string` | | 仅当后端开启口令门时需要 | +| `taskId` | `string` | | 传入则挂载时自动加载并回显该任务 | +| `model` | `string` | | 可选模型名(不传走默认 / 后端回退) | +| `dark` | `boolean`(默认 `true`) | | 深色面板(自带成色,不依赖宿主主题) | + +`AgentInput`/`SavedTask` 等类型也从包里导出:`import type { AgentInput, SavedTask } from "parallel-agents-vue"`。 + +## 七、事件 + +| 事件 | 载荷 | 触发时机 | +| ----------------- | ------------ | --------------------------------------- | +| `saved` | `SavedTask` | 保存成功(新建或更新)后 | +| `update:taskId` | `string` | 当前任务 id 变化(保存 / 加载历史时) | + +```vue + console.log('saved', t.id)" + @update:taskId="(id) => (taskId = id)" +/> +``` + +--- + +## 八、交互要点(与圆桌 Step2 一致) + +- 输入「任务名称 + 简介」→「开始分析」→ 各智能体**并行**输出,运行期间**禁止写文件**(产出走正文)。 +- 主区每次只显示**一个**席位的消息列表;**点席位头像切换**要看哪个(单选)。 +- 顶部席位状态条显示每个智能体的进行中 / 完成 / 出错。 +- 操作区右侧有**模型切换**下拉(拉取后端 `/api/models`);选「默认」则用 `model` prop / 配置 / 后端回退。 + 切换作用于**下一次**「开始分析 / 重新分析」,运行中禁用。 +- 「保存」存档;「历史」里选回**回显**;「重新分析」清空重跑。 + +--- + +## 九、注意事项 + +- **不会污染宿主全局样式**:库未引入 Tailwind preflight,类名都在 `.parallel-agents` 作用域内。 +- **深色自带成色**:面板始终深蓝深色,不随宿主明暗切换(与圆桌 Step2 观感一致)。 +- **跨域**:若宿主与后端不同源,后端需允许该来源的 CORS + 带 `credentials`;登录端点公共且 CSRF 豁免, + 业务请求用 Bearer 头已绕过 CSRF。 +- **agent_id 必须真实**:见 [06-可用智能体清单.md](./06-可用智能体清单.md) 获取可用 id 与自查方法。 diff --git a/parallel-agents-vue/index.html b/parallel-agents-vue/index.html new file mode 100644 index 00000000..f213f6ce --- /dev/null +++ b/parallel-agents-vue/index.html @@ -0,0 +1,12 @@ + + + + + + 并行多智能体面板 · Dev + + + + + + diff --git a/parallel-agents-vue/package.json b/parallel-agents-vue/package.json new file mode 100644 index 00000000..c7aef0fc --- /dev/null +++ b/parallel-agents-vue/package.json @@ -0,0 +1,41 @@ +{ + "name": "parallel-agents-vue", + "version": "0.1.0", + "description": "并行多智能体面板 —— Vue3 可嵌入组件(输入任务名+简介,并行调用多个智能体,深色消息列表 + 步骤条 + 保存回显 + 重新分析)", + "type": "module", + "main": "./dist/parallel-agents-vue.umd.cjs", + "module": "./dist/parallel-agents-vue.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/parallel-agents-vue.js", + "require": "./dist/parallel-agents-vue.umd.cjs" + }, + "./style.css": "./dist/parallel-agents-vue.css" + }, + "files": ["dist"], + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "build:lib": "vite build --mode lib", + "typecheck": "vue-tsc --noEmit", + "preview": "vite preview" + }, + "peerDependencies": { + "vue": "^3.4.0" + }, + "dependencies": { + "markdown-it": "^14.1.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.0", + "tailwindcss": "^4.1.0", + "typescript": "^5.6.0", + "vite": "^6.0.0", + "vue": "^3.5.0", + "vue-tsc": "^2.1.10" + } +} diff --git a/parallel-agents-vue/pnpm-lock.yaml b/parallel-agents-vue/pnpm-lock.yaml new file mode 100644 index 00000000..ce27a4dc --- /dev/null +++ b/parallel-agents-vue/pnpm-lock.yaml @@ -0,0 +1,1389 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + markdown-it: + specifier: ^14.1.0 + version: 14.2.0 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.1.0 + version: 4.3.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)) + '@types/markdown-it': + specifier: ^14.1.2 + version: 14.1.2 + '@vitejs/plugin-vue': + specifier: ^5.2.0 + version: 5.2.4(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.35(typescript@5.9.3)) + tailwindcss: + specifier: ^4.1.0 + version: 4.3.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vite: + specifier: ^6.0.0 + version: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0) + vue: + specifier: ^3.5.0 + version: 3.5.35(typescript@5.9.3) + vue-tsc: + specifier: ^2.1.10 + version: 2.2.12(typescript@5.9.3) + +packages: + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@rollup/rollup-android-arm-eabi@4.61.1': + resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.61.1': + resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.61.1': + resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.61.1': + resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.61.1': + resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.61.1': + resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.61.1': + resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.61.1': + resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.61.1': + resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.61.1': + resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.61.1': + resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.61.1': + resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.61.1': + resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.61.1': + resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.61.1': + resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.61.1': + resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.61.1': + resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.61.1': + resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.61.1': + resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.61.1': + resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.61.1': + resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.61.1': + resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.61.1': + resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.61.1': + resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.61.1': + resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==} + cpu: [x64] + os: [win32] + + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.15': + resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} + + '@volar/source-map@2.4.15': + resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} + + '@volar/typescript@2.4.15': + resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} + + '@vue/compiler-core@3.5.35': + resolution: {integrity: sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==} + + '@vue/compiler-dom@3.5.35': + resolution: {integrity: sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==} + + '@vue/compiler-sfc@3.5.35': + resolution: {integrity: sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==} + + '@vue/compiler-ssr@3.5.35': + resolution: {integrity: sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/language-core@2.2.12': + resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.35': + resolution: {integrity: sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==} + + '@vue/runtime-core@3.5.35': + resolution: {integrity: sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==} + + '@vue/runtime-dom@3.5.35': + resolution: {integrity: sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==} + + '@vue/server-renderer@3.5.35': + resolution: {integrity: sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==} + peerDependencies: + vue: 3.5.35 + + '@vue/shared@3.5.35': + resolution: {integrity: sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==} + + alien-signals@1.0.13: + resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + enhanced-resolve@5.23.0: + resolution: {integrity: sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + linkify-it@5.0.1: + resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + markdown-it@14.2.0: + resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==} + hasBin: true + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + rollup@4.61.1: + resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-tsc@2.2.12: + resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.35: + resolution: {integrity: sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + +snapshots: + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@rollup/rollup-android-arm-eabi@4.61.1': + optional: true + + '@rollup/rollup-android-arm64@4.61.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.61.1': + optional: true + + '@rollup/rollup-darwin-x64@4.61.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.61.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.61.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.61.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.61.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.61.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.61.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.61.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.61.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.61.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.61.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.61.1': + optional: true + + '@tailwindcss/node@4.3.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.23.0 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 + + '@tailwindcss/oxide-android-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide@4.3.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/vite@4.3.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0) + + '@types/estree@1.0.9': {} + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + + '@vitejs/plugin-vue@5.2.4(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.35(typescript@5.9.3))': + dependencies: + vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0) + vue: 3.5.35(typescript@5.9.3) + + '@volar/language-core@2.4.15': + dependencies: + '@volar/source-map': 2.4.15 + + '@volar/source-map@2.4.15': {} + + '@volar/typescript@2.4.15': + dependencies: + '@volar/language-core': 2.4.15 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.35': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.35 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.35': + dependencies: + '@vue/compiler-core': 3.5.35 + '@vue/shared': 3.5.35 + + '@vue/compiler-sfc@3.5.35': + dependencies: + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.35 + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-ssr': 3.5.35 + '@vue/shared': 3.5.35 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.15 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.35': + dependencies: + '@vue/compiler-dom': 3.5.35 + '@vue/shared': 3.5.35 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/language-core@2.2.12(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.15 + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.35 + alien-signals: 1.0.13 + minimatch: 9.0.9 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + + '@vue/reactivity@3.5.35': + dependencies: + '@vue/shared': 3.5.35 + + '@vue/runtime-core@3.5.35': + dependencies: + '@vue/reactivity': 3.5.35 + '@vue/shared': 3.5.35 + + '@vue/runtime-dom@3.5.35': + dependencies: + '@vue/reactivity': 3.5.35 + '@vue/runtime-core': 3.5.35 + '@vue/shared': 3.5.35 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.35(vue@3.5.35(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.35 + '@vue/shared': 3.5.35 + vue: 3.5.35(typescript@5.9.3) + + '@vue/shared@3.5.35': {} + + alien-signals@1.0.13: {} + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + + csstype@3.2.3: {} + + de-indent@1.0.2: {} + + detect-libc@2.1.2: {} + + enhanced-resolve@5.23.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@4.5.0: {} + + entities@7.0.1: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + estree-walker@2.0.2: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.3: + optional: true + + graceful-fs@4.2.11: {} + + he@1.2.0: {} + + jiti@2.7.0: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + linkify-it@5.0.1: + dependencies: + uc.micro: 2.1.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-it@14.2.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.1 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + mdurl@2.0.0: {} + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.1 + + muggle-string@0.4.1: {} + + nanoid@3.3.12: {} + + path-browserify@1.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + punycode.js@2.3.1: {} + + rollup@4.61.1: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.61.1 + '@rollup/rollup-android-arm64': 4.61.1 + '@rollup/rollup-darwin-arm64': 4.61.1 + '@rollup/rollup-darwin-x64': 4.61.1 + '@rollup/rollup-freebsd-arm64': 4.61.1 + '@rollup/rollup-freebsd-x64': 4.61.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.61.1 + '@rollup/rollup-linux-arm-musleabihf': 4.61.1 + '@rollup/rollup-linux-arm64-gnu': 4.61.1 + '@rollup/rollup-linux-arm64-musl': 4.61.1 + '@rollup/rollup-linux-loong64-gnu': 4.61.1 + '@rollup/rollup-linux-loong64-musl': 4.61.1 + '@rollup/rollup-linux-ppc64-gnu': 4.61.1 + '@rollup/rollup-linux-ppc64-musl': 4.61.1 + '@rollup/rollup-linux-riscv64-gnu': 4.61.1 + '@rollup/rollup-linux-riscv64-musl': 4.61.1 + '@rollup/rollup-linux-s390x-gnu': 4.61.1 + '@rollup/rollup-linux-x64-gnu': 4.61.1 + '@rollup/rollup-linux-x64-musl': 4.61.1 + '@rollup/rollup-openbsd-x64': 4.61.1 + '@rollup/rollup-openharmony-arm64': 4.61.1 + '@rollup/rollup-win32-arm64-msvc': 4.61.1 + '@rollup/rollup-win32-ia32-msvc': 4.61.1 + '@rollup/rollup-win32-x64-gnu': 4.61.1 + '@rollup/rollup-win32-x64-msvc': 4.61.1 + fsevents: 2.3.3 + + source-map-js@1.2.1: {} + + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + typescript@5.9.3: {} + + uc.micro@2.1.0: {} + + vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.61.1 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + + vscode-uri@3.1.0: {} + + vue-tsc@2.2.12(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.15 + '@vue/language-core': 2.2.12(typescript@5.9.3) + typescript: 5.9.3 + + vue@3.5.35(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-sfc': 3.5.35 + '@vue/runtime-dom': 3.5.35 + '@vue/server-renderer': 3.5.35(vue@3.5.35(typescript@5.9.3)) + '@vue/shared': 3.5.35 + optionalDependencies: + typescript: 5.9.3 diff --git a/parallel-agents-vue/src/App.vue b/parallel-agents-vue/src/App.vue new file mode 100644 index 00000000..ee1c6319 --- /dev/null +++ b/parallel-agents-vue/src/App.vue @@ -0,0 +1,72 @@ + + + + + 并行多智能体面板 · Dev Demo + + 后端 baseUrl + + + 用户名(免密自动登录,由你自己传) + + + 智能体(每行一个:agent_id, 显示名) + + + + 打开面板(弹窗内) + + + + + + + + + + + diff --git a/parallel-agents-vue/src/api.ts b/parallel-agents-vue/src/api.ts new file mode 100644 index 00000000..7f260bab --- /dev/null +++ b/parallel-agents-vue/src/api.ts @@ -0,0 +1,236 @@ +/** + * 后端访问层:鉴权(宿主传入 token)+ 并行扇出 SSE + 任务 CRUD。 + * + * 全部打到 DeerFlow 网关(宿主传入 `baseUrl`,如 http://localhost:8001): + * - `POST {baseUrl}/api/parallel-agents/run/stream` —— 并行扇出,合并 SSE; + * - `{baseUrl}/api/parallel-agent-tasks` —— 任务保存/回显 CRUD。 + */ +import type { AgentInput, Dialogue, SavedTask, StreamEvent } from "./types"; +import { USERNAME_LOGIN_PATH } from "./config"; + +export interface ApiConfig { + baseUrl: string; + /** 已有登录态 token(Bearer)。可传字符串或一个(异步)取 token 的函数。 */ + token?: string; + getToken?: () => string | Promise; + /** + * 免密自动登录用户名:不传 token 时,组件用它走「用户名登录」(公共端点)换取 token + * 并缓存。用户名由宿主自己传。 + */ + username?: string; + /** 仅当后端 `auth_login.username_login.require_password=true` 时需要的口令。 */ + loginPassword?: string; +} + +function base(cfg: ApiConfig): string { + return cfg.baseUrl.replace(/\/$/, ""); +} + +// 免密登录拿到的 token 按 baseUrl::username 缓存,避免每个请求都登一次。 +const _tokenCache = new Map(); +const cacheKey = (cfg: ApiConfig) => `${base(cfg)}::${cfg.username ?? ""}`; + +/** 用 username 走公共「用户名登录」端点换 access_token(CSRF 豁免)。 */ +async function loginByUsername(cfg: ApiConfig): Promise { + if (!cfg.username) throw new Error("缺少 token 且未提供 username,无法登录"); + let url = `${base(cfg)}${USERNAME_LOGIN_PATH}`; + if (cfg.loginPassword) url += `?password=${encodeURIComponent(cfg.loginPassword)}`; + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: cfg.username }), + credentials: "include", + }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`username login failed (${res.status}): ${text}`); + } + const data = (await res.json()) as { access_token: string; token_type?: string }; + const token = data.access_token; + _tokenCache.set(cacheKey(cfg), token); + return token; +} + +/** token 解析优先级:显式 token > getToken > 缓存 > 用 username 免密登录。 */ +async function resolveToken(cfg: ApiConfig): Promise { + if (cfg.token) return cfg.token; + if (cfg.getToken) return await cfg.getToken(); + const cached = _tokenCache.get(cacheKey(cfg)); + if (cached) return cached; + if (cfg.username) return await loginByUsername(cfg); + return undefined; +} + +/** 清掉缓存 token(401 时强制下次重登)。 */ +function invalidateToken(cfg: ApiConfig): void { + _tokenCache.delete(cacheKey(cfg)); +} + +async function authHeaders(cfg: ApiConfig, json = true): Promise> { + const h: Record = {}; + if (json) h["Content-Type"] = "application/json"; + const token = await resolveToken(cfg); + if (token) h["Authorization"] = token.startsWith("Bearer ") ? token : `Bearer ${token}`; + return h; +} + +/** 带鉴权的 fetch;若 401 且用 username 免密登录,则清缓存重登并重试一次。 */ +async function authedFetch( + cfg: ApiConfig, + url: string, + init: RequestInit & { json?: boolean } = {}, +): Promise { + const { json = true, headers, ...rest } = init; + const run = async () => + fetch(url, { + ...rest, + headers: { ...(await authHeaders(cfg, json)), ...(headers as Record | undefined) }, + credentials: "include", + }); + let res = await run(); + if (res.status === 401 && !cfg.token && !cfg.getToken && cfg.username) { + invalidateToken(cfg); + res = await run(); + } + return res; +} + +// ── 并行扇出 SSE ────────────────────────────────────────────────────────────── + +export interface StreamHandlers { + onEvent: (ev: StreamEvent) => void; + onError?: (err: unknown) => void; +} + +/** + * 打开 `/api/parallel-agents/run/stream` 一条合并 SSE,逐条把统一事件回调出去。 + * 返回的 Promise 在流结束(all_done / 连接关闭)后 resolve。用 `signal` 取消。 + */ +export async function streamParallelAgents( + cfg: ApiConfig, + body: { task_name: string; brief: string; agent_ids: string[]; model?: string }, + handlers: StreamHandlers, + signal?: AbortSignal, +): Promise { + const res = await authedFetch(cfg, `${base(cfg)}/api/parallel-agents/run/stream`, { + method: "POST", + body: JSON.stringify(body), + signal, + }); + if (!res.ok || !res.body) { + const text = await res.text().catch(() => ""); + throw new Error(`run/stream failed (${res.status}): ${text}`); + } + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const raw of lines) { + const line = raw.trim(); + if (!line.startsWith("data:")) continue; + const payload = line.slice(5).trim(); + if (!payload) continue; + try { + handlers.onEvent(JSON.parse(payload) as StreamEvent); + } catch { + /* 忽略坏帧 */ + } + } + } + } catch (err) { + if ((err as { name?: string })?.name !== "AbortError") handlers.onError?.(err); + } finally { + reader.releaseLock(); + } +} + +// ── 任务 CRUD(保存 / 回显)───────────────────────────────────────────────── + +interface ServerTask { + id: string; + title: string; + brief: string | null; + agents: AgentInput[]; + messages?: Dialogue[]; + created_at: string | null; + updated_at: string | null; +} + +const TASKS = (cfg: ApiConfig) => `${base(cfg)}/api/parallel-agent-tasks`; + +async function readJson(res: Response, ctx: string): Promise { + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`${ctx} failed (${res.status}): ${text}`); + } + return (await res.json()) as T; +} + +export async function listTasks(cfg: ApiConfig): Promise { + const res = await authedFetch(cfg, TASKS(cfg), { json: false }); + const data = await readJson<{ tasks: ServerTask[] }>(res, "list tasks"); + return data.tasks ?? []; +} + +export async function getTask(cfg: ApiConfig, id: string): Promise { + const res = await authedFetch(cfg, `${TASKS(cfg)}/${encodeURIComponent(id)}`, { json: false }); + if (res.status === 404) return null; + return readJson(res, "get task"); +} + +export async function createTask( + cfg: ApiConfig, + payload: { id?: string; title: string; brief?: string; agents: AgentInput[]; messages: Dialogue[] }, +): Promise { + const res = await authedFetch(cfg, TASKS(cfg), { method: "POST", body: JSON.stringify(payload) }); + return readJson(res, "create task"); +} + +export async function updateTask( + cfg: ApiConfig, + id: string, + patch: { title?: string; brief?: string; agents?: AgentInput[]; messages?: Dialogue[] }, +): Promise { + const res = await authedFetch(cfg, `${TASKS(cfg)}/${encodeURIComponent(id)}`, { + method: "PUT", + body: JSON.stringify(patch), + }); + if (res.status === 404) return null; + return readJson(res, "update task"); +} + +// ── 模型列表 ────────────────────────────────────────────────────────────── + +export interface ModelItem { + name: string; + display_name?: string; +} + +/** 列出后端可用模型(GET /api/models)。失败返回空数组(不阻断主流程)。 */ +export async function listModels(cfg: ApiConfig): Promise { + try { + const res = await authedFetch(cfg, `${base(cfg)}/api/models`, { json: false }); + if (!res.ok) return []; + const data = (await res.json()) as { models?: ModelItem[] }; + return data.models ?? []; + } catch { + return []; + } +} + +export async function deleteTask(cfg: ApiConfig, id: string): Promise { + const res = await authedFetch(cfg, `${TASKS(cfg)}/${encodeURIComponent(id)}`, { + method: "DELETE", + json: false, + }); + if (!res.ok && res.status !== 404) { + const text = await res.text().catch(() => ""); + throw new Error(`delete task failed (${res.status}): ${text}`); + } +} diff --git a/parallel-agents-vue/src/components/Avatar.vue b/parallel-agents-vue/src/components/Avatar.vue new file mode 100644 index 00000000..615a3011 --- /dev/null +++ b/parallel-agents-vue/src/components/Avatar.vue @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/parallel-agents-vue/src/components/MarkdownContent.vue b/parallel-agents-vue/src/components/MarkdownContent.vue new file mode 100644 index 00000000..a56b3f06 --- /dev/null +++ b/parallel-agents-vue/src/components/MarkdownContent.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/parallel-agents-vue/src/components/MessageBubble.vue b/parallel-agents-vue/src/components/MessageBubble.vue new file mode 100644 index 00000000..a5e5c9e4 --- /dev/null +++ b/parallel-agents-vue/src/components/MessageBubble.vue @@ -0,0 +1,50 @@ + + + + + + + + + {{ msg.sender }} + {{ msg.confidence }} + + {{ msg.time }} + + + + + + + ◜ + {{ msg.confidence || "正在生成…" }} + + + + + + + + + + + + diff --git a/parallel-agents-vue/src/components/MessageStepsCard.vue b/parallel-agents-vue/src/components/MessageStepsCard.vue new file mode 100644 index 00000000..9cb8ecef --- /dev/null +++ b/parallel-agents-vue/src/components/MessageStepsCard.vue @@ -0,0 +1,52 @@ + + + + + + ▶ + 执行步骤({{ steps.length }}) + ◜ + + + + + + {{ s.label }} + · {{ argSummary(s.args) }} + · {{ s.ts }} + + + + + diff --git a/parallel-agents-vue/src/components/ParallelAgentsPanel.vue b/parallel-agents-vue/src/components/ParallelAgentsPanel.vue new file mode 100644 index 00000000..00cc2928 --- /dev/null +++ b/parallel-agents-vue/src/components/ParallelAgentsPanel.vue @@ -0,0 +1,329 @@ + + + + + + + + + + + 历史 ▾ + + + + 暂无保存的任务 + + + {{ t.title || "未命名任务" }} + · {{ (t.updated_at || "").slice(0, 16).replace("T", " ") }} + + + + + + + + {{ hasResults ? "重新分析" : "开始分析" }} + + + 停止 + + + {{ saving ? "保存中…" : currentTaskId ? "更新保存" : "保存" }} + + + + + 模型 + + 默认 + + {{ m.display_name || m.name }} + + + + + + ⚠️ 未传入任何智能体(agents 为空) + + + {{ errorMsg }} + + + + + + + + + + + + + + {{ seat.name }} · 等待输出… + + + + + 填写任务名称与简介,点「开始分析」并行调用各智能体。 + 并行运行期间禁止写文件,产出直接呈现在正文。 + + + + diff --git a/parallel-agents-vue/src/components/ParallelSeatStrip.vue b/parallel-agents-vue/src/components/ParallelSeatStrip.vue new file mode 100644 index 00000000..e58aa88e --- /dev/null +++ b/parallel-agents-vue/src/components/ParallelSeatStrip.vue @@ -0,0 +1,60 @@ + + + + + + 并行 {{ seats.length }} 席 + ({{ runningCount }} 进行中) + · 点头像切换显示 + + + + + + + + + ◜ + ✓ + ✕ + + + + + diff --git a/parallel-agents-vue/src/composables/useParallelRun.ts b/parallel-agents-vue/src/composables/useParallelRun.ts new file mode 100644 index 00000000..3d238c95 --- /dev/null +++ b/parallel-agents-vue/src/composables/useParallelRun.ts @@ -0,0 +1,212 @@ +/** + * 并行运行编排(前端态)。消费后端合并 SSE,按 agent_id 把增量/步骤/收口路由到对应席位 + * 气泡,维护并行席位状态条;带轻量节流(rAF 合并增量),支持取消、回显 hydrate、重置。 + */ +import { ref, shallowRef } from "vue"; +import type { AgentInput, Dialogue, SeatView, StreamEvent } from "@/types"; +import { getAvatarTypeFor } from "@/lib/avatar-constants"; +import { streamParallelAgents, type ApiConfig } from "@/api"; + +function nowTime(): string { + const d = new Date(); + const p = (n: number) => String(n).padStart(2, "0"); + return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`; +} + +let _seq = 1; +function nextId(): number { + return _seq++; +} + +export function useParallelRun() { + const dialogues = ref([]); + const seats = ref([]); + const running = ref(false); + const abortRef = shallowRef(null); + + // 增量节流:累积到 pending,rAF 合并刷新一次,避免逐 token reflow。 + const pending = new Map(); // agentId -> 待追加文本 + let rafScheduled = false; + const bubbleByAgent = new Map(); // agentId -> dialogue.id + + function flushPending() { + rafScheduled = false; + if (pending.size === 0) return; + for (const [agentId, text] of pending) { + const bid = bubbleByAgent.get(agentId); + const d = dialogues.value.find((x) => x.id === bid); + if (d) d.content += text; + } + pending.clear(); + // 触发数组响应(对象内字段已变更,这里强制一次浅刷新以更新双列视图)。 + dialogues.value = [...dialogues.value]; + } + + function scheduleFlush() { + if (rafScheduled) return; + rafScheduled = true; + requestAnimationFrame(flushPending); + } + + function setSeatStatus(agentId: string, status: SeatView["status"]) { + const s = seats.value.find((x) => x.agentId === agentId); + if (s) s.status = status; + seats.value = [...seats.value]; + } + + function findDialogue(agentId: string): Dialogue | undefined { + const bid = bubbleByAgent.get(agentId); + return dialogues.value.find((x) => x.id === bid); + } + + function handleEvent(ev: StreamEvent) { + switch (ev.type) { + case "agent_start": + setSeatStatus(ev.agent_id, "running"); + break; + case "delta": + pending.set(ev.agent_id, (pending.get(ev.agent_id) ?? "") + ev.text); + scheduleFlush(); + break; + case "step": { + const d = findDialogue(ev.agent_id); + if (d) { + d.steps = d.steps ?? []; + d.steps.push({ + id: `${d.id}-${d.steps.length}`, + key: ev.key, + label: ev.label, + ts: nowTime(), + args: ev.args ?? null, + }); + dialogues.value = [...dialogues.value]; + } + break; + } + case "agent_done": { + flushPending(); + const d = findDialogue(ev.agent_id); + if (d) { + if (ev.content) d.content = ev.content; // 终态正文以后端为准 + d.streaming = false; + d.confidence = "已完成"; + } + setSeatStatus(ev.agent_id, "done"); + dialogues.value = [...dialogues.value]; + break; + } + case "agent_error": { + flushPending(); + const d = findDialogue(ev.agent_id); + if (d) { + d.content = d.content || `⚠️ 该智能体执行出错:${ev.message}`; + d.streaming = false; + d.confidence = "出错"; + } + setSeatStatus(ev.agent_id, "error"); + dialogues.value = [...dialogues.value]; + break; + } + case "all_done": + flushPending(); + break; + } + } + + /** 启动一轮并行运行。会清空旧对话、为每个 agent 建一个流式气泡。 */ + async function start( + cfg: ApiConfig, + args: { taskName: string; brief: string; agents: AgentInput[]; model?: string }, + ): Promise { + if (running.value) return; + reset(); + running.value = true; + + bubbleByAgent.clear(); + const newSeats: SeatView[] = []; + const newDialogues: Dialogue[] = []; + args.agents.forEach((a, i) => { + const avatarType = a.avatarType || getAvatarTypeFor(a.agent_id, i); + const bubbleId = nextId(); + bubbleByAgent.set(a.agent_id, bubbleId); + newDialogues.push({ + id: bubbleId, + agentId: a.agent_id, + sender: a.name || a.agent_id, + avatarType, + content: "", + time: nowTime(), + confidence: "并行执行中", + streaming: true, + steps: [], + }); + newSeats.push({ agentId: a.agent_id, name: a.name || a.agent_id, avatarType, status: "pending", bubbleId }); + }); + dialogues.value = newDialogues; + seats.value = newSeats; + + const ac = new AbortController(); + abortRef.value = ac; + try { + await streamParallelAgents( + cfg, + { + task_name: args.taskName, + brief: args.brief, + agent_ids: args.agents.map((a) => a.agent_id), + model: args.model, + }, + { onEvent: handleEvent, onError: (err) => console.error("[parallel-run]", err) }, + ac.signal, + ); + } finally { + // 收尾:任何仍在 streaming 的气泡都置完成态。 + for (const d of dialogues.value) { + if (d.streaming) { + d.streaming = false; + if (d.confidence === "并行执行中") d.confidence = "已完成"; + } + } + dialogues.value = [...dialogues.value]; + running.value = false; + abortRef.value = null; + } + } + + function cancel() { + abortRef.value?.abort(); + running.value = false; + } + + /** 回显:从保存的对话列表重建(并据此还原席位条为已完成)。 */ + function hydrate(saved: Dialogue[]) { + cancel(); + bubbleByAgent.clear(); + const restored = saved.map((d) => ({ ...d, streaming: false })); + dialogues.value = restored; + const seatMap = new Map(); + restored.forEach((d) => { + bubbleByAgent.set(d.agentId, d.id); + if (!seatMap.has(d.agentId)) { + seatMap.set(d.agentId, { + agentId: d.agentId, + name: d.sender, + avatarType: d.avatarType, + status: d.confidence === "出错" ? "error" : "done", + bubbleId: d.id, + }); + } + }); + seats.value = [...seatMap.values()]; + } + + function reset() { + cancel(); + pending.clear(); + bubbleByAgent.clear(); + dialogues.value = []; + seats.value = []; + } + + return { dialogues, seats, running, start, cancel, hydrate, reset }; +} diff --git a/parallel-agents-vue/src/config.ts b/parallel-agents-vue/src/config.ts new file mode 100644 index 00000000..c3430a51 --- /dev/null +++ b/parallel-agents-vue/src/config.ts @@ -0,0 +1,16 @@ +/** + * 前端配置:后端接口地址等。嵌入方一般直接改这里的默认值即可;也可在 dev 用 + * Vite 环境变量覆盖(VITE_PA_BASE_URL)。组件 props 传入时优先级最高。 + */ + +/** DeerFlow 网关默认地址(接口都打到这里)。 */ +export const DEFAULT_BASE_URL: string = + (import.meta as unknown as { env?: Record }).env?.VITE_PA_BASE_URL || + "http://localhost:8001"; + +/** 可选默认模型名(不填由后端回退到 config.yaml models[0])。 */ +export const DEFAULT_MODEL: string | undefined = + (import.meta as unknown as { env?: Record }).env?.VITE_PA_MODEL || undefined; + +/** 免密「用户名登录」端点(公共 + CSRF 豁免)。 */ +export const USERNAME_LOGIN_PATH = "/api/v1/auth/login/username"; diff --git a/parallel-agents-vue/src/index.ts b/parallel-agents-vue/src/index.ts new file mode 100644 index 00000000..a42640d7 --- /dev/null +++ b/parallel-agents-vue/src/index.ts @@ -0,0 +1,18 @@ +/** + * 组件库入口。其它 Vue 项目: + * import { ParallelAgentsPanel } from "parallel-agents-vue"; + * import "parallel-agents-vue/style.css"; + * 或全局注册:app.use(ParallelAgentsVue)。 + */ +import "./styles/parallel-agents.css"; +import type { App } from "vue"; +import ParallelAgentsPanel from "./components/ParallelAgentsPanel.vue"; + +export { ParallelAgentsPanel }; +export type { AgentInput, Dialogue, SavedTask, SeatView, StreamEvent } from "./types"; + +export default { + install(app: App) { + app.component("ParallelAgentsPanel", ParallelAgentsPanel); + }, +}; diff --git a/parallel-agents-vue/src/lib/avatar-constants.ts b/parallel-agents-vue/src/lib/avatar-constants.ts new file mode 100644 index 00000000..99c9369a --- /dev/null +++ b/parallel-agents-vue/src/lib/avatar-constants.ts @@ -0,0 +1,27 @@ +/** 头像配色(对齐圆桌 Step2 的 avatarType:blue/purple/emerald/amber/pink/human)。 */ + +export const FALLBACK_AVATAR_TYPES = ["blue", "purple", "emerald", "amber", "pink"] as const; + +/** 无显式 avatarType 时按下标轮转一个配色(与 Step2 getAvatarTypeFor 行为一致)。 */ +export function getAvatarTypeFor(_agentId: string, index: number): string { + return FALLBACK_AVATAR_TYPES[index % FALLBACK_AVATAR_TYPES.length]; +} + +/** 每种 avatarType 在深色面板里的底色 + 主体色(机器人头 / 真人)。 */ +export const AVATAR_COLORS: Record = { + blue: { bg: "rgba(22,119,255,0.18)", fg: "#4096ff" }, + purple: { bg: "rgba(124,58,237,0.20)", fg: "#a78bfa" }, + emerald: { bg: "rgba(16,185,129,0.20)", fg: "#34d399" }, + amber: { bg: "rgba(245,158,11,0.20)", fg: "#fbbf24" }, + pink: { bg: "rgba(219,39,119,0.20)", fg: "#f472b6" }, + human: { bg: "rgba(148,163,184,0.20)", fg: "#cbd5e1" }, + man: { bg: "rgba(148,163,184,0.20)", fg: "#cbd5e1" }, +}; + +export function avatarColor(type: string): { bg: string; fg: string } { + return AVATAR_COLORS[type] ?? AVATAR_COLORS.blue; +} + +export function isHuman(type: string): boolean { + return type === "human" || type === "man"; +} diff --git a/parallel-agents-vue/src/main.ts b/parallel-agents-vue/src/main.ts new file mode 100644 index 00000000..4a05fe35 --- /dev/null +++ b/parallel-agents-vue/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import "./styles/parallel-agents.css"; + +createApp(App).mount("#app"); diff --git a/parallel-agents-vue/src/styles/parallel-agents.css b/parallel-agents-vue/src/styles/parallel-agents.css new file mode 100644 index 00000000..cb90b13f --- /dev/null +++ b/parallel-agents-vue/src/styles/parallel-agents.css @@ -0,0 +1,282 @@ +/* 并行多智能体面板样式。 + * + * 嵌入安全:只引 Tailwind 的 theme + utilities(**不引 preflight**,避免重置宿主全局样式)。 + * 颜色用作用域内的 `--pa-*` 深蓝 token(取自《深蓝 UI 规范》ZS-004 / ZS-002,与圆桌 Step2 + * 深色一致),全部包在根类 `.parallel-agents` 下,绝不污染宿主。布局用 Tailwind 工具类, + * 主题色用下面的 `.pa-*` 语义类。 */ + +@layer theme, base, components, utilities; +@import "tailwindcss/theme.css" layer(theme); +@import "tailwindcss/utilities.css" layer(utilities); + +/* ── 作用域 token(深色深蓝,自带成色,不依赖宿主主题)─────────────────────── */ +.parallel-agents { + --pa-bg: #0c1836; /* 页面背景 zs-004-09 */ + --pa-card: #122347; /* 卡片 / 气泡容器 zs-004-08 */ + --pa-muted: #16294d; /* 次级表面(步骤卡 / 并行列底) */ + --pa-accent: #1a2f5a; /* hover / 软高亮 */ + --pa-fg: #c5cfe0; /* 正文 zs-004-02 */ + --pa-fg-muted: #6e88b0; /* 次级文字 zs-004-04 */ + --pa-border: #2d4473; /* 容器边框 zs-004-06 */ + --pa-input: #4a6491; /* 控件描边 */ + --pa-primary: #1677ff; /* 主操作 zs-002-06 */ + --pa-primary-fg: #ffffff; + --pa-chip-bg: #16335f; /* badge 底 */ + --pa-chip-fg: #4096ff; /* badge 文字 */ + --pa-amber: #f59e0b; + --pa-emerald: #10b981; + --pa-rose: #ef4444; + + color: var(--pa-fg); + background-color: var(--pa-bg); + font-size: 14px; + line-height: 1.6; + box-sizing: border-box; +} +.parallel-agents *, +.parallel-agents *::before, +.parallel-agents *::after { + box-sizing: border-box; +} + +/* 滚动条 */ +.parallel-agents ::-webkit-scrollbar { + width: 8px; + height: 8px; +} +.parallel-agents ::-webkit-scrollbar-thumb { + background: color-mix(in oklch, var(--pa-primary) 40%, var(--pa-border)); + border-radius: 999px; +} +.parallel-agents ::-webkit-scrollbar-thumb:hover { + background: color-mix(in oklch, var(--pa-primary) 60%, var(--pa-border)); +} + +/* ── 语义颜色类(组件用这些上色,布局仍用 Tailwind 工具类)──────────────────── */ +.parallel-agents .pa-bg { + background-color: var(--pa-bg); +} +.parallel-agents .pa-card { + background-color: var(--pa-card); +} +.parallel-agents .pa-surface { + background-color: var(--pa-muted); +} +.parallel-agents .pa-surface-soft { + background-color: color-mix(in oklch, var(--pa-muted) 55%, transparent); +} +.parallel-agents .pa-border { + border-color: var(--pa-border); +} +.parallel-agents .pa-text { + color: var(--pa-fg); +} +.parallel-agents .pa-text-muted { + color: var(--pa-fg-muted); +} +.parallel-agents .pa-text-primary { + color: var(--pa-primary); +} + +/* 输入框 / 文本域 */ +.parallel-agents .pa-input { + background-color: var(--pa-card); + border: 1px solid var(--pa-border); + color: var(--pa-fg); + border-radius: 8px; +} +.parallel-agents .pa-input::placeholder { + color: color-mix(in oklch, var(--pa-fg-muted) 80%, transparent); +} +.parallel-agents .pa-input:focus { + outline: none; + border-color: color-mix(in oklch, var(--pa-primary) 55%, var(--pa-border)); +} + +/* 主按钮 / 次按钮 */ +.parallel-agents .pa-btn-primary { + background-color: var(--pa-primary); + color: var(--pa-primary-fg); + border-radius: 8px; + transition: filter 0.15s ease; +} +.parallel-agents .pa-btn-primary:hover { + filter: brightness(1.08); +} +.parallel-agents .pa-btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.parallel-agents .pa-btn-ghost { + border: 1px solid var(--pa-border); + color: var(--pa-fg); + background-color: transparent; + border-radius: 8px; + transition: background-color 0.15s ease; +} +.parallel-agents .pa-btn-ghost:hover { + background-color: var(--pa-accent); +} +.parallel-agents .pa-btn-ghost:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* 状态徽标("实时输出" / "已完成" …)—— 对齐 Step2 confidence chip */ +.parallel-agents .pa-badge { + background-color: var(--pa-chip-bg); + color: var(--pa-chip-fg); + border: 1px solid var(--pa-border); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 800; + text-transform: uppercase; + border-radius: 4px; + padding: 1px 6px; + font-size: 11px; + letter-spacing: 0.02em; +} + +/* 席位状态角标色 */ +.parallel-agents .pa-dot-running { + background-color: var(--pa-primary); + color: #fff; +} +.parallel-agents .pa-dot-done { + background-color: var(--pa-emerald); + color: #fff; +} +.parallel-agents .pa-dot-error { + background-color: var(--pa-rose); + color: #fff; +} + +/* 流式光标 + loading 点 */ +.parallel-agents .pa-cursor { + display: inline-block; + width: 4px; + height: 13px; + margin-left: 2px; + vertical-align: middle; + background: var(--pa-primary); + animation: pa-pulse 1s ease-in-out infinite; +} +@keyframes pa-pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.25; + } +} +.parallel-agents .pa-bounce { + animation: pa-bounce 0.9s infinite; +} +@keyframes pa-bounce { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-3px); + } +} +@keyframes pa-spin { + to { + transform: rotate(360deg); + } +} +.parallel-agents .pa-spin { + animation: pa-spin 0.8s linear infinite; +} +@keyframes pa-ping { + 75%, + 100% { + transform: scale(1.8); + opacity: 0; + } +} +.parallel-agents .pa-ping { + animation: pa-ping 1.2s cubic-bezier(0, 0, 0.2, 1) infinite; +} + +/* ── Markdown 正文(对齐 Step2 sharedMarkdownComponents 的深色观感)──────────── */ +.parallel-agents .pa-md { + color: var(--pa-fg); + font-size: 14px; + line-height: 1.7; + word-break: break-word; +} +.parallel-agents .pa-md p { + margin: 0.35em 0; +} +.parallel-agents .pa-md h1, +.parallel-agents .pa-md h2, +.parallel-agents .pa-md h3, +.parallel-agents .pa-md h4 { + font-weight: 700; + margin: 0.7em 0 0.35em; + color: var(--pa-fg); +} +.parallel-agents .pa-md h1 { + font-size: 1.3em; +} +.parallel-agents .pa-md h2 { + font-size: 1.18em; +} +.parallel-agents .pa-md h3 { + font-size: 1.06em; +} +.parallel-agents .pa-md ul, +.parallel-agents .pa-md ol { + margin: 0.35em 0; + padding-left: 1.4em; +} +.parallel-agents .pa-md li { + margin: 0.2em 0; +} +.parallel-agents .pa-md a { + color: var(--pa-chip-fg); + text-decoration: underline; +} +.parallel-agents .pa-md code { + background: var(--pa-muted); + border: 1px solid var(--pa-border); + border-radius: 4px; + padding: 0.1em 0.35em; + font-size: 0.9em; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} +.parallel-agents .pa-md pre { + background: var(--pa-muted); + border: 1px solid var(--pa-border); + border-radius: 8px; + padding: 0.75em; + overflow-x: auto; + margin: 0.5em 0; +} +.parallel-agents .pa-md pre code { + background: transparent; + border: 0; + padding: 0; +} +.parallel-agents .pa-md blockquote { + border-left: 3px solid var(--pa-border); + padding-left: 0.75em; + color: var(--pa-fg-muted); + margin: 0.5em 0; +} +.parallel-agents .pa-md table { + border-collapse: collapse; + margin: 0.5em 0; + width: 100%; +} +.parallel-agents .pa-md th, +.parallel-agents .pa-md td { + border: 1px solid var(--pa-border); + padding: 0.35em 0.6em; + text-align: left; +} +.parallel-agents .pa-md th { + background: var(--pa-muted); +} diff --git a/parallel-agents-vue/src/types.ts b/parallel-agents-vue/src/types.ts new file mode 100644 index 00000000..a7c1a5a1 --- /dev/null +++ b/parallel-agents-vue/src/types.ts @@ -0,0 +1,67 @@ +/** + * 并行多智能体面板的共享类型。 + * + * `Dialogue` 镜像圆桌 Step2 的 `Step2Dialogue`,外加 `agentId`(并行场景下用来把消息 + * 归到对应席位列)。`StreamEvent` 是后端 `/api/parallel-agents/run/stream` 的统一事件协议。 + */ + +/** 一条工具调用步骤(喂步骤条 / 思维链卡)。 */ +export interface DialogueStep { + id: string; + key: string; + label: string; + ts: string; + args?: Record | null; +} + +/** 一条消息气泡(对齐 Step2Dialogue + agentId)。 */ +export interface Dialogue { + id: number; + /** 归属席位的 agent_id(并行列定位用)。 */ + agentId: string; + sender: string; + avatarType: string; + content: string; + time: string; + /** 状态标签("实时输出" / "已完成" / "出错" 等)。 */ + confidence: string; + streaming?: boolean; + steps?: DialogueStep[]; +} + +/** 并行席位的视图状态(席位状态条用)。 */ +export interface SeatView { + agentId: string; + name: string; + avatarType: string; + status: "pending" | "running" | "done" | "error"; + /** 关联的 Dialogue.id。 */ + bubbleId: number; +} + +/** 宿主传入的一个待并行调用的智能体。 */ +export interface AgentInput { + agent_id: string; + name: string; + avatarType?: string; +} + +/** 后端合并 SSE 的统一事件。 */ +export type StreamEvent = + | { type: "agent_start"; agent_id: string } + | { type: "delta"; agent_id: string; text: string } + | { type: "step"; agent_id: string; key: string; label: string; args?: Record | null } + | { type: "agent_done"; agent_id: string; content: string } + | { type: "agent_error"; agent_id: string; message: string } + | { type: "all_done" }; + +/** 持久化的任务记录(与后端 /api/parallel-agent-tasks 对应)。 */ +export interface SavedTask { + id: string; + title: string; + brief?: string | null; + agents: AgentInput[]; + messages?: Dialogue[]; + created_at?: string | null; + updated_at?: string | null; +} diff --git a/parallel-agents-vue/src/vite-env.d.ts b/parallel-agents-vue/src/vite-env.d.ts new file mode 100644 index 00000000..fc812394 --- /dev/null +++ b/parallel-agents-vue/src/vite-env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module "*.vue" { + import type { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/parallel-agents-vue/tsconfig.json b/parallel-agents-vue/tsconfig.json new file mode 100644 index 00000000..ed13457f --- /dev/null +++ b/parallel-agents-vue/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["vite/client"], + "baseUrl": ".", + "paths": { "@/*": ["./src/*"] } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"] +} diff --git a/parallel-agents-vue/vite.config.ts b/parallel-agents-vue/vite.config.ts new file mode 100644 index 00000000..1032a296 --- /dev/null +++ b/parallel-agents-vue/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath, URL } from "node:url"; +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import tailwindcss from "@tailwindcss/vite"; + +// 两种构建: +// - 默认(`vite build` / `vite dev`):独立 demo 应用(index.html)。 +// - `--mode lib`(`pnpm build:lib`):组件库,产出 ESM + UMD + 一个 style.css, +// 供其它 Vue 项目 `import { ParallelAgentsPanel } from "parallel-agents-vue"`。 +export default defineConfig(({ mode }) => { + const isLib = mode === "lib"; + return { + plugins: [vue(), tailwindcss()], + resolve: { + alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) }, + }, + server: { port: 5183 }, + build: isLib + ? { + lib: { + entry: fileURLToPath(new URL("./src/index.ts", import.meta.url)), + name: "ParallelAgentsVue", + fileName: "parallel-agents-vue", + }, + rollupOptions: { + // vue 由宿主提供,不打进库。 + external: ["vue"], + output: { + exports: "named", + globals: { vue: "Vue" }, + assetFileNames: "parallel-agents-vue.[ext]", + }, + }, + cssCodeSplit: false, + } + : { + outDir: "dist-app", + }, + }; +});
{{ errorMsg }}
填写任务名称与简介,点「开始分析」并行调用各智能体。
并行运行期间禁止写文件,产出直接呈现在正文。